keepassxc

Форк
0
/
DatabaseSettingsWidgetEncryption.cpp 
433 строки · 16.3 Кб
1
/*
2
 *  Copyright (C) 2018 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 "DatabaseSettingsWidgetEncryption.h"
19
#include "ui_DatabaseSettingsWidgetEncryption.h"
20

21
#include "core/AsyncTask.h"
22
#include "core/Database.h"
23
#include "core/Global.h"
24
#include "core/Metadata.h"
25
#include "crypto/kdf/Argon2Kdf.h"
26
#include "format/KeePass2.h"
27
#include "format/KeePass2Writer.h"
28
#include "gui/MessageBox.h"
29

30
#include <QPushButton>
31

32
const char* DatabaseSettingsWidgetEncryption::CD_DECRYPTION_TIME_PREFERENCE_KEY = "KPXC_DECRYPTION_TIME_PREFERENCE";
33

34
#define IS_ARGON2(uuid) (uuid == KeePass2::KDF_ARGON2D || uuid == KeePass2::KDF_ARGON2ID)
35
#define IS_AES_KDF(uuid) (uuid == KeePass2::KDF_AES_KDBX3 || uuid == KeePass2::KDF_AES_KDBX4)
36

37
namespace
38
{
39
    QString getTextualEncryptionTime(int millisecs)
40
    {
41
        if (millisecs < 1000) {
42
            return QObject::tr("%1 ms", "milliseconds", millisecs).arg(millisecs);
43
        }
44
        return QObject::tr("%1 s", "seconds", millisecs / 1000).arg(millisecs / 1000.0, 0, 'f', 1);
45
    }
46
} // namespace
47

48
DatabaseSettingsWidgetEncryption::DatabaseSettingsWidgetEncryption(QWidget* parent)
49
    : DatabaseSettingsWidget(parent)
50
    , m_ui(new Ui::DatabaseSettingsWidgetEncryption())
51
{
52
    m_ui->setupUi(this);
53

54
    connect(m_ui->transformBenchmarkButton, SIGNAL(clicked()), SLOT(benchmarkTransformRounds()));
55
    connect(m_ui->kdfComboBox, SIGNAL(currentIndexChanged(int)), SLOT(changeKdf(int)));
56
    m_ui->formatCannotBeChanged->setVisible(false);
57

58
    connect(m_ui->memorySpinBox, SIGNAL(valueChanged(int)), this, SLOT(memoryChanged(int)));
59
    connect(m_ui->parallelismSpinBox, SIGNAL(valueChanged(int)), this, SLOT(parallelismChanged(int)));
60

61
    m_ui->compatibilitySelection->addItem(tr("KDBX 4 (recommended)"), KeePass2::KDF_ARGON2D.toByteArray());
62
    m_ui->compatibilitySelection->addItem(tr("KDBX 3"), KeePass2::KDF_AES_KDBX3.toByteArray());
63
    m_ui->decryptionTimeSlider->setMinimum(Kdf::MIN_ENCRYPTION_TIME / 100);
64
    m_ui->decryptionTimeSlider->setMaximum(Kdf::MAX_ENCRYPTION_TIME / 100);
65
    m_ui->decryptionTimeSlider->setValue(Kdf::DEFAULT_ENCRYPTION_TIME / 100);
66
    updateDecryptionTime(m_ui->decryptionTimeSlider->value());
67

68
    m_ui->transformBenchmarkButton->setText(
69
        QObject::tr("Benchmark %1 delay").arg(getTextualEncryptionTime(Kdf::DEFAULT_ENCRYPTION_TIME)));
70
    m_ui->minTimeLabel->setText(getTextualEncryptionTime(Kdf::MIN_ENCRYPTION_TIME));
71
    m_ui->maxTimeLabel->setText(getTextualEncryptionTime(Kdf::MAX_ENCRYPTION_TIME));
72

73
    connect(m_ui->decryptionTimeSlider, SIGNAL(valueChanged(int)), SLOT(updateDecryptionTime(int)));
74
    connect(m_ui->compatibilitySelection, SIGNAL(currentIndexChanged(int)), SLOT(updateFormatCompatibility(int)));
75

76
    // conditions under which a key re-transformation is needed
77
    connect(m_ui->decryptionTimeSlider, SIGNAL(valueChanged(int)), SLOT(markDirty()));
78
    connect(m_ui->compatibilitySelection, SIGNAL(currentIndexChanged(int)), SLOT(markDirty()));
79
    connect(m_ui->algorithmComboBox, SIGNAL(currentIndexChanged(int)), SLOT(markDirty()));
80
    connect(m_ui->kdfComboBox, SIGNAL(currentIndexChanged(int)), SLOT(markDirty()));
81
    connect(m_ui->transformRoundsSpinBox, SIGNAL(valueChanged(int)), SLOT(markDirty()));
82
    connect(m_ui->memorySpinBox, SIGNAL(valueChanged(int)), SLOT(markDirty()));
83
    connect(m_ui->parallelismSpinBox, SIGNAL(valueChanged(int)), SLOT(markDirty()));
84
}
85

86
DatabaseSettingsWidgetEncryption::~DatabaseSettingsWidgetEncryption() = default;
87

88
void DatabaseSettingsWidgetEncryption::showBasicEncryption(int decryptionMillisecs)
89
{
90
    // Show the basic encryption settings tab and set the slider to the stored values
91
    m_ui->decryptionTimeSlider->setValue(decryptionMillisecs / 100);
92
    m_ui->encryptionSettingsTabWidget->setCurrentWidget(m_ui->basicTab);
93
    m_initWithAdvanced = false;
94
}
95

96
void DatabaseSettingsWidgetEncryption::initialize()
97
{
98
    Q_ASSERT(m_db);
99
    if (!m_db) {
100
        return;
101
    }
102

103
    auto version = KDBX4;
104
    if (m_db->key() && m_db->kdf()) {
105
        version = (m_db->kdf()->uuid() == KeePass2::KDF_AES_KDBX3) ? KDBX3 : KDBX4;
106
    }
107
    m_ui->compatibilitySelection->setCurrentIndex(version);
108

109
    bool isNewDatabase = false;
110

111
    if (!m_db->key()) {
112
        m_db->setKey(QSharedPointer<CompositeKey>::create(), true, false, false);
113
        m_db->setKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2D));
114
        m_db->setCipher(KeePass2::CIPHER_AES256);
115
        isNewDatabase = true;
116
    } else if (!m_db->kdf()) {
117
        m_db->setKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2D));
118
        isNewDatabase = true;
119
    }
120

121
    bool kdbx3Enabled = KeePass2Writer::kdbxVersionRequired(m_db.data(), true, true) <= KeePass2::FILE_VERSION_3_1;
122

123
    // check if the DB's custom data has a decryption time setting stored
124
    // and set the slider to it, otherwise just state that the time is unchanged
125
    // (we cannot infer the time from the raw KDF settings)
126

127
    auto* cd = m_db->metadata()->customData();
128
    if (cd->hasKey(CD_DECRYPTION_TIME_PREFERENCE_KEY)) {
129
        int decryptionTime = qMax(100, cd->value(CD_DECRYPTION_TIME_PREFERENCE_KEY).toInt());
130
        showBasicEncryption(decryptionTime);
131
    } else if (isNewDatabase) {
132
        showBasicEncryption();
133
    } else {
134
        // Set default basic decryption time
135
        m_ui->decryptionTimeSlider->setValue(Kdf::DEFAULT_ENCRYPTION_TIME / 100);
136
        // Show the advanced encryption settings tab
137
        m_ui->encryptionSettingsTabWidget->setCurrentWidget(m_ui->advancedTab);
138
        m_initWithAdvanced = true;
139
    }
140

141
    updateFormatCompatibility(m_db->kdf()->uuid() == KeePass2::KDF_AES_KDBX3 ? KDBX3 : KDBX4, isNewDatabase);
142
    setupAlgorithmComboBox();
143
    setupKdfComboBox(kdbx3Enabled);
144
    loadKdfParameters();
145

146
    if (!kdbx3Enabled) {
147
        m_ui->compatibilitySelection->setEnabled(false);
148
        m_ui->formatCannotBeChanged->setVisible(true);
149
    }
150

151
    m_isDirty = isNewDatabase;
152
}
153

154
void DatabaseSettingsWidgetEncryption::uninitialize()
155
{
156
}
157

158
void DatabaseSettingsWidgetEncryption::showEvent(QShowEvent* event)
159
{
160
    QWidget::showEvent(event);
161
    m_ui->decryptionTimeSlider->setFocus();
162
}
163

164
void DatabaseSettingsWidgetEncryption::setupAlgorithmComboBox()
165
{
166
    m_ui->algorithmComboBox->clear();
167
    for (auto& cipher : asConst(KeePass2::CIPHERS)) {
168
        m_ui->algorithmComboBox->addItem(KeePass2::cipherToString(cipher), cipher.toByteArray());
169
    }
170
    int cipherIndex = m_ui->algorithmComboBox->findData(m_db->cipher().toByteArray());
171
    if (cipherIndex > -1) {
172
        m_ui->algorithmComboBox->setCurrentIndex(cipherIndex);
173
    }
174
}
175

176
void DatabaseSettingsWidgetEncryption::setupKdfComboBox(bool enableKdbx3)
177
{
178
    // Set up kdf combo box
179
    bool block = m_ui->kdfComboBox->blockSignals(true);
180
    m_ui->kdfComboBox->clear();
181
    for (auto& kdf : asConst(KeePass2::KDFS)) {
182
        if (kdf != KeePass2::KDF_AES_KDBX3 or enableKdbx3) {
183
            m_ui->kdfComboBox->addItem(KeePass2::kdfToString(kdf), kdf.toByteArray());
184
        }
185
    }
186
    m_ui->kdfComboBox->blockSignals(block);
187
}
188

189
void DatabaseSettingsWidgetEncryption::loadKdfParameters()
190
{
191
    Q_ASSERT(m_db);
192
    if (!m_db) {
193
        return;
194
    }
195

196
    auto kdf = m_db->kdf();
197
    if (!kdf) {
198
        return;
199
    }
200

201
    int kdfIndex = m_ui->kdfComboBox->findData(m_db->kdf()->uuid().toByteArray());
202
    if (kdfIndex > -1) {
203
        bool block = m_ui->kdfComboBox->blockSignals(true);
204
        m_ui->kdfComboBox->setCurrentIndex(kdfIndex);
205
        m_ui->kdfComboBox->blockSignals(block);
206
    }
207

208
    m_ui->transformRoundsSpinBox->setValue(kdf->rounds());
209
    if (IS_ARGON2(m_db->kdf()->uuid())) {
210
        auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
211
        m_ui->memorySpinBox->setValue(static_cast<int>(argon2Kdf->memory()) / (1 << 10));
212
        m_ui->parallelismSpinBox->setValue(argon2Kdf->parallelism());
213
    }
214

215
    updateKdfFields();
216
}
217

218
void DatabaseSettingsWidgetEncryption::updateKdfFields()
219
{
220
    QUuid id = m_db->kdf()->uuid();
221

222
    m_ui->memoryUsageLabel->setVisible(IS_ARGON2(id));
223
    m_ui->memorySpinBox->setVisible(IS_ARGON2(id));
224
    m_ui->parallelismLabel->setVisible(IS_ARGON2(id));
225
    m_ui->parallelismSpinBox->setVisible(IS_ARGON2(id));
226
}
227

228
void DatabaseSettingsWidgetEncryption::markDirty()
229
{
230
    m_isDirty = true;
231
}
232

233
bool DatabaseSettingsWidgetEncryption::save()
234
{
235
    Q_ASSERT(m_db);
236
    if (!m_db) {
237
        return false;
238
    }
239

240
    if (m_initWithAdvanced != isAdvancedMode()) {
241
        // Switched from basic <-> advanced mode, need to recalculate everything
242
        m_isDirty = true;
243
    }
244

245
    if (m_db->key() && !m_db->key()->keys().isEmpty() && !m_isDirty) {
246
        // nothing has changed, don't re-transform
247
        return true;
248
    }
249

250
    auto kdf = m_db->kdf();
251
    Q_ASSERT(kdf);
252

253
    if (!isAdvancedMode()) {
254
        if (kdf && !m_isDirty && !m_ui->decryptionTimeSettings->isVisible()) {
255
            return true;
256
        }
257

258
        int time = m_ui->decryptionTimeSlider->value() * 100;
259
        updateFormatCompatibility(m_ui->compatibilitySelection->currentIndex(), false);
260

261
        QApplication::setOverrideCursor(Qt::BusyCursor);
262

263
        int rounds = AsyncTask::runAndWaitForFuture([&kdf, time]() { return kdf->benchmark(time); });
264
        kdf->setRounds(rounds);
265

266
        // TODO: we should probably use AsyncTask::runAndWaitForFuture() here,
267
        //       but not without making Database thread-safe
268
        bool ok = m_db->changeKdf(kdf);
269

270
        QApplication::restoreOverrideCursor();
271

272
        m_db->metadata()->customData()->set(CD_DECRYPTION_TIME_PREFERENCE_KEY, QString("%1").arg(time));
273

274
        return ok;
275
    }
276

277
    // remove a stored decryption time from custom data when advanced settings are used
278
    // we don't know it until we actually run the KDF
279
    m_db->metadata()->customData()->remove(CD_DECRYPTION_TIME_PREFERENCE_KEY);
280

281
    // first perform safety check for KDF rounds
282
    if (IS_ARGON2(kdf->uuid()) && m_ui->transformRoundsSpinBox->value() > 10000) {
283
        QMessageBox warning;
284
        warning.setIcon(QMessageBox::Warning);
285
        warning.setWindowTitle(tr("Number of rounds too high", "Key transformation rounds"));
286
        warning.setText(tr("You are using a very high number of key transform rounds with Argon2.\n\n"
287
                           "If you keep this number, your database may take hours, days, or even longer to open."));
288
        auto ok = warning.addButton(tr("Understood, keep number"), QMessageBox::ButtonRole::AcceptRole);
289
        auto cancel = warning.addButton(tr("Cancel"), QMessageBox::ButtonRole::RejectRole);
290
        warning.setDefaultButton(cancel);
291
        warning.layout()->setSizeConstraint(QLayout::SetMinimumSize);
292
        warning.exec();
293
        if (warning.clickedButton() != ok) {
294
            return false;
295
        }
296
    } else if (IS_AES_KDF(kdf->uuid()) && m_ui->transformRoundsSpinBox->value() < 100000) {
297
        QMessageBox warning;
298
        warning.setIcon(QMessageBox::Warning);
299
        warning.setWindowTitle(tr("Number of rounds too low", "Key transformation rounds"));
300
        warning.setText(tr("You are using a very low number of key transform rounds with AES-KDF.\n\n"
301
                           "If you keep this number, your database will not be protected from brute force attacks."));
302
        auto ok = warning.addButton(tr("Understood, keep number"), QMessageBox::ButtonRole::AcceptRole);
303
        auto cancel = warning.addButton(tr("Cancel"), QMessageBox::ButtonRole::RejectRole);
304
        warning.setDefaultButton(cancel);
305
        warning.layout()->setSizeConstraint(QLayout::SetMinimumSize);
306
        warning.exec();
307
        if (warning.clickedButton() != ok) {
308
            return false;
309
        }
310
    }
311

312
    m_db->setCipher(QUuid(m_ui->algorithmComboBox->currentData().toByteArray()));
313

314
    // Save kdf parameters
315
    kdf->setRounds(m_ui->transformRoundsSpinBox->value());
316
    if (IS_ARGON2(kdf->uuid())) {
317
        auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
318
        argon2Kdf->setMemory(static_cast<quint64>(m_ui->memorySpinBox->value()) * (1 << 10));
319
        argon2Kdf->setParallelism(static_cast<quint32>(m_ui->parallelismSpinBox->value()));
320
    }
321

322
    QApplication::setOverrideCursor(Qt::WaitCursor);
323
    // TODO: we should probably use AsyncTask::runAndWaitForFuture() here,
324
    //       but not without making Database thread-safe
325
    bool ok = m_db->changeKdf(kdf);
326
    QApplication::restoreOverrideCursor();
327

328
    if (!ok) {
329
        MessageBox::warning(this,
330
                            tr("KDF unchanged"),
331
                            tr("Failed to transform key with new KDF parameters; KDF unchanged."),
332
                            QMessageBox::Ok);
333
    }
334

335
    return ok;
336
}
337

338
void DatabaseSettingsWidgetEncryption::benchmarkTransformRounds(int millisecs)
339
{
340
    QApplication::setOverrideCursor(Qt::BusyCursor);
341
    m_ui->transformBenchmarkButton->setEnabled(false);
342
    m_ui->transformRoundsSpinBox->setFocus();
343

344
    // Create a new kdf with the current parameters
345
    auto kdf = KeePass2::uuidToKdf(QUuid(m_ui->kdfComboBox->currentData().toByteArray()));
346
    kdf->setRounds(m_ui->transformRoundsSpinBox->value());
347
    if (IS_ARGON2(kdf->uuid())) {
348
        auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
349
        // Set a small static number of rounds for the benchmark
350
        argon2Kdf->setRounds(4);
351
        if (!argon2Kdf->setMemory(static_cast<quint64>(m_ui->memorySpinBox->value()) * (1 << 10))) {
352
            m_ui->memorySpinBox->setValue(static_cast<int>(argon2Kdf->memory() / (1 << 10)));
353
        }
354
        if (!argon2Kdf->setParallelism(static_cast<quint32>(m_ui->parallelismSpinBox->value()))) {
355
            m_ui->parallelismSpinBox->setValue(argon2Kdf->parallelism());
356
        }
357
    }
358

359
    // Determine the number of rounds required to meet 1 second delay
360
    int rounds = AsyncTask::runAndWaitForFuture([&kdf, millisecs]() { return kdf->benchmark(millisecs); });
361

362
    m_ui->transformRoundsSpinBox->setValue(rounds);
363
    m_ui->transformBenchmarkButton->setEnabled(true);
364
    m_ui->decryptionTimeSlider->setValue(millisecs / 100);
365
    QApplication::restoreOverrideCursor();
366
}
367

368
void DatabaseSettingsWidgetEncryption::changeKdf(int index)
369
{
370
    Q_ASSERT(m_db);
371
    if (!m_db) {
372
        return;
373
    }
374

375
    QUuid id(m_ui->kdfComboBox->itemData(index).toByteArray());
376
    m_db->setKdf(KeePass2::uuidToKdf(id));
377
    updateKdfFields();
378
    benchmarkTransformRounds();
379
}
380

381
/**
382
 * Update memory spin box suffix on value change.
383
 */
384
void DatabaseSettingsWidgetEncryption::memoryChanged(int value)
385
{
386
    m_ui->memorySpinBox->setSuffix(tr(" MiB", "Abbreviation for Mebibytes (KDF settings)", value));
387
}
388

389
/**
390
 * Update parallelism spin box suffix on value change.
391
 */
392
void DatabaseSettingsWidgetEncryption::parallelismChanged(int value)
393
{
394
    m_ui->parallelismSpinBox->setSuffix(tr(" thread(s)", "Threads for parallel execution (KDF settings)", value));
395
}
396

397
bool DatabaseSettingsWidgetEncryption::isAdvancedMode()
398
{
399
    return m_ui->encryptionSettingsTabWidget->currentWidget() == m_ui->advancedTab;
400
}
401

402
void DatabaseSettingsWidgetEncryption::updateDecryptionTime(int value)
403
{
404
    m_ui->decryptionTimeValueLabel->setText(getTextualEncryptionTime(value * 100));
405
}
406

407
void DatabaseSettingsWidgetEncryption::updateFormatCompatibility(int index, bool retransform)
408
{
409
    Q_ASSERT(m_db);
410
    if (!m_db) {
411
        return;
412
    }
413

414
    if (m_ui->compatibilitySelection->currentIndex() != index) {
415
        bool block = m_ui->compatibilitySelection->blockSignals(true);
416
        m_ui->compatibilitySelection->setCurrentIndex(index);
417
        m_ui->compatibilitySelection->blockSignals(block);
418
    }
419

420
    QUuid kdfUuid(m_ui->compatibilitySelection->itemData(index).toByteArray());
421
    if (retransform) {
422
        auto kdf = KeePass2::uuidToKdf(kdfUuid);
423
        m_db->setKdf(kdf);
424

425
        if (IS_ARGON2(kdf->uuid())) {
426
            auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
427
            // Default to 64 MiB of memory and 2 threads
428
            // these settings are safe for desktop and mobile devices
429
            argon2Kdf->setMemory(1 << 16);
430
            argon2Kdf->setParallelism(2);
431
        }
432
    }
433
}
434

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

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

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

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