keepassxc

Форк
0
/
Kdbx4Writer.cpp 
329 строк · 12.4 Кб
1
/*
2
 *  Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
3
 *
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.
8
 *
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.
13
 *
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/>.
16
 */
17

18
#include "Kdbx4Writer.h"
19

20
#include <QBuffer>
21

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"
29
#endif
30
#include "streams/HmacBlockStream.h"
31
#include "streams/SymmetricCipherStream.h"
32
#include "streams/qtiocompressor.h"
33

34
bool Kdbx4Writer::writeDatabase(QIODevice* device, Database* db)
35
{
36
    m_error = false;
37
    m_errorStr.clear();
38

39
    auto mode = SymmetricCipher::cipherUuidToMode(db->cipher());
40
    if (mode == SymmetricCipher::InvalidMode) {
41
        raiseError(tr("Invalid symmetric cipher algorithm."));
42
        return false;
43
    }
44
    int ivSize = SymmetricCipher::defaultIvSize(mode);
45
    if (ivSize < 0) {
46
        raiseError(tr("Invalid symmetric cipher IV size.", "IV = Initialization Vector for symmetric cipher"));
47
        return false;
48
    }
49

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";
54

55
    if (!db->setKey(db->key(), false, true)) {
56
        raiseError(tr("Unable to calculate database key: %1").arg(db->keyError()));
57
        return false;
58
    }
59

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();
66

67
    // write header
68
    QByteArray headerData;
69
    {
70
        QBuffer header;
71
        header.open(QIODevice::WriteOnly);
72

73
        writeMagicNumbers(&header, KeePass2::SIGNATURE_1, KeePass2::SIGNATURE_2, db->formatVersion());
74

75
        CHECK_RETURN_FALSE(
76
            writeHeaderField<quint32>(&header, KeePass2::HeaderFieldID::CipherID, db->cipher().toRfc4122()));
77
        CHECK_RETURN_FALSE(writeHeaderField<quint32>(
78
            &header,
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));
83

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"));
90
            return false;
91
        }
92

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);
98
            CHECK_RETURN_FALSE(
99
                writeHeaderField<quint32>(&header, KeePass2::HeaderFieldID::PublicCustomData, serialized));
100
        }
101

102
        CHECK_RETURN_FALSE(writeHeaderField<quint32>(&header, KeePass2::HeaderFieldID::EndOfHeader, endOfHeader));
103
        header.close();
104
        headerData = header.data();
105
    }
106
    CHECK_RETURN_FALSE(writeData(device, headerData));
107

108
    // hash header
109
    QByteArray headerHash = CryptoHash::hash(headerData, CryptoHash::Sha256);
110

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));
117

118
    QScopedPointer<HmacBlockStream> hmacBlockStream;
119
    QScopedPointer<SymmetricCipherStream> cipherStream;
120

121
    hmacBlockStream.reset(new HmacBlockStream(device, hmacKey));
122
    if (!hmacBlockStream->open(QIODevice::WriteOnly)) {
123
        raiseError(hmacBlockStream->errorString());
124
        return false;
125
    }
126

127
    cipherStream.reset(new SymmetricCipherStream(hmacBlockStream.data()));
128

129
    if (!cipherStream->init(mode, SymmetricCipher::Encrypt, finalKey, encryptionIV)) {
130
        raiseError(cipherStream->errorString());
131
        return false;
132
    }
133
    if (!cipherStream->open(QIODevice::WriteOnly)) {
134
        raiseError(cipherStream->errorString());
135
        return false;
136
    }
137

138
    QIODevice* outputDevice = nullptr;
139
    QScopedPointer<QtIOCompressor> ioCompressor;
140

141
    if (db->compressionAlgorithm() == Database::CompressionNone) {
142
        outputDevice = cipherStream.data();
143
    } else {
144
        ioCompressor.reset(new QtIOCompressor(cipherStream.data()));
145
        ioCompressor->setStreamFormat(QtIOCompressor::GzipFormat);
146
        if (!ioCompressor->open(QIODevice::WriteOnly)) {
147
            raiseError(ioCompressor->errorString());
148
            return false;
149
        }
150
        outputDevice = ioCompressor.data();
151
    }
152

153
    Q_ASSERT(outputDevice);
154

155
    CHECK_RETURN_FALSE(writeInnerHeaderField(
156
        outputDevice,
157
        KeePass2::InnerHeaderFieldID::InnerRandomStreamID,
158
        Endian::sizedIntToBytes(static_cast<int>(KeePass2::ProtectedStreamAlgo::ChaCha20), KeePass2::BYTEORDER)));
159
    CHECK_RETURN_FALSE(
160
        writeInnerHeaderField(outputDevice, KeePass2::InnerHeaderFieldID::InnerRandomStreamKey, protectedStreamKey));
161

162
    // Write attachments to the inner header
163
    auto idxMap = writeAttachments(outputDevice, db);
164

165
    CHECK_RETURN_FALSE(writeInnerHeaderField(outputDevice, KeePass2::InnerHeaderFieldID::End, QByteArray()));
166

167
    KeePass2RandomStream randomStream;
168
    if (!randomStream.init(SymmetricCipher::ChaCha20, protectedStreamKey)) {
169
        raiseError(randomStream.errorString());
170
        return false;
171
    }
172

173
    KdbxXmlWriter xmlWriter(db->formatVersion(), idxMap);
174
    xmlWriter.writeDatabase(outputDevice, db, &randomStream, headerHash);
175

176
    // Explicitly close/reset streams so they are flushed and we can detect
177
    // errors. QIODevice::close() resets errorString() etc.
178
    if (ioCompressor) {
179
        ioCompressor->close();
180
    }
181
    if (!cipherStream->reset()) {
182
        raiseError(cipherStream->errorString());
183
        return false;
184
    }
185
    if (!hmacBlockStream->reset()) {
186
        raiseError(hmacBlockStream->errorString());
187
        return false;
188
    }
189

190
    if (xmlWriter.hasError()) {
191
        raiseError(xmlWriter.errorString());
192
        return false;
193
    }
194

195
    return true;
196
}
197

198
/**
199
 * Write KDBX4 inner header field.
200
 *
201
 * @param device output device
202
 * @param fieldId field identifier
203
 * @param data header payload
204
 * @return true on success
205
 */
206
bool Kdbx4Writer::writeInnerHeaderField(QIODevice* device, KeePass2::InnerHeaderFieldID fieldId, const QByteArray& data)
207
{
208
    QByteArray fieldIdArr;
209
    fieldIdArr.append(static_cast<char>(fieldId));
210
    CHECK_RETURN_FALSE(writeData(device, fieldIdArr));
211
    CHECK_RETURN_FALSE(
212
        writeData(device, Endian::sizedIntToBytes(static_cast<quint32>(data.size()), KeePass2::BYTEORDER)));
213
    CHECK_RETURN_FALSE(writeData(device, data));
214

215
    return true;
216
}
217

218
KdbxXmlWriter::BinaryIdxMap Kdbx4Writer::writeAttachments(QIODevice* device, Database* db)
219
{
220
    const QList<Entry*> allEntries = db->rootGroup()->entriesRecursive(true);
221
    QHash<QByteArray, qint64> writtenAttachments;
222
    KdbxXmlWriter::BinaryIdxMap idxMap;
223
    qint64 nextIdx = 0;
224

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));
230

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());
237
            } else {
238
                hash.addData(db->uuid().toByteArray());
239
            }
240
#endif
241
            hash.addData(data);
242

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++);
248
            }
249
            idxMap.insert(qMakePair(entry, key), writtenAttachments[hashResult]);
250
        }
251
    }
252

253
    return idxMap;
254
}
255

256
/**
257
 * Serialize variant map to byte array.
258
 *
259
 * @param map input variant map
260
 * @param outputBytes output byte array
261
 * @return true on success
262
 */
263
bool Kdbx4Writer::serializeVariantMap(const QVariantMap& map, QByteArray& outputBytes)
264
{
265
    QBuffer buf(&outputBytes);
266
    buf.open(QIODevice::WriteOnly);
267
    CHECK_RETURN_FALSE(buf.write(Endian::sizedIntToBytes(KeePass2::VARIANTMAP_VERSION, KeePass2::BYTEORDER)) == 2);
268

269
    bool ok;
270
    QList<QString> keys = map.keys();
271
    for (const auto& k : keys) {
272
        KeePass2::VariantMapFieldType fieldType;
273
        QByteArray data;
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);
280
            break;
281
        case QMetaType::Type::UInt:
282
            fieldType = KeePass2::VariantMapFieldType::UInt32;
283
            data = Endian::sizedIntToBytes(v.toUInt(&ok), KeePass2::BYTEORDER);
284
            CHECK_RETURN_FALSE(ok);
285
            break;
286
        case QMetaType::Type::LongLong:
287
            fieldType = KeePass2::VariantMapFieldType::Int64;
288
            data = Endian::sizedIntToBytes(v.toLongLong(&ok), KeePass2::BYTEORDER);
289
            CHECK_RETURN_FALSE(ok);
290
            break;
291
        case QMetaType::Type::ULongLong:
292
            fieldType = KeePass2::VariantMapFieldType::UInt64;
293
            data = Endian::sizedIntToBytes(v.toULongLong(&ok), KeePass2::BYTEORDER);
294
            CHECK_RETURN_FALSE(ok);
295
            break;
296
        case QMetaType::Type::QString:
297
            fieldType = KeePass2::VariantMapFieldType::String;
298
            data = v.toString().toUtf8();
299
            break;
300
        case QMetaType::Type::Bool:
301
            fieldType = KeePass2::VariantMapFieldType::Bool;
302
            data = QByteArray(1, static_cast<char>(v.toBool() ? '\1' : '\0'));
303
            break;
304
        case QMetaType::Type::QByteArray:
305
            fieldType = KeePass2::VariantMapFieldType::ByteArray;
306
            data = v.toByteArray();
307
            break;
308
        default:
309
            qWarning("Unknown object type %d in QVariantMap", v.type());
310
            return false;
311
        }
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);
317

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());
323
    }
324

325
    QByteArray endBytes;
326
    endBytes.append(static_cast<char>(KeePass2::VariantMapFieldType::End));
327
    CHECK_RETURN_FALSE(buf.write(endBytes) == 1);
328
    return true;
329
}
330

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.