diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d45464943..1f2558e71 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -100,6 +100,7 @@ set(keepassx_SOURCES streams/HashedBlockStream.cpp streams/LayeredStream.cpp streams/qtiocompressor.cpp + streams/StoreDataStream.cpp streams/SymmetricCipherStream.cpp ) @@ -151,6 +152,7 @@ set(keepassx_MOC streams/HashedBlockStream.h streams/LayeredStream.h streams/qtiocompressor.h + streams/StoreDataStream.h streams/SymmetricCipherStream.h ) diff --git a/src/format/KeePass2.h b/src/format/KeePass2.h index edb8ec5c9..d0002ddf5 100644 --- a/src/format/KeePass2.h +++ b/src/format/KeePass2.h @@ -26,7 +26,7 @@ namespace KeePass2 { const quint32 SIGNATURE_1 = 0x9AA2D903; const quint32 SIGNATURE_2 = 0xB54BFB67; - const quint32 FILE_VERSION = 0x00030000; + const quint32 FILE_VERSION = 0x00030001; const quint32 FILE_VERSION_MIN = 0x00020000; const quint32 FILE_VERSION_CRITICAL_MASK = 0xFFFF0000; diff --git a/src/format/KeePass2Reader.cpp b/src/format/KeePass2Reader.cpp index 49fb89f0c..f6818f4b2 100644 --- a/src/format/KeePass2Reader.cpp +++ b/src/format/KeePass2Reader.cpp @@ -29,6 +29,7 @@ #include "format/KeePass2XmlReader.h" #include "streams/HashedBlockStream.h" #include "streams/QtIOCompressor" +#include "streams/StoreDataStream.h" #include "streams/SymmetricCipherStream.h" KeePass2Reader::KeePass2Reader() @@ -45,21 +46,26 @@ Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& ke m_errorStr = QString(); m_headerEnd = false; + StoreDataStream headerStream(m_device); + headerStream.open(QIODevice::ReadOnly); + m_headerStream = &headerStream; + bool ok; - quint32 signature1 = Endian::readUInt32(m_device, KeePass2::BYTEORDER, &ok); + quint32 signature1 = Endian::readUInt32(m_headerStream, KeePass2::BYTEORDER, &ok); if (!ok || signature1 != KeePass2::SIGNATURE_1) { raiseError(tr("Not a KeePass database.")); return Q_NULLPTR; } - quint32 signature2 = Endian::readUInt32(m_device, KeePass2::BYTEORDER, &ok); + quint32 signature2 = Endian::readUInt32(m_headerStream, KeePass2::BYTEORDER, &ok); if (!ok || signature2 != KeePass2::SIGNATURE_2) { raiseError(tr("Not a KeePass database.")); return Q_NULLPTR; } - quint32 version = Endian::readUInt32(m_device, KeePass2::BYTEORDER, &ok) & KeePass2::FILE_VERSION_CRITICAL_MASK; + quint32 version = Endian::readUInt32(m_headerStream, KeePass2::BYTEORDER, &ok) + & KeePass2::FILE_VERSION_CRITICAL_MASK; quint32 maxVersion = KeePass2::FILE_VERSION & KeePass2::FILE_VERSION_CRITICAL_MASK; if (!ok || (version < KeePass2::FILE_VERSION_MIN) || (version > maxVersion)) { raiseError(tr("Unsupported KeePass database version.")); @@ -69,6 +75,8 @@ Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& ke while (readHeaderField() && !hasError()) { } + headerStream.close(); + // TODO: check if all header fields have been parsed m_db->setKey(key, m_transformSeed, false); @@ -78,7 +86,7 @@ Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& ke hash.addData(m_db->transformedMasterKey()); QByteArray finalKey = hash.result(); - SymmetricCipherStream cipherStream(device, SymmetricCipher::Aes256, SymmetricCipher::Cbc, + SymmetricCipherStream cipherStream(m_device, SymmetricCipher::Aes256, SymmetricCipher::Cbc, SymmetricCipher::Decrypt, finalKey, m_encryptionIV); cipherStream.open(QIODevice::ReadOnly); @@ -124,6 +132,16 @@ Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& ke return Q_NULLPTR; } + Q_ASSERT(version < 0x00030001 || !xmlReader.headerHash().isEmpty()); + + if (!xmlReader.headerHash().isEmpty()) { + QByteArray headerHash = CryptoHash::hash(headerStream.storedData(), CryptoHash::Sha256); + if (headerHash != xmlReader.headerHash()) { + raiseError(""); + return Q_NULLPTR; + } + } + return db.take(); } @@ -173,7 +191,7 @@ void KeePass2Reader::raiseError(const QString& str) bool KeePass2Reader::readHeaderField() { - QByteArray fieldIDArray = m_device->read(1); + QByteArray fieldIDArray = m_headerStream->read(1); if (fieldIDArray.size() != 1) { raiseError(""); return false; @@ -181,7 +199,7 @@ bool KeePass2Reader::readHeaderField() quint8 fieldID = fieldIDArray.at(0); bool ok; - quint16 fieldLen = Endian::readUInt16(m_device, KeePass2::BYTEORDER, &ok); + quint16 fieldLen = Endian::readUInt16(m_headerStream, KeePass2::BYTEORDER, &ok); if (!ok) { raiseError(""); return false; @@ -189,7 +207,7 @@ bool KeePass2Reader::readHeaderField() QByteArray fieldData; if (fieldLen != 0) { - fieldData = m_device->read(fieldLen); + fieldData = m_headerStream->read(fieldLen); if (fieldData.size() != fieldLen) { raiseError(""); return false; diff --git a/src/format/KeePass2Reader.h b/src/format/KeePass2Reader.h index 97a71b4c5..4b38de570 100644 --- a/src/format/KeePass2Reader.h +++ b/src/format/KeePass2Reader.h @@ -54,6 +54,7 @@ private: void setInnerRandomStreamID(const QByteArray& data); QIODevice* m_device; + QIODevice* m_headerStream; bool m_error; QString m_errorStr; bool m_headerEnd; diff --git a/src/format/KeePass2Writer.cpp b/src/format/KeePass2Writer.cpp index 027cc1945..91ad84159 100644 --- a/src/format/KeePass2Writer.cpp +++ b/src/format/KeePass2Writer.cpp @@ -17,6 +17,7 @@ #include "KeePass2Writer.h" +#include #include #include @@ -44,8 +45,6 @@ void KeePass2Writer::writeDatabase(QIODevice* device, Database* db) m_error = false; m_errorStr = QString(); - m_device = device; - QByteArray masterSeed = Random::randomArray(32); QByteArray encryptionIV = Random::randomArray(16); QByteArray protectedStreamKey = Random::randomArray(32); @@ -58,6 +57,9 @@ void KeePass2Writer::writeDatabase(QIODevice* device, Database* db) hash.addData(db->transformedMasterKey()); QByteArray finalKey = hash.result(); + QBuffer header; + header.open(QIODevice::WriteOnly); + m_device = &header; CHECK_RETURN(writeData(Endian::int32ToBytes(KeePass2::SIGNATURE_1, KeePass2::BYTEORDER))); CHECK_RETURN(writeData(Endian::int32ToBytes(KeePass2::SIGNATURE_2, KeePass2::BYTEORDER))); @@ -80,6 +82,11 @@ void KeePass2Writer::writeDatabase(QIODevice* device, Database* db) KeePass2::BYTEORDER))); CHECK_RETURN(writeHeaderField(KeePass2::EndOfHeader, endOfHeader)); + header.close(); + m_device = device; + QByteArray headerHash = CryptoHash::hash(header.data(), CryptoHash::Sha256); + CHECK_RETURN(writeData(header.data())); + SymmetricCipherStream cipherStream(device, SymmetricCipher::Aes256, SymmetricCipher::Cbc, SymmetricCipher::Encrypt, finalKey, encryptionIV); cipherStream.open(QIODevice::WriteOnly); @@ -104,7 +111,7 @@ void KeePass2Writer::writeDatabase(QIODevice* device, Database* db) KeePass2RandomStream randomStream(protectedStreamKey); KeePass2XmlWriter xmlWriter; - xmlWriter.writeDatabase(m_device, db, &randomStream); + xmlWriter.writeDatabase(m_device, db, &randomStream, headerHash); } bool KeePass2Writer::writeData(const QByteArray& data) diff --git a/src/format/KeePass2XmlReader.cpp b/src/format/KeePass2XmlReader.cpp index e7a4024c3..dbeb62291 100644 --- a/src/format/KeePass2XmlReader.cpp +++ b/src/format/KeePass2XmlReader.cpp @@ -45,6 +45,7 @@ void KeePass2XmlReader::readDatabase(QIODevice* device, Database* db, KeePass2Ra m_meta->setUpdateDatetime(false); m_randomStream = randomStream; + m_headerHash.clear(); m_tmpParent = new Group(); @@ -133,6 +134,11 @@ QString KeePass2XmlReader::errorString() .arg(m_xml.columnNumber()); } +QByteArray KeePass2XmlReader::headerHash() +{ + return m_headerHash; +} + void KeePass2XmlReader::parseKeePassFile() { Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "KeePassFile"); @@ -158,6 +164,9 @@ void KeePass2XmlReader::parseMeta() if (m_xml.name() == "Generator") { m_meta->setGenerator(readString()); } + else if (m_xml.name() == "HeaderHash") { + m_headerHash = readBinary(); + } else if (m_xml.name() == "DatabaseName") { m_meta->setName(readString()); } diff --git a/src/format/KeePass2XmlReader.h b/src/format/KeePass2XmlReader.h index 556bcdba2..032da4c69 100644 --- a/src/format/KeePass2XmlReader.h +++ b/src/format/KeePass2XmlReader.h @@ -46,6 +46,7 @@ public: Database* readDatabase(const QString& filename); bool hasError(); QString errorString(); + QByteArray headerHash(); private: void parseKeePassFile(); @@ -91,6 +92,7 @@ private: QHash m_entries; QHash m_binaryPool; QHash > m_binaryMap; + QByteArray m_headerHash; }; #endif // KEEPASSX_KEEPASS2XMLREADER_H diff --git a/src/format/KeePass2XmlWriter.cpp b/src/format/KeePass2XmlWriter.cpp index 8bd4649b8..d582476ce 100644 --- a/src/format/KeePass2XmlWriter.cpp +++ b/src/format/KeePass2XmlWriter.cpp @@ -34,11 +34,13 @@ KeePass2XmlWriter::KeePass2XmlWriter() m_xml.setCodec("UTF-8"); } -void KeePass2XmlWriter::writeDatabase(QIODevice* device, Database* db, KeePass2RandomStream* randomStream) +void KeePass2XmlWriter::writeDatabase(QIODevice* device, Database* db, KeePass2RandomStream* randomStream, + const QByteArray& headerHash) { m_db = db; m_meta = db->metadata(); m_randomStream = randomStream; + m_headerHash = headerHash; generateIdMap(); @@ -56,11 +58,11 @@ void KeePass2XmlWriter::writeDatabase(QIODevice* device, Database* db, KeePass2R m_xml.writeEndDocument(); } -void KeePass2XmlWriter::writeDatabase(const QString& filename, Database* db, KeePass2RandomStream* randomStream) +void KeePass2XmlWriter::writeDatabase(const QString& filename, Database* db) { QFile file(filename); file.open(QIODevice::WriteOnly|QIODevice::Truncate); - writeDatabase(&file, db, randomStream); + writeDatabase(&file, db); } void KeePass2XmlWriter::generateIdMap() @@ -83,6 +85,9 @@ void KeePass2XmlWriter::writeMetadata() m_xml.writeStartElement("Meta"); writeString("Generator", m_meta->generator()); + if (!m_headerHash.isEmpty()) { + writeBinary("HeaderHash", m_headerHash); + } writeString("DatabaseName", m_meta->name()); writeDateTime("DatabaseNameChanged", m_meta->nameChanged()); writeString("DatabaseDescription", m_meta->description()); diff --git a/src/format/KeePass2XmlWriter.h b/src/format/KeePass2XmlWriter.h index 7706c2625..78842cac4 100644 --- a/src/format/KeePass2XmlWriter.h +++ b/src/format/KeePass2XmlWriter.h @@ -36,8 +36,9 @@ class KeePass2XmlWriter { public: KeePass2XmlWriter(); - void writeDatabase(QIODevice* device, Database* db, KeePass2RandomStream* randomStream = Q_NULLPTR); - void writeDatabase(const QString& filename, Database* db, KeePass2RandomStream* randomStream = Q_NULLPTR); + void writeDatabase(QIODevice* device, Database* db, KeePass2RandomStream* randomStream = Q_NULLPTR, + const QByteArray& headerHash = QByteArray()); + void writeDatabase(const QString& filename, Database* db); bool error(); QString errorString(); @@ -77,6 +78,7 @@ private: Database* m_db; Metadata* m_meta; KeePass2RandomStream* m_randomStream; + QByteArray m_headerHash; QHash m_idMap; }; diff --git a/src/streams/StoreDataStream.cpp b/src/streams/StoreDataStream.cpp new file mode 100644 index 000000000..da94b851f --- /dev/null +++ b/src/streams/StoreDataStream.cpp @@ -0,0 +1,48 @@ +/* +* Copyright (C) 2012 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 "StoreDataStream.h" + +StoreDataStream::StoreDataStream(QIODevice* baseDevice) + : LayeredStream(baseDevice) +{ +} + +bool StoreDataStream::open(QIODevice::OpenMode mode) +{ + bool result = LayeredStream::open(mode); + + if (result) { + m_storedData.clear(); + } + + return result; +} + +QByteArray StoreDataStream::storedData() const +{ + return m_storedData; +} + +qint64 StoreDataStream::readData(char* data, qint64 maxSize) +{ + qint64 bytesRead = LayeredStream::readData(data, maxSize); + + m_storedData.append(data, bytesRead); + + return bytesRead; +} diff --git a/src/streams/StoreDataStream.h b/src/streams/StoreDataStream.h new file mode 100644 index 000000000..414343854 --- /dev/null +++ b/src/streams/StoreDataStream.h @@ -0,0 +1,39 @@ +/* +* Copyright (C) 2012 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_STOREDATASTREAM_H +#define KEEPASSX_STOREDATASTREAM_H + +#include "streams/LayeredStream.h" + +class StoreDataStream : public LayeredStream +{ + Q_OBJECT + +public: + explicit StoreDataStream(QIODevice* baseDevice); + bool open(QIODevice::OpenMode mode) Q_DECL_OVERRIDE; + QByteArray storedData() const; + +protected: + qint64 readData(char* data, qint64 maxSize) Q_DECL_OVERRIDE; + +private: + QByteArray m_storedData; +}; + +#endif // KEEPASSX_STOREDATASTREAM_H diff --git a/tests/TestKeePass2Reader.cpp b/tests/TestKeePass2Reader.cpp index 153f2fb63..ee28dfc77 100644 --- a/tests/TestKeePass2Reader.cpp +++ b/tests/TestKeePass2Reader.cpp @@ -43,6 +43,7 @@ void TestKeePass2Reader::testNonAscii() QVERIFY(db); QVERIFY(!reader.hasError()); QCOMPARE(db->metadata()->name(), QString("NonAsciiTest")); + QCOMPARE(db->compressionAlgo(), Database::CompressionNone); delete db; } @@ -57,6 +58,7 @@ void TestKeePass2Reader::testCompressed() QVERIFY(db); QVERIFY(!reader.hasError()); QCOMPARE(db->metadata()->name(), QString("Compressed")); + QCOMPARE(db->compressionAlgo(), Database::CompressionGZip); delete db; } @@ -87,6 +89,22 @@ void TestKeePass2Reader::testProtectedStrings() delete db; } +void TestKeePass2Reader::testBrokenHeaderHash() +{ + // The protected stream key has been modified in the header. + // Make sure the database won't open. + + QString filename = QString(KEEPASSX_TEST_DATA_DIR).append("/BrokenHeaderHash.kdbx"); + CompositeKey key; + key.addKey(PasswordKey("")); + KeePass2Reader reader; + Database* db = reader.readDatabase(filename, key); + QVERIFY(!db); + QVERIFY(reader.hasError()); + + delete db; +} + void TestKeePass2Reader::testFormat200() { QString filename = QString(KEEPASSX_TEST_DATA_DIR).append("/Format200.kdbx"); @@ -121,4 +139,20 @@ void TestKeePass2Reader::testFormat200() delete db; } +void TestKeePass2Reader::testFormat300() +{ + QString filename = QString(KEEPASSX_TEST_DATA_DIR).append("/Format300.kdbx"); + CompositeKey key; + key.addKey(PasswordKey("a")); + KeePass2Reader reader; + Database* db = reader.readDatabase(filename, key); + QVERIFY(db); + QVERIFY(!reader.hasError()); + + QCOMPARE(db->rootGroup()->name(), QString("Format300")); + QCOMPARE(db->metadata()->name(), QString("Test Database Format 0x00030000")); + + delete db; +} + QTEST_GUILESS_MAIN(TestKeePass2Reader) diff --git a/tests/TestKeePass2Reader.h b/tests/TestKeePass2Reader.h index 27680c855..8de873f93 100644 --- a/tests/TestKeePass2Reader.h +++ b/tests/TestKeePass2Reader.h @@ -29,7 +29,9 @@ private Q_SLOTS: void testNonAscii(); void testCompressed(); void testProtectedStrings(); + void testBrokenHeaderHash(); void testFormat200(); + void testFormat300(); }; #endif // KEEPASSX_TESTKEEPASS2READER_H diff --git a/tests/data/BrokenHeaderHash.kdbx b/tests/data/BrokenHeaderHash.kdbx new file mode 100644 index 000000000..6c4c43991 Binary files /dev/null and b/tests/data/BrokenHeaderHash.kdbx differ diff --git a/tests/data/Compressed.kdbx b/tests/data/Compressed.kdbx index af12d73aa..1f8ec2de6 100644 Binary files a/tests/data/Compressed.kdbx and b/tests/data/Compressed.kdbx differ diff --git a/tests/data/Format300.kdbx b/tests/data/Format300.kdbx new file mode 100644 index 000000000..dc67f35a1 Binary files /dev/null and b/tests/data/Format300.kdbx differ diff --git a/tests/data/NonAscii.kdbx b/tests/data/NonAscii.kdbx index 285831f8e..06aa5bf2c 100644 Binary files a/tests/data/NonAscii.kdbx and b/tests/data/NonAscii.kdbx differ diff --git a/tests/data/ProtectedStrings.kdbx b/tests/data/ProtectedStrings.kdbx index 2614097fc..bb50c03fb 100644 Binary files a/tests/data/ProtectedStrings.kdbx and b/tests/data/ProtectedStrings.kdbx differ