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/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<Database> db(new Database());
QScopedPointer<Group> 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<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();
}
@ -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> 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)
{
m_error = true;
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
{

View file

@ -19,8 +19,12 @@
#define KEEPASSX_KEEPASS1READER_H
#include <QtCore/QCoreApplication>
#include <QtCore/QDateTime>
#include <QtCore/QHash>
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<Group*> 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<quint32, Group*> m_groupIds;
QHash<Group*, quint32> m_groupLevels;
QHash<QByteArray, Entry*> m_entryUuids;
bool m_error;
QString m_errorStr;