Browse Source

Merge pull request #4880 from spesmilo/2fa_segwit

2fa segwit (from ghost43's PR)
3.3.3.1
ThomasV 6 years ago
committed by GitHub
parent
commit
dd848304e6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 26
      electrum/base_wizard.py
  2. 6
      electrum/bitcoin.py
  3. 12
      electrum/plugins/trustedcoin/qt.py
  4. 140
      electrum/plugins/trustedcoin/trustedcoin.py
  5. 2
      electrum/storage.py
  6. 5
      electrum/tests/test_wallet_vertical.py
  7. 9
      electrum/version.py

26
electrum/base_wizard.py

@ -417,7 +417,7 @@ class BaseWizard(object):
self.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f('')
elif self.seed_type == 'old':
self.run('create_keystore', seed, '')
elif self.seed_type == '2fa':
elif bitcoin.is_any_2fa_seed_type(self.seed_type):
self.load_2fa()
self.run('on_restore_seed', seed, is_ext)
else:
@ -540,18 +540,20 @@ class BaseWizard(object):
def show_xpub_and_add_cosigners(self, xpub):
self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore'))
def choose_seed_type(self):
def choose_seed_type(self, message=None, choices=None):
title = _('Choose Seed type')
message = ' '.join([
_("The type of addresses used by your wallet will depend on your seed."),
_("Segwit wallets use bech32 addresses, defined in BIP173."),
_("Please note that websites and other wallets may not support these addresses yet."),
_("Thus, you might want to keep using a non-segwit wallet in order to be able to receive bitcoins during the transition period.")
])
choices = [
('create_segwit_seed', _('Segwit')),
('create_standard_seed', _('Legacy')),
]
if message is None:
message = ' '.join([
_("The type of addresses used by your wallet will depend on your seed."),
_("Segwit wallets use bech32 addresses, defined in BIP173."),
_("Please note that websites and other wallets may not support these addresses yet."),
_("Thus, you might want to keep using a non-segwit wallet in order to be able to receive bitcoins during the transition period.")
])
if choices is None:
choices = [
('create_segwit_seed', _('Segwit')),
('create_standard_seed', _('Legacy')),
]
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run)
def create_segwit_seed(self): self.create_seed('segwit')

6
electrum/bitcoin.py

@ -207,6 +207,8 @@ def seed_type(x: str) -> str:
return 'segwit'
elif is_new_seed(x, version.SEED_PREFIX_2FA):
return '2fa'
elif is_new_seed(x, version.SEED_PREFIX_2FA_SW):
return '2fa_segwit'
return ''
@ -214,6 +216,10 @@ def is_seed(x: str) -> bool:
return bool(seed_type(x))
def is_any_2fa_seed_type(seed_type):
return seed_type in ['2fa', '2fa_segwit']
############ functions from pywallet #####################
def hash160_to_b58_address(h160: bytes, addrtype: int) -> str:

12
electrum/plugins/trustedcoin/qt.py

@ -195,18 +195,6 @@ class Plugin(TrustedCoinPlugin):
vbox.addLayout(Buttons(CloseButton(d)))
d.exec_()
def on_buy(self, window, k, v, d):
d.close()
if window.pluginsdialog:
window.pluginsdialog.close()
wallet = window.wallet
uri = "bitcoin:" + wallet.billing_info['billing_address'] + "?message=TrustedCoin %d Prepaid Transactions&amount="%k + str(Decimal(v)/100000000)
wallet.is_billing = True
window.pay_to_URI(uri)
window.payto_e.setFrozen(True)
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)),

140
electrum/plugins/trustedcoin/trustedcoin.py

@ -28,15 +28,17 @@ import json
import base64
import time
import hashlib
from collections import defaultdict
from typing import Dict
from urllib.parse import urljoin
from urllib.parse import quote
from aiohttp import ClientResponse
from electrum import ecc, constants, keystore, version, bip32
from electrum.bitcoin import TYPE_ADDRESS, is_new_seed, public_key_to_p2pkh
from electrum import ecc, constants, keystore, version, bip32, bitcoin
from electrum.bitcoin import TYPE_ADDRESS, is_new_seed, seed_type, is_any_2fa_seed_type
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.transaction import TxOutput
from electrum.mnemonic import Mnemonic
@ -47,12 +49,18 @@ from electrum.util import NotEnoughFunds
from electrum.storage import STO_EV_USER_PW
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():
if constants.net.TESTNET:
return "tpubD6NzVbkrYhZ4XdmyJQcCPjQfg6RXVUzGFhPjZ7uvRC8JLcS7Hw1i7UTpyhp9grHpak4TyK2hzBJrujDVLXQ6qB5tNpVx9rC6ixijUXadnmY"
def get_signing_xpub(xtype):
if not constants.net.TESTNET:
xpub = "xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL"
else:
return "xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL"
xpub = "tpubD6NzVbkrYhZ4XdmyJQcCPjQfg6RXVUzGFhPjZ7uvRC8JLcS7Hw1i7UTpyhp9grHpak4TyK2hzBJrujDVLXQ6qB5tNpVx9rC6ixijUXadnmY"
if xtype not in ('standard', 'p2wsh'):
raise NotImplementedError('xtype: {}'.format(xtype))
if xtype == 'standard':
return xpub
_, depth, fingerprint, child_number, c, cK = bip32.deserialize_xpub(xpub)
xpub = bip32.serialize_xpub(xtype, c, cK, depth, fingerprint, child_number)
return xpub
def get_billing_xpub():
if constants.net.TESTNET:
@ -60,7 +68,6 @@ def get_billing_xpub():
else:
return "xpub6DTBdtBB8qUmH5c77v8qVGVoYk7WjJNpGvutqjLasNG1mbux6KsojaLrYf2sRhXAVU4NaFuHhbD9SvVPRt1MB1MaMooRuhHcAZH1yhQ1qDU"
SEED_PREFIX = version.SEED_PREFIX_2FA
DISCLAIMER = [
_("Two-factor authentication is a service provided by TrustedCoin. "
@ -239,12 +246,18 @@ class Wallet_2fa(Multisig_Wallet):
self._load_billing_addresses()
def _load_billing_addresses(self):
billing_addresses = self.storage.get('trustedcoin_billing_addresses', {})
self._billing_addresses = {} # index -> addr
# convert keys from str to int
for index, addr in list(billing_addresses.items()):
self._billing_addresses[int(index)] = addr
self._billing_addresses_set = set(self._billing_addresses.values()) # set of addrs
billing_addresses = {
'legacy': self.storage.get('trustedcoin_billing_addresses', {}),
'segwit': self.storage.get('trustedcoin_billing_addresses_segwit', {})
}
self._billing_addresses = {} # type: Dict[str, Dict[int, str]] # addr_type -> index -> addr
self._billing_addresses_set = set() # set of addrs
for addr_type, d in list(billing_addresses.items()):
self._billing_addresses[addr_type] = {}
# convert keys from str to int
for index, addr in d.items():
self._billing_addresses[addr_type][int(index)] = addr
self._billing_addresses_set.add(addr)
def can_sign_without_server(self):
return not self.keystores['x2/'].is_watching_only()
@ -284,7 +297,7 @@ class Wallet_2fa(Multisig_Wallet):
self, coins, o, config, fixed_fee, change_addr)
fee = self.extra_fee(config) if not is_sweep else 0
if fee:
address = self.billing_info['billing_address']
address = self.billing_info['billing_address_segwit']
fee_output = TxOutput(TYPE_ADDRESS, address, fee)
try:
tx = mk_tx(outputs + [fee_output])
@ -305,7 +318,7 @@ class Wallet_2fa(Multisig_Wallet):
return
otp = int(otp)
long_user_id, short_id = self.get_user_id()
raw_tx = tx.serialize_to_network()
raw_tx = tx.serialize()
r = server.sign(short_id, raw_tx, otp)
if r:
raw_tx = r.get('transaction')
@ -315,8 +328,9 @@ class Wallet_2fa(Multisig_Wallet):
self.billing_info = None
self.plugin.start_request_thread(self)
def add_new_billing_address(self, billing_index: int, address: str):
saved_addr = self._billing_addresses.get(billing_index)
def add_new_billing_address(self, billing_index: int, address: str, addr_type: str):
billing_addresses_of_this_type = self._billing_addresses[addr_type]
saved_addr = billing_addresses_of_this_type.get(billing_index)
if saved_addr is not None:
if saved_addr == address:
return # already saved this address
@ -325,16 +339,18 @@ class Wallet_2fa(Multisig_Wallet):
'for index {}, already saved {}, now got {}'
.format(billing_index, saved_addr, address))
# do we have all prior indices? (are we synced?)
largest_index_we_have = max(self._billing_addresses) if self._billing_addresses else -1
largest_index_we_have = max(billing_addresses_of_this_type) if billing_addresses_of_this_type else -1
if largest_index_we_have + 1 < billing_index: # need to sync
for i in range(largest_index_we_have + 1, billing_index):
addr = make_billing_address(self, i)
self._billing_addresses[i] = addr
addr = make_billing_address(self, i, addr_type=addr_type)
billing_addresses_of_this_type[i] = addr
self._billing_addresses_set.add(addr)
# save this address; and persist to disk
self._billing_addresses[billing_index] = address
billing_addresses_of_this_type[billing_index] = address
self._billing_addresses_set.add(address)
self.storage.put('trustedcoin_billing_addresses', self._billing_addresses)
self._billing_addresses[addr_type] = billing_addresses_of_this_type
self.storage.put('trustedcoin_billing_addresses', self._billing_addresses['legacy'])
self.storage.put('trustedcoin_billing_addresses_segwit', self._billing_addresses['segwit'])
# FIXME this often runs in a daemon thread, where storage.write will fail
self.storage.write()
@ -358,12 +374,17 @@ def make_xpub(xpub, s):
cK2, c2 = bip32._CKD_pub(cK, c, s)
return serialize_xpub(version, c2, cK2)
def make_billing_address(wallet, num):
def make_billing_address(wallet, num, addr_type):
long_id, short_id = wallet.get_user_id()
xpub = make_xpub(get_billing_xpub(), long_id)
version, _, _, _, c, cK = deserialize_xpub(xpub)
cK, c = CKD_pub(cK, c, num)
return public_key_to_p2pkh(cK)
if addr_type == 'legacy':
return bitcoin.public_key_to_p2pkh(cK)
elif addr_type == 'segwit':
return bitcoin.public_key_to_p2wpkh(cK)
else:
raise ValueError(f'unexpected billing type: {addr_type}')
class TrustedCoinPlugin(BasePlugin):
@ -377,7 +398,8 @@ class TrustedCoinPlugin(BasePlugin):
@staticmethod
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):
return True
@ -420,7 +442,7 @@ class TrustedCoinPlugin(BasePlugin):
return f
@finish_requesting
def request_billing_info(self, wallet):
def request_billing_info(self, wallet: 'Wallet_2fa'):
if wallet.can_sign_without_server():
return
self.print_error("request billing info")
@ -430,11 +452,16 @@ class TrustedCoinPlugin(BasePlugin):
self.print_error('cannot connect to TrustedCoin server: {}'.format(repr(e)))
return
billing_index = billing_info['billing_index']
billing_address = make_billing_address(wallet, billing_index)
if billing_address != billing_info['billing_address']:
raise Exception('unexpected trustedcoin billing address: expected {}, received {}'
.format(billing_address, billing_info['billing_address']))
wallet.add_new_billing_address(billing_index, billing_address)
# add segwit billing address; this will be used for actual billing
billing_address = make_billing_address(wallet, billing_index, addr_type='segwit')
if billing_address != billing_info['billing_address_segwit']:
raise Exception(f'unexpected trustedcoin billing address: '
f'calculated {billing_address}, received {billing_info["billing_address_segwit"]}')
wallet.add_new_billing_address(billing_index, billing_address, addr_type='segwit')
# also add legacy billing address; only used for detecting past payments in GUI
billing_address = make_billing_address(wallet, billing_index, addr_type='legacy')
wallet.add_new_billing_address(billing_index, billing_address, addr_type='legacy')
wallet.billing_info = billing_info
wallet.price_per_tx = dict(billing_info['price_per_tx'])
wallet.price_per_tx.pop(1, None)
@ -449,8 +476,10 @@ class TrustedCoinPlugin(BasePlugin):
t.start()
return t
def make_seed(self):
return Mnemonic('english').make_seed(seed_type='2fa', num_bits=128)
def make_seed(self, seed_type):
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
def do_clear(self, window):
@ -465,25 +494,40 @@ class TrustedCoinPlugin(BasePlugin):
title = _('Create or restore')
message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?')
choices = [
('create_seed', _('Create a new seed')),
('choose_seed_type', _('Create a new seed')),
('restore_wallet', _('I already have a seed')),
]
wizard.choice_dialog(title=title, message=message, choices=choices, run_next=wizard.run)
def create_seed(self, wizard):
seed = self.make_seed()
def choose_seed_type(self, wizard):
choices = [
('create_2fa_segwit_seed', _('Segwit 2FA')),
('create_2fa_seed', _('Legacy 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)
wizard.show_seed_dialog(run_next=f, seed_text=seed)
@classmethod
def get_xkeys(self, seed, passphrase, derivation):
def get_xkeys(self, seed, t, passphrase, derivation):
assert is_any_2fa_seed_type(t)
xtype = 'standard' if t == '2fa' else 'p2wsh'
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)
return xprv, xpub
@classmethod
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()
n = len(words)
# old version use long seed phrases
@ -493,11 +537,11 @@ class TrustedCoinPlugin(BasePlugin):
# the probability of it being < 20 words is about 2^(-(256+12-19*11)) = 2^(-59)
if passphrase != '':
raise Exception('old 2fa seed cannot have passphrase')
xprv1, xpub1 = self.get_xkeys(' '.join(words[0:12]), '', "m/")
xprv2, xpub2 = self.get_xkeys(' '.join(words[12:]), '', "m/")
elif n==12:
xprv1, xpub1 = self.get_xkeys(seed, passphrase, "m/0'/")
xprv2, xpub2 = self.get_xkeys(seed, passphrase, "m/1'/")
xprv1, xpub1 = self.get_xkeys(' '.join(words[0:12]), t, '', "m/")
xprv2, xpub2 = self.get_xkeys(' '.join(words[12:]), t, '', "m/")
elif not t == '2fa' or n == 12:
xprv1, xpub1 = self.get_xkeys(seed, t, passphrase, "m/0'/")
xprv2, xpub2 = self.get_xkeys(seed, t, passphrase, "m/1'/")
else:
raise Exception('unrecognized seed length: {} words'.format(n))
return xprv1, xpub1, xprv2, xpub2
@ -561,7 +605,8 @@ class TrustedCoinPlugin(BasePlugin):
storage.put('x1/', k1.dump())
storage.put('x2/', k2.dump())
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)
storage.put('x3/', k3.dump())
@ -578,7 +623,8 @@ class TrustedCoinPlugin(BasePlugin):
xpub2 = wizard.storage.get('x2/')['xpub']
# Generate third key deterministically.
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
try:
r = server.create(xpub1, xpub2, email)

2
electrum/storage.py

@ -572,9 +572,7 @@ class WalletStorage(JsonDB):
# delete verified_tx3 as its structure changed
if not self._is_upgrade_method_needed(17, 17):
return
self.put('verified_tx3', None)
self.put('seed_version', 18)
def convert_imported(self):

5
electrum/tests/test_wallet_vertical.py

@ -4,7 +4,7 @@ import tempfile
from typing import Sequence
import asyncio
from electrum import storage, bitcoin, keystore
from electrum import storage, bitcoin, keystore, bip32
from electrum import Transaction
from electrum import SimpleConfig
from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT
@ -178,7 +178,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(SequentialTestCase):
long_user_id, short_id = trustedcoin.get_user_id(
{'x1/': {'xpub': xpub1},
'x2/': {'xpub': xpub2}})
xpub3 = trustedcoin.make_xpub(trustedcoin.get_signing_xpub(), long_user_id)
xtype = bip32.xpub_type(xpub1)
xpub3 = trustedcoin.make_xpub(trustedcoin.get_signing_xpub(xtype), long_user_id)
ks3 = keystore.from_xpub(xpub3)
WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks3)
self.assertTrue(isinstance(ks3, keystore.BIP32_KeyStore))

9
electrum/version.py

@ -4,9 +4,10 @@ APK_VERSION = '3.3.0.0' # read by buildozer.spec
PROTOCOL_VERSION = '1.4' # protocol version requested
# The hash of the mnemonic seed must begin with this
SEED_PREFIX = '01' # Standard wallet
SEED_PREFIX_2FA = '101' # Two-factor authentication
SEED_PREFIX_SW = '100' # Segwit wallet
SEED_PREFIX = '01' # Standard wallet
SEED_PREFIX_SW = '100' # Segwit wallet
SEED_PREFIX_2FA = '101' # Two-factor authentication
SEED_PREFIX_2FA_SW = '102' # Two-factor auth, using segwit
def seed_prefix(seed_type):
@ -16,3 +17,5 @@ def seed_prefix(seed_type):
return SEED_PREFIX_SW
elif seed_type == '2fa':
return SEED_PREFIX_2FA
elif seed_type == '2fa_segwit':
return SEED_PREFIX_2FA_SW

Loading…
Cancel
Save