Skip to content

Commit

Permalink
Implement KDBX 4.1 item-level modification date
Browse files Browse the repository at this point in the history
We keep the old merging behaviour for now, since deleting a
CustomData entry does not create DeletedObject.
  • Loading branch information
phoerious committed Nov 10, 2021
1 parent bf97b73 commit 625315c
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 40 deletions.
82 changes: 61 additions & 21 deletions src/core/CustomData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ bool CustomData::hasKey(const QString& key) const
}

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

CustomData::CustomDataItem CustomData::item(const QString& key) const
{
return m_data.value(key);
}
Expand All @@ -53,20 +58,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, const QString& value, QDateTime lastModified)
{
bool addAttribute = !m_data.contains(key);
bool changeValue = !addAttribute && (m_data.value(key) != value);
bool changeValue = !addAttribute && (m_data.value(key).value != value);

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

if (!lastModified.isValid()) {
Clock::currentDateTimeUtc();
}
if (addAttribute || changeValue) {
m_data.insert(key, value);
m_data.insert(key, CustomDataItem{value, lastModified});
updateLastModified();
emitModified();
}
Expand Down Expand Up @@ -98,11 +111,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 @@ -119,21 +133,52 @@ void CustomData::copyDataFrom(const CustomData* other)
emit aboutToBeReset();

m_data = other->m_data;
auto i = m_data.begin();
while (i != m_data.end()) {
i.value().lastModified = Clock::currentDateTimeUtc();
++i;
}

updateLastModified();
emit reset();
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;
}

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

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

bool CustomData::isProtected(const QString& key) const
{
return key.startsWith(CustomData::BrowserKeyPrefix) || key.startsWith(CustomData::Created);
}
Expand Down Expand Up @@ -172,20 +217,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;
}
30 changes: 24 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,47 @@ class CustomData : public ModifiableObject
Q_OBJECT

public:
struct CustomDataItem
{
QString value;
QDateTime lastModified;

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

explicit CustomData(QObject* parent = nullptr);
QList<QString> keys() const;
bool hasKey(const QString& key) const;
QString value(const QString& key) 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, const QString& value, 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 ExcludeFromReportsLegacy; // Pre-KDBX 4.1

// Pre-KDBX 4.1
static const QString ExcludeFromReportsLegacy;

signals:
void aboutToBeAdded(const QString& key);
Expand All @@ -64,10 +82,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
7 changes: 3 additions & 4 deletions src/core/Merger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -618,8 +618,8 @@ Merger::ChangeList Merger::mergeMetadata(const MergeContext& context)
}

// Merge Custom Data if source is newer
const auto targetCustomDataModificationTime = targetMetadata->customData()->getLastModified();
const auto sourceCustomDataModificationTime = sourceMetadata->customData()->getLastModified();
const auto targetCustomDataModificationTime = targetMetadata->customData()->lastModified();
const auto sourceCustomDataModificationTime = sourceMetadata->customData()->lastModified();
if (!targetMetadata->customData()->contains(CustomData::LastModified)
|| (targetCustomDataModificationTime.isValid() && sourceCustomDataModificationTime.isValid()
&& targetCustomDataModificationTime < sourceCustomDataModificationTime)) {
Expand All @@ -629,8 +629,7 @@ Merger::ChangeList Merger::mergeMetadata(const MergeContext& context)
// Check missing keys from source. Remove those from target
for (const auto& key : targetCustomDataKeys) {
// Do not remove protected custom data
if (!sourceMetadata->customData()->contains(key)
&& !sourceMetadata->customData()->isProtectedCustomData(key)) {
if (!sourceMetadata->customData()->contains(key) && !sourceMetadata->customData()->isProtected(key)) {
auto value = targetMetadata->customData()->value(key);
targetMetadata->customData()->remove(key);
changes << tr("Removed custom data %1 [%2]").arg(key, value);
Expand Down
5 changes: 4 additions & 1 deletion src/format/KdbxXmlReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ void KdbxXmlReader::parseCustomDataItem(CustomData* customData)

QString key;
QString value;
QDateTime lastModified;
bool keySet = false;
bool valueSet = false;

Expand All @@ -431,13 +432,15 @@ void KdbxXmlReader::parseCustomDataItem(CustomData* customData)
} else if (m_xml.name() == "Value") {
value = readString();
valueSet = true;
} else if (m_xml.name() == "LastModificationTime") {
lastModified = readDateTime();
} else {
skipCurrentElement();
}
}

if (keySet && valueSet) {
customData->set(key, value);
customData->set(key, value, lastModified);
return;
}

Expand Down
15 changes: 10 additions & 5 deletions src/format/KdbxXmlWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ void KdbxXmlWriter::writeMetadata()
if (m_kdbxVersion < KeePass2::FILE_VERSION_4) {
writeBinaries();
}
writeCustomData(m_meta->customData());
writeCustomData(m_meta->customData(), true);

m_xml.writeEndElement();
}
Expand Down Expand Up @@ -228,7 +228,7 @@ void KdbxXmlWriter::writeBinaries()
m_xml.writeEndElement();
}

void KdbxXmlWriter::writeCustomData(const CustomData* customData)
void KdbxXmlWriter::writeCustomData(const CustomData* customData, bool writeItemLastModified)
{
if (customData->isEmpty()) {
return;
Expand All @@ -237,18 +237,23 @@ void KdbxXmlWriter::writeCustomData(const CustomData* customData)

const QList<QString> keyList = customData->keys();
for (const QString& key : keyList) {
writeCustomDataItem(key, customData->value(key));
writeCustomDataItem(key, customData->item(key), writeItemLastModified);
}

m_xml.writeEndElement();
}

void KdbxXmlWriter::writeCustomDataItem(const QString& key, const QString& value)
void KdbxXmlWriter::writeCustomDataItem(const QString& key,
const CustomData::CustomDataItem& item,
bool writeLastModified)
{
m_xml.writeStartElement("Item");

writeString("Key", key);
writeString("Value", value);
writeString("Value", item.value);
if (writeLastModified && m_kdbxVersion >= KeePass2::FILE_VERSION_4 && item.lastModified.isValid()) {
writeDateTime("LastModificationTime", item.lastModified);
}

m_xml.writeEndElement();
}
Expand Down
6 changes: 4 additions & 2 deletions src/format/KdbxXmlWriter.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

#include <QXmlStreamWriter>

#include "core/CustomData.h"
#include "core/Group.h"
#include "core/Metadata.h"

Expand Down Expand Up @@ -48,8 +49,9 @@ class KdbxXmlWriter
void writeCustomIcons();
void writeIcon(const QUuid& uuid, const Metadata::CustomIconData& iconData);
void writeBinaries();
void writeCustomData(const CustomData* customData);
void writeCustomDataItem(const QString& key, const QString& value);
void writeCustomData(const CustomData* customData, bool writeItemLastModified = false);
void
writeCustomDataItem(const QString& key, const CustomData::CustomDataItem& item, bool writeLastModified = false);
void writeRoot();
void writeGroup(const Group* group);
void writeTimes(const TimeInfo& ti);
Expand Down
2 changes: 1 addition & 1 deletion tests/TestKdbx4.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ void TestKdbx4Argon2::testCustomData()
db.metadata()->customData()->set(customDataKey1, customData1);
db.metadata()->customData()->set(customDataKey2, customData2);
auto lastModified = db.metadata()->customData()->value(CustomData::LastModified);
const int dataSize = customDataKey1.toUtf8().size() + customDataKey1.toUtf8().size() + customData1.toUtf8().size()
const int dataSize = customDataKey1.toUtf8().size() + customDataKey2.toUtf8().size() + customData1.toUtf8().size()
+ customData2.toUtf8().size() + lastModified.toUtf8().size()
+ CustomData::LastModified.toUtf8().size();
QCOMPARE(db.metadata()->customData()->size(), 3);
Expand Down

0 comments on commit 625315c

Please sign in to comment.