diff --git a/.gitignore b/.gitignore index a94adba..545d14c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,20 @@ .debug tags _tests .videproject/ctags *swp *~ .kdev4 .cmake-params .ycm_extra_conf.py .ycm_extra_conf.pyc .clang_complete +.clangd +.ccls-cache kactivities.kdev4 compile_commands.json /apidocs GPATH GRTAGS GSYMS GTAGS diff --git a/kded/engine/backends/cryfs/cryfsbackend.cpp b/kded/engine/backends/cryfs/cryfsbackend.cpp index 4b046ce..e81d1b2 100644 --- a/kded/engine/backends/cryfs/cryfsbackend.cpp +++ b/kded/engine/backends/cryfs/cryfsbackend.cpp @@ -1,254 +1,255 @@ /* * Copyright 2017 by Ivan Cukic * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "cryfsbackend.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace AsynQt; namespace PlasmaVault { // see: https://github.com/cryfs/cryfs/blob/develop/src/cryfs/ErrorCodes.h enum class ExitCode : int{ Success = 0, // An error happened that doesn't have an error code associated with it UnspecifiedError = 1, // The command line arguments are invalid. InvalidArguments = 10, // Couldn't load config file. Probably the password is wrong WrongPassword = 11, // Password cannot be empty EmptyPassword = 12, // The file system format is too new for this CryFS version. Please update your CryFS version. TooNewFilesystemFormat = 13, // The file system format is too old for this CryFS version. Run with --allow-filesystem-upgrade to upgrade it. TooOldFilesystemFormat = 14, // The file system uses a different cipher than the one specified on the command line using the --cipher argument. WrongCipher = 15, // Base directory doesn't exist or is inaccessible (i.e. not read or writable or not a directory) InaccessibleBaseDir = 16, // Mount directory doesn't exist or is inaccessible (i.e. not read or writable or not a directory) InaccessibleMountDir = 17, // Base directory can't be a subdirectory of the mount directory BaseDirInsideMountDir = 18, // Something's wrong with the file system. InvalidFilesystem = 19, }; CryFsBackend::CryFsBackend() { } CryFsBackend::~CryFsBackend() { } Backend::Ptr CryFsBackend::instance() { return singleton::instance(); } FutureResult<> CryFsBackend::mount(const Device &device, const MountPoint &mountPoint, const Vault::Payload &payload) { QDir dir; const auto password = payload[KEY_PASSWORD].toString(); const auto cypher = payload["cryfs-cipher"].toString(); const auto shouldUpgrade = payload["cryfs-fs-upgrade"].toBool(); if (!dir.mkpath(device.data()) || !dir.mkpath(mountPoint.data())) { return errorResult(Error::BackendError, i18n("Failed to create directories, check your permissions")); } auto process = // Cypher is specified, use it to create the device (!cypher.isEmpty()) ? cryfs({ "--cipher", cypher, device.data(), // source directory to initialize cryfs in mountPoint.data() // where to mount the file system }) // Cypher is not specified, use the default, whatever it is :shouldUpgrade ? cryfs({ device.data(), // source directory to initialize cryfs in mountPoint.data(), // where to mount the file system "--allow-filesystem-upgrade" }) : cryfs({ device.data(), // source directory to initialize cryfs in mountPoint.data() // where to mount the file system }) ; auto result = makeFuture(process, [this, device, mountPoint, payload] (QProcess *process) { const auto out = process->readAllStandardOutput(); const auto err = process->readAllStandardError(); qDebug() << "OUT: " << out; qDebug() << "ERR: " << err; const auto exitCode = (ExitCode) process->exitCode(); auto upgradeFileSystem = [this, device, mountPoint, payload] { const auto upgrade = QMessageBox::Yes == QMessageBox::question( nullptr, i18n("Upgrade the vault?"), i18n("This vault was created with an older version of cryfs and needs to be upgraded.\n\nMind that this process is irreversible and the vault will no longer work with older versions of cryfs.\n\nDo you want to perform the upgrade now?")); if (!upgrade) { return Result<>::error(Error::BackendError, i18n("The vault needs to be upgraded before it can be opened with this version of cryfs")); } auto new_payload = payload; new_payload["cryfs-fs-upgrade"] = true; return AsynQt::await(mount(device, mountPoint, new_payload)); }; return err.contains("'nonempty'") ? Result<>::error(Error::CommandError, i18n("The mount point directory is not empty, refusing to open the vault")) : // If all went well, just return success (process->exitStatus() == QProcess::NormalExit && exitCode == ExitCode::Success) ? Result<>::success() : // If we tried to mount into a non-empty location, report exitCode == ExitCode::WrongPassword ? Result<>::error(Error::BackendError, i18n("You entered the wrong password")) : exitCode == ExitCode::TooNewFilesystemFormat ? Result<>::error(Error::BackendError, i18n("The installed version of cryfs is too old to open this vault.")) : exitCode == ExitCode::TooOldFilesystemFormat ? upgradeFileSystem() : // otherwise just report that we failed Result<>::error(Error::CommandError, - i18n("Unable to perform the operation (error code %1).", QString::number((int)exitCode))); + i18n("Unable to perform the operation (error code %1).", QString::number((int)exitCode)), + out, err); }); // Writing the password process->write(password.toUtf8()); process->write("\n"); return result; } FutureResult<> CryFsBackend::validateBackend() { using namespace AsynQt::operators; // We need to check whether all the commands are installed // and whether the user has permissions to run them return collect(checkVersion(cryfs({ "--version" }), std::make_tuple(0, 9, 9)), checkVersion(fusermount({ "--version" }), std::make_tuple(2, 9, 7))) | transform([this] (const QPair &cryfs, const QPair &fusermount) { bool success = cryfs.first && fusermount.first; QString message = formatMessageLine("cryfs", cryfs) + formatMessageLine("fusermount", fusermount); return success ? Result<>::success() : Result<>::error(Error::BackendError, message); }); } bool CryFsBackend::isInitialized(const Device &device) const { QFile cryFsConfig(device.data() + QStringLiteral("/cryfs.config")); return cryFsConfig.exists(); } QProcess *CryFsBackend::cryfs(const QStringList &arguments) const { auto config = KSharedConfig::openConfig(PLASMAVAULT_CONFIG_FILE); KConfigGroup backendConfig(config, "CryfsBackend"); return process("cryfs", arguments + backendConfig.readEntry("extraMountOptions", QStringList{}), { { "CRYFS_FRONTEND", "noninteractive" } }); } } // namespace PlasmaVault diff --git a/kded/engine/commandresult.cpp b/kded/engine/commandresult.cpp index fe3dc51..9b53541 100644 --- a/kded/engine/commandresult.cpp +++ b/kded/engine/commandresult.cpp @@ -1,42 +1,54 @@ /* * Copyright 2017 by Ivan Cukic * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "commandresult.h" namespace PlasmaVault { -Error::Error(Code code, const QString &message) +Error::Error(Code code, const QString &message, const QString &out, const QString &err) : m_code(code) , m_message(message) + , m_out(out) + , m_err(err) { } Error::Code Error::code() const { return m_code; } QString Error::message() const { return m_message; } +QString Error::out() const +{ + return m_out; +} + +QString Error::err() const +{ + return m_err; +} + } // namespace PlasmaVault diff --git a/kded/engine/commandresult.h b/kded/engine/commandresult.h index 297872b..ddb097c 100644 --- a/kded/engine/commandresult.h +++ b/kded/engine/commandresult.h @@ -1,72 +1,77 @@ /* * Copyright 2017 by Ivan Cukic * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef PLASMAVAULT_KDED_ENGINE_COMMAND_RESULT_H #define PLASMAVAULT_KDED_ENGINE_COMMAND_RESULT_H #include #include "asynqt/basic/all.h" #include "asynqt/utils/expected.h" namespace PlasmaVault { class Error { public: enum Code { MountPointError, DeviceError, BackendError, CommandError, - DeletionError + DeletionError, + UnknownError }; - Error(Code code, const QString &message = QString()); + Error(Code code = UnknownError, const QString &message = {}, const QString &out = {}, const QString &err = {}); Code code() const; QString message() const; + QString out() const; + QString err() const; private: Code m_code; QString m_message; + QString m_out; + QString m_err; }; template using Result = AsynQt::Expected; template using FutureResult = QFuture>; inline -FutureResult<> errorResult(Error::Code error, const QString &message) +FutureResult<> errorResult(Error::Code error, const QString &message, const QString &out = {}, const QString &err = {}) { qWarning() << message; - return makeReadyFuture(Result<>::error(error, message)); + return makeReadyFuture(Result<>::error(error, message, out, err)); } } // namespace PlasmaVault #endif // include guard diff --git a/kded/engine/fusebackend_p.cpp b/kded/engine/fusebackend_p.cpp index 919b1cd..4e5fd77 100644 --- a/kded/engine/fusebackend_p.cpp +++ b/kded/engine/fusebackend_p.cpp @@ -1,282 +1,283 @@ /* * Copyright 2017 by Ivan Cukic * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "fusebackend_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "singleton_p.h" using namespace AsynQt; namespace PlasmaVault { Result<> FuseBackend::hasProcessFinishedSuccessfully(QProcess *process) { const auto out = process->readAllStandardOutput(); const auto err = process->readAllStandardError(); return // If all went well, just return success (process->exitStatus() == QProcess::NormalExit && process->exitCode() == 0) ? Result<>::success() : // If we tried to mount into a non-empty location, report err.contains("'nonempty'") ? Result<>::error(Error::CommandError, i18n("The mount point directory is not empty, refusing to open the vault")) : // If we have a message for the user, report it // !out.isEmpty() ? // Result<>::error(Error::CommandError, // out) : // otherwise just report that we failed Result<>::error(Error::CommandError, - i18n("Unable to perform the operation")); + i18n("Unable to perform the operation"), + out, err); } FuseBackend::FuseBackend() { } FuseBackend::~FuseBackend() { } QProcess *FuseBackend::process(const QString &executable, const QStringList &arguments, const QHash &environment) const { auto result = new QProcess(); result->setProgram(executable); result->setArguments(arguments); if (environment.count() > 0) { auto env = result->processEnvironment(); for (const auto& key: environment.keys()) { env.insert(key, environment[key]); } result->setProcessEnvironment(env); } return result; } QProcess *FuseBackend::fusermount(const QStringList &arguments) const { return process("fusermount", arguments, {}); } FutureResult<> FuseBackend::initialize(const QString &name, const Device &device, const MountPoint &mountPoint, const Vault::Payload &payload) { Q_UNUSED(name); return isInitialized(device) ? errorResult(Error::BackendError, i18n("This directory already contains encrypted data")) : directoryExists(device.data()) || directoryExists(mountPoint.data()) ? errorResult(Error::BackendError, i18n("You need to select empty directories for the encrypted storage and for the mount point")) : // otherwise mount(device, mountPoint, payload); } FutureResult<> FuseBackend::import(const QString &name, const Device &device, const MountPoint &mountPoint, const Vault::Payload &payload) { Q_UNUSED(name); return !isInitialized(device) ? errorResult(Error::BackendError, i18n("This directory doesn't contain encrypted data")) : !directoryExists(device.data()) || directoryExists(mountPoint.data()) ? errorResult(Error::BackendError, i18n("You need to select an empty directory for the mount point")) : // otherwise mount(device, mountPoint, payload); } FutureResult<> FuseBackend::open(const Device &device, const MountPoint &mountPoint, const Vault::Payload &payload) { return isOpened(mountPoint) ? errorResult(Error::BackendError, i18n("Device is already open")) : // otherwise mount(device, mountPoint, payload); } FutureResult<> FuseBackend::close(const Device &device, const MountPoint &mountPoint) { Q_UNUSED(device); return !isOpened(mountPoint) ? errorResult(Error::BackendError, i18n("Device is not open")) : // otherwise makeFuture(fusermount({ "-u", mountPoint.data() }), hasProcessFinishedSuccessfully); } FutureResult<> FuseBackend::dismantle(const Device &device, const MountPoint &mountPoint, const Vault::Payload &payload) { // TODO: // mount // unmount // remove the directories // return Fuse::dismantle(device, mountPoint, password); Q_UNUSED(payload) // Removing the data and the mount point return transform(makeFuture( KIO::del( { QUrl::fromLocalFile(device.data()) , QUrl::fromLocalFile(mountPoint.data()) } )), [] (KJob *job) { job->deleteLater(); return job->error() == 0 ? Result<>::success() : Result<>::error(Error::DeletionError, job->errorString()); } ); } QFuture> FuseBackend::checkVersion( QProcess *process, const std::tuple &requiredVersion) const { using namespace AsynQt::operators; return makeFuture(process, [=] (QProcess *process) { if (process->exitStatus() != QProcess::NormalExit) { return qMakePair( false, i18n("Failed to execute")); } QRegularExpression versionMatcher("([0-9]+)[.]([0-9]+)[.]([0-9]+)"); const auto out = process->readAllStandardOutput(); const auto err = process->readAllStandardError(); const auto all = out + err; const auto matches = versionMatcher.match(all); if (!matches.hasMatch()) { return qMakePair( false, i18n("Unable to detect the version")); } const auto matchedVersion = std::make_tuple(matches.captured(1).toInt(), matches.captured(2).toInt(), matches.captured(3).toInt()); if (matchedVersion < requiredVersion) { // Bad version, we need to notify the world return qMakePair( false, i18n("Wrong version installed. The required version is %1.%2.%3", std::get<0>(requiredVersion), std::get<1>(requiredVersion), std::get<2>(requiredVersion) )); } return qMakePair( true, i18n("Correct version found")); }); } bool FuseBackend::isOpened(const MountPoint &mountPoint) const { // warning: KMountPoint depends on /etc/mtab according to the documentation. KMountPoint::Ptr ptr = KMountPoint::currentMountPoints().findByPath(mountPoint.data()); // we can not rely on ptr->realDeviceName() since it is empty, // KMountPoint can not get the source return ptr && ptr->mountPoint() == mountPoint.data(); } } // namespace PlasmaVault diff --git a/kded/ui/mountdialog.cpp b/kded/ui/mountdialog.cpp index 5cb81e3..e5582ce 100644 --- a/kded/ui/mountdialog.cpp +++ b/kded/ui/mountdialog.cpp @@ -1,76 +1,107 @@ /* * Copyright 2017 by Kees vd Broek * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "mountdialog.h" -#include "engine/vault.h" #include #include #include +#include +#include #include MountDialog::MountDialog(PlasmaVault::Vault *vault) : m_vault(vault) { m_ui.setupUi(this); m_errorLabel = new KMessageWidget(this); m_errorLabel->setMessageType(KMessageWidget::Error); m_errorLabel->setCloseButtonVisible(false); m_errorLabel->setIcon(QIcon::fromTheme("dialog-error")); m_errorLabel->setVisible(false); + m_detailsAction = new QAction(this); + m_detailsAction->setToolTip(i18n("Details...")); + m_detailsAction->setIcon(QIcon::fromTheme("view-list-details")); + + connect(m_detailsAction, &QAction::triggered, + this, [this] { + QString message; + const auto out = m_lastError.out().trimmed(); + const auto err = m_lastError.err().trimmed(); + + if (!out.isEmpty() && !err.isEmpty()) { + message = i18n("Command output:\n%1\n\nError output: %2", m_lastError.out(), m_lastError.err()); + } else { + message = out + err; + } + + auto messageBox = new QMessageBox(QMessageBox::Critical, i18n("Error details"), message, QMessageBox::Ok, this); + messageBox->setAttribute(Qt::WA_DeleteOnClose); + messageBox->show(); + + }); + auto errorLabelSizePolicy = m_errorLabel->sizePolicy(); errorLabelSizePolicy.setHorizontalPolicy(QSizePolicy::Expanding); m_errorLabel->setSizePolicy(errorLabelSizePolicy); m_errorLabel->setVisible(false); m_ui.formLayout->addRow(QString(), m_errorLabel); m_ui.vaultName->setText(vault->name()); QStyleOption option; option.initFrom(this); const int iconSize = style()->pixelMetric(QStyle::PM_MessageBoxIconSize, &option, this); m_ui.icon->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-password")).pixmap(iconSize)); } void MountDialog::accept() { setCursor(Qt::WaitCursor); m_errorLabel->setVisible(false); setEnabled(false); m_ui.password->lineEdit()->setCursor(Qt::WaitCursor); QString pwd = m_ui.password->password(); auto future = m_vault->open({ { KEY_PASSWORD, pwd } }); const auto result = AsynQt::await(future); unsetCursor(); setEnabled(true); m_ui.password->lineEdit()->unsetCursor(); if (result) { QDialog::accept(); } else { - qDebug() << "We've got an error" << result.error().message(); - // m_ui.errorLabel->setText(i18n("Failed to open: %1").arg(result.error().message())); - m_errorLabel->setText(i18n("Failed to open: %1", result.error().message())); + m_lastError = result.error(); + + m_errorLabel->setText(i18n("Failed to open: %1", m_lastError.message())); m_errorLabel->setVisible(true); + + if (!m_lastError.out().isEmpty() || !m_lastError.err().isEmpty()) { + m_errorLabel->addAction(m_detailsAction); + + } else { + m_errorLabel->removeAction(m_detailsAction); + + } } } diff --git a/kded/ui/mountdialog.h b/kded/ui/mountdialog.h index e050a9f..e86a1ec 100644 --- a/kded/ui/mountdialog.h +++ b/kded/ui/mountdialog.h @@ -1,48 +1,53 @@ /* * Copyright 2017 by Kees vd Broek * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef MOUNTDIALOG_H #define MOUNTDIALOG_H #include #include #include "ui_mountdialog.h" +#include "engine/vault.h" + class KMessageWidget; +class QAction; namespace PlasmaVault { class Vault; } class MountDialog : public QDialog { public: MountDialog(PlasmaVault::Vault *vault); protected: void accept() override; private: PlasmaVault::Vault *m_vault; Ui_MountDialog m_ui; KMessageWidget* m_errorLabel; + QAction* m_detailsAction; + PlasmaVault::Error m_lastError; }; #endif