tImplement decrypting, copying and expiring passwords. - 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 28c9776bda3ca635a02edcb38fb84ff2a10a2594
 (DIR) parent 5a204c6193b570c5da0ad47b18fcddac329530e9
 (HTM) 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:
         M harbour-passilic.pro                |       3 ++-
         M qml/harbour-passilic.qml            |       6 ++++++
         A qml/pages/PassphraseRequester.qml   |      32 +++++++++++++++++++++++++++++++
         M qml/pages/PasswordListPage.qml      |      98 +++++++++++++++++++++++++------
         M src/passwordprovider.cpp            |     131 +++++++++++++++++++------------
         M src/passwordprovider.h              |       7 ++++++-
       
       6 files changed, 208 insertions(+), 69 deletions(-)
       ---
 (DIR) diff --git a/harbour-passilic.pro b/harbour-passilic.pro
       t@@ -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
        
 (DIR) diff --git a/qml/harbour-passilic.qml b/qml/harbour-passilic.qml
       t@@ -24,6 +24,12 @@ ApplicationWindow
            }
        
            Component {
       +        id: passphraseRequester
       +
       +        PassphraseRequester {}
       +    }
       +
       +    Component {
                id: passwordsPage
        
                PasswordListPage {
 (DIR) diff --git a/qml/pages/PassphraseRequester.qml b/qml/pages/PassphraseRequester.qml
       t@@ -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)
       +}
 (DIR) diff --git a/qml/pages/PasswordListPage.qml b/qml/pages/PasswordListPage.qml
       t@@ -12,6 +12,7 @@ Page {
        
        
            signal folderSelected(var index, var name)
       +    signal passwordRequested(var requester)
        
        
            SilicaListView {
       t@@ -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);
       +                                }
       +                            });
       +                        });
                            }
                        }
                    }
 (DIR) diff --git a/src/passwordprovider.cpp b/src/passwordprovider.cpp
       t@@ -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]() {
       t@@ -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;
            }
       t@@ -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
       t@@ -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)
        {
 (DIR) diff --git a/src/passwordprovider.h b/src/passwordprovider.h
       t@@ -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();
       t@@ -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();