tCreate a wrapper for GPG operations - sailfish-safe - Sailfish frontend for safe(1)
 (HTM) git clone git://git.z3bra.org/sailfish-safe.git
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit f1b5077fd73c21f61dbd6a5bfc5fb93bc5be99ec
 (DIR) parent b76b96edbea9bea1bbeee6db3fbf0e99277ecfd9
 (HTM) Author: Daniel Vrátil <daniel.vratil@avast.com>
       Date:   Mon, 19 Apr 2021 20:26:41 +0200
       
       Create a wrapper for GPG operations
       
       Diffstat:
         M harbour-passilic.pro                |       4 ++++
         A src/gpg.cpp                         |     243 +++++++++++++++++++++++++++++++
         A src/gpg.h                           |     123 +++++++++++++++++++++++++++++++
         M src/passwordprovider.cpp            |     122 +++++++++----------------------
         M src/passwordprovider.h              |      11 -----------
       
       5 files changed, 403 insertions(+), 100 deletions(-)
       ---
 (DIR) diff --git a/harbour-passilic.pro b/harbour-passilic.pro
       t@@ -2,6 +2,8 @@ TARGET = harbour-passilic
        
        CONFIG += sailfishapp
        
       +QT += concurrent
       +
        DEFINES += \
            QT_NO_CAST_FROM_ASCII \
            QT_NO_CAST_TO_ASCII \
       t@@ -15,6 +17,7 @@ DEFINES += \
        INCLUDEPATH += 3rdparty/kitemmodels/
        
        SOURCES += \
       +    src/gpg.cpp \
            src/main.cpp \
            src/abbreviations.cpp \
            src/imageprovider.cpp \
       t@@ -29,6 +32,7 @@ SOURCES += \
        
        HEADERS += \
            src/abbreviations.h \
       +    src/gpg.h \
            src/imageprovider.h \
            src/passwordfiltermodel.h \
            src/passwordprovider.h \
 (DIR) diff --git a/src/gpg.cpp b/src/gpg.cpp
       t@@ -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());
       +    }
       +}
 (DIR) diff --git a/src/gpg.h b/src/gpg.h
       t@@ -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
 (DIR) diff --git a/src/passwordprovider.cpp b/src/passwordprovider.cpp
       t@@ -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)
       t@@ -39,12 +42,7 @@ PasswordProvider::PasswordProvider(const QString &path, QObject *parent)
        
        
        PasswordProvider::~PasswordProvider()
       -{
       -    if (mGpg) {
       -        mGpg->terminate();
       -        delete mGpg;
       -    }
       -}
       +{}
        
        bool PasswordProvider::isValid() const
        {
       t@@ -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({});
       t@@ -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
       t@@ -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)
 (DIR) diff --git a/src/passwordprovider.h b/src/passwordprovider.h
       t@@ -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();
       t@@ -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;