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

@ -18,77 +18,24 @@
#include "Kdbx4Reader.h"
#include <QBuffer>
#include <QFile>
#include "crypto/kdf/AesKdf.h"
#include "streams/HmacBlockStream.h"
#include "core/Group.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 "format/KdbxXmlReader.h"
#include "streams/HmacBlockStream.h"
#include "streams/QtIOCompressor"
#include "streams/StoreDataStream.h"
#include "streams/SymmetricCipherStream.h"
Kdbx4Reader::Kdbx4Reader()
: m_device(nullptr)
, m_db(nullptr)
Database* Kdbx4Reader::readDatabaseImpl(QIODevice* device, const QByteArray& headerData,
const CompositeKey& key, bool keepDatabase)
{
}
Q_ASSERT(m_kdbxVersion == KeePass2::FILE_VERSION_4);
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;
}
@ -97,7 +44,7 @@ Database* Kdbx4Reader::readDatabase(QIODevice* device, const CompositeKey& key,
if (m_masterSeed.isEmpty()
|| m_encryptionIV.isEmpty()
|| m_db->cipher().isNull()) {
raiseError("missing database headers");
raiseError(tr("missing database headers"));
return nullptr;
}
@ -106,7 +53,7 @@ Database* Kdbx4Reader::readDatabase(QIODevice* device, const CompositeKey& key,
return nullptr;
}
if (m_db->challengeMasterSeed(m_masterSeed) == false) {
if (!m_db->challengeMasterSeed(m_masterSeed)) {
raiseError(tr("Unable to issue challenge-response."));
return nullptr;
}
@ -117,24 +64,24 @@ Database* Kdbx4Reader::readDatabase(QIODevice* device, const CompositeKey& key,
hash.addData(m_db->transformedMasterKey());
QByteArray finalKey = hash.result();
QByteArray headerSha256 = m_device->read(32);
QByteArray headerHmac = m_device->read(32);
QByteArray headerSha256 = device->read(32);
QByteArray headerHmac = device->read(32);
if (headerSha256.size() != 32 || headerHmac.size() != 32) {
raiseError("Invalid header checksum size");
raiseError(tr("Invalid header checksum size"));
return nullptr;
}
if (headerSha256 != CryptoHash::hash(headerStream.storedData(), CryptoHash::Sha256)) {
raiseError("Header SHA256 mismatch");
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(headerStream.storedData(),
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(m_device, hmacKey);
HmacBlockStream hmacStream(device, hmacKey);
if (!hmacStream.open(QIODevice::ReadOnly)) {
raiseError(hmacStream.errorString());
return nullptr;
@ -142,7 +89,7 @@ Database* Kdbx4Reader::readDatabase(QIODevice* device, const CompositeKey& key,
SymmetricCipher::Algorithm cipher = SymmetricCipher::cipherToAlgorithm(m_db->cipher());
if (cipher == SymmetricCipher::InvalidAlgorithm) {
raiseError("Unknown cipher");
raiseError(tr("Unknown cipher"));
return nullptr;
}
SymmetricCipherStream cipherStream(&hmacStream, cipher,
@ -156,7 +103,7 @@ Database* Kdbx4Reader::readDatabase(QIODevice* device, const CompositeKey& key,
return nullptr;
}
QIODevice* xmlDevice;
QIODevice* xmlDevice = nullptr;
QScopedPointer<QtIOCompressor> ioCompressor;
if (m_db->compressionAlgo() == Database::CompressionNone) {
@ -171,7 +118,6 @@ Database* Kdbx4Reader::readDatabase(QIODevice* device, const CompositeKey& key,
xmlDevice = ioCompressor.data();
}
while (readInnerHeaderField(xmlDevice) && !hasError()) {
}
@ -185,50 +131,51 @@ Database* Kdbx4Reader::readDatabase(QIODevice* device, const CompositeKey& key,
return nullptr;
}
QScopedPointer<QBuffer> buffer;
if (m_saveXml) {
QBuffer buffer;
if (saveXml()) {
m_xmlData = xmlDevice->readAll();
buffer.reset(new QBuffer(&m_xmlData));
buffer->open(QIODevice::ReadOnly);
xmlDevice = buffer.data();
buffer.setBuffer(&m_xmlData);
buffer.open(QIODevice::ReadOnly);
xmlDevice = &buffer;
}
Kdbx4XmlReader xmlReader(m_binaryPool);
xmlReader.readDatabase(xmlDevice, m_db, &randomStream);
Q_ASSERT(xmlDevice);
KdbxXmlReader xmlReader(KeePass2::FILE_VERSION_4, m_binaryPool);
xmlReader.readDatabase(xmlDevice, m_db.data(), &randomStream);
if (xmlReader.hasError()) {
raiseError(xmlReader.errorString());
if (keepDatabase) {
return db.take();
return m_db.take();
}
return nullptr;
}
return db.take();
return m_db.take();
}
bool Kdbx4Reader::readHeaderField(QIODevice* device)
bool Kdbx4Reader::readHeaderField(StoreDataStream& device)
{
QByteArray fieldIDArray = device->read(1);
QByteArray fieldIDArray = device.read(1);
if (fieldIDArray.size() != 1) {
raiseError("Invalid header id size");
raiseError(tr("Invalid header id size"));
return false;
}
char fieldID = fieldIDArray.at(0);
bool ok;
auto fieldLen = Endian::readSizedInt<quint32>(device, KeePass2::BYTEORDER, &ok);
auto fieldLen = Endian::readSizedInt<quint32>(&device, KeePass2::BYTEORDER, &ok);
if (!ok) {
raiseError("Invalid header field length");
raiseError(tr("Invalid header field length"));
return false;
}
QByteArray fieldData;
if (fieldLen != 0) {
fieldData = device->read(fieldLen);
fieldData = device.read(fieldLen);
if (static_cast<quint32>(fieldData.size()) != fieldLen) {
raiseError("Invalid header data length");
raiseError(tr("Invalid header data length"));
return false;
}
}
@ -256,13 +203,13 @@ bool Kdbx4Reader::readHeaderField(QIODevice* device)
case KeePass2::HeaderFieldID::KdfParameters: {
QBuffer bufIoDevice(&fieldData);
if (!bufIoDevice.open(QIODevice::ReadOnly)) {
raiseError("Failed to open buffer for KDF parameters in header");
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("Invalid KDF parameters");
raiseError(tr("Invalid KDF parameters"));
return false;
}
m_db->setKdf(kdf);
@ -278,7 +225,7 @@ bool Kdbx4Reader::readHeaderField(QIODevice* device)
case KeePass2::HeaderFieldID::TransformSeed:
case KeePass2::HeaderFieldID::StreamStartBytes:
case KeePass2::HeaderFieldID::InnerRandomStreamID:
raiseError("Legacy header fields found in KDBX4 file.");
raiseError(tr("Legacy header fields found in KDBX4 file."));
return false;
default:
@ -289,19 +236,25 @@ bool Kdbx4Reader::readHeaderField(QIODevice* device)
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("Invalid inner header id size");
raiseError(tr("Invalid inner header id size"));
return false;
}
KeePass2::InnerHeaderFieldID fieldID = static_cast<KeePass2::InnerHeaderFieldID>(fieldIDArray.at(0));
auto fieldID = static_cast<KeePass2::InnerHeaderFieldID>(fieldIDArray.at(0));
bool ok;
quint32 fieldLen = Endian::readSizedInt<quint32>(device, KeePass2::BYTEORDER, &ok);
auto fieldLen = Endian::readSizedInt<quint32>(device, KeePass2::BYTEORDER, &ok);
if (!ok) {
raiseError("Invalid inner header field length");
raiseError(tr("Invalid inner header field length"));
return false;
}
@ -309,7 +262,7 @@ bool Kdbx4Reader::readInnerHeaderField(QIODevice* device)
if (fieldLen != 0) {
fieldData = device->read(fieldLen);
if (static_cast<quint32>(fieldData.size()) != fieldLen) {
raiseError("Invalid header data length");
raiseError(tr("Invalid header data length"));
return false;
}
}
@ -328,20 +281,22 @@ bool Kdbx4Reader::readInnerHeaderField(QIODevice* device)
case KeePass2::InnerHeaderFieldID::Binary:
if (fieldLen < 1) {
raiseError("Invalid inner header binary size");
raiseError(tr("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;
}
/**
* Helper method for reading KDF parameters into variant map.
*
* @param device input device
* @return filled variant map
*/
QVariantMap Kdbx4Reader::readVariantMap(QIODevice* device)
{
bool ok;
@ -350,41 +305,41 @@ QVariantMap Kdbx4Reader::readVariantMap(QIODevice* device)
quint16 maxVersion = KeePass2::VARIANTMAP_VERSION & KeePass2::VARIANTMAP_CRITICAL_MASK;
if (!ok || (version > maxVersion)) {
raiseError(tr("Unsupported KeePass variant map version."));
return QVariantMap();
return {};
}
QVariantMap vm;
QByteArray fieldTypeArray;
KeePass2::VariantMapFieldType fieldType;
KeePass2::VariantMapFieldType fieldType = KeePass2::VariantMapFieldType::End;
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);
auto nameLen = Endian::readSizedInt<quint32>(device, KeePass2::BYTEORDER, &ok);
if (!ok) {
raiseError("Invalid variant map entry name length");
return QVariantMap();
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) {
raiseError("Invalid variant map entry name data");
return QVariantMap();
raiseError(tr("Invalid variant map entry name data"));
return {};
}
}
QString name = QString::fromUtf8(nameBytes);
quint32 valueLen = Endian::readSizedInt<quint32>(device, KeePass2::BYTEORDER, &ok);
auto valueLen = Endian::readSizedInt<quint32>(device, KeePass2::BYTEORDER, &ok);
if (!ok) {
raiseError("Invalid variant map entry value length");
return QVariantMap();
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) {
raiseError("Invalid variant map entry value data");
return QVariantMap();
raiseError(tr("Invalid variant map entry value data"));
return {};
}
}
@ -393,127 +348,70 @@ QVariantMap Kdbx4Reader::readVariantMap(QIODevice* device)
if (valueLen == 1) {
vm.insert(name, QVariant(valueBytes.at(0) != 0));
} else {
raiseError("Invalid variant map Bool entry value length");
return QVariantMap();
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 {
raiseError("Invalid variant map Int32 entry value length");
return QVariantMap();
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 {
raiseError("Invalid variant map UInt32 entry value length");
return QVariantMap();
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 {
raiseError("Invalid variant map Int64 entry value length");
return QVariantMap();
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 {
raiseError("Invalid variant map UInt64 entry value length");
return QVariantMap();
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:
raiseError("Invalid variant map entry type");
return QVariantMap();
raiseError(tr("Invalid variant map entry type"));
return {};
}
}
if (fieldTypeArray.size() != 1) {
raiseError("Invalid variant map field type size");
return QVariantMap();
raiseError(tr("Invalid variant map field type size"));
return {};
}
return vm;
}
void Kdbx4Reader::setCipher(const QByteArray& data)
{
if (data.size() != Uuid::Length) {
raiseError("Invalid cipher uuid length");
return;
}
Uuid uuid(data);
if (SymmetricCipher::cipherToAlgorithm(uuid) == SymmetricCipher::InvalidAlgorithm) {
raiseError("Unsupported cipher");
return;
}
m_db->setCipher(uuid);
}
void Kdbx4Reader::setCompressionFlags(const QByteArray& data)
{
if (data.size() != 4) {
raiseError("Invalid compression flags length");
return;
}
auto id = Endian::bytesToSizedInt<quint32>(data, KeePass2::BYTEORDER);
if (id > Database::CompressionAlgorithmMax) {
raiseError("Unsupported compression algorithm");
return;
}
m_db->setCompressionAlgo(static_cast<Database::CompressionAlgorithm>(id));
}
void Kdbx4Reader::setMasterSeed(const QByteArray& data)
{
if (data.size() != 32) {
raiseError("Invalid master seed size");
return;
}
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");
return;
}
auto id = Endian::bytesToSizedInt<quint32>(data, KeePass2::BYTEORDER);
KeePass2::ProtectedStreamAlgo irsAlgo = KeePass2::idToProtectedStreamAlgo(id);
if (irsAlgo == KeePass2::ProtectedStreamAlgo::InvalidProtectedStreamAlgo || irsAlgo == KeePass2::ProtectedStreamAlgo::ArcFourVariant) {
raiseError("Invalid inner random stream cipher");
return;
}
m_irsAlgo = irsAlgo;
}
QHash<QString, QByteArray> Kdbx4Reader::binaryPool()
QHash<QString, QByteArray> Kdbx4Reader::binaryPool() const
{
return m_binaryPool;
}