File indexing completed on 2024-06-23 05:30:39

0001 /*
0002  *   SPDX-FileCopyrightText: 2020 Martino Pilia <martino.pilia (at) gmail.com>
0003  *
0004  *   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 #include "gocryptfsbackend.h"
0008 
0009 #include <QDir>
0010 #include <QProcess>
0011 
0012 #include <KConfigGroup>
0013 #include <KLocalizedString>
0014 #include <KMountPoint>
0015 #include <KSharedConfig>
0016 
0017 #include <algorithm>
0018 
0019 #include <asynqt/basic/all.h>
0020 #include <asynqt/operations/collect.h>
0021 #include <asynqt/operations/transform.h>
0022 #include <asynqt/wrappers/process.h>
0023 
0024 #include <qregularexpression.h>
0025 #include <singleton_p.h>
0026 
0027 using namespace AsynQt;
0028 
0029 namespace PlasmaVault
0030 {
0031 // See `man gocryptfs`, section EXIT CODES.
0032 enum class ExitCode : int {
0033     Success = 0,
0034 
0035     // CIPHERDIR is not an emtpy directory (on "-init")
0036     NonEmptyCipherDir = 6,
0037 
0038     // MOUNTPOINT is not an empty directory
0039     NonEmptyMountPoint = 10,
0040 
0041     // Password incorrect
0042     WrongPassword = 12,
0043 
0044     // Password is empty (on "-init")
0045     EmptyPassword = 22,
0046 
0047     // Could not read gocryptfs.conf
0048     CannotReadConfig = 23,
0049 
0050     // Could not write gocryptfs.conf (on "-init" or "-password")
0051     CannotWriteConfig = 24,
0052 
0053     // fsck found errors
0054     FsckError = 26,
0055 };
0056 
0057 GocryptfsBackend::GocryptfsBackend()
0058 {
0059 }
0060 
0061 GocryptfsBackend::~GocryptfsBackend()
0062 {
0063 }
0064 
0065 Backend::Ptr GocryptfsBackend::instance()
0066 {
0067     return singleton::instance<GocryptfsBackend>();
0068 }
0069 
0070 FutureResult<> GocryptfsBackend::mount(const Device &device, const MountPoint &mountPoint, const Vault::Payload &payload)
0071 {
0072     QDir dir;
0073 
0074     const auto password = payload[KEY_PASSWORD].toString();
0075 
0076     if (!dir.mkpath(device.data()) || !dir.mkpath(mountPoint.data())) {
0077         return errorResult(Error::BackendError, i18n("Failed to create directories, check your permissions"));
0078     }
0079     removeDotDirectory(mountPoint);
0080 
0081     if (isInitialized(device)) {
0082         auto mountProcess = gocryptfs({
0083             device.data(), // cypher data directory
0084             mountPoint.data() // mount point
0085         });
0086 
0087         auto mountResult = makeFuture(mountProcess, hasProcessFinishedSuccessfully);
0088 
0089         // Write password
0090         mountProcess->write(password.toUtf8() + "\n");
0091 
0092         return mountResult;
0093     } else {
0094         // Initialise cipherdir
0095         auto initProcess = gocryptfs({
0096             "-init",
0097             device.data(),
0098         });
0099 
0100         auto initResult = makeFuture(initProcess, [this, device, mountPoint, payload](QProcess *process) {
0101             auto const exitCode = static_cast<ExitCode>(process->exitCode());
0102 
0103             switch (exitCode) {
0104             case ExitCode::Success:
0105                 return AsynQt::await(mount(device, mountPoint, payload));
0106 
0107             case ExitCode::NonEmptyCipherDir:
0108                 return Result<>::error(Error::BackendError, i18n("The cipher directory is not empty, cannot initialise the vault."));
0109 
0110             case ExitCode::EmptyPassword:
0111                 return Result<>::error(Error::BackendError, i18n("The password is empty, cannot initialise the vault."));
0112 
0113             case ExitCode::CannotWriteConfig:
0114                 return Result<>::error(Error::BackendError, i18n("Cannot write gocryptfs.conf inside cipher directory, check your permissions."));
0115 
0116             default:
0117                 return Result<>::error(Error::CommandError,
0118                                        i18n("Unable to perform the operation (error code %1).", QString::number((int)exitCode)),
0119                                        process->readAllStandardOutput(),
0120                                        process->readAllStandardError());
0121             }
0122         });
0123 
0124         // Write password twice (insert and confirm)
0125         for (int i = 0; i < 2; ++i) {
0126             initProcess->write(password.toUtf8() + "\n");
0127         }
0128 
0129         return initResult;
0130     }
0131 }
0132 
0133 FutureResult<> GocryptfsBackend::validateBackend()
0134 {
0135     using namespace AsynQt::operators;
0136 
0137     auto customCheckVersion = [](QProcess *process, const std::tuple<int, int> &requiredVersion) {
0138         using namespace AsynQt::operators;
0139 
0140         return makeFuture(process, [=](QProcess *process) {
0141             if (process->exitStatus() != QProcess::NormalExit) {
0142                 return qMakePair(false, i18n("Failed to execute"));
0143             }
0144 
0145             // We don't care about the minor version for gocryptfs
0146             QRegularExpression versionMatcher("([0-9]+)[.]([0-9]+)");
0147 
0148             const auto out = process->readAllStandardOutput();
0149             const auto err = process->readAllStandardError();
0150 
0151             if (out.isEmpty() && err.isEmpty()) {
0152                 return qMakePair(false, i18n("Unable to detect the version"));
0153             }
0154 
0155             // gocryptfs prints out several versions separated by semicolons
0156             // -- of itself, of go-fuse and of go
0157             // We just need the first
0158             const auto gocryptfsVersionString = (out + err).split(';').at(0);
0159 
0160             if (!gocryptfsVersionString.startsWith("gocryptfs")) {
0161                 return qMakePair(false, i18n("Unable to detect the version, the version string is invalid"));
0162             }
0163 
0164             const auto matches = versionMatcher.match(gocryptfsVersionString);
0165 
0166             if (!matches.hasMatch()) {
0167                 return qMakePair(false, i18n("Unable to detect the version"));
0168             }
0169 
0170             const auto matchedVersion = std::make_tuple(matches.captured(1).toInt(), matches.captured(2).toInt());
0171 
0172             if (matchedVersion < requiredVersion) {
0173                 // Bad version, we need to notify the world
0174                 return qMakePair(false,
0175                                  i18n("Wrong version installed. The required version is %1.%2", std::get<0>(requiredVersion), std::get<1>(requiredVersion)));
0176             }
0177 
0178             return qMakePair(true, i18n("Correct version found"));
0179         });
0180     };
0181 
0182     // We need to check whether all the commands are installed
0183     // and whether the user has permissions to run them
0184     return collect(customCheckVersion(gocryptfs({"--version"}), std::make_tuple(1, 8)), checkVersion(fusermount({"--version"}), std::make_tuple(2, 9, 7)))
0185 
0186         | transform([this](const QPair<bool, QString> &gocryptfs, const QPair<bool, QString> &fusermount) {
0187                bool success = gocryptfs.first && fusermount.first;
0188                QString message = formatMessageLine("gocryptfs", gocryptfs) + formatMessageLine("fusermount", fusermount);
0189 
0190                return success ? Result<>::success() : Result<>::error(Error::BackendError, message);
0191            });
0192 }
0193 
0194 bool GocryptfsBackend::isInitialized(const Device &device) const
0195 {
0196     QFile gocryptfsConfig(getConfigFilePath(device));
0197     return gocryptfsConfig.exists();
0198 }
0199 
0200 QProcess *GocryptfsBackend::gocryptfs(const QStringList &arguments) const
0201 {
0202     auto config = KSharedConfig::openConfig(PLASMAVAULT_CONFIG_FILE);
0203     KConfigGroup backendConfig(config, "GocryptfsBackend");
0204 
0205     return process("gocryptfs", arguments + backendConfig.readEntry("extraMountOptions", QStringList{}), {});
0206 }
0207 
0208 QString GocryptfsBackend::getConfigFilePath(const Device &device) const
0209 {
0210     return device.data() + QStringLiteral("/gocryptfs.conf");
0211 }
0212 
0213 } // namespace PlasmaVault