support TrustedCoin plugin in the kivy GUI
This commit is contained in:
@@ -8,4 +8,4 @@ description = ''.join([
|
||||
])
|
||||
requires_wallet_type = ['2fa']
|
||||
registers_wallet_type = '2fa'
|
||||
available_for = ['qt', 'cmdline']
|
||||
available_for = ['qt', 'cmdline', 'kivy']
|
||||
|
||||
@@ -38,7 +38,7 @@ from electrum_gui.qt.amountedit import AmountEdit
|
||||
from electrum_gui.qt.main_window import StatusBarButton
|
||||
from electrum.i18n import _
|
||||
from electrum.plugins import hook
|
||||
from electrum.util import PrintError
|
||||
from electrum.util import PrintError, is_valid_email
|
||||
from .trustedcoin import TrustedCoinPlugin, server
|
||||
|
||||
|
||||
@@ -48,36 +48,28 @@ class TOS(QTextEdit):
|
||||
|
||||
|
||||
class HandlerTwoFactor(QObject, PrintError):
|
||||
otp_start_signal = pyqtSignal(object, object)
|
||||
|
||||
def __init__(self, plugin, window):
|
||||
super().__init__()
|
||||
self.plugin = plugin
|
||||
self.window = window
|
||||
self.otp_start_signal.connect(self._prompt_user_for_otp)
|
||||
self.otp_done = threading.Event()
|
||||
|
||||
def prompt_user_for_otp(self, wallet, tx):
|
||||
self.otp_done.clear()
|
||||
self.otp_start_signal.emit(wallet, tx)
|
||||
self.otp_done.wait()
|
||||
|
||||
def _prompt_user_for_otp(self, wallet, tx):
|
||||
def prompt_user_for_otp(self, wallet, tx, on_success, on_failure):
|
||||
if not isinstance(wallet, self.plugin.wallet_class):
|
||||
return
|
||||
if wallet.can_sign_without_server():
|
||||
return
|
||||
if not wallet.keystores['x3/'].get_tx_derivations(tx):
|
||||
self.print_error("twofactor: xpub3 not needed")
|
||||
return
|
||||
window = self.window.top_level_window()
|
||||
auth_code = self.plugin.auth_dialog(window)
|
||||
try:
|
||||
window = self.window.top_level_window()
|
||||
if not isinstance(wallet, self.plugin.wallet_class):
|
||||
return
|
||||
if not wallet.can_sign_without_server():
|
||||
self.print_error("twofactor:sign_tx")
|
||||
auth_code = None
|
||||
if wallet.keystores['x3/'].get_tx_derivations(tx):
|
||||
auth_code = self.plugin.auth_dialog(window)
|
||||
else:
|
||||
self.print_error("twofactor: xpub3 not needed")
|
||||
wallet.auth_code = auth_code
|
||||
finally:
|
||||
self.otp_done.set()
|
||||
|
||||
wallet.on_otp(tx, auth_code)
|
||||
except:
|
||||
on_failure(sys.exc_info())
|
||||
return
|
||||
on_success(tx)
|
||||
|
||||
class Plugin(TrustedCoinPlugin):
|
||||
|
||||
@@ -123,8 +115,8 @@ class Plugin(TrustedCoinPlugin):
|
||||
return
|
||||
return pw.get_amount()
|
||||
|
||||
def prompt_user_for_otp(self, wallet, tx):
|
||||
wallet.handler_2fa.prompt_user_for_otp(wallet, tx)
|
||||
def prompt_user_for_otp(self, wallet, tx, on_success, on_failure):
|
||||
wallet.handler_2fa.prompt_user_for_otp(wallet, tx, on_success, on_failure)
|
||||
|
||||
def waiting_dialog(self, window, on_finished=None):
|
||||
task = partial(self.request_billing_info, window.wallet)
|
||||
@@ -145,7 +137,6 @@ class Plugin(TrustedCoinPlugin):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def settings_dialog(self, window):
|
||||
self.waiting_dialog(window, partial(self.show_settings_dialog, window))
|
||||
|
||||
@@ -216,6 +207,20 @@ class Plugin(TrustedCoinPlugin):
|
||||
window.message_e.setFrozen(True)
|
||||
window.amount_e.setFrozen(True)
|
||||
|
||||
def go_online_dialog(self, wizard):
|
||||
msg = [
|
||||
_("Your wallet file is: {}.").format(os.path.abspath(wizard.storage.path)),
|
||||
_("You need to be online in order to complete the creation of "
|
||||
"your wallet. If you generated your seed on an offline "
|
||||
'computer, click on "{}" to close this window, move your '
|
||||
"wallet file to an online computer, and reopen it with "
|
||||
"Electrum.").format(_('Cancel')),
|
||||
_('If you are online, click on "{}" to continue.').format(_('Next'))
|
||||
]
|
||||
msg = '\n\n'.join(msg)
|
||||
wizard.stack = []
|
||||
wizard.confirm_dialog(title='', message=msg, run_next = lambda x: wizard.run('accept_terms_of_use'))
|
||||
|
||||
def accept_terms_of_use(self, window):
|
||||
vbox = QVBoxLayout()
|
||||
vbox.addWidget(QLabel(_("Terms of Service")))
|
||||
@@ -256,24 +261,21 @@ class Plugin(TrustedCoinPlugin):
|
||||
window.terminate()
|
||||
|
||||
def set_enabled():
|
||||
valid_email = re.match(regexp, email_e.text()) is not None
|
||||
next_button.setEnabled(tos_received and valid_email)
|
||||
next_button.setEnabled(tos_received and is_valid_email(email_e.text()))
|
||||
|
||||
tos_e.tos_signal.connect(on_result)
|
||||
tos_e.error_signal.connect(on_error)
|
||||
t = Thread(target=request_TOS)
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
|
||||
regexp = r"[^@]+@[^@]+\.[^@]+"
|
||||
email_e.textChanged.connect(set_enabled)
|
||||
email_e.setFocus(True)
|
||||
|
||||
window.exec_layout(vbox, next_enabled=False)
|
||||
next_button.setText(prior_button_text)
|
||||
return str(email_e.text())
|
||||
email = str(email_e.text())
|
||||
self.create_remote_key(email, window)
|
||||
|
||||
def request_otp_dialog(self, window, _id, otp_secret):
|
||||
def request_otp_dialog(self, window, short_id, otp_secret, xpub3):
|
||||
vbox = QVBoxLayout()
|
||||
if otp_secret is not None:
|
||||
uri = "otpauth://totp/%s?secret=%s"%('trustedcoin.com', otp_secret)
|
||||
@@ -291,7 +293,6 @@ class Plugin(TrustedCoinPlugin):
|
||||
label.setWordWrap(1)
|
||||
vbox.addWidget(label)
|
||||
msg = _('Google Authenticator code:')
|
||||
|
||||
hbox = QHBoxLayout()
|
||||
hbox.addWidget(WWLabel(msg))
|
||||
pw = AmountEdit(None, is_int = True)
|
||||
@@ -299,21 +300,14 @@ class Plugin(TrustedCoinPlugin):
|
||||
pw.setMaximumWidth(50)
|
||||
hbox.addWidget(pw)
|
||||
vbox.addLayout(hbox)
|
||||
|
||||
cb_lost = QCheckBox(_("I have lost my Google Authenticator account"))
|
||||
cb_lost.setToolTip(_("Check this box to request a new secret. You will need to retype your seed."))
|
||||
vbox.addWidget(cb_lost)
|
||||
cb_lost.setVisible(otp_secret is None)
|
||||
|
||||
def set_enabled():
|
||||
b = True if cb_lost.isChecked() else len(pw.text()) == 6
|
||||
window.next_button.setEnabled(b)
|
||||
|
||||
pw.textChanged.connect(set_enabled)
|
||||
cb_lost.toggled.connect(set_enabled)
|
||||
|
||||
window.exec_layout(vbox, next_enabled=False,
|
||||
raise_on_cancel=False)
|
||||
return pw.get_amount(), cb_lost.isChecked()
|
||||
|
||||
|
||||
window.exec_layout(vbox, next_enabled=False, raise_on_cancel=False)
|
||||
self.check_otp(window, short_id, otp_secret, xpub3, pw.get_amount(), cb_lost.isChecked())
|
||||
|
||||
@@ -75,6 +75,18 @@ DISCLAIMER = [
|
||||
"To be safe from malware, you may want to do this on an offline "
|
||||
"computer, and move your wallet later to an online computer."),
|
||||
]
|
||||
|
||||
KIVY_DISCLAIMER = [
|
||||
_("Two-factor authentication is a service provided by TrustedCoin. "
|
||||
"To use it, you must have a separate device with Google Authenticator."),
|
||||
_("This service uses a multi-signature wallet, where you own 2 of 3 keys. "
|
||||
"The third key is stored on a remote server that signs transactions on "
|
||||
"your behalf.A small fee will be charged on each transaction that uses the "
|
||||
"remote server."),
|
||||
_("Note that your coins are not locked in this service. You may withdraw "
|
||||
"your funds at any time and at no cost, without the remote server, by "
|
||||
"using the 'restore wallet' option with your wallet seed."),
|
||||
]
|
||||
RESTORE_MSG = _("Enter the seed for your 2-factor wallet:")
|
||||
|
||||
class TrustedCoinException(Exception):
|
||||
@@ -215,7 +227,6 @@ class Wallet_2fa(Multisig_Wallet):
|
||||
Deterministic_Wallet.__init__(self, storage)
|
||||
self.is_billing = False
|
||||
self.billing_info = None
|
||||
self.auth_code = None
|
||||
|
||||
def can_sign_without_server(self):
|
||||
return not self.keystores['x2/'].is_watching_only()
|
||||
@@ -269,25 +280,22 @@ class Wallet_2fa(Multisig_Wallet):
|
||||
tx = mk_tx(outputs)
|
||||
return tx
|
||||
|
||||
def sign_transaction(self, tx, password):
|
||||
Multisig_Wallet.sign_transaction(self, tx, password)
|
||||
if tx.is_complete():
|
||||
return
|
||||
self.plugin.prompt_user_for_otp(self, tx)
|
||||
if not self.auth_code:
|
||||
def on_otp(self, tx, otp):
|
||||
if not otp:
|
||||
self.print_error("sign_transaction: no auth code")
|
||||
return
|
||||
otp = int(otp)
|
||||
long_user_id, short_id = self.get_user_id()
|
||||
tx_dict = tx.as_dict()
|
||||
raw_tx = tx_dict["hex"]
|
||||
r = server.sign(short_id, raw_tx, self.auth_code)
|
||||
r = server.sign(short_id, raw_tx, otp)
|
||||
if r:
|
||||
raw_tx = r.get('transaction')
|
||||
tx.update(raw_tx)
|
||||
self.print_error("twofactor: is complete", tx.is_complete())
|
||||
# reset billing_info
|
||||
self.billing_info = None
|
||||
self.auth_code = None
|
||||
|
||||
|
||||
|
||||
# Utility functions
|
||||
@@ -316,6 +324,7 @@ def make_billing_address(wallet, num):
|
||||
|
||||
class TrustedCoinPlugin(BasePlugin):
|
||||
wallet_class = Wallet_2fa
|
||||
disclaimer_msg = DISCLAIMER
|
||||
|
||||
def __init__(self, parent, config, name):
|
||||
BasePlugin.__init__(self, parent, config, name)
|
||||
@@ -335,6 +344,21 @@ class TrustedCoinPlugin(BasePlugin):
|
||||
def can_user_disable(self):
|
||||
return False
|
||||
|
||||
@hook
|
||||
def tc_sign_wrapper(self, wallet, tx, on_success, on_failure):
|
||||
if not isinstance(wallet, self.wallet_class):
|
||||
return
|
||||
if tx.is_complete():
|
||||
return
|
||||
if wallet.can_sign_without_server():
|
||||
return
|
||||
if not wallet.keystores['x3/'].get_tx_derivations(tx):
|
||||
self.print_error("twofactor: xpub3 not needed")
|
||||
return
|
||||
def wrapper(tx):
|
||||
self.prompt_user_for_otp(wallet, tx, on_success, on_failure)
|
||||
return wrapper
|
||||
|
||||
@hook
|
||||
def get_tx_extra_fee(self, wallet, tx):
|
||||
if type(wallet) != Wallet_2fa:
|
||||
@@ -391,7 +415,7 @@ class TrustedCoinPlugin(BasePlugin):
|
||||
def show_disclaimer(self, wizard):
|
||||
wizard.set_icon(':icons/trustedcoin-wizard.png')
|
||||
wizard.stack = []
|
||||
wizard.confirm_dialog(title='Disclaimer', message='\n\n'.join(DISCLAIMER), run_next = lambda x: wizard.run('choose_seed'))
|
||||
wizard.confirm_dialog(title='Disclaimer', message='\n\n'.join(self.disclaimer_msg), run_next = lambda x: wizard.run('choose_seed'))
|
||||
|
||||
def choose_seed(self, wizard):
|
||||
title = _('Create or restore')
|
||||
@@ -450,18 +474,7 @@ class TrustedCoinPlugin(BasePlugin):
|
||||
wizard.storage.put('x1/', k1.dump())
|
||||
wizard.storage.put('x2/', k2.dump())
|
||||
wizard.storage.write()
|
||||
msg = [
|
||||
_("Your wallet file is: {}.").format(os.path.abspath(wizard.storage.path)),
|
||||
_("You need to be online in order to complete the creation of "
|
||||
"your wallet. If you generated your seed on an offline "
|
||||
'computer, click on "{}" to close this window, move your '
|
||||
"wallet file to an online computer, and reopen it with "
|
||||
"Electrum.").format(_('Cancel')),
|
||||
_('If you are online, click on "{}" to continue.').format(_('Next'))
|
||||
]
|
||||
msg = '\n\n'.join(msg)
|
||||
wizard.stack = []
|
||||
wizard.confirm_dialog(title='', message=msg, run_next = lambda x: wizard.run('create_remote_key'))
|
||||
self.go_online_dialog(wizard)
|
||||
|
||||
def restore_wallet(self, wizard):
|
||||
wizard.opt_bip39 = False
|
||||
@@ -516,8 +529,8 @@ class TrustedCoinPlugin(BasePlugin):
|
||||
wizard.wallet = Wallet_2fa(storage)
|
||||
wizard.create_addresses()
|
||||
|
||||
def create_remote_key(self, wizard):
|
||||
email = self.accept_terms_of_use(wizard)
|
||||
|
||||
def create_remote_key(self, email, wizard):
|
||||
xpub1 = wizard.storage.get('x1/')['xpub']
|
||||
xpub2 = wizard.storage.get('x2/')['xpub']
|
||||
# Generate third key deterministically.
|
||||
@@ -550,10 +563,9 @@ class TrustedCoinPlugin(BasePlugin):
|
||||
except Exception as e:
|
||||
wizard.show_message(str(e))
|
||||
return
|
||||
self.check_otp(wizard, short_id, otp_secret, xpub3)
|
||||
self.request_otp_dialog(wizard, short_id, otp_secret, xpub3)
|
||||
|
||||
def check_otp(self, wizard, short_id, otp_secret, xpub3):
|
||||
otp, reset = self.request_otp_dialog(wizard, short_id, otp_secret)
|
||||
def check_otp(self, wizard, short_id, otp_secret, xpub3, otp, reset):
|
||||
if otp:
|
||||
self.do_auth(wizard, short_id, otp, xpub3)
|
||||
elif reset:
|
||||
@@ -569,8 +581,8 @@ class TrustedCoinPlugin(BasePlugin):
|
||||
def do_auth(self, wizard, short_id, otp, xpub3):
|
||||
try:
|
||||
server.auth(short_id, otp)
|
||||
except:
|
||||
wizard.show_message(_('Incorrect password'))
|
||||
except Exception as e:
|
||||
wizard.show_message(str(e))
|
||||
return
|
||||
k3 = keystore.from_xpub(xpub3)
|
||||
wizard.storage.put('x3/', k3.dump())
|
||||
@@ -603,7 +615,7 @@ class TrustedCoinPlugin(BasePlugin):
|
||||
if not new_secret:
|
||||
wizard.show_message(_('Request rejected by server'))
|
||||
return
|
||||
self.check_otp(wizard, short_id, new_secret, xpub3)
|
||||
self.request_otp_dialog(wizard, short_id, new_secret, xpub3)
|
||||
|
||||
@hook
|
||||
def get_action(self, storage):
|
||||
@@ -614,4 +626,4 @@ class TrustedCoinPlugin(BasePlugin):
|
||||
if not storage.get('x2/'):
|
||||
return self, 'show_disclaimer'
|
||||
if not storage.get('x3/'):
|
||||
return self, 'create_remote_key'
|
||||
return self, 'accept_terms_of_use'
|
||||
|
||||
Reference in New Issue
Block a user