Browse Source

add recovery paths (disable and confirm/reset OTP) for 2FA

patch-4
Sander van Grieken 2 years ago
parent
commit
c6496d02ef
  1. 2
      electrum/gui/qml/qewizard.py
  2. 12
      electrum/gui/wizard.py
  3. 150
      electrum/plugins/trustedcoin/qml.py
  4. 35
      electrum/plugins/trustedcoin/qml/KeepDisable.qml
  5. 54
      electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml

2
electrum/gui/qml/qewizard.py

@ -73,7 +73,7 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard):
self._path = path
self.pathChanged.emit()
def last_if_single_password(self, view, wizard_data):
def last_if_single_password(self, *args):
return self._daemon.singlePasswordEnabled
@pyqtSlot('QJSValue', bool, str)

12
electrum/gui/wizard.py

@ -234,7 +234,7 @@ class NewWalletWizard(AbstractWizard):
for addr in data['address_list'].split():
addresses[addr] = {}
elif data['keystore_type'] in ['createseed', 'haveseed']:
if data['seed_type'] in ['old', 'standard', 'segwit']: #2fa, 2fa-segwit
if data['seed_type'] in ['old', 'standard', 'segwit']:
self._logger.debug('creating keystore from electrum seed')
k = keystore.from_seed(data['seed'], data['seed_extra_words'], data['wallet_type'] == 'multisig')
elif data['seed_type'] == 'bip39':
@ -243,7 +243,7 @@ class NewWalletWizard(AbstractWizard):
derivation = normalize_bip32_derivation(data['derivation_path'])
script = data['script_type'] if data['script_type'] != 'p2pkh' else 'standard'
k = keystore.from_bip43_rootseed(root_seed, derivation, xtype=script)
elif data['seed_type'] == '2fa_segwit': # TODO: legacy 2fa
elif data['seed_type'] == '2fa_segwit': # TODO: legacy 2fa '2fa'
self._logger.debug('creating keystore from 2fa seed')
k = keystore.from_xprv(data['x1/']['xprv'])
else:
@ -274,7 +274,13 @@ class NewWalletWizard(AbstractWizard):
db.put('keystore', k.dump())
elif data['wallet_type'] == '2fa':
db.put('x1/', k.dump())
db.put('x2/', data['x2/'])
if data['trustedcoin_keepordisable'] == 'disable':
k2 = keystore.from_xprv(data['x2/']['xprv'])
if data['encrypt'] and k2.may_have_password():
k2.update_password(None, data['password'])
db.put('x2/', k2.dump())
else:
db.put('x2/', data['x2/'])
db.put('x3/', data['x3/'])
db.put('use_trustedcoin', True)
elif data['wallet_type'] == 'imported':

150
electrum/plugins/trustedcoin/qml.py

@ -1,12 +1,13 @@
import threading
import socket
import base64
from typing import TYPE_CHECKING
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
from electrum.i18n import _
from electrum.plugin import hook
from electrum.bip32 import xpub_type
from electrum.bip32 import xpub_type, BIP32Node
from electrum.util import UserFacingException
from electrum import keystore
@ -30,9 +31,7 @@ class Plugin(TrustedCoinPlugin):
_termsAndConditions = ''
termsAndConditionsErrorChanged = pyqtSignal()
_termsAndConditionsError = ''
createRemoteKeyErrorChanged = pyqtSignal()
_createRemoteKeyError = ''
otpError = pyqtSignal()
otpError = pyqtSignal([str], arguments=['message'])
otpSuccess = pyqtSignal()
disclaimerChanged = pyqtSignal()
keystoreChanged = pyqtSignal()
@ -41,6 +40,10 @@ class Plugin(TrustedCoinPlugin):
shortIdChanged = pyqtSignal()
_shortId = ''
_remoteKeyState = ''
remoteKeyStateChanged = pyqtSignal()
remoteKeyError = pyqtSignal([str], arguments=['message'])
requestOtp = pyqtSignal()
def __init__(self, plugin, parent):
@ -81,9 +84,15 @@ class Plugin(TrustedCoinPlugin):
def termsAndConditionsError(self):
return self._termsAndConditionsError
@pyqtProperty(str, notify=createRemoteKeyErrorChanged)
def createRemoteKeyError(self):
return self._createRemoteKeyError
@pyqtProperty(str, notify=remoteKeyStateChanged)
def remoteKeyState(self):
return self._remoteKeyState
@remoteKeyState.setter
def remoteKeyState(self, new_state):
if self._remoteKeyState != new_state:
self._remoteKeyState = new_state
self.remoteKeyStateChanged.emit()
@pyqtSlot()
def fetchTermsAndConditions(self):
@ -112,7 +121,8 @@ class Plugin(TrustedCoinPlugin):
@pyqtSlot(str)
def createKeystore(self, email):
xprv1, xpub1, xpub2, xpub3, short_id = self.plugin.create_keys()
self.remoteKeyState = ''
xprv1, xpub1, xprv2, xpub2, xpub3, short_id = self.plugin.create_keys()
def create_remote_key_task():
try:
self.plugin.logger.debug('create remote key')
@ -121,25 +131,32 @@ class Plugin(TrustedCoinPlugin):
otp_secret = r['otp_secret']
_xpub3 = r['xpubkey_cosigner']
_id = r['id']
except (socket.error, ErrorConnectingServer):
self._createRemoteKeyError = _('Error creating key')
self.createRemoteKeyErrorChanged.emit()
except (socket.error, ErrorConnectingServer) as e:
self.remoteKeyState = 'error'
self.remoteKeyError.emit(f'Network error: {str(e)}')
except TrustedCoinException as e:
# if e.status_code == 409: TODO ?
# r = None
self._createRemoteKeyError = str(e)
self.createRemoteKeyErrorChanged.emit()
if e.status_code == 409:
self.remoteKeyState = 'wallet_known'
self._shortId = short_id
self.shortIdChanged.emit()
else:
self.remoteKeyState = 'error'
self.logger.warning(str(e))
self.remoteKeyError.emit(f'Service error: {str(e)}')
except (KeyError,TypeError) as e: # catch any assumptions
self._createRemoteKeyError = str(e)
self.createRemoteKeyErrorChanged.emit()
self.remoteKeyState = 'error'
self.remoteKeyError.emit(f'Error: {str(e)}')
self.logger.error(str(e))
else:
if short_id != _id:
self._createRemoteKeyError = "unexpected trustedcoin short_id: expected {}, received {}".format(short_id, _id)
self.createRemoteKeyErrorChanged.emit()
self.remoteKeyState = 'error'
self.logger.error("unexpected trustedcoin short_id: expected {}, received {}".format(short_id, _id))
self.remoteKeyError.emit('Unexpected short_id')
return
if xpub3 != _xpub3:
self._createRemoteKeyError = "unexpected trustedcoin xpub3: expected {}, received {}".format(xpub3, _xpub3)
self.createRemoteKeyErrorChanged.emit()
self.remoteKeyState = 'error'
self.logger.error("unexpected trustedcoin xpub3: expected {}, received {}".format(xpub3, _xpub3))
self.remoteKeyError.emit('Unexpected trustedcoin xpub3')
return
self._otpSecret = otp_secret
self.otpSecretChanged.emit()
@ -151,10 +168,49 @@ class Plugin(TrustedCoinPlugin):
self._busy = True
self.busyChanged.emit()
t = threading.Thread(target=create_remote_key_task)
t.daemon = True
t.start()
@pyqtSlot()
def resetOtpSecret(self):
self.remoteKeyState = ''
xprv1, xpub1, xprv2, xpub2, xpub3, short_id = self.plugin.create_keys()
def reset_otp_task():
try:
self.plugin.logger.debug('reset_otp')
r = server.get_challenge(short_id)
challenge = r.get('challenge')
message = 'TRUSTEDCOIN CHALLENGE: ' + challenge
def f(xprv):
rootnode = BIP32Node.from_xkey(xprv)
key = rootnode.subkey_at_private_derivation((0, 0)).eckey
sig = key.sign_message(message, True)
return base64.b64encode(sig).decode()
signatures = [f(x) for x in [xprv1, xprv2]]
r = server.reset_auth(short_id, challenge, signatures)
otp_secret = r.get('otp_secret')
except (socket.error, ErrorConnectingServer) as e:
self.remoteKeyState = 'error'
self.remoteKeyError.emit(f'Network error: {str(e)}')
except Exception as e:
self.remoteKeyState = 'error'
self.remoteKeyError.emit(f'Error: {str(e)}')
else:
self._otpSecret = otp_secret
self.otpSecretChanged.emit()
finally:
self._busy = False
self.busyChanged.emit()
self._busy = True
self.busyChanged.emit()
t = threading.Thread(target=reset_otp_task, daemon=True)
t.start()
@pyqtSlot(str, int)
def checkOtp(self, short_id, otp):
def check_otp_task():
@ -164,15 +220,13 @@ class Plugin(TrustedCoinPlugin):
except TrustedCoinException as e:
if e.status_code == 400: # invalid OTP
self.plugin.logger.debug('Invalid one-time password.')
self.otpError.emit()
self.otpError.emit(_('Invalid one-time password.'))
else:
self.plugin.logger.error(str(e))
self._createRemoteKeyError = str(e)
self.createRemoteKeyErrorChanged.emit()
self.otpError.emit(f'Service error: {str(e)}')
except Exception as e:
self.plugin.logger.error(str(e))
self._createRemoteKeyError = str(e)
self.createRemoteKeyErrorChanged.emit()
self.otpError.emit(f'Error: {str(e)}')
else:
self.plugin.logger.debug('OTP verify success')
self.otpSuccess.emit()
@ -182,8 +236,7 @@ class Plugin(TrustedCoinPlugin):
self._busy = True
self.busyChanged.emit()
t = threading.Thread(target=check_otp_task)
t.daemon = True
t = threading.Thread(target=check_otp_task, daemon=True)
t.start()
@ -204,6 +257,7 @@ class Plugin(TrustedCoinPlugin):
_('This wallet was restored from seed, and it contains two master private keys.'),
_('Therefore, two-factor authentication is disabled.')
])
self.logger.info(msg)
#action = lambda: window.show_message(msg)
#else:
#action = partial(self.settings_dialog, window)
@ -233,7 +287,8 @@ class Plugin(TrustedCoinPlugin):
},
'trustedcoin_choose_seed': {
'gui': '../../../../plugins/trustedcoin/qml/ChooseSeed',
'next': self.on_choose_seed
'next': lambda d: 'trustedcoin_create_seed' if d['keystore_type'] == 'createseed'
else 'trustedcoin_have_seed'
},
'trustedcoin_create_seed': {
'gui': 'WCCreateSeed',
@ -245,7 +300,14 @@ class Plugin(TrustedCoinPlugin):
},
'trustedcoin_have_seed': {
'gui': 'WCHaveSeed',
'next': 'trustedcoin_tos_email'
'next': 'trustedcoin_keep_disable'
},
'trustedcoin_keep_disable': {
'gui': '../../../../plugins/trustedcoin/qml/KeepDisable',
'next': lambda d: 'trustedcoin_tos_email' if d['trustedcoin_keepordisable'] != 'disable'
else 'wallet_password',
'accept': self.recovery_disable,
'last': lambda v,d: wizard.last_if_single_password() and d['trustedcoin_keepordisable'] == 'disable'
},
'trustedcoin_tos_email': {
'gui': '../../../../plugins/trustedcoin/qml/Terms',
@ -260,12 +322,6 @@ class Plugin(TrustedCoinPlugin):
}
wizard.navmap_merge(views)
def on_choose_seed(self, wizard_data):
self.logger.debug('on_choose_seed')
if wizard_data['keystore_type'] == 'createseed':
return 'trustedcoin_create_seed'
else:
return 'trustedcoin_have_seed'
# combined create_keystore and create_remote_key pre
def create_keys(self):
@ -286,21 +342,33 @@ class Plugin(TrustedCoinPlugin):
xtype = xpub_type(xpub1)
xpub3 = make_xpub(get_signing_xpub(xtype), long_user_id)
return (xprv1,xpub1,xpub2,xpub3,short_id)
return (xprv1,xpub1,xprv2,xpub2,xpub3,short_id)
def on_accept_otp_secret(self, wizard_data):
self.logger.debug('on accept otp: ' + repr(wizard_data))
self.logger.debug('OTP secret accepted, creating keystores')
xprv1,xpub1,xprv2,xpub2,xpub3,short_id = self.create_keys()
k1 = keystore.from_xprv(xprv1)
k2 = keystore.from_xpub(xpub2)
k3 = keystore.from_xpub(xpub3)
wizard_data['x1/'] = k1.dump()
wizard_data['x2/'] = k2.dump()
wizard_data['x3/'] = k3.dump()
xprv1,xpub1,xpub2,xpub3,short_id = self.create_keys()
def recovery_disable(self, wizard_data):
if wizard_data['trustedcoin_keepordisable'] != 'disable':
return
self.logger.debug('2fa disabled, creating keystores')
xprv1,xpub1,xprv2,xpub2,xpub3,short_id = self.create_keys()
k1 = keystore.from_xprv(xprv1)
k2 = keystore.from_xpub(xpub2)
k2 = keystore.from_xprv(xprv2)
k3 = keystore.from_xpub(xpub3)
wizard_data['x1/'] = k1.dump()
wizard_data['x2/'] = k2.dump()
wizard_data['x3/'] = k3.dump()
# wizard_data['use_trustedcoin'] = True
# regular wallet prompt functions

35
electrum/plugins/trustedcoin/qml/KeepDisable.qml

@ -0,0 +1,35 @@
import QtQuick 2.6
import QtQuick.Layouts 1.0
import QtQuick.Controls 2.1
import "../../../gui/qml/components/wizard"
WizardComponent {
valid: keepordisablegroup.checkedButton
function apply() {
wizard_data['trustedcoin_keepordisable'] = keepordisablegroup.checkedButton.keepordisable
}
ButtonGroup {
id: keepordisablegroup
onCheckedButtonChanged: checkIsLast()
}
ColumnLayout {
Label {
text: qsTr('Restore 2FA wallet')
}
RadioButton {
ButtonGroup.group: keepordisablegroup
property string keepordisable: 'keep'
checked: true
text: qsTr('Keep')
}
RadioButton {
ButtonGroup.group: keepordisablegroup
property string keepordisable: 'disable'
text: qsTr('Disable')
}
}
}

54
electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml

@ -12,6 +12,10 @@ WizardComponent {
property bool otpVerified: false
function apply() {
wizard_data['trustedcoin_new_otp_secret'] = requestNewSecret.checked
}
ColumnLayout {
width: parent.width
@ -20,16 +24,24 @@ WizardComponent {
}
InfoTextArea {
id: errorBox
iconStyle: InfoTextArea.IconStyle.Error
visible: plugin ? plugin.createRemoteKeyError : false
text: plugin ? plugin.createRemoteKeyError : ''
visible: !otpVerified && plugin.remoteKeyState == 'error'
}
InfoTextArea {
iconStyle: InfoTextArea.IconStyle.Warn
visible: plugin.remoteKeyState == 'wallet_known'
text: qsTr('This wallet is already registered with TrustedCoin. ')
+ qsTr('To finalize wallet creation, please enter your Google Authenticator Code. ')
}
QRImage {
Layout.alignment: Qt.AlignHCenter
visible: plugin.remoteKeyState == ''
qrdata: encodeURI('otpauth://totp/Electrum 2FA ' + wizard_data['wallet_name']
+ '?secret=' + plugin.otpSecret + '&digits=6')
render: plugin ? plugin.otpSecret : false
render: plugin.otpSecret
}
TextHighlightPane {
@ -43,17 +55,24 @@ WizardComponent {
}
Label {
visible: !otpVerified && plugin.otpSecret
Layout.preferredWidth: parent.width
wrapMode: Text.Wrap
text: qsTr('Enter or scan into authenticator app. Then authenticate below')
visible: plugin.otpSecret && !otpVerified
}
Label {
visible: !otpVerified && plugin.remoteKeyState == 'wallet_known'
Layout.preferredWidth: parent.width
wrapMode: Text.Wrap
text: qsTr('If you still have your OTP secret, then authenticate below')
}
TextField {
id: otp_auth
visible: !otpVerified && (plugin.otpSecret || plugin.remoteKeyState == 'wallet_known')
Layout.alignment: Qt.AlignHCenter
focus: true
visible: plugin.otpSecret && !otpVerified
inputMethodHints: Qt.ImhSensitiveData | Qt.ImhDigitsOnly
font.family: FixedFont
font.pixelSize: constants.fontSizeLarge
@ -65,12 +84,26 @@ WizardComponent {
}
}
Label {
visible: !otpVerified && plugin.remoteKeyState == 'wallet_known'
Layout.preferredWidth: parent.width
wrapMode: Text.Wrap
text: qsTr('Otherwise, you can request your OTP secret from the server, by pressing the button below')
}
Button {
Layout.alignment: Qt.AlignHCenter
visible: plugin.remoteKeyState == 'wallet_known' && !otpVerified
text: qsTr('Request OTP secret')
onClicked: plugin.resetOtpSecret()
}
Image {
Layout.alignment: Qt.AlignHCenter
source: '../../../gui/icons/confirmed.png'
visible: otpVerified
Layout.preferredWidth: constants.iconSizeLarge
Layout.preferredHeight: constants.iconSizeLarge
Layout.preferredWidth: constants.iconSizeXLarge
Layout.preferredHeight: constants.iconSizeXLarge
}
}
@ -88,14 +121,17 @@ WizardComponent {
Connections {
target: plugin
function onOtpError() {
function onOtpError(message) {
console.log('OTP verify error')
// TODO: show error in UI
errorBox.text = message
}
function onOtpSuccess() {
console.log('OTP verify success')
otpVerified = true
}
function onRemoteKeyError(message) {
errorBox.text = message
}
}
}

Loading…
Cancel
Save