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

Fix database merge issues #10452

Merged
merged 2 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/core/CustomData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const QString CustomData::Created = QStringLiteral("_CREATED");
const QString CustomData::BrowserKeyPrefix = QStringLiteral("KPXC_BROWSER_");
const QString CustomData::BrowserLegacyKeyPrefix = QStringLiteral("Public Key: ");
const QString CustomData::ExcludeFromReportsLegacy = QStringLiteral("KnownBad");
const QString CustomData::FdoSecretsExposedGroup = QStringLiteral("FDO_SECRETS_EXPOSED_GROUP");
const QString CustomData::RandomSlug = QStringLiteral("KPXC_RANDOM_SLUG");

// Fallback item for return by reference
static const CustomData::CustomDataItem NULL_ITEM{};
Expand Down Expand Up @@ -188,7 +190,12 @@ void CustomData::updateLastModified(QDateTime lastModified)

bool CustomData::isProtected(const QString& key) const
{
return key.startsWith(CustomData::BrowserKeyPrefix) || key.startsWith(CustomData::Created);
return key.startsWith(BrowserKeyPrefix) || key == Created || key == FdoSecretsExposedGroup;
}

bool CustomData::isAutoGenerated(const QString& key) const
{
return key == LastModified || key == RandomSlug;
}

bool CustomData::operator==(const CustomData& other) const
Expand Down
3 changes: 3 additions & 0 deletions src/core/CustomData.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class CustomData : public ModifiableObject
QDateTime lastModified() const;
QDateTime lastModified(const QString& key) const;
bool isProtected(const QString& key) const;
bool isAutoGenerated(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);
Expand All @@ -68,6 +69,8 @@ class CustomData : public ModifiableObject
static const QString Created;
static const QString BrowserKeyPrefix;
static const QString BrowserLegacyKeyPrefix;
static const QString FdoSecretsExposedGroup;
static const QString RandomSlug;

// Pre-KDBX 4.1
static const QString ExcludeFromReportsLegacy;
Expand Down
2 changes: 1 addition & 1 deletion src/core/Database.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ bool Database::saveAs(const QString& filePath, SaveAction action, const QString&

// Add random data to prevent side-channel data deduplication attacks
int length = Random::instance()->randomUIntRange(64, 512);
m_metadata->customData()->set("KPXC_RANDOM_SLUG", Random::instance()->randomArray(length).toHex());
m_metadata->customData()->set(CustomData::RandomSlug, Random::instance()->randomArray(length).toHex());

// Prevent destructive operations while saving
QMutexLocker locker(&m_saveMutex);
Expand Down
14 changes: 12 additions & 2 deletions src/core/Merger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ void Merger::resetForcedMergeMode()
m_mode = Group::Default;
}

void Merger::setSkipDatabaseCustomData(bool state)
{
m_skipCustomData = state;
}

QStringList Merger::merge()
{
// Order of merge steps is important - it is possible that we
Expand Down Expand Up @@ -514,6 +519,11 @@ Merger::ChangeList Merger::mergeMetadata(const MergeContext& context)
}
}

// Some merges shouldn't modify the database custom data
if (m_skipCustomData) {
return changes;
}

// Merge Custom Data if source is newer
const auto targetCustomDataModificationTime = targetMetadata->customData()->lastModified();
const auto sourceCustomDataModificationTime = sourceMetadata->customData()->lastModified();
Expand All @@ -535,8 +545,8 @@ Merger::ChangeList Merger::mergeMetadata(const MergeContext& context)

// Transfer new/existing keys
for (const auto& key : sourceCustomDataKeys) {
// Don't merge this meta field, it is updated automatically.
if (key == CustomData::LastModified) {
// Don't merge auto-generated keys
if (sourceMetadata->customData()->isAutoGenerated(key)) {
continue;
}

Expand Down
2 changes: 2 additions & 0 deletions src/core/Merger.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class Merger : public QObject
Merger(const Group* sourceGroup, Group* targetGroup);
void setForcedMergeMode(Group::MergeMode mode);
void resetForcedMergeMode();
void setSkipDatabaseCustomData(bool state);
QStringList merge();

private:
Expand Down Expand Up @@ -66,6 +67,7 @@ class Merger : public QObject
private:
MergeContext m_context;
Group::MergeMode m_mode;
bool m_skipCustomData = false;
};

#endif // KEEPASSXC_MERGER_H
18 changes: 4 additions & 14 deletions src/fdosecrets/FdoSecretsSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,6 @@
#include "core/Database.h"
#include "core/Metadata.h"

namespace Keys
{
namespace Db
{
constexpr auto FdoSecretsExposedGroup = "FDO_SECRETS_EXPOSED_GROUP";
} // namespace Db

} // namespace Keys

namespace FdoSecrets
{

Expand Down Expand Up @@ -98,20 +89,19 @@ namespace FdoSecrets
return exposedGroup(db.data());
}

void FdoSecretsSettings::setExposedGroup(const QSharedPointer<Database>& db,
const QUuid& group) // clazy:exclude=function-args-by-value
void FdoSecretsSettings::setExposedGroup(const QSharedPointer<Database>& db, const QUuid& group)
{
setExposedGroup(db.data(), group);
}

QUuid FdoSecretsSettings::exposedGroup(Database* db) const
{
return {db->metadata()->customData()->value(Keys::Db::FdoSecretsExposedGroup)};
return {db->metadata()->customData()->value(CustomData::FdoSecretsExposedGroup)};
}

void FdoSecretsSettings::setExposedGroup(Database* db, const QUuid& group) // clazy:exclude=function-args-by-value
void FdoSecretsSettings::setExposedGroup(Database* db, const QUuid& group)
{
db->metadata()->customData()->set(Keys::Db::FdoSecretsExposedGroup, group.toString());
db->metadata()->customData()->set(CustomData::FdoSecretsExposedGroup, group.toString());
}

} // namespace FdoSecrets
1 change: 1 addition & 0 deletions src/gui/DatabaseTabWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ DatabaseWidget* DatabaseTabWidget::importFile()
if (newDb) {
// Merge the imported db into the new one
Merger merger(db.data(), newDb.data());
merger.setSkipDatabaseCustomData(true);
merger.merge();
// Show the new database
auto dbWidget = new DatabaseWidget(newDb, this);
Expand Down
3 changes: 3 additions & 0 deletions src/keeshare/ShareImport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,12 @@ ShareObserver::Result ShareImport::containerInto(const QString& resolvedPath,
auto key = QSharedPointer<CompositeKey>::create();
key->addKey(QSharedPointer<PasswordKey>::create(reference.password));
auto sourceDb = QSharedPointer<Database>::create();
sourceDb->setEmitModified(false);
if (!reader.readDatabase(&buffer, key, sourceDb.data())) {
qCritical("Error while parsing the database: %s", qPrintable(reader.errorString()));
return {reference.path, ShareObserver::Result::Error, reader.errorString()};
}
sourceDb->setEmitModified(true);

qDebug("Synchronize %s %s with %s",
qPrintable(reference.path),
Expand All @@ -92,6 +94,7 @@ ShareObserver::Result ShareImport::containerInto(const QString& resolvedPath,

Merger merger(sourceDb->rootGroup(), targetGroup);
merger.setForcedMergeMode(Group::Synchronize);
merger.setSkipDatabaseCustomData(true);
auto changelist = merger.merge();
if (!changelist.isEmpty()) {
return {reference.path, ShareObserver::Result::Success, ShareImport::tr("Successful import")};
Expand Down
40 changes: 38 additions & 2 deletions tests/TestMerge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,20 +87,56 @@ void TestMerge::testMergeNoChanges()
m_clock->advanceSecond(1);

Merger merger1(dbSource.data(), dbDestination.data());
merger1.merge();
auto changes = merger1.merge();

QVERIFY(changes.isEmpty());
QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
QCOMPARE(dbSource->rootGroup()->entriesRecursive().size(), 2);

m_clock->advanceSecond(1);

Merger merger2(dbSource.data(), dbDestination.data());
merger2.merge();
changes = merger2.merge();

QVERIFY(changes.isEmpty());
QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
QCOMPARE(dbSource->rootGroup()->entriesRecursive().size(), 2);
}

/**
* Merging without database custom data (used by imports and KeeShare)
*/
void TestMerge::testMergeCustomData()
{
QScopedPointer<Database> dbDestination(createTestDatabase());
QScopedPointer<Database> dbSource(
createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));

QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
QCOMPARE(dbSource->rootGroup()->entriesRecursive().size(), 2);

dbDestination->metadata()->customData()->set("TEST_CUSTOM_DATA", "OLD TESTING");

m_clock->advanceSecond(1);

dbSource->metadata()->customData()->set("TEST_CUSTOM_DATA", "TESTING");

// First check that the custom data is not merged when skipped
Merger merger1(dbSource.data(), dbDestination.data());
merger1.setSkipDatabaseCustomData(true);
auto changes = merger1.merge();

QVERIFY(changes.isEmpty());
QCOMPARE(dbDestination->metadata()->customData()->value("TEST_CUSTOM_DATA"), QString("OLD TESTING"));

// Second check that the custom data is merged otherwise
Merger merger2(dbSource.data(), dbDestination.data());
changes = merger2.merge();

QCOMPARE(changes.size(), 1);
QCOMPARE(dbDestination->metadata()->customData()->value("TEST_CUSTOM_DATA"), QString("TESTING"));
}

/**
* If the entry is updated in the source database, the update
* should propagate in the destination database.
Expand Down
1 change: 1 addition & 0 deletions tests/TestMerge.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ private slots:
void cleanup();
void testMergeIntoNew();
void testMergeNoChanges();
void testMergeCustomData();
void testResolveConflictNewer();
void testResolveConflictExisting();
void testResolveGroupConflictOlder();
Expand Down
Loading