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