diff --git a/share/demo.kdbx b/share/demo.kdbx
index 7be77579f..b1a37e99f 100644
Binary files a/share/demo.kdbx and b/share/demo.kdbx differ
diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts
index daef0a8de..b30987eee 100644
--- a/share/translations/keepassxc_en.ts
+++ b/share/translations/keepassxc_en.ts
@@ -1535,6 +1535,28 @@ If you do not have a key file, please leave the field empty.
Please present or touch your YubiKey to continue…
+
+ Database Version Mismatch
+
+
+
+ The database you are trying to open was most likely
+created by a newer version of KeePassXC.
+
+You can try to open it anyway, but it may be incomplete
+and saving any changes may incur data loss.
+
+We recommend you update your KeePassXC installation.
+
+
+
+ Open database anyway
+
+
+
+ Database unlock canceled.
+
+
DatabaseSettingWidgetMetaData
@@ -1808,18 +1830,6 @@ Are you sure you want to continue without a password?
Database format:
Database format:
-
- This is only important if you need to use your database with other programs.
- This is only important if you need to use your database with other programs.
-
-
- KDBX 4.0 (recommended)
- KDBX 4.0 (recommended)
-
-
- KDBX 3.1
- KDBX 3.1
-
unchanged
Database decryption time is unchanged
@@ -1919,6 +1929,22 @@ If you keep this number, your database may take hours, days, or even longer to o
If you keep this number, your database will not be protected from brute force attacks.
+
+ Format cannot be changed: Your database uses KDBX 4 features
+
+
+
+ Unless you need to open your database with other programs, always use the latest format.
+
+
+
+ KDBX 4 (recommended)
+ KDBX 4.0 (recommended) {4 ?}
+
+
+ KDBX 3
+ KDBX 3
+
DatabaseSettingsWidgetFdoSecrets
@@ -6523,10 +6549,6 @@ Available commands:
AES-KDF (KDBX 4)
AES-KDF (KDBX 4)
-
- AES-KDF (KDBX 3.1)
- AES-KDF (KDBX 3.1)
-
Invalid Settings
TOTP
@@ -7498,6 +7520,10 @@ Please consider generating a new key file.
Attachments:
+
+ AES-KDF (KDBX 3)
+ AES-KDF (KDBX 3.1) {3)?}
+
QtIOCompressor
diff --git a/src/core/Database.cpp b/src/core/Database.cpp
index 5738a3b0a..6f7dd6610 100644
--- a/src/core/Database.cpp
+++ b/src/core/Database.cpp
@@ -163,6 +163,27 @@ bool Database::open(const QString& filePath, QSharedPointer
return true;
}
+/**
+ * KDBX format version.
+ */
+quint32 Database::formatVersion() const
+{
+ return m_data.formatVersion;
+}
+
+void Database::setFormatVersion(quint32 version)
+{
+ m_data.formatVersion = version;
+}
+
+/**
+ * Whether the KDBX minor version is greater than the newest supported.
+ */
+bool Database::hasMinorVersionMismatch() const
+{
+ return m_data.formatVersion > KeePass2::FILE_VERSION_MAX;
+}
+
bool Database::isSaving()
{
bool locked = m_saveMutex.tryLock();
@@ -935,6 +956,7 @@ void Database::setKdf(QSharedPointer kdf)
{
Q_ASSERT(!m_data.isReadOnly);
m_data.kdf = std::move(kdf);
+ setFormatVersion(KeePass2Writer::kdbxVersionRequired(this, true, m_data.kdf.isNull()));
}
bool Database::changeKdf(const QSharedPointer& kdf)
diff --git a/src/core/Database.h b/src/core/Database.h
index c42025f85..31d29da7b 100644
--- a/src/core/Database.h
+++ b/src/core/Database.h
@@ -87,6 +87,10 @@ public:
bool extract(QByteArray&, QString* error = nullptr);
bool import(const QString& xmlExportPath, QString* error = nullptr);
+ quint32 formatVersion() const;
+ void setFormatVersion(quint32 version);
+ bool hasMinorVersionMismatch() const;
+
void releaseData();
bool isInitialized() const;
@@ -166,6 +170,7 @@ signals:
private:
struct DatabaseData
{
+ quint32 formatVersion = 0;
QString filePath;
bool isReadOnly = false;
QUuid cipher = KeePass2::CIPHER_AES256;
diff --git a/src/format/Kdbx3Reader.cpp b/src/format/Kdbx3Reader.cpp
index 5c718d6a0..66935b7f7 100644
--- a/src/format/Kdbx3Reader.cpp
+++ b/src/format/Kdbx3Reader.cpp
@@ -34,7 +34,7 @@ bool Kdbx3Reader::readDatabaseImpl(QIODevice* device,
QSharedPointer key,
Database* db)
{
- Q_ASSERT(m_kdbxVersion <= KeePass2::FILE_VERSION_3_1);
+ Q_ASSERT((db->formatVersion() & KeePass2::FILE_VERSION_CRITICAL_MASK) <= KeePass2::FILE_VERSION_3);
if (hasError()) {
return false;
@@ -120,7 +120,7 @@ bool Kdbx3Reader::readDatabaseImpl(QIODevice* device,
return false;
}
- Q_ASSERT(!xmlReader.headerHash().isEmpty() || m_kdbxVersion < KeePass2::FILE_VERSION_3_1);
+ Q_ASSERT(!xmlReader.headerHash().isEmpty() || db->formatVersion() < KeePass2::FILE_VERSION_3_1);
if (!xmlReader.headerHash().isEmpty()) {
QByteArray headerHash = CryptoHash::hash(headerData, CryptoHash::Sha256);
diff --git a/src/format/Kdbx3Writer.cpp b/src/format/Kdbx3Writer.cpp
index 7ba4c3f36..2770239a7 100644
--- a/src/format/Kdbx3Writer.cpp
+++ b/src/format/Kdbx3Writer.cpp
@@ -68,7 +68,7 @@ bool Kdbx3Writer::writeDatabase(QIODevice* device, Database* db)
QBuffer header;
header.open(QIODevice::WriteOnly);
- writeMagicNumbers(&header, KeePass2::SIGNATURE_1, KeePass2::SIGNATURE_2, formatVersion());
+ writeMagicNumbers(&header, KeePass2::SIGNATURE_1, KeePass2::SIGNATURE_2, db->formatVersion());
CHECK_RETURN_FALSE(writeHeaderField(&header, KeePass2::HeaderFieldID::CipherID, db->cipher().toRfc4122()));
CHECK_RETURN_FALSE(
@@ -137,7 +137,7 @@ bool Kdbx3Writer::writeDatabase(QIODevice* device, Database* db)
return false;
}
- KdbxXmlWriter xmlWriter(formatVersion());
+ KdbxXmlWriter xmlWriter(db->formatVersion());
xmlWriter.writeDatabase(outputDevice, db, &randomStream, headerHash);
// Explicitly close/reset streams so they are flushed and we can detect
@@ -161,8 +161,3 @@ bool Kdbx3Writer::writeDatabase(QIODevice* device, Database* db)
return true;
}
-
-quint32 Kdbx3Writer::formatVersion()
-{
- return KeePass2::FILE_VERSION_3_1;
-}
diff --git a/src/format/Kdbx3Writer.h b/src/format/Kdbx3Writer.h
index 45b0a8b51..eb98a470d 100644
--- a/src/format/Kdbx3Writer.h
+++ b/src/format/Kdbx3Writer.h
@@ -29,7 +29,6 @@ class Kdbx3Writer : public KdbxWriter
public:
bool writeDatabase(QIODevice* device, Database* db) override;
- quint32 formatVersion() override;
};
#endif // KEEPASSX_KDBX3WRITER_H
diff --git a/src/format/Kdbx4Reader.cpp b/src/format/Kdbx4Reader.cpp
index c25c3e31b..1dc7067dd 100644
--- a/src/format/Kdbx4Reader.cpp
+++ b/src/format/Kdbx4Reader.cpp
@@ -36,7 +36,7 @@ bool Kdbx4Reader::readDatabaseImpl(QIODevice* device,
QSharedPointer key,
Database* db)
{
- Q_ASSERT(m_kdbxVersion == KeePass2::FILE_VERSION_4);
+ Q_ASSERT((db->formatVersion() & KeePass2::FILE_VERSION_CRITICAL_MASK) == KeePass2::FILE_VERSION_4);
m_binaryPool.clear();
diff --git a/src/format/Kdbx4Writer.cpp b/src/format/Kdbx4Writer.cpp
index cbd6f9511..08a0df013 100644
--- a/src/format/Kdbx4Writer.cpp
+++ b/src/format/Kdbx4Writer.cpp
@@ -66,7 +66,7 @@ bool Kdbx4Writer::writeDatabase(QIODevice* device, Database* db)
QBuffer header;
header.open(QIODevice::WriteOnly);
- writeMagicNumbers(&header, KeePass2::SIGNATURE_1, KeePass2::SIGNATURE_2, formatVersion());
+ writeMagicNumbers(&header, KeePass2::SIGNATURE_1, KeePass2::SIGNATURE_2, db->formatVersion());
CHECK_RETURN_FALSE(
writeHeaderField(&header, KeePass2::HeaderFieldID::CipherID, db->cipher().toRfc4122()));
@@ -166,7 +166,7 @@ bool Kdbx4Writer::writeDatabase(QIODevice* device, Database* db)
return false;
}
- KdbxXmlWriter xmlWriter(formatVersion());
+ KdbxXmlWriter xmlWriter(db->formatVersion());
xmlWriter.writeDatabase(outputDevice, db, &randomStream, headerHash);
// Explicitly close/reset streams so they are flushed and we can detect
@@ -306,8 +306,3 @@ bool Kdbx4Writer::serializeVariantMap(const QVariantMap& map, QByteArray& output
CHECK_RETURN_FALSE(buf.write(endBytes) == 1);
return true;
}
-
-quint32 Kdbx4Writer::formatVersion()
-{
- return KeePass2::FILE_VERSION_4;
-}
diff --git a/src/format/Kdbx4Writer.h b/src/format/Kdbx4Writer.h
index 8ef82f18f..c8540245b 100644
--- a/src/format/Kdbx4Writer.h
+++ b/src/format/Kdbx4Writer.h
@@ -29,7 +29,6 @@ class Kdbx4Writer : public KdbxWriter
public:
bool writeDatabase(QIODevice* device, Database* db) override;
- quint32 formatVersion() override;
private:
bool writeInnerHeaderField(QIODevice* device, KeePass2::InnerHeaderFieldID fieldId, const QByteArray& data);
diff --git a/src/format/KdbxReader.cpp b/src/format/KdbxReader.cpp
index 94ccf3342..5610897c8 100644
--- a/src/format/KdbxReader.cpp
+++ b/src/format/KdbxReader.cpp
@@ -74,14 +74,12 @@ bool KdbxReader::readDatabase(QIODevice* device, QSharedPointersetFormatVersion(version);
// read header fields
while (readHeaderField(headerStream, m_db) && !hasError()) {
diff --git a/src/format/KdbxReader.h b/src/format/KdbxReader.h
index cbc13b20b..a7b9fc37e 100644
--- a/src/format/KdbxReader.h
+++ b/src/format/KdbxReader.h
@@ -83,8 +83,6 @@ protected:
void raiseError(const QString& errorMessage);
- quint32 m_kdbxVersion = 0;
-
QByteArray m_masterSeed;
QByteArray m_encryptionIV;
QByteArray m_streamStartBytes;
diff --git a/src/format/KdbxWriter.cpp b/src/format/KdbxWriter.cpp
index b69cedbf7..b7758c751 100644
--- a/src/format/KdbxWriter.cpp
+++ b/src/format/KdbxWriter.cpp
@@ -71,7 +71,7 @@ void KdbxWriter::extractDatabase(QByteArray& xmlOutput, Database* db)
QBuffer buffer;
buffer.setBuffer(&xmlOutput);
buffer.open(QIODevice::WriteOnly);
- KdbxXmlWriter writer(formatVersion());
+ KdbxXmlWriter writer(db->formatVersion());
writer.disableInnerStreamProtection(true);
writer.writeDatabase(&buffer, db);
}
diff --git a/src/format/KdbxWriter.h b/src/format/KdbxWriter.h
index d5e214a51..bec8fc8df 100644
--- a/src/format/KdbxWriter.h
+++ b/src/format/KdbxWriter.h
@@ -52,11 +52,6 @@ public:
*/
virtual bool writeDatabase(QIODevice* device, Database* db) = 0;
- /**
- * Get the database format version for the writer.
- */
- virtual quint32 formatVersion() = 0;
-
void extractDatabase(QByteArray& xmlOutput, Database* db);
bool hasError() const;
diff --git a/src/format/KdbxXmlWriter.cpp b/src/format/KdbxXmlWriter.cpp
index 4b49b6972..35ed5ffdb 100644
--- a/src/format/KdbxXmlWriter.cpp
+++ b/src/format/KdbxXmlWriter.cpp
@@ -166,7 +166,7 @@ void KdbxXmlWriter::writeIcon(const QUuid& uuid, const Metadata::CustomIconData&
m_xml.writeStartElement("Icon");
writeUuid("UUID", uuid);
- if (m_kdbxVersion >= KeePass2::FILE_VERSION_4) {
+ if (m_kdbxVersion >= KeePass2::FILE_VERSION_4_1) {
if (!iconData.name.isEmpty()) {
writeString("Name", iconData.name);
}
@@ -243,7 +243,7 @@ void KdbxXmlWriter::writeCustomDataItem(const QString& key,
writeString("Key", key);
writeString("Value", item.value);
- if (writeLastModified && m_kdbxVersion >= KeePass2::FILE_VERSION_4 && item.lastModified.isValid()) {
+ if (writeLastModified && m_kdbxVersion >= KeePass2::FILE_VERSION_4_1 && item.lastModified.isValid()) {
writeDateTime("LastModificationTime", item.lastModified);
}
@@ -291,9 +291,9 @@ void KdbxXmlWriter::writeGroup(const Group* group)
if (m_kdbxVersion >= KeePass2::FILE_VERSION_4) {
writeCustomData(group->customData());
- if (!group->previousParentGroupUuid().isNull()) {
- writeUuid("PreviousParentGroup", group->previousParentGroupUuid());
- }
+ }
+ if (m_kdbxVersion >= KeePass2::FILE_VERSION_4_1 && !group->previousParentGroupUuid().isNull()) {
+ writeUuid("PreviousParentGroup", group->previousParentGroupUuid());
}
const QList& entryList = group->entries();
@@ -363,7 +363,7 @@ void KdbxXmlWriter::writeEntry(const Entry* entry)
writeString("Tags", entry->tags());
writeTimes(entry->timeInfo());
- if (m_kdbxVersion >= KeePass2::FILE_VERSION_4) {
+ if (m_kdbxVersion >= KeePass2::FILE_VERSION_4_1) {
if (entry->excludeFromReports()) {
writeBool("QualityCheck", false);
}
diff --git a/src/format/KeePass2.cpp b/src/format/KeePass2.cpp
index cc57ccffa..bf991f880 100644
--- a/src/format/KeePass2.cpp
+++ b/src/format/KeePass2.cpp
@@ -56,7 +56,7 @@ const QList> KeePass2::KDFS{
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)"))};
+ qMakePair(KeePass2::KDF_AES_KDBX3, QObject::tr("AES-KDF (KDBX 3)"))};
QByteArray KeePass2::hmacKey(const QByteArray& masterSeed, const QByteArray& transformedMasterKey)
{
diff --git a/src/format/KeePass2.h b/src/format/KeePass2.h
index c42183295..5aed903c3 100644
--- a/src/format/KeePass2.h
+++ b/src/format/KeePass2.h
@@ -31,16 +31,18 @@ namespace KeePass2
constexpr quint32 SIGNATURE_2 = 0xB54BFB67;
constexpr quint32 FILE_VERSION_CRITICAL_MASK = 0xFFFF0000;
+ constexpr quint32 FILE_VERSION_4_1 = 0x00040001;
constexpr quint32 FILE_VERSION_4 = 0x00040000;
constexpr quint32 FILE_VERSION_3_1 = 0x00030001;
constexpr quint32 FILE_VERSION_3 = 0x00030000;
constexpr quint32 FILE_VERSION_2 = 0x00020000;
constexpr quint32 FILE_VERSION_MIN = FILE_VERSION_2;
+ constexpr quint32 FILE_VERSION_MAX = FILE_VERSION_4_1;
constexpr quint16 VARIANTMAP_VERSION = 0x0100;
constexpr quint16 VARIANTMAP_CRITICAL_MASK = 0xFF00;
- const QSysInfo::Endian BYTEORDER = QSysInfo::LittleEndian;
+ constexpr QSysInfo::Endian BYTEORDER = QSysInfo::LittleEndian;
extern const QUuid CIPHER_AES128;
extern const QUuid CIPHER_AES256;
diff --git a/src/format/KeePass2Reader.cpp b/src/format/KeePass2Reader.cpp
index 3c9cd23be..30ab70d79 100644
--- a/src/format/KeePass2Reader.cpp
+++ b/src/format/KeePass2Reader.cpp
@@ -81,11 +81,8 @@ bool KeePass2Reader::readDatabase(QIODevice* device, QSharedPointer maxVersion) {
+ if (m_version < KeePass2::FILE_VERSION_MIN
+ || (m_version & KeePass2::FILE_VERSION_CRITICAL_MASK) > KeePass2::FILE_VERSION_MAX) {
raiseError(tr("Unsupported KeePass 2 database version."));
return false;
}
diff --git a/src/format/KeePass2Writer.cpp b/src/format/KeePass2Writer.cpp
index 86480e74c..4cf0d4ad6 100644
--- a/src/format/KeePass2Writer.cpp
+++ b/src/format/KeePass2Writer.cpp
@@ -40,44 +40,57 @@ bool KeePass2Writer::writeDatabase(const QString& filename, Database* db)
return writeDatabase(&file, db);
}
+#define VERSION_MAX(a, b) \
+ a = qMax(a, b); \
+ if (a >= KeePass2::FILE_VERSION_MAX) { \
+ return a; \
+ }
+
/**
- * @return true if the database should upgrade to KDBX4.
+ * Get the minimum KDBX version required for writing the database.
*/
-bool KeePass2Writer::implicitKDBXUpgradeNeeded(Database const* db)
+quint32 KeePass2Writer::kdbxVersionRequired(Database const* db, bool ignoreCurrent, bool ignoreKdf)
{
- if (db->kdf()->uuid() != KeePass2::KDF_AES_KDBX3) {
- return false;
+ quint32 version = KeePass2::FILE_VERSION_3_1;
+ if (!ignoreCurrent) {
+ VERSION_MAX(version, db->formatVersion())
+ }
+
+ if (!ignoreKdf && !db->kdf().isNull() && !db->kdf()->uuid().isNull()
+ && db->kdf()->uuid() != KeePass2::KDF_AES_KDBX3) {
+ VERSION_MAX(version, KeePass2::FILE_VERSION_4)
}
if (!db->publicCustomData().isEmpty()) {
- return true;
+ VERSION_MAX(version, KeePass2::FILE_VERSION_4)
}
- for (const auto& group : db->rootGroup()->groupsRecursive(true)) {
+ for (const auto* group : db->rootGroup()->groupsRecursive(true)) {
if (group->customData() && !group->customData()->isEmpty()) {
- return true;
+ VERSION_MAX(version, KeePass2::FILE_VERSION_4)
}
if (!group->tags().isEmpty()) {
- return true;
+ VERSION_MAX(version, KeePass2::FILE_VERSION_4_1)
}
if (group->previousParentGroup()) {
- return true;
+ VERSION_MAX(version, KeePass2::FILE_VERSION_4_1)
}
- for (const auto& entry : group->entries()) {
+ for (const auto* entry : group->entries()) {
+
if (entry->customData() && !entry->customData()->isEmpty()) {
- return true;
+ VERSION_MAX(version, KeePass2::FILE_VERSION_4)
}
if (entry->excludeFromReports()) {
- return true;
+ VERSION_MAX(version, KeePass2::FILE_VERSION_4_1)
}
if (entry->previousParentGroup()) {
- return true;
+ VERSION_MAX(version, KeePass2::FILE_VERSION_4_1)
}
- for (const auto& historyItem : entry->historyItems()) {
+ for (const auto* historyItem : entry->historyItems()) {
if (historyItem->customData() && !historyItem->customData()->isEmpty()) {
- return true;
+ VERSION_MAX(version, KeePass2::FILE_VERSION_4)
}
}
}
@@ -87,11 +100,11 @@ bool KeePass2Writer::implicitKDBXUpgradeNeeded(Database const* db)
for (const QUuid& uuid : customIconsOrder) {
const auto& icon = db->metadata()->customIcon(uuid);
if (!icon.name.isEmpty() || icon.lastModified.isValid()) {
- return true;
+ VERSION_MAX(version, KeePass2::FILE_VERSION_4_1)
}
}
- return false;
+ return version;
}
/**
@@ -106,8 +119,8 @@ bool KeePass2Writer::writeDatabase(QIODevice* device, Database* db)
m_error = false;
m_errorStr.clear();
- bool upgradeNeeded = implicitKDBXUpgradeNeeded(db);
- if (upgradeNeeded) {
+ m_version = kdbxVersionRequired(db);
+ if (db->kdf()->uuid() == KeePass2::KDF_AES_KDBX3 && m_version >= KeePass2::FILE_VERSION_4) {
// We MUST re-transform the key, because challenge-response hashing has changed in KDBX 4.
// If we forget to re-transform, the database will be saved WITHOUT a challenge-response key component!
auto kdf = KeePass2::uuidToKdf(KeePass2::KDF_AES_KDBX4);
@@ -115,12 +128,12 @@ bool KeePass2Writer::writeDatabase(QIODevice* device, Database* db)
db->changeKdf(kdf);
}
+ db->setFormatVersion(m_version);
if (db->kdf()->uuid() == KeePass2::KDF_AES_KDBX3) {
- Q_ASSERT(!upgradeNeeded);
- m_version = KeePass2::FILE_VERSION_3_1;
+ Q_ASSERT(m_version <= KeePass2::FILE_VERSION_3_1);
m_writer.reset(new Kdbx3Writer());
} else {
- m_version = KeePass2::FILE_VERSION_4;
+ Q_ASSERT(m_version >= KeePass2::FILE_VERSION_4);
m_writer.reset(new Kdbx4Writer());
}
@@ -132,11 +145,13 @@ void KeePass2Writer::extractDatabase(Database* db, QByteArray& xmlOutput)
m_error = false;
m_errorStr.clear();
+ m_version = kdbxVersionRequired(db);
+ db->setFormatVersion(m_version);
if (db->kdf()->uuid() == KeePass2::KDF_AES_KDBX3) {
- m_version = KeePass2::FILE_VERSION_3_1;
+ Q_ASSERT(m_version <= KeePass2::FILE_VERSION_3_1);
m_writer.reset(new Kdbx3Writer());
} else {
- m_version = KeePass2::FILE_VERSION_4;
+ Q_ASSERT(m_version >= KeePass2::FILE_VERSION_4);
m_writer.reset(new Kdbx4Writer());
}
diff --git a/src/format/KeePass2Writer.h b/src/format/KeePass2Writer.h
index 049b1555c..b4d817813 100644
--- a/src/format/KeePass2Writer.h
+++ b/src/format/KeePass2Writer.h
@@ -33,7 +33,7 @@ public:
bool writeDatabase(const QString& filename, Database* db);
bool writeDatabase(QIODevice* device, Database* db);
void extractDatabase(Database* db, QByteArray& xmlOutput);
- static bool implicitKDBXUpgradeNeeded(Database const* db);
+ static quint32 kdbxVersionRequired(Database const* db, bool ignoreCurrent = false, bool ignoreKdf = false);
QSharedPointer writer() const;
quint32 version() const;
diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp
index 43e529551..f0d494b27 100644
--- a/src/gui/DatabaseOpenWidget.cpp
+++ b/src/gui/DatabaseOpenWidget.cpp
@@ -206,6 +206,26 @@ void DatabaseOpenWidget::openDatabase()
QApplication::restoreOverrideCursor();
m_ui->passwordFormFrame->setEnabled(true);
+ if (ok && m_db->hasMinorVersionMismatch()) {
+ QScopedPointer msgBox(new QMessageBox(this));
+ msgBox->setIcon(QMessageBox::Warning);
+ msgBox->setWindowTitle(tr("Database Version Mismatch"));
+ msgBox->setText(tr("The database you are trying to open was most likely\n"
+ "created by a newer version of KeePassXC.\n\n"
+ "You can try to open it anyway, but it may be incomplete\n"
+ "and saving any changes may incur data loss.\n\n"
+ "We recommend you update your KeePassXC installation."));
+ auto btn = msgBox->addButton(tr("Open database anyway"), QMessageBox::ButtonRole::AcceptRole);
+ msgBox->setDefaultButton(btn);
+ msgBox->addButton(QMessageBox::Cancel);
+ msgBox->exec();
+ if (msgBox->clickedButton() != btn) {
+ m_db.reset(new Database());
+ m_ui->messageWidget->showMessage(tr("Database unlock canceled."), MessageWidget::MessageType::Error);
+ return;
+ }
+ }
+
if (ok) {
#ifdef WITH_XC_TOUCHID
QHash useTouchID = config()->get(Config::UseTouchID).toHash();
diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp b/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp
index 1a967b773..67f894e44 100644
--- a/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp
+++ b/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp
@@ -24,6 +24,7 @@
#include "core/Metadata.h"
#include "crypto/kdf/Argon2Kdf.h"
#include "format/KeePass2.h"
+#include "format/KeePass2Writer.h"
#include "gui/MessageBox.h"
const char* DatabaseSettingsWidgetEncryption::CD_DECRYPTION_TIME_PREFERENCE_KEY = "KPXC_DECRYPTION_TIME_PREFERENCE";
@@ -36,12 +37,13 @@ DatabaseSettingsWidgetEncryption::DatabaseSettingsWidgetEncryption(QWidget* pare
connect(m_ui->transformBenchmarkButton, SIGNAL(clicked()), SLOT(benchmarkTransformRounds()));
connect(m_ui->kdfComboBox, SIGNAL(currentIndexChanged(int)), SLOT(changeKdf(int)));
+ m_ui->formatCannotBeChanged->setVisible(false);
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_ARGON2D.toByteArray());
- m_ui->compatibilitySelection->addItem(tr("KDBX 3.1"), KeePass2::KDF_AES_KDBX3.toByteArray());
+ m_ui->compatibilitySelection->addItem(tr("KDBX 4 (recommended)"), KeePass2::KDF_ARGON2D.toByteArray());
+ m_ui->compatibilitySelection->addItem(tr("KDBX 3"), KeePass2::KDF_AES_KDBX3.toByteArray());
m_ui->decryptionTimeSlider->setMinimum(Kdf::MIN_ENCRYPTION_TIME / 100);
m_ui->decryptionTimeSlider->setMaximum(Kdf::MAX_ENCRYPTION_TIME / 100);
m_ui->decryptionTimeSlider->setValue(Kdf::DEFAULT_ENCRYPTION_TIME / 100);
@@ -93,6 +95,7 @@ void DatabaseSettingsWidgetEncryption::initialize()
m_db->setCipher(KeePass2::CIPHER_AES256);
isDirty = true;
}
+ bool kdbx3Enabled = KeePass2Writer::kdbxVersionRequired(m_db.data(), true, true) <= KeePass2::FILE_VERSION_3_1;
// check if the DB's custom data has a decryption time setting stored
// and set the slider to it, otherwise just state that the time is unchanged
@@ -115,9 +118,14 @@ void DatabaseSettingsWidgetEncryption::initialize()
updateFormatCompatibility(m_db->kdf()->uuid() == KeePass2::KDF_AES_KDBX3 ? KDBX3 : KDBX4, isDirty);
setupAlgorithmComboBox();
- setupKdfComboBox();
+ setupKdfComboBox(kdbx3Enabled);
loadKdfParameters();
+ if (!kdbx3Enabled) {
+ m_ui->compatibilitySelection->setEnabled(false);
+ m_ui->formatCannotBeChanged->setVisible(true);
+ }
+
m_isDirty = isDirty;
}
@@ -143,13 +151,15 @@ void DatabaseSettingsWidgetEncryption::setupAlgorithmComboBox()
}
}
-void DatabaseSettingsWidgetEncryption::setupKdfComboBox()
+void DatabaseSettingsWidgetEncryption::setupKdfComboBox(bool enableKdbx3)
{
- // Setup kdf combo box
+ // Set up kdf combo box
bool block = m_ui->kdfComboBox->blockSignals(true);
m_ui->kdfComboBox->clear();
for (auto& kdf : asConst(KeePass2::KDFS)) {
- m_ui->kdfComboBox->addItem(kdf.second.toUtf8(), kdf.first.toByteArray());
+ if (kdf.first != KeePass2::KDF_AES_KDBX3 or enableKdbx3) {
+ m_ui->kdfComboBox->addItem(kdf.second.toUtf8(), kdf.first.toByteArray());
+ }
}
m_ui->kdfComboBox->blockSignals(block);
}
@@ -393,8 +403,8 @@ void DatabaseSettingsWidgetEncryption::updateFormatCompatibility(int index, bool
m_ui->compatibilitySelection->blockSignals(block);
}
+ QUuid kdfUuid(m_ui->compatibilitySelection->itemData(index).toByteArray());
if (retransform) {
- QUuid kdfUuid(m_ui->compatibilitySelection->itemData(index).toByteArray());
auto kdf = KeePass2::uuidToKdf(kdfUuid);
m_db->setKdf(kdf);
diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.h b/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.h
index 2c7b5bac9..c3d7ccf74 100644
--- a/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.h
+++ b/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.h
@@ -61,7 +61,7 @@ private slots:
void updateDecryptionTime(int value);
void updateFormatCompatibility(int index, bool retransform = true);
void setupAlgorithmComboBox();
- void setupKdfComboBox();
+ void setupKdfComboBox(bool enableKdbx3);
void loadKdfParameters();
void updateKdfFields();
void activateChangeDecryptionTime();
diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.ui b/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.ui
index 97da37475..2b8598862 100644
--- a/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.ui
+++ b/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.ui
@@ -183,6 +183,9 @@
-
+
+ 2
+
-
@@ -203,12 +206,24 @@
+ -
+
+
+
+ true
+
+
+
+ Format cannot be changed: Your database uses KDBX 4 features
+
+
+
-
- This is only important if you need to use your database with other programs.
+ Unless you need to open your database with other programs, always use the latest format.
diff --git a/tests/TestKdbx2.cpp b/tests/TestKdbx2.cpp
index bf22be375..c0da4d4a3 100644
--- a/tests/TestKdbx2.cpp
+++ b/tests/TestKdbx2.cpp
@@ -87,7 +87,7 @@ void TestKdbx2::testFormat200Upgrade()
reader.readDatabase(filename, key, db.data());
QVERIFY2(!reader.hasError(), reader.errorString().toStdString().c_str());
QVERIFY(!db.isNull());
- QCOMPARE(reader.version(), KeePass2::FILE_VERSION_2 & KeePass2::FILE_VERSION_CRITICAL_MASK);
+ QCOMPARE(reader.version(), KeePass2::FILE_VERSION_2);
QCOMPARE(db->kdf()->uuid(), KeePass2::KDF_AES_KDBX3);
QBuffer buffer;
@@ -110,6 +110,6 @@ void TestKdbx2::testFormat200Upgrade()
// database should now be upgraded to KDBX 3 without data loss
verifyKdbx2Db(targetDb);
- QCOMPARE(reader.version(), KeePass2::FILE_VERSION_3_1 & KeePass2::FILE_VERSION_CRITICAL_MASK);
+ QCOMPARE(reader.version(), KeePass2::FILE_VERSION_3_1);
QCOMPARE(targetDb->kdf()->uuid(), KeePass2::KDF_AES_KDBX3);
}
diff --git a/tests/TestKdbx3.cpp b/tests/TestKdbx3.cpp
index 27fa70c11..bab8ab8dc 100644
--- a/tests/TestKdbx3.cpp
+++ b/tests/TestKdbx3.cpp
@@ -75,22 +75,7 @@ void TestKdbx3::readKdbx(QIODevice* device,
if (hasError) {
errorString = reader.errorString();
}
- QCOMPARE(reader.version(), KeePass2::FILE_VERSION_3_1 & KeePass2::FILE_VERSION_CRITICAL_MASK);
-}
-
-void TestKdbx3::readKdbx(const QString& path,
- QSharedPointer key,
- QSharedPointer db,
- bool& hasError,
- QString& errorString)
-{
- KeePass2Reader reader;
- reader.readDatabase(path, key, db.data());
- hasError = reader.hasError();
- if (hasError) {
- errorString = reader.errorString();
- }
- QCOMPARE(reader.version(), KeePass2::FILE_VERSION_3_1 & KeePass2::FILE_VERSION_CRITICAL_MASK);
+ QCOMPARE(reader.version(), KeePass2::FILE_VERSION_3_1);
}
void TestKdbx3::writeKdbx(QIODevice* device, Database* db, bool& hasError, QString& errorString)
diff --git a/tests/TestKdbx3.h b/tests/TestKdbx3.h
index ca571fbc8..deb965d90 100644
--- a/tests/TestKdbx3.h
+++ b/tests/TestKdbx3.h
@@ -44,11 +44,6 @@ protected:
QSharedPointer db,
bool& hasError,
QString& errorString) override;
- void readKdbx(const QString& path,
- QSharedPointer key,
- QSharedPointer db,
- bool& hasError,
- QString& errorString) override;
void writeKdbx(QIODevice* device, Database* db, bool& hasError, QString& errorString) override;
};
diff --git a/tests/TestKdbx4.cpp b/tests/TestKdbx4.cpp
index 4521140dc..a40bd1cdf 100644
--- a/tests/TestKdbx4.cpp
+++ b/tests/TestKdbx4.cpp
@@ -92,21 +92,6 @@ void TestKdbx4Argon2::readKdbx(QIODevice* device,
QCOMPARE(reader.version(), KeePass2::FILE_VERSION_4);
}
-void TestKdbx4Argon2::readKdbx(const QString& path,
- QSharedPointer key,
- QSharedPointer db,
- bool& hasError,
- QString& errorString)
-{
- KeePass2Reader reader;
- reader.readDatabase(path, key, db.data());
- hasError = reader.hasError();
- if (hasError) {
- errorString = reader.errorString();
- }
- QCOMPARE(reader.version(), KeePass2::FILE_VERSION_4);
-}
-
void TestKdbx4Argon2::writeKdbx(QIODevice* device, Database* db, bool& hasError, QString& errorString)
{
if (db->kdf()->uuid() == KeePass2::KDF_AES_KDBX3) {
@@ -218,8 +203,8 @@ void TestKdbx4Format::testFormat400Upgrade_data()
QTest::addColumn("addCustomData");
QTest::addColumn("expectedVersion");
- 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;
+ auto constexpr kdbx3 = KeePass2::FILE_VERSION_3_1;
+ auto constexpr kdbx4 = KeePass2::FILE_VERSION_4;
QTest::newRow("Argon2d + AES") << KeePass2::KDF_ARGON2D << KeePass2::CIPHER_AES256 << false << kdbx4;
QTest::newRow("Argon2id + AES") << KeePass2::KDF_ARGON2ID << KeePass2::CIPHER_AES256 << false << kdbx4;
@@ -255,7 +240,7 @@ void TestKdbx4Format::testFormat410Upgrade()
Database db;
db.changeKdf(fastKdf(db.kdf()));
QCOMPARE(db.kdf()->uuid(), KeePass2::KDF_AES_KDBX3);
- QVERIFY(!KeePass2Writer::implicitKDBXUpgradeNeeded(&db));
+ QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_3_1);
auto group1 = new Group();
group1->setUuid(QUuid::createUuid());
@@ -271,44 +256,45 @@ void TestKdbx4Format::testFormat410Upgrade()
// Groups with tags
group1->setTags("tag");
- QVERIFY(KeePass2Writer::implicitKDBXUpgradeNeeded(&db));
+ QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_4_1);
group1->setTags("");
- QVERIFY(!KeePass2Writer::implicitKDBXUpgradeNeeded(&db));
+ QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_3_1);
// PasswordQuality flag set
entry->setExcludeFromReports(true);
- QVERIFY(KeePass2Writer::implicitKDBXUpgradeNeeded(&db));
+ QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_4_1);
entry->setExcludeFromReports(false);
- QVERIFY(!KeePass2Writer::implicitKDBXUpgradeNeeded(&db));
+ QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_3_1);
// Previous parent group set on group
group1->setPreviousParentGroup(group2);
QCOMPARE(group1->previousParentGroup(), group2);
- QVERIFY(KeePass2Writer::implicitKDBXUpgradeNeeded(&db));
+ QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_4_1);
group1->setPreviousParentGroup(nullptr);
- QVERIFY(!KeePass2Writer::implicitKDBXUpgradeNeeded(&db));
+ QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_3_1);
// Previous parent group set on entry
entry->setPreviousParentGroup(group2);
QCOMPARE(entry->previousParentGroup(), group2);
- QVERIFY(KeePass2Writer::implicitKDBXUpgradeNeeded(&db));
+ QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_4_1);
entry->setPreviousParentGroup(nullptr);
- QVERIFY(!KeePass2Writer::implicitKDBXUpgradeNeeded(&db));
+ QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_3_1);
// Custom icons with name or modification date
Metadata::CustomIconData customIcon;
auto iconUuid = QUuid::createUuid();
db.metadata()->addCustomIcon(iconUuid, customIcon);
- QVERIFY(!KeePass2Writer::implicitKDBXUpgradeNeeded(&db));
+ QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_3_1);
customIcon.name = "abc";
db.metadata()->removeCustomIcon(iconUuid);
db.metadata()->addCustomIcon(iconUuid, customIcon);
- QVERIFY(KeePass2Writer::implicitKDBXUpgradeNeeded(&db));
+ QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_4_1);
customIcon.name.clear();
customIcon.lastModified = Clock::currentDateTimeUtc();
db.metadata()->removeCustomIcon(iconUuid);
+ QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_3_1);
db.metadata()->addCustomIcon(iconUuid, customIcon);
- QVERIFY(KeePass2Writer::implicitKDBXUpgradeNeeded(&db));
+ QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_4_1);
}
void TestKdbx4Format::testUpgradeMasterKeyIntegrity()
@@ -394,8 +380,8 @@ void TestKdbx4Format::testUpgradeMasterKeyIntegrity()
if (reader.hasError()) {
QFAIL(qPrintable(reader.errorString()));
}
- QCOMPARE(reader.version(), expectedVersion & KeePass2::FILE_VERSION_CRITICAL_MASK);
- if (expectedVersion != KeePass2::FILE_VERSION_3) {
+ QCOMPARE(reader.version(), expectedVersion);
+ if (expectedVersion >= KeePass2::FILE_VERSION_4) {
QVERIFY(db2->kdf()->uuid() != KeePass2::KDF_AES_KDBX3);
}
}
@@ -405,9 +391,9 @@ void TestKdbx4Format::testUpgradeMasterKeyIntegrity_data()
QTest::addColumn("upgradeAction");
QTest::addColumn("expectedVersion");
- QTest::newRow("Upgrade: none") << QString("none") << KeePass2::FILE_VERSION_3;
- QTest::newRow("Upgrade: none (meta-customdata)") << QString("meta-customdata") << KeePass2::FILE_VERSION_3;
- QTest::newRow("Upgrade: none (explicit kdf-aes-kdbx3)") << QString("kdf-aes-kdbx3") << KeePass2::FILE_VERSION_3;
+ QTest::newRow("Upgrade: none") << QString("none") << KeePass2::FILE_VERSION_3_1;
+ QTest::newRow("Upgrade: none (meta-customdata)") << QString("meta-customdata") << KeePass2::FILE_VERSION_3_1;
+ QTest::newRow("Upgrade: none (explicit kdf-aes-kdbx3)") << QString("kdf-aes-kdbx3") << KeePass2::FILE_VERSION_3_1;
QTest::newRow("Upgrade (explicit): kdf-argon2") << QString("kdf-argon2") << KeePass2::FILE_VERSION_4;
QTest::newRow("Upgrade (explicit): kdf-aes-kdbx4") << QString("kdf-aes-kdbx4") << KeePass2::FILE_VERSION_4;
QTest::newRow("Upgrade (implicit): public-customdata") << QString("public-customdata") << KeePass2::FILE_VERSION_4;
diff --git a/tests/TestKdbx4.h b/tests/TestKdbx4.h
index 0eddef6c0..5d7f6cc50 100644
--- a/tests/TestKdbx4.h
+++ b/tests/TestKdbx4.h
@@ -32,11 +32,6 @@ protected:
readXml(const QString& path, bool strictMode, bool& hasError, QString& errorString) override;
void writeXml(QBuffer* buf, Database* db, bool& hasError, QString& errorString) override;
- void readKdbx(const QString& path,
- QSharedPointer key,
- QSharedPointer db,
- bool& hasError,
- QString& errorString) override;
void readKdbx(QIODevice* device,
QSharedPointer key,
QSharedPointer db,
diff --git a/tests/TestKeePass2Format.h b/tests/TestKeePass2Format.h
index 759172ce6..140739c03 100644
--- a/tests/TestKeePass2Format.h
+++ b/tests/TestKeePass2Format.h
@@ -76,11 +76,6 @@ protected:
QSharedPointer db,
bool& hasError,
QString& errorString) = 0;
- virtual void readKdbx(const QString& path,
- QSharedPointer key,
- QSharedPointer db,
- bool& hasError,
- QString& errorString) = 0;
virtual void writeKdbx(QIODevice* device, Database* db, bool& hasError, QString& errorString) = 0;
QSharedPointer m_xmlDb;