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:
Diffstat (limited to 'src/format/Kdbx4Reader.cpp')
-rw-r--r--src/format/Kdbx4Reader.cpp448
1 files changed, 448 insertions, 0 deletions
diff --git a/src/format/Kdbx4Reader.cpp b/src/format/Kdbx4Reader.cpp
new file mode 100644
index 000000000..2919db785
--- /dev/null
+++ b/src/format/Kdbx4Reader.cpp
@@ -0,0 +1,448 @@
+/*
+ * 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 "Kdbx4Reader.h"
+
+#include <QBuffer>
+
+#include "core/Group.h"
+#include "core/Endian.h"
+#include "crypto/CryptoHash.h"
+#include "format/KeePass2RandomStream.h"
+#include "format/KdbxXmlReader.h"
+#include "streams/HmacBlockStream.h"
+#include "streams/QtIOCompressor"
+#include "streams/SymmetricCipherStream.h"
+
+Database* Kdbx4Reader::readDatabaseImpl(QIODevice* device, const QByteArray& headerData,
+ const CompositeKey& key, bool keepDatabase)
+{
+ Q_ASSERT(m_kdbxVersion == KeePass2::FILE_VERSION_4);
+
+ m_binaryPoolInverse.clear();
+
+ if (hasError()) {
+ return nullptr;
+ }
+
+ // check if all required headers were present
+ if (m_masterSeed.isEmpty()
+ || m_encryptionIV.isEmpty()
+ || m_db->cipher().isNull()) {
+ raiseError(tr("missing database headers"));
+ return nullptr;
+ }
+
+ if (!m_db->setKey(key, false, false)) {
+ raiseError(tr("Unable to calculate master key"));
+ return nullptr;
+ }
+
+ CryptoHash hash(CryptoHash::Sha256);
+ hash.addData(m_masterSeed);
+ hash.addData(m_db->transformedMasterKey());
+ QByteArray finalKey = hash.result();
+
+ QByteArray headerSha256 = device->read(32);
+ QByteArray headerHmac = device->read(32);
+ if (headerSha256.size() != 32 || headerHmac.size() != 32) {
+ raiseError(tr("Invalid header checksum size"));
+ return nullptr;
+ }
+ if (headerSha256 != CryptoHash::hash(headerData, CryptoHash::Sha256)) {
+ raiseError(tr("Header SHA256 mismatch"));
+ return nullptr;
+ }
+
+ QByteArray hmacKey = KeePass2::hmacKey(m_masterSeed, m_db->transformedMasterKey());
+ if (headerHmac != CryptoHash::hmac(headerData,
+ HmacBlockStream::getHmacKey(UINT64_MAX, hmacKey), CryptoHash::Sha256)) {
+ raiseError(tr("Wrong key or database file is corrupt. (HMAC mismatch)"));
+ return nullptr;
+ }
+ HmacBlockStream hmacStream(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(tr("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 = nullptr;
+ 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;
+ }
+
+ QBuffer buffer;
+ if (saveXml()) {
+ m_xmlData = xmlDevice->readAll();
+ buffer.setBuffer(&m_xmlData);
+ buffer.open(QIODevice::ReadOnly);
+ xmlDevice = &buffer;
+ }
+
+ Q_ASSERT(xmlDevice);
+
+ KdbxXmlReader xmlReader(KeePass2::FILE_VERSION_4, binaryPool());
+ xmlReader.readDatabase(xmlDevice, m_db.data(), &randomStream);
+
+ if (xmlReader.hasError()) {
+ raiseError(xmlReader.errorString());
+ if (keepDatabase) {
+ return m_db.take();
+ }
+ return nullptr;
+ }
+
+ return m_db.take();
+}
+
+bool Kdbx4Reader::readHeaderField(StoreDataStream& device)
+{
+ QByteArray fieldIDArray = device.read(1);
+ if (fieldIDArray.size() != 1) {
+ raiseError(tr("Invalid header id size"));
+ return false;
+ }
+ char fieldID = fieldIDArray.at(0);
+
+ bool ok;
+ auto fieldLen = Endian::readSizedInt<quint32>(&device, KeePass2::BYTEORDER, &ok);
+ if (!ok) {
+ raiseError(tr("Invalid header field length"));
+ return false;
+ }
+
+ QByteArray fieldData;
+ if (fieldLen != 0) {
+ fieldData = device.read(fieldLen);
+ if (static_cast<quint32>(fieldData.size()) != fieldLen) {
+ raiseError(tr("Invalid header data length"));
+ return false;
+ }
+ }
+
+ switch (static_cast<KeePass2::HeaderFieldID>(fieldID)) {
+ case KeePass2::HeaderFieldID::EndOfHeader:
+ return false;
+
+ case KeePass2::HeaderFieldID::CipherID:
+ setCipher(fieldData);
+ break;
+
+ case KeePass2::HeaderFieldID::CompressionFlags:
+ setCompressionFlags(fieldData);
+ break;
+
+ case KeePass2::HeaderFieldID::MasterSeed:
+ setMasterSeed(fieldData);
+ break;
+
+ case KeePass2::HeaderFieldID::EncryptionIV:
+ setEncryptionIV(fieldData);
+ break;
+
+ case KeePass2::HeaderFieldID::KdfParameters: {
+ QBuffer bufIoDevice(&fieldData);
+ if (!bufIoDevice.open(QIODevice::ReadOnly)) {
+ raiseError(tr("Failed to open buffer for KDF parameters in header"));
+ return false;
+ }
+ QVariantMap kdfParams = readVariantMap(&bufIoDevice);
+ QSharedPointer<Kdf> kdf = KeePass2::kdfFromParameters(kdfParams);
+ if (!kdf) {
+ raiseError(tr("Unsupported key derivation function (KDF) or invalid parameters"));
+ return false;
+ }
+ m_db->setKdf(kdf);
+ break;
+ }
+
+ case KeePass2::HeaderFieldID::PublicCustomData: {
+ QBuffer variantBuffer;
+ variantBuffer.setBuffer(&fieldData);
+ variantBuffer.open(QBuffer::ReadOnly);
+ QVariantMap data = readVariantMap(&variantBuffer);
+ m_db->setPublicCustomData(data);
+ break;
+ }
+
+ case KeePass2::HeaderFieldID::ProtectedStreamKey:
+ case KeePass2::HeaderFieldID::TransformRounds:
+ case KeePass2::HeaderFieldID::TransformSeed:
+ case KeePass2::HeaderFieldID::StreamStartBytes:
+ case KeePass2::HeaderFieldID::InnerRandomStreamID:
+ raiseError(tr("Legacy header fields found in KDBX4 file."));
+ return false;
+
+ default:
+ qWarning("Unknown header field read: id=%d", fieldID);
+ break;
+ }
+
+ return true;
+}
+
+/**
+ * Helper method for reading KDBX4 inner header fields.
+ *
+ * @param device input device
+ * @return true if there are more inner header fields
+ */
+bool Kdbx4Reader::readInnerHeaderField(QIODevice* device)
+{
+ QByteArray fieldIDArray = device->read(1);
+ if (fieldIDArray.size() != 1) {
+ raiseError(tr("Invalid inner header id size"));
+ return false;
+ }
+ auto fieldID = static_cast<KeePass2::InnerHeaderFieldID>(fieldIDArray.at(0));
+
+ bool ok;
+ auto fieldLen = Endian::readSizedInt<quint32>(device, KeePass2::BYTEORDER, &ok);
+ if (!ok) {
+ raiseError(tr("Invalid inner header field length"));
+ return false;
+ }
+
+ QByteArray fieldData;
+ if (fieldLen != 0) {
+ fieldData = device->read(fieldLen);
+ if (static_cast<quint32>(fieldData.size()) != fieldLen) {
+ raiseError(tr("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(tr("Invalid inner header binary size"));
+ return false;
+ }
+ auto data = fieldData.mid(1);
+ if (m_binaryPoolInverse.contains(data)) {
+ qWarning("Skipping duplicate binary record");
+ break;
+ }
+ m_binaryPoolInverse.insert(data, QString::number(m_binaryPoolInverse.size()));
+ break;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Helper method for reading a serialized variant map.
+ *
+ * @param device input device
+ * @return de-serialized variant map
+ */
+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)) {
+ //: Translation: variant map = data structure for storing meta data
+ raiseError(tr("Unsupported KeePass variant map version."));
+ return {};
+ }
+
+ QVariantMap vm;
+ QByteArray fieldTypeArray;
+ KeePass2::VariantMapFieldType fieldType = KeePass2::VariantMapFieldType::End;
+ while (((fieldTypeArray = device->read(1)).size() == 1)
+ && ((fieldType = static_cast<KeePass2::VariantMapFieldType>(fieldTypeArray.at(0)))
+ != KeePass2::VariantMapFieldType::End)) {
+ auto nameLen = Endian::readSizedInt<quint32>(device, KeePass2::BYTEORDER, &ok);
+ if (!ok) {
+ //: Translation: variant map = data structure for storing meta data
+ raiseError(tr("Invalid variant map entry name length"));
+ return {};
+ }
+ QByteArray nameBytes;
+ if (nameLen != 0) {
+ nameBytes = device->read(nameLen);
+ if (static_cast<quint32>(nameBytes.size()) != nameLen) {
+ //: Translation: variant map = data structure for storing meta data
+ raiseError(tr("Invalid variant map entry name data"));
+ return {};
+ }
+ }
+ QString name = QString::fromUtf8(nameBytes);
+
+ auto valueLen = Endian::readSizedInt<quint32>(device, KeePass2::BYTEORDER, &ok);
+ if (!ok) {
+ //: Translation: variant map = data structure for storing meta data
+ raiseError(tr("Invalid variant map entry value length"));
+ return {};
+ }
+ QByteArray valueBytes;
+ if (valueLen != 0) {
+ valueBytes = device->read(valueLen);
+ if (static_cast<quint32>(valueBytes.size()) != valueLen) {
+ //: Translation comment: variant map = data structure for storing meta data
+ raiseError(tr("Invalid variant map entry value data"));
+ return {};
+ }
+ }
+
+ switch (fieldType) {
+ case KeePass2::VariantMapFieldType::Bool:
+ if (valueLen == 1) {
+ vm.insert(name, QVariant(valueBytes.at(0) != 0));
+ } else {
+ //: Translation: variant map = data structure for storing meta data
+ raiseError(tr("Invalid variant map Bool entry value length"));
+ return {};
+ }
+ break;
+
+ case KeePass2::VariantMapFieldType::Int32:
+ if (valueLen == 4) {
+ vm.insert(name, QVariant(Endian::bytesToSizedInt<qint32>(valueBytes, KeePass2::BYTEORDER)));
+ } else {
+ //: Translation: variant map = data structure for storing meta data
+ raiseError(tr("Invalid variant map Int32 entry value length"));
+ return {};
+ }
+ break;
+
+ case KeePass2::VariantMapFieldType::UInt32:
+ if (valueLen == 4) {
+ vm.insert(name, QVariant(Endian::bytesToSizedInt<quint32>(valueBytes, KeePass2::BYTEORDER)));
+ } else {
+ //: Translation: variant map = data structure for storing meta data
+ raiseError(tr("Invalid variant map UInt32 entry value length"));
+ return {};
+ }
+ break;
+
+ case KeePass2::VariantMapFieldType::Int64:
+ if (valueLen == 8) {
+ vm.insert(name, QVariant(Endian::bytesToSizedInt<qint64>(valueBytes, KeePass2::BYTEORDER)));
+ } else {
+ //: Translation: variant map = data structure for storing meta data
+ raiseError(tr("Invalid variant map Int64 entry value length"));
+ return {};
+ }
+ break;
+
+ case KeePass2::VariantMapFieldType::UInt64:
+ if (valueLen == 8) {
+ vm.insert(name, QVariant(Endian::bytesToSizedInt<quint64>(valueBytes, KeePass2::BYTEORDER)));
+ } else {
+ //: Translation: variant map = data structure for storing meta data
+ raiseError(tr("Invalid variant map UInt64 entry value length"));
+ return {};
+ }
+ break;
+
+ case KeePass2::VariantMapFieldType::String:
+ vm.insert(name, QVariant(QString::fromUtf8(valueBytes)));
+ break;
+
+ case KeePass2::VariantMapFieldType::ByteArray:
+ vm.insert(name, QVariant(valueBytes));
+ break;
+
+ default:
+ //: Translation: variant map = data structure for storing meta data
+ raiseError(tr("Invalid variant map entry type"));
+ return {};
+ }
+ }
+
+ if (fieldTypeArray.size() != 1) {
+ //: Translation: variant map = data structure for storing meta data
+ raiseError(tr("Invalid variant map field type size"));
+ return {};
+ }
+
+ return vm;
+}
+
+/**
+ * @return mapping from attachment keys to binary data
+ */
+QHash<QString, QByteArray> Kdbx4Reader::binaryPool() const
+{
+ QHash<QString, QByteArray> binaryPool;
+ for (auto it = m_binaryPoolInverse.cbegin(); it != m_binaryPoolInverse.cend(); ++it) {
+ binaryPool.insert(it.value(), it.key());
+ }
+ return binaryPool;
+}
+
+/**
+ * @return mapping from binary data to attachment keys
+ */
+QHash<QByteArray, QString> Kdbx4Reader::binaryPoolInverse() const
+{
+ return m_binaryPoolInverse;
+}