From 65f2790170ac80fdd33b9ae54968470bf81b2ef6 Mon Sep 17 00:00:00 2001 From: Felix Geyer Date: Thu, 10 May 2012 11:44:25 +0200 Subject: [PATCH] Parse KeePass 1 database meta streams. Refs #2 --- src/format/KeePass1Reader.cpp | 134 +++++++++++++++++++++++++++++++--- src/format/KeePass1Reader.h | 4 +- tests/TestKeePass1Reader.cpp | 78 ++++++++++++++++---- tests/TestKeePass1Reader.h | 7 ++ tests/data/basic.kdb | Bin 1100 -> 1836 bytes 5 files changed, 198 insertions(+), 25 deletions(-) diff --git a/src/format/KeePass1Reader.cpp b/src/format/KeePass1Reader.cpp index 5b4725ba..dc968cdd 100644 --- a/src/format/KeePass1Reader.cpp +++ b/src/format/KeePass1Reader.cpp @@ -19,11 +19,13 @@ #include #include +#include #include "core/Database.h" #include "core/Endian.h" #include "core/Entry.h" #include "core/Group.h" +#include "core/Metadata.h" #include "crypto/CryptoHash.h" #include "format/KeePass1.h" #include "keys/CompositeKey.h" @@ -170,9 +172,7 @@ Database* KeePass1Reader::readDatabase(QIODevice* device, const QString& passwor Q_FOREACH (Entry* entry, entries) { if (isMetaStream(entry)) { - if (!parseMetaStream(entry)) { - return 0; - } + parseMetaStream(entry); delete entry; } @@ -189,12 +189,10 @@ Database* KeePass1Reader::readDatabase(QIODevice* device, const QString& passwor group->setUpdateTimeinfo(true); } - Q_FOREACH (Entry* entry, entries) { + Q_FOREACH (Entry* entry, m_db->rootGroup()->entriesRecursive()) { entry->setUpdateTimeinfo(true); } - - return db.take(); } @@ -607,7 +605,7 @@ bool KeePass1Reader::constructGroupTree(const QList groups) else { for (int j = (i - 1); j >= 0; j--) { if (m_groupLevels.value(groups[j]) < level) { - if ((m_groupLevels.value(groups[j]) - level) != 1) { + if ((level - m_groupLevels.value(groups[j])) != 1) { return false; } @@ -625,9 +623,127 @@ bool KeePass1Reader::constructGroupTree(const QList groups) return true; } -bool KeePass1Reader::parseMetaStream(const Entry* entry) +void KeePass1Reader::parseMetaStream(const Entry* entry) { - // TODO: implement + QByteArray data = entry->attachments()->value("bin-stream"); + + if (entry->notes() == "KPX_GROUP_TREE_STATE") { + if (!parseGroupTreeState(data)) { + qWarning("Unable to parse group tree state metastream."); + } + } + else if (entry->notes() == "KPX_CUSTOM_ICONS_4") { + if (!parseCustomIcons4(data)) { + qWarning("Unable to parse custom icons metastream."); + } + } + else { + qWarning("Ignoring unknown metastream \"%s\".", entry->notes().toLocal8Bit().constData()); + } +} + +bool KeePass1Reader::parseGroupTreeState(const QByteArray& data) +{ + if (data.size() < 4) { + return false; + } + + int pos = 0; + quint32 num = Endian::bytesToUInt32(data.mid(pos, 4), KeePass1::BYTEORDER); + pos += 4; + + if ((data.size() - 4) != (num * 5)) { + return false; + } + + for (quint32 i = 0; i < num; i++) { + quint32 groupId = Endian::bytesToUInt32(data.mid(pos, 4), KeePass1::BYTEORDER); + pos += 4; + + bool expanded = data.at(pos); + pos += 1; + + if (m_groupIds.contains(groupId)) { + m_groupIds[groupId]->setExpanded(expanded); + } + } + + return true; +} + +bool KeePass1Reader::parseCustomIcons4(const QByteArray& data) +{ + if (data.size() < 12) { + return false; + } + + int pos = 0; + + quint32 numIcons = Endian::bytesToUInt32(data.mid(pos, 4), KeePass1::BYTEORDER); + pos += 4; + + quint32 numEntries = Endian::bytesToUInt32(data.mid(pos, 4), KeePass1::BYTEORDER); + pos += 4; + + quint32 numGroups = Endian::bytesToUInt32(data.mid(pos, 4), KeePass1::BYTEORDER); + pos += 4; + + QList iconUuids; + + for (quint32 i = 0; i < numIcons; i++) { + if (data.size() < (pos + 4)) { + return false; + } + quint32 iconSize = Endian::bytesToUInt32(data.mid(pos, 4), KeePass1::BYTEORDER); + pos += 4; + + if (data.size() < (pos + iconSize)) { + return false; + } + QImage icon = QImage::fromData(data.mid(pos, iconSize)); + pos += iconSize; + + if (icon.width() != 16 || icon.height() != 16) { + icon = icon.scaled(16, 16); + } + + Uuid uuid = Uuid::random(); + iconUuids.append(uuid); + m_db->metadata()->addCustomIcon(uuid, icon); + } + + if (data.size() < (pos + numEntries * 20)) { + return false; + } + + for (quint32 i = 0; i < numEntries; i++) { + QByteArray entryUuid = data.mid(pos, 16); + pos += 16; + + int iconId = Endian::bytesToUInt32(data.mid(pos, 4), KeePass1::BYTEORDER); + pos += 4; + + if (m_entryUuids.contains(entryUuid) && (iconId < iconUuids.size())) { + m_entryUuids[entryUuid]->setIcon(iconUuids[iconId]); + } + } + + if (data.size() < (pos + numGroups * 8)) { + return false; + } + + for (quint32 i = 0; i < numGroups; i++) { + quint32 groupId = Endian::bytesToUInt32(data.mid(pos, 4), KeePass1::BYTEORDER); + pos += 4; + + int iconId = Endian::bytesToUInt32(data.mid(pos, 4), KeePass1::BYTEORDER); + pos += 4; + + if (m_groupIds.contains(groupId) && (iconId < iconUuids.size())) { + m_groupIds[groupId]->setIcon(iconUuids[iconId]); + } + } + return true; } diff --git a/src/format/KeePass1Reader.h b/src/format/KeePass1Reader.h index f0459438..95cb883f 100644 --- a/src/format/KeePass1Reader.h +++ b/src/format/KeePass1Reader.h @@ -56,7 +56,9 @@ private: Group* readGroup(QIODevice* cipherStream); Entry* readEntry(QIODevice* cipherStream); bool constructGroupTree(const QList groups); - bool parseMetaStream(const Entry* entry); + void parseMetaStream(const Entry* entry); + bool parseGroupTreeState(const QByteArray& data); + bool parseCustomIcons4(const QByteArray& data); void raiseError(const QString& str); static QDateTime dateFromPackedStruct(const QByteArray& data); static bool isMetaStream(const Entry* entry); diff --git a/tests/TestKeePass1Reader.cpp b/tests/TestKeePass1Reader.cpp index 54f7ac1f..eff5093f 100644 --- a/tests/TestKeePass1Reader.cpp +++ b/tests/TestKeePass1Reader.cpp @@ -24,29 +24,31 @@ #include "core/Database.h" #include "core/Entry.h" #include "core/Group.h" +#include "core/Metadata.h" #include "crypto/Crypto.h" #include "format/KeePass1Reader.h" void TestKeePass1Reader::initTestCase() { Crypto::init(); + + QString filename = QString(KEEPASSX_TEST_DATA_DIR).append("/basic.kdb"); + + KeePass1Reader reader; + m_db = reader.readDatabase(filename, "masterpw", QByteArray()); + QVERIFY(m_db); + QVERIFY(!reader.hasError()); } void TestKeePass1Reader::testBasic() { - QString filename = QString(KEEPASSX_TEST_DATA_DIR).append("/basic.kdb"); + QCOMPARE(m_db->rootGroup()->children().size(), 2); - KeePass1Reader reader; - Database* db = reader.readDatabase(filename, "masterpw", QByteArray()); - QVERIFY(db); - QVERIFY(!reader.hasError()); - - QCOMPARE(db->rootGroup()->children().size(), 2); - - Group* group1 = db->rootGroup()->children().at(0); + Group* group1 = m_db->rootGroup()->children().at(0); QCOMPARE(group1->name(), QString("Internet")); - QCOMPARE(group1->iconNumber(), 1); + QCOMPARE(group1->children().size(), 2); QCOMPARE(group1->entries().size(), 2); + QCOMPARE(group1->iconNumber(), 1); Entry* entry11 = group1->entries().at(0); QCOMPARE(entry11->title(), QString("Test entry")); @@ -71,12 +73,58 @@ void TestKeePass1Reader::testBasic() QVERIFY(!entry12->timeInfo().expires()); QCOMPARE(entry12->attachments()->keys().size(), 0); - Group* group2 = db->rootGroup()->children().at(1); - QCOMPARE(group2->name(), QString("eMail")); - QCOMPARE(group2->iconNumber(), 19); - QCOMPARE(group2->entries().size(), 0); + Group* group11 = group1->children().at(0); + QCOMPARE(group11->name(), QString("Subgroup 1")); + QCOMPARE(group11->children().size(), 1); - delete db; + Group* group111 = group11->children().at(0); + QCOMPARE(group111->name(), QString("Unexpanded")); + QCOMPARE(group111->children().size(), 1); + + Group* group1111 = group111->children().at(0); + QCOMPARE(group1111->name(), QString("abc")); + QCOMPARE(group1111->children().size(), 0); + + Group* group12 = group1->children().at(1); + QCOMPARE(group12->name(), QString("Subgroup 2")); + QCOMPARE(group12->children().size(), 0); + + Group* group2 = m_db->rootGroup()->children().at(1); + QCOMPARE(group2->name(), QString("eMail")); + QCOMPARE(group2->entries().size(), 1); + QCOMPARE(group2->iconNumber(), 19); +} + +void TestKeePass1Reader::testCustomIcons() +{ + QCOMPARE(m_db->metadata()->customIcons().size(), 1); + + Entry* entry = m_db->rootGroup()->children().at(1)->entries().first(); + + QCOMPARE(entry->icon().width(), 16); + QCOMPARE(entry->icon().height(), 16); + + for (int x = 0; x < 16; x++) { + for (int y = 0; y < 16; y++) { + QRgb rgb = entry->icon().pixel(x, y); + QCOMPARE(qRed(rgb), 8); + QCOMPARE(qGreen(rgb), 160); + QCOMPARE(qBlue(rgb), 60); + } + } +} + +void TestKeePass1Reader::testGroupExpanded() +{ + QCOMPARE(m_db->rootGroup()->children().at(0)->isExpanded(), true); + QCOMPARE(m_db->rootGroup()->children().at(0)->children().at(0)->isExpanded(), true); + QCOMPARE(m_db->rootGroup()->children().at(0)->children().at(0)->children().at(0)->isExpanded(), + false); +} + +void TestKeePass1Reader::cleanupTestCase() +{ + delete m_db; } QDateTime TestKeePass1Reader::genDT(int year, int month, int day, int hour, int min) diff --git a/tests/TestKeePass1Reader.h b/tests/TestKeePass1Reader.h index 55f4a9fd..19416768 100644 --- a/tests/TestKeePass1Reader.h +++ b/tests/TestKeePass1Reader.h @@ -21,6 +21,8 @@ #include #include +class Database; + class TestKeePass1Reader : public QObject { Q_OBJECT @@ -28,9 +30,14 @@ class TestKeePass1Reader : public QObject private Q_SLOTS: void initTestCase(); void testBasic(); + void testCustomIcons(); + void testGroupExpanded(); + void cleanupTestCase(); private: static QDateTime genDT(int year, int month, int day, int hour, int min); + + Database* m_db; }; #endif // KEEPASSX_TESTKEEPASS1READER_H diff --git a/tests/data/basic.kdb b/tests/data/basic.kdb index 76402e4190832af03774399b80848690b334715d..3bf5f75a1bf128323b90d393e28c37b7f2e119aa 100644 GIT binary patch delta 1822 zcmV+(2jTe42&@iR1KFaQW&2CD0{{R30ssR57gv_NO7WAMnuwPxJlwlFjEL?9T6@(sTfS{wGkys>u zj~uXL_h;2!ShNS;F1`EZ_xiURqH@`1nD{S3AWgKN`dhQ=R|LC%YWy13OVX^geyS&D z**~o(r1CP01N#n_7)4rNDM;qt^n1$kN63t=&=sZ4M%6-*@<$Al&D4tAx<$j-;nyXPK*BAa zIUEt&SVM%gQ@MjVQt58Uh(C=y(cD!Z0F(cS8_=}(2d}r{^BH0A*X>e+rU1_ULtj!zm0*9eL3!b9IpEjL!->ZeDzJ$wzl7}q1eE!nYDJ!N z0$8D4q#HYRATfnj3fL!eJA>;VZ~0=MGk{kuU~u33ICL^-2R@_NJ5lZ^xlkhS)2%v2 zu5@L=Umn9u6PVd&!&nDT(%O%I9{Z?+=nH=b@OnI4YHLBYg`MtkUPg80zEh}HH1kq6 zvH#>&n^Jb^L*;c3ueGQ+0kGm}hIq=Z88dB1 zxGacY%1KwDH{Dvz zVN1s%Gcc0OC8)NTxm?h|yFJC=-`{wSI1U0Z7n9Koy0lqIXq+O;UI`lJN7` zxZ(hWwC52arVP0OjX?eM^mXj%?dF2Wci|Fm=~@_5?J+s%``}-dOu{m-Av$QpeGpg3 z4v_&F7_^7OM&?6*avfwzFYa`!hG@e>QDTdP9S5E!WVqT3-H<0w1i`ema~kq*@R8Qj_Y{2Ozg@sg8{jQ@5hZ(B3C{E~+beMbJ7&^4t8*Akj!U_sFD7;Kbz?i?(bK zfFFqqj@kH3jDESWqu~{LN|Ot3-da%Fx@<(k4LMhT4ndp6(Z;LKncpm-6xkT$Sb^)7 zgobwLF>@optZ5V{_o)SYS74+C*)?ufrHIF!X#oQwI9UYMlEI*~&wNJ?FG!}(AN}~rJP~K<3=|y_6dOHyc zc~qqUelcp7(ham&VgRL$=rkw)$nJArA#LB2pU%k7Z-WaB*`wajekcDgyjuwi8@=Z3 zT5CPE^j&90X3aTvtmEp5MRMEA_!p#Y-yLIr9mev2yt|}oF*TGyns$&uu6O9H`<3)9 zLz0l{HSOAH@Qqjq)tS%^ zG~-%^YCr4Q0ssI21ONa4e(Y`|2GJHSsV-cUaj{_E5}XON9PZ(^j+{iS^iR}wkys>u z1+1cXJoas@(GPmucFuwm%ZFj}ya3?Sj`?9uX`$KqSY$n{;?)NS-kAehVP(Iwcw~As zKMS?m;9*adyo&myQMPC?z>;?IHteTE^(`zb;snThTxaY6!T}=Ef_g`(mTj3bQ5EHr z3Vw*71QV~-4t(`;mpi9$MkzHZB7rS`N@)}8AA=8e$rlrbAXFW;MF6kB8{A2FtQ-J& zid-nK zcSGe=3f+z@QnvxwSCxzQHa^_w>3dyFxKQ-pTh22a`PGTb_gbf|=X#$`v_pq~q=It$ zL=Zu81UsH!72?%`M$sNW68V(p!uqR)HdKJ(*N53jyj6bYw5IasjdNKE zIM1eB>8_y#Cn>97$oG)6Kp$Fvto;ZFTn-#_sJg&GmfP0X3<){w-!HbBX?t3bVC(`h zaFq}ec$~)lVqLsbGS+9Qd!OkPD^p&ZFuyqx>i^JA*Mh{A3y^761Qq|kuhp%_8YK@a zSz{Z3+et%{Er6|kX6|G_YE5&BU^#XO55Mih~>vA}{VaKMuz$PsH2@57MaDY8T z$NkN^a#lzvD|L4QL{DoW!(;SePY#n+m1{J8!Vwh4Msg5hTA*wk{} zWnB8-O4ACA;`=PF(!Lww5HyJaMB8kXS@o5kFDx1;u)3l(j9x7nnB$AVX ybdOVs;6oC#p7Nd2A8mRAZr6T0twgtCaT6NR33`Od