From 284ba1e1a8e997cd1bb3212de0982a97c760980e Mon Sep 17 00:00:00 2001 From: scgbckbone Date: Wed, 6 Jul 2022 18:13:32 +0200 Subject: [PATCH] BIP-85 Passwords & Keyboard emulation --- docs/bip85-passwords.md | 50 +++++++++++++ releases/ChangeLog-mk4.md | 8 +- shared/actions.py | 22 ++++-- shared/address_explorer.py | 2 +- shared/drv_entro.py | 121 ++++++++++++++++++++++++++++-- shared/flow.py | 9 ++- shared/multisig.py | 2 +- shared/nvstore.py | 1 + shared/usb.py | 150 ++++++++++++++++++++++++++++++++++++- shared/ux.py | 9 ++- testing/test_drv_entro.py | 61 ++++++++++++++- testing/test_sign.py | 4 +- 12 files changed, 409 insertions(+), 30 deletions(-) create mode 100644 docs/bip85-passwords.md diff --git a/docs/bip85-passwords.md b/docs/bip85-passwords.md new file mode 100644 index 000000000..0ec5c3702 --- /dev/null +++ b/docs/bip85-passwords.md @@ -0,0 +1,50 @@ +# BIP-85 Passwords + +#### Requirements: +* Coldcard Mk4 with version 5.0.5 or newer +* USB-C with data link (won't work with power only cable from Coinkite) + +This feature derives a deterministic password according [BIP-85](https://github.com/scgbckbone/bips/blob/passwords/bip-0085.mediawiki), from the seed. \ +Generated passwords can be sent as keystrokes via USB to the host computer, +effectively using Coldcard as password manager. + +## Type Passwords + +1. to enable "Type Passwords" feature, connect your Coldcard to host PC with USB cable (check requirements) and go to Settings -> Keyboard EMU -> Enable. +2. go back to top menu and "Type Passwords" option is right below "Address Explorer". +At this point no USB protocol switching happened (can check with `dmesg` ) and Coldcard is still usable in normal USB mode. +3. after you enter "Type Passwords" and press OK in description of the feature, USB +protocol is changed to emulate keyboard ( `Switching...` shown on the screen). +4. choose "Password number" (BIP-85 index) and press OK to generate password +5. at this point password is generated and you can scroll down to check BIP-85 path and password. +To send keystrokes, place mouse at required password prompt and press OK. This will send desired keystrokes plus hit enter at the end. +6. you're back at step 4. nd can continue to generate passwords or you can press X +to exit. Exit from "Type Passwords" will cause Coldcard to turn off keyboard emulation and enable normal USB mode if it was enabled before. Otherwise USB stays disabled. +7. to disable "Type Passwords" feature go to Settings -> Keyboard EMU -> Default Off. +After return to top menu "Type Passwords" is not available. + +## Backup BIP-85 passwords +1. go to Advanced/Tools -> Derive Seed B85 -> Passwords +2. choose "Password/Index number" (BIP-85 index) and press OK to generate password +3. screen is showing generated password, path, and entropy from which password was derived +4. few different options available at this point: + 1. press 1 to save password backup file on MicroSD card + 2. press 2 to send keystrokes (this will first of all enable keyboard emulation, then send keystrokes + enter, and finally disables keyboard emulation) + 3. press 3 to view password as QR code + 4. press 4 to send over NFC (only appears when NFC is enabled) + +## Keyboard language settings +Keys are mapped to specific characters based on your host PC keyboard language settings. +For Coldcard to be able to type correct BIP-85 passwords you MUST use language that fulfil below requirements: +1. has to be `qwerty` + +Passwords generated and shown on Coldcard will always be BIP-85 correct. However +if you send keystrokes, for example on german keyboard, what was typed will not match what was generated on Coldcard. + +For example, correct password is `dKLoepugzdVJvdL56ogNV` but with german or slovak keyboard language settings +what will be typed is `dKLoepugydVJvdL56ogNV`. You can see that german keyboard is not qwerty (y instead of z). + +Even with "exotic" keyboard language settings will Coldcard always send exact same keystrokes (for exact same language settings). +Yet BIP-85 won't be respected. + +It is considered best practice to always adjust your keyboard language settings to meet requirements. For instance English US or English UK. \ No newline at end of file diff --git a/releases/ChangeLog-mk4.md b/releases/ChangeLog-mk4.md index 625955769..8f76476cc 100644 --- a/releases/ChangeLog-mk4.md +++ b/releases/ChangeLog-mk4.md @@ -1,5 +1,11 @@ -## 5.0.5 - 2022-06-21 +## 5.0.5 - 2022-07-14 +- Enhancement: BIP-85 derived passwords. Pick an index number, and COLDCARD will derive + a deterministic, strong (136 bit) password for you, it will even type the password by + emulating a USB keyboard. See new areas: Settings > Keyboard EMU and + Settings > Derive Seed B85 > Passwords. +- Documentation: added `docs/bip85-passwords.md` documenting new BIP-85 passwords and keyboard emulation. +- Enhancement: BIP-85 derived values can now be exported via NFC in addition to QR code. - Enhancement: Allow signing transaction where foreign UTXO(s) is/are missing. Only applies to cases where partial signatures are being added. Thanks to [@straylight-orbit](https://github.com/straylight-orbit) diff --git a/shared/actions.py b/shared/actions.py index b29ca8626..b9b70d4cf 100644 --- a/shared/actions.py +++ b/shared/actions.py @@ -936,9 +936,8 @@ async def start_login_sequence(): if not settings.get('du', 0): from usb import enable_usb enable_usb() - -def goto_top_menu(first_time=False): - # Start/restart menu system + +def make_top_menu(): from menu import MenuSystem from flow import VirginSystem, NormalSystem, EmptyWallet, FactoryMenu from glob import hsm_active @@ -955,7 +954,11 @@ def goto_top_menu(first_time=False): assert pa.is_successful(), "nonblank but wrong pin" m = MenuSystem(EmptyWallet if pa.is_secret_blank() else NormalSystem) + return m +def goto_top_menu(first_time=False): + # Start/restart menu system + m = make_top_menu() the_ux.reset(m) if first_time and not pa.is_secret_blank(): @@ -1064,7 +1067,7 @@ async def electrum_skeleton(*a): account_num = 0 if ch == '1': - account_num = await ux_enter_number('Account Number:', 9999) + account_num = await ux_enter_number('Account Number:', 9999) or 0 elif ch != 'y': return @@ -1094,7 +1097,7 @@ async def bitcoin_core_skeleton(*A): account_num = 0 if ch == '1': - account_num = await ux_enter_number('Account Number:', 9999) + account_num = await ux_enter_number('Account Number:', 9999) or 0 elif ch != 'y': return @@ -1119,7 +1122,7 @@ async def generic_skeleton(*A): single-signer UTXO associated with this Coldcard.''' + SENSITIVE_NOT_SECRET) != 'y': return - account_num = await ux_enter_number('Account Number:', 9999) + account_num = await ux_enter_number('Account Number:', 9999) or 0 # no choices to be made, just do it. import export @@ -1906,6 +1909,13 @@ async def change_usb_disable(dis): # USB disabled, but now should be enable_usb() +async def usb_keyboard_emulation(enable): + # just sets emu flag on and adds Entry Password into top menu + # no USB switching at this point + # - need to force reload of main menu, so it shows/hides + new_top_menu = make_top_menu() + the_ux.stack[0] = new_top_menu # top menu is always element 0 + async def change_nfc_enable(enable): # NFC enable / disable import glob diff --git a/shared/address_explorer.py b/shared/address_explorer.py index 4fedc8db9..431ecc75c 100644 --- a/shared/address_explorer.py +++ b/shared/address_explorer.py @@ -165,7 +165,7 @@ async def render(self): self.replace_items(items) async def change_account(self, *a): - self.account_num = await ux_enter_number('Account Number:', 9999) + self.account_num = await ux_enter_number('Account Number:', 9999) or 0 await self.render() async def pick_single(self, _1, menu_idx, item): diff --git a/shared/drv_entro.py b/shared/drv_entro.py index 67c00d76c..e30614b7c 100644 --- a/shared/drv_entro.py +++ b/shared/drv_entro.py @@ -6,12 +6,15 @@ # Using the system's BIP-32 master key, safely derive seeds phrases/entropy for other # wallet systems, which may expect seed phrases, XPRV, or other entropy. # -import stash, ngu, chains, bip39, version -from ux import ux_show_story, ux_enter_number, the_ux, ux_confirm +import stash, ngu, chains, bip39, version, glob +from ux import ux_show_story, ux_enter_number, the_ux, ux_confirm, ux_dramatic_pause from menu import MenuItem, MenuSystem from ubinascii import hexlify as b2a_hex +from ubinascii import b2a_base64 from serializations import hash160 +BIP85_PWD_LEN = 21 + def drv_entro_start(*a): # UX entry @@ -36,7 +39,7 @@ def drv_entro_start(*a): return choices = [ '12 words', '18 words', '24 words', 'WIF (privkey)', - 'XPRV (BIP-32)', '32-bytes hex', '64-bytes hex'] + 'XPRV (BIP-32)', '32-bytes hex', '64-bytes hex', 'Passwords'] m = MenuSystem([MenuItem(c, f=drv_entro_step2) for c in choices]) the_ux.push(m) @@ -64,6 +67,12 @@ def bip85_derive(picked, index): width = 32 if picked == 5 else 64 path = "m/83696968'/128169'/{width}'/{index}'".format(width=width, index=index) s_mode = 'hex' + elif picked == 7: + width = 64 + # hardcoded width for now + # b"pwd".hex() --> 707764 + path = "m/83696968'/707764'/{pwd_len}'/{index}'".format(pwd_len=BIP85_PWD_LEN, index=index) + s_mode = 'pw' else: raise ValueError(picked) @@ -78,13 +87,34 @@ def bip85_derive(picked, index): return new_secret, width, s_mode, path + +def bip85_pwd(secret): + # Convert raw secret (64 bytes) into type-able password text. + + # See BIP85 specification. + # path --> m/83696968'/707764'/{pwd_len}'/{index}' + # + # Base64 encode whole 64 bytes of entropy. + # Slice pwd_len from base64 encoded string [0:pwd_len] + # we use hardcoded pwd_len=21, which has cca 126 bits of entropy + + # python bas64 puts newline at the end - strip + assert len(secret) == 64 + secret_b64 = b2a_base64(secret).decode().strip() + return secret_b64[:BIP85_PWD_LEN] + async def drv_entro_step2(_1, picked, _2): from glob import dis from files import CardSlot, CardMissingError, needs_microsd the_ux.pop() - - index = await ux_enter_number("Index Number?", 9999) + msg = "Index Number?" + if picked == 7: + # Passwords + msg = "Password Index?" + index = await ux_enter_number(msg, 9999) + if index is None: + return dis.fullscreen("Working...") new_secret, width, s_mode, path = bip85_derive(picked, index) @@ -95,7 +125,12 @@ async def drv_entro_step2(_1, picked, _2): qr = None qr_alnum = False - if s_mode == 'words': + if s_mode == "pw": + pw = bip85_pwd(new_secret) + qr = pw + msg = 'Password:\n' + pw + + elif s_mode == 'words': # BIP-39 seed phrase, various lengths words = bip39.b2a_words(new_secret).split(' ') @@ -150,11 +185,17 @@ async def drv_entro_step2(_1, picked, _2): prompt = '\n\nPress 1 to save to MicroSD card' if encoded is not None: prompt += ', 2 to switch to derived secret' + elif s_mode == 'pw': + prompt += ', 2 to type password over USB' if (qr is not None) and version.has_fatram: - prompt += ', 3 to view as QR code.' + prompt += ', 3 to view as QR code' + if glob.NFC: + prompt += ', 4 to send by NFC' + + prompt += '.' while 1: - ch = await ux_show_story(msg+prompt, sensitive=True, escape='123') + ch = await ux_show_story(msg+prompt, sensitive=True, escape='1234') if ch == '1': # write to SD card: simple text file @@ -177,6 +218,14 @@ async def drv_entro_step2(_1, picked, _2): from ux import show_qr_code await show_qr_code(qr, qr_alnum) continue + elif ch == '2' and s_mode == 'pw': + # gets confirmation then types it + await single_send_keystrokes(qr, path) + continue + elif ch == '4' and glob.NFC and qr: + # Share any of these over NFC + await glob.NFC.share_text(qr) + continue else: break @@ -198,4 +247,60 @@ async def drv_entro_step2(_1, picked, _2): if encoded is not None: stash.blank_object(encoded) + +async def password_entry(*args, **kwargs): + from glob import dis + from usb import EmulatedKeyboard + + # TODO: maybe a way to kill this info dialog w/ a setting + ch = await ux_show_story('''\ +Type Passwords (BIP-85) + +This feature derives a deterministic password according BIP-85, from the seed. \ +The password will be sent as keystrokes via USB to the host computer.''') + if ch != 'y': + return + + with EmulatedKeyboard() as kbd: + if await kbd.connect(): return + + while True: + the_ux.pop() + index = await ux_enter_number("Password Index?", 9999, can_cancel=True) + if index is None: + break + + dis.fullscreen("Working...") + new_secret, _, _, path = bip85_derive(7, index) + pw = bip85_pwd(new_secret) + + await send_keystrokes(kbd, pw, path) + + the_ux.pop() # WHY? + +async def send_keystrokes(kbd, password, path): + ch = await ux_show_story( + "Place mouse at required password prompt, then press OK to send keystrokes.\n\n" + "Password:\n%s\n\n" + "Path:\n%s" % (password, path), + ) + + if ch == 'y': + await kbd.send_keystrokes(password + '\r') + + await ux_dramatic_pause("Sent.", 0.250) + return True + + await ux_dramatic_pause("Aborted.", 1) + + return False + +async def single_send_keystrokes(password, path): + # switches to USB mode required, then does send + from usb import EmulatedKeyboard + + with EmulatedKeyboard() as kbd: + if await kbd.connect(): return + await send_keystrokes(kbd, password, path) + # EOF diff --git a/shared/flow.py b/shared/flow.py index 56fa00376..46dd657c5 100644 --- a/shared/flow.py +++ b/shared/flow.py @@ -11,7 +11,7 @@ from multisig import make_multisig_menu from address_explorer import address_explore from users import make_users_menu -from drv_entro import drv_entro_start +from drv_entro import drv_entro_start, password_entry from backups import clone_start, clone_write_data from xor_seed import xor_split_start, xor_restore_start from countdowns import countdown_pin_submenu, countdown_chooser @@ -132,6 +132,12 @@ def vdisk_enabled(): MS-DOS tools should not be able to find the PSBT data (ie. undelete), but forensic tools \ which take apart the flash chips of the SDCard may still be able to find the \ data or filenames.'''), + ToggleMenuItem('Keyboard EMU', 'emu', ['Default Off', 'Enable'], + on_change=usb_keyboard_emulation, + predicate=has_secrets, # cannot generate BIP85 passwords without secret + story='''This mode adds a top-level menu item for typing \ +deterministically-generated passwords (BIP-85), directly into an \ +attached USB computer (as an emulated keyboard).'''), ] XpubExportMenu = [ @@ -325,6 +331,7 @@ def vdisk_enabled(): MenuItem('Passphrase', f=start_b39_pw, predicate=lambda: settings.get('words', True)), MenuItem('Start HSM Mode', f=start_hsm_menu_item, predicate=hsm_policy_available), MenuItem("Address Explorer", f=address_explore), + MenuItem('Type Passwords', f=password_entry, predicate=lambda: settings.get("emu", False) and has_secrets()), MenuItem('Secure Logout', f=logout_now), MenuItem('Advanced/Tools', menu=AdvancedNormalMenu), MenuItem('Settings', menu=SettingsMenu), diff --git a/shared/multisig.py b/shared/multisig.py index 15b6fe005..34d39cf61 100644 --- a/shared/multisig.py +++ b/shared/multisig.py @@ -1313,7 +1313,7 @@ async def export_multisig_xpubs(*a): ch = await ux_show_story(msg, escape='3') if ch == 'x': return - acct_num = await ux_enter_number('Account Number:', 9999) + acct_num = await ux_enter_number('Account Number:', 9999) or 0 todo = [ ( "m/45'", 'p2sh', AF_P2SH), # iff acct_num == 0 diff --git a/shared/nvstore.py b/shared/nvstore.py index 99cf91989..82e5a9ad4 100644 --- a/shared/nvstore.py +++ b/shared/nvstore.py @@ -51,6 +51,7 @@ # tp = (complex) trick pins' config on Mk4 # nfc = (bool) if set, enable the NFC feature; default is OFF=>DISABLED (mk4+) # vdsk = (bool) if set, enable the Virtual Disk features; default is OFF=>DISABLED (mk4+) +# emu = (bool) if set, enables the USB Keyboard emulation (BIP-85 password entry) # Stored w/ key=00 for access before login # _skip_pin = hard code a PIN value (dangerous, only for debug) # nick = optional nickname for this coldcard (personalization) diff --git a/shared/usb.py b/shared/usb.py index ed3bae85a..2af022187 100644 --- a/shared/usb.py +++ b/shared/usb.py @@ -8,9 +8,7 @@ from public_constants import MAX_MSG_LEN, MAX_BLK_LEN, AFC_SCRIPT from public_constants import STXN_FLAGS_MASK from ustruct import pack, unpack_from -from ubinascii import hexlify as b2a_hex from ckcc import watchpoint, is_simulator -import uselect as select from utils import problem_file_line, call_later_ms from version import has_fatram, is_devmode, has_psram, MAX_TXN_LEN, MAX_UPLOAD_LEN from exceptions import FramingError, CCBusyError, HSMDenied @@ -63,6 +61,16 @@ # singleton instance of USBHandler() handler = None +def enable_keyboard_emulation(): + if is_simulator(): + enable_usb() + else: + # real device + pyb.usb_mode('VCP+HID', hid=pyb.hid_keyboard) + global handler + if not handler: + handler = USBHandler() + def enable_usb(): # We can't change it on the fly; must be disabled before here # - only one combo of subclasses can be used during a single power-up cycle @@ -71,17 +79,24 @@ def enable_usb(): print("USB already enabled: %s" % cur) else: # subclass, protocol, max packet length, polling interval, report descriptor - hid_info = (0x0, 0x0, 64, 5, hid_descp ) + hid_info = (0x0, 0x0, 64, 5, hid_descp) classes = 'VCP+HID' if not has_psram else 'VCP+MSC+HID' pyb.usb_mode(classes, vid=COINKITE_VID, pid=CKCC_PID, hid=hid_info) global handler if not handler: handler = USBHandler() - from imptask import IMPT + from imptask import IMPT + if "USB" not in IMPT.tasks: IMPT.start_task('USB', handler.usb_hid_recv()) def disable_usb(): + from imptask import IMPT + + task_usb = IMPT.tasks.pop("USB", None) + if task_usb: + task_usb.cancel() + # pull the plug pyb.usb_mode(None) @@ -838,4 +853,131 @@ def handle_bag_number(self, bag_num): return b'asci' + val +class EmulatedKeyboard: + # be a context manager, used during kbd emulation + + def __enter__(self): + return self + + async def connect(self): + # can be slow; needs to wait until host has enumerated us + # - does UX for that + # - can fail when host doesn't want to enumerate us, shows msg + # - returns T if problem + from glob import dis + + dis.fullscreen("Switching...") + + if is_simulator(): return + + # if USB was enabled, reset is needed + disable_usb() + enable_keyboard_emulation() + + # wait for emeration, with timeout + self.dev = pyb.USB_HID() + + for retry in range(100): + rv = self.dev.send(bytes(8)) + if rv == 8: break + await sleep_ms(10) + + # macOS at least: need twice to be sure + for retry in range(100): + rv = self.dev.send(bytes(8)) + if rv == 8: return + await sleep_ms(10) + + # if we are connected to a COLDPOWER, for example, this will happen. + from ux import ux_show_story + ux_show_story("USB Host computer is not enumerating us.", title="FAILED") + + return True + + def __exit__(self, exc_type, exc_val, exc_tb): + # disable keyboard emulation mode (all USB) + from glob import settings + + disable_usb() + + if not settings.get("du"): + # enable usb only if it was previously enabled, otherwise keep disabled + enable_usb() + + async def send_keystrokes(self, keystroke_string): + # Send keystrokes to enter a password... only expected to support Base64 charset + if is_simulator(): + print("Simulating keystrokes: " + repr(keystroke_string)) + return + + # page 88+ + char_map = { + "a": 0x04, + "b": 0x05, + "c": 0x06, + "d": 0x07, + "e": 0x08, + "f": 0x09, + "g": 0x0A, + "h": 0x0B, + "i": 0x0C, + "j": 0x0D, + "k": 0x0E, + "l": 0x0F, + "m": 0x10, + "n": 0x11, + "o": 0x12, + "p": 0x13, + "q": 0x14, + "r": 0x15, + "s": 0x16, + "t": 0x17, + "u": 0x18, + "v": 0x19, + "w": 0x1A, + "x": 0x1B, + "y": 0x1C, # qwerty + "z": 0x1D, + # numbers (keypad) + "1": 0x59, + "2": 0x5A, + "3": 0x5B, + "4": 0x5C, + "5": 0x5D, + "6": 0x5E, + "7": 0x5F, + "8": 0x60, + "9": 0x61, + "0": 0x62, + # only symbols required for b64 without padding + "+": 0x57, # Keypad + "/": 0x54, # Keypad + # Keyboard Enter + "\r": 0x28, + } + buf = bytearray(8) + + for ch in keystroke_string: + cap = False + if ch in char_map: + to_press = char_map[ch] + else: + cap = True + to_press = char_map[ch.lower()] + + buf[2] = to_press + if cap: + # set LEFT SHIFT for capital letters + buf[0] = 0x02 + + while self.dev.send(buf) == 0: + await sleep_ms(5) + + # all keys up + buf[2] = 0x00 + buf[0] = 0x00 + + while self.dev.send(buf) == 0: + await sleep_ms(5) + # EOF diff --git a/shared/ux.py b/shared/ux.py index c44112200..fb56ae82c 100644 --- a/shared/ux.py +++ b/shared/ux.py @@ -363,7 +363,7 @@ async def show_qr_code(data, is_alnum): o = QRDisplaySingle([data], is_alnum) await o.interact_bare() -async def ux_enter_number(prompt, max_value): +async def ux_enter_number(prompt, max_value, can_cancel=False): # return the decimal number which the user has entered # - default/blank value assumed to be zero # - clamps large values to the max @@ -380,7 +380,8 @@ async def ux_enter_number(prompt, max_value): dis.clear() dis.text(0, 0, prompt) - dis.text(None, -1, "X to DELETE, or OK when DONE.", FontTiny) + dis.text(None, -1, ("X to CANCEL, or OK when DONE." if can_cancel else + "X to DELETE, or OK when DONE."), FontTiny) dis.save() while 1: @@ -404,9 +405,9 @@ async def ux_enter_number(prompt, max_value): elif ch == 'x': if value: value = value[0:-1] - else: + elif can_cancel: # quit if they press X on empty screen - return 0 + return None else: if len(value) == max_w: value = value[0:-1] + ch diff --git a/testing/test_drv_entro.py b/testing/test_drv_entro.py index 194606ce3..82f1cc86e 100644 --- a/testing/test_drv_entro.py +++ b/testing/test_drv_entro.py @@ -36,6 +36,10 @@ ('64-bytes hex', 0, None, '492db4698cf3b73a5a24998aa3e9d7fa96275d85724a91e71aa2d645442f878555d078fd1f1f67e368976f04137b1f7a0d19232136ca50c44614af72b5582a5c'), + + ('Passwords', 0, + None, + "dKLoepugzdVJvdL56ogNV"), ]) def test_bip_vectors(mode, index, entropy, expect, set_encoded_secret, dev, cap_menu, pick_menu_item, @@ -80,7 +84,7 @@ def test_bip_vectors(mode, index, entropy, expect, do_import = False - if 'words' in mode: + if ' words' in mode: num_words = int(mode.split()[0]) assert f'Seed words ({num_words}):' in story assert f"m/83696968'/39'/0'/{num_words}'/{index}'" in story @@ -108,6 +112,12 @@ def test_bip_vectors(mode, index, entropy, expect, assert f"m/83696968'/128169'/{width}'/{index}'" in story assert expect in story + elif 'Passwords' == mode: + assert "Password:" in story + assert f"m/83696968'/707764'/21'/{index}'" in story + assert expect in story + assert "2 to type password over USB" in story + else: raise ValueError(mode) @@ -260,6 +270,53 @@ def test_path_index(mode, pattern, index, elif 'WIF' in mode: assert qr == got - + + +def test_type_passwords(dev, cap_menu, pick_menu_item, + goto_home, cap_story, need_keypress, cap_screen +): + goto_home() + pick_menu_item('Settings') + pick_menu_item('Keyboard EMU') + _, story = cap_story() + story1 = "This mode adds a top-level menu item for typing deterministically-generated passwords (BIP-85), directly into an attached USB computer (as an emulated keyboard)." + assert story1 == story + need_keypress("y") + pick_menu_item('Enable') + time.sleep(0.3) + goto_home() + menu = cap_menu() + assert "Type Passwords" in menu + pick_menu_item("Type Passwords") + time.sleep(1) + _, story = cap_story() + story2 = 'Type Passwords (BIP-85)\n\nThis feature derives a deterministic password according BIP-85, from the seed. The password will be sent as keystrokes via USB to the host computer.' + assert story == story2 + need_keypress("y") + time.sleep(0.5) + # here we accessed index loop and can derive + for index in [0, 10, 100, 1000, 9999]: + time.sleep(0.5) + for n in str(index): + need_keypress(n) + need_keypress("y") + time.sleep(1) + _, story = cap_story() + assert "Place mouse at required password prompt, then press OK to send keystrokes." in story + split_story = story.split("\n\n") + _, pwd = split_story[1].split("\n") + _, path = split_story[2].split("\n") + assert path == f"m/83696968'/707764'/21'/{index}'" + assert len(pwd) == 21 + assert "=" not in pwd + need_keypress("y") # does nothing on simulator + time.sleep(0.2) + # exit Enter Password menu + need_keypress("x") + pick_menu_item('Settings') + pick_menu_item('Keyboard EMU') + pick_menu_item('Default Off') + menu = cap_menu() + assert "Type Passwords" not in menu # EOF diff --git a/testing/test_sign.py b/testing/test_sign.py index 681d52a28..ecd520d08 100644 --- a/testing/test_sign.py +++ b/testing/test_sign.py @@ -1670,9 +1670,9 @@ def test_bitcoind_missing_foreign_utxo(bitcoind, bitcoind_d_sim_watch, microsd_p assert orig != res # coldcard signs no problem - only our UTXO matters for signing # now alice and bob UTXOs are still missing but bitcoind does not care either # lets sign with bob first - bobs wallet will ignore missing alice UTXO but will supply his UTXO - psbt1 = bob.walletprocesspsbt(base64.b64encode(res).decode())["psbt"] + psbt1 = bob.walletprocesspsbt(base64.b64encode(res).decode(), True, "ALL")["psbt"] # finally sign with alice - res = alice.walletprocesspsbt(psbt1) + res = alice.walletprocesspsbt(psbt1, True, "ALL") psbt2 = res["psbt"] assert res["complete"] is True tx = alice.finalizepsbt(psbt2)["hex"]