Skip to content

Commit

Permalink
Perform password strength calculation asynchronously. Debounce passwo…
Browse files Browse the repository at this point in the history
…rd strength recalculation.
  • Loading branch information
libklein committed May 13, 2022
1 parent 93ac899 commit 46e8e7c
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 44 deletions.
57 changes: 47 additions & 10 deletions src/gui/PasswordGeneratorWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
#include <QDir>
#include <QShortcut>
#include <QTimer>
#include <optional>

#include "core/AsyncTask.h"
#include "core/Config.h"
#include "core/PasswordHealth.h"
#include "core/Resources.h"
Expand All @@ -49,14 +51,19 @@ PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent)
m_ui->buttonAddWordList->setIcon(icons()->icon("document-new"));
m_ui->buttonClose->setShortcut(Qt::Key_Escape);

// Configure password health timer
m_passwordHealthTimer.setSingleShot(true);
connect(&m_passwordHealthTimer, &QTimer::timeout, this, [this] { queuePasswordStrengthUpdate(); });

// Add two shortcuts to save the form CTRL+Enter and CTRL+S
auto shortcut = new QShortcut(Qt::CTRL + Qt::Key_Return, this);
connect(shortcut, &QShortcut::activated, this, [this] { applyPassword(); });
shortcut = new QShortcut(Qt::CTRL + Qt::Key_S, this);
connect(shortcut, &QShortcut::activated, this, [this] { applyPassword(); });

connect(m_ui->editNewPassword, SIGNAL(textChanged(QString)), SLOT(updateButtonsEnabled(QString)));
connect(m_ui->editNewPassword, SIGNAL(textChanged(QString)), SLOT(updatePasswordStrength(QString)));
connect(
m_ui->editNewPassword, &PasswordEdit::textChanged, [this](const QString&) { m_passwordHealthTimer.start(50); });
connect(m_ui->buttonAdvancedMode, SIGNAL(toggled(bool)), SLOT(setAdvancedMode(bool)));
connect(m_ui->buttonAddHex, SIGNAL(clicked()), SLOT(excludeHexChars()));
connect(m_ui->editAdditionalChars, SIGNAL(textChanged(QString)), SLOT(updateGenerator()));
Expand Down Expand Up @@ -253,15 +260,14 @@ void PasswordGeneratorWidget::regeneratePassword()
if (m_passwordGenerator->isValid()) {
QString password = m_passwordGenerator->generatePassword();
m_ui->editNewPassword->setText(password);
updatePasswordStrength(password);
}
} else {
if (m_dicewareGenerator->isValid()) {
QString password = m_dicewareGenerator->generatePassphrase();
m_ui->editNewPassword->setText(password);
updatePasswordStrength(password);
}
}
m_passwordHealthTimer.start(50);
}

void PasswordGeneratorWidget::updateButtonsEnabled(const QString& password)
Expand All @@ -272,28 +278,59 @@ void PasswordGeneratorWidget::updateButtonsEnabled(const QString& password)
m_ui->buttonCopy->setEnabled(!password.isEmpty());
}

PasswordHealth PasswordGeneratorWidget::calculatePasswordHealth(const QString& password) const {
void PasswordGeneratorWidget::queuePasswordStrengthUpdate()
{
const static int MAX_SYNC_PASSWORD_LENGTH = 100;
const static int MAX_WORD_LENGTH = 16;

const auto& password = getGeneratedPassword();

if (m_ui->tabWidget->currentIndex() == Diceware || password.size() < MAX_SYNC_PASSWORD_LENGTH) {
updatePasswordStrength(password, calculatePasswordHealth(password));
return;
}

// TODO Get wordcount size
auto truncatedPassword = password.left(MAX_SYNC_PASSWORD_LENGTH - MAX_WORD_LENGTH);

// TODO Display "Entropy > xxx"?
updatePasswordStrength(truncatedPassword, calculatePasswordHealth(truncatedPassword));

AsyncTask::runThenCallback(
[this, password]() {
// QFuture requires default constructible result type, which PasswordHealth is not. Wrap it in an optional.
return QPair<QString, std::optional<PasswordHealth>>{password, calculatePasswordHealth(password)};
},
this,
[this](const QPair<QString, std::optional<PasswordHealth>>& result) {
if (result.first == getGeneratedPassword()) {
updatePasswordStrength(result.first, *result.second);
}
});
}

PasswordHealth PasswordGeneratorWidget::calculatePasswordHealth(const QString& password) const
{
if (m_ui->tabWidget->currentIndex() == Diceware) {
// Diceware estimates entropy differently
return PasswordHealth(m_dicewareGenerator->estimateEntropy());
}
return PasswordHealth(password);
}

void PasswordGeneratorWidget::updatePasswordStrength(const QString& password)
// TODO Maybe make password a member of PasswordHealth?
void PasswordGeneratorWidget::updatePasswordStrength(const QString& password, const PasswordHealth& passwordHealth)
{
auto health = calculatePasswordHealth(password);

if (m_ui->tabWidget->currentIndex() == Diceware) {
// Additionally show password length
m_ui->charactersInPassphraseLabel->setText(QString::number(password.length()));
}

m_ui->entropyLabel->setText(tr("Entropy: %1 bit").arg(QString::number(health.entropy(), 'f', 2)));
m_ui->entropyLabel->setText(tr("Entropy: %1 bit").arg(QString::number(passwordHealth.entropy(), 'f', 2)));

m_ui->entropyProgressBar->setValue(std::min(int(health.entropy()), m_ui->entropyProgressBar->maximum()));
m_ui->entropyProgressBar->setValue(std::min(int(passwordHealth.entropy()), m_ui->entropyProgressBar->maximum()));

colorStrengthIndicator(health);
colorStrengthIndicator(passwordHealth);
}

void PasswordGeneratorWidget::applyPassword()
Expand Down
6 changes: 5 additions & 1 deletion src/gui/PasswordGeneratorWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#define KEEPASSX_PASSWORDGENERATORWIDGET_H

#include <QComboBox>
#include <QTimer>

#include "core/PassphraseGenerator.h"
#include "core/PasswordGenerator.h"
Expand Down Expand Up @@ -71,7 +72,7 @@ public slots:

private slots:
void updateButtonsEnabled(const QString& password);
void updatePasswordStrength(const QString& password);
void queuePasswordStrengthUpdate();
void setAdvancedMode(bool advanced);
void excludeHexChars();

Expand All @@ -89,11 +90,14 @@ private slots:
void closeEvent(QCloseEvent* event) override;
PasswordGenerator::CharClasses charClasses();
PasswordGenerator::GeneratorFlags generatorFlags();
void updatePasswordStrength(const QString& password, const PasswordHealth& passwordHealth);
PasswordHealth calculatePasswordHealth(const QString& password) const;

const QScopedPointer<PasswordGenerator> m_passwordGenerator;
const QScopedPointer<PassphraseGenerator> m_dicewareGenerator;
const QScopedPointer<Ui::PasswordGeneratorWidget> m_ui;

QTimer m_passwordHealthTimer;
};

#endif // KEEPASSX_PASSWORDGENERATORWIDGET_H
103 changes: 70 additions & 33 deletions tests/gui/TestGui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -648,44 +648,80 @@ void TestGui::testAddEntry()
QTRY_COMPARE(entryView->model()->rowCount(), 3);
}

void TestGui::testPasswordEntryEntropy_data() {
void TestGui::testPasswordEntryEntropy_data()
{
QTest::addColumn<QString>("password");
QTest::addColumn<QString>("expectedEntropyLabel");
QTest::addColumn<QString>("expectedStrengthLabel");

QTest::newRow("Well-known password") << "hello" << "Entropy: 6.38 bit" << "Password Quality: Poor";

QTest::newRow("Password composed of well-known words.") << "helloworld" << "Entropy: 13.10 bit" << "Password Quality: Poor";

QTest::newRow("Password composed of well-known words with number.") << "password1" << "Entropy: 4.00 bit" << "Password Quality: Poor";

QTest::newRow("Password out of small character space.") << "D0g.................." << "Entropy: 19.02 bit" << "Password Quality: Poor";

QTest::newRow("XKCD, easy substitutions.") << "Tr0ub4dour&3" << "Entropy: 30.87 bit" << "Password Quality: Poor";

QTest::newRow("XKCD, word generator.") << "correcthorsebatterystaple" << "Entropy: 47.98 bit" << "Password Quality: Weak";

QTest::newRow("Random characters, medium length.") << "YQC3kbXbjC652dTDH" << "Entropy: 95.83 bit" << "Password Quality: Good";

QTest::newRow("Random characters, long.") << "Bs5ZFfthWzR8DGFEjaCM6bGqhmCT4km" << "Entropy: 174.59 bit" << "Password Quality: Excellent";

QTest::newRow("Long random password.") <<
"y32Lv4yAb4enNfwt6SiU/GkO2jg/+IPv43fjVzrK8zVaotppGgNP/susKKQO9Tq48uZW/5blJoc6"
"h0343cW1rmkwoGBJUVS/gx8z3pikcozD7f/yBIBptxBNQCUE9uwwuZAjGQdsoftRpfHpcD60MU8k"
"ftUUmZ5QTin585yiVwDw5KKX+XnbpqK5n5gLMkqhIXxx/i8HTqjdXQeQ2mIorDYMLHMLPT3uax4u"
"A0w8fotOTqrGFi62LpGe3IFkYtBO6Ds0yot/cdgrssYGrLWU2c5HaxaIvPJBDkpM485QzMUP1OiY"
"ECuJ6SUWycGaE7d4vRwNASDZFBHuiziZ89SlS9FmoKvACHBgfzr0y4sypTZmC8mt7IcaJAl8FBb/"
"i6St8fRImNhkyA5eShBk31TaqtGkuGqfwiQN4/RYotpGRK0bLc3o1P6AONB8B/m6EsA4LvUfQW9i"
"0Muy/OHRgYeDM2NigkFXOueedISbN5MtummJsdCsHcSkjB+/h6XvvX7R7IS9iz2OkyAggqlMtQ5P"
"AHCKGrwEv5SHBbEMf0+5WfrSB7OmCpy8N9YZ1ZMmZJEeZ/TxbPr4rt+FWB1cfYBxT5o/B1WzwfJ5"
"/hdDe023uxotp6D3Yt9uMomC05n8OqwzFvGDOwKRqz/svUQ="
<< "Entropy: 3657.46 bit" << "Password Quality: Excellent";
QTest::newRow("Well-known password") << "hello"
<< "Entropy: 6.38 bit"
<< "Password Quality: Poor";

QTest::newRow("Password composed of well-known words.") << "helloworld"
<< "Entropy: 13.10 bit"
<< "Password Quality: Poor";

QTest::newRow("Password composed of well-known words with number.") << "password1"
<< "Entropy: 4.00 bit"
<< "Password Quality: Poor";

QTest::newRow("Password out of small character space.") << "D0g.................."
<< "Entropy: 19.02 bit"
<< "Password Quality: Poor";

QTest::newRow("XKCD, easy substitutions.") << "Tr0ub4dour&3"
<< "Entropy: 30.87 bit"
<< "Password Quality: Poor";

QTest::newRow("XKCD, word generator.") << "correcthorsebatterystaple"
<< "Entropy: 47.98 bit"
<< "Password Quality: Weak";

QTest::newRow("Random characters, medium length.") << "YQC3kbXbjC652dTDH"
<< "Entropy: 95.83 bit"
<< "Password Quality: Good";

QTest::newRow("Random characters, long.") << "Bs5ZFfthWzR8DGFEjaCM6bGqhmCT4km"
<< "Entropy: 174.59 bit"
<< "Password Quality: Excellent";

QTest::newRow("Very long random password, triggers async request")
<< "quintet-tamper-kinswoman-humility-vengeful-haven-tastiness-aspire-widget-ipad-cussed-reaffirm-ladylike-"
"ashamed-anatomy-daybed-jam-swear-strudel-neatness-stalemate-unbundle-flavored-relation-emergency-underrate-"
"registry-getting-award-unveiled-unshaken-stagnate-cartridge-magnitude-ointment-hardener-enforced-scrubbed-"
"radial-fiddling-envelope-unpaved-moisture-unused-crawlers-quartered-crushed-kangaroo-tiptop-doily"
<< "Entropy: 1155.96 bit"
<< "Password Quality: Excellent";

QTest::newRow("Even longer random password, triggers async request")
<< "modulator &712&7123x/thinly &712&7123x/straggler &712&7123x/overripe &712&7123x/steering &712&7123x/from "
"&712&7123x/grit &712&7123x/jovial &712&7123x/saddled &712&7123x/employee &712&7123x/mothproof "
"&712&7123x/modular &712&7123x/stretch &712&7123x/marital &712&7123x/capitol &712&7123x/smock "
"&712&7123x/trace &712&7123x/stucco &712&7123x/bruising &712&7123x/emission &712&7123x/submarine "
"&712&7123x/skyward &712&7123x/pending &712&7123x/imitate &712&7123x/vessel &712&7123x/sufferer "
"&712&7123x/overfed &712&7123x/sixfold &712&7123x/corset &712&7123x/ditto &712&7123x/awry "
"&712&7123x/stiffen &712&7123x/unicorn &712&7123x/payday &712&7123x/headscarf &712&7123x/certainty "
"&712&7123x/headstand &712&7123x/monitor &712&7123x/jazz &712&7123x/shelf &712&7123x/payphone "
"&712&7123x/sheep &712&7123x/tanning &712&7123x/exfoliate &712&7123x/porcupine &712&7123x/snowbound "
"&712&7123x/daytime &712&7123x/skater &712&7123x/manager &712&7123x/poplar &712&7123x/earflap "
"&712&7123x/snorkel &712&7123x/headcount &712&7123x/crook &712&7123x/boney &712&7123x/deniable "
"&712&7123x/glacial &712&7123x/subdivide &712&7123x/flop &712&7123x/buffed &712&7123x/praising "
"&712&7123x/scope &712&7123x/recant &712&7123x/giant &712&7123x/resemble &712&7123x/ruined "
"&712&7123x/prelude &712&7123x/refinance &712&7123x/iodine &712&7123x/pulse &712&7123x/jalapeno "
"&712&7123x/regulate &712&7123x/crawlers &712&7123x/knickers &712&7123x/recharger &712&7123x/boneless "
"&712&7123x/divinity &712&7123x/freewill &712&7123x/flyer &712&7123x/freckled &712&7123x/refuse "
"&712&7123x/thong &712&7123x/cryptic &712&7123x/sharpness &712&7123x/roamer &712&7123x/decaf "
"&712&7123x/drum &712&7123x/heavily &712&7123x/antiviral &712&7123x/depict &712&7123x/walmart "
"&712&7123x/epilepsy &712&7123x/botany &712&7123x/wince &712&7123x/mating &712&7123x/starlet "
"&712&7123x/revise &712&7123x/helper &712&7123x/cycling &712&7123x/operable"
<< "Entropy: 6714.15 bit"
<< "Password Quality: Excellent";
}

void TestGui::testPasswordEntryEntropy()
{
// 16 ms of irresponsiveness (60 fps)
const qint64 MAX_TIME_TO_FIRST_RESPONSE = 16;
const qint64 MAX_TIME_TO_FIRST_RESPONSE = 75;

auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");

Expand Down Expand Up @@ -732,10 +768,11 @@ void TestGui::testPasswordEntryEntropy()

generatedPassword->setText("");

QDeadlineTimer timer(MAX_TIME_TO_FIRST_RESPONSE);
QDeadlineTimer timer(MAX_TIME_TO_FIRST_RESPONSE + password.size() / 2);
QTest::keyClicks(generatedPassword, password);
QVERIFY(!timer.hasExpired());
QCOMPARE(entropyLabel->text(), expectedEntropyLabel);
// Wait for change, then check validity
QTRY_COMPARE_WITH_TIMEOUT(entropyLabel->text(), expectedEntropyLabel, 4000);
QCOMPARE(strengthLabel->text(), expectedStrengthLabel);

QTest::mouseClick(generatedPassword, Qt::LeftButton);
Expand Down Expand Up @@ -797,7 +834,7 @@ void TestGui::testDicewareEntryEntropy()
auto* strengthLabel = pwGeneratorWidget->findChild<QLabel*>("strengthLabel");
auto* wordLengthLabel = pwGeneratorWidget->findChild<QLabel*>("charactersInPassphraseLabel");

QCOMPARE(entropyLabel->text(), QString("Entropy: 77.55 bit"));
QTRY_COMPARE_WITH_TIMEOUT(entropyLabel->text(), QString("Entropy: 77.55 bit"), 200);
QCOMPARE(strengthLabel->text(), QString("Password Quality: Good"));
QCOMPARE(wordLengthLabel->text().toInt(), pwGeneratorWidget->getGeneratedPassword().size());

Expand Down

0 comments on commit 46e8e7c

Please sign in to comment.