add recovery paths (disable and confirm/reset OTP) for 2FA
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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,13 +342,11 @@ 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))
|
||||
|
||||
xprv1,xpub1,xpub2,xpub3,short_id = self.create_keys()
|
||||
|
||||
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)
|
||||
@@ -300,7 +354,21 @@ class Plugin(TrustedCoinPlugin):
|
||||
wizard_data['x1/'] = k1.dump()
|
||||
wizard_data['x2/'] = k2.dump()
|
||||
wizard_data['x3/'] = k3.dump()
|
||||
# wizard_data['use_trustedcoin'] = True
|
||||
|
||||
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_xprv(xprv2)
|
||||
k3 = keystore.from_xpub(xpub3)
|
||||
|
||||
wizard_data['x1/'] = k1.dump()
|
||||
wizard_data['x2/'] = k2.dump()
|
||||
wizard_data['x3/'] = k3.dump()
|
||||
|
||||
|
||||
# regular wallet prompt functions
|
||||
|
||||
|
||||
35
electrum/plugins/trustedcoin/qml/KeepDisable.qml
Normal file
35
electrum/plugins/trustedcoin/qml/KeepDisable.qml
Normal file
@@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user