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 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 diff --git a/tests/data/Format300.kdbx b/tests/data/Format300.kdbx new file mode 100644 index 0000000000000000000000000000000000000000..dc67f35a11ec8caf49583798280aa883657436e2 GIT binary patch literal 2014 zcmZR+xoB4UZ||)P49pBn0t|)+KRw%D=p3*wf>kl=Pt<>A76wKJ1_l-dhNu~DU(Yc9 zIA6Emsd%2Rkd5oAGuZ;`4?QkWs$?xrD_3AuV0iFGQs^aWE8!Gk^iR07K8Q6H-R(mH{)4t-i+J^JFcD0>d*d3#Ln3GFe&o z2>sXQD{*?=!N`=$6smgd`P}np^B@5 zLSE0;-o-ghCwsq{#Y~+Sy~q@T2navZl~W4T{T1L z?`I!QHqdarR~p^&aeChFiHy1D7iI~+n`n5u#8Z1!@XUF|az|zzKe#RE-Kx`1_jh}8 zG)Q@99W!*=^73o&dIim|im%Up(4TZ+A?x%HyMDz6oU~fa^`PB4+tlc-}o1^e$eow3cW&zxI%_1u$X9Hn)uvuze{@D?%X zTU)b1?grDOmF4>kC(r0?HchCU8+PWhTSoTm>vuYq1e#B~Ae7KFHMCLv`Qm<&pd7`u ze~g^=ht`Dcy|Jj>O7)`gx=G%lADk8Lon*OYsD0Z#Y_jJ83D>uLC22Nmf-V|cE%KV; z(Ei0P)oOQJVcqJrR+mq-^Q*ULZ#~4}FH@m@BGD+6L= zKWdQE%RBx_eafN$x4ByDbDrv+@u*xp$FDDR(vgyC;mz5nV|MW=%({AhyHB zXP3^5Q{gy0F>A^1)k4xUzTJOSRd;Qc%MVT+4O8iYC)Jy3SNlaPSYHyjt7N@fP_$C& zBh&j3XYX&*dIZz{7SCpzTDs4gflW?+)&Yr2hxYU=+cAS7w7%%idC8jVH^5I zJU$n$=uS>N-K@rJeI@NyS-|#}xyl+}T6nLqtTf?X_H=XNg-B=ayFMOmduGnD{#;h& zz9aj2HdFV`S65H(mdo3-=YIK}NGs9CeACxRQq$YZW>7#(b3~ryL zG9pU3D`z*olsFhtz5nyI3&F7gTi<^^t*hI4i%;kCLMCQ~KbZ;%U+;ap**f7s!%QDG zA=|$dtY;(d>BW7Y!r)hWdT(00!+gh``d4%&KQQL^=t}^@Q!HUnX zZk0+`EL!q!`k#4TNld=5Ud6OM&x@6wq$?U3^1(~~yxpP2ES+j7#=g=|+Nbq}3S`Lb^& zkK^w7n|_Ntn`PkRvo>JLqT=Mcisi1oPPU9rn=gO6qJ`X@vZ%MnCVU0Pv^3UPakF&*dNJ$t0<)?vNB}* zid&2)>;9*e7&BPEJ;s0SljV{`q2{ibs?YLzY;($a7BKFgS@z;RfBfeXb#03|ynB_s z+w@B>cAt#hb^Oo$siDpWwINGiH~iy&w`_*a8aFj>mnpliIEM(yPBn5}ImPwf^qpml zEg7F)hb&xG|8>s-PX(*awu@Pzrq!LrC7ZrT8;E3o()ro3sh@xM%hvyv3Fp-}eVg&b zWsQ;anWHh+w^||vq=AY%WPscuzW^iF{^V;6_FmhXj zSK|$>&~)@lM{ty@BWd0Z&?`Ibv(7v;{L9p z>N8W%?)+Kpe0V8WU!#b_p*_8uKiF>LT3P*EOmwa5#MP|#pZz^&^l3iN!DUX{{Ex1i sV199y&&ABWZWjbnCw};PW6!2O+Yq4#m3;qxJ#Ku{Z~W)dFa4?F0D!O05&!@I literal 0 HcmV?d00001 diff --git a/tests/data/NonAscii.kdbx b/tests/data/NonAscii.kdbx index 285831f8eff0a5c0eb6c2a141b4eedb19b4f57fb..06aa5bf2c8bba3faac9c998b2a0e39530be1c083 100644 GIT binary patch delta 2861 zcmaDSx=xIf`R1Zo>A$_VGEU@FsONt1qo-Qjj6bDJGCbNtY!}Pj|L^wtDKR-3e(9bz z|FAEs0z=N46>ArSp8j(0obsAuGkH`)+r8%QmEl-nY$mpl{eajlHV%dYaRxA87hu@3 zt|Ry4sp37MZJcpUm%qkxC@@4ViT%x;&|zU~$2;Za?{oWBvz@PccSw#Wyxy!n@SBv3 z4yOXcgn$fjrHt8en{(4{TW$(HJ+I1*N0`I%@-y3AM{6|~gmAGiFoCRKVc_NB#l^@Tpb>1T+zpOR;nOCXp zK~c5Lzhw_T?OAlBv541^`S8_G=m(dw4|OAW(P0(zrc>^4l3tXIy{EuG=nX zU%ULBM)&;W$LE7e#WSX#nf~)Qe^}O*zK$JhM3y&gRf`DP&%TjEd^fkE<>Zpyj}`Gx zH9!1gtY7xPdwXAua-b8#+L!mf2tSMzzBMz@Fe6S#^G)mq(T~%m(;gq*cy_9bI8W3E zqxvcKW(iB8?o`<=+w|n!ar4ho8?N*`3)g8oD4JGq=Dmhf#UagIzm9M=CjXb^%3k3p zAy{V2qVXrJ?&1ZN3!gg{>e{~YyC@y;ah;9%)@5>Ol3xVcHIHO|D7tZO(^s$Cdd$mD z%+bvI=d@};%i*c+^HOpP-RJ3YnOE7DvzmxhZl9@Me>Ci6_L=NBiA!6UwyfuKu4GvH z)D+STdj z{vlGF`BioCvzs5T!AoHLtc9Lbct@HUu z^X4z{JQ*Fe^hu3=aPU9dV^c!%^ldC74ynbilvuY-;b!u09zoMjo;vj%0{2e)yq|L` z<)fqO?MRQt68X7v6#E`1UNDPsaA;+XI6d*D^Zug49j+VIwJcMUC9+H#4_vnKFcg@W zxNh>HPZ{RFpPp=SbXr?HMY%#DIGu&FM7e#f*4b}M9~HJnexH|I_4|a;42!uB{uX{^ zO0aF{-lAvxdwZjhPWC0=!g|#c^`CaX?mg`}js0pz_x+PrMZXf3e%Q_@R2KKzt1-I3 zhiAUz7m1GzX;WAK`F`|X&zxPe-o+(0^0?jFx@G6n3za_#ct5%(?kb;?p!JGHzW12# z%Db(b9d7p~EULU&^KowTEuQ*wEP<~?d{$igGMQ;(Jcrj$o|51Dj;}a4cj5b|?Qv!s z>jR~u+WxVW-Mw{iQcPRG+~*dux;Hv&QsN`e_|$}YOk5oO`+br1{+=2C3OVIgeA=_4 ztlUcahw^4qH=Bu^k)J1Qd-JmLeB}y_Lq7gxUo+ovm(8_}4RVvWkYY|+qnxsw!&+;u z^A*c9+mkLVXJR>Xn(yC>@bwaVn{sZECC@U}hNJa)X(lOe6aKAwbK&otyz+M()AKg& z;ah97=Z=AbwXiVft;G|VU-|g`nkⅇ^S*%wYPS~u5Hgcr0!~FP7A1>&U!?med%@c ztjIlW)9gcD_L{Xl^>1{`tIIz4pnRtBV>8%N;RL` zmg+l?Ug)nq8eG3lG{nX&?+x1x$u}PPYt+mFUqW!2VOx?hjwFDm{a^Y>MOvAM~*4ox|=#3xN+Gdv?tZB^QoDV4XVc=GMZ z9sBpqGjkM=*{b^9Z3FkxAKTqm=l%J|aH!+a>g9qTMbhSIcdq@cSO2Oskn#0?^*>oB ze|c^0=QT(%u$I(1cj2jK*^m0@6zRw9uVQ-6B(Jqg^|4C#^y3mc#%VI=S;0+azK~OW zAAY31+4OC0f0W5`kAV9QHtZc5GoHpxVOr#?$Kj~AJSF1G*^?jFE{vXj|IEqen;-0- zs}@e#(zKuN1k;_Y$X(nXM;qGfGh=VvnPh#xQ^uK9@U)))%u|PEMy{x6n6iA5%7-6X z4IAvX?=t66XI{r5bA0y&xys*$72e-MHm}$&F1lFLPyYC|u+mKyuaBR%FJV3&w40-R zqg29zHb2Jga&0dbonN4q*-+NRJNemU1M{C})Fqzq^l#s-Z)|+s@XiL66ye8r(z5HX zCno*S-P2%Fm1(AzKV9Mei>mC%l@WX6^7=*mt+FM&%Pu_UeB`z4W7nBae63arA#=~% zN(p*#`_2wKzb3s%$^9!A9JzCvxqX7d7w^oYcEX&q7x^5TA@|Dg^Yc$9)~;}0IKAX_ zg64dw8^7(hF8;CWRz=Yb<^$aarYA-hbhEx_46Fa3@pY%+T&;Qkodnf{n$B?Me|juk zcXNZ@)p8z@O>fI;Pd|LLr&st)wBf{6T`vq$GTS=#1y`1SUb?sah}uQ%n}w&7BHKkR zy?=IvvL0dT)@7|+^H8$?L+uL=T@#^9fsnP+x=lKWK> zEx7W<Q_v6MkE&E+tN*xR8t~mX%-Zi0@>sS4T5R=dC3)C;~7TC9E ziOZBfpTsY}c*hu$(L8gB$gJxbtB>}IOy&BGaXw$4{ysQhN#?|NiGiUybb zk)*Z0HG4Q~!Hf6ue!K2yx_!o3vufO-b_08sA#yeK*`^bAz%GOpq z?YQ@owY%N&H=kdmxor=p=kxmg`a8bpcFxXmF=^7ewDC&iiKCyl$&`IF;9&S@7o>Q^ z<)qJr9pS&OHdWNxv3KR#q|N z&kCi5EBA#yUvPQ*nV3l?zZn`WRnZMcJ}8+Z=WZj7ygyF+-E!Gd)wyyhuPe-Oj4z*+m(0jp70~) z{P(^TrM;fzLfeF;uL*vcd{6hrshdG<*Nsz-?|5pXtWqHLE9G27?cB}B`y~&%>$4sxqb^io^mhi!;cNpcP@pthi^|NrTiankH6!XEtPxo!JiYAyDux8XP7W2 z?#OZzE1~tZkIsgd-Mn!?^4+oEmD-EXzZ2T4rI=JbGqF0>ygsV4?ox(o?}LO8!JqpQ zGYZyx;PKM!?-Vv&{r$wj0gc(&B?O7F)e4AW!{A$_VGEC%DsIQ8ApR>ebX<_kS{uZ+%j~91czqb9(&i#2^{YP!S zXg1ogDlnY-a_Vt{72k>D#+zJ9S2)b?=(c&py*+O|&!ggr`bSLuvvDvKh%I9h=O>!oUQwf`x&Xi{psW>2?1Hm{fcRioubV%xV|+vZanauNASAC zS~;KGJ)Af5kFH@_|IfD}Dd)<&`<0$7N(>8RtdHhRv=muuuw=(swOKuvYc5%z`e&te zKU;0tzH?t+ulmDVYA~TNI+E8QDl#wK`&zQvme;A03$97L41O(s6@T&z{UI5mauNoU#^$`F~!v7DjRf_)O$S@mgYV;A#%D%Z)%`wVS@jc zw~WsfMT|Dctux|O%#ohl*10v3>n*P>!z$xQm+#Z&WIYw%{{FPFra|m1!)&d4G2g!# zDZMcF{bN}i;P08jesK3oe$AJk0=GN=eK+Av`2tXzBht+{f&hmwhGw!i$P^X4ULAKVU$eD#qM*xu+kTUqep%ZuCpoZq2x#pOV{ z!?xDds}1K)5}iLW;oOhIz-~3CG z!o7bVrv+@PSgo9XBrQ=h^`0UBGI;~_Rnu-q78Gv$_ncStwB;YoTl?z+*U6qQ-g25< z_16FD_yqrNGb@|!zFcC!YQI;)$SCvSx`R75UYX~|tr8z={WnCX^8BgO`8w0|c5zty z{aSlrTgk5bLI&NkASNSXW*5971ckL?ksfIoKcr4gFZcO0%D`&EOy*lgLy-}eq z{#VpmzY1yZY z_0J0o)~+t%a*2NPZORr~KYj;eY2jM27ra}Y-EP?|t-h&$Rl}zCrInOXp_#YeUHiJ# zlI><{XU@)g-IlMrDsruZ$_|zP7XxL>^&)RBV!gQ9I_@dYto!e~@AJp(^k=mZGE(>= z`HXMhJo9CtY>E<(C$YV=XJ2{CLeo1wZSH&K)gJ*7`2{ zqp-r)V@-sOC|6NQ15blLr}(anjy?T?61f|UgF=|+{-_Lgewx33alfxm-JHeq*X{8< zD=x{Rd-vnxdZBlk@n0L*GbQ+5bA3Gi%51@r>5tuheYOwlX1SNu_qe3%WuS}hWA}93 z#vP$`oSVANoSvuLD}J!?pn%_}m6CO*G9I{eE;_0roO6B}%cekaGwD}q%8ZKwBd*v@KEY1LKk{NsP~=SBB+?EiJ}kGhCRAcH*fx}M8h+C_t9 zCWx)rCplG>>mK8hVwJWxhMb>&vtM=6e9Y3jx@cME^G{E2{9n0VdkMRe*gLJVC381d z7kk-X)P0k(V`j?2_MXc%_Ga~4ncU3<&Gx-ta5izZ|v_939qD3rLO6dxF8TSZux z3Um0y7&|VE@>zduU*(zhB|ZzX!gwEY-Ay{Wp`jo<%VN#@8)Zw)bL{=z{7Bqe|7?TM z3HSSxuUWC*H*D55_7YRPxFFfS)1WVR`v=*3d2tuNomRWAmcplUioxc!fqh!+u05J_ zV#LyU9fWKYB0gtMve7(T|IzS5!2Ewm85kJ{0K)r=D^I5A_rI&BTk{Zu$2ySmoSpIPrNm$19sjwc>}Ruho7H*meBuo;h!Tr&x6W?+ge03BjZTp;X+Pp*iB4^w0h5){YI%`&}QIN{u z?h;;Y`sP-?XYoUCxv-Y$^&H23UCs{e%3z-0K1ajq%k;a4WLg$oKJej4${O=f)BXD| z?C;`PxOUyX2ON7e{uTtbiZSjzu=v7*95W*^_N6z(jQ=U@cYUgK^ZG}>ZwhK5CBm5; zU(YT}IjpcFYu@YKaxcEl%ZNIyGmkH^Ol@P+{Ql3koLU34jjFdV);(q0%E_Bve=X-h z|H0zv3HM@eWF1}kXg=@#>gxyAwpoo}=^txr`F5m`5_aE|mb zQAS_UeG>n+Dd%0WxOM6#TgI02*Pq5UZ@FThRsy^2R=?5=cMZNYr^=zKB z)}zx*6BR||-u9R_9-EW)<(#8-I`nSs~!>vBJD zma1IJcxjPQ0>hD)_rE4wcw3#saynr4#N6)m2TBu8ls&VLi+YjJXXLlYUgr6d+vQ?s zj~w)~%bvC244b0FbIqK}Q$jpyPx+_z$34$=DEY)HleacRde^c|;=!^0tGMD$y!fd~?yP^xxiF87Fco)W4OoF+6$UtVqk%6|&l^OfOEUu>bh>MA#*Pn^%;6 z-RYRks=!dW^7^UVa0{oGyT2%hDCb)J$TnP1_Q7@Ok*ixz-(TA4%ErM^AkF{=>;eq@ z=9TZ%`g@l=Jas;5S?=eR910AozPF>E3Wa=LFaD+N)g4~uk1IZ$>0Uiw*lATp{jtRw z=G>eL3@*y|H|RXj+jr`a-@VG&dq1qG+4W3I@58!T6E@o&W)J+y#lpY@vVw(ymy4IH z>&J!{zkiyH0c#jyGi-Kmw#mv|0cGy?u|UoGbQst68G7cOVdy4N(9U2 zhF2RL66E9lc#F5oJ^kRR)b1sP2dz!m>myG;v;TC*k?Hlve5RexbwhpbHpcBdDZ9z@ zxM;0i9%EVF!}y$@)|?$@IBKr*w{ZR`lV@C_Ia#zQP|S{_+u3AR-d4Nm=1j-Ept0KV--iKGg|qcAxTirBeB%Ehmq4Jg;+FVb0NHoX@%Z z;ACDNq1H`V3op0&9^_jb8Sr;ybP|8CfSg^eU=w{d|Z1G^90ld>dc z-TrAdf8r{&-(o4o(y;>5-?XsnuqRHPwLLZdg^svKn7!_r*4OWvY;?{%zQDoQHE+kn zz3=%hRoy(LB0a6%wCC*BfcqKJKQ{baxUMGqXIRaL4cTYkx96V`min+~%b95vix;c7 zBy)sw)_<9)yZ)f`x|K%re{)>_@;~a%O+zmMp5tGmqBu|PC^~&wv82HKUGOKLB!?B| z*}4`J&jea8KIOUBjX`t4%-bT&3o=hRl}~-wnig*JTH>ltJ7?xs%^md&`%NN^=e?>w za7U6gI@|V1qA0)M4Y5mmZ_JA3Ya=O@B_!2^>)8l*U%sP z|H6zJ??lfiO^`jyJMnqi`dg=Fhn}BycV1SO?Vb9v?MyOKlNqb*w+N-GpLxHqUvT$9 z>3j37qW-h)zqB(Z)@sU*AAkP@v+aHVAf~0fd)7qTgGUAIs<+f1f4i97`#?H#%-e*2 z^$R7Zf4%T@(LxLRwQ}MA%BHrehs91_Aiaor`znTqC!}s?9$vlJ{`YoPV@rjryZ9_0 z&-0xxcBEGP&sXaL-=A^Xuz;8I(*K%sU&VTb&AvG_Y`Ge-wI)@pzFkOFmS2gb$*Q2@{Xt><>H;2d zQ~UbG{8<49ETxN&H_Sh~mj8$)e_ zTJ3_V%!FLdiFwDTtNhn^wtK?luC`70Tr*|%Z(noLg=NFFe)%Ue!|PL5M4r0$h(T{F zb3wqbo|o=h&h*CTUMuxlXI8zN`G!_+qgNB(?1S7@x0Y@(nqQvn)gbY{_@SSg!2FQd zeN1Mn8v+@pC@fslmZQ9E+6^CD-A|YH*WA;Ld8T+qX{NyPr0vdMW-7hyo7vIP5m>ZH z$H3XdOML0_cU&vO8(Hg87FRgczs*_Z`eM!ob_b6+-%htpGHbuIzINIfpQ}uNJC`Mz_0;#9(>3@he%vJyZ delta 1925 zcmX@d-^b6%d~?yP^xxiF876Wn)aP5W^=%D2`znj=hSA}RnHx+lwLM>+R~RJEm%{$h zzqo}}f#CtmRKYTx!%r^$U0%WXqGQS{rXPjP#w+<<{WW(#kNB9$#=%e^&Hx7N0u0xq zZdTelm&a#Ee$=U$arq;M0z9rKiV-Cu&#@{#md^j+4^plTh6k74n_Ve1P_;1Z#oy>_1pe;LgtijXK%OjK6+KX zR+xYBq2w9<=Wm^e7G_EDD_;A3+0n=K{|$F{&e2O@SSc(Up}_p+(apk;CYu|ab2e`0 zeK~Xe$1N%i6DFmdUb{l8$l{9GOw&P^lw{iQ1`%6w| zosdm$yrpxm-qCtioWP3fjP~;0ZD#v^X?0|@*nU{JmA`)5tQlfk4{X@LrMyeT&(r9O z!l834ouVt%Rw#3qZTc~PQqqbOVLtPx@^B@o>D>`BJR&9d^0Jq4c=Y<{|DsN^!uf$8 z6}P;)d%)VK_MF4QO+Q^1^Zr$BzRMCTZ@uP?redOm)t>cBV)TrpmLHfQ5)f%>{(CN? zNN9KLt_QOtFZ^FOr9P@thw=MMul~N`V!P(Z?g@RdeRsr}m)t)jJlVWp$vd{G!R(Qn z`8USeom2X2)DbGIUNp~T7e|N5Q{}@A?~)>E}+CxgK<%?bg2fXVckvdkvO6OK%dqHtAJs{GYd*dJ`(* z<;82eE9KaK7p&TO)BI~m@79tV9gYSCigjlh#W~zxxtx{p-w-_g*UCd_zfWF#>@jt! z?>h;xSB%oJTov%PpFOlM%lEbMyR-kRt7eDmzrJbMJzH?cqErPJx14=K!SziO<0P;9&6s>*(btq# zK|NdUqW=X9saJ}kcdG`@*%xF~#5g~9^{x7UH3ynrtYC-mhms&HO;ZCzw>_I>U^)|ER0uPW^ljf}s*S!pJhxN7f)cVDI|9&T(F)3<%* zVHNS-#!XP?!MBL8nY^1?FXto_AL0@EvHXpR%{K|1jo$N@d*0_S`B4>-L`{i(m+OtrQ?xt?wJ;WK=*%hJ}xxZX91 zo%$f*I5)$r1r0khET0_-;fk7IQ+TVvO?3SUmi8~1GD0tQ2>bYdu47vEYg+xOA5VTA zaX)v%spgrMSM#RrvCmGuWx7%lUtO=1yIIxg^!xKN*Zp5<3;sRz_-RO8vHU5vJDLox zM^^G?c|8|n)UMYkbKVoSv}a5H*A*J`G(s4x@_yWpF@E@G_xHeftX8Q*e!Ntya{X&?v?;Db8!|oL+R|>2e9(FIih1 zFX#AQzQ%X_&w>4|Z*TQn>Yg;Q-P7@Czv`yKAHi9d)*mj{;TPX{?hgllXx-~#w_Ovu z(%fbj)`@(oE?Vhvtxj2Q;ALS2htYnzY9vPc9r{mMsM4*xgH&r%^woJyMEgr%N{Fu zsqMQc?*SjRN++8q+xIb9u|EE)&$HIwuu)BI%UV0OYP-~et}7Qa#11el$<2IL_D%fr zvF8n~f8U*PUah-g!UD(!`0@hZlb+j)Q`c^O%rRlt xF`2j$ySaT_2EQxx+P^)_dOPQ4#kC*FoB*kds9yj8