sailfish-safe

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

commit 28c9776bda3ca635a02edcb38fb84ff2a10a2594
parent 5a204c6193b570c5da0ad47b18fcddac329530e9
Author: Daniel Vrátil <dvratil@kde.org>
Date:   Sun,  3 Feb 2019 13:51:37 +0100

Implement decrypting, copying and expiring passwords.

It's still a bit rough around the edges but it's generally
useful now.

Diffstat:
Mharbour-passilic.pro | 3++-
Mqml/harbour-passilic.qml | 6++++++
Aqml/pages/PassphraseRequester.qml | 32++++++++++++++++++++++++++++++++
Mqml/pages/PasswordListPage.qml | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Msrc/passwordprovider.cpp | 131+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/passwordprovider.h | 7++++++-
6 files changed, 208 insertions(+), 69 deletions(-)

diff --git a/harbour-passilic.pro b/harbour-passilic.pro @@ -31,7 +31,8 @@ DISTFILES += \ rpm/harbour-passilic.spec \ rpm/harbour-passilic.yaml \ translations/*.ts \ - harbour-passilic.desktop + harbour-passilic.desktop \ + qml/pages/PassphraseRequester.qml SAILFISHAPP_ICONS = 86x86 108x108 128x128 172x172 diff --git a/qml/harbour-passilic.qml b/qml/harbour-passilic.qml @@ -24,6 +24,12 @@ ApplicationWindow } Component { + id: passphraseRequester + + PassphraseRequester {} + } + + Component { id: passwordsPage PasswordListPage { diff --git a/qml/pages/PassphraseRequester.qml b/qml/pages/PassphraseRequester.qml @@ -0,0 +1,32 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import harbour.passilic 1.0 + +Dialog { + id: dlg + + property var requester: null + + DialogHeader { + } + + PasswordField { + id: passwordField + + anchors { + centerIn: parent + left: parent.left + right: parent.right + leftMargin: Theme.horizontalPageMargin + rightMargin: Theme.horizontalPageMargin + } + + placeholderText: "Key passphrase" + EnterKey.iconSource: "image://theme/icom-m-enter-accept" + EnterKey.onClicked: dlg.accept() + + } + + onRejected: requester.cancel() + onAccepted: requester.setPassphrase(passwordField.text) +} diff --git a/qml/pages/PasswordListPage.qml b/qml/pages/PasswordListPage.qml @@ -12,6 +12,7 @@ Page { signal folderSelected(var index, var name) + signal passwordRequested(var requester) SilicaListView { @@ -32,42 +33,105 @@ Page { delegate: ListItem { id: listItem - height: Theme.itemSizeSmall - Row { + property var password : null - spacing: Theme.paddingMedium + contentHeight: password === null ? Theme.itemSizeSmall : Theme.itemSizeLarge + Row { anchors { - left: parent.left + fill: parent leftMargin: Theme.horizontalPageMargin - right: parent.right rightMargin: Theme.horizontalPageMargin verticalCenter: parent.verticalCenter + topMargin: Theme.paddingMedium } - Image { - anchors.verticalCenter: parent.verticalCenter - source: "image://theme/" - + ((model.type === PasswordsModel.FolderEntry) ? "icon-m-folder" : "icon-m-device-lock") - + "?" - + (listItem.highlighted ? Theme.highlightColor : Theme.primaryColor) - width: Theme.iconSizeSmall - height: width + Column { + spacing: Theme.paddingSmall + width: parent.width + + Row { + spacing: Theme.paddingMedium + + Image { + anchors.verticalCenter: parent.verticalCenter + source: "image://theme/" + + ((model.type === PasswordsModel.FolderEntry) ? "icon-m-folder" : "icon-m-device-lock") + + "?" + + (listItem.highlighted ? Theme.highlightColor : Theme.primaryColor) + width: Theme.iconSizeSmall + height: width + } + + Label { + id: label + text: model.name + } + } + + Row { + visible: password !== null + width: parent.width + + Label { + id: errorLabel + + visible: password !== null && password.hasError + + text: password ? password.error : "" + font.pixelSize: Theme.fontSizeTiny + } + + Label { + id: okLabel + + visible: password !== null && password.valid + + text: qsTr("Password copied to clipboard") + font.pixelSize: Theme.fontSizeTiny + } + } } + } + + RemorseItem { + id: remorse - Label { - id: label - text: model.name + cancelText: qsTr("Expire password") + + // HACK: override RemorseItem._execute() to act as cancel when the timer expires + function _execute(closeAfterExecute) { + cancel() } + onCanceled: { + if (listItem.password) { + listItem.password.expirePassword(); + } + } } onClicked: { if (model.type === PasswordsModel.FolderEntry) { passwordListPage.folderSelected(delegateModel.modelIndex(index), model.name); } else { - console.log("Password for " + model.name + " requested"); + model.password.requestPassword() + var dialog = pageStack.push(Qt.resolvedUrl("PassphraseRequester.qml"), + { "requester": model.password }) + dialog.done.connect(function() { + listItem.password = model.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 @@ -33,7 +33,66 @@ static const auto PasswordTimeoutUpdateInterval = 100; PasswordProvider::PasswordProvider(const QString &path, QObject *parent) : QObject(parent) + , mPath(path) +{} + + +PasswordProvider::~PasswordProvider() +{ + if (mGpg) { + mGpg->terminate(); + delete mGpg; + } +} + +bool PasswordProvider::isValid() const +{ + return !mPassword.isNull(); +} + +QString PasswordProvider::password() const +{ + return mPassword; +} + +void PasswordProvider::setPassword(const QString &password) +{ + qGuiApp->clipboard()->setText(password, QClipboard::Clipboard); + + if (qGuiApp->clipboard()->supportsSelection()) { + qGuiApp->clipboard()->setText(password, QClipboard::Selection); + } + + mPassword = password; + Q_EMIT validChanged(); + Q_EMIT passwordChanged(); + + mTimeout = defaultTimeout(); + Q_EMIT timeoutChanged(); + mTimer.start(); +} + +void PasswordProvider::expirePassword() { + removePasswordFromClipboard(mPassword); + + mPassword.clear(); + mTimer.stop(); + Q_EMIT validChanged(); + Q_EMIT passwordChanged(); + + // Delete the provider, it's no longer needed + deleteLater(); +} + +void PasswordProvider::requestPassword() +{ + setError({}); + mPassword.clear(); + mTimer.stop(); + Q_EMIT validChanged(); + Q_EMIT passwordChanged(); + mTimer.setInterval(PasswordTimeoutUpdateInterval); connect(&mTimer, &QTimer::timeout, this, [this]() { @@ -61,7 +120,8 @@ PasswordProvider::PasswordProvider(const QString &path, QObject *parent) QStringLiteral("--yes"), QStringLiteral("--compress-algo=none"), QStringLiteral("--no-encrypt-to"), - path }; + QStringLiteral("--passphrase-fd=0"), + mPath }; if (isGpg2) { args = QStringList{ QStringLiteral("--batch"), QStringLiteral("--use-agent") } + args; } @@ -95,55 +155,7 @@ PasswordProvider::PasswordProvider(const QString &path, QObject *parent) }); mGpg->setProgram(gpgExe); mGpg->setArguments(args); - mGpg->start(QIODevice::ReadOnly); -} - -PasswordProvider::~PasswordProvider() -{ - if (mGpg) { - mGpg->terminate(); - delete mGpg; - } -} - -bool PasswordProvider::isValid() const -{ - return !mPassword.isNull(); -} - -QString PasswordProvider::password() const -{ - return mPassword; -} - -void PasswordProvider::setPassword(const QString &password) -{ - qGuiApp->clipboard()->setText(password, QClipboard::Clipboard); - - if (qGuiApp->clipboard()->supportsSelection()) { - qGuiApp->clipboard()->setText(password, QClipboard::Selection); - } - - mPassword = password; - Q_EMIT validChanged(); - Q_EMIT passwordChanged(); - - mTimeout = defaultTimeout(); - Q_EMIT timeoutChanged(); - mTimer.start(); -} - -void PasswordProvider::expirePassword() -{ - removePasswordFromClipboard(mPassword); - - mPassword.clear(); - mTimer.stop(); - Q_EMIT validChanged(); - Q_EMIT passwordChanged(); - - // Delete the provider, it's no longer needed - deleteLater(); + mGpg->start(QIODevice::ReadWrite); } int PasswordProvider::timeout() const @@ -172,6 +184,25 @@ void PasswordProvider::setError(const QString &error) Q_EMIT errorChanged(); } +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"); + return; + } + + mGpg->write(passphrase.toUtf8()); + mGpg->closeWriteChannel(); +} void PasswordProvider::removePasswordFromClipboard(const QString &password) { diff --git a/src/passwordprovider.h b/src/passwordprovider.h @@ -47,6 +47,12 @@ public: bool hasError() const; QString error() const; +public Q_SLOTS: + void requestPassword(); + void cancel(); + void setPassphrase(const QString &passphrase); + void expirePassword(); + Q_SIGNALS: void passwordChanged(); void validChanged(); @@ -56,7 +62,6 @@ Q_SIGNALS: private: void setError(const QString &error); void setPassword(const QString &password); - void expirePassword(); void removePasswordFromClipboard(const QString &password); void clearClipboard();