Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/keepassxreboot/keepassxc.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonathan White <support@dmapps.us>2018-01-05 18:41:29 +0300
committerJonathan White <support@dmapps.us>2018-01-13 22:24:55 +0300
commitbef7ba2cfed00b88175473ae68481cebb7746098 (patch)
tree08f185e94f6ef7705174a70ea979576c81478e15 /src/format
parent7dba788d09b3333087961e6fa2af4b02579999b4 (diff)
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
Diffstat (limited to 'src/format')
-rw-r--r--src/format/Kdbx3Reader.cpp2
-rw-r--r--src/format/Kdbx3Writer.cpp2
-rw-r--r--src/format/Kdbx4Reader.cpp522
-rw-r--r--src/format/Kdbx4Reader.h66
-rw-r--r--src/format/Kdbx4Writer.cpp326
-rw-r--r--src/format/Kdbx4Writer.h52
-rw-r--r--src/format/Kdbx4XmlReader.cpp1080
-rw-r--r--src/format/Kdbx4XmlReader.h102
-rw-r--r--src/format/Kdbx4XmlWriter.cpp611
-rw-r--r--src/format/Kdbx4XmlWriter.h93
-rw-r--r--src/format/KeePass2.cpp46
-rw-r--r--src/format/KeePass2.h58
-rw-r--r--src/format/KeePass2Reader.cpp17
-rw-r--r--src/format/KeePass2Reader.h3
-rw-r--r--src/format/KeePass2Repair.cpp19
-rw-r--r--src/format/KeePass2Writer.cpp19
16 files changed, 3007 insertions, 11 deletions
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<quint32>(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<qint32>(KeePass2::SIGNATURE_1, KeePass2::BYTEORDER)));
CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes<qint32>(KeePass2::SIGNATURE_2, KeePass2::BYTEORDER)));
- CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes<qint32>(KeePass2::FILE_VERSION, KeePass2::BYTEORDER)));
+ CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes<qint32>(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 <debfx@fobos.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "Kdbx4Reader.h"
+
+#include <QBuffer>
+#include <QFile>
+
+#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<Database> 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<quint32>(headerIo, KeePass2::BYTEORDER, &ok);
+ if (!ok || signature1 != KeePass2::SIGNATURE_1) {
+ raiseError(tr("Not a KeePass database."));
+ return nullptr;
+ }
+
+ quint32 signature2 = Endian::readSizedInt<quint32>(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<quint32>(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<QtIOCompressor> 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<QBuffer> 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<quint32>(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<quint32>(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> 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<KeePass2::InnerHeaderFieldID>(fieldIDArray.at(0));
+
+ bool ok;
+ quint32 fieldLen = Endian::readSizedInt<quint32>(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<quint32>(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<quint8>(fieldID));
+ break;
+ }
+
+ return true;
+}
+
+QVariantMap Kdbx4Reader::readVariantMap(QIODevice* device)
+{
+ bool ok;
+ quint16 version = Endian::readSizedInt<quint16>(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<KeePass2::VariantMapFieldType>(fieldTypeArray.at(0)))
+ != KeePass2::VariantMapFieldType::End)) {
+ quint32 nameLen = Endian::readSizedInt<quint32>(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<quint32>(nameBytes.size()) != nameLen) {
+ raiseError("Invalid variant map entry name data");
+ return QVariantMap();
+ }
+ }
+ QString name = QString::fromUtf8(nameBytes);
+
+ quint32 valueLen = Endian::readSizedInt<quint32>(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<quint32>(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<qint32>(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<quint32>(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<qint64>(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<quint64>(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<quint32>(data, KeePass2::BYTEORDER);
+
+ if (id > Database::CompressionAlgorithmMax) {
+ raiseError("Unsupported compression algorithm");
+ } else {
+ m_db->setCompressionAlgo(static_cast<Database::CompressionAlgorithm>(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<quint32>(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<QString, QByteArray> 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 <debfx@fobos.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSX_KDBX4READER_H
+#define KEEPASSX_KDBX4READER_H
+
+#include <QCoreApplication>
+#include <QHash>
+#include <QString>
+#include <QByteArray>
+
+#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<QString, QByteArray> 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<QString, QByteArray> 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 <debfx@fobos.de>
+ * Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "Kdbx4Writer.h"
+
+#include <QBuffer>
+#include <QFile>
+#include <QIODevice>
+#include <QList>
+#include <QString>
+
+#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<int>(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<QIODevice> 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<QIODevice*>(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<QIODevice*>(cipherStream));
+
+ QScopedPointer<QtIOCompressor> 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<QByteArray, int> idMap;
+
+ CHECK_RETURN_FALSE(writeInnerHeaderField(KeePass2::InnerHeaderFieldID::InnerRandomStreamID,
+ Endian::sizedIntToBytes(static_cast<int>(KeePass2::ChaCha20),
+ KeePass2::BYTEORDER)));
+ CHECK_RETURN_FALSE(writeInnerHeaderField(KeePass2::InnerHeaderFieldID::InnerRandomStreamKey,
+ protectedStreamKey));
+ const QList<Entry*> allEntries = db->rootGroup()->entriesRecursive(true);
+ int nextId = 0;
+
+ for (Entry* entry : allEntries) {
+ const QList<QString> 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<quint32>(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<char>(fieldId);
+ CHECK_RETURN_FALSE(writeData(fieldIdArr));
+ CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes(static_cast<quint32>(data.size()), KeePass2::BYTEORDER)));
+ CHECK_RETURN_FALSE(writeData(data));
+
+ return true;
+}
+
+bool Kdbx4Writer::writeBinary(const QByteArray& data)
+{
+ QByteArray fieldIdArr;
+ fieldIdArr[0] = static_cast<char>(KeePass2::InnerHeaderFieldID::Binary);
+ CHECK_RETURN_FALSE(writeData(fieldIdArr));
+ CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes(static_cast<quint32>(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<QString> 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<QMetaType::Type>(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<char>(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<char>(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 <debfx@fobos.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSX_KDBX4WRITER_H
+#define KEEPASSX_KDBX4WRITER_H
+
+#include <QCoreApplication>
+
+#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 <debfx@fobos.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "Kdbx4XmlReader.h"
+
+#include <QBuffer>
+#include <QFile>
+#include <QRegularExpression>
+
+#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<QString, QString> StringPair;
+
+Kdbx4XmlReader::Kdbx4XmlReader()
+ : m_randomStream(nullptr)
+ , m_db(nullptr)
+ , m_meta(nullptr)
+ , m_tmpParent(nullptr)
+ , m_error(false)
+ , m_strictMode(false)
+{
+}
+
+Kdbx4XmlReader::Kdbx4XmlReader(QHash<QString, QByteArray>& 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<QString> poolKeys = m_binaryPool.keys().toSet();
+ const QSet<QString> entryKeys = m_binaryMap.keys().toSet();
+ const QSet<QString> unmappedKeys = entryKeys - poolKeys;
+ const QSet<QString> 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<QString, QPair<Entry*, QString> >::const_iterator i;
+ for (i = m_binaryMap.constBegin(); i != m_binaryMap.constEnd(); ++i) {
+ const QPair<Entry*, QString>& target = i.value();
+ target.first->attachments()->set(target.second, m_binaryPool[i.key()]);
+ }
+
+ m_meta->setUpdateDatetime(true);
+
+ QHash<Uuid, Group*>::const_iterator iGroup;
+ for (iGroup = m_groups.constBegin(); iGroup != m_groups.constEnd(); ++iGroup) {
+ iGroup.value()->setUpdateTimeinfo(true);
+ }
+
+ QHash<Uuid, Entry*>::const_iterator iEntry;
+ for (iEntry = m_entries.constBegin(); iEntry != m_entries.constEnd(); ++iEntry) {
+ iEntry.value()->setUpdateTimeinfo(true);
+
+ const QList<Entry*> 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<Group*> children;
+ QList<Entry*> 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<Entry*> historyItems;
+ QList<StringPair> 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<QString, QString> 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<QString, QString> Kdbx4XmlReader::parseEntryBinary(Entry* entry)
+{
+ Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Binary");
+
+ QPair<QString, QString> 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<Entry*> Kdbx4XmlReader::parseEntryHistory()
+{
+ Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "History");
+
+ QList<Entry*> 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<quint64>(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 <debfx@fobos.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSX_KDBX4XMLREADER_H
+#define KEEPASSX_KDBX4XMLREADER_H
+
+#include <QColor>
+#include <QCoreApplication>
+#include <QDateTime>
+#include <QHash>
+#include <QPair>
+#include <QXmlStreamReader>
+
+#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<QString, QByteArray>& 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<QString, QString> parseEntryBinary(Entry* entry);
+ void parseAutoType(Entry* entry);
+ void parseAutoTypeAssoc(Entry* entry);
+ QList<Entry*> 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<Uuid, Group*> m_groups;
+ QHash<Uuid, Entry*> m_entries;
+ QHash<QString, QByteArray> m_binaryPool;
+ QHash<QString, QPair<Entry*, QString> > 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 <debfx@fobos.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "Kdbx4XmlWriter.h"
+
+#include <QBuffer>
+#include <QFile>
+
+#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<QByteArray, int>())
+{
+}
+
+Kdbx4XmlWriter::Kdbx4XmlWriter(quint32 version, QHash<QByteArray, int> 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<Entry*> allEntries = m_db->rootGroup()->entriesRecursive(true);
+ int nextId = 0;
+
+ for (Entry* entry : allEntries) {
+ const QList<QString> 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<Uuid> 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<QByteArray, int>::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<QString, QString> customFields = m_meta->customFields();
+ const QList<QString> 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<Entry*> entryList = group->entries();
+ for (const Entry* entry : entryList) {
+ writeEntry(entry);
+ }
+
+ const QList<Group*> 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<DeletedObject> 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<QString> 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<QString> 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::Association> 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<Entry*>& 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 <debfx@fobos.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSX_KDBX4XMLWRITER_H
+#define KEEPASSX_KDBX4XMLWRITER_H
+
+#include <QColor>
+#include <QDateTime>
+#include <QImage>
+#include <QXmlStreamWriter>
+
+#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<QByteArray, int> 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<QByteArray, int> 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 <QSharedPointer>
#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<QPair<Uuid, QString>> 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<QPair<Uuid, QString>> 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<Kdf> KeePass2::kdfFromParameters(const QVariantMap &p)
+{
+ QByteArray uuidBytes = p.value(KDFPARAM_UUID).toByteArray();
+ if (uuidBytes.size() != Uuid::Length) {
+ return nullptr;
+ }
+
+ QSharedPointer<Kdf> kdf(uuidToKdf(Uuid(uuidBytes)));
+ if (kdf.isNull()) {
+ return nullptr;
+ }
+
+ if (!kdf->processParameters(p)) {
+ return nullptr;
+ }
+
+ return kdf;
+}
+
+QVariantMap KeePass2::kdfToParameters(QSharedPointer<Kdf> kdf)
+{
+ return kdf->writeParameters();
+}
+
QSharedPointer<Kdf> 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 <QtGlobal>
+#include <QMap>
+#include <QVariantMap>
#include <QList>
#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<QPair<Uuid, QString>> CIPHERS;
extern const QList<QPair<Uuid, QString>> 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<Kdf> kdfFromParameters(const QVariantMap &p);
+ QVariantMap kdfToParameters(QSharedPointer<Kdf> kdf);
QSharedPointer<Kdf> uuidToKdf(const Uuid& uuid);
+ Uuid kdfToUuid(QSharedPointer<Kdf> 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<quint32>(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<BaseKeePass2Reader*>(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<BaseKeePass2Reader> 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<BaseKeePass2Reader> reader();
KeePass2::ProtectedStreamAlgo protectedStreamAlgo() const override;
quint32 version() const;
private:
- QScopedPointer<BaseKeePass2Reader> m_reader;
+ QSharedPointer<BaseKeePass2Reader> 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<Kdbx4Reader>();
+ QHash<QString, QByteArray> 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<BaseKeePass2Writer*>(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);
}