mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-08-21 12:38:12 -04:00
Implement KDBX 4.1 CustomData modification date
We keep the old merging behaviour for now, since deleting a CustomData entry does not create DeletedObject.
This commit is contained in:
parent
390e14b2c6
commit
835e31ac3c
7 changed files with 118 additions and 43 deletions
|
@ -26,6 +26,9 @@ const QString CustomData::BrowserKeyPrefix = QStringLiteral("KPXC_BROWSER_");
|
||||||
const QString CustomData::BrowserLegacyKeyPrefix = QStringLiteral("Public Key: ");
|
const QString CustomData::BrowserLegacyKeyPrefix = QStringLiteral("Public Key: ");
|
||||||
const QString CustomData::ExcludeFromReportsLegacy = QStringLiteral("KnownBad");
|
const QString CustomData::ExcludeFromReportsLegacy = QStringLiteral("KnownBad");
|
||||||
|
|
||||||
|
// Fallback item for return by reference
|
||||||
|
static const CustomData::CustomDataItem NULL_ITEM;
|
||||||
|
|
||||||
CustomData::CustomData(QObject* parent)
|
CustomData::CustomData(QObject* parent)
|
||||||
: ModifiableObject(parent)
|
: ModifiableObject(parent)
|
||||||
{
|
{
|
||||||
|
@ -43,7 +46,17 @@ bool CustomData::hasKey(const QString& key) const
|
||||||
|
|
||||||
QString CustomData::value(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
|
bool CustomData::contains(const QString& key) const
|
||||||
|
@ -53,20 +66,28 @@ bool CustomData::contains(const QString& key) const
|
||||||
|
|
||||||
bool CustomData::containsValue(const QString& value) 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 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) {
|
if (addAttribute) {
|
||||||
emit aboutToBeAdded(key);
|
emit aboutToBeAdded(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!item.lastModified.isValid()) {
|
||||||
|
item.lastModified = Clock::currentDateTimeUtc();
|
||||||
|
}
|
||||||
if (addAttribute || changeValue) {
|
if (addAttribute || changeValue) {
|
||||||
m_data.insert(key, value);
|
m_data.insert(key, item);
|
||||||
updateLastModified();
|
updateLastModified();
|
||||||
emitModified();
|
emitModified();
|
||||||
}
|
}
|
||||||
|
@ -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)
|
void CustomData::remove(const QString& key)
|
||||||
{
|
{
|
||||||
emit aboutToBeRemoved(key);
|
emit aboutToBeRemoved(key);
|
||||||
|
@ -98,11 +124,12 @@ void CustomData::rename(const QString& oldKey, const QString& newKey)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString data = value(oldKey);
|
CustomDataItem data = m_data.value(oldKey);
|
||||||
|
|
||||||
emit aboutToRename(oldKey, newKey);
|
emit aboutToRename(oldKey, newKey);
|
||||||
|
|
||||||
m_data.remove(oldKey);
|
m_data.remove(oldKey);
|
||||||
|
data.lastModified = Clock::currentDateTimeUtc();
|
||||||
m_data.insert(newKey, data);
|
m_data.insert(newKey, data);
|
||||||
|
|
||||||
updateLastModified();
|
updateLastModified();
|
||||||
|
@ -125,15 +152,41 @@ void CustomData::copyDataFrom(const CustomData* other)
|
||||||
emitModified();
|
emitModified();
|
||||||
}
|
}
|
||||||
|
|
||||||
QDateTime CustomData::getLastModified() const
|
QDateTime CustomData::lastModified() const
|
||||||
{
|
{
|
||||||
if (m_data.contains(LastModified)) {
|
if (m_data.contains(LastModified)) {
|
||||||
return Clock::parse(m_data.value(LastModified));
|
return Clock::parse(m_data.value(LastModified).value);
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CustomData::isProtectedCustomData(const QString& key) const
|
// 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 modified;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
return key.startsWith(CustomData::BrowserKeyPrefix) || key.startsWith(CustomData::Created);
|
||||||
}
|
}
|
||||||
|
@ -172,20 +225,15 @@ int CustomData::dataSize() const
|
||||||
{
|
{
|
||||||
int size = 0;
|
int size = 0;
|
||||||
|
|
||||||
QHashIterator<QString, QString> i(m_data);
|
QHashIterator<QString, CustomDataItem> i(m_data);
|
||||||
while (i.hasNext()) {
|
while (i.hasNext()) {
|
||||||
i.next();
|
i.next();
|
||||||
size += i.key().toUtf8().size() + i.value().toUtf8().size();
|
|
||||||
|
// 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();
|
||||||
}
|
}
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CustomData::updateLastModified()
|
|
||||||
{
|
|
||||||
if (m_data.isEmpty() || (m_data.size() == 1 && m_data.contains(LastModified))) {
|
|
||||||
m_data.remove(LastModified);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_data.insert(LastModified, Clock::currentDateTimeUtc().toString());
|
|
||||||
}
|
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
#ifndef KEEPASSXC_CUSTOMDATA_H
|
#ifndef KEEPASSXC_CUSTOMDATA_H
|
||||||
#define KEEPASSXC_CUSTOMDATA_H
|
#define KEEPASSXC_CUSTOMDATA_H
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
|
||||||
|
@ -28,13 +29,30 @@ class CustomData : public ModifiableObject
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
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);
|
explicit CustomData(QObject* parent = nullptr);
|
||||||
QList<QString> keys() const;
|
QList<QString> keys() const;
|
||||||
bool hasKey(const QString& key) const;
|
bool hasKey(const QString& key) const;
|
||||||
QString value(const QString& key) const;
|
QString value(const QString& key) const;
|
||||||
|
const CustomDataItem& item(const QString& key) const;
|
||||||
bool contains(const QString& key) const;
|
bool contains(const QString& key) const;
|
||||||
bool containsValue(const QString& value) 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 remove(const QString& key);
|
||||||
void rename(const QString& oldKey, const QString& newKey);
|
void rename(const QString& oldKey, const QString& newKey);
|
||||||
void clear();
|
void clear();
|
||||||
|
@ -42,16 +60,17 @@ public:
|
||||||
int size() const;
|
int size() const;
|
||||||
int dataSize() const;
|
int dataSize() const;
|
||||||
void copyDataFrom(const CustomData* other);
|
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;
|
||||||
bool operator!=(const CustomData& other) const;
|
bool operator!=(const CustomData& other) const;
|
||||||
|
|
||||||
|
// Pre-defined keys
|
||||||
static const QString LastModified;
|
static const QString LastModified;
|
||||||
static const QString Created;
|
static const QString Created;
|
||||||
static const QString BrowserKeyPrefix;
|
static const QString BrowserKeyPrefix;
|
||||||
static const QString BrowserLegacyKeyPrefix;
|
static const QString BrowserLegacyKeyPrefix;
|
||||||
static const QString ExcludeFromReportsLegacy; // Pre-KDBX 4.1
|
|
||||||
|
// Pre-KDBX 4.1
|
||||||
|
static const QString ExcludeFromReportsLegacy;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void aboutToBeAdded(const QString& key);
|
void aboutToBeAdded(const QString& key);
|
||||||
|
@ -64,10 +83,10 @@ signals:
|
||||||
void reset();
|
void reset();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void updateLastModified();
|
void updateLastModified(QDateTime lastModified = {});
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QHash<QString, QString> m_data;
|
QHash<QString, CustomDataItem> m_data;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // KEEPASSXC_CUSTOMDATA_H
|
#endif // KEEPASSXC_CUSTOMDATA_H
|
||||||
|
|
|
@ -617,8 +617,8 @@ Merger::ChangeList Merger::mergeMetadata(const MergeContext& context)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge Custom Data if source is newer
|
// Merge Custom Data if source is newer
|
||||||
const auto targetCustomDataModificationTime = targetMetadata->customData()->getLastModified();
|
const auto targetCustomDataModificationTime = targetMetadata->customData()->lastModified();
|
||||||
const auto sourceCustomDataModificationTime = sourceMetadata->customData()->getLastModified();
|
const auto sourceCustomDataModificationTime = sourceMetadata->customData()->lastModified();
|
||||||
if (!targetMetadata->customData()->contains(CustomData::LastModified)
|
if (!targetMetadata->customData()->contains(CustomData::LastModified)
|
||||||
|| (targetCustomDataModificationTime.isValid() && sourceCustomDataModificationTime.isValid()
|
|| (targetCustomDataModificationTime.isValid() && sourceCustomDataModificationTime.isValid()
|
||||||
&& targetCustomDataModificationTime < sourceCustomDataModificationTime)) {
|
&& targetCustomDataModificationTime < sourceCustomDataModificationTime)) {
|
||||||
|
@ -628,8 +628,7 @@ Merger::ChangeList Merger::mergeMetadata(const MergeContext& context)
|
||||||
// Check missing keys from source. Remove those from target
|
// Check missing keys from source. Remove those from target
|
||||||
for (const auto& key : targetCustomDataKeys) {
|
for (const auto& key : targetCustomDataKeys) {
|
||||||
// Do not remove protected custom data
|
// Do not remove protected custom data
|
||||||
if (!sourceMetadata->customData()->contains(key)
|
if (!sourceMetadata->customData()->contains(key) && !sourceMetadata->customData()->isProtected(key)) {
|
||||||
&& !sourceMetadata->customData()->isProtectedCustomData(key)) {
|
|
||||||
auto value = targetMetadata->customData()->value(key);
|
auto value = targetMetadata->customData()->value(key);
|
||||||
targetMetadata->customData()->remove(key);
|
targetMetadata->customData()->remove(key);
|
||||||
changes << tr("Removed custom data %1 [%2]").arg(key, value);
|
changes << tr("Removed custom data %1 [%2]").arg(key, value);
|
||||||
|
|
|
@ -420,7 +420,7 @@ void KdbxXmlReader::parseCustomDataItem(CustomData* customData)
|
||||||
Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Item");
|
Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Item");
|
||||||
|
|
||||||
QString key;
|
QString key;
|
||||||
QString value;
|
CustomData::CustomDataItem item;
|
||||||
bool keySet = false;
|
bool keySet = false;
|
||||||
bool valueSet = false;
|
bool valueSet = false;
|
||||||
|
|
||||||
|
@ -429,15 +429,17 @@ void KdbxXmlReader::parseCustomDataItem(CustomData* customData)
|
||||||
key = readString();
|
key = readString();
|
||||||
keySet = true;
|
keySet = true;
|
||||||
} else if (m_xml.name() == "Value") {
|
} else if (m_xml.name() == "Value") {
|
||||||
value = readString();
|
item.value = readString();
|
||||||
valueSet = true;
|
valueSet = true;
|
||||||
|
} else if (m_xml.name() == "LastModificationTime") {
|
||||||
|
item.lastModified = readDateTime();
|
||||||
} else {
|
} else {
|
||||||
skipCurrentElement();
|
skipCurrentElement();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keySet && valueSet) {
|
if (keySet && valueSet) {
|
||||||
customData->set(key, value);
|
customData->set(key, item);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -131,7 +131,7 @@ void KdbxXmlWriter::writeMetadata()
|
||||||
if (m_kdbxVersion < KeePass2::FILE_VERSION_4) {
|
if (m_kdbxVersion < KeePass2::FILE_VERSION_4) {
|
||||||
writeBinaries();
|
writeBinaries();
|
||||||
}
|
}
|
||||||
writeCustomData(m_meta->customData());
|
writeCustomData(m_meta->customData(), true);
|
||||||
|
|
||||||
m_xml.writeEndElement();
|
m_xml.writeEndElement();
|
||||||
}
|
}
|
||||||
|
@ -220,7 +220,7 @@ void KdbxXmlWriter::writeBinaries()
|
||||||
m_xml.writeEndElement();
|
m_xml.writeEndElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
void KdbxXmlWriter::writeCustomData(const CustomData* customData)
|
void KdbxXmlWriter::writeCustomData(const CustomData* customData, bool writeItemLastModified)
|
||||||
{
|
{
|
||||||
if (customData->isEmpty()) {
|
if (customData->isEmpty()) {
|
||||||
return;
|
return;
|
||||||
|
@ -229,18 +229,23 @@ void KdbxXmlWriter::writeCustomData(const CustomData* customData)
|
||||||
|
|
||||||
const QList<QString> keyList = customData->keys();
|
const QList<QString> keyList = customData->keys();
|
||||||
for (const QString& key : keyList) {
|
for (const QString& key : keyList) {
|
||||||
writeCustomDataItem(key, customData->value(key));
|
writeCustomDataItem(key, customData->item(key), writeItemLastModified);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_xml.writeEndElement();
|
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");
|
m_xml.writeStartElement("Item");
|
||||||
|
|
||||||
writeString("Key", key);
|
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();
|
m_xml.writeEndElement();
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QXmlStreamWriter>
|
#include <QXmlStreamWriter>
|
||||||
|
|
||||||
|
#include "core/CustomData.h"
|
||||||
#include "core/Group.h"
|
#include "core/Group.h"
|
||||||
#include "core/Metadata.h"
|
#include "core/Metadata.h"
|
||||||
|
|
||||||
|
@ -49,8 +50,9 @@ private:
|
||||||
void writeCustomIcons();
|
void writeCustomIcons();
|
||||||
void writeIcon(const QUuid& uuid, const Metadata::CustomIconData& iconData);
|
void writeIcon(const QUuid& uuid, const Metadata::CustomIconData& iconData);
|
||||||
void writeBinaries();
|
void writeBinaries();
|
||||||
void writeCustomData(const CustomData* customData);
|
void writeCustomData(const CustomData* customData, bool writeItemLastModified = false);
|
||||||
void writeCustomDataItem(const QString& key, const QString& value);
|
void
|
||||||
|
writeCustomDataItem(const QString& key, const CustomData::CustomDataItem& item, bool writeLastModified = false);
|
||||||
void writeRoot();
|
void writeRoot();
|
||||||
void writeGroup(const Group* group);
|
void writeGroup(const Group* group);
|
||||||
void writeTimes(const TimeInfo& ti);
|
void writeTimes(const TimeInfo& ti);
|
||||||
|
|
|
@ -373,7 +373,7 @@ void TestKdbx4Argon2::testCustomData()
|
||||||
db.metadata()->customData()->set(customDataKey1, customData1);
|
db.metadata()->customData()->set(customDataKey1, customData1);
|
||||||
db.metadata()->customData()->set(customDataKey2, customData2);
|
db.metadata()->customData()->set(customDataKey2, customData2);
|
||||||
auto lastModified = db.metadata()->customData()->value(CustomData::LastModified);
|
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()
|
+ customData2.toUtf8().size() + lastModified.toUtf8().size()
|
||||||
+ CustomData::LastModified.toUtf8().size();
|
+ CustomData::LastModified.toUtf8().size();
|
||||||
QCOMPARE(db.metadata()->customData()->size(), 3);
|
QCOMPARE(db.metadata()->customData()->size(), 3);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue