Compare commits
469 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c5907d812 | ||
|
|
7af4593e66 | ||
|
|
58a7a1c756 | ||
|
|
b9ff1a2a16 | ||
|
|
5d08ca08b0 | ||
|
|
ed5b7b1b63 | ||
|
|
76f6b9d0cb | ||
|
|
c36184e6bf | ||
|
|
28ce88b6a6 | ||
|
|
25c60cfc20 | ||
|
|
f080fe20ee | ||
|
|
82c081b9e0 | ||
|
|
cff4f3bf08 | ||
|
|
ce1fc62789 | ||
|
|
3f2e471ef5 | ||
|
|
e630dd614f | ||
|
|
bf0086bbc1 | ||
|
|
cd003034d7 | ||
|
|
d2f93b6a58 | ||
|
|
b0424125f9 | ||
|
|
1ee1b0eb36 | ||
|
|
0edb093201 | ||
|
|
1acd8fde75 | ||
|
|
cc58ff0b2b | ||
|
|
d8f9a88bcb | ||
|
|
8cf271ab4a | ||
|
|
6637c521ad | ||
|
|
ec92f13d23 | ||
|
|
fdafce2a42 | ||
|
|
716af73798 | ||
|
|
3cffd6089a | ||
|
|
302bf2a4c3 | ||
|
|
fb8e1047c9 | ||
|
|
c59b122bd3 | ||
|
|
0e2c7ce0d6 | ||
|
|
05d792cb79 | ||
|
|
398911c48a | ||
|
|
094b58bb38 | ||
|
|
52d7e4c541 | ||
|
|
395e31b34a | ||
|
|
5ed8f5a4a9 | ||
|
|
d3408165af | ||
|
|
fee2eac4fc | ||
|
|
119e9ef0fa | ||
|
|
ce79507a9a | ||
|
|
c7e0f6d18f | ||
|
|
6157c1d73d | ||
|
|
f4b242dcc8 | ||
|
|
1c95588b01 | ||
|
|
2908d95afe | ||
|
|
55d6cf8f2a | ||
|
|
647410fbf9 | ||
|
|
a820e52fce | ||
|
|
01ceb902c5 | ||
|
|
573a47ca98 | ||
|
|
ae39515ddf | ||
|
|
87a6956f06 | ||
|
|
239642e5f2 | ||
|
|
4ce6de51f8 | ||
|
|
79374d7a7d | ||
|
|
515b74b606 | ||
|
|
667e37e6f3 | ||
|
|
8342e289ea | ||
|
|
d5d7c4bb68 | ||
|
|
271599fde6 | ||
|
|
e914fd8f03 | ||
|
|
5c82bd684c | ||
|
|
64be61e562 | ||
|
|
84213cc250 | ||
|
|
36be797608 | ||
|
|
a9a90f30c6 | ||
|
|
a8d9eda6e5 | ||
|
|
bb4a1f9b30 | ||
|
|
42c8e3e8c0 | ||
|
|
aa72433207 | ||
|
|
c2edfc8da7 | ||
|
|
95cc006d7b | ||
|
|
3a5350d1ea | ||
|
|
e6b99ac352 | ||
|
|
e449ef32ff | ||
|
|
8cdc09d0af | ||
|
|
fcb88f5cdd | ||
|
|
9e0e187e49 | ||
|
|
413a91cf39 | ||
|
|
f9c0711dee | ||
|
|
904601f531 | ||
|
|
51e0a81e77 | ||
|
|
19f200a210 | ||
|
|
01c27fe568 | ||
|
|
440421731e | ||
|
|
54b1afb5eb | ||
|
|
387258f9a9 | ||
|
|
3792292b5d | ||
|
|
76f30b66c9 | ||
|
|
9e83ac99a4 | ||
|
|
5d9c1a553e | ||
|
|
421d3c1bf4 | ||
|
|
0484e3cd3b | ||
|
|
f7721cbbbe | ||
|
|
7b8a533265 | ||
|
|
f7c7275f12 | ||
|
|
373cf604b3 | ||
|
|
a62ee99fd5 | ||
|
|
a2d272336c | ||
|
|
f306f0a308 | ||
|
|
147e191456 | ||
|
|
acc58f9122 | ||
|
|
ac6525c10c | ||
|
|
2d002e3ef6 | ||
|
|
aaa91f6301 | ||
|
|
ab7b8f3a1f | ||
|
|
f3f3589613 | ||
|
|
f6807e0907 | ||
|
|
80ffbd65b7 | ||
|
|
0f92ac7f68 | ||
|
|
bf058bf756 | ||
|
|
68e8f45f96 | ||
|
|
89a2cb26ed | ||
|
|
79ecc09adc | ||
|
|
32068fd49f | ||
|
|
76802f5b64 | ||
|
|
d92c0e624c | ||
|
|
8e305739d8 | ||
|
|
995f3c7426 | ||
|
|
83ee24f326 | ||
|
|
2b333c9f5c | ||
|
|
255ca17905 | ||
|
|
46f4576746 | ||
|
|
d0c8316cf9 | ||
|
|
70ebd3365c | ||
|
|
cc4034bce7 | ||
|
|
37f4a1fd26 | ||
|
|
c84eb79fe6 | ||
|
|
d9c9098fa0 | ||
|
|
497478212a | ||
|
|
54fb9d6a08 | ||
|
|
b0f4d8b4a5 | ||
|
|
9d04ea8233 | ||
|
|
e1cbe655ef | ||
|
|
5f38ae20c1 | ||
|
|
1d3be6d17e | ||
|
|
eeabaef852 | ||
|
|
9259b03e15 | ||
|
|
35501077e7 | ||
|
|
b4a3304e35 | ||
|
|
a1e486a424 | ||
|
|
fc4daa6200 | ||
|
|
8552571c38 | ||
|
|
514b1ae2d3 | ||
|
|
52ae9f96cd | ||
|
|
a5caa48489 | ||
|
|
d05568059b | ||
|
|
fa6a36140c | ||
|
|
203c288de8 | ||
|
|
3f938c31e1 | ||
|
|
30b1c04259 | ||
|
|
e361408a28 | ||
|
|
ff3b385b0a | ||
|
|
fc2bfe26ae | ||
|
|
337ee8c463 | ||
|
|
a1eb3e901b | ||
|
|
8d8b496044 | ||
|
|
ca1b7b6345 | ||
|
|
e3b2e49bb0 | ||
|
|
449e889b1b | ||
|
|
cb539f6f18 | ||
|
|
807e7a00d2 | ||
|
|
512349b230 | ||
|
|
6dd8237123 | ||
|
|
7c8aceb753 | ||
|
|
30946eb1aa | ||
|
|
2dc1b97a92 | ||
|
|
b1b2edd72e | ||
|
|
1349b730b9 | ||
|
|
c0b9ea1087 | ||
|
|
8709817d47 | ||
|
|
4abf8b7077 | ||
|
|
f34faaf57d | ||
|
|
727beab889 | ||
|
|
8081732dea | ||
|
|
9841af46bb | ||
|
|
8d0d9af9a0 | ||
|
|
5f06e0949f | ||
|
|
785b8a479d | ||
|
|
3a1ad131e5 | ||
|
|
27843086ea | ||
|
|
44701a3e0a | ||
|
|
edc859300c | ||
|
|
64eb5cc3e0 | ||
|
|
3b8930b281 | ||
|
|
fa9bff5d36 | ||
|
|
89bdf2d499 | ||
|
|
4ea01f7d04 | ||
|
|
0ee23f5faf | ||
|
|
aa4e2595c2 | ||
|
|
c72b77ad3f | ||
|
|
5b67e2c3cb | ||
|
|
eb55a6dc11 | ||
|
|
a1342ac3bb | ||
|
|
0a0d93799a | ||
|
|
9a592179aa | ||
|
|
548c63b9e7 | ||
|
|
c323d2f5c2 | ||
|
|
ba3bc33611 | ||
|
|
d55a869550 | ||
|
|
fe7244c205 | ||
|
|
95a1b25c22 | ||
|
|
f3168475a1 | ||
|
|
b870f93eb0 | ||
|
|
ee39e91415 | ||
|
|
65e712121a | ||
|
|
f8622021f8 | ||
|
|
8f8b952223 | ||
|
|
1ca5f2e6a1 | ||
|
|
5bd7f4b351 | ||
|
|
6a47b29580 | ||
|
|
b30fc664c0 | ||
|
|
74c81c293f | ||
|
|
e22c6b6af0 | ||
|
|
292e580952 | ||
|
|
13888fd59b | ||
|
|
32272b93b3 | ||
|
|
0bc4c4f06e | ||
|
|
c2f1041927 | ||
|
|
5edc688e22 | ||
|
|
edc558493f | ||
|
|
b5d05359bd | ||
|
|
c3500f087d | ||
|
|
5f29751254 | ||
|
|
d2f4871b0e | ||
|
|
437961ca93 | ||
|
|
08df482119 | ||
|
|
041f803155 | ||
|
|
76d4e53b67 | ||
|
|
3cae633446 | ||
|
|
7da6d97934 | ||
|
|
bea721a8d6 | ||
|
|
8ac90d4c4d | ||
|
|
47cc8eaf04 | ||
|
|
5020ee546b | ||
|
|
e7f0a089f4 | ||
|
|
1a10e11bdc | ||
|
|
79dbfa9780 | ||
|
|
ee62d42fb3 | ||
|
|
39d8767949 | ||
|
|
b9deed6b80 | ||
|
|
ec9d6251e4 | ||
|
|
56180d7961 | ||
|
|
e4eaaf16fa | ||
|
|
6cb1173671 | ||
|
|
a7713e38db | ||
|
|
9c4257a51b | ||
|
|
78d5f2dc52 | ||
|
|
e7f42cf7f1 | ||
|
|
d8210a25a5 | ||
|
|
ad3b1e4ace | ||
|
|
2aa6c45daf | ||
|
|
dcc028fa2e | ||
|
|
e296bda059 | ||
|
|
a70dd4ecf2 | ||
|
|
8e90fe67b0 | ||
|
|
e01822c25e | ||
|
|
2d5e1234e8 | ||
|
|
d2aeedbd4d | ||
|
|
5b7bee406a | ||
|
|
4e77d38108 | ||
|
|
5132051d43 | ||
|
|
5fd2ee9404 | ||
|
|
b4d3e50f00 | ||
|
|
e202ab986b | ||
|
|
329c6c51bd | ||
|
|
4808ae45df | ||
|
|
b4e45057f3 | ||
|
|
c3a894192a | ||
|
|
734bcc78ca | ||
|
|
f3f3f1dbb9 | ||
|
|
b09fde8337 | ||
|
|
3d57e1f94b | ||
|
|
b8d702c0bc | ||
|
|
a12ac4b26d | ||
|
|
b1fdbae35c | ||
|
|
c4c2ae8c58 | ||
|
|
9998b576b7 | ||
|
|
d2dce34b9f | ||
|
|
6f6b245965 | ||
|
|
bc6b5a26c3 | ||
|
|
c6a0ab9bf5 | ||
|
|
20207f27db | ||
|
|
30457c98cc | ||
|
|
4439f3d7fe | ||
|
|
5a27fcc45a | ||
|
|
7a0e0a269b | ||
|
|
ef92efe67a | ||
|
|
e01e23aa63 | ||
|
|
a973c7edc1 | ||
|
|
7d3c9828ba | ||
|
|
962dd2ef3f | ||
|
|
8223765d1a | ||
|
|
70299186b9 | ||
|
|
3a986413e7 | ||
|
|
8169ec067b | ||
|
|
0ca4109693 | ||
|
|
754c4c3f64 | ||
|
|
54ccfe6cd6 | ||
|
|
f70b05ba6f | ||
|
|
6eb1ef272d | ||
|
|
f8d32a8272 | ||
|
|
789b87c33c | ||
|
|
638e7acc55 | ||
|
|
3090d220c0 | ||
|
|
37a677e6f9 | ||
|
|
0b20ef5360 | ||
|
|
f7cb241e9c | ||
|
|
e9b04ff408 | ||
|
|
32d28dea01 | ||
|
|
d4e8dfb9d5 | ||
|
|
68ffcecc30 | ||
|
|
0ac89511f1 | ||
|
|
f6639e45a7 | ||
|
|
7d84ba4c32 | ||
|
|
4254fbad5d | ||
|
|
24dccb694a | ||
|
|
dc73bdf253 | ||
|
|
d4e9549f24 | ||
|
|
cd344cb646 | ||
|
|
1ff76f30c7 | ||
|
|
9cd60433e7 | ||
|
|
f70b3247e4 | ||
|
|
34b7152e9f | ||
|
|
4c6a6b8b87 | ||
|
|
6852ce3a4d | ||
|
|
7a94a6f8e0 | ||
|
|
ab91463ec6 | ||
|
|
9d31349298 | ||
|
|
e04d391884 | ||
|
|
93e182724a | ||
|
|
b134cd9ed2 | ||
|
|
b9d8d8c0dc | ||
|
|
042b0eec3b | ||
|
|
ac472ab669 | ||
|
|
0dc88cc55b | ||
|
|
68792a7392 | ||
|
|
6507856861 | ||
|
|
3075cd07b2 | ||
|
|
60a6777b08 | ||
|
|
da80fd357b | ||
|
|
215357a9c2 | ||
|
|
2ebe376be2 | ||
|
|
a1d9652436 | ||
|
|
b6a3564a03 | ||
|
|
41b200bd6e | ||
|
|
e68123caa0 | ||
|
|
e209980630 | ||
|
|
339b2bce5d | ||
|
|
c10aff8a02 | ||
|
|
940aa3206a | ||
|
|
706466afe4 | ||
|
|
5a3b64e6c7 | ||
|
|
06a6796f11 | ||
|
|
a735a4c60e | ||
|
|
0427af8caa | ||
|
|
3c016004c7 | ||
|
|
d3d301f48c | ||
|
|
5c3be0caac | ||
|
|
ef2ff9793d | ||
|
|
d2ef212079 | ||
|
|
e6ac73b95b | ||
|
|
d95b1db57b | ||
|
|
77dda12301 | ||
|
|
16cf7abb17 | ||
|
|
9f31ed3f09 | ||
|
|
374315115d | ||
|
|
b335d92f4b | ||
|
|
2e30c9d576 | ||
|
|
1f7205186f | ||
|
|
97ee810a6d | ||
|
|
127f651a46 | ||
|
|
312432549d | ||
|
|
5d15fe4a27 | ||
|
|
65d3a9ffa1 | ||
|
|
607794aeb0 | ||
|
|
7173a92baf | ||
|
|
e6658312ae | ||
|
|
1bc2a95a1b | ||
|
|
58c809fc3f | ||
|
|
0073298061 | ||
|
|
2f18f17315 | ||
|
|
21cf0f97d0 | ||
|
|
17d6d58657 | ||
|
|
6985512be7 | ||
|
|
1e7cb8970f | ||
|
|
5bfb7388c9 | ||
|
|
a0a71e0393 | ||
|
|
4a6766579a | ||
|
|
b2fc03a4da | ||
|
|
dafb475ee5 | ||
|
|
b26ee00d27 | ||
|
|
6d4a4f4eaa | ||
|
|
7134f03eab | ||
|
|
132315f72b | ||
|
|
d2705983a6 | ||
|
|
3e33e310fd | ||
|
|
556d5084b2 | ||
|
|
ee572d89c1 | ||
|
|
e05a1a4898 | ||
|
|
b6d58a7468 | ||
|
|
a180fe4b20 | ||
|
|
cbfe72b8d5 | ||
|
|
ebf824a7f0 | ||
|
|
53b7aae325 | ||
|
|
718c0ca354 | ||
|
|
6a71550709 | ||
|
|
d42221e64e | ||
|
|
8fce5bae3a | ||
|
|
3ccd73b8e6 | ||
|
|
2794916ee8 | ||
|
|
4e3efee30f | ||
|
|
a5d0399c14 | ||
|
|
966f48dfa7 | ||
|
|
5df7583ab3 | ||
|
|
b756d43334 | ||
|
|
49643617a1 | ||
|
|
21daefdbea | ||
|
|
6e52fd8d72 | ||
|
|
5802827528 | ||
|
|
54f3fcdf95 | ||
|
|
3c8252d3ef | ||
|
|
500ac3588a | ||
|
|
ead0f009f2 | ||
|
|
8ed0e05f47 | ||
|
|
55ea38116c | ||
|
|
480b2d7cb0 | ||
|
|
4b93075df3 | ||
|
|
51c1b1d056 | ||
|
|
dc8732ad58 | ||
|
|
ed5f54e32b | ||
|
|
23d252f79b | ||
|
|
e794ecf979 | ||
|
|
5b08eaead7 | ||
|
|
1cdb0900b7 | ||
|
|
053c9165bb | ||
|
|
b07fb8a6b3 | ||
|
|
35c16c1491 | ||
|
|
49de639bac | ||
|
|
1a41164271 | ||
|
|
e344bac322 | ||
|
|
e3f75619d5 | ||
|
|
99ab403f66 | ||
|
|
9b4cc260aa | ||
|
|
fce554257f | ||
|
|
06dde5af49 | ||
|
|
ef2c5a7f1f | ||
|
|
b1fe5e194d | ||
|
|
427cf89975 | ||
|
|
bb87cdd59a | ||
|
|
5b2772a4b0 | ||
|
|
e7f8a1a71e | ||
|
|
f7f41fe6e3 | ||
|
|
47754d1785 | ||
|
|
bf67c5ad7e | ||
|
|
a18938cefd | ||
|
|
f618af12d1 | ||
|
|
d2920d1c60 | ||
|
|
a798e96de0 | ||
|
|
89405e819a | ||
|
|
eef1f6d561 | ||
|
|
98db85f2e2 | ||
|
|
e1ff15bab4 | ||
|
|
991965ac60 |
17
README.md
17
README.md
@ -8,7 +8,7 @@ with the latest updates and security alerts.
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
## Quick Links
|
||||
|
||||
@ -49,14 +49,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 +184,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 +233,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 +266,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`
|
||||
|
||||
@ -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, \
|
||||
|
||||
15
docs/bip-21-extensions.md
Normal file
15
docs/bip-21-extensions.md
Normal 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
|
||||
```
|
||||
@ -57,7 +57,7 @@ to be the first (non-change) receive address for the wallet.
|
||||
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`.
|
||||
|
||||
224
docs/key-teleport.md
Normal file
224
docs/key-teleport.md
Normal 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.
|
||||
@ -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.
|
||||
@ -72,8 +77,10 @@
|
||||
- derivation path for each cosigner must be known and consistent with PSBT
|
||||
- XFP values (fingerprints) MUST be unique for each of the co-signers
|
||||
- multisig wallet `name` can only contain printable ASCII characters `range(32, 127)`
|
||||
- for taproot multisig (musig) limitations check musig.md
|
||||
|
||||
### 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)
|
||||
@ -128,12 +135,17 @@ We will summarize transaction outputs as "change" back into same wallet, however
|
||||
- `p2wsh-p2sh`: _redeemScript_ (which is: `0x00 + 0x20 + sha256(witnessScript)`), and
|
||||
_witnessScript_ (which contains the multisig script)
|
||||
- `p2wsh`: only _witnessScript_ (which contains the actual multisig script)
|
||||
|
||||
- `p2tr`(keypath singlesig): no _redeemScript_, no _witnessScript_ and output key MUST commit to an unspendable script path as follows `Q = P + int(hashTapTweak(bytes(P)))G`
|
||||
- `p2tr`(scriptpath multisig): _taproot_merkle_root_ and _leaf_script_ more info in docs/taproot.md
|
||||
|
||||
# Derivation Paths
|
||||
|
||||
- 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 +210,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
|
||||
|
||||
|
||||
@ -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,18 @@
|
||||
Restore Backup
|
||||
Clone Coldcard
|
||||
Export Wallet
|
||||
Sparrow
|
||||
Cove
|
||||
Bitcoin Core
|
||||
Sparrow Wallet
|
||||
Nunchuk
|
||||
Bull Bitcoin
|
||||
Zeus
|
||||
Electrum Wallet
|
||||
Wasabi Wallet
|
||||
Fully Noded
|
||||
Unchained
|
||||
Lily Wallet
|
||||
Theya
|
||||
Bitcoin Safe
|
||||
Samourai Postmix
|
||||
Samourai Premix
|
||||
Descriptor
|
||||
@ -235,9 +261,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 +274,18 @@
|
||||
Verify Backup
|
||||
Backup System
|
||||
Export Wallet
|
||||
Sparrow
|
||||
Cove
|
||||
Bitcoin Core
|
||||
Sparrow Wallet
|
||||
Nunchuk
|
||||
Bull Bitcoin
|
||||
Zeus
|
||||
Electrum Wallet
|
||||
Wasabi Wallet
|
||||
Fully Noded
|
||||
Unchained
|
||||
Lily Wallet
|
||||
Theya
|
||||
Bitcoin Safe
|
||||
Samourai Postmix
|
||||
Samourai Premix
|
||||
Descriptor
|
||||
@ -260,42 +293,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 +350,25 @@
|
||||
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]
|
||||
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 +377,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 +398,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 +480,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 +533,186 @@
|
||||
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
|
||||
Zeus
|
||||
Electrum Wallet
|
||||
Wasabi Wallet
|
||||
Fully Noded
|
||||
Unchained
|
||||
Theya
|
||||
Bitcoin Safe
|
||||
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
|
||||
Zeus
|
||||
Electrum Wallet
|
||||
Wasabi Wallet
|
||||
Fully Noded
|
||||
Unchained
|
||||
Theya
|
||||
Bitcoin Safe
|
||||
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
|
||||
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]
|
||||
---
|
||||
|
||||
|
||||
27
docs/miniscript.md
Normal file
27
docs/miniscript.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Miniscript
|
||||
|
||||
**COLDCARD<sup>®</sup>** `EDGE` versions support Miniscript and MiniTapscript.
|
||||
|
||||
## Import/Export
|
||||
|
||||
* `Settings` -> `Miniscript` -> `Import from file`
|
||||
* only [descriptors](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki) allowed for import
|
||||
* `Settings` -> `Miniscript` -> `<name>` -> `Descriptors`
|
||||
* only [descriptors](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki) are exported
|
||||
* export extended keys to participate in miniscript:
|
||||
* `Advanced/Tools` -> `Export Wallet` -> `Generic JSON`
|
||||
* `Settings` -> `Multisig Wallets` -> `Export XPUB`
|
||||
|
||||
## Address Explorer
|
||||
|
||||
Same as with basic multisig. After miniscript wallet is imported,
|
||||
item with `<name>` is added to `Address Explorer` menu.
|
||||
|
||||
|
||||
## Limitations
|
||||
* no duplicate keys in miniscript (at least change indexes in subderivation has to be different)
|
||||
* subderivation may be omitted during the import - default `<0;1>/*` is implied
|
||||
* both keys with key origin info `[xfp/p/a/t/h]xpub/<0;1>/*` & blinded keys `xpub/<2;3>/*` allowed
|
||||
* use of blinded keys for co-signers requires PSBT provider to supply path from current key fingerprint
|
||||
* maximum number of keys allowed in segwit v0 miniscript is 20
|
||||
* check MiniTapscript limitations in `docs/taproot.md`
|
||||
@ -41,20 +41,26 @@ IFOvGVJrm31S0j+F4dVfQ5kbRKWKcmhmXIn/Lw8iIgaCG5QNZswjrN4X673R7jTZo1kvLmiD4hlIrbuL
|
||||
|
||||
### 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`
|
||||
### What Is Signed
|
||||
|
||||
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.
|
||||
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>`
|
||||
|
||||
### What is NOT signed
|
||||
|
||||
|
||||
40
docs/musig.md
Normal file
40
docs/musig.md
Normal file
@ -0,0 +1,40 @@
|
||||
# MuSig2
|
||||
|
||||
**COLDCARD<sup>®</sup>** `EDGE` versions support [MuSig2](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki) from version `6.5.0X` & `6.5.0QX`.
|
||||
|
||||
COLDCARD implements all following BIPs, further restricting their scope (read more in Limitations section):
|
||||
* PSBT fields [BIP-373](https://github.com/bitcoin/bips/blob/master/bip-0373.mediawiki)
|
||||
* `musig()` descriptor key expression [BIP-390](https://github.com/bitcoin/bips/blob/master/bip-0390.mediawiki)
|
||||
* Derivation Scheme for MuSig2 Aggregate Keys [BIP-328](https://github.com/bitcoin/bips/blob/master/bip-0328.mediawiki)
|
||||
|
||||
|
||||
### Why MuSig2?
|
||||
* higher level of **privacy** than OP_CHECKSIGADD. MuSig2 Taproot outputs are indistinguishable for a blockchain observer from regular, single-signer Taproot outputs even though they are actually controlled by multiple signers
|
||||
* **on-chain footprint** of a MuSig2 Taproot output is essentially a single BIP340 public key. This is more compact and has lower verification cost than each signer providing an individual public key and signature
|
||||
|
||||
|
||||
### Limitations:
|
||||
* COLDCARD must stay powered up between 1st and 2nd round as necessary musig session data are stored in volatile memory only
|
||||
* `musig()` can only be used inside `tr()` expression as key expression
|
||||
* cannot be nested within another `musig()` expression
|
||||
* only one own key in `musig()` expression
|
||||
* `musig(KEY, KEY, ..., KEY)/<NUM;NUM;...>/*`
|
||||
* all `KEY`s MUST be unique - no repeated keys
|
||||
* `KEY` expression MUST be extended key (not plain pubkey)
|
||||
* `KEY` expression cannot contain child derivation, only `musig()` expression can contain derivation steps
|
||||
* `KEY`s are sorted prior to aggregation
|
||||
* hardened derivation not allowed for `musig()` expression
|
||||
* derivation must end with `*` - only ranged `musig()` expression allowed, if `musig()` derivation is omitted, `/<0;1>/*` is implied
|
||||
* PSBT must contain all the data required by BIP-373
|
||||
* COLDCARD strictly differentiate between 1st & 2nd MuSig2 round. If COLDCARD provides nonce, it will not attempt to sign even if it could (a.k.a enough nonces from cosigners are available).
|
||||
To provide both nonce(s) & signature(s) signing needs to be preformed twice.
|
||||
* keys from WIF Store cannot be used for MuSig2 signing
|
||||
* `musig()` key expression is not allowed inside `multi_a` & `sortedmulti_a` fragments, use `thresh` instead
|
||||
* inputs that are in different musig rounds in same PSBT are not allowed
|
||||
* transaction cannot be modified after 1st musig round was initiated as that would change musig session
|
||||
|
||||
### Example
|
||||
|
||||
Following policy is example how to do threshold multisig with MuSig2 (and Taptree) even thought MuSig2 is not a native threshold scheme.
|
||||
|
||||
`tr(musig(@0,@1,@2),{{pk(musig(@0,@1)),pk(musig(@1,@2))},pk(musig(@0,@2))})`
|
||||
64
docs/proof-of-reserves-bip-322.md
Normal file
64
docs/proof-of-reserves-bip-322.md
Normal file
@ -0,0 +1,64 @@
|
||||
# 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:
|
||||
|
||||
* 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`
|
||||
* First (0th) input in `to_sign` transaction MUST have full (pre-segwit) UTXO (`PSBT_IN_NON_WITNESS_UTXO`) a.k.a `to_spend`.
|
||||
* First (0th) input in `to_sign` `PSBT_IN_NON_WITNESS_UTXO` transaction (`to_spend`) is 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 have at least one input & first input MUST be `to_spend` full txn
|
||||
* PSBT (`to_sign`) MUST only have one output with null-data `OP_RETURN`
|
||||
* Optionally inputs can be added to `to_sign` for Proof of Reserve signing.
|
||||
* PSBT MUST be version 0.
|
||||
* 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.
|
||||
|
||||
### Proof of Reserves Signing Experience
|
||||
|
||||
After Coldcard recognizes BIP-322 PoR PSBT it asks the user to
|
||||
import a human-readable message that was used to build `to_spend`
|
||||
scriptSig. This message must hash exactly the `message_hash` from
|
||||
the PSBT, otherwise signing is not offered.
|
||||
|
||||
Read more [here.](https://gist.github.com/orangesurf/0c1d0a31d3ebe7e48335a34d56788d4c)
|
||||
|
||||
Example screen text:
|
||||
|
||||
```text
|
||||
Proof of Reserves
|
||||
|
||||
Amount 0.20000000 BTC
|
||||
|
||||
Message Hash:
|
||||
11b5fe357842f5c368d2e3884d6a5ba577e3bc7cde132004f39b8c2a43a9cdec
|
||||
|
||||
Message Challenge:
|
||||
00140b2537a7d6f3cc668c9e9fa0303ffb3cad6e9b81
|
||||
|
||||
21 inputs
|
||||
1 output
|
||||
|
||||
0.00000000 BTC
|
||||
- OP_RETURN -
|
||||
null-data
|
||||
|
||||
Press ENTER to approve and sign transaction. Press (2) to explore txn
|
||||
outputs. CANCEL to abort.
|
||||
```
|
||||
@ -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.
|
||||
|
||||
@ -157,6 +157,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 ⊕ b ⊕ 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
209
docs/spending-policy.md
Normal 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.
|
||||
|
||||
|
||||
77
docs/taproot.md
Normal file
77
docs/taproot.md
Normal file
@ -0,0 +1,77 @@
|
||||
# Taproot
|
||||
|
||||
**COLDCARD<sup>®</sup>** Mk4 experimental `EDGE` versions
|
||||
support Schnorr signatures ([BIP-0340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki)),
|
||||
Taproot ([BIP-0341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki))
|
||||
and Tapscript ([BIP-0342](https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki)) support.
|
||||
|
||||
## Output script (a.k.a address) generation
|
||||
|
||||
If the spending conditions do not require a script path, the output key MUST commit to an unspendable script path.
|
||||
`Q = P + int(hashTapTweak(bytes(P)))G` a.k.a internal key MUST be tweaked by `TapTweak` tagged hash of itself. If
|
||||
the spending conditions require script path, internal key MUST be tweaked by `TapTweak` tagged hash of tree merkle root.
|
||||
|
||||
Addresses in `Address Explorer` for `p2tr` are generated with above-mentioned methods. Outputs `scriptPubkeys` in PSBT
|
||||
MUST be generated with above-mentoned methods to be considered change.
|
||||
|
||||
## Allowed descriptors
|
||||
|
||||
1. Single signature wallet without script path: `tr(key)`
|
||||
2. Tapscript multisig with internal key and up to 8 leaf scripts:
|
||||
* `tr(internal_key, sortedmulti_a(2,@0,@1))`
|
||||
* `tr(internal_key, pk(@0))`
|
||||
* `tr(internal_key, {sortedmulti_a(2,@0,@1),pk(@2)})`
|
||||
* `tr(internal_key, {or_d(pk(@0),and_v(v:pkh(@1),older(1000))),pk(@2)})`
|
||||
|
||||
## Provably unspendable internal key
|
||||
|
||||
There are 2 methods to provide provably unspendable internal key, if users wish to only use tapscript script path.
|
||||
|
||||
1. **(recommended)** Origin-less extended key serialization with H from [BIP-0341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs) as BIP-32 key and random chaincode.
|
||||
|
||||
`tr(xpub/<0:1>/*, sortedmulti_a(2,@0,@1))` which is the same thing as `tr(xpub, sortedmulti_a(2,@0,@1))` because `/<0;1>/*` is implied if not derivation path not provided.
|
||||
|
||||
### Below option was deprecated in version 6.3.5X & 6.3.5QX
|
||||
2. Use `unspend(` [notation](https://gist.github.com/sipa/06c5c844df155d4e5044c2c8cac9c05e#unspendable-keys). Has to be ranged.
|
||||
|
||||
`tr(unspend(77ec0c0fdb9733e6a3c753b1374c4a465cba80dff52fc196972640a26dd08b76)/<0:1>/*, sortedmulti_a(2,@0,@1))`
|
||||
|
||||
### Below option were deprecated in version 6.3.5X & 6.3.5QX
|
||||
3. use **static** provably unspendable internal key H from [BIP-0341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs).
|
||||
|
||||
`tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0, sortedmulti_a(2,@0,@1))`
|
||||
|
||||
4. use COLDCARD specific placeholder `@` to let HWW pick a fresh integer r in the range 0...n-1 uniformly at random and use `H + rG` as internal key. COLDCARD will not store r and therefore user is not able to prove to other party how the key was generated and whether it is actually unspendable.
|
||||
|
||||
`tr(r=@, sortedmulti_a(MofN))`
|
||||
|
||||
5. pick a fresh integer r in the range 0...n-1 uniformly at random yourself and provide that in the descriptor. COLDCARD generates internal key with `H + rG`. It is possible to prove to other party that this internal key does not have a known discrete logarithm with respect to G by revealing r to a verifier who can then reconstruct how the internal key was created.
|
||||
|
||||
`tr(r=77ec0c0fdb9733e6a3c753b1374c4a465cba80dff52fc196972640a26dd08b76, sortedmulti_a(2,@0,@1))`
|
||||
|
||||
Option 3. leaks the information that key path spending is not possible and therefore is not recommended privacy-wise.
|
||||
Options 4. and 5. are problematic to some extent as internal key is static. Use recommended options 1. and 2. if the fact that internal key is unspendable should remain private.
|
||||
|
||||
|
||||
## Limitations
|
||||
|
||||
### Tapscript Limitations
|
||||
|
||||
In current version only `TREE` of max depth 4 is allowed (max 8 leaf script allowed).
|
||||
Taproot single leaf multisig has artificial limit of max 32 signers (M=N=32).
|
||||
Number of keys in whole taptree is limited to 32.
|
||||
|
||||
If Coldcard can sign by both key path and script path - key path has precedence.
|
||||
|
||||
### PSBT Requirements
|
||||
|
||||
PSBT provider MUST provide following Taproot specific input fields in PSBT:
|
||||
1. `PSBT_IN_TAP_BIP32_DERIVATION` with all the necessary keys with their leaf hashes and derivation (including XFP). Internal key has to be specified here with empty leaf hashes.
|
||||
2. `PSBT_IN_TAP_INTERNAL_KEY` MUST match internal key provided in `PSBT_IN_TAP_BIP32_DERIVATION`
|
||||
3. `PSBT_IN_TAP_MERKLE_ROOT` MUST be empty if there is no script path. Otherwise it MUST match what Coldcard can calculate from registered descriptor.
|
||||
4. `PSBT_IN_TAP_LEAF_SCRIPT` MUST be specified if there is a script path.
|
||||
|
||||
PSBT provider MUST provide following Taproot specific output fields in PSBT:
|
||||
1. `PSBT_OUT_TAP_BIP32_DERIVATION` with all the necessary keys with their leaf hashes and derivation (including XFP). Internal key has to be specified here with empty leaf hashes.
|
||||
2. `PSBT_OUT_TAP_INTERNAL_KEY` must match internal key provided in `PSBT_OUT_TAP_BIP32_DERIVATION`
|
||||
3. `PSBT_OUT_TAP_TREE` with depth, leaf version and script defined.
|
||||
94
docs/web2fa.md
Normal file
94
docs/web2fa.md
Normal file
@ -0,0 +1,94 @@
|
||||
# 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 (16 bytes, or 8 digits for 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?ss={shared_secret}&q={is_q}&g={nonce}&nm={label_text}
|
||||
|
||||
- `shared_secret`: 16 chars of Base32-encoded pre-shared secret
|
||||
- `is_q`: flag indicating use of QR to provide nonce back to user
|
||||
- `nonce`: text string that is either 8 digits for Mk4, or hex digits for QR
|
||||
- `nm`: human readable label for the transaction/purpose
|
||||
|
||||
Server will accept plaintext arguments as above, but normally everything
|
||||
after the question mark is encrypted.
|
||||
|
||||
2
external/ckcc-protocol
vendored
2
external/ckcc-protocol
vendored
@ -1 +1 @@
|
||||
Subproject commit 0e686dbda686f76c4d3e8069558b2a31f9d1c2b1
|
||||
Subproject commit 6d9f7193b336ab1097c7f941ce8c7e2ae80bfe29
|
||||
2
external/libngu
vendored
2
external/libngu
vendored
@ -1 +1 @@
|
||||
Subproject commit 1cccb25ef7736efae4a1de83d5dbdc13a2db0e80
|
||||
Subproject commit b0ce9acffa455d9630c64d3614d0fb9b913c919e
|
||||
2
external/micropython
vendored
2
external/micropython
vendored
@ -1 +1 @@
|
||||
Subproject commit 97d35f058f504a354fc6df79a8b3db5c91862501
|
||||
Subproject commit 4107246f8a080807b62c3b4838e71e812ea68b6f
|
||||
@ -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')
|
||||
|
||||
|
||||
49
graphics/mono/mk5_nfc_1.txt
Normal file
49
graphics/mono/mk5_nfc_1.txt
Normal 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
|
||||
49
graphics/mono/mk5_nfc_2.txt
Normal file
49
graphics/mono/mk5_nfc_2.txt
Normal 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
|
||||
49
graphics/mono/mk5_nfc_3.txt
Normal file
49
graphics/mono/mk5_nfc_3.txt
Normal 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
|
||||
49
graphics/mono/mk5_nfc_4.txt
Normal file
49
graphics/mono/mk5_nfc_4.txt
Normal 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
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||

|
||||
@ -15,6 +13,12 @@ researchers who wish to analyse the Coldcard more completely.
|
||||
|
||||
This is the Q rev D schematic.
|
||||
|
||||

|
||||
|
||||
`schematic-mark5f.png`
|
||||
|
||||
This is the Mark4 rev F schematic.
|
||||
|
||||

|
||||
|
||||
`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
BIN
hardware/bom-mark5f.xlsx
Normal file
Binary file not shown.
BIN
hardware/schematic-mark5f.png
Normal file
BIN
hardware/schematic-mark5f.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 428 KiB |
@ -74,4 +74,9 @@ special_chars = dict(small=[
|
||||
x x x x x
|
||||
'''),
|
||||
|
||||
# thin space
|
||||
('\u2009', dict(y=0, w=5), '''\
|
||||
|
||||
'''),
|
||||
|
||||
])
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
Pillow==10.3.0
|
||||
Pillow==12.1.1
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
|
||||
|
||||
37
releases/EdgeChangeLog.md
Normal file
37
releases/EdgeChangeLog.md
Normal file
@ -0,0 +1,37 @@
|
||||
# Change Log
|
||||
|
||||
## Warning: Edge Version
|
||||
|
||||
```diff
|
||||
- This preview version of firmware has not yet been qualified
|
||||
- and tested to the same standard as normal Coinkite products.
|
||||
- It is recommended only for developers and early adopters
|
||||
- for experimental use.
|
||||
```
|
||||
|
||||
This lists the changes in the most recent EDGE firmware, for each hardware platform.
|
||||
|
||||
# Shared Improvements - Both Mk4 and Q
|
||||
|
||||
- New Feature: Ability to sign MuSig2 UTXOs. Read more [here](https://github.com/Coldcard/firmware/blob/new_edge/docs/musig.md)
|
||||
- New Feature: BIP-322 Proof of Reserves for Miniscript & MuSig2 UTXOs
|
||||
- Bugfix: PSBT global XPUBs validation when signing with specific wallet
|
||||
- Bugfix: Do not allow sighash DEFAULT outside taproot context
|
||||
|
||||
# Mk4 Specific Changes
|
||||
|
||||
## 6.5.0X - 2026-03-24
|
||||
|
||||
- synced with master up to `5.5.0`
|
||||
|
||||
|
||||
# Q Specific Changes
|
||||
|
||||
## 6.5.0QX - 2026-03-24
|
||||
|
||||
- synced with master up to `1.4.0Q`
|
||||
|
||||
|
||||
# Release History
|
||||
|
||||
- [`History-Edge.md`](History-Edge.md)
|
||||
161
releases/History-Edge.md
Normal file
161
releases/History-Edge.md
Normal file
@ -0,0 +1,161 @@
|
||||
## Warning: Edge Version
|
||||
|
||||
```diff
|
||||
- This preview version of firmware has not yet been qualified
|
||||
- and tested to the same standard as normal Coinkite products.
|
||||
- It is recommended only for developers and early adopters
|
||||
- for experimental use. DO NOT use for large Bitcoin amounts.
|
||||
```
|
||||
|
||||
## 6.4.1X & 6.4.1QX
|
||||
|
||||
-Bugfix: Multisig migration only worked for K of K multisig wallets (those where M is the same as N)
|
||||
|
||||
|
||||
# Shared Improvements - Both Mk4 and Q
|
||||
|
||||
### WARNING: 6.4.0X is not backwards-compatible with previous EDGE firmware versions.
|
||||
#### 6.4.0X stores multisig wallet internally as Miniscript wallets. Newly created multisig wallets won't be visible if you downgrade after creating them on 6.4.0X. Existing multisig wallets will be converted into Miniscript, yet preserved in old format if downgrade is desired.
|
||||
|
||||
- New Feature: Key Teleport
|
||||
- New Feature: Spending Policy for Miniscript Wallets
|
||||
- New Feature: Internal descriptor cache speeding up sequential operation with miniscript wallets.
|
||||
To take full advantage of the feature work with miniscript wallets sequentially. First, do all operations
|
||||
needed with `wallet1` before changing to `wallet2`.
|
||||
- New Feature: Add ability to import/export [BIP-388](https://github.com/bitcoin/bips/blob/master/bip-0388.mediawiki) Wallet Policies.
|
||||
BIP-388 policies are now also used as our wallet serialization format, which optimized setting storage.
|
||||
- New Feature: Sign with specific miniscript wallet. `Settings -> Multisig/Miniscript -> <name> -> Sign PSBT`
|
||||
- New Feature: Miniscript wallet name can be specified for `sign` USB command
|
||||
- New Feature: Rename Miniscript wallet via UX. `Settings -> Multisig/Miniscript -> <wallet> -> Rename`.
|
||||
- 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`
|
||||
- Enhancement: Slightly faster HW accelerated tagged hash
|
||||
- Enhancement: PSBT class optimizations. Ability to sign bigger txn.
|
||||
- Enhancement: Signing TXN UI shows Miniscript wallet name.
|
||||
- Change: Deprecation of legacy mulitsig import format. Ability to import/export in this format was removed.
|
||||
Old functionality - renaming by reimporting descriptor with different name was removed.
|
||||
Use descriptors or BIP-388 wallet policies
|
||||
- Change: Deprecated `p2sh` USB command. Use `miniscript` USB commands to handle multisig wallets.
|
||||
- Change: Descriptor template was remove from Generic JSON export, and `key_exp` was added
|
||||
with BIP-380 extended key expression `[xfp/origin_path]xpub`.
|
||||
- Bugfix: Disjoint derivation in miniscript wallets
|
||||
- Bugfix: Disallow P2SH legacy miniscript
|
||||
- Bugfix: Do not allow to import miniscripts with relative lock without consensus meaning.
|
||||
Only allow to import block-based in range `older(1 - 65535)` & time-based in range `older(4194305 - 4259839)`
|
||||
|
||||
# Mk4 Specific Changes
|
||||
|
||||
## 6.4.0X - 2025-11-20
|
||||
|
||||
- synced with master up to `5.4.5`
|
||||
- Enhancement: Show QR of XOR-split seeds
|
||||
|
||||
|
||||
# Q Specific Changes
|
||||
|
||||
## 6.4.0QX - 2025-11-20
|
||||
|
||||
- synced with master up to `1.3.5Q`
|
||||
|
||||
|
||||
# 6.3.5X & 6.3.5QX Shared Improvements - Both Mk4 and Q
|
||||
|
||||
Change: Allow origin-less extended keys in multisig & miniscript descriptors
|
||||
Change: Static internal keys disallowed - all keys need to be ranged extended keys
|
||||
|
||||
# Mk4 Specific Changes
|
||||
|
||||
- all updates from `5.4.1`
|
||||
|
||||
# Q Specific Changes
|
||||
|
||||
- all updates from version `1.3.1Q`
|
||||
|
||||
|
||||
# 6.3.4X & 6.3.4QX Shared Improvements - Both Mk4 and Q
|
||||
|
||||
- Bugfix: Complex miniscript wallets with keys in policy that are not in strictly ascending order were incorrectly filled
|
||||
upon load from settings. All users on versions `6.2.2X`+ needs to update.
|
||||
- Bugfix: Single key miniscript descriptor support
|
||||
- Enhancement: Hide Secure Notes & Passwords in Deltamode. Wipe seed if notes menu accessed.
|
||||
- Enhancement: Hide Seed Vault in Deltamode. Wipe seed if Seed Vault menu accessed.
|
||||
- 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
|
||||
- Change: Do not allow to purge settings of current active tmp seed when deleting it from Seed Vault
|
||||
|
||||
# Mk4 Specific Changes
|
||||
|
||||
- all updates from `5.4.0`
|
||||
- Enhancement: Export single sig descriptor with simple QR
|
||||
|
||||
# Q Specific Changes
|
||||
|
||||
- all updates from version `1.3.0Q`
|
||||
- Bugfix: Properly re-draw status bar after Restore Master on COLDCARD without master seed.
|
||||
|
||||
|
||||
## 6.3.3X & 6.3.3QX Shared Improvements - Both Mk4 and Q (2024-07-04)
|
||||
|
||||
- New Feature: Ranged provably unspendable keys and `unspend(` support for Taproot descriptors
|
||||
- New Feature: Address ownership for miniscript and tapscript wallets
|
||||
- Enhancement: Address explorer simplified UI for tapscript addresses
|
||||
- Bugfix: Constant `AFC_BECH32M` incorrectly set `AFC_WRAPPED` and `AFC_BECH32`.
|
||||
- Bugfix: Trying to set custom URL for NFC push transaction caused yikes
|
||||
|
||||
### Mk4 Specific Changes
|
||||
|
||||
- Bugfix: Fix yikes displaying BIP-85 WIF when both NFC and VDisk are OFF
|
||||
- Bugfix: Fix inability to export change addresses when both NFC and Vdisk id OFF
|
||||
- Bugfix: In BIP-39 words menu, show space character rather than Nokia-style placeholder
|
||||
which could be confused for an underscore.
|
||||
|
||||
### Q Specific Changes
|
||||
|
||||
- Enhancement: Miniscript and (BB)Qr codes
|
||||
- Bugfix: Properly clear LCD screen after simple QR code is shown
|
||||
|
||||
|
||||
## 6.2.2X - 2024-01-18
|
||||
|
||||
- New Feature: Miniscript [USB interface](https://github.com/Coldcard/ckcc-protocol/blob/master/README.md#miniscript)
|
||||
- New Feature: Named miniscript imports. Wrap descriptor in json
|
||||
`{"name:"n0", "desc":"<descriptor>"}` with `name` key to use this name instead of the
|
||||
filename. Mostly usefull for USB and NFC imports that have no file, in which case name
|
||||
was created from descriptor checksum.
|
||||
- Enhancement: Allow keys with same origin, differentiated only by change index derivation
|
||||
in miniscript descriptor.
|
||||
- Enhancement: HSM `wallet` rule enabled for miniscript
|
||||
- Enhancement: Add `msas` in to the `share_addrs` HSM [rule](https://coldcard.com/docs/hsm/rules/)
|
||||
to be able to check miniscript addresses in HSM mode.
|
||||
- Enhancement: HW Accelerated AES CTR for BSMS and passphrase saver
|
||||
- Bugfix: Do not allow to import duplicate miniscript
|
||||
wallets (thanks to [AnchorWatch](https://www.anchorwatch.com/))
|
||||
- Bugfix: Saving passphrase on SD Card caused a freeze that required reboot
|
||||
|
||||
## 6.2.1X - 2023-10-26
|
||||
|
||||
- New Feature: Enroll Miniscript wallet via USB (requires ckcc `v1.4.0`)
|
||||
- New Feature: Temporary Seed from COLDCARD encrypted backup
|
||||
- Enhancement: Add current temporary seed to Seed Vault from within Seed Vault menu.
|
||||
If current active temporary seed is not saved yet, `Add current tmp` menu item is
|
||||
present in Seed Vault menu.
|
||||
- Reorg: `12 Words` menu option preferred on the top of the menu in all the seed menus
|
||||
- Enhancement: Mainnet/Testnet separation. Only show wallets for current active chain.
|
||||
- contains all the changes from the newest stable `5.2.0-mk4` firmware
|
||||
|
||||
## 6.1.0X - 2023-06-20
|
||||
|
||||
- New Feature: Miniscript and MiniTapscript support (`docs/miniscript.md`)
|
||||
- Enhancement: Tapscript up to 8 leafs
|
||||
- Address explorer display refined slightly (cosmetic)
|
||||
|
||||
## 6.0.0X - 2023-05-12
|
||||
|
||||
- New Feature: Taproot keyspend & Tapscript multisig `sortedmulti_a` (tree depth = 0)
|
||||
- New Feature: Support BIP-0129 Bitcoin Secure Multisig Setup (BSMS).
|
||||
Both Coordinator and Signer roles are supported.
|
||||
- Enhancement: change Key Origin Information export format in multisig `addresses.csv` according to [BIP-0380](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#key-expressions)
|
||||
`(m=0F056943)/m/48'/1'/0'/2'/0/0` --> `[0F056943/48'/1'/0'/2'/0/0]`
|
||||
- Bugfix: correct `scriptPubkey` parsing for segwit v1-v16
|
||||
- Bugfix: do not infer segwit just by availability of `PSBT_IN_WITNESS_UTXO` in PSBT
|
||||
@ -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
|
||||
|
||||
@ -1,6 +1,235 @@
|
||||
*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.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
|
||||
|
||||
@ -2,21 +2,21 @@
|
||||
|
||||
This lists the new changes that have not yet been published in a normal release.
|
||||
|
||||
# Shared Improvements - Both Mk4 and Q
|
||||
|
||||
# Mk4 Specific Changes
|
||||
# Shared Improvements - Both Mk and Q
|
||||
|
||||
- tbd
|
||||
|
||||
# Mk Specific Changes
|
||||
|
||||
## 5.4.? - 2024-??-??
|
||||
## 5.5.x - 2065-04-xx
|
||||
|
||||
- tbd
|
||||
|
||||
|
||||
|
||||
# Q Specific Changes
|
||||
|
||||
## 1.3.?Q - 2024-??-??
|
||||
## 1.4.xQ - 2065-04-xx
|
||||
|
||||
- tbd
|
||||
|
||||
|
||||
- Bugfix: Properly re-draw status bar after Restore Master on COLDCARD without master seed.
|
||||
|
||||
@ -2,104 +2,53 @@
|
||||
Hash: SHA256
|
||||
|
||||
95eff9e044cdb6b3d00961ae72d450684d5441c6a3661ab550a3c3aa0882e754 README.md
|
||||
97107b5be1c8b65efa4bd36b7d1798e4ed15917861bd2d40784d66302a61d335 Next-ChangeLog.md
|
||||
f6d8a1edf0993cdecea7cdc34f48ce344f249ec0fc2d28fbc4da9ebc163c6148 History-Q.md
|
||||
3e98b0f292b30460e128c3d41e9dd33428524516ce433fe4a3b99132025ca64c History-Mk4.md
|
||||
412597a0e30684400cb61ee04650c13ef9fc3dc16fc2570bd5e33a1dc0085d7a Next-ChangeLog.md
|
||||
7d9dd67289f717aeb80f13a8e283e2dcc0da3036359afd2a5774dc04a2947680 History-Q.md
|
||||
d4891b509915800650a881556cca37604caab7a268afc0b1ed31021cea125891 History-Mk4.md
|
||||
c8ad43b4e3f9d77777026da6d1210c6fc5cfe435bcfcd241c0f67c9392ad7b82 History-Mk3.md
|
||||
7c06aa1d5168e02d928da087f13c74b94e40f52e5eb281af21edcfdf6cabe5ce ChangeLog.md
|
||||
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
|
||||
4db89ecffa1376bfc68a37110c2041a29afe52b005d527ecde701131168fc19c 2024-09-12T1733-v1.3.0Q-q1-coldcard-factory.dfu
|
||||
4d83715772b31643abde3b9a0bb328003f4a31d14e2fe9c1e038077a518acaea 2024-07-05T1348-v5.3.3-mk4-coldcard.dfu
|
||||
020d6d5c3baa724713b2f906112bb95f7eff43c3f5a4f8f11b77d8c2e96ccc88 2024-07-05T1348-v5.3.3-mk4-coldcard-factory.dfu
|
||||
54da941c8df84fcb84adcc62fdd3ee97d1fc12e2a9a648551ca614fcbacade3f 2024-07-05T1342-v1.2.3Q-q1-coldcard.dfu
|
||||
7f704aa37887ed84d6a25f124e9b4a31187430d7cf6b198eb83b86af8ae4e5ea 2024-07-05T1342-v1.2.3Q-q1-coldcard-factory.dfu
|
||||
ddf5ce1ef1ee2e6ba2922b333213d0cb939a2658b294c0f24c0e489de3fe7c75 2024-07-04T1501-v6.3.3X-mk4-coldcard.dfu
|
||||
9a2c5ef80a6f8212caa3b455e203da3549a79b08b473113662cf80fff587566a 2024-07-04T1459-v6.3.3QX-q1-coldcard.dfu
|
||||
a990cc94066486a37071c011cd85a29caed433cb4ca3f1c4dce7f715ef81dc3c 2024-06-26T1741-v5.3.2-mk4-coldcard.dfu
|
||||
218d17069d05c0ec2829e5629c5216121028d15b145c31b552e2f52daa7bf172 2024-06-26T1741-v5.3.2-mk4-coldcard-factory.dfu
|
||||
b87505b407b0477e2d15f71cfb20645ac55ac5b7c74493d25a2c9c97e807b2b3 2024-06-26T1739-v1.2.2Q-q1-coldcard.dfu
|
||||
efff41069f3f82d4e69d08a02a565ae0d2cd55c07dbbbe4c1328e6e3b6d8faa1 2024-06-26T1739-v1.2.2Q-q1-coldcard-factory.dfu
|
||||
90b1edfbe194b093258f9cda8f4add4aa3317e9ea205ff35914da7d91410fdae 2024-05-09T1529-v1.2.1Q-q1-coldcard.dfu
|
||||
c7889532323f7b0c08e84589c7cc756e2c46e209b4eea031bdfef4a633a813c1 2024-05-09T1529-v1.2.1Q-q1-coldcard-factory.dfu
|
||||
ef6526d37bc1a929c94dc8388f3863f6cc1582addf26495f761123f0bfb7aa30 2024-05-09T1527-v5.3.1-mk4-coldcard.dfu
|
||||
98c675e98a18b2437c52e30a9867c271bbca9969771caa34299556ef3fcb1a43 2024-05-09T1527-v5.3.1-mk4-coldcard-factory.dfu
|
||||
c7c79a21c206e8b0e816c86ef1b43cd6932cb767ed97291d5fbc2f0e749f95b7 2024-05-06T1812-v1.2.0Q-q1-coldcard.dfu
|
||||
5c6b69948f0193b3a7bd252195136d6d9f84ab14fbc8c5349150e7d238708c6f 2024-05-06T1812-v1.2.0Q-q1-coldcard-factory.dfu
|
||||
bab6818787eec45ef28b6c297e2504ffd4fa041ab19da8a3fd27543dffe876b8 2024-05-06T1811-v5.3.0-mk4-coldcard.dfu
|
||||
3da458c0dabe9a17eaeb92ee959006a64a3e6838eeb31f887a18840f020ef8b9 2024-05-06T1811-v5.3.0-mk4-coldcard-factory.dfu
|
||||
101f336310b9b460d717d91d2572ea9e9ef7ac3edbdaf132c7c3aa46bb89050a 2024-04-02T1416-v1.1.0Q-q1-coldcard.dfu
|
||||
5d034bc6b1abec49a067a90766bdb769faf9a1b52b2c9b7e541d32484cf783fc 2024-04-02T1416-v1.1.0Q-q1-coldcard-factory.dfu
|
||||
6ea843a56e87d7d811d90be6bfa4703794bbc8318d9709e88ada05740e03b12d 2024-03-14T1419-v1.0.1Q-q1-coldcard.dfu
|
||||
f53c79c64f02dd1e860a8d32f9319edd279485d97f07815b2a1eb180a1305459 2024-03-14T1419-v1.0.1Q-q1-coldcard-factory.dfu
|
||||
122e6d757eb5a8ce073d98a85851f376adec97856336c5a8f05b953b5c87a533 2024-03-10T1537-v1.0.0Q-q1-coldcard.dfu
|
||||
ae04aaac47f07e10143c75b5c772b54739830214c8234356d003137897f3f4f4 2024-03-10T1537-v1.0.0Q-q1-coldcard-factory.dfu
|
||||
6aaa9d5bf1726fe4d4a4834010d9b9b6525e8592bb97945cd08cc728fc884068 2024-03-02T1750-v0.0.8Q-q1-coldcard.dfu
|
||||
a0cd556693fae5b8b03f2a498c0abb1e6d747f91a92bd8f2559a676f8707d840 2024-03-02T1750-v0.0.8Q-q1-coldcard-factory.dfu
|
||||
18fe081d84a950e1fddb2151ad50917697dfc218cd68e2e359229b0bdadbff37 2024-02-26T1442-v0.0.7Q-q1-coldcard.dfu
|
||||
e4f4fe89cf3743d794568fd5b32b14551966139e9199602ea10468f925fab1cf 2024-02-26T1442-v0.0.7Q-q1-coldcard-factory.dfu
|
||||
2dc7a27f43958f2de9851f221183c94258ac915ae43d997b39b644e7b9daff8f 2024-02-22T1423-v0.0.6Q-q1-coldcard.dfu
|
||||
1e4f4d4c04835d78fcc4857d3264034a56dccf594e307d7408d7c4cdcdb0a926 2024-02-22T1423-v0.0.6Q-q1-coldcard-factory.dfu
|
||||
d51573c72d8958ea35357d4e0a36ce6aaa2d05924577efb219e2cc189be63f08 2024-02-16T1635-v0.0.5Q-q1-coldcard.dfu
|
||||
55f4ef9c3ae116f50db938acfc3a4b09717965f82cf6de8cc7385f68cd66d285 2024-02-16T1635-v0.0.5Q-q1-coldcard-factory.dfu
|
||||
8fd1ced0d5e0338d845f6d5ec5ab069a5143cceade02d4f17e86b7d182b489eb 2024-02-15T1843-v0.0.4Q-q1-coldcard.dfu
|
||||
43fac084727b0e69bae7fc040a62854673fd585dc2435d93bf146c80762e41cf 2024-02-15T1843-v0.0.4Q-q1-coldcard-factory.dfu
|
||||
3064bf7f1a039e7cd5c1a13c6aff8cc4338e52ef2177abbdca4b196955f9e434 2024-02-08T2005-v0.0.3Q-q1-coldcard.dfu
|
||||
788e7a1b182f920016617411b875fa7095ae007c6a53fc476afb1c93f0eed1c9 2024-02-08T2005-v0.0.3Q-q1-coldcard-factory.dfu
|
||||
6dbf9ddb58b4fcc25b084ffd57a0fd7ad56bd0934ac464735420c6c835b31a19 History-Edge.md
|
||||
cb8316310a4c165b26cc68ed584775125d52abc0bf744712662e91ded4286e01 EdgeChangeLog.md
|
||||
9ebab063b57ff07e5d8df20c266ac94736a6ad0e4c71ad1f1db46ec16b0c94be ChangeLog.md
|
||||
b2fa9f4b9a9778b71cf4b09ad79732192fdb457214f868a3af5234094deea33f 2026-03-25T1408-v6.5.0X-mk-coldcard.dfu
|
||||
400789bfd4fe4912161078b28a547ec029f1d108d109715c57919697746c6c31 2026-03-25T1408-v6.5.0X-mk-coldcard-factory.dfu
|
||||
f7bed9f1d2d49a35e7c53c8208e73ceaccbee2ab3e7fcd7c020fbd4923140313 2026-03-25T1407-v6.5.0QX-q1-coldcard.dfu
|
||||
a45770254c1fcfa09324cc9ff0d85c4e19559f493ec78c25d1bdf7cabc5cc65e 2026-03-25T1407-v6.5.0QX-q1-coldcard-factory.dfu
|
||||
372fa1f82e54f632574c56a695a1ed332464bf029bd733b2db2131a591d8f126 2025-11-25T1618-v6.4.1X-mk4-coldcard.dfu
|
||||
580acb64157cf3e2167d3afd46e1e406d75c3532356c36b67321cd2f1a218fc8 2025-11-25T1618-v6.4.1X-mk4-coldcard-factory.dfu
|
||||
1059560fb598e5e8fd6aed0164aa4cad166552bf8e47a0365e986429c9a15346 2025-11-25T1617-v6.4.1QX-q1-coldcard.dfu
|
||||
dc5fcc6a633c2cca1d1d709accc556a3ae4730f1a579062745b830cd5fd07656 2025-11-25T1617-v6.4.1QX-q1-coldcard-factory.dfu
|
||||
f04617b52fc0db6e95cac0dddd9ddd90754219f38b63a26d08c848e208069edb 2025-11-20T1602-v6.4.0X-mk4-coldcard.dfu
|
||||
993ef645ca83988c576febfaa248c0a5044e948d3d1e4443f31d5f9fd5734fe1 2025-11-20T1602-v6.4.0X-mk4-coldcard-factory.dfu
|
||||
371f13f3e1a5ef28d14933daf03820f0e51d26ffa96008dd5595da0dfac646cf 2025-11-20T1601-v6.4.0QX-q1-coldcard.dfu
|
||||
f7e73850b3c3dc33b1cd0fa7a94909931c1a4bbd881a7224a71da77807976640 2025-11-20T1601-v6.4.0QX-q1-coldcard-factory.dfu
|
||||
495f37ce7ddaba2e9fc3f03dec582f1646f258a3d0cec5e71c04d127357b2fa3 2025-02-19T1941-v6.3.5X-mk4-coldcard.dfu
|
||||
580701fb2de24362d8de6cf998d5fd42ca9ab003aff75f3c0140d915a06a6803 2025-02-19T1941-v6.3.5X-mk4-coldcard-factory.dfu
|
||||
605ebb5acde19447e5c1d7c8cfd0302c89de5c5870d85f06b185ecab3437f94e 2025-02-19T1939-v6.3.5QX-q1-coldcard.dfu
|
||||
245db07574a535a3f068ed9a759bf0088f0d0e1e39704a0e0727f90119833602 2025-02-19T1939-v6.3.5QX-q1-coldcard-factory.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
|
||||
73f31fbcb064a6b763d50852aafcdff01d7ec72906b5cb0af6cf28328fd80a89 2024-12-18T1413-v6.3.4X-mk4-coldcard-factory.dfu
|
||||
93ab7615bcedeeff123498c109e5859dae28e58885e29ed86b6f3fd6ba709cce 2024-12-18T1407-v6.3.4QX-q1-coldcard.dfu
|
||||
7e284bcead1f9c2f468230a588ddf62064014682772a552d05f453d91d55b6ae 2024-12-18T1407-v6.3.4QX-q1-coldcard-factory.dfu
|
||||
a9d0b416c3cb4f122f2826283fce82bbc5fe4464817b601a3a5787b1f8aaba20 2024-01-18T1507-v6.2.2X-mk4-coldcard.dfu
|
||||
4651fb81dc04ac07ae53535f4246ef7f32611c50853de9edaefa68f3c64e1fac 2023-12-21T1526-v5.2.2-mk4-coldcard.dfu
|
||||
a49cd00808732c67b359c9f86814ddeafc63a1040823b6c1d2035a870575c9ed 2023-12-21T1526-v5.2.2-mk4-coldcard-factory.dfu
|
||||
06d1048bea43c5d7c72c5e5f395a676620ce884aed0cd152627a86d922e2f3ab 2023-12-19T1444-v5.2.1-mk4-coldcard.dfu
|
||||
3eb9c4b1add88a6fe412d783b8f4b895241a67e423bbacc6a13816a5216a30fe 2023-12-19T1444-v5.2.1-mk4-coldcard-factory.dfu
|
||||
cc93209e800bc05386b5613969e62c27b9acd4388e3a922686525da90a505778 2024-01-18T1507-v6.2.2X-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
|
||||
d2a4a8b71b0b102971bf8a6c98968dee776a77e0a5707db862e34be5276fbc78 2023-09-08T2009-v5.1.4-mk4-coldcard-factory.dfu
|
||||
c03d4e2d1115e9440d1762c95fc82ae5a31122e84ee88d6537a8e75f26f66954 2023-09-07T1501-v5.1.3-mk4-coldcard.dfu
|
||||
3602f307df06b6658d7731172c2eb3f192a0bc8ee02c606e3cb97c1aa8d49af2 2023-09-07T1501-v5.1.3-mk4-coldcard-factory.dfu
|
||||
f6fb19d95bd1e38535f137bed60cafbfcd52379a686e3d12f372f881d78e640e 2023-06-26T1241-v4.1.9-coldcard.dfu
|
||||
489e161f686a0c631fc605054f8e7271208b16191b669174b8a58f5af28b0f4a 2023-06-20T1506-v6.1.0X-mk4-coldcard.dfu
|
||||
233398cc8f6b9e894072448eb8b8a82a4f546219ce461dd821f0ed0a38b61900 2023-06-19T1627-v4.1.8-coldcard.dfu
|
||||
66c83c3f95fd3d0796b1e452d2e8ed8ac6a4abead53faf5ae793eceb6f7bbdb5 2023-06-20T1506-v6.1.0X-mk4-coldcard-factory.dfu
|
||||
2e8ed970f518a476d0b34752ecbad75bab246669aa65de8f43801364c6f5753e 2023-05-12T1316-v6.0.0X-mk4-coldcard.dfu
|
||||
7aefd5bcce533f15337e83618ebbd42925d336792c82a5ca19a430b209b30b8a 2023-04-07T1330-v5.1.2-mk4-coldcard.dfu
|
||||
a6c007992139a847f0f238769023727e8cbc05c54c916b388a4dd8bc7490f0aa 2023-04-07T1330-v5.1.2-mk4-coldcard-factory.dfu
|
||||
99804b440f41ea47675456b4e20e7bb4e9cb434556c5813ab83c26fcda0f4e80 2023-02-27T2105-v5.1.1-mk4-coldcard.dfu
|
||||
8b37d0f2bf9ca8990f424e5a79fe62405e1ec3aca515760e509afec8f2dbacbc 2023-02-27T2105-v5.1.1-mk4-coldcard-factory.dfu
|
||||
bcf4284f7733e9de8d4dba238368552b056a27308e466721be7ca624192e257f 2023-02-27T1509-v5.1.0-mk4-coldcard.dfu
|
||||
cc946bcb63211e15d85db577e25ab2432d4a74d5dad77d710539e505dce7914a 2022-11-14T1854-v4.1.7-coldcard.dfu
|
||||
010827a60ebfc25b8a6e2bb94cc69b938419957ac6d4a9b6c0b1357c4c6c8632 2022-10-05T1724-v5.0.7-mk4-coldcard.dfu
|
||||
bc4d0b2b985aea3a78eb9351cdadf60d1ab00801ed1e7192765b94181cb8933b 2022-10-05T1517-v4.1.6-coldcard.dfu
|
||||
884f373717c9c605920a1dc29e0f890bf7b3cc6b141666814e396094aeedb3f8 2022-07-29T1816-v5.0.6-mk4-coldcard.dfu
|
||||
3c680195ef49cd0eb86d8e2426443511e8834bcea2d0a86ab52a35cc9365a801 2022-07-20T1508-v5.0.5-mk4-coldcard.dfu
|
||||
7bd2b98186370f2d895e1e43949694f6ba61a1c021f72a63f0f86a30f338a0fc 2022-05-27T1500-v5.0.4-mk4-coldcard.dfu
|
||||
5aa2ccc65e2e5279db78b3068b9f3c60c34dd7cc330c2cc1243160db31a2d0f0 2022-05-04T1258-v4.1.5-coldcard.dfu
|
||||
6dbf0aca0f98fb7bdc761eeead4786617b804dad4afb42ee02febf23d31b5e9b 2022-05-04T1254-v5.0.3-mk3-coldcard.dfu
|
||||
d5d9bf50892a4aab6e2ffb106a3d206853a60f879daa94a6f90d68a69bf4fa33 2022-05-04T1252-v5.0.3-mk4-coldcard.dfu
|
||||
9bb028d3e60239f0fcdb3b1f91075785e2c21795789b38c4c619c1f64c2950ef 2022-04-25T1618-v4.1.4-coldcard.dfu
|
||||
a363b1f0d1b27b8f21dbaac32844a59dacab8c2fee126815cda84c4df31fd7cd 2022-04-19T1805-v5.0.2-mk4-coldcard.dfu
|
||||
afb6048397af4093e63567563544098e1cfb45b7ca673536253eb6494d60125c 2022-03-24T1645-v5.0.1-mk3-coldcard.dfu
|
||||
605807bd448711d54e14057892a100bac299a103f5b5fb6466d73f9a36d0694b 2022-03-24T1643-v5.0.1-mk4-coldcard.dfu
|
||||
badd10c078996516c6464c9bfa5f696747dd7206c97d1e6a75d6f5ee0436619a 2022-03-14T1907-v5.0.0-mk4-coldcard.dfu
|
||||
dedfcf8385e35dbdbb26b92f8c0667105404062ad83c8830d809cf9193434d9c 2021-09-02T1752-v4.1.3-coldcard.dfu
|
||||
d01d81305b209dadcf960b9e9d20affb8d4f11e9f9f916c5a06be29298c80dc2 2021-07-28T1347-v4.1.2-coldcard.dfu
|
||||
08e1ec1fd073afbbc9014db6da07fd96c6b20a6710fe491eb805afeba865fe3f 2021-04-30T1748-v4.1.1-coldcard.dfu
|
||||
2c39330bef467af8dcd7e2f393a970e1ca177b1812f830269916657ff79598eb 2021-04-29T1725-v4.1.0-coldcard.dfu
|
||||
5e0c5f4ba9fa0e5fd7f9846e25c6cd28821a86ff5e1207c56cc3a4f4c3741f15 2021-04-07T1424-v4.0.2-coldcard.dfu
|
||||
f05bc8dfed047c0c0abe5ed60621d2d14899b10717221c4af0942d96a1754f33 2021-03-29T1927-v4.0.1-coldcard.dfu
|
||||
3097fa3c173247637aa27376036e384940adeb67ce727c9795471f46deaa5210 2021-01-14T1617-v3.2.2-coldcard.dfu
|
||||
9e4aeee48d4399a761fec5d4c65cb2495ef5bc0b46995c085d63a65cf67362cb 2021-01-07T1439-v3.2.1-coldcard.dfu
|
||||
bea27f263b524a66b3ed0a58c16805e98be0d7c3db20c2f7aab3238f2c6a6995 2019-12-19T1623-v3.0.6-coldcard.dfu
|
||||
8dd5ff029bb2b08c857604f0c9b5773931f6683ee331ecbc35d9ab4c460b745f 2023-05-12T1316-v6.0.0X-mk4-coldcard-factory.dfu
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmbjJicACgkQo6MbrVoq
|
||||
WxAnMwf/e2kR1aK6AJiriRa1n3XDomw8ivaUQXUApmK0kawBhVBDLKw5aa3lvTcS
|
||||
dg80wnenzNdE/QxctL+FkaZzKYsKbFpstkBEbZKcgbHVcinypKJJfICrhIBVVyZw
|
||||
wdhJMGOLEyWMysqfaYMtYJQPkg5nIn0rRxn4yWXIeXAQLcFgdlWzVykqfGZW1xYr
|
||||
CcVvxMqufXfc6c5aRFQzBO/YVHiRYzvK1NGDPztJEjXYU3zxnExAZFxk0vgpxvE3
|
||||
CahKfSSTNv54u4CTLxYCdHPRq9OM6yL/w3OUyUQFklCizk2PjrObsJQW4szbbjlx
|
||||
r7+587Pc5cpJCZn73Q0Y5/SWgnqm4g==
|
||||
=/h9F
|
||||
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmnD7GQACgkQo6MbrVoq
|
||||
WxCMxwf/Q4oCzsi5r/CKHXHMgxO+vZdVS21pbwKBO8myjWkvsje2DKran9djUIj0
|
||||
GO+J9fMYuIRqzKocwh3R7NLRTBYl2CWvNA8X5t6HBgg8GekRonVRtYz3v5layj/I
|
||||
DO13xenkUBsCi0R/pkTVK+flKx720Ph/JEf5yRclhpWPZtuj5FztEFxJ+iwK+ipV
|
||||
OG1JwlMRFoNRKwC+ayp8Fz607dPrI5dSd7TTz02PcCNXkMauQYwhvzxHWF6ExLly
|
||||
ddSmHdBFRxDS8PRokOvXQOQkzsF55aiv+UMt76l37FPmmbHTPCWdrHYguVm/g9Tz
|
||||
roezfeiGOcyfvodyr9mjq7PUB75A9g==
|
||||
=08Ws
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -7,32 +7,23 @@
|
||||
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 multisig import MultisigWallet
|
||||
from menu import MenuSystem, MenuItem, ToggleMenuItem
|
||||
from public_constants import AFC_BECH32, AFC_BECH32M, AF_P2WPKH, AF_P2TR, AF_CLASSIC
|
||||
from wallet import MiniScriptWallet
|
||||
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:]
|
||||
|
||||
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 +32,14 @@ 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/86h/⋯", f=self.deeper),
|
||||
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 +49,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)
|
||||
@ -67,7 +65,7 @@ class KeypathMenu(MenuSystem):
|
||||
pl = p[0:p.rfind('/')].rfind('/')
|
||||
else:
|
||||
self.prefix = p # displayed on mk4 only
|
||||
pl = len(p)-2
|
||||
pl = len(p)-2
|
||||
for mi in items:
|
||||
mi.arg = mi.label
|
||||
mi.label = '⋯'+mi.label[pl:]
|
||||
@ -96,9 +94,12 @@ 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):
|
||||
@ -106,15 +107,14 @@ class KeypathMenu(MenuSystem):
|
||||
assert val.endswith('/⋯')
|
||||
cpath = val[:-2]
|
||||
nl = await ux_enter_bip32_index('%s/' % cpath, unlimited=True)
|
||||
return KeypathMenu(cpath, nl)
|
||||
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"):
|
||||
@ -179,8 +179,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 +188,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 +198,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)))
|
||||
@ -210,10 +209,15 @@ class AddressListMenu(MenuSystem):
|
||||
items.append(MenuItem("Account Number", f=self.change_account))
|
||||
items.append(MenuItem("Custom Path", menu=self.make_custom))
|
||||
|
||||
# if they have MS wallets, add those next
|
||||
for ms in MultisigWallet.iter_wallets():
|
||||
if not ms.addr_fmt: continue
|
||||
items.append(MenuItem(ms.name, f=self.pick_multisig, arg=ms))
|
||||
# if they have miniscript wallets, add those next
|
||||
if MiniScriptWallet.exists():
|
||||
items.append(ToggleMenuItem('MS Scripts/Derivs', 'aemscsv',
|
||||
['Default Off', 'Enable'], story=(
|
||||
"Enable this option to add script(s) and derivations to the CSV export"
|
||||
" of Multisig/Miniscript wallets. Default is to only export addresses.")))
|
||||
|
||||
for msc in MiniScriptWallet.iter_wallets():
|
||||
items.append(MenuItem(msc.name, f=self.pick_miniscript, arg=msc))
|
||||
else:
|
||||
items.append(MenuItem("Account: %d" % self.account_num, f=self.change_account))
|
||||
|
||||
@ -245,10 +249,10 @@ class AddressListMenu(MenuSystem):
|
||||
settings.put('axi', axi) # update last clicked address
|
||||
await self.show_n_addresses(path, addr_fmt, None)
|
||||
|
||||
async def pick_multisig(self, _1, _2, item):
|
||||
ms_wallet = item.arg
|
||||
settings.put('axi', item.label) # update last clicked address
|
||||
await self.show_n_addresses(None, None, ms_wallet)
|
||||
async def pick_miniscript(self, _1, _2, item):
|
||||
msc_wallet = item.arg
|
||||
settings.put('axi', item.label) # update last clicked address
|
||||
await self.show_n_addresses(None, msc_wallet.addr_fmt, msc_wallet)
|
||||
|
||||
async def make_custom(self, *a):
|
||||
# picking a custom derivation path: makes a tree of menus, with chance
|
||||
@ -274,13 +278,13 @@ Press (3) if you really understand and accept these risks.
|
||||
async def show_n_addresses(self, path, addr_fmt, ms_wallet, start=0, n=10, allow_change=True):
|
||||
# Displays n addresses by replacing {idx} in path format.
|
||||
# - also for other {account} numbers
|
||||
# - or multisig case
|
||||
# - or miniscript case
|
||||
from glob import dis, NFC
|
||||
from wallet import MAX_BIP32_IDX
|
||||
|
||||
start = self.start
|
||||
|
||||
def make_msg(change=0):
|
||||
def make_msg(change=0, start=start, n=n):
|
||||
# Build message and CTA about export, plus the actual addresses.
|
||||
if n:
|
||||
msg = "Addresses %d⋯%d:\n\n" % (start, min(start + n - 1, MAX_BIP32_IDX))
|
||||
@ -293,22 +297,7 @@ Press (3) if you really understand and accept these risks.
|
||||
dis.fullscreen('Wait...')
|
||||
|
||||
if ms_wallet:
|
||||
# IMPORTANT safety feature: never show complete address
|
||||
# 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))
|
||||
|
||||
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'
|
||||
dis.progress_sofar(idx-start+1, n)
|
||||
|
||||
msg, addrs = ms_wallet.make_addresses_msg(msg, start, n, change)
|
||||
else:
|
||||
# single-signer wallets
|
||||
from wallet import MasterSingleSigWallet
|
||||
@ -319,16 +308,17 @@ 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',
|
||||
key0=k0, force_prompt=True
|
||||
)
|
||||
if version.has_qwerty:
|
||||
escape += KEY_LEFT+KEY_RIGHT+KEY_HOME+KEY_PAGE_UP+KEY_PAGE_DOWN
|
||||
escape += KEY_LEFT+KEY_RIGHT+KEY_HOME+KEY_PAGE_UP+KEY_PAGE_DOWN+KEY_QR
|
||||
else:
|
||||
escape += "79"
|
||||
|
||||
@ -339,11 +329,15 @@ 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:
|
||||
if addr_fmt != AF_P2TR:
|
||||
escape += "0"
|
||||
msg += " Press (0) to sign message with this key."
|
||||
|
||||
return msg, addrs, escape
|
||||
|
||||
msg, addrs, escape = make_msg()
|
||||
change = 0
|
||||
msg, addrs, escape = make_msg(change, start)
|
||||
while 1:
|
||||
ch = await ux_show_story(msg, escape=escape)
|
||||
|
||||
@ -364,14 +358,10 @@ 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
|
||||
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)
|
||||
await show_qr_codes(addrs, is_alnum, start, is_addrs=True)
|
||||
|
||||
continue
|
||||
|
||||
@ -384,8 +374,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
|
||||
@ -408,7 +405,7 @@ Press (3) if you really understand and accept these risks.
|
||||
else:
|
||||
continue # 3 in non-NFC mode
|
||||
|
||||
msg, addrs, escape = make_msg(change)
|
||||
msg, addrs, escape = make_msg(change, start)
|
||||
|
||||
def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, change=0):
|
||||
# Produce CSV file contents as a generator
|
||||
@ -416,31 +413,14 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
|
||||
from ownership import OWNERSHIP
|
||||
|
||||
if ms_wallet:
|
||||
# For multisig, include redeem script and derivation for each signer
|
||||
yield '"' + '","'.join(['Index', 'Payment Address', 'Redeem Script']
|
||||
+ ['Derivation (%d of %d)' % (i+1, ms_wallet.N) for i in range(ms_wallet.N)]
|
||||
) + '"\n'
|
||||
# saver will be None if we don't think it worth saving these addresses
|
||||
saver = OWNERSHIP.saver(ms_wallet, change, start, n)
|
||||
|
||||
if (start == 0) and (n > 100) and change in (0, 1):
|
||||
saver = OWNERSHIP.saver(ms_wallet, change, start)
|
||||
else:
|
||||
saver = None
|
||||
|
||||
for (idx, addr, derivs, script) in ms_wallet.yield_addresses(start, n, change_idx=change):
|
||||
if saver:
|
||||
saver(addr)
|
||||
|
||||
# policy choice: never provide a complete multisig address to user.
|
||||
addr = censor_address(addr)
|
||||
|
||||
ln = '%d,"%s","%s","' % (idx, addr, b2a_hex(script).decode())
|
||||
ln += '","'.join(derivs)
|
||||
ln += '"\n'
|
||||
|
||||
yield ln
|
||||
for line in ms_wallet.generate_address_csv(start, n, change, saver=saver):
|
||||
yield line
|
||||
|
||||
if saver:
|
||||
saver(None) # close file
|
||||
saver(None, 0) # close cache file
|
||||
|
||||
return
|
||||
|
||||
@ -448,26 +428,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):
|
||||
for (idx, addr, deriv) in main.yield_addresses(start, n, 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 +457,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 +467,32 @@ 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()
|
||||
derive += "/%d/%d" % (change, start)
|
||||
else:
|
||||
addr_fmt = AF_CLASSIC if addr_fmt == AF_P2TR else addr_fmt
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
1809
shared/auth.py
1809
shared/auth.py
File diff suppressed because it is too large
Load Diff
@ -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,12 +45,7 @@ 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))
|
||||
|
||||
@ -103,6 +100,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 +123,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 +138,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 +165,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 +183,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)
|
||||
|
||||
@ -214,13 +207,17 @@ def restore_from_dict_ll(vals):
|
||||
# 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 +258,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"]:
|
||||
if key == "miniscript":
|
||||
# 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 +320,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 +340,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 +392,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 +447,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 +475,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 +540,164 @@ 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(12)
|
||||
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 +780,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 +822,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.")
|
||||
|
||||
|
||||
@ -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
|
||||
@ -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")
|
||||
|
||||
|
||||
1062
shared/bsms.py
Normal file
1062
shared/bsms.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -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))
|
||||
|
||||
1297
shared/ccc.py
Normal file
1297
shared/ccc.py
Normal file
File diff suppressed because it is too large
Load Diff
279
shared/chains.py
279
shared/chains.py
@ -5,12 +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_CLASSIC, AF_P2WPKH, AF_P2TR, AF_BARE_PK
|
||||
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 serializations import hash160, ser_compact_size, disassemble
|
||||
from public_constants import AFC_PUBKEY, AFC_BECH32, AFC_SCRIPT
|
||||
from public_constants import TAPROOT_LEAF_TAPSCRIPT, TAPROOT_LEAF_MASK
|
||||
from serializations import hash160, ser_compact_size, disassemble, ser_string
|
||||
from ucollections import namedtuple
|
||||
from opcodes import OP_RETURN, OP_1, OP_16
|
||||
from precomp_tag_hash import TAP_TWEAK_H, TAP_LEAF_H
|
||||
|
||||
|
||||
SINGLESIG_AF = (AF_P2WPKH, AF_CLASSIC, AF_P2TR, 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.
|
||||
@ -19,18 +24,40 @@ 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)
|
||||
|
||||
|
||||
def taptweak(internal_key, tweak=None):
|
||||
# BIP 341 states: "If the spending conditions do not require a script path,
|
||||
# the output key should commit to an unspendable script path instead of having no script path.
|
||||
# This can be achieved by computing the output key point as:
|
||||
# Q = P + int(hashTapTweak(bytes(P)))G."
|
||||
actual_tweak = internal_key if tweak is None else internal_key + tweak
|
||||
tweak = ngu.hash.sha256t(TAP_TWEAK_H, actual_tweak, True)
|
||||
xo_pubkey = ngu.secp256k1.xonly_pubkey(internal_key)
|
||||
xo_pubkey_tweaked = xo_pubkey.tweak_add(tweak)
|
||||
return xo_pubkey_tweaked.to_bytes()
|
||||
|
||||
def tapscript_serialize(script, leaf_version=TAPROOT_LEAF_TAPSCRIPT):
|
||||
# leaf version is only 7 msb
|
||||
lv = leaf_version % TAPROOT_LEAF_MASK
|
||||
return bytes([lv]) + ser_string(script)
|
||||
|
||||
def tapleaf_hash(script, leaf_version=TAPROOT_LEAF_TAPSCRIPT):
|
||||
return ngu.hash.sha256t(TAP_LEAF_H, tapscript_serialize(script, leaf_version), True)
|
||||
|
||||
|
||||
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>
|
||||
@ -65,68 +92,57 @@ class ChainsBase:
|
||||
return node.serialize(cls.slip132[addr_fmt].pub, False)
|
||||
|
||||
@classmethod
|
||||
def deserialize_node(cls, text, addr_fmt):
|
||||
# xpub/xprv to object
|
||||
addr_fmt = AF_CLASSIC if addr_fmt == AF_P2SH else addr_fmt
|
||||
node = ngu.hdnode.HDNode()
|
||||
version = node.deserialize(text)
|
||||
assert (version == cls.slip132[addr_fmt].pub) \
|
||||
or (version == cls.slip132[addr_fmt].priv)
|
||||
return node
|
||||
def script_pubkey(cls, addr_fmt, pubkey=None, script=None):
|
||||
digest = None
|
||||
if addr_fmt & AFC_SCRIPT:
|
||||
assert script, "need witness/redeem script"
|
||||
|
||||
@classmethod
|
||||
def p2sh_address(cls, addr_fmt, witdeem_script):
|
||||
# Multisig and general P2SH support
|
||||
# - witdeem => witness script for segwit, or redeem script otherwise
|
||||
# - redeem script can be generated from witness script if needed.
|
||||
# - this function needs a witdeem script to be provided, not simple to make
|
||||
# - more verification needed to prove it's change/included address (NOT HERE)
|
||||
# - reference: <https://bitcoincore.org/en/segwit_wallet_dev/>
|
||||
# - returns: str(address)
|
||||
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'
|
||||
|
||||
assert addr_fmt & AFC_SCRIPT, 'for p2sh only'
|
||||
assert witdeem_script, "need witness/redeem script"
|
||||
else:
|
||||
assert addr_fmt == AF_P2SH
|
||||
digest = hash160(script)
|
||||
spk = b'\xA9\x14' + digest + b'\x87'
|
||||
|
||||
if addr_fmt & AFC_SEGWIT:
|
||||
digest = ngu.hash.sha256s(witdeem_script)
|
||||
else:
|
||||
digest = hash160(witdeem_script)
|
||||
assert pubkey
|
||||
keyhash = ngu.hash.hash160(pubkey)
|
||||
if addr_fmt == AF_P2TR:
|
||||
assert len(pubkey) == 32 # internal
|
||||
spk = b'\x51\x20' + taptweak(pubkey)
|
||||
elif 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)
|
||||
|
||||
if addr_fmt & AFC_BECH32:
|
||||
# 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
|
||||
addr = ngu.codecs.b58_encode(cls.b58_script + digest)
|
||||
|
||||
return addr
|
||||
return spk, digest
|
||||
|
||||
@classmethod
|
||||
def pubkey_to_address(cls, pubkey, addr_fmt):
|
||||
# - 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):
|
||||
# return a human-readable, properly formatted address
|
||||
if addr_fmt == AF_P2TR:
|
||||
xo_pk = node.pubkey()[1:]
|
||||
return ngu.codecs.segwit_encode(cls.bech32_hrp, 1, taptweak(xo_pk))
|
||||
|
||||
if addr_fmt == AF_CLASSIC:
|
||||
# olde fashioned P2PKH
|
||||
@ -134,7 +150,7 @@ class ChainsBase:
|
||||
return node.addr_help(cls.b58_addr[0])
|
||||
|
||||
if addr_fmt & AFC_SCRIPT:
|
||||
# use p2sh_address() instead.
|
||||
# use chain.render_address
|
||||
raise ValueError(hex(addr_fmt))
|
||||
|
||||
# so must be P2PKH, fetch it.
|
||||
@ -161,7 +177,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())
|
||||
@ -242,37 +258,37 @@ class ChainsBase:
|
||||
|
||||
@classmethod
|
||||
def op_return(cls, script):
|
||||
"""Returns decoded string op return data if script is op return otherwise None"""
|
||||
# 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
|
||||
if OP_RETURN not in script_type:
|
||||
return
|
||||
|
||||
try:
|
||||
data = next(gen)[0]
|
||||
if data:
|
||||
return data
|
||||
except StopIteration:
|
||||
pass
|
||||
|
||||
return b""
|
||||
|
||||
@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...
|
||||
hrp = cls.bech32_hrp + "1"
|
||||
if addr.startswith(hrp):
|
||||
if addr.startswith(hrp+'p'):
|
||||
# segwit v1 (any ver=1 script or address, but for now just taproot...)
|
||||
return AF_P2TR
|
||||
else:
|
||||
elif addr.startswith(hrp+'q'):
|
||||
# segwit v0
|
||||
return AF_P2WPKH if len(addr) < 55 else AF_P2WSH
|
||||
|
||||
return 0
|
||||
|
||||
try:
|
||||
raw = ngu.codecs.b58_decode(addr)
|
||||
except ValueError:
|
||||
@ -290,8 +306,8 @@ class ChainsBase:
|
||||
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 = 939464 # Mar 5/2026
|
||||
|
||||
slip132 = {
|
||||
AF_CLASSIC: Slip132Version(0x0488B21E, 0x0488ADE4, 'x'),
|
||||
@ -299,6 +315,7 @@ class BitcoinMain(ChainsBase):
|
||||
AF_P2WPKH: Slip132Version(0x04b24746, 0x04b2430c, 'z'),
|
||||
AF_P2WSH_P2SH: Slip132Version(0x0295b43f, 0x0295b005, 'Y'),
|
||||
AF_P2WSH: Slip132Version(0x02aa7ed3, 0x02aa7a99, 'Z'),
|
||||
AF_P2TR: Slip132Version(0x0488B21E, 0x0488ADE4, 'x'),
|
||||
}
|
||||
|
||||
bech32_hrp = 'bc'
|
||||
@ -309,10 +326,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'),
|
||||
@ -320,6 +337,7 @@ class BitcoinTestnet(BitcoinMain):
|
||||
AF_P2WPKH: Slip132Version(0x045f1cf6, 0x045f18bc, 'v'),
|
||||
AF_P2WSH_P2SH: Slip132Version(0x024289ef, 0x024285b5, 'U'),
|
||||
AF_P2WSH: Slip132Version(0x02575483, 0x02575048, 'V'),
|
||||
AF_P2TR: Slip132Version(0x043587cf, 0x04358394, 't'),
|
||||
}
|
||||
|
||||
bech32_hrp = 'tb'
|
||||
@ -331,27 +349,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'
|
||||
@ -376,10 +378,17 @@ def current_chain():
|
||||
|
||||
return get_chain(chain)
|
||||
|
||||
def current_key_chain():
|
||||
c = current_chain()
|
||||
if c == BitcoinRegtest:
|
||||
# regtest has same extended keys as testnet
|
||||
c = BitcoinTestnet
|
||||
return c
|
||||
|
||||
# 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)
|
||||
@ -403,8 +412,82 @@ CommonDerivations = [
|
||||
AF_P2WPKH_P2SH ), # generates 3xxx/2xxx p2sh-looking addresses
|
||||
( 'BIP-84 (Native Segwit P2WPKH)', "m/84h/{coin_type}h/{account}h/{change}/{idx}",
|
||||
AF_P2WPKH ), # generates bc1 bech32 addresses
|
||||
('BIP-86 (Taproot Segwit P2TR)', "m/86h/{coin_type}h/{account}h/{change}/{idx}",
|
||||
AF_P2TR), # generates bc1p bech32m addresses
|
||||
]
|
||||
|
||||
STD_DERIVATIONS = {
|
||||
"p2pkh": CommonDerivations[0][1],
|
||||
"p2sh-p2wpkh": CommonDerivations[1][1],
|
||||
"p2wpkh-p2sh": CommonDerivations[1][1],
|
||||
"p2wpkh": CommonDerivations[2][1],
|
||||
"p2tr": CommonDerivations[3][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),
|
||||
('p2tr', "m/48h/{coin}h/{acct_num}h/3h", AF_P2TR),
|
||||
}
|
||||
|
||||
AF_TO_STR_AF = {
|
||||
AF_BARE_PK: "p2pk",
|
||||
AF_CLASSIC: "p2pkh",
|
||||
AF_P2TR: "p2tr",
|
||||
AF_P2WPKH: "p2wpkh",
|
||||
AF_P2WPKH_P2SH: "p2sh-p2wpkh",
|
||||
AF_P2SH: "p2sh",
|
||||
AF_P2WSH: "p2wsh",
|
||||
AF_P2WSH_P2SH: "p2sh-p2wsh",
|
||||
}
|
||||
|
||||
def parse_addr_fmt_str(addr_fmt):
|
||||
# accepts strings and also integers if already parsed
|
||||
# integers are coming from USB
|
||||
try:
|
||||
if isinstance(addr_fmt, int):
|
||||
if addr_fmt in [AF_P2WPKH_P2SH, AF_P2WPKH, AF_CLASSIC]:
|
||||
return addr_fmt
|
||||
else:
|
||||
try:
|
||||
addr_fmt = AF_TO_STR_AF[addr_fmt] # just for error msg
|
||||
except: pass
|
||||
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
|
||||
elif addr_fmt == "p2tr":
|
||||
return AF_P2TR
|
||||
else:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
raise ValueError("Unsupported address format: '%s'" % 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,
|
||||
AF_P2TR: 86}[addr_fmt]
|
||||
|
||||
def addr_fmt_label(addr_fmt):
|
||||
return {
|
||||
AF_CLASSIC: "Classic P2PKH",
|
||||
AF_P2WPKH_P2SH: "P2SH-Segwit",
|
||||
AF_P2WPKH: "Segwit P2WPKH",
|
||||
AF_P2TR: "Taproot P2TR",
|
||||
AF_P2WSH: "Segwit P2WSH",
|
||||
AF_P2WSH_P2SH: "P2SH-P2WSH",
|
||||
AF_P2SH: "Legacy P2SH",
|
||||
}[addr_fmt]
|
||||
|
||||
def verify_recover_pubkey(sig, digest):
|
||||
# verifies a message digest against a signature and recovers
|
||||
|
||||
@ -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
|
||||
|
||||
@ -198,7 +198,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
|
||||
|
||||
@ -315,7 +315,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)
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
#
|
||||
# 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
|
||||
@ -59,17 +59,9 @@ 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()
|
||||
@ -101,7 +93,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:
|
||||
@ -131,7 +123,23 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
|
||||
pass
|
||||
|
||||
elif ty == 'J':
|
||||
return 'json', (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 +167,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)
|
||||
|
||||
@ -178,6 +196,9 @@ def decode_short_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))
|
||||
@ -194,25 +215,9 @@ def decode_short_text(got):
|
||||
# was something else.
|
||||
pass
|
||||
|
||||
# multisig descriptor
|
||||
# multi( catches both multi( and sortedmulti(
|
||||
if ("multi(" in got):
|
||||
return 'multi', (got,)
|
||||
|
||||
if ("\n" in got) and ('pub' in got):
|
||||
# legacy multisig import/export format
|
||||
# [0-9a-fA-F]{8}\s*:\s*[xtyYzZuUvV]pub[1-9A-HJ-NP-Za-km-z]{107}
|
||||
# above is more precise BUT counted repetitions not supported in mpy
|
||||
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)
|
||||
c = 0 # match count
|
||||
for l in got.split("\n"):
|
||||
if rgx.search(l):
|
||||
c += 1
|
||||
if c > 1:
|
||||
return 'multi', (got,)
|
||||
from descriptor import Descriptor
|
||||
if Descriptor.is_descriptor(got):
|
||||
return 'minisc', (got,)
|
||||
|
||||
# Things with newlines in them are not URL's
|
||||
# - working URLs are not >4k
|
||||
|
||||
620
shared/desc_utils.py
Normal file
620
shared/desc_utils.py
Normal file
@ -0,0 +1,620 @@
|
||||
# (c) Copyright 2020 by Stepan Snigirev, see <https://github.com/diybitcoinhardware/embit/blob/master/LICENSE>
|
||||
#
|
||||
# Changes (c) Copyright 2023 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
import ngu, chains, ustruct, stash
|
||||
from io import BytesIO
|
||||
from public_constants import MAX_PATH_DEPTH
|
||||
from binascii import unhexlify as a2b_hex
|
||||
from binascii import hexlify as b2a_hex
|
||||
from utils import keypath_to_str, str_to_keypath, swab32, xfp2str
|
||||
from serializations import ser_compact_size
|
||||
|
||||
|
||||
WILDCARD = "*"
|
||||
PROVABLY_UNSPENDABLE = b'\x02P\x92\x9bt\xc1\xa0IT\xb7\x8bK`5\xe9z^\x07\x8aZ\x0f(\xec\x96\xd5G\xbf\xee\x9a\xce\x80:\xc0'
|
||||
|
||||
# sha256(b"MuSig2MuSig2MuSig2")
|
||||
MUSIG_CHAIN_CODE = b'\x86\x80\x87\xca\x02\xa6\xf9t\xc4Y\x89$\xc3kWv-2\xcbEqqg\xe3\x00b,qg\xe3\x89e'
|
||||
|
||||
INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
|
||||
CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||
|
||||
|
||||
def polymod(c, val):
|
||||
c0 = c >> 35
|
||||
c = ((c & 0x7ffffffff) << 5) ^ val
|
||||
if (c0 & 1):
|
||||
c ^= 0xf5dee51989
|
||||
if (c0 & 2):
|
||||
c ^= 0xa9fdca3312
|
||||
if (c0 & 4):
|
||||
c ^= 0x1bab10e32d
|
||||
if (c0 & 8):
|
||||
c ^= 0x3706b1677a
|
||||
if (c0 & 16):
|
||||
c ^= 0x644d626ffd
|
||||
|
||||
return c
|
||||
|
||||
def descriptor_checksum(desc):
|
||||
c = 1
|
||||
cls = 0
|
||||
clscount = 0
|
||||
for ch in desc:
|
||||
pos = INPUT_CHARSET.find(ch)
|
||||
if pos == -1:
|
||||
raise ValueError(ch)
|
||||
|
||||
c = polymod(c, pos & 31)
|
||||
cls = cls * 3 + (pos >> 5)
|
||||
clscount += 1
|
||||
if clscount == 3:
|
||||
c = polymod(c, cls)
|
||||
cls = 0
|
||||
clscount = 0
|
||||
|
||||
if clscount > 0:
|
||||
c = polymod(c, cls)
|
||||
for j in range(0, 8):
|
||||
c = polymod(c, 0)
|
||||
c ^= 1
|
||||
|
||||
rv = ''
|
||||
for j in range(0, 8):
|
||||
rv += CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31]
|
||||
|
||||
return rv
|
||||
|
||||
def append_checksum(desc):
|
||||
return desc + "#" + descriptor_checksum(desc)
|
||||
|
||||
|
||||
def parse_desc_str(string):
|
||||
"""Remove comments, empty lines and strip line. Produce single line string"""
|
||||
res = ""
|
||||
for l in string.split("\n"):
|
||||
strip_l = l.strip()
|
||||
if not strip_l:
|
||||
continue
|
||||
if strip_l.startswith("#"):
|
||||
continue
|
||||
res += strip_l
|
||||
return res
|
||||
|
||||
|
||||
def read_until(s, chars=b",)(#"):
|
||||
res = b""
|
||||
while True:
|
||||
chunk = s.read(1)
|
||||
if len(chunk) == 0:
|
||||
return res, None
|
||||
if chunk in chars:
|
||||
return res, chunk
|
||||
res += chunk
|
||||
|
||||
|
||||
def musig_synthetic_node(agg_pk_bytes):
|
||||
assert len(agg_pk_bytes) == 33 # need non-xonly pubkey
|
||||
node = ngu.hdnode.HDNode()
|
||||
node.from_chaincode_pubkey(MUSIG_CHAIN_CODE, agg_pk_bytes)
|
||||
return node
|
||||
|
||||
|
||||
class KeyOriginInfo:
|
||||
def __init__(self, fingerprint: bytes, derivation: list, cc_fp=None):
|
||||
self.fingerprint = fingerprint
|
||||
self.derivation = derivation
|
||||
self._cc_fp = cc_fp
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.psbt_derivation() == other.psbt_derivation()
|
||||
|
||||
def __hash__(self):
|
||||
return hash(tuple(self.psbt_derivation()))
|
||||
|
||||
@property
|
||||
def cc_fp(self):
|
||||
if self._cc_fp is None:
|
||||
self._cc_fp = ustruct.unpack('<I', self.fingerprint)[0]
|
||||
return self._cc_fp
|
||||
|
||||
def str_derivation(self):
|
||||
return keypath_to_str(self.derivation, prefix='m/', skip=0)
|
||||
|
||||
def psbt_derivation(self):
|
||||
res = [self.cc_fp]
|
||||
for i in self.derivation:
|
||||
res.append(i)
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, s: str):
|
||||
arr = s.split("/")
|
||||
xfp = a2b_hex(arr[0])
|
||||
assert len(xfp) == 4
|
||||
arr[0] = "m"
|
||||
path = "/".join(arr)
|
||||
derivation = str_to_keypath(xfp, path)[1:] # ignoring xfp here, already stored
|
||||
assert len(derivation) <= MAX_PATH_DEPTH, "origin too deep"
|
||||
return cls(xfp, derivation)
|
||||
|
||||
def __str__(self):
|
||||
rv = "%s" % b2a_hex(self.fingerprint).decode()
|
||||
if self.derivation:
|
||||
rv += "/%s" % keypath_to_str(self.derivation, prefix='', skip=0)
|
||||
return rv
|
||||
|
||||
|
||||
class KeyDerivationInfo:
|
||||
|
||||
def __init__(self, indexes=None):
|
||||
self.indexes = indexes
|
||||
if self.indexes is None:
|
||||
self.indexes = ((0, 1), WILDCARD)
|
||||
self.multi_path_index = 0
|
||||
else:
|
||||
self.multi_path_index = None
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.indexes)
|
||||
|
||||
@staticmethod
|
||||
def not_hardened(x):
|
||||
assert (b"'" not in x) and (b"h" not in x), "Cannot use hardened sub derivation path"
|
||||
|
||||
def get_ext_int(self):
|
||||
return self.indexes[self.multi_path_index]
|
||||
|
||||
@classmethod
|
||||
def parse(cls, s):
|
||||
err = "Malformed key derivation"
|
||||
multi_i = None
|
||||
idxs = []
|
||||
while True:
|
||||
got, char = read_until(s, b"<,)/")
|
||||
if char == b"<":
|
||||
assert multi_i is None, "too many multipaths"
|
||||
ext_num, char = read_until(s, b";")
|
||||
assert char, err
|
||||
cls.not_hardened(ext_num)
|
||||
int_num, char = read_until(s, b">")
|
||||
assert char, err
|
||||
assert b";" not in int_num, "Solved cardinality > 2"
|
||||
cls.not_hardened(int_num)
|
||||
|
||||
assert int_num != ext_num # cannot be the same
|
||||
multi_i = len(idxs)
|
||||
idxs.append((int(ext_num.decode()), int(int_num.decode())))
|
||||
|
||||
else:
|
||||
# char in "/),"
|
||||
if got == b"*":
|
||||
# every derivation has to end with wildcard (only ranged keys allowed)
|
||||
idxs.append(WILDCARD)
|
||||
break
|
||||
elif got:
|
||||
cls.not_hardened(got)
|
||||
idxs.append(int(got.decode()))
|
||||
|
||||
# comma and parenthesis not allowed in subderivation, marker of the end
|
||||
if char in b",)": break
|
||||
|
||||
assert idxs[-1] == WILDCARD, "All keys must be ranged"
|
||||
if idxs == [0, WILDCARD]:
|
||||
# normalize and instead save as <0;1> as change derivation was not provided
|
||||
obj = cls()
|
||||
else:
|
||||
|
||||
assert multi_i is not None, "need multipath"
|
||||
assert len(idxs[multi_i]) == 2, "wrong multipath"
|
||||
|
||||
obj = cls(tuple(idxs))
|
||||
obj.multi_path_index = multi_i
|
||||
|
||||
return obj
|
||||
|
||||
def to_string(self, external=True, internal=True):
|
||||
res = []
|
||||
for i in self.indexes:
|
||||
if isinstance(i, tuple):
|
||||
if internal is True and external is False:
|
||||
i = str(i[1])
|
||||
elif internal is False and external is True:
|
||||
i = str(i[0])
|
||||
else:
|
||||
i = "<%d;%d>" % (i[0], i[1])
|
||||
else:
|
||||
i = str(i)
|
||||
res.append(i)
|
||||
return "/".join(res)
|
||||
|
||||
def der_index(self, idx, change=False):
|
||||
if isinstance(idx, list):
|
||||
for i in idx:
|
||||
mp_i = self.multi_path_index or 0
|
||||
if i in self.indexes[mp_i]:
|
||||
idx = i
|
||||
break
|
||||
else:
|
||||
assert False
|
||||
|
||||
elif idx is None:
|
||||
# derive according to key subderivation if any
|
||||
if self is None:
|
||||
idx = 1 if change else 0
|
||||
else:
|
||||
if self.multi_path_index is not None:
|
||||
ext, inter = self.indexes[self.multi_path_index]
|
||||
idx = inter if change else ext
|
||||
|
||||
return idx
|
||||
|
||||
|
||||
class ExtendedKey:
|
||||
def __init__(self, node, origin, derivation=None, taproot=False, chain_type=None):
|
||||
self.origin = origin
|
||||
self.node = node
|
||||
self.derivation = derivation or KeyDerivationInfo()
|
||||
self.taproot = taproot
|
||||
self.chain_type = chain_type
|
||||
|
||||
def __eq__(self, other):
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.node.pubkey()) + hash(self.derivation)
|
||||
|
||||
def __len__(self):
|
||||
return 34 - int(self.taproot) # <33:sec> or <32:xonly>
|
||||
|
||||
@property
|
||||
def fingerprint(self):
|
||||
return self.origin.fingerprint
|
||||
|
||||
def serialize(self):
|
||||
return self.key_bytes()
|
||||
|
||||
def compile(self):
|
||||
d = self.serialize()
|
||||
return ser_compact_size(len(d)) + d
|
||||
|
||||
@classmethod
|
||||
def parse_key(cls, key_str):
|
||||
assert key_str[1:4].lower() == b"pub", "only extended pubkeys allowed"
|
||||
# extended key
|
||||
# or xpub or tpub as we use descriptors (SLIP-132 NOT allowed)
|
||||
hint = key_str[0:1].lower()
|
||||
if hint == b"x":
|
||||
chain_type = "BTC"
|
||||
elif hint == b"t":
|
||||
chain_type = "XTN"
|
||||
else:
|
||||
# slip (ignore any implied address format)
|
||||
chain_type = "BTC" if hint in b"yz" else "XTN"
|
||||
|
||||
node = ngu.hdnode.HDNode()
|
||||
node.deserialize(key_str)
|
||||
try:
|
||||
assert node.privkey() is None, "no privkeys"
|
||||
except ValueError:
|
||||
# ValueError is thrown from libngu if key is public
|
||||
pass
|
||||
|
||||
return node, chain_type
|
||||
|
||||
def validate(self, my_xfp, disable_checks=False):
|
||||
assert self.chain_type == chains.current_key_chain().ctype, "wrong chain"
|
||||
|
||||
# xfp is always available, even if key was serialized without origin info
|
||||
# upon parse root origin info is generated from key itself
|
||||
xfp = self.origin.cc_fp
|
||||
is_mine = (xfp == my_xfp)
|
||||
|
||||
# raises ValueError on invalid pubkey (should be in libngu)
|
||||
# invalid public key not allowed even with disable checks
|
||||
ngu.secp256k1.pubkey(self.node.pubkey())
|
||||
|
||||
if not disable_checks:
|
||||
depth = self.node.depth()
|
||||
# we now allow blinded keys that have depth X but derivation len is 0,
|
||||
# where only fingerprint constitutes key origin
|
||||
# only check if derivation length is greater than 0
|
||||
if self.origin.derivation:
|
||||
assert len(self.origin.derivation) == depth, \
|
||||
"deriv len != xpub depth (xfp=%s)" % xfp2str(xfp)
|
||||
if depth == 0:
|
||||
# blinded keys allowed
|
||||
# assert not self.node.parent_fp()
|
||||
# assert self.node.child_number()[0] == 0
|
||||
assert swab32(self.node.my_fp()) == xfp, "master xfp mismatch"
|
||||
elif depth == 1:
|
||||
target = swab32(self.node.parent_fp())
|
||||
assert xfp == target, 'xfp depth=1 wrong'
|
||||
|
||||
if is_mine:
|
||||
# it's supposed to be my key, so I should be able to generate pubkey
|
||||
# - might indicate collision on xfp value between co-signers,
|
||||
# and that's not supported
|
||||
deriv = self.origin.str_derivation()
|
||||
with stash.SensitiveValues() as sv:
|
||||
chk_node = sv.derive_path(deriv)
|
||||
assert self.node.pubkey() == chk_node.pubkey(), \
|
||||
"[%s/%s] wrong pubkey" % (xfp2str(xfp), deriv[2:])
|
||||
|
||||
return is_mine
|
||||
|
||||
def derive(self, idx=None, change=False):
|
||||
if self.derivation:
|
||||
idx = self.derivation.der_index(idx, change)
|
||||
else:
|
||||
assert idx
|
||||
|
||||
new_node = self.node.copy()
|
||||
new_node.derive(idx, False)
|
||||
if self.origin:
|
||||
origin = KeyOriginInfo(self.origin.fingerprint, self.origin.derivation + [idx],
|
||||
self.origin.cc_fp)
|
||||
else:
|
||||
origin = KeyOriginInfo(self.origin.fingerprint, [idx], self.origin.cc_fp)
|
||||
|
||||
new_der = None
|
||||
if self.derivation:
|
||||
new_der = KeyDerivationInfo(self.derivation.indexes[1:])
|
||||
|
||||
return type(self)(new_node, origin, new_der, taproot=self.taproot)
|
||||
|
||||
@classmethod
|
||||
def read_from(cls, s, taproot=False, musig=False):
|
||||
first = s.read(1)
|
||||
origin = None
|
||||
|
||||
if first == b"[":
|
||||
prefix, char = read_until(s, b"]")
|
||||
if char != b"]":
|
||||
raise ValueError("Invalid key - missing ] in key origin info")
|
||||
origin = KeyOriginInfo.from_string(prefix.decode())
|
||||
else:
|
||||
s.seek(-1, 1)
|
||||
|
||||
k, char = read_until(s, b",)/")
|
||||
if musig and char not in b",)":
|
||||
assert b"musig(" not in k, "nested musig not allowed"
|
||||
assert char != b"/", "key derivation not allowed inside musig"
|
||||
|
||||
der = None
|
||||
if char == b"/":
|
||||
der = KeyDerivationInfo.parse(s)
|
||||
if char is not None:
|
||||
s.seek(-1, 1)
|
||||
|
||||
# parse key
|
||||
node, chain_type = cls.parse_key(k)
|
||||
if origin is None:
|
||||
cc_fp = swab32(node.my_fp())
|
||||
origin = KeyOriginInfo(ustruct.pack('<I', cc_fp), [], cc_fp)
|
||||
return cls(node, origin, der, chain_type=chain_type, taproot=taproot)
|
||||
|
||||
@classmethod
|
||||
def from_cc_data(cls, xfp, deriv, xpub):
|
||||
xfp_str = xfp if isinstance(xfp, str) else xfp2str(xfp)
|
||||
koi = KeyOriginInfo.from_string("%s/%s" % (xfp_str, deriv.replace("m/", "")))
|
||||
node, chain_type = cls.parse_key(xpub.encode())
|
||||
|
||||
return cls(node, koi, KeyDerivationInfo(), chain_type=chain_type)
|
||||
|
||||
@classmethod
|
||||
def from_cc_json(cls, vals, af_str):
|
||||
key_exp = af_str + "_key_exp"
|
||||
if key_exp in vals:
|
||||
# new firmware, prefer key expression
|
||||
return cls.from_string(vals[key_exp])
|
||||
|
||||
# TODO
|
||||
node, _, _, _ = chains.slip132_deserialize(vals[af_str])
|
||||
ek = chains.current_chain().serialize_public(node)
|
||||
return cls.from_cc_data(vals["xfp"], vals["%s_deriv" % af_str], ek)
|
||||
|
||||
@classmethod
|
||||
def from_psbt_xpub(cls, ek_bytes, xfp_path):
|
||||
xfp, *path = xfp_path
|
||||
koi = KeyOriginInfo(a2b_hex(xfp2str(xfp)), path)
|
||||
# TODO this should be done by C code, no need to base58 encode/decode
|
||||
# byte-serialized key should be decodable
|
||||
ek = ngu.codecs.b58_encode(ek_bytes)
|
||||
node, chain_type = cls.parse_key(ek.encode())
|
||||
|
||||
return cls(node, koi, KeyDerivationInfo(), chain_type=chain_type)
|
||||
|
||||
@property
|
||||
def is_provably_unspendable(self):
|
||||
if PROVABLY_UNSPENDABLE == self.node.pubkey():
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def prefix(self):
|
||||
if self.origin and self.origin.derivation:
|
||||
return "[%s]" % self.origin
|
||||
# jut a bare [xfp]key - omit origin info (jut xfp)
|
||||
# or no origin at all
|
||||
return ""
|
||||
|
||||
def key_bytes(self):
|
||||
kb = self.node.pubkey()
|
||||
if self.taproot:
|
||||
# xonly
|
||||
kb = kb[1:]
|
||||
return kb
|
||||
|
||||
def extended_public_key(self):
|
||||
return chains.current_chain().serialize_public(self.node)
|
||||
|
||||
def to_string(self, external=True, internal=True):
|
||||
key = self.prefix
|
||||
key += self.extended_public_key()
|
||||
if self.derivation and (external or internal):
|
||||
key += "/" + self.derivation.to_string(external, internal)
|
||||
|
||||
return key
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, s):
|
||||
s = BytesIO(s.encode())
|
||||
return cls.read_from(s)
|
||||
|
||||
|
||||
class MusigKey:
|
||||
def __init__(self, keys, der=None, node=None):
|
||||
self.keys = keys
|
||||
self.derivation = der or KeyDerivationInfo()
|
||||
self._node = node
|
||||
|
||||
def __len__(self):
|
||||
return 33 # length + <32:xonly>
|
||||
|
||||
def __eq__(self, other):
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.node.pubkey()) + hash(self.derivation)
|
||||
|
||||
def serialize(self):
|
||||
return self.key_bytes()
|
||||
|
||||
def compile(self):
|
||||
d = self.serialize()
|
||||
return ser_compact_size(len(d)) + d
|
||||
|
||||
@property
|
||||
def node(self):
|
||||
if self._node is None:
|
||||
self._node = musig_synthetic_node(self.aggregate_pubkey().to_bytes())
|
||||
return self._node
|
||||
|
||||
def validate(self, my_xfp, disable_checks=False):
|
||||
has_mine = 0
|
||||
for k in self.keys:
|
||||
assert not k.is_provably_unspendable, "unspendable key inside musig"
|
||||
if k.validate(my_xfp, disable_checks):
|
||||
has_mine += 1
|
||||
|
||||
assert len(self.keys) == len(set(self.keys)), "musig keys not unique"
|
||||
assert has_mine <= 1, "multiple own keys in musig"
|
||||
return has_mine
|
||||
|
||||
def key_bytes(self):
|
||||
return ngu.secp256k1.pubkey(self.node.pubkey()).to_xonly().to_bytes()
|
||||
|
||||
def aggregate_pubkey(self):
|
||||
keyagg_cache = ngu.secp256k1.MusigKeyAggCache()
|
||||
secp_pubkeys = [ngu.secp256k1.pubkey(k.node.pubkey()) for k in self.keys]
|
||||
ngu.secp256k1.musig_pubkey_agg(secp_pubkeys, keyagg_cache)
|
||||
return keyagg_cache.agg_pubkey()
|
||||
|
||||
def to_string(self, external=True, internal=True):
|
||||
base = "musig(%s)" % (",".join([k.to_string(False, False) for k in self.keys]))
|
||||
base += "/" + self.derivation.to_string(external, internal)
|
||||
return base
|
||||
|
||||
def derive(self, idx=None, change=False):
|
||||
idx = self.derivation.der_index(idx, change)
|
||||
new_node = self.node.copy()
|
||||
new_node.derive(idx, False)
|
||||
|
||||
return type(self)(self.keys, KeyDerivationInfo(self.derivation.indexes[1:]),
|
||||
node=new_node)
|
||||
|
||||
@property
|
||||
def is_provably_unspendable(self):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def read_from(cls, s, taproot=True):
|
||||
assert taproot, "musig in non-taproot context"
|
||||
assert s.read(6) == b"musig(", "not musig()"
|
||||
|
||||
der = None
|
||||
keys = []
|
||||
while True:
|
||||
k = ExtendedKey.read_from(s, taproot=taproot, musig=True)
|
||||
k.der = None
|
||||
k.taproot = taproot
|
||||
# already verified that no der present in keys
|
||||
k.derivation = None
|
||||
keys.append(k)
|
||||
c = s.read(1)
|
||||
if c == b")":
|
||||
sep = s.read(1)
|
||||
if sep == b"/":
|
||||
der = KeyDerivationInfo.parse(s)
|
||||
|
||||
s.seek(-1, 1)
|
||||
break
|
||||
|
||||
assert c == b","
|
||||
|
||||
return cls(keys, der)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, s):
|
||||
s = BytesIO(s.encode())
|
||||
return cls.read_from(s)
|
||||
|
||||
|
||||
class KeyExpression:
|
||||
@classmethod
|
||||
def read_from(cls, s, taproot=False):
|
||||
is_musig = (s.read(6) == b"musig(")
|
||||
s.seek(-6, 1)
|
||||
if is_musig:
|
||||
return MusigKey.read_from(s, taproot=taproot)
|
||||
else:
|
||||
return ExtendedKey.read_from(s, taproot=taproot)
|
||||
|
||||
|
||||
def bip388_wallet_policy_to_descriptor(desc_tmplt, keys_info):
|
||||
for i in range(len(keys_info) - 1, -1, -1):
|
||||
k_str = keys_info[i]
|
||||
ph = "@%d" % i
|
||||
desc_tmplt = desc_tmplt.replace(ph, k_str)
|
||||
return desc_tmplt.replace("/**", "/<0;1>/*")
|
||||
|
||||
|
||||
def bip388_validate_policy(desc_tmplt, keys_info):
|
||||
s = BytesIO(desc_tmplt)
|
||||
r = []
|
||||
while True:
|
||||
g1, char = read_until(s, b"@")
|
||||
if not char:
|
||||
# no more - done
|
||||
break
|
||||
|
||||
# key derivation info required for policy
|
||||
g2, char = read_until(s, b"/")
|
||||
assert char, "key derivation missing"
|
||||
if g1.endswith(b"musig("):
|
||||
# key derivations not allowed inside musig
|
||||
assert b"/" not in g2
|
||||
assert g2[-1:] == b")"
|
||||
|
||||
for i, num in enumerate(g2[:-1].split(b",")):
|
||||
if i:
|
||||
# 0th element has @ already removed
|
||||
assert num[0:1] == b"@"
|
||||
num = num[1:]
|
||||
|
||||
num = int(num.decode())
|
||||
if num not in r:
|
||||
r.append(num)
|
||||
|
||||
else:
|
||||
num = int(g2.decode())
|
||||
if num not in r:
|
||||
r.append(num)
|
||||
|
||||
assert s.read(1) in b"<*", "need multipath"
|
||||
|
||||
|
||||
assert len(r) == len(keys_info), "Invalid policy"
|
||||
assert r == list(range(len(r))), "Out of order"
|
||||
@ -1,252 +1,333 @@
|
||||
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
# (c) Copyright 2020 by Stepan Snigirev, see <https://github.com/diybitcoinhardware/embit/blob/master/LICENSE>
|
||||
#
|
||||
# descriptor.py - Bitcoin Core's descriptors and their specialized checksums.
|
||||
# Changes (c) Copyright 2023 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
# Based on: https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp
|
||||
#
|
||||
from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH
|
||||
|
||||
MULTI_FMT_TO_SCRIPT = {
|
||||
AF_P2SH: "sh(%s)",
|
||||
AF_P2WSH_P2SH: "sh(wsh(%s))",
|
||||
AF_P2WSH: "wsh(%s)",
|
||||
None: "wsh(%s)",
|
||||
# hack for tests
|
||||
"p2sh": "sh(%s)",
|
||||
"p2sh-p2wsh": "sh(wsh(%s))",
|
||||
"p2wsh-p2sh": "sh(wsh(%s))",
|
||||
"p2wsh": "wsh(%s)",
|
||||
}
|
||||
|
||||
SINGLE_FMT_TO_SCRIPT = {
|
||||
AF_P2WPKH: "wpkh(%s)",
|
||||
AF_CLASSIC: "pkh(%s)",
|
||||
AF_P2WPKH_P2SH: "sh(wpkh(%s))",
|
||||
None: "wpkh(%s)",
|
||||
"p2pkh": "pkh(%s)",
|
||||
"p2wpkh": "wpkh(%s)",
|
||||
"p2sh-p2wpkh": "sh(wpkh(%s))",
|
||||
"p2wpkh-p2sh": "sh(wpkh(%s))",
|
||||
}
|
||||
|
||||
INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
|
||||
CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||
|
||||
try:
|
||||
from utils import xfp2str, str2xfp
|
||||
except ModuleNotFoundError:
|
||||
import struct
|
||||
from binascii import unhexlify as a2b_hex
|
||||
from binascii import hexlify as b2a_hex
|
||||
# assuming not micro python
|
||||
def xfp2str(xfp):
|
||||
# Standardized way to show an xpub's fingerprint... it's a 4-byte string
|
||||
# and not really an integer. Used to show as '0x%08x' but that's wrong endian.
|
||||
return b2a_hex(struct.pack('<I', xfp)).decode().upper()
|
||||
|
||||
def str2xfp(txt):
|
||||
# Inverse of xfp2str
|
||||
return struct.unpack('<I', a2b_hex(txt))[0]
|
||||
import ngu, chains
|
||||
from io import BytesIO
|
||||
from collections import OrderedDict
|
||||
from utils import xfp2str, swab32
|
||||
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR
|
||||
from public_constants import AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH, MAX_TR_SIGNERS
|
||||
from desc_utils import (parse_desc_str, append_checksum, descriptor_checksum,
|
||||
KeyExpression, ExtendedKey, MusigKey)
|
||||
from miniscript import Miniscript
|
||||
from precomp_tag_hash import TAP_BRANCH_H
|
||||
|
||||
|
||||
class WrongCheckSumError(Exception):
|
||||
pass
|
||||
class Tapscript:
|
||||
def __init__(self, tree):
|
||||
self.tree = tree # miniscript or (tapscript, tapscript)
|
||||
self._merkle_root = None
|
||||
self._processed_tree = None
|
||||
|
||||
def iter_leaves(self):
|
||||
if isinstance(self.tree, Miniscript):
|
||||
yield self.tree
|
||||
else:
|
||||
for ts in self.tree:
|
||||
yield from ts.iter_leaves()
|
||||
|
||||
def polymod(c, val):
|
||||
c0 = c >> 35
|
||||
c = ((c & 0x7ffffffff) << 5) ^ val
|
||||
if (c0 & 1):
|
||||
c ^= 0xf5dee51989
|
||||
if (c0 & 2):
|
||||
c ^= 0xa9fdca3312
|
||||
if (c0 & 4):
|
||||
c ^= 0x1bab10e32d
|
||||
if (c0 & 8):
|
||||
c ^= 0x3706b1677a
|
||||
if (c0 & 16):
|
||||
c ^= 0x644d626ffd
|
||||
@property
|
||||
def merkle_root(self):
|
||||
if not self._merkle_root:
|
||||
self._processed_tree, self._merkle_root = self.process_tree()
|
||||
return self._merkle_root
|
||||
|
||||
return c
|
||||
def derive(self, idx, key_map, change=False):
|
||||
if isinstance(self.tree, Miniscript):
|
||||
tree = self.tree.derive(idx, key_map, change=change)
|
||||
else:
|
||||
l, r = self.tree
|
||||
tree = [l.derive(idx, key_map, change=change),
|
||||
r.derive(idx, key_map, change=change)]
|
||||
|
||||
def descriptor_checksum(desc):
|
||||
c = 1
|
||||
cls = 0
|
||||
clscount = 0
|
||||
for ch in desc:
|
||||
pos = INPUT_CHARSET.find(ch)
|
||||
if pos == -1:
|
||||
raise ValueError(ch)
|
||||
return type(self)(tree)
|
||||
|
||||
c = polymod(c, pos & 31)
|
||||
cls = cls * 3 + (pos >> 5)
|
||||
clscount += 1
|
||||
if clscount == 3:
|
||||
c = polymod(c, cls)
|
||||
cls = 0
|
||||
clscount = 0
|
||||
def process_tree(self):
|
||||
if isinstance(self.tree, Miniscript):
|
||||
script = self.tree.compile()
|
||||
h = chains.tapleaf_hash(script)
|
||||
return [(chains.TAPROOT_LEAF_TAPSCRIPT, script, bytes())], h
|
||||
|
||||
if clscount > 0:
|
||||
c = polymod(c, cls)
|
||||
for j in range(0, 8):
|
||||
c = polymod(c, 0)
|
||||
c ^= 1
|
||||
l, r = self.tree
|
||||
left, left_h = l.process_tree()
|
||||
right, right_h = r.process_tree()
|
||||
left = [(version, script, control + right_h) for version, script, control in left]
|
||||
right = [(version, script, control + left_h) for version, script, control in right]
|
||||
if right_h < left_h:
|
||||
right_h, left_h = left_h, right_h
|
||||
|
||||
rv = ''
|
||||
for j in range(0, 8):
|
||||
rv += CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31]
|
||||
h = ngu.hash.sha256t(TAP_BRANCH_H, left_h + right_h, True)
|
||||
return left + right, h
|
||||
|
||||
return rv
|
||||
# UNUSED - using above proces tree cached result to dump scripts to CSV
|
||||
# def script_tree(self):
|
||||
# if isinstance(self.tree, Miniscript):
|
||||
# return b2a_hex(chains.tapscript_serialize(self.tree.compile())).decode()
|
||||
#
|
||||
# l, r = self.tree
|
||||
# return "{" + l.script_tree() + "," +r.script_tree() + "}"
|
||||
|
||||
def append_checksum(desc):
|
||||
return desc + "#" + descriptor_checksum(desc)
|
||||
@classmethod
|
||||
def read_from(cls, s):
|
||||
c = s.read(1)
|
||||
assert len(c)
|
||||
if c == b"{": # more than one miniscript
|
||||
left = cls.read_from(s)
|
||||
c = s.read(1)
|
||||
if c == b"}":
|
||||
return left
|
||||
if c != b",":
|
||||
raise ValueError("Invalid tapscript: expected ','")
|
||||
|
||||
right = cls.read_from(s)
|
||||
if s.read(1) != b"}":
|
||||
raise ValueError("Invalid tapscript: expected '}'")
|
||||
|
||||
def parse_desc_str(string):
|
||||
"""Remove comments, empty lines and strip line. Produce single line string"""
|
||||
res = ""
|
||||
for l in string.split("\n"):
|
||||
strip_l = l.strip()
|
||||
if not strip_l:
|
||||
continue
|
||||
if strip_l.startswith("#"):
|
||||
continue
|
||||
res += strip_l
|
||||
return res
|
||||
return cls((left, right))
|
||||
|
||||
s.seek(-1, 1)
|
||||
ms = Miniscript.read_from(s, taproot=True)
|
||||
return cls(ms)
|
||||
|
||||
def multisig_descriptor_template(xpub, path, xfp, addr_fmt):
|
||||
key_exp = "[%s%s]%s/0/*" % (xfp.lower(), path.replace("m", ''), xpub)
|
||||
if addr_fmt == AF_P2WSH_P2SH:
|
||||
descriptor_template = "sh(wsh(sortedmulti(M,%s,...)))"
|
||||
elif addr_fmt == AF_P2WSH:
|
||||
descriptor_template = "wsh(sortedmulti(M,%s,...))"
|
||||
elif addr_fmt == AF_P2SH:
|
||||
descriptor_template = "sh(sortedmulti(M,%s,...))"
|
||||
else:
|
||||
return None
|
||||
descriptor_template = descriptor_template % key_exp
|
||||
return descriptor_template
|
||||
def to_string(self, external=True, internal=True):
|
||||
if isinstance(self.tree, Miniscript):
|
||||
return self.tree.to_string(external, internal)
|
||||
|
||||
l, r = self.tree
|
||||
return ("{" + l.to_string(external,internal) + ","
|
||||
+ r.to_string(external, internal) + "}")
|
||||
|
||||
|
||||
class Descriptor:
|
||||
__slots__ = (
|
||||
"keys",
|
||||
"addr_fmt",
|
||||
)
|
||||
def __init__(self, key=None, miniscript=None, tapscript=None, addr_fmt=None, keys=None):
|
||||
if addr_fmt in [AF_P2SH, AF_P2WSH, AF_P2WSH_P2SH]:
|
||||
assert miniscript
|
||||
assert not key
|
||||
else:
|
||||
# single-sig + taproot/tapscript
|
||||
assert miniscript is None
|
||||
assert key
|
||||
|
||||
def __init__(self, keys, addr_fmt):
|
||||
self.keys = keys
|
||||
self.key = key
|
||||
self.miniscript = miniscript
|
||||
self.tapscript = tapscript
|
||||
self.addr_fmt = addr_fmt
|
||||
# cached keys
|
||||
self._keys = keys
|
||||
|
||||
@staticmethod
|
||||
def checksum_check(desc_w_checksum , csum_required=False):
|
||||
try:
|
||||
desc, checksum = desc_w_checksum.split("#")
|
||||
except ValueError:
|
||||
if csum_required:
|
||||
raise ValueError("Missing descriptor checksum")
|
||||
return desc_w_checksum, None
|
||||
calc_checksum = descriptor_checksum(desc)
|
||||
if calc_checksum != checksum:
|
||||
raise WrongCheckSumError("Wrong checksum %s, expected %s" % (checksum, calc_checksum))
|
||||
return desc, checksum
|
||||
def validate(self, disable_checks=False):
|
||||
# should only be run once while importing wallet
|
||||
from glob import settings
|
||||
|
||||
@staticmethod
|
||||
def parse_key_orig_info(key):
|
||||
# key origin info is required for our MultisigWallet
|
||||
close_index = key.find("]")
|
||||
if key[0] != "[" or close_index == -1:
|
||||
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
|
||||
c = 0
|
||||
has_mine = 0
|
||||
err_top_B = "Top level miniscript should be 'B'"
|
||||
max_signers = 20
|
||||
|
||||
@staticmethod
|
||||
def parse_key_derivation_info(key):
|
||||
invalid_subderiv_msg = "Invalid subderivation path - only 0/* or <0;1>/* allowed"
|
||||
slash_split = key.split("/")
|
||||
assert len(slash_split) > 1, invalid_subderiv_msg
|
||||
if all(["h" not in elem and "'" not in elem for elem in slash_split[1:]]):
|
||||
assert slash_split[-1] == "*", invalid_subderiv_msg
|
||||
assert slash_split[-2] in ["0", "<0;1>", "<1;0>"], invalid_subderiv_msg
|
||||
assert len(slash_split[1:]) == 2, invalid_subderiv_msg
|
||||
return slash_split[0]
|
||||
else:
|
||||
raise ValueError("Cannot use hardened sub derivation path")
|
||||
if self.tapscript:
|
||||
assert self.key # internal key (would fail during parse)
|
||||
max_signers = MAX_TR_SIGNERS
|
||||
for l in self.tapscript.iter_leaves():
|
||||
assert l.type == "B", err_top_B
|
||||
l.verify()
|
||||
l.is_sane(taproot=True)
|
||||
# cannot have same keys in single miniscript
|
||||
# provably unspendable taproot internal key is not covered here
|
||||
assert len(l.keys) == len(set(l.keys)), "Insane"
|
||||
|
||||
def checksum(self):
|
||||
return descriptor_checksum(self._serialize())
|
||||
elif self.miniscript:
|
||||
assert self.key is None
|
||||
assert self.miniscript.type == "B", err_top_B
|
||||
self.miniscript.verify()
|
||||
self.miniscript.is_sane(taproot=False)
|
||||
# cannot have same keys in single miniscript
|
||||
assert len(self.miniscript.keys) == len(set(self.miniscript.keys)), "Insane"
|
||||
|
||||
def serialize_keys(self, internal=False, int_ext=False):
|
||||
result = []
|
||||
for xfp, deriv, xpub in self.keys:
|
||||
if deriv[0] == "m":
|
||||
# get rid of 'm'
|
||||
deriv = deriv[1:]
|
||||
elif deriv[0] != "/":
|
||||
# input "84'/0'/0'" would lack slash separtor with xfp
|
||||
deriv = "/" + deriv
|
||||
if not isinstance(xfp, str):
|
||||
xfp = xfp2str(xfp)
|
||||
koi = xfp + deriv
|
||||
# normalize xpub to use h for hardened instead of '
|
||||
key_str = "[%s]%s" % (koi.lower(), xpub)
|
||||
if int_ext:
|
||||
key_str = key_str + "/" + "<0;1>" + "/" + "*"
|
||||
my_xfp = settings.get('xfp', 0)
|
||||
ext_nums = set()
|
||||
int_nums = set()
|
||||
for k in self.keys:
|
||||
has_mine += k.validate(my_xfp, disable_checks)
|
||||
ext, int = k.derivation.get_ext_int()
|
||||
ext_nums.add(ext)
|
||||
int_nums.add(int)
|
||||
c += 1
|
||||
|
||||
if not self.tapscript and not self.is_basic_multisig:
|
||||
# this is non-taproot Miniscript
|
||||
# Miniscript expressions can only be used in wsh or tr.
|
||||
assert self.addr_fmt != AF_P2SH, "Miniscript in legacy P2SH not allowed"
|
||||
|
||||
assert ext_nums.isdisjoint(int_nums), "Non-disjoint multipath"
|
||||
assert c <= max_signers, "max signers"
|
||||
|
||||
assert has_mine > 0, 'My key %s missing in descriptor.' % xfp2str(my_xfp).upper()
|
||||
|
||||
def bip388_wallet_policy(self):
|
||||
# Return compact descriptor (BIP-388 style) template and key info
|
||||
# - only same origin keys
|
||||
keys_info = OrderedDict()
|
||||
|
||||
for k in self.keys:
|
||||
ks = k.keys if isinstance(k, MusigKey) else [k]
|
||||
|
||||
for kk in ks:
|
||||
pk = kk.node.pubkey()
|
||||
if pk not in keys_info:
|
||||
keys_info[pk] = kk.to_string(external=False, internal=False)
|
||||
|
||||
desc_tmplt = self.to_string(checksum=False).replace("/<0;1>/*", "/**")
|
||||
|
||||
keys_info = list(keys_info.values())
|
||||
for i, k_str in enumerate(keys_info):
|
||||
desc_tmplt = desc_tmplt.replace(k_str, '@%d' % i)
|
||||
|
||||
return desc_tmplt, keys_info
|
||||
|
||||
@property
|
||||
def script_len(self):
|
||||
if self.is_taproot:
|
||||
return 34 # OP_1 <32:xonly>
|
||||
if self.miniscript:
|
||||
return len(self.miniscript)
|
||||
if self.addr_fmt == AF_P2WPKH:
|
||||
return 22 # 00 <20:pkh>
|
||||
return 25 # OP_DUP OP_HASH160 <20:pkh> OP_EQUALVERIFY OP_CHECKSIG
|
||||
|
||||
def xfp_paths(self, skip_unspend_ik=False):
|
||||
res = []
|
||||
for k in self.keys:
|
||||
if self.is_taproot and k.is_provably_unspendable and skip_unspend_ik:
|
||||
continue
|
||||
|
||||
if isinstance(k, MusigKey):
|
||||
agg_k = [swab32(k.node.my_fp())]
|
||||
# even if dupes - add
|
||||
res.append(agg_k)
|
||||
|
||||
for kk in k.keys:
|
||||
psbt_der = kk.origin.psbt_derivation()
|
||||
if psbt_der not in res:
|
||||
res.append(psbt_der)
|
||||
else:
|
||||
key_str = key_str + "/" + "/".join(["1", "*"] if internal else ["0", "*"])
|
||||
result.append(key_str.replace("'", "h"))
|
||||
return result
|
||||
res.append(k.origin.psbt_derivation())
|
||||
|
||||
def _serialize(self, internal=False, int_ext=False):
|
||||
"""Serialize without checksum"""
|
||||
assert len(self.keys) == 1 # "Multiple keys for single signature script"
|
||||
desc_base = SINGLE_FMT_TO_SCRIPT[self.addr_fmt]
|
||||
inner = self.serialize_keys(internal=internal, int_ext=int_ext)[0]
|
||||
return desc_base % (inner)
|
||||
return res
|
||||
|
||||
def serialize(self, internal=False, int_ext=False):
|
||||
"""Serialize with checksum"""
|
||||
return append_checksum(self._serialize(internal=internal, int_ext=int_ext))
|
||||
@property
|
||||
def is_segwit_v0(self):
|
||||
return self.addr_fmt in [AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2WSH, AF_P2WSH_P2SH]
|
||||
|
||||
@classmethod
|
||||
def parse(cls, desc_w_checksum):
|
||||
# remove garbage
|
||||
desc_w_checksum = parse_desc_str(desc_w_checksum)
|
||||
# check correct checksum
|
||||
desc, checksum = cls.checksum_check(desc_w_checksum)
|
||||
# legacy
|
||||
if desc.startswith("pkh("):
|
||||
addr_fmt = AF_CLASSIC
|
||||
tmp_desc = desc.replace("pkh(", "")
|
||||
tmp_desc = tmp_desc.rstrip(")")
|
||||
@property
|
||||
def is_segwit(self):
|
||||
return self.is_taproot or self.is_segwit_v0
|
||||
|
||||
# native segwit
|
||||
elif desc.startswith("wpkh("):
|
||||
addr_fmt = AF_P2WPKH
|
||||
tmp_desc = desc.replace("wpkh(", "")
|
||||
tmp_desc = tmp_desc.rstrip(")")
|
||||
@property
|
||||
def is_taproot(self):
|
||||
return self.addr_fmt == AF_P2TR
|
||||
|
||||
# wrapped segwit
|
||||
elif desc.startswith("sh(wpkh("):
|
||||
addr_fmt = AF_P2WPKH_P2SH
|
||||
tmp_desc = desc.replace("sh(wpkh(", "")
|
||||
tmp_desc = tmp_desc.rstrip("))")
|
||||
@property
|
||||
def is_legacy_sh(self):
|
||||
return self.addr_fmt in [AF_P2SH, AF_P2WSH_P2SH, AF_P2WPKH_P2SH]
|
||||
|
||||
@property
|
||||
def is_basic_multisig(self):
|
||||
return self.miniscript and self.miniscript.NAME in ["multi", "sortedmulti"]
|
||||
|
||||
@property
|
||||
def is_sortedmulti(self):
|
||||
return self.is_basic_multisig and self.miniscript.NAME == "sortedmulti"
|
||||
|
||||
@property
|
||||
def keys(self):
|
||||
if self._keys:
|
||||
return self._keys
|
||||
|
||||
if self.is_taproot:
|
||||
# internal is always first
|
||||
# use ordered dict as order preserving set
|
||||
keys = OrderedDict()
|
||||
# add internal key (whether musig or not)
|
||||
keys[self.key] = None
|
||||
|
||||
if self.tapscript:
|
||||
# taptree keys
|
||||
for lv in self.tapscript.iter_leaves():
|
||||
for k in lv.keys:
|
||||
keys[k] = None
|
||||
|
||||
self._keys = list(keys)
|
||||
|
||||
elif self.miniscript:
|
||||
self._keys = self.miniscript.keys
|
||||
|
||||
else:
|
||||
raise ValueError("Unsupported descriptor. Supported: pkh(), wpkh(), sh(wpkh()).")
|
||||
# single-sig
|
||||
self._keys = [self.key]
|
||||
|
||||
koi, key = cls.parse_key_orig_info(tmp_desc)
|
||||
if key[0:4] not in ["tpub", "xpub"]:
|
||||
raise ValueError("Only extended public keys are supported")
|
||||
return self._keys
|
||||
|
||||
xpub = cls.parse_key_derivation_info(key)
|
||||
xfp = str2xfp(koi[:8])
|
||||
origin_deriv = "m" + koi[8:]
|
||||
def derive(self, idx=None, change=False):
|
||||
if self.is_taproot:
|
||||
# derive keys first
|
||||
# duplicate keys can be may be found in different leaves
|
||||
# use map to derive each key just once
|
||||
derived_keys = OrderedDict()
|
||||
for i, k in enumerate(self.keys):
|
||||
if not isinstance(k, MusigKey):
|
||||
dk = k.derive(idx, change=change)
|
||||
dk.taproot = self.is_taproot
|
||||
derived_keys[k] = dk
|
||||
|
||||
return cls(keys=[(xfp, origin_deriv, xpub)], addr_fmt=addr_fmt)
|
||||
derived_tapsript = None
|
||||
if self.tapscript:
|
||||
derived_tapsript = self.tapscript.derive(idx, derived_keys, change=change)
|
||||
|
||||
return type(self)(self.key.derive(idx, change=change),
|
||||
tapscript=derived_tapsript, addr_fmt=self.addr_fmt,
|
||||
keys=list(derived_keys.values()))
|
||||
|
||||
if self.miniscript:
|
||||
return type(self)(
|
||||
None,
|
||||
self.miniscript.derive(idx, change=change),
|
||||
addr_fmt=self.addr_fmt,
|
||||
)
|
||||
|
||||
# single-sig
|
||||
return type(self)(self.key.derive(idx, change=change))
|
||||
|
||||
def script_pubkey(self, compiled_scr=None):
|
||||
if self.is_taproot:
|
||||
tweak = None
|
||||
if self.tapscript:
|
||||
tweak = self.tapscript.merkle_root
|
||||
output_pubkey = chains.taptweak(self.key.serialize(), tweak)
|
||||
return b"\x51\x20" + output_pubkey
|
||||
|
||||
if self.is_legacy_sh:
|
||||
if self.miniscript:
|
||||
# caller may have already built a script
|
||||
scr = compiled_scr or self.miniscript.compile()
|
||||
redeem_scr = scr
|
||||
if self.addr_fmt == AF_P2WSH_P2SH:
|
||||
redeem_scr = b"\x00\x20" + ngu.hash.sha256s(scr)
|
||||
else:
|
||||
redeem_scr = b"\x00\x14" + ngu.hash.hash160(self.key.node.pubkey())
|
||||
|
||||
return b"\xa9\x14" + ngu.hash.hash160(redeem_scr) + b"\x87"
|
||||
|
||||
if self.addr_fmt == AF_P2WSH:
|
||||
# witness script p2wsh only
|
||||
return b"\x00\x20" + ngu.hash.sha256s(compiled_scr or self.miniscript.compile())
|
||||
|
||||
if self.addr_fmt == AF_P2WPKH:
|
||||
return b"\x00\x14" + ngu.hash.hash160(self.key.serialize())
|
||||
|
||||
# p2pkh
|
||||
assert self.addr_fmt == AF_CLASSIC
|
||||
return b"\x76\xa9\x14" + ngu.hash.hash160(self.key.serialize()) + b"\x88\xac"
|
||||
|
||||
@classmethod
|
||||
def is_descriptor(cls, desc_str):
|
||||
@ -267,142 +348,131 @@ class Descriptor:
|
||||
return True
|
||||
return False
|
||||
|
||||
def bitcoin_core_serialize(self, external_label=None):
|
||||
@staticmethod
|
||||
def checksum_check(desc_w_checksum, csum_required=False):
|
||||
try:
|
||||
desc, checksum = desc_w_checksum.split("#")
|
||||
except ValueError:
|
||||
if csum_required:
|
||||
raise ValueError("Missing descriptor checksum")
|
||||
return desc_w_checksum, None
|
||||
calc_checksum = descriptor_checksum(desc)
|
||||
if calc_checksum != checksum:
|
||||
raise ValueError("Wrong checksum %s, expected %s" % (checksum, calc_checksum))
|
||||
return desc, checksum
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, desc, checksum=False):
|
||||
desc = parse_desc_str(desc)
|
||||
desc, cs = cls.checksum_check(desc)
|
||||
s = BytesIO(desc.encode())
|
||||
res = cls.read_from(s)
|
||||
left = s.read()
|
||||
if len(left) > 0:
|
||||
raise ValueError("Unexpected characters after descriptor: %r" % left)
|
||||
if checksum:
|
||||
if cs is None:
|
||||
_, cs = res.to_string().split("#")
|
||||
return res, cs
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def read_from(cls, s):
|
||||
start = s.read(8)
|
||||
af = AF_CLASSIC
|
||||
internal_key = None
|
||||
tapscript = None
|
||||
if start.startswith(b"tr("):
|
||||
af = AF_P2TR
|
||||
s.seek(-5, 1)
|
||||
internal_key = KeyExpression.read_from(s, taproot=True)
|
||||
sep = s.read(1)
|
||||
if sep == b")":
|
||||
s.seek(-1, 1)
|
||||
else:
|
||||
assert sep == b","
|
||||
tapscript = Tapscript.read_from(s)
|
||||
|
||||
elif start.startswith(b"sh(wsh("):
|
||||
af = AF_P2WSH_P2SH
|
||||
s.seek(-1, 1)
|
||||
elif start.startswith(b"wsh("):
|
||||
af = AF_P2WSH
|
||||
s.seek(-4, 1)
|
||||
elif start.startswith(b"sh(wpkh("):
|
||||
af = AF_P2WPKH_P2SH
|
||||
elif start.startswith(b"wpkh("):
|
||||
af = AF_P2WPKH
|
||||
s.seek(-3, 1)
|
||||
elif start.startswith(b"pkh("):
|
||||
s.seek(-4, 1)
|
||||
elif start.startswith(b"sh("):
|
||||
af = AF_P2SH
|
||||
s.seek(-5, 1)
|
||||
else:
|
||||
raise ValueError("Invalid descriptor")
|
||||
|
||||
miniscript = None
|
||||
if af == AF_P2TR:
|
||||
key = internal_key
|
||||
nbrackets = 1
|
||||
elif af in [AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH]:
|
||||
miniscript = Miniscript.read_from(s)
|
||||
key = internal_key
|
||||
nbrackets = 1 + int(af == AF_P2WSH_P2SH)
|
||||
else:
|
||||
key = ExtendedKey.read_from(s, taproot=False)
|
||||
nbrackets = 1 + int(af == AF_P2WPKH_P2SH)
|
||||
|
||||
end = s.read(nbrackets)
|
||||
if end != b")" * nbrackets:
|
||||
raise ValueError("Invalid descriptor")
|
||||
|
||||
desc = cls(key, miniscript, tapscript, af)
|
||||
return desc
|
||||
|
||||
def to_string(self, external=True, internal=True, checksum=True):
|
||||
if self.is_taproot:
|
||||
desc = "tr(%s" % self.key.to_string(external, internal)
|
||||
if self.tapscript:
|
||||
desc += ","
|
||||
tree = self.tapscript.to_string(external, internal)
|
||||
desc += tree
|
||||
|
||||
res = desc + ")"
|
||||
|
||||
else:
|
||||
if self.miniscript is not None:
|
||||
res = self.miniscript.to_string(external, internal)
|
||||
if self.addr_fmt in [AF_P2WSH, AF_P2WSH_P2SH]:
|
||||
res = "wsh(%s)" % res
|
||||
else:
|
||||
if self.addr_fmt in [AF_P2WPKH, AF_P2WPKH_P2SH]:
|
||||
res = "wpkh(%s)" % self.key.to_string(external, internal)
|
||||
else:
|
||||
res = "pkh(%s)" % self.key.to_string(external, internal)
|
||||
|
||||
if self.is_legacy_sh:
|
||||
res = "sh(%s)" % res
|
||||
|
||||
if checksum:
|
||||
res = append_checksum(res)
|
||||
return res
|
||||
|
||||
def bitcoin_core_serialize(self):
|
||||
# this will become legacy one day
|
||||
# instead use <0;1> descriptor format
|
||||
res = []
|
||||
for internal in [False, True]:
|
||||
for external in (True, False):
|
||||
desc_obj = {
|
||||
"desc": self.serialize(internal=internal),
|
||||
"desc": self.to_string(external, not external),
|
||||
"active": True,
|
||||
"timestamp": "now",
|
||||
"internal": internal,
|
||||
"internal": not external,
|
||||
"range": [0, 100],
|
||||
}
|
||||
if internal is False and external_label:
|
||||
desc_obj["label"] = external_label
|
||||
res.append(desc_obj)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
class MultisigDescriptor(Descriptor):
|
||||
# only supprt with key derivation info
|
||||
# only xpubs
|
||||
# can be extended when needed
|
||||
__slots__ = (
|
||||
"M",
|
||||
"N",
|
||||
"keys",
|
||||
"addr_fmt",
|
||||
"is_sorted" # whether to use sortedmulti() or multi()
|
||||
)
|
||||
|
||||
def __init__(self, M, N, keys, addr_fmt, is_sorted=True):
|
||||
self.M = M
|
||||
self.N = N
|
||||
self.is_sorted = is_sorted
|
||||
super().__init__(keys, addr_fmt)
|
||||
|
||||
@classmethod
|
||||
def parse(cls, desc_w_checksum):
|
||||
# remove garbage
|
||||
desc_w_checksum = parse_desc_str(desc_w_checksum)
|
||||
# check correct checksum
|
||||
desc, checksum = cls.checksum_check(desc_w_checksum)
|
||||
is_sorted = "sortedmulti(" in desc
|
||||
rplc = "sortedmulti(" if is_sorted else "multi("
|
||||
|
||||
# wrapped segwit
|
||||
if desc.startswith("sh(wsh("+rplc):
|
||||
addr_fmt = AF_P2WSH_P2SH
|
||||
tmp_desc = desc.replace("sh(wsh("+rplc, "")
|
||||
tmp_desc = tmp_desc.rstrip(")))")
|
||||
|
||||
# native segwit
|
||||
elif desc.startswith("wsh("+rplc):
|
||||
addr_fmt = AF_P2WSH
|
||||
tmp_desc = desc.replace("wsh("+rplc, "")
|
||||
tmp_desc = tmp_desc.rstrip("))")
|
||||
|
||||
# legacy
|
||||
elif desc.startswith("sh("+rplc):
|
||||
addr_fmt = AF_P2SH
|
||||
tmp_desc = desc.replace("sh("+rplc, "")
|
||||
tmp_desc = tmp_desc.rstrip("))")
|
||||
|
||||
else:
|
||||
raise ValueError("Unsupported descriptor. Supported: sh(), sh(wsh()), wsh().")
|
||||
|
||||
splitted = tmp_desc.split(",")
|
||||
M, keys = int(splitted[0]), splitted[1:]
|
||||
N = int(len(keys))
|
||||
if M > N:
|
||||
raise ValueError("M must be <= N: got M=%d and N=%d" % (M, N))
|
||||
|
||||
res_keys = []
|
||||
for key in keys:
|
||||
koi, key = cls.parse_key_orig_info(key)
|
||||
if key[0:4] not in ["tpub", "xpub"]:
|
||||
raise ValueError("Only extended public keys are supported")
|
||||
|
||||
xpub = cls.parse_key_derivation_info(key)
|
||||
xfp = str2xfp(koi[:8])
|
||||
origin_deriv = "m" + koi[8:]
|
||||
res_keys.append((xfp, origin_deriv, xpub))
|
||||
|
||||
return cls(M=M, N=N, keys=res_keys, addr_fmt=addr_fmt, is_sorted=is_sorted)
|
||||
|
||||
def _serialize(self, internal=False, int_ext=False):
|
||||
"""Serialize without checksum"""
|
||||
desc_base = MULTI_FMT_TO_SCRIPT[self.addr_fmt]
|
||||
_type = "sortedmulti" if self.is_sorted else "multi"
|
||||
_type += "(%s)"
|
||||
desc_base = desc_base % _type
|
||||
assert len(self.keys) == self.N
|
||||
inner = str(self.M) + "," + ",".join(
|
||||
self.serialize_keys(internal=internal, int_ext=int_ext))
|
||||
|
||||
return desc_base % (inner)
|
||||
|
||||
def pretty_serialize(self):
|
||||
"""Serialize in pretty and human-readable format"""
|
||||
_type = "sortedmulti" if self.is_sorted else "multi"
|
||||
res = "# Coldcard descriptor export\n"
|
||||
if self.is_sorted:
|
||||
res += "# order of keys in the descriptor does not matter, will be sorted before creating script (BIP-67)\n"
|
||||
else:
|
||||
res += ("# !!! DANGER: order of keys in descriptor MUST be preserved. "
|
||||
"Correct order of keys is required to compose valid redeem/witness script.\n")
|
||||
if self.addr_fmt == AF_P2SH:
|
||||
res += "# bare multisig - p2sh\n"
|
||||
res += "sh("+_type+"(\n%s\n))"
|
||||
# native segwit
|
||||
elif self.addr_fmt == AF_P2WSH:
|
||||
res += "# native segwit - p2wsh\n"
|
||||
res += "wsh("+_type+"(\n%s\n))"
|
||||
|
||||
# wrapped segwit
|
||||
elif self.addr_fmt == AF_P2WSH_P2SH:
|
||||
res += "# wrapped segwit - p2sh-p2wsh\n"
|
||||
res += "sh(wsh(" + _type + "(\n%s\n)))"
|
||||
else:
|
||||
raise ValueError("Malformed descriptor")
|
||||
|
||||
assert len(self.keys) == self.N
|
||||
inner = "\t" + "# %d of %d (%s)\n" % (
|
||||
self.M, self.N,
|
||||
"requires all participants to sign" if self.M == self.N else "threshold")
|
||||
inner += "\t" + str(self.M) + ",\n"
|
||||
ser_keys = self.serialize_keys()
|
||||
for i, key_str in enumerate(ser_keys, start=1):
|
||||
if i == self.N:
|
||||
inner += "\t" + key_str
|
||||
else:
|
||||
inner += "\t" + key_str + ",\n"
|
||||
|
||||
checksum = self.serialize().split("#")[1]
|
||||
|
||||
return (res % inner) + "#" + checksum
|
||||
|
||||
# EOF
|
||||
|
||||
@ -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,19 +144,28 @@ 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)
|
||||
self.text(-2, 35, 'V', font=FontTiny, invert=1)
|
||||
elif version.is_edge:
|
||||
self.dis.fill_rect(128 - 6, 19, 5, 26, 1)
|
||||
self.text(-2, 20, 'E', font=FontTiny, invert=1)
|
||||
self.text(-2, 27, 'D', font=FontTiny, invert=1)
|
||||
self.text(-2, 33, 'G', font=FontTiny, invert=1)
|
||||
self.text(-2, 39, 'E', font=FontTiny, invert=1)
|
||||
|
||||
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 +212,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 +236,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 +276,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 +299,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 +357,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 +384,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)
|
||||
|
||||
@ -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)
|
||||
@ -161,7 +161,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 +180,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 +205,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 +222,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 +241,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)
|
||||
|
||||
@ -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
|
||||
|
||||
394
shared/export.py
394
shared/export.py
@ -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, problem_file_line
|
||||
from ux import ux_show_story, import_export_prompt
|
||||
from glob import settings
|
||||
from auth import write_sig_file
|
||||
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH
|
||||
from msgsign import write_sig_file
|
||||
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH, AF_P2TR
|
||||
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,82 @@ 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)
|
||||
|
||||
if addr_fmt == AF_P2TR:
|
||||
sig = None
|
||||
else:
|
||||
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%s\n%s' % (e, problem_file_line(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 +151,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 +174,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 not in (AF_CLASSIC, AF_P2TR) and (addr_fmt in chain.slip132):
|
||||
yield ("%s => %s ##SLIP-132##\n" % (
|
||||
hard_sub, chain.serialize_public(node, addr_fmt)))
|
||||
|
||||
@ -120,59 +191,13 @@ be needed for different systems.
|
||||
|
||||
yield ('\n\n')
|
||||
|
||||
from multisig import MultisigWallet
|
||||
if MultisigWallet.exists():
|
||||
yield '\n# Your Multisig Wallets\n\n'
|
||||
from wallet import MiniScriptWallet
|
||||
if MiniScriptWallet.exists():
|
||||
yield '\n# Your Multisig/Miniscript Wallets\n\n'
|
||||
|
||||
for ms in MultisigWallet.get_all():
|
||||
fp = StringIO()
|
||||
for msc in MiniScriptWallet.iter_wallets():
|
||||
yield msc.to_string() + "\n---\n"
|
||||
|
||||
ms.render_export(fp)
|
||||
print("\n---\n", file=fp)
|
||||
|
||||
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 +208,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)
|
||||
|
||||
@ -195,10 +220,11 @@ async def make_bitcoin_core_wallet(account_num=0, fname_pattern='bitcoin-core.tx
|
||||
|
||||
# make the data
|
||||
examples = []
|
||||
imp_multi, imp_desc = generate_bitcoin_core_wallet(account_num, examples)
|
||||
imp_multi, imp_desc, imp_desc_tr = generate_bitcoin_core_wallet(account_num, examples)
|
||||
|
||||
imp_multi = ujson.dumps(imp_multi)
|
||||
imp_desc = ujson.dumps(imp_desc)
|
||||
imp_desc_tr = ujson.dumps(imp_desc_tr)
|
||||
|
||||
body = '''\
|
||||
# Bitcoin Core Wallet Import File
|
||||
@ -214,7 +240,10 @@ Wallet operates on blockchain: {nb}
|
||||
The following command can be entered after opening Window -> Console
|
||||
in Bitcoin Core, or using bitcoin-cli:
|
||||
|
||||
importdescriptors '{imp_desc}'
|
||||
p2wpkh:
|
||||
importdescriptors '{imp_desc}'
|
||||
p2tr:
|
||||
importdescriptors '{imp_desc_tr}'
|
||||
|
||||
> **NOTE** If your UTXO was created before generating `importdescriptors` command, you should adjust the value of `timestamp` before executing command in bitcoin core.
|
||||
By default it is set to `now` meaning do not rescan the blockchain. If approximate time of UTXO creation is known - adjust `timestamp` from `now` to UNIX epoch time.
|
||||
@ -229,59 +258,80 @@ importmulti '{imp_multi}'
|
||||
|
||||
## Resulting Addresses (first 3)
|
||||
|
||||
'''.format(imp_multi=imp_multi, imp_desc=imp_desc, xfp=xfp, nb=chains.current_chain().name)
|
||||
'''.format(imp_multi=imp_multi, imp_desc=imp_desc, imp_desc_tr=imp_desc_tr,
|
||||
xfp=xfp, nb=chains.current_chain().name)
|
||||
|
||||
body += '\n'.join('%s => %s' % t for t in examples)
|
||||
|
||||
body += '\n'
|
||||
|
||||
OWNERSHIP.note_wallet_used(AF_P2WPKH, account_num)
|
||||
OWNERSHIP.note_wallet_used(AF_P2TR, account_num)
|
||||
|
||||
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
|
||||
# - yields dicts for json purposes
|
||||
from descriptor import Descriptor
|
||||
from descriptor import Descriptor, ExtendedKey
|
||||
|
||||
chain = chains.current_chain()
|
||||
|
||||
derive = "84h/{coin_type}h/{account}h".format(account=account_num,
|
||||
coin_type=chain.b44_cointype)
|
||||
|
||||
derive_v0 = "84h/{coin_type}h/{account}h".format(
|
||||
account=account_num, coin_type=chain.b44_cointype
|
||||
)
|
||||
derive_v1 = "86h/{coin_type}h/{account}h".format(
|
||||
account=account_num, coin_type=chain.b44_cointype
|
||||
)
|
||||
with stash.SensitiveValues() as sv:
|
||||
prefix = sv.derive_path(derive)
|
||||
xpub = chain.serialize_public(prefix)
|
||||
prefix = sv.derive_path(derive_v0)
|
||||
xpub_v0 = chain.serialize_public(prefix)
|
||||
|
||||
for i in range(3):
|
||||
sp = '0/%d' % i
|
||||
node = sv.derive_path(sp, master=prefix)
|
||||
a = chain.address(node, AF_P2WPKH)
|
||||
example_addrs.append( ('m/%s/%s' % (derive, sp), a) )
|
||||
example_addrs.append(('m/%s/%s' % (derive_v0, sp), a))
|
||||
|
||||
with stash.SensitiveValues() as sv:
|
||||
prefix = sv.derive_path(derive_v1)
|
||||
xpub_v1 = chain.serialize_public(prefix)
|
||||
|
||||
for i in range(3):
|
||||
sp = '0/%d' % i
|
||||
node = sv.derive_path(sp, master=prefix)
|
||||
a = chain.address(node, AF_P2TR)
|
||||
example_addrs.append(('m/%s/%s' % (derive_v1, sp), a))
|
||||
|
||||
xfp = settings.get('xfp')
|
||||
_, vers, _ = version.get_mpy_version()
|
||||
key0 = ExtendedKey.from_cc_data(xfp, derive_v0, xpub_v0)
|
||||
desc_v0 = Descriptor(key=key0, addr_fmt=AF_P2WPKH)
|
||||
|
||||
key1 = ExtendedKey.from_cc_data(xfp, derive_v1, xpub_v1)
|
||||
desc_v1 = Descriptor(key=key1, addr_fmt=AF_P2TR)
|
||||
|
||||
OWNERSHIP.note_wallet_used(AF_P2WPKH, account_num)
|
||||
OWNERSHIP.note_wallet_used(AF_P2TR, account_num)
|
||||
|
||||
desc_obj = Descriptor(keys=[(xfp, derive, xpub)], addr_fmt=AF_P2WPKH)
|
||||
# for importmulti
|
||||
imm_list = [
|
||||
{
|
||||
'desc': desc_obj.serialize(internal=internal),
|
||||
'desc': desc_v0.to_string(external, internal),
|
||||
'range': [0, 1000],
|
||||
'timestamp': 'now',
|
||||
'internal': internal,
|
||||
'keypool': True,
|
||||
'watchonly': True
|
||||
}
|
||||
for internal in [False, True]
|
||||
for external, internal in [(True, False), (False, True)]
|
||||
]
|
||||
# for importdescriptors
|
||||
imd_list = desc_obj.bitcoin_core_serialize()
|
||||
return imm_list, imd_list
|
||||
imd_list = desc_v0.bitcoin_core_serialize()
|
||||
imd_list_v1 = desc_v1.bitcoin_core_serialize()
|
||||
return imm_list, imd_list, imd_list_v1
|
||||
|
||||
|
||||
def generate_wasabi_wallet():
|
||||
# Generate the data for a JSON file which Wasabi can open directly as a new wallet.
|
||||
@ -319,20 +369,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,13 +387,11 @@ 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)
|
||||
from descriptor import Descriptor, multisig_descriptor_template
|
||||
from descriptor import Descriptor, ExtendedKey
|
||||
|
||||
chain = chains.current_chain()
|
||||
master_xfp = settings.get("xfp")
|
||||
@ -361,12 +405,14 @@ def generate_generic_export(account_num=0):
|
||||
with stash.SensitiveValues() as sv:
|
||||
# each of these paths would have /{change}/{idx} in usage (not hardened)
|
||||
for name, deriv, fmt, atype, is_ms in [
|
||||
( 'bip44', "m/44h/{ct}h/{acc}h", AF_CLASSIC, 'p2pkh', False ),
|
||||
( 'bip49', "m/49h/{ct}h/{acc}h", AF_P2WPKH_P2SH, 'p2sh-p2wpkh', False ), # was "p2wpkh-p2sh"
|
||||
( 'bip84', "m/84h/{ct}h/{acc}h", AF_P2WPKH, 'p2wpkh', False ),
|
||||
( 'bip48_1', "m/48h/{ct}h/{acc}h/1h", AF_P2WSH_P2SH, 'p2sh-p2wsh', True ),
|
||||
( 'bip48_2', "m/48h/{ct}h/{acc}h/2h", AF_P2WSH, 'p2wsh', True ),
|
||||
( 'bip45', "m/45h", AF_P2SH, 'p2sh', True ),
|
||||
('bip44', "m/44h/{ct}h/{acc}h", AF_CLASSIC, 'p2pkh', False),
|
||||
('bip49', "m/49h/{ct}h/{acc}h", AF_P2WPKH_P2SH, 'p2sh-p2wpkh', False), # was "p2wpkh-p2sh"
|
||||
('bip84', "m/84h/{ct}h/{acc}h", AF_P2WPKH, 'p2wpkh', False),
|
||||
('bip86', "m/86h/{ct}h/{acc}h", AF_P2TR, 'p2tr', False),
|
||||
('bip48_1', "m/48h/{ct}h/{acc}h/1h", AF_P2WSH_P2SH, 'p2sh-p2wsh', True),
|
||||
('bip48_2', "m/48h/{ct}h/{acc}h/2h", AF_P2WSH, 'p2wsh', True),
|
||||
('bip48_3', "m/48h/{ct}h/{acc}h/3h", AF_P2TR, 'p2tr', True),
|
||||
('bip45', "m/45h", AF_P2SH, 'p2sh', True),
|
||||
]:
|
||||
if fmt == AF_P2SH and account_num:
|
||||
continue
|
||||
@ -375,24 +421,25 @@ def generate_generic_export(account_num=0):
|
||||
node = sv.derive_path(dd)
|
||||
xfp = xfp2str(swab32(node.my_fp()))
|
||||
xp = chain.serialize_public(node, AF_CLASSIC)
|
||||
zp = chain.serialize_public(node, fmt) if fmt != AF_CLASSIC else None
|
||||
if is_ms:
|
||||
desc = multisig_descriptor_template(xp, dd, master_xfp_str, fmt)
|
||||
else:
|
||||
desc = Descriptor(keys=[(master_xfp, dd, xp)], addr_fmt=fmt).serialize(int_ext=True)
|
||||
|
||||
OWNERSHIP.note_wallet_used(fmt, account_num)
|
||||
zp = chain.serialize_public(node, fmt) if fmt not in (AF_CLASSIC, AF_P2TR) else None
|
||||
key = ExtendedKey.from_cc_data(master_xfp, dd, xp)
|
||||
key_exp = key.to_string(external=False, internal=False)
|
||||
|
||||
rv[name] = OrderedDict(name=atype,
|
||||
xfp=xfp,
|
||||
deriv=dd,
|
||||
xpub=xp,
|
||||
desc=desc)
|
||||
key_exp=key_exp)
|
||||
|
||||
if zp and zp != xp:
|
||||
rv[name]['_pub'] = zp
|
||||
|
||||
if not is_ms:
|
||||
desc_obj = Descriptor(key=key, addr_fmt=fmt)
|
||||
rv[name]['desc'] = desc_obj.to_string()
|
||||
|
||||
OWNERSHIP.note_wallet_used(fmt, account_num)
|
||||
|
||||
# bonus/check: first non-change address: 0/0
|
||||
node.derive(0, False).derive(0, False)
|
||||
rv[name]['first'] = chain.address(node, fmt)
|
||||
@ -403,22 +450,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,106 +488,72 @@ 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"):
|
||||
from descriptor import Descriptor
|
||||
fname_pattern="descriptor.txt", direct_way=None):
|
||||
from descriptor import Descriptor, ExtendedKey
|
||||
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)
|
||||
|
||||
derive = "m/{mode}h/{coin_type}h/{account}h".format(mode=mode,
|
||||
account=account_num, coin_type=chain.b44_cointype)
|
||||
derive = "m/{mode}h/{coin_type}h/{account}h".format(
|
||||
mode=mode, account=account_num, coin_type=chain.b44_cointype
|
||||
)
|
||||
dis.progress_bar_show(0.2)
|
||||
with stash.SensitiveValues() as sv:
|
||||
dis.progress_bar_show(0.3)
|
||||
xpub = chain.serialize_public(sv.derive_path(derive))
|
||||
|
||||
dis.progress_bar_show(0.7)
|
||||
desc = Descriptor(keys=[(xfp, derive, xpub)], addr_fmt=addr_type)
|
||||
|
||||
key = ExtendedKey.from_cc_data(xfp, derive, xpub)
|
||||
desc = Descriptor(key=key, addr_fmt=addr_type)
|
||||
dis.progress_bar_show(0.8)
|
||||
if int_ext:
|
||||
# with <0;1> notation
|
||||
body = desc.serialize(int_ext=True)
|
||||
body = desc.to_string()
|
||||
else:
|
||||
# external descriptor
|
||||
# internal descriptor
|
||||
body = "%s\n%s" % (
|
||||
desc.serialize(internal=False),
|
||||
desc.serialize(internal=True),
|
||||
desc.to_string(internal=False),
|
||||
desc.to_string(external=False),
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
216
shared/flow.py
216
shared/flow.py
@ -9,7 +9,7 @@ from glob import settings
|
||||
from actions import *
|
||||
from choosers import *
|
||||
from mk4 import dev_enable_repl
|
||||
from multisig import make_multisig_menu, import_multisig_nfc
|
||||
from wallet import make_miniscript_menu, import_miniscript_nfc
|
||||
from seed import make_ephemeral_seed_menu, make_seed_vault_menu, start_b39_pw
|
||||
from address_explorer import address_explore
|
||||
from drv_entro import drv_entro_start, password_entry
|
||||
@ -19,9 +19,12 @@ 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 WIFStore
|
||||
|
||||
# useful shortcut keys
|
||||
from charcodes import KEY_QR, KEY_NFC
|
||||
from public_constants import AF_P2WPKH_P2SH, AF_P2WPKH
|
||||
|
||||
|
||||
# Optional feature: HSM, depends on hardware
|
||||
@ -38,12 +41,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 +74,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 +102,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('miniscript', 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 +166,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),
|
||||
NonDefaultMenuItem('Multisig/Miniscript', 'miniscript',
|
||||
menu=make_miniscript_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 +203,49 @@ 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("Taproot/P2TR"+("(BIP-86)" if version.has_qwerty else "(86)"), f=export_xpub, arg=86),
|
||||
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 +257,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/Miniscript 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 +279,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 +298,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 +309,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 +344,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 +357,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 +376,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),
|
||||
]
|
||||
|
||||
@ -342,8 +395,21 @@ NFCToolsMenu = [
|
||||
MenuItem('Verify Sig File', f=nfc_sign_verify),
|
||||
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('Import Miniscript', f=import_miniscript_nfc),
|
||||
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 +418,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=WIFStore.make_menu),
|
||||
MenuItem('NFC Tools', predicate=nfc_enabled, menu=NFCToolsMenu, shortcut=KEY_NFC),
|
||||
MenuItem("Danger Zone", menu=DangerZoneMenu, shortcut='z'),
|
||||
]
|
||||
@ -373,7 +436,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 +447,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 +471,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,7 +489,7 @@ 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)),
|
||||
predicate=lambda: version.has_qwerty and settings.get("secnap", False)),
|
||||
MenuItem('Type Passwords', f=password_entry, shortcut='t',
|
||||
predicate=lambda: settings.get("emu", False) and has_secrets()),
|
||||
MenuItem('Seed Vault', menu=make_seed_vault_menu, shortcut='v',
|
||||
@ -437,10 +502,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/Miniscript 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=WIFStore.make_menu, 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),
|
||||
]
|
||||
|
||||
@ -29,4 +29,9 @@ NFC = None
|
||||
# QR scanner (Q1 only)
|
||||
SCAN = None
|
||||
|
||||
# Multisig/Miniscript descriptor cache
|
||||
# mapping from unique wallet name to Descriptor object
|
||||
# cache size = 1
|
||||
DESC_CACHE = {}
|
||||
|
||||
# EOF
|
||||
|
||||
@ -8,7 +8,6 @@
|
||||
#
|
||||
import utime, struct
|
||||
import uasyncio as asyncio
|
||||
from utils import B2A
|
||||
from machine import Pin
|
||||
from ustruct import pack
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -4,16 +4,15 @@
|
||||
#
|
||||
# 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
|
||||
import ustruct, chains, sys, gc, uio, ujson, uos, utime, ckcc, ngu
|
||||
from utils import problem_file_line, cleanup_deriv_path, match_deriv_path
|
||||
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
|
||||
from public_constants import MAX_USERNAME_LEN
|
||||
from multisig import MultisigWallet
|
||||
from wallet import MiniScriptWallet
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from ubinascii import unhexlify as a2b_hex
|
||||
from uhashlib import sha256
|
||||
from ucollections import OrderedDict
|
||||
from files import CardSlot, CardMissingError
|
||||
@ -70,9 +69,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):
|
||||
@ -88,13 +87,13 @@ def pop_list(j, fld_name, cleanup_fcn=None):
|
||||
else:
|
||||
return []
|
||||
|
||||
def pop_deriv_list(j, fld_name, extra_val=None):
|
||||
def pop_deriv_list(j, fld_name, extra_vals=None):
|
||||
# expect a list of derivation paths, but also 'any' meaning accept all
|
||||
# - maybe also 'p2sh' as special value
|
||||
# - also, path can have n
|
||||
def cu(s):
|
||||
if s.lower() == 'any': return s.lower()
|
||||
if extra_val and s.lower() == extra_val: return s.lower()
|
||||
if extra_vals and s.lower() in extra_vals:
|
||||
return s.lower()
|
||||
try:
|
||||
return cleanup_deriv_path(s, allow_star=True)
|
||||
except:
|
||||
@ -149,22 +148,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
|
||||
@ -195,7 +178,7 @@ class ApprovalRule:
|
||||
# - users: list of authorized users
|
||||
# - min_users: how many of those are needed to approve
|
||||
# - local_conf: local user must also confirm w/ code
|
||||
# - wallet: which multisig wallet to restrict to, or '1' for single signer only
|
||||
# - wallet: which miniscript wallet to restrict to, or '1' for single signer only
|
||||
# - min_pct_self_transfer: minimum percentage of own input value that must go back to self
|
||||
# - patterns: list of transaction patterns to check for. Valid values:
|
||||
# * EQ_NUM_INS_OUTS: the number of inputs and outputs must be equal
|
||||
@ -215,7 +198,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')
|
||||
@ -236,10 +219,10 @@ class ApprovalRule:
|
||||
# redundant w/ code in pop_int() above
|
||||
assert 1 <= self.min_users <= len(self.users), "range"
|
||||
|
||||
# if specified, 'wallet' must be an existing multisig wallet's name
|
||||
# if specified, 'wallet' must be an existing miniscript wallet's name
|
||||
if self.wallet and self.wallet != '1':
|
||||
names = [ms.name for ms in MultisigWallet.get_all()]
|
||||
assert self.wallet in names, "unknown MS wallet: "+self.wallet
|
||||
msc_names = [msc.name for msc in MiniScriptWallet.iter_wallets()]
|
||||
assert self.wallet in msc_names, "unknown wallet: " + self.wallet
|
||||
|
||||
# patterns must be valid
|
||||
for p in self.patterns:
|
||||
@ -283,9 +266,9 @@ class ApprovalRule:
|
||||
rv = 'Any amount'
|
||||
|
||||
if self.wallet == '1':
|
||||
rv += ' (non multisig)'
|
||||
rv += ' (singlesig only)'
|
||||
elif self.wallet:
|
||||
rv += ' from multisig wallet "%s"' % self.wallet
|
||||
rv += ' from miniscript wallet "%s"' % self.wallet
|
||||
|
||||
if self.users:
|
||||
rv += ' may be authorized by '
|
||||
@ -326,12 +309,11 @@ class ApprovalRule:
|
||||
# Does this rule apply to this PSBT file?
|
||||
if self.wallet:
|
||||
# rule limited to one wallet
|
||||
if psbt.active_multisig:
|
||||
# if multisig signing, might need to match specific wallet name
|
||||
assert self.wallet == psbt.active_multisig.name, 'wrong wallet'
|
||||
if psbt.active_miniscript:
|
||||
assert self.wallet == psbt.active_miniscript.name, 'wrong miniscript wallet'
|
||||
else:
|
||||
# non multisig, but does this rule apply to all wallets or single-singers
|
||||
assert self.wallet == '1', 'not multisig'
|
||||
# not miniscript, but does this rule apply to all wallets or single-singers
|
||||
assert self.wallet == '1', 'singlesig only'
|
||||
|
||||
if self.max_amount is not None:
|
||||
assert total_out <= self.max_amount, 'amount exceeded'
|
||||
@ -367,7 +349,7 @@ class ApprovalRule:
|
||||
# we are verifying the whole consensus-encoded txout
|
||||
txo_bytes = CTxOut(txo.nValue, txo.scriptPubKey).serialize()
|
||||
digest = chain.hash_message(txo_bytes)
|
||||
addr_fmt, pubkey = chains.verify_recover_pubkey(o.attestation, digest)
|
||||
addr_fmt, pubkey = chains.verify_recover_pubkey(psbt.get(o.attestation), digest)
|
||||
# we have extracted a valid pubkey from the sig, but is it
|
||||
# a whitelisted pubkey or something else?
|
||||
ver_addr = chain.pubkey_to_address(pubkey, addr_fmt)
|
||||
@ -390,11 +372,11 @@ class ApprovalRule:
|
||||
|
||||
# check the self-transfer percentage
|
||||
if self.min_pct_self_transfer:
|
||||
own_in_value = sum([i.amount for i in psbt.inputs if i.num_our_keys])
|
||||
own_in_value = sum([i.amount for i in psbt.inputs if i.sp_idxs])
|
||||
own_out_value = 0
|
||||
for idx, txo in psbt.output_iter():
|
||||
o = psbt.outputs[idx]
|
||||
if o.num_our_keys:
|
||||
if o.sp_idxs:
|
||||
own_out_value += txo.nValue
|
||||
percentage = (float(own_out_value) / own_in_value) * 100.0
|
||||
assert percentage >= self.min_pct_self_transfer, 'does not meet self transfer threshold, expected: %.2f, actual: %.2f' % (self.min_pct_self_transfer, percentage)
|
||||
@ -405,8 +387,8 @@ class ApprovalRule:
|
||||
assert len(psbt.inputs) == len(psbt.outputs), 'unequal number of inputs and outputs'
|
||||
|
||||
if "EQ_NUM_OWN_INS_OUTS" in self.patterns:
|
||||
own_ins = sum([1 for i in psbt.inputs if i.num_our_keys])
|
||||
own_outs = sum([1 for o in psbt.outputs if o.num_our_keys])
|
||||
own_ins = sum([1 for i in psbt.inputs if i.sp_idxs])
|
||||
own_outs = sum([1 for o in psbt.outputs if o.sp_idxs])
|
||||
assert own_ins == own_outs, 'unequal number of own inputs and outputs'
|
||||
|
||||
if "EQ_OUT_AMOUNTS" in self.patterns:
|
||||
@ -504,9 +486,9 @@ class HSMPolicy:
|
||||
self.warnings_ok = pop_bool(j, 'warnings_ok')
|
||||
|
||||
# a list of paths we can accept for signing
|
||||
self.msg_paths = pop_deriv_list(j, 'msg_paths')
|
||||
self.share_xpubs = pop_deriv_list(j, 'share_xpubs')
|
||||
self.share_addrs = pop_deriv_list(j, 'share_addrs', 'p2sh')
|
||||
self.msg_paths = pop_deriv_list(j, 'msg_paths', ['any'])
|
||||
self.share_xpubs = pop_deriv_list(j, 'share_xpubs', ['any'])
|
||||
self.share_addrs = pop_deriv_list(j, 'share_addrs', ['any', 'msas'])
|
||||
|
||||
# free text shown at top
|
||||
self.notes = pop_string(j, 'notes', 1, 80)
|
||||
@ -591,7 +573,7 @@ class HSMPolicy:
|
||||
fd.write('\n')
|
||||
|
||||
def plist(pl):
|
||||
remap = {'any': '(any path)', 'p2sh': '(any P2SH)' }
|
||||
remap = {'any': '(any path)', 'msas': '(any miniscript)' }
|
||||
return ' OR '.join(remap.get(i, i) for i in pl)
|
||||
|
||||
fd.write('\nMessage signing:\n')
|
||||
@ -621,7 +603,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')
|
||||
@ -814,14 +796,14 @@ class HSMPolicy:
|
||||
|
||||
return match_deriv_path(self.share_xpubs, subpath)
|
||||
|
||||
def approve_address_share(self, subpath=None, is_p2sh=False):
|
||||
def approve_address_share(self, subpath=None, miniscript=False):
|
||||
# Are we allowing "show address" requests over USB?
|
||||
|
||||
if not self.share_addrs:
|
||||
return False
|
||||
|
||||
if is_p2sh:
|
||||
return ('p2sh' in self.share_addrs)
|
||||
if miniscript:
|
||||
return ('msas' in self.share_addrs)
|
||||
|
||||
return match_deriv_path(self.share_addrs, subpath)
|
||||
|
||||
@ -894,6 +876,7 @@ class HSMPolicy:
|
||||
|
||||
# reject anything with warning, probably
|
||||
if psbt.warnings:
|
||||
print(psbt.warnings)
|
||||
if self.warnings_ok:
|
||||
log.info("Txn has warnings, but policy is to accept anyway.")
|
||||
else:
|
||||
@ -951,7 +934,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)
|
||||
|
||||
@ -994,7 +977,7 @@ def hsm_status_report():
|
||||
rv['approval_wait'] = True
|
||||
|
||||
rv['users'] = Users.list()
|
||||
rv['wallets'] = [ms.name for ms in MultisigWallet.get_all()]
|
||||
rv['wallets'] = [msc.name for msc in MiniScriptWallet.iter_wallets()]
|
||||
|
||||
rv['chain'] = settings.get('chain', 'BTC')
|
||||
|
||||
|
||||
@ -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
|
||||
@ -297,7 +297,7 @@ class hsmUxInteraction:
|
||||
|
||||
|
||||
# replacements for display.py:Display functions
|
||||
def hack_fullscreen(self, msg, percent=None):
|
||||
def hack_fullscreen(self, msg, percent=None, **kwargs):
|
||||
self.draw_busy(msg, percent)
|
||||
def hack_progress_bar(self, percent):
|
||||
self.draw_busy(None, percent)
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,133 @@ 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)
|
||||
|
||||
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 +746,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 +779,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 +827,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 +868,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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
# 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',
|
||||
'bsms.py',
|
||||
'callgate.py',
|
||||
'ccc.py',
|
||||
'chains.py',
|
||||
'choosers.py',
|
||||
'compat7z.py',
|
||||
'countdowns.py',
|
||||
'descriptor.py',
|
||||
'desc_utils.py',
|
||||
'dev_helper.py',
|
||||
'display.py',
|
||||
'drv_entro.py',
|
||||
@ -26,16 +29,24 @@ freeze_as_mpy('', [
|
||||
'login.py',
|
||||
'main.py',
|
||||
'menu.py',
|
||||
"miniscript.py",
|
||||
'mk4.py',
|
||||
'msgsign.py',
|
||||
'multisig.py',
|
||||
'ndef.py',
|
||||
'nfc.py',
|
||||
'numpad.py',
|
||||
'nvstore.py',
|
||||
'opcodes.py',
|
||||
'ownership.py',
|
||||
'paper.py',
|
||||
'pincodes.py',
|
||||
'precomp_tag_hash.py',
|
||||
'psbt.py',
|
||||
'psram.py',
|
||||
'pwsave.py',
|
||||
'queues.py',
|
||||
'qrs.py',
|
||||
'queues.py',
|
||||
'random.py',
|
||||
'seed.py',
|
||||
'selftest.py',
|
||||
@ -43,31 +54,34 @@ freeze_as_mpy('', [
|
||||
'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")
|
||||
|
||||
@ -1,17 +1,11 @@
|
||||
# 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',
|
||||
'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.
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
@ -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():
|
||||
|
||||
1171
shared/miniscript.py
Normal file
1171
shared/miniscript.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
||||
512
shared/msgsign.py
Normal file
512
shared/msgsign.py
Normal file
@ -0,0 +1,512 @@
|
||||
# (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:') or 0
|
||||
|
||||
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:') or 0
|
||||
|
||||
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, only_printable=True):
|
||||
# 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
|
||||
|
||||
result = to_ascii_printable(text, only_printable=only_printable)
|
||||
|
||||
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)
|
||||
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:
|
||||
subpath = chains.STD_DERIVATIONS[addr_fmt]
|
||||
subpath = subpath.format(
|
||||
coin_type=chains.current_chain().b44_cointype,
|
||||
account=0, change=0, idx=0
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
await approve_msg_sign(text, subpath, af, approved_cb=approved_cb,
|
||||
kill_menu=kill_menu, only_printable=False)
|
||||
|
||||
# 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
|
||||
1884
shared/multisig.py
1884
shared/multisig.py
File diff suppressed because it is too large
Load Diff
385
shared/nfc.py
385
shared/nfc.py
@ -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,14 @@ class NFCHandler:
|
||||
|
||||
self.set_rf_disable(1)
|
||||
|
||||
async def share_loop(self, n, **kws):
|
||||
while 1:
|
||||
done = await self.share_start(n, **kws)
|
||||
if done:
|
||||
# do not wipe if we are not done
|
||||
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 +240,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 +283,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 +355,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 +375,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 +401,12 @@ 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, write_mode, allow_enter=True, prompt=None, line2=None,
|
||||
is_secret=False):
|
||||
# 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 +419,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
|
||||
@ -467,8 +476,6 @@ class NFCHandler:
|
||||
break
|
||||
|
||||
self.set_rf_disable(1)
|
||||
if not write_mode:
|
||||
await self.wipe(False)
|
||||
|
||||
return aborted
|
||||
|
||||
@ -476,9 +483,7 @@ 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)
|
||||
|
||||
async def start_nfc_rx(self, **kws):
|
||||
@ -514,8 +519,7 @@ class NFCHandler:
|
||||
await self.wipe(False)
|
||||
return rv
|
||||
|
||||
|
||||
async def start_psbt_rx(self):
|
||||
async def start_psbt_rx(self, miniscript_wallet=None):
|
||||
from auth import psbt_encoding_taster, TXN_INPUT_OFFSET
|
||||
from auth import UserAuthorizedAction, ApproveTransaction
|
||||
from ux import the_ux
|
||||
@ -540,10 +544,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 +565,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, miniscript_wallet=miniscript_wallet,
|
||||
)
|
||||
# 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,10 +580,12 @@ class NFCHandler:
|
||||
n.setup()
|
||||
assert n.uid
|
||||
|
||||
aborted = await n.share_text("NFC is working: %s" % n.get_uid(), allow_enter=False)
|
||||
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...
|
||||
from actions import file_picker
|
||||
@ -659,82 +631,33 @@ class NFCHandler:
|
||||
else:
|
||||
raise ValueError(ext)
|
||||
|
||||
async def import_multisig_nfc(self, *a):
|
||||
# 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
|
||||
|
||||
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 not winner:
|
||||
await ux_show_story('Unable to find multisig descriptor.')
|
||||
return
|
||||
|
||||
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 +666,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,133 +677,133 @@ 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.
|
||||
_, addr, args = await self.read_address()
|
||||
if addr:
|
||||
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
|
||||
msg = bytes(msg)
|
||||
try:
|
||||
if 150 <= len(msg) <= 280:
|
||||
winner = a2b_base64(msg)
|
||||
r = func(msg)
|
||||
if r is not None:
|
||||
winner = r
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
if not winner:
|
||||
await ux_show_story('Unable to find base64 encoded TAPSIGNER backup.')
|
||||
await ux_show_story(fail_msg)
|
||||
return
|
||||
|
||||
return winner
|
||||
|
||||
async def read_bsms_token(self):
|
||||
def f(m):
|
||||
m = m.decode().strip()
|
||||
try:
|
||||
int(m, 16)
|
||||
return m
|
||||
except: pass
|
||||
|
||||
return await self._nfc_reader(f, 'Unable to find BSMS token in NDEF data')
|
||||
|
||||
async def read_bsms_data(self):
|
||||
def f(m):
|
||||
m = m.decode().strip() # from memory view
|
||||
try:
|
||||
if "BSMS" in m or int(m[:6], 16):
|
||||
# unencrypted/encrypted case
|
||||
return m
|
||||
except: pass
|
||||
|
||||
return await self._nfc_reader(f, 'Unable to find BSMS data in NDEF data')
|
||||
|
||||
async def import_miniscript_nfc(self):
|
||||
def f(m):
|
||||
if len(m) < 70: return
|
||||
m = m.decode()
|
||||
# TODO this should be Descriptor.is_descriptor() ?
|
||||
if 'pub' in m:
|
||||
return m
|
||||
|
||||
winner = await self._nfc_reader(f, 'Unable to find miniscript descriptor expected in NDEF')
|
||||
if not winner:
|
||||
return
|
||||
|
||||
from auth import maybe_enroll_xpub
|
||||
try:
|
||||
maybe_enroll_xpub(config=winner)
|
||||
except Exception as e:
|
||||
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
|
||||
|
||||
# EOF
|
||||
|
||||
208
shared/notes.py
208
shared/notes.py
@ -13,7 +13,7 @@ from files import CardMissingError, needs_microsd, CardSlot
|
||||
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
|
||||
|
||||
# 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 +21,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 +44,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 +108,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,9 +124,12 @@ 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:
|
||||
wipe_if_deltamode()
|
||||
|
||||
rv = []
|
||||
for note in NoteContent.get_all():
|
||||
rv.append(MenuItem('%d: %s' % (note.idx+1, note.title), menu=note.make_menu))
|
||||
@ -121,14 +138,40 @@ class NotesMenu(MenuSystem):
|
||||
|
||||
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 = []
|
||||
for note in NoteContent.get_all():
|
||||
rv.append(MenuItem('%d: %s' % (note.idx+1, note.title),
|
||||
menu=note.make_menu, arg=True)) # readonly=True
|
||||
|
||||
if not rv:
|
||||
rv.append(MenuItem('(none saved yet)'))
|
||||
|
||||
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 +188,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 +202,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.
|
||||
@ -170,6 +212,7 @@ class NotesMenu(MenuSystem):
|
||||
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
|
||||
settings.remove_key('secnap')
|
||||
settings.remove_key('notes')
|
||||
settings.save()
|
||||
|
||||
@ -188,8 +231,8 @@ class NotesMenu(MenuSystem):
|
||||
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)
|
||||
m = await item._make_menu()
|
||||
the_ux.push(MenuSystem(m))
|
||||
|
||||
|
||||
class NoteContentBase:
|
||||
@ -223,6 +266,17 @@ 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()
|
||||
|
||||
async def delete(self, *a):
|
||||
# Remove note
|
||||
ok = await ux_confirm("Everything about this note/password will be lost.")
|
||||
@ -246,7 +300,7 @@ class NoteContentBase:
|
||||
|
||||
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,12 +310,26 @@ 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)
|
||||
@ -289,28 +357,49 @@ 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)
|
||||
|
||||
|
||||
class PasswordContent(NoteContentBase):
|
||||
# "Passwords" have a few more fields and are more structured
|
||||
flds = ['title', 'user', 'password', 'site', 'misc' ]
|
||||
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'),
|
||||
]
|
||||
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),
|
||||
]
|
||||
|
||||
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 +439,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 +451,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
|
||||
|
||||
@ -429,33 +514,34 @@ class NoteContent(NoteContentBase):
|
||||
flds = ['title', 'misc']
|
||||
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'),
|
||||
]
|
||||
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
|
||||
@ -498,16 +584,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 +622,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 +651,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 +664,13 @@ 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
|
||||
await import_from_json(records)
|
||||
|
||||
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 +680,11 @@ 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()
|
||||
|
||||
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
|
||||
|
||||
@ -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
|
||||
@ -32,7 +32,8 @@ from utils import call_later_ms
|
||||
# batt_to = (when on battery only) idle timeout period
|
||||
# _age = internal verison number for data (see below)
|
||||
# tested = selftest has been completed successfully
|
||||
# multisig = list of defined multisig wallets (complex)
|
||||
# multisig = list of defined multisig wallets (complex) [before removal of MultisigWallet]
|
||||
# miniscript = list of defined miniscript wallets, including multisig (complex)
|
||||
# pms = trust/import/distrust xpubs found in PSBT files
|
||||
# fee_limit = (int) percentage of tx value allowed as max fee
|
||||
# axi = index of last selected address in explorer
|
||||
@ -56,13 +57,19 @@ 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
|
||||
# b85max = (bool) allow max BIP-32 int value in BIP-85 derivations
|
||||
# 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
|
||||
# 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
|
||||
# aemscsv = (bool) opt-in enable more verbose CSV output for miniscript wallets with Derivations and Scripts
|
||||
# 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)
|
||||
@ -76,16 +83,19 @@ from utils import call_later_ms
|
||||
# terms_ok = customer has signed-off on the terms of sale
|
||||
|
||||
# settings linked to seed
|
||||
# LINKED_SETTINGS = ["multisig", "tp", "ovc", "xfp", "xpub", "words"]
|
||||
# LINKED_SETTINGS = ["miniscript", "tp", "ovc", "xfp", "xpub", "words"]
|
||||
# settings that does not make sense to copy to temporary secret
|
||||
# LINKED_SETTINGS += ["sd2fa", "usr", "axi", "hsmcmd"]
|
||||
# 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"]
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@ -82,7 +82,7 @@ OP_RETURN = const(106)
|
||||
#OP_RSHIFT = const(153)
|
||||
#OP_BOOLAND = const(154)
|
||||
#OP_BOOLOR = const(155)
|
||||
#OP_NUMEQUAL = const(156)
|
||||
OP_NUMEQUAL = const(156)
|
||||
#OP_NUMEQUALVERIFY = const(157)
|
||||
#OP_NUMNOTEQUAL = const(158)
|
||||
#OP_LESSTHAN = const(159)
|
||||
@ -114,6 +114,7 @@ OP_CHECKMULTISIGVERIFY = const(175)
|
||||
#OP_NOP8 = const(183)
|
||||
#OP_NOP9 = const(184)
|
||||
#OP_NOP10 = const(185)
|
||||
OP_CHECKSIGADD = const(186)
|
||||
#OP_NULLDATA = const(252)
|
||||
#OP_PUBKEYHASH = const(253)
|
||||
#OP_PUBKEY = const(254)
|
||||
|
||||
@ -2,16 +2,17 @@
|
||||
#
|
||||
# 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 exceptions import UnknownAddressExplained
|
||||
from utils import problem_file_line, show_single_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 +39,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.
|
||||
@ -49,12 +50,13 @@ class AddressCacheFile:
|
||||
def __init__(self, wallet, change_idx):
|
||||
self.wallet = wallet
|
||||
self.change_idx = change_idx
|
||||
desc = wallet.to_descriptor().serialize()
|
||||
desc = wallet.to_descriptor().to_string(internal=False)
|
||||
h = b2a_hex(ngu.hash.sha256d(wallet.chain.ctype + desc))
|
||||
self.fname = h[0:32] + '-%d.own' % change_idx
|
||||
self.salt = h[32:]
|
||||
self.count = 0
|
||||
self.hdr = None
|
||||
self.fd = None
|
||||
|
||||
self.peek()
|
||||
|
||||
@ -64,9 +66,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 +79,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 +103,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 +118,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 +130,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 +146,108 @@ 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)
|
||||
|
||||
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)
|
||||
|
||||
bonus = None
|
||||
for idx,here,*_ in self.wallet.yield_addresses(start_idx, count, 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 wallet import MiniScriptWallet
|
||||
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(MiniScriptWallet.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 == AF_P2TR:
|
||||
possibles.extend([w for w in MiniScriptWallet.iter_wallets() if w.addr_fmt == AF_P2TR])
|
||||
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(MiniScriptWallet.iter_wallets(addr_fmts=afs))
|
||||
|
||||
try:
|
||||
# Construct possible single-signer wallets, always at least account=0 case
|
||||
@ -247,89 +261,156 @@ 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/miniscript 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...")
|
||||
|
||||
ch = chains.current_chain()
|
||||
addr_fmt = ch.possible_address_fmt(addr)
|
||||
if not addr_fmt:
|
||||
# might be valid address over on testnet vs mainnet
|
||||
raise UnknownAddressExplained('That address is not valid on ' + ch.name)
|
||||
|
||||
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_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(ch, 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 wallet import MiniScriptWallet
|
||||
from public_constants import AFC_BECH32, AFC_BECH32M
|
||||
|
||||
try:
|
||||
wallet, subpath = OWNERSHIP.search(addr)
|
||||
_, wallet, subpath = cls.search(addr, args)
|
||||
is_complex = isinstance(wallet, MiniScriptWallet)
|
||||
|
||||
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
|
||||
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'
|
||||
msg += '\n\nFound in wallet:\n' + wallet.name
|
||||
msg += '\n\nDerivation path:\n'
|
||||
addr_fmt = wallet.addr_fmt
|
||||
if hasattr(wallet, "render_path"):
|
||||
sp = wallet.render_path(*subpath)
|
||||
msg += sp
|
||||
else:
|
||||
sp = None
|
||||
msg += ".../%d/%d" % subpath
|
||||
|
||||
if not is_complex:
|
||||
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,
|
||||
is_alnum=(addr_fmt & (AFC_BECH32 | AFC_BECH32M)),
|
||||
msg=addr, is_addrs=True
|
||||
)
|
||||
elif not is_complex 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 +444,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
|
||||
|
||||
@ -3,10 +3,10 @@
|
||||
#
|
||||
# paper.py - generate paper wallets, based on random values (not linked to wallet)
|
||||
#
|
||||
import ujson
|
||||
import ujson, ngu, chains
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from utils import imported
|
||||
from public_constants import AF_CLASSIC, AF_P2WPKH
|
||||
from utils import imported, problem_file_line
|
||||
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2TR
|
||||
from ux import ux_show_story, ux_dramatic_pause
|
||||
from files import CardSlot, CardMissingError, needs_microsd
|
||||
from actions import file_picker
|
||||
@ -29,10 +29,6 @@ can still be made. Visit the Coldcard website to get some interesting templates.
|
||||
|
||||
SECP256K1_ORDER = b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xba\xae\xdc\xe6\xaf\x48\xa0\x3b\xbf\xd2\x5e\x8c\xd0\x36\x41\x41"
|
||||
|
||||
# Aprox. time of this feature release (Nov 20/2019) so no need to scan
|
||||
# blockchain earlier than this during "importmulti"
|
||||
FEATURE_RELEASE_TIME = const(1574277000)
|
||||
|
||||
# These very-specific text values are matched on the Coldcard; cannot be changed.
|
||||
class placeholders:
|
||||
addr = b'ADDRESS_XXXXXXXXXXXXXXXXXXXXXXXXXXXXX' # 37 long
|
||||
@ -51,6 +47,12 @@ class PaperWalletMaker:
|
||||
self.my_menu = my_menu
|
||||
self.template_fn = None
|
||||
self.is_segwit = False
|
||||
self.is_taproot = False
|
||||
|
||||
def atype(self):
|
||||
if self.is_taproot: return 2, 'Taproot P2TR'
|
||||
if self.is_segwit: return 1, 'Segwit P2WPKH'
|
||||
return 0, 'Classic P2PKH'
|
||||
|
||||
async def pick_template(self, *a):
|
||||
fn = await file_picker(suffix='.pdf', min_size=20000, taster=template_taster,
|
||||
@ -62,17 +64,17 @@ class PaperWalletMaker:
|
||||
def addr_format_chooser(self, *a):
|
||||
# simple bool choice
|
||||
def set(idx, text):
|
||||
self.is_segwit = bool(idx)
|
||||
self.is_segwit = idx == 1
|
||||
self.is_taproot = idx == 2
|
||||
self.update_menu()
|
||||
return int(self.is_segwit), ['Classic P2PKH', 'Segwit P2WPKH'], set
|
||||
return self.atype()[0], ['Classic P2PKH', 'Segwit P2WPKH', 'Taproot P2TR'], set
|
||||
|
||||
def update_menu(self):
|
||||
# Reconstruct the menu contents based on our state.
|
||||
self.my_menu.replace_items([
|
||||
MenuItem("Don't make PDF" if not self.template_fn else 'Making PDF',
|
||||
f=self.pick_template),
|
||||
MenuItem('Classic P2PKH' if not self.is_segwit else 'Segwit P2WPKH',
|
||||
chooser=self.addr_format_chooser),
|
||||
MenuItem(self.atype()[1], chooser=self.addr_format_chooser),
|
||||
MenuItem('Use Dice', f=self.use_dice),
|
||||
MenuItem('GENERATE WALLET', f=self.doit),
|
||||
], keep_position=True)
|
||||
@ -83,7 +85,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
|
||||
@ -104,12 +106,16 @@ class PaperWalletMaker:
|
||||
dis.fullscreen("Rendering...")
|
||||
|
||||
# make payment address
|
||||
digest = hash160(pubkey)
|
||||
ch = current_chain()
|
||||
ch = chains.current_chain()
|
||||
if self.is_segwit:
|
||||
addr = ngu.codecs.segwit_encode(ch.bech32_hrp, 0, digest)
|
||||
af = AF_P2WPKH
|
||||
elif self.is_taproot:
|
||||
af = AF_P2TR
|
||||
pubkey = pubkey[1:]
|
||||
else:
|
||||
addr = ngu.codecs.b58_encode(ch.b58_addr + digest)
|
||||
af = AF_CLASSIC
|
||||
|
||||
addr = ch.pubkey_to_address(pubkey, af)
|
||||
|
||||
wif = ngu.codecs.b58_encode(ch.b58_privkey + privkey + b'\x01')
|
||||
|
||||
@ -164,8 +170,10 @@ class PaperWalletMaker:
|
||||
else:
|
||||
nice_pdf = ''
|
||||
|
||||
nice_sig = write_sig_file(sig_cont, pk=privkey, sig_name=basename,
|
||||
addr_fmt=AF_P2WPKH if self.is_segwit else AF_CLASSIC)
|
||||
nice_sig = None
|
||||
if af != AF_P2TR:
|
||||
nice_sig = write_sig_file(sig_cont, pk=privkey, sig_name=basename,
|
||||
addr_fmt=AF_P2WPKH if self.is_segwit else AF_CLASSIC)
|
||||
|
||||
# Half-hearted attempt to cleanup secrets-contaminated memory
|
||||
# - better would be force user to reboot
|
||||
@ -178,14 +186,14 @@ class PaperWalletMaker:
|
||||
await needs_microsd()
|
||||
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
|
||||
if nice_pdf:
|
||||
story += "\n\n%s" % nice_pdf
|
||||
story += "\n\n%s" % nice_sig
|
||||
if nice_sig:
|
||||
story += "\n\n%s" % nice_sig
|
||||
await ux_show_story(story)
|
||||
|
||||
async def use_dice(self, *a):
|
||||
@ -214,10 +222,17 @@ class PaperWalletMaker:
|
||||
fp.write('Bitcoin Core command:\n\n')
|
||||
|
||||
# new hotness: output descriptors
|
||||
desc = ('wpkh(%s)' if self.is_segwit else 'pkh(%s)') % wif
|
||||
multi = ujson.dumps(dict(timestamp=FEATURE_RELEASE_TIME, desc=append_checksum(desc)))
|
||||
fp.write(" bitcoin-cli importmulti '[%s]'\n\n" % multi)
|
||||
fp.write('# OR (more compatible, but slower)\n\n bitcoin-cli importprivkey "%s"\n\n' % wif)
|
||||
if self.is_taproot:
|
||||
desc = 'tr(%s)'
|
||||
elif self.is_segwit:
|
||||
desc = 'wpkh(%s)'
|
||||
else:
|
||||
desc = 'pkh(%s)'
|
||||
desc = desc % wif
|
||||
descriptor = ujson.dumps(dict(timestamp="now", desc=append_checksum(desc)))
|
||||
fp.write(" bitcoin-cli importdescriptors '[%s]'\n\n" % descriptor)
|
||||
if not self.is_taproot:
|
||||
fp.write('# OR (only supported with legacy wallets)\n\n bitcoin-cli importprivkey "%s"\n\n' % wif)
|
||||
|
||||
if qr_addr and qr_wif:
|
||||
fp.write('\n\n--- QR Codes --- (requires UTF-8, unicode, white background)\n\n\n\n')
|
||||
|
||||
@ -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)
|
||||
@ -418,9 +410,13 @@ class PinAttempt:
|
||||
# Main secret has changed: reset the settings+their key,
|
||||
# and capture xfp/xpub
|
||||
# if None is provided as raw_secret -> restore to main seed
|
||||
import glob
|
||||
from glob import settings, dis
|
||||
stash.SensitiveValues.clear_cache()
|
||||
|
||||
# invalidate descriptor cache - upon new secret load
|
||||
glob.DESC_CACHE.clear()
|
||||
|
||||
bypass_tmp = False
|
||||
stash.bip39_passphrase = bool(bip39pw)
|
||||
|
||||
@ -473,6 +469,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 +480,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 +529,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
|
||||
|
||||
12
shared/precomp_tag_hash.py
Normal file
12
shared/precomp_tag_hash.py
Normal file
@ -0,0 +1,12 @@
|
||||
# (c) Copyright 2025 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
# taproot precomputed tag hashes
|
||||
#
|
||||
# SHA256(TapLeaf)
|
||||
TAP_LEAF_H = b'\xae\xea\x8f\xdcB\x08\x981\x05sKX\x08\x1d\x1e&8\xd3_\x1c\xb5@\x08\xd4\xd3W\xca\x03\xbex\xe9\xee'
|
||||
# SHA256(TapBranch)
|
||||
TAP_BRANCH_H = b'\x19A\xa1\xf2\xe5n\xb9_\xa2\xa9\xf1\x94\xbe\\\x01\xf7!o3\xed\x82\xb0\x91F4\x90\xd0[\xf5\x16\xa0\x15'
|
||||
# SHA256(TapTweak)
|
||||
TAP_TWEAK_H = b'\xe8\x0f\xe1c\x9c\x9c\xa0P\xe3\xaf\x1b9\xc1C\xc6>B\x9c\xbc\xeb\x15\xd9@\xfb\xb5\xc5\xa1\xf4\xafW\xc5\xe9'
|
||||
# SHA256(TapSighash)
|
||||
TAP_SIGHASH_H = b'\xf4\nH\xdfK*p\xc8\xb4\x92K\xf2eFa\xed=\x95\xfdf\xa3\x13\xeb\x87#u\x97\xc6(\xe4\xa01'
|
||||
3220
shared/psbt.py
3220
shared/psbt.py
File diff suppressed because it is too large
Load Diff
@ -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]
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -201,7 +201,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
|
||||
|
||||
415
shared/seed.py
415
shared/seed.py
@ -10,30 +10,46 @@
|
||||
# - 'abandon' * 17 + 'agent'
|
||||
# - 'abandon' * 11 + 'about'
|
||||
#
|
||||
import ngu, uctypes, bip39, random, stash, version
|
||||
import ngu, uctypes, bip39, random, version, ure, chains
|
||||
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, 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)
|
||||
|
||||
# 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
|
||||
if not sofar:
|
||||
@ -138,23 +154,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 +270,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 +328,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 +345,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 +481,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 +535,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 +546,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 +566,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 +594,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 +617,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 +631,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):
|
||||
@ -625,17 +710,25 @@ def seed_words_to_encoded_secret(words):
|
||||
return nv
|
||||
|
||||
def xprv_to_encoded_secret(xprv):
|
||||
node, chain, _ = parse_extended_key(xprv, private=True)
|
||||
if node is None:
|
||||
raise ValueError("Failed to parse extended private key.")
|
||||
# read an xprv/tprv/etc and return BIP-32 node and what chain it's on.
|
||||
# - can handle any garbage line
|
||||
# - returns (node, chain)
|
||||
# - people are using SLIP132 so we need this
|
||||
ln = xprv.strip()
|
||||
pat = ure.compile('.prv[A-Za-z0-9]+')
|
||||
found = pat.search(ln)
|
||||
assert found, "not extended privkey"
|
||||
# serialize, and note version code
|
||||
node, chain, addr_fmt, is_private = chains.slip132_deserialize(found.group(0))
|
||||
assert node, "wrong extended privkey"
|
||||
nv = SecretStash.encode(xprv=node)
|
||||
node.blank()
|
||||
return nv, chain # need to know chain
|
||||
|
||||
|
||||
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 +752,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 +769,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 +799,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 +830,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 +904,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 +914,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 +969,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 +985,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 +1017,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 +1038,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 +1052,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 +1094,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 +1116,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 +1172,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_start
|
||||
from charcodes import KEY_QR
|
||||
|
||||
import_ephemeral_menu = [
|
||||
@ -1060,32 +1191,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_start),
|
||||
]
|
||||
|
||||
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()
|
||||
@ -1191,7 +1321,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 +1336,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()
|
||||
@ -1277,15 +1409,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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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_BARE_PK, AF_P2TR
|
||||
|
||||
# single-shot hash functions
|
||||
sha256 = ngu.hash.sha256s
|
||||
@ -27,9 +27,7 @@ ripemd160 = ngu.hash.ripemd160
|
||||
hash256 = ngu.hash.sha256d
|
||||
hash160 = ngu.hash.hash160
|
||||
|
||||
def bytes_to_hex_str(s):
|
||||
return str(b2a_hex(s), 'ascii')
|
||||
|
||||
SIGHASH_DEFAULT = const(0) # in taproot meaning same as SIGHASH_ALL (over whole TX)
|
||||
SIGHASH_ALL = const(1)
|
||||
SIGHASH_NONE = const(2)
|
||||
SIGHASH_SINGLE = const(3)
|
||||
@ -37,6 +35,7 @@ SIGHASH_ANYONECANPAY = const(0x80)
|
||||
|
||||
# list containing all flags that we support signing for
|
||||
ALL_SIGHASH_FLAGS = [
|
||||
SIGHASH_DEFAULT,
|
||||
SIGHASH_ALL,
|
||||
SIGHASH_NONE,
|
||||
SIGHASH_SINGLE,
|
||||
@ -56,14 +55,23 @@ def ser_compact_size(l):
|
||||
else:
|
||||
return struct.pack("<BQ", 255, l)
|
||||
|
||||
def deser_compact_size(f):
|
||||
def deser_compact_size(f, ret_num_bytes=False):
|
||||
nit = struct.unpack("<B", f.read(1))[0]
|
||||
num_bytes = 1
|
||||
if nit == 253:
|
||||
nit = struct.unpack("<H", f.read(2))[0]
|
||||
assert nit >= 253
|
||||
num_bytes += 2
|
||||
elif nit == 254:
|
||||
nit = struct.unpack("<I", f.read(4))[0]
|
||||
assert nit >= 0x1_0000
|
||||
num_bytes += 4
|
||||
elif nit == 255:
|
||||
nit = struct.unpack("<Q", f.read(8))[0]
|
||||
assert nit >= 0x1_0000_0000
|
||||
num_bytes += 8
|
||||
if ret_num_bytes:
|
||||
return nit, num_bytes
|
||||
return nit
|
||||
|
||||
def deser_string(f):
|
||||
@ -80,7 +88,6 @@ def deser_uint256(f):
|
||||
r += t << (i * 32)
|
||||
return r
|
||||
|
||||
|
||||
def ser_uint256(u):
|
||||
rs = b""
|
||||
for i in range(8):
|
||||
@ -88,7 +95,6 @@ def ser_uint256(u):
|
||||
u >>= 32
|
||||
return rs
|
||||
|
||||
|
||||
def uint256_from_str(s):
|
||||
r = 0
|
||||
t = struct.unpack("<IIIIIIII", s[:32])
|
||||
@ -96,13 +102,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 +116,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 +128,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 +136,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 +161,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 +171,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])
|
||||
@ -220,11 +216,13 @@ def disassemble(script):
|
||||
#print('dis %d: number=%d' % (offset, (c - OP_1 + 1)))
|
||||
yield (c - OP_1 + 1, None)
|
||||
elif c == OP_PUSHDATA1:
|
||||
cnt = script[offset]; offset += 1
|
||||
cnt = script[offset]
|
||||
offset += 1
|
||||
yield (script[offset:offset+cnt], None)
|
||||
offset += cnt
|
||||
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
|
||||
@ -237,11 +235,12 @@ def disassemble(script):
|
||||
# OP_0 included here
|
||||
#print('dis %d: opcode=%d' % (offset, c))
|
||||
yield (None, c)
|
||||
except:
|
||||
except Exception as e:
|
||||
# import sys;sys.print_exception(e)
|
||||
raise ValueError("bad script")
|
||||
|
||||
|
||||
def ser_sig_der(r, s, sighash_type=1):
|
||||
def ser_sig_der(r, s, sighash_type=SIGHASH_ALL):
|
||||
# Take R and S values from a signature and encode into DER format.
|
||||
sig = b"\x30"
|
||||
|
||||
@ -326,7 +325,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]
|
||||
@ -358,33 +356,48 @@ class CTxOut(object):
|
||||
return r
|
||||
|
||||
def get_address(self):
|
||||
# Detect type of output from scriptPubKey, and return 3-tuple:
|
||||
# (addr_type_code, addr, is_segwit)
|
||||
# Detect type of output from scriptPubKey, and return 2-tuple:
|
||||
# (addr_type_code, pubkey/pubkeyhash/scripthash)
|
||||
# 'addr' is byte string, either 20 or 32 long
|
||||
if self.is_p2tr():
|
||||
return AF_P2TR, self.scriptPubKey[2:2+32]
|
||||
|
||||
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]
|
||||
|
||||
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]
|
||||
|
||||
if self.is_p2pkh():
|
||||
return 'p2pkh', self.scriptPubKey[3:3+20], False
|
||||
return AF_CLASSIC, self.scriptPubKey[3:3+20]
|
||||
|
||||
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]
|
||||
|
||||
if self.is_p2pk():
|
||||
# rare, pay to full pubkey
|
||||
return 'p2pk', self.scriptPubKey[2:2+33], False
|
||||
return AF_BARE_PK, self.scriptPubKey[2:2+33]
|
||||
|
||||
# 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.scriptPubKey[0] == OP_RETURN:
|
||||
return OP_RETURN, self.scriptPubKey
|
||||
|
||||
return None, self.scriptPubKey
|
||||
|
||||
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 \
|
||||
@ -489,7 +502,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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
791
shared/teleport.py
Normal file
791
shared/teleport.py
Normal file
@ -0,0 +1,791 @@
|
||||
# (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 wallet import MiniScriptWallet
|
||||
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.
|
||||
dis.fullscreen("Wait...")
|
||||
|
||||
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 MiniScriptWallet.exists():
|
||||
await ux_show_story("Incoming PSBT requires miniscript wallet(s) to be already setup, but you have none.")
|
||||
return
|
||||
|
||||
ses_key, body, sender_xfp = MiniScriptWallet.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)
|
||||
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
|
||||
|
||||
vals = text_bk_parser(raw)
|
||||
assert vals # empty?
|
||||
|
||||
raw_sec, _ = extract_raw_secret(vals)
|
||||
|
||||
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), miniscript 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 miniscripts 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
|
||||
# all_xfps is set, no need to list one master xfp more than once - assuming CC can sign it all
|
||||
assert psbt.active_miniscript
|
||||
ms = psbt.active_miniscript
|
||||
all_xfps = {x for x,*p in ms.to_descriptor().xfp_paths(skip_unspend_ik=True)}
|
||||
|
||||
# ignore -> keys to ignore, currently only musig aggregate keys
|
||||
need, ignore = psbt.miniscript_xfps_needed()
|
||||
need = [x for x in need 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
|
||||
|
||||
# move out of PSRAM
|
||||
from auth import TXN_OUTPUT_OFFSET
|
||||
|
||||
with SFFile(TXN_OUTPUT_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 - ignore): # set diff
|
||||
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, 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
|
||||
|
||||
return None
|
||||
|
||||
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)
|
||||
|
||||
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_miniscript:
|
||||
await ux_show_story("We are not part of this wallet.", "Cannot Teleport PSBT")
|
||||
return
|
||||
|
||||
await kt_send_psbt(psbt, psbt_len=psbt_len, psbt_offset=TXN_INPUT_OFFSET)
|
||||
|
||||
# EOF
|
||||
@ -12,6 +12,9 @@ from menu import MenuSystem, MenuItem
|
||||
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_enter_number, the_ux
|
||||
from stash import SecretStash
|
||||
from drv_entro import bip85_derive
|
||||
from glob import settings
|
||||
|
||||
from utils import node_from_privkey
|
||||
|
||||
# see from mk4-bootloader/se2.h
|
||||
NUM_TRICKS = const(14)
|
||||
@ -32,7 +35,7 @@ TC_WORD_WALLET = const(0x1000)
|
||||
TC_XPRV_WALLET = const(0x0800)
|
||||
TC_DELTA_MODE = const(0x0400)
|
||||
TC_REBOOT = const(0x0200)
|
||||
TC_RFU = const(0x0100)
|
||||
TC_FW_DEFINED = const(0x0100)
|
||||
# for our use, not implemented in bootrom
|
||||
TC_BLANK_WALLET = const(0x0080)
|
||||
TC_COUNTDOWN = const(0x0040) # tc_arg = minutes of delay
|
||||
@ -40,6 +43,10 @@ TC_COUNTDOWN = const(0x0040) # tc_arg = minutes of delay
|
||||
# tc_args encoding:
|
||||
# TC_WORD_WALLET -> BIP-85 index, 1001..1003 for 24 words, 2001..2003 for 12-words
|
||||
|
||||
# If TC_FW_DEFINED is true, then we can do anything with this PIN at the firmware
|
||||
# level. First application is to unlock spending stuff.
|
||||
TCA_SP_UNLOCK = const(0x0001) # spending policy unlock
|
||||
|
||||
# special "pin" used as catch-all for wrong pins
|
||||
WRONG_PIN_CODE = '!p'
|
||||
|
||||
@ -94,22 +101,6 @@ class TrickPinMgmt:
|
||||
|
||||
def __init__(self):
|
||||
assert uctypes.sizeof(TRICK_SLOT_LAYOUT) == 128
|
||||
self.reload()
|
||||
|
||||
def reload(self):
|
||||
# we track known PINS as a dictionary:
|
||||
# pin (in ascii) => (slot_num, tc_flags, arg)
|
||||
from glob import settings
|
||||
self.tp = settings.get('tp', {})
|
||||
|
||||
def save_record(self):
|
||||
# commit changes back to settings
|
||||
from glob import settings
|
||||
if self.tp:
|
||||
settings.set('tp', self.tp)
|
||||
else:
|
||||
settings.remove_key('tp')
|
||||
settings.save()
|
||||
|
||||
def roundtrip(self, method_num, slot_buf=None):
|
||||
from pincodes import pa
|
||||
@ -129,26 +120,36 @@ class TrickPinMgmt:
|
||||
|
||||
return rc
|
||||
|
||||
def get_all(self):
|
||||
return settings.get("tp", {})
|
||||
|
||||
def commit(self, trick_pins):
|
||||
settings.set("tp", trick_pins)
|
||||
settings.save()
|
||||
|
||||
def clear_all(self):
|
||||
# get rid of them all
|
||||
self.roundtrip(0)
|
||||
self.tp = {}
|
||||
self.save_record()
|
||||
settings.remove_key('tp')
|
||||
settings.save()
|
||||
|
||||
def forget_pin(self, pin):
|
||||
# forget about settings for a PIN
|
||||
self.tp.pop(pin, None)
|
||||
self.save_record()
|
||||
t_pins = self.get_all()
|
||||
t_pins.pop(pin, None)
|
||||
self.commit(t_pins)
|
||||
|
||||
def restore_pin(self, new_pin):
|
||||
# remember/restore PIN that we "forgot", return T if worked
|
||||
b, slot = tp.get_by_pin(new_pin)
|
||||
b, slot = self.get_by_pin(new_pin)
|
||||
if slot is None: return False
|
||||
|
||||
record = (slot.slot_num, slot.tc_flags,
|
||||
0xffff if slot.tc_flags & TC_DELTA_MODE else slot.tc_arg)
|
||||
self.tp[new_pin] = record
|
||||
self.save_record()
|
||||
|
||||
t_pins = self.get_all()
|
||||
t_pins[new_pin] = record
|
||||
self.commit(t_pins)
|
||||
|
||||
return True
|
||||
|
||||
@ -221,17 +222,18 @@ class TrickPinMgmt:
|
||||
|
||||
# pick a free slot
|
||||
sn = self.find_empty_slots(1 if not secret else 1+(len(secret)//32))
|
||||
if sn == None:
|
||||
if sn is None:
|
||||
# we are full
|
||||
raise RuntimeError("no space left")
|
||||
|
||||
slot.slot_num = sn
|
||||
|
||||
t_pins = self.get_all()
|
||||
if new_pin is not None:
|
||||
slot.pin_len = len(new_pin)
|
||||
slot.pin[0:slot.pin_len] = new_pin
|
||||
if new_pin != pin:
|
||||
self.tp.pop(pin.decode(), None)
|
||||
t_pins.pop(pin.decode(), None)
|
||||
pin = new_pin
|
||||
|
||||
if tc_flags is not None:
|
||||
@ -265,14 +267,18 @@ class TrickPinMgmt:
|
||||
assert rc == 0
|
||||
|
||||
# record key details.
|
||||
self.tp[pin.decode()] = record
|
||||
self.save_record()
|
||||
t_pins[pin.decode()] = record
|
||||
self.commit(t_pins)
|
||||
|
||||
return b, slot
|
||||
|
||||
def all_tricks(self):
|
||||
# put them in order, with "wrong" last
|
||||
return sorted(self.tp.keys(), key=lambda i: i if (i != WRONG_PIN_CODE) else 'Z')
|
||||
return sorted(self.get_all().keys(), key=lambda i: i if (i != WRONG_PIN_CODE) else 'Z')
|
||||
|
||||
def define_unlock_pin(self, new_pin):
|
||||
# user is setting the bypass PIN for first time.
|
||||
self.update_slot(new_pin.encode(), new=True, tc_flags=TC_FW_DEFINED, tc_arg=TCA_SP_UNLOCK)
|
||||
|
||||
def was_countdown_pin(self):
|
||||
# was the trick pin just used? if so how much delay needed (or zero if not)
|
||||
@ -284,24 +290,51 @@ class TrickPinMgmt:
|
||||
else:
|
||||
return 0
|
||||
|
||||
def was_sp_unlock(self):
|
||||
# was a trick pin just used that enables acess to spending policy?
|
||||
# - ok if it's also a trick PIN .. a wiping bypass for example
|
||||
from pincodes import pa
|
||||
tc_flags, tc_arg = pa.get_tc_values()
|
||||
return bool(tc_flags & TC_FW_DEFINED) and (tc_arg == TCA_SP_UNLOCK)
|
||||
|
||||
def has_sp_unlock(self):
|
||||
# if spending policy defined, this PIN allows adjustment
|
||||
# - not TRICK bypass choices, like ones that wipe
|
||||
# - could be multiple, but only first returned.
|
||||
for k, (sn,flags,arg) in self.get_all().items():
|
||||
if (flags == TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
|
||||
return k
|
||||
return None
|
||||
|
||||
def delete_sp_unlock_pins(self):
|
||||
# remove all bypass pins, they are done w/ feature
|
||||
for k, (sn,flags,arg) in self.get_all().items():
|
||||
if (flags & TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
|
||||
self.clear_slots([sn])
|
||||
self.forget_pin(k)
|
||||
|
||||
|
||||
def get_deltamode_pins(self):
|
||||
# iterate over all delta-mode PIN's defined.
|
||||
for k, (sn,flags,args) in self.tp.items():
|
||||
for k, (sn,flags,args) in self.get_all().items():
|
||||
if flags & TC_DELTA_MODE:
|
||||
yield k
|
||||
|
||||
def get_duress_pins(self):
|
||||
# iterate over all duress wallets
|
||||
for k, (sn,flags,args) in self.tp.items():
|
||||
for k, (sn,flags,args) in self.get_all().items():
|
||||
if flags & (TC_WORD_WALLET | TC_XPRV_WALLET):
|
||||
yield k
|
||||
|
||||
def check_new_main_pin(self, pin):
|
||||
# user is trying to change main PIN to new value; check for issues
|
||||
# - dups bad but also: delta mode pin might not work w/ longer main true pin
|
||||
# - deciding whether TP already exists must be done via comms with SE2
|
||||
# as checking only self.tp is not sufficient for hidden TPs or after fast wipe
|
||||
# - return error msg or None
|
||||
assert isinstance(pin, str)
|
||||
if pin in self.tp:
|
||||
b, slot = self.get_by_pin(pin)
|
||||
if slot is not None:
|
||||
return 'That PIN is already in use as a Trick PIN.'
|
||||
|
||||
for d_pin in self.get_deltamode_pins():
|
||||
@ -319,8 +352,9 @@ class TrickPinMgmt:
|
||||
def backup_duress_wallets(self, sv):
|
||||
# for backup file, yield (label, path, pairs-of-data)
|
||||
done = set()
|
||||
t_pins = self.get_all()
|
||||
for pin in self.get_duress_pins():
|
||||
sn, flags, arg = self.tp[pin]
|
||||
sn, flags, arg = t_pins[pin]
|
||||
|
||||
if (flags, arg) in done:
|
||||
continue
|
||||
@ -330,7 +364,7 @@ class TrickPinMgmt:
|
||||
label = "Duress: BIP-85 Derived wallet"
|
||||
nwords = 12 if ((arg // 1000) == 2) else 24
|
||||
path = "BIP85(words=%d, index=%d)" % (nwords, arg)
|
||||
b, slot = tp.get_by_pin(pin)
|
||||
b, slot = self.get_by_pin(pin)
|
||||
words = bip39.b2a_words(slot.xdata[0:(32 if nwords==24 else 16)])
|
||||
|
||||
d = [ ('duress_%d_words' % arg, words) ]
|
||||
@ -369,10 +403,15 @@ class TrickPinMgmt:
|
||||
# might need to construct a BIP-85 or XPRV secret to match
|
||||
path, new_secret = construct_duress_secret(flags, arg)
|
||||
|
||||
b, slot = tp.update_slot(pin.encode(), new=True,
|
||||
tc_flags=flags, tc_arg=arg, secret=new_secret)
|
||||
except Exception as exc:
|
||||
sys.print_exception(exc) # not visible
|
||||
self.update_slot(pin.encode(), new=True, tc_flags=flags,
|
||||
tc_arg=arg, secret=new_secret)
|
||||
except: pass
|
||||
|
||||
@staticmethod
|
||||
async def err_unique_pin(pin):
|
||||
# standardized error UX
|
||||
return await ux_show_story(
|
||||
"That PIN (%s) is already in use. All PIN codes must be unique." % pin)
|
||||
|
||||
|
||||
tp = TrickPinMgmt()
|
||||
@ -401,7 +440,6 @@ class TrickPinMenu(MenuSystem):
|
||||
if bool(pa.tmp_value):
|
||||
return [MenuItem('Not Available')]
|
||||
|
||||
tp.reload()
|
||||
tricks = tp.all_tricks()
|
||||
|
||||
if self.current_pin in tricks:
|
||||
@ -489,7 +527,7 @@ class TrickPinMenu(MenuSystem):
|
||||
tc_arg=tc_arg, secret=new_secret)
|
||||
await ux_dramatic_pause("Saved.", 1)
|
||||
except BaseException as exc:
|
||||
sys.print_exception(exc)
|
||||
# sys.print_exception(exc)
|
||||
await ux_show_story("Failed: %s" % exc)
|
||||
|
||||
self.update_contents()
|
||||
@ -518,8 +556,7 @@ class TrickPinMenu(MenuSystem):
|
||||
have.remove(existing_pin)
|
||||
|
||||
if (new_pin == self.current_pin) or (new_pin in have):
|
||||
await ux_show_story("That PIN (%s) is already in use. All PIN codes must be unique." % new_pin)
|
||||
return
|
||||
return await tp.err_unique_pin(new_pin)
|
||||
|
||||
# check if we "forgot" this pin, and read it back if we did.
|
||||
# - important this is after the above checks so we don't reveal any trick pin used
|
||||
@ -604,6 +641,9 @@ the seed phrase, but still a somewhat riskier mode.
|
||||
For this mode only, trick PIN must be same length as true PIN and \
|
||||
differ only in final 4 positions (ignoring dash).\
|
||||
''', flags=TC_DELTA_MODE),
|
||||
StoryMenuItem('Policy Unlock', "Adds (another?) Spending Policy unlock PIN.", flags=TC_FW_DEFINED, arg=TCA_SP_UNLOCK),
|
||||
StoryMenuItem('Policy Unlock & Wipe' if version.has_qwerty else 'P.U. & Wipe',
|
||||
"Pretends correct Spending Policy unlock PIN given, but silently wipes seed before asking for main PIN.", flags=TC_FW_DEFINED|TC_WIPE, arg=TCA_SP_UNLOCK),
|
||||
]
|
||||
m = MenuSystem(FirstMenu)
|
||||
m.goto_idx(1)
|
||||
@ -632,23 +672,31 @@ setting) the Coldcard will always brick after 13 failed PIN attempts.''')
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem('[%s WRONG PIN]' % rel),
|
||||
StoryMenuItem('Wipe, Stop', "Seed is wiped and a message is shown.",
|
||||
arg=num, flags=TC_WIPE),
|
||||
arg=num, flags=TC_WIPE),
|
||||
StoryMenuItem('Wipe & Reboot', "Seed is wiped and Coldcard reboots without notice.",
|
||||
arg=num, flags=TC_WIPE|TC_REBOOT),
|
||||
arg=num, flags=TC_WIPE|TC_REBOOT),
|
||||
StoryMenuItem('Silent Wipe', "Seed is silently wiped and Coldcard acts as if PIN code was just wrong.",
|
||||
arg=num, flags=TC_WIPE|TC_FAKE_OUT),
|
||||
StoryMenuItem('Brick Self', "Become a brick instantly and forever.", flags=TC_BRICK, arg=num),
|
||||
StoryMenuItem('Last Chance', "Wipe seed, then give one more try and then brick if wrong PIN.", arg=num, flags=TC_WIPE|TC_BRICK),
|
||||
StoryMenuItem('Just Reboot', "Reboot when this happens. Doesn't do anything else.", arg=num, flags=TC_REBOOT),
|
||||
arg=num, flags=TC_WIPE|TC_FAKE_OUT),
|
||||
StoryMenuItem('Brick Self', "Become a brick instantly and forever.",
|
||||
arg=num, flags=TC_BRICK,),
|
||||
StoryMenuItem('Last Chance', "Wipe seed, then give one more try and then brick if wrong PIN.",
|
||||
arg=num, flags=TC_WIPE|TC_BRICK),
|
||||
StoryMenuItem('Just Reboot', "Reboot when this happens. Doesn't do anything else.",
|
||||
arg=num, flags=TC_REBOOT),
|
||||
])
|
||||
|
||||
m.goto_idx(1)
|
||||
the_ux.push(m)
|
||||
|
||||
async def clear_all(self, m,l,item):
|
||||
|
||||
if not await ux_confirm("Remove ALL TRICK PIN codes and special wrong-pin handling?"):
|
||||
return
|
||||
|
||||
if tp.has_sp_unlock():
|
||||
if not await ux_confirm("You will not be able to bypass spending policy anymore."):
|
||||
return
|
||||
|
||||
if any(tp.get_duress_pins()):
|
||||
if not await ux_confirm("Any funds on the duress wallet(s) have been moved already?"):
|
||||
return
|
||||
@ -657,7 +705,7 @@ setting) the Coldcard will always brick after 13 failed PIN attempts.''')
|
||||
m.update_contents()
|
||||
|
||||
async def hide_pin(self, m,l, item):
|
||||
pin, slot_num, flags = item.arg
|
||||
pin, slot_num, flags, arg = item.arg
|
||||
|
||||
if flags & TC_DELTA_MODE:
|
||||
await ux_show_story('''Delta mode PIN will be hidden if trick PIN menu is shown \
|
||||
@ -665,12 +713,14 @@ to attacker, and we need to update this record if the main PIN is changed, so we
|
||||
hiding this item.''')
|
||||
return
|
||||
|
||||
if pin != WRONG_PIN_CODE:
|
||||
if (flags == TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
|
||||
msg = "It will still be possible to change or disable the spending policy if this PIN is known."
|
||||
elif pin == WRONG_PIN_CODE:
|
||||
msg = "This will hide what happens with wrong PINs from the menus but it will still be in effect."
|
||||
else:
|
||||
msg = '''This will hide the PIN from the menus but it will still be in effect.
|
||||
|
||||
You can restore it by trying to re-add the same PIN (%s) again later.''' % pin
|
||||
else:
|
||||
msg = "This will hide what happens with wrong PINs from the menus but it will still be in effect."
|
||||
|
||||
if not await ux_confirm(msg): return
|
||||
|
||||
@ -706,16 +756,20 @@ You can restore it by trying to re-add the same PIN (%s) again later.''' % pin
|
||||
|
||||
self.pop_submenu() # too lazy to get redraw right
|
||||
except BaseException as exc:
|
||||
sys.print_exception(exc)
|
||||
# sys.print_exception(exc)
|
||||
await ux_show_story("Failed: %s" % exc)
|
||||
|
||||
async def delete_pin(self, m,l, item):
|
||||
pin, slot_num, flags = item.arg
|
||||
pin, slot_num, flags, arg = item.arg
|
||||
|
||||
if flags & (TC_WORD_WALLET | TC_XPRV_WALLET):
|
||||
if not await ux_confirm("Any funds on this duress wallet have been moved already?"):
|
||||
return
|
||||
|
||||
if (flags == TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
|
||||
if not await ux_confirm("Changes to the spending policy will not be possible anymore."):
|
||||
return
|
||||
|
||||
if pin == WRONG_PIN_CODE:
|
||||
msg = "Remove special handling of wrong PINs?"
|
||||
else:
|
||||
@ -743,11 +797,9 @@ You can restore it by trying to re-add the same PIN (%s) again later.''' % pin
|
||||
|
||||
ch = await ux_show_story('''\
|
||||
This will temporarily load the secrets associated with this trick wallet \
|
||||
so you may perform transactions with it. Reboot the Coldcard to restore \
|
||||
normal operation.''')
|
||||
so you may perform transactions with it.''')
|
||||
if ch != 'y': return
|
||||
|
||||
from pincodes import pa, AE_SECRET_LEN
|
||||
b, slot = tp.get_by_pin(pin)
|
||||
assert slot
|
||||
|
||||
@ -771,7 +823,7 @@ normal operation.''')
|
||||
|
||||
# switch over to new secret!
|
||||
dis.fullscreen("Applying...")
|
||||
await set_ephemeral_seed(encoded, meta=name)
|
||||
await set_ephemeral_seed(encoded, origin=name)
|
||||
goto_top_menu()
|
||||
|
||||
async def countdown_details(self, m, l, item):
|
||||
@ -784,7 +836,7 @@ normal operation.''')
|
||||
|
||||
# "arg" can be out-of-date, if they edited timer value after parent was
|
||||
# rendered, where arg was captured into item.arg ... so don't use it.
|
||||
cd_val = tp.tp[pin][2]
|
||||
cd_val = tp.get_all()[pin][2]
|
||||
|
||||
msg = 'Shows login countdown (%s)' % lgto_map.get(cd_val, '???').strip()
|
||||
if flags & TC_WIPE:
|
||||
@ -800,16 +852,14 @@ normal operation.''')
|
||||
|
||||
def adjust_countdown_chooser():
|
||||
# 'disabled' choice not appropriate for this case
|
||||
ch = lgto_ch[1:]
|
||||
va = lgto_va[1:]
|
||||
|
||||
def set_it(idx, text):
|
||||
new_val = va[idx]
|
||||
# save it
|
||||
try:
|
||||
b, slot = tp.update_slot(pin.encode(), tc_flags=flags, tc_arg=new_val)
|
||||
except BaseException as exc:
|
||||
sys.print_exception(exc)
|
||||
tp.update_slot(pin.encode(), tc_flags=flags, tc_arg=new_val)
|
||||
except: pass
|
||||
|
||||
return va.index(cd_val), lgto_ch[1:], set_it
|
||||
|
||||
@ -833,7 +883,8 @@ Wallet is XPRV-based and derived from a fixed path.''' % pin
|
||||
if ch != '6': return
|
||||
|
||||
b, s = tp.get_by_pin(pin)
|
||||
if s == None:
|
||||
if s is None:
|
||||
title = None
|
||||
# could not find in SE2. Our settings vs. SE2 are not in sync.
|
||||
msg = "Not found in SE2. Delete and remake."
|
||||
else:
|
||||
@ -841,25 +892,25 @@ Wallet is XPRV-based and derived from a fixed path.''' % pin
|
||||
|
||||
assert s.tc_flags == flags
|
||||
if flags & TC_XPRV_WALLET:
|
||||
node = ngu.hdnode.HDNode()
|
||||
ch, pk = s.xdata[0:32], s.xdata[32:64]
|
||||
node.from_chaincode_privkey(ch, pk)
|
||||
node = node_from_privkey(pk, ch)
|
||||
|
||||
msg, *_ = render_master_secrets('xprv', None, node)
|
||||
title, msg, *_ = render_master_secrets('xprv', None, node)
|
||||
elif flags & TC_WORD_WALLET:
|
||||
raw = s.xdata[0:(32 if nwords == 24 else 16)]
|
||||
msg, *_ = render_master_secrets('words', raw, None)
|
||||
title, msg, *_ = render_master_secrets('words', raw, None)
|
||||
else:
|
||||
raise ValueError(hex(flags))
|
||||
|
||||
await ux_show_story(msg, sensitive=True)
|
||||
await ux_show_story(msg, title=title, sensitive=True)
|
||||
|
||||
|
||||
async def pin_submenu(self, menu, label, item):
|
||||
# drill down into a sub-menu per existing PIN
|
||||
# - data display only, no editing; just clear and redo
|
||||
pin = item.arg
|
||||
slot_num, flags, arg = tp.tp[pin] if (pin in tp.tp) else (-1, 0, 0)
|
||||
t_pins = tp.get_all()
|
||||
slot_num, flags, arg = t_pins[pin] if (pin in t_pins) else (-1, 0, 0)
|
||||
|
||||
rv = []
|
||||
|
||||
@ -878,6 +929,8 @@ Wallet is XPRV-based and derived from a fixed path.''' % pin
|
||||
rv.append(MenuItem("↳Pretends Wrong"))
|
||||
elif flags & TC_DELTA_MODE:
|
||||
rv.append(MenuItem("↳Delta Mode"))
|
||||
elif (flags & TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
|
||||
rv.append(MenuItem("↳Unlock Policy")) # width issues on Mk4
|
||||
|
||||
for m, msg in [
|
||||
(TC_WIPE, '↳Wipes seed'),
|
||||
@ -891,8 +944,8 @@ Wallet is XPRV-based and derived from a fixed path.''' % pin
|
||||
rv.append(MenuItem("Activate Wallet", f=self.activate_wallet, arg=(pin, flags, arg)))
|
||||
|
||||
rv.extend([
|
||||
MenuItem('Hide Trick', f=self.hide_pin, arg=(pin, slot_num, flags)),
|
||||
MenuItem('Delete Trick', f=self.delete_pin, arg=(pin, slot_num, flags)),
|
||||
MenuItem('Hide Trick', f=self.hide_pin, arg=(pin, slot_num, flags, arg)),
|
||||
MenuItem('Delete Trick', f=self.delete_pin, arg=(pin, slot_num, flags, arg)),
|
||||
])
|
||||
if pin != WRONG_PIN_CODE:
|
||||
rv.append(
|
||||
@ -903,6 +956,7 @@ Wallet is XPRV-based and derived from a fixed path.''' % pin
|
||||
|
||||
class StoryMenuItem(MenuItem):
|
||||
def __init__(self, label, story, flags=0, **kws):
|
||||
# arg= .. handled by super
|
||||
self.story = story
|
||||
self.flags = flags
|
||||
super().__init__(label, **kws)
|
||||
|
||||
218
shared/usb.py
218
shared/usb.py
@ -2,16 +2,17 @@
|
||||
#
|
||||
# usb.py - USB related things
|
||||
#
|
||||
import ckcc, pyb, callgate, sys, ux, ngu, stash, aes256ctr
|
||||
import ckcc, pyb, callgate, sys, ux, ngu, stash, aes256ctr, ujson
|
||||
from uasyncio import sleep_ms, core
|
||||
from uhashlib import sha256
|
||||
from public_constants import MAX_MSG_LEN, MAX_BLK_LEN, AFC_SCRIPT
|
||||
from public_constants import MAX_MSG_LEN, MAX_BLK_LEN
|
||||
from public_constants import STXN_FLAGS_MASK
|
||||
from ustruct import pack, unpack_from
|
||||
from ckcc import watchpoint, is_simulator
|
||||
from utils import problem_file_line, call_later_ms
|
||||
from version import supports_hsm, is_devmode, MAX_TXN_LEN, MAX_UPLOAD_LEN
|
||||
from exceptions import FramingError, CCBusyError, HSMDenied, HSMCMDDisabled
|
||||
from exceptions import FramingError, CCBusyError, HSMDenied, HSMCMDDisabled, SpendPolicyViolation
|
||||
from pincodes import pa
|
||||
|
||||
# Unofficial, unpermissioned... numbers
|
||||
COINKITE_VID = 0xd13e
|
||||
@ -52,8 +53,8 @@ HSM_WHITELIST = frozenset({
|
||||
'smsg', # limited by policy
|
||||
'blkc', 'hsts', # report status values
|
||||
'stok', 'smok', # completion check: sign txn or msg
|
||||
'xpub', 'msck', # quick status checks
|
||||
'p2sh', 'show', # limited by HSM policy
|
||||
'xpub', # quick status checks
|
||||
'show', 'msas', # limited by HSM policy
|
||||
'user', # auth HSM user, other user cmds not allowed
|
||||
'gslr', # read storage locker; hsm mode only, limited usage
|
||||
})
|
||||
@ -68,6 +69,21 @@ HSM_DISABLE_CMDS = frozenset({
|
||||
"hsms",
|
||||
})
|
||||
|
||||
# spending policy active: blacklist some commands
|
||||
# - 'pass' may be allowed if 'okeys' is enabled
|
||||
HOBBLED_CMDS = frozenset({
|
||||
'enrl', # no new multisigs during policy enforcement
|
||||
'back', # no backups
|
||||
'bagi', 'dfu_', # just in case
|
||||
|
||||
"user", # same as HSM_DISABLE_CMDS
|
||||
"rmur",
|
||||
"nwur",
|
||||
"gslr",
|
||||
"hsts",
|
||||
"hsms",
|
||||
})
|
||||
|
||||
# singleton instance of USBHandler()
|
||||
handler = None
|
||||
|
||||
@ -117,6 +133,16 @@ def is_vcp_active():
|
||||
|
||||
return cur and ('VCP' in cur) and en
|
||||
|
||||
|
||||
def get_miniscript_by_name(name_bytes):
|
||||
from wallet import MiniScriptWallet
|
||||
|
||||
for w in MiniScriptWallet.iter_wallets():
|
||||
if w.name == str(name_bytes, 'ascii'):
|
||||
return True, w
|
||||
else:
|
||||
return False, b'err_Miniscript wallet not found'
|
||||
|
||||
class USBHandler:
|
||||
def __init__(self):
|
||||
self.dev = pyb.USB_HID()
|
||||
@ -169,6 +195,7 @@ class USBHandler:
|
||||
msg_len = 0
|
||||
|
||||
while 1:
|
||||
success = False
|
||||
yield core._io_queue.queue_read(self.blockable)
|
||||
|
||||
try:
|
||||
@ -212,14 +239,14 @@ class USBHandler:
|
||||
# this saves memory over a simple slice (confirmed)
|
||||
args = memoryview(self.msg)[4:msg_len]
|
||||
resp = await self.handle(self.msg[0:4], args)
|
||||
msg_len = 0
|
||||
success = True
|
||||
except CCBusyError:
|
||||
# auth UX is doing something else
|
||||
resp = b'busy'
|
||||
msg_len = 0
|
||||
except SpendPolicyViolation:
|
||||
resp = b'err_Spending policy in effect'
|
||||
except HSMDenied:
|
||||
resp = b'err_Not allowed in HSM mode'
|
||||
msg_len = 0
|
||||
except HSMCMDDisabled:
|
||||
# do NOT change below error msg as other applications depend on it
|
||||
resp = b'err_HSM commands disabled'
|
||||
@ -227,16 +254,14 @@ class USBHandler:
|
||||
except (ValueError, AssertionError) as exc:
|
||||
# some limited invalid args feedback
|
||||
#print("USB request caused assert: ", end='')
|
||||
#sys.print_exception(exc)
|
||||
# sys.print_exception(exc)
|
||||
msg = str(exc)
|
||||
if not msg:
|
||||
msg = 'Assertion ' + problem_file_line(exc)
|
||||
resp = b'err_' + msg.encode()[0:80]
|
||||
msg_len = 0
|
||||
except MemoryError:
|
||||
# prefer to catch at higher layers, but sometimes can't
|
||||
resp = b'err_Out of RAM'
|
||||
msg_len = 0
|
||||
except FramingError as exc:
|
||||
raise exc
|
||||
except Exception as exc:
|
||||
@ -245,9 +270,15 @@ class USBHandler:
|
||||
print("USB request caused this: ", end='')
|
||||
sys.print_exception(exc)
|
||||
resp = b'err_Confused ' + problem_file_line(exc)
|
||||
msg_len = 0
|
||||
|
||||
# aways send a reply if they get this far
|
||||
if not success:
|
||||
# do not let the progress screen hang on "Receiving..."
|
||||
from ux import restore_menu
|
||||
restore_menu()
|
||||
|
||||
msg_len = 0
|
||||
|
||||
# always send a reply if they get this far
|
||||
await self.send_response(resp)
|
||||
|
||||
except FramingError as exc:
|
||||
@ -342,7 +373,7 @@ class USBHandler:
|
||||
except:
|
||||
raise FramingError('decode')
|
||||
|
||||
if cmd[0].isupper() and is_devmode:
|
||||
if is_devmode and cmd[0].isupper():
|
||||
# special hacky commands to support testing w/ the simulator
|
||||
try:
|
||||
from usb_test_commands import do_usb_command
|
||||
@ -355,7 +386,18 @@ class USBHandler:
|
||||
if cmd not in HSM_WHITELIST:
|
||||
raise HSMDenied
|
||||
|
||||
if not settings.get('hsmcmd', False):
|
||||
if pa.hobbled_mode:
|
||||
# block some commands when we are hobbled.
|
||||
if cmd in HOBBLED_CMDS:
|
||||
raise SpendPolicyViolation
|
||||
|
||||
if cmd in {'pwok', 'pass'}:
|
||||
from ccc import sssp_spending_policy
|
||||
if not sssp_spending_policy('okeys'):
|
||||
raise SpendPolicyViolation
|
||||
|
||||
elif not settings.get('hsmcmd', False):
|
||||
# block these HSM-related command if not using feature
|
||||
if cmd in HSM_DISABLE_CMDS:
|
||||
raise HSMCMDDisabled
|
||||
|
||||
@ -432,39 +474,6 @@ class USBHandler:
|
||||
sign_msg(msg, subpath, addr_fmt)
|
||||
return None
|
||||
|
||||
if cmd == 'p2sh':
|
||||
# show P2SH (probably multisig) address on screen (also provides it back)
|
||||
# - must provide redeem script, and list of [xfp+path]
|
||||
from auth import start_show_p2sh_address
|
||||
|
||||
if hsm_active and not hsm_active.approve_address_share(is_p2sh=True):
|
||||
raise HSMDenied
|
||||
|
||||
# new multsig goodness, needs mapping from xfp->path and M values
|
||||
addr_fmt, M, N, script_len = unpack_from('<IBBH', args)
|
||||
|
||||
assert addr_fmt & AFC_SCRIPT
|
||||
assert 1 <= M <= N <= 20
|
||||
assert 30 <= script_len <= 520
|
||||
|
||||
offset = 8
|
||||
witdeem_script = args[offset:offset+script_len]
|
||||
offset += script_len
|
||||
|
||||
assert len(witdeem_script) == script_len
|
||||
|
||||
xfp_paths = []
|
||||
for i in range(N):
|
||||
ln = args[offset]
|
||||
assert 1 <= ln <= 16, 'badlen'
|
||||
xfp_paths.append(unpack_from('<%dI' % ln, args, offset+1))
|
||||
offset += (ln*4) + 1
|
||||
|
||||
assert offset == len(args)
|
||||
|
||||
return b'asci' + start_show_p2sh_address(M, N, addr_fmt, xfp_paths,
|
||||
witdeem_script)
|
||||
|
||||
if cmd == 'show':
|
||||
# simple cases, older code: text subpath
|
||||
from auth import usb_show_address
|
||||
@ -483,7 +492,7 @@ class USBHandler:
|
||||
file_len, file_sha = unpack_from('<I32s', args)
|
||||
if file_sha != self.file_checksum.digest():
|
||||
return b'err_Checksum'
|
||||
assert 100 < file_len <= (20*200), "badlen"
|
||||
assert 100 < file_len <= (32*200), "badlen"
|
||||
|
||||
# Start an UX interaction, return immediately here
|
||||
from auth import maybe_enroll_xpub
|
||||
@ -491,12 +500,71 @@ class USBHandler:
|
||||
|
||||
return None
|
||||
|
||||
if cmd == 'msck':
|
||||
# Quick check to test if we have a wallet already installed.
|
||||
from multisig import MultisigWallet
|
||||
M, N, xfp_xor = unpack_from('<3I', args)
|
||||
if cmd == 'mins':
|
||||
# Enroll new xpubkey to be involved in miniscript.
|
||||
# - descriptor text config file must already be uploaded
|
||||
|
||||
return int(MultisigWallet.quick_check(M, N, xfp_xor))
|
||||
file_len, file_sha = unpack_from('<I32s', args)
|
||||
if file_sha != self.file_checksum.digest():
|
||||
return b'err_Checksum'
|
||||
assert 100 < file_len <= (100 * 200), "badlen"
|
||||
|
||||
# Start an UX interaction, return immediately here
|
||||
from auth import maybe_enroll_xpub
|
||||
maybe_enroll_xpub(sf_len=file_len, ux_reset=True)
|
||||
|
||||
return None
|
||||
|
||||
if cmd.startswith("ms"):
|
||||
# miniscript related commands
|
||||
assert self.encrypted_req, 'must encrypt'
|
||||
|
||||
if cmd == "msls":
|
||||
# list all registered miniscript wallet names
|
||||
from wallet import MiniScriptWallet
|
||||
wallets = [w.name for w in MiniScriptWallet.iter_wallets()]
|
||||
return b'asci' + ujson.dumps(wallets)
|
||||
|
||||
if cmd == "msas":
|
||||
# get miniscript address based on int/ext index
|
||||
if hsm_active and not hsm_active.approve_address_share(miniscript=True):
|
||||
raise HSMDenied
|
||||
|
||||
change, idx, = unpack_from('<II', args)
|
||||
assert change in (0, 1), "change not bool"
|
||||
assert 0 <= idx < (2 ** 31), "child idx"
|
||||
|
||||
name = args[8:]
|
||||
assert len(name) <= 32, "name len"
|
||||
|
||||
ok, w = get_miniscript_by_name(name)
|
||||
if not ok:
|
||||
return w
|
||||
|
||||
from auth import start_show_miniscript_address
|
||||
return b'asci' + start_show_miniscript_address(w, change, idx)
|
||||
|
||||
|
||||
assert len(args) <= 32, "name len"
|
||||
ok, w = get_miniscript_by_name(args)
|
||||
if not ok:
|
||||
return w
|
||||
|
||||
if cmd == "msdl":
|
||||
# delete miniscript wallet by its name (unique id)
|
||||
from auth import maybe_delete_miniscript
|
||||
maybe_delete_miniscript(w)
|
||||
return None
|
||||
|
||||
if cmd == "msgt":
|
||||
# takes name and returns descriptor + name json
|
||||
# MiniscriptWallet.to_string only fills policy
|
||||
return b'asci' + ujson.dumps({"name": w.name, "desc": w.to_string()})
|
||||
|
||||
if cmd == "mspl":
|
||||
# takes name and returns BIP-388 Wallet Policy
|
||||
return b'asci' + ujson.dumps({"name": w.name, "desc_template": w.desc_tmplt,
|
||||
"keys_info": w.keys_info})
|
||||
|
||||
if cmd == 'stxn':
|
||||
# sign transaction
|
||||
@ -506,8 +574,22 @@ class USBHandler:
|
||||
|
||||
assert 50 < txn_len <= MAX_TXN_LEN, "badlen"
|
||||
|
||||
# optional miniscript wallet name
|
||||
try:
|
||||
name_len = unpack_from('B', args[40:])[0]
|
||||
name = str(args[41:41 + name_len], "ascii")
|
||||
assert 1 <= len(name) <= 32, "name len"
|
||||
except:
|
||||
name = None
|
||||
|
||||
w = None
|
||||
if name:
|
||||
ok, w = get_miniscript_by_name(name)
|
||||
if not ok:
|
||||
return w
|
||||
|
||||
from auth import sign_transaction
|
||||
sign_transaction(txn_len, (flags & STXN_FLAGS_MASK), txn_sha)
|
||||
sign_transaction(txn_len, (flags & STXN_FLAGS_MASK), txn_sha, miniscript_wallet=w)
|
||||
return None
|
||||
|
||||
if cmd == 'stok' or cmd == 'bkok' or cmd == 'smok' or cmd == 'pwok':
|
||||
@ -532,7 +614,6 @@ class USBHandler:
|
||||
# STILL waiting on user
|
||||
return None
|
||||
|
||||
|
||||
if cmd == 'pwok':
|
||||
# return new root xpub
|
||||
xpub = req.result
|
||||
@ -558,6 +639,7 @@ class USBHandler:
|
||||
assert settings.get("words", True), 'no seed'
|
||||
assert len(args) < 400, 'too long'
|
||||
pw = str(args, 'utf8')
|
||||
assert len(pw), 'too short'
|
||||
assert len(pw) < 100, 'too long'
|
||||
|
||||
return start_bip39_passphrase(pw)
|
||||
@ -567,6 +649,15 @@ class USBHandler:
|
||||
from auth import start_remote_backup
|
||||
return start_remote_backup()
|
||||
|
||||
if cmd == 'rest':
|
||||
# restore backup from what is already uploaded in PSRAM
|
||||
file_len, file_sha, bf = unpack_from('<I32sB', args)
|
||||
if file_sha != self.file_checksum.digest():
|
||||
return b'err_Checksum'
|
||||
|
||||
from auth import start_remote_restore_backup
|
||||
return start_remote_restore_backup(file_len, bf)
|
||||
|
||||
if cmd == 'blkc':
|
||||
# report which blockchain we are configured for
|
||||
from chains import current_chain
|
||||
@ -598,7 +689,6 @@ class USBHandler:
|
||||
if cmd == 'hsts':
|
||||
# can always query HSM mode
|
||||
from hsm import hsm_status_report
|
||||
import ujson
|
||||
return b'asci' + ujson.dumps(hsm_status_report())
|
||||
|
||||
if cmd == 'gslr':
|
||||
@ -738,29 +828,30 @@ class USBHandler:
|
||||
from glob import dis, hsm_active
|
||||
from utils import check_firmware_hdr
|
||||
from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE, FW_HEADER_MAGIC
|
||||
from pincodes import pa
|
||||
|
||||
# maintain a running SHA256 over what's received
|
||||
if offset == 0:
|
||||
self.file_checksum = sha256()
|
||||
self.is_fw_upgrade = False
|
||||
dis.fullscreen("Receiving...", 0)
|
||||
else:
|
||||
dis.progress_sofar(offset, total_size)
|
||||
|
||||
assert offset % 256 == 0, 'alignment'
|
||||
assert offset+len(data) <= total_size <= MAX_UPLOAD_LEN, 'long'
|
||||
|
||||
if hsm_active:
|
||||
# additional restrictions in HSM mode
|
||||
if hsm_active or pa.hobbled_mode:
|
||||
# additional restriction in HSM mode or hobbled: must be PSBT
|
||||
assert offset+len(data) <= total_size <= MAX_TXN_LEN, 'psbt'
|
||||
if offset == 0:
|
||||
assert data[0:5] == b'psbt\xff', 'psbt'
|
||||
|
||||
self.file_checksum.update(data)
|
||||
|
||||
for pos in range(offset, offset+len(data), 256):
|
||||
if pos % 4096 == 0:
|
||||
dis.fullscreen("Receiving...", offset/total_size)
|
||||
|
||||
# write up to 256 bytes
|
||||
here = data[pos-offset:pos-offset+256]
|
||||
self.file_checksum.update(here)
|
||||
|
||||
# Very special case for firmware upgrades: intercept and modify
|
||||
# header contents on the fly, and also fail faster if wouldn't work
|
||||
@ -829,7 +920,6 @@ class USBHandler:
|
||||
def handle_bag_number(self, bag_num):
|
||||
import version, callgate
|
||||
from glob import dis, settings
|
||||
from pincodes import pa
|
||||
|
||||
if bag_num and version.is_factory_mode and not version.has_qr:
|
||||
# check state first
|
||||
|
||||
324
shared/utils.py
324
shared/utils.py
@ -2,12 +2,13 @@
|
||||
#
|
||||
# utils.py - Misc utils. My favourite kind of source file.
|
||||
#
|
||||
import gc, sys, ustruct, ngu, chains, ure, time, bip39
|
||||
import gc, sys, ustruct, ngu, chains, ure, uos, uio, time, bip39, version, uasyncio
|
||||
from ubinascii import unhexlify as a2b_hex
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from ubinascii import a2b_base64, b2a_base64
|
||||
from charcodes import OUT_CTRL_ADDRESS, OUT_CTRL_NOWRAP
|
||||
from uhashlib import sha256
|
||||
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
|
||||
from public_constants import MAX_PATH_DEPTH, AF_CLASSIC
|
||||
|
||||
B2A = lambda x: str(b2a_hex(x), 'ascii')
|
||||
|
||||
@ -91,7 +92,6 @@ def pop_count(i):
|
||||
|
||||
def get_filesize(fn):
|
||||
# like os.path.getsize()
|
||||
import uos
|
||||
try:
|
||||
return uos.stat(fn)[6]
|
||||
except OSError:
|
||||
@ -205,22 +205,22 @@ def is_printable(s):
|
||||
return False
|
||||
return True
|
||||
|
||||
def to_ascii_printable(s, strip=False):
|
||||
def to_ascii_printable(s, strip=False, only_printable=True):
|
||||
try:
|
||||
s = str(s, 'ascii')
|
||||
if strip:
|
||||
s = s.strip()
|
||||
assert is_ascii(s)
|
||||
assert is_printable(s)
|
||||
if only_printable:
|
||||
assert is_printable(s)
|
||||
return s
|
||||
except:
|
||||
raise AssertionError('must be ascii printable')
|
||||
raise AssertionError("must be ascii" + (" printable" if only_printable else ""))
|
||||
|
||||
|
||||
def problem_file_line(exc):
|
||||
# return a string of just the filename.py and line number where
|
||||
# an exception occured. Best used on AssertionError.
|
||||
import uio, sys, ure
|
||||
|
||||
tmp = uio.StringIO()
|
||||
sys.print_exception(exc, tmp)
|
||||
@ -251,8 +251,6 @@ def cleanup_deriv_path(bin_path, allow_star=False):
|
||||
# - assume 'm' prefix, so '34' becomes 'm/34', etc
|
||||
# - do not assume /// is m/0/0/0
|
||||
# - if allow_star, then final position can be * or *h (wildcard)
|
||||
import ure
|
||||
from public_constants import MAX_PATH_DEPTH
|
||||
|
||||
s = to_ascii_printable(bin_path, strip=True).lower()
|
||||
|
||||
@ -264,7 +262,7 @@ def cleanup_deriv_path(bin_path, allow_star=False):
|
||||
|
||||
# regex for valid chars, m at start, maybe /*h or /* at end sometimes
|
||||
mat = ure.match(r"(m|m/|)[0-9/h]*" + ('' if not allow_star else r"(\*h|\*|)"), s)
|
||||
assert mat.group(0) == s, "invalid characters"
|
||||
assert mat.group(0) == s, "invalid characters in path"
|
||||
|
||||
parts = s.split('/')
|
||||
|
||||
@ -345,6 +343,13 @@ def match_deriv_path(patterns, path):
|
||||
|
||||
return False
|
||||
|
||||
def validate_derivation_path_length(length, allow_master=False):
|
||||
# force them to use a derived key, never the master
|
||||
if not allow_master:
|
||||
assert length >= 4, 'too short key path'
|
||||
assert (length % 4) == 0, 'corrupt key path'
|
||||
assert (length // 4) <= MAX_PATH_DEPTH, 'too deep'
|
||||
|
||||
class DecodeStreamer:
|
||||
def __init__(self):
|
||||
self.runt = bytearray()
|
||||
@ -385,7 +390,7 @@ def check_firmware_hdr(hdr, binary_size):
|
||||
# - hdr must be a bytearray(FW_HEADER_SIZE+more)
|
||||
|
||||
from sigheader import FW_HEADER_SIZE, FW_HEADER_MAGIC, FWH_PY_FORMAT
|
||||
from sigheader import MK_1_OK, MK_2_OK, MK_3_OK, MK_4_OK, MK_Q1_OK
|
||||
from sigheader import MK_1_OK, MK_2_OK, MK_3_OK, MK_4_OK, MK_5_OK, MK_Q1_OK
|
||||
from ustruct import unpack_from
|
||||
from version import hw_label
|
||||
import callgate
|
||||
@ -414,9 +419,11 @@ def check_firmware_hdr(hdr, binary_size):
|
||||
ok = (hw_compat & MK_3_OK)
|
||||
elif hw_label == 'mk4':
|
||||
ok = (hw_compat & MK_4_OK)
|
||||
elif hw_label == 'mk5':
|
||||
ok = (hw_compat & MK_5_OK)
|
||||
elif hw_label == 'q1':
|
||||
ok = (hw_compat & MK_Q1_OK)
|
||||
|
||||
|
||||
if not ok:
|
||||
return "That firmware doesn't support this version of Coldcard hardware (%s)."%hw_label
|
||||
|
||||
@ -431,7 +438,9 @@ def clean_shutdown(style=0):
|
||||
# wipe SPI flash and shutdown (wiping main memory)
|
||||
# - mk4: SPI flash not used, but NFC may hold data (PSRAM cleared by bootrom)
|
||||
# - bootrom wipes every byte of SRAM, so no need to repeat here
|
||||
import callgate, version, uasyncio
|
||||
# - style=2 => reboot and try login again
|
||||
# - default is logout and (if applicable) power down.
|
||||
import callgate
|
||||
|
||||
# save if anything pending
|
||||
from glob import settings
|
||||
@ -454,129 +463,87 @@ def clean_shutdown(style=0):
|
||||
callgate.show_logout(style)
|
||||
|
||||
def call_later_ms(delay, cb, *args, **kws):
|
||||
import uasyncio
|
||||
|
||||
async def doit():
|
||||
await uasyncio.sleep_ms(delay)
|
||||
await cb(*args, **kws)
|
||||
|
||||
uasyncio.create_task(doit())
|
||||
|
||||
def txtlen(s):
|
||||
# width of string in chars, accounting for
|
||||
# double-wide characters which happen on Q.
|
||||
rv = len(s)
|
||||
|
||||
if DOUBLE_WIDE:
|
||||
rv += sum(1 for ch in s if ch in DOUBLE_WIDE)
|
||||
|
||||
return rv
|
||||
|
||||
def word_wrap(ln, w):
|
||||
# Generate the lines needed to wrap one line into X "width"-long lines.
|
||||
# - tests in testing/test_unit.py
|
||||
|
||||
if txtlen(ln) <= w:
|
||||
yield ln
|
||||
if ln and (ln[0] == OUT_CTRL_NOWRAP):
|
||||
# no need to wrap this line - as requested by caller
|
||||
yield ln[1:]
|
||||
return
|
||||
|
||||
while ln:
|
||||
while True:
|
||||
# ln_len considers DOUBLE_WIDTH chars
|
||||
ln_len = 0
|
||||
sp = None
|
||||
for idx, ch in enumerate(ln):
|
||||
if ch == ' ':
|
||||
# split point on space if possible
|
||||
sp = idx
|
||||
|
||||
# find a space in (width) first part of remainder
|
||||
sp = ln.rfind(' ', 0, w-1)
|
||||
ln_len += 1
|
||||
if ch in DOUBLE_WIDE:
|
||||
ln_len += 1
|
||||
|
||||
if ln_len > w:
|
||||
# if one of .,:; is last -> allow one more character
|
||||
# even if only half visible on Mk4
|
||||
# on Q it's OK as (CHARS_W-1) is used as w
|
||||
if ch in ".,:;":
|
||||
idx += 1
|
||||
sp = None
|
||||
|
||||
break
|
||||
|
||||
else:
|
||||
yield ln
|
||||
return
|
||||
|
||||
if sp is None:
|
||||
if ln[0] == OUT_CTRL_ADDRESS:
|
||||
# special handling for lines w/ payment address in them
|
||||
# - add same marker to newly split lines
|
||||
addr = ln[1:]
|
||||
# - 3 4-char groups on Mk4
|
||||
# - 6 4-char groups on Q
|
||||
aw = 24 if version.has_qwerty else 12
|
||||
|
||||
pos = 0
|
||||
while pos < len(addr):
|
||||
yield OUT_CTRL_ADDRESS + addr[pos:pos+aw]
|
||||
pos += aw
|
||||
return
|
||||
|
||||
if sp == -1:
|
||||
# bad-break the line
|
||||
sp = min(txtlen(ln), w)
|
||||
nsp = sp
|
||||
if ln[nsp:nsp+1] == ' ':
|
||||
sp = nsp = idx
|
||||
if ln[sp:nsp+1] == " ":
|
||||
nsp += 1
|
||||
else:
|
||||
# split on found space
|
||||
nsp = sp+1
|
||||
|
||||
left = ln[0:sp]
|
||||
ln = ln[nsp:]
|
||||
|
||||
if txtlen(left) + 1 + txtlen(ln) <= w:
|
||||
# not clear when this would happen? final bit??
|
||||
left = left + ' ' + ln
|
||||
ln = ''
|
||||
|
||||
yield left
|
||||
ln = ln[nsp:]
|
||||
if not ln: return
|
||||
|
||||
def parse_addr_fmt_str(addr_fmt):
|
||||
# accepts strings and also integers if already parsed
|
||||
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
|
||||
|
||||
if addr_fmt in [AF_P2WPKH_P2SH, AF_P2WPKH, AF_CLASSIC]:
|
||||
return addr_fmt
|
||||
|
||||
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("Invalid address format: '%s'\n\n"
|
||||
"Choose from p2pkh, p2wpkh, p2sh-p2wpkh." % addr_fmt)
|
||||
|
||||
def parse_extended_key(ln, private=False):
|
||||
# read an xpub/ypub/etc and return BIP-32 node and what chain it's on.
|
||||
# - can handle any garbage line
|
||||
# - returns (node, chain, addr_fmt)
|
||||
# - people are using SLIP132 so we need this
|
||||
node, chain, addr_fmt = None, None, None
|
||||
if ln is None:
|
||||
return node, chain, addr_fmt
|
||||
|
||||
ln = ln.strip()
|
||||
if private:
|
||||
rgx = r'.prv[A-Za-z0-9]+'
|
||||
else:
|
||||
rgx = r'.pub[A-Za-z0-9]+'
|
||||
|
||||
pat = ure.compile(rgx)
|
||||
found = pat.search(ln)
|
||||
# serialize, and note version code
|
||||
try:
|
||||
node, chain, addr_fmt, is_private = chains.slip32_deserialize(found.group(0))
|
||||
except:
|
||||
pass
|
||||
|
||||
return node, chain, addr_fmt
|
||||
|
||||
def chunk_writer(fd, body):
|
||||
from glob import dis
|
||||
dis.fullscreen("Saving...")
|
||||
body_len = len(body)
|
||||
chunk = body_len // 10
|
||||
for idx, i in enumerate(range(0, body_len, chunk)):
|
||||
fd.write(body[i:i + chunk])
|
||||
dis.progress_bar_show(idx / 10)
|
||||
dis.progress_bar_show(1)
|
||||
|
||||
|
||||
def addr_fmt_label(addr_fmt):
|
||||
return {
|
||||
AF_CLASSIC: "Classic P2PKH",
|
||||
AF_P2WPKH_P2SH: "P2SH-Segwit",
|
||||
AF_P2WPKH: "Segwit P2WPKH"
|
||||
}[addr_fmt]
|
||||
|
||||
|
||||
def pad_raw_secret(raw_sec_str):
|
||||
def deserialize_secret(text_sec_str):
|
||||
# Chip can hold 72-bytes as a secret
|
||||
# every secret has 0th byte as marker
|
||||
# then secret and padded to zero to AE_SECRET_LEN
|
||||
# - has 0th byte as marker, secret and zero padding to AE_SECRET_LEN
|
||||
# - also does hex to binary conversion
|
||||
# - converse of: SecretStash.storage_serialize()
|
||||
from pincodes import AE_SECRET_LEN
|
||||
|
||||
raw = bytearray(AE_SECRET_LEN)
|
||||
if len(raw_sec_str) % 2:
|
||||
raw_sec_str += '0'
|
||||
x = a2b_hex(raw_sec_str)
|
||||
if len(text_sec_str) % 2:
|
||||
text_sec_str += '0'
|
||||
x = a2b_hex(text_sec_str)
|
||||
raw[0:len(x)] = x
|
||||
return raw
|
||||
|
||||
@ -615,11 +582,6 @@ def datetime_to_str(dt, fmt="%d-%02d-%02d %02d:%02d:%02d"):
|
||||
dts = fmt % (y, mo, d, h, mi, s)
|
||||
return dts + " UTC"
|
||||
|
||||
def censor_address(addr):
|
||||
# We don't like to show the user multisig addresses because we cannot be certain
|
||||
# they are valid and could actually be signed. And yet, dont blank too many
|
||||
# spots or else an attacker could grind out a suitable replacement.
|
||||
return addr[0:12] + '___' + addr[12+3:]
|
||||
|
||||
def txid_from_fname(fname):
|
||||
if len(fname) >= 64:
|
||||
@ -630,7 +592,7 @@ def txid_from_fname(fname):
|
||||
except: pass
|
||||
return None
|
||||
|
||||
def url_decode(u):
|
||||
def url_unquote(u):
|
||||
# expand control chars from %XX and '+'
|
||||
# - equiv to urllib.parse.unquote_plus
|
||||
# - ure.sub is missing, so not being clever here.
|
||||
@ -650,29 +612,38 @@ def url_decode(u):
|
||||
|
||||
return u
|
||||
|
||||
def url_quote(u):
|
||||
# convert non-text chars into %hex for URL usage
|
||||
# - urllib.parse.quote() but w/o as much thought
|
||||
return ''.join( (ch if 33 <= ord(ch) <= 127 else '%%%02x' % ord(ch)) \
|
||||
for ch in u)
|
||||
|
||||
def decode_bip21_text(got):
|
||||
# Assume text is a BIP-21 payment address (url), with amount, description
|
||||
# and url protocol prefix ... all optional except the address.
|
||||
# - also will detect correctly encoded & checksummed xpubs
|
||||
# - always verifies checksum of data it finds
|
||||
|
||||
proto, args, addr = None, None, None
|
||||
|
||||
# remove URL protocol: if present
|
||||
if ':' in got:
|
||||
proto, got = got.split(':', 1)
|
||||
|
||||
# remove query params first - if any
|
||||
# looks like BIP-21 payment URL
|
||||
if '?' in got:
|
||||
addr, args = got.split('?', 1)
|
||||
got, args = got.split('?', 1)
|
||||
|
||||
# full URL decode here, but assuming no repeated keys
|
||||
parts = args.split('&')
|
||||
args = dict()
|
||||
for p in parts:
|
||||
k, v = p.split('=', 1)
|
||||
args[k] = url_decode(v)
|
||||
args[k] = url_unquote(v)
|
||||
|
||||
# assume it's an bare address for now
|
||||
# remove URL protocol: if present
|
||||
if ':' in got:
|
||||
proto, got = got.split(':', 1)
|
||||
assert proto.lower() == "bitcoin"
|
||||
|
||||
# assume it's a bare address for now
|
||||
if not addr:
|
||||
addr = got
|
||||
|
||||
@ -680,10 +651,12 @@ def decode_bip21_text(got):
|
||||
try:
|
||||
raw = ngu.codecs.b58_decode(addr)
|
||||
|
||||
# it's valid base58
|
||||
# an address, P2PKH or xpub (xprv checked above)
|
||||
# It's valid base58: could be
|
||||
# an address, P2PKH or xpub/xprv
|
||||
if addr[1:4] == 'pub':
|
||||
return 'xpub', (addr,)
|
||||
if addr[1:4] == 'prv':
|
||||
return 'xprv', (addr,)
|
||||
|
||||
return 'addr', (proto, addr, args)
|
||||
except:
|
||||
@ -701,4 +674,107 @@ def decode_bip21_text(got):
|
||||
def encode_seed_qr(words):
|
||||
return ''.join('%04d' % bip39.get_word_index(w) for w in words)
|
||||
|
||||
def show_single_address(addr):
|
||||
# insert some metadata so display layer can do special rendering
|
||||
# of addresses (based on hardware capabilities)
|
||||
return OUT_CTRL_ADDRESS + addr
|
||||
|
||||
def chunk_address(addr):
|
||||
# useful to show payment addresses specially
|
||||
return [addr[i:i+4] for i in range(0, len(addr), 4)]
|
||||
|
||||
def cleanup_payment_address(s):
|
||||
# Cleanup a payment address, or raise if bad checksum
|
||||
# - later matching is string-based, so just doing basic syntax check here
|
||||
# - must be checksumed-base58 or bech32
|
||||
try:
|
||||
ngu.codecs.b58_decode(s)
|
||||
assert len(s) < 40 # or else it's an xpub/xprv
|
||||
return s
|
||||
except: pass
|
||||
|
||||
try:
|
||||
ngu.codecs.segwit_decode(s)
|
||||
return s.lower()
|
||||
except: pass
|
||||
|
||||
raise ValueError('bad address value: ' + s)
|
||||
|
||||
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 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 pa.is_deltamode():
|
||||
import callgate
|
||||
callgate.fast_wipe()
|
||||
|
||||
def chunk_checksum(fd, chunk=1024):
|
||||
# reads from open file descriptor
|
||||
md = sha256()
|
||||
while True:
|
||||
data = fd.read(chunk)
|
||||
if not data:
|
||||
break
|
||||
md.update(data)
|
||||
|
||||
return md.digest()
|
||||
|
||||
def xor(*args):
|
||||
# bit-wise xor between all args
|
||||
vlen = len(args[0])
|
||||
# all have to be same length
|
||||
assert all(len(e) == vlen for e in args)
|
||||
rv = bytearray(vlen)
|
||||
|
||||
for i in range(vlen):
|
||||
for a in args:
|
||||
rv[i] ^= a[i]
|
||||
|
||||
return rv
|
||||
|
||||
def extract_cosigner(data, af_str):
|
||||
# decodes any text, looking for key expression [xfp/p/a/t/h]xpub123
|
||||
# BIP-380 https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#key-expressions
|
||||
# only first key expression will be parsed from the data
|
||||
# key origin info is required
|
||||
# failure to find "proper" key expression results in None being returned
|
||||
pub = "%spub" % chains.current_chain().slip132[AF_CLASSIC].hint
|
||||
if pub not in data:
|
||||
return
|
||||
|
||||
o_start = data.find("[")
|
||||
o_end = data.find("]")
|
||||
if 0 <= o_start < o_end:
|
||||
key_orig_info = data[o_start+1:o_end]
|
||||
ss = key_orig_info.split("/")
|
||||
xfp = ss[0]
|
||||
if (len(xfp) == 8) and (data[o_end+1:o_end+1+len(pub)] == pub):
|
||||
deriv = "m"
|
||||
der_nums = "/".join(ss[1:])
|
||||
if der_nums:
|
||||
deriv += ("/" + der_nums)
|
||||
ek = data[o_end+1:o_end+1+112]
|
||||
key_deriv = "%s_deriv" % af_str
|
||||
# emulate coldcard export xpubs
|
||||
return {"xfp": xfp, af_str: ek, key_deriv: deriv}
|
||||
|
||||
|
||||
def node_from_privkey(privkey, chain_code=bytes(32)):
|
||||
return ngu.hdnode.HDNode().from_chaincode_privkey(chain_code, privkey)
|
||||
|
||||
def node_from_pubkey(pubkey, chain_code=bytes(32)):
|
||||
return ngu.hdnode.HDNode().from_chaincode_pubkey(chain_code, pubkey)
|
||||
|
||||
# EOF
|
||||
|
||||
98
shared/ux.py
98
shared/ux.py
@ -6,8 +6,10 @@ from uasyncio import sleep_ms
|
||||
from queues import QueueEmpty
|
||||
import utime, gc, version
|
||||
from utils import word_wrap
|
||||
from charcodes import (KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_NFC, KEY_QR,
|
||||
KEY_END, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_ENTER, KEY_CANCEL)
|
||||
from version import has_qwerty, num_sd_slots, has_qr
|
||||
from charcodes import (KEY_UP, KEY_DOWN, KEY_HOME, KEY_NFC, KEY_QR, KEY_END, KEY_PAGE_UP,
|
||||
KEY_PAGE_DOWN, KEY_ENTER, KEY_CANCEL, OUT_CTRL_TITLE)
|
||||
|
||||
from exceptions import AbortInteraction
|
||||
|
||||
DEFAULT_IDLE_TIMEOUT = const(4*3600) # (seconds) 4 hours
|
||||
@ -15,21 +17,24 @@ DEFAULT_IDLE_TIMEOUT = const(4*3600) # (seconds) 4 hours
|
||||
# See ux_mk or ux_q1 for some display functions now
|
||||
if version.has_qwerty:
|
||||
from lcd_display import CHARS_W, CHARS_H
|
||||
CH_PER_W = CHARS_W
|
||||
# stories look nicer if we do not use the whole width
|
||||
CH_PER_W = (CHARS_W - 1)
|
||||
STORY_H = CHARS_H
|
||||
from ux_q1 import PressRelease, ux_enter_number, ux_input_numbers, ux_input_text, ux_show_pin
|
||||
from ux_q1 import ux_login_countdown, ux_confirm, ux_dice_rolling, ux_render_words
|
||||
from ux_q1 import PressRelease, ux_enter_number, ux_input_text, ux_show_pin
|
||||
from ux_q1 import ux_login_countdown, ux_dice_rolling, ux_render_words
|
||||
from ux_q1 import ux_show_phish_words
|
||||
OK = "ENTER"
|
||||
X = "CANCEL"
|
||||
|
||||
else:
|
||||
# How many characters can we fit on each line? How many lines?
|
||||
# (using FontSmall)
|
||||
# (using FontSmall) .. except it's an approximation since variable-width font.
|
||||
# - 18 can work but rightmost spot is half-width. We allow . and , in that spot.
|
||||
# - really should look at rendered-width of text
|
||||
CH_PER_W = 17
|
||||
STORY_H = 5
|
||||
from ux_mk4 import PressRelease, ux_enter_number, ux_input_numbers, ux_input_text, ux_show_pin
|
||||
from ux_mk4 import ux_login_countdown, ux_confirm, ux_dice_rolling, ux_render_words
|
||||
from ux_mk4 import PressRelease, ux_enter_number, ux_input_text, ux_show_pin
|
||||
from ux_mk4 import ux_login_countdown, ux_dice_rolling, ux_render_words
|
||||
from ux_mk4 import ux_show_phish_words
|
||||
OK = "OK"
|
||||
X = "X"
|
||||
@ -169,7 +174,6 @@ def ux_poll_key():
|
||||
|
||||
return ch
|
||||
|
||||
|
||||
async def ux_show_story(msg, title=None, escape=None, sensitive=False,
|
||||
strict_escape=False, hint_icons=None):
|
||||
# show a big long string, and wait for XY to continue
|
||||
@ -181,8 +185,8 @@ async def ux_show_story(msg, title=None, escape=None, sensitive=False,
|
||||
|
||||
lines = []
|
||||
if title:
|
||||
# kinda weak rendering but it works.
|
||||
lines.append('\x01' + title)
|
||||
# render the title line specially, see display/lcd_display.py
|
||||
lines.append(OUT_CTRL_TITLE + title)
|
||||
|
||||
if version.has_qwerty:
|
||||
# big screen always needs blank after title
|
||||
@ -245,7 +249,21 @@ async def ux_show_story(msg, title=None, escape=None, sensitive=False,
|
||||
if ch in { KEY_NFC, KEY_QR }:
|
||||
return ch
|
||||
|
||||
|
||||
async def ux_confirm(msg, title="Are you SURE ?!?", confirm_key=None):
|
||||
# confirmation screen, with stock title and Y=of course.
|
||||
if not version.has_qwerty and len(title) > 12:
|
||||
msg = title + "\n\n" + msg
|
||||
title = None
|
||||
|
||||
suffix = ""
|
||||
if confirm_key:
|
||||
suffix = ("\n\nPress (%s) to prove you read to the end of this message"
|
||||
" and accept all consequences.") % confirm_key
|
||||
|
||||
msg += suffix
|
||||
r = await ux_show_story(msg, title=title, escape=confirm_key)
|
||||
|
||||
return r == (confirm_key or 'y')
|
||||
|
||||
async def idle_logout():
|
||||
import glob
|
||||
@ -281,7 +299,7 @@ async def ux_dramatic_pause(msg, seconds):
|
||||
# show a full-screen msg, with a dramatic pause + progress bar
|
||||
n = seconds * 8
|
||||
dis.fullscreen(msg)
|
||||
for i in range(n):
|
||||
for i in range(1, n+1):
|
||||
dis.progress_bar_show(i/n)
|
||||
await sleep_ms(125)
|
||||
|
||||
@ -321,14 +339,14 @@ def abort_and_push(m):
|
||||
the_ux.push(m)
|
||||
numpad.abort_ux()
|
||||
|
||||
async def show_qr_codes(addrs, is_alnum, start_n):
|
||||
async def show_qr_codes(addrs, is_alnum, start_n, **kw):
|
||||
from qrs import QRDisplaySingle
|
||||
o = QRDisplaySingle(addrs, is_alnum, start_n, sidebar=None)
|
||||
o = QRDisplaySingle(addrs, is_alnum, start_n, **kw)
|
||||
await o.interact_bare()
|
||||
|
||||
async def show_qr_code(data, is_alnum=False, msg=None):
|
||||
async def show_qr_code(data, is_alnum=False, msg=None, **kw):
|
||||
from qrs import QRDisplaySingle
|
||||
o = QRDisplaySingle([data], is_alnum, msg=msg)
|
||||
o = QRDisplaySingle([data], is_alnum, msg=msg, **kw)
|
||||
await o.interact_bare()
|
||||
|
||||
async def ux_enter_bip32_index(prompt, can_cancel=False, unlimited=False):
|
||||
@ -339,13 +357,12 @@ async def ux_enter_bip32_index(prompt, can_cancel=False, unlimited=False):
|
||||
|
||||
return await ux_enter_number(prompt=prompt, max_value=max_value, can_cancel=can_cancel)
|
||||
|
||||
def _import_prompt_builder(title, no_qr, no_nfc, slot_b_only=False):
|
||||
from version import has_qwerty, num_sd_slots, has_qr
|
||||
def _import_prompt_builder(title, no_qr, no_nfc, slot_b_only=False, key0=None, key6=None):
|
||||
from glob import NFC, VD
|
||||
|
||||
prompt, escape = None, KEY_CANCEL+"x"
|
||||
|
||||
if (NFC or VD) or num_sd_slots>1:
|
||||
if (NFC or VD) or (num_sd_slots > 1) or key0 or key6:
|
||||
if slot_b_only and (num_sd_slots>1):
|
||||
prompt = "Press (B) to import %s from lower slot SD Card" % title
|
||||
escape += "b"
|
||||
@ -371,21 +388,28 @@ def _import_prompt_builder(title, no_qr, no_nfc, slot_b_only=False):
|
||||
prompt += ", " + KEY_QR + " to scan QR code"
|
||||
escape += KEY_QR
|
||||
|
||||
if key6:
|
||||
prompt += ', (6) ' + key6
|
||||
escape += '6'
|
||||
|
||||
if key0:
|
||||
prompt += ', (0) ' + key0
|
||||
escape += '0'
|
||||
|
||||
prompt += "."
|
||||
|
||||
return prompt, escape
|
||||
|
||||
|
||||
def export_prompt_builder(what_it_is, no_qr=False, no_nfc=False, key0=None,
|
||||
force_prompt=False):
|
||||
def export_prompt_builder(what_it_is, no_qr=False, no_nfc=False, key0=None, offer_kt=False,
|
||||
force_prompt=False, key6=None):
|
||||
# Build the prompt for export
|
||||
# - key0 can be for special stuff
|
||||
from version import has_qwerty, num_sd_slots, has_qr
|
||||
from glob import NFC, VD
|
||||
|
||||
prompt, escape = None, KEY_CANCEL+"x"
|
||||
|
||||
if (NFC or VD) or (num_sd_slots>1) or key0 or force_prompt:
|
||||
if (NFC or VD) or (num_sd_slots>1) or key0 or force_prompt or offer_kt or key6 or (not no_qr):
|
||||
# no need to spam with another prompt, only option is SD card
|
||||
|
||||
prompt = "Press (1) to save %s to SD Card" % what_it_is
|
||||
@ -415,10 +439,18 @@ def export_prompt_builder(what_it_is, no_qr=False, no_nfc=False, key0=None,
|
||||
prompt += ", (4) to show QR code"
|
||||
escape += '4'
|
||||
|
||||
if offer_kt:
|
||||
prompt += ", (T) to " + offer_kt
|
||||
escape += 't'
|
||||
|
||||
if key0:
|
||||
prompt += ', (0) ' + key0
|
||||
escape += '0'
|
||||
|
||||
if key6:
|
||||
prompt += ", (6) " + key6
|
||||
escape += "6"
|
||||
|
||||
prompt += "."
|
||||
|
||||
return prompt, escape
|
||||
@ -456,17 +488,23 @@ def import_export_prompt_decode(ch):
|
||||
|
||||
async def import_export_prompt(what_it_is, is_import=False, no_qr=False,
|
||||
no_nfc=False, title=None, intro='', footnotes='',
|
||||
slot_b_only=False):
|
||||
offer_kt=False, slot_b_only=False, force_prompt=False,
|
||||
key0=None, key6=None):
|
||||
|
||||
# Show story allowing user to select source for importing/exporting
|
||||
# - return either str(mode) OR dict(file_args)
|
||||
# - KEY_NFC or KEY_QR for those sources
|
||||
# - KEY_CANCEL for abort by user
|
||||
# - dict() => do file system thing, using file_args to control vdisk vs. SD vs slot_b
|
||||
# - 't' => key teleport, but only offered with offer_kt is set (contetxt, and Q only)
|
||||
from glob import NFC
|
||||
|
||||
if is_import:
|
||||
prompt, escape = _import_prompt_builder(what_it_is, no_qr, no_nfc, slot_b_only)
|
||||
prompt, escape = _import_prompt_builder(what_it_is, no_qr, no_nfc, slot_b_only,
|
||||
key0=key0, key6=key6)
|
||||
else:
|
||||
prompt, escape = export_prompt_builder(what_it_is, no_qr, no_nfc)
|
||||
prompt, escape = export_prompt_builder(what_it_is, no_qr, no_nfc, key6=key6, key0=key0,
|
||||
force_prompt=force_prompt, offer_kt=offer_kt)
|
||||
|
||||
# TODO: detect if we're only asking A or B, when just one card is inserted
|
||||
# - assume that's what they want to do
|
||||
@ -476,8 +514,10 @@ async def import_export_prompt(what_it_is, is_import=False, no_qr=False,
|
||||
# they don't have NFC nor VD enabled, and no second slots... so will be file.
|
||||
return dict(force_vdisk=False, slot_b=None)
|
||||
else:
|
||||
ch = await ux_show_story(intro+prompt+footnotes, escape=escape, title=title,
|
||||
strict_escape=True)
|
||||
hints = ("" if no_qr else KEY_QR) + (KEY_NFC if not no_nfc and NFC else "")
|
||||
msg_lst = [i for i in (intro, prompt, footnotes) if i]
|
||||
ch = await ux_show_story("\n\n".join(msg_lst), escape=escape, title=title,
|
||||
strict_escape=True, hint_icons=hints)
|
||||
|
||||
return import_export_prompt_decode(ch)
|
||||
|
||||
|
||||
@ -58,17 +58,9 @@ class PressRelease:
|
||||
else:
|
||||
self.last_key = ch
|
||||
return ch
|
||||
|
||||
|
||||
async def ux_confirm(msg):
|
||||
# confirmation screen, with stock title and Y=of course.
|
||||
from ux import ux_show_story
|
||||
|
||||
resp = await ux_show_story("Are you SURE ?!?\n\n" + msg)
|
||||
|
||||
return resp == 'y'
|
||||
|
||||
async def ux_enter_number(prompt, max_value, can_cancel=False):
|
||||
async def ux_enter_number(prompt, max_value, can_cancel=False, value=''):
|
||||
# return the decimal number which the user has entered
|
||||
# - default/blank value assumed to be zero
|
||||
# - clamps large values to the max
|
||||
@ -80,7 +72,7 @@ async def ux_enter_number(prompt, max_value, can_cancel=False):
|
||||
press = PressRelease('1234567890y')
|
||||
|
||||
y = 26
|
||||
value = ''
|
||||
value = str(value)
|
||||
max_w = int(log(max_value, 10) + 1)
|
||||
|
||||
dis.clear()
|
||||
@ -122,8 +114,8 @@ async def ux_enter_number(prompt, max_value, can_cancel=False):
|
||||
# cleanup leading zeros and such
|
||||
value = str(min(int(value), max_value))
|
||||
|
||||
async def ux_input_numbers(val, validate_func):
|
||||
# collect a series of digits
|
||||
async def ux_input_digits(val, prompt=None, maxlen=32):
|
||||
# collect a series of digits.
|
||||
from glob import dis
|
||||
from display import FontTiny
|
||||
|
||||
@ -137,6 +129,11 @@ async def ux_input_numbers(val, validate_func):
|
||||
|
||||
dis.clear()
|
||||
dis.text(None, -1, footer, FontTiny)
|
||||
|
||||
if prompt:
|
||||
dis.text(0, 0, prompt)
|
||||
y += 8
|
||||
|
||||
dis.save()
|
||||
|
||||
while 1:
|
||||
@ -161,7 +158,6 @@ async def ux_input_numbers(val, validate_func):
|
||||
ch = await press.wait()
|
||||
if ch == 'y':
|
||||
val += here
|
||||
validate_func()
|
||||
return val
|
||||
elif ch == 'x':
|
||||
if here:
|
||||
@ -170,7 +166,7 @@ async def ux_input_numbers(val, validate_func):
|
||||
# quit if they press X on empty screen
|
||||
return
|
||||
else:
|
||||
if len(here) < 32:
|
||||
if len(here) < maxlen:
|
||||
here += ch
|
||||
|
||||
async def ux_input_text(pw, confirm_exit=True, hex_only=False, max_len=100, min_len=0, **_kws):
|
||||
@ -287,7 +283,7 @@ async def ux_input_text(pw, confirm_exit=True, hex_only=False, max_len=100, min_
|
||||
ch = await press.wait()
|
||||
if ch == 'y':
|
||||
if len(pw) < min_len:
|
||||
ch = await ux_show_story('Need %d characters at least. Press OK '
|
||||
ch = await ux_show_story('Need %d character(s) at least. Press OK '
|
||||
'to continue X to exit.' % min_len, escape="xy",
|
||||
strict_escape=True)
|
||||
if ch == "x": return
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user