# (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC. # # tests for ../shared/notes.py # import pytest, time, json, random, os, pdb from helpers import prandom from charcodes import * from constants import AF_CLASSIC, AF_P2WPKH_P2SH, AF_P2WPKH, simulator_fixed_words from bbqr import split_qrs from ckcc.protocol import CCProtocolPacker from bip32 import BIP32Node from mnemonic import Mnemonic # All tests in this file are exclusively meant for Q # @pytest.fixture(autouse=True) def THIS_FILE_requires_q1(is_q1): if not is_q1: raise pytest.skip('Q1 only') @pytest.fixture def goto_notes(cap_story, cap_menu, press_select, goto_home, pick_menu_item): # drill to the notes menu def doit(item=None): mt = 'Secure Notes & Passwords' goto_home() # TODO this is probably why all menus are properly generated m = cap_menu() if mt in m: pick_menu_item(mt) else: pick_menu_item('Advanced/Tools') pick_menu_item(mt) title, story = cap_story() if title == 'Secure Notes': # enable feature press_select() if item: pick_menu_item(item) return doit @pytest.fixture def need_some_notes(is_q1, settings_get, settings_set): # create a note or use what's there, provide as obj def doit(title='Title Here', body='Body'): assert is_q1 notes = settings_get('notes', []) if not notes: settings_set('notes', [dict(misc=body, title=title)]) settings_set('secnap', True) return notes return doit @pytest.fixture def need_some_passwords(settings_get, settings_set): def doit(): notes = settings_get('notes', []) if not any(1 for n in notes if n.get('password', False)): notes.extend([ {'misc': 'More Notes AAAA', 'password': 'fds65fd5f1sd51s', 'site': 'https://a.com', 'title': 'A', 'user': 'AAA'}, {'misc': 'More Notes BBB', 'password': 'default', 'site': 'www.site.b.com', 'title': 'B-Title', 'user': 'Buzzer'} ]) settings_set('notes', notes) settings_set('secnap', True) return notes return doit @pytest.fixture def delete_note(press_select, goto_notes, cap_menu, pick_menu_item, cap_story): def doit(n_title): goto_notes() m = cap_menu() found = [i for i in m if f': {n_title}' in i] assert found pick_menu_item(found[-1]) pick_menu_item('Delete') title, story = cap_story() assert 'SURE' in title assert 'Everything about this' in story # back to top notes menu press_select() return doit @pytest.fixture def build_note(goto_notes, pick_menu_item, enter_text, cap_menu, cap_story, need_keypress, cap_screen_qr, readback_bbqr, nfc_read_text, press_select, press_cancel, is_headless, nfc_disabled): def doit(n_title, n_body, group=None): # we don't try to preserve leading/trailing spaces on note bodies n_body= n_body.strip() goto_notes('New Note') # create enter_text(n_title) enter_text(n_body, multiline=True) if group: pick_menu_item('New Group') enter_text(group) else: pick_menu_item('(none)') # view time.sleep(0.1) m = cap_menu() assert m[0] == f'"{n_title}"' assert m[1] == 'View Note' assert m[2] == 'Edit' assert m[3] == 'Delete' assert m[4] == 'Export' # test readback for mi in ['View Note', f'"{n_title}"']: time.sleep(0.1) pick_menu_item(mi) time.sleep(1) title, story = cap_story() assert title == n_title assert story == n_body if not is_headless: need_keypress(KEY_QR) qr_rb = cap_screen_qr().decode('utf-8') assert qr_rb == n_body press_cancel() # hidden QR button on menu feature m = cap_menu() assert m[1] == 'View Note' if not is_headless: need_keypress(KEY_QR) qr_rb = cap_screen_qr().decode('utf-8') assert qr_rb == n_body press_cancel() # hidden NFC button on menu feature m = cap_menu() assert m[1] == 'View Note' if not nfc_disabled: need_keypress(KEY_NFC) time.sleep(.1) nfc_rb = nfc_read_text() time.sleep(.1) assert nfc_rb == n_body press_cancel() # export pick_menu_item('Export') title, story = cap_story() assert 'Export' in title assert 'to save note to SD' in story assert 'to show QR' in story assert 'WARNING' in story assert 'will be cleartext' in story need_keypress(KEY_QR) file_type, data = readback_bbqr() assert file_type == 'J' obj = json.loads(data) assert obj.keys() == {'coldcard_notes'} obj = obj['coldcard_notes'] assert len(obj) == 1 obj = obj[0] assert obj['title'] == n_title assert obj['misc'] == n_body if group: assert obj['group'] == group else: assert 'group' not in obj # back to top notes menu press_select() return doit @pytest.fixture def build_password(goto_notes, pick_menu_item, enter_text, cap_menu, cap_story, need_keypress, cap_screen_qr, nfc_read_text, cap_text_box, settings_get, settings_set, scan_a_qr, press_select, press_cancel, is_headless): def doit(n_title, n_user=None, n_pw='secret', n_site=None, n_body=None, key_pw=None, group=None): goto_notes('New Password') enter_text(n_title) if n_user: enter_text(n_user) else: press_select() if key_pw and key_pw == KEY_QR: need_keypress(KEY_QR) time.sleep(1.1) scan_a_qr(n_pw) time.sleep(1.1) elif key_pw: # function keys: let it auto gen need_keypress(key_pw) time.sleep(0.1) if key_pw == KEY_F5: # bip-85 enter_text('34') time.sleep(0.1) n_pw = ''.join(cap_text_box()).strip() assert n_pw and len(n_pw) > 10 need_keypress(KEY_ENTER) else: enter_text(n_pw) if n_site: enter_text(n_site) else: press_select() if n_body: enter_text(n_body, multiline=True) else: press_cancel() if group: pick_menu_item('New Group') enter_text(group) else: pick_menu_item('(none)') # view time.sleep(0.1) m = cap_menu() N = 1 assert m[0] == f'"{n_title}"' if n_user and not n_site: assert n_user in m[1] N += 1 elif n_site and not n_user: assert n_site in m[1] N += 1 elif n_site and n_user: assert n_user in m[1] assert n_site in m[2] N += 2 assert 'View Password' in m assert 'Send Password' in m assert 'Export' in m assert 'Edit Metadata' in m assert 'Delete' in m assert 'Change Password' in m # top 3 menu items do same thing: view details for idx in range(N): pick_menu_item(m[idx]) title, story = cap_story() assert title == n_title if n_user: assert f'User: {n_user}' in story if n_site: assert f'Site: {n_site}' in story assert 'Password: (' in story if n_body: assert 'Notes:' in story assert story.endswith(n_body) need_keypress(KEY_CANCEL) # view pw as text and QR pick_menu_item('View Password') title, story = cap_story() assert title == n_title assert story == n_pw if not is_headless: need_keypress(KEY_QR) qr_rb = cap_screen_qr().decode('utf-8') assert qr_rb == n_pw need_keypress(KEY_CANCEL) return doit @pytest.fixture def change_password(goto_notes, pick_menu_item, enter_text, cap_story, need_keypress, settings_get, press_select, press_cancel, cap_menu): def doit(id_title, new_title=None, new_username=None, new_site=None, new_misc=None, new_password=None, new_group=None): goto_notes() m = cap_menu() found = [i for i in m if f': {id_title}' in i] assert found pick_menu_item(found[0]) if new_title or new_username or new_site or new_misc: need_in_story = [] pick_menu_item('Edit Metadata') if new_title: enter_text(KEY_CLEAR + new_title) need_in_story.append('Title') else: press_select() if new_username: enter_text(KEY_CLEAR + new_username) need_in_story.append('Username') else: press_select() if new_site: enter_text(KEY_CLEAR + new_site) need_in_story.append('Site Name') else: press_select() if new_misc: enter_text(KEY_CLEAR + new_misc, multiline=True) need_in_story.append('Other Notes') else: press_cancel() if new_group is not None: if new_group: if new_group in cap_menu(): pick_menu_item(new_group) else: pick_menu_item('New Group') enter_text(new_group) else: pick_menu_item('(none)') need_in_story.append('Group') else: press_cancel() # approve change time.sleep(0.1) title, story = cap_story() assert 'SURE' in title for i in need_in_story: assert i in story need_keypress(KEY_ENTER) if new_password: pick_menu_item('Change Password') enter_text(KEY_CLEAR + new_password) # confirm time.sleep(0.1) title, story = cap_story() assert 'Confirm' in title assert 'New Password' in story assert 'Old Password' in story assert new_password in story need_keypress(KEY_ENTER) # test changes at low-level time.sleep(0.1) notes = settings_get('notes') note = [n for n in notes if n['title'] == (new_title or id_title)][0] assert note if new_site: assert note['site'] == new_site if new_username: assert note['user'] == new_username if new_misc: assert note['misc'] == new_misc if new_password: assert note['password'] == new_password if new_group is not None: assert note.get('group', '') == new_group return doit @pytest.mark.parametrize('n_title', [ 'a', 'aaa', 'b'*32]) @pytest.mark.parametrize('n_body', [ 'short', 'very long '*30, 'mOKa', 'x X x']) def test_build_note(n_title, n_body, build_note, delete_note): build_note(n_title, n_body) delete_note(n_title) @pytest.mark.parametrize('size', [ 4000, 30000]) @pytest.mark.parametrize('encoding', '2Z' ) def test_huge_notes(size, encoding, goto_notes, enter_text, cap_menu, need_keypress, scan_a_qr, settings_set, settings_get, pick_menu_item): # Since we don't limit note sizes, by request of NVK ... test them n_body = ''.join(chr((i%95) + 32) for i in prandom(size)) n_title = f'Size {size} {random.randint(100000, 999999)}' # kill old things, enable feature settings_set('notes', []) goto_notes('New Note') enter_text(n_title) # use BBRq to import body -- fast and verbatim need_keypress(KEY_QR) actual_vers, parts = split_qrs(n_body, 'U', max_version=20, encoding=encoding) random.shuffle(parts) for p in parts: scan_a_qr(p) time.sleep(2.0 / len(parts)) # just so we can watch time.sleep(.5) # decompression time in some cases pick_menu_item('(none)') m = cap_menu() assert 'Export' in m notes = settings_get('notes') assert len(notes) == 1 assert notes[0]['title'] == n_title assert notes[0]['misc'] == n_body settings_set('notes', []) goto_notes() # redraw @pytest.mark.parametrize('key', [None, KEY_QR]) @pytest.mark.parametrize('site', ["https://feed.org", None]) @pytest.mark.parametrize('user', [None, "joe"]) @pytest.mark.parametrize('misc', [None, "bla bla bla bla"]) def test_password_flow(key, site, user, misc, build_password, change_password): # Test password entry, including all the auto-generation capabilities title = os.urandom(4).hex() build_password(n_title=title, n_user=user, n_pw='A'*99, n_site=site, n_body=misc, key_pw=None) change_password(id_title=title, new_username="changed" if user is None else None, new_site="https://changed.org" if site is None else None, new_misc="new bla newer bla newest bla" if misc is None else None) @pytest.mark.parametrize('key', [KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5]) def test_password_flow_gen(key, build_password, change_password): title = os.urandom(4).hex() build_password(n_title=title, n_pw='B'*3, key_pw=key) change_password(id_title=title, new_password="changed") def test_password_change_title(build_password, change_password): build_password(n_title="old_title", n_pw="default") change_password(id_title="old_title", new_title="new_title") def test_top_export(goto_notes, pick_menu_item, cap_story, need_keypress, settings_get, readback_bbqr, need_some_notes): notes = settings_get('notes', []) if not len(notes): notes = need_some_notes() goto_notes() pick_menu_item('Export All') title, story = cap_story() assert 'Export' in title assert 'to SD Card' in story assert 'to show QR' in story assert 'WARNING' in story assert 'will be cleartext' in story need_keypress(KEY_QR) file_type, data = readback_bbqr() assert file_type == 'J' obj = json.loads(data) assert obj.keys() == {'coldcard_notes'} assert obj['coldcard_notes'] == notes need_keypress(KEY_ENTER) def test_sort_by_title(goto_notes, pick_menu_item, cap_story, need_keypress, settings_get, settings_set, build_note, cap_menu, build_password): settings_set('notes', []) build_note('ZZZ', 'b1') goto_notes() assert 'Sort By Title' not in cap_menu() build_note('MMM', 'b2') build_note('AAA', 'b3') build_note('mmm', 'b2') build_note('Aaa', 'b3') build_password('Bbb') notes = settings_get('notes') goto_notes() pick_menu_item('Sort By Title') # effect is immedate after = settings_get('notes', []) assert sorted((i['title'] for i in after), key=lambda i:i.lower()) \ == [i['title'] for i in after] def test_grouped_note_menu(settings_set, settings_get, goto_notes, cap_menu, pick_menu_item, build_note, press_cancel, press_select): settings_set('notes', []) settings_set('secnap', True) build_note('group-note', 'body', group='Work') notes = settings_get('notes') assert notes[-1]['group'] == 'Work' goto_notes() m = cap_menu() assert '↳ Work' in m assert not any(': group-note' in i for i in m) press_select() m = cap_menu() assert '1: group-note' in m press_cancel() def test_grouped_password_menu(settings_set, settings_get, goto_notes, cap_menu, pick_menu_item, build_password, press_cancel, press_select): settings_set('notes', []) settings_set('secnap', True) build_password('group-pw', n_pw='secret', group='Accounts') press_cancel() notes = settings_get('notes') assert notes[-1]['group'] == 'Accounts' goto_notes() m = cap_menu() assert '↳ Accounts' in m assert not any(': group-pw' in i for i in m) press_select() m = cap_menu() assert '1: group-pw' in m press_cancel() def test_grouped_and_ungrouped_menu(settings_set, goto_notes, cap_menu, pick_menu_item, press_cancel): settings_set('secnap', True) settings_set('notes', [ {'title': 'loose-note', 'misc': 'aaa'}, {'title': 'work-note', 'misc': 'bbb', 'group': 'Work'}, {'title': 'work-pw', 'misc': '', 'password': 'secret', 'site': '', 'user': '', 'group': 'Work'}, ]) goto_notes() m = cap_menu() assert '1: loose-note' in m assert '↳ Work' in m assert not any(': work-note' in i for i in m) assert not any(': work-pw' in i for i in m) pick_menu_item('↳ Work') m = cap_menu() assert '2: work-note' in m assert '3: work-pw' in m press_cancel() def test_new_grouped_note_cancel_lands_in_group(settings_set, goto_notes, cap_menu, pick_menu_item, enter_text, press_cancel): settings_set('notes', []) settings_set('secnap', True) goto_notes('New Note') enter_text('new-note') enter_text('body', multiline=True) pick_menu_item('New Group') enter_text('Work') assert '"new-note"' in cap_menu() press_cancel() m = cap_menu() assert '1: new-note' in m assert 'New Note' not in m press_cancel() m = cap_menu() assert '↳ Work' in m assert '1: new-note' not in m def test_edit_note_group_moves(settings_set, settings_get, goto_notes, cap_menu, pick_menu_item, enter_text, press_select, press_cancel, cap_story): settings_set('secnap', True) settings_set('notes', [{'title': 'move-note', 'misc': 'body'}]) goto_notes() pick_menu_item('1: move-note') pick_menu_item('Edit') press_select() # unchanged title press_cancel() # unchanged note body pick_menu_item('New Group') enter_text('Work') time.sleep(.1) title, story = cap_story() assert "SURE" in title assert 'Group' in story press_select() time.sleep(.1) goto_notes() m = cap_menu() assert '↳ Work' in m assert '1: move-note' not in m press_select() pick_menu_item('1: move-note') pick_menu_item('Edit') press_select() press_cancel() pick_menu_item('New Group') enter_text('Home') time.sleep(.1) title, story = cap_story() assert 'SURE' in title assert 'Group' in story press_select() goto_notes() m = cap_menu() assert '↳ Work' not in m assert '↳ Home' in m press_select() pick_menu_item('1: move-note') pick_menu_item('Edit') press_select() press_cancel() pick_menu_item('(none)') time.sleep(.1) title, story = cap_story() assert 'SURE' in title assert 'Group' in story press_select() press_cancel() m = cap_menu() assert '↳ Home' not in m assert '1: move-note' in m assert '(none)' not in m def test_old_records_without_group(settings_set, settings_get, goto_notes, cap_menu): settings_set('secnap', True) settings_set('notes', [{'title': 'old-note', 'misc': 'body'}]) goto_notes() assert '1: old-note' in cap_menu() assert settings_get('notes')[0].get('group', '') == '' def test_top_import(goto_notes, cap_menu, cap_story, need_keypress, settings_get, settings_set, scan_a_qr, need_some_notes): # make some notes = need_some_notes() # wipe them settings_set('notes', []) goto_notes('Import') title, story = cap_story() assert 'Import' in title assert 'from SD Card' in story assert 'to scan QR' in story assert 'WARNING' not in story jj = json.dumps(dict(coldcard_notes=notes)) need_keypress(KEY_QR) _, parts = split_qrs(jj, 'J', max_version=20) random.shuffle(parts) for p in parts: scan_a_qr(p) time.sleep(.5) # decompression time in some cases m = cap_menu() for _ in range(3): if "1:" in m[0]: break time.sleep(.2) m = cap_menu() continue mm = [n.split(":")[-1].strip() for n in m if ":" in n] for note in notes: assert note['title'] in mm assert settings_get('notes', 'MISSING') == notes goto_notes() def test_top_import_u_typed_json(goto_notes, cap_menu, cap_story, need_keypress, settings_get, settings_set, scan_a_qr): settings_set('notes', []) goto_notes('Import') need_keypress(KEY_QR) notes = {"coldcard_notes": [{"title": "demo", "misc": "x"}]} jj = json.dumps(notes) _, parts = split_qrs(jj, 'U', max_version=20) # deliberately U-typed for p in parts: scan_a_qr(p) time.sleep(.5) m = cap_menu() for _ in range(3): if "1:" in m[0]: break time.sleep(.2) m = cap_menu() assert settings_get('notes') == notes["coldcard_notes"] goto_notes() @pytest.mark.parametrize('qr,title', [ ('otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30', 'ACME Co:john.doe@email.com'), ('otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi', 'pi@raspberrypi'), ('otpauth-migration://offline?data=CiAKCghCEIa1rWta1rUSDEV4YW1wbGUgRGF0YSABKAEwAhAB', 'Google Auth'), ]) def test_top_qr(qr, title, goto_notes, pick_menu_item, cap_menu, cap_story, need_keypress, settings_get, settings_set, scan_a_qr): # import some fun QR codes (will be notes) from top-level, undocumented goto_notes() need_keypress(KEY_QR) scan_a_qr(qr) time.sleep(1) # lazy readback notes = settings_get('notes', []) assert notes[-1]['title'] == title assert notes[-1]['misc'] == qr #pick_menu_item('Delete') #need_keypress(KEY_ENTER) def test_top_disable(goto_notes, pick_menu_item, cap_menu, settings_get, settings_set): # Keep last - disables, deletes notes settings_set('notes', []) goto_notes() m = cap_menu() assert 'Disable Feature' in m pick_menu_item('Disable Feature') m = cap_menu() assert 'Ready To Sign' in m assert settings_get('notes', 'MISSING') == 'MISSING' def test_tmp_notes_separation(goto_notes, pick_menu_item, generate_ephemeral_words, build_note, build_password, seed_vault_enable, cap_menu, restore_main_seed, press_select, goto_home): seed_vault_enable() goto_notes() # create some notes in master settings build_note(n_title="note-master", n_body="Master seed note meta") build_password(n_title="pwd-master", n_user="ccu", n_pw="fdshjd76342gdhj", n_body="WIF: 5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ") # switch to random ephemeral seed generate_ephemeral_words(12, from_main=True, seed_vault=True) time.sleep(.1) goto_notes() m = cap_menu() assert len(m) == 4 # EMPTY - no saved notes build_note(n_title="note-tmp", n_body="Temporary seed note meta") build_password(n_title="pwd-tmp", n_user="ttu", n_pw="n7c4tvb6erdgg8", n_body="HEX: 800C28FCA386C7A227600B2FE50B7CAE11EC86D3BF1FBE471BE89827E19D72AA1D507A5B8D") # switch to yet another random ephemeral seed generate_ephemeral_words(24, from_main=False, seed_vault=True) time.sleep(.1) goto_notes() m = cap_menu() assert len(m) == 4 # EMPTY - no saved notes build_note(n_title="note-tmp2", n_body="Second Temporary seed note meta") # back to master restore_main_seed(seed_vault=True) time.sleep(.1) goto_notes() m = cap_menu() mm = [n.split(":")[-1].strip() for n in m if ":" in n] assert 'note-master' in mm assert 'pwd-master' in mm assert 'note-tmp' not in mm assert 'pwd-tmp' not in mm assert 'note-tmp2' not in mm goto_home() pick_menu_item("Seed Vault") time.sleep(.1) m = cap_menu() # first tmp pick_menu_item(m[0]) pick_menu_item("Use This Seed") press_select() goto_notes() m = cap_menu() mm = [n.split(":")[-1].strip() for n in m if ":" in n] assert 'note-master' not in mm assert 'pwd-master' not in mm assert 'note-tmp' in mm assert 'pwd-tmp' in mm assert 'note-tmp2' not in mm goto_home() pick_menu_item("Seed Vault") time.sleep(.1) m = cap_menu() # second tmp (directly from first tmp) pick_menu_item(m[1]) pick_menu_item("Use This Seed") press_select() goto_notes() m = cap_menu() mm = [n.split(":")[-1].strip() for n in m if ":" in n] assert 'note-master' not in mm assert 'pwd-master' not in mm assert 'note-tmp' not in mm assert 'pwd-tmp' not in mm assert 'note-tmp2' in mm # back to master (again) restore_main_seed(seed_vault=True) time.sleep(.1) goto_notes() m = cap_menu() mm = [n.split(":")[-1].strip() for n in m if ":" in n] assert 'note-master' in mm assert 'pwd-master' in mm assert 'note-tmp' not in mm assert 'pwd-tmp' not in mm assert 'note-tmp2' not in mm @pytest.mark.parametrize("msg", ["COLDCARD rocks!", "cc\nCC"]) @pytest.mark.parametrize("addr_fmt", [AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH]) @pytest.mark.parametrize("acct", [None, 0, 9999]) @pytest.mark.parametrize("way", ["sd", "qr", "nfc", "vdisk"]) def test_sign_note_body(msg, addr_fmt, acct, need_some_notes, pick_menu_item, sign_msg_from_text, way, goto_notes, settings_set): settings_set("notes", []) title = "aaa" need_some_notes(title, msg) goto_notes() pick_menu_item(f"1: {title}") pick_menu_item("Sign Note Text") sign_msg_from_text(msg, addr_fmt, acct, False, 0, way) def test_send_password_menu_item(need_some_passwords, goto_notes, cap_menu, pick_menu_item, settings_set, settings_remove, press_cancel): # covers regression where "Send Password" menu item was only shown when USB was disabled settings_set("notes", []) need_some_passwords() settings_set('du', 1) goto_notes() pick_menu_item([i for i in cap_menu() if i.endswith(': A')][0]) time.sleep(.2) m = cap_menu() assert 'Send Password' not in m press_cancel() settings_set('du', 0) goto_notes() pick_menu_item([i for i in cap_menu() if i.endswith(': A')][0]) time.sleep(.2) m = cap_menu() assert 'Send Password' in m for _ in range(3): press_cancel() @pytest.mark.onetime def test_password_cancel_stores_empty_not_none(goto_notes, need_keypress, press_select, press_cancel, enter_text, settings_get, settings_set, cap_screen, pick_menu_item): # canceling the password field when creating a new password entry stored # None instead of ''. EmulatedKeyboard.can_type(None) then raised # TypeError: 'NoneType' object is not iterable when "Send Password" was selected. # settings_set('secnap', True) settings_set('notes', []) goto_notes('New Password') enter_text('cancel-pw-test') # title press_select() # skip username press_cancel() # cancel password field - bug, stores None press_select() # skip site press_cancel() # exit misc pick_menu_item('(none)') # no group time.sleep(0.2) goto_notes() pick_menu_item('1: cancel-pw-test') pick_menu_item('Send Password') time.sleep(.5) scr = cap_screen() assert 'Traceback' not in scr assert "Place mouse at" in scr for _ in range(5): press_cancel() @pytest.mark.parametrize("chain", ["BTC", "XTN"]) @pytest.mark.parametrize("change", [True, False]) @pytest.mark.parametrize("idx", [None, 0, 9999]) def test_sign_password_free_form(chain, change, idx, need_some_passwords, settings_set, goto_notes, pick_menu_item, sign_msg_from_text): settings_set('notes', []) # clear title = "A" msg = 'More Notes AAAA' settings_set('notes', [ {'misc': msg, 'password': 'fds65fd5f1sd51s', 'site': 'https://a.com', 'title': title, 'user': 'AAA'} ]) goto_notes() pick_menu_item(f"1: {title}") pick_menu_item("Sign Note Text") sign_msg_from_text(msg, AF_P2WPKH, None, change, idx, "qr", chain) @pytest.mark.parametrize("length", [1, 241]) def test_sign_misc_length(length, settings_set, cap_menu, goto_notes, pick_menu_item, press_cancel): msg = "a" * length settings_set('notes', [ {'misc': msg, 'password': '89898989898989898989898989898', 'site': 'https://abaaba.com', 'title': "BA", 'user': 'BABA'}, {'title': "AB", 'misc': msg,} ]) goto_notes() pick_menu_item(f"1: BA") assert "Sign Note Text" not in cap_menu() press_cancel() pick_menu_item(f"2: AB") assert "Sign Note Text" not in cap_menu() @pytest.mark.parametrize("pw", [ "My secret BIP-39 passphrase!!", "a" * 100, "secret\n\t", # newline+tab will be stripped "secret1 ", # space will be stripped # below, not allowed "a" * 101, # too long "aaaaaaa\nbbbbbbbbb", # non-printable ASCII ]) @pytest.mark.parametrize("sv", [True, False]) # Seed Vault @pytest.mark.parametrize("pwd", [True, False]) # whether note or password def test_bip39_passphrase_from_note(dev, need_some_notes, settings_set, goto_notes, pick_menu_item, cap_story, press_select, cap_menu, reset_seed_words, pw, sv, pwd, seed_vault_enable, need_keypress, settings_remove): reset_seed_words() settings_remove("seeds") # clear seed_vault_enable(enable=sv) settings_set('notes', []) # clear title = "A1" if pwd: settings_set('notes', [ {'misc': "some\nrandom\nnote", 'password': pw, 'site': 'https://a.com', 'title': title, 'user': 'AAA'} ]) mi = "Apply as BIP-39 Passphrase" else: need_some_notes(title=title, body=pw) mi = "Apply as BIP-39 Passphrase" goto_notes() pick_menu_item(f"1: {title}") time.sleep(.1) if len(pw) > 100 or "\n" in pw: # not allowed - must be ASCII 32-127 and length <= 100 assert mi not in cap_menu() return # done pick_menu_item(mi) # firmware rstrips any note before using it pw = pw.rstrip() # what it should be seed = Mnemonic.to_seed(simulator_fixed_words, passphrase=pw) expect = BIP32Node.from_master_secret(seed) time.sleep(.1) title, story = cap_story() title_xfp = title[1:-1] assert "created by adding passphrase to master seed [0F056943]" in story assert expect.fingerprint().hex().upper() == title_xfp press_select() time.sleep(.2) if sv: title, story = cap_story() assert "Press (1) to store temporary seed into Seed Vault" in story time.sleep(.1) need_keypress("1") # store it time.sleep(.1) title, story = cap_story() assert "Saved to Seed Vault" in story assert title_xfp in story press_select() assert title_xfp in cap_menu()[0] xpub = dev.send_recv(CCProtocolPacker.get_xpub("m"), timeout=None) got = BIP32Node.from_wallet_key(xpub) assert got.sec() == expect.sec() @pytest.mark.parametrize("words", [True, False]) @pytest.mark.parametrize("pwd", [True, False]) def test_b39_from_note_eph_seed(words, pwd, generate_ephemeral_words, set_bip39_pw, settings_remove, reset_seed_words, settings_set, need_some_notes, goto_notes, pick_menu_item, cap_menu, cap_story, press_select, dev): reset_seed_words() settings_remove("seeds") settings_remove("seedvault") if words: e_seed_words = generate_ephemeral_words(num_words=12, seed_vault=False) e_seed_words = " ".join(e_seed_words) else: set_bip39_pw('bdfhjkds', seed_vault=False, reset=False) # enabling notes & pwds in temporary settings settings_set('notes', []) # clear title = "A1" pw = "abcdefg" # allowed if pwd: settings_set('notes', [ {'misc': "some\nrandom\nnote", 'password': pw, 'site': 'https://a.com', 'title': title, 'user': 'AAA'} ]) mi = "Apply as BIP-39 Passphrase" else: need_some_notes(title=title, body=pw) mi = "Apply as BIP-39 Passphrase" goto_notes() pick_menu_item(f"1: {title}") time.sleep(.1) if not words: # no way to apply passphrase on secret that is not word-based assert mi not in cap_menu() return # done pick_menu_item(mi) # what it should be e_xfp = BIP32Node.from_master_secret(Mnemonic.to_seed(e_seed_words)).fingerprint().hex().upper() seed = Mnemonic.to_seed(e_seed_words, passphrase=pw) expect = BIP32Node.from_master_secret(seed) time.sleep(.1) title, story = cap_story() title_xfp = title[1:-1] assert f"created by adding passphrase to current active temporary seed [{e_xfp}]" in story assert expect.fingerprint().hex().upper() == title_xfp press_select() time.sleep(.2) assert title_xfp in cap_menu()[0] xpub = dev.send_recv(CCProtocolPacker.get_xpub("m"), timeout=None) got = BIP32Node.from_wallet_key(xpub) assert got.sec() == expect.sec() # EOF