Skip to content

Commit

Permalink
qml: rework auth
Browse files Browse the repository at this point in the history
now handles keystore-only password and defines a new set of auth methods
  • Loading branch information
accumulator committed May 3, 2023
1 parent 56fa832 commit 6abce44
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 61 deletions.
8 changes: 6 additions & 2 deletions electrum/gui/qml/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,16 @@ class AuthMixin:
authRequired = pyqtSignal([str, str], arguments=['method', 'authMessage'])

@pyqtSlot()
def authProceed(self):
@pyqtSlot(str)
def authProceed(self, password=None):
self._auth_logger.debug('Proceeding with authed fn()')
try:
self._auth_logger.debug(str(getattr(self, '__auth_fcall')))
(func,args,kwargs,reject) = getattr(self, '__auth_fcall')
r = func(self, *args, **kwargs)
if password:
r = func(self, *args, **dict(kwargs, password=password))
else:
r = func(self, *args, **kwargs)
return r
except Exception as e:
self._auth_logger.error(f'Error executing wrapped fn(): {repr(e)}')
Expand Down
119 changes: 94 additions & 25 deletions electrum/gui/qml/components/main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -550,51 +550,104 @@ ApplicationWindow
}
}

// handle auth_protect decorator events
//
// 'wallet_password': User must supply a password
// that matches the storage password (if set)
// or the keystore password. This forces password
// verification in all cases, even for wallets using
// keystore-only passwords (unless the storage and
// keystore are both unencrypted).
// It's primary use is password knowledge verification
// before presenting a secret (e.g. seed) or doing
// something irreversible (e.g. delete wallet)
//
// 'keystore': User must supply a password
// that matches the keystore password (if set).
//
// 'keystore_else_pin': User must supply a password
// that matches the keystore password (if set), unless
// the keystore is 'unlocked' which means the wallet password
// has been given when opening the wallet, and is the same as
// the keystore password (should always be the case). In that
// case a PIN is asked.
// This is mainly used when signing a transaction.
//
// 'pin': User must supply the configured PIN code
//

function handleAuthRequired(qtobject, method, authMessage) {
console.log('auth using method ' + method)
if (method == 'wallet') {
if (Daemon.currentWallet.verifyPassword('')) {
if (method == 'wallet_password') {
if (!Daemon.currentWallet.isEncrypted
&& Daemon.currentWallet.verifyKeystorePassword('')) {
// wallet has no password
qtobject.authProceed()
} else {
var dialog = app.passwordDialog.createObject(app, {'title': qsTr('Enter current password')})
dialog.accepted.connect(function() {
if (Daemon.currentWallet.verifyPassword(dialog.password)) {
qtobject.authProceed()
} else {
qtobject.authCancel()
}
if (!Daemon.currentWallet.isEncrypted) {
handleAuthVerifyPassword(qtobject, authMessage, function(password) {
return Daemon.currentWallet.verifyKeystorePassword(password)
})
} else {
handleAuthVerifyPassword(qtobject, authMessage, function(password) {
return Daemon.currentWallet.verifyPassword(password)
})
}
}
} else if (method == 'keystore_else_pin') {
if (!Daemon.currentWallet.canHaveKeystoreEncryption()
|| Daemon.currentWallet.verifyKeystorePassword('')) {
handleAuthRequired(qtobject, 'pin', authMessage)
} else if (Daemon.currentWallet.isKeystorePasswordWalletPassword()) {
handleAuthRequired(qtobject, 'pin', authMessage)
} else {
handleAuthVerifyPassword(qtobject, authMessage, function(password) {
return Daemon.currentWallet.verifyKeystorePassword(password)
})
dialog.rejected.connect(function() {
qtobject.authCancel()
}
} else if (method == 'keystore') {
if (!Daemon.currentWallet.canHaveKeystoreEncryption()
|| Daemon.currentWallet.verifyKeystorePassword('')) {
qtobject.authProceed()
} else {
handleAuthVerifyPassword(qtobject, authMessage, function(password) {
return Daemon.currentWallet.verifyKeystorePassword(password)
})
dialog.open()
}
} else if (method == 'pin') {
if (Config.pinCode == '') {
// no PIN configured
handleAuthConfirmationOnly(qtobject, authMessage)
} else {
var dialog = app.pinDialog.createObject(app, {
mode: 'check',
pincode: Config.pinCode,
authMessage: authMessage
})
dialog.accepted.connect(function() {
qtobject.authProceed()
dialog.close()
})
dialog.rejected.connect(function() {
qtobject.authCancel()
})
dialog.open()
handleAuthVerifyPin(qtobject, authMessage)
}
} else {
console.log('unknown auth method ' + method)
qtobject.authCancel()
}
}

function handleAuthVerifyPassword(qtobject, authMessage, validator) {
var dialog = app.passwordDialog.createObject(app, {
title: authMessage ? authMessage : qsTr('Enter current password')
})
dialog.accepted.connect(function() {
if (validator(dialog.password)) {
qtobject.authProceed(dialog.password)
} else {
qtobject.authCancel()
var fdialog = app.messageDialog.createObject(app, {
title: qsTr('Password incorrect')
})
fdialog.open()
}
})
dialog.rejected.connect(function() {
qtobject.authCancel()
})
dialog.open()
}

function handleAuthConfirmationOnly(qtobject, authMessage) {
if (!authMessage) {
qtobject.authProceed()
Expand All @@ -610,6 +663,22 @@ ApplicationWindow
dialog.open()
}

function handleAuthVerifyPin(qtobject, authMessage) {
var dialog = app.pinDialog.createObject(app, {
mode: 'check',
pincode: Config.pinCode,
authMessage: authMessage
})
dialog.accepted.connect(function() {
qtobject.authProceed()
dialog.close()
})
dialog.rejected.connect(function() {
qtobject.authCancel()
})
dialog.open()
}

function startSwap() {
var swapdialog = swapDialog.createObject(app)
swapdialog.open()
Expand Down
9 changes: 6 additions & 3 deletions electrum/gui/qml/qechannelopener.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,16 +165,16 @@ def openChannel(self, confirm_backup_conflict=False):
node_id=self._node_pubkey,
fee_est=None)

acpt = lambda tx: self.do_open_channel(tx, self._connect_str_resolved, self._wallet.password)
acpt = lambda tx: self.do_open_channel(tx, self._connect_str_resolved)

self._finalizer = QETxFinalizer(self, make_tx=mktx, accept=acpt)
self._finalizer.canRbf = False
self._finalizer.amount = self._amount
self._finalizer.wallet = self._wallet
self.finalizerChanged.emit()

@auth_protect(message=_('Open Lightning channel?'))
def do_open_channel(self, funding_tx, conn_str, password):
@auth_protect(method='keystore_else_pin', message=_('Open Lightning channel?'))
def do_open_channel(self, funding_tx, conn_str, password=None):
"""
conn_str: a connection string that extract_nodeid can parse, i.e. cannot be a trampoline name
"""
Expand All @@ -183,6 +183,9 @@ def do_open_channel(self, funding_tx, conn_str, password):
funding_sat = funding_tx.output_value_for_address(ln_dummy_address())
lnworker = self._wallet.wallet.lnworker

if password is None:
password = self._wallet.password

def open_thread():
error = None
try:
Expand Down
6 changes: 4 additions & 2 deletions electrum/gui/qml/qeconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,10 @@ def pinCode(self, pin_code):
self.config.set_key('pin_code', pin_code, True)
self.pinCodeChanged.emit()

@auth_protect(method='wallet')
def pinCodeRemoveAuth(self):
# TODO: this allows disabling PIN unconditionally if wallet has no password
# (which should never be the case however)
@auth_protect(method='wallet_password')
def pinCodeRemoveAuth(self, password=None):
self.config.set_key('pin_code', '', True)
self.pinCodeChanged.emit()

Expand Down
11 changes: 7 additions & 4 deletions electrum/gui/qml/qedaemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ def checkThenDeleteWallet(self, wallet, confirm_requests=False, confirm_balance=

self.delete_wallet(wallet)

@auth_protect(message=_('Really delete this wallet?'))
@auth_protect(method='wallet_password', message=_('Really delete this wallet?'))
def delete_wallet(self, wallet):
path = standardize_path(wallet.wallet.storage.path)
self._logger.debug('deleting wallet with path %s' % path)
Expand Down Expand Up @@ -315,12 +315,15 @@ def suggestWalletName(self):
return f'wallet_{i}'

@pyqtSlot()
@auth_protect(method='wallet')
def startChangePassword(self):
if self._use_single_password:
self.requestNewPassword.emit()
self._do_start_change_all_passwords()
else:
self.currentWallet.requestNewPassword.emit()
self.currentWallet.startChangePassword()

@auth_protect(method='wallet_password')
def _do_start_change_all_passwords(self):
self.requestNewPassword.emit()

@pyqtSlot(str)
def setPassword(self, password):
Expand Down
32 changes: 20 additions & 12 deletions electrum/gui/qml/qeswaphelper.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,15 +341,19 @@ def fwd_swap_updatetx(self):
self.miningfee = QEAmount(amount_sat=self._tx.get_fee()) if self._tx else QEAmount()
self.check_valid(pay_amount, self._receive_amount)

def do_normal_swap(self, lightning_amount, onchain_amount):
def do_normal_swap(self, lightning_amount, onchain_amount, password):
assert self._tx
if lightning_amount is None or onchain_amount is None:
return

if password is None:
password = self._wallet.password

loop = get_asyncio_loop()
coro = self._wallet.wallet.lnworker.swap_manager.normal_swap(
lightning_amount_sat=lightning_amount,
expected_onchain_amount_sat=onchain_amount,
password=self._wallet.password,
password=password,
tx=self._tx,
)

Expand Down Expand Up @@ -424,15 +428,19 @@ def executeSwap(self):
if not self._wallet.wallet.network:
self.error.emit(_("You are offline."))
return
self._do_execute_swap()

@auth_protect(message=_('Confirm Lightning swap?'))
def _do_execute_swap(self):
if self.isReverse:
lightning_amount = self._send_amount
onchain_amount = self._receive_amount
self.do_reverse_swap(lightning_amount, onchain_amount)
self._do_execute_reverse_swap()
else:
lightning_amount = self._receive_amount
onchain_amount = self._send_amount
self.do_normal_swap(lightning_amount, onchain_amount)
self._do_execute_forward_swap()

@auth_protect(method='pin', message=_('Confirm Lightning swap?'))
def _do_execute_reverse_swap(self):
lightning_amount = self._send_amount
onchain_amount = self._receive_amount
self.do_reverse_swap(lightning_amount, onchain_amount)

@auth_protect(method='keystore_or_pin', message=_('Confirm Lightning swap?'))
def _do_execute_forward_swap(self, password=None):
lightning_amount = self._receive_amount
onchain_amount = self._send_amount
self.do_normal_swap(lightning_amount, onchain_amount, password)
Loading

0 comments on commit 6abce44

Please sign in to comment.