BIP39 passphrase additions, no UX yet

This commit is contained in:
Peter D. Gray 2019-03-27 11:00:43 -04:00
parent 1454dca96a
commit 6302fd88fb
17 changed files with 503 additions and 35 deletions

@ -1 +1 @@
Subproject commit 45878fe6e9d7f15d922126c6bdba73457cf5afee
Subproject commit c3c3cc4c6e8cfa407c2fb9f26c3097d58e124605

View File

@ -75,7 +75,7 @@ async def view_ident(*a):
# show the XPUB, and other ident on screen
from main import settings, pa
from version import serial_number
import callgate
import callgate, stash
tpl = '''\
Master Key Fingerprint:
@ -97,6 +97,9 @@ Extended Master Key:
if pa.is_secondary:
msg += '\n(Secondary wallet)\n'
if stash.bip39_passphrase:
msg += '\nA passphrase (BIP39) is in effect.\n'
bn = callgate.get_bag_number()
if bn:
msg += '\nShipping Bag:\n %s\n' % bn
@ -371,7 +374,24 @@ async def start_seed_import(menu, label, item):
def pick_new_wallet(*a):
import seed
return seed.make_new_wallet()
async def convert_bip39_to_bip32(*a):
if not await ux_confirm('''This operation computes the extended master private key using your BIP39 seed words and optional passphrase, and then saves the resulting value (which is an xprv) as the wallet secret.
The seed words themselves are erased forever, but effectively there is no other change. If a BIP39 passphrase is currently in effect, its value is captured during this process and will be 'in effect' going forward, but the passphrase itself is erased and unrecoverable. The resulting wallet cannot be used with other passphrases.
A reboot is part of this process. PIN code, and funds are not affected.
'''):
return await ux_aborted()
import seed
await seed.remember_bip39_passphrase()
async def set_bip39_phrase(*a):
# gather a passphrase, up to 100 chars long via a complex ux process
import seed
pw = 'test'
seed.set_bip39_passphrase(pw)
async def clear_seed(*a):
# Erase the seed words, and private key from this wallet!
@ -444,7 +464,7 @@ async def start_login_sequence():
# Populate xfp/xpub values, if missing.
# - can happen for first-time login of duress wallet
# - might indicate lost settings?
# - may indicate lost settings, which we can easily recover from
# - these values are important to USB protocol
if not (settings.get('xfp', 0) and settings.get('xpub', 0)) and not pa.is_secret_blank():
try:
@ -457,7 +477,6 @@ async def start_login_sequence():
# just in case, keep going; we're not useless and this
# is early in boot process
print("XFP save failed: %s" % exc)
# Allow USB protocol, now that we are auth'ed
from usb import enable_usb

View File

@ -58,7 +58,7 @@ class UserAuthorizedAction:
@classmethod
def check_busy(cls, allowed_cls=None):
# see if we're busy. don't interrupt that... unless it's of allowed_clas
# see if we're busy. don't interrupt that... unless it's of allowed_cls
# - also handle cleanup of stale actions
global active_request
@ -553,6 +553,67 @@ def start_remote_backup():
abort_and_goto(active_request)
class NewPassphrase(UserAuthorizedAction):
def __init__(self, pw):
super().__init__()
self._pw = pw
# self.result ... will be (len, sha256) of the resulting file at zero
async def interact(self):
# prompt them
showit = False
while 1:
if showit:
ch = await ux_show_story('''Given:\n\n%s\n\nShould we switch to that wallet now?
OK to continue, X to cancel.''' % self._pw, title="Passphrase")
else:
ch = await ux_show_story('''BIP39 passphrase (%d chars long) has been provided over USB connection. Should we switch to that wallet now?
Press 2 to view the provided passphrase.\n\nOK to continue, X to cancel.''' % len(self._pw), title="Passphrase", escape='2')
if ch == '2':
showit = True
continue
break
try:
if ch != 'y':
# they don't want to!
self.refused = True
await ux_dramatic_pause("Refused.", 1)
else:
from seed import set_bip39_passphrase
from main import settings
await ux_dramatic_pause("Switching....", 0.25)
err = set_bip39_passphrase(self._pw)
if err:
await self.failure(err)
else:
self.result = settings.get('xpub')
except BaseException as exc:
self.failed = "Exception"
sys.print_exception(exc)
finally:
self.done()
def start_bip39_passphrase(pw):
# tell the local user the secret words, and then save to SPI flash
# USB caller has to come back and download encrypted contents.
global active_request
UserAuthorizedAction.cleanup()
active_request = NewPassphrase(pw)
# kill any menu stack, and put our thing at the top
abort_and_goto(active_request)
SHOW_ADDR_TEMPLATE = '''\
{addr}

View File

@ -39,7 +39,7 @@ def render_backup_contents():
COMMENT('Private key details: ' + chain.name)
with stash.SensitiveValues() as sv:
with stash.SensitiveValues(for_backup=True) as sv:
if sv.mode == 'words':
ADD('mnemonic', tcc.bip39.from_data(sv.raw))
@ -72,7 +72,8 @@ def render_backup_contents():
# user preferences
for k,v in settings.current.items():
if k[0] == '_': continue # debug stuff in simulator
if k == 'xpub': continue # redundant
if k == 'xpub': continue # redundant, and wrong if bip39pw
if k == 'xfp': continue # redundant, and wrong if bip39pw
ADD('setting.' + k, v)
rv.write('\n# EOF\n')

View File

@ -121,6 +121,7 @@ AdvancedNormalMenu = [
MenuItem("Upgrade", menu=UpgradeMenu),
MenuItem("Backup", menu=BackupStuffMenu),
MenuItem("MicroSD Card", menu=SDCardMenu),
MenuItem('Lock Down Seed', f=convert_bip39_to_bip32),
MenuItem("Danger Zone", menu=DangerZoneMenu),
]
@ -157,11 +158,13 @@ EmptyWallet = [
NormalSystem = [
# xxxxxxxxxxxxxxxx
MenuItem('Ready To Sign', f=ready2sign),
MenuItem('Passphrase BIP39', f=set_bip39_phrase),
MenuItem('Secure Logout', f=logout_now),
MenuItem('Advanced', menu=AdvancedNormalMenu),
MenuItem('Settings', menu=SettingsMenu),
]
# Shown until unit is put into a numbered bag
FactoryMenu = [
MenuItem('Bag Me Now'), # nice to have NOP at top of menu

View File

@ -50,12 +50,12 @@ class SettingsObject:
def __init__(self, loop=None):
self.loop = loop
self.current = {}
self.is_dirty = 0
self.my_pos = 0
self.nvram_key = b'\0'*32
self.current = self.default_values()
self.overrides = {} # volatile overide values
self.load()
@ -108,7 +108,8 @@ class SettingsObject:
from main import sf
# reset
self.current = {}
self.current.clear()
self.overrides.clear()
self.my_pos = 0
self.is_dirty = 0
@ -198,7 +199,10 @@ class SettingsObject:
sf.write(pos+i, h)
def get(self, kn, default=None):
return self.current.get(kn, default)
if kn in self.overrides:
return self.overrides.get(kn)
else:
return self.current.get(kn, default)
def changed(self):
self.is_dirty += 1
@ -209,6 +213,9 @@ class SettingsObject:
self.current[kn] = v
self.changed()
def put_volatile(self, kn, v):
self.overrides[kn] = v
set = put
def clear(self):
@ -219,6 +226,7 @@ class SettingsObject:
for k in rk:
del self.current[k]
self.overrides.clear()
self.changed()
async def write_out(self):
@ -322,7 +330,8 @@ class SettingsObject:
self.my_pos = 0
# act blank too, just in case.
self.current = {}
self.current.clear()
self.overrides.clear()
self.is_dirty = 0
@staticmethod

View File

@ -324,7 +324,9 @@ async def make_new_wallet():
goto_top_menu()
def set_seed_value(words, pw=''):
def set_seed_value(words):
# Save the seed words into secure element, and reboot. BIP39 password
# is not set at this point (empty string)
ok = tcc.bip39.check(' '.join(words))
assert ok, "seed check: %r" % words
@ -350,6 +352,53 @@ def set_seed_value(words, pw=''):
# encode it for our limited secret space
nv = SecretStash.encode(seed_phrase=seed)
dis.fullscreen('Applying...')
pa.change(new_secret=nv)
# re-read settings since key is now different
# - also captures xfp, xpub at this point
pa.new_main_secret(nv)
# check and reload secret
pa.reset()
pa.login()
def set_bip39_passphrase(pw):
# apply bip39 passphrase for now (volatile)
import stash
stash.bip39_passphrase = pw
with stash.SensitiveValues() as sv:
if sv.mode != 'words':
# can't do it without original seed woods
return 'No BIP39 seed words'
sv.capture_xpub()
# Might need to bounce the USB connection, because our pubkey has changed,
# altho if they have already picked a shared session key, no need, and
# only affects MitM testing.
async def remember_bip39_passphrase():
# Compute current xprv and switch to using that as root secret.
import stash
from main import dis, pa
if not stash.bip39_passphrase:
if not await ux_confirm('''You do not have a BIP39 passphrase set right now, so this command does little except forget the seed words. It does not enhance security.'''):
return
dis.fullscreen('Check...')
with stash.SensitiveValues() as sv:
if sv.mode != 'words':
# not a BIP39 derived secret, so cannot work.
await ux_show_story('''The wallet secret was not based on a seed phrase, so we cannot add a BIP39 passphrase at this time.''', title='Failed')
return
nv = SecretStash.encode(xprv=sv.node)
dis.fullscreen('Saving...')
pa.change(new_secret=nv)

View File

@ -30,11 +30,13 @@ def blank_object(item):
else:
raise TypeError(item)
# Chip can hold 72-bytes as a secret: we need to store either
# a list of seed words (packed), of various lengths, or maybe
# a raw master secret, and so on
class SecretStash:
@staticmethod
def encode(seed_phrase=None, master_secret=None, xprv=None):
nv = bytearray(AE_SECRET_LEN)
@ -65,7 +67,7 @@ class SecretStash:
return nv
@staticmethod
def decode(secret):
def decode(secret, _bip39pw=''):
# expecting 72-bytes of secret payload; decode meaning
# returns:
# type, secrets bytes, HDNode(root)
@ -88,9 +90,9 @@ class SecretStash:
# - not storing checksum
assert ll in [16, 24, 32]
# make master secret, using the memonic words, and empty passphrase
# make master secret, using the memonic words, and passphrase (or empty string)
seed_bits = secret[1:1+ll]
ms = tcc.bip39.seed(tcc.bip39.from_data(seed_bits), '')
ms = tcc.bip39.seed(tcc.bip39.from_data(seed_bits), _bip39pw)
hd = tcc.bip32.from_seed(ms, 'secp256k1')
@ -106,10 +108,13 @@ class SecretStash:
return 'master', ms, hd
# optional global value: user-supplied passphrase to salt BIP39 seed process
bip39_passphrase = ''
class SensitiveValues:
# be a context manager, and holder to secrets in-memory
def __init__(self, secret=None):
def __init__(self, secret=None, for_backup=False):
if secret is None:
# fetch the secret from bootloader/atecc508a
from main import pa
@ -123,10 +128,13 @@ class SensitiveValues:
assert set(secret) != {0}
self.secret = secret
# backup during volatile bip39 encryption: do not use passphrase
self._bip39pw = '' if for_backup else str(bip39_passphrase)
def __enter__(self):
import chains
self.mode, self.raw, self.node = SecretStash.decode(self.secret)
self.mode, self.raw, self.node = SecretStash.decode(self.secret, self._bip39pw)
self.chain = chains.current_chain()
@ -164,18 +172,29 @@ class SensitiveValues:
def capture_xpub(self):
# track my xpubkey fingerprint & value in settings (not sensitive really)
# - we share this on any USB connection
# - we share these on any USB connection
from main import settings
#print("capture xfp/xpub/chain")
settings.set('xfp', self.node.my_fingerprint())
settings.set('xpub', self.chain.serialize_public(self.node))
settings.set('chain', self.chain.ctype)
# Implicit in the values is the BIP39 encryption passphrase,
# which we might not want to actually store.
xfp = self.node.my_fingerprint()
xpub = self.chain.serialize_public(self.node)
if self._bip39pw:
settings.put_volatile('xfp', xfp)
settings.put_volatile('xpub', xpub)
else:
settings.overrides.clear()
settings.put('xfp', xfp)
settings.put('xpub', xpub)
settings.put('chain', self.chain.ctype)
def register(self, item):
# Caller can add his own sensitive (derived?) data to our wiper
# typically would be byte arrays or byte strings, but also
# support bip32 nodes
# supports bip32 nodes
self.spots.append(item)
def derive_path(self, path, master=None):

View File

@ -74,6 +74,7 @@ def enable_usb(loop, repl_enable=False):
loop.create_task(handler.usb_hid_recv())
def is_vcp_active():
# VCP = Virtual Comm Port
en = ckcc.vcp_enabled(None)
cur = pyb.usb_mode()
@ -358,7 +359,7 @@ class USBHandler:
sign_transaction(txn_len, bool(finalize))
return None
if cmd == 'stok' or cmd == 'bkok' or cmd == 'smok':
if cmd == 'stok' or cmd == 'bkok' or cmd == 'smok' or cmd == 'pwok':
# Have we finished (whatever) the transaction,
# which needed user approval? If so, provide result.
from auth import active_request, UserAuthorizedAction
@ -379,7 +380,12 @@ class USBHandler:
# STILL waiting on user
return None
if cmd == 'smok':
if cmd == 'pwok':
# return new root xpub
xpub = active_request.result
UserAuthorizedAction.cleanup()
return b'asci' + bytes(xpub, 'ascii')
elif cmd == 'smok':
# signed message done: just give them the signature
addr, sig = active_request.address, active_request.result
UserAuthorizedAction.cleanup()
@ -390,11 +396,23 @@ class USBHandler:
UserAuthorizedAction.cleanup()
return pack('<4sI32s', 'strx', resp_len, sha)
if cmd == 'pass':
# bip39 passphrase provided, maybe use it if authorized
assert self.encrypted_req, 'must encrypt'
from auth import start_bip39_passphrase
assert len(args) < 400, 'too long'
pw = str(args, 'utf8')
assert len(pw) < 100, 'too long'
return start_bip39_passphrase(pw)
if cmd == 'back':
# start backup: asks user, takes long time.
from auth import start_remote_backup
return start_remote_backup()
if cmd == 'bagi':
return self.handle_bag_number(args)

View File

@ -41,7 +41,7 @@ firmware-signed.dfu: firmware-signed.bin Makefile
$(PYTHON_MAKE_DFU) -b $(FIRMWARE_BASE):$< $@
# This is fast for Coinkite devs, but no DFU support in wild.
dfu: firmware-signed.dfu
dfu up: firmware-signed.dfu
$(PYTHON_DO_DFU) -u firmware-signed.dfu
# Make a factory release: using key #1

148
testing/bip39-vectors.json Normal file
View File

@ -0,0 +1,148 @@
{
"english": [
[
"00000000000000000000000000000000",
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
"c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04",
"xprv9s21ZrQH143K3h3fDYiay8mocZ3afhfULfb5GX8kCBdno77K4HiA15Tg23wpbeF1pLfs1c5SPmYHrEpTuuRhxMwvKDwqdKiGJS9XFKzUsAF"
],
[
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
"legal winner thank year wave sausage worth useful legal winner thank yellow",
"2e8905819b8723fe2c1d161860e5ee1830318dbf49a83bd451cfb8440c28bd6fa457fe1296106559a3c80937a1c1069be3a3a5bd381ee6260e8d9739fce1f607",
"xprv9s21ZrQH143K2gA81bYFHqU68xz1cX2APaSq5tt6MFSLeXnCKV1RVUJt9FWNTbrrryem4ZckN8k4Ls1H6nwdvDTvnV7zEXs2HgPezuVccsq"
],
[
"80808080808080808080808080808080",
"letter advice cage absurd amount doctor acoustic avoid letter advice cage above",
"d71de856f81a8acc65e6fc851a38d4d7ec216fd0796d0a6827a3ad6ed5511a30fa280f12eb2e47ed2ac03b5c462a0358d18d69fe4f985ec81778c1b370b652a8",
"xprv9s21ZrQH143K2shfP28KM3nr5Ap1SXjz8gc2rAqqMEynmjt6o1qboCDpxckqXavCwdnYds6yBHZGKHv7ef2eTXy461PXUjBFQg6PrwY4Gzq"
],
[
"ffffffffffffffffffffffffffffffff",
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong",
"ac27495480225222079d7be181583751e86f571027b0497b5b5d11218e0a8a13332572917f0f8e5a589620c6f15b11c61dee327651a14c34e18231052e48c069",
"xprv9s21ZrQH143K2V4oox4M8Zmhi2Fjx5XK4Lf7GKRvPSgydU3mjZuKGCTg7UPiBUD7ydVPvSLtg9hjp7MQTYsW67rZHAXeccqYqrsx8LcXnyd"
],
[
"000000000000000000000000000000000000000000000000",
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent",
"035895f2f481b1b0f01fcf8c289c794660b289981a78f8106447707fdd9666ca06da5a9a565181599b79f53b844d8a71dd9f439c52a3d7b3e8a79c906ac845fa",
"xprv9s21ZrQH143K3mEDrypcZ2usWqFgzKB6jBBx9B6GfC7fu26X6hPRzVjzkqkPvDqp6g5eypdk6cyhGnBngbjeHTe4LsuLG1cCmKJka5SMkmU"
],
[
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
"legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will",
"f2b94508732bcbacbcc020faefecfc89feafa6649a5491b8c952cede496c214a0c7b3c392d168748f2d4a612bada0753b52a1c7ac53c1e93abd5c6320b9e95dd",
"xprv9s21ZrQH143K3Lv9MZLj16np5GzLe7tDKQfVusBni7toqJGcnKRtHSxUwbKUyUWiwpK55g1DUSsw76TF1T93VT4gz4wt5RM23pkaQLnvBh7"
],
[
"808080808080808080808080808080808080808080808080",
"letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always",
"107d7c02a5aa6f38c58083ff74f04c607c2d2c0ecc55501dadd72d025b751bc27fe913ffb796f841c49b1d33b610cf0e91d3aa239027f5e99fe4ce9e5088cd65",
"xprv9s21ZrQH143K3VPCbxbUtpkh9pRG371UCLDz3BjceqP1jz7XZsQ5EnNkYAEkfeZp62cDNj13ZTEVG1TEro9sZ9grfRmcYWLBhCocViKEJae"
],
[
"ffffffffffffffffffffffffffffffffffffffffffffffff",
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when",
"0cd6e5d827bb62eb8fc1e262254223817fd068a74b5b449cc2f667c3f1f985a76379b43348d952e2265b4cd129090758b3e3c2c49103b5051aac2eaeb890a528",
"xprv9s21ZrQH143K36Ao5jHRVhFGDbLP6FCx8BEEmpru77ef3bmA928BxsqvVM27WnvvyfWywiFN8K6yToqMaGYfzS6Db1EHAXT5TuyCLBXUfdm"
],
[
"0000000000000000000000000000000000000000000000000000000000000000",
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
"bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd3097170af7a4d73245cafa9c3cca8d561a7c3de6f5d4a10be8ed2a5e608d68f92fcc8",
"xprv9s21ZrQH143K32qBagUJAMU2LsHg3ka7jqMcV98Y7gVeVyNStwYS3U7yVVoDZ4btbRNf4h6ibWpY22iRmXq35qgLs79f312g2kj5539ebPM"
],
[
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
"legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title",
"bc09fca1804f7e69da93c2f2028eb238c227f2e9dda30cd63699232578480a4021b146ad717fbb7e451ce9eb835f43620bf5c514db0f8add49f5d121449d3e87",
"xprv9s21ZrQH143K3Y1sd2XVu9wtqxJRvybCfAetjUrMMco6r3v9qZTBeXiBZkS8JxWbcGJZyio8TrZtm6pkbzG8SYt1sxwNLh3Wx7to5pgiVFU"
],
[
"8080808080808080808080808080808080808080808080808080808080808080",
"letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless",
"c0c519bd0e91a2ed54357d9d1ebef6f5af218a153624cf4f2da911a0ed8f7a09e2ef61af0aca007096df430022f7a2b6fb91661a9589097069720d015e4e982f",
"xprv9s21ZrQH143K3CSnQNYC3MqAAqHwxeTLhDbhF43A4ss4ciWNmCY9zQGvAKUSqVUf2vPHBTSE1rB2pg4avopqSiLVzXEU8KziNnVPauTqLRo"
],
[
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote",
"dd48c104698c30cfe2b6142103248622fb7bb0ff692eebb00089b32d22484e1613912f0a5b694407be899ffd31ed3992c456cdf60f5d4564b8ba3f05a69890ad",
"xprv9s21ZrQH143K2WFF16X85T2QCpndrGwx6GueB72Zf3AHwHJaknRXNF37ZmDrtHrrLSHvbuRejXcnYxoZKvRquTPyp2JiNG3XcjQyzSEgqCB"
],
[
"9e885d952ad362caeb4efe34a8e91bd2",
"ozone drill grab fiber curtain grace pudding thank cruise elder eight picnic",
"274ddc525802f7c828d8ef7ddbcdc5304e87ac3535913611fbbfa986d0c9e5476c91689f9c8a54fd55bd38606aa6a8595ad213d4c9c9f9aca3fb217069a41028",
"xprv9s21ZrQH143K2oZ9stBYpoaZ2ktHj7jLz7iMqpgg1En8kKFTXJHsjxry1JbKH19YrDTicVwKPehFKTbmaxgVEc5TpHdS1aYhB2s9aFJBeJH"
],
[
"6610b25967cdcca9d59875f5cb50b0ea75433311869e930b",
"gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog",
"628c3827a8823298ee685db84f55caa34b5cc195a778e52d45f59bcf75aba68e4d7590e101dc414bc1bbd5737666fbbef35d1f1903953b66624f910feef245ac",
"xprv9s21ZrQH143K3uT8eQowUjsxrmsA9YUuQQK1RLqFufzybxD6DH6gPY7NjJ5G3EPHjsWDrs9iivSbmvjc9DQJbJGatfa9pv4MZ3wjr8qWPAK"
],
[
"68a79eaca2324873eacc50cb9c6eca8cc68ea5d936f98787c60c7ebc74e6ce7c",
"hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length",
"64c87cde7e12ecf6704ab95bb1408bef047c22db4cc7491c4271d170a1b213d20b385bc1588d9c7b38f1b39d415665b8a9030c9ec653d75e65f847d8fc1fc440",
"xprv9s21ZrQH143K2XTAhys3pMNcGn261Fi5Ta2Pw8PwaVPhg3D8DWkzWQwjTJfskj8ofb81i9NP2cUNKxwjueJHHMQAnxtivTA75uUFqPFeWzk"
],
[
"c0ba5a8e914111210f2bd131f3d5e08d",
"scheme spot photo card baby mountain device kick cradle pact join borrow",
"ea725895aaae8d4c1cf682c1bfd2d358d52ed9f0f0591131b559e2724bb234fca05aa9c02c57407e04ee9dc3b454aa63fbff483a8b11de949624b9f1831a9612",
"xprv9s21ZrQH143K3FperxDp8vFsFycKCRcJGAFmcV7umQmcnMZaLtZRt13QJDsoS5F6oYT6BB4sS6zmTmyQAEkJKxJ7yByDNtRe5asP2jFGhT6"
],
[
"6d9be1ee6ebd27a258115aad99b7317b9c8d28b6d76431c3",
"horn tenant knee talent sponsor spell gate clip pulse soap slush warm silver nephew swap uncle crack brave",
"fd579828af3da1d32544ce4db5c73d53fc8acc4ddb1e3b251a31179cdb71e853c56d2fcb11aed39898ce6c34b10b5382772db8796e52837b54468aeb312cfc3d",
"xprv9s21ZrQH143K3R1SfVZZLtVbXEB9ryVxmVtVMsMwmEyEvgXN6Q84LKkLRmf4ST6QrLeBm3jQsb9gx1uo23TS7vo3vAkZGZz71uuLCcywUkt"
],
[
"9f6a2878b2520799a44ef18bc7df394e7061a224d2c33cd015b157d746869863",
"panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich alcohol speed nation flash devote level hobby quick inner drive ghost inside",
"72be8e052fc4919d2adf28d5306b5474b0069df35b02303de8c1729c9538dbb6fc2d731d5f832193cd9fb6aeecbc469594a70e3dd50811b5067f3b88b28c3e8d",
"xprv9s21ZrQH143K2WNnKmssvZYM96VAr47iHUQUTUyUXH3sAGNjhJANddnhw3i3y3pBbRAVk5M5qUGFr4rHbEWwXgX4qrvrceifCYQJbbFDems"
],
[
"23db8160a31d3e0dca3688ed941adbf3",
"cat swing flag economy stadium alone churn speed unique patch report train",
"deb5f45449e615feff5640f2e49f933ff51895de3b4381832b3139941c57b59205a42480c52175b6efcffaa58a2503887c1e8b363a707256bdd2b587b46541f5",
"xprv9s21ZrQH143K4G28omGMogEoYgDQuigBo8AFHAGDaJdqQ99QKMQ5J6fYTMfANTJy6xBmhvsNZ1CJzRZ64PWbnTFUn6CDV2FxoMDLXdk95DQ"
],
[
"8197a4a47f0425faeaa69deebc05ca29c0a5b5cc76ceacc0",
"light rule cinnamon wrap drastic word pride squirrel upgrade then income fatal apart sustain crack supply proud access",
"4cbdff1ca2db800fd61cae72a57475fdc6bab03e441fd63f96dabd1f183ef5b782925f00105f318309a7e9c3ea6967c7801e46c8a58082674c860a37b93eda02",
"xprv9s21ZrQH143K3wtsvY8L2aZyxkiWULZH4vyQE5XkHTXkmx8gHo6RUEfH3Jyr6NwkJhvano7Xb2o6UqFKWHVo5scE31SGDCAUsgVhiUuUDyh"
],
[
"066dca1a2bb7e8a1db2832148ce9933eea0f3ac9548d793112d9a95c9407efad",
"all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform",
"26e975ec644423f4a4c4f4215ef09b4bd7ef924e85d1d17c4cf3f136c2863cf6df0a475045652c57eb5fb41513ca2a2d67722b77e954b4b3fc11f7590449191d",
"xprv9s21ZrQH143K3rEfqSM4QZRVmiMuSWY9wugscmaCjYja3SbUD3KPEB1a7QXJoajyR2T1SiXU7rFVRXMV9XdYVSZe7JoUXdP4SRHTxsT1nzm"
],
[
"f30f8c1da665478f49b001d94c5fc452",
"vessel ladder alter error federal sibling chat ability sun glass valve picture",
"2aaa9242daafcee6aa9d7269f17d4efe271e1b9a529178d7dc139cd18747090bf9d60295d0ce74309a78852a9caadf0af48aae1c6253839624076224374bc63f",
"xprv9s21ZrQH143K2QWV9Wn8Vvs6jbqfF1YbTCdURQW9dLFKDovpKaKrqS3SEWsXCu6ZNky9PSAENg6c9AQYHcg4PjopRGGKmdD313ZHszymnps"
],
[
"c10ec20dc3cd9f652c7fac2f1230f7a3c828389a14392f05",
"scissors invite lock maple supreme raw rapid void congress muscle digital elegant little brisk hair mango congress clump",
"7b4a10be9d98e6cba265566db7f136718e1398c71cb581e1b2f464cac1ceedf4f3e274dc270003c670ad8d02c4558b2f8e39edea2775c9e232c7cb798b069e88",
"xprv9s21ZrQH143K4aERa2bq7559eMCCEs2QmmqVjUuzfy5eAeDX4mqZffkYwpzGQRE2YEEeLVRoH4CSHxianrFaVnMN2RYaPUZJhJx8S5j6puX"
],
[
"f585c11aec520db57dd353c69554b21a89b20fb0650966fa0a9d6f74fd989d8f",
"void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold",
"01f5bced59dec48e362f2c45b5de68b9fd6c92c6634f44d6d40aab69056506f0e35524a518034ddc1192e1dacd32c1ed3eaa3c3b131c88ed8e7e54c49a5d0998",
"xprv9s21ZrQH143K39rnQJknpH1WEPFJrzmAqqasiDcVrNuk926oizzJDDQkdiTvNPr2FYDYzWgiMiC63YmfPAa2oPyNB23r2g7d1yiK6WpqaQS"
]
]
}

View File

@ -8,6 +8,11 @@ from api import bitcoind, match_key
SIM_PATH = '/tmp/ckcc-simulator.sock'
# Simulator normally powers up with this 'wallet'
simulator_fixed_xprv = "tprv8ZgxMBicQKsPeXJHL3vPPgTAEqQ5P2FD9qDeCQT4Cp1EMY5QkwMPWFxHdxHrxZhhcVRJ2m7BNWTz9Xre68y7mX5vCdMJ5qXMUfnrZ2si2X4"
simulator_fixed_words = "wife shiver author away frog air rough vanish fantasy frozen noodle athlete pioneer citizen symptom firm much faith extend rare axis garment kiwi clarify"
simulator_fixed_xfp = 0x4369050f
def pytest_addoption(parser):
parser.addoption("--dev", action="store_true",
default=False, help="run on real dev")
@ -43,7 +48,6 @@ def simulator(request):
try:
return ColdcardDevice(sn=SIM_PATH)
except:
raise
print("Simulator is required for this test")
raise pytest.fail('missing simulator')
@ -281,6 +285,7 @@ def set_master_key(sim_exec, sim_execfile, simulator):
if rv: pytest.fail(rv)
simulator.start_encryption()
simulator.check_mitm()
print("sim xfp: 0x%08x" % simulator.master_fingerprint)
@ -288,11 +293,10 @@ def set_master_key(sim_exec, sim_execfile, simulator):
# Important cleanup: restore normal key, because other tests assume that
simulator_fixed_xprv = "tprv8ZgxMBicQKsPeXJHL3vPPgTAEqQ5P2FD9qDeCQT4Cp1EMY5QkwMPWFxHdxHrxZhhcVRJ2m7BNWTz9Xre68y7mX5vCdMJ5qXMUfnrZ2si2X4"
doit(simulator_fixed_xprv)
@pytest.fixture()
def set_seed_words(sim_exec, sim_execfile, simulator, set_master_key):
def set_seed_words(sim_exec, sim_execfile, simulator):
# load simulator w/ a specific bip32 master key
def doit(words):
@ -302,6 +306,7 @@ def set_seed_words(sim_exec, sim_execfile, simulator, set_master_key):
if rv: pytest.fail(rv)
simulator.start_encryption()
simulator.check_mitm()
print("sim xfp: 0x%08x" % simulator.master_fingerprint)
@ -309,8 +314,30 @@ def set_seed_words(sim_exec, sim_execfile, simulator, set_master_key):
# Important cleanup: restore normal key, because other tests assume that
simulator_fixed_xprv = "tprv8ZgxMBicQKsPeXJHL3vPPgTAEqQ5P2FD9qDeCQT4Cp1EMY5QkwMPWFxHdxHrxZhhcVRJ2m7BNWTz9Xre68y7mX5vCdMJ5qXMUfnrZ2si2X4"
set_master_key(simulator_fixed_xprv)
doit(simulator_fixed_words)
@pytest.fixture()
def reset_seed_words(sim_exec, sim_execfile, simulator):
# load simulator w/ a specific bip39 seed phrase
def doit():
words = simulator_fixed_words
sim_exec('import main; main.WORDS = %r; ' % words.split())
rv = sim_execfile('devtest/set_seed.py')
if rv: pytest.fail(rv)
simulator.start_encryption()
simulator.check_mitm()
print("sim xfp: 0x%08x (reset)" % simulator.master_fingerprint)
assert simulator.master_fingerprint == simulator_fixed_xfp
return words
yield doit
@pytest.fixture()

View File

@ -4,13 +4,15 @@ from sim_settings import sim_defaults
import stash, chains
from h import b2a_hex
from main import settings, pa
from stash import SecretStash, SensitiveValues
import stash
from seed import set_seed_value
tn = chains.BitcoinTestnet
if 1:
stash.bip39_passphrase = ''
settings.current = sim_defaults
settings.overrides.clear()
settings.set('chain', 'XTN')
set_seed_value(main.WORDS)
@ -18,5 +20,3 @@ if 1:
print("New key in effect: %s" % settings.get('xpub', 'MISSING'))
print("Fingerprint: 0x%08x" % settings.get('xfp', 0))
#assert settings.get('xfp', 0) == node.my_fingerprint()

View File

@ -19,6 +19,7 @@ if settings.get('xfp') == node.my_fingerprint():
else:
settings.current = sim_defaults
settings.overrides.clear()
settings.set('chain', 'XTN')
raw = SecretStash.encode(xprv=node)

View File

@ -5,3 +5,4 @@ pytest
pycoin==0.80
python-bitcoinrpc>=1.0
pyserial
mnemonic==0.18

108
testing/test_bip39pw.py Normal file
View File

@ -0,0 +1,108 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
#
# BIP39 seed word encryption
#
import pytest, time
from pycoin.key.BIP32Node import BIP32Node
from pycoin.contrib.msg_signing import verify_message
from base64 import b64encode
from binascii import b2a_hex, a2b_hex
from ckcc_protocol.protocol import CCProtocolPacker, CCProtoError, CCUserRefused
from ckcc_protocol.constants import *
import json
from mnemonic import Mnemonic
# add the BIP39 test vectors
vectors = json.load(open('bip39-vectors.json'))['english']
@pytest.mark.parametrize('vector', vectors)
def test_b9p_vectors(dev, set_seed_words, need_keypress, vector, pw='RoZert'[::-1].upper()):
# Test all BIP39 vectors. Slow.
_, words, cooked, xprv = vector
seed = Mnemonic.to_seed(words, passphrase=pw)
assert seed == a2b_hex(cooked)
set_seed_words(words)
dev.send_recv(CCProtocolPacker.bip39_passphrase(pw), timeout=None)
need_keypress('y')
xpub = None
while xpub == None:
time.sleep(0.050)
xpub = dev.send_recv(CCProtocolPacker.get_passphrase_done(), timeout=None)
# check our math (ignore testnet vs. mainnet)
got = BIP32Node.from_wallet_key(xpub)
exp = BIP32Node.from_wallet_key(xprv)
assert got.public_pair() == exp.public_pair()
@pytest.mark.parametrize('pw', ['test 2', 'with some spaces',
'123 12l3kj1l2k3j 1l2k3j 1l2k3j ',
'a'*99,
'' # keep last, resets state
])
def test_b9p_basic(dev, need_keypress, pw, reset_seed_words, cap_story):
# reset from previous runs
words = reset_seed_words()
dev.send_recv(CCProtocolPacker.bip39_passphrase(pw), timeout=None)
if pw:
time.sleep(0.050)
title, body = cap_story()
assert pw not in body
# verify display of passphrase
need_keypress('2')
time.sleep(0.050)
title, body = cap_story()
assert pw in body
need_keypress('y')
done = None
while done == None:
time.sleep(0.050)
done = dev.send_recv(CCProtocolPacker.get_passphrase_done(), timeout=None)
xpub = done
assert xpub[1:4] == 'pub'
got = BIP32Node.from_wallet_key(xpub)
# what it should be
seed = Mnemonic.to_seed(words, passphrase=pw)
expect = BIP32Node.from_master_secret(seed)
assert got.public_pair() == expect.public_pair()
@pytest.mark.parametrize('pw', [
'a'*1000, # way too big
'a'*100, # just too big
])
def test_b39_fails(dev, pw):
with pytest.raises(CCProtoError):
dev.send_recv(CCProtocolPacker.bip39_passphrase(pw), timeout=None)
def test_b39p_refused(dev, need_keypress, pw='testing 123'):
# user can refuse the passphrase (cancel)
dev.send_recv(CCProtocolPacker.bip39_passphrase(pw), timeout=None)
need_keypress('x')
with pytest.raises(CCUserRefused):
done = None
while done == None:
time.sleep(0.050)
done = dev.send_recv(CCProtocolPacker.get_passphrase_done(), timeout=None)
# EOF

View File

@ -47,5 +47,9 @@ if '-2' in sys.argv:
'_pin2_secret': '000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
})
if '-p' in sys.argv:
sim_defaults['b39pw'] = 'test'
#del sim_defaults['xpub']
#del sim_defaults['xfp']
# EOF