diff --git a/src/format/KeePass1Reader.cpp b/src/format/KeePass1Reader.cpp index d85f16127..5b4725bad 100644 --- a/src/format/KeePass1Reader.cpp +++ b/src/format/KeePass1Reader.cpp @@ -22,6 +22,8 @@ #include "core/Database.h" #include "core/Endian.h" +#include "core/Entry.h" +#include "core/Group.h" #include "crypto/CryptoHash.h" #include "format/KeePass1.h" #include "keys/CompositeKey.h" @@ -50,7 +52,9 @@ Database* KeePass1Reader::readDatabase(QIODevice* device, const QString& passwor const QByteArray& keyfileData) { QScopedPointer db(new Database()); + QScopedPointer tmpParent(new Group()); m_db = db.data(); + m_tmpParent = tmpParent.data(); m_device = device; m_error = false; m_errorStr = QString(); @@ -142,6 +146,55 @@ Database* KeePass1Reader::readDatabase(QIODevice* device, const QString& passwor return 0; } + QList groups; + for (quint32 i = 0; i < numGroups; i++) { + Group* group = readGroup(cipherStream.data()); + if (!group) { + return 0; + } + groups.append(group); + } + + QList entries; + for (quint32 i = 0; i < numEntries; i++) { + Entry* entry = readEntry(cipherStream.data()); + if (!entry) { + return 0; + } + entries.append(entry); + } + + if (!constructGroupTree(groups)) { + return 0; + } + + Q_FOREACH (Entry* entry, entries) { + if (isMetaStream(entry)) { + if (!parseMetaStream(entry)) { + return 0; + } + + delete entry; + } + } + + Q_ASSERT(m_tmpParent->children().isEmpty()); + + Q_FOREACH (Entry* entry, m_tmpParent->entries()) { + qWarning("Orphaned entry found, assigning to root group."); + entry->setGroup(m_db->rootGroup()); + } + + Q_FOREACH (Group* group, groups) { + group->setUpdateTimeinfo(true); + } + + Q_FOREACH (Entry* entry, entries) { + entry->setUpdateTimeinfo(true); + } + + + return db.take(); } @@ -271,7 +324,6 @@ bool KeePass1Reader::verifyKey(SymmetricCipherStream* cipherStream) if (readResult != buffer.size()) { buffer.resize(readResult); } - qDebug("read %d", buffer.size()); contentHash.addData(buffer); } } while (readResult == buffer.size()); @@ -279,12 +331,350 @@ bool KeePass1Reader::verifyKey(SymmetricCipherStream* cipherStream) return contentHash.result() == m_contentHashHeader; } +Group* KeePass1Reader::readGroup(QIODevice* cipherStream) +{ + QScopedPointer group(new Group()); + group->setUpdateTimeinfo(false); + group->setParent(m_tmpParent); + + TimeInfo timeInfo; + // TODO: make sure these are initalized + quint32 groupId; + quint32 groupLevel; + bool ok; + bool reachedEnd = false; + + do { + quint16 fieldType = Endian::readUInt16(cipherStream, KeePass1::BYTEORDER, &ok); + if (!ok) { + return 0; + } + + int fieldSize = static_cast(Endian::readUInt32(cipherStream, KeePass1::BYTEORDER, &ok)); + if (!ok) { + return 0; + } + + QByteArray fieldData = cipherStream->read(fieldSize); + if (fieldData.size() != fieldSize) { + // TODO error + Q_ASSERT(false); + return 0; + } + + switch (fieldType) { + case 0x0000: + // ignore field + break; + case 0x0001: + if (fieldSize != 4) { + return 0; + } + groupId = Endian::bytesToUInt32(fieldData, KeePass1::BYTEORDER); + break; + case 0x0002: + group->setName(QString::fromUtf8(fieldData.constData())); + break; + case 0x0003: + { + if (fieldSize != 5) { + return 0; + } + QDateTime dateTime = dateFromPackedStruct(fieldData); + if (dateTime.isValid()) { + timeInfo.setCreationTime(dateTime); + } + break; + } + case 0x0004: + { + if (fieldSize != 5) { + return 0; + } + QDateTime dateTime = dateFromPackedStruct(fieldData); + if (dateTime.isValid()) { + timeInfo.setLastModificationTime(dateTime); + } + break; + } + case 0x0005: + { + if (fieldSize != 5) { + return 0; + } + QDateTime dateTime = dateFromPackedStruct(fieldData); + if (dateTime.isValid()) { + timeInfo.setLastAccessTime(dateTime); + } + break; + } + case 0x0006: + { + if (fieldSize != 5) { + return 0; + } + QDateTime dateTime = dateFromPackedStruct(fieldData); + if (dateTime.isValid()) { + timeInfo.setExpires(true); + timeInfo.setExpiryTime(dateTime); + } + break; + } + case 0x0007: + { + if (fieldSize != 4) { + return 0; + } + quint32 iconNumber = Endian::bytesToUInt32(fieldData, KeePass1::BYTEORDER); + group->setIcon(iconNumber); + break; + } + case 0x0008: + { + if (fieldSize != 2) { + return 0; + } + groupLevel = Endian::bytesToUInt16(fieldData, KeePass1::BYTEORDER); + break; + } + case 0x0009: + // flags, ignore field + break; + case 0xFFFF: + reachedEnd = true; + break; + default: + // invalid field + return 0; + } + } while (!reachedEnd); + + group->setUuid(Uuid::random()); + group->setTimeInfo(timeInfo); + m_groupIds.insert(groupId, group.data()); + m_groupLevels.insert(group.data(), groupLevel); + + return group.take(); +} + +Entry* KeePass1Reader::readEntry(QIODevice* cipherStream) +{ + QScopedPointer entry(new Entry()); + entry->setUpdateTimeinfo(false); + entry->setGroup(m_tmpParent); + + TimeInfo timeInfo; + QString binaryName; + bool ok; + bool reachedEnd = false; + + do { + quint16 fieldType = Endian::readUInt16(cipherStream, KeePass1::BYTEORDER, &ok); + if (!ok) { + return 0; + } + + int fieldSize = static_cast(Endian::readUInt32(cipherStream, KeePass1::BYTEORDER, &ok)); + if (!ok) { + return 0; + } + + QByteArray fieldData = cipherStream->read(fieldSize); + if (fieldData.size() != fieldSize) { + // TODO error + Q_ASSERT(false); + return 0; + } + + switch (fieldType) { + case 0x0000: + // ignore field + break; + case 0x0001: + if (fieldSize != 16) { + return 0; + } + m_entryUuids.insert(fieldData, entry.data()); + break; + case 0x0002: + { + if (fieldSize != 4) { + return 0; + } + quint32 groupId = Endian::bytesToUInt32(fieldData, KeePass1::BYTEORDER); + entry->setGroup(m_groupIds.value(groupId)); + break; + } + case 0x0003: + { + if (fieldSize != 4) { + return 0; + } + quint32 iconNumber = Endian::bytesToUInt32(fieldData, KeePass1::BYTEORDER); + entry->setIcon(iconNumber); + break; + } + case 0x0004: + entry->setTitle(QString::fromUtf8(fieldData.constData())); + break; + case 0x0005: + entry->setUrl(QString::fromUtf8(fieldData.constData())); + break; + case 0x0006: + entry->setUsername(QString::fromUtf8(fieldData.constData())); + break; + case 0x0007: + entry->setPassword(QString::fromUtf8(fieldData.constData())); + break; + case 0x0008: + entry->setNotes(QString::fromUtf8(fieldData.constData())); + break; + case 0x0009: + { + if (fieldSize != 5) { + return 0; + } + QDateTime dateTime = dateFromPackedStruct(fieldData); + if (dateTime.isValid()) { + timeInfo.setCreationTime(dateTime); + } + break; + } + case 0x000A: + { + if (fieldSize != 5) { + return 0; + } + QDateTime dateTime = dateFromPackedStruct(fieldData); + if (dateTime.isValid()) { + timeInfo.setLastModificationTime(dateTime); + } + break; + } + case 0x000B: + { + if (fieldSize != 5) { + return 0; + } + QDateTime dateTime = dateFromPackedStruct(fieldData); + if (dateTime.isValid()) { + timeInfo.setLastAccessTime(dateTime); + } + break; + } + case 0x000C: + { + if (fieldSize != 5) { + return 0; + } + QDateTime dateTime = dateFromPackedStruct(fieldData); + if (dateTime.isValid()) { + timeInfo.setExpires(true); + timeInfo.setExpiryTime(dateTime); + } + break; + } + case 0x000D: + binaryName = QString::fromUtf8(fieldData.constData()); + break; + case 0x000E: + if (fieldSize != 0) { + entry->attachments()->set(binaryName, fieldData); + } + break; + case 0xFFFF: + reachedEnd = true; + break; + default: + // invalid field + return 0; + } + } while (!reachedEnd); + + entry->setTimeInfo(timeInfo); + + return entry.take(); +} + +bool KeePass1Reader::constructGroupTree(const QList groups) +{ + for (int i = 0; i < groups.size(); i++) { + quint32 level = m_groupLevels.value(groups[i]); + + if (level == 0) { + groups[i]->setParent(m_db->rootGroup()); + } + else { + for (int j = (i - 1); j >= 0; j--) { + if (m_groupLevels.value(groups[j]) < level) { + if ((m_groupLevels.value(groups[j]) - level) != 1) { + return false; + } + + groups[i]->setParent(groups[j]); + break; + } + } + } + + if (groups[i]->parent() == m_tmpParent) { + return false; + } + } + + return true; +} + +bool KeePass1Reader::parseMetaStream(const Entry* entry) +{ + // TODO: implement + return true; +} + void KeePass1Reader::raiseError(const QString& str) { m_error = true; m_errorStr = str; } +QDateTime KeePass1Reader::dateFromPackedStruct(const QByteArray& data) +{ + Q_ASSERT(data.size() == 5); + + quint32 dw1 = static_cast(data.at(0)); + quint32 dw2 = static_cast(data.at(1)); + quint32 dw3 = static_cast(data.at(2)); + quint32 dw4 = static_cast(data.at(3)); + quint32 dw5 = static_cast(data.at(4)); + + int y = (dw1 << 6) | (dw2 >> 2); + int mon = ((dw2 & 0x00000003) << 2) | (dw3 >> 6); + int d = (dw3 >> 1) & 0x0000001F; + int h = ((dw3 & 0x00000001) << 4) | (dw4 >> 4); + int min = ((dw4 & 0x0000000F) << 2) | (dw5 >> 6); + int s = dw5 & 0x0000003F; + + QDateTime dateTime = QDateTime(QDate(y, mon, d), QTime(h, min, s), Qt::UTC); + + // check for the special "never" datetime + if (dateTime == QDateTime(QDate(2999, 12, 28), QTime(23, 59, 59), Qt::UTC)) { + return QDateTime(); + } + else { + return dateTime; + } +} + +bool KeePass1Reader::isMetaStream(const Entry* entry) +{ + return entry->attachments()->keys().contains("bin-stream") + && !entry->notes().isEmpty() + && entry->title() == "Meta-Info" + && entry->username() == "SYSTEM" + && entry->url() == "$" + && entry->iconNumber() == 0; +} + QByteArray KeePass1Key::rawKey() const { diff --git a/src/format/KeePass1Reader.h b/src/format/KeePass1Reader.h index e3d5eafe1..f0459438b 100644 --- a/src/format/KeePass1Reader.h +++ b/src/format/KeePass1Reader.h @@ -19,8 +19,12 @@ #define KEEPASSX_KEEPASS1READER_H #include +#include +#include class Database; +class Entry; +class Group; class SymmetricCipherStream; class QIODevice; @@ -49,9 +53,16 @@ private: qint64 contentPos); QByteArray key(const QByteArray& password, const QByteArray& keyfileData); bool verifyKey(SymmetricCipherStream* cipherStream); + Group* readGroup(QIODevice* cipherStream); + Entry* readEntry(QIODevice* cipherStream); + bool constructGroupTree(const QList groups); + bool parseMetaStream(const Entry* entry); void raiseError(const QString& str); + static QDateTime dateFromPackedStruct(const QByteArray& data); + static bool isMetaStream(const Entry* entry); Database* m_db; + Group* m_tmpParent; QIODevice* m_device; quint32 m_encryptionFlags; QByteArray m_masterSeed; @@ -59,6 +70,9 @@ private: QByteArray m_contentHashHeader; QByteArray m_transformSeed; quint32 m_transformRounds; + QHash m_groupIds; + QHash m_groupLevels; + QHash m_entryUuids; bool m_error; QString m_errorStr; diff --git a/tests/TestKeePass1Reader.cpp b/tests/TestKeePass1Reader.cpp index 1dc31ee2a..54f7ac1f3 100644 --- a/tests/TestKeePass1Reader.cpp +++ b/tests/TestKeePass1Reader.cpp @@ -22,6 +22,8 @@ #include "config-keepassx-tests.h" #include "tests.h" #include "core/Database.h" +#include "core/Entry.h" +#include "core/Group.h" #include "crypto/Crypto.h" #include "format/KeePass1Reader.h" @@ -39,7 +41,49 @@ void TestKeePass1Reader::testBasic() QVERIFY(db); QVERIFY(!reader.hasError()); + QCOMPARE(db->rootGroup()->children().size(), 2); + + Group* group1 = db->rootGroup()->children().at(0); + QCOMPARE(group1->name(), QString("Internet")); + QCOMPARE(group1->iconNumber(), 1); + QCOMPARE(group1->entries().size(), 2); + + Entry* entry11 = group1->entries().at(0); + QCOMPARE(entry11->title(), QString("Test entry")); + QCOMPARE(entry11->iconNumber(), 1); + QCOMPARE(entry11->username(), QString("I")); + QCOMPARE(entry11->url(), QString("http://example.com/")); + QCOMPARE(entry11->password(), QString("secretpassword")); + QCOMPARE(entry11->notes(), QString("Lorem ipsum\ndolor sit amet")); + QVERIFY(entry11->timeInfo().expires()); + QCOMPARE(entry11->timeInfo().expiryTime(), genDT(2012, 5, 9, 10, 32)); + QCOMPARE(entry11->attachments()->keys().size(), 1); + QCOMPARE(entry11->attachments()->keys().first(), QString("attachment.txt")); + QCOMPARE(entry11->attachments()->value("attachment.txt"), QByteArray("hello world\n")); + + Entry* entry12 = group1->entries().at(1); + QCOMPARE(entry12->title(), QString("")); + QCOMPARE(entry12->iconNumber(), 0); + QCOMPARE(entry12->username(), QString("")); + QCOMPARE(entry12->url(), QString("")); + QCOMPARE(entry12->password(), QString("")); + QCOMPARE(entry12->notes(), QString("")); + 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); + delete db; } +QDateTime TestKeePass1Reader::genDT(int year, int month, int day, int hour, int min) +{ + QDate date(year, month, day); + QTime time(hour, min, 0); + return QDateTime(date, time, Qt::UTC); +} + KEEPASSX_QTEST_CORE_MAIN(TestKeePass1Reader) diff --git a/tests/TestKeePass1Reader.h b/tests/TestKeePass1Reader.h index 390174c4a..55f4a9fd3 100644 --- a/tests/TestKeePass1Reader.h +++ b/tests/TestKeePass1Reader.h @@ -18,6 +18,7 @@ #ifndef KEEPASSX_TESTKEEPASS1READER_H #define KEEPASSX_TESTKEEPASS1READER_H +#include #include class TestKeePass1Reader : public QObject @@ -27,6 +28,9 @@ class TestKeePass1Reader : public QObject private Q_SLOTS: void initTestCase(); void testBasic(); + +private: + static QDateTime genDT(int year, int month, int day, int hour, int min); }; #endif // KEEPASSX_TESTKEEPASS1READER_H diff --git a/tests/TestKeePass2XmlReader.h b/tests/TestKeePass2XmlReader.h index 415d734d1..2ed25fadb 100644 --- a/tests/TestKeePass2XmlReader.h +++ b/tests/TestKeePass2XmlReader.h @@ -42,7 +42,7 @@ private Q_SLOTS: void cleanupTestCase(); private: - QDateTime genDT(int year, int month, int day, int hour, int min, int second); + static QDateTime genDT(int year, int month, int day, int hour, int min, int second); Database* m_db; }; diff --git a/tests/data/basic.kdb b/tests/data/basic.kdb index 251c3edf0..76402e419 100644 Binary files a/tests/data/basic.kdb and b/tests/data/basic.kdb differ