keepassxc
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
32const 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
37namespace
38{
39QString getTextualEncryptionTime(int millisecs)
40{
41if (millisecs < 1000) {
42return QObject::tr("%1 ms", "milliseconds", millisecs).arg(millisecs);
43}
44return QObject::tr("%1 s", "seconds", millisecs / 1000).arg(millisecs / 1000.0, 0, 'f', 1);
45}
46} // namespace
47
48DatabaseSettingsWidgetEncryption::DatabaseSettingsWidgetEncryption(QWidget* parent)
49: DatabaseSettingsWidget(parent)
50, m_ui(new Ui::DatabaseSettingsWidgetEncryption())
51{
52m_ui->setupUi(this);
53
54connect(m_ui->transformBenchmarkButton, SIGNAL(clicked()), SLOT(benchmarkTransformRounds()));
55connect(m_ui->kdfComboBox, SIGNAL(currentIndexChanged(int)), SLOT(changeKdf(int)));
56m_ui->formatCannotBeChanged->setVisible(false);
57
58connect(m_ui->memorySpinBox, SIGNAL(valueChanged(int)), this, SLOT(memoryChanged(int)));
59connect(m_ui->parallelismSpinBox, SIGNAL(valueChanged(int)), this, SLOT(parallelismChanged(int)));
60
61m_ui->compatibilitySelection->addItem(tr("KDBX 4 (recommended)"), KeePass2::KDF_ARGON2D.toByteArray());
62m_ui->compatibilitySelection->addItem(tr("KDBX 3"), KeePass2::KDF_AES_KDBX3.toByteArray());
63m_ui->decryptionTimeSlider->setMinimum(Kdf::MIN_ENCRYPTION_TIME / 100);
64m_ui->decryptionTimeSlider->setMaximum(Kdf::MAX_ENCRYPTION_TIME / 100);
65m_ui->decryptionTimeSlider->setValue(Kdf::DEFAULT_ENCRYPTION_TIME / 100);
66updateDecryptionTime(m_ui->decryptionTimeSlider->value());
67
68m_ui->transformBenchmarkButton->setText(
69QObject::tr("Benchmark %1 delay").arg(getTextualEncryptionTime(Kdf::DEFAULT_ENCRYPTION_TIME)));
70m_ui->minTimeLabel->setText(getTextualEncryptionTime(Kdf::MIN_ENCRYPTION_TIME));
71m_ui->maxTimeLabel->setText(getTextualEncryptionTime(Kdf::MAX_ENCRYPTION_TIME));
72
73connect(m_ui->decryptionTimeSlider, SIGNAL(valueChanged(int)), SLOT(updateDecryptionTime(int)));
74connect(m_ui->compatibilitySelection, SIGNAL(currentIndexChanged(int)), SLOT(updateFormatCompatibility(int)));
75
76// conditions under which a key re-transformation is needed
77connect(m_ui->decryptionTimeSlider, SIGNAL(valueChanged(int)), SLOT(markDirty()));
78connect(m_ui->compatibilitySelection, SIGNAL(currentIndexChanged(int)), SLOT(markDirty()));
79connect(m_ui->algorithmComboBox, SIGNAL(currentIndexChanged(int)), SLOT(markDirty()));
80connect(m_ui->kdfComboBox, SIGNAL(currentIndexChanged(int)), SLOT(markDirty()));
81connect(m_ui->transformRoundsSpinBox, SIGNAL(valueChanged(int)), SLOT(markDirty()));
82connect(m_ui->memorySpinBox, SIGNAL(valueChanged(int)), SLOT(markDirty()));
83connect(m_ui->parallelismSpinBox, SIGNAL(valueChanged(int)), SLOT(markDirty()));
84}
85
86DatabaseSettingsWidgetEncryption::~DatabaseSettingsWidgetEncryption() = default;
87
88void DatabaseSettingsWidgetEncryption::showBasicEncryption(int decryptionMillisecs)
89{
90// Show the basic encryption settings tab and set the slider to the stored values
91m_ui->decryptionTimeSlider->setValue(decryptionMillisecs / 100);
92m_ui->encryptionSettingsTabWidget->setCurrentWidget(m_ui->basicTab);
93m_initWithAdvanced = false;
94}
95
96void DatabaseSettingsWidgetEncryption::initialize()
97{
98Q_ASSERT(m_db);
99if (!m_db) {
100return;
101}
102
103auto version = KDBX4;
104if (m_db->key() && m_db->kdf()) {
105version = (m_db->kdf()->uuid() == KeePass2::KDF_AES_KDBX3) ? KDBX3 : KDBX4;
106}
107m_ui->compatibilitySelection->setCurrentIndex(version);
108
109bool isNewDatabase = false;
110
111if (!m_db->key()) {
112m_db->setKey(QSharedPointer<CompositeKey>::create(), true, false, false);
113m_db->setKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2D));
114m_db->setCipher(KeePass2::CIPHER_AES256);
115isNewDatabase = true;
116} else if (!m_db->kdf()) {
117m_db->setKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2D));
118isNewDatabase = true;
119}
120
121bool 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
127auto* cd = m_db->metadata()->customData();
128if (cd->hasKey(CD_DECRYPTION_TIME_PREFERENCE_KEY)) {
129int decryptionTime = qMax(100, cd->value(CD_DECRYPTION_TIME_PREFERENCE_KEY).toInt());
130showBasicEncryption(decryptionTime);
131} else if (isNewDatabase) {
132showBasicEncryption();
133} else {
134// Set default basic decryption time
135m_ui->decryptionTimeSlider->setValue(Kdf::DEFAULT_ENCRYPTION_TIME / 100);
136// Show the advanced encryption settings tab
137m_ui->encryptionSettingsTabWidget->setCurrentWidget(m_ui->advancedTab);
138m_initWithAdvanced = true;
139}
140
141updateFormatCompatibility(m_db->kdf()->uuid() == KeePass2::KDF_AES_KDBX3 ? KDBX3 : KDBX4, isNewDatabase);
142setupAlgorithmComboBox();
143setupKdfComboBox(kdbx3Enabled);
144loadKdfParameters();
145
146if (!kdbx3Enabled) {
147m_ui->compatibilitySelection->setEnabled(false);
148m_ui->formatCannotBeChanged->setVisible(true);
149}
150
151m_isDirty = isNewDatabase;
152}
153
154void DatabaseSettingsWidgetEncryption::uninitialize()
155{
156}
157
158void DatabaseSettingsWidgetEncryption::showEvent(QShowEvent* event)
159{
160QWidget::showEvent(event);
161m_ui->decryptionTimeSlider->setFocus();
162}
163
164void DatabaseSettingsWidgetEncryption::setupAlgorithmComboBox()
165{
166m_ui->algorithmComboBox->clear();
167for (auto& cipher : asConst(KeePass2::CIPHERS)) {
168m_ui->algorithmComboBox->addItem(KeePass2::cipherToString(cipher), cipher.toByteArray());
169}
170int cipherIndex = m_ui->algorithmComboBox->findData(m_db->cipher().toByteArray());
171if (cipherIndex > -1) {
172m_ui->algorithmComboBox->setCurrentIndex(cipherIndex);
173}
174}
175
176void DatabaseSettingsWidgetEncryption::setupKdfComboBox(bool enableKdbx3)
177{
178// Set up kdf combo box
179bool block = m_ui->kdfComboBox->blockSignals(true);
180m_ui->kdfComboBox->clear();
181for (auto& kdf : asConst(KeePass2::KDFS)) {
182if (kdf != KeePass2::KDF_AES_KDBX3 or enableKdbx3) {
183m_ui->kdfComboBox->addItem(KeePass2::kdfToString(kdf), kdf.toByteArray());
184}
185}
186m_ui->kdfComboBox->blockSignals(block);
187}
188
189void DatabaseSettingsWidgetEncryption::loadKdfParameters()
190{
191Q_ASSERT(m_db);
192if (!m_db) {
193return;
194}
195
196auto kdf = m_db->kdf();
197if (!kdf) {
198return;
199}
200
201int kdfIndex = m_ui->kdfComboBox->findData(m_db->kdf()->uuid().toByteArray());
202if (kdfIndex > -1) {
203bool block = m_ui->kdfComboBox->blockSignals(true);
204m_ui->kdfComboBox->setCurrentIndex(kdfIndex);
205m_ui->kdfComboBox->blockSignals(block);
206}
207
208m_ui->transformRoundsSpinBox->setValue(kdf->rounds());
209if (IS_ARGON2(m_db->kdf()->uuid())) {
210auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
211m_ui->memorySpinBox->setValue(static_cast<int>(argon2Kdf->memory()) / (1 << 10));
212m_ui->parallelismSpinBox->setValue(argon2Kdf->parallelism());
213}
214
215updateKdfFields();
216}
217
218void DatabaseSettingsWidgetEncryption::updateKdfFields()
219{
220QUuid id = m_db->kdf()->uuid();
221
222m_ui->memoryUsageLabel->setVisible(IS_ARGON2(id));
223m_ui->memorySpinBox->setVisible(IS_ARGON2(id));
224m_ui->parallelismLabel->setVisible(IS_ARGON2(id));
225m_ui->parallelismSpinBox->setVisible(IS_ARGON2(id));
226}
227
228void DatabaseSettingsWidgetEncryption::markDirty()
229{
230m_isDirty = true;
231}
232
233bool DatabaseSettingsWidgetEncryption::save()
234{
235Q_ASSERT(m_db);
236if (!m_db) {
237return false;
238}
239
240if (m_initWithAdvanced != isAdvancedMode()) {
241// Switched from basic <-> advanced mode, need to recalculate everything
242m_isDirty = true;
243}
244
245if (m_db->key() && !m_db->key()->keys().isEmpty() && !m_isDirty) {
246// nothing has changed, don't re-transform
247return true;
248}
249
250auto kdf = m_db->kdf();
251Q_ASSERT(kdf);
252
253if (!isAdvancedMode()) {
254if (kdf && !m_isDirty && !m_ui->decryptionTimeSettings->isVisible()) {
255return true;
256}
257
258int time = m_ui->decryptionTimeSlider->value() * 100;
259updateFormatCompatibility(m_ui->compatibilitySelection->currentIndex(), false);
260
261QApplication::setOverrideCursor(Qt::BusyCursor);
262
263int rounds = AsyncTask::runAndWaitForFuture([&kdf, time]() { return kdf->benchmark(time); });
264kdf->setRounds(rounds);
265
266// TODO: we should probably use AsyncTask::runAndWaitForFuture() here,
267// but not without making Database thread-safe
268bool ok = m_db->changeKdf(kdf);
269
270QApplication::restoreOverrideCursor();
271
272m_db->metadata()->customData()->set(CD_DECRYPTION_TIME_PREFERENCE_KEY, QString("%1").arg(time));
273
274return 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
279m_db->metadata()->customData()->remove(CD_DECRYPTION_TIME_PREFERENCE_KEY);
280
281// first perform safety check for KDF rounds
282if (IS_ARGON2(kdf->uuid()) && m_ui->transformRoundsSpinBox->value() > 10000) {
283QMessageBox warning;
284warning.setIcon(QMessageBox::Warning);
285warning.setWindowTitle(tr("Number of rounds too high", "Key transformation rounds"));
286warning.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."));
288auto ok = warning.addButton(tr("Understood, keep number"), QMessageBox::ButtonRole::AcceptRole);
289auto cancel = warning.addButton(tr("Cancel"), QMessageBox::ButtonRole::RejectRole);
290warning.setDefaultButton(cancel);
291warning.layout()->setSizeConstraint(QLayout::SetMinimumSize);
292warning.exec();
293if (warning.clickedButton() != ok) {
294return false;
295}
296} else if (IS_AES_KDF(kdf->uuid()) && m_ui->transformRoundsSpinBox->value() < 100000) {
297QMessageBox warning;
298warning.setIcon(QMessageBox::Warning);
299warning.setWindowTitle(tr("Number of rounds too low", "Key transformation rounds"));
300warning.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."));
302auto ok = warning.addButton(tr("Understood, keep number"), QMessageBox::ButtonRole::AcceptRole);
303auto cancel = warning.addButton(tr("Cancel"), QMessageBox::ButtonRole::RejectRole);
304warning.setDefaultButton(cancel);
305warning.layout()->setSizeConstraint(QLayout::SetMinimumSize);
306warning.exec();
307if (warning.clickedButton() != ok) {
308return false;
309}
310}
311
312m_db->setCipher(QUuid(m_ui->algorithmComboBox->currentData().toByteArray()));
313
314// Save kdf parameters
315kdf->setRounds(m_ui->transformRoundsSpinBox->value());
316if (IS_ARGON2(kdf->uuid())) {
317auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
318argon2Kdf->setMemory(static_cast<quint64>(m_ui->memorySpinBox->value()) * (1 << 10));
319argon2Kdf->setParallelism(static_cast<quint32>(m_ui->parallelismSpinBox->value()));
320}
321
322QApplication::setOverrideCursor(Qt::WaitCursor);
323// TODO: we should probably use AsyncTask::runAndWaitForFuture() here,
324// but not without making Database thread-safe
325bool ok = m_db->changeKdf(kdf);
326QApplication::restoreOverrideCursor();
327
328if (!ok) {
329MessageBox::warning(this,
330tr("KDF unchanged"),
331tr("Failed to transform key with new KDF parameters; KDF unchanged."),
332QMessageBox::Ok);
333}
334
335return ok;
336}
337
338void DatabaseSettingsWidgetEncryption::benchmarkTransformRounds(int millisecs)
339{
340QApplication::setOverrideCursor(Qt::BusyCursor);
341m_ui->transformBenchmarkButton->setEnabled(false);
342m_ui->transformRoundsSpinBox->setFocus();
343
344// Create a new kdf with the current parameters
345auto kdf = KeePass2::uuidToKdf(QUuid(m_ui->kdfComboBox->currentData().toByteArray()));
346kdf->setRounds(m_ui->transformRoundsSpinBox->value());
347if (IS_ARGON2(kdf->uuid())) {
348auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
349// Set a small static number of rounds for the benchmark
350argon2Kdf->setRounds(4);
351if (!argon2Kdf->setMemory(static_cast<quint64>(m_ui->memorySpinBox->value()) * (1 << 10))) {
352m_ui->memorySpinBox->setValue(static_cast<int>(argon2Kdf->memory() / (1 << 10)));
353}
354if (!argon2Kdf->setParallelism(static_cast<quint32>(m_ui->parallelismSpinBox->value()))) {
355m_ui->parallelismSpinBox->setValue(argon2Kdf->parallelism());
356}
357}
358
359// Determine the number of rounds required to meet 1 second delay
360int rounds = AsyncTask::runAndWaitForFuture([&kdf, millisecs]() { return kdf->benchmark(millisecs); });
361
362m_ui->transformRoundsSpinBox->setValue(rounds);
363m_ui->transformBenchmarkButton->setEnabled(true);
364m_ui->decryptionTimeSlider->setValue(millisecs / 100);
365QApplication::restoreOverrideCursor();
366}
367
368void DatabaseSettingsWidgetEncryption::changeKdf(int index)
369{
370Q_ASSERT(m_db);
371if (!m_db) {
372return;
373}
374
375QUuid id(m_ui->kdfComboBox->itemData(index).toByteArray());
376m_db->setKdf(KeePass2::uuidToKdf(id));
377updateKdfFields();
378benchmarkTransformRounds();
379}
380
381/**
382* Update memory spin box suffix on value change.
383*/
384void DatabaseSettingsWidgetEncryption::memoryChanged(int value)
385{
386m_ui->memorySpinBox->setSuffix(tr(" MiB", "Abbreviation for Mebibytes (KDF settings)", value));
387}
388
389/**
390* Update parallelism spin box suffix on value change.
391*/
392void DatabaseSettingsWidgetEncryption::parallelismChanged(int value)
393{
394m_ui->parallelismSpinBox->setSuffix(tr(" thread(s)", "Threads for parallel execution (KDF settings)", value));
395}
396
397bool DatabaseSettingsWidgetEncryption::isAdvancedMode()
398{
399return m_ui->encryptionSettingsTabWidget->currentWidget() == m_ui->advancedTab;
400}
401
402void DatabaseSettingsWidgetEncryption::updateDecryptionTime(int value)
403{
404m_ui->decryptionTimeValueLabel->setText(getTextualEncryptionTime(value * 100));
405}
406
407void DatabaseSettingsWidgetEncryption::updateFormatCompatibility(int index, bool retransform)
408{
409Q_ASSERT(m_db);
410if (!m_db) {
411return;
412}
413
414if (m_ui->compatibilitySelection->currentIndex() != index) {
415bool block = m_ui->compatibilitySelection->blockSignals(true);
416m_ui->compatibilitySelection->setCurrentIndex(index);
417m_ui->compatibilitySelection->blockSignals(block);
418}
419
420QUuid kdfUuid(m_ui->compatibilitySelection->itemData(index).toByteArray());
421if (retransform) {
422auto kdf = KeePass2::uuidToKdf(kdfUuid);
423m_db->setKdf(kdf);
424
425if (IS_ARGON2(kdf->uuid())) {
426auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
427// Default to 64 MiB of memory and 2 threads
428// these settings are safe for desktop and mobile devices
429argon2Kdf->setMemory(1 << 16);
430argon2Kdf->setParallelism(2);
431}
432}
433}
434