From bef7ba2cfed00b88175473ae68481cebb7746098 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Fri, 5 Jan 2018 10:41:29 -0500 Subject: [PATCH] Implements KDBX4 format with Argon2 KDF * Adds KDBX4 reader/writer interfaces * Adds KDBX4 XML reader/write interfaces * Implements test cases for KDBX4 * Fully compatible with KeePass2 * Corrects minor issues with Argon2 KDF --- src/CMakeLists.txt | 4 + src/core/Database.cpp | 8 + src/core/Database.h | 3 + src/core/Metadata.cpp | 10 + src/core/Metadata.h | 4 + src/crypto/kdf/AesKdf.cpp | 25 + src/crypto/kdf/AesKdf.h | 2 + src/crypto/kdf/Argon2Kdf.cpp | 139 +++- src/crypto/kdf/Argon2Kdf.h | 16 +- src/crypto/kdf/Kdf.cpp | 8 +- src/crypto/kdf/Kdf.h | 2 + src/format/Kdbx3Reader.cpp | 2 +- src/format/Kdbx3Writer.cpp | 2 +- src/format/Kdbx4Reader.cpp | 522 ++++++++++++++ src/format/Kdbx4Reader.h | 66 ++ src/format/Kdbx4Writer.cpp | 326 +++++++++ src/format/Kdbx4Writer.h | 52 ++ src/format/Kdbx4XmlReader.cpp | 1080 ++++++++++++++++++++++++++++ src/format/Kdbx4XmlReader.h | 102 +++ src/format/Kdbx4XmlWriter.cpp | 611 ++++++++++++++++ src/format/Kdbx4XmlWriter.h | 93 +++ src/format/KeePass2.cpp | 46 ++ src/format/KeePass2.h | 58 +- src/format/KeePass2Reader.cpp | 17 +- src/format/KeePass2Reader.h | 3 +- src/format/KeePass2Repair.cpp | 19 +- src/format/KeePass2Writer.cpp | 19 +- src/gui/DatabaseSettingsWidget.cpp | 13 +- src/gui/DatabaseWidget.cpp | 2 +- tests/CMakeLists.txt | 3 + tests/TestKdbx4XmlReader.cpp | 22 + tests/TestKeePass2Reader.cpp | 23 + tests/TestKeePass2Reader.h | 1 + tests/TestKeePass2XmlReader.cpp | 40 ++ tests/TestKeePass2XmlReader.h | 13 + tests/data/Format400.kdbx | Bin 0 -> 1801 bytes 36 files changed, 3305 insertions(+), 51 deletions(-) create mode 100644 src/format/Kdbx4Reader.cpp create mode 100644 src/format/Kdbx4Reader.h create mode 100644 src/format/Kdbx4Writer.cpp create mode 100644 src/format/Kdbx4Writer.h create mode 100644 src/format/Kdbx4XmlReader.cpp create mode 100644 src/format/Kdbx4XmlReader.h create mode 100644 src/format/Kdbx4XmlWriter.cpp create mode 100644 src/format/Kdbx4XmlWriter.h create mode 100644 tests/TestKdbx4XmlReader.cpp create mode 100644 tests/data/Format400.kdbx diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1e927ee38..6ab5308dc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -91,6 +91,10 @@ set(keepassx_SOURCES format/Kdbx3Writer.cpp format/Kdbx3XmlReader.cpp format/Kdbx3XmlWriter.cpp + format/Kdbx4Reader.cpp + format/Kdbx4Writer.cpp + format/Kdbx4XmlReader.cpp + format/Kdbx4XmlWriter.cpp gui/AboutDialog.cpp gui/Application.cpp gui/CategoryListWidget.cpp diff --git a/src/core/Database.cpp b/src/core/Database.cpp index e3fa5a269..98a4fc817 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -494,6 +494,14 @@ void Database::setKdf(QSharedPointer kdf) m_data.kdf = std::move(kdf); } +void Database::setPublicCustomData(QByteArray data) { + m_data.publicCustomData = data; +} + +QByteArray Database::publicCustomData() const { + return m_data.publicCustomData; +} + bool Database::changeKdf(QSharedPointer kdf) { kdf->randomizeSeed(); diff --git a/src/core/Database.h b/src/core/Database.h index b293c760d..3bf43f62d 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -58,6 +58,7 @@ public: Uuid cipher; CompressionAlgorithm compressionAlgo; QByteArray transformedMasterKey; + QByteArray publicCustomData; QSharedPointer kdf; CompositeKey key; bool hasKey; @@ -91,6 +92,7 @@ public: Uuid cipher() const; Database::CompressionAlgorithm compressionAlgo() const; QSharedPointer kdf() const; + QByteArray publicCustomData() const; QByteArray transformedMasterKey() const; const CompositeKey& key() const; QByteArray challengeResponseKey() const; @@ -99,6 +101,7 @@ public: void setCipher(const Uuid& cipher); void setCompressionAlgo(Database::CompressionAlgorithm algo); void setKdf(QSharedPointer kdf); + void setPublicCustomData(QByteArray data); bool setKey(const CompositeKey& key, bool updateChangedTime = true, bool updateTransformSalt = false); bool hasKey() const; diff --git a/src/core/Metadata.cpp b/src/core/Metadata.cpp index 46b0a0b5e..ab56dab7f 100644 --- a/src/core/Metadata.cpp +++ b/src/core/Metadata.cpp @@ -49,6 +49,7 @@ Metadata::Metadata(QObject* parent) m_recycleBinChanged = now; m_entryTemplatesGroupChanged = now; m_masterKeyChanged = now; + m_settingsChanged = now; } template bool Metadata::set(P& property, const V& value) @@ -525,3 +526,12 @@ void Metadata::removeCustomField(const QString& key) m_customFields.remove(key); emit modified(); } + +QDateTime Metadata::settingsChanged() const { + return m_settingsChanged; +} + +void Metadata::setSettingsChanged(const QDateTime& value) { + Q_ASSERT(value.timeSpec() == Qt::UTC); + m_settingsChanged = value; +} diff --git a/src/core/Metadata.h b/src/core/Metadata.h index 1e972fd5a..7791b0387 100644 --- a/src/core/Metadata.h +++ b/src/core/Metadata.h @@ -69,6 +69,7 @@ public: QDateTime descriptionChanged() const; QString defaultUserName() const; QDateTime defaultUserNameChanged() const; + QDateTime settingsChanged() const; int maintenanceHistoryDays() const; QColor color() const; bool protectTitle() const; @@ -108,6 +109,7 @@ public: void setDescriptionChanged(const QDateTime& value); void setDefaultUserName(const QString& value); void setDefaultUserNameChanged(const QDateTime& value); + void setSettingsChanged(const QDateTime& value); void setMaintenanceHistoryDays(int value); void setColor(const QColor& value); void setProtectTitle(bool value); @@ -141,6 +143,7 @@ public: * - Master key changed date * - Custom icons * - Custom fields + * - Settings changed date */ void copyAttributesFrom(const Metadata* other); @@ -170,6 +173,7 @@ private: QPointer m_lastTopVisibleGroup; QDateTime m_masterKeyChanged; + QDateTime m_settingsChanged; QHash m_customFields; diff --git a/src/crypto/kdf/AesKdf.cpp b/src/crypto/kdf/AesKdf.cpp index 3177506fc..d668652aa 100644 --- a/src/crypto/kdf/AesKdf.cpp +++ b/src/crypto/kdf/AesKdf.cpp @@ -27,6 +27,31 @@ AesKdf::AesKdf() { } +bool AesKdf::processParameters(const QVariantMap &p) +{ + bool ok; + int rounds = p.value(KeePass2::KDFPARAM_AES_ROUNDS).toInt(&ok); + if (!ok || !setRounds(rounds)) { + return false; + } + + QByteArray seed = p.value(KeePass2::KDFPARAM_AES_SEED).toByteArray(); + if (!setSeed(seed)) { + return false; + } + + return true; +} + +QVariantMap AesKdf::writeParameters() +{ + QVariantMap p; + p.insert(KeePass2::KDFPARAM_UUID, KeePass2::KDF_AES.toByteArray()); + p.insert(KeePass2::KDFPARAM_AES_ROUNDS, rounds()); + p.insert(KeePass2::KDFPARAM_AES_SEED, seed()); + return p; +} + bool AesKdf::transform(const QByteArray& raw, QByteArray& result) const { QByteArray resultLeft; diff --git a/src/crypto/kdf/AesKdf.h b/src/crypto/kdf/AesKdf.h index 3e2c8ada6..69c15b8af 100644 --- a/src/crypto/kdf/AesKdf.h +++ b/src/crypto/kdf/AesKdf.h @@ -25,6 +25,8 @@ class AesKdf: public Kdf public: AesKdf(); + bool processParameters(const QVariantMap& p) override; + QVariantMap writeParameters() override; bool transform(const QByteArray& raw, QByteArray& result) const override; QSharedPointer clone() const override; diff --git a/src/crypto/kdf/Argon2Kdf.cpp b/src/crypto/kdf/Argon2Kdf.cpp index fa410dc93..12e9135af 100644 --- a/src/crypto/kdf/Argon2Kdf.cpp +++ b/src/crypto/kdf/Argon2Kdf.cpp @@ -32,24 +32,43 @@ */ Argon2Kdf::Argon2Kdf() : Kdf::Kdf(KeePass2::KDF_ARGON2) + , m_version(0x13) , m_memory(1<<16) , m_parallelism(2) { m_rounds = 1; } -quint32 Argon2Kdf::memory() const +quint32 Argon2Kdf::version() const { - // Convert to Megabytes - return m_memory / (1<<10); + return m_version; } -bool Argon2Kdf::setMemory(quint32 memoryMegabytes) +bool Argon2Kdf::setVersion(quint32 version) { - // TODO: add bounds check - // Convert to Kibibytes - m_memory = (1<<10) * memoryMegabytes; - return true; + // MIN=0x10; MAX=0x13) + if (version >= 0x10 && version <= 0x13) { + m_version = version; + return true; + } + m_version = 0x13; + return false; +} + +quint64 Argon2Kdf::memory() const +{ + return m_memory; +} + +bool Argon2Kdf::setMemory(quint64 kibibytes) +{ + // MIN=8KB; MAX=2,147,483,648KB + if (kibibytes >= 8 && kibibytes < (1ULL<<32)) { + m_memory = kibibytes; + return true; + } + m_memory = 16; + return false; } quint32 Argon2Kdf::parallelism() const @@ -59,30 +78,97 @@ quint32 Argon2Kdf::parallelism() const bool Argon2Kdf::setParallelism(quint32 threads) { - // TODO: add bounds check - m_parallelism = threads; + // MIN=1; MAX=16,777,215 + if (threads >= 1 && threads < (1<<24)) { + m_parallelism = threads; + return true; + } + m_parallelism = 1; + return false; +} + +bool Argon2Kdf::processParameters(const QVariantMap &p) +{ + QByteArray salt = p.value(KeePass2::KDFPARAM_ARGON2_SALT).toByteArray(); + if (!setSeed(salt)) { + return false; + } + + bool ok; + quint32 version = p.value(KeePass2::KDFPARAM_ARGON2_VERSION).toUInt(&ok); + if (!ok || !setVersion(version)) { + return false; + } + + quint32 lanes = p.value(KeePass2::KDFPARAM_ARGON2_PARALLELISM).toUInt(&ok); + if (!ok || !setParallelism(lanes)) { + return false; + } + + quint64 memory = p.value(KeePass2::KDFPARAM_ARGON2_MEMORY).toULongLong(&ok) / 1024ULL; + if (!ok || !setMemory(memory)) { + return false; + } + + quint64 iterations = p.value(KeePass2::KDFPARAM_ARGON2_ITERATIONS).toULongLong(&ok); + if (!ok || !setRounds(iterations)) { + return false; + } + + /* KeePass2 does not currently implement these parameters + * + QByteArray secret = p.value(KeePass2::KDFPARAM_ARGON2_SECRET).toByteArray(); + if (!argon2Kdf->setSecret(secret)) { + return nullptr; + } + + QByteArray ad = p.value(KeePass2::KDFPARAM_ARGON2_ASSOCDATA).toByteArray(); + if (!argon2Kdf->setAssocData(ad)) { + return nullptr; + } + */ + return true; } +QVariantMap Argon2Kdf::writeParameters() +{ + QVariantMap p; + p.insert(KeePass2::KDFPARAM_UUID, KeePass2::KDF_ARGON2.toByteArray()); + p.insert(KeePass2::KDFPARAM_ARGON2_VERSION, version()); + p.insert(KeePass2::KDFPARAM_ARGON2_PARALLELISM, parallelism()); + p.insert(KeePass2::KDFPARAM_ARGON2_MEMORY, memory() * 1024); + p.insert(KeePass2::KDFPARAM_ARGON2_ITERATIONS, static_cast(rounds())); + p.insert(KeePass2::KDFPARAM_ARGON2_SALT, seed()); + + /* KeePass2 does not currently implement these + * + if (!assocData().isEmpty()) { + p.insert(KeePass2::KDFPARAM_ARGON2_ASSOCDATA, argon2Kdf.assocData()); + } + + if (!secret().isEmpty()) { + p.insert(KeePass2::KDFPARAM_ARGON2_SECRET, argon2Kdf.secret()); + } + */ + + return p; +} + bool Argon2Kdf::transform(const QByteArray& raw, QByteArray& result) const { result.clear(); result.resize(32); - - if (!transformKeyRaw(raw, seed(), rounds(), memory(), parallelism(), result)) { - return false; - } - - result = CryptoHash::hash(result, CryptoHash::Sha256); - return true; + return transformKeyRaw(raw, seed(), version(), rounds(), memory(), parallelism(), result); } -bool Argon2Kdf::transformKeyRaw(const QByteArray& key, const QByteArray& seed, int rounds, - quint32 memory, quint32 parallelism, QByteArray& result) +bool Argon2Kdf::transformKeyRaw(const QByteArray& key, const QByteArray& seed, quint32 version, + quint32 rounds, quint64 memory, quint32 parallelism, QByteArray& result) { // Time Cost, Mem Cost, Threads/Lanes, Password, length, Salt, length, out, length - int rc = argon2d_hash_raw(rounds, memory, parallelism, key.data(), key.size(), - seed.data(), seed.size(), result.data(), result.size()); + int rc = argon2_hash(rounds, memory, parallelism, key.data(), key.size(), + seed.data(), seed.size(), result.data(), result.size(), + nullptr, 0, Argon2_d, version); if (rc != ARGON2_OK) { qWarning("Argon2 error: %s", argon2_error_message(rc)); return false; @@ -105,12 +191,9 @@ int Argon2Kdf::benchmarkImpl(int msec) const timer.start(); int rounds = 4; - - int rc = argon2d_hash_raw(rounds, m_memory, m_parallelism, key.data(), key.size(), seed.data(), seed.size(), key.data(), key.size()); - if (rc != ARGON2_OK) { - qWarning("Argon2 error: %s", argon2_error_message(rc)); - return -1; + if (transformKeyRaw(key, seed, version(), rounds, memory(), parallelism(), key)) { + return static_cast(rounds * (static_cast(msec) / timer.elapsed())); } - return static_cast(rounds * (static_cast(msec) / timer.elapsed())); -} \ No newline at end of file + return 1; +} diff --git a/src/crypto/kdf/Argon2Kdf.h b/src/crypto/kdf/Argon2Kdf.h index c01698120..345ca279c 100644 --- a/src/crypto/kdf/Argon2Kdf.h +++ b/src/crypto/kdf/Argon2Kdf.h @@ -24,25 +24,31 @@ class Argon2Kdf : public Kdf { public: Argon2Kdf(); + bool processParameters(const QVariantMap& p) override; + QVariantMap writeParameters() override; bool transform(const QByteArray& raw, QByteArray& result) const override; QSharedPointer clone() const override; - quint32 memory() const; - bool setMemory(quint32 memory_kb); + quint32 version() const; + bool setVersion(quint32 version); + quint64 memory() const; + bool setMemory(quint64 kibibytes); quint32 parallelism() const; bool setParallelism(quint32 threads); protected: int benchmarkImpl(int msec) const override; - quint32 m_memory; + quint32 m_version; + quint64 m_memory; quint32 m_parallelism; private: static bool transformKeyRaw(const QByteArray& key, const QByteArray& seed, - int rounds, - quint32 memory, + quint32 version, + quint32 rounds, + quint64 memory, quint32 parallelism, QByteArray& result) Q_REQUIRED_RESULT; }; diff --git a/src/crypto/kdf/Kdf.cpp b/src/crypto/kdf/Kdf.cpp index 5134adc5f..e500dbe6f 100644 --- a/src/crypto/kdf/Kdf.cpp +++ b/src/crypto/kdf/Kdf.cpp @@ -46,8 +46,12 @@ QByteArray Kdf::seed() const bool Kdf::setRounds(int rounds) { - m_rounds = rounds; - return true; + if (rounds >= 1 && rounds < INT_MAX) { + m_rounds = rounds; + return true; + } + m_rounds = 1; + return false; } bool Kdf::setSeed(const QByteArray& seed) diff --git a/src/crypto/kdf/Kdf.h b/src/crypto/kdf/Kdf.h index cb0bcc364..e45d23bcd 100644 --- a/src/crypto/kdf/Kdf.h +++ b/src/crypto/kdf/Kdf.h @@ -39,6 +39,8 @@ public: virtual bool setSeed(const QByteArray& seed); virtual void randomizeSeed(); + virtual bool processParameters(const QVariantMap& p) = 0; + virtual QVariantMap writeParameters() = 0; virtual bool transform(const QByteArray& raw, QByteArray& result) const = 0; virtual QSharedPointer clone() const = 0; diff --git a/src/format/Kdbx3Reader.cpp b/src/format/Kdbx3Reader.cpp index b5e5e2df8..3187442be 100644 --- a/src/format/Kdbx3Reader.cpp +++ b/src/format/Kdbx3Reader.cpp @@ -82,7 +82,7 @@ Database* Kdbx3Reader::readDatabase(QIODevice* device, const CompositeKey& key, quint32 version = Endian::readSizedInt(m_headerStream, KeePass2::BYTEORDER, &ok) & KeePass2::FILE_VERSION_CRITICAL_MASK; - quint32 maxVersion = KeePass2::FILE_VERSION & KeePass2::FILE_VERSION_CRITICAL_MASK; + quint32 maxVersion = KeePass2::FILE_VERSION_3 & KeePass2::FILE_VERSION_CRITICAL_MASK; if (!ok || (version < KeePass2::FILE_VERSION_MIN) || (version > maxVersion)) { raiseError(tr("Unsupported KeePass KDBX 2 or 3 database version.")); return nullptr; diff --git a/src/format/Kdbx3Writer.cpp b/src/format/Kdbx3Writer.cpp index 770e7270e..2fedf273c 100644 --- a/src/format/Kdbx3Writer.cpp +++ b/src/format/Kdbx3Writer.cpp @@ -74,7 +74,7 @@ bool Kdbx3Writer::writeDatabase(QIODevice* device, Database* db) CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes(KeePass2::SIGNATURE_1, KeePass2::BYTEORDER))); CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes(KeePass2::SIGNATURE_2, KeePass2::BYTEORDER))); - CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes(KeePass2::FILE_VERSION, KeePass2::BYTEORDER))); + CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes(KeePass2::FILE_VERSION_3, KeePass2::BYTEORDER))); CHECK_RETURN_FALSE(writeHeaderField(KeePass2::CipherID, db->cipher().toByteArray())); CHECK_RETURN_FALSE(writeHeaderField(KeePass2::CompressionFlags, diff --git a/src/format/Kdbx4Reader.cpp b/src/format/Kdbx4Reader.cpp new file mode 100644 index 000000000..0a69cbf2d --- /dev/null +++ b/src/format/Kdbx4Reader.cpp @@ -0,0 +1,522 @@ +/* + * Copyright (C) 2010 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 "Kdbx4Reader.h" + +#include +#include + +#include "crypto/kdf/AesKdf.h" +#include "streams/HmacBlockStream.h" +#include "core/Database.h" +#include "core/Endian.h" +#include "crypto/CryptoHash.h" +#include "format/KeePass1.h" +#include "format/KeePass2.h" +#include "format/KeePass2RandomStream.h" +#include "format/Kdbx4XmlReader.h" +#include "streams/HashedBlockStream.h" +#include "streams/QtIOCompressor" +#include "streams/StoreDataStream.h" +#include "streams/SymmetricCipherStream.h" + +Kdbx4Reader::Kdbx4Reader() + : m_device(nullptr) + , m_db(nullptr) +{ +} + +Database* Kdbx4Reader::readDatabase(QIODevice* device, const CompositeKey& key, bool keepDatabase) +{ + QScopedPointer db(new Database()); + m_db = db.data(); + m_device = device; + m_error = false; + m_errorStr.clear(); + m_xmlData.clear(); + m_masterSeed.clear(); + m_encryptionIV.clear(); + m_protectedStreamKey.clear(); + m_binaryPool.clear(); + + StoreDataStream headerStream(m_device); + headerStream.open(QIODevice::ReadOnly); + QIODevice* headerIo = &headerStream; + + bool ok; + + quint32 signature1 = Endian::readSizedInt(headerIo, KeePass2::BYTEORDER, &ok); + if (!ok || signature1 != KeePass2::SIGNATURE_1) { + raiseError(tr("Not a KeePass database.")); + return nullptr; + } + + quint32 signature2 = Endian::readSizedInt(headerIo, KeePass2::BYTEORDER, &ok); + if (ok && signature2 == KeePass1::SIGNATURE_2) { + raiseError(tr("The selected file is an old KeePass 1 database (.kdb).\n\n" + "You can import it by clicking on Database > 'Import KeePass 1 database...'.\n" + "This is a one-way migration. You won't be able to open the imported " + "database with the old KeePassX 0.4 version.")); + return nullptr; + } + else if (!ok || signature2 != KeePass2::SIGNATURE_2) { + raiseError(tr("Not a KeePass database.")); + return nullptr; + } + + quint32 version = Endian::readSizedInt(headerIo, KeePass2::BYTEORDER, &ok) + & KeePass2::FILE_VERSION_CRITICAL_MASK; + if (!ok || version != (KeePass2::FILE_VERSION_4 & KeePass2::FILE_VERSION_CRITICAL_MASK)) { + raiseError(tr("Unsupported KeePass KDBX 4 database version.")); + return nullptr; + } + + while (readHeaderField(headerIo) && !hasError()) { + } + + headerStream.close(); + + if (hasError()) { + return nullptr; + } + + // check if all required headers were present + if (m_masterSeed.isEmpty() + || m_encryptionIV.isEmpty() + || m_db->cipher().isNull()) { + raiseError("missing database headers"); + return nullptr; + } + + if (!m_db->setKey(key, false)) { + raiseError(tr("Unable to calculate master key")); + return nullptr; + } + + if (m_db->challengeMasterSeed(m_masterSeed) == false) { + raiseError(tr("Unable to issue challenge-response.")); + return nullptr; + } + + CryptoHash hash(CryptoHash::Sha256); + hash.addData(m_masterSeed); + hash.addData(m_db->challengeResponseKey()); + hash.addData(m_db->transformedMasterKey()); + QByteArray finalKey = hash.result(); + + QByteArray headerSha256 = m_device->read(32); + QByteArray headerHmac = m_device->read(32); + if (headerSha256.size() != 32 || headerHmac.size() != 32) { + raiseError("Invalid header checksum size"); + return nullptr; + } + if (headerSha256 != CryptoHash::hash(headerStream.storedData(), CryptoHash::Sha256)) { + raiseError("Header SHA256 mismatch"); + return nullptr; + } + + QByteArray hmacKey = KeePass2::hmacKey(m_masterSeed, m_db->transformedMasterKey()); + if (headerHmac != CryptoHash::hmac(headerStream.storedData(), + HmacBlockStream::getHmacKey(UINT64_MAX, hmacKey), CryptoHash::Sha256)) { + raiseError(tr("Wrong key or database file is corrupt. (HMAC mismatch)")); + return nullptr; + } + HmacBlockStream hmacStream(m_device, hmacKey); + if (!hmacStream.open(QIODevice::ReadOnly)) { + raiseError(hmacStream.errorString()); + return nullptr; + } + + SymmetricCipher::Algorithm cipher = SymmetricCipher::cipherToAlgorithm(m_db->cipher()); + if (cipher == SymmetricCipher::InvalidAlgorithm) { + raiseError("Unknown cipher"); + return nullptr; + } + SymmetricCipherStream cipherStream(&hmacStream, cipher, + SymmetricCipher::algorithmMode(cipher), SymmetricCipher::Decrypt); + if (!cipherStream.init(finalKey, m_encryptionIV)) { + raiseError(cipherStream.errorString()); + return nullptr; + } + if (!cipherStream.open(QIODevice::ReadOnly)) { + raiseError(cipherStream.errorString()); + return nullptr; + } + + QIODevice* xmlDevice; + QScopedPointer ioCompressor; + + if (m_db->compressionAlgo() == Database::CompressionNone) { + xmlDevice = &cipherStream; + } else { + ioCompressor.reset(new QtIOCompressor(&cipherStream)); + ioCompressor->setStreamFormat(QtIOCompressor::GzipFormat); + if (!ioCompressor->open(QIODevice::ReadOnly)) { + raiseError(ioCompressor->errorString()); + return nullptr; + } + xmlDevice = ioCompressor.data(); + } + + + while (readInnerHeaderField(xmlDevice) && !hasError()) { + } + + if (hasError()) { + return nullptr; + } + + KeePass2RandomStream randomStream(m_irsAlgo); + if (!randomStream.init(m_protectedStreamKey)) { + raiseError(randomStream.errorString()); + return nullptr; + } + + QScopedPointer buffer; + + if (m_saveXml) { + m_xmlData = xmlDevice->readAll(); + buffer.reset(new QBuffer(&m_xmlData)); + buffer->open(QIODevice::ReadOnly); + xmlDevice = buffer.data(); + } + + Kdbx4XmlReader xmlReader(m_binaryPool); + xmlReader.readDatabase(xmlDevice, m_db, &randomStream); + + if (xmlReader.hasError()) { + raiseError(xmlReader.errorString()); + if (keepDatabase) { + return db.take(); + } + else { + return nullptr; + } + } + + return db.take(); +} + +bool Kdbx4Reader::readHeaderField(QIODevice* device) +{ + QByteArray fieldIDArray = device->read(1); + if (fieldIDArray.size() != 1) { + raiseError("Invalid header id size"); + return false; + } + quint8 fieldID = fieldIDArray.at(0); + + bool ok; + quint32 fieldLen = Endian::readSizedInt(device, KeePass2::BYTEORDER, &ok); + if (!ok) { + raiseError("Invalid header field length"); + return false; + } + + QByteArray fieldData; + if (fieldLen != 0) { + fieldData = device->read(fieldLen); + if (static_cast(fieldData.size()) != fieldLen) { + raiseError("Invalid header data length"); + return false; + } + } + + switch (fieldID) { + case KeePass2::EndOfHeader: + return false; + + case KeePass2::CipherID: + setCipher(fieldData); + break; + + case KeePass2::CompressionFlags: + setCompressionFlags(fieldData); + break; + + case KeePass2::MasterSeed: + setMasterSeed(fieldData); + break; + + case KeePass2::EncryptionIV: + setEncryptionIV(fieldData); + break; + + case KeePass2::KdfParameters: { + QBuffer bufIoDevice(&fieldData); + if (!bufIoDevice.open(QIODevice::ReadOnly)) { + raiseError("Failed to open buffer for KDF parameters in header"); + return false; + } + QVariantMap kdfParams = readVariantMap(&bufIoDevice); + QSharedPointer kdf = KeePass2::kdfFromParameters(kdfParams); + if (kdf == nullptr) { + raiseError("Invalid KDF parameters"); + return false; + } + m_db->setKdf(kdf); + break; + } + + case KeePass2::PublicCustomData: + m_db->setPublicCustomData(fieldData); + break; + + case KeePass2::ProtectedStreamKey: + case KeePass2::TransformRounds: + case KeePass2::TransformSeed: + case KeePass2::StreamStartBytes: + case KeePass2::InnerRandomStreamID: + raiseError("Legacy header fields found in KDBX4 file."); + return false; + + default: + qWarning("Unknown header field read: id=%d", fieldID); + break; + } + + return true; +} + +bool Kdbx4Reader::readInnerHeaderField(QIODevice* device) +{ + QByteArray fieldIDArray = device->read(1); + if (fieldIDArray.size() != 1) { + raiseError("Invalid inner header id size"); + return false; + } + KeePass2::InnerHeaderFieldID fieldID = static_cast(fieldIDArray.at(0)); + + bool ok; + quint32 fieldLen = Endian::readSizedInt(device, KeePass2::BYTEORDER, &ok); + if (!ok) { + raiseError("Invalid inner header field length"); + return false; + } + + QByteArray fieldData; + if (fieldLen != 0) { + fieldData = device->read(fieldLen); + if (static_cast(fieldData.size()) != fieldLen) { + raiseError("Invalid header data length"); + return false; + } + } + + switch (fieldID) { + case KeePass2::InnerHeaderFieldID::End: + return false; + + case KeePass2::InnerHeaderFieldID::InnerRandomStreamID: + setInnerRandomStreamID(fieldData); + break; + + case KeePass2::InnerHeaderFieldID::InnerRandomStreamKey: + setProtectedStreamKey(fieldData); + break; + + case KeePass2::InnerHeaderFieldID::Binary: + if (fieldLen < 1) { + raiseError("Invalid inner header binary size"); + return false; + } + m_binaryPool.insert(QString::number(m_binaryPool.size()), fieldData.mid(1)); + break; + + default: + qWarning("Unknown inner header field read: id=%hhu", static_cast(fieldID)); + break; + } + + return true; +} + +QVariantMap Kdbx4Reader::readVariantMap(QIODevice* device) +{ + bool ok; + quint16 version = Endian::readSizedInt(device, KeePass2::BYTEORDER, &ok) + & KeePass2::VARIANTMAP_CRITICAL_MASK; + quint16 maxVersion = KeePass2::VARIANTMAP_VERSION & KeePass2::VARIANTMAP_CRITICAL_MASK; + if (!ok || (version > maxVersion)) { + raiseError(tr("Unsupported KeePass variant map version.")); + return QVariantMap(); + } + + QVariantMap vm; + QByteArray fieldTypeArray; + KeePass2::VariantMapFieldType fieldType; + while (((fieldTypeArray = device->read(1)).size() == 1) + && ((fieldType = static_cast(fieldTypeArray.at(0))) + != KeePass2::VariantMapFieldType::End)) { + quint32 nameLen = Endian::readSizedInt(device, KeePass2::BYTEORDER, &ok); + if (!ok) { + raiseError("Invalid variant map entry name length"); + return QVariantMap(); + } + QByteArray nameBytes; + if (nameLen != 0) { + nameBytes = device->read(nameLen); + if (static_cast(nameBytes.size()) != nameLen) { + raiseError("Invalid variant map entry name data"); + return QVariantMap(); + } + } + QString name = QString::fromUtf8(nameBytes); + + quint32 valueLen = Endian::readSizedInt(device, KeePass2::BYTEORDER, &ok); + if (!ok) { + raiseError("Invalid variant map entry value length"); + return QVariantMap(); + } + QByteArray valueBytes; + if (valueLen != 0) { + valueBytes = device->read(valueLen); + if (static_cast(valueBytes.size()) != valueLen) { + raiseError("Invalid variant map entry value data"); + return QVariantMap(); + } + } + + switch (fieldType) { + case KeePass2::VariantMapFieldType::Bool: + if (valueLen == 1) { + vm.insert(name, QVariant(valueBytes.at(0) != 0)); + } else { + raiseError("Invalid variant map Bool entry value length"); + return QVariantMap(); + } + break; + case KeePass2::VariantMapFieldType::Int32: + if (valueLen == 4) { + vm.insert(name, QVariant(Endian::bytesToSizedInt(valueBytes, KeePass2::BYTEORDER))); + } else { + raiseError("Invalid variant map Int32 entry value length"); + return QVariantMap(); + } + break; + case KeePass2::VariantMapFieldType::UInt32: + if (valueLen == 4) { + vm.insert(name, QVariant(Endian::bytesToSizedInt(valueBytes, KeePass2::BYTEORDER))); + } else { + raiseError("Invalid variant map UInt32 entry value length"); + return QVariantMap(); + } + break; + case KeePass2::VariantMapFieldType::Int64: + if (valueLen == 8) { + vm.insert(name, QVariant(Endian::bytesToSizedInt(valueBytes, KeePass2::BYTEORDER))); + } else { + raiseError("Invalid variant map Int64 entry value length"); + return QVariantMap(); + } + break; + case KeePass2::VariantMapFieldType::UInt64: + if (valueLen == 8) { + vm.insert(name, QVariant(Endian::bytesToSizedInt(valueBytes, KeePass2::BYTEORDER))); + } else { + raiseError("Invalid variant map UInt64 entry value length"); + return QVariantMap(); + } + break; + case KeePass2::VariantMapFieldType::String: + vm.insert(name, QVariant(QString::fromUtf8(valueBytes))); + break; + case KeePass2::VariantMapFieldType::ByteArray: + vm.insert(name, QVariant(valueBytes)); + break; + default: + raiseError("Invalid variant map entry type"); + return QVariantMap(); + } + } + + if (fieldTypeArray.size() != 1) { + raiseError("Invalid variant map field type size"); + return QVariantMap(); + } + + return vm; +} + +void Kdbx4Reader::setCipher(const QByteArray& data) +{ + if (data.size() != Uuid::Length) { + raiseError("Invalid cipher uuid length"); + } else { + Uuid uuid(data); + + if (SymmetricCipher::cipherToAlgorithm(uuid) == SymmetricCipher::InvalidAlgorithm) { + raiseError("Unsupported cipher"); + } else { + m_db->setCipher(uuid); + } + } +} + +void Kdbx4Reader::setCompressionFlags(const QByteArray& data) +{ + if (data.size() != 4) { + raiseError("Invalid compression flags length"); + } else { + quint32 id = Endian::bytesToSizedInt(data, KeePass2::BYTEORDER); + + if (id > Database::CompressionAlgorithmMax) { + raiseError("Unsupported compression algorithm"); + } else { + m_db->setCompressionAlgo(static_cast(id)); + } + } +} + +void Kdbx4Reader::setMasterSeed(const QByteArray& data) +{ + if (data.size() != 32) { + raiseError("Invalid master seed size"); + } else { + m_masterSeed = data; + } +} + +void Kdbx4Reader::setEncryptionIV(const QByteArray& data) +{ + m_encryptionIV = data; +} + +void Kdbx4Reader::setProtectedStreamKey(const QByteArray& data) +{ + m_protectedStreamKey = data; +} + +void Kdbx4Reader::setInnerRandomStreamID(const QByteArray& data) +{ + if (data.size() != 4) { + raiseError("Invalid random stream id size"); + } else { + quint32 id = Endian::bytesToSizedInt(data, KeePass2::BYTEORDER); + KeePass2::ProtectedStreamAlgo irsAlgo = KeePass2::idToProtectedStreamAlgo(id); + if (irsAlgo == KeePass2::InvalidProtectedStreamAlgo || irsAlgo == KeePass2::ArcFourVariant) { + raiseError("Invalid inner random stream cipher"); + } else { + m_irsAlgo = irsAlgo; + } + } +} + +QHash Kdbx4Reader::binaryPool() +{ + return m_binaryPool; +} diff --git a/src/format/Kdbx4Reader.h b/src/format/Kdbx4Reader.h new file mode 100644 index 000000000..0375209c4 --- /dev/null +++ b/src/format/Kdbx4Reader.h @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2010 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_KDBX4READER_H +#define KEEPASSX_KDBX4READER_H + +#include +#include +#include +#include + +#include "format/KeePass2.h" +#include "format/KeePass2Reader.h" +#include "crypto/SymmetricCipher.h" +#include "keys/CompositeKey.h" + +class Database; +class QIODevice; + +class Kdbx4Reader : public BaseKeePass2Reader +{ + Q_DECLARE_TR_FUNCTIONS(Kdbx4Reader) + +public: + Kdbx4Reader(); + + using BaseKeePass2Reader::readDatabase; + virtual Database* readDatabase(QIODevice* device, const CompositeKey& key, bool keepDatabase = false) override; + + QHash binaryPool(); + +private: + bool readHeaderField(QIODevice* device); + bool readInnerHeaderField(QIODevice* device); + QVariantMap readVariantMap(QIODevice* device); + + void setCipher(const QByteArray& data); + void setCompressionFlags(const QByteArray& data); + void setMasterSeed(const QByteArray& data); + void setEncryptionIV(const QByteArray& data); + void setProtectedStreamKey(const QByteArray& data); + void setInnerRandomStreamID(const QByteArray& data); + + QIODevice* m_device; + + Database* m_db; + QByteArray m_masterSeed; + QByteArray m_encryptionIV; + QHash m_binaryPool; +}; + +#endif // KEEPASSX_KDBX4READER_H diff --git a/src/format/Kdbx4Writer.cpp b/src/format/Kdbx4Writer.cpp new file mode 100644 index 000000000..49d04c853 --- /dev/null +++ b/src/format/Kdbx4Writer.cpp @@ -0,0 +1,326 @@ +/* + * Copyright (C) 2010 Felix Geyer + * Copyright (C) 2017 KeePassXC Team + * + * 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 "Kdbx4Writer.h" + +#include +#include +#include +#include +#include + +#include "streams/HmacBlockStream.h" +#include "core/Database.h" +#include "core/Endian.h" +#include "crypto/CryptoHash.h" +#include "crypto/Random.h" +#include "format/KeePass2RandomStream.h" +#include "format/Kdbx4XmlWriter.h" +#include "streams/QtIOCompressor" +#include "streams/SymmetricCipherStream.h" + +#define CHECK_RETURN_FALSE(x) if (!(x)) return false; + +Kdbx4Writer::Kdbx4Writer() + : m_device(nullptr) +{ +} + +bool Kdbx4Writer::writeDatabase(QIODevice* device, Database* db) +{ + m_error = false; + m_errorStr.clear(); + + SymmetricCipher::Algorithm algo = SymmetricCipher::cipherToAlgorithm(db->cipher()); + if (algo == SymmetricCipher::InvalidAlgorithm) { + raiseError("Invalid symmetric cipher algorithm."); + return false; + } + int ivSize = SymmetricCipher::algorithmIvSize(algo); + if (ivSize < 0) { + raiseError("Invalid symmetric cipher IV size."); + return false; + } + + QByteArray masterSeed = randomGen()->randomArray(32); + QByteArray encryptionIV = randomGen()->randomArray(ivSize); + QByteArray protectedStreamKey = randomGen()->randomArray(64); + QByteArray startBytes; + QByteArray endOfHeader = "\r\n\r\n"; + + if (db->challengeMasterSeed(masterSeed) == false) { + raiseError(tr("Unable to issue challenge-response.")); + return false; + } + + if (!db->setKey(db->key(), false, true)) { + raiseError(tr("Unable to calculate master key")); + return false; + } + + CryptoHash hash(CryptoHash::Sha256); + hash.addData(masterSeed); + hash.addData(db->challengeResponseKey()); + Q_ASSERT(!db->transformedMasterKey().isEmpty()); + hash.addData(db->transformedMasterKey()); + QByteArray finalKey = hash.result(); + + QByteArray headerData; + { + QBuffer header; + header.open(QIODevice::WriteOnly); + m_device = &header; + CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes(KeePass2::SIGNATURE_1, KeePass2::BYTEORDER))); + CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes(KeePass2::SIGNATURE_2, KeePass2::BYTEORDER))); + CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes(KeePass2::FILE_VERSION_4, KeePass2::BYTEORDER))); + CHECK_RETURN_FALSE(writeHeaderField(KeePass2::CipherID, db->cipher().toByteArray())); + CHECK_RETURN_FALSE(writeHeaderField(KeePass2::CompressionFlags, + Endian::sizedIntToBytes(static_cast(db->compressionAlgo()), + KeePass2::BYTEORDER))); + CHECK_RETURN_FALSE(writeHeaderField(KeePass2::MasterSeed, masterSeed)); + CHECK_RETURN_FALSE(writeHeaderField(KeePass2::EncryptionIV, encryptionIV)); + + // Convert current Kdf to basic parameters + QVariantMap kdfParams = KeePass2::kdfToParameters(db->kdf()); + + QByteArray kdfParamBytes; + if (!serializeVariantMap(kdfParams, kdfParamBytes)) { + raiseError("Failed to serialise KDF parameters variant map"); + return false; + } + QByteArray publicCustomData = db->publicCustomData(); + CHECK_RETURN_FALSE(writeHeaderField(KeePass2::KdfParameters, kdfParamBytes)); + if (!publicCustomData.isEmpty()) { + CHECK_RETURN_FALSE(writeHeaderField(KeePass2::PublicCustomData, publicCustomData)); + } + + CHECK_RETURN_FALSE(writeHeaderField(KeePass2::EndOfHeader, endOfHeader)); + header.close(); + m_device = device; + headerData = header.data(); + } + CHECK_RETURN_FALSE(writeData(headerData)); + QByteArray headerHash = CryptoHash::hash(headerData, CryptoHash::Sha256); + + QScopedPointer firstLayer, secondLayer; + + QByteArray hmacKey = KeePass2::hmacKey(masterSeed, db->transformedMasterKey()); + QByteArray headerHmac = CryptoHash::hmac(headerData, HmacBlockStream::getHmacKey(UINT64_MAX, hmacKey), + CryptoHash::Sha256); + CHECK_RETURN_FALSE(writeData(headerHash)); + CHECK_RETURN_FALSE(writeData(headerHmac)); + + HmacBlockStream* hmacStream = new HmacBlockStream(device, hmacKey); + if (!hmacStream->open(QIODevice::WriteOnly)) { + raiseError(hmacStream->errorString()); + return false; + } + firstLayer.reset(static_cast(hmacStream)); + + SymmetricCipherStream* cipherStream = new SymmetricCipherStream(hmacStream, algo, + SymmetricCipher::algorithmMode(algo), + SymmetricCipher::Encrypt); + if (!cipherStream->init(finalKey, encryptionIV)) { + raiseError(cipherStream->errorString()); + return false; + } + if (!cipherStream->open(QIODevice::WriteOnly)) { + raiseError(cipherStream->errorString()); + return false; + } + secondLayer.reset(static_cast(cipherStream)); + + QScopedPointer ioCompressor; + if (db->compressionAlgo() == Database::CompressionNone) { + m_device = secondLayer.data(); + } else { + ioCompressor.reset(new QtIOCompressor(secondLayer.data())); + ioCompressor->setStreamFormat(QtIOCompressor::GzipFormat); + if (!ioCompressor->open(QIODevice::WriteOnly)) { + raiseError(ioCompressor->errorString()); + return false; + } + m_device = ioCompressor.data(); + } + + QHash idMap; + + CHECK_RETURN_FALSE(writeInnerHeaderField(KeePass2::InnerHeaderFieldID::InnerRandomStreamID, + Endian::sizedIntToBytes(static_cast(KeePass2::ChaCha20), + KeePass2::BYTEORDER))); + CHECK_RETURN_FALSE(writeInnerHeaderField(KeePass2::InnerHeaderFieldID::InnerRandomStreamKey, + protectedStreamKey)); + const QList allEntries = db->rootGroup()->entriesRecursive(true); + int nextId = 0; + + for (Entry* entry : allEntries) { + const QList attachmentKeys = entry->attachments()->keys(); + for (const QString& key : attachmentKeys) { + QByteArray data = entry->attachments()->value(key); + if (!idMap.contains(data)) { + CHECK_RETURN_FALSE(writeBinary(data)); + idMap.insert(data, nextId++); + } + } + } + CHECK_RETURN_FALSE(writeInnerHeaderField(KeePass2::InnerHeaderFieldID::End, QByteArray())); + + KeePass2RandomStream randomStream(KeePass2::ChaCha20); + if (!randomStream.init(protectedStreamKey)) { + raiseError(randomStream.errorString()); + return false; + } + + Kdbx4XmlWriter xmlWriter(KeePass2::FILE_VERSION_4, idMap); + xmlWriter.writeDatabase(m_device, db, &randomStream, headerHash); + + // Explicitly close/reset streams so they are flushed and we can detect + // errors. QIODevice::close() resets errorString() etc. + if (ioCompressor) { + ioCompressor->close(); + } + if (!secondLayer->reset()) { + raiseError(secondLayer->errorString()); + return false; + } + if (!firstLayer->reset()) { + raiseError(firstLayer->errorString()); + return false; + } + + if (xmlWriter.hasError()) { + raiseError(xmlWriter.errorString()); + return false; + } + + return true; +} + +bool Kdbx4Writer::writeData(const QByteArray& data) +{ + if (m_device->write(data) != data.size()) { + raiseError(m_device->errorString()); + return false; + } + else { + return true; + } +} + +bool Kdbx4Writer::writeHeaderField(KeePass2::HeaderFieldID fieldId, const QByteArray& data) +{ + QByteArray fieldIdArr; + fieldIdArr[0] = fieldId; + CHECK_RETURN_FALSE(writeData(fieldIdArr)); + CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes(static_cast(data.size()), KeePass2::BYTEORDER))); + CHECK_RETURN_FALSE(writeData(data)); + + return true; +} + +bool Kdbx4Writer::writeInnerHeaderField(KeePass2::InnerHeaderFieldID fieldId, const QByteArray& data) +{ + QByteArray fieldIdArr; + fieldIdArr[0] = static_cast(fieldId); + CHECK_RETURN_FALSE(writeData(fieldIdArr)); + CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes(static_cast(data.size()), KeePass2::BYTEORDER))); + CHECK_RETURN_FALSE(writeData(data)); + + return true; +} + +bool Kdbx4Writer::writeBinary(const QByteArray& data) +{ + QByteArray fieldIdArr; + fieldIdArr[0] = static_cast(KeePass2::InnerHeaderFieldID::Binary); + CHECK_RETURN_FALSE(writeData(fieldIdArr)); + CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes(static_cast(data.size() + 1), KeePass2::BYTEORDER))); + CHECK_RETURN_FALSE(writeData(QByteArray(1, '\1'))); + CHECK_RETURN_FALSE(writeData(data)); + + return true; +} + +bool Kdbx4Writer::serializeVariantMap(const QVariantMap& p, QByteArray& o) +{ + QBuffer buf(&o); + buf.open(QIODevice::WriteOnly); + CHECK_RETURN_FALSE(buf.write(Endian::sizedIntToBytes(KeePass2::VARIANTMAP_VERSION, KeePass2::BYTEORDER)) == 2); + + bool ok; + QList keys = p.keys(); + for (int i = 0; i < keys.size(); ++i) { + QString k = keys.at(i); + KeePass2::VariantMapFieldType fieldType; + QByteArray data; + QVariant v = p.value(k); + switch (static_cast(v.type())) { + case QMetaType::Type::Int: + fieldType = KeePass2::VariantMapFieldType::Int32; + data = Endian::sizedIntToBytes(v.toInt(&ok), KeePass2::BYTEORDER); + CHECK_RETURN_FALSE(ok); + break; + case QMetaType::Type::UInt: + fieldType = KeePass2::VariantMapFieldType::UInt32; + data = Endian::sizedIntToBytes(v.toUInt(&ok), KeePass2::BYTEORDER); + CHECK_RETURN_FALSE(ok); + break; + case QMetaType::Type::LongLong: + fieldType = KeePass2::VariantMapFieldType::Int64; + data = Endian::sizedIntToBytes(v.toLongLong(&ok), KeePass2::BYTEORDER); + CHECK_RETURN_FALSE(ok); + break; + case QMetaType::Type::ULongLong: + fieldType = KeePass2::VariantMapFieldType::UInt64; + data = Endian::sizedIntToBytes(v.toULongLong(&ok), KeePass2::BYTEORDER); + CHECK_RETURN_FALSE(ok); + break; + case QMetaType::Type::QString: + fieldType = KeePass2::VariantMapFieldType::String; + data = v.toString().toUtf8(); + break; + case QMetaType::Type::Bool: + fieldType = KeePass2::VariantMapFieldType::Bool; + data = QByteArray(1, (v.toBool() ? '\1' : '\0')); + break; + case QMetaType::Type::QByteArray: + fieldType = KeePass2::VariantMapFieldType::ByteArray; + data = v.toByteArray(); + break; + default: + qWarning("Unknown object type %d in QVariantMap", v.type()); + return false; + } + QByteArray typeBytes; + typeBytes[0] = static_cast(fieldType); + QByteArray nameBytes = k.toUtf8(); + QByteArray nameLenBytes = Endian::sizedIntToBytes(nameBytes.size(), KeePass2::BYTEORDER); + QByteArray dataLenBytes = Endian::sizedIntToBytes(data.size(), KeePass2::BYTEORDER); + + CHECK_RETURN_FALSE(buf.write(typeBytes) == 1); + CHECK_RETURN_FALSE(buf.write(nameLenBytes) == 4); + CHECK_RETURN_FALSE(buf.write(nameBytes) == nameBytes.size()); + CHECK_RETURN_FALSE(buf.write(dataLenBytes) == 4); + CHECK_RETURN_FALSE(buf.write(data) == data.size()); + } + + QByteArray endBytes; + endBytes[0] = static_cast(KeePass2::VariantMapFieldType::End); + CHECK_RETURN_FALSE(buf.write(endBytes) == 1); + return true; +} diff --git a/src/format/Kdbx4Writer.h b/src/format/Kdbx4Writer.h new file mode 100644 index 000000000..4e703324d --- /dev/null +++ b/src/format/Kdbx4Writer.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2010 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_KDBX4WRITER_H +#define KEEPASSX_KDBX4WRITER_H + +#include + +#include "format/KeePass2.h" +#include "format/KeePass2Writer.h" +#include "keys/CompositeKey.h" + +class Database; +class QIODevice; + +class Kdbx4Writer : public BaseKeePass2Writer +{ + Q_DECLARE_TR_FUNCTIONS(Kdbx4Writer) + +public: + Kdbx4Writer(); + + using BaseKeePass2Writer::writeDatabase; + bool writeDatabase(QIODevice* device, Database* db); + +private: + bool writeData(const QByteArray& data); + bool writeHeaderField(KeePass2::HeaderFieldID fieldId, const QByteArray& data); + bool writeInnerHeaderField(KeePass2::InnerHeaderFieldID fieldId, const QByteArray& data); + + QIODevice* m_device; + + bool writeBinary(const QByteArray& data); + + static bool serializeVariantMap(const QVariantMap& p, QByteArray& o); +}; + +#endif // KEEPASSX_KDBX4WRITER_H diff --git a/src/format/Kdbx4XmlReader.cpp b/src/format/Kdbx4XmlReader.cpp new file mode 100644 index 000000000..10dfe6475 --- /dev/null +++ b/src/format/Kdbx4XmlReader.cpp @@ -0,0 +1,1080 @@ +/* + * Copyright (C) 2010 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 "Kdbx4XmlReader.h" + +#include +#include +#include + +#include "core/Endian.h" +#include "core/Database.h" +#include "core/DatabaseIcons.h" +#include "core/Group.h" +#include "core/Metadata.h" +#include "core/Tools.h" +#include "format/KeePass2RandomStream.h" +#include "streams/QtIOCompressor" + +typedef QPair StringPair; + +Kdbx4XmlReader::Kdbx4XmlReader() + : m_randomStream(nullptr) + , m_db(nullptr) + , m_meta(nullptr) + , m_tmpParent(nullptr) + , m_error(false) + , m_strictMode(false) +{ +} + +Kdbx4XmlReader::Kdbx4XmlReader(QHash& binaryPool) + : Kdbx4XmlReader() +{ + m_binaryPool = binaryPool; +} + +void Kdbx4XmlReader::setStrictMode(bool strictMode) +{ + m_strictMode = strictMode; +} + +void Kdbx4XmlReader::readDatabase(QIODevice* device, Database* db, KeePass2RandomStream* randomStream) +{ + m_error = false; + m_errorStr.clear(); + + m_xml.clear(); + m_xml.setDevice(device); + + m_db = db; + m_meta = m_db->metadata(); + m_meta->setUpdateDatetime(false); + + m_randomStream = randomStream; + m_headerHash.clear(); + + m_tmpParent = new Group(); + + bool rootGroupParsed = false; + + if (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "KeePassFile") { + rootGroupParsed = parseKeePassFile(); + } + } + + if (!m_xml.hasError() && !rootGroupParsed) { + raiseError("No root group"); + } + + if (!m_xml.hasError()) { + if (!m_tmpParent->children().isEmpty()) { + qWarning("Kdbx4XmlReader::readDatabase: found %d invalid group reference(s)", + m_tmpParent->children().size()); + } + + if (!m_tmpParent->entries().isEmpty()) { + qWarning("Kdbx4XmlReader::readDatabase: found %d invalid entry reference(s)", + m_tmpParent->children().size()); + } + } + + const QSet poolKeys = m_binaryPool.keys().toSet(); + const QSet entryKeys = m_binaryMap.keys().toSet(); + const QSet unmappedKeys = entryKeys - poolKeys; + const QSet unusedKeys = poolKeys - entryKeys; + + if (!unmappedKeys.isEmpty()) { + raiseError("Unmapped keys left."); + } + + if (!m_xml.hasError()) { + for (const QString& key : unusedKeys) { + qWarning("Kdbx4XmlReader::readDatabase: found unused key \"%s\"", qPrintable(key)); + } + } + + QHash >::const_iterator i; + for (i = m_binaryMap.constBegin(); i != m_binaryMap.constEnd(); ++i) { + const QPair& target = i.value(); + target.first->attachments()->set(target.second, m_binaryPool[i.key()]); + } + + m_meta->setUpdateDatetime(true); + + QHash::const_iterator iGroup; + for (iGroup = m_groups.constBegin(); iGroup != m_groups.constEnd(); ++iGroup) { + iGroup.value()->setUpdateTimeinfo(true); + } + + QHash::const_iterator iEntry; + for (iEntry = m_entries.constBegin(); iEntry != m_entries.constEnd(); ++iEntry) { + iEntry.value()->setUpdateTimeinfo(true); + + const QList historyItems = iEntry.value()->historyItems(); + for (Entry* histEntry : historyItems) { + histEntry->setUpdateTimeinfo(true); + } + } + + delete m_tmpParent; +} + +Database* Kdbx4XmlReader::readDatabase(QIODevice* device) +{ + Database* db = new Database(); + readDatabase(device, db); + return db; +} + +Database* Kdbx4XmlReader::readDatabase(const QString& filename) +{ + QFile file(filename); + file.open(QIODevice::ReadOnly); + return readDatabase(&file); +} + +bool Kdbx4XmlReader::hasError() +{ + return m_error || m_xml.hasError(); +} + +QString Kdbx4XmlReader::errorString() +{ + if (m_error) { + return m_errorStr; + } else if (m_xml.hasError()) { + return QString("XML error:\n%1\nLine %2, column %3") + .arg(m_xml.errorString()) + .arg(m_xml.lineNumber()) + .arg(m_xml.columnNumber()); + } else { + return QString(); + } +} + +void Kdbx4XmlReader::raiseError(const QString& errorMessage) +{ + m_error = true; + m_errorStr = errorMessage; +} + +QByteArray Kdbx4XmlReader::headerHash() +{ + return m_headerHash; +} + +bool Kdbx4XmlReader::parseKeePassFile() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "KeePassFile"); + + bool rootElementFound = false; + bool rootParsedSuccessfully = false; + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "Meta") { + parseMeta(); + } else if (m_xml.name() == "Root") { + if (rootElementFound) { + rootParsedSuccessfully = false; + raiseError("Multiple root elements"); + } else { + rootParsedSuccessfully = parseRoot(); + rootElementFound = true; + } + } else { + skipCurrentElement(); + } + } + + return rootParsedSuccessfully; +} + +void Kdbx4XmlReader::parseMeta() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Meta"); + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + 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()); + } else if (m_xml.name() == "DatabaseNameChanged") { + m_meta->setNameChanged(readDateTime()); + } else if (m_xml.name() == "DatabaseDescription") { + m_meta->setDescription(readString()); + } else if (m_xml.name() == "DatabaseDescriptionChanged") { + m_meta->setDescriptionChanged(readDateTime()); + } else if (m_xml.name() == "DefaultUserName") { + m_meta->setDefaultUserName(readString()); + } else if (m_xml.name() == "DefaultUserNameChanged") { + m_meta->setDefaultUserNameChanged(readDateTime()); + } else if (m_xml.name() == "MaintenanceHistoryDays") { + m_meta->setMaintenanceHistoryDays(readNumber()); + } else if (m_xml.name() == "Color") { + m_meta->setColor(readColor()); + } else if (m_xml.name() == "MasterKeyChanged") { + m_meta->setMasterKeyChanged(readDateTime()); + } else if (m_xml.name() == "MasterKeyChangeRec") { + m_meta->setMasterKeyChangeRec(readNumber()); + } else if (m_xml.name() == "MasterKeyChangeForce") { + m_meta->setMasterKeyChangeForce(readNumber()); + } else if (m_xml.name() == "MemoryProtection") { + parseMemoryProtection(); + } else if (m_xml.name() == "CustomIcons") { + parseCustomIcons(); + } else if (m_xml.name() == "RecycleBinEnabled") { + m_meta->setRecycleBinEnabled(readBool()); + } else if (m_xml.name() == "RecycleBinUUID") { + m_meta->setRecycleBin(getGroup(readUuid())); + } else if (m_xml.name() == "RecycleBinChanged") { + m_meta->setRecycleBinChanged(readDateTime()); + } else if (m_xml.name() == "EntryTemplatesGroup") { + m_meta->setEntryTemplatesGroup(getGroup(readUuid())); + } else if (m_xml.name() == "EntryTemplatesGroupChanged") { + m_meta->setEntryTemplatesGroupChanged(readDateTime()); + } else if (m_xml.name() == "LastSelectedGroup") { + m_meta->setLastSelectedGroup(getGroup(readUuid())); + } else if (m_xml.name() == "LastTopVisibleGroup") { + m_meta->setLastTopVisibleGroup(getGroup(readUuid())); + } else if (m_xml.name() == "HistoryMaxItems") { + int value = readNumber(); + if (value >= -1) { + m_meta->setHistoryMaxItems(value); + } else { + raiseError("HistoryMaxItems invalid number"); + } + } else if (m_xml.name() == "HistoryMaxSize") { + int value = readNumber(); + if (value >= -1) { + m_meta->setHistoryMaxSize(value); + } else { + raiseError("HistoryMaxSize invalid number"); + } + } else if (m_xml.name() == "Binaries") { + parseBinaries(); + } else if (m_xml.name() == "CustomData") { + parseCustomData(); + } else if (m_xml.name() == "SettingsChanged") { + m_meta->setSettingsChanged(readDateTime()); + } else { + skipCurrentElement(); + } + } +} + +void Kdbx4XmlReader::parseMemoryProtection() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "MemoryProtection"); + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "ProtectTitle") { + m_meta->setProtectTitle(readBool()); + } else if (m_xml.name() == "ProtectUserName") { + m_meta->setProtectUsername(readBool()); + } else if (m_xml.name() == "ProtectPassword") { + m_meta->setProtectPassword(readBool()); + } else if (m_xml.name() == "ProtectURL") { + m_meta->setProtectUrl(readBool()); + } else if (m_xml.name() == "ProtectNotes") { + m_meta->setProtectNotes(readBool()); + } else { + skipCurrentElement(); + } + } +} + +void Kdbx4XmlReader::parseCustomIcons() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "CustomIcons"); + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "Icon") { + parseIcon(); + } else { + skipCurrentElement(); + } + } +} + +void Kdbx4XmlReader::parseIcon() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Icon"); + + Uuid uuid; + QImage icon; + bool uuidSet = false; + bool iconSet = false; + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "UUID") { + uuid = readUuid(); + uuidSet = !uuid.isNull(); + } else if (m_xml.name() == "Data") { + icon.loadFromData(readBinary()); + iconSet = true; + } else { + skipCurrentElement(); + } + } + + if (uuidSet && iconSet) { + m_meta->addCustomIcon(uuid, icon); + } else { + raiseError("Missing icon uuid or data"); + } +} + +void Kdbx4XmlReader::parseBinaries() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Binaries"); + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "Binary") { + QXmlStreamAttributes attr = m_xml.attributes(); + + QString id = attr.value("ID").toString(); + + QByteArray data; + if (attr.value("Compressed").compare(QLatin1String("True"), Qt::CaseInsensitive) == 0) { + data = readCompressedBinary(); + } else { + data = readBinary(); + } + + if (m_binaryPool.contains(id)) { + qWarning("Kdbx4XmlReader::parseBinaries: overwriting binary item \"%s\"", + qPrintable(id)); + } + + m_binaryPool.insert(id, data); + } else { + skipCurrentElement(); + } + } +} + +void Kdbx4XmlReader::parseCustomData() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "CustomData"); + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "Item") { + parseCustomDataItem(); + } else { + skipCurrentElement(); + } + } +} + +void Kdbx4XmlReader::parseCustomDataItem() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Item"); + + QString key; + QString value; + bool keySet = false; + bool valueSet = false; + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "Key") { + key = readString(); + keySet = true; + } else if (m_xml.name() == "Value") { + value = readString(); + valueSet = true; + } else { + skipCurrentElement(); + } + } + + if (keySet && valueSet) { + m_meta->addCustomField(key, value); + } else { + raiseError("Missing custom data key or value"); + } +} + +bool Kdbx4XmlReader::parseRoot() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Root"); + + bool groupElementFound = false; + bool groupParsedSuccessfully = false; + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "Group") { + if (groupElementFound) { + groupParsedSuccessfully = false; + raiseError("Multiple group elements"); + continue; + } + + Group* rootGroup = parseGroup(); + if (rootGroup) { + Group* oldRoot = m_db->rootGroup(); + m_db->setRootGroup(rootGroup); + delete oldRoot; + groupParsedSuccessfully = true; + } + + groupElementFound = true; + } else if (m_xml.name() == "DeletedObjects") { + parseDeletedObjects(); + } else { + skipCurrentElement(); + } + } + + return groupParsedSuccessfully; +} + +Group* Kdbx4XmlReader::parseGroup() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Group"); + + Group* group = new Group(); + group->setUpdateTimeinfo(false); + QList children; + QList entries; + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "UUID") { + Uuid uuid = readUuid(); + if (uuid.isNull()) { + if (m_strictMode) { + raiseError("Null group uuid"); + } else { + group->setUuid(Uuid::random()); + } + } else { + group->setUuid(uuid); + } + } else if (m_xml.name() == "Name") { + group->setName(readString()); + } else if (m_xml.name() == "Notes") { + group->setNotes(readString()); + } else if (m_xml.name() == "IconID") { + int iconId = readNumber(); + if (iconId < 0) { + if (m_strictMode) { + raiseError("Invalid group icon number"); + } + iconId = 0; + } else if (iconId >= DatabaseIcons::IconCount) { + qWarning("Kdbx4XmlReader::parseGroup: icon id \"%d\" not supported", iconId); + iconId = DatabaseIcons::IconCount - 1; + } + + group->setIcon(iconId); + } else if (m_xml.name() == "CustomIconUUID") { + Uuid uuid = readUuid(); + if (!uuid.isNull()) { + group->setIcon(uuid); + } + } else if (m_xml.name() == "Times") { + group->setTimeInfo(parseTimes()); + } else if (m_xml.name() == "IsExpanded") { + group->setExpanded(readBool()); + } else if (m_xml.name() == "DefaultAutoTypeSequence") { + group->setDefaultAutoTypeSequence(readString()); + } else if (m_xml.name() == "EnableAutoType") { + QString str = readString(); + + if (str.compare("null", Qt::CaseInsensitive) == 0) { + group->setAutoTypeEnabled(Group::Inherit); + } else if (str.compare("true", Qt::CaseInsensitive) == 0) { + group->setAutoTypeEnabled(Group::Enable); + } else if (str.compare("false", Qt::CaseInsensitive) == 0) { + group->setAutoTypeEnabled(Group::Disable); + } else { + raiseError("Invalid EnableAutoType value"); + } + } else if (m_xml.name() == "EnableSearching") { + QString str = readString(); + + if (str.compare("null", Qt::CaseInsensitive) == 0) { + group->setSearchingEnabled(Group::Inherit); + } else if (str.compare("true", Qt::CaseInsensitive) == 0) { + group->setSearchingEnabled(Group::Enable); + } else if (str.compare("false", Qt::CaseInsensitive) == 0) { + group->setSearchingEnabled(Group::Disable); + } else { + raiseError("Invalid EnableSearching value"); + } + } else if (m_xml.name() == "LastTopVisibleEntry") { + group->setLastTopVisibleEntry(getEntry(readUuid())); + } else if (m_xml.name() == "Group") { + Group* newGroup = parseGroup(); + if (newGroup) { + children.append(newGroup); + } + } else if (m_xml.name() == "Entry") { + Entry* newEntry = parseEntry(false); + if (newEntry) { + entries.append(newEntry); + } + } else { + skipCurrentElement(); + } + } + + if (group->uuid().isNull() && !m_strictMode) { + group->setUuid(Uuid::random()); + } + + if (!group->uuid().isNull()) { + Group* tmpGroup = group; + group = getGroup(tmpGroup->uuid()); + group->copyDataFrom(tmpGroup); + group->setUpdateTimeinfo(false); + delete tmpGroup; + } else if (!hasError()) { + raiseError("No group uuid found"); + } + + for (Group* child : asConst(children)) { + child->setParent(group); + } + + for (Entry* entry : asConst(entries)) { + entry->setGroup(group); + } + + return group; +} + +void Kdbx4XmlReader::parseDeletedObjects() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "DeletedObjects"); + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "DeletedObject") { + parseDeletedObject(); + } else { + skipCurrentElement(); + } + } +} + +void Kdbx4XmlReader::parseDeletedObject() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "DeletedObject"); + + DeletedObject delObj; + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "UUID") { + Uuid uuid = readUuid(); + if (uuid.isNull()) { + if (m_strictMode) { + raiseError("Null DeleteObject uuid"); + } + } else { + delObj.uuid = uuid; + } + } else if (m_xml.name() == "DeletionTime") { + delObj.deletionTime = readDateTime(); + } else { + skipCurrentElement(); + } + } + + if (!delObj.uuid.isNull() && !delObj.deletionTime.isNull()) { + m_db->addDeletedObject(delObj); + } else if (m_strictMode) { + raiseError("Missing DeletedObject uuid or time"); + } +} + +Entry* Kdbx4XmlReader::parseEntry(bool history) +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Entry"); + + Entry* entry = new Entry(); + entry->setUpdateTimeinfo(false); + QList historyItems; + QList binaryRefs; + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "UUID") { + Uuid uuid = readUuid(); + if (uuid.isNull()) { + if (m_strictMode) { + raiseError("Null entry uuid"); + } else { + entry->setUuid(Uuid::random()); + } + } else { + entry->setUuid(uuid); + } + } else if (m_xml.name() == "IconID") { + int iconId = readNumber(); + if (iconId < 0) { + if (m_strictMode) { + raiseError("Invalid entry icon number"); + } + iconId = 0; + } + entry->setIcon(iconId); + } else if (m_xml.name() == "CustomIconUUID") { + Uuid uuid = readUuid(); + if (!uuid.isNull()) { + entry->setIcon(uuid); + } + } else if (m_xml.name() == "ForegroundColor") { + entry->setForegroundColor(readColor()); + } else if (m_xml.name() == "BackgroundColor") { + entry->setBackgroundColor(readColor()); + } else if (m_xml.name() == "OverrideURL") { + entry->setOverrideUrl(readString()); + } else if (m_xml.name() == "Tags") { + entry->setTags(readString()); + } else if (m_xml.name() == "Times") { + entry->setTimeInfo(parseTimes()); + } else if (m_xml.name() == "String") { + parseEntryString(entry); + } else if (m_xml.name() == "Binary") { + QPair ref = parseEntryBinary(entry); + if (!ref.first.isNull() && !ref.second.isNull()) { + binaryRefs.append(ref); + } + } else if (m_xml.name() == "AutoType") { + parseAutoType(entry); + } else if (m_xml.name() == "History") { + if (history) { + raiseError("History element in history entry"); + } else { + historyItems = parseEntryHistory(); + } + } else { + skipCurrentElement(); + } + } + + if (entry->uuid().isNull() && !m_strictMode) { + entry->setUuid(Uuid::random()); + } + + if (!entry->uuid().isNull()) { + if (history) { + entry->setUpdateTimeinfo(false); + } else { + Entry* tmpEntry = entry; + + entry = getEntry(tmpEntry->uuid()); + entry->copyDataFrom(tmpEntry); + entry->setUpdateTimeinfo(false); + + delete tmpEntry; + } + } else if (!hasError()) { + raiseError("No entry uuid found"); + } + + for (Entry* historyItem : asConst(historyItems)) { + if (historyItem->uuid() != entry->uuid()) { + if (m_strictMode) { + raiseError("History element with different uuid"); + } else { + historyItem->setUuid(entry->uuid()); + } + } + entry->addHistoryItem(historyItem); + } + + for (const StringPair& ref : asConst(binaryRefs)) { + m_binaryMap.insertMulti(ref.first, qMakePair(entry, ref.second)); + } + + return entry; +} + +void Kdbx4XmlReader::parseEntryString(Entry* entry) +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "String"); + + QString key; + QString value; + bool protect = false; + bool keySet = false; + bool valueSet = false; + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "Key") { + key = readString(); + keySet = true; + } else if (m_xml.name() == "Value") { + QXmlStreamAttributes attr = m_xml.attributes(); + value = readString(); + + bool isProtected = attr.value("Protected") == "True"; + bool protectInMemory = attr.value("ProtectInMemory") == "True"; + + if (isProtected && !value.isEmpty()) { + if (m_randomStream) { + QByteArray ciphertext = QByteArray::fromBase64(value.toLatin1()); + bool ok; + QByteArray plaintext = m_randomStream->process(ciphertext, &ok); + if (!ok) { + value.clear(); + raiseError(m_randomStream->errorString()); + } else { + value = QString::fromUtf8(plaintext); + } + } else { + raiseError("Unable to decrypt entry string"); + } + } + + protect = isProtected || protectInMemory; + valueSet = true; + } else { + skipCurrentElement(); + } + } + + if (keySet && valueSet) { + // the default attributes are always there so additionally check if it's empty + if (entry->attributes()->hasKey(key) && !entry->attributes()->value(key).isEmpty()) { + raiseError("Duplicate custom attribute found"); + } else { + entry->attributes()->set(key, value, protect); + } + } else { + raiseError("Entry string key or value missing"); + } +} + +QPair Kdbx4XmlReader::parseEntryBinary(Entry* entry) +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Binary"); + + QPair poolRef; + + QString key; + QByteArray value; + bool keySet = false; + bool valueSet = false; + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "Key") { + key = readString(); + keySet = true; + } else if (m_xml.name() == "Value") { + QXmlStreamAttributes attr = m_xml.attributes(); + + if (attr.hasAttribute("Ref")) { + poolRef = qMakePair(attr.value("Ref").toString(), key); + m_xml.skipCurrentElement(); + } else { + // format compatibility + value = readBinary(); + bool isProtected = attr.hasAttribute("Protected") + && (attr.value("Protected") == "True"); + + if (isProtected && !value.isEmpty()) { + if (!m_randomStream->processInPlace(value)) { + raiseError(m_randomStream->errorString()); + } + } + } + + valueSet = true; + } else { + skipCurrentElement(); + } + } + + if (keySet && valueSet) { + if (entry->attachments()->hasKey(key)) { + raiseError("Duplicate attachment found"); + } else { + entry->attachments()->set(key, value); + } + } else { + raiseError("Entry binary key or value missing"); + } + + return poolRef; +} + +void Kdbx4XmlReader::parseAutoType(Entry* entry) +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "AutoType"); + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "Enabled") { + entry->setAutoTypeEnabled(readBool()); + } else if (m_xml.name() == "DataTransferObfuscation") { + entry->setAutoTypeObfuscation(readNumber()); + } else if (m_xml.name() == "DefaultSequence") { + entry->setDefaultAutoTypeSequence(readString()); + } else if (m_xml.name() == "Association") { + parseAutoTypeAssoc(entry); + } else { + skipCurrentElement(); + } + } +} + +void Kdbx4XmlReader::parseAutoTypeAssoc(Entry* entry) +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Association"); + + AutoTypeAssociations::Association assoc; + bool windowSet = false; + bool sequenceSet = false; + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "Window") { + assoc.window = readString(); + windowSet = true; + } else if (m_xml.name() == "KeystrokeSequence") { + assoc.sequence = readString(); + sequenceSet = true; + } else { + skipCurrentElement(); + } + } + + if (windowSet && sequenceSet) { + entry->autoTypeAssociations()->add(assoc); + } else { + raiseError("Auto-type association window or sequence missing"); + } +} + +QList Kdbx4XmlReader::parseEntryHistory() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "History"); + + QList historyItems; + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "Entry") { + historyItems.append(parseEntry(true)); + } else { + skipCurrentElement(); + } + } + + return historyItems; +} + +TimeInfo Kdbx4XmlReader::parseTimes() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Times"); + + TimeInfo timeInfo; + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "LastModificationTime") { + timeInfo.setLastModificationTime(readDateTime()); + } else if (m_xml.name() == "CreationTime") { + timeInfo.setCreationTime(readDateTime()); + } else if (m_xml.name() == "LastAccessTime") { + timeInfo.setLastAccessTime(readDateTime()); + } else if (m_xml.name() == "ExpiryTime") { + timeInfo.setExpiryTime(readDateTime()); + } else if (m_xml.name() == "Expires") { + timeInfo.setExpires(readBool()); + } else if (m_xml.name() == "UsageCount") { + timeInfo.setUsageCount(readNumber()); + } else if (m_xml.name() == "LocationChanged") { + timeInfo.setLocationChanged(readDateTime()); + } else { + skipCurrentElement(); + } + } + + return timeInfo; +} + +QString Kdbx4XmlReader::readString() +{ + return m_xml.readElementText(); +} + +bool Kdbx4XmlReader::readBool() +{ + QString str = readString(); + + if (str.compare("True", Qt::CaseInsensitive) == 0) { + return true; + } else if (str.compare("False", Qt::CaseInsensitive) == 0) { + return false; + } else if (str.length() == 0) { + return false; + } else { + raiseError("Invalid bool value"); + return false; + } +} + +QDateTime Kdbx4XmlReader::readDateTime() +{ + static QRegularExpression b64regex("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"); + QString str = readString(); + + if (b64regex.match(str).hasMatch()) { + QByteArray secsBytes = QByteArray::fromBase64(str.toUtf8()).leftJustified(8, '\0', true).left(8); + qint64 secs = Endian::bytesToSizedInt(secsBytes, KeePass2::BYTEORDER); + return QDateTime(QDate(1, 1, 1), QTime(0, 0, 0, 0), Qt::UTC).addSecs(secs); + } else { + QDateTime dt = QDateTime::fromString(str, Qt::ISODate); + if (dt.isValid()) { + return dt; + } else { + if (m_strictMode) { + raiseError("Invalid date time value"); + } + + return QDateTime::currentDateTimeUtc(); + } + } +} + +QColor Kdbx4XmlReader::readColor() +{ + QString colorStr = readString(); + + if (colorStr.isEmpty()) { + return QColor(); + } + + if (colorStr.length() != 7 || colorStr[0] != '#') { + if (m_strictMode) { + raiseError("Invalid color value"); + } + return QColor(); + } + + QColor color; + for (int i = 0; i <= 2; i++) { + QString rgbPartStr = colorStr.mid(1 + 2*i, 2); + bool ok; + int rgbPart = rgbPartStr.toInt(&ok, 16); + if (!ok || rgbPart > 255) { + if (m_strictMode) { + raiseError("Invalid color rgb part"); + } + return QColor(); + } + + if (i == 0) { + color.setRed(rgbPart); + } else if (i == 1) { + color.setGreen(rgbPart); + } else { + color.setBlue(rgbPart); + } + } + + return color; +} + +int Kdbx4XmlReader::readNumber() +{ + bool ok; + int result = readString().toInt(&ok); + if (!ok) { + raiseError("Invalid number value"); + } + return result; +} + +Uuid Kdbx4XmlReader::readUuid() +{ + QByteArray uuidBin = readBinary(); + if (uuidBin.isEmpty()) { + return Uuid(); + } else if (uuidBin.length() != Uuid::Length) { + if (m_strictMode) { + raiseError("Invalid uuid value"); + } + return Uuid(); + } else { + return Uuid(uuidBin); + } +} + +QByteArray Kdbx4XmlReader::readBinary() +{ + return QByteArray::fromBase64(readString().toLatin1()); +} + +QByteArray Kdbx4XmlReader::readCompressedBinary() +{ + QByteArray rawData = readBinary(); + + QBuffer buffer(&rawData); + buffer.open(QIODevice::ReadOnly); + + QtIOCompressor compressor(&buffer); + compressor.setStreamFormat(QtIOCompressor::GzipFormat); + compressor.open(QIODevice::ReadOnly); + + QByteArray result; + if (!Tools::readAllFromDevice(&compressor, result)) { + raiseError("Unable to decompress binary"); + } + return result; +} + +Group* Kdbx4XmlReader::getGroup(const Uuid& uuid) +{ + if (uuid.isNull()) { + return nullptr; + } + + if (m_groups.contains(uuid)) { + return m_groups.value(uuid); + } else { + Group* group = new Group(); + group->setUpdateTimeinfo(false); + group->setUuid(uuid); + group->setParent(m_tmpParent); + m_groups.insert(uuid, group); + return group; + } +} + +Entry* Kdbx4XmlReader::getEntry(const Uuid& uuid) +{ + if (uuid.isNull()) { + return nullptr; + } + + if (m_entries.contains(uuid)) { + return m_entries.value(uuid); + } else { + Entry* entry = new Entry(); + entry->setUpdateTimeinfo(false); + entry->setUuid(uuid); + entry->setGroup(m_tmpParent); + m_entries.insert(uuid, entry); + return entry; + } +} + +void Kdbx4XmlReader::skipCurrentElement() +{ + qWarning("Kdbx4XmlReader::skipCurrentElement: skip element \"%s\"", qPrintable(m_xml.name().toString())); + m_xml.skipCurrentElement(); +} diff --git a/src/format/Kdbx4XmlReader.h b/src/format/Kdbx4XmlReader.h new file mode 100644 index 000000000..6a0a6d4f4 --- /dev/null +++ b/src/format/Kdbx4XmlReader.h @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2010 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_KDBX4XMLREADER_H +#define KEEPASSX_KDBX4XMLREADER_H + +#include +#include +#include +#include +#include +#include + +#include "core/TimeInfo.h" +#include "core/Uuid.h" + +class Database; +class Entry; +class Group; +class KeePass2RandomStream; +class Metadata; + +class Kdbx4XmlReader +{ + Q_DECLARE_TR_FUNCTIONS(Kdbx4XmlReader) + +public: + Kdbx4XmlReader(); + Kdbx4XmlReader(QHash& binaryPool); + Database* readDatabase(QIODevice* device); + void readDatabase(QIODevice* device, Database* db, KeePass2RandomStream* randomStream = nullptr); + Database* readDatabase(const QString& filename); + bool hasError(); + QString errorString(); + QByteArray headerHash(); + void setStrictMode(bool strictMode); + +private: + bool parseKeePassFile(); + void parseMeta(); + void parseMemoryProtection(); + void parseCustomIcons(); + void parseIcon(); + void parseBinaries(); + void parseCustomData(); + void parseCustomDataItem(); + bool parseRoot(); + Group* parseGroup(); + void parseDeletedObjects(); + void parseDeletedObject(); + Entry* parseEntry(bool history); + void parseEntryString(Entry* entry); + QPair parseEntryBinary(Entry* entry); + void parseAutoType(Entry* entry); + void parseAutoTypeAssoc(Entry* entry); + QList parseEntryHistory(); + TimeInfo parseTimes(); + + QString readString(); + bool readBool(); + QDateTime readDateTime(); + QColor readColor(); + int readNumber(); + Uuid readUuid(); + QByteArray readBinary(); + QByteArray readCompressedBinary(); + + Group* getGroup(const Uuid& uuid); + Entry* getEntry(const Uuid& uuid); + void raiseError(const QString& errorMessage); + void skipCurrentElement(); + + QXmlStreamReader m_xml; + KeePass2RandomStream* m_randomStream; + Database* m_db; + Metadata* m_meta; + Group* m_tmpParent; + QHash m_groups; + QHash m_entries; + QHash m_binaryPool; + QHash > m_binaryMap; + QByteArray m_headerHash; + bool m_error; + QString m_errorStr; + bool m_strictMode; +}; + +#endif // KEEPASSX_KDBX4XMLREADER_H diff --git a/src/format/Kdbx4XmlWriter.cpp b/src/format/Kdbx4XmlWriter.cpp new file mode 100644 index 000000000..374744563 --- /dev/null +++ b/src/format/Kdbx4XmlWriter.cpp @@ -0,0 +1,611 @@ +/* + * Copyright (C) 2010 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 "Kdbx4XmlWriter.h" + +#include +#include + +#include "core/Endian.h" +#include "core/Metadata.h" +#include "format/KeePass2RandomStream.h" +#include "streams/QtIOCompressor" + +Kdbx4XmlWriter::Kdbx4XmlWriter() + : Kdbx4XmlWriter(KeePass2::FILE_VERSION_3) +{ +} + +Kdbx4XmlWriter::Kdbx4XmlWriter(quint32 version) + : Kdbx4XmlWriter(version, QHash()) +{ +} + +Kdbx4XmlWriter::Kdbx4XmlWriter(quint32 version, QHash idMap) + : m_db(nullptr) + , m_meta(nullptr) + , m_randomStream(nullptr) + , m_idMap(idMap) + , m_error(false) + , m_version(version) +{ + m_xml.setAutoFormatting(true); + m_xml.setAutoFormattingIndent(-1); // 1 tab + m_xml.setCodec("UTF-8"); +} + +void Kdbx4XmlWriter::writeDatabase(QIODevice* device, Database* db, KeePass2RandomStream* randomStream, const QByteArray& headerHash) +{ + m_db = db; + m_meta = db->metadata(); + m_randomStream = randomStream; + m_headerHash = headerHash; + + if (m_version < KeePass2::FILE_VERSION_4 && m_idMap.isEmpty()) { + generateIdMap(); + } + + m_xml.setDevice(device); + + m_xml.writeStartDocument("1.0", true); + + m_xml.writeStartElement("KeePassFile"); + + writeMetadata(); + writeRoot(); + + m_xml.writeEndElement(); + + m_xml.writeEndDocument(); + + if (m_xml.hasError()) { + raiseError(device->errorString()); + } +} + +void Kdbx4XmlWriter::writeDatabase(const QString& filename, Database* db) +{ + QFile file(filename); + file.open(QIODevice::WriteOnly|QIODevice::Truncate); + writeDatabase(&file, db); +} + +bool Kdbx4XmlWriter::hasError() +{ + return m_error; +} + +QString Kdbx4XmlWriter::errorString() +{ + return m_errorStr; +} + +void Kdbx4XmlWriter::generateIdMap() +{ + const QList allEntries = m_db->rootGroup()->entriesRecursive(true); + int nextId = 0; + + for (Entry* entry : allEntries) { + const QList attachmentKeys = entry->attachments()->keys(); + for (const QString& key : attachmentKeys) { + QByteArray data = entry->attachments()->value(key); + if (!m_idMap.contains(data)) { + m_idMap.insert(data, nextId++); + } + } + } +} + +void Kdbx4XmlWriter::writeMetadata() +{ + m_xml.writeStartElement("Meta"); + writeString("Generator", m_meta->generator()); + if (m_version < KeePass2::FILE_VERSION_4 && !m_headerHash.isEmpty()) { + writeBinary("HeaderHash", m_headerHash); + } + writeString("DatabaseName", m_meta->name()); + writeDateTime("DatabaseNameChanged", m_meta->nameChanged()); + writeString("DatabaseDescription", m_meta->description()); + writeDateTime("DatabaseDescriptionChanged", m_meta->descriptionChanged()); + writeString("DefaultUserName", m_meta->defaultUserName()); + writeDateTime("DefaultUserNameChanged", m_meta->defaultUserNameChanged()); + writeNumber("MaintenanceHistoryDays", m_meta->maintenanceHistoryDays()); + writeColor("Color", m_meta->color()); + writeDateTime("MasterKeyChanged", m_meta->masterKeyChanged()); + writeNumber("MasterKeyChangeRec", m_meta->masterKeyChangeRec()); + writeNumber("MasterKeyChangeForce", m_meta->masterKeyChangeForce()); + writeMemoryProtection(); + writeCustomIcons(); + writeBool("RecycleBinEnabled", m_meta->recycleBinEnabled()); + writeUuid("RecycleBinUUID", m_meta->recycleBin()); + writeDateTime("RecycleBinChanged", m_meta->recycleBinChanged()); + writeUuid("EntryTemplatesGroup", m_meta->entryTemplatesGroup()); + writeDateTime("EntryTemplatesGroupChanged", m_meta->entryTemplatesGroupChanged()); + writeUuid("LastSelectedGroup", m_meta->lastSelectedGroup()); + writeUuid("LastTopVisibleGroup", m_meta->lastTopVisibleGroup()); + writeNumber("HistoryMaxItems", m_meta->historyMaxItems()); + writeNumber("HistoryMaxSize", m_meta->historyMaxSize()); + if (m_version >= KeePass2::FILE_VERSION_4) { + writeDateTime("SettingsChanged", m_meta->settingsChanged()); + } + if (m_version < KeePass2::FILE_VERSION_4) { + writeBinaries(); + } + writeCustomData(); + + m_xml.writeEndElement(); +} + +void Kdbx4XmlWriter::writeMemoryProtection() +{ + m_xml.writeStartElement("MemoryProtection"); + + writeBool("ProtectTitle", m_meta->protectTitle()); + writeBool("ProtectUserName", m_meta->protectUsername()); + writeBool("ProtectPassword", m_meta->protectPassword()); + writeBool("ProtectURL", m_meta->protectUrl()); + writeBool("ProtectNotes", m_meta->protectNotes()); + + m_xml.writeEndElement(); +} + +void Kdbx4XmlWriter::writeCustomIcons() +{ + m_xml.writeStartElement("CustomIcons"); + + const QList customIconsOrder = m_meta->customIconsOrder(); + for (const Uuid& uuid : customIconsOrder) { + writeIcon(uuid, m_meta->customIcon(uuid)); + } + + m_xml.writeEndElement(); +} + +void Kdbx4XmlWriter::writeIcon(const Uuid& uuid, const QImage& icon) +{ + m_xml.writeStartElement("Icon"); + + writeUuid("UUID", uuid); + + QByteArray ba; + QBuffer buffer(&ba); + buffer.open(QIODevice::WriteOnly); + // TODO: check !icon.save() + icon.save(&buffer, "PNG"); + buffer.close(); + writeBinary("Data", ba); + + m_xml.writeEndElement(); +} + +void Kdbx4XmlWriter::writeBinaries() +{ + m_xml.writeStartElement("Binaries"); + + QHash::const_iterator i; + for (i = m_idMap.constBegin(); i != m_idMap.constEnd(); ++i) { + m_xml.writeStartElement("Binary"); + + m_xml.writeAttribute("ID", QString::number(i.value())); + + QByteArray data; + if (m_db->compressionAlgo() == Database::CompressionGZip) { + m_xml.writeAttribute("Compressed", "True"); + + QBuffer buffer; + buffer.open(QIODevice::ReadWrite); + + QtIOCompressor compressor(&buffer); + compressor.setStreamFormat(QtIOCompressor::GzipFormat); + compressor.open(QIODevice::WriteOnly); + + qint64 bytesWritten = compressor.write(i.key()); + Q_ASSERT(bytesWritten == i.key().size()); + Q_UNUSED(bytesWritten); + compressor.close(); + + buffer.seek(0); + data = buffer.readAll(); + } + else { + data = i.key(); + } + + if (!data.isEmpty()) { + m_xml.writeCharacters(QString::fromLatin1(data.toBase64())); + } + m_xml.writeEndElement(); + } + + m_xml.writeEndElement(); +} + +void Kdbx4XmlWriter::writeCustomData() +{ + m_xml.writeStartElement("CustomData"); + + QHash customFields = m_meta->customFields(); + const QList keyList = customFields.keys(); + for (const QString& key : keyList) { + writeCustomDataItem(key, customFields.value(key)); + } + + m_xml.writeEndElement(); +} + +void Kdbx4XmlWriter::writeCustomDataItem(const QString& key, const QString& value) +{ + m_xml.writeStartElement("Item"); + + writeString("Key", key); + writeString("Value", value); + + m_xml.writeEndElement(); +} + +void Kdbx4XmlWriter::writeRoot() +{ + Q_ASSERT(m_db->rootGroup()); + + m_xml.writeStartElement("Root"); + + writeGroup(m_db->rootGroup()); + writeDeletedObjects(); + + m_xml.writeEndElement(); +} + +void Kdbx4XmlWriter::writeGroup(const Group* group) +{ + Q_ASSERT(!group->uuid().isNull()); + + m_xml.writeStartElement("Group"); + + writeUuid("UUID", group->uuid()); + writeString("Name", group->name()); + writeString("Notes", group->notes()); + writeNumber("IconID", group->iconNumber()); + + if (!group->iconUuid().isNull()) { + writeUuid("CustomIconUUID", group->iconUuid()); + } + writeTimes(group->timeInfo()); + writeBool("IsExpanded", group->isExpanded()); + writeString("DefaultAutoTypeSequence", group->defaultAutoTypeSequence()); + + writeTriState("EnableAutoType", group->autoTypeEnabled()); + + writeTriState("EnableSearching", group->searchingEnabled()); + + writeUuid("LastTopVisibleEntry", group->lastTopVisibleEntry()); + + const QList entryList = group->entries(); + for (const Entry* entry : entryList) { + writeEntry(entry); + } + + const QList children = group->children(); + for (const Group* child : children) { + writeGroup(child); + } + + m_xml.writeEndElement(); +} + +void Kdbx4XmlWriter::writeTimes(const TimeInfo& ti) +{ + m_xml.writeStartElement("Times"); + + writeDateTime("LastModificationTime", ti.lastModificationTime()); + writeDateTime("CreationTime", ti.creationTime()); + writeDateTime("LastAccessTime", ti.lastAccessTime()); + writeDateTime("ExpiryTime", ti.expiryTime()); + writeBool("Expires", ti.expires()); + writeNumber("UsageCount", ti.usageCount()); + writeDateTime("LocationChanged", ti.locationChanged()); + + m_xml.writeEndElement(); +} + +void Kdbx4XmlWriter::writeDeletedObjects() +{ + m_xml.writeStartElement("DeletedObjects"); + + const QList delObjList = m_db->deletedObjects(); + for (const DeletedObject& delObj : delObjList) { + writeDeletedObject(delObj); + } + + m_xml.writeEndElement(); +} + +void Kdbx4XmlWriter::writeDeletedObject(const DeletedObject& delObj) +{ + m_xml.writeStartElement("DeletedObject"); + + writeUuid("UUID", delObj.uuid); + writeDateTime("DeletionTime", delObj.deletionTime); + + m_xml.writeEndElement(); +} + +void Kdbx4XmlWriter::writeEntry(const Entry* entry) +{ + Q_ASSERT(!entry->uuid().isNull()); + + m_xml.writeStartElement("Entry"); + + writeUuid("UUID", entry->uuid()); + writeNumber("IconID", entry->iconNumber()); + if (!entry->iconUuid().isNull()) { + writeUuid("CustomIconUUID", entry->iconUuid()); + } + writeColor("ForegroundColor", entry->foregroundColor()); + writeColor("BackgroundColor", entry->backgroundColor()); + writeString("OverrideURL", entry->overrideUrl()); + writeString("Tags", entry->tags()); + writeTimes(entry->timeInfo()); + + const QList attributesKeyList = entry->attributes()->keys(); + for (const QString& key : attributesKeyList) { + m_xml.writeStartElement("String"); + + bool protect = ( ((key == "Title") && m_meta->protectTitle()) || + ((key == "UserName") && m_meta->protectUsername()) || + ((key == "Password") && m_meta->protectPassword()) || + ((key == "URL") && m_meta->protectUrl()) || + ((key == "Notes") && m_meta->protectNotes()) || + entry->attributes()->isProtected(key) ); + + writeString("Key", key); + + m_xml.writeStartElement("Value"); + QString value; + + if (protect) { + if (m_randomStream) { + m_xml.writeAttribute("Protected", "True"); + bool ok; + QByteArray rawData = m_randomStream->process(entry->attributes()->value(key).toUtf8(), &ok); + if (!ok) { + raiseError(m_randomStream->errorString()); + } + value = QString::fromLatin1(rawData.toBase64()); + } + else { + m_xml.writeAttribute("ProtectInMemory", "True"); + value = entry->attributes()->value(key); + } + } + else { + value = entry->attributes()->value(key); + } + + if (!value.isEmpty()) { + m_xml.writeCharacters(stripInvalidXml10Chars(value)); + } + m_xml.writeEndElement(); + + m_xml.writeEndElement(); + } + + const QList attachmentsKeyList = entry->attachments()->keys(); + for (const QString& key : attachmentsKeyList) { + m_xml.writeStartElement("Binary"); + + writeString("Key", key); + + m_xml.writeStartElement("Value"); + m_xml.writeAttribute("Ref", QString::number(m_idMap[entry->attachments()->value(key)])); + m_xml.writeEndElement(); + + m_xml.writeEndElement(); + } + + writeAutoType(entry); + // write history only for entries that are not history items + if (entry->parent()) { + writeEntryHistory(entry); + } + + m_xml.writeEndElement(); +} + +void Kdbx4XmlWriter::writeAutoType(const Entry* entry) +{ + m_xml.writeStartElement("AutoType"); + + writeBool("Enabled", entry->autoTypeEnabled()); + writeNumber("DataTransferObfuscation", entry->autoTypeObfuscation()); + writeString("DefaultSequence", entry->defaultAutoTypeSequence()); + + const QList autoTypeAssociations = entry->autoTypeAssociations()->getAll(); + for (const AutoTypeAssociations::Association& assoc : autoTypeAssociations) { + writeAutoTypeAssoc(assoc); + } + + m_xml.writeEndElement(); +} + +void Kdbx4XmlWriter::writeAutoTypeAssoc(const AutoTypeAssociations::Association& assoc) +{ + m_xml.writeStartElement("Association"); + + writeString("Window", assoc.window); + writeString("KeystrokeSequence", assoc.sequence); + + m_xml.writeEndElement(); +} + +void Kdbx4XmlWriter::writeEntryHistory(const Entry* entry) +{ + m_xml.writeStartElement("History"); + + const QList& historyItems = entry->historyItems(); + for (const Entry* item : historyItems) { + writeEntry(item); + } + + m_xml.writeEndElement(); +} + +void Kdbx4XmlWriter::writeString(const QString& qualifiedName, const QString& string) +{ + if (string.isEmpty()) { + m_xml.writeEmptyElement(qualifiedName); + } + else { + m_xml.writeTextElement(qualifiedName, stripInvalidXml10Chars(string)); + } +} + +void Kdbx4XmlWriter::writeNumber(const QString& qualifiedName, int number) +{ + writeString(qualifiedName, QString::number(number)); +} + +void Kdbx4XmlWriter::writeBool(const QString& qualifiedName, bool b) +{ + if (b) { + writeString(qualifiedName, "True"); + } + else { + writeString(qualifiedName, "False"); + } +} + +void Kdbx4XmlWriter::writeDateTime(const QString& qualifiedName, const QDateTime& dateTime) +{ + Q_ASSERT(dateTime.isValid()); + Q_ASSERT(dateTime.timeSpec() == Qt::UTC); + + QString dateTimeStr; + if (m_version < KeePass2::FILE_VERSION_4) { + dateTimeStr = dateTime.toString(Qt::ISODate); + + // Qt < 4.8 doesn't append a 'Z' at the end + if (!dateTimeStr.isEmpty() && dateTimeStr[dateTimeStr.size() - 1] != 'Z') { + dateTimeStr.append('Z'); + } + } else { + qint64 secs = QDateTime(QDate(1, 1, 1), QTime(0, 0, 0, 0), Qt::UTC).secsTo(dateTime); + QByteArray secsBytes = Endian::sizedIntToBytes(secs, KeePass2::BYTEORDER); + dateTimeStr = QString::fromLatin1(secsBytes.toBase64()); + } + writeString(qualifiedName, dateTimeStr); +} + +void Kdbx4XmlWriter::writeUuid(const QString& qualifiedName, const Uuid& uuid) +{ + writeString(qualifiedName, uuid.toBase64()); +} + +void Kdbx4XmlWriter::writeUuid(const QString& qualifiedName, const Group* group) +{ + if (group) { + writeUuid(qualifiedName, group->uuid()); + } + else { + writeUuid(qualifiedName, Uuid()); + } +} + +void Kdbx4XmlWriter::writeUuid(const QString& qualifiedName, const Entry* entry) +{ + if (entry) { + writeUuid(qualifiedName, entry->uuid()); + } + else { + writeUuid(qualifiedName, Uuid()); + } +} + +void Kdbx4XmlWriter::writeBinary(const QString& qualifiedName, const QByteArray& ba) +{ + writeString(qualifiedName, QString::fromLatin1(ba.toBase64())); +} + +void Kdbx4XmlWriter::writeColor(const QString& qualifiedName, const QColor& color) +{ + QString colorStr; + + if (color.isValid()) { + colorStr = QString("#%1%2%3").arg(colorPartToString(color.red()), + colorPartToString(color.green()), + colorPartToString(color.blue())); + } + + writeString(qualifiedName, colorStr); +} + +void Kdbx4XmlWriter::writeTriState(const QString& qualifiedName, Group::TriState triState) +{ + QString value; + + if (triState == Group::Inherit) { + value = "null"; + } + else if (triState == Group::Enable) { + value = "true"; + } + else { + value = "false"; + } + + writeString(qualifiedName, value); +} + +QString Kdbx4XmlWriter::colorPartToString(int value) +{ + QString str = QString::number(value, 16).toUpper(); + if (str.length() == 1) { + str.prepend("0"); + } + + return str; +} + +QString Kdbx4XmlWriter::stripInvalidXml10Chars(QString str) +{ + for (int i = str.size() - 1; i >= 0; i--) { + const QChar ch = str.at(i); + const ushort uc = ch.unicode(); + + if (ch.isLowSurrogate() && i != 0 && str.at(i - 1).isHighSurrogate()) { + // keep valid surrogate pair + i--; + } + else if ((uc < 0x20 && uc != 0x09 && uc != 0x0A && uc != 0x0D) // control characters + || (uc >= 0x7F && uc <= 0x84) // control characters, valid but discouraged by XML + || (uc >= 0x86 && uc <= 0x9F) // control characters, valid but discouraged by XML + || (uc > 0xFFFD) // noncharacter + || ch.isLowSurrogate() // single low surrogate + || ch.isHighSurrogate()) // single high surrogate + { + qWarning("Stripping invalid XML 1.0 codepoint %x", uc); + str.remove(i, 1); + } + } + + return str; +} + +void Kdbx4XmlWriter::raiseError(const QString& errorMessage) +{ + m_error = true; + m_errorStr = errorMessage; +} diff --git a/src/format/Kdbx4XmlWriter.h b/src/format/Kdbx4XmlWriter.h new file mode 100644 index 000000000..79f27c98b --- /dev/null +++ b/src/format/Kdbx4XmlWriter.h @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2010 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_KDBX4XMLWRITER_H +#define KEEPASSX_KDBX4XMLWRITER_H + +#include +#include +#include +#include + +#include "core/Database.h" +#include "core/Entry.h" +#include "core/Group.h" +#include "core/TimeInfo.h" +#include "core/Uuid.h" + +class KeePass2RandomStream; +class Metadata; + +class Kdbx4XmlWriter +{ +public: + Kdbx4XmlWriter(); + Kdbx4XmlWriter(quint32 version); + Kdbx4XmlWriter(quint32 version, QHash idMap); + void writeDatabase(QIODevice* device, Database* db, KeePass2RandomStream* randomStream = nullptr, + const QByteArray& headerHash = QByteArray()); + void writeDatabase(const QString& filename, Database* db); + bool hasError(); + QString errorString(); + +private: + void generateIdMap(); + + void writeMetadata(); + void writeMemoryProtection(); + void writeCustomIcons(); + void writeIcon(const Uuid& uuid, const QImage& icon); + void writeBinaries(); + void writeCustomData(); + void writeCustomDataItem(const QString& key, const QString& value); + void writeRoot(); + void writeGroup(const Group* group); + void writeTimes(const TimeInfo& ti); + void writeDeletedObjects(); + void writeDeletedObject(const DeletedObject& delObj); + void writeEntry(const Entry* entry); + void writeAutoType(const Entry* entry); + void writeAutoTypeAssoc(const AutoTypeAssociations::Association& assoc); + void writeEntryHistory(const Entry* entry); + + void writeString(const QString& qualifiedName, const QString& string); + void writeNumber(const QString& qualifiedName, int number); + void writeBool(const QString& qualifiedName, bool b); + void writeDateTime(const QString& qualifiedName, const QDateTime& dateTime); + void writeUuid(const QString& qualifiedName, const Uuid& uuid); + void writeUuid(const QString& qualifiedName, const Group* group); + void writeUuid(const QString& qualifiedName, const Entry* entry); + void writeBinary(const QString& qualifiedName, const QByteArray& ba); + void writeColor(const QString& qualifiedName, const QColor& color); + void writeTriState(const QString& qualifiedName, Group::TriState triState); + QString colorPartToString(int value); + QString stripInvalidXml10Chars(QString str); + + void raiseError(const QString& errorMessage); + + QXmlStreamWriter m_xml; + Database* m_db; + Metadata* m_meta; + KeePass2RandomStream* m_randomStream; + QHash m_idMap; + bool m_error; + QString m_errorStr; + quint32 m_version; + QByteArray m_headerHash; +}; + +#endif // KEEPASSX_KDBX4XMLWRITER_H diff --git a/src/format/KeePass2.cpp b/src/format/KeePass2.cpp index fd57148d0..f89e828a1 100644 --- a/src/format/KeePass2.cpp +++ b/src/format/KeePass2.cpp @@ -19,6 +19,7 @@ #include #include "crypto/kdf/AesKdf.h" #include "crypto/kdf/Argon2Kdf.h" +#include "crypto/CryptoHash.h" const Uuid KeePass2::CIPHER_AES = Uuid(QByteArray::fromHex("31c1f2e6bf714350be5805216afc5aff")); const Uuid KeePass2::CIPHER_TWOFISH = Uuid(QByteArray::fromHex("ad68f29f576f4bb9a36ad47af965346c")); @@ -29,6 +30,19 @@ const Uuid KeePass2::KDF_ARGON2 = Uuid(QByteArray::fromHex("EF636DDF8C29444B91F7 const QByteArray KeePass2::INNER_STREAM_SALSA20_IV("\xE8\x30\x09\x4B\x97\x20\x5D\x2A"); +const QString KeePass2::KDFPARAM_UUID("$UUID"); +// AES parameters +const QString KeePass2::KDFPARAM_AES_ROUNDS("R"); +const QString KeePass2::KDFPARAM_AES_SEED("S"); +// Argon2 parameters +const QString KeePass2::KDFPARAM_ARGON2_SALT("S"); +const QString KeePass2::KDFPARAM_ARGON2_PARALLELISM("P"); +const QString KeePass2::KDFPARAM_ARGON2_MEMORY("M"); +const QString KeePass2::KDFPARAM_ARGON2_ITERATIONS("I"); +const QString KeePass2::KDFPARAM_ARGON2_VERSION("V"); +const QString KeePass2::KDFPARAM_ARGON2_SECRET("K"); +const QString KeePass2::KDFPARAM_ARGON2_ASSOCDATA("A"); + const QList> KeePass2::CIPHERS { qMakePair(KeePass2::CIPHER_AES, QObject::tr("AES: 256-bit")), qMakePair(KeePass2::CIPHER_TWOFISH, QObject::tr("Twofish: 256-bit")), @@ -39,6 +53,38 @@ const QList> KeePass2::KDFS { qMakePair(KeePass2::KDF_ARGON2, QObject::tr("Argon2")), }; +QByteArray KeePass2::hmacKey(QByteArray masterSeed, QByteArray transformedMasterKey) { + CryptoHash hmacKeyHash(CryptoHash::Sha512); + hmacKeyHash.addData(masterSeed); + hmacKeyHash.addData(transformedMasterKey); + hmacKeyHash.addData(QByteArray(1, '\x01')); + return hmacKeyHash.result(); +} + +QSharedPointer KeePass2::kdfFromParameters(const QVariantMap &p) +{ + QByteArray uuidBytes = p.value(KDFPARAM_UUID).toByteArray(); + if (uuidBytes.size() != Uuid::Length) { + return nullptr; + } + + QSharedPointer kdf(uuidToKdf(Uuid(uuidBytes))); + if (kdf.isNull()) { + return nullptr; + } + + if (!kdf->processParameters(p)) { + return nullptr; + } + + return kdf; +} + +QVariantMap KeePass2::kdfToParameters(QSharedPointer kdf) +{ + return kdf->writeParameters(); +} + QSharedPointer KeePass2::uuidToKdf(const Uuid& uuid) { if (uuid == KDF_AES) { diff --git a/src/format/KeePass2.h b/src/format/KeePass2.h index 99bc5a0b0..cdc594f5a 100644 --- a/src/format/KeePass2.h +++ b/src/format/KeePass2.h @@ -19,6 +19,8 @@ #define KEEPASSX_KEEPASS2_H #include +#include +#include #include #include "crypto/SymmetricCipher.h" @@ -29,9 +31,14 @@ namespace KeePass2 { const quint32 SIGNATURE_1 = 0x9AA2D903; const quint32 SIGNATURE_2 = 0xB54BFB67; - const quint32 FILE_VERSION = 0x00030001; + const quint32 FILE_VERSION_MIN = 0x00020000; const quint32 FILE_VERSION_CRITICAL_MASK = 0xFFFF0000; + const quint32 FILE_VERSION_4 = 0x00040000; + const quint32 FILE_VERSION_3 = 0x00030001; + + const quint16 VARIANTMAP_VERSION = 0x0100; + const quint16 VARIANTMAP_CRITICAL_MASK = 0xFF00; const QSysInfo::Endian BYTEORDER = QSysInfo::LittleEndian; @@ -44,6 +51,17 @@ namespace KeePass2 extern const QByteArray INNER_STREAM_SALSA20_IV; + extern const QString KDFPARAM_UUID; + extern const QString KDFPARAM_AES_ROUNDS; + extern const QString KDFPARAM_AES_SEED; + extern const QString KDFPARAM_ARGON2_SALT; + extern const QString KDFPARAM_ARGON2_PARALLELISM; + extern const QString KDFPARAM_ARGON2_MEMORY; + extern const QString KDFPARAM_ARGON2_ITERATIONS; + extern const QString KDFPARAM_ARGON2_VERSION; + extern const QString KDFPARAM_ARGON2_SECRET; + extern const QString KDFPARAM_ARGON2_ASSOCDATA; + extern const QList> CIPHERS; extern const QList> KDFS; @@ -59,7 +77,17 @@ namespace KeePass2 EncryptionIV = 7, ProtectedStreamKey = 8, StreamStartBytes = 9, - InnerRandomStreamID = 10 + InnerRandomStreamID = 10, + KdfParameters = 11, + PublicCustomData = 12 + }; + + enum class InnerHeaderFieldID : quint8 + { + End = 0, + InnerRandomStreamID = 1, + InnerRandomStreamKey = 2, + Binary = 3 }; enum ProtectedStreamAlgo @@ -70,7 +98,33 @@ namespace KeePass2 InvalidProtectedStreamAlgo = -1 }; + enum class VariantMapFieldType : quint8 + { + End = 0, + // Byte = 0x02, + // UInt16 = 0x03, + UInt32 = 0x04, + UInt64 = 0x05, + // Signed mask: 0x08 + Bool = 0x08, + // SByte = 0x0A, + // Int16 = 0x0B, + Int32 = 0x0C, + Int64 = 0x0D, + // Float = 0x10, + // Double = 0x11, + // Decimal = 0x12, + // Char = 0x17, // 16-bit Unicode character + String = 0x18, + // Array mask: 0x40 + ByteArray = 0x42 + }; + + QByteArray hmacKey(QByteArray masterSeed, QByteArray transformedMasterKey); + QSharedPointer kdfFromParameters(const QVariantMap &p); + QVariantMap kdfToParameters(QSharedPointer kdf); QSharedPointer uuidToKdf(const Uuid& uuid); + Uuid kdfToUuid(QSharedPointer kdf); ProtectedStreamAlgo idToProtectedStreamAlgo(quint32 id); } diff --git a/src/format/KeePass2Reader.cpp b/src/format/KeePass2Reader.cpp index daa6c9aa8..0a04c79c6 100644 --- a/src/format/KeePass2Reader.cpp +++ b/src/format/KeePass2Reader.cpp @@ -25,6 +25,7 @@ #include "format/KeePass1.h" #include "format/KeePass2.h" #include "format/Kdbx3Reader.h" +#include "format/Kdbx4Reader.h" BaseKeePass2Reader::BaseKeePass2Reader() : m_error(false) @@ -118,14 +119,21 @@ Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& ke m_version = Endian::readSizedInt(device, KeePass2::BYTEORDER, &ok) & KeePass2::FILE_VERSION_CRITICAL_MASK; - quint32 maxVersion = KeePass2::FILE_VERSION & KeePass2::FILE_VERSION_CRITICAL_MASK; + quint32 maxVersion = KeePass2::FILE_VERSION_4 & KeePass2::FILE_VERSION_CRITICAL_MASK; if (!ok || (m_version < KeePass2::FILE_VERSION_MIN) || (m_version > maxVersion)) { raiseError(tr("Unsupported KeePass 2 database version.")); return nullptr; } device->seek(0); - m_reader.reset(static_cast(new Kdbx3Reader())); + + // Determine KDBX3 vs KDBX4 + if (m_version < KeePass2::FILE_VERSION_4) { + m_reader.reset(new Kdbx3Reader()); + } else { + m_reader.reset(new Kdbx4Reader()); + } + m_reader->setSaveXml(m_saveXml); return m_reader->readDatabase(device, key, keepDatabase); } @@ -159,3 +167,8 @@ quint32 KeePass2Reader::version() const { return m_version; } + +QSharedPointer KeePass2Reader::reader() +{ + return m_reader; +} \ No newline at end of file diff --git a/src/format/KeePass2Reader.h b/src/format/KeePass2Reader.h index 93348f565..fd28db2b7 100644 --- a/src/format/KeePass2Reader.h +++ b/src/format/KeePass2Reader.h @@ -70,11 +70,12 @@ public: QString errorString() override; QByteArray xmlData() override; QByteArray streamKey() override; + QSharedPointer reader(); KeePass2::ProtectedStreamAlgo protectedStreamAlgo() const override; quint32 version() const; private: - QScopedPointer m_reader; + QSharedPointer m_reader; quint32 m_version; }; diff --git a/src/format/KeePass2Repair.cpp b/src/format/KeePass2Repair.cpp index 0e79fa8ba..fdaa45d62 100644 --- a/src/format/KeePass2Repair.cpp +++ b/src/format/KeePass2Repair.cpp @@ -25,7 +25,9 @@ #include "format/KeePass2.h" #include "format/KeePass2RandomStream.h" #include "format/KeePass2Reader.h" +#include "format/Kdbx4Reader.h" #include "format/Kdbx3XmlReader.h" +#include "format/Kdbx4XmlReader.h" KeePass2Repair::RepairOutcome KeePass2Repair::repairDatabase(QIODevice* device, const CompositeKey& key) { @@ -74,12 +76,23 @@ KeePass2Repair::RepairOutcome KeePass2Repair::repairDatabase(QIODevice* device, KeePass2RandomStream randomStream(reader.protectedStreamAlgo()); randomStream.init(reader.streamKey()); - Kdbx3XmlReader xmlReader; + bool hasError; + QBuffer buffer(&xmlData); buffer.open(QIODevice::ReadOnly); - xmlReader.readDatabase(&buffer, db.data(), &randomStream); + if ((reader.version() & KeePass2::FILE_VERSION_CRITICAL_MASK) < KeePass2::FILE_VERSION_4) { + Kdbx3XmlReader xmlReader; + xmlReader.readDatabase(&buffer, db.data(), &randomStream); + hasError = xmlReader.hasError(); + } else { + auto reader4 = reader.reader().staticCast(); + QHash pool = reader4->binaryPool(); + Kdbx4XmlReader xmlReader(pool); + xmlReader.readDatabase(&buffer, db.data(), &randomStream); + hasError = xmlReader.hasError(); + } - if (xmlReader.hasError()) { + if (hasError) { return qMakePair(RepairFailed, nullptr); } else { diff --git a/src/format/KeePass2Writer.cpp b/src/format/KeePass2Writer.cpp index 00392dc05..baea9968a 100644 --- a/src/format/KeePass2Writer.cpp +++ b/src/format/KeePass2Writer.cpp @@ -22,6 +22,7 @@ #include "format/KeePass2Writer.h" #include "core/Database.h" #include "format/Kdbx3Writer.h" +#include "format/Kdbx4Writer.h" BaseKeePass2Writer::BaseKeePass2Writer() : m_error(false) { @@ -67,6 +68,22 @@ QString KeePass2Writer::errorString() } bool KeePass2Writer::writeDatabase(QIODevice* device, Database* db) { - m_writer.reset(static_cast(new Kdbx3Writer())); + bool useKdbx4 = false; + + if (db->kdf()->uuid() != KeePass2::KDF_AES) { + useKdbx4 = true; + } + + if (db->publicCustomData().size() > 0) { + useKdbx4 = true; + } + + // Determine KDBX3 vs KDBX4 + if (useKdbx4) { + m_writer.reset(new Kdbx4Writer()); + } else { + m_writer.reset(new Kdbx3Writer()); + } + return m_writer->writeDatabase(device, db); } diff --git a/src/gui/DatabaseSettingsWidget.cpp b/src/gui/DatabaseSettingsWidget.cpp index 2f4fe177d..b5c1ad459 100644 --- a/src/gui/DatabaseSettingsWidget.cpp +++ b/src/gui/DatabaseSettingsWidget.cpp @@ -105,7 +105,7 @@ void DatabaseSettingsWidget::load(Database* db) m_ui->transformRoundsSpinBox->setValue(kdf->rounds()); if (kdfUuid == KeePass2::KDF_ARGON2) { auto argon2Kdf = kdf.staticCast(); - m_ui->memorySpinBox->setValue(argon2Kdf->memory()); + m_ui->memorySpinBox->setValue(argon2Kdf->memory() / (1<<10)); m_ui->parallelismSpinBox->setValue(argon2Kdf->parallelism()); } @@ -120,6 +120,7 @@ void DatabaseSettingsWidget::save() meta->setDescription(m_ui->dbDescriptionEdit->text()); meta->setDefaultUserName(m_ui->defaultUsernameEdit->text()); meta->setRecycleBinEnabled(m_ui->recycleBinEnabledCheckBox->isChecked()); + meta->setSettingsChanged(QDateTime::currentDateTimeUtc()); bool truncate = false; @@ -156,7 +157,7 @@ void DatabaseSettingsWidget::save() kdf->setRounds(m_ui->transformRoundsSpinBox->value()); if (kdf->uuid() == KeePass2::KDF_ARGON2) { auto argon2Kdf = kdf.staticCast(); - argon2Kdf->setMemory(m_ui->memorySpinBox->value()); + argon2Kdf->setMemory(m_ui->memorySpinBox->value() * (1<<10)); argon2Kdf->setParallelism(m_ui->parallelismSpinBox->value()); } @@ -189,8 +190,12 @@ void DatabaseSettingsWidget::transformRoundsBenchmark() kdf->setRounds(m_ui->transformRoundsSpinBox->value()); if (kdf->uuid() == KeePass2::KDF_ARGON2) { auto argon2Kdf = kdf.staticCast(); - argon2Kdf->setMemory(m_ui->memorySpinBox->value()); - argon2Kdf->setParallelism(m_ui->parallelismSpinBox->value()); + if (!argon2Kdf->setMemory(m_ui->memorySpinBox->value() * (1<<10))) { + m_ui->memorySpinBox->setValue(argon2Kdf->memory() / (1<<10)); + } + if (!argon2Kdf->setParallelism(m_ui->parallelismSpinBox->value())) { + m_ui->parallelismSpinBox->setValue(argon2Kdf->parallelism()); + } } // Determine the number of rounds required to meet 1 second delay diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 5325ed3de..6ea82b330 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -809,7 +809,7 @@ void DatabaseWidget::updateMasterKey(bool accepted) if (accepted) { QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); - bool result = m_db->setKey(m_changeMasterKeyWidget->newMasterKey()); + bool result = m_db->setKey(m_changeMasterKeyWidget->newMasterKey(), true, true); QApplication::restoreOverrideCursor(); if (!result) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 91c6b2e2e..4472fc27a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -110,6 +110,9 @@ add_unit_test(NAME testgroup SOURCES TestGroup.cpp add_unit_test(NAME testkdbx3xmlreader SOURCES TestKeePass2XmlReader.cpp TestKdbx3XmlReader.cpp LIBS ${TEST_LIBRARIES}) +add_unit_test(NAME testkdbx4xmlreader SOURCES TestKeePass2XmlReader.cpp TestKdbx4XmlReader.cpp + LIBS ${TEST_LIBRARIES}) + add_unit_test(NAME testkeys SOURCES TestKeys.cpp LIBS ${TEST_LIBRARIES}) diff --git a/tests/TestKdbx4XmlReader.cpp b/tests/TestKdbx4XmlReader.cpp new file mode 100644 index 000000000..c1a0b42ee --- /dev/null +++ b/tests/TestKdbx4XmlReader.cpp @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2017 KeePassXC Team + * + * 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 + +#include "TestKeePass2XmlReader.h" + +QTEST_GUILESS_MAIN(TestKdbx4XmlReader) diff --git a/tests/TestKeePass2Reader.cpp b/tests/TestKeePass2Reader.cpp index 22973ee00..86dc6db2c 100644 --- a/tests/TestKeePass2Reader.cpp +++ b/tests/TestKeePass2Reader.cpp @@ -155,3 +155,26 @@ void TestKeePass2Reader::testFormat300() delete db; } + +void TestKeePass2Reader::testFormat400() +{ + QString filename = QString(KEEPASSX_TEST_DATA_DIR).append("/Format400.kdbx"); + CompositeKey key; + key.addKey(PasswordKey("t")); + KeePass2Reader reader; + Database* db = reader.readDatabase(filename, key); + QVERIFY(db); + QVERIFY(!reader.hasError()); + + QCOMPARE(db->rootGroup()->name(), QString("Format400")); + QCOMPARE(db->metadata()->name(), QString("Format400")); + QCOMPARE(db->rootGroup()->entries().size(), 1); + Entry* entry = db->rootGroup()->entries().at(0); + + QCOMPARE(entry->title(), QString("Format400")); + QCOMPARE(entry->username(), QString("Format400")); + QCOMPARE(entry->attributes()->keys().size(), 6); + QCOMPARE(entry->attributes()->value("Format400"), QString("Format400")); + QCOMPARE(entry->attachments()->keys().size(), 1); + QCOMPARE(entry->attachments()->value("Format400"), QByteArray("Format400\n")); +} diff --git a/tests/TestKeePass2Reader.h b/tests/TestKeePass2Reader.h index 76ffe0297..6ba9b0dc1 100644 --- a/tests/TestKeePass2Reader.h +++ b/tests/TestKeePass2Reader.h @@ -32,6 +32,7 @@ private slots: void testBrokenHeaderHash(); void testFormat200(); void testFormat300(); + void testFormat400(); }; #endif // KEEPASSX_TESTKEEPASS2READER_H diff --git a/tests/TestKeePass2XmlReader.cpp b/tests/TestKeePass2XmlReader.cpp index c1e3b4b63..fda8fffd6 100644 --- a/tests/TestKeePass2XmlReader.cpp +++ b/tests/TestKeePass2XmlReader.cpp @@ -27,6 +27,8 @@ #include "crypto/Crypto.h" #include "format/Kdbx3XmlReader.h" #include "format/Kdbx3XmlWriter.h" +#include "format/Kdbx4XmlReader.h" +#include "format/Kdbx4XmlWriter.h" #include "config-keepassx-tests.h" namespace QTest { @@ -89,6 +91,18 @@ void TestKdbx3XmlReader::initTestCase() QVERIFY(!reader.hasError()); } +void TestKdbx4XmlReader::initTestCase() +{ + QVERIFY(Crypto::init()); + + Kdbx4XmlReader reader; + reader.setStrictMode(true); + QString xmlFile = QString(KEEPASSX_TEST_DATA_DIR).append("/NewDatabase.xml"); + m_db = reader.readDatabase(xmlFile); + QVERIFY(m_db); + QVERIFY(!reader.hasError()); +} + void TestKdbx3XmlReader::readDatabase(QString path, bool strictMode, Database*& db, bool& hasError, QString& errorString) { Kdbx3XmlReader reader; @@ -115,6 +129,32 @@ void TestKdbx3XmlReader::writeDatabase(QBuffer* buf, Database* db, bool& hasErro errorString = writer.errorString(); } +void TestKdbx4XmlReader::readDatabase(QString path, bool strictMode, Database*& db, bool& hasError, QString& errorString) +{ + Kdbx4XmlReader reader; + reader.setStrictMode(strictMode); + db = reader.readDatabase(path); + hasError = reader.hasError(); + errorString = reader.errorString(); +} + +void TestKdbx4XmlReader::readDatabase(QBuffer* buf, bool strictMode, Database*& db, bool& hasError, QString& errorString) +{ + Kdbx4XmlReader reader; + reader.setStrictMode(strictMode); + db = reader.readDatabase(buf); + hasError = reader.hasError(); + errorString = reader.errorString(); +} + +void TestKdbx4XmlReader::writeDatabase(QBuffer* buf, Database* db, bool& hasError, QString& errorString) +{ + Kdbx4XmlWriter writer; + writer.writeDatabase(buf, db); + hasError = writer.hasError(); + errorString = writer.errorString(); +} + void TestKeePass2XmlReader::testMetadata() { QCOMPARE(m_db->metadata()->generator(), QString("KeePass")); diff --git a/tests/TestKeePass2XmlReader.h b/tests/TestKeePass2XmlReader.h index 2ce122235..e07f575b3 100644 --- a/tests/TestKeePass2XmlReader.h +++ b/tests/TestKeePass2XmlReader.h @@ -70,4 +70,17 @@ protected: virtual void writeDatabase(QBuffer* buf, Database* db, bool& hasError, QString& errorString) override; }; +class TestKdbx4XmlReader : public TestKeePass2XmlReader +{ + Q_OBJECT + +private slots: + virtual void initTestCase() override; + +protected: + virtual void readDatabase(QBuffer* buf, bool strictMode, Database*& db, bool& hasError, QString& errorString) override; + virtual void readDatabase(QString path, bool strictMode, Database*& db, bool& hasError, QString& errorString) override; + virtual void writeDatabase(QBuffer* buf, Database* db, bool& hasError, QString& errorString) override; +}; + #endif // KEEPASSX_TESTKEEPASS2XMLREADER_H diff --git a/tests/data/Format400.kdbx b/tests/data/Format400.kdbx new file mode 100644 index 0000000000000000000000000000000000000000..1a877508e838a68870fc8ba6420b0e6b55aead72 GIT binary patch literal 1801 zcmV+k2ln^_*`k_f`%AR|00aO65C8xG)&q(wi*HP|r6e<&G26A80|Wp70096100bZa z002AsdBAy7CEGE7ne=tFS@D(EGicgUEE1q$pu)ND%M%NW000000YU`;001OaRY^n; z0002*V{PAzDMU+=_o<`<;|dG}0RR91Rs;Y5022TJ00jX6002n{000020000000005 z0RR91O$Y!00000G0000000aR5002+~000020000&0RR91Qy>5U09LF9Z7qf>u*D1j z_7plao%BKS=8+32eW;I6!@~mEr2q#E0000pKZj78Z*q{0Xp}Gj1ONa44GIkk(A|W+ z?(w4C8xGa`1D~vE0Is>2-H*l&(4ZVrZRd%*bGLJjo?PbTu0158zkZaan9Ml=_3!qI=EUp(86)Bl)HqqwaowA@b;H9Og{7ytKFz7hPjH#4HK`QSqQwU z<9DYO&?^Lo>q3uNhMAc^iZR($z@AZTWMH3{Jg#^VK8hC3Kjz7ZO~zbjx)`(aL0*K2x~r{FV!qevR=3qX z6;QycGP6WhmBUS%kdz+iHp9=DT`WN8!H-9?B!B&zN8N_e=n^OyRGAtHr&g@vZ|_yu z%zFCg9UR7*{GFN~ur@Z;zI~3CkX7UVUK|$LSZZU7jf3s%I~!}K=m%<|Q>GK-$hRzz zEAfz;xzo?gf~4o9-!*~IVP$Ug@hJ(0HLMI1Y7kza2F9!!?H_Xtc&1|4>KN&;zr|WG zkve*EA+CmsOKnflHj5hO?kAUre)_)CgSvdYx-$&_lQs0<%BrP@_(?hxq}6w;u32;a zaa*cXHV=h?su|2nHJHpFKIbTQH1`(a!VY-57+N|%u@8=VVmgspcQ@r@IQ`jwn-Zahgw^+(auwgmDM zuuEm^;qFyP^A6n^GDRaKZ>48O(Bt5d_J{5b_P6~tva>QjaOhR%dQijo1#7gv{hZ0I zS>o1g=+gn_WcPArddeiCyS{Krs+A03fjgrxI%sxF5Gypbz-M4Mjhwl@&m)5vx6l`s z__Dcap5oC*o>MwknTZ+qBHve(N0%Ge&ytVuRDmNB{-yzcV!~9S0(ueg3Sr2;07YSc zCcWsX(@fxVg{@W>x~?)2c6q8!w+zdt&ZRL2e5tbNKjqHjLHP-K77?yk=*Z2f2e+w| zYb=U1(tbR7iS)NwJK ztA8GJ{_4<>x9dPzpN$xc13a(2T`V^Sjazwc8;C$=GskV#7;1mt-_n>>RNnl?(L(=E zO0~7BeBlb}(SW>~mB!6tnLX>2z8HoLV&`|7jcsm@mbInY^iAL1mEpH<3l7c?%tj2G zi^Z_m_bZ{?mhXA3$%W<(FYy=@$_o49eslC+z$9t%$?RZ2ni+u z{s+ywuTdHA83ti&`1xR?j!j5Ad>GqTNjGORo>Ae=(^drWz^h_)IY-;Ii9nKd!3Qz#~>hZeE2&w~|aQ;2#`i~@qnL;Gu&&Tx8O<^y56H%`h*nRRFP*%@Q0CbHs! zZ8GIVEyoYYL7@@68%6m2u*cf2Ovmdr%z+OT_6{rUYk?q+Cf>{MPP%vPZ=7;k(su)O zEo_<24kzHat5>PP+zLbE^eh?4Pe3z2owR|cBd?Cx;|K#ODZg7|Nm~+|)x?Zay*5~$ r`H}nGlUTlv#AmSXdYJo0x{`4F%hdC3e3OW`R%IFyI;%Ph00000Hm_K2 literal 0 HcmV?d00001