diff --git a/src/format/KeePass1Reader.cpp b/src/format/KeePass1Reader.cpp index 5b4725bad..dc968cdd0 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 f0459438b..95cb883f5 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 54f7ac1f3..eff5093fc 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 55f4a9fd3..194167687 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 76402e419..3bf5f75a1 100644 Binary files a/tests/data/basic.kdb and b/tests/data/basic.kdb differ