Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement KDBX 4.1 #7114

Merged
merged 10 commits into from
Nov 22, 2021
Binary file modified share/demo.kdbx
Binary file not shown.
1 change: 1 addition & 0 deletions share/icons/application/scalable/actions/entry-restore.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions share/icons/icons.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<file>application/scalable/actions/edit-clear-locationbar-rtl.svg</file>
<file>application/scalable/actions/entry-clone.svg</file>
<file>application/scalable/actions/entry-delete.svg</file>
<file>application/scalable/actions/entry-restore.svg</file>
<file>application/scalable/actions/entry-edit.svg</file>
<file>application/scalable/actions/entry-new.svg</file>
<file>application/scalable/actions/favicon-download.svg</file>
Expand Down
62 changes: 46 additions & 16 deletions share/translations/keepassxc_en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -5227,6 +5253,10 @@ We recommend you use the AppImage available on our downloads page.</source>
<source>Please present or touch your YubiKey to continue…</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Restore Entry(s)</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ManageDatabase</name>
Expand Down Expand Up @@ -6519,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>
Expand Down Expand Up @@ -7494,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>
Expand Down
94 changes: 71 additions & 23 deletions src/core/CustomData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ const QString CustomData::LastModified = QStringLiteral("_LAST_MODIFIED");
const QString CustomData::Created = QStringLiteral("_CREATED");
const QString CustomData::BrowserKeyPrefix = QStringLiteral("KPXC_BROWSER_");
const QString CustomData::BrowserLegacyKeyPrefix = QStringLiteral("Public Key: ");
const QString CustomData::ExcludeFromReports = QStringLiteral("KnownBad");
const QString CustomData::ExcludeFromReportsLegacy = QStringLiteral("KnownBad");

// Fallback item for return by reference
static const CustomData::CustomDataItem NULL_ITEM;

CustomData::CustomData(QObject* parent)
: ModifiableObject(parent)
Expand All @@ -43,7 +46,17 @@ bool CustomData::hasKey(const QString& key) const

QString CustomData::value(const QString& key) const
{
return m_data.value(key);
return m_data.value(key).value;
}

const CustomData::CustomDataItem& CustomData::item(const QString& key) const
{
auto item = m_data.find(key);
Q_ASSERT(item != m_data.end());
if (item == m_data.end()) {
return NULL_ITEM;
}
return item.value();
}

bool CustomData::contains(const QString& key) const
Expand All @@ -53,20 +66,28 @@ bool CustomData::contains(const QString& key) const

bool CustomData::containsValue(const QString& value) const
{
return asConst(m_data).values().contains(value);
for (auto i = m_data.constBegin(); i != m_data.constEnd(); ++i) {
if (i.value().value == value) {
return true;
}
}
return false;
}

void CustomData::set(const QString& key, const QString& value)
void CustomData::set(const QString& key, CustomDataItem item)
{
bool addAttribute = !m_data.contains(key);
bool changeValue = !addAttribute && (m_data.value(key) != value);
bool changeValue = !addAttribute && (m_data.value(key).value != item.value);

if (addAttribute) {
emit aboutToBeAdded(key);
}

if (!item.lastModified.isValid()) {
item.lastModified = Clock::currentDateTimeUtc();
droidmonkey marked this conversation as resolved.
Show resolved Hide resolved
}
if (addAttribute || changeValue) {
m_data.insert(key, value);
m_data.insert(key, item);
updateLastModified();
emitModified();
}
Expand All @@ -76,6 +97,11 @@ void CustomData::set(const QString& key, const QString& value)
}
}

void CustomData::set(const QString& key, const QString& value, const QDateTime& lastModified)
{
set(key, {value, lastModified});
}

void CustomData::remove(const QString& key)
{
emit aboutToBeRemoved(key);
Expand All @@ -98,11 +124,12 @@ void CustomData::rename(const QString& oldKey, const QString& newKey)
return;
}

QString data = value(oldKey);
CustomDataItem data = m_data.value(oldKey);

emit aboutToRename(oldKey, newKey);

m_data.remove(oldKey);
data.lastModified = Clock::currentDateTimeUtc();
m_data.insert(newKey, data);

updateLastModified();
Expand All @@ -125,15 +152,41 @@ void CustomData::copyDataFrom(const CustomData* other)
emitModified();
}

QDateTime CustomData::getLastModified() const
QDateTime CustomData::lastModified() const
{
if (m_data.contains(LastModified)) {
return Clock::parse(m_data.value(LastModified));
return Clock::parse(m_data.value(LastModified).value);
}

// Try to find the latest modification time in items as a fallback
QDateTime modified;
for (auto i = m_data.constBegin(); i != m_data.constEnd(); ++i) {
if (i->lastModified.isValid() && (!modified.isValid() || i->lastModified > modified)) {
modified = i->lastModified;
}
}
return {};
return modified;
}

QDateTime CustomData::lastModified(const QString& key) const
{
return m_data.value(key).lastModified;
}

bool CustomData::isProtectedCustomData(const QString& key) const
void CustomData::updateLastModified(QDateTime lastModified)
{
if (m_data.isEmpty() || (m_data.size() == 1 && m_data.contains(LastModified))) {
m_data.remove(LastModified);
return;
}

if (!lastModified.isValid()) {
lastModified = Clock::currentDateTimeUtc();
}
m_data.insert(LastModified, {lastModified.toString(), QDateTime()});
}
droidmonkey marked this conversation as resolved.
Show resolved Hide resolved

bool CustomData::isProtected(const QString& key) const
{
return key.startsWith(CustomData::BrowserKeyPrefix) || key.startsWith(CustomData::Created);
}
Expand Down Expand Up @@ -172,20 +225,15 @@ int CustomData::dataSize() const
{
int size = 0;

QHashIterator<QString, QString> i(m_data);
QHashIterator<QString, CustomDataItem> i(m_data);
while (i.hasNext()) {
i.next();
size += i.key().toUtf8().size() + i.value().toUtf8().size();
}
return size;
}

void CustomData::updateLastModified()
{
if (m_data.isEmpty() || (m_data.size() == 1 && m_data.contains(LastModified))) {
m_data.remove(LastModified);
return;
// In theory, we should be adding the datetime string size as well, but it makes
// length calculations rather unpredictable. We also don't know if this instance
// is entry/group-level CustomData or global CustomData (the only CustomData that
// actually retains the datetime in the KDBX file).
size += i.key().toUtf8().size() + i.value().value.toUtf8().size();
}

m_data.insert(LastModified, Clock::currentDateTimeUtc().toString());
return size;
}
31 changes: 25 additions & 6 deletions src/core/CustomData.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#ifndef KEEPASSXC_CUSTOMDATA_H
#define KEEPASSXC_CUSTOMDATA_H

#include <QDateTime>
#include <QHash>
#include <QObject>

Expand All @@ -28,30 +29,48 @@ class CustomData : public ModifiableObject
Q_OBJECT

public:
struct CustomDataItem
{
QString value;
QDateTime lastModified;

bool inline operator==(const CustomDataItem& rhs) const
{
// Compare only actual values, not modification dates
return value == rhs.value;
}
};

explicit CustomData(QObject* parent = nullptr);
QList<QString> keys() const;
bool hasKey(const QString& key) const;
QString value(const QString& key) const;
const CustomDataItem& item(const QString& key) const;
bool contains(const QString& key) const;
bool containsValue(const QString& value) const;
void set(const QString& key, const QString& value);
QDateTime lastModified() const;
QDateTime lastModified(const QString& key) const;
bool isProtected(const QString& key) const;
void set(const QString& key, CustomDataItem item);
void set(const QString& key, const QString& value, const QDateTime& lastModified = {});
void remove(const QString& key);
void rename(const QString& oldKey, const QString& newKey);
void clear();
bool isEmpty() const;
int size() const;
int dataSize() const;
void copyDataFrom(const CustomData* other);
QDateTime getLastModified() const;
bool isProtectedCustomData(const QString& key) const;
bool operator==(const CustomData& other) const;
bool operator!=(const CustomData& other) const;

// Pre-defined keys
static const QString LastModified;
static const QString Created;
static const QString BrowserKeyPrefix;
static const QString BrowserLegacyKeyPrefix;
static const QString ExcludeFromReports;

// Pre-KDBX 4.1
static const QString ExcludeFromReportsLegacy;

signals:
void aboutToBeAdded(const QString& key);
Expand All @@ -64,10 +83,10 @@ class CustomData : public ModifiableObject
void reset();

private slots:
void updateLastModified();
void updateLastModified(QDateTime lastModified = {});

private:
QHash<QString, QString> m_data;
QHash<QString, CustomDataItem> m_data;
};

#endif // KEEPASSXC_CUSTOMDATA_H
22 changes: 22 additions & 0 deletions src/core/Database.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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)
Expand Down
Loading