qml: OpenWalletDialog: load any wallet if password matches
What changed, and why it matters
This commit changes the mobile/QML version of Electrum so that, when the app starts and the user's entered password does not unlock the most recently used wallet, the app will automatically try to open any other wallet on the device that can be unlocked with that same password. The goal is to help users who have multiple wallets with different passwords. However, it means a password intended for one wallet may silently open a different wallet, potentially showing balances, transaction history, and addresses the user did not expect. It also adds a stored config flag that remembers whether all wallets previously used the same password.
Treat this as a UX/security trade-off worth reviewing. If adopted, add an explicit prompt such as 'Password does not unlock <wallet>. Open <other-wallet> instead?' before loading a different wallet. Ensure getWalletsUnlockableWithPassword does not leak timing or file-existence information, and consider whether the stored did_use_single_password flag creates a fingerprinting or privacy concern. Review whether the fallback should be disabled when wallets have explicit distinct names or when biometric/keychain unlocking is in use.
Security signals we found
Cross-wallet password reuse fallback can open an unintended wallet
Silent automatic wallet selection bypasses explicit user choice
New persistent config flag records past single-password state
Behavior is gated only by startup context and a heuristic config flag
No explicit user confirmation before loading a different wallet
Evidence from the diff
The patch adds a fallback path in OpenWalletDialog.qml. When the daemon emits walletRequiresPassword, the dialog first calls maybeUnlockAnyOtherWallet(). If the dialog was opened at startup (isStartup), more than one wallet exists, a password was entered, and the config flag walletDidUseSinglePassword is false, the code calls Daemon.getWalletsUnlockableWithPassword(password.text) and loads the first matching wallet. The commit also introduces WALLET_DID_USE_SINGLE_PASSWORD in simple_config.py and exposes it to QML via QEConfig, and sets it in QEDaemon after a wallet is loaded. The fallback is skipped if the user manually selected a wallet after startup.
Changed components
electrum/gui/qml/components/OpenWalletDialog.qmlelectrum/gui/qml/components/main.qmlelectrum/gui/qml/qeconfig.pyelectrum/gui/qml/qedaemon.pyelectrum/simple_config.pyInspect captured patch +52 / −2
diff --git a/electrum/gui/qml/components/OpenWalletDialog.qml b/electrum/gui/qml/components/OpenWalletDialog.qml
index b6209de..51ec37e 100644
--- a/electrum/gui/qml/components/OpenWalletDialog.qml
+++ b/electrum/gui/qml/components/OpenWalletDialog.qml
@@ -12,6 +12,7 @@ ElDialog {
property string name
property string path
+ property bool isStartup
property bool _invalidPassword: false
property bool _unlockClicked: false
@@ -40,7 +41,7 @@ ElDialog {
InfoTextArea {
id: notice
- text: Daemon.singlePasswordEnabled || !Daemon.currentWallet
+ text: Daemon.singlePasswordEnabled || isStartup
? qsTr('Please enter password')
: qsTr('Wallet <b>%1</b> requires password to unlock').arg(name)
iconStyle: InfoTextArea.IconStyle.Warn
@@ -94,9 +95,39 @@ ElDialog {
Daemon.loadWallet(openwalletdialog.path, password.text)
}
+ function maybeUnlockAnyOtherWallet() {
+ // try to open any other wallet with the password the user entered, hack to improve ux for
+ // users with non-unified wallet password.
+ // we should only fall back to opening a random wallet if:
+ // - the user did not select a specific wallet, otherwise this is confusing
+ // - there can be more than one password, otherwise this scan would be pointless
+ if (Daemon.availableWallets.rowCount() <= 1 || password.text === '') {
+ return false
+ }
+ if (Config.walletDidUseSinglePassword) {
+ // the last time the wallet was unlocked all wallets used the same password.
+ // trying to decrypt all of them now is most probably useless.
+ return false
+ }
+ if (!openwalletdialog.isStartup) {
+ return false // this dialog got opened because the user clicked on a specific wallet
+ }
+ let wallet_paths = Daemon.getWalletsUnlockableWithPassword(password.text)
+ if (wallet_paths && wallet_paths.length > 0) {
+ console.log('could not unlock recent wallet, falling back to: ' + wallet_paths[0])
+ Daemon.loadWallet(wallet_paths[0], password.text)
+ return true
+ }
+ return false
+ }
+
Connections {
target: Daemon
function onWalletRequiresPassword() {
+ if (maybeUnlockAnyOtherWallet()) {
+ password.text = '' // reset pw so we cannot end up in a loop
+ return
+ }
console.log('invalid password')
_invalidPassword = true
password.tf.forceActiveFocus()
diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml
index bf0b0c9..c8d3645 100644
--- a/electrum/gui/qml/components/main.qml
+++ b/electrum/gui/qml/components/main.qml
@@ -634,12 +634,18 @@ ApplicationWindow
}
property var _opendialog: undefined
+ property var _opendialog_startup: true
function showOpenWalletDialog(name, path) {
if (_opendialog == undefined) {
- _opendialog = openWalletDialog.createObject(app, { name: name, path: path })
+ _opendialog = openWalletDialog.createObject(app, {
+ name: name,
+ path: path,
+ isStartup: _opendialog_startup,
+ })
_opendialog.closed.connect(function() {
_opendialog = undefined
+ _opendialog_startup = false
})
_opendialog.open()
}
diff --git a/electrum/gui/qml/qeconfig.py b/electrum/gui/qml/qeconfig.py
index 9bf2a29..d0d4d79 100644
--- a/electrum/gui/qml/qeconfig.py
+++ b/electrum/gui/qml/qeconfig.py
@@ -340,6 +340,16 @@ class QEConfig(AuthMixin, QObject):
"""
return self.config.WALLET_SHOULD_USE_SINGLE_PASSWORD
+ walletDidUseSinglePasswordChanged = pyqtSignal()
+ @pyqtProperty(bool, notify=walletDidUseSinglePasswordChanged)
+ def walletDidUseSinglePassword(self):
+ """
+ Allows to guess if this is a unified password instance without having
+ unlocked any wallet yet. Might be out of sync e.g. if wallet files get copied manually.
+ """
+ # TODO: consider removing once encrypted wallet file headers are available
+ return self.config.WALLET_DID_USE_SINGLE_PASSWORD
+
@pyqtSlot('qint64', result=str)
@pyqtSlot(QEAmount, result=str)
def formatSatsForEditing(self, satoshis):
diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py
index acbe1a7..2b1bfa3 100644
--- a/electrum/gui/qml/qedaemon.py
+++ b/electrum/gui/qml/qedaemon.py
@@ -237,6 +237,7 @@ class QEDaemon(AuthMixin, QObject):
self._logger.info(f'use single password: {self._use_single_password}')
else:
self._logger.info('use single password disabled by config')
+ self.daemon.config.WALLET_DID_USE_SINGLE_PASSWORD = self._use_single_password
run_hook('load_wallet', wallet)
diff --git a/electrum/simple_config.py b/electrum/simple_config.py
index 92b11de..a748186 100644
--- a/electrum/simple_config.py
+++ b/electrum/simple_config.py
@@ -676,6 +676,8 @@ class SimpleConfig(Logger):
WALLET_UNCONF_UTXO_FREEZE_THRESHOLD_SAT = ConfigVar('unconf_utxo_freeze_threshold', default=5_000, type_=int)
WALLET_PAYREQ_EXPIRY_SECONDS = ConfigVar('request_expiry', default=invoices.PR_DEFAULT_EXPIRATION_WHEN_CREATING, type_=int)
WALLET_SHOULD_USE_SINGLE_PASSWORD = ConfigVar('should_use_single_password', default=False, type_=bool)
+ # TODO: consider removing WALLET_DID_USE_SINGLE_PASSWORD once encrypted wallet file headers are available
+ WALLET_DID_USE_SINGLE_PASSWORD = ConfigVar('did_use_single_password', default=False, type_=bool)
# note: 'use_change' and 'multiple_change' are per-wallet settings
WALLET_SEND_CHANGE_TO_LIGHTNING = ConfigVar(
'send_change_to_lightning', default=False, type_=bool,
Why this scored 38/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.