2
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
4
* This program is free software: you can redistribute it and/or modify
5
* it under the terms of the GNU General Public License as published by
6
* the Free Software Foundation, either version 2 or (at your option)
7
* version 3 of the License.
9
* This program is distributed in the hope that it will be useful,
10
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
* GNU General Public License for more details.
14
* You should have received a copy of the GNU General Public License
15
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18
#include "Kdbx4Writer.h"
22
#include "config-keepassx.h"
23
#include "crypto/CryptoHash.h"
24
#include "crypto/Random.h"
25
#include "format/KeePass2RandomStream.h"
26
#ifdef WITH_XC_KEESHARE
27
#include "keeshare/KeeShare.h"
28
#include "keeshare/KeeShareSettings.h"
30
#include "streams/HmacBlockStream.h"
31
#include "streams/SymmetricCipherStream.h"
32
#include "streams/qtiocompressor.h"
34
bool Kdbx4Writer::writeDatabase(QIODevice* device, Database* db)
39
auto mode = SymmetricCipher::cipherUuidToMode(db->cipher());
40
if (mode == SymmetricCipher::InvalidMode) {
41
raiseError(tr("Invalid symmetric cipher algorithm."));
44
int ivSize = SymmetricCipher::defaultIvSize(mode);
46
raiseError(tr("Invalid symmetric cipher IV size.", "IV = Initialization Vector for symmetric cipher"));
50
QByteArray masterSeed = randomGen()->randomArray(32);
51
QByteArray encryptionIV = randomGen()->randomArray(ivSize);
52
QByteArray protectedStreamKey = randomGen()->randomArray(64);
53
QByteArray endOfHeader = "\r\n\r\n";
55
if (!db->setKey(db->key(), false, true)) {
56
raiseError(tr("Unable to calculate database key: %1").arg(db->keyError()));
60
// generate transformed database key
61
CryptoHash hash(CryptoHash::Sha256);
62
hash.addData(masterSeed);
63
Q_ASSERT(!db->transformedDatabaseKey().isEmpty());
64
hash.addData(db->transformedDatabaseKey());
65
QByteArray finalKey = hash.result();
68
QByteArray headerData;
71
header.open(QIODevice::WriteOnly);
73
writeMagicNumbers(&header, KeePass2::SIGNATURE_1, KeePass2::SIGNATURE_2, db->formatVersion());
76
writeHeaderField<quint32>(&header, KeePass2::HeaderFieldID::CipherID, db->cipher().toRfc4122()));
77
CHECK_RETURN_FALSE(writeHeaderField<quint32>(
79
KeePass2::HeaderFieldID::CompressionFlags,
80
Endian::sizedIntToBytes(static_cast<int>(db->compressionAlgorithm()), KeePass2::BYTEORDER)));
81
CHECK_RETURN_FALSE(writeHeaderField<quint32>(&header, KeePass2::HeaderFieldID::MasterSeed, masterSeed));
82
CHECK_RETURN_FALSE(writeHeaderField<quint32>(&header, KeePass2::HeaderFieldID::EncryptionIV, encryptionIV));
84
// convert current Kdf to basic parameters
85
QVariantMap kdfParams = KeePass2::kdfToParameters(db->kdf());
86
QByteArray kdfParamBytes;
87
if (!serializeVariantMap(kdfParams, kdfParamBytes)) {
88
//: Translation comment: variant map = data structure for storing meta data
89
raiseError(tr("Failed to serialize KDF parameters variant map"));
93
CHECK_RETURN_FALSE(writeHeaderField<quint32>(&header, KeePass2::HeaderFieldID::KdfParameters, kdfParamBytes));
94
QVariantMap publicCustomData = db->publicCustomData();
95
if (!publicCustomData.isEmpty()) {
96
QByteArray serialized;
97
serializeVariantMap(publicCustomData, serialized);
99
writeHeaderField<quint32>(&header, KeePass2::HeaderFieldID::PublicCustomData, serialized));
102
CHECK_RETURN_FALSE(writeHeaderField<quint32>(&header, KeePass2::HeaderFieldID::EndOfHeader, endOfHeader));
104
headerData = header.data();
106
CHECK_RETURN_FALSE(writeData(device, headerData));
109
QByteArray headerHash = CryptoHash::hash(headerData, CryptoHash::Sha256);
111
// write HMAC-authenticated cipher stream
112
QByteArray hmacKey = KeePass2::hmacKey(masterSeed, db->transformedDatabaseKey());
113
QByteArray headerHmac =
114
CryptoHash::hmac(headerData, HmacBlockStream::getHmacKey(UINT64_MAX, hmacKey), CryptoHash::Sha256);
115
CHECK_RETURN_FALSE(writeData(device, headerHash));
116
CHECK_RETURN_FALSE(writeData(device, headerHmac));
118
QScopedPointer<HmacBlockStream> hmacBlockStream;
119
QScopedPointer<SymmetricCipherStream> cipherStream;
121
hmacBlockStream.reset(new HmacBlockStream(device, hmacKey));
122
if (!hmacBlockStream->open(QIODevice::WriteOnly)) {
123
raiseError(hmacBlockStream->errorString());
127
cipherStream.reset(new SymmetricCipherStream(hmacBlockStream.data()));
129
if (!cipherStream->init(mode, SymmetricCipher::Encrypt, finalKey, encryptionIV)) {
130
raiseError(cipherStream->errorString());
133
if (!cipherStream->open(QIODevice::WriteOnly)) {
134
raiseError(cipherStream->errorString());
138
QIODevice* outputDevice = nullptr;
139
QScopedPointer<QtIOCompressor> ioCompressor;
141
if (db->compressionAlgorithm() == Database::CompressionNone) {
142
outputDevice = cipherStream.data();
144
ioCompressor.reset(new QtIOCompressor(cipherStream.data()));
145
ioCompressor->setStreamFormat(QtIOCompressor::GzipFormat);
146
if (!ioCompressor->open(QIODevice::WriteOnly)) {
147
raiseError(ioCompressor->errorString());
150
outputDevice = ioCompressor.data();
153
Q_ASSERT(outputDevice);
155
CHECK_RETURN_FALSE(writeInnerHeaderField(
157
KeePass2::InnerHeaderFieldID::InnerRandomStreamID,
158
Endian::sizedIntToBytes(static_cast<int>(KeePass2::ProtectedStreamAlgo::ChaCha20), KeePass2::BYTEORDER)));
160
writeInnerHeaderField(outputDevice, KeePass2::InnerHeaderFieldID::InnerRandomStreamKey, protectedStreamKey));
162
// Write attachments to the inner header
163
auto idxMap = writeAttachments(outputDevice, db);
165
CHECK_RETURN_FALSE(writeInnerHeaderField(outputDevice, KeePass2::InnerHeaderFieldID::End, QByteArray()));
167
KeePass2RandomStream randomStream;
168
if (!randomStream.init(SymmetricCipher::ChaCha20, protectedStreamKey)) {
169
raiseError(randomStream.errorString());
173
KdbxXmlWriter xmlWriter(db->formatVersion(), idxMap);
174
xmlWriter.writeDatabase(outputDevice, db, &randomStream, headerHash);
176
// Explicitly close/reset streams so they are flushed and we can detect
177
// errors. QIODevice::close() resets errorString() etc.
179
ioCompressor->close();
181
if (!cipherStream->reset()) {
182
raiseError(cipherStream->errorString());
185
if (!hmacBlockStream->reset()) {
186
raiseError(hmacBlockStream->errorString());
190
if (xmlWriter.hasError()) {
191
raiseError(xmlWriter.errorString());
199
* Write KDBX4 inner header field.
201
* @param device output device
202
* @param fieldId field identifier
203
* @param data header payload
204
* @return true on success
206
bool Kdbx4Writer::writeInnerHeaderField(QIODevice* device, KeePass2::InnerHeaderFieldID fieldId, const QByteArray& data)
208
QByteArray fieldIdArr;
209
fieldIdArr.append(static_cast<char>(fieldId));
210
CHECK_RETURN_FALSE(writeData(device, fieldIdArr));
212
writeData(device, Endian::sizedIntToBytes(static_cast<quint32>(data.size()), KeePass2::BYTEORDER)));
213
CHECK_RETURN_FALSE(writeData(device, data));
218
KdbxXmlWriter::BinaryIdxMap Kdbx4Writer::writeAttachments(QIODevice* device, Database* db)
220
const QList<Entry*> allEntries = db->rootGroup()->entriesRecursive(true);
221
QHash<QByteArray, qint64> writtenAttachments;
222
KdbxXmlWriter::BinaryIdxMap idxMap;
225
for (const Entry* entry : allEntries) {
226
const QList<QString> attachmentKeys = entry->attachments()->keys();
227
for (const QString& key : attachmentKeys) {
228
QByteArray data("\x01");
229
data.append(entry->attachments()->value(key));
231
CryptoHash hash(CryptoHash::Sha256);
232
#ifdef WITH_XC_KEESHARE
233
// Namespace KeeShare attachments so they don't get deduplicated together with attachments
234
// from other databases. Prevents potential filesize side channels.
235
if (auto shared = KeeShare::resolveSharedGroup(entry->group())) {
236
hash.addData(KeeShare::referenceOf(shared).uuid.toByteArray());
238
hash.addData(db->uuid().toByteArray());
243
// Deduplicate attachments with the same hash
244
const auto hashResult = hash.result();
245
if (!writtenAttachments.contains(hashResult)) {
246
writeInnerHeaderField(device, KeePass2::InnerHeaderFieldID::Binary, data);
247
writtenAttachments.insert(hashResult, nextIdx++);
249
idxMap.insert(qMakePair(entry, key), writtenAttachments[hashResult]);
257
* Serialize variant map to byte array.
259
* @param map input variant map
260
* @param outputBytes output byte array
261
* @return true on success
263
bool Kdbx4Writer::serializeVariantMap(const QVariantMap& map, QByteArray& outputBytes)
265
QBuffer buf(&outputBytes);
266
buf.open(QIODevice::WriteOnly);
267
CHECK_RETURN_FALSE(buf.write(Endian::sizedIntToBytes(KeePass2::VARIANTMAP_VERSION, KeePass2::BYTEORDER)) == 2);
270
QList<QString> keys = map.keys();
271
for (const auto& k : keys) {
272
KeePass2::VariantMapFieldType fieldType;
274
QVariant v = map.value(k);
275
switch (static_cast<QMetaType::Type>(v.type())) {
276
case QMetaType::Type::Int:
277
fieldType = KeePass2::VariantMapFieldType::Int32;
278
data = Endian::sizedIntToBytes(v.toInt(&ok), KeePass2::BYTEORDER);
279
CHECK_RETURN_FALSE(ok);
281
case QMetaType::Type::UInt:
282
fieldType = KeePass2::VariantMapFieldType::UInt32;
283
data = Endian::sizedIntToBytes(v.toUInt(&ok), KeePass2::BYTEORDER);
284
CHECK_RETURN_FALSE(ok);
286
case QMetaType::Type::LongLong:
287
fieldType = KeePass2::VariantMapFieldType::Int64;
288
data = Endian::sizedIntToBytes(v.toLongLong(&ok), KeePass2::BYTEORDER);
289
CHECK_RETURN_FALSE(ok);
291
case QMetaType::Type::ULongLong:
292
fieldType = KeePass2::VariantMapFieldType::UInt64;
293
data = Endian::sizedIntToBytes(v.toULongLong(&ok), KeePass2::BYTEORDER);
294
CHECK_RETURN_FALSE(ok);
296
case QMetaType::Type::QString:
297
fieldType = KeePass2::VariantMapFieldType::String;
298
data = v.toString().toUtf8();
300
case QMetaType::Type::Bool:
301
fieldType = KeePass2::VariantMapFieldType::Bool;
302
data = QByteArray(1, static_cast<char>(v.toBool() ? '\1' : '\0'));
304
case QMetaType::Type::QByteArray:
305
fieldType = KeePass2::VariantMapFieldType::ByteArray;
306
data = v.toByteArray();
309
qWarning("Unknown object type %d in QVariantMap", v.type());
312
QByteArray typeBytes;
313
typeBytes.append(static_cast<char>(fieldType));
314
QByteArray nameBytes = k.toUtf8();
315
QByteArray nameLenBytes = Endian::sizedIntToBytes(nameBytes.size(), KeePass2::BYTEORDER);
316
QByteArray dataLenBytes = Endian::sizedIntToBytes(data.size(), KeePass2::BYTEORDER);
318
CHECK_RETURN_FALSE(buf.write(typeBytes) == 1);
319
CHECK_RETURN_FALSE(buf.write(nameLenBytes) == 4);
320
CHECK_RETURN_FALSE(buf.write(nameBytes) == nameBytes.size());
321
CHECK_RETURN_FALSE(buf.write(dataLenBytes) == 4);
322
CHECK_RETURN_FALSE(buf.write(data) == data.size());
326
endBytes.append(static_cast<char>(KeePass2::VariantMapFieldType::End));
327
CHECK_RETURN_FALSE(buf.write(endBytes) == 1);