diff options
author | Felix Geyer <debfx@fobos.de> | 2012-05-09 00:03:59 +0400 |
---|---|---|
committer | Felix Geyer <debfx@fobos.de> | 2012-05-09 00:06:12 +0400 |
commit | ebce183925d7d22359e48a367eaf31c14677bee3 (patch) | |
tree | 4d0d437f428fb8cf270a5639f3bc0e4c1c48b25b | |
parent | 38e421d9c1ecc41f6161b0121a55aa3a59889c7f (diff) |
Start implementing support for reading KeePass 1 databases.
For now only decrypting the database works.
Still missing:
- Actually parsing the database.
- Key files.
Refs #1
-rw-r--r-- | src/CMakeLists.txt | 2 | ||||
-rw-r--r-- | src/format/KeePass1.h | 42 | ||||
-rw-r--r-- | src/format/KeePass1Reader.cpp | 321 | ||||
-rw-r--r-- | src/format/KeePass1Reader.h | 67 | ||||
-rw-r--r-- | tests/CMakeLists.txt | 1 | ||||
-rw-r--r-- | tests/TestKeePass1Reader.cpp | 45 | ||||
-rw-r--r-- | tests/TestKeePass1Reader.h | 32 | ||||
-rw-r--r-- | tests/data/basic.kdb | bin | 0 -> 876 bytes |
8 files changed, 510 insertions, 0 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 680c8307b..9cde2bf8d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -43,6 +43,8 @@ set(keepassx_SOURCES crypto/SymmetricCipherBackend.h crypto/SymmetricCipherGcrypt.cpp crypto/SymmetricCipherSalsa20.cpp + format/KeePass1.h + format/KeePass1Reader.cpp format/KeePass2.h format/KeePass2RandomStream.cpp format/KeePass2Reader.cpp diff --git a/src/format/KeePass1.h b/src/format/KeePass1.h new file mode 100644 index 000000000..7e656c0cd --- /dev/null +++ b/src/format/KeePass1.h @@ -0,0 +1,42 @@ +/* +* Copyright (C) 2012 Felix Geyer <debfx@fobos.de> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 2 or (at your option) +* version 3 of the License. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef KEEPASSX_KEEPASS1_H +#define KEEPASSX_KEEPASS1_H + +#include <QtCore/QtGlobal> + +#include "core/Uuid.h" + +namespace KeePass1 +{ + const quint32 SIGNATURE_1 = 0x9AA2D903; + const quint32 SIGNATURE_2 = 0xB54BFB65; + const quint32 PWM_DBVER_DW = 0x00030002; + const quint32 FILE_VERSION = 0x00030002; + const quint32 FILE_VERSION_CRITICAL_MASK = 0xFFFFFF00; + + const QSysInfo::Endian BYTEORDER = QSysInfo::LittleEndian; + + enum EncryptionFlags + { + Rijndael = 2, + Twofish = 8 + }; +} + +#endif // KEEPASSX_KEEPASS1_H diff --git a/src/format/KeePass1Reader.cpp b/src/format/KeePass1Reader.cpp new file mode 100644 index 000000000..d85f16127 --- /dev/null +++ b/src/format/KeePass1Reader.cpp @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2012 Felix Geyer <debfx@fobos.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "KeePass1Reader.h" + +#include <QtCore/QFile> +#include <QtCore/QTextCodec> + +#include "core/Database.h" +#include "core/Endian.h" +#include "crypto/CryptoHash.h" +#include "format/KeePass1.h" +#include "keys/CompositeKey.h" +#include "streams/SymmetricCipherStream.h" + +class KeePass1Key : public CompositeKey +{ +public: + virtual QByteArray rawKey() const; + virtual void clear(); + void setPassword(const QByteArray& password); + void setKeyfileData(const QByteArray& keyfileData); + +private: + QByteArray m_password; + QByteArray m_keyfileData; +}; + + +KeePass1Reader::KeePass1Reader() + : m_error(false) +{ +} + +Database* KeePass1Reader::readDatabase(QIODevice* device, const QString& password, + const QByteArray& keyfileData) +{ + QScopedPointer<Database> db(new Database()); + m_db = db.data(); + m_device = device; + m_error = false; + m_errorStr = QString(); + + bool ok; + + quint32 signature1 = Endian::readUInt32(m_device, KeePass1::BYTEORDER, &ok); + if (!ok || signature1 != 0x9AA2D903) { + raiseError(tr("Not a KeePass database.")); + return 0; + } + + quint32 signature2 = Endian::readUInt32(m_device, KeePass1::BYTEORDER, &ok); + if (!ok || signature2 != 0xB54BFB65) { + raiseError(tr("Not a KeePass database.")); + return 0; + } + + m_encryptionFlags = Endian::readUInt32(m_device, KeePass1::BYTEORDER, &ok); + if (!ok || !(m_encryptionFlags & KeePass1::Rijndael || m_encryptionFlags & KeePass1::Twofish)) { + raiseError(tr("Unsupported encryption algorithm.")); + return 0; + } + + quint32 version = Endian::readUInt32(m_device, KeePass1::BYTEORDER, &ok); + if (!ok || (version & KeePass1::FILE_VERSION_CRITICAL_MASK) + != (KeePass1::FILE_VERSION & KeePass1::FILE_VERSION_CRITICAL_MASK)) { + raiseError(tr("Unsupported KeePass database version.")); + return 0; + } + + m_masterSeed = m_device->read(16); + if (m_masterSeed.size() != 16) { + // TODO error + Q_ASSERT(false); + return 0; + } + + m_encryptionIV = m_device->read(16); + if (m_encryptionIV.size() != 16) { + // TODO error + Q_ASSERT(false); + return 0; + } + + quint32 numGroups = Endian::readUInt32(m_device, KeePass1::BYTEORDER, &ok); + if (!ok) { + // TODO error + Q_ASSERT(false); + return 0; + } + + quint32 numEntries = Endian::readUInt32(m_device, KeePass1::BYTEORDER, &ok); + if (!ok) { + // TODO error + Q_ASSERT(false); + return 0; + } + + m_contentHashHeader = m_device->read(32); + if (m_contentHashHeader.size() != 32) { + // TODO error + Q_ASSERT(false); + return 0; + } + + m_transformSeed = m_device->read(32); + if (m_transformSeed.size() != 32) { + // TODO error + Q_ASSERT(false); + return 0; + } + + m_transformRounds = Endian::readUInt32(m_device, KeePass1::BYTEORDER, &ok); + if (!ok) { + // TODO error + Q_ASSERT(false); + return 0; + } + m_db->setTransformRounds(m_transformRounds); + + qint64 contentPos = m_device->pos(); + + QScopedPointer<SymmetricCipherStream> cipherStream(testKeys(password, keyfileData, contentPos)); + + if (!cipherStream) { + // TODO error + Q_ASSERT(false); + return 0; + } + + return db.take(); +} + +Database* KeePass1Reader::readDatabase(const QString& filename, const QString& password, + const QByteArray& keyfileData) +{ + QFile file(filename); + if (!file.open(QFile::ReadOnly)) { + raiseError(file.errorString()); + return 0; + } + + QScopedPointer<Database> db(readDatabase(&file, password, keyfileData)); + + if (file.error() != QFile::NoError) { + raiseError(file.errorString()); + return 0; + } + + return db.take(); +} + +bool KeePass1Reader::hasError() +{ + return m_error; +} + +QString KeePass1Reader::errorString() +{ + return m_errorStr; +} + +SymmetricCipherStream* KeePass1Reader::testKeys(const QString& password, const QByteArray& keyfileData, + qint64 contentPos) +{ + QList<PasswordEncoding> encodings; + encodings << Windows1252 << Latin1 << UTF8; + + QScopedPointer<SymmetricCipherStream> cipherStream; + QByteArray passwordData; + QTextCodec* codec = QTextCodec::codecForName("Windows-1252"); + QByteArray passwordDataCorrect = codec->fromUnicode(password); + + Q_FOREACH (PasswordEncoding encoding, encodings) { + if (encoding == Windows1252) { + passwordData = passwordDataCorrect; + } + else if (encoding == Latin1) { + // KeePassX used Latin-1 encoding for passwords until version 0.3.1 + // but KeePass/Win32 uses Windows Codepage 1252. + passwordData = password.toLatin1(); + + if (passwordData == passwordDataCorrect) { + continue; + } + else { + qWarning("Testing password encoded as Latin-1."); + } + } + else if (encoding == UTF8) { + // KeePassX used UTF-8 encoding for passwords until version 0.2.2 + // but KeePass/Win32 uses Windows Codepage 1252. + passwordData = password.toUtf8(); + + if (passwordData == passwordDataCorrect) { + continue; + } + else { + qWarning("Testing password encoded as UTF-8."); + } + } + + QByteArray finalKey = key(passwordData, keyfileData); + if (m_encryptionFlags & KeePass1::Rijndael) { + cipherStream.reset(new SymmetricCipherStream(m_device, SymmetricCipher::Aes256, + SymmetricCipher::Cbc, SymmetricCipher::Decrypt, finalKey, m_encryptionIV)); + } + else { + // TODO twofish + } + + cipherStream->open(QIODevice::ReadOnly); + + bool success = verifyKey(cipherStream.data()); + + cipherStream->reset(); + cipherStream->close(); + if (!m_device->seek(contentPos)) { + // TODO error + Q_ASSERT(false); + return 0; + } + cipherStream->open(QIODevice::ReadOnly); + + if (success) { + break; + } + else { + cipherStream.reset(); + } + } + + return cipherStream.take(); +} + +QByteArray KeePass1Reader::key(const QByteArray& password, const QByteArray& keyfileData) +{ + KeePass1Key key; + key.setPassword(password); + key.setKeyfileData(keyfileData); + + CryptoHash hash(CryptoHash::Sha256); + hash.addData(m_masterSeed); + hash.addData(key.transform(m_transformSeed, m_transformRounds)); + return hash.result(); +} + +bool KeePass1Reader::verifyKey(SymmetricCipherStream* cipherStream) +{ + CryptoHash contentHash(CryptoHash::Sha256); + QByteArray buffer; + buffer.resize(16384); + qint64 readResult; + do { + readResult = cipherStream->read(buffer.data(), buffer.size()); + if (readResult > 0) { + if (readResult != buffer.size()) { + buffer.resize(readResult); + } + qDebug("read %d", buffer.size()); + contentHash.addData(buffer); + } + } while (readResult == buffer.size()); + + return contentHash.result() == m_contentHashHeader; +} + +void KeePass1Reader::raiseError(const QString& str) +{ + m_error = true; + m_errorStr = str; +} + + +QByteArray KeePass1Key::rawKey() const +{ + if (m_keyfileData.isEmpty()) { + return CryptoHash::hash(m_password, CryptoHash::Sha256); + } + else if (m_password.isEmpty()) { + return m_keyfileData; + } + else { + CryptoHash keyHash(CryptoHash::Sha256); + keyHash.addData(m_password); + keyHash.addData(m_keyfileData); + return keyHash.result(); + } +} + +void KeePass1Key::clear() +{ + CompositeKey::clear(); + + m_password.clear(); + m_keyfileData.clear(); +} + +void KeePass1Key::setPassword(const QByteArray& password) +{ + m_password = password; +} + +void KeePass1Key::setKeyfileData(const QByteArray& keyfileData) +{ + m_keyfileData = keyfileData; +} diff --git a/src/format/KeePass1Reader.h b/src/format/KeePass1Reader.h new file mode 100644 index 000000000..e3d5eafe1 --- /dev/null +++ b/src/format/KeePass1Reader.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2012 Felix Geyer <debfx@fobos.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef KEEPASSX_KEEPASS1READER_H +#define KEEPASSX_KEEPASS1READER_H + +#include <QtCore/QCoreApplication> + +class Database; +class SymmetricCipherStream; +class QIODevice; + +class KeePass1Reader +{ + Q_DECLARE_TR_FUNCTIONS(KeePass1Reader) + +public: + KeePass1Reader(); + Database* readDatabase(QIODevice* device, const QString& password, + const QByteArray& keyfileData); + Database* readDatabase(const QString& filename, const QString& password, + const QByteArray& keyfileData); + bool hasError(); + QString errorString(); + +private: + enum PasswordEncoding + { + Windows1252, + Latin1, + UTF8 + }; + + SymmetricCipherStream* testKeys(const QString& password, const QByteArray& keyfileData, + qint64 contentPos); + QByteArray key(const QByteArray& password, const QByteArray& keyfileData); + bool verifyKey(SymmetricCipherStream* cipherStream); + void raiseError(const QString& str); + + Database* m_db; + QIODevice* m_device; + quint32 m_encryptionFlags; + QByteArray m_masterSeed; + QByteArray m_encryptionIV; + QByteArray m_contentHashHeader; + QByteArray m_transformSeed; + quint32 m_transformRounds; + + bool m_error; + QString m_errorStr; +}; + +#endif // KEEPASSX_KEEPASS1READER_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index df87c3d89..dc4bd5ff4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -127,6 +127,7 @@ add_unit_test(NAME testmodified SOURCES TestModified.cpp MOCS TestModified.h LIB add_unit_test(NAME testdeletedobjects SOURCES TestDeletedObjects.cpp MOCS TestDeletedObjects.h LIBS ${TEST_LIBRARIES}) +add_unit_test(NAME testkeepass1reader SOURCES TestKeePass1Reader.cpp MOCS TestKeePass1Reader.h LIBS ${TEST_LIBRARIES}) if(WITH_GUI_TESTS) add_subdirectory(gui) diff --git a/tests/TestKeePass1Reader.cpp b/tests/TestKeePass1Reader.cpp new file mode 100644 index 000000000..1dc31ee2a --- /dev/null +++ b/tests/TestKeePass1Reader.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2010 Felix Geyer <debfx@fobos.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "TestKeePass1Reader.h" + +#include <QtTest/QTest> + +#include "config-keepassx-tests.h" +#include "tests.h" +#include "core/Database.h" +#include "crypto/Crypto.h" +#include "format/KeePass1Reader.h" + +void TestKeePass1Reader::initTestCase() +{ + Crypto::init(); +} + +void TestKeePass1Reader::testBasic() +{ + QString filename = QString(KEEPASSX_TEST_DATA_DIR).append("/basic.kdb"); + + KeePass1Reader reader; + Database* db = reader.readDatabase(filename, "masterpw", QByteArray()); + QVERIFY(db); + QVERIFY(!reader.hasError()); + + delete db; +} + +KEEPASSX_QTEST_CORE_MAIN(TestKeePass1Reader) diff --git a/tests/TestKeePass1Reader.h b/tests/TestKeePass1Reader.h new file mode 100644 index 000000000..390174c4a --- /dev/null +++ b/tests/TestKeePass1Reader.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2012 Felix Geyer <debfx@fobos.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef KEEPASSX_TESTKEEPASS1READER_H +#define KEEPASSX_TESTKEEPASS1READER_H + +#include <QtCore/QObject> + +class TestKeePass1Reader : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void testBasic(); +}; + +#endif // KEEPASSX_TESTKEEPASS1READER_H diff --git a/tests/data/basic.kdb b/tests/data/basic.kdb Binary files differnew file mode 100644 index 000000000..251c3edf0 --- /dev/null +++ b/tests/data/basic.kdb |