Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/keepassxreboot/keepassxc.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJanek Bevendorff <janek@jbev.net>2020-12-10 03:28:01 +0300
committerJonathan White <support@dmapps.us>2020-12-19 17:42:21 +0300
commitcd0084f21cc4eb7c0f2b00bdafb0eeba051fd927 (patch)
tree2101eb5da0a82a4b3d3669e899d0b615a52b8825
parent404fd941e8e5428c8fe80c78f16ff7e4fa09aff4 (diff)
Add support for version 2 XML key files.
As discussed in #4317, the next KeePass2 release will ship with support for a new generation of XML key files which enable hash integrity checks. This patch adds support for reading and generating this new format. By default, KeePass2 now uses the .keyx extension for generated key files, which was added to KeePassXC's key generation file chooser filter. We continue to generate hashed binary key files by default, but the user can explicitly save the file with the new .keyx extension to generate an XML v2 key file (currently undocumented). When opening a database, the key file type is still determined by content negotation, so the file extension has no impact here. As an additional change, the legacy key file warnings have been improved slightly to be less confusing and more helpful.
-rw-r--r--src/cli/Utils.cpp6
-rw-r--r--src/gui/DatabaseOpenWidget.cpp14
-rw-r--r--src/gui/databasekey/KeyFileEditWidget.cpp14
-rw-r--r--src/keys/FileKey.cpp219
-rw-r--r--src/keys/FileKey.h12
-rw-r--r--tests/TestKeys.cpp50
-rw-r--r--tests/data/FileKeyXmlV2.kdbxbin0 -> 1582 bytes
-rw-r--r--tests/data/FileKeyXmlV2.keyx12
-rw-r--r--tests/data/FileKeyXmlV2BrokenHex.kdbxbin0 -> 1550 bytes
-rw-r--r--tests/data/FileKeyXmlV2BrokenHex.keyx12
-rw-r--r--tests/data/FileKeyXmlV2HashFail.kdbxbin0 -> 1582 bytes
-rw-r--r--tests/data/FileKeyXmlV2HashFail.keyx12
12 files changed, 233 insertions, 118 deletions
diff --git a/src/cli/Utils.cpp b/src/cli/Utils.cpp
index bd3f3e2cc..ca440a393 100644
--- a/src/cli/Utils.cpp
+++ b/src/cli/Utils.cpp
@@ -132,9 +132,9 @@ namespace Utils
return {};
}
- if (fileKey->type() != FileKey::Hashed) {
- err << QObject::tr("WARNING: You are using a legacy key file format which may become\n"
- "unsupported in the future.\n\n"
+ if (fileKey->type() != FileKey::KeePass2XMLv2 && fileKey->type() != FileKey::Hashed) {
+ err << QObject::tr("WARNING: You are using an old key file format which KeePassXC may\n"
+ "stop supporting in the future.\n\n"
"Please consider generating a new key file.")
<< endl;
}
diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp
index b9f77823c..789025234 100644
--- a/src/gui/DatabaseOpenWidget.cpp
+++ b/src/gui/DatabaseOpenWidget.cpp
@@ -301,12 +301,14 @@ QSharedPointer<CompositeKey> DatabaseOpenWidget::buildDatabaseKey()
m_ui->messageWidget->showMessage(tr("Failed to open key file: %1").arg(errorMsg), MessageWidget::Error);
return {};
}
- if (key->type() != FileKey::Hashed && !config()->get(Config::Messages_NoLegacyKeyFileWarning).toBool()) {
+ if (key->type() != FileKey::KeePass2XMLv2 && key->type() != FileKey::Hashed
+ && !config()->get(Config::Messages_NoLegacyKeyFileWarning).toBool()) {
QMessageBox legacyWarning;
- legacyWarning.setWindowTitle(tr("Legacy key file format"));
- legacyWarning.setText(tr("You are using a legacy key file format which may become\n"
- "unsupported in the future.\n\n"
- "Please consider generating a new key file."));
+ legacyWarning.setWindowTitle(tr("Old key file format"));
+ legacyWarning.setText(tr("You are using an old key file format which KeePassXC may<br>"
+ "stop supporting in the future.<br><br>"
+ "Please consider generating a new key file by going to:<br>"
+ "<strong>Database / Database Security / Change Key File.</strong><br>"));
legacyWarning.setIcon(QMessageBox::Icon::Warning);
legacyWarning.addButton(QMessageBox::Ok);
legacyWarning.setDefaultButton(QMessageBox::Ok);
@@ -355,7 +357,7 @@ void DatabaseOpenWidget::reject()
void DatabaseOpenWidget::browseKeyFile()
{
- QString filters = QString("%1 (*);;%2 (*.key)").arg(tr("All files"), tr("Key files"));
+ QString filters = QString("%1 (*);;%2 (*.keyx; *.key)").arg(tr("All files"), tr("Key files"));
if (!config()->get(Config::RememberLastKeyFiles).toBool()) {
fileDialog()->setNextForgetDialog();
}
diff --git a/src/gui/databasekey/KeyFileEditWidget.cpp b/src/gui/databasekey/KeyFileEditWidget.cpp
index e0486ae07..2fb0b3de2 100644
--- a/src/gui/databasekey/KeyFileEditWidget.cpp
+++ b/src/gui/databasekey/KeyFileEditWidget.cpp
@@ -47,12 +47,12 @@ bool KeyFileEditWidget::addToCompositeKey(QSharedPointer<CompositeKey> key)
return false;
}
- if (fileKey->type() != FileKey::Hashed) {
+ if (fileKey->type() != FileKey::KeePass2XMLv2 && fileKey->type() != FileKey::Hashed) {
QMessageBox::warning(getMainWindow(),
- tr("Legacy key file format"),
- tr("You are using a legacy key file format which may become\n"
- "unsupported in the future.\n\n"
- "Generate a new key file in the database security settings."),
+ tr("Old key file format"),
+ tr("You selected a key file in an old format which KeePassXC<br>"
+ "may stop supporting in the future.<br><br>"
+ "Please consider generating a new key file instead."),
QMessageBox::Ok);
}
@@ -96,7 +96,7 @@ void KeyFileEditWidget::createKeyFile()
if (!m_compEditWidget) {
return;
}
- QString filters = QString("%1 (*.key);;%2 (*)").arg(tr("Key files"), tr("All files"));
+ QString filters = QString("%1 (*.keyx; *.key);;%2 (*)").arg(tr("Key files"), tr("All files"));
QString fileName = fileDialog()->getSaveFileName(this, tr("Create Key File..."), QString(), filters);
if (!fileName.isEmpty()) {
@@ -119,7 +119,7 @@ void KeyFileEditWidget::browseKeyFile()
if (!m_compEditWidget) {
return;
}
- QString filters = QString("%1 (*.key);;%2 (*)").arg(tr("Key files"), tr("All files"));
+ QString filters = QString("%1 (*.keyx; *.key);;%2 (*)").arg(tr("Key files"), tr("All files"));
QString fileName = fileDialog()->getOpenFileName(this, tr("Select a key file"), QString(), filters);
if (QFileInfo(fileName).canonicalFilePath() == m_parent->getDatabase()->canonicalFilePath()) {
diff --git a/src/keys/FileKey.cpp b/src/keys/FileKey.cpp
index 6142d58d7..2ac52ae69 100644
--- a/src/keys/FileKey.cpp
+++ b/src/keys/FileKey.cpp
@@ -64,9 +64,10 @@ FileKey::~FileKey()
* removed in a future version.
*
* @param device input device
+ * @param errorMsg error message in case of fatal failure
* @return true if key file was loaded successfully
*/
-bool FileKey::load(QIODevice* device)
+bool FileKey::load(QIODevice* device, QString* errorMsg)
{
m_type = None;
@@ -75,32 +76,33 @@ bool FileKey::load(QIODevice* device)
return false;
}
- if (device->size() == 0) {
+ if (device->size() == 0 || !device->reset()) {
return false;
}
- // try different legacy key file formats
- if (!device->reset()) {
- return false;
- }
- if (loadXml(device)) {
- m_type = KeePass2XML;
+ // load XML key file v1 or v2
+ QString xmlError;
+ if (loadXml(device, &xmlError)) {
return true;
}
- if (!device->reset()) {
+ if (!device->reset() || !xmlError.isEmpty()) {
+ if (errorMsg) {
+ *errorMsg = xmlError;
+ }
return false;
}
+
+ // try legacy key file formats
if (loadBinary(device)) {
- m_type = FixedBinary;
return true;
}
if (!device->reset()) {
return false;
}
+
if (loadHex(device)) {
- m_type = FixedBinaryHex;
return true;
}
@@ -109,7 +111,6 @@ bool FileKey::load(QIODevice* device)
return false;
}
if (loadHashed(device)) {
- m_type = Hashed;
return true;
}
@@ -145,10 +146,14 @@ bool FileKey::load(const QString& fileName, QString* errorMsg)
}
return false;
}
- bool result = load(&file);
+ bool result = load(&file, errorMsg);
file.close();
+ if (errorMsg && !errorMsg->isEmpty()) {
+ return false;
+ }
+
if (file.error()) {
result = false;
if (errorMsg) {
@@ -171,17 +176,65 @@ QByteArray FileKey::rawKey() const
}
/**
- * Generate a new key file from random bytes.
+ * Generate a new key file with random bytes.
*
* @param device output device
* @param number of random bytes to generate
*/
-void FileKey::create(QIODevice* device, int size)
+void FileKey::createRandom(QIODevice* device, int size)
{
device->write(randomGen()->randomArray(size));
}
/**
+ * Generate a new key file in the KeePass2 XML format v2.
+ *
+ * @param device output device
+ * @param number of random bytes to generate
+ */
+void FileKey::createXMLv2(QIODevice* device, int size)
+{
+ QXmlStreamWriter w(device);
+ w.setAutoFormatting(true);
+ w.setAutoFormattingIndent(4);
+ w.writeStartDocument();
+
+ w.writeStartElement("KeyFile");
+
+ w.writeStartElement("Meta");
+ w.writeTextElement("Version", "2.0");
+ w.writeEndElement();
+
+ w.writeStartElement("Key");
+ w.writeStartElement("Data");
+
+ QByteArray key = randomGen()->randomArray(size);
+ CryptoHash hash(CryptoHash::Sha256);
+ hash.addData(key);
+ QByteArray result = hash.result().left(4);
+ key = key.toHex().toUpper();
+
+ w.writeAttribute("Hash", result.toHex().toUpper());
+ w.writeCharacters("\n ");
+ for (int i = 0; i < key.size(); ++i) {
+ // Pretty-print hex value (not strictly necessary, but nicer to read and KeePass2 does it)
+ if (i != 0 && i % 32 == 0) {
+ w.writeCharacters("\n ");
+ } else if (i != 0 && i % 8 == 0) {
+ w.writeCharacters(" ");
+ }
+ w.writeCharacters(QChar(key[i]));
+ }
+ sodium_memzero(key.data(), static_cast<std::size_t>(key.capacity()));
+ w.writeCharacters("\n ");
+
+ w.writeEndElement();
+ w.writeEndElement();
+
+ w.writeEndDocument();
+}
+
+/**
* Create a new key file from random bytes.
*
* @param fileName output file name
@@ -189,7 +242,7 @@ void FileKey::create(QIODevice* device, int size)
* @param number of random bytes to generate
* @return true on successful creation
*/
-bool FileKey::create(const QString& fileName, QString* errorMsg, int size)
+bool FileKey::create(const QString& fileName, QString* errorMsg)
{
QFile file(fileName);
if (!file.open(QFile::WriteOnly)) {
@@ -198,7 +251,11 @@ bool FileKey::create(const QString& fileName, QString* errorMsg, int size)
}
return false;
}
- create(&file, size);
+ if (fileName.endsWith(".keyx")) {
+ createXMLv2(&file);
+ } else {
+ createRandom(&file);
+ }
file.close();
file.setPermissions(QFile::ReadUser);
@@ -218,88 +275,87 @@ bool FileKey::create(const QString& fileName, QString* errorMsg, int size)
*
* @param device input device
* @return true on success
- * @deprecated
*/
-bool FileKey::loadXml(QIODevice* device)
+bool FileKey::loadXml(QIODevice* device, QString* errorMsg)
{
QXmlStreamReader xmlReader(device);
- if (!xmlReader.error() && xmlReader.readNextStartElement()) {
- if (xmlReader.name() != "KeyFile") {
- return false;
- }
- } else {
+ if (xmlReader.error()) {
+ return false;
+ }
+ if (xmlReader.readNextStartElement() && xmlReader.name() != "KeyFile") {
return false;
}
- bool correctMeta = false;
- QByteArray data;
+ struct
+ {
+ QString version;
+ QByteArray hash;
+ QByteArray data;
+ } keyFileData;
while (!xmlReader.error() && xmlReader.readNextStartElement()) {
if (xmlReader.name() == "Meta") {
- correctMeta = loadXmlMeta(xmlReader);
+ while (!xmlReader.error() && xmlReader.readNextStartElement()) {
+ if (xmlReader.name() == "Version") {
+ keyFileData.version = xmlReader.readElementText();
+ if (keyFileData.version.startsWith("1.0")) {
+ m_type = KeePass2XML;
+ } else if (keyFileData.version == "2.0") {
+ m_type = KeePass2XMLv2;
+ } else {
+ if (errorMsg) {
+ *errorMsg = QObject::tr("Unsupported key file version: %1").arg(keyFileData.version);
+ }
+ return false;
+ }
+ }
+ }
} else if (xmlReader.name() == "Key") {
- data = loadXmlKey(xmlReader);
+ while (!xmlReader.error() && xmlReader.readNextStartElement()) {
+ if (xmlReader.name() == "Data") {
+ keyFileData.hash = QByteArray::fromHex(xmlReader.attributes().value("Hash").toLatin1());
+ QByteArray rawData = xmlReader.readElementText().simplified().replace(" ", "").toLatin1();
+
+ if (keyFileData.version.startsWith("1.0") && Tools::isBase64(rawData)) {
+ keyFileData.data = QByteArray::fromBase64(rawData);
+ } else if (keyFileData.version == "2.0" && Tools::isHex(rawData)) {
+ keyFileData.data = QByteArray::fromHex(rawData);
+
+ CryptoHash hash(CryptoHash::Sha256);
+ hash.addData(keyFileData.data);
+ QByteArray result = hash.result().left(4);
+ if (keyFileData.hash != result) {
+ if (errorMsg) {
+ *errorMsg = QObject::tr("Checksum mismatch! Key file may be corrupt.");
+ }
+ return false;
+ }
+ } else {
+ if (errorMsg) {
+ *errorMsg = QObject::tr("Unexpected key file data! Key file may be corrupt.");
+ }
+ return false;
+ }
+
+ sodium_memzero(rawData.data(), static_cast<std::size_t>(rawData.capacity()));
+ }
+ }
}
}
bool ok = false;
- if (!xmlReader.error() && correctMeta && !data.isEmpty()) {
- std::memcpy(m_key, data.data(), std::min(SHA256_SIZE, data.size()));
+ if (!xmlReader.error() && !keyFileData.data.isEmpty()) {
+ std::memcpy(m_key, keyFileData.data.data(), std::min(SHA256_SIZE, keyFileData.data.size()));
ok = true;
}
- sodium_memzero(data.data(), static_cast<std::size_t>(data.capacity()));
+ sodium_memzero(keyFileData.data.data(), static_cast<std::size_t>(keyFileData.data.capacity()));
return ok;
}
/**
- * Load meta data from legacy KeePass 2 XML key file.
- *
- * @param xmlReader input XML reader
- * @return true on success
- * @deprecated
- */
-bool FileKey::loadXmlMeta(QXmlStreamReader& xmlReader)
-{
- bool correctVersion = false;
-
- while (!xmlReader.error() && xmlReader.readNextStartElement()) {
- if (xmlReader.name() == "Version") {
- if (xmlReader.readElementText() == "1.00") {
- correctVersion = true;
- }
- }
- }
-
- return correctVersion;
-}
-
-/**
- * Load base64 key data from legacy KeePass 2 XML key file.
- *
- * @param xmlReader input XML reader
- * @return true on success
- * @deprecated
- */
-QByteArray FileKey::loadXmlKey(QXmlStreamReader& xmlReader)
-{
- QByteArray data;
-
- while (!xmlReader.error() && xmlReader.readNextStartElement()) {
- if (xmlReader.name() == "Data") {
- QByteArray rawData = xmlReader.readElementText().toLatin1();
- if (Tools::isBase64(rawData)) {
- data = QByteArray::fromBase64(rawData);
- }
- }
- }
-
- return data;
-}
-
-/**
* Load fixed 32-bit binary key file.
*
* @param device input device
@@ -315,11 +371,12 @@ bool FileKey::loadBinary(QIODevice* device)
QByteArray data;
if (!Tools::readAllFromDevice(device, data) || data.size() != 32) {
return false;
- } else {
- std::memcpy(m_key, data.data(), std::min(SHA256_SIZE, data.size()));
- sodium_memzero(data.data(), static_cast<std::size_t>(data.capacity()));
- return true;
}
+
+ std::memcpy(m_key, data.data(), std::min(SHA256_SIZE, data.size()));
+ sodium_memzero(data.data(), static_cast<std::size_t>(data.capacity()));
+ m_type = FixedBinary;
+ return true;
}
/**
@@ -354,6 +411,7 @@ bool FileKey::loadHex(QIODevice* device)
std::memcpy(m_key, key.data(), std::min(SHA256_SIZE, key.size()));
sodium_memzero(key.data(), static_cast<std::size_t>(key.capacity()));
+ m_type = FixedBinaryHex;
return true;
}
@@ -379,6 +437,7 @@ bool FileKey::loadHashed(QIODevice* device)
std::memcpy(m_key, result.data(), std::min(SHA256_SIZE, result.size()));
sodium_memzero(result.data(), static_cast<std::size_t>(result.capacity()));
+ m_type = Hashed;
return true;
}
diff --git a/src/keys/FileKey.h b/src/keys/FileKey.h
index 290a04af0..540dde7d2 100644
--- a/src/keys/FileKey.h
+++ b/src/keys/FileKey.h
@@ -35,25 +35,25 @@ public:
None,
Hashed,
KeePass2XML,
+ KeePass2XMLv2,
FixedBinary,
FixedBinaryHex
};
FileKey();
~FileKey() override;
- bool load(QIODevice* device);
+ bool load(QIODevice* device, QString* errorMsg = nullptr);
bool load(const QString& fileName, QString* errorMsg = nullptr);
QByteArray rawKey() const override;
Type type() const;
- static void create(QIODevice* device, int size = 128);
- static bool create(const QString& fileName, QString* errorMsg = nullptr, int size = 128);
+ static void createRandom(QIODevice* device, int size = 128);
+ static void createXMLv2(QIODevice* device, int size = 32);
+ static bool create(const QString& fileName, QString* errorMsg = nullptr);
private:
static constexpr int SHA256_SIZE = 32;
- bool loadXml(QIODevice* device);
- bool loadXmlMeta(QXmlStreamReader& xmlReader);
- QByteArray loadXmlKey(QXmlStreamReader& xmlReader);
+ bool loadXml(QIODevice* device, QString* errorMsg = nullptr);
bool loadBinary(QIODevice* device);
bool loadHex(QIODevice* device);
bool loadHashed(QIODevice* device);
diff --git a/tests/TestKeys.cpp b/tests/TestKeys.cpp
index d25a2bca8..cbbaae398 100644
--- a/tests/TestKeys.cpp
+++ b/tests/TestKeys.cpp
@@ -69,22 +69,36 @@ void TestKeys::testComposite()
void TestKeys::testFileKey()
{
QFETCH(FileKey::Type, type);
- QFETCH(QString, typeString);
+ QFETCH(QString, keyExt);
+ QFETCH(bool, fileKeyOk);
- QString name = QString("FileKey").append(typeString);
+ QString name = QString("FileKey").append(QTest::currentDataTag());
KeePass2Reader reader;
QString dbFilename = QString("%1/%2.kdbx").arg(QString(KEEPASSX_TEST_DATA_DIR), name);
- QString keyFilename = QString("%1/%2.key").arg(QString(KEEPASSX_TEST_DATA_DIR), name);
+ QString keyFilename = QString("%1/%2.%3").arg(QString(KEEPASSX_TEST_DATA_DIR), name, keyExt);
auto compositeKey = QSharedPointer<CompositeKey>::create();
auto fileKey = QSharedPointer<FileKey>::create();
- QVERIFY(fileKey->load(keyFilename));
- QCOMPARE(fileKey->rawKey().size(), 32);
-
+ QString error;
+ QVERIFY(fileKey->load(keyFilename, &error) == fileKeyOk);
+ QVERIFY(error.isEmpty() == fileKeyOk);
QCOMPARE(fileKey->type(), type);
+ // Test for same behaviour on code path without error parameter
+ auto fileKeyNoErrorParam = QSharedPointer<FileKey>::create();
+ QVERIFY(fileKeyNoErrorParam->load(keyFilename) == fileKeyOk);
+ QCOMPARE(fileKeyNoErrorParam->type(), type);
+
+ QCOMPARE(fileKey->rawKey(), fileKeyNoErrorParam->rawKey());
+
+ if (!fileKeyOk) {
+ return;
+ }
+
+ QCOMPARE(fileKey->rawKey().size(), 32);
+
compositeKey->addKey(fileKey);
auto db = QSharedPointer<Database>::create();
@@ -97,12 +111,16 @@ void TestKeys::testFileKey()
void TestKeys::testFileKey_data()
{
QTest::addColumn<FileKey::Type>("type");
- QTest::addColumn<QString>("typeString");
- QTest::newRow("Xml") << FileKey::KeePass2XML << QString("Xml");
- QTest::newRow("XmlBrokenBase64") << FileKey::Hashed << QString("XmlBrokenBase64");
- QTest::newRow("Binary") << FileKey::FixedBinary << QString("Binary");
- QTest::newRow("Hex") << FileKey::FixedBinaryHex << QString("Hex");
- QTest::newRow("Hashed") << FileKey::Hashed << QString("Hashed");
+ QTest::addColumn<QString>("keyExt");
+ QTest::addColumn<bool>("fileKeyOk");
+ QTest::newRow("Xml") << FileKey::KeePass2XML << QString("key") << true;
+ QTest::newRow("XmlBrokenBase64") << FileKey::KeePass2XML << QString("key") << false;
+ QTest::newRow("XmlV2") << FileKey::KeePass2XMLv2 << QString("keyx") << true;
+ QTest::newRow("XmlV2HashFail") << FileKey::KeePass2XMLv2 << QString("keyx") << false;
+ QTest::newRow("XmlV2BrokenHex") << FileKey::KeePass2XMLv2 << QString("keyx") << false;
+ QTest::newRow("Binary") << FileKey::FixedBinary << QString("key") << true;
+ QTest::newRow("Hex") << FileKey::FixedBinaryHex << QString("key") << true;
+ QTest::newRow("Hashed") << FileKey::Hashed << QString("key") << true;
}
// clang-format on
@@ -111,12 +129,12 @@ void TestKeys::testCreateFileKey()
QBuffer keyBuffer1;
keyBuffer1.open(QBuffer::ReadWrite);
- FileKey::create(&keyBuffer1, 128);
+ FileKey::createRandom(&keyBuffer1, 128);
QCOMPARE(keyBuffer1.size(), 128);
QBuffer keyBuffer2;
keyBuffer2.open(QBuffer::ReadWrite);
- FileKey::create(&keyBuffer2, 64);
+ FileKey::createRandom(&keyBuffer2, 64);
QCOMPARE(keyBuffer2.size(), 64);
}
@@ -127,7 +145,7 @@ void TestKeys::testCreateAndOpenFileKey()
QBuffer keyBuffer;
keyBuffer.open(QBuffer::ReadWrite);
- FileKey::create(&keyBuffer);
+ FileKey::createRandom(&keyBuffer);
keyBuffer.reset();
auto fileKey = QSharedPointer<FileKey>::create();
@@ -166,7 +184,7 @@ void TestKeys::testFileKeyHash()
QBuffer keyBuffer;
keyBuffer.open(QBuffer::ReadWrite);
- FileKey::create(&keyBuffer);
+ FileKey::createRandom(&keyBuffer);
CryptoHash cryptoHash(CryptoHash::Sha256);
cryptoHash.addData(keyBuffer.data());
diff --git a/tests/data/FileKeyXmlV2.kdbx b/tests/data/FileKeyXmlV2.kdbx
new file mode 100644
index 000000000..4b13c2ec1
--- /dev/null
+++ b/tests/data/FileKeyXmlV2.kdbx
Binary files differ
diff --git a/tests/data/FileKeyXmlV2.keyx b/tests/data/FileKeyXmlV2.keyx
new file mode 100644
index 000000000..ef38d4a89
--- /dev/null
+++ b/tests/data/FileKeyXmlV2.keyx
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<KeyFile xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <Meta>
+ <Version>2.0</Version>
+ </Meta>
+ <Key>
+ <Data Hash="FE2949B8">
+ A7007945 D07D54BA 28DF6434 1B4500FC
+ 9750DFB1 D36ADA2D 9C32DC19 4C7AB01B
+ </Data>
+ </Key>
+</KeyFile> \ No newline at end of file
diff --git a/tests/data/FileKeyXmlV2BrokenHex.kdbx b/tests/data/FileKeyXmlV2BrokenHex.kdbx
new file mode 100644
index 000000000..5a7e7355e
--- /dev/null
+++ b/tests/data/FileKeyXmlV2BrokenHex.kdbx
Binary files differ
diff --git a/tests/data/FileKeyXmlV2BrokenHex.keyx b/tests/data/FileKeyXmlV2BrokenHex.keyx
new file mode 100644
index 000000000..a52803832
--- /dev/null
+++ b/tests/data/FileKeyXmlV2BrokenHex.keyx
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<KeyFile xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <Meta>
+ <Version>2.0</Version>
+ </Meta>
+ <Key>
+ <Data Hash="FE2949B8">
+ X7007945 D07D54BA 28DF6434 1B4500FC
+ 9750DFB1 D36ADA2D 9C32DC19 4C7AB01B
+ </Data>
+ </Key>
+</KeyFile> \ No newline at end of file
diff --git a/tests/data/FileKeyXmlV2HashFail.kdbx b/tests/data/FileKeyXmlV2HashFail.kdbx
new file mode 100644
index 000000000..9044b1288
--- /dev/null
+++ b/tests/data/FileKeyXmlV2HashFail.kdbx
Binary files differ
diff --git a/tests/data/FileKeyXmlV2HashFail.keyx b/tests/data/FileKeyXmlV2HashFail.keyx
new file mode 100644
index 000000000..a52e96f18
--- /dev/null
+++ b/tests/data/FileKeyXmlV2HashFail.keyx
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<KeyFile xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <Meta>
+ <Version>2.0</Version>
+ </Meta>
+ <Key>
+ <Data Hash="FE2949B9">
+ A7007945 D07D54BA 28DF6434 1B4500FC
+ 9750DFB1 D36ADA2D 9C32DC19 4C7AB01B
+ </Data>
+ </Key>
+</KeyFile> \ No newline at end of file