sailfish-safe

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

commit abbdabdccfbf416dd57bee203b7d0ca8fc72bbf6
parent 2b1e7c91cc4f6ec37d1f8a66f45d15680d7837ea
Author: Daniel Vrátil <dvratil@kde.org>
Date:   Mon, 18 Feb 2019 23:33:52 +0100

Initial implementation for adding new passwords

Needs a bit of clean up, adding a UI to remove and possibly even
edit a password should come next.

Diffstat:
Mharbour-passilic.pro | 10+++++++---
Mqml/components/GlobalPullDownMenu.qml | 9+++++++++
Aqml/pages/GeneratePasswordDialog.qml | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aqml/pages/NewPasswordDialog.qml | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mqml/pages/PasswordListPage.qml | 10+++++++++-
Msrc/main.cpp | 5+++++
Asrc/passwordgenerator.cpp | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/passwordgenerator.h | 34++++++++++++++++++++++++++++++++++
Msrc/passwordprovider.cpp | 25+++++++++++++++----------
Msrc/passwordprovider.h | 9+++++++++
Msrc/passwordsmodel.cpp | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/passwordsmodel.h | 2++
Msrc/passwordsortproxymodel.cpp | 6++++++
Msrc/passwordsortproxymodel.h | 2++
Mtranslations/harbour-passilic-cs.ts | 34++++++++++++++++++++++++++++++++++
Mtranslations/harbour-passilic-fr.ts | 34++++++++++++++++++++++++++++++++++
Mtranslations/harbour-passilic-zh.ts | 34++++++++++++++++++++++++++++++++++
Mtranslations/harbour-passilic.ts | 34++++++++++++++++++++++++++++++++++
18 files changed, 528 insertions(+), 14 deletions(-)

diff --git a/harbour-passilic.pro b/harbour-passilic.pro @@ -23,7 +23,8 @@ SOURCES += \ src/passwordsmodel.cpp \ src/passwordsortproxymodel.cpp \ 3rdparty/kitemmodels/kdescendantsproxymodel.cpp \ - src/settings.cpp + src/settings.cpp \ + src/passwordgenerator.cpp HEADERS += \ @@ -35,7 +36,8 @@ HEADERS += \ src/passwordsortproxymodel.h \ 3rdparty/kitemmodels/kdescendantsproxymodel.h \ src/settings.h \ - src/scopeguard.h + src/scopeguard.h \ + src/passwordgenerator.h DISTFILES += \ qml/harbour-passilic.qml \ @@ -52,7 +54,9 @@ DISTFILES += \ harbour-passilic.desktop \ qml/pages/SearchPage.qml \ qml/components/PasswordDelegate.qml \ - qml/pages/SettingsPage.qml + qml/pages/SettingsPage.qml \ + qml/pages/NewPasswordDialog.qml \ + qml/pages/GeneratePasswordDialog.qml OTHER_FILES += \ README.md diff --git a/qml/components/GlobalPullDownMenu.qml b/qml/components/GlobalPullDownMenu.qml @@ -20,6 +20,9 @@ import QtQml.Models 2.2 import Sailfish.Silica 1.0 PullDownMenu { + property var currentIndex + property var model + MenuItem { text: qsTr("About") onClicked: app.pageStack.push(Qt.resolvedUrl("../pages/AboutPage.qml")) @@ -29,6 +32,12 @@ PullDownMenu { onClicked: app.pageStack.push(Qt.resolvedUrl("../pages/SettingsPage.qml")) } MenuItem { + text: qsTr("New Password") + onClicked: app.pageStack.push(Qt.resolvedUrl("../pages/NewPasswordDialog.qml"), + { "currentIndex": currentIndex, + "model": model }) + } + MenuItem { text: qsTr("Search") onClicked: app.pageStack.push(searchPage) } diff --git a/qml/pages/GeneratePasswordDialog.qml b/qml/pages/GeneratePasswordDialog.qml @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2019 Daniel Vrátil <dvratil@kde.org> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import QtQuick 2.2 +import QtQml.Models 2.2 +import Sailfish.Silica 1.0 + +Dialog { + + property alias passLen : passLenSlider.value + property alias allowSymbols: symbolsSwitch.checked + + DialogHeader { + id: header + width: parent.width + } + + SilicaFlickable { + anchors { + top: header.bottom + left: parent.left + right: parent.right + bottom: parent.bottom + } + + clip: true + contentHeight: column.height + + Column { + id: column + + anchors { + left: parent.left + right: parent.right + leftMargin: Theme.horizontalPageMargin + rightMargin: Theme.horizontalPageMargin + } + + spacing: Theme.paddingMedium + + Label { + text: qsTr("Password Length") + width: parent.width + } + + Slider { + id: passLenSlider + width: parent.width + + minimumValue: 6 + maximumValue: 64 + stepSize: 1 + value: 20 + valueText: value + } + + TextSwitch { + id: symbolsSwitch + width: parent.width + + text: qsTr("Allow non-alphanumeric characters") + checked: true + } + } + } +} diff --git a/qml/pages/NewPasswordDialog.qml b/qml/pages/NewPasswordDialog.qml @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2019 Daniel Vrátil <dvratil@kde.org> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import QtQuick 2.2 +import QtQml.Models 2.2 +import Sailfish.Silica 1.0 +import harbour.passilic 1.0 + +Dialog { + id: newPasswordDialog + + property var model + property var currentIndex + + canAccept: nameField.text !== "" && passwordField.text !== "" + + onAccepted: { + model.addPassword(currentIndex, nameField.text, passwordField.text, extrasField.text) + } + + DialogHeader { + id: header + width: parent.width + } + + SilicaFlickable { + id: flickable + + anchors { + top: header.bottom + left: parent.left + right: parent.right + bottom: parent.bottom + } + + clip: true + contentHeight: column.height + + Column { + id: column + + anchors { + left: parent.left + right: parent.right + leftMargin: Theme.horizontalPageMargin + rightMargin: Theme.horizontalPageMargin + } + + spacing: Theme.paddingMedium + + Label { + text: qsTr("Name:") + width: parent.width + } + + TextField { + id: nameField + width: parent.width + } + + Label { + text: qsTr("Password:") + width: parent.width + } + + PasswordField { + id: passwordField + width: parent.width + } + + Button { + id: generatePassButton + width: parent.width + text: qsTr("Generate Password") + + onClicked: app.pageStack.push(genPassDialog) + } + + Label { + text: qsTr("Additional Info:") + width: parent.width + } + + TextArea { + id: extrasField + width: parent.width + } + } + + + HorizontalScrollDecorator { + flickable: parent + } + } + + Component { + id: genPassDialog + + GeneratePasswordDialog { + onAccepted: { + passwordField.text = PasswordGenerator.generate(passLen, allowSymbols) + } + } + } +} diff --git a/qml/pages/PasswordListPage.qml b/qml/pages/PasswordListPage.qml @@ -33,6 +33,11 @@ Page { signal passwordRequested(var requester) + Connections { + target: model + onModelReset: app.pageStack.pop(passwordListPage, PageStackAction.Immediate) + } + SilicaListView { id: listView @@ -44,7 +49,10 @@ Page { title: passwordListPage.currentPath === "" ? qsTr("Passilic") : passwordListPage.currentPath } - GlobalPullDownMenu {} + GlobalPullDownMenu { + currentIndex: passwordListPage.rootIndex + model: passwordListPage.model + } model: DelegateModel { id: delegateModel diff --git a/src/main.cpp b/src/main.cpp @@ -18,6 +18,7 @@ #include "passwordsmodel.h" #include "passwordfiltermodel.h" #include "passwordsortproxymodel.h" +#include "passwordgenerator.h" #include "imageprovider.h" #include "scopeguard.h" #include "settings.h" @@ -54,6 +55,10 @@ int main(int argc, char *argv[]) [](QQmlEngine *, QJSEngine *) -> QObject* { return Settings::self(); }); + qmlRegisterSingletonType<PasswordGenerator>("harbour.passilic", 1, 0, "PasswordGenerator", + [](QQmlEngine *, QJSEngine *) -> QObject* { + return new PasswordGenerator; + }); addImageProvider(view->engine(), QStringLiteral("passIcon")); addImageProvider(view->engine(), QStringLiteral("passImage")); diff --git a/src/passwordgenerator.cpp b/src/passwordgenerator.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2019 Daniel Vrátil <dvratil@kde.org> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "passwordgenerator.h" + +#include <QFile> + +#include <ctype.h> + +PasswordGenerator::PasswordGenerator() +{ +} + +QString PasswordGenerator::generate(int len, bool allowSymbols) +{ + QString pass; + pass.reserve(len); + + QFile urand(QStringLiteral("/dev/urandom")); + if (!urand.open(QIODevice::ReadOnly)) { + return {}; + } + + while (pass.size() < len) { + const char c = urand.read(1)[0]; + if (isalnum(c) || (allowSymbols && isprint(c))) { + pass.append(QLatin1Char(c)); + } + } + + return pass; +} diff --git a/src/passwordgenerator.h b/src/passwordgenerator.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2019 Daniel Vrátil <dvratil@kde.org> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#ifndef PASSWORDGENERATOR_H +#define PASSWORDGENERATOR_H + +#include <QObject> + +class PasswordGenerator : public QObject +{ + Q_OBJECT + +public: + PasswordGenerator(); + +public Q_SLOTS: + QString generate(int len, bool allowSymbols); +}; + +#endif // PASSWORDGENERATOR_H diff --git a/src/passwordprovider.cpp b/src/passwordprovider.cpp @@ -85,6 +85,16 @@ void PasswordProvider::expirePassword() deleteLater(); } +PasswordProvider::GpgExecutable PasswordProvider::findGpgExecutable() +{ + auto gpgExe = QStandardPaths::findExecutable(QStringLiteral("gpg2")); + if (gpgExe.isEmpty()) { + gpgExe = QStandardPaths::findExecutable(QStringLiteral("gpg")); + return {gpgExe, false}; + } + return {gpgExe, true}; +} + void PasswordProvider::requestPassword() { setError({}); @@ -103,13 +113,8 @@ void PasswordProvider::requestPassword() } }); - bool isGpg2 = true; - auto gpgExe = QStandardPaths::findExecutable(QStringLiteral("gpg2")); - if (gpgExe.isEmpty()) { - gpgExe = QStandardPaths::findExecutable(QStringLiteral("gpg")); - isGpg2 = false; - } - if (gpgExe.isEmpty()) { + 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; @@ -122,7 +127,7 @@ void PasswordProvider::requestPassword() QStringLiteral("--no-encrypt-to"), QStringLiteral("--passphrase-fd=0"), mPath }; - if (isGpg2) { + if (gpgExe.isGpg2) { args = QStringList{ QStringLiteral("--pinentry-mode=loopback"), QStringLiteral("--batch"), QStringLiteral("--use-agent") } @@ -133,7 +138,7 @@ void PasswordProvider::requestPassword() connect(mGpg, &QProcess::errorOccurred, this, [this, gpgExe](QProcess::ProcessError state) { if (state == QProcess::FailedToStart) { - qWarning("Failed to start %s: %s", qUtf8Printable(gpgExe), qUtf8Printable(mGpg->errorString())); + qWarning("Failed to start %s: %s", qUtf8Printable(gpgExe.path), qUtf8Printable(mGpg->errorString())); setError(tr("Failed to decrypt password: Failed to start GPG")); } }); @@ -156,7 +161,7 @@ void PasswordProvider::requestPassword() mGpg->deleteLater(); mGpg = nullptr; }); - mGpg->setProgram(gpgExe); + mGpg->setProgram(gpgExe.path); mGpg->setArguments(args); mGpg->start(QIODevice::ReadWrite); } diff --git a/src/passwordprovider.h b/src/passwordprovider.h @@ -47,6 +47,15 @@ public: bool hasError() const; QString error() const; + struct GpgExecutable { + GpgExecutable(const QString &path, bool isGpg2) + : path(path), isGpg2(isGpg2) + {} + QString path = {}; + bool isGpg2 = false; + }; + + static GpgExecutable findGpgExecutable(); public Q_SLOTS: void requestPassword(); void cancel(); diff --git a/src/passwordsmodel.cpp b/src/passwordsmodel.cpp @@ -23,6 +23,9 @@ #include <QDir> #include <QDebug> #include <QPointer> +#include <QProcess> +#include <QTemporaryFile> +#include <QFile> #define PASSWORD_STORE_DIR "PASSWORD_STORE_DIR" @@ -219,3 +222,49 @@ void PasswordsModel::populateDir(const QDir& dir, Node *parent) populateDir(entry.absoluteFilePath(), node); } } + +// FIXME: This is absolutely not the right place for this piece of code +// Should introduce PasswordManager to abstract password generation, +// creation and access in an asynchronous manner. +void PasswordsModel::addPassword(const QModelIndex &parent, const QString &name, + const QString &password, const QString &extras) +{ + auto node = this->node(parent); + + // Escape forward slash to avoid the name "escaping" the current folder + QString safeName = name; + safeName.replace(QLatin1Char('/'), QLatin1Char(' ')); + + QFile gpgIdFile(mRoot->path() + 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(); + + + const auto gpgExe = PasswordProvider::findGpgExecutable(); + if (gpgExe.path.isEmpty()) { + qWarning() << "Failed to find GPG executable"; + return; + } + + QProcess process; + process.setProgram(gpgExe.path); + process.setArguments({ QStringLiteral("-e"), + QStringLiteral("--no-tty"), + QStringLiteral("-r%1").arg(gpgId), + QStringLiteral("-o%1/%2.gpg").arg(node->path(), safeName) + }); + process.start(QIODevice::ReadWrite); + process.waitForStarted(); + process.write(password.toUtf8()); + if (!extras.isEmpty()) { + process.write("\n"); + process.write(extras.toUtf8()); + } + process.closeWriteChannel(); + process.waitForFinished(); +} diff --git a/src/passwordsmodel.h b/src/passwordsmodel.h @@ -58,6 +58,8 @@ public: QVariant data(const QModelIndex &index, int role) const override; + Q_INVOKABLE void addPassword(const QModelIndex &parent, const QString &name, + const QString &password, const QString &extras); private: void populate(); void populateDir(const QDir &dir, Node *parent); diff --git a/src/passwordsortproxymodel.cpp b/src/passwordsortproxymodel.cpp @@ -40,3 +40,9 @@ bool PasswordSortProxyModel::lessThan(const QModelIndex &source_left, const QMod return QSortFilterProxyModel::lessThan(source_left, source_right); } + +void PasswordSortProxyModel::addPassword(const QModelIndex &parent, const QString &name, + const QString &password, const QString &extras) +{ + qobject_cast<PasswordsModel*>(sourceModel())->addPassword(mapToSource(parent), name, password, extras); +} diff --git a/src/passwordsortproxymodel.h b/src/passwordsortproxymodel.h @@ -28,6 +28,8 @@ class PasswordSortProxyModel : public QSortFilterProxyModel public: explicit PasswordSortProxyModel(QObject *parent = nullptr); + Q_INVOKABLE void addPassword(const QModelIndex &parent, const QString &name, + const QString &password, const QString &extra); protected: bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override; }; diff --git a/translations/harbour-passilic-cs.ts b/translations/harbour-passilic-cs.ts @@ -29,6 +29,17 @@ </message> </context> <context> + <name>GeneratePasswordDialog</name> + <message> + <source>Password Length</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Allow non-alphanumeric characters</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> <name>GlobalPullDownMenu</name> <message> <source>About</source> @@ -42,6 +53,29 @@ <source>Settings</source> <translation>Nastavení</translation> </message> + <message> + <source>New Password</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>NewPasswordDialog</name> + <message> + <source>Name:</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Password:</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Additional Info:</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Generate Password</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>PasswordDelegate</name> diff --git a/translations/harbour-passilic-fr.ts b/translations/harbour-passilic-fr.ts @@ -29,6 +29,17 @@ </message> </context> <context> + <name>GeneratePasswordDialog</name> + <message> + <source>Password Length</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Allow non-alphanumeric characters</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> <name>GlobalPullDownMenu</name> <message> <source>About</source> @@ -42,6 +53,29 @@ <source>Settings</source> <translation>Paramètres</translation> </message> + <message> + <source>New Password</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>NewPasswordDialog</name> + <message> + <source>Name:</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Password:</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Additional Info:</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Generate Password</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>PasswordDelegate</name> diff --git a/translations/harbour-passilic-zh.ts b/translations/harbour-passilic-zh.ts @@ -29,6 +29,17 @@ </message> </context> <context> + <name>GeneratePasswordDialog</name> + <message> + <source>Password Length</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Allow non-alphanumeric characters</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> <name>GlobalPullDownMenu</name> <message> <source>About</source> @@ -42,6 +53,29 @@ <source>Settings</source> <translation>设置</translation> </message> + <message> + <source>New Password</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>NewPasswordDialog</name> + <message> + <source>Name:</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Password:</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Additional Info:</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Generate Password</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>PasswordDelegate</name> diff --git a/translations/harbour-passilic.ts b/translations/harbour-passilic.ts @@ -29,6 +29,17 @@ </message> </context> <context> + <name>GeneratePasswordDialog</name> + <message> + <source>Password Length</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Allow non-alphanumeric characters</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> <name>GlobalPullDownMenu</name> <message> <source>About</source> @@ -42,6 +53,29 @@ <source>Settings</source> <translation type="unfinished"></translation> </message> + <message> + <source>New Password</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>NewPasswordDialog</name> + <message> + <source>Name:</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Password:</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Additional Info:</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Generate Password</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>PasswordDelegate</name>