Compare commits

...

608 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
Peter D. Gray
637624dea9
Key Teleport easier to access 2025-08-11 09:33:53 -04:00
Peter D. Gray
9d5b86e39b
cleanups 2025-07-31 10:24:28 -04:00
scgbckbone
7bd952973e bugfix: use full LCD display width (34) when displaying seed words; new OUT_CTRL_NOWRAP flag for stories 2025-07-31 10:11:57 -04:00
chad
aa4524339b restore note on 520 byte stack element limit 2025-07-31 09:59:02 -04:00
chad
5dd034c051 update documentation for multisig quorum constraints 2025-07-31 09:59:02 -04:00
scgbckbone
f90973af9b Bull Bitcoin export 2025-07-31 09:58:06 -04:00
nvk
d71f24959c reorder/rename exports and add Cove 2025-06-12 12:26:56 -04:00
scgbckbone
00b05e20b8 remove linux address patch 2025-06-11 09:18:13 -04:00
scgbckbone
11da344abf multiprocess simulator 2025-06-11 08:32:22 -04:00
dependabot[bot]
89dfe1f6d4 Bump requests from 2.32.3 to 2.32.4 in /testing
Bumps [requests](https://github.com/psf/requests) from 2.32.3 to 2.32.4.
- [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.3...v2.32.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-10 09:11:02 -04:00
scgbckbone
81f2830425 fix word_wrap; adjust tests for new double wide policy 2025-06-06 09:54:53 -04:00
scgbckbone
efb445fbe3 CHANGE -> CHANGE BACK 2025-06-03 11:34:30 -04:00
scgbckbone
0a899727b5 move OP_RETURN ux rendition from chains to render_output 2025-06-03 11:34:30 -04:00
scgbckbone
6d17350293 QRs in txn output explorer 2025-06-03 11:34:30 -04:00
scgbckbone
86d5eb890b remove dup comment 2025-06-03 10:54:34 -04:00
scgbckbone
b3b384b7d6 sort manifest modules alphabetically 2025-06-03 10:54:34 -04:00
scgbckbone
6dbedfaeb5 move shared modules from version manifests to default manifest.py 2025-06-03 10:54:34 -04:00
scgbckbone
e29e0b65e3 is None, not equals None 2025-06-03 09:20:12 -04:00
scgbckbone
2b750c993b word_wrap: if last character is double wide on Q move to next line 2025-06-03 09:19:31 -04:00
scgbckbone
17a715bfc5 bugfix: PSBT corner cases 2025-06-03 09:18:42 -04:00
Peter D. Gray
15678037d5
New release: 2025-05-14T1344-v5.4.3 2025-05-14 09:44:42 -04:00
Peter D. Gray
213fdd3c57
Signed for mk4 release. 2025-05-14 09:44:38 -04:00
Peter D. Gray
406ab5adaa
New release: 2025-05-14T1343-v1.3.3Q 2025-05-14 09:43:24 -04:00
Peter D. Gray
416f2efffd
Signed for q1 release. 2025-05-14 09:43:20 -04:00
Peter D. Gray
8aba8fe655
Revert "Signed for q1 release."
This reverts commit 4a0cd36c9c.
2025-05-14 09:41:29 -04:00
Peter D. Gray
68d5a8fc35
Merge branch 'master' of github.com:Coldcard/firmware 2025-05-14 09:37:59 -04:00
Peter D. Gray
170b701e94
New release: 2025-05-14T1337-v1.3.3Q 2025-05-14 09:37:25 -04:00
Peter D. Gray
4a0cd36c9c
Signed for q1 release. 2025-05-14 09:37:21 -04:00
Peter D. Gray
6a5311e8f5
new release 2025-05-14 09:35:59 -04:00
Peter D. Gray
20a8d14ede
updates 2025-05-14 09:34:56 -04:00
scgbckbone
bab58af710 UX story fix; improve warning tests 2025-05-14 09:16:29 -04:00
scgbckbone
d4c4cc1b69 remove exception dumping to console 2025-05-13 12:07:32 -04:00
Peter D. Gray
75a9f9f3eb
placement 2025-05-13 11:49:12 -04:00
Peter D. Gray
e856343c66
More 2025-05-13 11:11:47 -04:00
scgbckbone
51bbee9eb1 bugfix: Mk4: fix extended keys not fully visible in stories 2025-05-13 11:00:43 -04:00
Peter D. Gray
f40d16b76b
tweaks 2025-05-13 10:54:03 -04:00
Peter D. Gray
227196da42
version bump 2025-05-13 09:51:08 -04:00
scgbckbone
ea9d183a48 bugfix: PUSHDATA2 in scripts cause yikes
bugfix: missing warning summary in the top of the story for unknown scripts
2025-05-12 13:03:46 -04:00
scgbckbone
32cfd53569 exceptions also stop after first loop if nothing else enabled 2025-05-12 11:15:03 -04:00
scgbckbone
49087f6f41 bugfix: master settings corrupted by using "Add current tmp" in Seed Vault 2025-05-09 10:13:52 -04:00
scgbckbone
6c0ee684dc bugfix: Mk4: export loop for devices with NFC=ViDsk=0 2025-04-28 11:33:58 -04:00
scgbckbone
bee2f95e0a bugfix: disable teleport PSBT from CC without secret; update menu tree 2025-04-23 08:28:47 -04:00
Peter D. Gray
594e64affc
markdown 2025-04-16 15:56:48 -04:00
Peter D. Gray
6f3cbf20c8
New release: 2025-04-16T1908-v5.4.2 2025-04-16 15:08:10 -04:00
Peter D. Gray
7ca83c5929
Signed for mk4 release. 2025-04-16 15:08:06 -04:00
Peter D. Gray
3bf9c13bbb
New release: 2025-04-16T1906-v1.3.2Q 2025-04-16 15:06:51 -04:00
Peter D. Gray
063ee4cf4d
Signed for q1 release. 2025-04-16 15:06:48 -04:00
Peter D. Gray
d7b9ed4813
time levels 2025-04-16 15:05:28 -04:00
Peter D. Gray
10b82e080f
edits 2025-04-16 15:01:32 -04:00
Peter D. Gray
a1b319347a
update test 2025-04-16 14:10:37 -04:00
scgbckbone
709207f28e
bugfix: only export BBQr for multisig core, coldcard, and pretty descriptor (contains newlines, cannot read from simple QR) 2025-04-16 13:05:25 -04:00
Peter D. Gray
c680b0461f
Revert "mk4: decrease CHAR_PER_W from 19 to 18 (one char missing in view)"
This reverts commit 33a2567452d944a724ec0571da1859ab6c028910.
2025-04-16 09:17:11 -04:00
scgbckbone
a4fa421a17
mk4: decrease CHAR_PER_W from 19 to 18 (one char missing in view) 2025-04-16 08:59:52 -04:00
scgbckbone
2f35c0d496
small nits 2025-04-16 08:58:20 -04:00
scgbckbone
315e650344 refuse to do ownership scan with regtest address on mainnet 2025-04-15 11:09:17 -04:00
scgbckbone
d9be8cb2f1
locktime ux fix (less spacing) 2025-04-15 10:42:50 -04:00
scgbckbone
176a7f80cb
change: Lock Down Seed needs confirm key (4) 2025-04-15 10:42:00 -04:00
Peter D. Gray
2441e6044e
edits 2025-04-15 10:38:53 -04:00
Peter D. Gray
f076695120
cover -factory.dfu as well 2025-04-15 10:34:40 -04:00
Peter D. Gray
6a3c8ae676
New bootrom version releases 2025-04-15 10:06:16 -04:00
Peter D. Gray
28926acd06
bugfix 2025-04-15 09:36:19 -04:00
Peter D. Gray
adcf2c8e22
compilier compat 2025-04-15 09:09:17 -04:00
Peter D. Gray
1ae8c51a3c
feel good msg 2025-04-14 10:41:24 -04:00
Peter D. Gray
36d4df8a49
nits 2025-04-14 10:17:46 -04:00
scgbckbone
1aed412080 teleport testing protocol 2025-04-14 10:12:44 -04:00
Peter D. Gray
bc0f3e2d12
Merge branch 'master' of github.com:Coldcard/firmware 2025-04-14 09:55:26 -04:00
Peter D. Gray
cae59ecddd
edits 2025-04-14 09:55:22 -04:00
scgbckbone
9b95d29152 fix rename in Seed Vault 2025-04-14 09:47:25 -04:00
scgbckbone
d376c4efbf bugfix: temporary seed from CC backup failed to load stored multisig wallets 2025-04-14 09:27:09 -04:00
scgbckbone
e021fc7317 finalize foreign single sig outputs from PSBT partial signatures 2025-04-14 09:17:29 -04:00
scgbckbone
ef72dc00ae fix Mk4 re-export infinite loop 2025-04-14 09:14:02 -04:00
scgbckbone
e2fc69661a NFC selftest - revert to simple share and stop 2025-04-11 07:17:16 -04:00
Peter D. Gray
b65f2948d4
better feels on version number 2025-04-10 10:01:15 -04:00
scgbckbone
fbbf0a3413 bugfix: fix "Wipe -> Wallet" trick pin option 2025-04-10 09:49:37 -04:00
scgbckbone
c4b7260686 fix it 2025-04-10 09:49:04 -04:00
Peter D. Gray
6a63c7bde9
Merge branch 'master' of github.com:Coldcard/firmware 2025-04-10 09:48:51 -04:00
Peter D. Gray
efd5a4ff45
edits 2025-04-10 09:48:44 -04:00
scgbckbone
b866e0912d improve responsiveness of Teleport UX 2025-04-09 15:06:16 -04:00
scgbckbone
9ba8aeaaad kt_send_psbt fix 2025-04-09 14:14:01 -04:00
scgbckbone
3239dc6cd5 improve re-export UX; unify USB to done_signing; bugfixes 2025-04-09 14:04:26 -04:00
Peter D. Gray
8a18e413e9
better URL 2025-04-08 09:35:02 -04:00
scgbckbone
78fcfa56a5 backup fixes 2025-04-08 09:34:24 -04:00
Peter D. Gray
634bb69873
edits 2025-04-07 16:35:55 -04:00
Peter D. Gray
00f8d7a5ca
ux feedback 2025-04-07 15:38:27 -04:00
Peter D. Gray
0a200d1f1c
ktrx is semi-ephemeral 2025-04-07 15:38:17 -04:00
Peter D. Gray
30bc6a1f57
cleanups/bugs 2025-04-07 13:48:18 -04:00
Peter D. Gray
f23d7f09bf
add full backup to key-teleport 2025-04-05 13:33:09 -04:00
Peter D. Gray
5c5f8902a1
better pw display 2025-04-05 11:17:00 -04:00
Peter D. Gray
6bba62224c
add T shortcut for other modes where Advanced/Tools is shown 2025-04-05 11:00:29 -04:00
Peter D. Gray
8837cdcdda
Merge branch 'master' of github.com:Coldcard/firmware 2025-04-04 11:51:48 -04:00
Peter D. Gray
c4b56d95d0
key teleport MS test 2025-04-04 11:51:44 -04:00
scgbckbone
2156844d18 UX showing words now offers NFC, Q1 hint icons; full wipe NFC chip after secret data shared 2025-04-04 11:07:37 -04:00
scgbckbone
b47412bbc1 fixes 2025-04-03 10:05:00 -04:00
scgbckbone
1ca170946f sign multisig export artifacts 2025-04-02 13:53:12 -04:00
scgbckbone
0dedaf353d CCC usability improvements;ability to remove all addrs from whitelist (with confirmation) 2025-04-02 13:16:17 -04:00
scgbckbone
6779345665 fix default NFC screen prompt for Q 2025-04-02 12:51:26 -04:00
scgbckbone
698f84ff97 empty list checks 2025-04-01 16:22:12 -04:00
scgbckbone
6a5f4843aa fix error message order, first check for our/other keys then validate multisigs 2025-04-01 15:27:55 -04:00
scgbckbone
e851e4382a stop scanning upon receiving unsupported data 2025-03-31 14:48:23 -04:00
Peter D. Gray
fc21241a49
fun real vs simu test 2025-03-31 11:52:56 -04:00
scgbckbone
5de2ba364d finalize multisig error messages 2025-03-31 11:50:36 -04:00
scgbckbone
cc62502ab1 test fixes 2025-03-31 09:45:10 -04:00
Peter D. Gray
0815fbcc81 little fixes 2025-03-28 15:08:57 -04:00
Peter D. Gray
a676ef8eb7 rework post-signing save process 2025-03-28 15:08:57 -04:00
Peter D. Gray
78e560376c refactor-out msgsign.py from auth.py 2025-03-28 15:08:57 -04:00
Peter D. Gray
7e65cc3e47 robustness fix 2025-03-28 15:08:57 -04:00
Peter D. Gray
a954821826 sign our leg from teleport co-signer menu 2025-03-28 15:08:57 -04:00
Peter D. Gray
828b09dba2 teleport 2-of-15 2025-03-28 15:08:57 -04:00
Peter D. Gray
2b88a14124 bugfix 2025-03-28 15:08:57 -04:00
Peter D. Gray
8acb15e4ce historical note 2025-03-28 15:08:57 -04:00
scgbckbone
97d8398e8a remove finms: always finalize multisig txns if possible 2025-03-28 15:08:57 -04:00
Peter D. Gray
5f87d21811 refactoring NFC 2025-03-28 15:08:57 -04:00
Peter D. Gray
5c8a73ddb0 catch bad numeric password 2025-03-28 15:08:57 -04:00
Peter D. Gray
979a4e65e9 nits 2025-03-28 15:08:57 -04:00
Peter D. Gray
6e5c68abe5 bugfix, cleanups 2025-03-28 15:08:57 -04:00
Peter D. Gray
d3634b3448 test bug fixes 2025-03-28 15:08:57 -04:00
scgbckbone
67fa34666d fixes 2025-03-28 15:08:57 -04:00
Peter D. Gray
39adb2ac41 Multisig PSBT support 2025-03-28 15:08:57 -04:00
Peter D. Gray
6fd2ef619e remove checksum on rx pubkey 2025-03-28 15:08:57 -04:00
Peter D. Gray
4f25f6dbf5 nits 2025-03-28 15:08:57 -04:00
Peter D. Gray
b4eeeda53a key teleport tests 2025-03-28 15:08:57 -04:00
Peter D. Gray
f8bc38b558 fixes 2025-03-28 15:08:57 -04:00
Peter D. Gray
414793053e teleport tests 2025-03-28 15:08:57 -04:00
Peter D. Gray
b885976601 added quick note 2025-03-28 15:08:57 -04:00
Peter D. Gray
ec64a9aa38 Seedvault refactor, more on KT 2025-03-28 15:08:57 -04:00
Peter D. Gray
0aa0fc4500 Rebased 2025-03-28 15:08:57 -04:00
scgbckbone
3ebde0ea34 change: "Destroy Seed" purges all Trick PINs from SE2 2025-03-27 15:04:50 -04:00
scgbckbone
b6098a94e5 bugfix: check candidate Main PIN with trick pins from SE2, not just settings 2025-03-27 15:04:50 -04:00
scgbckbone
e726637319 add test for getting C key from Seed Vault 2025-03-27 13:52:56 -04:00
scgbckbone
446bea9926 bugfix: auto-vdisk, do not consider already signed PSBTs; fix stucked at Reading... screen 2025-03-27 12:37:11 -04:00
scgbckbone
9a28d36097 signing artifacts re-export 2025-03-27 11:00:07 -04:00
scgbckbone
a4d7f884c0 remove finms: always finalize multisig txns if possible 2025-03-27 10:02:46 -04:00
scgbckbone
ce1ce080ab fix qrs force_msg 2025-03-24 09:13:25 -04:00
Peter D. Gray
e06449f59f
I should prefer testing before pushing 2025-03-14 12:02:13 -04:00
Peter D. Gray
ffda830f66
personal perference 2025-03-14 11:53:40 -04:00
scgbckbone
d23187f187 multisig tx finalization 2025-03-14 11:50:56 -04:00
scgbckbone
15766b418d CCC test fix 2025-03-14 09:07:12 -04:00
Peter D. Gray
7450940730
tweak 2025-03-11 13:31:29 -04:00
scgbckbone
d1fd24c9ef improve CCC enable UX 2025-03-11 13:29:57 -04:00
scgbckbone
19ce22e607 deltamode: wipe mcu key slot before getting actual secret 2025-03-11 13:28:18 -04:00
scgbckbone
4476089d0f bugfix: Selftest SD card test for Q 2025-03-11 11:29:48 -04:00
scgbckbone
341265c486 bugfix: hanging progress bar after Selftest on Q 2025-03-11 11:29:48 -04:00
scgbckbone
f3a2f59549 bugfix: enable to restore backup with custom bkpw 2025-03-11 10:32:08 -04:00
scgbckbone
4a245ce553 more robust export NFC export; export wallet in loop with selected account number 2025-03-11 10:15:21 -04:00
Peter D. Gray
de443d5b89
by request of factory; saves a click 2025-03-10 14:42:34 -04:00
Peter D. Gray
ff756f086e
Signed for removing edge factory release. 2025-03-10 11:54:31 -04:00
scgbckbone
cd1728b81d new parameter to "force_msg" during QR display 2025-03-10 09:55:04 -04:00
scgbckbone
633120b760 fix mk4 word menu; use new address display format for whitelist; fix mk4 compat in tests 2025-03-10 09:41:27 -04:00
scgbckbone
3fd8fb9dc1 remove dead code around CLEAR_PIN 2025-03-10 09:30:41 -04:00
scgbckbone
7a27adfcfd bugfix: implement missing if wrong pin options 2025-03-10 09:26:40 -04:00
scgbckbone
33faa04652 bugfix: UI multisig derivation for root keys in Address Explorer 2025-03-10 09:08:19 -04:00
Peter D. Gray
56f0d56a08
missed year bump 2025-02-27 11:52:54 -05:00
Peter D. Gray
481d64d2dd
handle larger firmware 2025-02-27 11:52:15 -05:00
Peter D. Gray
e3ae6bcbdf
Cleanup use of MAX_TXN_LEN_MK4 vs. MAX_TXN_LEN 2025-02-27 10:37:33 -05:00
Peter D. Gray
e150150d11
performance upgrade 2025-02-27 09:50:28 -05:00
Peter D. Gray
f117423210
better progress bars 2025-02-27 09:46:26 -05:00
scgbckbone
962bb4b0f2 enumerate default option before offering to choose non-zero acct num in PICK_ACCOUNT 2025-02-27 09:04:00 -05:00
scgbckbone
ae9806f702 clear dispaly after usb failure - do not hang on Receiving... 2025-02-27 09:03:40 -05:00
Peter D. Gray
66e4cf130f
Updated 2025-02-27 09:03:12 -05:00
Peter D. Gray
72fef6d5b8
comments 2025-02-27 09:02:30 -05:00
Peter D. Gray
bb3073af76
edits 2025-02-26 10:28:51 -05:00
Peter D. Gray
b09bb521a8
version big bump 2025-02-26 10:16:25 -05:00
Peter D. Gray
22ff7b1fe9
edits 2025-02-26 10:12:39 -05:00
Peter D. Gray
7eb5a7ea03
copy tweets 2025-02-26 09:53:12 -05:00
doc-hex
6b9e2ef9b9
Merge pull request #473 from scgbckbone/ccc
ColdCard Cosign = CCC
2025-02-26 09:48:18 -05:00
scgbckbone
7c9436d237 after rebase fixes 2025-02-26 12:05:48 +01:00
scgbckbone
8bf4731cf5 remove ccc confirm with (4) key; remove ccc tests 2025-02-26 12:05:48 +01:00
Peter D. Gray
e14fb64904 numbers for humans 2025-02-26 12:05:48 +01:00
scgbckbone
12f62f95bc unique ms names from ccc feature; multiple ms wallets test 2025-02-26 12:05:48 +01:00
scgbckbone
58eec8be8b load C key + seed vault access 2025-02-26 12:05:48 +01:00
scgbckbone
b5a6bf5d18 test signing with C key as tmp 2025-02-26 12:05:48 +01:00
scgbckbone
70acd6a602 test export C XPUBs 2025-02-26 12:05:48 +01:00
scgbckbone
1fe0c58b76 maxed tests 2025-02-26 12:05:48 +01:00
scgbckbone
18ea47f334 more velocity + warning ccc tests 2025-02-26 12:05:48 +01:00
Peter D. Gray
96b8d48136 text 2025-02-26 12:05:48 +01:00
scgbckbone
4a463da9db master rebase; fix test_decoders.py; NLOCK_IS_TIME moved to constants.py 2025-02-26 12:05:48 +01:00
Peter D. Gray
239d1ebd00 Copy changes, 25-addr limit 2025-02-26 12:05:48 +01:00
Peter D. Gray
77956158ae key rotated to final value 2025-02-26 12:05:48 +01:00
Peter D. Gray
3d4336bab7 edits 2025-02-26 12:05:48 +01:00
Peter D. Gray
b2f6e290d1 lowercase 2025-02-26 12:05:48 +01:00
scgbckbone
f920020ba0 simulator nLockTime 2025-02-26 12:05:48 +01:00
Peter D. Gray
4a234e8452 tweaks 2025-02-26 12:05:48 +01:00
scgbckbone
d980c42dad ccc velocity review 2025-02-26 12:05:48 +01:00
scgbckbone
37c7119ae4 ccc velocity 2025-02-26 12:05:48 +01:00
scgbckbone
ab55c00065 non-async func 2025-02-26 12:05:48 +01:00
scgbckbone
6f93847f8a fixes 02 2025-02-26 12:05:48 +01:00
scgbckbone
2eb615b358 reworked 2025-02-26 12:05:48 +01:00
Peter D. Gray
c899f6e7ce key C from vault, stash cleanups 2025-02-26 12:05:48 +01:00
scgbckbone
d0f7a451ef small fixes 2025-02-26 12:05:48 +01:00
Peter D. Gray
14be94e049 file import 2025-02-26 12:05:48 +01:00
Peter D. Gray
5fb1839620 more 2025-02-26 12:05:48 +01:00
scgbckbone
a2eed31416 CCC config first time entry without C key prompt 2025-02-26 12:05:48 +01:00
Peter D. Gray
9203c4f2aa more 2025-02-26 12:05:48 +01:00
Peter D. Gray
3d08b749b3 sign w/ other key 2025-02-26 12:05:48 +01:00
Peter D. Gray
58b5d1071f cleanup 2025-02-26 12:05:48 +01:00
Peter D. Gray
f795f9027f generalize number entry (mk4) 2025-02-26 12:05:48 +01:00
Peter D. Gray
2cf006beaa moved to web2fa 2025-02-26 12:05:48 +01:00
Peter D. Gray
0c28987190 more web2fa code 2025-02-26 12:05:48 +01:00
Peter D. Gray
012433aba4 19 seems to work and looks better 2025-02-26 12:05:48 +01:00
Peter D. Gray
57cdd69c81 Lots of UX and some TODOs 2025-02-26 12:05:48 +01:00
Peter D. Gray
9e082570aa test code for web backend 2025-02-26 12:05:48 +01:00
Peter D. Gray
117daaf17f todoos 2025-02-26 12:05:48 +01:00
Peter D. Gray
359d05dc7b note 2025-02-26 12:05:48 +01:00
Peter D. Gray
77059ffcf1 about ccc 2025-02-26 12:05:48 +01:00
Peter D. Gray
17306e2a38 about ccc 2025-02-26 12:05:48 +01:00
Peter D. Gray
1f1045b401 docs 2025-02-26 12:05:48 +01:00
Peter D. Gray
29b860e84e cleanups 2025-02-26 12:05:48 +01:00
Peter D. Gray
0849e538b5 2fa link encryption, tests 2025-02-26 12:05:48 +01:00
Peter D. Gray
44dae36141 faster q seed word entry 2025-02-26 12:05:48 +01:00
Peter D. Gray
8a8860d9a0 cleanups 2025-02-26 12:05:48 +01:00
Peter D. Gray
0a185669d0 more conservative 2025-02-26 12:05:48 +01:00
Peter D. Gray
6445fad042 lowercase 2025-02-26 12:05:48 +01:00
scgbckbone
0ccd701421 simulator nLockTime 2025-02-26 12:05:48 +01:00
Peter D. Gray
7ff8342e01 more 2025-02-26 12:05:48 +01:00
Peter D. Gray
0009028817 tweaks 2025-02-26 12:05:48 +01:00
scgbckbone
7e9ce496d8 ccc velocity review 2025-02-26 12:05:48 +01:00
scgbckbone
869e317db8 ccc velocity 2025-02-26 12:05:48 +01:00
Peter D. Gray
6df24f646d Bugs 2025-02-26 12:05:48 +01:00
Peter D. Gray
9d863edcdb misc 2025-02-26 12:05:48 +01:00
Peter D. Gray
4c308205a6 debug fails 2025-02-26 12:05:48 +01:00
scgbckbone
2641650090 non-async func 2025-02-26 12:05:48 +01:00
scgbckbone
84a476d586 addr -> addrs 2025-02-26 12:05:48 +01:00
scgbckbone
8b97535a40 whitelist init 2025-02-26 12:05:48 +01:00
scgbckbone
119fc35c43 fix 2025-02-26 12:05:48 +01:00
scgbckbone
2feade4f74 proper storage serialization + whitelist tests for Q 2025-02-26 12:05:48 +01:00
scgbckbone
9761c65614 fixes 02 2025-02-26 12:05:48 +01:00
Peter D. Gray
00d8c841f7 auth checks 2025-02-26 12:05:48 +01:00
scgbckbone
531ae613c7 reworked 2025-02-26 12:05:48 +01:00
scgbckbone
ac782fdd59 signing psbt POC 2025-02-26 12:05:48 +01:00
Peter D. Gray
d35ccc1ade key C from vault, stash cleanups 2025-02-26 12:05:48 +01:00
scgbckbone
2c52cca4bd test fix 2025-02-26 12:05:48 +01:00
scgbckbone
07e66811f3 yikes on Q 2025-02-26 12:05:48 +01:00
Peter D. Gray
077d502139 copy 2025-02-26 12:05:48 +01:00
scgbckbone
4030f6f59f small fixes 2025-02-26 12:05:48 +01:00
scgbckbone
15990fa0f6 use secp256k1 for 2FA tests 2025-02-26 12:05:48 +01:00
Peter D. Gray
ef01a57626 file import 2025-02-26 12:05:48 +01:00
Peter D. Gray
d21515e2cf whitelist import by qr 2025-02-26 12:05:48 +01:00
Peter D. Gray
fc5e99c226 more 2025-02-26 12:05:48 +01:00
scgbckbone
bc59709966 only push CCCConfig if first time 2025-02-26 12:05:48 +01:00
scgbckbone
d983549fae CCC config first time entry without C key prompt 2025-02-26 12:05:48 +01:00
scgbckbone
b6fc24e705 CCC setup test fixtures 2025-02-26 12:05:47 +01:00
Peter D. Gray
fcae022ba8 block exit 2025-02-26 12:05:47 +01:00
Peter D. Gray
65d202160e oops 2025-02-26 12:05:47 +01:00
Peter D. Gray
4fb37d7850 multisig 2025-02-26 12:05:47 +01:00
Peter D. Gray
5b69ec07fc more 2025-02-26 12:05:47 +01:00
Peter D. Gray
60d254314b sign w/ other key 2025-02-26 12:05:47 +01:00
Peter D. Gray
0f3fe830b9 Cleanup 2025-02-26 12:05:47 +01:00
Peter D. Gray
4087f8f25b cleanup 2025-02-26 12:05:47 +01:00
Peter D. Gray
6b770024ce rename url_encode to quote 2025-02-26 12:05:47 +01:00
Peter D. Gray
bbd8f9b282 generalize number entry (mk4) 2025-02-26 12:05:47 +01:00
Peter D. Gray
d81d6ea80c moved to web2fa 2025-02-26 12:05:47 +01:00
Peter D. Gray
8edef87ae5 refactor for reuse 2025-02-26 12:05:47 +01:00
Peter D. Gray
9cc6ce368d more web2fa code 2025-02-26 12:05:47 +01:00
Peter D. Gray
e037037924 19 seems to work and looks better 2025-02-26 12:05:47 +01:00
Peter D. Gray
5b38dd87a6 Lots of UX and some TODOs 2025-02-26 12:05:47 +01:00
Peter D. Gray
b55db05aab test code for web backend 2025-02-26 12:05:47 +01:00
doc-hex
6d4b3d6990
Merge pull request #471 from scgbckbone/unreadable_qrs
always choose the biggest possible display size for QR on LCD
2025-02-24 08:39:05 -05:00
doc-hex
c7d216fff3
Merge pull request #470 from scgbckbone/bitcoin_safe_export
add Bitcoin Safe to Export menu
2025-02-24 08:37:52 -05:00
scgbckbone
a3f815a74d always choose the biggest possible display size for QR on LCD 2025-02-24 13:53:50 +01:00
scgbckbone
e61493668b add Bitcoin Safe to Export menu 2025-02-24 13:39:41 +01:00
Peter D. Gray
eb325e5fec
Signed for Edge release. 2025-02-19 15:41:02 -05:00
Peter D. Gray
242641d396
version bump 2025-02-13 09:35:13 -05:00
Peter D. Gray
a507677dc1
New release: 2025-02-13T1415-v5.4.1 2025-02-13 09:15:16 -05:00
Peter D. Gray
262c4ea717
Signed for mk4 release. 2025-02-13 09:15:13 -05:00
Peter D. Gray
a16516b8e1
New release: 2025-02-13T1413-v1.3.1Q 2025-02-13 09:14:01 -05:00
Peter D. Gray
695c3b4b29
Signed for q1 release. 2025-02-13 09:13:57 -05:00
Peter D. Gray
08bb9783d2
update for new release 2025-02-13 09:09:43 -05:00
Peter D. Gray
4515e688ed
fixing tests 2025-02-13 09:04:21 -05:00
Peter D. Gray
5694ada611
Ownership success UX story title compat Mk4/Q 2025-02-13 09:02:04 -05:00
Peter D. Gray
d2636c7621
edits 2025-02-12 13:23:28 -05:00
scgbckbone
67febe2758 BBQr switch in QR File Share 2025-02-12 13:10:10 -05:00
scgbckbone
7e6be45e2d fix: multisg address display 2025-02-12 12:19:53 -05:00
scgbckbone
660b4617bd fix: busy bar after failed calc_qr 2025-02-12 12:08:56 -05:00
scgbckbone
afe85a3772 fix: id hint while displaying qr segwit address overlap QR 2025-02-12 12:08:30 -05:00
scgbckbone
6c91bd7328 allow multisig descriptor with root keys 2025-02-12 11:12:28 -05:00
Peter D. Gray
9c7f4c5451
Merge branch 'master' of github.com:Coldcard/firmware 2025-02-11 10:03:07 -05:00
Peter D. Gray
4ea455aa20
add sort-notes feature 2025-02-11 10:02:55 -05:00
scgbckbone
9e38974f7a revert: mpy submodule commit 2025-02-11 09:07:13 -05:00
scgbckbone
75751b8d2b testing: fix multisg tests after UI text improvements 2025-02-11 08:42:18 -05:00
Peter D. Gray
dd66cd8811
verify addr in QR better, fix some related issues 2025-02-10 11:48:59 -05:00
Peter D. Gray
a640dd5d78
bugfix 2025-02-10 10:44:00 -05:00
Peter D. Gray
14e85304df
asperation 2025-02-10 10:33:15 -05:00
Peter D. Gray
1eec58ece7
merge and reword 2025-02-10 10:28:15 -05:00
Peter D. Gray
41cde6be6c
text tweaks 2025-02-10 09:55:50 -05:00
Peter D. Gray
15536e4c9e
fix double space 2025-02-10 09:43:39 -05:00
scgbckbone
2feb991d96 msg sign: address format from standard derivation paths if address format not specified 2025-02-07 11:50:20 -05:00
scgbckbone
a2bdfc9a58 msg sign: Sparrow QR compat 2025-02-07 11:50:20 -05:00
Peter D. Gray
d3c50521e8
linebreak 2025-02-07 11:41:13 -05:00
Peter D. Gray
2558ed7ac0
text tweaks 2025-02-07 11:30:39 -05:00
Peter D. Gray
4f8f1fe593
edits 2025-02-07 11:26:51 -05:00
scgbckbone
3878897369 testing: fix test_msas_enable_disable 2025-02-06 08:06:09 -05:00
scgbckbone
1a18258b5a new address format for UX display 2025-02-05 11:28:09 -05:00
scgbckbone
38c92ef0c1 show ms address qr is msas=1 2025-02-04 12:48:38 -05:00
scgbckbone
cc7097b4f7 stabilize temporary seed tests 2025-02-02 11:46:06 -05:00
scgbckbone
9b597592bc option to show/export full multisig addresses; do not return to home menu after setting unsort_ms 2025-01-31 12:20:48 -05:00
scgbckbone
70d303af78 add ability to use master bkpw for tmp seeds; add bkpw override 2025-01-28 12:08:01 -05:00
Peter D. Gray
cd6d74d9f3
note 2025-01-23 09:14:00 -05:00
Dmitry Monakhov
a71f350c78 deltamode & xor_seed
Die rather than give up our secrets

- Do not allow split master via SeedXOR
- Do not allow to use master and seedvault in SeedXOR restore.
2025-01-23 09:11:56 -05:00
scgbckbone
ce1026cb4b fix test_iss6743 after removal of SIGHASH_ALL from psbt input 2025-01-23 08:08:05 -05:00
scgbckbone
e039fb8603 update mpy submodule 2025-01-22 08:41:35 -05:00
scgbckbone
a0949ecb87 upgrade msg signing 2025-01-21 13:05:21 -05:00
scgbckbone
a8202972b3 default non-root derivation paths for sd/nfc msg signing 2025-01-21 13:05:21 -05:00
Peter D. Gray
85ff2dcf45
robustness 2025-01-17 12:04:37 -05:00
Peter D. Gray
262df4a257
bump 2025-01-17 12:04:29 -05:00
Peter D. Gray
e9d17e5efb
Merge branch 'master' of github.com:Coldcard/firmware 2025-01-17 09:05:12 -05:00
Peter D. Gray
513a3b8258
NFC hardware detect bug 2025-01-17 09:04:35 -05:00
scgbckbone
ac761c23d5 add message about successful master seed recovery when trying to use master as tmp 2025-01-17 08:30:14 -05:00
scgbckbone
de0a679eef add ability to switch between slip132 and bip32 representations of extended public keys in Export XPUB 2025-01-15 13:05:11 -05:00
scgbckbone
92a776cfc3 testnet4 2025-01-15 13:04:17 -05:00
Peter D. Gray
f2a3667593
fix for blank/3dots screen crash 2025-01-15 09:47:56 -05:00
scgbckbone
6b73eb2fa6 save bytes by removing some duplicate from glob import settings 2025-01-15 08:38:42 -05:00
scgbckbone
66b01c1fd5 special menu keys 1..9 change from cursor scroll-only to skip-to-item 2025-01-14 09:56:07 -05:00
scgbckbone
14ce2ca6e0 remove unnecessary validate_func arg from ux_input_numbers (Mk4) 2025-01-14 09:35:20 -05:00
scgbckbone
86fe33137f psbt: sighash not included in input data if SIGHASH_ALL 2025-01-14 08:18:54 -05:00
scgbckbone
c65280cd42 cope with buggy core 28.0 behavior wrt labels in importdescriptors 2025-01-07 08:52:07 -05:00
scgbckbone
2fb66da58d wider mk4 QR check 2025-01-07 08:49:08 -05:00
Tyler Nieman
87b5142145 add missing word to seed xor docs 2025-01-01 10:29:17 -05:00
scgbckbone
6aedb0a73a testing: speed up backup tests by removing artifacts after test 2024-12-19 08:43:45 -05:00
Peter D. Gray
072eb24ed9
Signed for q1 release. 2024-12-18 11:05:37 -05:00
spicyzboss
b443f38d60 docs: add space between words 2024-12-12 08:39:37 -05:00
scgbckbone
8957ad3c10 prevent ownership yikes 2024-11-25 09:43:52 -05:00
scgbckbone
d270cf66c6 provide generalized nfc reader function (saving bytes) 2024-11-25 09:43:32 -05:00
scgbckbone
c425fc6bcc improve Wipe LFS UX message 2024-11-25 09:42:17 -05:00
scgbckbone
c9882d7a8a save bytes drv_entro.py 2024-10-25 09:28:39 -04:00
scgbckbone
85b478346b Mk4: export descriptor as simple QR 2024-10-18 08:41:01 -04:00
scgbckbone
95b13083dc do not allow to delete current active tmp seed from seed vault and purge its settings 2024-10-17 12:44:45 -04:00
scgbckbone
5568082f35 deltamode & Seed Vault 2024-10-16 13:47:23 -04:00
scgbckbone
8f86ed1c0e deltamode & secure notes and passwords 2024-10-16 13:30:37 -04:00
scgbckbone
1b54536eff bugfix: bless firmware causes hanging progress bar 2024-10-16 13:29:58 -04:00
Henrique Albuquerque
d1d104cb7e Fix grammar error 2024-09-30 09:10:20 -04:00
scgbckbone
9e1ce7a956 do NOT allow to enable/disable Seed Vault while in temporary seed mode 2024-09-27 11:56:04 -04:00
scgbckbone
f30686f252 update menu-tree.txt 2024-09-24 08:54:56 -04:00
Peter D. Gray
3dbc9caa73
Bugfix: blank screen on boot 2024-09-23 14:39:00 -04:00
229 changed files with 132732 additions and 6190 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)
![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
@ -28,9 +28,11 @@ has been automated using Docker. Steps are as follows:
```shell
git clone https://github.com/Coldcard/firmware.git
git checkout 2023-12-21T1526-v5.2.2
# get a copy of that binary into ./releases/2023-12-21T1526-v5.2.2-mk4-coldcard.dfu
cd firmware/stm32
cd firmware
# DOWNLOAD https://coldcard.com/downloads
# 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
```
@ -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
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`
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
**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.
Do a checkout, recursively, to get all the submodules:
@ -183,7 +186,9 @@ git clone --recursive https://github.com/Coldcard/firmware.git
cd firmware
# Apply address patch
git apply unix/linux_addr.patch
# if unix/linux_addr.patch exists use below command
# not needed in current revision
# git apply unix/linux_addr.patch
# * below is needed for ubuntu 24.04
pushd external/micropython
@ -230,8 +235,8 @@ Top-level dirs:
- shared code between desktop test version and real-deal
- 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
to earlier hardware is in `manifest_mk3.py`
- code exclusive to the Mk4 or Mk5 will be listed in `manifest_mk4.py`, and
to the Q will be listed in `manifest_q1.py`
`unix`
@ -263,7 +268,7 @@ Top-level dirs:
`stm32/mk4-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.
`hardware`

View File

@ -208,8 +208,9 @@ def readback(fname):
if v & MK_2_OK: d.append('Mk2')
if v & MK_3_OK: d.append('Mk3')
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_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?')
v = nv + '+'.join(d)
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('--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('--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',
help='Make downgrade attack test version', default=0)
@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()
body = open(build_dir + '/firmware1.bin', 'rb').read()
if hw_compat in { 'mk4', '4'}:
hw_compat = MK_4_OK
if hw_compat in { 'mk4', '4', 'mk5', '5', 'mk' }:
# 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':
hw_compat = MK_Q1_OK
elif hw_compat in { 'mk3', '3'}:
@ -319,13 +321,14 @@ def doit(keydir, outfn=None, build_dir=None, high_water=False,
pubkey_num=pubkey_num,
timestamp=timestamp(backdate) )
assert FW_MIN_LENGTH <= hdr.firmware_length <= FW_MAX_LENGTH, hdr.firmware_length
if hw_compat & MK_3_OK:
# actual file length limited by size of SPI flash area reserved to txn data/uploads
assert FW_MIN_LENGTH <= hdr.firmware_length <= FW_MAX_LENGTH, hdr.firmware_length
USB_MAX_LEN = (786432-128)
else:
# new value for Mk4: limited only by final binary size, not SPI flash
# new value for Mk4 and later: limited only by final binary size, not SPI flash
assert FW_MIN_LENGTH <= hdr.firmware_length <= FW_MAX_LENGTH_MK4, hdr.firmware_length
USB_MAX_LEN = 1472 * 1024
assert hdr.firmware_length <= USB_MAX_LEN, \

View File

@ -3,14 +3,32 @@
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.
- [`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.
- [`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.
- [`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.
- [`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).
- [`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.
- [`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.

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,
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
keystrokes needed to type the password.
#### 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)
## 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
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
4. A few different options are available at this point:
1. press 1 to save password backup file on MicroSD card (cleartext!)
2. press 2 to send keystrokes (this will first of all enable keyboard emulation, then send keystrokes + enter, and finally disables keyboard emulation)
3. press 3 to view password as QR code
4. press 4 to send over NFC (only appears when NFC is enabled)
4. A few different options are available at this point (on Mk; on Q the NFC and
QR buttons are used instead of (3)/(4)):
1. press (1) to save password backup file on MicroSD card (cleartext!)
2. press (2) to save to Virtual Disk (only when available)
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

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.
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
more in this file.
@ -18,32 +21,51 @@ Here is an example, produced by the Simulator for account number 123.
```javascript
{
"chain": "XTN",
"chain": "BTC",
"xfp": "0F056943",
"xpub": "tpubD6NzVbkrYhZ4XzL5Dhayo67Gorv1YMS7j8pRUvVMd5odC2LBPLAygka9p7748JtSq82FNGPppFEz5xxZUdasBRCqJqXvUHq6xpnsMcYJzeh",
"account": 123,
"xpub": "xpub661MyMwAqRbcGC9DmWbtbAmuUjpMYxw4BWE88NSDHB3jSjfUK7KtYJuKa52GbowD3DVLkgsxH9QwPnTx5mjdHykYFEncnmAsNsCTbWzBhA7",
"bip44": {
"deriv": "m/44'/1'/123'",
"first": "n44vs1Rv7T8SANrg2PFGQhzVkhr5Q6jMMD",
"name": "p2pkh",
"xfp": "B7908B26",
"xpub": "tpubDCiHGUNYdRRGoSH22j8YnruUKgguCK1CC2NFQUf9PApeZh8ewAJJWGMUrhggDNK73iCTanWXv1RN5FYemUH8UrVUBjqDb8WF2VoKmDh9UTo"
"xfp": "5F898064",
"deriv": "m/44h/0h/123h",
"xpub": "xpub6DStQXfAgHuLbMpCf86ruVkF4yT9pSLyWsFiqQTWY9osuinq8Dyee4W5jCjMfyku5LNkRB9oFinrY5ufn9XXEn8Vvzc2jnifKMaQCNV7RBZ",
"desc": "pkh([0f056943/44h/0h/123h]xpub6DStQXfAgHuLbMpCf86ruVkF4yT9pSLyWsFiqQTWY9osuinq8Dyee4W5jCjMfyku5LNkRB9oFinrY5ufn9XXEn8Vvzc2jnifKMaQCNV7RBZ/<0;1>/*)#4tl8jryn",
"first": "1GTNtzG5xX2UhdD5e3Nu7i1WPxFdjxQMJt"
},
"bip49": {
"_pub": "upub5DMRSsh6mNak9KbcVjJ7xAgHJvbE3Nx22CBTier5C35kv8j7g2q58ywxskBe6JCcAE2VH86CE2aL4MifJyKbRw8Gj9ay7SWvUBkp2DJ7y52",
"deriv": "m/49'/1'/123'",
"first": "2N87V39riUUCd4vmXfDjMWAu9gUCiBji5jB",
"name": "p2wpkh-p2sh",
"xfp": "CEE1D809",
"xpub": "tpubDCDqt7XXvhAdy1MpSze5nMJA9x8DrdRaKALRRPasfxyHpiqWWEAr9cbDBQ9BcX7cB3up98Pk97U2QQ3xrvQsi5dNPmRYYhdcsKY9wwEY87T"
"name": "p2sh-p2wpkh",
"xfp": "A748B1FC",
"deriv": "m/49h/0h/123h",
"xpub": "xpub6DDm8WzH5a9qjKkttzqSB3uGofNohU9D3n3UG8WMxkUZzJEMPTYiQRf1dvTFCQR82MjGW4LUMVuTtnW4hF17RpzCqVwhf6Z2fnJPWtjG164",
"desc": "sh(wpkh([0f056943/49h/0h/123h]xpub6DDm8WzH5a9qjKkttzqSB3uGofNohU9D3n3UG8WMxkUZzJEMPTYiQRf1dvTFCQR82MjGW4LUMVuTtnW4hF17RpzCqVwhf6Z2fnJPWtjG164/<0;1>/*))#5j7t2n2u",
"_pub": "ypub6Y42SBfCEFhKacx1jMd4P8zmydXFe68hxtZh3XQFLkrT3Q3ae7iH2VK9f8QqCK53Rzr5FXw2pAG1n57dQwR8E4fohqe8F1NWwWN2uVRfBry",
"first": "3CeBRbJKCpg7BpJME2vM8ZxhCjBnhG4toy"
},
"bip84": {
"_pub": "vpub5Y5a91QvDT45EnXQaKeuvJupVvX8f9BiywDcadSTtaeJ1VgJPPXMitnYsqd9k7GnEqh44FKJ5McJfu6KrihFXhAmvSWgm7BAVVK8Gupu4fL",
"deriv": "m/84'/1'/123'",
"first": "tb1qc58ys2dphtphg6yuugdf3d0kufmk0tye044g3l",
"name": "p2wpkh",
"xfp": "78CF94E5",
"xpub": "tpubDC7jGaaSE66VDB6VhEDFYQSCAyugXmfnMnrMVyHNzW9wryyTxvha7TmfAHd7GRXrr2TaAn2HXn9T8ep4gyNX1bzGiieqcTUNcu2poyntrET"
"xfp": "2C5207AA",
"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
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
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
(`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,
to spend a UTXO on `tb1qc58ys2dphtphg6yuugdf3d0kufmk0tye044g3l`, the input section
of your PSBT would need to specify `(m=0F056943)/84'/1'/123'/0/0`.
to spend a UTXO on `bc1qhj6avwmp5lhpgqwm6dgxrf3v5lf67rjm99a8an`, the input section
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
a specific address format.

224
docs/key-teleport.md Normal file
View File

@ -0,0 +1,224 @@
# Key Teleport
Purpose: Send a small quantity of very secret data between two COLDCARD Q systems, with
no risk of anything in the middle learning the secret.
Method: ECDH and AES-256-CTR plus an extra wrapping layer, transmitted over a mixture of
NFC, passive websites, and QR/BBQr codes.
# Protocol Overview
## Steps
- Receiver picks an EC keypair, stores it in settings, and publishes the pubkey via a QR/NFC
- The pubkey is encrypted by a short 8-digit numeric code, which should be
sent by a different channel.
- Sender gets QR and numeric code, picks own keypair, and does ECDH to arrive at a
shared session key
- Sender picks a human-readable secret which is independent of anything else (P key)
- The secret data (perhaps a seed phrase, XPRV, secure note, full backup, etc) is
AES-256-CTR encrypted with P key, then encrypted + MAC added with session key
- Data packet is sent to receiver (via BBQr), who can reconstruct the session key via ECDH
- Prompt user for the P key to finish decoding
- Decoded secret value is saved to Seed Vault or secure notes as appropriate
- Receiver destroys EC keypair used in transfer
### When used for PSBT Multisig
- No action required on receiver
- Sender uses the pubkey derived from pre-shared XPUB involved in the multisig wallet.
- Same steps, but drops immediately into signing process when decoded correctly
## Notes and Limitations
- max 4k (after encoding) of data is possible due to HTTP limitations
- all transfers are "data typed" and decode only on COLDCARD
- Q model is required due to the use of QR codes to ultimately get data into the COLDCARD
# Details
## Data Type Codes
The first byte encodes what the package contents (under all the encryption).
- `s` - 12/18/24 words/raw master/xprv - 17-72 bytes follow, encoded in an internal format
- `x` - XPRV mode, full details - 4 bytes (XPRV) + base58 *decoded* binary-XPRV follows
- `n` - one or many notes export (JSON array)
- `v` - seed vault export (JSON: one secret key but includes name, source of key)
- `p` - binary PSBT to be signed, perhaps multisig but not required.
- `b` - complete system backup file (text lines, internal format)
## QR details
BBQr is always used for the QR's involved in this process, even if
they are short enough for a normal QR code. Because the BBQr is
being generated by the COLDCARD embedded firmware, it will not be
compressed and will always be Base32 encoded.
New type codes for BBQr are defined for the purposes of this application:
- `R` contains `(pubkey)` ... begins the process from receiver; compressed pubkey is 33 bytes
- `S` contains `(pubkey)(data)` ... data from sender; first 33 bytes are sender's pubkey
- `E` for Multisig PSBT: `(randint)(data)` ... randint (4 byte nonce) indicates which
derived subkey from pre-shared xpub associated with receiver
All the data is encrypted with the exception randint. Keep in mind
this is a nonce value picked uniquely for each transfer. The
receiver's pubkey is only weakly encrypted by the 8-digit numeric
password, but is also a nonce effectively.
### PSBT Key Selection
When sending PSBT data, a nonce is picked at random by the sender
in range: `0..(2^28)`
This nonce is called `randint`. The receiver's pubkey will be
.../20250317/(randint)
where `...` is the derivation used in the multisig wallet for the co-signer who will
receive the package. The sender's keypair has the same sub key path assuming all
co-signers have same derivation path from root (not required).
Because both the sender and receiver already have each other's XPUB they can derive
the appropriate pubkeys (and privkey for their side) without communicating
more than `randint`. The sending COLDCARD will pick a new random value each time.
When receiving a multisig PSBT encrypted this way, the receiver does not need
to do any setup (nor numeric password) and can receive a QR code at any time.
This works because the shared multisig wallet is already setup. Receiver will
take the nonce value (randint) and seach all pre-defined multisig wallets for
any pubkey that can decrypt the package successfully (based on checksum inside
first layer of ECDH encryption).
The next layer of encryption (paranoid password) is unchanged.
## Encryption Details
AES-256-CTR is used exclusively. Session key is picked via ECDH with final
key value being the SHA256 over 64 bytes of coordinate X (concat) Y.
While ECDH is enough to assure privacy from men in the middle, we
add an additional layer of encryption. We call this the "paranoid key" internally
and in the UX it is called "Teleport Password".
The user sees a random 8-character password, generated as a random 40-bit value, but
shown in Base32 (8 chars) for the human to enter. We apply PBKDF2-SHA512 with
an iteration count of 5000 to stretch that to 512 bits, of which we use half.
The session key is used as the key for the KDF, and the entered value as salt.
- ECDH arrives at session key
- decrypt (AES-256-CTR) the binary body of message
- verify checksum:
- final 2 bytes should be `== SHA256(decrypted body[0:-2])[-2:]`
- if not, corruption, truncation, or wrong keys
- if that decryption is correct, then prompt user for the paranoid key (8 chars)
- stretch that value using session key and 5000 iterations of PBKDF2-SHA512
- use upper 256 bits and run AES-256-CTR again
- same checksum of 2 bytes of SHA256 are found inside after decryption
Encryption adds 4 bytes of overhead because of these MAC values,
but should catch truncation and bitrot. There are no other
protections against truncation as length data is not transmitted.
# Receiver Password
When the teleport process is started, the receiver shares his pubkey
as QR. However, we also show an 8-digit numeric password. The
purpose of this is force the receiver to share this separately from
the pubkey QR on another channel. The code is randomly picked, but
only represents about 26 bits of entropy and is stretched with
a single round of SHA256 before being used as a AES-256-CTR key
to decrypt the pubkey. No checksum verifies correct
decryption, so any code is accepted, and will with near-50% odds,
decrypt to a valid pubkey.
When the sender is given the receiver's pubkey via QR code, it
prompts for the numeric code and uses it to decrypt the pubkey.
Thus a MiTM who injects their pubkey will be detected and blocked.
The "paranoid key" serves the same role in the other direction but
it is Base32 character set, so it will not look similar or be
confusing as to its purpose.
# Web Component
In order to "teleport" the contents of a QR code over NFC, we will
publish a static website directly from an open Github repository.
The single-page website contains javascript code which looks at the
"hash" part of the incoming URL (`window.location.hash`) and if it
meets the requirements, renders a large QR. The QR data must look like
a correctly-encoded BBQr with one of the 3 type-codes above (`R` `S` or `E`).
Otherwise the website could render any QR, which we don't want to
support.
The page will offer "copy to clipboard" features for the data inside
the QR as a URL (ie. same URL as shown) and as an image and of course,
the COLDCARD Q can scan from the web browser screen itself.
When the BBQr data is larger than comfortable for a single QR, the
website can split into a multi-frame BBQr. The website can
do this without understanding the contents of the BBQr data (all
of which is encrypted). Download options will be provided for
single-frame QR, animated PNG, and "stacked BBQr" (a single tall
PNG with each QR frame stacked).
On the COLDCARD side, when NFC is tapped, it will offer a long URL
to this site with the data to be transferred "after the hash". This
is optional since the QR can be shown on the Q itself, and would
pass the same data.
Since the website is running on Github, Coinkite does not have
access to IP addresses or other access log details. Because the data for
teleport is "after the hash" it is never sent to Github's servers
but remains in the browser only. All JS resources referenced by the
webpage will have content hashes applied to prevent interference,
and the site will be served over SSL.
Visit [keyteleport.com](https://keyteleport.com/), or an
[example small QR](https://keyteleport.com/#B$2R0100VHT2AGUUH7KUZUUSTOWOIWHJX3XM7GA2N4BHQOXDFHXLVHVA7K6ZO)
and [view source code](https://github.com/coinkite/keyteleport.com).
# UX Details
- When the receive process is started by the user, a pubkey is picked
and stored, so that they can come back later (after a power cycle)
and make use of the data encoded by the sender. However once a package
is decoded successfully, that key is deleted.
- Sender must start by scanning the QR from a receiver. Then can pick what
to send, from secure notes to seeds and so on.
- For PSBT multisig, user must pick a single co-signer (who hasn't already
signed) and the QR is prepared for that receiver. Because we
cannot do arbitary combining, it's best if the next signer continues
to teleport the updated PSBT to further signers. In other words,
a daisy-chain pattern is prefered to a star pattern. The signer
who completes the Mth (of N) signature will be able to finalize
the transaction, and ideally with PushTx feature, broadcast it.
# Security Comments
## Such short passwords?
We are using 8-character passwords because we want them to be
practical to share over non-digital channels such as a voice phone
call, or hand-written note.
It is very important to remind users that the passwords should be sent
by a different channel from the QR itself. Best is to call up your
other party and say the letters to them directly.
## Is it safe to save image of QR to cloud?
Yes, this seems safe. Of course, if you can control it, perhaps not
a risk to accept... but the QR is encrypted via ECDH using a key
that is forgotten after the transfer, so forward privacy is protected.
Also your cloud service (or photo roll, chat app log, etc) will not
have the 8-character password which is also required unpack the secrets.
The QR codes themselves are fully random and do not reveal the
identity of your COLDCARD, your on chain funds or anything linked
to you.

View File

@ -14,11 +14,12 @@
# PIN Codes
- 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
- 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
# Micro SD
@ -55,14 +56,18 @@
- only one signature will be added per input. However, if needed the partly-signed
PSBT can be given again, and the "next" leg will be signed.
- we do not support PSBT combining or finalizing of transactions involving
P2SH signatures (so the combine step must be off-device)
- finalizing of multisig transactions involving P2SH signatures:
* SD/Vdisk signing exports both signed PSBT and finalized txn ready for broadcast (if txn is complete)
* QR/NFC outputs finalized txn ready for broadcast if txn is complete otherwise signed PSBT only
* USB signing requires `--finalize` parameter (as for standard single signature wallets)
- we can sign for P2SH and P2WSH addresses that represent multisig (M of N) but
we cannot sign for non-standard scripts because we don't know how to present
that to the user for approval.
- during USB "show address" for multisig, we limit subkey paths to
16 levels deep (including master fingerprint)
- max of 15 co-signers due to 520 byte script limitation in consensus layer with classic P2SH (same limit applies to segwit even though consensus allows up to 20 co-signers)
- max of 15 co-signers due to 1650 byte `scriptSig` limitation in policy with classic P2SH (same limit applies to segwit even though consensus allows up to 20 co-signers).
note: the consensus layer sets an upper bound of 520 bytes for the length of each stack element
- (mk3) we have space for up to 8 M-of-3 wallets, or a single M-of-15 wallet. YMMV
- only a single multisig wallet can be involved in a PSBT; can't sign inputs from two different
multisig wallets at the same time.
@ -74,6 +79,7 @@
- multisig wallet `name` can only contain printable ASCII characters `range(32, 127)`
### BIP-67
- 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
- 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)
@ -134,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`)
# 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
@ -198,3 +208,16 @@ 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
with same descriptors, but different seeds) you will get false negatives
# Spending Policy
- (Cosign mode) only 12 or 24 word seeds (not XPRV) are accepted for "key C"
- velocity limit:
- based on a max magnitude per txn, and a required minimum block height
gap, based on previous `nLockTime` value in last-signed PSBT.
- if you sign a transaction, but never broadcast it, you will still have to wait out
the velocity policy.
- PSBT creator must put in `nLockTime` block heights (most already do to avoid fee sniping)
- 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
- 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
|---------------|-----------|--------------------------
| 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 e000 | 8k | MCU keys, consumable; 256 32-bit write-once slots.
| 0x0802 0000 | 16k | Interrupt handlers, file header (Micropython and Coldcard code)

View File

@ -16,7 +16,6 @@
Advanced
12 Word Dice Roll
24 Word Dice Roll
Migrate COLDCARD
Import Existing
12 Words
[SEED WORD ENTRY]
@ -30,6 +29,8 @@
Import XPRV
Tapsigner Backup
Seed XOR
Migrate Coldcard
Key Teleport (start)
Help
Advanced/Tools
View Identity
@ -48,6 +49,7 @@
Import XPRV
Tapsigner Backup
Coldcard Backup
Restore Seed XOR
Upgrade Firmware [IF NOT TMP SEED]
Show Version
From MicroSD
@ -57,14 +59,18 @@
List Files
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]
Key Teleport (start)
Paper Wallets
Perform Selftest
I Am Developer.
Serial REPL
Warm Reset
Restore Txt Bkup
Restore Bkup
Reflash GPU [IF QWERTY KEYBOARD]
Secure Logout
Settings
Login Settings
@ -106,6 +112,11 @@
NFC Sharing
Default Off
Enable NFC
NFC Push Tx
coldcard.com
mempool.space
Custom URL...
Disable
Display Units
BTC
mBTC
@ -140,32 +151,32 @@
50%
60%
70%
80% (default)
80%
90%
95% (default)
100%
Delete PSBTs
Default Keep
Delete PSBTs
Menu Wrapping
Default Off
Enable
Buried Settings
Home Menu XFP [IF SECRET AND NOT TMP SEED]
Only Tmp
Always Show
Menu Wrapping
Default
Always Wrap
[QR key shortcut] [IF QR SCANNER]
---
[NORMAL OPERATION]
Ready To Sign
Passphrase [IF WORD BASED SEED]
Restore Saved [MAYBE]
A***********
[0C52BAD4]
Restore Saved
c*******
[3A14F788]
Restore
Delete
Edit Phrase [MAYBE]
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]
Edit Phrase
Scan Any QR Code [IF QR SCANNER]
Start HSM Mode [IF HSM POLICY]
Address Explorer
@ -183,35 +194,44 @@
Account Number
Custom Path
CC-2-of-4
Secure Notes & Passwords [IF ENBALED]
1: note1
"note1"
Secure Notes & Passwords [IF ENBALED] [MAYBE]
1: note0
"note0"
View Note
Edit
Delete
Export
SHORTCUT
SHORTCUT
2: nostr
"nostr"
↳ scg
↳ brb.io
Sign Note Text
2: secret-PWD
"secret-PWD"
↳ satoshi
↳ abc.org
View Password
Send Password [MAYBE]
Export
Edit Metadata
Delete
Change Password
SHORTCUT
SHORTCUT
Sign Note Text
New Note
New Password
Export All
Sort By Title
Import
Type Passwords [MAYBE]
Seed Vault [MAYBE]
1: [B14E9AE0]
[B14E9AE0]
1: [7126EB3C]
[7126EB3C]
Use This Seed
Rename
Delete
2: [CCEE13B9]
[CCEE13B9]
Use This Seed
Rename
Delete
3: [03EE9989]
[03EE9989]
Use This Seed
Rename
Delete
@ -222,12 +242,19 @@
Restore Backup
Clone Coldcard
Export Wallet
Sparrow
Cove
Bitcoin Core
Sparrow Wallet
Nunchuk
Bull Bitcoin
Blue Wallet
Electrum Wallet
Wasabi Wallet
Fully Noded
Unchained
Lily Wallet
Theya
Bitcoin Safe
Zeus
Samourai Postmix
Samourai Premix
Descriptor
@ -235,9 +262,10 @@
Export XPUB
Segwit (BIP-84)
Classic (BIP-44)
P2WPKH/P2SH (49)
P2WPKH/P2SH (BIP-49)
Master XPUB
Current XFP
Key Expression
Dump Summary
Upgrade Firmware [IF NOT TMP SEED]
Show Version
@ -247,12 +275,19 @@
Verify Backup
Backup System
Export Wallet
Sparrow
Cove
Bitcoin Core
Sparrow Wallet
Nunchuk
Bull Bitcoin
Blue Wallet
Electrum Wallet
Wasabi Wallet
Fully Noded
Unchained
Lily Wallet
Theya
Bitcoin Safe
Zeus
Samourai Postmix
Samourai Premix
Descriptor
@ -260,42 +295,45 @@
Export XPUB
Segwit (BIP-84)
Classic (BIP-44)
P2WPKH/P2SH (49)
P2WPKH/P2SH (BIP-49)
Master XPUB
Current XFP
Key Expression
Dump Summary
Sign Text File
Batch Sign PSBT
Teleport Multisig PSBT
List Files
Verify Sig File
NFC File Share [IF NFC ENABLED]
BBQr File Share [IF QR SCANNER]
QR File Share [IF QR SCANNER]
Clone Coldcard
Format SD Card
Format RAM Disk [IF VIRTDISK ENABLED]
Secure Notes & Passwords [IF QWERTY KEYBOARD]
1: note1
"note1"
1: note0
"note0"
View Note
Edit
Delete
Export
SHORTCUT
SHORTCUT
2: nostr
"nostr"
↳ scg
↳ brb.io
Sign Note Text
2: secret-PWD
"secret-PWD"
↳ satoshi
↳ abc.org
View Password
Send Password [MAYBE]
Export
Edit Metadata
Delete
Change Password
SHORTCUT
SHORTCUT
Sign Note Text
New Note
New Password
Export All
Sort By Title
Import
Derive Seeds (BIP-85)
View Identity
@ -314,18 +352,26 @@
Import XPRV
Tapsigner Backup
Coldcard Backup
Restore Seed XOR
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
Enable HSM [IF HSM AND SECRET]
Default Off
Enable
User Management [IF HSM AND SECRET]
WIF Store
NFC Tools [IF NFC ENABLED]
Sign PSBT
Show Address
Sign Message
Verify Sig File
Verify Address
File Share
Import Multisig
Push Transaction [IF PUSHTX ENABLED]
Danger Zone
Debug Functions
Seed Functions
@ -334,13 +380,15 @@
Split Existing [IF WORD BASED SEED]
Restore Seed XOR
Destroy Seed [IF SECRET AND NOT TMP SEED]
Lock Down Seed
Lock Down Seed [MAYBE]
Export SeedQR [IF WORD BASED SEED]
I Am Developer.
Serial REPL
Warm Reset
Restore Txt Bkup
Seed Vault [IF SECRET]
Restore Bkup
BKPW Override
Reflash GPU [IF QWERTY KEYBOARD]
Seed Vault [IF SECRET AND NOT TMP SEED]
Default Off
Enable
Perform Selftest
@ -353,30 +401,44 @@
Warn
Testnet Mode
Bitcoin
Testnet3
Testnet4
Regtest
AE Start IDX
AE Start Index
Default Off
Enable
B85 Idx Values
Default Off
Unlimited
Settings Space
MCU Key Slots
Bless Firmware
Reflash GPU [IF QWERTY KEYBOARD]
Wipe LFS
Nuke Device
Settings
Login Settings
Change Main PIN
Trick PINs [IF SECRET AND NOT TMP SEED]
Trick PINs:
↳123-254
PIN 123-254
↳11-11
PIN 11-11
↳Bricks CC
Hide Trick
Delete Trick
Change PIN
↳333-3334
PIN 333-3334
↳Duress Wallet
Activate Wallet
Hide Trick
Delete Trick
Change PIN
↳WRONG PIN
After 3 wrong:
↳Wipes seed
↳Reboots
Hide Trick
Delete Trick
Add New Trick
Add If Wrong
Delete All
Set Nickname
Scramble Keys
@ -421,17 +483,25 @@
View Details
Delete
Coldcard Export
Electrum Wallet
Descriptors
View Descriptor
Export
Bitcoin Core
Electrum Wallet
Import from File
Import via NFC [IF NFC ENABLED]
Import
Export XPUB
Create Airgapped
Trust PSBT?
Skip Checks?
Full Address View?
Partly Censor
Show Full
Unsorted Multisig?
NFC Push Tx
coldcard.com
mempool.space
Custom URL...
Disable
Display Units
BTC
mBTC
@ -466,34 +536,189 @@
50%
60%
70%
80% (default)
80%
90%
95% (default)
100%
Delete PSBTs
Default Keep
Delete PSBTs
Menu Wrapping
Default Off
Enable
Keyboard EMU
Default Off
Enable
Buried Settings
Home Menu XFP [IF SECRET AND NOT TMP SEED]
Only Tmp
Always Show
Menu Wrapping
Default
Always Wrap
Secure Logout
SHORTCUT [IF NFC ENABLED]
[NFC key shortcut] [IF NFC ENABLED]
Sign PSBT
Show Address
Sign Message
Verify Sig File
Verify Address
File Share
Import Multisig
Push Transaction [IF PUSHTX ENABLED]
---
[FACTORY MODE]
Version: 5.x.x
Bag Me Now
Version: 5.x.x
DFU Upgrade
Ship W/O Bag
Debug Functions
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,
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.
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
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
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.
@ -21,7 +22,7 @@ Bitcoin core can only verify P2PKH.
## 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`.
### Message construction and signature file format
@ -39,29 +40,23 @@ IFOvGVJrm31S0j+F4dVfQ5kbRKWKcmhmXIn/Lw8iIgaCG5QNZswjrN4X673R7jTZo1kvLmiD4hlIrbuL
-----END BITCOIN SIGNATURE-----
```
### What is signed
### What Is Signed
1. **Single sig address explorer exports**. Signed by key corresponding to first (0th) address on the exported list.
2. **Specific single sig exports**. Signed by key corresponding to external address at index zero of chosen application specific derivation `m/<app_deriv>/0/0`
1. **Single sig address explorer exports:** Signed by the key corresponding to the first (0th) address on the exported list.
2. **Specific single sig exports:** Signed by the key corresponding to the external address at index zero of chosen application specific derivation `m/<app_deriv>h/<coin_type>'h/<account>h/0/0`.
* Bitcoin Core
* Electrum Wallet
* Wasabi Wallet
* Samourai Postmix
* Samourai Premix
* Descriptor
3. **Generic single sig exports**. Signed by key that corresponds to address at derivation `m/44'/<coin_type>'/0'/0/0`
Lily Wallet
Generic JSON
Dump Summary
4. **BIP85 derived entropy exports**. Signed by path that corresponds to specific BIP85 application.
5. **Paper wallet exports**. Signed by key and address exported as paper wallet itself.
### 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**
3. **Generic single sig exports:** Signed by key that corresponds to first (0th) external address at derivation `m/44h/<coin_type>h/<account>h/0/0`.
* Lily Wallet
* Generic JSON
* Dump Summary
4. **BIP85 derived entropy exports:** Signed by path that corresponds to specific BIP85 application.
5. **Paper wallet exports:** Signed by key and address exported as paper wallet itself.
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`
* 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>`

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
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
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
@ -36,7 +46,7 @@ in general. Good interoperability is critical with radio standards.
## 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
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
the Coldcard is powered-down, regardless of the NFC setting.
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.
If the above is not enough for you, the antenna can be destroyed:
- **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.
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`
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.
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.
```makefile
repro: submods-match code-committed
repro:
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:
```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!
...
@ -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."
- `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.

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
| `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
| `SE2 easy key` | page 15 | 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 easy key` | page 14 | AES via HMAC | SE2 | Another SE2 part of AES seed key
| `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
| `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)

View File

@ -1,4 +1,4 @@
# COLDCARD Mk4/Q Security Model
# COLDCARD Mk4/Mk5/Q Security Model
## 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.
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 and uses a 24-word seed, it behaves exactly like a normal
wallet. Defining a passphrase for the wallet is also possible.
based on account numbers 1001, 1002, or 1003 for a 24-word duress wallet
(or 2001, 2002, 2003 for a 12-word one). Because this is BIP-85
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
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
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
COLDCARD was powered down during the upgrade process. At that point,
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
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
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
@ -22,7 +22,7 @@ This one more solution for your game-theory arsenal.
- *Q*: I'm lazy, can I do this to my Existing Seed?
- *A*: Yes. You can split the words you have already in your Coldcard, making
2, 3 or 4 new SEEDPLATES. You could also any number of existing SEEDPLATES
2, 3 or 4 new SEEDPLATES. You could also use any number of existing SEEDPLATES
you have, and combine them to make a new random wallet that is the XOR of
their values. Effectively that makes a new random wallet.
@ -78,10 +78,12 @@ words right the next day.
When the parts are made deterministically, we take a double-SHA256 over
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
them) from the Coldcard's True Random Number Generator (TRNG)..
In random mode, we simply pick random bytes (and then double-SHA256
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
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
- 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

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
[_(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
@ -42,7 +42,7 @@ Read more about `Seed Vault` feature below.
- `24 words`
- `XPRV (BIP-32)`
- 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
- 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
[_(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
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
[_(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
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
_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
(serial flash) chip entirely (used in Mk1-Mk3). In it's place we

97
docs/web2fa.md Normal file
View File

@ -0,0 +1,97 @@
# Web 2FA Authentication
How to support [RFC 6238](https://www.rfc-editor.org/rfc/rfc6238)
TOTP (Time based One Time Password) 2FA check, on our little embedded
device without a real-time clock?
Solution: Store the pre-shared secret in the COLDCARD, and send that
securely to a trusted webserver which knows the time and can do a
fancy UX. That webserver accepts the time-based-one-time 2FA numeric
code from the user, and if correct, reveals a secret
that can be used back on the COLDCARD to authorize an action.
For the Mk4, the secret is 8 digit numeric code to be entered,
for the COLDCARD Q, it is a QR code to be scanned.
### History / Background
The HSM feature uses HOTP tokens, which do not require a backend,
but are not as robust as time-based tokens.
Web2FA is available to be enabled as part of a Spending Policy,
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
- Web backend has a ECC keypair, with pubkey known to CC firmware releases.
- 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:
- shared secret for TOTP (same value as held in user's phone)
- the response nonce (32 bytes, shown as 64 hex chars, on Q; or 8 digits on Mk4)
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)
- some text label for what's being approved, which is presented to user so they can pick
correct 2fa shared secret.
- above is all encrypted in transit, and only the server can decrypt
- user is sent to that encrypted URL using NFC tap on the COLDCARD
- user arrives at server:
- shown label [which also indicates the server can be trusted, since only it could decrypt it]
- prompt for 6 digits from authenticator app
- does [RFC 6238](https://www.rfc-editor.org/rfc/rfc6238) 2FA check using current time
- checks using current time and the shared secret provided by CC, fails if wrong.
- time based failure: offer retry (they typed too slow / minor clock drift)
- can offer to retry, but also do some rate limiting (only one attempt per 30-sec period)
- server will store very recent responses so attacker cannot get two codes
in any 30sec period (ie. blocks immediate reuse of same URL)
- until a valid code is given, user is stuck here
- when valid token received:
- if Q, show a QR code to be scanned, with the full nonce
- for non-Q system, a 8-digit decimal value is given: user has to enter that into the COLDCARD
- web site shows instructions about what to do next on product.
## From COLDCARD PoV
- makes complex encrypted URL, which contains a nonce it wants, waits for that nonce back (or QR)
- it's either the nonce from the URL, or fail
- if the right nonce, then we know the server knows the decryption key, and we
are trusting it actually verify the 2FA token properly.
## Encryption - Simple ECDH
- CC picks a secp256k1 keypair, generates compressed pubkey
- multiplies that private key by server's known public key
- apply sha256(resulting coordinate) => the session key
- apply AES-256-CTR over URL contents (ascii text)
- prepend 33 bytes of pubkey, and then base64url encode all of it
- full url is: `https://coldcard.com/2fa?{base64 encoded binary}`
## Trust Issues
- 2FA enrol happens on the CC, which picks the shared secret and shows QR for mobile
app setup. Same TRNG process as picking a seed.
- Server knows the shared secret, but only during operation, and we won't store it [sorry,
gotta trust us on that, but no help to us to store it].
- Only we can run the server, because the private key is company-secret.
- MiTM and network snoopers get nothing because HTTPS is used and only your browser
can see the nonce, and only after you've given the right digits.
- Coinkite server could skip the 2FA checks and just give you the answer
you want to type into the COLDCARD. Again, you have to trust us on that.
## URL Format
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
- `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
after the question mark is encrypted.

@ -1 +1 @@
Subproject commit 0e686dbda686f76c4d3e8069558b2a31f9d1c2b1
Subproject commit 3d1dfa858beb58b8dac37d8c66d7aed2909812f2

2
external/libngu vendored

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

@ -1 +1 @@
Subproject commit 97d35f058f504a354fc6df79a8b3db5c91862501
Subproject commit 4107246f8a080807b62c3b4838e71e812ea68b6f

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')
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@')
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')

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
xx
xx
xx
xx xx
xx xx
xx xx
xxx
x
X
XX
X
XX
X X
XX XX
XX X
XXXX
XX

View File

@ -1,4 +1,3 @@
# Coldcard Hardware Details
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
researchers who wish to analyse the Coldcard more completely.
# Schematic
![](schematic-q1d.png)
@ -15,6 +13,12 @@ researchers who wish to analyse the Coldcard more completely.
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`
@ -30,27 +34,20 @@ This is the Mark3 rev B schematic.
# 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.
- 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:
- the plastic case (custom)
- the secure bag (with barcode serial number)
- 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
- 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

@ -74,4 +74,9 @@ special_chars = dict(small=[
x x x x x
'''),
# thin space
('\u2009', dict(y=0, w=5), '''\
'''),
])

View File

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

View File

@ -2,55 +2,40 @@
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
- New Feature: Opt-in support for unsorted multisig, which ignores BIP-67 policy. Use
descriptor with `multi(...)`. Disabled by default, Enable in
`Settings > Multisig Wallets > Legacy Multisig`. Recommended for existing multisig
wallets, not new ones.
- New Feature: Named multisig descriptor imports. Wrap descriptor in json:
`{"name:"ms0", "desc":"<descriptor>"}` to provide a name for the menu in `name`.
instead of the filename. Most useful for USB and NFC imports which have no filename,
(name is created from descriptor checksum in those cases).
- New Feature: XOR from Seed Vault (select other parts of the XOR from seeds in the vault).
- Enhancement: upgrade to latest
[libsecp256k1: 0.5.0](https://github.com/bitcoin-core/secp256k1/releases/tag/v0.5.0)
- Enhancement: Signature grinding optimizations. Now about 30% faster signing!
- Enhancement: Improve side-channel protection: libsecp256k1 context randomization now happens
before each signing session.
- Enhancement: Allow JSON files in `NFC File Share`.
- Change: Do not require descriptor checksum when importing multisig wallets.
- Bugfix: Do not allow import of multisig wallet when same keys are shuffled.
- Bugfix: Do not read whole PSBT into memory when writing finalized transaction (performance).
- Bugfix: Prevent user from restoring Seed XOR when number of parts is smaller than 2.
- Bugfix: Fix display alignment of Seed Vault menu.
- Bugfix: Properly handle null data in `OP_RETURN`.
- Bugfix: Do not allow lateral scroll in Address Explorer when showing single address
from custom path.
- Change: Remove Lamp Test from Debug Options (covered by selftest).
- New Feature: Sign [BIP-322](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki) Proof of Reserve PSBT files.
- Requires a carefully crafted PSBT that does not represent a monetary transaction, but instead is demonstrating
control over the keys for a list of UTXO, and commits to a short text message.
- Read more [here](https://github.com/Coldcard/firmware/blob/master/docs/proof-of-reserves-bip-322.md).
- New Feature: WIF Store. Ability to import foreign WIF keys (Wallet Import Format) and use them for PSBT signing.
- 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.
# Mk4 Specific Changes
# Mk Specific Changes
## 5.4.0 - 2024-09-12
## 5.5.0 - 2065-03-05
- Shared enhancements and fixes listed above.
- Bugfix: Correct intermittent card inserted/not inserted detection error.
- This release supports both the newer Mk5 hardware and existing Mk4.
- Enhancement: Show QR of XOR-split seeds.
# Q Specific Changes
## 1.3.0Q - 2024-09-12
## 1.4.0Q - 2065-03-05
- New Feature: Seed XOR can be imported by scanning SeedQR parts.
- New Feature: Input backup password from QR scan.
- New Feature: (BB)QR file share of arbitrary files.
- New Feature: `Create Airgapped` now works with BBQRs.
- Change: Default brightness (on battery) adjusted from 80% to 95%.
- Bugfix: Properly clear LCD screen after BBQR is shown.
- Bugfix: Writing to empty slot B caused broken card reader.
- Bugfix: During Seed XOR import, display correct letter B if own seed already added to the mix.
- Bugfix: Stop re-wording UX stories using a regular expression.
- Bugfix: Fixed "easy exit" from quiz after split Seed XOR.
- Bugfix: Empty notes in hobbled mode caused yikes upon menu entry.

View File

@ -1,6 +1,165 @@
*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
- Huge new feature: CCC - ColdCard Cosign
- COLDCARD holds a key in a 2-of-3 multisig, in addition to the normal signing key it has.
- it applies a spending policy like an HSM:
- velocity and magnitude limits
- whitelisted destination addresses
- 2FA authentication using phone app ([RFC 6238](https://www.rfc-editor.org/rfc/rfc6238))
- but will sign its part of a transaction automatically if those condition are met,
giving you 2 keys of the multisig and control over the funds
- spending policy can be exceeded with help of the other co-signer (3rd key), when needed
- cannot view or change the CCC spending policy once set, policy violations are not explained
- existing multisig wallets can be used by importing the spending-policy-controlled key
- New Feature: Multisig transactions are finalized. Allows use of [PushTX](https://pushtx.org/)
with multisig wallets. Read more [here](https://github.com/Coldcard/firmware/blob/master/docs/limitations.md#p2sh--multisig)
- New Feature: Signing artifacts re-export to various media. Now you have the option of
exporting the signing products (transaction/PSBT) to different media than the original source.
Incoming PSBT over QR can be signed and saved to SD card if desired.
- New Feature: Multisig export files are signed now. Read more [here](https://github.com/Coldcard/firmware/blob/master/docs/msg-signing.md#signed-exports)
- Enhancement: NFC export usability upgrade: NFC keeps exporting until CANCEL/X is pressed
- Enhancement: Add `Bitcoin Safe` option to `Export Wallet`
- Enhancement: 10% performance improvement in USB upload speed for large files
- Bugfix: Do not allow change Main PIN to same value already used as Trick PIN, even if
Trick PIN is hidden.
- Bugfix: Fix stuck progress bar under `Receiving...` after a USB communications failure
- Bugfix: Showing derivation path in Address Explorer for root key (m) showed double slash (//)
- Bugfix: Can restore developer backup with custom password other than 12 words format
- Bugfix: Virtual Disk auto mode ignores already signed PSBTs (with "-signed" in file name)
- Bugfix: Virtual Disk auto mode stuck on "Reading..." screen sometimes
- Bugfix: Finalization of foreign inputs from partial signatures. Thanks Christian Uebber
- Bugfix: Temporary seed from COLDCARD backup failed to load stored multisig wallets
- Change: `Destroy Seed` also removes all Trick PINs from SE2.
- Change: `Lock Down Seed` requires pressing confirm key (4) to execute
## 5.4.1 - 2025-02-13
- New signing features:
- Sign message from note text, or password note
- JSON message signing. Use JSON object to pass data to sign in form
`{"msg":"<required msg>","subpath":"<optional sp>","addr_fmt": "<optional af>"}`
- Sign message with key resulting from positive ownership check. Press (0) and
enter or scan message text to be signed.
- Sign message with key selected from Address Explorer Custom Path menu. Press (2) and
enter or scan message text to be signed.
- Enhancement: New address display format improves address verification on screen (groups of 4).
- Deltamode enhancements:
- Hide Secure Notes & Passwords in Deltamode. Wipe seed if notes menu accessed.
- Hide Seed Vault in Deltamode. Wipe seed if Seed Vault menu accessed.
- Catch more DeltaMode cases in XOR submenus. Thanks [@dmonakhov](https://github.com/dmonakhov)
- Enhancement: Add ability to switch between BIP-32 xpub, and obsolete SLIP-132 format
in `Export XPUB`
- Enhancement: Use the fact that master seed cannot be used as ephemeral seed, to show message
about successful master seed verification.
- Enhancement: Allow devs to override backup password.
- Enhancement: Add option to show/export full multisg addresses without censorship. Enable
in `Settings > Multisig Wallets > Full Address View`.
- Enhancement: If derivation path is omitted during message signing, derivation path
default is no longer root (m), instead it is based on requested address format
(`m/44h/0h/0h/0/0` for p2pkh, and `m/84h/0h/0h/0/0` for p2wpkh). Conversely,
if address format is not provided but subpath derivation starts with:
`m/84h/...` or `m/49h/...`, then p2wpkh or p2sh-p2wpkh respectively, is used.
- Bugfix: Sometimes see a struck screen after _Verifying..._ in boot up sequence.
On Q, result is blank screen, on Mk4, result is three-dots screen.
- Bugfix: Do not allow to enable/disable Seed Vault feature when in temporary seed mode.
- Bugfix: Bless Firmware causes hanging progress bar.
- Bugfix: Prevent yikes in ownership search.
- Bugfix: Factory-disabled NFC was not recognized correctly.
- Bugfix: Be more robust about flash filesystem holding the settings.
- Bugfix: Do not include sighash in PSBT input data, if sighash value is `SIGHASH_ALL`.
- Bugfix: Allow import of multisig descriptor with root (m) keys in it.
Thanks [@turkycat](https://github.com/turkycat)
- Change: Do not purge settings of current active tmp seed when deleting it from Seed Vault.
- Change: Rename Testnet3 -> Testnet4 (all parameters unchanged).
- Mk4 Specific Change:
- Enhancement: Export single sig descriptor with simple QR.
## 5.4.0 - 2024-09-12
- New Feature: Opt-in support for unsorted multisig, which ignores BIP-67 policy. Use
descriptor with `multi(...)`. Disabled by default, Enable in
`Settings > Multisig Wallets > Legacy Multisig`. Recommended for existing multisig
wallets, not new ones.
- New Feature: Named multisig descriptor imports. Wrap descriptor in json:
`{"name:"ms0", "desc":"<descriptor>"}` to provide a name for the menu in `name`.
instead of the filename. Most useful for USB and NFC imports which have no filename,
(name is created from descriptor checksum in those cases).
- New Feature: XOR from Seed Vault (select other parts of the XOR from seeds in the vault).
- Enhancement: upgrade to latest
[libsecp256k1: 0.5.0](https://github.com/bitcoin-core/secp256k1/releases/tag/v0.5.0)
- Enhancement: Signature grinding optimizations. Now about 30% faster signing!
- Enhancement: Improve side-channel protection: libsecp256k1 context randomization now happens
before each signing session.
- Enhancement: Allow JSON files in `NFC File Share`.
- Change: Do not require descriptor checksum when importing multisig wallets.
- Bugfix: Do not allow import of multisig wallet when same keys are shuffled.
- Bugfix: Do not read whole PSBT into memory when writing finalized transaction (performance).
- Bugfix: Prevent user from restoring Seed XOR when number of parts is smaller than 2.
- Bugfix: Fix display alignment of Seed Vault menu.
- Bugfix: Properly handle null data in `OP_RETURN`.
- Bugfix: Do not allow lateral scroll in Address Explorer when showing single address
from custom path.
- Change: Remove Lamp Test from Debug Options (covered by selftest).
- Shared enhancements and fixes listed above.
- Bugfix: Correct intermittent card inserted/not inserted detection error.
## 5.3.3 - 2024-07-05
- New Feature: PushTX: once enabled with a service provider's URL, you can tap the COLDCARD

View File

@ -1,6 +1,197 @@
*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
- Feature: Key Teleport -- Easily and securely move seed phrases, secure notes/passwords,
multisig PSBT files, and even full Coldcard backups, between two Q using QR codes
and/or NFC with helper website. See protocol spec in
[docs/key-teleport.md](https://github.com/Coldcard/firmware/blob/master/docs/key-teleport.md)
- can send master seed (words, xprv), anything held in seed vault, secure notes/passwords
(singular, or all) and PSBT involved in a multisig to the other co-signers
- full COLDCARD backup is possible as well, but receiver must be "unseeded" Q for best result
- ECDH to create session key for AES-256-CTR, with another layer of AES-256-CTR using a
short password (stretched by PBKDF2-SHA512) inside
- receiver shows sender a (simple) QR and a numeric code; sender replies with larger BBQr
and 8-char password
- Enhancement: Always choose the biggest possible display size for QR
- Bugfix: Only BBQr is allowed to export Coldcard, Core, and pretty descriptor
- Huge new feature: CCC - ColdCard Cosign
- COLDCARD holds a key in a 2-of-3 multisig, in addition to the normal signing key it has.
- it applies a spending policy like an HSM:
- velocity and magnitude limits
- whitelisted destination addresses
- 2FA authentication using phone app ([RFC 6238](https://www.rfc-editor.org/rfc/rfc6238))
- but will sign its part of a transaction automatically if those condition are met,
giving you 2 keys of the multisig and control over the funds
- spending policy can be exceeded with help of the other co-signer (3rd key), when needed
- cannot view or change the CCC spending policy once set, policy violations are not explained
- existing multisig wallets can be used by importing the spending-policy-controlled key
- New Feature: Multisig transactions are finalized. Allows use of [PushTX](https://pushtx.org/)
with multisig wallets. Read more [here](https://github.com/Coldcard/firmware/blob/master/docs/limitations.md#p2sh--multisig)
- New Feature: Signing artifacts re-export to various media. Now you have the option of
exporting the signing products (transaction/PSBT) to different media than the original source.
Incoming PSBT over QR can be signed and saved to SD card if desired.
- New Feature: Multisig export files are signed now. Read more [here](https://github.com/Coldcard/firmware/blob/master/docs/msg-signing.md#signed-exports)
- Enhancement: NFC export usability upgrade: NFC keeps exporting until CANCEL/X is pressed
- Enhancement: Add `Bitcoin Safe` option to `Export Wallet`
- Enhancement: 10% performance improvement in USB upload speed for large files
- Bugfix: Do not allow change Main PIN to same value already used as Trick PIN, even if
Trick PIN is hidden.
- Bugfix: Fix stuck progress bar under `Receiving...` after a USB communications failure
- Bugfix: Showing derivation path in Address Explorer for root key (m) showed double slash (//)
- Bugfix: Can restore developer backup with custom password other than 12 words format
- Bugfix: Virtual Disk auto mode ignores already signed PSBTs (with "-signed" in file name)
- Bugfix: Virtual Disk auto mode stuck on "Reading..." screen sometimes
- Bugfix: Finalization of foreign inputs from partial signatures. Thanks Christian Uebber
- Bugfix: Temporary seed from COLDCARD backup failed to load stored multisig wallets
- Change: `Destroy Seed` also removes all Trick PINs from SE2.
- Change: `Lock Down Seed` requires pressing confirm key (4) to execute
## 1.3.1Q - 2025-02-13
- New signing features:
- Sign message from note text, or password note
- JSON message signing. Use JSON object to pass data to sign in form
`{"msg":"<required msg>","subpath":"<optional sp>","addr_fmt": "<optional af>"}`
- Sign message with key resulting from positive ownership check. Press (0) and
enter or scan message text to be signed.
- Sign message with key selected from Address Explorer Custom Path menu. Press (2) and
enter or scan message text to be signed.
- Enhancement: New address display format improves address verification on screen (groups of 4).
- Deltamode enhancements:
- Hide Secure Notes & Passwords in Deltamode. Wipe seed if notes menu accessed.
- Hide Seed Vault in Deltamode. Wipe seed if Seed Vault menu accessed.
- Catch more DeltaMode cases in XOR submenus. Thanks [@dmonakhov](https://github.com/dmonakhov)
- Enhancement: Add ability to switch between BIP-32 xpub, and obsolete SLIP-132 format
in `Export XPUB`
- Enhancement: Use the fact that master seed cannot be used as ephemeral seed, to show message
about successful master seed verification.
- Enhancement: Allow devs to override backup password.
- Enhancement: Add option to show/export full multisg addresses without censorship. Enable
in `Settings > Multisig Wallets > Full Address View`.
- Enhancement: If derivation path is omitted during message signing, derivation path
default is no longer root (m), instead it is based on requested address format
(`m/44h/0h/0h/0/0` for p2pkh, and `m/84h/0h/0h/0/0` for p2wpkh). Conversely,
if address format is not provided but subpath derivation starts with:
`m/84h/...` or `m/49h/...`, then p2wpkh or p2sh-p2wpkh respectively, is used.
- Bugfix: Sometimes see a struck screen after _Verifying..._ in boot up sequence.
On Q, result is blank screen, on Mk4, result is three-dots screen.
- Bugfix: Do not allow to enable/disable Seed Vault feature when in temporary seed mode.
- Bugfix: Bless Firmware causes hanging progress bar.
- Bugfix: Prevent yikes in ownership search.
- Bugfix: Factory-disabled NFC was not recognized correctly.
- Bugfix: Be more robust about flash filesystem holding the settings.
- Bugfix: Do not include sighash in PSBT input data, if sighash value is `SIGHASH_ALL`.
- Bugfix: Allow import of multisig descriptor with root (m) keys in it.
Thanks [@turkycat](https://github.com/turkycat)
- Change: Do not purge settings of current active tmp seed when deleting it from Seed Vault.
- Change: Rename Testnet3 -> Testnet4 (all parameters unchanged).
- New Feature: Verify Signed RFC messages via BBQr
- New Feature: Sign message from QR scan (format has to be JSON)
- Enhancement: Sign/Verify Address in Sparrow via QR
- Enhancement: Sign scanned Simple Text by pressing (0). Next screen query information
about which key to use.
- Enhancement: Add option to "Sort By Title" in Secure Notes and Passwords. Thanks to
[@MTRitchey](https://x.com/MTRitchey) for suggestion.
- Bugfix: Properly re-draw status bar after Restore Master on COLDCARD without master seed.
## 1.3.0Q - 2024-09-12
- New Feature: Opt-in support for unsorted multisig, which ignores BIP-67 policy. Use
descriptor with `multi(...)`. Disabled by default, Enable in
`Settings > Multisig Wallets > Legacy Multisig`. Recommended for existing multisig
wallets, not new ones.
- New Feature: Named multisig descriptor imports. Wrap descriptor in json:
`{"name:"ms0", "desc":"<descriptor>"}` to provide a name for the menu in `name`.
instead of the filename. Most useful for USB and NFC imports which have no filename,
(name is created from descriptor checksum in those cases).
- New Feature: XOR from Seed Vault (select other parts of the XOR from seeds in the vault).
- Enhancement: upgrade to latest
[libsecp256k1: 0.5.0](https://github.com/bitcoin-core/secp256k1/releases/tag/v0.5.0)
- Enhancement: Signature grinding optimizations. Now about 30% faster signing!
- Enhancement: Improve side-channel protection: libsecp256k1 context randomization now happens
before each signing session.
- Enhancement: Allow JSON files in `NFC File Share`.
- Change: Do not require descriptor checksum when importing multisig wallets.
- Bugfix: Do not allow import of multisig wallet when same keys are shuffled.
- Bugfix: Do not read whole PSBT into memory when writing finalized transaction (performance).
- Bugfix: Prevent user from restoring Seed XOR when number of parts is smaller than 2.
- Bugfix: Fix display alignment of Seed Vault menu.
- Bugfix: Properly handle null data in `OP_RETURN`.
- Bugfix: Do not allow lateral scroll in Address Explorer when showing single address
from custom path.
- Change: Remove Lamp Test from Debug Options (covered by selftest).
- New Feature: Seed XOR can be imported by scanning SeedQR parts.
- New Feature: Input backup password from QR scan.
- New Feature: (BB)QR file share of arbitrary files.
- New Feature: `Create Airgapped` now works with BBQRs.
- Change: Default brightness (on battery) adjusted from 80% to 95%.
- Bugfix: Properly clear LCD screen after BBQR is shown.
- Bugfix: Writing to empty slot B caused broken card reader.
- Bugfix: During Seed XOR import, display correct letter B if own seed already added to the mix.
- Bugfix: Stop re-wording UX stories using a regular expression.
- Bugfix: Fixed "easy exit" from quiz after split Seed XOR.
## 1.2.3Q - 2024-07-05
- New Feature: PushTX: once enabled with a service provider's URL, you can tap the COLDCARD

View File

@ -2,21 +2,73 @@
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
# Mk4 Specific Changes
- Change: BIP-322 Proof of Reserves & message signing PSBT requires PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE
(read more [BIP-322 Proof of Reserves documentation](../docs/proof-of-reserves-bip-322.md) )
- Enhancement: WIF Store export watch-only descriptor
- Enhancement: WIF Store address detection without the need for PSBT_IN_BIP32_DERIVATION (Electrum support)
- Enhancement: Improve USB length validation
- 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
# Mk Specific Changes
## 5.5.x - 2065-04-xx
- tbd
## 5.4.? - 2024-??-??
- tbd
# Q Specific Changes
## 1.3.?Q - 2024-??-??
## 1.4.xQ - 2065-04-xx
- Bugfix: Properly re-draw status bar after Restore Master on COLDCARD without master seed.
- 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,45 @@
Hash: SHA256
95eff9e044cdb6b3d00961ae72d450684d5441c6a3661ab550a3c3aa0882e754 README.md
97107b5be1c8b65efa4bd36b7d1798e4ed15917861bd2d40784d66302a61d335 Next-ChangeLog.md
f6d8a1edf0993cdecea7cdc34f48ce344f249ec0fc2d28fbc4da9ebc163c6148 History-Q.md
3e98b0f292b30460e128c3d41e9dd33428524516ce433fe4a3b99132025ca64c History-Mk4.md
412597a0e30684400cb61ee04650c13ef9fc3dc16fc2570bd5e33a1dc0085d7a Next-ChangeLog.md
72458ab9eb2872d263bf4d3f4ca0fbf0ff9c6186f08d27f13fd600cb511ed2a7 History-Q.md
d4891b509915800650a881556cca37604caab7a268afc0b1ed31021cea125891 History-Mk4.md
c8ad43b4e3f9d77777026da6d1210c6fc5cfe435bcfcd241c0f67c9392ad7b82 History-Mk3.md
7c06aa1d5168e02d928da087f13c74b94e40f52e5eb281af21edcfdf6cabe5ce 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
876932d4ea7634d268145d5bf45577c7198c9d60e8a271b5079faba4d4c91acd 2025-05-14T1344-v5.4.3-mk4-coldcard-factory.dfu
aaed0b90be5de310c8ac9f2d0cb3a7eea58923a53d349eb4b9ac8a902e5cba4e 2025-05-14T1343-v1.3.3Q-q1-coldcard.dfu
9daa2b48abdfa2303a43ee1d0ba3d0d905e7f6286018f44a0dda3c755c46039e 2025-05-14T1343-v1.3.3Q-q1-coldcard-factory.dfu
c1202ba30db68a12b882176997f08844da4ec31087ac2f507ea1d4281d2faa9a 2025-04-16T1907-v5.4.2-mk4-coldcard.dfu
a82fff91f8da35c122b09d54b01b3d3f3b8825fbc7cddee8e686fd8d57a69285 2025-04-16T1907-v5.4.2-mk4-coldcard-factory.dfu
4393c67f8dcbb8890950678f5856d2cca81042b9a447ce149fe624ddfe336a2a 2025-04-16T1906-v1.3.2Q-q1-coldcard.dfu
6f2d77f99d61cad9328cc617754aadb3b828150386def41c946d90db8cfb2277 2025-04-16T1906-v1.3.2Q-q1-coldcard-factory.dfu
495f37ce7ddaba2e9fc3f03dec582f1646f258a3d0cec5e71c04d127357b2fa3 2025-02-19T1941-v6.3.5X-mk4-coldcard.dfu
605ebb5acde19447e5c1d7c8cfd0302c89de5c5870d85f06b185ecab3437f94e 2025-02-19T1939-v6.3.5QX-q1-coldcard.dfu
eb750a4f095eacc6133b2c8b38fe0738a22b2496a6cdf423ca865acde8c9bc4e 2025-02-13T1415-v5.4.1-mk4-coldcard.dfu
4236453fea241fe044a462a560d8b42df43e560683110306a2714a2ef561eac5 2025-02-13T1415-v5.4.1-mk4-coldcard-factory.dfu
2e1aad0a7a3ceb84db34322b54855a0c5496699e46e53606bfa443fcc992adec 2025-02-13T1413-v1.3.1Q-q1-coldcard.dfu
e43932d04bf782f7b9ba218b54f29b9cd361b83ac3aadff9722714bca1ab7ee9 2025-02-13T1413-v1.3.1Q-q1-coldcard-factory.dfu
681874256bcfca71a3908f1dd6c623804517fdba99a51ed04c73b96119650c13 2024-12-18T1413-v6.3.4X-mk4-coldcard.dfu
93ab7615bcedeeff123498c109e5859dae28e58885e29ed86b6f3fd6ba709cce 2024-12-18T1407-v6.3.4QX-q1-coldcard.dfu
237cfcb3fdf9217550eae1d9ea6fc828c1c8d09470bd60c9f72f9b00a3bb2d11 2024-09-12T1734-v5.4.0-mk4-coldcard.dfu
6d1178f07d543e1777dbbdca41d872b00ca9c40e0c0c1ffb8ef96e19c51daa52 2024-09-12T1734-v5.4.0-mk4-coldcard-factory.dfu
d840fa4e83ebc7b0f961f30f68d795bed61271e2314dda4ab0eb0b8bfe7192f4 2024-09-12T1733-v1.3.0Q-q1-coldcard.dfu
@ -53,7 +87,6 @@ a49cd00808732c67b359c9f86814ddeafc63a1040823b6c1d2035a870575c9ed 2023-12-21T152
06d1048bea43c5d7c72c5e5f395a676620ce884aed0cd152627a86d922e2f3ab 2023-12-19T1444-v5.2.1-mk4-coldcard.dfu
3eb9c4b1add88a6fe412d783b8f4b895241a67e423bbacc6a13816a5216a30fe 2023-12-19T1444-v5.2.1-mk4-coldcard-factory.dfu
f4457dc44d08cbed9517e6260aa7163ecc254457276d3cdb0c2611af0f49ba9b 2023-10-26T1343-v6.2.1X-mk4-coldcard.dfu
1dcfb450f81883afe8f655239f06e238de7bae51e740cd4aa5ae6a0541772ad8 2023-10-26T1343-v6.2.1X-mk4-coldcard-factory.dfu
7fbed097d2757b21fde920f4b10f5f50d7e1aeca01ff52186dfde4883af5cace 2023-10-10T1735-v5.2.0-mk4-coldcard.dfu
4e3023676be88d6c6480c7f37de302f3a865077f9a2214de9c5a55b24afcba2c 2023-10-10T1735-v5.2.0-mk4-coldcard-factory.dfu
fd707f2f69d006c9db84ceacd2a0dde79c3cb71730750e2676af610942898717 2023-09-08T2009-v5.1.4-mk4-coldcard.dfu
@ -94,12 +127,12 @@ f05bc8dfed047c0c0abe5ed60621d2d14899b10717221c4af0942d96a1754f33 2021-03-29T192
bea27f263b524a66b3ed0a58c16805e98be0d7c3db20c2f7aab3238f2c6a6995 2019-12-19T1623-v3.0.6-coldcard.dfu
-----BEGIN PGP SIGNATURE-----
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmbjJicACgkQo6MbrVoq
WxAnMwf/e2kR1aK6AJiriRa1n3XDomw8ivaUQXUApmK0kawBhVBDLKw5aa3lvTcS
dg80wnenzNdE/QxctL+FkaZzKYsKbFpstkBEbZKcgbHVcinypKJJfICrhIBVVyZw
wdhJMGOLEyWMysqfaYMtYJQPkg5nIn0rRxn4yWXIeXAQLcFgdlWzVykqfGZW1xYr
CcVvxMqufXfc6c5aRFQzBO/YVHiRYzvK1NGDPztJEjXYU3zxnExAZFxk0vgpxvE3
CahKfSSTNv54u4CTLxYCdHPRq9OM6yL/w3OUyUQFklCizk2PjrObsJQW4szbbjlx
r7+587Pc5cpJCZn73Q0Y5/SWgnqm4g==
=/h9F
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmnD9ncACgkQo6MbrVoq
WxD9RAf+JkP/XVUPMDyfz+79AxBWFNU9r6RuYzXdzX3Z/XCKomZCZDtV7Ak6XlZi
GTNfsUNHaPC8WP6smFzYg07NoY2U1fVdY7+qeOi7UXF0hBBDJw7Gsa49P2zmt+DB
lfzivQG2n+mT4cM64Z0WF3BYBWmCuDJdctqUAnLJe2p8bh6S8n5hFeKqndRhffNK
773amkUrDW3RkHkIuevH4MQlR4ozWBmHzcehFDlTYT8BVLR8gg6hBzBEylxyDJNO
Ld4W5SzsT6We0RGX2uOpMERDjkizqT9t5J63drzpuPrUQA8XVQPaOc07vpFHRbbZ
BhA61XO8yazNLVvata611pSTikNnDQ==
=8Ti0
-----END PGP SIGNATURE-----

File diff suppressed because it is too large Load Diff

View File

@ -8,31 +8,32 @@ import chains, stash, version
from ux import ux_show_story, the_ux, ux_enter_bip32_index
from ux import export_prompt_builder, import_export_prompt_decode
from menu import MenuSystem, MenuItem
from public_constants import AFC_BECH32, AFC_BECH32M, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
from public_constants import AFC_BECH32, AFC_BECH32M, AF_P2WPKH, AF_CLASSIC
from multisig import MultisigWallet
from uasyncio import sleep_ms
from uhashlib import sha256
from ubinascii import hexlify as b2a_hex
from glob import settings
from auth import write_sig_file
from utils import addr_fmt_label, censor_address
from msgsign import write_sig_file
from charcodes import KEY_QR, KEY_NFC, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_HOME, KEY_LEFT, KEY_RIGHT
from charcodes import KEY_CANCEL
from utils import show_single_address, problem_file_line, truncate_address
def truncate_address(addr):
# Truncates address to width of screen, replacing middle chars
if not version.has_qwerty:
# - 16 chars screen width
# - but 2 lost at left (menu arrow, corner arrow)
# - want to show not truncated on right side
return addr[0:6] + '' + addr[-6:]
else:
# tons of space on Q1
return addr[0:12] + '' + addr[-12:]
def censor_address(addr):
# We don't like to show the user full multisig addresses because we cannot be certain
# they could actually be signed. And yet, don't blank too many
# spots or else an attacker could grind out a suitable replacement.
# 3 chars in the middle hidden by default
# censoring can be disabled by msas setting
if settings.get("msas", 0):
return addr
return addr[0:12] + '___' + addr[12+3:]
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.done_fn = done_fn
self.ranged = ranged
if path is None:
# Top level menu; useful shortcuts, and special case just "m"
@ -41,10 +42,13 @@ class KeypathMenu(MenuSystem):
MenuItem("m/44h/⋯", f=self.deeper),
MenuItem("m/49h/⋯", 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),
]
if self.ranged:
items += [
MenuItem("m/0/{idx}", menu=self.done),
MenuItem("m/{idx}", menu=self.done),
]
else:
# drill down one layer: (nl) is the current leaf
# - hardened choice first
@ -54,11 +58,14 @@ class KeypathMenu(MenuSystem):
MenuItem(p+"/⋯", menu=self.deeper),
MenuItem(p+"h", 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
max_wide = max(len(mi.label) for mi in items)
@ -96,30 +103,33 @@ class KeypathMenu(MenuSystem):
if isinstance(top, KeypathMenu):
the_ux.pop()
continue
assert isinstance(top, AddressListMenu)
# assert isinstance(top, AddressListMenu), type(top)
break
if self.done_fn:
return await self.done_fn(final_path)
return PickAddrFmtMenu(final_path, top)
async def deeper(self, _1, _2, item):
val = item.arg or item.label
assert val.endswith('/⋯')
cpath = val[:-2]
nl = await ux_enter_bip32_index('%s/' % cpath, unlimited=True)
return KeypathMenu(cpath, nl)
nl = await ux_enter_bip32_index('%s/' % cpath, unlimited=True, can_cancel=False)
return KeypathMenu(cpath, nl, ranged=self.ranged, done_fn=self.done_fn)
class PickAddrFmtMenu(MenuSystem):
def __init__(self, path, parent):
self.parent = parent
items = [
MenuItem(addr_fmt_label(AF_CLASSIC), f=self.done, arg=(path, AF_CLASSIC)),
MenuItem(addr_fmt_label(AF_P2WPKH), f=self.done, arg=(path, AF_P2WPKH)),
MenuItem(addr_fmt_label(AF_P2WPKH_P2SH), f=self.done, arg=(path, AF_P2WPKH_P2SH)),
MenuItem(chains.addr_fmt_label(af), f=self.done, arg=(path, af))
for af in chains.SINGLESIG_AF
]
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)
if path.startswith("m/49h"):
elif path.startswith("m/49h"):
self.goto_idx(2)
async def done(self, _1, _2, item):
@ -179,8 +189,7 @@ class AddressListMenu(MenuSystem):
# Create list of choices (address_index_0, path, addr_fmt)
choices = []
for name, path, addr_fmt in chains.CommonDerivations:
if '{coin_type}' in path:
path = path.replace('{coin_type}', str(chain.b44_cointype))
path = path.replace('{coin_type}', str(chain.b44_cointype))
if self.account_num != 0 and '{account}' not in path:
# skip derivations that are not affected by account number
@ -189,7 +198,7 @@ class AddressListMenu(MenuSystem):
deriv = path.format(account=self.account_num, change=0, idx=self.start)
node = sv.derive_path(deriv, register=False)
address = chain.address(node, addr_fmt)
choices.append( (truncate_address(address), path, addr_fmt) )
choices.append((truncate_address(address), path, addr_fmt))
dis.progress_sofar(len(choices), len(chains.CommonDerivations))
@ -199,7 +208,7 @@ class AddressListMenu(MenuSystem):
indent = '' if version.has_qwerty else ''
for i, (address, path, addr_fmt) in enumerate(choices):
axi = address[-4:] # last 4 address characters
items.append(MenuItem(addr_fmt_label(addr_fmt), f=self.pick_single,
items.append(MenuItem(chains.addr_fmt_label(addr_fmt), f=self.pick_single,
arg=(path, addr_fmt, axi)))
items.append(MenuItem(indent+address, f=self.pick_single,
arg=(path, addr_fmt, axi)))
@ -233,11 +242,15 @@ class AddressListMenu(MenuSystem):
self.goto_idx(axi)
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()
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()
async def pick_single(self, _1, _2, item):
@ -279,6 +292,7 @@ Press (3) if you really understand and accept these risks.
from wallet import MAX_BIP32_IDX
start = self.start
allow_qr = (not ms_wallet) or settings.get("msas", 0)
def make_msg(change=0):
# Build message and CTA about export, plus the actual addresses.
@ -293,20 +307,21 @@ Press (3) if you really understand and accept these risks.
dis.fullscreen('Wait...')
if ms_wallet:
# IMPORTANT safety feature: never show complete address
# IMPORTANT safety feature: do not show complete address unless user opt-in
# but show enough they can verify addrs shown elsewhere.
# - makes a redeem script
# - converts into addr
# - assumes 0/0 is first address.
for idx, addr, paths, script in ms_wallet.yield_addresses(start, n, change):
addrs.append(censor_address(addr))
addr = censor_address(addr)
addrs.append(addr)
if idx == 0 and ms_wallet.N <= 4:
msg += '\n'.join(paths) + '\n =>\n'
else:
msg += '⋯/%d/%d =>\n' % (change, idx)
msg += truncate_address(addr) + '\n\n'
msg += show_single_address(addr) + '\n\n'
dis.progress_sofar(idx-start+1, n)
else:
@ -319,14 +334,16 @@ Press (3) if you really understand and accept these risks.
for idx, addr, deriv in main.yield_addresses(start, n, change if allow_change else None):
addrs.append(addr)
msg += "%s =>\n%s\n\n" % (deriv, addr)
msg += "%s =>\n%s\n\n" % (deriv, show_single_address(addr))
dis.progress_sofar(idx-start+1, n or 1)
# export options
k0 = 'to show change addresses' if allow_change and change == 0 else None
export_msg, escape = export_prompt_builder('address summary file',
no_qr=bool(ms_wallet), key0=k0,
force_prompt=True)
export_msg, escape = export_prompt_builder(
'address summary file',
no_qr=not allow_qr,
key0=k0, force_prompt=True
)
if version.has_qwerty:
escape += KEY_LEFT+KEY_RIGHT+KEY_HOME+KEY_PAGE_UP+KEY_PAGE_DOWN
else:
@ -339,6 +356,9 @@ Press (3) if you really understand and accept these risks.
msg += '\n\n'
if n:
msg += "Press RIGHT to see next group, LEFT to go back. X to quit."
else:
escape += "0"
msg += " Press (0) to sign message with this key."
return msg, addrs, escape
@ -364,14 +384,11 @@ Press (3) if you really understand and accept these risks.
# continue on same screen in case they want to write to multiple cards
elif choice == KEY_QR:
# switch into a mode that shows them as QR codes
if ms_wallet:
# requires not multisig
continue
from ux import show_qr_codes
is_alnum = bool(addr_fmt & (AFC_BECH32 | AFC_BECH32M))
await show_qr_codes(addrs, is_alnum, start)
if allow_qr:
addr_fmt = addr_fmt or ms_wallet.addr_fmt
is_alnum = bool(addr_fmt & (AFC_BECH32 | AFC_BECH32M))
await show_qr_codes(addrs, is_alnum, start, is_addrs=True)
continue
@ -384,8 +401,15 @@ Press (3) if you really understand and accept these risks.
continue
elif choice == '0' and allow_change:
change = 1
elif choice == '0':
if allow_change:
change = 1
else:
# only custom path sets allow_change to False
# msg sign
from msgsign import sign_with_own_address
await sign_with_own_address(path, addr_fmt)
elif n is None:
# makes no sense to do any of below, showing just single address
continue
@ -421,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)]
) + '"\n'
if (start == 0) and (n > 100) and change in (0, 1):
saver = OWNERSHIP.saver(ms_wallet, change, start)
else:
saver = None
# saver will be None if we don't think it worth saving these addresses
saver = OWNERSHIP.saver(ms_wallet, change, start, n)
for (idx, addr, derivs, script) in ms_wallet.yield_addresses(start, n, change_idx=change):
if saver:
saver(addr)
saver(addr, idx)
# policy choice: never provide a complete multisig address to user.
addr = censor_address(addr)
@ -440,7 +462,7 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
yield ln
if saver:
saver(None) # close file
saver(None, 0) # close cache file
return
@ -448,26 +470,24 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
from wallet import MasterSingleSigWallet
main = MasterSingleSigWallet(addr_fmt, path, account_num)
if n and (start == 0) and (n > 100) and change in (0, 1):
saver = OWNERSHIP.saver(main, change, start)
else:
saver = None
# saver will be None if we don't think it worth saving these addresses
saver = OWNERSHIP.saver(main, change, start, n)
yield '"Index","Payment Address","Derivation"\n'
for (idx, addr, deriv) in main.yield_addresses(start, n, change_idx=change):
if saver:
saver(addr)
saver(addr, idx)
yield '%d,"%s","%s"\n' % (idx, addr, deriv)
if saver:
saver(None) # close
saver(None, 0) # close cache file
async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num,
start=0, count=250, change=0, **save_opts):
# write addresses into a text file on the MicroSD/VirtDisk
from glob import dis
from glob import dis, settings
from files import CardSlot, CardMissingError, needs_microsd
# simple: always set number of addresses.
@ -479,7 +499,6 @@ async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num,
# generator function
body = generate_address_csv(path, addr_fmt, ms_wallet, account_num, count,
start=start, change=change)
# pick filename and write
try:
with CardSlot(**save_opts) as card:
@ -490,28 +509,27 @@ async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num,
for idx, part in enumerate(body):
ep = part.encode()
fd.write(ep)
if not ms_wallet:
h.update(ep)
h.update(ep)
dis.progress_sofar(idx, count or 1)
sig_nice = None
if not ms_wallet:
if ms_wallet:
# sign with my key at the same path as first address of export
addr_fmt = AF_CLASSIC
derive = ms_wallet.get_my_deriv(settings.get('xfp'))
derive += "/%d/%d" % (change, start)
else:
derive = path.format(account=account_num, change=change, idx=start) # first addr
sig_nice = write_sig_file([(h.digest(), fname)], derive, addr_fmt)
sig_nice = write_sig_file([(h.digest(), fname)], derive, addr_fmt)
await ux_show_story("Address summary file written:\n\n%s\n\nAddress"
" signature file written:\n\n%s" % (nice, sig_nice))
except CardMissingError:
await needs_microsd()
return
except Exception as e:
from utils import problem_file_line
await ux_show_story('Failed to write!\n\n\n%s\n%s' % (e, problem_file_line(e)))
return
await ux_show_story('Failed to write!\n\n%s\n%s' % (e, problem_file_line(e)))
msg = '''Address summary file written:\n\n%s''' % nice
if sig_nice:
msg += "\n\nAddress signature file written:\n\n%s" % sig_nice
await ux_show_story(msg)
async def address_explore(*a):
# explore addresses based on derivation path chosen

File diff suppressed because it is too large Load Diff

View File

@ -5,16 +5,18 @@
import compat7z, stash, ckcc, chains, gc, sys, bip39, uos, ngu
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from utils import pad_raw_secret
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, OK, X
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
import version, ujson
from uio import StringIO
from uio import StringIO, BytesIO
import seed
from glob import settings
from pincodes import pa
# we make passwords with this number of words
num_pw_words = const(12)
bkpw_min_len = const(32)
# max size we expect for a backup data file (encrypted or cleartext)
# - limited by size of LFS area of flash, since all settings are held there
@ -43,16 +45,11 @@ def render_backup_contents(bypass_tmp=False):
COMMENT('Private key details: ' + chain.name)
with stash.SensitiveValues(bypass_tmp=bypass_tmp) as sv:
if sv.deltamode:
# die rather than give up our secrets
import callgate
callgate.fast_wipe()
with stash.SensitiveValues(bypass_tmp=bypass_tmp, enforce_delta=True) as sv:
if sv.mode == 'words':
ADD('mnemonic', bip39.b2a_words(sv.raw))
if sv.mode == 'master':
elif sv.mode == 'master':
ADD('bip32_master_key', b2a_hex(sv.raw))
ADD('chain', chain.ctype)
@ -79,7 +76,12 @@ def render_backup_contents(bypass_tmp=False):
current_tmp = pa.tmp_value[:]
pa.tmp_value = None
# 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.load()
stash.blank_object(nv)
@ -103,6 +105,9 @@ def render_backup_contents(bypass_tmp=False):
if k == 'bkpw': continue # confusing/circular
if k == 'sd2fa': continue # do NOT backup SD 2FA (card can be lost or damaged)
if k == 'words': continue # words length is recalculated from secret
if k == 'ccc': continue # not supported, security issue
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 == 'seeds' and not v: continue
ADD('setting.' + k, v)
@ -123,14 +128,14 @@ def render_backup_contents(bypass_tmp=False):
return rv.getvalue()
def extract_raw_secret(chain, vals):
def extract_raw_secret(vals):
# step1: the private key
# - prefer raw_secret over other values
# - TODO: fail back to other values
assert 'raw_secret' in vals
rs = vals.pop('raw_secret')
raw = pad_raw_secret(rs)
raw = deserialize_secret(rs)
# check we can decode this right (might be different firmare)
opmode, bits, node = stash.SecretStash.decode(raw)
@ -138,22 +143,23 @@ def extract_raw_secret(chain, vals):
# verify against xprv value (if we have it)
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'
return raw
return raw, node
def extract_long_secret(vals):
ls = None
if ('long_secret' in vals) and version.has_608:
try:
ls = a2b_hex(vals.pop('long_secret'))
except Exception as exc:
sys.print_exception(exc)
except:
# sys.print_exception(exc)
# but keep going.
pass
return ls
def restore_from_dict_ll(vals):
def restore_from_dict_ll(vals, raw):
# Restore from a dict of values. Already JSON decoded.
# Need a Reboot on success, return string on failure
# - low-level version, factored out for better testing
@ -164,12 +170,6 @@ def restore_from_dict_ll(vals):
#print("Restoring from: %r" % vals)
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.progress_bar_show(.1)
@ -188,9 +188,7 @@ def restore_from_dict_ll(vals):
if ls is not None:
try:
pa.ls_change(ls)
except Exception as exc:
sys.print_exception(exc)
# but keep going
except: pass # but keep going
pb = .70
dis.progress_bar_show(pb)
@ -208,19 +206,30 @@ def restore_from_dict_ll(vals):
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':
# do NOT restore sd2fa as SD card can be lost or damaged
# new version of firmware 5.1.3+ will not back sd2fa
# old backups need this to function properly
continue
if k == 'ccc':
# CCC feature cannot be backed-up nor restored for security reasons
# (would allow replay attacks)
continue
if k == 'tp':
# restore trick pins, which may involve many ops
from trick_pins import tp
try:
tp.restore_backup(vals[key])
except Exception as exc:
sys.print_exception(exc)
except: pass
# continue as `tp.restore_backup` handles
# saving into settings
@ -261,36 +270,50 @@ def restore_from_dict_ll(vals):
return None, need_ftux
async def restore_tmp_from_dict_ll(vals):
def text_bk_parser(contents):
# given a (binary encoded) text file, decode into a dict of values
# - use json rules to decode the "value" sides
vals = {}
for line in contents.decode().split('\n'):
if not line: continue
if line[0] == '#': continue
try:
k,v = line.split(' = ', 1)
#print("%s = %s" % (k, v))
vals[k] = ujson.loads(v)
except:
print("unable to decode line: %r" % line)
# but keep going!
return vals
async def restore_tmp_from_dict_ll(vals, raw):
from glob import dis
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...")
from seed import set_ephemeral_seed
from actions import goto_top_menu
await set_ephemeral_seed(raw, chain, meta="Coldcard Backup")
await set_ephemeral_seed(raw, chain, origin="Coldcard Backup")
for k, v in vals.items():
if not k[:8] == "setting.":
continue
key = k[8:]
if key in ["multisig"]:
# whitelist
settings.set(k, v)
settings.set(key, v)
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).
# 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 need_ftux:
@ -309,7 +332,7 @@ async def restore_from_dict(vals):
async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False):
from stash import bip39_passphrase
words = None
pwd = None
skip_quiz = False
bypass_tmp = False
@ -329,35 +352,49 @@ async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False):
"so backup will be of that seed."):
return
stored_words = settings.get('bkpw', None)
# first check if bkpw already defined on tmp seed settings
stored_pwd = None
master_pwd = settings.master_get("bkpw", None)
if pa.tmp_value:
stored_pwd = settings.get('bkpw', None)
if stored_words:
stored_words = stored_words.split()
ch = await ux_show_story("Use same backup file password as last time?\n\n"
" 1: %s\n ...\n%d: %s"
% (stored_words[0], len(stored_words), stored_words[-1]), sensitive=True)
if not stored_pwd and master_pwd:
stored_pwd = master_pwd
if stored_pwd:
# we can have words or other type of password here
split_pwd = stored_pwd.split()
if len(split_pwd) == num_pw_words: # weak
hint = " 1: %s\n ...\n%d: %s" % (split_pwd[0], len(split_pwd), split_pwd[-1])
else:
hint = " %s...%s" % (stored_pwd[0], stored_pwd[-1])
ch = await ux_show_story("Use same backup file password as last time?\n\n" + hint,
sensitive=True)
if ch == 'y':
words = stored_words
pwd = stored_pwd # string, not list
skip_quiz = True
if not words:
if not pwd:
# Pick a password: like bip39 but no checksum word
#
b = bytearray(32)
while 1:
ckcc.rng_bytes(b)
words = bip39.b2a_words(b).split(' ')[0:num_pw_words]
pwd = bip39.b2a_words(b).rsplit(' ', num_pw_words)[0]
ch = await seed.show_words(words,
prompt="Record this (%d word) backup file password:\n", escape='6')
ch = await seed.show_words(
prompt="Record this (%d word) backup file password:\n" % num_pw_words,
words=pwd.split(" "), escape='6'
)
if ch == '6' and not write_sflash:
if (ch == '6') and not write_sflash:
# Secret feature: plaintext mode
# - only safe for people living in faraday cages inside locked vaults.
if await ux_confirm("The file will **NOT** be encrypted and "
"anyone who finds the file will get all of your money for free!"):
words = []
pwd = []
fname_pattern = 'backup.txt'
break
continue
@ -367,43 +404,43 @@ async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False):
break
if words and not skip_quiz:
if pwd and not skip_quiz:
# quiz them, but be nice and do a shorter test.
ch = await seed.word_quiz(words, limited=(num_pw_words//3))
ch = await seed.word_quiz(pwd.split(" "), limited=(num_pw_words//3))
if ch == 'x': return
if words and words != stored_words:
if pwd and pwd != stored_pwd:
ch = await ux_show_story("Would you like to use these same words next time you perform a backup?"
" Press (1) to save them into this Coldcard for next time.", escape='1')
if ch == '1':
settings.put('bkpw', ' '.join(words))
settings.save()
elif stored_words:
settings.remove_key('bkpw')
settings.set('bkpw', pwd) # if on tmp save to tmp, do not update master
settings.save()
# stop droping bkpw just because someone decided to use differrent password
# elif stored_words:
# settings.remove_key('bkpw')
# settings.save()
return await write_complete_backup(words, fname_pattern, write_sflash=write_sflash,
return await write_complete_backup(pwd, fname_pattern, write_sflash=write_sflash,
bypass_tmp=bypass_tmp)
async def write_complete_backup(words, fname_pattern, write_sflash=False,
async def write_complete_backup(pwd, fname_pattern, write_sflash=False,
allow_copies=True, bypass_tmp=False):
# Just do the writing
from glob import dis
from files import CardSlot
# Show progress:
dis.fullscreen('Encrypting...' if words else 'Generating...')
dis.fullscreen('Encrypting...' if pwd else 'Generating...')
body = render_backup_contents(bypass_tmp=bypass_tmp).encode()
gc.collect()
if words:
if pwd:
# NOTE: Takes a few seconds to do the key-streching, but little actual
# time to do the encryption.
pw = ' '.join(words)
zz = compat7z.Builder(password=pw, progress_fcn=dis.progress_bar_show)
zz = compat7z.Builder(password=pwd, progress_fcn=dis.progress_bar_show)
zz.add_data(body)
# pick random filename, but ending in .txt
@ -422,8 +459,6 @@ async def write_complete_backup(words, fname_pattern, write_sflash=False,
if write_sflash:
# 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:
if zz:
fd.write(hdr)
@ -452,11 +487,9 @@ async def write_complete_backup(words, fname_pattern, write_sflash=False,
except Exception as e:
# includes CardMissingError
import sys
sys.print_exception(e)
# catch any error
ch = await ux_show_story('Failed to write! Please insert formated MicroSD card, '
'and press %s to try again.\n\nX to cancel.\n\n\n' % OK +str(e))
'and press %s to try again.\n\n%s to cancel.\n\n\n%s' % (OK, X, e))
if ch == 'x': break
continue
@ -519,106 +552,168 @@ async def verify_backup_file(fname):
# might be already closed on vdisk case due to filesystem unmount/mount
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):
async def restore_complete(fname_or_fd, temporary=False, words=True, usb=False):
from ux import the_ux
async def done(words):
# remove all pw-picking from menu stack
seed.WordNestMenu.pop_all()
if not version.has_qwerty and words:
seed.WordNestMenu.pop_all()
prob = await restore_complete_doit(fname_or_fd, words,
temporary=temporary)
if prob:
await ux_show_story(prob, title='FAILED')
if version.has_qwerty:
from ux_q1 import seed_word_entry
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)
if words:
if version.has_qwerty:
from ux_q1 import seed_word_entry, CHARS_W
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:]
async def restore_complete_doit(fname_or_fd, words, file_cleanup=None, temporary=False):
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:
pwd = [] # cleartext if words=None
if words is False:
ipw = await ux_input_text("", prompt="Your Backup Password",
min_len=bkpw_min_len, max_len=128)
if not ipw: return
pwd.append(ipw)
await done(pwd)
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
# - some errors will be shown, None return in that case
# - no return if successful (due to reboot)
from glob import dis
from files import CardSlot, CardMissingError, needs_microsd
# build password
password = ' '.join(words)
prob = None
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') if isinstance(fname_or_fd, str) else fname_or_fd
except:
return 'Unable to open backup file.\n\n' + str(fname_or_fd)
if isinstance(fname_or_fd, int):
# USB restore - backup is already in PSRAM, fname of fd is length
# TXN_INPUT_OFFSET = 0
with SFFile(0, length=fname_or_fd) as fd:
if not words:
contents = fd.read(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:
if not words:
contents = fd.read()
else:
try:
compat7z.check_file_headers(fd)
except Exception as e:
return 'Unable to read backup file. Has it been touched?\n\nError: ' \
+ str(e)
try:
if words:
contents = check_and_decrypt(fd, password)
else:
contents = fd.read()
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'
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()
except RuntimeError as e:
return str(e)
finally:
fd.close()
if file_cleanup:
file_cleanup(fname_or_fd)
except CardMissingError:
await needs_microsd()
return
except CardMissingError:
await needs_microsd()
return
vals = {}
for line in contents.decode().split('\n'):
if not line: continue
if line[0] == '#': continue
try:
vals = text_bk_parser(contents)
except:
return "Invalid backup file."
try:
k,v = line.split(' = ', 1)
#print("%s = %s" % (k, v))
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))
vals[k] = ujson.loads(v)
except:
print("unable to decode line: %r" % line)
# but keep going!
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.
if temporary:
return await restore_tmp_from_dict_ll(vals)
return await restore_tmp_from_dict_ll(vals, raw)
else:
return await restore_from_dict(vals)
return await restore_from_dict(vals, raw)
async def clone_start(*a):
# Begins cloning process, on target device.
@ -701,8 +796,9 @@ back and press %s to complete clone process.''' % OK)
uos.remove(fname) # ccbk-start.json
# 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:
await ux_show_story(prob, title='FAILED')
@ -742,11 +838,9 @@ async def clone_write_data(*a):
my_pubkey = pair.pubkey().to_bytes(False)
session_key = pair.ecdh_multiply(his_pubkey)
words = [b2a_hex(session_key).decode()]
fname = b2a_hex(my_pubkey).decode() + '-ccbk.7z'
await write_complete_backup(words, fname, allow_copies=False, bypass_tmp=True)
await write_complete_backup(b2a_hex(session_key).decode(), fname, allow_copies=False, bypass_tmp=True)
await ux_show_story("Done.\n\nTake this MicroSD card back to other Coldcard and continue from there.")

View File

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

View File

@ -6,12 +6,14 @@ import utime, uzlib, ngu
from utils import problem_file_line
from exceptions import QRDecodeExplained
from ubinascii import unhexlify as a2b_hex
from version import MAX_TXN_LEN
b32encode = ngu.codecs.b32_encode
b32decode = ngu.codecs.b32_decode
TYPE_LABELS = dict(P='PSBT File', T='Transaction', J='JSON', C='CBOR', U='Unicode Text',
X='Executable', B='Binary')
X='Executable', B='Binary',
R='KT Rx', S='KT Tx', E='KT PSBT')
def int2base36(n):
# convert an integer to two digits of base 36 string. 00 thu ZZ as bytes
@ -52,7 +54,7 @@ def calc_num_qr(char_capacity, char_len, split_mod):
if char_len > actual:
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
# and be more robust. Must respect split_mod alignment tho.
level = ceil(char_len / need)
@ -212,7 +214,7 @@ class BBQrState:
# can happen if QR got corrupted between scanner and us (overlap)
# or back BBQr implementation
#print("corrupt QR: %s" % scan)
import sys; sys.print_exception(exc)
# import sys; sys.print_exception(exc)
dis.draw_bbqr_progress(hdr, self.parts, corrupt=True)
return True
@ -241,7 +243,7 @@ class BBQrState:
# provide UX -- even if we didn't use it
dis.draw_bbqr_progress(hdr, self.parts)
# do we need more still?
# return T if we need more parts still
return (len(self.parts) < hdr.num_parts) or self.runt
class BBQrStorage:
@ -328,14 +330,12 @@ class BBQrPsramStorage(BBQrStorage):
def alloc_buf(self, upper_bound):
# using first part of PSRAM
from public_constants import MAX_TXN_LEN_MK4
if upper_bound >= MAX_TXN_LEN_MK4:
if upper_bound >= MAX_TXN_LEN:
raise QRDecodeExplained("Too big")
# If data is compressed, write tmp (compressed) copy into top half of PSRAM
# and we'll put final, decompressed copy at zero offset (later)
self.psr_offset = MAX_TXN_LEN_MK4 if self.hdr.encoding == 'Z' else 0
self.psr_offset = MAX_TXN_LEN if self.hdr.encoding == 'Z' else 0
self.buf = True
@ -394,7 +394,6 @@ class BBQrPsramStorage(BBQrStorage):
from glob import PSRAM, dis
from uzlib import DecompIO
from io import BytesIO
from public_constants import MAX_TXN_LEN_MK4
dis.fullscreen('Decompressing...')
@ -414,7 +413,7 @@ class BBQrPsramStorage(BBQrStorage):
buf += here
ln = len(buf) & ~3
if off+ln > MAX_TXN_LEN_MK4:
if off+ln > MAX_TXN_LEN:
# test with: `yes | dd bs=1000 count=2700 | bbqr make - | pbcopy`
raise QRDecodeExplained("Too big")
@ -440,5 +439,18 @@ class BBQrPsramStorage(BBQrStorage):
from glob import PSRAM
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

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

@ -1,6 +1,6 @@
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# calc.py - Simple python REPL before login
# calc.py - Simple TOY calculator, before login. Not meant to be useful, just fun!
#
# Test with: ./simulator.py --q1 --eff -g --set calc=1
#
@ -9,7 +9,7 @@ from utils import B2A, word_wrap
from ux_q1 import ux_input_text
async def login_repl():
from glob import dis, settings
from glob import dis
from pincodes import pa
NUM_LINES = 7 # 10 - title - 2 for prompt
@ -19,22 +19,25 @@ async def login_repl():
re_pin = re.compile(r'^(\d\d+)[-_ ](\d\d+)$')
# in decreasing order of hazard...
blacklist = ['import', '__', 'exec', 'locals', 'globals', 'eval', 'input']
# - find these with: import builtins; help(builtins)
blacklist = ['import', '__', 'exec', 'locals', 'globals', 'eval', 'input',
'getattr', 'setattr', 'delattr', 'open', 'execfile', 'compile' ]
lines = '''\
Example Commands:
>> 23 + 55 / 22
>> a = 4; b = 3;
>> a*b
>> sha256('123456123456')
>> cls() # clear screen\
>> 1.020 * 45.88
>> sha256('some message')
>> cls # clear screen
>> help\
'''.split('\n')
state = dict()
state['sha256'] = lambda x: B2A(ngu.hash.sha256s(x))
state['sha512'] = lambda x: B2A(ngu.hash.sha512(x).digest())
state['ripemd'] = lambda x: B2A(ngu.hash.ripemd160(x))
state['rand'] = lambda x=32: B2A(ngu.random.bytes(x))
state['cls'] = lambda: lines.clear()
state['help'] = lambda: 'Commands: ' + (', '.join(state))
@ -56,17 +59,17 @@ Example Commands:
try:
dis.busy_bar(1)
if ln == None :
if ln is None :
# Cancel key - do nothing
ans = None
elif ln in state and callable(state[ln]):
# no needs for () in my world
elif ln in ('help', 'cls', 'rand'):
# no need for () for these commands
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
m = re_pin.match(ln)
ln = m.group(1)+ '-' + m.group(2)
print(ln)
try:
pa.setup(ln)
ok = pa.login()
@ -80,16 +83,14 @@ Example Commands:
else:
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
ans = pa.prefix_words(ln[:-1].encode())
else:
if any((b in ln) for b in blacklist):
ans = None
elif '=' in ln:
ans = exec(ln, state)
else:
ans = eval(ln, state)
ans = eval(ln, state.copy())
except Exception as exc:
lines.extend(word_wrap(str(exc), 34))

1301
shared/ccc.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,13 +5,17 @@
import ngu
from uhashlib import sha256
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 AFC_PUBKEY, AFC_SEGWIT, AFC_BECH32, AFC_SCRIPT
from block_height import BLOCK_HEIGHT
from serializations import hash160, ser_compact_size, disassemble
from ucollections import namedtuple
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)
# See SLIP 132 <https://github.com/satoshilabs/slips/blob/master/slip-0132.md>
# for background on these version bytes. Not to be confused with SLIP-32 which involves Bech32.
Slip132Version = namedtuple('Slip132Version', ('pub', 'priv', 'hint'))
@ -19,18 +23,21 @@ Slip132Version = namedtuple('Slip132Version', ('pub', 'priv', 'hint'))
# See also:
# - <https://github.com/satoshilabs/slips/blob/master/slip-0132.md>
# - 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>
# - mailing list post proposed ypub, etc.
# - from <https://github.com/Bit-Wasp/bitcoin-php/issues/576>
# - also electrum source: electrum/lib/constants.py
# nLockTime in transaction equal or above this value is a unix timestamp (time_t) not block height.
NLOCK_IS_TIME = const(500000000)
class ChainsBase:
curve = 'secp256k1'
menu_name = None # use 'name' if this isn't defined
core_name = None # name of chain's "core" p2p software
ccc_min_block = 0
# b44_cointype comes from
# <https://github.com/satoshilabs/slips/blob/master/slip-0044.md>
@ -74,6 +81,41 @@ class ChainsBase:
or (version == cls.slip132[addr_fmt].priv)
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
def p2sh_address(cls, addr_fmt, witdeem_script):
# Multisig and general P2SH support
@ -85,21 +127,14 @@ class ChainsBase:
# - returns: str(address)
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:
digest = ngu.hash.sha256s(witdeem_script)
else:
digest = hash160(witdeem_script)
if addr_fmt & AFC_BECH32:
if addr_fmt == AF_P2WSH:
# bech32 encoded segwit p2sh
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:
# P2SH classic
# segwit p2wsh encoded as classic P2SH
# and P2SH classic
addr = ngu.codecs.b58_encode(cls.b58_script + digest)
return addr
@ -109,20 +144,8 @@ class ChainsBase:
# - renders a pubkey to an address
# - works only with single-key addresses
assert not addr_fmt & AFC_SCRIPT
keyhash = ngu.hash.hash160(pubkey)
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)
spk, _ = cls.script_pubkey(addr_fmt, pubkey=pubkey)
return cls.render_address(spk)
@classmethod
def address(cls, node, addr_fmt):
@ -161,7 +184,7 @@ class ChainsBase:
@classmethod
def hash_message(cls, msg=None, msg_len=0):
# Perform sha256 for message-signing purposes (only)
# - or get setup for that, if msg == None
# - or get setup for that, if msg is None
s = sha256()
s.update(cls.msg_signing_prefix())
@ -231,7 +254,7 @@ class ChainsBase:
return ngu.codecs.b58_encode(cls.b58_script + script[2:2+20])
# 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:])
# segwit v1 (P2TR) and later segwit version
@ -242,56 +265,40 @@ class ChainsBase:
@classmethod
def op_return(cls, script):
"""Returns decoded string op return data if script is op return otherwise None"""
gen = disassemble(script)
script_type = next(gen)
if OP_RETURN in script_type:
try:
data = next(gen)[0]
if data is None: raise RuntimeError
except (RuntimeError, StopIteration):
return "null-data", ""
data_hex = b2a_hex(data).decode()
data_ascii = None
if min(data) >= 32 and max(data) < 127: # printable
try:
data_ascii = data.decode("ascii")
except:
pass
return data_hex, data_ascii
return None
try:
gen = disassemble(script)
item, opcode = next(gen)
except (StopIteration, ValueError):
return None
@classmethod
def possible_address_fmt(cls, addr):
# 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
if addr.startswith(cls.bech32_hrp):
if addr.startswith(cls.bech32_hrp+'1p'):
# really any ver=1 script or address, but for now...
return AF_P2TR
else:
return AF_P2WPKH if len(addr) < 55 else AF_P2WSH
if opcode != OP_RETURN:
return None
try:
raw = ngu.codecs.b58_decode(addr)
except ValueError:
# not base58, not an error
return 0
try:
data, opcode = next(gen)
except StopIteration:
return b"" # bare OP_RETURN
if raw[0] == cls.b58_addr[0]:
return AF_CLASSIC
if raw[0] == cls.b58_script[0]:
return AF_P2SH
try:
next(gen)
return None # extra ops/pushes -> raw script display
except StopIteration: pass
return 0
except ValueError:
return None
if isinstance(data, bytes):
return data
if data is None and opcode == 0:
return b"" # OP_RETURN OP_0
return None
class BitcoinMain(ChainsBase):
# see <https://github.com/bitcoin/bitcoin/blob/master/src/chainparams.cpp#L140>
ctype = 'BTC'
name = 'Bitcoin'
core_name = 'Bitcoin Core'
name = 'Bitcoin Mainnet'
ccc_min_block = BLOCK_HEIGHT
slip132 = {
AF_CLASSIC: Slip132Version(0x0488B21E, 0x0488ADE4, 'x'),
@ -309,10 +316,10 @@ class BitcoinMain(ChainsBase):
b44_cointype = 0
class BitcoinTestnet(BitcoinMain):
class BitcoinTestnet(ChainsBase):
# testnet4 (was testnet3 up until 2025 but all parameters are the same)
ctype = 'XTN'
name = 'Bitcoin Testnet'
menu_name = 'Testnet: BTC'
name = 'Bitcoin Testnet 4'
slip132 = {
AF_CLASSIC: Slip132Version(0x043587cf, 0x04358394, 't'),
@ -331,27 +338,11 @@ class BitcoinTestnet(BitcoinMain):
b44_cointype = 1
class BitcoinRegtest(BitcoinMain):
class BitcoinRegtest(BitcoinTestnet):
ctype = 'XRT'
name = 'Bitcoin Regtest'
menu_name = 'Regtest: BTC'
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'
b58_addr = bytes([111])
b58_script = bytes([196])
b58_privkey = bytes([239])
b44_cointype = 1
def get_chain(short_name):
# lookup object from name: 'BTC' or 'XTN'
@ -379,7 +370,7 @@ def current_chain():
# Overbuilt: will only be testnet and mainchain.
AllChains = [BitcoinMain, BitcoinTestnet, BitcoinRegtest]
def slip32_deserialize(xp):
def slip132_deserialize(xp):
# .. and classify chain and addr-type, as implied by prefix
node = ngu.hdnode.HDNode()
version = node.deserialize(xp)
@ -405,6 +396,67 @@ CommonDerivations = [
AF_P2WPKH ), # generates bc1 bech32 addresses
]
STD_DERIVATIONS = {
"p2pkh": CommonDerivations[0][1],
"p2sh-p2wpkh": CommonDerivations[1][1],
"p2wpkh-p2sh": CommonDerivations[1][1],
"p2wpkh": CommonDerivations[2][1],
}
MS_STD_DERIVATIONS = {
("p2sh", "m/45h", AF_P2SH),
("p2sh_p2wsh", "m/48h/{coin}h/{acct_num}h/1h", AF_P2WSH_P2SH),
("p2wsh", "m/48h/{coin}h/{acct_num}h/2h", AF_P2WSH),
}
def parse_addr_fmt_str(addr_fmt):
# accepts strings and also integers if already parsed
try:
if isinstance(addr_fmt, int):
if addr_fmt in [AF_P2WPKH_P2SH, AF_P2WPKH, AF_CLASSIC]:
return addr_fmt
else:
raise ValueError
addr_fmt = addr_fmt.lower()
if addr_fmt in ("p2sh-p2wpkh", "p2wpkh-p2sh"):
return AF_P2WPKH_P2SH
elif addr_fmt == "p2pkh":
return AF_CLASSIC
elif addr_fmt == "p2wpkh":
return AF_P2WPKH
else:
raise ValueError
except ValueError:
raise ValueError("Invalid address format: '%s'\n\n"
"Choose from p2pkh, p2wpkh, p2sh-p2wpkh." % addr_fmt)
def af_to_bip44_purpose(addr_fmt):
# Address format to BIP-44 "purpose" number
# - single signature only
return {AF_CLASSIC: 44,
AF_P2WPKH_P2SH: 49,
AF_P2WPKH: 84}[addr_fmt]
def addr_fmt_label(addr_fmt):
# Text used in menus
return {AF_CLASSIC: "Classic P2PKH",
AF_P2WPKH_P2SH: "P2SH-Segwit",
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):
# verifies a message digest against a signature and recovers

View File

@ -107,4 +107,11 @@ if has_qwerty:
assert DECODER[KEYNUM_SYMBOL] == KEY_SYMBOL
assert DECODER[KEYNUM_LAMP] == KEY_LAMP
# These affect how 'ux stories' are rendered; they are control
# characters on the output side of things, not input.
# - must be first char in line
OUT_CTRL_TITLE = '\x01' # be a title line
OUT_CTRL_ADDRESS = '\x02' # it's a payment address
OUT_CTRL_NOWRAP = '\x03' # do not word wrap this line
# EOF

View File

@ -51,9 +51,7 @@ def decode_utf_16_le(s):
'''
def read_var64(f):
'''
Decode their silly 64-bit encoding.
'''
# Decode their silly 64-bit encoding.
first = ord(f.read(1))
if first < 128:
return first
@ -100,7 +98,7 @@ def check_file_headers(f):
# assume f is seekable
fh = FileHeader.read(f)
if not fh.has_good_magic:
if not fh.has_good_magic():
raise ValueError("Bad magic bytes")
# read only first header
@ -113,22 +111,21 @@ def check_file_headers(f):
if sh.size > 10000:
raise ValueError("Second header too big")
# capture this spot
# TODO 'data_start' unused
data_start = f.tell() # expect 0x20
# FileHeader.read() always reads exactly calcsize('<6sBBL') = 12 bytes
# SectionHeader.read() always reads exactly calcsize('<QQL') = 20 bytes
# after those two calls, f.tell() is always start_pos + 32
# assert f.tell() == 0x20 # expect 0x20
try:
f.seek(sh.offset, 1)
th = f.read(sh.size)
if len(th) != sh.size:
raise IndexError("Truncated file?")
assert len(th) == sh.size, "Truncated file?"
# Look for properties about compression. this could be
# faked-out but good enough for now
if b'\x24\x06\xf1\x07\x01' not in th:
raise RuntimeError("Not marked as AES+SHA encrypted?")
assert b'\x24\x06\xf1\x07\x01' in th, "Not marked as AES+SHA encrypted?"
except Exception as e:
raise ValueError("Confused file? %s" % e.message)
raise ValueError("Confused file? %s" % e)
if masked_crc(th) != sh.crc:
raise ValueError("Trailing header has wrong CRC")
@ -174,7 +171,6 @@ class FileHeader(object):
def actual_crc(self):
return masked_crc(self.bits)
class SectionHeader(namedtuple('SectionHeader', ['offset', 'size', 'crc' ])):
@ -198,7 +194,7 @@ class SectionHeader(namedtuple('SectionHeader', ['offset', 'size', 'crc' ])):
# read only next one; ftell has to be on first byte already
rv = cls.read(f)
if expect_crc != None:
if expect_crc is not None:
assert rv # read past end
assert masked_crc(rv.bits) == expect_crc
@ -213,6 +209,7 @@ class SectionHeader(namedtuple('SectionHeader', ['offset', 'size', 'crc' ])):
def actual_crc(self):
return masked_crc(self.bits)
class Builder(object):
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
@ -315,7 +312,7 @@ class Builder(object):
padded_len = (here + 15) & ~15
if padded_len != here:
if self.padding != None:
if self.padding is not None:
raise ValueError() # "can't do less than a block except at end"
self.padding = (padded_len - here)
raw += bytes(self.padding)

View File

@ -4,13 +4,22 @@
#
# included in Q builds only, not Mk4 --> manifest_q1.py
#
import ngu, bip39, ure, stash
import ngu, bip39, ure, stash, json
from ubinascii import unhexlify as a2b_hex
from exceptions import QRDecodeExplained
from bbqr import TYPE_LABELS
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):
# SeedQR: 4 digit groups of index into word list
parts = [data[pos:pos + 4] for pos in range(0, len(data), 4)]
@ -39,6 +48,8 @@ def decode_secret(got):
# - xprv / tprv
# - words (either full or prefixes, case insensitive)
# - 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:
raise ValueError("Too big.")
@ -51,7 +62,7 @@ def decode_secret(got):
# xprv or tprv: private key import for sure
# - verify checksum is right
try:
raw = ngu.codecs.b58_decode(got)
ngu.codecs.b58_decode(got)
except:
raise ValueError('corrupt xprv?')
@ -59,19 +70,11 @@ def decode_secret(got):
if len(got) in (51, 52):
try:
raw = ngu.codecs.b58_decode(got)
if raw[0] in (0xef, 0x80):
testnet = True if raw[0] == 0xef else False
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)
from wif import decode_wif
kp, testnet, compressed = decode_wif(got)
return 'wif', (got, kp, compressed, testnet)
except: pass
taste = got.strip().lower()
if taste.isdigit():
@ -101,7 +104,7 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
try:
ty, final_size, got = got.storage.finalize()
except BaseException as exc:
import sys; sys.print_exception(exc)
#import sys; sys.print_exception(exc)
raise QRDecodeExplained("BBQr decode failed: " + str(exc))
if expect_bbqr:
@ -116,11 +119,8 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
return got.decode()
if ty == 'P':
# may already be in PSRAM, avoid a copy here
from glob import PSRAM
if PSRAM.is_at(got, 0):
got = 'PSRAM' # see qr_psbt_sign()
# `got` is the literal 'PSRAM' from BBQrPsramStorage when data already there
# otherwise it's real bytes
return 'psbt', (None, final_size, got)
elif ty == 'T':
@ -128,10 +128,27 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
elif ty == 'U':
# continue thru code below for TEXT
pass
got = decode_qr_text(got)
elif ty == 'J':
return 'json', (got,)
got = decode_qr_text(got)
what = "json"
if "msg" in got:
what = "smsg"
return what, (got,)
elif ty in 'RSE':
# 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:
raise QRDecodeExplained("Truncated KT RX")
return 'teleport', (ty, got)
else:
msg = TYPE_LABELS.get(ty, 'Unknown FileType')
raise QRDecodeExplained("Sorry, %s not useful." % msg)
@ -159,6 +176,16 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
if expect_secret:
raise QRDecodeExplained("Not a secret?")
try:
dct = json.loads(got)
if "msg" in dct:
return "smsg", (got,)
except: pass
# Sparrow compat
if "signmessage" in got:
return "smsg", (got,)
# try to recognize various bitcoin-related text strings...
return decode_short_text(got)
@ -169,15 +196,13 @@ def decode_short_text(got):
# - if bad checksum on bitcoin addr, we treat as text... since might be
# return: what-it-is, (tuple)
if not isinstance(got, str):
# decode utf-8
try:
got = got.decode()
except UnicodeError:
raise QRDecodeExplained('UTF-8 decode failed')
got = decode_qr_text(got)
# might be a PSBT?
if len(got) > 100:
if got.lstrip().startswith("-----BEGIN BITCOIN SIGNED MESSAGE-----"):
return "vmsg", (got,)
from auth import psbt_encoding_taster
try:
decoder, _, psbt_len = psbt_encoding_taster(got[0:10].encode(), len(got))
@ -206,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]+"
rgx = ure.compile(cc_ms_pat)
# 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
for l in got.split("\n"):
if rgx.search(l):
if len(l) <= 150 and rgx.search(l):
c += 1
if c > 1:
return 'multi', (got,)

View File

@ -160,7 +160,6 @@ class Descriptor:
raise ValueError("Key origin info is required for %s" % (key))
key_orig_info = key[1:close_index] # remove brackets
key = key[close_index + 1:]
assert "/" in key_orig_info, "Malformed key derivation info"
return key_orig_info, key
@staticmethod

View File

@ -2,11 +2,11 @@
#
# display.py - OLED rendering
#
import machine, uzlib, ckcc, utime
import machine, uzlib, ckcc, utime, version
from ssd1306 import SSD1306_SPI
from version import is_devmode
import framebuf
from graphics_mk4 import Graphics
from charcodes import OUT_CTRL_TITLE, OUT_CTRL_ADDRESS
# we support 4 fonts
from zevvpeep import FontSmall, FontLarge, FontTiny
@ -34,11 +34,14 @@ class Display:
dc_pin = Pin('PA8', Pin.OUT)
cs_pin = Pin('PA4', Pin.OUT)
try:
self.dis = SSD1306_SPI(128, 64, spi, dc_pin, reset_pin, cs_pin)
except OSError:
print("OLED unplugged?")
raise
if version.mk_num == 5:
# Early revs (A-D) needed this pin asserted to enable +12v to OLED
# - removed in rev E and later boards, but keep here for dev boards
# - remove this in 2027
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.clear()
@ -75,7 +78,7 @@ class Display:
if x is None or x < 0:
# center/rjust
w = self.width(msg, font)
if x == None:
if x is None:
x = max(0, (self.WIDTH - w) // 2)
else:
# measure from right edge (right justify)
@ -141,7 +144,7 @@ class Display:
self.icon(128-3, 1, 'scroll')
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.text(-2, 21, 'D', font=FontTiny, invert=1)
self.text(-2, 28, 'E', font=FontTiny, invert=1)
@ -149,11 +152,14 @@ class Display:
def fullscreen(self, msg, percent=None, line2=None):
# show a simple message "fullscreen".
# - 'line2' not supported on smaller screen sizes, ignore
self.clear()
y = 14
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:
self.progress_bar(percent)
self.show()
@ -200,61 +206,20 @@ class Display:
def busy_bar(self, enable):
# 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:
# stop animation, and redraw old (new) screen
self.write_cmds(cleanup)
self.dis.busy_bar(False, None)
self.show()
else:
# a pattern that repeats nicely mod 128
# Need a pattern that repeats nicely mod 128
# - 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():
# 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)
self.dis.busy_bar(True, pat)
def set_brightness(self, val):
# normal = 0x7f, brightness=0xff, dim=0x00 (but they are all very similar)
self.dis.write_cmd(0x81) # Set Contrast Control
self.dis.write_cmd(val)
return self.dis.contrast(val)
def menu_draw(self, ry, msg, is_sel, is_checked, space_indicators):
# draw a menu item, perhaps selected, checked.
@ -265,17 +230,18 @@ class Display:
if is_sel:
self.dis.fill_rect(0, y, Display.WIDTH, h-1, 1)
self.icon(2, y, 'wedge', invert=1)
self.text(x, y, msg, invert=1)
nx = self.text(x, y, msg, invert=1)
else:
self.text(x, y, msg)
nx = self.text(x, y, msg)
# LATER: removed because caused confusion w/ underscore
#if msg[0] == ' ' and space_indicators:
# see also graphics/mono/space.txt
#self.icon(x-2, y+9, 'space', invert=is_sel)
if is_checked:
self.icon(108, y, 'selected', invert=is_sel)
if is_checked and nx <= 113:
# omit checkmark if it doesn't fit
self.icon(113, y, 'selected', invert=is_sel)
def menu_show(self, *a):
self.show()
@ -304,9 +270,14 @@ class Display:
for ln in lines:
if ln == 'EOT':
self.hline(y+3)
elif ln and ln[0] == '\x01':
elif ln and ln[0] == OUT_CTRL_TITLE:
self.text(0, y, ln[1:], FontLarge)
y += 21
elif ln and ln[0] == OUT_CTRL_ADDRESS:
from utils import chunk_address
fmt = '\u2009'.join(chunk_address(ln[1:]))
self.text(14, y, fmt) # fixed indent, to be centered
y += 15 # a bit extra vertical line height
else:
self.text(0, y, ln)
@ -322,13 +293,25 @@ class Display:
# no status bar on Mk4
return
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert):
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,
is_addr=False, force_msg=False, side_msg=None):
# '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
from utils import word_wrap
# - ignores "is_addr" because exactly zero space to do anything special
self.clear()
w = qr_data.width()
if w <= 29:
# version 1,2,3 => we can double-up the pixels
@ -368,13 +351,23 @@ class Display:
gly = framebuf.FrameBuffer(bytearray(packed), w, w, framebuf.MONO_HLSB)
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:
pass
elif not sidebar and len(msg) > (5*7):
elif not sidebar and ((len(msg) > (5*7)) or side_msg):
# use FontTiny and word wrap (will just split if no spaces)
# native segwit addresses and taproot
# if 'side_msg' also p2pkh and p2sh fall into this category as space is needed for "CHANGE BACK" text
x = bw + lm + 4
ww = ((128 - x)//4) - 1 # char width avail
y = 1
parts = list(word_wrap(msg, ww))
if len(parts) > 8:
parts = parts[:8]
@ -385,9 +378,16 @@ class Display:
for line in parts:
self.text(x, y, line, FontTiny)
y += 8
if side_msg and (len(side_msg) < 15):
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:
# hand-positioned for known cases
# - sidebar = (text, #of char per line)
# p2pkh and p2sh addresses (if is_change=False)
x, y = 73, (0 if is_alnum else 2)
dy = 10 if is_alnum else 12
sidebar, ll = sidebar if sidebar else (msg, 7)

View File

@ -11,8 +11,8 @@ from ux import ux_show_story, ux_enter_bip32_index, the_ux, ux_confirm, ux_drama
from menu import MenuItem, MenuSystem
from ubinascii import hexlify as b2a_hex
from ubinascii import b2a_base64
from auth import write_sig_file
from utils import chunk_writer, xfp2str, swab32
from msgsign import write_sig_file
from utils import xfp2str, swab32, node_from_privkey
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
BIP85_PWD_LEN = 21
@ -56,32 +56,32 @@ still backed-up.''')
def bip85_derive(picked, index):
# implement the core step of BIP85 from our master secret
path = "m/83696968h/"
if picked in (0,1,2):
# BIP-39 seed phrases (we only support English)
num_words = stash.SEED_LEN_OPTS[picked]
width = (16, 24, 32)[picked] # of bytes
path = "m/83696968h/39h/0h/{num_words}h/{index}h".format(num_words=num_words, index=index)
path += "39h/0h/%dh/%dh" % (num_words, index)
s_mode = 'words'
elif picked == 3:
# HDSeed for Bitcoin Core: but really a WIF of a private key, can be used anywhere
# HDSeed for Bitcoin Core: but really a WIF of a private key
s_mode = 'wif'
path = "m/83696968h/2h/{index}h".format(index=index)
path += "2h/%dh" % index
width = 32
elif picked == 4:
# New XPRV
path = "m/83696968h/32h/{index}h".format(index=index)
path += "32h/%dh" % index
s_mode = 'xprv'
width = 64
elif picked in (5, 6):
width = 32 if picked == 5 else 64
path = "m/83696968h/128169h/{width}h/{index}h".format(width=width, index=index)
path += "128169h/%dh/%dh" % (width, index)
s_mode = 'hex'
elif picked == 7:
width = 64
# hardcoded width for now
# b"pwd".hex() --> 707764
path = "m/83696968h/707764h/{pwd_len}h/{index}h".format(pwd_len=BIP85_PWD_LEN, index=index)
path += "707764h/%dh/%dh" % (BIP85_PWD_LEN, index)
s_mode = 'pw'
else:
raise ValueError(picked)
@ -124,8 +124,7 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
msg = "Password Index?" if picked == 7 else "Index Number?"
index = await ux_enter_bip32_index(msg, unlimited=settings.get("b85max", False))
if index is None:
return
if index is None: return
dis.fullscreen("Working...")
new_secret, width, s_mode, path = bip85_derive(picked, index)
@ -161,7 +160,7 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
qr_alnum = True
msg = 'Seed words (%d):\n' % len(words)
msg += ux_render_words(words)
msg += ux_render_words(words, leading_blanks=1)
encoded = stash.SecretStash.encode(seed_phrase=new_secret)
@ -180,7 +179,7 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
elif s_mode == 'xprv':
# Raw XPRV value.
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
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:
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
if encoded is not None:
key0 = 'to switch to derived secret'
elif s_mode == 'pw':
key0 = 'to type password over USB'
prompt, escape = export_prompt_builder('data', key0=key0,
prompt, escape = export_prompt_builder('data', key0=key0, key6=key6,
no_qr=(not qr), force_prompt=True)
title = None
if node:
@ -224,14 +221,17 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
ch = await ux_show_story(msg+'\n\n'+prompt, title=title, escape=escape,
strict_escape=True, sensitive=True)
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
dis.fullscreen("Saving...")
try:
with CardSlot(**choice) as card:
fname, out_fn = card.pick_filename('drv-%s-idx%d.txt' % (s_mode, index))
body = msg + "\n"
with open(fname, 'wt') as fp:
chunk_writer(fp, body)
fp.write(body)
h = ngu.hash.sha256s(body.encode())
sig_nice = write_sig_file([(h, fname)], derive=path)
@ -240,37 +240,37 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
await needs_microsd()
continue
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
story = "Filename is:\n\n%s" % out_fn
story += "\n\nSignature filename is:\n\n%s" % sig_nice
await ux_show_story(story, title='Saved')
elif choice == KEY_CANCEL:
break
elif choice == KEY_QR:
from ux import show_qr_code
await show_qr_code(qr, qr_alnum)
elif choice == '0':
if s_mode == 'pw':
# gets confirmation then types it
await single_send_keystrokes(qr, path)
elif encoded is not None:
# switch over to new secret!
dis.fullscreen("Applying...")
from actions import goto_top_menu
from glob import settings
xfp_str = xfp2str(settings.get("xfp", 0))
await seed.set_ephemeral_seed(
encoded,
meta='BIP85 Derived from [%s], index=%d' % (xfp_str, index)
)
goto_top_menu()
break
await show_qr_code(qr, qr_alnum, is_secret=True)
elif (choice == '0') and (encoded is not None):
# switch over to new secret!
dis.fullscreen("Applying...")
from actions import goto_top_menu
from glob import settings
xfp_str = xfp2str(settings.get("xfp", 0))
await seed.set_ephemeral_seed(
encoded,
origin='BIP85 Derived from [%s], index=%d' % (xfp_str, index)
)
goto_top_menu()
break
elif choice == "6":
# gets confirmation then types it
await single_send_keystrokes(qr, path)
elif NFC and choice == KEY_NFC:
# Share any of these over NFC
await NFC.share_text(qr)
await NFC.share_text(qr, is_secret=True)
stash.blank_object(msg)
stash.blank_object(new_secret)
@ -291,7 +291,7 @@ async def password_entry(*args, **kwargs):
while True:
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:
break

View File

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

View File

@ -5,23 +5,25 @@
import stash, chains, version, ujson, ngu
from uio import StringIO
from ucollections import OrderedDict
from utils import xfp2str, swab32, chunk_writer
from ux import ux_show_story
from utils import xfp2str, swab32
from ux import ux_show_story, import_export_prompt
from glob import settings
from auth import write_sig_file
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 charcodes import KEY_NFC, KEY_CANCEL, KEY_QR
from ownership import OWNERSHIP
from exceptions import QRTooBigError
async def export_by_qr(body, label, type_code):
async def export_by_qr(body, label, type_code, force_bbqr=False):
# render as QR and show on-screen
from ux import show_qr_code
try:
# ignore label/title - provides no useful info
# makes qr smaller and harder to read
if force_bbqr or len(body) > 2000:
raise QRTooBigError
await show_qr_code(body)
except (ValueError, RuntimeError, TypeError):
except QRTooBigError:
if version.has_qwerty:
# do BBQr on Q
from ux_q1 import show_bbqr_codes
@ -31,6 +33,79 @@ async def export_by_qr(body, label, type_code):
return
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,
intro="", footer="", ux_title=None):
# 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)
# checks if suitable to offer QR export on Mk4
# argument contents can support function that generates content
# argument direct way can be KEY_{NFC,QR}, any other truth value is SD/Vdisk,
# if None ask for way via UX story
from glob import dis, NFC, VD
from files import CardSlot, CardMissingError, needs_microsd
from qrs import MAX_V11_CHAR_LIMIT
if callable(contents):
dis.fullscreen('Generating...')
contents, derive, addr_fmt = contents()
# figure out if offering QR code export make sense given HW
# len() is O(1)
no_qr = not version.has_qwerty and (len(contents) >= MAX_V11_CHAR_LIMIT)
sig = not (derive is None and addr_fmt is None)
ch = direct_way # set it to direct way only once, outside the loop
while True:
if direct_way is None:
ch = await import_export_prompt("%s file" % title, intro=intro, footnotes=footer,
force_prompt=force_prompt, no_qr=no_qr, title=ux_title)
if ch == KEY_CANCEL:
break
elif ch == KEY_QR:
await export_by_qr(contents, title, "J" if is_json else "U", force_bbqr=force_bbqr)
elif ch == KEY_NFC:
if is_json:
await NFC.share_json(contents)
else:
await NFC.share_text(contents)
else:
# SD/VDisk
# choose a filename
try:
dis.fullscreen("Saving...")
with CardSlot(**ch) as card:
fname, nice = card.pick_filename(fname_pattern)
# do actual write
with open(fname, 'wt' if is_json else 'wb') as fd:
fd.write(contents)
if sig:
h = ngu.hash.sha256s(contents.encode())
sig_nice = write_sig_file([(h, fname)], derive, addr_fmt)
msg = '%s file written:\n\n%s' % (title, nice)
if sig:
msg += "\n\n%s signature file written:\n\n%s" % (title, sig_nice)
await ux_show_story(msg)
except CardMissingError:
await needs_microsd()
except Exception as e:
await ux_show_story('Failed to write!\n\n' + str(e))
# both exceptions & success gets here
if no_qr and (NFC is None) and (VD is None) and not force_prompt:
# user has no other ways enabled, we already exported to SD - done
return
if direct_way:
return
def generate_public_contents():
# Generate public details about wallet.
#
@ -73,14 +148,7 @@ be needed for different systems.
sym=chain.ctype, ct=chain.b44_cointype, xfp=xfp))
for name, path, addr_fmt in chains.CommonDerivations:
if '{coin_type}' in path:
path = path.replace('{coin_type}', str(chain.b44_cointype))
if '{' in name:
name = name.format(core_name=chain.core_name)
show_slip132 = ('Core' not in name)
path = path.replace('{coin_type}', str(chain.b44_cointype))
yield ('''## For {name}: {path}\n\n'''.format(name=name, path=path))
yield ('''First %d receive addresses (account=0, change=0):\n\n''' % num_rx)
@ -103,7 +171,7 @@ be needed for different systems.
node = sv.derive_path(hard_sub, register=False)
yield ("%s => %s\n" % (hard_sub, chain.serialize_public(node)))
if show_slip132 and addr_fmt != AF_CLASSIC and (addr_fmt in chain.slip132):
if addr_fmt != AF_CLASSIC and (addr_fmt in chain.slip132):
yield ("%s => %s ##SLIP-132##\n" % (
hard_sub, chain.serialize_public(node, addr_fmt)))
@ -133,46 +201,6 @@ be needed for different systems.
yield fp.getvalue()
del fp
async def write_text_file(fname_pattern, body, title, derive, addr_fmt):
# Export data as a text file.
from glob import dis, NFC
from files import CardSlot, CardMissingError, needs_microsd
from ux import import_export_prompt
choice = await import_export_prompt("%s file" % title, is_import=False,
no_qr=(not version.has_qwerty))
if choice == KEY_CANCEL:
return
elif choice == KEY_QR:
await export_by_qr(body, title, "U")
return
elif choice == KEY_NFC:
await NFC.share_text(body)
return
# choose a filename
try:
dis.fullscreen("Saving...")
with CardSlot(**choice) as card:
fname, nice = card.pick_filename(fname_pattern)
# do actual write
with open(fname, 'wb') as fd:
chunk_writer(fd, body)
h = ngu.hash.sha256s(body.encode())
sig_nice = write_sig_file([(h, fname)], derive, addr_fmt)
except CardMissingError:
await needs_microsd()
return
except Exception as e:
await ux_show_story('Failed to write!\n\n\n'+str(e))
return
msg = '%s file written:\n\n%s\n\n%s signature file written:\n\n%s' % (title, nice, title,
sig_nice)
await ux_show_story(msg)
async def make_summary_file(fname_pattern='public.txt'):
from glob import dis
@ -183,7 +211,7 @@ async def make_summary_file(fname_pattern='public.txt'):
# generator function:
body = "".join(list(generate_public_contents()))
ch = chains.current_chain()
await write_text_file(fname_pattern, body, 'Summary',
await export_contents('Summary', body, fname_pattern,
"m/44h/%dh/0h/0/0" % ch.b44_cointype,
AF_CLASSIC)
@ -239,7 +267,7 @@ importmulti '{imp_multi}'
ch = chains.current_chain()
derive = "84h/{coin_type}h/{account}h".format(account=account_num, coin_type=ch.b44_cointype)
await write_text_file(fname_pattern, body, 'Bitcoin Core', derive + "/0/0", AF_P2WPKH)
await export_contents('Bitcoin Core', body, fname_pattern, derive + "/0/0", AF_P2WPKH)
def generate_bitcoin_core_wallet(account_num, example_addrs):
# Generate the data for an RPC command to import keys into Bitcoin Core
@ -319,20 +347,16 @@ def generate_unchained_export(account_num=0):
# - no account numbers (at this level)
chain = chains.current_chain()
todo = [
( "m/48h/{coin}h/{acct_num}h/2h", 'p2wsh', AF_P2WSH ),
( "m/48h/{coin}h/{acct_num}h/1h", 'p2sh_p2wsh', AF_P2WSH_P2SH),
( "m/45h", 'p2sh', AF_P2SH), # if acct_num == 0
]
xfp = xfp2str(settings.get('xfp', 0))
rv = OrderedDict(xfp=xfp, account=account_num)
sign_der = None
with stash.SensitiveValues() as sv:
for deriv, name, fmt in todo:
for name, deriv, fmt in chains.MS_STD_DERIVATIONS:
if fmt == AF_P2SH and account_num:
continue
dd = deriv.format(coin=chain.b44_cointype, acct_num=account_num)
if fmt == AF_P2WSH:
sign_der = dd + "/0/0"
node = sv.derive_path(dd)
xp = chain.serialize_public(node, fmt)
@ -341,9 +365,7 @@ def generate_unchained_export(account_num=0):
rv['%s_deriv' % name] = dd
rv[name] = xp
# sig_deriv = "m/44'/{ct}'/{acc}'".format(ct=chain.b44_cointype, acc=account_num) + "/0/0"
# return ujson.dumps(rv), sig_deriv, AF_CLASSIC
return ujson.dumps(rv), False, False
return ujson.dumps(rv), sign_der, AF_CLASSIC
def generate_generic_export(account_num=0):
# Generate data that other programers will use to import Coldcard (single-signer)
@ -403,22 +425,15 @@ def generate_generic_export(account_num=0):
def generate_electrum_wallet(addr_type, account_num):
# 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.
chain = chains.current_chain()
xfp = settings.get('xfp')
# Must get the derivation path, and the SLIP32 version bytes right!
if addr_type == AF_CLASSIC:
mode = 44
elif addr_type == AF_P2WPKH:
mode = 84
elif addr_type == AF_P2WPKH_P2SH:
mode = 49
else:
raise ValueError(addr_type)
# Must get the derivation path, and the SLIP132 version bytes right!
mode = chains.af_to_bip44_purpose(addr_type)
OWNERSHIP.note_wallet_used(addr_type, account_num)
@ -448,80 +463,19 @@ def generate_electrum_wallet(addr_type, account_num):
return ujson.dumps(rv), derive + "/0/0", addr_type
async def make_json_wallet(label, func, fname_pattern='new-wallet.json'):
# Record **public** values and helpful data into a JSON file
# - OWNERSHIP.note_wallet_used(..) should be called already by our caller or func
from glob import dis, NFC
from files import CardSlot, CardMissingError, needs_microsd
from ux import import_export_prompt
from qrs import MAX_V11_CHAR_LIMIT
dis.fullscreen('Generating...')
json_str, derive, addr_fmt = func()
skip_sig = derive is False and addr_fmt is False
choice = await import_export_prompt("%s file" % label, is_import=False,
no_qr=(not version.has_qwerty and len(json_str) >= MAX_V11_CHAR_LIMIT))
if choice == KEY_CANCEL:
return
elif choice == KEY_NFC:
await NFC.share_json(json_str)
return
elif choice == KEY_QR:
# render as QR and show on-screen
# - on mk4, this isn't offered if more than about 300 bytes because we can't
# show that as a single QR
await export_by_qr(json_str, label, "J")
return
# choose a filename and save
try:
with CardSlot(**choice) as card:
fname, nice = card.pick_filename(fname_pattern)
# do actual write
with open(fname, 'wt') as fd:
chunk_writer(fd, json_str)
if not skip_sig:
h = ngu.hash.sha256s(json_str.encode())
sig_nice = write_sig_file([(h, fname)], derive, addr_fmt)
except CardMissingError:
await needs_microsd()
return
except Exception as e:
await ux_show_story('Failed to write!\n\n\n'+str(e))
return
msg = '%s file written:\n\n%s' % (label, nice)
if not skip_sig:
msg += '\n\n%s signature file written:\n\n%s' % (label, sig_nice)
await ux_show_story(msg)
async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int_ext=True,
fname_pattern="descriptor.txt"):
fname_pattern="descriptor.txt", direct_way=None):
from descriptor import Descriptor
from glob import dis
dis.fullscreen('Generating...')
chain = chains.current_chain()
xfp = settings.get('xfp')
xfp = settings.get('xfp', 0)
dis.progress_bar_show(0.1)
if mode is None:
if addr_type == AF_CLASSIC:
mode = 44
elif addr_type == AF_P2WPKH:
mode = 84
elif addr_type == AF_P2WPKH_P2SH:
mode = 49
else:
raise ValueError(addr_type)
mode = chains.af_to_bip44_purpose(addr_type)
OWNERSHIP.note_wallet_used(addr_type, account_num)
@ -547,7 +501,31 @@ async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int
)
dis.progress_bar_show(1)
await write_text_file(fname_pattern, body, "Descriptor", derive + "/0/0", addr_type)
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

View File

@ -264,7 +264,7 @@ class CardSlot:
self.active_led = self.active_led2 if use_b_slot else self.active_led1
def __enter__(self):
# Mk4: maybe use our virtual disk in preference to SD Card
# maybe use our virtual disk in preference to SD Card
if glob.VD and (self.force_vdisk or not self.is_inserted()):
self.mountpt = glob.VD.mount(self.readonly)
return self

View File

@ -19,6 +19,8 @@ from countdowns import countdown_chooser
from paper import make_paper_wallet
from trick_pins import TrickPinMenu
from tapsigner import import_tapsigner_backup_file
from ccc import toggle_ccc_feature, sssp_spending_policy, sssp_feature_menu
from wif import WIFStoreMenu
# useful shortcut keys
from charcodes import KEY_QR, KEY_NFC
@ -38,12 +40,14 @@ if version.has_battery:
from battery import battery_idle_timeout_chooser, brightness_chooser
from q1 import scan_and_bag
from notes import make_notes_menu
from teleport import kt_start_rx, kt_send_file_psbt
else:
battery_idle_timeout_chooser = None
brightness_chooser = None
scan_and_bag = None
make_notes_menu = None
kt_start_rx = None
kt_send_file_psbt = None
#
# NOTE: "Always In Title Case"
@ -69,6 +73,8 @@ def has_secrets():
from pincodes import pa
return pa.has_secrets()
qr_and_has_secrets = has_secrets if version.has_qr else False
def nfc_enabled():
from glob import NFC
return bool(NFC)
@ -95,6 +101,33 @@ def hsm_available():
# contains hsm feature + can it be used (needs se2 secret and no tmp active)
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):
goto_top_menu()
@ -132,12 +165,27 @@ LoginPrefsMenu = [
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 = [
# xxxxxxxxxxxxxxxx
MenuItem('Login Settings', menu=LoginPrefsMenu),
MenuItem('Hardware On/Off', menu=HWTogglesMenu),
NonDefaultMenuItem('Multisig Wallets', 'multisig',
menu=make_multisig_menu, predicate=has_secrets),
menu=make_multisig_menu, predicate=has_secrets, shortcut='m'),
NonDefaultMenuItem('NFC Push Tx', 'ptxurl', menu=pushtx_setup_menu),
MenuItem('Display Units', chooser=value_resolution_chooser),
MenuItem('Max Network Fee', chooser=max_fee_chooser),
@ -154,52 +202,48 @@ 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 \
which take apart the flash chips of the SDCard may still be able to find the \
data or filenames.'''),
ToggleMenuItem('Menu Wrapping', 'wa', ['Default Off', 'Enable'],
story='''When enabled, allows scrolling past menu top/bottom \
(wrap around). By default, this is 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'],
on_change=usb_keyboard_emulation,
predicate=has_secrets, # cannot generate BIP85 passwords without secret
story='''This mode adds a top-level menu item for typing \
deterministically-generated passwords (BIP-85), directly into an \
attached USB computer (as an emulated keyboard).'''),
MenuItem('Buried Settings', menu=BuriedSettingsMenu),
]
XpubExportMenu = [
# xxxxxxxxxxxxxxxx
MenuItem("Segwit (BIP-84)", f=export_xpub, arg=84),
MenuItem("Classic (BIP-44)", f=export_xpub, arg=44),
MenuItem("P2WPKH/P2SH (49)", f=export_xpub, arg=49),
MenuItem("P2WPKH/P2SH "+("(BIP-49)"if version.has_qwerty else "(49)"), f=export_xpub, arg=49),
MenuItem("Master XPUB", f=export_xpub, arg=0),
MenuItem("Current XFP", f=export_xpub, arg=-1),
]
WalletExportMenu = [
# xxxxxxxxxxxxxxxx
MenuItem("Sparrow", f=named_generic_skeleton, arg="Sparrow"),
MenuItem("Cove", f=named_generic_skeleton, arg="Cove"),
MenuItem("Bitcoin Core", f=bitcoin_core_skeleton),
MenuItem("Fully Noded", f=named_generic_skeleton, arg="Fully Noded"),
MenuItem("Sparrow Wallet", f=named_generic_skeleton, arg="Sparrow"),
MenuItem("Nunchuk", f=named_generic_skeleton, arg="Nunchuk"),
MenuItem("Zeus", f=ss_descriptor_skeleton,
arg=(True, [AF_P2WPKH, AF_P2WPKH_P2SH], "Zeus Wallet", "zeus-export.txt")),
MenuItem("Electrum Wallet", f=electrum_skeleton),
MenuItem("Theya", f=named_generic_skeleton, arg="Theya"),
MenuItem("Bull Bitcoin", f=ss_descriptor_skeleton,
arg=(True, [AF_P2WPKH], "", "bull-bitcoin.txt", KEY_QR)),
MenuItem("Blue Wallet", f=electrum_skeleton, arg="Blue"),
MenuItem("Electrum Wallet", f=electrum_skeleton, arg="Electrum"),
MenuItem("Wasabi Wallet", f=wasabi_skeleton),
MenuItem("Fully Noded", f=named_generic_skeleton, arg="Fully Noded"),
MenuItem("Unchained", f=unchained_capital_export),
MenuItem("Lily Wallet", f=named_generic_skeleton, arg="Lily"),
MenuItem("Theya", f=named_generic_skeleton, arg="Theya"),
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 Premix", f=samourai_pre_mix_descriptor_export),
# MenuItem("Samourai BadBank", f=samourai_bad_bank_descriptor_export), # not released yet
MenuItem("Descriptor", f=ss_descriptor_skeleton),
MenuItem("Generic JSON", f=generic_skeleton),
MenuItem("Export XPUB", menu=XpubExportMenu),
MenuItem("Key Expression", f=key_expression_skeleton),
MenuItem("Dump Summary", predicate=has_secrets, f=dump_summary),
]
@ -211,9 +255,11 @@ FileMgmtMenu = [
MenuItem('Export Wallet', predicate=has_secrets, menu=WalletExportMenu), #dup elsewhere
MenuItem('Sign Text File', predicate=has_secrets, f=sign_message_on_sd),
MenuItem('Batch Sign PSBT', predicate=has_secrets, f=batch_sign),
MenuItem('Teleport Multisig PSBT', predicate=qr_and_has_secrets, f=kt_send_file_psbt),
MenuItem('List Files', f=list_files),
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('Clone Coldcard', predicate=has_secrets, f=clone_write_data),
MenuItem('Format SD Card', f=wipe_sd_card),
@ -231,7 +277,9 @@ DevelopersMenu = [
# xxxxxxxxxxxxxxxx
MenuItem("Serial REPL", f=dev_enable_repl),
MenuItem('Warm Reset', f=reset_self),
MenuItem("Restore Txt Bkup", f=restore_everything_cleartext),
MenuItem("Restore Bkup", f=restore_backup_dev),
MenuItem("BKPW Override", menu=bkpw_override, predicate=has_secrets),
MenuItem('Reflash GPU', f=reflash_gpu, predicate=version.has_qwerty),
]
AdvancedVirginMenu = [ # No PIN, no secrets yet (factory fresh)
@ -248,6 +296,7 @@ AdvancedPinnedVirginMenu = [ # Has PIN but no secrets yet
MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu),
MenuItem("Upgrade Firmware", menu=UpgradeMenu, predicate=is_not_tmp),
MenuItem("File Management", menu=FileMgmtMenu),
MenuItem("Key Teleport (start)", f=kt_start_rx, predicate=version.has_qr),
MenuItem('Paper Wallets', f=make_paper_wallet),
MenuItem('Perform Selftest', f=start_selftest),
MenuItem("I Am Developer.", menu=maybe_dev_menu),
@ -258,6 +307,8 @@ DebugFunctionsMenu = [
# xxxxxxxxxxxxxxxx
MenuItem("Keyboard Test", f=keyboard_test),
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: except', f=debug_except),
MenuItem('Check: BL FW', f=check_firewall_read),
@ -291,7 +342,7 @@ DangerZoneMenu = [
"WARNING: Seed Vault is encrypted (AES-256-CTR) by your seed,"
" but not held directly inside secure elements. Backups are required"
" after any change to vault! Recommended for experiments or temporary use."),
predicate=has_se_secrets),
predicate=has_real_secret),
MenuItem('Perform Selftest', f=start_selftest), # little harmful
MenuItem("Set High-Water", f=set_highwater),
MenuItem('Wipe HSM Policy', f=wipe_hsm_policy, predicate=hsm_policy_available),
@ -304,7 +355,7 @@ If you disable sighash flag restrictions, and ignore the \
warnings, funds can be stolen by specially crafted PSBT or MitM.
Keep blocked unless you intend to sign special transactions.'''),
ToggleMenuItem('Testnet Mode', 'chain', ['Bitcoin', 'Testnet3', 'Regtest'],
ToggleMenuItem('Testnet Mode', 'chain', ['Bitcoin', 'Testnet4', 'Regtest'],
value_map=['BTC', 'XTN', 'XRT'],
on_change=change_which_chain,
story="Testnet must only be used by developers because \
@ -323,15 +374,15 @@ correctly- crafted transactions signed on Testnet could be broadcast on Mainnet.
MenuItem('Settings Space', f=show_settings_space),
MenuItem('MCU Key Slots', f=show_mcu_keys_left),
MenuItem('Bless Firmware', f=bless_flash), # no need for this anymore?
MenuItem('Reflash GPU', f=reflash_gpu, predicate=version.has_qwerty),
MenuItem("Wipe LFS", f=wipe_filesystem), # kills other-seed settings, HSM stuff, addr cache
MenuItem("Nuke Device", f=nuke_device),
]
BackupStuffMenu = [
# xxxxxxxxxxxxxxxx
MenuItem("Backup System", f=backup_everything),
MenuItem("Verify Backup", f=verify_backup),
MenuItem("Restore Backup", f=restore_everything), # just a redirect really
MenuItem("Restore Backup", f=need_clear_seed), # just a UX msg really
MenuItem('Clone Coldcard', predicate=has_secrets, f=clone_write_data),
]
@ -343,7 +394,20 @@ NFCToolsMenu = [
MenuItem('Verify Address', f=nfc_address_verify),
MenuItem('File Share', f=nfc_share_file),
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 = [
@ -352,19 +416,16 @@ AdvancedNormalMenu = [
MenuItem('Export Wallet', predicate=has_secrets, menu=WalletExportMenu, shortcut='x'), # also inside FileMgmt
MenuItem("Upgrade Firmware", menu=UpgradeMenu, predicate=is_not_tmp),
MenuItem("File Management", menu=FileMgmtMenu),
NonDefaultMenuItem('Secure Notes & Passwords', 'notes', menu=make_notes_menu,
NonDefaultMenuItem('Secure Notes & Passwords', 'secnap', menu=make_notes_menu,
predicate=version.has_qwerty),
MenuItem('Derive Seed B85' if not version.has_qwerty else 'Derive Seeds (BIP-85)',
f=drv_entro_start),
MenuItem("View Identity", f=view_ident),
MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu),
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),
ToggleMenuItem('Enable HSM', '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=hsm_available),
MenuItem('WIF Store', menu=WIFStoreMenu.make),
MenuItem('NFC Tools', predicate=nfc_enabled, menu=NFCToolsMenu, shortcut=KEY_NFC),
MenuItem("Danger Zone", menu=DangerZoneMenu, shortcut='z'),
]
@ -373,7 +434,7 @@ AdvancedNormalMenu = [
VirginSystem = [
# xxxxxxxxxxxxxxxx
MenuItem('Choose PIN Code', f=initial_pin_setup),
MenuItem('Advanced/Tools', menu=AdvancedVirginMenu),
MenuItem('Advanced/Tools', menu=AdvancedVirginMenu, shortcut='t'),
MenuItem('Bag Number', f=show_bag_number),
MenuItem('Help', f=virgin_help, predicate=not version.has_qwerty),
]
@ -384,7 +445,7 @@ ImportWallet = [
MenuItem("24 Words", menu=start_seed_import, arg=24),
MenuItem('Scan QR Code', predicate=version.has_qr,
shortcut=KEY_QR, f=scan_any_qr, arg=(True, False)),
MenuItem("Restore Backup", f=restore_everything),
MenuItem("Restore Backup", f=restore_backup, arg=False), # tmp=False
MenuItem("Clone Coldcard", menu=clone_start),
MenuItem("Import XPRV", f=import_xprv, arg=False), # ephemeral=False
MenuItem("Tapsigner Backup", f=import_tapsigner_backup_file, arg=False),
@ -408,9 +469,11 @@ EmptyWallet = [
MenuItem('New Seed Words', menu=NewSeedMenu),
MenuItem('Import Existing', menu=ImportWallet),
MenuItem("Migrate Coldcard", menu=clone_start),
MenuItem("Key Teleport (start)", f=kt_start_rx, predicate=version.has_qr),
MenuItem('Help', f=virgin_help, predicate=not version.has_qwerty),
MenuItem('Advanced/Tools', menu=AdvancedPinnedVirginMenu),
MenuItem('Advanced/Tools', menu=AdvancedPinnedVirginMenu, shortcut='t'),
MenuItem('Settings', menu=SettingsMenu),
ShortcutItem(KEY_QR, predicate=version.has_qr, f=scan_any_qr, arg=(True, False)),
]
# In operation, normal system, after a good PIN received.
@ -424,8 +487,8 @@ NormalSystem = [
MenuItem('Start HSM Mode', f=start_hsm_menu_item, predicate=hsm_policy_available),
MenuItem("Address Explorer", menu=address_explore, shortcut='x'),
MenuItem('Secure Notes & Passwords', menu=make_notes_menu, shortcut='n',
predicate=lambda: version.has_qwerty and (settings.get("notes", False) != False)),
MenuItem('Type Passwords', f=password_entry, shortcut='t',
predicate=lambda: version.has_qwerty and settings.get("secnap", False)),
MenuItem('Type Passwords', f=password_entry, shortcut='e',
predicate=lambda: settings.get("emu", False) and has_secrets()),
MenuItem('Seed Vault', menu=make_seed_vault_menu, shortcut='v',
predicate=lambda: settings.master_get('seedvault') and has_secrets()),
@ -437,10 +500,79 @@ NormalSystem = [
# Shown until unit is put into a numbered bag
FactoryMenu = [
MenuItem('Version: ' + version.get_mpy_version()[1], f=show_version),
MenuItem('Bag Me Now', f=scan_and_bag),
MenuItem('Version: ' + version.get_mpy_version()[1], f=show_version),
MenuItem('DFU Upgrade', f=start_dfu, shortcut='u'),
MenuItem('Ship W/O Bag', f=ship_wo_bag),
MenuItem("Debug Functions", menu=DebugFunctionsMenu, shortcut='f'),
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 uasyncio as asyncio
from utils import B2A
from machine import Pin
from ustruct import pack

View File

@ -18,7 +18,7 @@ from glob import settings
# - 8 bytes exact satoshi value => base64 (pad trimmed) => 11 chars
# - stored satoshi value is XOR'ed with LSB from prevout txn hash, which isn't stored
# - result is a 31 character string for each history entry, plus 4 overhead => 35 each
# - if we store 30 of those it's about 25% of total setting space
# - if we store 30 of those it's about 25% of total setting space (Mk3)
#
HISTORY_SAVED = const(30)
HISTORY_MAX_MEM = const(128)
@ -132,7 +132,7 @@ class OutptValueCache:
# save new addition
assert len(key) == ENCKEY_LEN
assert amount > 0
# assert amount > 0
entry = key + cls.encode_value(prevout, amount)
cls.runtime_cache.append(entry)

View File

@ -4,9 +4,9 @@
#
# 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
from sffile import SFFile
from utils import problem_file_line, cleanup_deriv_path, match_deriv_path
import ustruct, chains, sys, gc, uio, ujson, uos, utime, ckcc, ngu
from utils import problem_file_line, cleanup_deriv_path, match_deriv_path, keypath_to_str
from utils import cleanup_payment_address
from pincodes import AE_LONG_SECRET_LEN
from stash import blank_object
from users import Users, MAX_NUMBER_USERS, calc_local_pincode
@ -70,9 +70,9 @@ def restore_backup(s):
with open(POLICY_FNAME, 'wt') as f:
f.write(s)
except BaseException as exc:
except:
# keep going, we don't want to brick
sys.print_exception(exc)
# sys.print_exception(exc)
pass
def pop_list(j, fld_name, cleanup_fcn=None):
@ -149,22 +149,6 @@ def assert_empty_dict(j):
if extra:
raise ValueError("Unknown item: " + ', '.join(extra))
def cleanup_whitelist_value(s):
# one element in a list of addresses or paths or descriptors?
# - later matching is string-based, so just doing basic syntax check here
# - must be checksumed-base58 or bech32
try:
ngu.codecs.b58_decode(s)
return s
except: pass
try:
ngu.codecs.segwit_decode(s)
return s
except: pass
raise ValueError('bad whitelist value: ' + s)
class WhitelistOpts:
# contains various options related to whitelisting
@ -215,7 +199,7 @@ class ApprovalRule:
self.per_period = pop_int(j, 'per_period', 0, MAX_SATS)
self.max_amount = pop_int(j, 'max_amount', 0, MAX_SATS)
self.users = pop_list(j, 'users', check_user)
self.whitelist = pop_list(j, 'whitelist', cleanup_whitelist_value)
self.whitelist = pop_list(j, 'whitelist', cleanup_payment_address)
self.whitelist_opts = pop_dict(j, 'whitelist_opts', False, WhitelistOpts)
self.min_users = pop_int(j, 'min_users', 1, len(self.users))
self.local_conf = pop_bool(j, 'local_conf')
@ -621,7 +605,7 @@ class HSMPolicy:
fd.write('- XPUB values will be shared, if path matches: m OR %s.\n'
% plist(self.share_xpubs))
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))
if self.priv_over_ux:
fd.write('- Status responses optimized for privacy.\n')
@ -672,6 +656,15 @@ class HSMPolicy:
assert not glob.hsm_active
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()
if new_file:
@ -889,9 +882,6 @@ class HSMPolicy:
# do this super early so always cleared even if other issues
local_ok = self.consume_local_code(psbt_sha)
if not self.rules:
raise ValueError("no txn signing allowed")
# reject anything with warning, probably
if psbt.warnings:
if self.warnings_ok:
@ -899,6 +889,32 @@ class HSMPolicy:
else:
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).
users = []
for u, (token, counter) in auth.items():
@ -951,7 +967,7 @@ class HSMPolicy:
return 'y'
except BaseException as exc:
sys.print_exception(exc)
# sys.print_exception(exc)
err = "Rejected: " + (str(exc) or problem_file_line(exc))
self.refuse(log, err)

View File

@ -59,7 +59,7 @@ class ApproveHSMPolicy(UserAuthorizedAction):
msg = '''Last chance. You are defining a new policy which \
allows the Coldcard to sign specific transactions without any further user approval.\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,
escape='x'+confirm_char, strict_escape=True)
@ -67,7 +67,7 @@ Press %s to save policy and enable HSM mode.''' % (self.policy.hash(), confirm_c
except BaseException as exc:
self.failed = "Exception"
sys.print_exception(exc)
# sys.print_exception(exc)
self.refused = True
self.ux_done = True
@ -354,7 +354,7 @@ class hsmUxInteraction:
await sleep_ms(100)
except BaseException as exc:
# just in case, keep going
sys.print_exception(exc)
# sys.print_exception(exc)
continue
# do the interactions, but don't let user actually press anything

View File

@ -58,8 +58,8 @@ class ImportantTask:
else:
# uncaught exception in an unnamed (and unimportant) task
print("UNNAMED: " + context["message"])
sys.print_exception(context["exception"])
print("... future: %r" % context.get("future", '?'))
sys.print_exception(context["exception"]) # VERY USEFUL on sim
#print("... future: %r" % context.get("future", '?'))
def start_task(self, name, awaitable):
# 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:
self.is_pressed[kn] = 1
new_presses.add(kn)
elif self._history[i] == 0:
elif self._history[kn] == 0:
self.is_pressed[kn] = 0
self._history[kn] = 0

View File

@ -3,11 +3,11 @@
# lcd_display.py - LCD rendering for Q1's 320x240 pixel *colour* display!
#
import machine, uzlib, utime, array
from uasyncio import sleep_ms
from graphics_q1 import Graphics
from st7788 import ST7788
from utils import xfp2str, word_wrap
from utils import xfp2str, word_wrap, chunk_address
from ucollections import namedtuple
from charcodes import OUT_CTRL_TITLE, OUT_CTRL_ADDRESS
# the one font: fixed-width (except for a few double-width chars)
from font_iosevka import CELL_W, CELL_H, TEXT_PALETTES, COL_TEXT, COL_DARK_TEXT, COL_SCROLL_DARK
@ -154,7 +154,7 @@ class Display:
# otherwise: respect setting
if on_battery is None:
on_battery = (get_batt_threshold() != None)
on_battery = (get_batt_threshold() is not None)
if on_battery:
# user-defined brightness when running on batteries.
@ -190,7 +190,7 @@ class Display:
self.image(165, 0, 'tmp_%d' % kws['tmp'])
xfp = kws.get('xfp', None) # expects an integer
if xfp != None:
if xfp is not None:
x = 217
for ch in xfp2str(xfp).lower():
self.image(x, 0, 'ch_'+ch)
@ -268,7 +268,7 @@ class Display:
if x is None or x < 0:
w = self.width(msg)
if x == None:
if x is None:
# center: also blanks rest of line
x = max(0, (CHARS_W - w) // 2)
end_x = x + w
@ -612,25 +612,135 @@ class Display:
self.clear()
y=0
prev_x = None
for ln in lines:
if ln == 'EOT':
self.text(0, y, ''*CHARS_W, dark=True)
continue
elif ln and ln[0] == '\x01':
elif ln and ln[0] == OUT_CTRL_TITLE:
# title ... but we have no special font? Inverse!
self.text(0, y, ' '+ln[1:]+' ', invert=True)
if hint_icons:
# maybe show that [QR] can do something
# hint_icons not shown if is story without title
# maybe show that [QR,NFC] can do something
self.text(-1, y, hint_icons, dark=True)
elif ln and ln[0] == OUT_CTRL_ADDRESS:
# we can assume this will be a single line for our display
# thanks to code in utils.word_wrap
prev_x = self._draw_addr(y, ln[1:], prev_x=prev_x)
else:
self.text(0, y, ln)
prev_x = None
y += 1
self.scroll_bar(top, num_lines, CHARS_H)
self.show()
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert, partial_bar=None):
def _draw_addr(self, y, addr, prev_x=None):
# Draw a single-line of an address
# - use prev_x=0 to start centered
if prev_x is None:
# left justify (for stories)
prev_x = x = 1
elif prev_x == 0:
# center first line, following line(s) will be left-justified to match that
prev_x = x = max(((CHARS_W - (len(addr) * 5) // 4) // 2), 0)
else:
x = prev_x
self.text(x, y, ' '+' '.join(chunk_address(addr))+' ', invert=True)
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,
is_addr=False, force_msg=False, side_msg=None):
# Show a QR code on screen w/ some text under it
# - invert not supported on Q1
# - sidebar not supported here (see users.py)
@ -638,18 +748,19 @@ class Display:
assert not sidebar
# maybe show something other than QR contents under it
if msg:
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 (ie. payment addr)
# so split nicely, and shift off center
hh = len(msg) // 2
parts = [msg[0:hh] + ' ', ' '+msg[hh:]]
if is_addr:
# With fancy display, no address, even classic can fit in single line,
# so always split nicely in middle and at mod4
hh = len(msg) // 2
if hh <= 20:
hh = (hh + 3) & ~0x3
parts = [msg[0:hh], msg[hh:]]
num_lines = 2
else:
# do word wrap
parts = list(word_wrap(msg, CHARS_W))
# p2wsh address would need 3 lines to show, so we won't
num_lines = 0
elif msg:
parts = self.handle_qr_msg(msg)
num_lines = len(parts)
else:
num_lines = 0
@ -670,25 +781,21 @@ class Display:
fullscreen = False
trim_lines = 0
if w == 77:
# v15 => 77px x 3: 77*3 = 231px
expand = 3
num_lines = 0
fullscreen = True
elif w in (109, 113, 117):
# v23 => 109px x 2 = 218px
# v24 => 113px x 2 = 226px
# v25 => 117px x 2 = 234px
expand = 2
num_lines = 0
fullscreen = True
elif expand == 1 and num_lines:
# Maybe loose the text lines?
expand2 = max(1, ACTIVE_H // (w+2))
if expand2 > expand:
# v18,v19,v20,v21,v22
# always try to show the biggest possible QR code if not force_msg
if not force_msg:
if num_lines:
# better with text dropped?
e2 = max(1, ACTIVE_H // (w + 2))
if e2 > expand:
num_lines = 0
expand = e2
# fullscreen ?
e3 = (ACTIVE_H + 20) // (w + 2)
if expand < e3:
expand = e3
fullscreen = True
num_lines = 0
expand = expand2
# vert center in available space
qw = (w+2) * expand
@ -722,20 +829,13 @@ class Display:
if num_lines:
# centered text under that
y = CHARS_H - num_lines
for line in parts:
self.text(None, y, line)
y += 1
self.draw_qr_lines(parts, is_addr)
if idx_hint:
lh = len(idx_hint)
assert lh <= 10
if lh > 6:
# needs 2 lines
self.text(-1, 0, idx_hint[:6])
self.text(-1, 1, idx_hint[6:])
else:
self.text(-1, 0, idx_hint)
self.draw_qr_idx_hint(idx_hint)
if side_msg:
self.draw_side_msg(side_msg, idx_hint)
# pass a max brightness flag here, which will be cleared after next show
self.show(max_bright=True)
@ -770,8 +870,12 @@ class Display:
else:
pat = '' # clear line
self.text(None, -3, pat)
if count == hdr.num_parts and count == 1:
# skip the BS, it's a simple one
self.progress_bar_show(1)
return
self.text(None, -3, pat)
self.text(None, -2, 'Keep scanning more...' if count < hdr.num_parts else 'Got all parts!')
self.text(None, -1, '%s: %d of %d parts' % (hdr.file_label(), count, hdr.num_parts),
dark=True)

View File

@ -181,14 +181,22 @@ class LoginUX:
async def we_are_ewaste(self, num_fails):
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 \
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:
ch = await ux_show_story(msg, title='I Am Brick!', escape='6')
if ch == '6': break
if has_qwerty:
from calc import login_repl
await login_repl()
async def confirm_attempt(self, attempts_left, value):
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()
async def get_new_pin(self, title, story=None, allow_clear=False):
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
self.is_setting = True
@ -283,10 +291,6 @@ suffix break point is correct.\n\n'''
first_pin = await self.interact()
if first_pin is None: return None
if allow_clear and first_pin == '999999-999999':
# don't make them repeat the 'clear pin' value
return first_pin
self.is_repeat = True
while 1:

View File

@ -61,9 +61,7 @@ try:
from psram import PSRAMWrapper
glob.PSRAM = PSRAMWrapper()
except BaseException as exc:
sys.print_exception(exc)
# continue tho
except: pass # continue tho
# Setup keypad/keyboard
if version.has_qwerty:
@ -83,7 +81,6 @@ glob.settings = settings
async def more_setup():
# Boot up code; splash screen is being shown
try:
from files import CardSlot
CardSlot.setup()
@ -91,6 +88,10 @@ async def more_setup():
# This "pa" object holds some state shared w/ bootloader about the PIN
try:
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.
is_blank = pa.is_blank()
except RuntimeError as e:

View File

@ -1,19 +1,19 @@
# Freeze everything in this list.
# - not optimized because we need asserts to work
# - for specific boards, see manifest_mk[34].py and manifest_q1.py
# - for specific boards, see manifest_{mk4,q1}.py
freeze_as_mpy('', [
'actions.py',
'address_explorer.py',
'auth.py',
'backups.py',
'block_height.py',
'callgate.py',
'ccc.py',
'chains.py',
'choosers.py',
'compat7z.py',
'countdowns.py',
'descriptor.py',
'dev_helper.py',
'display.py',
'drv_entro.py',
'exceptions.py',
'export.py',
@ -26,48 +26,56 @@ freeze_as_mpy('', [
'login.py',
'main.py',
'menu.py',
'mk4.py',
'msgsign.py',
'multisig.py',
'ndef.py',
'nfc.py',
'numpad.py',
'nvstore.py',
'opcodes.py',
'ownership.py',
'paper.py',
'pincodes.py',
'psbt.py',
'psram.py',
'pwsave.py',
'queues.py',
'qrs.py',
'queues.py',
'random.py',
'seed.py',
'selftest.py',
'serializations.py',
'sffile.py',
'ssd1306.py',
'stash.py',
'tapsigner.py',
'trick_pins.py',
'usb.py',
'utils.py',
'ux.py',
'vdisk.py',
'version.py',
'xor_seed.py',
'tapsigner.py',
'wallet.py',
'ownership.py',
'web2fa.py',
'wif.py',
'xor_seed.py'
], opt=0)
# Optimize data-like files, since no need to debug them.
freeze_as_mpy('', [
'sigheader.py',
'public_constants.py',
'charcodes.py',
'public_constants.py',
'sigheader.py',
], opt=3)
# Maybe include test code.
import os
if int(os.environ.get('DEBUG_BUILD', 0)):
freeze_as_mpy('', [
'dev_helper.py',
'h.py',
'dev_helper.py',
'usb_test_commands.py',
'sim_display.py',
'usb_test_commands.py',
], opt=0)
include("$(MPY_DIR)/extmod/uasyncio/manifest.py")

View File

@ -1,17 +1,12 @@
# Mk4 only files; would not be needed on Mk3 or earlier.
freeze_as_mpy('', [
'ssd1306.py',
'mempad.py',
'psram.py',
'mk4.py',
'vdisk.py',
'nfc.py',
'ndef.py',
'trick_pins.py',
'ux_mk4.py',
'display.py',
'hsm.py',
'hsm_ux.py',
'mempad.py',
'ssd1306.py',
'users.py',
'ux_mk4.py'
], opt=0)
# Optimize data-like files, since no need to debug them.

View File

@ -1,29 +1,24 @@
# Q1/Mk4 only files; would not be needed on Mk3 or earlier.
# Q1 only files; would not be needed on Mk4
freeze_as_mpy('', [
'psram.py',
'mk4.py',
'q1.py',
'keyboard.py',
'scanner.py',
'bbqr.py',
'decoders.py',
'lcd_display.py',
'st7788.py',
'gpu.py',
'vdisk.py',
'nfc.py',
'ndef.py',
'trick_pins.py',
'ux_q1.py',
'battery.py',
'notes.py',
'bbqr.py',
'calc.py',
'decoders.py',
'gpu.py',
'keyboard.py',
'lcd_display.py',
'notes.py',
'q1.py',
'scanner.py',
'st7788.py',
'teleport.py',
'ux_q1.py'
], opt=0)
# Optimize data-like files, since no need to debug them.
freeze_as_mpy('', [
'graphics_q1.py',
'font_iosevka.py',
'gpu_binary.py', # remove someday?
'graphics_q1.py',
], opt=3)

View File

@ -119,7 +119,7 @@ class ShortcutItem(MenuItem):
super().__init__('SHORTCUT', shortcut=key, **kws)
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):
super().__init__(label, **kws)
self.nvkey = nvkey
@ -182,7 +182,7 @@ class ToggleMenuItem(MenuItem):
if self.nvkey == "chain":
default = (self.get() == "BTC")
else:
default = (self.get(None) == None)
default = (self.get(None) is None)
if self.story and default:
ch = await ux_show_story(self.story)
if ch == 'x': return
@ -290,7 +290,7 @@ class MenuSystem:
dis.clear()
cursor_y = None
for n in range(self.ypos+PER_M+1):
for n in range(PER_M+1):
real_idx = n+self.ypos
if real_idx >= self.count: break
@ -306,10 +306,6 @@ class MenuSystem:
if fcn and fcn():
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):
# ignore length constraint above, we need to visually show that
# smthg is selected - in any case
@ -335,9 +331,8 @@ class MenuSystem:
if wrap: return True
# 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.
limit = 10 if has_qwerty else 16
return self.count > limit
# Mk4: same limit
return self.count > 10
def down(self):
if self.cursor < self.count-1:
@ -362,12 +357,6 @@ class MenuSystem:
self.cursor = 0
self.ypos = 0
def goto_n(self, n):
# goto N from top of (current) screen
# change scroll only if needed to make it visible
self.cursor = max(min(n + self.ypos, self.count-1), 0)
self.ypos = max(self.cursor - n, 0)
def goto_idx(self, n):
# skip to any item, force cusor near middle of screen
n = self.count-1 if n >= self.count else n
@ -388,7 +377,7 @@ class MenuSystem:
self.up()
# events
def on_cancel(self):
async def on_cancel(self):
# override me
if the_ux.pop():
# top of stack (main top-level menu)
@ -399,7 +388,7 @@ class MenuSystem:
#
if picked is None:
# "go back" or cancel or something
self.on_cancel()
await self.on_cancel()
else:
await picked.activate(self, self.cursor)
@ -412,7 +401,7 @@ class MenuSystem:
gc.collect()
if self.multi_selected is not None:
# multichoice
self.on_cancel()
await self.on_cancel()
return ch
await self.activate(ch)
@ -474,7 +463,7 @@ class MenuSystem:
self.ypos = 0
elif '1' <= key <= '9':
# jump down, based on screen postion
self.goto_n(ord(key)-ord('1'))
self.goto_idx(ord(key)-ord('1'))
elif key in self.shortcuts:
# run the function, if predicate allows
m = self.shortcuts[key]
@ -489,7 +478,7 @@ class MenuSystem:
return self.items[self.cursor]
# search downwards for a menu item that starts with indicated letter
# if found, select it but dont drill down
# if found, select it but don't drill down
lst = list(range(self.cursor+1, self.count)) + list(range(0, self.cursor))
for n in lst:
if self.items[n].label[0].upper() == key.upper():

View File

@ -1,6 +1,6 @@
# (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
@ -11,8 +11,8 @@ def make_flash_fs():
os.VfsLfs2.mkfs(fl)
os.mount(fl, '/flash')
os.mkdir('/flash/settings')
os.chdir('/flash')
os.mkdir('settings')
def make_psram_fs():
# Filesystem is wiped and rebuilt on each boot before this point, but
@ -58,8 +58,7 @@ def init0():
try:
make_psram_fs()
except BaseException as exc:
sys.print_exception(exc)
except: pass
if version.is_devmode:
try:
@ -71,10 +70,13 @@ def init0():
rng_seeding()
async def dev_enable_repl(*a):
# Mk4: Enable serial port connection. You'll have to break case open.
# Enable serial port connection. You'll have to break case open.
from ux import ux_show_story
from utils import wipe_if_deltamode
wipe_if_deltamode()
if not version.is_devmode: return
# allow REPL access
ckcc.vcp_enabled(True)
@ -83,15 +85,4 @@ async def dev_enable_repl(*a):
await ux_show_story("""\
The serial port has now been enabled.\n\n3.3v TTL on Tx/Rx/Gnd pads @ 115,200 bps.""")
def wipe_if_deltamode():
# If in deltamode, give up and wipe self rather do
# a thing that might reveal true master secret...
from pincodes import pa
if not pa.is_deltamode():
return
callgate.fast_wipe()
# EOF

518
shared/msgsign.py Normal file
View File

@ -0,0 +1,518 @@
# (c) Copyright 2025 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# Signatures over text ... not transactions.
#
import stash, chains, sys, gc, ngu, ujson, version
from ubinascii import b2a_base64, a2b_base64
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from uhashlib import sha256
from public_constants import MSG_SIGNING_MAX_LENGTH
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
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,
import_export_prompt, ux_aborted)
from utils import problem_file_line, to_ascii_printable, show_single_address, node_from_privkey
from files import CardSlot, CardMissingError, needs_microsd
def rfc_signature_template(msg, addr, sig):
# RFC2440 <https://www.ietf.org/rfc/rfc2440.txt> style signatures, popular
# since the genesis block, but not really part of any BIP as far as I know.
#
return [
"-----BEGIN BITCOIN SIGNED MESSAGE-----\n",
"%s\n" % msg,
"-----BEGIN BITCOIN SIGNATURE-----\n",
"%s\n" % addr,
"%s\n" % sig,
"-----END BITCOIN SIGNATURE-----\n"
]
def parse_armored_signature_file(contents):
# XXX limited parser: will fail w/ messages containing dashes
sep = "-----"
assert contents.count(sep) == 6, "Armor text MUST be surrounded by exactly five (5) dashes."
temp = contents.split(sep)
msg = temp[2].strip()
addr_sig = temp[4].strip()
addr, sig_str = addr_sig.split()
return msg, addr, sig_str
def verify_signature(msg, addr, sig_str):
# Look at a base64 signature, and given address. Do full verification.
# - raise on errors
# - return warnings as string: can only be mismatch between addr format encoded in recid
warnings = ""
script = None
hash160 = None
invalid_addr_fmt_msg = "Invalid address format - must be one of p2pkh, p2sh-p2wpkh, or p2wpkh."
invalid_addr = "Invalid signature for message."
if addr[0] in "1mn":
addr_fmt = AF_CLASSIC
decoded_addr = ngu.codecs.b58_decode(addr)
hash160 = decoded_addr[1:] # remove prefix
elif addr.startswith("bc1q") or addr.startswith("tb1q") or addr.startswith("bcrt1q"):
if len(addr) > 44: # testnet/mainnet max singlesig len 42, regtest 44
# p2wsh
raise ValueError(invalid_addr_fmt_msg)
addr_fmt = AF_P2WPKH
_, _, hash160 = ngu.codecs.segwit_decode(addr)
elif addr[0] in "32":
addr_fmt = AF_P2WPKH_P2SH
decoded_addr = ngu.codecs.b58_decode(addr)
script = decoded_addr[1:] # remove prefix
else:
raise ValueError(invalid_addr_fmt_msg)
try:
sig_bytes = a2b_base64(sig_str)
if not sig_bytes or len(sig_bytes) != 65:
# can return b'' in case of wrong, can also raise
raise ValueError("invalid encoding")
header_byte = sig_bytes[0]
header_base = chains.current_chain().sig_hdr_base(addr_fmt)
if (header_byte - header_base) not in (0, 1, 2, 3):
# wrong header value only - this can still verify OK
warnings += "Specified address format does not match signature header byte format."
# least two significant bits
rec_id = (header_byte - 27) & 0x03
# need to normalize it to 31 base for ngu
new_header_byte = 31 + rec_id
sig = ngu.secp256k1.signature(bytes([new_header_byte]) + sig_bytes[1:])
except ValueError as e:
raise ValueError("Parsing signature failed - %s." % str(e))
digest = chains.current_chain().hash_message(msg.encode('ascii'))
try:
rec_pubkey = sig.verify_recover(digest)
except ValueError as e:
raise ValueError("Invalid signature for msg - %s." % str(e))
rec_pubkey_bytes = rec_pubkey.to_bytes()
rec_hash160 = ngu.hash.hash160(rec_pubkey_bytes)
if script:
target = bytes([0, 20]) + rec_hash160
target = ngu.hash.hash160(target)
if target != script:
raise ValueError(invalid_addr)
else:
if rec_hash160 != hash160:
raise ValueError(invalid_addr)
return warnings
async def verify_armored_signed_msg(contents, digest_check=True):
# Verify on-disk checksums of files listed inside a signed file.
# - digest_check=False for NFC cases, where we do not have filesystem
from glob import dis
dis.fullscreen("Verifying...")
try:
msg, addr, sig_str = parse_armored_signature_file(contents)
except Exception as e:
e_line = problem_file_line(e)
await ux_show_story("Malformed signature file. %s %s" % (str(e), e_line), title="FAILURE")
return
try:
sig_warn = verify_signature(msg, addr, sig_str)
except Exception as e:
await ux_show_story(str(e), title="ERROR")
return
title = "CORRECT"
warn_msg = ""
err_msg = ""
story = "Good signature by address:\n%s" % show_single_address(addr)
if digest_check:
digest_prob = verify_signed_file_digest(msg)
if digest_prob:
err, digest_warn = digest_prob
if digest_warn:
title = "WARNING"
wmsg_base = "not present. Contents verification not possible."
if len(digest_warn) == 1:
fname = digest_warn[0][0]
warn_msg += "'%s' is %s" % (fname, wmsg_base)
else:
warn_msg += "Files:\n" + "\n".join("> %s" % fname for fname, _ in digest_warn)
warn_msg += "\nare %s" % wmsg_base
if err:
title = "ERROR"
for fname, calc, got in err:
err_msg += ("Referenced file '%s' has wrong contents.\n"
"Got:\n%s\n\nExpected:\n%s" % (fname, got, calc))
if sig_warn:
# we know not ours only because wrong recid header used & not BIP-137 compliant
story = "Correctly signed, but not by this Coldcard. %s" % sig_warn
await ux_show_story('\n\n'.join(m for m in [err_msg, story, warn_msg] if m), title=title)
async def verify_txt_sig_file(filename):
# copy message into memory
try:
with CardSlot() as card:
with card.open(filename, 'rt') as fd:
text = fd.read()
except CardMissingError:
await needs_microsd()
return
except Exception as e:
await ux_show_story('Error: ' + str(e))
return
await verify_armored_signed_msg(text)
async def msg_sign_ux_get_subpath(addr_fmt):
# Ask for account number, and maybe change component of path for signature.
# - return full derivation path to be used.
purpose = chains.af_to_bip44_purpose(addr_fmt)
chain_n = chains.current_chain().b44_cointype
acct = await ux_enter_bip32_index('Account Number:')
if acct is None: return
ch = await ux_show_story(title="Change?",
msg="Press (0) to use internal/change address,"
" %s to use external/receive address." % OK, escape="0")
change = 1 if ch == '0' else 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)
def sign_export_contents(content_list, deriv, addr_fmt, pk=None):
# Return signed message over hashes of files.
msg2sign = make_signature_file_msg(content_list)
bitcoin_digest = chains.current_chain().hash_message(msg2sign)
sig_bytes, addr = sign_message_digest(bitcoin_digest, deriv, "Signing...", addr_fmt, pk=pk)
sig = b2a_base64(sig_bytes).decode().strip()
return rfc_signature_template(addr=addr, msg=msg2sign.decode(), sig=sig)
def verify_signed_file_digest(msg):
# Look inside a list of hashs and file names, and
# verify at their actual hashes and return list of issues if any.
parsed_msg = parse_signature_file_msg(msg)
if not parsed_msg:
# not our format
return
try:
err, warn = [], []
with CardSlot() as card:
for digest, fname in parsed_msg:
path = card.abs_path(fname)
if not card.exists(path):
warn.append((fname, None))
continue
path = card.abs_path(fname)
md = sha256()
with open(path, "rb") as f:
while True:
chunk = f.read(1024)
if not chunk:
break
md.update(chunk)
h = b2a_hex(md.digest()).decode().strip()
if h != digest:
err.append((fname, h, digest))
except:
# fail silently if issues with reading files or SD issues
# no digest checking
return
return err, warn
def write_sig_file(content_list, derive=None, addr_fmt=AF_CLASSIC, pk=None, sig_name=None):
if derive is None:
ct = chains.current_chain().b44_cointype
derive = "m/44'/%d'/0'/0/0" % ct
fpath = content_list[0][1]
if len(content_list) > 1:
# we're signing contents of more files - need generic name for sig file
assert sig_name
sig_nice = sig_name + ".sig"
sig_fpath = fpath.rsplit("/", 1)[0] + "/" + sig_nice
else:
sig_fpath = fpath.rsplit(".", 1)[0] + ".sig"
sig_nice = sig_fpath.split("/")[-1]
sig_gen = sign_export_contents([(h, f.split("/")[-1]) for h, f in content_list],
derive, addr_fmt, pk=pk)
with open(sig_fpath, 'wt') as fd:
for i, part in enumerate(sig_gen):
fd.write(part)
return sig_nice
def validate_text_for_signing(text, allow_tab_nl=False):
# Check for some UX/UI traps in the message itself.
# - messages must be short and ascii only. Our charset is limited
# - too many spaces, leading/trailing can be an issue
# MSG_MAX_SPACES = 4 # impt. compared to -=- positioning
text = str(text, "ascii") # handle memoryview coming from USB
result = to_ascii_printable(text, allow_tab_nl=allow_tab_nl)
length = len(result)
assert length >= 2, "msg too short (min. 2)"
assert length <= MSG_SIGNING_MAX_LENGTH, "msg too long (max. %d)" % MSG_SIGNING_MAX_LENGTH
assert " " not in result, 'too many spaces together in msg(max. 3)'
# other confusion w/ whitepace
assert result[0] != ' ', 'leading space(s) in msg'
assert result[-1] != ' ', 'trailing space(s) in msg'
# looks ok
return result
def addr_fmt_from_subpath(subpath):
if not subpath:
af = "p2pkh"
elif subpath[:4] == "m/84":
af = "p2wpkh"
elif subpath[:4] == "m/49":
af = "p2sh-p2wpkh"
else:
af = "p2pkh"
return af
def parse_msg_sign_request(data):
subpath = ""
addr_fmt = None
is_json = False
# sparrow compat
if "signmessage" in data:
try:
mark, subpath, *msg_line = data.split(" ", 2)
assert mark == "signmessage"
# subpath will be verified & cleaned later
assert msg_line[0][:6] == "ascii:"
text = msg_line[0][6:]
return text, subpath, addr_fmt_from_subpath(subpath), is_json
except:pass
# ===
try:
data_dict = ujson.loads(data.strip())
text = data_dict.get("msg", None)
if text is None:
raise AssertionError("MSG required")
subpath = data_dict.get("subpath", subpath)
assert isinstance(subpath, str), "subpath"
addr_fmt = data_dict.get("addr_fmt", addr_fmt)
is_json = True
except ValueError:
lines = data.split("\n")
assert lines, "min 1 line"
assert len(lines) <= 3, "max 3 lines"
if len(lines) == 1:
text = lines[0]
elif len(lines) == 2:
text, subpath = lines
else:
text, subpath, addr_fmt = lines
if not addr_fmt:
addr_fmt = addr_fmt_from_subpath(subpath)
if not subpath:
try:
subpath = chains.STD_DERIVATIONS[addr_fmt]
subpath = subpath.format(
coin_type=chains.current_chain().b44_cointype,
account=0, change=0, idx=0
)
except: pass
return text, subpath, addr_fmt, is_json
def make_signature_file_msg(content_list):
# list of tuples consisting of (hash, file_name)
return b"\n".join([
b2a_hex(h) + b" " + fname.encode()
for h, fname in content_list
])
def parse_signature_file_msg(msg):
# only succeed for our format digest + 2 spaces + fname
try:
res = []
lines = msg.split('\n')
for ln in lines:
d, fn = ln.split(' ')
# should not need to strip if our file format, so dont
# is hex? is 32 bytes long?
assert len(a2b_hex(d)) == 32
res.append((d, fn))
return res
except:
return
def sign_message_digest(digest, subpath, prompt, addr_fmt=AF_CLASSIC, pk=None):
# do the signature itself!
from glob import dis
ch = chains.current_chain()
if prompt:
dis.fullscreen(prompt, percent=.25)
if pk is None:
with stash.SensitiveValues() as sv:
node = sv.derive_path(subpath)
dis.progress_sofar(50, 100)
pk = node.privkey()
addr = ch.address(node, addr_fmt)
else:
# if private key is provided, derivation subpath is ignored
# and given private key is used for signing.
node = node_from_privkey(pk)
dis.progress_sofar(50, 100)
addr = ch.address(node, addr_fmt)
dis.progress_sofar(75, 100)
rv = ngu.secp256k1.sign(pk, digest, 0).to_bytes()
# AF_CLASSIC header byte base 31 is returned by default from ngu - NOOP
if addr_fmt != AF_CLASSIC:
# ngu only produces header base for compressed p2pkh, anyways get only rec_id
rv = bytearray(rv)
rec_id = (rv[0] - 27) & 0x03
rv[0] = rec_id + ch.sig_hdr_base(addr_fmt=addr_fmt)
dis.progress_bar_show(1)
return rv, addr
async def ux_sign_msg(txt, approved_cb=None, kill_menu=True):
from menu import MenuSystem, MenuItem
async def done(_1, _2, item):
from auth import approve_msg_sign
text, af = item.arg
subpath = await msg_sign_ux_get_subpath(af)
if subpath is None: return
await approve_msg_sign(text, subpath, af, approved_cb=approved_cb,
kill_menu=kill_menu, allow_tab_nl=True)
# pick address format
rv = [
MenuItem(chains.addr_fmt_label(af), f=done, arg=(txt, af))
for af in chains.SINGLESIG_AF
]
the_ux.push(MenuSystem(rv))
async def msg_signing_done(signature, address, text):
ch = await import_export_prompt("Signed Msg")
if ch == KEY_CANCEL:
return
if isinstance(ch, dict):
await sd_sign_msg_done(signature, address, text, "msg_sign", **ch)
elif version.has_qr and ch == KEY_QR:
from ux_q1 import qr_msg_sign_done
await qr_msg_sign_done(signature, address, text)
elif ch in KEY_NFC+"3":
from glob import NFC
if NFC:
await NFC.msg_sign_done(signature, address, text)
async def sign_with_own_address(subpath, addr_fmt):
# used for cases where we already have the key picked, but need the message:
# * address_explorer custom path
# * positive ownership test
to_sign = await ux_input_text("", scan_ok=True, prompt="Enter MSG") # max len is 100 only here
if not to_sign: return
from auth import approve_msg_sign
await approve_msg_sign(to_sign, subpath, addr_fmt, approved_cb=msg_signing_done, kill_menu=True)
async def sd_sign_msg_done(signature, address, text, base=None, orig_path=None,
slot_b=None, force_vdisk=False):
from glob import dis
dis.fullscreen('Generating...')
out_fn = None
sig = b2a_base64(signature).decode('ascii').strip()
while 1:
# try to put back into same spot
# add -signed to end.
target_fname = base + '-signed.txt'
lst = [orig_path]
if orig_path:
lst.append(None)
for path in lst:
try:
with CardSlot(readonly=True, slot_b=slot_b, force_vdisk=force_vdisk) as card:
out_full, out_fn = card.pick_filename(target_fname, path)
out_path = path
if out_full: break
except CardMissingError:
prob = 'Missing card.\n\n'
out_fn = None
if not out_fn:
# need them to insert a card
prob = ''
else:
# attempt write-out
try:
dis.fullscreen("Saving...")
with CardSlot(slot_b=slot_b, force_vdisk=force_vdisk) as card:
with card.open(out_full, 'wt') as fd:
# save in full RFC style
# gen length is 6
gen = rfc_signature_template(addr=address, msg=text, sig=sig)
for i, part in enumerate(gen):
fd.write(part)
# success and done!
break
except OSError as exc:
prob = 'Failed to write!\n\n%s\n\n' % exc
# sys.print_exception(exc)
# fall through to try again
# prompt them to input another card?
ch = await ux_show_story(prob + "Please insert an SDCard to receive signed message, "
"and press %s." % OK, title="Need Card")
if ch == 'x':
await ux_aborted()
return
# done.
msg = "Created new file:\n\n%s" % out_fn
await ux_show_story(msg, title='File Signed')
# EOF

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
#
# 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
#
from struct import pack, unpack

View File

@ -7,7 +7,7 @@
# - has GPIO signal "??" which is multipurpose on its own pin
# - this chip chosen because it can disable RF interaction
#
import utime, ngu, ndef, stash
import utime, ngu, ndef, stash, chains
from uasyncio import sleep_ms
import uasyncio as asyncio
from ustruct import pack, unpack
@ -15,7 +15,7 @@ from ubinascii import unhexlify as a2b_hex
from ubinascii import b2a_base64, a2b_base64
from ux import ux_show_story, ux_wait_keydown, OK, X
from utils import B2A, problem_file_line, parse_addr_fmt_str, txid_from_fname
from utils import B2A, problem_file_line, txid_from_fname
from public_constants import AF_CLASSIC
from charcodes import KEY_ENTER, KEY_CANCEL
@ -107,13 +107,14 @@ class NFCHandler:
from glob import dis
here = bytes(256)
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)
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
if full_wipe:
dis.progress_bar_show(pos / end)
await self.wait_ready()
# system config area (flash cells, but affect operation): table 12
@ -224,6 +225,17 @@ class NFCHandler:
self.set_rf_disable(1)
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:
aborted = await self.ux_animation(exit_after_activity=False, **kws)
if aborted:
await self.wipe(kws.get("is_secret", False))
break
async def share_signed_txn(self, txid, file_offset, txn_len, txn_sha):
# we just signed something, share it over NFC
if txn_len >= MAX_NFC_SIZE:
@ -231,13 +243,20 @@ class NFCHandler:
return
n = ndef.ndefMaker()
line2 = None
if txid is not None:
n.add_text('Signed Transaction: ' + txid)
n.add_custom('bitcoin.org:txid', a2b_hex(txid)) # want binary
line2 = self.txid_line2(txid)
n.add_custom('bitcoin.org:sha256', txn_sha)
n.add_large_object('bitcoin.org:txn', file_offset, txn_len)
return await self.share_start(n)
return await self.share_loop(n, line2=line2)
@staticmethod
def txid_line2(txid):
return "Signed TXID: %s%s" % (txid[0:8], txid[-8:])
async def share_push_tx(self, url, txid, txn, txn_sha, line2=None):
# Given a signed TXN, we convert to URL which a web backend can broadcast directly
@ -267,13 +286,9 @@ class NFCHandler:
n.add_url(url, https=is_https)
if line2 is None:
line2 = "Signed TXID: %s%s" % (txid[0:8], txid[-8:])
line2 = self.txid_line2(txid)
while 1:
done = await self.share_start(n, prompt="Tap to broadcast, CANCEL when done",
line2=line2)
if done: break
await self.share_loop(n, prompt="Tap to broadcast, CANCEL when done", line2=line2)
async def push_tx_from_file(self):
# Pick (signed txn) file from SD card and broadcast via PushTx
@ -343,24 +358,19 @@ class NFCHandler:
return
n = ndef.ndefMaker()
n.add_text(label or 'Partly signed PSBT')
label = label or 'Partly signed PSBT'
n.add_text(label)
n.add_custom('bitcoin.org:sha256', psbt_sha)
n.add_large_object('bitcoin.org:psbt', file_offset, psbt_len)
return await self.share_start(n)
async def share_deposit_address(self, addr, **kws):
n = ndef.ndefMaker()
n.add_text('Deposit Address')
n.add_custom('bitcoin.org:addr', addr.encode())
return await self.share_start(n, **kws)
return await self.share_loop(n, line2=label)
async def share_json(self, json_data, **kws):
# a text file of JSON for programs to read
n = ndef.ndefMaker()
n.add_mime_data('application/json', json_data)
return await self.share_start(n, **kws)
return await self.share_loop(n, **kws)
async def share_text(self, data, **kws):
# share text from a list of values
@ -368,7 +378,7 @@ class NFCHandler:
n = ndef.ndefMaker()
n.add_text(data)
return await self.share_start(n, **kws)
return await self.share_loop(n, **kws)
async def wait_ready(self):
# block until chip ready to continue (ACK happens)
@ -394,11 +404,13 @@ class NFCHandler:
self.write_dyn(GPO_CTRL_Dyn, 0x01) # GPO_EN
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, exit_after_activity=True,
min_delay=1000):
# 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
# - return T if aborted by user
from glob import dis, numpad
from glob import dis
await self.wait_ready()
self.set_rf_disable(0)
@ -411,7 +423,8 @@ class NFCHandler:
dis.text(None, -3, line2)
else:
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
phase = -1
@ -419,7 +432,6 @@ class NFCHandler:
# (ms) How long to wait after RF field comes and goes
# - user can press OK during this period if they know they are done
min_delay = (3000 if write_mode else 1000)
while 1:
if dis.has_lcd:
@ -458,7 +470,7 @@ class NFCHandler:
aborted = False
break
if last_activity:
if exit_after_activity and last_activity:
dt = utime.ticks_diff(utime.ticks_ms(), last_activity)
if dt >= min_delay:
# They acheived some RF activity and then nothing for some time, so
@ -467,8 +479,6 @@ class NFCHandler:
break
self.set_rf_disable(1)
if not write_mode:
await self.wipe(False)
return aborted
@ -476,17 +486,15 @@ class NFCHandler:
# do the UX while we are sharing a value over NFC
# - assumpting is people know what they are scanning
# - x key to abort early, but also self-clears
await self.big_write(ndef_obj.bytes())
return await self.ux_animation(False, **kws)
return await self.ux_animation(**kws)
async def start_nfc_rx(self, **kws):
# Pretend to be a big warm empty tag ready to be stuffed with data
await self.big_write(ndef.CC_WR_FILE)
# wait until something is written
aborted = await self.ux_animation(True, **kws)
aborted = await self.ux_animation(min_delay=3000, **kws)
if aborted: return
# read CCFILE area (header)
@ -514,7 +522,6 @@ class NFCHandler:
await self.wipe(False)
return rv
async def start_psbt_rx(self):
from auth import psbt_encoding_taster, TXN_INPUT_OFFSET
from auth import UserAuthorizedAction, ApproveTransaction
@ -540,10 +547,7 @@ class NFCHandler:
if urn == 'urn:nfc:ext:bitcoin.org:sha256' and len(msg) == 32:
# probably produced by another Coldcard: SHA256 over expected contents
psbt_sha = bytes(msg)
except Exception as e:
# dont crash when given garbage
import sys; sys.print_exception(e)
pass
except Exception: pass # dont crash when given garbage
if psbt_in is None:
await ux_show_story("Could not find PSBT in what was written.", title="Sorry!")
@ -564,44 +568,13 @@ class NFCHandler:
# start signing UX
UserAuthorizedAction.cleanup()
UserAuthorizedAction.active_request = ApproveTransaction(psbt_len, 0x0, psbt_sha=psbt_sha,
approved_cb=self.signing_done)
UserAuthorizedAction.active_request = ApproveTransaction(
psbt_len, psbt_sha=psbt_sha, input_method="nfc",
output_encoder=output_encoder
)
# kill any menu stack, and put our thing at the top
the_ux.push(UserAuthorizedAction.active_request)
async def signing_done(self, psbt):
# User approved the PSBT, and signing worked... share result over NFC (only)
from auth import TXN_OUTPUT_OFFSET, try_push_tx
from version import MAX_TXN_LEN
from sffile import SFFile
txid = None
# asssume they want final transaction when possible, else PSBT output
is_comp = psbt.is_complete()
# re-serialize the PSBT back out (into PSRAM)
with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as fd:
if is_comp:
txid = psbt.finalize(fd)
else:
psbt.serialize(fd)
self.result = (fd.tell(), fd.checksum.digest())
out_len, out_sha = self.result
if is_comp:
if txid and await try_push_tx(out_len, txid, out_sha):
return # success, exit
await self.share_signed_txn(txid, TXN_OUTPUT_OFFSET, out_len, out_sha)
else:
await self.share_psbt(TXN_OUTPUT_OFFSET, out_len, out_sha)
# ? show txid on screen ?
# thank them?
@classmethod
async def selftest(cls):
# Check for chip present, field present .. and that it works
@ -610,9 +583,11 @@ class NFCHandler:
n.setup()
assert n.uid
aborted = await n.share_text("NFC is working: %s" % n.get_uid(), allow_enter=False)
assert not aborted, "Aborted"
nn = ndef.ndefMaker()
nn.add_text("NFC is working: %s" % n.get_uid())
aborted = await n.share_start(nn, allow_enter=False)
assert not aborted, "Aborted"
async def share_file(self):
# Pick file from SD card and share over NFC...
@ -646,7 +621,7 @@ class NFCHandler:
# it's a txn, and we wrote as hex
data = a2b_hex(data)
else:
assert data[2:8] == bytes(6)
assert data[1:4] == bytes(3)
sha = ngu.hash.sha256s(data)
await self.share_signed_txn(txid, data, len(data), sha)
elif ext == 'psbt':
@ -663,78 +638,52 @@ class NFCHandler:
# user is pushing a file downloaded from another CC over NFC
# - would need an NFC app in between for the sneakernet step
# get some data
data = await self.start_nfc_rx()
if not data: return
def f(m):
if len(m) < 70:
return
m = m.decode()
winner = None
for urn, msg, meta in ndef.record_parser(data):
if len(msg) < 70: continue
msg = bytes(msg).decode() # from memory view
# multi( catches both multi( and sortedmulti(
if 'pub' in msg or "multi(" in msg:
winner = msg
break
if 'pub' in m or "multi(" in m:
return m
if not winner:
await ux_show_story('Unable to find multisig descriptor.')
return
winner = await self._nfc_reader(f, 'Unable to find multisig descriptor.')
from auth import maybe_enroll_xpub
try:
maybe_enroll_xpub(config=winner)
except Exception as e:
#import sys; sys.print_exception(e)
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
if winner:
from auth import maybe_enroll_xpub
try:
maybe_enroll_xpub(config=winner)
except Exception as e:
#import sys; sys.print_exception(e)
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
async def import_ephemeral_seed_words_nfc(self, *a):
data = await self.start_nfc_rx()
if not data: return
def f(m):
sm = m.decode().strip().split(" ")
if len(sm) in stash.SEED_LEN_OPTS:
return sm
winner = None
for urn, msg, meta in ndef.record_parser(data):
msg = bytes(msg).decode().strip() # from memory view
split_msg = msg.split(" ")
if len(split_msg) in stash.SEED_LEN_OPTS:
winner = split_msg
break
winner = await self._nfc_reader(f, 'Unable to find seed words')
if not winner:
await ux_show_story('Unable to find seed words')
return
try:
from seed import set_ephemeral_seed_words
await set_ephemeral_seed_words(winner, meta='NFC Import')
except Exception as e:
#import sys; sys.print_exception(e)
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
async def confirm_share_loop(self, string):
while True:
# added loop here as NFC send can fail, or not send the data
# and in that case one would have to start from beginning (send us cmd, approve, etc.)
# => get chance to check if you received the data and if something went wrong - retry just send
await self.share_text(string)
ch = await ux_show_story(title="Shared", msg="Press %s to share again, otherwise %s to stop." % (OK, X))
if ch != "y":
break
if winner:
try:
from seed import set_ephemeral_seed_words
await set_ephemeral_seed_words(winner, origin='NFC Import')
except Exception as e:
#import sys; sys.print_exception(e)
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
async def address_show_and_share(self):
from auth import show_address, ApproveMessageSign
from auth import show_address
data = await self.start_nfc_rx()
if not data: return
def f(m):
sm = m.decode().split("\n")
if 1 <= len(sm) <= 2:
return sm
winner = None
for urn, msg, meta in ndef.record_parser(data):
msg = bytes(msg).decode() # from memory view
split_msg = msg.split("\n")
if 1 <= len(split_msg) <= 2:
winner = split_msg
break
winner = await self._nfc_reader(f, 'Expected address and derivation path.')
if not winner:
await ux_show_story('Expected address and derivation path.')
return
if len(winner) == 1:
@ -743,7 +692,7 @@ class NFCHandler:
else:
subpath, addr_fmt_str = winner
try:
addr_fmt = parse_addr_fmt_str(addr_fmt_str)
addr_fmt = chains.parse_addr_fmt_str(addr_fmt_str)
except AssertionError as e:
await ux_show_story(str(e))
return
@ -754,131 +703,95 @@ class NFCHandler:
await the_ux.interact() # need this otherwise NFC animation takes over
async def start_msg_sign(self):
from auth import UserAuthorizedAction, ApproveMessageSign
from ux import the_ux
from auth import approve_msg_sign
UserAuthorizedAction.cleanup()
data = await self.start_nfc_rx()
if not data: return
winner = None
for urn, msg, meta in ndef.record_parser(data):
msg = bytes(msg).decode() # from memory view
split_msg = msg.split("\n")
def f(m):
m = m.decode()
split_msg = m.split("\n")
if 1 <= len(split_msg) <= 3:
winner = split_msg
break
return m
winner = await self._nfc_reader(f, 'Unable to find correctly formated message to sign.')
if not winner:
await ux_show_story('Unable to find correctly formated message to sign.')
return
if len(winner) == 1:
text = winner[0]
subpath = "m"
addr_fmt = AF_CLASSIC
elif len(winner) == 2:
text, subpath = winner
addr_fmt = AF_CLASSIC # maybe default to native segwit?
else:
# len(winner) == 3
text, subpath, addr_fmt = winner
UserAuthorizedAction.check_busy(ApproveMessageSign)
try:
UserAuthorizedAction.active_request = ApproveMessageSign(
text, subpath, addr_fmt, approved_cb=self.msg_sign_done
)
the_ux.push(UserAuthorizedAction.active_request)
except AssertionError as exc:
await ux_show_story("Problem: %s\n\nMessage to be signed must be a single line of ASCII text." % exc)
return
await approve_msg_sign(None, None, None, approved_cb=self.msg_sign_done,
msg_sign_request=winner)
async def msg_sign_done(self, signature, address, text):
from auth import rfc_signature_template_gen
from msgsign import rfc_signature_template
sig = b2a_base64(signature).decode('ascii').strip()
armored_str = "".join(rfc_signature_template_gen(addr=address, msg=text, sig=sig))
await self.confirm_share_loop(armored_str)
armored_str = "".join(rfc_signature_template(addr=address, msg=text, sig=sig))
await self.share_text(armored_str)
async def verify_sig_nfc(self):
from auth import verify_armored_signed_msg
from msgsign import verify_armored_signed_msg
data = await self.start_nfc_rx()
if not data: return
f = lambda x: x.decode().strip() if b"SIGNED MESSAGE" in x else None
winner = await self._nfc_reader(f, 'Unable to find signed message.')
winner = None
for urn, msg, meta in ndef.record_parser(data):
msg = bytes(msg).decode() # from memory view
if "SIGNED MESSAGE" in msg:
winner = msg.strip()
break
if winner:
await verify_armored_signed_msg(winner, digest_check=False)
if not winner:
await ux_show_story('Unable to find signed message.')
return
await verify_armored_signed_msg(winner, digest_check=False)
async def verify_address_nfc(self):
# Get an address or complete bip-21 url even and search it... slow.
async def read_address(self):
# Read an address or BIP-21 url and parse out addr (just one)
from utils import decode_bip21_text
data = await self.start_nfc_rx()
if not data: return
def f(m):
m = m.decode()
what, vals = decode_bip21_text(m)
if what == 'addr':
return vals
winner = None
for urn, msg, meta in ndef.record_parser(data):
msg = bytes(msg).decode() # from memory view
try:
what, vals = decode_bip21_text(msg)
if what == 'addr':
winner = vals[1]
break
except ValueError:
pass
if not winner:
await ux_show_story('Unable to find address from NFC data.')
return
from ownership import OWNERSHIP
await OWNERSHIP.search_ux(winner)
async def read_extended_private_key(self):
data = await self.start_nfc_rx()
if not data: return
winner = None
for urn, msg, meta in ndef.record_parser(data):
msg = bytes(msg).decode() # from memory view
if "prv" in msg:
winner = msg.strip()
break
if not winner:
await ux_show_story('Unable to find extended private key.')
return
winner = await self._nfc_reader(f, 'Unable to find address from NFC data.')
return winner
async def verify_address_nfc(self):
# Get an address or complete bip-21 url even and search it... slow.
res = await self.read_address()
if not res: return
_, addr, args = res
from ownership import OWNERSHIP
await OWNERSHIP.search_ux(addr, args)
async def read_extended_private_key(self):
f = lambda x: x.decode().strip() if b"prv" in x else None
return await self._nfc_reader(f, 'Unable to find extended private key.')
async def read_tapsigner_b64_backup(self):
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.')
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):
data = await self.start_nfc_rx()
if not data: return
winner = None
for urn, msg, meta in ndef.record_parser(data):
msg = bytes(msg).decode() # from memory view
try:
if 150 <= len(msg) <= 280:
winner = a2b_base64(msg)
break
except:
pass
try:
for urn, msg, meta in ndef.record_parser(data):
msg = bytes(msg)
try:
r = func(msg)
if r is not None:
winner = r
break
except:
pass
except Exception: pass # dont crash when given garbage
if not winner:
await ux_show_story('Unable to find base64 encoded TAPSIGNER backup.')
await ux_show_story(fail_msg)
return
return winner

View File

@ -10,10 +10,11 @@ from ux_q1 import QRScannerInteraction
from actions import goto_top_menu
from glob import settings, dis
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_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6
from lcd_display import CHARS_W
from utils import problem_file_line, url_decode
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
# text entry (W-2) and also in menu display (W-3)
@ -21,7 +22,17 @@ from utils import problem_file_line, url_decode
ONE_LINE = CHARS_W-2
async def make_notes_menu(*a):
if settings.get('notes', False) == False:
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):
# Explain feature, and then enable if interested. Drop them into menu.
ch = await ux_show_story('''\
Enable this feature to store short text notes and passwords inside the Coldcard.
@ -34,15 +45,17 @@ Press ENTER to enable and get started otherwise CANCEL.''',
if ch != 'y':
return
# mark as enabled (altho empty)
settings.set('notes', [])
# mark as enabled
settings.set('secnap', True)
if settings.get('notes', None) is None:
settings.set('notes', [])
# need to correct top menu now, so this choice is there.
goto_top_menu()
return NotesMenu(NotesMenu.construct())
async def get_a_password(old_value):
async def get_a_password(old_value, min_len=0, max_len=128):
# Get a (new) password as a string.
# - does some fun generation as well.
@ -96,12 +109,14 @@ async def get_a_password(old_value):
handlers = {KEY_F1: _pick_12, KEY_F2: _pick_24, KEY_F3: _pick_dense,
KEY_F4: _do_dumb, KEY_F6: _toggle_case, KEY_F5: _bip85}
return await ux_input_text(old_value, confirm_exit=False, max_len=128, scan_ok=True,
b39_complete=True, prompt='Password', placeholder='(optional)',
funct_keys=(fmsg, handlers))
return await ux_input_text(old_value, confirm_exit=False, max_len=max_len, min_len=min_len,
scan_ok=True, b39_complete=True, prompt='Password',
placeholder='(optional)', funct_keys=(fmsg, handlers))
class NotesMenu(MenuSystem):
readonly = False
@classmethod
def construct(cls):
# Dynamic menu with user-defined names of notes shown
@ -110,25 +125,70 @@ class NotesMenu(MenuSystem):
MenuItem('New Password', f=cls.new_note, arg='p'),
ShortcutItem(KEY_QR, f=cls.quick_create)]
if not NoteContent.count():
cnt = NoteContent.count()
if not cnt:
rv = news + [ MenuItem('Disable Feature', f=cls.disable_notes) ]
else:
rv = []
for note in NoteContent.get_all():
rv.append(MenuItem('%d: %s' % (note.idx+1, note.title), menu=note.make_menu))
wipe_if_deltamode()
rv = cls.construct_note_items(readonly=False)
rv.extend(news)
rv.append(MenuItem('Export All', f=cls.export_all))
if cnt >= 2:
rv.append(MenuItem('Sort By Title', f=cls.sort_titles))
rv.append(MenuItem('Import', f=import_from_other))
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
async def export_all(cls, *a):
await start_export(NoteContent.get_all())
@classmethod
async def sort_titles(cls, menu, _, item):
# sort by title, one time and then reconstruct menu
NoteContent.sort_all()
# force redraw
menu.update_contents()
@classmethod
async def quick_create(cls, menu, _, item):
# using QR, created a Note (never a password) with auto-generated title.
@ -145,7 +205,7 @@ class NotesMenu(MenuSystem):
if got.startswith('otpauth://totp/'):
# see <https://github.com/google/google-authenticator/wiki/Key-Uri-Format>
tmp.title = url_decode(got[15:]).split('?', 1)[0]
tmp.title = url_unquote(got[15:]).split('?', 1)[0]
elif got.startswith('otpauth-migration://offline'):
# see <https://github.com/qistoph/otp_export>
tmp.title = 'Google Auth'
@ -159,7 +219,6 @@ class NotesMenu(MenuSystem):
await tmp._save_ux(menu)
await cls.drill_to(menu, tmp)
def update_contents(self):
# Reconstruct the list of notes on this dynamic menu, because
# we added or changed them and are showing that same menu again.
@ -169,7 +228,8 @@ class NotesMenu(MenuSystem):
@classmethod
async def disable_notes(cls, *a):
# 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('notes')
settings.save()
@ -187,9 +247,27 @@ class NotesMenu(MenuSystem):
@classmethod
async def drill_to(cls, menu, item):
# make it so looks like we drilled down into the new note
menu.goto_idx(item.idx)
m = MenuSystem(await item.make_menu())
the_ux.push(m)
label = '%d: %s' % (item.idx+1, item.title)
group = item.group
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:
@ -206,9 +284,15 @@ class NoteContentBase:
return PasswordContent(j, idx) if 'user' in j else NoteContent(j, idx)
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
def get_all(cls):
@ -223,6 +307,26 @@ class NoteContentBase:
# how many do we have?
return len(settings.get('notes', []))
@classmethod
def sort_all(cls):
# sort and resave all notes based on title
# - careful: self.idx values will be wrong for any existing instances
# - 'title' is only common field to subclasses
notes = cls.get_all()
notes.sort(key=lambda j: j.title.lower())
settings.put('notes', [n.serialize() for n in notes])
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):
# Remove note
ok = await ux_confirm("Everything about this note/password will be lost.")
@ -243,10 +347,15 @@ class NoteContentBase:
the_ux.pop()
m = the_ux.top_of_stack()
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)
async def share_nfc(self, menu, _, item):
async def share_nfc(self, a, b, item):
# share something via NFC -- if small enough and enabled
from glob import NFC
@ -256,16 +365,35 @@ class NoteContentBase:
if len(v) < 8000: # see MAX_NFC_SIZE
await NFC.share_text(v)
async def view_qr(self, k):
# full screen QR
try:
await show_qr_code(getattr(self, k), msg=self.title, is_secret=True)
except Exception as exc:
# - not all data can be a QR (non-text, binary, zeros)
# - might be too big for single QR
# - may be a RuntimeError(n) where n is line number inside uqr
await ux_show_story("Unable to display as QR.\n\nError: " + str(exc))
async def view_qr_menu(self, a, b, item):
await self.view_qr(item.arg)
async def _save_ux(self, menu):
is_new = self.save()
if not is_new:
# change our own menu contents
menu.replace_items(await self.make_menu())
mi = await self._make_menu()
menu.replace_items(mi)
# update parent
parent = the_ux.parent_of(menu)
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:
menu.update_contents()
@ -289,28 +417,137 @@ class NoteContentBase:
# single export
await start_export([self])
async def sign_txt_msg(self, a, b, item):
from msgsign import ux_sign_msg, msg_signing_done
txt = item.arg
await ux_sign_msg(txt, approved_cb=msg_signing_done, kill_menu=False)
def sign_misc_menu_item(self):
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):
# "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'
async def make_menu(self, *a):
async def _make_menu(self, readonly=False):
rv = [MenuItem('"%s"' % self.title, f=self.view)]
if self.user:
rv.append(MenuItem('%s' % self.user, f=self.view))
if self.site:
rv.append(MenuItem('%s' % self.site, f=self.view))
#if self.misc: rv.append(MenuItem('↳ (notes)', f=self.view))
return rv + [
# if self.misc: rv.append(MenuItem('↳ (notes)', f=self.view))
rv += [
MenuItem('View Password', f=self.view_pw),
MenuItem('Send Password', f=self.send_pw, predicate=lambda: settings.get('du', True)),
MenuItem('Export', f=self.export),
MenuItem('Edit Metadata', f=self.edit),
MenuItem('Delete', f=self.delete),
MenuItem('Change Password', f=self.change_pw),
ShortcutItem(KEY_QR, f=self.view_qr),
ShortcutItem(KEY_NFC, f=self.share_nfc, arg='password'),
MenuItem('Send Password', f=self.send_pw, predicate=lambda: not settings.get('du', 0)),
]
if not readonly:
rv += [
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(),
ShortcutItem(KEY_QR, f=self.view_qr_menu, 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):
pl = len(self.password)
@ -350,7 +587,7 @@ class PasswordContent(NoteContentBase):
ch = await ux_show_story(msg, title=self.title, escape=KEY_QR,
hint_icons=KEY_QR)
if ch == KEY_QR:
await self.view_qr()
await self.view_qr(self.type_label)
async def send_pw(self, *a):
# use USB to send it -- weak at present
@ -362,10 +599,6 @@ class PasswordContent(NoteContentBase):
"we cannot type at this time.")
await single_send_keystrokes(self.password)
async def view_qr(self, *a):
# full screen QR
await show_qr_code(self.password, msg=self.title)
async def edit(self, menu, _, item):
# Edit, also used for add new
@ -383,7 +616,8 @@ class PasswordContent(NoteContentBase):
if self.idx == -1:
# 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,
prompt='Website', placeholder='(optional)')
@ -395,6 +629,8 @@ class PasswordContent(NoteContentBase):
if misc is None:
misc = self.misc
group = await GroupPickerMenu.pick(self.group)
if self.idx != -1:
# confirm changes, don't for new records
chgs = []
@ -406,6 +642,8 @@ class PasswordContent(NoteContentBase):
chgs.append('Username')
if self.misc != misc:
chgs.append('Other Notes')
if self.group != group:
chgs.append('Group')
if not chgs:
await ux_dramatic_pause('No changes.', 3)
@ -419,6 +657,7 @@ class PasswordContent(NoteContentBase):
self.user = user
self.site = site
self.misc = misc
self.group = group
await self._save_ux(menu)
return self
@ -426,36 +665,46 @@ class PasswordContent(NoteContentBase):
class NoteContent(NoteContentBase):
# Pure "notes" have just a title and free-form text
flds = ['title', 'misc']
flds = ['title', 'misc', 'group']
type_label = 'note'
async def make_menu(self, *a):
async def _make_menu(self, readonly=False):
# Details and actions for this Note
return [
rv = [
MenuItem('"%s"' % self.title, f=self.view),
MenuItem('View Note', f=self.view),
MenuItem('Edit', f=self.edit),
MenuItem('Delete', f=self.delete),
MenuItem('Export', f=self.export),
ShortcutItem(KEY_QR, f=self.view_qr),
]
if not readonly:
rv += [
MenuItem('Edit', f=self.edit),
MenuItem('Delete', f=self.delete),
MenuItem('Export', f=self.export),
]
rv += [
self.sign_misc_menu_item(),
ShortcutItem(KEY_QR, f=self.view_qr_menu, 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):
ch = await ux_show_story(self.misc, title=self.title, escape=KEY_QR,
hint_icons=KEY_QR)
if ch == KEY_QR:
await self.view_qr()
async def view_qr(self, *a):
# full screen QR
try:
await show_qr_code(self.misc, msg=self.title)
except Exception as exc:
# - not all data can be a QR (non-text, binary, zeros)
# - might be too big for single QR
# - may be a RuntimeError(n) where n is line number inside uqr
await ux_show_story("Unable to display as QR.\n\nError: "+str(exc))
await self.view_qr("misc")
async def edit(self, menu, _, item):
# Edit, also used for add new
@ -471,6 +720,8 @@ class NoteContent(NoteContentBase):
if misc is None:
misc = self.misc
group = await GroupPickerMenu.pick(self.group)
if self.idx != -1:
# confirm changes, don't for new records
chgs = []
@ -478,6 +729,8 @@ class NoteContent(NoteContentBase):
chgs.append('Title')
if self.misc != misc:
chgs.append('Note Text')
if self.group != group:
chgs.append('Group')
if not chgs:
await ux_dramatic_pause('No changes.', 3)
@ -490,6 +743,7 @@ class NoteContent(NoteContentBase):
self.title = title
self.misc = misc
self.group = group
await self._save_ux(menu)
@ -498,16 +752,16 @@ class NoteContent(NoteContentBase):
async def start_export(notes):
# Save out notes/passwords
from glob import NFC
from auth import write_sig_file
from msgsign import write_sig_file
import ujson as json
from ux_q1 import show_bbqr_codes
singular = (len(notes) == 1)
item = notes[0].type_label if singular else 'all notes & passwords'
choice = await import_export_prompt(item, is_import=False, title="Data Export", no_nfc=True,
footnotes="\n\nWARNING: No encryption happens here. "
"Your secrets will be cleartext.")
choice = await import_export_prompt(item, title="Data Export", no_nfc=True,
footnotes="WARNING: No encryption happens here."
" Your secrets will be cleartext.")
if choice == KEY_CANCEL:
return
@ -536,7 +790,7 @@ async def start_export(notes):
await needs_microsd()
return
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
msg = 'Export file written:\n\n%s\n\nSignature file written:\n\n%s' % (
@ -565,14 +819,11 @@ async def import_from_other(menu, *a):
else:
def contains_json(fname):
if not fname.endswith('.json'): return False
print(fname)
try:
obj = json.load(open(fname, 'rt'))
assert 'coldcard_notes' in obj
return True
except Exception as exc:
import sys; sys.print_exception(exc)
pass
except: pass
fn = await file_picker(min_size=8, max_size=100000, taster=contains_json, **choice)
if not fn: return
@ -581,7 +832,14 @@ async def import_from_other(menu, *a):
records = json.load(open(fn, 'rt'))
# We have some JSON, parsed now.
# - should dedup, but we aren't
ok = await import_from_json(records)
if not ok: return
await ux_dramatic_pause('Saved.', 3)
menu.update_contents()
async def import_from_json(records):
# should dedup, but we aren't
try:
assert 'coldcard_notes' in records, 'Incorrect format'
@ -591,14 +849,12 @@ async def import_from_other(menu, *a):
was = list(settings.get('notes', []))
was.extend(new)
settings.put('notes', was)
settings.set('notes', was)
settings.set('secnap', True)
settings.save()
return True
except Exception as e:
await ux_show_story(title="Failure", msg=str(e) + '\n\n' + problem_file_line(e))
await ux_dramatic_pause('Saved.', 3)
menu.update_contents()
# EOF

View File

@ -8,13 +8,13 @@
# - recover from empty/blank/failed chips w/o user action
#
# Result:
# - up to 4k of values supported (after json encoding)
# - encrypted and stored in SPI flash, in last 128k area
# - up to a few k of values supported (after json encoding)
# - encrypted and stored in main flash, in a dedicated 512k area
# - AES encryption key is derived from actual wallet secret
# - if logged out, then use fixed key instead (ie. it's public)
# - you cannot move data between slots because AES-CTR with CTR seed based on slot #
# - SHA-256 check on decrypted data
# - (Mk4) each slot is a file on /flash/settings
# - each "slot" is a file in /flash/settings; in Mk1-3 was SPI flash block
# - os.sync() not helpful because block device under filesystem doesnt implement it
#
import os, ujson, ustruct, ckcc, gc, ngu, aes256ctr, version
@ -56,6 +56,7 @@ from utils import call_later_ms
# seedvault = (bool) opt-in enable seed vault feature
# seeds = list of stored secrets for seedvault feature
# bright = (int:0-255) LCD brightness when on battery
# secnap = (bool) opt-in enable Secure Notes & Passwords feature
# notes = (complex) Secure notes held for user, see notes.py
# accts = (list of tuples: (addr_fmt, account#)) Single-sig wallets we've seen them use
# aei = (bool) allow changing start index in Address Explorer
@ -63,6 +64,12 @@ from utils import call_later_ms
# ptxurl = (str) URL for PushTx feature, clear to disable feature
# hmx = (bool) Force display of current XFP in home menu, even w/o tmp seed active
# unsort_ms = (bool) Allow unsorted multisig with BIP-67 disabled
# msas = multisig address show (do not censor multisig addresses)
# 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
# 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
# _skip_pin = hard code a PIN value (dangerous, only for debug)
@ -82,10 +89,13 @@ from utils import call_later_ms
# prelogin settings - do not need to be part of other saved settings
# PRELOGIN_SETTINGS = ["_skip_pin", "nick", "rngk", "lgto", "kbtn", "terms_ok"]
# keep these settings only if unspecified on the other end
KEEP_IF_BLANK_SETTINGS = ["bkpw", "wa", "sighshchk", "emu", "rz", "b39skip",
"axskip", "del", "pms", "idle_to", "batt_to", "bright"]
KEEP_IF_BLANK_SETTINGS = ["wa", "sighshchk", "emu", "rz", "b39skip",
"axskip", "del", "pms", "idle_to", "batt_to",
"bright", "msas"]
SEEDVAULT_FIELDS = ['seeds', 'seedvault', 'xfp', 'words']
# 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)
SLOTS = range(NUM_SLOTS)
@ -175,6 +185,13 @@ class SettingsObject:
return (blocks-bfree) / blocks
def _open_file(self, pos, mode='rb'):
if 'w' in mode:
# make directory, when needed (recovery/robustness)
try:
os.stat(MK4_WORKDIR)
except OSError: # ENOENT
os.mkdir(MK4_WORKDIR[:-1])
return open(MK4_FILENAME(pos), mode)
def _slot_is_blank(self, pos, buf):
@ -191,13 +208,13 @@ class SettingsObject:
fn = MK4_FILENAME(pos)
try:
os.remove(fn)
except Exception:
# Error (ENOENT) expected here when saving first time, because the
except:
# OSError (ENOENT) expected here when saving first time, because the
# "old" slot was not in use
pass
def _read_slot(self, pos, decryptor):
# Mk4 is just reading a binary file and decrypt as we go.
# read a binary file and decrypt as we go.
with self._open_file(pos) as fd:
# missing ftell(), so emulate
ln = fd.seek(0, 2)
@ -242,9 +259,12 @@ class SettingsObject:
fd.write(aes(chk.digest()))
def _used_slots(self):
# mk4: faster list of slots in use; doesn't open them
files = os.listdir(MK4_WORKDIR)
return [int(fn[0:-4], 16) for fn in files if fn.endswith('.aes')]
# list of slots in use; doesn't open them
try:
files = os.listdir(MK4_WORKDIR)
return [int(fn[0:-4], 16) for fn in files if fn.endswith('.aes')]
except:
return []
def _nonempty_slots(self, dis=None):
# generate slots that are non-empty
@ -265,10 +285,11 @@ class SettingsObject:
def leaving_master_seed(self):
# going from master seed to a tmp seed, so capture a few values we need.
self.save_if_dirty()
SettingsObject.master_nvram_key = self.nvram_key
for fn in SEEDVAULT_FIELDS:
for fn in MASTER_FIELDS:
curr = self.current.get(fn, None)
if curr is not None:
SettingsObject.master_sv_data[fn] = curr
@ -284,7 +305,7 @@ class SettingsObject:
SettingsObject.master_sv_data.clear()
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
# Concern is we may be changing a setting from a tmp seed mode
# - always does a save
@ -295,6 +316,7 @@ class SettingsObject:
self.set(key, value)
self.save()
else:
assert not master_only
# harder, slower: have to load, change and write
master = SettingsObject(nvram_key=SettingsObject.master_nvram_key)
master.load()
@ -303,7 +325,7 @@ class SettingsObject:
del master
# track our copies
if key in SEEDVAULT_FIELDS:
if key in MASTER_FIELDS:
SettingsObject.master_sv_data[key] = value
def master_get(self, kn, default=None):
@ -315,7 +337,7 @@ class SettingsObject:
return self.get(kn, default)
# 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)
if res is None:
return default
@ -391,8 +413,9 @@ class SettingsObject:
set = put
def remove_key(self, kn):
self.current.pop(kn, None)
self.changed()
if kn in self.current:
self.current.pop(kn, None)
self.changed()
def merge_previous_active(self, previous):
import pyb
@ -400,7 +423,7 @@ class SettingsObject:
if previous:
for k in KEEP_IF_BLANK_SETTINGS:
if k in previous and k not in self.current:
if (k in previous) and (k not in self.current):
self.current[k] = previous[k]
# nfc, usb, vidsk handling
@ -450,11 +473,8 @@ class SettingsObject:
call_later_ms(250, self.write_out)
def find_spot(self, not_here=0):
# search for a blank sector to use
# - check randomly and pick first blank one (wear leveling, deniability)
# - we will write and then erase old slot
# search for a blank slot to use
# - if "full", blow away a random one
# on mk4, use the filesystem to see what's already taken
avail = set(SLOTS) - set(self._used_slots())
avail.discard(not_here)

View File

@ -2,16 +2,18 @@
#
# 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 ucollections import namedtuple
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from exceptions import UnknownAddressExplained
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
# - map from random Bech32/Base58 payment address to (wallet) + keypath
# - only normal (external, not change) addresses, and won't consider
# any keypath that does not end in 0/*
# - won't consider any keypath that does not end in <0;1>/*
# - 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
# - storing bulk data into LFS, not settings
@ -38,7 +40,7 @@ OWNERSHIP_MAGIC = 0x10A0 # "Address Ownership" v1.0
# target 3 flash blocks, max file size => 764 addresses
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):
# Convert text address to something we can store while preserving privacy.
@ -55,6 +57,7 @@ class AddressCacheFile:
self.salt = h[32:]
self.count = 0
self.hdr = None
self.fd = None
self.peek()
@ -64,9 +67,6 @@ class AddressCacheFile:
rv += ' (change)'
return rv
def exists(self):
return bool(self.count)
def peek(self):
# see what we have on-disk; just reads header.
try:
@ -80,7 +80,7 @@ class AddressCacheFile:
except OSError:
return
except Exception as exc:
sys.print_exception(exc)
# sys.print_exception(exc)
self.count = 0
self.hdr = None
return
@ -104,15 +104,14 @@ class AddressCacheFile:
self.fd.write(hdr)
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))
def close(self):
# close file, done
if self.fd is not None:
self.fd.close()
self.fd = None
def fast_search(self, addr):
# Do the easy part of the searching, using the existing file's contents.
# - generates candidate path subcomponents; might be false positive
@ -120,6 +119,7 @@ class AddressCacheFile:
from glob import dis
if not self.hdr or not self.count:
# cache empty
return
with open(self.fname, 'rb') as fd:
@ -131,7 +131,7 @@ class AddressCacheFile:
chk = encode_addr(addr, self.salt)
for idx in range(self.count):
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)
@ -147,93 +147,106 @@ class AddressCacheFile:
# - return subpath for a hit or None
from glob import dis
bonus = 0
match = None
start_idx = self.count
count = MAX_ADDRS_STORED - start_idx
if count <= 0:
return None
return match
self.setup(self.change_idx, start_idx)
bonus = None
for idx,here,*_ in self.wallet.yield_addresses(start_idx, count,
change_idx=self.change_idx):
if here == addr:
# Found it! But keep going a little for next time.
match = (self.change_idx, idx)
change_idx=self.change_idx):
self.append(here)
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
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:
@classmethod
def saver(cls, wallet, change_idx, start_idx):
# when we are generating many addresses for export, capture them
def saver(cls, wallet, change_idx, start_idx, count):
# when we are generating many addresses for export, capture them (if suitable)
# 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)
current_pos = file.count
if file.exists():
# don't save to existing file, has some already
return None
if start_idx > current_pos:
# nothing to do here, we are missing some addresses in the middle
return
if (start_idx + count) <= current_pos:
# we already have all these addresses
return
try:
file.setup(change_idx, start_idx)
except:
# in some cases we don't want to save anything, not an error
return None
file.setup(change_idx, current_pos)
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
def search(cls, addr):
# Find it!
# - returns wallet object, and tuple2 of final 2 subpath components
def filter(cls, addr_fmt, args):
# Filter possible candidates!
# - if you start w/ testnet, we'll follow that
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)
if not addr_fmt:
# might be valid address over on testnet vs mainnet
nm = ch.name if ch.ctype != 'BTC' else 'Bitcoin Mainnet'
raise UnknownAddressExplained('That address is not valid on ' + nm)
# user has specified specific (named) wallet
named_wal = args.get("wallet", None)
if named_wal:
# 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 = []
if addr_fmt & AFC_SCRIPT:
# multisig or script at least.. must exist already
possibles.extend(MultisigWallet.iter_wallets(addr_fmt=addr_fmt))
# multisig or script at least... must exist already
afs = [addr_fmt]
if addr_fmt == AF_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
# thing that hopefully is going away, so if they have any multisig wallets,
# defined, assume that that's the only p2sh address source.
addr_fmt = AF_P2WPKH_P2SH
# TODO: add tapscript and such fancy stuff here
possibles.extend(MultisigWallet.iter_wallets(addr_fmts=afs))
try:
# Construct possible single-signer wallets, always at least account=0 case
@ -247,89 +260,145 @@ class OwnershipCache:
if af == addr_fmt and acct_num:
w = MasterSingleSigWallet(addr_fmt, account_idx=acct_num)
possibles.append(w)
except ValueError: pass # if not single sig address format
except (KeyError, ValueError):
pass # if not single sig address format
if not possibles:
# can only happen w/ scripts; for single-signer we have things to check
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
# 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
# - 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
# - 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
for f in phase2:
b4 = f.count
if dis.has_lcd:
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
result = cf.build_and_search(addr)
if result:
# found it, so report it and stop
return cf.wallet, result
# possible phase 3: other seedvault... slow, rare and not implemented
raise UnknownAddressExplained('Searched %d candidates without finding a match.' % count)
return None, None
@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.
from ux import ux_show_story, show_qr_code
from charcodes import KEY_QR
from multisig import MultisigWallet
from public_constants import AFC_BECH32, AFC_BECH32M
try:
wallet, subpath = OWNERSHIP.search(addr)
msg = addr
msg += '\n\nFound in wallet:\n ' + wallet.name
msg += '\nDerivation path:\n ' + wallet.render_path(*subpath)
if version.has_qwerty:
esc = KEY_QR
_, wallet, subpath = cls.search(addr, args)
is_ms = isinstance(wallet, MultisigWallet)
msg = show_single_address(addr)
esc = ""
if isinstance(wallet, tuple) and (wallet[0] == "wif"):
msg += '\n\nFound in WIF store at index %d' % subpath
addr_fmt = wallet[1]
else:
msg += '\n\nPress (1) for QR'
esc = '1'
sp = wallet.render_path(*subpath)
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"
if version.has_qwerty:
esc += KEY_QR
title += " Address"
else:
msg += ' Press (1) for address QR.'
esc += '1'
title += "!"
while 1:
ch = await ux_show_story(msg, title="Verified Address",
escape=esc, hint_icons=KEY_QR)
if ch != esc: break
await show_qr_code(addr, is_alnum=(wallet.addr_fmt & (AFC_BECH32 | AFC_BECH32M)),
msg=addr)
ch = await ux_show_story(msg, title=title, escape=esc, hint_icons=KEY_QR)
if ch in ("1"+KEY_QR):
await show_qr_code(addr, msg=addr, is_addrs=True,
is_alnum=(addr_fmt & (AFC_BECH32 | AFC_BECH32M)))
elif not is_ms and (ch == "0"): # only singlesig
from msgsign import sign_with_own_address
await sign_with_own_address(sp, addr_fmt)
else:
break
except UnknownAddressExplained as exc:
await ux_show_story(addr + '\n\n' + str(exc), title="Unknown Address")
await ux_show_story(show_single_address(addr) + '\n\n' + str(exc), title="Unknown Address")
except Exception as e:
await ux_show_story('Ownership search failed.\n\n%s\n%s' % (e, problem_file_line(e)))
@classmethod
def note_subpath_used(cls, subpath):
@ -363,8 +432,6 @@ class OwnershipCache:
# - if they explore it (non-zero subaccount)
# - if they sign those paths
# - but ignore testnet vs. not
from glob import settings
if subaccount == 0:
# only interested in non-zero subaccounts
return

View File

@ -83,7 +83,7 @@ class PaperWalletMaker:
try:
import ngu
from auth import write_sig_file
from msgsign import write_sig_file
from chains import current_chain
from serializations import hash160
from stash import blank_object
@ -179,7 +179,7 @@ class PaperWalletMaker:
return
except Exception as e:
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
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)
#
import ustruct, ckcc, version, chains, stash
# from ubinascii import hexlify as b2a_hex
from callgate import enter_dfu
from callgate import enter_dfu, get_is_bricked
from bip39 import wordlist_en
# 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.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 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
# 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)
#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_V2_ADDITIONS) \
# == PIN_ATTEMPT_SIZE - PIN_ATTEMPT_SIZE_V1
def __repr__(self):
return '<PinAttempt: fails/left=%d/%d tc_flag/arg=0x%x/0x%x>' % (
@ -177,7 +173,7 @@ class PinAttempt:
old_pin = self.pin
assert len(new_pin) <= MAX_PIN_LEN
assert old_pin != None
assert old_pin is not None
assert len(old_pin) <= MAX_PIN_LEN
else:
new_pin = b''
@ -339,10 +335,6 @@ class PinAttempt:
return self.state_flags
def delay(self):
# obsolete since Mk3, but called from login.py
self.roundtrip(1)
def login(self):
# test we have the PIN code right, and unlock access if so.
chk = self.roundtrip(2)
@ -473,6 +465,7 @@ class PinAttempt:
def tmp_secret(self, encoded, chain=None, bip39pw=''):
# Use indicated secret and stop using the SE; operate like this until reboot
from glob import settings
from utils import xfp2str
from nvstore import SettingsObject
val = bytes(encoded + bytes(AE_SECRET_LEN - len(encoded)))
@ -483,7 +476,9 @@ class PinAttempt:
target_nvram_key = None
if encoded is not None:
# disallow using master seed as temporary
master_err = "Cannot use master seed as temporary."
xfp = xfp2str(settings.master_get("xfp", 0))
master_err = ("Cannot use master seed as temporary. BUT you have just successfully "
"tested recovery of your master seed [%s].") % xfp
target_nvram_key = settings.hash_key(val)
if SettingsObject.master_nvram_key:
assert self.tmp_value
@ -530,10 +525,24 @@ class PinAttempt:
from trick_pins import TC_DELTA_MODE
return bool(self.delay_required & TC_DELTA_MODE)
def get_tc_values(self):
# Mk4 only
# return (tc_flags, tc_arg)
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

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]
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...
def read(self, address, buf, cmd=None):

View File

@ -7,6 +7,7 @@ from files import CardSlot, CardMissingError, needs_microsd
from ux import ux_dramatic_pause, ux_confirm, ux_show_story, OK, X
from utils import xfp2str, problem_file_line, B2A
from menu import MenuItem, MenuSystem
from glob import settings
class PassphraseSaver:
@ -110,7 +111,6 @@ class PassphraseSaverMenu(MenuSystem):
from ux import ux_show_story
from seed import set_bip39_passphrase
from pincodes import pa
from glob import settings
bypass_tmp = True
pw, expect_xfp = item.arg
@ -253,7 +253,6 @@ class MicroSD2FA(PassphraseSaver):
@classmethod
def get_nonces(cls):
# this is the only setting: list of nonce values we have saved to various cards
from glob import settings
return settings.get('sd2fa') or []
def read_card(self):
@ -288,7 +287,6 @@ class MicroSD2FA(PassphraseSaver):
except:
# die. wrong
import callgate
from glob import settings
settings.remove_key("sd2fa")
settings.save()
callgate.fast_wipe(silent=False)
@ -353,8 +351,6 @@ class MicroSD2FA(PassphraseSaver):
async def remove(self, nonce):
# remove indicated nonce from records
# - doesn't delete file, since might not have card anymore and useless w/o nonce
from glob import settings
v = self.get_nonces()
assert nonce in v, 'missing card nonce'
v2 = [i for i in v if i != nonce]

View File

@ -3,11 +3,11 @@
# qrs.py - QR Display related UX
#
import framebuf, uqr
from ux import UserInteraction, ux_wait_keyup, the_ux
from charcodes import (KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_NFC,
KEY_END, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_ENTER, KEY_CANCEL)
from ux import UserInteraction, ux_wait_keyup, the_ux
from version import has_qwerty
from exceptions import QRTooBigError
from charcodes import (KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_NFC,
KEY_END, KEY_ENTER, KEY_CANCEL)
# TODO: This class has a terrible API!
@ -17,15 +17,26 @@ MAX_V11_CHAR_LIMIT = const(321)
class QRDisplaySingle(UserInteraction):
# Show a single QR code for (typically) a list of addresses, or a single value.
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,
change_idxs=None, can_raise=True, qr_msgs=None, no_index=None):
self.is_alnum = is_alnum
self.idx = 0 # start with first address
self.invert = False # looks better, but neither mode is ideal
self.addrs = addrs
self.sidebar = sidebar
self.start_n = start_n
self.is_addrs = is_addrs
self.msg = msg
self.qr_data = None
self.force_msg = force_msg
self.allow_nfc = allow_nfc
# only used for NFC sharing secret material - full chip wipe if is_secret=True
self.is_secret = is_secret
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):
# Version 2 would be nice, but can't hold what we need, even at min error correction,
@ -54,8 +65,21 @@ class QRDisplaySingle(UserInteraction):
# draw_qr_display takes this and renders hint in the top right corner
# this member function decides what type of hint will be shown
# numbers, letters, etc.
if self.no_index:
return None
return str(self.start_n + self.idx) if len(self.addrs) > 1 else None
def side_msg(self):
if self.idx in self.change_idxs:
return "CHANGE BACK"
elif self.qr_msgs:
try:
return self.qr_msgs[self.idx]
except IndexError: pass
return None
def redraw(self):
# Redraw screen.
from glob import dis
@ -63,17 +87,36 @@ class QRDisplaySingle(UserInteraction):
# what we are showing inside the QR
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.
if not self.qr_data:
dis.busy_bar(True)
try:
self.calc_qr(body)
except Exception:
dis.busy_bar(False)
if not self.can_raise:
dis.draw_qr_error(idx_hint, msg)
return
self.calc_qr(body)
# other code paths require raise to switch to BBQr
raise QRTooBigError
# draw display
dis.busy_bar(False)
dis.draw_qr_display(self.qr_data, self.msg or body, self.is_alnum,
self.sidebar, self.idx_hint(), self.invert)
dis.draw_qr_display(self.qr_data, msg, self.is_alnum,
self.sidebar, idx_hint, self.invert,
is_addr=self.is_addrs, force_msg=self.force_msg,
side_msg=self.side_msg())
async def interact_bare(self):
from glob import NFC, dis
@ -88,13 +131,15 @@ class QRDisplaySingle(UserInteraction):
self.redraw()
continue
elif NFC and (ch == '3' or ch == KEY_NFC):
# Share any QR over NFC!
await NFC.share_text(self.addrs[self.idx])
self.redraw()
if not self.allow_nfc:
# not a valid as text over NFC sometimes; treat as cancel
break
else:
# Share any QR over NFC!
await NFC.share_text(self.addrs[self.idx], is_secret=self.is_secret)
self.redraw()
continue
elif ch in 'xy'+KEY_ENTER+KEY_CANCEL:
if dis.has_lcd:
dis.real_clear() # bugfix
break
elif len(self.addrs) == 1:
continue
@ -116,6 +161,10 @@ class QRDisplaySingle(UserInteraction):
self.qr_data = None
self.redraw()
# bugfix
if dis.has_lcd:
dis.real_clear()
async def interact(self):
await self.interact_bare()
the_ux.pop()

View File

@ -72,7 +72,7 @@ class Queue:
return len(self._queue)
def empty(self): # Return True if the queue is empty, False otherwise.
return len(self._queue) == 0
return not self._queue
def full(self): # Return True if there are maxsize items in the queue.
# Note: if the Queue was initialized with maxsize=0 (the default) or

View File

@ -57,9 +57,8 @@ SLOW_BAUD = const(9600)
FAST_BAUD = const(57600)
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
# login we can do full setup (2+ seconds) and then sleep again until needed.
# TODO: constructor should avoid full setup until after login; after setup,
# command sleep is the known low-power state.
class QRScanner:
def __init__(self):
@ -68,6 +67,8 @@ class QRScanner:
self.scan_light = False # is light on during scanning?
self.version = None
self.setup_done = False
self.needs_reinit = False
self.sleep_seq = 0
# hodl this lock when communicating w/ QR scanner
self.lock = asyncio.Lock()
@ -84,16 +85,21 @@ class QRScanner:
# setup hardware, reset scanner and return time to delay until ready
from machine import UART, Pin
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
# 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)
utime.sleep_ms(10)
self.reset(1)
# needs full 2 seconds of recovery time
return 2
self.needs_reinit = False
def set_baud(self, br):
# change serial port baud rate
@ -118,56 +124,104 @@ class QRScanner:
async def setup_task(self, start_delay):
# 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):
baud = await self.probe_baud()
if baud: break
else:
#print("QR Scanner: missing")
return
raise RuntimeError('no contact after S_CMD_FFFF')
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!
if baud != FAST_BAUD:
await self.txrx('S_CMD_H3BR%d' % FAST_BAUD)
self.set_baud(FAST_BAUD)
# configure it like we want it
await self.txrx('S_CMD_MTRS5000') # 5s to read before fail (unused)
await self.txrx('S_CMD_MT11') # trigger is edge-based (not level)
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
await self.txrx('S_CMD_MTRS5000') # 5s to read before fail (unused)
await self.txrx('S_CMD_MT11') # trigger is edge-based (not level)
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
# settings under continuous scan mode
await self.txrx('S_CMD_MARS0000') # "Modify the duration of single code reading" (ms)
await self.txrx('S_CMD_MARR000') # "Modify the time of the reading interval 0ms"
await self.txrx('S_CMD_MA31') # Enable "Same code reading delay"
await self.txrx('S_CMD_MARI0050') # "Modify the same code reading delay 50ms"
# settings under continuous scan mode
await self.txrx('S_CMD_MARS0000') # "Modify the duration of single code reading" (ms)
await self.txrx('S_CMD_MARR000') # "Modify the time of the reading interval 0ms"
await self.txrx('S_CMD_MA31') # Enable "Same code reading delay"
await self.txrx('S_CMD_MARI0050') # "Modify the same code reading delay 50ms"
# these aren't useful (yet?) and just make things harder to decode.
#await self.txrx('S_CMD_05F1') # add all information on
#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
# these aren't useful (yet?) and just make things harder to decode.
#await self.txrx('S_CMD_05F1') # add all information on
#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()
# prevent scanning magic QR to affect settings
await self.txrx('S_CMD_0000') # close setting codes
async def scan_once(self):
# Blocks until something is scanned. Returns it as string
@ -176,6 +230,16 @@ class QRScanner:
# - returns a BBQr object at that point
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)
# - few seconds of boot time needed
for retry in range(10):
@ -201,7 +265,7 @@ class QRScanner:
if not rv: continue
if rv[0:2] == 'B$' and bbqr.collect(rv):
# BBQr protocol detected; collect more data
# BBQr protocol detected, accepted need to collect more data
continue
break
@ -211,19 +275,22 @@ class QRScanner:
finally:
# Problem: another valid scan can come in just as we are trying
# to get out of scanner mode
for retry in range(10):
for retry in range(3):
try:
await self.txrx('S_CMD_020D') # return to "Command mode"
await self.txrx('S_CMD_03L0') # turn off bright light
await self.txrx('S_CMD_020D', timeout=1000) # return to "Command mode"
await self.txrx('S_CMD_03L0', timeout=1000) # turn off bright light
#print('rest after %d retries' % retry)
break
except: pass
await asyncio.sleep_ms(25)
except Exception:
pass
await asyncio.sleep_ms(50)
else:
pass
#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
# return BBQr object or string if simple QR
@ -254,13 +321,14 @@ class QRScanner:
# send specific command until it responds
# - it will wake on any command, but not instant
# - first one seems to fail 100%
self.sleep_seq += 1
await self.tx('SRDF0051') # blindly at first
for retry in range(5):
try:
await self.txrx('SRDF0051', timeout=50) # 50 ok, 20 too short
return
except:
except Exception:
# first try usually fails, that's okay... its asleep and groggy
pass
@ -270,9 +338,13 @@ class QRScanner:
# - need blind retries here
# - 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
self.sleep_seq += 1
sleep_seq = self.sleep_seq
await self.tx('SRDF0050')
async def later():
await asyncio.sleep_ms(150)
if sleep_seq != self.sleep_seq or self.busy_scanning:
return
await self.tx('SRDF0050')
asyncio.create_task(later())
@ -290,6 +362,22 @@ class QRScanner:
#print('tx >> ' + 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):
# Send a command, get the corresponding response.
# - has a long timeout, collects rx based on framing
@ -310,13 +398,8 @@ class QRScanner:
expect = LEN_OKAY
rx = b''
while 1:
try:
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
rx += await self.readexactly_timeout(expect, timeout, msg)
#print('txrx << ' + B2A(rx))

View File

@ -10,29 +10,48 @@
# - 'abandon' * 17 + 'agent'
# - 'abandon' * 11 + 'about'
#
import ngu, uctypes, bip39, random, stash, version
import ngu, uctypes, bip39, random, version
from ucollections import OrderedDict
from menu import MenuItem, MenuSystem
from utils import xfp2str, parse_extended_key, swab32, pad_raw_secret, problem_file_line
from utils import xfp2str, parse_extended_key, swab32
from utils import deserialize_secret, problem_file_line, wipe_if_deltamode
from uhashlib import sha256
from ux import ux_show_story, the_ux, ux_dramatic_pause, ux_confirm, OK, X
from ux import PressRelease, ux_input_numbers, ux_input_text, show_qr_code
from ux import PressRelease, ux_input_text, show_qr_code
from actions import goto_top_menu
from stash import SecretStash, ZeroSecretException
from stash import SecretStash, SensitiveValues
from ubinascii import hexlify as b2a_hex
from pwsave import PassphraseSaver, PassphraseSaverMenu
from glob import settings, dis
from pincodes import pa
from nvstore import SettingsObject
from files import CardMissingError, needs_microsd, CardSlot
from charcodes import KEY_QR, KEY_ENTER, KEY_CANCEL, KEY_CLEAR
from files import CardMissingError, needs_microsd
from charcodes import KEY_QR, KEY_ENTER, KEY_CANCEL, KEY_NFC
from uasyncio import sleep_ms
from ucollections import namedtuple
# seed words lengths we support: 24=>256 bits, and recommended
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"
_PREFIX_MARKER = const(1<<26)
# what we store (in JSON as a tuple) for each seed vault key.
# - 'encoded' is hex, and has is trimmed of right side zeros
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():
# iterate over all seeds in the vault; returns VaultEntry instances.
# raw vault entries are list type when json.loaded from flash
for lst in settings.master_get("seeds", []):
yield VaultEntry(*lst)
def letter_choices(sofar='', depth=0, thres=5):
# make a list of word completions based on indicated prefix
@ -138,23 +157,62 @@ class WordNestMenu(MenuSystem):
done_cb = None
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:
WordNestMenu.target_words = num_words
WordNestMenu.has_checksum = has_checksum
WordNestMenu.words = []
assert done_cb
WordNestMenu.done_cb = done_cb
is_commit = True
if words:
WordNestMenu.words = words
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
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
async def next_menu(self, idx, choice):
@ -215,7 +273,7 @@ class WordNestMenu(MenuSystem):
while isinstance(the_ux.top_of_stack(), cls):
the_ux.pop()
def on_cancel(self):
async def on_cancel(self):
# user pressed cancel on a menu (so he's going upwards)
# - if it's a step where we added to the word list, undo that.
# - but keep them in our system until:
@ -273,9 +331,16 @@ individual words if you wish.''')
async def show_words(words, prompt=None, escape=None, extra='', ephemeral=False):
msg = (prompt or 'Record these %d secret words!\n') % len(words)
from ux import ux_render_words
from glob import NFC
if prompt:
title = None
msg = prompt
else:
m = 'Record these %d secret words!' % len(words)
title, msg = (m, "") if version.has_qwerty else (None, m+"\n")
msg += ux_render_words(words)
msg += '\n\nPlease check and double check your notes.'
@ -283,22 +348,30 @@ async def show_words(words, prompt=None, escape=None, extra='', ephemeral=False)
# user can skip quiz for ephemeral secrets
msg += " There will be a test!"
escape = (escape or '') + '1'
if not version.has_qwerty:
escape = (escape or '') + '1'
extra += 'Press (1) to view as QR Code. '
else:
escape = (escape or '') + KEY_QR
extra += 'Press '+ KEY_QR + ' to view as QR Code. '
title = None
extra += 'Press (1) to view as QR Code'
if NFC:
extra += ", (3) to share via NFC"
escape += "3"
extra += "."
if extra:
msg += '\n\n'
msg += extra
while 1:
ch = await ux_show_story(msg, escape=escape, sensitive=True)
if ch == '1' or ch == KEY_QR:
await show_qr_code(' '.join(w[0:4] for w in words), True)
rv = ' '.join(w[0:4] for w in words)
ch = await ux_show_story(msg, title=title, escape=escape, sensitive=True,
hint_icons=KEY_QR+(KEY_NFC if NFC else ''))
if ch in ('1'+KEY_QR):
await show_qr_code(rv, True, is_secret=True)
continue
if NFC and (ch in "3"+KEY_NFC):
await NFC.share_text(rv, is_secret=True)
continue
break
return ch
@ -411,27 +484,35 @@ async def new_from_dice(nwords):
await commit_new_words(words)
def in_seed_vault(encoded):
# Test if indicated xfp (or currently active XFP) is in the seed vault already.
seeds = settings.master_get("seeds", [])
if seeds:
ss = stash.SecretStash.storage_serialize(encoded)
if ss in [s[1] for s in seeds]:
# Test if indicated secret is in the seed vault already.
hss = None
for rec in seed_vault_iter():
if not hss:
hss = SecretStash.storage_serialize(encoded)
if hss == rec.encoded:
return True
return False
async def add_seed_to_vault(encoded, meta=None):
async def add_seed_to_vault(encoded, origin=None, label=None):
if not settings.master_get("seedvault", False):
# seed vault disabled
# this can be re-enabled by attacker in deltamode
return
if pa.is_secret_blank():
if pa.is_secret_blank() or pa.is_deltamode():
# do not save anything if no SE secret yet
# do not offer any access to SV in deltamode
return
# do not offer to store secrets that are already in vault
if in_seed_vault(encoded):
return
# stay "read only" in hobbled mode
if pa.hobbled_mode:
return
main_xfp = settings.master_get("xfp", 0)
# parse encoded
@ -457,10 +538,9 @@ async def add_seed_to_vault(encoded, meta=None):
return
# Save it into master settings
seeds.append((new_xfp_str,
stash.SecretStash.storage_serialize(encoded),
xfp_ui,
meta))
rec = VaultEntry(xfp=new_xfp_str, encoded=SecretStash.storage_serialize(encoded),
label=(label or xfp_ui), origin=origin)
seeds.append(list(rec))
settings.master_set("seeds", seeds)
@ -469,13 +549,18 @@ async def add_seed_to_vault(encoded, meta=None):
return True
async def set_ephemeral_seed(encoded, chain=None, summarize_ux=True, bip39pw='',
is_restore=False, meta=None):
if not is_restore:
await add_seed_to_vault(encoded, meta=meta)
is_restore=False, origin=None, label=None):
# Capture tmp seed into vault, if so enabled, and regardless apply it as new tmp.
if not is_restore and not_hobbled_mode():
await add_seed_to_vault(encoded, origin=origin, label=label)
dis.fullscreen("Wait...")
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)
if not applied:
@ -484,15 +569,18 @@ async def set_ephemeral_seed(encoded, chain=None, summarize_ux=True, bip39pw='',
xfp = "[" + xfp2str(settings.get("xfp", 0)) + "]"
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
async def set_ephemeral_seed_words(words, meta):
async def set_ephemeral_seed_words(words, origin):
dis.progress_bar_show(0.1)
encoded = seed_words_to_encoded_secret(words)
dis.progress_bar_show(0.5)
await set_ephemeral_seed(encoded, meta=meta)
await set_ephemeral_seed(encoded, origin=origin)
goto_top_menu()
async def ephemeral_seed_generate_from_dice(nwords):
@ -509,7 +597,7 @@ async def ephemeral_seed_generate_from_dice(nwords):
words = await approve_word_list(seed, nwords, ephemeral=True)
if words:
dis.fullscreen("Applying...")
await set_ephemeral_seed_words(words, meta='Dice')
await set_ephemeral_seed_words(words, origin='Dice')
def generate_seed():
# Generate 32 bytes of best-quality high entropy TRNG bytes.
@ -532,7 +620,7 @@ async def make_new_wallet(nwords):
async def ephemeral_seed_import(nwords):
async def import_done_cb(words):
dis.fullscreen("Applying...")
await set_ephemeral_seed_words(words, meta='Imported')
await set_ephemeral_seed_words(words, origin='Imported')
if version.has_qwerty:
from ux_q1 import seed_word_entry
@ -546,17 +634,17 @@ async def ephemeral_seed_generate(nwords):
words = await approve_word_list(seed, nwords, ephemeral=True)
if words:
dis.fullscreen("Applying...")
await set_ephemeral_seed_words(words, meta="TRNG Words")
await set_ephemeral_seed_words(words, origin="TRNG Words")
async def set_seed_extended_key(extended_key):
encoded, chain = xprv_to_encoded_secret(extended_key)
set_seed_value(encoded=encoded, chain=chain)
goto_top_menu(first_time=True)
async def set_ephemeral_seed_extended_key(extended_key, meta=None):
async def set_ephemeral_seed_extended_key(extended_key, origin=None):
encoded, chain = xprv_to_encoded_secret(extended_key)
dis.fullscreen("Applying...")
await set_ephemeral_seed(encoded=encoded, chain=chain, meta=meta)
await set_ephemeral_seed(encoded=encoded, chain=chain, origin=origin)
goto_top_menu()
async def approve_word_list(seed, nwords, ephemeral=False):
@ -634,8 +722,8 @@ def xprv_to_encoded_secret(xprv):
def set_seed_value(words=None, encoded=None, chain=None):
# Save the seed words (or other encoded private key) into secure element,
# and reboot. BIP-39 passphrase is not set at this point (empty string).
# Save the seed words (or other encoded private key) into secure element.
# BIP-39 passphrase is not set at this point (empty string).
if words:
nv = seed_words_to_encoded_secret(words)
else:
@ -659,14 +747,14 @@ def set_seed_value(words=None, encoded=None, chain=None):
async def calc_bip39_passphrase(pw, bypass_tmp=False):
# Returns (new) encoded secret, new xfp, old xfp
from glob import dis, settings
from pincodes import pa
dis.fullscreen("Working...")
current_xfp = settings.get("xfp", 0)
with stash.SensitiveValues(bip39pw=pw, bypass_tmp=bypass_tmp) as sv:
with SensitiveValues(bip39pw=pw, bypass_tmp=bypass_tmp) as sv:
# can't do it without original seed words (late, but caller has checked)
assert sv.mode == 'words', sv.mode
nv = SecretStash.encode(xprv=sv.node)
@ -676,14 +764,13 @@ async def calc_bip39_passphrase(pw, bypass_tmp=False):
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)
ret = await set_ephemeral_seed(nv, summarize_ux=summarize_ux, bip39pw=pw,
meta="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,
# altho if they have already picked a shared session key, no need, and
# would only affect MitM test, which has already been done.
ret = await set_ephemeral_seed(nv, summarize_ux=summarize_ux, bip39pw=pw,
origin="BIP-39 Passphrase on [%s]" % xfp2str(parent_xfp))
dis.draw_status(bip39=int(bool(pw)), xfp=xfp, tmp=1)
return ret
async def remember_ephemeral_seed():
# Compute current xprv and switch to using that as root secret.
@ -707,7 +794,7 @@ async def remember_ephemeral_seed():
# address cache, settings from tmp seeds / seedvault seeds
# rebuild fs as we want to save current tmp settings immediately
from files import wipe_flash_filesystem
wipe_flash_filesystem(True)
wipe_flash_filesystem()
dis.draw_status(bip39=0, tmp=0)
dis.fullscreen('Saving...')
@ -738,12 +825,6 @@ def clear_seed():
callgate.fast_wipe(True)
# 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?'):
# Perform a test, to check they wrote them down
# Return X if they cancel early.
@ -818,7 +899,7 @@ class SeedVaultMenu(MenuSystem):
from glob import dis
dis.fullscreen("Applying...")
xfp, encoded = item.arg
encoded = item.arg # 72 bytes binary
await set_ephemeral_seed(encoded, is_restore=True)
@ -828,42 +909,40 @@ class SeedVaultMenu(MenuSystem):
async def _remove(menu, label, item):
from glob import dis, settings
idx, xfp_str, encoded = item.arg
esc = ""
tmp_val = False
idx, rec, encoded = item.arg
current_active = (pa.tmp_value == bytes(encoded))
msg = ("Remove seed from seed vault and delete its "
"settings?\n\nPress %s to continue, press (1) to "
"only remove from seed vault and keep "
"encrypted settings for later use.\n\n"
"WARNING: Funds will be lost if wallet is"
" not backed-up elsewhere.") % OK
msg = "Remove seed from seed vault"
if pa.tmp_value and current_active:
tmp_val = True
msg += "?\n\n"
else:
msg += (" and delete its settings?\n\n"
"Press %s to continue, press (1) to "
"only remove from seed vault and keep "
"encrypted settings for later use.\n\n") % OK
esc += "1"
ch = await ux_show_story(title="[" + xfp_str + "]", msg=msg, escape="1")
msg += "WARNING: Funds will be lost if wallet is not backed-up elsewhere."
ch = await ux_show_story(title="[" + rec.xfp + "]", msg=msg, escape=esc)
if ch == "x": return
assert not_hobbled_mode()
dis.fullscreen("Saving...")
wipe_slot = (ch != "1")
tmp_val = False
if pa.tmp_value:
tmp_val = True
wipe_slot = not current_active and (ch != "1")
if wipe_slot:
# are we deleting current active ephemeral wallet
# and its settings ?
# slot wiping
if tmp_val:
# wipe current settings
settings.blank()
pa.tmp_value = False
settings.return_to_master_seed()
else:
# in main settings
xs = SettingsObject()
xs.set_key(encoded)
xs.load()
xs.blank()
del xs
xs = SettingsObject()
xs.set_key(encoded)
xs.load()
xs.blank()
del xs
# CAUTION: will get shadow copy if in tmp seed mode already
seeds = settings.master_get("seeds", [])
@ -885,13 +964,13 @@ class SeedVaultMenu(MenuSystem):
@staticmethod
async def _detail(menu, label, item):
xfp_str, encoded, name, meta = item.arg
rec, encoded = item.arg
# - first byte represents type of secret (internal encoding flag)
# - first byte represents type of secret (internal encoding flags)
txt = SecretStash.summary(encoded[0])
detail = "Name:\n%s\n\nMaster XFP:\n%s\n\nOrigin:\n%s\n\nSecret Type:\n%s" \
% (name, xfp_str, meta, txt)
detail = "Name:\n%s\n\nMaster XFP: %s\nSecret Type: %s\n\nOrigin:\n%s\n\n" \
% (rec.label, rec.xfp, txt, rec.origin)
await ux_show_story(detail)
@ -901,30 +980,30 @@ class SeedVaultMenu(MenuSystem):
from glob import dis
from ux import ux_input_text
idx, xfp_str = item.arg
assert not_hobbled_mode()
seeds = settings.master_get("seeds", [])
chk_xfp, encoded, old_name, meta = seeds[idx]
assert chk_xfp == xfp_str
idx, old = item.arg
new_label = await ux_input_text(old.label, confirm_exit=False, max_len=40)
new_name = await ux_input_text(old_name, confirm_exit=False, max_len=40)
if not new_name:
if not new_label:
return
dis.fullscreen("Saving...")
seeds = settings.master_get("seeds", [])
# save it
seeds[idx] = (chk_xfp, encoded, new_name, meta)
seeds[idx] = (old.xfp, old.encoded, new_label, old.origin)
# need to load and work on master secrets, will be slow if on tmp seed
settings.master_set("seeds", seeds)
# update label in sub-menu
menu.items[0].label = new_name
menu.items[0].arg = menu.items[0].arg[0:2] + (new_name,) + menu.items[0].arg[3:]
menu.items[0].label = new_label
# take old arg, in rename we cannot change encoded value, so it can be used without
# the need to deserialize it again
_, encoded = menu.items[0].arg
menu.items[0].arg = VaultEntry(*seeds[idx]), encoded
# .. and name in parent menu too
# and name in parent menu too
parent = the_ux.parent_of(menu)
if parent:
parent.update_contents()
@ -933,6 +1012,8 @@ class SeedVaultMenu(MenuSystem):
async def _add_current_tmp(*a):
from pincodes import pa
assert not_hobbled_mode()
assert pa.tmp_value
main_xfp = settings.master_get("xfp", 0)
@ -952,10 +1033,9 @@ class SeedVaultMenu(MenuSystem):
seeds = settings.master_get("seeds", [])
# Save it into master settings
seeds.append((new_xfp_str,
stash.SecretStash.storage_serialize(pa.tmp_value),
xfp_ui,
"unknown origin"))
seeds.append(list(VaultEntry(new_xfp_str,
SecretStash.storage_serialize(pa.tmp_value),
xfp_ui, "unknown origin")))
settings.master_set("seeds", seeds)
@ -967,31 +1047,38 @@ class SeedVaultMenu(MenuSystem):
@classmethod
def construct(cls):
# Dynamic menu with user-defined names of seeds shown
from glob import settings
from pincodes import pa
rv = []
add_current_tmp = MenuItem("Add current tmp", f=cls._add_current_tmp)
seeds = settings.master_get("seeds", [])
seeds = list(seed_vault_iter())
if not seeds:
rv.append(MenuItem('(none saved yet)'))
if pa.tmp_value:
rv.append(add_current_tmp)
rv.append(MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu))
if not_hobbled_mode():
if pa.tmp_value:
rv.append(add_current_tmp)
rv.append(MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu))
else:
wipe_if_deltamode()
tmp_in_sv = False
for i, (xfp_str, encoded, name, meta) in enumerate(seeds):
for i, rec in enumerate(seeds):
is_active = False
encoded = pad_raw_secret(encoded)
# de-serialize encoded secret
encoded = deserialize_secret(rec.encoded)
if encoded == pa.tmp_value:
is_active = tmp_in_sv = True
submenu = [
MenuItem(name, f=cls._detail, arg=(xfp_str, encoded, name, meta)),
MenuItem('Use This Seed', f=cls._set, arg=(xfp_str, encoded)),
MenuItem('Rename', f=cls._rename, arg=(i, xfp_str)),
MenuItem('Delete', f=cls._remove, arg=(i, xfp_str, encoded)),
MenuItem(rec.label, f=cls._detail, arg=(rec, encoded)),
MenuItem('Use This Seed', f=cls._set, arg=encoded),
MenuItem('Rename', f=cls._rename, arg=(i, rec),
predicate=not_hobbled_mode),
MenuItem('Delete', f=cls._remove, arg=(i, rec, encoded),
predicate=not_hobbled_mode),
]
if is_active:
submenu[1] = MenuItem("Seed In Use")
@ -1002,14 +1089,14 @@ class SeedVaultMenu(MenuSystem):
# DO NOT offer any modification api (rename/delete)
submenu = submenu[:2]
item = MenuItem('%2d: %s' % (i+1, name), menu=MenuSystem(submenu))
item = MenuItem('%2d: %s' % (i+1, rec.label), menu=MenuSystem(submenu))
if is_active:
item.is_chosen = lambda: True
rv.append(item)
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
rv.append(add_current_tmp)
@ -1024,6 +1111,44 @@ class SeedVaultMenu(MenuSystem):
tmp = self.construct()
self.replace_items(tmp)
class SeedVaultChooserMenu(MenuSystem):
def __init__(self, words_only=False):
self.result = None
items = []
for i, rec in enumerate(seed_vault_iter()):
if words_only and not SecretStash.is_words(deserialize_secret(rec.encoded)):
continue
item = MenuItem('%2d: %s' % (i+1, rec.label), arg=rec, f=self.picked)
items.append(item)
if not items:
items.append(MenuItem("(none suitable)"))
super().__init__(items)
async def picked(self, menu, idx, mi):
assert menu == self
# show as "checked", for a touch
menu.chosen = idx
menu.show()
await sleep_ms(100)
self.result = mi.arg
the_ux.pop() # causes interact to stop
@classmethod
async def pick(cls, **kws):
# nice simple blocking menu present and pick
m = cls(**kws)
the_ux.push(m)
await m.interact()
return m.result
class EphemeralSeedMenu(MenuSystem):
@staticmethod
@ -1042,8 +1167,9 @@ class EphemeralSeedMenu(MenuSystem):
def construct(cls):
from glob import NFC
from actions import nfc_recv_ephemeral, import_xprv
from actions import restore_temporary, scan_any_qr
from actions import restore_backup, scan_any_qr
from tapsigner import import_tapsigner_backup_file
from xor_seed import xor_restore_temporary
from charcodes import KEY_QR
import_ephemeral_menu = [
@ -1060,32 +1186,31 @@ class EphemeralSeedMenu(MenuSystem):
]
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,
shortcut=KEY_QR, f=scan_any_qr, arg=(True, True)),
MenuItem("Import Words", menu=import_ephemeral_menu),
MenuItem("Import XPRV", f=import_xprv, arg=True), # ephemeral=True
MenuItem("Tapsigner Backup", f=import_tapsigner_backup_file, arg=True), # ephemeral=True
MenuItem("Coldcard Backup", f=restore_temporary),
MenuItem("Coldcard Backup", f=restore_backup, arg=True), # tmp=True
MenuItem("Restore Seed XOR", f=xor_restore_temporary),
]
return rv
async def make_ephemeral_seed_menu(*a):
if (not pa.tmp_value) and (not settings.master_get("seedvault", False)):
# force a warning on them, unless they are already doing it.
ch = await ux_show_story(
if not await ux_confirm(
"Temporary seed is a secret completely separate "
"from the master seed, typically held in device RAM and "
"not persisted between reboots in the Secure Element. "
"Enable the Seed Vault feature to store these secrets longer-term."
"\n\nPress (4) to prove you read to the end"
" of this message and accept all consequences.",
"Enable the Seed Vault feature to store these secrets longer-term.",
title="WARNING",
escape="4"
)
if ch != "4":
confirm_key="4"
):
return
rv = EphemeralSeedMenu.construct()
@ -1113,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, \
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.
''' % (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')
if ch == '2':
@ -1126,8 +1251,8 @@ Limitations: 100 characters max length, ASCII characters 32-126 (0x20-0x7e) only
if version.has_qwerty and not PassphraseSaver.has_file():
# no need for any menus if Q and no card present
pp = await ux_input_text('', prompt="Your BIP-39 Passphrase",
b39_complete=True, scan_ok=True, max_len=100)
pp = await ux_input_text('', prompt="Your BIP-39 Passphrase", b39_complete=True,
scan_ok=True, max_len=MAX_PASS_LEN)
if not pp: return
await apply_pass_value(pp)
@ -1137,7 +1262,7 @@ Limitations: 100 characters max length, ASCII characters 32-126 (0x20-0x7e) only
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
done_cb = None
@ -1191,7 +1316,7 @@ class PassphraseMenu(MenuSystem):
return PassphraseSaverMenu(items)
def on_cancel(self):
async def on_cancel(self):
if not version.has_qwerty:
# zip to cancel item when they fail to exit via X button
self.goto_idx(self.count - 1)
@ -1206,7 +1331,9 @@ class PassphraseMenu(MenuSystem):
@classmethod
async def add_numbers(cls, *a):
# Mk4 only: add some digits (quick, easy)
pw = await ux_input_numbers(cls.pp_sofar, cls.check_length)
from ux_mk4 import ux_input_digits
pw = await ux_input_digits(cls.pp_sofar)
if pw is not None:
cls.pp_sofar = pw
cls.check_length()
@ -1224,7 +1351,7 @@ class PassphraseMenu(MenuSystem):
async def view_edit_phrase(cls, *a):
# let them control each character
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:
cls.pp_sofar = pw
cls.check_length()
@ -1235,8 +1362,8 @@ class PassphraseMenu(MenuSystem):
@classmethod
def check_length(cls):
# enforce a limit of 100 chars
cls.pp_sofar = cls.pp_sofar[0:100]
# enforce a limit of MAX_PASS_LEN chars
cls.pp_sofar = cls.pp_sofar[0:MAX_PASS_LEN]
@classmethod
async def add_text(cls, _1, _2, item):
@ -1277,15 +1404,16 @@ async def apply_pass_value(new_pp):
msg = ('Above is the master key fingerprint of the new wallet'
' created by adding passphrase to %s.'
'\n\nPassphrase: %s'
'\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')
if ch == 'x':
return
await set_ephemeral_seed(nv, summarize_ux=False, bip39pw=new_pp,
meta="BIP-39 Passphrase on [%s]" % parent_xfp_str)
origin="BIP-39 Passphrase on [%s]" % parent_xfp_str)
if ch == '1':
try:

View File

@ -171,7 +171,7 @@ async def test_secure_element():
dis.clear()
if version.has_qwerty:
if version.has_qwerty or version.mk_num == 5:
dis.text(0, 0, "^^-- Green? " if gg else " ^^-- Red?")
else:
if gg:
@ -194,6 +194,7 @@ async def test_secure_element():
dis.fullscreen("Wait...")
set_genuine()
ux_clear_keys()
dis.busy_bar(False)
ng = get_genuine()
assert ng != gg # "Could not invert LED"
@ -321,14 +322,25 @@ async def test_microsd():
from files import CardSlot
import os
def _is_inserted(slot_num):
if num_sd_slots > 1:
if slot_num == 0:
return CardSlot.sd_detect() == 0
elif slot_num == 1:
return CardSlot.sd_detect2() == 0
else:
assert False
else:
return CardSlot.is_inserted()
async def wait_til_state(num, want):
title = 'MicroSD Card'
if num_sd_slots > 1:
title += ' ' + chr(65+num)
label_test(title +':', 'Remove' if CardSlot.is_inserted() else 'Insert')
label_test(title +':', 'Remove' if _is_inserted(num) else 'Insert')
while 1:
if want == CardSlot.is_inserted(): return
if want == _is_inserted(num): return
await sleep_ms(100)
if ux_poll_key():
raise RuntimeError("MicroSD test aborted")
@ -336,23 +348,23 @@ async def test_microsd():
for slot_num in range(num_sd_slots):
# test presence switch
for ph in range(7):
await wait_til_state(slot_num, not CardSlot.is_inserted())
await wait_til_state(slot_num, not _is_inserted(slot_num))
if ph >= 2 and CardSlot.is_inserted():
if ph >= 2 and _is_inserted(slot_num):
# debounce
await sleep_ms(100)
if CardSlot.is_inserted(): break
if _is_inserted(slot_num): break
if ux_poll_key():
raise RuntimeError("MicroSD test aborted")
label_test('MicroSD Card:', 'Testing')
# card inserted
assert CardSlot.is_inserted() #, "SD not present?"
assert _is_inserted(slot_num) #, "SD not present?"
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:
fd.write("Hello")
@ -365,9 +377,7 @@ async def test_microsd():
await wait_til_state(slot_num, False)
async def start_selftest():
try:
if version.has_battery:
await test_battery()
@ -403,6 +413,5 @@ async def start_selftest():
except (RuntimeError, AssertionError) as e:
e = str(e) or problem_file_line(e)
await ux_show_story("Test failed:\n" + str(e), 'FAIL')
# EOF

View File

@ -16,10 +16,10 @@ ser_*, deser_*: functions that handle serialization/deserialization
"""
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
import ustruct as struct
import ngu
from opcodes import *
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2SH, AF_P2WSH, AF_P2TR, AF_BARE_PK
# single-shot hash functions
sha256 = ngu.hash.sha256s
@ -27,8 +27,8 @@ ripemd160 = ngu.hash.ripemd160
hash256 = ngu.hash.sha256d
hash160 = ngu.hash.hash160
def bytes_to_hex_str(s):
return str(b2a_hex(s), 'ascii')
#def bytes_to_hex_str(s):
# return str(b2a_hex(s), 'ascii')
SIGHASH_ALL = const(1)
SIGHASH_NONE = const(2)
@ -60,10 +60,13 @@ def deser_compact_size(f):
nit = struct.unpack("<B", f.read(1))[0]
if nit == 253:
nit = struct.unpack("<H", f.read(2))[0]
assert nit >= 253
elif nit == 254:
nit = struct.unpack("<I", f.read(4))[0]
assert nit >= 0x1_0000
elif nit == 255:
nit = struct.unpack("<Q", f.read(8))[0]
assert nit >= 0x1_0000_0000
return nit
def deser_string(f):
@ -80,7 +83,6 @@ def deser_uint256(f):
r += t << (i * 32)
return r
def ser_uint256(u):
rs = b""
for i in range(8):
@ -88,7 +90,6 @@ def ser_uint256(u):
u >>= 32
return rs
def uint256_from_str(s):
r = 0
t = struct.unpack("<IIIIIIII", s[:32])
@ -96,13 +97,11 @@ def uint256_from_str(s):
r += t[i] << (i * 32)
return r
def uint256_from_compact(c):
nbytes = (c >> 24) & 0xFF
v = (c & 0xFFFFFF) << (8 * (nbytes - 3))
return v
def deser_vector(f, c):
nit = deser_compact_size(f)
r = []
@ -112,7 +111,6 @@ def deser_vector(f, c):
r.append(t)
return r
# ser_function_name: Allow for an alternate serialization function on the
# entries in the vector (we use this for serializing the vector of transactions
# for a witness block).
@ -125,7 +123,6 @@ def ser_vector(l, ser_function_name=None):
r += i.serialize()
return r
def deser_uint256_vector(f):
nit = deser_compact_size(f)
r = []
@ -134,29 +131,22 @@ def deser_uint256_vector(f):
r.append(t)
return r
def ser_uint256_vector(l):
r = ser_compact_size(len(l))
for i in l:
r += ser_uint256(i)
return r
def deser_string_vector(f):
nit = deser_compact_size(f)
r = []
for i in range(nit):
t = deser_string(f)
r.append(t)
return r
return [deser_string(f) for _ in range(nit)]
def ser_string_vector(l):
r = ser_compact_size(len(l))
for sv in l:
r += ser_string(sv)
return r
return r
def deser_int_vector(f):
nit = deser_compact_size(f)
@ -166,7 +156,6 @@ def deser_int_vector(f):
r.append(t)
return r
def ser_int_vector(l):
r = ser_compact_size(len(l))
for i in l:
@ -177,16 +166,18 @@ def ser_push_data(dd):
# "compile" data to be pushed on the script stack
# - will be minimal sized, but only supports size ranges we're likely to see
ll = len(dd)
assert 2 <= ll <= 255
if ll <= 75:
if ll < 0x4c:
return bytes([ll]) + dd # OP_PUSHDATAn + data
elif ll <= 0xff:
return bytes([0x4c, ll]) + dd # 0x4c = 76 => OP_PUSHDATA1 + size + data
elif ll <= 0xffff:
return bytes([0x4d]) + struct.pack(b'<H', ll) + dd # # 0x4d = 77 => OP_PUSHDATA2
else:
return bytes([76, ll]) + dd # 0x4c = 76 => OP_PUSHDATA1 + size + data
assert False
def ser_push_int(n):
# push a small integer onto the stack
from opcodes import OP_0, OP_1, OP_16, OP_PUSHDATA1
from opcodes import OP_0, OP_1
if n == 0:
return bytes([OP_0])
@ -204,40 +195,45 @@ def disassemble(script):
try:
offset = 0
slen = len(script)
while 1:
if offset >= len(script):
if offset >= slen:
#print('dis %d done' % offset)
return
c = script[offset]
offset += 1
if 1 <= c <= 75:
#print('dis %d: bytes=%s' % (offset, b2a_hex(script[offset:offset+c])))
yield (script[offset:offset+c], None)
offset += c
cnt = c
elif OP_1 <= c <= OP_16:
# OP_1 thru OP_16
#print('dis %d: number=%d' % (offset, (c - OP_1 + 1)))
yield (c - OP_1 + 1, None)
continue
elif c == OP_PUSHDATA1:
cnt = script[offset]; offset += 1
yield (script[offset:offset+cnt], None)
offset += cnt
cnt = script[offset]
offset += 1
elif c == OP_PUSHDATA2:
cnt = struct.unpack_from("H", script, offset)
# up to 65535 bytes
cnt, = struct.unpack_from("H", script, offset)
offset += 2
yield (script[offset:offset+cnt], None)
offset += cnt
elif c == OP_PUSHDATA4:
# no where to put so much data
raise NotImplementedError
elif c == OP_1NEGATE:
yield (-1, None)
continue
else:
# OP_0 included here
#print('dis %d: opcode=%d' % (offset, c))
yield (None, c)
except:
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:
# import sys;sys.print_exception(e)
raise ValueError("bad script")
@ -326,7 +322,6 @@ class CTxIn(object):
self.nSequence = nSequence
def deserialize(self, f):
self.prevout = COutPoint()
self.prevout.deserialize(f)
self.scriptSig = deser_string(f)
self.nSequence = struct.unpack("<I", f.read(4))[0]
@ -361,30 +356,47 @@ class CTxOut(object):
# Detect type of output from scriptPubKey, and return 3-tuple:
# (addr_type_code, addr, is_segwit)
# 'addr' is byte string, either 20 or 32 long
if self.is_p2tr():
return AF_P2TR, self.scriptPubKey[2:2+32], True
if len(self.scriptPubKey) == 22 and \
self.scriptPubKey[0] == 0 and self.scriptPubKey[1] == 20:
# aka. P2WPKH
return 'p2pkh', self.scriptPubKey[2:2+20], True
if self.is_p2wpkh():
return AF_P2WPKH, self.scriptPubKey[2:2+20], True
if len(self.scriptPubKey) == 34 and \
self.scriptPubKey[0] == 0 and self.scriptPubKey[1] == 32:
# aka. P2WSH
return 'p2sh', self.scriptPubKey[2:2+32], True
if self.is_p2wsh():
return AF_P2WSH, self.scriptPubKey[2:2+32], True
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():
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():
# rare, pay to full pubkey
return 'p2pk', self.scriptPubKey[2:2+33], False
# rare, pay to full pubkey: <push_op> <pubkey> OP_CHECKSIG
# 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 this is reached, we do not understand the output well
# enough to allow the user to authorize the spend, so fail hard.
raise ValueError('scriptPubKey template fail: ' + b2a_hex(self.scriptPubKey).decode())
if self.is_op_return():
return OP_RETURN, self.scriptPubKey, False
return None, self.scriptPubKey, None
def is_p2tr(self):
return len(self.scriptPubKey) == 34 and \
(OP_1 <= self.scriptPubKey[0] <= OP_16) and self.scriptPubKey[1] == 0x20
def is_p2wpkh(self):
return len(self.scriptPubKey) == 22 and \
self.scriptPubKey[0] == 0 and self.scriptPubKey[1] == 0x14
def is_p2wsh(self):
return len(self.scriptPubKey) == 34 and \
self.scriptPubKey[0] == 0 and self.scriptPubKey[1] == 0x20
def is_p2sh(self):
return len(self.scriptPubKey) == 23 and self.scriptPubKey[0] == 0xa9 \
@ -397,8 +409,11 @@ class CTxOut(object):
def is_p2pk(self):
return (len(self.scriptPubKey) == 35 or len(self.scriptPubKey) == 67) \
and (self.scriptPubKey[0] == 0x21 or self.scriptPubKey[0] == 0x41) \
and self.scriptPubKey[-1] == 0xac
and self.scriptPubKey[0] == len(self.scriptPubKey) - 2 \
and self.scriptPubKey[-1] == OP_CHECKSIG
def is_op_return(self):
return self.scriptPubKey and (self.scriptPubKey[0] == OP_RETURN)
#def __repr__(self):
# return "CTxOut(nValue=%d scriptPubKey=%s)" \
@ -489,7 +504,7 @@ class CTransaction(object):
self.nVersion = struct.unpack("<i", f.read(4))[0]
self.vin = deser_vector(f, CTxIn)
flags = 0
if len(self.vin) == 0:
if not self.vin:
flags = struct.unpack("<B", f.read(1))[0]
# Not sure why flags can't be zero, but this
# matches the implementation in bitcoind

View File

@ -2,7 +2,7 @@
#
# 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
# - only a few of these are possible
# - the offset is the file name
@ -26,7 +26,7 @@ class SFFile:
self.message = message
self.runt = False
if max_size != None:
if max_size is not None:
# Write
self.max_size = max_size
self.readonly = False

View File

@ -37,7 +37,17 @@ if not has_lcd:
x, y, msg = a[0:3]
global contents
contents[y] = msg
is_idx = False
if x == 0 and len(msg) == 1:
# something on index zero - is it index num in top right with QR display?
# msg will just single int without any dot or smthg
try:
int(msg)
is_idx = True
except: pass
if not is_idx:
contents[y] = msg
#print('text (%s, %s): %s' % (x,y, msg))

View File

@ -1,6 +1,6 @@
# (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
#
@ -28,49 +28,81 @@ SET_VCOM_DESEL = const(0xdb)
SET_CHARGE_PUMP = const(0x8d)
# 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):
def __init__(self, width, height, external_vcc):
def __init__(self, width, height, is_mk5):
self.width = width
self.height = height
self.external_vcc = external_vcc
self.is_mk5 = is_mk5
self.pages = self.height // 8
#self.buffer = bytearray(self.pages * self.width)
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)
self.init_display()
def init_display(self):
for cmd in (
SET_DISP | 0x00, # off
# address setting
SET_MEM_ADDR, 0x00, # horizontal
# resolution and layout
SET_DISP_START_LINE | 0x00,
SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0
SET_MUX_RATIO, self.height - 1,
SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0
SET_DISP_OFFSET, 0x00,
SET_COM_PIN_CFG, 0x02 if self.height == 32 else 0x12,
# timing and driving scheme
SET_DISP_CLK_DIV, 0xF0,
SET_PRECHARGE, 0x22 if self.external_vcc else 0xf1,
SET_VCOM_DESEL, 0x30, # 0.83*Vcc
# display
SET_CONTRAST, 0xff, # maximum
SET_ENTIRE_ON, # output follows RAM contents
SET_NORM_INV, # not inverted
# charge pump
SET_CHARGE_PUMP, 0x10 if self.external_vcc else 0x14,
SET_DISP | 0x01): # on
self.write_cmd(cmd)
if not self.is_mk5:
# Mk4 and earlier
cmds = (
SET_DISP | 0x00, # display off
# address setting
SET_MEM_ADDR, 0x00, # horizontal
# resolution and layout
SET_DISP_START_LINE | 0x00,
SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0
SET_MUX_RATIO, self.height - 1,
SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0
SET_DISP_OFFSET, 0x00,
SET_COM_PIN_CFG, 0x12,
# timing and driving scheme
SET_DISP_CLK_DIV, 0xF0,
SET_PRECHARGE, 0xf1,
SET_VCOM_DESEL, 0x30, # 0.83*Vcc
# display
SET_CONTRAST, 0xff, # maximum
SET_ENTIRE_ON, # output follows RAM contents
SET_NORM_INV, # not inverted
# charge pump
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.show()
self.write_cmd(SET_DISP | 0x01)
def write_cmds(self, cmds):
for c in cmds:
self.write_cmd(c)
def poweroff(self):
self.write_cmd(SET_DISP | 0x00)
@ -78,6 +110,10 @@ class SSD1306(framebuf.FrameBuffer):
self.write_cmd(SET_DISP | 0x01)
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(contrast)
@ -85,56 +121,113 @@ class SSD1306(framebuf.FrameBuffer):
self.write_cmd(SET_NORM_INV | (invert & 1))
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(x0)
self.write_cmd(x1)
self.write_cmd(0)
self.write_cmd(self.width - 1)
self.write_cmd(SET_PAGE_ADDR)
self.write_cmd(0)
self.write_cmd(self.pages - 1)
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):
def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):
dc.init(dc.OUT, value=0)
res.init(res.OUT, value=0)
cs.init(cs.OUT, value=1)
def __init__(self, width, height, spi, dc, res, cs, is_mk5=False):
self.spi = spi
self.dc = dc
self.res = res
self.cs = cs
self.res(1)
self.res = res
# initial states
dc(0)
cs(1)
# reset sequence
res(1)
time.sleep_ms(1)
self.res(0)
res(0)
time.sleep_ms(10)
self.res(1)
super().__init__(width, height, external_vcc)
res(1)
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):
self.spi.init(baudrate=SPI_RATE, polarity=0, phase=0)
self._setup_spi()
self.cs(1)
self.dc(0)
self.cs(0)
try:
self.spi.write(bytearray([cmd]))
except:
print("SPI[cmd]: %r" % self.spi)
self.spi.write(bytearray([cmd]))
self.cs(1)
def write_data(self, buf):
self.spi.init(baudrate=SPI_RATE, polarity=0, phase=0)
self._setup_spi()
self.cs(1)
self.dc(1)
self.cs(0)
try:
self.spi.write(buf)
except:
print("SPI[data]: %r" % self.spi)
self.spi.write(buf)
self.cs(1)
# EOF

View File

@ -12,7 +12,7 @@
#
import ngu, uctypes, gc, bip39, utime
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]
@ -49,8 +49,10 @@ def numwords_to_len(num_words):
assert num_words in SEED_LEN_OPTS
return (num_words * 8) // 6
def len_from_marker(marker):
def _len_from_marker(marker):
# calculates length of entropy from CC marker
# - private detail of SecretStash
assert marker & 0x80 # wasn't actual words, might be xprv, etc
return ((marker & 0x3) + 2) * 8
class SecretStash:
@ -102,12 +104,12 @@ class SecretStash:
ch, pk = secret[1:33], secret[33:65]
assert not _bip39pw
hd.from_chaincode_privkey(ch, pk)
hd = node_from_privkey(pk, ch)
return 'xprv', ch+pk, hd
elif marker & 0x80:
# seed phrase
ll = len_from_marker(marker)
ll = _len_from_marker(marker)
# note:
# - byte length > number of words
@ -138,9 +140,34 @@ class SecretStash:
return 'master', ms, hd
@staticmethod
def is_words(secret):
# return False or number of words: 12, 18, 24
marker = secret[0]
if marker & 0x80:
return len_to_numwords(_len_from_marker(marker))
return False
@staticmethod
def decode_words(secret, bin_mode=False):
# Give a list of BIP-39 words from an encoded secret. Must be "words" type.
# - if bin_mode, return binary string representing the words, based on BIP-39
ll = _len_from_marker(secret[0])
# note:
# - byte length > number of words
# - not storing checksum
assert ll in [16, 24, 32]
# make master secret, using the memonic words, and passphrase (or empty string)
seed_bits = secret[1:1+ll]
return bip39.b2a_words(seed_bits).split() if not bin_mode else seed_bits
@staticmethod
def storage_serialize(secret):
# make it a JSON-compatible field
# - converse: utils.deserialize_secret()
return B2A(bytes(secret).rstrip(b"\x00"))
@staticmethod
@ -153,7 +180,7 @@ class SecretStash:
if marker & 0x80:
# seed phrase
ll = len_from_marker(marker)
ll = _len_from_marker(marker)
return '%d words' % len_to_numwords(ll)
if marker == 0x00:
@ -177,7 +204,7 @@ class SensitiveValues:
_cache_secret = None
_cache_used = None
def __init__(self, secret=None, bip39pw='', bypass_tmp=False):
def __init__(self, secret=None, bip39pw='', bypass_tmp=False, enforce_delta=False):
self.spots = []
self._bip39pw = bip39pw
@ -195,7 +222,12 @@ class SensitiveValues:
if not pa.has_secrets():
raise ZeroSecretException
self.deltamode = pa.is_deltamode()
if self.deltamode and enforce_delta:
# wipe self before fetching secret
import callgate
callgate.fast_wipe()
if self._cache_secret and not bypass_tmp:
# they are using new BIP39 passphrase but we already have raw secret
@ -326,6 +358,9 @@ class SensitiveValues:
return xfp
def get_xfp(self):
return swab32(self.node.my_fp())
def register(self, item):
# Caller can add his own sensitive (derived?) data to our wiper
# typically would be byte arrays or byte strings, but also
@ -368,8 +403,7 @@ class SensitiveValues:
self.register(cc)
self.register(pk)
rv = ngu.hdnode.HDNode()
rv.from_chaincode_privkey(cc, pk)
rv = node_from_privkey(pk, cc)
self.register(rv)
return rv, p
@ -388,13 +422,4 @@ class SensitiveValues:
self.register(pk)
return pk
def encoded_secret(self):
# we do not support master as secret - only extended keys and mnemonics
if self.mode == "xprv":
nv = SecretStash.encode(xprv=self.node)
else:
assert self.mode == "words"
nv = SecretStash.encode(seed_phrase=self.raw)
return nv
# EOF

View File

@ -33,7 +33,7 @@ async def import_tapsigner_backup_file(_1, _2, item):
from pincodes import pa
assert pa.is_secret_blank() # "must not have secret"
meta = "from "
origin = "from "
label = "TAPSIGNER encrypted backup file"
choice = await import_export_prompt(label, is_import=True)
@ -67,9 +67,9 @@ async def import_tapsigner_backup_file(_1, _2, item):
continue
break
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
meta += (" (%s)" % fn)
origin += (" (%s)" % fn)
try:
with CardSlot(**choice) as card:
with open(fn, 'rb') as fp:
@ -103,6 +103,6 @@ async def import_tapsigner_backup_file(_1, _2, item):
await ux_show_story(title="FAILURE", msg=str(e))
continue
await import_extended_key_as_secret(extended_key, ephemeral, meta=meta)
await import_extended_key_as_secret(extended_key, ephemeral, origin=origin)
# EOF

787
shared/teleport.py Normal file
View File

@ -0,0 +1,787 @@
# (c) Copyright 2025 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# teleport.py - Magically transport extremely sensitive data between the
# secure environment of two Q's.
#
import ngu, aes256ctr, bip39, json, ndef, chains
from utils import xfp2str, deserialize_secret
from ubinascii import unhexlify as a2b_hex
from ubinascii import hexlify as b2a_hex
from glob import settings, dis
from ux import ux_show_story, ux_confirm, the_ux, ux_dramatic_pause
from ux_q1 import show_bbqr_codes, QRScannerInteraction, ux_input_text
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
from bbqr import b32encode, b32decode
from menu import MenuItem, MenuSystem
from notes import NoteContentBase
from sffile import SFFile
from multisig import MultisigWallet
from stash import SensitiveValues, SecretStash, blank_object, bip39_passphrase
# One page github-hosted static website that shows QR based on URL contents pushed by NFC
KT_DOMAIN = 'keyteleport.com'
# No length/size worries with simple secrets, but massive notes and big PSBT,
# with lots of UTXO, cannot be passed via NFC URL, because we are limited by
# NFC chip (8k) and URL length (4k or less) inside. BBQr is not limited however.
# - but the website is ready to make animated BBQr nicely
NFC_SIZE_LIMIT = const(4096)
def short_bbqr(type_code, data):
# Short-circuit basic BBQr encoding here: always Base32, single part: 1 of 1
# - used only for NFC link, where website may split again into parts
hdr = 'B$2%s0100' % type_code
return hdr + b32encode(data)
def txt_grouper(txt):
# split into 2-char groups and add spaces -- to make it easier to read/remember
return ' '.join(txt[n:n+2] for n in range(0, len(txt), 2))
async def nfc_push_kt(qrdata):
# NFC push to send them to our QR-rendering website
url = KT_DOMAIN + '/#' + qrdata
n = ndef.ndefMaker()
n.add_url(url, https=True)
from glob import NFC
await NFC.share_loop(n, prompt="View QR on web", line2=KT_DOMAIN)
async def kt_start_rx(*a):
# menu item to "start a receive" operation
rx_key = settings.get("ktrx")
if rx_key:
# Maybe re-use same one? Vaguely risky? Concern is they are confused and
# we don't want to lose the pubkey if they should be scanning not here.
ch = await ux_show_story('''Looks like last attempt wasn't completed. \
You need to do QR scan of data from the sender to move to the next step. \
We will re-use same values as last try, unless you press (R) for new values to be picked.''',
title='Reuse Pubkey?', escape='r'+KEY_QR, hint_icons=KEY_QR)
if ch == KEY_QR:
# help them scan now!
x = QRScannerInteraction()
await x.scan_anything(expect_secret=False, tmp=False)
return
elif ch == 'r':
# wipe and restart; sender's work might be lost
rx_key = None
else:
# keep old keypair -- they might be confused
kp = ngu.secp256k1.keypair(a2b_hex(rx_key))
if not rx_key:
# pick a random key pair, just for this session
kp = ngu.secp256k1.keypair()
settings.set("ktrx", b2a_hex(kp.privkey()))
settings.save()
short_code, payload = generate_rx_code(kp)
msg = '''To receive sensitive data from another COLDCARD, \
share this Receiver Password with sender:
%s = %s
and show the QR on next screen to the sender. ENTER or %s to show here''' % (
short_code, txt_grouper(short_code), KEY_QR)
await tk_show_payload('R', payload, 'Key Teleport: Receive', msg, cta='Show to Sender')
def generate_rx_code(kp):
# Receiver-side password: given a pubkey (33 bytes, compressed format)
# - construct an 8-digit decimal "password"
# - it's a AES key, but only 26 bits worth
pubkey = bytearray(kp.pubkey().to_bytes()) # default: compressed format
#assert len(pubkey) == 33
# - want the code to be deterministic, but I also don't want to save it
nk = ngu.hash.sha256d(kp.privkey() + b'COLCARD4EVER')
# first byte will be 0x02 or 0x03 (Y coord) -- remove those known 7 bits
pubkey[0] ^= nk[20] & 0xfe
num = '%08d' % (int.from_bytes(nk[4:8], 'big') % 1_0000_0000)
# encryption after baby key stretch
kk = ngu.hash.sha256s(num.encode())
enc = aes256ctr.new(kk).cipher(pubkey)
return num, enc
def decrypt_rx_pubkey(code, payload):
# given a 8-digit numeric code, make the key and then decrypt/checksum check
# - every value works, there is no fail.
kk = ngu.hash.sha256s(code.encode())
rx_pubkey = bytearray(aes256ctr.new(kk).cipher(payload))
# first byte will be 0x02 or 0x03 but other 7 bits are noise
rx_pubkey[0] &= 0x01
rx_pubkey[0] |= 0x02
# validate that it's on the curve... otherwise the code is wrong
try:
ngu.secp256k1.pubkey(rx_pubkey)
return rx_pubkey
except:
return None
async def tk_show_payload(type_code, payload, title, msg, cta=None):
# show the QR and/or NFC
# - MAYBE: make easier/faster to pick NFC from QR screen and vice-versa
from glob import NFC
hints = KEY_QR
if NFC and len(payload) < NFC_SIZE_LIMIT:
hints += KEY_NFC
msg += ' or %s to view on your phone' % KEY_NFC
msg += '. CANCEL to stop.'
# simply show the QR
while 1:
ch = await ux_show_story(msg, title=title, hint_icons=hints)
if ch == KEY_NFC and NFC:
await nfc_push_kt(short_bbqr(type_code, payload))
elif ch == KEY_QR or ch == 'y':
# NOTE: CTA rarely seen, but maybe sometimes?
await show_bbqr_codes(type_code, payload, msg=cta)
elif ch == 'x':
return
async def kt_start_send(rx_data):
# a QR was scanned and it held (most of) a pubkey
# - they want to send to this guy
# - ask them what to send, etc
while 1:
# - ask for the sender's password -- nearly any value will be accepted
code = await ux_input_text('', confirm_exit=False, hex_only=True, max_len=8,
prompt='Teleport Password (number)', min_len=8, b39_complete=False, scan_ok=False,
placeholder='########', funct_keys=None, force_xy=None)
if not code: return
rx_pubkey = decrypt_rx_pubkey(code, rx_data)
if rx_pubkey:
break
# I think only about 50% odds of catching an incorrect code. Not sure.
ch = await ux_show_story(
"Incorrect Teleport Password. You can try again or CANCEL to stop.")
if ch == 'x': return
msg = '''You can now Key Teleport secrets! Choose what to share on next screen.\
\n
WARNING: Receiver will have full access to all Bitcoin controlled by these keys!'''
ch = await ux_show_story(msg, title="Key Teleport: Send")
if ch != 'y': return
# pick what to send from a series of submenus
menu = SecretPickerMenu(rx_pubkey)
the_ux.push(menu)
async def kt_do_send(rx_pubkey, dtype, raw=None, obj=None, prefix=b'', rx_label='the receiver', kp=None):
# We are rendering a QR and showing it to them for sending to another Q
dis.fullscreen("Wait...")
cleartext = dtype.encode() + (raw or json.dumps(obj).encode())
dis.progress_bar_show(0.1)
# Pick and show noid key to sender
noid_key, txt = pick_noid_key()
dis.progress_bar_show(0.25)
# all new EC key
my_keypair = kp or ngu.secp256k1.keypair()
dis.progress_bar_show(0.75)
payload = prefix + encode_payload(my_keypair, rx_pubkey, noid_key, cleartext,
for_psbt=bool(prefix))
dis.progress_bar_show(1)
msg = "Share this password with %s, via some different channel:"\
"\n\n %s = %s\n\n" % (rx_label, txt, txt_grouper(txt))
msg += "ENTER to view QR"
await tk_show_payload('S' if not prefix else 'E', payload,
'Teleport Password', msg, cta='Show to Receiver')
if not prefix:
# not PSBT case ... reset menus, we are deep!
from actions import goto_top_menu
goto_top_menu()
def pick_noid_key():
# pick an 40 bit password, shown as base32
# - on rx, libngu base32 decoder will convert '018' into 'OLB'
# - but a little tempted to removed vowels here?
k = ngu.random.bytes(5)
txt = b32encode(k).upper()
return k, txt
async def kt_decode_rx(is_psbt, payload):
# we are getting data back from a sender, decode it.
prompt = 'Teleport Password (text)'
if not is_psbt:
rx_key = settings.get("ktrx")
if not rx_key:
await ux_show_story("Not expecting any teleports. You need to start over.")
await kt_start_rx() # help them to start over? idk maybe not.
return
his_pubkey = payload[0:33]
body = payload[33:]
pair = ngu.secp256k1.keypair(a2b_hex(rx_key))
ses_key, body = decode_step1(pair, his_pubkey, body)
else:
# Multisig PSBT: will need to iterate over a few wallets and each N-1 possible senders
if not MultisigWallet.exists():
await ux_show_story("Incoming PSBT requires multisig wallet(s) to be already setup, but you have none.")
return
ses_key, body, sender_xfp = MultisigWallet.kt_search_rxkey(payload)
if sender_xfp is not None:
prompt = 'Teleport Password from [%s]' % xfp2str(sender_xfp)
if not ses_key:
# when ECDH fails, it's truncation or wrong RX key (due to sender using old rx key,
# or the numeric code the sender entered was wrong, etc)
await ux_show_story("QR code was damaged, "+
("numeric password was wrong, " if not is_psbt else "")+
"or it was sent to a different user. "
"Sender must start again.", title="Teleport Fail")
return
while 1:
# ask for noid key
pw = await ux_input_text('', confirm_exit=False, hex_only=False, max_len=8,
prompt=prompt, min_len=8, b39_complete=False, scan_ok=False,
placeholder='********', funct_keys=None, force_xy=None)
if not pw: return
dis.fullscreen("Wait...")
try:
assert len(pw) == 8
noid_key = b32decode(pw) # case insenstive, and smart about confused chars
final = decode_step2(ses_key, noid_key, body)
if final is not None:
break
except: pass
ch = await ux_show_story(
"Incorrect Teleport Password. You can try again or CANCEL to stop.")
if ch == 'x': return
# will ask again
# success w/ decoding. but maybe something goes wrong or they reject a confirm step
# so keep the rx key alive still
await kt_accept_values(chr(final[0]), final[1:])
async def kt_accept_values(dtype, raw):
# We got some secret, decode it more, and save it.
'''
- `s` - secret, encoded per stash.py
- `r` - raw XPRV mode - 64 bytes follow which are the chain code then master privkey
- `x` - XPRV mode, full details - 4 bytes (XPRV) + base58 *decoded* binary-XPRV follows
- `n` - one or many notes export (JSON array)
- `v` - seed vault export (JSON: one secret key but includes includes name, source of key)
- `p` - binary PSBT to be signed
- `b` - complete system backup file (text, internal format)
'''
from flow import has_se_secrets, goto_top_menu
from pincodes import pa
enc = None
origin = 'Teleported'
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':
# words / bip 32 master / xprv, etc
enc = bytearray(72)
enc[0:len(raw)] = raw
elif dtype == 'x':
# 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
txt = ngu.codecs.b58_encode(raw)
node, ch, _, _ = chains.slip132_deserialize(txt)
assert ch.name == chains.current_chain().name, 'wrong chain'
enc = SecretStash.encode(xprv=node)
elif dtype == 'p':
# raw PSBT -- much bigger more complex
from auth import sign_transaction, TXN_INPUT_OFFSET
psbt_len = len(raw)
# copy into PSRAM
with SFFile(TXN_INPUT_OFFSET, max_size=psbt_len) as out:
out.write(raw)
# This will take over UX w/ the signing process
# flags=None --> whether to finalize is decided based on psbt.is_complete
sign_transaction(psbt_len, flags=None, input_method="kt")
return
elif dtype == 'b':
# full system backup, including master: text lines
from backups import text_bk_parser, restore_tmp_from_dict_ll, restore_from_dict, extract_raw_secret
try:
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
if has_secrets():
# restores as tmp secret and/or offers to save to SeedVault
# need to remove key before I get into tmp seed settings
# so even if this errors out, new ktrx is needed
settings.remove_key("ktrx")
prob = await restore_tmp_from_dict_ll(vals, raw_sec)
else:
# we have no secret, so... reboot if it works, else errors shown, etc.
prob = await restore_from_dict(vals, raw_sec)
if prob:
await ux_show_story(prob, title='FAILED')
else:
# force new rx key because this tfr worked
# only has effect if in master seed settings
settings.remove_key("ktrx")
return
elif dtype in 'nv':
# all are JSON things
js = json.loads(raw)
if dtype == 'v':
# one key export from a seed vault
# - watch for incompatibility here if we ever change VaultEntry
from seed import VaultEntry
rec = VaultEntry(*js)
enc = deserialize_secret(rec.encoded)
origin = rec.origin
label = rec.label
elif dtype == 'n':
# import secure note(s)
from notes import import_from_json, make_notes_menu, NoteContent
settings.remove_key("ktrx") # force new rx key after this point
await import_from_json(dict(coldcard_notes=js))
await ux_dramatic_pause('Imported.', 2)
# force them into notes submenu so they can see result right away
# - highlight to last note, which should be the just-added one(s)
goto_top_menu()
nm = await make_notes_menu()
nm.goto_idx(NoteContent.count()-1)
the_ux.push(nm)
return
else:
raise ValueError(dtype)
# key material is arriving; offer to use as main secret, or tmp, or seed vault?
settings.remove_key("ktrx") # force new rx key after this point
assert enc
from seed import set_ephemeral_seed, set_seed_value
if not has_se_secrets():
# unit has nothing, so this will be the master seed
set_seed_value(encoded=enc)
ok = True
else:
ok = await set_ephemeral_seed(enc, origin=origin, label=label)
if ok:
goto_top_menu()
def noid_stretch(session_key, noid_key):
# TODO: measure timing of this on real Q
return ngu.hash.pbkdf2_sha512(session_key, noid_key, 5000)[0:32]
def encode_payload(my_keypair, his_pubkey, noid_key, body, for_psbt=False):
# do all the encryption for sender
assert len(his_pubkey) == 33
assert len(noid_key) == 5
# this can fail with ValueError: secp256k1_ec_pubkey_parse
# if the user has provided the wrong value for numeric password
# - better to catch this sooner in decrypt_rx_pubkey
session_key = my_keypair.ecdh_multiply(his_pubkey)
# stretch noid key out -- will be slow
pk = noid_stretch(session_key, noid_key)
b1 = aes256ctr.new(pk).cipher(body)
b1 += ngu.hash.sha256s(body)[-2:]
b2 = aes256ctr.new(session_key).cipher(b1)
b2 += ngu.hash.sha256s(b1)[-2:]
if for_psbt:
# no need to share pubkey for PSBT files
return b2
return my_keypair.pubkey().to_bytes() + b2
def decode_step1(my_keypair, his_pubkey, body):
# Do ECDH and remove top layer of encryption
try:
assert len(body) >= 3
session_key = my_keypair.ecdh_multiply(his_pubkey)
rv = aes256ctr.new(session_key).cipher(body[:-2])
chk = ngu.hash.sha256s(rv)[-2:]
assert chk == body[-2:] # likely means wrong rx key, or truncation
except:
return None, None
return session_key, rv
def decode_step2(session_key, noid_key, body):
# After we have the noid key, can decode true payload
assert len(noid_key) == 5
pk = noid_stretch(session_key, noid_key)
msg = aes256ctr.new(pk).cipher(body[:-2])
chk = ngu.hash.sha256s(msg)[-2:]
return msg if chk == body[-2:] else None
async def kt_incoming(type_code, payload):
# 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':
# they want to send to this guy
return await kt_start_send(payload)
elif type_code == 'S':
# we are receiving something, let's try to decode
return await kt_decode_rx(False, payload)
elif type_code == 'E':
# incoming PSBT!
return await kt_decode_rx(True, payload)
else:
raise ValueError(type_code)
class SecretPickerMenu(MenuSystem):
def __init__(self, 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
has_notes = bool(NoteContentBase.count())
has_sv = bool(settings.get('seedvault', False))
# Q-only feature, so menu can be W I D E
# - in increasing order of importance & sensitivity!
# - pinned-virgin mode is supported, so might not have any secrets to share yet,
# but can do secret notes still
m = [
MenuItem('Quick Text Message', f=self.quick_note),
MenuItem('Single Note / Password', predicate=has_notes, menu=self.pick_note_submenu),
MenuItem('Export All Notes & Passwords', predicate=has_notes, f=self.picked_note),
]
if has_sv:
m.append( MenuItem('From Seed Vault', menu=self.pick_vault_submenu) )
msg = None
if is_tmp():
# tmp seed, or maybe bip39 is in effect
# - share the current master secret, not the real master
msg = 'Temp Secret (words)' if word_based_seed() else (
'XPRV from Words+Passphrase' if bip39_passphrase else 'Temp XPRV Secret')
elif has_se_secrets():
# sharing real master secret
msg = 'Master Seed Words' if word_based_seed() else 'Master XPRV'
if msg:
m.append( MenuItem(msg, f=self.share_master_secret) )
m.append( MenuItem("Full COLDCARD Backup", f=self.share_full_backup) )
super().__init__(m)
async def pick_vault_submenu(self, *a):
# pick a secret from seed vault
from seed import SeedVaultChooserMenu
rec = await SeedVaultChooserMenu.pick()
if rec:
await kt_do_send(self.rx_pubkey, 'v', obj=list(rec))
async def pick_note_submenu(self, *a):
# Make a submenu to select a single note/password
rv = []
for note in NoteContentBase.get_all():
rv.append(MenuItem('%d: %s' % (note.idx+1, note.title), f=self.picked_note, arg=note))
return rv
async def quick_note(self, _, _2, item):
# accept a text string, and send as a note
from notes import NoteContent
txt = await ux_input_text('', max_len=100,
prompt='Enter your message', min_len=1, b39_complete=True, scan_ok=True,
placeholder='Attack at dawn.')
if not txt: return
n = NoteContent(dict(title="Quick Note", misc=txt))
await kt_do_send(self.rx_pubkey, 'n', obj=[n.serialize()])
async def picked_note(self, _, _2, item):
# exporting note(s)
if item.arg is None:
# export all
body = [n.serialize() for n in NoteContentBase.get_all()]
else:
# single note/password
body = [item.arg.serialize()]
await kt_do_send(self.rx_pubkey, 'n', obj=body)
async def share_full_backup(self, *a):
# context, and warn them
ch = await ux_show_story("Sending complete backup, including master secret, "
"seed vault (if any), multisig wallets, notes/passwords, and all settings! "
"The receiving "
"COLDCARD must already have the master seed wiped to be able to install "
"everything, otherwise only master secret and multisig are saved into a tmp seed. "
"OK to proceed?")
if ch != 'y': return
from backups import render_backup_contents
dis.fullscreen("Buiding Backup...")
# renders a text file, with rather a lot of comments; strip them
bkup = render_backup_contents(bypass_tmp=True)
out = []
for ln in bkup.split('\n'):
if not ln: continue
if ln[0] == '#': continue
out.append(ln)
await kt_do_send(self.rx_pubkey, 'b', raw=b'\n'.join(ln.encode() for ln in out))
async def share_master_secret(self, _, _2, item):
# altho menu items look different we are sharing same thing:
# - up to 72 bytes from secure elements
dis.fullscreen("Wait...")
with SensitiveValues(bypass_tmp=False, enforce_delta=True) as sv:
raw = bytearray(sv.secret)
xfp = xfp2str(sv.get_xfp())
# rtrim zeros
while raw[-1] == 0:
raw = raw[0:-1]
summary = SecretStash.summary(raw[0])
from pincodes import pa
scale = 'your MASTER secret' if not pa.tmp_value else 'a temporary secret'
msg = "Sharing %s [%s] (%s)." % (scale, xfp, summary)
msg += "\n\nWARNING: Allows full control over all associated Bitcoin!"
if not await ux_confirm(msg):
blank_object(raw)
return
await kt_do_send(self.rx_pubkey, 's', raw=raw)
async def kt_send_psbt(psbt, psbt_len, psbt_offset):
# 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.
# who remains to sign? look at inputs
ms = psbt.active_multisig
all_xfps = [x for x,*p in ms.get_xfp_paths()]
need = [x for x in psbt.multisig_xfps_needed() if x in all_xfps]
# maybe it's not really a PSBT where we know the other signers? might be
# a weird coinjoin we don't fully understand
if not need:
await ux_show_story("No more signers?")
return
# (TXN_OUTPUT_OFFSET after signing, TXN_INPUT_OFFSET for the file-teleport path)
with SFFile(psbt_offset, psbt_len) as fd:
bin_psbt = fd.read(psbt_len)
my_xfp = settings.get('xfp')
# if my_xfp in need:
# - we haven't signed yet? let's do that now .. except we've lost some of the
# data we need such as filename to save back into.
# - so just keep going instead... maybe they want to be last signer?
# Make them pick a single next signer. It's not helpful to do multiple at once
# here, since we need signatures to be added serially so that last
# signer can do finalization. We don't have a general purpose combiner.
async def done_cb(m, idx, item):
m.next_xfp = item.arg
the_ux.pop()
ci = []
next_signer = None
for idx, x in enumerate(all_xfps):
txt = '[%s] Co-signer #%d' % (xfp2str(x), idx+1)
f = done_cb
if x == my_xfp:
txt += ': YOU'
f = None
if x in need:
# we haven't signed ourselves yet, so allow that
from auth import sign_transaction
async def sign_now(*a):
# this will reset the UX stack:
# flags=None --> whether to finalize is decided based on psbt.is_complete
sign_transaction(psbt_len, flags=None, input_method="kt", offset=psbt_offset)
f = sign_now
elif x not in need:
txt += ': DONE'
f = None
mi = MenuItem(txt, f=f, arg=x)
if x not in need:
# show check if we've got sig
mi.is_chosen = lambda: True
elif next_signer is None:
next_signer = idx
ci.append(mi)
m = MenuSystem(ci)
m.next_xfp = None
m.goto_idx(next_signer) # position cursor on next candidate
the_ux.push(m)
await m.interact()
if m.next_xfp:
assert m.next_xfp != my_xfp
ri, rx_pubkey, kp = ms.kt_make_rxkey(m.next_xfp)
await kt_do_send(rx_pubkey, 'p', raw=bin_psbt, prefix=ri, kp=kp,
rx_label='[%s] co-signer' % xfp2str(m.next_xfp))
return True, ms.M - (ms.N - len(need))
async def kt_send_file_psbt(*a):
# Menu item: choose a PSBT file from SD card, and send to co-signers.
# Heavy code re-use here. Need to find the multisig wallet associated w/ file,
# so we need to parse it and we must be one of the co-signers.
from actions import is_psbt, file_picker
from auth import sign_psbt_file, TXN_INPUT_OFFSET
from version import MAX_TXN_LEN
from ux import import_export_prompt
from psbt import psbtObject
# choose any PSBT from SD
picked = await import_export_prompt("PSBT", is_import=True, no_nfc=True, no_qr=True)
if picked == KEY_CANCEL:
return
choices = await file_picker(suffix='.psbt', min_size=50, ux=False,
max_size=MAX_TXN_LEN, taster=is_psbt, **picked)
if not choices:
# error msg already shown
return
if len(choices) == 1:
# single - skip the menu
label,path,fn = choices[0]
input_psbt = path + '/' + fn
else:
# multiples - make them pick one
input_psbt = await file_picker(choices=choices)
if not input_psbt:
return
# read into PSRAM from wherever
psbt_len = await sign_psbt_file(input_psbt, just_read=True, **picked)
dis.fullscreen("Validating...")
try:
dis.progress_sofar(1, 4)
with SFFile(TXN_INPUT_OFFSET, length=psbt_len, message='Reading...') as fd:
# NOTE: psbtObject captures the file descriptor and uses it later
psbt = psbtObject.read_psbt(fd)
await psbt.validate() # might do UX: accept multisig import
dis.progress_sofar(2, 4)
psbt.consider_inputs()
dis.progress_sofar(3, 4)
psbt.consider_keys()
except Exception as exc:
# not going to do full reporting here, use our other code for that!
await ux_show_story("Cannot validate PSBT?\n\n"+str(exc), "PSBT Load Failed")
return
finally:
dis.progress_bar_show(1)
if not psbt.active_multisig:
await ux_show_story("We are not part of this multisig wallet.", "Cannot Teleport PSBT")
return
await kt_send_psbt(psbt, psbt_len=psbt_len, psbt_offset=TXN_INPUT_OFFSET)
# EOF

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