Add password strength indicator to PasswordEditWidget

Fixes #7437 (entry edit view only)
Fixes #5220
This commit is contained in:
J.M. Dana 2022-04-13 11:46:47 +02:00 committed by Jonathan White
parent ba8f787d0d
commit a740fe128c
16 changed files with 402 additions and 156 deletions

View file

@ -121,7 +121,7 @@ set(keepassx_SOURCES
gui/MessageBox.cpp
gui/MessageWidget.cpp
gui/OpVaultOpenWidget.cpp
gui/PasswordEdit.cpp
gui/PasswordWidget.cpp
gui/PasswordGeneratorWidget.cpp
gui/ApplicationSettingsWidget.cpp
gui/Icons.cpp

View file

@ -34,7 +34,7 @@
<layout class="QGridLayout" name="charsGrid"/>
</item>
<item>
<widget class="PasswordEdit" name="selectedChars">
<widget class="PasswordWidget" name="selectedChars">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
@ -74,9 +74,10 @@
</widget>
<customwidgets>
<customwidget>
<class>PasswordEdit</class>
<class>PasswordWidget</class>
<extends>QLineEdit</extends>
<header>gui/PasswordEdit.h</header>
<header>gui/PasswordWidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>

View file

@ -145,7 +145,7 @@
</widget>
</item>
<item>
<widget class="PasswordEdit" name="editPassword">
<widget class="PasswordWidget" name="editPassword">
<property name="accessibleName">
<string>Password field</string>
</property>
@ -380,7 +380,7 @@
<number>0</number>
</property>
<item row="0" column="1">
<widget class="PasswordEdit" name="keyFileLineEdit">
<widget class="PasswordWidget" name="keyFileLineEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
@ -617,9 +617,9 @@
</widget>
<customwidgets>
<customwidget>
<class>PasswordEdit</class>
<class>PasswordWidget</class>
<extends>QLineEdit</extends>
<header>gui/PasswordEdit.h</header>
<header>gui/PasswordWidget.h</header>
<container>1</container>
</customwidget>
<customwidget>

View file

@ -87,7 +87,7 @@
</layout>
</item>
<item row="0" column="0">
<widget class="PasswordEdit" name="editNewPassword">
<widget class="PasswordWidget" name="editNewPassword">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Minimum">
<horstretch>0</horstretch>
@ -990,9 +990,9 @@ QProgressBar::chunk {
</widget>
<customwidgets>
<customwidget>
<class>PasswordEdit</class>
<class>PasswordWidget</class>
<extends>QLineEdit</extends>
<header>gui/PasswordEdit.h</header>
<header>gui/PasswordWidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>

View file

@ -16,9 +16,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "PasswordEdit.h"
#include "PasswordWidget.h"
#include "ui_PasswordWidget.h"
#include "core/Config.h"
#include "core/PasswordHealth.h"
#include "gui/Font.h"
#include "gui/Icons.h"
#include "gui/PasswordGeneratorWidget.h"
@ -26,19 +28,24 @@
#include "gui/styles/StateColorPalette.h"
#include <QEvent>
#include <QLineEdit>
#include <QTimer>
#include <QToolTip>
PasswordEdit::PasswordEdit(QWidget* parent)
: QLineEdit(parent)
PasswordWidget::PasswordWidget(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::PasswordWidget())
{
m_ui->setupUi(this);
setFocusProxy(m_ui->passwordEdit);
const QIcon errorIcon = icons()->icon("dialog-error");
m_errorAction = addAction(errorIcon, QLineEdit::TrailingPosition);
m_errorAction = m_ui->passwordEdit->addAction(errorIcon, QLineEdit::TrailingPosition);
m_errorAction->setVisible(false);
m_errorAction->setToolTip(tr("Passwords do not match"));
const QIcon correctIcon = icons()->icon("dialog-ok");
m_correctAction = addAction(correctIcon, QLineEdit::TrailingPosition);
m_correctAction = m_ui->passwordEdit->addAction(correctIcon, QLineEdit::TrailingPosition);
m_correctAction->setVisible(false);
m_correctAction->setToolTip(tr("Passwords match so far"));
@ -63,8 +70,8 @@ PasswordEdit::PasswordEdit(QWidget* parent)
m_toggleVisibleAction->setCheckable(true);
m_toggleVisibleAction->setShortcut(modifier + Qt::Key_H);
m_toggleVisibleAction->setShortcutContext(Qt::WidgetShortcut);
addAction(m_toggleVisibleAction, QLineEdit::TrailingPosition);
connect(m_toggleVisibleAction, &QAction::triggered, this, &PasswordEdit::setShowPassword);
m_ui->passwordEdit->addAction(m_toggleVisibleAction, QLineEdit::TrailingPosition);
connect(m_toggleVisibleAction, &QAction::triggered, this, &PasswordWidget::setShowPassword);
m_passwordGeneratorAction = new QAction(
icons()->icon("password-generator"),
@ -72,44 +79,98 @@ PasswordEdit::PasswordEdit(QWidget* parent)
this);
m_passwordGeneratorAction->setShortcut(modifier + Qt::Key_G);
m_passwordGeneratorAction->setShortcutContext(Qt::WidgetShortcut);
addAction(m_passwordGeneratorAction, QLineEdit::TrailingPosition);
m_ui->passwordEdit->addAction(m_passwordGeneratorAction, QLineEdit::TrailingPosition);
m_passwordGeneratorAction->setVisible(false);
m_capslockAction =
new QAction(icons()->icon("dialog-warning", true, StateColorPalette().color(StateColorPalette::Error)),
tr("Warning: Caps Lock enabled!"),
this);
addAction(m_capslockAction, QLineEdit::LeadingPosition);
m_ui->passwordEdit->addAction(m_capslockAction, QLineEdit::LeadingPosition);
m_capslockAction->setVisible(false);
// Reset the password strength bar, hidden by default
updatePasswordStrength("");
m_ui->qualityProgressBar->setVisible(false);
connect(m_ui->passwordEdit, &QLineEdit::textChanged, this, [this](const QString& pwd) {
updatePasswordStrength(pwd);
emit textChanged(pwd);
});
}
void PasswordEdit::setRepeatPartner(PasswordEdit* repeatEdit)
PasswordWidget::~PasswordWidget()
{
}
void PasswordWidget::setQualityVisible(bool state)
{
m_ui->qualityProgressBar->setVisible(state);
}
QString PasswordWidget::text()
{
return m_ui->passwordEdit->text();
}
void PasswordWidget::setText(const QString& text)
{
m_ui->passwordEdit->setText(text);
}
void PasswordWidget::setEchoMode(QLineEdit::EchoMode mode)
{
m_ui->passwordEdit->setEchoMode(mode);
}
void PasswordWidget::clear()
{
m_ui->passwordEdit->clear();
}
void PasswordWidget::setClearButtonEnabled(bool enabled)
{
m_ui->passwordEdit->setClearButtonEnabled(enabled);
}
void PasswordWidget::selectAll()
{
m_ui->passwordEdit->selectAll();
}
void PasswordWidget::setReadOnly(bool state)
{
m_ui->passwordEdit->setReadOnly(state);
}
void PasswordWidget::setRepeatPartner(PasswordWidget* repeatEdit)
{
m_repeatPasswordEdit = repeatEdit;
m_repeatPasswordEdit->setParentPasswordEdit(this);
connect(this, SIGNAL(textChanged(QString)), m_repeatPasswordEdit, SLOT(autocompletePassword(QString)));
connect(this, SIGNAL(textChanged(QString)), m_repeatPasswordEdit, SLOT(updateRepeatStatus()));
connect(m_repeatPasswordEdit, SIGNAL(textChanged(QString)), m_repeatPasswordEdit, SLOT(updateRepeatStatus()));
connect(
m_ui->passwordEdit, SIGNAL(textChanged(QString)), m_repeatPasswordEdit, SLOT(autocompletePassword(QString)));
connect(m_ui->passwordEdit, SIGNAL(textChanged(QString)), m_repeatPasswordEdit, SLOT(updateRepeatStatus()));
}
void PasswordEdit::setParentPasswordEdit(PasswordEdit* parent)
void PasswordWidget::setParentPasswordEdit(PasswordWidget* parent)
{
m_parentPasswordEdit = parent;
// Hide actions
m_toggleVisibleAction->setVisible(false);
m_passwordGeneratorAction->setVisible(false);
connect(m_ui->passwordEdit, SIGNAL(textChanged(QString)), this, SLOT(updateRepeatStatus()));
}
void PasswordEdit::enablePasswordGenerator()
void PasswordWidget::enablePasswordGenerator()
{
if (!m_passwordGeneratorAction->isVisible()) {
m_passwordGeneratorAction->setVisible(true);
connect(m_passwordGeneratorAction, &QAction::triggered, this, &PasswordEdit::popupPasswordGenerator);
connect(m_passwordGeneratorAction, &QAction::triggered, this, &PasswordWidget::popupPasswordGenerator);
}
}
void PasswordEdit::setShowPassword(bool show)
void PasswordWidget::setShowPassword(bool show)
{
setEchoMode(show ? QLineEdit::Normal : QLineEdit::Password);
m_toggleVisibleAction->setIcon(icons()->onOffIcon("password-show", show));
@ -126,12 +187,12 @@ void PasswordEdit::setShowPassword(bool show)
}
}
bool PasswordEdit::isPasswordVisible() const
bool PasswordWidget::isPasswordVisible() const
{
return echoMode() == QLineEdit::Normal;
return m_ui->passwordEdit->echoMode() == QLineEdit::Normal;
}
void PasswordEdit::popupPasswordGenerator()
void PasswordWidget::popupPasswordGenerator()
{
auto generator = PasswordGeneratorWidget::popupGenerator(this);
generator->setPasswordVisible(isPasswordVisible());
@ -143,7 +204,7 @@ void PasswordEdit::popupPasswordGenerator()
}
}
void PasswordEdit::updateRepeatStatus()
void PasswordWidget::updateRepeatStatus()
{
static const auto stylesheetTemplate = QStringLiteral("QLineEdit { background: %1; }");
if (!m_parentPasswordEdit) {
@ -170,24 +231,25 @@ void PasswordEdit::updateRepeatStatus()
}
}
void PasswordEdit::autocompletePassword(const QString& password)
void PasswordWidget::autocompletePassword(const QString& password)
{
if (!config()->get(Config::Security_PasswordsRepeatVisible).toBool() && echoMode() == QLineEdit::Normal) {
if (!config()->get(Config::Security_PasswordsRepeatVisible).toBool()
&& m_ui->passwordEdit->echoMode() == QLineEdit::Normal) {
setText(password);
}
}
bool PasswordEdit::event(QEvent* event)
bool PasswordWidget::event(QEvent* event)
{
if (isVisible()
&& (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease
|| event->type() == QEvent::FocusIn)) {
checkCapslockState();
}
return QLineEdit::event(event);
return QWidget::event(event);
}
void PasswordEdit::checkCapslockState()
void PasswordWidget::checkCapslockState()
{
if (m_parentPasswordEdit) {
return;
@ -201,8 +263,6 @@ void PasswordEdit::checkCapslockState()
// Force repaint to avoid rendering glitches of QLineEdit contents
repaint();
emit capslockToggled(m_capslockState);
if (newCapslockState) {
QTimer::singleShot(
150, [this] { QToolTip::showText(mapToGlobal(rect().bottomLeft()), m_capslockAction->text()); });
@ -211,3 +271,55 @@ void PasswordEdit::checkCapslockState()
}
}
}
void PasswordWidget::updatePasswordStrength(const QString& password)
{
if (password.isEmpty()) {
m_ui->qualityProgressBar->setValue(0);
m_ui->qualityProgressBar->setToolTip((tr("")));
return;
}
PasswordHealth health(password);
m_ui->qualityProgressBar->setValue(std::min(int(health.entropy()), m_ui->qualityProgressBar->maximum()));
QString style = m_ui->qualityProgressBar->styleSheet();
QRegularExpression re("(QProgressBar::chunk\\s*\\{.*?background-color:)[^;]+;",
QRegularExpression::CaseInsensitiveOption | QRegularExpression::DotMatchesEverythingOption);
style.replace(re, "\\1 %1;");
StateColorPalette qualityPalette;
switch (health.quality()) {
case PasswordHealth::Quality::Bad:
case PasswordHealth::Quality::Poor:
m_ui->qualityProgressBar->setStyleSheet(
style.arg(qualityPalette.color(StateColorPalette::HealthCritical).name()));
m_ui->qualityProgressBar->setToolTip(tr("Quality: %1").arg(tr("Poor", "Password quality")));
break;
case PasswordHealth::Quality::Weak:
m_ui->qualityProgressBar->setStyleSheet(style.arg(qualityPalette.color(StateColorPalette::HealthBad).name()));
m_ui->qualityProgressBar->setToolTip(tr("Quality: %1").arg(tr("Weak", "Password quality")));
break;
case PasswordHealth::Quality::Good:
m_ui->qualityProgressBar->setStyleSheet(style.arg(qualityPalette.color(StateColorPalette::HealthOk).name()));
m_ui->qualityProgressBar->setToolTip(tr("Quality: %1").arg(tr("Good", "Password quality")));
break;
case PasswordHealth::Quality::Excellent:
m_ui->qualityProgressBar->setStyleSheet(
style.arg(qualityPalette.color(StateColorPalette::HealthExcellent).name()));
m_ui->qualityProgressBar->setToolTip(tr("Quality: %1").arg(tr("Excellent", "Password quality")));
break;
}
}

View file

@ -16,50 +16,70 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSX_PASSWORDEDIT_H
#define KEEPASSX_PASSWORDEDIT_H
#ifndef KEEPASSX_PASSWORDWIDGET_H
#define KEEPASSX_PASSWORDWIDGET_H
#include <QAction>
#include <QLineEdit>
#include <QPointer>
#include <QWidget>
class QDialog;
namespace Ui
{
class PasswordWidget;
}
class PasswordEdit : public QLineEdit
class PasswordWidget : public QWidget
{
Q_OBJECT
public:
explicit PasswordEdit(QWidget* parent = nullptr);
explicit PasswordWidget(QWidget* parent = nullptr);
~PasswordWidget() override;
void enablePasswordGenerator();
void setRepeatPartner(PasswordEdit* repeatEdit);
void setRepeatPartner(PasswordWidget* repeatEdit);
void setQualityVisible(bool state);
bool isPasswordVisible() const;
QString text();
signals:
void textChanged(QString text);
public slots:
void setText(const QString& text);
void setShowPassword(bool show);
void updateRepeatStatus();
void clear();
void selectAll();
void setReadOnly(bool state);
void setEchoMode(QLineEdit::EchoMode mode);
void setClearButtonEnabled(bool enabled);
protected:
bool event(QEvent* event) override;
signals:
void capslockToggled(bool capslockOn);
private slots:
void autocompletePassword(const QString& password);
void popupPasswordGenerator();
void setParentPasswordEdit(PasswordEdit* parent);
void checkCapslockState();
void updateRepeatStatus();
void updatePasswordStrength(const QString& password);
private:
void checkCapslockState();
void setParentPasswordEdit(PasswordWidget* parent);
const QScopedPointer<Ui::PasswordWidget> m_ui;
QPointer<QAction> m_errorAction;
QPointer<QAction> m_correctAction;
QPointer<QAction> m_toggleVisibleAction;
QPointer<QAction> m_passwordGeneratorAction;
QPointer<QAction> m_capslockAction;
QPointer<PasswordEdit> m_repeatPasswordEdit;
QPointer<PasswordEdit> m_parentPasswordEdit;
QPointer<PasswordWidget> m_repeatPasswordEdit;
QPointer<PasswordWidget> m_parentPasswordEdit;
bool m_capslockState = false;
};
#endif // KEEPASSX_PASSWORDEDIT_H
#endif // KEEPASSX_PASSWORDWIDGET_H

66
src/gui/PasswordWidget.ui Normal file
View file

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PasswordWidget</class>
<widget class="QWidget" name="PasswordWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>471</width>
<height>25</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="passwordEdit"/>
</item>
<item>
<widget class="QProgressBar" name="qualityProgressBar">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>4</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QProgressBar {
border: none;
background-color: transparent;
}
QProgressBar::chunk {
background-color: #c0392b;
border-radius: 1px;
}
</string>
</property>
<property name="value">
<number>24</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>passwordEdit</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View file

@ -78,6 +78,9 @@ void PasswordEditWidget::initComponentEditWidget(QWidget* widget)
Q_UNUSED(widget);
Q_ASSERT(m_compEditWidget);
m_compUi->enterPasswordEdit->setFocus();
m_compUi->enterPasswordEdit->setQualityVisible(true);
m_compUi->repeatPasswordEdit->setQualityVisible(false);
}
void PasswordEditWidget::initComponent()

View file

@ -31,7 +31,7 @@
</widget>
</item>
<item row="0" column="1">
<widget class="PasswordEdit" name="enterPasswordEdit">
<widget class="PasswordWidget" name="enterPasswordEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
@ -60,7 +60,7 @@
</widget>
</item>
<item row="1" column="1">
<widget class="PasswordEdit" name="repeatPasswordEdit">
<widget class="PasswordWidget" name="repeatPasswordEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
@ -85,9 +85,9 @@
</widget>
<customwidgets>
<customwidget>
<class>PasswordEdit</class>
<class>PasswordWidget</class>
<extends>QLineEdit</extends>
<header>gui/PasswordEdit.h</header>
<header>gui/PasswordWidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>

View file

@ -131,6 +131,8 @@ EditEntryWidget::EditEntryWidget(QWidget* parent)
connect(m_iconsWidget, SIGNAL(messageEditEntryDismiss()), SLOT(hideMessage()));
m_editWidgetProperties->setCustomData(m_customData.data());
m_mainUi->passwordEdit->setQualityVisible(true);
}
EditEntryWidget::~EditEntryWidget()

View file

@ -243,7 +243,7 @@
</widget>
</item>
<item row="2" column="1">
<widget class="PasswordEdit" name="passwordEdit">
<widget class="PasswordWidget" name="passwordEdit">
<property name="accessibleName">
<string>Password field</string>
</property>
@ -297,15 +297,15 @@
</widget>
<customwidgets>
<customwidget>
<class>TagsEdit</class>
<extends>QWidget</extends>
<header>gui/tag/TagsEdit.h</header>
<class>PasswordWidget</class>
<extends>QLineEdit</extends>
<header>gui/PasswordWidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>PasswordEdit</class>
<extends>QLineEdit</extends>
<header>gui/PasswordEdit.h</header>
<class>TagsEdit</class>
<extends>QWidget</extends>
<header>gui/tag/TagsEdit.h</header>
<container>1</container>
</customwidget>
<customwidget>

View file

@ -51,7 +51,7 @@
</widget>
</item>
<item row="2" column="1">
<widget class="PasswordEdit" name="passwordEdit">
<widget class="PasswordWidget" name="passwordEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
@ -190,9 +190,9 @@
</widget>
<customwidgets>
<customwidget>
<class>PasswordEdit</class>
<class>PasswordWidget</class>
<extends>QLineEdit</extends>
<header>gui/PasswordEdit.h</header>
<header>gui/PasswordWidget.h</header>
<container>1</container>
</customwidget>
<customwidget>