mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-10-01 01:26:01 -04:00
Implement support for parsing groups and entries from KeePass 1 databases.
Still missing: - Key files. - Twofish encryption. Refs #2
This commit is contained in:
parent
6234065898
commit
79b15e2ac6
@ -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
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -18,6 +18,7 @@
|
||||
#ifndef KEEPASSX_TESTKEEPASS1READER_H
|
||||
#define KEEPASSX_TESTKEEPASS1READER_H
|
||||
|
||||
#include <QtCore/QDateTime>
|
||||
#include <QtCore/QObject>
|
||||
|
||||
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
|
||||
|
@ -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;
|
||||
};
|
||||
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user