Compare commits

...

275 Commits

Author SHA1 Message Date
scgbckbone
542dcd32c7 revert SSSP bypass PIN login 2026-06-25 11:18:33 -04:00
scgbckbone
0ef6413cd8 apply note or pwd as b39 passphrase 2026-06-24 14:07:25 -04:00
Dmitry Monakhov
97d86c9571 Fix BBQr share of Unicode text: encode str to UTF-8 before sizing/splitting
For 'U'/'J' payloads data is a str; planning counted codepoints while
b32encode consumes UTF-8 bytes, so multi-byte text (e.g. paper-wallet QR
art) overflowed target_vers and tripped the assert in show_bbqr_codes.
2026-06-24 13:29:58 -04:00
scgbckbone
edae8c1ee6 Add groups for secure notes 2026-06-24 13:28:43 -04:00
scgbckbone
553405776f Keep scanner reinit state instance-local 2026-06-24 11:12:35 -04:00
scgbckbone
ad2088d231 Fix QR scanner setup and sleep handling 2026-06-24 11:12:35 -04:00
scgbckbone
67a5c6c270 fix bypass_tmp return to master secret with xprv type 2026-06-24 08:23:47 -04:00
scgbckbone
eb112eb3a1 fix tests 2026-06-24 08:22:54 -04:00
scgbckbone
0d04e5e1f8 bugfix: p2pk 2026-06-23 11:43:39 -04:00
scgbckbone
59eb529a20 Reject witness-only UTXO for legacy inputs; Suppress fee for unverified witness UTXOs;normalize legacy inputs to proper utxo 2026-06-23 11:25:53 -04:00
scgbckbone
d5aba396a6 improve USB validation 2026-06-23 10:55:08 -04:00
scgbckbone
6fd256dbdc bugfix: 1of1 multisig 2026-06-23 10:34:44 -04:00
scgbckbone
6716fcbacb keep NFC export tag live for repeated probes 2026-06-23 10:29:26 -04:00
scgbckbone
1dddd88525 WIF Store upgrade 2026-06-22 12:46:50 -04:00
scgbckbone
d656f371c7 BIP-322 changes after BIP got in to the complete state 2026-06-22 11:20:44 -04:00
scgbckbone
7e92e5162a Restore borrowed secret handling in SensitiveValues 2026-06-22 11:16:20 -04:00
scgbckbone
74d34cfcf7 stabilize tests 2026-06-22 10:39:46 -04:00
scgbckbone
64658621bb simulator:attribute catchup with real SCAN 2026-06-22 10:39:30 -04:00
scgbckbone
755353f029 docs: NFC antenna by HW 2026-06-22 09:28:53 -04:00
scgbckbone
2981d15933 build: automatic block height update 2026-06-22 09:28:30 -04:00
scgbckbone
9ff3f5c447 slight menu optimization for long menus 2026-06-19 12:51:07 -04:00
scgbckbone
f5a1ef32c9 test nits 2026-06-19 12:50:54 -04:00
scgbckbone
841e44335e testing: block_h bumped for SSSP too, when CCC overrides SSSP block 2026-06-19 12:50:26 -04:00
scgbckbone
38616234e7 testing: cope with bitcoin core v30 2026-06-19 12:50:10 -04:00
scgbckbone
8e3bbfdf84 docs: index all docs and fix drift vs firmware 2026-06-19 12:48:49 -04:00
Peter D. Gray
f9b65ce968
credit 2026-06-19 11:00:37 -04:00
Dmitry Monakhov
8d71040acf Don't restore cached backup password (bkpw) from backup file
Restore mirrored the write-side strip of bkpw: a crafted backup could inject
setting.bkpw and fixate the password used for future backups. Drop it on restore
2026-06-19 10:59:16 -04:00
dependabot[bot]
5feae87e03 Bump requests from 2.32.4 to 2.33.0 in /testing
Bumps [requests](https://github.com/psf/requests) from 2.32.4 to 2.33.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.32.4...v2.33.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.33.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-19 10:58:24 -04:00
scgbckbone
0949c0ac86 README.md update build repro steps (were misordered before) 2026-06-19 10:57:08 -04:00
scgbckbone
c36eac23d2 bundle small fixes 2026-06-19 10:56:45 -04:00
scgbckbone
a24a894cfd fix typo in nfc-pushtx.md 2026-05-16 10:59:23 -04:00
scgbckbone
ca06dfd250 unreleased regression introduced in 300323f18d 2026-04-25 10:36:15 -04:00
scgbckbone
3a1ef6fe50 bugfix: NFC verify address wrong error message 2026-04-20 15:21:43 -04:00
scgbckbone
883be60fc5 bugfix: attribute error on exception object + more 7z header tests 2026-04-20 15:19:31 -04:00
scgbckbone
393ebf5b43 bugfix: default menu position in custom path address format menu 2026-04-20 15:18:30 -04:00
scgbckbone
2b5178bd63 bugfix: "Send Password" menu item visibility reversed, do not store password as None, UX fixes 2026-04-20 15:15:19 -04:00
scgbckbone
44e7be3681 fix: correct container type for settings.wifs; proper button text UX with parentheses 2026-04-20 12:40:40 -04:00
scgbckbone
300323f18d final solution can_cancel=True 2026-04-20 12:40:07 -04:00
scgbckbone
c998432fc4 bugfix: exiting nickname entry with nickname already saved deleted previous nickname; fixed settings_get with prelogin arg 2026-04-20 12:37:54 -04:00
scgbckbone
9c6cfcbbd7 bugfix: enable disabled 7z magic check in check_file_headers 2026-04-20 11:28:28 -04:00
scgbckbone
be614dab92 bugfix: Delta Mode Trick PIN restore from backup 2026-04-20 11:17:52 -04:00
scgbckbone
02bd428786 do not repeat HSM_DISABLE_CMDS in HOBBLED_CMDS 2026-04-20 11:17:17 -04:00
scgbckbone
6869ba87b0 typos 2026-04-20 11:17:17 -04:00
scgbckbone
d0f834570b testing: fix bitcoind param list 2026-04-20 11:15:02 -04:00
scgbckbone
00afe533ca better fitting UX message for MK versions 2026-04-20 11:14:36 -04:00
scgbckbone
621523c1a8 remove redundant double newline in "Show Version" 2026-04-08 10:14:42 -04:00
scgbckbone
fdf630ab31 Wait UX for chain changes 2026-04-08 10:14:05 -04:00
Peter D. Gray
d2ad7a5923
Merge branch 'master' of github.com:Coldcard/firmware 2026-03-25 10:51:52 -04:00
Peter D. Gray
29ef16be63
Signed for Edge release. 2026-03-25 10:51:35 -04:00
scgbckbone
3344975607 regtest inherits chains parameters from testnet (saves flash space) 2026-03-16 13:23:02 -04:00
Peter D. Gray
15191eaaa5
edits 2026-03-12 10:17:09 -04:00
Peter D. Gray
717a3f591a
Mk5 hardware details 2026-03-12 10:12:44 -04:00
Peter D. Gray
74790ab80d
Merge branch 'master' of github.com:Coldcard/firmware 2026-03-10 10:55:49 -04:00
Peter D. Gray
50b1704c60
mk5 now public 2026-03-10 10:55:04 -04:00
scgbckbone
1e9338c550 testing: add BIP-322 POR signed with WIF Store test 2026-03-10 08:00:06 -04:00
scgbckbone
17fc097cbd bip322 doc mainnet 2026-03-06 15:05:16 -05:00
Peter D. Gray
6f6563c2cb
update logs 2026-03-05 17:27:00 -05:00
Peter D. Gray
4904d38bb6
New release: 2026-03-05T2052-v5.5.0 2026-03-05 15:52:33 -05:00
Peter D. Gray
1ea83d9b70
Signed for mk release. 2026-03-05 15:52:31 -05:00
Peter D. Gray
2410897c86
New release: 2026-03-05T2051-v1.4.0Q 2026-03-05 15:51:22 -05:00
Peter D. Gray
6541812e20
Signed for q1 release. 2026-03-05 15:51:20 -05:00
Peter D. Gray
2604f4d092
m 2026-03-05 15:50:03 -05:00
scgbckbone
c6da2612ef
handle WIF import duplicates 2026-03-05 15:47:09 -05:00
Peter D. Gray
538b1a6df8
rc2+factory 2026-03-05 14:37:20 -05:00
Peter D. Gray
2edf3c72e4
nits 2026-03-05 14:22:56 -05:00
scgbckbone
5e3f7a9321
Visualize WIF with ability to import to WIF store 2026-03-05 14:17:42 -05:00
scgbckbone
00beed7b94
bump ccc_min_block 2026-03-05 14:17:32 -05:00
scgbckbone
12e0af3f5c
WIF store testing 2026-03-05 12:49:31 -05:00
Peter D. Gray
01629e1396
WIF tests, support paper wallet format 2026-03-05 11:51:21 -05:00
Peter D. Gray
d893575ecf
bump 2026-03-05 11:51:01 -05:00
scgbckbone
0cc6818728
fix WIF store ownership showing QR address 2026-03-05 09:56:05 -05:00
Peter D. Gray
b7bc614323
text 2026-03-05 09:24:04 -05:00
Peter D. Gray
02b5a75675
new bootloader 2026-03-05 09:14:33 -05:00
Peter D. Gray
e35f60ed5e
version bumps 2026-03-05 09:12:25 -05:00
scgbckbone
0b425b8609
fix: file picker in import BIP-322 msg needs vdisk and slot_b args 2026-03-05 08:29:51 -05:00
Peter D. Gray
43ef951d83
Edits 2026-03-04 17:32:38 -05:00
Peter D. Gray
38553d1ac5
Mk hardware 2026-03-04 17:32:21 -05:00
scgbckbone
9b131b2eff WIF Store 2026-03-04 17:16:38 -05:00
scgbckbone
c19be4f41e bugfix: do not offer to show QR code of TXID if txn is not finalized 2026-03-04 15:24:42 -05:00
Peter D. Gray
e92a8ccde1
edits 2026-03-04 12:58:21 -05:00
Peter D. Gray
7d937aca84
code formating 2026-03-04 10:50:05 -05:00
Peter D. Gray
382eef61d2
add ./debug/* 2026-03-04 10:48:06 -05:00
scgbckbone
cfc46b565e show descriptor & key expression in story; signed key expression export 2026-03-04 10:46:45 -05:00
scgbckbone
45542d1d4f prevent dupe inputs for specific kind of path_mappers 2026-03-04 10:32:52 -05:00
scgbckbone
470fe2843c bugfix: dramatic pause progress bar off by one 2026-03-04 10:32:32 -05:00
scgbckbone
6c247d4852 typo in Nuke Device UX message 2026-03-04 10:32:09 -05:00
Peter D. Gray
6f366d1603
slight rework 2026-02-25 11:05:24 -05:00
Peter D. Gray
d53c7b2e1b
optimizations, petty 2026-02-25 11:04:13 -05:00
Peter D. Gray
598ccda8c0
edits 2026-02-25 10:18:09 -05:00
scgbckbone
1bbaeef439 bugfix: duplicate inputs 2026-02-25 10:16:49 -05:00
dependabot[bot]
eb50a0e198 Bump pillow from 10.3.0 to 12.1.1 in /misc/q1font
Bumps [pillow](https://github.com/python-pillow/Pillow) from 10.3.0 to 12.1.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/10.3.0...12.1.1)

---
updated-dependencies:
- dependency-name: pillow
  dependency-version: 12.1.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-25 09:57:56 -05:00
Peter D. Gray
08d0a70de2
edits 2026-02-25 09:57:00 -05:00
Peter D. Gray
a3d5d6152e
MSG->Message when we have space 2026-02-25 09:54:29 -05:00
Peter D. Gray
9f832476eb
add BIP-322 feature 2026-02-25 09:44:14 -05:00
Peter D. Gray
78195d8fb9
edits 2026-02-25 09:41:50 -05:00
scgbckbone
aecc870c6b use HW accelerated tagged_sha256 2026-02-25 09:31:34 -05:00
scgbckbone
0c43c802e1 bump libngu to version with HW accelerated tagged_sha256 2026-02-25 09:31:34 -05:00
scgbckbone
4ce43c74c6 move 2 2026-02-25 09:31:34 -05:00
scgbckbone
1176c83e34 move 2026-02-25 09:31:34 -05:00
scgbckbone
ef2f35b1fb BIP-322 msg verification 2026-02-25 09:31:34 -05:00
scgbckbone
5d7d5d881d BIP-322 Proof of Reserves 2026-02-25 09:31:34 -05:00
scgbckbone
5047512bae blue wallet export option 2026-02-24 14:10:37 -05:00
scgbckbone
9862f53bec review 2026-02-24 12:34:33 -05:00
Peter D. Gray
6a34760943
fix tests 2026-02-24 11:03:00 -05:00
Peter D. Gray
d0576a205f
Updates 2026-02-24 10:00:54 -05:00
Peter D. Gray
dfd52e5c03
buried settings menu 2026-02-24 10:00:48 -05:00
Peter D. Gray
6fd47d4c16
Updated 2026-02-24 10:00:22 -05:00
Peter D. Gray
06879c59e0
test we show BIP-39 passphrase 2026-02-24 09:57:08 -05:00
Peter D. Gray
21392bd3df
show BIP-39 passphrase on-screen 2026-02-24 09:56:48 -05:00
Peter D. Gray
e4d2326959
edits 2026-02-24 08:58:07 -05:00
scgbckbone
72e336628a Nuke Device 2026-02-17 11:36:32 -05:00
scgbckbone
7ca3baae43 allow cancel from tx explorer goto index 2026-02-17 11:22:57 -05:00
scgbckbone
89d6e226d7 txexplorer: goto index 2026-02-17 11:22:57 -05:00
scgbckbone
f966d47012 input explorer 2026-02-13 11:56:41 -05:00
scgbckbone
dbf9482fea add note to "Verify Backup" that proper verify is attainable via Restore Backup 2026-02-11 14:33:36 -05:00
scgbckbone
ecd796b3a5 bugfix: empty notes in hobbled mode 2026-02-10 09:01:09 -05:00
scgbckbone
b1fe2cca26 bugfix: fwd slash in multisig name caused export to yikes. Replace fwd slash with dash in export filenames 2026-02-10 09:00:16 -05:00
scgbckbone
5d9ab62595 nit: tx nVersion serialization is signed integer 2026-02-10 08:59:48 -05:00
scgbckbone
7dbbf29b8d add missing QR tests for tmp secret import 2026-02-06 11:46:22 -05:00
scgbckbone
722c45df0a improve import secret tests 2026-02-06 10:42:01 -05:00
scgbckbone
88e4a5e8ab improve UX responsivness for Key Expression 2026-02-06 10:41:11 -05:00
scgbckbone
910c096145 allow resetting block_h in CCC menu 2026-02-02 11:35:38 -05:00
scgbckbone
a3d5485fd2 unified multisig import 2026-02-02 11:34:52 -05:00
scgbckbone
366670b4d5 USB send keystrokes for all BIP-85 secret types 2026-01-21 11:43:32 -05:00
scgbckbone
0fffb07e9e fix: remove unnecessary total_out counting in output_iter 2026-01-21 11:23:13 -05:00
Peter D. Gray
4e91d2ca3f
nit 2026-01-02 15:14:01 -05:00
tadeubas
eb467988bb chore: revert .gitignore 2026-01-02 15:10:22 -05:00
tadeubas
9fbf208c3f refactor: drop --shallow-submodules since lwIP submodule does not support shallow clone 2026-01-02 15:10:22 -05:00
tadeubas
da1d6e28bd refactor(simulator): --log output path respects --segregate 2026-01-02 15:10:22 -05:00
tadeubas
c1e17d3a26 feat: add --log flag to enable simulator logging 2026-01-02 15:10:22 -05:00
tadeubas
a2f0eb323a chore: removed Docker 2026-01-02 15:10:22 -05:00
tadeubas
53c6d33c2f chore: Simulator Docker instructions 2026-01-02 15:10:22 -05:00
russeree
a6a66bc367 [Policy] Support raw transaction versions == 3
Co-authored-by: scgbckbone <scgbckbone@proton.me>
2025-12-30 12:04:20 -05:00
scgbckbone
386cbcbb1d note that phone cannot be in airplane mode for NFC PushTx to work 2025-11-27 14:14:15 -05:00
Peter D. Gray
d5713851ea
Signed for Edge release. 2025-11-25 12:29:30 -05:00
Peter D. Gray
91f96ff870
Merge branch 'master' of github.com:Coldcard/firmware 2025-11-20 11:34:39 -05:00
Peter D. Gray
c25af2bfb1
Signed for Edge release. 2025-11-20 11:34:32 -05:00
scgbckbone
b3cd82ed61 allow viewing QR codes for XOR split mnemonics 2025-11-18 14:53:39 -05:00
scgbckbone
02ee6a58e2 fix login tests 2025-11-17 09:12:26 -05:00
scgbckbone
192f2d2dda Key expression export 2025-11-13 10:23:24 -05:00
Peter D. Gray
f235a83cf3
update changelogs 2025-11-03 10:44:28 -05:00
Peter D. Gray
d34f59697c
New release: 2025-11-03T1527-v5.4.5 2025-11-03 10:27:16 -05:00
Peter D. Gray
f966d3b079
Signed for mk4 release. 2025-11-03 10:27:12 -05:00
Peter D. Gray
6b7294cea0
New release: 2025-11-03T1525-v1.3.5Q 2025-11-03 10:25:55 -05:00
Peter D. Gray
efa8e1d56a
Signed for q1 release. 2025-11-03 10:25:49 -05:00
Peter D. Gray
946b4e9ae4
For 2025-11-03T1525-v1.3.5Q 2025-11-03 10:25:48 -05:00
Peter D. Gray
0d1104dfe0
block bump 2025-11-03 09:37:23 -05:00
Peter D. Gray
1e371d9297
bump 2025-10-30 14:17:17 -04:00
Peter D. Gray
f88b8da729
version bump 2025-10-30 13:48:14 -04:00
scgbckbone
52a090e31b bugfix: allow setting block_h from tmp seed 2025-10-30 13:16:17 -04:00
scgbckbone
cbd1b841b9 testing: add ability to set nLockTime in fake_{ms,}_txn 2025-10-30 13:16:17 -04:00
scgbckbone
82fabe75d4 test fixes 2025-10-30 09:43:56 -04:00
scgbckbone
bc8b55a059 re-fix: bugfix: exiting custom backup password text form causes yikes 2025-10-29 16:35:48 -04:00
scgbckbone
c54b3801ce UX confirm loading backup 2025-10-29 16:35:48 -04:00
scgbckbone
076bb34285 show backup filename during backup password entry (Q only) 2025-10-29 16:06:42 -04:00
scgbckbone
48709f3329 show fw version in hobbled mode 2025-10-29 13:26:34 -04:00
scgbckbone
ef0ba6a556 address format matching from PSBT witness/redeem script instead of PSBT_XPUBs derivation paths 2025-10-29 12:56:50 -04:00
scgbckbone
a08550cfd8 bugfix: exiting custom backup password text form causes yikes 2025-10-29 12:48:20 -04:00
scgbckbone
3e818cbbf6 remove unused import from SSSP menu constructor 2025-10-05 14:24:24 -04:00
Peter D. Gray
fe0041f99c
bump date 2025-09-30 08:42:29 -04:00
Peter D. Gray
15e571b0d9
New release: 2025-09-30T1238-v5.4.4 2025-09-30 08:39:04 -04:00
Peter D. Gray
36521dfef9
Signed for mk4 release. 2025-09-30 08:39:00 -04:00
Peter D. Gray
25249eb68c
New release: 2025-09-30T1237-v1.3.4Q 2025-09-30 08:37:38 -04:00
Peter D. Gray
be1328c720
Signed for q1 release. 2025-09-30 08:37:34 -04:00
Peter D. Gray
8d6ce99cd3
undo-gold-rc 2025-09-30 08:35:56 -04:00
Peter D. Gray
fcd848d821
deltamode timing fix 2025-09-29 17:19:44 -04:00
Peter D. Gray
203394a709
spelling 2025-09-29 12:11:24 -04:00
scgbckbone
284616d597
test_sssp.py more sleeps 2025-09-29 09:42:45 -04:00
Peter D. Gray
55c9ee4626
New release: 2025-09-26T1814-v5.4.4 2025-09-26 14:14:51 -04:00
Peter D. Gray
47430cb211
Signed for mk4 release. 2025-09-26 14:14:46 -04:00
Peter D. Gray
dd1bea1949
New release: 2025-09-26T1813-v1.3.4Q 2025-09-26 14:13:29 -04:00
Peter D. Gray
2aea18ba53
Signed for q1 release. 2025-09-26 14:13:24 -04:00
Peter D. Gray
3c4922e3ca
another day, another RC 2025-09-26 14:11:49 -04:00
Peter D. Gray
c6ec10206c
undo-rc 2025-09-26 14:10:17 -04:00
Peter D. Gray
88a9f4719b
cleanups 2025-09-26 11:14:19 -04:00
scgbckbone
6a3eec50f1
bugfix: only list files with proper extension delimited by dot; fix UX showing suffixes in file_picker when no suitable files found 2025-09-26 11:03:49 -04:00
Peter D. Gray
722facf0d9
add heartbeats, cleanups 2025-09-26 10:59:37 -04:00
Peter D. Gray
0a9d99429b
tweak 2025-09-26 09:27:36 -04:00
Peter D. Gray
d8c13ddc73
dont show "allow notes" on mk4 2025-09-25 14:59:33 -04:00
Peter D. Gray
d73c1bd8d3
New release: 2025-09-25T1503-v5.4.4 2025-09-25 11:03:27 -04:00
Peter D. Gray
5764073837
Signed for mk4 release. 2025-09-25 11:03:22 -04:00
Peter D. Gray
259be18962
New release: 2025-09-25T1501-v1.3.4Q 2025-09-25 11:02:01 -04:00
Peter D. Gray
248e0b568d
Signed for q1 release. 2025-09-25 11:01:56 -04:00
Peter D. Gray
ca00ee0798
Edits 2025-09-25 11:00:16 -04:00
scgbckbone
f3aaf3d5cb reload Trick Pins before deleting unlock pins 2025-09-25 10:07:00 -04:00
scgbckbone
06fa4f338c disallow Type Passwords if not okeys in sssp 2025-09-25 10:07:00 -04:00
scgbckbone
2d81b7251d proper label for Web 2FA warning 2025-09-25 10:07:00 -04:00
scgbckbone
91f10c2f35 do not allow empty BIP-39 passphrase over USB 2025-09-25 10:07:00 -04:00
scgbckbone
52e7c75539 fix SSSP unable to find unlock policy PIN 2025-09-25 10:07:00 -04:00
scgbckbone
a1e7e4c8de fix SSSP test drive to actually enforce policy 2025-09-25 10:07:00 -04:00
scgbckbone
fce9503d0e bump ccc_min_block (block height) 2025-09-25 10:07:00 -04:00
scgbckbone
5fc25566ee ownership improve UI 2025-09-25 10:07:00 -04:00
scgbckbone
b88590f8e8
fix multisig test ms_sign_simple 2025-09-24 08:28:05 -04:00
Peter D. Gray
765cc2a5a4
Merge branch 'master' of github.com:Coldcard/firmware 2025-09-24 08:27:26 -04:00
Peter D. Gray
1a591a70eb
reword 2025-09-23 15:44:14 -04:00
scgbckbone
26680a00f0 test fixes 2025-09-23 13:09:06 -04:00
scgbckbone
429b8e645e ckcc bump 2025-09-23 11:20:16 -04:00
Peter D. Gray
74fd862c9c
edits 2025-09-23 11:06:28 -04:00
Peter D. Gray
0625aa462c
comments 2025-09-23 10:53:31 -04:00
scgbckbone
faa5ebf11e test hobbled Teleport 2025-09-23 10:47:16 -04:00
scgbckbone
d0c5998e55 multisig input/output address format 2025-09-23 10:47:16 -04:00
Peter D. Gray
50d20713a0
hide empty menu 2025-09-23 09:33:05 -04:00
scgbckbone
2445b4d435
mk4 fix word entry after restore via USB 2025-09-22 12:14:27 -04:00
Peter D. Gray
a8366f55e0
nits 2025-09-22 12:13:35 -04:00
Peter D. Gray
41b8167837
seedxor allowed when hobbled 2025-09-22 11:57:21 -04:00
Peter D. Gray
3461ce336d
nit 2025-09-22 10:53:55 -04:00
Peter D. Gray
4457576ad4
Q becomes calculator rather than e-waste 2025-09-22 10:47:01 -04:00
scgbckbone
038199a3e2
review 2025-09-22 09:51:11 -04:00
scgbckbone
1929067aa1
Q: brick into forever calculator 2025-09-22 09:51:04 -04:00
scgbckbone
2b115059e8
add "Restore from XOR" to Temporary Seed menu 2025-09-22 09:44:13 -04:00
scgbckbone
88110bc5c5
SSSP settings shared across temporary seeds 2025-09-22 09:31:13 -04:00
scgbckbone
0a2c0cba12 update ckcc to latest master 2025-09-19 13:10:19 -04:00
scgbckbone
1e9e3ffb9d fix tests 2025-09-19 13:10:04 -04:00
Peter D. Gray
863e0d85ad
nits 2025-09-19 08:50:05 -04:00
Peter D. Gray
2109ab4ab1
ownership 2025-09-18 10:58:28 -04:00
Peter D. Gray
e5dce7105b
bug 2025-09-18 10:45:05 -04:00
scgbckbone
0237fd29ba txout explorer do not yikes on big QRs 2025-09-18 10:43:32 -04:00
scgbckbone
3efeb4f1ef docs & nits 2025-09-18 10:34:22 -04:00
scgbckbone
8b3603b15f ownership: search particular named wallet via BIP-21 wallet query param 2025-09-18 10:34:22 -04:00
scgbckbone
e8ba25fd04 bugfix: ownership check needed re-run for values near max 2025-09-18 10:34:22 -04:00
Peter D. Gray
9e762d29a5
edit 2025-09-18 10:27:37 -04:00
scgbckbone
de043f2250 restore backup via USB 2025-09-18 10:09:35 -04:00
Peter D. Gray
20ce5f2bae
edits 2025-09-18 08:57:22 -04:00
scgbckbone
3353f3d4a4 rename files on SD card via List Files 2025-09-18 08:37:14 -04:00
scgbckbone
4d2349fef4 lower Mk4 default wrap-around from 16 to 10 (same as Q) 2025-09-18 08:35:39 -04:00
Peter D. Gray
54dcf2dce8
little bug 2025-09-17 10:03:50 -04:00
scgbckbone
3291faa31e decouple wiping NFC chip from ux_animation routine 2025-09-17 09:31:42 -04:00
scgbckbone
bbac20b453 bugfix: premature wipe while exporting secret material via NFC - only first export loop (0th) was actually sending data 2025-09-17 09:31:42 -04:00
scgbckbone
98420f8ac3 bugfix: selftest MicroSD test 2025-09-17 08:45:54 -04:00
Peter D. Gray
5b26b306b5
version bump 2025-09-16 12:16:45 -04:00
Peter D. Gray
a3cac15a53 nits 2025-09-16 10:35:07 -04:00
scgbckbone
c7a19ee50f add SSSP login tests 2025-09-16 10:35:07 -04:00
Peter D. Gray
e42c0631d0 done 2025-09-16 10:35:07 -04:00
Peter D. Gray
609af3a257 cleanups 2025-09-16 10:35:07 -04:00
Peter D. Gray
7daa67cc63 test deltamode works 2025-09-16 10:35:07 -04:00
Peter D. Gray
3f24307dcd nits 2025-09-16 10:35:07 -04:00
scgbckbone
372954e43a SSSP update menu tree & related adjustments 2025-09-16 10:35:07 -04:00
Peter D. Gray
391aea0462 nits 2025-09-16 10:35:07 -04:00
scgbckbone
ee464f4a40 fixes after --eff changes 2025-09-16 10:35:07 -04:00
Peter D. Gray
54d58d4b43 block some USB command in hobble mode 2025-09-16 10:35:07 -04:00
Peter D. Gray
4bd8d12d9d tune 2025-09-16 10:35:07 -04:00
scgbckbone
8ceb6a4602 more tests 2025-09-16 10:35:07 -04:00
Peter D. Gray
76cc136a9e testing 2025-09-16 10:35:07 -04:00
Peter D. Gray
8d84979ddf forgotten pin 2025-09-16 10:35:07 -04:00
Peter D. Gray
bb391515a0 add seedvault 2025-09-16 10:35:07 -04:00
Peter D. Gray
16c3caee28 cleanup 2025-09-16 10:35:07 -04:00
Peter D. Gray
ab1b656277 improve --eff handling 2025-09-16 10:35:07 -04:00
Peter D. Gray
1a1daf32e3 nits 2025-09-16 10:35:07 -04:00
Peter D. Gray
6b1c38fe2f bug note 2025-09-16 10:35:07 -04:00
Peter D. Gray
43544f4f96 word entry 2025-09-16 10:35:07 -04:00
scgbckbone
d15de0321d Mk4 SSSP Word Check 2025-09-16 10:35:07 -04:00
scgbckbone
0420d4b6eb nits 2025-09-16 10:35:07 -04:00
Peter D. Gray
00b2f67d55 tidy 2025-09-16 10:35:07 -04:00
scgbckbone
6ab63b9dcf (some) policy test for sssp 2025-09-16 10:35:07 -04:00
Peter D. Gray
d053398a9a test cases 2025-09-16 10:35:07 -04:00
Peter D. Gray
d9a601e87c improvements 2025-09-16 10:35:07 -04:00
scgbckbone
6abb24443e fix0 2025-09-16 10:35:07 -04:00
Peter D. Gray
9d54e261ec passes test_ccc_magnitude 2025-09-16 10:35:07 -04:00
Peter D. Gray
335df666ab reword last_fail_reason 2025-09-16 10:35:07 -04:00
Peter D. Gray
7c5503d81a more 2025-09-16 10:35:07 -04:00
Peter D. Gray
a4c7f95dc1 more 2025-09-16 10:35:07 -04:00
Peter D. Gray
dbb2a21798 mroe docs 2025-09-16 10:35:07 -04:00
Peter D. Gray
f12457cbb5 spending policy implemented 2025-09-16 10:35:07 -04:00
Peter D. Gray
28ba1adce3 cleanup 2025-09-16 10:35:07 -04:00
Peter D. Gray
a62d5fb31e few notes 2025-09-16 10:35:07 -04:00
Peter D. Gray
b8435fdd79 slightly better SE2 emulation 2025-09-16 10:35:07 -04:00
Peter D. Gray
d3caf63265 planning 2025-09-16 10:35:07 -04:00
Peter D. Gray
c3a454abd6 hobbled mode support for spending policy 2025-09-16 10:35:07 -04:00
Peter D. Gray
73bb6b850d few notes 2025-09-16 10:35:07 -04:00
Peter D. Gray
bc49347a69 notes 2025-09-16 10:35:07 -04:00
scgbckbone
9a4d3986b7 bugfix: enter vfs after creating it 2025-09-16 09:54:49 -04:00
scgbckbone
f89061ffbe slip32 --> slip132 2025-09-04 09:50:05 -04:00
kdmukai
3eb99272b3 Clarify allowed usage of Seed XOR standard and name 2025-08-29 10:18:26 -04:00
nvk
56e5b98438
narrower 2025-08-18 15:20:57 -04:00
scgbckbone
123caec8d1 fix HSM UX message text 2025-08-13 08:13:34 -04:00
180 changed files with 50663 additions and 2695 deletions

View File

@ -8,7 +8,7 @@ with the latest updates and security alerts.
![coldcard logo](https://coldcard.com/static/images/coldcard-logo-nav.png) ![coldcard logo](https://coldcard.com/static/images/coldcard-logo-nav.png)
![Mk4 coldcard picture front](https://coldcard.com/static/images/mk4.png) ![Mk5 coldcard picture front](https://coldcard.com/static/images/mk5-front.png)
## Quick Links ## Quick Links
@ -28,9 +28,11 @@ has been automated using Docker. Steps are as follows:
```shell ```shell
git clone https://github.com/Coldcard/firmware.git git clone https://github.com/Coldcard/firmware.git
git checkout 2023-12-21T1526-v5.2.2 cd firmware
# get a copy of that binary into ./releases/2023-12-21T1526-v5.2.2-mk4-coldcard.dfu # DOWNLOAD https://coldcard.com/downloads
cd firmware/stm32 # get a copy of binary into ./releases/2026-03-05T2052-v5.5.0-mk-coldcard.dfu
git checkout 2026-03-05T2052-v5.5.0
cd stm32
make -f MK4-Makefile repro make -f MK4-Makefile repro
``` ```
@ -49,14 +51,15 @@ such as Taproot or Miniscript. Our standards for releasing new Edge
versions are lower, so we can iterate faster and get these advancements versions are lower, so we can iterate faster and get these advancements
out to other developers. out to other developers.
Q and Mk4 share the same code base. Individual files that are added, Q and Mk series share the same code base. Individual files that are added,
or removed, can be see in differences between `shared/manifest_mk4.py` or removed, can be see in differences between `shared/manifest_mk4.py`
and `shared/manifest_q1.py`. Common files are in `shared/manifest.py`. and `shared/manifest_q1.py`. Common files are in `shared/manifest.py`.
Firmware built for Mk5, supports the Mk4 without any functional differences.
## Check-out and Setup ## Check-out and Setup
**NOTE** This is the `master` branch and covers the latest hardware (Mk4 and Q). **NOTE** This is the `master` branch and covers the latest hardware (Mk and Q).
See branch `v4-legacy` for firmware which supports only Mk3/Mk2 and earlier. See branch `v4-legacy` for firmware which supports only Mk3/Mk2 and earlier.
Do a checkout, recursively, to get all the submodules: Do a checkout, recursively, to get all the submodules:
@ -232,8 +235,8 @@ Top-level dirs:
- shared code between desktop test version and real-deal - shared code between desktop test version and real-deal
- expected to be largely in python, and higher-level - expected to be largely in python, and higher-level
- new code found only on the Mk4 will be listed in `manifest_mk4.py` code exclusive - code exclusive to the Mk4 or Mk5 will be listed in `manifest_mk4.py`, and
to earlier hardware is in `manifest_mk3.py` to the Q will be listed in `manifest_q1.py`
`unix` `unix`
@ -265,7 +268,7 @@ Top-level dirs:
`stm32/mk4-bootloader` `stm32/mk4-bootloader`
`stm32/q1-bootloader` `stm32/q1-bootloader`
- 128k of factory-set code that you cannot change for Mk4 or Q - 128k of factory-set code that you cannot change
- however, you can inspect what code is on your coldcard and compare to this. - however, you can inspect what code is on your coldcard and compare to this.
`hardware` `hardware`

View File

@ -208,8 +208,9 @@ def readback(fname):
if v & MK_2_OK: d.append('Mk2') if v & MK_2_OK: d.append('Mk2')
if v & MK_3_OK: d.append('Mk3') if v & MK_3_OK: d.append('Mk3')
if v & MK_4_OK: d.append('Mk4') if v & MK_4_OK: d.append('Mk4')
if v & MK_5_OK: d.append('Mk5')
if v & MK_Q1_OK: d.append('Q1') if v & MK_Q1_OK: d.append('Q1')
if v & ~(MK_1_OK | MK_2_OK | MK_3_OK | MK_4_OK | MK_Q1_OK): if v & ~(MK_1_OK | MK_2_OK | MK_3_OK | MK_4_OK | MK_5_OK | MK_Q1_OK):
d.append('?other?') d.append('?other?')
v = nv + '+'.join(d) v = nv + '+'.join(d)
elif fld == 'timestamp': elif fld == 'timestamp':
@ -245,7 +246,7 @@ def readback(fname):
@click.option('--pubkey-num', '-k', type=int, help='Which key # to use for signing', default=0) @click.option('--pubkey-num', '-k', type=int, help='Which key # to use for signing', default=0)
@click.option('--high_water', '-h', is_flag=True, help='Mark version as new highwater mark (no downgrades below this version)') @click.option('--high_water', '-h', is_flag=True, help='Mark version as new highwater mark (no downgrades below this version)')
@click.option('--verbose', '-v', default=False, is_flag=True, help='Show numbers related to signature') @click.option('--verbose', '-v', default=False, is_flag=True, help='Show numbers related to signature')
@click.option('--hw-compat', '-m', type=str, metavar='Mk4', help="Set HW compat field (hw_label value)") @click.option('--hw-compat', '-m', type=str, metavar='mk', help="Set HW compat field (hw_label value)")
@click.option('--backdate', type=int, metavar='DAYS', @click.option('--backdate', type=int, metavar='DAYS',
help='Make downgrade attack test version', default=0) help='Make downgrade attack test version', default=0)
@click.option('--build_dir', '-b', default='l-port/build-COLDCARD') @click.option('--build_dir', '-b', default='l-port/build-COLDCARD')
@ -278,8 +279,9 @@ def doit(keydir, outfn=None, build_dir=None, high_water=False,
vectors = open(build_dir + '/firmware0.bin', 'rb').read() vectors = open(build_dir + '/firmware0.bin', 'rb').read()
body = open(build_dir + '/firmware1.bin', 'rb').read() body = open(build_dir + '/firmware1.bin', 'rb').read()
if hw_compat in { 'mk4', '4'}: if hw_compat in { 'mk4', '4', 'mk5', '5', 'mk' }:
hw_compat = MK_4_OK # Mk4 and 5 can run the same firmware, once Mk5 support was added
hw_compat = MK_4_OK | MK_5_OK
elif hw_compat == 'q1': elif hw_compat == 'q1':
hw_compat = MK_Q1_OK hw_compat = MK_Q1_OK
elif hw_compat in { 'mk3', '3'}: elif hw_compat in { 'mk3', '3'}:

View File

@ -3,14 +3,32 @@
These docs are meant for you hackers out there... but also for anyone who These docs are meant for you hackers out there... but also for anyone who
wants to understand why it's safe to put your moneys into Coldcard. wants to understand why it's safe to put your moneys into Coldcard.
- [`security-model.md`](security-model.md) The COLDCARD Mk4/Mk5/Q security model.
- [`pin-entry.md`](pin-entry.md) Huge and detailed discussion of PIN codes and the security element that holds the secrets. - [`pin-entry.md`](pin-entry.md) Huge and detailed discussion of PIN codes and the security element that holds the secrets.
- [`secure-elements.md`](secure-elements.md) How the dual secure elements work together.
- [`dev-access.md`](dev-access.md) How developers can modify Coldcard to extend it. - [`dev-access.md`](dev-access.md) How developers can modify Coldcard to extend it.
- [`memory-map.md`](memory-map.md) Memory map highlights - [`memory-map.md`](memory-map.md) Memory map highlights
- [`notes-on-repro.md`](notes-on-repro.md) Detailed breakdown of the reproducible build process.
- [`upgrade-recovery.md`](upgrade-recovery.md) Firmware upgrade and recovery process.
- [`backup-files.md`](backup-files.md) Some details of our encrypted backup files. - [`backup-files.md`](backup-files.md) Some details of our encrypted backup files.
- [`temporary-seeds.md`](temporary-seeds.md) Temporary (ephemeral) seeds and the Seed Vault.
- [`seed-xor.md`](seed-xor.md) More about _Seed XOR_ feature, including fully worked Seed XOR example, and useful XOR lookup chart.
- [`key-teleport.md`](key-teleport.md) Key Teleport: encrypted transfer of seeds and secrets between Q devices.
- [`spending-policy.md`](spending-policy.md) Spending policy: autonomous signing with configurable limits.
- [`microsd-2fa.md`](microsd-2fa.md) Using a MicroSD card as a second factor for login.
- [`web2fa.md`](web2fa.md) Web 2FA authentication.
- [`bip85-passwords.md`](bip85-passwords.md) Deriving deterministic passwords via BIP-85.
- [`msg-signing.md`](msg-signing.md) COLDCARD message signing.
- [`proof-of-reserves-bip-322.md`](proof-of-reserves-bip-322.md) BIP-322 generic signed message format and proof of reserves.
- [`generic-wallet-export.md`](generic-wallet-export.md) Generic JSON wallet export file format.
- [`bip-21-extensions.md`](bip-21-extensions.md) Coldcard's BIP-21 URI extensions, including multisig ownership address check.
- [`nfc-coldcard.md`](nfc-coldcard.md) NFC support on Coldcard Mk4 and Q.
- [`nfc-pushtx.md`](nfc-pushtx.md) NFC Push Transaction: broadcast a signed transaction via your phone.
- [`usb-batteries.md`](usb-batteries.md) Using USB battery packs with Coldcard.
- [`electrum-usage.md`](electrum-usage.md) Importing seed words into Electrum for funds usage (and other tips). - [`electrum-usage.md`](electrum-usage.md) Importing seed words into Electrum for funds usage (and other tips).
- [`bitcoin-core-usage.md`](bitcoin-core-usage.md) How to use with Bitcoin Core. - [`bitcoin-core-usage.md`](bitcoin-core-usage.md) How to use with Bitcoin Core.
- [`bitcoin-core2of2desc.md`](bitcoin-core2of2desc.md) Airgapped 2-of-2 multisig with Bitcoin Core using descriptors.
- [`limitations.md`](limitations.md) Documented limitations, policy choices, and TODO items. - [`limitations.md`](limitations.md) Documented limitations, policy choices, and TODO items.
- [`paperwallet.pdf`](paperwallet.pdf) Example paper wallet template file. - [`paperwallet.pdf`](paperwallet.pdf) Example paper wallet template file.
- [`seed-xor.md`](seed-xor.md) More about _Seed XOR_ feature, including fully worked Seed XOR example, and useful XOR lookup chart.
- [`menu-tree.txt`](menu-tree.txt) Dump of the menu system. Incomplete, may be out of date. - [`menu-tree.txt`](menu-tree.txt) Dump of the menu system. Incomplete, may be out of date.

15
docs/bip-21-extensions.md Normal file
View File

@ -0,0 +1,15 @@
## Multisig Ownership address check: "wallet"
If the name of the multisig wallet related to an address is provided, address search
can be greatly accelerated. Just provide `wallet=name` parameter in a standard
[BIP-21](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki) URL
shown in QR code or NFC record. If omitted, search will continue across
all multisig wallets known by COLDCARD.
### Examples
```
tb1q4d67p7stxml3kdudrgkg5mgaxsrgzcqzjrrj4gg62nxtvnsnvqjsxjkej0?wallet=goldmine
bitcoin:mtHSVByP9EYZmB26jASDdPVm19gvpecb5R?label=coldcard_purchase&amount=50&wallet=Haystack%20Four
```

View File

@ -5,13 +5,13 @@ according to [BIP-85 PWD BASE64](https://github.com/bitcoin/bips/blob/master/bip
Generated passwords can be sent as keystrokes via USB to the host computer, Generated passwords can be sent as keystrokes via USB to the host computer,
effectively using Coldcard as specialized password manager. effectively using Coldcard as specialized password manager.
In addition to deriving up to 10,000 distinct secure passwords, the Coldcard Mk4 In addition to deriving up to 10,000 distinct secure passwords, the Coldcard
can also type them into a computer by emulating a USB keyboard, and simulating the can also type them into a computer by emulating a USB keyboard, and simulating the
keystrokes needed to type the password. keystrokes needed to type the password.
#### Requirements #### Requirements
* Coldcard Mk4 with version 5.0.5 or newer * Coldcard Mk4 or Mk5 (firmware 5.0.5 or newer), or any Q
* USB-C with data link (won't work with power only cable from Coinkite) * USB-C with data link (won't work with power only cable from Coinkite)
## Type Passwords over USB ## Type Passwords over USB
@ -32,11 +32,13 @@ to exit. Exiting from "Type Passwords" will cause Coldcard to turn off keyboard
1. Go to Advanced/Tools -> Derive Seed B85 -> Passwords 1. Go to Advanced/Tools -> Derive Seed B85 -> Passwords
2. Choose "Password/Index number" (BIP-85 index) and press OK to generate password. 2. Choose "Password/Index number" (BIP-85 index) and press OK to generate password.
3. Screen shows generated password, path, and entropy from which password was derived 3. Screen shows generated password, path, and entropy from which password was derived
4. A few different options are available at this point: 4. A few different options are available at this point (on Mk; on Q the NFC and
1. press 1 to save password backup file on MicroSD card (cleartext!) QR buttons are used instead of (3)/(4)):
2. press 2 to send keystrokes (this will first of all enable keyboard emulation, then send keystrokes + enter, and finally disables keyboard emulation) 1. press (1) to save password backup file on MicroSD card (cleartext!)
3. press 3 to view password as QR code 2. press (2) to save to Virtual Disk (only when available)
4. press 4 to send over NFC (only appears when NFC is enabled) 3. press (3) to send over NFC (only appears when NFC is enabled)
4. press (4) to view password as QR code
5. press (6) to send keystrokes over USB (this enables keyboard emulation, sends keystrokes + enter, then disables keyboard emulation)
## Keyboard language settings ## Keyboard language settings

View File

@ -5,9 +5,12 @@ wallet systems, but we also have a file format for general purpose
exports, which we hope future wallet makers will leverage. exports, which we hope future wallet makers will leverage.
It contains master XPUB, XFP for that, and derived values for the top hardened It contains master XPUB, XFP for that, and derived values for the top hardened
position of BIP44, BIP84 and BIP49. position of the single-signature schemes BIP44, BIP49 and BIP84, plus the
multisig schemes BIP48 (`bip48_1` = `.../1h` P2SH-P2WSH and `bip48_2` = `.../2h` P2WSH).
When the account number is zero, a BIP45 (`m/45h`) multisig section is also included
(it is omitted for non-zero accounts, as in the example below).
The feature can be found here: _Advanced > MicroSD > Export Wallet > Generic JSON_ The feature can be found here: _Advanced/Tools > Export Wallet > Generic JSON_
Please contact us (or better yet, make a pull request), if you need something Please contact us (or better yet, make a pull request), if you need something
more in this file. more in this file.
@ -18,32 +21,51 @@ Here is an example, produced by the Simulator for account number 123.
```javascript ```javascript
{ {
"chain": "XTN", "chain": "BTC",
"xfp": "0F056943", "xfp": "0F056943",
"xpub": "tpubD6NzVbkrYhZ4XzL5Dhayo67Gorv1YMS7j8pRUvVMd5odC2LBPLAygka9p7748JtSq82FNGPppFEz5xxZUdasBRCqJqXvUHq6xpnsMcYJzeh",
"account": 123, "account": 123,
"xpub": "xpub661MyMwAqRbcGC9DmWbtbAmuUjpMYxw4BWE88NSDHB3jSjfUK7KtYJuKa52GbowD3DVLkgsxH9QwPnTx5mjdHykYFEncnmAsNsCTbWzBhA7",
"bip44": { "bip44": {
"deriv": "m/44'/1'/123'",
"first": "n44vs1Rv7T8SANrg2PFGQhzVkhr5Q6jMMD",
"name": "p2pkh", "name": "p2pkh",
"xfp": "B7908B26", "xfp": "5F898064",
"xpub": "tpubDCiHGUNYdRRGoSH22j8YnruUKgguCK1CC2NFQUf9PApeZh8ewAJJWGMUrhggDNK73iCTanWXv1RN5FYemUH8UrVUBjqDb8WF2VoKmDh9UTo" "deriv": "m/44h/0h/123h",
"xpub": "xpub6DStQXfAgHuLbMpCf86ruVkF4yT9pSLyWsFiqQTWY9osuinq8Dyee4W5jCjMfyku5LNkRB9oFinrY5ufn9XXEn8Vvzc2jnifKMaQCNV7RBZ",
"desc": "pkh([0f056943/44h/0h/123h]xpub6DStQXfAgHuLbMpCf86ruVkF4yT9pSLyWsFiqQTWY9osuinq8Dyee4W5jCjMfyku5LNkRB9oFinrY5ufn9XXEn8Vvzc2jnifKMaQCNV7RBZ/<0;1>/*)#4tl8jryn",
"first": "1GTNtzG5xX2UhdD5e3Nu7i1WPxFdjxQMJt"
}, },
"bip49": { "bip49": {
"_pub": "upub5DMRSsh6mNak9KbcVjJ7xAgHJvbE3Nx22CBTier5C35kv8j7g2q58ywxskBe6JCcAE2VH86CE2aL4MifJyKbRw8Gj9ay7SWvUBkp2DJ7y52", "name": "p2sh-p2wpkh",
"deriv": "m/49'/1'/123'", "xfp": "A748B1FC",
"first": "2N87V39riUUCd4vmXfDjMWAu9gUCiBji5jB", "deriv": "m/49h/0h/123h",
"name": "p2wpkh-p2sh", "xpub": "xpub6DDm8WzH5a9qjKkttzqSB3uGofNohU9D3n3UG8WMxkUZzJEMPTYiQRf1dvTFCQR82MjGW4LUMVuTtnW4hF17RpzCqVwhf6Z2fnJPWtjG164",
"xfp": "CEE1D809", "desc": "sh(wpkh([0f056943/49h/0h/123h]xpub6DDm8WzH5a9qjKkttzqSB3uGofNohU9D3n3UG8WMxkUZzJEMPTYiQRf1dvTFCQR82MjGW4LUMVuTtnW4hF17RpzCqVwhf6Z2fnJPWtjG164/<0;1>/*))#5j7t2n2u",
"xpub": "tpubDCDqt7XXvhAdy1MpSze5nMJA9x8DrdRaKALRRPasfxyHpiqWWEAr9cbDBQ9BcX7cB3up98Pk97U2QQ3xrvQsi5dNPmRYYhdcsKY9wwEY87T" "_pub": "ypub6Y42SBfCEFhKacx1jMd4P8zmydXFe68hxtZh3XQFLkrT3Q3ae7iH2VK9f8QqCK53Rzr5FXw2pAG1n57dQwR8E4fohqe8F1NWwWN2uVRfBry",
"first": "3CeBRbJKCpg7BpJME2vM8ZxhCjBnhG4toy"
}, },
"bip84": { "bip84": {
"_pub": "vpub5Y5a91QvDT45EnXQaKeuvJupVvX8f9BiywDcadSTtaeJ1VgJPPXMitnYsqd9k7GnEqh44FKJ5McJfu6KrihFXhAmvSWgm7BAVVK8Gupu4fL",
"deriv": "m/84'/1'/123'",
"first": "tb1qc58ys2dphtphg6yuugdf3d0kufmk0tye044g3l",
"name": "p2wpkh", "name": "p2wpkh",
"xfp": "78CF94E5", "xfp": "2C5207AA",
"xpub": "tpubDC7jGaaSE66VDB6VhEDFYQSCAyugXmfnMnrMVyHNzW9wryyTxvha7TmfAHd7GRXrr2TaAn2HXn9T8ep4gyNX1bzGiieqcTUNcu2poyntrET" "deriv": "m/84h/0h/123h",
"xpub": "xpub6CaWStGvcXqSW9BzU2vpCoP7aWjz9VfR5DS2nuYWVvKV2nug2dESg3HdFsaWHeoZaxuAhNcPB3TH2gq8MugS3JX1yGuhB4QbC2BneaYqB16",
"desc": "wpkh([0f056943/84h/0h/123h]xpub6CaWStGvcXqSW9BzU2vpCoP7aWjz9VfR5DS2nuYWVvKV2nug2dESg3HdFsaWHeoZaxuAhNcPB3TH2gq8MugS3JX1yGuhB4QbC2BneaYqB16/<0;1>/*)#yk84tprf",
"_pub": "zpub6rF34DckutvQCjaE8kW4cya7vT2t2jeQuSUUMhLHFw5F8zY8XwZZvAbuJHVgHU7QQF8nCKoW6NANoG4FoJWTdmtDhxJYLt3ZjUK5RqUSMdF",
"first": "bc1qhj6avwmp5lhpgqwm6dgxrf3v5lf67rjm99a8an"
},
"bip48_1": {
"name": "p2sh-p2wsh",
"xfp": "845A3542",
"deriv": "m/48h/0h/123h/1h",
"xpub": "xpub6EkcQSTygvxVnBP2X2fM6HY5D7wv46tWbBc54ADaypuCr47vQh1GPdPAZFdx81ou5Rp4vBnzeJT5MDWDZstzijxkHfrofXRycpt1ASfg1La",
"desc": "sh(wsh(sortedmulti(M,[0f056943/48h/0h/123h/1h]xpub6EkcQSTygvxVnBP2X2fM6HY5D7wv46tWbBc54ADaypuCr47vQh1GPdPAZFdx81ou5Rp4vBnzeJT5MDWDZstzijxkHfrofXRycpt1ASfg1La/0/*,...)))",
"_pub": "Ypub6kUxqLsLQa4M43jXJ3ux8SyP6t8dD5ZbpZmxkpP1jc7VXLW4RkZ76ouEPAZ1gMgiiXzrYFPfzBC8MfjYaoTxfTm1zUfdeqiTnHDX8raCfeg"
},
"bip48_2": {
"name": "p2wsh",
"xfp": "2A01C6B0",
"deriv": "m/48h/0h/123h/2h",
"xpub": "xpub6EkcQSTygvxVneXmk3ywiS2PFhBdiPxeMxYf6RFxHCHH36NxdcN7DjUpudCppAAxs58CG6DQLjtqZNmyC3MpgVob6wpdeATjpZZ1woX92EF",
"desc": "wsh(sortedmulti(M,[0f056943/48h/0h/123h/2h]xpub6EkcQSTygvxVneXmk3ywiS2PFhBdiPxeMxYf6RFxHCHH36NxdcN7DjUpudCppAAxs58CG6DQLjtqZNmyC3MpgVob6wpdeATjpZZ1woX92EF/0/*,...))",
"_pub": "Zpub75KE91YFZFbpup5PMS2AxgZCKRWnozdEWTEmaUKGQysSmUaKuL5WYyf2kk5UNQhhupRnddQe9GzST7crvfLoRTHTg6KtDPZiFjxBJobzcUz"
} }
} }
``` ```
@ -51,16 +73,23 @@ Here is an example, produced by the Simulator for account number 123.
## Notes ## Notes
1. The `first` address is formed by added `/0/0` onto the given derivation, and is assumed 1. The `first` address is formed by added `/0/0` onto the given derivation, and is assumed
to be the first (non-change) receive address for the wallet. to be the first (non-change) receive address for the wallet. It is only present on the
single-signature sections (`bip44`, `bip49`, `bip84`); multisig sections omit it.
1a. Each section includes a `desc` field: a ready-to-import Bitcoin output descriptor
(with `#checksum`). Single-sig descriptors use the `<0;1>/*` multipath form. Multisig
sections (`bip48_1`, `bip48_2`, and `bip45` when present) emit a `sortedmulti(...)`
template with `M` and a trailing `...` as placeholders, to be completed with your
threshold and the other co-signers' keys.
2. The user may specify any value (up to 9999) for the account number, and it's meant to 2. The user may specify any value (up to 9999) for the account number, and it's meant to
segregate funds into sub-wallets. Don't assume it's zero. segregate funds into sub-wallets. Don't assume it's zero.
3. When making your PSBT files to spend these amounts, remember that the XFP of the master 3. When making your PSBT files to spend these amounts, remember that the XFP of the master
(`0F056943` in this example) is is the root of the subkey paths found in the file, and (`0F056943` in this example) is the root of the subkey paths found in the file, and
you must include the full derivation path from master. So based on this example, you must include the full derivation path from master. So based on this example,
to spend a UTXO on `tb1qc58ys2dphtphg6yuugdf3d0kufmk0tye044g3l`, the input section to spend a UTXO on `bc1qhj6avwmp5lhpgqwm6dgxrf3v5lf67rjm99a8an`, the input section
of your PSBT would need to specify `(m=0F056943)/84'/1'/123'/0/0`. of your PSBT would need to specify `(m=0F056943)/84'/0'/123'/0/0`.
4. The `_pub` value is the [SLIP-132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) style "ypub/zpub/etc" which some systems might want. It implies 4. The `_pub` value is the [SLIP-132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) style "ypub/zpub/etc" which some systems might want. It implies
a specific address format. a specific address format.

View File

@ -14,11 +14,12 @@
# PIN Codes # PIN Codes
- 2-2 through 6-6 in size, numeric digits only - 2-2 through 6-6 in size, numeric digits only
- pin code 999999-999999 is reserved (means 'clear pin') - pin code 999999-999999 was reserved (meaning 'clear pin'), but now available again
# Backup Files # Backup Files
- we don't know what day it is, so meta data on files will not have correct date/time - we don't know what day it is, so meta data on files will not have correct date/time
- release date of the firmware version that made the file is used instead of true date
- encrypted files produced cannot be changed, and we don't support other tools making them - encrypted files produced cannot be changed, and we don't support other tools making them
# Micro SD # Micro SD
@ -78,6 +79,7 @@
- multisig wallet `name` can only contain printable ASCII characters `range(32, 127)` - multisig wallet `name` can only contain printable ASCII characters `range(32, 127)`
### BIP-67 ### BIP-67
- importing multisig from PSBT can ONLY create `sortedmulti(...)` multisig according to BIP-67, DO NOT use with `multi(...)` - importing multisig from PSBT can ONLY create `sortedmulti(...)` multisig according to BIP-67, DO NOT use with `multi(...)`
- creating airgapped multisig using COLDCARD as coordinator always produces `sortedmulti(...)` multisig according to BIP-67 - creating airgapped multisig using COLDCARD as coordinator always produces `sortedmulti(...)` multisig according to BIP-67
- COLDCARD import/export [format](https://coldcard.com/docs/multisig/#configuration-text-file-for-multisig) only supports `sortedmulti(...)` multisig according to BIP-67. To import multisig wallet with `multi(...)` use descriptor import [format](https://github.com/bitcoin/bips/blob/master/bip-0383.mediawiki) - COLDCARD import/export [format](https://coldcard.com/docs/multisig/#configuration-text-file-for-multisig) only supports `sortedmulti(...)` multisig according to BIP-67. To import multisig wallet with `multi(...)` use descriptor import [format](https://github.com/bitcoin/bips/blob/master/bip-0383.mediawiki)
@ -138,6 +140,10 @@ We will summarize transaction outputs as "change" back into same wallet, however
- key derivatation paths must be 12 or less in depth (`MAX_PATH_DEPTH`) - key derivatation paths must be 12 or less in depth (`MAX_PATH_DEPTH`)
# Pay-to-Pubkey
- although we have some code for "pay to pubkey" (P2PK not P2PKH), it is untested
and unused since this style of payment address is obsolete and largely unused today
# NFC Feature # NFC Feature
@ -202,9 +208,9 @@ We will summarize transaction outputs as "change" back into same wallet, however
- if you have an XFP collision between multiple wallets in SeedVault (ie. two wallets - if you have an XFP collision between multiple wallets in SeedVault (ie. two wallets
with same descriptors, but different seeds) you will get false negatives with same descriptors, but different seeds) you will get false negatives
# CCC Feature (ColdCard Cosigning) # Spending Policy
- only 12 or 24 word seeds (not XPRV) are accepted for "key C" - (Cosign mode) only 12 or 24 word seeds (not XPRV) are accepted for "key C"
- velocity limit: - velocity limit:
- based on a max magnitude per txn, and a required minimum block height - based on a max magnitude per txn, and a required minimum block height
gap, based on previous `nLockTime` value in last-signed PSBT. gap, based on previous `nLockTime` value in last-signed PSBT.
@ -213,5 +219,5 @@ We will summarize transaction outputs as "change" back into same wallet, however
- PSBT creator must put in `nLockTime` block heights (most already do to avoid fee sniping) - PSBT creator must put in `nLockTime` block heights (most already do to avoid fee sniping)
- maximum of 25 whitelisted addresses can be stored - maximum of 25 whitelisted addresses can be stored
- Web2FA: any number of mobile devices can be enrolled, but all will have the same shared secret - Web2FA: any number of mobile devices can be enrolled, but all will have the same shared secret
- any warning from the PSBT, such as huge fees, will prevent CCC cosign. - any warning from the PSBT, such as huge fees, will cause the transaction to be rejected

View File

@ -38,7 +38,7 @@ directly from python programs.
| Start | Size | Notes | Start | Size | Notes
|---------------|-----------|-------------------------- |---------------|-----------|--------------------------
| 0x0800 0000 | 128k | Bootloader code, including reset vector. See `stm32/mk4-bootloader` | 0x0800 0000 | 112k | Bootloader code, including reset vector. See `stm32/mk4-bootloader`
| 0x0801 c000 | 8k | Sensitive "pairing secrets" for SE1 and SE2 | 0x0801 c000 | 8k | Sensitive "pairing secrets" for SE1 and SE2
| 0x0801 e000 | 8k | MCU keys, consumable; 256 32-bit write-once slots. | 0x0801 e000 | 8k | MCU keys, consumable; 256 32-bit write-once slots.
| 0x0802 0000 | 16k | Interrupt handlers, file header (Micropython and Coldcard code) | 0x0802 0000 | 16k | Interrupt handlers, file header (Micropython and Coldcard code)

View File

@ -30,6 +30,7 @@
Tapsigner Backup Tapsigner Backup
Seed XOR Seed XOR
Migrate Coldcard Migrate Coldcard
Key Teleport (start)
Help Help
Advanced/Tools Advanced/Tools
View Identity View Identity
@ -48,13 +49,13 @@
Import XPRV Import XPRV
Tapsigner Backup Tapsigner Backup
Coldcard Backup Coldcard Backup
Restore Seed XOR
Upgrade Firmware [IF NOT TMP SEED] Upgrade Firmware [IF NOT TMP SEED]
Show Version Show Version
From MicroSD From MicroSD
From VirtDisk [IF VIRTDISK ENABLED] From VirtDisk [IF VIRTDISK ENABLED]
File Management File Management
Verify Backup Verify Backup
Teleport Multisig PSBT [IF QR AND SECRET]
List Files List Files
Verify Sig File Verify Sig File
NFC File Share [IF NFC ENABLED] NFC File Share [IF NFC ENABLED]
@ -157,29 +158,25 @@
Delete PSBTs Delete PSBTs
Default Keep Default Keep
Delete PSBTs Delete PSBTs
Menu Wrapping Buried Settings
Default Off Home Menu XFP [IF SECRET AND NOT TMP SEED]
Enable Only Tmp
Home Menu XFP [IF SECRET AND NOT TMP SEED] Always Show
Only Tmp Menu Wrapping
Always Show Default
Always Wrap
[QR key shortcut] [IF QR SCANNER]
--- ---
[NORMAL OPERATION] [NORMAL OPERATION]
Ready To Sign Ready To Sign
Passphrase [IF WORD BASED SEED] Passphrase [IF WORD BASED SEED]
Restore Saved [MAYBE] Restore Saved
A*********** c*******
[0C52BAD4] [3A14F788]
Restore Restore
Delete Delete
Edit Phrase [MAYBE] Edit Phrase
Add Word [IF NOT QWERTY]
[SEED WORD MENUS]
Add Numbers [IF NOT QWERTY]
Clear All [IF NOT QWERTY]
APPLY [IF NOT QWERTY]
CANCEL [IF NOT QWERTY]
Scan Any QR Code [IF QR SCANNER] Scan Any QR Code [IF QR SCANNER]
Start HSM Mode [IF HSM POLICY] Start HSM Mode [IF HSM POLICY]
Address Explorer Address Explorer
@ -197,35 +194,44 @@
Account Number Account Number
Custom Path Custom Path
CC-2-of-4 CC-2-of-4
Secure Notes & Passwords [IF ENBALED] Secure Notes & Passwords [IF ENBALED] [MAYBE]
1: note1 1: note0
"note1" "note0"
View Note View Note
Edit Edit
Delete Delete
Export Export
SHORTCUT Sign Note Text
SHORTCUT 2: secret-PWD
2: nostr "secret-PWD"
"nostr" ↳ satoshi
↳ scg ↳ abc.org
↳ brb.io
View Password View Password
Send Password [MAYBE] Send Password [MAYBE]
Export Export
Edit Metadata Edit Metadata
Delete Delete
Change Password Change Password
SHORTCUT Sign Note Text
SHORTCUT
New Note New Note
New Password New Password
Export All Export All
Sort By Title
Import Import
Type Passwords [MAYBE] Type Passwords [MAYBE]
Seed Vault [MAYBE] Seed Vault [MAYBE]
1: [B14E9AE0] 1: [7126EB3C]
[B14E9AE0] [7126EB3C]
Use This Seed
Rename
Delete
2: [CCEE13B9]
[CCEE13B9]
Use This Seed
Rename
Delete
3: [03EE9989]
[03EE9989]
Use This Seed Use This Seed
Rename Rename
Delete Delete
@ -236,17 +242,19 @@
Restore Backup Restore Backup
Clone Coldcard Clone Coldcard
Export Wallet Export Wallet
Sparrow
Cove
Bitcoin Core Bitcoin Core
Fully Noded
Sparrow Wallet
Nunchuk Nunchuk
Zeus Bull Bitcoin
Blue Wallet
Electrum Wallet Electrum Wallet
Wasabi Wallet
Fully Noded
Unchained
Theya Theya
Bitcoin Safe Bitcoin Safe
Wasabi Wallet Zeus
Unchained
Lily Wallet
Samourai Postmix Samourai Postmix
Samourai Premix Samourai Premix
Descriptor Descriptor
@ -257,6 +265,7 @@
P2WPKH/P2SH (BIP-49) P2WPKH/P2SH (BIP-49)
Master XPUB Master XPUB
Current XFP Current XFP
Key Expression
Dump Summary Dump Summary
Upgrade Firmware [IF NOT TMP SEED] Upgrade Firmware [IF NOT TMP SEED]
Show Version Show Version
@ -266,17 +275,19 @@
Verify Backup Verify Backup
Backup System Backup System
Export Wallet Export Wallet
Sparrow
Cove
Bitcoin Core Bitcoin Core
Fully Noded
Sparrow Wallet
Nunchuk Nunchuk
Zeus Bull Bitcoin
Blue Wallet
Electrum Wallet Electrum Wallet
Wasabi Wallet
Fully Noded
Unchained
Theya Theya
Bitcoin Safe Bitcoin Safe
Wasabi Wallet Zeus
Unchained
Lily Wallet
Samourai Postmix Samourai Postmix
Samourai Premix Samourai Premix
Descriptor Descriptor
@ -287,10 +298,11 @@
P2WPKH/P2SH (BIP-49) P2WPKH/P2SH (BIP-49)
Master XPUB Master XPUB
Current XFP Current XFP
Key Expression
Dump Summary Dump Summary
Sign Text File Sign Text File
Batch Sign PSBT Batch Sign PSBT
Teleport Multisig PSBT [IF QR AND SECRET] Teleport Multisig PSBT
List Files List Files
Verify Sig File Verify Sig File
NFC File Share [IF NFC ENABLED] NFC File Share [IF NFC ENABLED]
@ -300,29 +312,28 @@
Format SD Card Format SD Card
Format RAM Disk [IF VIRTDISK ENABLED] Format RAM Disk [IF VIRTDISK ENABLED]
Secure Notes & Passwords [IF QWERTY KEYBOARD] Secure Notes & Passwords [IF QWERTY KEYBOARD]
1: note1 1: note0
"note1" "note0"
View Note View Note
Edit Edit
Delete Delete
Export Export
SHORTCUT Sign Note Text
SHORTCUT 2: secret-PWD
2: nostr "secret-PWD"
"nostr" ↳ satoshi
↳ scg ↳ abc.org
↳ brb.io
View Password View Password
Send Password [MAYBE] Send Password [MAYBE]
Export Export
Edit Metadata Edit Metadata
Delete Delete
Change Password Change Password
SHORTCUT Sign Note Text
SHORTCUT
New Note New Note
New Password New Password
Export All Export All
Sort By Title
Import Import
Derive Seeds (BIP-85) Derive Seeds (BIP-85)
View Identity View Identity
@ -341,14 +352,17 @@
Import XPRV Import XPRV
Tapsigner Backup Tapsigner Backup
Coldcard Backup Coldcard Backup
Restore Seed XOR
Key Teleport (start) Key Teleport (start)
Spending Policy [IF SECRET AND NOT TMP SEED]
Single-Signer [IF SECRET AND NOT TMP SEED]
Co-Sign Multisig (CCC) [IF NOT TMP SEED]
HSM Mode [IF HSM AND SECRET]
Default Off
Enable
User Management [MAYBE]
Paper Wallets Paper Wallets
Enable HSM [IF HSM AND SECRET] WIF Store
Default Off
Enable
Coldcard Co-Signing [IF NOT TMP SEED]
User Management [IF HSM AND SECRET]
(no users yet)
NFC Tools [IF NFC ENABLED] NFC Tools [IF NFC ENABLED]
Sign PSBT Sign PSBT
Show Address Show Address
@ -357,7 +371,7 @@
Verify Address Verify Address
File Share File Share
Import Multisig Import Multisig
Push Transaction [IF ENBALED] Push Transaction [IF PUSHTX ENABLED]
Danger Zone Danger Zone
Debug Functions Debug Functions
Seed Functions Seed Functions
@ -398,22 +412,33 @@
Settings Space Settings Space
MCU Key Slots MCU Key Slots
Bless Firmware Bless Firmware
Reflash GPU [IF QWERTY KEYBOARD]
Wipe LFS Wipe LFS
Nuke Device
Settings Settings
Login Settings Login Settings
Change Main PIN Change Main PIN
Trick PINs [IF SECRET AND NOT TMP SEED] Trick PINs [IF SECRET AND NOT TMP SEED]
Trick PINs: Trick PINs:
↳123-254 ↳11-11
PIN 123-254 PIN 11-11
↳Bricks CC
Hide Trick
Delete Trick
Change PIN
↳333-3334
PIN 333-3334
↳Duress Wallet ↳Duress Wallet
Activate Wallet Activate Wallet
Hide Trick Hide Trick
Delete Trick Delete Trick
Change PIN Change PIN
↳WRONG PIN
After 3 wrong:
↳Wipes seed
↳Reboots
Hide Trick
Delete Trick
Add New Trick Add New Trick
Add If Wrong
Delete All Delete All
Set Nickname Set Nickname
Scramble Keys Scramble Keys
@ -458,14 +483,12 @@
View Details View Details
Delete Delete
Coldcard Export Coldcard Export
Electrum Wallet
Descriptors Descriptors
View Descriptor View Descriptor
Export Export
Bitcoin Core Bitcoin Core
Electrum Wallet Import
Import from File
Import from QR [IF QR SCANNER]
Import via NFC [IF NFC ENABLED]
Export XPUB Export XPUB
Create Airgapped Create Airgapped
Trust PSBT? Trust PSBT?
@ -520,17 +543,18 @@
Delete PSBTs Delete PSBTs
Default Keep Default Keep
Delete PSBTs Delete PSBTs
Menu Wrapping
Default Off
Enable
Home Menu XFP [IF SECRET AND NOT TMP SEED]
Only Tmp
Always Show
Keyboard EMU Keyboard EMU
Default Off Default Off
Enable Enable
Buried Settings
Home Menu XFP [IF SECRET AND NOT TMP SEED]
Only Tmp
Always Show
Menu Wrapping
Default
Always Wrap
Secure Logout Secure Logout
SHORTCUT [IF NFC ENABLED] [NFC key shortcut] [IF NFC ENABLED]
Sign PSBT Sign PSBT
Show Address Show Address
Sign Message Sign Message
@ -538,7 +562,7 @@
Verify Address Verify Address
File Share File Share
Import Multisig Import Multisig
Push Transaction [IF ENBALED] Push Transaction [IF PUSHTX ENABLED]
--- ---
[FACTORY MODE] [FACTORY MODE]
@ -550,3 +574,151 @@
Perform Selftest Perform Selftest
--- ---
[SSSP]
Ready To Sign
Passphrase [IF WORD BASED SEED & SSSP RELATED KEYS ENABLED]
Restore Saved
c*******
[3A14F788]
Restore
Delete
Edit Phrase
Scan Any QR Code [IF QR SCANNER]
Address Explorer
Classic P2PKH
↳ mtHSVByP9EYZ⋯Vm19gvpecb5R
P2SH-Segwit
↳ 2NCAJ5wD4Gvm⋯NphNU8UYoEJv
Segwit P2WPKH
↳ tb1qupyd58nd⋯vu9jtdyws9n9
Applications
Samourai
Post-mix
Pre-mix
Wasabi
Account Number
Custom Path
CC-2-of-4
Secure Notes & Passwords[IF ENABLED & SSSP ALLOW NOTES]
1: note0
"note0"
View Note
Sign Note Text
2: secret-PWD
"secret-PWD"
↳ satoshi
↳ abc.org
View Password
Send Password [MAYBE]
Sign Note Text
Type Passwords [MAYBE]
Seed Vault[IF ENABLED & SSSP RELATED KEYS ENABLED]
1: [7126EB3C]
[7126EB3C]
Use This Seed
2: [CCEE13B9]
[CCEE13B9]
Use This Seed
3: [03EE9989]
[03EE9989]
Use This Seed
Advanced/Tools
File Management
Sign Text File
Batch Sign PSBT
List Files
Export Wallet
Sparrow
Cove
Bitcoin Core
Nunchuk
Bull Bitcoin
Blue Wallet
Electrum Wallet
Wasabi Wallet
Fully Noded
Unchained
Theya
Bitcoin Safe
Zeus
Samourai Postmix
Samourai Premix
Descriptor
Generic JSON
Export XPUB
Segwit (BIP-84)
Classic (BIP-44)
P2WPKH/P2SH (BIP-49)
Master XPUB
Current XFP
Key Expression
Dump Summary
Verify Sig File
NFC File Share [IF NFC ENABLED]
BBQr File Share [IF QR SCANNER]
QR File Share [IF QR SCANNER]
Format SD Card
Format RAM Disk [IF VIRTDISK ENABLED]
Export Wallet
Sparrow
Cove
Bitcoin Core
Nunchuk
Bull Bitcoin
Blue Wallet
Electrum Wallet
Wasabi Wallet
Fully Noded
Unchained
Theya
Bitcoin Safe
Zeus
Samourai Postmix
Samourai Premix
Descriptor
Generic JSON
Export XPUB
Segwit (BIP-84)
Classic (BIP-44)
P2WPKH/P2SH (BIP-49)
Master XPUB
Current XFP
Key Expression
Dump Summary
Teleport Multisig PSBT [MAYBE]
View Identity
Temporary Seed [IF SSSP RELATED KEYS ENABLED]
Import from QR Scan [IF QR SCANNER]
Import Words
12 Words
18 Words
24 Words
Import via NFC [IF NFC ENABLED]
Import XPRV
Tapsigner Backup
Coldcard Backup
Restore Seed XOR
Paper Wallets
WIF Store
NFC Tools [IF NFC ENABLED]
Sign PSBT
Show Address
Sign Message
Verify Sig File
Verify Address
File Share
Push Transaction [IF PUSHTX ENABLED]
Show Firmware Version
Destroy Seed [IF SECRET AND NOT TMP SEED]
Secure Logout
EXIT TEST DRIVE [MAYBE]
[NFC key shortcut] [IF NFC ENABLED]
Sign PSBT
Show Address
Sign Message
Verify Sig File
Verify Address
File Share
Push Transaction [IF PUSHTX ENABLED]
---

View File

@ -2,15 +2,16 @@
COLDCARD can sign messages send to it via USB with the help of `ckcc` utility, COLDCARD can sign messages send to it via USB with the help of `ckcc` utility,
sign messages provided via specially crafted file on SD card or Vdisk, sign messages provided via specially crafted file on SD card or Vdisk,
and Mk4 can also sign messages sent to COLDCARD via NFC. and NFC-equipped models (Mk4, Mk5, and Q) can also sign messages sent to COLDCARD via NFC.
The resulting signature can be returned over SD card/Vdisk, NFC, or — on Q — as a QR code.
Signature format follows [BIP-0137](https://github.com/bitcoin/bips/blob/master/bip-0137.mediawiki) specification. Signature format follows [BIP-0137](https://github.com/bitcoin/bips/blob/master/bip-0137.mediawiki) specification.
COLDCARD Mk3 and COLDCARD Mk4 up to version `5.1.0` used compressed P2PKH header byte for all script types. COLDCARD Mk3 and COLDCARD Mk4 up to version `5.1.0` used compressed P2PKH header byte for all script types.
From Mk4 `5.1.0` correct header byte is used for corresponding script type. From version `5.1.0` correct header byte is used for corresponding script type.
### Verification ### Verification
From COLDCARD Mk4 version `5.1.0` users can verify signed messages directly on the device. From version `5.1.0` users can verify signed messages directly on the device.
If signature file is on SD card or Virtual disk `Advanced/Tools -> File Management -> Verify Sig File`. In case If signature file is on SD card or Virtual disk `Advanced/Tools -> File Management -> Verify Sig File`. In case
signature file is detached signature of signed export (or any other file), COLDCARD can check if digest of file signature file is detached signature of signed export (or any other file), COLDCARD can check if digest of file
specified in the message matches contents of file. This requires file signed to be available on SD card or Vdisk. specified in the message matches contents of file. This requires file signed to be available on SD card or Vdisk.
@ -21,7 +22,7 @@ Bitcoin core can only verify P2PKH.
## Signed Exports ## Signed Exports
From Mk4 version `5.1.0` most of SD card and Virtual disk exports are accompanied by detached signature file. From version `5.1.0` most of SD card and Virtual disk exports are accompanied by detached signature file.
If exported file name is `addresses.csv` signature file name will be `addresses.sig`. If exported file name is `addresses.csv` signature file name will be `addresses.sig`.
### Message construction and signature file format ### Message construction and signature file format
@ -39,8 +40,6 @@ IFOvGVJrm31S0j+F4dVfQ5kbRKWKcmhmXIn/Lw8iIgaCG5QNZswjrN4X673R7jTZo1kvLmiD4hlIrbuL
-----END BITCOIN SIGNATURE----- -----END BITCOIN SIGNATURE-----
``` ```
### What is signed
### What Is Signed ### What Is Signed
1. **Single sig address explorer exports:** Signed by the key corresponding to the first (0th) address on the exported list. 1. **Single sig address explorer exports:** Signed by the key corresponding to the first (0th) address on the exported list.
@ -60,14 +59,4 @@ IFOvGVJrm31S0j+F4dVfQ5kbRKWKcmhmXIn/Lw8iIgaCG5QNZswjrN4X673R7jTZo1kvLmiD4hlIrbuL
6. **Multisig exports:** public keys are encoded as P2PKH address for all multisg signature exports 6. **Multisig exports:** public keys are encoded as P2PKH address for all multisg signature exports
* Multisig wallet descriptor: signed by the key corresponding to the first external address of own enrolled extended key `my_key/0/0` * Multisig wallet descriptor: signed by the key corresponding to the first external address of own enrolled extended key `my_key/0/0`
* Generic XPUBs export: signed by the key corresponding to the first external address of own standard P2WSH derivation `m/48h/<coin_type>h/<account>h/2h/0/0` * Generic XPUBs export: signed by the key corresponding to the first external address of own standard P2WSH derivation `m/48h/<coin_type>h/<account>h/2h/0/0`
* Multisig address explorer export: Signed by own key at the same derivation as first (0th) row on exported list. `my_key/<change>/<start_index>` * Multisig address explorer export: Signed by own key at the same derivation as first (0th) row on exported list. `my_key/<change>/<start_index>`
### What is NOT signed
Multisig exports and generic multisig xpub exports are not signed. It is not clear at this point
whether to sign these exports with some generic single signature key (i.e. `m/44'/<coin_type>'/0'/0/0`)
or with our portion (leg) of script. In both cases script type (address format) would not match as multisignature
message signing is not standardized.
1. **Multisig exports**
2. **Generic multisig exports**

View File

@ -1,10 +1,20 @@
# NFC and Coldcard Mk4 # NFC and Coldcard
(Applies to Coldcard Mk4 only) (Applies to NFC-equipped models: Mk4, Mk5, and Q)
## Usage ## Usage
Mk4 NFC antenna is centered under number `8` on the keypad. Before using NFC, The NFC antenna location depends on the hardware:
- **Mk4**: a PCB trace loop, centered under number `8` on the keypad.
- **Mk5**: a discrete coil (`L6`) in the **top-right corner** of the device
- **Q1**: a flexible "sticker" antenna behind the display. The green LED below the
bottom-right of the display (`D12`) lights up while an NFC transfer is active —
it is the activity indicator, not the antenna.
![nfc tap sweet-spots per model](nfc-tap-locations.png)
Before using NFC,
it is important to locate the position of NFC antenna on your device and point it it is important to locate the position of NFC antenna on your device and point it
correctly towards the Coldcard NFC antenna. Picture below shows an example with iPhone correctly towards the Coldcard NFC antenna. Picture below shows an example with iPhone
that has NFC antenna located at the top right edge. The NFC smartphone antenna that has NFC antenna located at the top right edge. The NFC smartphone antenna
@ -36,7 +46,7 @@ in general. Good interoperability is critical with radio standards.
## Lower Layers ## Lower Layers
The Coldcard Mk4 has an chip that acts as a Type 5 NFC tag. The The Coldcard has a chip that acts as a Type 5 NFC tag. The
radio standard is called "NFC-V" or ISO-15693, and operates on a radio standard is called "NFC-V" or ISO-15693, and operates on a
13.56 Mhz carrier wave. 13.56 Mhz carrier wave.
@ -58,9 +68,13 @@ unless we are actively sharing something. We disable the "energy
harvesting" features of the chip, so it will not do anything when harvesting" features of the chip, so it will not do anything when
the Coldcard is powered-down, regardless of the NFC setting. the Coldcard is powered-down, regardless of the NFC setting.
If the above is not enough for you, the antenna can be destroyed If the above is not enough for you, the antenna can be destroyed:
by cutting the trace labeled "NFC" inside the hole for the MicroSD
card. Use the point of a sharp knife to cut and peel up the trace. - **Mk4**: cut the trace labeled "NFC" inside the hole for the MicroSD card,
using the point of a sharp knife to cut and peel up the trace.
- **Mk5**: has no such trace — its antenna is the discrete coil `L6` in the
top-right corner, which would have to be physically removed instead.
- **Q1**: cut the trace labeled "NFC DATA" under the batteries.
The NFC traffic is not encrypted and is subject to eavesdropping. The NFC traffic is not encrypted and is subject to eavesdropping.
While the NFC feature is active, your Coldcard can be uniquely While the NFC feature is active, your Coldcard can be uniquely

View File

@ -34,7 +34,7 @@ The COLDCARD needs a URL prefix. To that it appends some values:
- when RegTest is enabled, the value will be `XRT` - when RegTest is enabled, the value will be `XRT`
We provide a few default URL values to our customers, including one backend we We provide a few default URL values to our customers, including one backend we
will operate on `colcard.com`. The URL can also be directly entered by the will operate on `coldcard.com`. The URL can also be directly entered by the
customer. On the Q, it can be scanned from a QR code. customer. On the Q, it can be scanned from a QR code.
For COLDCARD backend, the url used is: For COLDCARD backend, the url used is:

BIN
docs/nfc-tap-locations.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

View File

@ -11,11 +11,17 @@ The entrypoint makefile for repro builds.
The `repro` command in `shared.mk` is the first step in the repro build process, which triggers a docker build and run process. The `repro` command in `shared.mk` is the first step in the repro build process, which triggers a docker build and run process.
```makefile ```makefile
repro: submods-match code-committed
repro: repro:
docker build -t coldcard-build - < dockerfile.build docker build -t coldcard-build - < dockerfile.build
(cd ..; docker run $(DOCK_RUN_ARGS) sh src/stm32/repro-build.sh $(VERSION_STRING) $(MK_NUM)) (cd ..; docker run $(DOCK_RUN_ARGS) sh src/stm32/repro-build.sh $(VERSION_STRING) $(HW_MODEL) $(PARENT_MKFILE))
``` ```
`$(HW_MODEL)` is the model string (e.g. `mk4`, `q1`) and `$(PARENT_MKFILE)` is the
top-level makefile being used (`MK-Makefile` or `Q1-Makefile`). The `submods-match`
and `code-committed` prerequisites ensure the submodules and working tree are clean
before a repro build.
Below are interesting sections from the docker logs that give an idea as to what is going on in build process: Below are interesting sections from the docker logs that give an idea as to what is going on in build process:
```stdout ```stdout
@ -61,19 +67,19 @@ Successfully installed signit-1.0
... ...
+ make -f MK4-Makefile setup + make -f MK-Makefile setup
... ...
+ make -f MK4-Makefile firmware-signed.bin firmware-signed.dfu production.bin dev.dfu firmware.lss firmware.elf + make -f MK-Makefile firmware-signed.bin firmware-signed.dfu production.bin dev.dfu firmware.lss firmware.elf
... ...
signit sign -b l-port/build-COLDCARD_MK4 -m 4 5.0.7 -o firmware-signed.bin signit sign -b l-port/build-COLDCARD_MK4 -m mk4 5.0.7 -o firmware-signed.bin
... ...
signit sign -m 4 5.0.7 -r firmware-signed.bin -k 1 -o production.bin signit sign -m mk4 5.0.7 -r firmware-signed.bin -k 1 -o production.bin
You don't have that key (1), so using key zero instead! You don't have that key (1), so using key zero instead!
... ...
@ -96,7 +102,7 @@ production.bin
... ...
+ make -f MK4-Makefile 'PUBLISHED_BIN=/tmp/checkout/firmware/releases/2022-10-05T1724-v5.0.7-mk4-coldcard.dfu' check-repro + make -f MK-Makefile 'PUBLISHED_BIN=/tmp/checkout/firmware/releases/2022-10-05T1724-v5.0.7-mk4-coldcard.dfu' check-repro
... ...
@ -183,7 +189,7 @@ To summarize `check-repro`:
- `split` (cli/signit.py: Line 153-175) is run against the release `*.dfu` resulting in a `check-fw.bin` and `check-bootrom.bin`. "This splits the DFU file into the two parts it contains: the main firmware (COLDCARD application) and the boot loader code." - `split` (cli/signit.py: Line 153-175) is run against the release `*.dfu` resulting in a `check-fw.bin` and `check-bootrom.bin`. "This splits the DFU file into the two parts it contains: the main firmware (COLDCARD application) and the boot loader code."
- `check` (cli/signit.py: Line 176-241) is run against each the release `check-fw.bin` and our built `firmware-signed.bin`. - `check` (cli/signit.py: Line 176-243) is run against each the release `check-fw.bin` and our built `firmware-signed.bin`.
- a hexdump is taken of each the release `check-fw.bin` and our built `firmware-signed.bin` piped through $TRIM_SIG which removes 64 bytes of signature data and subsitutes it with a common string. - a hexdump is taken of each the release `check-fw.bin` and our built `firmware-signed.bin` piped through $TRIM_SIG which removes 64 bytes of signature data and subsitutes it with a common string.

View File

@ -0,0 +1,112 @@
# BIP-322 Generic Signed Message Format
BIP-322 specification: <https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki>
## Proof of Reserves (POR)
### PoR PSBT
COLDCARD accepts a specially crafted PSBT file to sign as BIP-322 Proof of Reserves. The PSBT
must meet all these requirements:
* COLDCARD acts as a BIP-322 PSBT signer. It validates the BIP-322 `to_sign`
transaction, shows the message from `PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE`, and
adds signatures to the PSBT. Finalizing and encoding the final BIP-322
signature string is the responsibility of the finalizer.
* PSBT MUST include `PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE = 0x09`; the value is
the exact message shown to the user and signed by BIP-322.
* PSBT requires `PSBT_IN_BIP32_DERIVATION` for each input
* P2SH wrapped segwit addresses MUST have proper redeem script in PSBT: `PSBT_IN_REDEEM_SCRIPT`
* P2WSH segwit addresses MUST have proper witness script in PSBT: `PSBT_IN_WITNESS_SCRIPT`
* PSBT (`to_sign`) MUST have at least one input.
* First (0th) input of `to_sign` MUST spend the BIP-322 `to_spend` output.
* Input 0 MUST include one of `PSBT_IN_NON_WITNESS_UTXO` or `PSBT_IN_WITNESS_UTXO`.
* When input 0 provides `PSBT_IN_WITNESS_UTXO`, COLDCARD reconstructs the
expected `to_spend` txid from `PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE` and the
witness UTXO scriptPubKey.
* When input 0 provides `PSBT_IN_NON_WITNESS_UTXO`, it MUST be the BIP-322
`to_spend` transaction as defined in
[BIP-322](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full):
* 1 input, 1 output
* output nValue is 0
* input prevout hash is 0
* input prevout n is 0xffffffff
* input scriptSig is `OP_0 PUSH32 message_hash`
* PSBT (`to_sign`) MUST only have one output with null-data `OP_RETURN`
* `to_sign` transaction version MUST be 0 or 2.
* Optionally inputs can be added to `to_sign` for Proof of Reserve signing.
* PSBT MUST be version 0 or 2.
* Foreign inputs not allowed in POR PSBT.
The signatures created by the BIP-322 process will never be suitable
for a on-chain Bitcoin transaction that could move funds, because
of these restrictions imposed by BIP-322.
### Output
COLDCARD always returns a signed PSBT for BIP-322 message signing and Proof of
Reserves. It never returns an extracted/finalized transaction for these PSBTs.
This is true even when finalization is requested over USB, such as with
`ckcc unsigned.psbt --finalize`.
The signed PSBT is the handoff artifact for the external finalizer/verifier. It
keeps the PSBT metadata needed to verify or finalize the BIP-322 signature,
including public keys, scripts, partial signatures, and UTXO data. This matters
because the address being proven normally commits only to a hash of the public
key or script, not the public key or script itself.
### Proof of Reserves Signing Experience
After Coldcard recognizes a BIP-322 PSBT it reads the message from
`PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE` and shows it to the user for approval.
COLDCARD verifies that the message hash matches the input 0 `to_spend`
commitment before offering to sign.
When the PSBT contains only input 0, COLDCARD labels the request as
`BIP-322 Message`, because it is message signing and does not prove ownership
of any additional reserve UTXOs. In that case it does not show transaction
input/output counts. When the PSBT contains additional inputs, COLDCARD labels
the request as `Proof of Reserves` and shows the reserve amount.
If the message contains non-ASCII characters, COLDCARD warns that some
characters may not be readable on screen.
Legacy PoR PSBTs without `PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE` are rejected by
this flow.
Read more [here.](https://gist.github.com/orangesurf/0c1d0a31d3ebe7e48335a34d56788d4c)
Example screen text for a one-input BIP-322 message signing PSBT:
```text
BIP-322 Message
Message:
This is the signed message
Challenge Address:
bc1qzvjnhf7k70uxv6xvneaqxql7k09dd6nsr5wheq
Press ENTER to approve and sign message. Press (2) to explore transaction.
CANCEL to abort.
```
Example screen text for a Proof of Reserves PSBT:
```text
Proof of Reserves
Message:
POR
Amount 0.20000000 BTC
Challenge Address:
bc1qzvjnhf7k70uxv6xvneaqxql7k09dd6nsr5wheq
21 inputs
1 output
Press ENTER to approve and sign proof of reserves. Press (2) to explore transaction.
CANCEL to abort.
```

View File

@ -92,8 +92,8 @@ increases flexibility and resistance to known plain text attacks.
| `pin stretch` | slot 2 | HMAC | SE1 | Key stretching for PIN entry and anti-phish words | `pin stretch` | slot 2 | HMAC | SE1 | Key stretching for PIN entry and anti-phish words
| `firmware` | slot 14 | SHA256d | SE1 | Firmware checksum, controls green/red LEDs | `firmware` | slot 14 | SHA256d | SE1 | Firmware checksum, controls green/red LEDs
| `nonce/chksum` | slot 10 | data | SE1 | AES nonce and GMAC tag, protected by PIN | `nonce/chksum` | slot 10 | data | SE1 | AES nonce and GMAC tag, protected by PIN
| `SE2 easy key` | page 15 | AES via HMAC | SE2 | Another SE2 part of AES seed key | `SE2 easy key` | page 14 | AES via HMAC | SE2 | Another SE2 part of AES seed key
| `SE2 hard key` | page 14 | AES via ECC | SE2 | SE2's part of AES seed key; ECC used to unlock | `SE2 hard key` | page 15 | AES via ECC | SE2 | SE2's part of AES seed key; ECC used to unlock
| `tpin key` | `tpin_key` | HMAC(key) | MCU | Key for HMAC used to encrypt trick PINs | `tpin key` | `tpin_key` | HMAC(key) | MCU | Key for HMAC used to encrypt trick PINs
| `trick PIN slots` | pages 0-12 | HMAC | SE2 | Protect duress wallet seeds and pins (6 spots) | `trick PIN slots` | pages 0-12 | HMAC | SE2 | Protect duress wallet seeds and pins (6 spots)
| `SE2 trash` | secret B | HMAC | SE2 | Used to destroy values (only SE2 knows the value) | `SE2 trash` | secret B | HMAC | SE2 | Used to destroy values (only SE2 knows the value)

View File

@ -1,4 +1,4 @@
# COLDCARD Mk4/Q Security Model # COLDCARD Mk4/Mk5/Q Security Model
## Abstract ## Abstract
@ -96,9 +96,10 @@ user entered the True PIN. An attacker will only have access to the
duress wallet. They won't have access to steal the main stash. duress wallet. They won't have access to steal the main stash.
The private key can be automatically derived using BIP-85 methods, The private key can be automatically derived using BIP-85 methods,
based on account numbers 1001, 1002, or 1003. Because this is BIP-85 based on account numbers 1001, 1002, or 1003 for a 24-word duress wallet
based and uses a 24-word seed, it behaves exactly like a normal (or 2001, 2002, 2003 for a 12-word one). Because this is BIP-85
wallet. Defining a passphrase for the wallet is also possible. based, it behaves exactly like a normal wallet. Defining a passphrase
for the wallet is also possible.
The Mk4 also supports older COLDCARD duress wallets and their UTXOs The Mk4 also supports older COLDCARD duress wallets and their UTXOs
on the blockchain. There is an option to create compatible wallets on the blockchain. There is an option to create compatible wallets
@ -243,7 +244,7 @@ COLDCARD's case to do so, but the option is there if needed.
## SD Card Recovery Mode ## SD Card Recovery Mode
Mk4/Q bootloader is smart enough to be able to read an SD card. You Mk4/Mk5/Q bootloader is smart enough to be able to read an SD card. You
will only be able to trigger the SD card loading code, if the will only be able to trigger the SD card loading code, if the
COLDCARD was powered down during the upgrade process. At that point, COLDCARD was powered down during the upgrade process. At that point,
the intended firmware image has been lost because it it held in the intended firmware image has been lost because it it held in

View File

@ -12,7 +12,7 @@ are not discrete and you could be compelled to produce the passphrase.
Enter [_Seed XOR_](https://seedxor.com), a plausibly deniable means Enter [_Seed XOR_](https://seedxor.com), a plausibly deniable means
of storing secrets in two or more parts that look and behave just of storing secrets in two or more parts that look and behave just
like the original secret. One 12 or 24-word seed phrase becomes two or more parts like the original secret. One 12-, 18-, or 24-word seed phrase becomes two or more parts
that are also BIP-39 compatible seeds phrases. These should be backed up in your that are also BIP-39 compatible seeds phrases. These should be backed up in your
preferred method, metal or otherwise. These parts can be individually loaded preferred method, metal or otherwise. These parts can be individually loaded
with honeypot funds as each one has same word length, with the last being with honeypot funds as each one has same word length, with the last being
@ -78,10 +78,12 @@ words right the next day.
When the parts are made deterministically, we take a double-SHA256 over When the parts are made deterministically, we take a double-SHA256 over
a fixed string (`Batshitoshi`), your master secret, and the text a fixed string (`Batshitoshi`), your master secret, and the text
`1 of 4 parts` which changes for each part. `0 of 4 parts` which changes for each part (the index is 0-based).
In random mode, we simply pick 32 random bytes (and then double-SHA256 In random mode, we simply pick random bytes (and then double-SHA256
them) from the Coldcard's True Random Number Generator (TRNG).. them) from the Coldcard's True Random Number Generator (TRNG). The number
of bytes matches your secret length: 16, 24, or 32 bytes for a 12-, 18-,
or 24-word seed respectively.
This is done to make all but the one part. The final part is the This is done to make all but the one part. The final part is the
value needed to get back to your secret, so it's the XOR of the value needed to get back to your secret, so it's the XOR of the
@ -157,6 +159,12 @@ with the others on a SEEDPLATE.
- right to A, down to B ... take that number, and go to that column - right to A, down to B ... take that number, and go to that column
- down to C, that is answer: a &oplus; b &oplus; c - down to C, that is answer: a &oplus; b &oplus; c
## Open Standard
Seed XOR is an open standard. Other software and hardware wallets are encouraged to
implement support. No license or permission is required, including usage of the term
"Seed XOR" when referring to implementations of this feature. Such implementations
should match the process described in this documentation and be fully interoperable.
--- ---
# 24 Words XOR Seed Example Using 3 Parts # 24 Words XOR Seed Example Using 3 Parts

209
docs/spending-policy.md Normal file
View File

@ -0,0 +1,209 @@
# Spending Policy
This special mode will stop you from signing transactions if they
exceed a spending policy you define beforehand. Once enabled, many
features of the COLDCARD are disabled or inaccessible.
You might want to use this feature when traveling with your COLDCARD.
## Spending Policy: Multisig (formerly CCC)
We also support a mode where the COLDCARD is a multisig co-signer
and only performs its signature when a spending policy is met. The
other multisig signers are free to sign or not sign as appropriate.
Multisig mode is more advanced and requires use of multisig addresses,
new UTXO, and cooperating multisig on-chain wallets.
This document will only discuss the "Single signer" version of
Spending Policy. Both modes can be active at the same time, but if
a transaction would be signed by Multisig policy, then we assume
it's also okay to sign your main key as well.
# Before You Start
When a Spending Policy is in effect, there are limitations
in effect:
- Firmware updates are blocked.
- There is no way to backup the COLDCARD.
- Seed vault and Secure Notes are read-only (and can also be hidden).
- Settings menu is inaccessible.
- BIP-39 passphrases may be blocked (optional).
We recommend getting the COLDCARD fully configured and setup
for typical transactions before enabling the Spending Policy.
# Setup Spending Policy
Visit `Advanced / Tools > Spending Policy` menu and choose
"Single-Signer". First some background information is shown,
then you are prompted to define the "Bypass PIN". This PIN code
is only used when you need to disable the spending policy, but is
also the only way to do so once enabled... so don't loose it.
Once the "Bypass PIN" is confirmed, you will arrive at menu for
related settings. Use "Edit Policy..." to change the spending policy
and define a Max Magnitude (limit number of BTC per transaction),
Velocity (minimum time gaps between signed transactions). You can
define a whitelist of up to 25 destination addresses (leave empty
for any). Finally you can enroll your phone in 2FA (second factor)
so that you must open an Authenticator app on your phone before
transactions are signed.
## Other Security Settings
In addition to policy itself, there are a number of on/off
switches which affect operation of the COLDCARD while the Spending
Policy is in effect:
### Word Check
If enabled, you will have to enter the first and last seed word
after the Bypass PIN as an additional security check.
### Allow Notes
On the Q, secure notes and passwords may be visible or hidden
using this setting. In either case they are strictly readonly.
### Related Keys
BIP-39 passphrase entry, Seed Vault usage will be blocked unless this
setting is enabled. Even when enabled, the Seed Vault is always readonly
and cannot be changed.
# Other Menu Items
## Last Violation
If you have recently tried and failed to sign a transaction, the
reason for the transaction being rejected can be viewed and cleared,
using menu item "Last Violation". It is shown only if a Spending
Policy violation (attempt) has occurred since the last valid signing.
This is meant as a debugging tool, and the information stored is
terse.
## Remove Policy
This will remove your spending policy completely and remove
the Bypass PIN. Your COLDCARD will be back to normal.
## Test Drive
Experiment with how the COLDCARD will function if the Spending
Policy was enabled. You can try to sign transactions that should
be rejected and view the menus in the new mode without rebooting.
Choose "EXIT TEST DRIVE" on top menu to return to the Spending
Policy menu. Reboot will also restore normal operation without
any special challenges.
## ACTIVATE
This step will enable the Spending Policy and return to the
main menu with it in effect. When you reboot the COLDCARD,
the policy will still be in effect. You must use the
Bypass PIN, followed by the normal main PIN, possibly
followed by entering the first and last words of your seed
phrase, before you can disable and change the policy.
We recommend test-driving the feature before doing that.
# Tips and Tricks
## Money Manager Mode
You could setup a Coldcard for another person, perhaps a family member,
and enable web 2FA authentication. There does not need to be any
other spending policy limits (velocity could be unlimited).
Then enroll your own phone with the required 2FA values, and
keep both that and the spending policy bypass PIN confidential.
The holder the the Coldcard will need a 2FA code from your phone
when they want to spend. They can call you for the 6-digit code
from the 2FA app on your phone. This is not hard to provide over a
voice call.
Because a spending policy is in effect, they will not be able to
see the seed words, other private key material, so regardless of
any spoofing or phishing, they cannot move funds without your help.
You should record the bypass PIN, so it can be revealed somehow,
should you die. You do not need to share the risks associated with
holding a copy of the seed words.
## Passphrase Considerations
If you are using the same BIP-39 passphrase for everything, you should
probably do a "Lock Down Seed" (Advanced/Tools > Danger Zone > Seed
Functions) first. This takes your master seed and BIP-39 passphrase
and cooks them together into an XPRV which then is stored as your
master secret. (Replacing the master seed phrase.) This process
cannot be reversed, so other funds you may have on the same seed
words are protected. Once you are operating in XPRV mode, you can
define a spending policy, and know that it is restricted to only
that wallet.
When operating in XPRV mode, the "Passphrase" menu item is not shown
because BIP-39 passwords cannot be applied to XPRV secrets.
## Trick PIN Thoughts
When doing your game theory w.r.t to bypass mode and this feature,
remember that you should assume the attacker already has your main
PIN. That's how they know they cannot spend all your coin, because
they either tried to, or noticed the menus are very limited. They also
have all your UTXO locations and total wallet balance (because they
can export your xpubs to any wallet and load balance from there).
Therefore, a trick pin that leads to a duress wallet after giving up
the bypass unlock PIN, will not fool them. Best would be to provide
a false bypass PIN that is in fact a brick/wipe PIN.
## Lock Out Changes to Policy
In the Trick Pin menu once Spending Policy has been enabled, you will
find the Bypass PIN listed. You could delete or "hide" it. Hiding
it is pointless since you cannot get to the trick PIN menu while
the policy is in effect. Deleting the PIN however, is useful because
it assures changes to spending policy are impossible. To recover
the COLDCARD when this move is later regretted, under Advanced,
there is "Destroy Seed" option which will clear the seed words and
all settings, including the spending policy.
### Unlock Policy & Wipe
We've provided a new trick PIN that pretends to be the unlock
spending policy pin, so the login sequence is correct... but it
will wipe the seed in the process. It will be obvious to your
attackers that you've wiped the seed because the main PIN will lead
to blank wallet now (no seed loaded).
### Delta Mode and Spending Policy
If, from the start, you gave your "delta mode PIN" to the attackers,
then when they bypass the policy (after also getting the bypass PIN
from you), they will still be in Delta Mode.
They could attempt unlimited spending, but transactions signed will
not be valid. If they try to view the seed words or generally export
private key material, they will hit many of the "wipe seed if delta
mode" cases.
## Forgotten Bypass PIN Code
If you've enabled a spending policy and still remember the main PIN,
but cannot disable the feature because you've forgotten the Bypass
PIN, your only option is to use `Advanced > Destroy Seed`. After
some confirmations, this erases the master seed, all settings, seed
vault items, secure notes, and trick pins. It's basically a factory
reset except for the main PIN code which is unchanged. Once you've
done that, you can enter your seed words from backup (or restore a
backup file) and continue to use the COLDCARD again.

View File

@ -1,7 +1,7 @@
# Temporary Seeds # Temporary Seeds
[_(new in v5.0.7, requires Mk4)_](upgrade.md) [_(new in v5.0.7, requires Mk4, Mk5, or Q)_](upgrade.md)
Temporary seed (renamed in `5.2.0` from Ephemeral seed) is a temporary secret completely separate Temporary seed (renamed in `5.2.0` from Ephemeral seed) is a temporary secret completely separate
@ -42,7 +42,7 @@ Read more about `Seed Vault` feature below.
- `24 words` - `24 words`
- `XPRV (BIP-32)` - `XPRV (BIP-32)`
- pick derivation `Index` in next prompt, or just press OK for index 0 - pick derivation `Index` in next prompt, or just press OK for index 0
- Press (2) in next prompt to activate derived secret as a temporary seed - Press (0) in next prompt to activate derived secret as a temporary seed
* temporary seed can be activated from Duress Wallet * temporary seed can be activated from Duress Wallet
- go to `Settings -> Login Settings -> Trick Pins` - go to `Settings -> Login Settings -> Trick Pins`
@ -66,7 +66,7 @@ Ability to generate and use **Temporary seed** is available on Coldcard when:
# Restore Master # Restore Master
[_(new in v5.2.0, requires Mk4)_](upgrade.md) [_(new in v5.2.0, requires Mk4, Mk5, or Q)_](upgrade.md)
From version `5.2.0` users no longer need to reboot COLDCARD to return From version `5.2.0` users no longer need to reboot COLDCARD to return
to their "master seed" (one stored in SE2). Once COLDCARD has temporary to their "master seed" (one stored in SE2). Once COLDCARD has temporary
@ -84,7 +84,7 @@ Seed Vault entries can only be deleted in Seed Vault menu.
# Seed Vault # Seed Vault
[_(new in v5.2.0, requires Mk4)_](upgrade.md) [_(new in v5.2.0, requires Mk4, Mk5, or Q)_](upgrade.md)
Seed Vault adds the ability to store multiple temporary secrets into encrypted settings for simple Seed Vault adds the ability to store multiple temporary secrets into encrypted settings for simple
recall and later use (AES-256-CTR encrypted with your master seed's key). recall and later use (AES-256-CTR encrypted with your master seed's key).

View File

@ -1,7 +1,7 @@
# Firmware Upgrade and Recovery Process # Firmware Upgrade and Recovery Process
_This document applies only to the Mk4. Earlier COLDCARDs did not use this approach._ _This document applies to the Mk4, Mk5, and Q. Earlier COLDCARDs did not use this approach._
On the COLDCARD, we have done away with the slow external SPI flash On the COLDCARD, we have done away with the slow external SPI flash
(serial flash) chip entirely (used in Mk1-Mk3). In it's place we (serial flash) chip entirely (used in Mk1-Mk3). In it's place we

View File

@ -18,8 +18,11 @@ for the COLDCARD Q, it is a QR code to be scanned.
The HSM feature uses HOTP tokens, which do not require a backend, The HSM feature uses HOTP tokens, which do not require a backend,
but are not as robust as time-based tokens. but are not as robust as time-based tokens.
For now, Web2FA is only being used as part of CCC spending policy (opt-in), Web2FA is available to be enabled as part of a Spending Policy,
but we may find other uses for it. both in Multisig and Single Signer modes. When enabled, you will be
prompted complete 2FA authentication after viewing the details of
the transaction to be signed. You will not be able to sign without
the correct code.
## How It Works ## How It Works
@ -27,8 +30,8 @@ but we may find other uses for it.
- Usual 2fa base32 secret is picked by CC and stored in CC (so that server is stateless) - Usual 2fa base32 secret is picked by CC and stored in CC (so that server is stateless)
- CC creates URL encrypted to the pubkey of server, containing args: - CC creates URL encrypted to the pubkey of server, containing args:
- shared secret for TOTP (same value as held in user's phone) - shared secret for TOTP (same value as held in user's phone)
- the response nonce (16 bytes, or 8 digits for Mk4) to be revealed to the user - the response nonce (32 bytes, shown as 64 hex chars, on Q; or 8 digits on Mk4)
on successful auth to be revealed to the user on successful auth
- flag if Q model, so can provide a QR to be scanned in that case (rather than digits) - flag if Q model, so can provide a QR to be scanned in that case (rather than digits)
- some text label for what's being approved, which is presented to user so they can pick - some text label for what's being approved, which is presented to user so they can pick
correct 2fa shared secret. correct 2fa shared secret.
@ -62,7 +65,7 @@ but we may find other uses for it.
- multiplies that private key by server's known public key - multiplies that private key by server's known public key
- apply sha256(resulting coordinate) => the session key - apply sha256(resulting coordinate) => the session key
- apply AES-256-CTR over URL contents (ascii text) - apply AES-256-CTR over URL contents (ascii text)
- prepend 33 bytes of pubkey, and base64url encode all of it - prepend 33 bytes of pubkey, and then base64url encode all of it
- full url is: `https://coldcard.com/2fa?{base64 encoded binary}` - full url is: `https://coldcard.com/2fa?{base64 encoded binary}`
## Trust Issues ## Trust Issues
@ -79,12 +82,15 @@ but we may find other uses for it.
## URL Format ## URL Format
https://coldcard.com/2fa?ss={shared_secret}&q={is_q}&g={nonce}&nm={label_text} https://coldcard.com/2fa?g={nonce}&ss={shared_secret}&nm={label_text}&q={is_q}
(the query string is then encrypted to the server's pubkey, so the args above
are what is inside the encrypted payload.)
- `nonce`: text string that is either 8 digits on Mk4, or 64 hex chars on Q
- `shared_secret`: 16 chars of Base32-encoded pre-shared secret - `shared_secret`: 16 chars of Base32-encoded pre-shared secret
- `is_q`: flag indicating use of QR to provide nonce back to user
- `nonce`: text string that is either 8 digits for Mk4, or hex digits for QR
- `nm`: human readable label for the transaction/purpose - `nm`: human readable label for the transaction/purpose
- `is_q`: flag indicating use of QR to provide nonce back to user
Server will accept plaintext arguments as above, but normally everything Server will accept plaintext arguments as above, but normally everything
after the question mark is encrypted. after the question mark is encrypted.

@ -1 +1 @@
Subproject commit f87d30f220cb6334eb3c4ace93c1b62e04942022 Subproject commit 3d1dfa858beb58b8dac37d8c66d7aed2909812f2

2
external/libngu vendored

@ -1 +1 @@
Subproject commit 1cccb25ef7736efae4a1de83d5dbdc13a2db0e80 Subproject commit 537519a829259622ea6b0334fbafd6cae852852f

View File

@ -17,9 +17,17 @@ class Graphics:
mk4_nfc_4 = (102, 49, 13, 0, b'\x00\x7f\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x0e\x03\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\xe0\x0e\x07\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xe0\x0e\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e0\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e0\x000D\x04\x10\x00\x00\x7f\xf0\x00\xe0\x0e0\x000D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00(D\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00(D\x04\x00\x00\x00xp\x00\xff\xff\xb0\x00$G\xe4\x00\x00\x00xp\x00\xff\xff\xb0\x00$G\xe4\x00\x00\x00xp\x00\xe0\x0e0\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e0\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e0\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e?\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xff\xff\x9f\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\xff\xff\x8f\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x7f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00') mk4_nfc_4 = (102, 49, 13, 0, b'\x00\x7f\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x0e\x03\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\xe0\x0e\x07\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xe0\x0e\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e0\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e0\x000D\x04\x10\x00\x00\x7f\xf0\x00\xe0\x0e0\x000D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00(D\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00(D\x04\x00\x00\x00xp\x00\xff\xff\xb0\x00$G\xe4\x00\x00\x00xp\x00\xff\xff\xb0\x00$G\xe4\x00\x00\x00xp\x00\xe0\x0e0\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e0\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e0\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e?\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xff\xff\x9f\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\xff\xff\x8f\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x7f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00')
mk5_nfc_1 = (126, 49, 16, 0, b'\x00\x7f\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0\xf0\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x0e\x00\xe0\x0e\x03\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\xe0\x0e\x00\xe0\x0e\x07\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xe0\x0e\x00\xe0\x0e\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\xe0\x0e\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x000D\x04\x10\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x000D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00(D\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00(D\x04\x00\x00\x00xp\x00\xff\xff\xff\xff\xfe0\x00$G\xe4\x00\x00\x00xp\x00\xff\xff\xff\xff\xfe0\x00$G\xe4\x00\x00\x00xp\x00\xe0\x0e\x00\xe0\x0e0\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e\x00\xe0\x0e0\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e\x00\xe0\x0e0\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e?\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xff\xff\xff\xff\xfe\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\xff\xff\xff\xff\xfe\x0f\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x7f\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
mk5_nfc_2 = (118, 49, 15, 0, b'\x00\x7f\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0\xf0\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x0e\x00\xe0\x03\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\xe0\x0e\x00\xe0\x07\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xe0\x0e\x00\xe0\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\xe0\x0e\x00\xe00\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x000D\x04\x10\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x000D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00(D\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00(D\x04\x00\x00\x00xp\x00\xff\xff\xff\xff0\x00$G\xe4\x00\x00\x00xp\x00\xff\xff\xff\xff0\x00$G\xe4\x00\x00\x00xp\x00\xe0\x0e\x00\xe00\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e\x00\xe00\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e\x00\xe00\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0?\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xff\xff\xff\xff\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\xff\xff\xff\xff\x0f\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x7f\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00')
mk5_nfc_3 = (110, 49, 14, 0, b'\x00\x7f\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0\xf0\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x0e\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\xe0\x0e\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xe0\x0e\x00\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\xe0\x0e\x000\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x000D\x04\x10\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x000D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00(D\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00(D\x04\x00\x00\x00xp\x00\xff\xff\xff0\x00$G\xe4\x00\x00\x00xp\x00\xff\xff\xff0\x00$G\xe4\x00\x00\x00xp\x00\xe0\x0e\x000\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e\x000\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e\x000\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00?\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xff\xff\xff\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\xff\xff\xff\x0f\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x7f\xff\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00')
mk5_nfc_4 = (102, 49, 13, 0, b'\x00\x7f\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\xf0\xf0\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x0e\x03\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\xe0\x0e\x07\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xe0\x0e\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e0\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e0\x000D\x04\x10\x00\x00\x7f\xf0\x00\xe0\x0e0\x000D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00(D\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00(D\x04\x00\x00\x00xp\x00\xff\xff\xb0\x00$G\xe4\x00\x00\x00xp\x00\xff\xff\xb0\x00$G\xe4\x00\x00\x00xp\x00\xe0\x0e0\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e0\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e0\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e?\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xff\xff\x9f\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\xff\xff\x8f\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x7f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00')
scroll = (3, 61, 1, 0, b'@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@@\xe0@') scroll = (3, 61, 1, 0, b'@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@@\xe0@')
selected = (15, 12, 2, 0, b'\x00\x00\x00\x00\x00\x06\x00\x0c\x00\x18\x0000`\x18\xc0\r\x80\x07\x00\x02\x00\x00\x00') selected = (9, 12, 2, 0, b'\x00\x00\x00\x00\x00\x80\x01\x80\x01\x00\x03\x00\x82\x00\xc6\x00d\x00<\x00\x18\x00\x00\x00')
sm_box = (11, 17, 2, 0, b'\xe4\xe0\x80 \x80 \x80 \x00\x00\x00\x00\x80 \x00\x00\x00\x00\x00\x00\x80 \x00\x00\x00\x00\x80 \x80 \x80 \xe4\xe0') sm_box = (11, 17, 2, 0, b'\xe4\xe0\x80 \x80 \x80 \x00\x00\x00\x00\x80 \x00\x00\x00\x00\x00\x00\x80 \x00\x00\x00\x00\x80 \x80 \x80 \xe4\xe0')

View File

@ -0,0 +1,49 @@
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxx xxxx xxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxx xxx xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxx xxx xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxx xxx xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxx xxx xxx xxx yy yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
xxx xxx xxx xxx yy n n ffffff ccccc yyyyyyyyyyy
xxx xxx xxx xxx yy n n ffffff ccccc yyyyyyyyyyy
xxx xxx xxx xxx yy nn n f c c yyyyyyyyyyy
xxx xxx xxx xxx yy nn n f c c yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n n f c yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n n f c yyyy yyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n n ffffff c yyyy yyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n n ffffff c yyyy yyy
xxx xxx xxx xxx yy n n n f c yyyy yyy
xxx xxx xxx xxx yy n n n f c yyyy yyy
xxx xxx xxx xxx yy n nn f c yyyyyyyyyyy
xxx xxx xxx xxx yy n nn f c yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n f c c yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n f c c yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n f ccccc yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n f ccccc yyyyyyyyyyy
xxx xxx xxx xxx yy yyyyyyyyyyy
xxx xxx xxx xxx yy yyyyyyyyyyy
xxx xxx xxx xxx yy yyyyyyyyyyy
xxx xxx xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

View File

@ -0,0 +1,49 @@
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxx xxxx xxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxx xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxx xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxx xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxx xxx xxx yy yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
xxx xxx xxx yy n n ffffff ccccc yyyyyyyyyyy
xxx xxx xxx yy n n ffffff ccccc yyyyyyyyyyy
xxx xxx xxx yy nn n f c c yyyyyyyyyyy
xxx xxx xxx yy nn n f c c yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n n f c yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n n f c yyyy yyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n n ffffff c yyyy yyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n n ffffff c yyyy yyy
xxx xxx xxx yy n n n f c yyyy yyy
xxx xxx xxx yy n n n f c yyyy yyy
xxx xxx xxx yy n nn f c yyyyyyyyyyy
xxx xxx xxx yy n nn f c yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n f c c yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n f c c yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n f ccccc yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n f ccccc yyyyyyyyyyy
xxx xxx xxx yy yyyyyyyyyyy
xxx xxx xxx yy yyyyyyyyyyy
xxx xxx xxx yy yyyyyyyyyyy
xxx xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

View File

@ -0,0 +1,49 @@
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxx xxxx xxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxx xxx yy yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
xxx xxx yy n n ffffff ccccc yyyyyyyyyyy
xxx xxx yy n n ffffff ccccc yyyyyyyyyyy
xxx xxx yy nn n f c c yyyyyyyyyyy
xxx xxx yy nn n f c c yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxx yy n n n f c yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxx yy n n n f c yyyy yyy
xxxxxxxxxxxxxxxxxxxxxxxx yy n n n ffffff c yyyy yyy
xxxxxxxxxxxxxxxxxxxxxxxx yy n n n ffffff c yyyy yyy
xxx xxx yy n n n f c yyyy yyy
xxx xxx yy n n n f c yyyy yyy
xxx xxx yy n nn f c yyyyyyyyyyy
xxx xxx yy n nn f c yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxx yy n n f c c yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxx yy n n f c c yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxx yy n n f ccccc yyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxx yy n n f ccccc yyyyyyyyyyy
xxx xxx yy yyyyyyyyyyy
xxx xxx yy yyyyyyyyyyy
xxx xxx yy yyyyyyyyyyy
xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

View File

@ -0,0 +1,49 @@
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxx xxxx xxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxx xxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxx
xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxx xxx yy yyyyyyyyyyy
xxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
xxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
xxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
xxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
xxx xxx yy n n ffffff ccccc yyyyyyyyyyy
xxx xxx yy n n ffffff ccccc yyyyyyyyyyy
xxx xxx yy nn n f c c yyyyyyyyyyy
xxx xxx yy nn n f c c yyyyyyyyyyy
xxxxxxxxxxxxxxxxx yy n n n f c yyyyyyyyyyy
xxxxxxxxxxxxxxxxx yy n n n f c yyyy yyy
xxxxxxxxxxxxxxxxx yy n n n ffffff c yyyy yyy
xxxxxxxxxxxxxxxxx yy n n n ffffff c yyyy yyy
xxx xxx yy n n n f c yyyy yyy
xxx xxx yy n n n f c yyyy yyy
xxx xxx yy n nn f c yyyyyyyyyyy
xxx xxx yy n nn f c yyyyyyyyyyy
xxxxxxxxxxxxxxxxx yy n n f c c yyyyyyyyyyy
xxxxxxxxxxxxxxxxx yy n n f c c yyyyyyyyyyy
xxxxxxxxxxxxxxxxx yy n n f ccccc yyyyyyyyyyy
xxxxxxxxxxxxxxxxx yy n n f ccccc yyyyyyyyyyy
xxx xxx yy yyyyyyyyyyy
xxx xxx yy yyyyyyyyyyy
xxx xxx yy yyyyyyyyyyy
xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxxxxxxxxxxxxxxxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxxxxxxxxxxxxxxxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

View File

@ -1,12 +1,12 @@
xx X
xx XX
xx X
xx XX
xx xx X X
xx xx XX XX
xx xx XX X
xxx XXXX
x XX

View File

@ -1,4 +1,3 @@
# Coldcard Hardware Details # Coldcard Hardware Details
This directory contains enough information for you to be able to This directory contains enough information for you to be able to
@ -6,7 +5,6 @@ build your own Coldcard from off-the-shelf parts.
We are sharing this information for the benefit of security We are sharing this information for the benefit of security
researchers who wish to analyse the Coldcard more completely. researchers who wish to analyse the Coldcard more completely.
# Schematic # Schematic
![](schematic-q1d.png) ![](schematic-q1d.png)
@ -15,6 +13,12 @@ researchers who wish to analyse the Coldcard more completely.
This is the Q rev D schematic. This is the Q rev D schematic.
![](schematic-mark5f.png)
`schematic-mark5f.png`
This is the Mark4 rev F schematic.
![](schematic-mark4d.png) ![](schematic-mark4d.png)
`schematic-mark4d.png` `schematic-mark4d.png`
@ -30,27 +34,20 @@ This is the Mark3 rev B schematic.
# BOM - Bill of Materials # BOM - Bill of Materials
The parts used in the Coldcard are detailed in this spreadsheet file. The parts used in the Coldcard are detailed in these spreadsheets.
Most of them could be bought on Digikey, but some are direct from suppliers. Most of them could be bought on Digikey, but some are direct from suppliers.
- BOM for Q rev D: `bom-q1d.xlsx`
- BOM for Mk5 rev F: `bom-mark5f.xlsx`
- BOM for Mk4 rev D: `bom-mark4d.xlsx`
- BOM for Mk3 rev B: `bom-mark3b.xlsx`
Not included are these minor bits: Not included are these minor bits:
- the plastic case (custom) - the plastic case (custom)
- the secure bag (with barcode serial number) - the secure bag (with barcode serial number)
- pin-recovery card - pin-recovery card
`bom-q1d.xlsx`
- BOM for Q rev D.
`bom-mark4d.xlsx`
- BOM for Mk3 rev D.
`bom-mark3b.xlsx`
- BOM for Mk3 rev B.
# Important # Important
- No promises that these files are 100% current because we constantly make quality improvements. - No promises that these files are 100% current because we constantly make quality improvements.

BIN
hardware/bom-mark5f.xlsx Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

View File

@ -1,2 +1,2 @@
Pillow==10.3.0 Pillow==12.1.1

View File

@ -2,32 +2,41 @@
This lists the changes in the most recent firmware, for each hardware platform. This lists the changes in the most recent firmware, for each hardware platform.
# Shared Improvements - Both Mk4 and Q # Shared Improvements - Both Mk and Q
- Enhancement: Text word-wrap done more carefully so never cuts off any text, and yet - New Feature: Sign [BIP-322](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki) Proof of Reserve PSBT files.
doesn't waste space. - Requires a carefully crafted PSBT that does not represent a monetary transaction, but instead is demonstrating
- Bugfix: `Add current tmp` option, which could be shown in `Seed Vault` menu under control over the keys for a list of UTXO, and commits to a short text message.
specific circumstances, would corrupt master settings if selected. - Read more [here](https://github.com/Coldcard/firmware/blob/master/docs/proof-of-reserves-bip-322.md).
- Bugfix: PUSHDATA2 in bitcoin script caused yikes. - New Feature: WIF Store. Ability to import foreign WIF keys (Wallet Import Format) and use them for PSBT signing.
- Bugfix: Warning for unknown scripts was not shown at the top of the signing story. - New Feature: Export [BIP-380](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki) extended key expression.
Navigate to "Advanced/Tools -> Export Wallet -> Key Expression"
- New Feature: Transaction Input Explorer. Shows data about UTXO(s) being spent. Press (2) before approving
transaction to enter Transaction Explorer.
- New Feature: Support for v3 transactions in PSBT files.
- New Feature: Option to type a derived BIP-85 secret as an emulated USB keyboard.
- New Feature: Nuke Device: purges all sensitive data and makes your COLDCARD e-waste.
- Enhancement: CCC debug menu allows you to reset block height.
- Enhancement: Show the BIP-39 passphrase on-screen (must scroll down) once new key is in effect.
- Enhancement: New "Buried Settings" menu, inside Settings menu, for rarely-applied settings.
- Enhancement: Add "Blue Wallet" option to "Export Wallet"
- Enhancement: Detect duplicated inputs in PSBT file.
- Bugfix: Replace `/` with `-` in exported file names of multisig wallet export artifacts.
# Mk Specific Changes
# Mk4 Specific Changes ## 5.5.0 - 2065-03-05
## 5.4.3 - 2025-05-14 - This release supports both the newer Mk5 hardware and existing Mk4.
- Enhancement: Show QR of XOR-split seeds.
- Bugfix: With both NFC & Virtual Disk OFF, user cannot exit `Export Wallet` menu. Gets stuck
in export loop and needs reboot to escape.
- Bugfix: Part of extended keys in stories were not always visible.
# Q Specific Changes # Q Specific Changes
## 1.3.3Q - 2025-05-14 ## 1.4.0Q - 2065-03-05
- Bugfix: Empty notes in hobbled mode caused yikes upon menu entry.
- Bugfix: Do not allow to teleport PSBTs from SD card when CC has no secrets.
- Bugfix: Calculator login mode: added "rand()" command, removed support
for variables/assignments.
# Release History # Release History

View File

@ -1,5 +1,58 @@
*See ChangeLog.md for more recent changes, these are historic versions* *See ChangeLog.md for more recent changes, these are historic versions*
## 5.4.5 - 2025-11-03
- Enhancement: Address format guessing changed away from using PSBT XPUB's derivation paths.
Now based on witness/redeem script of first PSBT input instead.
- Enhancement: Show master XFP of backup secret and ask user for confirmation before loading backup.
- Enhancement: Show firmware version added to hobbled Advanced/Tools menu.
- Bugfix: Exiting text input of Custom Backup Password caused yikes.
- Bugfix: Temporary seeds in SSSP mode were not able to update block height.
## 5.4.4 - 2025-09-30
- Spending policies for "Single Signers" adds new spending policy options:
- limit your Coldcard so it refuses to sign transactions that are "too big"
- require 2FA authentication before signing any transaction (NFC+web)
- velocity limits can restrict how often new transactions can be signed
- see `docs/spending-policy.md` for more details
- "Enable HSM" and "User Management" have moved into `Advanced > Spending Policy`.
- Old "CCC" feature has been renamed and moved into that menu as well: "Co-Sign Multisig"
- Added `Bull Bitcoin` export to `Export Wallet` menu.
- Enhancement: Added warning for zero value outputs if not `OP_RETURN`.
- Enhancement: Show QR codes of output addresses in transaction output explorer. Explorer is
now offered for transactions of all sizes, not just complex ones.
- Enhancement: Added file rename, when listing contents of SD card.
- Enhancement: Added ability to restore Coldcard backup via USB (needs latest of ckcc version)
- Enhancement: Address ownership allows to specify particular multisig wallet in which to search,
if `wallet` query parameter is provided via trivial extension to
[BIP-21](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki).
Example: `tb1q4d67p7stxml3kdudrgkg5mgaxsrgzcqzjrrj4gg62nxtvnsnvqjsxjkej0?wallet=Haystack`
- Bugfix: If all change outputs have `nValue=0`, they were not shown in UX.
- Bugfix: Disallow negative input/output amounts in PSBT.
- Bugfix: Fix filesystem initialization after Wipe LFS or Destroy Seed.
- Bugfix: Fix MicroSD selftest code.
- Bugfix: NFC loop exporting secrets would not work after first value exported.
- Bugfix: Multisig address format handling.
- Bugfix: Ownership check failing to find addresses near max (~760), needed to be re-run to succeed
- (Mk4 only) Bugfix: Part of extended keys (xpubs) were not always visible.
- (Mk4 only) Change: Mk4 default menu wrap-around lowered from 16 to 10 items.
## 5.4.3 - 2025-05-14
- Enhancement: Text word-wrap done more carefully so never cuts off any text, and yet
doesn't waste space.
- Bugfix: `Add current tmp` option, which could be shown in `Seed Vault` menu under
specific circumstances, would corrupt master settings if selected.
- Bugfix: PUSHDATA2 in bitcoin script caused yikes.
- Bugfix: Warning for unknown scripts was not shown at the top of the signing story.
- Bugfix: With both NFC & Virtual Disk OFF, user cannot exit `Export Wallet` menu. Gets stuck
in export loop and needs reboot to escape.
- Bugfix: Part of extended keys in stories were not always visible.
## 5.4.2 - 2025-04-16 ## 5.4.2 - 2025-04-16
- Huge new feature: CCC - ColdCard Cosign - Huge new feature: CCC - ColdCard Cosign

View File

@ -1,5 +1,62 @@
*See ChangeLog.md for more recent changes, these are historic versions* *See ChangeLog.md for more recent changes, these are historic versions*
## 1.3.5Q - 2025-11-03
- Enhancement: Address format guessing changed away from using PSBT XPUB's derivation paths.
Now based on witness/redeem script of first PSBT input instead.
- Enhancement: Show master XFP of backup secret and ask user for confirmation before loading backup.
- Enhancement: Show firmware version added to hobbled Advanced/Tools menu.
- Bugfix: Exiting text input of Custom Backup Password caused yikes.
- Bugfix: Temporary seeds in SSSP mode were not able to update block height.
- Enhancement: Show backup filename at the top of the screen during backup password entry.
## 1.3.4Q - 2025-09-30
- Spending policies for "Single Signers" adds new spending policy options:
- limit your Coldcard so it refuses to sign transactions that are "too big"
- require 2FA authentication before signing any transaction (NFC+web)
- velocity limits can restrict how often new transactions can be signed
- see `docs/spending-policy.md` for more details
- "Enable HSM" and "User Management" have moved into `Advanced > Spending Policy`.
- Old "CCC" feature has been renamed and moved into that menu as well: "Co-Sign Multisig"
- Added `Bull Bitcoin` export to `Export Wallet` menu.
- Enhancement: Added warning for zero value outputs if not `OP_RETURN`.
- Enhancement: Show QR codes of output addresses in transaction output explorer. Explorer is
now offered for transactions of all sizes, not just complex ones.
- Enhancement: Added file rename, when listing contents of SD card.
- Enhancement: Added ability to restore Coldcard backup via USB (needs latest of ckcc version)
- Enhancement: Address ownership allows to specify particular multisig wallet in which to search,
if `wallet` query parameter is provided via trivial extension to
[BIP-21](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki).
Example: `tb1q4d67p7stxml3kdudrgkg5mgaxsrgzcqzjrrj4gg62nxtvnsnvqjsxjkej0?wallet=Haystack`
- Bugfix: If all change outputs have `nValue=0`, they were not shown in UX.
- Bugfix: Disallow negative input/output amounts in PSBT.
- Bugfix: Fix filesystem initialization after Wipe LFS or Destroy Seed.
- Bugfix: Fix MicroSD selftest code.
- Bugfix: NFC loop exporting secrets would not work after first value exported.
- Bugfix: Multisig address format handling.
- Bugfix: Ownership check failing to find addresses near max (~760), needed to be re-run to succeed
- (Q only) Enhancement: Enters "forever calculator" mode when Q would otherwise be electronic waste
(ie. after 13 PIN failures). Always enabled, regardless of "login calculator" setting.
- (Q only) Bugfix: Correct line positioning when 24 seed words displayed.
## 1.3.3Q - 2025-05-14
- Enhancement: Text word-wrap done more carefully so never cuts off any text, and yet
doesn't waste space.
- Bugfix: `Add current tmp` option, which could be shown in `Seed Vault` menu under
specific circumstances, would corrupt master settings if selected.
- Bugfix: PUSHDATA2 in bitcoin script caused yikes.
- Bugfix: Warning for unknown scripts was not shown at the top of the signing story.
- Bugfix: Do not allow to teleport PSBTs from SD card when CC has no secrets.
- Bugfix: Calculator login mode: added "rand()" command, removed support
for variables/assignments.
## 1.3.2Q - 2025-04-16 ## 1.3.2Q - 2025-04-16
- Feature: Key Teleport -- Easily and securely move seed phrases, secure notes/passwords, - Feature: Key Teleport -- Easily and securely move seed phrases, secure notes/passwords,

View File

@ -2,26 +2,73 @@
This lists the new changes that have not yet been published in a normal release. This lists the new changes that have not yet been published in a normal release.
# Shared Improvements - Both Mk4 and Q # Shared Improvements - Both Mk and Q
- New: Added `Bull Bitcoin` export to `Export Wallet` menu - Change: BIP-322 Proof of Reserves & message signing PSBT requires PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE
- Enhancement: Add warning for zero value outputs if not `OP_RETURN` (read more [BIP-322 Proof of Reserves documentation](../docs/proof-of-reserves-bip-322.md) )
- Enhancement: Show QR codes of output addresses in Txn output explorer. Output explorer is - Enhancement: WIF Store export watch-only descriptor
now offered for transactions of all sizes. - Enhancement: WIF Store address detection without the need for PSBT_IN_BIP32_DERIVATION (Electrum support)
- Bugfix: If all change outputs have `nValue=0` they're not shown in UX. - Enhancement: Improve USB length validation
- Bugfix: Disallow negative input/output amounts in PSBT. - Bugfix: Fixes legacy input amount spoofing by rejecting witness-utxo-only PSBT inputs when Coldcard is expected to sign a non-segwit input.
When both UTXO fields are present the full non_witness_utxo is now preferred for amount/script lookup. Thanks, @Damir
- Bugfix: Emit warning and do not calculate fee for legacy UTXOs with only witness utxo
- Bugfix: Disable Virtual Disk and NFC before activating HSM
- Bugfix: P2PK signing was broken. Now supports both compressed and uncompressed P2PK spend
- Bugfix: Custom address default menu position wrong
- Bugfix: Delta Mode Trick PIN was never restored from backup
- Bugfix: Proper error message for incorrect 7z headers
- Bugfix: Exiting nickname entry with nickname already saved deleted previous nickname
- Bugfix: "Send Password" menu item inside Notes & Passwords visibility reversed
- Bugfix: Yikes when using "Send Password" on entry with password None field
- Bugfix: Do not show "Saving..." UX after failed Notes & Passwords import
- Bugfix: Incorrect error message caused by error in Verify/Decrypt Backup
- Bugfix: NFC Verify Address raised incorrect error message
- Bugfix: Notes & Passwords bulk import JSON with BBQr encoded as text
- Bugfix: CCC key C challenge handled bad BIP-39 checksum by crashing the UX; now treated as a wrong attempt (counts toward 3-strike lockout)
- Bugfix: CCC magnitude reset from CANCEL on empty input
- Bugfix: OP_RETURN in CCC with whitelist enabled caused yikes
- Bugfix: TX Explorer crashed on foreign input with non-standard sighash
- Bugfix: Malformed JSON message-sign request crashed signing UX
- Bugfix: Reject UI-control bytes in JSON / QR text message-signing
- Bugfix: Non-standard OP_RETURN outputs shown as "null-data", hiding part of the script
- Bugfix: Over-limit CCC address-whitelist import was rejected but still modified the policy
- Bugfix: Deleting a file right after renaming it (List Files) blanked the old name, leaving the renamed file
- Bugfix: Reordered `multi(...)` multisig with same keys was misreported as name-only change. Now blocked as duplicate.
- Bugfix: Max WIF store capacity limit was ignored if saving via QR WIF visualization
- Bugfix: Force Seed XOR restore from Temporary Seed menu to remain temporary even when master seed is blank
- Bugfix: Q1 seed word entry cursor alignment for 12-word seeds and preserve visible words after failed QR scans
- Bugfix: Binary signed-transaction (.txn) failed in NFC/QR file share
- Bugfix: yikes in transaction explorer for goto index for tx with only one output
- Bugfix: Sending `signmessage` payload encoded as BBQr caused yikes
- Bugfix: CCC/SSSP NFC whitelist import caused Yikes
- Bugfix: Stricter address ownership validation rejects unrecognized payment addresses before wallet search
- Bugfix: Handle malformed NDEF records robustly. Thanks, @Damir
- Bugfix: Ignore `bkpw` if added to backup. Thanks [@dmonakhov](https://github.com/dmonakhov)
- Bugfix: Keep NFC export tag live for repeated probes
- Bugfix: Fix 1of1 multisig signing failure
# Mk4 Specific Changes # Mk Specific Changes
## 5.4.? - 2025-08-xx ## 5.5.x - 2065-04-xx
- Bugfix: Part of extended keys in stories were not always visible. - tbd
# Q Specific Changes # Q Specific Changes
## 1.3.?Q - 2025-08-xx ## 1.4.xQ - 2065-04-xx
- Bugfix: Correct line positioning when 24 seed words displayed.
- New Feature: Secure Notes & Passwords UX groups
- New Feature: Apply Secure Note text, or Secure Note password as BIP-39 passphrase
- Bugfix: Teleporting a multisig PSBT file (without signing it first) sent stale data instead of the selected file
- Bugfix: Fix export UX message after teleport PSBT import & sign
- Bugfix: BIP-21 QR `amount` rendered with wrong decimal scaling on the Payment Address screen (e.g. `amount=1.1` was shown as `1.00000001 BTC`)
- Bugfix: QR scan import (Scan Any QR Code, master/temp seed via QR) now surfaces a clean error story on any parser or seed-loading failure (e.g. wordlist-valid but bad-checksum SeedQR) instead of yikesing the menu task
- Bugfix: Yikes when showing "QR too big" for a transaction output alone on an output-explorer page
- Bugfix: Yikes receiving a malformed full-backup via Key Teleport
- Bugfix: Keyboard debounce could leave a key stuck as "pressed" after release when another key was held
- Bugfix: Scanner robustness
- Avoid holding the QR scanner reset line low; reset is now only pulsed and then left deasserted.
- Recover scanner setup failures by retrying configuration and reinitializing on the next scan when needed.
- Prevent delayed scanner sleep commands from racing with a newly started scan.
- Improve scanner shutdown/recovery after scan cancel or command timeout.

View File

@ -2,11 +2,29 @@
Hash: SHA256 Hash: SHA256
95eff9e044cdb6b3d00961ae72d450684d5441c6a3661ab550a3c3aa0882e754 README.md 95eff9e044cdb6b3d00961ae72d450684d5441c6a3661ab550a3c3aa0882e754 README.md
3ba92e73d5260656641828e962e8eae4590f59774150d14276818a5229daf734 Next-ChangeLog.md 412597a0e30684400cb61ee04650c13ef9fc3dc16fc2570bd5e33a1dc0085d7a Next-ChangeLog.md
0173cade759704320e7a43810dabd5f18cf2034b447c6c7996f447c8d3ad21de History-Q.md 72458ab9eb2872d263bf4d3f4ca0fbf0ff9c6186f08d27f13fd600cb511ed2a7 History-Q.md
e6192bd7c2b27df7c9d8e58ae9a41bda4ef0615991c3159fb05ff60dc3cfedd1 History-Mk4.md d4891b509915800650a881556cca37604caab7a268afc0b1ed31021cea125891 History-Mk4.md
c8ad43b4e3f9d77777026da6d1210c6fc5cfe435bcfcd241c0f67c9392ad7b82 History-Mk3.md c8ad43b4e3f9d77777026da6d1210c6fc5cfe435bcfcd241c0f67c9392ad7b82 History-Mk3.md
6e8b95855e05dc7889b1476acfb1854107b4e8df6f12cdf4a643a9776e60c798 ChangeLog.md 9ebab063b57ff07e5d8df20c266ac94736a6ad0e4c71ad1f1db46ec16b0c94be ChangeLog.md
b2fa9f4b9a9778b71cf4b09ad79732192fdb457214f868a3af5234094deea33f 2026-03-25T1408-v6.5.0X-mk-coldcard.dfu
f7bed9f1d2d49a35e7c53c8208e73ceaccbee2ab3e7fcd7c020fbd4923140313 2026-03-25T1407-v6.5.0QX-q1-coldcard.dfu
2b7b4d95cd5d606b0a32e692db7a27c1d860140c6e919e20ea6672ad6afc3088 2026-03-05T2052-v5.5.0-mk-coldcard.dfu
15f26aa0b8fe33e29e338b74acc52d5532922af56bcf486e38085de55c86b82a 2026-03-05T2052-v5.5.0-mk-coldcard-factory.dfu
b7ce3ba55ae4bb1e1ebe0090507bcada5a4439e57a19552c78da7ef103bd144c 2026-03-05T2051-v1.4.0Q-q1-coldcard.dfu
82c2ed5ee5cf75cc8af0f54839b9a1bc8e4a329174657d61861a6576fb6e31d9 2026-03-05T2051-v1.4.0Q-q1-coldcard-factory.dfu
372fa1f82e54f632574c56a695a1ed332464bf029bd733b2db2131a591d8f126 2025-11-25T1618-v6.4.1X-mk4-coldcard.dfu
1059560fb598e5e8fd6aed0164aa4cad166552bf8e47a0365e986429c9a15346 2025-11-25T1617-v6.4.1QX-q1-coldcard.dfu
f04617b52fc0db6e95cac0dddd9ddd90754219f38b63a26d08c848e208069edb 2025-11-20T1602-v6.4.0X-mk4-coldcard.dfu
371f13f3e1a5ef28d14933daf03820f0e51d26ffa96008dd5595da0dfac646cf 2025-11-20T1601-v6.4.0QX-q1-coldcard.dfu
7076ae29c509d3120db0fae434c132e6abd3fb79c1a2a2f1383ab3b2acaba27c 2025-11-03T1527-v5.4.5-mk4-coldcard.dfu
00a337888ff86bf875bcfdab7a734981bce29a49f94f3df9f932924765848ab0 2025-11-03T1527-v5.4.5-mk4-coldcard-factory.dfu
ff6371545943518eb4eb00ba73b6aa3a5ac4e63459621ecec8a300c28c281b3c 2025-11-03T1525-v1.3.5Q-q1-coldcard.dfu
0ce02c8e549cb67b682d621b4a628f3fba2c56350a9ab090b9f08532f49e7afa 2025-11-03T1525-v1.3.5Q-q1-coldcard-factory.dfu
8a8c94e5f64d0bfe4914a236fb8a779f956989fe8de998133b85b23920f46283 2025-09-30T1238-v5.4.4-mk4-coldcard.dfu
0d0aba89027d5127f74b2a2b777a7c592cba12903a3c4c3ce9b0e060c09dddb7 2025-09-30T1238-v5.4.4-mk4-coldcard-factory.dfu
bc9918968b67fefe634342c77513c9c354e7821e9ff002c7e5c8c356d7507892 2025-09-30T1237-v1.3.4Q-q1-coldcard.dfu
00cb1fc2ef360aacf48ba8c9dd2167b3f5c5f1241ba1b2b17d61ea1b7bff0a45 2025-09-30T1237-v1.3.4Q-q1-coldcard-factory.dfu
be166b3bb3ec2259991db998c20c3d44e88eeaa73c2b8114f31cb14cab5e66e6 2025-05-14T1344-v5.4.3-mk4-coldcard.dfu be166b3bb3ec2259991db998c20c3d44e88eeaa73c2b8114f31cb14cab5e66e6 2025-05-14T1344-v5.4.3-mk4-coldcard.dfu
876932d4ea7634d268145d5bf45577c7198c9d60e8a271b5079faba4d4c91acd 2025-05-14T1344-v5.4.3-mk4-coldcard-factory.dfu 876932d4ea7634d268145d5bf45577c7198c9d60e8a271b5079faba4d4c91acd 2025-05-14T1344-v5.4.3-mk4-coldcard-factory.dfu
aaed0b90be5de310c8ac9f2d0cb3a7eea58923a53d349eb4b9ac8a902e5cba4e 2025-05-14T1343-v1.3.3Q-q1-coldcard.dfu aaed0b90be5de310c8ac9f2d0cb3a7eea58923a53d349eb4b9ac8a902e5cba4e 2025-05-14T1343-v1.3.3Q-q1-coldcard.dfu
@ -109,12 +127,12 @@ f05bc8dfed047c0c0abe5ed60621d2d14899b10717221c4af0942d96a1754f33 2021-03-29T192
bea27f263b524a66b3ed0a58c16805e98be0d7c3db20c2f7aab3238f2c6a6995 2019-12-19T1623-v3.0.6-coldcard.dfu bea27f263b524a66b3ed0a58c16805e98be0d7c3db20c2f7aab3238f2c6a6995 2019-12-19T1623-v3.0.6-coldcard.dfu
-----BEGIN PGP SIGNATURE----- -----BEGIN PGP SIGNATURE-----
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmgknkYACgkQo6MbrVoq iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmnD9ncACgkQo6MbrVoq
WxBkuggAqTFP4YJdkzdNPbPDxtnCL4ZFJ+Rtnybp9JigTazbMvA/pjR+uODPFI3M WxD9RAf+JkP/XVUPMDyfz+79AxBWFNU9r6RuYzXdzX3Z/XCKomZCZDtV7Ak6XlZi
Pm8I6kNPY8lMOPptEiFpNHn8EL8i2jOdH4NcmSP9OYInCRWyknm8fbmboSkOueAp GTNfsUNHaPC8WP6smFzYg07NoY2U1fVdY7+qeOi7UXF0hBBDJw7Gsa49P2zmt+DB
SG3irwVXf/XWMMpBdXvALPPvttPzlVOLYowYnervDPiINiQDkd5jRP+Kd0AStVEt lfzivQG2n+mT4cM64Z0WF3BYBWmCuDJdctqUAnLJe2p8bh6S8n5hFeKqndRhffNK
/QNq3ocmYHj4AUhJ5YSkyyVnnmGrZzKpcJ1q0XxXFCMJnyBrkjkJ60SgDx+ucy7c 773amkUrDW3RkHkIuevH4MQlR4ozWBmHzcehFDlTYT8BVLR8gg6hBzBEylxyDJNO
vTVk+W8QyLfqFkbhv4OT7YBITNGHEwk8sZ6V3N98r2/8Hx5PI42QOKEARYtOTpip Ld4W5SzsT6We0RGX2uOpMERDjkizqT9t5J63drzpuPrUQA8XVQPaOc07vpFHRbbZ
oj0LNnPFnAIkOTwZVazuc+vtG/GgSA== BhA61XO8yazNLVvata611pSTikNnDQ==
=IRUs =8Ti0
-----END PGP SIGNATURE----- -----END PGP SIGNATURE-----

View File

@ -14,7 +14,7 @@ from ux import ux_show_story, the_ux, ux_confirm, ux_dramatic_pause, ux_aborted
from ux import ux_enter_bip32_index, ux_input_text, import_export_prompt, OK, X, ux_render_words from ux import ux_enter_bip32_index, ux_input_text, import_export_prompt, OK, X, ux_render_words
from export import export_contents, make_summary_file, make_descriptor_wallet_export from export import export_contents, make_summary_file, make_descriptor_wallet_export
from export import make_bitcoin_core_wallet, generate_wasabi_wallet, generate_generic_export from export import make_bitcoin_core_wallet, generate_wasabi_wallet, generate_generic_export
from export import generate_unchained_export, generate_electrum_wallet from export import generate_unchained_export, generate_electrum_wallet, make_key_expression_export
from files import CardSlot, CardMissingError, needs_microsd from files import CardSlot, CardMissingError, needs_microsd
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
from glob import settings from glob import settings
@ -319,7 +319,7 @@ Press (6) to prove you read to the end of this message.''', title='WARNING', esc
if ch == '6': break if ch == '6': break
# do the actual picking # do the actual picking
pin = await lll.get_new_pin(title) pin = await lll.get_new_pin()
del lll del lll
if pin is None: return if pin is None: return
@ -459,21 +459,27 @@ async def pick_nickname(*a):
# Value is not stored with normal settings, it's part of "prelogin" settings # Value is not stored with normal settings, it's part of "prelogin" settings
# which are encrypted with zero-key. # which are encrypted with zero-key.
s = SettingsObject.prelogin() s = SettingsObject.prelogin()
nick = s.get('nick', '') k = "nick"
nick = s.get(k, '')
if not nick: if not nick:
ch = await ux_show_story('''\ ch = await ux_show_story("You can give this Coldcard a nickname"
You can give this Coldcard a nickname and it will be shown before login.''') " and it will be shown before login.")
if ch != 'y': return if ch != 'y': return
nn = await ux_input_text(nick, confirm_exit=False, prompt="Enter Nickname") nn = await ux_input_text(nick, confirm_exit=False, prompt="Enter Nickname")
if nn is None or (nick == nn): return # user exit & same value - noop
from glob import dis from glob import dis
dis.fullscreen("Saving...") dis.fullscreen("Saving...")
dis.busy_bar(True) dis.busy_bar(True)
nn = nn.strip() if nn else None if not nn:
s.set('nick', nn) s.remove_key(k)
else:
s.set(k, nn.strip())
s.save() s.save()
dis.busy_bar(False) dis.busy_bar(False)
del s del s
@ -573,8 +579,11 @@ async def clear_seed(*a):
# This is super dangerous for the customer's money. # This is super dangerous for the customer's money.
import seed import seed
if await any_active_duress_ux(): # in hobble mode, they cannot reach duress wallets and/or maybe we don't
return await ux_aborted() # want to reveal them? So don't block them based on that.
if not pa.hobbled_mode:
if await any_active_duress_ux():
return await ux_aborted()
if not await ux_confirm('Wipe seed words and reset wallet. ' if not await ux_confirm('Wipe seed words and reset wallet. '
'All funds will be lost. ' 'All funds will be lost. '
@ -587,7 +596,7 @@ async def clear_seed(*a):
if not await ux_confirm('''Are you REALLY sure though???\n\n\ if not await ux_confirm('''Are you REALLY sure though???\n\n\
This action will certainly cause you to lose all funds associated with this wallet, \ This action will certainly cause you to lose all funds associated with this wallet, \
unless you have a backup of the seed words and know how to import them into a \ unless you have a backup of the seed words and know how to import them into a \
new wallet.''', confirm_key='4'): new wallet.''', 'AGAIN...', confirm_key='4'):
return await ux_aborted() return await ux_aborted()
# clear all trick PINs from SE2 # clear all trick PINs from SE2
@ -800,26 +809,37 @@ async def start_login_sequence():
# If that didn't work, or no skip defined, force # If that didn't work, or no skip defined, force
# them to login successfully. # them to login successfully.
sp_unlock = False
try: try:
from trick_pins import tp
# Get a PIN and try to use it to login # Get a PIN and try to use it to login
# - does warnings about attempt usage counts # - does warnings about attempt usage counts
await block_until_login() await block_until_login()
sp_unlock = tp.was_sp_unlock()
if sp_unlock:
# Trying to unlock spending policy: ask for main PIN next.
await ux_show_story("Spending Policy Unlock: Please provide Main PIN next.")
pa.reset()
await block_until_login()
# we don't really know if that was the Main PIN (could easily be the bypass
# PIN again) and if it's a duress wallet, that's cool...
# Do we need to do countdown delay? (real or otherwise) # Do we need to do countdown delay? (real or otherwise)
# Q/Mk4 approach: # - wiping has already occurred if that was selected by trick details
# - wiping has already occured if that was picked
# - delay is variable, stored in tc_arg # - delay is variable, stored in tc_arg
from trick_pins import tp
delay = tp.was_countdown_pin() delay = tp.was_countdown_pin()
# Maybe they do know the right PIN, but do a delay anyway, because they wanted that # Maybe they do know the right PIN, but always do a delay anyway, because they wanted that
if not delay: if not delay:
delay = settings.get('lgto', 0) delay = settings.get('lgto', 0)
if delay: if delay:
# kill some time, with countdown, and get "the" PIN again for real login # kill some time, with countdown, and get "the" PIN again for real login
pa.reset() pa.reset()
await ux_login_countdown(delay * (60 if not version.is_devmode else 1)) await ux_login_countdown(delay * (60 if not version.is_devmode else 1))
# keep it simple for Mk4+: just challenge again for any PIN # keep it simple for Mk4+: just challenge again for any PIN
@ -847,14 +867,32 @@ async def start_login_sequence():
# handle upgrades/downgrade issues # handle upgrades/downgrade issues
try: try:
await version_migration() await version_migration()
except: except: pass
pass
# Maybe insist on the "right" microSD being already installed? # Maybe insist on the "right" microSD being already installed?
try: try:
from pwsave import MicroSD2FA from pwsave import MicroSD2FA
MicroSD2FA.enforce_policy() MicroSD2FA.enforce_policy()
except: pass # robustness: keep going! except: pass
# apply the hobbling for the spending policy, if appropriate
try:
from ccc import sssp_spending_policy, sssp_word_challenge
if sp_unlock and sssp_spending_policy('words'):
# challenge them also for first and last seed word! (will reboot on fail)
await sssp_word_challenge()
dis.fullscreen("Startup...")
if sp_unlock:
# Disable spending policy going forward; user has to re-enable.
pa.hobbled_mode = False
sssp_spending_policy('en', set_value=False)
else:
# normal entry mode, but might have policy enabled, if so enable it now.
pa.hobbled_mode = sssp_spending_policy('en')
except: pass
# implement idle timeout now that we are logged-in # implement idle timeout now that we are logged-in
IMPT.start_task('idle', idle_logout()) IMPT.start_task('idle', idle_logout())
@ -900,12 +938,14 @@ async def start_login_sequence():
settings.master_set("seedvault", False) settings.master_set("seedvault", False)
except: pass except: pass
if version.has_nfc and settings.get('nfc', 0):
from glob import hsm_active
if version.has_nfc and settings.get('nfc', 0) and not hsm_active:
# Maybe allow NFC now # Maybe allow NFC now
import nfc import nfc
nfc.NFCHandler.startup() nfc.NFCHandler.startup()
if settings.get('vidsk', 0): if settings.get('vidsk', 0) and not hsm_active:
# Maybe start virtual disk # Maybe start virtual disk
import vdisk import vdisk
vdisk.VirtDisk() vdisk.VirtDisk()
@ -943,7 +983,7 @@ async def restore_main_secret(*a):
goto_top_menu() goto_top_menu()
def make_top_menu(): def make_top_menu():
from flow import VirginSystem, NormalSystem, EmptyWallet, FactoryMenu from flow import VirginSystem, NormalSystem, EmptyWallet, FactoryMenu, HobbledTopMenu
from glob import hsm_active, settings from glob import hsm_active, settings
from pincodes import pa from pincodes import pa
@ -959,7 +999,9 @@ def make_top_menu():
assert pa.is_successful(), "nonblank but wrong pin" assert pa.is_successful(), "nonblank but wrong pin"
if pa.has_secrets(): if pa.has_secrets():
_cls = NormalSystem[:] # let them do a few things, but not all the things, when "hobbled"
_cls = HobbledTopMenu[:] if pa.hobbled_mode else NormalSystem[:]
if pa.tmp_value or settings.get("hmx", False): if pa.tmp_value or settings.get("hmx", False):
active_xfp = settings.get("xfp", 0) active_xfp = settings.get("xfp", 0)
sl, sr = ("[", "]") if pa.tmp_value else ("<", ">") sl, sr = ("[", "]") if pa.tmp_value else ("<", ">")
@ -1061,8 +1103,10 @@ async def export_xpub(label, _2, item):
if ch == "2": if ch == "2":
slip132 = not slip132 slip132 = not slip132
continue continue
if ch == '1': if ch == '1':
acct = await ux_enter_bip32_index('Account Number:') or 0 acct = await ux_enter_bip32_index('Account Number:')
if acct is None: continue
pth_split = path.split("/") pth_split = path.split("/")
pth_split[-1] = ("%dh" % acct) pth_split[-1] = ("%dh" % acct)
path = "/".join(pth_split) path = "/".join(pth_split)
@ -1088,29 +1132,32 @@ async def export_xpub(label, _2, item):
await show_qr_code(xpub, False) await show_qr_code(xpub, False)
def electrum_export_story(background=False): def electrum_export_story(noun="Electrum", background=False):
# saves memory being in a function # saves memory being in a function
return ('''\ return ('''\
This saves a skeleton Electrum wallet file. \ This saves a skeleton %s wallet file. \
You can then open that file in Electrum without ever connecting this Coldcard to a computer.\n You can then open that file in the wallet without ever connecting this Coldcard to a computer.\n
''' ''' % noun
+ (background or 'Choose an address type for the wallet on the next screen.'+PICK_ACCOUNT) + (background or 'Choose an address type for the wallet on the next screen.'+PICK_ACCOUNT)
+ SENSITIVE_NOT_SECRET) + SENSITIVE_NOT_SECRET)
async def electrum_skeleton(*a): async def electrum_skeleton(a, b, item):
# save xpub, and some other public details into a file: NOT MULTISIG # save xpub, and some other public details into a file: NOT MULTISIG
title = item.arg
fname_pat = "new-%s.json" % title.lower()
ch = await ux_show_story(electrum_export_story(), escape='1') ch = await ux_show_story(electrum_export_story(title), escape='1')
account_num = 0 acct = 0
if ch == '1': if ch == '1':
account_num = await ux_enter_bip32_index('Account Number:') or 0 acct = await ux_enter_bip32_index('Account Number:')
elif ch != 'y':
if (ch not in '1y') or acct is None:
return return
rv = [ rv = [
MenuItem(chains.addr_fmt_label(af), f=electrum_skeleton_step2, MenuItem(chains.addr_fmt_label(af), f=electrum_skeleton_step2,
arg=(af, account_num)) arg=(af, acct, title, fname_pat))
for af in chains.SINGLESIG_AF for af in chains.SINGLESIG_AF
] ]
the_ux.push(MenuSystem(rv)) the_ux.push(MenuSystem(rv))
@ -1131,13 +1178,14 @@ async def ss_descriptor_skeleton(_0, _1, item):
int_ext, allowed_af, ll, f_pattern, direct_way = item.arg int_ext, allowed_af, ll, f_pattern, direct_way = item.arg
addition = " for " + ll addition = " for " + ll
account_num = 0 acct = 0
if not direct_way: if not direct_way:
ch = await ux_show_story(ss_descriptor_export_story(addition), escape='1') ch = await ux_show_story(ss_descriptor_export_story(addition), escape='1')
if ch == '1': if ch == '1':
account_num = await ux_enter_bip32_index('Account Number:', unlimited=True) or 0 acct = await ux_enter_bip32_index('Account Number:', unlimited=True)
elif ch != 'y':
if (ch not in '1y') or acct is None:
return return
if int_ext is None: if int_ext is None:
@ -1149,16 +1197,57 @@ async def ss_descriptor_skeleton(_0, _1, item):
int_ext = False if ch == "1" else True int_ext = False if ch == "1" else True
if len(allowed_af) == 1: if len(allowed_af) == 1:
await make_descriptor_wallet_export(allowed_af[0], account_num, int_ext=int_ext, await make_descriptor_wallet_export(allowed_af[0], acct, int_ext=int_ext,
fname_pattern=f_pattern, direct_way=direct_way) fname_pattern=f_pattern, direct_way=direct_way)
else: else:
rv = [ rv = [
MenuItem(chains.addr_fmt_label(af), f=descriptor_skeleton_step2, MenuItem(chains.addr_fmt_label(af), f=descriptor_skeleton_step2,
arg=(af, account_num, int_ext, f_pattern, direct_way)) arg=(af, acct, int_ext, f_pattern, direct_way))
for af in allowed_af for af in allowed_af
] ]
the_ux.push(MenuSystem(rv)) the_ux.push(MenuSystem(rv))
async def key_expression_skeleton_step2(_1, _2, item):
# pick a semi-random file name, render and save it.
orig_path, addr_fmt = item.arg
await make_key_expression_export(orig_path, addr_fmt)
async def key_expression_skeleton(_0, _1, item):
# Export key expression -> [xfp/d/e/r]xpub
acct = 0
ch = await ux_show_story("This saves a extended key expression."
+ PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape='1')
if ch == '1':
acct = await ux_enter_bip32_index('Account Number:', unlimited=True)
if (ch not in '1y') or acct is None:
return
# element on 2nd index is address format for signed exports
# if multisig key use p2pkh
todo = [
("Segwit P2WPKH", "m/84h/%dh/%dh", AF_P2WPKH),
("Classic P2PKH", "m/44h/%dh/%dh", AF_CLASSIC),
("P2SH-Segwit", "m/49h/%dh/%dh", AF_P2WPKH_P2SH),
("Multi P2WSH", "m/48h/%dh/%dh/2h", AF_CLASSIC),
("Multi P2SH-P2WSH", "m/48h/%dh/%dh/1h", AF_CLASSIC),
]
from address_explorer import KeypathMenu
async def doit(*a):
return KeypathMenu(ranged=False, done_fn=make_key_expression_export)
ct = chains.current_chain().b44_cointype
rv = [ MenuItem(label, f=key_expression_skeleton_step2, arg=(orig_der % (ct, acct), af))
for label, orig_der, af in todo ]
rv += [ MenuItem("Custom Path", menu=doit) ]
the_ux.push(MenuSystem(rv))
async def samourai_post_mix_descriptor_export(*a): async def samourai_post_mix_descriptor_export(*a):
name = "POST-MIX" name = "POST-MIX"
post_mix_acct_num = 2147483646 post_mix_acct_num = 2147483646
@ -1203,34 +1292,36 @@ You can then run the commands in Bitcoin Core's console window, \
without ever connecting this Coldcard to a computer.\ without ever connecting this Coldcard to a computer.\
''' + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape='1') ''' + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape='1')
account_num = 0 acct = 0
if ch == '1': if ch == '1':
account_num = await ux_enter_bip32_index('Account Number:') or 0 acct = await ux_enter_bip32_index('Account Number:')
elif ch != 'y':
if (ch not in '1y') or acct is None:
return return
# no choices to be made, just do it. # no choices to be made, just do it.
await make_bitcoin_core_wallet(account_num) await make_bitcoin_core_wallet(acct)
async def electrum_skeleton_step2(_1, _2, item): async def electrum_skeleton_step2(_1, _2, item):
# pick a semi-random file name, render and save it. # pick a semi-random file name, render and save it.
addr_fmt, account_num = item.arg addr_fmt, account_num, title, fname_pat = item.arg
await export_contents('Electrum wallet', await export_contents(title + " wallet",
lambda: generate_electrum_wallet(addr_fmt, account_num), lambda: generate_electrum_wallet(addr_fmt, account_num),
"new-electrum.json", is_json=True) fname_pat, is_json=True)
async def _generic_export(prompt, label, f_pattern): async def _generic_export(prompt, label, f_pattern):
# like the Multisig export, make a single JSON file with # like the Multisig export, make a single JSON file with
# basically all useful XPUB's in it. # basically all useful XPUB's in it.
ch = await ux_show_story(prompt + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape="1") ch = await ux_show_story(prompt + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape="1")
account_num = 0 acct = 0
if ch == '1': if ch == '1':
account_num = await ux_enter_bip32_index('Account Number:') or 0 acct = await ux_enter_bip32_index('Account Number:')
elif ch != 'y':
if (ch not in '1y') or acct is None:
return return
await export_contents(label, lambda: generate_generic_export(account_num), await export_contents(label, lambda: generate_generic_export(acct),
f_pattern, is_json=True) f_pattern, is_json=True)
async def generic_skeleton(*A): async def generic_skeleton(*A):
@ -1275,16 +1366,17 @@ async def unchained_capital_export(*a):
ch = await ux_show_story('''\ ch = await ux_show_story('''\
This saves multisig XPUB information required to setup on the Unchained platform. \ This saves multisig XPUB information required to setup on the Unchained platform. \
''' + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape="1") ''' + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape="1")
account_num = 0 acct = 0
if ch == '1': if ch == '1':
account_num = await ux_enter_bip32_index('Account Number:') or 0 acct = await ux_enter_bip32_index('Account Number:')
elif ch != 'y':
if (ch not in '1y') or acct is None:
return return
xfp = xfp2str(settings.get('xfp', 0)) xfp = xfp2str(settings.get('xfp', 0))
fname = 'unchained-%s.json' % xfp fname = 'unchained-%s.json' % xfp
await export_contents('Unchained', lambda: generate_unchained_export(account_num), await export_contents('Unchained', lambda: generate_unchained_export(acct),
fname, is_json=True) fname, is_json=True)
@ -1363,7 +1455,7 @@ async def import_xprv(_1, _2, item):
else: else:
# only get here if NFC was not chosen # only get here if NFC was not chosen
# pick a likely-looking file. # pick a likely-looking file.
fn = await file_picker(suffix='txt', min_size=50, max_size=2000, taster=contains_xprv, fn = await file_picker(suffix='.txt', min_size=50, max_size=2000, taster=contains_xprv,
none_msg="Must contain " + label + ".", **choice) none_msg="Must contain " + label + ".", **choice)
if not fn: return if not fn: return
@ -1459,12 +1551,26 @@ async def wipe_filesystem(*A):
Erase internal filesystem and rebuild it. Resets contents of internal flash area \ Erase internal filesystem and rebuild it. Resets contents of internal flash area \
used for settings, address search cache, and HSM config file. Does not affect funds, \ used for settings, address search cache, and HSM config file. Does not affect funds, \
or seed words but will reset settings used with other temporary seeds & BIP-39 passphrases. \ or seed words but will reset settings used with other temporary seeds & BIP-39 passphrases. \
Does not affect MicroSD card, if any.'''): Does not affect MicroSD card, if any.''', confirm_key="4"):
return return
from files import wipe_flash_filesystem from files import wipe_flash_filesystem
wipe_flash_filesystem() wipe_flash_filesystem()
async def nuke_device(*a):
if not await ux_confirm("Wipe Seed & Brick device? This will wipe the seed, purge"
" all related settings, and makes ewaste from this device."):
return
if not await ux_confirm("Brick device?\n\nBy design, there is no way to reset or recover"
" the secure element, and its contents become forever inaccessible.",
confirm_key="1"):
return
import callgate
callgate.fast_brick()
# NOT REACHED
async def wipe_vdisk(*A): async def wipe_vdisk(*A):
if not await ux_confirm('''\ if not await ux_confirm('''\
Erases and reformats shared RAM disk. This is a secure erase that blanks every byte.'''): Erases and reformats shared RAM disk. This is a secure erase that blanks every byte.'''):
@ -1519,7 +1625,7 @@ async def qr_share_file(_1, _2, item):
# it's a txn, and we wrote as hex # it's a txn, and we wrote as hex
data = data.decode() data = data.decode()
else: else:
assert data[2:8] == bytes(6) assert data[1:4] == bytes(3)
data = b2a_hex(data).decode() data = b2a_hex(data).decode()
elif data[0:5] == b'psbt\xff': elif data[0:5] == b'psbt\xff':
tc = "P" tc = "P"
@ -1649,30 +1755,40 @@ async def list_files(*A):
from pincodes import pa from pincodes import pa
digest = chk.digest() digest = chk.digest()
basename = fn.rsplit('/', 1)[-1] path, basename = fn.rsplit('/', 1)
msg_base = 'SHA256(%s)\n\n%s\n\nPress ' % (basename, B2A(digest)) msg_base = 'SHA256(%s)\n\n' + B2A(digest) + '\n\nPress (1) to rename file, '
escape = "6" escape = "61"
if pa.has_secrets(): if pa.has_secrets():
msg_sign = '(4) to sign file digest and export detached signature, ' msg_base += '(4) to sign file digest and export detached signature, '
escape += "4" escape += "4"
else: msg_base += '(6) to delete.'
msg_sign = ""
msg_delete = '(6) to delete.'
msg = msg_base + msg_sign + msg_delete
while True: while True:
ch = await ux_show_story(msg, escape=escape) ch = await ux_show_story(msg_base % basename, escape=escape)
if ch == "x": break if ch == "x": break
if ch in '46': if ch in '461':
with CardSlot() as card: with CardSlot() as card:
if ch == '6': if ch == '6':
card.securely_blank_file(fn) card.securely_blank_file(fn)
break break
elif ch == '1':
new_basename = await ux_input_text(basename, max_len=32, min_len=3)
if new_basename:
try:
# prohibit both slashes and space in filenames
for s in "\/ ":
assert s not in new_basename, "illegal char"
uos.rename(path + "/" + basename, path + "/" + new_basename)
basename = new_basename
fn = path + "/" + basename # keep full path in sync (delete/sign use it)
except Exception as e:
await ux_show_story("Failed to rename the file. " + str(e),
title="Failure")
else: else:
from msgsign import write_sig_file from msgsign import write_sig_file
sig_nice = write_sig_file([(digest, fn)]) sig_nice = write_sig_file([(digest, fn)])
await ux_show_story("Signature file %s written." % sig_nice) await ux_show_story("Signature file %s written." % sig_nice)
msg = msg_base + msg_delete
return return
async def file_picker(suffix=None, min_size=1, max_size=1000000, taster=None, async def file_picker(suffix=None, min_size=1, max_size=1000000, taster=None,
@ -1685,6 +1801,13 @@ async def file_picker(suffix=None, min_size=1, max_size=1000000, taster=None,
# - escape: allow these chars to skip picking process # - escape: allow these chars to skip picking process
# - slot_b: None=>pick slot w/ card in it, or A if both. # - slot_b: None=>pick slot w/ card in it, or A if both.
# - allow_batch: adds an "all of the above" choice: ("menu label", menu_handler) # - allow_batch: adds an "all of the above" choice: ("menu label", menu_handler)
# - suffix argument MUST contain the dot (.txt), if list of suffixes, all MUST
if suffix:
# actually make it a list of "suffixes"
if not isinstance(suffix, list):
suffix = [suffix]
assert all(s[0] == '.' for s in suffix)
if choices is None: if choices is None:
choices = [] choices = []
@ -1698,13 +1821,13 @@ async def file_picker(suffix=None, min_size=1, max_size=1000000, taster=None,
# ignore subdirs # ignore subdirs
continue continue
if suffix: if fn[0] == '.':
if not isinstance(suffix, list): # unix-style hidden files
suffix = [suffix] continue
if not any([fn.lower().endswith(s) for s in suffix]):
continue
if fn[0] == '.': continue if suffix and not any(fn.lower().endswith(s) for s in suffix):
# wrong suffix, skip
continue
full_fname = path + '/' + fn full_fname = path + '/' + fn
@ -1750,7 +1873,7 @@ async def file_picker(suffix=None, min_size=1, max_size=1000000, taster=None,
if none_msg: if none_msg:
msg += none_msg msg += none_msg
if suffix: if suffix:
msg += '\n\nThe filename must end in %r. ' % suffix msg += '\n\nThe filename must end in: ' + ' OR '.join(suffix)
msg += '\n\nMaybe insert (another) SD card and try again?' msg += '\n\nMaybe insert (another) SD card and try again?'
@ -1830,7 +1953,7 @@ async def _batch_sign(choices=None):
return return
assert isinstance(picked, dict) assert isinstance(picked, dict)
choices = await file_picker(suffix='psbt', min_size=50, ux=False, choices = await file_picker(suffix='.psbt', min_size=50, ux=False,
max_size=MAX_TXN_LEN, taster=is_psbt, **picked) max_size=MAX_TXN_LEN, taster=is_psbt, **picked)
if not choices: if not choices:
@ -1868,7 +1991,7 @@ async def ready2sign(*a):
opt = {} opt = {}
# just check if we have candidates, no UI # just check if we have candidates, no UI
choices = await file_picker(suffix='psbt', min_size=50, ux=False, choices = await file_picker(suffix='.psbt', min_size=50, ux=False,
max_size=MAX_TXN_LEN, taster=is_psbt) max_size=MAX_TXN_LEN, taster=is_psbt)
if pa.tmp_value: if pa.tmp_value:
@ -1895,7 +2018,7 @@ from your desktop wallet software or command line tools.'''
title=title) title=title)
if isinstance(picked, dict): if isinstance(picked, dict):
opt = picked # reset options to what was chosen by user opt = picked # reset options to what was chosen by user
choices = await file_picker(suffix='psbt', min_size=50, ux=False, choices = await file_picker(suffix='.psbt', min_size=50, ux=False,
max_size=MAX_TXN_LEN, taster=is_psbt, max_size=MAX_TXN_LEN, taster=is_psbt,
**opt) **opt)
if not choices: if not choices:
@ -1937,7 +2060,7 @@ async def sign_message_on_sd(*a):
# min 1 line max 3 lines # min 1 line max 3 lines
return 1 <= len(lines) <= 3 return 1 <= len(lines) <= 3
fn = await file_picker(suffix=['txt', "json"], min_size=2, max_size=500, taster=is_signable, fn = await file_picker(suffix=['.txt', ".json"], min_size=2, max_size=500, taster=is_signable,
none_msg=('Must be txt file with one msg line, optionally ' none_msg=('Must be txt file with one msg line, optionally '
'followed by a subkey derivation path on a second line ' 'followed by a subkey derivation path on a second line '
'and/or address format on third line. JSON msg signing ' 'and/or address format on third line. JSON msg signing '
@ -2013,7 +2136,7 @@ Write it down.'''
while 1: while 1:
lll.reset() lll.reset()
lll.subtitle = "New " + title lll.subtitle = "New " + title
pin = await lll.get_new_pin(title) pin = await lll.get_new_pin()
if pin is None: if pin is None:
return await ux_aborted() return await ux_aborted()
@ -2095,7 +2218,6 @@ Coldcard Firmware
{rel} {rel}
{built} {built}
Bootloader: Bootloader:
{bl} {bl}
{chk} {chk}
@ -2191,7 +2313,7 @@ async def wipe_address_cache(*a):
async def wipe_ovc(*a): async def wipe_ovc(*a):
ok = await ux_confirm('''Clear history of segwit UTXO input values we have seen already. \ ok = await ux_confirm('''Clear history of segwit UTXO input values we have seen already. \
This data protects you against specific attacks. Use this only if certain a false-positive \ This data protects you against specific attacks. Use this only if certain a false-positive \
has occured in the detection logic.''') has occurred in the detection logic.''')
if not ok: return if not ok: return
import history import history
@ -2260,6 +2382,8 @@ async def change_seed_vault(is_enabled):
async def change_which_chain(*a): async def change_which_chain(*a):
# setting already changed, but reflect that value in other settings # setting already changed, but reflect that value in other settings
from glob import dis
dis.fullscreen("Wait...")
try: try:
# update xpub stored in settings # update xpub stored in settings
import stash import stash
@ -2291,9 +2415,23 @@ async def microsd_2fa(*a):
async def keyboard_test(*a): async def keyboard_test(*a):
# to aid keyboard testing/dev # to aid keyboard testing/dev
from ux import ux_input_text if version.has_qwerty:
await ux_input_text('', max_len=128, scan_ok=True, confirm_exit=False, await ux_input_text('', max_len=128, scan_ok=True, confirm_exit=False,
prompt='Keyboard Test', placeholder='(type whatever)') prompt='Keyboard Test', placeholder='(type whatever)')
else:
from ux_mk4 import ux_input_digits
await ux_input_digits('')
async def quick_nfc_test(*a):
from selftest import test_nfc
await test_nfc()
async def clear_tested_flag(*a):
# so can re-create first time power up in
# factory case (direct to selftest)
settings.remove_key('tested')
settings.save()
await reset_self()
# #
# Q wrappers; these will be present, but are very short on mk4 # Q wrappers; these will be present, but are very short on mk4
@ -2309,7 +2447,11 @@ async def scan_any_qr(menu, label, item):
async def _scan_any_qr(expect_secret=False, tmp=False): async def _scan_any_qr(expect_secret=False, tmp=False):
from ux_q1 import QRScannerInteraction from ux_q1 import QRScannerInteraction
x = QRScannerInteraction() x = QRScannerInteraction()
await x.scan_anything(expect_secret=expect_secret, tmp=tmp) try:
await x.scan_anything(expect_secret=expect_secret, tmp=tmp)
except Exception as e:
await ux_show_story(msg="Failed to import from QR.\n\n%s\n%s" % (e, problem_file_line(e)),
title="ERROR")
PUSHTX_SUPPLIERS = [ PUSHTX_SUPPLIERS = [
@ -2346,7 +2488,7 @@ async def pushtx_setup_menu(*a):
"transaction will be immediately broadcast on the public network.\n\n" "transaction will be immediately broadcast on the public network.\n\n"
"You must choose a provider by URL here, or give your own URL. " "You must choose a provider by URL here, or give your own URL. "
"\n\nYour phone's IP address vs. transaction details could be linked by the service. " "\n\nYour phone's IP address vs. transaction details could be linked by the service. "
"Requires NFC.", "Make sure your phone is not in airplane mode. Requires NFC.",
title="PUSH TX", title="PUSH TX",
) )
if ch != "y": if ch != "y":

View File

@ -30,8 +30,10 @@ def censor_address(addr):
return addr[0:12] + '___' + addr[12+3:] return addr[0:12] + '___' + addr[12+3:]
class KeypathMenu(MenuSystem): class KeypathMenu(MenuSystem):
def __init__(self, path=None, nl=0): def __init__(self, path=None, nl=0, ranged=True, done_fn=None):
self.prefix = None self.prefix = None
self.done_fn = done_fn
self.ranged = ranged
if path is None: if path is None:
# Top level menu; useful shortcuts, and special case just "m" # Top level menu; useful shortcuts, and special case just "m"
@ -40,10 +42,13 @@ class KeypathMenu(MenuSystem):
MenuItem("m/44h/⋯", f=self.deeper), MenuItem("m/44h/⋯", f=self.deeper),
MenuItem("m/49h/⋯", f=self.deeper), MenuItem("m/49h/⋯", f=self.deeper),
MenuItem("m/84h/⋯", f=self.deeper), MenuItem("m/84h/⋯", f=self.deeper),
MenuItem("m/0/{idx}", menu=self.done),
MenuItem("m/{idx}", menu=self.done),
MenuItem("m", f=self.done), MenuItem("m", f=self.done),
] ]
if self.ranged:
items += [
MenuItem("m/0/{idx}", menu=self.done),
MenuItem("m/{idx}", menu=self.done),
]
else: else:
# drill down one layer: (nl) is the current leaf # drill down one layer: (nl) is the current leaf
# - hardened choice first # - hardened choice first
@ -53,11 +58,14 @@ class KeypathMenu(MenuSystem):
MenuItem(p+"/⋯", menu=self.deeper), MenuItem(p+"/⋯", menu=self.deeper),
MenuItem(p+"h", menu=self.done), MenuItem(p+"h", menu=self.done),
MenuItem(p, menu=self.done), MenuItem(p, menu=self.done),
MenuItem(p+"h/0/{idx}", menu=self.done),
MenuItem(p+"/0/{idx}", menu=self.done), #useful shortcut?
MenuItem(p+"h/{idx}", menu=self.done),
MenuItem(p+"/{idx}", menu=self.done),
] ]
if self.ranged:
items += [
MenuItem(p + "h/0/{idx}", menu=self.done),
MenuItem(p + "/0/{idx}", menu=self.done), # useful shortcut?
MenuItem(p + "h/{idx}", menu=self.done),
MenuItem(p + "/{idx}", menu=self.done),
]
# simple consistent truncation when needed # simple consistent truncation when needed
max_wide = max(len(mi.label) for mi in items) max_wide = max(len(mi.label) for mi in items)
@ -95,17 +103,20 @@ class KeypathMenu(MenuSystem):
if isinstance(top, KeypathMenu): if isinstance(top, KeypathMenu):
the_ux.pop() the_ux.pop()
continue continue
assert isinstance(top, AddressListMenu) # assert isinstance(top, AddressListMenu), type(top)
break break
if self.done_fn:
return await self.done_fn(final_path)
return PickAddrFmtMenu(final_path, top) return PickAddrFmtMenu(final_path, top)
async def deeper(self, _1, _2, item): async def deeper(self, _1, _2, item):
val = item.arg or item.label val = item.arg or item.label
assert val.endswith('/⋯') assert val.endswith('/⋯')
cpath = val[:-2] cpath = val[:-2]
nl = await ux_enter_bip32_index('%s/' % cpath, unlimited=True) nl = await ux_enter_bip32_index('%s/' % cpath, unlimited=True, can_cancel=False)
return KeypathMenu(cpath, nl) return KeypathMenu(cpath, nl, ranged=self.ranged, done_fn=self.done_fn)
class PickAddrFmtMenu(MenuSystem): class PickAddrFmtMenu(MenuSystem):
def __init__(self, path, parent): def __init__(self, path, parent):
@ -115,9 +126,10 @@ class PickAddrFmtMenu(MenuSystem):
for af in chains.SINGLESIG_AF for af in chains.SINGLESIG_AF
] ]
super().__init__(items) super().__init__(items)
if path.startswith("m/84h"): # below is sensitive to order in chains.SINGLESIG_AF
if path.startswith("m/44h"):
self.goto_idx(1) self.goto_idx(1)
if path.startswith("m/49h"): elif path.startswith("m/49h"):
self.goto_idx(2) self.goto_idx(2)
async def done(self, _1, _2, item): async def done(self, _1, _2, item):
@ -230,11 +242,15 @@ class AddressListMenu(MenuSystem):
self.goto_idx(axi) self.goto_idx(axi)
async def change_account(self, *a): async def change_account(self, *a):
self.account_num = await ux_enter_bip32_index('Account Number:') or 0 acct = await ux_enter_bip32_index('Account Number:')
if acct is None: return
self.account_num = acct
await self.render() await self.render()
async def change_start_idx(self, *a): async def change_start_idx(self, *a):
self.start = await ux_enter_bip32_index("Start index:", unlimited=True) idx = await ux_enter_bip32_index("Start index:", unlimited=True)
if idx is None: return
self.start = idx
await self.render() await self.render()
async def pick_single(self, _1, _2, item): async def pick_single(self, _1, _2, item):
@ -429,14 +445,12 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
+ ['Derivation (%d of %d)' % (i+1, ms_wallet.N) for i in range(ms_wallet.N)] + ['Derivation (%d of %d)' % (i+1, ms_wallet.N) for i in range(ms_wallet.N)]
) + '"\n' ) + '"\n'
if (start == 0) and (n > 100) and change in (0, 1): # saver will be None if we don't think it worth saving these addresses
saver = OWNERSHIP.saver(ms_wallet, change, start) saver = OWNERSHIP.saver(ms_wallet, change, start, n)
else:
saver = None
for (idx, addr, derivs, script) in ms_wallet.yield_addresses(start, n, change_idx=change): for (idx, addr, derivs, script) in ms_wallet.yield_addresses(start, n, change_idx=change):
if saver: if saver:
saver(addr) saver(addr, idx)
# policy choice: never provide a complete multisig address to user. # policy choice: never provide a complete multisig address to user.
addr = censor_address(addr) addr = censor_address(addr)
@ -448,7 +462,7 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
yield ln yield ln
if saver: if saver:
saver(None) # close file saver(None, 0) # close cache file
return return
@ -456,20 +470,18 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
from wallet import MasterSingleSigWallet from wallet import MasterSingleSigWallet
main = MasterSingleSigWallet(addr_fmt, path, account_num) main = MasterSingleSigWallet(addr_fmt, path, account_num)
if n and (start == 0) and (n > 100) and change in (0, 1): # saver will be None if we don't think it worth saving these addresses
saver = OWNERSHIP.saver(main, change, start) saver = OWNERSHIP.saver(main, change, start, n)
else:
saver = None
yield '"Index","Payment Address","Derivation"\n' yield '"Index","Payment Address","Derivation"\n'
for (idx, addr, deriv) in main.yield_addresses(start, n, change_idx=change): for (idx, addr, deriv) in main.yield_addresses(start, n, change_idx=change):
if saver: if saver:
saver(addr) saver(addr, idx)
yield '%d,"%s","%s"\n' % (idx, addr, deriv) yield '%d,"%s","%s"\n' % (idx, addr, deriv)
if saver: if saver:
saver(None) # close saver(None, 0) # close cache file
async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num, async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num,
start=0, count=250, change=0, **save_opts): start=0, count=250, change=0, **save_opts):
@ -516,7 +528,7 @@ async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num,
except CardMissingError: except CardMissingError:
await needs_microsd() await needs_microsd()
except Exception as e: except Exception as e:
await ux_show_story('Failed to write!\n\n\n%s\n%s' % (e, problem_file_line(e))) await ux_show_story('Failed to write!\n\n%s\n%s' % (e, problem_file_line(e)))
async def address_explore(*a): async def address_explore(*a):

View File

@ -9,15 +9,18 @@ from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex from ubinascii import unhexlify as a2b_hex
from uhashlib import sha256 from uhashlib import sha256
from public_constants import AFC_SCRIPT, AF_CLASSIC, AFC_BECH32, SUPPORTED_ADDR_FORMATS from public_constants import AFC_SCRIPT, AF_CLASSIC, AFC_BECH32, SUPPORTED_ADDR_FORMATS
from public_constants import STXN_FINALIZE, STXN_VISUALIZE, STXN_SIGNED from public_constants import STXN_FINALIZE, STXN_VISUALIZE, STXN_SIGNED, AF_P2SH, AF_P2WPKH_P2SH
from sffile import SFFile from sffile import SFFile
from ux import ux_show_story, abort_and_goto, ux_dramatic_pause, ux_clear_keys from menu import MenuSystem, MenuItem
from ux import show_qr_code, OK, X, abort_and_push, AbortInteraction from serializations import ser_uint256, SIGHASH_ALL
from ux import ux_show_story, abort_and_goto, ux_dramatic_pause, ux_clear_keys, ux_confirm, the_ux
from ux import show_qr_code, OK, X, abort_and_push, AbortInteraction, ux_input_text, ux_enter_number
from usb import CCBusyError from usb import CCBusyError
from utils import HexWriter, xfp2str, problem_file_line, cleanup_deriv_path, B2A, show_single_address from utils import (HexWriter, xfp2str, problem_file_line, cleanup_deriv_path, B2A, node_from_privkey,
show_single_address, keypath_to_str, seconds2human_readable)
from psbt import psbtObject, FatalPSBTIssue, FraudulentChangeOutput from psbt import psbtObject, FatalPSBTIssue, FraudulentChangeOutput
from files import CardSlot, CardMissingError from files import CardSlot, CardMissingError
from exceptions import HSMDenied from exceptions import HSMDenied, QRTooBigError
from version import MAX_TXN_LEN from version import MAX_TXN_LEN
from charcodes import KEY_QR, KEY_NFC, KEY_ENTER, KEY_CANCEL, KEY_LEFT, KEY_RIGHT from charcodes import KEY_QR, KEY_NFC, KEY_ENTER, KEY_CANCEL, KEY_LEFT, KEY_RIGHT
from msgsign import sign_message_digest from msgsign import sign_message_digest
@ -128,7 +131,7 @@ Press %s to continue, otherwise %s to cancel.''' % (OK, X)
class ApproveMessageSign(UserAuthorizedAction): class ApproveMessageSign(UserAuthorizedAction):
def __init__(self, text, subpath, addr_fmt, approved_cb=None, def __init__(self, text, subpath, addr_fmt, approved_cb=None,
msg_sign_request=None, only_printable=True): msg_sign_request=None, allow_tab_nl=False, privkey=None):
super().__init__() super().__init__()
is_json = False is_json = False
@ -138,18 +141,23 @@ class ApproveMessageSign(UserAuthorizedAction):
text, subpath, addr_fmt, is_json = parse_msg_sign_request(msg_sign_request) text, subpath, addr_fmt, is_json = parse_msg_sign_request(msg_sign_request)
self.text = validate_text_for_signing( self.text = validate_text_for_signing(
text, only_printable=not is_json and only_printable text, allow_tab_nl=is_json and allow_tab_nl
) )
self.subpath = cleanup_deriv_path(subpath) self.subpath = cleanup_deriv_path(subpath)
self.addr_fmt = chains.parse_addr_fmt_str(addr_fmt) self.addr_fmt = chains.parse_addr_fmt_str(addr_fmt)
self.approved_cb = approved_cb self.approved_cb = approved_cb
self.privkey = privkey
from glob import dis from glob import dis
dis.fullscreen('Wait...') dis.fullscreen('Wait...')
with stash.SensitiveValues() as sv: if self.privkey:
node = sv.derive_path(self.subpath) node = node_from_privkey(self.privkey)
self.address = sv.chain.address(node, self.addr_fmt) self.address = chains.current_chain().address(node, self.addr_fmt)
else:
with stash.SensitiveValues() as sv:
node = sv.derive_path(self.subpath)
self.address = sv.chain.address(node, self.addr_fmt)
dis.progress_bar_show(1) dis.progress_bar_show(1)
@ -170,7 +178,8 @@ class ApproveMessageSign(UserAuthorizedAction):
else: else:
# perform signing (progress bar shown) # perform signing (progress bar shown)
digest = chains.current_chain().hash_message(self.text.encode()) digest = chains.current_chain().hash_message(self.text.encode())
self.result, _ = sign_message_digest(digest, self.subpath, "Signing...", self.addr_fmt) self.result, _ = sign_message_digest(digest, self.subpath, "Signing...",
self.addr_fmt, pk=self.privkey)
if self.approved_cb: if self.approved_cb:
# for micro sd case # for micro sd case
@ -194,7 +203,7 @@ def sign_msg(text, subpath, addr_fmt):
async def approve_msg_sign(text, subpath, addr_fmt, approved_cb=None, async def approve_msg_sign(text, subpath, addr_fmt, approved_cb=None,
msg_sign_request=None, kill_menu=False, msg_sign_request=None, kill_menu=False,
only_printable=True): allow_tab_nl=False, privkey=None):
# Ask user if they want to sign some short text message. # Ask user if they want to sign some short text message.
UserAuthorizedAction.cleanup() UserAuthorizedAction.cleanup()
@ -204,7 +213,8 @@ async def approve_msg_sign(text, subpath, addr_fmt, approved_cb=None,
text, subpath, addr_fmt, text, subpath, addr_fmt,
approved_cb=approved_cb, approved_cb=approved_cb,
msg_sign_request=msg_sign_request, msg_sign_request=msg_sign_request,
only_printable=only_printable, allow_tab_nl=allow_tab_nl,
privkey=privkey
) )
if kill_menu: if kill_menu:
@ -225,8 +235,6 @@ async def sign_txt_file(filename):
async def done(signature, address, text): async def done(signature, address, text):
# complete. write out result # complete. write out result
from glob import dis
orig_path, basename = filename.rsplit('/', 1) orig_path, basename = filename.rsplit('/', 1)
orig_path += '/' orig_path += '/'
base = basename.rsplit('.', 1)[0] base = basename.rsplit('.', 1)[0]
@ -263,8 +271,9 @@ async def try_push_tx(data, txid, txn_sha=None):
class ApproveTransaction(UserAuthorizedAction): class ApproveTransaction(UserAuthorizedAction):
def __init__(self, psbt_len, flags=None, psbt_sha=None, input_method=None, def __init__(self, psbt_len, flags=None, psbt_sha=None, input_method=None,
output_encoder=None, filename=None): output_encoder=None, filename=None, offset=TXN_INPUT_OFFSET):
super().__init__() super().__init__()
self.offset = offset
self.psbt_len = psbt_len self.psbt_len = psbt_len
# do finalize is None if not USB, None = decide based on is_complete # do finalize is None if not USB, None = decide based on is_complete
@ -287,52 +296,62 @@ class ApproveTransaction(UserAuthorizedAction):
# Pretty-print a transactions output. # Pretty-print a transactions output.
# - expects CTxOut object # - expects CTxOut object
# - gives user-visible string # - gives user-visible string
# returns: tuple(ux_output_rendition, address_or_script_str_for_qr_display)
# #
val = ' '.join(self.chain.render_value(o.nValue)) val = ' '.join(self.chain.render_value(o.nValue))
try: try:
dest = self.chain.render_address(o.scriptPubKey) dest = self.chain.render_address(o.scriptPubKey)
# known script types are short enough that we can display QR on both hw versions
return '%s\n - to address -\n%s\n' % (val, show_single_address(dest)), dest return '%s\n - to address -\n%s\n' % (val, show_single_address(dest)), dest
except ValueError: except ValueError:
pass pass
# Handle future things better: allow them to happen at least.
# sending to some unknown script, possibly very long
# but full-show required for verification
# OP_RETURN dest contains also OP_RETURN itself (for PSBT qr explorer)
dest = B2A(o.scriptPubKey)
# check for OP_RETURN # check for OP_RETURN
data = self.chain.op_return(o.scriptPubKey) data = self.chain.op_return(o.scriptPubKey)
if data is not None: # In UX story only data are shown as OP_RETURN is part of base msg
if data is None:
rv = '%s\n - to script -\n%s\n' % (val, dest)
else:
base = '%s\n - OP_RETURN -\n%s' base = '%s\n - OP_RETURN -\n%s'
if not data: if not data:
return base % (val, "null-data\n"), "" dest = ""
rv = base % (val, "null-data\n")
else: else:
data_ascii = None data_ascii = None
if len(data) > 200: if len(data) > 160:
# completely arbitrary limit, prevents huge stories # completely arbitrary limit, prevents huge stories
data_hex = b2a_hex(data[:100]).decode() + "\n\n" + b2a_hex(data[-100:]).decode() # anchor data are not relevant for verification - can be hidden
ss = b2a_hex(data[:80]).decode() + "\n\n" + b2a_hex(data[-80:]).decode()
# but we show empty QR in txn explorer for these big, modified data
else: else:
data_hex = b2a_hex(data).decode() ss = b2a_hex(data).decode()
if (min(data) >= 32) and (max(data) < 127): # printable & not huge if (min(data) >= 32) and (max(data) < 127): # printable & not huge
try: try:
data_ascii = data.decode("ascii") data_ascii = data.decode("ascii")
except: pass except: pass
to_ret = base % (val, data_hex) rv = base % (val, ss)
if data_ascii: if data_ascii:
to_ret += " (ascii: %s)" % data_ascii rv += " (ascii: %s)" % data_ascii
return to_ret + "\n", data_hex rv += "\n"
# Handle future things better: allow them to happen at least. return rv, dest
dest = B2A(o.scriptPubKey)
return '%s\n - to script -\n%s\n' % (val, dest), dest
async def interact(self): async def interact(self):
# Prompt user w/ details and get approval # Prompt user w/ details and get approval
from glob import dis, hsm_active from glob import dis, hsm_active
from ccc import CCCFeature from ccc import CCCFeature, SSSPFeature
# step 1: parse PSBT from PSRAM into in-memory objects. # step 1: parse PSBT from PSRAM into in-memory objects.
try: try:
with SFFile(TXN_INPUT_OFFSET, length=self.psbt_len, message='Reading...') as fd: with SFFile(self.offset, length=self.psbt_len, message='Reading...') as fd:
# NOTE: psbtObject captures the file descriptor and uses it later # NOTE: psbtObject captures the file descriptor and uses it later
self.psbt = psbtObject.read_psbt(fd) self.psbt = psbtObject.read_psbt(fd)
except BaseException as exc: except BaseException as exc:
@ -351,13 +370,14 @@ class ApproveTransaction(UserAuthorizedAction):
await self.psbt.validate() # might do UX: accept multisig import await self.psbt.validate() # might do UX: accept multisig import
dis.progress_sofar(10, 100) dis.progress_sofar(10, 100)
# consider_keys only needs num_our_keys to be set if not self.psbt.wif_store:
# it set during psbt.validate() self.psbt.consider_keys()
self.psbt.consider_keys()
dis.progress_sofar(20, 100) dis.progress_sofar(20, 100)
ccc_c_xfp = CCCFeature.get_xfp() # can be None ccc_c_xfp = CCCFeature.get_xfp() # can be None
self.psbt.consider_inputs(cosign_xfp=ccc_c_xfp) self.psbt.consider_inputs(cosign_xfp=ccc_c_xfp)
if self.psbt.wif_store:
self.psbt.consider_keys()
dis.progress_sofar(50, 100) dis.progress_sofar(50, 100)
self.psbt.consider_outputs() self.psbt.consider_outputs()
@ -387,7 +407,13 @@ class ApproveTransaction(UserAuthorizedAction):
# early test for spending policy; not an error if violates policy # early test for spending policy; not an error if violates policy
# - might add warnings # - might add warnings
could_ccc_sign, needs_2fa = CCCFeature.could_sign(self.psbt) could_ccc_sign, ccc_needs_2fa = CCCFeature.could_cosign(self.psbt)
# test for allowing any signature when in single-signer mode
# - but CCC will override it.
should_block, ss_needs_2fa = SSSPFeature.can_allow(self.psbt)
if should_block and not could_ccc_sign:
return await self.failure('Spending Policy violation.')
# step 2: figure out what we are approving, so we can get sign-off # step 2: figure out what we are approving, so we can get sign-off
# - outputs, amounts # - outputs, amounts
@ -402,6 +428,7 @@ class ApproveTransaction(UserAuthorizedAction):
# #
try: try:
msg = uio.StringIO() msg = uio.StringIO()
is_por = self.psbt.por322 and (self.psbt.num_inputs > 1)
# mention warning at top # mention warning at top
wl= len(self.psbt.warnings) wl= len(self.psbt.warnings)
@ -410,27 +437,41 @@ class ApproveTransaction(UserAuthorizedAction):
elif wl >= 2: elif wl >= 2:
msg.write('(%d warnings below)\n\n' % wl) msg.write('(%d warnings below)\n\n' % wl)
if self.psbt.consolidation_tx: if self.psbt.por322:
# consolidating txn that doesn't change balance of account. msg.write("%s\n\n" % ("Proof of Reserves" if is_por else "BIP-322 Message"))
msg.write("Consolidating %s %s\nwithin wallet.\n\n" % msg.write("Message:\n%s\n\n" % self.psbt.por322_msg)
self.chain.render_value(self.psbt.total_value_out)) if is_por:
msg.write("Amount %s %s\n\n" % self.chain.render_value(self.psbt.total_value_in))
try:
addr = self.chain.render_address(self.psbt.por322_msg_challenge)
msg.write("Challenge Address:\n%s\n\n" % show_single_address(addr))
except ValueError:
msg.write("Message Challenge:\n%s\n\n" % b2a_hex(self.psbt.por322_msg_challenge).decode())
else: else:
msg.write("Sending %s %s\n" % self.chain.render_value( if self.psbt.consolidation_tx:
self.psbt.total_value_out - self.psbt.total_change_value)) # consolidating txn that doesn't change balance of account.
msg.write("Consolidating %s %s\nwithin wallet.\n\n" %
self.chain.render_value(self.psbt.total_value_out))
else:
msg.write("Sending %s %s\n" % self.chain.render_value(
self.psbt.total_value_out - self.psbt.total_change_value))
fee = self.psbt.calculate_fee() fee = self.psbt.calculate_fee()
if fee is not None: if fee is not None:
msg.write("Network fee %s %s\n\n" % self.chain.render_value(fee)) msg.write("Network fee %s %s\n\n" % self.chain.render_value(fee))
msg.write(" %d %s\n %d %s\n\n" % ( if not self.psbt.por322 or is_por:
self.psbt.num_inputs, msg.write(" %d %s\n %d %s\n\n" % (
"input" if self.psbt.num_inputs == 1 else "inputs", self.psbt.num_inputs,
self.psbt.num_outputs, "input" if self.psbt.num_inputs == 1 else "inputs",
"output" if self.psbt.num_outputs == 1 else "outputs", self.psbt.num_outputs,
)) "output" if self.psbt.num_outputs == 1 else "outputs",
))
if not self.psbt.por322:
# outputs + change story created here
self.output_summary_text(msg)
# outputs + change story created here
self.output_summary_text(msg)
gc.collect() gc.collect()
if self.psbt.ux_notes: if self.psbt.ux_notes:
@ -458,17 +499,23 @@ class ApproveTransaction(UserAuthorizedAction):
if not hsm_active: if not hsm_active:
esc = "2" esc = "2"
msg.write("Press %s to approve and sign transaction." noun = "transaction"
" Press (2) to explore txn outputs." % OK) if self.psbt.por322:
noun = "proof of reserves" if is_por else "message"
msg.write("Press %s to approve and sign %s."
" Press (2) to explore transaction." % (OK, noun))
if (self.input_method == "sd") and CardSlot.both_inserted(): if (self.input_method == "sd") and CardSlot.both_inserted():
esc += "b" esc += "b"
msg.write(" (B) to write to lower SD slot.") msg.write(" (B) to write to lower SD slot.")
msg.write(" %s to abort." % X) msg.write(" %s to abort." % X)
title = "OK TO %s?" % ("SIGN" if self.psbt.por322 else "SEND")
while True: while True:
ch = await ux_show_story(msg, title="OK TO SEND?", escape=esc) ch = await ux_show_story(msg, title=title, escape=esc)
if ch == "2": if ch == "2":
await self.txn_explorer() await TXExplorer.start(self)
continue continue
else: else:
msg.close() msg.close()
@ -500,7 +547,7 @@ class ApproveTransaction(UserAuthorizedAction):
self.done() self.done()
return return
if needs_2fa and could_ccc_sign: if ccc_needs_2fa and could_ccc_sign:
# They still need to pass web2fa challenge (but it meets other specs ok) # They still need to pass web2fa challenge (but it meets other specs ok)
try: try:
await CCCFeature.web2fa_challenge() await CCCFeature.web2fa_challenge()
@ -510,6 +557,13 @@ class ApproveTransaction(UserAuthorizedAction):
if ch2 != 'y': if ch2 != 'y':
return await self.failure("2FA Failed") return await self.failure("2FA Failed")
elif ss_needs_2fa:
# Need 2FA for single-sig case .. refuse to sign if it fails.
try:
await SSSPFeature.web2fa_challenge()
except:
return await self.failure("2FA Failed")
# do the actual signing. # do the actual signing.
try: try:
dis.fullscreen('Wait...') dis.fullscreen('Wait...')
@ -517,10 +571,15 @@ class ApproveTransaction(UserAuthorizedAction):
self.psbt.sign_it() self.psbt.sign_it()
if could_ccc_sign: if could_ccc_sign:
dis.fullscreen('CCC Sign...') # this is where the CCC co-signing happens.
dis.fullscreen('Co-Signing...')
gc.collect() gc.collect()
CCCFeature.sign_psbt(self.psbt) CCCFeature.sign_psbt(self.psbt)
if SSSPFeature.is_enabled():
# update SSSP block_h even if SSSP blocks and overridden by CCC
SSSPFeature.update_last_signed(self.psbt)
except FraudulentChangeOutput as exc: except FraudulentChangeOutput as exc:
return await self.failure(exc.args[0], title='Change Fraud') return await self.failure(exc.args[0], title='Change Fraud')
except MemoryError: except MemoryError:
@ -530,8 +589,9 @@ class ApproveTransaction(UserAuthorizedAction):
return await self.failure("Signing failed late", exc) return await self.failure("Signing failed late", exc)
try: try:
await done_signing(self.psbt, self, self.input_method, self.filename, self.output_encoder, await done_signing(self.psbt, self, self.input_method,
slot_b=True if ch == "b" else False, finalize=self.do_finalize) self.filename, self.output_encoder,
slot_b=(ch == "b"), finalize=self.do_finalize)
self.done() self.done()
except AbortInteraction: except AbortInteraction:
# user might have sent new sign cmd, while we still at export prompt # user might have sent new sign cmd, while we still at export prompt
@ -540,73 +600,6 @@ class ApproveTransaction(UserAuthorizedAction):
# sys.print_exception(exc) # sys.print_exception(exc)
return await self.failure("PSBT output failed", exc) return await self.failure("PSBT output failed", exc)
async def txn_explorer(self):
# Page through unlimited-sized transaction details
# - shows all outputs (including change): their address and amounts.
from glob import dis
def make_msg(offset, count):
dis.fullscreen('Wait...')
rv = ""
end = min(offset + count, self.psbt.num_outputs)
addrs = []
change = []
for i, (idx, out) in enumerate(self.psbt.output_iter(offset, end)):
outp = self.psbt.outputs[idx]
item = "Output %d%s:\n\n" % (idx, " (change)" if outp.is_change else "")
msg, addr_or_script = self.render_output(out)
item += msg
addrs.append(addr_or_script)
if outp.is_change:
change.append(i)
item += "\n"
rv += item
dis.progress_sofar(idx-offset+1, count)
rv += 'Press RIGHT to see next group'
if offset:
rv += ', LEFT to go back'
if not version.has_qwerty:
# Q has hint key
rv += ", (4) to show QR code"
rv += ('. %s to quit.' % X)
return rv, addrs, change, end
start = 0
n = 10
msg, addrs, change, end = make_msg(start, n)
while True:
ch = await ux_show_story(msg, title="%d-%d" % (start, end-1),
escape='479'+KEY_RIGHT+KEY_LEFT+KEY_QR,
hint_icons=KEY_QR)
if ch == 'x':
del msg
return
elif ch in "4"+KEY_QR:
from ux import show_qr_codes
await show_qr_codes(addrs, False, start, is_addrs=True, change_idxs=change)
continue
elif (ch in KEY_LEFT+"7"):
if (start - n) < 0:
continue
else:
# go backwards in explorer
start -= n
elif (ch in KEY_RIGHT+"9"):
if (start + n) >= self.psbt.num_outputs:
continue
else:
# go forwards
start += n
else:
# nothing changed - do not recalc msg
continue
msg, addrs, change, end = make_msg(start, n)
async def save_visualization(self, msg, sign_text=False): async def save_visualization(self, msg, sign_text=False):
# write story text out, maybe signing it as we go # write story text out, maybe signing it as we go
# - return length and checksum # - return length and checksum
@ -658,7 +651,8 @@ class ApproveTransaction(UserAuthorizedAction):
has_change = True has_change = True
total_change += tx_out.nValue total_change += tx_out.nValue
if len(largest_change) < MAX_VISIBLE_CHANGE: if len(largest_change) < MAX_VISIBLE_CHANGE:
largest_change.append((tx_out.nValue, self.chain.render_address(tx_out.scriptPubKey))) _, addr = self.render_output(tx_out)
largest_change.append((tx_out.nValue, addr))
if len(largest_change) == MAX_VISIBLE_CHANGE: if len(largest_change) == MAX_VISIBLE_CHANGE:
largest_change = sorted(largest_change, key=lambda x: x[0], reverse=True) largest_change = sorted(largest_change, key=lambda x: x[0], reverse=True)
continue continue
@ -683,12 +677,9 @@ class ApproveTransaction(UserAuthorizedAction):
continue # too small continue # too small
largest.pop(-1) largest.pop(-1)
if outp.is_change:
ret = (here, self.chain.render_address(tx_out.scriptPubKey)) rendered, dest = self.render_output(tx_out)
else: largest.insert(keep, (here, dest if outp.is_change else rendered))
rendered, _ = self.render_output(tx_out)
ret = (here, rendered)
largest.insert(keep, ret)
# foreign outputs (soon to be other people's coins) # foreign outputs (soon to be other people's coins)
visible_out_sum = 0 visible_out_sum = 0
@ -727,11 +718,12 @@ class ApproveTransaction(UserAuthorizedAction):
msg.write('%s %s\n\n' % self.chain.render_value(total_change - visible_change_sum)) msg.write('%s %s\n\n' % self.chain.render_value(total_change - visible_change_sum))
def sign_transaction(psbt_len, flags=0x0, psbt_sha=None): def sign_transaction(psbt_len, flags=0x0, psbt_sha=None, input_method="usb", offset=TXN_INPUT_OFFSET):
# transaction (binary) loaded into PSRAM already, checksum checked # transaction (binary) loaded into PSRAM already, checksum checked
UserAuthorizedAction.check_busy(ApproveTransaction) UserAuthorizedAction.check_busy(ApproveTransaction)
UserAuthorizedAction.active_request = ApproveTransaction( UserAuthorizedAction.active_request = ApproveTransaction(
psbt_len, flags, psbt_sha=psbt_sha, input_method="usb", psbt_len, flags, psbt_sha=psbt_sha, input_method=input_method,
offset=offset
) )
# kill any menu stack, and put our thing at the top # kill any menu stack, and put our thing at the top
@ -778,13 +770,19 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
# USB case - user can choose whether to attempt finalization # USB case - user can choose whether to attempt finalization
is_complete = finalize is_complete = finalize
if psbt.por322:
# network txn strips PSBT BIP-32 with paths with pubkey required for verification
# overrides --finalize from USB
# disable pushTX for BIP-322
is_complete = False
with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as psram: with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as psram:
if is_complete: if is_complete:
txid = psbt.finalize(psram) txid = psbt.finalize(psram)
noun = "Finalized TX ready for broadcast" noun = "Finalized TX ready for broadcast"
else: else:
psbt.serialize(psram) psbt.serialize(psram)
noun = "Partly Signed PSBT" noun = "Signed BIP-322 PSBT" if psbt.por322 else "Partly Signed PSBT"
txid = None txid = None
data_len = psram.tell() data_len = psram.tell()
@ -802,6 +800,10 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
msg = noun + " shared via USB." msg = noun + " shared via USB."
title = "PSBT Signed" title = "PSBT Signed"
elif input_method == "kt":
first_time = False
title = "PSBT Signed"
if txid and await try_push_tx(data_len, txid, data_sha2): if txid and await try_push_tx(data_len, txid, data_sha2):
# go directly to reexport menu after pushTX # go directly to reexport menu after pushTX
first_time = False first_time = False
@ -820,8 +822,6 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
ch = KEY_QR ch = KEY_QR
elif input_method == "nfc": elif input_method == "nfc":
ch = KEY_NFC ch = KEY_NFC
elif input_method == "kt":
ch = 't'
else: else:
# SD/VDisk # SD/VDisk
ch = {"force_vdisk": input_method == "vdisk", "slot_b": slot_b} ch = {"force_vdisk": input_method == "vdisk", "slot_b": slot_b}
@ -829,9 +829,11 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
if not ch: if not ch:
# show all possible export options (based on hardware enabled, features) # show all possible export options (based on hardware enabled, features)
intro = [] intro = []
key6 = None
if msg: if msg:
intro.append(msg) intro.append(msg)
if txid: if txid:
key6 = "for QR Code of TXID"
intro.append('TXID:\n' + txid) intro.append('TXID:\n' + txid)
# "force_prompt" is needed after first iteration as we can be Mk4, with NFC,Vdisk off, # "force_prompt" is needed after first iteration as we can be Mk4, with NFC,Vdisk off,
@ -839,7 +841,7 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
# In that case this would just return dict and keep producing signed # In that case this would just return dict and keep producing signed
# files on SD infinitely (would never actually prompt). # files on SD infinitely (would never actually prompt).
ch = await import_export_prompt(noun, intro="\n\n".join(intro), offer_kt=offer_kt, ch = await import_export_prompt(noun, intro="\n\n".join(intro), offer_kt=offer_kt,
txid=txid, title=title, force_prompt=not first_time, key6=key6, title=title, force_prompt=not first_time,
no_qr=not version.has_qwerty) no_qr=not version.has_qwerty)
if ch == KEY_CANCEL: if ch == KEY_CANCEL:
UserAuthorizedAction.cleanup() UserAuthorizedAction.cleanup()
@ -851,14 +853,14 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
elif ch == KEY_QR: elif ch == KEY_QR:
here = PSRAM.read_at(TXN_OUTPUT_OFFSET, data_len) here = PSRAM.read_at(TXN_OUTPUT_OFFSET, data_len)
msg = txid or 'Partly Signed PSBT' msg = txid or noun
try: try:
if len(here) > 920: if len(here) > 920:
# too big for simple QR - use BBQr instead # too big for simple QR - use BBQr instead
raise ValueError raise QRTooBigError
hex_here = b2a_hex(here).upper().decode() hex_here = b2a_hex(here).upper().decode()
await show_qr_code(hex_here, is_alnum=True, msg=msg) await show_qr_code(hex_here, is_alnum=True, msg=msg)
except (ValueError, RuntimeError, TypeError): except QRTooBigError:
from ux_q1 import show_bbqr_codes from ux_q1 import show_bbqr_codes
await show_bbqr_codes('T' if txid else 'P', here, msg) await show_bbqr_codes('T' if txid else 'P', here, msg)
@ -877,8 +879,9 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
elif (ch == 't') and not is_complete: elif (ch == 't') and not is_complete:
# they might want to teleport it, but only if we have PSBT # they might want to teleport it, but only if we have PSBT
# there is no need to teleport PSBT if txn is already complete & ready to be broadcast # there is no need to teleport PSBT if txn is already complete & ready to be broadcast
# updated PSBT is at TXN_OUTPUT_OFFSET (at TXN_INPUT_OFFSET is PSBT that is NOT updated)
from teleport import kt_send_psbt from teleport import kt_send_psbt
ok = await kt_send_psbt(psbt, data_len) ok = await kt_send_psbt(psbt, data_len, psbt_offset=TXN_OUTPUT_OFFSET)
if ok is None: if ok is None:
title = "Failed to Teleport" title = "Failed to Teleport"
else: else:
@ -1026,7 +1029,7 @@ async def _save_to_disk(psbt, txid, save_options, is_complete, data_len, output_
return msg return msg
async def sign_psbt_file(filename, force_vdisk=False, slot_b=None, just_read=False, ux_abort=False): async def sign_psbt_file(filename, force_vdisk=False, slot_b=None, just_read=False, ux_abort=False):
# sign a PSBT file found on a MicroSD card # sign a PSBT file found on a MicroSD card
# - or from VirtualDisk (mk4) # - or from VirtualDisk (mk4)
@ -1118,6 +1121,51 @@ class RemoteBackup(UserAuthorizedAction):
self.done() self.done()
class RemoteRestoreBackup(UserAuthorizedAction):
def __init__(self, file_len, bitflag):
super().__init__()
self.file_len = file_len
self.custom_pwd = bitflag & 1
self.plaintext = bitflag & 2
self.force_tmp = bitflag & 4
def to_words(self):
# conversion to "words" argument of "restore_complete" function
if self.plaintext:
return None
elif self.custom_pwd:
return False
return True
def to_tmp(self):
# conversion to "temporary" argument of "restore_complete" function
from pincodes import pa
if pa.is_secret_blank() and not self.force_tmp:
# no master secret & not forcing tmp
# will load backup as master seed
return False, "master"
# has master secret --> load backup as tmp
# secret is blank but user forcing tmp
return True, "temporary"
async def interact(self):
try:
# requires confirm from user
tmp, noun = self.to_tmp()
if await ux_confirm("Restore uploaded backup as a %s seed?" % noun):
from backups import restore_complete
await restore_complete(self.file_len, tmp, self.to_words(), usb=True)
else:
self.refused = True
except BaseException as exc:
self.failed = "Error during backup restore."
# sys.print_exception(exc)
finally:
self.done()
def start_remote_backup(): def start_remote_backup():
# tell the local user the secret words, and then save to SPI flash # tell the local user the secret words, and then save to SPI flash
# USB caller has to come back and download encrypted contents. # USB caller has to come back and download encrypted contents.
@ -1128,6 +1176,12 @@ def start_remote_backup():
# kill any menu stack, and put our thing at the top # kill any menu stack, and put our thing at the top
abort_and_goto(UserAuthorizedAction.active_request) abort_and_goto(UserAuthorizedAction.active_request)
def start_remote_restore_backup(file_len, bitflag):
UserAuthorizedAction.cleanup()
UserAuthorizedAction.active_request = RemoteRestoreBackup(file_len, bitflag)
# kill any menu stack, and put our thing at the top
abort_and_goto(UserAuthorizedAction.active_request)
class NewPassphrase(UserAuthorizedAction): class NewPassphrase(UserAuthorizedAction):
def __init__(self, pw): def __init__(self, pw):
@ -1496,4 +1550,230 @@ def authorize_upgrade(hdr, length, **kws):
abort_and_goto(UserAuthorizedAction.active_request) abort_and_goto(UserAuthorizedAction.active_request)
class TXExplorer:
def __init__(self, n, user_auth_action, max_items):
self.n = n
self.user_auth_action = user_auth_action
self.max_items = max_items
self.chain = chains.current_chain()
self.qr_msgs = []
self.title = None
def can_goto_idx(self):
return self.max_items > 1
@classmethod
async def start(cls, user_auth_action):
rv = [
MenuItem("Inputs", f=TXInpExplorer(user_auth_action).explore),
MenuItem("Outputs", f=TXOutExplorer(user_auth_action).explore),
]
the_ux.push(MenuSystem(rv))
await the_ux.interact()
def make_ux_msg(self, offset, count):
from glob import dis
dis.fullscreen('Wait...')
esc = "4"+KEY_QR
rv = ""
qrs = []
change = []
end = min(offset + count, self.max_items)
for idx, item in self.yield_item(offset, end, qrs, change):
rv += item
dis.progress_sofar(idx-offset+1, count)
hints = []
if end < self.max_items:
hints.append('RIGHT to see next group')
esc += KEY_RIGHT + "9"
if offset:
hints.append('LEFT to go back')
esc += KEY_LEFT + "7"
if self.can_goto_idx():
hints.append("(2) to go to index")
esc += "2"
if not version.has_qwerty:
# Q has hint key
hints.append("(4) to show QR code")
if hints:
rv += 'Press ' + ', '.join(hints)
rv += ('. %s to quit.' % X)
else:
rv += 'Press %s to quit.' % X
return rv, qrs, change, end, esc
async def explore(self, *a):
# Page through unlimited-sized transaction details
# - shows all outputs (including change): their address and amounts.
# - shows all inputs: utxo amount and address, txid & tx index.
start = 0
msg, addrs, change, end, esc = self.make_ux_msg(start, self.n)
while True:
ch = await ux_show_story(msg, title=self.title, hint_icons=KEY_QR, escape=esc)
if ch == 'x':
del msg
return
elif (ch in "4"+KEY_QR) and addrs:
from ux import show_qr_codes
# showing addresses from PSBT, no idea what is in there
# handle QR code failures gracefully
await show_qr_codes(addrs, False, start, is_addrs=True,
change_idxs=change, can_raise=False,
qr_msgs=self.qr_msgs, no_index=bool(self.qr_msgs))
continue
elif ch in (KEY_LEFT+"7"):
if not start: continue # 0
start = max(start - self.n, 0)
elif ch in (KEY_RIGHT+"9"):
if (start + self.n) >= self.max_items:
continue
else:
# go forwards
start += self.n
elif (ch == "2") and (self.max_items > 1):
max_v = self.max_items - 1
res = await ux_enter_number("Start Idx (0-%d):" % max_v, max_value=max_v)
if res is None: continue
start = res
else:
# nothing changed - do not recalc msg
continue
msg, addrs, change, end, esc = self.make_ux_msg(start, self.n)
class TXOutExplorer(TXExplorer):
def __init__(self, user_auth_action):
super().__init__(10, user_auth_action, user_auth_action.psbt.num_outputs)
def yield_item(self, offset, end, qr_items, change_idxs):
# showing 10 outputs per UX page (just address/script + whether change)
self.title = "%d-%d" % (offset, end - 1)
for i, (idx, out) in enumerate(self.user_auth_action.psbt.output_iter(offset, end)):
outp = self.user_auth_action.psbt.outputs[idx]
item = "Output %d%s:\n\n" % (idx, " (change)" if outp.is_change else "")
msg, addr_or_script = self.user_auth_action.render_output(out)
item += msg
qr_items.append(addr_or_script)
if outp.is_change:
change_idxs.append(i)
item += "\n"
yield idx, item
class TXInpExplorer(TXExplorer):
def __init__(self, user_auth_action):
super().__init__(1, user_auth_action, user_auth_action.psbt.num_inputs)
self.qr_msgs = ["TXID", "UTXO ADDR"]
def yield_item(self, offset, end, qr_items, change_idxs):
# showing just one input per UX page
i, (idx, txin) = next(enumerate(self.user_auth_action.psbt.input_iter(offset, offset+1)))
self.title = "Input %d" % idx
inp = self.user_auth_action.psbt.inputs[idx]
txid = b2a_hex(ser_uint256(txin.prevout.hash)).decode()
qr_items.append(txid)
item = "%s:%d\n\n" % (txid, txin.prevout.n)
has_utxo = inp.has_utxo()
if has_utxo:
utxo = inp.get_utxo(txin.prevout.n)
spk = b2a_hex(utxo.scriptPubKey).decode()
try:
addr = self.chain.render_address(utxo.scriptPubKey)
except:
# some script we do not understand
addr = None
val, unit = self.chain.render_value(utxo.nValue)
item += "=== UTXO ===\n\n%s %s\n\n%s\n\n" % (val, unit, spk)
if addr:
item += show_single_address(addr) + "\n\n"
qr_items.append(addr)
if inp.addr_fmt is not None:
item += "Address Format: %s\n\n" % chains.addr_fmt_str(inp.addr_fmt)
if self.user_auth_action.psbt.txn_version >= 2:
has_rtl = inp.has_relative_timelock(txin)
if has_rtl:
if has_rtl[0]:
val = seconds2human_readable(has_rtl[1])
msg = "time-based timelock of:\n %s" % val
else:
msg = "block height timelock of %d blocks" % (has_rtl[1])
item += "Input has relative %s\n\n" % msg
psbt_item = ""
if inp.required_key:
ws = self.user_auth_action.psbt.wif_store
our = [inp.required_key] if isinstance(inp.required_key, bytes) else inp.required_key
psbt_item += "Our key%s:\n\n" % ("s" if len(our) > 1 else "")
wif_note = "(WIF Store)"
for k in our:
pubkey = b2a_hex(k).decode()
pth = inp.subpaths.get(k)
note = ""
if pth:
label = keypath_to_str(pth, prefix="%s/" % xfp2str(pth[0]))
if ws and k in ws:
note = "\n" + wif_note
psbt_item += "%s:\n%s%s\n\n" % (label, pubkey, note)
else:
psbt_item += "%s\n%s\n\n" % (pubkey, wif_note)
if inp.is_multisig:
ks_coord = inp.witness_script or inp.redeem_script
if ks_coord:
ks = self.user_auth_action.psbt.get(ks_coord)
from multisig import disassemble_multisig_mn
try:
M, N = disassemble_multisig_mn(ks)
psbt_item += "Multisig: %dof%d\n\n" % (M, N)
except: pass
if inp.part_sigs:
# do not show XFPs in case input is fully signed --> elif
# only part_sig should be available, as we haven't signed yet so added_sigs empty
done = []
for pk, pth in inp.subpaths.items():
if pk in inp.part_sigs:
done.append(xfp2str(pth[0]))
if inp.fully_signed:
psbt_item += "Input fully signed.\n\n"
else:
psbt_item += "Already signed:\n"
for xfp in done:
psbt_item += " %s\n" % xfp
psbt_item += "\n"
if inp.sighash and (inp.sighash != SIGHASH_ALL):
# only show sighash value to the user if it is non-standard
psbt_item += "sighash: %s\n\n" % {
1: "ALL", 2: "NONE", 3: "SINGLE",
1 | 0x80: "ALL|ANYONECANPAY",
2 | 0x80: "NONE|ANYONECANPAY",
3 | 0x80: "SINGLE|ANYONECANPAY",
}.get(inp.sighash, "0x%02x (non-standard)" % inp.sighash)
if psbt_item:
psbt_item = "=== PSBT ===\n\n" + psbt_item
item += psbt_item
yield idx, item
# EOF # EOF

View File

@ -5,10 +5,11 @@
import compat7z, stash, ckcc, chains, gc, sys, bip39, uos, ngu import compat7z, stash, ckcc, chains, gc, sys, bip39, uos, ngu
from ubinascii import hexlify as b2a_hex from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex from ubinascii import unhexlify as a2b_hex
from utils import deserialize_secret from utils import deserialize_secret, swab32, xfp2str
from sffile import SFFile
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, OK, X, ux_input_text from ux import ux_show_story, ux_confirm, ux_dramatic_pause, OK, X, ux_input_text
import version, ujson import version, ujson
from uio import StringIO from uio import StringIO, BytesIO
import seed import seed
from glob import settings from glob import settings
from pincodes import pa from pincodes import pa
@ -48,7 +49,7 @@ def render_backup_contents(bypass_tmp=False):
if sv.mode == 'words': if sv.mode == 'words':
ADD('mnemonic', bip39.b2a_words(sv.raw)) ADD('mnemonic', bip39.b2a_words(sv.raw))
if sv.mode == 'master': elif sv.mode == 'master':
ADD('bip32_master_key', b2a_hex(sv.raw)) ADD('bip32_master_key', b2a_hex(sv.raw))
ADD('chain', chain.ctype) ADD('chain', chain.ctype)
@ -75,7 +76,12 @@ def render_backup_contents(bypass_tmp=False):
current_tmp = pa.tmp_value[:] current_tmp = pa.tmp_value[:]
pa.tmp_value = None pa.tmp_value = None
# we also need correct settings from main seed # we also need correct settings from main seed
nv = stash.SecretStash.encode(seed_phrase=sv.raw) if sv.mode == 'words':
nv = stash.SecretStash.encode(seed_phrase=sv.raw)
else:
assert sv.mode == "xprv"
nv = stash.SecretStash.encode(xprv=sv.node)
settings.set_key(nv) settings.set_key(nv)
settings.load() settings.load()
stash.blank_object(nv) stash.blank_object(nv)
@ -101,6 +107,7 @@ def render_backup_contents(bypass_tmp=False):
if k == 'words': continue # words length is recalculated from secret if k == 'words': continue # words length is recalculated from secret
if k == 'ccc': continue # not supported, security issue if k == 'ccc': continue # not supported, security issue
if k == 'ktrx': continue # not useful after the fact if k == 'ktrx': continue # not useful after the fact
if k == 'lfr': continue # temporary error msg value
if k == 'seedvault' and not v: continue if k == 'seedvault' and not v: continue
if k == 'seeds' and not v: continue if k == 'seeds' and not v: continue
ADD('setting.' + k, v) ADD('setting.' + k, v)
@ -121,7 +128,7 @@ def render_backup_contents(bypass_tmp=False):
return rv.getvalue() return rv.getvalue()
def extract_raw_secret(chain, vals): def extract_raw_secret(vals):
# step1: the private key # step1: the private key
# - prefer raw_secret over other values # - prefer raw_secret over other values
# - TODO: fail back to other values # - TODO: fail back to other values
@ -136,10 +143,10 @@ def extract_raw_secret(chain, vals):
# verify against xprv value (if we have it) # verify against xprv value (if we have it)
if 'xprv' in vals: if 'xprv' in vals:
check_xprv = chain.serialize_private(node) check_xprv = chains.get_chain(vals.get('chain', 'BTC')).serialize_private(node)
assert check_xprv == vals['xprv'], 'xprv mismatch' assert check_xprv == vals['xprv'], 'xprv mismatch'
return raw return raw, node
def extract_long_secret(vals): def extract_long_secret(vals):
ls = None ls = None
@ -152,7 +159,7 @@ def extract_long_secret(vals):
pass pass
return ls return ls
def restore_from_dict_ll(vals): def restore_from_dict_ll(vals, raw):
# Restore from a dict of values. Already JSON decoded. # Restore from a dict of values. Already JSON decoded.
# Need a Reboot on success, return string on failure # Need a Reboot on success, return string on failure
# - low-level version, factored out for better testing # - low-level version, factored out for better testing
@ -163,12 +170,6 @@ def restore_from_dict_ll(vals):
#print("Restoring from: %r" % vals) #print("Restoring from: %r" % vals)
chain = chains.get_chain(vals.get('chain', 'BTC')) chain = chains.get_chain(vals.get('chain', 'BTC'))
try:
raw = extract_raw_secret(chain, vals)
except Exception as e:
return ('Unable to decode raw_secret and '
'restore the seed value!\n\n\n'+str(e)), None
dis.fullscreen("Saving...") dis.fullscreen("Saving...")
dis.progress_bar_show(.1) dis.progress_bar_show(.1)
@ -205,6 +206,13 @@ def restore_from_dict_ll(vals):
k = key[8:] k = key[8:]
if k == 'bkpw':
# never import a cached backup password from a backup file.
# write-side (render_backup_contents) strips bkpw, so a present
# value means a tampered/crafted file trying to fixate the
# password used for all FUTURE backups - drop it.
continue
if k == 'sd2fa': if k == 'sd2fa':
# do NOT restore sd2fa as SD card can be lost or damaged # do NOT restore sd2fa as SD card can be lost or damaged
# new version of firmware 5.1.3+ will not back sd2fa # new version of firmware 5.1.3+ will not back sd2fa
@ -281,15 +289,10 @@ def text_bk_parser(contents):
return vals return vals
async def restore_tmp_from_dict_ll(vals): async def restore_tmp_from_dict_ll(vals, raw):
from glob import dis from glob import dis
chain = chains.get_chain(vals.get('chain', 'BTC')) chain = chains.get_chain(vals.get('chain', 'BTC'))
try:
raw = extract_raw_secret(chain, vals)
except Exception as e:
return ('Unable to decode raw_secret and '
'restore the seed value!\n\n\n' + str(e))
dis.fullscreen("Applying...") dis.fullscreen("Applying...")
from seed import set_ephemeral_seed from seed import set_ephemeral_seed
@ -306,11 +309,11 @@ async def restore_tmp_from_dict_ll(vals):
goto_top_menu() goto_top_menu()
async def restore_from_dict(vals): async def restore_from_dict(vals, raw):
# Restore from a dict of values. Already JSON decoded (ie. dict object). # Restore from a dict of values. Already JSON decoded (ie. dict object).
# Need a Reboot on success, return string on failure # Need a Reboot on success, return string on failure
prob, need_ftux = restore_from_dict_ll(vals) prob, need_ftux = restore_from_dict_ll(vals, raw)
if prob: return prob if prob: return prob
if need_ftux: if need_ftux:
@ -456,8 +459,6 @@ async def write_complete_backup(pwd, fname_pattern, write_sflash=False,
if write_sflash: if write_sflash:
# for use over USB and unit testing: commit file into PSRAM # for use over USB and unit testing: commit file into PSRAM
from sffile import SFFile
with SFFile(0, max_size=MAX_BACKUP_FILE_SIZE, message='Saving...') as fd: with SFFile(0, max_size=MAX_BACKUP_FILE_SIZE, message='Saving...') as fd:
if zz: if zz:
fd.write(hdr) fd.write(hdr)
@ -551,10 +552,14 @@ async def verify_backup_file(fname):
# might be already closed on vdisk case due to filesystem unmount/mount # might be already closed on vdisk case due to filesystem unmount/mount
pass pass
await ux_show_story("Backup file CRC checks out okay.\n\nPlease note this is only a check against accidental truncation and similar. Targeted modifications can still pass this test.") await ux_show_story("Backup file CRC checks out okay.\n\n"
"Please note this is only a check against accidental truncation and similar."
" Targeted modifications can still pass this test. You may further verify"
" this backup file by starting the normal restore process (Restore Backup)"
" and aborting it once decryption has been achieved.")
async def restore_complete(fname_or_fd, temporary=False, words=True): async def restore_complete(fname_or_fd, temporary=False, words=True, usb=False):
from ux import the_ux from ux import the_ux
async def done(words): async def done(words):
@ -564,91 +569,151 @@ async def restore_complete(fname_or_fd, temporary=False, words=True):
prob = await restore_complete_doit(fname_or_fd, words, prob = await restore_complete_doit(fname_or_fd, words,
temporary=temporary) temporary=temporary)
if prob: if prob:
await ux_show_story(prob, title='FAILED') await ux_show_story(prob, title='FAILED')
if words: if words:
if version.has_qwerty: if version.has_qwerty:
from ux_q1 import seed_word_entry from ux_q1 import seed_word_entry, CHARS_W
return await seed_word_entry('Enter Password:', num_pw_words,
done_cb=done, has_checksum=False)
# give them a menu to pick from, and start picking
m = seed.WordNestMenu(num_words=num_pw_words, has_checksum=False, done_cb=done)
the_ux.push(m) basename = None
if isinstance(fname_or_fd, str):
basename = fname_or_fd.split('/')[-1]
if len(basename) > CHARS_W:
basename = basename[:16] + "" + basename[-16:]
return await seed_word_entry("Enter Password%s:" % (" for" if basename else ""),
num_pw_words, done_cb=done, has_checksum=False,
line2=basename)
# give them a menu to pick from, and start picking
if usb:
# we're not originating from a menu
words = await seed.WordNestMenu.get_n_words(num_pw_words)
if len(words) != num_pw_words:
seed.WordNestMenu.pop_all()
return
await done(words)
else:
m = seed.WordNestMenu(num_words=num_pw_words, has_checksum=False, done_cb=done)
the_ux.push(m)
else: else:
pwd = [] # cleartext if words=None pwd = [] # cleartext if words=None
if words is False: if words is False:
ipw = await ux_input_text("", prompt="Your Backup Password", ipw = await ux_input_text("", prompt="Your Backup Password",
min_len=bkpw_min_len, max_len=128) min_len=bkpw_min_len, max_len=128)
if not ipw: return
pwd.append(ipw) pwd.append(ipw)
await done(pwd) await done(pwd)
async def restore_complete_doit(fname_or_fd, words, file_cleanup=None, temporary=False):
def check_and_decrypt(fd, password):
try:
compat7z.check_file_headers(fd)
except Exception as e:
raise RuntimeError('Unable to read backup file.'
' Has it been touched?\n\nError: '+str(e))
from glob import dis
dis.fullscreen("Decrypting...")
try:
zz = compat7z.Builder()
fname, contents = zz.read_file(fd, password, MAX_BACKUP_FILE_SIZE,
progress_fcn=dis.progress_bar_show)
# simple quick sanity checks
assert fname.endswith('.txt') # was == 'ckcc-backup.txt'
assert contents[0:1] == b'#' and contents[-1:] == b'\n'
return contents
except Exception as e:
# assume everything here is "password wrong" errors
raise RuntimeError('Unable to decrypt backup file. Incorrect password?'
'\n\nTried:\n\n' + password)
async def restore_complete_doit(fname_or_fd, words, file_cleanup=None, temporary=False,
ux_confirm=True):
# Open file, read it, maybe decrypt it; return string if any error # Open file, read it, maybe decrypt it; return string if any error
# - some errors will be shown, None return in that case # - some errors will be shown, None return in that case
# - no return if successful (due to reboot) # - no return if successful (due to reboot)
from glob import dis
from files import CardSlot, CardMissingError, needs_microsd from files import CardSlot, CardMissingError, needs_microsd
# build password # build password
password = ' '.join(words) password = ' '.join(words)
prob = None prob = None
try: if isinstance(fname_or_fd, int):
with CardSlot(readonly=True) as card: # USB restore - backup is already in PSRAM, fname of fd is length
# filename already picked, taste it and maybe consider using its data. # TXN_INPUT_OFFSET = 0
try: with SFFile(0, length=fname_or_fd) as fd:
fd = open(fname_or_fd, 'rb') if isinstance(fname_or_fd, str) else fname_or_fd if not words:
except: contents = fd.read(fname_or_fd)
return 'Unable to open backup file.\n\n' + str(fname_or_fd) else:
# read full size, then decrypt
fd = BytesIO(fd.read(fname_or_fd))
try:
contents = check_and_decrypt(fd, password)
except RuntimeError as e:
return str(e)
else:
try:
with CardSlot(readonly=True) as card:
# filename already picked, taste it and maybe consider using its data.
try:
fd = open(fname_or_fd, 'rb')
except:
return 'Unable to open backup file.\n\n' + str(fname_or_fd)
try: try:
if not words: if words:
contents = fd.read() contents = check_and_decrypt(fd, password)
else: else:
try: contents = fd.read()
compat7z.check_file_headers(fd)
except Exception as e:
return 'Unable to read backup file. Has it been touched?\n\nError: ' \
+ str(e)
dis.fullscreen("Decrypting...") except RuntimeError as e:
try: return str(e)
zz = compat7z.Builder() finally:
fname, contents = zz.read_file(fd, password, MAX_BACKUP_FILE_SIZE, fd.close()
progress_fcn=dis.progress_bar_show)
# simple quick sanity checks
assert fname.endswith('.txt') # was == 'ckcc-backup.txt'
assert contents[0:1] == b'#' and contents[-1:] == b'\n'
except Exception as e:
# assume everything here is "password wrong" errors
#print("pw wrong? %s" % e)
return ('Unable to decrypt backup file. Incorrect password?'
'\n\nTried:\n\n' + password)
finally:
fd.close()
if file_cleanup: if file_cleanup:
file_cleanup(fname_or_fd) file_cleanup(fname_or_fd)
except CardMissingError: except CardMissingError:
await needs_microsd() await needs_microsd()
return return
vals = text_bk_parser(contents) try:
vals = text_bk_parser(contents)
except:
return "Invalid backup file."
try:
raw, node = extract_raw_secret(vals)
except Exception as e:
return ('Unable to decode raw_secret and '
'restore the seed value!\n\n\n'+str(e))
if ux_confirm:
# check master fingerprint from raw secret that is actually being loaded
# master extended public keys can be wrong & is unverified
xfp_str = xfp2str(swab32(node.my_fp()))
ch = await ux_show_story("Above is the master fingerprint of the seed stored in the backup."
" Press %s to continue, and load backup as %s seed. Press %s"
" to abort." % (OK, "temporary" if temporary else "master", X),
title="["+xfp_str+"]")
if ch != "y":
await ux_dramatic_pause('Aborted.', 2)
return
# this leads to reboot if it works, else errors shown, etc. # this leads to reboot if it works, else errors shown, etc.
if temporary: if temporary:
return await restore_tmp_from_dict_ll(vals) return await restore_tmp_from_dict_ll(vals, raw)
else: else:
return await restore_from_dict(vals) return await restore_from_dict(vals, raw)
async def clone_start(*a): async def clone_start(*a):
# Begins cloning process, on target device. # Begins cloning process, on target device.
@ -731,8 +796,9 @@ back and press %s to complete clone process.''' % OK)
uos.remove(fname) # ccbk-start.json uos.remove(fname) # ccbk-start.json
# this will reset in successful case, no return (but delme is called) # this will reset in successful case, no return (but delme is called)
prob = await restore_complete_doit(incoming, words, file_cleanup=delme) # no need to ask for UX confirmation during clone - as user can see what is loaded on source CC
prob = await restore_complete_doit(incoming, words, file_cleanup=delme,
ux_confirm=False)
if prob: if prob:
await ux_show_story(prob, title='FAILED') await ux_show_story(prob, title='FAILED')

View File

@ -138,12 +138,15 @@ async def batt_idle_logout():
# - even before login # - even before login
import glob import glob
from uasyncio import sleep_ms from uasyncio import sleep_ms
from glob import settings, dis from glob import settings, dis, SCAN
import utime import utime
while True: while True:
await sleep_ms(20000) # 20 seconds await sleep_ms(20000) # 20 seconds
if SCAN.busy_scanning:
continue
if get_batt_level() is None: if get_batt_level() is None:
# on USB power # on USB power
continue continue

View File

@ -54,7 +54,7 @@ def calc_num_qr(char_capacity, char_len, split_mod):
if char_len > actual: if char_len > actual:
need += 1 need += 1
# Challenge: the final QR might have just a a few chars in it, if we redistribute # Challenge: the final QR might have just a few chars in it, if we redistribute
# the data into the other parts, then each QR can have more forward error correction # the data into the other parts, then each QR can have more forward error correction
# and be more robust. Must respect split_mod alignment tho. # and be more robust. Must respect split_mod alignment tho.
level = ceil(char_len / need) level = ceil(char_len / need)
@ -439,5 +439,18 @@ class BBQrPsramStorage(BBQrStorage):
from glob import PSRAM from glob import PSRAM
return PSRAM.read_at(0, self.final_size) return PSRAM.read_at(0, self.final_size)
def finalize(self):
self._finalize()
if self.hdr.encoding == 'Z':
self.zlib_decompress()
# PSBT-typed BBQrs end up at PSRAM[0..size]
# skip a redundant PSRAM->heap->PSRAM round-trip
if self.hdr.file_type == 'P':
return self.hdr.file_type, self.final_size, 'PSRAM'
return self.hdr.file_type, self.final_size, self.get_buffer()
# EOF # EOF

9
shared/block_height.py Normal file
View File

@ -0,0 +1,9 @@
# (c) Copyright 2026 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# AUTO-generated.
#
# Updated: 2026-06-19 14:13:23 UTC
BLOCK_HEIGHT = 932301
# EOF

View File

@ -9,7 +9,7 @@ from utils import B2A, word_wrap
from ux_q1 import ux_input_text from ux_q1 import ux_input_text
async def login_repl(): async def login_repl():
from glob import dis, settings from glob import dis
from pincodes import pa from pincodes import pa
NUM_LINES = 7 # 10 - title - 2 for prompt NUM_LINES = 7 # 10 - title - 2 for prompt
@ -65,11 +65,11 @@ Example Commands:
elif ln in ('help', 'cls', 'rand'): elif ln in ('help', 'cls', 'rand'):
# no need for () for these commands # no need for () for these commands
ans = state[ln]() ans = state[ln]()
elif re_pin.match(ln) and len(ln) <= 13: elif pa.attempts_left and re_pin.match(ln) and (len(ln) <= 13):
# try login # try login
m = re_pin.match(ln) m = re_pin.match(ln)
ln = m.group(1)+ '-' + m.group(2) ln = m.group(1)+ '-' + m.group(2)
print(ln)
try: try:
pa.setup(ln) pa.setup(ln)
ok = pa.login() ok = pa.login()
@ -83,7 +83,7 @@ Example Commands:
else: else:
ans = 'Error: ' + repr(exc.args) ans = 'Error: ' + repr(exc.args)
elif re_prefix.match(ln) and len(ln) <= 7: elif re_prefix.match(ln) and (len(ln) <= 7):
# show words # show words
ans = pa.prefix_words(ln[:-1].encode()) ans = pa.prefix_words(ln[:-1].encode())
else: else:

File diff suppressed because it is too large Load Diff

View File

@ -5,14 +5,15 @@
import ngu import ngu
from uhashlib import sha256 from uhashlib import sha256
from ubinascii import hexlify as b2a_hex from ubinascii import hexlify as b2a_hex
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2TR from public_constants import AF_BARE_PK, AF_CLASSIC, AF_P2WPKH, AF_P2TR
from public_constants import AF_P2SH, AF_P2WSH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH from public_constants import AF_P2SH, AF_P2WSH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH
from public_constants import AFC_PUBKEY, AFC_SEGWIT, AFC_BECH32, AFC_SCRIPT from public_constants import AFC_PUBKEY, AFC_SEGWIT, AFC_BECH32, AFC_SCRIPT
from block_height import BLOCK_HEIGHT
from serializations import hash160, ser_compact_size, disassemble from serializations import hash160, ser_compact_size, disassemble
from ucollections import namedtuple from ucollections import namedtuple
from opcodes import OP_RETURN, OP_1, OP_16 from opcodes import OP_RETURN, OP_1, OP_16
# DO NOT CHANGE ORDER! PickAddrFmtMenu.__init__ expects correct order
SINGLESIG_AF = (AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH) SINGLESIG_AF = (AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH)
# See SLIP 132 <https://github.com/satoshilabs/slips/blob/master/slip-0132.md> # See SLIP 132 <https://github.com/satoshilabs/slips/blob/master/slip-0132.md>
@ -22,8 +23,6 @@ Slip132Version = namedtuple('Slip132Version', ('pub', 'priv', 'hint'))
# See also: # See also:
# - <https://github.com/satoshilabs/slips/blob/master/slip-0132.md> # - <https://github.com/satoshilabs/slips/blob/master/slip-0132.md>
# - defines ypub/zpub/Xprc variants # - defines ypub/zpub/Xprc variants
# - <https://github.com/satoshilabs/slips/blob/master/slip-0032.md>
# - nice bech32 encoded scheme for going forward
# - <https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-September/014907.html> # - <https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-September/014907.html>
# - mailing list post proposed ypub, etc. # - mailing list post proposed ypub, etc.
# - from <https://github.com/Bit-Wasp/bitcoin-php/issues/576> # - from <https://github.com/Bit-Wasp/bitcoin-php/issues/576>
@ -82,6 +81,41 @@ class ChainsBase:
or (version == cls.slip132[addr_fmt].priv) or (version == cls.slip132[addr_fmt].priv)
return node return node
@classmethod
def script_pubkey(cls, addr_fmt, pubkey=None, script=None):
digest = None
if addr_fmt & AFC_SCRIPT:
assert script, "need witness/redeem script"
if addr_fmt in [AF_P2WSH, AF_P2WSH_P2SH]:
digest = ngu.hash.sha256s(script)
# bech32 encoded segwit p2sh
spk = b'\x00\x20' + digest
if addr_fmt == AF_P2WSH_P2SH:
# segwit p2wsh encoded as classic P2SH
digest = hash160(spk)
spk = b'\xA9\x14' + digest + b'\x87'
else:
assert addr_fmt == AF_P2SH
digest = hash160(script)
spk = b'\xA9\x14' + digest + b'\x87'
else:
assert pubkey
keyhash = ngu.hash.hash160(pubkey)
if addr_fmt == AF_CLASSIC:
spk = b'\x76\xA9\x14' + keyhash + b'\x88\xAC'
elif addr_fmt == AF_P2WPKH_P2SH:
redeem_script = b'\x00\x14' + keyhash
spk = b'\xA9\x14' + ngu.hash.hash160(redeem_script) + b'\x87'
elif addr_fmt == AF_P2WPKH:
spk = b'\x00\x14' + keyhash
else:
raise ValueError('bad address template: %s' % addr_fmt)
return spk, digest
@classmethod @classmethod
def p2sh_address(cls, addr_fmt, witdeem_script): def p2sh_address(cls, addr_fmt, witdeem_script):
# Multisig and general P2SH support # Multisig and general P2SH support
@ -93,21 +127,14 @@ class ChainsBase:
# - returns: str(address) # - returns: str(address)
assert addr_fmt & AFC_SCRIPT, 'for p2sh only' assert addr_fmt & AFC_SCRIPT, 'for p2sh only'
assert witdeem_script, "need witness/redeem script" _, digest = cls.script_pubkey(addr_fmt, script=witdeem_script)
if addr_fmt & AFC_SEGWIT: if addr_fmt == AF_P2WSH:
digest = ngu.hash.sha256s(witdeem_script)
else:
digest = hash160(witdeem_script)
if addr_fmt & AFC_BECH32:
# bech32 encoded segwit p2sh # bech32 encoded segwit p2sh
addr = ngu.codecs.segwit_encode(cls.bech32_hrp, 0, digest) addr = ngu.codecs.segwit_encode(cls.bech32_hrp, 0, digest)
elif addr_fmt == AF_P2WSH_P2SH:
# segwit p2wsh encoded as classic P2SH
addr = ngu.codecs.b58_encode(cls.b58_script + hash160(b'\x00\x20' + digest))
else: else:
# P2SH classic # segwit p2wsh encoded as classic P2SH
# and P2SH classic
addr = ngu.codecs.b58_encode(cls.b58_script + digest) addr = ngu.codecs.b58_encode(cls.b58_script + digest)
return addr return addr
@ -117,20 +144,8 @@ class ChainsBase:
# - renders a pubkey to an address # - renders a pubkey to an address
# - works only with single-key addresses # - works only with single-key addresses
assert not addr_fmt & AFC_SCRIPT assert not addr_fmt & AFC_SCRIPT
spk, _ = cls.script_pubkey(addr_fmt, pubkey=pubkey)
keyhash = ngu.hash.hash160(pubkey) return cls.render_address(spk)
if addr_fmt == AF_CLASSIC:
script = b'\x76\xA9\x14' + keyhash + b'\x88\xAC'
elif addr_fmt == AF_P2WPKH_P2SH:
redeem_script = b'\x00\x14' + keyhash
scripthash = ngu.hash.hash160(redeem_script)
script = b'\xA9\x14' + scripthash + b'\x87'
elif addr_fmt == AF_P2WPKH:
script = b'\x00\x14' + keyhash
else:
raise ValueError('bad address template: %s' % addr_fmt)
return cls.render_address(script)
@classmethod @classmethod
def address(cls, node, addr_fmt): def address(cls, node, addr_fmt):
@ -239,7 +254,7 @@ class ChainsBase:
return ngu.codecs.b58_encode(cls.b58_script + script[2:2+20]) return ngu.codecs.b58_encode(cls.b58_script + script[2:2+20])
# segwit v0 (P2WPKH, P2WSH) # segwit v0 (P2WPKH, P2WSH)
if script[0] == 0 and script[1] in (0x14, 0x20) and (ll-2) == script[1]: if ll in (22, 34) and script[0] == 0 and script[1] in (0x14, 0x20) and (ll-2) == script[1]:
return ngu.codecs.segwit_encode(cls.bech32_hrp, script[0], script[2:]) return ngu.codecs.segwit_encode(cls.bech32_hrp, script[0], script[2:])
# segwit v1 (P2TR) and later segwit version # segwit v1 (P2TR) and later segwit version
@ -250,56 +265,40 @@ class ChainsBase:
@classmethod @classmethod
def op_return(cls, script): def op_return(cls, script):
# returns decoded string op return data if script is op return otherwise None try:
gen = disassemble(script) gen = disassemble(script)
script_type = next(gen) item, opcode = next(gen)
if OP_RETURN not in script_type: except (StopIteration, ValueError):
return return None
if opcode != OP_RETURN:
return None
try: try:
data = next(gen)[0] try:
if data: data, opcode = next(gen)
return data except StopIteration:
except StopIteration: return b"" # bare OP_RETURN
pass
return b"" try:
next(gen)
return None # extra ops/pushes -> raw script display
except StopIteration: pass
@classmethod except ValueError:
def possible_address_fmt(cls, addr): return None
# Given a text (serialized) address, return what
# address format applies to the address, but
# for AF_P2SH case, could be: AF_P2SH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH. .. we don't know
hrp = cls.bech32_hrp + "1"
if addr.startswith(hrp):
if addr.startswith(hrp+'p'):
# segwit v1 (any ver=1 script or address, but for now just taproot...)
return AF_P2TR
elif addr.startswith(hrp+'q'):
# segwit v0
return AF_P2WPKH if len(addr) < 55 else AF_P2WSH
return 0
try:
raw = ngu.codecs.b58_decode(addr)
except ValueError:
# not base58, not an error
return 0
if raw[0] == cls.b58_addr[0]:
return AF_CLASSIC
if raw[0] == cls.b58_script[0]:
return AF_P2SH
return 0
if isinstance(data, bytes):
return data
if data is None and opcode == 0:
return b"" # OP_RETURN OP_0
return None
class BitcoinMain(ChainsBase): class BitcoinMain(ChainsBase):
# see <https://github.com/bitcoin/bitcoin/blob/master/src/chainparams.cpp#L140> # see <https://github.com/bitcoin/bitcoin/blob/master/src/chainparams.cpp#L140>
ctype = 'BTC' ctype = 'BTC'
name = 'Bitcoin Mainnet' name = 'Bitcoin Mainnet'
ccc_min_block = 892714 # Apr 16/2025 ccc_min_block = BLOCK_HEIGHT
slip132 = { slip132 = {
AF_CLASSIC: Slip132Version(0x0488B21E, 0x0488ADE4, 'x'), AF_CLASSIC: Slip132Version(0x0488B21E, 0x0488ADE4, 'x'),
@ -339,26 +338,11 @@ class BitcoinTestnet(ChainsBase):
b44_cointype = 1 b44_cointype = 1
class BitcoinRegtest(ChainsBase): class BitcoinRegtest(BitcoinTestnet):
ctype = 'XRT' ctype = 'XRT'
name = 'Bitcoin Regtest' name = 'Bitcoin Regtest'
slip132 = {
AF_CLASSIC: Slip132Version(0x043587cf, 0x04358394, 't'),
AF_P2WPKH_P2SH: Slip132Version(0x044a5262, 0x044a4e28, 'u'),
AF_P2WPKH: Slip132Version(0x045f1cf6, 0x045f18bc, 'v'),
AF_P2WSH_P2SH: Slip132Version(0x024289ef, 0x024285b5, 'U'),
AF_P2WSH: Slip132Version(0x02575483, 0x02575048, 'V'),
}
bech32_hrp = 'bcrt' bech32_hrp = 'bcrt'
b58_addr = bytes([111])
b58_script = bytes([196])
b58_privkey = bytes([239])
b44_cointype = 1
def get_chain(short_name): def get_chain(short_name):
# lookup object from name: 'BTC' or 'XTN' # lookup object from name: 'BTC' or 'XTN'
@ -386,7 +370,7 @@ def current_chain():
# Overbuilt: will only be testnet and mainchain. # Overbuilt: will only be testnet and mainchain.
AllChains = [BitcoinMain, BitcoinTestnet, BitcoinRegtest] AllChains = [BitcoinMain, BitcoinTestnet, BitcoinRegtest]
def slip32_deserialize(xp): def slip132_deserialize(xp):
# .. and classify chain and addr-type, as implied by prefix # .. and classify chain and addr-type, as implied by prefix
node = ngu.hdnode.HDNode() node = ngu.hdnode.HDNode()
version = node.deserialize(xp) version = node.deserialize(xp)
@ -449,17 +433,31 @@ def parse_addr_fmt_str(addr_fmt):
def af_to_bip44_purpose(addr_fmt): def af_to_bip44_purpose(addr_fmt):
# single signature only # Address format to BIP-44 "purpose" number
# - single signature only
return {AF_CLASSIC: 44, return {AF_CLASSIC: 44,
AF_P2WPKH_P2SH: 49, AF_P2WPKH_P2SH: 49,
AF_P2WPKH: 84}[addr_fmt] AF_P2WPKH: 84}[addr_fmt]
def addr_fmt_label(addr_fmt): def addr_fmt_label(addr_fmt):
# Text used in menus
return {AF_CLASSIC: "Classic P2PKH", return {AF_CLASSIC: "Classic P2PKH",
AF_P2WPKH_P2SH: "P2SH-Segwit", AF_P2WPKH_P2SH: "P2SH-Segwit",
AF_P2WPKH: "Segwit P2WPKH"}[addr_fmt] AF_P2WPKH: "Segwit P2WPKH"}[addr_fmt]
def addr_fmt_str(addr_fmt):
# Short string codes used for address format (industry standard)
return {AF_CLASSIC: "p2pkh",
AF_BARE_PK: "p2pk",
AF_P2SH: "p2sh",
AF_P2TR: "p2tr",
AF_P2WPKH: "p2wpkh",
AF_P2WSH: "p2wsh",
AF_P2WPKH_P2SH: "p2sh-p2wpkh",
AF_P2WSH_P2SH: "p2sh-p2wsh"}[addr_fmt]
def verify_recover_pubkey(sig, digest): def verify_recover_pubkey(sig, digest):
# verifies a message digest against a signature and recovers # verifies a message digest against a signature and recovers
# the address type and public key that did the signing # the address type and public key that did the signing

View File

@ -51,9 +51,7 @@ def decode_utf_16_le(s):
''' '''
def read_var64(f): def read_var64(f):
''' # Decode their silly 64-bit encoding.
Decode their silly 64-bit encoding.
'''
first = ord(f.read(1)) first = ord(f.read(1))
if first < 128: if first < 128:
return first return first
@ -100,7 +98,7 @@ def check_file_headers(f):
# assume f is seekable # assume f is seekable
fh = FileHeader.read(f) fh = FileHeader.read(f)
if not fh.has_good_magic: if not fh.has_good_magic():
raise ValueError("Bad magic bytes") raise ValueError("Bad magic bytes")
# read only first header # read only first header
@ -113,22 +111,21 @@ def check_file_headers(f):
if sh.size > 10000: if sh.size > 10000:
raise ValueError("Second header too big") raise ValueError("Second header too big")
# capture this spot # FileHeader.read() always reads exactly calcsize('<6sBBL') = 12 bytes
# TODO 'data_start' unused # SectionHeader.read() always reads exactly calcsize('<QQL') = 20 bytes
data_start = f.tell() # expect 0x20 # after those two calls, f.tell() is always start_pos + 32
# assert f.tell() == 0x20 # expect 0x20
try: try:
f.seek(sh.offset, 1) f.seek(sh.offset, 1)
th = f.read(sh.size) th = f.read(sh.size)
if len(th) != sh.size: assert len(th) == sh.size, "Truncated file?"
raise IndexError("Truncated file?")
# Look for properties about compression. this could be # Look for properties about compression. this could be
# faked-out but good enough for now # faked-out but good enough for now
if b'\x24\x06\xf1\x07\x01' not in th: assert b'\x24\x06\xf1\x07\x01' in th, "Not marked as AES+SHA encrypted?"
raise RuntimeError("Not marked as AES+SHA encrypted?")
except Exception as e: except Exception as e:
raise ValueError("Confused file? %s" % e.message) raise ValueError("Confused file? %s" % e)
if masked_crc(th) != sh.crc: if masked_crc(th) != sh.crc:
raise ValueError("Trailing header has wrong CRC") raise ValueError("Trailing header has wrong CRC")
@ -174,7 +171,6 @@ class FileHeader(object):
def actual_crc(self): def actual_crc(self):
return masked_crc(self.bits) return masked_crc(self.bits)
class SectionHeader(namedtuple('SectionHeader', ['offset', 'size', 'crc' ])): class SectionHeader(namedtuple('SectionHeader', ['offset', 'size', 'crc' ])):
@ -213,6 +209,7 @@ class SectionHeader(namedtuple('SectionHeader', ['offset', 'size', 'crc' ])):
def actual_crc(self): def actual_crc(self):
return masked_crc(self.bits) return masked_crc(self.bits)
class Builder(object): class Builder(object):
def __init__(self, password=None, salt_len=16, iv_len=16, rounds_pow=13, progress_fcn=None): def __init__(self, password=None, salt_len=16, iv_len=16, rounds_pow=13, progress_fcn=None):
self.rounds_pow = rounds_pow # standard is 19, 16 and 17 work fine self.rounds_pow = rounds_pow # standard is 19, 16 and 17 work fine

View File

@ -11,6 +11,15 @@ from bbqr import TYPE_LABELS
from utils import decode_bip21_text from utils import decode_bip21_text
def decode_qr_text(got):
if isinstance(got, str):
return got
try:
return got.decode()
except UnicodeError:
raise QRDecodeExplained('UTF-8 decode failed')
def decode_seed_qr(data): def decode_seed_qr(data):
# SeedQR: 4 digit groups of index into word list # SeedQR: 4 digit groups of index into word list
parts = [data[pos:pos + 4] for pos in range(0, len(data), 4)] parts = [data[pos:pos + 4] for pos in range(0, len(data), 4)]
@ -39,6 +48,8 @@ def decode_secret(got):
# - xprv / tprv # - xprv / tprv
# - words (either full or prefixes, case insensitive) # - words (either full or prefixes, case insensitive)
# - SeedQR (github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md) # - SeedQR (github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md)
# - word lists are NOT BIP-39-checksum-validated here. Callers that
# require a valid seed must run bip39.a2b_words(...)
if len(got) > 300: if len(got) > 300:
raise ValueError("Too big.") raise ValueError("Too big.")
@ -51,7 +62,7 @@ def decode_secret(got):
# xprv or tprv: private key import for sure # xprv or tprv: private key import for sure
# - verify checksum is right # - verify checksum is right
try: try:
raw = ngu.codecs.b58_decode(got) ngu.codecs.b58_decode(got)
except: except:
raise ValueError('corrupt xprv?') raise ValueError('corrupt xprv?')
@ -59,19 +70,11 @@ def decode_secret(got):
if len(got) in (51, 52): if len(got) in (51, 52):
try: try:
raw = ngu.codecs.b58_decode(got) from wif import decode_wif
if raw[0] in (0xef, 0x80): kp, testnet, compressed = decode_wif(got)
testnet = True if raw[0] == 0xef else False return 'wif', (got, kp, compressed, testnet)
if len(raw) in (33, 34): # uncompressed pubkey
compressed = False
if len(raw) == 34: # compressed pubkey
assert raw[33] == 0x01
compressed = True
sk = raw[1:33]
kp = ngu.secp256k1.keypair(sk)
return 'wif', (got, kp, compressed, testnet)
except: pass except: pass
taste = got.strip().lower() taste = got.strip().lower()
if taste.isdigit(): if taste.isdigit():
@ -116,11 +119,8 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
return got.decode() return got.decode()
if ty == 'P': if ty == 'P':
# may already be in PSRAM, avoid a copy here # `got` is the literal 'PSRAM' from BBQrPsramStorage when data already there
from glob import PSRAM # otherwise it's real bytes
if PSRAM.is_at(got, 0):
got = 'PSRAM' # see qr_psbt_sign()
return 'psbt', (None, final_size, got) return 'psbt', (None, final_size, got)
elif ty == 'T': elif ty == 'T':
@ -128,9 +128,10 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
elif ty == 'U': elif ty == 'U':
# continue thru code below for TEXT # continue thru code below for TEXT
pass got = decode_qr_text(got)
elif ty == 'J': elif ty == 'J':
got = decode_qr_text(got)
what = "json" what = "json"
if "msg" in got: if "msg" in got:
what = "smsg" what = "smsg"
@ -139,6 +140,11 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
elif ty in 'RSE': elif ty in 'RSE':
# key-teleport related # key-teleport related
from pincodes import pa
if pa.hobbled_mode and ty != 'E':
raise QRDecodeExplained("KT Blocked")
if ty == 'R' and len(got) != 33: if ty == 'R' and len(got) != 33:
raise QRDecodeExplained("Truncated KT RX") raise QRDecodeExplained("Truncated KT RX")
@ -190,12 +196,7 @@ def decode_short_text(got):
# - if bad checksum on bitcoin addr, we treat as text... since might be # - if bad checksum on bitcoin addr, we treat as text... since might be
# return: what-it-is, (tuple) # return: what-it-is, (tuple)
if not isinstance(got, str): got = decode_qr_text(got)
# decode utf-8
try:
got = got.decode()
except UnicodeError:
raise QRDecodeExplained('UTF-8 decode failed')
# might be a PSBT? # might be a PSBT?
if len(got) > 100: if len(got) > 100:
@ -230,10 +231,11 @@ def decode_short_text(got):
cc_ms_pat = r"[0-9a-fA-F]+\s*:\s*[xtyYzZuUvV]pub[1-9A-HJ-NP-Za-km-z]+" cc_ms_pat = r"[0-9a-fA-F]+\s*:\s*[xtyYzZuUvV]pub[1-9A-HJ-NP-Za-km-z]+"
rgx = ure.compile(cc_ms_pat) rgx = ure.compile(cc_ms_pat)
# go line by line and match above, once 2 matches observed - considered multisig # go line by line and match above, once 2 matches observed - considered multisig
# important to not use ure.search for big strings (can run out of stack) # important to not use ure.search for big strings (can run out of stack);
# a real line here is a "<8-hex xfp>: <xpub>" key (~121 chars)
c = 0 # match count c = 0 # match count
for l in got.split("\n"): for l in got.split("\n"):
if rgx.search(l): if len(l) <= 150 and rgx.search(l):
c += 1 c += 1
if c > 1: if c > 1:
return 'multi', (got,) return 'multi', (got,)

View File

@ -2,9 +2,8 @@
# #
# display.py - OLED rendering # display.py - OLED rendering
# #
import machine, uzlib, ckcc, utime import machine, uzlib, ckcc, utime, version
from ssd1306 import SSD1306_SPI from ssd1306 import SSD1306_SPI
from version import is_devmode
import framebuf import framebuf
from graphics_mk4 import Graphics from graphics_mk4 import Graphics
from charcodes import OUT_CTRL_TITLE, OUT_CTRL_ADDRESS from charcodes import OUT_CTRL_TITLE, OUT_CTRL_ADDRESS
@ -35,11 +34,14 @@ class Display:
dc_pin = Pin('PA8', Pin.OUT) dc_pin = Pin('PA8', Pin.OUT)
cs_pin = Pin('PA4', Pin.OUT) cs_pin = Pin('PA4', Pin.OUT)
try: if version.mk_num == 5:
self.dis = SSD1306_SPI(128, 64, spi, dc_pin, reset_pin, cs_pin) # Early revs (A-D) needed this pin asserted to enable +12v to OLED
except OSError: # - removed in rev E and later boards, but keep here for dev boards
print("OLED unplugged?") # - remove this in 2027
raise vcc_en = Pin('V12EN', Pin.OUT) # aka PC1
vcc_en(1)
self.dis = SSD1306_SPI(128, 64, spi, dc_pin, reset_pin, cs_pin, is_mk5=(version.mk_num==5))
self.last_bar_update = 0 self.last_bar_update = 0
self.clear() self.clear()
@ -142,7 +144,7 @@ class Display:
self.icon(128-3, 1, 'scroll') self.icon(128-3, 1, 'scroll')
self.dis.fill_rect(128-2, pos, 1, bh, 1) self.dis.fill_rect(128-2, pos, 1, bh, 1)
if is_devmode and not ckcc.is_simulator(): if version.is_devmode and not ckcc.is_simulator():
self.dis.fill_rect(128-6, 20, 5, 21, 1) self.dis.fill_rect(128-6, 20, 5, 21, 1)
self.text(-2, 21, 'D', font=FontTiny, invert=1) self.text(-2, 21, 'D', font=FontTiny, invert=1)
self.text(-2, 28, 'E', font=FontTiny, invert=1) self.text(-2, 28, 'E', font=FontTiny, invert=1)
@ -150,11 +152,14 @@ class Display:
def fullscreen(self, msg, percent=None, line2=None): def fullscreen(self, msg, percent=None, line2=None):
# show a simple message "fullscreen". # show a simple message "fullscreen".
# - 'line2' not supported on smaller screen sizes, ignore
self.clear() self.clear()
y = 14 y = 14
self.text(None, y, msg, font=FontLarge) self.text(None, y, msg, font=FontLarge)
if line2:
# 21 + 6 ie. FontLarge.height of above text + FontTiny.height as space between
self.text(None, y + 27, line2, font=FontSmall)
if percent is not None: if percent is not None:
self.progress_bar(percent) self.progress_bar(percent)
self.show() self.show()
@ -201,61 +206,20 @@ class Display:
def busy_bar(self, enable): def busy_bar(self, enable):
# Render a continuous activity (not progress) bar in lower 8 lines of display # Render a continuous activity (not progress) bar in lower 8 lines of display
# - using OLED itself to do the animation, so smooth and CPU free #
# - cannot preserve bottom 8 lines, since we have to destructively write there
# - assumes normal horz addr mode: 0x20, 0x00
# - speed_code=>framedelay: 0=5fr, 1=64fr, 2=128, 3=256, 4=3, 5=4, 6=25, 7=2frames
# unused: assert 0 <= speed_code <= 7
setup = bytes([
0x21, 0x00, 0x7f, # setup column address range (start, end): 0-127
0x22, 7, 7, # setup page start/end address: page 7=last 8 lines
])
animate = bytes([
0x2e, # stop animations in progress
0x26, # scroll leftwards (stock ticker mode)
0, # placeholder
7, # start 'page' (vertical)
5, # "speed_code" # scroll speed: 7=fastest, but no order to it
7, # end 'page'
0, 0xff, # placeholders
0x2f # start
])
cleanup = bytes([
0x2e, # stop animation
0x20, 0x00, # horz addr-ing mode
0x21, 0x00, 0x7f, # setup column address range (start, end): 0-127
0x22, 7, 7, # setup page start/end address: page 7=last 8 lines
])
if not enable: if not enable:
# stop animation, and redraw old (new) screen self.dis.busy_bar(False, None)
self.write_cmds(cleanup)
self.show() self.show()
else: else:
# Need a pattern that repeats nicely mod 128
# a pattern that repeats nicely mod 128
# - each byte here is a vertical column, 8 pixels tall, MSB at bottom # - each byte here is a vertical column, 8 pixels tall, MSB at bottom
data = bytes(0x80 if (x%4)<2 else 0x0 for x in range(128)) pat = bytes(0x80 if (x%4)<2 else 0x0 for x in range(128))
if ckcc.is_simulator(): self.dis.busy_bar(True, pat)
# just show as static pattern
t = self.dis.buffer[:-128] + data
self.dis.write_data(t)
else:
self.write_cmds(setup)
self.dis.write_data(data)
self.write_cmds(animate)
def write_cmds(self, cmds):
for c in cmds:
self.dis.write_cmd(c)
def set_brightness(self, val): def set_brightness(self, val):
# normal = 0x7f, brightness=0xff, dim=0x00 (but they are all very similar) # normal = 0x7f, brightness=0xff, dim=0x00 (but they are all very similar)
self.dis.write_cmd(0x81) # Set Contrast Control return self.dis.contrast(val)
self.dis.write_cmd(val)
def menu_draw(self, ry, msg, is_sel, is_checked, space_indicators): def menu_draw(self, ry, msg, is_sel, is_checked, space_indicators):
# draw a menu item, perhaps selected, checked. # draw a menu item, perhaps selected, checked.
@ -266,17 +230,18 @@ class Display:
if is_sel: if is_sel:
self.dis.fill_rect(0, y, Display.WIDTH, h-1, 1) self.dis.fill_rect(0, y, Display.WIDTH, h-1, 1)
self.icon(2, y, 'wedge', invert=1) self.icon(2, y, 'wedge', invert=1)
self.text(x, y, msg, invert=1) nx = self.text(x, y, msg, invert=1)
else: else:
self.text(x, y, msg) nx = self.text(x, y, msg)
# LATER: removed because caused confusion w/ underscore # LATER: removed because caused confusion w/ underscore
#if msg[0] == ' ' and space_indicators: #if msg[0] == ' ' and space_indicators:
# see also graphics/mono/space.txt # see also graphics/mono/space.txt
#self.icon(x-2, y+9, 'space', invert=is_sel) #self.icon(x-2, y+9, 'space', invert=is_sel)
if is_checked: if is_checked and nx <= 113:
self.icon(108, y, 'selected', invert=is_sel) # omit checkmark if it doesn't fit
self.icon(113, y, 'selected', invert=is_sel)
def menu_show(self, *a): def menu_show(self, *a):
self.show() self.show()
@ -328,15 +293,25 @@ class Display:
# no status bar on Mk4 # no status bar on Mk4
return return
def draw_qr_error(self, idx_hint, msg):
self.clear()
lm = 4
bw = 54
y = (self.HEIGHT - bw) // 2
# empty rectangle
self.dis.fill_rect(lm, y, bw, bw, 1)
self.dis.fill_rect(lm+1, y+1, bw-2, bw-2, 0)
# error in rectangle - handpicked position
self.text(lm+5,y+10, "QR too")
self.text(lm+16,y+24, "big")
self._draw_qr_display(bw, lm, msg, False, None, idx_hint, False)
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert, def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert,
is_addr=False, force_msg=False, is_change=False): is_addr=False, force_msg=False, side_msg=None):
# 'sidebar' is a pre-formated obj to show to right of QR -- oled life # 'sidebar' is a pre-formated obj to show to right of QR -- oled life
# - 'msg' will appear to right if very short, else under in tiny # - 'msg' will appear to right if very short, else under in tiny
# - ignores "is_addr" because exactly zero space to do anything special # - ignores "is_addr" because exactly zero space to do anything special
from utils import word_wrap
self.clear() self.clear()
w = qr_data.width() w = qr_data.width()
if w <= 29: if w <= 29:
# version 1,2,3 => we can double-up the pixels # version 1,2,3 => we can double-up the pixels
@ -376,12 +351,19 @@ class Display:
gly = framebuf.FrameBuffer(bytearray(packed), w, w, framebuf.MONO_HLSB) gly = framebuf.FrameBuffer(bytearray(packed), w, w, framebuf.MONO_HLSB)
self.dis.blit(gly, XO, YO, 1) self.dis.blit(gly, XO, YO, 1)
self._draw_qr_display(bw, lm, msg, is_alnum, sidebar, idx_hint, invert, is_addr, side_msg)
def _draw_qr_display(self, bw, lm, msg, is_alnum, sidebar, idx_hint, invert,
is_addr=False, side_msg=None):
# does not draw actual QR, but all other things in the screen
from utils import word_wrap
if not sidebar and not msg: if not sidebar and not msg:
pass pass
elif not sidebar and ((len(msg) > (5*7)) or is_change): elif not sidebar and ((len(msg) > (5*7)) or side_msg):
# use FontTiny and word wrap (will just split if no spaces) # use FontTiny and word wrap (will just split if no spaces)
# native segwit addresses and taproot # native segwit addresses and taproot
# if is_change=True also p2pkh and p2sh fall into this category as space is needed for "CHANGE" # if 'side_msg' also p2pkh and p2sh fall into this category as space is needed for "CHANGE BACK" text
x = bw + lm + 4 x = bw + lm + 4
ww = ((128 - x)//4) - 1 # char width avail ww = ((128 - x)//4) - 1 # char width avail
y = 1 y = 1
@ -397,8 +379,11 @@ class Display:
self.text(x, y, line, FontTiny) self.text(x, y, line, FontTiny)
y += 8 y += 8
if is_addr and is_change: if side_msg and (len(side_msg) < 15):
self.text(x+4, y+8, "CHANGE BACK", FontTiny) y_pos = y + 8
# only render if there is space
if (self.HEIGHT - y_pos) >= FontTiny.height:
self.text(x+4, y+8, side_msg, FontTiny)
else: else:
# hand-positioned for known cases # hand-positioned for known cases
# - sidebar = (text, #of char per line) # - sidebar = (text, #of char per line)

View File

@ -12,7 +12,7 @@ from menu import MenuItem, MenuSystem
from ubinascii import hexlify as b2a_hex from ubinascii import hexlify as b2a_hex
from ubinascii import b2a_base64 from ubinascii import b2a_base64
from msgsign import write_sig_file from msgsign import write_sig_file
from utils import xfp2str, swab32 from utils import xfp2str, swab32, node_from_privkey
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
BIP85_PWD_LEN = 21 BIP85_PWD_LEN = 21
@ -124,8 +124,7 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
msg = "Password Index?" if picked == 7 else "Index Number?" msg = "Password Index?" if picked == 7 else "Index Number?"
index = await ux_enter_bip32_index(msg, unlimited=settings.get("b85max", False)) index = await ux_enter_bip32_index(msg, unlimited=settings.get("b85max", False))
if index is None: if index is None: return
return
dis.fullscreen("Working...") dis.fullscreen("Working...")
new_secret, width, s_mode, path = bip85_derive(picked, index) new_secret, width, s_mode, path = bip85_derive(picked, index)
@ -180,7 +179,7 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
elif s_mode == 'xprv': elif s_mode == 'xprv':
# Raw XPRV value. # Raw XPRV value.
ch, pk = new_secret[0:32], new_secret[32:64] ch, pk = new_secret[0:32], new_secret[32:64]
master_node = ngu.hdnode.HDNode().from_chaincode_privkey(ch, pk) master_node = node_from_privkey(pk, ch)
node = master_node node = master_node
encoded = stash.SecretStash.encode(xprv=master_node) encoded = stash.SecretStash.encode(xprv=master_node)
@ -205,14 +204,12 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
if new_secret: if new_secret:
msg += '\n\nRaw Entropy:\n' + str(b2a_hex(new_secret), 'ascii') msg += '\n\nRaw Entropy:\n' + str(b2a_hex(new_secret), 'ascii')
# Add the standard export prompt at the end, with extra (5) option sometimes. key6 = 'to type %s over USB' % s_mode
key0 = None key0 = None
if encoded is not None: if encoded is not None:
key0 = 'to switch to derived secret' key0 = 'to switch to derived secret'
elif s_mode == 'pw':
key0 = 'to type password over USB' prompt, escape = export_prompt_builder('data', key0=key0, key6=key6,
prompt, escape = export_prompt_builder('data', key0=key0,
no_qr=(not qr), force_prompt=True) no_qr=(not qr), force_prompt=True)
title = None title = None
if node: if node:
@ -224,7 +221,9 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
ch = await ux_show_story(msg+'\n\n'+prompt, title=title, escape=escape, ch = await ux_show_story(msg+'\n\n'+prompt, title=title, escape=escape,
strict_escape=True, sensitive=True) strict_escape=True, sensitive=True)
choice = import_export_prompt_decode(ch) choice = import_export_prompt_decode(ch)
if isinstance(choice, dict): if choice == KEY_CANCEL:
break
elif isinstance(choice, dict):
# write to SD card or Virtual Disk: simple text file # write to SD card or Virtual Disk: simple text file
dis.fullscreen("Saving...") dis.fullscreen("Saving...")
try: try:
@ -241,33 +240,33 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
await needs_microsd() await needs_microsd()
continue continue
except Exception as e: except Exception as e:
await ux_show_story('Failed to write!\n\n\n'+str(e)) await ux_show_story('Failed to write!\n\n'+str(e))
continue continue
story = "Filename is:\n\n%s" % out_fn story = "Filename is:\n\n%s" % out_fn
story += "\n\nSignature filename is:\n\n%s" % sig_nice story += "\n\nSignature filename is:\n\n%s" % sig_nice
await ux_show_story(story, title='Saved') await ux_show_story(story, title='Saved')
elif choice == KEY_CANCEL:
break
elif choice == KEY_QR: elif choice == KEY_QR:
from ux import show_qr_code from ux import show_qr_code
await show_qr_code(qr, qr_alnum, is_secret=True) await show_qr_code(qr, qr_alnum, is_secret=True)
elif choice == '0':
if s_mode == 'pw': elif (choice == '0') and (encoded is not None):
# gets confirmation then types it # switch over to new secret!
await single_send_keystrokes(qr, path) dis.fullscreen("Applying...")
elif encoded is not None: from actions import goto_top_menu
# switch over to new secret! from glob import settings
dis.fullscreen("Applying...") xfp_str = xfp2str(settings.get("xfp", 0))
from actions import goto_top_menu await seed.set_ephemeral_seed(
from glob import settings encoded,
xfp_str = xfp2str(settings.get("xfp", 0)) origin='BIP85 Derived from [%s], index=%d' % (xfp_str, index)
await seed.set_ephemeral_seed( )
encoded, goto_top_menu()
origin='BIP85 Derived from [%s], index=%d' % (xfp_str, index) break
)
goto_top_menu() elif choice == "6":
break # gets confirmation then types it
await single_send_keystrokes(qr, path)
elif NFC and choice == KEY_NFC: elif NFC and choice == KEY_NFC:
# Share any of these over NFC # Share any of these over NFC
@ -292,7 +291,7 @@ async def password_entry(*args, **kwargs):
while True: while True:
the_ux.pop() the_ux.pop()
index = await ux_enter_bip32_index("Password Index?", can_cancel=True) index = await ux_enter_bip32_index("Password Index?")
if index is None: if index is None:
break break

View File

@ -19,10 +19,10 @@ class CCBusyError(RuntimeError):
# HSM is blocking your action # HSM is blocking your action
class HSMDenied(RuntimeError): class HSMDenied(RuntimeError):
pass pass
class HSMCMDDisabled(RuntimeError): class HSMCMDDisabled(RuntimeError):
pass pass
# PSBT / transaction related # PSBT / transaction related
class FatalPSBTIssue(RuntimeError): class FatalPSBTIssue(RuntimeError):
pass pass
@ -51,8 +51,12 @@ class QRDecodeExplained(ValueError):
class UnknownAddressExplained(ValueError): class UnknownAddressExplained(ValueError):
pass pass
# We're not going to co-sign using CCC feature # We're not going to (co-)sign using spending policy features
class CCCPolicyViolationError(RuntimeError): class SpendPolicyViolation(RuntimeError):
pass
# data too big for simple QR
class QRTooBigError(ValueError):
pass pass
# EOF # EOF

View File

@ -12,6 +12,7 @@ from msgsign import write_sig_file
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH
from charcodes import KEY_NFC, KEY_CANCEL, KEY_QR from charcodes import KEY_NFC, KEY_CANCEL, KEY_QR
from ownership import OWNERSHIP from ownership import OWNERSHIP
from exceptions import QRTooBigError
async def export_by_qr(body, label, type_code, force_bbqr=False): async def export_by_qr(body, label, type_code, force_bbqr=False):
# render as QR and show on-screen # render as QR and show on-screen
@ -19,10 +20,10 @@ async def export_by_qr(body, label, type_code, force_bbqr=False):
try: try:
if force_bbqr or len(body) > 2000: if force_bbqr or len(body) > 2000:
raise ValueError raise QRTooBigError
await show_qr_code(body) await show_qr_code(body)
except (ValueError, RuntimeError, TypeError): except QRTooBigError:
if version.has_qwerty: if version.has_qwerty:
# do BBQr on Q # do BBQr on Q
from ux_q1 import show_bbqr_codes from ux_q1 import show_bbqr_codes
@ -34,7 +35,8 @@ async def export_by_qr(body, label, type_code, force_bbqr=False):
async def export_contents(title, contents, fname_pattern, derive=None, addr_fmt=None, async def export_contents(title, contents, fname_pattern, derive=None, addr_fmt=None,
is_json=False, force_bbqr=False, force_prompt=False, direct_way=None): is_json=False, force_bbqr=False, force_prompt=False, direct_way=None,
intro="", footer="", ux_title=None):
# export text and json files while offering NFC, QR & Vdisk # export text and json files while offering NFC, QR & Vdisk
# produces signed export in case of SD/Vdisk (signed with key at deriv and addr_fmt) # produces signed export in case of SD/Vdisk (signed with key at deriv and addr_fmt)
# checks if suitable to offer QR export on Mk4 # checks if suitable to offer QR export on Mk4
@ -58,8 +60,8 @@ async def export_contents(title, contents, fname_pattern, derive=None, addr_fmt=
ch = direct_way # set it to direct way only once, outside the loop ch = direct_way # set it to direct way only once, outside the loop
while True: while True:
if direct_way is None: if direct_way is None:
ch = await import_export_prompt("%s file" % title, ch = await import_export_prompt("%s file" % title, intro=intro, footnotes=footer,
force_prompt=force_prompt, no_qr=no_qr) force_prompt=force_prompt, no_qr=no_qr, title=ux_title)
if ch == KEY_CANCEL: if ch == KEY_CANCEL:
break break
elif ch == KEY_QR: elif ch == KEY_QR:
@ -94,7 +96,7 @@ async def export_contents(title, contents, fname_pattern, derive=None, addr_fmt=
except CardMissingError: except CardMissingError:
await needs_microsd() await needs_microsd()
except Exception as e: except Exception as e:
await ux_show_story('Failed to write!\n\n\n' + str(e)) await ux_show_story('Failed to write!\n\n' + str(e))
# both exceptions & success gets here # both exceptions & success gets here
if no_qr and (NFC is None) and (VD is None) and not force_prompt: if no_qr and (NFC is None) and (VD is None) and not force_prompt:
@ -423,14 +425,14 @@ def generate_generic_export(account_num=0):
def generate_electrum_wallet(addr_type, account_num): def generate_electrum_wallet(addr_type, account_num):
# Generate line-by-line JSON details about wallet. # Generate line-by-line JSON details about wallet.
# #
# Much reverse enginerring of Electrum here. It's a complex # Much reverse engineering of Electrum here. It's a complex
# legacy file format. # legacy file format.
chain = chains.current_chain() chain = chains.current_chain()
xfp = settings.get('xfp') xfp = settings.get('xfp')
# Must get the derivation path, and the SLIP32 version bytes right! # Must get the derivation path, and the SLIP132 version bytes right!
mode = chains.af_to_bip44_purpose(addr_type) mode = chains.af_to_bip44_purpose(addr_type)
OWNERSHIP.note_wallet_used(addr_type, account_num) OWNERSHIP.note_wallet_used(addr_type, account_num)
@ -470,7 +472,7 @@ async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int
dis.fullscreen('Generating...') dis.fullscreen('Generating...')
chain = chains.current_chain() chain = chains.current_chain()
xfp = settings.get('xfp') xfp = settings.get('xfp', 0)
dis.progress_bar_show(0.1) dis.progress_bar_show(0.1)
if mode is None: if mode is None:
mode = chains.af_to_bip44_purpose(addr_type) mode = chains.af_to_bip44_purpose(addr_type)
@ -499,8 +501,31 @@ async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int
) )
dis.progress_bar_show(1) dis.progress_bar_show(1)
await export_contents("Descriptor", body, fname_pattern, derive + "/0/0",
addr_type, force_prompt=True, direct_way=direct_way) intro, footer = (body, "") if version.has_qwerty else ("", body)
title = "Descriptor"
await export_contents(title, body, fname_pattern, derive + "/0/0", addr_type,
force_prompt=True, direct_way=direct_way, intro=intro, footer=footer,
ux_title=title if version.has_qwerty else None)
async def make_key_expression_export(orig_der, addr_fmt=AF_CLASSIC, fname_pattern="key_expr.txt"):
from glob import dis
dis.fullscreen('Generating...')
xfp = xfp2str(settings.get('xfp', 0)).lower()
with stash.SensitiveValues() as sv:
ek = chains.current_chain().serialize_public(sv.derive_path(orig_der))
body = "[%s/%s]%s" % (xfp, orig_der.replace("m/", ""), ek)
intro, footer = (body, "") if version.has_qwerty else ("", body)
title = "Key Expression"
await export_contents(title, body, fname_pattern, orig_der + "/0/0", addr_fmt,
force_prompt=True, intro=intro, footer=footer,
ux_title=title if version.has_qwerty else None)
# EOF # EOF

View File

@ -19,7 +19,8 @@ from countdowns import countdown_chooser
from paper import make_paper_wallet from paper import make_paper_wallet
from trick_pins import TrickPinMenu from trick_pins import TrickPinMenu
from tapsigner import import_tapsigner_backup_file from tapsigner import import_tapsigner_backup_file
from ccc import toggle_ccc_feature from ccc import toggle_ccc_feature, sssp_spending_policy, sssp_feature_menu
from wif import WIFStoreMenu
# useful shortcut keys # useful shortcut keys
from charcodes import KEY_QR, KEY_NFC from charcodes import KEY_QR, KEY_NFC
@ -100,6 +101,33 @@ def hsm_available():
# contains hsm feature + can it be used (needs se2 secret and no tmp active) # contains hsm feature + can it be used (needs se2 secret and no tmp active)
return version.supports_hsm and has_real_secret() return version.supports_hsm and has_real_secret()
def qr_and_ms():
# has QR scanner, and at least one MS wallet
if not version.has_qr: return False
return bool(settings.get('multisig', False))
def has_pushtx_url():
# they want to use PushTX feature
return bool(settings.get("ptxurl", False))
# Spending Policy (Hobbled mode) predicates.
#
def is_hobble_testdrive():
from pincodes import pa
return (pa.hobbled_mode == 2)
def sssp_related_keys():
return sssp_spending_policy('okeys')
def sssp_allow_passphrase():
return word_based_seed() and sssp_related_keys()
def sssp_allow_notes():
return settings.get("secnap", False) and sssp_spending_policy('notes')
def sssp_allow_vault():
return settings.master_get('seedvault') and sssp_related_keys()
async def goto_home(*a): async def goto_home(*a):
goto_top_menu() goto_top_menu()
@ -137,6 +165,21 @@ LoginPrefsMenu = [
MenuItem('Test Login Now', f=login_now, arg=1), MenuItem('Test Login Now', f=login_now, arg=1),
] ]
# obscure settings, not more dangerous, just more personal
BuriedSettingsMenu = [
ToggleMenuItem('Home Menu XFP', 'hmx', ['Only Tmp', 'Always Show'],
story=('Forces display of XFP (seed fingerprint) '
'at top of main menu. Normally, XFP is shown only when '
'temporary seed is active.\n\n'
'Master seed is displayed as <XFP>, temporary seeds as [XFP].'),
predicate=has_real_secret,
on_change=goto_home),
ToggleMenuItem('Menu Wrapping', 'wa', ['Default', 'Always Wrap'],
story='''When enabled, allows scrolling past menu top/bottom \
(wrap around). By default, this only happens in menus whose length is greater than 10.'''),
]
SettingsMenu = [ SettingsMenu = [
# xxxxxxxxxxxxxxxx # xxxxxxxxxxxxxxxx
MenuItem('Login Settings', menu=LoginPrefsMenu), MenuItem('Login Settings', menu=LoginPrefsMenu),
@ -159,22 +202,13 @@ The signed transaction will be named <TXID>.txn, so the file name does not leak
MS-DOS tools should not be able to find the PSBT data (ie. undelete), but forensic tools \ 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 \ which take apart the flash chips of the SDCard may still be able to find the \
data or filenames.'''), data or filenames.'''),
ToggleMenuItem('Menu Wrapping', 'wa', ['Default Off', 'Enable'],
story='''When enabled, allows scrolling past menu top/bottom \
(wrap around). By default, this only happens in very large menus.'''),
ToggleMenuItem('Home Menu XFP', 'hmx', ['Only Tmp', 'Always Show'],
story=('Forces display of XFP (seed fingerprint) '
'at top of main menu. Normally, XFP is shown only when '
'temporary seed is active.\n\n'
'Master seed is displayed as <XFP>, temporary seeds as [XFP].'),
predicate=has_real_secret,
on_change=goto_home),
ToggleMenuItem('Keyboard EMU', 'emu', ['Default Off', 'Enable'], ToggleMenuItem('Keyboard EMU', 'emu', ['Default Off', 'Enable'],
on_change=usb_keyboard_emulation, on_change=usb_keyboard_emulation,
predicate=has_secrets, # cannot generate BIP85 passwords without secret predicate=has_secrets, # cannot generate BIP85 passwords without secret
story='''This mode adds a top-level menu item for typing \ story='''This mode adds a top-level menu item for typing \
deterministically-generated passwords (BIP-85), directly into an \ deterministically-generated passwords (BIP-85), directly into an \
attached USB computer (as an emulated keyboard).'''), attached USB computer (as an emulated keyboard).'''),
MenuItem('Buried Settings', menu=BuriedSettingsMenu),
] ]
XpubExportMenu = [ XpubExportMenu = [
@ -194,20 +228,22 @@ WalletExportMenu = [
MenuItem("Nunchuk", f=named_generic_skeleton, arg="Nunchuk"), MenuItem("Nunchuk", f=named_generic_skeleton, arg="Nunchuk"),
MenuItem("Bull Bitcoin", f=ss_descriptor_skeleton, MenuItem("Bull Bitcoin", f=ss_descriptor_skeleton,
arg=(True, [AF_P2WPKH], "", "bull-bitcoin.txt", KEY_QR)), arg=(True, [AF_P2WPKH], "", "bull-bitcoin.txt", KEY_QR)),
MenuItem("Zeus", f=ss_descriptor_skeleton, MenuItem("Blue Wallet", f=electrum_skeleton, arg="Blue"),
arg=(True, [AF_P2WPKH, AF_P2WPKH_P2SH], "Zeus Wallet", "zeus-export.txt", None)), MenuItem("Electrum Wallet", f=electrum_skeleton, arg="Electrum"),
MenuItem("Electrum Wallet", f=electrum_skeleton),
MenuItem("Wasabi Wallet", f=wasabi_skeleton), MenuItem("Wasabi Wallet", f=wasabi_skeleton),
MenuItem("Fully Noded", f=named_generic_skeleton, arg="Fully Noded"), MenuItem("Fully Noded", f=named_generic_skeleton, arg="Fully Noded"),
MenuItem("Unchained", f=unchained_capital_export), MenuItem("Unchained", f=unchained_capital_export),
MenuItem("Theya", f=named_generic_skeleton, arg="Theya"), MenuItem("Theya", f=named_generic_skeleton, arg="Theya"),
MenuItem("Bitcoin Safe", f=named_generic_skeleton, arg="Bitcoin Safe"), MenuItem("Bitcoin Safe", f=named_generic_skeleton, arg="Bitcoin Safe"),
MenuItem("Zeus", f=ss_descriptor_skeleton,
arg=(True, [AF_P2WPKH, AF_P2WPKH_P2SH], "Zeus Wallet", "zeus-export.txt", None)),
MenuItem("Samourai Postmix", f=samourai_post_mix_descriptor_export), MenuItem("Samourai Postmix", f=samourai_post_mix_descriptor_export),
MenuItem("Samourai Premix", f=samourai_pre_mix_descriptor_export), MenuItem("Samourai Premix", f=samourai_pre_mix_descriptor_export),
# MenuItem("Samourai BadBank", f=samourai_bad_bank_descriptor_export), # not released yet # MenuItem("Samourai BadBank", f=samourai_bad_bank_descriptor_export), # not released yet
MenuItem("Descriptor", f=ss_descriptor_skeleton), MenuItem("Descriptor", f=ss_descriptor_skeleton),
MenuItem("Generic JSON", f=generic_skeleton), MenuItem("Generic JSON", f=generic_skeleton),
MenuItem("Export XPUB", menu=XpubExportMenu), MenuItem("Export XPUB", menu=XpubExportMenu),
MenuItem("Key Expression", f=key_expression_skeleton),
MenuItem("Dump Summary", predicate=has_secrets, f=dump_summary), MenuItem("Dump Summary", predicate=has_secrets, f=dump_summary),
] ]
@ -271,6 +307,8 @@ DebugFunctionsMenu = [
# xxxxxxxxxxxxxxxx # xxxxxxxxxxxxxxxx
MenuItem("Keyboard Test", f=keyboard_test), MenuItem("Keyboard Test", f=keyboard_test),
MenuItem('BBQr Demo', f=debug_bbqr_test, predicate=version.has_qwerty), MenuItem('BBQr Demo', f=debug_bbqr_test, predicate=version.has_qwerty),
MenuItem("NFC Test", f=quick_nfc_test),
MenuItem('Clear Tested', f=clear_tested_flag),
MenuItem('Debug: assert', f=debug_assert), MenuItem('Debug: assert', f=debug_assert),
MenuItem('Debug: except', f=debug_except), MenuItem('Debug: except', f=debug_except),
MenuItem('Check: BL FW', f=check_firewall_read), MenuItem('Check: BL FW', f=check_firewall_read),
@ -337,6 +375,7 @@ correctly- crafted transactions signed on Testnet could be broadcast on Mainnet.
MenuItem('MCU Key Slots', f=show_mcu_keys_left), MenuItem('MCU Key Slots', f=show_mcu_keys_left),
MenuItem('Bless Firmware', f=bless_flash), # no need for this anymore? MenuItem('Bless Firmware', f=bless_flash), # no need for this anymore?
MenuItem("Wipe LFS", f=wipe_filesystem), # kills other-seed settings, HSM stuff, addr cache MenuItem("Wipe LFS", f=wipe_filesystem), # kills other-seed settings, HSM stuff, addr cache
MenuItem("Nuke Device", f=nuke_device),
] ]
BackupStuffMenu = [ BackupStuffMenu = [
@ -355,7 +394,20 @@ NFCToolsMenu = [
MenuItem('Verify Address', f=nfc_address_verify), MenuItem('Verify Address', f=nfc_address_verify),
MenuItem('File Share', f=nfc_share_file), MenuItem('File Share', f=nfc_share_file),
MenuItem('Import Multisig', f=import_multisig_nfc), MenuItem('Import Multisig', f=import_multisig_nfc),
MenuItem('Push Transaction', f=nfc_pushtx_file, predicate=lambda: settings.get("ptxurl", False)), MenuItem('Push Transaction', f=nfc_pushtx_file, predicate=has_pushtx_url),
]
SpendingPolicySubMenu = [
NonDefaultMenuItem('Single-Signer', 'sssp', f=sssp_feature_menu, predicate=has_real_secret),
NonDefaultMenuItem('Co-Sign Multi.' if not version.has_qwerty else 'Co-Sign Multisig (CCC)',
'ccc', f=toggle_ccc_feature, predicate=is_not_tmp),
ToggleMenuItem('HSM Mode', 'hsmcmd', ['Default Off', 'Enable'],
story=("Enable HSM? Enables all user management commands, and other HSM-only USB commands. "
"By default these commands are disabled."),
predicate=hsm_available),
MenuItem('User Management', menu=make_users_menu,
predicate=lambda: hsm_available() and settings.get('hsmcmd', False)),
] ]
AdvancedNormalMenu = [ AdvancedNormalMenu = [
@ -371,14 +423,9 @@ AdvancedNormalMenu = [
MenuItem("View Identity", f=view_ident), MenuItem("View Identity", f=view_ident),
MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu), MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu),
MenuItem("Key Teleport (start)", f=kt_start_rx, predicate=version.has_qr), MenuItem("Key Teleport (start)", f=kt_start_rx, predicate=version.has_qr),
MenuItem("Spending Policy", menu=SpendingPolicySubMenu,shortcut='s',predicate=has_real_secret),
MenuItem('Paper Wallets', f=make_paper_wallet), MenuItem('Paper Wallets', f=make_paper_wallet),
ToggleMenuItem('Enable HSM', 'hsmcmd', ['Default Off', 'Enable'], MenuItem('WIF Store', menu=WIFStoreMenu.make),
story=("Enable HSM? Enables all user management commands, and other HSM-only USB commands. "
"By default these commands are disabled."),
predicate=hsm_available),
NonDefaultMenuItem('Coldcard Co-Signing', 'ccc', f=toggle_ccc_feature, predicate=is_not_tmp),
MenuItem('User Management', menu=make_users_menu,
predicate=hsm_available),
MenuItem('NFC Tools', predicate=nfc_enabled, menu=NFCToolsMenu, shortcut=KEY_NFC), MenuItem('NFC Tools', predicate=nfc_enabled, menu=NFCToolsMenu, shortcut=KEY_NFC),
MenuItem("Danger Zone", menu=DangerZoneMenu, shortcut='z'), MenuItem("Danger Zone", menu=DangerZoneMenu, shortcut='z'),
] ]
@ -441,7 +488,7 @@ NormalSystem = [
MenuItem("Address Explorer", menu=address_explore, shortcut='x'), MenuItem("Address Explorer", menu=address_explore, shortcut='x'),
MenuItem('Secure Notes & Passwords', menu=make_notes_menu, shortcut='n', MenuItem('Secure Notes & Passwords', menu=make_notes_menu, shortcut='n',
predicate=lambda: version.has_qwerty and settings.get("secnap", False)), predicate=lambda: version.has_qwerty and settings.get("secnap", False)),
MenuItem('Type Passwords', f=password_entry, shortcut='t', MenuItem('Type Passwords', f=password_entry, shortcut='e',
predicate=lambda: settings.get("emu", False) and has_secrets()), predicate=lambda: settings.get("emu", False) and has_secrets()),
MenuItem('Seed Vault', menu=make_seed_vault_menu, shortcut='v', MenuItem('Seed Vault', menu=make_seed_vault_menu, shortcut='v',
predicate=lambda: settings.master_get('seedvault') and has_secrets()), predicate=lambda: settings.master_get('seedvault') and has_secrets()),
@ -460,3 +507,72 @@ FactoryMenu = [
MenuItem("Debug Functions", menu=DebugFunctionsMenu, shortcut='f'), MenuItem("Debug Functions", menu=DebugFunctionsMenu, shortcut='f'),
MenuItem("Perform Selftest", f=start_selftest, shortcut='s'), MenuItem("Perform Selftest", f=start_selftest, shortcut='s'),
] ]
# Special menus for hobbled mode where we have a (single signer) spending policy in effect.
# - no access to secrets, backups, firmware up/downgrades.
# - secure notes, but readonly; can be disabled completely.
# - key teleport, but only for PSBT & multisig purposes.
# - can only be enabled after we have secrets, so no need for has_secrets tests here
#
# Slightly limited file menu when hobbled.
# - no backup/restore
HobbledFileMgmtMenu = [
# xxxxxxxxxxxxxxxx
MenuItem('Sign Text File', f=sign_message_on_sd),
MenuItem('Batch Sign PSBT', f=batch_sign),
MenuItem('List Files', f=list_files),
MenuItem('Export Wallet', menu=WalletExportMenu), # dup under Adv/Tools
MenuItem('Verify Sig File', f=verify_sig_file),
MenuItem('NFC File Share', predicate=nfc_enabled, f=nfc_share_file, shortcut=KEY_NFC),
MenuItem('BBQr File Share', predicate=version.has_qr, f=qr_share_file, arg=True),
MenuItem('QR File Share', predicate=version.has_qr, f=qr_share_file, shortcut=KEY_QR),
MenuItem('Format SD Card', f=wipe_sd_card),
MenuItem('Format RAM Disk', predicate=vdisk_enabled, f=wipe_vdisk),
]
# NFC tools when hobbled: not much different.
HobbledNFCToolsMenu = [
MenuItem('Sign PSBT', f=nfc_sign_psbt),
MenuItem('Show Address', f=nfc_show_address),
MenuItem('Sign Message', f=nfc_sign_msg),
MenuItem('Verify Sig File', f=nfc_sign_verify),
MenuItem('Verify Address', f=nfc_address_verify),
MenuItem('File Share', f=nfc_share_file),
MenuItem('Push Transaction', f=nfc_pushtx_file, predicate=has_pushtx_url),
]
# Very limited advanced menu when hobbled.
HobbledAdvancedMenu = [
# xxxxxxxxxxxxxxxx
MenuItem("File Management", menu=HobbledFileMgmtMenu),
MenuItem('Export Wallet', menu=WalletExportMenu, shortcut='x'), # also inside FileMgmt
MenuItem('Teleport Multisig PSBT', predicate=qr_and_ms, f=kt_send_file_psbt),
MenuItem("View Identity", f=view_ident),
MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu, predicate=sssp_related_keys),
MenuItem('Paper Wallets', f=make_paper_wallet),
MenuItem('NFC Tools', predicate=nfc_enabled, menu=HobbledNFCToolsMenu, shortcut=KEY_NFC),
MenuItem('WIF Store', menu=WIFStoreMenu.make, predicate=sssp_related_keys),
MenuItem('Show %s Version' % ("Firmware" if version.has_qwerty else "FW"), f=show_version),
MenuItem("Destroy Seed", f=clear_seed, predicate=has_real_secret),
]
# Main menu when a spending policy (hobbled) is in effect.
HobbledTopMenu = [
# xxxxxxxxxxxxxxxx
MenuItem('Ready To Sign', f=ready2sign, shortcut='r'),
MenuItem('Passphrase', menu=start_b39_pw, predicate=sssp_allow_passphrase, shortcut='p'),
MenuItem('Scan Any QR Code', predicate=version.has_qr, f=scan_any_qr, arg=(False, True),
shortcut=KEY_QR),
MenuItem("Address Explorer", menu=address_explore, shortcut='x'),
MenuItem('Secure Notes & Passwords', menu=make_notes_menu, predicate=sssp_allow_notes,
shortcut='n'),
MenuItem('Type Passwords', f=password_entry, shortcut='t',
predicate=lambda: settings.get("emu", False) and sssp_related_keys()),
MenuItem('Seed Vault', menu=make_seed_vault_menu, predicate=sssp_allow_vault,
shortcut='v'),
MenuItem('Advanced/Tools', menu=HobbledAdvancedMenu, shortcut='t'),
MenuItem('Secure Logout', f=logout_now, predicate=not version.has_battery),
MenuItem('EXIT TEST DRIVE', f=sssp_feature_menu, predicate=is_hobble_testdrive),
ShortcutItem(KEY_NFC, predicate=nfc_enabled, menu=HobbledNFCToolsMenu),
]

View File

@ -8,7 +8,6 @@
# #
import utime, struct import utime, struct
import uasyncio as asyncio import uasyncio as asyncio
from utils import B2A
from machine import Pin from machine import Pin
from ustruct import pack from ustruct import pack

View File

@ -4,9 +4,8 @@
# #
# Unattended signing of transactions and messages, subject to a set of rules. # Unattended signing of transactions and messages, subject to a set of rules.
# #
import stash, ustruct, chains, sys, gc, uio, ujson, uos, utime, ckcc, ngu, version import ustruct, chains, sys, gc, uio, ujson, uos, utime, ckcc, ngu
from sffile import SFFile from utils import problem_file_line, cleanup_deriv_path, match_deriv_path, keypath_to_str
from utils import problem_file_line, cleanup_deriv_path, match_deriv_path
from utils import cleanup_payment_address from utils import cleanup_payment_address
from pincodes import AE_LONG_SECRET_LEN from pincodes import AE_LONG_SECRET_LEN
from stash import blank_object from stash import blank_object
@ -606,7 +605,7 @@ class HSMPolicy:
fd.write('- XPUB values will be shared, if path matches: m OR %s.\n' fd.write('- XPUB values will be shared, if path matches: m OR %s.\n'
% plist(self.share_xpubs)) % plist(self.share_xpubs))
if self.share_addrs: if self.share_addrs:
fd.write('- Address values values will be shared, if path matches: %s.\n' fd.write('- Address values will be shared, if path matches: %s.\n'
% plist(self.share_addrs)) % plist(self.share_addrs))
if self.priv_over_ux: if self.priv_over_ux:
fd.write('- Status responses optimized for privacy.\n') fd.write('- Status responses optimized for privacy.\n')
@ -657,6 +656,15 @@ class HSMPolicy:
assert not glob.hsm_active assert not glob.hsm_active
glob.hsm_active = self glob.hsm_active = self
# HSM is the locked-down operating mode: shut down peripherals
# that enlarge the USB-stack interaction surface.
# - VDisk: MSC bulk OUT and HID OUT share the STM32 OTG_FS RX FIFO;
# under load this can wedge the HID OUT endpoint permanently
if glob.VD is not None:
glob.VD.shutdown()
if glob.NFC is not None:
glob.NFC.shutdown()
self.start_time = utime.ticks_ms() self.start_time = utime.ticks_ms()
if new_file: if new_file:
@ -874,9 +882,6 @@ class HSMPolicy:
# do this super early so always cleared even if other issues # do this super early so always cleared even if other issues
local_ok = self.consume_local_code(psbt_sha) local_ok = self.consume_local_code(psbt_sha)
if not self.rules:
raise ValueError("no txn signing allowed")
# reject anything with warning, probably # reject anything with warning, probably
if psbt.warnings: if psbt.warnings:
if self.warnings_ok: if self.warnings_ok:
@ -884,6 +889,32 @@ class HSMPolicy:
else: else:
raise ValueError("has %d warning(s)" % len(psbt.warnings)) raise ValueError("has %d warning(s)" % len(psbt.warnings))
if psbt.por322:
if not self.msg_paths:
raise ValueError("Message signing not permitted")
for inp in psbt.inputs:
if not inp.required_key:
continue
if inp.is_multisig:
paths = [
keypath_to_str(inp.subpaths[pk])
for pk in inp.required_key
if pk in inp.subpaths
]
else:
paths = [keypath_to_str(inp.subpaths[inp.required_key])]
if not any(match_deriv_path(self.msg_paths, p) for p in paths):
raise ValueError("Message signing not enabled for that path")
self.approve(log, "BIP-322 message signing allowed")
return 'y'
if not self.rules:
raise ValueError("no txn signing allowed")
# See who has entered creditials already (all must be valid). # See who has entered creditials already (all must be valid).
users = [] users = []
for u, (token, counter) in auth.items(): for u, (token, counter) in auth.items():

View File

@ -59,7 +59,7 @@ class ApproveHSMPolicy(UserAuthorizedAction):
msg = '''Last chance. You are defining a new policy which \ msg = '''Last chance. You are defining a new policy which \
allows the Coldcard to sign specific transactions without any further user approval.\n\n\ allows the Coldcard to sign specific transactions without any further user approval.\n\n\
Policy hash:\n%s\n\n Policy hash:\n%s\n\n
Press %s to save policy and enable HSM mode.''' % (self.policy.hash(), confirm_char) Press (%s) to save policy and enable HSM mode.''' % (self.policy.hash(), confirm_char)
ch = await ux_show_story(msg, title=self.title, ch = await ux_show_story(msg, title=self.title,
escape='x'+confirm_char, strict_escape=True) escape='x'+confirm_char, strict_escape=True)

View File

@ -58,8 +58,8 @@ class ImportantTask:
else: else:
# uncaught exception in an unnamed (and unimportant) task # uncaught exception in an unnamed (and unimportant) task
print("UNNAMED: " + context["message"]) print("UNNAMED: " + context["message"])
# sys.print_exception(context["exception"]) sys.print_exception(context["exception"]) # VERY USEFUL on sim
print("... future: %r" % context.get("future", '?')) #print("... future: %r" % context.get("future", '?'))
def start_task(self, name, awaitable): def start_task(self, name, awaitable):
# start a critical task and watch for it to never die # start a critical task and watch for it to never die

View File

@ -134,7 +134,7 @@ class FullKeyboard(NumpadBase):
if self._history[kn] == NUM_SAMPLES: if self._history[kn] == NUM_SAMPLES:
self.is_pressed[kn] = 1 self.is_pressed[kn] = 1
new_presses.add(kn) new_presses.add(kn)
elif self._history[i] == 0: elif self._history[kn] == 0:
self.is_pressed[kn] = 0 self.is_pressed[kn] = 0
self._history[kn] = 0 self._history[kn] = 0

View File

@ -656,8 +656,91 @@ class Display:
return prev_x return prev_x
@staticmethod
def handle_qr_msg(msg, max_lines=False):
if len(msg) <= CHARS_W:
parts = [msg]
elif ' ' not in msg and (len(msg) <= (CHARS_W * 2)):
# fits in two lines, but has no spaces
hh = len(msg) // 2
parts = [msg[0:hh], msg[hh:]]
else:
if not max_lines:
# do word wrap
parts = list(word_wrap(msg, CHARS_W))
else:
# 2 lines max
parts = [msg[:30] + "", "" + msg[-30:]]
return parts
def draw_qr_lines(self, lines, is_addr):
y = CHARS_H - len(lines)
prev_x = 0
for line in lines:
if not is_addr:
self.text(None, y, line)
else:
prev_x = self._draw_addr(y, line, prev_x=prev_x)
y += 1
def draw_qr_idx_hint(self, str_idx):
lh = len(str_idx)
assert lh <= 10
if lh > 5:
# needs 2 lines
self.text(-1, 0, str_idx[:5])
self.text(-1, 1, str_idx[5:])
else:
self.text(-1, 0, str_idx)
def draw_side_msg(self, msg, has_idx):
right_sub = 2 if has_idx else 0
start_right = right_msg = None
if len(msg) <= CHARS_H:
# we only need left side
start_left = CHARS_H - len(msg)
left_msg = msg
else:
split_msg = msg.split()
if len(split_msg) == 1 or len(split_msg) > 2:
return # not possible
left_msg, right_msg = split_msg
if len(left_msg) > CHARS_H:
return
if len(right_msg) > (CHARS_H - right_sub):
return
start_left = CHARS_H - len(left_msg)
start_right = CHARS_H - len(right_msg)
for i, c in enumerate(left_msg, start=start_left):
self.text(1, i, c)
if start_right:
for i, c in enumerate(right_msg, start=start_right):
self.text(-1, i, c)
def draw_qr_error(self, idx_hint, msg=None):
x = 85
y = 30
w = 150
self.clear()
self.dis.fill_rect(x, y, w, w, COL_TEXT)
self.dis.fill_rect(x + 1, y + 1, w - 2, w - 2) # Black
self.text(12, 3, "QR too big")
if msg:
lines = self.handle_qr_msg(msg, max_lines=True)
self.draw_qr_lines(lines, False)
if idx_hint:
self.draw_qr_idx_hint(idx_hint)
self.show()
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert, partial_bar=None, def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert, partial_bar=None,
is_addr=False, force_msg=False, is_change=False): is_addr=False, force_msg=False, side_msg=None):
# Show a QR code on screen w/ some text under it # Show a QR code on screen w/ some text under it
# - invert not supported on Q1 # - invert not supported on Q1
# - sidebar not supported here (see users.py) # - sidebar not supported here (see users.py)
@ -677,16 +760,7 @@ class Display:
# p2wsh address would need 3 lines to show, so we won't # p2wsh address would need 3 lines to show, so we won't
num_lines = 0 num_lines = 0
elif msg: elif msg:
if len(msg) <= CHARS_W: parts = self.handle_qr_msg(msg)
parts = [msg]
elif ' ' not in msg and (len(msg) <= CHARS_W*2):
# fits in two lines, but has no spaces
hh = len(msg) // 2
parts = [msg[0:hh], msg[hh:]]
else:
# do word wrap
parts = list(word_wrap(msg, CHARS_W))
num_lines = len(parts) num_lines = len(parts)
else: else:
num_lines = 0 num_lines = 0
@ -755,31 +829,13 @@ class Display:
if num_lines: if num_lines:
# centered text under that # centered text under that
y = CHARS_H - num_lines self.draw_qr_lines(parts, is_addr)
prev_x = 0
for line in parts:
if not is_addr:
self.text(None, y, line)
else:
prev_x = self._draw_addr(y, line, prev_x=prev_x)
y += 1
if idx_hint: if idx_hint:
lh = len(idx_hint) self.draw_qr_idx_hint(idx_hint)
assert lh <= 10
if lh > 5:
# needs 2 lines
self.text(-1, 0, idx_hint[:5])
self.text(-1, 1, idx_hint[5:])
else:
self.text(-1, 0, idx_hint)
if is_addr and is_change: if side_msg:
for i, c in enumerate("CHANGE", start=4): self.draw_side_msg(side_msg, idx_hint)
self.text(1, i, c)
for i, c in enumerate("BACK", start=6):
self.text(-1, i, c)
# pass a max brightness flag here, which will be cleared after next show # pass a max brightness flag here, which will be cleared after next show
self.show(max_bright=True) self.show(max_bright=True)

View File

@ -181,14 +181,22 @@ class LoginUX:
async def we_are_ewaste(self, num_fails): async def we_are_ewaste(self, num_fails):
msg = '''After %d failed PIN attempts this Coldcard is locked forever. \ msg = '''After %d failed PIN attempts this Coldcard is locked forever. \
By design, there is no way to reset or recover the secure element, and its contents \ By design, there is no way to reset or recover the secure element, and its contents \
are now forever inaccessible. are now forever inaccessible.\n\n''' % num_fails
Restore your seed words onto a new Coldcard.''' % num_fails if has_qwerty:
msg += 'Calculator mode starts now.'
else:
msg += 'Restore your seed words onto a new Coldcard.'
while 1: while 1:
ch = await ux_show_story(msg, title='I Am Brick!', escape='6') ch = await ux_show_story(msg, title='I Am Brick!', escape='6')
if ch == '6': break if ch == '6': break
if has_qwerty:
from calc import login_repl
await login_repl()
async def confirm_attempt(self, attempts_left, value): async def confirm_attempt(self, attempts_left, value):
ch = await ux_show_story('''You have %d attempts left before this Coldcard BRICKS \ ch = await ux_show_story('''You have %d attempts left before this Coldcard BRICKS \
@ -270,7 +278,7 @@ suffix break point is correct.\n\n'''
return await self.interact() return await self.interact()
async def get_new_pin(self, title, story=None): async def get_new_pin(self, title=None, story=None):
# Do UX flow to get new (or change) PIN. Always does the double-entry thing # Do UX flow to get new (or change) PIN. Always does the double-entry thing
self.is_setting = True self.is_setting = True

View File

@ -81,7 +81,6 @@ glob.settings = settings
async def more_setup(): async def more_setup():
# Boot up code; splash screen is being shown # Boot up code; splash screen is being shown
try: try:
from files import CardSlot from files import CardSlot
CardSlot.setup() CardSlot.setup()
@ -89,6 +88,10 @@ async def more_setup():
# This "pa" object holds some state shared w/ bootloader about the PIN # This "pa" object holds some state shared w/ bootloader about the PIN
try: try:
from pincodes import pa from pincodes import pa
# check for bricked system early
# bricked CC not going past this point
await pa.enforce_brick()
pa.setup(b'') # just to see where we stand. pa.setup(b'') # just to see where we stand.
is_blank = pa.is_blank() is_blank = pa.is_blank()
except RuntimeError as e: except RuntimeError as e:

View File

@ -6,6 +6,7 @@ freeze_as_mpy('', [
'address_explorer.py', 'address_explorer.py',
'auth.py', 'auth.py',
'backups.py', 'backups.py',
'block_height.py',
'callgate.py', 'callgate.py',
'ccc.py', 'ccc.py',
'chains.py', 'chains.py',
@ -13,8 +14,6 @@ freeze_as_mpy('', [
'compat7z.py', 'compat7z.py',
'countdowns.py', 'countdowns.py',
'descriptor.py', 'descriptor.py',
'dev_helper.py',
'display.py',
'drv_entro.py', 'drv_entro.py',
'exceptions.py', 'exceptions.py',
'export.py', 'export.py',
@ -48,7 +47,6 @@ freeze_as_mpy('', [
'selftest.py', 'selftest.py',
'serializations.py', 'serializations.py',
'sffile.py', 'sffile.py',
'ssd1306.py',
'stash.py', 'stash.py',
'tapsigner.py', 'tapsigner.py',
'trick_pins.py', 'trick_pins.py',
@ -59,6 +57,7 @@ freeze_as_mpy('', [
'version.py', 'version.py',
'wallet.py', 'wallet.py',
'web2fa.py', 'web2fa.py',
'wif.py',
'xor_seed.py' 'xor_seed.py'
], opt=0) ], opt=0)

View File

@ -1,5 +1,6 @@
# Mk4 only files; would not be needed on Mk3 or earlier. # Mk4 only files; would not be needed on Mk3 or earlier.
freeze_as_mpy('', [ freeze_as_mpy('', [
'display.py',
'hsm.py', 'hsm.py',
'hsm_ux.py', 'hsm_ux.py',
'mempad.py', 'mempad.py',

View File

@ -119,7 +119,7 @@ class ShortcutItem(MenuItem):
super().__init__('SHORTCUT', shortcut=key, **kws) super().__init__('SHORTCUT', shortcut=key, **kws)
class NonDefaultMenuItem(MenuItem): class NonDefaultMenuItem(MenuItem):
# Show a checkmark if setting is defined and not the default ... so know know it's set # Show a checkmark if setting is defined and not the default
def __init__(self, label, nvkey, prelogin=False, default_value=None, **kws): def __init__(self, label, nvkey, prelogin=False, default_value=None, **kws):
super().__init__(label, **kws) super().__init__(label, **kws)
self.nvkey = nvkey self.nvkey = nvkey
@ -290,7 +290,7 @@ class MenuSystem:
dis.clear() dis.clear()
cursor_y = None cursor_y = None
for n in range(self.ypos+PER_M+1): for n in range(PER_M+1):
real_idx = n+self.ypos real_idx = n+self.ypos
if real_idx >= self.count: break if real_idx >= self.count: break
@ -306,10 +306,6 @@ class MenuSystem:
if fcn and fcn(): if fcn and fcn():
checked = True checked = True
if not has_qwerty and checked and (len(msg) > 14):
# on mk4 every label longer than 14 will overlap with checkmark
checked = False
if self.multi_selected is not None and (real_idx in self.multi_selected): if self.multi_selected is not None and (real_idx in self.multi_selected):
# ignore length constraint above, we need to visually show that # ignore length constraint above, we need to visually show that
# smthg is selected - in any case # smthg is selected - in any case
@ -335,9 +331,8 @@ class MenuSystem:
if wrap: return True if wrap: return True
# Do wrap-around (by request from NVK) if longer than the screen itself (on Q), # Do wrap-around (by request from NVK) if longer than the screen itself (on Q),
# for mk4, limit is 16 which hits mostly the seed word menus. # Mk4: same limit
limit = 10 if has_qwerty else 16 return self.count > 10
return self.count > limit
def down(self): def down(self):
if self.cursor < self.count-1: if self.cursor < self.count-1:

View File

@ -1,6 +1,6 @@
# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC. # (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC.
# #
# mk4.py - Mk4 specific code, not needed on earlier devices. # mk4.py - Mk4 and Mk5 specific code, not needed on earlier devices.
# #
# #
import os, sys, pyb, ckcc, version, glob import os, sys, pyb, ckcc, version, glob
@ -11,7 +11,8 @@ def make_flash_fs():
os.VfsLfs2.mkfs(fl) os.VfsLfs2.mkfs(fl)
os.mount(fl, '/flash') os.mount(fl, '/flash')
os.mkdir('/flash/settings') os.chdir('/flash')
os.mkdir('settings')
def make_psram_fs(): def make_psram_fs():
# Filesystem is wiped and rebuilt on each boot before this point, but # Filesystem is wiped and rebuilt on each boot before this point, but

View File

@ -12,7 +12,7 @@ from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
from ux import (ux_show_story, OK, ux_enter_bip32_index, ux_input_text, the_ux, from ux import (ux_show_story, OK, ux_enter_bip32_index, ux_input_text, the_ux,
import_export_prompt, ux_aborted) import_export_prompt, ux_aborted)
from utils import problem_file_line, to_ascii_printable, show_single_address from utils import problem_file_line, to_ascii_printable, show_single_address, node_from_privkey
from files import CardSlot, CardMissingError, needs_microsd from files import CardSlot, CardMissingError, needs_microsd
def rfc_signature_template(msg, addr, sig): def rfc_signature_template(msg, addr, sig):
@ -179,14 +179,16 @@ async def msg_sign_ux_get_subpath(addr_fmt):
purpose = chains.af_to_bip44_purpose(addr_fmt) purpose = chains.af_to_bip44_purpose(addr_fmt)
chain_n = chains.current_chain().b44_cointype chain_n = chains.current_chain().b44_cointype
acct = await ux_enter_bip32_index('Account Number:') or 0 acct = await ux_enter_bip32_index('Account Number:')
if acct is None: return
ch = await ux_show_story(title="Change?", ch = await ux_show_story(title="Change?",
msg="Press (0) to use internal/change address," msg="Press (0) to use internal/change address,"
" %s to use external/receive address." % OK, escape="0") " %s to use external/receive address." % OK, escape="0")
change = 1 if ch == '0' else 0 change = 1 if ch == '0' else 0
idx = await ux_enter_bip32_index('Index Number:') or 0 idx = await ux_enter_bip32_index('Index Number:')
if idx is None: return
return "m/%dh/%dh/%dh/%d/%d" % (purpose, chain_n, acct, change, idx) return "m/%dh/%dh/%dh/%d/%d" % (purpose, chain_n, acct, change, idx)
@ -260,13 +262,13 @@ def write_sig_file(content_list, derive=None, addr_fmt=AF_CLASSIC, pk=None, sig_
return sig_nice return sig_nice
def validate_text_for_signing(text, only_printable=True): def validate_text_for_signing(text, allow_tab_nl=False):
# Check for some UX/UI traps in the message itself. # Check for some UX/UI traps in the message itself.
# - messages must be short and ascii only. Our charset is limited # - messages must be short and ascii only. Our charset is limited
# - too many spaces, leading/trailing can be an issue # - too many spaces, leading/trailing can be an issue
# MSG_MAX_SPACES = 4 # impt. compared to -=- positioning # MSG_MAX_SPACES = 4 # impt. compared to -=- positioning
text = str(text, "ascii") # handle memoryview coming from USB
result = to_ascii_printable(text, only_printable=only_printable) result = to_ascii_printable(text, allow_tab_nl=allow_tab_nl)
length = len(result) length = len(result)
assert length >= 2, "msg too short (min. 2)" assert length >= 2, "msg too short (min. 2)"
@ -313,6 +315,7 @@ def parse_msg_sign_request(data):
if text is None: if text is None:
raise AssertionError("MSG required") raise AssertionError("MSG required")
subpath = data_dict.get("subpath", subpath) subpath = data_dict.get("subpath", subpath)
assert isinstance(subpath, str), "subpath"
addr_fmt = data_dict.get("addr_fmt", addr_fmt) addr_fmt = data_dict.get("addr_fmt", addr_fmt)
is_json = True is_json = True
except ValueError: except ValueError:
@ -331,11 +334,13 @@ def parse_msg_sign_request(data):
addr_fmt = addr_fmt_from_subpath(subpath) addr_fmt = addr_fmt_from_subpath(subpath)
if not subpath: if not subpath:
subpath = chains.STD_DERIVATIONS[addr_fmt] try:
subpath = subpath.format( subpath = chains.STD_DERIVATIONS[addr_fmt]
coin_type=chains.current_chain().b44_cointype, subpath = subpath.format(
account=0, change=0, idx=0 coin_type=chains.current_chain().b44_cointype,
) account=0, change=0, idx=0
)
except: pass
return text, subpath, addr_fmt, is_json return text, subpath, addr_fmt, is_json
@ -381,7 +386,7 @@ def sign_message_digest(digest, subpath, prompt, addr_fmt=AF_CLASSIC, pk=None):
else: else:
# if private key is provided, derivation subpath is ignored # if private key is provided, derivation subpath is ignored
# and given private key is used for signing. # and given private key is used for signing.
node = ngu.hdnode.HDNode().from_chaincode_privkey(bytes(32), pk) node = node_from_privkey(pk)
dis.progress_sofar(50, 100) dis.progress_sofar(50, 100)
addr = ch.address(node, addr_fmt) addr = ch.address(node, addr_fmt)
@ -408,9 +413,10 @@ async def ux_sign_msg(txt, approved_cb=None, kill_menu=True):
text, af = item.arg text, af = item.arg
subpath = await msg_sign_ux_get_subpath(af) subpath = await msg_sign_ux_get_subpath(af)
if subpath is None: return
await approve_msg_sign(text, subpath, af, approved_cb=approved_cb, await approve_msg_sign(text, subpath, af, approved_cb=approved_cb,
kill_menu=kill_menu, only_printable=False) kill_menu=kill_menu, allow_tab_nl=True)
# pick address format # pick address format
rv = [ rv = [

View File

@ -10,7 +10,7 @@ from ux import ux_enter_bip32_index, ux_enter_number, OK, X
from files import CardSlot, CardMissingError, needs_microsd from files import CardSlot, CardMissingError, needs_microsd
from descriptor import MultisigDescriptor, multisig_descriptor_template from descriptor import MultisigDescriptor, multisig_descriptor_template
from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AFC_SCRIPT, MAX_SIGNERS, AF_CLASSIC from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AFC_SCRIPT, MAX_SIGNERS, AF_CLASSIC
from menu import MenuSystem, MenuItem, NonDefaultMenuItem, start_chooser, ToggleMenuItem from menu import MenuSystem, MenuItem, NonDefaultMenuItem, start_chooser, ToggleMenuItem, ShortcutItem
from opcodes import OP_CHECKMULTISIG from opcodes import OP_CHECKMULTISIG
from exceptions import FatalPSBTIssue from exceptions import FatalPSBTIssue
from glob import settings from glob import settings
@ -245,9 +245,11 @@ class MultisigWallet(WalletABC):
return rv return rv
@classmethod @classmethod
def iter_wallets(cls, M=None, N=None, not_idx=None, addr_fmt=None): def iter_wallets(cls, M=None, N=None, not_idx=None, addr_fmts=None, name=None):
# yield MS wallets we know about, that match at least right M,N if known. # yield MS wallets we know about, that match at least right M,N if known.
# - this is only place we should be searching this list, please!! # - this is only place we should be searching this list, please!!
# addr_fmts: list of address formats we're interested in
# name: string ms wallet name
lst = settings.get('multisig', []) lst = settings.get('multisig', [])
for idx, rec in enumerate(lst): for idx, rec in enumerate(lst):
@ -255,17 +257,20 @@ class MultisigWallet(WalletABC):
# ignore one by index # ignore one by index
continue continue
if name and (rec[0] != name):
continue
if M or N: if M or N:
# peek at M/N # peek at M/N
has_m, has_n = tuple(rec[1]) has_m, has_n = tuple(rec[1])
if M is not None and has_m != M: continue if M is not None and has_m != M: continue
if N is not None and has_n != N: continue if N is not None and has_n != N: continue
if addr_fmt is not None: if addr_fmts:
opts = rec[3] opts = rec[3]
af = opts.get('ft', AF_P2SH) af = opts.get('ft', AF_P2SH)
if af != addr_fmt: continue if af not in addr_fmts: continue
yield cls.deserialize(rec, idx) yield cls.deserialize(rec, idx)
def get_xfp_paths(self): def get_xfp_paths(self):
@ -273,28 +278,23 @@ class MultisigWallet(WalletABC):
return list(self.xfp_paths.values()) return list(self.xfp_paths.values())
@classmethod @classmethod
def find_match(cls, M, N, xfp_paths, addr_fmt=None): def find_match(cls, M, N, xfp_paths, addr_fmts=None):
# Find index of matching wallet # Find index of matching wallet
# - xfp_paths is list of lists: [xfp, *path] like in psbt files # - xfp_paths is list of lists: [xfp, *path] like in psbt files
# - M and N must be known # - M and N must be known
# - returns instance, or None if not found # - returns instance, or None if not found
for rv in cls.iter_wallets(M, N, addr_fmt=addr_fmt): for rv in cls.iter_wallets(M, N, addr_fmts=addr_fmts):
if rv.matching_subpaths(xfp_paths): if rv.matching_subpaths(xfp_paths):
return rv return rv
return None return None
@classmethod @classmethod
def find_candidates(cls, xfp_paths, addr_fmt=None, M=None): def find_candidates(cls, xfp_paths):
# Return a list of matching wallets for various M values. # Return a list of matching wallets for various M values.
# - xpfs_paths should already be sorted # - xpfs_paths should already be sorted
# - returns set of matches, of any M value
# we know N, but not M at this point.
N = len(xfp_paths)
matches = [] matches = []
for rv in cls.iter_wallets(M=M, addr_fmt=addr_fmt): for rv in cls.iter_wallets():
if rv.matching_subpaths(xfp_paths): if rv.matching_subpaths(xfp_paths):
matches.append(rv) matches.append(rv)
@ -327,11 +327,12 @@ class MultisigWallet(WalletABC):
return True return True
def assert_matching(self, M, N, xfp_paths): def assert_matching(self, M, N, xfp_paths, addr_fmt):
# compare in-memory wallet with details recovered from PSBT # compare in-memory wallet with details recovered from PSBT
# - xfp_paths must be sorted already # - xfp_paths must be sorted already
assert (self.M, self.N) == (M, N), "M/N mismatch" assert (self.M, self.N) == (M, N), "M/N mismatch"
assert len(xfp_paths) == N, "XFP count" assert len(xfp_paths) == N, "XFP count"
assert self.addr_fmt == addr_fmt, "addr fmt"
if self.disable_checks: return if self.disable_checks: return
assert self.matching_subpaths(xfp_paths), "wrong XFP/derivs" assert self.matching_subpaths(xfp_paths), "wrong XFP/derivs"
@ -408,7 +409,7 @@ class MultisigWallet(WalletABC):
# - count_similar: same N, same xfp+paths # - count_similar: same N, same xfp+paths
lst = self.get_xfp_paths() lst = self.get_xfp_paths()
c = self.find_match(self.M, self.N, lst, addr_fmt=self.addr_fmt) c = self.find_match(self.M, self.N, lst, addr_fmts=[self.addr_fmt])
if c: if c:
# All details are same: M/N, paths, addr fmt # All details are same: M/N, paths, addr fmt
if sorted(self.xpubs) != sorted(c.xpubs): if sorted(self.xpubs) != sorted(c.xpubs):
@ -423,6 +424,11 @@ class MultisigWallet(WalletABC):
# do not allow to import multi if sortedmulti with the same set of keys # do not allow to import multi if sortedmulti with the same set of keys
# already imported and vice-versa # already imported and vice-versa
return None, ["BIP-67 clash"], 1 return None, ["BIP-67 clash"], 1
elif not self.bip67 and self.xpubs != c.xpubs:
# multi(2,A,B) and multi(2,B,A) are consensus-different scripts;
# treat as duplicates -- don't allow either if a same-keys variant
# in a different order is already enrolled
return None, ["key order"], 1
elif self.name == c.name: elif self.name == c.name:
return None, [], 1 return None, [], 1
else: else:
@ -454,7 +460,7 @@ class MultisigWallet(WalletABC):
assert self.storage_idx >= 0 assert self.storage_idx >= 0
# safety check # safety check
for existing in self.iter_wallets(M=self.M, N=self.N, addr_fmt=self.addr_fmt): for existing in self.iter_wallets(M=self.M, N=self.N, addr_fmts=[self.addr_fmt]):
if existing.storage_idx != self.storage_idx: continue if existing.storage_idx != self.storage_idx: continue
break break
else: else:
@ -582,7 +588,7 @@ class MultisigWallet(WalletABC):
found_pk = node.pubkey() found_pk = node.pubkey()
# Document path(s) used. Not sure this is useful info to user tho. # Document path(s) used. Not sure this is useful info to user tho.
# - Do not show what we can't verify: we don't really know the hardeneded # - Do not show what we can't verify: we don't really know the hardened
# part of the path from fingerprint to here. # part of the path from fingerprint to here.
here = '[%s]' % xfp2str(xfp) here = '[%s]' % xfp2str(xfp)
if dp != len(path): if dp != len(path):
@ -864,7 +870,8 @@ class MultisigWallet(WalletABC):
def make_fname(self, prefix, suffix='txt'): def make_fname(self, prefix, suffix='txt'):
rv = '%s-%s.%s' % (prefix, self.name, suffix) rv = '%s-%s.%s' % (prefix, self.name, suffix)
return rv.replace(' ', '_') rv = rv.replace(' ', '_')
return rv.replace('/', '-')
async def export_electrum(self): async def export_electrum(self):
# Generate and save an Electrum JSON file. # Generate and save an Electrum JSON file.
@ -968,34 +975,7 @@ class MultisigWallet(WalletABC):
print('%s: %s' % (xfp2str(xfp), val), file=fp) print('%s: %s' % (xfp2str(xfp), val), file=fp)
@classmethod @classmethod
def guess_addr_fmt(cls, npath): def import_from_psbt(cls, af, M, N, xpubs_list):
# Assuming the bips are being respected, what address format will be used,
# based on indicated numeric subkey path observed.
# - return None if unsure, no errors
#
#( "m/45h", 'p2sh', AF_P2SH),
#( "m/48h/{coin}h/0h/1h", 'p2sh_p2wsh', AF_P2WSH_P2SH),
#( "m/48h/{coin}h/0h/2h", 'p2wsh', AF_P2WSH)
top = npath[0] & 0x7fffffff
if top == npath[0]:
# non-hardened top? rare/bad
return
if top == 45:
return AF_P2SH
if top == 48:
if len(npath) < 4: return
last = npath[3] & 0x7fffffff
if last == 1:
return AF_P2WSH_P2SH
if last == 2:
return AF_P2WSH
@classmethod
def import_from_psbt(cls, M, N, xpubs_list):
# given the raw data from PSBT global header, offer the user # given the raw data from PSBT global header, offer the user
# the details, and/or bypass that all and just trust the data. # the details, and/or bypass that all and just trust the data.
# - xpubs_list is a list of (xfp+path, binary BIP-32 xpub) # - xpubs_list is a list of (xfp+path, binary BIP-32 xpub)
@ -1024,14 +1004,13 @@ class MultisigWallet(WalletABC):
expect_chain, my_xfp, xpubs) expect_chain, my_xfp, xpubs)
if is_mine: if is_mine:
has_mine += 1 has_mine += 1
addr_fmt = cls.guess_addr_fmt(path)
assert has_mine == 1 # 'my key not included' assert has_mine == 1 # 'my key not included'
name = 'PSBT-%d-of-%d' % (M, N) name = 'PSBT-%d-of-%d' % (M, N)
# this will always create sortedmulti multisig (BIP-67) # this will always create sortedmulti multisig (BIP-67)
# because BIP-174 came years after wide spread acceptance of BIP-67 policy # because BIP-174 came years after wide spread acceptance of BIP-67 policy
ms = cls(name, (M, N), xpubs, chain_type=expect_chain, addr_fmt=addr_fmt or AF_P2SH) ms = cls(name, (M, N), xpubs, chain_type=expect_chain, addr_fmt=af)
# may just keep in-memory version, no approval required, if we are # may just keep in-memory version, no approval required, if we are
# trusting PSBT's today, otherwise caller will need to handle UX w.r.t new wallet # trusting PSBT's today, otherwise caller will need to handle UX w.r.t new wallet
@ -1108,11 +1087,11 @@ class MultisigWallet(WalletABC):
story = 'Update NAME only of existing multisig wallet?' story = 'Update NAME only of existing multisig wallet?'
elif num_dups and isinstance(diff_items, list): elif num_dups and isinstance(diff_items, list):
# failures only # failures only
story = "Duplicate wallet." story = "Duplicate wallet. "
if diff_items: if diff_items:
story += diff_items[0] story += diff_items[0]
else: else:
story += ' All details are the same as existing!' story += 'All details are the same as existing!'
is_dup = True is_dup = True
elif diff_items: elif diff_items:
# Concern here is overwrite when similar, but we don't overwrite anymore, so # Concern here is overwrite when similar, but we don't overwrite anymore, so
@ -1407,7 +1386,7 @@ class MultisigMenu(MenuSystem):
@classmethod @classmethod
def construct(cls): def construct(cls):
# Dynamic menu with user-defined names of wallets shown # Dynamic menu with user-defined names of wallets shown
from glob import NFC from flow import nfc_enabled
if not MultisigWallet.exists(): if not MultisigWallet.exists():
rv = [MenuItem('(none setup yet)', f=no_ms_yet)] rv = [MenuItem('(none setup yet)', f=no_ms_yet)]
@ -1417,11 +1396,7 @@ class MultisigMenu(MenuSystem):
rv.append(MenuItem('%d/%d: %s' % (ms.M, ms.N, ms.name), rv.append(MenuItem('%d/%d: %s' % (ms.M, ms.N, ms.name),
menu=make_ms_wallet_menu, arg=ms.storage_idx)) menu=make_ms_wallet_menu, arg=ms.storage_idx))
rv.append(MenuItem('Import from File', f=import_multisig)) rv.append(MenuItem('Import', f=import_multisig))
rv.append(MenuItem('Import from QR', f=import_multisig_qr,
predicate=version.has_qwerty, shortcut=KEY_QR))
rv.append(MenuItem('Import via NFC', f=import_multisig_nfc,
predicate=bool(NFC), shortcut=KEY_NFC))
rv.append(MenuItem('Export XPUB', f=export_multisig_xpubs)) rv.append(MenuItem('Export XPUB', f=export_multisig_xpubs))
rv.append(MenuItem('Create Airgapped', f=create_ms_step1)) rv.append(MenuItem('Create Airgapped', f=create_ms_step1))
rv.append(MenuItem('Trust PSBT?', f=trust_psbt_menu)) rv.append(MenuItem('Trust PSBT?', f=trust_psbt_menu))
@ -1435,6 +1410,9 @@ class MultisigMenu(MenuSystem):
rv.append(NonDefaultMenuItem( rv.append(NonDefaultMenuItem(
'Unsorted Multisig?' if version.has_qwerty else 'Unsorted Multi?', 'Unsorted Multisig?' if version.has_qwerty else 'Unsorted Multi?',
'unsort_ms', f=unsorted_ms_menu)) 'unsort_ms', f=unsorted_ms_menu))
rv.append(ShortcutItem(KEY_NFC, predicate=nfc_enabled, f=import_multisig_nfc))
rv.append(ShortcutItem(KEY_QR, predicate=version.has_qwerty, f=import_multisig_qr))
return rv return rv
def update_contents(self): def update_contents(self):
@ -1545,7 +1523,7 @@ async def ms_wallet_electrum_export(menu, label, item):
msg = 'The new wallet will have derivation path:\n %s\n and use %s addresses.\n' % ( msg = 'The new wallet will have derivation path:\n %s\n and use %s addresses.\n' % (
dsum, MultisigWallet.render_addr_fmt(ms.addr_fmt) ) dsum, MultisigWallet.render_addr_fmt(ms.addr_fmt) )
if await ux_show_story(electrum_export_story(msg)) != 'y': if await ux_show_story(electrum_export_story("Electrum", msg)) != 'y':
return return
await ms.export_electrum() await ms.export_electrum()
@ -1593,7 +1571,8 @@ P2WSH:
if ch != "y": if ch != "y":
return return
acct = await ux_enter_bip32_index('Account Number:') or 0 acct = await ux_enter_bip32_index('Account Number:')
if acct is None: return
def render(acct_num): def render(acct_num):
sign_der = None sign_der = None
@ -1806,7 +1785,8 @@ async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH, is_qr=False,
secret, ccc_ms_count = for_ccc secret, ccc_ms_count = for_ccc
# Always include 2 keys from CCC: own master (key A) and key C # Always include 2 keys from CCC: own master (key A) and key C
# - force them to same derivation. # - force them to same derivation.
acct = await ux_enter_bip32_index('CCC Account Number:') or 0 acct = await ux_enter_bip32_index('CCC Account Number:')
if acct is None: return
dis.fullscreen("Wait...") dis.fullscreen("Wait...")
a = add_own_xpub(chain, acct, addr_fmt) # master: key A a = add_own_xpub(chain, acct, addr_fmt) # master: key A
@ -1831,7 +1811,8 @@ async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH, is_qr=False,
ch = await ux_show_story("Add current Coldcard with above XFP ?", ch = await ux_show_story("Add current Coldcard with above XFP ?",
title="[%s]" % xfp2str(my_xfp)) title="[%s]" % xfp2str(my_xfp))
if ch == "y": if ch == "y":
acct = await ux_enter_bip32_index('Account Number:') or 0 acct = await ux_enter_bip32_index('Account Number:')
if acct is None: return
dis.fullscreen("Wait...") dis.fullscreen("Wait...")
xpubs.append(add_own_xpub(chain, acct, addr_fmt)) xpubs.append(add_own_xpub(chain, acct, addr_fmt))
num_mine += 1 num_mine += 1
@ -1846,10 +1827,8 @@ async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH, is_qr=False,
M = 2 M = 2
else: else:
# pick useful M value to start # pick useful M value to start
M = await ux_enter_number("How many need to sign?(M)", N, can_cancel=True) M = await ux_enter_number("How many need to sign?(M)", N)
if not M: if M is None: return
await ux_dramatic_pause('Aborted.', 2)
return # user cancel
dis.fullscreen("Wait...") dis.fullscreen("Wait...")
@ -1940,26 +1919,19 @@ async def import_multisig_qr(*a):
except Exception as e: except Exception as e:
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e))) await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
async def import_multisig(*a): async def import_multisig(*a):
# pick text file from SD card, import as multisig setup file # pick text file from SD card, import as multisig setup file
from actions import file_picker from actions import file_picker
from glob import VD from ux import import_export_prompt
force_vdisk = False ch = await import_export_prompt("multisig wallet file", is_import=True)
if VD: if isinstance(ch, str):
prompt = "Press (1) to import multisig wallet file from SD Card" if ch == KEY_QR:
escape = "1" await import_multisig_qr()
if VD is not None: elif ch == KEY_NFC:
prompt += ", press (2) to import from Virtual Disk" await import_multisig_nfc()
escape += "2" return
prompt += "."
ch = await ux_show_story(prompt, escape=escape)
if ch == "1":
force_vdisk=False
elif ch == "2":
force_vdisk = True
else:
return
def possible(filename): def possible(filename):
with open(filename, 'rt') as fd: with open(filename, 'rt') as fd:
@ -1971,11 +1943,11 @@ async def import_multisig(*a):
return True return True
fn = await file_picker(suffix=['.txt', '.json'], min_size=100, max_size=20*200, fn = await file_picker(suffix=['.txt', '.json'], min_size=100, max_size=20*200,
taster=possible, force_vdisk=force_vdisk) taster=possible, **ch)
if not fn: return if not fn: return
try: try:
with CardSlot(force_vdisk=force_vdisk) as card: with CardSlot(**ch) as card:
with open(fn, 'rt') as fp: with open(fn, 'rt') as fp:
data = fp.read() data = fp.read()
except CardMissingError: except CardMissingError:
@ -1986,8 +1958,8 @@ async def import_multisig(*a):
try: try:
possible_name = (fn.split('/')[-1].split('.'))[0] possible_name = (fn.split('/')[-1].split('.'))[0]
maybe_enroll_xpub(config=data, name=possible_name) maybe_enroll_xpub(config=data, name=possible_name)
except Exception as e: except BaseException as e:
#import sys; sys.print_exception(e) # import sys; sys.print_exception(e)
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e))) await ux_show_story('Failed to import multisig.\n\n%s\n%s' % (e, problem_file_line(e)))
# EOF # EOF

View File

@ -2,7 +2,7 @@
# #
# ndef.py -- NDEF records: making them and parsing them. # ndef.py -- NDEF records: making them and parsing them.
# #
# - see ../docs/nfc-on-coldcard.md for background. # - see ../docs/nfc-coldcard.md for background.
# - cross platform file # - cross platform file
# #
from struct import pack, unpack from struct import pack, unpack

View File

@ -107,13 +107,14 @@ class NFCHandler:
from glob import dis from glob import dis
here = bytes(256) here = bytes(256)
end = 8196 end = 8196
for pos in range(0, end, 256) : for pos in range(0, end, 256):
self.i2c.writeto_mem(I2C_ADDR_USER, pos, here, addrsize=16) self.i2c.writeto_mem(I2C_ADDR_USER, pos, here, addrsize=16)
if pos == 256 and not full_wipe: break if (pos == 256) and not full_wipe: break
# 6ms per 16 byte row, worst case, so ~100ms here per iter! 3.2seconds total # 6ms per 16 byte row, worst case, so ~100ms here per iter! 3.2seconds total
if full_wipe: if full_wipe:
dis.progress_bar_show(pos / end) dis.progress_bar_show(pos / end)
await self.wait_ready() await self.wait_ready()
# system config area (flash cells, but affect operation): table 12 # system config area (flash cells, but affect operation): table 12
@ -225,9 +226,15 @@ class NFCHandler:
self.set_rf_disable(1) self.set_rf_disable(1)
async def share_loop(self, n, **kws): async def share_loop(self, n, **kws):
# Keep one fully-written tag image live until the user exits. Some
# phones perform multiple probes while deciding if a tag is NDEF.
await self.big_write(n.bytes())
while 1: while 1:
done = await self.share_start(n, **kws) aborted = await self.ux_animation(exit_after_activity=False, **kws)
if done: break if aborted:
await self.wipe(kws.get("is_secret", False))
break
async def share_signed_txn(self, txid, file_offset, txn_len, txn_sha): async def share_signed_txn(self, txid, file_offset, txn_len, txn_sha):
# we just signed something, share it over NFC # we just signed something, share it over NFC
@ -397,12 +404,13 @@ class NFCHandler:
self.write_dyn(GPO_CTRL_Dyn, 0x01) # GPO_EN self.write_dyn(GPO_CTRL_Dyn, 0x01) # GPO_EN
self.read_dyn(IT_STS_Dyn) # clear interrupt self.read_dyn(IT_STS_Dyn) # clear interrupt
async def ux_animation(self, write_mode, allow_enter=True, prompt=None, line2=None, async def ux_animation(self, allow_enter=True, prompt=None, line2=None,
is_secret=False): is_secret=False, exit_after_activity=True,
min_delay=1000):
# Run the pretty animation, and detect both when we are written, and/or key to exit/abort. # Run the pretty animation, and detect both when we are written, and/or key to exit/abort.
# - similar when "read" and then removed from field # - similar when "read" and then removed from field
# - return T if aborted by user # - return T if aborted by user
from glob import dis, numpad from glob import dis
await self.wait_ready() await self.wait_ready()
self.set_rf_disable(0) self.set_rf_disable(0)
@ -415,7 +423,8 @@ class NFCHandler:
dis.text(None, -3, line2) dis.text(None, -3, line2)
else: else:
from graphics_mk4 import Graphics from graphics_mk4 import Graphics
frames = [getattr(Graphics, 'mk4_nfc_%d'%i) for i in range(1, 5)] from version import mk_num
frames = [getattr(Graphics, 'mk%d_nfc_%d'%(mk_num, i)) for i in range(1, 5)]
aborted = True aborted = True
phase = -1 phase = -1
@ -423,7 +432,6 @@ class NFCHandler:
# (ms) How long to wait after RF field comes and goes # (ms) How long to wait after RF field comes and goes
# - user can press OK during this period if they know they are done # - user can press OK during this period if they know they are done
min_delay = (3000 if write_mode else 1000)
while 1: while 1:
if dis.has_lcd: if dis.has_lcd:
@ -462,7 +470,7 @@ class NFCHandler:
aborted = False aborted = False
break break
if last_activity: if exit_after_activity and last_activity:
dt = utime.ticks_diff(utime.ticks_ms(), last_activity) dt = utime.ticks_diff(utime.ticks_ms(), last_activity)
if dt >= min_delay: if dt >= min_delay:
# They acheived some RF activity and then nothing for some time, so # They acheived some RF activity and then nothing for some time, so
@ -471,9 +479,6 @@ class NFCHandler:
break break
self.set_rf_disable(1) self.set_rf_disable(1)
if not write_mode:
# function argument secret decides whether to do full wipe after writing to chip
await self.wipe(is_secret)
return aborted return aborted
@ -481,17 +486,15 @@ class NFCHandler:
# do the UX while we are sharing a value over NFC # do the UX while we are sharing a value over NFC
# - assumpting is people know what they are scanning # - assumpting is people know what they are scanning
# - x key to abort early, but also self-clears # - x key to abort early, but also self-clears
await self.big_write(ndef_obj.bytes()) await self.big_write(ndef_obj.bytes())
return await self.ux_animation(**kws)
return await self.ux_animation(False, **kws)
async def start_nfc_rx(self, **kws): async def start_nfc_rx(self, **kws):
# Pretend to be a big warm empty tag ready to be stuffed with data # Pretend to be a big warm empty tag ready to be stuffed with data
await self.big_write(ndef.CC_WR_FILE) await self.big_write(ndef.CC_WR_FILE)
# wait until something is written # wait until something is written
aborted = await self.ux_animation(True, **kws) aborted = await self.ux_animation(min_delay=3000, **kws)
if aborted: return if aborted: return
# read CCFILE area (header) # read CCFILE area (header)
@ -618,7 +621,7 @@ class NFCHandler:
# it's a txn, and we wrote as hex # it's a txn, and we wrote as hex
data = a2b_hex(data) data = a2b_hex(data)
else: else:
assert data[2:8] == bytes(6) assert data[1:4] == bytes(3)
sha = ngu.hash.sha256s(data) sha = ngu.hash.sha256s(data)
await self.share_signed_txn(txid, data, len(data), sha) await self.share_signed_txn(txid, data, len(data), sha)
elif ext == 'psbt': elif ext == 'psbt':
@ -739,7 +742,7 @@ class NFCHandler:
m = m.decode() m = m.decode()
what, vals = decode_bip21_text(m) what, vals = decode_bip21_text(m)
if what == 'addr': if what == 'addr':
return vals[1] return vals
winner = await self._nfc_reader(f, 'Unable to find address from NFC data.') winner = await self._nfc_reader(f, 'Unable to find address from NFC data.')
@ -747,10 +750,11 @@ class NFCHandler:
async def verify_address_nfc(self): async def verify_address_nfc(self):
# Get an address or complete bip-21 url even and search it... slow. # Get an address or complete bip-21 url even and search it... slow.
winner = await self.read_address() res = await self.read_address()
if winner: if not res: return
from ownership import OWNERSHIP _, addr, args = res
await OWNERSHIP.search_ux(winner) from ownership import OWNERSHIP
await OWNERSHIP.search_ux(addr, args)
async def read_extended_private_key(self): async def read_extended_private_key(self):
f = lambda x: x.decode().strip() if b"prv" in x else None f = lambda x: x.decode().strip() if b"prv" in x else None
@ -760,20 +764,31 @@ class NFCHandler:
f = lambda x: a2b_base64(x.decode()) if 150 <= len(x) <= 280 else None f = lambda x: a2b_base64(x.decode()) if 150 <= len(x) <= 280 else None
return await self._nfc_reader(f, 'Unable to find base64 encoded TAPSIGNER backup.') return await self._nfc_reader(f, 'Unable to find base64 encoded TAPSIGNER backup.')
async def read_bip322_msg(self):
f = lambda x: x.decode()
return await self._nfc_reader(f, 'Unable to find BIP-322 message.')
async def read_wif(self):
# only compressed WIFs allowed
f = lambda x: x.decode() if len(x) >= 51 else None
return await self._nfc_reader(f, 'Unable to find WIF key(s).')
async def _nfc_reader(self, func, fail_msg): async def _nfc_reader(self, func, fail_msg):
data = await self.start_nfc_rx() data = await self.start_nfc_rx()
if not data: return if not data: return
winner = None winner = None
for urn, msg, meta in ndef.record_parser(data): try:
msg = bytes(msg) for urn, msg, meta in ndef.record_parser(data):
try: msg = bytes(msg)
r = func(msg) try:
if r is not None: r = func(msg)
winner = r if r is not None:
break winner = r
except: break
pass except:
pass
except Exception: pass # dont crash when given garbage
if not winner: if not winner:
await ux_show_story(fail_msg) await ux_show_story(fail_msg)

View File

@ -10,10 +10,11 @@ from ux_q1 import QRScannerInteraction
from actions import goto_top_menu from actions import goto_top_menu
from glob import settings, dis from glob import settings, dis
from files import CardMissingError, needs_microsd, CardSlot from files import CardMissingError, needs_microsd, CardSlot
from public_constants import MSG_SIGNING_MAX_LENGTH
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
from charcodes import KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6 from charcodes import KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6
from lcd_display import CHARS_W from lcd_display import CHARS_W
from utils import problem_file_line, url_unquote, wipe_if_deltamode from utils import problem_file_line, url_unquote, wipe_if_deltamode, is_printable
# title, username and such are limited that they fit on the one line both in # title, username and such are limited that they fit on the one line both in
# text entry (W-2) and also in menu display (W-3) # text entry (W-2) and also in menu display (W-3)
@ -21,6 +22,15 @@ from utils import problem_file_line, url_unquote, wipe_if_deltamode
ONE_LINE = CHARS_W-2 ONE_LINE = CHARS_W-2
async def make_notes_menu(*a): async def make_notes_menu(*a):
from pincodes import pa
if pa.hobbled_mode:
# Read only version of menu system
# - used when spending policy in effect
# - must have some notes already, or unreachable
rv = NotesMenu(NotesMenu.construct_readonly())
rv.readonly = True
return rv
if not settings.get('secnap', False): if not settings.get('secnap', False):
# Explain feature, and then enable if interested. Drop them into menu. # Explain feature, and then enable if interested. Drop them into menu.
@ -105,6 +115,8 @@ async def get_a_password(old_value, min_len=0, max_len=128):
class NotesMenu(MenuSystem): class NotesMenu(MenuSystem):
readonly = False
@classmethod @classmethod
def construct(cls): def construct(cls):
# Dynamic menu with user-defined names of notes shown # Dynamic menu with user-defined names of notes shown
@ -119,9 +131,7 @@ class NotesMenu(MenuSystem):
else: else:
wipe_if_deltamode() wipe_if_deltamode()
rv = [] rv = cls.construct_note_items(readonly=False)
for note in NoteContent.get_all():
rv.append(MenuItem('%d: %s' % (note.idx+1, note.title), menu=note.make_menu))
rv.extend(news) rv.extend(news)
@ -134,6 +144,39 @@ class NotesMenu(MenuSystem):
return rv return rv
@classmethod
def construct_readonly(cls):
# When only allowed to view, no export/add new/delete.
wipe_if_deltamode()
rv = cls.construct_note_items(readonly=True)
if not rv:
rv.append(MenuItem('(none saved yet)'))
return rv
@classmethod
def construct_note_items(cls, readonly=False):
rv = []
by_group = {}
for note in NoteContent.get_all():
item = MenuItem('%d: %s' % (note.idx+1, note.title),
menu=note.make_menu, arg=readonly)
group = note.group
if group:
if group not in by_group:
by_group[group] = []
by_group[group].append(item)
else:
rv.append(item)
for group in sorted(by_group):
rv.append(MenuItem('' + group, menu=NoteGroupMenu(group, readonly)))
return rv
@classmethod @classmethod
async def export_all(cls, *a): async def export_all(cls, *a):
await start_export(NoteContent.get_all()) await start_export(NoteContent.get_all())
@ -185,7 +228,7 @@ class NotesMenu(MenuSystem):
@classmethod @classmethod
async def disable_notes(cls, *a): async def disable_notes(cls, *a):
# they don't want feature anymore; already checked no notes in effect # they don't want feature anymore; already checked no notes in effect
# - no need for confirm, they aren't loosing anything # - no need for confirm, they aren't losing anything
settings.remove_key('secnap') settings.remove_key('secnap')
settings.remove_key('notes') settings.remove_key('notes')
settings.save() settings.save()
@ -204,9 +247,27 @@ class NotesMenu(MenuSystem):
@classmethod @classmethod
async def drill_to(cls, menu, item): async def drill_to(cls, menu, item):
# make it so looks like we drilled down into the new note # make it so looks like we drilled down into the new note
menu.goto_idx(item.idx) label = '%d: %s' % (item.idx+1, item.title)
m = MenuSystem(await item.make_menu()) group = item.group
the_ux.push(m) if group:
cls.goto_exact_label(menu, '' + group)
gm = NoteGroupMenu(group)
cls.goto_exact_label(gm, label)
the_ux.push(gm)
else:
cls.goto_exact_label(menu, label)
m = await item._make_menu()
the_ux.push(MenuSystem(m))
@staticmethod
def goto_exact_label(menu, label):
for i, mi in enumerate(menu.items):
if mi.label == label:
menu.goto_idx(i)
return True
return False
class NoteContentBase: class NoteContentBase:
@ -223,9 +284,15 @@ class NoteContentBase:
return PasswordContent(j, idx) if 'user' in j else NoteContent(j, idx) return PasswordContent(j, idx) if 'user' in j else NoteContent(j, idx)
def serialize(self): def serialize(self):
return {fld:getattr(self, fld, '') for fld in self.flds} res = {}
for fld in self.flds:
val = getattr(self, fld, '')
# user field is necessary for proper password identification in constructor
if not val and (fld != "user"):
continue
res[fld] = val
to_json = serialize return res
@classmethod @classmethod
def get_all(cls): def get_all(cls):
@ -251,6 +318,15 @@ class NoteContentBase:
settings.put('notes', [n.serialize() for n in notes]) settings.put('notes', [n.serialize() for n in notes])
settings.save() settings.save()
@classmethod
def get_groups(cls):
groups = set()
for note in cls.get_all():
if note.group:
groups.add(note.group)
return sorted(groups)
async def delete(self, *a): async def delete(self, *a):
# Remove note # Remove note
ok = await ux_confirm("Everything about this note/password will be lost.") ok = await ux_confirm("Everything about this note/password will be lost.")
@ -271,6 +347,11 @@ class NoteContentBase:
the_ux.pop() the_ux.pop()
m = the_ux.top_of_stack() m = the_ux.top_of_stack()
m.update_contents() m.update_contents()
parent = the_ux.parent_of(m)
if parent:
parent.update_contents()
if isinstance(m, NoteGroupMenu) and not m.has_notes():
the_ux.pop()
await ux_dramatic_pause('Deleted.', 3) await ux_dramatic_pause('Deleted.', 3)
@ -302,11 +383,17 @@ class NoteContentBase:
if not is_new: if not is_new:
# change our own menu contents # change our own menu contents
menu.replace_items(await self.make_menu()) mi = await self._make_menu()
menu.replace_items(mi)
# update parent # update parent
parent = the_ux.parent_of(menu) parent = the_ux.parent_of(menu)
parent.update_contents() parent.update_contents()
grandparent = the_ux.parent_of(parent)
if grandparent:
grandparent.update_contents()
if isinstance(parent, NoteGroupMenu) and not parent.has_notes():
the_ux.stack.remove(parent)
else: else:
menu.update_contents() menu.update_contents()
@ -336,33 +423,132 @@ class NoteContentBase:
await ux_sign_msg(txt, approved_cb=msg_signing_done, kill_menu=False) await ux_sign_msg(txt, approved_cb=msg_signing_done, kill_menu=False)
def sign_misc_menu_item(self): def sign_misc_menu_item(self):
return MenuItem("Sign Note Text", f=self.sign_txt_msg, arg=self.misc) return MenuItem("Sign Note Text", f=self.sign_txt_msg, arg=self.misc,
predicate=2 <= len(self.misc) <= MSG_SIGNING_MAX_LENGTH)
@staticmethod
def is_b39pass_applicable(data, read_only):
from seed import MAX_PASS_LEN
from ccc import sssp_spending_policy
if read_only and not sssp_spending_policy('okeys'):
return False
return (len(data) <= MAX_PASS_LEN) and is_printable(data) and settings.get("words", True)
async def apply_as_b39_pass(self, a, b, item):
data, readonly = item.arg
# rstrip just trailing whitespaces/tabs/newlines
data = data.rstrip()
# do not allow any more tabs/newlines
assert self.is_b39pass_applicable(data, readonly)
from seed import apply_pass_value
await apply_pass_value(data)
class NoteGroupMenu(MenuSystem):
def __init__(self, group, readonly=False):
self.group = group
self.readonly = readonly
super().__init__(self.construct())
def construct(self):
items = []
for note in NoteContent.get_all():
if note.group == self.group:
items.append(MenuItem('%d: %s' % (note.idx+1, note.title),
menu=note.make_menu, arg=self.readonly))
return items or [MenuItem('(none)')]
def has_notes(self):
return any(note.group == self.group for note in NoteContent.get_all())
def update_contents(self):
self.replace_items(self.construct())
class GroupPickerMenu(MenuSystem):
def __init__(self, current=''):
self.result = None
self.current = current
groups = NoteContentBase.get_groups()
chosen = 0
items = [MenuItem('(none)', f=self.picked, arg='')]
for group in groups:
if group == self.current:
chosen = len(items)
items.append(MenuItem(group, f=self.picked, arg=group))
items.append(MenuItem('New Group', f=self.new_group))
super().__init__(items, chosen=chosen)
async def picked(self, menu, idx, mi):
assert menu == self
self.result = mi.arg
the_ux.pop()
async def new_group(self, menu, idx, mi):
group = await ux_input_text('', max_len=ONE_LINE, confirm_exit=False,
prompt='Group', placeholder='(optional)')
if group is None:
self.result = None
else:
self.result = group
the_ux.pop()
@classmethod
async def pick(cls, current=''):
m = cls(current)
the_ux.push(m)
await m.interact()
return current if m.result is None else m.result
class PasswordContent(NoteContentBase): class PasswordContent(NoteContentBase):
# "Passwords" have a few more fields and are more structured # "Passwords" have a few more fields and are more structured
flds = ['title', 'user', 'password', 'site', 'misc' ] flds = ['title', 'user', 'password', 'site', 'misc', 'group']
type_label = 'password' type_label = 'password'
async def make_menu(self, *a): async def _make_menu(self, readonly=False):
rv = [MenuItem('"%s"' % self.title, f=self.view)] rv = [MenuItem('"%s"' % self.title, f=self.view)]
if self.user: if self.user:
rv.append(MenuItem('%s' % self.user, f=self.view)) rv.append(MenuItem('%s' % self.user, f=self.view))
if self.site: if self.site:
rv.append(MenuItem('%s' % self.site, f=self.view)) rv.append(MenuItem('%s' % self.site, f=self.view))
#if self.misc: rv.append(MenuItem('↳ (notes)', f=self.view)) # if self.misc: rv.append(MenuItem('↳ (notes)', f=self.view))
return rv + [ rv += [
MenuItem('View Password', f=self.view_pw), MenuItem('View Password', f=self.view_pw),
MenuItem('Send Password', f=self.send_pw, predicate=lambda: settings.get('du', True)), MenuItem('Send Password', f=self.send_pw, predicate=lambda: not settings.get('du', 0)),
MenuItem('Export', f=self.export), ]
MenuItem('Edit Metadata', f=self.edit), if not readonly:
MenuItem('Delete', f=self.delete), rv += [
MenuItem('Change Password', f=self.change_pw), MenuItem('Export', f=self.export),
MenuItem('Edit Metadata', f=self.edit),
MenuItem('Delete', f=self.delete),
MenuItem('Change Password', f=self.change_pw),
]
rv += [
self.sign_misc_menu_item(), self.sign_misc_menu_item(),
ShortcutItem(KEY_QR, f=self.view_qr_menu, arg=self.type_label), ShortcutItem(KEY_QR, f=self.view_qr_menu, arg=self.type_label),
ShortcutItem(KEY_NFC, f=self.share_nfc, arg=self.type_label), ShortcutItem(KEY_NFC, f=self.share_nfc, arg=self.type_label),
] ]
# if password is less than MAX_PASS_LEN and only consist of printable ASCII characters
# and current seed (master or tmp) is word based - offer to apply pwd text as BIP-39 passphrase
if self.is_b39pass_applicable(self.password, readonly):
rv += [MenuItem('Apply as BIP-39 Passphrase',
f=self.apply_as_b39_pass, arg=(self.password, readonly))]
return rv
async def make_menu(self, a, b, item):
items = await self._make_menu(readonly=item.arg)
return MenuSystem(items)
async def view(self, *a): async def view(self, *a):
pl = len(self.password) pl = len(self.password)
m = '' m = ''
@ -430,7 +616,8 @@ class PasswordContent(NoteContentBase):
if self.idx == -1: if self.idx == -1:
# prompt for password only on new records. # prompt for password only on new records.
self.password = await get_a_password(self.password) # can be None if CANCEL is pressed - handle, Send Password requires string
self.password = await get_a_password(self.password) or ""
site = await ux_input_text(self.site, max_len=ONE_LINE, scan_ok=True, confirm_exit=False, site = await ux_input_text(self.site, max_len=ONE_LINE, scan_ok=True, confirm_exit=False,
prompt='Website', placeholder='(optional)') prompt='Website', placeholder='(optional)')
@ -442,6 +629,8 @@ class PasswordContent(NoteContentBase):
if misc is None: if misc is None:
misc = self.misc misc = self.misc
group = await GroupPickerMenu.pick(self.group)
if self.idx != -1: if self.idx != -1:
# confirm changes, don't for new records # confirm changes, don't for new records
chgs = [] chgs = []
@ -453,6 +642,8 @@ class PasswordContent(NoteContentBase):
chgs.append('Username') chgs.append('Username')
if self.misc != misc: if self.misc != misc:
chgs.append('Other Notes') chgs.append('Other Notes')
if self.group != group:
chgs.append('Group')
if not chgs: if not chgs:
await ux_dramatic_pause('No changes.', 3) await ux_dramatic_pause('No changes.', 3)
@ -466,6 +657,7 @@ class PasswordContent(NoteContentBase):
self.user = user self.user = user
self.site = site self.site = site
self.misc = misc self.misc = misc
self.group = group
await self._save_ux(menu) await self._save_ux(menu)
return self return self
@ -473,22 +665,41 @@ class PasswordContent(NoteContentBase):
class NoteContent(NoteContentBase): class NoteContent(NoteContentBase):
# Pure "notes" have just a title and free-form text # Pure "notes" have just a title and free-form text
flds = ['title', 'misc'] flds = ['title', 'misc', 'group']
type_label = 'note' type_label = 'note'
async def make_menu(self, *a): async def _make_menu(self, readonly=False):
# Details and actions for this Note # Details and actions for this Note
return [
rv = [
MenuItem('"%s"' % self.title, f=self.view), MenuItem('"%s"' % self.title, f=self.view),
MenuItem('View Note', f=self.view), MenuItem('View Note', f=self.view),
MenuItem('Edit', f=self.edit), ]
MenuItem('Delete', f=self.delete), if not readonly:
MenuItem('Export', f=self.export), rv += [
MenuItem('Edit', f=self.edit),
MenuItem('Delete', f=self.delete),
MenuItem('Export', f=self.export),
]
rv += [
self.sign_misc_menu_item(), self.sign_misc_menu_item(),
ShortcutItem(KEY_QR, f=self.view_qr_menu, arg="misc"), ShortcutItem(KEY_QR, f=self.view_qr_menu, arg="misc"),
ShortcutItem(KEY_NFC, f=self.share_nfc, arg='misc'), ShortcutItem(KEY_NFC, f=self.share_nfc, arg='misc'),
] ]
# if misc is less than MAX_PASS_LEN and only consist of printable ASCII characters
# and current seed (master or tmp) is word based - offer to apply note text as BIP-39 passphrase
if self.is_b39pass_applicable(self.misc, readonly):
rv += [MenuItem('Apply as BIP-39 Passphrase',
f=self.apply_as_b39_pass, arg=(self.misc, readonly))]
return rv
async def make_menu(self, a, b, item):
items = await self._make_menu(readonly=item.arg)
return MenuSystem(items)
async def view(self, *a): async def view(self, *a):
ch = await ux_show_story(self.misc, title=self.title, escape=KEY_QR, ch = await ux_show_story(self.misc, title=self.title, escape=KEY_QR,
hint_icons=KEY_QR) hint_icons=KEY_QR)
@ -509,6 +720,8 @@ class NoteContent(NoteContentBase):
if misc is None: if misc is None:
misc = self.misc misc = self.misc
group = await GroupPickerMenu.pick(self.group)
if self.idx != -1: if self.idx != -1:
# confirm changes, don't for new records # confirm changes, don't for new records
chgs = [] chgs = []
@ -516,6 +729,8 @@ class NoteContent(NoteContentBase):
chgs.append('Title') chgs.append('Title')
if self.misc != misc: if self.misc != misc:
chgs.append('Note Text') chgs.append('Note Text')
if self.group != group:
chgs.append('Group')
if not chgs: if not chgs:
await ux_dramatic_pause('No changes.', 3) await ux_dramatic_pause('No changes.', 3)
@ -528,6 +743,7 @@ class NoteContent(NoteContentBase):
self.title = title self.title = title
self.misc = misc self.misc = misc
self.group = group
await self._save_ux(menu) await self._save_ux(menu)
@ -574,7 +790,7 @@ async def start_export(notes):
await needs_microsd() await needs_microsd()
return return
except Exception as e: except Exception as e:
await ux_show_story('Failed to write!\n\n\n'+str(e)) await ux_show_story('Failed to write!\n\n'+str(e))
return return
msg = 'Export file written:\n\n%s\n\nSignature file written:\n\n%s' % ( msg = 'Export file written:\n\n%s\n\nSignature file written:\n\n%s' % (
@ -616,8 +832,9 @@ async def import_from_other(menu, *a):
records = json.load(open(fn, 'rt')) records = json.load(open(fn, 'rt'))
# We have some JSON, parsed now. # We have some JSON, parsed now.
await import_from_json(records) ok = await import_from_json(records)
if not ok: return
await ux_dramatic_pause('Saved.', 3) await ux_dramatic_pause('Saved.', 3)
menu.update_contents() menu.update_contents()
@ -635,6 +852,7 @@ async def import_from_json(records):
settings.set('notes', was) settings.set('notes', was)
settings.set('secnap', True) settings.set('secnap', True)
settings.save() settings.save()
return True
except Exception as e: except Exception as e:
await ux_show_story(title="Failure", msg=str(e) + '\n\n' + problem_file_line(e)) await ux_show_story(title="Failure", msg=str(e) + '\n\n' + problem_file_line(e))

View File

@ -67,6 +67,9 @@ from utils import call_later_ms
# msas = multisig address show (do not censor multisig addresses) # msas = multisig address show (do not censor multisig addresses)
# ccc = (complex) If present, CCC feature is enabled and key details stored here. # ccc = (complex) If present, CCC feature is enabled and key details stored here.
# ktrx = (privkey) Key teleport Rx has been started, this will be our keypair # ktrx = (privkey) Key teleport Rx has been started, this will be our keypair
# sssp = (complex) If present, a (single signer) spending-policy is defined (maybe disabled)
# lfr = (string) If present, the reason why Spending Policy blocked last transaction
# wifs = (list) List of tuples (public/private key)
# Stored w/ key=00 for access before login # Stored w/ key=00 for access before login
# _skip_pin = hard code a PIN value (dangerous, only for debug) # _skip_pin = hard code a PIN value (dangerous, only for debug)
@ -90,7 +93,9 @@ KEEP_IF_BLANK_SETTINGS = ["wa", "sighshchk", "emu", "rz", "b39skip",
"axskip", "del", "pms", "idle_to", "batt_to", "axskip", "del", "pms", "idle_to", "batt_to",
"bright", "msas"] "bright", "msas"]
SEEDVAULT_FIELDS = ['seeds', 'seedvault', 'xfp', 'words', "bkpw"] # key value pairs saved directly to master seed settings
# held in RAM for tmp seed sessions
MASTER_FIELDS = ['seeds', 'seedvault', 'xfp', 'words', "bkpw", "sssp"]
NUM_SLOTS = const(100) NUM_SLOTS = const(100)
SLOTS = range(NUM_SLOTS) SLOTS = range(NUM_SLOTS)
@ -284,7 +289,7 @@ class SettingsObject:
SettingsObject.master_nvram_key = self.nvram_key SettingsObject.master_nvram_key = self.nvram_key
for fn in SEEDVAULT_FIELDS: for fn in MASTER_FIELDS:
curr = self.current.get(fn, None) curr = self.current.get(fn, None)
if curr is not None: if curr is not None:
SettingsObject.master_sv_data[fn] = curr SettingsObject.master_sv_data[fn] = curr
@ -300,7 +305,7 @@ class SettingsObject:
SettingsObject.master_sv_data.clear() SettingsObject.master_sv_data.clear()
SettingsObject.master_nvram_key = None SettingsObject.master_nvram_key = None
def master_set(self, key, value): def master_set(self, key, value, master_only=False):
# Set a value, and it must be saved under the master seed's # Set a value, and it must be saved under the master seed's
# Concern is we may be changing a setting from a tmp seed mode # Concern is we may be changing a setting from a tmp seed mode
# - always does a save # - always does a save
@ -311,6 +316,7 @@ class SettingsObject:
self.set(key, value) self.set(key, value)
self.save() self.save()
else: else:
assert not master_only
# harder, slower: have to load, change and write # harder, slower: have to load, change and write
master = SettingsObject(nvram_key=SettingsObject.master_nvram_key) master = SettingsObject(nvram_key=SettingsObject.master_nvram_key)
master.load() master.load()
@ -319,7 +325,7 @@ class SettingsObject:
del master del master
# track our copies # track our copies
if key in SEEDVAULT_FIELDS: if key in MASTER_FIELDS:
SettingsObject.master_sv_data[key] = value SettingsObject.master_sv_data[key] = value
def master_get(self, kn, default=None): def master_get(self, kn, default=None):
@ -331,7 +337,7 @@ class SettingsObject:
return self.get(kn, default) return self.get(kn, default)
# LIMITATION: only supporting a few values we know we will need # LIMITATION: only supporting a few values we know we will need
assert kn in SEEDVAULT_FIELDS assert kn in MASTER_FIELDS
res = SettingsObject.master_sv_data.get(kn, default) res = SettingsObject.master_sv_data.get(kn, default)
if res is None: if res is None:
return default return default

View File

@ -2,17 +2,18 @@
# #
# ownership.py - store a cache of hashes related to addresses we might control. # ownership.py - store a cache of hashes related to addresses we might control.
# #
import os, sys, chains, ngu, struct, version import os, chains, ngu, struct, version
from glob import settings from glob import settings
from ucollections import namedtuple from ucollections import namedtuple
from ubinascii import hexlify as b2a_hex from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from exceptions import UnknownAddressExplained from exceptions import UnknownAddressExplained
from utils import problem_file_line, show_single_address from utils import problem_file_line, show_single_address, validate_own_address
from public_constants import AFC_SCRIPT, AF_P2WPKH_P2SH, AF_P2SH, AF_P2WSH_P2SH, AF_P2TR, AF_P2WSH
# Track many addresses, but in compressed form # Track many addresses, but in compressed form
# - map from random Bech32/Base58 payment address to (wallet) + keypath # - map from random Bech32/Base58 payment address to (wallet) + keypath
# - only normal (external, not change) addresses, and won't consider # - won't consider any keypath that does not end in <0;1>/*
# any keypath that does not end in 0/*
# - store only hints, since we can re-construct any address and want to fully verify # - store only hints, since we can re-construct any address and want to fully verify
# - try to keep private between different duress wallets, and seed vaults # - try to keep private between different duress wallets, and seed vaults
# - storing bulk data into LFS, not settings # - storing bulk data into LFS, not settings
@ -39,7 +40,7 @@ OWNERSHIP_MAGIC = 0x10A0 # "Address Ownership" v1.0
# target 3 flash blocks, max file size => 764 addresses # target 3 flash blocks, max file size => 764 addresses
MAX_ADDRS_STORED = const(764) # =((3*512) - OWNERSHIP_FILE_HDR_LEN) // HASH_ENC_LEN MAX_ADDRS_STORED = const(764) # =((3*512) - OWNERSHIP_FILE_HDR_LEN) // HASH_ENC_LEN
BONUS_GAP_LIMIT = const(20) BONUS_AFTER_MATCH = const(20) # number of addresses to still generate after match found
def encode_addr(addr, salt): def encode_addr(addr, salt):
# Convert text address to something we can store while preserving privacy. # Convert text address to something we can store while preserving privacy.
@ -56,6 +57,7 @@ class AddressCacheFile:
self.salt = h[32:] self.salt = h[32:]
self.count = 0 self.count = 0
self.hdr = None self.hdr = None
self.fd = None
self.peek() self.peek()
@ -65,9 +67,6 @@ class AddressCacheFile:
rv += ' (change)' rv += ' (change)'
return rv return rv
def exists(self):
return bool(self.count)
def peek(self): def peek(self):
# see what we have on-disk; just reads header. # see what we have on-disk; just reads header.
try: try:
@ -105,15 +104,14 @@ class AddressCacheFile:
self.fd.write(hdr) self.fd.write(hdr)
def append(self, addr): def append(self, addr):
if addr is None:
# close file, done
self.fd.close()
del self.fd
return
assert '_' not in addr
self.fd.write(encode_addr(addr, self.salt)) self.fd.write(encode_addr(addr, self.salt))
def close(self):
# close file, done
if self.fd is not None:
self.fd.close()
self.fd = None
def fast_search(self, addr): def fast_search(self, addr):
# Do the easy part of the searching, using the existing file's contents. # Do the easy part of the searching, using the existing file's contents.
# - generates candidate path subcomponents; might be false positive # - generates candidate path subcomponents; might be false positive
@ -121,6 +119,7 @@ class AddressCacheFile:
from glob import dis from glob import dis
if not self.hdr or not self.count: if not self.hdr or not self.count:
# cache empty
return return
with open(self.fname, 'rb') as fd: with open(self.fname, 'rb') as fd:
@ -132,7 +131,7 @@ class AddressCacheFile:
chk = encode_addr(addr, self.salt) chk = encode_addr(addr, self.salt)
for idx in range(self.count): for idx in range(self.count):
if buf[idx*HASH_ENC_LEN : (idx*HASH_ENC_LEN)+HASH_ENC_LEN] == chk: if buf[idx*HASH_ENC_LEN : (idx*HASH_ENC_LEN)+HASH_ENC_LEN] == chk:
yield (self.change_idx, idx) yield self.change_idx, idx
dis.progress_sofar(idx, self.count) dis.progress_sofar(idx, self.count)
@ -148,92 +147,106 @@ class AddressCacheFile:
# - return subpath for a hit or None # - return subpath for a hit or None
from glob import dis from glob import dis
bonus = 0
match = None match = None
start_idx = self.count start_idx = self.count
count = MAX_ADDRS_STORED - start_idx count = MAX_ADDRS_STORED - start_idx
if count <= 0: if count <= 0:
return None return match
self.setup(self.change_idx, start_idx) self.setup(self.change_idx, start_idx)
bonus = None
for idx,here,*_ in self.wallet.yield_addresses(start_idx, count, for idx,here,*_ in self.wallet.yield_addresses(start_idx, count,
change_idx=self.change_idx): change_idx=self.change_idx):
if here == addr:
# Found it! But keep going a little for next time.
match = (self.change_idx, idx)
self.append(here) self.append(here)
self.count += 1 self.count += 1
if match:
if bonus:
if bonus >= BONUS_AFTER_MATCH:
# do (at most) 20 more - limited by 'start_idx' & 'count'
break
bonus += 1 bonus += 1
if match and bonus >= BONUS_GAP_LIMIT:
self.append(None)
return match
dis.progress_sofar(idx-start_idx, count) if here == addr:
# match but keep going
match = (self.change_idx, idx)
bonus = 1
self.append(None) dis.progress_sofar(idx - start_idx, count)
return None self.close()
return match
class OwnershipCache: class OwnershipCache:
@classmethod @classmethod
def saver(cls, wallet, change_idx, start_idx): def saver(cls, wallet, change_idx, start_idx, count):
# when we are generating many addresses for export, capture them # when we are generating many addresses for export, capture them (if suitable)
# as we go with this function # as we go with this function
# - not change -- only main addrs if not count:
return
if change_idx not in (0, 1):
return
if start_idx >= MAX_ADDRS_STORED:
return
file = AddressCacheFile(wallet, change_idx) file = AddressCacheFile(wallet, change_idx)
current_pos = file.count
if file.exists(): if start_idx > current_pos:
# don't save to existing file, has some already # nothing to do here, we are missing some addresses in the middle
return None return
if (start_idx + count) <= current_pos:
# we already have all these addresses
return
try: file.setup(change_idx, current_pos)
file.setup(change_idx, start_idx)
except:
# in some cases we don't want to save anything, not an error
return None
return file.append def doit(addr, idx):
if addr is None:
file.close()
elif (idx < MAX_ADDRS_STORED) and idx >= current_pos:
file.append(addr)
return doit
@classmethod @classmethod
def search(cls, addr): def filter(cls, addr_fmt, args):
# Find it! # Filter possible candidates!
# - returns wallet object, and tuple2 of final 2 subpath components
# - if you start w/ testnet, we'll follow that # - if you start w/ testnet, we'll follow that
from multisig import MultisigWallet from multisig import MultisigWallet
from public_constants import AFC_SCRIPT, AF_P2WPKH_P2SH, AF_P2SH, AF_P2WSH_P2SH
from glob import dis
ch = chains.current_chain() args = args or {}
addr_fmt = ch.possible_address_fmt(addr) # user has specified specific (named) wallet
if not addr_fmt: named_wal = args.get("wallet", None)
# might be valid address over on testnet vs mainnet if named_wal:
raise UnknownAddressExplained('That address is not valid on ' + ch.name) # quick search without deserialization
res = list(MultisigWallet.iter_wallets(name=named_wal))
if not res:
raise UnknownAddressExplained("Wallet '%s' not defined." % named_wal)
# only return desired named wallet, no other wallets are searched
return res
possibles = [] possibles = []
if addr_fmt & AFC_SCRIPT: if addr_fmt & AFC_SCRIPT:
# multisig or script at least.. must exist already # multisig or script at least... must exist already
possibles.extend(MultisigWallet.iter_wallets(addr_fmt=addr_fmt)) afs = [addr_fmt]
if addr_fmt == AF_P2SH: if addr_fmt == AF_P2SH:
# might look like P2SH but actually be AF_P2WSH_P2SH # might look like P2SH but actually be AF_P2WSH_P2SH
possibles.extend(MultisigWallet.iter_wallets(addr_fmt=AF_P2WSH_P2SH)) # wrapped segwit is more used than legacy
afs = [AF_P2WSH_P2SH, AF_P2SH]
# Might be single-sig p2wpkh wrapped in p2sh ... but that was a transition # Might be single-sig p2wpkh wrapped in p2sh ... but that was a transition
# thing that hopefully is going away, so if they have any multisig wallets, # thing that hopefully is going away, so if they have any multisig wallets,
# defined, assume that that's the only p2sh address source. # defined, assume that that's the only p2sh address source.
addr_fmt = AF_P2WPKH_P2SH addr_fmt = AF_P2WPKH_P2SH
# TODO: add tapscript and such fancy stuff here possibles.extend(MultisigWallet.iter_wallets(addr_fmts=afs))
try: try:
# Construct possible single-signer wallets, always at least account=0 case # Construct possible single-signer wallets, always at least account=0 case
@ -247,63 +260,97 @@ class OwnershipCache:
if af == addr_fmt and acct_num: if af == addr_fmt and acct_num:
w = MasterSingleSigWallet(addr_fmt, account_idx=acct_num) w = MasterSingleSigWallet(addr_fmt, account_idx=acct_num)
possibles.append(w) possibles.append(w)
except (KeyError, ValueError): pass # if not single sig address format except (KeyError, ValueError):
pass # if not single sig address format
if not possibles: if not possibles:
# can only happen w/ scripts; for single-signer we have things to check # can only happen w/ scripts; for single-signer we have things to check
raise UnknownAddressExplained( raise UnknownAddressExplained(
"No suitable multisig wallets are currently defined.") "No suitable multisig wallets are currently defined.")
# ordering here
return possibles
@classmethod
def search_wallet_cache(cls, addr, cf):
# - returns wallet object, and tuple2 of final 2 subpath components
# "quick" check first, before doing any generations # "quick" check first, before doing any generations
# external chain first, then internal (change)
for maybe in cf.fast_search(addr):
ok = cf.check_match(addr, maybe)
if ok:
return cf.wallet, maybe
return None, None
count = 0
phase2 = []
for change_idx in (0, 1):
files = [AddressCacheFile(w, change_idx) for w in possibles]
for f in files:
if dis.has_lcd:
dis.fullscreen('Searching wallet(s)...', line2=f.nice_name())
else:
dis.fullscreen('Searching...')
for maybe in f.fast_search(addr):
ok = f.check_match(addr, maybe)
if not ok: continue # false positive - will happen
# found winner.
return f.wallet, maybe
if f.count < MAX_ADDRS_STORED:
phase2.append(f)
count += f.count
@classmethod
def search_build_wallet(cls, addr, cf):
# maybe we haven't calculated all the addresses yet, so do that # maybe we haven't calculated all the addresses yet, so do that
# - very slow, but only needed once; any negative (failed) search causes this # - very slow, but only needed once; any negative (failed) search causes this
# - could stop when match found, but we go a bit beyond that for next time # - could stop when match found, but we go a bit beyond that for next time
# - we could search all in parallel, rather than serially because # - we could search all in parallel, rather than serially because
# more likely to find a match with low index... but seen as too much memory # more likely to find a match with low index... but seen as too much memory
result = cf.build_and_search(addr)
for f in phase2: if result:
b4 = f.count # found it, so report it and stop
if dis.has_lcd: return cf.wallet, result
dis.fullscreen("Generating addresses...", line2=f.nice_name())
else:
dis.fullscreen("Generating...")
result = f.build_and_search(addr)
if result:
# found it, so report it and stop
return f.wallet, result
count += f.count - b4
# possible phase 3: other seedvault... slow, rare and not implemented # possible phase 3: other seedvault... slow, rare and not implemented
return None, None
raise UnknownAddressExplained('Searched %d candidates without finding a match.' % count)
@classmethod @classmethod
async def search_ux(cls, addr): def search(cls, addr, args=None):
from glob import dis
dis.fullscreen("Wait...")
try:
addr, addr_fmt = validate_own_address(addr)
except Exception as e:
raise UnknownAddressExplained('That address is not valid on ' + e.args[0])
matches = OWNERSHIP.filter(addr_fmt, args)
# build cache files for both external & internal chain
cachefs = []
for w in matches:
cachefs.append(AddressCacheFile(w, 0))
cachefs.append(AddressCacheFile(w, 1))
for cf in cachefs:
msg = "Searching wallet(s)..." if dis.has_lcd else "Searching..."
dis.fullscreen(msg, line2=cf.nice_name())
wallet, subpath = OWNERSHIP.search_wallet_cache(addr, cf)
if wallet:
# first arg from_cache=True
return True, wallet, subpath
# nothing found in existing cache files
c = 0
for cf in cachefs:
msg = "Generating addresses..." if dis.has_lcd else "Generating..."
dis.fullscreen(msg, line2=cf.nice_name())
wallet, subpath = OWNERSHIP.search_build_wallet(addr, cf)
c += cf.count
if wallet:
# first arg from_cache=False
return False, wallet, subpath
# nothing found among singlesig & registered multisig wallets
# check WIF store (single sig only)
if addr_fmt not in [AF_P2TR, AF_P2WSH]:
dis.fullscreen("WIF Store...")
from wif import iter_wif_store_addresses
target_af = AF_P2WPKH_P2SH if addr_fmt == AF_P2SH else addr_fmt
for i, store_addr in iter_wif_store_addresses(target_af):
if store_addr == addr:
return False, ("wif", target_af), i+1
raise UnknownAddressExplained('Searched %d candidate addresses in %d wallet(s)'
' without finding a match.' % (c, len(matches)))
@classmethod
async def search_ux(cls, addr, args):
# Provide a simple UX. Called functions do fullscreen, progress bar stuff. # Provide a simple UX. Called functions do fullscreen, progress bar stuff.
from ux import ux_show_story, show_qr_code from ux import ux_show_story, show_qr_code
from charcodes import KEY_QR from charcodes import KEY_QR
@ -311,25 +358,28 @@ class OwnershipCache:
from public_constants import AFC_BECH32, AFC_BECH32M from public_constants import AFC_BECH32, AFC_BECH32M
try: try:
wallet, subpath = OWNERSHIP.search(addr) _, wallet, subpath = cls.search(addr, args)
is_ms = isinstance(wallet, MultisigWallet) is_ms = isinstance(wallet, MultisigWallet)
sp = wallet.render_path(*subpath)
msg = show_single_address(addr) msg = show_single_address(addr)
msg += '\n\nFound in wallet:\n ' + wallet.name esc = ""
msg += '\nDerivation path:\n ' + sp if isinstance(wallet, tuple) and (wallet[0] == "wif"):
if is_ms: msg += '\n\nFound in WIF store at index %d' % subpath
esc = "" addr_fmt = wallet[1]
else: else:
esc = "0" sp = wallet.render_path(*subpath)
msg += "\n\nPress (0) to sign message with this key." msg += '\n\nFound in wallet:\n ' + wallet.name
msg += '\nDerivation path:\n ' + sp
addr_fmt = wallet.addr_fmt
if not is_ms:
esc = "0"
msg += "\n\nPress (0) to sign message with this key."
title = "Verified" title = "Verified"
if version.has_qwerty: if version.has_qwerty:
esc += KEY_QR esc += KEY_QR
title += " Address" title += " Address"
else: else:
msg += ' (1) for address QR' msg += ' Press (1) for address QR.'
esc += '1' esc += '1'
title += "!" title += "!"
@ -337,10 +387,10 @@ class OwnershipCache:
ch = await ux_show_story(msg, title=title, escape=esc, hint_icons=KEY_QR) ch = await ux_show_story(msg, title=title, escape=esc, hint_icons=KEY_QR)
if ch in ("1"+KEY_QR): if ch in ("1"+KEY_QR):
await show_qr_code(addr, msg=addr, is_addrs=True, await show_qr_code(addr, msg=addr, is_addrs=True,
is_alnum=(wallet.addr_fmt & (AFC_BECH32 | AFC_BECH32M))) is_alnum=(addr_fmt & (AFC_BECH32 | AFC_BECH32M)))
elif not is_ms and (ch == "0"): # only singlesig elif not is_ms and (ch == "0"): # only singlesig
from msgsign import sign_with_own_address from msgsign import sign_with_own_address
await sign_with_own_address(sp, wallet.addr_fmt) await sign_with_own_address(sp, addr_fmt)
else: else:
break break

View File

@ -179,7 +179,7 @@ class PaperWalletMaker:
return return
except Exception as e: except Exception as e:
from utils import problem_file_line from utils import problem_file_line
await ux_show_story('Failed to write!\n\n\n'+problem_file_line(e)) await ux_show_story('Failed to write!\n\n'+problem_file_line(e))
return return
story = "Done! Created file(s):\n\n%s" % nice_txt story = "Done! Created file(s):\n\n%s" % nice_txt

View File

@ -3,8 +3,7 @@
# pincodes.py - manage PIN code (which map to wallet seeds) # pincodes.py - manage PIN code (which map to wallet seeds)
# #
import ustruct, ckcc, version, chains, stash import ustruct, ckcc, version, chains, stash
# from ubinascii import hexlify as b2a_hex from callgate import enter_dfu, get_is_bricked
from callgate import enter_dfu
from bip39 import wordlist_en from bip39 import wordlist_en
# See ../stm32/bootloader/pins.h for source of these constants. # See ../stm32/bootloader/pins.h for source of these constants.
@ -127,17 +126,14 @@ class PinAttempt:
self.private_state = 0 # opaque data, but preserve self.private_state = 0 # opaque data, but preserve
self.cached_main_pin = bytearray(32) self.cached_main_pin = bytearray(32)
# If set, a spending policy is in effect, and so even tho we know the master
# seed, we are not going to let them see it, nor sign things we dont like, etc.
self.hobbled_mode = False
assert MAX_PIN_LEN == 32 # update FMT otherwise #assert MAX_PIN_LEN == 32 # update FMT otherwise
assert ustruct.calcsize(PIN_ATTEMPT_FMT_V1) == PIN_ATTEMPT_SIZE_V1 #assert ustruct.calcsize(PIN_ATTEMPT_FMT_V1) == PIN_ATTEMPT_SIZE_V1
assert ustruct.calcsize(PIN_ATTEMPT_FMT_V2_ADDITIONS) == PIN_ATTEMPT_SIZE - PIN_ATTEMPT_SIZE_V1 #assert ustruct.calcsize(PIN_ATTEMPT_FMT_V2_ADDITIONS) \
# == PIN_ATTEMPT_SIZE - PIN_ATTEMPT_SIZE_V1
# check for bricked system early
import callgate
if callgate.get_is_bricked():
# die right away if it's not going to work
print("SE bricked")
callgate.enter_dfu(3)
def __repr__(self): def __repr__(self):
return '<PinAttempt: fails/left=%d/%d tc_flag/arg=0x%x/0x%x>' % ( return '<PinAttempt: fails/left=%d/%d tc_flag/arg=0x%x/0x%x>' % (
@ -339,10 +335,6 @@ class PinAttempt:
return self.state_flags return self.state_flags
def delay(self):
# obsolete since Mk3, but called from login.py
self.roundtrip(1)
def login(self): def login(self):
# test we have the PIN code right, and unlock access if so. # test we have the PIN code right, and unlock access if so.
chk = self.roundtrip(2) chk = self.roundtrip(2)
@ -533,10 +525,24 @@ class PinAttempt:
from trick_pins import TC_DELTA_MODE from trick_pins import TC_DELTA_MODE
return bool(self.delay_required & TC_DELTA_MODE) return bool(self.delay_required & TC_DELTA_MODE)
def get_tc_values(self): def get_tc_values(self):
# Mk4 only # Mk4 only
# return (tc_flags, tc_arg) # return (tc_flags, tc_arg)
return self.delay_required, self.delay_achieved return self.delay_required, self.delay_achieved
@staticmethod
async def enforce_brick():
# check for bricked system early
if get_is_bricked():
try:
# regardless of settings, become a forever calculator after brickage.
while version.has_qwerty:
from calc import login_repl
await login_repl()
finally:
# die right away if it's not going to work
enter_dfu(3)
# singleton # singleton

File diff suppressed because it is too large Load Diff

View File

@ -25,10 +25,6 @@ class PSRAMWrapper:
return memoryview(self._wr)[offset:offset+ln] return memoryview(self._wr)[offset:offset+ln]
def is_at(self, ptr, offset):
# is bytes() object really one we created at read_at
return uctypes.addressof(ptr) == self.base+offset
# Be compatible with SPIFlash class... # Be compatible with SPIFlash class...
def read(self, address, buf, cmd=None): def read(self, address, buf, cmd=None):

View File

@ -5,6 +5,7 @@
import framebuf, uqr import framebuf, uqr
from ux import UserInteraction, ux_wait_keyup, the_ux from ux import UserInteraction, ux_wait_keyup, the_ux
from version import has_qwerty from version import has_qwerty
from exceptions import QRTooBigError
from charcodes import (KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_NFC, from charcodes import (KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_NFC,
KEY_END, KEY_ENTER, KEY_CANCEL) KEY_END, KEY_ENTER, KEY_CANCEL)
@ -18,7 +19,7 @@ class QRDisplaySingle(UserInteraction):
def __init__(self, addrs, is_alnum, start_n=0, sidebar=None, msg=None, def __init__(self, addrs, is_alnum, start_n=0, sidebar=None, msg=None,
is_addrs=False, force_msg=False, allow_nfc=True, is_secret=False, is_addrs=False, force_msg=False, allow_nfc=True, is_secret=False,
change_idxs=None): change_idxs=None, can_raise=True, qr_msgs=None, no_index=None):
self.is_alnum = is_alnum self.is_alnum = is_alnum
self.idx = 0 # start with first address self.idx = 0 # start with first address
self.invert = False # looks better, but neither mode is ideal self.invert = False # looks better, but neither mode is ideal
@ -33,6 +34,9 @@ class QRDisplaySingle(UserInteraction):
# only used for NFC sharing secret material - full chip wipe if is_secret=True # only used for NFC sharing secret material - full chip wipe if is_secret=True
self.is_secret = is_secret self.is_secret = is_secret
self.change_idxs = change_idxs or [] self.change_idxs = change_idxs or []
self.can_raise = can_raise
self.qr_msgs = qr_msgs
self.no_index = no_index
def calc_qr(self, msg): def calc_qr(self, msg):
# Version 2 would be nice, but can't hold what we need, even at min error correction, # Version 2 would be nice, but can't hold what we need, even at min error correction,
@ -61,12 +65,20 @@ class QRDisplaySingle(UserInteraction):
# draw_qr_display takes this and renders hint in the top right corner # draw_qr_display takes this and renders hint in the top right corner
# this member function decides what type of hint will be shown # this member function decides what type of hint will be shown
# numbers, letters, etc. # numbers, letters, etc.
if self.no_index:
return None
return str(self.start_n + self.idx) if len(self.addrs) > 1 else None return str(self.start_n + self.idx) if len(self.addrs) > 1 else None
def is_change(self): def side_msg(self):
if self.idx in self.change_idxs: if self.idx in self.change_idxs:
return True return "CHANGE BACK"
return False
elif self.qr_msgs:
try:
return self.qr_msgs[self.idx]
except IndexError: pass
return None
def redraw(self): def redraw(self):
# Redraw screen. # Redraw screen.
@ -75,6 +87,15 @@ class QRDisplaySingle(UserInteraction):
# what we are showing inside the QR # what we are showing inside the QR
body = self.addrs[self.idx] body = self.addrs[self.idx]
idx_hint = self.idx_hint()
msg = None
if self.msg:
msg = self.msg
else:
if isinstance(body, str):
# sanity check
msg = body
# make the QR, if needed. # make the QR, if needed.
if not self.qr_data: if not self.qr_data:
@ -83,23 +104,19 @@ class QRDisplaySingle(UserInteraction):
self.calc_qr(body) self.calc_qr(body)
except Exception: except Exception:
dis.busy_bar(False) dis.busy_bar(False)
raise if not self.can_raise:
dis.draw_qr_error(idx_hint, msg)
return
# other code paths require raise to switch to BBQr
raise QRTooBigError
# draw display # draw display
dis.busy_bar(False) dis.busy_bar(False)
if self.msg:
msg = self.msg
else:
msg = None
if isinstance(body, str):
# sanity check
msg = body
dis.draw_qr_display(self.qr_data, msg, self.is_alnum, dis.draw_qr_display(self.qr_data, msg, self.is_alnum,
self.sidebar, self.idx_hint(), self.invert, self.sidebar, idx_hint, self.invert,
is_addr=self.is_addrs, force_msg=self.force_msg, is_addr=self.is_addrs, force_msg=self.force_msg,
is_change=self.is_change()) side_msg=self.side_msg())
async def interact_bare(self): async def interact_bare(self):
from glob import NFC, dis from glob import NFC, dis

View File

@ -57,9 +57,8 @@ SLOW_BAUD = const(9600)
FAST_BAUD = const(57600) FAST_BAUD = const(57600)
RX_BUF_SIZE = const(4350) # big enough for full v40 decoded RX_BUF_SIZE = const(4350) # big enough for full v40 decoded
# TODO: constructor should leave it in reset for simple lower-power usage; then after # TODO: constructor should avoid full setup until after login; after setup,
# login we can do full setup (2+ seconds) and then sleep again until needed. # command sleep is the known low-power state.
class QRScanner: class QRScanner:
def __init__(self): def __init__(self):
@ -68,6 +67,8 @@ class QRScanner:
self.scan_light = False # is light on during scanning? self.scan_light = False # is light on during scanning?
self.version = None self.version = None
self.setup_done = False self.setup_done = False
self.needs_reinit = False
self.sleep_seq = 0
# hodl this lock when communicating w/ QR scanner # hodl this lock when communicating w/ QR scanner
self.lock = asyncio.Lock() self.lock = asyncio.Lock()
@ -84,16 +85,21 @@ class QRScanner:
# setup hardware, reset scanner and return time to delay until ready # setup hardware, reset scanner and return time to delay until ready
from machine import UART, Pin from machine import UART, Pin
self.serial = UART(2, SLOW_BAUD, rxbuf=RX_BUF_SIZE) self.serial = UART(2, SLOW_BAUD, rxbuf=RX_BUF_SIZE)
self.reset = Pin('QR_RESET', Pin.OUT_OD, value=0) self.reset = Pin('QR_RESET', Pin.OUT_OD, value=1)
self.trigger = Pin('QR_TRIG', Pin.OUT_OD, value=1) # wasn't needed self.trigger = Pin('QR_TRIG', Pin.OUT_OD, value=1) # wasn't needed
# NOTE: reset is active low (open drain) self.pulse_reset()
# needs full 2 seconds of recovery time after reset
return 2
def pulse_reset(self):
# RESET is active low (open drain). Keep it as a pulse; module docs
# describe low on this pin as wake-up, so don't use it as parking state.
self.reset(0) self.reset(0)
utime.sleep_ms(10) utime.sleep_ms(10)
self.reset(1) self.reset(1)
self.needs_reinit = False
# needs full 2 seconds of recovery time
return 2
def set_baud(self, br): def set_baud(self, br):
# change serial port baud rate # change serial port baud rate
@ -118,56 +124,104 @@ class QRScanner:
async def setup_task(self, start_delay): async def setup_task(self, start_delay):
# Task to setup device, and then die. # Task to setup device, and then die.
await asyncio.sleep(start_delay) async with self.lock:
for attempt in range(3):
await asyncio.sleep(start_delay)
async with self.lock: try:
await self._configure()
except Exception:
# a step failed or timed out (would have left scanner dead
# until next boot); reset module and start over
await self.blind_shutdown()
if attempt == 2:
break
start_delay = self.reset_stream()
continue
# might need to repeat a few time to get into right state self.setup_done = True
await self.goto_sleep()
return
self.mark_needs_reinit()
def reset_stream(self):
self.sleep_seq += 1
start_delay = self.hardware_setup()
self.stream = asyncio.StreamReader(self.serial, {})
return start_delay
def mark_needs_reinit(self):
self.setup_done = False
self.version = None
self.needs_reinit = True
if hasattr(self, 'reset'):
self.reset(1)
async def blind_shutdown(self):
for baud in (SLOW_BAUD, FAST_BAUD):
self.set_baud(baud)
await self.tx('S_CMD_020D') # return to "Command mode"
await asyncio.sleep_ms(20)
await self.tx('S_CMD_03L0') # turn off bright light
await asyncio.sleep_ms(20)
await self.tx('SRDF0050') # sleep scanner
await asyncio.sleep_ms(150)
await self.tx('SRDF0050')
await asyncio.sleep_ms(20)
async def _configure(self):
# full config sequence; any step may raise on timeout/framing error
# might need to repeat a few time to get into right state
for retry in range(5):
baud = await self.probe_baud()
if baud: break
else:
#print("QR Scanner: missing")
raise RuntimeError('no contact')
try:
await self.txrx('S_CMD_FFFF') # factory reset of settings
except RuntimeError:
await asyncio.sleep_ms(1000)
for retry in range(5): for retry in range(5):
baud = await self.probe_baud() baud = await self.probe_baud()
if baud: break if baud: break
else: else:
#print("QR Scanner: missing") raise RuntimeError('no contact after S_CMD_FFFF')
return
await self.txrx('S_CMD_FFFF') # factory reset of settings # go to high speed!
if baud != FAST_BAUD:
await self.txrx('S_CMD_H3BR%d' % FAST_BAUD)
self.set_baud(FAST_BAUD)
# go to high speed! # configure it like we want it
if baud != FAST_BAUD: await self.txrx('S_CMD_MTRS5000') # 5s to read before fail (unused)
await self.txrx('S_CMD_H3BR%d' % FAST_BAUD) await self.txrx('S_CMD_MT11') # trigger is edge-based (not level)
self.set_baud(FAST_BAUD) await self.txrx('S_CMD_MT30') # Same code reading without delay
await self.txrx('S_CMD_MT20') # Enable automatic sleep when idle
await self.txrx('S_CMD_MTRF500') # Idle time: 500ms
await self.txrx('S_CMD_059A') # add CR LF after QR data (important)
await self.txrx('S_CMD_03L0') # light off all the time by default
await self.txrx('S_CMD_0407') # turn on signal for our yellow led
# configure it like we want it # settings under continuous scan mode
await self.txrx('S_CMD_MTRS5000') # 5s to read before fail (unused) await self.txrx('S_CMD_MARS0000') # "Modify the duration of single code reading" (ms)
await self.txrx('S_CMD_MT11') # trigger is edge-based (not level) await self.txrx('S_CMD_MARR000') # "Modify the time of the reading interval 0ms"
await self.txrx('S_CMD_MT30') # Same code reading without delay await self.txrx('S_CMD_MA31') # Enable "Same code reading delay"
await self.txrx('S_CMD_MT20') # Enable automatic sleep when idle await self.txrx('S_CMD_MARI0050') # "Modify the same code reading delay 50ms"
await self.txrx('S_CMD_MTRF500') # Idle time: 500ms
await self.txrx('S_CMD_059A') # add CR LF after QR data (important)
await self.txrx('S_CMD_03L0') # light off all the time by default
await self.txrx('S_CMD_0407') # turn on signal for our yellow led
# settings under continuous scan mode # these aren't useful (yet?) and just make things harder to decode.
await self.txrx('S_CMD_MARS0000') # "Modify the duration of single code reading" (ms) #await self.txrx('S_CMD_05F1') # add all information on
await self.txrx('S_CMD_MARR000') # "Modify the time of the reading interval 0ms" #await self.txrx('S_CMD_05L1') # output decoding length info on
await self.txrx('S_CMD_MA31') # Enable "Same code reading delay" #await self.txrx('S_CMD_05S1') # STX start char
await self.txrx('S_CMD_MARI0050') # "Modify the same code reading delay 50ms" #await self.txrx('S_CMD_05C1') # CodeID+prefix
#await self.txrx('S_CMD_0501') # prefix on
#await self.txrx('S_CMD_0506') # suffix
#await self.txrx('S_CMD_05D0') # tx total data
# these aren't useful (yet?) and just make things harder to decode. # prevent scanning magic QR to affect settings
#await self.txrx('S_CMD_05F1') # add all information on await self.txrx('S_CMD_0000') # close setting codes
#await self.txrx('S_CMD_05L1') # output decoding length info on
#await self.txrx('S_CMD_05S1') # STX start char
#await self.txrx('S_CMD_05C1') # CodeID+prefix
#await self.txrx('S_CMD_0501') # prefix on
#await self.txrx('S_CMD_0506') # suffix
#await self.txrx('S_CMD_05D0') # tx total data
# prevent scanning magic QR to affect settings
await self.txrx('S_CMD_0000') # close setting codes
self.setup_done = True
await self.goto_sleep()
async def scan_once(self): async def scan_once(self):
# Blocks until something is scanned. Returns it as string # Blocks until something is scanned. Returns it as string
@ -176,6 +230,16 @@ class QRScanner:
# - returns a BBQr object at that point # - returns a BBQr object at that point
self.scan_light = False self.scan_light = False
if self.needs_reinit:
try:
await self.setup_task(self.reset_stream())
if self.setup_done:
await asyncio.sleep_ms(200)
except asyncio.CancelledError:
await self.blind_shutdown()
self.mark_needs_reinit()
return None
# wait for reset process to complete (can be an issue right after boot) # wait for reset process to complete (can be an issue right after boot)
# - few seconds of boot time needed # - few seconds of boot time needed
for retry in range(10): for retry in range(10):
@ -211,19 +275,22 @@ class QRScanner:
finally: finally:
# Problem: another valid scan can come in just as we are trying # Problem: another valid scan can come in just as we are trying
# to get out of scanner mode # to get out of scanner mode
for retry in range(10): for retry in range(3):
try: try:
await self.txrx('S_CMD_020D') # return to "Command mode" await self.txrx('S_CMD_020D', timeout=1000) # return to "Command mode"
await self.txrx('S_CMD_03L0') # turn off bright light await self.txrx('S_CMD_03L0', timeout=1000) # turn off bright light
#print('rest after %d retries' % retry) #print('rest after %d retries' % retry)
break break
except: pass except Exception:
await asyncio.sleep_ms(25) pass
await asyncio.sleep_ms(50)
else: else:
pass
#print('reset failed') #print('reset failed')
await self.blind_shutdown()
self.mark_needs_reinit()
await self.goto_sleep() if self.setup_done:
await self.goto_sleep()
self.busy_scanning = False self.busy_scanning = False
# return BBQr object or string if simple QR # return BBQr object or string if simple QR
@ -254,13 +321,14 @@ class QRScanner:
# send specific command until it responds # send specific command until it responds
# - it will wake on any command, but not instant # - it will wake on any command, but not instant
# - first one seems to fail 100% # - first one seems to fail 100%
self.sleep_seq += 1
await self.tx('SRDF0051') # blindly at first await self.tx('SRDF0051') # blindly at first
for retry in range(5): for retry in range(5):
try: try:
await self.txrx('SRDF0051', timeout=50) # 50 ok, 20 too short await self.txrx('SRDF0051', timeout=50) # 50 ok, 20 too short
return return
except: except Exception:
# first try usually fails, that's okay... its asleep and groggy # first try usually fails, that's okay... its asleep and groggy
pass pass
@ -270,9 +338,13 @@ class QRScanner:
# - need blind retries here # - need blind retries here
# - might be two layers of sleep, and we need this second command after the first # - might be two layers of sleep, and we need this second command after the first
# - helps to turn off the yellow LED, and save power as well # - helps to turn off the yellow LED, and save power as well
self.sleep_seq += 1
sleep_seq = self.sleep_seq
await self.tx('SRDF0050') await self.tx('SRDF0050')
async def later(): async def later():
await asyncio.sleep_ms(150) await asyncio.sleep_ms(150)
if sleep_seq != self.sleep_seq or self.busy_scanning:
return
await self.tx('SRDF0050') await self.tx('SRDF0050')
asyncio.create_task(later()) asyncio.create_task(later())
@ -290,6 +362,22 @@ class QRScanner:
#print('tx >> ' + msg) #print('tx >> ' + msg)
self.serial.write(msg) self.serial.write(msg)
async def readexactly_timeout(self, num, timeout, msg=None):
# Avoid asyncio.wait_for_ms here: it can leave the scanner setup task
# stuck after a CancelledError. Convert scanner silence into a normal
# retryable command failure instead.
if timeout is None:
return await self.stream.readexactly(num)
start = utime.ticks_ms()
while self.stream.s.any() < num:
if utime.ticks_diff(utime.ticks_ms(), start) >= timeout:
#print("no rx after %s" % msg)
raise RuntimeError
await asyncio.sleep_ms(5)
return await self.stream.readexactly(num)
async def txrx(self, msg, timeout=250): async def txrx(self, msg, timeout=250):
# Send a command, get the corresponding response. # Send a command, get the corresponding response.
# - has a long timeout, collects rx based on framing # - has a long timeout, collects rx based on framing
@ -310,13 +398,8 @@ class QRScanner:
expect = LEN_OKAY expect = LEN_OKAY
rx = b'' rx = b''
while 1: while 1:
try: rx += await self.readexactly_timeout(expect, timeout, msg)
rx += await asyncio.wait_for_ms(self.stream.readexactly(expect), timeout)
except asyncio.TimeoutError:
if timeout is None:
continue
#print("no rx after %s" % msg)
raise RuntimeError
#print('txrx << ' + B2A(rx)) #print('txrx << ' + B2A(rx))

View File

@ -33,6 +33,9 @@ from ucollections import namedtuple
# seed words lengths we support: 24=>256 bits, and recommended # seed words lengths we support: 24=>256 bits, and recommended
VALID_LENGTHS = (24, 18, 12) VALID_LENGTHS = (24, 18, 12)
# maximum length for BIP-39 passphrase
MAX_PASS_LEN = 100
# bit flag that means "also include bare prefix as a valid word" # bit flag that means "also include bare prefix as a valid word"
_PREFIX_MARKER = const(1<<26) _PREFIX_MARKER = const(1<<26)
@ -40,6 +43,10 @@ _PREFIX_MARKER = const(1<<26)
# - 'encoded' is hex, and has is trimmed of right side zeros # - 'encoded' is hex, and has is trimmed of right side zeros
VaultEntry = namedtuple('VaultEntry', 'xfp encoded label origin') VaultEntry = namedtuple('VaultEntry', 'xfp encoded label origin')
def not_hobbled_mode():
# used as menu predicate and similar
return not pa.hobbled_mode
def seed_vault_iter(): def seed_vault_iter():
# iterate over all seeds in the vault; returns VaultEntry instances. # iterate over all seeds in the vault; returns VaultEntry instances.
# raw vault entries are list type when json.loaded from flash # raw vault entries are list type when json.loaded from flash
@ -150,23 +157,62 @@ class WordNestMenu(MenuSystem):
done_cb = None done_cb = None
def __init__(self, num_words=None, has_checksum=True, done_cb=commit_new_words, def __init__(self, num_words=None, has_checksum=True, done_cb=commit_new_words,
items=None, is_commit=False): items=None, is_commit=False, menu_cbf=None, prefix="", words=None):
if num_words is not None: if num_words is not None:
WordNestMenu.target_words = num_words WordNestMenu.target_words = num_words
WordNestMenu.has_checksum = has_checksum WordNestMenu.has_checksum = has_checksum
WordNestMenu.words = [] WordNestMenu.words = []
assert done_cb
WordNestMenu.done_cb = done_cb WordNestMenu.done_cb = done_cb
is_commit = True is_commit = True
if words:
WordNestMenu.words = words
if not items: if not items:
items = [MenuItem(i, menu=self.next_menu) for i in letter_choices()] ch = letter_choices(prefix)
if menu_cbf:
items = [MenuItem(i, f=menu_cbf) for i in ch]
else:
items = [MenuItem(i, menu=self.next_menu) for i in ch]
self.is_commit = is_commit self.is_commit = is_commit
super(WordNestMenu, self).__init__(items) super(WordNestMenu, self).__init__(items)
@classmethod
async def get_n_words(cls, num_words):
rv = []
for _ in range(num_words):
rv = await cls.get_word(rv, num_words)
return rv
@classmethod
async def get_word(cls, words=None, target_words=None):
# Just block until N words are provided. May only work before menus start?
from glob import numpad
async def menu_done_cbf(menu, b, c):
# duplicates some of the logic of next_menu
if c.label[-1] == '-':
lc = c.label[0:-1]
else:
cls.words.append(c.label)
numpad.abort_ux()
return
m = cls(prefix=lc, menu_cbf=menu_done_cbf)
the_ux.push(m)
await the_ux.interact()
m = cls(num_words=target_words, menu_cbf=menu_done_cbf, has_checksum=False, words=words)
the_ux.push(m)
await the_ux.interact()
return cls.words
@staticmethod @staticmethod
async def next_menu(self, idx, choice): async def next_menu(self, idx, choice):
@ -463,6 +509,10 @@ async def add_seed_to_vault(encoded, origin=None, label=None):
if in_seed_vault(encoded): if in_seed_vault(encoded):
return return
# stay "read only" in hobbled mode
if pa.hobbled_mode:
return
main_xfp = settings.master_get("xfp", 0) main_xfp = settings.master_get("xfp", 0)
# parse encoded # parse encoded
@ -501,12 +551,16 @@ async def add_seed_to_vault(encoded, origin=None, label=None):
async def set_ephemeral_seed(encoded, chain=None, summarize_ux=True, bip39pw='', async def set_ephemeral_seed(encoded, chain=None, summarize_ux=True, bip39pw='',
is_restore=False, origin=None, label=None): is_restore=False, origin=None, label=None):
# Capture tmp seed into vault, if so enabled, and regardless apply it as new tmp. # Capture tmp seed into vault, if so enabled, and regardless apply it as new tmp.
if not is_restore: if not is_restore and not_hobbled_mode():
await add_seed_to_vault(encoded, origin=origin, label=label) await add_seed_to_vault(encoded, origin=origin, label=label)
dis.fullscreen("Wait...") dis.fullscreen("Wait...")
applied, err_msg = pa.tmp_secret(encoded, chain=chain, bip39pw=bip39pw) applied, err_msg = pa.tmp_secret(encoded, chain=chain, bip39pw=bip39pw)
# FYI: 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
# would only affect MitM test, which has already been done.
dis.progress_bar_show(1) dis.progress_bar_show(1)
if not applied: if not applied:
@ -515,7 +569,10 @@ async def set_ephemeral_seed(encoded, chain=None, summarize_ux=True, bip39pw='',
xfp = "[" + xfp2str(settings.get("xfp", 0)) + "]" xfp = "[" + xfp2str(settings.get("xfp", 0)) + "]"
if summarize_ux: if summarize_ux:
await ux_show_story(title=xfp, msg="New temporary master key is in effect now.") msg = "New temporary master key is in effect now."
if bip39pw:
msg += "\n\nPassphrase: %s" % bip39pw
await ux_show_story(title=xfp, msg=msg)
return applied return applied
@ -690,6 +747,7 @@ def set_seed_value(words=None, encoded=None, chain=None):
async def calc_bip39_passphrase(pw, bypass_tmp=False): async def calc_bip39_passphrase(pw, bypass_tmp=False):
# Returns (new) encoded secret, new xfp, old xfp
from glob import dis, settings from glob import dis, settings
dis.fullscreen("Working...") dis.fullscreen("Working...")
@ -706,14 +764,13 @@ async def calc_bip39_passphrase(pw, bypass_tmp=False):
async def set_bip39_passphrase(pw, bypass_tmp=False, summarize_ux=True): async def set_bip39_passphrase(pw, bypass_tmp=False, summarize_ux=True):
nv, xfp, parent_xfp = await calc_bip39_passphrase(pw, bypass_tmp=bypass_tmp) nv, xfp, parent_xfp = await calc_bip39_passphrase(pw, bypass_tmp=bypass_tmp)
ret = await set_ephemeral_seed(nv, summarize_ux=summarize_ux, bip39pw=pw, ret = await set_ephemeral_seed(nv, summarize_ux=summarize_ux, bip39pw=pw,
origin="BIP-39 Passphrase on [%s]" % xfp2str(parent_xfp)) origin="BIP-39 Passphrase on [%s]" % xfp2str(parent_xfp))
dis.draw_status(bip39=int(bool(pw)), xfp=xfp, tmp=1)
return ret
# Might need to bounce the USB connection, because our pubkey has changed, dis.draw_status(bip39=int(bool(pw)), xfp=xfp, tmp=1)
# altho if they have already picked a shared session key, no need, and
# would only affect MitM test, which has already been done. return ret
async def remember_ephemeral_seed(): async def remember_ephemeral_seed():
# Compute current xprv and switch to using that as root secret. # Compute current xprv and switch to using that as root secret.
@ -737,7 +794,7 @@ async def remember_ephemeral_seed():
# address cache, settings from tmp seeds / seedvault seeds # address cache, settings from tmp seeds / seedvault seeds
# rebuild fs as we want to save current tmp settings immediately # rebuild fs as we want to save current tmp settings immediately
from files import wipe_flash_filesystem from files import wipe_flash_filesystem
wipe_flash_filesystem(True) wipe_flash_filesystem()
dis.draw_status(bip39=0, tmp=0) dis.draw_status(bip39=0, tmp=0)
dis.fullscreen('Saving...') dis.fullscreen('Saving...')
@ -768,12 +825,6 @@ def clear_seed():
callgate.fast_wipe(True) callgate.fast_wipe(True)
# NOT REACHED # NOT REACHED
utime.sleep(1)
# security: need to reboot to really be sure to clear the secrets from main memory.
from machine import reset
reset()
async def word_quiz(words, limited=None, title='Word %d is?'): async def word_quiz(words, limited=None, title='Word %d is?'):
# Perform a test, to check they wrote them down # Perform a test, to check they wrote them down
# Return X if they cancel early. # Return X if they cancel early.
@ -879,6 +930,8 @@ class SeedVaultMenu(MenuSystem):
ch = await ux_show_story(title="[" + rec.xfp + "]", msg=msg, escape=esc) ch = await ux_show_story(title="[" + rec.xfp + "]", msg=msg, escape=esc)
if ch == "x": return if ch == "x": return
assert not_hobbled_mode()
dis.fullscreen("Saving...") dis.fullscreen("Saving...")
wipe_slot = not current_active and (ch != "1") wipe_slot = not current_active and (ch != "1")
@ -890,6 +943,7 @@ class SeedVaultMenu(MenuSystem):
xs.blank() xs.blank()
del xs del xs
# CAUTION: will get shadow copy if in tmp seed mode already # CAUTION: will get shadow copy if in tmp seed mode already
seeds = settings.master_get("seeds", []) seeds = settings.master_get("seeds", [])
try: try:
@ -926,6 +980,8 @@ class SeedVaultMenu(MenuSystem):
from glob import dis from glob import dis
from ux import ux_input_text from ux import ux_input_text
assert not_hobbled_mode()
idx, old = item.arg idx, old = item.arg
new_label = await ux_input_text(old.label, confirm_exit=False, max_len=40) new_label = await ux_input_text(old.label, confirm_exit=False, max_len=40)
@ -956,6 +1012,8 @@ class SeedVaultMenu(MenuSystem):
async def _add_current_tmp(*a): async def _add_current_tmp(*a):
from pincodes import pa from pincodes import pa
assert not_hobbled_mode()
assert pa.tmp_value assert pa.tmp_value
main_xfp = settings.master_get("xfp", 0) main_xfp = settings.master_get("xfp", 0)
@ -998,9 +1056,10 @@ class SeedVaultMenu(MenuSystem):
if not seeds: if not seeds:
rv.append(MenuItem('(none saved yet)')) rv.append(MenuItem('(none saved yet)'))
if pa.tmp_value: if not_hobbled_mode():
rv.append(add_current_tmp) if pa.tmp_value:
rv.append(MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu)) rv.append(add_current_tmp)
rv.append(MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu))
else: else:
wipe_if_deltamode() wipe_if_deltamode()
@ -1016,8 +1075,10 @@ class SeedVaultMenu(MenuSystem):
submenu = [ submenu = [
MenuItem(rec.label, f=cls._detail, arg=(rec, encoded)), MenuItem(rec.label, f=cls._detail, arg=(rec, encoded)),
MenuItem('Use This Seed', f=cls._set, arg=encoded), MenuItem('Use This Seed', f=cls._set, arg=encoded),
MenuItem('Rename', f=cls._rename, arg=(i, rec)), MenuItem('Rename', f=cls._rename, arg=(i, rec),
MenuItem('Delete', f=cls._remove, arg=(i, rec, encoded)), predicate=not_hobbled_mode),
MenuItem('Delete', f=cls._remove, arg=(i, rec, encoded),
predicate=not_hobbled_mode),
] ]
if is_active: if is_active:
submenu[1] = MenuItem("Seed In Use") submenu[1] = MenuItem("Seed In Use")
@ -1035,7 +1096,7 @@ class SeedVaultMenu(MenuSystem):
rv.append(item) rv.append(item)
if pa.tmp_value: if pa.tmp_value:
if seeds and (not tmp_in_sv): if seeds and (not tmp_in_sv) and not_hobbled_mode():
# give em chance to store current active # give em chance to store current active
rv.append(add_current_tmp) rv.append(add_current_tmp)
@ -1108,6 +1169,7 @@ class EphemeralSeedMenu(MenuSystem):
from actions import nfc_recv_ephemeral, import_xprv from actions import nfc_recv_ephemeral, import_xprv
from actions import restore_backup, scan_any_qr from actions import restore_backup, scan_any_qr
from tapsigner import import_tapsigner_backup_file from tapsigner import import_tapsigner_backup_file
from xor_seed import xor_restore_temporary
from charcodes import KEY_QR from charcodes import KEY_QR
import_ephemeral_menu = [ import_ephemeral_menu = [
@ -1124,19 +1186,21 @@ class EphemeralSeedMenu(MenuSystem):
] ]
rv = [ rv = [
MenuItem("Generate Words", menu=gen_ephemeral_menu), MenuItem("Generate Words", menu=gen_ephemeral_menu, predicate=not_hobbled_mode),
MenuItem('Import from QR Scan', predicate=version.has_qr, MenuItem('Import from QR Scan', predicate=version.has_qr,
shortcut=KEY_QR, f=scan_any_qr, arg=(True, True)), shortcut=KEY_QR, f=scan_any_qr, arg=(True, True)),
MenuItem("Import Words", menu=import_ephemeral_menu), MenuItem("Import Words", menu=import_ephemeral_menu),
MenuItem("Import XPRV", f=import_xprv, arg=True), # ephemeral=True MenuItem("Import XPRV", f=import_xprv, arg=True), # ephemeral=True
MenuItem("Tapsigner Backup", f=import_tapsigner_backup_file, arg=True), # ephemeral=True MenuItem("Tapsigner Backup", f=import_tapsigner_backup_file, arg=True), # ephemeral=True
MenuItem("Coldcard Backup", f=restore_backup, arg=True), # tmp=True MenuItem("Coldcard Backup", f=restore_backup, arg=True), # tmp=True
MenuItem("Restore Seed XOR", f=xor_restore_temporary),
] ]
return rv return rv
async def make_ephemeral_seed_menu(*a): async def make_ephemeral_seed_menu(*a):
if (not pa.tmp_value) and (not settings.master_get("seedvault", False)): if (not pa.tmp_value) and (not settings.master_get("seedvault", False)):
# force a warning on them, unless they are already doing it. # force a warning on them, unless they are already doing it.
if not await ux_confirm( if not await ux_confirm(
@ -1174,10 +1238,10 @@ the passphrase as well, it's okay to put them together.) There is no way for \
the Coldcard to know if your entry is correct, and if you have it wrong, \ the Coldcard to know if your entry is correct, and if you have it wrong, \
you will be looking at an empty wallet. you will be looking at an empty wallet.
Limitations: 100 characters max length, ASCII characters 32-126 (0x20-0x7e) only. Limitations: %d characters max length, ASCII characters 32-126 (0x20-0x7e) only.
%s to continue or press (2) to hide this message forever. %s to continue or press (2) to hide this message forever.
''' % (howto if not version.has_qwerty else '', OK) ''' % (howto if not version.has_qwerty else '', MAX_PASS_LEN, OK)
ch = await ux_show_story(msg, escape='2') ch = await ux_show_story(msg, escape='2')
if ch == '2': if ch == '2':
@ -1187,8 +1251,8 @@ Limitations: 100 characters max length, ASCII characters 32-126 (0x20-0x7e) only
if version.has_qwerty and not PassphraseSaver.has_file(): if version.has_qwerty and not PassphraseSaver.has_file():
# no need for any menus if Q and no card present # no need for any menus if Q and no card present
pp = await ux_input_text('', prompt="Your BIP-39 Passphrase", pp = await ux_input_text('', prompt="Your BIP-39 Passphrase", b39_complete=True,
b39_complete=True, scan_ok=True, max_len=100) scan_ok=True, max_len=MAX_PASS_LEN)
if not pp: return if not pp: return
await apply_pass_value(pp) await apply_pass_value(pp)
@ -1198,7 +1262,7 @@ Limitations: 100 characters max length, ASCII characters 32-126 (0x20-0x7e) only
class PassphraseMenu(MenuSystem): class PassphraseMenu(MenuSystem):
# Collect up to 100 chars as a BIP-39 passphrase # Collect up to MAX_PASS_LEN chars as a BIP-39 passphrase
# singleton (cls level) vars # singleton (cls level) vars
done_cb = None done_cb = None
@ -1287,7 +1351,7 @@ class PassphraseMenu(MenuSystem):
async def view_edit_phrase(cls, *a): async def view_edit_phrase(cls, *a):
# let them control each character # let them control each character
pw = await ux_input_text(cls.pp_sofar, prompt="Your BIP-39 Passphrase", pw = await ux_input_text(cls.pp_sofar, prompt="Your BIP-39 Passphrase",
b39_complete=True, scan_ok=True, max_len=100) b39_complete=True, scan_ok=True, max_len=MAX_PASS_LEN)
if pw is not None: if pw is not None:
cls.pp_sofar = pw cls.pp_sofar = pw
cls.check_length() cls.check_length()
@ -1298,8 +1362,8 @@ class PassphraseMenu(MenuSystem):
@classmethod @classmethod
def check_length(cls): def check_length(cls):
# enforce a limit of 100 chars # enforce a limit of MAX_PASS_LEN chars
cls.pp_sofar = cls.pp_sofar[0:100] cls.pp_sofar = cls.pp_sofar[0:MAX_PASS_LEN]
@classmethod @classmethod
async def add_text(cls, _1, _2, item): async def add_text(cls, _1, _2, item):
@ -1340,8 +1404,9 @@ async def apply_pass_value(new_pp):
msg = ('Above is the master key fingerprint of the new wallet' msg = ('Above is the master key fingerprint of the new wallet'
' created by adding passphrase to %s.' ' created by adding passphrase to %s.'
'\n\nPassphrase: %s'
'\n\nPress %s to abort, %s to use the new wallet, (1) to apply' '\n\nPress %s to abort, %s to use the new wallet, (1) to apply'
' and save to MicroSD for future.') % (msg, X, OK) ' and save to MicroSD for future.') % (msg, new_pp, X, OK)
ch = await ux_show_story(msg, title="[%s]" % xfp_str, escape='1') ch = await ux_show_story(msg, title="[%s]" % xfp_str, escape='1')
if ch == 'x': if ch == 'x':

View File

@ -171,7 +171,7 @@ async def test_secure_element():
dis.clear() dis.clear()
if version.has_qwerty: if version.has_qwerty or version.mk_num == 5:
dis.text(0, 0, "^^-- Green? " if gg else " ^^-- Red?") dis.text(0, 0, "^^-- Green? " if gg else " ^^-- Red?")
else: else:
if gg: if gg:
@ -364,7 +364,7 @@ async def test_microsd():
with CardSlot(slot_b=slot_num) as card: with CardSlot(slot_b=slot_num) as card:
_, fn = card.pick_filename('test-delme.txt') fn, _ = card.pick_filename('test-delme.txt')
with open(fn, 'wt') as fd: with open(fn, 'wt') as fd:
fd.write("Hello") fd.write("Hello")

View File

@ -19,6 +19,7 @@ from ubinascii import hexlify as b2a_hex
import ustruct as struct import ustruct as struct
import ngu import ngu
from opcodes import * from opcodes import *
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2SH, AF_P2WSH, AF_P2TR, AF_BARE_PK
# single-shot hash functions # single-shot hash functions
sha256 = ngu.hash.sha256s sha256 = ngu.hash.sha256s
@ -26,8 +27,8 @@ ripemd160 = ngu.hash.ripemd160
hash256 = ngu.hash.sha256d hash256 = ngu.hash.sha256d
hash160 = ngu.hash.hash160 hash160 = ngu.hash.hash160
def bytes_to_hex_str(s): #def bytes_to_hex_str(s):
return str(b2a_hex(s), 'ascii') # return str(b2a_hex(s), 'ascii')
SIGHASH_ALL = const(1) SIGHASH_ALL = const(1)
SIGHASH_NONE = const(2) SIGHASH_NONE = const(2)
@ -194,41 +195,43 @@ def disassemble(script):
try: try:
offset = 0 offset = 0
slen = len(script)
while 1: while 1:
if offset >= len(script): if offset >= slen:
#print('dis %d done' % offset) #print('dis %d done' % offset)
return return
c = script[offset] c = script[offset]
offset += 1 offset += 1
if 1 <= c <= 75: if 1 <= c <= 75:
#print('dis %d: bytes=%s' % (offset, b2a_hex(script[offset:offset+c]))) cnt = c
yield (script[offset:offset+c], None)
offset += c
elif OP_1 <= c <= OP_16: elif OP_1 <= c <= OP_16:
# OP_1 thru OP_16 # OP_1 thru OP_16
#print('dis %d: number=%d' % (offset, (c - OP_1 + 1)))
yield (c - OP_1 + 1, None) yield (c - OP_1 + 1, None)
continue
elif c == OP_PUSHDATA1: elif c == OP_PUSHDATA1:
cnt = script[offset] cnt = script[offset]
offset += 1 offset += 1
yield (script[offset:offset+cnt], None)
offset += cnt
elif c == OP_PUSHDATA2: elif c == OP_PUSHDATA2:
# up to 65535 bytes # up to 65535 bytes
cnt, = struct.unpack_from("H", script, offset) cnt, = struct.unpack_from("H", script, offset)
offset += 2 offset += 2
yield (script[offset:offset+cnt], None)
offset += cnt
elif c == OP_PUSHDATA4: elif c == OP_PUSHDATA4:
# no where to put so much data # no where to put so much data
raise NotImplementedError raise NotImplementedError
elif c == OP_1NEGATE: elif c == OP_1NEGATE:
yield (-1, None) yield (-1, None)
continue
else: else:
# OP_0 included here # OP_0 included here
#print('dis %d: opcode=%d' % (offset, c))
yield (None, c) yield (None, c)
continue
# a data push of `cnt` bytes - reject if it runs off the end
if offset + cnt > slen:
raise ValueError
yield (script[offset:offset+cnt], None)
offset += cnt
except Exception as e: except Exception as e:
# import sys;sys.print_exception(e) # import sys;sys.print_exception(e)
raise ValueError("bad script") raise ValueError("bad script")
@ -319,7 +322,6 @@ class CTxIn(object):
self.nSequence = nSequence self.nSequence = nSequence
def deserialize(self, f): def deserialize(self, f):
self.prevout = COutPoint()
self.prevout.deserialize(f) self.prevout.deserialize(f)
self.scriptSig = deser_string(f) self.scriptSig = deser_string(f)
self.nSequence = struct.unpack("<I", f.read(4))[0] self.nSequence = struct.unpack("<I", f.read(4))[0]
@ -355,26 +357,32 @@ class CTxOut(object):
# (addr_type_code, addr, is_segwit) # (addr_type_code, addr, is_segwit)
# 'addr' is byte string, either 20 or 32 long # 'addr' is byte string, either 20 or 32 long
if self.is_p2tr(): if self.is_p2tr():
return 'p2tr', self.scriptPubKey[2:2+32], True return AF_P2TR, self.scriptPubKey[2:2+32], True
if self.is_p2wpkh(): if self.is_p2wpkh():
return 'p2pkh', self.scriptPubKey[2:2+20], True return AF_P2WPKH, self.scriptPubKey[2:2+20], True
if self.is_p2wsh(): if self.is_p2wsh():
return 'p2sh', self.scriptPubKey[2:2+32], True return AF_P2WSH, self.scriptPubKey[2:2+32], True
if self.is_p2pkh(): if self.is_p2pkh():
return 'p2pkh', self.scriptPubKey[3:3+20], False return AF_CLASSIC, self.scriptPubKey[3:3+20], False
if self.is_p2sh(): if self.is_p2sh():
return 'p2sh', self.scriptPubKey[2:2+20], False # can be:
# * bare P2SH
# * P2SH-P2WPKH
# * P2SH-P2WSH
return AF_P2SH, self.scriptPubKey[2:2+20], False
if self.is_p2pk(): if self.is_p2pk():
# rare, pay to full pubkey # rare, pay to full pubkey: <push_op> <pubkey> OP_CHECKSIG
return 'p2pk', self.scriptPubKey[2:2+33], False # push_op is 0x21 (33) for compressed, 0x41 (65) for uncompressed
pk_len = self.scriptPubKey[0]
return AF_BARE_PK, self.scriptPubKey[1:1+pk_len], False
if self.scriptPubKey[0] == OP_RETURN: if self.is_op_return():
return 'op_return', self.scriptPubKey, False return OP_RETURN, self.scriptPubKey, False
return None, self.scriptPubKey, None return None, self.scriptPubKey, None
@ -401,8 +409,11 @@ class CTxOut(object):
def is_p2pk(self): def is_p2pk(self):
return (len(self.scriptPubKey) == 35 or len(self.scriptPubKey) == 67) \ return (len(self.scriptPubKey) == 35 or len(self.scriptPubKey) == 67) \
and (self.scriptPubKey[0] == 0x21 or self.scriptPubKey[0] == 0x41) \ and self.scriptPubKey[0] == len(self.scriptPubKey) - 2 \
and self.scriptPubKey[-1] == 0xac and self.scriptPubKey[-1] == OP_CHECKSIG
def is_op_return(self):
return self.scriptPubKey and (self.scriptPubKey[0] == OP_RETURN)
#def __repr__(self): #def __repr__(self):
# return "CTxOut(nValue=%d scriptPubKey=%s)" \ # return "CTxOut(nValue=%d scriptPubKey=%s)" \

View File

@ -2,7 +2,7 @@
# #
# sffile.py - file-like objects stored in PSRAM (Mk4+) (used to be SPI Flash) # sffile.py - file-like objects stored in PSRAM (Mk4+) (used to be SPI Flash)
# #
# - implements stream IO protoccol # - implements stream IO protocol
# - random read, sequential write # - random read, sequential write
# - only a few of these are possible # - only a few of these are possible
# - the offset is the file name # - the offset is the file name

View File

@ -1,6 +1,6 @@
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC. # (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
# #
# ssd1306.py - MicroPython SSD1306 OLED driver, I2C and SPI interfaces # ssd1306.py - MicroPython SSD1306 OLED driver, with SPI interface
# #
# Copied from ../external/micropython/drivers/display/ssd1306.py # Copied from ../external/micropython/drivers/display/ssd1306.py
# #
@ -28,49 +28,81 @@ SET_VCOM_DESEL = const(0xdb)
SET_CHARGE_PUMP = const(0x8d) SET_CHARGE_PUMP = const(0x8d)
# Subclassing FrameBuffer provides support for graphics primitives # Subclassing FrameBuffer provides support for graphics primitives
# http://docs.micropython.org/en/latest/pyboard/library/framebuf.html # see <http://docs.micropython.org/en/latest/pyboard/library/framebuf.html>
#
class SSD1306(framebuf.FrameBuffer): class SSD1306(framebuf.FrameBuffer):
def __init__(self, width, height, external_vcc): def __init__(self, width, height, is_mk5):
self.width = width self.width = width
self.height = height self.height = height
self.external_vcc = external_vcc self.is_mk5 = is_mk5
self.pages = self.height // 8 self.pages = self.height // 8
#self.buffer = bytearray(self.pages * self.width)
self.buffer = bytearray(1024) self.buffer = bytearray(1024)
assert len(self.buffer) == self.pages * self.width #assert len(self.buffer) == self.pages * self.width
super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB) super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
self.init_display() self.init_display()
def init_display(self): def init_display(self):
for cmd in ( if not self.is_mk5:
SET_DISP | 0x00, # off # Mk4 and earlier
# address setting cmds = (
SET_MEM_ADDR, 0x00, # horizontal SET_DISP | 0x00, # display off
# resolution and layout # address setting
SET_DISP_START_LINE | 0x00, SET_MEM_ADDR, 0x00, # horizontal
SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0 # resolution and layout
SET_MUX_RATIO, self.height - 1, SET_DISP_START_LINE | 0x00,
SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0 SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0
SET_DISP_OFFSET, 0x00, SET_MUX_RATIO, self.height - 1,
SET_COM_PIN_CFG, 0x02 if self.height == 32 else 0x12, SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0
# timing and driving scheme SET_DISP_OFFSET, 0x00,
SET_DISP_CLK_DIV, 0xF0, SET_COM_PIN_CFG, 0x12,
SET_PRECHARGE, 0x22 if self.external_vcc else 0xf1, # timing and driving scheme
SET_VCOM_DESEL, 0x30, # 0.83*Vcc SET_DISP_CLK_DIV, 0xF0,
# display SET_PRECHARGE, 0xf1,
SET_CONTRAST, 0xff, # maximum SET_VCOM_DESEL, 0x30, # 0.83*Vcc
SET_ENTIRE_ON, # output follows RAM contents # display
SET_NORM_INV, # not inverted SET_CONTRAST, 0xff, # maximum
# charge pump SET_ENTIRE_ON, # output follows RAM contents
SET_CHARGE_PUMP, 0x10 if self.external_vcc else 0x14, SET_NORM_INV, # not inverted
SET_DISP | 0x01): # on # charge pump
self.write_cmd(cmd) SET_CHARGE_PUMP, 0x14)
else:
# Mk5 has external +12v power supply, and different setup protocol
cmds = (
SET_DISP | 0x00, # display off
# address setting
SET_MEM_ADDR, 0x00, # horizontal
# resolution and layout
SET_DISP_START_LINE | 0x00,
SET_SEG_REMAP | 0x00, # column addr 0 mapped to SEG127
SET_MUX_RATIO, self.height - 1,
SET_COM_OUT_DIR | 0x00, # scan from COM[8] to COM[N]
SET_DISP_OFFSET, 0x00,
SET_COM_PIN_CFG, 0x12,
# timing and driving scheme
SET_DISP_CLK_DIV, 0xF0,
SET_PRECHARGE, 0x22,
SET_VCOM_DESEL, 0x40, # per spec sheet
# display
SET_CONTRAST, 0x85, # NOT maximum, because spec sheet
SET_ENTIRE_ON, # output follows RAM contents
SET_NORM_INV, # not inverted
SET_CHARGE_PUMP, 0x10, # charge pump: DISABLE
)
self.write_cmds(cmds)
self.fill(0) self.fill(0)
self.show() self.show()
self.write_cmd(SET_DISP | 0x01)
def write_cmds(self, cmds):
for c in cmds:
self.write_cmd(c)
def poweroff(self): def poweroff(self):
self.write_cmd(SET_DISP | 0x00) self.write_cmd(SET_DISP | 0x00)
@ -78,6 +110,10 @@ class SSD1306(framebuf.FrameBuffer):
self.write_cmd(SET_DISP | 0x01) self.write_cmd(SET_DISP | 0x01)
def contrast(self, contrast): def contrast(self, contrast):
# brightness: normal = 0x7f, brightness=0xff, dim=0x00 (but they are all very similar)
if self.is_mk5:
# - limit to a specific max value from OLED specs used on Mk5
contrast = max(contrast, 0x85)
self.write_cmd(SET_CONTRAST) self.write_cmd(SET_CONTRAST)
self.write_cmd(contrast) self.write_cmd(contrast)
@ -85,56 +121,113 @@ class SSD1306(framebuf.FrameBuffer):
self.write_cmd(SET_NORM_INV | (invert & 1)) self.write_cmd(SET_NORM_INV | (invert & 1))
def show(self): def show(self):
x0 = 0
x1 = self.width - 1
if self.width == 64:
# displays with width of 64 pixels are shifted by 32
x0 += 32
x1 += 32
self.write_cmd(SET_COL_ADDR) self.write_cmd(SET_COL_ADDR)
self.write_cmd(x0) self.write_cmd(0)
self.write_cmd(x1) self.write_cmd(self.width - 1)
self.write_cmd(SET_PAGE_ADDR) self.write_cmd(SET_PAGE_ADDR)
self.write_cmd(0) self.write_cmd(0)
self.write_cmd(self.pages - 1) self.write_cmd(self.pages - 1)
self.write_data(self.buffer) self.write_data(self.buffer)
SPI_RATE = const(40000000) # max chip can do, still slower than display limit tho def busy_bar(self, enable, pattern):
# Render a continuous activity (not progress) bar in lower 8 lines of display
# - using OLED itself to do the animation, so smooth and CPU free
# - cannot preserve bottom 8 lines, since we have to destructively write there
# - assumes normal horz addr mode: 0x20, 0x00
# - speed_code=>framedelay: 0=5fr, 1=64fr, 2=128, 3=256, 4=3, 5=4, 6=25, 7=2frames
# unused: assert 0 <= speed_code <= 7
setup = bytes([
0x21, 0x00, 0x7f, # setup column address range (start, end): 0-127
0x22, 7, 7, # setup page start/end address: page 7=last 8 lines
])
if not self.is_mk5:
animate = bytes([
0x2e, # stop animations in progress
0x26, # scroll leftwards (stock ticker mode)
0, # placeholder
7, # start 'page' (vertical)
5, # "speed_code" # scroll speed: 7=fastest, but no order to it
7, # end 'page'
0, 0x7f, # start/end columns
0x2f # start
])
else:
# SSD1309? doesn't implement 0x26 but has other commands
animate = bytes([
0x2e, # stop animations in progress
0x29, # Vert+Right horz animation setup
1, # A: enable horz scroll
7, # B: start 'page' (vertical)
5, # C: "speed_code" # scroll speed: 7=fastest, but no order to it
7, # D: end 'page'
1, # E: vert scrolling offset (unused)
0, 0x7f, # F,G: start/end columns
0xa3, # Set Vertical scroll Area
0, 0, # A, B: # of rows in fixed vs. scroll area
0x2f # start animating
])
cleanup = bytes([
0x2e, # stop animation
0x20, 0x00, # horz addr-ing mode
0x21, 0x00, 0x7f, # setup column address range (start, end): 0-127
0x22, 7, 7, # setup page start/end address: page 7=last 8 lines
])
if not enable:
# stop animation, and redraw old (new) screen
self.write_cmds(cleanup)
else:
# needs a pattern that repeats nicely mod 128
self.write_cmds(setup)
self.write_data(pattern)
self.write_cmds(animate)
class SSD1306_SPI(SSD1306): class SSD1306_SPI(SSD1306):
def __init__(self, width, height, spi, dc, res, cs, external_vcc=False): def __init__(self, width, height, spi, dc, res, cs, is_mk5=False):
dc.init(dc.OUT, value=0)
res.init(res.OUT, value=0)
cs.init(cs.OUT, value=1)
self.spi = spi self.spi = spi
self.dc = dc self.dc = dc
self.res = res
self.cs = cs self.cs = cs
self.res(1) self.res = res
# initial states
dc(0)
cs(1)
# reset sequence
res(1)
time.sleep_ms(1) time.sleep_ms(1)
self.res(0) res(0)
time.sleep_ms(10) time.sleep_ms(10)
self.res(1) res(1)
super().__init__(width, height, external_vcc)
super().__init__(width, height, is_mk5)
def _setup_spi(self):
# need to re-do this constantly
# max chip can do, still slower than display limit tho
# - 40Mhz (target) is fine for short-cabled Mk4 (actual is lower?)
# - max spec is 10Mhz on Mk5
rate = 40_000_000 if not self.is_mk5 else 10_000_000
self.spi.init(baudrate=rate, polarity=0, phase=0)
def write_cmd(self, cmd): def write_cmd(self, cmd):
self.spi.init(baudrate=SPI_RATE, polarity=0, phase=0) self._setup_spi()
self.cs(1) self.cs(1)
self.dc(0) self.dc(0)
self.cs(0) self.cs(0)
try: self.spi.write(bytearray([cmd]))
self.spi.write(bytearray([cmd]))
except:
print("SPI[cmd]: %r" % self.spi)
self.cs(1) self.cs(1)
def write_data(self, buf): def write_data(self, buf):
self.spi.init(baudrate=SPI_RATE, polarity=0, phase=0) self._setup_spi()
self.cs(1) self.cs(1)
self.dc(1) self.dc(1)
self.cs(0) self.cs(0)
try: self.spi.write(buf)
self.spi.write(buf)
except:
print("SPI[data]: %r" % self.spi)
self.cs(1) self.cs(1)
# EOF

View File

@ -12,7 +12,7 @@
# #
import ngu, uctypes, gc, bip39, utime import ngu, uctypes, gc, bip39, utime
from uhashlib import sha256 from uhashlib import sha256
from utils import swab32, call_later_ms, B2A from utils import swab32, call_later_ms, B2A, node_from_privkey
SEED_LEN_OPTS = [12, 18, 24] SEED_LEN_OPTS = [12, 18, 24]
@ -104,7 +104,7 @@ class SecretStash:
ch, pk = secret[1:33], secret[33:65] ch, pk = secret[1:33], secret[33:65]
assert not _bip39pw assert not _bip39pw
hd.from_chaincode_privkey(ch, pk) hd = node_from_privkey(pk, ch)
return 'xprv', ch+pk, hd return 'xprv', ch+pk, hd
elif marker & 0x80: elif marker & 0x80:
@ -403,8 +403,7 @@ class SensitiveValues:
self.register(cc) self.register(cc)
self.register(pk) self.register(pk)
rv = ngu.hdnode.HDNode() rv = node_from_privkey(pk, cc)
rv.from_chaincode_privkey(cc, pk)
self.register(rv) self.register(rv)
return rv, p return rv, p

View File

@ -67,7 +67,7 @@ async def import_tapsigner_backup_file(_1, _2, item):
continue continue
break break
else: else:
fn = await file_picker(suffix="aes", min_size=100, max_size=160, **choice) fn = await file_picker(suffix=".aes", min_size=100, max_size=160, **choice)
if not fn: return if not fn: return
origin += (" (%s)" % fn) origin += (" (%s)" % fn)
try: try:

View File

@ -307,11 +307,17 @@ async def kt_accept_values(dtype, raw):
- `b` - complete system backup file (text, internal format) - `b` - complete system backup file (text, internal format)
''' '''
from flow import has_se_secrets, goto_top_menu from flow import has_se_secrets, goto_top_menu
from pincodes import pa
enc = None enc = None
origin = 'Teleported' origin = 'Teleported'
label = None label = None
if pa.hobbled_mode and dtype != 'p':
await ux_show_story('Only PSBT for multisig accepted in this mode.', title='FAILED')
return
if dtype == 's': if dtype == 's':
# words / bip 32 master / xprv, etc # words / bip 32 master / xprv, etc
enc = bytearray(72) enc = bytearray(72)
@ -321,7 +327,7 @@ async def kt_accept_values(dtype, raw):
# it's an XPRV, but in binary.. some extra data we throw away here; sigh # it's an XPRV, but in binary.. some extra data we throw away here; sigh
# XXX no way to send this .. but was thinking of address explorer # XXX no way to send this .. but was thinking of address explorer
txt = ngu.codecs.b58_encode(raw) txt = ngu.codecs.b58_encode(raw)
node, ch, _, _ = chains.slip32_deserialize(txt) node, ch, _, _ = chains.slip132_deserialize(txt)
assert ch.name == chains.current_chain().name, 'wrong chain' assert ch.name == chains.current_chain().name, 'wrong chain'
enc = SecretStash.encode(xprv=node) enc = SecretStash.encode(xprv=node)
@ -337,15 +343,20 @@ async def kt_accept_values(dtype, raw):
# This will take over UX w/ the signing process # This will take over UX w/ the signing process
# flags=None --> whether to finalize is decided based on psbt.is_complete # flags=None --> whether to finalize is decided based on psbt.is_complete
sign_transaction(psbt_len, flags=None) sign_transaction(psbt_len, flags=None, input_method="kt")
return return
elif dtype == 'b': elif dtype == 'b':
# full system backup, including master: text lines # full system backup, including master: text lines
from backups import text_bk_parser, restore_tmp_from_dict_ll, restore_from_dict from backups import text_bk_parser, restore_tmp_from_dict_ll, restore_from_dict, extract_raw_secret
vals = text_bk_parser(raw) try:
assert vals # empty? vals = text_bk_parser(raw)
assert vals # empty?
raw_sec, _ = extract_raw_secret(vals)
except Exception as e:
await ux_show_story("Invalid backup\n\n" + str(e), title='FAILED')
return
from flow import has_secrets from flow import has_secrets
@ -354,10 +365,10 @@ async def kt_accept_values(dtype, raw):
# need to remove key before I get into tmp seed settings # need to remove key before I get into tmp seed settings
# so even if this errors out, new ktrx is needed # so even if this errors out, new ktrx is needed
settings.remove_key("ktrx") settings.remove_key("ktrx")
prob = await restore_tmp_from_dict_ll(vals) prob = await restore_tmp_from_dict_ll(vals, raw_sec)
else: else:
# we have no secret, so... reboot if it works, else errors shown, etc. # we have no secret, so... reboot if it works, else errors shown, etc.
prob = await restore_from_dict(vals) prob = await restore_from_dict(vals, raw_sec)
if prob: if prob:
await ux_show_story(prob, title='FAILED') await ux_show_story(prob, title='FAILED')
@ -475,6 +486,12 @@ def decode_step2(session_key, noid_key, body):
async def kt_incoming(type_code, payload): async def kt_incoming(type_code, payload):
# incoming BBQr was scanned (via main menu, etc) # incoming BBQr was scanned (via main menu, etc)
from pincodes import pa
if pa.hobbled_mode and type_code != 'E':
# only PSBT rx is supported in hobbled mode
# fail silently, this is second check, see decoders.py
return
if type_code == 'R': if type_code == 'R':
# they want to send to this guy # they want to send to this guy
return await kt_start_send(payload) return await kt_start_send(payload)
@ -495,6 +512,10 @@ class SecretPickerMenu(MenuSystem):
def __init__(self, rx_pubkey): def __init__(self, rx_pubkey):
self.rx_pubkey = rx_pubkey self.rx_pubkey = rx_pubkey
# this menu should be unreachable in hobbled mode.
from pincodes import pa
assert not pa.hobbled_mode
from flow import word_based_seed, is_tmp, has_se_secrets from flow import word_based_seed, is_tmp, has_se_secrets
has_notes = bool(NoteContentBase.count()) has_notes = bool(NoteContentBase.count())
has_sv = bool(settings.get('seedvault', False)) has_sv = bool(settings.get('seedvault', False))
@ -620,7 +641,7 @@ class SecretPickerMenu(MenuSystem):
await kt_do_send(self.rx_pubkey, 's', raw=raw) await kt_do_send(self.rx_pubkey, 's', raw=raw)
async def kt_send_psbt(psbt, psbt_len): async def kt_send_psbt(psbt, psbt_len, psbt_offset):
# We just finished adding our signature to an incomplete PSBT. # We just finished adding our signature to an incomplete PSBT.
# User wants to send to one or more other senders for them to complete signing. # User wants to send to one or more other senders for them to complete signing.
@ -635,10 +656,8 @@ async def kt_send_psbt(psbt, psbt_len):
await ux_show_story("No more signers?") await ux_show_story("No more signers?")
return return
# move out of PSRAM # (TXN_OUTPUT_OFFSET after signing, TXN_INPUT_OFFSET for the file-teleport path)
from auth import TXN_OUTPUT_OFFSET with SFFile(psbt_offset, psbt_len) as fd:
with SFFile(TXN_OUTPUT_OFFSET, psbt_len) as fd:
bin_psbt = fd.read(psbt_len) bin_psbt = fd.read(psbt_len)
my_xfp = settings.get('xfp') my_xfp = settings.get('xfp')
@ -666,12 +685,12 @@ async def kt_send_psbt(psbt, psbt_len):
f = None f = None
if x in need: if x in need:
# we haven't signed ourselves yet, so allow that # we haven't signed ourselves yet, so allow that
from auth import sign_transaction, TXN_INPUT_OFFSET from auth import sign_transaction
async def sign_now(*a): async def sign_now(*a):
# this will reset the UX stack: # this will reset the UX stack:
# flags=None --> whether to finalize is decided based on psbt.is_complete # flags=None --> whether to finalize is decided based on psbt.is_complete
sign_transaction(psbt_len, flags=None) sign_transaction(psbt_len, flags=None, input_method="kt", offset=psbt_offset)
f = sign_now f = sign_now
@ -718,7 +737,7 @@ async def kt_send_file_psbt(*a):
picked = await import_export_prompt("PSBT", is_import=True, no_nfc=True, no_qr=True) picked = await import_export_prompt("PSBT", is_import=True, no_nfc=True, no_qr=True)
if picked == KEY_CANCEL: if picked == KEY_CANCEL:
return return
choices = await file_picker(suffix='psbt', min_size=50, ux=False, choices = await file_picker(suffix='.psbt', min_size=50, ux=False,
max_size=MAX_TXN_LEN, taster=is_psbt, **picked) max_size=MAX_TXN_LEN, taster=is_psbt, **picked)
if not choices: if not choices:
# error msg already shown # error msg already shown
@ -763,6 +782,6 @@ async def kt_send_file_psbt(*a):
await ux_show_story("We are not part of this multisig wallet.", "Cannot Teleport PSBT") await ux_show_story("We are not part of this multisig wallet.", "Cannot Teleport PSBT")
return return
await kt_send_psbt(psbt, psbt_len=psbt_len) await kt_send_psbt(psbt, psbt_len=psbt_len, psbt_offset=TXN_INPUT_OFFSET)
# EOF # EOF

View File

@ -12,6 +12,7 @@ from menu import MenuSystem, MenuItem
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_enter_number, the_ux from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_enter_number, the_ux
from stash import SecretStash from stash import SecretStash
from drv_entro import bip85_derive from drv_entro import bip85_derive
from utils import node_from_privkey
# see from mk4-bootloader/se2.h # see from mk4-bootloader/se2.h
NUM_TRICKS = const(14) NUM_TRICKS = const(14)
@ -32,7 +33,7 @@ TC_WORD_WALLET = const(0x1000)
TC_XPRV_WALLET = const(0x0800) TC_XPRV_WALLET = const(0x0800)
TC_DELTA_MODE = const(0x0400) TC_DELTA_MODE = const(0x0400)
TC_REBOOT = const(0x0200) TC_REBOOT = const(0x0200)
TC_RFU = const(0x0100) TC_FW_DEFINED = const(0x0100)
# for our use, not implemented in bootrom # for our use, not implemented in bootrom
TC_BLANK_WALLET = const(0x0080) TC_BLANK_WALLET = const(0x0080)
TC_COUNTDOWN = const(0x0040) # tc_arg = minutes of delay TC_COUNTDOWN = const(0x0040) # tc_arg = minutes of delay
@ -40,6 +41,10 @@ TC_COUNTDOWN = const(0x0040) # tc_arg = minutes of delay
# tc_args encoding: # tc_args encoding:
# TC_WORD_WALLET -> BIP-85 index, 1001..1003 for 24 words, 2001..2003 for 12-words # TC_WORD_WALLET -> BIP-85 index, 1001..1003 for 24 words, 2001..2003 for 12-words
# If TC_FW_DEFINED is true, then we can do anything with this PIN at the firmware
# level. First application is to unlock spending stuff.
TCA_SP_UNLOCK = const(0x0001) # spending policy unlock
# special "pin" used as catch-all for wrong pins # special "pin" used as catch-all for wrong pins
WRONG_PIN_CODE = '!p' WRONG_PIN_CODE = '!p'
@ -206,14 +211,14 @@ class TrickPinMgmt:
def update_slot(self, pin, new=False, new_pin=None, tc_flags=None, tc_arg=None, secret=None): def update_slot(self, pin, new=False, new_pin=None, tc_flags=None, tc_arg=None, secret=None):
# create or update a trick pin # create or update a trick pin
# - doesn't support wallet to no-wallet transitions # - doesn't support wallet to no-wallet transitions
''' #
>>> from pincodes import pa; pa.setup(b'12-12'); pa.login(); from trick_pins import * # from pincodes import pa; pa.setup(b'12-12'); pa.login(); from trick_pins import *
''' #
assert isinstance(pin, bytes) assert isinstance(pin, bytes)
b, slot = self.get_by_pin(pin) b, slot = self.get_by_pin(pin)
if not slot: if not slot:
if not new: raise KeyError("wrong pin") assert new, "wrong pin"
# Making a new entry # Making a new entry
b, slot = make_slot() b, slot = make_slot()
@ -274,6 +279,10 @@ class TrickPinMgmt:
# put them in order, with "wrong" last # put them in order, with "wrong" last
return sorted(self.tp.keys(), key=lambda i: i if (i != WRONG_PIN_CODE) else 'Z') return sorted(self.tp.keys(), key=lambda i: i if (i != WRONG_PIN_CODE) else 'Z')
def define_unlock_pin(self, new_pin):
# user is setting the bypass PIN for first time.
self.update_slot(new_pin.encode(), new=True, tc_flags=TC_FW_DEFINED, tc_arg=TCA_SP_UNLOCK)
def was_countdown_pin(self): def was_countdown_pin(self):
# was the trick pin just used? if so how much delay needed (or zero if not) # was the trick pin just used? if so how much delay needed (or zero if not)
from pincodes import pa from pincodes import pa
@ -284,6 +293,32 @@ class TrickPinMgmt:
else: else:
return 0 return 0
def was_sp_unlock(self):
# was a trick pin just used that enables acess to spending policy?
# - ok if it's also a trick PIN .. a wiping bypass for example
from pincodes import pa
tc_flags, tc_arg = pa.get_tc_values()
return bool(tc_flags & TC_FW_DEFINED) and (tc_arg == TCA_SP_UNLOCK)
def has_sp_unlock(self):
# if spending policy defined, this PIN allows adjustment
# - not TRICK bypass choices, like ones that wipe
# - could be multiple, but only first returned.
self.reload()
for k, (sn,flags,arg) in self.tp.items():
if (flags == TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
return k
return None
def delete_sp_unlock_pins(self):
# remove all bypass pins, they are done w/ feature
self.reload()
for k, (sn,flags,arg) in self.tp.items():
if (flags & TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
self.clear_slots([sn])
self.forget_pin(k)
def get_deltamode_pins(self): def get_deltamode_pins(self):
# iterate over all delta-mode PIN's defined. # iterate over all delta-mode PIN's defined.
for k, (sn,flags,args) in self.tp.items(): for k, (sn,flags,args) in self.tp.items():
@ -363,18 +398,24 @@ class TrickPinMgmt:
continue continue
if flags & TC_DELTA_MODE: if flags & TC_DELTA_MODE:
prob = validate_delta_pin(true_pin, pin) prob, arg = validate_delta_pin(true_pin, pin)
if prob: if prob:
# just forget it, no UI here to report issue # just forget it, no UI here to report issue
continue continue
try: try:
# might need to construct a BIP-85 or XPRV secret to match # might need to construct a BIP-85 or XPRV secret to match
path, new_secret = construct_duress_secret(flags, arg) path, new_secret = construct_duress_secret(flags, arg)
b, slot = tp.update_slot(pin.encode(), new=True, tp.update_slot(pin.encode(), new=True, secret=new_secret,
tc_flags=flags, tc_arg=arg, secret=new_secret) tc_flags=flags, tc_arg=arg)
except: pass except: pass
@staticmethod
async def err_unique_pin(pin):
# standardized error UX
return await ux_show_story(
"That PIN (%s) is already in use. All PIN codes must be unique." % pin)
tp = TrickPinMgmt() tp = TrickPinMgmt()
@ -520,8 +561,7 @@ class TrickPinMenu(MenuSystem):
have.remove(existing_pin) have.remove(existing_pin)
if (new_pin == self.current_pin) or (new_pin in have): if (new_pin == self.current_pin) or (new_pin in have):
await ux_show_story("That PIN (%s) is already in use. All PIN codes must be unique." % new_pin) return await tp.err_unique_pin(new_pin)
return
# check if we "forgot" this pin, and read it back if we did. # check if we "forgot" this pin, and read it back if we did.
# - important this is after the above checks so we don't reveal any trick pin used # - important this is after the above checks so we don't reveal any trick pin used
@ -606,6 +646,9 @@ the seed phrase, but still a somewhat riskier mode.
For this mode only, trick PIN must be same length as true PIN and \ For this mode only, trick PIN must be same length as true PIN and \
differ only in final 4 positions (ignoring dash).\ differ only in final 4 positions (ignoring dash).\
''', flags=TC_DELTA_MODE), ''', flags=TC_DELTA_MODE),
StoryMenuItem('Policy Unlock', "Adds (another?) Spending Policy unlock PIN.", flags=TC_FW_DEFINED, arg=TCA_SP_UNLOCK),
StoryMenuItem('Policy Unlock & Wipe' if version.has_qwerty else 'P.U. & Wipe',
"Pretends correct Spending Policy unlock PIN given, but silently wipes seed before asking for main PIN.", flags=TC_FW_DEFINED|TC_WIPE, arg=TCA_SP_UNLOCK),
] ]
m = MenuSystem(FirstMenu) m = MenuSystem(FirstMenu)
m.goto_idx(1) m.goto_idx(1)
@ -651,9 +694,14 @@ setting) the Coldcard will always brick after 13 failed PIN attempts.''')
the_ux.push(m) the_ux.push(m)
async def clear_all(self, m,l,item): async def clear_all(self, m,l,item):
if not await ux_confirm("Remove ALL TRICK PIN codes and special wrong-pin handling?"): if not await ux_confirm("Remove ALL TRICK PIN codes and special wrong-pin handling?"):
return return
if tp.has_sp_unlock():
if not await ux_confirm("You will not be able to bypass spending policy anymore."):
return
if any(tp.get_duress_pins()): if any(tp.get_duress_pins()):
if not await ux_confirm("Any funds on the duress wallet(s) have been moved already?"): if not await ux_confirm("Any funds on the duress wallet(s) have been moved already?"):
return return
@ -662,7 +710,7 @@ setting) the Coldcard will always brick after 13 failed PIN attempts.''')
m.update_contents() m.update_contents()
async def hide_pin(self, m,l, item): async def hide_pin(self, m,l, item):
pin, slot_num, flags = item.arg pin, slot_num, flags, arg = item.arg
if flags & TC_DELTA_MODE: if flags & TC_DELTA_MODE:
await ux_show_story('''Delta mode PIN will be hidden if trick PIN menu is shown \ await ux_show_story('''Delta mode PIN will be hidden if trick PIN menu is shown \
@ -670,12 +718,14 @@ to attacker, and we need to update this record if the main PIN is changed, so we
hiding this item.''') hiding this item.''')
return return
if pin != WRONG_PIN_CODE: if (flags == TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
msg = "It will still be possible to change or disable the spending policy if this PIN is known."
elif pin == WRONG_PIN_CODE:
msg = "This will hide what happens with wrong PINs from the menus but it will still be in effect."
else:
msg = '''This will hide the PIN from the menus but it will still be in effect. msg = '''This will hide the PIN from the menus but it will still be in effect.
You can restore it by trying to re-add the same PIN (%s) again later.''' % pin You can restore it by trying to re-add the same PIN (%s) again later.''' % pin
else:
msg = "This will hide what happens with wrong PINs from the menus but it will still be in effect."
if not await ux_confirm(msg): return if not await ux_confirm(msg): return
@ -715,12 +765,16 @@ You can restore it by trying to re-add the same PIN (%s) again later.''' % pin
await ux_show_story("Failed: %s" % exc) await ux_show_story("Failed: %s" % exc)
async def delete_pin(self, m,l, item): async def delete_pin(self, m,l, item):
pin, slot_num, flags = item.arg pin, slot_num, flags, arg = item.arg
if flags & (TC_WORD_WALLET | TC_XPRV_WALLET): if flags & (TC_WORD_WALLET | TC_XPRV_WALLET):
if not await ux_confirm("Any funds on this duress wallet have been moved already?"): if not await ux_confirm("Any funds on this duress wallet have been moved already?"):
return return
if (flags == TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
if not await ux_confirm("Changes to the spending policy will not be possible anymore."):
return
if pin == WRONG_PIN_CODE: if pin == WRONG_PIN_CODE:
msg = "Remove special handling of wrong PINs?" msg = "Remove special handling of wrong PINs?"
else: else:
@ -748,8 +802,7 @@ You can restore it by trying to re-add the same PIN (%s) again later.''' % pin
ch = await ux_show_story('''\ ch = await ux_show_story('''\
This will temporarily load the secrets associated with this trick wallet \ This will temporarily load the secrets associated with this trick wallet \
so you may perform transactions with it. Reboot the Coldcard to restore \ so you may perform transactions with it.''')
normal operation.''')
if ch != 'y': return if ch != 'y': return
b, slot = tp.get_by_pin(pin) b, slot = tp.get_by_pin(pin)
@ -845,9 +898,8 @@ Wallet is XPRV-based and derived from a fixed path.''' % pin
assert s.tc_flags == flags assert s.tc_flags == flags
if flags & TC_XPRV_WALLET: if flags & TC_XPRV_WALLET:
node = ngu.hdnode.HDNode()
ch, pk = s.xdata[0:32], s.xdata[32:64] ch, pk = s.xdata[0:32], s.xdata[32:64]
node.from_chaincode_privkey(ch, pk) node = node_from_privkey(pk, ch)
title, msg, *_ = render_master_secrets('xprv', None, node) title, msg, *_ = render_master_secrets('xprv', None, node)
elif flags & TC_WORD_WALLET: elif flags & TC_WORD_WALLET:
@ -882,6 +934,8 @@ Wallet is XPRV-based and derived from a fixed path.''' % pin
rv.append(MenuItem("↳Pretends Wrong")) rv.append(MenuItem("↳Pretends Wrong"))
elif flags & TC_DELTA_MODE: elif flags & TC_DELTA_MODE:
rv.append(MenuItem("↳Delta Mode")) rv.append(MenuItem("↳Delta Mode"))
elif (flags & TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
rv.append(MenuItem("↳Unlock Policy")) # width issues on Mk4
for m, msg in [ for m, msg in [
(TC_WIPE, '↳Wipes seed'), (TC_WIPE, '↳Wipes seed'),
@ -895,8 +949,8 @@ Wallet is XPRV-based and derived from a fixed path.''' % pin
rv.append(MenuItem("Activate Wallet", f=self.activate_wallet, arg=(pin, flags, arg))) rv.append(MenuItem("Activate Wallet", f=self.activate_wallet, arg=(pin, flags, arg)))
rv.extend([ rv.extend([
MenuItem('Hide Trick', f=self.hide_pin, arg=(pin, slot_num, flags)), MenuItem('Hide Trick', f=self.hide_pin, arg=(pin, slot_num, flags, arg)),
MenuItem('Delete Trick', f=self.delete_pin, arg=(pin, slot_num, flags)), MenuItem('Delete Trick', f=self.delete_pin, arg=(pin, slot_num, flags, arg)),
]) ])
if pin != WRONG_PIN_CODE: if pin != WRONG_PIN_CODE:
rv.append( rv.append(
@ -907,6 +961,7 @@ Wallet is XPRV-based and derived from a fixed path.''' % pin
class StoryMenuItem(MenuItem): class StoryMenuItem(MenuItem):
def __init__(self, label, story, flags=0, **kws): def __init__(self, label, story, flags=0, **kws):
# arg= .. handled by super
self.story = story self.story = story
self.flags = flags self.flags = flags
super().__init__(label, **kws) super().__init__(label, **kws)

View File

@ -11,7 +11,8 @@ from ustruct import pack, unpack_from
from ckcc import watchpoint, is_simulator from ckcc import watchpoint, is_simulator
from utils import problem_file_line, call_later_ms from utils import problem_file_line, call_later_ms
from version import supports_hsm, is_devmode, MAX_TXN_LEN, MAX_UPLOAD_LEN from version import supports_hsm, is_devmode, MAX_TXN_LEN, MAX_UPLOAD_LEN
from exceptions import FramingError, CCBusyError, HSMDenied, HSMCMDDisabled from exceptions import FramingError, CCBusyError, HSMDenied, HSMCMDDisabled, SpendPolicyViolation
from pincodes import pa
# Unofficial, unpermissioned... numbers # Unofficial, unpermissioned... numbers
COINKITE_VID = 0xd13e COINKITE_VID = 0xd13e
@ -68,6 +69,14 @@ HSM_DISABLE_CMDS = frozenset({
"hsms", "hsms",
}) })
# spending policy active: blacklist some commands
# - 'pass' may be allowed if 'okeys' is enabled
HOBBLED_CMDS = frozenset({
'enrl', # no new multisigs during policy enforcement
'back', # no backups
'bagi', 'dfu_', # just in case
}) | HSM_DISABLE_CMDS
# singleton instance of USBHandler() # singleton instance of USBHandler()
handler = None handler = None
@ -217,6 +226,8 @@ class USBHandler:
except CCBusyError: except CCBusyError:
# auth UX is doing something else # auth UX is doing something else
resp = b'busy' resp = b'busy'
except SpendPolicyViolation:
resp = b'err_Spending policy in effect'
except HSMDenied: except HSMDenied:
resp = b'err_Not allowed in HSM mode' resp = b'err_Not allowed in HSM mode'
except HSMCMDDisabled: except HSMCMDDisabled:
@ -240,7 +251,7 @@ class USBHandler:
# catch bugs and fuzzing too # catch bugs and fuzzing too
if is_simulator() or is_devmode: if is_simulator() or is_devmode:
print("USB request caused this: ", end='') print("USB request caused this: ", end='')
# sys.print_exception(exc) sys.print_exception(exc)
resp = b'err_Confused ' + problem_file_line(exc) resp = b'err_Confused ' + problem_file_line(exc)
if not success: if not success:
@ -345,7 +356,7 @@ class USBHandler:
except: except:
raise FramingError('decode') raise FramingError('decode')
if cmd[0].isupper() and is_devmode: if is_devmode and cmd[0].isupper():
# special hacky commands to support testing w/ the simulator # special hacky commands to support testing w/ the simulator
try: try:
from usb_test_commands import do_usb_command from usb_test_commands import do_usb_command
@ -358,7 +369,18 @@ class USBHandler:
if cmd not in HSM_WHITELIST: if cmd not in HSM_WHITELIST:
raise HSMDenied raise HSMDenied
if not settings.get('hsmcmd', False): if pa.hobbled_mode:
# block some commands when we are hobbled.
if cmd in HOBBLED_CMDS:
raise SpendPolicyViolation
if cmd in {'pwok', 'pass'}:
from ccc import sssp_spending_policy
if not sssp_spending_policy('okeys'):
raise SpendPolicyViolation
elif not settings.get('hsmcmd', False):
# block these HSM-related command if not using feature
if cmd in HSM_DISABLE_CMDS: if cmd in HSM_DISABLE_CMDS:
raise HSMCMDDisabled raise HSMCMDDisabled
@ -394,10 +416,12 @@ class USBHandler:
if cmd == 'dwld': if cmd == 'dwld':
offset, length, fileno = unpack_from('<III', args) offset, length, fileno = unpack_from('<III', args)
assert len(args) == 12, 'badlen'
return await self.handle_download(offset, length, fileno) return await self.handle_download(offset, length, fileno)
if cmd == 'ncry': if cmd == 'ncry':
version, his_pubkey = unpack_from('<I64s', args) version, his_pubkey = unpack_from('<I64s', args)
assert len(args) == 68, 'badlen'
return self.handle_crypto_setup(version, his_pubkey) return self.handle_crypto_setup(version, his_pubkey)
@ -427,9 +451,9 @@ class USBHandler:
if cmd == 'smsg': if cmd == 'smsg':
# sign message # sign message
addr_fmt, len_subpath, len_msg = unpack_from('<III', args) addr_fmt, len_subpath, len_msg = unpack_from('<III', args)
assert len(args) == (12 + len_subpath + len_msg), 'badlen'
subpath = args[12:12+len_subpath] subpath = args[12:12+len_subpath]
msg = args[12+len_subpath:] msg = args[12+len_subpath:]
assert len(msg) == len_msg, "badlen"
from auth import sign_msg from auth import sign_msg
sign_msg(msg, subpath, addr_fmt) sign_msg(msg, subpath, addr_fmt)
@ -458,6 +482,7 @@ class USBHandler:
xfp_paths = [] xfp_paths = []
for i in range(N): for i in range(N):
assert offset < len(args), 'badlen'
ln = args[offset] ln = args[offset]
assert 1 <= ln <= 16, 'badlen' assert 1 <= ln <= 16, 'badlen'
xfp_paths.append(unpack_from('<%dI' % ln, args, offset+1)) xfp_paths.append(unpack_from('<%dI' % ln, args, offset+1))
@ -473,6 +498,7 @@ class USBHandler:
from auth import usb_show_address from auth import usb_show_address
addr_fmt, = unpack_from('<I', args) addr_fmt, = unpack_from('<I', args)
assert len(args) >= 4, 'badlen'
# regression patch of AFC_BECH32M flag # regression patch of AFC_BECH32M flag
# fixed here https://github.com/Coldcard/ckcc-protocol/commit/a6d901f9fca50755835eca895586ca74d0ca81ed # fixed here https://github.com/Coldcard/ckcc-protocol/commit/a6d901f9fca50755835eca895586ca74d0ca81ed
if addr_fmt == 0x17: # old P2TR if addr_fmt == 0x17: # old P2TR
@ -484,6 +510,7 @@ class USBHandler:
# - text config file must already be uploaded # - text config file must already be uploaded
file_len, file_sha = unpack_from('<I32s', args) file_len, file_sha = unpack_from('<I32s', args)
assert len(args) == 36, 'badlen'
if file_sha != self.file_checksum.digest(): if file_sha != self.file_checksum.digest():
return b'err_Checksum' return b'err_Checksum'
assert 100 < file_len <= (20*200), "badlen" assert 100 < file_len <= (20*200), "badlen"
@ -498,19 +525,20 @@ class USBHandler:
# Quick check to test if we have a wallet already installed. # Quick check to test if we have a wallet already installed.
from multisig import MultisigWallet from multisig import MultisigWallet
M, N, xfp_xor = unpack_from('<3I', args) M, N, xfp_xor = unpack_from('<3I', args)
assert len(args) == 12, 'badlen'
return int(MultisigWallet.quick_check(M, N, xfp_xor)) return int(MultisigWallet.quick_check(M, N, xfp_xor))
if cmd == 'stxn': if cmd == 'stxn':
# sign transaction # sign transaction
txn_len, flags, txn_sha = unpack_from('<II32s', args) txn_len, flags, txn_sha = unpack_from('<II32s', args)
assert len(args) == 40, 'badlen'
if txn_sha != self.file_checksum.digest(): if txn_sha != self.file_checksum.digest():
return b'err_Checksum' return b'err_Checksum'
assert 50 < txn_len <= MAX_TXN_LEN, "badlen" assert 50 < txn_len <= MAX_TXN_LEN, "badlen"
from auth import sign_transaction from auth import sign_transaction
sign_transaction(txn_len, (flags & STXN_FLAGS_MASK), txn_sha) sign_transaction(txn_len, (flags & STXN_FLAGS_MASK), txn_sha, input_method="usb")
return None return None
if cmd == 'stok' or cmd == 'bkok' or cmd == 'smok' or cmd == 'pwok': if cmd == 'stok' or cmd == 'bkok' or cmd == 'smok' or cmd == 'pwok':
@ -535,7 +563,6 @@ class USBHandler:
# STILL waiting on user # STILL waiting on user
return None return None
if cmd == 'pwok': if cmd == 'pwok':
# return new root xpub # return new root xpub
xpub = req.result xpub = req.result
@ -561,6 +588,7 @@ class USBHandler:
assert settings.get("words", True), 'no seed' assert settings.get("words", True), 'no seed'
assert len(args) < 400, 'too long' assert len(args) < 400, 'too long'
pw = str(args, 'utf8') pw = str(args, 'utf8')
assert len(pw), 'too short'
assert len(pw) < 100, 'too long' assert len(pw) < 100, 'too long'
return start_bip39_passphrase(pw) return start_bip39_passphrase(pw)
@ -570,6 +598,17 @@ class USBHandler:
from auth import start_remote_backup from auth import start_remote_backup
return start_remote_backup() return start_remote_backup()
if cmd == 'rest':
# restore backup from what is already uploaded in PSRAM
file_len, file_sha, bf = unpack_from('<I32sB', args)
assert len(args) == 37, 'badlen'
assert 0 < file_len <= MAX_TXN_LEN, "badlen"
if file_sha != self.file_checksum.digest():
return b'err_Checksum'
from auth import start_remote_restore_backup
return start_remote_restore_backup(file_len, bf)
if cmd == 'blkc': if cmd == 'blkc':
# report which blockchain we are configured for # report which blockchain we are configured for
from chains import current_chain from chains import current_chain
@ -586,6 +625,7 @@ class USBHandler:
# HSM mode "start" -- requires user approval # HSM mode "start" -- requires user approval
if args: if args:
file_len, file_sha = unpack_from('<I32s', args) file_len, file_sha = unpack_from('<I32s', args)
assert len(args) == 36, 'badlen'
if file_sha != self.file_checksum.digest(): if file_sha != self.file_checksum.digest():
return b'err_Checksum' return b'err_Checksum'
assert 2 <= file_len <= (200*1000), "badlen" assert 2 <= file_len <= (200*1000), "badlen"
@ -613,6 +653,8 @@ class USBHandler:
if cmd == 'nwur': # new user if cmd == 'nwur': # new user
from users import Users from users import Users
auth_mode, ul, sl = unpack_from('<BBB', args) auth_mode, ul, sl = unpack_from('<BBB', args)
assert len(args) == (3 + ul + sl), 'badlen'
assert ul and sl, "badlen"
username = bytes(args[3:3+ul]).decode('ascii') username = bytes(args[3:3+ul]).decode('ascii')
secret = bytes(args[3+ul:3+ul+sl]) secret = bytes(args[3+ul:3+ul+sl])
@ -621,6 +663,8 @@ class USBHandler:
if cmd == 'rmur': # delete user if cmd == 'rmur': # delete user
from users import Users from users import Users
ul, = unpack_from('<B', args) ul, = unpack_from('<B', args)
assert len(args) == (1 + ul), 'badlen'
assert ul, "badlen"
username = bytes(args[1:1+ul]).decode('ascii') username = bytes(args[1:1+ul]).decode('ascii')
return Users.delete(username) return Users.delete(username)
@ -628,6 +672,8 @@ class USBHandler:
if cmd == 'user': # auth user (HSM mode) if cmd == 'user': # auth user (HSM mode)
from users import Users from users import Users
totp_time, ul, tl = unpack_from('<IBB', args) totp_time, ul, tl = unpack_from('<IBB', args)
assert len(args) == (6 + ul + tl), 'badlen'
assert ul and tl, "badlen"
username = bytes(args[6:6+ul]).decode('ascii') username = bytes(args[6:6+ul]).decode('ascii')
token = bytes(args[6+ul:6+ul+tl]) token = bytes(args[6+ul:6+ul+tl])
@ -716,7 +762,8 @@ class USBHandler:
length = min(length, MAX_BLK_LEN) length = min(length, MAX_BLK_LEN)
assert 0 <= file_number < 2, 'bad fnum' assert 0 <= file_number < 2, 'bad fnum'
assert 0 <= offset <= MAX_TXN_LEN, "bad offset" assert 0 <= offset < MAX_TXN_LEN, "bad offset"
assert offset + length <= MAX_TXN_LEN, "bad offset"
assert 1 <= length, 'len' assert 1 <= length, 'len'
# maintain a running SHA256 over what's sent # maintain a running SHA256 over what's sent
@ -741,7 +788,6 @@ class USBHandler:
from glob import dis, hsm_active from glob import dis, hsm_active
from utils import check_firmware_hdr from utils import check_firmware_hdr
from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE, FW_HEADER_MAGIC from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE, FW_HEADER_MAGIC
from pincodes import pa
# maintain a running SHA256 over what's received # maintain a running SHA256 over what's received
if offset == 0: if offset == 0:
@ -752,10 +798,11 @@ class USBHandler:
dis.progress_sofar(offset, total_size) dis.progress_sofar(offset, total_size)
assert offset % 256 == 0, 'alignment' assert offset % 256 == 0, 'alignment'
assert offset+len(data) <= total_size <= MAX_UPLOAD_LEN, 'long' assert 1 <= total_size <= MAX_UPLOAD_LEN, 'long'
assert offset + len(data) <= total_size, 'long'
if hsm_active: if hsm_active or pa.hobbled_mode:
# additional restrictions in HSM mode # additional restriction in HSM mode or hobbled: must be PSBT
assert offset+len(data) <= total_size <= MAX_TXN_LEN, 'psbt' assert offset+len(data) <= total_size <= MAX_TXN_LEN, 'psbt'
if offset == 0: if offset == 0:
assert data[0:5] == b'psbt\xff', 'psbt' assert data[0:5] == b'psbt\xff', 'psbt'
@ -834,7 +881,6 @@ class USBHandler:
def handle_bag_number(self, bag_num): def handle_bag_number(self, bag_num):
import version, callgate import version, callgate
from glob import dis, settings from glob import dis, settings
from pincodes import pa
if bag_num and version.is_factory_mode and not version.has_qr: if bag_num and version.is_factory_mode and not version.has_qr:
# check state first # check state first

View File

@ -78,7 +78,7 @@ KEY = 'usr'
UserInfo = namedtuple('UserInfo', 'auth_mode secret last_counter') UserInfo = namedtuple('UserInfo', 'auth_mode secret last_counter')
class Users: class Users:
'''Track users and thier TOTP secrets or hashed passwords''' '''Track users and their TOTP secrets or hashed passwords'''
@classmethod @classmethod
def get(cls): def get(cls):

View File

@ -8,7 +8,7 @@ from ubinascii import hexlify as b2a_hex
from ubinascii import a2b_base64, b2a_base64 from ubinascii import a2b_base64, b2a_base64
from charcodes import OUT_CTRL_ADDRESS, OUT_CTRL_NOWRAP from charcodes import OUT_CTRL_ADDRESS, OUT_CTRL_NOWRAP
from uhashlib import sha256 from uhashlib import sha256
from public_constants import MAX_PATH_DEPTH, AF_CLASSIC from public_constants import MAX_PATH_DEPTH, AF_CLASSIC, AF_P2SH, AF_P2WPKH, AF_P2WSH, AF_P2TR
B2A = lambda x: str(b2a_hex(x), 'ascii') B2A = lambda x: str(b2a_hex(x), 'ascii')
@ -193,34 +193,31 @@ def str2xfp(txt):
# Inverse of xfp2str # Inverse of xfp2str
return ustruct.unpack('<I', a2b_hex(txt))[0] return ustruct.unpack('<I', a2b_hex(txt))[0]
def is_ascii(s):
if len(s) == len(s.encode()):
return True
return False
def is_printable(s): def is_printable(s):
PRINTABLE = range(32, 127)
for ch in s: for ch in s:
if ord(ch) not in PRINTABLE: o = ord(ch)
if o < 32 or o > 126:
return False return False
return True return True
def to_ascii_printable(s, strip=False, only_printable=True): def to_ascii_printable(s, allow_tab_nl=False):
try: try:
s = str(s, 'ascii') # s must be a string!
if strip: assert len(s) == len(s.encode())
s = s.strip() if not allow_tab_nl:
assert is_ascii(s)
if only_printable:
assert is_printable(s) assert is_printable(s)
else:
for ch in s:
o = ord(ch)
assert 32 <= o <= 126 or o == 9 or o == 10
return s return s
except: except:
raise AssertionError("must be ascii" + (" printable" if only_printable else "")) err = "must be ascii printable" + (", tab, or newline" if allow_tab_nl else "")
raise AssertionError(err)
def problem_file_line(exc): def problem_file_line(exc):
# return a string of just the filename.py and line number where # return a string of just the filename.py and line number where
# an exception occured. Best used on AssertionError. # an exception occurred. Best used on AssertionError.
tmp = uio.StringIO() tmp = uio.StringIO()
sys.print_exception(exc, tmp) sys.print_exception(exc, tmp)
@ -252,7 +249,7 @@ def cleanup_deriv_path(bin_path, allow_star=False):
# - do not assume /// is m/0/0/0 # - do not assume /// is m/0/0/0
# - if allow_star, then final position can be * or *h (wildcard) # - if allow_star, then final position can be * or *h (wildcard)
s = to_ascii_printable(bin_path, strip=True).lower() s = to_ascii_printable(str(bin_path, "ascii").strip()).lower()
# empty string is valid # empty string is valid
if s == '': return 'm' if s == '': return 'm'
@ -383,7 +380,7 @@ def check_firmware_hdr(hdr, binary_size):
# - hdr must be a bytearray(FW_HEADER_SIZE+more) # - hdr must be a bytearray(FW_HEADER_SIZE+more)
from sigheader import FW_HEADER_SIZE, FW_HEADER_MAGIC, FWH_PY_FORMAT from sigheader import FW_HEADER_SIZE, FW_HEADER_MAGIC, FWH_PY_FORMAT
from sigheader import MK_1_OK, MK_2_OK, MK_3_OK, MK_4_OK, MK_Q1_OK from sigheader import MK_1_OK, MK_2_OK, MK_3_OK, MK_4_OK, MK_5_OK, MK_Q1_OK
from ustruct import unpack_from from ustruct import unpack_from
from version import hw_label from version import hw_label
import callgate import callgate
@ -412,6 +409,8 @@ def check_firmware_hdr(hdr, binary_size):
ok = (hw_compat & MK_3_OK) ok = (hw_compat & MK_3_OK)
elif hw_label == 'mk4': elif hw_label == 'mk4':
ok = (hw_compat & MK_4_OK) ok = (hw_compat & MK_4_OK)
elif hw_label == 'mk5':
ok = (hw_compat & MK_5_OK)
elif hw_label == 'q1': elif hw_label == 'q1':
ok = (hw_compat & MK_Q1_OK) ok = (hw_compat & MK_Q1_OK)
@ -429,6 +428,8 @@ def clean_shutdown(style=0):
# wipe SPI flash and shutdown (wiping main memory) # wipe SPI flash and shutdown (wiping main memory)
# - mk4: SPI flash not used, but NFC may hold data (PSRAM cleared by bootrom) # - mk4: SPI flash not used, but NFC may hold data (PSRAM cleared by bootrom)
# - bootrom wipes every byte of SRAM, so no need to repeat here # - bootrom wipes every byte of SRAM, so no need to repeat here
# - style=2 => reboot and try login again
# - default is logout and (if applicable) power down.
import callgate import callgate
# save if anything pending # save if anything pending
@ -544,7 +545,7 @@ def parse_extended_key(ln, private=False):
found = pat.search(ln) found = pat.search(ln)
# serialize, and note version code # serialize, and note version code
try: try:
node, chain, addr_fmt, is_private = chains.slip32_deserialize(found.group(0)) node, chain, addr_fmt, is_private = chains.slip132_deserialize(found.group(0))
except: except:
pass pass
@ -642,13 +643,10 @@ def decode_bip21_text(got):
proto, args, addr = None, None, None proto, args, addr = None, None, None
# remove URL protocol: if present # remove query params first - if any
if ':' in got:
proto, got = got.split(':', 1)
# looks like BIP-21 payment URL # looks like BIP-21 payment URL
if '?' in got: if '?' in got:
addr, args = got.split('?', 1) got, args = got.split('?', 1)
# full URL decode here, but assuming no repeated keys # full URL decode here, but assuming no repeated keys
parts = args.split('&') parts = args.split('&')
@ -657,7 +655,12 @@ def decode_bip21_text(got):
k, v = p.split('=', 1) k, v = p.split('=', 1)
args[k] = url_unquote(v) args[k] = url_unquote(v)
# assume it's an bare address for now # remove URL protocol: if present
if ':' in got:
proto, got = got.split(':', 1)
assert proto.lower() == "bitcoin"
# assume it's a bare address for now
if not addr: if not addr:
addr = got addr = got
@ -685,6 +688,35 @@ def decode_bip21_text(got):
raise ValueError('not bip-21') raise ValueError('not bip-21')
def validate_own_address(addr):
ch = chains.current_chain()
addr_l = addr.lower()
if addr_l[:3] in ("bc1", "tb1") or addr_l[:5] == 'bcrt1':
try:
hrp, witver, data = ngu.codecs.segwit_decode(addr)
assert hrp == ch.bech32_hrp
assert witver == 0
if len(data) == 20:
return addr_l, AF_P2WPKH
if len(data) == 32:
return addr_l, AF_P2WSH
except: pass
# Bitcoin main/test/reg base58 address prefixes.
elif addr and addr[0] in '123mn':
try:
raw = ngu.codecs.b58_decode(addr)
assert len(raw) == 21
if raw[0] == ch.b58_addr[0]:
return addr, AF_CLASSIC
if raw[0] == ch.b58_script[0]:
return addr, AF_P2SH
except: pass
assert False, ch.name
def encode_seed_qr(words): def encode_seed_qr(words):
return ''.join('%04d' % bip39.get_word_index(w) for w in words) return ''.join('%04d' % bip39.get_word_index(w) for w in words)
@ -784,4 +816,11 @@ def extract_cosigner(data, af_str):
# emulate coldcard export xpubs # emulate coldcard export xpubs
return {"xfp": xfp, af_str: ek, key_deriv: deriv} return {"xfp": xfp, af_str: ek, key_deriv: deriv}
def node_from_privkey(privkey, chain_code=bytes(32)):
return ngu.hdnode.HDNode().from_chaincode_privkey(chain_code, privkey)
def node_from_pubkey(pubkey, chain_code=bytes(32)):
return ngu.hdnode.HDNode().from_chaincode_pubkey(chain_code, pubkey)
# EOF # EOF

View File

@ -299,7 +299,7 @@ async def ux_dramatic_pause(msg, seconds):
# show a full-screen msg, with a dramatic pause + progress bar # show a full-screen msg, with a dramatic pause + progress bar
n = seconds * 8 n = seconds * 8
dis.fullscreen(msg) dis.fullscreen(msg)
for i in range(n): for i in range(1, n+1):
dis.progress_bar_show(i/n) dis.progress_bar_show(i/n)
await sleep_ms(125) await sleep_ms(125)
@ -349,7 +349,7 @@ async def show_qr_code(data, is_alnum=False, msg=None, **kw):
o = QRDisplaySingle([data], is_alnum, msg=msg, **kw) o = QRDisplaySingle([data], is_alnum, msg=msg, **kw)
await o.interact_bare() await o.interact_bare()
async def ux_enter_bip32_index(prompt, can_cancel=False, unlimited=False): async def ux_enter_bip32_index(prompt, can_cancel=True, unlimited=False):
if unlimited: if unlimited:
max_value = (2 ** 31) - 1 # we handle hardened max_value = (2 ** 31) - 1 # we handle hardened
else: else:
@ -357,12 +357,12 @@ async def ux_enter_bip32_index(prompt, can_cancel=False, unlimited=False):
return await ux_enter_number(prompt=prompt, max_value=max_value, can_cancel=can_cancel) return await ux_enter_number(prompt=prompt, max_value=max_value, can_cancel=can_cancel)
def _import_prompt_builder(title, no_qr, no_nfc, slot_b_only=False): def _import_prompt_builder(title, no_qr, no_nfc, slot_b_only=False, key0=None, key6=None):
from glob import NFC, VD from glob import NFC, VD
prompt, escape = None, KEY_CANCEL+"x" prompt, escape = None, KEY_CANCEL+"x"
if (NFC or VD) or num_sd_slots>1: if (NFC or VD) or (num_sd_slots > 1) or key0 or key6:
if slot_b_only and (num_sd_slots>1): if slot_b_only and (num_sd_slots>1):
prompt = "Press (B) to import %s from lower slot SD Card" % title prompt = "Press (B) to import %s from lower slot SD Card" % title
escape += "b" escape += "b"
@ -388,20 +388,28 @@ def _import_prompt_builder(title, no_qr, no_nfc, slot_b_only=False):
prompt += ", " + KEY_QR + " to scan QR code" prompt += ", " + KEY_QR + " to scan QR code"
escape += KEY_QR escape += KEY_QR
if key6:
prompt += ', (6) ' + key6
escape += '6'
if key0:
prompt += ', (0) ' + key0
escape += '0'
prompt += "." prompt += "."
return prompt, escape return prompt, escape
def export_prompt_builder(what_it_is, no_qr=False, no_nfc=False, key0=None, offer_kt=False, def export_prompt_builder(what_it_is, no_qr=False, no_nfc=False, key0=None, offer_kt=False,
force_prompt=False, txid=None): force_prompt=False, key6=None):
# Build the prompt for export # Build the prompt for export
# - key0 can be for special stuff # - key0 can be for special stuff
from glob import NFC, VD from glob import NFC, VD
prompt, escape = None, KEY_CANCEL+"x" prompt, escape = None, KEY_CANCEL+"x"
if (NFC or VD) or (num_sd_slots>1) or key0 or force_prompt or offer_kt or txid or (not no_qr): if (NFC or VD) or (num_sd_slots>1) or key0 or force_prompt or offer_kt or key6 or (not no_qr):
# no need to spam with another prompt, only option is SD card # no need to spam with another prompt, only option is SD card
prompt = "Press (1) to save %s to SD Card" % what_it_is prompt = "Press (1) to save %s to SD Card" % what_it_is
@ -431,10 +439,6 @@ def export_prompt_builder(what_it_is, no_qr=False, no_nfc=False, key0=None, offe
prompt += ", (4) to show QR code" prompt += ", (4) to show QR code"
escape += '4' escape += '4'
if txid:
prompt += ", (6) for QR Code of TXID"
escape += "6"
if offer_kt: if offer_kt:
prompt += ", (T) to " + offer_kt prompt += ", (T) to " + offer_kt
escape += 't' escape += 't'
@ -443,6 +447,10 @@ def export_prompt_builder(what_it_is, no_qr=False, no_nfc=False, key0=None, offe
prompt += ', (0) ' + key0 prompt += ', (0) ' + key0
escape += '0' escape += '0'
if key6:
prompt += ", (6) " + key6
escape += "6"
prompt += "." prompt += "."
return prompt, escape return prompt, escape
@ -481,7 +489,7 @@ def import_export_prompt_decode(ch):
async def import_export_prompt(what_it_is, is_import=False, no_qr=False, async def import_export_prompt(what_it_is, is_import=False, no_qr=False,
no_nfc=False, title=None, intro='', footnotes='', no_nfc=False, title=None, intro='', footnotes='',
offer_kt=False, slot_b_only=False, force_prompt=False, offer_kt=False, slot_b_only=False, force_prompt=False,
txid=None): key0=None, key6=None):
# Show story allowing user to select source for importing/exporting # Show story allowing user to select source for importing/exporting
# - return either str(mode) OR dict(file_args) # - return either str(mode) OR dict(file_args)
@ -492,9 +500,10 @@ async def import_export_prompt(what_it_is, is_import=False, no_qr=False,
from glob import NFC from glob import NFC
if is_import: if is_import:
prompt, escape = _import_prompt_builder(what_it_is, no_qr, no_nfc, slot_b_only) prompt, escape = _import_prompt_builder(what_it_is, no_qr, no_nfc, slot_b_only,
key0=key0, key6=key6)
else: else:
prompt, escape = export_prompt_builder(what_it_is, no_qr, no_nfc, txid=txid, prompt, escape = export_prompt_builder(what_it_is, no_qr, no_nfc, key6=key6, key0=key0,
force_prompt=force_prompt, offer_kt=offer_kt) force_prompt=force_prompt, offer_kt=offer_kt)
# TODO: detect if we're only asking A or B, when just one card is inserted # TODO: detect if we're only asking A or B, when just one card is inserted

View File

@ -60,7 +60,7 @@ class PressRelease:
return ch return ch
async def ux_enter_number(prompt, max_value, can_cancel=False, value=''): async def ux_enter_number(prompt, max_value, can_cancel=True, value=''):
# return the decimal number which the user has entered # return the decimal number which the user has entered
# - default/blank value assumed to be zero # - default/blank value assumed to be zero
# - clamps large values to the max # - clamps large values to the max

View File

@ -76,7 +76,7 @@ class PressRelease:
self.last_key = ch self.last_key = ch
return ch return ch
async def ux_enter_number(prompt, max_value, can_cancel=False, value=''): async def ux_enter_number(prompt, max_value, can_cancel=True, value=''):
# return the decimal number which the user has entered # return the decimal number which the user has entered
# - default/blank value assumed to be zero # - default/blank value assumed to be zero
# - clamps large values to the max # - clamps large values to the max
@ -121,7 +121,7 @@ async def ux_enter_number(prompt, max_value, can_cancel=False, value=''):
dis.text(0, 4, ' '*CHARS_W) dis.text(0, 4, ' '*CHARS_W)
elif ch == KEY_CANCEL: elif ch == KEY_CANCEL:
if can_cancel: if can_cancel:
# quit if they press X on empty screen # quit if they press CANCEL on any screen
return None return None
elif '0' <= ch <= '9': elif '0' <= ch <= '9':
if len(value) == max_w: if len(value) == max_w:
@ -553,6 +553,16 @@ def ux_draw_words(y, num_words, words):
# Draw seed words on single screen (hard) and return x/y position of start of each # Draw seed words on single screen (hard) and return x/y position of start of each
from glob import dis from glob import dis
if num_words == 2:
# simple version for first & last words, used only during login to spending policy
X = 14
Y = y+1
dis.text(X-7, Y, 'FIRST: %s' % words[0])
dis.text(X-4, Y+1, '')
dis.text(X-6, Y+2, 'LAST: %s' % words[-1])
return [ (X, Y), (X, Y+2) ]
if num_words == 12: if num_words == 12:
cols = 2 cols = 2
xpos = [2, 18] xpos = [2, 18]
@ -568,7 +578,7 @@ def ux_draw_words(y, num_words, words):
if num_words == 12: if num_words == 12:
# luxious space after colon # luxious space after colon
msg = ('%2d: ' % n) + word msg = ('%2d: ' % n) + word
x_off = 3 x_off = 4
else: else:
if n <= n_per_c: if n <= n_per_c:
# no space in front of 1: thru N: in leftmost column of 3 # no space in front of 1: thru N: in leftmost column of 3
@ -584,7 +594,7 @@ def ux_draw_words(y, num_words, words):
return rv return rv
async def seed_word_entry(prompt, num_words, has_checksum=True, done_cb=None): async def seed_word_entry(prompt, num_words, has_checksum=True, done_cb=None, line2=None):
# Accept a seed phrase, only # Accept a seed phrase, only
# - replaces WordNestMenu on Q1 # - replaces WordNestMenu on Q1
# - max word length is 8, min is 3 # - max word length is 8, min is 3
@ -594,13 +604,23 @@ async def seed_word_entry(prompt, num_words, has_checksum=True, done_cb=None):
assert num_words and prompt assert num_words and prompt
not24 = (num_words != 24)
def redraw_words(wrds=None): def redraw_words(wrds=None):
if not wrds: if not wrds:
wrds = ['' for _ in range(num_words)] wrds = ['' for _ in range(num_words)]
dis.clear() dis.clear()
dis.text(None, 0, prompt, invert=1) dis.text(None, 0, prompt, invert=1)
p = ux_draw_words(2 if num_words != 24 else 1, num_words, wrds)
Y = 2 if not24 else 1
if line2 and not24:
# add second line, if provided, but only if words length < 24
# currently only used to show backup filename during backup pwd entry
dis.text(None, 1, line2, invert=1)
Y += 1
p = ux_draw_words(Y, num_words, wrds)
return wrds, p return wrds, p
words, pos = redraw_words() words, pos = redraw_words()
@ -647,7 +667,7 @@ async def seed_word_entry(prompt, num_words, has_checksum=True, done_cb=None):
what, vals = decode_qr_result(got, expect_secret=True) what, vals = decode_qr_result(got, expect_secret=True)
except QRDecodeExplained as e: except QRDecodeExplained as e:
err_msg = str(e) err_msg = str(e)
redraw_words() redraw_words(words)
continue continue
if what != "words": if what != "words":
@ -805,7 +825,6 @@ class QRScannerInteraction:
while 1: while 1:
if task.done(): if task.done():
data = await task data = await task
#print("Scanned: %r" % data)
break break
dis.image(None, 40, 'scan_%d' % frames[ph]) dis.image(None, 40, 'scan_%d' % frames[ph])
@ -818,7 +837,12 @@ class QRScannerInteraction:
data = None data = None
break break
task.cancel() if not task.done():
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
# clear screen right away so user knows we got it # clear screen right away so user knows we got it
dis.clear() dis.clear()
@ -861,7 +885,7 @@ class QRScannerInteraction:
file_type, _, data = decode_qr_result(got, expect_bbqr=True) file_type, _, data = decode_qr_result(got, expect_bbqr=True)
if file_type == 'U': if file_type == 'U':
data = data.strip() data = data.strip()
if data[0] == '{' and data[-1] == '}': if data[:1] == b'{' and data[-1:] == b'}':
file_type = 'J' file_type = 'J'
if file_type != 'J': if file_type != 'J':
raise QRDecodeExplained('Expected JSON data') raise QRDecodeExplained('Expected JSON data')
@ -902,6 +926,8 @@ class QRScannerInteraction:
async def scan_anything(self, expect_secret=False, tmp=False): async def scan_anything(self, expect_secret=False, tmp=False):
# start a QR scan, and act on what we find, whatever it may be. # start a QR scan, and act on what we find, whatever it may be.
from ux import ux_show_story from ux import ux_show_story
from pincodes import pa
problem = None problem = None
while 1: while 1:
prompt = 'Scan any QR code, or CANCEL' if not expect_secret else \ prompt = 'Scan any QR code, or CANCEL' if not expect_secret else \
@ -923,6 +949,21 @@ class QRScannerInteraction:
problem = "Unable to decode QR" problem = "Unable to decode QR"
continue continue
if pa.hobbled_mode:
# block most imports in hobbled mode.
# - specific checks in place for teleport (PSBT is okay)
from ccc import sssp_spending_policy
whitelist = {'psbt', 'addr', 'vmsg', 'text', 'xpub', 'teleport' }
sv_ok = sssp_spending_policy('okeys')
if sv_ok:
# seed vault, and tmp seeds are okay with user, even in hobble mode
whitelist.update({'xprv', 'words'})
if what not in whitelist:
await ux_show_story("Blocked when Spending Policy is in force.", title='Sorry')
return
if what == 'xprv': if what == 'xprv':
from actions import import_extended_key_as_secret from actions import import_extended_key_as_secret
text_xprv, = vals text_xprv, = vals
@ -963,6 +1004,7 @@ class QRScannerInteraction:
elif what == "wif": elif what == "wif":
data, = vals data, = vals
wif_str, key_pair, compressed, testnet = data wif_str, key_pair, compressed, testnet = data
from wif import ux_visualize_wif
await ux_visualize_wif(wif_str, key_pair, compressed, testnet) await ux_visualize_wif(wif_str, key_pair, compressed, testnet)
elif what == "vmsg": elif what == "vmsg":
@ -1019,7 +1061,7 @@ async def qr_psbt_sign(decoder, psbt_len, raw):
psbt_len = total psbt_len = total
else: else:
with SFFile(TXN_INPUT_OFFSET, max_size=psbt_len) as out: with SFFile(TXN_INPUT_OFFSET, length=psbt_len) as out:
taste = out.read(10) taste = out.read(10)
_, output_encoder, _ = psbt_encoding_taster(taste, psbt_len) _, output_encoder, _ = psbt_encoding_taster(taste, psbt_len)
@ -1071,20 +1113,22 @@ async def ux_visualize_bip21(proto, addr, args):
# - imho, a bare address is a valid BIP-21 URL so we come here too # - imho, a bare address is a valid BIP-21 URL so we come here too
# - validate address ownership on request # - validate address ownership on request
from ux import ux_show_story from ux import ux_show_story
from chains import current_chain
msg = show_single_address(addr) + '\n\n' msg = show_single_address(addr) + '\n\n'
args = args or {} args = args or {}
if 'amount' in args: if 'amount' in args:
msg += 'Amount: '
try: try:
amt = args.pop('amount') amt = args.pop('amount')
whole, frac = amt.split('.', 1) whole, _, frac = amt.partition('.')
frac = int(frac) if frac else 0 assert whole.isdigit()
whole = int(whole) if whole else 0 assert len(whole) <= 8
msg += '%d.%08d BTC\n' % (whole, frac) assert len(frac) <= 8
sats = int((whole or '0') + (frac + '00000000')[:8])
msg += 'Amount: %s %s\n' % current_chain().render_value(sats)
except: except:
msg += '(corrupt)\n' msg += 'Amount: (corrupt)\n'
for fn in ['label', 'message', 'lightning']: for fn in ['label', 'message', 'lightning']:
if fn in args: if fn in args:
@ -1101,15 +1145,8 @@ async def ux_visualize_bip21(proto, addr, args):
if ch == '1': if ch == '1':
from ownership import OWNERSHIP from ownership import OWNERSHIP
await OWNERSHIP.search_ux(addr) await OWNERSHIP.search_ux(addr, args)
async def ux_visualize_wif(wif_str, kp, compressed, testnet):
from ux import ux_show_story
msg = wif_str + "\n\n"
msg += "chain: %s\n\n" % ("XTN" if testnet else "BTC")
msg += "private key hex:\n" + b2a_hex(kp.privkey()).decode() + "\n\n"
msg += "public key sec:\n" + b2a_hex(kp.pubkey().to_bytes(not compressed)).decode() + "\n\n"
await ux_show_story(msg, title="WIF")
async def qr_msg_sign_done(signature, address, text): async def qr_msg_sign_done(signature, address, text):
from ux import ux_show_story from ux import ux_show_story
@ -1168,7 +1205,6 @@ async def show_bbqr_codes(type_code, data, msg, already_hex=False):
from ux import ux_wait_keydown from ux import ux_wait_keydown
import uqr import uqr
assert not PSRAM.is_at(data, 0) # input data would be overwritten with our work
assert type_code in TYPE_LABELS assert type_code in TYPE_LABELS
dis.fullscreen('Generating BBQr...', .1) dis.fullscreen('Generating BBQr...', .1)
@ -1179,6 +1215,11 @@ async def show_bbqr_codes(type_code, data, msg, already_hex=False):
else: else:
# default to Base32, because always best option # default to Base32, because always best option
encoding = '2' encoding = '2'
if isinstance(data, str):
# 'U'/'J' payloads are UTF-8 text; b32encode consumes the UTF-8
# bytes, so convert now to keep length/slicing consistent (else
# multi-byte chars overflow target_vers -> assert below trips)
data = data.encode()
data_len = len(data) data_len = len(data)
# try a few select resolutions (sizes) in order such that we use either single QR # try a few select resolutions (sizes) in order such that we use either single QR

View File

@ -80,6 +80,7 @@ def probe_system():
from sigheader import RAM_BOOT_FLAGS, RBF_FACTORY_MODE from sigheader import RAM_BOOT_FLAGS, RBF_FACTORY_MODE
import ckcc, callgate, machine import ckcc, callgate, machine
from machine import Pin
hw_label = 'mk4' hw_label = 'mk4'
has_608 = True has_608 = True
@ -97,7 +98,7 @@ def probe_system():
# detect Q1 based on pins.csv # detect Q1 based on pins.csv
try: try:
machine.Pin('LCD_TEAR') # only defined on Q1 build, will error otherwise Pin('LCD_TEAR') # only defined on Q1 build, will error otherwise
has_qr = True has_qr = True
num_sd_slots = 2 num_sd_slots = 2
hw_label = 'q1' hw_label = 'q1'
@ -108,6 +109,15 @@ def probe_system():
except ValueError: except ValueError:
pass pass
try:
# only defined on Mk4/5 build, will error otherwise; was open on Mk1-4, low on Mk5
s0 = Pin('STRAP_MK5', mode=Pin.IN, pull=Pin.PULL_UP)
if s0() == 0:
hw_label = 'mk5'
mk_num = 5
except ValueError:
pass
# Boot loader needs to tell us stuff about how we were booted, sometimes: # Boot loader needs to tell us stuff about how we were booted, sometimes:
# - did we just install a new version, for example (obsolete in mk4) # - did we just install a new version, for example (obsolete in mk4)
# - are we running in "factory mode" with flash un-secured? # - are we running in "factory mode" with flash un-secured?

View File

@ -2,7 +2,7 @@
# #
# wallet.py - A place you find UTXO, addresses and descriptors. # wallet.py - A place you find UTXO, addresses and descriptors.
# #
import chains import chains, version
from descriptor import Descriptor from descriptor import Descriptor
from stash import SensitiveValues from stash import SensitiveValues
@ -38,9 +38,16 @@ class MasterSingleSigWallet(WalletABC):
def __init__(self, addr_fmt, path=None, account_idx=0, chain_name=None): def __init__(self, addr_fmt, path=None, account_idx=0, chain_name=None):
# Construct a wallet based on current master secret, and chain. # Construct a wallet based on current master secret, and chain.
# - path is optional, and then we use standard path for addr_fmt # - path is optional, and then we use standard path for addr_fmt
# - path can be overriden when we come here via address explorer # - path can be overridden when we come here via address explorer
n = chains.addr_fmt_label(addr_fmt) n = chains.addr_fmt_label(addr_fmt)
if not version.has_qwerty:
# Mk4 tiny display
# Classic P2PKH -> P2PKH
# Segwit P2WPKH -> P2WPKH
# P2SH-Segwit -> no change (should not be used that much)
n = n.split(" ")[-1]
purpose = chains.af_to_bip44_purpose(addr_fmt) purpose = chains.af_to_bip44_purpose(addr_fmt)
prefix = path or 'm/%dh/{coin_type}h/{account}h' % purpose prefix = path or 'm/%dh/{coin_type}h/{account}h' % purpose
@ -50,12 +57,13 @@ class MasterSingleSigWallet(WalletABC):
self.chain = chains.current_chain() self.chain = chains.current_chain()
if account_idx != 0: if account_idx != 0:
n += ' Account#%d' % account_idx rv = " Account#%d" if version.has_qwerty else " Acct#%d"
n += rv % account_idx
if self.chain.ctype == 'XTN': if self.chain.ctype == 'XTN':
n += ' (Testnet)' n += ' (Testnet)' if version.has_qwerty else " XTN"
if self.chain.ctype == 'XRT': if self.chain.ctype == 'XRT':
n += ' (Regtest)' n += ' (Regtest)' if version.has_qwerty else " XRT"
self.name = n self.name = n

View File

@ -95,7 +95,7 @@ async def perform_web2fa(label, shared_secret):
return False return False
async def web2fa_enroll(label, ss=None): async def web2fa_enroll(ss=None):
# #
# Enroll: Pick a secret and test they have loaded it into their phone. # Enroll: Pick a secret and test they have loaded it into their phone.
# #
@ -115,22 +115,21 @@ async def web2fa_enroll(label, ss=None):
# - can't fit any metadata, like username or our serial # in there # - can't fit any metadata, like username or our serial # in there
# - better on Q1 where no limitations for this size of QR # - better on Q1 where no limitations for this size of QR
qr = 'otpauth://totp/{nm}?secret={ss}'.format(ss=ss, nm = 'COLDCARD' if has_qr else 'CC' # must be url-safe
nm=url_quote(label if has_qr else label[0:4])) qr = 'otpauth://totp/{nm}?secret={ss}'.format(ss=ss, nm=nm)
while 1: while 1:
# show QR for enroll # show QR for enroll
await show_qr_code(qr, is_alnum=False, msg="Import into 2FA Mobile App", await show_qr_code(qr, is_alnum=False, msg="Import into 2FA Mobile App",
force_msg=True) force_msg=True)
# important: force them to prove they store it correctly # important: force them to prove they stored it correctly
ok = await perform_web2fa('Enroll: ' + label, ss) ok = await perform_web2fa('Enroll: COLDCARD', ss)
if ok: break if ok: break
ch = await ux_show_story("That isn't correct. Please re-import and/or " ch = await ux_show_story("That isn't correct. Please re-import and/or "
"try again or %s to give up." % X) "try again or %s to give up." % X)
if ch == 'x': if ch == 'x':
# mk4 only?
return None return None
return ss return ss

423
shared/wif.py Normal file
View File

@ -0,0 +1,423 @@
# (c) Copyright 2026 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
import chains, ngu, version
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from ux import ux_show_story, ux_confirm, the_ux, import_export_prompt, ux_input_text, show_qr_code
from menu import MenuSystem, MenuItem
from utils import problem_file_line, show_single_address, node_from_pubkey
from files import CardSlot, CardMissingError, needs_microsd
from glob import settings
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
from public_constants import AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH, AF_P2SH
from msgsign import msg_signing_done
MAX_ITEMS = 30
def decode_wif(wif):
# Decode base58 encoded WIF string, return keypair and metadata
raw = ngu.codecs.b58_decode(wif)
assert raw[0] in (0xef, 0x80)
testnet = True if raw[0] == 0xef else False
assert len(raw) in (33, 34)
compressed = False
if len(raw) == 34: # compressed pubkey
assert raw[33] == 0x01
compressed = True
sk = raw[1:33]
kp = ngu.secp256k1.keypair(sk) # catches wrong private keys
return kp, testnet, compressed
def iter_wif_store_addresses(addr_fmt):
# nothing found among singlesig & registered multisig wallets
# check WIF store
wifs = settings.get("wifs", [])
if not wifs: return
for i, (pk, sk) in enumerate(wifs):
node = node_from_pubkey(a2b_hex(pk))
yield i, chains.current_chain().address(node, addr_fmt)
def save_wif_store_items(new_wifs):
saved = settings.get("wifs", [])
len_saved = len(saved)
unique = []
dups = 0
for item in new_wifs:
if item in unique:
continue
if item not in saved:
unique.append(item)
else:
dups += 1
err = ("No valid WIF key found." + (" Contains duplicate WIF(s)" if dups else ""))
assert unique, err
err = ("Max %d items allowed in WIF Store.\n\nAttempted to import %d keys,"
" while remaining WIF store capacity is only %d. Please, make room"
" first." % (MAX_ITEMS, len(unique), MAX_ITEMS - len_saved))
assert (len_saved + len(unique)) <= MAX_ITEMS, err
saved.extend(unique)
settings.set('wifs', saved)
settings.save()
return len(unique)
async def ux_visualize_wif(wif_str, kp, compressed, testnet):
ch_str = ("XTN" if testnet else "BTC")
sk = b2a_hex(kp.privkey()).decode()
pk = b2a_hex(kp.pubkey().to_bytes(not compressed)).decode()
msg = "%s\n\nchain: %s\n\nPrivkey:\n%s\n\nPubkey:\n%s" % (wif_str, ch_str, sk, pk)
esc = ""
if compressed and (testnet == (chains.current_chain().ctype != "BTC")):
# we only support compressed in WIF store
msg += "\n\nPress (1) to import to WIF Store."
esc += "1"
ch = await ux_show_story(msg, title="WIF Key", escape=esc)
if ch == "1":
title = "Success"
try:
save_wif_store_items([(pk, sk)])
msg = "Saved to WIF Store."
except Exception as e:
title = "Failure"
msg = str(e)
await ux_show_story(msg, title=title)
class WIFStoreMenu(MenuSystem):
def __init__(self):
items = self.construct()
super().__init__(items)
@classmethod
async def make(cls, *a):
if not settings.get("wifs", None):
intro = ("Individual private keys, encoded as WIF (Wallet Import Format) keys"
" can be imported and used for signing. Any PSBT that uses a WIF stored here"
" will be signed as normal, but warning is shown."
" Remove all imported keys to disable WIF store signing")
ch = await ux_show_story(intro, title="WIF Store")
if ch != 'y': return
return cls()
def update_contents(self):
tmp = self.construct()
self.replace_items(tmp)
def construct(self):
from glob import dis
from seed import not_hobbled_mode
dis.fullscreen("Wait...")
ch = chains.current_chain()
wifs = settings.get('wifs', [])
items = []
if len(wifs) < MAX_ITEMS:
items.append(MenuItem('Import WIF', f=self.import_wif, predicate=not_hobbled_mode))
a_items = []
export_all = []
for i, (pk, sk) in enumerate(wifs):
wif = ngu.codecs.b58_encode(ch.b58_privkey + a2b_hex(sk) + b'\x01')
export_all.append(wif)
submenu = [
MenuItem("Detail", f=self.detail, arg=(wif,pk,sk)),
MenuItem("Descriptors", f=self.show_desc_step1, arg=pk),
MenuItem("Addresses", f=self.show_addr_step1, arg=pk),
MenuItem("Sign MSG", f=self.sign_msg_step1, arg=sk),
MenuItem('Delete', f=self.delete, arg=(i, pk), predicate=not_hobbled_mode),
]
# cannot use truncate_address here, as it does nto fit on Mk4 (because padded numbering)
clen = 12 if version.has_qwerty else 5
a_items.append(MenuItem("%2d: %s" % (i+1, wif[0:clen] + '' + wif[-clen:]),
menu=MenuSystem(submenu)))
if a_items:
items += a_items
if len(a_items) > 1:
items.append(MenuItem("Export All", f=self.export_all, arg=export_all))
items.append(MenuItem("Clear All", f=self.clear_all, predicate=not_hobbled_mode))
else:
items.append(MenuItem("(none yet)"))
return items
async def detail(self, a, b, item):
wif, pk, sk = item.arg
msg = "%s\n\nPrivkey:\n%s\n\nPubkey:\n%s" % (wif, sk, pk)
from export import export_contents
title = "WIF"
await export_contents(title, wif, "wif.txt", None, None,
force_prompt=True, intro=msg, ux_title=title)
async def show_desc_step1(self, a, b, item):
rv = [
MenuItem(chains.addr_fmt_label(af), f=self.show_desc_step2, arg=(item.arg, af))
for af in chains.SINGLESIG_AF
]
the_ux.push(MenuSystem(rv))
async def show_desc_step2(self, a, b, item):
# allow to export pubkey, instead of main detail where WIF is exported
pk, af = item.arg
title = "Descriptor"
if af == AF_P2WPKH:
desc = "wpkh(%s)"
elif af == AF_CLASSIC:
desc = "pkh(%s)"
else:
assert af == AF_P2WPKH_P2SH
desc = "sh(wpkh(%s))"
from descriptor import append_checksum
desc = append_checksum(desc % pk)
from export import export_contents
await export_contents(title, desc, "wif_desc_%d.txt" % af, None, None,
force_prompt=True, intro=desc, ux_title=title)
async def show_addr_step1(self, a, b, item):
rv = [
MenuItem(chains.addr_fmt_label(af), f=self.show_addr_step2, arg=(item.arg, af))
for af in chains.SINGLESIG_AF
]
the_ux.push(MenuSystem(rv))
async def show_addr_step2(self, a, b, item):
pubkey, af = item.arg
node = node_from_pubkey(a2b_hex(pubkey))
addr = chains.current_chain().address(node, af)
msg = show_single_address(addr)
ux_title = chains.addr_fmt_label(af) if version.has_qwerty else None
from export import export_contents
await export_contents("Address", addr, "wif_addr.txt", None, None,
force_prompt=True, intro=msg, ux_title=ux_title)
async def sign_msg_step1(self, a, b, item):
privkey = a2b_hex(item.arg)
rv = [
MenuItem(chains.addr_fmt_label(af), f=self.sign_msg_step2, arg=(privkey, af))
for af in chains.SINGLESIG_AF
]
the_ux.push(MenuSystem(rv))
async def sign_msg_step2(self, a, b, item):
from glob import NFC
from actions import file_picker
from auth import approve_msg_sign
ch = await import_export_prompt("message", is_import=True, force_prompt=True,
key0="to input message manually",
no_qr=not version.has_qwerty)
if ch == KEY_CANCEL:
return
elif ch == "0":
msg = await ux_input_text("", confirm_exit=False)
elif ch == KEY_NFC:
msg = await NFC.read_bip322_msg()
elif ch == KEY_QR:
from ux_q1 import QRScannerInteraction
msg = await QRScannerInteraction().scan_text('Scan message from a QR code')
else:
fn = await file_picker(suffix='.txt')
if not fn: return
with CardSlot(readonly=True, **ch) as card:
with open(fn, 'rt') as fd:
msg = fd.read()
if not msg: return
privkey, af = item.arg
await approve_msg_sign(msg, "", af, privkey=privkey, approved_cb=msg_signing_done)
async def delete(self, a, b, item):
# no confirm, stakes are low
if not await ux_confirm("Delete WIF key?"):
return
idx, pubkey = item.arg
wifs = settings.get('wifs', [])
if not wifs: return
try:
entry = wifs[idx]
assert entry[0] == pubkey
del wifs[idx]
settings.set('wifs', wifs)
settings.save()
except IndexError:
return
the_ux.pop() # pop submenu
self.update_contents()
async def clear_all(self, *a):
if await ux_confirm("Remove all saved WIF keys?", confirm_key='4'):
settings.remove_key("wifs")
settings.save()
self.update_contents()
async def export_all(self, a, b, item):
wifs = item.arg
from export import export_contents
title = "WIF Store"
await export_contents(title, "\n".join(wifs), "wif_store.txt",
None, None, force_prompt=True, ux_title=title)
async def import_wif(self, *a):
from glob import NFC, dis
from actions import file_picker
label = "WIF private key"
ch = await import_export_prompt(label, is_import=True, key0="to input WIF manually")
if ch == KEY_CANCEL:
return
elif ch == KEY_NFC:
got = await NFC.read_wif()
elif ch == KEY_QR:
from ux_q1 import QRScannerInteraction
got = await QRScannerInteraction().scan_text(label)
elif ch == "0":
got = await ux_input_text("", confirm_exit=False, max_len=52) # compressed WIF key str length is 52
else:
# pick a likely-looking file: just looking at size and extension
# - kinda big so we can import paper wallet directly
fn = await file_picker(suffix=['.csv', '.txt'], min_size=51, max_size=11000,
none_msg="Must contain WIF(s)", **ch)
if not fn: return
try:
with CardSlot(readonly=True, **ch) as card:
with open(fn, 'rt') as fd:
got = fd.read()
except CardMissingError:
await needs_microsd()
return
except Exception as e:
await ux_show_story('Failed to read file!\n\n%s' % e)
return
if not got:
return
dis.fullscreen("Wait...")
# allow commas, spaces, and newlines as separators
got = got.replace(',', ' ').split()
try:
new_wifs = []
for here in got:
here = here.strip()
if not here:
continue
try:
kp, testnet, compressed = decode_wif(here)
except Exception:
# ignore garbage text, headers, addresses, etc.
continue
assert compressed, "compressed only"
assert testnet == (chains.current_chain().ctype != "BTC"), "chain"
sk = b2a_hex(kp.privkey()).decode()
pk = b2a_hex(kp.pubkey().to_bytes()).decode()
new_wifs.append((pk, sk))
save_wif_store_items(new_wifs)
self.update_contents()
except Exception as e:
await ux_show_story('Failed to import WIF.\n\n%s\n%s' % (e, problem_file_line(e)),
title="Failure")
class WIFStore:
def __init__(self):
wifs = settings.get('wifs', [])
self.wifs = [] # max 30 items, each (pubkey, privkey)
for pk, sk in wifs:
self.wifs.append((a2b_hex(pk), a2b_hex(sk)))
# built lazily, on first match_address_hash() call
self._pkh = [] # hash160(pubkey) — P2PKH / P2WPKH
self._sh = [] # hash160(0014 || _pkh) — P2SH-P2WPKH
def __bool__(self):
return len(self.wifs) > 0
def __contains__(self, pubkey):
return self._privkey_for(pubkey) is not None
def __getitem__(self, pubkey):
sk = self._privkey_for(pubkey)
if sk is None: raise KeyError
return sk
def _privkey_for(self, pubkey):
for pk, sk in self.wifs:
if pk == pubkey:
return sk
def match_address_hash(self, addr_fmt, hash20):
if not self.wifs:
return None
if not self._pkh:
self._pkh = [ngu.hash.hash160(pk) for pk, _ in self.wifs]
if addr_fmt in (AF_P2WPKH, AF_CLASSIC):
table = self._pkh
elif addr_fmt == AF_P2SH:
if not self._sh:
self._sh = [ngu.hash.hash160(b'\x00\x14' + h) for h in self._pkh]
table = self._sh
else:
return None # AF_P2WSH / AF_P2TR / AF_BARE_PK / unknown — not us
try:
idx = table.index(hash20)
return idx, self.wifs[idx][0]
except ValueError:
return None
# EOF

Some files were not shown because too many files have changed in this diff Show More