BIP39 passphrase additions, no UX yet
This commit is contained in:
parent
1454dca96a
commit
6302fd88fb
2
external/ckcc-protocol
vendored
2
external/ckcc-protocol
vendored
@ -1 +1 @@
|
||||
Subproject commit 45878fe6e9d7f15d922126c6bdba73457cf5afee
|
||||
Subproject commit c3c3cc4c6e8cfa407c2fb9f26c3097d58e124605
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
148
testing/bip39-vectors.json
Normal 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"
|
||||
]
|
||||
]
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -5,3 +5,4 @@ pytest
|
||||
pycoin==0.80
|
||||
python-bitcoinrpc>=1.0
|
||||
pyserial
|
||||
mnemonic==0.18
|
||||
|
||||
108
testing/test_bip39pw.py
Normal file
108
testing/test_bip39pw.py
Normal 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
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user