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:
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;