diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c10a19d93..4f8e41b29 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -54,6 +54,7 @@ set(keepassx_SOURCES gui/KeyOpenDialog.cpp gui/MainWindow.cpp keys/CompositeKey.cpp + keys/FileKey.cpp keys/Key.h keys/PasswordKey.cpp streams/HashedBlockStream.cpp diff --git a/src/keys/FileKey.cpp b/src/keys/FileKey.cpp new file mode 100644 index 000000000..40f9db64e --- /dev/null +++ b/src/keys/FileKey.cpp @@ -0,0 +1,275 @@ +/* +* Copyright (C) 2011 Felix Geyer +* +* 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 . +*/ + +#include "FileKey.h" + +#include +#include + +#include "crypto/CryptoHash.h" +#include "crypto/Random.h" + +FileKey::FileKey() +{ +} + +bool FileKey::load(QIODevice* device) +{ + // we may need to read the file multiple times + if (device->isSequential()) { + return false; + } + + if (device->size() == 0) { + return false; + } + + if (!device->reset()) { + return false; + } + if (loadXml(device)) { + return true; + } + + if (!device->reset()) { + return false; + } + if (loadBinary(device)) { + return true; + } + + if (!device->reset()) { + return false; + } + if (loadHex(device)) { + return true; + } + + if (!device->reset()) { + return false; + } + if (loadHashed(device)) { + return true; + } + + return false; +} + +bool FileKey::load(const QString& fileName, QString* errorMsg) +{ + QFile file(fileName); + if (!file.open(QFile::ReadOnly)) { + if (errorMsg) { + *errorMsg = file.errorString(); + } + return false; + } + bool result = load(&file); + + file.close(); + + if (file.error()) { + result = false; + if (errorMsg) { + *errorMsg = file.errorString(); + } + } + + return result; +} + +QByteArray FileKey::rawKey() const +{ + return m_key; +} + +FileKey* FileKey::clone() const +{ + return new FileKey(*this); +} + +void FileKey::create(QIODevice* device) +{ + QXmlStreamWriter xmlWriter(device); + + xmlWriter.writeStartDocument("1.0"); + + xmlWriter.writeStartElement("KeyFile"); + + xmlWriter.writeStartElement("Meta"); + + xmlWriter.writeTextElement("Version", "1.00"); + + xmlWriter.writeEndElement(); + + xmlWriter.writeStartElement("Key"); + + QByteArray data = Random::randomArray(32); + xmlWriter.writeTextElement("Data", QString::fromAscii(data.toBase64())); + + xmlWriter.writeEndElement(); + + xmlWriter.writeEndDocument(); +} + +bool FileKey::create(const QString& fileName, QString* errorMsg) +{ + QFile file(fileName); + if (!file.open(QFile::WriteOnly)) { + if (errorMsg) { + *errorMsg = file.errorString(); + } + return false; + } + create(&file); + + file.close(); + + if (file.error()) { + if (errorMsg) { + *errorMsg = file.errorString(); + } + + return false; + } + else { + return true; + } +} + +bool FileKey::loadXml(QIODevice *device) +{ + QXmlStreamReader xmlReader(device); + + if (!xmlReader.error() && xmlReader.readNextStartElement()) { + if (xmlReader.name() != "KeyFile") { + return false; + } + } + else { + return false; + } + + bool correctMeta = false; + QByteArray data; + + while (!xmlReader.error() && xmlReader.readNextStartElement()) { + if (xmlReader.name() == "Meta") { + correctMeta = loadXmlMeta(xmlReader); + } + else if (xmlReader.name() == "Key") { + data = loadXmlKey(xmlReader); + } + } + + if (!xmlReader.error() && correctMeta && !data.isEmpty()) { + m_key = data; + return true; + } + else { + return false; + } +} + +bool FileKey::loadXmlMeta(QXmlStreamReader& xmlReader) +{ + bool corectVersion = false; + + while (!xmlReader.error() && xmlReader.readNextStartElement()) { + if (xmlReader.name() == "Version") { + // TODO error message about incompatible key file version + if (xmlReader.readElementText() == "1.00") { + corectVersion = true; + } + } + } + + return corectVersion; +} + +QByteArray FileKey::loadXmlKey(QXmlStreamReader& xmlReader) +{ + QByteArray data; + + while (!xmlReader.error() && xmlReader.readNextStartElement()) { + if (xmlReader.name() == "Data") { + // TODO do we need to enforce a specific data.size()? + data = QByteArray::fromBase64(xmlReader.readElementText().toAscii()); + } + } + + return data; +} + +bool FileKey::loadBinary(QIODevice *device) +{ + if (device->size() != 32) { + return false; + } + + m_key = device->readAll(); + return true; +} + +bool FileKey::loadHex(QIODevice *device) +{ + if (device->size() != 64) { + return false; + } + + QByteArray data = device->readAll(); + + if (!isHex(data)) { + return false; + } + + QByteArray key = QByteArray::fromHex(data); + + if (key.size() != 32) { + return false; + } + + m_key = key; + return true; +} + +bool FileKey::loadHashed(QIODevice *device) +{ + CryptoHash cryptoHash(CryptoHash::Sha256); + + QByteArray buffer; + + while (!device->atEnd()) { + buffer = device->read(1024); + cryptoHash.addData(buffer); + } + + m_key = cryptoHash.result(); + + return true; +} + +bool FileKey::isHex(const QByteArray& ba) +{ + Q_FOREACH (char c, ba) { + if ( !( (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') ) ) { + return false; + } + } + + return true; +} diff --git a/src/keys/FileKey.h b/src/keys/FileKey.h new file mode 100644 index 000000000..cc2c87255 --- /dev/null +++ b/src/keys/FileKey.h @@ -0,0 +1,50 @@ +/* +* Copyright (C) 2011 Felix Geyer +* +* 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 . +*/ + +#ifndef KEEPASSX_FILEKEY_H +#define KEEPASSX_FILEKEY_H + +#include + +#include "keys/Key.h" + +class QIODevice; + +class FileKey : public Key +{ +public: + FileKey(); + bool load(QIODevice* device); + bool load(const QString& fileName, QString* errorMsg = 0); + QByteArray rawKey() const; + FileKey* clone() const; + static void create(QIODevice* device); + static bool create(const QString& fileName, QString* errorMsg = 0); + +private: + bool loadXml(QIODevice* device); + bool loadXmlMeta(QXmlStreamReader& xmlReader); + QByteArray loadXmlKey(QXmlStreamReader& xmlReader); + bool loadBinary(QIODevice* device); + bool loadHex(QIODevice* device); + bool loadHashed(QIODevice* device); + static bool isHex(const QByteArray& ba); + + QByteArray m_key; +}; + +#endif // KEEPASSX_FILEKEY_H diff --git a/tests/TestKeys.cpp b/tests/TestKeys.cpp index 0698f8701..3854efe2f 100644 --- a/tests/TestKeys.cpp +++ b/tests/TestKeys.cpp @@ -17,10 +17,17 @@ #include "TestKeys.h" +#include #include +#include "config-keepassx-tests.h" +#include "core/Database.h" +#include "core/Metadata.h" #include "crypto/Crypto.h" +#include "format/KeePass2Reader.h" +#include "format/KeePass2Writer.h" #include "keys/CompositeKey.h" +#include "keys/FileKey.h" #include "keys/PasswordKey.h" void TestKeys::initTestCase() @@ -50,4 +57,91 @@ void TestKeys::testComposite() delete compositeKey2; } +void TestKeys::testFileKey() +{ + QFETCH(QString, type); + + QString name = QString("FileKey").append(type); + + KeePass2Reader reader; + + QString dbFilename = QString("%1/%2.kdbx").arg(QString(KEEPASSX_TEST_DATA_DIR), name); + QString keyFilename = QString("%1/%2.key").arg(QString(KEEPASSX_TEST_DATA_DIR), name); + + CompositeKey compositeKey; + FileKey fileKey; + QVERIFY(fileKey.load(keyFilename)); + QCOMPARE(fileKey.rawKey().size(), 32); + compositeKey.addKey(fileKey); + + Database* db = reader.readDatabase(dbFilename, compositeKey); + QVERIFY(db); + QVERIFY(!reader.error()); + QCOMPARE(db->metadata()->name(), QString("%1 Database").arg(name)); + + delete db; +} + +void TestKeys::testFileKey_data() +{ + QTest::addColumn("type"); + QTest::newRow("Xml") << QString("Xml"); + QTest::newRow("Binary") << QString("Binary"); + QTest::newRow("Hex") << QString("Hex"); + QTest::newRow("Hashed") << QString("Hashed"); +} + +void TestKeys::testCreateFileKey() +{ + const QString dbName("testCreateFileKey database"); + + QBuffer keyBuffer; + keyBuffer.open(QBuffer::ReadWrite); + + FileKey::create(&keyBuffer); + keyBuffer.reset(); + + FileKey fileKey; + QVERIFY(fileKey.load(&keyBuffer)); + CompositeKey compositeKey; + compositeKey.addKey(fileKey); + + Database* dbOrg = new Database(); + dbOrg->setKey(compositeKey); + dbOrg->metadata()->setName(dbName); + + QBuffer dbBuffer; + dbBuffer.open(QBuffer::ReadWrite); + + KeePass2Writer writer; + writer.writeDatabase(&dbBuffer, dbOrg); + dbBuffer.reset(); + delete dbOrg; + + KeePass2Reader reader; + Database* dbRead = reader.readDatabase(&dbBuffer, compositeKey); + QVERIFY(dbRead); + QVERIFY(!reader.error()); + QCOMPARE(dbRead->metadata()->name(), dbName); + delete dbRead; +} + +void TestKeys::testFileKeyError() +{ + bool result; + QString errorMsg; + const QString fileName(QString(KEEPASSX_TEST_DATA_DIR).append("/does/not/exist")); + + FileKey fileKey; + result = fileKey.load(fileName, &errorMsg); + QVERIFY(!result); + QVERIFY(!errorMsg.isEmpty()); + errorMsg = ""; + + result = FileKey::create(fileName, &errorMsg); + QVERIFY(!result); + QVERIFY(!errorMsg.isEmpty()); + errorMsg = ""; +} + QTEST_MAIN(TestKeys); diff --git a/tests/TestKeys.h b/tests/TestKeys.h index d525c706a..d1b62db82 100644 --- a/tests/TestKeys.h +++ b/tests/TestKeys.h @@ -27,6 +27,10 @@ class TestKeys : public QObject private Q_SLOTS: void initTestCase(); void testComposite(); + void testFileKey(); + void testFileKey_data(); + void testCreateFileKey(); + void testFileKeyError(); }; #endif // KEEPASSX_TESTKEYS_H diff --git a/tests/data/FileKeyBinary.kdbx b/tests/data/FileKeyBinary.kdbx new file mode 100644 index 000000000..fb9493f72 Binary files /dev/null and b/tests/data/FileKeyBinary.kdbx differ diff --git a/tests/data/FileKeyBinary.key b/tests/data/FileKeyBinary.key new file mode 100644 index 000000000..bc9591b2d --- /dev/null +++ b/tests/data/FileKeyBinary.key @@ -0,0 +1 @@ +  !"#$%&'()012 \ No newline at end of file diff --git a/tests/data/FileKeyHashed.kdbx b/tests/data/FileKeyHashed.kdbx new file mode 100644 index 000000000..dd60ddcc9 Binary files /dev/null and b/tests/data/FileKeyHashed.kdbx differ diff --git a/tests/data/FileKeyHashed.key b/tests/data/FileKeyHashed.key new file mode 100644 index 000000000..33f4a9fc7 Binary files /dev/null and b/tests/data/FileKeyHashed.key differ diff --git a/tests/data/FileKeyHex.kdbx b/tests/data/FileKeyHex.kdbx new file mode 100644 index 000000000..33f1fb1f3 Binary files /dev/null and b/tests/data/FileKeyHex.kdbx differ diff --git a/tests/data/FileKeyHex.key b/tests/data/FileKeyHex.key new file mode 100644 index 000000000..1bf8e5db3 --- /dev/null +++ b/tests/data/FileKeyHex.key @@ -0,0 +1 @@ +0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210 \ No newline at end of file diff --git a/tests/data/FileKeyXml.kdbx b/tests/data/FileKeyXml.kdbx new file mode 100644 index 000000000..a9db3ac40 Binary files /dev/null and b/tests/data/FileKeyXml.kdbx differ diff --git a/tests/data/FileKeyXml.key b/tests/data/FileKeyXml.key new file mode 100644 index 000000000..57138d91f --- /dev/null +++ b/tests/data/FileKeyXml.key @@ -0,0 +1,9 @@ + + + + 1.00 + + + nhNal+U9p6h1rWAAJ5YrNkMazMTZkIWLi3WC4JQv5jk= + +