URI: 
       tTrustedcoin: add Google Authenticator reset - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 730cbefeb15aa708b955d12088ce3b26d603c55f
   DIR parent dfef56491b7af5ded0b646f145a84a5b2fbe909d
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Sat,  1 Oct 2016 11:45:43 +0200
       
       Trustedcoin: add Google Authenticator reset
       
       Diffstat:
         M plugins/trustedcoin/qt.py           |      32 ++++++++++++++++----------------
         M plugins/trustedcoin/trustedcoin.py  |      70 +++++++++++++++++++++++++++----
       
       2 files changed, 77 insertions(+), 25 deletions(-)
       ---
   DIR diff --git a/plugins/trustedcoin/qt.py b/plugins/trustedcoin/qt.py
       t@@ -237,11 +237,9 @@ class Plugin(TrustedCoinPlugin):
        
                window.set_main_layout(vbox, next_enabled=False)
                next_button.setText(prior_button_text)
       -
                return str(email_e.text())
        
       -
       -    def setup_google_auth(self, window, _id, otp_secret):
       +    def request_otp_dialog(self, window, _id, otp_secret):
                vbox = QVBoxLayout()
                if otp_secret is not None:
                    uri = "otpauth://totp/%s?secret=%s"%('trustedcoin.com', otp_secret)
       t@@ -255,7 +253,7 @@ class Plugin(TrustedCoinPlugin):
                    label = QLabel(
                        "This wallet is already registered with Trustedcoin. "
                        "To finalize wallet creation, please enter your Google Authenticator Code. "
       -                "If you do not have this code, delete the wallet file and start a new registration")
       +            )
                    label.setWordWrap(1)
                    vbox.addWidget(label)
                    msg = _('Google Authenticator code:')
       t@@ -268,18 +266,20 @@ class Plugin(TrustedCoinPlugin):
                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():
       -            window.next_button.setEnabled(len(pw.text()) == 6)
       +            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.set_main_layout(vbox, next_enabled=False,
       +                               raise_on_cancel=False)
       +        return pw.get_amount(), cb_lost.isChecked()
       +
        
       -        while True:
       -            if not window.set_main_layout(vbox, next_enabled=False,
       -                                          raise_on_cancel=False):
       -                return False
       -            otp = pw.get_amount()
       -            try:
       -                server.auth(_id, otp)
       -                return True
       -            except:
       -                window.show_message(_('Incorrect password'))
       -                pw.setText('')
   DIR diff --git a/plugins/trustedcoin/trustedcoin.py b/plugins/trustedcoin/trustedcoin.py
       t@@ -74,9 +74,9 @@ class TrustedCoinException(Exception):
                self.status_code = status_code
        
        class TrustedCoinCosignerClient(object):
       -    def __init__(self, user_agent=None, base_url='https://api.trustedcoin.com/2/', debug=False):
       +    def __init__(self, user_agent=None, base_url='https://api.trustedcoin.com/2/'):
                self.base_url = base_url
       -        self.debug = debug
       +        self.debug = False
                self.user_agent = user_agent
        
            def send_request(self, method, relative_url, data=None):
       t@@ -141,13 +141,18 @@ class TrustedCoinCosignerClient(object):
                return self.send_request('post', 'cosigner/%s/auth' % quote(id), payload)
        
            def get(self, id):
       -        """
       -        Attempt to authenticate for a particular cosigner.
       -        :param id: the id of the cosigner
       -        :param otp: the one time password
       -        """
       +        """ Get billing info """
                return self.send_request('get', 'cosigner/%s' % quote(id))
        
       +    def get_challenge(self, id):
       +        """ Get challenge to reset Google Auth secret """
       +        return self.send_request('get', 'cosigner/%s/otp_secret' % quote(id))
       +
       +    def reset_auth(self, id, challenge, signatures):
       +        """ Reset Google Auth secret """
       +        payload = {'challenge':challenge, 'signatures':signatures}
       +        return self.send_request('post', 'cosigner/%s/otp_secret' % quote(id), payload)
       +
            def sign(self, id, transaction, otp):
                """
                Attempt to authenticate for a particular cosigner.
       t@@ -478,8 +483,27 @@ class TrustedCoinPlugin(BasePlugin):
                    except Exception as e:
                        wizard.show_message(str(e))
                        return
       -        if not self.setup_google_auth(wizard, short_id, otp_secret):
       -            wizard.show_message("otp error")
       +        self.check_otp(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)
       +        if otp:
       +            self.do_auth(wizard, short_id, otp, xpub3)
       +        elif reset:
       +            wizard.opt_bip39 = False
       +            wizard.opt_ext = True
       +            f = lambda seed, is_bip39, is_ext: wizard.run('on_reset_seed', short_id, seed, is_ext, xpub3)
       +            wizard.restore_seed_dialog(run_next=f, test=self.is_valid_seed)
       +
       +    def on_reset_seed(self, wizard, short_id, seed, is_ext, xpub3):
       +        f = lambda passphrase: wizard.run('on_reset_auth', short_id, seed, passphrase, xpub3)
       +        wizard.passphrase_dialog(run_next=f) if is_ext else f('')
       +
       +    def do_auth(self, wizard, short_id, otp, xpub3):
       +        try:
       +            server.auth(short_id, otp)
       +        except:
       +            wizard.show_message(_('Incorrect password'))
                    return
                k3 = keystore.from_xpub(xpub3)
                wizard.storage.put('x3/', k3.dump())
       t@@ -488,6 +512,34 @@ class TrustedCoinPlugin(BasePlugin):
                wizard.wallet = Wallet_2fa(wizard.storage)
                wizard.run('create_addresses')
        
       +    def on_reset_auth(self, wizard, short_id, seed, passphrase, xpub3):
       +        xprv1, xpub1, xprv2, xpub2 = self.xkeys_from_seed(seed, passphrase)
       +        try:
       +            assert xpub1 == wizard.storage.get('x1/')['xpub']
       +            assert xpub2 == wizard.storage.get('x2/')['xpub']
       +        except:
       +            wizard.show_message(_('Incorrect seed'))
       +            return
       +        r = server.get_challenge(short_id)
       +        challenge = r.get('challenge')
       +        message = 'TRUSTEDCOIN CHALLENGE: ' + challenge
       +        def f(xprv):
       +            from electrum.bitcoin import deserialize_xkey, bip32_private_key, regenerate_key, is_compressed
       +            _, _, _, c, k = deserialize_xkey(xprv)
       +            pk = bip32_private_key([0, 0], k, c)
       +            key = regenerate_key(pk)
       +            compressed = is_compressed(pk)
       +            sig = key.sign_message(message, compressed)
       +            return base64.b64encode(sig)
       +
       +        signatures = [f(x) for x in [xprv1, xprv2]]
       +        r = server.reset_auth(short_id, challenge, signatures)
       +        new_secret = r.get('otp_secret')
       +        if not new_secret:
       +            wizard.show_message(_('Request rejected by server'))
       +            return
       +        self.check_otp(wizard, short_id, new_secret, xpub3)
       +
            @hook
            def get_action(self, storage):
                if storage.get('wallet_type') != '2fa':