Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions docs/bip85-passwords.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 7 additions & 1 deletion releases/ChangeLog-mk4.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
22 changes: 16 additions & 6 deletions shared/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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():
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion shared/address_explorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
121 changes: 113 additions & 8 deletions shared/drv_entro.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)

Expand All @@ -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)
Expand All @@ -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(' ')

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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
9 changes: 8 additions & 1 deletion shared/flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion shared/multisig.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions shared/nvstore.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading