mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-04-05 13:37:43 +03:00
Add a new database settings wizard
This patch implements a new database wizard to guide users through the process of setting up a new database and choosing sane encryption settings. It also reimplements the master key settings to be more user-friendly. Users can now add, change, or remove individual composite key components instead of having to set all components at once. This avoids confusion about a password being reset if the user only wants to add a key file. With these changes comes a major refactor of how database composite keys and key components are handled. Copying of keys is prohibited and each key exists only once in memory and is referenced via shared pointers. GUI components for changing individual keys are encapsulated into separate classes to be more reusable. The password edit and generator widgets have also been refactored to be more reusable.
This commit is contained in:
parent
e4ded388b4
commit
e443cde452
116 changed files with 5054 additions and 1692 deletions
410
src/gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp
Normal file
410
src/gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp
Normal file
|
@ -0,0 +1,410 @@
|
|||
/*
|
||||
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 2 or (at your option)
|
||||
* version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "DatabaseSettingsWidgetEncryption.h"
|
||||
#include "ui_DatabaseSettingsWidgetEncryption.h"
|
||||
#include "core/Database.h"
|
||||
#include "core/Metadata.h"
|
||||
#include "core/Global.h"
|
||||
#include "core/AsyncTask.h"
|
||||
#include "gui/MessageBox.h"
|
||||
#include "crypto/kdf/Argon2Kdf.h"
|
||||
#include "format/KeePass2.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QPushButton>
|
||||
|
||||
const char* DatabaseSettingsWidgetEncryption::CD_DECRYPTION_TIME_PREFERENCE_KEY = "KPXC_DECRYPTION_TIME_PREFERENCE";
|
||||
|
||||
DatabaseSettingsWidgetEncryption::DatabaseSettingsWidgetEncryption(QWidget* parent)
|
||||
: DatabaseSettingsWidget(parent)
|
||||
, m_ui(new Ui::DatabaseSettingsWidgetEncryption())
|
||||
{
|
||||
m_ui->setupUi(this);
|
||||
|
||||
connect(m_ui->transformBenchmarkButton, SIGNAL(clicked()), SLOT(benchmarkTransformRounds()));
|
||||
connect(m_ui->kdfComboBox, SIGNAL(currentIndexChanged(int)), SLOT(changeKdf(int)));
|
||||
|
||||
connect(m_ui->memorySpinBox, SIGNAL(valueChanged(int)), this, SLOT(memoryChanged(int)));
|
||||
connect(m_ui->parallelismSpinBox, SIGNAL(valueChanged(int)), this, SLOT(parallelismChanged(int)));
|
||||
|
||||
m_ui->compatibilitySelection->addItem(tr("KDBX 4.0 (recommended)"), KeePass2::KDF_ARGON2.toByteArray());
|
||||
m_ui->compatibilitySelection->addItem(tr("KDBX 3.1"), KeePass2::KDF_AES_KDBX3.toByteArray());
|
||||
m_ui->decryptionTimeSlider->setValue(10);
|
||||
updateDecryptionTime(m_ui->decryptionTimeSlider->value());
|
||||
|
||||
connect(m_ui->activateChangeDecryptionTimeButton, SIGNAL(clicked()), SLOT(activateChangeDecryptionTime()));
|
||||
connect(m_ui->decryptionTimeSlider, SIGNAL(valueChanged(int)), SLOT(updateDecryptionTime(int)));
|
||||
connect(m_ui->compatibilitySelection, SIGNAL(currentIndexChanged(int)), SLOT(updateFormatCompatibility(int)));
|
||||
|
||||
// conditions under which a key re-transformation is needed
|
||||
connect(m_ui->decryptionTimeSlider, SIGNAL(valueChanged(int)), SLOT(markDirty()));
|
||||
connect(m_ui->compatibilitySelection, SIGNAL(currentIndexChanged(int)), SLOT(markDirty()));
|
||||
connect(m_ui->activateChangeDecryptionTimeButton, SIGNAL(clicked()), SLOT(markDirty()));
|
||||
connect(m_ui->algorithmComboBox, SIGNAL(currentIndexChanged(int)), SLOT(markDirty()));
|
||||
connect(m_ui->kdfComboBox, SIGNAL(currentIndexChanged(int)), SLOT(markDirty()));
|
||||
connect(m_ui->transformRoundsSpinBox, SIGNAL(valueChanged(int)), SLOT(markDirty()));
|
||||
connect(m_ui->memorySpinBox, SIGNAL(valueChanged(int)), SLOT(markDirty()));
|
||||
connect(m_ui->parallelismSpinBox, SIGNAL(valueChanged(int)), SLOT(markDirty()));
|
||||
}
|
||||
|
||||
DatabaseSettingsWidgetEncryption::~DatabaseSettingsWidgetEncryption()
|
||||
{
|
||||
}
|
||||
|
||||
void DatabaseSettingsWidgetEncryption::initialize()
|
||||
{
|
||||
Q_ASSERT(m_db);
|
||||
if (!m_db) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool isDirty = false;
|
||||
|
||||
if (!m_db->kdf()) {
|
||||
m_db->setKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2));
|
||||
isDirty = true;
|
||||
}
|
||||
if (!m_db->key()) {
|
||||
m_db->setKey(QSharedPointer<CompositeKey>::create());
|
||||
m_db->setCipher(KeePass2::CIPHER_AES);
|
||||
isDirty = true;
|
||||
}
|
||||
|
||||
// check if the DB's custom data has a decryption time setting stored
|
||||
// and set the slider to it, otherwise just state that the time is unchanged
|
||||
// (we cannot infer the time from the raw KDF settings)
|
||||
auto* cd = m_db->metadata()->customData();
|
||||
if (cd->hasKey(CD_DECRYPTION_TIME_PREFERENCE_KEY)) {
|
||||
int decryptionTime = qMax(100, cd->value(CD_DECRYPTION_TIME_PREFERENCE_KEY).toInt());
|
||||
bool block = m_ui->decryptionTimeSlider->blockSignals(true);
|
||||
m_ui->decryptionTimeSlider->setValue(decryptionTime / 100);
|
||||
updateDecryptionTime(decryptionTime / 100);
|
||||
m_ui->decryptionTimeSlider->blockSignals(block);
|
||||
m_ui->activateChangeDecryptionTimeButton->setVisible(false);
|
||||
} else {
|
||||
m_ui->decryptionTimeSettings->setVisible(isDirty);
|
||||
m_ui->activateChangeDecryptionTimeButton->setVisible(!isDirty);
|
||||
if (!isDirty) {
|
||||
m_ui->decryptionTimeValueLabel->setText(tr("unchanged", "Database decryption time is unchanged"));
|
||||
}
|
||||
}
|
||||
|
||||
updateFormatCompatibility(m_db->kdf()->uuid() == KeePass2::KDF_AES_KDBX3 ? KDBX3 : KDBX4, isDirty);
|
||||
setupAlgorithmComboBox();
|
||||
setupKdfComboBox();
|
||||
loadKdfParameters();
|
||||
|
||||
m_isDirty = isDirty;
|
||||
}
|
||||
|
||||
void DatabaseSettingsWidgetEncryption::uninitialize()
|
||||
{
|
||||
}
|
||||
|
||||
void DatabaseSettingsWidgetEncryption::showEvent(QShowEvent* event)
|
||||
{
|
||||
QWidget::showEvent(event);
|
||||
m_ui->decryptionTimeSlider->setFocus();
|
||||
}
|
||||
|
||||
void DatabaseSettingsWidgetEncryption::setupAlgorithmComboBox()
|
||||
{
|
||||
m_ui->algorithmComboBox->clear();
|
||||
for (auto& cipher : asConst(KeePass2::CIPHERS)) {
|
||||
m_ui->algorithmComboBox->addItem(QCoreApplication::translate("KeePass2", cipher.second.toUtf8()),
|
||||
cipher.first.toByteArray());
|
||||
}
|
||||
int cipherIndex = m_ui->algorithmComboBox->findData(m_db->cipher().toByteArray());
|
||||
if (cipherIndex > -1) {
|
||||
m_ui->algorithmComboBox->setCurrentIndex(cipherIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseSettingsWidgetEncryption::setupKdfComboBox()
|
||||
{
|
||||
// Setup kdf combo box
|
||||
bool block = m_ui->kdfComboBox->blockSignals(true);
|
||||
m_ui->kdfComboBox->clear();
|
||||
for (auto& kdf : asConst(KeePass2::KDFS)) {
|
||||
m_ui->kdfComboBox->addItem(QCoreApplication::translate("KeePass2", kdf.second.toUtf8()),
|
||||
kdf.first.toByteArray());
|
||||
}
|
||||
m_ui->kdfComboBox->blockSignals(block);
|
||||
}
|
||||
|
||||
void DatabaseSettingsWidgetEncryption::loadKdfParameters()
|
||||
{
|
||||
Q_ASSERT(m_db);
|
||||
if (!m_db) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto kdf = m_db->kdf();
|
||||
Q_ASSERT(kdf);
|
||||
if (!kdf) {
|
||||
return;
|
||||
}
|
||||
|
||||
int kdfIndex = m_ui->kdfComboBox->findData(m_db->kdf()->uuid().toByteArray());
|
||||
if (kdfIndex > -1) {
|
||||
bool block = m_ui->kdfComboBox->blockSignals(true);
|
||||
m_ui->kdfComboBox->setCurrentIndex(kdfIndex);
|
||||
m_ui->kdfComboBox->blockSignals(block);
|
||||
}
|
||||
|
||||
m_ui->transformRoundsSpinBox->setValue(kdf->rounds());
|
||||
if (m_db->kdf()->uuid() == KeePass2::KDF_ARGON2) {
|
||||
auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
|
||||
m_ui->memorySpinBox->setValue(static_cast<int>(argon2Kdf->memory()) / (1 << 10));
|
||||
m_ui->parallelismSpinBox->setValue(argon2Kdf->parallelism());
|
||||
}
|
||||
|
||||
updateKdfFields();
|
||||
}
|
||||
|
||||
void DatabaseSettingsWidgetEncryption::updateKdfFields()
|
||||
{
|
||||
QUuid id = m_db->kdf()->uuid();
|
||||
|
||||
bool memoryVisible = (id == KeePass2::KDF_ARGON2);
|
||||
m_ui->memoryUsageLabel->setVisible(memoryVisible);
|
||||
m_ui->memorySpinBox->setVisible(memoryVisible);
|
||||
|
||||
bool parallelismVisible = (id == KeePass2::KDF_ARGON2);
|
||||
m_ui->parallelismLabel->setVisible(parallelismVisible);
|
||||
m_ui->parallelismSpinBox->setVisible(parallelismVisible);
|
||||
}
|
||||
|
||||
void DatabaseSettingsWidgetEncryption::activateChangeDecryptionTime()
|
||||
{
|
||||
m_ui->decryptionTimeSettings->setVisible(true);
|
||||
m_ui->activateChangeDecryptionTimeButton->setVisible(false);
|
||||
updateDecryptionTime(m_ui->decryptionTimeSlider->value());
|
||||
}
|
||||
|
||||
void DatabaseSettingsWidgetEncryption::markDirty()
|
||||
{
|
||||
m_isDirty = true;
|
||||
}
|
||||
|
||||
bool DatabaseSettingsWidgetEncryption::save()
|
||||
{
|
||||
Q_ASSERT(m_db);
|
||||
if (!m_db) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_db->key() && !m_db->key()->keys().isEmpty() && !m_isDirty) {
|
||||
// nothing has changed, don't re-transform
|
||||
return true;
|
||||
}
|
||||
|
||||
auto kdf = m_db->kdf();
|
||||
Q_ASSERT(kdf);
|
||||
|
||||
if (!advancedMode()) {
|
||||
if (kdf && !m_isDirty && !m_ui->decryptionTimeSettings->isVisible()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int time = m_ui->decryptionTimeSlider->value() * 100;
|
||||
updateFormatCompatibility(m_ui->compatibilitySelection->currentIndex(), false);
|
||||
|
||||
QApplication::setOverrideCursor(Qt::BusyCursor);
|
||||
|
||||
int rounds = AsyncTask::runAndWaitForFuture([&kdf, time]() { return kdf->benchmark(time); });
|
||||
kdf->setRounds(rounds);
|
||||
|
||||
// TODO: we should probably use AsyncTask::runAndWaitForFuture() here,
|
||||
// but not without making Database thread-safe
|
||||
bool ok = m_db->changeKdf(kdf);
|
||||
|
||||
QApplication::restoreOverrideCursor();
|
||||
|
||||
m_db->metadata()->customData()->set(CD_DECRYPTION_TIME_PREFERENCE_KEY, QString("%1").arg(time));
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
// remove a stored decryption time from custom data when advanced settings are used
|
||||
// we don't know it until we actually run the KDF
|
||||
m_db->metadata()->customData()->remove(CD_DECRYPTION_TIME_PREFERENCE_KEY);
|
||||
|
||||
// first perform safety check for KDF rounds
|
||||
if (kdf->uuid() == KeePass2::KDF_ARGON2 && m_ui->transformRoundsSpinBox->value() > 10000) {
|
||||
QMessageBox warning;
|
||||
warning.setIcon(QMessageBox::Warning);
|
||||
warning.setWindowTitle(tr("Number of rounds too high", "Key transformation rounds"));
|
||||
warning.setText(tr("You are using a very high number of key transform rounds with Argon2.\n\n"
|
||||
"If you keep this number, your database may take hours or days (or even longer) to open!"));
|
||||
auto ok = warning.addButton(tr("Understood, keep number"), QMessageBox::ButtonRole::AcceptRole);
|
||||
auto cancel = warning.addButton(tr("Cancel"), QMessageBox::ButtonRole::RejectRole);
|
||||
warning.setDefaultButton(cancel);
|
||||
warning.exec();
|
||||
if (warning.clickedButton() != ok) {
|
||||
return false;
|
||||
}
|
||||
} else if ((kdf->uuid() == KeePass2::KDF_AES_KDBX3 || kdf->uuid() == KeePass2::KDF_AES_KDBX4)
|
||||
&& m_ui->transformRoundsSpinBox->value() < 100000) {
|
||||
QMessageBox warning;
|
||||
warning.setIcon(QMessageBox::Warning);
|
||||
warning.setWindowTitle(tr("Number of rounds too low", "Key transformation rounds"));
|
||||
warning.setText(tr("You are using a very low number of key transform rounds with AES-KDF.\n\n"
|
||||
"If you keep this number, your database may be too easy to crack!"));
|
||||
auto ok = warning.addButton(tr("Understood, keep number"), QMessageBox::ButtonRole::AcceptRole);
|
||||
auto cancel = warning.addButton(tr("Cancel"), QMessageBox::ButtonRole::RejectRole);
|
||||
warning.setDefaultButton(cancel);
|
||||
warning.exec();
|
||||
if (warning.clickedButton() != ok) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
m_db->setCipher(QUuid(m_ui->algorithmComboBox->currentData().toByteArray()));
|
||||
|
||||
// Save kdf parameters
|
||||
kdf->setRounds(m_ui->transformRoundsSpinBox->value());
|
||||
if (kdf->uuid() == KeePass2::KDF_ARGON2) {
|
||||
auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
|
||||
argon2Kdf->setMemory(static_cast<quint64>(m_ui->memorySpinBox->value()) * (1 << 10));
|
||||
argon2Kdf->setParallelism(static_cast<quint32>(m_ui->parallelismSpinBox->value()));
|
||||
}
|
||||
|
||||
QApplication::setOverrideCursor(Qt::WaitCursor);
|
||||
// TODO: we should probably use AsyncTask::runAndWaitForFuture() here,
|
||||
// but not without making Database thread-safe
|
||||
bool ok = m_db->changeKdf(kdf);
|
||||
QApplication::restoreOverrideCursor();
|
||||
|
||||
if (!ok) {
|
||||
MessageBox::warning(this,
|
||||
tr("KDF unchanged"),
|
||||
tr("Failed to transform key with new KDF parameters; KDF unchanged."),
|
||||
QMessageBox::Ok);
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
void DatabaseSettingsWidgetEncryption::benchmarkTransformRounds(int millisecs)
|
||||
{
|
||||
QApplication::setOverrideCursor(Qt::BusyCursor);
|
||||
m_ui->transformBenchmarkButton->setEnabled(false);
|
||||
m_ui->transformRoundsSpinBox->setFocus();
|
||||
|
||||
// Create a new kdf with the current parameters
|
||||
auto kdf = KeePass2::uuidToKdf(QUuid(m_ui->kdfComboBox->currentData().toByteArray()));
|
||||
kdf->setRounds(m_ui->transformRoundsSpinBox->value());
|
||||
if (kdf->uuid() == KeePass2::KDF_ARGON2) {
|
||||
auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
|
||||
if (!argon2Kdf->setMemory(static_cast<quint64>(m_ui->memorySpinBox->value()) * (1 << 10))) {
|
||||
m_ui->memorySpinBox->setValue(static_cast<int>(argon2Kdf->memory() / (1 << 10)));
|
||||
}
|
||||
if (!argon2Kdf->setParallelism(static_cast<quint32>(m_ui->parallelismSpinBox->value()))) {
|
||||
m_ui->parallelismSpinBox->setValue(argon2Kdf->parallelism());
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the number of rounds required to meet 1 second delay
|
||||
int rounds = AsyncTask::runAndWaitForFuture([&kdf, millisecs]() { return kdf->benchmark(millisecs); });
|
||||
|
||||
m_ui->transformRoundsSpinBox->setValue(rounds);
|
||||
m_ui->transformBenchmarkButton->setEnabled(true);
|
||||
m_ui->decryptionTimeSlider->setValue(millisecs / 100);
|
||||
QApplication::restoreOverrideCursor();
|
||||
}
|
||||
|
||||
void DatabaseSettingsWidgetEncryption::changeKdf(int index)
|
||||
{
|
||||
Q_ASSERT(m_db);
|
||||
if (!m_db) {
|
||||
return;
|
||||
}
|
||||
|
||||
QUuid id(m_ui->kdfComboBox->itemData(index).toByteArray());
|
||||
m_db->setKdf(KeePass2::uuidToKdf(id));
|
||||
updateKdfFields();
|
||||
activateChangeDecryptionTime();
|
||||
benchmarkTransformRounds();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update memory spin box suffix on value change.
|
||||
*/
|
||||
void DatabaseSettingsWidgetEncryption::memoryChanged(int value)
|
||||
{
|
||||
m_ui->memorySpinBox->setSuffix(tr(" MiB", "Abbreviation for Mebibytes (KDF settings)", value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update parallelism spin box suffix on value change.
|
||||
*/
|
||||
void DatabaseSettingsWidgetEncryption::parallelismChanged(int value)
|
||||
{
|
||||
m_ui->parallelismSpinBox->setSuffix(tr(" thread(s)", "Threads for parallel execution (KDF settings)", value));
|
||||
}
|
||||
|
||||
void DatabaseSettingsWidgetEncryption::setAdvancedMode(bool advanced)
|
||||
{
|
||||
DatabaseSettingsWidget::setAdvancedMode(advanced);
|
||||
|
||||
if (advanced) {
|
||||
loadKdfParameters();
|
||||
m_ui->stackedWidget->setCurrentIndex(1);
|
||||
} else {
|
||||
m_ui->compatibilitySelection->setCurrentIndex(m_db->kdf()->uuid() == KeePass2::KDF_AES_KDBX3 ? KDBX3 : KDBX4);
|
||||
m_ui->stackedWidget->setCurrentIndex(0);
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseSettingsWidgetEncryption::updateDecryptionTime(int value)
|
||||
{
|
||||
if (value < 10) {
|
||||
m_ui->decryptionTimeValueLabel->setText(tr("%1 ms", "milliseconds", value * 100).arg(value * 100));
|
||||
} else {
|
||||
m_ui->decryptionTimeValueLabel->setText(tr("%1 s", "seconds", value / 10).arg(value / 10.0, 0, 'f', 1));
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseSettingsWidgetEncryption::updateFormatCompatibility(int index, bool retransform)
|
||||
{
|
||||
Q_ASSERT(m_db);
|
||||
if (!m_db) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_ui->compatibilitySelection->currentIndex() != index) {
|
||||
bool block = m_ui->compatibilitySelection->blockSignals(true);
|
||||
m_ui->compatibilitySelection->setCurrentIndex(index);
|
||||
m_ui->compatibilitySelection->blockSignals(block);
|
||||
}
|
||||
|
||||
if (retransform) {
|
||||
QUuid kdfUuid(m_ui->compatibilitySelection->itemData(index).toByteArray());
|
||||
auto kdf = KeePass2::uuidToKdf(kdfUuid);
|
||||
m_db->setKdf(kdf);
|
||||
|
||||
if (kdf->uuid() == KeePass2::KDF_ARGON2) {
|
||||
auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
|
||||
argon2Kdf->setMemory(128 * 1024);
|
||||
argon2Kdf->setParallelism(static_cast<quint32>(QThread::idealThreadCount()));
|
||||
}
|
||||
|
||||
activateChangeDecryptionTime();
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue