From 18d3fe55f883d000b499804e22590f0c86399a63 Mon Sep 17 00:00:00 2001 From: Felix Geyer Date: Tue, 25 Sep 2012 22:33:36 +0200 Subject: [PATCH] Add support for database format 3.01 (HeaderHash). Add test for the format 3.00 and upgrade Compressed.kdbx, NonAscii.kdbx and ProtectedStrings.kdbx to 3.01. Add a test for an incorrect HeaderHash. --- src/CMakeLists.txt | 2 ++ src/format/KeePass2.h | 2 +- src/format/KeePass2Reader.cpp | 32 ++++++++++++++++----- src/format/KeePass2Reader.h | 1 + src/format/KeePass2Writer.cpp | 13 +++++++-- src/format/KeePass2XmlReader.cpp | 9 ++++++ src/format/KeePass2XmlReader.h | 2 ++ src/format/KeePass2XmlWriter.cpp | 11 +++++-- src/format/KeePass2XmlWriter.h | 6 ++-- src/streams/StoreDataStream.cpp | 48 +++++++++++++++++++++++++++++++ src/streams/StoreDataStream.h | 39 +++++++++++++++++++++++++ tests/TestKeePass2Reader.cpp | 34 ++++++++++++++++++++++ tests/TestKeePass2Reader.h | 2 ++ tests/data/BrokenHeaderHash.kdbx | Bin 0 -> 1982 bytes tests/data/Compressed.kdbx | Bin 1918 -> 1982 bytes tests/data/Format300.kdbx | Bin 0 -> 2014 bytes tests/data/NonAscii.kdbx | Bin 2798 -> 2862 bytes tests/data/ProtectedStrings.kdbx | Bin 1934 -> 1998 bytes 18 files changed, 185 insertions(+), 16 deletions(-) create mode 100644 src/streams/StoreDataStream.cpp create mode 100644 src/streams/StoreDataStream.h create mode 100644 tests/data/BrokenHeaderHash.kdbx create mode 100644 tests/data/Format300.kdbx diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d45464943..1f2558e71 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -100,6 +100,7 @@ set(keepassx_SOURCES streams/HashedBlockStream.cpp streams/LayeredStream.cpp streams/qtiocompressor.cpp + streams/StoreDataStream.cpp streams/SymmetricCipherStream.cpp ) @@ -151,6 +152,7 @@ set(keepassx_MOC streams/HashedBlockStream.h streams/LayeredStream.h streams/qtiocompressor.h + streams/StoreDataStream.h streams/SymmetricCipherStream.h ) diff --git a/src/format/KeePass2.h b/src/format/KeePass2.h index edb8ec5c9..d0002ddf5 100644 --- a/src/format/KeePass2.h +++ b/src/format/KeePass2.h @@ -26,7 +26,7 @@ namespace KeePass2 { const quint32 SIGNATURE_1 = 0x9AA2D903; const quint32 SIGNATURE_2 = 0xB54BFB67; - const quint32 FILE_VERSION = 0x00030000; + const quint32 FILE_VERSION = 0x00030001; const quint32 FILE_VERSION_MIN = 0x00020000; const quint32 FILE_VERSION_CRITICAL_MASK = 0xFFFF0000; diff --git a/src/format/KeePass2Reader.cpp b/src/format/KeePass2Reader.cpp index 49fb89f0c..f6818f4b2 100644 --- a/src/format/KeePass2Reader.cpp +++ b/src/format/KeePass2Reader.cpp @@ -29,6 +29,7 @@ #include "format/KeePass2XmlReader.h" #include "streams/HashedBlockStream.h" #include "streams/QtIOCompressor" +#include "streams/StoreDataStream.h" #include "streams/SymmetricCipherStream.h" KeePass2Reader::KeePass2Reader() @@ -45,21 +46,26 @@ Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& ke m_errorStr = QString(); m_headerEnd = false; + StoreDataStream headerStream(m_device); + headerStream.open(QIODevice::ReadOnly); + m_headerStream = &headerStream; + bool ok; - quint32 signature1 = Endian::readUInt32(m_device, KeePass2::BYTEORDER, &ok); + quint32 signature1 = Endian::readUInt32(m_headerStream, KeePass2::BYTEORDER, &ok); if (!ok || signature1 != KeePass2::SIGNATURE_1) { raiseError(tr("Not a KeePass database.")); return Q_NULLPTR; } - quint32 signature2 = Endian::readUInt32(m_device, KeePass2::BYTEORDER, &ok); + quint32 signature2 = Endian::readUInt32(m_headerStream, KeePass2::BYTEORDER, &ok); if (!ok || signature2 != KeePass2::SIGNATURE_2) { raiseError(tr("Not a KeePass database.")); return Q_NULLPTR; } - quint32 version = Endian::readUInt32(m_device, KeePass2::BYTEORDER, &ok) & KeePass2::FILE_VERSION_CRITICAL_MASK; + quint32 version = Endian::readUInt32(m_headerStream, KeePass2::BYTEORDER, &ok) + & KeePass2::FILE_VERSION_CRITICAL_MASK; quint32 maxVersion = KeePass2::FILE_VERSION & KeePass2::FILE_VERSION_CRITICAL_MASK; if (!ok || (version < KeePass2::FILE_VERSION_MIN) || (version > maxVersion)) { raiseError(tr("Unsupported KeePass database version.")); @@ -69,6 +75,8 @@ Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& ke while (readHeaderField() && !hasError()) { } + headerStream.close(); + // TODO: check if all header fields have been parsed m_db->setKey(key, m_transformSeed, false); @@ -78,7 +86,7 @@ Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& ke hash.addData(m_db->transformedMasterKey()); QByteArray finalKey = hash.result(); - SymmetricCipherStream cipherStream(device, SymmetricCipher::Aes256, SymmetricCipher::Cbc, + SymmetricCipherStream cipherStream(m_device, SymmetricCipher::Aes256, SymmetricCipher::Cbc, SymmetricCipher::Decrypt, finalKey, m_encryptionIV); cipherStream.open(QIODevice::ReadOnly); @@ -124,6 +132,16 @@ Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& ke return Q_NULLPTR; } + Q_ASSERT(version < 0x00030001 || !xmlReader.headerHash().isEmpty()); + + if (!xmlReader.headerHash().isEmpty()) { + QByteArray headerHash = CryptoHash::hash(headerStream.storedData(), CryptoHash::Sha256); + if (headerHash != xmlReader.headerHash()) { + raiseError(""); + return Q_NULLPTR; + } + } + return db.take(); } @@ -173,7 +191,7 @@ void KeePass2Reader::raiseError(const QString& str) bool KeePass2Reader::readHeaderField() { - QByteArray fieldIDArray = m_device->read(1); + QByteArray fieldIDArray = m_headerStream->read(1); if (fieldIDArray.size() != 1) { raiseError(""); return false; @@ -181,7 +199,7 @@ bool KeePass2Reader::readHeaderField() quint8 fieldID = fieldIDArray.at(0); bool ok; - quint16 fieldLen = Endian::readUInt16(m_device, KeePass2::BYTEORDER, &ok); + quint16 fieldLen = Endian::readUInt16(m_headerStream, KeePass2::BYTEORDER, &ok); if (!ok) { raiseError(""); return false; @@ -189,7 +207,7 @@ bool KeePass2Reader::readHeaderField() QByteArray fieldData; if (fieldLen != 0) { - fieldData = m_device->read(fieldLen); + fieldData = m_headerStream->read(fieldLen); if (fieldData.size() != fieldLen) { raiseError(""); return false; diff --git a/src/format/KeePass2Reader.h b/src/format/KeePass2Reader.h index 97a71b4c5..4b38de570 100644 --- a/src/format/KeePass2Reader.h +++ b/src/format/KeePass2Reader.h @@ -54,6 +54,7 @@ private: void setInnerRandomStreamID(const QByteArray& data); QIODevice* m_device; + QIODevice* m_headerStream; bool m_error; QString m_errorStr; bool m_headerEnd; diff --git a/src/format/KeePass2Writer.cpp b/src/format/KeePass2Writer.cpp index 027cc1945..91ad84159 100644 --- a/src/format/KeePass2Writer.cpp +++ b/src/format/KeePass2Writer.cpp @@ -17,6 +17,7 @@ #include "KeePass2Writer.h" +#include #include #include @@ -44,8 +45,6 @@ void KeePass2Writer::writeDatabase(QIODevice* device, Database* db) m_error = false; m_errorStr = QString(); - m_device = device; - QByteArray masterSeed = Random::randomArray(32); QByteArray encryptionIV = Random::randomArray(16); QByteArray protectedStreamKey = Random::randomArray(32); @@ -58,6 +57,9 @@ void KeePass2Writer::writeDatabase(QIODevice* device, Database* db) hash.addData(db->transformedMasterKey()); QByteArray finalKey = hash.result(); + QBuffer header; + header.open(QIODevice::WriteOnly); + m_device = &header; CHECK_RETURN(writeData(Endian::int32ToBytes(KeePass2::SIGNATURE_1, KeePass2::BYTEORDER))); CHECK_RETURN(writeData(Endian::int32ToBytes(KeePass2::SIGNATURE_2, KeePass2::BYTEORDER))); @@ -80,6 +82,11 @@ void KeePass2Writer::writeDatabase(QIODevice* device, Database* db) KeePass2::BYTEORDER))); CHECK_RETURN(writeHeaderField(KeePass2::EndOfHeader, endOfHeader)); + header.close(); + m_device = device; + QByteArray headerHash = CryptoHash::hash(header.data(), CryptoHash::Sha256); + CHECK_RETURN(writeData(header.data())); + SymmetricCipherStream cipherStream(device, SymmetricCipher::Aes256, SymmetricCipher::Cbc, SymmetricCipher::Encrypt, finalKey, encryptionIV); cipherStream.open(QIODevice::WriteOnly); @@ -104,7 +111,7 @@ void KeePass2Writer::writeDatabase(QIODevice* device, Database* db) KeePass2RandomStream randomStream(protectedStreamKey); KeePass2XmlWriter xmlWriter; - xmlWriter.writeDatabase(m_device, db, &randomStream); + xmlWriter.writeDatabase(m_device, db, &randomStream, headerHash); } bool KeePass2Writer::writeData(const QByteArray& data) diff --git a/src/format/KeePass2XmlReader.cpp b/src/format/KeePass2XmlReader.cpp index e7a4024c3..dbeb62291 100644 --- a/src/format/KeePass2XmlReader.cpp +++ b/src/format/KeePass2XmlReader.cpp @@ -45,6 +45,7 @@ void KeePass2XmlReader::readDatabase(QIODevice* device, Database* db, KeePass2Ra m_meta->setUpdateDatetime(false); m_randomStream = randomStream; + m_headerHash.clear(); m_tmpParent = new Group(); @@ -133,6 +134,11 @@ QString KeePass2XmlReader::errorString() .arg(m_xml.columnNumber()); } +QByteArray KeePass2XmlReader::headerHash() +{ + return m_headerHash; +} + void KeePass2XmlReader::parseKeePassFile() { Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "KeePassFile"); @@ -158,6 +164,9 @@ void KeePass2XmlReader::parseMeta() if (m_xml.name() == "Generator") { m_meta->setGenerator(readString()); } + else if (m_xml.name() == "HeaderHash") { + m_headerHash = readBinary(); + } else if (m_xml.name() == "DatabaseName") { m_meta->setName(readString()); } diff --git a/src/format/KeePass2XmlReader.h b/src/format/KeePass2XmlReader.h index 556bcdba2..032da4c69 100644 --- a/src/format/KeePass2XmlReader.h +++ b/src/format/KeePass2XmlReader.h @@ -46,6 +46,7 @@ public: Database* readDatabase(const QString& filename); bool hasError(); QString errorString(); + QByteArray headerHash(); private: void parseKeePassFile(); @@ -91,6 +92,7 @@ private: QHash m_entries; QHash m_binaryPool; QHash > m_binaryMap; + QByteArray m_headerHash; }; #endif // KEEPASSX_KEEPASS2XMLREADER_H diff --git a/src/format/KeePass2XmlWriter.cpp b/src/format/KeePass2XmlWriter.cpp index 8bd4649b8..d582476ce 100644 --- a/src/format/KeePass2XmlWriter.cpp +++ b/src/format/KeePass2XmlWriter.cpp @@ -34,11 +34,13 @@ KeePass2XmlWriter::KeePass2XmlWriter() m_xml.setCodec("UTF-8"); } -void KeePass2XmlWriter::writeDatabase(QIODevice* device, Database* db, KeePass2RandomStream* randomStream) +void KeePass2XmlWriter::writeDatabase(QIODevice* device, Database* db, KeePass2RandomStream* randomStream, + const QByteArray& headerHash) { m_db = db; m_meta = db->metadata(); m_randomStream = randomStream; + m_headerHash = headerHash; generateIdMap(); @@ -56,11 +58,11 @@ void KeePass2XmlWriter::writeDatabase(QIODevice* device, Database* db, KeePass2R m_xml.writeEndDocument(); } -void KeePass2XmlWriter::writeDatabase(const QString& filename, Database* db, KeePass2RandomStream* randomStream) +void KeePass2XmlWriter::writeDatabase(const QString& filename, Database* db) { QFile file(filename); file.open(QIODevice::WriteOnly|QIODevice::Truncate); - writeDatabase(&file, db, randomStream); + writeDatabase(&file, db); } void KeePass2XmlWriter::generateIdMap() @@ -83,6 +85,9 @@ void KeePass2XmlWriter::writeMetadata() m_xml.writeStartElement("Meta"); writeString("Generator", m_meta->generator()); + if (!m_headerHash.isEmpty()) { + writeBinary("HeaderHash", m_headerHash); + } writeString("DatabaseName", m_meta->name()); writeDateTime("DatabaseNameChanged", m_meta->nameChanged()); writeString("DatabaseDescription", m_meta->description()); diff --git a/src/format/KeePass2XmlWriter.h b/src/format/KeePass2XmlWriter.h index 7706c2625..78842cac4 100644 --- a/src/format/KeePass2XmlWriter.h +++ b/src/format/KeePass2XmlWriter.h @@ -36,8 +36,9 @@ class KeePass2XmlWriter { public: KeePass2XmlWriter(); - void writeDatabase(QIODevice* device, Database* db, KeePass2RandomStream* randomStream = Q_NULLPTR); - void writeDatabase(const QString& filename, Database* db, KeePass2RandomStream* randomStream = Q_NULLPTR); + void writeDatabase(QIODevice* device, Database* db, KeePass2RandomStream* randomStream = Q_NULLPTR, + const QByteArray& headerHash = QByteArray()); + void writeDatabase(const QString& filename, Database* db); bool error(); QString errorString(); @@ -77,6 +78,7 @@ private: Database* m_db; Metadata* m_meta; KeePass2RandomStream* m_randomStream; + QByteArray m_headerHash; QHash m_idMap; }; diff --git a/src/streams/StoreDataStream.cpp b/src/streams/StoreDataStream.cpp new file mode 100644 index 000000000..da94b851f --- /dev/null +++ b/src/streams/StoreDataStream.cpp @@ -0,0 +1,48 @@ +/* +* Copyright (C) 2012 Felix Geyer +* +* 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 . +*/ + +#include "StoreDataStream.h" + +StoreDataStream::StoreDataStream(QIODevice* baseDevice) + : LayeredStream(baseDevice) +{ +} + +bool StoreDataStream::open(QIODevice::OpenMode mode) +{ + bool result = LayeredStream::open(mode); + + if (result) { + m_storedData.clear(); + } + + return result; +} + +QByteArray StoreDataStream::storedData() const +{ + return m_storedData; +} + +qint64 StoreDataStream::readData(char* data, qint64 maxSize) +{ + qint64 bytesRead = LayeredStream::readData(data, maxSize); + + m_storedData.append(data, bytesRead); + + return bytesRead; +} diff --git a/src/streams/StoreDataStream.h b/src/streams/StoreDataStream.h new file mode 100644 index 000000000..414343854 --- /dev/null +++ b/src/streams/StoreDataStream.h @@ -0,0 +1,39 @@ +/* +* Copyright (C) 2012 Felix Geyer +* +* 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 . +*/ + +#ifndef KEEPASSX_STOREDATASTREAM_H +#define KEEPASSX_STOREDATASTREAM_H + +#include "streams/LayeredStream.h" + +class StoreDataStream : public LayeredStream +{ + Q_OBJECT + +public: + explicit StoreDataStream(QIODevice* baseDevice); + bool open(QIODevice::OpenMode mode) Q_DECL_OVERRIDE; + QByteArray storedData() const; + +protected: + qint64 readData(char* data, qint64 maxSize) Q_DECL_OVERRIDE; + +private: + QByteArray m_storedData; +}; + +#endif // KEEPASSX_STOREDATASTREAM_H diff --git a/tests/TestKeePass2Reader.cpp b/tests/TestKeePass2Reader.cpp index 153f2fb63..ee28dfc77 100644 --- a/tests/TestKeePass2Reader.cpp +++ b/tests/TestKeePass2Reader.cpp @@ -43,6 +43,7 @@ void TestKeePass2Reader::testNonAscii() QVERIFY(db); QVERIFY(!reader.hasError()); QCOMPARE(db->metadata()->name(), QString("NonAsciiTest")); + QCOMPARE(db->compressionAlgo(), Database::CompressionNone); delete db; } @@ -57,6 +58,7 @@ void TestKeePass2Reader::testCompressed() QVERIFY(db); QVERIFY(!reader.hasError()); QCOMPARE(db->metadata()->name(), QString("Compressed")); + QCOMPARE(db->compressionAlgo(), Database::CompressionGZip); delete db; } @@ -87,6 +89,22 @@ void TestKeePass2Reader::testProtectedStrings() delete db; } +void TestKeePass2Reader::testBrokenHeaderHash() +{ + // The protected stream key has been modified in the header. + // Make sure the database won't open. + + QString filename = QString(KEEPASSX_TEST_DATA_DIR).append("/BrokenHeaderHash.kdbx"); + CompositeKey key; + key.addKey(PasswordKey("")); + KeePass2Reader reader; + Database* db = reader.readDatabase(filename, key); + QVERIFY(!db); + QVERIFY(reader.hasError()); + + delete db; +} + void TestKeePass2Reader::testFormat200() { QString filename = QString(KEEPASSX_TEST_DATA_DIR).append("/Format200.kdbx"); @@ -121,4 +139,20 @@ void TestKeePass2Reader::testFormat200() delete db; } +void TestKeePass2Reader::testFormat300() +{ + QString filename = QString(KEEPASSX_TEST_DATA_DIR).append("/Format300.kdbx"); + CompositeKey key; + key.addKey(PasswordKey("a")); + KeePass2Reader reader; + Database* db = reader.readDatabase(filename, key); + QVERIFY(db); + QVERIFY(!reader.hasError()); + + QCOMPARE(db->rootGroup()->name(), QString("Format300")); + QCOMPARE(db->metadata()->name(), QString("Test Database Format 0x00030000")); + + delete db; +} + QTEST_GUILESS_MAIN(TestKeePass2Reader) diff --git a/tests/TestKeePass2Reader.h b/tests/TestKeePass2Reader.h index 27680c855..8de873f93 100644 --- a/tests/TestKeePass2Reader.h +++ b/tests/TestKeePass2Reader.h @@ -29,7 +29,9 @@ private Q_SLOTS: void testNonAscii(); void testCompressed(); void testProtectedStrings(); + void testBrokenHeaderHash(); void testFormat200(); + void testFormat300(); }; #endif // KEEPASSX_TESTKEEPASS2READER_H diff --git a/tests/data/BrokenHeaderHash.kdbx b/tests/data/BrokenHeaderHash.kdbx new file mode 100644 index 0000000000000000000000000000000000000000..6c4c43991479aab7c4fbcbd0ec7168edae72c8a4 GIT binary patch literal 1982 zcmV;v2SNA)*`k_f`%AR}00RI55CAd3^5(yBLr}h01tDtuTK@wC0096100bZa5H_T> zgKaT1uT=VP4!OqXjrIP-5bospGXul(2L*-I1t0(eA(&`*^N?;<6X4FWh!$E0Y(L|m zu+_2|X_I*f#^hE82mo*w00000000LN09q)^;)-|-*3#n3(CX|uQwSgcE1HwS5ZkQc zSw#69`z*pb!>0)rBnS9s%o<9d`8!pd2_OJzB}{G@4Y7(*wg`b6bmD`9{QO13+P}~X z#9@<-BYw6D1ONg60000401XNa3SD<8*H{FLuhqA{>Ogogde_Zc|8gQ%?V&Xz|Ag^u zUdbZH@-g6LQu+f?=jOW;N~G>)`Ab>F`w7!O8o3Cv2ZDk6_(mV#ESS zAf7B}%D>MoCL4%aII2a1Aa; ztkIV@5jiZhQ&8VX)+%gT=cZ1RC^5%EIF*7VZQ2T@HObb+KP9Uz5DxB)ZNMy@!>#bg zlK`<{U%4Nd*>D82JdP(ZHIorInGmDQ-R7oSAF!Ea498?jLNfYP;v2JmBbGDf%D5g% z>7*6qjtgVoMaD$Rp!rfPbd>*PX7N$zIQ?26%u+DINX^+!hHDRUF22% zOA=s6^^MGNfg1?l6E_U48*s+&<#{i*ev1qAeJP@?oZi}lRY}tPwU3#+3P#yno73mm zla0-VWF~XBkeGOaYsHC4$x=uH;n?jUYiFo>bs$=*$VlWrrHjA>Ae`*?nHHl&0BqRW zrQx_hl<`IAsC zoYG&P$+Dsr^4$S+-Ps02nt9CT0WY8BS0v_cvjxT5Vrx*9xX8y7}a> zhnlugKyv;GO(J&EdYH#NaCGQ*g44|yo$gnZ>Se$q&F7FvrJ~&10RaJAG+e!e)1g+y zGvZ(KQUScgY>8!{Cb)ASHp!YjSzR1@D^s#1VE#id(qb!Nk4r70#0 zEt0}2S)~5857J2SuyvE8gK(bNIf3b9$P-NBbKB`8t9up|g}l5-l>!>jYDz68(Yl7l zT+q{!yFp+PEz=q@P+^tIVx^j%Ww~ec9_5TIj$I#~%&*K$*L&-Mh|!~8)YJnSltZdz zRkmC56)X30t{s&hZc!FM8;>1}R+z4TwnGLwqLgMAr;FX}=u?~)d z+CBTK#Sv?YH;5{`%|m|*3+gC@o1KJZN?aLF8CJkB*LzoaDpHK=$j}*}D#4_T60PjB zQ0?T<*csZvyM*;y)uIPlp+GKJob)~ewr{AVtnBb24_mG!Q2(#GyYIw#Dt(`pdqMQj z#MAAdcr6NeG?7U+XrFqah#RuuW#RDbZx(&c95rCM%YEOZB2}j@UJ}kqR2C^8*?dH= z9M6016i^W0KZ z6Rm(|a{3F<(vW68w7@X=9qWp~niwyP1-a>q4k4`8{vO@D?Q^}i#n5MX(sK|1q%q;c QLO=%s2m2v%Emfv*CucFy%m4rY literal 0 HcmV?d00001 diff --git a/tests/data/Compressed.kdbx b/tests/data/Compressed.kdbx index af12d73aaac7ddcdc8b8e00dec5b15be57df659f..1f8ec2de6871a80cc65cf7e42e38efb5881a9ef3 100644 GIT binary patch delta 1974 zcmV;n2TAz;4!#cw1KFaQXZuUF0g(wHe-JjLwS#RjG_O?pZw|S}=Z*FL#1QV}`7;B< z^9Kcm)de5`10k4bc=M2MRukaPvWOO12W&s%ps>}l8EKPw2*%`A1_%If7XSbN00007 z5CB>z%i@Z74A#=(%h2lVI#UQB04ti3!Vuf6;#oxb9Q!Q7I>V<479T{!7eAH^e8B zE;PH>l;Ak)7FkbU7i%yLRi^btf2)W~b@qfnm~)rCHB(t%r!Y&9W5fN2_?+hT>k(Lj zVWv1{HSOE$;OrLZ@Ko2q$@_aJY@aWWVBKP3!~#em&?LH78qjZJ!|l}m`A;dnn55LM z7@+Y_lk?qkCw47e(qTRh zce}+H`TGfV4yS8}N0lh~`6WuJhekRfY+C20 zPLn7x$3i%jf+TI)3Z*s4*2O<1t1S=??u>1~ES;;9!lw?73Gc#W8X!_MEG#5sap)l zgop{eAqmQi()=Rje~tKgBWI`KpS7Jvrle!~5wHL}>7|baP+kzrB*|=B z<);l*=Ff6ZC=+E){{Kk6cR}A35_%Rm+?hhceE8yBGl5@1O6jm&X@8wlSMHw>*C zaK`WDc`vqpiwpF9DWa{M-r9pzNz(kakD0s*M%i4O)92Wee~rzBWF~XBkeGOaYsHC4 z$x=uH;n?jUYiFo>bs$=*$VlWrrHjA>Ae`*?nHHl&0BqRWrQx_hl<KY^A8a( z_0&spGuP|#u=@ZQHxDLe1Lqk|P%QU1VX9hfUuoA0rYgGmd>oU-MD{yu@sYWuPXw zb00Rzf0{j6&Aqiu34;&C9#LIX6W?5_Qh+d+ULSf6=2~)YJnSltZdzRkmC56)X30t{s&h zZc!FM8;>1}R+z4TwnGLwqLgMAr;FX}=u?~)d+CBTK#Sv?YH;5{` zf6YUG3JdBegqxj&WlCHbPZ?IgFxPumc`8zj>&Va~9u*%^Wpgxyyauf21N+r!HO+&Pr4kDIeKJR70{(mTb?fL^ z)=UWOYSfQB9CbznkCJK6$3iFDIca;0*&5T3-iSOrrmY0y&lIoY^CA>4`K_d(e@5Q8 z=zLv`=_kHmQwMzLJiG=8=ppYEu3)080S*e@EU~l5dd8QAn>iBK%;`NP2mne6V?H<1 zWG58oplb8=yX#P|HY9oBHPpx|2ZcT>j-*Wm)ts*9&5h-x`f#S|hQvY>t$=27`U}v~ zkY+x#z%cn8>x#gd7%z+kx#^1zBq6NU{vO@D?Q^}i#n5MX(sK|1q%q;cLO=%s2m2v% IEmfv*Cl-d&GXMYp delta 1909 zcmV-*2a5Q<5B?4b1KFaQXZuUF0FenGe`~aAz!|dn9M5s6L087V)hs^Bhn0xxB4+5f*LxjrBM*>?zk#5=3^;?G`MhD)i6NkW(#WR6wte&bGFZzQQ6CSrf zfbdM$YMK$f4G$D&%W<&WO=4xfe^^C1Y`2qao1ZxwYt;|;p^ZkS2%ZqtSN3*b+siWi z43ZIv$(lexw9c@$pbC73DpG;NEXT~HA94WgA(V*}4wdf?)?>{^(kJS&s zK157{IZD$=!l2qi-`eQoJuvqZBAHR$!6;kZ=&I3nO^6%Ag0;&ScU=u>#pee|e{q;hnOphU9=rTQ9Q&Hz9otS3RSH4$LPq z?hyUZaP{@bpAGnivL#LWfB3S{_Rn>Cl>tzCT@tl^X9$HcssP5Hi8&)MRs^O-#WPIE z;tYj2%F*LH4?yJR>A4}bT0l*_Y7fA2sgcSRrfsS?&f*%8^Ss&*?{91Pt~6sE(~%A5 zs_ee7%pO2Rvw!k;K?%PiH&|R3zpk;1*R|#{kQb_+ZLFZ**Aj-uf2zYog|mWOrzcUe z6-hEr|JP3O%3}W%#U`L!S;0{?<*(f2PD**|c;v?J4aUR1c?=upHSUty);!#qwPqWt zB>XY$6qY_acUMS%{=W;PW@K6LAdc@(7#XtEu{3aC7bYXP_DekDJFr|Pw1s(jZ9SNI z66324UZS?iv>=Q}e;%i34mjVK6c^iy!3Wc-LYskFRAG7zOQn^lZWf4NI)7C8Hf7(Y zOO$bEcs-(~Go*zQk=ncU(AA3r?OmlMwvCdPDF2B~7fps%Hcv}AOp`nNvrBbo~kd78o)J^8#T1K`2vNDoe{H~7bmSO;rJJYB)r)O`b_<1X!B0^<9 zr(9gyG*E>%ZNWxOk6$1n3tT?&6bhr&1!MZaq2gdw*e=rKn=OYcvduH{K5z(jP< z&gQaQ5M&R%@CF7;#FN$}k*okU*nAsd7-4vk`|=tKf2T=H?UoO~euo_#=rk`)n*`|h zwn&4nrdity-au)@nYx;XRQRuL*~B$YNcXbIMO{;`he01kf)R`8gW(Cr@ zWg!Tsf7Iop{-+vJGdk+CMGsB`Ke4UJnM2xTiE)EC6Ww$YuC$WX18MK8Rs0tZ)l*jY zwWog!L3!e`wH`Je)_FI};Y2aU^rUv?axz3Y3uebI7P@?K|GUXu!zpgJ^*6*tNa!0? z1yi;+(tV?&YbrK-d!EOlouUS>diX9?73&Uqe`Qa++4|MFQvj)A?Ij635-rVX|McE;JWHpg@@?3*W0_^?FY>Tk%uGgc)>Z+a{+;eP%y@5GaSX=73*}JMp ze}30fmAif$%*w$cr(_UK18y#rtV$-nG2F^=+XqC%vG-JFuT%;SgCqfxqQ?kj}jJv#Lm$ z7#XJLR3<{2Ews`+jnt(_){0lsi223`f8$zeyQ7OFdSQRENBrC6*hs(BrwhPCRK-DM z`HxB*_gYDBYya^d;Ncgq0eJk9zul9b5}IBW07`)s8?Bg)P6}_NFb*S0Ok3@idnrl` z!N^pmmR2TQ1e8+AFc|4*M>?{Meymz#JP#mz7s$u_y*!9Sxt8}(J9E~$ZUc>4V}o4O zAIc(m+vkVH(DdABxU}fyJiZGIcp;eKI0@`?`06~|pERfK+x&JK@6~RSkP#&<3ikVv va|>sM4P0Te3Cvfe=@zRCrlNSG=XUN!+!Cv(#uYg;-*K|_g%_u5ooGN#l%u32 diff --git a/tests/data/Format300.kdbx b/tests/data/Format300.kdbx new file mode 100644 index 0000000000000000000000000000000000000000..dc67f35a11ec8caf49583798280aa883657436e2 GIT binary patch literal 2014 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 LrN#yrbBFyIkOFb} delta 2796 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 diff --git a/tests/data/ProtectedStrings.kdbx b/tests/data/ProtectedStrings.kdbx index 2614097fc2bb8f60086ad3fae1e4daabcbf39296..bb50c03fbfe9afbc99e889c1abc87cec00ed1982 100644 GIT binary patch delta 1990 zcmV;%2RZnT56%w>1KFaQXZuUF0g(wHf9)DPG0D))6ol2N9V@Ce(Uf>U@$JY~(h%9y zBKq8jn*|^Md8yaRZC5x#>AUnJR3mLV_-iqscJM`|#MQOS-=&E~1_%If7XSbN00007 z5C9K1dG02Ujilkq&swH!^Qj0R03}V^TICW{^RE~5hU(l61M#Tv%!{j^6GEzJf5xLI zHwy_M07N6-uq@y%zRJQ*-g%q7@Th#c<|;1mu9}duKEnr5`3eL80ssI200aOH3JnU1 z_^{~v`7~TiGu=?>I{c8cmC=99AW)ogoV9X#|)Hdz+wx{>7{4MEf`ZDZC86R z!VwM&@!AcFM`yvxWs9V7!8b6$xXcKA*AIjV{B|D!q$!gXa#0pO2#Z5Bnr^i|mpA3kv?Fj_vb9YWlbQJ~ zMUx9eQ0txv!zSgzaV3=_9+V=6X4%}}vMAkfwCIH%Xmedzc-U-4{Z!a*JP}XANdUo1US>QhK3zBLWn%ns{pOLC2`xay~8(k2W?1Tp_ z2Vs?(w`E`GEEh;tKP{|<>+XU)EX?E32my+oxRJf@4$^wr$|M_>e>IHFwNT$^8~CvK zp{{&u`Br@Juxrilhi}Xi8t}Zd%$9hgqa;LQ2v-Sz^qDQM!5glrGN1bh*Yy8d+}SZo z5DdrlT3QLoxN^(OA#`vz?o;wiVnC=jYb`jD%uzd|%1OOO04boE+Y|$!Y05%(mF|UR zS3K(&)l7#8Y4s_%e*nKUSu>vMf56-s1zT%9ez1W&t35M)e|3pwsPhHv= zh)K#r{L3sdca{NUO(OUut70u{T@YDov8T*ez{X!2wFq@b%7c87G0aR)w(?4#@X)yp z{K@go-VW7RQ41u1qO>kRS6_x!(nj_JctCewZ;dxPbonazf8DvY5gUWI?qWjKz&$VV zB~GxY4_b*XyXdZfk6r)Jn3(Pr%p#B-%?**~X0O`Hn^n)2-JWV{J=}kGw*nj*lL2}^ zv=U_}%3DW<3&h-|J6E^lhfV9S*dq4Lm8{zF}V=izO47lO2V@QLFymjnI%y$gOU? zDa;E)4p^n0rmb}W_y<|jcoa*>MDdp_W|_|fyLUT{|YwH5`y_36XBc zmn8ov=DU!SiiWb@MQI$rx2(xT1hCeRALN-=e`Tmy%HHAtF0})2Q2LDNN3_h1Uv1WP zO0G70y93xNje$yn4x7OXdfKJ5GM{&AN`M&ebKy=V5T8_Cz5+I@fKdUIAfc>=Y$K+Y z*i1bw^3uP2-YHz>AdIWu+cOC?G)eex_};7t z)j+|YCOR}04*GsjYQvvMZjzRwPJ@bx zR=Mr<2p|_(%U!F^U4uB(_V*ATSzvHU9oNCa2rZ|NT03To)^IfzGb)ccB^Hr|e~1LV zmeK$2tkI4Z@1HozTwZyYx08fqN!6&F{51)@X0z$R?pgi8whD&4ENxA{{fN|_zo)Oa4N%8+?2q=5M5bas_3>9SxswQ1B7Xs189g9GfN5-16g?H%m1}mI5^?tGGU&E{`LBWG)1TG_QAoSi}A<G&=vDRx<0%T>(RsaA1 delta 1925 zcmV;02YUF<4~`EB1KFaQXZuUF0FenGe{VSkjS_kqGQ-hnur$(!=cjIQQXdXv z2k}pHgase~-~^Qsb}Yl>(fy}*0qBU7>H_$2gEOfQMNcWa=UDM+1_%If7XSbN00007 z5CGR&*?B!fcVBB+@ho_l)A0x(0EbKcd#@P(^+4{r;k9cAB|g_mFpSd?|8Bf?fBC*f zH|+@^0I@IEqmpiT0TB42`lI3FU@C5n(O{SRm@N;ZkEv|9MG6D}0ssI200aOH3JnT1 z0=*-{8Sy>=a0RYj+64uO35D*OMr@}3Kypv05yN$Rvd&+M`#<|&X_WTO+lLL}>U*sd z52M0km`~5z$XgQxWKMIf_ol_;fB!MNiJUHE0I3rlSRe!J;@NRjf;`v>oUykJ>6x$b zv?PF#l4i@Ts21o!1ZX8zntIa3*qNVC12yt7F~WCkhN@bz<|U-QFHUpit~ak^YeI9G z+T`x(_c%dQbt|E|qSRrk^)NUl1`9cbsG*{WQ+4`q_Wbh5&>%SaXswAHfADca*&%HX zry$`K=g!HWf1Fvqxas=CC6Wsjp?<%l`U+LB3guQ!)Pgf}YKzuMgYihk{ERG=M6p*( zM=zg+IP9glWJ%<~uGb)gFX5#sMHw)K3qA<~zOlEk-=xcG$Q@^a+AQ9GK|7jW5UAGy zKOakmHop2Qh-id8@S(L2f3}*K7PY{zunHr(6i!Jp^dQ2{1c?=?Ca5C|cCz@Nl47XH zR!pCj3<_c?t8(7&^SKq+BjC8mGXR6i`_;H~XCd6jh5|y5O5x(EqNKe_Dww0r%-jkB)P5 zK7(0{kd9rB+!q6+-@+4S+ftZT|Ukt{W6GE6DM+>M7juwH02}1fbL>g z8$VyjH;yN!%}v;PcW+&Zf6XoNXhnHClQjeTNz3hJe(;7)pn$}AXU@rXMZrg#Mu)5Ug3X@2 zFr?;Zf)Un|>V;qY?XrzvcwZkEeT#V>2m5fUx!E`Mbd9xi*oZ+ea3Oxp0T&2I>O{>P zPq0&$`l-TZ`^nMcNR^dM?id#80UKQk(Uv;+a}KtF8Ff;{f9LvaA!ZaiNmJCfB(K=r zq>Jkyod<(jYLbld-fwJETg}40YEA1i?#=&udYe}->)A1jn-RF8WgtXGY`zjxe}a)- z8P`sjlgOg=WQ7qfJqvRGZ~$f0a$CD4QJlU~GI9Z*ZL8XU{(Qh%G>YKxPk4+bEqL#? z-&cX-U*_kFe|au_kdAGlctfeyu32+y-wRO%sku?rBDxh>U(gA8HXdQBy|C`|l_A4{ zgBCA6=14kN?>t5kEa3K7R+$a5h0|Nq3e`9|JyTi;5n|5ZdTt(e9 zU6tTq#|r?Opn$n(Ip)Mv3R;jnaoT`J6|cwyhxBP266m-SOi%NE0;c+wf6Dme`ou@h z*g|~fDoTU0w_WDS?E=(vUweNdZL=jp%kR$|*H7vz5&g>J;tL#*7(qR8c^7%lW_wL#NtPt(>8$Na#*h3(pm z(u^oFo5I9#!ao8Q>v|l-Xl3&VP9#yG%HBiP+ds{7aiM2 zIqQq?cnO@`+B*+U;t0AAf8`Xd9d1M?+`nB~TxpcIGf#}NBJA0PV4`RtucbCL(+D*; z8D(Ie(23Og$sJFf7^NNXytK%tY|D#`lk$~$48j8zs(!KW}`3e zQe%5X9{bELw&tBkhbD6%D{lCV4_Uh{dp>1wiqz3)7Qg_cZE5Cq_80TU=YWO%?#x50EvS&78GDM@nTF=tF704LbMmaL zqx2hAatmE4?2A`dlt!K?J4$>pKKs&`BoJp0emtUS=re@A47;Ry#B!GPxYEUEKfwvJ z7j}W^&<^B`+oNTzv*QSmy2cz{bUvMq3NZV4E{FHys6M-dl;kk0*5PtSQ;H!!SiO{m L+icl**7#!ysf(yz