From 1dd99559bbad9d4797b94a5723115051b399a564 Mon Sep 17 00:00:00 2001 From: Bodo Graumann Date: Wed, 25 Nov 2020 07:43:22 +0100 Subject: [PATCH 01/27] Remove length limit on password Software should make the user free, not restrict him. --- src/gui/PasswordGeneratorWidget.ui | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/gui/PasswordGeneratorWidget.ui b/src/gui/PasswordGeneratorWidget.ui index 81964a517..d2b2f703b 100644 --- a/src/gui/PasswordGeneratorWidget.ui +++ b/src/gui/PasswordGeneratorWidget.ui @@ -259,9 +259,6 @@ QProgressBar::chunk { 1 - - 128 - 20 From 9cb36abe91cddd2b6962938afebdd1946bb69a26 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sat, 24 Oct 2020 07:44:05 -0400 Subject: [PATCH 02/27] Properly save Password Generator settings * Fix #5605 --- src/gui/PasswordGeneratorWidget.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui/PasswordGeneratorWidget.cpp b/src/gui/PasswordGeneratorWidget.cpp index 2dc643df2..4be78fce9 100644 --- a/src/gui/PasswordGeneratorWidget.cpp +++ b/src/gui/PasswordGeneratorWidget.cpp @@ -173,9 +173,9 @@ void PasswordGeneratorWidget::saveSettings() config()->set(Config::PasswordGenerator_AdvancedMode, m_ui->buttonAdvancedMode->isChecked()); if (m_ui->buttonAdvancedMode->isChecked()) { - config()->set(Config::PasswordGenerator_SpecialChars, m_ui->checkBoxSpecialChars->isChecked()); - } else { config()->set(Config::PasswordGenerator_Logograms, m_ui->checkBoxSpecialChars->isChecked()); + } else { + config()->set(Config::PasswordGenerator_SpecialChars, m_ui->checkBoxSpecialChars->isChecked()); } config()->set(Config::PasswordGenerator_Braces, m_ui->checkBoxBraces->isChecked()); config()->set(Config::PasswordGenerator_Punctuation, m_ui->checkBoxPunctuation->isChecked()); From 3f7e79cdf33876506c4c169828bcfd177a4c97eb Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Fri, 20 Nov 2020 21:49:56 +0100 Subject: [PATCH 03/27] Add Argon2id KDF (backport of #5726) --- src/cli/Import.cpp | 2 +- src/crypto/kdf/Argon2Kdf.cpp | 32 +++++++++--- src/crypto/kdf/Argon2Kdf.h | 12 ++++- src/format/KeePass2.cpp | 13 +++-- src/format/KeePass2.h | 3 +- src/format/OpVaultReader.cpp | 2 +- .../DatabaseSettingsWidgetEncryption.cpp | 31 ++++++------ tests/TestKdbx4.cpp | 50 +++++++++++-------- tests/TestKeePass2Format.cpp | 2 +- tests/gui/TestGui.cpp | 2 +- 10 files changed, 94 insertions(+), 55 deletions(-) diff --git a/src/cli/Import.cpp b/src/cli/Import.cpp index 930158988..148ee5285 100644 --- a/src/cli/Import.cpp +++ b/src/cli/Import.cpp @@ -83,7 +83,7 @@ int Import::execute(const QStringList& arguments) QString errorMessage; Database db; - db.setKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2)); + db.setKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2D)); db.setKey(key); if (!db.import(xmlExportPath, &errorMessage)) { diff --git a/src/crypto/kdf/Argon2Kdf.cpp b/src/crypto/kdf/Argon2Kdf.cpp index 31995fdd0..cc23def83 100644 --- a/src/crypto/kdf/Argon2Kdf.cpp +++ b/src/crypto/kdf/Argon2Kdf.cpp @@ -29,8 +29,9 @@ * a 256-bit salt is generated each time the database is saved, the tag length is 256 bits, no secret key * or associated data. KeePass uses the latest version of Argon2, v1.3. */ -Argon2Kdf::Argon2Kdf() - : Kdf::Kdf(KeePass2::KDF_ARGON2) +Argon2Kdf::Argon2Kdf(Type type) + : Kdf::Kdf(KeePass2::KDF_ARGON2D) + , m_type(type) , m_version(0x13) , m_memory(1 << 16) , m_parallelism(static_cast(QThread::idealThreadCount())) @@ -54,6 +55,16 @@ bool Argon2Kdf::setVersion(quint32 version) return false; } +Argon2Kdf::Type Argon2Kdf::type() const +{ + return m_type; +} + +void Argon2Kdf::setType(Type type) +{ + m_type = type; +} + quint64 Argon2Kdf::memory() const { return m_memory; @@ -133,7 +144,11 @@ bool Argon2Kdf::processParameters(const QVariantMap& p) QVariantMap Argon2Kdf::writeParameters() { QVariantMap p; - p.insert(KeePass2::KDFPARAM_UUID, KeePass2::KDF_ARGON2.toRfc4122()); + if (type() == Type::Argon2d) { + p.insert(KeePass2::KDFPARAM_UUID, KeePass2::KDF_ARGON2D.toRfc4122()); + } else { + p.insert(KeePass2::KDFPARAM_UUID, KeePass2::KDF_ARGON2ID.toRfc4122()); + } p.insert(KeePass2::KDFPARAM_ARGON2_VERSION, version()); p.insert(KeePass2::KDFPARAM_ARGON2_PARALLELISM, parallelism()); p.insert(KeePass2::KDFPARAM_ARGON2_MEMORY, memory() * 1024); @@ -158,18 +173,20 @@ bool Argon2Kdf::transform(const QByteArray& raw, QByteArray& result) const { result.clear(); result.resize(32); - return transformKeyRaw(raw, seed(), version(), rounds(), memory(), parallelism(), result); + return transformKeyRaw(raw, seed(), version(), type(), rounds(), memory(), parallelism(), result); } bool Argon2Kdf::transformKeyRaw(const QByteArray& key, const QByteArray& seed, quint32 version, + Type type, quint32 rounds, quint64 memory, quint32 parallelism, QByteArray& result) { // Time Cost, Mem Cost, Threads/Lanes, Password, length, Salt, length, out, length + int rc = argon2_hash(rounds, memory, parallelism, @@ -181,7 +198,7 @@ bool Argon2Kdf::transformKeyRaw(const QByteArray& key, result.size(), nullptr, 0, - Argon2_d, + type == Type::Argon2d ? Argon2_d : Argon2_id, version); if (rc != ARGON2_OK) { qWarning("Argon2 error: %s", argon2_error_message(rc)); @@ -205,7 +222,7 @@ int Argon2Kdf::benchmarkImpl(int msec) const timer.start(); int rounds = 4; - if (transformKeyRaw(key, seed, version(), rounds, memory(), parallelism(), key)) { + if (transformKeyRaw(key, seed, version(), type(), rounds, memory(), parallelism(), key)) { return static_cast(rounds * (static_cast(msec) / timer.elapsed())); } @@ -214,5 +231,6 @@ int Argon2Kdf::benchmarkImpl(int msec) const QString Argon2Kdf::toString() const { - return QObject::tr("Argon2 (%1 rounds, %2 KB)").arg(QString::number(rounds()), QString::number(memory())); + return QObject::tr("Argon2%1 (%2 rounds, %3 KB)") + .arg(type() == Type::Argon2d ? "d" : "id", QString::number(rounds()), QString::number(memory())); } diff --git a/src/crypto/kdf/Argon2Kdf.h b/src/crypto/kdf/Argon2Kdf.h index 6a16ee96e..b3a8c49b3 100644 --- a/src/crypto/kdf/Argon2Kdf.h +++ b/src/crypto/kdf/Argon2Kdf.h @@ -23,7 +23,13 @@ class Argon2Kdf : public Kdf { public: - Argon2Kdf(); + enum class Type + { + Argon2d, + Argon2id + }; + + Argon2Kdf(Type type); bool processParameters(const QVariantMap& p) override; QVariantMap writeParameters() override; @@ -32,6 +38,8 @@ public: quint32 version() const; bool setVersion(quint32 version); + Type type() const; + void setType(Type type); quint64 memory() const; bool setMemory(quint64 kibibytes); quint32 parallelism() const; @@ -41,6 +49,7 @@ public: protected: int benchmarkImpl(int msec) const override; + Type m_type; quint32 m_version; quint64 m_memory; quint32 m_parallelism; @@ -49,6 +58,7 @@ private: Q_REQUIRED_RESULT static bool transformKeyRaw(const QByteArray& key, const QByteArray& seed, quint32 version, + Type type, quint32 rounds, quint64 memory, quint32 parallelism, diff --git a/src/format/KeePass2.cpp b/src/format/KeePass2.cpp index dc50ca001..bf5bb1cae 100644 --- a/src/format/KeePass2.cpp +++ b/src/format/KeePass2.cpp @@ -30,7 +30,8 @@ const QUuid KeePass2::CIPHER_CHACHA20 = QUuid("d6038a2b-8b6f-4cb5-a524-339a31dbb const QUuid KeePass2::KDF_AES_KDBX3 = QUuid("c9d9f39a-628a-4460-bf74-0d08c18a4fea"); const QUuid KeePass2::KDF_AES_KDBX4 = QUuid("7c02bb82-79a7-4ac0-927d-114a00648238"); -const QUuid KeePass2::KDF_ARGON2 = QUuid("ef636ddf-8c29-444b-91f7-a9a403e30a0c"); +const QUuid KeePass2::KDF_ARGON2D = QUuid("ef636ddf-8c29-444b-91f7-a9a403e30a0c"); +const QUuid KeePass2::KDF_ARGON2ID = QUuid("9e298b19-56db-4773-b23d-fc3ec6f0a1e6"); const QByteArray KeePass2::INNER_STREAM_SALSA20_IV("\xe8\x30\x09\x4b\x97\x20\x5d\x2a"); @@ -53,7 +54,8 @@ const QList> KeePass2::CIPHERS{ qMakePair(KeePass2::CIPHER_CHACHA20, QObject::tr("ChaCha20 256-bit"))}; const QList> KeePass2::KDFS{ - qMakePair(KeePass2::KDF_ARGON2, QObject::tr("Argon2 (KDBX 4 – recommended)")), + qMakePair(KeePass2::KDF_ARGON2D, QObject::tr("Argon2d (KDBX 4 – recommended)")), + qMakePair(KeePass2::KDF_ARGON2ID, QObject::tr("Argon2id (KDBX 4)")), qMakePair(KeePass2::KDF_AES_KDBX4, QObject::tr("AES-KDF (KDBX 4)")), qMakePair(KeePass2::KDF_AES_KDBX3, QObject::tr("AES-KDF (KDBX 3.1)"))}; @@ -109,8 +111,11 @@ QSharedPointer KeePass2::uuidToKdf(const QUuid& uuid) if (uuid == KDF_AES_KDBX4) { return QSharedPointer::create(); } - if (uuid == KDF_ARGON2) { - return QSharedPointer::create(); + if (uuid == KDF_ARGON2D) { + return QSharedPointer::create(Argon2Kdf::Type::Argon2d); + } + if (uuid == KDF_ARGON2ID) { + return QSharedPointer::create(Argon2Kdf::Type::Argon2id); } return {}; diff --git a/src/format/KeePass2.h b/src/format/KeePass2.h index d18db3578..abb24a800 100644 --- a/src/format/KeePass2.h +++ b/src/format/KeePass2.h @@ -53,7 +53,8 @@ namespace KeePass2 extern const QUuid KDF_AES_KDBX3; extern const QUuid KDF_AES_KDBX4; - extern const QUuid KDF_ARGON2; + extern const QUuid KDF_ARGON2D; + extern const QUuid KDF_ARGON2ID; extern const QByteArray INNER_STREAM_SALSA20_IV; diff --git a/src/format/OpVaultReader.cpp b/src/format/OpVaultReader.cpp index a64b009de..dab08bd82 100644 --- a/src/format/OpVaultReader.cpp +++ b/src/format/OpVaultReader.cpp @@ -72,7 +72,7 @@ Database* OpVaultReader::readDatabase(QDir& opdataDir, const QString& password) key->addKey(QSharedPointer::create(password)); QScopedPointer db(new Database()); - db->setKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2)); + db->setKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2D)); db->setCipher(KeePass2::CIPHER_AES256); db->setKey(key, true, false); db->metadata()->setName(vaultName); diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp b/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp index cc57e453a..2df1f28f0 100644 --- a/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp +++ b/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp @@ -43,7 +43,7 @@ DatabaseSettingsWidgetEncryption::DatabaseSettingsWidgetEncryption(QWidget* pare connect(m_ui->memorySpinBox, SIGNAL(valueChanged(int)), this, SLOT(memoryChanged(int))); connect(m_ui->parallelismSpinBox, SIGNAL(valueChanged(int)), this, SLOT(parallelismChanged(int))); - m_ui->compatibilitySelection->addItem(tr("KDBX 4.0 (recommended)"), KeePass2::KDF_ARGON2.toByteArray()); + m_ui->compatibilitySelection->addItem(tr("KDBX 4.0 (recommended)"), KeePass2::KDF_ARGON2D.toByteArray()); m_ui->compatibilitySelection->addItem(tr("KDBX 3.1"), KeePass2::KDF_AES_KDBX3.toByteArray()); m_ui->decryptionTimeSlider->setMinimum(Kdf::MIN_ENCRYPTION_TIME / 100); m_ui->decryptionTimeSlider->setMaximum(Kdf::MAX_ENCRYPTION_TIME / 100); @@ -75,6 +75,9 @@ DatabaseSettingsWidgetEncryption::~DatabaseSettingsWidgetEncryption() { } +#define IS_ARGON2(uuid) (uuid == KeePass2::KDF_ARGON2D || uuid == KeePass2::KDF_ARGON2ID) +#define IS_AES_KDF(uuid) (uuid == KeePass2::KDF_AES_KDBX3 || uuid == KeePass2::KDF_AES_KDBX4) + void DatabaseSettingsWidgetEncryption::initialize() { Q_ASSERT(m_db); @@ -85,7 +88,7 @@ void DatabaseSettingsWidgetEncryption::initialize() bool isDirty = false; if (!m_db->kdf()) { - m_db->setKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2)); + m_db->setKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2D)); isDirty = true; } if (!m_db->key()) { @@ -175,7 +178,7 @@ void DatabaseSettingsWidgetEncryption::loadKdfParameters() } m_ui->transformRoundsSpinBox->setValue(kdf->rounds()); - if (m_db->kdf()->uuid() == KeePass2::KDF_ARGON2) { + if (IS_ARGON2(m_db->kdf()->uuid())) { auto argon2Kdf = kdf.staticCast(); m_ui->memorySpinBox->setValue(static_cast(argon2Kdf->memory()) / (1 << 10)); m_ui->parallelismSpinBox->setValue(argon2Kdf->parallelism()); @@ -188,13 +191,10 @@ void DatabaseSettingsWidgetEncryption::updateKdfFields() { QUuid id = m_db->kdf()->uuid(); - bool memoryVisible = (id == KeePass2::KDF_ARGON2); - m_ui->memoryUsageLabel->setVisible(memoryVisible); - m_ui->memorySpinBox->setVisible(memoryVisible); - - bool parallelismVisible = (id == KeePass2::KDF_ARGON2); - m_ui->parallelismLabel->setVisible(parallelismVisible); - m_ui->parallelismSpinBox->setVisible(parallelismVisible); + m_ui->memoryUsageLabel->setVisible(IS_ARGON2(id)); + m_ui->memorySpinBox->setVisible(IS_ARGON2(id)); + m_ui->parallelismLabel->setVisible(IS_ARGON2(id)); + m_ui->parallelismSpinBox->setVisible(IS_ARGON2(id)); } void DatabaseSettingsWidgetEncryption::activateChangeDecryptionTime() @@ -253,7 +253,7 @@ bool DatabaseSettingsWidgetEncryption::save() m_db->metadata()->customData()->remove(CD_DECRYPTION_TIME_PREFERENCE_KEY); // first perform safety check for KDF rounds - if (kdf->uuid() == KeePass2::KDF_ARGON2 && m_ui->transformRoundsSpinBox->value() > 10000) { + if (IS_ARGON2(kdf->uuid()) && m_ui->transformRoundsSpinBox->value() > 10000) { QMessageBox warning; warning.setIcon(QMessageBox::Warning); warning.setWindowTitle(tr("Number of rounds too high", "Key transformation rounds")); @@ -266,8 +266,7 @@ bool DatabaseSettingsWidgetEncryption::save() if (warning.clickedButton() != ok) { return false; } - } else if ((kdf->uuid() == KeePass2::KDF_AES_KDBX3 || kdf->uuid() == KeePass2::KDF_AES_KDBX4) - && m_ui->transformRoundsSpinBox->value() < 100000) { + } else if (IS_AES_KDF(kdf->uuid()) && m_ui->transformRoundsSpinBox->value() < 100000) { QMessageBox warning; warning.setIcon(QMessageBox::Warning); warning.setWindowTitle(tr("Number of rounds too low", "Key transformation rounds")); @@ -286,7 +285,7 @@ bool DatabaseSettingsWidgetEncryption::save() // Save kdf parameters kdf->setRounds(m_ui->transformRoundsSpinBox->value()); - if (kdf->uuid() == KeePass2::KDF_ARGON2) { + if (IS_ARGON2(kdf->uuid())) { auto argon2Kdf = kdf.staticCast(); argon2Kdf->setMemory(static_cast(m_ui->memorySpinBox->value()) * (1 << 10)); argon2Kdf->setParallelism(static_cast(m_ui->parallelismSpinBox->value())); @@ -317,7 +316,7 @@ void DatabaseSettingsWidgetEncryption::benchmarkTransformRounds(int millisecs) // Create a new kdf with the current parameters auto kdf = KeePass2::uuidToKdf(QUuid(m_ui->kdfComboBox->currentData().toByteArray())); kdf->setRounds(m_ui->transformRoundsSpinBox->value()); - if (kdf->uuid() == KeePass2::KDF_ARGON2) { + if (IS_ARGON2(kdf->uuid())) { auto argon2Kdf = kdf.staticCast(); if (!argon2Kdf->setMemory(static_cast(m_ui->memorySpinBox->value()) * (1 << 10))) { m_ui->memorySpinBox->setValue(static_cast(argon2Kdf->memory() / (1 << 10))); @@ -402,7 +401,7 @@ void DatabaseSettingsWidgetEncryption::updateFormatCompatibility(int index, bool auto kdf = KeePass2::uuidToKdf(kdfUuid); m_db->setKdf(kdf); - if (kdf->uuid() == KeePass2::KDF_ARGON2) { + if (IS_ARGON2(kdf->uuid())) { auto argon2Kdf = kdf.staticCast(); // Default to 64 MiB of memory and 2 threads // these settings are safe for desktop and mobile devices diff --git a/tests/TestKdbx4.cpp b/tests/TestKdbx4.cpp index 51784f062..46eaacc84 100644 --- a/tests/TestKdbx4.cpp +++ b/tests/TestKdbx4.cpp @@ -42,8 +42,8 @@ int main(int argc, char* argv[]) void TestKdbx4Argon2::initTestCaseImpl() { - m_xmlDb->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2))); - m_kdbxSourceDb->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2))); + m_xmlDb->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2D))); + m_kdbxSourceDb->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2D))); } QSharedPointer @@ -108,7 +108,7 @@ void TestKdbx4Argon2::readKdbx(const QString& path, void TestKdbx4Argon2::writeKdbx(QIODevice* device, Database* db, bool& hasError, QString& errorString) { if (db->kdf()->uuid() == KeePass2::KDF_AES_KDBX3) { - db->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2))); + db->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2D))); } KeePass2Writer writer; hasError = writer.writeDatabase(device, db); @@ -213,26 +213,32 @@ void TestKdbx4Argon2::testFormat400Upgrade_data() auto constexpr kdbx3 = KeePass2::FILE_VERSION_3_1 & KeePass2::FILE_VERSION_CRITICAL_MASK; auto constexpr kdbx4 = KeePass2::FILE_VERSION_4 & KeePass2::FILE_VERSION_CRITICAL_MASK; - QTest::newRow("Argon2 + AES") << KeePass2::KDF_ARGON2 << KeePass2::CIPHER_AES256 << false << kdbx4; - QTest::newRow("AES-KDF + AES") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_AES256 << false << kdbx4; - QTest::newRow("AES-KDF (legacy) + AES") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_AES256 << false << kdbx3; - QTest::newRow("Argon2 + AES + CustomData") << KeePass2::KDF_ARGON2 << KeePass2::CIPHER_AES256 << true << kdbx4; - QTest::newRow("AES-KDF + AES + CustomData") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_AES256 << true << kdbx4; - QTest::newRow("AES-KDF (legacy) + AES + CustomData") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_AES256 << true << kdbx4; + QTest::newRow("Argon2d + AES") << KeePass2::KDF_ARGON2D << KeePass2::CIPHER_AES256 << false << kdbx4; + QTest::newRow("Argon2id + AES") << KeePass2::KDF_ARGON2ID << KeePass2::CIPHER_AES256 << false << kdbx4; + QTest::newRow("AES-KDF + AES") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_AES256 << false << kdbx4; + QTest::newRow("AES-KDF (legacy) + AES") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_AES256 << false << kdbx3; + QTest::newRow("Argon2d + AES + CustomData") << KeePass2::KDF_ARGON2D << KeePass2::CIPHER_AES256 << true << kdbx4; + QTest::newRow("Argon2id + AES + CustomData") << KeePass2::KDF_ARGON2ID << KeePass2::CIPHER_AES256 << true << kdbx4; + QTest::newRow("AES-KDF + AES + CustomData") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_AES256 << true << kdbx4; + QTest::newRow("AES-KDF (legacy) + AES + CustomData") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_AES256 << true << kdbx4; - QTest::newRow("Argon2 + ChaCha20") << KeePass2::KDF_ARGON2 << KeePass2::CIPHER_CHACHA20 << false << kdbx4; - QTest::newRow("AES-KDF + ChaCha20") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_CHACHA20 << false << kdbx4; - QTest::newRow("AES-KDF (legacy) + ChaCha20") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_CHACHA20 << false << kdbx3; - QTest::newRow("Argon2 + ChaCha20 + CustomData") << KeePass2::KDF_ARGON2 << KeePass2::CIPHER_CHACHA20 << true << kdbx4; - QTest::newRow("AES-KDF + ChaCha20 + CustomData") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_CHACHA20 << true << kdbx4; - QTest::newRow("AES-KDF (legacy) + ChaCha20 + CustomData") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_CHACHA20 << true << kdbx4; + QTest::newRow("Argon2d + ChaCha20") << KeePass2::KDF_ARGON2D << KeePass2::CIPHER_CHACHA20 << false << kdbx4; + QTest::newRow("Argon2id + ChaCha20") << KeePass2::KDF_ARGON2ID << KeePass2::CIPHER_CHACHA20 << false << kdbx4; + QTest::newRow("AES-KDF + ChaCha20") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_CHACHA20 << false << kdbx4; + QTest::newRow("AES-KDF (legacy) + ChaCha20") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_CHACHA20 << false << kdbx3; + QTest::newRow("Argon2d + ChaCha20 + CustomData") << KeePass2::KDF_ARGON2D << KeePass2::CIPHER_CHACHA20 << true << kdbx4; + QTest::newRow("Argon2id + ChaCha20 + CustomData") << KeePass2::KDF_ARGON2ID << KeePass2::CIPHER_CHACHA20 << true << kdbx4; + QTest::newRow("AES-KDF + ChaCha20 + CustomData") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_CHACHA20 << true << kdbx4; + QTest::newRow("AES-KDF (legacy) + ChaCha20 + CustomData") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_CHACHA20 << true << kdbx4; - QTest::newRow("Argon2 + Twofish") << KeePass2::KDF_ARGON2 << KeePass2::CIPHER_TWOFISH << false << kdbx4; - QTest::newRow("AES-KDF + Twofish") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_TWOFISH << false << kdbx4; - QTest::newRow("AES-KDF (legacy) + Twofish") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_TWOFISH << false << kdbx3; - QTest::newRow("Argon2 + Twofish + CustomData") << KeePass2::KDF_ARGON2 << KeePass2::CIPHER_TWOFISH << true << kdbx4; - QTest::newRow("AES-KDF + Twofish + CustomData") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_TWOFISH << true << kdbx4; - QTest::newRow("AES-KDF (legacy) + Twofish + CustomData") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_TWOFISH << true << kdbx4; + QTest::newRow("Argon2d + Twofish") << KeePass2::KDF_ARGON2D << KeePass2::CIPHER_TWOFISH << false << kdbx4; + QTest::newRow("Argon2id + Twofish") << KeePass2::KDF_ARGON2ID << KeePass2::CIPHER_TWOFISH << false << kdbx4; + QTest::newRow("AES-KDF + Twofish") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_TWOFISH << false << kdbx4; + QTest::newRow("AES-KDF (legacy) + Twofish") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_TWOFISH << false << kdbx3; + QTest::newRow("Argon2d + Twofish + CustomData") << KeePass2::KDF_ARGON2D << KeePass2::CIPHER_TWOFISH << true << kdbx4; + QTest::newRow("Argon2id + Twofish + CustomData") << KeePass2::KDF_ARGON2ID << KeePass2::CIPHER_TWOFISH << true << kdbx4; + QTest::newRow("AES-KDF + Twofish + CustomData") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_TWOFISH << true << kdbx4; + QTest::newRow("AES-KDF (legacy) + Twofish + CustomData") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_TWOFISH << true << kdbx4; } // clang-format on @@ -270,7 +276,7 @@ void TestKdbx4Argon2::testUpgradeMasterKeyIntegrity() } else if (upgradeAction == "kdf-aes-kdbx3") { db->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_AES_KDBX3))); } else if (upgradeAction == "kdf-argon2") { - db->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2))); + db->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2D))); } else if (upgradeAction == "kdf-aes-kdbx4") { db->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_AES_KDBX4))); } else if (upgradeAction == "public-customdata") { diff --git a/tests/TestKeePass2Format.cpp b/tests/TestKeePass2Format.cpp index b0217c942..2aa3948ae 100644 --- a/tests/TestKeePass2Format.cpp +++ b/tests/TestKeePass2Format.cpp @@ -809,7 +809,7 @@ QSharedPointer TestKeePass2Format::fastKdf(QSharedPointer kdf) const { kdf->setRounds(1); - if (kdf->uuid() == KeePass2::KDF_ARGON2) { + if (kdf->uuid() == KeePass2::KDF_ARGON2D) { kdf->processParameters({{KeePass2::KDFPARAM_ARGON2_MEMORY, 1024}, {KeePass2::KDFPARAM_ARGON2_PARALLELISM, 1}}); } diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index c79de84bb..4ab29ce37 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -302,7 +302,7 @@ void TestGui::testCreateDatabase() // check key and encryption QCOMPARE(m_db->key()->keys().size(), 2); QCOMPARE(m_db->kdf()->rounds(), 2); - QCOMPARE(m_db->kdf()->uuid(), KeePass2::KDF_ARGON2); + QCOMPARE(m_db->kdf()->uuid(), KeePass2::KDF_ARGON2D); QCOMPARE(m_db->cipher(), KeePass2::CIPHER_AES256); auto compositeKey = QSharedPointer::create(); compositeKey->addKey(QSharedPointer::create("test")); From 7ac651763c05daf0d4871abe52aeff903c664178 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Wed, 26 Aug 2020 15:17:31 -0400 Subject: [PATCH 04/27] Improve CSV export and import capability * Fixes #3541 * CSV export now includes TOTP settings, Entry Icon (database icon number only), Modified Time, and Created Time. * CSV import properly understands time in ISO 8601 format and Unix Timestamp. * CSV import will set the TOTP settings and entry icon based on the chosen column. --- src/core/Entry.cpp | 12 +- src/core/Entry.h | 1 + src/format/CsvExporter.cpp | 8 ++ src/gui/csvImport/CsvImportWidget.cpp | 43 +++++-- src/gui/csvImport/CsvImportWidget.ui | 176 ++++++++++++++++++-------- tests/TestCli.cpp | 4 +- tests/TestCsvExporter.cpp | 21 ++- 7 files changed, 189 insertions(+), 76 deletions(-) diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 81f856ff5..82c9e7bc3 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -465,7 +465,8 @@ void Entry::setTotp(QSharedPointer settings) m_data.totpSettings.reset(); } else { m_data.totpSettings = std::move(settings); - auto text = Totp::writeSettings(m_data.totpSettings, title(), username()); + auto text = Totp::writeSettings( + m_data.totpSettings, resolveMultiplePlaceholders(title()), resolveMultiplePlaceholders(username())); if (m_data.totpSettings->format != Totp::StorageFormat::LEGACY) { m_attributes->set(Totp::ATTRIBUTE_OTP, text, true); } else { @@ -493,6 +494,15 @@ QSharedPointer Entry::totpSettings() const return m_data.totpSettings; } +QString Entry::totpSettingsString() const +{ + if (m_data.totpSettings) { + return Totp::writeSettings( + m_data.totpSettings, resolveMultiplePlaceholders(title()), resolveMultiplePlaceholders(username()), true); + } + return {}; +} + void Entry::setUuid(const QUuid& uuid) { Q_ASSERT(!uuid.isNull()); diff --git a/src/core/Entry.h b/src/core/Entry.h index 27df86596..a0dbbf7d4 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -106,6 +106,7 @@ public: QString notes() const; QString attribute(const QString& key) const; QString totp() const; + QString totpSettingsString() const; QSharedPointer totpSettings() const; int size() const; diff --git a/src/format/CsvExporter.cpp b/src/format/CsvExporter.cpp index 98fc6fdc8..281b94761 100644 --- a/src/format/CsvExporter.cpp +++ b/src/format/CsvExporter.cpp @@ -67,6 +67,10 @@ QString CsvExporter::exportHeader() addColumn(header, "Password"); addColumn(header, "URL"); addColumn(header, "Notes"); + addColumn(header, "TOTP"); + addColumn(header, "Icon"); + addColumn(header, "Last Modified"); + addColumn(header, "Created"); return header + QString("\n"); } @@ -88,6 +92,10 @@ QString CsvExporter::exportGroup(const Group* group, QString groupPath) addColumn(line, entry->password()); addColumn(line, entry->url()); addColumn(line, entry->notes()); + addColumn(line, entry->totpSettingsString()); + addColumn(line, QString::number(entry->iconNumber())); + addColumn(line, entry->timeInfo().lastModificationTime().toString(Qt::ISODate)); + addColumn(line, entry->timeInfo().creationTime().toString(Qt::ISODate)); line.append("\n"); response.append(line); diff --git a/src/gui/csvImport/CsvImportWidget.cpp b/src/gui/csvImport/CsvImportWidget.cpp index 01fd5fc89..e78e9f94a 100644 --- a/src/gui/csvImport/CsvImportWidget.cpp +++ b/src/gui/csvImport/CsvImportWidget.cpp @@ -27,6 +27,7 @@ #include "format/KeePass2Writer.h" #include "gui/MessageBox.h" #include "gui/MessageWidget.h" +#include "totp/totp.h" // I wanted to make the CSV import GUI future-proof, so if one day you need a new field, // all you have to do is add a field to m_columnHeader, and the GUI will follow: @@ -39,7 +40,8 @@ CsvImportWidget::CsvImportWidget(QWidget* parent) , m_comboModel(new QStringListModel(this)) , m_columnHeader(QStringList() << QObject::tr("Group") << QObject::tr("Title") << QObject::tr("Username") << QObject::tr("Password") << QObject::tr("URL") << QObject::tr("Notes") - << QObject::tr("Last Modified") << QObject::tr("Created")) + << QObject::tr("TOTP") << QObject::tr("Icon") << QObject::tr("Last Modified") + << QObject::tr("Created")) , m_fieldSeparatorList(QStringList() << "," << ";" << "-" @@ -54,7 +56,7 @@ CsvImportWidget::CsvImportWidget(QWidget* parent) m_ui->messageWidget->setHidden(true); m_combos << m_ui->groupCombo << m_ui->titleCombo << m_ui->usernameCombo << m_ui->passwordCombo << m_ui->urlCombo - << m_ui->notesCombo << m_ui->lastModifiedCombo << m_ui->createdCombo; + << m_ui->notesCombo << m_ui->totpCombo << m_ui->iconCombo << m_ui->lastModifiedCombo << m_ui->createdCombo; for (auto combo : m_combos) { combo->setModel(m_comboModel); @@ -206,17 +208,38 @@ void CsvImportWidget::writeDatabase() entry->setUrl(m_parserModel->data(m_parserModel->index(r, 4)).toString()); entry->setNotes(m_parserModel->data(m_parserModel->index(r, 5)).toString()); - TimeInfo timeInfo; if (m_parserModel->data(m_parserModel->index(r, 6)).isValid()) { - qint64 lastModified = m_parserModel->data(m_parserModel->index(r, 6)).toString().toLongLong(); - if (lastModified) { - timeInfo.setLastModificationTime(Clock::datetimeUtc(lastModified * 1000)); + auto totp = Totp::parseSettings(m_parserModel->data(m_parserModel->index(r, 6)).toString()); + entry->setTotp(totp); + } + + bool ok; + int icon = m_parserModel->data(m_parserModel->index(r, 7)).toInt(&ok); + if (ok) { + entry->setIcon(icon); + } + + TimeInfo timeInfo; + if (m_parserModel->data(m_parserModel->index(r, 8)).isValid()) { + auto datetime = m_parserModel->data(m_parserModel->index(r, 8)).toString(); + if (datetime.contains(QRegularExpression("^\\d+$"))) { + timeInfo.setLastModificationTime(Clock::datetimeUtc(datetime.toLongLong() * 1000)); + } else { + auto lastModified = QDateTime::fromString(datetime, Qt::ISODate); + if (lastModified.isValid()) { + timeInfo.setLastModificationTime(lastModified); + } } } - if (m_parserModel->data(m_parserModel->index(r, 7)).isValid()) { - qint64 created = m_parserModel->data(m_parserModel->index(r, 7)).toString().toLongLong(); - if (created) { - timeInfo.setCreationTime(Clock::datetimeUtc(created * 1000)); + if (m_parserModel->data(m_parserModel->index(r, 9)).isValid()) { + auto datetime = m_parserModel->data(m_parserModel->index(r, 9)).toString(); + if (datetime.contains(QRegularExpression("^\\d+$"))) { + timeInfo.setCreationTime(Clock::datetimeUtc(datetime.toLongLong() * 1000)); + } else { + auto created = QDateTime::fromString(datetime, Qt::ISODate); + if (created.isValid()) { + timeInfo.setCreationTime(created); + } } } entry->setTimeInfo(timeInfo); diff --git a/src/gui/csvImport/CsvImportWidget.ui b/src/gui/csvImport/CsvImportWidget.ui index 1c268fd9d..d3364cb4c 100644 --- a/src/gui/csvImport/CsvImportWidget.ui +++ b/src/gui/csvImport/CsvImportWidget.ui @@ -96,21 +96,8 @@ - - - - - 50 - false - - - - Last Modified - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - + + @@ -126,13 +113,13 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + 2 + - - - - - + + 50 @@ -140,28 +127,21 @@ - Created + Username Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + 2 + - - - - - 50 - false - - - - Notes - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - + + + + + @@ -177,6 +157,9 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + 2 + @@ -196,9 +179,12 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + 2 + - + @@ -212,10 +198,16 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + 2 + - - + + + + + 50 @@ -223,30 +215,106 @@ - Username + Notes Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + 2 + - - - - - - - - - - - + + + + + 50 + false + + + + TOTP + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 2 + + + + + + + + + + + + + + 50 + false + + + + Created + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 2 + + + + + + + + 50 + false + + + + Last Modified + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 2 + + - + + + + + + + 50 + false + + + + Icon + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 2 + + + + + @@ -682,10 +750,6 @@ titleCombo usernameCombo passwordCombo - urlCombo - notesCombo - lastModifiedCombo - createdCombo comboBoxCodec comboBoxTextQualifier comboBoxFieldSeparator diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index 348afb670..80af58bad 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -928,10 +928,10 @@ void TestCli::testExport() setInput("a"); execCmd(exportCmd, {"export", "-f", "csv", m_dbFile->fileName()}); QByteArray csvHeader = m_stdout->readLine(); - QCOMPARE(csvHeader, QByteArray("\"Group\",\"Title\",\"Username\",\"Password\",\"URL\",\"Notes\"\n")); + QVERIFY(csvHeader.contains(QByteArray("\"Group\",\"Title\",\"Username\",\"Password\",\"URL\",\"Notes\""))); QByteArray csvData = m_stdout->readAll(); QVERIFY(csvData.contains(QByteArray( - "\"NewDatabase\",\"Sample Entry\",\"User Name\",\"Password\",\"http://www.somesite.com/\",\"Notes\"\n"))); + "\"NewDatabase\",\"Sample Entry\",\"User Name\",\"Password\",\"http://www.somesite.com/\",\"Notes\""))); // test invalid format setInput("a"); diff --git a/tests/TestCsvExporter.cpp b/tests/TestCsvExporter.cpp index 63ba11488..8e7e6021d 100644 --- a/tests/TestCsvExporter.cpp +++ b/tests/TestCsvExporter.cpp @@ -23,11 +23,13 @@ #include "crypto/Crypto.h" #include "format/CsvExporter.h" +#include "totp/totp.h" QTEST_GUILESS_MAIN(TestCsvExporter) const QString TestCsvExporter::ExpectedHeaderLine = - QString("\"Group\",\"Title\",\"Username\",\"Password\",\"URL\",\"Notes\"\n"); + QString("\"Group\",\"Title\",\"Username\",\"Password\",\"URL\",\"Notes\",\"TOTP\",\"Icon\",\"Last " + "Modified\",\"Created\"\n"); void TestCsvExporter::init() { @@ -57,17 +59,23 @@ void TestCsvExporter::testExport() entry->setPassword("Test Password"); entry->setUrl("http://test.url"); entry->setNotes("Test Notes"); + entry->setTotp(Totp::createSettings("DFDF", Totp::DEFAULT_DIGITS, Totp::DEFAULT_STEP)); + entry->setIcon(5); QBuffer buffer; QVERIFY(buffer.open(QIODevice::ReadWrite)); m_csvExporter->exportDatabase(&buffer, m_db); + auto exported = QString::fromUtf8(buffer.buffer()); QString expectedResult = QString() .append(ExpectedHeaderLine) .append("\"Passwords/Test Group Name\",\"Test Entry Title\",\"Test Username\",\"Test " - "Password\",\"http://test.url\",\"Test Notes\"\n"); + "Password\",\"http://test.url\",\"Test Notes\""); - QCOMPARE(QString::fromUtf8(buffer.buffer().constData()), expectedResult); + QVERIFY(exported.startsWith(expectedResult)); + exported.remove(expectedResult); + QVERIFY(exported.contains("otpauth://")); + QVERIFY(exported.contains(",\"5\",")); } void TestCsvExporter::testEmptyDatabase() @@ -95,10 +103,9 @@ void TestCsvExporter::testNestedGroups() QBuffer buffer; QVERIFY(buffer.open(QIODevice::ReadWrite)); m_csvExporter->exportDatabase(&buffer, m_db); - - QCOMPARE( - QString::fromUtf8(buffer.buffer().constData()), + auto exported = QString::fromUtf8(buffer.buffer()); + QVERIFY(exported.startsWith( QString() .append(ExpectedHeaderLine) - .append("\"Passwords/Test Group Name/Test Sub Group Name\",\"Test Entry Title\",\"\",\"\",\"\",\"\"\n")); + .append("\"Passwords/Test Group Name/Test Sub Group Name\",\"Test Entry Title\",\"\",\"\",\"\",\"\""))); } From 30989e35bf11c508a310463308a340fd3dce99bd Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sun, 1 Nov 2020 08:20:40 -0500 Subject: [PATCH 05/27] Use strict check for std::sort to prevent recursion * Fixes #5596 --- src/browser/BrowserService.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index 9cb2e2729..c2198faf2 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -745,7 +745,7 @@ BrowserService::sortEntries(QList& pwEntries, const QString& siteUrlStr, // Sort same priority entries by Title or UserName auto entries = priorities.values(key); std::sort(entries.begin(), entries.end(), [&sortField](Entry* left, Entry* right) { - return QString::localeAwareCompare(left->attribute(sortField), right->attribute(sortField)); + return QString::localeAwareCompare(left->attribute(sortField), right->attribute(sortField)) < 0; }); results << entries; if (browserSettings()->bestMatchOnly() && !results.isEmpty()) { From 2fe9ea3f41cac87fb1c524d5a6f223dd2c9f1f7c Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Wed, 11 Nov 2020 17:59:53 -0500 Subject: [PATCH 06/27] Document support for managed Microsoft Edge Add documentation for system administrators to setup support for KeePassXC extension in a managed Microsoft Edge. --- docs/topics/BrowserPlugin.adoc | 37 ++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/topics/BrowserPlugin.adoc b/docs/topics/BrowserPlugin.adoc index 0331eda7f..380daf1a5 100644 --- a/docs/topics/BrowserPlugin.adoc +++ b/docs/topics/BrowserPlugin.adoc @@ -23,6 +23,10 @@ You can download the KeePassXC-Browser extension from your web browser. To downl 2. Click the button to install/add the extension to the browser. Accept any confirmation dialogs. +// tag::advanced[] +NOTE: When Microsoft Edge is installed as a managed application, system administrators are required to deploy a custom native messaging configuration. Instructions for this are found in the advanced section below. +// end::advanced[] + === Configure KeePassXC-Browser To start using KeePassXC-Browser, you must configure it so that it can communicate with the KeePassXC application on your desktop. @@ -104,5 +108,38 @@ WARNING: We do not recommend changing any of these settings as they may break th .Advanced browser settings image::browser_advanced_settings.png[] + +=== Advanced Setup +==== Managed Microsoft Edge on Windows +1. Deploy *org.keepassxc.keepassxc_browser_edge.json* to, for example, `C:\ProgramData\KeepassXC` on all managed platforms. ++ +---- +{ + "allowed_origins": [ + "chrome-extension://pdffhmdngciaglkoonimfcmckehcpafo/" + ], + "description": "KeePassXC integration with native messaging support", + "name": "org.keepassxc.keepassxc_browser", + "path": "C:\\Program Files\\KeePassXC\\keepassxc-proxy.exe", + "type": "stdio" +} +---- + +2. Configure GPO options (registry result): ++ +---- +Windows Registry Editor Version 5.00 +[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Edge\NativeMessagingHosts\org.keepassxc.keepassxc_browser] +@="C:\ProgramData\KeepassXC\org.keepassxc.keepassxc_browser_edge.json" + +[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Edge] +"NativeMessagingUserLevelHosts"=dword:00000000 + +[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Edge\ExtensionInstallAllowlist] +"1"="pdffhmdngciaglkoonimfcmckehcpafo" + +[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Edge\NativeMessagingAllowlist] +"1"="org.keepassxc.keepassxc_browser" +---- // end::advanced[] // end::content[] From a273deae120e56c78dc27fd76ee64a9ad2f38388 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Thu, 19 Nov 2020 22:08:49 -0500 Subject: [PATCH 07/27] Add delay to login startup on Linux * Fix #5691 - add a 2 second delay to startup on Gnome to allow for tray initialization and Auto-Type shortcut registration. On KDE, start after the panel is started. --- src/gui/osutils/nixutils/NixUtils.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/gui/osutils/nixutils/NixUtils.cpp b/src/gui/osutils/nixutils/NixUtils.cpp index b252458e5..a5e023f2d 100644 --- a/src/gui/osutils/nixutils/NixUtils.cpp +++ b/src/gui/osutils/nixutils/NixUtils.cpp @@ -107,7 +107,9 @@ void NixUtils::setLaunchAtStartup(bool enable) << QStringLiteral("Version=1.0") << "true" << '\n' << QStringLiteral("Categories=Utility;Security;Qt;") << '\n' << QStringLiteral("MimeType=application/x-keepass2;") << '\n' - << QStringLiteral("X-GNOME-Autostart-enabled=true") << endl; + << QStringLiteral("X-GNOME-Autostart-enabled=true") << '\n' + << QStringLiteral("X-GNOME-Autostart-Delay=2") << '\n' + << QStringLiteral("X-KDE-autostart-after=panel") << endl; desktopFile.close(); } else if (isLaunchAtStartupEnabled()) { QFile::remove(getAutostartDesktopFilename()); From 59bd238ae19bd93b8bdf632a55c42593b974861c Mon Sep 17 00:00:00 2001 From: tWido Date: Fri, 4 Dec 2020 14:01:36 +0100 Subject: [PATCH 08/27] Hide keyfile path by default Fixes #5576 --- src/gui/DatabaseOpenWidget.cpp | 2 ++ src/gui/DatabaseOpenWidget.ui | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp index b48f86365..6cf61e20a 100644 --- a/src/gui/DatabaseOpenWidget.cpp +++ b/src/gui/DatabaseOpenWidget.cpp @@ -165,6 +165,7 @@ void DatabaseOpenWidget::clearForms() m_ui->editPassword->setText(""); m_ui->editPassword->setShowPassword(false); m_ui->keyFileLineEdit->clear(); + m_ui->keyFileLineEdit->setShowPassword(false); m_ui->checkTouchID->setChecked(false); m_ui->challengeResponseCombo->clear(); m_db.reset(); @@ -378,6 +379,7 @@ void DatabaseOpenWidget::browseKeyFile() void DatabaseOpenWidget::clearKeyFileText() { m_ui->keyFileLineEdit->clear(); + m_ui->keyFileLineEdit->setShowPassword(false); } void DatabaseOpenWidget::pollHardwareKey() diff --git a/src/gui/DatabaseOpenWidget.ui b/src/gui/DatabaseOpenWidget.ui index eef4a2ed2..167f4b7d4 100644 --- a/src/gui/DatabaseOpenWidget.ui +++ b/src/gui/DatabaseOpenWidget.ui @@ -406,7 +406,7 @@ 0 - + 0 @@ -416,6 +416,9 @@ Key file to unlock the database + + QLineEdit::Password + true From 3b29f20d60f8fa00581c8c19a2e43e420d7a5d09 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Wed, 9 Dec 2020 18:24:59 -0500 Subject: [PATCH 09/27] Hide actions when features are disabled * Fix #5794 - Don't show "Download All Favicons" in group menu * Don't show offline documentation links if built documentation is disabled --- src/gui/MainWindow.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index f3a6e5831..cbfc1b077 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -519,6 +519,11 @@ MainWindow::MainWindow() m_ui->actionGroupDownloadFavicons->setVisible(false); m_ui->actionEntryDownloadIcon->setVisible(false); #endif +#ifndef WITH_XC_DOCS + m_ui->actionGettingStarted->setVisible(false); + m_ui->actionUserGuide->setVisible(false); + m_ui->actionKeyboardShortcuts->setVisible(false); +#endif // clang-format off connect(m_ui->tabWidget, SIGNAL(messageGlobal(QString,MessageWidget::MessageType)), @@ -743,7 +748,9 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_ui->actionGroupSortDesc->setEnabled(groupSelected && currentGroupHasChildren); m_ui->actionGroupEmptyRecycleBin->setVisible(recycleBinSelected); m_ui->actionGroupEmptyRecycleBin->setEnabled(recycleBinSelected); +#ifdef WITH_XC_NETWORKING m_ui->actionGroupDownloadFavicons->setVisible(!recycleBinSelected); +#endif m_ui->actionGroupDownloadFavicons->setEnabled(groupSelected && currentGroupHasEntries && !recycleBinSelected); m_ui->actionDatabaseSecurity->setEnabled(true); From f0204dbb109a8fbaf739c35b9c459d247bdccd88 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sat, 12 Dec 2020 12:08:02 -0500 Subject: [PATCH 10/27] Fix closing modal dialogs on database lock * Fixes #5719, Fixes #5744 --- src/browser/BrowserService.cpp | 4 ++-- src/gui/DatabaseWidget.cpp | 10 +++++++++- src/gui/MainWindow.cpp | 5 ----- src/gui/TotpDialog.cpp | 8 +------- src/gui/TotpExportSettingsDialog.cpp | 1 - 5 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index c2198faf2..5e1173df4 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -321,7 +321,7 @@ QString BrowserService::storeKey(const QString& key) do { QInputDialog keyDialog; - connect(m_currentDatabaseWidget, SIGNAL(databaseLocked()), &keyDialog, SLOT(reject())); + connect(m_currentDatabaseWidget, SIGNAL(databaseLockRequested()), &keyDialog, SLOT(reject())); keyDialog.setWindowTitle(tr("KeePassXC: New key association request")); keyDialog.setLabelText(tr("You have received an association request for the following database:\n%1\n\n" "Give the connection a unique name or ID, for example:\nchrome-laptop.") @@ -772,7 +772,7 @@ QList BrowserService::confirmEntries(QList& pwEntriesToConfirm, updateWindowState(); BrowserAccessControlDialog accessControlDialog; - connect(m_currentDatabaseWidget, SIGNAL(databaseLocked()), &accessControlDialog, SLOT(reject())); + connect(m_currentDatabaseWidget, SIGNAL(databaseLockRequested()), &accessControlDialog, SLOT(reject())); connect(&accessControlDialog, &BrowserAccessControlDialog::disableAccess, [&](QTableWidgetItem* item) { auto entry = pwEntriesToConfirm[item->row()]; diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 505e7f0ba..3846db76b 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -216,7 +216,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent) #ifdef WITH_XC_SSHAGENT if (sshAgent()->isEnabled()) { - connect(this, SIGNAL(databaseLockRequested()), sshAgent(), SLOT(databaseLocked())); + connect(this, SIGNAL(databaseLocked()), sshAgent(), SLOT(databaseLocked())); connect(this, SIGNAL(databaseUnlocked()), sshAgent(), SLOT(databaseUnlocked())); } #endif @@ -437,6 +437,7 @@ void DatabaseWidget::showTotp() } auto totpDialog = new TotpDialog(this, currentEntry); + connect(this, &DatabaseWidget::databaseLockRequested, totpDialog, &TotpDialog::close); totpDialog->open(); } @@ -460,6 +461,7 @@ void DatabaseWidget::setupTotp() auto setupTotpDialog = new TotpSetupDialog(this, currentEntry); connect(setupTotpDialog, SIGNAL(totpUpdated()), SIGNAL(entrySelectionChanged())); + connect(this, &DatabaseWidget::databaseLockRequested, setupTotpDialog, &TotpSetupDialog::close); setupTotpDialog->open(); } @@ -701,6 +703,7 @@ void DatabaseWidget::showTotpKeyQrCode() auto currentEntry = currentSelectedEntry(); if (currentEntry) { auto totpDisplayDialog = new TotpExportSettingsDialog(this, currentEntry); + connect(this, &DatabaseWidget::databaseLockRequested, totpDisplayDialog, &TotpExportSettingsDialog::close); totpDisplayDialog->open(); } } @@ -1528,6 +1531,11 @@ bool DatabaseWidget::lock() emit databaseLockRequested(); + // ignore event if we are active and a modal dialog is still open (such as a message box or file dialog) + if (isVisible() && QApplication::activeModalWidget()) { + return false; + } + clipboard()->clearCopiedText(); if (isEditWidgetModified()) { diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index cbfc1b077..aa6205e15 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -1536,11 +1536,6 @@ void MainWindow::toggleWindow() void MainWindow::lockDatabasesAfterInactivity() { - // ignore event if a modal dialog is open (such as a message box or file dialog) - if (QApplication::activeModalWidget()) { - return; - } - m_ui->tabWidget->lockDatabases(); } diff --git a/src/gui/TotpDialog.cpp b/src/gui/TotpDialog.cpp index 871c2863c..c348b3f27 100644 --- a/src/gui/TotpDialog.cpp +++ b/src/gui/TotpDialog.cpp @@ -31,10 +31,7 @@ TotpDialog::TotpDialog(QWidget* parent, Entry* entry) , m_ui(new Ui::TotpDialog()) , m_entry(entry) { - if (!m_entry->hasTotp()) { - close(); - return; - } + setAttribute(Qt::WA_DeleteOnClose); m_ui->setupUi(this); @@ -42,14 +39,11 @@ TotpDialog::TotpDialog(QWidget* parent, Entry* entry) resetCounter(); updateProgressBar(); - connect(parent, SIGNAL(databaseLocked()), SLOT(close())); connect(&m_totpUpdateTimer, SIGNAL(timeout()), this, SLOT(updateProgressBar())); connect(&m_totpUpdateTimer, SIGNAL(timeout()), this, SLOT(updateSeconds())); m_totpUpdateTimer.start(m_step * 10); updateTotp(); - setAttribute(Qt::WA_DeleteOnClose); - new QShortcut(QKeySequence(QKeySequence::Copy), this, SLOT(copyToClipboard())); m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Copy")); diff --git a/src/gui/TotpExportSettingsDialog.cpp b/src/gui/TotpExportSettingsDialog.cpp index f73b9877a..cee9fc824 100644 --- a/src/gui/TotpExportSettingsDialog.cpp +++ b/src/gui/TotpExportSettingsDialog.cpp @@ -59,7 +59,6 @@ TotpExportSettingsDialog::TotpExportSettingsDialog(DatabaseWidget* parent, Entry connect(m_buttonBox, SIGNAL(rejected()), SLOT(close())); connect(m_buttonBox, SIGNAL(accepted()), SLOT(copyToClipboard())); connect(m_timer, SIGNAL(timeout()), SLOT(autoClose())); - connect(parent, SIGNAL(lockedDatabase()), SLOT(close())); new QShortcut(QKeySequence(QKeySequence::Copy), this, SLOT(copyToClipboard())); From 2ee416895682d83cd49e9772a124c39ef2430e65 Mon Sep 17 00:00:00 2001 From: krsnik93 Date: Sun, 1 Nov 2020 16:24:47 +0000 Subject: [PATCH 11/27] Display default autotype sequence on entry preview pane * Fix #5450 --- src/gui/EntryPreviewWidget.cpp | 4 +- src/gui/EntryPreviewWidget.ui | 56 +++++++++++ tests/gui/TestGui.cpp | 168 ++++++++++++++++++++++++++++++++- tests/gui/TestGui.h | 1 + 4 files changed, 227 insertions(+), 2 deletions(-) diff --git a/src/gui/EntryPreviewWidget.cpp b/src/gui/EntryPreviewWidget.cpp index 1dc05c3b7..38afbcf24 100644 --- a/src/gui/EntryPreviewWidget.cpp +++ b/src/gui/EntryPreviewWidget.cpp @@ -303,6 +303,8 @@ void EntryPreviewWidget::updateEntryAdvancedTab() void EntryPreviewWidget::updateEntryAutotypeTab() { Q_ASSERT(m_currentEntry); + + m_ui->entrySequenceLabel->setText(m_currentEntry->effectiveAutoTypeSequence()); m_ui->entryAutotypeTree->clear(); QList items; const AutoTypeAssociations* autotypeAssociations = m_currentEntry->autoTypeAssociations(); @@ -314,7 +316,7 @@ void EntryPreviewWidget::updateEntryAutotypeTab() } m_ui->entryAutotypeTree->addTopLevelItems(items); - setTabEnabled(m_ui->entryTabWidget, m_ui->entryAutotypeTab, !items.isEmpty()); + setTabEnabled(m_ui->entryTabWidget, m_ui->entryAutotypeTab, m_currentEntry->autoTypeEnabled()); } void EntryPreviewWidget::updateGroupHeaderLine() diff --git a/src/gui/EntryPreviewWidget.ui b/src/gui/EntryPreviewWidget.ui index d78b04a43..961e75505 100644 --- a/src/gui/EntryPreviewWidget.ui +++ b/src/gui/EntryPreviewWidget.ui @@ -705,6 +705,62 @@ Autotype + + + + + 0 + + + 0 + + + 0 + + + 5 + + + + + + 0 + 0 + + + + + 75 + true + + + + Default Sequence + + + Qt::AlignRight|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + sequence + + + Qt::AlignLeft|Qt::AlignVCenter + + + + + + diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 4ab29ce37..7c24b0fc2 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -29,14 +29,18 @@ #include #include #include +#include #include #include #include +#include #include #include +#include #include #include #include +#include #include "config-keepassx-tests.h" #include "core/Bootstrap.h" @@ -142,6 +146,9 @@ void TestGui::init() fileDialog()->setNextFileName(m_dbFilePath); triggerAction("actionDatabaseOpen"); + QApplication::processEvents(); + + m_dbWidget = m_tabWidget->currentDatabaseWidget(); auto* databaseOpenWidget = m_tabWidget->currentDatabaseWidget()->findChild("databaseOpenWidget"); QVERIFY(databaseOpenWidget); auto* editPassword = databaseOpenWidget->findChild("editPassword"); @@ -151,8 +158,10 @@ void TestGui::init() QTest::keyClicks(editPassword, "a"); QTest::keyClick(editPassword, Qt::Key_Enter); - m_dbWidget = m_tabWidget->currentDatabaseWidget(); + QTRY_VERIFY(!m_dbWidget->isLocked()); m_db = m_dbWidget->database(); + + QApplication::processEvents(); } // Every test ends with closing the temp database without saving @@ -1484,6 +1493,163 @@ void TestGui::testTrayRestoreHide() trayIcon->activated(QSystemTrayIcon::DoubleClick); QTRY_VERIFY(!m_mainWindow->isVisible()); + + // Ensure window is visible at the end + trayIcon->activated(QSystemTrayIcon::DoubleClick); + QTRY_VERIFY(m_mainWindow->isVisible()); +} + +void TestGui::testAutoType() +{ + // Clear entries from root group to guarantee order + for (Entry* entry : m_db->rootGroup()->entries()) { + m_db->rootGroup()->removeEntry(entry); + } + Tools::wait(150); + + // 1. Create an entry with Auto-Type disabled + + // 1.a) Click the new entry button and set the title + auto* entryNewAction = m_mainWindow->findChild("actionEntryNew"); + QVERIFY(entryNewAction->isEnabled()); + + auto* toolBar = m_mainWindow->findChild("toolBar"); + QVERIFY(toolBar); + + QWidget* entryNewWidget = toolBar->widgetForAction(entryNewAction); + QVERIFY(entryNewWidget->isVisible()); + QVERIFY(entryNewWidget->isEnabled()); + + QTest::mouseClick(entryNewWidget, Qt::LeftButton); + QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode); + + auto* editEntryWidget = m_dbWidget->findChild("editEntryWidget"); + QVERIFY(editEntryWidget); + + auto* titleEdit = editEntryWidget->findChild("titleEdit"); + QVERIFY(titleEdit); + + QTest::keyClicks(titleEdit, "1. Entry With Disabled Auto-Type"); + + auto* usernameComboBox = editEntryWidget->findChild("usernameComboBox"); + QVERIFY(usernameComboBox); + + QTest::mouseClick(usernameComboBox, Qt::LeftButton); + QTest::keyClicks(usernameComboBox, "AutocompletionUsername"); + + // 1.b) Uncheck Auto-Type checkbox + editEntryWidget->setCurrentPage(3); + auto* enableAutoTypeButton = editEntryWidget->findChild("enableButton"); + QVERIFY(enableAutoTypeButton); + QVERIFY(enableAutoTypeButton->isVisible()); + QVERIFY(enableAutoTypeButton->isEnabled()); + + enableAutoTypeButton->click(); + QVERIFY(!enableAutoTypeButton->isChecked()); + + // 1.c) Save changes + editEntryWidget->setCurrentPage(0); + auto* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox"); + QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + + // 2. Create an entry with default/inherited Auto-Type sequence + + // 2.a) Click the new entry button and set the title + QTest::mouseClick(entryNewWidget, Qt::LeftButton); + QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode); + QTest::keyClicks(titleEdit, "2. Entry With Default Auto-Type Sequence"); + QTest::mouseClick(usernameComboBox, Qt::LeftButton); + QTest::keyClicks(usernameComboBox, "AutocompletionUsername"); + + // 2.b) Confirm AutoType is enabled and default + editEntryWidget->setCurrentPage(3); + QVERIFY(enableAutoTypeButton->isChecked()); + auto* inheritSequenceButton = editEntryWidget->findChild("inheritSequenceButton"); + QVERIFY(inheritSequenceButton->isChecked()); + + // 2.c) Save changes + editEntryWidget->setCurrentPage(0); + QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + + // 3. Create an entry with custom Auto-Type sequence + + // 3.a) Click the new entry button and set the title + QTest::mouseClick(entryNewWidget, Qt::LeftButton); + QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode); + QTest::keyClicks(titleEdit, "3. Entry With Custom Auto-Type Sequence"); + QTest::mouseClick(usernameComboBox, Qt::LeftButton); + QTest::keyClicks(usernameComboBox, "AutocompletionUsername"); + + // 3.b) Confirm AutoType is enabled and set custom sequence + editEntryWidget->setCurrentPage(3); + QVERIFY(enableAutoTypeButton->isChecked()); + auto* customSequenceButton = editEntryWidget->findChild("customSequenceButton"); + QTest::mouseClick(customSequenceButton, Qt::LeftButton); + QVERIFY(customSequenceButton->isChecked()); + QVERIFY(!inheritSequenceButton->isChecked()); + auto* sequenceEdit = editEntryWidget->findChild("sequenceEdit"); + QVERIFY(sequenceEdit); + sequenceEdit->setFocus(); + QTRY_VERIFY(sequenceEdit->hasFocus()); + QTest::keyClicks(sequenceEdit, "{USERNAME}{TAB}{TAB}{PASSWORD}{ENTER}"); + + // 3.c) Save changes + editEntryWidget->setCurrentPage(0); + QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + QApplication::processEvents(); + + // Check total number of entries matches expected + auto* entryView = m_dbWidget->findChild("entryView"); + QVERIFY(entryView); + QTRY_COMPARE(entryView->model()->rowCount(), 3); + + // Sort entries by title + entryView->sortByColumn(1, Qt::AscendingOrder); + + // Select first entry + entryView->selectionModel()->clearSelection(); + QModelIndex entryIndex = entryView->model()->index(0, 0); + entryView->selectionModel()->select(entryIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select); + + auto* entryPreviewWidget = m_dbWidget->findChild("previewWidget"); + QVERIFY(entryPreviewWidget->isVisible()); + + // Check that the Autotype tab in entry preview pane is disabled for entry with disabled Auto-Type + auto* entryAutotypeTab = entryPreviewWidget->findChild("entryAutotypeTab"); + QVERIFY(!entryAutotypeTab->isEnabled()); + + // Check that Auto-Type is disabled in the actual entry model as well + Entry* entry = entryView->entryFromIndex(entryIndex); + QVERIFY(!entry->autoTypeEnabled()); + + // Select second entry + entryView->selectionModel()->clearSelection(); + entryIndex = entryView->model()->index(1, 0); + entryView->selectionModel()->select(entryIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select); + QVERIFY(entryPreviewWidget->isVisible()); + + // Check that the Autotype tab in entry preview pane is enabled for entry with default Auto-Type sequence; + QVERIFY(entryAutotypeTab->isEnabled()); + + // Check that Auto-Type is enabled in the actual entry model as well + entry = entryView->entryFromIndex(entryIndex); + QVERIFY(entry->autoTypeEnabled()); + + // Select third entry + entryView->selectionModel()->clearSelection(); + entryIndex = entryView->model()->index(2, 0); + entryView->selectionModel()->select(entryIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select); + QVERIFY(entryPreviewWidget->isVisible()); + + // Check that the Autotype tab in entry preview pane is enabled for entry with custom Auto-Type sequence + QVERIFY(entryAutotypeTab->isEnabled()); + + // Check that Auto-Type is enabled in the actual entry model as well + entry = entryView->entryFromIndex(entryIndex); + QVERIFY(entry->autoTypeEnabled()); + + // De-select third entry + entryView->selectionModel()->clearSelection(); } int TestGui::addCannedEntries() diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h index 8d82e021e..5bfc04265 100644 --- a/tests/gui/TestGui.h +++ b/tests/gui/TestGui.h @@ -68,6 +68,7 @@ private slots: void testDatabaseLocking(); void testDragAndDropKdbxFiles(); void testSortGroups(); + void testAutoType(); void testTrayRestoreHide(); private: From e8dfa9cfa1ce72fdb0b85e5f62ad98fd0173364e Mon Sep 17 00:00:00 2001 From: Michel D'HOOGE Date: Sat, 19 Dec 2020 10:49:53 -0500 Subject: [PATCH 12/27] Fix display issues of entry attributes in preview pane * Fix #5755 - HTML escape attributes prior to preview * Place attribute preview into a table and convert line breaks --- src/gui/EntryPreviewWidget.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/gui/EntryPreviewWidget.cpp b/src/gui/EntryPreviewWidget.cpp index 38afbcf24..45a71a348 100644 --- a/src/gui/EntryPreviewWidget.cpp +++ b/src/gui/EntryPreviewWidget.cpp @@ -286,14 +286,18 @@ void EntryPreviewWidget::updateEntryAdvancedTab() setTabEnabled(m_ui->entryTabWidget, m_ui->entryAdvancedTab, hasAttributes || hasAttachments); if (hasAttributes) { - QString attributesText; + QString attributesText(""); for (const QString& key : customAttributes) { - QString value = m_currentEntry->attributes()->value(key); + QString value; if (m_currentEntry->attributes()->isProtected(key)) { value = "" + tr("[PROTECTED]") + ""; + } else { + value = m_currentEntry->attributes()->value(key).toHtmlEscaped(); + value.replace('\n', QLatin1String("
")); } - attributesText.append(tr("%1: %2", "attributes line").arg(key, value).append("
")); + attributesText.append(tr("", "attributes line").arg(key, value)); } + attributesText.append("
%1:%2
"); m_ui->entryAttributesEdit->setText(attributesText); } From 260c84ccf0ba91223ca844ea705808f4b3fd1fd7 Mon Sep 17 00:00:00 2001 From: Bernhard <34011017+Colfenor@users.noreply.github.com> Date: Sat, 17 Oct 2020 00:42:59 +0200 Subject: [PATCH 13/27] Fix reSelect entry & group on loadDb --- src/gui/DatabaseWidget.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 3846db76b..28d3aedd7 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -1084,6 +1084,9 @@ void DatabaseWidget::loadDatabase(bool accepted) replaceDatabase(openWidget->database()); switchToMainView(); processAutoOpen(); + restoreGroupEntryFocus(m_groupBeforeLock, m_entryBeforeLock); + m_groupBeforeLock = QUuid(); + m_entryBeforeLock = QUuid(); m_saveAttempts = 0; emit databaseUnlocked(); if (config()->get(Config::MinimizeAfterUnlock).toBool()) { From f9b2cf848406f6b30dd1da4fc3280138795377bb Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Wed, 9 Dec 2020 17:58:07 -0500 Subject: [PATCH 14/27] Remove offset on username field in classic theme * Fix #5601 Fix padding offset in editable QComboBox Accepted --- src/gui/entry/EditEntryWidget.cpp | 3 --- src/gui/styles/base/BaseStyle.cpp | 6 +++++- src/gui/styles/base/classicstyle.qss | 4 ++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 680243280..718a08a84 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -178,9 +178,6 @@ void EditEntryWidget::setupMain() m_mainUi->expirePresets->setMenu(createPresetsMenu()); connect(m_mainUi->expirePresets->menu(), SIGNAL(triggered(QAction*)), this, SLOT(useExpiryPreset(QAction*))); - - // HACK: Align username text with other line edits. Qt does not let you do this with an application stylesheet. - m_mainUi->usernameComboBox->lineEdit()->setStyleSheet("padding-left: 8px;"); } void EditEntryWidget::setupAdvanced() diff --git a/src/gui/styles/base/BaseStyle.cpp b/src/gui/styles/base/BaseStyle.cpp index 104e1d4d9..98ce08f4d 100644 --- a/src/gui/styles/base/BaseStyle.cpp +++ b/src/gui/styles/base/BaseStyle.cpp @@ -4775,7 +4775,11 @@ QRect BaseStyle::subElementRect(SubElement sr, const QStyleOption* opt, const QW } case SE_LineEditContents: { QRect r = QCommonStyle::subElementRect(sr, opt, w); - int pad = Phantom::dpiScaled(Phantom::LineEdit_ContentsHPad); + int pad = Phantom::LineEdit_ContentsHPad; + if (w && qobject_cast(w->parentWidget())) { + pad += 3; + } + pad = Phantom::dpiScaled(pad); return r.adjusted(pad, 0, -pad, 0); } default: diff --git a/src/gui/styles/base/classicstyle.qss b/src/gui/styles/base/classicstyle.qss index 653edd5bb..2d856a3cf 100644 --- a/src/gui/styles/base/classicstyle.qss +++ b/src/gui/styles/base/classicstyle.qss @@ -13,3 +13,7 @@ DatabaseWidget #SearchBanner, DatabaseWidget #KeeShareBanner { border: 1px solid rgb(190, 190, 190); padding: 2px; } + +QLineEdit { + padding-left: 2px; +} From 60c2d89cb02f5821e87906e5cbf1ac1582f5e613 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sun, 13 Dec 2020 11:23:39 -0500 Subject: [PATCH 15/27] Prevent crash when KeeShare merges an entry that is in edit mode * Hack for #5722 until a refactor of KeeShare, Merger, and EditEntryWidget can be performed. This hack should only ever be triggered on the rare occurrence of two people editing the same entry at the same time. The end result is potential data loss, but the current result is a hard crash. Unfortunately the way everything is interfaced currently doesn't afford any solution without a major refactor. * Additionally add a short delay before actually reloading a share to prevent read/write locks from preventing proper import. This delay also prevents conflicting saves between the main database and the KeeShare database. This should eventually be moved into the FileObserver itself to smooth out all merge operations once the above refactor occurs. Side note: KeeShare operates independently of DatabaseWidget causing unexpected behavior when files are updated/merged/etc. This needs to be corrected in a refactor. --- src/gui/entry/EditEntryWidget.cpp | 9 ++++++++ src/keeshare/ShareObserver.cpp | 38 ++++++++++++++++++------------- src/keeshare/ShareObserver.h | 1 + 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 718a08a84..123bb9d48 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -994,6 +994,15 @@ bool EditEntryWidget::commitEntry() return true; } + // HACK: Check that entry pointer is still valid, see https://github.com/keepassxreboot/keepassxc/issues/5722 + if (!m_entry) { + QMessageBox::information(this, + tr("Invalid Entry"), + tr("An external merge operation has invalidated this entry.\n" + "Unfortunately, any changes made have been lost.")); + return true; + } + // Check Auto-Type validity early if (!AutoType::verifyAutoTypeSyntax(m_autoTypeUi->sequenceEdit->text())) { return false; diff --git a/src/keeshare/ShareObserver.cpp b/src/keeshare/ShareObserver.cpp index 6fd629b4c..f74e800a0 100644 --- a/src/keeshare/ShareObserver.cpp +++ b/src/keeshare/ShareObserver.cpp @@ -186,23 +186,29 @@ void ShareObserver::handleDatabaseChanged() void ShareObserver::handleFileUpdated(const QString& path) { - const Result result = importShare(path); - if (!result.isValid()) { - return; + if (!m_inFileUpdate) { + QTimer::singleShot(100, this, [this, path] { + const Result result = importShare(path); + m_inFileUpdate = false; + if (!result.isValid()) { + return; + } + QStringList success; + QStringList warning; + QStringList error; + if (result.isError()) { + error << tr("Import from %1 failed (%2)").arg(result.path, result.message); + } else if (result.isWarning()) { + warning << tr("Import from %1 failed (%2)").arg(result.path, result.message); + } else if (result.isInfo()) { + success << tr("Import from %1 successful (%2)").arg(result.path, result.message); + } else { + success << tr("Imported from %1").arg(result.path); + } + notifyAbout(success, warning, error); + }); + m_inFileUpdate = true; } - QStringList success; - QStringList warning; - QStringList error; - if (result.isError()) { - error << tr("Import from %1 failed (%2)").arg(result.path, result.message); - } else if (result.isWarning()) { - warning << tr("Import from %1 failed (%2)").arg(result.path, result.message); - } else if (result.isInfo()) { - success << tr("Import from %1 successful (%2)").arg(result.path, result.message); - } else { - success << tr("Imported from %1").arg(result.path); - } - notifyAbout(success, warning, error); } ShareObserver::Result ShareObserver::importShare(const QString& path) diff --git a/src/keeshare/ShareObserver.h b/src/keeshare/ShareObserver.h index b98d58981..8b881142d 100644 --- a/src/keeshare/ShareObserver.h +++ b/src/keeshare/ShareObserver.h @@ -83,6 +83,7 @@ private: QMap, KeeShareSettings::Reference> m_groupToReference; QMap> m_shareToGroup; QMap> m_fileWatchers; + bool m_inFileUpdate = false; }; #endif // KEEPASSXC_SHAREOBSERVER_H From 4b5248ee989ec889294a05ed2895768acefdf562 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sun, 20 Dec 2020 10:59:35 -0500 Subject: [PATCH 16/27] Prevent clipboard history and cloud sync on Windows * Fix #2358 --- src/gui/Clipboard.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/gui/Clipboard.cpp b/src/gui/Clipboard.cpp index ddd07f29f..ab4a84cf0 100644 --- a/src/gui/Clipboard.cpp +++ b/src/gui/Clipboard.cpp @@ -59,7 +59,12 @@ void Clipboard::setText(const QString& text, bool clear) clipboard->setMimeData(mime, QClipboard::Clipboard); #else mime->setText(text); +#ifdef Q_OS_LINUX mime->setData("x-kde-passwordManagerHint", QByteArrayLiteral("secret")); +#endif +#ifdef Q_OS_WIN + mime->setData("ExcludeClipboardContentFromMonitorProcessing", QByteArrayLiteral("1")); +#endif clipboard->setMimeData(mime, QClipboard::Clipboard); if (clipboard->supportsSelection()) { From a74e2391e858667dce9d7ea412f409393fbbec6a Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sat, 12 Dec 2020 09:46:23 -0500 Subject: [PATCH 17/27] Copy history when drag/drop entries and groups * Fix #5809 --- src/core/Entry.cpp | 2 -- src/core/Entry.h | 5 +++-- src/core/Group.cpp | 3 --- src/core/Group.h | 6 +++--- src/gui/group/GroupModel.cpp | 10 +++++----- tests/gui/TestGui.cpp | 29 +++++++++++++++++++++++++---- 6 files changed, 36 insertions(+), 19 deletions(-) diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 82c9e7bc3..72b48283a 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -36,8 +36,6 @@ const int Entry::ResolveMaximumDepth = 10; const QString Entry::AutoTypeSequenceUsername = "{USERNAME}{ENTER}"; const QString Entry::AutoTypeSequencePassword = "{PASSWORD}{ENTER}"; -Entry::CloneFlags Entry::DefaultCloneFlags = Entry::CloneNewUuid | Entry::CloneResetTimeInfo; - Entry::Entry() : m_attributes(new EntryAttributes(this)) , m_attachments(new EntryAttachments(this)) diff --git a/src/core/Entry.h b/src/core/Entry.h index a0dbbf7d4..83c8eeb1d 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -160,6 +160,8 @@ public: CloneNewUuid = 1, // generate a random uuid for the clone CloneResetTimeInfo = 2, // set all TimeInfo attributes to the current time CloneIncludeHistory = 4, // clone the history items + CloneDefault = CloneNewUuid | CloneResetTimeInfo, + CloneCopy = CloneNewUuid | CloneResetTimeInfo | CloneIncludeHistory, CloneRenameTitle = 8, // add "-Clone" after the original title CloneUserAsRef = 16, // Add the user as a reference to the original entry ClonePassAsRef = 32, // Add the password as a reference to the original entry @@ -209,7 +211,6 @@ public: static const int ResolveMaximumDepth; static const QString AutoTypeSequenceUsername; static const QString AutoTypeSequencePassword; - static CloneFlags DefaultCloneFlags; /** * Creates a duplicate of this entry except that the returned entry isn't @@ -217,7 +218,7 @@ public: * Note that you need to copy the custom icons manually when inserting the * new entry into another database. */ - Entry* clone(CloneFlags flags = DefaultCloneFlags) const; + Entry* clone(CloneFlags flags = CloneDefault) const; void copyDataFrom(const Entry* other); QString maskPasswordPlaceholders(const QString& str) const; Entry* resolveReference(const QString& str) const; diff --git a/src/core/Group.cpp b/src/core/Group.cpp index 56e545bf6..45e44bff0 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -36,9 +36,6 @@ const int Group::DefaultIconNumber = 48; const int Group::RecycleBinIconNumber = 43; const QString Group::RootAutoTypeSequence = "{USERNAME}{TAB}{PASSWORD}{ENTER}"; -Group::CloneFlags Group::DefaultCloneFlags = - Group::CloneNewUuid | Group::CloneResetTimeInfo | Group::CloneIncludeEntries; - Group::Group() : m_customData(new CustomData(this)) , m_updateTimeinfo(true) diff --git a/src/core/Group.h b/src/core/Group.h index 4bea8e4eb..2997abe17 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -56,6 +56,7 @@ public: CloneNewUuid = 1, // generate a random uuid for the clone CloneResetTimeInfo = 2, // set all TimeInfo attributes to the current time CloneIncludeEntries = 4, // clone the group entries + CloneDefault = CloneNewUuid | CloneResetTimeInfo | CloneIncludeEntries, }; Q_DECLARE_FLAGS(CloneFlags, CloneFlag) @@ -108,7 +109,6 @@ public: static const int DefaultIconNumber; static const int RecycleBinIconNumber; - static CloneFlags DefaultCloneFlags; static const QString RootAutoTypeSequence; Group* findChildByName(const QString& name); @@ -157,8 +157,8 @@ public: QSet customIconsRecursive() const; QList usernamesRecursive(int topN = -1) const; - Group* clone(Entry::CloneFlags entryFlags = Entry::DefaultCloneFlags, - CloneFlags groupFlags = DefaultCloneFlags) const; + Group* clone(Entry::CloneFlags entryFlags = Entry::CloneDefault, + Group::CloneFlags groupFlags = Group::CloneDefault) const; void copyDataFrom(const Group* other); QString print(bool recursive = false, bool flatten = false, int depth = 0); diff --git a/src/gui/group/GroupModel.cpp b/src/gui/group/GroupModel.cpp index e61410334..5feb8d6fe 100644 --- a/src/gui/group/GroupModel.cpp +++ b/src/gui/group/GroupModel.cpp @@ -262,13 +262,13 @@ bool GroupModel::dropMimeData(const QMimeData* data, targetDb->metadata()->copyCustomIcons(customIcons, sourceDb->metadata()); // Always clone the group across db's to reset UUIDs - group = dragGroup->clone(); + group = dragGroup->clone(Entry::CloneDefault | Entry::CloneIncludeHistory); if (action == Qt::MoveAction) { // Remove the original group from the sourceDb delete dragGroup; } } else if (action == Qt::CopyAction) { - group = dragGroup->clone(); + group = dragGroup->clone(Entry::CloneCopy); } group->setParent(parentGroup, row); @@ -303,13 +303,13 @@ bool GroupModel::dropMimeData(const QMimeData* data, targetDb->metadata()->addCustomIcon(customIcon, sourceDb->metadata()->customIcon(customIcon)); } - // Always clone the entry across db's to reset the UUID - entry = dragEntry->clone(); + // Reset the UUID when moving across db boundary + entry = dragEntry->clone(Entry::CloneDefault | Entry::CloneIncludeHistory); if (action == Qt::MoveAction) { delete dragEntry; } } else if (action == Qt::CopyAction) { - entry = dragEntry->clone(); + entry = dragEntry->clone(Entry::CloneCopy); } entry->setGroup(parentGroup); diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 7c24b0fc2..2463f13f6 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -1155,24 +1155,45 @@ void TestGui::testEntryPlaceholders() void TestGui::testDragAndDropEntry() { - auto* entryView = m_dbWidget->findChild("entryView"); - auto* groupView = m_dbWidget->findChild("groupView"); - QAbstractItemModel* groupModel = groupView->model(); + auto entryView = m_dbWidget->findChild("entryView"); + auto groupView = m_dbWidget->findChild("groupView"); + auto groupModel = qobject_cast(groupView->model()); QModelIndex sourceIndex = entryView->model()->index(0, 1); QModelIndex targetIndex = groupModel->index(0, 0, groupModel->index(0, 0)); QVERIFY(sourceIndex.isValid()); QVERIFY(targetIndex.isValid()); + auto targetGroup = groupModel->groupFromIndex(targetIndex); QMimeData mimeData; QByteArray encoded; QDataStream stream(&encoded, QIODevice::WriteOnly); - Entry* entry = entryView->entryFromIndex(sourceIndex); + + auto entry = entryView->entryFromIndex(sourceIndex); stream << entry->group()->database()->uuid() << entry->uuid(); mimeData.setData("application/x-keepassx-entry", encoded); + // Test Copy, UUID should change, history remain + QVERIFY(groupModel->dropMimeData(&mimeData, Qt::CopyAction, -1, 0, targetIndex)); + // Find the copied entry + auto newEntry = targetGroup->findEntryByPath(entry->title()); + QVERIFY(newEntry); + QVERIFY(entry->uuid() != newEntry->uuid()); + QCOMPARE(entry->historyItems().count(), newEntry->historyItems().count()); + + encoded.clear(); + entry = entryView->entryFromIndex(sourceIndex); + auto history = entry->historyItems().count(); + auto uuid = entry->uuid(); + stream << entry->group()->database()->uuid() << entry->uuid(); + mimeData.setData("application/x-keepassx-entry", encoded); + + // Test Move, entry pointer should remain the same + QCOMPARE(entry->group()->name(), QString("NewDatabase")); QVERIFY(groupModel->dropMimeData(&mimeData, Qt::MoveAction, -1, 0, targetIndex)); QCOMPARE(entry->group()->name(), QString("General")); + QCOMPARE(entry->uuid(), uuid); + QCOMPARE(entry->historyItems().count(), history); } void TestGui::testDragAndDropGroup() From 14b01784ec5cfec157a831a7e77028bcc452addf Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sun, 1 Nov 2020 08:19:03 -0500 Subject: [PATCH 18/27] Patron Names and Debug Info * Fix patron name and add new VIP * Add dash in front of libgcrypt in debug info --- src/crypto/Crypto.cpp | 2 +- src/gui/AboutDialog.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/crypto/Crypto.cpp b/src/crypto/Crypto.cpp index 4f54ac1d3..fb9f28bd9 100644 --- a/src/crypto/Crypto.cpp +++ b/src/crypto/Crypto.cpp @@ -73,7 +73,7 @@ QString Crypto::debugInfo() Q_ASSERT(Crypto::initialized()); QString debugInfo = QObject::tr("Cryptographic libraries:").append("\n"); - debugInfo.append(" libgcrypt ").append(m_backendVersion).append("\n"); + debugInfo.append("- libgcrypt ").append(m_backendVersion).append("\n"); return debugInfo; } diff --git a/src/gui/AboutDialog.cpp b/src/gui/AboutDialog.cpp index b97d62590..7c89b8804 100644 --- a/src/gui/AboutDialog.cpp +++ b/src/gui/AboutDialog.cpp @@ -59,6 +59,7 @@ static const QString aboutContributors = R"(
  • Kernellinux
  • Micha Ober
  • PublicByte
  • +
  • Clayton Casciato
  • Notable Code Contributions:

      @@ -86,7 +87,6 @@ static const QString aboutContributors = R"(

    Patreon Supporters: