diff --git a/electrum/gui/qml/components/Constants.qml b/electrum/gui/qml/components/Constants.qml index b9bdac1ff..ff88c6009 100644 --- a/electrum/gui/qml/components/Constants.qml +++ b/electrum/gui/qml/components/Constants.qml @@ -43,6 +43,7 @@ Item { property color colorDone: '#ff80ff80' property color colorValidBackground: '#ff008000' property color colorInvalidBackground: '#ff800000' + property color colorAcceptable: '#ff8080ff' property color colorLightningLocal: "#6060ff" property color colorLightningLocalReserve: "#0000a0" diff --git a/electrum/gui/qml/components/PasswordDialog.qml b/electrum/gui/qml/components/PasswordDialog.qml index 07a788830..442f44a19 100644 --- a/electrum/gui/qml/components/PasswordDialog.qml +++ b/electrum/gui/qml/components/PasswordDialog.qml @@ -62,7 +62,26 @@ ElDialog { visible: confirmPassword showReveal: false echoMode: pw_1.echoMode - enabled: pw_1.text.length >= 6 + } + + RowLayout { + Layout.fillWidth: true + Layout.rightMargin: constants.paddingXLarge + Layout.topMargin: constants.paddingLarge + Layout.bottomMargin: constants.paddingLarge + + visible: confirmPassword + + Label { + text: qsTr('Strength') + color: Material.accentColor + font.pixelSize: constants.fontSizeSmall + } + + PasswordStrengthIndicator { + Layout.fillWidth: true + password: pw_1.text + } } } @@ -77,4 +96,5 @@ ElDialog { } } } + } diff --git a/electrum/gui/qml/components/controls/PasswordStrengthIndicator.qml b/electrum/gui/qml/components/controls/PasswordStrengthIndicator.qml new file mode 100644 index 000000000..0a0080b03 --- /dev/null +++ b/electrum/gui/qml/components/controls/PasswordStrengthIndicator.qml @@ -0,0 +1,44 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.0 + +Rectangle { + property string password + property int strength: 0 + property color strengthColor + property string strengthText + + onPasswordChanged: checkPasswordStrength(password) + + function checkPasswordStrength() { + var _strength = Daemon.passwordStrength(password) + var map = { + 0: [constants.colorError, qsTr('Weak')], + 1: [constants.colorAcceptable, qsTr('Medium')], + 2: [constants.colorDone, qsTr('Strong')], + 3: [constants.colorDone, qsTr('Very Strong')] + } + strength = password.length ? _strength + 1 : 0 + strengthText = password.length ? map[_strength][1] : '' + strengthColor = map[_strength][0] + } + + height: strengthLabel.height + color: 'transparent' + border.color: Material.foreground + + Rectangle { + id: strengthBar + x: 1 + y: 1 + width: (parent.width - 2) * strength / 4 + height: parent.height - 2 + color: strengthColor + Label { + id: strengthLabel + anchors.centerIn: parent + text: strengthText + color: strength <= 2 ? Material.foreground : '#004000' + } + } +} diff --git a/electrum/gui/qml/components/wizard/WCWalletPassword.qml b/electrum/gui/qml/components/wizard/WCWalletPassword.qml index af9241684..d2629ae75 100644 --- a/electrum/gui/qml/components/wizard/WCWalletPassword.qml +++ b/electrum/gui/qml/components/wizard/WCWalletPassword.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls +import QtQuick.Controls.Material import "../controls" @@ -13,7 +14,7 @@ WizardComponent { } ColumnLayout { - width: parent.width + anchors.fill: parent Label { Layout.fillWidth: true @@ -22,17 +23,50 @@ WizardComponent { : qsTr('Enter password for %1').arg(wizard_data['wallet_name']) wrapMode: Text.Wrap } + PasswordField { id: password1 } + Label { text: qsTr('Enter password (again)') } + PasswordField { id: password2 showReveal: false echoMode: password1.echoMode - enabled: password1.text.length >= 6 + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: constants.paddingXLarge + Layout.rightMargin: constants.paddingXLarge + Layout.topMargin: constants.paddingXLarge + + visible: password1.text != '' + + Label { + Layout.rightMargin: constants.paddingLarge + text: qsTr('Strength') + } + + PasswordStrengthIndicator { + Layout.fillWidth: true + password: password1.text + } + } + + Item { + Layout.preferredWidth: 1 + Layout.fillHeight: true + } + + InfoTextArea { + Layout.alignment: Qt.AlignCenter + text: qsTr('Passwords don\'t match') + visible: password1.text != password2.text + iconStyle: InfoTextArea.IconStyle.Warn } } } diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index 6b44933cc..6f5a1bbb1 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -27,6 +27,9 @@ if TYPE_CHECKING: # wallet list model. supports both wallet basenames (wallet file basenames) # and whole Wallet instances (loaded wallets) +from .util import check_password_strength + + class QEWalletListModel(QAbstractListModel): _logger = get_logger(__name__) @@ -366,3 +369,9 @@ class QEDaemon(AuthMixin, QObject): except Exception as e: verified = False return verified + + @pyqtSlot(str, result=int) + def passwordStrength(self, password): + if len(password) == 0: + return 0 + return check_password_strength(password)[0] diff --git a/electrum/gui/qml/util.py b/electrum/gui/qml/util.py index bebc62320..9a2825e01 100644 --- a/electrum/gui/qml/util.py +++ b/electrum/gui/qml/util.py @@ -1,18 +1,20 @@ +import math +import re import sys import queue from functools import wraps from time import time -from typing import Callable, Optional, NamedTuple +from typing import Callable, Optional, NamedTuple, Tuple from PyQt6.QtCore import pyqtSignal, QThread +from electrum.i18n import _ from electrum.logging import Logger from electrum.util import EventListener, event_listener class QtEventListener(EventListener): - qt_callback_signal = pyqtSignal(tuple) def register_callbacks(self): @@ -58,6 +60,21 @@ def status_update_timer_interval(exp): return interval +# TODO: copied from qt password_dialog.py, move to common code +def check_password_strength(password: str) -> Tuple[int, str]: + """Check the strength of the password entered by the user and return back the same + :param password: password entered by user in New Password + :return: password strength Weak or Medium or Strong""" + password = password + n = math.log(len(set(password))) + num = re.search("[0-9]", password) is not None and re.match("^[0-9]*$", password) is None + caps = password != password.upper() and password != password.lower() + extra = re.match("^[a-zA-Z0-9]*$", password) is None + score = len(password)*(n + caps + num + extra)/20 + password_strength = {0: _('Weak'), 1: _('Medium'), 2: _('Strong'), 3: _('Very Strong')} + return min(3, int(score)), password_strength[min(3, int(score))] + + # TODO: copied from desktop client, this could be moved to a set of common code. class TaskThread(QThread, Logger): """Thread that runs background tasks. Callbacks are guaranteed