Implement support for parsing groups and entries from KeePass 1 databases.

Still missing:
- Key files.
- Twofish encryption.

Refs #2
This commit is contained in:
Felix Geyer 2012-05-09 23:14:27 +02:00
parent 6234065898
commit 79b15e2ac6
6 changed files with 454 additions and 2 deletions

View File

@ -22,6 +22,8 @@
#include "core/Database.h" #include "core/Database.h"
#include "core/Endian.h" #include "core/Endian.h"
#include "core/Entry.h"
#include "core/Group.h"
#include "crypto/CryptoHash.h" #include "crypto/CryptoHash.h"
#include "format/KeePass1.h" #include "format/KeePass1.h"
#include "keys/CompositeKey.h" #include "keys/CompositeKey.h"
@ -50,7 +52,9 @@ Database* KeePass1Reader::readDatabase(QIODevice* device, const QString& passwor
const QByteArray& keyfileData) const QByteArray& keyfileData)
{ {
QScopedPointer<Database> db(new Database()); QScopedPointer<Database> db(new Database());
QScopedPointer<Group> tmpParent(new Group());
m_db = db.data(); m_db = db.data();
m_tmpParent = tmpParent.data();
m_device = device; m_device = device;
m_error = false; m_error = false;
m_errorStr = QString(); m_errorStr = QString();
@ -142,6 +146,55 @@ Database* KeePass1Reader::readDatabase(QIODevice* device, const QString& passwor
return 0; return 0;
} }
QList<Group*> groups;
for (quint32 i = 0; i < numGroups; i++) {
Group* group = readGroup(cipherStream.data());
if (!group) {
return 0;
}
groups.append(group);
}
QList<Entry*> 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(); return db.take();
} }
@ -271,7 +324,6 @@ bool KeePass1Reader::verifyKey(SymmetricCipherStream* cipherStream)
if (readResult != buffer.size()) { if (readResult != buffer.size()) {
buffer.resize(readResult); buffer.resize(readResult);
} }
qDebug("read %d", buffer.size());
contentHash.addData(buffer); contentHash.addData(buffer);
} }
} while (readResult == buffer.size()); } while (readResult == buffer.size());
@ -279,12 +331,350 @@ bool KeePass1Reader::verifyKey(SymmetricCipherStream* cipherStream)
return contentHash.result() == m_contentHashHeader; return contentHash.result() == m_contentHashHeader;
} }
Group* KeePass1Reader::readGroup(QIODevice* cipherStream)
{
QScopedPointer<Group> 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<int>(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> 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<int>(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<Group*> 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) void KeePass1Reader::raiseError(const QString& str)
{ {
m_error = true; m_error = true;
m_errorStr = str; m_errorStr = str;
} }
QDateTime KeePass1Reader::dateFromPackedStruct(const QByteArray& data)
{
Q_ASSERT(data.size() == 5);
quint32 dw1 = static_cast<uchar>(data.at(0));
quint32 dw2 = static_cast<uchar>(data.at(1));
quint32 dw3 = static_cast<uchar>(data.at(2));
quint32 dw4 = static_cast<uchar>(data.at(3));
quint32 dw5 = static_cast<uchar>(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 QByteArray KeePass1Key::rawKey() const
{ {

View File

@ -19,8 +19,12 @@
#define KEEPASSX_KEEPASS1READER_H #define KEEPASSX_KEEPASS1READER_H
#include <QtCore/QCoreApplication> #include <QtCore/QCoreApplication>
#include <QtCore/QDateTime>
#include <QtCore/QHash>
class Database; class Database;
class Entry;
class Group;
class SymmetricCipherStream; class SymmetricCipherStream;
class QIODevice; class QIODevice;
@ -49,9 +53,16 @@ private:
qint64 contentPos); qint64 contentPos);
QByteArray key(const QByteArray& password, const QByteArray& keyfileData); QByteArray key(const QByteArray& password, const QByteArray& keyfileData);
bool verifyKey(SymmetricCipherStream* cipherStream); bool verifyKey(SymmetricCipherStream* cipherStream);
Group* readGroup(QIODevice* cipherStream);
Entry* readEntry(QIODevice* cipherStream);
bool constructGroupTree(const QList<Group*> groups);
bool parseMetaStream(const Entry* entry);
void raiseError(const QString& str); void raiseError(const QString& str);
static QDateTime dateFromPackedStruct(const QByteArray& data);
static bool isMetaStream(const Entry* entry);
Database* m_db; Database* m_db;
Group* m_tmpParent;
QIODevice* m_device; QIODevice* m_device;
quint32 m_encryptionFlags; quint32 m_encryptionFlags;
QByteArray m_masterSeed; QByteArray m_masterSeed;
@ -59,6 +70,9 @@ private:
QByteArray m_contentHashHeader; QByteArray m_contentHashHeader;
QByteArray m_transformSeed; QByteArray m_transformSeed;
quint32 m_transformRounds; quint32 m_transformRounds;
QHash<quint32, Group*> m_groupIds;
QHash<Group*, quint32> m_groupLevels;
QHash<QByteArray, Entry*> m_entryUuids;
bool m_error; bool m_error;
QString m_errorStr; QString m_errorStr;

View File

@ -22,6 +22,8 @@
#include "config-keepassx-tests.h" #include "config-keepassx-tests.h"
#include "tests.h" #include "tests.h"
#include "core/Database.h" #include "core/Database.h"
#include "core/Entry.h"
#include "core/Group.h"
#include "crypto/Crypto.h" #include "crypto/Crypto.h"
#include "format/KeePass1Reader.h" #include "format/KeePass1Reader.h"
@ -39,7 +41,49 @@ void TestKeePass1Reader::testBasic()
QVERIFY(db); QVERIFY(db);
QVERIFY(!reader.hasError()); 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; 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) KEEPASSX_QTEST_CORE_MAIN(TestKeePass1Reader)

View File

@ -18,6 +18,7 @@
#ifndef KEEPASSX_TESTKEEPASS1READER_H #ifndef KEEPASSX_TESTKEEPASS1READER_H
#define KEEPASSX_TESTKEEPASS1READER_H #define KEEPASSX_TESTKEEPASS1READER_H
#include <QtCore/QDateTime>
#include <QtCore/QObject> #include <QtCore/QObject>
class TestKeePass1Reader : public QObject class TestKeePass1Reader : public QObject
@ -27,6 +28,9 @@ class TestKeePass1Reader : public QObject
private Q_SLOTS: private Q_SLOTS:
void initTestCase(); void initTestCase();
void testBasic(); void testBasic();
private:
static QDateTime genDT(int year, int month, int day, int hour, int min);
}; };
#endif // KEEPASSX_TESTKEEPASS1READER_H #endif // KEEPASSX_TESTKEEPASS1READER_H

View File

@ -42,7 +42,7 @@ private Q_SLOTS:
void cleanupTestCase(); void cleanupTestCase();
private: 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; Database* m_db;
}; };

Binary file not shown.