keepassxc

Форк
0
/
BitwardenReader.cpp 
354 строки · 13.9 Кб
1
/*
2
 *  Copyright (C) 2023 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 "BitwardenReader.h"
19

20
#include "core/Database.h"
21
#include "core/Entry.h"
22
#include "core/Group.h"
23
#include "core/Metadata.h"
24
#include "core/Tools.h"
25
#include "core/Totp.h"
26
#include "crypto/CryptoHash.h"
27
#include "crypto/SymmetricCipher.h"
28
#include "crypto/kdf/Argon2Kdf.h"
29

30
#include <botan/kdf.h>
31
#include <botan/pwdhash.h>
32

33
#include <QFileInfo>
34
#include <QJsonArray>
35
#include <QJsonDocument>
36
#include <QJsonObject>
37
#include <QJsonParseError>
38
#include <QMap>
39
#include <QScopedPointer>
40
#include <QUrl>
41

42
namespace
43
{
44
    Entry* readItem(const QJsonObject& item, QString& folderId)
45
    {
46
        // Create the item map and extract the folder id
47
        const auto itemMap = item.toVariantMap();
48
        folderId = itemMap.value("folderId").toString();
49
        if (folderId.isEmpty()) {
50
            // Bitwarden organization vaults use collectionId instead of folderId
51
            auto collectionIds = itemMap.value("collectionIds").toStringList();
52
            if (!collectionIds.empty()) {
53
                folderId = collectionIds.first();
54
            }
55
        }
56

57
        // Create entry and assign basic values
58
        QScopedPointer<Entry> entry(new Entry());
59
        entry->setUuid(QUuid::createUuid());
60
        entry->setTitle(itemMap.value("name").toString());
61
        entry->setNotes(itemMap.value("notes").toString());
62

63
        if (itemMap.value("favorite").toBool()) {
64
            entry->addTag(QObject::tr("Favorite", "Tag for favorite entries"));
65
        }
66

67
        // Parse login details if present
68
        if (itemMap.contains("login")) {
69
            const auto loginMap = itemMap.value("login").toMap();
70
            entry->setUsername(loginMap.value("username").toString());
71
            entry->setPassword(loginMap.value("password").toString());
72
            if (loginMap.contains("totp")) {
73
                auto totp = loginMap.value("totp").toString();
74
                if (!totp.startsWith("otpauth://")) {
75
                    QUrl url(QString("otpauth://totp/%1:%2?secret=%3")
76
                                 .arg(QString(QUrl::toPercentEncoding(entry->title())),
77
                                      QString(QUrl::toPercentEncoding(entry->username())),
78
                                      QString(QUrl::toPercentEncoding(totp))));
79
                    totp = url.toString(QUrl::FullyEncoded);
80
                }
81
                entry->setTotp(Totp::parseSettings(totp));
82
            }
83

84
            // Set the entry url(s)
85
            int i = 1;
86
            for (const auto& urlObj : loginMap.value("uris").toList()) {
87
                const auto url = urlObj.toMap().value("uri").toString();
88
                if (entry->url().isEmpty()) {
89
                    // First url encountered is set as the primary url
90
                    entry->setUrl(url);
91
                } else {
92
                    // Subsequent urls
93
                    entry->attributes()->set(
94
                        QString("%1_%2").arg(EntryAttributes::AdditionalUrlAttribute, QString::number(i)), url);
95
                    ++i;
96
                }
97
            }
98
        }
99

100
        // Parse identity details if present
101
        if (itemMap.contains("identity")) {
102
            const auto idMap = itemMap.value("identity").toMap();
103

104
            // Combine name attributes
105
            auto attrs = QStringList({idMap.value("title").toString(),
106
                                      idMap.value("firstName").toString(),
107
                                      idMap.value("middleName").toString(),
108
                                      idMap.value("lastName").toString()});
109
            attrs.removeAll("");
110
            entry->attributes()->set("identity_name", attrs.join(" "));
111

112
            // Combine all the address attributes
113
            attrs = QStringList({idMap.value("address1").toString(),
114
                                 idMap.value("address2").toString(),
115
                                 idMap.value("address3").toString()});
116
            attrs.removeAll("");
117
            auto address = attrs.join("\n") + "\n" + idMap.value("city").toString() + ", "
118
                           + idMap.value("state").toString() + " " + idMap.value("postalCode").toString() + "\n"
119
                           + idMap.value("country").toString();
120
            entry->attributes()->set("identity_address", address);
121

122
            // Add the remaining attributes
123
            attrs = QStringList({"company", "email", "phone", "ssn", "passportNumber", "licenseNumber"});
124
            const QStringList sensitive({"ssn", "passportNumber", "licenseNumber"});
125
            for (const auto& attr : attrs) {
126
                const auto value = idMap.value(attr).toString();
127
                if (!value.isEmpty()) {
128
                    entry->attributes()->set("identity_" + attr, value, sensitive.contains(attr));
129
                }
130
            }
131

132
            // Set the username or push it into attributes if already set
133
            const auto username = idMap.value("username").toString();
134
            if (!username.isEmpty()) {
135
                if (entry->username().isEmpty()) {
136
                    entry->setUsername(username);
137
                } else {
138
                    entry->attributes()->set("identity_username", username);
139
                }
140
            }
141
        }
142

143
        // Parse card details if present
144
        if (itemMap.contains("card")) {
145
            const auto cardMap = itemMap.value("card").toMap();
146
            const QStringList attrs({"cardholderName", "brand", "number", "expMonth", "expYear", "code"});
147
            const QStringList sensitive({"code"});
148
            for (const auto& attr : attrs) {
149
                auto value = cardMap.value(attr).toString();
150
                if (!value.isEmpty()) {
151
                    entry->attributes()->set("card_" + attr, value, sensitive.contains(attr));
152
                }
153
            }
154
        }
155

156
        // Parse remaining fields
157
        for (const auto& field : itemMap.value("fields").toList()) {
158
            // Derive a prefix for attribute names using the title or uuid if missing
159
            const auto fieldMap = field.toMap();
160
            auto name = fieldMap.value("name").toString();
161
            if (entry->attributes()->hasKey(name)) {
162
                name = QString("%1_%2").arg(name, QUuid::createUuid().toString().mid(1, 5));
163
            }
164

165
            const auto value = fieldMap.value("value").toString();
166
            const auto type = fieldMap.value("type").toInt();
167

168
            entry->attributes()->set(name, value, type == 1);
169
        }
170

171
        // Collapse any accumulated history
172
        entry->removeHistoryItems(entry->historyItems());
173

174
        return entry.take();
175
    }
176

177
    void writeVaultToDatabase(const QJsonObject& vault, QSharedPointer<Database> db)
178
    {
179
        auto folderField = QString("folders");
180
        if (!vault.contains(folderField)) {
181
            // Handle Bitwarden organization vaults
182
            folderField = "collections";
183
        }
184

185
        if (!vault.contains(folderField) || !vault.contains("items")) {
186
            // Early out if the vault is missing critical items
187
            return;
188
        }
189

190
        // Create groups from folders and store a temporary map of id -> uuid
191
        QMap<QString, Group*> folderMap;
192
        for (const auto& folder : vault.value(folderField).toArray()) {
193
            auto group = new Group();
194
            group->setUuid(QUuid::createUuid());
195
            group->setName(folder.toObject().value("name").toString());
196
            group->setParent(db->rootGroup());
197

198
            folderMap.insert(folder.toObject().value("id").toString(), group);
199
        }
200

201
        QString folderId;
202
        const auto items = vault.value("items").toArray();
203
        for (const auto& item : items) {
204
            auto entry = readItem(item.toObject(), folderId);
205
            if (entry) {
206
                entry->setGroup(folderMap.value(folderId, db->rootGroup()), false);
207
            }
208
        }
209
    }
210
} // namespace
211

212
bool BitwardenReader::hasError()
213
{
214
    return !m_error.isEmpty();
215
}
216

217
QString BitwardenReader::errorString()
218
{
219
    return m_error;
220
}
221

222
QSharedPointer<Database> BitwardenReader::convert(const QString& path, const QString& password)
223
{
224
    m_error.clear();
225

226
    QFileInfo fileinfo(path);
227
    if (!fileinfo.exists()) {
228
        m_error = QObject::tr("File does not exist.").arg(path);
229
        return {};
230
    }
231

232
    // Bitwarden uses a json file format
233
    QFile file(fileinfo.absoluteFilePath());
234
    if (!file.open(QFile::ReadOnly)) {
235
        m_error = QObject::tr("Cannot open file: %1").arg(file.errorString());
236
        return {};
237
    }
238

239
    QJsonParseError error;
240
    auto json = QJsonDocument::fromJson(file.readAll(), &error).object();
241
    if (error.error != QJsonParseError::NoError) {
242
        m_error =
243
            QObject::tr("Cannot parse file: %1 at position %2").arg(error.errorString(), QString::number(error.offset));
244
        return {};
245
    }
246

247
    file.close();
248

249
    // Check if this is an encrypted json
250
    if (json.contains("encrypted") && json.value("encrypted").toBool()) {
251
        auto buildError = [](const QString& errorString) {
252
            return QObject::tr("Failed to decrypt json file: %1").arg(errorString);
253
        };
254

255
        QByteArray key(32, '\0');
256
        auto salt = json.value("salt").toString().toUtf8();
257
        auto kdfType = json.value("kdfType").toInt();
258

259
        // Derive the Master Key
260
        if (kdfType == 0) {
261
            auto pwd_fam = Botan::PasswordHashFamily::create_or_throw("PBKDF2(SHA-256)");
262
            auto pwd_hash = pwd_fam->from_params(json.value("kdfIterations").toInt());
263
            pwd_hash->derive_key(reinterpret_cast<uint8_t*>(key.data()),
264
                                 key.size(),
265
                                 password.toUtf8().data(),
266
                                 password.toUtf8().size(),
267
                                 reinterpret_cast<uint8_t*>(salt.data()),
268
                                 salt.size());
269
        } else if (kdfType == 1) {
270
            // Bitwarden hashes the salt for Argon2 for some reason
271
            CryptoHash saltHash(CryptoHash::Sha256);
272
            saltHash.addData(salt);
273
            salt = saltHash.result();
274

275
            Argon2Kdf argon2(Argon2Kdf::Type::Argon2id);
276
            argon2.setSeed(salt);
277
            argon2.setRounds(json.value("kdfIterations").toInt());
278
            argon2.setMemory(json.value("kdfMemory").toInt() * 1024);
279
            argon2.setParallelism(json.value("kdfParallelism").toInt());
280
            argon2.transform(password.toUtf8(), key);
281
        } else {
282
            m_error = buildError(QObject::tr("Unsupported KDF type, cannot decrypt json file"));
283
            return {};
284
        }
285

286
        auto hkdf = Botan::KDF::create_or_throw("HKDF-Expand(SHA-256)");
287

288
        // Derive the MAC Key
289
        auto stretched_mac = hkdf->derive_key(32, reinterpret_cast<const uint8_t*>(key.data()), key.size(), "", "mac");
290
        auto mac = QByteArray(reinterpret_cast<const char*>(stretched_mac.data()), stretched_mac.size());
291

292
        // Stretch the Master Key
293
        auto stretched_key = hkdf->derive_key(32, reinterpret_cast<const uint8_t*>(key.data()), key.size(), "", "enc");
294
        key = QByteArray(reinterpret_cast<const char*>(stretched_key.data()), stretched_key.size());
295

296
        // Validate the encryption key
297
        auto keyList = json.value("encKeyValidation_DO_NOT_EDIT").toString().split(".");
298
        if (keyList.size() < 2) {
299
            m_error = buildError(QObject::tr("Invalid encKeyValidation field"));
300
            return {};
301
        }
302
        auto cipherList = keyList[1].split("|");
303
        if (cipherList.size() < 3) {
304
            m_error = buildError(QObject::tr("Invalid cipher list within encKeyValidation field"));
305
            return {};
306
        }
307
        CryptoHash hash(CryptoHash::Sha256, true);
308
        hash.setKey(mac);
309
        hash.addData(QByteArray::fromBase64(cipherList[0].toUtf8())); // iv
310
        hash.addData(QByteArray::fromBase64(cipherList[1].toUtf8())); // ciphertext
311
        if (hash.result().toBase64() != cipherList[2].toUtf8()) {
312
            // Calculated MAC doesn't equal the Validation
313
            m_error = buildError(QObject::tr("Wrong password"));
314
            return {};
315
        }
316

317
        // Decrypt data field using AES-256-CBC
318
        keyList = json.value("data").toString().split(".");
319
        if (keyList.size() < 2) {
320
            m_error = buildError(QObject::tr("Invalid encrypted data field"));
321
            return {};
322
        }
323
        cipherList = keyList[1].split("|");
324
        if (cipherList.size() < 2) {
325
            m_error = buildError(QObject::tr("Invalid cipher list within encrypted data field"));
326
            return {};
327
        }
328
        auto iv = QByteArray::fromBase64(cipherList[0].toUtf8());
329
        auto data = QByteArray::fromBase64(cipherList[1].toUtf8());
330

331
        SymmetricCipher cipher;
332
        if (!cipher.init(SymmetricCipher::Aes256_CBC, SymmetricCipher::Decrypt, key, iv)) {
333
            m_error = buildError(QObject::tr("Cannot initialize cipher"));
334
            return {};
335
        }
336
        if (!cipher.finish(data)) {
337
            m_error = buildError(QObject::tr("Cannot decrypt data"));
338
            return {};
339
        }
340

341
        json = QJsonDocument::fromJson(data, &error).object();
342
        if (error.error != QJsonParseError::NoError) {
343
            m_error = buildError(error.errorString());
344
            return {};
345
        }
346
    }
347

348
    auto db = QSharedPointer<Database>::create();
349
    db->rootGroup()->setName(QObject::tr("Bitwarden Import"));
350

351
    writeVaultToDatabase(json, db);
352

353
    return db;
354
}
355

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

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

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

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