electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

trustedcoin.py (32783B)


      1 #!/usr/bin/env python
      2 #
      3 # Electrum - Lightweight Bitcoin Client
      4 # Copyright (C) 2015 Thomas Voegtlin
      5 #
      6 # Permission is hereby granted, free of charge, to any person
      7 # obtaining a copy of this software and associated documentation files
      8 # (the "Software"), to deal in the Software without restriction,
      9 # including without limitation the rights to use, copy, modify, merge,
     10 # publish, distribute, sublicense, and/or sell copies of the Software,
     11 # and to permit persons to whom the Software is furnished to do so,
     12 # subject to the following conditions:
     13 #
     14 # The above copyright notice and this permission notice shall be
     15 # included in all copies or substantial portions of the Software.
     16 #
     17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
     18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
     19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
     20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
     21 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
     22 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
     23 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     24 # SOFTWARE.
     25 import asyncio
     26 import socket
     27 import json
     28 import base64
     29 import time
     30 import hashlib
     31 from collections import defaultdict
     32 from typing import Dict, Union, Sequence, List
     33 
     34 from urllib.parse import urljoin
     35 from urllib.parse import quote
     36 from aiohttp import ClientResponse
     37 
     38 from electrum import ecc, constants, keystore, version, bip32, bitcoin
     39 from electrum.bip32 import BIP32Node, xpub_type
     40 from electrum.crypto import sha256
     41 from electrum.transaction import PartialTxOutput, PartialTxInput, PartialTransaction, Transaction
     42 from electrum.mnemonic import Mnemonic, seed_type, is_any_2fa_seed_type
     43 from electrum.wallet import Multisig_Wallet, Deterministic_Wallet
     44 from electrum.i18n import _
     45 from electrum.plugin import BasePlugin, hook
     46 from electrum.util import NotEnoughFunds, UserFacingException
     47 from electrum.storage import StorageEncryptionVersion
     48 from electrum.network import Network
     49 from electrum.base_wizard import BaseWizard, WizardWalletPasswordSetting
     50 from electrum.logging import Logger
     51 
     52 
     53 def get_signing_xpub(xtype):
     54     if not constants.net.TESTNET:
     55         xpub = "xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL"
     56     else:
     57         xpub = "tpubD6NzVbkrYhZ4XdmyJQcCPjQfg6RXVUzGFhPjZ7uvRC8JLcS7Hw1i7UTpyhp9grHpak4TyK2hzBJrujDVLXQ6qB5tNpVx9rC6ixijUXadnmY"
     58     if xtype not in ('standard', 'p2wsh'):
     59         raise NotImplementedError('xtype: {}'.format(xtype))
     60     if xtype == 'standard':
     61         return xpub
     62     node = BIP32Node.from_xkey(xpub)
     63     return node._replace(xtype=xtype).to_xpub()
     64 
     65 def get_billing_xpub():
     66     if constants.net.TESTNET:
     67         return "tpubD6NzVbkrYhZ4X11EJFTJujsYbUmVASAYY7gXsEt4sL97AMBdypiH1E9ZVTpdXXEy3Kj9Eqd1UkxdGtvDt5z23DKsh6211CfNJo8bLLyem5r"
     68     else:
     69         return "xpub6DTBdtBB8qUmH5c77v8qVGVoYk7WjJNpGvutqjLasNG1mbux6KsojaLrYf2sRhXAVU4NaFuHhbD9SvVPRt1MB1MaMooRuhHcAZH1yhQ1qDU"
     70 
     71 
     72 DISCLAIMER = [
     73     _("Two-factor authentication is a service provided by TrustedCoin.  "
     74       "It uses a multi-signature wallet, where you own 2 of 3 keys.  "
     75       "The third key is stored on a remote server that signs transactions on "
     76       "your behalf.  To use this service, you will need a smartphone with "
     77       "Google Authenticator installed."),
     78     _("A small fee will be charged on each transaction that uses the "
     79       "remote server.  You may check and modify your billing preferences "
     80       "once the installation is complete."),
     81     _("Note that your coins are not locked in this service.  You may withdraw "
     82       "your funds at any time and at no cost, without the remote server, by "
     83       "using the 'restore wallet' option with your wallet seed."),
     84     _("The next step will generate the seed of your wallet.  This seed will "
     85       "NOT be saved in your computer, and it must be stored on paper.  "
     86       "To be safe from malware, you may want to do this on an offline "
     87       "computer, and move your wallet later to an online computer."),
     88 ]
     89 
     90 KIVY_DISCLAIMER = [
     91     _("Two-factor authentication is a service provided by TrustedCoin. "
     92       "To use it, you must have a separate device with Google Authenticator."),
     93     _("This service uses a multi-signature wallet, where you own 2 of 3 keys.  "
     94       "The third key is stored on a remote server that signs transactions on "
     95       "your behalf. A small fee will be charged on each transaction that uses the "
     96       "remote server."),
     97     _("Note that your coins are not locked in this service.  You may withdraw "
     98       "your funds at any time and at no cost, without the remote server, by "
     99       "using the 'restore wallet' option with your wallet seed."),
    100 ]
    101 RESTORE_MSG = _("Enter the seed for your 2-factor wallet:")
    102 
    103 class TrustedCoinException(Exception):
    104     def __init__(self, message, status_code=0):
    105         Exception.__init__(self, message)
    106         self.status_code = status_code
    107 
    108 
    109 class ErrorConnectingServer(Exception):
    110     def __init__(self, reason: Union[str, Exception] = None):
    111         self.reason = reason
    112 
    113     def __str__(self):
    114         header = _("Error connecting to {} server").format('TrustedCoin')
    115         reason = self.reason
    116         if isinstance(reason, BaseException):
    117             reason = repr(reason)
    118         return f"{header}:\n{reason}" if reason else header
    119 
    120 
    121 class TrustedCoinCosignerClient(Logger):
    122     def __init__(self, user_agent=None, base_url='https://api.trustedcoin.com/2/'):
    123         self.base_url = base_url
    124         self.debug = False
    125         self.user_agent = user_agent
    126         Logger.__init__(self)
    127 
    128     async def handle_response(self, resp: ClientResponse):
    129         if resp.status != 200:
    130             try:
    131                 r = await resp.json()
    132                 message = r['message']
    133             except:
    134                 message = await resp.text()
    135             raise TrustedCoinException(message, resp.status)
    136         try:
    137             return await resp.json()
    138         except:
    139             return await resp.text()
    140 
    141     def send_request(self, method, relative_url, data=None, *, timeout=None):
    142         network = Network.get_instance()
    143         if not network:
    144             raise ErrorConnectingServer('You are offline.')
    145         url = urljoin(self.base_url, relative_url)
    146         if self.debug:
    147             self.logger.debug(f'<-- {method} {url} {data}')
    148         headers = {}
    149         if self.user_agent:
    150             headers['user-agent'] = self.user_agent
    151         try:
    152             if method == 'get':
    153                 response = Network.send_http_on_proxy(method, url,
    154                                                       params=data,
    155                                                       headers=headers,
    156                                                       on_finish=self.handle_response,
    157                                                       timeout=timeout)
    158             elif method == 'post':
    159                 response = Network.send_http_on_proxy(method, url,
    160                                                       json=data,
    161                                                       headers=headers,
    162                                                       on_finish=self.handle_response,
    163                                                       timeout=timeout)
    164             else:
    165                 assert False
    166         except TrustedCoinException:
    167             raise
    168         except Exception as e:
    169             raise ErrorConnectingServer(e)
    170         else:
    171             if self.debug:
    172                 self.logger.debug(f'--> {response}')
    173             return response
    174 
    175     def get_terms_of_service(self, billing_plan='electrum-per-tx-otp'):
    176         """
    177         Returns the TOS for the given billing plan as a plain/text unicode string.
    178         :param billing_plan: the plan to return the terms for
    179         """
    180         payload = {'billing_plan': billing_plan}
    181         return self.send_request('get', 'tos', payload)
    182 
    183     def create(self, xpubkey1, xpubkey2, email, billing_plan='electrum-per-tx-otp'):
    184         """
    185         Creates a new cosigner resource.
    186         :param xpubkey1: a bip32 extended public key (customarily the hot key)
    187         :param xpubkey2: a bip32 extended public key (customarily the cold key)
    188         :param email: a contact email
    189         :param billing_plan: the billing plan for the cosigner
    190         """
    191         payload = {
    192             'email': email,
    193             'xpubkey1': xpubkey1,
    194             'xpubkey2': xpubkey2,
    195             'billing_plan': billing_plan,
    196         }
    197         return self.send_request('post', 'cosigner', payload)
    198 
    199     def auth(self, id, otp):
    200         """
    201         Attempt to authenticate for a particular cosigner.
    202         :param id: the id of the cosigner
    203         :param otp: the one time password
    204         """
    205         payload = {'otp': otp}
    206         return self.send_request('post', 'cosigner/%s/auth' % quote(id), payload)
    207 
    208     def get(self, id):
    209         """ Get billing info """
    210         return self.send_request('get', 'cosigner/%s' % quote(id))
    211 
    212     def get_challenge(self, id):
    213         """ Get challenge to reset Google Auth secret """
    214         return self.send_request('get', 'cosigner/%s/otp_secret' % quote(id))
    215 
    216     def reset_auth(self, id, challenge, signatures):
    217         """ Reset Google Auth secret """
    218         payload = {'challenge':challenge, 'signatures':signatures}
    219         return self.send_request('post', 'cosigner/%s/otp_secret' % quote(id), payload)
    220 
    221     def sign(self, id, transaction, otp):
    222         """
    223         Attempt to authenticate for a particular cosigner.
    224         :param id: the id of the cosigner
    225         :param transaction: the hex encoded [partially signed] compact transaction to sign
    226         :param otp: the one time password
    227         """
    228         payload = {
    229             'otp': otp,
    230             'transaction': transaction
    231         }
    232         return self.send_request('post', 'cosigner/%s/sign' % quote(id), payload,
    233                                  timeout=60)
    234 
    235     def transfer_credit(self, id, recipient, otp, signature_callback):
    236         """
    237         Transfer a cosigner's credits to another cosigner.
    238         :param id: the id of the sending cosigner
    239         :param recipient: the id of the recipient cosigner
    240         :param otp: the one time password (of the sender)
    241         :param signature_callback: a callback that signs a text message using xpubkey1/0/0 returning a compact sig
    242         """
    243         payload = {
    244             'otp': otp,
    245             'recipient': recipient,
    246             'timestamp': int(time.time()),
    247 
    248         }
    249         relative_url = 'cosigner/%s/transfer' % quote(id)
    250         full_url = urljoin(self.base_url, relative_url)
    251         headers = {
    252             'x-signature': signature_callback(full_url + '\n' + json.dumps(payload))
    253         }
    254         return self.send_request('post', relative_url, payload, headers)
    255 
    256 
    257 server = TrustedCoinCosignerClient(user_agent="Electrum/" + version.ELECTRUM_VERSION)
    258 
    259 class Wallet_2fa(Multisig_Wallet):
    260 
    261     plugin: 'TrustedCoinPlugin'
    262 
    263     wallet_type = '2fa'
    264 
    265     def __init__(self, db, storage, *, config):
    266         self.m, self.n = 2, 3
    267         Deterministic_Wallet.__init__(self, db, storage, config=config)
    268         self.is_billing = False
    269         self.billing_info = None
    270         self._load_billing_addresses()
    271 
    272     def _load_billing_addresses(self):
    273         billing_addresses = {
    274             'legacy': self.db.get('trustedcoin_billing_addresses', {}),
    275             'segwit': self.db.get('trustedcoin_billing_addresses_segwit', {})
    276         }
    277         self._billing_addresses = {}  # type: Dict[str, Dict[int, str]]  # addr_type -> index -> addr
    278         self._billing_addresses_set = set()  # set of addrs
    279         for addr_type, d in list(billing_addresses.items()):
    280             self._billing_addresses[addr_type] = {}
    281             # convert keys from str to int
    282             for index, addr in d.items():
    283                 self._billing_addresses[addr_type][int(index)] = addr
    284                 self._billing_addresses_set.add(addr)
    285 
    286     def can_sign_without_server(self):
    287         return not self.keystores['x2/'].is_watching_only()
    288 
    289     def get_user_id(self):
    290         return get_user_id(self.db)
    291 
    292     def min_prepay(self):
    293         return min(self.price_per_tx.keys())
    294 
    295     def num_prepay(self):
    296         default = self.min_prepay()
    297         n = self.config.get('trustedcoin_prepay', default)
    298         if n not in self.price_per_tx:
    299             n = default
    300         return n
    301 
    302     def extra_fee(self):
    303         if self.can_sign_without_server():
    304             return 0
    305         if self.billing_info is None:
    306             self.plugin.start_request_thread(self)
    307             return 0
    308         if self.billing_info.get('tx_remaining'):
    309             return 0
    310         if self.is_billing:
    311             return 0
    312         n = self.num_prepay()
    313         price = int(self.price_per_tx[n])
    314         if price > 100000 * n:
    315             raise Exception('too high trustedcoin fee ({} for {} txns)'.format(price, n))
    316         return price
    317 
    318     def make_unsigned_transaction(self, *, coins: Sequence[PartialTxInput],
    319                                   outputs: List[PartialTxOutput], fee=None,
    320                                   change_addr: str = None, is_sweep=False) -> PartialTransaction:
    321         mk_tx = lambda o: Multisig_Wallet.make_unsigned_transaction(
    322             self, coins=coins, outputs=o, fee=fee, change_addr=change_addr)
    323         extra_fee = self.extra_fee() if not is_sweep else 0
    324         if extra_fee:
    325             address = self.billing_info['billing_address_segwit']
    326             fee_output = PartialTxOutput.from_address_and_value(address, extra_fee)
    327             try:
    328                 tx = mk_tx(outputs + [fee_output])
    329             except NotEnoughFunds:
    330                 # TrustedCoin won't charge if the total inputs is
    331                 # lower than their fee
    332                 tx = mk_tx(outputs)
    333                 if tx.input_value() >= extra_fee:
    334                     raise
    335                 self.logger.info("not charging for this tx")
    336         else:
    337             tx = mk_tx(outputs)
    338         return tx
    339 
    340     def on_otp(self, tx: PartialTransaction, otp):
    341         if not otp:
    342             self.logger.info("sign_transaction: no auth code")
    343             return
    344         otp = int(otp)
    345         long_user_id, short_id = self.get_user_id()
    346         raw_tx = tx.serialize_as_bytes().hex()
    347         assert raw_tx[:10] == "70736274ff", f"bad magic. {raw_tx[:10]}"
    348         try:
    349             r = server.sign(short_id, raw_tx, otp)
    350         except TrustedCoinException as e:
    351             if e.status_code == 400:  # invalid OTP
    352                 raise UserFacingException(_('Invalid one-time password.')) from e
    353             else:
    354                 raise
    355         if r:
    356             received_raw_tx = r.get('transaction')
    357             received_tx = Transaction(received_raw_tx)
    358             tx.combine_with_other_psbt(received_tx)
    359         self.logger.info(f"twofactor: is complete {tx.is_complete()}")
    360         # reset billing_info
    361         self.billing_info = None
    362         self.plugin.start_request_thread(self)
    363 
    364     def add_new_billing_address(self, billing_index: int, address: str, addr_type: str):
    365         billing_addresses_of_this_type = self._billing_addresses[addr_type]
    366         saved_addr = billing_addresses_of_this_type.get(billing_index)
    367         if saved_addr is not None:
    368             if saved_addr == address:
    369                 return  # already saved this address
    370             else:
    371                 raise Exception('trustedcoin billing address inconsistency.. '
    372                                 'for index {}, already saved {}, now got {}'
    373                                 .format(billing_index, saved_addr, address))
    374         # do we have all prior indices? (are we synced?)
    375         largest_index_we_have = max(billing_addresses_of_this_type) if billing_addresses_of_this_type else -1
    376         if largest_index_we_have + 1 < billing_index:  # need to sync
    377             for i in range(largest_index_we_have + 1, billing_index):
    378                 addr = make_billing_address(self, i, addr_type=addr_type)
    379                 billing_addresses_of_this_type[i] = addr
    380                 self._billing_addresses_set.add(addr)
    381         # save this address; and persist to disk
    382         billing_addresses_of_this_type[billing_index] = address
    383         self._billing_addresses_set.add(address)
    384         self._billing_addresses[addr_type] = billing_addresses_of_this_type
    385         self.db.put('trustedcoin_billing_addresses', self._billing_addresses['legacy'])
    386         self.db.put('trustedcoin_billing_addresses_segwit', self._billing_addresses['segwit'])
    387         # FIXME this often runs in a daemon thread, where storage.write will fail
    388         self.db.write(self.storage)
    389 
    390     def is_billing_address(self, addr: str) -> bool:
    391         return addr in self._billing_addresses_set
    392 
    393 
    394 # Utility functions
    395 
    396 def get_user_id(db):
    397     def make_long_id(xpub_hot, xpub_cold):
    398         return sha256(''.join(sorted([xpub_hot, xpub_cold])))
    399     xpub1 = db.get('x1/')['xpub']
    400     xpub2 = db.get('x2/')['xpub']
    401     long_id = make_long_id(xpub1, xpub2)
    402     short_id = hashlib.sha256(long_id).hexdigest()
    403     return long_id, short_id
    404 
    405 def make_xpub(xpub, s) -> str:
    406     rootnode = BIP32Node.from_xkey(xpub)
    407     child_pubkey, child_chaincode = bip32._CKD_pub(parent_pubkey=rootnode.eckey.get_public_key_bytes(compressed=True),
    408                                                    parent_chaincode=rootnode.chaincode,
    409                                                    child_index=s)
    410     child_node = BIP32Node(xtype=rootnode.xtype,
    411                            eckey=ecc.ECPubkey(child_pubkey),
    412                            chaincode=child_chaincode)
    413     return child_node.to_xpub()
    414 
    415 def make_billing_address(wallet, num, addr_type):
    416     long_id, short_id = wallet.get_user_id()
    417     xpub = make_xpub(get_billing_xpub(), long_id)
    418     usernode = BIP32Node.from_xkey(xpub)
    419     child_node = usernode.subkey_at_public_derivation([num])
    420     pubkey = child_node.eckey.get_public_key_bytes(compressed=True)
    421     if addr_type == 'legacy':
    422         return bitcoin.public_key_to_p2pkh(pubkey)
    423     elif addr_type == 'segwit':
    424         return bitcoin.public_key_to_p2wpkh(pubkey)
    425     else:
    426         raise ValueError(f'unexpected billing type: {addr_type}')
    427 
    428 
    429 class TrustedCoinPlugin(BasePlugin):
    430     wallet_class = Wallet_2fa
    431     disclaimer_msg = DISCLAIMER
    432 
    433     def __init__(self, parent, config, name):
    434         BasePlugin.__init__(self, parent, config, name)
    435         self.wallet_class.plugin = self
    436         self.requesting = False
    437 
    438     @staticmethod
    439     def is_valid_seed(seed):
    440         t = seed_type(seed)
    441         return is_any_2fa_seed_type(t)
    442 
    443     def is_available(self):
    444         return True
    445 
    446     def is_enabled(self):
    447         return True
    448 
    449     def can_user_disable(self):
    450         return False
    451 
    452     @hook
    453     def tc_sign_wrapper(self, wallet, tx, on_success, on_failure):
    454         if not isinstance(wallet, self.wallet_class):
    455             return
    456         if tx.is_complete():
    457             return
    458         if wallet.can_sign_without_server():
    459             return
    460         if not wallet.keystores['x3/'].can_sign(tx, ignore_watching_only=True):
    461             self.logger.info("twofactor: xpub3 not needed")
    462             return
    463         def wrapper(tx):
    464             assert tx
    465             self.prompt_user_for_otp(wallet, tx, on_success, on_failure)
    466         return wrapper
    467 
    468     def prompt_user_for_otp(self, wallet, tx, on_success, on_failure) -> None:
    469         raise NotImplementedError()
    470 
    471     @hook
    472     def get_tx_extra_fee(self, wallet, tx: Transaction):
    473         if type(wallet) != Wallet_2fa:
    474             return
    475         for o in tx.outputs():
    476             if wallet.is_billing_address(o.address):
    477                 return o.address, o.value
    478 
    479     def finish_requesting(func):
    480         def f(self, *args, **kwargs):
    481             try:
    482                 return func(self, *args, **kwargs)
    483             finally:
    484                 self.requesting = False
    485         return f
    486 
    487     @finish_requesting
    488     def request_billing_info(self, wallet: 'Wallet_2fa', *, suppress_connection_error=True):
    489         if wallet.can_sign_without_server():
    490             return
    491         self.logger.info("request billing info")
    492         try:
    493             billing_info = server.get(wallet.get_user_id()[1])
    494         except ErrorConnectingServer as e:
    495             if suppress_connection_error:
    496                 self.logger.info(repr(e))
    497                 return
    498             raise
    499         billing_index = billing_info['billing_index']
    500         # add segwit billing address; this will be used for actual billing
    501         billing_address = make_billing_address(wallet, billing_index, addr_type='segwit')
    502         if billing_address != billing_info['billing_address_segwit']:
    503             raise Exception(f'unexpected trustedcoin billing address: '
    504                             f'calculated {billing_address}, received {billing_info["billing_address_segwit"]}')
    505         wallet.add_new_billing_address(billing_index, billing_address, addr_type='segwit')
    506         # also add legacy billing address; only used for detecting past payments in GUI
    507         billing_address = make_billing_address(wallet, billing_index, addr_type='legacy')
    508         wallet.add_new_billing_address(billing_index, billing_address, addr_type='legacy')
    509 
    510         wallet.billing_info = billing_info
    511         wallet.price_per_tx = dict(billing_info['price_per_tx'])
    512         wallet.price_per_tx.pop(1, None)
    513         return True
    514 
    515     def start_request_thread(self, wallet):
    516         from threading import Thread
    517         if self.requesting is False:
    518             self.requesting = True
    519             t = Thread(target=self.request_billing_info, args=(wallet,))
    520             t.setDaemon(True)
    521             t.start()
    522             return t
    523 
    524     def make_seed(self, seed_type):
    525         if not is_any_2fa_seed_type(seed_type):
    526             raise Exception(f'unexpected seed type: {seed_type}')
    527         return Mnemonic('english').make_seed(seed_type=seed_type)
    528 
    529     @hook
    530     def do_clear(self, window):
    531         window.wallet.is_billing = False
    532 
    533     def show_disclaimer(self, wizard: BaseWizard):
    534         wizard.set_icon('trustedcoin-wizard.png')
    535         wizard.reset_stack()
    536         wizard.confirm_dialog(title='Disclaimer', message='\n\n'.join(self.disclaimer_msg), run_next = lambda x: wizard.run('choose_seed'))
    537 
    538     def choose_seed(self, wizard):
    539         title = _('Create or restore')
    540         message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?')
    541         choices = [
    542             ('choose_seed_type', _('Create a new seed')),
    543             ('restore_wallet', _('I already have a seed')),
    544         ]
    545         wizard.choice_dialog(title=title, message=message, choices=choices, run_next=wizard.run)
    546 
    547     def choose_seed_type(self, wizard):
    548         seed_type = '2fa' if self.config.get('nosegwit') else '2fa_segwit'
    549         self.create_seed(wizard, seed_type)
    550 
    551     def create_seed(self, wizard, seed_type):
    552         seed = self.make_seed(seed_type)
    553         f = lambda x: wizard.request_passphrase(seed, x)
    554         wizard.show_seed_dialog(run_next=f, seed_text=seed)
    555 
    556     @classmethod
    557     def get_xkeys(self, seed, t, passphrase, derivation):
    558         assert is_any_2fa_seed_type(t)
    559         xtype = 'standard' if t == '2fa' else 'p2wsh'
    560         bip32_seed = Mnemonic.mnemonic_to_seed(seed, passphrase)
    561         rootnode = BIP32Node.from_rootseed(bip32_seed, xtype=xtype)
    562         child_node = rootnode.subkey_at_private_derivation(derivation)
    563         return child_node.to_xprv(), child_node.to_xpub()
    564 
    565     @classmethod
    566     def xkeys_from_seed(self, seed, passphrase):
    567         t = seed_type(seed)
    568         if not is_any_2fa_seed_type(t):
    569             raise Exception(f'unexpected seed type: {t}')
    570         words = seed.split()
    571         n = len(words)
    572         if t == '2fa':
    573             if n >= 20:  # old scheme
    574                 # note: pre-2.7 2fa seeds were typically 24-25 words, however they
    575                 # could probabilistically be arbitrarily shorter due to a bug. (see #3611)
    576                 # the probability of it being < 20 words is about 2^(-(256+12-19*11)) = 2^(-59)
    577                 if passphrase != '':
    578                     raise Exception('old 2fa seed cannot have passphrase')
    579                 xprv1, xpub1 = self.get_xkeys(' '.join(words[0:12]), t, '', "m/")
    580                 xprv2, xpub2 = self.get_xkeys(' '.join(words[12:]), t, '', "m/")
    581             elif n == 12:  # new scheme
    582                 xprv1, xpub1 = self.get_xkeys(seed, t, passphrase, "m/0'/")
    583                 xprv2, xpub2 = self.get_xkeys(seed, t, passphrase, "m/1'/")
    584             else:
    585                 raise Exception(f'unrecognized seed length for "2fa" seed: {n}')
    586         elif t == '2fa_segwit':
    587             xprv1, xpub1 = self.get_xkeys(seed, t, passphrase, "m/0'/")
    588             xprv2, xpub2 = self.get_xkeys(seed, t, passphrase, "m/1'/")
    589         else:
    590             raise Exception(f'unexpected seed type: {t}')
    591         return xprv1, xpub1, xprv2, xpub2
    592 
    593     def create_keystore(self, wizard, seed, passphrase):
    594         # this overloads the wizard's method
    595         xprv1, xpub1, xprv2, xpub2 = self.xkeys_from_seed(seed, passphrase)
    596         k1 = keystore.from_xprv(xprv1)
    597         k2 = keystore.from_xpub(xpub2)
    598         wizard.request_password(run_next=lambda pw, encrypt: self.on_password(wizard, pw, encrypt, k1, k2))
    599 
    600     def on_password(self, wizard, password, encrypt_storage, k1, k2):
    601         k1.update_password(None, password)
    602         wizard.data['x1/'] = k1.dump()
    603         wizard.data['x2/'] = k2.dump()
    604         wizard.pw_args = WizardWalletPasswordSetting(password=password,
    605                                                      encrypt_storage=encrypt_storage,
    606                                                      storage_enc_version=StorageEncryptionVersion.USER_PASSWORD,
    607                                                      encrypt_keystore=bool(password))
    608         self.go_online_dialog(wizard)
    609 
    610     def restore_wallet(self, wizard):
    611         wizard.opt_bip39 = False
    612         wizard.opt_ext = True
    613         title = _("Restore two-factor Wallet")
    614         f = lambda seed, is_bip39, is_ext: wizard.run('on_restore_seed', seed, is_ext)
    615         wizard.restore_seed_dialog(run_next=f, test=self.is_valid_seed)
    616 
    617     def on_restore_seed(self, wizard, seed, is_ext):
    618         f = lambda x: self.restore_choice(wizard, seed, x)
    619         wizard.passphrase_dialog(run_next=f) if is_ext else f('')
    620 
    621     def restore_choice(self, wizard: BaseWizard, seed, passphrase):
    622         wizard.set_icon('trustedcoin-wizard.png')
    623         wizard.reset_stack()
    624         title = _('Restore 2FA wallet')
    625         msg = ' '.join([
    626             'You are going to restore a wallet protected with two-factor authentication.',
    627             'Do you want to keep using two-factor authentication with this wallet,',
    628             'or do you want to disable it, and have two master private keys in your wallet?'
    629         ])
    630         choices = [('keep', 'Keep'), ('disable', 'Disable')]
    631         f = lambda x: self.on_choice(wizard, seed, passphrase, x)
    632         wizard.choice_dialog(choices=choices, message=msg, title=title, run_next=f)
    633 
    634     def on_choice(self, wizard, seed, passphrase, x):
    635         if x == 'disable':
    636             f = lambda pw, encrypt: wizard.run('on_restore_pw', seed, passphrase, pw, encrypt)
    637             wizard.request_password(run_next=f)
    638         else:
    639             self.create_keystore(wizard, seed, passphrase)
    640 
    641     def on_restore_pw(self, wizard, seed, passphrase, password, encrypt_storage):
    642         xprv1, xpub1, xprv2, xpub2 = self.xkeys_from_seed(seed, passphrase)
    643         k1 = keystore.from_xprv(xprv1)
    644         k2 = keystore.from_xprv(xprv2)
    645         k1.add_seed(seed)
    646         k1.update_password(None, password)
    647         k2.update_password(None, password)
    648         wizard.data['x1/'] = k1.dump()
    649         wizard.data['x2/'] = k2.dump()
    650         long_user_id, short_id = get_user_id(wizard.data)
    651         xtype = xpub_type(xpub1)
    652         xpub3 = make_xpub(get_signing_xpub(xtype), long_user_id)
    653         k3 = keystore.from_xpub(xpub3)
    654         wizard.data['x3/'] = k3.dump()
    655         wizard.pw_args = WizardWalletPasswordSetting(password=password,
    656                                                      encrypt_storage=encrypt_storage,
    657                                                      storage_enc_version=StorageEncryptionVersion.USER_PASSWORD,
    658                                                      encrypt_keystore=bool(password))
    659         wizard.terminate()
    660 
    661     def create_remote_key(self, email, wizard):
    662         xpub1 = wizard.data['x1/']['xpub']
    663         xpub2 = wizard.data['x2/']['xpub']
    664         # Generate third key deterministically.
    665         long_user_id, short_id = get_user_id(wizard.data)
    666         xtype = xpub_type(xpub1)
    667         xpub3 = make_xpub(get_signing_xpub(xtype), long_user_id)
    668         # secret must be sent by the server
    669         try:
    670             r = server.create(xpub1, xpub2, email)
    671         except (socket.error, ErrorConnectingServer):
    672             wizard.show_message('Server not reachable, aborting')
    673             wizard.terminate(aborted=True)
    674             return
    675         except TrustedCoinException as e:
    676             if e.status_code == 409:
    677                 r = None
    678             else:
    679                 wizard.show_message(str(e))
    680                 return
    681         if r is None:
    682             otp_secret = None
    683         else:
    684             otp_secret = r.get('otp_secret')
    685             if not otp_secret:
    686                 wizard.show_message(_('Error'))
    687                 return
    688             _xpub3 = r['xpubkey_cosigner']
    689             _id = r['id']
    690             if short_id != _id:
    691                 wizard.show_message("unexpected trustedcoin short_id: expected {}, received {}"
    692                                     .format(short_id, _id))
    693                 return
    694             if xpub3 != _xpub3:
    695                 wizard.show_message("unexpected trustedcoin xpub3: expected {}, received {}"
    696                                     .format(xpub3, _xpub3))
    697                 return
    698         self.request_otp_dialog(wizard, short_id, otp_secret, xpub3)
    699 
    700     def check_otp(self, wizard, short_id, otp_secret, xpub3, otp, reset):
    701         if otp:
    702             self.do_auth(wizard, short_id, otp, xpub3)
    703         elif reset:
    704             wizard.opt_bip39 = False
    705             wizard.opt_ext = True
    706             f = lambda seed, is_bip39, is_ext: wizard.run('on_reset_seed', short_id, seed, is_ext, xpub3)
    707             wizard.restore_seed_dialog(run_next=f, test=self.is_valid_seed)
    708 
    709     def on_reset_seed(self, wizard, short_id, seed, is_ext, xpub3):
    710         f = lambda passphrase: wizard.run('on_reset_auth', short_id, seed, passphrase, xpub3)
    711         wizard.passphrase_dialog(run_next=f) if is_ext else f('')
    712 
    713     def do_auth(self, wizard, short_id, otp, xpub3):
    714         try:
    715             server.auth(short_id, otp)
    716         except TrustedCoinException as e:
    717             if e.status_code == 400:  # invalid OTP
    718                 wizard.show_message(_('Invalid one-time password.'))
    719                 # ask again for otp
    720                 self.request_otp_dialog(wizard, short_id, None, xpub3)
    721             else:
    722                 wizard.show_message(str(e))
    723                 wizard.terminate(aborted=True)
    724         except Exception as e:
    725             wizard.show_message(repr(e))
    726             wizard.terminate(aborted=True)
    727         else:
    728             k3 = keystore.from_xpub(xpub3)
    729             wizard.data['x3/'] = k3.dump()
    730             wizard.data['use_trustedcoin'] = True
    731             wizard.terminate()
    732 
    733     def on_reset_auth(self, wizard, short_id, seed, passphrase, xpub3):
    734         xprv1, xpub1, xprv2, xpub2 = self.xkeys_from_seed(seed, passphrase)
    735         if (wizard.data['x1/']['xpub'] != xpub1 or
    736                 wizard.data['x2/']['xpub'] != xpub2):
    737             wizard.show_message(_('Incorrect seed'))
    738             return
    739         r = server.get_challenge(short_id)
    740         challenge = r.get('challenge')
    741         message = 'TRUSTEDCOIN CHALLENGE: ' + challenge
    742         def f(xprv):
    743             rootnode = BIP32Node.from_xkey(xprv)
    744             key = rootnode.subkey_at_private_derivation((0, 0)).eckey
    745             sig = key.sign_message(message, True)
    746             return base64.b64encode(sig).decode()
    747 
    748         signatures = [f(x) for x in [xprv1, xprv2]]
    749         r = server.reset_auth(short_id, challenge, signatures)
    750         new_secret = r.get('otp_secret')
    751         if not new_secret:
    752             wizard.show_message(_('Request rejected by server'))
    753             return
    754         self.request_otp_dialog(wizard, short_id, new_secret, xpub3)
    755 
    756     @hook
    757     def get_action(self, db):
    758         if db.get('wallet_type') != '2fa':
    759             return
    760         if not db.get('x1/'):
    761             return self, 'show_disclaimer'
    762         if not db.get('x2/'):
    763             return self, 'show_disclaimer'
    764         if not db.get('x3/'):
    765             return self, 'accept_terms_of_use'