Improve overall database settings behavior

* Fixes #10723 - only display password strength warning when actively editing the password
* Also improve behavior of minimum quality warning
* Improve behavior and handling of password changes with the database settings dialog
* Prevents loss of newly entered password when toggling between elements in the settings page
* On error, switch to tab that prevents saving database settings for easier correction
This commit is contained in:
Jonathan White 2024-05-27 16:03:29 -04:00
parent 2b08af712f
commit 2c0844807e
9 changed files with 94 additions and 86 deletions

View File

@ -1856,11 +1856,11 @@ Are you sure you want to continue without a password?</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<source>You must enter a stronger password to protect your database.</source> <source>This is a weak password! For better protection of your secrets, you should choose a stronger password.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<source>This is a weak password! For better protection of your secrets, you should choose a stronger password.</source> <source>The provided password does not meet the minimum quality requirement.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>

View File

@ -73,6 +73,10 @@ KeyComponentWidget::Page KeyComponentWidget::visiblePage() const
void KeyComponentWidget::updateAddStatus(bool added) void KeyComponentWidget::updateAddStatus(bool added)
{ {
if (m_ui->stackedWidget->currentIndex() == Page::Edit) {
emit editCanceled();
}
if (added) { if (added) {
m_ui->stackedWidget->setCurrentIndex(Page::LeaveOrRemove); m_ui->stackedWidget->setCurrentIndex(Page::LeaveOrRemove);
} else { } else {
@ -101,12 +105,6 @@ void KeyComponentWidget::cancelEdit()
emit editCanceled(); emit editCanceled();
} }
void KeyComponentWidget::showEvent(QShowEvent* event)
{
resetComponentEditWidget();
QWidget::showEvent(event);
}
void KeyComponentWidget::resetComponentEditWidget() void KeyComponentWidget::resetComponentEditWidget()
{ {
if (!m_componentWidget || static_cast<Page>(m_ui->stackedWidget->currentIndex()) == Page::Edit) { if (!m_componentWidget || static_cast<Page>(m_ui->stackedWidget->currentIndex()) == Page::Edit) {

View File

@ -104,9 +104,6 @@ signals:
void editCanceled(); void editCanceled();
void componentRemovalRequested(); void componentRemovalRequested();
protected:
void showEvent(QShowEvent* event) override;
private slots: private slots:
void updateAddStatus(bool added); void updateAddStatus(bool added);
void doAdd(); void doAdd();

View File

@ -27,6 +27,9 @@ PasswordEditWidget::PasswordEditWidget(QWidget* parent)
, m_compUi(new Ui::PasswordEditWidget()) , m_compUi(new Ui::PasswordEditWidget())
{ {
initComponent(); initComponent();
// Explicitly clear password on cancel
connect(this, &PasswordEditWidget::editCanceled, this, [this] { setPassword({}); });
} }
PasswordEditWidget::~PasswordEditWidget() = default; PasswordEditWidget::~PasswordEditWidget() = default;
@ -59,7 +62,7 @@ bool PasswordEditWidget::isPasswordVisible() const
bool PasswordEditWidget::isEmpty() const bool PasswordEditWidget::isEmpty() const
{ {
return (visiblePage() == Page::Edit) && m_compUi->enterPasswordEdit->text().isEmpty(); return visiblePage() != Page::Edit || m_compUi->enterPasswordEdit->text().isEmpty();
} }
PasswordHealth::Quality PasswordEditWidget::getPasswordQuality() const PasswordHealth::Quality PasswordEditWidget::getPasswordQuality() const
@ -83,8 +86,7 @@ void PasswordEditWidget::initComponentEditWidget(QWidget* widget)
{ {
Q_UNUSED(widget); Q_UNUSED(widget);
Q_ASSERT(m_compEditWidget); Q_ASSERT(m_compEditWidget);
m_compUi->enterPasswordEdit->setFocus(); setFocusProxy(m_compUi->enterPasswordEdit);
m_compUi->enterPasswordEdit->setQualityVisible(true); m_compUi->enterPasswordEdit->setQualityVisible(true);
m_compUi->repeatPasswordEdit->setQualityVisible(false); m_compUi->repeatPasswordEdit->setQualityVisible(false);
} }
@ -103,16 +105,6 @@ void PasswordEditWidget::initComponent()
"<p>Good passwords are long and unique. KeePassXC can generate one for you.</p>")); "<p>Good passwords are long and unique. KeePassXC can generate one for you.</p>"));
} }
void PasswordEditWidget::hideEvent(QHideEvent* event)
{
if (!isVisible() && m_compUi->enterPasswordEdit) {
m_compUi->enterPasswordEdit->setText("");
m_compUi->repeatPasswordEdit->setText("");
}
QWidget::hideEvent(event);
}
bool PasswordEditWidget::validate(QString& errorMessage) const bool PasswordEditWidget::validate(QString& errorMessage) const
{ {
if (m_compUi->enterPasswordEdit->text() != m_compUi->repeatPasswordEdit->text()) { if (m_compUi->enterPasswordEdit->text() != m_compUi->repeatPasswordEdit->text()) {

View File

@ -47,7 +47,6 @@ protected:
QWidget* componentEditWidget() override; QWidget* componentEditWidget() override;
void initComponentEditWidget(QWidget* widget) override; void initComponentEditWidget(QWidget* widget) override;
void initComponent() override; void initComponent() override;
void hideEvent(QHideEvent* event) override;
private slots: private slots:
void setPassword(const QString& password); void setPassword(const QString& password);

View File

@ -92,36 +92,32 @@ DatabaseSettingsDialog::DatabaseSettingsDialog(QWidget* parent)
scrollArea->setSizeAdjustPolicy(QScrollArea::AdjustToContents); scrollArea->setSizeAdjustPolicy(QScrollArea::AdjustToContents);
scrollArea->setWidgetResizable(true); scrollArea->setWidgetResizable(true);
scrollArea->setWidget(m_databaseKeyWidget); scrollArea->setWidget(m_databaseKeyWidget);
m_securityTabWidget->addTab(scrollArea, tr("Database Credentials"));
m_securityTabWidget->addTab(scrollArea, tr("Database Credentials"));
m_securityTabWidget->addTab(m_encryptionWidget, tr("Encryption Settings")); m_securityTabWidget->addTab(m_encryptionWidget, tr("Encryption Settings"));
m_securityTabWidget->setCurrentIndex(0);
m_ui->categoryList->addCategory(tr("Remote Sync"), icons()->icon("remote-sync")); m_ui->categoryList->addCategory(tr("Remote Sync"), icons()->icon("remote-sync"));
m_ui->stackedWidget->addWidget(m_remoteWidget); m_ui->stackedWidget->addWidget(m_remoteWidget);
#if defined(WITH_XC_KEESHARE)
addSettingsPage(new DatabaseSettingsPageKeeShare());
#endif
#if defined(WITH_XC_FDOSECRETS)
addSettingsPage(new DatabaseSettingsPageFdoSecrets());
#endif
m_ui->stackedWidget->setCurrentIndex(0);
m_securityTabWidget->setCurrentIndex(0);
connect(m_securityTabWidget, SIGNAL(currentChanged(int)), SLOT(pageChanged()));
connect(m_ui->categoryList, SIGNAL(categoryChanged(int)), m_ui->stackedWidget, SLOT(setCurrentIndex(int)));
#ifdef WITH_XC_BROWSER #ifdef WITH_XC_BROWSER
m_ui->categoryList->addCategory(tr("Browser Integration"), icons()->icon("internet-web-browser")); m_ui->categoryList->addCategory(tr("Browser Integration"), icons()->icon("internet-web-browser"));
m_ui->stackedWidget->addWidget(m_browserWidget); m_ui->stackedWidget->addWidget(m_browserWidget);
#endif #endif
#ifdef WITH_XC_KEESHARE
addSettingsPage(new DatabaseSettingsPageKeeShare());
#endif
#ifdef WITH_XC_FDOSECRETS
addSettingsPage(new DatabaseSettingsPageFdoSecrets());
#endif
m_ui->categoryList->addCategory(tr("Maintenance"), icons()->icon("hammer-wrench")); m_ui->categoryList->addCategory(tr("Maintenance"), icons()->icon("hammer-wrench"));
m_ui->stackedWidget->addWidget(m_maintenanceWidget); m_ui->stackedWidget->addWidget(m_maintenanceWidget);
pageChanged(); m_ui->stackedWidget->setCurrentIndex(0);
connect(m_ui->categoryList, SIGNAL(categoryChanged(int)), m_ui->stackedWidget, SLOT(setCurrentIndex(int)));
} }
DatabaseSettingsDialog::~DatabaseSettingsDialog() = default; DatabaseSettingsDialog::~DatabaseSettingsDialog() = default;
@ -171,21 +167,29 @@ void DatabaseSettingsDialog::showRemoteSettings()
void DatabaseSettingsDialog::save() void DatabaseSettingsDialog::save()
{ {
if (!m_generalWidget->save()) { if (!m_generalWidget->save()) {
m_ui->categoryList->setCurrentCategory(0);
return; return;
} }
if (!m_databaseKeyWidget->save()) { if (!m_databaseKeyWidget->save()) {
m_ui->categoryList->setCurrentCategory(1);
m_securityTabWidget->setCurrentIndex(0);
return; return;
} }
if (!m_encryptionWidget->save()) { if (!m_encryptionWidget->save()) {
m_ui->categoryList->setCurrentCategory(1);
m_securityTabWidget->setCurrentIndex(1);
return; return;
} }
if (!m_remoteWidget->save()) { if (!m_remoteWidget->save()) {
m_ui->categoryList->setCurrentCategory(2);
return; return;
} }
// Browser settings don't have anything to save
for (const ExtraPage& extraPage : asConst(m_extraPages)) { for (const ExtraPage& extraPage : asConst(m_extraPages)) {
extraPage.saveSettings(); extraPage.saveSettings();
} }
@ -195,10 +199,13 @@ void DatabaseSettingsDialog::save()
void DatabaseSettingsDialog::reject() void DatabaseSettingsDialog::reject()
{ {
m_generalWidget->discard();
m_databaseKeyWidget->discard();
m_encryptionWidget->discard();
m_remoteWidget->discard();
#ifdef WITH_XC_BROWSER
m_browserWidget->discard();
#endif
emit editFinished(false); emit editFinished(false);
} }
void DatabaseSettingsDialog::pageChanged()
{
m_ui->stackedWidget->currentIndex();
}

View File

@ -70,7 +70,6 @@ signals:
private slots: private slots:
void save(); void save();
void reject(); void reject();
void pageChanged();
private: private:
enum Page enum Page

View File

@ -79,28 +79,30 @@ void DatabaseSettingsWidgetDatabaseKey::load(QSharedPointer<Database> db)
// database has no key, we are about to add a new one // database has no key, we are about to add a new one
m_passwordEditWidget->changeVisiblePage(KeyComponentWidget::Page::Edit); m_passwordEditWidget->changeVisiblePage(KeyComponentWidget::Page::Edit);
m_passwordEditWidget->setPasswordVisible(true); m_passwordEditWidget->setPasswordVisible(true);
} // Focus won't work until the UI settles
QTimer::singleShot(0, m_passwordEditWidget, SLOT(setFocus()));
bool hasAdditionalKeys = false; } else {
for (const auto& key : m_db->key()->keys()) { bool hasAdditionalKeys = false;
if (key->uuid() == PasswordKey::UUID) { for (const auto& key : m_db->key()->keys()) {
m_passwordEditWidget->setComponentAdded(true); if (key->uuid() == PasswordKey::UUID) {
} else if (key->uuid() == FileKey::UUID) { m_passwordEditWidget->setComponentAdded(true);
m_keyFileEditWidget->setComponentAdded(true); } else if (key->uuid() == FileKey::UUID) {
hasAdditionalKeys = true; m_keyFileEditWidget->setComponentAdded(true);
hasAdditionalKeys = true;
}
} }
}
#ifdef WITH_XC_YUBIKEY #ifdef WITH_XC_YUBIKEY
for (const auto& key : m_db->key()->challengeResponseKeys()) { for (const auto& key : m_db->key()->challengeResponseKeys()) {
if (key->uuid() == ChallengeResponseKey::UUID) { if (key->uuid() == ChallengeResponseKey::UUID) {
m_yubiKeyEditWidget->setComponentAdded(true); m_yubiKeyEditWidget->setComponentAdded(true);
hasAdditionalKeys = true; hasAdditionalKeys = true;
}
} }
}
#endif #endif
setAdditionalKeyOptionsVisible(hasAdditionalKeys); setAdditionalKeyOptionsVisible(hasAdditionalKeys);
}
connect(m_passwordEditWidget->findChild<QPushButton*>("removeButton"), SIGNAL(clicked()), SLOT(markDirty())); connect(m_passwordEditWidget->findChild<QPushButton*>("removeButton"), SIGNAL(clicked()), SLOT(markDirty()));
connect(m_keyFileEditWidget->findChild<QPushButton*>("removeButton"), SIGNAL(clicked()), SLOT(markDirty())); connect(m_keyFileEditWidget->findChild<QPushButton*>("removeButton"), SIGNAL(clicked()), SLOT(markDirty()));
@ -177,31 +179,32 @@ bool DatabaseSettingsWidgetDatabaseKey::save()
return false; return false;
} }
// Show warning if database password is weak if (!m_passwordEditWidget->isEmpty()) {
if (!m_passwordEditWidget->isEmpty() // Prevent setting password with a quality less than the minimum required
&& m_passwordEditWidget->getPasswordQuality() < PasswordHealth::Quality::Good) { auto minQuality = qBound(0, config()->get(Config::Security_DatabasePasswordMinimumQuality).toInt(), 4);
auto dialogResult = MessageBox::warning(this, if (m_passwordEditWidget->getPasswordQuality() < static_cast<PasswordHealth::Quality>(minQuality)) {
tr("Weak password"), MessageBox::critical(this,
tr("This is a weak password! For better protection of your secrets, " tr("Weak password"),
"you should choose a stronger password."), tr("The provided password does not meet the minimum quality requirement."),
MessageBox::ContinueWithWeakPass | MessageBox::Cancel, MessageBox::Ok,
MessageBox::Cancel); MessageBox::Ok);
if (dialogResult == MessageBox::Cancel) {
return false; return false;
} }
}
// If enforced in the config file, deny users from continuing with a weak password // Show warning if database password is weak or poor
auto minQuality = if (m_passwordEditWidget->getPasswordQuality() < PasswordHealth::Quality::Good) {
static_cast<PasswordHealth::Quality>(config()->get(Config::Security_DatabasePasswordMinimumQuality).toInt()); auto dialogResult =
if (!m_passwordEditWidget->isEmpty() && m_passwordEditWidget->getPasswordQuality() < minQuality) { MessageBox::warning(this,
MessageBox::critical(this, tr("Weak password"),
tr("Weak password"), tr("This is a weak password! For better protection of your secrets, "
tr("You must enter a stronger password to protect your database."), "you should choose a stronger password."),
MessageBox::Ok, MessageBox::ContinueWithWeakPass | MessageBox::Cancel,
MessageBox::Ok); MessageBox::Cancel);
return false;
if (dialogResult == MessageBox::Cancel) {
return false;
}
}
} }
if (!addToCompositeKey(m_keyFileEditWidget, newKey, oldFileKey)) { if (!addToCompositeKey(m_keyFileEditWidget, newKey, oldFileKey)) {
@ -232,11 +235,16 @@ bool DatabaseSettingsWidgetDatabaseKey::save()
m_db->markAsModified(); m_db->markAsModified();
} }
// Reset fields
initialize();
return true; return true;
} }
void DatabaseSettingsWidgetDatabaseKey::discard() void DatabaseSettingsWidgetDatabaseKey::discard()
{ {
// Reset fields
initialize();
emit editFinished(false); emit editFinished(false);
} }

View File

@ -1607,12 +1607,20 @@ void TestGui::testDatabaseSettings()
passwordWidgets[0]->setText("b"); passwordWidgets[0]->setText("b");
passwordWidgets[1]->setText("b"); passwordWidgets[1]->setText("b");
// Cancel password change // Toggle between tabs to ensure the password remains
securityTabWidget->setCurrentIndex(1);
QApplication::processEvents();
securityTabWidget->setCurrentIndex(0);
QApplication::processEvents();
QCOMPARE(passwordWidgets[0]->text(), QString("b"));
// Cancel password change and confirm password is cleared
auto cancelPasswordButton = passwordEditWidget->findChild<QPushButton*>("cancelButton"); auto cancelPasswordButton = passwordEditWidget->findChild<QPushButton*>("cancelButton");
QVERIFY(cancelPasswordButton); QVERIFY(cancelPasswordButton);
QTest::mouseClick(cancelPasswordButton, Qt::LeftButton); QTest::mouseClick(cancelPasswordButton, Qt::LeftButton);
QApplication::processEvents(); QApplication::processEvents();
QVERIFY(!passwordWidgets[0]->isVisible()); QVERIFY(!passwordWidgets[0]->isVisible());
QCOMPARE(passwordWidgets[0]->text(), QString(""));
QVERIFY(editPasswordButton->isVisible()); QVERIFY(editPasswordButton->isVisible());
// Switch to encryption tab and interact with various settings // Switch to encryption tab and interact with various settings