Correctly set KDBX envelope version

Shows a warning when trying to open with a newer minor version than what is currently supported.

We always try to save with the lowest KDBX version possible for maximum compatibility.
This commit is contained in:
Janek Bevendorff 2021-11-20 00:32:09 +01:00
parent 67603ab42e
commit a3dc977e58
30 changed files with 209 additions and 162 deletions

Binary file not shown.

View File

@ -1535,6 +1535,28 @@ If you do not have a key file, please leave the field empty.</source>
<source>Please present or touch your YubiKey to continue</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Database Version Mismatch</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>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.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Open database anyway</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Database unlock canceled.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DatabaseSettingWidgetMetaData</name>
@ -1808,18 +1830,6 @@ Are you sure you want to continue without a password?</translation>
<source>Database format:</source>
<translation>Database format:</translation>
</message>
<message>
<source>This is only important if you need to use your database with other programs.</source>
<translation>This is only important if you need to use your database with other programs.</translation>
</message>
<message>
<source>KDBX 4.0 (recommended)</source>
<translation>KDBX 4.0 (recommended)</translation>
</message>
<message>
<source>KDBX 3.1</source>
<translation>KDBX 3.1</translation>
</message>
<message>
<source>unchanged</source>
<comment>Database decryption time is unchanged</comment>
@ -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.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Format cannot be changed: Your database uses KDBX 4 features</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unless you need to open your database with other programs, always use the latest format.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>KDBX 4 (recommended)</source>
<translation type="unfinished">KDBX 4.0 (recommended) {4 ?}</translation>
</message>
<message>
<source>KDBX 3</source>
<translation type="unfinished">KDBX 3</translation>
</message>
</context>
<context>
<name>DatabaseSettingsWidgetFdoSecrets</name>
@ -6523,10 +6549,6 @@ Available commands:
<source>AES-KDF (KDBX 4)</source>
<translation>AES-KDF (KDBX 4)</translation>
</message>
<message>
<source>AES-KDF (KDBX 3.1)</source>
<translation>AES-KDF (KDBX 3.1)</translation>
</message>
<message>
<source>Invalid Settings</source>
<comment>TOTP</comment>
@ -7498,6 +7520,10 @@ Please consider generating a new key file.</source>
<source>Attachments:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>AES-KDF (KDBX 3)</source>
<translation type="unfinished">AES-KDF (KDBX 3.1) {3)?}</translation>
</message>
</context>
<context>
<name>QtIOCompressor</name>

View File

@ -163,6 +163,27 @@ bool Database::open(const QString& filePath, QSharedPointer<const CompositeKey>
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> 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>& kdf)

View File

@ -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;

View File

@ -34,7 +34,7 @@ bool Kdbx3Reader::readDatabaseImpl(QIODevice* device,
QSharedPointer<const CompositeKey> 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);

View File

@ -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<quint16>(&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;
}

View File

@ -29,7 +29,6 @@ class Kdbx3Writer : public KdbxWriter
public:
bool writeDatabase(QIODevice* device, Database* db) override;
quint32 formatVersion() override;
};
#endif // KEEPASSX_KDBX3WRITER_H

View File

@ -36,7 +36,7 @@ bool Kdbx4Reader::readDatabaseImpl(QIODevice* device,
QSharedPointer<const CompositeKey> 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();

View File

@ -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<quint32>(&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;
}

View File

@ -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);

View File

@ -74,14 +74,12 @@ bool KdbxReader::readDatabase(QIODevice* device, QSharedPointer<const CompositeK
headerStream.open(QIODevice::ReadOnly);
// read KDBX magic numbers
quint32 sig1, sig2;
if (!readMagicNumbers(&headerStream, sig1, sig2, m_kdbxVersion)) {
quint32 sig1, sig2, version;
if (!readMagicNumbers(&headerStream, sig1, sig2, version)) {
return false;
}
m_kdbxSignature = qMakePair(sig1, sig2);
// mask out minor version
m_kdbxVersion &= KeePass2::FILE_VERSION_CRITICAL_MASK;
m_db->setFormatVersion(version);
// read header fields
while (readHeaderField(headerStream, m_db) && !hasError()) {

View File

@ -83,8 +83,6 @@ protected:
void raiseError(const QString& errorMessage);
quint32 m_kdbxVersion = 0;
QByteArray m_masterSeed;
QByteArray m_encryptionIV;
QByteArray m_streamStartBytes;

View File

@ -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);
}

View File

@ -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;

View File

@ -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<Entry*>& 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);
}

View File

@ -56,7 +56,7 @@ const QList<QPair<QUuid, QString>> 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)
{

View File

@ -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;

View File

@ -81,11 +81,8 @@ bool KeePass2Reader::readDatabase(QIODevice* device, QSharedPointer<const Compos
return false;
}
// 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;
}

View File

@ -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());
}

View File

@ -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<KdbxWriter> writer() const;
quint32 version() const;

View File

@ -206,6 +206,26 @@ void DatabaseOpenWidget::openDatabase()
QApplication::restoreOverrideCursor();
m_ui->passwordFormFrame->setEnabled(true);
if (ok && m_db->hasMinorVersionMismatch()) {
QScopedPointer<QMessageBox> 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<QString, QVariant> useTouchID = config()->get(Config::UseTouchID).toHash();

View File

@ -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);

View File

@ -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();

View File

@ -183,6 +183,9 @@
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<property name="verticalSpacing">
<number>2</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="compatibilityLabel">
<property name="text">
@ -203,12 +206,24 @@
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="formatCannotBeChanged">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Format cannot be changed: Your database uses KDBX 4 features</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>This is only important if you need to use your database with other programs.</string>
<string>Unless you need to open your database with other programs, always use the latest format.</string>
</property>
</widget>
</item>

View File

@ -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);
}

View File

@ -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<const CompositeKey> key,
QSharedPointer<Database> 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)

View File

@ -44,11 +44,6 @@ protected:
QSharedPointer<Database> db,
bool& hasError,
QString& errorString) override;
void readKdbx(const QString& path,
QSharedPointer<const CompositeKey> key,
QSharedPointer<Database> db,
bool& hasError,
QString& errorString) override;
void writeKdbx(QIODevice* device, Database* db, bool& hasError, QString& errorString) override;
};

View File

@ -92,21 +92,6 @@ void TestKdbx4Argon2::readKdbx(QIODevice* device,
QCOMPARE(reader.version(), KeePass2::FILE_VERSION_4);
}
void TestKdbx4Argon2::readKdbx(const QString& path,
QSharedPointer<const CompositeKey> key,
QSharedPointer<Database> 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<bool>("addCustomData");
QTest::addColumn<quint32>("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<QString>("upgradeAction");
QTest::addColumn<quint32>("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;

View File

@ -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<const CompositeKey> key,
QSharedPointer<Database> db,
bool& hasError,
QString& errorString) override;
void readKdbx(QIODevice* device,
QSharedPointer<const CompositeKey> key,
QSharedPointer<Database> db,

View File

@ -76,11 +76,6 @@ protected:
QSharedPointer<Database> db,
bool& hasError,
QString& errorString) = 0;
virtual void readKdbx(const QString& path,
QSharedPointer<const CompositeKey> key,
QSharedPointer<Database> db,
bool& hasError,
QString& errorString) = 0;
virtual void writeKdbx(QIODevice* device, Database* db, bool& hasError, QString& errorString) = 0;
QSharedPointer<Database> m_xmlDb;