sailfish-safe

Sailfish frontend for safe(1)
git clone git://git.z3bra.org/sailfish-safe.git
Log | Files | Refs | README | LICENSE

commit f1b5077fd73c21f61dbd6a5bfc5fb93bc5be99ec
parent b76b96edbea9bea1bbeee6db3fbf0e99277ecfd9
Author: Daniel Vrátil <daniel.vratil@avast.com>
Date:   Mon, 19 Apr 2021 20:26:41 +0200

Create a wrapper for GPG operations

Diffstat:
Mharbour-passilic.pro | 4++++
Asrc/gpg.cpp | 243+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/gpg.h | 123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/passwordprovider.cpp | 122++++++++++++++++++++++---------------------------------------------------------
Msrc/passwordprovider.h | 11-----------
5 files changed, 403 insertions(+), 100 deletions(-)

diff --git a/harbour-passilic.pro b/harbour-passilic.pro @@ -2,6 +2,8 @@ TARGET = harbour-passilic CONFIG += sailfishapp +QT += concurrent + DEFINES += \ QT_NO_CAST_FROM_ASCII \ QT_NO_CAST_TO_ASCII \ @@ -15,6 +17,7 @@ DEFINES += \ INCLUDEPATH += 3rdparty/kitemmodels/ SOURCES += \ + src/gpg.cpp \ src/main.cpp \ src/abbreviations.cpp \ src/imageprovider.cpp \ @@ -29,6 +32,7 @@ SOURCES += \ HEADERS += \ src/abbreviations.h \ + src/gpg.h \ src/imageprovider.h \ src/passwordfiltermodel.h \ src/passwordprovider.h \ diff --git a/src/gpg.cpp b/src/gpg.cpp @@ -0,0 +1,243 @@ +#include "gpg.h" + +#include <QStandardPaths> +#include <QProcess> +#include <QIODevice> +#include <QRegularExpression> +#include <QRegularExpressionMatch> +#include <QTimer> +#include <QtConcurrent> +#include <QFutureWatcher> + +namespace { + +struct GpgExecutable { + GpgExecutable(const QString &path, int major, int minor) + : path(path), major_version(major), minor_version(minor) + {} + QString path = {}; + int major_version = 0; + int minor_version = 0; +}; + +GpgExecutable findGpgExecutable() +{ + auto gpgExe = QStandardPaths::findExecutable(QStringLiteral("gpg2")); + if (gpgExe.isEmpty()) { + gpgExe = QStandardPaths::findExecutable(QStringLiteral("gpg")); + } + + QProcess process; + process.start(gpgExe, {QStringLiteral("--version")}, QIODevice::ReadOnly); + process.waitForFinished(); + const auto line = process.readLine(); + static const QRegularExpression rex(QStringLiteral("([0-9]+).([0-9]+).([0-9]+)")); + const auto match = rex.match(QString::fromUtf8(line)); + + return {gpgExe, match.captured(1).toInt(), match.captured(2).toInt()}; +} + +} // namespace + +Gpg::GetKeyTrustTask *Gpg::getKeyTrust(const Key &key) +{ + return new GetKeyTrustTask(key); +} + +Gpg::UpdateKeyTrustTask *Gpg::updateKeyTrust(const Key &key, Key::Trust trust) +{ + return new UpdateKeyTrustTask(key, trust); +} + +Gpg::EncryptTask *Gpg::encrypt(const QString &file, const Key &key, const QString &content) +{ + return new EncryptTask(file, key, content); +} + +Gpg::DecryptTask *Gpg::decrypt(const QString &file, const Key &key, const QString &passphrase) +{ + return new DecryptTask(file, key, passphrase); +} + + +Gpg::Task::Task(QObject *parent) + : QObject(parent) +{ + QTimer::singleShot(0, this, &Task::start); +} + +bool Gpg::Task::error() const +{ + return !mError.isNull(); +} + +QString Gpg::Task::errorString() const +{ + return mError; +} + +void Gpg::Task::setError(const QString &error) +{ + mError = error; +} + +void Gpg::Task::start() +{ + qDebug() << "Starting task" << this; + auto future = QtConcurrent::run(this, &Task::run); + auto *watcher = new QFutureWatcher<void>; + connect(watcher, &QFutureWatcher<void>::finished, watcher, &QObject::deleteLater); + connect(watcher, &QFutureWatcher<void>::finished, this, &Gpg::Task::finished); + connect(watcher, &QFutureWatcher<void>::finished, this, &QObject::deleteLater); + watcher->setFuture(future); +} + +Gpg::GetKeyTrustTask::GetKeyTrustTask(const Key &key) + : mKey(key) +{} + +Gpg::Key::Trust Gpg::GetKeyTrustTask::trust() const +{ + return mTrust; +} + +void Gpg::GetKeyTrustTask::run() +{ + const auto gpg = findGpgExecutable(); + QProcess process; + process.setProgram(gpg.path); + process.setArguments({QStringLiteral("--list-key \"%1\"").arg(mKey.id), QStringLiteral("--with-colons")}); + process.start(QIODevice::ReadOnly); + process.waitForFinished(); + while (!process.atEnd()) { + const auto line = process.readLine(); + const auto cols = line.split(':'); + if (cols.size() < 8) { + continue; + } + if (cols[0] == "uid") { + if (cols[1].isEmpty()) { + mTrust = Key::Trust::Unknown; + } + switch (cols[1][0]) { + case 'u': + mTrust = Key::Trust::Ultimate; + break; + case 'f': + mTrust = Key::Trust::Full; + break; + case 'm': + mTrust = Key::Trust::Marginal; + break; + case 'n': + mTrust = Key::Trust::Never; + break; + case '-': + default: + mTrust = Key::Trust::Unknown; + break; + } + break; + } + } +} + +Gpg::UpdateKeyTrustTask::UpdateKeyTrustTask(const Key &key, Key::Trust trust) + : Task() + , mKey(key) + , mTrust(trust) +{} + +void Gpg::UpdateKeyTrustTask::run() +{ + const auto gpg = findGpgExecutable(); + QProcess process; + process.setProgram(gpg.path); + process.setArguments({QStringLiteral("--command-fd=1"), + QStringLiteral("--status-fd=1"), + QStringLiteral("--batch"), + QStringLiteral("--edit-key"), + mKey.id, + QStringLiteral("trust")}); + process.start(); + process.waitForStarted(); + while (process.state() == QProcess::Running) { + process.waitForReadyRead(); + const auto line = process.readLine(); + if (line == "[GNUPG:] GET_LINE edit_ownertrust.value\n") { + process.write(QByteArray::number(static_cast<int>(mTrust)) + "\n"); + process.closeWriteChannel(); + break; + } + } + + process.waitForFinished(); +} + +Gpg::EncryptTask::EncryptTask(const QString &file, const Key &key, const QString &content) + : mFile(file), mKey(key), mContent(content) +{} + +void Gpg::EncryptTask::run() +{ + const auto gpg = findGpgExecutable(); + QProcess process; + process.setProgram(gpg.path); + process.setArguments({QStringLiteral("--quiet"), + QStringLiteral("--status-fd=1"), + QStringLiteral("--command-fd=1"), + QStringLiteral("--batch"), + QStringLiteral("--encrypt"), + QStringLiteral("--no-encrypt-to"), + QStringLiteral("-r %1").arg(mKey.id), + QStringLiteral("-o%1").arg(mFile)}); + process.start(); + process.waitForStarted(); + process.write(mContent.toUtf8()); + process.closeWriteChannel(); + process.waitForFinished(); + if (process.exitCode() != 0) { + const auto err = process.readAllStandardError(); + qWarning() << "Failed to encrypt data:" << err; + setError(QString::fromUtf8(err)); + } +} + +Gpg::DecryptTask::DecryptTask(const QString &file, const Key &key, const QString &passphrase) + : Task(), mFile(file), mPassphrase(passphrase), mKey(key) +{} + +QString Gpg::DecryptTask::content() const +{ + return mContent; +} + +void Gpg::DecryptTask::run() +{ + const auto gpg = findGpgExecutable(); + QProcess process; + process.setProgram(gpg.path); + process.setArguments({QStringLiteral("--quiet"), + QStringLiteral("--batch"), + QStringLiteral("--decrypt"), + QStringLiteral("--no-tty"), + QStringLiteral("--command-fd=1"), + QStringLiteral("--no-encrypt-to"), + QStringLiteral("--compress-algo=none"), + QStringLiteral("--passphrase-fd=0"), + QStringLiteral("--pinentry-mode=loopback"), + QStringLiteral("-r %1").arg(mKey.id), + mFile}); + process.start(); + process.waitForStarted(); + process.write(mPassphrase.toUtf8()); + process.closeWriteChannel(); + process.waitForFinished(); + if (process.exitCode() != 0) { + const auto err = process.readAllStandardError(); + qWarning() << "Failed to decrypt data:" << err; + setError(QString::fromUtf8(err)); + } else { + mContent = QString::fromUtf8(process.readAllStandardOutput()); + } +} diff --git a/src/gpg.h b/src/gpg.h @@ -0,0 +1,123 @@ +#ifndef GPG_H +#define GPG_H + +#include <QObject> +#include <QVector> + +namespace Gpg +{ +class ListKeysTask; +class FindKeyTask; +class GetKeyTrustTask; +class UpdateKeyTrustTask; +class DecryptTask; +class EncryptTask; + +struct Key { + enum class Trust { + Unknown = 1, + Never = 2, + Marginal = 3, + Full = 4, + Ultimate = 5 + }; + + QString id; +}; + +GetKeyTrustTask *getKeyTrust(const Key &key); + +UpdateKeyTrustTask *updateKeyTrust(const Key &key, Key::Trust trust); + +DecryptTask *decrypt(const QString &file, const Key &key, const QString &passphrase); + +EncryptTask *encrypt(const QString &data, const Key &key, const QString &file); + + +class Task : public QObject { + Q_OBJECT +public: + bool error() const; + QString errorString() const; + +Q_SIGNALS: + void finished(); + +protected: + explicit Task(QObject *parent = nullptr); + + virtual void run() = 0; + + void setError(const QString &error); + +private Q_SLOTS: + void start(); + +private: + QString mError; +}; + +class GetKeyTrustTask : public Task { + Q_OBJECT + friend GetKeyTrustTask *Gpg::getKeyTrust(const Key &); +public: + Gpg::Key::Trust trust() const; + +protected: + void run() override; + +private: + explicit GetKeyTrustTask(const Key &key); + + Key mKey; + Key::Trust mTrust = Key::Trust::Never; +}; + +class UpdateKeyTrustTask : public Task { + Q_OBJECT + friend UpdateKeyTrustTask *Gpg::updateKeyTrust(const Key &, Key::Trust); +protected: + void run() override; + +private: + UpdateKeyTrustTask(const Gpg::Key &key, Gpg::Key::Trust trust); + + Key mKey; + Key::Trust mTrust = Key::Trust::Never; +}; + +class DecryptTask : public Task { + Q_OBJECT + friend DecryptTask *Gpg::decrypt(const QString &, const Key &, const QString &); +public: + QString content() const; + +protected: + void run() override; + +private: + DecryptTask(const QString &file, const Key &key, const QString &passphrase); + + QString mFile; + QString mPassphrase; + Key mKey; + QString mContent; +}; + +class EncryptTask : public Task { + Q_OBJECT + friend EncryptTask *Gpg::encrypt(const QString &, const Key &, const QString &); +protected: + void run() override; + +private: + EncryptTask(const QString &file, const Key &key, const QString &content); + + QString mFile; + Key mKey; + QString mContent; +}; + +} // namespace Gpg + +#endif // GPG_H diff --git a/src/passwordprovider.cpp b/src/passwordprovider.cpp @@ -19,17 +19,20 @@ #include "passwordprovider.h" #include "settings.h" +#include "gpg.h" -#include <QProcess> -#include <QStandardPaths> #include <QClipboard> #include <QGuiApplication> -#include <QRegularExpression> +#include <QDir> +#include <QDebug> namespace { static const auto PasswordTimeoutUpdateInterval = 100; + +#define PASSWORD_STORE_DIR "PASSWORD_STORE_DIR" + } PasswordProvider::PasswordProvider(const QString &path, QObject *parent) @@ -39,12 +42,7 @@ PasswordProvider::PasswordProvider(const QString &path, QObject *parent) PasswordProvider::~PasswordProvider() -{ - if (mGpg) { - mGpg->terminate(); - delete mGpg; - } -} +{} bool PasswordProvider::isValid() const { @@ -86,23 +84,6 @@ void PasswordProvider::expirePassword() deleteLater(); } -PasswordProvider::GpgExecutable PasswordProvider::findGpgExecutable() -{ - auto gpgExe = QStandardPaths::findExecutable(QStringLiteral("gpg2")); - if (gpgExe.isEmpty()) { - gpgExe = QStandardPaths::findExecutable(QStringLiteral("gpg")); - } - - QProcess process; - process.start(gpgExe, {QStringLiteral("--version")}, QIODevice::ReadOnly); - process.waitForFinished(); - const auto line = process.readLine(); - static const QRegularExpression rex(QStringLiteral("([0-9]+).([0-9]+).([0-9]+)")); - const auto match = rex.match(QString::fromUtf8(line)); - - return {gpgExe, match.captured(1).toInt(), match.captured(2).toInt()}; -} - void PasswordProvider::requestPassword() { setError({}); @@ -121,62 +102,7 @@ void PasswordProvider::requestPassword() } }); - const auto gpgExe = findGpgExecutable(); - if (gpgExe.path.isEmpty()) { - qWarning("Failed to find gpg or gpg2 executables"); - setError(tr("Failed to decrypt password: GPG is not available")); - return; - } - qDebug("Detected gpg version: %d.%d", gpgExe.major_version, gpgExe.minor_version); - - QStringList args = { QStringLiteral("--decrypt"), - QStringLiteral("--quiet"), - QStringLiteral("--yes"), - QStringLiteral("--compress-algo=none"), - QStringLiteral("--no-encrypt-to"), - QStringLiteral("--passphrase-fd=0") }; - if (gpgExe.major_version >= 2) { - args += QStringList{ QStringLiteral("--batch"), - QStringLiteral("--no-use-agent") }; - - if (gpgExe.minor_version >= 1) { - args.push_back(QStringLiteral("--pinentry-mode=loopback")); - } - } - - args.push_back(mPath); - - mGpg = new QProcess; - connect(mGpg, &QProcess::errorOccurred, - this, [this, gpgExe](QProcess::ProcessError state) { - if (state == QProcess::FailedToStart) { - qWarning("Failed to start %s: %s", qUtf8Printable(gpgExe.path), qUtf8Printable(mGpg->errorString())); - setError(tr("Failed to decrypt password: Failed to start GPG")); - } - }); - connect(mGpg, &QProcess::readyReadStandardOutput, - this, [this]() { - // We only read the first line, second line usually is a username - setPassword(QString::fromUtf8(mGpg->readLine()).trimmed()); - }); - connect(mGpg, static_cast<void(QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), - this, [this]() { - const auto err = mGpg->readAllStandardError(); - if (mPassword.isEmpty()) { - if (err.isEmpty()) { - setError(tr("Failed to decrypt password")); - } else { - setError(tr("Failed to decrypt password: %1").arg(QString::fromUtf8(err))); - } - } - - mGpg->deleteLater(); - mGpg = nullptr; - }); - mGpg->setProgram(gpgExe.path); - mGpg->setArguments(args); - mGpg->start(QIODevice::ReadWrite); } int PasswordProvider::timeout() const @@ -207,22 +133,40 @@ void PasswordProvider::setError(const QString &error) void PasswordProvider::cancel() { - if (mGpg) { - mGpg->terminate(); - delete mGpg; - } setError(tr("Cancelled by user.")); } void PasswordProvider::setPassphrase(const QString &passphrase) { - if (!mGpg) { - qWarning("Called PasswordProvider::setPassphrase without active GPG process"); + const QString root = qEnvironmentVariableIsSet(PASSWORD_STORE_DIR) + ? QString::fromUtf8(qgetenv(PASSWORD_STORE_DIR)) + : QStringLiteral("%1/.password-store").arg(QDir::homePath()); + QFile gpgIdFile(root + QStringLiteral("/.gpg-id")); + if (!gpgIdFile.exists()) { + qWarning() << "Missing .gpg-id file (" << gpgIdFile.fileName() << ")"; return; } + gpgIdFile.open(QIODevice::ReadOnly); + const auto gpgId = QString::fromUtf8(gpgIdFile.readAll()).trimmed(); + gpgIdFile.close(); + + auto *job = Gpg::decrypt(mPath, Gpg::Key{gpgId}, passphrase); + connect(job, &Gpg::DecryptTask::finished, + this, [this, job]() { + if (job->error()) { + qWarning() << "Failed to decrypt password: " << job->errorString(); + setError(job->errorString()); + return; + } - mGpg->write(passphrase.toUtf8()); - mGpg->closeWriteChannel(); + const QStringList lines = job->content().split(QLatin1Char('\n')); + if (lines.empty()) { + qWarning() << "Failed to decrypt password or file empty"; + setError(tr("Failed to decrypt password")); + } else { + setPassword(lines[0]); + } + }); } void PasswordProvider::removePasswordFromClipboard(const QString &password) diff --git a/src/passwordprovider.h b/src/passwordprovider.h @@ -47,16 +47,6 @@ public: bool hasError() const; QString error() const; - struct GpgExecutable { - GpgExecutable(const QString &path, int major, int minor) - : path(path), major_version(major), minor_version(minor) - {} - QString path = {}; - int major_version = 0; - int minor_version = 0; - }; - - static GpgExecutable findGpgExecutable(); public Q_SLOTS: void requestPassword(); void cancel(); @@ -79,7 +69,6 @@ private: friend class PasswordsModel; explicit PasswordProvider(const QString &path, QObject *parent = nullptr); - QProcess *mGpg = nullptr; QString mPath; QString mPassword; QString mError;