keepassxc/tests/TestKdbx4.cpp
Janek Bevendorff e443cde452 Add a new database settings wizard
This patch implements a new database wizard to guide users through the process
of setting up a new database and choosing sane encryption settings.

It also reimplements the master key settings to be more
user-friendly. Users can now add, change, or remove individual composite
key components instead of having to set all components at once. This
avoids confusion about a password being reset if the user only wants to
add a key file.

With these changes comes a major refactor of how database composite keys and key
components are handled. Copying of keys is prohibited and each key
exists only once in memory and is referenced via shared pointers. GUI
components for changing individual keys are encapsulated into separate
classes to be more reusable. The password edit and generator widgets
have also been refactored to be more reusable.
2018-09-25 21:12:47 +02:00

429 lines
18 KiB
C++

/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "TestKdbx4.h"
#include "TestGlobal.h"
#include "config-keepassx-tests.h"
#include "core/Metadata.h"
#include "format/KdbxXmlReader.h"
#include "format/KdbxXmlWriter.h"
#include "format/KeePass2.h"
#include "format/KeePass2Reader.h"
#include "format/KeePass2Writer.h"
#include "keys/FileKey.h"
#include "keys/PasswordKey.h"
#include "mock/MockChallengeResponseKey.h"
QTEST_GUILESS_MAIN(TestKdbx4)
void TestKdbx4::initTestCaseImpl()
{
m_xmlDb->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2)));
m_kdbxSourceDb->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2)));
}
Database* TestKdbx4::readXml(const QString& path, bool strictMode, bool& hasError, QString& errorString)
{
KdbxXmlReader reader(KeePass2::FILE_VERSION_4);
reader.setStrictMode(strictMode);
auto db = reader.readDatabase(path);
hasError = reader.hasError();
errorString = reader.errorString();
return db;
}
Database* TestKdbx4::readXml(QBuffer* buf, bool strictMode, bool& hasError, QString& errorString)
{
KdbxXmlReader reader(KeePass2::FILE_VERSION_4);
reader.setStrictMode(strictMode);
auto db = reader.readDatabase(buf);
hasError = reader.hasError();
errorString = reader.errorString();
return db;
}
void TestKdbx4::writeXml(QBuffer* buf, Database* db, bool& hasError, QString& errorString)
{
KdbxXmlWriter writer(KeePass2::FILE_VERSION_4);
writer.writeDatabase(buf, db);
hasError = writer.hasError();
errorString = writer.errorString();
}
void TestKdbx4::readKdbx(QIODevice* device,
QSharedPointer<const CompositeKey> key,
QScopedPointer<Database>& db,
bool& hasError,
QString& errorString)
{
KeePass2Reader reader;
db.reset(reader.readDatabase(device, key));
hasError = reader.hasError();
if (hasError) {
errorString = reader.errorString();
}
QCOMPARE(reader.version(), KeePass2::FILE_VERSION_4);
}
void TestKdbx4::readKdbx(const QString& path,
QSharedPointer<const CompositeKey> key,
QScopedPointer<Database>& db,
bool& hasError,
QString& errorString)
{
KeePass2Reader reader;
db.reset(reader.readDatabase(path, key));
hasError = reader.hasError();
if (hasError) {
errorString = reader.errorString();
}
QCOMPARE(reader.version(), KeePass2::FILE_VERSION_4);
}
void TestKdbx4::writeKdbx(QIODevice* device, Database* db, bool& hasError, QString& errorString)
{
if (db->kdf()->uuid() == KeePass2::KDF_AES_KDBX3) {
db->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2)));
}
KeePass2Writer writer;
hasError = writer.writeDatabase(device, db);
hasError = writer.hasError();
if (hasError) {
errorString = writer.errorString();
}
QCOMPARE(writer.version(), KeePass2::FILE_VERSION_4);
}
Q_DECLARE_METATYPE(QUuid)
void TestKdbx4::testFormat400()
{
QString filename = QString(KEEPASSX_TEST_DATA_DIR).append("/Format400.kdbx");
auto key = QSharedPointer<CompositeKey>::create();
key->addKey(QSharedPointer<PasswordKey>::create("t"));
KeePass2Reader reader;
QScopedPointer<Database> db(reader.readDatabase(filename, key));
QCOMPARE(reader.version(), KeePass2::FILE_VERSION_4);
QVERIFY(db.data());
QVERIFY(!reader.hasError());
QCOMPARE(db->rootGroup()->name(), QString("Format400"));
QCOMPARE(db->metadata()->name(), QString("Format400"));
QCOMPARE(db->rootGroup()->entries().size(), 1);
auto entry = db->rootGroup()->entries().at(0);
QCOMPARE(entry->title(), QString("Format400"));
QCOMPARE(entry->username(), QString("Format400"));
QCOMPARE(entry->attributes()->keys().size(), 6);
QCOMPARE(entry->attributes()->value("Format400"), QString("Format400"));
QCOMPARE(entry->attachments()->keys().size(), 1);
QCOMPARE(entry->attachments()->value("Format400"), QByteArray("Format400\n"));
}
void TestKdbx4::testFormat400Upgrade()
{
QFETCH(QUuid, kdfUuid);
QFETCH(QUuid, cipherUuid);
QFETCH(bool, addCustomData);
QFETCH(quint32, expectedVersion);
QScopedPointer<Database> sourceDb(new Database());
sourceDb->changeKdf(fastKdf(sourceDb->kdf()));
sourceDb->metadata()->setName("Wubba lubba dub dub");
QCOMPARE(sourceDb->kdf()->uuid(), KeePass2::KDF_AES_KDBX3); // default is legacy AES-KDF
auto key = QSharedPointer<CompositeKey>::create();
key->addKey(QSharedPointer<PasswordKey>::create("I am in great pain, please help me!"));
sourceDb->setKey(key, true, true);
QBuffer buffer;
buffer.open(QBuffer::ReadWrite);
// upgrade to KDBX 4 by changing KDF and Cipher
sourceDb->changeKdf(fastKdf(KeePass2::uuidToKdf(kdfUuid)));
sourceDb->setCipher(cipherUuid);
// CustomData in meta should not cause any version change
sourceDb->metadata()->customData()->set("CustomPublicData", "Hey look, I turned myself into a pickle!");
if (addCustomData) {
// this, however, should
sourceDb->rootGroup()->customData()->set("CustomGroupData",
"I just killed my family! I don't care who they were!");
}
KeePass2Writer writer;
writer.writeDatabase(&buffer, sourceDb.data());
if (writer.hasError()) {
QFAIL(qPrintable(QString("Error while writing database: %1").arg(writer.errorString())));
}
// read buffer back
buffer.seek(0);
KeePass2Reader reader;
QScopedPointer<Database> targetDb(reader.readDatabase(&buffer, key));
if (reader.hasError()) {
QFAIL(qPrintable(QString("Error while reading database: %1").arg(reader.errorString())));
}
QVERIFY(targetDb->rootGroup());
QCOMPARE(targetDb->metadata()->name(), sourceDb->metadata()->name());
QCOMPARE(reader.version(), expectedVersion);
QCOMPARE(targetDb->cipher(), cipherUuid);
QCOMPARE(*targetDb->metadata()->customData(), *sourceDb->metadata()->customData());
QCOMPARE(*targetDb->rootGroup()->customData(), *sourceDb->rootGroup()->customData());
}
// clang-format off
void TestKdbx4::testFormat400Upgrade_data()
{
QTest::addColumn<QUuid>("kdfUuid");
QTest::addColumn<QUuid>("cipherUuid");
QTest::addColumn<bool>("addCustomData");
QTest::addColumn<quint32>("expectedVersion");
auto constexpr kdbx3 = KeePass2::FILE_VERSION_3_1 & KeePass2::FILE_VERSION_CRITICAL_MASK;
auto constexpr kdbx4 = KeePass2::FILE_VERSION_4 & KeePass2::FILE_VERSION_CRITICAL_MASK;
QTest::newRow("Argon2 + AES") << KeePass2::KDF_ARGON2 << KeePass2::CIPHER_AES << false << kdbx4;
QTest::newRow("AES-KDF + AES") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_AES << false << kdbx4;
QTest::newRow("AES-KDF (legacy) + AES") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_AES << false << kdbx3;
QTest::newRow("Argon2 + AES + CustomData") << KeePass2::KDF_ARGON2 << KeePass2::CIPHER_AES << true << kdbx4;
QTest::newRow("AES-KDF + AES + CustomData") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_AES << true << kdbx4;
QTest::newRow("AES-KDF (legacy) + AES + CustomData") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_AES << true << kdbx4;
QTest::newRow("Argon2 + ChaCha20") << KeePass2::KDF_ARGON2 << KeePass2::CIPHER_CHACHA20 << false << kdbx4;
QTest::newRow("AES-KDF + ChaCha20") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_CHACHA20 << false << kdbx4;
QTest::newRow("AES-KDF (legacy) + ChaCha20") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_CHACHA20 << false << kdbx3;
QTest::newRow("Argon2 + ChaCha20 + CustomData") << KeePass2::KDF_ARGON2 << KeePass2::CIPHER_CHACHA20 << true << kdbx4;
QTest::newRow("AES-KDF + ChaCha20 + CustomData") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_CHACHA20 << true << kdbx4;
QTest::newRow("AES-KDF (legacy) + ChaCha20 + CustomData") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_CHACHA20 << true << kdbx4;
QTest::newRow("Argon2 + Twofish") << KeePass2::KDF_ARGON2 << KeePass2::CIPHER_TWOFISH << false << kdbx4;
QTest::newRow("AES-KDF + Twofish") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_TWOFISH << false << kdbx4;
QTest::newRow("AES-KDF (legacy) + Twofish") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_TWOFISH << false << kdbx3;
QTest::newRow("Argon2 + Twofish + CustomData") << KeePass2::KDF_ARGON2 << KeePass2::CIPHER_TWOFISH << true << kdbx4;
QTest::newRow("AES-KDF + Twofish + CustomData") << KeePass2::KDF_AES_KDBX4 << KeePass2::CIPHER_TWOFISH << true << kdbx4;
QTest::newRow("AES-KDF (legacy) + Twofish + CustomData") << KeePass2::KDF_AES_KDBX3 << KeePass2::CIPHER_TWOFISH << true << kdbx4;
}
// clang-format on
void TestKdbx4::testUpgradeMasterKeyIntegrity()
{
QFETCH(QString, upgradeAction);
QFETCH(quint32, expectedVersion);
// prepare composite key
auto passwordKey = QSharedPointer<PasswordKey>::create("turXpGMQiUE6CkPvWacydAKsnp4cxz");
QByteArray fileKeyBytes("Ma6hHov98FbPeyAL22XhcgmpJk8xjQ");
QBuffer fileKeyBuffer(&fileKeyBytes);
fileKeyBuffer.open(QBuffer::ReadOnly);
auto fileKey = QSharedPointer<FileKey>::create();
fileKey->load(&fileKeyBuffer);
auto crKey = QSharedPointer<MockChallengeResponseKey>::create(QByteArray("azdJnbVCFE76vV6t9RJ2DS6xvSS93k"));
auto compositeKey = QSharedPointer<CompositeKey>::create();
compositeKey->addKey(passwordKey);
compositeKey->addKey(fileKey);
compositeKey->addChallengeResponseKey(crKey);
QScopedPointer<Database> db(new Database());
db->changeKdf(fastKdf(db->kdf()));
db->setKey(compositeKey);
// upgrade the database by a specific method
if (upgradeAction == "none") {
// do nothing
} else if (upgradeAction == "meta-customdata") {
db->metadata()->customData()->set("abc", "def");
} else if (upgradeAction == "kdf-aes-kdbx3") {
db->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_AES_KDBX3)));
} else if (upgradeAction == "kdf-argon2") {
db->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2)));
} else if (upgradeAction == "kdf-aes-kdbx4") {
db->changeKdf(fastKdf(KeePass2::uuidToKdf(KeePass2::KDF_AES_KDBX4)));
} else if (upgradeAction == "public-customdata") {
db->publicCustomData().insert("abc", "def");
} else if (upgradeAction == "rootgroup-customdata") {
db->rootGroup()->customData()->set("abc", "def");
} else if (upgradeAction == "group-customdata") {
auto group = new Group();
group->setParent(db->rootGroup());
group->setUuid(QUuid::createUuid());
group->customData()->set("abc", "def");
} else if (upgradeAction == "rootentry-customdata") {
auto entry = new Entry();
entry->setGroup(db->rootGroup());
entry->setUuid(QUuid::createUuid());
entry->customData()->set("abc", "def");
} else if (upgradeAction == "entry-customdata") {
auto group = new Group();
group->setParent(db->rootGroup());
group->setUuid(QUuid::createUuid());
auto entry = new Entry();
entry->setGroup(group);
entry->setUuid(QUuid::createUuid());
entry->customData()->set("abc", "def");
} else {
QFAIL(qPrintable(QString("Unknown action: %s").arg(upgradeAction)));
}
QBuffer buffer;
buffer.open(QBuffer::ReadWrite);
KeePass2Writer writer;
QVERIFY(writer.writeDatabase(&buffer, db.data()));
// paranoid check that we cannot decrypt the database without a key
buffer.seek(0);
KeePass2Reader reader;
QScopedPointer<Database> db2;
db2.reset(reader.readDatabase(&buffer, QSharedPointer<CompositeKey>::create()));
QVERIFY(reader.hasError());
// check that we can read back the database with the original composite key,
// i.e., no components have been lost on the way
buffer.seek(0);
db2.reset(reader.readDatabase(&buffer, compositeKey));
if (reader.hasError()) {
QFAIL(qPrintable(reader.errorString()));
}
QCOMPARE(reader.version(), expectedVersion & KeePass2::FILE_VERSION_CRITICAL_MASK);
}
void TestKdbx4::testUpgradeMasterKeyIntegrity_data()
{
QTest::addColumn<QString>("upgradeAction");
QTest::addColumn<quint32>("expectedVersion");
QTest::newRow("Upgrade: none") << QString("none") << KeePass2::FILE_VERSION_3;
QTest::newRow("Upgrade: none (meta-customdata)") << QString("meta-customdata") << KeePass2::FILE_VERSION_3;
QTest::newRow("Upgrade: none (explicit kdf-aes-kdbx3)") << QString("kdf-aes-kdbx3") << KeePass2::FILE_VERSION_3;
QTest::newRow("Upgrade (explicit): kdf-argon2") << QString("kdf-argon2") << KeePass2::FILE_VERSION_4;
QTest::newRow("Upgrade (explicit): kdf-aes-kdbx4") << QString("kdf-aes-kdbx4") << KeePass2::FILE_VERSION_4;
QTest::newRow("Upgrade (implicit): public-customdata") << QString("public-customdata") << KeePass2::FILE_VERSION_4;
QTest::newRow("Upgrade (implicit): rootgroup-customdata") << QString("rootgroup-customdata")
<< KeePass2::FILE_VERSION_4;
QTest::newRow("Upgrade (implicit): group-customdata") << QString("group-customdata") << KeePass2::FILE_VERSION_4;
QTest::newRow("Upgrade (implicit): rootentry-customdata") << QString("rootentry-customdata")
<< KeePass2::FILE_VERSION_4;
QTest::newRow("Upgrade (implicit): entry-customdata") << QString("entry-customdata") << KeePass2::FILE_VERSION_4;
}
void TestKdbx4::testCustomData()
{
Database db;
// test public custom data
QVariantMap publicCustomData;
publicCustomData.insert("CD1", 123);
publicCustomData.insert("CD2", true);
publicCustomData.insert("CD3", "abcäöü");
db.setPublicCustomData(publicCustomData);
publicCustomData.insert("CD4", QByteArray::fromHex("ababa123ff"));
db.publicCustomData().insert("CD4", publicCustomData.value("CD4"));
QCOMPARE(db.publicCustomData(), publicCustomData);
const QString customDataKey1 = "CD1";
const QString customDataKey2 = "CD2";
const QString customData1 = "abcäöü";
const QString customData2 = "Hello World";
const int dataSize = customDataKey1.toUtf8().size() + customDataKey1.toUtf8().size() + customData1.toUtf8().size()
+ customData2.toUtf8().size();
// test custom database data
db.metadata()->customData()->set(customDataKey1, customData1);
db.metadata()->customData()->set(customDataKey2, customData2);
QCOMPARE(db.metadata()->customData()->size(), 2);
QCOMPARE(db.metadata()->customData()->dataSize(), dataSize);
// test custom root group data
Group* root = db.rootGroup();
root->customData()->set(customDataKey1, customData1);
root->customData()->set(customDataKey2, customData2);
QCOMPARE(root->customData()->size(), 2);
QCOMPARE(root->customData()->dataSize(), dataSize);
// test copied custom group data
auto* group = new Group();
group->setParent(root);
group->setUuid(QUuid::createUuid());
group->customData()->copyDataFrom(root->customData());
QCOMPARE(*group->customData(), *root->customData());
// test copied custom entry data
auto* entry = new Entry();
entry->setGroup(group);
entry->setUuid(QUuid::createUuid());
entry->customData()->copyDataFrom(group->customData());
QCOMPARE(*entry->customData(), *root->customData());
// test custom data deletion
entry->customData()->set("additional item", "foobar");
QCOMPARE(entry->customData()->size(), 3);
entry->customData()->remove("additional item");
QCOMPARE(entry->customData()->size(), 2);
QCOMPARE(entry->customData()->dataSize(), dataSize);
// test custom data on cloned groups
QScopedPointer<Group> clonedGroup(group->clone());
QCOMPARE(*clonedGroup->customData(), *group->customData());
// test custom data on cloned entries
QScopedPointer<Entry> clonedEntry(entry->clone(Entry::CloneNoFlags));
QCOMPARE(*clonedEntry->customData(), *entry->customData());
QBuffer buffer;
buffer.open(QBuffer::ReadWrite);
KeePass2Writer writer;
writer.writeDatabase(&buffer, &db);
// read buffer back
buffer.seek(0);
KeePass2Reader reader;
QSharedPointer<Database> newDb(reader.readDatabase(&buffer, QSharedPointer<CompositeKey>::create()));
// test all custom data are read back successfully from KDBX
QCOMPARE(newDb->publicCustomData(), publicCustomData);
QCOMPARE(newDb->metadata()->customData()->value(customDataKey1), customData1);
QCOMPARE(newDb->metadata()->customData()->value(customDataKey2), customData2);
QCOMPARE(newDb->rootGroup()->customData()->value(customDataKey1), customData1);
QCOMPARE(newDb->rootGroup()->customData()->value(customDataKey2), customData2);
auto* newGroup = newDb->rootGroup()->children()[0];
QCOMPARE(newGroup->customData()->value(customDataKey1), customData1);
QCOMPARE(newGroup->customData()->value(customDataKey2), customData2);
auto* newEntry = newDb->rootGroup()->children()[0]->entries()[0];
QCOMPARE(newEntry->customData()->value(customDataKey1), customData1);
QCOMPARE(newEntry->customData()->value(customDataKey2), customData2);
}
QSharedPointer<Kdf> TestKdbx4::fastKdf(QSharedPointer<Kdf> kdf)
{
kdf->setRounds(1);
if (kdf->uuid() == KeePass2::KDF_ARGON2) {
kdf->processParameters({{KeePass2::KDFPARAM_ARGON2_MEMORY, 1024}, {KeePass2::KDFPARAM_ARGON2_PARALLELISM, 1}});
}
return kdf;
}