2
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
4
* This program is free software: you can redistribute it and/or modify
5
* it under the terms of the GNU General Public License as published by
6
* the Free Software Foundation, either version 2 or (at your option)
7
* version 3 of the License.
9
* This program is distributed in the hope that it will be useful,
10
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
* GNU General Public License for more details.
14
* You should have received a copy of the GNU General Public License
15
* along with this program. If not, see <http://www.gnu.org/licenses/>.
20
#include "config-keepassx-tests.h"
21
#include "core/Metadata.h"
22
#include "format/KdbxXmlReader.h"
23
#include "format/KdbxXmlWriter.h"
24
#include "format/KeePass2.h"
25
#include "format/KeePass2Reader.h"
26
#include "format/KeePass2Writer.h"
27
#ifdef WITH_XC_KEESHARE
28
#include "keeshare/KeeShare.h"
29
#include "keeshare/KeeShareSettings.h"
31
#include "keys/FileKey.h"
32
#include "keys/PasswordKey.h"
33
#include "mock/MockChallengeResponseKey.h"
34
#include "mock/MockClock.h"
37
int main(int argc, char* argv[])
39
QCoreApplication app(argc, argv);
40
QCoreApplication::setAttribute(Qt::AA_Use96Dpi, true);
41
QTEST_SET_MAIN_SOURCE_PATH
43
TestKdbx4Argon2 argon2Test;
44
TestKdbx4AesKdf aesKdfTest;
45
TestKdbx4Format kdbx4Test;
46
return QTest::qExec(&argon2Test, argc, argv) || QTest::qExec(&aesKdfTest, argc, argv)
47
|| QTest::qExec(&kdbx4Test, argc, argv);
50
void TestKdbx4Argon2::initTestCaseImpl()
52
m_xmlDb->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2D)));
53
m_kdbxSourceDb->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2D)));
56
QSharedPointer<Database>
57
TestKdbx4Argon2::readXml(const QString& path, bool strictMode, bool& hasError, QString& errorString)
59
KdbxXmlReader reader(KeePass2::FILE_VERSION_4);
60
reader.setStrictMode(strictMode);
61
auto db = reader.readDatabase(path);
62
hasError = reader.hasError();
63
errorString = reader.errorString();
67
QSharedPointer<Database> TestKdbx4Argon2::readXml(QBuffer* buf, bool strictMode, bool& hasError, QString& errorString)
69
KdbxXmlReader reader(KeePass2::FILE_VERSION_4);
70
reader.setStrictMode(strictMode);
71
auto db = reader.readDatabase(buf);
72
hasError = reader.hasError();
73
errorString = reader.errorString();
77
void TestKdbx4Argon2::writeXml(QBuffer* buf, Database* db, bool& hasError, QString& errorString)
79
KdbxXmlWriter writer(KeePass2::FILE_VERSION_4, {});
80
writer.writeDatabase(buf, db);
81
hasError = writer.hasError();
82
errorString = writer.errorString();
85
void TestKdbx4Argon2::readKdbx(QIODevice* device,
86
QSharedPointer<const CompositeKey> key,
87
QSharedPointer<Database> db,
91
KeePass2Reader reader;
92
reader.readDatabase(device, key, db.data());
93
hasError = reader.hasError();
95
errorString = reader.errorString();
97
QCOMPARE(reader.version(), KeePass2::FILE_VERSION_4);
100
void TestKdbx4Argon2::writeKdbx(QIODevice* device, Database* db, bool& hasError, QString& errorString)
102
if (db->kdf()->uuid() == KeePass2::KDF_AES_KDBX3) {
103
db->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2D)));
105
KeePass2Writer writer;
106
hasError = writer.writeDatabase(device, db);
107
hasError = writer.hasError();
109
errorString = writer.errorString();
111
QCOMPARE(writer.version(), KeePass2::FILE_VERSION_4);
114
void TestKdbx4AesKdf::initTestCaseImpl()
116
m_xmlDb->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_AES_KDBX4)));
117
m_kdbxSourceDb->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_AES_KDBX4)));
120
Q_DECLARE_METATYPE(QUuid)
122
void TestKdbx4Format::init()
124
MockClock::setup(new MockClock());
127
void TestKdbx4Format::cleanup()
129
MockClock::teardown();
132
void TestKdbx4Format::testFormat400()
134
QString filename = QString(KEEPASSX_TEST_DATA_DIR).append("/Format400.kdbx");
135
auto key = QSharedPointer<CompositeKey>::create();
136
key->addKey(QSharedPointer<PasswordKey>::create("t"));
137
KeePass2Reader reader;
138
auto db = QSharedPointer<Database>::create();
139
reader.readDatabase(filename, key, db.data());
140
QCOMPARE(reader.version(), KeePass2::FILE_VERSION_4);
142
QVERIFY(!reader.hasError());
144
QCOMPARE(db->rootGroup()->name(), QString("Format400"));
145
QCOMPARE(db->metadata()->name(), QString("Format400"));
146
QCOMPARE(db->rootGroup()->entries().size(), 1);
147
auto entry = db->rootGroup()->entries().at(0);
149
QCOMPARE(entry->title(), QString("Format400"));
150
QCOMPARE(entry->username(), QString("Format400"));
151
QCOMPARE(entry->attributes()->keys().size(), 6);
152
QCOMPARE(entry->attributes()->value("Format400"), QString("Format400"));
153
QCOMPARE(entry->attachments()->keys().size(), 1);
154
QCOMPARE(entry->attachments()->value("Format400"), QByteArray("Format400\n"));
157
void TestKdbx4Format::testFormat400Upgrade()
159
QFETCH(QUuid, kdfUuid);
160
QFETCH(QUuid, cipherUuid);
161
QFETCH(bool, addCustomData);
162
QFETCH(quint32, expectedVersion);
164
QScopedPointer<Database> sourceDb(new Database());
165
sourceDb->changeKdf(fastKdf(sourceDb->kdf()));
166
sourceDb->metadata()->setName("Wubba lubba dub dub");
167
QCOMPARE(sourceDb->kdf()->uuid(), KeePass2::KDF_AES_KDBX3); // default is legacy AES-KDF
169
auto key = QSharedPointer<CompositeKey>::create();
170
key->addKey(QSharedPointer<PasswordKey>::create("I am in great pain, please help me!"));
171
sourceDb->setKey(key, true, true);
174
buffer.open(QBuffer::ReadWrite);
176
// upgrade to KDBX 4 by changing KDF and Cipher
177
sourceDb->changeKdf(fastKdf(KeePass2::uuidToKdf(kdfUuid)));
178
sourceDb->setCipher(cipherUuid);
180
// CustomData in meta should not cause any version change
181
sourceDb->metadata()->customData()->set("CustomPublicData", "Hey look, I turned myself into a pickle!");
183
// this, however, should
184
sourceDb->rootGroup()->customData()->set("CustomGroupData",
185
"I just killed my family! I don't care who they were!");
188
KeePass2Writer writer;
189
writer.writeDatabase(&buffer, sourceDb.data());
190
if (writer.hasError()) {
191
QFAIL(qPrintable(QString("Error while writing database: %1").arg(writer.errorString())));
196
KeePass2Reader reader;
197
auto targetDb = QSharedPointer<Database>::create();
198
reader.readDatabase(&buffer, key, targetDb.data());
199
if (reader.hasError()) {
200
QFAIL(qPrintable(QString("Error while reading database: %1").arg(reader.errorString())));
203
QVERIFY(targetDb->rootGroup());
204
QCOMPARE(targetDb->metadata()->name(), sourceDb->metadata()->name());
206
QCOMPARE(reader.version(), expectedVersion);
207
QCOMPARE(targetDb->cipher(), cipherUuid);
208
QCOMPARE(targetDb->metadata()->customData()->value("CustomPublicData"),
209
sourceDb->metadata()->customData()->value("CustomPublicData"));
210
QCOMPARE(targetDb->rootGroup()->customData()->value("CustomGroupData"),
211
sourceDb->rootGroup()->customData()->value("CustomGroupData"));
215
void TestKdbx4Format::testFormat400Upgrade_data()
217
QTest::addColumn<QUuid>("kdfUuid");
218
QTest::addColumn<QUuid>("cipherUuid");
219
QTest::addColumn<bool>("addCustomData");
220
QTest::addColumn<quint32>("expectedVersion");
222
auto constexpr kdbx3 = KeePass2::FILE_VERSION_3_1;
223
auto constexpr kdbx4 = KeePass2::FILE_VERSION_4;
225
QTest::newRow("Argon2d + AES") << KeePass2::KDF_ARGON2D << KeePass2::CIPHER_AES256 << false << kdbx4;
226
QTest::newRow("Argon2id + AES") << KeePass2::KDF_ARGON2ID << KeePass2::CIPHER_AES256 << false << kdbx4;
227
QTest::newRow("AES-KDF + AES") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_AES256 << false << kdbx4;
228
QTest::newRow("AES-KDF (legacy) + AES") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_AES256 << false << kdbx3;
229
QTest::newRow("Argon2d + AES + CustomData") << KeePass2::KDF_ARGON2D << KeePass2::CIPHER_AES256 << true << kdbx4;
230
QTest::newRow("Argon2id + AES + CustomData") << KeePass2::KDF_ARGON2ID << KeePass2::CIPHER_AES256 << true << kdbx4;
231
QTest::newRow("AES-KDF + AES + CustomData") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_AES256 << true << kdbx4;
232
QTest::newRow("AES-KDF (legacy) + AES + CustomData") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_AES256 << true << kdbx4;
234
QTest::newRow("Argon2d + ChaCha20") << KeePass2::KDF_ARGON2D << KeePass2::CIPHER_CHACHA20 << false << kdbx4;
235
QTest::newRow("Argon2id + ChaCha20") << KeePass2::KDF_ARGON2ID << KeePass2::CIPHER_CHACHA20 << false << kdbx4;
236
QTest::newRow("AES-KDF + ChaCha20") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_CHACHA20 << false << kdbx4;
237
QTest::newRow("AES-KDF (legacy) + ChaCha20") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_CHACHA20 << false << kdbx3;
238
QTest::newRow("Argon2d + ChaCha20 + CustomData") << KeePass2::KDF_ARGON2D << KeePass2::CIPHER_CHACHA20 << true << kdbx4;
239
QTest::newRow("Argon2id + ChaCha20 + CustomData") << KeePass2::KDF_ARGON2ID << KeePass2::CIPHER_CHACHA20 << true << kdbx4;
240
QTest::newRow("AES-KDF + ChaCha20 + CustomData") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_CHACHA20 << true << kdbx4;
241
QTest::newRow("AES-KDF (legacy) + ChaCha20 + CustomData") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_CHACHA20 << true << kdbx4;
243
QTest::newRow("Argon2d + Twofish") << KeePass2::KDF_ARGON2D << KeePass2::CIPHER_TWOFISH << false << kdbx4;
244
QTest::newRow("Argon2id + Twofish") << KeePass2::KDF_ARGON2ID << KeePass2::CIPHER_TWOFISH << false << kdbx4;
245
QTest::newRow("AES-KDF + Twofish") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_TWOFISH << false << kdbx4;
246
QTest::newRow("AES-KDF (legacy) + Twofish") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_TWOFISH << false << kdbx3;
247
QTest::newRow("Argon2d + Twofish + CustomData") << KeePass2::KDF_ARGON2D << KeePass2::CIPHER_TWOFISH << true << kdbx4;
248
QTest::newRow("Argon2id + Twofish + CustomData") << KeePass2::KDF_ARGON2ID << KeePass2::CIPHER_TWOFISH << true << kdbx4;
249
QTest::newRow("AES-KDF + Twofish + CustomData") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_TWOFISH << true << kdbx4;
250
QTest::newRow("AES-KDF (legacy) + Twofish + CustomData") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_TWOFISH << true << kdbx4;
254
void TestKdbx4Format::testFormat410Upgrade()
257
db.changeKdf(fastKdf(db.kdf()));
258
QCOMPARE(db.kdf()->uuid(), KeePass2::KDF_AES_KDBX3);
259
QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_3_1);
261
auto group1 = new Group();
262
group1->setUuid(QUuid::createUuid());
263
group1->setParent(db.rootGroup());
265
auto group2 = new Group();
266
group2->setUuid(QUuid::createUuid());
267
group2->setParent(db.rootGroup());
269
auto entry = new Entry();
270
entry->setUuid(QUuid::createUuid());
271
entry->setGroup(group1);
274
group1->setTags("tag");
275
QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_4_1);
277
QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_3_1);
279
// PasswordQuality flag set
280
entry->setExcludeFromReports(true);
281
QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_4_1);
282
entry->setExcludeFromReports(false);
283
QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_3_1);
285
// Previous parent group set on group
286
group1->setPreviousParentGroup(group2);
287
QCOMPARE(group1->previousParentGroup(), group2);
288
QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_4_1);
289
group1->setPreviousParentGroup(nullptr);
290
QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_3_1);
292
// Previous parent group set on entry
293
entry->setPreviousParentGroup(group2);
294
QCOMPARE(entry->previousParentGroup(), group2);
295
QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_4_1);
296
entry->setPreviousParentGroup(nullptr);
297
QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_3_1);
299
// Custom icons with name or modification date
300
Metadata::CustomIconData customIcon;
301
auto iconUuid = QUuid::createUuid();
302
db.metadata()->addCustomIcon(iconUuid, customIcon);
303
QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_3_1);
304
customIcon.name = "abc";
305
db.metadata()->removeCustomIcon(iconUuid);
306
db.metadata()->addCustomIcon(iconUuid, customIcon);
307
QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_4_1);
308
customIcon.name.clear();
309
customIcon.lastModified = Clock::currentDateTimeUtc();
310
db.metadata()->removeCustomIcon(iconUuid);
311
QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_3_1);
312
db.metadata()->addCustomIcon(iconUuid, customIcon);
313
QCOMPARE(KeePass2Writer::kdbxVersionRequired(&db), KeePass2::FILE_VERSION_4_1);
316
void TestKdbx4Format::testUpgradeMasterKeyIntegrity()
318
QFETCH(QString, upgradeAction);
319
QFETCH(quint32, expectedVersion);
321
// prepare composite key
322
auto passwordKey = QSharedPointer<PasswordKey>::create("turXpGMQiUE6CkPvWacydAKsnp4cxz");
324
QByteArray fileKeyBytes("Ma6hHov98FbPeyAL22XhcgmpJk8xjQ");
325
QBuffer fileKeyBuffer(&fileKeyBytes);
326
fileKeyBuffer.open(QBuffer::ReadOnly);
327
auto fileKey = QSharedPointer<FileKey>::create();
328
fileKey->load(&fileKeyBuffer);
330
auto crKey = QSharedPointer<MockChallengeResponseKey>::create(QByteArray("azdJnbVCFE76vV6t9RJ2DS6xvSS93k"));
332
auto compositeKey = QSharedPointer<CompositeKey>::create();
333
compositeKey->addKey(passwordKey);
334
compositeKey->addKey(fileKey);
335
compositeKey->addChallengeResponseKey(crKey);
337
QScopedPointer<Database> db(new Database());
338
db->changeKdf(fastKdf(db->kdf()));
339
QCOMPARE(db->kdf()->uuid(), KeePass2::KDF_AES_KDBX3); // default is legacy AES-KDF
340
db->setKey(compositeKey);
342
// upgrade the database by a specific method
343
if (upgradeAction == "none") {
345
} else if (upgradeAction == "meta-customdata") {
346
db->metadata()->customData()->set("abc", "def");
347
} else if (upgradeAction == "kdf-aes-kdbx3") {
348
db->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_AES_KDBX3)));
349
} else if (upgradeAction == "kdf-argon2") {
350
db->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2D)));
351
} else if (upgradeAction == "kdf-aes-kdbx4") {
352
db->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_AES_KDBX4)));
353
} else if (upgradeAction == "public-customdata") {
354
db->publicCustomData().insert("abc", "def");
355
} else if (upgradeAction == "rootgroup-customdata") {
356
db->rootGroup()->customData()->set("abc", "def");
357
} else if (upgradeAction == "group-customdata") {
358
auto group = new Group();
359
group->setParent(db->rootGroup());
360
group->setUuid(QUuid::createUuid());
361
group->customData()->set("abc", "def");
362
} else if (upgradeAction == "rootentry-customdata") {
363
auto entry = new Entry();
364
entry->setGroup(db->rootGroup());
365
entry->setUuid(QUuid::createUuid());
366
entry->customData()->set("abc", "def");
367
} else if (upgradeAction == "entry-customdata") {
368
auto group = new Group();
369
group->setParent(db->rootGroup());
370
group->setUuid(QUuid::createUuid());
371
auto entry = new Entry();
372
entry->setGroup(group);
373
entry->setUuid(QUuid::createUuid());
374
entry->customData()->set("abc", "def");
376
QFAIL(qPrintable(QString("Unknown action: %s").arg(upgradeAction)));
380
buffer.open(QBuffer::ReadWrite);
381
KeePass2Writer writer;
382
QVERIFY(writer.writeDatabase(&buffer, db.data()));
384
// paranoid check that we cannot decrypt the database without a key
386
KeePass2Reader reader;
387
auto db2 = QSharedPointer<Database>::create();
388
reader.readDatabase(&buffer, QSharedPointer<CompositeKey>::create(), db2.data());
389
QVERIFY(reader.hasError());
391
// check that we can read back the database with the original composite key,
392
// i.e., no components have been lost on the way
394
db2 = QSharedPointer<Database>::create();
395
reader.readDatabase(&buffer, compositeKey, db2.data());
396
if (reader.hasError()) {
397
QFAIL(qPrintable(reader.errorString()));
399
QCOMPARE(reader.version(), expectedVersion);
400
if (expectedVersion >= KeePass2::FILE_VERSION_4) {
401
QVERIFY(db2->kdf()->uuid() != KeePass2::KDF_AES_KDBX3);
405
void TestKdbx4Format::testUpgradeMasterKeyIntegrity_data()
407
QTest::addColumn<QString>("upgradeAction");
408
QTest::addColumn<quint32>("expectedVersion");
410
QTest::newRow("Upgrade: none") << QString("none") << KeePass2::FILE_VERSION_3_1;
411
QTest::newRow("Upgrade: none (meta-customdata)") << QString("meta-customdata") << KeePass2::FILE_VERSION_3_1;
412
QTest::newRow("Upgrade: none (explicit kdf-aes-kdbx3)") << QString("kdf-aes-kdbx3") << KeePass2::FILE_VERSION_3_1;
413
QTest::newRow("Upgrade (explicit): kdf-argon2") << QString("kdf-argon2") << KeePass2::FILE_VERSION_4;
414
QTest::newRow("Upgrade (explicit): kdf-aes-kdbx4") << QString("kdf-aes-kdbx4") << KeePass2::FILE_VERSION_4;
415
QTest::newRow("Upgrade (implicit): public-customdata") << QString("public-customdata") << KeePass2::FILE_VERSION_4;
416
QTest::newRow("Upgrade (implicit): rootgroup-customdata")
417
<< QString("rootgroup-customdata") << KeePass2::FILE_VERSION_4;
418
QTest::newRow("Upgrade (implicit): group-customdata") << QString("group-customdata") << KeePass2::FILE_VERSION_4;
419
QTest::newRow("Upgrade (implicit): rootentry-customdata")
420
<< QString("rootentry-customdata") << KeePass2::FILE_VERSION_4;
421
QTest::newRow("Upgrade (implicit): entry-customdata") << QString("entry-customdata") << KeePass2::FILE_VERSION_4;
424
void TestKdbx4Format::testAttachmentIndexStability()
426
QScopedPointer<Database> db(new Database());
427
db->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2ID)));
428
auto compositeKey = QSharedPointer<CompositeKey>::create();
429
db->setKey(compositeKey);
430
QVERIFY(!db->uuid().isNull());
432
auto root = db->rootGroup();
434
auto group1 = new Group();
435
group1->setUuid(QUuid::createUuid());
436
QVERIFY(!group1->uuid().isNull());
437
group1->setParent(root);
439
// Simulate KeeShare group, which uses its own attachment namespace
440
auto group2 = new Group();
441
group2->setUuid(QUuid::createUuid());
442
QVERIFY(!group2->uuid().isNull());
443
group2->setParent(group1);
444
#ifdef WITH_XC_KEESHARE
445
KeeShareSettings::Reference ref;
446
ref.type = KeeShareSettings::SynchronizeWith;
448
KeeShare::setReferenceTo(group2, ref);
449
QVERIFY(KeeShare::isShared(group2));
452
auto attachment1 = QByteArray("qwerty");
453
auto attachment2 = QByteArray("asdf");
454
auto attachment3 = QByteArray("zxcv");
456
auto entry1 = new Entry();
457
entry1->setUuid(QUuid::createUuid());
458
QVERIFY(!entry1->uuid().isNull());
459
auto uuid1 = entry1->uuid();
460
entry1->attachments()->set("a", attachment1);
461
QCOMPARE(entry1->attachments()->keys().size(), 1);
462
QCOMPARE(entry1->attachments()->values().size(), 1);
463
entry1->setGroup(root);
465
auto entry2 = new Entry();
466
entry2->setUuid(QUuid::createUuid());
467
QVERIFY(!entry2->uuid().isNull());
468
auto uuid2 = entry2->uuid();
469
entry2->attachments()->set("a", attachment1);
470
entry2->attachments()->set("b", attachment2);
471
QCOMPARE(entry2->attachments()->keys().size(), 2);
472
QCOMPARE(entry2->attachments()->values().size(), 2);
473
entry2->setGroup(group1);
475
auto entry3 = new Entry();
476
entry3->setUuid(QUuid::createUuid());
477
QVERIFY(!entry3->uuid().isNull());
478
auto uuid3 = entry3->uuid();
479
entry3->attachments()->set("a", attachment1);
480
entry3->attachments()->set("b", attachment2);
481
entry3->attachments()->set("x", attachment3);
482
entry3->attachments()->set("y", attachment3);
483
QCOMPARE(entry3->attachments()->keys().size(), 4);
484
QCOMPARE(entry3->attachments()->values().size(), 3);
485
entry3->setGroup(group2);
488
buffer.open(QBuffer::ReadWrite);
489
KeePass2Writer writer;
490
QVERIFY(writer.writeDatabase(&buffer, db.data()));
491
QVERIFY(writer.version() >= KeePass2::FILE_VERSION_4);
494
KeePass2Reader reader;
496
// Re-read database and check that all attachments are still correctly assigned
497
auto db2 = QSharedPointer<Database>::create();
498
reader.readDatabase(&buffer, QSharedPointer<CompositeKey>::create(), db2.data());
499
QVERIFY(!reader.hasError());
500
QVERIFY(!db->uuid().isNull());
502
auto a1 = db2->rootGroup()->findEntryByUuid(uuid1)->attachments();
503
QCOMPARE(a1->keys().size(), 1);
504
QCOMPARE(a1->values().size(), 1);
505
QCOMPARE(a1->value("a"), attachment1);
507
auto a2 = db2->rootGroup()->findEntryByUuid(uuid2)->attachments();
508
QCOMPARE(a2->keys().size(), 2);
509
QCOMPARE(a2->values().size(), 2);
510
QCOMPARE(a2->value("a"), attachment1);
511
QCOMPARE(a2->value("b"), attachment2);
513
#ifdef WITH_XC_KEESHARE
514
QVERIFY(KeeShare::isShared(db2->rootGroup()->findEntryByUuid(uuid3)->group()));
516
auto a3 = db2->rootGroup()->findEntryByUuid(uuid3)->attachments();
517
QCOMPARE(a3->keys().size(), 4);
518
QCOMPARE(a3->values().size(), 3);
519
QCOMPARE(a3->value("a"), attachment1);
520
QCOMPARE(a3->value("b"), attachment2);
521
QCOMPARE(a3->value("x"), attachment3);
522
QCOMPARE(a3->value("y"), attachment3);
525
void TestKdbx4Format::testCustomData()
529
// test public custom data
530
QVariantMap publicCustomData;
531
publicCustomData.insert("CD1", 123);
532
publicCustomData.insert("CD2", true);
533
publicCustomData.insert("CD3", "abcäöü");
534
db.setPublicCustomData(publicCustomData);
535
publicCustomData.insert("CD4", QByteArray::fromHex("ababa123ff"));
536
db.publicCustomData().insert("CD4", publicCustomData.value("CD4"));
537
QCOMPARE(db.publicCustomData(), publicCustomData);
539
const QString customDataKey1 = "CD1";
540
const QString customDataKey2 = "CD2";
541
const QString customData1 = "abcäöü";
542
const QString customData2 = "Hello World";
544
// test custom database data
545
db.metadata()->customData()->set(customDataKey1, customData1);
546
db.metadata()->customData()->set(customDataKey2, customData2);
547
auto lastModified = db.metadata()->customData()->value(CustomData::LastModified);
548
const int dataSize = customDataKey1.toUtf8().size() + customDataKey2.toUtf8().size() + customData1.toUtf8().size()
549
+ customData2.toUtf8().size() + lastModified.toUtf8().size()
550
+ CustomData::LastModified.toUtf8().size();
551
QCOMPARE(db.metadata()->customData()->size(), 3);
552
QCOMPARE(db.metadata()->customData()->dataSize(), dataSize);
554
// test custom root group data
555
Group* root = db.rootGroup();
556
root->customData()->set(customDataKey1, customData1);
557
root->customData()->set(customDataKey2, customData2);
558
QCOMPARE(root->customData()->size(), 3);
559
QCOMPARE(root->customData()->dataSize(), dataSize);
561
// test copied custom group data
562
auto* group = new Group();
563
group->setParent(root);
564
group->setUuid(QUuid::createUuid());
565
group->customData()->copyDataFrom(root->customData());
566
QCOMPARE(*group->customData(), *root->customData());
568
// test copied custom entry data
569
auto* entry = new Entry();
570
entry->setGroup(group);
571
entry->setUuid(QUuid::createUuid());
572
entry->customData()->copyDataFrom(group->customData());
573
QCOMPARE(*entry->customData(), *root->customData());
575
// test custom data deletion
576
entry->customData()->set("additional item", "foobar");
577
QCOMPARE(entry->customData()->size(), 4);
578
entry->customData()->remove("additional item");
579
QCOMPARE(entry->customData()->size(), 3);
580
QCOMPARE(entry->customData()->dataSize(), dataSize);
582
// test custom data on cloned groups
583
QScopedPointer<Group> clonedGroup(group->clone());
584
QCOMPARE(*clonedGroup->customData(), *group->customData());
586
// test custom data on cloned entries
587
QScopedPointer<Entry> clonedEntry(entry->clone(Entry::CloneNoFlags));
588
QCOMPARE(*clonedEntry->customData(), *entry->customData());
591
buffer.open(QBuffer::ReadWrite);
592
KeePass2Writer writer;
593
writer.writeDatabase(&buffer, &db);
597
KeePass2Reader reader;
598
auto newDb = QSharedPointer<Database>::create();
599
reader.readDatabase(&buffer, QSharedPointer<CompositeKey>::create(), newDb.data());
601
// test all custom data are read back successfully from KDBX
602
QCOMPARE(newDb->publicCustomData(), publicCustomData);
604
QCOMPARE(newDb->metadata()->customData()->value(customDataKey1), customData1);
605
QCOMPARE(newDb->metadata()->customData()->value(customDataKey2), customData2);
607
QCOMPARE(newDb->rootGroup()->customData()->value(customDataKey1), customData1);
608
QCOMPARE(newDb->rootGroup()->customData()->value(customDataKey2), customData2);
610
auto* newGroup = newDb->rootGroup()->children()[0];
611
QCOMPARE(newGroup->customData()->value(customDataKey1), customData1);
612
QCOMPARE(newGroup->customData()->value(customDataKey2), customData2);
614
auto* newEntry = newDb->rootGroup()->children()[0]->entries()[0];
615
QCOMPARE(newEntry->customData()->value(customDataKey1), customData1);
616
QCOMPARE(newEntry->customData()->value(customDataKey2), customData2);