|
|
|
@ -34,9 +34,9 @@ from urllib.parse import quote |
|
|
|
from aiohttp import ClientResponse |
|
|
|
from aiohttp import ClientResponse |
|
|
|
|
|
|
|
|
|
|
|
from electrum import ecc, constants, keystore, version, bip32 |
|
|
|
from electrum import ecc, constants, keystore, version, bip32 |
|
|
|
from electrum.bitcoin import TYPE_ADDRESS, is_new_seed, public_key_to_p2pkh |
|
|
|
from electrum.bitcoin import TYPE_ADDRESS, is_new_seed, public_key_to_p2pkh, seed_type, is_any_2fa_seed_type |
|
|
|
from electrum.bip32 import (deserialize_xpub, deserialize_xprv, bip32_private_key, CKD_pub, |
|
|
|
from electrum.bip32 import (deserialize_xpub, deserialize_xprv, bip32_private_key, CKD_pub, |
|
|
|
serialize_xpub, bip32_root, bip32_private_derivation) |
|
|
|
serialize_xpub, bip32_root, bip32_private_derivation, xpub_type) |
|
|
|
from electrum.crypto import sha256 |
|
|
|
from electrum.crypto import sha256 |
|
|
|
from electrum.transaction import TxOutput |
|
|
|
from electrum.transaction import TxOutput |
|
|
|
from electrum.mnemonic import Mnemonic |
|
|
|
from electrum.mnemonic import Mnemonic |
|
|
|
@ -47,12 +47,20 @@ from electrum.util import NotEnoughFunds |
|
|
|
from electrum.storage import STO_EV_USER_PW |
|
|
|
from electrum.storage import STO_EV_USER_PW |
|
|
|
from electrum.network import Network |
|
|
|
from electrum.network import Network |
|
|
|
|
|
|
|
|
|
|
|
# signing_xpub is hardcoded so that the wallet can be restored from seed, without TrustedCoin's server |
|
|
|
def get_signing_xpub(xtype): |
|
|
|
def get_signing_xpub(): |
|
|
|
if xtype == 'standard': |
|
|
|
if constants.net.TESTNET: |
|
|
|
if constants.net.TESTNET: |
|
|
|
return "tpubD6NzVbkrYhZ4XdmyJQcCPjQfg6RXVUzGFhPjZ7uvRC8JLcS7Hw1i7UTpyhp9grHpak4TyK2hzBJrujDVLXQ6qB5tNpVx9rC6ixijUXadnmY" |
|
|
|
return "tpubD6NzVbkrYhZ4XdmyJQcCPjQfg6RXVUzGFhPjZ7uvRC8JLcS7Hw1i7UTpyhp9grHpak4TyK2hzBJrujDVLXQ6qB5tNpVx9rC6ixijUXadnmY" |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
return "xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL" |
|
|
|
|
|
|
|
elif xtype == 'p2wsh': |
|
|
|
|
|
|
|
# TODO these are temp xpubs |
|
|
|
|
|
|
|
if constants.net.TESTNET: |
|
|
|
|
|
|
|
return "Vpub5fcdcgEwTJmbmqAktuK8Kyq92fMf7sWkcP6oqAii2tG47dNbfkGEGUbfS9NuZaRywLkHE6EmUksrqo32ZL3ouLN1HTar6oRiHpDzKMAF1tf" |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
return "Zpub6xwgqLvc42wXB1wEELTdALD9iXwStMUkGqBgxkJFYumaL2dWgNvUkjEDWyDFZD3fZuDWDzd1KQJ4NwVHS7hs6H6QkpNYSShfNiUZsgMdtNg" |
|
|
|
else: |
|
|
|
else: |
|
|
|
return "xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL" |
|
|
|
raise NotImplementedError('xtype: {}'.format(xtype)) |
|
|
|
|
|
|
|
|
|
|
|
def get_billing_xpub(): |
|
|
|
def get_billing_xpub(): |
|
|
|
if constants.net.TESTNET: |
|
|
|
if constants.net.TESTNET: |
|
|
|
@ -60,7 +68,6 @@ def get_billing_xpub(): |
|
|
|
else: |
|
|
|
else: |
|
|
|
return "xpub6DTBdtBB8qUmH5c77v8qVGVoYk7WjJNpGvutqjLasNG1mbux6KsojaLrYf2sRhXAVU4NaFuHhbD9SvVPRt1MB1MaMooRuhHcAZH1yhQ1qDU" |
|
|
|
return "xpub6DTBdtBB8qUmH5c77v8qVGVoYk7WjJNpGvutqjLasNG1mbux6KsojaLrYf2sRhXAVU4NaFuHhbD9SvVPRt1MB1MaMooRuhHcAZH1yhQ1qDU" |
|
|
|
|
|
|
|
|
|
|
|
SEED_PREFIX = version.SEED_PREFIX_2FA |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DISCLAIMER = [ |
|
|
|
DISCLAIMER = [ |
|
|
|
_("Two-factor authentication is a service provided by TrustedCoin. " |
|
|
|
_("Two-factor authentication is a service provided by TrustedCoin. " |
|
|
|
@ -377,7 +384,8 @@ class TrustedCoinPlugin(BasePlugin): |
|
|
|
|
|
|
|
|
|
|
|
@staticmethod |
|
|
|
@staticmethod |
|
|
|
def is_valid_seed(seed): |
|
|
|
def is_valid_seed(seed): |
|
|
|
return is_new_seed(seed, SEED_PREFIX) |
|
|
|
t = seed_type(seed) |
|
|
|
|
|
|
|
return is_any_2fa_seed_type(t) |
|
|
|
|
|
|
|
|
|
|
|
def is_available(self): |
|
|
|
def is_available(self): |
|
|
|
return True |
|
|
|
return True |
|
|
|
@ -449,8 +457,10 @@ class TrustedCoinPlugin(BasePlugin): |
|
|
|
t.start() |
|
|
|
t.start() |
|
|
|
return t |
|
|
|
return t |
|
|
|
|
|
|
|
|
|
|
|
def make_seed(self): |
|
|
|
def make_seed(self, seed_type): |
|
|
|
return Mnemonic('english').make_seed(seed_type='2fa', num_bits=128) |
|
|
|
if not is_any_2fa_seed_type(seed_type): |
|
|
|
|
|
|
|
raise BaseException('unexpected seed type: {}'.format(seed_type)) |
|
|
|
|
|
|
|
return Mnemonic('english').make_seed(seed_type=seed_type, num_bits=128) |
|
|
|
|
|
|
|
|
|
|
|
@hook |
|
|
|
@hook |
|
|
|
def do_clear(self, window): |
|
|
|
def do_clear(self, window): |
|
|
|
@ -465,25 +475,41 @@ class TrustedCoinPlugin(BasePlugin): |
|
|
|
title = _('Create or restore') |
|
|
|
title = _('Create or restore') |
|
|
|
message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?') |
|
|
|
message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?') |
|
|
|
choices = [ |
|
|
|
choices = [ |
|
|
|
('create_seed', _('Create a new seed')), |
|
|
|
('choose_seed_type', _('Create a new seed')), |
|
|
|
('restore_wallet', _('I already have a seed')), |
|
|
|
('restore_wallet', _('I already have a seed')), |
|
|
|
] |
|
|
|
] |
|
|
|
wizard.choice_dialog(title=title, message=message, choices=choices, run_next=wizard.run) |
|
|
|
wizard.choice_dialog(title=title, message=message, choices=choices, run_next=wizard.run) |
|
|
|
|
|
|
|
|
|
|
|
def create_seed(self, wizard): |
|
|
|
def choose_seed_type(self, wizard): |
|
|
|
seed = self.make_seed() |
|
|
|
choices = [ |
|
|
|
|
|
|
|
('create_2fa_seed', _('Standard 2FA')), |
|
|
|
|
|
|
|
('create_2fa_segwit_seed', _('Segwit 2FA')), |
|
|
|
|
|
|
|
] |
|
|
|
|
|
|
|
wizard.choose_seed_type(choices=choices) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_2fa_seed(self, wizard): self.create_seed(wizard, '2fa') |
|
|
|
|
|
|
|
def create_2fa_segwit_seed(self, wizard): self.create_seed(wizard, '2fa_segwit') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_seed(self, wizard, seed_type): |
|
|
|
|
|
|
|
seed = self.make_seed(seed_type) |
|
|
|
f = lambda x: wizard.request_passphrase(seed, x) |
|
|
|
f = lambda x: wizard.request_passphrase(seed, x) |
|
|
|
wizard.show_seed_dialog(run_next=f, seed_text=seed) |
|
|
|
wizard.show_seed_dialog(run_next=f, seed_text=seed) |
|
|
|
|
|
|
|
|
|
|
|
@classmethod |
|
|
|
@classmethod |
|
|
|
def get_xkeys(self, seed, passphrase, derivation): |
|
|
|
def get_xkeys(self, seed, passphrase, derivation): |
|
|
|
|
|
|
|
t = seed_type(seed) |
|
|
|
|
|
|
|
assert is_any_2fa_seed_type(t) |
|
|
|
|
|
|
|
xtype = 'standard' if t == '2fa' else 'p2wsh' |
|
|
|
bip32_seed = Mnemonic.mnemonic_to_seed(seed, passphrase) |
|
|
|
bip32_seed = Mnemonic.mnemonic_to_seed(seed, passphrase) |
|
|
|
xprv, xpub = bip32_root(bip32_seed, 'standard') |
|
|
|
xprv, xpub = bip32_root(bip32_seed, xtype) |
|
|
|
xprv, xpub = bip32_private_derivation(xprv, "m/", derivation) |
|
|
|
xprv, xpub = bip32_private_derivation(xprv, "m/", derivation) |
|
|
|
return xprv, xpub |
|
|
|
return xprv, xpub |
|
|
|
|
|
|
|
|
|
|
|
@classmethod |
|
|
|
@classmethod |
|
|
|
def xkeys_from_seed(self, seed, passphrase): |
|
|
|
def xkeys_from_seed(self, seed, passphrase): |
|
|
|
|
|
|
|
t = seed_type(seed) |
|
|
|
|
|
|
|
if not is_any_2fa_seed_type(t): |
|
|
|
|
|
|
|
raise BaseException('unexpected seed type: {}'.format(t)) |
|
|
|
words = seed.split() |
|
|
|
words = seed.split() |
|
|
|
n = len(words) |
|
|
|
n = len(words) |
|
|
|
# old version use long seed phrases |
|
|
|
# old version use long seed phrases |
|
|
|
@ -495,7 +521,7 @@ class TrustedCoinPlugin(BasePlugin): |
|
|
|
raise Exception('old 2fa seed cannot have passphrase') |
|
|
|
raise Exception('old 2fa seed cannot have passphrase') |
|
|
|
xprv1, xpub1 = self.get_xkeys(' '.join(words[0:12]), '', "m/") |
|
|
|
xprv1, xpub1 = self.get_xkeys(' '.join(words[0:12]), '', "m/") |
|
|
|
xprv2, xpub2 = self.get_xkeys(' '.join(words[12:]), '', "m/") |
|
|
|
xprv2, xpub2 = self.get_xkeys(' '.join(words[12:]), '', "m/") |
|
|
|
elif n==12: |
|
|
|
elif not t == '2fa' or n == 12: |
|
|
|
xprv1, xpub1 = self.get_xkeys(seed, passphrase, "m/0'/") |
|
|
|
xprv1, xpub1 = self.get_xkeys(seed, passphrase, "m/0'/") |
|
|
|
xprv2, xpub2 = self.get_xkeys(seed, passphrase, "m/1'/") |
|
|
|
xprv2, xpub2 = self.get_xkeys(seed, passphrase, "m/1'/") |
|
|
|
else: |
|
|
|
else: |
|
|
|
@ -561,7 +587,8 @@ class TrustedCoinPlugin(BasePlugin): |
|
|
|
storage.put('x1/', k1.dump()) |
|
|
|
storage.put('x1/', k1.dump()) |
|
|
|
storage.put('x2/', k2.dump()) |
|
|
|
storage.put('x2/', k2.dump()) |
|
|
|
long_user_id, short_id = get_user_id(storage) |
|
|
|
long_user_id, short_id = get_user_id(storage) |
|
|
|
xpub3 = make_xpub(get_signing_xpub(), long_user_id) |
|
|
|
xtype = xpub_type(xpub1) |
|
|
|
|
|
|
|
xpub3 = make_xpub(get_signing_xpub(xtype), long_user_id) |
|
|
|
k3 = keystore.from_xpub(xpub3) |
|
|
|
k3 = keystore.from_xpub(xpub3) |
|
|
|
storage.put('x3/', k3.dump()) |
|
|
|
storage.put('x3/', k3.dump()) |
|
|
|
|
|
|
|
|
|
|
|
@ -578,7 +605,8 @@ class TrustedCoinPlugin(BasePlugin): |
|
|
|
xpub2 = wizard.storage.get('x2/')['xpub'] |
|
|
|
xpub2 = wizard.storage.get('x2/')['xpub'] |
|
|
|
# Generate third key deterministically. |
|
|
|
# Generate third key deterministically. |
|
|
|
long_user_id, short_id = get_user_id(wizard.storage) |
|
|
|
long_user_id, short_id = get_user_id(wizard.storage) |
|
|
|
xpub3 = make_xpub(get_signing_xpub(), long_user_id) |
|
|
|
xtype = xpub_type(xpub1) |
|
|
|
|
|
|
|
xpub3 = make_xpub(get_signing_xpub(xtype), long_user_id) |
|
|
|
# secret must be sent by the server |
|
|
|
# secret must be sent by the server |
|
|
|
try: |
|
|
|
try: |
|
|
|
r = server.create(xpub1, xpub2, email) |
|
|
|
r = server.create(xpub1, xpub2, email) |
|
|
|
|