sailfish-safe

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

commit c47b28af98cf0e370d42100ae736f71802f0cf50
parent dd3e65b8ef4a24517f1c7f62132fab50154f660f
Author: Willy Goiffon <contact@z3bra.org>
Date:   Tue, 13 Jul 2021 15:52:34 +0200

Fetch and Create safe(1) secrets using safe-agent

Diffstat:
Mqml/components/PasswordDelegate.qml | 24++++++++++--------------
Msrc/passwordprovider.cpp | 45+++++++++++++++------------------------------
Msrc/passwordprovider.h | 1-
Msrc/passwordsmodel.cpp | 17+++++------------
Msrc/safe.cpp | 89++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/safe.h | 44++++++++++++++++++++++++++++++++------------
6 files changed, 137 insertions(+), 83 deletions(-)

diff --git a/qml/components/PasswordDelegate.qml b/qml/components/PasswordDelegate.qml @@ -109,20 +109,16 @@ ListItem { listItem.folderSelected() } else { modelData.password.requestPassword() - var dialog = pageStack.push(Qt.resolvedUrl("../pages/PassphraseRequester.qml"), - { "requester": modelData.password }) - dialog.done.connect(function() { - listItem.password = modelData.password - listItem.password.validChanged.connect(function() { - if (listItem.password.valid) { - remorse.execute(listItem, qsTr("Password will expire"), - function() { - if (listItem.password) { - listItem.password.expirePassword(); - } - }, listItem.password.defaultTimeout); - } - }); + listItem.password = modelData.password + listItem.password.validChanged.connect(function() { + if (listItem.password.valid) { + remorse.execute(listItem, qsTr("Password will expire"), + function() { + if (listItem.password) { + listItem.password.expirePassword(); + } + }, listItem.password.defaultTimeout); + } }); } } diff --git a/src/passwordprovider.cpp b/src/passwordprovider.cpp @@ -93,6 +93,21 @@ void PasswordProvider::requestPassword() Q_EMIT validChanged(); Q_EMIT passwordChanged(); + auto *job = Safe::decrypt(mPath); + connect(job, &Safe::DecryptTask::finished, + this, [this, job]() { + if (job->error()) { + qWarning() << "Failed to unlock safe: " << job->errorString(); + setError(job->errorString()); + return; + } + // .split() always return one argument, so it's used here to remove + // trailing \n if any, or keep the whole content otherwise + const QStringList lines = job->content().split(QLatin1Char('\n')); + setPassword(lines[0]); + qDebug() << "requestPassword() got: " << lines[0]; + }); + mTimer.setInterval(PasswordTimeoutUpdateInterval); connect(&mTimer, &QTimer::timeout, this, [this]() { @@ -102,8 +117,6 @@ void PasswordProvider::requestPassword() expirePassword(); } }); - - } int PasswordProvider::timeout() const @@ -137,34 +150,6 @@ void PasswordProvider::cancel() setError(tr("Cancelled by user.")); } -void PasswordProvider::setPassphrase(const QString &passphrase) -{ - const QString root = qEnvironmentVariableIsSet(SAFE_DIR) - ? QString::fromUtf8(qgetenv(SAFE_DIR)) - : QStringLiteral("%1/.safe.d").arg(QDir::homePath()); - - const QString socket = qEnvironmentVariableIsSet(SAFE_SOCK) - ? QString::fromUtf8(qgetenv(SAFE_SOCK)) - : QStringLiteral("%1/.safe.sock").arg(QDir::homePath()); - - QFile safeAgentSocket(socket); - if (!safeAgentSocket.exists()) { - qWarning() << "Missing socket file (" << safeAgentSocket.fileName() << ")"; - return; - } - /* - auto *job = Safe::unlock(passphrase); - connect(job, &Safe::UnlockTask::finished, - this, [this, job]() { - if (job->error()) { - qWarning() << "Failed to unlock safe: " << job->errorString(); - setError(job->errorString()); - return; - } - }); - */ -} - void PasswordProvider::removePasswordFromClipboard(const QString &password) { // Clear the WS clipboard itself diff --git a/src/passwordprovider.h b/src/passwordprovider.h @@ -50,7 +50,6 @@ public: public Q_SLOTS: void requestPassword(); void cancel(); - void setPassphrase(const QString &passphrase); void expirePassword(); Q_SIGNALS: diff --git a/src/passwordsmodel.cpp b/src/passwordsmodel.cpp @@ -186,7 +186,7 @@ QVariant PasswordsModel::data(const QModelIndex &index, int role) const return node->fullName(); case PasswordRole: if (!node->provider) { - node->provider = new PasswordProvider(node->path()); + node->provider = new PasswordProvider(node->fullName()); } return QVariant::fromValue(node->provider.data()); case HasPasswordRole: @@ -226,27 +226,20 @@ void PasswordsModel::populateDir(const QDir& dir, Node *parent) void PasswordsModel::addPassword(const QModelIndex &parent, const QString &name, const QString &password, const QString &extras) { - auto node = this->node(parent); - if (!node) { - node = mRoot; - } - - // Escape forward slash to avoid the name "escaping" the current folder - QString safeName = name; - safeName.replace(QLatin1Char('/'), QLatin1Char(' ')); QString data = password; + QString secret = name; if (!extras.isEmpty()) { data += QStringLiteral("\n%1").arg(extras); } - auto *task = Safe::encrypt(QStringLiteral("%1/%2").arg(node->path(), safeName), data); + auto *task = Safe::encrypt(secret, data); connect(task, &Safe::EncryptTask::finished, - this, [safeName, task]() { + this, [secret, task]() { if (task->error()) { qWarning() << "Error:" << task->errorString(); return; } - qDebug() << "Successfully encrypted password for" << safeName; + qDebug() << "Successfully stored secret " << secret; }); } diff --git a/src/safe.cpp b/src/safe.cpp @@ -9,21 +9,23 @@ #include <QtConcurrent> #include <QFutureWatcher> -namespace { +#define SAFE_DIR "SAFE_DIR" +#define SAFE_PID "SAFE_PID" +#define SAFE_SOCK "SAFE_SOCK" +#define SAFE_ASKPASS "SAFE_ASKPASS" -struct SafeExecutable { - SafeExecutable(const QString &path) - : path(path) - {} - QString path = {}; -}; +namespace { +} // namespace -SafeExecutable findSafeExecutable() +Safe::LockTask *Safe::lock() { - return QStandardPaths::findExecutable(QStringLiteral("safe")); + return new LockTask(); } -} // namespace +Safe::UnlockTask *Safe::unlock(const QString &passphrase) +{ + return new UnlockTask(passphrase); +} Safe::EncryptTask *Safe::encrypt(const QString &file, const QString &content) { @@ -68,15 +70,74 @@ void Safe::Task::start() watcher->setFuture(future); } +Safe::LockTask::LockTask() +{} + +void Safe::LockTask::run() +{ + if (qEnvironmentVariableIsSet(SAFE_PID)) + return; + + const auto kill = QStandardPaths::findExecutable(QStringLiteral("kill")); + const auto agent = QString::fromUtf8(qgetenv(SAFE_PID)); + + QProcess process; + process.setProgram(kill); + process.setArguments({ + QStringLiteral("-USR1"), + QStringLiteral("%1").arg(agent) + }); + process.start(); + process.waitForStarted(); + process.waitForFinished(); + if (process.exitCode() != 0) { + const auto err = process.readAllStandardError(); + qWarning() << "Failed to forget key:" << err; + setError(QString::fromUtf8(err)); + } +} + +Safe::UnlockTask::UnlockTask(const QString &passphrase) + : mPassphrase(passphrase) +{} + +void Safe::UnlockTask::run() +{ + const QString safe = QStandardPaths::findExecutable(QStringLiteral("safe")); + + if (!qEnvironmentVariableIsSet(SAFE_PID)) { + qWarning() << "Agent is not running"; + return; + } + + if (!qEnvironmentVariableIsSet(SAFE_SOCK)) { + qWarning() << "Agent is not found"; + return; + } + + QProcess process; + process.setProgram(safe); + process.setArguments({QStringLiteral("-r")}); + process.start(); + process.waitForStarted(); + process.write(mPassphrase.toUtf8()); + process.waitForFinished(); + if (process.exitCode() != 0) { + const auto err = process.readAllStandardError(); + qWarning() << "Failed to unlock safe:" << err; + setError(QString::fromUtf8(err)); + } +} + Safe::EncryptTask::EncryptTask(const QString &file, const QString &content) : mFile(file), mContent(content) {} void Safe::EncryptTask::run() { - const auto safe = findSafeExecutable(); + const QString safe = QStandardPaths::findExecutable(QStringLiteral("safe")); QProcess process; - process.setProgram(safe.path); + process.setProgram(safe); process.setArguments({ QStringLiteral("-a"), QStringLiteral("%1").arg(mFile) @@ -104,9 +165,9 @@ QString Safe::DecryptTask::content() const void Safe::DecryptTask::run() { - const auto safe = findSafeExecutable(); + const QString safe = QStandardPaths::findExecutable(QStringLiteral("safe")); QProcess process; - process.setProgram(safe.path); + process.setProgram(safe); process.setArguments({QStringLiteral("%1").arg(mFile)}); process.start(); process.waitForStarted(); diff --git a/src/safe.h b/src/safe.h @@ -6,21 +6,13 @@ namespace Safe { +class LockTask; +class UnlockTask; class DecryptTask; class EncryptTask; -struct Key { - enum class Trust { - Unknown = 1, - Never = 2, - Marginal = 3, - Full = 4, - Ultimate = 5 - }; - - QString id; -}; - +LockTask *lock(); +UnlockTask *unlock(const QString &passphrase); DecryptTask *decrypt(const QString &file); EncryptTask *encrypt(const QString &data, const QString &file); @@ -48,6 +40,34 @@ private: QString mError; }; +class LockTask : public Task { + Q_OBJECT + friend LockTask *Safe::lock(); +public: + QString content() const; + +protected: + void run() override; + +private: + LockTask(); +}; + +class UnlockTask : public Task { + Q_OBJECT + friend UnlockTask *Safe::unlock(const QString &); +public: + QString content() const; + +protected: + void run() override; + +private: + UnlockTask(const QString &passphrase); + + QString mPassphrase; +}; + class DecryptTask : public Task { Q_OBJECT friend DecryptTask *Safe::decrypt(const QString &);