Refactor database readers/writers and XML handling

* Refactor Kdbx*Reader
* Refactor KdbxWriter
* Refactor KdbxXmlReader
* Refactor KdbxXmlWriter
This commit is contained in:
Janek Bevendorff 2018-01-07 04:08:32 +01:00 committed by Jonathan White
parent 72a1c65d00
commit a6ddc22fb8
No known key found for this signature in database
GPG key ID: 440FC65F2E0C6E01
29 changed files with 1313 additions and 2917 deletions

View file

@ -19,25 +19,16 @@
#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 "format/KdbxXmlWriter.h"
#include "streams/QtIOCompressor"
#include "streams/SymmetricCipherStream.h"
Kdbx4Writer::Kdbx4Writer()
: m_device(nullptr)
{
}
bool Kdbx4Writer::writeDatabase(QIODevice* device, Database* db)
{
m_error = false;
@ -45,12 +36,12 @@ bool Kdbx4Writer::writeDatabase(QIODevice* device, Database* db)
SymmetricCipher::Algorithm algo = SymmetricCipher::cipherToAlgorithm(db->cipher());
if (algo == SymmetricCipher::InvalidAlgorithm) {
raiseError("Invalid symmetric cipher algorithm.");
raiseError(tr("Invalid symmetric cipher algorithm."));
return false;
}
int ivSize = SymmetricCipher::algorithmIvSize(algo);
if (ivSize < 0) {
raiseError("Invalid symmetric cipher IV size.");
raiseError(tr("Invalid symmetric cipher IV size."));
return false;
}
@ -70,6 +61,7 @@ bool Kdbx4Writer::writeDatabase(QIODevice* device, Database* db)
return false;
}
// generate transformed master key
CryptoHash hash(CryptoHash::Sha256);
hash.addData(masterSeed);
hash.addData(db->challengeResponseKey());
@ -77,48 +69,49 @@ bool Kdbx4Writer::writeDatabase(QIODevice* device, Database* db)
hash.addData(db->transformedMasterKey());
QByteArray finalKey = hash.result();
// write header
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::HeaderFieldID::CipherID, db->cipher().toByteArray()));
CHECK_RETURN_FALSE(writeHeaderField(KeePass2::HeaderFieldID::CompressionFlags,
Endian::sizedIntToBytes(static_cast<int>(db->compressionAlgo()),
KeePass2::BYTEORDER)));
CHECK_RETURN_FALSE(writeHeaderField(KeePass2::HeaderFieldID::MasterSeed, masterSeed));
CHECK_RETURN_FALSE(writeHeaderField(KeePass2::HeaderFieldID::EncryptionIV, encryptionIV));
// Convert current Kdf to basic parameters
writeMagicNumbers(&header, KeePass2::SIGNATURE_1, KeePass2::SIGNATURE_2, KeePass2::FILE_VERSION_4);
CHECK_RETURN_FALSE(writeHeaderField<quint32>(&header, KeePass2::HeaderFieldID::CipherID, db->cipher().toByteArray()));
CHECK_RETURN_FALSE(writeHeaderField<quint32>(&header, KeePass2::HeaderFieldID::CompressionFlags,
Endian::sizedIntToBytes(static_cast<int>(db->compressionAlgo()),
KeePass2::BYTEORDER)));
CHECK_RETURN_FALSE(writeHeaderField<quint32>(&header, KeePass2::HeaderFieldID::MasterSeed, masterSeed));
CHECK_RETURN_FALSE(writeHeaderField<quint32>(&header, KeePass2::HeaderFieldID::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");
raiseError(tr("Failed to serialize KDF parameters variant map"));
return false;
}
QByteArray publicCustomData = db->publicCustomData();
CHECK_RETURN_FALSE(writeHeaderField(KeePass2::HeaderFieldID::KdfParameters, kdfParamBytes));
CHECK_RETURN_FALSE(writeHeaderField<quint32>(&header, KeePass2::HeaderFieldID::KdfParameters, kdfParamBytes));
if (!publicCustomData.isEmpty()) {
CHECK_RETURN_FALSE(writeHeaderField(KeePass2::HeaderFieldID::PublicCustomData, publicCustomData));
CHECK_RETURN_FALSE(writeHeaderField<quint32>(&header, KeePass2::HeaderFieldID::PublicCustomData, publicCustomData));
}
CHECK_RETURN_FALSE(writeHeaderField(KeePass2::HeaderFieldID::EndOfHeader, endOfHeader));
CHECK_RETURN_FALSE(writeHeaderField<quint32>(&header, KeePass2::HeaderFieldID::EndOfHeader, endOfHeader));
header.close();
m_device = device;
headerData = header.data();
}
CHECK_RETURN_FALSE(writeData(headerData));
CHECK_RETURN_FALSE(writeData(device, headerData));
// hash header
QByteArray headerHash = CryptoHash::hash(headerData, CryptoHash::Sha256);
// write HMAC-authenticated cipher stream
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));
CHECK_RETURN_FALSE(writeData(device, headerHash));
CHECK_RETURN_FALSE(writeData(device, headerHmac));
QScopedPointer<HmacBlockStream> hmacBlockStream;
QScopedPointer<SymmetricCipherStream> cipherStream;
@ -130,8 +123,8 @@ bool Kdbx4Writer::writeDatabase(QIODevice* device, Database* db)
}
cipherStream.reset(new SymmetricCipherStream(hmacBlockStream.data(), algo,
SymmetricCipher::algorithmMode(algo),
SymmetricCipher::Encrypt));
SymmetricCipher::algorithmMode(algo),
SymmetricCipher::Encrypt));
if (!cipherStream->init(finalKey, encryptionIV)) {
raiseError(cipherStream->errorString());
@ -142,9 +135,11 @@ bool Kdbx4Writer::writeDatabase(QIODevice* device, Database* db)
return false;
}
QIODevice* outputDevice = nullptr;
QScopedPointer<QtIOCompressor> ioCompressor;
if (db->compressionAlgo() == Database::CompressionNone) {
m_device = cipherStream.data();
outputDevice = cipherStream.data();
} else {
ioCompressor.reset(new QtIOCompressor(cipherStream.data()));
ioCompressor->setStreamFormat(QtIOCompressor::GzipFormat);
@ -152,30 +147,18 @@ bool Kdbx4Writer::writeDatabase(QIODevice* device, Database* db)
raiseError(ioCompressor->errorString());
return false;
}
m_device = ioCompressor.data();
outputDevice = ioCompressor.data();
}
QHash<QByteArray, int> idMap;
Q_ASSERT(outputDevice);
CHECK_RETURN_FALSE(writeInnerHeaderField(KeePass2::InnerHeaderFieldID::InnerRandomStreamID,
CHECK_RETURN_FALSE(writeInnerHeaderField(outputDevice, KeePass2::InnerHeaderFieldID::InnerRandomStreamID,
Endian::sizedIntToBytes(static_cast<int>(KeePass2::ProtectedStreamAlgo::ChaCha20),
KeePass2::BYTEORDER)));
CHECK_RETURN_FALSE(writeInnerHeaderField(KeePass2::InnerHeaderFieldID::InnerRandomStreamKey,
CHECK_RETURN_FALSE(writeInnerHeaderField(outputDevice, 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()));
CHECK_RETURN_FALSE(writeInnerHeaderField(outputDevice, KeePass2::InnerHeaderFieldID::End, QByteArray()));
KeePass2RandomStream randomStream(KeePass2::ProtectedStreamAlgo::ChaCha20);
if (!randomStream.init(protectedStreamKey)) {
@ -183,8 +166,8 @@ bool Kdbx4Writer::writeDatabase(QIODevice* device, Database* db)
return false;
}
Kdbx4XmlWriter xmlWriter(KeePass2::FILE_VERSION_4, idMap);
xmlWriter.writeDatabase(m_device, db, &randomStream, headerHash);
KdbxXmlWriter xmlWriter(KeePass2::FILE_VERSION_4);
xmlWriter.writeDatabase(outputDevice, db, &randomStream, headerHash);
// Explicitly close/reset streams so they are flushed and we can detect
// errors. QIODevice::close() resets errorString() etc.
@ -208,61 +191,64 @@ bool Kdbx4Writer::writeDatabase(QIODevice* device, Database* db)
return true;
}
bool Kdbx4Writer::writeData(const QByteArray& data)
{
if (m_device->write(data) != data.size()) {
raiseError(m_device->errorString());
return false;
}
return true;
}
bool Kdbx4Writer::writeHeaderField(KeePass2::HeaderFieldID fieldId, const QByteArray& data)
/**
* Write KDBX4 inner header field.
*
* @param device output device
* @param fieldId field identifier
* @param data header payload
* @return true on success
*/
bool Kdbx4Writer::writeInnerHeaderField(QIODevice* device, 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));
CHECK_RETURN_FALSE(writeData(device, fieldIdArr));
CHECK_RETURN_FALSE(writeData(device, Endian::sizedIntToBytes(static_cast<quint32>(data.size()), KeePass2::BYTEORDER)));
CHECK_RETURN_FALSE(writeData(device, 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)
/**
* Write binary header field..
*
* @param device output device
* @param fieldId field identifier
* @param data header payload
* @return true on success
*/
bool Kdbx4Writer::writeBinary(QIODevice* device, 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));
CHECK_RETURN_FALSE(writeData(device, fieldIdArr));
CHECK_RETURN_FALSE(writeData(device, Endian::sizedIntToBytes(static_cast<quint32>(data.size() + 1), KeePass2::BYTEORDER)));
CHECK_RETURN_FALSE(writeData(device, QByteArray(1, '\1')));
CHECK_RETURN_FALSE(writeData(device, data));
return true;
}
bool Kdbx4Writer::serializeVariantMap(const QVariantMap& p, QByteArray& o)
/**
* Serialize KDF parameter variant map to byte array.
*
* @param map input variant map
* @param outputBytes output byte array
* @return true on success
*/
bool Kdbx4Writer::serializeVariantMap(const QVariantMap& map, QByteArray& outputBytes)
{
QBuffer buf(&o);
QBuffer buf(&outputBytes);
buf.open(QIODevice::WriteOnly);
CHECK_RETURN_FALSE(buf.write(Endian::sizedIntToBytes(KeePass2::VARIANTMAP_VERSION, KeePass2::BYTEORDER)) == 2);
bool ok;
QList<QString> keys = p.keys();
QList<QString> keys = map.keys();
for (const auto& k : keys) {
KeePass2::VariantMapFieldType fieldType;
QByteArray data;
QVariant v = p.value(k);
QVariant v = map.value(k);
switch (static_cast<QMetaType::Type>(v.type())) {
case QMetaType::Type::Int:
fieldType = KeePass2::VariantMapFieldType::Int32;