Page MenuHome GnuPG

No OneTemporary

This document is not UTF8. It was detected as ISO-8859-1 (Latin 1) and converted to UTF8 for display.
diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt
new file mode 100644
index 0000000..d2c09c8
--- /dev/null
+++ b/autotests/CMakeLists.txt
@@ -0,0 +1,15 @@
+# SPDX-FileCopyrightText: 2024 g10 Code GmbH
+# SPDX-FileContributor: Carl Schwan <carl.schwan@gnupg.com>
+# SPDX-License-Identifier: BSD-3-Clauses
+
+add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/testdata")
+
+add_subdirectory(gnupg_home)
+include(${CMAKE_SOURCE_DIR}/cmake/modules/add_gpg_crypto_test.cmake)
+
+#ecm_add_test(tst_ui.cpp LINK_LIBRARIES gpgpass_internal Qt::Test)
+ecm_add_test(tst_util.cpp LINK_LIBRARIES gpgpass_internal Qt::Test)
+ecm_add_test(tst_passentry.cpp LINK_LIBRARIES gpgpass_internal Qt::Test)
+
+gpgpass_add_crypto_test(tst_userslistmodel.cpp LINK_LIBRARIES gpgpass_internal Qt::Test)
+gpgpass_add_crypto_test(tst_passnode.cpp LINK_LIBRARIES gpgpass_internal Qt::Test)
diff --git a/tests/README.md b/autotests/README.md
similarity index 100%
rename from tests/README.md
rename to autotests/README.md
diff --git a/tests/gnupg_home/.gpg-v21-migrated b/autotests/gnupg_home/.gpg-v21-migrated
similarity index 100%
rename from tests/gnupg_home/.gpg-v21-migrated
rename to autotests/gnupg_home/.gpg-v21-migrated
diff --git a/tests/gnupg_home/CMakeLists.txt b/autotests/gnupg_home/CMakeLists.txt
similarity index 100%
rename from tests/gnupg_home/CMakeLists.txt
rename to autotests/gnupg_home/CMakeLists.txt
diff --git a/tests/gnupg_home/gpg-agent.conf.in b/autotests/gnupg_home/gpg-agent.conf.in
similarity index 100%
rename from tests/gnupg_home/gpg-agent.conf.in
rename to autotests/gnupg_home/gpg-agent.conf.in
diff --git a/tests/gnupg_home/gpg.conf b/autotests/gnupg_home/gpg.conf
similarity index 100%
rename from tests/gnupg_home/gpg.conf
rename to autotests/gnupg_home/gpg.conf
diff --git a/tests/gnupg_home/pinentry-fake.sh b/autotests/gnupg_home/pinentry-fake.sh
similarity index 100%
rename from tests/gnupg_home/pinentry-fake.sh
rename to autotests/gnupg_home/pinentry-fake.sh
diff --git a/tests/gnupg_home/private-keys-v1.d/01A7EA42DB00E28D85BB27378D7A47829B63FDB6.key b/autotests/gnupg_home/private-keys-v1.d/01A7EA42DB00E28D85BB27378D7A47829B63FDB6.key
similarity index 100%
rename from tests/gnupg_home/private-keys-v1.d/01A7EA42DB00E28D85BB27378D7A47829B63FDB6.key
rename to autotests/gnupg_home/private-keys-v1.d/01A7EA42DB00E28D85BB27378D7A47829B63FDB6.key
diff --git a/tests/gnupg_home/private-keys-v1.d/1AA8BA52430E51AE249AF0DA97D59F869E4101A8.key b/autotests/gnupg_home/private-keys-v1.d/1AA8BA52430E51AE249AF0DA97D59F869E4101A8.key
similarity index 100%
rename from tests/gnupg_home/private-keys-v1.d/1AA8BA52430E51AE249AF0DA97D59F869E4101A8.key
rename to autotests/gnupg_home/private-keys-v1.d/1AA8BA52430E51AE249AF0DA97D59F869E4101A8.key
diff --git a/autotests/gnupg_home/private-keys-v1.d/3BD9080DE9C88A88A67965B8E49F677004D6F6B7.key b/autotests/gnupg_home/private-keys-v1.d/3BD9080DE9C88A88A67965B8E49F677004D6F6B7.key
new file mode 100644
index 0000000..b15d6f2
--- /dev/null
+++ b/autotests/gnupg_home/private-keys-v1.d/3BD9080DE9C88A88A67965B8E49F677004D6F6B7.key
@@ -0,0 +1,15 @@
+Created: 20091125T205104
+Key: (private-key (rsa (n #00AA481E53690BD9B3B7B7E06E7BAC772D185600626E
+ C2D66143DE61C03C90959992B959C2E4BE3561B867D6665E42280CD07676421F4457F3
+ FB18627A9914C569382587A19BBB5D6142E1B2D55393E3E09818A1704F3B4A652FF029
+ 45A78E8C522F1B661F73F6E931DE4DFE22B15EA0C99E216DACB1DD8160B948B728FC84
+ 737D#)(e #010001#)(d #04AF9A620A3C80BFB0D9170C834BC235D3FB1DE4978EB60E
+ 942B0CFE98CC13C9D79F51288B59EEA723477C3F71ED99238D230D6B116834916AC207
+ 87BF29B2AB00984CFC9733917EEF87DB07D821FA4C026CE51424FCDFE877FF68B1FE90
+ EC55EA8DF4D574EB9751CF6DC7C32F3697924BD3D1F64AFEF61381AACEE1127FB071#)
+ (p #00C94B0E567403B32B73CB549B3666F57D42F9098BDB997C451F078FEA0D96972B
+ 6F88EE1D47BC6704834858DD2D92418A02E181E35E0F8F789F36197DF46B4DA9#)(q
+ #00D88F7192AB5998F69170FDF587DF949BDDD4C97F3C0EB15518A97A005D280536CD
+ C8EBC72875CED795B4365877D5E9BB9C5E65FC6F17D155A657863166AB13B5#)(u
+ #2DC9B76B9D33DE67447477173D82F54BD58D1FD83F71D37A3659B03341EAE125B49B
+ 9EB50F9047DAE7801A9309657FC25B873DFFFA379F9F3D1DDC5853661AEA#)))
diff --git a/tests/gnupg_home/private-keys-v1.d/53F70182AE3A9CFDDA3DA5B3A1742B875F43524B.key b/autotests/gnupg_home/private-keys-v1.d/53F70182AE3A9CFDDA3DA5B3A1742B875F43524B.key
similarity index 100%
rename from tests/gnupg_home/private-keys-v1.d/53F70182AE3A9CFDDA3DA5B3A1742B875F43524B.key
rename to autotests/gnupg_home/private-keys-v1.d/53F70182AE3A9CFDDA3DA5B3A1742B875F43524B.key
diff --git a/tests/gnupg_home/private-keys-v1.d/61A7BB3E7F89151CFB8B18AC27668585CE77A7A7.key b/autotests/gnupg_home/private-keys-v1.d/61A7BB3E7F89151CFB8B18AC27668585CE77A7A7.key
similarity index 100%
rename from tests/gnupg_home/private-keys-v1.d/61A7BB3E7F89151CFB8B18AC27668585CE77A7A7.key
rename to autotests/gnupg_home/private-keys-v1.d/61A7BB3E7F89151CFB8B18AC27668585CE77A7A7.key
diff --git a/tests/gnupg_home/private-keys-v1.d/B8E914E1B03F0238FF0A999E69DE8C8D1FDFFFCD.key b/autotests/gnupg_home/private-keys-v1.d/B8E914E1B03F0238FF0A999E69DE8C8D1FDFFFCD.key
similarity index 100%
rename from tests/gnupg_home/private-keys-v1.d/B8E914E1B03F0238FF0A999E69DE8C8D1FDFFFCD.key
rename to autotests/gnupg_home/private-keys-v1.d/B8E914E1B03F0238FF0A999E69DE8C8D1FDFFFCD.key
diff --git a/tests/gnupg_home/private-keys-v1.d/EC06D8C339EF73304D5B2CCF5363B437E0C915F2.key b/autotests/gnupg_home/private-keys-v1.d/EC06D8C339EF73304D5B2CCF5363B437E0C915F2.key
similarity index 100%
rename from tests/gnupg_home/private-keys-v1.d/EC06D8C339EF73304D5B2CCF5363B437E0C915F2.key
rename to autotests/gnupg_home/private-keys-v1.d/EC06D8C339EF73304D5B2CCF5363B437E0C915F2.key
diff --git a/tests/gnupg_home/pubring.gpg b/autotests/gnupg_home/pubring.gpg
similarity index 100%
rename from tests/gnupg_home/pubring.gpg
rename to autotests/gnupg_home/pubring.gpg
diff --git a/autotests/gnupg_home/pubring.kbx b/autotests/gnupg_home/pubring.kbx
new file mode 100644
index 0000000..2d8c327
Binary files /dev/null and b/autotests/gnupg_home/pubring.kbx differ
diff --git a/autotests/gnupg_home/random_seed b/autotests/gnupg_home/random_seed
new file mode 100644
index 0000000..492ab18
--- /dev/null
+++ b/autotests/gnupg_home/random_seed
@@ -0,0 +1,2 @@
+ˆ—®ôø”?úÍÝ ^±·ÍbÏ-tà ×(Œ Áòñ‰
+Å#,ǯLaJ…C S·ÀoȤ@é+ÜØ앲töâŸv_Ë[VWÅ2+&}‡aeØë7 ºKBOÄÉMO®-2{w9úÅí¯Ž8&0ÜáÌ¥ž#8(ú¦1È=cüë¢D 6"ÝX™˜$ø¤Ë¿Ú‰Ùå};J¢È©y܎ßÍøÁ๾(QQj©z#¦ÝïV@יçÃœÒcg`¥ ¾eÓ†°Ùë,75“$]Ú1˜ã§È˜~ËluÛ{*č«Îï×%N#("ÆÖ:æFU˜Ö¨ *6ÞJ ‹³ßü¯ötÿŽîáŒ¸ ®ïP¶%Ow¸æŒß…B‰\‰(fÕÎù2!ãέæ߸'˜7b]~6­.­;HóPcܝ¦m…54ùJ™7€(‰eC6z[ܘy< ØÃ¥·mÑÁ9Á–è*ó«3Ç3aœÞ¹Wˆ’#C֋‚aƒ®: ÞܦÈûfª–š¼”èi¢jã7¿Çû‚·˜koùòT½ m± ›‚¸‚jÁþ”€iÞ¸?ÒÈÑ ÎÔ DLG‡ŸþеGò.•Z¾waAy³Y›W1IOgìíåf‚·:G{ŠÙö7K3ÌM1Þméi”i˜J¹£}û*h¬® «}¶ŒÐ]ôQËÂ\×)Íy¯ŒÈ’™€Ç4dŽ¯ÚGóna@Ú©zví׮źÉÂ@N\¾«Iö:þU…ùV|V*^€–KFrÝHj
\ No newline at end of file
diff --git a/tests/gnupg_home/scdaemon.conf b/autotests/gnupg_home/scdaemon.conf
similarity index 100%
rename from tests/gnupg_home/scdaemon.conf
rename to autotests/gnupg_home/scdaemon.conf
diff --git a/tests/gnupg_home/secring.gpg b/autotests/gnupg_home/secring.gpg
similarity index 100%
rename from tests/gnupg_home/secring.gpg
rename to autotests/gnupg_home/secring.gpg
diff --git a/tests/gnupg_home/trustdb.gpg b/autotests/gnupg_home/trustdb.gpg
similarity index 100%
rename from tests/gnupg_home/trustdb.gpg
rename to autotests/gnupg_home/trustdb.gpg
diff --git a/tests/gnupg_home/trustlist.txt b/autotests/gnupg_home/trustlist.txt
similarity index 100%
rename from tests/gnupg_home/trustlist.txt
rename to autotests/gnupg_home/trustlist.txt
diff --git a/tests/setupenv.cpp b/autotests/setupenv.cpp
similarity index 100%
rename from tests/setupenv.cpp
rename to autotests/setupenv.cpp
diff --git a/tests/setupenv.h b/autotests/setupenv.h
similarity index 100%
rename from tests/setupenv.h
rename to autotests/setupenv.h
diff --git a/autotests/testdata/.gpg-id b/autotests/testdata/.gpg-id
new file mode 100644
index 0000000..2fc930b
--- /dev/null
+++ b/autotests/testdata/.gpg-id
@@ -0,0 +1,2 @@
+14B79E26050467AA
+02325448204E452A
diff --git a/autotests/testdata/mywebsite.gpg b/autotests/testdata/mywebsite.gpg
new file mode 100644
index 0000000..1b15a06
--- /dev/null
+++ b/autotests/testdata/mywebsite.gpg
@@ -0,0 +1,2 @@
+„Œ™m…4û¢­ÿcm[:©h„fðúÄÄÔ8¡Ê7r7;:„Ê¥SøJ^§¸åÁ­_Ë=ëc[U£Æ3`~Q"沸#¼ɾ+y;4`fv•<§ÔQ™ˆ‰‚d||ȍÖSjüôZ¦äI+T#ÚiXぢ÷§ºT6×'¨ßÜ¿›5¬£9œk.01ÝgÒrXca¦ÝJ¯BH4þ:I/+͞ü›˜·›øCžú?P(õk[Ö48D„€Ž]ù˜¾m šÖ·¹jQïåÌ}åðKÒ¨€ƒÝš~?ó
+ñôÓµÿúÊ(™úÃó|*(zx8AW(H>::MH
\ No newline at end of file
diff --git a/autotests/tst_passentry.cpp b/autotests/tst_passentry.cpp
new file mode 100644
index 0000000..b3316af
--- /dev/null
+++ b/autotests/tst_passentry.cpp
@@ -0,0 +1,51 @@
+// SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer <brouwer@annejan.com>
+// SPDX-FileCopyrightText: 2018 Lukas Vogel <lukedirtwalker@gmail.com>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "passentry.h"
+
+#include <QSignalSpy>
+#include <QTest>
+
+class TestPassEntry : public QObject
+{
+ Q_OBJECT
+
+private Q_SLOTS:
+ void readPassword();
+};
+
+void TestPassEntry::readPassword()
+{
+ PassEntry::NamedValue key = {QStringLiteral("key"), QStringLiteral("val")};
+ PassEntry::NamedValue key2 = {QStringLiteral("key2"), QStringLiteral("val2")};
+ QString password = QStringLiteral("password");
+
+ PassEntry entry(QStringLiteral("password\n"), {}, false);
+ QCOMPARE(entry.password(), password);
+ QCOMPARE(entry.namedValues(), {});
+ QCOMPARE(entry.remainingData(), QString());
+
+ entry = PassEntry(QStringLiteral("password"), {}, false);
+ QCOMPARE(entry.password(), password);
+ QCOMPARE(entry.namedValues(), {});
+ QCOMPARE(entry.remainingData(), QString());
+
+ entry = PassEntry(QStringLiteral("password\nfoobar\n"), {}, false);
+ QCOMPARE(entry.password(), password);
+ QCOMPARE(entry.namedValues(), {});
+ QCOMPARE(entry.remainingData(), QStringLiteral("foobar\n"));
+
+ entry = PassEntry(QStringLiteral("password\nkey: val\nkey2: val2"), {QStringLiteral("key2")}, false);
+ QCOMPARE(entry.password(), password);
+ QCOMPARE(entry.namedValues(), QList<PassEntry::NamedValue>{key2});
+ QCOMPARE(entry.remainingData(), QStringLiteral("key: val"));
+
+ entry = PassEntry(QStringLiteral("password\nkey: val\nkey2: val2"), {QStringLiteral("key2")}, true);
+ QCOMPARE(entry.password(), password);
+ QCOMPARE(entry.namedValues(), PassEntry::NamedValues({key, key2}));
+ QCOMPARE(entry.remainingData(), QString());
+}
+
+QTEST_MAIN(TestPassEntry)
+#include "tst_passentry.moc"
diff --git a/autotests/tst_passnode.cpp b/autotests/tst_passnode.cpp
new file mode 100644
index 0000000..f5bb0e7
--- /dev/null
+++ b/autotests/tst_passnode.cpp
@@ -0,0 +1,35 @@
+// SPDX-FileCopyrightText: 2024 g10 Code GmbH
+// SPDX-FileContributor: Carl Schwan <carl@carlschwan.eu>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "passentry.h"
+#include "passnode.h"
+#include <QSignalSpy>
+#include <QTest>
+
+class TestPassNode : public QObject
+{
+ Q_OBJECT
+
+private Q_SLOTS:
+ void readPassword();
+};
+
+void TestPassNode::readPassword()
+{
+ PassDatabase database(QString::fromLatin1(DATA_DIR));
+ auto node = database.node(QString::fromLatin1(DATA_DIR) + QStringLiteral("/mywebsite.gpg"));
+ QSignalSpy spy(node.get(), &PassNode::loaded);
+ node->load();
+ spy.wait();
+ PassEntry entry(node->content(), QStringList{QStringLiteral("url"), QStringLiteral("login")}, true);
+ QCOMPARE(node->content(), QStringLiteral("mypassword\nlogin: myusername\nurl: mywebsite.com\nmy notes\n"));
+ QCOMPARE(entry.password(), QStringLiteral("mypassword"));
+ QCOMPARE(entry.namedValues().count(), 2);
+ QCOMPARE(entry.namedValues().takeValue(QStringLiteral("login")), QStringLiteral("myusername"));
+ QCOMPARE(entry.namedValues().takeValue(QStringLiteral("url")), QStringLiteral("mywebsite.com"));
+ QCOMPARE(entry.remainingData(), QStringLiteral("my notes\n"));
+}
+
+QTEST_MAIN(TestPassNode)
+#include "tst_passnode.moc"
diff --git a/tests/auto/ui/tst_ui.cpp b/autotests/tst_ui.cpp
similarity index 100%
rename from tests/auto/ui/tst_ui.cpp
rename to autotests/tst_ui.cpp
diff --git a/tests/auto/model/tst_userslistmodel.cpp b/autotests/tst_userslistmodel.cpp
similarity index 89%
rename from tests/auto/model/tst_userslistmodel.cpp
rename to autotests/tst_userslistmodel.cpp
index 2510304..79b39e5 100644
--- a/tests/auto/model/tst_userslistmodel.cpp
+++ b/autotests/tst_userslistmodel.cpp
@@ -1,55 +1,58 @@
// SPDX-FileCopyrightText: 2024 g10 Code GmbH
// SPDX-FileContributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "settings.h"
#include "setupenv.h"
#include "userslistmodel.h"
#include <QAbstractItemModelTester>
#include <QObject>
#include <QSignalSpy>
#include <QTest>
using namespace Qt::StringLiterals;
class UsersListModelTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void initTestCase();
void testModel();
};
void UsersListModelTest::initTestCase()
{
Test::setupEnv();
}
void UsersListModelTest::testModel()
{
Settings::setPassStore(QString::fromLatin1(DATA_DIR));
auto model = new UsersListModel(QString::fromLatin1(DATA_DIR) + u"/hello", this);
auto tester = new QAbstractItemModelTester(model);
Q_UNUSED(tester);
QSignalSpy spy(model, &UsersListModel::finishedKeysFetching);
spy.wait();
- QCOMPARE(model->rowCount({}), 5);
+ QCOMPARE(model->rowCount({}), 6);
// valid key with secrets
QCOMPARE(model->data(model->index(0, 0), Qt::DisplayRole).toString(),
u"unittest key (no password) <test@kolab.org>\n8D9860C58F246DE6 created 11/13/09 5:33\u202FPM"_s);
QCOMPARE(model->data(model->index(0, 0), UsersListModel::NameRole).toString(), u"unittest key (no password) <test@kolab.org>"_s);
QCOMPARE(model->data(model->index(0, 0), Qt::CheckStateRole).value<Qt::CheckState>(), Qt::Unchecked);
QCOMPARE(model->data(model->index(0, 0), Qt::UserRole).value<UserInfo>().have_secret, true);
QCOMPARE(model->data(model->index(0, 0), Qt::ForegroundRole).value<QColor>(), Qt::blue);
QCOMPARE(model->data(model->index(0, 0), Qt::FontRole).value<QFont>().bold(), 1);
// no secret
QCOMPARE(model->data(model->index(2, 0), UsersListModel::NameRole).toString(), u"Leonardo Franchi (lfranchi) <lfranchi@kde.org>"_s);
QCOMPARE(model->data(model->index(2, 0), Qt::UserRole).value<UserInfo>().have_secret, false);
QCOMPARE(model->data(model->index(2, 0), Qt::ForegroundRole).value<QColor>(), Qt::white);
+
+ // key not found in keyring
+ QCOMPARE(model->index(5, 0).data(UsersListModel::NameRole).toString(), QStringLiteral(" ?? Key not found in keyring"));
}
QTEST_MAIN(UsersListModelTest)
#include "tst_userslistmodel.moc"
\ No newline at end of file
diff --git a/autotests/tst_util.cpp b/autotests/tst_util.cpp
new file mode 100644
index 0000000..11d79a9
--- /dev/null
+++ b/autotests/tst_util.cpp
@@ -0,0 +1,22 @@
+/*
+ SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer <brouwer@annejan.com>
+ SPDX-FileCopyrightText: 2018 Lukas Vogel <lukedirtwalker@gmail.com>
+
+ SPDX-License-Identifier: GPL-3.0-or-later
+*/
+#include "util.h"
+#include <QTest>
+
+class UtilTest : public QObject
+{
+ Q_OBJECT
+
+private Q_SLOTS:
+ void normalizedFolderPath()
+ {
+ QCOMPARE(Util::normalizeFolderPath(QStringLiteral("test")), QStringLiteral("test/"));
+ QCOMPARE(Util::normalizeFolderPath(QStringLiteral("test/")), QStringLiteral("test/"));
+ }
+};
+QTEST_MAIN(UtilTest)
+#include "tst_util.moc"
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 02e00d9..80d626c 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,68 +1,70 @@
# let's put most of the "meat" in a static library this way, we can also unit
# test parts of it
add_library(gpgpass_internal STATIC)
target_sources(
gpgpass_internal
PRIVATE
addfileinfoproxy.h
clipboardhelper.h
configdialog.h
deselectabletreeview.h
firsttimedialog.h
gpgmehelpers.h
mainwindow.h
pass.h
+ passentry.h
passnode.h
passwordconfiguration.h
passworddialog.h
passwordgenerator.h
qpushbuttonfactory.h
settings.h
storemodel.h
userinfo.h
usersdialog.h
userslistmodel.cpp
util.h
welcomewidget.h
addfileinfoproxy.cpp
clipboardhelper.cpp
configdialog.cpp
firsttimedialog.cpp
mainwindow.cpp
pass.cpp
+ passentry.cpp
passnode.cpp
passworddialog.cpp
passwordgenerator.cpp
settings.cpp
storemodel.cpp
usersdialog.cpp
userslistmodel.cpp
util.cpp
welcomewidget.cpp
../resources.qrc
)
ki18n_wrap_ui(gpgpass_internal
mainwindow.ui
configdialog.ui
usersdialog.ui
passworddialog.ui
userswidget.ui
welcomewidget.ui
)
target_link_libraries(gpgpass_internal
Qt::Widgets
KF${QT_MAJOR_VERSION}::Prison
KF${QT_MAJOR_VERSION}::IconThemes
KF${QT_MAJOR_VERSION}::I18n
KF${QT_MAJOR_VERSION}::WidgetsAddons
KF${QT_MAJOR_VERSION}::ItemModels
)
if (QT_MAJOR_VERSION STREQUAL "6")
target_link_libraries(gpgpass_internal QGpgmeQt6)
else()
target_link_libraries(gpgpass_internal QGpgme)
endif()
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index f17d022..277aad4 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -1,1045 +1,1054 @@
/*
SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer <brouwer@annejan.com>
SPDX-FileCopyrightText: 2016-2017 tezeb <tezeb+github@outoftheblue.pl>
SPDX-FileCopyrightText: 2018 Lukas Vogel <lukedirtwalker@gmail.com>
SPDX-FileCopyrightText: 2018 Claudio Maradonna <penguyman@stronzi.org>
SPDX-FileCopyrightText: 2019 Maciej S. Szmigiero <mail@maciej.szmigiero.name>
SPDX-FileCopyrightText: 2023 g10 Code GmbH
SPDX-FileContributor: Sune Stolborg Vuorela <sune@vuorela.dk>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "mainwindow.h"
#include <gpgpass_version.h>
#include "addfileinfoproxy.h"
#include "clipboardhelper.h"
#include "configdialog.h"
#include "pass.h"
+#include "passentry.h"
+#include "passnode.h"
#include "passworddialog.h"
#include "qpushbuttonfactory.h"
#include "settings.h"
#include "ui_mainwindow.h"
#include "usersdialog.h"
#include "util.h"
#include <KLocalizedString>
#include <KMessageWidget>
#include <KPasswordLineEdit>
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
#include <Prison/Barcode>
#else
#include <Prison/AbstractBarcode>
#include <Prison/prison.h>
#endif
#include <QCloseEvent>
#include <QComboBox>
#include <QDebug>
#include <QDirIterator>
#include <QFileInfo>
#include <QInputDialog>
#include <QLabel>
#include <QMenu>
#include <QMessageBox>
#include <QShortcut>
#include <QTextBrowser>
#include <QTimer>
static QString directoryName(const QString &dirOrFile)
{
QFileInfo fi{dirOrFile};
if (fi.isDir()) {
return fi.absoluteFilePath();
} else {
return fi.absolutePath();
}
}
/**
* @brief MainWindow::MainWindow handles all of the main functionality and also
* the main window.
* @param searchText for searching from cli
* @param parent pointer
*/
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, m_pass{std::make_unique<Pass>()}
, ui(new Ui::MainWindow)
, m_storeModel{*m_pass}
, m_database(std::make_unique<PassDatabase>(Settings::getPassStore()))
{
#ifdef __APPLE__
// extra treatment for mac os
// see http://doc.qt.io/qt-5/qkeysequence.html#qt_set_sequence_auto_mnemonic
qt_set_sequence_auto_mnemonic(true);
#endif
ui->setupUi(this);
m_clipboardHelper = new ClipboardHelper(this);
ui->mainLayout->setContentsMargins(style()->pixelMetric(QStyle::PM_LayoutLeftMargin),
style()->pixelMetric(QStyle::PM_LayoutTopMargin),
style()->pixelMetric(QStyle::PM_LayoutRightMargin),
style()->pixelMetric(QStyle::PM_LayoutBottomMargin));
m_errorMessage = new KMessageWidget();
m_errorMessage->setMessageType(KMessageWidget::Error);
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
m_errorMessage->setPosition(KMessageWidget::Position::Header);
#endif
m_errorMessage->hide();
ui->messagesArea->addWidget(m_errorMessage);
connect(m_pass.get(), &Pass::errorString, this, [this](auto str) {
m_errorMessage->setText(str);
m_errorMessage->animatedShow();
setUiElementsEnabled(true);
});
connect(m_pass.get(), &Pass::critical, this, &MainWindow::critical);
connect(m_pass.get(), &Pass::finishedInsert, this, [this]() {
this->selectTreeItem(this->getCurrentTreeViewIndex());
});
connect(m_pass.get(), &Pass::startReencryptPath, this, &MainWindow::startReencryptPath);
connect(m_pass.get(), &Pass::endReencryptPath, this, &MainWindow::endReencryptPath);
{
m_notInitialized = new KMessageWidget(i18n("Password store not initialized"));
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
m_notInitialized->setPosition(KMessageWidget::Position::Header);
#endif
m_notInitialized->setCloseButtonVisible(false);
QAction *action = new QAction(i18n("Initialize with users"));
connect(action, &QAction::triggered, this, [this]() {
this->userDialog();
});
m_notInitialized->addAction(action);
m_notInitialized->setMessageType(KMessageWidget::Error);
ui->messagesArea->addWidget(m_notInitialized);
m_notInitialized->hide();
}
{
auto w = new QWidget();
w->setLayout(new QHBoxLayout);
auto l = new QLabel(i18n("Select profile"));
auto sep = ui->toolBar->addSeparator();
w->layout()->addWidget(l);
m_profileBox = new QComboBox();
w->layout()->addWidget(m_profileBox);
m_profiles = ui->toolBar->addWidget(w);
connect(m_profiles, &QAction::changed, sep, [this, sep]() {
sep->setVisible(this->m_profiles->isVisible());
});
}
ui->copyPasswordName->hide();
connect(ui->copyPasswordName, &QPushButton::clicked, this, [this]() {
m_clipboardHelper->copyTextToClipboard(ui->passwordName->text().trimmed());
});
}
void MainWindow::changeRootItem(const QString &path)
{
m_storeModel.setRootPath(path);
}
void MainWindow::setVisible(bool visible)
{
// This originated in the ctor, but we want this to happen after the first start wizard has been run
// so for now, moved to show() on first call
if (visible && firstShow) {
firstShow = false;
// register shortcut ctrl/cmd + Q to close the main window
new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Q), this, SLOT(close()));
/*
* I added this to solve Windows bug but now on GNU/Linux the main folder,
* if hidden, disappear
*
* model.setFilter(QDir::NoDot);
*/
auto defaultPassStore = Util::findPasswordStore();
QString passStore = Settings::getPassStore(defaultPassStore);
if (passStore == defaultPassStore) {
// let's write it back
Settings::setPassStore(passStore);
}
m_storeModel.setRootPath(passStore);
ui->treeView->setModel(&m_storeModel);
ui->treeView->setColumnHidden(1, true);
ui->treeView->setColumnHidden(2, true);
ui->treeView->setColumnHidden(3, true);
ui->treeView->setHeaderHidden(true);
ui->treeView->setIndentation(15);
ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
ui->treeView->setContextMenuPolicy(Qt::CustomContextMenu);
ui->treeView->header()->setSectionResizeMode(0, QHeaderView::Stretch);
ui->treeView->sortByColumn(0, Qt::AscendingOrder);
connect(ui->treeView, &QWidget::customContextMenuRequested, this, &MainWindow::showContextMenu);
connect(ui->treeView, &DeselectableTreeView::emptyClicked, this, &MainWindow::deselect);
updateProfileBox();
clearPanelTimer.setInterval(1000 * Settings::getAutoclearPanelSeconds());
clearPanelTimer.setSingleShot(true);
connect(&clearPanelTimer, SIGNAL(timeout()), this, SLOT(clearPanel()));
searchTimer.setInterval(350);
searchTimer.setSingleShot(true);
connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch);
initToolBarButtons();
initStatusBar();
ui->lineEdit->setClearButtonEnabled(true);
setUiElementsEnabled(true);
QTimer::singleShot(10, this, SLOT(focusInput()));
verifyInitialized();
}
QMainWindow::setVisible(visible);
}
std::unique_ptr<QTextBrowser> createTextBrowserForNotes()
{
auto textBrowser = std::make_unique<QTextBrowser>();
if (Settings::isNoLineWrapping()) {
textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
}
textBrowser->setOpenExternalLinks(true);
textBrowser->setContextMenuPolicy(Qt::DefaultContextMenu);
textBrowser->document()->setDocumentMargin(0);
return textBrowser;
}
MainWindow::~MainWindow() = default;
/**
* @brief MainWindow::focusInput selects any text (if applicable) in the search
* box and sets focus to it. Allows for easy searching, called at application
* start and when receiving empty message in MainWindow::messageAvailable when
* compiled with SINGLE_APP=1 (default).
*/
void MainWindow::focusInput()
{
ui->lineEdit->selectAll();
ui->lineEdit->setFocus();
}
/**
* @brief MainWindow::changeEvent sets focus to the search box
* @param event
*/
void MainWindow::changeEvent(QEvent *event)
{
QWidget::changeEvent(event);
if (event->type() == QEvent::ActivationChange) {
if (isActiveWindow()) {
focusInput();
}
}
}
/**
* @brief MainWindow::initToolBarButtons init main ToolBar and connect actions
*/
void MainWindow::initToolBarButtons()
{
connect(ui->actionAddPassword, &QAction::triggered, this, &MainWindow::addPassword);
connect(ui->actionAddFolder, &QAction::triggered, this, &MainWindow::addFolder);
connect(ui->actionEdit, &QAction::triggered, this, &MainWindow::onEdit);
connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDelete);
connect(ui->actionUsers, &QAction::triggered, this, &MainWindow::onUsers);
connect(ui->actionConfig, &QAction::triggered, this, &MainWindow::onConfig);
connect(ui->treeView, &QTreeView::clicked, this, &MainWindow::selectTreeItem);
connect(ui->treeView, &QTreeView::doubleClicked, this, &MainWindow::editTreeItem);
connect(m_profileBox, &QComboBox::currentTextChanged, this, &MainWindow::selectProfile);
connect(ui->lineEdit, &QLineEdit::textChanged, this, &MainWindow::filterList);
connect(ui->lineEdit, &QLineEdit::returnPressed, this, &MainWindow::selectFromSearch);
ui->actionAddPassword->setIcon(QIcon::fromTheme(QStringLiteral("document-new")));
ui->actionAddFolder->setIcon(QIcon::fromTheme(QStringLiteral("folder-new")));
ui->actionEdit->setIcon(QIcon::fromTheme(QStringLiteral("document-properties")));
ui->actionDelete->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete")));
ui->actionUsers->setIcon(QIcon::fromTheme(QStringLiteral("x-office-address-book")));
ui->actionConfig->setIcon(QIcon::fromTheme(QStringLiteral("configure-symbolic")));
}
/**
* @brief MainWindow::initStatusBar init statusBar with default message and logo
*/
void MainWindow::initStatusBar()
{
ui->statusBar->showMessage(i18nc("placeholder is version number", "Welcome to GnuPG Password Manager %1", QString::fromLocal8Bit(GPGPASS_VERSION_STRING)));
QPixmap logo = QPixmap(QStringLiteral(":/artwork/32-gpgpass.png"));
QLabel *logoApp = new QLabel(statusBar());
logoApp->setPixmap(logo);
statusBar()->addPermanentWidget(logoApp);
}
const QModelIndex MainWindow::getCurrentTreeViewIndex()
{
return ui->treeView->currentIndex();
}
/**
* @brief MainWindow::config pops up the configuration screen and handles all
* inter-window communication
*/
void MainWindow::config()
{
QScopedPointer<ConfigDialog> d(new ConfigDialog(this));
d->setModal(true);
if (d->exec()) {
if (d->result() == QDialog::Accepted) {
this->show();
updateProfileBox();
changeRootItem(Settings::getPassStore());
clearPanelTimer.setInterval(1000 * Settings::getAutoclearPanelSeconds());
m_clipboardHelper->setClipboardTimer();
}
}
}
/**
* @brief MainWindow::on_treeView_clicked read the selected password file
* @param index
*/
void MainWindow::selectTreeItem(const QModelIndex &index)
{
bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags;
// TODO(bezet): "Could not decrypt";
m_clipboardHelper->clearClippedText();
QString file = index.data(QFileSystemModel::FilePathRole).toString();
ui->passwordName->setText(index.data().toString());
ui->copyPasswordName->show();
if (!file.isEmpty() && QFileInfo(file).isFile() && !cleared) {
- if (m_selectedEntry) {
- disconnect(m_selectedEntry.get(), &PassEntry::decrypted, this, &MainWindow::passShowHandler);
+ if (m_selectedNode) {
+ disconnect(m_selectedNode.get(), &PassNode::loaded, this, &MainWindow::passShowHandler);
}
- m_selectedEntry = m_database->entry(file);
- const QStringList templ = Settings::isUseTemplate() ? Settings::getPassTemplate().split(QLatin1Char('\n')) : QStringList();
- const bool allFields = Settings::isUseTemplate() && Settings::isTemplateAllFields();
- m_selectedEntry->load(templ, allFields);
- connect(m_selectedEntry.get(), &PassEntry::decrypted, this, &MainWindow::passShowHandler);
+ m_selectedNode = m_database->node(file);
+ m_selectedNode->load();
+ connect(m_selectedNode.get(), &PassNode::loaded, this, &MainWindow::passShowHandler);
ui->stackedWidget->setCurrentIndex(1);
} else {
clearPanel();
ui->actionEdit->setEnabled(false);
ui->actionDelete->setEnabled(true);
ui->stackedWidget->setCurrentIndex(0);
}
}
/**
* @brief MainWindow::on_treeView_doubleClicked when doubleclicked on
* TreeViewItem, open the edit Window
* @param index
*/
void MainWindow::editTreeItem(const QModelIndex &index)
{
QFileInfo fileOrFolder{index.data(QFileSystemModel::Roles::FilePathRole).toString()};
if (fileOrFolder.isFile()) {
editPassword(fileOrFolder.absoluteFilePath());
}
}
/**
* @brief MainWindow::deselect clear the selection, password and copy buffer
*/
void MainWindow::deselect()
{
m_clipboardHelper->clearClipboard();
ui->treeView->clearSelection();
ui->actionEdit->setEnabled(false);
ui->actionDelete->setEnabled(false);
ui->passwordName->setText(QString{});
ui->copyPasswordName->hide();
clearPanel();
ui->stackedWidget->setCurrentIndex(0);
}
void MainWindow::passShowHandler()
{
- QString password = m_selectedEntry->password();
+ const QStringList templ = Settings::isUseTemplate() ? Settings::getPassTemplate().split(QLatin1Char('\n')) : QStringList();
+ const bool allFields = Settings::isUseTemplate() && Settings::isTemplateAllFields();
+ const auto content = m_selectedNode->content();
+
+ const PassEntry entry(content, templ, allFields);
+ QString password = entry.password();
+ QString output = content;
- // set clipped text
m_clipboardHelper->setClippedText(password);
// first clear the current view:
clearPanel();
- QString output = m_selectedEntry->rawContent();
-
// show what is needed:
if (!Settings::isDisplayAsIs()) {
if (!password.isEmpty()) {
// set the password, it is hidden if needed in addToGridLayout
addToGridLayout(i18n("Password:"), password);
}
- const auto namedValues = m_selectedEntry->namedValues();
+ const auto namedValues = entry.namedValues();
for (const auto &nv : namedValues) {
addToGridLayout(i18nc("Field label", "%1:", nv.name), nv.value);
}
- output = m_selectedEntry->remainingDataForDisplay().join(QLatin1Char('\n'));
+ output = entry.remainingDataForDisplay();
}
if (Settings::isUseAutoclearPanel()) {
clearPanelTimer.start();
}
if (!output.isEmpty()) {
output = output.toHtmlEscaped();
output.replace(Util::protocolRegex(), QStringLiteral(R"(<a href="\1">\1</a>)"));
output.replace(QLatin1Char('\n'), QStringLiteral("<br />"));
auto textBrowser = createTextBrowserForNotes();
textBrowser->setHtml(output);
ui->contentLayout->addRow(new QLabel(i18nc("@label", "Notes:")), textBrowser.release());
}
setUiElementsEnabled(true);
m_errorMessage->animatedHide();
}
/**
* @brief MainWindow::clearPanel hide the information from shoulder surfers
*/
void MainWindow::clearPanel()
{
clearTemplateWidgets();
ui->passwordName->setText(QString{});
ui->copyPasswordName->hide();
}
/**
* @brief MainWindow::setUiElementsEnabled enable or disable the relevant UI
* elements
* @param state
*/
void MainWindow::setUiElementsEnabled(bool state)
{
ui->treeView->setEnabled(state);
ui->lineEdit->setEnabled(state);
ui->lineEdit->installEventFilter(this);
ui->actionAddPassword->setEnabled(state);
ui->actionAddFolder->setEnabled(state);
ui->actionUsers->setEnabled(state);
ui->actionConfig->setEnabled(state);
// is a file selected?
state &= ui->treeView->currentIndex().isValid();
ui->actionDelete->setEnabled(state);
ui->actionEdit->setEnabled(state);
}
/**
* @brief MainWindow::on_configButton_clicked run Mainwindow::config
*/
void MainWindow::onConfig()
{
config();
}
/**
* @brief Executes when the string in the search box changes, collapses the
* TreeView
* @param arg1
*/
void MainWindow::filterList(const QString &arg1)
{
ui->statusBar->showMessage(i18n("Looking for: %1", arg1), 1000);
ui->treeView->expandAll();
clearPanel();
ui->passwordName->setText(QString{});
ui->copyPasswordName->hide();
ui->actionEdit->setEnabled(false);
ui->actionDelete->setEnabled(false);
searchTimer.start();
}
/**
* @brief MainWindow::onTimeoutSearch Fired when search is finished or too much
* time from two keypresses is elapsed
*/
void MainWindow::onTimeoutSearch()
{
QString query = ui->lineEdit->text();
if (query.isEmpty()) {
ui->treeView->collapseAll();
deselect();
}
query.replace(QStringLiteral(" "), QStringLiteral(".*"));
QRegularExpression regExp(query, QRegularExpression::CaseInsensitiveOption);
m_storeModel.setFilterRegularExpression(regExp);
if (m_storeModel.rowCount() > 0 && !query.isEmpty()) {
selectFirstFile();
} else {
ui->actionEdit->setEnabled(false);
ui->actionDelete->setEnabled(false);
}
}
/**
* @brief MainWindow::on_lineEdit_returnPressed get searching
*
* Select the first possible file in the tree
*/
void MainWindow::selectFromSearch()
{
if (m_storeModel.rowCount() > 0) {
selectFirstFile();
selectTreeItem(ui->treeView->currentIndex());
}
}
/**
* @brief MainWindow::selectFirstFile select the first possible file in the
* tree
*/
void MainWindow::selectFirstFile()
{
auto model = ui->treeView->model();
auto index = firstFile(model->index(0, 0));
ui->treeView->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect);
}
/**
* @brief MainWindow::firstFile return location of first possible file
* @param parentIndex
* @return QModelIndex
*/
QModelIndex MainWindow::firstFile(QModelIndex parentIndex)
{
auto model = parentIndex.model();
int numRows = model->rowCount(parentIndex);
for (int row = 0; row < numRows; ++row) {
auto index = model->index(row, 0, parentIndex);
if (index.data(AddFileInfoProxy::FileInfoRole).value<QFileInfo>().isFile())
return index;
if (model->hasChildren(index))
return firstFile(index);
}
return parentIndex;
}
/**
* @brief MainWindow::setPassword open passworddialog
* @param file which pgp file
* @param isNew insert (not update)
*/
void MainWindow::setPassword(QString file, bool isNew)
{
- auto entry = m_database->entry(file);
- PasswordDialog d(entry.get(), isNew, this);
+ std::shared_ptr<PassNode> node;
+ if (m_selectedNode && m_selectedNode->filePath() == file) {
+ node = m_selectedNode; // reuse the same instance
+ } else {
+ node = m_database->node(file);
+ }
+
+ PasswordDialog d(node, isNew, this);
if (!d.exec()) {
ui->treeView->setFocus();
}
}
/**
* @brief MainWindow::addPassword add a new password by showing a
* number of dialogs.
*/
void MainWindow::addPassword()
{
bool ok;
QString dir = directoryName(ui->treeView->currentIndex().data(QFileSystemModel::Roles::FilePathRole).toString());
if (dir.isEmpty()) {
dir = Settings::getPassStore();
}
QString file = QInputDialog::getText(this, i18n("New file"), i18n("New password file: \n(Will be placed in %1 )", dir), QLineEdit::Normal, QString{}, &ok);
if (!ok || file.isEmpty())
return;
file = QDir(dir).absoluteFilePath(file + QStringLiteral(".gpg"));
setPassword(file);
}
/**
* @brief MainWindow::onDelete remove password, if you are
* sure.
*/
void MainWindow::onDelete()
{
QModelIndex currentIndex = ui->treeView->currentIndex();
if (!currentIndex.isValid()) {
// If not valid, we might end up passing empty string
// to delete, and taht might delete unexpected things on disk
return;
}
QFileInfo fileOrFolder{currentIndex.data(QFileSystemModel::FilePathRole).toString()};
bool isDir = fileOrFolder.isDir();
QString file = fileOrFolder.absoluteFilePath();
QString message;
if (isDir) {
message = i18nc("deleting a folder; placeholder is folder name", "Are you sure you want to delete %1 and the whole content?", file);
QDirIterator it(m_storeModel.rootPath() + QLatin1Char('/') + file, QDirIterator::Subdirectories);
while (it.hasNext()) {
it.next();
if (auto fi = it.fileInfo(); fi.isFile()) {
if (fi.suffix() != QStringLiteral("gpg")) {
message += QStringLiteral("<br><strong>")
+ i18nc("extra warning during certain folder deletions",
"Attention: "
"there are unexpected files in the given folder, "
"check them before continue")
+ QStringLiteral("</strong>");
break;
}
}
}
} else {
message = i18nc("deleting a file; placeholder is file name", "Are you sure you want to delete %1?", file);
}
if (QMessageBox::question(this, isDir ? i18n("Delete folder?") : i18n("Delete password?"), message, QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
return;
m_pass->Remove(file, isDir);
}
/**
* @brief MainWindow::onEdit try and edit (selected) password.
*/
void MainWindow::onEdit()
{
QString file = ui->treeView->currentIndex().data(QFileSystemModel::FilePathRole).toString();
if (!file.isEmpty()) {
editPassword(file);
}
}
/**
* @brief MainWindow::userDialog see MainWindow::onUsers()
* @param dir folder to edit users for.
*/
void MainWindow::userDialog(QString dir)
{
if (dir.isEmpty()) {
dir = Settings::getPassStore();
}
QFileInfo fi(dir);
if (!fi.isDir()) {
dir = fi.absolutePath();
}
UsersDialog d(dir, *m_pass, this);
if (!d.exec()) {
ui->treeView->setFocus();
}
verifyInitialized();
}
/**
* @brief MainWindow::onUsers edit users for the current
* folder,
* gets lists and opens UserDialog.
*/
void MainWindow::onUsers()
{
QString dir = ui->treeView->currentIndex().data(QFileSystemModel::Roles::FilePathRole).toString();
if (dir.isEmpty()) {
dir = Settings::getPassStore();
} else {
QFileInfo fi(dir);
if (!fi.isDir()) {
dir = fi.absolutePath();
}
dir = Util::normalizeFolderPath(dir);
}
userDialog(dir);
}
/**
* @brief MainWindow::updateProfileBox update the list of profiles, optionally
* select a more appropriate one to view too
*/
void MainWindow::updateProfileBox()
{
QHash<QString, QString> profiles = Settings::getProfiles();
if (profiles.isEmpty()) {
m_profiles->setVisible(false);
} else {
m_profiles->setVisible(true);
m_profileBox->setEnabled(profiles.size() > 1);
m_profileBox->clear();
QHashIterator<QString, QString> i(profiles);
while (i.hasNext()) {
i.next();
if (!i.key().isEmpty())
m_profileBox->addItem(i.key());
}
m_profileBox->model()->sort(0);
}
int index = m_profileBox->findText(Settings::getProfile());
if (index != -1) // -1 for not found
m_profileBox->setCurrentIndex(index);
}
/**
* @brief MainWindow::on_profileBox_currentIndexChanged make sure we show the
* correct "profile"
* @param name
*/
void MainWindow::selectProfile(QString name)
{
if (name == Settings::getProfile())
return;
ui->lineEdit->clear();
clearPanel();
Settings::setProfile(name);
Settings::setPassStore(Settings::getProfiles().value(name));
m_database = std::make_unique<PassDatabase>(Settings::getPassStore());
ui->statusBar->showMessage(i18n("Profile changed to %1", name), 2000);
ui->treeView->selectionModel()->clear();
changeRootItem(Settings::getPassStore());
verifyInitialized();
ui->actionEdit->setEnabled(false);
ui->actionDelete->setEnabled(false);
}
void MainWindow::verifyInitialized()
{
bool actionsEnabled;
if (!QFile::exists(Settings::getPassStore() + QStringLiteral("/.gpg-id"))) {
m_notInitialized->animatedShow();
actionsEnabled = false;
} else {
m_notInitialized->animatedHide();
actionsEnabled = true;
}
ui->actionAddFolder->setEnabled(actionsEnabled);
ui->actionAddPassword->setEnabled(actionsEnabled);
ui->actionDelete->setEnabled(ui->actionDelete->isEnabled() && actionsEnabled);
ui->actionEdit->setEnabled(ui->actionEdit->isEnabled() && actionsEnabled);
}
/**
* @brief MainWindow::closeEvent hide or quit
* @param event
*/
void MainWindow::closeEvent(QCloseEvent *event)
{
m_clipboardHelper->clearClipboard();
event->accept();
}
/**
* @brief MainWindow::eventFilter filter out some events and focus the
* treeview
* @param obj
* @param event
* @return
*/
bool MainWindow::eventFilter(QObject *obj, QEvent *event)
{
if (obj == ui->lineEdit && event->type() == QEvent::KeyPress) {
auto *key = dynamic_cast<QKeyEvent *>(event);
if (key != nullptr && key->key() == Qt::Key_Down) {
ui->treeView->setFocus();
}
}
return QObject::eventFilter(obj, event);
}
/**
* @brief MainWindow::keyPressEvent did anyone press return, enter or escape?
* @param event
*/
void MainWindow::keyPressEvent(QKeyEvent *event)
{
switch (event->key()) {
case Qt::Key_Delete:
onDelete();
break;
case Qt::Key_Return:
case Qt::Key_Enter:
if (m_storeModel.rowCount() > 0)
selectTreeItem(ui->treeView->currentIndex());
break;
case Qt::Key_Escape:
ui->lineEdit->clear();
break;
default:
break;
}
}
/**
* @brief MainWindow::showContextMenu show us the (file or folder) context
* menu
* @param pos
*/
void MainWindow::showContextMenu(const QPoint &pos)
{
QModelIndex index = ui->treeView->indexAt(pos);
bool selected = true;
if (!index.isValid()) {
ui->treeView->clearSelection();
ui->actionDelete->setEnabled(false);
ui->actionEdit->setEnabled(false);
selected = false;
}
ui->treeView->setCurrentIndex(index);
QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos);
QFileInfo fileOrFolder = ui->treeView->currentIndex().data(AddFileInfoProxy::FileInfoRole).value<QFileInfo>();
QMenu contextMenu;
if (!selected || fileOrFolder.isDir()) {
const QAction *addFolderAction = contextMenu.addAction(i18n("Add folder"));
const QAction *addPasswordAction = contextMenu.addAction(i18n("Add password"));
const QAction *usersAction = contextMenu.addAction(i18n("Users"));
connect(addFolderAction, &QAction::triggered, this, &MainWindow::addFolder);
connect(addPasswordAction, &QAction::triggered, this, &MainWindow::addPassword);
connect(usersAction, &QAction::triggered, this, &MainWindow::onUsers);
} else if (fileOrFolder.isFile()) {
const QAction *edit = contextMenu.addAction(i18n("Edit"));
connect(edit, &QAction::triggered, this, &MainWindow::onEdit);
}
if (selected) {
contextMenu.addSeparator();
if (fileOrFolder.isDir()) {
const QAction *renameFolderAction = contextMenu.addAction(i18n("Rename folder"));
connect(renameFolderAction, &QAction::triggered, this, &MainWindow::renameFolder);
} else if (fileOrFolder.isFile()) {
const QAction *renamePasswordAction = contextMenu.addAction(i18n("Rename password"));
connect(renamePasswordAction, &QAction::triggered, this, &MainWindow::renamePassword);
}
const QAction *deleteItem = contextMenu.addAction(i18n("Delete"));
connect(deleteItem, &QAction::triggered, this, &MainWindow::onDelete);
}
contextMenu.exec(globalPos);
}
/**
* @brief MainWindow::addFolder add a new folder to store passwords in
*/
void MainWindow::addFolder()
{
bool ok;
QString dir = directoryName(ui->treeView->currentIndex().data(QFileSystemModel::FilePathRole).toString());
if (dir.isEmpty()) {
dir = Settings::getPassStore();
}
QString newdir = QInputDialog::getText(this, i18n("New file"), i18n("New Folder: \n(Will be placed in %1 )", dir), QLineEdit::Normal, QString{}, &ok);
if (!ok || newdir.isEmpty())
return;
QDir(dir).mkdir(newdir);
}
/**
* @brief MainWindow::renameFolder rename an existing folder
*/
void MainWindow::renameFolder()
{
bool ok;
QString srcDir = QDir::cleanPath(directoryName(ui->treeView->currentIndex().data(QFileSystemModel::FilePathRole).toString()));
if (srcDir.isEmpty()) {
return;
}
QString srcDirName = QDir(srcDir).dirName();
QString newName = QInputDialog::getText(this, i18n("Rename file"), i18n("Rename Folder To: "), QLineEdit::Normal, srcDirName, &ok);
if (!ok || newName.isEmpty())
return;
QString destDir = srcDir;
destDir.replace(srcDir.lastIndexOf(srcDirName), srcDirName.length(), newName);
m_pass->Move(srcDir, destDir);
}
/**
* @brief MainWindow::editPassword read password and open edit window via
* MainWindow::onEdit()
*/
void MainWindow::editPassword(const QString &file)
{
if (!file.isEmpty()) {
setPassword(file, false);
}
}
/**
* @brief MainWindow::renamePassword rename an existing password
*/
void MainWindow::renamePassword()
{
bool ok;
QString file = ui->treeView->currentIndex().data(QFileSystemModel::FilePathRole).toString();
QString filePath = QFileInfo(file).path();
QString fileName = QFileInfo(file).fileName();
if (fileName.endsWith(QStringLiteral(".gpg"), Qt::CaseInsensitive))
fileName.chop(4);
QString newName = QInputDialog::getText(this, i18n("Rename file"), i18n("Rename File To: "), QLineEdit::Normal, fileName, &ok);
if (!ok || newName.isEmpty())
return;
QString newFile = QDir(filePath).filePath(newName);
m_pass->Move(file, newFile);
}
/**
* @brief MainWindow::clearTemplateWidgets empty the template widget fields in
* the UI
*/
void MainWindow::clearTemplateWidgets()
{
while (ui->contentLayout->count() > 0) {
ui->contentLayout->removeRow(ui->contentLayout->rowCount() - 1);
}
}
/**
* @brief MainWindow::addToGridLayout add a field to the template grid
* @param position
* @param field
* @param value
*/
void MainWindow::addToGridLayout(const QString &field, const QString &value)
{
QString trimmedField = field.trimmed();
QString trimmedValue = value.trimmed();
// Combine the Copy button and the line edit in one widget
auto rowLayout = new QHBoxLayout();
rowLayout->setContentsMargins(0, 2, 0, 2);
if (trimmedField == i18n("Password:")) {
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
auto *line = new KPasswordLineEdit();
line->setRevealPasswordMode(KPassword::RevealMode::Always);
line->setReadOnly(true);
line->setPassword(trimmedValue);
#else
auto *line = new QLineEdit();
line->setText(trimmedValue);
auto iconOn = QIcon::fromTheme(QStringLiteral("password-show-on"));
auto iconOff = QIcon::fromTheme(QStringLiteral("password-show-off"));
auto action = line->addAction(iconOn, QLineEdit::TrailingPosition);
action->setCheckable(true);
action->setText(i18n("Toggle password visibility"));
connect(action, &QAction::triggered, this, [line, action, iconOn, iconOff]() {
if (line->echoMode() == QLineEdit::Password) {
line->setEchoMode(QLineEdit::Normal);
action->setIcon(iconOff);
} else {
line->setEchoMode(QLineEdit::Password);
action->setIcon(iconOn);
}
});
#endif
line->setObjectName(trimmedField);
line->setContentsMargins(0, 0, 0, 0);
line->setEchoMode(QLineEdit::Password);
rowLayout->addWidget(line);
} else {
auto *line = new QLabel();
line->setOpenExternalLinks(true);
line->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard);
line->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum));
line->setObjectName(trimmedField);
trimmedValue.replace(Util::protocolRegex(), QStringLiteral(R"(<a href="\1">\1</a>)"));
line->setText(trimmedValue);
line->setContentsMargins(5, 0, 0, 0);
rowLayout->addWidget(line);
}
auto fieldName = trimmedField;
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
fieldName.removeLast(); // remove ':' from the end of the label
#else
fieldName.remove(fieldName.count() - 1, 1); // remove ':' from the end of the label
#endif
auto fieldLabel =
createPushButton(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy '%1' to clipboard", fieldName), m_clipboardHelper, [this, trimmedValue] {
m_clipboardHelper->copyTextToClipboard(trimmedValue);
});
rowLayout->addWidget(fieldLabel.release());
auto qrButton =
createPushButton(QIcon::fromTheme(QStringLiteral("view-barcode-qr")), i18n("View '%1' QR Code", fieldName), m_clipboardHelper, [this, trimmedValue]() {
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
auto barcode = Prison::Barcode::create(Prison::QRCode);
#else
auto barcode = Prison::createBarcode(Prison::BarcodeType::QRCode);
#endif
if (!barcode) {
return;
}
barcode->setData(trimmedValue);
auto image = barcode->toImage(barcode->preferredSize(window()->devicePixelRatioF()));
QDialog popup(nullptr, Qt::Popup | Qt::FramelessWindowHint);
QVBoxLayout *layout = new QVBoxLayout;
QLabel *popupLabel = new QLabel();
layout->addWidget(popupLabel);
popupLabel->setPixmap(QPixmap::fromImage(image));
popupLabel->setScaledContents(true);
popupLabel->show();
popup.setLayout(layout);
popup.move(QCursor::pos());
popup.exec();
});
rowLayout->addWidget(qrButton.release());
// set into the layout
ui->contentLayout->addRow(trimmedField, rowLayout);
}
/**
* @brief MainWindow::startReencryptPath disable ui elements and treeview
*/
void MainWindow::startReencryptPath()
{
statusBar()->showMessage(i18n("Re-encrypting folders"), 3000);
setUiElementsEnabled(false);
ui->treeView->setDisabled(true);
}
/**
* @brief MainWindow::endReencryptPath re-enable ui elements
*/
void MainWindow::endReencryptPath()
{
setUiElementsEnabled(true);
}
/**
* @brief MainWindow::critical critical message popup wrapper.
* @param title
* @param msg
*/
void MainWindow::critical(const QString &title, const QString &msg)
{
QMessageBox::critical(this, title, msg);
}
diff --git a/src/mainwindow.h b/src/mainwindow.h
index 87fe2df..496ae1d 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -1,134 +1,134 @@
/*
SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer <brouwer@annejan.com>
SPDX-FileCopyrightText: 2016-2017 tezeb <tezeb+github@outoftheblue.pl>
SPDX-FileCopyrightText: 2018 Lukas Vogel <lukedirtwalker@gmail.com>
SPDX-FileCopyrightText: 2018 Claudio Maradonna <penguyman@stronzi.org>
SPDX-FileCopyrightText: 2019 Maciej S. Szmigiero <mail@maciej.szmigiero.name>
SPDX-FileCopyrightText: 2023 g10 Code GmbH
SPDX-FileContributor: Sune Stolborg Vuorela <sune@vuorela.dk>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#ifndef MAINWINDOW_H_
#define MAINWINDOW_H_
#include "passnode.h"
#include "storemodel.h"
#include <KSelectionProxyModel>
#include <QFileSystemModel>
#include <QItemSelectionModel>
#include <QMainWindow>
#include <QProcess>
#include <QTimer>
#ifdef __APPLE__
// http://doc.qt.io/qt-5/qkeysequence.html#qt_set_sequence_auto_mnemonic
void qt_set_sequence_auto_mnemonic(bool b);
#endif
namespace Ui
{
class MainWindow;
}
/*!
\class MainWindow
\brief The MainWindow class does way too much, not only is it a switchboard,
configuration handler and more, it's also the process-manager.
This class could really do with an overhaul.
*/
class QComboBox;
class ClipboardHelper;
class Pass;
class KMessageWidget;
class AddFileInfoProxy;
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
void restoreWindow();
void userDialog(QString = {});
void config();
void setUiElementsEnabled(bool state);
const QModelIndex getCurrentTreeViewIndex();
void setVisible(bool visible) override;
protected:
void closeEvent(QCloseEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
void changeEvent(QEvent *event) override;
bool eventFilter(QObject *obj, QEvent *event) override;
public Q_SLOTS:
void deselect();
void critical(const QString &, const QString &);
void passShowHandler();
void selectTreeItem(const QModelIndex &index);
void startReencryptPath();
void endReencryptPath();
private Q_SLOTS:
void addPassword();
void addFolder();
void onEdit();
void onDelete();
void onUsers();
void onConfig();
void editTreeItem(const QModelIndex &index);
void clearPanel();
void filterList(const QString &arg1);
void selectFromSearch();
void selectProfile(QString);
void showContextMenu(const QPoint &pos);
void renameFolder();
void editPassword(const QString &);
void renamePassword();
void focusInput();
void onTimeoutSearch();
void verifyInitialized();
void changeRootItem(const QString &path);
private:
std::unique_ptr<Pass> m_pass;
ClipboardHelper *m_clipboardHelper;
QScopedPointer<Ui::MainWindow> ui;
StoreModel m_storeModel;
QTimer clearPanelTimer, searchTimer;
KMessageWidget *m_notInitialized;
KMessageWidget *m_errorMessage;
QAction *m_profiles;
QComboBox *m_profileBox;
std::unique_ptr<PassDatabase> m_database;
- std::unique_ptr<PassEntry> m_selectedEntry;
+ std::shared_ptr<PassNode> m_selectedNode;
bool firstShow = true;
void initToolBarButtons();
void initStatusBar();
void updateText();
void selectFirstFile();
QModelIndex firstFile(QModelIndex parentIndex);
void setPassword(QString, bool isNew = true);
void updateProfileBox();
void initTrayIcon();
void destroyTrayIcon();
void clearTemplateWidgets();
void reencryptPath(QString dir);
void addToGridLayout(const QString &field, const QString &value);
};
#endif // MAINWINDOW_H_
diff --git a/src/passentry.cpp b/src/passentry.cpp
new file mode 100644
index 0000000..840b744
--- /dev/null
+++ b/src/passentry.cpp
@@ -0,0 +1,89 @@
+// SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer <brouwer@annejan.com>
+// SPDX-FileCopyrightText: 2017 Jason A. Donenfeld <Jason@zx2c4.com>
+// SPDX-FileCopyrightText: 2020 Charlie Waters <cawiii@me.com>
+// SPDX-FileCopyrightText: 2023 g10 Code GmbH
+// SPDX-FileContributor: Sune Stolborg Vuorela <sune@vuorela.dk>
+// SPDX-FileContributor: Carl Schwan <carl@carlschwan.eu>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "passentry.h"
+
+namespace
+{
+bool isLineHidden(const QString &line)
+{
+ return line.startsWith(QStringLiteral("otpauth://"), Qt::CaseInsensitive);
+}
+}
+
+PassEntry::PassEntry(const QString &content, const QStringList &templateFields, bool allFields)
+{
+ auto lines = content.split(QLatin1Char('\n'));
+ m_password = lines.takeFirst();
+ QStringList remainingData;
+ QStringList remainingDataForDisplay;
+ for (const QString &line : std::as_const(lines)) {
+ if (line.contains(QLatin1Char(':'))) {
+ int colon = line.indexOf(QLatin1Char(':'));
+ const QString name = line.left(colon);
+ const QString value = line.right(line.length() - colon - 1);
+ if ((allFields && !value.startsWith(QStringLiteral("//"))) // if value startswith // colon is probably from a url
+ || templateFields.contains(name)) {
+ m_namedValues.append({name.trimmed(), value.trimmed()});
+ continue;
+ }
+ }
+
+ remainingData.append(line);
+ if (!isLineHidden(line)) {
+ remainingDataForDisplay.append(line);
+ }
+ }
+ m_remainingData = remainingData.join(u'\n');
+ m_remainingDataForDisplay = remainingDataForDisplay.join(u'\n');
+}
+
+QString PassEntry::password() const
+{
+ return m_password;
+}
+
+PassEntry::NamedValues PassEntry::namedValues() const
+{
+ return m_namedValues;
+}
+
+QString PassEntry::remainingData() const
+{
+ return m_remainingData;
+}
+
+QString PassEntry::remainingDataForDisplay() const
+{
+ return m_remainingDataForDisplay;
+}
+
+PassEntry::NamedValues::NamedValues()
+ : QList()
+{
+}
+
+PassEntry::NamedValues::NamedValues(std::initializer_list<NamedValue> values)
+ : QList(values)
+{
+}
+
+QString PassEntry::NamedValues::takeValue(const QString &name)
+{
+ for (int i = 0; i < length(); ++i) {
+ if (at(i).name == name) {
+ return takeAt(i).value;
+ }
+ }
+ return QString();
+}
+
+bool operator==(const PassEntry::NamedValue &a, const PassEntry::NamedValue &b)
+{
+ return a.name == b.name && a.value == b.value;
+}
diff --git a/src/passentry.h b/src/passentry.h
new file mode 100644
index 0000000..56c9e5d
--- /dev/null
+++ b/src/passentry.h
@@ -0,0 +1,65 @@
+// SPDX-FileCopyrightText: 2024 g10 Code GmbH
+// SPDX-FileContributor: Carl Schwan <carl@carlschwan.eu>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QList>
+#include <QString>
+
+/// This class represent a decrypted pass entry.
+class PassEntry
+{
+public:
+ /// \param templateFields the fields in the template. Fields in the
+ /// template will always be in namedValues() at the beginning of the
+ /// list in the same order.
+ ///
+ /// \param allFields whether all fields should be considered as named
+ /// values. If set to false only templateFields are returned in
+ /// namedValues().
+ explicit PassEntry(const QString &content, const QStringList &templateFields, bool allFields);
+
+ struct NamedValue {
+ QString name;
+ QString value;
+ };
+
+ /// \brief The NamedValues class is mostly a list of NamedValue but also
+ /// has a method to take a specific NamedValue pair out of the list.
+ class NamedValues : public QList<NamedValue>
+ {
+ public:
+ NamedValues();
+ NamedValues(std::initializer_list<NamedValue> values);
+
+ QString takeValue(const QString &name);
+ };
+
+ /// \return the password from the parsed file.
+ QString password() const;
+
+ /// \return the named values in the file in the order of appearence, with
+ /// template values first.
+ NamedValues namedValues() const;
+
+ /// \return the data that is not the password and not in namedValues.
+ QString remainingData() const;
+
+ /// Same as remainingData but without data that should not be displayed
+ /// (like a TOTP secret).
+ QString remainingDataForDisplay() const;
+
+Q_SIGNALS:
+ void decrypted();
+ void encrypted();
+ void errorOccurred(const QString &title, const QString &message);
+
+private:
+ QString m_password;
+ QString m_remainingData;
+ QString m_remainingDataForDisplay;
+ NamedValues m_namedValues;
+};
+
+bool operator==(const PassEntry::NamedValue &a, const PassEntry::NamedValue &b);
diff --git a/src/passnode.cpp b/src/passnode.cpp
index 4e77a21..663f212 100644
--- a/src/passnode.cpp
+++ b/src/passnode.cpp
@@ -1,160 +1,160 @@
// SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer <brouwer@annejan.com>
// SPDX-FileCopyrightText: 2017 Jason A. Donenfeld <Jason@zx2c4.com>
// SPDX-FileCopyrightText: 2020 Charlie Waters <cawiii@me.com>
// SPDX-FileCopyrightText: 2023 g10 Code GmbH
// SPDX-FileContributor: Sune Stolborg Vuorela <sune@vuorela.dk>
// SPDX-FileContributor: Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "passnode.h"
+#include <QDir>
#include <QFile>
+#include <QFileInfo>
+#include <QSaveFile>
+
+#include <KLocalizedString>
#include <QGpgME/DecryptJob>
+#include <QGpgME/EncryptJob>
#include <QGpgME/Protocol>
+
#include <gpgme++/decryptionresult.h>
+#include <gpgme++/encryptionresult.h>
#include "gpgmehelpers.h"
-static bool isLineHidden(const QString &line)
-{
- return line.startsWith(QStringLiteral("otpauth://"), Qt::CaseInsensitive);
-}
-
PassDatabase::PassDatabase(const QString &directory, QObject *parent)
: QObject(parent)
, m_directory(directory)
{
}
-PassEntry::PassEntry(const QString &filePath, PassDatabase *database)
- : QObject(nullptr)
- , m_filePath(filePath)
- , m_database(database)
-{
-}
-
-std::unique_ptr<PassEntry> PassDatabase::entry(const QString &filePath)
+std::unique_ptr<PassNode> PassDatabase::node(const QString &filePath)
{
Q_ASSERT(filePath.startsWith(m_directory));
Q_ASSERT(filePath.endsWith(QStringLiteral(".gpg")));
- return std::unique_ptr<PassEntry>(new PassEntry(filePath, this));
+ return std::unique_ptr<PassNode>(new PassNode(filePath, recipientsForFile(filePath), this));
}
-QString PassEntry::filePath() const
+QStringList PassDatabase::recipientsForFile(const QString &file)
{
- return m_filePath;
+ QDir gpgIdPath(QFileInfo(file).absoluteDir());
+ bool found = false;
+ while (gpgIdPath.exists() && gpgIdPath.absolutePath().startsWith(m_directory)) {
+ if (QFile(gpgIdPath.absoluteFilePath(QStringLiteral(".gpg-id"))).exists()) {
+ found = true;
+ break;
+ }
+ if (!gpgIdPath.cdUp())
+ break;
+ }
+ QFile gpgId(found ? gpgIdPath.absoluteFilePath(QStringLiteral(".gpg-id")) : m_directory + QStringLiteral(".gpg-id"));
+ if (!gpgId.open(QIODevice::ReadOnly | QIODevice::Text))
+ return QStringList();
+ QStringList recipients;
+ while (!gpgId.atEnd()) {
+ const QString recipient(QString::fromLocal8Bit(gpgId.readLine()).trimmed());
+ if (!recipient.isEmpty()) {
+ recipients += recipient;
+ }
+ }
+ return recipients;
}
-bool PassEntry::isDecrypted() const
+PassNode::PassNode(const QString &filePath, const QStringList &recipients, PassDatabase *database)
+ : QObject(nullptr)
+ , m_filePath(filePath)
+ , m_recipients(recipients)
+ , m_database(database)
{
- return m_isDecrypted;
}
-void PassEntry::load(const QStringList &templateFields, bool allFields)
+void PassNode::load()
{
+ const QString loadErrorTitle = i18nc("@info:status", "Unable to load password \"%1\"", m_filePath);
QFile f(m_filePath);
if (!f.open(QIODevice::ReadOnly)) {
- Q_EMIT m_database->errorOccurred(f.errorString());
+ Q_EMIT errorOccurred(loadErrorTitle, f.errorString());
return;
}
const auto data = f.readAll();
QGpgME::Protocol *protocol = QGpgME::openpgp();
auto job = protocol->decryptJob();
- connect(job, &QGpgME::DecryptJob::result, this, [this, templateFields, allFields](auto &&result, QByteArray plainText, QString auditLog, auto &&logError) {
+ connect(job, &QGpgME::DecryptJob::result, this, [this, loadErrorTitle](auto &&result, QByteArray plainText, QString auditLog, auto &&logError) {
if (isSuccess(result.error())) {
m_content = QString::fromUtf8(plainText);
- parse(templateFields, allFields);
m_isDecrypted = true;
- Q_EMIT decrypted();
+ Q_EMIT loaded();
} else {
- Q_EMIT m_database->errorOccurred(QString::fromLocal8Bit(result.error().asString()));
+ Q_EMIT errorOccurred(loadErrorTitle, QString::fromLocal8Bit(result.error().asString()));
}
Q_UNUSED(logError);
Q_UNUSED(auditLog);
});
job->start(data);
}
-void PassEntry::save()
+void PassNode::save()
{
-}
-
-void PassEntry::parse(const QStringList &templateFields, bool allFields)
-{
- auto lines = m_content.split(QLatin1Char('\n'));
- m_password = lines.takeFirst();
- for (const QString &line : std::as_const(lines)) {
- if (line.contains(QLatin1Char(':'))) {
- int colon = line.indexOf(QLatin1Char(':'));
- const QString name = line.left(colon);
- const QString value = line.right(line.length() - colon - 1);
- if ((allFields && !value.startsWith(QStringLiteral("//"))) // if value startswith // colon is probably from a url
- || templateFields.contains(name)) {
- m_namedValues.append({name.trimmed(), value.trimmed()});
- continue;
- }
- }
-
- m_remainingData.append(line);
- if (!isLineHidden(line)) {
- m_remainingDataForDisplay.append(line);
+ QGpgME::Protocol *protocol = QGpgME::openpgp();
+ auto job = protocol->encryptJob();
+ std::vector<GpgME::Key> keys;
+ auto ctx = QGpgME::Job::context(job);
+ for (const auto &keyId : std::as_const(m_recipients)) {
+ GpgME::Error error;
+ auto key = ctx->key(keyId.toUtf8().data(), error, false);
+ if (!error && !key.isNull()) {
+ keys.push_back(key);
}
}
-}
-
-QString PassEntry::password() const
-{
- Q_ASSERT(m_isDecrypted);
- return m_password;
-}
+ if (keys.empty()) {
+ Q_EMIT errorOccurred(i18n("Can not edit"), i18n("Could not read encryption key to use, .gpg-id file missing or invalid."));
+ return;
+ }
-PassEntry::NamedValues PassEntry::namedValues() const
-{
- Q_ASSERT(m_isDecrypted);
- return m_namedValues;
+ connect(job, &QGpgME::EncryptJob::result, this, [this](auto &&result, const QByteArray &ciphertext, const QString &log, auto &&auditResult) {
+ if (isSuccess(result.error())) {
+ QSaveFile f(m_filePath);
+ bool open = f.open(QIODevice::WriteOnly | QIODevice::Truncate);
+ if (!open) {
+ Q_EMIT errorOccurred(i18nc("@info:status", "Unable to save password"), f.errorString());
+ return;
+ }
+ f.write(ciphertext);
+ if (f.error() == QFileDevice::NoError) {
+ f.commit();
+ Q_EMIT saved();
+ } else {
+ Q_EMIT errorOccurred(i18nc("@info:status", "Unable to save password"), f.errorString());
+ }
+ } else {
+ Q_EMIT errorOccurred(i18nc("@info:status", "Unable to save password"), QString::fromUtf8(result.error().asString()));
+ }
+ Q_UNUSED(log);
+ Q_UNUSED(auditResult);
+ });
+ job->start(keys, m_content.toUtf8());
}
-QStringList PassEntry::remainingData() const
+QString PassNode::filePath() const
{
- Q_ASSERT(m_isDecrypted);
- return m_remainingData;
+ return m_filePath;
}
-QStringList PassEntry::remainingDataForDisplay() const
+bool PassNode::isDecrypted() const
{
- Q_ASSERT(m_isDecrypted);
- return m_remainingDataForDisplay;
+ return m_isDecrypted;
}
-QString PassEntry::rawContent() const
+QString PassNode::content() const
{
return m_content;
}
-void PassEntry::setRawContent(const QString &rawContent)
+void PassNode::setContent(const QString &content)
{
- m_content = rawContent;
-}
-
-PassEntry::NamedValues::NamedValues()
- : QList()
-{
-}
-
-PassEntry::NamedValues::NamedValues(std::initializer_list<NamedValue> values)
- : QList(values)
-{
-}
-
-QString PassEntry::NamedValues::takeValue(const QString &name)
-{
- for (int i = 0; i < length(); ++i) {
- if (at(i).name == name) {
- return takeAt(i).value;
- }
- }
- return QString();
+ m_content = content;
}
diff --git a/src/passnode.h b/src/passnode.h
index dc7aff1..9bf76af 100644
--- a/src/passnode.h
+++ b/src/passnode.h
@@ -1,106 +1,62 @@
// SPDX-FileCopyrightText: 2024 g10 Code GmbH
// SPDX-FileContributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QObject>
#include <QString>
-class PassEntry;
+class PassNode;
/// Abstraction for the pass storage.
class PassDatabase : public QObject
{
Q_OBJECT
public:
explicit PassDatabase(const QString &directory, QObject *parent = nullptr);
- std::unique_ptr<PassEntry> entry(const QString &filePath);
-
-Q_SIGNALS:
- void errorOccurred(const QString &message);
+ std::unique_ptr<PassNode> node(const QString &filePath);
private:
+ QStringList recipientsForFile(const QString &file);
+
QString m_directory;
};
-/// This class represent a pass entry.
-class PassEntry : public QObject
+class PassNode : public QObject
{
Q_OBJECT
+
public:
- struct NamedValue {
- QString name;
- QString value;
- };
-
- /// \brief The NamedValues class is mostly a list of NamedValue but also
- /// has a method to take a specific NamedValue pair out of the list.
- class NamedValues : public QList<NamedValue>
- {
- public:
- NamedValues();
- NamedValues(std::initializer_list<NamedValue> values);
-
- QString takeValue(const QString &name);
- };
-
- /// Decrypt and parse the pass entry. Once executed isDecrypted() will be set
+ explicit PassNode(const QString &filePath, const QStringList &recipients, PassDatabase *database);
+
+ /// Decrypt the pass entry. Once executed isDecrypted() will be set
/// to true.
- ///
- /// \param templateFields the fields in the template. Fields in the
- /// template will always be in namedValues() at the beginning of the
- /// list in the same order.
- ///
- /// \param allFields whether all fields should be considered as named
- /// values. If set to false only templateFields are returned in
- /// namedValues().
- void load(const QStringList &templateFields, bool allFields);
+ void load();
/// Encrypt back the in-memory stored content to the file.
void save();
/// The location of the node entry in the file system.
QString filePath() const;
/// Whether this file is decrypted.
bool isDecrypted() const;
- /// The raw content of the pass entry.
- QString rawContent() const;
- void setRawContent(const QString &content);
-
- /// \return the password from the parsed file.
- QString password() const;
-
- /// \return the named values in the file in the order of appearence, with
- /// template values first.
- NamedValues namedValues() const;
-
- /// \return the data that is not the password and not in getNamedValues.
- QStringList remainingData() const;
-
- /// Same as remainingData but without data that should not be displayed
- /// (like a TOTP secret).
- QStringList remainingDataForDisplay() const;
+ /// The raw content of the pass node.
+ QString content() const;
+ void setContent(const QString &content);
Q_SIGNALS:
- void decrypted();
- void encrypted();
+ void loaded();
+ void saved();
+ void errorOccurred(const QString &title, const QString &message);
private:
- friend PassDatabase;
- explicit PassEntry(const QString &filePath, PassDatabase *database);
-
- void parse(const QStringList &templateFields, bool allFields);
-
const QString m_filePath;
+ QStringList m_recipients;
PassDatabase *const m_database;
QString m_content;
bool m_isDecrypted = false;
- QString m_password;
- QStringList m_remainingData;
- QStringList m_remainingDataForDisplay;
- NamedValues m_namedValues;
};
diff --git a/src/passworddialog.cpp b/src/passworddialog.cpp
index e10789b..e686ae1 100644
--- a/src/passworddialog.cpp
+++ b/src/passworddialog.cpp
@@ -1,255 +1,256 @@
/*
SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer <brouwer@annejan.com>
SPDX-FileCopyrightText: 2018 Lukas Vogel <lukedirtwalker@gmail.com>
SPDX-FileCopyrightText: 2018 Claudio Maradonna <penguyman@stronzi.org>
SPDX-FileCopyrightText: 2023 g10 Code GmbH
SPDX-FileContributor: Sune Stolborg Vuorela <sune@vuorela.dk>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "passworddialog.h"
+#include "passentry.h"
#include "passnode.h"
#include "passwordconfiguration.h"
#include "passwordgenerator.h"
#include "settings.h"
#include "ui_passworddialog.h"
#include <QFileInfo>
#include <QLabel>
#include <QLineEdit>
+#include <QMessageBox>
/**
* @brief PasswordDialog::PasswordDialog basic constructor.
* @param passConfig configuration constant
* @param parent
*/
-PasswordDialog::PasswordDialog(PassEntry *passEntry, const PasswordConfiguration &passConfig, QWidget *parent)
+PasswordDialog::PasswordDialog(std::shared_ptr<PassNode> passNode, const PasswordConfiguration &passConfig, QWidget *parent)
: QDialog(parent)
, ui(std::make_unique<Ui::PasswordDialog>())
- , m_passEntry(passEntry)
+ , m_passNode(passNode)
, m_passConfig(passConfig)
{
m_templating = false;
m_allFields = false;
m_isNew = false;
ui->setupUi(this);
setLength(m_passConfig.length);
setPasswordCharTemplate(m_passConfig.selected);
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
ui->lineEditPassword->setRevealPasswordMode(KPassword::RevealMode::Always);
#endif
- connect(m_passEntry, &PassEntry::decrypted, this, &PasswordDialog::slotPasswordDecrypted);
+ connect(m_passNode.get(), &PassNode::loaded, this, &PasswordDialog::slotPasswordDecrypted);
+ connect(m_passNode.get(), &PassNode::errorOccurred, this, &PasswordDialog::handleError);
+ connect(m_passNode.get(), &PassNode::saved, this, &PasswordDialog::close);
connect(ui->createPasswordOptionsButton, &QAbstractButton::toggled, this, &PasswordDialog::togglePasswordGenerationOption);
togglePasswordGenerationOption(false);
}
/**
* @brief PasswordDialog::PasswordDialog complete constructor.
* @param file
* @param isNew
* @param parent pointer
*/
-PasswordDialog::PasswordDialog(PassEntry *passEntry, bool isNew, QWidget *parent)
+PasswordDialog::PasswordDialog(std::shared_ptr<PassNode> passNode, bool isNew, QWidget *parent)
: QDialog(parent)
, ui(std::make_unique<Ui::PasswordDialog>())
- , m_passEntry(passEntry)
+ , m_passNode(passNode)
, m_isNew(isNew)
{
if (!isNew) {
- m_passEntry->load(m_templating ? m_fields : QStringList(), m_allFields);
+ m_passNode->load();
}
ui->setupUi(this);
- setWindowTitle(this->windowTitle() + QLatin1Char(' ') + QFileInfo(m_passEntry->filePath()).baseName());
+ setWindowTitle(this->windowTitle() + QLatin1Char(' ') + QFileInfo(m_passNode->filePath()).baseName());
m_passConfig = Settings::getPasswordConfiguration();
setTemplate(Settings::getPassTemplate(), Settings::isUseTemplate());
templateAll(Settings::isTemplateAllFields());
setLength(m_passConfig.length);
setPasswordCharTemplate(m_passConfig.selected);
- connect(m_passEntry, &PassEntry::decrypted, this, &PasswordDialog::slotPasswordDecrypted);
- connect(this, &PasswordDialog::accepted, this, &PasswordDialog::dialogAccepted);
- connect(this, &PasswordDialog::rejected, this, &PasswordDialog::dialogCancelled);
+ connect(m_passNode.get(), &PassNode::loaded, this, &PasswordDialog::slotPasswordDecrypted);
+ connect(m_passNode.get(), &PassNode::errorOccurred, this, &PasswordDialog::handleError);
+ connect(m_passNode.get(), &PassNode::saved, this, &PasswordDialog::close);
connect(ui->createPasswordButton, &QAbstractButton::clicked, this, &PasswordDialog::createPassword);
connect(ui->createPasswordOptionsButton, &QAbstractButton::toggled, this, &PasswordDialog::togglePasswordGenerationOption);
togglePasswordGenerationOption(false);
}
/**
* @brief Pass{}{}wordDialog::~PasswordDialog basic destructor.
*/
PasswordDialog::~PasswordDialog() = default;
+void PasswordDialog::handleError(const QString &title, const QString &message)
+{
+ QMessageBox::critical(this, title, message);
+}
+
/**
* @brief PasswordDialog::on_createPasswordButton_clicked generate a random
* passwords.
* @todo refactor when process is untangled from MainWindow class.
*/
void PasswordDialog::createPassword()
{
setEnabled(false);
const auto charset = m_passConfig.Characters[static_cast<PasswordConfiguration::characterSet>(ui->passwordTemplateSwitch->currentIndex())];
const auto length = static_cast<unsigned int>(ui->spinBox_pwdLength->value());
QString newPass;
if (charset.length() > 0) {
newPass = PasswordGenerator::generateRandomPassword(charset, length);
} else {
// Q_EMIT m_pass.critical(i18n("No characters chosen"),
// i18n("Can't generate password, there are no characters to choose from "
// "set in the configuration!"));
}
if (newPass.length() > 0)
ui->lineEditPassword->setPassword(newPass);
setEnabled(true);
}
-/**
- * @brief PasswordDialog::on_accepted handle Ok click for QDialog
- */
-void PasswordDialog::dialogAccepted()
+void PasswordDialog::accept()
{
QString newValue = toRawContent();
if (newValue.isEmpty())
return;
if (newValue.right(1) != QLatin1Char('\n'))
newValue += QLatin1Char('\n');
- m_passEntry->setRawContent(newValue);
- m_passEntry->save();
-}
-
-/**
- * @brief PasswordDialog::on_rejected handle Cancel click for QDialog
- */
-void PasswordDialog::dialogCancelled()
-{
+ m_passNode->setContent(newValue);
+ m_passNode->save();
}
/**
* @brief PasswordDialog::setPassword populate the (templated) fields.
* @param password
*/
void PasswordDialog::slotPasswordDecrypted()
{
- ui->lineEditPassword->setPassword(m_passEntry->password());
+ PassEntry entry(m_passNode->content(), m_templating ? m_fields : QStringList(), m_allFields);
+
+ ui->lineEditPassword->setPassword(entry.password());
QWidget *previous = ui->createPasswordButton;
// first set templated values
- auto namedValues = m_passEntry->namedValues();
+ auto namedValues = entry.namedValues();
for (QLineEdit *line : std::as_const(templateLines)) {
line->setText(namedValues.takeValue(line->objectName()));
previous = line;
}
// show remaining values (if there are)
otherLines.clear();
for (const auto &nv : std::as_const(namedValues)) {
auto *line = new QLineEdit();
line->setObjectName(nv.name);
line->setText(nv.value);
ui->formLayout->addRow(new QLabel(nv.name), line);
setTabOrder(previous, line);
otherLines.append(line);
previous = line;
}
- ui->plainTextEdit->insertPlainText(m_passEntry->remainingData().join(QLatin1Char('\n')));
+ ui->plainTextEdit->insertPlainText(entry.remainingData());
}
QString PasswordDialog::toRawContent() const
{
QString passFile = ui->lineEditPassword->password() + QLatin1Char('\n');
QList<QLineEdit *> allLines(templateLines);
allLines.append(otherLines);
for (QLineEdit *line : allLines) {
QString text = line->text();
if (text.isEmpty())
continue;
passFile += line->objectName() + QStringLiteral(": ") + text + QLatin1Char('\n');
}
passFile += ui->plainTextEdit->toPlainText();
return passFile;
}
/**
* @brief PasswordDialog::setTemplate set the template and create the fields.
* @param rawFields
*/
void PasswordDialog::setTemplate(const QString &rawFields, bool useTemplate)
{
m_fields = rawFields.split(QLatin1Char('\n'));
m_templating = useTemplate;
templateLines.clear();
if (m_templating) {
QWidget *previous = ui->createPasswordButton;
for (const QString &field : std::as_const(m_fields)) {
if (field.isEmpty())
continue;
auto *line = new QLineEdit();
line->setObjectName(field);
ui->formLayout->addRow(new QLabel(i18nc("Field name", "%1:", field)), line);
setTabOrder(previous, line);
templateLines.append(line);
previous = line;
}
}
}
/**
* @brief PasswordDialog::templateAll basic setter for use in
* PasswordDialog::setPassword templating all tokenisable lines.
* @param templateAll
*/
void PasswordDialog::templateAll(bool templateAll)
{
m_allFields = templateAll;
}
/**
* @brief PasswordDialog::setLength
* PasswordDialog::setLength password length.
* @param l
*/
void PasswordDialog::setLength(int l)
{
ui->spinBox_pwdLength->setValue(l);
}
/**
* @brief PasswordDialog::setPasswordCharTemplate
* PasswordDialog::setPasswordCharTemplate chose the template style.
* @param t
*/
void PasswordDialog::setPasswordCharTemplate(int t)
{
ui->passwordTemplateSwitch->setCurrentIndex(t);
}
void PasswordDialog::togglePasswordGenerationOption(bool checked)
{
if (checked) {
ui->createPasswordOptionsButton->setIcon(QIcon::fromTheme(QStringLiteral("collapse-symbolic")));
ui->createPasswordOptionsButton->setToolTip(i18nc("@info:tooltip", "Collapse password options"));
ui->createPasswordOptionsButton->setAccessibleName(i18nc("@info:tooltip", "Collapse password options"));
ui->label_characterset->setVisible(true);
ui->passwordTemplateSwitch->setVisible(true);
ui->label_length->setVisible(true);
ui->spinBox_pwdLength->setVisible(true);
} else {
ui->createPasswordOptionsButton->setIcon(QIcon::fromTheme(QStringLiteral("expand-symbolic")));
ui->createPasswordOptionsButton->setToolTip(i18nc("@info:tooltip", "Toggle password options"));
ui->createPasswordOptionsButton->setAccessibleName(i18nc("@info:tooltip", "Toggle password options"));
ui->label_characterset->setVisible(false);
ui->passwordTemplateSwitch->setVisible(false);
ui->label_length->setVisible(false);
ui->spinBox_pwdLength->setVisible(false);
}
}
diff --git a/src/passworddialog.h b/src/passworddialog.h
index d0c7d0b..d9a02df 100644
--- a/src/passworddialog.h
+++ b/src/passworddialog.h
@@ -1,72 +1,75 @@
/*
SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer <brouwer@annejan.com>
SPDX-FileCopyrightText: 2018 Lukas Vogel <lukedirtwalker@gmail.com>
SPDX-FileCopyrightText: 2018 Claudio Maradonna <penguyman@stronzi.org>
SPDX-FileCopyrightText: 2023 g10 Code GmbH
SPDX-FileContributor: Sune Stolborg Vuorela <sune@vuorela.dk>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#ifndef PASSWORDDIALOG_H_
#define PASSWORDDIALOG_H_
#include "passwordconfiguration.h"
#include <QDialog>
namespace Ui
{
class PasswordDialog;
}
class QLineEdit;
class QWidget;
-class PassEntry;
+class PassNode;
/*!
\class PasswordDialog
\brief PasswordDialog Handles the inserting and editing of passwords.
Includes templated views.
*/
class PasswordDialog : public QDialog
{
Q_OBJECT
public:
- explicit PasswordDialog(PassEntry *passEntry, const PasswordConfiguration &passConfig, QWidget *parent = nullptr);
- PasswordDialog(PassEntry *passEntry, bool isNew, QWidget *parent = nullptr);
+ explicit PasswordDialog(std::shared_ptr<PassNode> passNode, const PasswordConfiguration &passConfig, QWidget *parent = nullptr);
+ PasswordDialog(std::shared_ptr<PassNode> passNode, bool isNew, QWidget *parent = nullptr);
~PasswordDialog();
/*! Sets content in the template for the interface.
\param rawFields is the template as a QString
\param useTemplate whether the template is used
*/
void setTemplate(const QString &rawFields, bool useTemplate);
void templateAll(bool templateAll);
void setLength(int l);
void setPasswordCharTemplate(int t);
+ void accept() override;
+
private Q_SLOTS:
void createPassword();
- void dialogAccepted();
- void dialogCancelled();
void togglePasswordGenerationOption(bool checked);
private:
/// Sets content in the password field in the interface.
void slotPasswordDecrypted();
+
+ void handleError(const QString &title, const QString &message);
+
QString toRawContent() const;
std::unique_ptr<Ui::PasswordDialog> ui;
- PassEntry *const m_passEntry;
+ std::shared_ptr<PassNode> const m_passNode;
PasswordConfiguration m_passConfig;
QStringList m_fields;
bool m_templating;
bool m_allFields;
bool m_isNew;
QList<QLineEdit *> templateLines;
QList<QLineEdit *> otherLines;
};
#endif // PASSWORDDIALOG_H_
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
deleted file mode 100644
index 97e2ad1..0000000
--- a/tests/CMakeLists.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-# SPDX-FileCopyrightText: 2024 g10 Code GmbH
-# SPDX-FileContributor: Carl Schwan <carl.schwan@gnupg.com>
-# SPDX-License-Identifier: BSD-3-Clauses
-
-add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/testdata")
-
-add_subdirectory(gnupg_home)
-include(${CMAKE_SOURCE_DIR}/cmake/modules/add_gpg_crypto_test.cmake)
-
-#ecm_add_test(auto/ui/tst_ui.cpp LINK_LIBRARIES gpgpass_internal Qt::Test)
-#ecm_add_test(auto/util/tst_util.cpp LINK_LIBRARIES gpgpass_internal Qt::Test)
-
-if (QT_MAJOR_VERSION STREQUAL "6")
- gpgpass_add_crypto_test(auto/model/tst_userslistmodel.cpp LINK_LIBRARIES gpgpass_internal Qt::Test)
-endif()
diff --git a/tests/auto/util/tst_util.cpp b/tests/auto/util/tst_util.cpp
deleted file mode 100644
index 60e5085..0000000
--- a/tests/auto/util/tst_util.cpp
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer <brouwer@annejan.com>
- SPDX-FileCopyrightText: 2018 Lukas Vogel <lukedirtwalker@gmail.com>
-
- SPDX-License-Identifier: GPL-3.0-or-later
-*/
-#include "filecontent.h"
-#include "util.h"
-#include <QCoreApplication>
-#include <QList>
-#include <QTest>
-
-/**
- * @brief The tst_util class is our first unit test
- */
-class tst_util : public QObject
-{
- Q_OBJECT
-
-public:
- tst_util();
- ~tst_util() override;
-
-public Q_SLOTS:
- void init();
- void cleanup();
-
-private Q_SLOTS:
- void initTestCase();
- void cleanupTestCase();
- void normalizeFolderPath();
- void fileContent();
-};
-
-bool operator==(const NamedValue &a, const NamedValue &b)
-{
- return a.name == b.name && a.value == b.value;
-}
-
-/**
- * @brief tst_util::tst_util basic constructor
- */
-tst_util::tst_util() = default;
-
-/**
- * @brief tst_util::~tst_util basic destructor
- */
-tst_util::~tst_util() = default;
-
-/**
- * @brief tst_util::init unit test init method
- */
-void tst_util::init()
-{
-}
-
-/**
- * @brief tst_util::cleanup unit test cleanup method
- */
-void tst_util::cleanup()
-{
-}
-
-/**
- * @brief tst_util::initTestCase test case init method
- */
-void tst_util::initTestCase()
-{
-}
-
-/**
- * @brief tst_util::cleanupTestCase test case cleanup method
- */
-void tst_util::cleanupTestCase()
-{
-}
-
-/**
- * @brief tst_util::normalizeFolderPath test to check correct working
- * of Util::normalizeFolderPath the paths should always end with a slash
- */
-void tst_util::normalizeFolderPath()
-{
- QCOMPARE(Util::normalizeFolderPath(QStringLiteral("test")), QStringLiteral("test/"));
- QCOMPARE(Util::normalizeFolderPath(QStringLiteral("test/")), QStringLiteral("test/"));
-}
-
-void tst_util::fileContent()
-{
- NamedValue key = {QStringLiteral("key"), QStringLiteral("val")};
- NamedValue key2 = {QStringLiteral("key2"), QStringLiteral("val2")};
- QString password = QStringLiteral("password");
-
- FileContent fc = FileContent::parse(QStringLiteral("password\n"), {}, false);
- QCOMPARE(fc.getPassword(), password);
- QCOMPARE(fc.getNamedValues(), {});
- QCOMPARE(fc.getRemainingData(), QString());
-
- fc = FileContent::parse(QStringLiteral("password"), {}, false);
- QCOMPARE(fc.getPassword(), password);
- QCOMPARE(fc.getNamedValues(), {});
- QCOMPARE(fc.getRemainingData(), QString());
-
- fc = FileContent::parse(QStringLiteral("password\nfoobar\n"), {}, false);
- QCOMPARE(fc.getPassword(), password);
- QCOMPARE(fc.getNamedValues(), {});
- QCOMPARE(fc.getRemainingData(), QStringLiteral("foobar\n"));
-
- fc = FileContent::parse(QStringLiteral("password\nkey: val\nkey2: val2"), {QStringLiteral("key2")}, false);
- QCOMPARE(fc.getPassword(), password);
- QCOMPARE(fc.getNamedValues(), QList<NamedValue>{key2});
- QCOMPARE(fc.getRemainingData(), QStringLiteral("key: val"));
-
- fc = FileContent::parse(QStringLiteral("password\nkey: val\nkey2: val2"), {QStringLiteral("key2")}, true);
- QCOMPARE(fc.getPassword(), password);
- QCOMPARE(fc.getNamedValues(), NamedValues({key, key2}));
- QCOMPARE(fc.getRemainingData(), QString());
-}
-
-QTEST_MAIN(tst_util)
-#include "tst_util.moc"
diff --git a/tests/gnupg_home/private-keys-v1.d/3BD9080DE9C88A88A67965B8E49F677004D6F6B7.key b/tests/gnupg_home/private-keys-v1.d/3BD9080DE9C88A88A67965B8E49F677004D6F6B7.key
deleted file mode 100644
index bb94326..0000000
--- a/tests/gnupg_home/private-keys-v1.d/3BD9080DE9C88A88A67965B8E49F677004D6F6B7.key
+++ /dev/null
@@ -1,22 +0,0 @@
-Created: 20091125T205104
-Key: (protected-private-key (rsa (n #00AA481E53690BD9B3B7B7E06E7BAC772D
- 185600626EC2D66143DE61C03C90959992B959C2E4BE3561B867D6665E42280CD07676
- 421F4457F3FB18627A9914C569382587A19BBB5D6142E1B2D55393E3E09818A1704F3B
- 4A652FF02945A78E8C522F1B661F73F6E931DE4DFE22B15EA0C99E216DACB1DD8160B9
- 48B728FC84737D#)(e #010001#)(protected openpgp-native
- (openpgp-private-key (version "4")(algo RSA)(skey _
- #00AA481E53690BD9B3B7B7E06E7BAC772D185600626EC2D66143DE61C03C90959992
- B959C2E4BE3561B867D6665E42280CD07676421F4457F3FB18627A9914C569382587A1
- 9BBB5D6142E1B2D55393E3E09818A1704F3B4A652FF02945A78E8C522F1B661F73F6E9
- 31DE4DFE22B15EA0C99E216DACB1DD8160B948B728FC84737D# _ #010001# _
- #04AF9A620A3C80BFB0D9170C834BC235D3FB1DE4978EB60E942B0CFE98CC13C9D79F
- 51288B59EEA723477C3F71ED99238D230D6B116834916AC20787BF29B2AB00984CFC97
- 33917EEF87DB07D821FA4C026CE51424FCDFE877FF68B1FE90EC55EA8DF4D574EB9751
- CF6DC7C32F3697924BD3D1F64AFEF61381AACEE1127FB071# _
- #00C94B0E567403B32B73CB549B3666F57D42F9098BDB997C451F078FEA0D96972B6F
- 88EE1D47BC6704834858DD2D92418A02E181E35E0F8F789F36197DF46B4DA9# _
- #00D88F7192AB5998F69170FDF587DF949BDDD4C97F3C0EB15518A97A005D280536CD
- C8EBC72875CED795B4365877D5E9BB9C5E65FC6F17D155A657863166AB13B5# _
- #2DC9B76B9D33DE67447477173D82F54BD58D1FD83F71D37A3659B03341EAE125B49B
- 9EB50F9047DAE7801A9309657FC25B873DFFFA379F9F3D1DDC5853661AEA#)(csum
- "41893")(protection none)))))
diff --git a/tests/gnupg_home/pubring.kbx b/tests/gnupg_home/pubring.kbx
deleted file mode 100644
index 2b4b09f..0000000
Binary files a/tests/gnupg_home/pubring.kbx and /dev/null differ

File Metadata

Mime Type
application/octet-stream
Expires
Tue, Nov 5, 1:15 PM (2 d)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
90/04/e6c69bea25cd737ce25ce459480d

Event Timeline