Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

qml: rework auth #8382

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 12 additions & 6 deletions electrum/gui/qml/auth.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from functools import wraps, partial

from PyQt5.QtCore import pyqtSignal, pyqtSlot, pyqtProperty
from PyQt5.QtCore import pyqtSignal, pyqtSlot

from electrum.logging import get_logger


def auth_protect(func=None, reject=None, method='pin', message=''):
if func is None:
return partial(auth_protect, reject=reject, method=method, message=message)
Expand All @@ -20,23 +21,28 @@ def wrapper(self, *args, **kwargs):

return wrapper


class AuthMixin:
_auth_logger = get_logger(__name__)
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)
(func, args, kwargs, reject) = getattr(self, '__auth_fcall')
if password and 'password' in func.__code__.co_varnames:
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)}')
raise e
finally:
delattr(self,'__auth_fcall')
delattr(self, '__auth_fcall')

@pyqtSlot()
def authCancel(self):
Expand All @@ -45,7 +51,7 @@ def authCancel(self):
return

try:
(func,args,kwargs,reject) = getattr(self, '__auth_fcall')
(func, args, kwargs, reject) = getattr(self, '__auth_fcall')
if reject is not None:
if hasattr(self, reject):
getattr(self, reject)()
Expand Down
123 changes: 98 additions & 25 deletions electrum/gui/qml/components/main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -549,51 +549,108 @@ ApplicationWindow
}
}

// handle auth_protect decorator events. These MUST
// (eventually) end with a call to qtobject.authProceed()
// or qtobject.authCancel().
//
// The following method types are defined:
//
// '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 @@ -609,6 +666,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 @@ -139,8 +139,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
13 changes: 8 additions & 5 deletions electrum/gui/qml/qedaemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,8 @@ def checkThenDeleteWallet(self, wallet, confirm_requests=False, confirm_balance=

self.delete_wallet(wallet)

@auth_protect(message=_('Really delete this wallet?'))
def delete_wallet(self, wallet):
@auth_protect(method='wallet_password', message=_('Really delete this wallet?'))
def delete_wallet(self, wallet, password=None):
path = standardize_path(wallet.wallet.storage.path)
self._logger.debug('deleting wallet with path %s' % path)
self._current_wallet = None
Expand Down Expand Up @@ -314,12 +314,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, password=None):
self.requestNewPassword.emit()

@pyqtSlot(str, result=bool)
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_else_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)