diff --git a/share/demo.kdbx b/share/demo.kdbx index 7be77579f0..b1a37e99fc 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 daef0a8de5..b30987eee6 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 5738a3b0aa..9bdf2cd0c2 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -116,14 +116,15 @@ bool Database::open(QSharedPointer key, QString* error, bool * @param error error message in case of failure * @return true on success */ -bool Database::open(const QString& filePath, QSharedPointer key, QString* error, bool readOnly) +KeePass2Reader::Status +Database::open(const QString& filePath, QSharedPointer key, QString* error, bool readOnly) { QFile dbFile(filePath); if (!dbFile.exists()) { if (error) { *error = tr("File %1 does not exist.").arg(filePath); } - return false; + return KeePass2Reader::Status::Error; } // Don't autodetect read-only mode, as it triggers an upstream bug. @@ -137,17 +138,18 @@ bool Database::open(const QString& filePath, QSharedPointer if (error) { *error = tr("Unable to open file %1.").arg(filePath); } - return false; + return KeePass2Reader::Status::Error; } setEmitModified(false); KeePass2Reader reader; - if (!reader.readDatabase(&dbFile, std::move(key), this)) { + KeePass2Reader::Status status = reader.readDatabase(&dbFile, std::move(key), this); + if (status == KeePass2Reader::Status::Error) { if (error) { *error = tr("Error while reading the database: %1").arg(reader.errorString()); } - return false; + return status; } setReadOnly(readOnly); @@ -160,7 +162,17 @@ bool Database::open(const QString& filePath, QSharedPointer m_fileWatcher->start(canonicalFilePath(), 30, 1); setEmitModified(true); - return true; + return status; +} + +quint32 Database::formatVersion() const +{ + return m_data.formatVersion; +} + +void Database::setFormatVersion(quint32 version) +{ + m_data.formatVersion = version; } bool Database::isSaving() @@ -935,6 +947,7 @@ void Database::setKdf(QSharedPointer kdf) { Q_ASSERT(!m_data.isReadOnly); m_data.kdf = std::move(kdf); + setFormatVersion(KeePass2Writer::needsKdbxVersion(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 c42025f850..b9cbed0f8f 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -29,6 +29,7 @@ #include "core/ModifiableObject.h" #include "crypto/kdf/AesKdf.h" #include "format/KeePass2.h" +#include "format/KeePass2Reader.h" #include "keys/CompositeKey.h" #include "keys/PasswordKey.h" @@ -75,10 +76,10 @@ class Database : public ModifiableObject ~Database() override; bool open(QSharedPointer key, QString* error = nullptr, bool readOnly = false); - bool open(const QString& filePath, - QSharedPointer key, - QString* error = nullptr, - bool readOnly = false); + KeePass2Reader::Status open(const QString& filePath, + QSharedPointer key, + QString* error = nullptr, + bool readOnly = false); bool save(SaveAction action = Atomic, const QString& backupFilePath = QString(), QString* error = nullptr); bool saveAs(const QString& filePath, SaveAction action = Atomic, @@ -87,6 +88,9 @@ class Database : public ModifiableObject bool extract(QByteArray&, QString* error = nullptr); bool import(const QString& xmlExportPath, QString* error = nullptr); + quint32 formatVersion() const; + void setFormatVersion(quint32 version); + void releaseData(); bool isInitialized() const; @@ -166,6 +170,7 @@ public slots: 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 5c718d6a0d..66935b7f79 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 7ba4c3f36c..2770239a7b 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 45b0a8b510..eb98a470db 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 c25c3e31b0..1dc7067dd2 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 cbd6f95119..08a0df013e 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 8ef82f18f7..c8540245b1 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 94ccf33423..5610897c84 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 cbc13b20bd..a7b9fc37e4 100644 --- a/src/format/KdbxReader.h +++ b/src/format/KdbxReader.h @@ -83,8 +83,6 @@ class KdbxReader 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 b69cedbf78..b7758c7517 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 d5e214a512..bec8fc8df9 100644 --- a/src/format/KdbxWriter.h +++ b/src/format/KdbxWriter.h @@ -52,11 +52,6 @@ class KdbxWriter */ 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 4b49b69728..35ed5ffdb3 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 cc57ccffa5..bf991f8805 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 c42183295c..5aed903c3a 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 3c9cd23bed..fed62d87ce 100644 --- a/src/format/KeePass2Reader.cpp +++ b/src/format/KeePass2Reader.cpp @@ -31,22 +31,23 @@ * @param db Database to read into * @return true on success */ -bool KeePass2Reader::readDatabase(const QString& filename, QSharedPointer key, Database* db) +KeePass2Reader::Status +KeePass2Reader::readDatabase(const QString& filename, QSharedPointer key, Database* db) { QFile file(filename); if (!file.open(QFile::ReadOnly)) { raiseError(file.errorString()); - return false; + return Error; } - bool ok = readDatabase(&file, std::move(key), db); + Status status = readDatabase(&file, std::move(key), db); if (file.error() != QFile::NoError) { raiseError(file.errorString()); - return false; + return Error; } - return ok; + return status; } /** @@ -57,7 +58,8 @@ bool KeePass2Reader::readDatabase(const QString& filename, QSharedPointer key, Database* db) +KeePass2Reader::Status +KeePass2Reader::readDatabase(QIODevice* device, QSharedPointer key, Database* db) { m_error = false; m_errorStr.clear(); @@ -67,7 +69,7 @@ bool KeePass2Reader::readDatabase(QIODevice* device, QSharedPointer 'Import KeePass 1 database…'.\n" "This is a one-way migration. You won't be able to open the imported " "database with the old KeePassX 0.4 version.")); - return false; + return Error; } else if (!(signature1 == KeePass2::SIGNATURE_1 && signature2 == KeePass2::SIGNATURE_2)) { raiseError(tr("Not a KeePass database.")); - return false; + return Error; } - // mask out minor version - m_version &= KeePass2::FILE_VERSION_CRITICAL_MASK; - - quint32 maxVersion = KeePass2::FILE_VERSION_4 & KeePass2::FILE_VERSION_CRITICAL_MASK; - if (m_version < KeePass2::FILE_VERSION_MIN || m_version > 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; + return Error; } // determine file format (KDBX 2/3 or 4) @@ -97,7 +96,10 @@ bool KeePass2Reader::readDatabase(QIODevice* device, QSharedPointerreadDatabase(device, std::move(key), db); + if (!m_reader->readDatabase(device, std::move(key), db)) { + return Error; + } + return hasMinorVersionMismatch() ? VersionWarn : Ok; } bool KeePass2Reader::hasError() const @@ -110,6 +112,14 @@ QString KeePass2Reader::errorString() const return !m_reader.isNull() ? m_reader->errorString() : m_errorStr; } +/** + * @return whether the KDBX minor version is greater than the newest supported. + */ +bool KeePass2Reader::hasMinorVersionMismatch() const +{ + return m_version > KeePass2::FILE_VERSION_MAX; +} + /** * @return detected KDBX version */ diff --git a/src/format/KeePass2Reader.h b/src/format/KeePass2Reader.h index a3f5c38b8d..6372d5ea4c 100644 --- a/src/format/KeePass2Reader.h +++ b/src/format/KeePass2Reader.h @@ -27,10 +27,18 @@ class KeePass2Reader Q_DECLARE_TR_FUNCTIONS(KdbxReader) public: - bool readDatabase(const QString& filename, QSharedPointer key, Database* db); - bool readDatabase(QIODevice* device, QSharedPointer key, Database* db); + enum Status + { + Error = 0, + Ok = 1, + VersionWarn = 2 + }; + + Status readDatabase(const QString& filename, QSharedPointer key, Database* db); + Status readDatabase(QIODevice* device, QSharedPointer key, Database* db); bool hasError() const; + bool hasMinorVersionMismatch() const; QString errorString() const; QSharedPointer reader() const; diff --git a/src/format/KeePass2Writer.cpp b/src/format/KeePass2Writer.cpp index 86480e74cc..5ff810a9e2 100644 --- a/src/format/KeePass2Writer.cpp +++ b/src/format/KeePass2Writer.cpp @@ -40,44 +40,56 @@ 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::needsKdbxVersion(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)) { 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()) { 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()) { if (historyItem->customData() && !historyItem->customData()->isEmpty()) { - return true; + VERSION_MAX(version, KeePass2::FILE_VERSION_4) } } } @@ -87,11 +99,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 +118,8 @@ bool KeePass2Writer::writeDatabase(QIODevice* device, Database* db) m_error = false; m_errorStr.clear(); - bool upgradeNeeded = implicitKDBXUpgradeNeeded(db); - if (upgradeNeeded) { + m_version = needsKdbxVersion(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 +127,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 +144,13 @@ void KeePass2Writer::extractDatabase(Database* db, QByteArray& xmlOutput) m_error = false; m_errorStr.clear(); + m_version = needsKdbxVersion(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 049b1555cd..88f66336c2 100644 --- a/src/format/KeePass2Writer.h +++ b/src/format/KeePass2Writer.h @@ -33,7 +33,7 @@ class KeePass2Writer 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 needsKdbxVersion(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 43e529551e..e708f31e75 100644 --- a/src/gui/DatabaseOpenWidget.cpp +++ b/src/gui/DatabaseOpenWidget.cpp @@ -202,11 +202,31 @@ void DatabaseOpenWidget::openDatabase() QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); m_ui->passwordFormFrame->setEnabled(false); QCoreApplication::processEvents(); - bool ok = m_db->open(m_filename, databaseKey, &error, false); + KeePass2Reader::Status status = m_db->open(m_filename, databaseKey, &error, false); QApplication::restoreOverrideCursor(); m_ui->passwordFormFrame->setEnabled(true); - if (ok) { + if (status == KeePass2Reader::Status::VersionWarn) { + 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 (status != KeePass2Reader::Status::Error) { #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 1a967b7739..03d30798cf 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::needsKdbxVersion(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 2c7b5bac9f..c3d7ccf749 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 97da37475e..2b8598862e 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 bf22be375a..c0da4d4a39 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 27fa70c11b..bab8ab8dc5 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 ca571fbc88..deb965d903 100644 --- a/tests/TestKdbx3.h +++ b/tests/TestKdbx3.h @@ -44,11 +44,6 @@ private slots: 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 4521140dc4..9bd210ca50 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::needsKdbxVersion(&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::needsKdbxVersion(&db), KeePass2::FILE_VERSION_4_1); group1->setTags(""); - QVERIFY(!KeePass2Writer::implicitKDBXUpgradeNeeded(&db)); + QCOMPARE(KeePass2Writer::needsKdbxVersion(&db), KeePass2::FILE_VERSION_3_1); // PasswordQuality flag set entry->setExcludeFromReports(true); - QVERIFY(KeePass2Writer::implicitKDBXUpgradeNeeded(&db)); + QCOMPARE(KeePass2Writer::needsKdbxVersion(&db), KeePass2::FILE_VERSION_4_1); entry->setExcludeFromReports(false); - QVERIFY(!KeePass2Writer::implicitKDBXUpgradeNeeded(&db)); + QCOMPARE(KeePass2Writer::needsKdbxVersion(&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::needsKdbxVersion(&db), KeePass2::FILE_VERSION_4_1); group1->setPreviousParentGroup(nullptr); - QVERIFY(!KeePass2Writer::implicitKDBXUpgradeNeeded(&db)); + QCOMPARE(KeePass2Writer::needsKdbxVersion(&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::needsKdbxVersion(&db), KeePass2::FILE_VERSION_4_1); entry->setPreviousParentGroup(nullptr); - QVERIFY(!KeePass2Writer::implicitKDBXUpgradeNeeded(&db)); + QCOMPARE(KeePass2Writer::needsKdbxVersion(&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::needsKdbxVersion(&db), KeePass2::FILE_VERSION_3_1); customIcon.name = "abc"; db.metadata()->removeCustomIcon(iconUuid); db.metadata()->addCustomIcon(iconUuid, customIcon); - QVERIFY(KeePass2Writer::implicitKDBXUpgradeNeeded(&db)); + QCOMPARE(KeePass2Writer::needsKdbxVersion(&db), KeePass2::FILE_VERSION_4_1); customIcon.name.clear(); customIcon.lastModified = Clock::currentDateTimeUtc(); db.metadata()->removeCustomIcon(iconUuid); + QCOMPARE(KeePass2Writer::needsKdbxVersion(&db), KeePass2::FILE_VERSION_3_1); db.metadata()->addCustomIcon(iconUuid, customIcon); - QVERIFY(KeePass2Writer::implicitKDBXUpgradeNeeded(&db)); + QCOMPARE(KeePass2Writer::needsKdbxVersion(&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 0eddef6c0d..5d7f6cc500 100644 --- a/tests/TestKdbx4.h +++ b/tests/TestKdbx4.h @@ -32,11 +32,6 @@ class TestKdbx4Argon2 : public TestKeePass2Format 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 759172ce61..140739c031 100644 --- a/tests/TestKeePass2Format.h +++ b/tests/TestKeePass2Format.h @@ -76,11 +76,6 @@ private slots: 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;