Compare commits
608 Commits
2025-11-25
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
542dcd32c7 | ||
|
|
0ef6413cd8 | ||
|
|
97d86c9571 | ||
|
|
edae8c1ee6 | ||
|
|
553405776f | ||
|
|
ad2088d231 | ||
|
|
67a5c6c270 | ||
|
|
eb112eb3a1 | ||
|
|
0d04e5e1f8 | ||
|
|
59eb529a20 | ||
|
|
d5aba396a6 | ||
|
|
6fd256dbdc | ||
|
|
6716fcbacb | ||
|
|
1dddd88525 | ||
|
|
d656f371c7 | ||
|
|
7e92e5162a | ||
|
|
74d34cfcf7 | ||
|
|
64658621bb | ||
|
|
755353f029 | ||
|
|
2981d15933 | ||
|
|
9ff3f5c447 | ||
|
|
f5a1ef32c9 | ||
|
|
841e44335e | ||
|
|
38616234e7 | ||
|
|
8e3bbfdf84 | ||
|
|
f9b65ce968 | ||
|
|
8d71040acf | ||
|
|
5feae87e03 | ||
|
|
0949c0ac86 | ||
|
|
c36eac23d2 | ||
|
|
a24a894cfd | ||
|
|
ca06dfd250 | ||
|
|
3a1ef6fe50 | ||
|
|
883be60fc5 | ||
|
|
393ebf5b43 | ||
|
|
2b5178bd63 | ||
|
|
44e7be3681 | ||
|
|
300323f18d | ||
|
|
c998432fc4 | ||
|
|
9c6cfcbbd7 | ||
|
|
be614dab92 | ||
|
|
02bd428786 | ||
|
|
6869ba87b0 | ||
|
|
d0f834570b | ||
|
|
00afe533ca | ||
|
|
621523c1a8 | ||
|
|
fdf630ab31 | ||
|
|
d2ad7a5923 | ||
|
|
29ef16be63 | ||
|
|
3344975607 | ||
|
|
15191eaaa5 | ||
|
|
717a3f591a | ||
|
|
74790ab80d | ||
|
|
50b1704c60 | ||
|
|
1e9338c550 | ||
|
|
17fc097cbd | ||
|
|
6f6563c2cb | ||
|
|
4904d38bb6 | ||
|
|
1ea83d9b70 | ||
|
|
2410897c86 | ||
|
|
6541812e20 | ||
|
|
2604f4d092 | ||
|
|
c6da2612ef | ||
|
|
538b1a6df8 | ||
|
|
2edf3c72e4 | ||
|
|
5e3f7a9321 | ||
|
|
00beed7b94 | ||
|
|
12e0af3f5c | ||
|
|
01629e1396 | ||
|
|
d893575ecf | ||
|
|
0cc6818728 | ||
|
|
b7bc614323 | ||
|
|
02b5a75675 | ||
|
|
e35f60ed5e | ||
|
|
0b425b8609 | ||
|
|
43ef951d83 | ||
|
|
38553d1ac5 | ||
|
|
9b131b2eff | ||
|
|
c19be4f41e | ||
|
|
e92a8ccde1 | ||
|
|
7d937aca84 | ||
|
|
382eef61d2 | ||
|
|
cfc46b565e | ||
|
|
45542d1d4f | ||
|
|
470fe2843c | ||
|
|
6c247d4852 | ||
|
|
6f366d1603 | ||
|
|
d53c7b2e1b | ||
|
|
598ccda8c0 | ||
|
|
1bbaeef439 | ||
|
|
eb50a0e198 | ||
|
|
08d0a70de2 | ||
|
|
a3d5d6152e | ||
|
|
9f832476eb | ||
|
|
78195d8fb9 | ||
|
|
aecc870c6b | ||
|
|
0c43c802e1 | ||
|
|
4ce43c74c6 | ||
|
|
1176c83e34 | ||
|
|
ef2f35b1fb | ||
|
|
5d7d5d881d | ||
|
|
5047512bae | ||
|
|
9862f53bec | ||
|
|
6a34760943 | ||
|
|
d0576a205f | ||
|
|
dfd52e5c03 | ||
|
|
6fd47d4c16 | ||
|
|
06879c59e0 | ||
|
|
21392bd3df | ||
|
|
e4d2326959 | ||
|
|
72e336628a | ||
|
|
7ca3baae43 | ||
|
|
89d6e226d7 | ||
|
|
f966d47012 | ||
|
|
dbf9482fea | ||
|
|
ecd796b3a5 | ||
|
|
b1fe2cca26 | ||
|
|
5d9ab62595 | ||
|
|
7dbbf29b8d | ||
|
|
722c45df0a | ||
|
|
88e4a5e8ab | ||
|
|
910c096145 | ||
|
|
a3d5485fd2 | ||
|
|
366670b4d5 | ||
|
|
0fffb07e9e | ||
|
|
4e91d2ca3f | ||
|
|
eb467988bb | ||
|
|
9fbf208c3f | ||
|
|
da1d6e28bd | ||
|
|
c1e17d3a26 | ||
|
|
a2f0eb323a | ||
|
|
53c6d33c2f | ||
|
|
a6a66bc367 | ||
|
|
386cbcbb1d | ||
|
|
d5713851ea | ||
|
|
91f96ff870 | ||
|
|
c25af2bfb1 | ||
|
|
b3cd82ed61 | ||
|
|
02ee6a58e2 | ||
|
|
192f2d2dda | ||
|
|
f235a83cf3 | ||
|
|
d34f59697c | ||
|
|
f966d3b079 | ||
|
|
6b7294cea0 | ||
|
|
efa8e1d56a | ||
|
|
946b4e9ae4 | ||
|
|
0d1104dfe0 | ||
|
|
1e371d9297 | ||
|
|
f88b8da729 | ||
|
|
52a090e31b | ||
|
|
cbd1b841b9 | ||
|
|
82fabe75d4 | ||
|
|
bc8b55a059 | ||
|
|
c54b3801ce | ||
|
|
076bb34285 | ||
|
|
48709f3329 | ||
|
|
ef0ba6a556 | ||
|
|
a08550cfd8 | ||
|
|
3e818cbbf6 | ||
|
|
fe0041f99c | ||
|
|
15e571b0d9 | ||
|
|
36521dfef9 | ||
|
|
25249eb68c | ||
|
|
be1328c720 | ||
|
|
8d6ce99cd3 | ||
|
|
fcd848d821 | ||
|
|
203394a709 | ||
|
|
284616d597 | ||
|
|
55c9ee4626 | ||
|
|
47430cb211 | ||
|
|
dd1bea1949 | ||
|
|
2aea18ba53 | ||
|
|
3c4922e3ca | ||
|
|
c6ec10206c | ||
|
|
88a9f4719b | ||
|
|
6a3eec50f1 | ||
|
|
722facf0d9 | ||
|
|
0a9d99429b | ||
|
|
d8c13ddc73 | ||
|
|
d73c1bd8d3 | ||
|
|
5764073837 | ||
|
|
259be18962 | ||
|
|
248e0b568d | ||
|
|
ca00ee0798 | ||
|
|
f3aaf3d5cb | ||
|
|
06fa4f338c | ||
|
|
2d81b7251d | ||
|
|
91f10c2f35 | ||
|
|
52e7c75539 | ||
|
|
a1e7e4c8de | ||
|
|
fce9503d0e | ||
|
|
5fc25566ee | ||
|
|
b88590f8e8 | ||
|
|
765cc2a5a4 | ||
|
|
1a591a70eb | ||
|
|
26680a00f0 | ||
|
|
429b8e645e | ||
|
|
74fd862c9c | ||
|
|
0625aa462c | ||
|
|
faa5ebf11e | ||
|
|
d0c5998e55 | ||
|
|
50d20713a0 | ||
|
|
2445b4d435 | ||
|
|
a8366f55e0 | ||
|
|
41b8167837 | ||
|
|
3461ce336d | ||
|
|
4457576ad4 | ||
|
|
038199a3e2 | ||
|
|
1929067aa1 | ||
|
|
2b115059e8 | ||
|
|
88110bc5c5 | ||
|
|
0a2c0cba12 | ||
|
|
1e9e3ffb9d | ||
|
|
863e0d85ad | ||
|
|
2109ab4ab1 | ||
|
|
e5dce7105b | ||
|
|
0237fd29ba | ||
|
|
3efeb4f1ef | ||
|
|
8b3603b15f | ||
|
|
e8ba25fd04 | ||
|
|
9e762d29a5 | ||
|
|
de043f2250 | ||
|
|
20ce5f2bae | ||
|
|
3353f3d4a4 | ||
|
|
4d2349fef4 | ||
|
|
54dcf2dce8 | ||
|
|
3291faa31e | ||
|
|
bbac20b453 | ||
|
|
98420f8ac3 | ||
|
|
5b26b306b5 | ||
|
|
a3cac15a53 | ||
|
|
c7a19ee50f | ||
|
|
e42c0631d0 | ||
|
|
609af3a257 | ||
|
|
7daa67cc63 | ||
|
|
3f24307dcd | ||
|
|
372954e43a | ||
|
|
391aea0462 | ||
|
|
ee464f4a40 | ||
|
|
54d58d4b43 | ||
|
|
4bd8d12d9d | ||
|
|
8ceb6a4602 | ||
|
|
76cc136a9e | ||
|
|
8d84979ddf | ||
|
|
bb391515a0 | ||
|
|
16c3caee28 | ||
|
|
ab1b656277 | ||
|
|
1a1daf32e3 | ||
|
|
6b1c38fe2f | ||
|
|
43544f4f96 | ||
|
|
d15de0321d | ||
|
|
0420d4b6eb | ||
|
|
00b2f67d55 | ||
|
|
6ab63b9dcf | ||
|
|
d053398a9a | ||
|
|
d9a601e87c | ||
|
|
6abb24443e | ||
|
|
9d54e261ec | ||
|
|
335df666ab | ||
|
|
7c5503d81a | ||
|
|
a4c7f95dc1 | ||
|
|
dbb2a21798 | ||
|
|
f12457cbb5 | ||
|
|
28ba1adce3 | ||
|
|
a62d5fb31e | ||
|
|
b8435fdd79 | ||
|
|
d3caf63265 | ||
|
|
c3a454abd6 | ||
|
|
73bb6b850d | ||
|
|
bc49347a69 | ||
|
|
9a4d3986b7 | ||
|
|
f89061ffbe | ||
|
|
3eb99272b3 | ||
|
|
56e5b98438 | ||
|
|
123caec8d1 | ||
|
|
637624dea9 | ||
|
|
9d5b86e39b | ||
|
|
7bd952973e | ||
|
|
aa4524339b | ||
|
|
5dd034c051 | ||
|
|
f90973af9b | ||
|
|
d71f24959c | ||
|
|
00b05e20b8 | ||
|
|
11da344abf | ||
|
|
89dfe1f6d4 | ||
|
|
81f2830425 | ||
|
|
efb445fbe3 | ||
|
|
0a899727b5 | ||
|
|
6d17350293 | ||
|
|
86d5eb890b | ||
|
|
b3b384b7d6 | ||
|
|
6dbedfaeb5 | ||
|
|
e29e0b65e3 | ||
|
|
2b750c993b | ||
|
|
17a715bfc5 | ||
|
|
15678037d5 | ||
|
|
213fdd3c57 | ||
|
|
406ab5adaa | ||
|
|
416f2efffd | ||
|
|
8aba8fe655 | ||
|
|
68d5a8fc35 | ||
|
|
170b701e94 | ||
|
|
4a0cd36c9c | ||
|
|
6a5311e8f5 | ||
|
|
20a8d14ede | ||
|
|
bab58af710 | ||
|
|
d4c4cc1b69 | ||
|
|
75a9f9f3eb | ||
|
|
e856343c66 | ||
|
|
51bbee9eb1 | ||
|
|
f40d16b76b | ||
|
|
227196da42 | ||
|
|
ea9d183a48 | ||
|
|
32cfd53569 | ||
|
|
49087f6f41 | ||
|
|
6c0ee684dc | ||
|
|
bee2f95e0a | ||
|
|
594e64affc | ||
|
|
6f3cbf20c8 | ||
|
|
7ca83c5929 | ||
|
|
3bf9c13bbb | ||
|
|
063ee4cf4d | ||
|
|
d7b9ed4813 | ||
|
|
10b82e080f | ||
|
|
a1b319347a | ||
|
|
709207f28e | ||
|
|
c680b0461f | ||
|
|
a4fa421a17 | ||
|
|
2f35c0d496 | ||
|
|
315e650344 | ||
|
|
d9be8cb2f1 | ||
|
|
176a7f80cb | ||
|
|
2441e6044e | ||
|
|
f076695120 | ||
|
|
6a3c8ae676 | ||
|
|
28926acd06 | ||
|
|
adcf2c8e22 | ||
|
|
1ae8c51a3c | ||
|
|
36d4df8a49 | ||
|
|
1aed412080 | ||
|
|
bc0f3e2d12 | ||
|
|
cae59ecddd | ||
|
|
9b95d29152 | ||
|
|
d376c4efbf | ||
|
|
e021fc7317 | ||
|
|
ef72dc00ae | ||
|
|
e2fc69661a | ||
|
|
b65f2948d4 | ||
|
|
fbbf0a3413 | ||
|
|
c4b7260686 | ||
|
|
6a63c7bde9 | ||
|
|
efd5a4ff45 | ||
|
|
b866e0912d | ||
|
|
9ba8aeaaad | ||
|
|
3239dc6cd5 | ||
|
|
8a18e413e9 | ||
|
|
78fcfa56a5 | ||
|
|
634bb69873 | ||
|
|
00f8d7a5ca | ||
|
|
0a200d1f1c | ||
|
|
30bc6a1f57 | ||
|
|
f23d7f09bf | ||
|
|
5c5f8902a1 | ||
|
|
6bba62224c | ||
|
|
8837cdcdda | ||
|
|
c4b56d95d0 | ||
|
|
2156844d18 | ||
|
|
b47412bbc1 | ||
|
|
1ca170946f | ||
|
|
0dedaf353d | ||
|
|
6779345665 | ||
|
|
698f84ff97 | ||
|
|
6a5f4843aa | ||
|
|
e851e4382a | ||
|
|
fc21241a49 | ||
|
|
5de2ba364d | ||
|
|
cc62502ab1 | ||
|
|
0815fbcc81 | ||
|
|
a676ef8eb7 | ||
|
|
78e560376c | ||
|
|
7e65cc3e47 | ||
|
|
a954821826 | ||
|
|
828b09dba2 | ||
|
|
2b88a14124 | ||
|
|
8acb15e4ce | ||
|
|
97d8398e8a | ||
|
|
5f87d21811 | ||
|
|
5c8a73ddb0 | ||
|
|
979a4e65e9 | ||
|
|
6e5c68abe5 | ||
|
|
d3634b3448 | ||
|
|
67fa34666d | ||
|
|
39adb2ac41 | ||
|
|
6fd2ef619e | ||
|
|
4f25f6dbf5 | ||
|
|
b4eeeda53a | ||
|
|
f8bc38b558 | ||
|
|
414793053e | ||
|
|
b885976601 | ||
|
|
ec64a9aa38 | ||
|
|
0aa0fc4500 | ||
|
|
3ebde0ea34 | ||
|
|
b6098a94e5 | ||
|
|
e726637319 | ||
|
|
446bea9926 | ||
|
|
9a28d36097 | ||
|
|
a4d7f884c0 | ||
|
|
ce1ce080ab | ||
|
|
e06449f59f | ||
|
|
ffda830f66 | ||
|
|
d23187f187 | ||
|
|
15766b418d | ||
|
|
7450940730 | ||
|
|
d1fd24c9ef | ||
|
|
19ce22e607 | ||
|
|
4476089d0f | ||
|
|
341265c486 | ||
|
|
f3a2f59549 | ||
|
|
4a245ce553 | ||
|
|
de443d5b89 | ||
|
|
ff756f086e | ||
|
|
cd1728b81d | ||
|
|
633120b760 | ||
|
|
3fd8fb9dc1 | ||
|
|
7a27adfcfd | ||
|
|
33faa04652 | ||
|
|
56f0d56a08 | ||
|
|
481d64d2dd | ||
|
|
e3ae6bcbdf | ||
|
|
e150150d11 | ||
|
|
f117423210 | ||
|
|
962bb4b0f2 | ||
|
|
ae9806f702 | ||
|
|
66e4cf130f | ||
|
|
72fef6d5b8 | ||
|
|
bb3073af76 | ||
|
|
b09bb521a8 | ||
|
|
22ff7b1fe9 | ||
|
|
7eb5a7ea03 | ||
|
|
6b9e2ef9b9 | ||
|
|
7c9436d237 | ||
|
|
8bf4731cf5 | ||
|
|
e14fb64904 | ||
|
|
12f62f95bc | ||
|
|
58eec8be8b | ||
|
|
b5a6bf5d18 | ||
|
|
70acd6a602 | ||
|
|
1fe0c58b76 | ||
|
|
18ea47f334 | ||
|
|
96b8d48136 | ||
|
|
4a463da9db | ||
|
|
239d1ebd00 | ||
|
|
77956158ae | ||
|
|
3d4336bab7 | ||
|
|
b2f6e290d1 | ||
|
|
f920020ba0 | ||
|
|
4a234e8452 | ||
|
|
d980c42dad | ||
|
|
37c7119ae4 | ||
|
|
ab55c00065 | ||
|
|
6f93847f8a | ||
|
|
2eb615b358 | ||
|
|
c899f6e7ce | ||
|
|
d0f7a451ef | ||
|
|
14be94e049 | ||
|
|
5fb1839620 | ||
|
|
a2eed31416 | ||
|
|
9203c4f2aa | ||
|
|
3d08b749b3 | ||
|
|
58b5d1071f | ||
|
|
f795f9027f | ||
|
|
2cf006beaa | ||
|
|
0c28987190 | ||
|
|
012433aba4 | ||
|
|
57cdd69c81 | ||
|
|
9e082570aa | ||
|
|
117daaf17f | ||
|
|
359d05dc7b | ||
|
|
77059ffcf1 | ||
|
|
17306e2a38 | ||
|
|
1f1045b401 | ||
|
|
29b860e84e | ||
|
|
0849e538b5 | ||
|
|
44dae36141 | ||
|
|
8a8860d9a0 | ||
|
|
0a185669d0 | ||
|
|
6445fad042 | ||
|
|
0ccd701421 | ||
|
|
7ff8342e01 | ||
|
|
0009028817 | ||
|
|
7e9ce496d8 | ||
|
|
869e317db8 | ||
|
|
6df24f646d | ||
|
|
9d863edcdb | ||
|
|
4c308205a6 | ||
|
|
2641650090 | ||
|
|
84a476d586 | ||
|
|
8b97535a40 | ||
|
|
119fc35c43 | ||
|
|
2feade4f74 | ||
|
|
9761c65614 | ||
|
|
00d8c841f7 | ||
|
|
531ae613c7 | ||
|
|
ac782fdd59 | ||
|
|
d35ccc1ade | ||
|
|
2c52cca4bd | ||
|
|
07e66811f3 | ||
|
|
077d502139 | ||
|
|
4030f6f59f | ||
|
|
15990fa0f6 | ||
|
|
ef01a57626 | ||
|
|
d21515e2cf | ||
|
|
fc5e99c226 | ||
|
|
bc59709966 | ||
|
|
d983549fae | ||
|
|
b6fc24e705 | ||
|
|
fcae022ba8 | ||
|
|
65d202160e | ||
|
|
4fb37d7850 | ||
|
|
5b69ec07fc | ||
|
|
60d254314b | ||
|
|
0f3fe830b9 | ||
|
|
4087f8f25b | ||
|
|
6b770024ce | ||
|
|
bbd8f9b282 | ||
|
|
d81d6ea80c | ||
|
|
8edef87ae5 | ||
|
|
9cc6ce368d | ||
|
|
e037037924 | ||
|
|
5b38dd87a6 | ||
|
|
b55db05aab | ||
|
|
6d4b3d6990 | ||
|
|
c7d216fff3 | ||
|
|
a3f815a74d | ||
|
|
e61493668b | ||
|
|
eb325e5fec | ||
|
|
242641d396 | ||
|
|
a507677dc1 | ||
|
|
262c4ea717 | ||
|
|
a16516b8e1 | ||
|
|
695c3b4b29 | ||
|
|
08bb9783d2 | ||
|
|
4515e688ed | ||
|
|
5694ada611 | ||
|
|
d2636c7621 | ||
|
|
67febe2758 | ||
|
|
7e6be45e2d | ||
|
|
660b4617bd | ||
|
|
afe85a3772 | ||
|
|
6c91bd7328 | ||
|
|
9c7f4c5451 | ||
|
|
4ea455aa20 | ||
|
|
9e38974f7a | ||
|
|
75751b8d2b | ||
|
|
dd66cd8811 | ||
|
|
a640dd5d78 | ||
|
|
14e85304df | ||
|
|
1eec58ece7 | ||
|
|
41cde6be6c | ||
|
|
15536e4c9e | ||
|
|
2feb991d96 | ||
|
|
a2bdfc9a58 | ||
|
|
d3c50521e8 | ||
|
|
2558ed7ac0 | ||
|
|
4f8f1fe593 | ||
|
|
3878897369 | ||
|
|
1a18258b5a | ||
|
|
38c92ef0c1 | ||
|
|
cc7097b4f7 | ||
|
|
9b597592bc | ||
|
|
70d303af78 | ||
|
|
cd6d74d9f3 | ||
|
|
a71f350c78 | ||
|
|
ce1026cb4b | ||
|
|
e039fb8603 | ||
|
|
a0949ecb87 | ||
|
|
a8202972b3 | ||
|
|
85ff2dcf45 | ||
|
|
262df4a257 | ||
|
|
e9d17e5efb | ||
|
|
513a3b8258 | ||
|
|
ac761c23d5 | ||
|
|
de0a679eef | ||
|
|
92a776cfc3 | ||
|
|
f2a3667593 | ||
|
|
6b73eb2fa6 | ||
|
|
66b01c1fd5 | ||
|
|
14ce2ca6e0 | ||
|
|
86fe33137f | ||
|
|
c65280cd42 | ||
|
|
2fb66da58d | ||
|
|
87b5142145 | ||
|
|
6aedb0a73a | ||
|
|
072eb24ed9 | ||
|
|
b443f38d60 | ||
|
|
8957ad3c10 | ||
|
|
d270cf66c6 | ||
|
|
c425fc6bcc | ||
|
|
c9882d7a8a | ||
|
|
85b478346b | ||
|
|
95b13083dc | ||
|
|
5568082f35 | ||
|
|
8f86ed1c0e | ||
|
|
1b54536eff | ||
|
|
d1d104cb7e | ||
|
|
9e1ce7a956 | ||
|
|
f30686f252 | ||
|
|
3dbc9caa73 |
25
README.md
25
README.md
@ -8,7 +8,7 @@ with the latest updates and security alerts.
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
## Quick Links
|
||||
|
||||
@ -28,9 +28,11 @@ has been automated using Docker. Steps are as follows:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/Coldcard/firmware.git
|
||||
git checkout 2023-12-21T1526-v5.2.2
|
||||
# get a copy of that binary into ./releases/2023-12-21T1526-v5.2.2-mk4-coldcard.dfu
|
||||
cd firmware/stm32
|
||||
cd firmware
|
||||
# DOWNLOAD https://coldcard.com/downloads
|
||||
# get a copy of binary into ./releases/2026-03-05T2052-v5.5.0-mk-coldcard.dfu
|
||||
git checkout 2026-03-05T2052-v5.5.0
|
||||
cd stm32
|
||||
make -f MK4-Makefile repro
|
||||
```
|
||||
|
||||
@ -49,14 +51,15 @@ such as Taproot or Miniscript. Our standards for releasing new Edge
|
||||
versions are lower, so we can iterate faster and get these advancements
|
||||
out to other developers.
|
||||
|
||||
Q and Mk4 share the same code base. Individual files that are added,
|
||||
Q and Mk series share the same code base. Individual files that are added,
|
||||
or removed, can be see in differences between `shared/manifest_mk4.py`
|
||||
and `shared/manifest_q1.py`. Common files are in `shared/manifest.py`.
|
||||
Firmware built for Mk5, supports the Mk4 without any functional differences.
|
||||
|
||||
|
||||
## Check-out and Setup
|
||||
|
||||
**NOTE** This is the `master` branch and covers the latest hardware (Mk4 and Q).
|
||||
**NOTE** This is the `master` branch and covers the latest hardware (Mk and Q).
|
||||
See branch `v4-legacy` for firmware which supports only Mk3/Mk2 and earlier.
|
||||
|
||||
Do a checkout, recursively, to get all the submodules:
|
||||
@ -183,7 +186,9 @@ git clone --recursive https://github.com/Coldcard/firmware.git
|
||||
cd firmware
|
||||
|
||||
# Apply address patch
|
||||
git apply unix/linux_addr.patch
|
||||
# if unix/linux_addr.patch exists use below command
|
||||
# not needed in current revision
|
||||
# git apply unix/linux_addr.patch
|
||||
|
||||
# * below is needed for ubuntu 24.04
|
||||
pushd external/micropython
|
||||
@ -230,8 +235,8 @@ Top-level dirs:
|
||||
|
||||
- shared code between desktop test version and real-deal
|
||||
- expected to be largely in python, and higher-level
|
||||
- new code found only on the Mk4 will be listed in `manifest_mk4.py` code exclusive
|
||||
to earlier hardware is in `manifest_mk3.py`
|
||||
- code exclusive to the Mk4 or Mk5 will be listed in `manifest_mk4.py`, and
|
||||
to the Q will be listed in `manifest_q1.py`
|
||||
|
||||
`unix`
|
||||
|
||||
@ -263,7 +268,7 @@ Top-level dirs:
|
||||
`stm32/mk4-bootloader`
|
||||
`stm32/q1-bootloader`
|
||||
|
||||
- 128k of factory-set code that you cannot change for Mk4 or Q
|
||||
- 128k of factory-set code that you cannot change
|
||||
- however, you can inspect what code is on your coldcard and compare to this.
|
||||
|
||||
`hardware`
|
||||
|
||||
@ -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, \
|
||||
|
||||
@ -3,14 +3,32 @@
|
||||
These docs are meant for you hackers out there... but also for anyone who
|
||||
wants to understand why it's safe to put your moneys into Coldcard.
|
||||
|
||||
- [`security-model.md`](security-model.md) The COLDCARD Mk4/Mk5/Q security model.
|
||||
- [`pin-entry.md`](pin-entry.md) Huge and detailed discussion of PIN codes and the security element that holds the secrets.
|
||||
- [`secure-elements.md`](secure-elements.md) How the dual secure elements work together.
|
||||
- [`dev-access.md`](dev-access.md) How developers can modify Coldcard to extend it.
|
||||
- [`memory-map.md`](memory-map.md) Memory map highlights
|
||||
- [`notes-on-repro.md`](notes-on-repro.md) Detailed breakdown of the reproducible build process.
|
||||
- [`upgrade-recovery.md`](upgrade-recovery.md) Firmware upgrade and recovery process.
|
||||
- [`backup-files.md`](backup-files.md) Some details of our encrypted backup files.
|
||||
- [`temporary-seeds.md`](temporary-seeds.md) Temporary (ephemeral) seeds and the Seed Vault.
|
||||
- [`seed-xor.md`](seed-xor.md) More about _Seed XOR_ feature, including fully worked Seed XOR example, and useful XOR lookup chart.
|
||||
- [`key-teleport.md`](key-teleport.md) Key Teleport: encrypted transfer of seeds and secrets between Q devices.
|
||||
- [`spending-policy.md`](spending-policy.md) Spending policy: autonomous signing with configurable limits.
|
||||
- [`microsd-2fa.md`](microsd-2fa.md) Using a MicroSD card as a second factor for login.
|
||||
- [`web2fa.md`](web2fa.md) Web 2FA authentication.
|
||||
- [`bip85-passwords.md`](bip85-passwords.md) Deriving deterministic passwords via BIP-85.
|
||||
- [`msg-signing.md`](msg-signing.md) COLDCARD message signing.
|
||||
- [`proof-of-reserves-bip-322.md`](proof-of-reserves-bip-322.md) BIP-322 generic signed message format and proof of reserves.
|
||||
- [`generic-wallet-export.md`](generic-wallet-export.md) Generic JSON wallet export file format.
|
||||
- [`bip-21-extensions.md`](bip-21-extensions.md) Coldcard's BIP-21 URI extensions, including multisig ownership address check.
|
||||
- [`nfc-coldcard.md`](nfc-coldcard.md) NFC support on Coldcard Mk4 and Q.
|
||||
- [`nfc-pushtx.md`](nfc-pushtx.md) NFC Push Transaction: broadcast a signed transaction via your phone.
|
||||
- [`usb-batteries.md`](usb-batteries.md) Using USB battery packs with Coldcard.
|
||||
- [`electrum-usage.md`](electrum-usage.md) Importing seed words into Electrum for funds usage (and other tips).
|
||||
- [`bitcoin-core-usage.md`](bitcoin-core-usage.md) How to use with Bitcoin Core.
|
||||
- [`bitcoin-core2of2desc.md`](bitcoin-core2of2desc.md) Airgapped 2-of-2 multisig with Bitcoin Core using descriptors.
|
||||
- [`limitations.md`](limitations.md) Documented limitations, policy choices, and TODO items.
|
||||
- [`paperwallet.pdf`](paperwallet.pdf) Example paper wallet template file.
|
||||
- [`seed-xor.md`](seed-xor.md) More about _Seed XOR_ feature, including fully worked Seed XOR example, and useful XOR lookup chart.
|
||||
- [`menu-tree.txt`](menu-tree.txt) Dump of the menu system. Incomplete, may be out of date.
|
||||
|
||||
|
||||
15
docs/bip-21-extensions.md
Normal file
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
|
||||
```
|
||||
@ -5,13 +5,13 @@ according to [BIP-85 PWD BASE64](https://github.com/bitcoin/bips/blob/master/bip
|
||||
Generated passwords can be sent as keystrokes via USB to the host computer,
|
||||
effectively using Coldcard as specialized password manager.
|
||||
|
||||
In addition to deriving up to 10,000 distinct secure passwords, the Coldcard Mk4
|
||||
In addition to deriving up to 10,000 distinct secure passwords, the Coldcard
|
||||
can also type them into a computer by emulating a USB keyboard, and simulating the
|
||||
keystrokes needed to type the password.
|
||||
|
||||
#### Requirements
|
||||
|
||||
* Coldcard Mk4 with version 5.0.5 or newer
|
||||
* Coldcard Mk4 or Mk5 (firmware 5.0.5 or newer), or any Q
|
||||
* USB-C with data link (won't work with power only cable from Coinkite)
|
||||
|
||||
## Type Passwords over USB
|
||||
@ -32,11 +32,13 @@ to exit. Exiting from "Type Passwords" will cause Coldcard to turn off keyboard
|
||||
1. Go to Advanced/Tools -> Derive Seed B85 -> Passwords
|
||||
2. Choose "Password/Index number" (BIP-85 index) and press OK to generate password.
|
||||
3. Screen shows generated password, path, and entropy from which password was derived
|
||||
4. A few different options are available at this point:
|
||||
1. press 1 to save password backup file on MicroSD card (cleartext!)
|
||||
2. press 2 to send keystrokes (this will first of all enable keyboard emulation, then send keystrokes + enter, and finally disables keyboard emulation)
|
||||
3. press 3 to view password as QR code
|
||||
4. press 4 to send over NFC (only appears when NFC is enabled)
|
||||
4. A few different options are available at this point (on Mk; on Q the NFC and
|
||||
QR buttons are used instead of (3)/(4)):
|
||||
1. press (1) to save password backup file on MicroSD card (cleartext!)
|
||||
2. press (2) to save to Virtual Disk (only when available)
|
||||
3. press (3) to send over NFC (only appears when NFC is enabled)
|
||||
4. press (4) to view password as QR code
|
||||
5. press (6) to send keystrokes over USB (this enables keyboard emulation, sends keystrokes + enter, then disables keyboard emulation)
|
||||
|
||||
## Keyboard language settings
|
||||
|
||||
|
||||
@ -5,9 +5,12 @@ wallet systems, but we also have a file format for general purpose
|
||||
exports, which we hope future wallet makers will leverage.
|
||||
|
||||
It contains master XPUB, XFP for that, and derived values for the top hardened
|
||||
position of BIP44, BIP84 and BIP49.
|
||||
position of the single-signature schemes BIP44, BIP49 and BIP84, plus the
|
||||
multisig schemes BIP48 (`bip48_1` = `.../1h` P2SH-P2WSH and `bip48_2` = `.../2h` P2WSH).
|
||||
When the account number is zero, a BIP45 (`m/45h`) multisig section is also included
|
||||
(it is omitted for non-zero accounts, as in the example below).
|
||||
|
||||
The feature can be found here: _Advanced > MicroSD > Export Wallet > Generic JSON_
|
||||
The feature can be found here: _Advanced/Tools > Export Wallet > Generic JSON_
|
||||
|
||||
Please contact us (or better yet, make a pull request), if you need something
|
||||
more in this file.
|
||||
@ -18,32 +21,51 @@ Here is an example, produced by the Simulator for account number 123.
|
||||
|
||||
```javascript
|
||||
{
|
||||
"chain": "XTN",
|
||||
"chain": "BTC",
|
||||
"xfp": "0F056943",
|
||||
"xpub": "tpubD6NzVbkrYhZ4XzL5Dhayo67Gorv1YMS7j8pRUvVMd5odC2LBPLAygka9p7748JtSq82FNGPppFEz5xxZUdasBRCqJqXvUHq6xpnsMcYJzeh",
|
||||
"account": 123,
|
||||
"xpub": "xpub661MyMwAqRbcGC9DmWbtbAmuUjpMYxw4BWE88NSDHB3jSjfUK7KtYJuKa52GbowD3DVLkgsxH9QwPnTx5mjdHykYFEncnmAsNsCTbWzBhA7",
|
||||
"bip44": {
|
||||
"deriv": "m/44'/1'/123'",
|
||||
"first": "n44vs1Rv7T8SANrg2PFGQhzVkhr5Q6jMMD",
|
||||
"name": "p2pkh",
|
||||
"xfp": "B7908B26",
|
||||
"xpub": "tpubDCiHGUNYdRRGoSH22j8YnruUKgguCK1CC2NFQUf9PApeZh8ewAJJWGMUrhggDNK73iCTanWXv1RN5FYemUH8UrVUBjqDb8WF2VoKmDh9UTo"
|
||||
"xfp": "5F898064",
|
||||
"deriv": "m/44h/0h/123h",
|
||||
"xpub": "xpub6DStQXfAgHuLbMpCf86ruVkF4yT9pSLyWsFiqQTWY9osuinq8Dyee4W5jCjMfyku5LNkRB9oFinrY5ufn9XXEn8Vvzc2jnifKMaQCNV7RBZ",
|
||||
"desc": "pkh([0f056943/44h/0h/123h]xpub6DStQXfAgHuLbMpCf86ruVkF4yT9pSLyWsFiqQTWY9osuinq8Dyee4W5jCjMfyku5LNkRB9oFinrY5ufn9XXEn8Vvzc2jnifKMaQCNV7RBZ/<0;1>/*)#4tl8jryn",
|
||||
"first": "1GTNtzG5xX2UhdD5e3Nu7i1WPxFdjxQMJt"
|
||||
},
|
||||
"bip49": {
|
||||
"_pub": "upub5DMRSsh6mNak9KbcVjJ7xAgHJvbE3Nx22CBTier5C35kv8j7g2q58ywxskBe6JCcAE2VH86CE2aL4MifJyKbRw8Gj9ay7SWvUBkp2DJ7y52",
|
||||
"deriv": "m/49'/1'/123'",
|
||||
"first": "2N87V39riUUCd4vmXfDjMWAu9gUCiBji5jB",
|
||||
"name": "p2wpkh-p2sh",
|
||||
"xfp": "CEE1D809",
|
||||
"xpub": "tpubDCDqt7XXvhAdy1MpSze5nMJA9x8DrdRaKALRRPasfxyHpiqWWEAr9cbDBQ9BcX7cB3up98Pk97U2QQ3xrvQsi5dNPmRYYhdcsKY9wwEY87T"
|
||||
"name": "p2sh-p2wpkh",
|
||||
"xfp": "A748B1FC",
|
||||
"deriv": "m/49h/0h/123h",
|
||||
"xpub": "xpub6DDm8WzH5a9qjKkttzqSB3uGofNohU9D3n3UG8WMxkUZzJEMPTYiQRf1dvTFCQR82MjGW4LUMVuTtnW4hF17RpzCqVwhf6Z2fnJPWtjG164",
|
||||
"desc": "sh(wpkh([0f056943/49h/0h/123h]xpub6DDm8WzH5a9qjKkttzqSB3uGofNohU9D3n3UG8WMxkUZzJEMPTYiQRf1dvTFCQR82MjGW4LUMVuTtnW4hF17RpzCqVwhf6Z2fnJPWtjG164/<0;1>/*))#5j7t2n2u",
|
||||
"_pub": "ypub6Y42SBfCEFhKacx1jMd4P8zmydXFe68hxtZh3XQFLkrT3Q3ae7iH2VK9f8QqCK53Rzr5FXw2pAG1n57dQwR8E4fohqe8F1NWwWN2uVRfBry",
|
||||
"first": "3CeBRbJKCpg7BpJME2vM8ZxhCjBnhG4toy"
|
||||
},
|
||||
"bip84": {
|
||||
"_pub": "vpub5Y5a91QvDT45EnXQaKeuvJupVvX8f9BiywDcadSTtaeJ1VgJPPXMitnYsqd9k7GnEqh44FKJ5McJfu6KrihFXhAmvSWgm7BAVVK8Gupu4fL",
|
||||
"deriv": "m/84'/1'/123'",
|
||||
"first": "tb1qc58ys2dphtphg6yuugdf3d0kufmk0tye044g3l",
|
||||
"name": "p2wpkh",
|
||||
"xfp": "78CF94E5",
|
||||
"xpub": "tpubDC7jGaaSE66VDB6VhEDFYQSCAyugXmfnMnrMVyHNzW9wryyTxvha7TmfAHd7GRXrr2TaAn2HXn9T8ep4gyNX1bzGiieqcTUNcu2poyntrET"
|
||||
"xfp": "2C5207AA",
|
||||
"deriv": "m/84h/0h/123h",
|
||||
"xpub": "xpub6CaWStGvcXqSW9BzU2vpCoP7aWjz9VfR5DS2nuYWVvKV2nug2dESg3HdFsaWHeoZaxuAhNcPB3TH2gq8MugS3JX1yGuhB4QbC2BneaYqB16",
|
||||
"desc": "wpkh([0f056943/84h/0h/123h]xpub6CaWStGvcXqSW9BzU2vpCoP7aWjz9VfR5DS2nuYWVvKV2nug2dESg3HdFsaWHeoZaxuAhNcPB3TH2gq8MugS3JX1yGuhB4QbC2BneaYqB16/<0;1>/*)#yk84tprf",
|
||||
"_pub": "zpub6rF34DckutvQCjaE8kW4cya7vT2t2jeQuSUUMhLHFw5F8zY8XwZZvAbuJHVgHU7QQF8nCKoW6NANoG4FoJWTdmtDhxJYLt3ZjUK5RqUSMdF",
|
||||
"first": "bc1qhj6avwmp5lhpgqwm6dgxrf3v5lf67rjm99a8an"
|
||||
},
|
||||
"bip48_1": {
|
||||
"name": "p2sh-p2wsh",
|
||||
"xfp": "845A3542",
|
||||
"deriv": "m/48h/0h/123h/1h",
|
||||
"xpub": "xpub6EkcQSTygvxVnBP2X2fM6HY5D7wv46tWbBc54ADaypuCr47vQh1GPdPAZFdx81ou5Rp4vBnzeJT5MDWDZstzijxkHfrofXRycpt1ASfg1La",
|
||||
"desc": "sh(wsh(sortedmulti(M,[0f056943/48h/0h/123h/1h]xpub6EkcQSTygvxVnBP2X2fM6HY5D7wv46tWbBc54ADaypuCr47vQh1GPdPAZFdx81ou5Rp4vBnzeJT5MDWDZstzijxkHfrofXRycpt1ASfg1La/0/*,...)))",
|
||||
"_pub": "Ypub6kUxqLsLQa4M43jXJ3ux8SyP6t8dD5ZbpZmxkpP1jc7VXLW4RkZ76ouEPAZ1gMgiiXzrYFPfzBC8MfjYaoTxfTm1zUfdeqiTnHDX8raCfeg"
|
||||
},
|
||||
"bip48_2": {
|
||||
"name": "p2wsh",
|
||||
"xfp": "2A01C6B0",
|
||||
"deriv": "m/48h/0h/123h/2h",
|
||||
"xpub": "xpub6EkcQSTygvxVneXmk3ywiS2PFhBdiPxeMxYf6RFxHCHH36NxdcN7DjUpudCppAAxs58CG6DQLjtqZNmyC3MpgVob6wpdeATjpZZ1woX92EF",
|
||||
"desc": "wsh(sortedmulti(M,[0f056943/48h/0h/123h/2h]xpub6EkcQSTygvxVneXmk3ywiS2PFhBdiPxeMxYf6RFxHCHH36NxdcN7DjUpudCppAAxs58CG6DQLjtqZNmyC3MpgVob6wpdeATjpZZ1woX92EF/0/*,...))",
|
||||
"_pub": "Zpub75KE91YFZFbpup5PMS2AxgZCKRWnozdEWTEmaUKGQysSmUaKuL5WYyf2kk5UNQhhupRnddQe9GzST7crvfLoRTHTg6KtDPZiFjxBJobzcUz"
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -51,16 +73,23 @@ Here is an example, produced by the Simulator for account number 123.
|
||||
## Notes
|
||||
|
||||
1. The `first` address is formed by added `/0/0` onto the given derivation, and is assumed
|
||||
to be the first (non-change) receive address for the wallet.
|
||||
to be the first (non-change) receive address for the wallet. It is only present on the
|
||||
single-signature sections (`bip44`, `bip49`, `bip84`); multisig sections omit it.
|
||||
|
||||
1a. Each section includes a `desc` field: a ready-to-import Bitcoin output descriptor
|
||||
(with `#checksum`). Single-sig descriptors use the `<0;1>/*` multipath form. Multisig
|
||||
sections (`bip48_1`, `bip48_2`, and `bip45` when present) emit a `sortedmulti(...)`
|
||||
template with `M` and a trailing `...` as placeholders, to be completed with your
|
||||
threshold and the other co-signers' keys.
|
||||
|
||||
2. The user may specify any value (up to 9999) for the account number, and it's meant to
|
||||
segregate funds into sub-wallets. Don't assume it's zero.
|
||||
|
||||
3. When making your PSBT files to spend these amounts, remember that the XFP of the master
|
||||
(`0F056943` in this example) is is the root of the subkey paths found in the file, and
|
||||
(`0F056943` in this example) is the root of the subkey paths found in the file, and
|
||||
you must include the full derivation path from master. So based on this example,
|
||||
to spend a UTXO on `tb1qc58ys2dphtphg6yuugdf3d0kufmk0tye044g3l`, the input section
|
||||
of your PSBT would need to specify `(m=0F056943)/84'/1'/123'/0/0`.
|
||||
to spend a UTXO on `bc1qhj6avwmp5lhpgqwm6dgxrf3v5lf67rjm99a8an`, the input section
|
||||
of your PSBT would need to specify `(m=0F056943)/84'/0'/123'/0/0`.
|
||||
|
||||
4. The `_pub` value is the [SLIP-132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) style "ypub/zpub/etc" which some systems might want. It implies
|
||||
a specific address format.
|
||||
|
||||
224
docs/key-teleport.md
Normal file
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.
|
||||
@ -74,6 +79,7 @@
|
||||
- multisig wallet `name` can only contain printable ASCII characters `range(32, 127)`
|
||||
|
||||
### BIP-67
|
||||
|
||||
- importing multisig from PSBT can ONLY create `sortedmulti(...)` multisig according to BIP-67, DO NOT use with `multi(...)`
|
||||
- creating airgapped multisig using COLDCARD as coordinator always produces `sortedmulti(...)` multisig according to BIP-67
|
||||
- COLDCARD import/export [format](https://coldcard.com/docs/multisig/#configuration-text-file-for-multisig) only supports `sortedmulti(...)` multisig according to BIP-67. To import multisig wallet with `multi(...)` use descriptor import [format](https://github.com/bitcoin/bips/blob/master/bip-0383.mediawiki)
|
||||
@ -134,6 +140,10 @@ We will summarize transaction outputs as "change" back into same wallet, however
|
||||
|
||||
- key derivatation paths must be 12 or less in depth (`MAX_PATH_DEPTH`)
|
||||
|
||||
# Pay-to-Pubkey
|
||||
|
||||
- although we have some code for "pay to pubkey" (P2PK not P2PKH), it is untested
|
||||
and unused since this style of payment address is obsolete and largely unused today
|
||||
|
||||
# NFC Feature
|
||||
|
||||
@ -198,3 +208,16 @@ We will summarize transaction outputs as "change" back into same wallet, however
|
||||
- if you have an XFP collision between multiple wallets in SeedVault (ie. two wallets
|
||||
with same descriptors, but different seeds) you will get false negatives
|
||||
|
||||
# Spending Policy
|
||||
|
||||
- (Cosign mode) only 12 or 24 word seeds (not XPRV) are accepted for "key C"
|
||||
- velocity limit:
|
||||
- based on a max magnitude per txn, and a required minimum block height
|
||||
gap, based on previous `nLockTime` value in last-signed PSBT.
|
||||
- if you sign a transaction, but never broadcast it, you will still have to wait out
|
||||
the velocity policy.
|
||||
- PSBT creator must put in `nLockTime` block heights (most already do to avoid fee sniping)
|
||||
- maximum of 25 whitelisted addresses can be stored
|
||||
- Web2FA: any number of mobile devices can be enrolled, but all will have the same shared secret
|
||||
- any warning from the PSBT, such as huge fees, will cause the transaction to be rejected
|
||||
|
||||
|
||||
@ -38,7 +38,7 @@ directly from python programs.
|
||||
|
||||
| Start | Size | Notes
|
||||
|---------------|-----------|--------------------------
|
||||
| 0x0800 0000 | 128k | Bootloader code, including reset vector. See `stm32/mk4-bootloader`
|
||||
| 0x0800 0000 | 112k | Bootloader code, including reset vector. See `stm32/mk4-bootloader`
|
||||
| 0x0801 c000 | 8k | Sensitive "pairing secrets" for SE1 and SE2
|
||||
| 0x0801 e000 | 8k | MCU keys, consumable; 256 32-bit write-once slots.
|
||||
| 0x0802 0000 | 16k | Interrupt handlers, file header (Micropython and Coldcard code)
|
||||
|
||||
@ -16,7 +16,6 @@
|
||||
Advanced
|
||||
12 Word Dice Roll
|
||||
24 Word Dice Roll
|
||||
Migrate COLDCARD
|
||||
Import Existing
|
||||
12 Words
|
||||
[SEED WORD ENTRY]
|
||||
@ -30,6 +29,8 @@
|
||||
Import XPRV
|
||||
Tapsigner Backup
|
||||
Seed XOR
|
||||
Migrate Coldcard
|
||||
Key Teleport (start)
|
||||
Help
|
||||
Advanced/Tools
|
||||
View Identity
|
||||
@ -48,6 +49,7 @@
|
||||
Import XPRV
|
||||
Tapsigner Backup
|
||||
Coldcard Backup
|
||||
Restore Seed XOR
|
||||
Upgrade Firmware [IF NOT TMP SEED]
|
||||
Show Version
|
||||
From MicroSD
|
||||
@ -57,14 +59,18 @@
|
||||
List Files
|
||||
Verify Sig File
|
||||
NFC File Share [IF NFC ENABLED]
|
||||
BBQr File Share [IF QR SCANNER]
|
||||
QR File Share [IF QR SCANNER]
|
||||
Format SD Card
|
||||
Format RAM Disk [IF VIRTDISK ENABLED]
|
||||
Key Teleport (start)
|
||||
Paper Wallets
|
||||
Perform Selftest
|
||||
I Am Developer.
|
||||
Serial REPL
|
||||
Warm Reset
|
||||
Restore Txt Bkup
|
||||
Restore Bkup
|
||||
Reflash GPU [IF QWERTY KEYBOARD]
|
||||
Secure Logout
|
||||
Settings
|
||||
Login Settings
|
||||
@ -106,6 +112,11 @@
|
||||
NFC Sharing
|
||||
Default Off
|
||||
Enable NFC
|
||||
NFC Push Tx
|
||||
coldcard.com
|
||||
mempool.space
|
||||
Custom URL...
|
||||
Disable
|
||||
Display Units
|
||||
BTC
|
||||
mBTC
|
||||
@ -140,32 +151,32 @@
|
||||
50%
|
||||
60%
|
||||
70%
|
||||
80% (default)
|
||||
80%
|
||||
90%
|
||||
95% (default)
|
||||
100%
|
||||
Delete PSBTs
|
||||
Default Keep
|
||||
Delete PSBTs
|
||||
Menu Wrapping
|
||||
Default Off
|
||||
Enable
|
||||
Buried Settings
|
||||
Home Menu XFP [IF SECRET AND NOT TMP SEED]
|
||||
Only Tmp
|
||||
Always Show
|
||||
Menu Wrapping
|
||||
Default
|
||||
Always Wrap
|
||||
[QR key shortcut] [IF QR SCANNER]
|
||||
---
|
||||
|
||||
[NORMAL OPERATION]
|
||||
Ready To Sign
|
||||
Passphrase [IF WORD BASED SEED]
|
||||
Restore Saved [MAYBE]
|
||||
A***********
|
||||
[0C52BAD4]
|
||||
Restore Saved
|
||||
c*******
|
||||
[3A14F788]
|
||||
Restore
|
||||
Delete
|
||||
Edit Phrase [MAYBE]
|
||||
Add Word [IF NOT QWERTY]
|
||||
[SEED WORD MENUS]
|
||||
Add Numbers [IF NOT QWERTY]
|
||||
Clear All [IF NOT QWERTY]
|
||||
APPLY [IF NOT QWERTY]
|
||||
CANCEL [IF NOT QWERTY]
|
||||
Edit Phrase
|
||||
Scan Any QR Code [IF QR SCANNER]
|
||||
Start HSM Mode [IF HSM POLICY]
|
||||
Address Explorer
|
||||
@ -183,35 +194,44 @@
|
||||
Account Number
|
||||
Custom Path
|
||||
CC-2-of-4
|
||||
Secure Notes & Passwords [IF ENBALED]
|
||||
1: note1
|
||||
"note1"
|
||||
Secure Notes & Passwords [IF ENBALED] [MAYBE]
|
||||
1: note0
|
||||
"note0"
|
||||
View Note
|
||||
Edit
|
||||
Delete
|
||||
Export
|
||||
SHORTCUT
|
||||
SHORTCUT
|
||||
2: nostr
|
||||
"nostr"
|
||||
↳ scg
|
||||
↳ brb.io
|
||||
Sign Note Text
|
||||
2: secret-PWD
|
||||
"secret-PWD"
|
||||
↳ satoshi
|
||||
↳ abc.org
|
||||
View Password
|
||||
Send Password [MAYBE]
|
||||
Export
|
||||
Edit Metadata
|
||||
Delete
|
||||
Change Password
|
||||
SHORTCUT
|
||||
SHORTCUT
|
||||
Sign Note Text
|
||||
New Note
|
||||
New Password
|
||||
Export All
|
||||
Sort By Title
|
||||
Import
|
||||
Type Passwords [MAYBE]
|
||||
Seed Vault [MAYBE]
|
||||
1: [B14E9AE0]
|
||||
[B14E9AE0]
|
||||
1: [7126EB3C]
|
||||
[7126EB3C]
|
||||
Use This Seed
|
||||
Rename
|
||||
Delete
|
||||
2: [CCEE13B9]
|
||||
[CCEE13B9]
|
||||
Use This Seed
|
||||
Rename
|
||||
Delete
|
||||
3: [03EE9989]
|
||||
[03EE9989]
|
||||
Use This Seed
|
||||
Rename
|
||||
Delete
|
||||
@ -222,12 +242,19 @@
|
||||
Restore Backup
|
||||
Clone Coldcard
|
||||
Export Wallet
|
||||
Sparrow
|
||||
Cove
|
||||
Bitcoin Core
|
||||
Sparrow Wallet
|
||||
Nunchuk
|
||||
Bull Bitcoin
|
||||
Blue Wallet
|
||||
Electrum Wallet
|
||||
Wasabi Wallet
|
||||
Fully Noded
|
||||
Unchained
|
||||
Lily Wallet
|
||||
Theya
|
||||
Bitcoin Safe
|
||||
Zeus
|
||||
Samourai Postmix
|
||||
Samourai Premix
|
||||
Descriptor
|
||||
@ -235,9 +262,10 @@
|
||||
Export XPUB
|
||||
Segwit (BIP-84)
|
||||
Classic (BIP-44)
|
||||
P2WPKH/P2SH (49)
|
||||
P2WPKH/P2SH (BIP-49)
|
||||
Master XPUB
|
||||
Current XFP
|
||||
Key Expression
|
||||
Dump Summary
|
||||
Upgrade Firmware [IF NOT TMP SEED]
|
||||
Show Version
|
||||
@ -247,12 +275,19 @@
|
||||
Verify Backup
|
||||
Backup System
|
||||
Export Wallet
|
||||
Sparrow
|
||||
Cove
|
||||
Bitcoin Core
|
||||
Sparrow Wallet
|
||||
Nunchuk
|
||||
Bull Bitcoin
|
||||
Blue Wallet
|
||||
Electrum Wallet
|
||||
Wasabi Wallet
|
||||
Fully Noded
|
||||
Unchained
|
||||
Lily Wallet
|
||||
Theya
|
||||
Bitcoin Safe
|
||||
Zeus
|
||||
Samourai Postmix
|
||||
Samourai Premix
|
||||
Descriptor
|
||||
@ -260,42 +295,45 @@
|
||||
Export XPUB
|
||||
Segwit (BIP-84)
|
||||
Classic (BIP-44)
|
||||
P2WPKH/P2SH (49)
|
||||
P2WPKH/P2SH (BIP-49)
|
||||
Master XPUB
|
||||
Current XFP
|
||||
Key Expression
|
||||
Dump Summary
|
||||
Sign Text File
|
||||
Batch Sign PSBT
|
||||
Teleport Multisig PSBT
|
||||
List Files
|
||||
Verify Sig File
|
||||
NFC File Share [IF NFC ENABLED]
|
||||
BBQr File Share [IF QR SCANNER]
|
||||
QR File Share [IF QR SCANNER]
|
||||
Clone Coldcard
|
||||
Format SD Card
|
||||
Format RAM Disk [IF VIRTDISK ENABLED]
|
||||
Secure Notes & Passwords [IF QWERTY KEYBOARD]
|
||||
1: note1
|
||||
"note1"
|
||||
1: note0
|
||||
"note0"
|
||||
View Note
|
||||
Edit
|
||||
Delete
|
||||
Export
|
||||
SHORTCUT
|
||||
SHORTCUT
|
||||
2: nostr
|
||||
"nostr"
|
||||
↳ scg
|
||||
↳ brb.io
|
||||
Sign Note Text
|
||||
2: secret-PWD
|
||||
"secret-PWD"
|
||||
↳ satoshi
|
||||
↳ abc.org
|
||||
View Password
|
||||
Send Password [MAYBE]
|
||||
Export
|
||||
Edit Metadata
|
||||
Delete
|
||||
Change Password
|
||||
SHORTCUT
|
||||
SHORTCUT
|
||||
Sign Note Text
|
||||
New Note
|
||||
New Password
|
||||
Export All
|
||||
Sort By Title
|
||||
Import
|
||||
Derive Seeds (BIP-85)
|
||||
View Identity
|
||||
@ -314,18 +352,26 @@
|
||||
Import XPRV
|
||||
Tapsigner Backup
|
||||
Coldcard Backup
|
||||
Restore Seed XOR
|
||||
Key Teleport (start)
|
||||
Spending Policy [IF SECRET AND NOT TMP SEED]
|
||||
Single-Signer [IF SECRET AND NOT TMP SEED]
|
||||
Co-Sign Multisig (CCC) [IF NOT TMP SEED]
|
||||
HSM Mode [IF HSM AND SECRET]
|
||||
Default Off
|
||||
Enable
|
||||
User Management [MAYBE]
|
||||
Paper Wallets
|
||||
Enable HSM [IF HSM AND SECRET]
|
||||
Default Off
|
||||
Enable
|
||||
User Management [IF HSM AND SECRET]
|
||||
WIF Store
|
||||
NFC Tools [IF NFC ENABLED]
|
||||
Sign PSBT
|
||||
Show Address
|
||||
Sign Message
|
||||
Verify Sig File
|
||||
Verify Address
|
||||
File Share
|
||||
Import Multisig
|
||||
Push Transaction [IF PUSHTX ENABLED]
|
||||
Danger Zone
|
||||
Debug Functions
|
||||
Seed Functions
|
||||
@ -334,13 +380,15 @@
|
||||
Split Existing [IF WORD BASED SEED]
|
||||
Restore Seed XOR
|
||||
Destroy Seed [IF SECRET AND NOT TMP SEED]
|
||||
Lock Down Seed
|
||||
Lock Down Seed [MAYBE]
|
||||
Export SeedQR [IF WORD BASED SEED]
|
||||
I Am Developer.
|
||||
Serial REPL
|
||||
Warm Reset
|
||||
Restore Txt Bkup
|
||||
Seed Vault [IF SECRET]
|
||||
Restore Bkup
|
||||
BKPW Override
|
||||
Reflash GPU [IF QWERTY KEYBOARD]
|
||||
Seed Vault [IF SECRET AND NOT TMP SEED]
|
||||
Default Off
|
||||
Enable
|
||||
Perform Selftest
|
||||
@ -353,30 +401,44 @@
|
||||
Warn
|
||||
Testnet Mode
|
||||
Bitcoin
|
||||
Testnet3
|
||||
Testnet4
|
||||
Regtest
|
||||
AE Start IDX
|
||||
AE Start Index
|
||||
Default Off
|
||||
Enable
|
||||
B85 Idx Values
|
||||
Default Off
|
||||
Unlimited
|
||||
Settings Space
|
||||
MCU Key Slots
|
||||
Bless Firmware
|
||||
Reflash GPU [IF QWERTY KEYBOARD]
|
||||
Wipe LFS
|
||||
Nuke Device
|
||||
Settings
|
||||
Login Settings
|
||||
Change Main PIN
|
||||
Trick PINs [IF SECRET AND NOT TMP SEED]
|
||||
Trick PINs:
|
||||
↳123-254
|
||||
PIN 123-254
|
||||
↳11-11
|
||||
PIN 11-11
|
||||
↳Bricks CC
|
||||
Hide Trick
|
||||
Delete Trick
|
||||
Change PIN
|
||||
↳333-3334
|
||||
PIN 333-3334
|
||||
↳Duress Wallet
|
||||
Activate Wallet
|
||||
Hide Trick
|
||||
Delete Trick
|
||||
Change PIN
|
||||
↳WRONG PIN
|
||||
After 3 wrong:
|
||||
↳Wipes seed
|
||||
↳Reboots
|
||||
Hide Trick
|
||||
Delete Trick
|
||||
Add New Trick
|
||||
Add If Wrong
|
||||
Delete All
|
||||
Set Nickname
|
||||
Scramble Keys
|
||||
@ -421,17 +483,25 @@
|
||||
View Details
|
||||
Delete
|
||||
Coldcard Export
|
||||
Electrum Wallet
|
||||
Descriptors
|
||||
View Descriptor
|
||||
Export
|
||||
Bitcoin Core
|
||||
Electrum Wallet
|
||||
Import from File
|
||||
Import via NFC [IF NFC ENABLED]
|
||||
Import
|
||||
Export XPUB
|
||||
Create Airgapped
|
||||
Trust PSBT?
|
||||
Skip Checks?
|
||||
Full Address View?
|
||||
Partly Censor
|
||||
Show Full
|
||||
Unsorted Multisig?
|
||||
NFC Push Tx
|
||||
coldcard.com
|
||||
mempool.space
|
||||
Custom URL...
|
||||
Disable
|
||||
Display Units
|
||||
BTC
|
||||
mBTC
|
||||
@ -466,34 +536,189 @@
|
||||
50%
|
||||
60%
|
||||
70%
|
||||
80% (default)
|
||||
80%
|
||||
90%
|
||||
95% (default)
|
||||
100%
|
||||
Delete PSBTs
|
||||
Default Keep
|
||||
Delete PSBTs
|
||||
Menu Wrapping
|
||||
Default Off
|
||||
Enable
|
||||
Keyboard EMU
|
||||
Default Off
|
||||
Enable
|
||||
Buried Settings
|
||||
Home Menu XFP [IF SECRET AND NOT TMP SEED]
|
||||
Only Tmp
|
||||
Always Show
|
||||
Menu Wrapping
|
||||
Default
|
||||
Always Wrap
|
||||
Secure Logout
|
||||
SHORTCUT [IF NFC ENABLED]
|
||||
[NFC key shortcut] [IF NFC ENABLED]
|
||||
Sign PSBT
|
||||
Show Address
|
||||
Sign Message
|
||||
Verify Sig File
|
||||
Verify Address
|
||||
File Share
|
||||
Import Multisig
|
||||
Push Transaction [IF PUSHTX ENABLED]
|
||||
---
|
||||
|
||||
[FACTORY MODE]
|
||||
Version: 5.x.x
|
||||
Bag Me Now
|
||||
Version: 5.x.x
|
||||
DFU Upgrade
|
||||
Ship W/O Bag
|
||||
Debug Functions
|
||||
Perform Selftest
|
||||
---
|
||||
|
||||
[SSSP]
|
||||
Ready To Sign
|
||||
Passphrase [IF WORD BASED SEED & SSSP RELATED KEYS ENABLED]
|
||||
Restore Saved
|
||||
c*******
|
||||
[3A14F788]
|
||||
Restore
|
||||
Delete
|
||||
Edit Phrase
|
||||
Scan Any QR Code [IF QR SCANNER]
|
||||
Address Explorer
|
||||
Classic P2PKH
|
||||
↳ mtHSVByP9EYZ⋯Vm19gvpecb5R
|
||||
P2SH-Segwit
|
||||
↳ 2NCAJ5wD4Gvm⋯NphNU8UYoEJv
|
||||
Segwit P2WPKH
|
||||
↳ tb1qupyd58nd⋯vu9jtdyws9n9
|
||||
Applications
|
||||
Samourai
|
||||
Post-mix
|
||||
Pre-mix
|
||||
Wasabi
|
||||
Account Number
|
||||
Custom Path
|
||||
CC-2-of-4
|
||||
Secure Notes & Passwords[IF ENABLED & SSSP ALLOW NOTES]
|
||||
1: note0
|
||||
"note0"
|
||||
View Note
|
||||
Sign Note Text
|
||||
2: secret-PWD
|
||||
"secret-PWD"
|
||||
↳ satoshi
|
||||
↳ abc.org
|
||||
View Password
|
||||
Send Password [MAYBE]
|
||||
Sign Note Text
|
||||
Type Passwords [MAYBE]
|
||||
Seed Vault[IF ENABLED & SSSP RELATED KEYS ENABLED]
|
||||
1: [7126EB3C]
|
||||
[7126EB3C]
|
||||
Use This Seed
|
||||
2: [CCEE13B9]
|
||||
[CCEE13B9]
|
||||
Use This Seed
|
||||
3: [03EE9989]
|
||||
[03EE9989]
|
||||
Use This Seed
|
||||
Advanced/Tools
|
||||
File Management
|
||||
Sign Text File
|
||||
Batch Sign PSBT
|
||||
List Files
|
||||
Export Wallet
|
||||
Sparrow
|
||||
Cove
|
||||
Bitcoin Core
|
||||
Nunchuk
|
||||
Bull Bitcoin
|
||||
Blue Wallet
|
||||
Electrum Wallet
|
||||
Wasabi Wallet
|
||||
Fully Noded
|
||||
Unchained
|
||||
Theya
|
||||
Bitcoin Safe
|
||||
Zeus
|
||||
Samourai Postmix
|
||||
Samourai Premix
|
||||
Descriptor
|
||||
Generic JSON
|
||||
Export XPUB
|
||||
Segwit (BIP-84)
|
||||
Classic (BIP-44)
|
||||
P2WPKH/P2SH (BIP-49)
|
||||
Master XPUB
|
||||
Current XFP
|
||||
Key Expression
|
||||
Dump Summary
|
||||
Verify Sig File
|
||||
NFC File Share [IF NFC ENABLED]
|
||||
BBQr File Share [IF QR SCANNER]
|
||||
QR File Share [IF QR SCANNER]
|
||||
Format SD Card
|
||||
Format RAM Disk [IF VIRTDISK ENABLED]
|
||||
Export Wallet
|
||||
Sparrow
|
||||
Cove
|
||||
Bitcoin Core
|
||||
Nunchuk
|
||||
Bull Bitcoin
|
||||
Blue Wallet
|
||||
Electrum Wallet
|
||||
Wasabi Wallet
|
||||
Fully Noded
|
||||
Unchained
|
||||
Theya
|
||||
Bitcoin Safe
|
||||
Zeus
|
||||
Samourai Postmix
|
||||
Samourai Premix
|
||||
Descriptor
|
||||
Generic JSON
|
||||
Export XPUB
|
||||
Segwit (BIP-84)
|
||||
Classic (BIP-44)
|
||||
P2WPKH/P2SH (BIP-49)
|
||||
Master XPUB
|
||||
Current XFP
|
||||
Key Expression
|
||||
Dump Summary
|
||||
Teleport Multisig PSBT [MAYBE]
|
||||
View Identity
|
||||
Temporary Seed [IF SSSP RELATED KEYS ENABLED]
|
||||
Import from QR Scan [IF QR SCANNER]
|
||||
Import Words
|
||||
12 Words
|
||||
18 Words
|
||||
24 Words
|
||||
Import via NFC [IF NFC ENABLED]
|
||||
Import XPRV
|
||||
Tapsigner Backup
|
||||
Coldcard Backup
|
||||
Restore Seed XOR
|
||||
Paper Wallets
|
||||
WIF Store
|
||||
NFC Tools [IF NFC ENABLED]
|
||||
Sign PSBT
|
||||
Show Address
|
||||
Sign Message
|
||||
Verify Sig File
|
||||
Verify Address
|
||||
File Share
|
||||
Push Transaction [IF PUSHTX ENABLED]
|
||||
Show Firmware Version
|
||||
Destroy Seed [IF SECRET AND NOT TMP SEED]
|
||||
Secure Logout
|
||||
EXIT TEST DRIVE [MAYBE]
|
||||
[NFC key shortcut] [IF NFC ENABLED]
|
||||
Sign PSBT
|
||||
Show Address
|
||||
Sign Message
|
||||
Verify Sig File
|
||||
Verify Address
|
||||
File Share
|
||||
Push Transaction [IF PUSHTX ENABLED]
|
||||
---
|
||||
|
||||
|
||||
@ -2,15 +2,16 @@
|
||||
|
||||
COLDCARD can sign messages send to it via USB with the help of `ckcc` utility,
|
||||
sign messages provided via specially crafted file on SD card or Vdisk,
|
||||
and Mk4 can also sign messages sent to COLDCARD via NFC.
|
||||
and NFC-equipped models (Mk4, Mk5, and Q) can also sign messages sent to COLDCARD via NFC.
|
||||
The resulting signature can be returned over SD card/Vdisk, NFC, or — on Q — as a QR code.
|
||||
|
||||
Signature format follows [BIP-0137](https://github.com/bitcoin/bips/blob/master/bip-0137.mediawiki) specification.
|
||||
COLDCARD Mk3 and COLDCARD Mk4 up to version `5.1.0` used compressed P2PKH header byte for all script types.
|
||||
From Mk4 `5.1.0` correct header byte is used for corresponding script type.
|
||||
From version `5.1.0` correct header byte is used for corresponding script type.
|
||||
|
||||
### Verification
|
||||
|
||||
From COLDCARD Mk4 version `5.1.0` users can verify signed messages directly on the device.
|
||||
From version `5.1.0` users can verify signed messages directly on the device.
|
||||
If signature file is on SD card or Virtual disk `Advanced/Tools -> File Management -> Verify Sig File`. In case
|
||||
signature file is detached signature of signed export (or any other file), COLDCARD can check if digest of file
|
||||
specified in the message matches contents of file. This requires file signed to be available on SD card or Vdisk.
|
||||
@ -21,7 +22,7 @@ Bitcoin core can only verify P2PKH.
|
||||
|
||||
## Signed Exports
|
||||
|
||||
From Mk4 version `5.1.0` most of SD card and Virtual disk exports are accompanied by detached signature file.
|
||||
From version `5.1.0` most of SD card and Virtual disk exports are accompanied by detached signature file.
|
||||
If exported file name is `addresses.csv` signature file name will be `addresses.sig`.
|
||||
|
||||
### Message construction and signature file format
|
||||
@ -39,29 +40,23 @@ IFOvGVJrm31S0j+F4dVfQ5kbRKWKcmhmXIn/Lw8iIgaCG5QNZswjrN4X673R7jTZo1kvLmiD4hlIrbuL
|
||||
-----END BITCOIN SIGNATURE-----
|
||||
```
|
||||
|
||||
### What is signed
|
||||
### What Is Signed
|
||||
|
||||
1. **Single sig address explorer exports**. Signed by key corresponding to first (0th) address on the exported list.
|
||||
2. **Specific single sig exports**. Signed by key corresponding to external address at index zero of chosen application specific derivation `m/<app_deriv>/0/0`
|
||||
1. **Single sig address explorer exports:** Signed by the key corresponding to the first (0th) address on the exported list.
|
||||
2. **Specific single sig exports:** Signed by the key corresponding to the external address at index zero of chosen application specific derivation `m/<app_deriv>h/<coin_type>'h/<account>h/0/0`.
|
||||
* Bitcoin Core
|
||||
* Electrum Wallet
|
||||
* Wasabi Wallet
|
||||
* Samourai Postmix
|
||||
* Samourai Premix
|
||||
* Descriptor
|
||||
3. **Generic single sig exports**. Signed by key that corresponds to address at derivation `m/44'/<coin_type>'/0'/0/0`
|
||||
Lily Wallet
|
||||
Generic JSON
|
||||
Dump Summary
|
||||
4. **BIP85 derived entropy exports**. Signed by path that corresponds to specific BIP85 application.
|
||||
5. **Paper wallet exports**. Signed by key and address exported as paper wallet itself.
|
||||
|
||||
### What is NOT signed
|
||||
|
||||
Multisig exports and generic multisig xpub exports are not signed. It is not clear at this point
|
||||
whether to sign these exports with some generic single signature key (i.e. `m/44'/<coin_type>'/0'/0/0`)
|
||||
or with our portion (leg) of script. In both cases script type (address format) would not match as multisignature
|
||||
message signing is not standardized.
|
||||
|
||||
1. **Multisig exports**
|
||||
2. **Generic multisig exports**
|
||||
3. **Generic single sig exports:** Signed by key that corresponds to first (0th) external address at derivation `m/44h/<coin_type>h/<account>h/0/0`.
|
||||
* Lily Wallet
|
||||
* Generic JSON
|
||||
* Dump Summary
|
||||
4. **BIP85 derived entropy exports:** Signed by path that corresponds to specific BIP85 application.
|
||||
5. **Paper wallet exports:** Signed by key and address exported as paper wallet itself.
|
||||
6. **Multisig exports:** public keys are encoded as P2PKH address for all multisg signature exports
|
||||
* Multisig wallet descriptor: signed by the key corresponding to the first external address of own enrolled extended key `my_key/0/0`
|
||||
* Generic XPUBs export: signed by the key corresponding to the first external address of own standard P2WSH derivation `m/48h/<coin_type>h/<account>h/2h/0/0`
|
||||
* Multisig address explorer export: Signed by own key at the same derivation as first (0th) row on exported list. `my_key/<change>/<start_index>`
|
||||
@ -1,10 +1,20 @@
|
||||
# NFC and Coldcard Mk4
|
||||
# NFC and Coldcard
|
||||
|
||||
(Applies to Coldcard Mk4 only)
|
||||
(Applies to NFC-equipped models: Mk4, Mk5, and Q)
|
||||
|
||||
## Usage
|
||||
|
||||
Mk4 NFC antenna is centered under number `8` on the keypad. Before using NFC,
|
||||
The NFC antenna location depends on the hardware:
|
||||
|
||||
- **Mk4**: a PCB trace loop, centered under number `8` on the keypad.
|
||||
- **Mk5**: a discrete coil (`L6`) in the **top-right corner** of the device
|
||||
- **Q1**: a flexible "sticker" antenna behind the display. The green LED below the
|
||||
bottom-right of the display (`D12`) lights up while an NFC transfer is active —
|
||||
it is the activity indicator, not the antenna.
|
||||
|
||||

|
||||
|
||||
Before using NFC,
|
||||
it is important to locate the position of NFC antenna on your device and point it
|
||||
correctly towards the Coldcard NFC antenna. Picture below shows an example with iPhone
|
||||
that has NFC antenna located at the top right edge. The NFC smartphone antenna
|
||||
@ -36,7 +46,7 @@ in general. Good interoperability is critical with radio standards.
|
||||
|
||||
## Lower Layers
|
||||
|
||||
The Coldcard Mk4 has an chip that acts as a Type 5 NFC tag. The
|
||||
The Coldcard has a chip that acts as a Type 5 NFC tag. The
|
||||
radio standard is called "NFC-V" or ISO-15693, and operates on a
|
||||
13.56 Mhz carrier wave.
|
||||
|
||||
@ -58,9 +68,13 @@ unless we are actively sharing something. We disable the "energy
|
||||
harvesting" features of the chip, so it will not do anything when
|
||||
the Coldcard is powered-down, regardless of the NFC setting.
|
||||
|
||||
If the above is not enough for you, the antenna can be destroyed
|
||||
by cutting the trace labeled "NFC" inside the hole for the MicroSD
|
||||
card. Use the point of a sharp knife to cut and peel up the trace.
|
||||
If the above is not enough for you, the antenna can be destroyed:
|
||||
|
||||
- **Mk4**: cut the trace labeled "NFC" inside the hole for the MicroSD card,
|
||||
using the point of a sharp knife to cut and peel up the trace.
|
||||
- **Mk5**: has no such trace — its antenna is the discrete coil `L6` in the
|
||||
top-right corner, which would have to be physically removed instead.
|
||||
- **Q1**: cut the trace labeled "NFC DATA" under the batteries.
|
||||
|
||||
The NFC traffic is not encrypted and is subject to eavesdropping.
|
||||
While the NFC feature is active, your Coldcard can be uniquely
|
||||
|
||||
@ -34,7 +34,7 @@ The COLDCARD needs a URL prefix. To that it appends some values:
|
||||
- when RegTest is enabled, the value will be `XRT`
|
||||
|
||||
We provide a few default URL values to our customers, including one backend we
|
||||
will operate on `colcard.com`. The URL can also be directly entered by the
|
||||
will operate on `coldcard.com`. The URL can also be directly entered by the
|
||||
customer. On the Q, it can be scanned from a QR code.
|
||||
|
||||
For COLDCARD backend, the url used is:
|
||||
|
||||
BIN
docs/nfc-tap-locations.png
Normal file
BIN
docs/nfc-tap-locations.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 258 KiB |
@ -11,11 +11,17 @@ The entrypoint makefile for repro builds.
|
||||
The `repro` command in `shared.mk` is the first step in the repro build process, which triggers a docker build and run process.
|
||||
|
||||
```makefile
|
||||
repro: submods-match code-committed
|
||||
repro:
|
||||
docker build -t coldcard-build - < dockerfile.build
|
||||
(cd ..; docker run $(DOCK_RUN_ARGS) sh src/stm32/repro-build.sh $(VERSION_STRING) $(MK_NUM))
|
||||
(cd ..; docker run $(DOCK_RUN_ARGS) sh src/stm32/repro-build.sh $(VERSION_STRING) $(HW_MODEL) $(PARENT_MKFILE))
|
||||
```
|
||||
|
||||
`$(HW_MODEL)` is the model string (e.g. `mk4`, `q1`) and `$(PARENT_MKFILE)` is the
|
||||
top-level makefile being used (`MK-Makefile` or `Q1-Makefile`). The `submods-match`
|
||||
and `code-committed` prerequisites ensure the submodules and working tree are clean
|
||||
before a repro build.
|
||||
|
||||
Below are interesting sections from the docker logs that give an idea as to what is going on in build process:
|
||||
|
||||
```stdout
|
||||
@ -61,19 +67,19 @@ Successfully installed signit-1.0
|
||||
|
||||
...
|
||||
|
||||
+ make -f MK4-Makefile setup
|
||||
+ make -f MK-Makefile setup
|
||||
|
||||
...
|
||||
|
||||
+ make -f MK4-Makefile firmware-signed.bin firmware-signed.dfu production.bin dev.dfu firmware.lss firmware.elf
|
||||
+ make -f MK-Makefile firmware-signed.bin firmware-signed.dfu production.bin dev.dfu firmware.lss firmware.elf
|
||||
|
||||
...
|
||||
|
||||
signit sign -b l-port/build-COLDCARD_MK4 -m 4 5.0.7 -o firmware-signed.bin
|
||||
signit sign -b l-port/build-COLDCARD_MK4 -m mk4 5.0.7 -o firmware-signed.bin
|
||||
|
||||
...
|
||||
|
||||
signit sign -m 4 5.0.7 -r firmware-signed.bin -k 1 -o production.bin
|
||||
signit sign -m mk4 5.0.7 -r firmware-signed.bin -k 1 -o production.bin
|
||||
You don't have that key (1), so using key zero instead!
|
||||
...
|
||||
|
||||
@ -96,7 +102,7 @@ production.bin
|
||||
|
||||
...
|
||||
|
||||
+ make -f MK4-Makefile 'PUBLISHED_BIN=/tmp/checkout/firmware/releases/2022-10-05T1724-v5.0.7-mk4-coldcard.dfu' check-repro
|
||||
+ make -f MK-Makefile 'PUBLISHED_BIN=/tmp/checkout/firmware/releases/2022-10-05T1724-v5.0.7-mk4-coldcard.dfu' check-repro
|
||||
|
||||
...
|
||||
|
||||
@ -183,7 +189,7 @@ To summarize `check-repro`:
|
||||
|
||||
- `split` (cli/signit.py: Line 153-175) is run against the release `*.dfu` resulting in a `check-fw.bin` and `check-bootrom.bin`. "This splits the DFU file into the two parts it contains: the main firmware (COLDCARD application) and the boot loader code."
|
||||
|
||||
- `check` (cli/signit.py: Line 176-241) is run against each the release `check-fw.bin` and our built `firmware-signed.bin`.
|
||||
- `check` (cli/signit.py: Line 176-243) is run against each the release `check-fw.bin` and our built `firmware-signed.bin`.
|
||||
|
||||
- a hexdump is taken of each the release `check-fw.bin` and our built `firmware-signed.bin` piped through $TRIM_SIG which removes 64 bytes of signature data and subsitutes it with a common string.
|
||||
|
||||
|
||||
112
docs/proof-of-reserves-bip-322.md
Normal file
112
docs/proof-of-reserves-bip-322.md
Normal file
@ -0,0 +1,112 @@
|
||||
# BIP-322 Generic Signed Message Format
|
||||
|
||||
BIP-322 specification: <https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki>
|
||||
|
||||
## Proof of Reserves (POR)
|
||||
|
||||
### PoR PSBT
|
||||
|
||||
COLDCARD accepts a specially crafted PSBT file to sign as BIP-322 Proof of Reserves. The PSBT
|
||||
must meet all these requirements:
|
||||
|
||||
* COLDCARD acts as a BIP-322 PSBT signer. It validates the BIP-322 `to_sign`
|
||||
transaction, shows the message from `PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE`, and
|
||||
adds signatures to the PSBT. Finalizing and encoding the final BIP-322
|
||||
signature string is the responsibility of the finalizer.
|
||||
* PSBT MUST include `PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE = 0x09`; the value is
|
||||
the exact message shown to the user and signed by BIP-322.
|
||||
* PSBT requires `PSBT_IN_BIP32_DERIVATION` for each input
|
||||
* P2SH wrapped segwit addresses MUST have proper redeem script in PSBT: `PSBT_IN_REDEEM_SCRIPT`
|
||||
* P2WSH segwit addresses MUST have proper witness script in PSBT: `PSBT_IN_WITNESS_SCRIPT`
|
||||
* PSBT (`to_sign`) MUST have at least one input.
|
||||
* First (0th) input of `to_sign` MUST spend the BIP-322 `to_spend` output.
|
||||
* Input 0 MUST include one of `PSBT_IN_NON_WITNESS_UTXO` or `PSBT_IN_WITNESS_UTXO`.
|
||||
* When input 0 provides `PSBT_IN_WITNESS_UTXO`, COLDCARD reconstructs the
|
||||
expected `to_spend` txid from `PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE` and the
|
||||
witness UTXO scriptPubKey.
|
||||
* When input 0 provides `PSBT_IN_NON_WITNESS_UTXO`, it MUST be the BIP-322
|
||||
`to_spend` transaction as defined in
|
||||
[BIP-322](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full):
|
||||
* 1 input, 1 output
|
||||
* output nValue is 0
|
||||
* input prevout hash is 0
|
||||
* input prevout n is 0xffffffff
|
||||
* input scriptSig is `OP_0 PUSH32 message_hash`
|
||||
* PSBT (`to_sign`) MUST only have one output with null-data `OP_RETURN`
|
||||
* `to_sign` transaction version MUST be 0 or 2.
|
||||
* Optionally inputs can be added to `to_sign` for Proof of Reserve signing.
|
||||
* PSBT MUST be version 0 or 2.
|
||||
* Foreign inputs not allowed in POR PSBT.
|
||||
|
||||
The signatures created by the BIP-322 process will never be suitable
|
||||
for a on-chain Bitcoin transaction that could move funds, because
|
||||
of these restrictions imposed by BIP-322.
|
||||
|
||||
### Output
|
||||
|
||||
COLDCARD always returns a signed PSBT for BIP-322 message signing and Proof of
|
||||
Reserves. It never returns an extracted/finalized transaction for these PSBTs.
|
||||
This is true even when finalization is requested over USB, such as with
|
||||
`ckcc unsigned.psbt --finalize`.
|
||||
|
||||
The signed PSBT is the handoff artifact for the external finalizer/verifier. It
|
||||
keeps the PSBT metadata needed to verify or finalize the BIP-322 signature,
|
||||
including public keys, scripts, partial signatures, and UTXO data. This matters
|
||||
because the address being proven normally commits only to a hash of the public
|
||||
key or script, not the public key or script itself.
|
||||
|
||||
### Proof of Reserves Signing Experience
|
||||
|
||||
After Coldcard recognizes a BIP-322 PSBT it reads the message from
|
||||
`PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE` and shows it to the user for approval.
|
||||
COLDCARD verifies that the message hash matches the input 0 `to_spend`
|
||||
commitment before offering to sign.
|
||||
|
||||
When the PSBT contains only input 0, COLDCARD labels the request as
|
||||
`BIP-322 Message`, because it is message signing and does not prove ownership
|
||||
of any additional reserve UTXOs. In that case it does not show transaction
|
||||
input/output counts. When the PSBT contains additional inputs, COLDCARD labels
|
||||
the request as `Proof of Reserves` and shows the reserve amount.
|
||||
|
||||
If the message contains non-ASCII characters, COLDCARD warns that some
|
||||
characters may not be readable on screen.
|
||||
|
||||
Legacy PoR PSBTs without `PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE` are rejected by
|
||||
this flow.
|
||||
|
||||
Read more [here.](https://gist.github.com/orangesurf/0c1d0a31d3ebe7e48335a34d56788d4c)
|
||||
|
||||
Example screen text for a one-input BIP-322 message signing PSBT:
|
||||
|
||||
```text
|
||||
BIP-322 Message
|
||||
|
||||
Message:
|
||||
This is the signed message
|
||||
|
||||
Challenge Address:
|
||||
bc1qzvjnhf7k70uxv6xvneaqxql7k09dd6nsr5wheq
|
||||
|
||||
Press ENTER to approve and sign message. Press (2) to explore transaction.
|
||||
CANCEL to abort.
|
||||
```
|
||||
|
||||
Example screen text for a Proof of Reserves PSBT:
|
||||
|
||||
```text
|
||||
Proof of Reserves
|
||||
|
||||
Message:
|
||||
POR
|
||||
|
||||
Amount 0.20000000 BTC
|
||||
|
||||
Challenge Address:
|
||||
bc1qzvjnhf7k70uxv6xvneaqxql7k09dd6nsr5wheq
|
||||
|
||||
21 inputs
|
||||
1 output
|
||||
|
||||
Press ENTER to approve and sign proof of reserves. Press (2) to explore transaction.
|
||||
CANCEL to abort.
|
||||
```
|
||||
@ -92,8 +92,8 @@ increases flexibility and resistance to known plain text attacks.
|
||||
| `pin stretch` | slot 2 | HMAC | SE1 | Key stretching for PIN entry and anti-phish words
|
||||
| `firmware` | slot 14 | SHA256d | SE1 | Firmware checksum, controls green/red LEDs
|
||||
| `nonce/chksum` | slot 10 | data | SE1 | AES nonce and GMAC tag, protected by PIN
|
||||
| `SE2 easy key` | page 15 | AES via HMAC | SE2 | Another SE2 part of AES seed key
|
||||
| `SE2 hard key` | page 14 | AES via ECC | SE2 | SE2's part of AES seed key; ECC used to unlock
|
||||
| `SE2 easy key` | page 14 | AES via HMAC | SE2 | Another SE2 part of AES seed key
|
||||
| `SE2 hard key` | page 15 | AES via ECC | SE2 | SE2's part of AES seed key; ECC used to unlock
|
||||
| `tpin key` | `tpin_key` | HMAC(key) | MCU | Key for HMAC used to encrypt trick PINs
|
||||
| `trick PIN slots` | pages 0-12 | HMAC | SE2 | Protect duress wallet seeds and pins (6 spots)
|
||||
| `SE2 trash` | secret B | HMAC | SE2 | Used to destroy values (only SE2 knows the value)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# COLDCARD Mk4/Q Security Model
|
||||
# COLDCARD Mk4/Mk5/Q Security Model
|
||||
|
||||
## Abstract
|
||||
|
||||
@ -96,9 +96,10 @@ user entered the True PIN. An attacker will only have access to the
|
||||
duress wallet. They won't have access to steal the main stash.
|
||||
|
||||
The private key can be automatically derived using BIP-85 methods,
|
||||
based on account numbers 1001, 1002, or 1003. Because this is BIP-85
|
||||
based and uses a 24-word seed, it behaves exactly like a normal
|
||||
wallet. Defining a passphrase for the wallet is also possible.
|
||||
based on account numbers 1001, 1002, or 1003 for a 24-word duress wallet
|
||||
(or 2001, 2002, 2003 for a 12-word one). Because this is BIP-85
|
||||
based, it behaves exactly like a normal wallet. Defining a passphrase
|
||||
for the wallet is also possible.
|
||||
|
||||
The Mk4 also supports older COLDCARD duress wallets and their UTXOs
|
||||
on the blockchain. There is an option to create compatible wallets
|
||||
@ -243,7 +244,7 @@ COLDCARD's case to do so, but the option is there if needed.
|
||||
|
||||
## SD Card Recovery Mode
|
||||
|
||||
Mk4/Q bootloader is smart enough to be able to read an SD card. You
|
||||
Mk4/Mk5/Q bootloader is smart enough to be able to read an SD card. You
|
||||
will only be able to trigger the SD card loading code, if the
|
||||
COLDCARD was powered down during the upgrade process. At that point,
|
||||
the intended firmware image has been lost because it it held in
|
||||
|
||||
@ -12,7 +12,7 @@ are not discrete and you could be compelled to produce the passphrase.
|
||||
|
||||
Enter [_Seed XOR_](https://seedxor.com), a plausibly deniable means
|
||||
of storing secrets in two or more parts that look and behave just
|
||||
like the original secret. One 12 or 24-word seed phrase becomes two or more parts
|
||||
like the original secret. One 12-, 18-, or 24-word seed phrase becomes two or more parts
|
||||
that are also BIP-39 compatible seeds phrases. These should be backed up in your
|
||||
preferred method, metal or otherwise. These parts can be individually loaded
|
||||
with honeypot funds as each one has same word length, with the last being
|
||||
@ -22,7 +22,7 @@ This one more solution for your game-theory arsenal.
|
||||
|
||||
- *Q*: I'm lazy, can I do this to my Existing Seed?
|
||||
- *A*: Yes. You can split the words you have already in your Coldcard, making
|
||||
2, 3 or 4 new SEEDPLATES. You could also any number of existing SEEDPLATES
|
||||
2, 3 or 4 new SEEDPLATES. You could also use any number of existing SEEDPLATES
|
||||
you have, and combine them to make a new random wallet that is the XOR of
|
||||
their values. Effectively that makes a new random wallet.
|
||||
|
||||
@ -78,10 +78,12 @@ words right the next day.
|
||||
|
||||
When the parts are made deterministically, we take a double-SHA256 over
|
||||
a fixed string (`Batshitoshi`), your master secret, and the text
|
||||
`1 of 4 parts` which changes for each part.
|
||||
`0 of 4 parts` which changes for each part (the index is 0-based).
|
||||
|
||||
In random mode, we simply pick 32 random bytes (and then double-SHA256
|
||||
them) from the Coldcard's True Random Number Generator (TRNG)..
|
||||
In random mode, we simply pick random bytes (and then double-SHA256
|
||||
them) from the Coldcard's True Random Number Generator (TRNG). The number
|
||||
of bytes matches your secret length: 16, 24, or 32 bytes for a 12-, 18-,
|
||||
or 24-word seed respectively.
|
||||
|
||||
This is done to make all but the one part. The final part is the
|
||||
value needed to get back to your secret, so it's the XOR of the
|
||||
@ -157,6 +159,12 @@ with the others on a SEEDPLATE.
|
||||
- right to A, down to B ... take that number, and go to that column
|
||||
- down to C, that is answer: a ⊕ 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.
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Temporary Seeds
|
||||
|
||||
|
||||
[_(new in v5.0.7, requires Mk4)_](upgrade.md)
|
||||
[_(new in v5.0.7, requires Mk4, Mk5, or Q)_](upgrade.md)
|
||||
|
||||
|
||||
Temporary seed (renamed in `5.2.0` from Ephemeral seed) is a temporary secret completely separate
|
||||
@ -42,7 +42,7 @@ Read more about `Seed Vault` feature below.
|
||||
- `24 words`
|
||||
- `XPRV (BIP-32)`
|
||||
- pick derivation `Index` in next prompt, or just press OK for index 0
|
||||
- Press (2) in next prompt to activate derived secret as a temporary seed
|
||||
- Press (0) in next prompt to activate derived secret as a temporary seed
|
||||
|
||||
* temporary seed can be activated from Duress Wallet
|
||||
- go to `Settings -> Login Settings -> Trick Pins`
|
||||
@ -66,7 +66,7 @@ Ability to generate and use **Temporary seed** is available on Coldcard when:
|
||||
|
||||
# Restore Master
|
||||
|
||||
[_(new in v5.2.0, requires Mk4)_](upgrade.md)
|
||||
[_(new in v5.2.0, requires Mk4, Mk5, or Q)_](upgrade.md)
|
||||
|
||||
From version `5.2.0` users no longer need to reboot COLDCARD to return
|
||||
to their "master seed" (one stored in SE2). Once COLDCARD has temporary
|
||||
@ -84,7 +84,7 @@ Seed Vault entries can only be deleted in Seed Vault menu.
|
||||
|
||||
# Seed Vault
|
||||
|
||||
[_(new in v5.2.0, requires Mk4)_](upgrade.md)
|
||||
[_(new in v5.2.0, requires Mk4, Mk5, or Q)_](upgrade.md)
|
||||
|
||||
Seed Vault adds the ability to store multiple temporary secrets into encrypted settings for simple
|
||||
recall and later use (AES-256-CTR encrypted with your master seed's key).
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
|
||||
# Firmware Upgrade and Recovery Process
|
||||
|
||||
_This document applies only to the Mk4. Earlier COLDCARDs did not use this approach._
|
||||
_This document applies to the Mk4, Mk5, and Q. Earlier COLDCARDs did not use this approach._
|
||||
|
||||
On the COLDCARD, we have done away with the slow external SPI flash
|
||||
(serial flash) chip entirely (used in Mk1-Mk3). In it's place we
|
||||
|
||||
97
docs/web2fa.md
Normal file
97
docs/web2fa.md
Normal file
@ -0,0 +1,97 @@
|
||||
# Web 2FA Authentication
|
||||
|
||||
How to support [RFC 6238](https://www.rfc-editor.org/rfc/rfc6238)
|
||||
TOTP (Time based One Time Password) 2FA check, on our little embedded
|
||||
device without a real-time clock?
|
||||
|
||||
Solution: Store the pre-shared secret in the COLDCARD, and send that
|
||||
securely to a trusted webserver which knows the time and can do a
|
||||
fancy UX. That webserver accepts the time-based-one-time 2FA numeric
|
||||
code from the user, and if correct, reveals a secret
|
||||
that can be used back on the COLDCARD to authorize an action.
|
||||
|
||||
For the Mk4, the secret is 8 digit numeric code to be entered,
|
||||
for the COLDCARD Q, it is a QR code to be scanned.
|
||||
|
||||
### History / Background
|
||||
|
||||
The HSM feature uses HOTP tokens, which do not require a backend,
|
||||
but are not as robust as time-based tokens.
|
||||
|
||||
Web2FA is available to be enabled as part of a Spending Policy,
|
||||
both in Multisig and Single Signer modes. When enabled, you will be
|
||||
prompted complete 2FA authentication after viewing the details of
|
||||
the transaction to be signed. You will not be able to sign without
|
||||
the correct code.
|
||||
|
||||
## How It Works
|
||||
|
||||
- Web backend has a ECC keypair, with pubkey known to CC firmware releases.
|
||||
- Usual 2fa base32 secret is picked by CC and stored in CC (so that server is stateless)
|
||||
- CC creates URL encrypted to the pubkey of server, containing args:
|
||||
- shared secret for TOTP (same value as held in user's phone)
|
||||
- the response nonce (32 bytes, shown as 64 hex chars, on Q; or 8 digits on Mk4)
|
||||
to be revealed to the user on successful auth
|
||||
- flag if Q model, so can provide a QR to be scanned in that case (rather than digits)
|
||||
- some text label for what's being approved, which is presented to user so they can pick
|
||||
correct 2fa shared secret.
|
||||
- above is all encrypted in transit, and only the server can decrypt
|
||||
- user is sent to that encrypted URL using NFC tap on the COLDCARD
|
||||
- user arrives at server:
|
||||
- shown label [which also indicates the server can be trusted, since only it could decrypt it]
|
||||
- prompt for 6 digits from authenticator app
|
||||
- does [RFC 6238](https://www.rfc-editor.org/rfc/rfc6238) 2FA check using current time
|
||||
- checks using current time and the shared secret provided by CC, fails if wrong.
|
||||
- time based failure: offer retry (they typed too slow / minor clock drift)
|
||||
- can offer to retry, but also do some rate limiting (only one attempt per 30-sec period)
|
||||
- server will store very recent responses so attacker cannot get two codes
|
||||
in any 30sec period (ie. blocks immediate reuse of same URL)
|
||||
- until a valid code is given, user is stuck here
|
||||
- when valid token received:
|
||||
- if Q, show a QR code to be scanned, with the full nonce
|
||||
- for non-Q system, a 8-digit decimal value is given: user has to enter that into the COLDCARD
|
||||
- web site shows instructions about what to do next on product.
|
||||
|
||||
## From COLDCARD PoV
|
||||
|
||||
- makes complex encrypted URL, which contains a nonce it wants, waits for that nonce back (or QR)
|
||||
- it's either the nonce from the URL, or fail
|
||||
- if the right nonce, then we know the server knows the decryption key, and we
|
||||
are trusting it actually verify the 2FA token properly.
|
||||
|
||||
## Encryption - Simple ECDH
|
||||
|
||||
- CC picks a secp256k1 keypair, generates compressed pubkey
|
||||
- multiplies that private key by server's known public key
|
||||
- apply sha256(resulting coordinate) => the session key
|
||||
- apply AES-256-CTR over URL contents (ascii text)
|
||||
- prepend 33 bytes of pubkey, and then base64url encode all of it
|
||||
- full url is: `https://coldcard.com/2fa?{base64 encoded binary}`
|
||||
|
||||
## Trust Issues
|
||||
|
||||
- 2FA enrol happens on the CC, which picks the shared secret and shows QR for mobile
|
||||
app setup. Same TRNG process as picking a seed.
|
||||
- Server knows the shared secret, but only during operation, and we won't store it [sorry,
|
||||
gotta trust us on that, but no help to us to store it].
|
||||
- Only we can run the server, because the private key is company-secret.
|
||||
- MiTM and network snoopers get nothing because HTTPS is used and only your browser
|
||||
can see the nonce, and only after you've given the right digits.
|
||||
- Coinkite server could skip the 2FA checks and just give you the answer
|
||||
you want to type into the COLDCARD. Again, you have to trust us on that.
|
||||
|
||||
## URL Format
|
||||
|
||||
https://coldcard.com/2fa?g={nonce}&ss={shared_secret}&nm={label_text}&q={is_q}
|
||||
|
||||
(the query string is then encrypted to the server's pubkey, so the args above
|
||||
are what is inside the encrypted payload.)
|
||||
|
||||
- `nonce`: text string that is either 8 digits on Mk4, or 64 hex chars on Q
|
||||
- `shared_secret`: 16 chars of Base32-encoded pre-shared secret
|
||||
- `nm`: human readable label for the transaction/purpose
|
||||
- `is_q`: flag indicating use of QR to provide nonce back to user
|
||||
|
||||
Server will accept plaintext arguments as above, but normally everything
|
||||
after the question mark is encrypted.
|
||||
|
||||
2
external/ckcc-protocol
vendored
2
external/ckcc-protocol
vendored
@ -1 +1 @@
|
||||
Subproject commit 0e686dbda686f76c4d3e8069558b2a31f9d1c2b1
|
||||
Subproject commit 3d1dfa858beb58b8dac37d8c66d7aed2909812f2
|
||||
2
external/libngu
vendored
2
external/libngu
vendored
@ -1 +1 @@
|
||||
Subproject commit 1cccb25ef7736efae4a1de83d5dbdc13a2db0e80
|
||||
Subproject commit 537519a829259622ea6b0334fbafd6cae852852f
|
||||
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.
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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,197 @@
|
||||
*See ChangeLog.md for more recent changes, these are historic versions*
|
||||
|
||||
|
||||
## 1.3.5Q - 2025-11-03
|
||||
|
||||
- Enhancement: Address format guessing changed away from using PSBT XPUB's derivation paths.
|
||||
Now based on witness/redeem script of first PSBT input instead.
|
||||
- Enhancement: Show master XFP of backup secret and ask user for confirmation before loading backup.
|
||||
- Enhancement: Show firmware version added to hobbled Advanced/Tools menu.
|
||||
- Bugfix: Exiting text input of Custom Backup Password caused yikes.
|
||||
- Bugfix: Temporary seeds in SSSP mode were not able to update block height.
|
||||
- Enhancement: Show backup filename at the top of the screen during backup password entry.
|
||||
|
||||
|
||||
## 1.3.4Q - 2025-09-30
|
||||
|
||||
- Spending policies for "Single Signers" adds new spending policy options:
|
||||
- limit your Coldcard so it refuses to sign transactions that are "too big"
|
||||
- require 2FA authentication before signing any transaction (NFC+web)
|
||||
- velocity limits can restrict how often new transactions can be signed
|
||||
- see `docs/spending-policy.md` for more details
|
||||
- "Enable HSM" and "User Management" have moved into `Advanced > Spending Policy`.
|
||||
- Old "CCC" feature has been renamed and moved into that menu as well: "Co-Sign Multisig"
|
||||
- Added `Bull Bitcoin` export to `Export Wallet` menu.
|
||||
- Enhancement: Added warning for zero value outputs if not `OP_RETURN`.
|
||||
- Enhancement: Show QR codes of output addresses in transaction output explorer. Explorer is
|
||||
now offered for transactions of all sizes, not just complex ones.
|
||||
- Enhancement: Added file rename, when listing contents of SD card.
|
||||
- Enhancement: Added ability to restore Coldcard backup via USB (needs latest of ckcc version)
|
||||
- Enhancement: Address ownership allows to specify particular multisig wallet in which to search,
|
||||
if `wallet` query parameter is provided via trivial extension to
|
||||
[BIP-21](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki).
|
||||
Example: `tb1q4d67p7stxml3kdudrgkg5mgaxsrgzcqzjrrj4gg62nxtvnsnvqjsxjkej0?wallet=Haystack`
|
||||
- Bugfix: If all change outputs have `nValue=0`, they were not shown in UX.
|
||||
- Bugfix: Disallow negative input/output amounts in PSBT.
|
||||
- Bugfix: Fix filesystem initialization after Wipe LFS or Destroy Seed.
|
||||
- Bugfix: Fix MicroSD selftest code.
|
||||
- Bugfix: NFC loop exporting secrets would not work after first value exported.
|
||||
- Bugfix: Multisig address format handling.
|
||||
- Bugfix: Ownership check failing to find addresses near max (~760), needed to be re-run to succeed
|
||||
- (Q only) Enhancement: Enters "forever calculator" mode when Q would otherwise be electronic waste
|
||||
(ie. after 13 PIN failures). Always enabled, regardless of "login calculator" setting.
|
||||
- (Q only) Bugfix: Correct line positioning when 24 seed words displayed.
|
||||
|
||||
|
||||
## 1.3.3Q - 2025-05-14
|
||||
|
||||
- Enhancement: Text word-wrap done more carefully so never cuts off any text, and yet
|
||||
doesn't waste space.
|
||||
- Bugfix: `Add current tmp` option, which could be shown in `Seed Vault` menu under
|
||||
specific circumstances, would corrupt master settings if selected.
|
||||
- Bugfix: PUSHDATA2 in bitcoin script caused yikes.
|
||||
- Bugfix: Warning for unknown scripts was not shown at the top of the signing story.
|
||||
|
||||
- Bugfix: Do not allow to teleport PSBTs from SD card when CC has no secrets.
|
||||
- Bugfix: Calculator login mode: added "rand()" command, removed support
|
||||
for variables/assignments.
|
||||
|
||||
|
||||
## 1.3.2Q - 2025-04-16
|
||||
|
||||
- Feature: Key Teleport -- Easily and securely move seed phrases, secure notes/passwords,
|
||||
multisig PSBT files, and even full Coldcard backups, between two Q using QR codes
|
||||
and/or NFC with helper website. See protocol spec in
|
||||
[docs/key-teleport.md](https://github.com/Coldcard/firmware/blob/master/docs/key-teleport.md)
|
||||
- can send master seed (words, xprv), anything held in seed vault, secure notes/passwords
|
||||
(singular, or all) and PSBT involved in a multisig to the other co-signers
|
||||
- full COLDCARD backup is possible as well, but receiver must be "unseeded" Q for best result
|
||||
- ECDH to create session key for AES-256-CTR, with another layer of AES-256-CTR using a
|
||||
short password (stretched by PBKDF2-SHA512) inside
|
||||
- receiver shows sender a (simple) QR and a numeric code; sender replies with larger BBQr
|
||||
and 8-char password
|
||||
- Enhancement: Always choose the biggest possible display size for QR
|
||||
- Bugfix: Only BBQr is allowed to export Coldcard, Core, and pretty descriptor
|
||||
- Huge new feature: CCC - ColdCard Cosign
|
||||
- COLDCARD holds a key in a 2-of-3 multisig, in addition to the normal signing key it has.
|
||||
- it applies a spending policy like an HSM:
|
||||
- velocity and magnitude limits
|
||||
- whitelisted destination addresses
|
||||
- 2FA authentication using phone app ([RFC 6238](https://www.rfc-editor.org/rfc/rfc6238))
|
||||
- but will sign its part of a transaction automatically if those condition are met,
|
||||
giving you 2 keys of the multisig and control over the funds
|
||||
- spending policy can be exceeded with help of the other co-signer (3rd key), when needed
|
||||
- cannot view or change the CCC spending policy once set, policy violations are not explained
|
||||
- existing multisig wallets can be used by importing the spending-policy-controlled key
|
||||
- New Feature: Multisig transactions are finalized. Allows use of [PushTX](https://pushtx.org/)
|
||||
with multisig wallets. Read more [here](https://github.com/Coldcard/firmware/blob/master/docs/limitations.md#p2sh--multisig)
|
||||
- New Feature: Signing artifacts re-export to various media. Now you have the option of
|
||||
exporting the signing products (transaction/PSBT) to different media than the original source.
|
||||
Incoming PSBT over QR can be signed and saved to SD card if desired.
|
||||
- New Feature: Multisig export files are signed now. Read more [here](https://github.com/Coldcard/firmware/blob/master/docs/msg-signing.md#signed-exports)
|
||||
- Enhancement: NFC export usability upgrade: NFC keeps exporting until CANCEL/X is pressed
|
||||
- Enhancement: Add `Bitcoin Safe` option to `Export Wallet`
|
||||
- Enhancement: 10% performance improvement in USB upload speed for large files
|
||||
- Bugfix: Do not allow change Main PIN to same value already used as Trick PIN, even if
|
||||
Trick PIN is hidden.
|
||||
- Bugfix: Fix stuck progress bar under `Receiving...` after a USB communications failure
|
||||
- Bugfix: Showing derivation path in Address Explorer for root key (m) showed double slash (//)
|
||||
- Bugfix: Can restore developer backup with custom password other than 12 words format
|
||||
- Bugfix: Virtual Disk auto mode ignores already signed PSBTs (with "-signed" in file name)
|
||||
- Bugfix: Virtual Disk auto mode stuck on "Reading..." screen sometimes
|
||||
- Bugfix: Finalization of foreign inputs from partial signatures. Thanks Christian Uebber
|
||||
- Bugfix: Temporary seed from COLDCARD backup failed to load stored multisig wallets
|
||||
- Change: `Destroy Seed` also removes all Trick PINs from SE2.
|
||||
- Change: `Lock Down Seed` requires pressing confirm key (4) to execute
|
||||
|
||||
## 1.3.1Q - 2025-02-13
|
||||
|
||||
- New signing features:
|
||||
- Sign message from note text, or password note
|
||||
- JSON message signing. Use JSON object to pass data to sign in form
|
||||
`{"msg":"<required msg>","subpath":"<optional sp>","addr_fmt": "<optional af>"}`
|
||||
- Sign message with key resulting from positive ownership check. Press (0) and
|
||||
enter or scan message text to be signed.
|
||||
- Sign message with key selected from Address Explorer Custom Path menu. Press (2) and
|
||||
enter or scan message text to be signed.
|
||||
- Enhancement: New address display format improves address verification on screen (groups of 4).
|
||||
- Deltamode enhancements:
|
||||
- Hide Secure Notes & Passwords in Deltamode. Wipe seed if notes menu accessed.
|
||||
- Hide Seed Vault in Deltamode. Wipe seed if Seed Vault menu accessed.
|
||||
- Catch more DeltaMode cases in XOR submenus. Thanks [@dmonakhov](https://github.com/dmonakhov)
|
||||
- Enhancement: Add ability to switch between BIP-32 xpub, and obsolete SLIP-132 format
|
||||
in `Export XPUB`
|
||||
- Enhancement: Use the fact that master seed cannot be used as ephemeral seed, to show message
|
||||
about successful master seed verification.
|
||||
- Enhancement: Allow devs to override backup password.
|
||||
- Enhancement: Add option to show/export full multisg addresses without censorship. Enable
|
||||
in `Settings > Multisig Wallets > Full Address View`.
|
||||
- Enhancement: If derivation path is omitted during message signing, derivation path
|
||||
default is no longer root (m), instead it is based on requested address format
|
||||
(`m/44h/0h/0h/0/0` for p2pkh, and `m/84h/0h/0h/0/0` for p2wpkh). Conversely,
|
||||
if address format is not provided but subpath derivation starts with:
|
||||
`m/84h/...` or `m/49h/...`, then p2wpkh or p2sh-p2wpkh respectively, is used.
|
||||
- Bugfix: Sometimes see a struck screen after _Verifying..._ in boot up sequence.
|
||||
On Q, result is blank screen, on Mk4, result is three-dots screen.
|
||||
- Bugfix: Do not allow to enable/disable Seed Vault feature when in temporary seed mode.
|
||||
- Bugfix: Bless Firmware causes hanging progress bar.
|
||||
- Bugfix: Prevent yikes in ownership search.
|
||||
- Bugfix: Factory-disabled NFC was not recognized correctly.
|
||||
- Bugfix: Be more robust about flash filesystem holding the settings.
|
||||
- Bugfix: Do not include sighash in PSBT input data, if sighash value is `SIGHASH_ALL`.
|
||||
- Bugfix: Allow import of multisig descriptor with root (m) keys in it.
|
||||
Thanks [@turkycat](https://github.com/turkycat)
|
||||
- Change: Do not purge settings of current active tmp seed when deleting it from Seed Vault.
|
||||
- Change: Rename Testnet3 -> Testnet4 (all parameters unchanged).
|
||||
|
||||
- New Feature: Verify Signed RFC messages via BBQr
|
||||
- New Feature: Sign message from QR scan (format has to be JSON)
|
||||
- Enhancement: Sign/Verify Address in Sparrow via QR
|
||||
- Enhancement: Sign scanned Simple Text by pressing (0). Next screen query information
|
||||
about which key to use.
|
||||
- Enhancement: Add option to "Sort By Title" in Secure Notes and Passwords. Thanks to
|
||||
[@MTRitchey](https://x.com/MTRitchey) for suggestion.
|
||||
- Bugfix: Properly re-draw status bar after Restore Master on COLDCARD without master seed.
|
||||
|
||||
|
||||
## 1.3.0Q - 2024-09-12
|
||||
|
||||
- New Feature: Opt-in support for unsorted multisig, which ignores BIP-67 policy. Use
|
||||
descriptor with `multi(...)`. Disabled by default, Enable in
|
||||
`Settings > Multisig Wallets > Legacy Multisig`. Recommended for existing multisig
|
||||
wallets, not new ones.
|
||||
- New Feature: Named multisig descriptor imports. Wrap descriptor in json:
|
||||
`{"name:"ms0", "desc":"<descriptor>"}` to provide a name for the menu in `name`.
|
||||
instead of the filename. Most useful for USB and NFC imports which have no filename,
|
||||
(name is created from descriptor checksum in those cases).
|
||||
- New Feature: XOR from Seed Vault (select other parts of the XOR from seeds in the vault).
|
||||
- Enhancement: upgrade to latest
|
||||
[libsecp256k1: 0.5.0](https://github.com/bitcoin-core/secp256k1/releases/tag/v0.5.0)
|
||||
- Enhancement: Signature grinding optimizations. Now about 30% faster signing!
|
||||
- Enhancement: Improve side-channel protection: libsecp256k1 context randomization now happens
|
||||
before each signing session.
|
||||
- Enhancement: Allow JSON files in `NFC File Share`.
|
||||
- Change: Do not require descriptor checksum when importing multisig wallets.
|
||||
- Bugfix: Do not allow import of multisig wallet when same keys are shuffled.
|
||||
- Bugfix: Do not read whole PSBT into memory when writing finalized transaction (performance).
|
||||
- Bugfix: Prevent user from restoring Seed XOR when number of parts is smaller than 2.
|
||||
- Bugfix: Fix display alignment of Seed Vault menu.
|
||||
- Bugfix: Properly handle null data in `OP_RETURN`.
|
||||
- Bugfix: Do not allow lateral scroll in Address Explorer when showing single address
|
||||
from custom path.
|
||||
- Change: Remove Lamp Test from Debug Options (covered by selftest).
|
||||
- New Feature: Seed XOR can be imported by scanning SeedQR parts.
|
||||
- New Feature: Input backup password from QR scan.
|
||||
- New Feature: (BB)QR file share of arbitrary files.
|
||||
- New Feature: `Create Airgapped` now works with BBQRs.
|
||||
- Change: Default brightness (on battery) adjusted from 80% to 95%.
|
||||
- Bugfix: Properly clear LCD screen after BBQR is shown.
|
||||
- Bugfix: Writing to empty slot B caused broken card reader.
|
||||
- Bugfix: During Seed XOR import, display correct letter B if own seed already added to the mix.
|
||||
- Bugfix: Stop re-wording UX stories using a regular expression.
|
||||
- Bugfix: Fixed "easy exit" from quiz after split Seed XOR.
|
||||
|
||||
|
||||
## 1.2.3Q - 2024-07-05
|
||||
|
||||
- New Feature: PushTX: once enabled with a service provider's URL, you can tap the COLDCARD
|
||||
|
||||
@ -2,21 +2,73 @@
|
||||
|
||||
This lists the new changes that have not yet been published in a normal release.
|
||||
|
||||
# Shared Improvements - Both Mk4 and Q
|
||||
# Shared Improvements - Both Mk and Q
|
||||
|
||||
# Mk4 Specific Changes
|
||||
- Change: BIP-322 Proof of Reserves & message signing PSBT requires PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE
|
||||
(read more [BIP-322 Proof of Reserves documentation](../docs/proof-of-reserves-bip-322.md) )
|
||||
- Enhancement: WIF Store export watch-only descriptor
|
||||
- Enhancement: WIF Store address detection without the need for PSBT_IN_BIP32_DERIVATION (Electrum support)
|
||||
- Enhancement: Improve USB length validation
|
||||
- Bugfix: Fixes legacy input amount spoofing by rejecting witness-utxo-only PSBT inputs when Coldcard is expected to sign a non-segwit input.
|
||||
When both UTXO fields are present the full non_witness_utxo is now preferred for amount/script lookup. Thanks, @Damir
|
||||
- Bugfix: Emit warning and do not calculate fee for legacy UTXOs with only witness utxo
|
||||
- Bugfix: Disable Virtual Disk and NFC before activating HSM
|
||||
- Bugfix: P2PK signing was broken. Now supports both compressed and uncompressed P2PK spend
|
||||
- Bugfix: Custom address default menu position wrong
|
||||
- Bugfix: Delta Mode Trick PIN was never restored from backup
|
||||
- Bugfix: Proper error message for incorrect 7z headers
|
||||
- Bugfix: Exiting nickname entry with nickname already saved deleted previous nickname
|
||||
- Bugfix: "Send Password" menu item inside Notes & Passwords visibility reversed
|
||||
- Bugfix: Yikes when using "Send Password" on entry with password None field
|
||||
- Bugfix: Do not show "Saving..." UX after failed Notes & Passwords import
|
||||
- Bugfix: Incorrect error message caused by error in Verify/Decrypt Backup
|
||||
- Bugfix: NFC Verify Address raised incorrect error message
|
||||
- Bugfix: Notes & Passwords bulk import JSON with BBQr encoded as text
|
||||
- Bugfix: CCC key C challenge handled bad BIP-39 checksum by crashing the UX; now treated as a wrong attempt (counts toward 3-strike lockout)
|
||||
- Bugfix: CCC magnitude reset from CANCEL on empty input
|
||||
- Bugfix: OP_RETURN in CCC with whitelist enabled caused yikes
|
||||
- Bugfix: TX Explorer crashed on foreign input with non-standard sighash
|
||||
- Bugfix: Malformed JSON message-sign request crashed signing UX
|
||||
- Bugfix: Reject UI-control bytes in JSON / QR text message-signing
|
||||
- Bugfix: Non-standard OP_RETURN outputs shown as "null-data", hiding part of the script
|
||||
- Bugfix: Over-limit CCC address-whitelist import was rejected but still modified the policy
|
||||
- Bugfix: Deleting a file right after renaming it (List Files) blanked the old name, leaving the renamed file
|
||||
- Bugfix: Reordered `multi(...)` multisig with same keys was misreported as name-only change. Now blocked as duplicate.
|
||||
- Bugfix: Max WIF store capacity limit was ignored if saving via QR WIF visualization
|
||||
- Bugfix: Force Seed XOR restore from Temporary Seed menu to remain temporary even when master seed is blank
|
||||
- Bugfix: Q1 seed word entry cursor alignment for 12-word seeds and preserve visible words after failed QR scans
|
||||
- Bugfix: Binary signed-transaction (.txn) failed in NFC/QR file share
|
||||
- Bugfix: yikes in transaction explorer for goto index for tx with only one output
|
||||
- Bugfix: Sending `signmessage` payload encoded as BBQr caused yikes
|
||||
- Bugfix: CCC/SSSP NFC whitelist import caused Yikes
|
||||
- Bugfix: Stricter address ownership validation rejects unrecognized payment addresses before wallet search
|
||||
- Bugfix: Handle malformed NDEF records robustly. Thanks, @Damir
|
||||
- Bugfix: Ignore `bkpw` if added to backup. Thanks [@dmonakhov](https://github.com/dmonakhov)
|
||||
- Bugfix: Keep NFC export tag live for repeated probes
|
||||
- Bugfix: Fix 1of1 multisig signing failure
|
||||
|
||||
# Mk Specific Changes
|
||||
|
||||
## 5.5.x - 2065-04-xx
|
||||
|
||||
- tbd
|
||||
|
||||
|
||||
## 5.4.? - 2024-??-??
|
||||
|
||||
- tbd
|
||||
|
||||
|
||||
|
||||
# Q Specific Changes
|
||||
|
||||
## 1.3.?Q - 2024-??-??
|
||||
## 1.4.xQ - 2065-04-xx
|
||||
|
||||
- Bugfix: Properly re-draw status bar after Restore Master on COLDCARD without master seed.
|
||||
- New Feature: Secure Notes & Passwords UX groups
|
||||
- New Feature: Apply Secure Note text, or Secure Note password as BIP-39 passphrase
|
||||
- Bugfix: Teleporting a multisig PSBT file (without signing it first) sent stale data instead of the selected file
|
||||
- Bugfix: Fix export UX message after teleport PSBT import & sign
|
||||
- Bugfix: BIP-21 QR `amount` rendered with wrong decimal scaling on the Payment Address screen (e.g. `amount=1.1` was shown as `1.00000001 BTC`)
|
||||
- Bugfix: QR scan import (Scan Any QR Code, master/temp seed via QR) now surfaces a clean error story on any parser or seed-loading failure (e.g. wordlist-valid but bad-checksum SeedQR) instead of yikesing the menu task
|
||||
- Bugfix: Yikes when showing "QR too big" for a transaction output alone on an output-explorer page
|
||||
- Bugfix: Yikes receiving a malformed full-backup via Key Teleport
|
||||
- Bugfix: Keyboard debounce could leave a key stuck as "pressed" after release when another key was held
|
||||
- Bugfix: Scanner robustness
|
||||
- Avoid holding the QR scanner reset line low; reset is now only pulsed and then left deasserted.
|
||||
- Recover scanner setup failures by retrying configuration and reinitializing on the next scan when needed.
|
||||
- Prevent delayed scanner sleep commands from racing with a newly started scan.
|
||||
- Improve scanner shutdown/recovery after scan cancel or command timeout.
|
||||
|
||||
@ -2,11 +2,45 @@
|
||||
Hash: SHA256
|
||||
|
||||
95eff9e044cdb6b3d00961ae72d450684d5441c6a3661ab550a3c3aa0882e754 README.md
|
||||
97107b5be1c8b65efa4bd36b7d1798e4ed15917861bd2d40784d66302a61d335 Next-ChangeLog.md
|
||||
f6d8a1edf0993cdecea7cdc34f48ce344f249ec0fc2d28fbc4da9ebc163c6148 History-Q.md
|
||||
3e98b0f292b30460e128c3d41e9dd33428524516ce433fe4a3b99132025ca64c History-Mk4.md
|
||||
412597a0e30684400cb61ee04650c13ef9fc3dc16fc2570bd5e33a1dc0085d7a Next-ChangeLog.md
|
||||
72458ab9eb2872d263bf4d3f4ca0fbf0ff9c6186f08d27f13fd600cb511ed2a7 History-Q.md
|
||||
d4891b509915800650a881556cca37604caab7a268afc0b1ed31021cea125891 History-Mk4.md
|
||||
c8ad43b4e3f9d77777026da6d1210c6fc5cfe435bcfcd241c0f67c9392ad7b82 History-Mk3.md
|
||||
7c06aa1d5168e02d928da087f13c74b94e40f52e5eb281af21edcfdf6cabe5ce ChangeLog.md
|
||||
9ebab063b57ff07e5d8df20c266ac94736a6ad0e4c71ad1f1db46ec16b0c94be ChangeLog.md
|
||||
b2fa9f4b9a9778b71cf4b09ad79732192fdb457214f868a3af5234094deea33f 2026-03-25T1408-v6.5.0X-mk-coldcard.dfu
|
||||
f7bed9f1d2d49a35e7c53c8208e73ceaccbee2ab3e7fcd7c020fbd4923140313 2026-03-25T1407-v6.5.0QX-q1-coldcard.dfu
|
||||
2b7b4d95cd5d606b0a32e692db7a27c1d860140c6e919e20ea6672ad6afc3088 2026-03-05T2052-v5.5.0-mk-coldcard.dfu
|
||||
15f26aa0b8fe33e29e338b74acc52d5532922af56bcf486e38085de55c86b82a 2026-03-05T2052-v5.5.0-mk-coldcard-factory.dfu
|
||||
b7ce3ba55ae4bb1e1ebe0090507bcada5a4439e57a19552c78da7ef103bd144c 2026-03-05T2051-v1.4.0Q-q1-coldcard.dfu
|
||||
82c2ed5ee5cf75cc8af0f54839b9a1bc8e4a329174657d61861a6576fb6e31d9 2026-03-05T2051-v1.4.0Q-q1-coldcard-factory.dfu
|
||||
372fa1f82e54f632574c56a695a1ed332464bf029bd733b2db2131a591d8f126 2025-11-25T1618-v6.4.1X-mk4-coldcard.dfu
|
||||
1059560fb598e5e8fd6aed0164aa4cad166552bf8e47a0365e986429c9a15346 2025-11-25T1617-v6.4.1QX-q1-coldcard.dfu
|
||||
f04617b52fc0db6e95cac0dddd9ddd90754219f38b63a26d08c848e208069edb 2025-11-20T1602-v6.4.0X-mk4-coldcard.dfu
|
||||
371f13f3e1a5ef28d14933daf03820f0e51d26ffa96008dd5595da0dfac646cf 2025-11-20T1601-v6.4.0QX-q1-coldcard.dfu
|
||||
7076ae29c509d3120db0fae434c132e6abd3fb79c1a2a2f1383ab3b2acaba27c 2025-11-03T1527-v5.4.5-mk4-coldcard.dfu
|
||||
00a337888ff86bf875bcfdab7a734981bce29a49f94f3df9f932924765848ab0 2025-11-03T1527-v5.4.5-mk4-coldcard-factory.dfu
|
||||
ff6371545943518eb4eb00ba73b6aa3a5ac4e63459621ecec8a300c28c281b3c 2025-11-03T1525-v1.3.5Q-q1-coldcard.dfu
|
||||
0ce02c8e549cb67b682d621b4a628f3fba2c56350a9ab090b9f08532f49e7afa 2025-11-03T1525-v1.3.5Q-q1-coldcard-factory.dfu
|
||||
8a8c94e5f64d0bfe4914a236fb8a779f956989fe8de998133b85b23920f46283 2025-09-30T1238-v5.4.4-mk4-coldcard.dfu
|
||||
0d0aba89027d5127f74b2a2b777a7c592cba12903a3c4c3ce9b0e060c09dddb7 2025-09-30T1238-v5.4.4-mk4-coldcard-factory.dfu
|
||||
bc9918968b67fefe634342c77513c9c354e7821e9ff002c7e5c8c356d7507892 2025-09-30T1237-v1.3.4Q-q1-coldcard.dfu
|
||||
00cb1fc2ef360aacf48ba8c9dd2167b3f5c5f1241ba1b2b17d61ea1b7bff0a45 2025-09-30T1237-v1.3.4Q-q1-coldcard-factory.dfu
|
||||
be166b3bb3ec2259991db998c20c3d44e88eeaa73c2b8114f31cb14cab5e66e6 2025-05-14T1344-v5.4.3-mk4-coldcard.dfu
|
||||
876932d4ea7634d268145d5bf45577c7198c9d60e8a271b5079faba4d4c91acd 2025-05-14T1344-v5.4.3-mk4-coldcard-factory.dfu
|
||||
aaed0b90be5de310c8ac9f2d0cb3a7eea58923a53d349eb4b9ac8a902e5cba4e 2025-05-14T1343-v1.3.3Q-q1-coldcard.dfu
|
||||
9daa2b48abdfa2303a43ee1d0ba3d0d905e7f6286018f44a0dda3c755c46039e 2025-05-14T1343-v1.3.3Q-q1-coldcard-factory.dfu
|
||||
c1202ba30db68a12b882176997f08844da4ec31087ac2f507ea1d4281d2faa9a 2025-04-16T1907-v5.4.2-mk4-coldcard.dfu
|
||||
a82fff91f8da35c122b09d54b01b3d3f3b8825fbc7cddee8e686fd8d57a69285 2025-04-16T1907-v5.4.2-mk4-coldcard-factory.dfu
|
||||
4393c67f8dcbb8890950678f5856d2cca81042b9a447ce149fe624ddfe336a2a 2025-04-16T1906-v1.3.2Q-q1-coldcard.dfu
|
||||
6f2d77f99d61cad9328cc617754aadb3b828150386def41c946d90db8cfb2277 2025-04-16T1906-v1.3.2Q-q1-coldcard-factory.dfu
|
||||
495f37ce7ddaba2e9fc3f03dec582f1646f258a3d0cec5e71c04d127357b2fa3 2025-02-19T1941-v6.3.5X-mk4-coldcard.dfu
|
||||
605ebb5acde19447e5c1d7c8cfd0302c89de5c5870d85f06b185ecab3437f94e 2025-02-19T1939-v6.3.5QX-q1-coldcard.dfu
|
||||
eb750a4f095eacc6133b2c8b38fe0738a22b2496a6cdf423ca865acde8c9bc4e 2025-02-13T1415-v5.4.1-mk4-coldcard.dfu
|
||||
4236453fea241fe044a462a560d8b42df43e560683110306a2714a2ef561eac5 2025-02-13T1415-v5.4.1-mk4-coldcard-factory.dfu
|
||||
2e1aad0a7a3ceb84db34322b54855a0c5496699e46e53606bfa443fcc992adec 2025-02-13T1413-v1.3.1Q-q1-coldcard.dfu
|
||||
e43932d04bf782f7b9ba218b54f29b9cd361b83ac3aadff9722714bca1ab7ee9 2025-02-13T1413-v1.3.1Q-q1-coldcard-factory.dfu
|
||||
681874256bcfca71a3908f1dd6c623804517fdba99a51ed04c73b96119650c13 2024-12-18T1413-v6.3.4X-mk4-coldcard.dfu
|
||||
93ab7615bcedeeff123498c109e5859dae28e58885e29ed86b6f3fd6ba709cce 2024-12-18T1407-v6.3.4QX-q1-coldcard.dfu
|
||||
237cfcb3fdf9217550eae1d9ea6fc828c1c8d09470bd60c9f72f9b00a3bb2d11 2024-09-12T1734-v5.4.0-mk4-coldcard.dfu
|
||||
6d1178f07d543e1777dbbdca41d872b00ca9c40e0c0c1ffb8ef96e19c51daa52 2024-09-12T1734-v5.4.0-mk4-coldcard-factory.dfu
|
||||
d840fa4e83ebc7b0f961f30f68d795bed61271e2314dda4ab0eb0b8bfe7192f4 2024-09-12T1733-v1.3.0Q-q1-coldcard.dfu
|
||||
@ -53,7 +87,6 @@ a49cd00808732c67b359c9f86814ddeafc63a1040823b6c1d2035a870575c9ed 2023-12-21T152
|
||||
06d1048bea43c5d7c72c5e5f395a676620ce884aed0cd152627a86d922e2f3ab 2023-12-19T1444-v5.2.1-mk4-coldcard.dfu
|
||||
3eb9c4b1add88a6fe412d783b8f4b895241a67e423bbacc6a13816a5216a30fe 2023-12-19T1444-v5.2.1-mk4-coldcard-factory.dfu
|
||||
f4457dc44d08cbed9517e6260aa7163ecc254457276d3cdb0c2611af0f49ba9b 2023-10-26T1343-v6.2.1X-mk4-coldcard.dfu
|
||||
1dcfb450f81883afe8f655239f06e238de7bae51e740cd4aa5ae6a0541772ad8 2023-10-26T1343-v6.2.1X-mk4-coldcard-factory.dfu
|
||||
7fbed097d2757b21fde920f4b10f5f50d7e1aeca01ff52186dfde4883af5cace 2023-10-10T1735-v5.2.0-mk4-coldcard.dfu
|
||||
4e3023676be88d6c6480c7f37de302f3a865077f9a2214de9c5a55b24afcba2c 2023-10-10T1735-v5.2.0-mk4-coldcard-factory.dfu
|
||||
fd707f2f69d006c9db84ceacd2a0dde79c3cb71730750e2676af610942898717 2023-09-08T2009-v5.1.4-mk4-coldcard.dfu
|
||||
@ -94,12 +127,12 @@ f05bc8dfed047c0c0abe5ed60621d2d14899b10717221c4af0942d96a1754f33 2021-03-29T192
|
||||
bea27f263b524a66b3ed0a58c16805e98be0d7c3db20c2f7aab3238f2c6a6995 2019-12-19T1623-v3.0.6-coldcard.dfu
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmbjJicACgkQo6MbrVoq
|
||||
WxAnMwf/e2kR1aK6AJiriRa1n3XDomw8ivaUQXUApmK0kawBhVBDLKw5aa3lvTcS
|
||||
dg80wnenzNdE/QxctL+FkaZzKYsKbFpstkBEbZKcgbHVcinypKJJfICrhIBVVyZw
|
||||
wdhJMGOLEyWMysqfaYMtYJQPkg5nIn0rRxn4yWXIeXAQLcFgdlWzVykqfGZW1xYr
|
||||
CcVvxMqufXfc6c5aRFQzBO/YVHiRYzvK1NGDPztJEjXYU3zxnExAZFxk0vgpxvE3
|
||||
CahKfSSTNv54u4CTLxYCdHPRq9OM6yL/w3OUyUQFklCizk2PjrObsJQW4szbbjlx
|
||||
r7+587Pc5cpJCZn73Q0Y5/SWgnqm4g==
|
||||
=/h9F
|
||||
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmnD9ncACgkQo6MbrVoq
|
||||
WxD9RAf+JkP/XVUPMDyfz+79AxBWFNU9r6RuYzXdzX3Z/XCKomZCZDtV7Ak6XlZi
|
||||
GTNfsUNHaPC8WP6smFzYg07NoY2U1fVdY7+qeOi7UXF0hBBDJw7Gsa49P2zmt+DB
|
||||
lfzivQG2n+mT4cM64Z0WF3BYBWmCuDJdctqUAnLJe2p8bh6S8n5hFeKqndRhffNK
|
||||
773amkUrDW3RkHkIuevH4MQlR4ozWBmHzcehFDlTYT8BVLR8gg6hBzBEylxyDJNO
|
||||
Ld4W5SzsT6We0RGX2uOpMERDjkizqT9t5J63drzpuPrUQA8XVQPaOc07vpFHRbbZ
|
||||
BhA61XO8yazNLVvata611pSTikNnDQ==
|
||||
=8Ti0
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -8,31 +8,32 @@ import chains, stash, version
|
||||
from ux import ux_show_story, the_ux, ux_enter_bip32_index
|
||||
from ux import export_prompt_builder, import_export_prompt_decode
|
||||
from menu import MenuSystem, MenuItem
|
||||
from public_constants import AFC_BECH32, AFC_BECH32M, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
|
||||
from public_constants import AFC_BECH32, AFC_BECH32M, AF_P2WPKH, AF_CLASSIC
|
||||
from multisig import MultisigWallet
|
||||
from uasyncio import sleep_ms
|
||||
from uhashlib import sha256
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from glob import settings
|
||||
from auth import write_sig_file
|
||||
from utils import addr_fmt_label, censor_address
|
||||
from msgsign import write_sig_file
|
||||
from charcodes import KEY_QR, KEY_NFC, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_HOME, KEY_LEFT, KEY_RIGHT
|
||||
from charcodes import KEY_CANCEL
|
||||
from utils import show_single_address, problem_file_line, truncate_address
|
||||
|
||||
def truncate_address(addr):
|
||||
# Truncates address to width of screen, replacing middle chars
|
||||
if not version.has_qwerty:
|
||||
# - 16 chars screen width
|
||||
# - but 2 lost at left (menu arrow, corner arrow)
|
||||
# - want to show not truncated on right side
|
||||
return addr[0:6] + '⋯' + addr[-6:]
|
||||
else:
|
||||
# tons of space on Q1
|
||||
return addr[0:12] + '⋯' + addr[-12:]
|
||||
def censor_address(addr):
|
||||
# We don't like to show the user full multisig addresses because we cannot be certain
|
||||
# they could actually be signed. And yet, don't blank too many
|
||||
# spots or else an attacker could grind out a suitable replacement.
|
||||
# 3 chars in the middle hidden by default
|
||||
# censoring can be disabled by msas setting
|
||||
if settings.get("msas", 0):
|
||||
return addr
|
||||
return addr[0:12] + '___' + addr[12+3:]
|
||||
|
||||
class KeypathMenu(MenuSystem):
|
||||
def __init__(self, path=None, nl=0):
|
||||
def __init__(self, path=None, nl=0, ranged=True, done_fn=None):
|
||||
self.prefix = None
|
||||
self.done_fn = done_fn
|
||||
self.ranged = ranged
|
||||
|
||||
if path is None:
|
||||
# Top level menu; useful shortcuts, and special case just "m"
|
||||
@ -41,10 +42,13 @@ class KeypathMenu(MenuSystem):
|
||||
MenuItem("m/44h/⋯", f=self.deeper),
|
||||
MenuItem("m/49h/⋯", f=self.deeper),
|
||||
MenuItem("m/84h/⋯", f=self.deeper),
|
||||
MenuItem("m/0/{idx}", menu=self.done),
|
||||
MenuItem("m/{idx}", menu=self.done),
|
||||
MenuItem("m", f=self.done),
|
||||
]
|
||||
if self.ranged:
|
||||
items += [
|
||||
MenuItem("m/0/{idx}", menu=self.done),
|
||||
MenuItem("m/{idx}", menu=self.done),
|
||||
]
|
||||
else:
|
||||
# drill down one layer: (nl) is the current leaf
|
||||
# - hardened choice first
|
||||
@ -54,11 +58,14 @@ class KeypathMenu(MenuSystem):
|
||||
MenuItem(p+"/⋯", menu=self.deeper),
|
||||
MenuItem(p+"h", menu=self.done),
|
||||
MenuItem(p, menu=self.done),
|
||||
MenuItem(p+"h/0/{idx}", menu=self.done),
|
||||
MenuItem(p+"/0/{idx}", menu=self.done), #useful shortcut?
|
||||
MenuItem(p+"h/{idx}", menu=self.done),
|
||||
MenuItem(p+"/{idx}", menu=self.done),
|
||||
]
|
||||
if self.ranged:
|
||||
items += [
|
||||
MenuItem(p + "h/0/{idx}", menu=self.done),
|
||||
MenuItem(p + "/0/{idx}", menu=self.done), # useful shortcut?
|
||||
MenuItem(p + "h/{idx}", menu=self.done),
|
||||
MenuItem(p + "/{idx}", menu=self.done),
|
||||
]
|
||||
|
||||
# simple consistent truncation when needed
|
||||
max_wide = max(len(mi.label) for mi in items)
|
||||
@ -96,30 +103,33 @@ class KeypathMenu(MenuSystem):
|
||||
if isinstance(top, KeypathMenu):
|
||||
the_ux.pop()
|
||||
continue
|
||||
assert isinstance(top, AddressListMenu)
|
||||
# assert isinstance(top, AddressListMenu), type(top)
|
||||
break
|
||||
|
||||
if self.done_fn:
|
||||
return await self.done_fn(final_path)
|
||||
|
||||
return PickAddrFmtMenu(final_path, top)
|
||||
|
||||
async def deeper(self, _1, _2, item):
|
||||
val = item.arg or item.label
|
||||
assert val.endswith('/⋯')
|
||||
cpath = val[:-2]
|
||||
nl = await ux_enter_bip32_index('%s/' % cpath, unlimited=True)
|
||||
return KeypathMenu(cpath, nl)
|
||||
nl = await ux_enter_bip32_index('%s/' % cpath, unlimited=True, can_cancel=False)
|
||||
return KeypathMenu(cpath, nl, ranged=self.ranged, done_fn=self.done_fn)
|
||||
|
||||
class PickAddrFmtMenu(MenuSystem):
|
||||
def __init__(self, path, parent):
|
||||
self.parent = parent
|
||||
items = [
|
||||
MenuItem(addr_fmt_label(AF_CLASSIC), f=self.done, arg=(path, AF_CLASSIC)),
|
||||
MenuItem(addr_fmt_label(AF_P2WPKH), f=self.done, arg=(path, AF_P2WPKH)),
|
||||
MenuItem(addr_fmt_label(AF_P2WPKH_P2SH), f=self.done, arg=(path, AF_P2WPKH_P2SH)),
|
||||
MenuItem(chains.addr_fmt_label(af), f=self.done, arg=(path, af))
|
||||
for af in chains.SINGLESIG_AF
|
||||
]
|
||||
super().__init__(items)
|
||||
if path.startswith("m/84h"):
|
||||
# below is sensitive to order in chains.SINGLESIG_AF
|
||||
if path.startswith("m/44h"):
|
||||
self.goto_idx(1)
|
||||
if path.startswith("m/49h"):
|
||||
elif path.startswith("m/49h"):
|
||||
self.goto_idx(2)
|
||||
|
||||
async def done(self, _1, _2, item):
|
||||
@ -179,8 +189,7 @@ class AddressListMenu(MenuSystem):
|
||||
# Create list of choices (address_index_0, path, addr_fmt)
|
||||
choices = []
|
||||
for name, path, addr_fmt in chains.CommonDerivations:
|
||||
if '{coin_type}' in path:
|
||||
path = path.replace('{coin_type}', str(chain.b44_cointype))
|
||||
path = path.replace('{coin_type}', str(chain.b44_cointype))
|
||||
|
||||
if self.account_num != 0 and '{account}' not in path:
|
||||
# skip derivations that are not affected by account number
|
||||
@ -189,7 +198,7 @@ class AddressListMenu(MenuSystem):
|
||||
deriv = path.format(account=self.account_num, change=0, idx=self.start)
|
||||
node = sv.derive_path(deriv, register=False)
|
||||
address = chain.address(node, addr_fmt)
|
||||
choices.append( (truncate_address(address), path, addr_fmt) )
|
||||
choices.append((truncate_address(address), path, addr_fmt))
|
||||
|
||||
dis.progress_sofar(len(choices), len(chains.CommonDerivations))
|
||||
|
||||
@ -199,7 +208,7 @@ class AddressListMenu(MenuSystem):
|
||||
indent = ' ↳ ' if version.has_qwerty else '↳'
|
||||
for i, (address, path, addr_fmt) in enumerate(choices):
|
||||
axi = address[-4:] # last 4 address characters
|
||||
items.append(MenuItem(addr_fmt_label(addr_fmt), f=self.pick_single,
|
||||
items.append(MenuItem(chains.addr_fmt_label(addr_fmt), f=self.pick_single,
|
||||
arg=(path, addr_fmt, axi)))
|
||||
items.append(MenuItem(indent+address, f=self.pick_single,
|
||||
arg=(path, addr_fmt, axi)))
|
||||
@ -233,11 +242,15 @@ class AddressListMenu(MenuSystem):
|
||||
self.goto_idx(axi)
|
||||
|
||||
async def change_account(self, *a):
|
||||
self.account_num = await ux_enter_bip32_index('Account Number:') or 0
|
||||
acct = await ux_enter_bip32_index('Account Number:')
|
||||
if acct is None: return
|
||||
self.account_num = acct
|
||||
await self.render()
|
||||
|
||||
async def change_start_idx(self, *a):
|
||||
self.start = await ux_enter_bip32_index("Start index:", unlimited=True)
|
||||
idx = await ux_enter_bip32_index("Start index:", unlimited=True)
|
||||
if idx is None: return
|
||||
self.start = idx
|
||||
await self.render()
|
||||
|
||||
async def pick_single(self, _1, _2, item):
|
||||
@ -279,6 +292,7 @@ Press (3) if you really understand and accept these risks.
|
||||
from wallet import MAX_BIP32_IDX
|
||||
|
||||
start = self.start
|
||||
allow_qr = (not ms_wallet) or settings.get("msas", 0)
|
||||
|
||||
def make_msg(change=0):
|
||||
# Build message and CTA about export, plus the actual addresses.
|
||||
@ -293,20 +307,21 @@ Press (3) if you really understand and accept these risks.
|
||||
dis.fullscreen('Wait...')
|
||||
|
||||
if ms_wallet:
|
||||
# IMPORTANT safety feature: never show complete address
|
||||
# IMPORTANT safety feature: do not show complete address unless user opt-in
|
||||
# but show enough they can verify addrs shown elsewhere.
|
||||
# - makes a redeem script
|
||||
# - converts into addr
|
||||
# - assumes 0/0 is first address.
|
||||
for idx, addr, paths, script in ms_wallet.yield_addresses(start, n, change):
|
||||
addrs.append(censor_address(addr))
|
||||
addr = censor_address(addr)
|
||||
addrs.append(addr)
|
||||
|
||||
if idx == 0 and ms_wallet.N <= 4:
|
||||
msg += '\n'.join(paths) + '\n =>\n'
|
||||
else:
|
||||
msg += '⋯/%d/%d =>\n' % (change, idx)
|
||||
|
||||
msg += truncate_address(addr) + '\n\n'
|
||||
msg += show_single_address(addr) + '\n\n'
|
||||
dis.progress_sofar(idx-start+1, n)
|
||||
|
||||
else:
|
||||
@ -319,14 +334,16 @@ Press (3) if you really understand and accept these risks.
|
||||
|
||||
for idx, addr, deriv in main.yield_addresses(start, n, change if allow_change else None):
|
||||
addrs.append(addr)
|
||||
msg += "%s =>\n%s\n\n" % (deriv, addr)
|
||||
msg += "%s =>\n%s\n\n" % (deriv, show_single_address(addr))
|
||||
dis.progress_sofar(idx-start+1, n or 1)
|
||||
|
||||
# export options
|
||||
k0 = 'to show change addresses' if allow_change and change == 0 else None
|
||||
export_msg, escape = export_prompt_builder('address summary file',
|
||||
no_qr=bool(ms_wallet), key0=k0,
|
||||
force_prompt=True)
|
||||
export_msg, escape = export_prompt_builder(
|
||||
'address summary file',
|
||||
no_qr=not allow_qr,
|
||||
key0=k0, force_prompt=True
|
||||
)
|
||||
if version.has_qwerty:
|
||||
escape += KEY_LEFT+KEY_RIGHT+KEY_HOME+KEY_PAGE_UP+KEY_PAGE_DOWN
|
||||
else:
|
||||
@ -339,6 +356,9 @@ Press (3) if you really understand and accept these risks.
|
||||
msg += '\n\n'
|
||||
if n:
|
||||
msg += "Press RIGHT to see next group, LEFT to go back. X to quit."
|
||||
else:
|
||||
escape += "0"
|
||||
msg += " Press (0) to sign message with this key."
|
||||
|
||||
return msg, addrs, escape
|
||||
|
||||
@ -364,14 +384,11 @@ Press (3) if you really understand and accept these risks.
|
||||
# continue on same screen in case they want to write to multiple cards
|
||||
|
||||
elif choice == KEY_QR:
|
||||
# switch into a mode that shows them as QR codes
|
||||
if ms_wallet:
|
||||
# requires not multisig
|
||||
continue
|
||||
|
||||
from ux import show_qr_codes
|
||||
is_alnum = bool(addr_fmt & (AFC_BECH32 | AFC_BECH32M))
|
||||
await show_qr_codes(addrs, is_alnum, start)
|
||||
if allow_qr:
|
||||
addr_fmt = addr_fmt or ms_wallet.addr_fmt
|
||||
is_alnum = bool(addr_fmt & (AFC_BECH32 | AFC_BECH32M))
|
||||
await show_qr_codes(addrs, is_alnum, start, is_addrs=True)
|
||||
|
||||
continue
|
||||
|
||||
@ -384,8 +401,15 @@ Press (3) if you really understand and accept these risks.
|
||||
|
||||
continue
|
||||
|
||||
elif choice == '0' and allow_change:
|
||||
change = 1
|
||||
elif choice == '0':
|
||||
if allow_change:
|
||||
change = 1
|
||||
else:
|
||||
# only custom path sets allow_change to False
|
||||
# msg sign
|
||||
from msgsign import sign_with_own_address
|
||||
await sign_with_own_address(path, addr_fmt)
|
||||
|
||||
elif n is None:
|
||||
# makes no sense to do any of below, showing just single address
|
||||
continue
|
||||
@ -421,14 +445,12 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
|
||||
+ ['Derivation (%d of %d)' % (i+1, ms_wallet.N) for i in range(ms_wallet.N)]
|
||||
) + '"\n'
|
||||
|
||||
if (start == 0) and (n > 100) and change in (0, 1):
|
||||
saver = OWNERSHIP.saver(ms_wallet, change, start)
|
||||
else:
|
||||
saver = None
|
||||
# saver will be None if we don't think it worth saving these addresses
|
||||
saver = OWNERSHIP.saver(ms_wallet, change, start, n)
|
||||
|
||||
for (idx, addr, derivs, script) in ms_wallet.yield_addresses(start, n, change_idx=change):
|
||||
if saver:
|
||||
saver(addr)
|
||||
saver(addr, idx)
|
||||
|
||||
# policy choice: never provide a complete multisig address to user.
|
||||
addr = censor_address(addr)
|
||||
@ -440,7 +462,7 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
|
||||
yield ln
|
||||
|
||||
if saver:
|
||||
saver(None) # close file
|
||||
saver(None, 0) # close cache file
|
||||
|
||||
return
|
||||
|
||||
@ -448,26 +470,24 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
|
||||
from wallet import MasterSingleSigWallet
|
||||
main = MasterSingleSigWallet(addr_fmt, path, account_num)
|
||||
|
||||
if n and (start == 0) and (n > 100) and change in (0, 1):
|
||||
saver = OWNERSHIP.saver(main, change, start)
|
||||
else:
|
||||
saver = None
|
||||
# saver will be None if we don't think it worth saving these addresses
|
||||
saver = OWNERSHIP.saver(main, change, start, n)
|
||||
|
||||
yield '"Index","Payment Address","Derivation"\n'
|
||||
for (idx, addr, deriv) in main.yield_addresses(start, n, change_idx=change):
|
||||
if saver:
|
||||
saver(addr)
|
||||
saver(addr, idx)
|
||||
|
||||
yield '%d,"%s","%s"\n' % (idx, addr, deriv)
|
||||
|
||||
if saver:
|
||||
saver(None) # close
|
||||
saver(None, 0) # close cache file
|
||||
|
||||
async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num,
|
||||
start=0, count=250, change=0, **save_opts):
|
||||
|
||||
# write addresses into a text file on the MicroSD/VirtDisk
|
||||
from glob import dis
|
||||
from glob import dis, settings
|
||||
from files import CardSlot, CardMissingError, needs_microsd
|
||||
|
||||
# simple: always set number of addresses.
|
||||
@ -479,7 +499,6 @@ async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num,
|
||||
# generator function
|
||||
body = generate_address_csv(path, addr_fmt, ms_wallet, account_num, count,
|
||||
start=start, change=change)
|
||||
|
||||
# pick filename and write
|
||||
try:
|
||||
with CardSlot(**save_opts) as card:
|
||||
@ -490,28 +509,27 @@ async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num,
|
||||
for idx, part in enumerate(body):
|
||||
ep = part.encode()
|
||||
fd.write(ep)
|
||||
if not ms_wallet:
|
||||
h.update(ep)
|
||||
|
||||
h.update(ep)
|
||||
dis.progress_sofar(idx, count or 1)
|
||||
|
||||
sig_nice = None
|
||||
if not ms_wallet:
|
||||
if ms_wallet:
|
||||
# sign with my key at the same path as first address of export
|
||||
addr_fmt = AF_CLASSIC
|
||||
derive = ms_wallet.get_my_deriv(settings.get('xfp'))
|
||||
derive += "/%d/%d" % (change, start)
|
||||
else:
|
||||
derive = path.format(account=account_num, change=change, idx=start) # first addr
|
||||
sig_nice = write_sig_file([(h.digest(), fname)], derive, addr_fmt)
|
||||
|
||||
sig_nice = write_sig_file([(h.digest(), fname)], derive, addr_fmt)
|
||||
|
||||
await ux_show_story("Address summary file written:\n\n%s\n\nAddress"
|
||||
" signature file written:\n\n%s" % (nice, sig_nice))
|
||||
|
||||
except CardMissingError:
|
||||
await needs_microsd()
|
||||
return
|
||||
except Exception as e:
|
||||
from utils import problem_file_line
|
||||
await ux_show_story('Failed to write!\n\n\n%s\n%s' % (e, problem_file_line(e)))
|
||||
return
|
||||
await ux_show_story('Failed to write!\n\n%s\n%s' % (e, problem_file_line(e)))
|
||||
|
||||
msg = '''Address summary file written:\n\n%s''' % nice
|
||||
if sig_nice:
|
||||
msg += "\n\nAddress signature file written:\n\n%s" % sig_nice
|
||||
await ux_show_story(msg)
|
||||
|
||||
async def address_explore(*a):
|
||||
# explore addresses based on derivation path chosen
|
||||
|
||||
1612
shared/auth.py
1612
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,16 +45,11 @@ def render_backup_contents(bypass_tmp=False):
|
||||
|
||||
COMMENT('Private key details: ' + chain.name)
|
||||
|
||||
with stash.SensitiveValues(bypass_tmp=bypass_tmp) as sv:
|
||||
if sv.deltamode:
|
||||
# die rather than give up our secrets
|
||||
import callgate
|
||||
callgate.fast_wipe()
|
||||
|
||||
with stash.SensitiveValues(bypass_tmp=bypass_tmp, enforce_delta=True) as sv:
|
||||
if sv.mode == 'words':
|
||||
ADD('mnemonic', bip39.b2a_words(sv.raw))
|
||||
|
||||
if sv.mode == 'master':
|
||||
elif sv.mode == 'master':
|
||||
ADD('bip32_master_key', b2a_hex(sv.raw))
|
||||
|
||||
ADD('chain', chain.ctype)
|
||||
@ -79,7 +76,12 @@ def render_backup_contents(bypass_tmp=False):
|
||||
current_tmp = pa.tmp_value[:]
|
||||
pa.tmp_value = None
|
||||
# we also need correct settings from main seed
|
||||
nv = stash.SecretStash.encode(seed_phrase=sv.raw)
|
||||
if sv.mode == 'words':
|
||||
nv = stash.SecretStash.encode(seed_phrase=sv.raw)
|
||||
else:
|
||||
assert sv.mode == "xprv"
|
||||
nv = stash.SecretStash.encode(xprv=sv.node)
|
||||
|
||||
settings.set_key(nv)
|
||||
settings.load()
|
||||
stash.blank_object(nv)
|
||||
@ -103,6 +105,9 @@ def render_backup_contents(bypass_tmp=False):
|
||||
if k == 'bkpw': continue # confusing/circular
|
||||
if k == 'sd2fa': continue # do NOT backup SD 2FA (card can be lost or damaged)
|
||||
if k == 'words': continue # words length is recalculated from secret
|
||||
if k == 'ccc': continue # not supported, security issue
|
||||
if k == 'ktrx': continue # not useful after the fact
|
||||
if k == 'lfr': continue # temporary error msg value
|
||||
if k == 'seedvault' and not v: continue
|
||||
if k == 'seeds' and not v: continue
|
||||
ADD('setting.' + k, v)
|
||||
@ -123,14 +128,14 @@ def render_backup_contents(bypass_tmp=False):
|
||||
|
||||
return rv.getvalue()
|
||||
|
||||
def extract_raw_secret(chain, vals):
|
||||
def extract_raw_secret(vals):
|
||||
# step1: the private key
|
||||
# - prefer raw_secret over other values
|
||||
# - TODO: fail back to other values
|
||||
assert 'raw_secret' in vals
|
||||
rs = vals.pop('raw_secret')
|
||||
|
||||
raw = pad_raw_secret(rs)
|
||||
raw = deserialize_secret(rs)
|
||||
|
||||
# check we can decode this right (might be different firmare)
|
||||
opmode, bits, node = stash.SecretStash.decode(raw)
|
||||
@ -138,22 +143,23 @@ def extract_raw_secret(chain, vals):
|
||||
|
||||
# verify against xprv value (if we have it)
|
||||
if 'xprv' in vals:
|
||||
check_xprv = chain.serialize_private(node)
|
||||
check_xprv = chains.get_chain(vals.get('chain', 'BTC')).serialize_private(node)
|
||||
assert check_xprv == vals['xprv'], 'xprv mismatch'
|
||||
|
||||
return raw
|
||||
return raw, node
|
||||
|
||||
def extract_long_secret(vals):
|
||||
ls = None
|
||||
if ('long_secret' in vals) and version.has_608:
|
||||
try:
|
||||
ls = a2b_hex(vals.pop('long_secret'))
|
||||
except Exception as exc:
|
||||
sys.print_exception(exc)
|
||||
except:
|
||||
# sys.print_exception(exc)
|
||||
# but keep going.
|
||||
pass
|
||||
return ls
|
||||
|
||||
def restore_from_dict_ll(vals):
|
||||
def restore_from_dict_ll(vals, raw):
|
||||
# Restore from a dict of values. Already JSON decoded.
|
||||
# Need a Reboot on success, return string on failure
|
||||
# - low-level version, factored out for better testing
|
||||
@ -164,12 +170,6 @@ def restore_from_dict_ll(vals):
|
||||
#print("Restoring from: %r" % vals)
|
||||
chain = chains.get_chain(vals.get('chain', 'BTC'))
|
||||
|
||||
try:
|
||||
raw = extract_raw_secret(chain, vals)
|
||||
except Exception as e:
|
||||
return ('Unable to decode raw_secret and '
|
||||
'restore the seed value!\n\n\n'+str(e)), None
|
||||
|
||||
dis.fullscreen("Saving...")
|
||||
dis.progress_bar_show(.1)
|
||||
|
||||
@ -188,9 +188,7 @@ def restore_from_dict_ll(vals):
|
||||
if ls is not None:
|
||||
try:
|
||||
pa.ls_change(ls)
|
||||
except Exception as exc:
|
||||
sys.print_exception(exc)
|
||||
# but keep going
|
||||
except: pass # but keep going
|
||||
pb = .70
|
||||
dis.progress_bar_show(pb)
|
||||
|
||||
@ -208,19 +206,30 @@ def restore_from_dict_ll(vals):
|
||||
|
||||
k = key[8:]
|
||||
|
||||
if k == 'bkpw':
|
||||
# never import a cached backup password from a backup file.
|
||||
# write-side (render_backup_contents) strips bkpw, so a present
|
||||
# value means a tampered/crafted file trying to fixate the
|
||||
# password used for all FUTURE backups - drop it.
|
||||
continue
|
||||
|
||||
if k == 'sd2fa':
|
||||
# do NOT restore sd2fa as SD card can be lost or damaged
|
||||
# new version of firmware 5.1.3+ will not back sd2fa
|
||||
# old backups need this to function properly
|
||||
continue
|
||||
|
||||
if k == 'ccc':
|
||||
# CCC feature cannot be backed-up nor restored for security reasons
|
||||
# (would allow replay attacks)
|
||||
continue
|
||||
|
||||
if k == 'tp':
|
||||
# restore trick pins, which may involve many ops
|
||||
from trick_pins import tp
|
||||
try:
|
||||
tp.restore_backup(vals[key])
|
||||
except Exception as exc:
|
||||
sys.print_exception(exc)
|
||||
except: pass
|
||||
|
||||
# continue as `tp.restore_backup` handles
|
||||
# saving into settings
|
||||
@ -261,36 +270,50 @@ def restore_from_dict_ll(vals):
|
||||
|
||||
return None, need_ftux
|
||||
|
||||
async def restore_tmp_from_dict_ll(vals):
|
||||
def text_bk_parser(contents):
|
||||
# given a (binary encoded) text file, decode into a dict of values
|
||||
# - use json rules to decode the "value" sides
|
||||
vals = {}
|
||||
for line in contents.decode().split('\n'):
|
||||
if not line: continue
|
||||
if line[0] == '#': continue
|
||||
|
||||
try:
|
||||
k,v = line.split(' = ', 1)
|
||||
#print("%s = %s" % (k, v))
|
||||
|
||||
vals[k] = ujson.loads(v)
|
||||
except:
|
||||
print("unable to decode line: %r" % line)
|
||||
# but keep going!
|
||||
|
||||
return vals
|
||||
|
||||
async def restore_tmp_from_dict_ll(vals, raw):
|
||||
from glob import dis
|
||||
|
||||
chain = chains.get_chain(vals.get('chain', 'BTC'))
|
||||
try:
|
||||
raw = extract_raw_secret(chain, vals)
|
||||
except Exception as e:
|
||||
return ('Unable to decode raw_secret and '
|
||||
'restore the seed value!\n\n\n' + str(e))
|
||||
|
||||
dis.fullscreen("Applying...")
|
||||
from seed import set_ephemeral_seed
|
||||
from actions import goto_top_menu
|
||||
|
||||
await set_ephemeral_seed(raw, chain, meta="Coldcard Backup")
|
||||
await set_ephemeral_seed(raw, chain, origin="Coldcard Backup")
|
||||
for k, v in vals.items():
|
||||
if not k[:8] == "setting.":
|
||||
continue
|
||||
key = k[8:]
|
||||
if key in ["multisig"]:
|
||||
# whitelist
|
||||
settings.set(k, v)
|
||||
settings.set(key, v)
|
||||
|
||||
goto_top_menu()
|
||||
|
||||
async def restore_from_dict(vals):
|
||||
async def restore_from_dict(vals, raw):
|
||||
# Restore from a dict of values. Already JSON decoded (ie. dict object).
|
||||
# Need a Reboot on success, return string on failure
|
||||
|
||||
prob, need_ftux = restore_from_dict_ll(vals)
|
||||
prob, need_ftux = restore_from_dict_ll(vals, raw)
|
||||
if prob: return prob
|
||||
|
||||
if need_ftux:
|
||||
@ -309,7 +332,7 @@ async def restore_from_dict(vals):
|
||||
async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False):
|
||||
from stash import bip39_passphrase
|
||||
|
||||
words = None
|
||||
pwd = None
|
||||
skip_quiz = False
|
||||
bypass_tmp = False
|
||||
|
||||
@ -329,35 +352,49 @@ async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False):
|
||||
"so backup will be of that seed."):
|
||||
return
|
||||
|
||||
stored_words = settings.get('bkpw', None)
|
||||
# first check if bkpw already defined on tmp seed settings
|
||||
stored_pwd = None
|
||||
master_pwd = settings.master_get("bkpw", None)
|
||||
if pa.tmp_value:
|
||||
stored_pwd = settings.get('bkpw', None)
|
||||
|
||||
if stored_words:
|
||||
stored_words = stored_words.split()
|
||||
ch = await ux_show_story("Use same backup file password as last time?\n\n"
|
||||
" 1: %s\n ...\n%d: %s"
|
||||
% (stored_words[0], len(stored_words), stored_words[-1]), sensitive=True)
|
||||
if not stored_pwd and master_pwd:
|
||||
stored_pwd = master_pwd
|
||||
|
||||
if stored_pwd:
|
||||
# we can have words or other type of password here
|
||||
split_pwd = stored_pwd.split()
|
||||
if len(split_pwd) == num_pw_words: # weak
|
||||
hint = " 1: %s\n ...\n%d: %s" % (split_pwd[0], len(split_pwd), split_pwd[-1])
|
||||
else:
|
||||
hint = " %s...%s" % (stored_pwd[0], stored_pwd[-1])
|
||||
|
||||
ch = await ux_show_story("Use same backup file password as last time?\n\n" + hint,
|
||||
sensitive=True)
|
||||
|
||||
if ch == 'y':
|
||||
words = stored_words
|
||||
pwd = stored_pwd # string, not list
|
||||
skip_quiz = True
|
||||
|
||||
if not words:
|
||||
if not pwd:
|
||||
# Pick a password: like bip39 but no checksum word
|
||||
#
|
||||
b = bytearray(32)
|
||||
while 1:
|
||||
ckcc.rng_bytes(b)
|
||||
words = bip39.b2a_words(b).split(' ')[0:num_pw_words]
|
||||
pwd = bip39.b2a_words(b).rsplit(' ', num_pw_words)[0]
|
||||
|
||||
ch = await seed.show_words(words,
|
||||
prompt="Record this (%d word) backup file password:\n", escape='6')
|
||||
ch = await seed.show_words(
|
||||
prompt="Record this (%d word) backup file password:\n" % num_pw_words,
|
||||
words=pwd.split(" "), escape='6'
|
||||
)
|
||||
|
||||
if ch == '6' and not write_sflash:
|
||||
if (ch == '6') and not write_sflash:
|
||||
# Secret feature: plaintext mode
|
||||
# - only safe for people living in faraday cages inside locked vaults.
|
||||
if await ux_confirm("The file will **NOT** be encrypted and "
|
||||
"anyone who finds the file will get all of your money for free!"):
|
||||
words = []
|
||||
pwd = []
|
||||
fname_pattern = 'backup.txt'
|
||||
break
|
||||
continue
|
||||
@ -367,43 +404,43 @@ async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False):
|
||||
|
||||
break
|
||||
|
||||
if words and not skip_quiz:
|
||||
if pwd and not skip_quiz:
|
||||
# quiz them, but be nice and do a shorter test.
|
||||
ch = await seed.word_quiz(words, limited=(num_pw_words//3))
|
||||
ch = await seed.word_quiz(pwd.split(" "), limited=(num_pw_words//3))
|
||||
if ch == 'x': return
|
||||
|
||||
if words and words != stored_words:
|
||||
if pwd and pwd != stored_pwd:
|
||||
ch = await ux_show_story("Would you like to use these same words next time you perform a backup?"
|
||||
" Press (1) to save them into this Coldcard for next time.", escape='1')
|
||||
|
||||
if ch == '1':
|
||||
settings.put('bkpw', ' '.join(words))
|
||||
settings.save()
|
||||
elif stored_words:
|
||||
settings.remove_key('bkpw')
|
||||
settings.set('bkpw', pwd) # if on tmp save to tmp, do not update master
|
||||
settings.save()
|
||||
# stop droping bkpw just because someone decided to use differrent password
|
||||
# elif stored_words:
|
||||
# settings.remove_key('bkpw')
|
||||
# settings.save()
|
||||
|
||||
return await write_complete_backup(words, fname_pattern, write_sflash=write_sflash,
|
||||
return await write_complete_backup(pwd, fname_pattern, write_sflash=write_sflash,
|
||||
bypass_tmp=bypass_tmp)
|
||||
|
||||
async def write_complete_backup(words, fname_pattern, write_sflash=False,
|
||||
async def write_complete_backup(pwd, fname_pattern, write_sflash=False,
|
||||
allow_copies=True, bypass_tmp=False):
|
||||
# Just do the writing
|
||||
from glob import dis
|
||||
from files import CardSlot
|
||||
|
||||
# Show progress:
|
||||
dis.fullscreen('Encrypting...' if words else 'Generating...')
|
||||
dis.fullscreen('Encrypting...' if pwd else 'Generating...')
|
||||
body = render_backup_contents(bypass_tmp=bypass_tmp).encode()
|
||||
|
||||
gc.collect()
|
||||
|
||||
if words:
|
||||
if pwd:
|
||||
# NOTE: Takes a few seconds to do the key-streching, but little actual
|
||||
# time to do the encryption.
|
||||
|
||||
pw = ' '.join(words)
|
||||
zz = compat7z.Builder(password=pw, progress_fcn=dis.progress_bar_show)
|
||||
zz = compat7z.Builder(password=pwd, progress_fcn=dis.progress_bar_show)
|
||||
zz.add_data(body)
|
||||
|
||||
# pick random filename, but ending in .txt
|
||||
@ -422,8 +459,6 @@ async def write_complete_backup(words, fname_pattern, write_sflash=False,
|
||||
|
||||
if write_sflash:
|
||||
# for use over USB and unit testing: commit file into PSRAM
|
||||
from sffile import SFFile
|
||||
|
||||
with SFFile(0, max_size=MAX_BACKUP_FILE_SIZE, message='Saving...') as fd:
|
||||
if zz:
|
||||
fd.write(hdr)
|
||||
@ -452,11 +487,9 @@ async def write_complete_backup(words, fname_pattern, write_sflash=False,
|
||||
|
||||
except Exception as e:
|
||||
# includes CardMissingError
|
||||
import sys
|
||||
sys.print_exception(e)
|
||||
# catch any error
|
||||
ch = await ux_show_story('Failed to write! Please insert formated MicroSD card, '
|
||||
'and press %s to try again.\n\nX to cancel.\n\n\n' % OK +str(e))
|
||||
'and press %s to try again.\n\n%s to cancel.\n\n\n%s' % (OK, X, e))
|
||||
if ch == 'x': break
|
||||
continue
|
||||
|
||||
@ -519,106 +552,168 @@ async def verify_backup_file(fname):
|
||||
# might be already closed on vdisk case due to filesystem unmount/mount
|
||||
pass
|
||||
|
||||
await ux_show_story("Backup file CRC checks out okay.\n\nPlease note this is only a check against accidental truncation and similar. Targeted modifications can still pass this test.")
|
||||
await ux_show_story("Backup file CRC checks out okay.\n\n"
|
||||
"Please note this is only a check against accidental truncation and similar."
|
||||
" Targeted modifications can still pass this test. You may further verify"
|
||||
" this backup file by starting the normal restore process (Restore Backup)"
|
||||
" and aborting it once decryption has been achieved.")
|
||||
|
||||
|
||||
async def restore_complete(fname_or_fd, temporary=False):
|
||||
async def restore_complete(fname_or_fd, temporary=False, words=True, usb=False):
|
||||
from ux import the_ux
|
||||
|
||||
async def done(words):
|
||||
# remove all pw-picking from menu stack
|
||||
seed.WordNestMenu.pop_all()
|
||||
if not version.has_qwerty and words:
|
||||
seed.WordNestMenu.pop_all()
|
||||
|
||||
prob = await restore_complete_doit(fname_or_fd, words,
|
||||
temporary=temporary)
|
||||
|
||||
if prob:
|
||||
await ux_show_story(prob, title='FAILED')
|
||||
|
||||
if version.has_qwerty:
|
||||
from ux_q1 import seed_word_entry
|
||||
return await seed_word_entry('Enter Password:', num_pw_words,
|
||||
done_cb=done, has_checksum=False)
|
||||
# give them a menu to pick from, and start picking
|
||||
m = seed.WordNestMenu(num_words=num_pw_words, has_checksum=False, done_cb=done)
|
||||
if words:
|
||||
if version.has_qwerty:
|
||||
from ux_q1 import seed_word_entry, CHARS_W
|
||||
|
||||
the_ux.push(m)
|
||||
basename = None
|
||||
if isinstance(fname_or_fd, str):
|
||||
basename = fname_or_fd.split('/')[-1]
|
||||
if len(basename) > CHARS_W:
|
||||
basename = basename[:16] + "⋯" + basename[-16:]
|
||||
|
||||
async def restore_complete_doit(fname_or_fd, words, file_cleanup=None, temporary=False):
|
||||
return await seed_word_entry("Enter Password%s:" % (" for" if basename else ""),
|
||||
num_pw_words, done_cb=done, has_checksum=False,
|
||||
line2=basename)
|
||||
|
||||
# give them a menu to pick from, and start picking
|
||||
if usb:
|
||||
# we're not originating from a menu
|
||||
words = await seed.WordNestMenu.get_n_words(num_pw_words)
|
||||
if len(words) != num_pw_words:
|
||||
seed.WordNestMenu.pop_all()
|
||||
return
|
||||
|
||||
await done(words)
|
||||
else:
|
||||
m = seed.WordNestMenu(num_words=num_pw_words, has_checksum=False, done_cb=done)
|
||||
the_ux.push(m)
|
||||
|
||||
else:
|
||||
pwd = [] # cleartext if words=None
|
||||
if words is False:
|
||||
ipw = await ux_input_text("", prompt="Your Backup Password",
|
||||
min_len=bkpw_min_len, max_len=128)
|
||||
if not ipw: return
|
||||
pwd.append(ipw)
|
||||
|
||||
await done(pwd)
|
||||
|
||||
|
||||
def check_and_decrypt(fd, password):
|
||||
try:
|
||||
compat7z.check_file_headers(fd)
|
||||
except Exception as e:
|
||||
raise RuntimeError('Unable to read backup file.'
|
||||
' Has it been touched?\n\nError: '+str(e))
|
||||
|
||||
from glob import dis
|
||||
dis.fullscreen("Decrypting...")
|
||||
try:
|
||||
zz = compat7z.Builder()
|
||||
fname, contents = zz.read_file(fd, password, MAX_BACKUP_FILE_SIZE,
|
||||
progress_fcn=dis.progress_bar_show)
|
||||
|
||||
# simple quick sanity checks
|
||||
assert fname.endswith('.txt') # was == 'ckcc-backup.txt'
|
||||
assert contents[0:1] == b'#' and contents[-1:] == b'\n'
|
||||
return contents
|
||||
|
||||
except Exception as e:
|
||||
# assume everything here is "password wrong" errors
|
||||
raise RuntimeError('Unable to decrypt backup file. Incorrect password?'
|
||||
'\n\nTried:\n\n' + password)
|
||||
|
||||
|
||||
async def restore_complete_doit(fname_or_fd, words, file_cleanup=None, temporary=False,
|
||||
ux_confirm=True):
|
||||
# Open file, read it, maybe decrypt it; return string if any error
|
||||
# - some errors will be shown, None return in that case
|
||||
# - no return if successful (due to reboot)
|
||||
from glob import dis
|
||||
from files import CardSlot, CardMissingError, needs_microsd
|
||||
|
||||
# build password
|
||||
password = ' '.join(words)
|
||||
|
||||
prob = None
|
||||
|
||||
try:
|
||||
with CardSlot(readonly=True) as card:
|
||||
# filename already picked, taste it and maybe consider using its data.
|
||||
try:
|
||||
fd = open(fname_or_fd, 'rb') if isinstance(fname_or_fd, str) else fname_or_fd
|
||||
except:
|
||||
return 'Unable to open backup file.\n\n' + str(fname_or_fd)
|
||||
if isinstance(fname_or_fd, int):
|
||||
# USB restore - backup is already in PSRAM, fname of fd is length
|
||||
# TXN_INPUT_OFFSET = 0
|
||||
with SFFile(0, length=fname_or_fd) as fd:
|
||||
if not words:
|
||||
contents = fd.read(fname_or_fd)
|
||||
else:
|
||||
# read full size, then decrypt
|
||||
fd = BytesIO(fd.read(fname_or_fd))
|
||||
try:
|
||||
contents = check_and_decrypt(fd, password)
|
||||
except RuntimeError as e:
|
||||
return str(e)
|
||||
else:
|
||||
try:
|
||||
with CardSlot(readonly=True) as card:
|
||||
# filename already picked, taste it and maybe consider using its data.
|
||||
try:
|
||||
fd = open(fname_or_fd, 'rb')
|
||||
except:
|
||||
return 'Unable to open backup file.\n\n' + str(fname_or_fd)
|
||||
|
||||
try:
|
||||
if not words:
|
||||
contents = fd.read()
|
||||
else:
|
||||
try:
|
||||
compat7z.check_file_headers(fd)
|
||||
except Exception as e:
|
||||
return 'Unable to read backup file. Has it been touched?\n\nError: ' \
|
||||
+ str(e)
|
||||
try:
|
||||
if words:
|
||||
contents = check_and_decrypt(fd, password)
|
||||
else:
|
||||
contents = fd.read()
|
||||
|
||||
dis.fullscreen("Decrypting...")
|
||||
try:
|
||||
zz = compat7z.Builder()
|
||||
fname, contents = zz.read_file(fd, password, MAX_BACKUP_FILE_SIZE,
|
||||
progress_fcn=dis.progress_bar_show)
|
||||
|
||||
# simple quick sanity checks
|
||||
assert fname.endswith('.txt') # was == 'ckcc-backup.txt'
|
||||
assert contents[0:1] == b'#' and contents[-1:] == b'\n'
|
||||
|
||||
except Exception as e:
|
||||
# assume everything here is "password wrong" errors
|
||||
#print("pw wrong? %s" % e)
|
||||
|
||||
return ('Unable to decrypt backup file. Incorrect password?'
|
||||
'\n\nTried:\n\n' + password)
|
||||
finally:
|
||||
fd.close()
|
||||
except RuntimeError as e:
|
||||
return str(e)
|
||||
finally:
|
||||
fd.close()
|
||||
|
||||
if file_cleanup:
|
||||
file_cleanup(fname_or_fd)
|
||||
|
||||
except CardMissingError:
|
||||
await needs_microsd()
|
||||
return
|
||||
except CardMissingError:
|
||||
await needs_microsd()
|
||||
return
|
||||
|
||||
vals = {}
|
||||
for line in contents.decode().split('\n'):
|
||||
if not line: continue
|
||||
if line[0] == '#': continue
|
||||
try:
|
||||
vals = text_bk_parser(contents)
|
||||
except:
|
||||
return "Invalid backup file."
|
||||
|
||||
try:
|
||||
k,v = line.split(' = ', 1)
|
||||
#print("%s = %s" % (k, v))
|
||||
try:
|
||||
raw, node = extract_raw_secret(vals)
|
||||
except Exception as e:
|
||||
return ('Unable to decode raw_secret and '
|
||||
'restore the seed value!\n\n\n'+str(e))
|
||||
|
||||
vals[k] = ujson.loads(v)
|
||||
except:
|
||||
print("unable to decode line: %r" % line)
|
||||
# but keep going!
|
||||
if ux_confirm:
|
||||
# check master fingerprint from raw secret that is actually being loaded
|
||||
# master extended public keys can be wrong & is unverified
|
||||
xfp_str = xfp2str(swab32(node.my_fp()))
|
||||
ch = await ux_show_story("Above is the master fingerprint of the seed stored in the backup."
|
||||
" Press %s to continue, and load backup as %s seed. Press %s"
|
||||
" to abort." % (OK, "temporary" if temporary else "master", X),
|
||||
title="["+xfp_str+"]")
|
||||
if ch != "y":
|
||||
await ux_dramatic_pause('Aborted.', 2)
|
||||
return
|
||||
|
||||
# this leads to reboot if it works, else errors shown, etc.
|
||||
if temporary:
|
||||
return await restore_tmp_from_dict_ll(vals)
|
||||
return await restore_tmp_from_dict_ll(vals, raw)
|
||||
else:
|
||||
return await restore_from_dict(vals)
|
||||
return await restore_from_dict(vals, raw)
|
||||
|
||||
async def clone_start(*a):
|
||||
# Begins cloning process, on target device.
|
||||
@ -701,8 +796,9 @@ back and press %s to complete clone process.''' % OK)
|
||||
uos.remove(fname) # ccbk-start.json
|
||||
|
||||
# this will reset in successful case, no return (but delme is called)
|
||||
prob = await restore_complete_doit(incoming, words, file_cleanup=delme)
|
||||
|
||||
# no need to ask for UX confirmation during clone - as user can see what is loaded on source CC
|
||||
prob = await restore_complete_doit(incoming, words, file_cleanup=delme,
|
||||
ux_confirm=False)
|
||||
if prob:
|
||||
await ux_show_story(prob, title='FAILED')
|
||||
|
||||
@ -742,11 +838,9 @@ async def clone_write_data(*a):
|
||||
my_pubkey = pair.pubkey().to_bytes(False)
|
||||
session_key = pair.ecdh_multiply(his_pubkey)
|
||||
|
||||
words = [b2a_hex(session_key).decode()]
|
||||
|
||||
fname = b2a_hex(my_pubkey).decode() + '-ccbk.7z'
|
||||
|
||||
await write_complete_backup(words, fname, allow_copies=False, bypass_tmp=True)
|
||||
await write_complete_backup(b2a_hex(session_key).decode(), fname, allow_copies=False, bypass_tmp=True)
|
||||
|
||||
await ux_show_story("Done.\n\nTake this MicroSD card back to other Coldcard and continue from there.")
|
||||
|
||||
|
||||
@ -138,12 +138,15 @@ async def batt_idle_logout():
|
||||
# - even before login
|
||||
import glob
|
||||
from uasyncio import sleep_ms
|
||||
from glob import settings, dis
|
||||
from glob import settings, dis, SCAN
|
||||
import utime
|
||||
|
||||
while True:
|
||||
await sleep_ms(20000) # 20 seconds
|
||||
|
||||
if SCAN.busy_scanning:
|
||||
continue
|
||||
|
||||
if get_batt_level() is None:
|
||||
# on USB power
|
||||
continue
|
||||
|
||||
@ -6,12 +6,14 @@ import utime, uzlib, ngu
|
||||
from utils import problem_file_line
|
||||
from exceptions import QRDecodeExplained
|
||||
from ubinascii import unhexlify as a2b_hex
|
||||
from version import MAX_TXN_LEN
|
||||
|
||||
b32encode = ngu.codecs.b32_encode
|
||||
b32decode = ngu.codecs.b32_decode
|
||||
|
||||
TYPE_LABELS = dict(P='PSBT File', T='Transaction', J='JSON', C='CBOR', U='Unicode Text',
|
||||
X='Executable', B='Binary')
|
||||
X='Executable', B='Binary',
|
||||
R='KT Rx', S='KT Tx', E='KT PSBT')
|
||||
|
||||
def int2base36(n):
|
||||
# convert an integer to two digits of base 36 string. 00 thu ZZ as bytes
|
||||
@ -52,7 +54,7 @@ def calc_num_qr(char_capacity, char_len, split_mod):
|
||||
if char_len > actual:
|
||||
need += 1
|
||||
|
||||
# Challenge: the final QR might have just a a few chars in it, if we redistribute
|
||||
# Challenge: the final QR might have just a few chars in it, if we redistribute
|
||||
# the data into the other parts, then each QR can have more forward error correction
|
||||
# and be more robust. Must respect split_mod alignment tho.
|
||||
level = ceil(char_len / need)
|
||||
@ -212,7 +214,7 @@ class BBQrState:
|
||||
# can happen if QR got corrupted between scanner and us (overlap)
|
||||
# or back BBQr implementation
|
||||
#print("corrupt QR: %s" % scan)
|
||||
import sys; sys.print_exception(exc)
|
||||
# import sys; sys.print_exception(exc)
|
||||
|
||||
dis.draw_bbqr_progress(hdr, self.parts, corrupt=True)
|
||||
return True
|
||||
@ -241,7 +243,7 @@ class BBQrState:
|
||||
# provide UX -- even if we didn't use it
|
||||
dis.draw_bbqr_progress(hdr, self.parts)
|
||||
|
||||
# do we need more still?
|
||||
# return T if we need more parts still
|
||||
return (len(self.parts) < hdr.num_parts) or self.runt
|
||||
|
||||
class BBQrStorage:
|
||||
@ -328,14 +330,12 @@ class BBQrPsramStorage(BBQrStorage):
|
||||
def alloc_buf(self, upper_bound):
|
||||
# using first part of PSRAM
|
||||
|
||||
from public_constants import MAX_TXN_LEN_MK4
|
||||
|
||||
if upper_bound >= MAX_TXN_LEN_MK4:
|
||||
if upper_bound >= MAX_TXN_LEN:
|
||||
raise QRDecodeExplained("Too big")
|
||||
|
||||
# If data is compressed, write tmp (compressed) copy into top half of PSRAM
|
||||
# and we'll put final, decompressed copy at zero offset (later)
|
||||
self.psr_offset = MAX_TXN_LEN_MK4 if self.hdr.encoding == 'Z' else 0
|
||||
self.psr_offset = MAX_TXN_LEN if self.hdr.encoding == 'Z' else 0
|
||||
|
||||
self.buf = True
|
||||
|
||||
@ -394,7 +394,6 @@ class BBQrPsramStorage(BBQrStorage):
|
||||
from glob import PSRAM, dis
|
||||
from uzlib import DecompIO
|
||||
from io import BytesIO
|
||||
from public_constants import MAX_TXN_LEN_MK4
|
||||
|
||||
dis.fullscreen('Decompressing...')
|
||||
|
||||
@ -414,7 +413,7 @@ class BBQrPsramStorage(BBQrStorage):
|
||||
buf += here
|
||||
ln = len(buf) & ~3
|
||||
|
||||
if off+ln > MAX_TXN_LEN_MK4:
|
||||
if off+ln > MAX_TXN_LEN:
|
||||
# test with: `yes | dd bs=1000 count=2700 | bbqr make - | pbcopy`
|
||||
raise QRDecodeExplained("Too big")
|
||||
|
||||
@ -440,5 +439,18 @@ class BBQrPsramStorage(BBQrStorage):
|
||||
from glob import PSRAM
|
||||
return PSRAM.read_at(0, self.final_size)
|
||||
|
||||
def finalize(self):
|
||||
self._finalize()
|
||||
|
||||
if self.hdr.encoding == 'Z':
|
||||
self.zlib_decompress()
|
||||
|
||||
# PSBT-typed BBQrs end up at PSRAM[0..size]
|
||||
# skip a redundant PSRAM->heap->PSRAM round-trip
|
||||
if self.hdr.file_type == 'P':
|
||||
return self.hdr.file_type, self.final_size, 'PSRAM'
|
||||
|
||||
return self.hdr.file_type, self.final_size, self.get_buffer()
|
||||
|
||||
|
||||
# EOF
|
||||
|
||||
9
shared/block_height.py
Normal file
9
shared/block_height.py
Normal file
@ -0,0 +1,9 @@
|
||||
# (c) Copyright 2026 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
# AUTO-generated.
|
||||
#
|
||||
# Updated: 2026-06-19 14:13:23 UTC
|
||||
|
||||
BLOCK_HEIGHT = 932301
|
||||
|
||||
# EOF
|
||||
@ -1,6 +1,6 @@
|
||||
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
# calc.py - Simple python REPL before login
|
||||
# calc.py - Simple TOY calculator, before login. Not meant to be useful, just fun!
|
||||
#
|
||||
# Test with: ./simulator.py --q1 --eff -g --set calc=1
|
||||
#
|
||||
@ -9,7 +9,7 @@ from utils import B2A, word_wrap
|
||||
from ux_q1 import ux_input_text
|
||||
|
||||
async def login_repl():
|
||||
from glob import dis, settings
|
||||
from glob import dis
|
||||
from pincodes import pa
|
||||
|
||||
NUM_LINES = 7 # 10 - title - 2 for prompt
|
||||
@ -19,22 +19,25 @@ async def login_repl():
|
||||
re_pin = re.compile(r'^(\d\d+)[-_ ](\d\d+)$')
|
||||
|
||||
# in decreasing order of hazard...
|
||||
blacklist = ['import', '__', 'exec', 'locals', 'globals', 'eval', 'input']
|
||||
# - find these with: import builtins; help(builtins)
|
||||
blacklist = ['import', '__', 'exec', 'locals', 'globals', 'eval', 'input',
|
||||
'getattr', 'setattr', 'delattr', 'open', 'execfile', 'compile' ]
|
||||
|
||||
lines = '''\
|
||||
|
||||
Example Commands:
|
||||
>> 23 + 55 / 22
|
||||
>> a = 4; b = 3;
|
||||
>> a*b
|
||||
>> sha256('123456123456')
|
||||
>> cls() # clear screen\
|
||||
>> 1.020 * 45.88
|
||||
>> sha256('some message')
|
||||
>> cls # clear screen
|
||||
>> help\
|
||||
'''.split('\n')
|
||||
|
||||
state = dict()
|
||||
state['sha256'] = lambda x: B2A(ngu.hash.sha256s(x))
|
||||
state['sha512'] = lambda x: B2A(ngu.hash.sha512(x).digest())
|
||||
state['ripemd'] = lambda x: B2A(ngu.hash.ripemd160(x))
|
||||
state['rand'] = lambda x=32: B2A(ngu.random.bytes(x))
|
||||
state['cls'] = lambda: lines.clear()
|
||||
state['help'] = lambda: 'Commands: ' + (', '.join(state))
|
||||
|
||||
@ -56,17 +59,17 @@ Example Commands:
|
||||
try:
|
||||
dis.busy_bar(1)
|
||||
|
||||
if ln == None :
|
||||
if ln is None :
|
||||
# Cancel key - do nothing
|
||||
ans = None
|
||||
elif ln in state and callable(state[ln]):
|
||||
# no needs for () in my world
|
||||
elif ln in ('help', 'cls', 'rand'):
|
||||
# no need for () for these commands
|
||||
ans = state[ln]()
|
||||
elif re_pin.match(ln) and len(ln) <= 13:
|
||||
elif pa.attempts_left and re_pin.match(ln) and (len(ln) <= 13):
|
||||
# try login
|
||||
m = re_pin.match(ln)
|
||||
ln = m.group(1)+ '-' + m.group(2)
|
||||
print(ln)
|
||||
|
||||
try:
|
||||
pa.setup(ln)
|
||||
ok = pa.login()
|
||||
@ -80,16 +83,14 @@ Example Commands:
|
||||
else:
|
||||
ans = 'Error: ' + repr(exc.args)
|
||||
|
||||
elif re_prefix.match(ln) and len(ln) <= 7:
|
||||
elif re_prefix.match(ln) and (len(ln) <= 7):
|
||||
# show words
|
||||
ans = pa.prefix_words(ln[:-1].encode())
|
||||
else:
|
||||
if any((b in ln) for b in blacklist):
|
||||
ans = None
|
||||
elif '=' in ln:
|
||||
ans = exec(ln, state)
|
||||
else:
|
||||
ans = eval(ln, state)
|
||||
ans = eval(ln, state.copy())
|
||||
|
||||
except Exception as exc:
|
||||
lines.extend(word_wrap(str(exc), 34))
|
||||
|
||||
1301
shared/ccc.py
Normal file
1301
shared/ccc.py
Normal file
File diff suppressed because it is too large
Load Diff
234
shared/chains.py
234
shared/chains.py
@ -5,13 +5,17 @@
|
||||
import ngu
|
||||
from uhashlib import sha256
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2TR
|
||||
from public_constants import AF_BARE_PK, AF_CLASSIC, AF_P2WPKH, AF_P2TR
|
||||
from public_constants import AF_P2SH, AF_P2WSH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH
|
||||
from public_constants import AFC_PUBKEY, AFC_SEGWIT, AFC_BECH32, AFC_SCRIPT
|
||||
from block_height import BLOCK_HEIGHT
|
||||
from serializations import hash160, ser_compact_size, disassemble
|
||||
from ucollections import namedtuple
|
||||
from opcodes import OP_RETURN, OP_1, OP_16
|
||||
|
||||
# DO NOT CHANGE ORDER! PickAddrFmtMenu.__init__ expects correct order
|
||||
SINGLESIG_AF = (AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH)
|
||||
|
||||
# See SLIP 132 <https://github.com/satoshilabs/slips/blob/master/slip-0132.md>
|
||||
# for background on these version bytes. Not to be confused with SLIP-32 which involves Bech32.
|
||||
Slip132Version = namedtuple('Slip132Version', ('pub', 'priv', 'hint'))
|
||||
@ -19,18 +23,21 @@ Slip132Version = namedtuple('Slip132Version', ('pub', 'priv', 'hint'))
|
||||
# See also:
|
||||
# - <https://github.com/satoshilabs/slips/blob/master/slip-0132.md>
|
||||
# - defines ypub/zpub/Xprc variants
|
||||
# - <https://github.com/satoshilabs/slips/blob/master/slip-0032.md>
|
||||
# - nice bech32 encoded scheme for going forward
|
||||
# - <https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-September/014907.html>
|
||||
# - mailing list post proposed ypub, etc.
|
||||
# - from <https://github.com/Bit-Wasp/bitcoin-php/issues/576>
|
||||
# - also electrum source: electrum/lib/constants.py
|
||||
|
||||
# nLockTime in transaction equal or above this value is a unix timestamp (time_t) not block height.
|
||||
NLOCK_IS_TIME = const(500000000)
|
||||
|
||||
|
||||
class ChainsBase:
|
||||
|
||||
curve = 'secp256k1'
|
||||
menu_name = None # use 'name' if this isn't defined
|
||||
core_name = None # name of chain's "core" p2p software
|
||||
ccc_min_block = 0
|
||||
|
||||
# b44_cointype comes from
|
||||
# <https://github.com/satoshilabs/slips/blob/master/slip-0044.md>
|
||||
@ -74,6 +81,41 @@ class ChainsBase:
|
||||
or (version == cls.slip132[addr_fmt].priv)
|
||||
return node
|
||||
|
||||
@classmethod
|
||||
def script_pubkey(cls, addr_fmt, pubkey=None, script=None):
|
||||
digest = None
|
||||
if addr_fmt & AFC_SCRIPT:
|
||||
assert script, "need witness/redeem script"
|
||||
|
||||
if addr_fmt in [AF_P2WSH, AF_P2WSH_P2SH]:
|
||||
digest = ngu.hash.sha256s(script)
|
||||
# bech32 encoded segwit p2sh
|
||||
spk = b'\x00\x20' + digest
|
||||
if addr_fmt == AF_P2WSH_P2SH:
|
||||
# segwit p2wsh encoded as classic P2SH
|
||||
digest = hash160(spk)
|
||||
spk = b'\xA9\x14' + digest + b'\x87'
|
||||
|
||||
else:
|
||||
assert addr_fmt == AF_P2SH
|
||||
digest = hash160(script)
|
||||
spk = b'\xA9\x14' + digest + b'\x87'
|
||||
|
||||
else:
|
||||
assert pubkey
|
||||
keyhash = ngu.hash.hash160(pubkey)
|
||||
if addr_fmt == AF_CLASSIC:
|
||||
spk = b'\x76\xA9\x14' + keyhash + b'\x88\xAC'
|
||||
elif addr_fmt == AF_P2WPKH_P2SH:
|
||||
redeem_script = b'\x00\x14' + keyhash
|
||||
spk = b'\xA9\x14' + ngu.hash.hash160(redeem_script) + b'\x87'
|
||||
elif addr_fmt == AF_P2WPKH:
|
||||
spk = b'\x00\x14' + keyhash
|
||||
else:
|
||||
raise ValueError('bad address template: %s' % addr_fmt)
|
||||
|
||||
return spk, digest
|
||||
|
||||
@classmethod
|
||||
def p2sh_address(cls, addr_fmt, witdeem_script):
|
||||
# Multisig and general P2SH support
|
||||
@ -85,21 +127,14 @@ class ChainsBase:
|
||||
# - returns: str(address)
|
||||
|
||||
assert addr_fmt & AFC_SCRIPT, 'for p2sh only'
|
||||
assert witdeem_script, "need witness/redeem script"
|
||||
_, digest = cls.script_pubkey(addr_fmt, script=witdeem_script)
|
||||
|
||||
if addr_fmt & AFC_SEGWIT:
|
||||
digest = ngu.hash.sha256s(witdeem_script)
|
||||
else:
|
||||
digest = hash160(witdeem_script)
|
||||
|
||||
if addr_fmt & AFC_BECH32:
|
||||
if addr_fmt == AF_P2WSH:
|
||||
# bech32 encoded segwit p2sh
|
||||
addr = ngu.codecs.segwit_encode(cls.bech32_hrp, 0, digest)
|
||||
elif addr_fmt == AF_P2WSH_P2SH:
|
||||
# segwit p2wsh encoded as classic P2SH
|
||||
addr = ngu.codecs.b58_encode(cls.b58_script + hash160(b'\x00\x20' + digest))
|
||||
else:
|
||||
# P2SH classic
|
||||
# segwit p2wsh encoded as classic P2SH
|
||||
# and P2SH classic
|
||||
addr = ngu.codecs.b58_encode(cls.b58_script + digest)
|
||||
|
||||
return addr
|
||||
@ -109,20 +144,8 @@ class ChainsBase:
|
||||
# - renders a pubkey to an address
|
||||
# - works only with single-key addresses
|
||||
assert not addr_fmt & AFC_SCRIPT
|
||||
|
||||
keyhash = ngu.hash.hash160(pubkey)
|
||||
if addr_fmt == AF_CLASSIC:
|
||||
script = b'\x76\xA9\x14' + keyhash + b'\x88\xAC'
|
||||
elif addr_fmt == AF_P2WPKH_P2SH:
|
||||
redeem_script = b'\x00\x14' + keyhash
|
||||
scripthash = ngu.hash.hash160(redeem_script)
|
||||
script = b'\xA9\x14' + scripthash + b'\x87'
|
||||
elif addr_fmt == AF_P2WPKH:
|
||||
script = b'\x00\x14' + keyhash
|
||||
else:
|
||||
raise ValueError('bad address template: %s' % addr_fmt)
|
||||
|
||||
return cls.render_address(script)
|
||||
spk, _ = cls.script_pubkey(addr_fmt, pubkey=pubkey)
|
||||
return cls.render_address(spk)
|
||||
|
||||
@classmethod
|
||||
def address(cls, node, addr_fmt):
|
||||
@ -161,7 +184,7 @@ class ChainsBase:
|
||||
@classmethod
|
||||
def hash_message(cls, msg=None, msg_len=0):
|
||||
# Perform sha256 for message-signing purposes (only)
|
||||
# - or get setup for that, if msg == None
|
||||
# - or get setup for that, if msg is None
|
||||
s = sha256()
|
||||
|
||||
s.update(cls.msg_signing_prefix())
|
||||
@ -231,7 +254,7 @@ class ChainsBase:
|
||||
return ngu.codecs.b58_encode(cls.b58_script + script[2:2+20])
|
||||
|
||||
# segwit v0 (P2WPKH, P2WSH)
|
||||
if script[0] == 0 and script[1] in (0x14, 0x20) and (ll-2) == script[1]:
|
||||
if ll in (22, 34) and script[0] == 0 and script[1] in (0x14, 0x20) and (ll-2) == script[1]:
|
||||
return ngu.codecs.segwit_encode(cls.bech32_hrp, script[0], script[2:])
|
||||
|
||||
# segwit v1 (P2TR) and later segwit version
|
||||
@ -242,56 +265,40 @@ class ChainsBase:
|
||||
|
||||
@classmethod
|
||||
def op_return(cls, script):
|
||||
"""Returns decoded string op return data if script is op return otherwise None"""
|
||||
gen = disassemble(script)
|
||||
script_type = next(gen)
|
||||
if OP_RETURN in script_type:
|
||||
try:
|
||||
data = next(gen)[0]
|
||||
if data is None: raise RuntimeError
|
||||
except (RuntimeError, StopIteration):
|
||||
return "null-data", ""
|
||||
data_hex = b2a_hex(data).decode()
|
||||
data_ascii = None
|
||||
if min(data) >= 32 and max(data) < 127: # printable
|
||||
try:
|
||||
data_ascii = data.decode("ascii")
|
||||
except:
|
||||
pass
|
||||
return data_hex, data_ascii
|
||||
return None
|
||||
try:
|
||||
gen = disassemble(script)
|
||||
item, opcode = next(gen)
|
||||
except (StopIteration, ValueError):
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def possible_address_fmt(cls, addr):
|
||||
# Given a text (serialized) address, return what
|
||||
# address format applies to the address, but
|
||||
# for AF_P2SH case, could be: AF_P2SH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH. .. we don't know
|
||||
if addr.startswith(cls.bech32_hrp):
|
||||
if addr.startswith(cls.bech32_hrp+'1p'):
|
||||
# really any ver=1 script or address, but for now...
|
||||
return AF_P2TR
|
||||
else:
|
||||
return AF_P2WPKH if len(addr) < 55 else AF_P2WSH
|
||||
if opcode != OP_RETURN:
|
||||
return None
|
||||
|
||||
try:
|
||||
raw = ngu.codecs.b58_decode(addr)
|
||||
except ValueError:
|
||||
# not base58, not an error
|
||||
return 0
|
||||
try:
|
||||
data, opcode = next(gen)
|
||||
except StopIteration:
|
||||
return b"" # bare OP_RETURN
|
||||
|
||||
if raw[0] == cls.b58_addr[0]:
|
||||
return AF_CLASSIC
|
||||
if raw[0] == cls.b58_script[0]:
|
||||
return AF_P2SH
|
||||
try:
|
||||
next(gen)
|
||||
return None # extra ops/pushes -> raw script display
|
||||
except StopIteration: pass
|
||||
|
||||
return 0
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
if isinstance(data, bytes):
|
||||
return data
|
||||
if data is None and opcode == 0:
|
||||
return b"" # OP_RETURN OP_0
|
||||
return None
|
||||
|
||||
class BitcoinMain(ChainsBase):
|
||||
# see <https://github.com/bitcoin/bitcoin/blob/master/src/chainparams.cpp#L140>
|
||||
ctype = 'BTC'
|
||||
name = 'Bitcoin'
|
||||
core_name = 'Bitcoin Core'
|
||||
name = 'Bitcoin Mainnet'
|
||||
ccc_min_block = BLOCK_HEIGHT
|
||||
|
||||
slip132 = {
|
||||
AF_CLASSIC: Slip132Version(0x0488B21E, 0x0488ADE4, 'x'),
|
||||
@ -309,10 +316,10 @@ class BitcoinMain(ChainsBase):
|
||||
|
||||
b44_cointype = 0
|
||||
|
||||
class BitcoinTestnet(BitcoinMain):
|
||||
class BitcoinTestnet(ChainsBase):
|
||||
# testnet4 (was testnet3 up until 2025 but all parameters are the same)
|
||||
ctype = 'XTN'
|
||||
name = 'Bitcoin Testnet'
|
||||
menu_name = 'Testnet: BTC'
|
||||
name = 'Bitcoin Testnet 4'
|
||||
|
||||
slip132 = {
|
||||
AF_CLASSIC: Slip132Version(0x043587cf, 0x04358394, 't'),
|
||||
@ -331,27 +338,11 @@ class BitcoinTestnet(BitcoinMain):
|
||||
b44_cointype = 1
|
||||
|
||||
|
||||
class BitcoinRegtest(BitcoinMain):
|
||||
class BitcoinRegtest(BitcoinTestnet):
|
||||
ctype = 'XRT'
|
||||
name = 'Bitcoin Regtest'
|
||||
menu_name = 'Regtest: BTC'
|
||||
|
||||
slip132 = {
|
||||
AF_CLASSIC: Slip132Version(0x043587cf, 0x04358394, 't'),
|
||||
AF_P2WPKH_P2SH: Slip132Version(0x044a5262, 0x044a4e28, 'u'),
|
||||
AF_P2WPKH: Slip132Version(0x045f1cf6, 0x045f18bc, 'v'),
|
||||
AF_P2WSH_P2SH: Slip132Version(0x024289ef, 0x024285b5, 'U'),
|
||||
AF_P2WSH: Slip132Version(0x02575483, 0x02575048, 'V'),
|
||||
}
|
||||
|
||||
bech32_hrp = 'bcrt'
|
||||
|
||||
b58_addr = bytes([111])
|
||||
b58_script = bytes([196])
|
||||
b58_privkey = bytes([239])
|
||||
|
||||
b44_cointype = 1
|
||||
|
||||
|
||||
def get_chain(short_name):
|
||||
# lookup object from name: 'BTC' or 'XTN'
|
||||
@ -379,7 +370,7 @@ def current_chain():
|
||||
# Overbuilt: will only be testnet and mainchain.
|
||||
AllChains = [BitcoinMain, BitcoinTestnet, BitcoinRegtest]
|
||||
|
||||
def slip32_deserialize(xp):
|
||||
def slip132_deserialize(xp):
|
||||
# .. and classify chain and addr-type, as implied by prefix
|
||||
node = ngu.hdnode.HDNode()
|
||||
version = node.deserialize(xp)
|
||||
@ -405,6 +396,67 @@ CommonDerivations = [
|
||||
AF_P2WPKH ), # generates bc1 bech32 addresses
|
||||
]
|
||||
|
||||
STD_DERIVATIONS = {
|
||||
"p2pkh": CommonDerivations[0][1],
|
||||
"p2sh-p2wpkh": CommonDerivations[1][1],
|
||||
"p2wpkh-p2sh": CommonDerivations[1][1],
|
||||
"p2wpkh": CommonDerivations[2][1],
|
||||
}
|
||||
|
||||
MS_STD_DERIVATIONS = {
|
||||
("p2sh", "m/45h", AF_P2SH),
|
||||
("p2sh_p2wsh", "m/48h/{coin}h/{acct_num}h/1h", AF_P2WSH_P2SH),
|
||||
("p2wsh", "m/48h/{coin}h/{acct_num}h/2h", AF_P2WSH),
|
||||
}
|
||||
|
||||
def parse_addr_fmt_str(addr_fmt):
|
||||
# accepts strings and also integers if already parsed
|
||||
try:
|
||||
if isinstance(addr_fmt, int):
|
||||
if addr_fmt in [AF_P2WPKH_P2SH, AF_P2WPKH, AF_CLASSIC]:
|
||||
return addr_fmt
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
addr_fmt = addr_fmt.lower()
|
||||
if addr_fmt in ("p2sh-p2wpkh", "p2wpkh-p2sh"):
|
||||
return AF_P2WPKH_P2SH
|
||||
elif addr_fmt == "p2pkh":
|
||||
return AF_CLASSIC
|
||||
elif addr_fmt == "p2wpkh":
|
||||
return AF_P2WPKH
|
||||
else:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
raise ValueError("Invalid address format: '%s'\n\n"
|
||||
"Choose from p2pkh, p2wpkh, p2sh-p2wpkh." % addr_fmt)
|
||||
|
||||
|
||||
def af_to_bip44_purpose(addr_fmt):
|
||||
# Address format to BIP-44 "purpose" number
|
||||
# - single signature only
|
||||
return {AF_CLASSIC: 44,
|
||||
AF_P2WPKH_P2SH: 49,
|
||||
AF_P2WPKH: 84}[addr_fmt]
|
||||
|
||||
|
||||
def addr_fmt_label(addr_fmt):
|
||||
# Text used in menus
|
||||
return {AF_CLASSIC: "Classic P2PKH",
|
||||
AF_P2WPKH_P2SH: "P2SH-Segwit",
|
||||
AF_P2WPKH: "Segwit P2WPKH"}[addr_fmt]
|
||||
|
||||
|
||||
def addr_fmt_str(addr_fmt):
|
||||
# Short string codes used for address format (industry standard)
|
||||
return {AF_CLASSIC: "p2pkh",
|
||||
AF_BARE_PK: "p2pk",
|
||||
AF_P2SH: "p2sh",
|
||||
AF_P2TR: "p2tr",
|
||||
AF_P2WPKH: "p2wpkh",
|
||||
AF_P2WSH: "p2wsh",
|
||||
AF_P2WPKH_P2SH: "p2sh-p2wpkh",
|
||||
AF_P2WSH_P2SH: "p2sh-p2wsh"}[addr_fmt]
|
||||
|
||||
def verify_recover_pubkey(sig, digest):
|
||||
# verifies a message digest against a signature and recovers
|
||||
|
||||
@ -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
|
||||
|
||||
@ -51,9 +51,7 @@ def decode_utf_16_le(s):
|
||||
'''
|
||||
|
||||
def read_var64(f):
|
||||
'''
|
||||
Decode their silly 64-bit encoding.
|
||||
'''
|
||||
# Decode their silly 64-bit encoding.
|
||||
first = ord(f.read(1))
|
||||
if first < 128:
|
||||
return first
|
||||
@ -100,7 +98,7 @@ def check_file_headers(f):
|
||||
# assume f is seekable
|
||||
fh = FileHeader.read(f)
|
||||
|
||||
if not fh.has_good_magic:
|
||||
if not fh.has_good_magic():
|
||||
raise ValueError("Bad magic bytes")
|
||||
|
||||
# read only first header
|
||||
@ -113,22 +111,21 @@ def check_file_headers(f):
|
||||
if sh.size > 10000:
|
||||
raise ValueError("Second header too big")
|
||||
|
||||
# capture this spot
|
||||
# TODO 'data_start' unused
|
||||
data_start = f.tell() # expect 0x20
|
||||
# FileHeader.read() always reads exactly calcsize('<6sBBL') = 12 bytes
|
||||
# SectionHeader.read() always reads exactly calcsize('<QQL') = 20 bytes
|
||||
# after those two calls, f.tell() is always start_pos + 32
|
||||
# assert f.tell() == 0x20 # expect 0x20
|
||||
|
||||
try:
|
||||
f.seek(sh.offset, 1)
|
||||
th = f.read(sh.size)
|
||||
if len(th) != sh.size:
|
||||
raise IndexError("Truncated file?")
|
||||
assert len(th) == sh.size, "Truncated file?"
|
||||
|
||||
# Look for properties about compression. this could be
|
||||
# faked-out but good enough for now
|
||||
if b'\x24\x06\xf1\x07\x01' not in th:
|
||||
raise RuntimeError("Not marked as AES+SHA encrypted?")
|
||||
assert b'\x24\x06\xf1\x07\x01' in th, "Not marked as AES+SHA encrypted?"
|
||||
except Exception as e:
|
||||
raise ValueError("Confused file? %s" % e.message)
|
||||
raise ValueError("Confused file? %s" % e)
|
||||
|
||||
if masked_crc(th) != sh.crc:
|
||||
raise ValueError("Trailing header has wrong CRC")
|
||||
@ -174,7 +171,6 @@ class FileHeader(object):
|
||||
|
||||
def actual_crc(self):
|
||||
return masked_crc(self.bits)
|
||||
|
||||
|
||||
|
||||
class SectionHeader(namedtuple('SectionHeader', ['offset', 'size', 'crc' ])):
|
||||
@ -198,7 +194,7 @@ class SectionHeader(namedtuple('SectionHeader', ['offset', 'size', 'crc' ])):
|
||||
# read only next one; ftell has to be on first byte already
|
||||
rv = cls.read(f)
|
||||
|
||||
if expect_crc != None:
|
||||
if expect_crc is not None:
|
||||
assert rv # read past end
|
||||
assert masked_crc(rv.bits) == expect_crc
|
||||
|
||||
@ -213,6 +209,7 @@ class SectionHeader(namedtuple('SectionHeader', ['offset', 'size', 'crc' ])):
|
||||
def actual_crc(self):
|
||||
return masked_crc(self.bits)
|
||||
|
||||
|
||||
class Builder(object):
|
||||
def __init__(self, password=None, salt_len=16, iv_len=16, rounds_pow=13, progress_fcn=None):
|
||||
self.rounds_pow = rounds_pow # standard is 19, 16 and 17 work fine
|
||||
@ -315,7 +312,7 @@ class Builder(object):
|
||||
|
||||
padded_len = (here + 15) & ~15
|
||||
if padded_len != here:
|
||||
if self.padding != None:
|
||||
if self.padding is not None:
|
||||
raise ValueError() # "can't do less than a block except at end"
|
||||
self.padding = (padded_len - here)
|
||||
raw += bytes(self.padding)
|
||||
|
||||
@ -4,13 +4,22 @@
|
||||
#
|
||||
# included in Q builds only, not Mk4 --> manifest_q1.py
|
||||
#
|
||||
import ngu, bip39, ure, stash
|
||||
import ngu, bip39, ure, stash, json
|
||||
from ubinascii import unhexlify as a2b_hex
|
||||
from exceptions import QRDecodeExplained
|
||||
from bbqr import TYPE_LABELS
|
||||
from utils import decode_bip21_text
|
||||
|
||||
|
||||
def decode_qr_text(got):
|
||||
if isinstance(got, str):
|
||||
return got
|
||||
|
||||
try:
|
||||
return got.decode()
|
||||
except UnicodeError:
|
||||
raise QRDecodeExplained('UTF-8 decode failed')
|
||||
|
||||
def decode_seed_qr(data):
|
||||
# SeedQR: 4 digit groups of index into word list
|
||||
parts = [data[pos:pos + 4] for pos in range(0, len(data), 4)]
|
||||
@ -39,6 +48,8 @@ def decode_secret(got):
|
||||
# - xprv / tprv
|
||||
# - words (either full or prefixes, case insensitive)
|
||||
# - SeedQR (github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md)
|
||||
# - word lists are NOT BIP-39-checksum-validated here. Callers that
|
||||
# require a valid seed must run bip39.a2b_words(...)
|
||||
|
||||
if len(got) > 300:
|
||||
raise ValueError("Too big.")
|
||||
@ -51,7 +62,7 @@ def decode_secret(got):
|
||||
# xprv or tprv: private key import for sure
|
||||
# - verify checksum is right
|
||||
try:
|
||||
raw = ngu.codecs.b58_decode(got)
|
||||
ngu.codecs.b58_decode(got)
|
||||
except:
|
||||
raise ValueError('corrupt xprv?')
|
||||
|
||||
@ -59,19 +70,11 @@ def decode_secret(got):
|
||||
|
||||
if len(got) in (51, 52):
|
||||
try:
|
||||
raw = ngu.codecs.b58_decode(got)
|
||||
if raw[0] in (0xef, 0x80):
|
||||
testnet = True if raw[0] == 0xef else False
|
||||
if len(raw) in (33, 34): # uncompressed pubkey
|
||||
compressed = False
|
||||
if len(raw) == 34: # compressed pubkey
|
||||
assert raw[33] == 0x01
|
||||
compressed = True
|
||||
sk = raw[1:33]
|
||||
kp = ngu.secp256k1.keypair(sk)
|
||||
return 'wif', (got, kp, compressed, testnet)
|
||||
from wif import decode_wif
|
||||
kp, testnet, compressed = decode_wif(got)
|
||||
return 'wif', (got, kp, compressed, testnet)
|
||||
except: pass
|
||||
|
||||
|
||||
taste = got.strip().lower()
|
||||
|
||||
if taste.isdigit():
|
||||
@ -101,7 +104,7 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
|
||||
try:
|
||||
ty, final_size, got = got.storage.finalize()
|
||||
except BaseException as exc:
|
||||
import sys; sys.print_exception(exc)
|
||||
#import sys; sys.print_exception(exc)
|
||||
raise QRDecodeExplained("BBQr decode failed: " + str(exc))
|
||||
|
||||
if expect_bbqr:
|
||||
@ -116,11 +119,8 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
|
||||
return got.decode()
|
||||
|
||||
if ty == 'P':
|
||||
# may already be in PSRAM, avoid a copy here
|
||||
from glob import PSRAM
|
||||
if PSRAM.is_at(got, 0):
|
||||
got = 'PSRAM' # see qr_psbt_sign()
|
||||
|
||||
# `got` is the literal 'PSRAM' from BBQrPsramStorage when data already there
|
||||
# otherwise it's real bytes
|
||||
return 'psbt', (None, final_size, got)
|
||||
|
||||
elif ty == 'T':
|
||||
@ -128,10 +128,27 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
|
||||
|
||||
elif ty == 'U':
|
||||
# continue thru code below for TEXT
|
||||
pass
|
||||
got = decode_qr_text(got)
|
||||
|
||||
elif ty == 'J':
|
||||
return 'json', (got,)
|
||||
got = decode_qr_text(got)
|
||||
what = "json"
|
||||
if "msg" in got:
|
||||
what = "smsg"
|
||||
|
||||
return what, (got,)
|
||||
|
||||
elif ty in 'RSE':
|
||||
# key-teleport related
|
||||
|
||||
from pincodes import pa
|
||||
if pa.hobbled_mode and ty != 'E':
|
||||
raise QRDecodeExplained("KT Blocked")
|
||||
|
||||
if ty == 'R' and len(got) != 33:
|
||||
raise QRDecodeExplained("Truncated KT RX")
|
||||
|
||||
return 'teleport', (ty, got)
|
||||
else:
|
||||
msg = TYPE_LABELS.get(ty, 'Unknown FileType')
|
||||
raise QRDecodeExplained("Sorry, %s not useful." % msg)
|
||||
@ -159,6 +176,16 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
|
||||
if expect_secret:
|
||||
raise QRDecodeExplained("Not a secret?")
|
||||
|
||||
try:
|
||||
dct = json.loads(got)
|
||||
if "msg" in dct:
|
||||
return "smsg", (got,)
|
||||
except: pass
|
||||
|
||||
# Sparrow compat
|
||||
if "signmessage" in got:
|
||||
return "smsg", (got,)
|
||||
|
||||
# try to recognize various bitcoin-related text strings...
|
||||
return decode_short_text(got)
|
||||
|
||||
@ -169,15 +196,13 @@ def decode_short_text(got):
|
||||
# - if bad checksum on bitcoin addr, we treat as text... since might be
|
||||
# return: what-it-is, (tuple)
|
||||
|
||||
if not isinstance(got, str):
|
||||
# decode utf-8
|
||||
try:
|
||||
got = got.decode()
|
||||
except UnicodeError:
|
||||
raise QRDecodeExplained('UTF-8 decode failed')
|
||||
got = decode_qr_text(got)
|
||||
|
||||
# might be a PSBT?
|
||||
if len(got) > 100:
|
||||
if got.lstrip().startswith("-----BEGIN BITCOIN SIGNED MESSAGE-----"):
|
||||
return "vmsg", (got,)
|
||||
|
||||
from auth import psbt_encoding_taster
|
||||
try:
|
||||
decoder, _, psbt_len = psbt_encoding_taster(got[0:10].encode(), len(got))
|
||||
@ -206,10 +231,11 @@ def decode_short_text(got):
|
||||
cc_ms_pat = r"[0-9a-fA-F]+\s*:\s*[xtyYzZuUvV]pub[1-9A-HJ-NP-Za-km-z]+"
|
||||
rgx = ure.compile(cc_ms_pat)
|
||||
# go line by line and match above, once 2 matches observed - considered multisig
|
||||
# important to not use ure.search for big strings (can run out of stack)
|
||||
# important to not use ure.search for big strings (can run out of stack);
|
||||
# a real line here is a "<8-hex xfp>: <xpub>" key (~121 chars)
|
||||
c = 0 # match count
|
||||
for l in got.split("\n"):
|
||||
if rgx.search(l):
|
||||
if len(l) <= 150 and rgx.search(l):
|
||||
c += 1
|
||||
if c > 1:
|
||||
return 'multi', (got,)
|
||||
|
||||
@ -160,7 +160,6 @@ class Descriptor:
|
||||
raise ValueError("Key origin info is required for %s" % (key))
|
||||
key_orig_info = key[1:close_index] # remove brackets
|
||||
key = key[close_index + 1:]
|
||||
assert "/" in key_orig_info, "Malformed key derivation info"
|
||||
return key_orig_info, key
|
||||
|
||||
@staticmethod
|
||||
|
||||
@ -2,11 +2,11 @@
|
||||
#
|
||||
# display.py - OLED rendering
|
||||
#
|
||||
import machine, uzlib, ckcc, utime
|
||||
import machine, uzlib, ckcc, utime, version
|
||||
from ssd1306 import SSD1306_SPI
|
||||
from version import is_devmode
|
||||
import framebuf
|
||||
from graphics_mk4 import Graphics
|
||||
from charcodes import OUT_CTRL_TITLE, OUT_CTRL_ADDRESS
|
||||
|
||||
# we support 4 fonts
|
||||
from zevvpeep import FontSmall, FontLarge, FontTiny
|
||||
@ -34,11 +34,14 @@ class Display:
|
||||
dc_pin = Pin('PA8', Pin.OUT)
|
||||
cs_pin = Pin('PA4', Pin.OUT)
|
||||
|
||||
try:
|
||||
self.dis = SSD1306_SPI(128, 64, spi, dc_pin, reset_pin, cs_pin)
|
||||
except OSError:
|
||||
print("OLED unplugged?")
|
||||
raise
|
||||
if version.mk_num == 5:
|
||||
# Early revs (A-D) needed this pin asserted to enable +12v to OLED
|
||||
# - removed in rev E and later boards, but keep here for dev boards
|
||||
# - remove this in 2027
|
||||
vcc_en = Pin('V12EN', Pin.OUT) # aka PC1
|
||||
vcc_en(1)
|
||||
|
||||
self.dis = SSD1306_SPI(128, 64, spi, dc_pin, reset_pin, cs_pin, is_mk5=(version.mk_num==5))
|
||||
|
||||
self.last_bar_update = 0
|
||||
self.clear()
|
||||
@ -75,7 +78,7 @@ class Display:
|
||||
if x is None or x < 0:
|
||||
# center/rjust
|
||||
w = self.width(msg, font)
|
||||
if x == None:
|
||||
if x is None:
|
||||
x = max(0, (self.WIDTH - w) // 2)
|
||||
else:
|
||||
# measure from right edge (right justify)
|
||||
@ -141,7 +144,7 @@ class Display:
|
||||
self.icon(128-3, 1, 'scroll')
|
||||
self.dis.fill_rect(128-2, pos, 1, bh, 1)
|
||||
|
||||
if is_devmode and not ckcc.is_simulator():
|
||||
if version.is_devmode and not ckcc.is_simulator():
|
||||
self.dis.fill_rect(128-6, 20, 5, 21, 1)
|
||||
self.text(-2, 21, 'D', font=FontTiny, invert=1)
|
||||
self.text(-2, 28, 'E', font=FontTiny, invert=1)
|
||||
@ -149,11 +152,14 @@ class Display:
|
||||
|
||||
def fullscreen(self, msg, percent=None, line2=None):
|
||||
# show a simple message "fullscreen".
|
||||
# - 'line2' not supported on smaller screen sizes, ignore
|
||||
self.clear()
|
||||
y = 14
|
||||
self.text(None, y, msg, font=FontLarge)
|
||||
|
||||
if line2:
|
||||
# 21 + 6 ie. FontLarge.height of above text + FontTiny.height as space between
|
||||
self.text(None, y + 27, line2, font=FontSmall)
|
||||
|
||||
if percent is not None:
|
||||
self.progress_bar(percent)
|
||||
self.show()
|
||||
@ -200,61 +206,20 @@ class Display:
|
||||
|
||||
def busy_bar(self, enable):
|
||||
# Render a continuous activity (not progress) bar in lower 8 lines of display
|
||||
# - using OLED itself to do the animation, so smooth and CPU free
|
||||
# - cannot preserve bottom 8 lines, since we have to destructively write there
|
||||
# - assumes normal horz addr mode: 0x20, 0x00
|
||||
# - speed_code=>framedelay: 0=5fr, 1=64fr, 2=128, 3=256, 4=3, 5=4, 6=25, 7=2frames
|
||||
# unused: assert 0 <= speed_code <= 7
|
||||
|
||||
setup = bytes([
|
||||
0x21, 0x00, 0x7f, # setup column address range (start, end): 0-127
|
||||
0x22, 7, 7, # setup page start/end address: page 7=last 8 lines
|
||||
])
|
||||
animate = bytes([
|
||||
0x2e, # stop animations in progress
|
||||
0x26, # scroll leftwards (stock ticker mode)
|
||||
0, # placeholder
|
||||
7, # start 'page' (vertical)
|
||||
5, # "speed_code" # scroll speed: 7=fastest, but no order to it
|
||||
7, # end 'page'
|
||||
0, 0xff, # placeholders
|
||||
0x2f # start
|
||||
])
|
||||
|
||||
cleanup = bytes([
|
||||
0x2e, # stop animation
|
||||
0x20, 0x00, # horz addr-ing mode
|
||||
0x21, 0x00, 0x7f, # setup column address range (start, end): 0-127
|
||||
0x22, 7, 7, # setup page start/end address: page 7=last 8 lines
|
||||
])
|
||||
|
||||
#
|
||||
if not enable:
|
||||
# stop animation, and redraw old (new) screen
|
||||
self.write_cmds(cleanup)
|
||||
self.dis.busy_bar(False, None)
|
||||
self.show()
|
||||
else:
|
||||
|
||||
# a pattern that repeats nicely mod 128
|
||||
# Need a pattern that repeats nicely mod 128
|
||||
# - each byte here is a vertical column, 8 pixels tall, MSB at bottom
|
||||
data = bytes(0x80 if (x%4)<2 else 0x0 for x in range(128))
|
||||
pat = bytes(0x80 if (x%4)<2 else 0x0 for x in range(128))
|
||||
|
||||
if ckcc.is_simulator():
|
||||
# just show as static pattern
|
||||
t = self.dis.buffer[:-128] + data
|
||||
self.dis.write_data(t)
|
||||
else:
|
||||
self.write_cmds(setup)
|
||||
self.dis.write_data(data)
|
||||
self.write_cmds(animate)
|
||||
|
||||
def write_cmds(self, cmds):
|
||||
for c in cmds:
|
||||
self.dis.write_cmd(c)
|
||||
self.dis.busy_bar(True, pat)
|
||||
|
||||
def set_brightness(self, val):
|
||||
# normal = 0x7f, brightness=0xff, dim=0x00 (but they are all very similar)
|
||||
self.dis.write_cmd(0x81) # Set Contrast Control
|
||||
self.dis.write_cmd(val)
|
||||
return self.dis.contrast(val)
|
||||
|
||||
def menu_draw(self, ry, msg, is_sel, is_checked, space_indicators):
|
||||
# draw a menu item, perhaps selected, checked.
|
||||
@ -265,17 +230,18 @@ class Display:
|
||||
if is_sel:
|
||||
self.dis.fill_rect(0, y, Display.WIDTH, h-1, 1)
|
||||
self.icon(2, y, 'wedge', invert=1)
|
||||
self.text(x, y, msg, invert=1)
|
||||
nx = self.text(x, y, msg, invert=1)
|
||||
else:
|
||||
self.text(x, y, msg)
|
||||
nx = self.text(x, y, msg)
|
||||
|
||||
# LATER: removed because caused confusion w/ underscore
|
||||
#if msg[0] == ' ' and space_indicators:
|
||||
# see also graphics/mono/space.txt
|
||||
#self.icon(x-2, y+9, 'space', invert=is_sel)
|
||||
|
||||
if is_checked:
|
||||
self.icon(108, y, 'selected', invert=is_sel)
|
||||
if is_checked and nx <= 113:
|
||||
# omit checkmark if it doesn't fit
|
||||
self.icon(113, y, 'selected', invert=is_sel)
|
||||
|
||||
def menu_show(self, *a):
|
||||
self.show()
|
||||
@ -304,9 +270,14 @@ class Display:
|
||||
for ln in lines:
|
||||
if ln == 'EOT':
|
||||
self.hline(y+3)
|
||||
elif ln and ln[0] == '\x01':
|
||||
elif ln and ln[0] == OUT_CTRL_TITLE:
|
||||
self.text(0, y, ln[1:], FontLarge)
|
||||
y += 21
|
||||
elif ln and ln[0] == OUT_CTRL_ADDRESS:
|
||||
from utils import chunk_address
|
||||
fmt = '\u2009'.join(chunk_address(ln[1:]))
|
||||
self.text(14, y, fmt) # fixed indent, to be centered
|
||||
y += 15 # a bit extra vertical line height
|
||||
else:
|
||||
self.text(0, y, ln)
|
||||
|
||||
@ -322,13 +293,25 @@ class Display:
|
||||
# no status bar on Mk4
|
||||
return
|
||||
|
||||
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert):
|
||||
def draw_qr_error(self, idx_hint, msg):
|
||||
self.clear()
|
||||
lm = 4
|
||||
bw = 54
|
||||
y = (self.HEIGHT - bw) // 2
|
||||
# empty rectangle
|
||||
self.dis.fill_rect(lm, y, bw, bw, 1)
|
||||
self.dis.fill_rect(lm+1, y+1, bw-2, bw-2, 0)
|
||||
# error in rectangle - handpicked position
|
||||
self.text(lm+5,y+10, "QR too")
|
||||
self.text(lm+16,y+24, "big")
|
||||
self._draw_qr_display(bw, lm, msg, False, None, idx_hint, False)
|
||||
|
||||
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert,
|
||||
is_addr=False, force_msg=False, side_msg=None):
|
||||
# 'sidebar' is a pre-formated obj to show to right of QR -- oled life
|
||||
# - 'msg' will appear to right if very short, else under in tiny
|
||||
from utils import word_wrap
|
||||
|
||||
# - ignores "is_addr" because exactly zero space to do anything special
|
||||
self.clear()
|
||||
|
||||
w = qr_data.width()
|
||||
if w <= 29:
|
||||
# version 1,2,3 => we can double-up the pixels
|
||||
@ -368,13 +351,23 @@ class Display:
|
||||
gly = framebuf.FrameBuffer(bytearray(packed), w, w, framebuf.MONO_HLSB)
|
||||
self.dis.blit(gly, XO, YO, 1)
|
||||
|
||||
self._draw_qr_display(bw, lm, msg, is_alnum, sidebar, idx_hint, invert, is_addr, side_msg)
|
||||
|
||||
def _draw_qr_display(self, bw, lm, msg, is_alnum, sidebar, idx_hint, invert,
|
||||
is_addr=False, side_msg=None):
|
||||
# does not draw actual QR, but all other things in the screen
|
||||
from utils import word_wrap
|
||||
|
||||
if not sidebar and not msg:
|
||||
pass
|
||||
elif not sidebar and len(msg) > (5*7):
|
||||
elif not sidebar and ((len(msg) > (5*7)) or side_msg):
|
||||
# use FontTiny and word wrap (will just split if no spaces)
|
||||
# native segwit addresses and taproot
|
||||
# if 'side_msg' also p2pkh and p2sh fall into this category as space is needed for "CHANGE BACK" text
|
||||
x = bw + lm + 4
|
||||
ww = ((128 - x)//4) - 1 # char width avail
|
||||
y = 1
|
||||
|
||||
parts = list(word_wrap(msg, ww))
|
||||
if len(parts) > 8:
|
||||
parts = parts[:8]
|
||||
@ -385,9 +378,16 @@ class Display:
|
||||
for line in parts:
|
||||
self.text(x, y, line, FontTiny)
|
||||
y += 8
|
||||
|
||||
if side_msg and (len(side_msg) < 15):
|
||||
y_pos = y + 8
|
||||
# only render if there is space
|
||||
if (self.HEIGHT - y_pos) >= FontTiny.height:
|
||||
self.text(x+4, y+8, side_msg, FontTiny)
|
||||
else:
|
||||
# hand-positioned for known cases
|
||||
# - sidebar = (text, #of char per line)
|
||||
# p2pkh and p2sh addresses (if is_change=False)
|
||||
x, y = 73, (0 if is_alnum else 2)
|
||||
dy = 10 if is_alnum else 12
|
||||
sidebar, ll = sidebar if sidebar else (msg, 7)
|
||||
|
||||
@ -11,8 +11,8 @@ from ux import ux_show_story, ux_enter_bip32_index, the_ux, ux_confirm, ux_drama
|
||||
from menu import MenuItem, MenuSystem
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from ubinascii import b2a_base64
|
||||
from auth import write_sig_file
|
||||
from utils import chunk_writer, xfp2str, swab32
|
||||
from msgsign import write_sig_file
|
||||
from utils import xfp2str, swab32, node_from_privkey
|
||||
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
|
||||
|
||||
BIP85_PWD_LEN = 21
|
||||
@ -56,32 +56,32 @@ still backed-up.''')
|
||||
|
||||
def bip85_derive(picked, index):
|
||||
# implement the core step of BIP85 from our master secret
|
||||
|
||||
path = "m/83696968h/"
|
||||
if picked in (0,1,2):
|
||||
# BIP-39 seed phrases (we only support English)
|
||||
num_words = stash.SEED_LEN_OPTS[picked]
|
||||
width = (16, 24, 32)[picked] # of bytes
|
||||
path = "m/83696968h/39h/0h/{num_words}h/{index}h".format(num_words=num_words, index=index)
|
||||
path += "39h/0h/%dh/%dh" % (num_words, index)
|
||||
s_mode = 'words'
|
||||
elif picked == 3:
|
||||
# HDSeed for Bitcoin Core: but really a WIF of a private key, can be used anywhere
|
||||
# HDSeed for Bitcoin Core: but really a WIF of a private key
|
||||
s_mode = 'wif'
|
||||
path = "m/83696968h/2h/{index}h".format(index=index)
|
||||
path += "2h/%dh" % index
|
||||
width = 32
|
||||
elif picked == 4:
|
||||
# New XPRV
|
||||
path = "m/83696968h/32h/{index}h".format(index=index)
|
||||
path += "32h/%dh" % index
|
||||
s_mode = 'xprv'
|
||||
width = 64
|
||||
elif picked in (5, 6):
|
||||
width = 32 if picked == 5 else 64
|
||||
path = "m/83696968h/128169h/{width}h/{index}h".format(width=width, index=index)
|
||||
path += "128169h/%dh/%dh" % (width, index)
|
||||
s_mode = 'hex'
|
||||
elif picked == 7:
|
||||
width = 64
|
||||
# hardcoded width for now
|
||||
# b"pwd".hex() --> 707764
|
||||
path = "m/83696968h/707764h/{pwd_len}h/{index}h".format(pwd_len=BIP85_PWD_LEN, index=index)
|
||||
path += "707764h/%dh/%dh" % (BIP85_PWD_LEN, index)
|
||||
s_mode = 'pw'
|
||||
else:
|
||||
raise ValueError(picked)
|
||||
@ -124,8 +124,7 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
|
||||
|
||||
msg = "Password Index?" if picked == 7 else "Index Number?"
|
||||
index = await ux_enter_bip32_index(msg, unlimited=settings.get("b85max", False))
|
||||
if index is None:
|
||||
return
|
||||
if index is None: return
|
||||
|
||||
dis.fullscreen("Working...")
|
||||
new_secret, width, s_mode, path = bip85_derive(picked, index)
|
||||
@ -161,7 +160,7 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
|
||||
qr_alnum = True
|
||||
|
||||
msg = 'Seed words (%d):\n' % len(words)
|
||||
msg += ux_render_words(words)
|
||||
msg += ux_render_words(words, leading_blanks=1)
|
||||
|
||||
encoded = stash.SecretStash.encode(seed_phrase=new_secret)
|
||||
|
||||
@ -180,7 +179,7 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
|
||||
elif s_mode == 'xprv':
|
||||
# Raw XPRV value.
|
||||
ch, pk = new_secret[0:32], new_secret[32:64]
|
||||
master_node = ngu.hdnode.HDNode().from_chaincode_privkey(ch, pk)
|
||||
master_node = node_from_privkey(pk, ch)
|
||||
node = master_node
|
||||
|
||||
encoded = stash.SecretStash.encode(xprv=master_node)
|
||||
@ -205,14 +204,12 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
|
||||
if new_secret:
|
||||
msg += '\n\nRaw Entropy:\n' + str(b2a_hex(new_secret), 'ascii')
|
||||
|
||||
# Add the standard export prompt at the end, with extra (5) option sometimes.
|
||||
|
||||
key6 = 'to type %s over USB' % s_mode
|
||||
key0 = None
|
||||
if encoded is not None:
|
||||
key0 = 'to switch to derived secret'
|
||||
elif s_mode == 'pw':
|
||||
key0 = 'to type password over USB'
|
||||
prompt, escape = export_prompt_builder('data', key0=key0,
|
||||
|
||||
prompt, escape = export_prompt_builder('data', key0=key0, key6=key6,
|
||||
no_qr=(not qr), force_prompt=True)
|
||||
title = None
|
||||
if node:
|
||||
@ -224,14 +221,17 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
|
||||
ch = await ux_show_story(msg+'\n\n'+prompt, title=title, escape=escape,
|
||||
strict_escape=True, sensitive=True)
|
||||
choice = import_export_prompt_decode(ch)
|
||||
if isinstance(choice, dict):
|
||||
if choice == KEY_CANCEL:
|
||||
break
|
||||
elif isinstance(choice, dict):
|
||||
# write to SD card or Virtual Disk: simple text file
|
||||
dis.fullscreen("Saving...")
|
||||
try:
|
||||
with CardSlot(**choice) as card:
|
||||
fname, out_fn = card.pick_filename('drv-%s-idx%d.txt' % (s_mode, index))
|
||||
body = msg + "\n"
|
||||
with open(fname, 'wt') as fp:
|
||||
chunk_writer(fp, body)
|
||||
fp.write(body)
|
||||
|
||||
h = ngu.hash.sha256s(body.encode())
|
||||
sig_nice = write_sig_file([(h, fname)], derive=path)
|
||||
@ -240,37 +240,37 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
|
||||
await needs_microsd()
|
||||
continue
|
||||
except Exception as e:
|
||||
await ux_show_story('Failed to write!\n\n\n'+str(e))
|
||||
await ux_show_story('Failed to write!\n\n'+str(e))
|
||||
continue
|
||||
|
||||
story = "Filename is:\n\n%s" % out_fn
|
||||
story += "\n\nSignature filename is:\n\n%s" % sig_nice
|
||||
await ux_show_story(story, title='Saved')
|
||||
elif choice == KEY_CANCEL:
|
||||
break
|
||||
|
||||
elif choice == KEY_QR:
|
||||
from ux import show_qr_code
|
||||
await show_qr_code(qr, qr_alnum)
|
||||
elif choice == '0':
|
||||
if s_mode == 'pw':
|
||||
# gets confirmation then types it
|
||||
await single_send_keystrokes(qr, path)
|
||||
elif encoded is not None:
|
||||
# switch over to new secret!
|
||||
dis.fullscreen("Applying...")
|
||||
from actions import goto_top_menu
|
||||
from glob import settings
|
||||
xfp_str = xfp2str(settings.get("xfp", 0))
|
||||
await seed.set_ephemeral_seed(
|
||||
encoded,
|
||||
meta='BIP85 Derived from [%s], index=%d' % (xfp_str, index)
|
||||
)
|
||||
goto_top_menu()
|
||||
break
|
||||
await show_qr_code(qr, qr_alnum, is_secret=True)
|
||||
|
||||
elif (choice == '0') and (encoded is not None):
|
||||
# switch over to new secret!
|
||||
dis.fullscreen("Applying...")
|
||||
from actions import goto_top_menu
|
||||
from glob import settings
|
||||
xfp_str = xfp2str(settings.get("xfp", 0))
|
||||
await seed.set_ephemeral_seed(
|
||||
encoded,
|
||||
origin='BIP85 Derived from [%s], index=%d' % (xfp_str, index)
|
||||
)
|
||||
goto_top_menu()
|
||||
break
|
||||
|
||||
elif choice == "6":
|
||||
# gets confirmation then types it
|
||||
await single_send_keystrokes(qr, path)
|
||||
|
||||
elif NFC and choice == KEY_NFC:
|
||||
# Share any of these over NFC
|
||||
await NFC.share_text(qr)
|
||||
await NFC.share_text(qr, is_secret=True)
|
||||
|
||||
stash.blank_object(msg)
|
||||
stash.blank_object(new_secret)
|
||||
@ -291,7 +291,7 @@ async def password_entry(*args, **kwargs):
|
||||
|
||||
while True:
|
||||
the_ux.pop()
|
||||
index = await ux_enter_bip32_index("Password Index?", can_cancel=True)
|
||||
index = await ux_enter_bip32_index("Password Index?")
|
||||
if index is None:
|
||||
break
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
266
shared/export.py
266
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
|
||||
from ux import ux_show_story, import_export_prompt
|
||||
from glob import settings
|
||||
from auth import write_sig_file
|
||||
from msgsign import write_sig_file
|
||||
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH
|
||||
from charcodes import KEY_NFC, KEY_CANCEL, KEY_QR
|
||||
from ownership import OWNERSHIP
|
||||
from exceptions import QRTooBigError
|
||||
|
||||
async def export_by_qr(body, label, type_code):
|
||||
async def export_by_qr(body, label, type_code, force_bbqr=False):
|
||||
# render as QR and show on-screen
|
||||
from ux import show_qr_code
|
||||
|
||||
try:
|
||||
# ignore label/title - provides no useful info
|
||||
# makes qr smaller and harder to read
|
||||
if force_bbqr or len(body) > 2000:
|
||||
raise QRTooBigError
|
||||
|
||||
await show_qr_code(body)
|
||||
except (ValueError, RuntimeError, TypeError):
|
||||
except QRTooBigError:
|
||||
if version.has_qwerty:
|
||||
# do BBQr on Q
|
||||
from ux_q1 import show_bbqr_codes
|
||||
@ -31,6 +33,79 @@ async def export_by_qr(body, label, type_code):
|
||||
|
||||
return
|
||||
|
||||
|
||||
async def export_contents(title, contents, fname_pattern, derive=None, addr_fmt=None,
|
||||
is_json=False, force_bbqr=False, force_prompt=False, direct_way=None,
|
||||
intro="", footer="", ux_title=None):
|
||||
# export text and json files while offering NFC, QR & Vdisk
|
||||
# produces signed export in case of SD/Vdisk (signed with key at deriv and addr_fmt)
|
||||
# checks if suitable to offer QR export on Mk4
|
||||
# argument contents can support function that generates content
|
||||
# argument direct way can be KEY_{NFC,QR}, any other truth value is SD/Vdisk,
|
||||
# if None ask for way via UX story
|
||||
from glob import dis, NFC, VD
|
||||
from files import CardSlot, CardMissingError, needs_microsd
|
||||
from qrs import MAX_V11_CHAR_LIMIT
|
||||
|
||||
if callable(contents):
|
||||
dis.fullscreen('Generating...')
|
||||
contents, derive, addr_fmt = contents()
|
||||
|
||||
# figure out if offering QR code export make sense given HW
|
||||
# len() is O(1)
|
||||
no_qr = not version.has_qwerty and (len(contents) >= MAX_V11_CHAR_LIMIT)
|
||||
|
||||
sig = not (derive is None and addr_fmt is None)
|
||||
|
||||
ch = direct_way # set it to direct way only once, outside the loop
|
||||
while True:
|
||||
if direct_way is None:
|
||||
ch = await import_export_prompt("%s file" % title, intro=intro, footnotes=footer,
|
||||
force_prompt=force_prompt, no_qr=no_qr, title=ux_title)
|
||||
if ch == KEY_CANCEL:
|
||||
break
|
||||
elif ch == KEY_QR:
|
||||
await export_by_qr(contents, title, "J" if is_json else "U", force_bbqr=force_bbqr)
|
||||
elif ch == KEY_NFC:
|
||||
if is_json:
|
||||
await NFC.share_json(contents)
|
||||
else:
|
||||
await NFC.share_text(contents)
|
||||
else:
|
||||
# SD/VDisk
|
||||
# choose a filename
|
||||
try:
|
||||
dis.fullscreen("Saving...")
|
||||
with CardSlot(**ch) as card:
|
||||
fname, nice = card.pick_filename(fname_pattern)
|
||||
|
||||
# do actual write
|
||||
with open(fname, 'wt' if is_json else 'wb') as fd:
|
||||
fd.write(contents)
|
||||
|
||||
if sig:
|
||||
h = ngu.hash.sha256s(contents.encode())
|
||||
sig_nice = write_sig_file([(h, fname)], derive, addr_fmt)
|
||||
|
||||
msg = '%s file written:\n\n%s' % (title, nice)
|
||||
if sig:
|
||||
msg += "\n\n%s signature file written:\n\n%s" % (title, sig_nice)
|
||||
|
||||
await ux_show_story(msg)
|
||||
|
||||
except CardMissingError:
|
||||
await needs_microsd()
|
||||
except Exception as e:
|
||||
await ux_show_story('Failed to write!\n\n' + str(e))
|
||||
|
||||
# both exceptions & success gets here
|
||||
if no_qr and (NFC is None) and (VD is None) and not force_prompt:
|
||||
# user has no other ways enabled, we already exported to SD - done
|
||||
return
|
||||
|
||||
if direct_way:
|
||||
return
|
||||
|
||||
def generate_public_contents():
|
||||
# Generate public details about wallet.
|
||||
#
|
||||
@ -73,14 +148,7 @@ be needed for different systems.
|
||||
sym=chain.ctype, ct=chain.b44_cointype, xfp=xfp))
|
||||
|
||||
for name, path, addr_fmt in chains.CommonDerivations:
|
||||
|
||||
if '{coin_type}' in path:
|
||||
path = path.replace('{coin_type}', str(chain.b44_cointype))
|
||||
|
||||
if '{' in name:
|
||||
name = name.format(core_name=chain.core_name)
|
||||
|
||||
show_slip132 = ('Core' not in name)
|
||||
path = path.replace('{coin_type}', str(chain.b44_cointype))
|
||||
|
||||
yield ('''## For {name}: {path}\n\n'''.format(name=name, path=path))
|
||||
yield ('''First %d receive addresses (account=0, change=0):\n\n''' % num_rx)
|
||||
@ -103,7 +171,7 @@ be needed for different systems.
|
||||
|
||||
node = sv.derive_path(hard_sub, register=False)
|
||||
yield ("%s => %s\n" % (hard_sub, chain.serialize_public(node)))
|
||||
if show_slip132 and addr_fmt != AF_CLASSIC and (addr_fmt in chain.slip132):
|
||||
if addr_fmt != AF_CLASSIC and (addr_fmt in chain.slip132):
|
||||
yield ("%s => %s ##SLIP-132##\n" % (
|
||||
hard_sub, chain.serialize_public(node, addr_fmt)))
|
||||
|
||||
@ -133,46 +201,6 @@ be needed for different systems.
|
||||
yield fp.getvalue()
|
||||
del fp
|
||||
|
||||
async def write_text_file(fname_pattern, body, title, derive, addr_fmt):
|
||||
# Export data as a text file.
|
||||
from glob import dis, NFC
|
||||
from files import CardSlot, CardMissingError, needs_microsd
|
||||
from ux import import_export_prompt
|
||||
|
||||
choice = await import_export_prompt("%s file" % title, is_import=False,
|
||||
no_qr=(not version.has_qwerty))
|
||||
if choice == KEY_CANCEL:
|
||||
return
|
||||
elif choice == KEY_QR:
|
||||
await export_by_qr(body, title, "U")
|
||||
return
|
||||
elif choice == KEY_NFC:
|
||||
await NFC.share_text(body)
|
||||
return
|
||||
|
||||
# choose a filename
|
||||
try:
|
||||
dis.fullscreen("Saving...")
|
||||
with CardSlot(**choice) as card:
|
||||
fname, nice = card.pick_filename(fname_pattern)
|
||||
|
||||
# do actual write
|
||||
with open(fname, 'wb') as fd:
|
||||
chunk_writer(fd, body)
|
||||
|
||||
h = ngu.hash.sha256s(body.encode())
|
||||
sig_nice = write_sig_file([(h, fname)], derive, addr_fmt)
|
||||
|
||||
except CardMissingError:
|
||||
await needs_microsd()
|
||||
return
|
||||
except Exception as e:
|
||||
await ux_show_story('Failed to write!\n\n\n'+str(e))
|
||||
return
|
||||
|
||||
msg = '%s file written:\n\n%s\n\n%s signature file written:\n\n%s' % (title, nice, title,
|
||||
sig_nice)
|
||||
await ux_show_story(msg)
|
||||
|
||||
async def make_summary_file(fname_pattern='public.txt'):
|
||||
from glob import dis
|
||||
@ -183,7 +211,7 @@ async def make_summary_file(fname_pattern='public.txt'):
|
||||
# generator function:
|
||||
body = "".join(list(generate_public_contents()))
|
||||
ch = chains.current_chain()
|
||||
await write_text_file(fname_pattern, body, 'Summary',
|
||||
await export_contents('Summary', body, fname_pattern,
|
||||
"m/44h/%dh/0h/0/0" % ch.b44_cointype,
|
||||
AF_CLASSIC)
|
||||
|
||||
@ -239,7 +267,7 @@ importmulti '{imp_multi}'
|
||||
|
||||
ch = chains.current_chain()
|
||||
derive = "84h/{coin_type}h/{account}h".format(account=account_num, coin_type=ch.b44_cointype)
|
||||
await write_text_file(fname_pattern, body, 'Bitcoin Core', derive + "/0/0", AF_P2WPKH)
|
||||
await export_contents('Bitcoin Core', body, fname_pattern, derive + "/0/0", AF_P2WPKH)
|
||||
|
||||
def generate_bitcoin_core_wallet(account_num, example_addrs):
|
||||
# Generate the data for an RPC command to import keys into Bitcoin Core
|
||||
@ -319,20 +347,16 @@ def generate_unchained_export(account_num=0):
|
||||
# - no account numbers (at this level)
|
||||
|
||||
chain = chains.current_chain()
|
||||
todo = [
|
||||
( "m/48h/{coin}h/{acct_num}h/2h", 'p2wsh', AF_P2WSH ),
|
||||
( "m/48h/{coin}h/{acct_num}h/1h", 'p2sh_p2wsh', AF_P2WSH_P2SH),
|
||||
( "m/45h", 'p2sh', AF_P2SH), # if acct_num == 0
|
||||
]
|
||||
|
||||
xfp = xfp2str(settings.get('xfp', 0))
|
||||
rv = OrderedDict(xfp=xfp, account=account_num)
|
||||
|
||||
sign_der = None
|
||||
with stash.SensitiveValues() as sv:
|
||||
for deriv, name, fmt in todo:
|
||||
for name, deriv, fmt in chains.MS_STD_DERIVATIONS:
|
||||
if fmt == AF_P2SH and account_num:
|
||||
continue
|
||||
dd = deriv.format(coin=chain.b44_cointype, acct_num=account_num)
|
||||
if fmt == AF_P2WSH:
|
||||
sign_der = dd + "/0/0"
|
||||
node = sv.derive_path(dd)
|
||||
xp = chain.serialize_public(node, fmt)
|
||||
|
||||
@ -341,9 +365,7 @@ def generate_unchained_export(account_num=0):
|
||||
rv['%s_deriv' % name] = dd
|
||||
rv[name] = xp
|
||||
|
||||
# sig_deriv = "m/44'/{ct}'/{acc}'".format(ct=chain.b44_cointype, acc=account_num) + "/0/0"
|
||||
# return ujson.dumps(rv), sig_deriv, AF_CLASSIC
|
||||
return ujson.dumps(rv), False, False
|
||||
return ujson.dumps(rv), sign_der, AF_CLASSIC
|
||||
|
||||
def generate_generic_export(account_num=0):
|
||||
# Generate data that other programers will use to import Coldcard (single-signer)
|
||||
@ -403,22 +425,15 @@ def generate_generic_export(account_num=0):
|
||||
def generate_electrum_wallet(addr_type, account_num):
|
||||
# Generate line-by-line JSON details about wallet.
|
||||
#
|
||||
# Much reverse enginerring of Electrum here. It's a complex
|
||||
# Much reverse engineering of Electrum here. It's a complex
|
||||
# legacy file format.
|
||||
|
||||
chain = chains.current_chain()
|
||||
|
||||
xfp = settings.get('xfp')
|
||||
|
||||
# Must get the derivation path, and the SLIP32 version bytes right!
|
||||
if addr_type == AF_CLASSIC:
|
||||
mode = 44
|
||||
elif addr_type == AF_P2WPKH:
|
||||
mode = 84
|
||||
elif addr_type == AF_P2WPKH_P2SH:
|
||||
mode = 49
|
||||
else:
|
||||
raise ValueError(addr_type)
|
||||
# Must get the derivation path, and the SLIP132 version bytes right!
|
||||
mode = chains.af_to_bip44_purpose(addr_type)
|
||||
|
||||
OWNERSHIP.note_wallet_used(addr_type, account_num)
|
||||
|
||||
@ -448,80 +463,19 @@ def generate_electrum_wallet(addr_type, account_num):
|
||||
|
||||
return ujson.dumps(rv), derive + "/0/0", addr_type
|
||||
|
||||
async def make_json_wallet(label, func, fname_pattern='new-wallet.json'):
|
||||
# Record **public** values and helpful data into a JSON file
|
||||
# - OWNERSHIP.note_wallet_used(..) should be called already by our caller or func
|
||||
|
||||
from glob import dis, NFC
|
||||
from files import CardSlot, CardMissingError, needs_microsd
|
||||
from ux import import_export_prompt
|
||||
from qrs import MAX_V11_CHAR_LIMIT
|
||||
|
||||
dis.fullscreen('Generating...')
|
||||
json_str, derive, addr_fmt = func()
|
||||
skip_sig = derive is False and addr_fmt is False
|
||||
|
||||
choice = await import_export_prompt("%s file" % label, is_import=False,
|
||||
no_qr=(not version.has_qwerty and len(json_str) >= MAX_V11_CHAR_LIMIT))
|
||||
|
||||
if choice == KEY_CANCEL:
|
||||
return
|
||||
elif choice == KEY_NFC:
|
||||
await NFC.share_json(json_str)
|
||||
return
|
||||
elif choice == KEY_QR:
|
||||
# render as QR and show on-screen
|
||||
# - on mk4, this isn't offered if more than about 300 bytes because we can't
|
||||
# show that as a single QR
|
||||
await export_by_qr(json_str, label, "J")
|
||||
return
|
||||
|
||||
# choose a filename and save
|
||||
try:
|
||||
with CardSlot(**choice) as card:
|
||||
fname, nice = card.pick_filename(fname_pattern)
|
||||
|
||||
# do actual write
|
||||
with open(fname, 'wt') as fd:
|
||||
chunk_writer(fd, json_str)
|
||||
|
||||
if not skip_sig:
|
||||
h = ngu.hash.sha256s(json_str.encode())
|
||||
sig_nice = write_sig_file([(h, fname)], derive, addr_fmt)
|
||||
|
||||
except CardMissingError:
|
||||
await needs_microsd()
|
||||
return
|
||||
except Exception as e:
|
||||
await ux_show_story('Failed to write!\n\n\n'+str(e))
|
||||
return
|
||||
|
||||
msg = '%s file written:\n\n%s' % (label, nice)
|
||||
if not skip_sig:
|
||||
msg += '\n\n%s signature file written:\n\n%s' % (label, sig_nice)
|
||||
|
||||
await ux_show_story(msg)
|
||||
|
||||
|
||||
async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int_ext=True,
|
||||
fname_pattern="descriptor.txt"):
|
||||
fname_pattern="descriptor.txt", direct_way=None):
|
||||
from descriptor import Descriptor
|
||||
from glob import dis
|
||||
|
||||
dis.fullscreen('Generating...')
|
||||
chain = chains.current_chain()
|
||||
|
||||
xfp = settings.get('xfp')
|
||||
xfp = settings.get('xfp', 0)
|
||||
dis.progress_bar_show(0.1)
|
||||
if mode is None:
|
||||
if addr_type == AF_CLASSIC:
|
||||
mode = 44
|
||||
elif addr_type == AF_P2WPKH:
|
||||
mode = 84
|
||||
elif addr_type == AF_P2WPKH_P2SH:
|
||||
mode = 49
|
||||
else:
|
||||
raise ValueError(addr_type)
|
||||
mode = chains.af_to_bip44_purpose(addr_type)
|
||||
|
||||
OWNERSHIP.note_wallet_used(addr_type, account_num)
|
||||
|
||||
@ -547,7 +501,31 @@ async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int
|
||||
)
|
||||
|
||||
dis.progress_bar_show(1)
|
||||
await write_text_file(fname_pattern, body, "Descriptor", derive + "/0/0", addr_type)
|
||||
|
||||
intro, footer = (body, "") if version.has_qwerty else ("", body)
|
||||
title = "Descriptor"
|
||||
await export_contents(title, body, fname_pattern, derive + "/0/0", addr_type,
|
||||
force_prompt=True, direct_way=direct_way, intro=intro, footer=footer,
|
||||
ux_title=title if version.has_qwerty else None)
|
||||
|
||||
|
||||
async def make_key_expression_export(orig_der, addr_fmt=AF_CLASSIC, fname_pattern="key_expr.txt"):
|
||||
from glob import dis
|
||||
|
||||
dis.fullscreen('Generating...')
|
||||
|
||||
xfp = xfp2str(settings.get('xfp', 0)).lower()
|
||||
|
||||
with stash.SensitiveValues() as sv:
|
||||
ek = chains.current_chain().serialize_public(sv.derive_path(orig_der))
|
||||
|
||||
body = "[%s/%s]%s" % (xfp, orig_der.replace("m/", ""), ek)
|
||||
|
||||
intro, footer = (body, "") if version.has_qwerty else ("", body)
|
||||
title = "Key Expression"
|
||||
await export_contents(title, body, fname_pattern, orig_der + "/0/0", addr_fmt,
|
||||
force_prompt=True, intro=intro, footer=footer,
|
||||
ux_title=title if version.has_qwerty else None)
|
||||
|
||||
# EOF
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
210
shared/flow.py
210
shared/flow.py
@ -19,6 +19,8 @@ from countdowns import countdown_chooser
|
||||
from paper import make_paper_wallet
|
||||
from trick_pins import TrickPinMenu
|
||||
from tapsigner import import_tapsigner_backup_file
|
||||
from ccc import toggle_ccc_feature, sssp_spending_policy, sssp_feature_menu
|
||||
from wif import WIFStoreMenu
|
||||
|
||||
# useful shortcut keys
|
||||
from charcodes import KEY_QR, KEY_NFC
|
||||
@ -38,12 +40,14 @@ if version.has_battery:
|
||||
from battery import battery_idle_timeout_chooser, brightness_chooser
|
||||
from q1 import scan_and_bag
|
||||
from notes import make_notes_menu
|
||||
from teleport import kt_start_rx, kt_send_file_psbt
|
||||
else:
|
||||
battery_idle_timeout_chooser = None
|
||||
brightness_chooser = None
|
||||
scan_and_bag = None
|
||||
make_notes_menu = None
|
||||
|
||||
kt_start_rx = None
|
||||
kt_send_file_psbt = None
|
||||
|
||||
#
|
||||
# NOTE: "Always In Title Case"
|
||||
@ -69,6 +73,8 @@ def has_secrets():
|
||||
from pincodes import pa
|
||||
return pa.has_secrets()
|
||||
|
||||
qr_and_has_secrets = has_secrets if version.has_qr else False
|
||||
|
||||
def nfc_enabled():
|
||||
from glob import NFC
|
||||
return bool(NFC)
|
||||
@ -95,6 +101,33 @@ def hsm_available():
|
||||
# contains hsm feature + can it be used (needs se2 secret and no tmp active)
|
||||
return version.supports_hsm and has_real_secret()
|
||||
|
||||
def qr_and_ms():
|
||||
# has QR scanner, and at least one MS wallet
|
||||
if not version.has_qr: return False
|
||||
return bool(settings.get('multisig', False))
|
||||
|
||||
def has_pushtx_url():
|
||||
# they want to use PushTX feature
|
||||
return bool(settings.get("ptxurl", False))
|
||||
|
||||
# Spending Policy (Hobbled mode) predicates.
|
||||
#
|
||||
def is_hobble_testdrive():
|
||||
from pincodes import pa
|
||||
return (pa.hobbled_mode == 2)
|
||||
|
||||
def sssp_related_keys():
|
||||
return sssp_spending_policy('okeys')
|
||||
|
||||
def sssp_allow_passphrase():
|
||||
return word_based_seed() and sssp_related_keys()
|
||||
|
||||
def sssp_allow_notes():
|
||||
return settings.get("secnap", False) and sssp_spending_policy('notes')
|
||||
|
||||
def sssp_allow_vault():
|
||||
return settings.master_get('seedvault') and sssp_related_keys()
|
||||
|
||||
async def goto_home(*a):
|
||||
goto_top_menu()
|
||||
|
||||
@ -132,12 +165,27 @@ LoginPrefsMenu = [
|
||||
MenuItem('Test Login Now', f=login_now, arg=1),
|
||||
]
|
||||
|
||||
|
||||
# obscure settings, not more dangerous, just more personal
|
||||
BuriedSettingsMenu = [
|
||||
ToggleMenuItem('Home Menu XFP', 'hmx', ['Only Tmp', 'Always Show'],
|
||||
story=('Forces display of XFP (seed fingerprint) '
|
||||
'at top of main menu. Normally, XFP is shown only when '
|
||||
'temporary seed is active.\n\n'
|
||||
'Master seed is displayed as <XFP>, temporary seeds as [XFP].'),
|
||||
predicate=has_real_secret,
|
||||
on_change=goto_home),
|
||||
ToggleMenuItem('Menu Wrapping', 'wa', ['Default', 'Always Wrap'],
|
||||
story='''When enabled, allows scrolling past menu top/bottom \
|
||||
(wrap around). By default, this only happens in menus whose length is greater than 10.'''),
|
||||
]
|
||||
|
||||
SettingsMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem('Login Settings', menu=LoginPrefsMenu),
|
||||
MenuItem('Hardware On/Off', menu=HWTogglesMenu),
|
||||
NonDefaultMenuItem('Multisig Wallets', 'multisig',
|
||||
menu=make_multisig_menu, predicate=has_secrets),
|
||||
menu=make_multisig_menu, predicate=has_secrets, shortcut='m'),
|
||||
NonDefaultMenuItem('NFC Push Tx', 'ptxurl', menu=pushtx_setup_menu),
|
||||
MenuItem('Display Units', chooser=value_resolution_chooser),
|
||||
MenuItem('Max Network Fee', chooser=max_fee_chooser),
|
||||
@ -154,52 +202,48 @@ The signed transaction will be named <TXID>.txn, so the file name does not leak
|
||||
MS-DOS tools should not be able to find the PSBT data (ie. undelete), but forensic tools \
|
||||
which take apart the flash chips of the SDCard may still be able to find the \
|
||||
data or filenames.'''),
|
||||
ToggleMenuItem('Menu Wrapping', 'wa', ['Default Off', 'Enable'],
|
||||
story='''When enabled, allows scrolling past menu top/bottom \
|
||||
(wrap around). By default, this is only happens in very large menus.'''),
|
||||
ToggleMenuItem('Home Menu XFP', 'hmx', ['Only Tmp', 'Always Show'],
|
||||
story=('Forces display of XFP (seed fingerprint) '
|
||||
'at top of main menu. Normally, XFP is shown only when '
|
||||
'temporary seed is active.\n\n'
|
||||
'Master seed is displayed as <XFP>, temporary seeds as [XFP].'),
|
||||
predicate=has_real_secret,
|
||||
on_change=goto_home),
|
||||
ToggleMenuItem('Keyboard EMU', 'emu', ['Default Off', 'Enable'],
|
||||
on_change=usb_keyboard_emulation,
|
||||
predicate=has_secrets, # cannot generate BIP85 passwords without secret
|
||||
story='''This mode adds a top-level menu item for typing \
|
||||
deterministically-generated passwords (BIP-85), directly into an \
|
||||
attached USB computer (as an emulated keyboard).'''),
|
||||
MenuItem('Buried Settings', menu=BuriedSettingsMenu),
|
||||
]
|
||||
|
||||
XpubExportMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem("Segwit (BIP-84)", f=export_xpub, arg=84),
|
||||
MenuItem("Classic (BIP-44)", f=export_xpub, arg=44),
|
||||
MenuItem("P2WPKH/P2SH (49)", f=export_xpub, arg=49),
|
||||
MenuItem("P2WPKH/P2SH "+("(BIP-49)"if version.has_qwerty else "(49)"), f=export_xpub, arg=49),
|
||||
MenuItem("Master XPUB", f=export_xpub, arg=0),
|
||||
MenuItem("Current XFP", f=export_xpub, arg=-1),
|
||||
]
|
||||
|
||||
WalletExportMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem("Sparrow", f=named_generic_skeleton, arg="Sparrow"),
|
||||
MenuItem("Cove", f=named_generic_skeleton, arg="Cove"),
|
||||
MenuItem("Bitcoin Core", f=bitcoin_core_skeleton),
|
||||
MenuItem("Fully Noded", f=named_generic_skeleton, arg="Fully Noded"),
|
||||
MenuItem("Sparrow Wallet", f=named_generic_skeleton, arg="Sparrow"),
|
||||
MenuItem("Nunchuk", f=named_generic_skeleton, arg="Nunchuk"),
|
||||
MenuItem("Zeus", f=ss_descriptor_skeleton,
|
||||
arg=(True, [AF_P2WPKH, AF_P2WPKH_P2SH], "Zeus Wallet", "zeus-export.txt")),
|
||||
MenuItem("Electrum Wallet", f=electrum_skeleton),
|
||||
MenuItem("Theya", f=named_generic_skeleton, arg="Theya"),
|
||||
MenuItem("Bull Bitcoin", f=ss_descriptor_skeleton,
|
||||
arg=(True, [AF_P2WPKH], "", "bull-bitcoin.txt", KEY_QR)),
|
||||
MenuItem("Blue Wallet", f=electrum_skeleton, arg="Blue"),
|
||||
MenuItem("Electrum Wallet", f=electrum_skeleton, arg="Electrum"),
|
||||
MenuItem("Wasabi Wallet", f=wasabi_skeleton),
|
||||
MenuItem("Fully Noded", f=named_generic_skeleton, arg="Fully Noded"),
|
||||
MenuItem("Unchained", f=unchained_capital_export),
|
||||
MenuItem("Lily Wallet", f=named_generic_skeleton, arg="Lily"),
|
||||
MenuItem("Theya", f=named_generic_skeleton, arg="Theya"),
|
||||
MenuItem("Bitcoin Safe", f=named_generic_skeleton, arg="Bitcoin Safe"),
|
||||
MenuItem("Zeus", f=ss_descriptor_skeleton,
|
||||
arg=(True, [AF_P2WPKH, AF_P2WPKH_P2SH], "Zeus Wallet", "zeus-export.txt", None)),
|
||||
MenuItem("Samourai Postmix", f=samourai_post_mix_descriptor_export),
|
||||
MenuItem("Samourai Premix", f=samourai_pre_mix_descriptor_export),
|
||||
# MenuItem("Samourai BadBank", f=samourai_bad_bank_descriptor_export), # not released yet
|
||||
MenuItem("Descriptor", f=ss_descriptor_skeleton),
|
||||
MenuItem("Generic JSON", f=generic_skeleton),
|
||||
MenuItem("Export XPUB", menu=XpubExportMenu),
|
||||
MenuItem("Key Expression", f=key_expression_skeleton),
|
||||
MenuItem("Dump Summary", predicate=has_secrets, f=dump_summary),
|
||||
]
|
||||
|
||||
@ -211,9 +255,11 @@ FileMgmtMenu = [
|
||||
MenuItem('Export Wallet', predicate=has_secrets, menu=WalletExportMenu), #dup elsewhere
|
||||
MenuItem('Sign Text File', predicate=has_secrets, f=sign_message_on_sd),
|
||||
MenuItem('Batch Sign PSBT', predicate=has_secrets, f=batch_sign),
|
||||
MenuItem('Teleport Multisig PSBT', predicate=qr_and_has_secrets, f=kt_send_file_psbt),
|
||||
MenuItem('List Files', f=list_files),
|
||||
MenuItem('Verify Sig File', f=verify_sig_file),
|
||||
MenuItem('NFC File Share', predicate=nfc_enabled, f=nfc_share_file, shortcut=KEY_NFC),
|
||||
MenuItem('BBQr File Share', predicate=version.has_qr, f=qr_share_file, arg=True),
|
||||
MenuItem('QR File Share', predicate=version.has_qr, f=qr_share_file, shortcut=KEY_QR),
|
||||
MenuItem('Clone Coldcard', predicate=has_secrets, f=clone_write_data),
|
||||
MenuItem('Format SD Card', f=wipe_sd_card),
|
||||
@ -231,7 +277,9 @@ DevelopersMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem("Serial REPL", f=dev_enable_repl),
|
||||
MenuItem('Warm Reset', f=reset_self),
|
||||
MenuItem("Restore Txt Bkup", f=restore_everything_cleartext),
|
||||
MenuItem("Restore Bkup", f=restore_backup_dev),
|
||||
MenuItem("BKPW Override", menu=bkpw_override, predicate=has_secrets),
|
||||
MenuItem('Reflash GPU', f=reflash_gpu, predicate=version.has_qwerty),
|
||||
]
|
||||
|
||||
AdvancedVirginMenu = [ # No PIN, no secrets yet (factory fresh)
|
||||
@ -248,6 +296,7 @@ AdvancedPinnedVirginMenu = [ # Has PIN but no secrets yet
|
||||
MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu),
|
||||
MenuItem("Upgrade Firmware", menu=UpgradeMenu, predicate=is_not_tmp),
|
||||
MenuItem("File Management", menu=FileMgmtMenu),
|
||||
MenuItem("Key Teleport (start)", f=kt_start_rx, predicate=version.has_qr),
|
||||
MenuItem('Paper Wallets', f=make_paper_wallet),
|
||||
MenuItem('Perform Selftest', f=start_selftest),
|
||||
MenuItem("I Am Developer.", menu=maybe_dev_menu),
|
||||
@ -258,6 +307,8 @@ DebugFunctionsMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem("Keyboard Test", f=keyboard_test),
|
||||
MenuItem('BBQr Demo', f=debug_bbqr_test, predicate=version.has_qwerty),
|
||||
MenuItem("NFC Test", f=quick_nfc_test),
|
||||
MenuItem('Clear Tested', f=clear_tested_flag),
|
||||
MenuItem('Debug: assert', f=debug_assert),
|
||||
MenuItem('Debug: except', f=debug_except),
|
||||
MenuItem('Check: BL FW', f=check_firewall_read),
|
||||
@ -291,7 +342,7 @@ DangerZoneMenu = [
|
||||
"WARNING: Seed Vault is encrypted (AES-256-CTR) by your seed,"
|
||||
" but not held directly inside secure elements. Backups are required"
|
||||
" after any change to vault! Recommended for experiments or temporary use."),
|
||||
predicate=has_se_secrets),
|
||||
predicate=has_real_secret),
|
||||
MenuItem('Perform Selftest', f=start_selftest), # little harmful
|
||||
MenuItem("Set High-Water", f=set_highwater),
|
||||
MenuItem('Wipe HSM Policy', f=wipe_hsm_policy, predicate=hsm_policy_available),
|
||||
@ -304,7 +355,7 @@ If you disable sighash flag restrictions, and ignore the \
|
||||
warnings, funds can be stolen by specially crafted PSBT or MitM.
|
||||
|
||||
Keep blocked unless you intend to sign special transactions.'''),
|
||||
ToggleMenuItem('Testnet Mode', 'chain', ['Bitcoin', 'Testnet3', 'Regtest'],
|
||||
ToggleMenuItem('Testnet Mode', 'chain', ['Bitcoin', 'Testnet4', 'Regtest'],
|
||||
value_map=['BTC', 'XTN', 'XRT'],
|
||||
on_change=change_which_chain,
|
||||
story="Testnet must only be used by developers because \
|
||||
@ -323,15 +374,15 @@ correctly- crafted transactions signed on Testnet could be broadcast on Mainnet.
|
||||
MenuItem('Settings Space', f=show_settings_space),
|
||||
MenuItem('MCU Key Slots', f=show_mcu_keys_left),
|
||||
MenuItem('Bless Firmware', f=bless_flash), # no need for this anymore?
|
||||
MenuItem('Reflash GPU', f=reflash_gpu, predicate=version.has_qwerty),
|
||||
MenuItem("Wipe LFS", f=wipe_filesystem), # kills other-seed settings, HSM stuff, addr cache
|
||||
MenuItem("Nuke Device", f=nuke_device),
|
||||
]
|
||||
|
||||
BackupStuffMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem("Backup System", f=backup_everything),
|
||||
MenuItem("Verify Backup", f=verify_backup),
|
||||
MenuItem("Restore Backup", f=restore_everything), # just a redirect really
|
||||
MenuItem("Restore Backup", f=need_clear_seed), # just a UX msg really
|
||||
MenuItem('Clone Coldcard', predicate=has_secrets, f=clone_write_data),
|
||||
]
|
||||
|
||||
@ -343,7 +394,20 @@ NFCToolsMenu = [
|
||||
MenuItem('Verify Address', f=nfc_address_verify),
|
||||
MenuItem('File Share', f=nfc_share_file),
|
||||
MenuItem('Import Multisig', f=import_multisig_nfc),
|
||||
MenuItem('Push Transaction', f=nfc_pushtx_file, predicate=lambda: settings.get("ptxurl", False)),
|
||||
MenuItem('Push Transaction', f=nfc_pushtx_file, predicate=has_pushtx_url),
|
||||
]
|
||||
|
||||
|
||||
SpendingPolicySubMenu = [
|
||||
NonDefaultMenuItem('Single-Signer', 'sssp', f=sssp_feature_menu, predicate=has_real_secret),
|
||||
NonDefaultMenuItem('Co-Sign Multi.' if not version.has_qwerty else 'Co-Sign Multisig (CCC)',
|
||||
'ccc', f=toggle_ccc_feature, predicate=is_not_tmp),
|
||||
ToggleMenuItem('HSM Mode', 'hsmcmd', ['Default Off', 'Enable'],
|
||||
story=("Enable HSM? Enables all user management commands, and other HSM-only USB commands. "
|
||||
"By default these commands are disabled."),
|
||||
predicate=hsm_available),
|
||||
MenuItem('User Management', menu=make_users_menu,
|
||||
predicate=lambda: hsm_available() and settings.get('hsmcmd', False)),
|
||||
]
|
||||
|
||||
AdvancedNormalMenu = [
|
||||
@ -352,19 +416,16 @@ AdvancedNormalMenu = [
|
||||
MenuItem('Export Wallet', predicate=has_secrets, menu=WalletExportMenu, shortcut='x'), # also inside FileMgmt
|
||||
MenuItem("Upgrade Firmware", menu=UpgradeMenu, predicate=is_not_tmp),
|
||||
MenuItem("File Management", menu=FileMgmtMenu),
|
||||
NonDefaultMenuItem('Secure Notes & Passwords', 'notes', menu=make_notes_menu,
|
||||
NonDefaultMenuItem('Secure Notes & Passwords', 'secnap', menu=make_notes_menu,
|
||||
predicate=version.has_qwerty),
|
||||
MenuItem('Derive Seed B85' if not version.has_qwerty else 'Derive Seeds (BIP-85)',
|
||||
f=drv_entro_start),
|
||||
MenuItem("View Identity", f=view_ident),
|
||||
MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu),
|
||||
MenuItem("Key Teleport (start)", f=kt_start_rx, predicate=version.has_qr),
|
||||
MenuItem("Spending Policy", menu=SpendingPolicySubMenu,shortcut='s',predicate=has_real_secret),
|
||||
MenuItem('Paper Wallets', f=make_paper_wallet),
|
||||
ToggleMenuItem('Enable HSM', 'hsmcmd', ['Default Off', 'Enable'],
|
||||
story=("Enable HSM? Enables all user management commands, and other HSM-only USB commands. "
|
||||
"By default these commands are disabled."),
|
||||
predicate=hsm_available),
|
||||
MenuItem('User Management', menu=make_users_menu,
|
||||
predicate=hsm_available),
|
||||
MenuItem('WIF Store', menu=WIFStoreMenu.make),
|
||||
MenuItem('NFC Tools', predicate=nfc_enabled, menu=NFCToolsMenu, shortcut=KEY_NFC),
|
||||
MenuItem("Danger Zone", menu=DangerZoneMenu, shortcut='z'),
|
||||
]
|
||||
@ -373,7 +434,7 @@ AdvancedNormalMenu = [
|
||||
VirginSystem = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem('Choose PIN Code', f=initial_pin_setup),
|
||||
MenuItem('Advanced/Tools', menu=AdvancedVirginMenu),
|
||||
MenuItem('Advanced/Tools', menu=AdvancedVirginMenu, shortcut='t'),
|
||||
MenuItem('Bag Number', f=show_bag_number),
|
||||
MenuItem('Help', f=virgin_help, predicate=not version.has_qwerty),
|
||||
]
|
||||
@ -384,7 +445,7 @@ ImportWallet = [
|
||||
MenuItem("24 Words", menu=start_seed_import, arg=24),
|
||||
MenuItem('Scan QR Code', predicate=version.has_qr,
|
||||
shortcut=KEY_QR, f=scan_any_qr, arg=(True, False)),
|
||||
MenuItem("Restore Backup", f=restore_everything),
|
||||
MenuItem("Restore Backup", f=restore_backup, arg=False), # tmp=False
|
||||
MenuItem("Clone Coldcard", menu=clone_start),
|
||||
MenuItem("Import XPRV", f=import_xprv, arg=False), # ephemeral=False
|
||||
MenuItem("Tapsigner Backup", f=import_tapsigner_backup_file, arg=False),
|
||||
@ -408,9 +469,11 @@ EmptyWallet = [
|
||||
MenuItem('New Seed Words', menu=NewSeedMenu),
|
||||
MenuItem('Import Existing', menu=ImportWallet),
|
||||
MenuItem("Migrate Coldcard", menu=clone_start),
|
||||
MenuItem("Key Teleport (start)", f=kt_start_rx, predicate=version.has_qr),
|
||||
MenuItem('Help', f=virgin_help, predicate=not version.has_qwerty),
|
||||
MenuItem('Advanced/Tools', menu=AdvancedPinnedVirginMenu),
|
||||
MenuItem('Advanced/Tools', menu=AdvancedPinnedVirginMenu, shortcut='t'),
|
||||
MenuItem('Settings', menu=SettingsMenu),
|
||||
ShortcutItem(KEY_QR, predicate=version.has_qr, f=scan_any_qr, arg=(True, False)),
|
||||
]
|
||||
|
||||
# In operation, normal system, after a good PIN received.
|
||||
@ -424,8 +487,8 @@ NormalSystem = [
|
||||
MenuItem('Start HSM Mode', f=start_hsm_menu_item, predicate=hsm_policy_available),
|
||||
MenuItem("Address Explorer", menu=address_explore, shortcut='x'),
|
||||
MenuItem('Secure Notes & Passwords', menu=make_notes_menu, shortcut='n',
|
||||
predicate=lambda: version.has_qwerty and (settings.get("notes", False) != False)),
|
||||
MenuItem('Type Passwords', f=password_entry, shortcut='t',
|
||||
predicate=lambda: version.has_qwerty and settings.get("secnap", False)),
|
||||
MenuItem('Type Passwords', f=password_entry, shortcut='e',
|
||||
predicate=lambda: settings.get("emu", False) and has_secrets()),
|
||||
MenuItem('Seed Vault', menu=make_seed_vault_menu, shortcut='v',
|
||||
predicate=lambda: settings.master_get('seedvault') and has_secrets()),
|
||||
@ -437,10 +500,79 @@ NormalSystem = [
|
||||
|
||||
# Shown until unit is put into a numbered bag
|
||||
FactoryMenu = [
|
||||
MenuItem('Version: ' + version.get_mpy_version()[1], f=show_version),
|
||||
MenuItem('Bag Me Now', f=scan_and_bag),
|
||||
MenuItem('Version: ' + version.get_mpy_version()[1], f=show_version),
|
||||
MenuItem('DFU Upgrade', f=start_dfu, shortcut='u'),
|
||||
MenuItem('Ship W/O Bag', f=ship_wo_bag),
|
||||
MenuItem("Debug Functions", menu=DebugFunctionsMenu, shortcut='f'),
|
||||
MenuItem("Perform Selftest", f=start_selftest, shortcut='s'),
|
||||
]
|
||||
|
||||
# Special menus for hobbled mode where we have a (single signer) spending policy in effect.
|
||||
# - no access to secrets, backups, firmware up/downgrades.
|
||||
# - secure notes, but readonly; can be disabled completely.
|
||||
# - key teleport, but only for PSBT & multisig purposes.
|
||||
# - can only be enabled after we have secrets, so no need for has_secrets tests here
|
||||
#
|
||||
|
||||
# Slightly limited file menu when hobbled.
|
||||
# - no backup/restore
|
||||
HobbledFileMgmtMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem('Sign Text File', f=sign_message_on_sd),
|
||||
MenuItem('Batch Sign PSBT', f=batch_sign),
|
||||
MenuItem('List Files', f=list_files),
|
||||
MenuItem('Export Wallet', menu=WalletExportMenu), # dup under Adv/Tools
|
||||
MenuItem('Verify Sig File', f=verify_sig_file),
|
||||
MenuItem('NFC File Share', predicate=nfc_enabled, f=nfc_share_file, shortcut=KEY_NFC),
|
||||
MenuItem('BBQr File Share', predicate=version.has_qr, f=qr_share_file, arg=True),
|
||||
MenuItem('QR File Share', predicate=version.has_qr, f=qr_share_file, shortcut=KEY_QR),
|
||||
MenuItem('Format SD Card', f=wipe_sd_card),
|
||||
MenuItem('Format RAM Disk', predicate=vdisk_enabled, f=wipe_vdisk),
|
||||
]
|
||||
|
||||
# NFC tools when hobbled: not much different.
|
||||
HobbledNFCToolsMenu = [
|
||||
MenuItem('Sign PSBT', f=nfc_sign_psbt),
|
||||
MenuItem('Show Address', f=nfc_show_address),
|
||||
MenuItem('Sign Message', f=nfc_sign_msg),
|
||||
MenuItem('Verify Sig File', f=nfc_sign_verify),
|
||||
MenuItem('Verify Address', f=nfc_address_verify),
|
||||
MenuItem('File Share', f=nfc_share_file),
|
||||
MenuItem('Push Transaction', f=nfc_pushtx_file, predicate=has_pushtx_url),
|
||||
]
|
||||
|
||||
# Very limited advanced menu when hobbled.
|
||||
HobbledAdvancedMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem("File Management", menu=HobbledFileMgmtMenu),
|
||||
MenuItem('Export Wallet', menu=WalletExportMenu, shortcut='x'), # also inside FileMgmt
|
||||
MenuItem('Teleport Multisig PSBT', predicate=qr_and_ms, f=kt_send_file_psbt),
|
||||
MenuItem("View Identity", f=view_ident),
|
||||
MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu, predicate=sssp_related_keys),
|
||||
MenuItem('Paper Wallets', f=make_paper_wallet),
|
||||
MenuItem('NFC Tools', predicate=nfc_enabled, menu=HobbledNFCToolsMenu, shortcut=KEY_NFC),
|
||||
MenuItem('WIF Store', menu=WIFStoreMenu.make, predicate=sssp_related_keys),
|
||||
MenuItem('Show %s Version' % ("Firmware" if version.has_qwerty else "FW"), f=show_version),
|
||||
MenuItem("Destroy Seed", f=clear_seed, predicate=has_real_secret),
|
||||
]
|
||||
|
||||
# Main menu when a spending policy (hobbled) is in effect.
|
||||
HobbledTopMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem('Ready To Sign', f=ready2sign, shortcut='r'),
|
||||
MenuItem('Passphrase', menu=start_b39_pw, predicate=sssp_allow_passphrase, shortcut='p'),
|
||||
MenuItem('Scan Any QR Code', predicate=version.has_qr, f=scan_any_qr, arg=(False, True),
|
||||
shortcut=KEY_QR),
|
||||
MenuItem("Address Explorer", menu=address_explore, shortcut='x'),
|
||||
MenuItem('Secure Notes & Passwords', menu=make_notes_menu, predicate=sssp_allow_notes,
|
||||
shortcut='n'),
|
||||
MenuItem('Type Passwords', f=password_entry, shortcut='t',
|
||||
predicate=lambda: settings.get("emu", False) and sssp_related_keys()),
|
||||
MenuItem('Seed Vault', menu=make_seed_vault_menu, predicate=sssp_allow_vault,
|
||||
shortcut='v'),
|
||||
MenuItem('Advanced/Tools', menu=HobbledAdvancedMenu, shortcut='t'),
|
||||
MenuItem('Secure Logout', f=logout_now, predicate=not version.has_battery),
|
||||
MenuItem('EXIT TEST DRIVE', f=sssp_feature_menu, predicate=is_hobble_testdrive),
|
||||
ShortcutItem(KEY_NFC, predicate=nfc_enabled, menu=HobbledNFCToolsMenu),
|
||||
]
|
||||
|
||||
@ -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,9 +4,9 @@
|
||||
#
|
||||
# Unattended signing of transactions and messages, subject to a set of rules.
|
||||
#
|
||||
import stash, ustruct, chains, sys, gc, uio, ujson, uos, utime, ckcc, ngu, version
|
||||
from sffile import SFFile
|
||||
from utils import problem_file_line, cleanup_deriv_path, match_deriv_path
|
||||
import ustruct, chains, sys, gc, uio, ujson, uos, utime, ckcc, ngu
|
||||
from utils import problem_file_line, cleanup_deriv_path, match_deriv_path, keypath_to_str
|
||||
from utils import cleanup_payment_address
|
||||
from pincodes import AE_LONG_SECRET_LEN
|
||||
from stash import blank_object
|
||||
from users import Users, MAX_NUMBER_USERS, calc_local_pincode
|
||||
@ -70,9 +70,9 @@ def restore_backup(s):
|
||||
|
||||
with open(POLICY_FNAME, 'wt') as f:
|
||||
f.write(s)
|
||||
except BaseException as exc:
|
||||
except:
|
||||
# keep going, we don't want to brick
|
||||
sys.print_exception(exc)
|
||||
# sys.print_exception(exc)
|
||||
pass
|
||||
|
||||
def pop_list(j, fld_name, cleanup_fcn=None):
|
||||
@ -149,22 +149,6 @@ def assert_empty_dict(j):
|
||||
if extra:
|
||||
raise ValueError("Unknown item: " + ', '.join(extra))
|
||||
|
||||
def cleanup_whitelist_value(s):
|
||||
# one element in a list of addresses or paths or descriptors?
|
||||
# - later matching is string-based, so just doing basic syntax check here
|
||||
# - must be checksumed-base58 or bech32
|
||||
try:
|
||||
ngu.codecs.b58_decode(s)
|
||||
return s
|
||||
except: pass
|
||||
|
||||
try:
|
||||
ngu.codecs.segwit_decode(s)
|
||||
return s
|
||||
except: pass
|
||||
|
||||
raise ValueError('bad whitelist value: ' + s)
|
||||
|
||||
|
||||
class WhitelistOpts:
|
||||
# contains various options related to whitelisting
|
||||
@ -215,7 +199,7 @@ class ApprovalRule:
|
||||
self.per_period = pop_int(j, 'per_period', 0, MAX_SATS)
|
||||
self.max_amount = pop_int(j, 'max_amount', 0, MAX_SATS)
|
||||
self.users = pop_list(j, 'users', check_user)
|
||||
self.whitelist = pop_list(j, 'whitelist', cleanup_whitelist_value)
|
||||
self.whitelist = pop_list(j, 'whitelist', cleanup_payment_address)
|
||||
self.whitelist_opts = pop_dict(j, 'whitelist_opts', False, WhitelistOpts)
|
||||
self.min_users = pop_int(j, 'min_users', 1, len(self.users))
|
||||
self.local_conf = pop_bool(j, 'local_conf')
|
||||
@ -621,7 +605,7 @@ class HSMPolicy:
|
||||
fd.write('- XPUB values will be shared, if path matches: m OR %s.\n'
|
||||
% plist(self.share_xpubs))
|
||||
if self.share_addrs:
|
||||
fd.write('- Address values values will be shared, if path matches: %s.\n'
|
||||
fd.write('- Address values will be shared, if path matches: %s.\n'
|
||||
% plist(self.share_addrs))
|
||||
if self.priv_over_ux:
|
||||
fd.write('- Status responses optimized for privacy.\n')
|
||||
@ -672,6 +656,15 @@ class HSMPolicy:
|
||||
assert not glob.hsm_active
|
||||
glob.hsm_active = self
|
||||
|
||||
# HSM is the locked-down operating mode: shut down peripherals
|
||||
# that enlarge the USB-stack interaction surface.
|
||||
# - VDisk: MSC bulk OUT and HID OUT share the STM32 OTG_FS RX FIFO;
|
||||
# under load this can wedge the HID OUT endpoint permanently
|
||||
if glob.VD is not None:
|
||||
glob.VD.shutdown()
|
||||
if glob.NFC is not None:
|
||||
glob.NFC.shutdown()
|
||||
|
||||
self.start_time = utime.ticks_ms()
|
||||
|
||||
if new_file:
|
||||
@ -889,9 +882,6 @@ class HSMPolicy:
|
||||
# do this super early so always cleared even if other issues
|
||||
local_ok = self.consume_local_code(psbt_sha)
|
||||
|
||||
if not self.rules:
|
||||
raise ValueError("no txn signing allowed")
|
||||
|
||||
# reject anything with warning, probably
|
||||
if psbt.warnings:
|
||||
if self.warnings_ok:
|
||||
@ -899,6 +889,32 @@ class HSMPolicy:
|
||||
else:
|
||||
raise ValueError("has %d warning(s)" % len(psbt.warnings))
|
||||
|
||||
if psbt.por322:
|
||||
if not self.msg_paths:
|
||||
raise ValueError("Message signing not permitted")
|
||||
|
||||
for inp in psbt.inputs:
|
||||
if not inp.required_key:
|
||||
continue
|
||||
|
||||
if inp.is_multisig:
|
||||
paths = [
|
||||
keypath_to_str(inp.subpaths[pk])
|
||||
for pk in inp.required_key
|
||||
if pk in inp.subpaths
|
||||
]
|
||||
else:
|
||||
paths = [keypath_to_str(inp.subpaths[inp.required_key])]
|
||||
|
||||
if not any(match_deriv_path(self.msg_paths, p) for p in paths):
|
||||
raise ValueError("Message signing not enabled for that path")
|
||||
|
||||
self.approve(log, "BIP-322 message signing allowed")
|
||||
return 'y'
|
||||
|
||||
if not self.rules:
|
||||
raise ValueError("no txn signing allowed")
|
||||
|
||||
# See who has entered creditials already (all must be valid).
|
||||
users = []
|
||||
for u, (token, counter) in auth.items():
|
||||
@ -951,7 +967,7 @@ class HSMPolicy:
|
||||
|
||||
return 'y'
|
||||
except BaseException as exc:
|
||||
sys.print_exception(exc)
|
||||
# sys.print_exception(exc)
|
||||
err = "Rejected: " + (str(exc) or problem_file_line(exc))
|
||||
self.refuse(log, err)
|
||||
|
||||
|
||||
@ -59,7 +59,7 @@ class ApproveHSMPolicy(UserAuthorizedAction):
|
||||
msg = '''Last chance. You are defining a new policy which \
|
||||
allows the Coldcard to sign specific transactions without any further user approval.\n\n\
|
||||
Policy hash:\n%s\n\n
|
||||
Press %s to save policy and enable HSM mode.''' % (self.policy.hash(), confirm_char)
|
||||
Press (%s) to save policy and enable HSM mode.''' % (self.policy.hash(), confirm_char)
|
||||
|
||||
ch = await ux_show_story(msg, title=self.title,
|
||||
escape='x'+confirm_char, strict_escape=True)
|
||||
@ -67,7 +67,7 @@ Press %s to save policy and enable HSM mode.''' % (self.policy.hash(), confirm_c
|
||||
|
||||
except BaseException as exc:
|
||||
self.failed = "Exception"
|
||||
sys.print_exception(exc)
|
||||
# sys.print_exception(exc)
|
||||
self.refused = True
|
||||
|
||||
self.ux_done = True
|
||||
@ -354,7 +354,7 @@ class hsmUxInteraction:
|
||||
await sleep_ms(100)
|
||||
except BaseException as exc:
|
||||
# just in case, keep going
|
||||
sys.print_exception(exc)
|
||||
# sys.print_exception(exc)
|
||||
continue
|
||||
|
||||
# do the interactions, but don't let user actually press anything
|
||||
|
||||
@ -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
|
||||
|
||||
@ -134,7 +134,7 @@ class FullKeyboard(NumpadBase):
|
||||
if self._history[kn] == NUM_SAMPLES:
|
||||
self.is_pressed[kn] = 1
|
||||
new_presses.add(kn)
|
||||
elif self._history[i] == 0:
|
||||
elif self._history[kn] == 0:
|
||||
self.is_pressed[kn] = 0
|
||||
self._history[kn] = 0
|
||||
|
||||
|
||||
@ -3,11 +3,11 @@
|
||||
# lcd_display.py - LCD rendering for Q1's 320x240 pixel *colour* display!
|
||||
#
|
||||
import machine, uzlib, utime, array
|
||||
from uasyncio import sleep_ms
|
||||
from graphics_q1 import Graphics
|
||||
from st7788 import ST7788
|
||||
from utils import xfp2str, word_wrap
|
||||
from utils import xfp2str, word_wrap, chunk_address
|
||||
from ucollections import namedtuple
|
||||
from charcodes import OUT_CTRL_TITLE, OUT_CTRL_ADDRESS
|
||||
|
||||
# the one font: fixed-width (except for a few double-width chars)
|
||||
from font_iosevka import CELL_W, CELL_H, TEXT_PALETTES, COL_TEXT, COL_DARK_TEXT, COL_SCROLL_DARK
|
||||
@ -154,7 +154,7 @@ class Display:
|
||||
# otherwise: respect setting
|
||||
|
||||
if on_battery is None:
|
||||
on_battery = (get_batt_threshold() != None)
|
||||
on_battery = (get_batt_threshold() is not None)
|
||||
|
||||
if on_battery:
|
||||
# user-defined brightness when running on batteries.
|
||||
@ -190,7 +190,7 @@ class Display:
|
||||
self.image(165, 0, 'tmp_%d' % kws['tmp'])
|
||||
|
||||
xfp = kws.get('xfp', None) # expects an integer
|
||||
if xfp != None:
|
||||
if xfp is not None:
|
||||
x = 217
|
||||
for ch in xfp2str(xfp).lower():
|
||||
self.image(x, 0, 'ch_'+ch)
|
||||
@ -268,7 +268,7 @@ class Display:
|
||||
|
||||
if x is None or x < 0:
|
||||
w = self.width(msg)
|
||||
if x == None:
|
||||
if x is None:
|
||||
# center: also blanks rest of line
|
||||
x = max(0, (CHARS_W - w) // 2)
|
||||
end_x = x + w
|
||||
@ -612,25 +612,135 @@ class Display:
|
||||
self.clear()
|
||||
|
||||
y=0
|
||||
prev_x = None
|
||||
for ln in lines:
|
||||
if ln == 'EOT':
|
||||
self.text(0, y, '┅'*CHARS_W, dark=True)
|
||||
continue
|
||||
elif ln and ln[0] == '\x01':
|
||||
|
||||
elif ln and ln[0] == OUT_CTRL_TITLE:
|
||||
# title ... but we have no special font? Inverse!
|
||||
self.text(0, y, ' '+ln[1:]+' ', invert=True)
|
||||
if hint_icons:
|
||||
# maybe show that [QR] can do something
|
||||
# hint_icons not shown if is story without title
|
||||
# maybe show that [QR,NFC] can do something
|
||||
self.text(-1, y, hint_icons, dark=True)
|
||||
|
||||
elif ln and ln[0] == OUT_CTRL_ADDRESS:
|
||||
# we can assume this will be a single line for our display
|
||||
# thanks to code in utils.word_wrap
|
||||
prev_x = self._draw_addr(y, ln[1:], prev_x=prev_x)
|
||||
|
||||
else:
|
||||
self.text(0, y, ln)
|
||||
prev_x = None
|
||||
|
||||
y += 1
|
||||
|
||||
self.scroll_bar(top, num_lines, CHARS_H)
|
||||
self.show()
|
||||
|
||||
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert, partial_bar=None):
|
||||
def _draw_addr(self, y, addr, prev_x=None):
|
||||
# Draw a single-line of an address
|
||||
# - use prev_x=0 to start centered
|
||||
if prev_x is None:
|
||||
# left justify (for stories)
|
||||
prev_x = x = 1
|
||||
elif prev_x == 0:
|
||||
# center first line, following line(s) will be left-justified to match that
|
||||
prev_x = x = max(((CHARS_W - (len(addr) * 5) // 4) // 2), 0)
|
||||
else:
|
||||
x = prev_x
|
||||
|
||||
self.text(x, y, ' '+' '.join(chunk_address(addr))+' ', invert=True)
|
||||
|
||||
return prev_x
|
||||
|
||||
@staticmethod
|
||||
def handle_qr_msg(msg, max_lines=False):
|
||||
if len(msg) <= CHARS_W:
|
||||
parts = [msg]
|
||||
elif ' ' not in msg and (len(msg) <= (CHARS_W * 2)):
|
||||
# fits in two lines, but has no spaces
|
||||
hh = len(msg) // 2
|
||||
parts = [msg[0:hh], msg[hh:]]
|
||||
else:
|
||||
if not max_lines:
|
||||
# do word wrap
|
||||
parts = list(word_wrap(msg, CHARS_W))
|
||||
else:
|
||||
# 2 lines max
|
||||
parts = [msg[:30] + "⋯", "⋯" + msg[-30:]]
|
||||
|
||||
return parts
|
||||
|
||||
def draw_qr_lines(self, lines, is_addr):
|
||||
y = CHARS_H - len(lines)
|
||||
prev_x = 0
|
||||
for line in lines:
|
||||
if not is_addr:
|
||||
self.text(None, y, line)
|
||||
else:
|
||||
prev_x = self._draw_addr(y, line, prev_x=prev_x)
|
||||
y += 1
|
||||
|
||||
def draw_qr_idx_hint(self, str_idx):
|
||||
lh = len(str_idx)
|
||||
assert lh <= 10
|
||||
if lh > 5:
|
||||
# needs 2 lines
|
||||
self.text(-1, 0, str_idx[:5])
|
||||
self.text(-1, 1, str_idx[5:])
|
||||
else:
|
||||
self.text(-1, 0, str_idx)
|
||||
|
||||
def draw_side_msg(self, msg, has_idx):
|
||||
right_sub = 2 if has_idx else 0
|
||||
start_right = right_msg = None
|
||||
if len(msg) <= CHARS_H:
|
||||
# we only need left side
|
||||
start_left = CHARS_H - len(msg)
|
||||
left_msg = msg
|
||||
else:
|
||||
split_msg = msg.split()
|
||||
if len(split_msg) == 1 or len(split_msg) > 2:
|
||||
return # not possible
|
||||
|
||||
left_msg, right_msg = split_msg
|
||||
if len(left_msg) > CHARS_H:
|
||||
return
|
||||
if len(right_msg) > (CHARS_H - right_sub):
|
||||
return
|
||||
|
||||
start_left = CHARS_H - len(left_msg)
|
||||
start_right = CHARS_H - len(right_msg)
|
||||
|
||||
for i, c in enumerate(left_msg, start=start_left):
|
||||
self.text(1, i, c)
|
||||
|
||||
if start_right:
|
||||
for i, c in enumerate(right_msg, start=start_right):
|
||||
self.text(-1, i, c)
|
||||
|
||||
def draw_qr_error(self, idx_hint, msg=None):
|
||||
x = 85
|
||||
y = 30
|
||||
w = 150
|
||||
self.clear()
|
||||
self.dis.fill_rect(x, y, w, w, COL_TEXT)
|
||||
self.dis.fill_rect(x + 1, y + 1, w - 2, w - 2) # Black
|
||||
self.text(12, 3, "QR too big")
|
||||
if msg:
|
||||
lines = self.handle_qr_msg(msg, max_lines=True)
|
||||
self.draw_qr_lines(lines, False)
|
||||
|
||||
if idx_hint:
|
||||
self.draw_qr_idx_hint(idx_hint)
|
||||
|
||||
self.show()
|
||||
|
||||
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert, partial_bar=None,
|
||||
is_addr=False, force_msg=False, side_msg=None):
|
||||
# Show a QR code on screen w/ some text under it
|
||||
# - invert not supported on Q1
|
||||
# - sidebar not supported here (see users.py)
|
||||
@ -638,18 +748,19 @@ class Display:
|
||||
assert not sidebar
|
||||
|
||||
# maybe show something other than QR contents under it
|
||||
if msg:
|
||||
if len(msg) <= CHARS_W:
|
||||
parts = [msg]
|
||||
elif ' ' not in msg and (len(msg) <= CHARS_W*2):
|
||||
# fits in two lines, but has no spaces (ie. payment addr)
|
||||
# so split nicely, and shift off center
|
||||
hh = len(msg) // 2
|
||||
parts = [msg[0:hh] + ' ', ' '+msg[hh:]]
|
||||
if is_addr:
|
||||
# With fancy display, no address, even classic can fit in single line,
|
||||
# so always split nicely in middle and at mod4
|
||||
hh = len(msg) // 2
|
||||
if hh <= 20:
|
||||
hh = (hh + 3) & ~0x3
|
||||
parts = [msg[0:hh], msg[hh:]]
|
||||
num_lines = 2
|
||||
else:
|
||||
# do word wrap
|
||||
parts = list(word_wrap(msg, CHARS_W))
|
||||
|
||||
# p2wsh address would need 3 lines to show, so we won't
|
||||
num_lines = 0
|
||||
elif msg:
|
||||
parts = self.handle_qr_msg(msg)
|
||||
num_lines = len(parts)
|
||||
else:
|
||||
num_lines = 0
|
||||
@ -670,25 +781,21 @@ class Display:
|
||||
fullscreen = False
|
||||
trim_lines = 0
|
||||
|
||||
if w == 77:
|
||||
# v15 => 77px x 3: 77*3 = 231px
|
||||
expand = 3
|
||||
num_lines = 0
|
||||
fullscreen = True
|
||||
elif w in (109, 113, 117):
|
||||
# v23 => 109px x 2 = 218px
|
||||
# v24 => 113px x 2 = 226px
|
||||
# v25 => 117px x 2 = 234px
|
||||
expand = 2
|
||||
num_lines = 0
|
||||
fullscreen = True
|
||||
elif expand == 1 and num_lines:
|
||||
# Maybe loose the text lines?
|
||||
expand2 = max(1, ACTIVE_H // (w+2))
|
||||
if expand2 > expand:
|
||||
# v18,v19,v20,v21,v22
|
||||
# always try to show the biggest possible QR code if not force_msg
|
||||
if not force_msg:
|
||||
if num_lines:
|
||||
# better with text dropped?
|
||||
e2 = max(1, ACTIVE_H // (w + 2))
|
||||
if e2 > expand:
|
||||
num_lines = 0
|
||||
expand = e2
|
||||
|
||||
# fullscreen ?
|
||||
e3 = (ACTIVE_H + 20) // (w + 2)
|
||||
if expand < e3:
|
||||
expand = e3
|
||||
fullscreen = True
|
||||
num_lines = 0
|
||||
expand = expand2
|
||||
|
||||
# vert center in available space
|
||||
qw = (w+2) * expand
|
||||
@ -722,20 +829,13 @@ class Display:
|
||||
|
||||
if num_lines:
|
||||
# centered text under that
|
||||
y = CHARS_H - num_lines
|
||||
for line in parts:
|
||||
self.text(None, y, line)
|
||||
y += 1
|
||||
self.draw_qr_lines(parts, is_addr)
|
||||
|
||||
if idx_hint:
|
||||
lh = len(idx_hint)
|
||||
assert lh <= 10
|
||||
if lh > 6:
|
||||
# needs 2 lines
|
||||
self.text(-1, 0, idx_hint[:6])
|
||||
self.text(-1, 1, idx_hint[6:])
|
||||
else:
|
||||
self.text(-1, 0, idx_hint)
|
||||
self.draw_qr_idx_hint(idx_hint)
|
||||
|
||||
if side_msg:
|
||||
self.draw_side_msg(side_msg, idx_hint)
|
||||
|
||||
# pass a max brightness flag here, which will be cleared after next show
|
||||
self.show(max_bright=True)
|
||||
@ -770,8 +870,12 @@ class Display:
|
||||
else:
|
||||
pat = '' # clear line
|
||||
|
||||
self.text(None, -3, pat)
|
||||
if count == hdr.num_parts and count == 1:
|
||||
# skip the BS, it's a simple one
|
||||
self.progress_bar_show(1)
|
||||
return
|
||||
|
||||
self.text(None, -3, pat)
|
||||
self.text(None, -2, 'Keep scanning more...' if count < hdr.num_parts else 'Got all parts!')
|
||||
self.text(None, -1, '%s: %d of %d parts' % (hdr.file_label(), count, hdr.num_parts),
|
||||
dark=True)
|
||||
|
||||
@ -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,19 +1,19 @@
|
||||
# Freeze everything in this list.
|
||||
# - not optimized because we need asserts to work
|
||||
# - for specific boards, see manifest_mk[34].py and manifest_q1.py
|
||||
# - for specific boards, see manifest_{mk4,q1}.py
|
||||
freeze_as_mpy('', [
|
||||
'actions.py',
|
||||
'address_explorer.py',
|
||||
'auth.py',
|
||||
'backups.py',
|
||||
'block_height.py',
|
||||
'callgate.py',
|
||||
'ccc.py',
|
||||
'chains.py',
|
||||
'choosers.py',
|
||||
'compat7z.py',
|
||||
'countdowns.py',
|
||||
'descriptor.py',
|
||||
'dev_helper.py',
|
||||
'display.py',
|
||||
'drv_entro.py',
|
||||
'exceptions.py',
|
||||
'export.py',
|
||||
@ -26,48 +26,56 @@ freeze_as_mpy('', [
|
||||
'login.py',
|
||||
'main.py',
|
||||
'menu.py',
|
||||
'mk4.py',
|
||||
'msgsign.py',
|
||||
'multisig.py',
|
||||
'ndef.py',
|
||||
'nfc.py',
|
||||
'numpad.py',
|
||||
'nvstore.py',
|
||||
'opcodes.py',
|
||||
'ownership.py',
|
||||
'paper.py',
|
||||
'pincodes.py',
|
||||
'psbt.py',
|
||||
'psram.py',
|
||||
'pwsave.py',
|
||||
'queues.py',
|
||||
'qrs.py',
|
||||
'queues.py',
|
||||
'random.py',
|
||||
'seed.py',
|
||||
'selftest.py',
|
||||
'serializations.py',
|
||||
'sffile.py',
|
||||
'ssd1306.py',
|
||||
'stash.py',
|
||||
'tapsigner.py',
|
||||
'trick_pins.py',
|
||||
'usb.py',
|
||||
'utils.py',
|
||||
'ux.py',
|
||||
'vdisk.py',
|
||||
'version.py',
|
||||
'xor_seed.py',
|
||||
'tapsigner.py',
|
||||
'wallet.py',
|
||||
'ownership.py',
|
||||
'web2fa.py',
|
||||
'wif.py',
|
||||
'xor_seed.py'
|
||||
], opt=0)
|
||||
|
||||
# Optimize data-like files, since no need to debug them.
|
||||
freeze_as_mpy('', [
|
||||
'sigheader.py',
|
||||
'public_constants.py',
|
||||
'charcodes.py',
|
||||
'public_constants.py',
|
||||
'sigheader.py',
|
||||
], opt=3)
|
||||
|
||||
# Maybe include test code.
|
||||
import os
|
||||
if int(os.environ.get('DEBUG_BUILD', 0)):
|
||||
freeze_as_mpy('', [
|
||||
'dev_helper.py',
|
||||
'h.py',
|
||||
'dev_helper.py',
|
||||
'usb_test_commands.py',
|
||||
'sim_display.py',
|
||||
'usb_test_commands.py',
|
||||
], opt=0)
|
||||
|
||||
include("$(MPY_DIR)/extmod/uasyncio/manifest.py")
|
||||
|
||||
@ -1,17 +1,12 @@
|
||||
# Mk4 only files; would not be needed on Mk3 or earlier.
|
||||
freeze_as_mpy('', [
|
||||
'ssd1306.py',
|
||||
'mempad.py',
|
||||
'psram.py',
|
||||
'mk4.py',
|
||||
'vdisk.py',
|
||||
'nfc.py',
|
||||
'ndef.py',
|
||||
'trick_pins.py',
|
||||
'ux_mk4.py',
|
||||
'display.py',
|
||||
'hsm.py',
|
||||
'hsm_ux.py',
|
||||
'mempad.py',
|
||||
'ssd1306.py',
|
||||
'users.py',
|
||||
'ux_mk4.py'
|
||||
], opt=0)
|
||||
|
||||
# Optimize data-like files, since no need to debug them.
|
||||
|
||||
@ -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
|
||||
@ -290,7 +290,7 @@ class MenuSystem:
|
||||
dis.clear()
|
||||
|
||||
cursor_y = None
|
||||
for n in range(self.ypos+PER_M+1):
|
||||
for n in range(PER_M+1):
|
||||
real_idx = n+self.ypos
|
||||
if real_idx >= self.count: break
|
||||
|
||||
@ -306,10 +306,6 @@ class MenuSystem:
|
||||
if fcn and fcn():
|
||||
checked = True
|
||||
|
||||
if not has_qwerty and checked and (len(msg) > 14):
|
||||
# on mk4 every label longer than 14 will overlap with checkmark
|
||||
checked = False
|
||||
|
||||
if self.multi_selected is not None and (real_idx in self.multi_selected):
|
||||
# ignore length constraint above, we need to visually show that
|
||||
# smthg is selected - in any case
|
||||
@ -335,9 +331,8 @@ class MenuSystem:
|
||||
if wrap: return True
|
||||
|
||||
# Do wrap-around (by request from NVK) if longer than the screen itself (on Q),
|
||||
# for mk4, limit is 16 which hits mostly the seed word menus.
|
||||
limit = 10 if has_qwerty else 16
|
||||
return self.count > limit
|
||||
# Mk4: same limit
|
||||
return self.count > 10
|
||||
|
||||
def down(self):
|
||||
if self.cursor < self.count-1:
|
||||
@ -362,12 +357,6 @@ class MenuSystem:
|
||||
self.cursor = 0
|
||||
self.ypos = 0
|
||||
|
||||
def goto_n(self, n):
|
||||
# goto N from top of (current) screen
|
||||
# change scroll only if needed to make it visible
|
||||
self.cursor = max(min(n + self.ypos, self.count-1), 0)
|
||||
self.ypos = max(self.cursor - n, 0)
|
||||
|
||||
def goto_idx(self, n):
|
||||
# skip to any item, force cusor near middle of screen
|
||||
n = self.count-1 if n >= self.count else n
|
||||
@ -388,7 +377,7 @@ class MenuSystem:
|
||||
self.up()
|
||||
|
||||
# events
|
||||
def on_cancel(self):
|
||||
async def on_cancel(self):
|
||||
# override me
|
||||
if the_ux.pop():
|
||||
# top of stack (main top-level menu)
|
||||
@ -399,7 +388,7 @@ class MenuSystem:
|
||||
#
|
||||
if picked is None:
|
||||
# "go back" or cancel or something
|
||||
self.on_cancel()
|
||||
await self.on_cancel()
|
||||
else:
|
||||
await picked.activate(self, self.cursor)
|
||||
|
||||
@ -412,7 +401,7 @@ class MenuSystem:
|
||||
gc.collect()
|
||||
if self.multi_selected is not None:
|
||||
# multichoice
|
||||
self.on_cancel()
|
||||
await self.on_cancel()
|
||||
return ch
|
||||
|
||||
await self.activate(ch)
|
||||
@ -474,7 +463,7 @@ class MenuSystem:
|
||||
self.ypos = 0
|
||||
elif '1' <= key <= '9':
|
||||
# jump down, based on screen postion
|
||||
self.goto_n(ord(key)-ord('1'))
|
||||
self.goto_idx(ord(key)-ord('1'))
|
||||
elif key in self.shortcuts:
|
||||
# run the function, if predicate allows
|
||||
m = self.shortcuts[key]
|
||||
@ -489,7 +478,7 @@ class MenuSystem:
|
||||
return self.items[self.cursor]
|
||||
|
||||
# search downwards for a menu item that starts with indicated letter
|
||||
# if found, select it but dont drill down
|
||||
# if found, select it but don't drill down
|
||||
lst = list(range(self.cursor+1, self.count)) + list(range(0, self.cursor))
|
||||
for n in lst:
|
||||
if self.items[n].label[0].upper() == key.upper():
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
# mk4.py - Mk4 specific code, not needed on earlier devices.
|
||||
# mk4.py - Mk4 and Mk5 specific code, not needed on earlier devices.
|
||||
#
|
||||
#
|
||||
import os, sys, pyb, ckcc, version, glob
|
||||
@ -11,8 +11,8 @@ def make_flash_fs():
|
||||
os.VfsLfs2.mkfs(fl)
|
||||
|
||||
os.mount(fl, '/flash')
|
||||
|
||||
os.mkdir('/flash/settings')
|
||||
os.chdir('/flash')
|
||||
os.mkdir('settings')
|
||||
|
||||
def make_psram_fs():
|
||||
# Filesystem is wiped and rebuilt on each boot before this point, but
|
||||
@ -58,8 +58,7 @@ def init0():
|
||||
|
||||
try:
|
||||
make_psram_fs()
|
||||
except BaseException as exc:
|
||||
sys.print_exception(exc)
|
||||
except: pass
|
||||
|
||||
if version.is_devmode:
|
||||
try:
|
||||
@ -71,10 +70,13 @@ def init0():
|
||||
rng_seeding()
|
||||
|
||||
async def dev_enable_repl(*a):
|
||||
# Mk4: Enable serial port connection. You'll have to break case open.
|
||||
# Enable serial port connection. You'll have to break case open.
|
||||
|
||||
from ux import ux_show_story
|
||||
from utils import wipe_if_deltamode
|
||||
|
||||
wipe_if_deltamode()
|
||||
if not version.is_devmode: return
|
||||
|
||||
# allow REPL access
|
||||
ckcc.vcp_enabled(True)
|
||||
@ -83,15 +85,4 @@ async def dev_enable_repl(*a):
|
||||
await ux_show_story("""\
|
||||
The serial port has now been enabled.\n\n3.3v TTL on Tx/Rx/Gnd pads @ 115,200 bps.""")
|
||||
|
||||
def wipe_if_deltamode():
|
||||
# If in deltamode, give up and wipe self rather do
|
||||
# a thing that might reveal true master secret...
|
||||
|
||||
from pincodes import pa
|
||||
|
||||
if not pa.is_deltamode():
|
||||
return
|
||||
|
||||
callgate.fast_wipe()
|
||||
|
||||
# EOF
|
||||
|
||||
518
shared/msgsign.py
Normal file
518
shared/msgsign.py
Normal file
@ -0,0 +1,518 @@
|
||||
# (c) Copyright 2025 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
# Signatures over text ... not transactions.
|
||||
#
|
||||
import stash, chains, sys, gc, ngu, ujson, version
|
||||
from ubinascii import b2a_base64, a2b_base64
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from ubinascii import unhexlify as a2b_hex
|
||||
from uhashlib import sha256
|
||||
from public_constants import MSG_SIGNING_MAX_LENGTH
|
||||
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
|
||||
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
|
||||
from ux import (ux_show_story, OK, ux_enter_bip32_index, ux_input_text, the_ux,
|
||||
import_export_prompt, ux_aborted)
|
||||
from utils import problem_file_line, to_ascii_printable, show_single_address, node_from_privkey
|
||||
from files import CardSlot, CardMissingError, needs_microsd
|
||||
|
||||
def rfc_signature_template(msg, addr, sig):
|
||||
# RFC2440 <https://www.ietf.org/rfc/rfc2440.txt> style signatures, popular
|
||||
# since the genesis block, but not really part of any BIP as far as I know.
|
||||
#
|
||||
return [
|
||||
"-----BEGIN BITCOIN SIGNED MESSAGE-----\n",
|
||||
"%s\n" % msg,
|
||||
"-----BEGIN BITCOIN SIGNATURE-----\n",
|
||||
"%s\n" % addr,
|
||||
"%s\n" % sig,
|
||||
"-----END BITCOIN SIGNATURE-----\n"
|
||||
]
|
||||
|
||||
def parse_armored_signature_file(contents):
|
||||
# XXX limited parser: will fail w/ messages containing dashes
|
||||
sep = "-----"
|
||||
assert contents.count(sep) == 6, "Armor text MUST be surrounded by exactly five (5) dashes."
|
||||
|
||||
temp = contents.split(sep)
|
||||
msg = temp[2].strip()
|
||||
addr_sig = temp[4].strip()
|
||||
addr, sig_str = addr_sig.split()
|
||||
|
||||
return msg, addr, sig_str
|
||||
|
||||
def verify_signature(msg, addr, sig_str):
|
||||
# Look at a base64 signature, and given address. Do full verification.
|
||||
# - raise on errors
|
||||
# - return warnings as string: can only be mismatch between addr format encoded in recid
|
||||
warnings = ""
|
||||
script = None
|
||||
hash160 = None
|
||||
invalid_addr_fmt_msg = "Invalid address format - must be one of p2pkh, p2sh-p2wpkh, or p2wpkh."
|
||||
invalid_addr = "Invalid signature for message."
|
||||
|
||||
if addr[0] in "1mn":
|
||||
addr_fmt = AF_CLASSIC
|
||||
decoded_addr = ngu.codecs.b58_decode(addr)
|
||||
hash160 = decoded_addr[1:] # remove prefix
|
||||
elif addr.startswith("bc1q") or addr.startswith("tb1q") or addr.startswith("bcrt1q"):
|
||||
if len(addr) > 44: # testnet/mainnet max singlesig len 42, regtest 44
|
||||
# p2wsh
|
||||
raise ValueError(invalid_addr_fmt_msg)
|
||||
addr_fmt = AF_P2WPKH
|
||||
_, _, hash160 = ngu.codecs.segwit_decode(addr)
|
||||
elif addr[0] in "32":
|
||||
addr_fmt = AF_P2WPKH_P2SH
|
||||
decoded_addr = ngu.codecs.b58_decode(addr)
|
||||
script = decoded_addr[1:] # remove prefix
|
||||
else:
|
||||
raise ValueError(invalid_addr_fmt_msg)
|
||||
|
||||
try:
|
||||
sig_bytes = a2b_base64(sig_str)
|
||||
if not sig_bytes or len(sig_bytes) != 65:
|
||||
# can return b'' in case of wrong, can also raise
|
||||
raise ValueError("invalid encoding")
|
||||
|
||||
header_byte = sig_bytes[0]
|
||||
header_base = chains.current_chain().sig_hdr_base(addr_fmt)
|
||||
if (header_byte - header_base) not in (0, 1, 2, 3):
|
||||
# wrong header value only - this can still verify OK
|
||||
warnings += "Specified address format does not match signature header byte format."
|
||||
|
||||
# least two significant bits
|
||||
rec_id = (header_byte - 27) & 0x03
|
||||
# need to normalize it to 31 base for ngu
|
||||
new_header_byte = 31 + rec_id
|
||||
sig = ngu.secp256k1.signature(bytes([new_header_byte]) + sig_bytes[1:])
|
||||
except ValueError as e:
|
||||
raise ValueError("Parsing signature failed - %s." % str(e))
|
||||
|
||||
digest = chains.current_chain().hash_message(msg.encode('ascii'))
|
||||
try:
|
||||
rec_pubkey = sig.verify_recover(digest)
|
||||
except ValueError as e:
|
||||
raise ValueError("Invalid signature for msg - %s." % str(e))
|
||||
|
||||
rec_pubkey_bytes = rec_pubkey.to_bytes()
|
||||
rec_hash160 = ngu.hash.hash160(rec_pubkey_bytes)
|
||||
|
||||
if script:
|
||||
target = bytes([0, 20]) + rec_hash160
|
||||
target = ngu.hash.hash160(target)
|
||||
if target != script:
|
||||
raise ValueError(invalid_addr)
|
||||
else:
|
||||
if rec_hash160 != hash160:
|
||||
raise ValueError(invalid_addr)
|
||||
|
||||
return warnings
|
||||
|
||||
async def verify_armored_signed_msg(contents, digest_check=True):
|
||||
# Verify on-disk checksums of files listed inside a signed file.
|
||||
# - digest_check=False for NFC cases, where we do not have filesystem
|
||||
from glob import dis
|
||||
|
||||
dis.fullscreen("Verifying...")
|
||||
|
||||
try:
|
||||
msg, addr, sig_str = parse_armored_signature_file(contents)
|
||||
except Exception as e:
|
||||
e_line = problem_file_line(e)
|
||||
await ux_show_story("Malformed signature file. %s %s" % (str(e), e_line), title="FAILURE")
|
||||
return
|
||||
|
||||
try:
|
||||
sig_warn = verify_signature(msg, addr, sig_str)
|
||||
except Exception as e:
|
||||
await ux_show_story(str(e), title="ERROR")
|
||||
return
|
||||
|
||||
title = "CORRECT"
|
||||
warn_msg = ""
|
||||
err_msg = ""
|
||||
story = "Good signature by address:\n%s" % show_single_address(addr)
|
||||
|
||||
if digest_check:
|
||||
digest_prob = verify_signed_file_digest(msg)
|
||||
if digest_prob:
|
||||
err, digest_warn = digest_prob
|
||||
if digest_warn:
|
||||
title = "WARNING"
|
||||
wmsg_base = "not present. Contents verification not possible."
|
||||
if len(digest_warn) == 1:
|
||||
fname = digest_warn[0][0]
|
||||
warn_msg += "'%s' is %s" % (fname, wmsg_base)
|
||||
else:
|
||||
warn_msg += "Files:\n" + "\n".join("> %s" % fname for fname, _ in digest_warn)
|
||||
warn_msg += "\nare %s" % wmsg_base
|
||||
|
||||
if err:
|
||||
title = "ERROR"
|
||||
for fname, calc, got in err:
|
||||
err_msg += ("Referenced file '%s' has wrong contents.\n"
|
||||
"Got:\n%s\n\nExpected:\n%s" % (fname, got, calc))
|
||||
|
||||
if sig_warn:
|
||||
# we know not ours only because wrong recid header used & not BIP-137 compliant
|
||||
story = "Correctly signed, but not by this Coldcard. %s" % sig_warn
|
||||
|
||||
await ux_show_story('\n\n'.join(m for m in [err_msg, story, warn_msg] if m), title=title)
|
||||
|
||||
async def verify_txt_sig_file(filename):
|
||||
# copy message into memory
|
||||
try:
|
||||
with CardSlot() as card:
|
||||
with card.open(filename, 'rt') as fd:
|
||||
text = fd.read()
|
||||
except CardMissingError:
|
||||
await needs_microsd()
|
||||
return
|
||||
except Exception as e:
|
||||
await ux_show_story('Error: ' + str(e))
|
||||
return
|
||||
|
||||
await verify_armored_signed_msg(text)
|
||||
|
||||
async def msg_sign_ux_get_subpath(addr_fmt):
|
||||
# Ask for account number, and maybe change component of path for signature.
|
||||
# - return full derivation path to be used.
|
||||
purpose = chains.af_to_bip44_purpose(addr_fmt)
|
||||
chain_n = chains.current_chain().b44_cointype
|
||||
|
||||
acct = await ux_enter_bip32_index('Account Number:')
|
||||
if acct is None: return
|
||||
|
||||
ch = await ux_show_story(title="Change?",
|
||||
msg="Press (0) to use internal/change address,"
|
||||
" %s to use external/receive address." % OK, escape="0")
|
||||
change = 1 if ch == '0' else 0
|
||||
|
||||
idx = await ux_enter_bip32_index('Index Number:')
|
||||
if idx is None: return
|
||||
|
||||
return "m/%dh/%dh/%dh/%d/%d" % (purpose, chain_n, acct, change, idx)
|
||||
|
||||
|
||||
def sign_export_contents(content_list, deriv, addr_fmt, pk=None):
|
||||
# Return signed message over hashes of files.
|
||||
msg2sign = make_signature_file_msg(content_list)
|
||||
bitcoin_digest = chains.current_chain().hash_message(msg2sign)
|
||||
sig_bytes, addr = sign_message_digest(bitcoin_digest, deriv, "Signing...", addr_fmt, pk=pk)
|
||||
sig = b2a_base64(sig_bytes).decode().strip()
|
||||
|
||||
return rfc_signature_template(addr=addr, msg=msg2sign.decode(), sig=sig)
|
||||
|
||||
def verify_signed_file_digest(msg):
|
||||
# Look inside a list of hashs and file names, and
|
||||
# verify at their actual hashes and return list of issues if any.
|
||||
parsed_msg = parse_signature_file_msg(msg)
|
||||
if not parsed_msg:
|
||||
# not our format
|
||||
return
|
||||
|
||||
try:
|
||||
err, warn = [], []
|
||||
with CardSlot() as card:
|
||||
for digest, fname in parsed_msg:
|
||||
path = card.abs_path(fname)
|
||||
if not card.exists(path):
|
||||
warn.append((fname, None))
|
||||
continue
|
||||
path = card.abs_path(fname)
|
||||
|
||||
md = sha256()
|
||||
with open(path, "rb") as f:
|
||||
while True:
|
||||
chunk = f.read(1024)
|
||||
if not chunk:
|
||||
break
|
||||
md.update(chunk)
|
||||
|
||||
h = b2a_hex(md.digest()).decode().strip()
|
||||
if h != digest:
|
||||
err.append((fname, h, digest))
|
||||
except:
|
||||
# fail silently if issues with reading files or SD issues
|
||||
# no digest checking
|
||||
return
|
||||
|
||||
return err, warn
|
||||
|
||||
def write_sig_file(content_list, derive=None, addr_fmt=AF_CLASSIC, pk=None, sig_name=None):
|
||||
if derive is None:
|
||||
ct = chains.current_chain().b44_cointype
|
||||
derive = "m/44'/%d'/0'/0/0" % ct
|
||||
|
||||
fpath = content_list[0][1]
|
||||
if len(content_list) > 1:
|
||||
# we're signing contents of more files - need generic name for sig file
|
||||
assert sig_name
|
||||
sig_nice = sig_name + ".sig"
|
||||
sig_fpath = fpath.rsplit("/", 1)[0] + "/" + sig_nice
|
||||
else:
|
||||
sig_fpath = fpath.rsplit(".", 1)[0] + ".sig"
|
||||
sig_nice = sig_fpath.split("/")[-1]
|
||||
|
||||
sig_gen = sign_export_contents([(h, f.split("/")[-1]) for h, f in content_list],
|
||||
derive, addr_fmt, pk=pk)
|
||||
|
||||
with open(sig_fpath, 'wt') as fd:
|
||||
for i, part in enumerate(sig_gen):
|
||||
fd.write(part)
|
||||
|
||||
return sig_nice
|
||||
|
||||
def validate_text_for_signing(text, allow_tab_nl=False):
|
||||
# Check for some UX/UI traps in the message itself.
|
||||
# - messages must be short and ascii only. Our charset is limited
|
||||
# - too many spaces, leading/trailing can be an issue
|
||||
# MSG_MAX_SPACES = 4 # impt. compared to -=- positioning
|
||||
text = str(text, "ascii") # handle memoryview coming from USB
|
||||
result = to_ascii_printable(text, allow_tab_nl=allow_tab_nl)
|
||||
|
||||
length = len(result)
|
||||
assert length >= 2, "msg too short (min. 2)"
|
||||
assert length <= MSG_SIGNING_MAX_LENGTH, "msg too long (max. %d)" % MSG_SIGNING_MAX_LENGTH
|
||||
assert " " not in result, 'too many spaces together in msg(max. 3)'
|
||||
# other confusion w/ whitepace
|
||||
assert result[0] != ' ', 'leading space(s) in msg'
|
||||
assert result[-1] != ' ', 'trailing space(s) in msg'
|
||||
|
||||
# looks ok
|
||||
return result
|
||||
|
||||
def addr_fmt_from_subpath(subpath):
|
||||
if not subpath:
|
||||
af = "p2pkh"
|
||||
elif subpath[:4] == "m/84":
|
||||
af = "p2wpkh"
|
||||
elif subpath[:4] == "m/49":
|
||||
af = "p2sh-p2wpkh"
|
||||
else:
|
||||
af = "p2pkh"
|
||||
return af
|
||||
|
||||
def parse_msg_sign_request(data):
|
||||
subpath = ""
|
||||
addr_fmt = None
|
||||
is_json = False
|
||||
|
||||
# sparrow compat
|
||||
if "signmessage" in data:
|
||||
try:
|
||||
mark, subpath, *msg_line = data.split(" ", 2)
|
||||
assert mark == "signmessage"
|
||||
# subpath will be verified & cleaned later
|
||||
assert msg_line[0][:6] == "ascii:"
|
||||
text = msg_line[0][6:]
|
||||
return text, subpath, addr_fmt_from_subpath(subpath), is_json
|
||||
except:pass
|
||||
# ===
|
||||
|
||||
try:
|
||||
data_dict = ujson.loads(data.strip())
|
||||
text = data_dict.get("msg", None)
|
||||
if text is None:
|
||||
raise AssertionError("MSG required")
|
||||
subpath = data_dict.get("subpath", subpath)
|
||||
assert isinstance(subpath, str), "subpath"
|
||||
addr_fmt = data_dict.get("addr_fmt", addr_fmt)
|
||||
is_json = True
|
||||
except ValueError:
|
||||
lines = data.split("\n")
|
||||
assert lines, "min 1 line"
|
||||
assert len(lines) <= 3, "max 3 lines"
|
||||
|
||||
if len(lines) == 1:
|
||||
text = lines[0]
|
||||
elif len(lines) == 2:
|
||||
text, subpath = lines
|
||||
else:
|
||||
text, subpath, addr_fmt = lines
|
||||
|
||||
if not addr_fmt:
|
||||
addr_fmt = addr_fmt_from_subpath(subpath)
|
||||
|
||||
if not subpath:
|
||||
try:
|
||||
subpath = chains.STD_DERIVATIONS[addr_fmt]
|
||||
subpath = subpath.format(
|
||||
coin_type=chains.current_chain().b44_cointype,
|
||||
account=0, change=0, idx=0
|
||||
)
|
||||
except: pass
|
||||
|
||||
return text, subpath, addr_fmt, is_json
|
||||
|
||||
|
||||
def make_signature_file_msg(content_list):
|
||||
# list of tuples consisting of (hash, file_name)
|
||||
return b"\n".join([
|
||||
b2a_hex(h) + b" " + fname.encode()
|
||||
for h, fname in content_list
|
||||
])
|
||||
|
||||
def parse_signature_file_msg(msg):
|
||||
# only succeed for our format digest + 2 spaces + fname
|
||||
try:
|
||||
res = []
|
||||
lines = msg.split('\n')
|
||||
for ln in lines:
|
||||
d, fn = ln.split(' ')
|
||||
# should not need to strip if our file format, so dont
|
||||
# is hex? is 32 bytes long?
|
||||
assert len(a2b_hex(d)) == 32
|
||||
res.append((d, fn))
|
||||
|
||||
return res
|
||||
except:
|
||||
return
|
||||
|
||||
def sign_message_digest(digest, subpath, prompt, addr_fmt=AF_CLASSIC, pk=None):
|
||||
# do the signature itself!
|
||||
from glob import dis
|
||||
|
||||
ch = chains.current_chain()
|
||||
|
||||
if prompt:
|
||||
dis.fullscreen(prompt, percent=.25)
|
||||
|
||||
if pk is None:
|
||||
with stash.SensitiveValues() as sv:
|
||||
node = sv.derive_path(subpath)
|
||||
dis.progress_sofar(50, 100)
|
||||
pk = node.privkey()
|
||||
addr = ch.address(node, addr_fmt)
|
||||
else:
|
||||
# if private key is provided, derivation subpath is ignored
|
||||
# and given private key is used for signing.
|
||||
node = node_from_privkey(pk)
|
||||
dis.progress_sofar(50, 100)
|
||||
addr = ch.address(node, addr_fmt)
|
||||
|
||||
dis.progress_sofar(75, 100)
|
||||
|
||||
rv = ngu.secp256k1.sign(pk, digest, 0).to_bytes()
|
||||
|
||||
# AF_CLASSIC header byte base 31 is returned by default from ngu - NOOP
|
||||
if addr_fmt != AF_CLASSIC:
|
||||
# ngu only produces header base for compressed p2pkh, anyways get only rec_id
|
||||
rv = bytearray(rv)
|
||||
rec_id = (rv[0] - 27) & 0x03
|
||||
rv[0] = rec_id + ch.sig_hdr_base(addr_fmt=addr_fmt)
|
||||
|
||||
dis.progress_bar_show(1)
|
||||
|
||||
return rv, addr
|
||||
|
||||
async def ux_sign_msg(txt, approved_cb=None, kill_menu=True):
|
||||
from menu import MenuSystem, MenuItem
|
||||
|
||||
async def done(_1, _2, item):
|
||||
from auth import approve_msg_sign
|
||||
|
||||
text, af = item.arg
|
||||
subpath = await msg_sign_ux_get_subpath(af)
|
||||
if subpath is None: return
|
||||
|
||||
await approve_msg_sign(text, subpath, af, approved_cb=approved_cb,
|
||||
kill_menu=kill_menu, allow_tab_nl=True)
|
||||
|
||||
# pick address format
|
||||
rv = [
|
||||
MenuItem(chains.addr_fmt_label(af), f=done, arg=(txt, af))
|
||||
for af in chains.SINGLESIG_AF
|
||||
]
|
||||
the_ux.push(MenuSystem(rv))
|
||||
|
||||
async def msg_signing_done(signature, address, text):
|
||||
ch = await import_export_prompt("Signed Msg")
|
||||
if ch == KEY_CANCEL:
|
||||
return
|
||||
|
||||
if isinstance(ch, dict):
|
||||
await sd_sign_msg_done(signature, address, text, "msg_sign", **ch)
|
||||
elif version.has_qr and ch == KEY_QR:
|
||||
from ux_q1 import qr_msg_sign_done
|
||||
await qr_msg_sign_done(signature, address, text)
|
||||
elif ch in KEY_NFC+"3":
|
||||
from glob import NFC
|
||||
if NFC:
|
||||
await NFC.msg_sign_done(signature, address, text)
|
||||
|
||||
|
||||
async def sign_with_own_address(subpath, addr_fmt):
|
||||
# used for cases where we already have the key picked, but need the message:
|
||||
# * address_explorer custom path
|
||||
# * positive ownership test
|
||||
|
||||
to_sign = await ux_input_text("", scan_ok=True, prompt="Enter MSG") # max len is 100 only here
|
||||
if not to_sign: return
|
||||
|
||||
from auth import approve_msg_sign
|
||||
await approve_msg_sign(to_sign, subpath, addr_fmt, approved_cb=msg_signing_done, kill_menu=True)
|
||||
|
||||
async def sd_sign_msg_done(signature, address, text, base=None, orig_path=None,
|
||||
slot_b=None, force_vdisk=False):
|
||||
from glob import dis
|
||||
dis.fullscreen('Generating...')
|
||||
|
||||
out_fn = None
|
||||
sig = b2a_base64(signature).decode('ascii').strip()
|
||||
|
||||
while 1:
|
||||
# try to put back into same spot
|
||||
# add -signed to end.
|
||||
target_fname = base + '-signed.txt'
|
||||
lst = [orig_path]
|
||||
if orig_path:
|
||||
lst.append(None)
|
||||
|
||||
for path in lst:
|
||||
try:
|
||||
with CardSlot(readonly=True, slot_b=slot_b, force_vdisk=force_vdisk) as card:
|
||||
out_full, out_fn = card.pick_filename(target_fname, path)
|
||||
out_path = path
|
||||
if out_full: break
|
||||
except CardMissingError:
|
||||
prob = 'Missing card.\n\n'
|
||||
out_fn = None
|
||||
|
||||
if not out_fn:
|
||||
# need them to insert a card
|
||||
prob = ''
|
||||
else:
|
||||
# attempt write-out
|
||||
try:
|
||||
dis.fullscreen("Saving...")
|
||||
|
||||
with CardSlot(slot_b=slot_b, force_vdisk=force_vdisk) as card:
|
||||
with card.open(out_full, 'wt') as fd:
|
||||
# save in full RFC style
|
||||
# gen length is 6
|
||||
gen = rfc_signature_template(addr=address, msg=text, sig=sig)
|
||||
for i, part in enumerate(gen):
|
||||
fd.write(part)
|
||||
|
||||
# success and done!
|
||||
break
|
||||
|
||||
except OSError as exc:
|
||||
prob = 'Failed to write!\n\n%s\n\n' % exc
|
||||
# sys.print_exception(exc)
|
||||
# fall through to try again
|
||||
|
||||
# prompt them to input another card?
|
||||
ch = await ux_show_story(prob + "Please insert an SDCard to receive signed message, "
|
||||
"and press %s." % OK, title="Need Card")
|
||||
if ch == 'x':
|
||||
await ux_aborted()
|
||||
return
|
||||
|
||||
# done.
|
||||
msg = "Created new file:\n\n%s" % out_fn
|
||||
await ux_show_story(msg, title='File Signed')
|
||||
|
||||
|
||||
|
||||
# EOF
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,7 @@
|
||||
#
|
||||
# ndef.py -- NDEF records: making them and parsing them.
|
||||
#
|
||||
# - see ../docs/nfc-on-coldcard.md for background.
|
||||
# - see ../docs/nfc-coldcard.md for background.
|
||||
# - cross platform file
|
||||
#
|
||||
from struct import pack, unpack
|
||||
|
||||
375
shared/nfc.py
375
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,17 @@ class NFCHandler:
|
||||
|
||||
self.set_rf_disable(1)
|
||||
|
||||
async def share_loop(self, n, **kws):
|
||||
# Keep one fully-written tag image live until the user exits. Some
|
||||
# phones perform multiple probes while deciding if a tag is NDEF.
|
||||
await self.big_write(n.bytes())
|
||||
|
||||
while 1:
|
||||
aborted = await self.ux_animation(exit_after_activity=False, **kws)
|
||||
if aborted:
|
||||
await self.wipe(kws.get("is_secret", False))
|
||||
break
|
||||
|
||||
async def share_signed_txn(self, txid, file_offset, txn_len, txn_sha):
|
||||
# we just signed something, share it over NFC
|
||||
if txn_len >= MAX_NFC_SIZE:
|
||||
@ -231,13 +243,20 @@ class NFCHandler:
|
||||
return
|
||||
|
||||
n = ndef.ndefMaker()
|
||||
line2 = None
|
||||
if txid is not None:
|
||||
n.add_text('Signed Transaction: ' + txid)
|
||||
n.add_custom('bitcoin.org:txid', a2b_hex(txid)) # want binary
|
||||
line2 = self.txid_line2(txid)
|
||||
|
||||
n.add_custom('bitcoin.org:sha256', txn_sha)
|
||||
n.add_large_object('bitcoin.org:txn', file_offset, txn_len)
|
||||
|
||||
return await self.share_start(n)
|
||||
return await self.share_loop(n, line2=line2)
|
||||
|
||||
@staticmethod
|
||||
def txid_line2(txid):
|
||||
return "Signed TXID: %s⋯%s" % (txid[0:8], txid[-8:])
|
||||
|
||||
async def share_push_tx(self, url, txid, txn, txn_sha, line2=None):
|
||||
# Given a signed TXN, we convert to URL which a web backend can broadcast directly
|
||||
@ -267,13 +286,9 @@ class NFCHandler:
|
||||
n.add_url(url, https=is_https)
|
||||
|
||||
if line2 is None:
|
||||
line2 = "Signed TXID: %s⋯%s" % (txid[0:8], txid[-8:])
|
||||
line2 = self.txid_line2(txid)
|
||||
|
||||
while 1:
|
||||
done = await self.share_start(n, prompt="Tap to broadcast, CANCEL when done",
|
||||
line2=line2)
|
||||
|
||||
if done: break
|
||||
await self.share_loop(n, prompt="Tap to broadcast, CANCEL when done", line2=line2)
|
||||
|
||||
async def push_tx_from_file(self):
|
||||
# Pick (signed txn) file from SD card and broadcast via PushTx
|
||||
@ -343,24 +358,19 @@ class NFCHandler:
|
||||
return
|
||||
|
||||
n = ndef.ndefMaker()
|
||||
n.add_text(label or 'Partly signed PSBT')
|
||||
label = label or 'Partly signed PSBT'
|
||||
n.add_text(label)
|
||||
n.add_custom('bitcoin.org:sha256', psbt_sha)
|
||||
n.add_large_object('bitcoin.org:psbt', file_offset, psbt_len)
|
||||
|
||||
return await self.share_start(n)
|
||||
|
||||
async def share_deposit_address(self, addr, **kws):
|
||||
n = ndef.ndefMaker()
|
||||
n.add_text('Deposit Address')
|
||||
n.add_custom('bitcoin.org:addr', addr.encode())
|
||||
return await self.share_start(n, **kws)
|
||||
return await self.share_loop(n, line2=label)
|
||||
|
||||
async def share_json(self, json_data, **kws):
|
||||
# a text file of JSON for programs to read
|
||||
n = ndef.ndefMaker()
|
||||
n.add_mime_data('application/json', json_data)
|
||||
|
||||
return await self.share_start(n, **kws)
|
||||
return await self.share_loop(n, **kws)
|
||||
|
||||
async def share_text(self, data, **kws):
|
||||
# share text from a list of values
|
||||
@ -368,7 +378,7 @@ class NFCHandler:
|
||||
n = ndef.ndefMaker()
|
||||
n.add_text(data)
|
||||
|
||||
return await self.share_start(n, **kws)
|
||||
return await self.share_loop(n, **kws)
|
||||
|
||||
async def wait_ready(self):
|
||||
# block until chip ready to continue (ACK happens)
|
||||
@ -394,11 +404,13 @@ class NFCHandler:
|
||||
self.write_dyn(GPO_CTRL_Dyn, 0x01) # GPO_EN
|
||||
self.read_dyn(IT_STS_Dyn) # clear interrupt
|
||||
|
||||
async def ux_animation(self, write_mode, allow_enter=True, prompt=None, line2=None):
|
||||
async def ux_animation(self, allow_enter=True, prompt=None, line2=None,
|
||||
is_secret=False, exit_after_activity=True,
|
||||
min_delay=1000):
|
||||
# Run the pretty animation, and detect both when we are written, and/or key to exit/abort.
|
||||
# - similar when "read" and then removed from field
|
||||
# - return T if aborted by user
|
||||
from glob import dis, numpad
|
||||
from glob import dis
|
||||
|
||||
await self.wait_ready()
|
||||
self.set_rf_disable(0)
|
||||
@ -411,7 +423,8 @@ class NFCHandler:
|
||||
dis.text(None, -3, line2)
|
||||
else:
|
||||
from graphics_mk4 import Graphics
|
||||
frames = [getattr(Graphics, 'mk4_nfc_%d'%i) for i in range(1, 5)]
|
||||
from version import mk_num
|
||||
frames = [getattr(Graphics, 'mk%d_nfc_%d'%(mk_num, i)) for i in range(1, 5)]
|
||||
|
||||
aborted = True
|
||||
phase = -1
|
||||
@ -419,7 +432,6 @@ class NFCHandler:
|
||||
|
||||
# (ms) How long to wait after RF field comes and goes
|
||||
# - user can press OK during this period if they know they are done
|
||||
min_delay = (3000 if write_mode else 1000)
|
||||
|
||||
while 1:
|
||||
if dis.has_lcd:
|
||||
@ -458,7 +470,7 @@ class NFCHandler:
|
||||
aborted = False
|
||||
break
|
||||
|
||||
if last_activity:
|
||||
if exit_after_activity and last_activity:
|
||||
dt = utime.ticks_diff(utime.ticks_ms(), last_activity)
|
||||
if dt >= min_delay:
|
||||
# They acheived some RF activity and then nothing for some time, so
|
||||
@ -467,8 +479,6 @@ class NFCHandler:
|
||||
break
|
||||
|
||||
self.set_rf_disable(1)
|
||||
if not write_mode:
|
||||
await self.wipe(False)
|
||||
|
||||
return aborted
|
||||
|
||||
@ -476,17 +486,15 @@ class NFCHandler:
|
||||
# do the UX while we are sharing a value over NFC
|
||||
# - assumpting is people know what they are scanning
|
||||
# - x key to abort early, but also self-clears
|
||||
|
||||
await self.big_write(ndef_obj.bytes())
|
||||
|
||||
return await self.ux_animation(False, **kws)
|
||||
return await self.ux_animation(**kws)
|
||||
|
||||
async def start_nfc_rx(self, **kws):
|
||||
# Pretend to be a big warm empty tag ready to be stuffed with data
|
||||
await self.big_write(ndef.CC_WR_FILE)
|
||||
|
||||
# wait until something is written
|
||||
aborted = await self.ux_animation(True, **kws)
|
||||
aborted = await self.ux_animation(min_delay=3000, **kws)
|
||||
if aborted: return
|
||||
|
||||
# read CCFILE area (header)
|
||||
@ -514,7 +522,6 @@ class NFCHandler:
|
||||
await self.wipe(False)
|
||||
return rv
|
||||
|
||||
|
||||
async def start_psbt_rx(self):
|
||||
from auth import psbt_encoding_taster, TXN_INPUT_OFFSET
|
||||
from auth import UserAuthorizedAction, ApproveTransaction
|
||||
@ -540,10 +547,7 @@ class NFCHandler:
|
||||
if urn == 'urn:nfc:ext:bitcoin.org:sha256' and len(msg) == 32:
|
||||
# probably produced by another Coldcard: SHA256 over expected contents
|
||||
psbt_sha = bytes(msg)
|
||||
except Exception as e:
|
||||
# dont crash when given garbage
|
||||
import sys; sys.print_exception(e)
|
||||
pass
|
||||
except Exception: pass # dont crash when given garbage
|
||||
|
||||
if psbt_in is None:
|
||||
await ux_show_story("Could not find PSBT in what was written.", title="Sorry!")
|
||||
@ -564,44 +568,13 @@ class NFCHandler:
|
||||
|
||||
# start signing UX
|
||||
UserAuthorizedAction.cleanup()
|
||||
UserAuthorizedAction.active_request = ApproveTransaction(psbt_len, 0x0, psbt_sha=psbt_sha,
|
||||
approved_cb=self.signing_done)
|
||||
UserAuthorizedAction.active_request = ApproveTransaction(
|
||||
psbt_len, psbt_sha=psbt_sha, input_method="nfc",
|
||||
output_encoder=output_encoder
|
||||
)
|
||||
# kill any menu stack, and put our thing at the top
|
||||
the_ux.push(UserAuthorizedAction.active_request)
|
||||
|
||||
async def signing_done(self, psbt):
|
||||
# User approved the PSBT, and signing worked... share result over NFC (only)
|
||||
from auth import TXN_OUTPUT_OFFSET, try_push_tx
|
||||
from version import MAX_TXN_LEN
|
||||
from sffile import SFFile
|
||||
|
||||
txid = None
|
||||
|
||||
# asssume they want final transaction when possible, else PSBT output
|
||||
is_comp = psbt.is_complete()
|
||||
|
||||
# re-serialize the PSBT back out (into PSRAM)
|
||||
with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as fd:
|
||||
if is_comp:
|
||||
txid = psbt.finalize(fd)
|
||||
else:
|
||||
psbt.serialize(fd)
|
||||
|
||||
self.result = (fd.tell(), fd.checksum.digest())
|
||||
|
||||
out_len, out_sha = self.result
|
||||
|
||||
if is_comp:
|
||||
if txid and await try_push_tx(out_len, txid, out_sha):
|
||||
return # success, exit
|
||||
|
||||
await self.share_signed_txn(txid, TXN_OUTPUT_OFFSET, out_len, out_sha)
|
||||
else:
|
||||
await self.share_psbt(TXN_OUTPUT_OFFSET, out_len, out_sha)
|
||||
|
||||
# ? show txid on screen ?
|
||||
# thank them?
|
||||
|
||||
@classmethod
|
||||
async def selftest(cls):
|
||||
# Check for chip present, field present .. and that it works
|
||||
@ -610,9 +583,11 @@ class NFCHandler:
|
||||
n.setup()
|
||||
assert n.uid
|
||||
|
||||
aborted = await n.share_text("NFC is working: %s" % n.get_uid(), allow_enter=False)
|
||||
assert not aborted, "Aborted"
|
||||
nn = ndef.ndefMaker()
|
||||
nn.add_text("NFC is working: %s" % n.get_uid())
|
||||
|
||||
aborted = await n.share_start(nn, allow_enter=False)
|
||||
assert not aborted, "Aborted"
|
||||
|
||||
async def share_file(self):
|
||||
# Pick file from SD card and share over NFC...
|
||||
@ -646,7 +621,7 @@ class NFCHandler:
|
||||
# it's a txn, and we wrote as hex
|
||||
data = a2b_hex(data)
|
||||
else:
|
||||
assert data[2:8] == bytes(6)
|
||||
assert data[1:4] == bytes(3)
|
||||
sha = ngu.hash.sha256s(data)
|
||||
await self.share_signed_txn(txid, data, len(data), sha)
|
||||
elif ext == 'psbt':
|
||||
@ -663,78 +638,52 @@ class NFCHandler:
|
||||
# user is pushing a file downloaded from another CC over NFC
|
||||
# - would need an NFC app in between for the sneakernet step
|
||||
# get some data
|
||||
data = await self.start_nfc_rx()
|
||||
if not data: return
|
||||
def f(m):
|
||||
if len(m) < 70:
|
||||
return
|
||||
m = m.decode()
|
||||
|
||||
winner = None
|
||||
for urn, msg, meta in ndef.record_parser(data):
|
||||
if len(msg) < 70: continue
|
||||
msg = bytes(msg).decode() # from memory view
|
||||
# multi( catches both multi( and sortedmulti(
|
||||
if 'pub' in msg or "multi(" in msg:
|
||||
winner = msg
|
||||
break
|
||||
if 'pub' in m or "multi(" in m:
|
||||
return m
|
||||
|
||||
if not winner:
|
||||
await ux_show_story('Unable to find multisig descriptor.')
|
||||
return
|
||||
winner = await self._nfc_reader(f, 'Unable to find multisig descriptor.')
|
||||
|
||||
from auth import maybe_enroll_xpub
|
||||
try:
|
||||
maybe_enroll_xpub(config=winner)
|
||||
except Exception as e:
|
||||
#import sys; sys.print_exception(e)
|
||||
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
|
||||
if winner:
|
||||
from auth import maybe_enroll_xpub
|
||||
try:
|
||||
maybe_enroll_xpub(config=winner)
|
||||
except Exception as e:
|
||||
#import sys; sys.print_exception(e)
|
||||
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
|
||||
|
||||
async def import_ephemeral_seed_words_nfc(self, *a):
|
||||
data = await self.start_nfc_rx()
|
||||
if not data: return
|
||||
def f(m):
|
||||
sm = m.decode().strip().split(" ")
|
||||
if len(sm) in stash.SEED_LEN_OPTS:
|
||||
return sm
|
||||
|
||||
winner = None
|
||||
for urn, msg, meta in ndef.record_parser(data):
|
||||
msg = bytes(msg).decode().strip() # from memory view
|
||||
split_msg = msg.split(" ")
|
||||
if len(split_msg) in stash.SEED_LEN_OPTS:
|
||||
winner = split_msg
|
||||
break
|
||||
winner = await self._nfc_reader(f, 'Unable to find seed words')
|
||||
|
||||
if not winner:
|
||||
await ux_show_story('Unable to find seed words')
|
||||
return
|
||||
|
||||
try:
|
||||
from seed import set_ephemeral_seed_words
|
||||
await set_ephemeral_seed_words(winner, meta='NFC Import')
|
||||
except Exception as e:
|
||||
#import sys; sys.print_exception(e)
|
||||
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
|
||||
|
||||
async def confirm_share_loop(self, string):
|
||||
while True:
|
||||
# added loop here as NFC send can fail, or not send the data
|
||||
# and in that case one would have to start from beginning (send us cmd, approve, etc.)
|
||||
# => get chance to check if you received the data and if something went wrong - retry just send
|
||||
await self.share_text(string)
|
||||
ch = await ux_show_story(title="Shared", msg="Press %s to share again, otherwise %s to stop." % (OK, X))
|
||||
if ch != "y":
|
||||
break
|
||||
if winner:
|
||||
try:
|
||||
from seed import set_ephemeral_seed_words
|
||||
await set_ephemeral_seed_words(winner, origin='NFC Import')
|
||||
except Exception as e:
|
||||
#import sys; sys.print_exception(e)
|
||||
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
|
||||
|
||||
async def address_show_and_share(self):
|
||||
from auth import show_address, ApproveMessageSign
|
||||
from auth import show_address
|
||||
|
||||
data = await self.start_nfc_rx()
|
||||
if not data: return
|
||||
def f(m):
|
||||
sm = m.decode().split("\n")
|
||||
if 1 <= len(sm) <= 2:
|
||||
return sm
|
||||
|
||||
winner = None
|
||||
for urn, msg, meta in ndef.record_parser(data):
|
||||
msg = bytes(msg).decode() # from memory view
|
||||
split_msg = msg.split("\n")
|
||||
if 1 <= len(split_msg) <= 2:
|
||||
winner = split_msg
|
||||
break
|
||||
winner = await self._nfc_reader(f, 'Expected address and derivation path.')
|
||||
|
||||
if not winner:
|
||||
await ux_show_story('Expected address and derivation path.')
|
||||
return
|
||||
|
||||
if len(winner) == 1:
|
||||
@ -743,7 +692,7 @@ class NFCHandler:
|
||||
else:
|
||||
subpath, addr_fmt_str = winner
|
||||
try:
|
||||
addr_fmt = parse_addr_fmt_str(addr_fmt_str)
|
||||
addr_fmt = chains.parse_addr_fmt_str(addr_fmt_str)
|
||||
except AssertionError as e:
|
||||
await ux_show_story(str(e))
|
||||
return
|
||||
@ -754,131 +703,95 @@ class NFCHandler:
|
||||
await the_ux.interact() # need this otherwise NFC animation takes over
|
||||
|
||||
async def start_msg_sign(self):
|
||||
from auth import UserAuthorizedAction, ApproveMessageSign
|
||||
from ux import the_ux
|
||||
from auth import approve_msg_sign
|
||||
|
||||
UserAuthorizedAction.cleanup()
|
||||
|
||||
data = await self.start_nfc_rx()
|
||||
if not data: return
|
||||
|
||||
winner = None
|
||||
for urn, msg, meta in ndef.record_parser(data):
|
||||
msg = bytes(msg).decode() # from memory view
|
||||
split_msg = msg.split("\n")
|
||||
def f(m):
|
||||
m = m.decode()
|
||||
split_msg = m.split("\n")
|
||||
if 1 <= len(split_msg) <= 3:
|
||||
winner = split_msg
|
||||
break
|
||||
return m
|
||||
|
||||
winner = await self._nfc_reader(f, 'Unable to find correctly formated message to sign.')
|
||||
if not winner:
|
||||
await ux_show_story('Unable to find correctly formated message to sign.')
|
||||
return
|
||||
|
||||
if len(winner) == 1:
|
||||
text = winner[0]
|
||||
subpath = "m"
|
||||
addr_fmt = AF_CLASSIC
|
||||
elif len(winner) == 2:
|
||||
text, subpath = winner
|
||||
addr_fmt = AF_CLASSIC # maybe default to native segwit?
|
||||
else:
|
||||
# len(winner) == 3
|
||||
text, subpath, addr_fmt = winner
|
||||
|
||||
UserAuthorizedAction.check_busy(ApproveMessageSign)
|
||||
try:
|
||||
UserAuthorizedAction.active_request = ApproveMessageSign(
|
||||
text, subpath, addr_fmt, approved_cb=self.msg_sign_done
|
||||
)
|
||||
the_ux.push(UserAuthorizedAction.active_request)
|
||||
except AssertionError as exc:
|
||||
await ux_show_story("Problem: %s\n\nMessage to be signed must be a single line of ASCII text." % exc)
|
||||
return
|
||||
await approve_msg_sign(None, None, None, approved_cb=self.msg_sign_done,
|
||||
msg_sign_request=winner)
|
||||
|
||||
async def msg_sign_done(self, signature, address, text):
|
||||
from auth import rfc_signature_template_gen
|
||||
from msgsign import rfc_signature_template
|
||||
|
||||
sig = b2a_base64(signature).decode('ascii').strip()
|
||||
armored_str = "".join(rfc_signature_template_gen(addr=address, msg=text, sig=sig))
|
||||
await self.confirm_share_loop(armored_str)
|
||||
armored_str = "".join(rfc_signature_template(addr=address, msg=text, sig=sig))
|
||||
await self.share_text(armored_str)
|
||||
|
||||
async def verify_sig_nfc(self):
|
||||
from auth import verify_armored_signed_msg
|
||||
from msgsign import verify_armored_signed_msg
|
||||
|
||||
data = await self.start_nfc_rx()
|
||||
if not data: return
|
||||
f = lambda x: x.decode().strip() if b"SIGNED MESSAGE" in x else None
|
||||
winner = await self._nfc_reader(f, 'Unable to find signed message.')
|
||||
|
||||
winner = None
|
||||
for urn, msg, meta in ndef.record_parser(data):
|
||||
msg = bytes(msg).decode() # from memory view
|
||||
if "SIGNED MESSAGE" in msg:
|
||||
winner = msg.strip()
|
||||
break
|
||||
if winner:
|
||||
await verify_armored_signed_msg(winner, digest_check=False)
|
||||
|
||||
if not winner:
|
||||
await ux_show_story('Unable to find signed message.')
|
||||
return
|
||||
|
||||
await verify_armored_signed_msg(winner, digest_check=False)
|
||||
|
||||
async def verify_address_nfc(self):
|
||||
# Get an address or complete bip-21 url even and search it... slow.
|
||||
async def read_address(self):
|
||||
# Read an address or BIP-21 url and parse out addr (just one)
|
||||
from utils import decode_bip21_text
|
||||
|
||||
data = await self.start_nfc_rx()
|
||||
if not data: return
|
||||
def f(m):
|
||||
m = m.decode()
|
||||
what, vals = decode_bip21_text(m)
|
||||
if what == 'addr':
|
||||
return vals
|
||||
|
||||
winner = None
|
||||
for urn, msg, meta in ndef.record_parser(data):
|
||||
msg = bytes(msg).decode() # from memory view
|
||||
try:
|
||||
what, vals = decode_bip21_text(msg)
|
||||
if what == 'addr':
|
||||
winner = vals[1]
|
||||
break
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if not winner:
|
||||
await ux_show_story('Unable to find address from NFC data.')
|
||||
return
|
||||
|
||||
from ownership import OWNERSHIP
|
||||
await OWNERSHIP.search_ux(winner)
|
||||
|
||||
async def read_extended_private_key(self):
|
||||
data = await self.start_nfc_rx()
|
||||
if not data: return
|
||||
|
||||
winner = None
|
||||
for urn, msg, meta in ndef.record_parser(data):
|
||||
msg = bytes(msg).decode() # from memory view
|
||||
if "prv" in msg:
|
||||
winner = msg.strip()
|
||||
break
|
||||
|
||||
if not winner:
|
||||
await ux_show_story('Unable to find extended private key.')
|
||||
return
|
||||
winner = await self._nfc_reader(f, 'Unable to find address from NFC data.')
|
||||
|
||||
return winner
|
||||
|
||||
async def verify_address_nfc(self):
|
||||
# Get an address or complete bip-21 url even and search it... slow.
|
||||
res = await self.read_address()
|
||||
if not res: return
|
||||
_, addr, args = res
|
||||
from ownership import OWNERSHIP
|
||||
await OWNERSHIP.search_ux(addr, args)
|
||||
|
||||
async def read_extended_private_key(self):
|
||||
f = lambda x: x.decode().strip() if b"prv" in x else None
|
||||
return await self._nfc_reader(f, 'Unable to find extended private key.')
|
||||
|
||||
async def read_tapsigner_b64_backup(self):
|
||||
f = lambda x: a2b_base64(x.decode()) if 150 <= len(x) <= 280 else None
|
||||
return await self._nfc_reader(f, 'Unable to find base64 encoded TAPSIGNER backup.')
|
||||
|
||||
async def read_bip322_msg(self):
|
||||
f = lambda x: x.decode()
|
||||
return await self._nfc_reader(f, 'Unable to find BIP-322 message.')
|
||||
|
||||
async def read_wif(self):
|
||||
# only compressed WIFs allowed
|
||||
f = lambda x: x.decode() if len(x) >= 51 else None
|
||||
return await self._nfc_reader(f, 'Unable to find WIF key(s).')
|
||||
|
||||
async def _nfc_reader(self, func, fail_msg):
|
||||
data = await self.start_nfc_rx()
|
||||
if not data: return
|
||||
|
||||
winner = None
|
||||
for urn, msg, meta in ndef.record_parser(data):
|
||||
msg = bytes(msg).decode() # from memory view
|
||||
try:
|
||||
if 150 <= len(msg) <= 280:
|
||||
winner = a2b_base64(msg)
|
||||
break
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
for urn, msg, meta in ndef.record_parser(data):
|
||||
msg = bytes(msg)
|
||||
try:
|
||||
r = func(msg)
|
||||
if r is not None:
|
||||
winner = r
|
||||
break
|
||||
except:
|
||||
pass
|
||||
except Exception: pass # dont crash when given garbage
|
||||
|
||||
if not winner:
|
||||
await ux_show_story('Unable to find base64 encoded TAPSIGNER backup.')
|
||||
await ux_show_story(fail_msg)
|
||||
return
|
||||
|
||||
return winner
|
||||
|
||||
400
shared/notes.py
400
shared/notes.py
@ -10,10 +10,11 @@ from ux_q1 import QRScannerInteraction
|
||||
from actions import goto_top_menu
|
||||
from glob import settings, dis
|
||||
from files import CardMissingError, needs_microsd, CardSlot
|
||||
from public_constants import MSG_SIGNING_MAX_LENGTH
|
||||
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
|
||||
from charcodes import KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6
|
||||
from lcd_display import CHARS_W
|
||||
from utils import problem_file_line, url_decode
|
||||
from utils import problem_file_line, url_unquote, wipe_if_deltamode, is_printable
|
||||
|
||||
# title, username and such are limited that they fit on the one line both in
|
||||
# text entry (W-2) and also in menu display (W-3)
|
||||
@ -21,7 +22,17 @@ from utils import problem_file_line, url_decode
|
||||
ONE_LINE = CHARS_W-2
|
||||
|
||||
async def make_notes_menu(*a):
|
||||
if settings.get('notes', False) == False:
|
||||
from pincodes import pa
|
||||
|
||||
if pa.hobbled_mode:
|
||||
# Read only version of menu system
|
||||
# - used when spending policy in effect
|
||||
# - must have some notes already, or unreachable
|
||||
rv = NotesMenu(NotesMenu.construct_readonly())
|
||||
rv.readonly = True
|
||||
return rv
|
||||
|
||||
if not settings.get('secnap', False):
|
||||
# Explain feature, and then enable if interested. Drop them into menu.
|
||||
ch = await ux_show_story('''\
|
||||
Enable this feature to store short text notes and passwords inside the Coldcard.
|
||||
@ -34,15 +45,17 @@ Press ENTER to enable and get started otherwise CANCEL.''',
|
||||
if ch != 'y':
|
||||
return
|
||||
|
||||
# mark as enabled (altho empty)
|
||||
settings.set('notes', [])
|
||||
# mark as enabled
|
||||
settings.set('secnap', True)
|
||||
if settings.get('notes', None) is None:
|
||||
settings.set('notes', [])
|
||||
|
||||
# need to correct top menu now, so this choice is there.
|
||||
goto_top_menu()
|
||||
|
||||
return NotesMenu(NotesMenu.construct())
|
||||
|
||||
async def get_a_password(old_value):
|
||||
async def get_a_password(old_value, min_len=0, max_len=128):
|
||||
# Get a (new) password as a string.
|
||||
# - does some fun generation as well.
|
||||
|
||||
@ -96,12 +109,14 @@ async def get_a_password(old_value):
|
||||
handlers = {KEY_F1: _pick_12, KEY_F2: _pick_24, KEY_F3: _pick_dense,
|
||||
KEY_F4: _do_dumb, KEY_F6: _toggle_case, KEY_F5: _bip85}
|
||||
|
||||
return await ux_input_text(old_value, confirm_exit=False, max_len=128, scan_ok=True,
|
||||
b39_complete=True, prompt='Password', placeholder='(optional)',
|
||||
funct_keys=(fmsg, handlers))
|
||||
return await ux_input_text(old_value, confirm_exit=False, max_len=max_len, min_len=min_len,
|
||||
scan_ok=True, b39_complete=True, prompt='Password',
|
||||
placeholder='(optional)', funct_keys=(fmsg, handlers))
|
||||
|
||||
class NotesMenu(MenuSystem):
|
||||
|
||||
readonly = False
|
||||
|
||||
@classmethod
|
||||
def construct(cls):
|
||||
# Dynamic menu with user-defined names of notes shown
|
||||
@ -110,25 +125,70 @@ class NotesMenu(MenuSystem):
|
||||
MenuItem('New Password', f=cls.new_note, arg='p'),
|
||||
ShortcutItem(KEY_QR, f=cls.quick_create)]
|
||||
|
||||
if not NoteContent.count():
|
||||
cnt = NoteContent.count()
|
||||
if not cnt:
|
||||
rv = news + [ MenuItem('Disable Feature', f=cls.disable_notes) ]
|
||||
else:
|
||||
rv = []
|
||||
for note in NoteContent.get_all():
|
||||
rv.append(MenuItem('%d: %s' % (note.idx+1, note.title), menu=note.make_menu))
|
||||
wipe_if_deltamode()
|
||||
|
||||
rv = cls.construct_note_items(readonly=False)
|
||||
|
||||
rv.extend(news)
|
||||
|
||||
rv.append(MenuItem('Export All', f=cls.export_all))
|
||||
|
||||
if cnt >= 2:
|
||||
rv.append(MenuItem('Sort By Title', f=cls.sort_titles))
|
||||
|
||||
rv.append(MenuItem('Import', f=import_from_other))
|
||||
|
||||
return rv
|
||||
|
||||
@classmethod
|
||||
def construct_readonly(cls):
|
||||
# When only allowed to view, no export/add new/delete.
|
||||
wipe_if_deltamode()
|
||||
|
||||
rv = cls.construct_note_items(readonly=True)
|
||||
|
||||
if not rv:
|
||||
rv.append(MenuItem('(none saved yet)'))
|
||||
|
||||
return rv
|
||||
|
||||
@classmethod
|
||||
def construct_note_items(cls, readonly=False):
|
||||
rv = []
|
||||
by_group = {}
|
||||
|
||||
for note in NoteContent.get_all():
|
||||
item = MenuItem('%d: %s' % (note.idx+1, note.title),
|
||||
menu=note.make_menu, arg=readonly)
|
||||
group = note.group
|
||||
if group:
|
||||
if group not in by_group:
|
||||
by_group[group] = []
|
||||
by_group[group].append(item)
|
||||
else:
|
||||
rv.append(item)
|
||||
|
||||
for group in sorted(by_group):
|
||||
rv.append(MenuItem('↳ ' + group, menu=NoteGroupMenu(group, readonly)))
|
||||
|
||||
return rv
|
||||
|
||||
@classmethod
|
||||
async def export_all(cls, *a):
|
||||
await start_export(NoteContent.get_all())
|
||||
|
||||
@classmethod
|
||||
async def sort_titles(cls, menu, _, item):
|
||||
# sort by title, one time and then reconstruct menu
|
||||
NoteContent.sort_all()
|
||||
|
||||
# force redraw
|
||||
menu.update_contents()
|
||||
|
||||
@classmethod
|
||||
async def quick_create(cls, menu, _, item):
|
||||
# using QR, created a Note (never a password) with auto-generated title.
|
||||
@ -145,7 +205,7 @@ class NotesMenu(MenuSystem):
|
||||
|
||||
if got.startswith('otpauth://totp/'):
|
||||
# see <https://github.com/google/google-authenticator/wiki/Key-Uri-Format>
|
||||
tmp.title = url_decode(got[15:]).split('?', 1)[0]
|
||||
tmp.title = url_unquote(got[15:]).split('?', 1)[0]
|
||||
elif got.startswith('otpauth-migration://offline'):
|
||||
# see <https://github.com/qistoph/otp_export>
|
||||
tmp.title = 'Google Auth'
|
||||
@ -159,7 +219,6 @@ class NotesMenu(MenuSystem):
|
||||
await tmp._save_ux(menu)
|
||||
await cls.drill_to(menu, tmp)
|
||||
|
||||
|
||||
def update_contents(self):
|
||||
# Reconstruct the list of notes on this dynamic menu, because
|
||||
# we added or changed them and are showing that same menu again.
|
||||
@ -169,7 +228,8 @@ class NotesMenu(MenuSystem):
|
||||
@classmethod
|
||||
async def disable_notes(cls, *a):
|
||||
# they don't want feature anymore; already checked no notes in effect
|
||||
# - no need for confirm, they aren't loosing anything
|
||||
# - no need for confirm, they aren't losing anything
|
||||
settings.remove_key('secnap')
|
||||
settings.remove_key('notes')
|
||||
settings.save()
|
||||
|
||||
@ -187,9 +247,27 @@ class NotesMenu(MenuSystem):
|
||||
@classmethod
|
||||
async def drill_to(cls, menu, item):
|
||||
# make it so looks like we drilled down into the new note
|
||||
menu.goto_idx(item.idx)
|
||||
m = MenuSystem(await item.make_menu())
|
||||
the_ux.push(m)
|
||||
label = '%d: %s' % (item.idx+1, item.title)
|
||||
group = item.group
|
||||
if group:
|
||||
cls.goto_exact_label(menu, '↳ ' + group)
|
||||
gm = NoteGroupMenu(group)
|
||||
cls.goto_exact_label(gm, label)
|
||||
the_ux.push(gm)
|
||||
else:
|
||||
cls.goto_exact_label(menu, label)
|
||||
|
||||
m = await item._make_menu()
|
||||
the_ux.push(MenuSystem(m))
|
||||
|
||||
@staticmethod
|
||||
def goto_exact_label(menu, label):
|
||||
for i, mi in enumerate(menu.items):
|
||||
if mi.label == label:
|
||||
menu.goto_idx(i)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class NoteContentBase:
|
||||
@ -206,9 +284,15 @@ class NoteContentBase:
|
||||
return PasswordContent(j, idx) if 'user' in j else NoteContent(j, idx)
|
||||
|
||||
def serialize(self):
|
||||
return {fld:getattr(self, fld, '') for fld in self.flds}
|
||||
res = {}
|
||||
for fld in self.flds:
|
||||
val = getattr(self, fld, '')
|
||||
# user field is necessary for proper password identification in constructor
|
||||
if not val and (fld != "user"):
|
||||
continue
|
||||
res[fld] = val
|
||||
|
||||
to_json = serialize
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def get_all(cls):
|
||||
@ -223,6 +307,26 @@ class NoteContentBase:
|
||||
# how many do we have?
|
||||
return len(settings.get('notes', []))
|
||||
|
||||
@classmethod
|
||||
def sort_all(cls):
|
||||
# sort and resave all notes based on title
|
||||
# - careful: self.idx values will be wrong for any existing instances
|
||||
# - 'title' is only common field to subclasses
|
||||
notes = cls.get_all()
|
||||
notes.sort(key=lambda j: j.title.lower())
|
||||
|
||||
settings.put('notes', [n.serialize() for n in notes])
|
||||
settings.save()
|
||||
|
||||
@classmethod
|
||||
def get_groups(cls):
|
||||
groups = set()
|
||||
for note in cls.get_all():
|
||||
if note.group:
|
||||
groups.add(note.group)
|
||||
|
||||
return sorted(groups)
|
||||
|
||||
async def delete(self, *a):
|
||||
# Remove note
|
||||
ok = await ux_confirm("Everything about this note/password will be lost.")
|
||||
@ -243,10 +347,15 @@ class NoteContentBase:
|
||||
the_ux.pop()
|
||||
m = the_ux.top_of_stack()
|
||||
m.update_contents()
|
||||
parent = the_ux.parent_of(m)
|
||||
if parent:
|
||||
parent.update_contents()
|
||||
if isinstance(m, NoteGroupMenu) and not m.has_notes():
|
||||
the_ux.pop()
|
||||
|
||||
await ux_dramatic_pause('Deleted.', 3)
|
||||
|
||||
async def share_nfc(self, menu, _, item):
|
||||
async def share_nfc(self, a, b, item):
|
||||
# share something via NFC -- if small enough and enabled
|
||||
from glob import NFC
|
||||
|
||||
@ -256,16 +365,35 @@ class NoteContentBase:
|
||||
if len(v) < 8000: # see MAX_NFC_SIZE
|
||||
await NFC.share_text(v)
|
||||
|
||||
async def view_qr(self, k):
|
||||
# full screen QR
|
||||
try:
|
||||
await show_qr_code(getattr(self, k), msg=self.title, is_secret=True)
|
||||
except Exception as exc:
|
||||
# - not all data can be a QR (non-text, binary, zeros)
|
||||
# - might be too big for single QR
|
||||
# - may be a RuntimeError(n) where n is line number inside uqr
|
||||
await ux_show_story("Unable to display as QR.\n\nError: " + str(exc))
|
||||
|
||||
async def view_qr_menu(self, a, b, item):
|
||||
await self.view_qr(item.arg)
|
||||
|
||||
async def _save_ux(self, menu):
|
||||
is_new = self.save()
|
||||
|
||||
if not is_new:
|
||||
# change our own menu contents
|
||||
menu.replace_items(await self.make_menu())
|
||||
mi = await self._make_menu()
|
||||
menu.replace_items(mi)
|
||||
|
||||
# update parent
|
||||
parent = the_ux.parent_of(menu)
|
||||
parent.update_contents()
|
||||
grandparent = the_ux.parent_of(parent)
|
||||
if grandparent:
|
||||
grandparent.update_contents()
|
||||
if isinstance(parent, NoteGroupMenu) and not parent.has_notes():
|
||||
the_ux.stack.remove(parent)
|
||||
else:
|
||||
menu.update_contents()
|
||||
|
||||
@ -289,28 +417,137 @@ class NoteContentBase:
|
||||
# single export
|
||||
await start_export([self])
|
||||
|
||||
async def sign_txt_msg(self, a, b, item):
|
||||
from msgsign import ux_sign_msg, msg_signing_done
|
||||
txt = item.arg
|
||||
await ux_sign_msg(txt, approved_cb=msg_signing_done, kill_menu=False)
|
||||
|
||||
def sign_misc_menu_item(self):
|
||||
return MenuItem("Sign Note Text", f=self.sign_txt_msg, arg=self.misc,
|
||||
predicate=2 <= len(self.misc) <= MSG_SIGNING_MAX_LENGTH)
|
||||
|
||||
@staticmethod
|
||||
def is_b39pass_applicable(data, read_only):
|
||||
from seed import MAX_PASS_LEN
|
||||
from ccc import sssp_spending_policy
|
||||
if read_only and not sssp_spending_policy('okeys'):
|
||||
return False
|
||||
return (len(data) <= MAX_PASS_LEN) and is_printable(data) and settings.get("words", True)
|
||||
|
||||
async def apply_as_b39_pass(self, a, b, item):
|
||||
data, readonly = item.arg
|
||||
# rstrip just trailing whitespaces/tabs/newlines
|
||||
data = data.rstrip()
|
||||
# do not allow any more tabs/newlines
|
||||
assert self.is_b39pass_applicable(data, readonly)
|
||||
from seed import apply_pass_value
|
||||
await apply_pass_value(data)
|
||||
|
||||
|
||||
class NoteGroupMenu(MenuSystem):
|
||||
def __init__(self, group, readonly=False):
|
||||
self.group = group
|
||||
self.readonly = readonly
|
||||
super().__init__(self.construct())
|
||||
|
||||
def construct(self):
|
||||
items = []
|
||||
for note in NoteContent.get_all():
|
||||
if note.group == self.group:
|
||||
items.append(MenuItem('%d: %s' % (note.idx+1, note.title),
|
||||
menu=note.make_menu, arg=self.readonly))
|
||||
|
||||
return items or [MenuItem('(none)')]
|
||||
|
||||
def has_notes(self):
|
||||
return any(note.group == self.group for note in NoteContent.get_all())
|
||||
|
||||
def update_contents(self):
|
||||
self.replace_items(self.construct())
|
||||
|
||||
|
||||
class GroupPickerMenu(MenuSystem):
|
||||
def __init__(self, current=''):
|
||||
self.result = None
|
||||
self.current = current
|
||||
|
||||
groups = NoteContentBase.get_groups()
|
||||
chosen = 0
|
||||
items = [MenuItem('(none)', f=self.picked, arg='')]
|
||||
|
||||
for group in groups:
|
||||
if group == self.current:
|
||||
chosen = len(items)
|
||||
items.append(MenuItem(group, f=self.picked, arg=group))
|
||||
|
||||
items.append(MenuItem('New Group', f=self.new_group))
|
||||
|
||||
super().__init__(items, chosen=chosen)
|
||||
|
||||
async def picked(self, menu, idx, mi):
|
||||
assert menu == self
|
||||
self.result = mi.arg
|
||||
the_ux.pop()
|
||||
|
||||
async def new_group(self, menu, idx, mi):
|
||||
group = await ux_input_text('', max_len=ONE_LINE, confirm_exit=False,
|
||||
prompt='Group', placeholder='(optional)')
|
||||
if group is None:
|
||||
self.result = None
|
||||
else:
|
||||
self.result = group
|
||||
|
||||
the_ux.pop()
|
||||
|
||||
@classmethod
|
||||
async def pick(cls, current=''):
|
||||
m = cls(current)
|
||||
the_ux.push(m)
|
||||
await m.interact()
|
||||
|
||||
return current if m.result is None else m.result
|
||||
|
||||
|
||||
class PasswordContent(NoteContentBase):
|
||||
# "Passwords" have a few more fields and are more structured
|
||||
flds = ['title', 'user', 'password', 'site', 'misc' ]
|
||||
flds = ['title', 'user', 'password', 'site', 'misc', 'group']
|
||||
type_label = 'password'
|
||||
|
||||
async def make_menu(self, *a):
|
||||
async def _make_menu(self, readonly=False):
|
||||
rv = [MenuItem('"%s"' % self.title, f=self.view)]
|
||||
if self.user:
|
||||
rv.append(MenuItem('↳ %s' % self.user, f=self.view))
|
||||
if self.site:
|
||||
rv.append(MenuItem('↳ %s' % self.site, f=self.view))
|
||||
#if self.misc: rv.append(MenuItem('↳ (notes)', f=self.view))
|
||||
return rv + [
|
||||
# if self.misc: rv.append(MenuItem('↳ (notes)', f=self.view))
|
||||
rv += [
|
||||
MenuItem('View Password', f=self.view_pw),
|
||||
MenuItem('Send Password', f=self.send_pw, predicate=lambda: settings.get('du', True)),
|
||||
MenuItem('Export', f=self.export),
|
||||
MenuItem('Edit Metadata', f=self.edit),
|
||||
MenuItem('Delete', f=self.delete),
|
||||
MenuItem('Change Password', f=self.change_pw),
|
||||
ShortcutItem(KEY_QR, f=self.view_qr),
|
||||
ShortcutItem(KEY_NFC, f=self.share_nfc, arg='password'),
|
||||
MenuItem('Send Password', f=self.send_pw, predicate=lambda: not settings.get('du', 0)),
|
||||
]
|
||||
if not readonly:
|
||||
rv += [
|
||||
MenuItem('Export', f=self.export),
|
||||
MenuItem('Edit Metadata', f=self.edit),
|
||||
MenuItem('Delete', f=self.delete),
|
||||
MenuItem('Change Password', f=self.change_pw),
|
||||
]
|
||||
rv += [
|
||||
self.sign_misc_menu_item(),
|
||||
ShortcutItem(KEY_QR, f=self.view_qr_menu, arg=self.type_label),
|
||||
ShortcutItem(KEY_NFC, f=self.share_nfc, arg=self.type_label),
|
||||
]
|
||||
|
||||
# if password is less than MAX_PASS_LEN and only consist of printable ASCII characters
|
||||
# and current seed (master or tmp) is word based - offer to apply pwd text as BIP-39 passphrase
|
||||
if self.is_b39pass_applicable(self.password, readonly):
|
||||
rv += [MenuItem('Apply as BIP-39 Passphrase',
|
||||
f=self.apply_as_b39_pass, arg=(self.password, readonly))]
|
||||
|
||||
return rv
|
||||
|
||||
async def make_menu(self, a, b, item):
|
||||
items = await self._make_menu(readonly=item.arg)
|
||||
return MenuSystem(items)
|
||||
|
||||
async def view(self, *a):
|
||||
pl = len(self.password)
|
||||
@ -350,7 +587,7 @@ class PasswordContent(NoteContentBase):
|
||||
ch = await ux_show_story(msg, title=self.title, escape=KEY_QR,
|
||||
hint_icons=KEY_QR)
|
||||
if ch == KEY_QR:
|
||||
await self.view_qr()
|
||||
await self.view_qr(self.type_label)
|
||||
|
||||
async def send_pw(self, *a):
|
||||
# use USB to send it -- weak at present
|
||||
@ -362,10 +599,6 @@ class PasswordContent(NoteContentBase):
|
||||
"we cannot type at this time.")
|
||||
await single_send_keystrokes(self.password)
|
||||
|
||||
async def view_qr(self, *a):
|
||||
# full screen QR
|
||||
await show_qr_code(self.password, msg=self.title)
|
||||
|
||||
async def edit(self, menu, _, item):
|
||||
# Edit, also used for add new
|
||||
|
||||
@ -383,7 +616,8 @@ class PasswordContent(NoteContentBase):
|
||||
|
||||
if self.idx == -1:
|
||||
# prompt for password only on new records.
|
||||
self.password = await get_a_password(self.password)
|
||||
# can be None if CANCEL is pressed - handle, Send Password requires string
|
||||
self.password = await get_a_password(self.password) or ""
|
||||
|
||||
site = await ux_input_text(self.site, max_len=ONE_LINE, scan_ok=True, confirm_exit=False,
|
||||
prompt='Website', placeholder='(optional)')
|
||||
@ -395,6 +629,8 @@ class PasswordContent(NoteContentBase):
|
||||
if misc is None:
|
||||
misc = self.misc
|
||||
|
||||
group = await GroupPickerMenu.pick(self.group)
|
||||
|
||||
if self.idx != -1:
|
||||
# confirm changes, don't for new records
|
||||
chgs = []
|
||||
@ -406,6 +642,8 @@ class PasswordContent(NoteContentBase):
|
||||
chgs.append('Username')
|
||||
if self.misc != misc:
|
||||
chgs.append('Other Notes')
|
||||
if self.group != group:
|
||||
chgs.append('Group')
|
||||
|
||||
if not chgs:
|
||||
await ux_dramatic_pause('No changes.', 3)
|
||||
@ -419,6 +657,7 @@ class PasswordContent(NoteContentBase):
|
||||
self.user = user
|
||||
self.site = site
|
||||
self.misc = misc
|
||||
self.group = group
|
||||
|
||||
await self._save_ux(menu)
|
||||
return self
|
||||
@ -426,36 +665,46 @@ class PasswordContent(NoteContentBase):
|
||||
|
||||
class NoteContent(NoteContentBase):
|
||||
# Pure "notes" have just a title and free-form text
|
||||
flds = ['title', 'misc']
|
||||
flds = ['title', 'misc', 'group']
|
||||
type_label = 'note'
|
||||
|
||||
async def make_menu(self, *a):
|
||||
async def _make_menu(self, readonly=False):
|
||||
# Details and actions for this Note
|
||||
return [
|
||||
|
||||
rv = [
|
||||
MenuItem('"%s"' % self.title, f=self.view),
|
||||
MenuItem('View Note', f=self.view),
|
||||
MenuItem('Edit', f=self.edit),
|
||||
MenuItem('Delete', f=self.delete),
|
||||
MenuItem('Export', f=self.export),
|
||||
ShortcutItem(KEY_QR, f=self.view_qr),
|
||||
]
|
||||
if not readonly:
|
||||
rv += [
|
||||
MenuItem('Edit', f=self.edit),
|
||||
MenuItem('Delete', f=self.delete),
|
||||
MenuItem('Export', f=self.export),
|
||||
]
|
||||
|
||||
rv += [
|
||||
self.sign_misc_menu_item(),
|
||||
ShortcutItem(KEY_QR, f=self.view_qr_menu, arg="misc"),
|
||||
ShortcutItem(KEY_NFC, f=self.share_nfc, arg='misc'),
|
||||
]
|
||||
|
||||
# if misc is less than MAX_PASS_LEN and only consist of printable ASCII characters
|
||||
# and current seed (master or tmp) is word based - offer to apply note text as BIP-39 passphrase
|
||||
if self.is_b39pass_applicable(self.misc, readonly):
|
||||
rv += [MenuItem('Apply as BIP-39 Passphrase',
|
||||
f=self.apply_as_b39_pass, arg=(self.misc, readonly))]
|
||||
|
||||
return rv
|
||||
|
||||
async def make_menu(self, a, b, item):
|
||||
items = await self._make_menu(readonly=item.arg)
|
||||
return MenuSystem(items)
|
||||
|
||||
async def view(self, *a):
|
||||
ch = await ux_show_story(self.misc, title=self.title, escape=KEY_QR,
|
||||
hint_icons=KEY_QR)
|
||||
if ch == KEY_QR:
|
||||
await self.view_qr()
|
||||
|
||||
async def view_qr(self, *a):
|
||||
# full screen QR
|
||||
try:
|
||||
await show_qr_code(self.misc, msg=self.title)
|
||||
except Exception as exc:
|
||||
# - not all data can be a QR (non-text, binary, zeros)
|
||||
# - might be too big for single QR
|
||||
# - may be a RuntimeError(n) where n is line number inside uqr
|
||||
await ux_show_story("Unable to display as QR.\n\nError: "+str(exc))
|
||||
await self.view_qr("misc")
|
||||
|
||||
async def edit(self, menu, _, item):
|
||||
# Edit, also used for add new
|
||||
@ -471,6 +720,8 @@ class NoteContent(NoteContentBase):
|
||||
if misc is None:
|
||||
misc = self.misc
|
||||
|
||||
group = await GroupPickerMenu.pick(self.group)
|
||||
|
||||
if self.idx != -1:
|
||||
# confirm changes, don't for new records
|
||||
chgs = []
|
||||
@ -478,6 +729,8 @@ class NoteContent(NoteContentBase):
|
||||
chgs.append('Title')
|
||||
if self.misc != misc:
|
||||
chgs.append('Note Text')
|
||||
if self.group != group:
|
||||
chgs.append('Group')
|
||||
|
||||
if not chgs:
|
||||
await ux_dramatic_pause('No changes.', 3)
|
||||
@ -490,6 +743,7 @@ class NoteContent(NoteContentBase):
|
||||
|
||||
self.title = title
|
||||
self.misc = misc
|
||||
self.group = group
|
||||
|
||||
await self._save_ux(menu)
|
||||
|
||||
@ -498,16 +752,16 @@ class NoteContent(NoteContentBase):
|
||||
async def start_export(notes):
|
||||
# Save out notes/passwords
|
||||
from glob import NFC
|
||||
from auth import write_sig_file
|
||||
from msgsign import write_sig_file
|
||||
import ujson as json
|
||||
from ux_q1 import show_bbqr_codes
|
||||
|
||||
singular = (len(notes) == 1)
|
||||
|
||||
item = notes[0].type_label if singular else 'all notes & passwords'
|
||||
choice = await import_export_prompt(item, is_import=False, title="Data Export", no_nfc=True,
|
||||
footnotes="\n\nWARNING: No encryption happens here. "
|
||||
"Your secrets will be cleartext.")
|
||||
choice = await import_export_prompt(item, title="Data Export", no_nfc=True,
|
||||
footnotes="WARNING: No encryption happens here."
|
||||
" Your secrets will be cleartext.")
|
||||
if choice == KEY_CANCEL:
|
||||
return
|
||||
|
||||
@ -536,7 +790,7 @@ async def start_export(notes):
|
||||
await needs_microsd()
|
||||
return
|
||||
except Exception as e:
|
||||
await ux_show_story('Failed to write!\n\n\n'+str(e))
|
||||
await ux_show_story('Failed to write!\n\n'+str(e))
|
||||
return
|
||||
|
||||
msg = 'Export file written:\n\n%s\n\nSignature file written:\n\n%s' % (
|
||||
@ -565,14 +819,11 @@ async def import_from_other(menu, *a):
|
||||
else:
|
||||
def contains_json(fname):
|
||||
if not fname.endswith('.json'): return False
|
||||
print(fname)
|
||||
try:
|
||||
obj = json.load(open(fname, 'rt'))
|
||||
assert 'coldcard_notes' in obj
|
||||
return True
|
||||
except Exception as exc:
|
||||
import sys; sys.print_exception(exc)
|
||||
pass
|
||||
except: pass
|
||||
|
||||
fn = await file_picker(min_size=8, max_size=100000, taster=contains_json, **choice)
|
||||
if not fn: return
|
||||
@ -581,7 +832,14 @@ async def import_from_other(menu, *a):
|
||||
records = json.load(open(fn, 'rt'))
|
||||
|
||||
# We have some JSON, parsed now.
|
||||
# - should dedup, but we aren't
|
||||
ok = await import_from_json(records)
|
||||
if not ok: return
|
||||
|
||||
await ux_dramatic_pause('Saved.', 3)
|
||||
menu.update_contents()
|
||||
|
||||
async def import_from_json(records):
|
||||
# should dedup, but we aren't
|
||||
try:
|
||||
assert 'coldcard_notes' in records, 'Incorrect format'
|
||||
|
||||
@ -591,14 +849,12 @@ async def import_from_other(menu, *a):
|
||||
|
||||
was = list(settings.get('notes', []))
|
||||
was.extend(new)
|
||||
settings.put('notes', was)
|
||||
settings.set('notes', was)
|
||||
settings.set('secnap', True)
|
||||
settings.save()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
await ux_show_story(title="Failure", msg=str(e) + '\n\n' + problem_file_line(e))
|
||||
|
||||
await ux_dramatic_pause('Saved.', 3)
|
||||
menu.update_contents()
|
||||
|
||||
|
||||
# EOF
|
||||
|
||||
@ -8,13 +8,13 @@
|
||||
# - recover from empty/blank/failed chips w/o user action
|
||||
#
|
||||
# Result:
|
||||
# - up to 4k of values supported (after json encoding)
|
||||
# - encrypted and stored in SPI flash, in last 128k area
|
||||
# - up to a few k of values supported (after json encoding)
|
||||
# - encrypted and stored in main flash, in a dedicated 512k area
|
||||
# - AES encryption key is derived from actual wallet secret
|
||||
# - if logged out, then use fixed key instead (ie. it's public)
|
||||
# - you cannot move data between slots because AES-CTR with CTR seed based on slot #
|
||||
# - SHA-256 check on decrypted data
|
||||
# - (Mk4) each slot is a file on /flash/settings
|
||||
# - each "slot" is a file in /flash/settings; in Mk1-3 was SPI flash block
|
||||
# - os.sync() not helpful because block device under filesystem doesnt implement it
|
||||
#
|
||||
import os, ujson, ustruct, ckcc, gc, ngu, aes256ctr, version
|
||||
@ -56,6 +56,7 @@ from utils import call_later_ms
|
||||
# seedvault = (bool) opt-in enable seed vault feature
|
||||
# seeds = list of stored secrets for seedvault feature
|
||||
# bright = (int:0-255) LCD brightness when on battery
|
||||
# secnap = (bool) opt-in enable Secure Notes & Passwords feature
|
||||
# notes = (complex) Secure notes held for user, see notes.py
|
||||
# accts = (list of tuples: (addr_fmt, account#)) Single-sig wallets we've seen them use
|
||||
# aei = (bool) allow changing start index in Address Explorer
|
||||
@ -63,6 +64,12 @@ from utils import call_later_ms
|
||||
# ptxurl = (str) URL for PushTx feature, clear to disable feature
|
||||
# hmx = (bool) Force display of current XFP in home menu, even w/o tmp seed active
|
||||
# unsort_ms = (bool) Allow unsorted multisig with BIP-67 disabled
|
||||
# msas = multisig address show (do not censor multisig addresses)
|
||||
# ccc = (complex) If present, CCC feature is enabled and key details stored here.
|
||||
# ktrx = (privkey) Key teleport Rx has been started, this will be our keypair
|
||||
# sssp = (complex) If present, a (single signer) spending-policy is defined (maybe disabled)
|
||||
# lfr = (string) If present, the reason why Spending Policy blocked last transaction
|
||||
# wifs = (list) List of tuples (public/private key)
|
||||
|
||||
# Stored w/ key=00 for access before login
|
||||
# _skip_pin = hard code a PIN value (dangerous, only for debug)
|
||||
@ -82,10 +89,13 @@ from utils import call_later_ms
|
||||
# prelogin settings - do not need to be part of other saved settings
|
||||
# PRELOGIN_SETTINGS = ["_skip_pin", "nick", "rngk", "lgto", "kbtn", "terms_ok"]
|
||||
# keep these settings only if unspecified on the other end
|
||||
KEEP_IF_BLANK_SETTINGS = ["bkpw", "wa", "sighshchk", "emu", "rz", "b39skip",
|
||||
"axskip", "del", "pms", "idle_to", "batt_to", "bright"]
|
||||
KEEP_IF_BLANK_SETTINGS = ["wa", "sighshchk", "emu", "rz", "b39skip",
|
||||
"axskip", "del", "pms", "idle_to", "batt_to",
|
||||
"bright", "msas"]
|
||||
|
||||
SEEDVAULT_FIELDS = ['seeds', 'seedvault', 'xfp', 'words']
|
||||
# key value pairs saved directly to master seed settings
|
||||
# held in RAM for tmp seed sessions
|
||||
MASTER_FIELDS = ['seeds', 'seedvault', 'xfp', 'words', "bkpw", "sssp"]
|
||||
|
||||
NUM_SLOTS = const(100)
|
||||
SLOTS = range(NUM_SLOTS)
|
||||
@ -175,6 +185,13 @@ class SettingsObject:
|
||||
return (blocks-bfree) / blocks
|
||||
|
||||
def _open_file(self, pos, mode='rb'):
|
||||
if 'w' in mode:
|
||||
# make directory, when needed (recovery/robustness)
|
||||
try:
|
||||
os.stat(MK4_WORKDIR)
|
||||
except OSError: # ENOENT
|
||||
os.mkdir(MK4_WORKDIR[:-1])
|
||||
|
||||
return open(MK4_FILENAME(pos), mode)
|
||||
|
||||
def _slot_is_blank(self, pos, buf):
|
||||
@ -191,13 +208,13 @@ class SettingsObject:
|
||||
fn = MK4_FILENAME(pos)
|
||||
try:
|
||||
os.remove(fn)
|
||||
except Exception:
|
||||
# Error (ENOENT) expected here when saving first time, because the
|
||||
except:
|
||||
# OSError (ENOENT) expected here when saving first time, because the
|
||||
# "old" slot was not in use
|
||||
pass
|
||||
|
||||
def _read_slot(self, pos, decryptor):
|
||||
# Mk4 is just reading a binary file and decrypt as we go.
|
||||
# read a binary file and decrypt as we go.
|
||||
with self._open_file(pos) as fd:
|
||||
# missing ftell(), so emulate
|
||||
ln = fd.seek(0, 2)
|
||||
@ -242,9 +259,12 @@ class SettingsObject:
|
||||
fd.write(aes(chk.digest()))
|
||||
|
||||
def _used_slots(self):
|
||||
# mk4: faster list of slots in use; doesn't open them
|
||||
files = os.listdir(MK4_WORKDIR)
|
||||
return [int(fn[0:-4], 16) for fn in files if fn.endswith('.aes')]
|
||||
# list of slots in use; doesn't open them
|
||||
try:
|
||||
files = os.listdir(MK4_WORKDIR)
|
||||
return [int(fn[0:-4], 16) for fn in files if fn.endswith('.aes')]
|
||||
except:
|
||||
return []
|
||||
|
||||
def _nonempty_slots(self, dis=None):
|
||||
# generate slots that are non-empty
|
||||
@ -265,10 +285,11 @@ class SettingsObject:
|
||||
|
||||
def leaving_master_seed(self):
|
||||
# going from master seed to a tmp seed, so capture a few values we need.
|
||||
self.save_if_dirty()
|
||||
|
||||
SettingsObject.master_nvram_key = self.nvram_key
|
||||
|
||||
for fn in SEEDVAULT_FIELDS:
|
||||
for fn in MASTER_FIELDS:
|
||||
curr = self.current.get(fn, None)
|
||||
if curr is not None:
|
||||
SettingsObject.master_sv_data[fn] = curr
|
||||
@ -284,7 +305,7 @@ class SettingsObject:
|
||||
SettingsObject.master_sv_data.clear()
|
||||
SettingsObject.master_nvram_key = None
|
||||
|
||||
def master_set(self, key, value):
|
||||
def master_set(self, key, value, master_only=False):
|
||||
# Set a value, and it must be saved under the master seed's
|
||||
# Concern is we may be changing a setting from a tmp seed mode
|
||||
# - always does a save
|
||||
@ -295,6 +316,7 @@ class SettingsObject:
|
||||
self.set(key, value)
|
||||
self.save()
|
||||
else:
|
||||
assert not master_only
|
||||
# harder, slower: have to load, change and write
|
||||
master = SettingsObject(nvram_key=SettingsObject.master_nvram_key)
|
||||
master.load()
|
||||
@ -303,7 +325,7 @@ class SettingsObject:
|
||||
del master
|
||||
|
||||
# track our copies
|
||||
if key in SEEDVAULT_FIELDS:
|
||||
if key in MASTER_FIELDS:
|
||||
SettingsObject.master_sv_data[key] = value
|
||||
|
||||
def master_get(self, kn, default=None):
|
||||
@ -315,7 +337,7 @@ class SettingsObject:
|
||||
return self.get(kn, default)
|
||||
|
||||
# LIMITATION: only supporting a few values we know we will need
|
||||
assert kn in SEEDVAULT_FIELDS
|
||||
assert kn in MASTER_FIELDS
|
||||
res = SettingsObject.master_sv_data.get(kn, default)
|
||||
if res is None:
|
||||
return default
|
||||
@ -391,8 +413,9 @@ class SettingsObject:
|
||||
set = put
|
||||
|
||||
def remove_key(self, kn):
|
||||
self.current.pop(kn, None)
|
||||
self.changed()
|
||||
if kn in self.current:
|
||||
self.current.pop(kn, None)
|
||||
self.changed()
|
||||
|
||||
def merge_previous_active(self, previous):
|
||||
import pyb
|
||||
@ -400,7 +423,7 @@ class SettingsObject:
|
||||
|
||||
if previous:
|
||||
for k in KEEP_IF_BLANK_SETTINGS:
|
||||
if k in previous and k not in self.current:
|
||||
if (k in previous) and (k not in self.current):
|
||||
self.current[k] = previous[k]
|
||||
|
||||
# nfc, usb, vidsk handling
|
||||
@ -450,11 +473,8 @@ class SettingsObject:
|
||||
call_later_ms(250, self.write_out)
|
||||
|
||||
def find_spot(self, not_here=0):
|
||||
# search for a blank sector to use
|
||||
# - check randomly and pick first blank one (wear leveling, deniability)
|
||||
# - we will write and then erase old slot
|
||||
# search for a blank slot to use
|
||||
# - if "full", blow away a random one
|
||||
# on mk4, use the filesystem to see what's already taken
|
||||
avail = set(SLOTS) - set(self._used_slots())
|
||||
avail.discard(not_here)
|
||||
|
||||
|
||||
@ -2,16 +2,18 @@
|
||||
#
|
||||
# ownership.py - store a cache of hashes related to addresses we might control.
|
||||
#
|
||||
import os, sys, chains, ngu, struct, version
|
||||
import os, chains, ngu, struct, version
|
||||
from glob import settings
|
||||
from ucollections import namedtuple
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from ubinascii import unhexlify as a2b_hex
|
||||
from exceptions import UnknownAddressExplained
|
||||
from utils import problem_file_line, show_single_address, validate_own_address
|
||||
from public_constants import AFC_SCRIPT, AF_P2WPKH_P2SH, AF_P2SH, AF_P2WSH_P2SH, AF_P2TR, AF_P2WSH
|
||||
|
||||
# Track many addresses, but in compressed form
|
||||
# - map from random Bech32/Base58 payment address to (wallet) + keypath
|
||||
# - only normal (external, not change) addresses, and won't consider
|
||||
# any keypath that does not end in 0/*
|
||||
# - won't consider any keypath that does not end in <0;1>/*
|
||||
# - store only hints, since we can re-construct any address and want to fully verify
|
||||
# - try to keep private between different duress wallets, and seed vaults
|
||||
# - storing bulk data into LFS, not settings
|
||||
@ -38,7 +40,7 @@ OWNERSHIP_MAGIC = 0x10A0 # "Address Ownership" v1.0
|
||||
|
||||
# target 3 flash blocks, max file size => 764 addresses
|
||||
MAX_ADDRS_STORED = const(764) # =((3*512) - OWNERSHIP_FILE_HDR_LEN) // HASH_ENC_LEN
|
||||
BONUS_GAP_LIMIT = const(20)
|
||||
BONUS_AFTER_MATCH = const(20) # number of addresses to still generate after match found
|
||||
|
||||
def encode_addr(addr, salt):
|
||||
# Convert text address to something we can store while preserving privacy.
|
||||
@ -55,6 +57,7 @@ class AddressCacheFile:
|
||||
self.salt = h[32:]
|
||||
self.count = 0
|
||||
self.hdr = None
|
||||
self.fd = None
|
||||
|
||||
self.peek()
|
||||
|
||||
@ -64,9 +67,6 @@ class AddressCacheFile:
|
||||
rv += ' (change)'
|
||||
return rv
|
||||
|
||||
def exists(self):
|
||||
return bool(self.count)
|
||||
|
||||
def peek(self):
|
||||
# see what we have on-disk; just reads header.
|
||||
try:
|
||||
@ -80,7 +80,7 @@ class AddressCacheFile:
|
||||
except OSError:
|
||||
return
|
||||
except Exception as exc:
|
||||
sys.print_exception(exc)
|
||||
# sys.print_exception(exc)
|
||||
self.count = 0
|
||||
self.hdr = None
|
||||
return
|
||||
@ -104,15 +104,14 @@ class AddressCacheFile:
|
||||
self.fd.write(hdr)
|
||||
|
||||
def append(self, addr):
|
||||
if addr is None:
|
||||
# close file, done
|
||||
self.fd.close()
|
||||
del self.fd
|
||||
return
|
||||
|
||||
assert '_' not in addr
|
||||
self.fd.write(encode_addr(addr, self.salt))
|
||||
|
||||
def close(self):
|
||||
# close file, done
|
||||
if self.fd is not None:
|
||||
self.fd.close()
|
||||
self.fd = None
|
||||
|
||||
def fast_search(self, addr):
|
||||
# Do the easy part of the searching, using the existing file's contents.
|
||||
# - generates candidate path subcomponents; might be false positive
|
||||
@ -120,6 +119,7 @@ class AddressCacheFile:
|
||||
from glob import dis
|
||||
|
||||
if not self.hdr or not self.count:
|
||||
# cache empty
|
||||
return
|
||||
|
||||
with open(self.fname, 'rb') as fd:
|
||||
@ -131,7 +131,7 @@ class AddressCacheFile:
|
||||
chk = encode_addr(addr, self.salt)
|
||||
for idx in range(self.count):
|
||||
if buf[idx*HASH_ENC_LEN : (idx*HASH_ENC_LEN)+HASH_ENC_LEN] == chk:
|
||||
yield (self.change_idx, idx)
|
||||
yield self.change_idx, idx
|
||||
|
||||
dis.progress_sofar(idx, self.count)
|
||||
|
||||
@ -147,93 +147,106 @@ class AddressCacheFile:
|
||||
# - return subpath for a hit or None
|
||||
from glob import dis
|
||||
|
||||
bonus = 0
|
||||
match = None
|
||||
|
||||
start_idx = self.count
|
||||
count = MAX_ADDRS_STORED - start_idx
|
||||
|
||||
if count <= 0:
|
||||
return None
|
||||
return match
|
||||
|
||||
self.setup(self.change_idx, start_idx)
|
||||
|
||||
bonus = None
|
||||
for idx,here,*_ in self.wallet.yield_addresses(start_idx, count,
|
||||
change_idx=self.change_idx):
|
||||
|
||||
if here == addr:
|
||||
# Found it! But keep going a little for next time.
|
||||
match = (self.change_idx, idx)
|
||||
|
||||
change_idx=self.change_idx):
|
||||
self.append(here)
|
||||
self.count += 1
|
||||
if match:
|
||||
|
||||
if bonus:
|
||||
if bonus >= BONUS_AFTER_MATCH:
|
||||
# do (at most) 20 more - limited by 'start_idx' & 'count'
|
||||
break
|
||||
bonus += 1
|
||||
|
||||
if match and bonus >= BONUS_GAP_LIMIT:
|
||||
self.append(None)
|
||||
return match
|
||||
|
||||
dis.progress_sofar(idx-start_idx, count)
|
||||
if here == addr:
|
||||
# match but keep going
|
||||
match = (self.change_idx, idx)
|
||||
bonus = 1
|
||||
|
||||
self.append(None)
|
||||
dis.progress_sofar(idx - start_idx, count)
|
||||
|
||||
return None
|
||||
self.close()
|
||||
return match
|
||||
|
||||
class OwnershipCache:
|
||||
|
||||
@classmethod
|
||||
def saver(cls, wallet, change_idx, start_idx):
|
||||
# when we are generating many addresses for export, capture them
|
||||
def saver(cls, wallet, change_idx, start_idx, count):
|
||||
# when we are generating many addresses for export, capture them (if suitable)
|
||||
# as we go with this function
|
||||
# - not change -- only main addrs
|
||||
if not count:
|
||||
return
|
||||
if change_idx not in (0, 1):
|
||||
return
|
||||
if start_idx >= MAX_ADDRS_STORED:
|
||||
return
|
||||
|
||||
file = AddressCacheFile(wallet, change_idx)
|
||||
current_pos = file.count
|
||||
|
||||
if file.exists():
|
||||
# don't save to existing file, has some already
|
||||
return None
|
||||
if start_idx > current_pos:
|
||||
# nothing to do here, we are missing some addresses in the middle
|
||||
return
|
||||
if (start_idx + count) <= current_pos:
|
||||
# we already have all these addresses
|
||||
return
|
||||
|
||||
try:
|
||||
file.setup(change_idx, start_idx)
|
||||
except:
|
||||
# in some cases we don't want to save anything, not an error
|
||||
return None
|
||||
file.setup(change_idx, current_pos)
|
||||
|
||||
return file.append
|
||||
def doit(addr, idx):
|
||||
if addr is None:
|
||||
file.close()
|
||||
elif (idx < MAX_ADDRS_STORED) and idx >= current_pos:
|
||||
file.append(addr)
|
||||
|
||||
return doit
|
||||
|
||||
@classmethod
|
||||
def search(cls, addr):
|
||||
# Find it!
|
||||
# - returns wallet object, and tuple2 of final 2 subpath components
|
||||
def filter(cls, addr_fmt, args):
|
||||
# Filter possible candidates!
|
||||
# - if you start w/ testnet, we'll follow that
|
||||
from multisig import MultisigWallet
|
||||
from public_constants import AFC_SCRIPT, AF_P2WPKH_P2SH, AF_P2SH, AF_P2WSH_P2SH
|
||||
from glob import dis
|
||||
|
||||
ch = chains.current_chain()
|
||||
args = args or {}
|
||||
|
||||
addr_fmt = ch.possible_address_fmt(addr)
|
||||
if not addr_fmt:
|
||||
# might be valid address over on testnet vs mainnet
|
||||
nm = ch.name if ch.ctype != 'BTC' else 'Bitcoin Mainnet'
|
||||
raise UnknownAddressExplained('That address is not valid on ' + nm)
|
||||
# user has specified specific (named) wallet
|
||||
named_wal = args.get("wallet", None)
|
||||
if named_wal:
|
||||
# quick search without deserialization
|
||||
res = list(MultisigWallet.iter_wallets(name=named_wal))
|
||||
if not res:
|
||||
raise UnknownAddressExplained("Wallet '%s' not defined." % named_wal)
|
||||
|
||||
# only return desired named wallet, no other wallets are searched
|
||||
return res
|
||||
|
||||
possibles = []
|
||||
|
||||
if addr_fmt & AFC_SCRIPT:
|
||||
# multisig or script at least.. must exist already
|
||||
possibles.extend(MultisigWallet.iter_wallets(addr_fmt=addr_fmt))
|
||||
|
||||
# multisig or script at least... must exist already
|
||||
afs = [addr_fmt]
|
||||
if addr_fmt == AF_P2SH:
|
||||
# might look like P2SH but actually be AF_P2WSH_P2SH
|
||||
possibles.extend(MultisigWallet.iter_wallets(addr_fmt=AF_P2WSH_P2SH))
|
||||
# wrapped segwit is more used than legacy
|
||||
afs = [AF_P2WSH_P2SH, AF_P2SH]
|
||||
|
||||
# Might be single-sig p2wpkh wrapped in p2sh ... but that was a transition
|
||||
# thing that hopefully is going away, so if they have any multisig wallets,
|
||||
# defined, assume that that's the only p2sh address source.
|
||||
addr_fmt = AF_P2WPKH_P2SH
|
||||
|
||||
# TODO: add tapscript and such fancy stuff here
|
||||
possibles.extend(MultisigWallet.iter_wallets(addr_fmts=afs))
|
||||
|
||||
try:
|
||||
# Construct possible single-signer wallets, always at least account=0 case
|
||||
@ -247,89 +260,145 @@ class OwnershipCache:
|
||||
if af == addr_fmt and acct_num:
|
||||
w = MasterSingleSigWallet(addr_fmt, account_idx=acct_num)
|
||||
possibles.append(w)
|
||||
except ValueError: pass # if not single sig address format
|
||||
except (KeyError, ValueError):
|
||||
pass # if not single sig address format
|
||||
|
||||
if not possibles:
|
||||
# can only happen w/ scripts; for single-signer we have things to check
|
||||
raise UnknownAddressExplained(
|
||||
"No suitable multisig wallets are currently defined.")
|
||||
"No suitable multisig wallets are currently defined.")
|
||||
|
||||
# ordering here
|
||||
return possibles
|
||||
|
||||
@classmethod
|
||||
def search_wallet_cache(cls, addr, cf):
|
||||
# - returns wallet object, and tuple2 of final 2 subpath components
|
||||
# "quick" check first, before doing any generations
|
||||
# external chain first, then internal (change)
|
||||
for maybe in cf.fast_search(addr):
|
||||
ok = cf.check_match(addr, maybe)
|
||||
if ok:
|
||||
return cf.wallet, maybe
|
||||
return None, None
|
||||
|
||||
count = 0
|
||||
phase2 = []
|
||||
for change_idx in (0, 1):
|
||||
files = [AddressCacheFile(w, change_idx) for w in possibles]
|
||||
for f in files:
|
||||
if dis.has_lcd:
|
||||
dis.fullscreen('Searching wallet(s)...', line2=f.nice_name())
|
||||
else:
|
||||
dis.fullscreen('Searching...')
|
||||
|
||||
for maybe in f.fast_search(addr):
|
||||
ok = f.check_match(addr, maybe)
|
||||
if not ok: continue # false positive - will happen
|
||||
|
||||
# found winner.
|
||||
return f.wallet, maybe
|
||||
|
||||
if f.count < MAX_ADDRS_STORED:
|
||||
phase2.append(f)
|
||||
|
||||
count += f.count
|
||||
|
||||
@classmethod
|
||||
def search_build_wallet(cls, addr, cf):
|
||||
# maybe we haven't calculated all the addresses yet, so do that
|
||||
# - very slow, but only needed once; any negative (failed) search causes this
|
||||
# - could stop when match found, but we go a bit beyond that for next time
|
||||
# - we could search all in parallel, rather than serially because
|
||||
# more likely to find a match with low index... but seen as too much memory
|
||||
|
||||
for f in phase2:
|
||||
b4 = f.count
|
||||
if dis.has_lcd:
|
||||
dis.fullscreen("Generating addresses...", line2=f.nice_name())
|
||||
else:
|
||||
dis.fullscreen("Generating...")
|
||||
|
||||
result = f.build_and_search(addr)
|
||||
if result:
|
||||
# found it, so report it and stop
|
||||
return f.wallet, result
|
||||
|
||||
count += f.count - b4
|
||||
result = cf.build_and_search(addr)
|
||||
if result:
|
||||
# found it, so report it and stop
|
||||
return cf.wallet, result
|
||||
|
||||
# possible phase 3: other seedvault... slow, rare and not implemented
|
||||
|
||||
raise UnknownAddressExplained('Searched %d candidates without finding a match.' % count)
|
||||
return None, None
|
||||
|
||||
@classmethod
|
||||
async def search_ux(cls, addr):
|
||||
def search(cls, addr, args=None):
|
||||
from glob import dis
|
||||
|
||||
dis.fullscreen("Wait...")
|
||||
|
||||
try:
|
||||
addr, addr_fmt = validate_own_address(addr)
|
||||
except Exception as e:
|
||||
raise UnknownAddressExplained('That address is not valid on ' + e.args[0])
|
||||
|
||||
matches = OWNERSHIP.filter(addr_fmt, args)
|
||||
|
||||
# build cache files for both external & internal chain
|
||||
cachefs = []
|
||||
for w in matches:
|
||||
cachefs.append(AddressCacheFile(w, 0))
|
||||
cachefs.append(AddressCacheFile(w, 1))
|
||||
|
||||
for cf in cachefs:
|
||||
msg = "Searching wallet(s)..." if dis.has_lcd else "Searching..."
|
||||
dis.fullscreen(msg, line2=cf.nice_name())
|
||||
wallet, subpath = OWNERSHIP.search_wallet_cache(addr, cf)
|
||||
if wallet:
|
||||
# first arg from_cache=True
|
||||
return True, wallet, subpath
|
||||
|
||||
# nothing found in existing cache files
|
||||
c = 0
|
||||
for cf in cachefs:
|
||||
msg = "Generating addresses..." if dis.has_lcd else "Generating..."
|
||||
dis.fullscreen(msg, line2=cf.nice_name())
|
||||
wallet, subpath = OWNERSHIP.search_build_wallet(addr, cf)
|
||||
c += cf.count
|
||||
if wallet:
|
||||
# first arg from_cache=False
|
||||
return False, wallet, subpath
|
||||
|
||||
# nothing found among singlesig & registered multisig wallets
|
||||
# check WIF store (single sig only)
|
||||
if addr_fmt not in [AF_P2TR, AF_P2WSH]:
|
||||
dis.fullscreen("WIF Store...")
|
||||
from wif import iter_wif_store_addresses
|
||||
target_af = AF_P2WPKH_P2SH if addr_fmt == AF_P2SH else addr_fmt
|
||||
for i, store_addr in iter_wif_store_addresses(target_af):
|
||||
if store_addr == addr:
|
||||
return False, ("wif", target_af), i+1
|
||||
|
||||
raise UnknownAddressExplained('Searched %d candidate addresses in %d wallet(s)'
|
||||
' without finding a match.' % (c, len(matches)))
|
||||
|
||||
@classmethod
|
||||
async def search_ux(cls, addr, args):
|
||||
# Provide a simple UX. Called functions do fullscreen, progress bar stuff.
|
||||
from ux import ux_show_story, show_qr_code
|
||||
from charcodes import KEY_QR
|
||||
from multisig import MultisigWallet
|
||||
from public_constants import AFC_BECH32, AFC_BECH32M
|
||||
|
||||
try:
|
||||
wallet, subpath = OWNERSHIP.search(addr)
|
||||
|
||||
msg = addr
|
||||
msg += '\n\nFound in wallet:\n ' + wallet.name
|
||||
msg += '\nDerivation path:\n ' + wallet.render_path(*subpath)
|
||||
if version.has_qwerty:
|
||||
esc = KEY_QR
|
||||
_, wallet, subpath = cls.search(addr, args)
|
||||
is_ms = isinstance(wallet, MultisigWallet)
|
||||
msg = show_single_address(addr)
|
||||
esc = ""
|
||||
if isinstance(wallet, tuple) and (wallet[0] == "wif"):
|
||||
msg += '\n\nFound in WIF store at index %d' % subpath
|
||||
addr_fmt = wallet[1]
|
||||
else:
|
||||
msg += '\n\nPress (1) for QR'
|
||||
esc = '1'
|
||||
sp = wallet.render_path(*subpath)
|
||||
msg += '\n\nFound in wallet:\n ' + wallet.name
|
||||
msg += '\nDerivation path:\n ' + sp
|
||||
addr_fmt = wallet.addr_fmt
|
||||
if not is_ms:
|
||||
esc = "0"
|
||||
msg += "\n\nPress (0) to sign message with this key."
|
||||
|
||||
title = "Verified"
|
||||
if version.has_qwerty:
|
||||
esc += KEY_QR
|
||||
title += " Address"
|
||||
else:
|
||||
msg += ' Press (1) for address QR.'
|
||||
esc += '1'
|
||||
title += "!"
|
||||
|
||||
while 1:
|
||||
ch = await ux_show_story(msg, title="Verified Address",
|
||||
escape=esc, hint_icons=KEY_QR)
|
||||
if ch != esc: break
|
||||
await show_qr_code(addr, is_alnum=(wallet.addr_fmt & (AFC_BECH32 | AFC_BECH32M)),
|
||||
msg=addr)
|
||||
ch = await ux_show_story(msg, title=title, escape=esc, hint_icons=KEY_QR)
|
||||
if ch in ("1"+KEY_QR):
|
||||
await show_qr_code(addr, msg=addr, is_addrs=True,
|
||||
is_alnum=(addr_fmt & (AFC_BECH32 | AFC_BECH32M)))
|
||||
elif not is_ms and (ch == "0"): # only singlesig
|
||||
from msgsign import sign_with_own_address
|
||||
await sign_with_own_address(sp, addr_fmt)
|
||||
else:
|
||||
break
|
||||
|
||||
except UnknownAddressExplained as exc:
|
||||
await ux_show_story(addr + '\n\n' + str(exc), title="Unknown Address")
|
||||
await ux_show_story(show_single_address(addr) + '\n\n' + str(exc), title="Unknown Address")
|
||||
except Exception as e:
|
||||
await ux_show_story('Ownership search failed.\n\n%s\n%s' % (e, problem_file_line(e)))
|
||||
|
||||
|
||||
@classmethod
|
||||
def note_subpath_used(cls, subpath):
|
||||
@ -363,8 +432,6 @@ class OwnershipCache:
|
||||
# - if they explore it (non-zero subaccount)
|
||||
# - if they sign those paths
|
||||
# - but ignore testnet vs. not
|
||||
from glob import settings
|
||||
|
||||
if subaccount == 0:
|
||||
# only interested in non-zero subaccounts
|
||||
return
|
||||
|
||||
@ -83,7 +83,7 @@ class PaperWalletMaker:
|
||||
|
||||
try:
|
||||
import ngu
|
||||
from auth import write_sig_file
|
||||
from msgsign import write_sig_file
|
||||
from chains import current_chain
|
||||
from serializations import hash160
|
||||
from stash import blank_object
|
||||
@ -179,7 +179,7 @@ class PaperWalletMaker:
|
||||
return
|
||||
except Exception as e:
|
||||
from utils import problem_file_line
|
||||
await ux_show_story('Failed to write!\n\n\n'+problem_file_line(e))
|
||||
await ux_show_story('Failed to write!\n\n'+problem_file_line(e))
|
||||
return
|
||||
|
||||
story = "Done! Created file(s):\n\n%s" % nice_txt
|
||||
|
||||
@ -3,8 +3,7 @@
|
||||
# pincodes.py - manage PIN code (which map to wallet seeds)
|
||||
#
|
||||
import ustruct, ckcc, version, chains, stash
|
||||
# from ubinascii import hexlify as b2a_hex
|
||||
from callgate import enter_dfu
|
||||
from callgate import enter_dfu, get_is_bricked
|
||||
from bip39 import wordlist_en
|
||||
|
||||
# See ../stm32/bootloader/pins.h for source of these constants.
|
||||
@ -127,17 +126,14 @@ class PinAttempt:
|
||||
self.private_state = 0 # opaque data, but preserve
|
||||
self.cached_main_pin = bytearray(32)
|
||||
|
||||
# If set, a spending policy is in effect, and so even tho we know the master
|
||||
# seed, we are not going to let them see it, nor sign things we dont like, etc.
|
||||
self.hobbled_mode = False
|
||||
|
||||
assert MAX_PIN_LEN == 32 # update FMT otherwise
|
||||
assert ustruct.calcsize(PIN_ATTEMPT_FMT_V1) == PIN_ATTEMPT_SIZE_V1
|
||||
assert ustruct.calcsize(PIN_ATTEMPT_FMT_V2_ADDITIONS) == PIN_ATTEMPT_SIZE - PIN_ATTEMPT_SIZE_V1
|
||||
|
||||
# check for bricked system early
|
||||
import callgate
|
||||
if callgate.get_is_bricked():
|
||||
# die right away if it's not going to work
|
||||
print("SE bricked")
|
||||
callgate.enter_dfu(3)
|
||||
#assert MAX_PIN_LEN == 32 # update FMT otherwise
|
||||
#assert ustruct.calcsize(PIN_ATTEMPT_FMT_V1) == PIN_ATTEMPT_SIZE_V1
|
||||
#assert ustruct.calcsize(PIN_ATTEMPT_FMT_V2_ADDITIONS) \
|
||||
# == PIN_ATTEMPT_SIZE - PIN_ATTEMPT_SIZE_V1
|
||||
|
||||
def __repr__(self):
|
||||
return '<PinAttempt: fails/left=%d/%d tc_flag/arg=0x%x/0x%x>' % (
|
||||
@ -177,7 +173,7 @@ class PinAttempt:
|
||||
old_pin = self.pin
|
||||
|
||||
assert len(new_pin) <= MAX_PIN_LEN
|
||||
assert old_pin != None
|
||||
assert old_pin is not None
|
||||
assert len(old_pin) <= MAX_PIN_LEN
|
||||
else:
|
||||
new_pin = b''
|
||||
@ -339,10 +335,6 @@ class PinAttempt:
|
||||
|
||||
return self.state_flags
|
||||
|
||||
def delay(self):
|
||||
# obsolete since Mk3, but called from login.py
|
||||
self.roundtrip(1)
|
||||
|
||||
def login(self):
|
||||
# test we have the PIN code right, and unlock access if so.
|
||||
chk = self.roundtrip(2)
|
||||
@ -473,6 +465,7 @@ class PinAttempt:
|
||||
def tmp_secret(self, encoded, chain=None, bip39pw=''):
|
||||
# Use indicated secret and stop using the SE; operate like this until reboot
|
||||
from glob import settings
|
||||
from utils import xfp2str
|
||||
from nvstore import SettingsObject
|
||||
|
||||
val = bytes(encoded + bytes(AE_SECRET_LEN - len(encoded)))
|
||||
@ -483,7 +476,9 @@ class PinAttempt:
|
||||
target_nvram_key = None
|
||||
if encoded is not None:
|
||||
# disallow using master seed as temporary
|
||||
master_err = "Cannot use master seed as temporary."
|
||||
xfp = xfp2str(settings.master_get("xfp", 0))
|
||||
master_err = ("Cannot use master seed as temporary. BUT you have just successfully "
|
||||
"tested recovery of your master seed [%s].") % xfp
|
||||
target_nvram_key = settings.hash_key(val)
|
||||
if SettingsObject.master_nvram_key:
|
||||
assert self.tmp_value
|
||||
@ -530,10 +525,24 @@ class PinAttempt:
|
||||
from trick_pins import TC_DELTA_MODE
|
||||
return bool(self.delay_required & TC_DELTA_MODE)
|
||||
|
||||
|
||||
def get_tc_values(self):
|
||||
# Mk4 only
|
||||
# return (tc_flags, tc_arg)
|
||||
return self.delay_required, self.delay_achieved
|
||||
|
||||
@staticmethod
|
||||
async def enforce_brick():
|
||||
# check for bricked system early
|
||||
if get_is_bricked():
|
||||
try:
|
||||
# regardless of settings, become a forever calculator after brickage.
|
||||
while version.has_qwerty:
|
||||
from calc import login_repl
|
||||
await login_repl()
|
||||
finally:
|
||||
# die right away if it's not going to work
|
||||
enter_dfu(3)
|
||||
|
||||
|
||||
# singleton
|
||||
|
||||
916
shared/psbt.py
916
shared/psbt.py
File diff suppressed because it is too large
Load Diff
@ -25,10 +25,6 @@ class PSRAMWrapper:
|
||||
|
||||
return memoryview(self._wr)[offset:offset+ln]
|
||||
|
||||
def is_at(self, ptr, offset):
|
||||
# is bytes() object really one we created at read_at
|
||||
return uctypes.addressof(ptr) == self.base+offset
|
||||
|
||||
# Be compatible with SPIFlash class...
|
||||
|
||||
def read(self, address, buf, cmd=None):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -57,9 +57,8 @@ SLOW_BAUD = const(9600)
|
||||
FAST_BAUD = const(57600)
|
||||
RX_BUF_SIZE = const(4350) # big enough for full v40 decoded
|
||||
|
||||
# TODO: constructor should leave it in reset for simple lower-power usage; then after
|
||||
# login we can do full setup (2+ seconds) and then sleep again until needed.
|
||||
|
||||
# TODO: constructor should avoid full setup until after login; after setup,
|
||||
# command sleep is the known low-power state.
|
||||
class QRScanner:
|
||||
|
||||
def __init__(self):
|
||||
@ -68,6 +67,8 @@ class QRScanner:
|
||||
self.scan_light = False # is light on during scanning?
|
||||
self.version = None
|
||||
self.setup_done = False
|
||||
self.needs_reinit = False
|
||||
self.sleep_seq = 0
|
||||
|
||||
# hodl this lock when communicating w/ QR scanner
|
||||
self.lock = asyncio.Lock()
|
||||
@ -84,16 +85,21 @@ class QRScanner:
|
||||
# setup hardware, reset scanner and return time to delay until ready
|
||||
from machine import UART, Pin
|
||||
self.serial = UART(2, SLOW_BAUD, rxbuf=RX_BUF_SIZE)
|
||||
self.reset = Pin('QR_RESET', Pin.OUT_OD, value=0)
|
||||
self.reset = Pin('QR_RESET', Pin.OUT_OD, value=1)
|
||||
self.trigger = Pin('QR_TRIG', Pin.OUT_OD, value=1) # wasn't needed
|
||||
|
||||
# NOTE: reset is active low (open drain)
|
||||
self.pulse_reset()
|
||||
|
||||
# needs full 2 seconds of recovery time after reset
|
||||
return 2
|
||||
|
||||
def pulse_reset(self):
|
||||
# RESET is active low (open drain). Keep it as a pulse; module docs
|
||||
# describe low on this pin as wake-up, so don't use it as parking state.
|
||||
self.reset(0)
|
||||
utime.sleep_ms(10)
|
||||
self.reset(1)
|
||||
|
||||
# needs full 2 seconds of recovery time
|
||||
return 2
|
||||
self.needs_reinit = False
|
||||
|
||||
def set_baud(self, br):
|
||||
# change serial port baud rate
|
||||
@ -118,56 +124,104 @@ class QRScanner:
|
||||
|
||||
async def setup_task(self, start_delay):
|
||||
# Task to setup device, and then die.
|
||||
await asyncio.sleep(start_delay)
|
||||
async with self.lock:
|
||||
for attempt in range(3):
|
||||
await asyncio.sleep(start_delay)
|
||||
|
||||
async with self.lock:
|
||||
try:
|
||||
await self._configure()
|
||||
except Exception:
|
||||
# a step failed or timed out (would have left scanner dead
|
||||
# until next boot); reset module and start over
|
||||
await self.blind_shutdown()
|
||||
if attempt == 2:
|
||||
break
|
||||
start_delay = self.reset_stream()
|
||||
continue
|
||||
|
||||
# might need to repeat a few time to get into right state
|
||||
self.setup_done = True
|
||||
await self.goto_sleep()
|
||||
return
|
||||
self.mark_needs_reinit()
|
||||
|
||||
def reset_stream(self):
|
||||
self.sleep_seq += 1
|
||||
start_delay = self.hardware_setup()
|
||||
self.stream = asyncio.StreamReader(self.serial, {})
|
||||
return start_delay
|
||||
|
||||
def mark_needs_reinit(self):
|
||||
self.setup_done = False
|
||||
self.version = None
|
||||
self.needs_reinit = True
|
||||
if hasattr(self, 'reset'):
|
||||
self.reset(1)
|
||||
|
||||
async def blind_shutdown(self):
|
||||
for baud in (SLOW_BAUD, FAST_BAUD):
|
||||
self.set_baud(baud)
|
||||
await self.tx('S_CMD_020D') # return to "Command mode"
|
||||
await asyncio.sleep_ms(20)
|
||||
await self.tx('S_CMD_03L0') # turn off bright light
|
||||
await asyncio.sleep_ms(20)
|
||||
await self.tx('SRDF0050') # sleep scanner
|
||||
await asyncio.sleep_ms(150)
|
||||
await self.tx('SRDF0050')
|
||||
await asyncio.sleep_ms(20)
|
||||
|
||||
async def _configure(self):
|
||||
# full config sequence; any step may raise on timeout/framing error
|
||||
|
||||
# might need to repeat a few time to get into right state
|
||||
for retry in range(5):
|
||||
baud = await self.probe_baud()
|
||||
if baud: break
|
||||
else:
|
||||
#print("QR Scanner: missing")
|
||||
raise RuntimeError('no contact')
|
||||
|
||||
try:
|
||||
await self.txrx('S_CMD_FFFF') # factory reset of settings
|
||||
except RuntimeError:
|
||||
await asyncio.sleep_ms(1000)
|
||||
for retry in range(5):
|
||||
baud = await self.probe_baud()
|
||||
if baud: break
|
||||
else:
|
||||
#print("QR Scanner: missing")
|
||||
return
|
||||
raise RuntimeError('no contact after S_CMD_FFFF')
|
||||
|
||||
await self.txrx('S_CMD_FFFF') # factory reset of settings
|
||||
# go to high speed!
|
||||
if baud != FAST_BAUD:
|
||||
await self.txrx('S_CMD_H3BR%d' % FAST_BAUD)
|
||||
self.set_baud(FAST_BAUD)
|
||||
|
||||
# go to high speed!
|
||||
if baud != FAST_BAUD:
|
||||
await self.txrx('S_CMD_H3BR%d' % FAST_BAUD)
|
||||
self.set_baud(FAST_BAUD)
|
||||
# configure it like we want it
|
||||
await self.txrx('S_CMD_MTRS5000') # 5s to read before fail (unused)
|
||||
await self.txrx('S_CMD_MT11') # trigger is edge-based (not level)
|
||||
await self.txrx('S_CMD_MT30') # Same code reading without delay
|
||||
await self.txrx('S_CMD_MT20') # Enable automatic sleep when idle
|
||||
await self.txrx('S_CMD_MTRF500') # Idle time: 500ms
|
||||
await self.txrx('S_CMD_059A') # add CR LF after QR data (important)
|
||||
await self.txrx('S_CMD_03L0') # light off all the time by default
|
||||
await self.txrx('S_CMD_0407') # turn on signal for our yellow led
|
||||
|
||||
# configure it like we want it
|
||||
await self.txrx('S_CMD_MTRS5000') # 5s to read before fail (unused)
|
||||
await self.txrx('S_CMD_MT11') # trigger is edge-based (not level)
|
||||
await self.txrx('S_CMD_MT30') # Same code reading without delay
|
||||
await self.txrx('S_CMD_MT20') # Enable automatic sleep when idle
|
||||
await self.txrx('S_CMD_MTRF500') # Idle time: 500ms
|
||||
await self.txrx('S_CMD_059A') # add CR LF after QR data (important)
|
||||
await self.txrx('S_CMD_03L0') # light off all the time by default
|
||||
await self.txrx('S_CMD_0407') # turn on signal for our yellow led
|
||||
# settings under continuous scan mode
|
||||
await self.txrx('S_CMD_MARS0000') # "Modify the duration of single code reading" (ms)
|
||||
await self.txrx('S_CMD_MARR000') # "Modify the time of the reading interval 0ms"
|
||||
await self.txrx('S_CMD_MA31') # Enable "Same code reading delay"
|
||||
await self.txrx('S_CMD_MARI0050') # "Modify the same code reading delay 50ms"
|
||||
|
||||
# settings under continuous scan mode
|
||||
await self.txrx('S_CMD_MARS0000') # "Modify the duration of single code reading" (ms)
|
||||
await self.txrx('S_CMD_MARR000') # "Modify the time of the reading interval 0ms"
|
||||
await self.txrx('S_CMD_MA31') # Enable "Same code reading delay"
|
||||
await self.txrx('S_CMD_MARI0050') # "Modify the same code reading delay 50ms"
|
||||
# these aren't useful (yet?) and just make things harder to decode.
|
||||
#await self.txrx('S_CMD_05F1') # add all information on
|
||||
#await self.txrx('S_CMD_05L1') # output decoding length info on
|
||||
#await self.txrx('S_CMD_05S1') # STX start char
|
||||
#await self.txrx('S_CMD_05C1') # CodeID+prefix
|
||||
#await self.txrx('S_CMD_0501') # prefix on
|
||||
#await self.txrx('S_CMD_0506') # suffix
|
||||
#await self.txrx('S_CMD_05D0') # tx total data
|
||||
|
||||
# these aren't useful (yet?) and just make things harder to decode.
|
||||
#await self.txrx('S_CMD_05F1') # add all information on
|
||||
#await self.txrx('S_CMD_05L1') # output decoding length info on
|
||||
#await self.txrx('S_CMD_05S1') # STX start char
|
||||
#await self.txrx('S_CMD_05C1') # CodeID+prefix
|
||||
#await self.txrx('S_CMD_0501') # prefix on
|
||||
#await self.txrx('S_CMD_0506') # suffix
|
||||
#await self.txrx('S_CMD_05D0') # tx total data
|
||||
|
||||
# prevent scanning magic QR to affect settings
|
||||
await self.txrx('S_CMD_0000') # close setting codes
|
||||
|
||||
self.setup_done = True
|
||||
|
||||
await self.goto_sleep()
|
||||
# prevent scanning magic QR to affect settings
|
||||
await self.txrx('S_CMD_0000') # close setting codes
|
||||
|
||||
async def scan_once(self):
|
||||
# Blocks until something is scanned. Returns it as string
|
||||
@ -176,6 +230,16 @@ class QRScanner:
|
||||
# - returns a BBQr object at that point
|
||||
self.scan_light = False
|
||||
|
||||
if self.needs_reinit:
|
||||
try:
|
||||
await self.setup_task(self.reset_stream())
|
||||
if self.setup_done:
|
||||
await asyncio.sleep_ms(200)
|
||||
except asyncio.CancelledError:
|
||||
await self.blind_shutdown()
|
||||
self.mark_needs_reinit()
|
||||
return None
|
||||
|
||||
# wait for reset process to complete (can be an issue right after boot)
|
||||
# - few seconds of boot time needed
|
||||
for retry in range(10):
|
||||
@ -201,7 +265,7 @@ class QRScanner:
|
||||
if not rv: continue
|
||||
|
||||
if rv[0:2] == 'B$' and bbqr.collect(rv):
|
||||
# BBQr protocol detected; collect more data
|
||||
# BBQr protocol detected, accepted need to collect more data
|
||||
continue
|
||||
|
||||
break
|
||||
@ -211,19 +275,22 @@ class QRScanner:
|
||||
finally:
|
||||
# Problem: another valid scan can come in just as we are trying
|
||||
# to get out of scanner mode
|
||||
for retry in range(10):
|
||||
for retry in range(3):
|
||||
try:
|
||||
await self.txrx('S_CMD_020D') # return to "Command mode"
|
||||
await self.txrx('S_CMD_03L0') # turn off bright light
|
||||
await self.txrx('S_CMD_020D', timeout=1000) # return to "Command mode"
|
||||
await self.txrx('S_CMD_03L0', timeout=1000) # turn off bright light
|
||||
#print('rest after %d retries' % retry)
|
||||
break
|
||||
except: pass
|
||||
await asyncio.sleep_ms(25)
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.sleep_ms(50)
|
||||
else:
|
||||
pass
|
||||
#print('reset failed')
|
||||
await self.blind_shutdown()
|
||||
self.mark_needs_reinit()
|
||||
|
||||
await self.goto_sleep()
|
||||
if self.setup_done:
|
||||
await self.goto_sleep()
|
||||
self.busy_scanning = False
|
||||
|
||||
# return BBQr object or string if simple QR
|
||||
@ -254,13 +321,14 @@ class QRScanner:
|
||||
# send specific command until it responds
|
||||
# - it will wake on any command, but not instant
|
||||
# - first one seems to fail 100%
|
||||
self.sleep_seq += 1
|
||||
await self.tx('SRDF0051') # blindly at first
|
||||
|
||||
for retry in range(5):
|
||||
try:
|
||||
await self.txrx('SRDF0051', timeout=50) # 50 ok, 20 too short
|
||||
return
|
||||
except:
|
||||
except Exception:
|
||||
# first try usually fails, that's okay... its asleep and groggy
|
||||
pass
|
||||
|
||||
@ -270,9 +338,13 @@ class QRScanner:
|
||||
# - need blind retries here
|
||||
# - might be two layers of sleep, and we need this second command after the first
|
||||
# - helps to turn off the yellow LED, and save power as well
|
||||
self.sleep_seq += 1
|
||||
sleep_seq = self.sleep_seq
|
||||
await self.tx('SRDF0050')
|
||||
async def later():
|
||||
await asyncio.sleep_ms(150)
|
||||
if sleep_seq != self.sleep_seq or self.busy_scanning:
|
||||
return
|
||||
await self.tx('SRDF0050')
|
||||
asyncio.create_task(later())
|
||||
|
||||
@ -290,6 +362,22 @@ class QRScanner:
|
||||
#print('tx >> ' + msg)
|
||||
self.serial.write(msg)
|
||||
|
||||
async def readexactly_timeout(self, num, timeout, msg=None):
|
||||
# Avoid asyncio.wait_for_ms here: it can leave the scanner setup task
|
||||
# stuck after a CancelledError. Convert scanner silence into a normal
|
||||
# retryable command failure instead.
|
||||
if timeout is None:
|
||||
return await self.stream.readexactly(num)
|
||||
|
||||
start = utime.ticks_ms()
|
||||
while self.stream.s.any() < num:
|
||||
if utime.ticks_diff(utime.ticks_ms(), start) >= timeout:
|
||||
#print("no rx after %s" % msg)
|
||||
raise RuntimeError
|
||||
await asyncio.sleep_ms(5)
|
||||
|
||||
return await self.stream.readexactly(num)
|
||||
|
||||
async def txrx(self, msg, timeout=250):
|
||||
# Send a command, get the corresponding response.
|
||||
# - has a long timeout, collects rx based on framing
|
||||
@ -310,13 +398,8 @@ class QRScanner:
|
||||
expect = LEN_OKAY
|
||||
rx = b''
|
||||
while 1:
|
||||
try:
|
||||
rx += await asyncio.wait_for_ms(self.stream.readexactly(expect), timeout)
|
||||
except asyncio.TimeoutError:
|
||||
if timeout is None:
|
||||
continue
|
||||
#print("no rx after %s" % msg)
|
||||
raise RuntimeError
|
||||
rx += await self.readexactly_timeout(expect, timeout, msg)
|
||||
|
||||
|
||||
#print('txrx << ' + B2A(rx))
|
||||
|
||||
|
||||
418
shared/seed.py
418
shared/seed.py
@ -10,29 +10,48 @@
|
||||
# - 'abandon' * 17 + 'agent'
|
||||
# - 'abandon' * 11 + 'about'
|
||||
#
|
||||
import ngu, uctypes, bip39, random, stash, version
|
||||
import ngu, uctypes, bip39, random, version
|
||||
from ucollections import OrderedDict
|
||||
from menu import MenuItem, MenuSystem
|
||||
from utils import xfp2str, parse_extended_key, swab32, pad_raw_secret, problem_file_line
|
||||
from utils import xfp2str, parse_extended_key, swab32
|
||||
from utils import deserialize_secret, problem_file_line, wipe_if_deltamode
|
||||
from uhashlib import sha256
|
||||
from ux import ux_show_story, the_ux, ux_dramatic_pause, ux_confirm, OK, X
|
||||
from ux import PressRelease, ux_input_numbers, ux_input_text, show_qr_code
|
||||
from ux import PressRelease, ux_input_text, show_qr_code
|
||||
from actions import goto_top_menu
|
||||
from stash import SecretStash, ZeroSecretException
|
||||
from stash import SecretStash, SensitiveValues
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from pwsave import PassphraseSaver, PassphraseSaverMenu
|
||||
from glob import settings, dis
|
||||
from pincodes import pa
|
||||
from nvstore import SettingsObject
|
||||
from files import CardMissingError, needs_microsd, CardSlot
|
||||
from charcodes import KEY_QR, KEY_ENTER, KEY_CANCEL, KEY_CLEAR
|
||||
|
||||
from files import CardMissingError, needs_microsd
|
||||
from charcodes import KEY_QR, KEY_ENTER, KEY_CANCEL, KEY_NFC
|
||||
from uasyncio import sleep_ms
|
||||
from ucollections import namedtuple
|
||||
|
||||
# seed words lengths we support: 24=>256 bits, and recommended
|
||||
VALID_LENGTHS = (24, 18, 12)
|
||||
|
||||
# maximum length for BIP-39 passphrase
|
||||
MAX_PASS_LEN = 100
|
||||
|
||||
# bit flag that means "also include bare prefix as a valid word"
|
||||
_PREFIX_MARKER = const(1<<26)
|
||||
|
||||
# what we store (in JSON as a tuple) for each seed vault key.
|
||||
# - 'encoded' is hex, and has is trimmed of right side zeros
|
||||
VaultEntry = namedtuple('VaultEntry', 'xfp encoded label origin')
|
||||
|
||||
def not_hobbled_mode():
|
||||
# used as menu predicate and similar
|
||||
return not pa.hobbled_mode
|
||||
|
||||
def seed_vault_iter():
|
||||
# iterate over all seeds in the vault; returns VaultEntry instances.
|
||||
# raw vault entries are list type when json.loaded from flash
|
||||
for lst in settings.master_get("seeds", []):
|
||||
yield VaultEntry(*lst)
|
||||
|
||||
def letter_choices(sofar='', depth=0, thres=5):
|
||||
# make a list of word completions based on indicated prefix
|
||||
@ -138,23 +157,62 @@ class WordNestMenu(MenuSystem):
|
||||
done_cb = None
|
||||
|
||||
def __init__(self, num_words=None, has_checksum=True, done_cb=commit_new_words,
|
||||
items=None, is_commit=False):
|
||||
items=None, is_commit=False, menu_cbf=None, prefix="", words=None):
|
||||
|
||||
if num_words is not None:
|
||||
WordNestMenu.target_words = num_words
|
||||
WordNestMenu.has_checksum = has_checksum
|
||||
WordNestMenu.words = []
|
||||
assert done_cb
|
||||
WordNestMenu.done_cb = done_cb
|
||||
is_commit = True
|
||||
|
||||
if words:
|
||||
WordNestMenu.words = words
|
||||
|
||||
if not items:
|
||||
items = [MenuItem(i, menu=self.next_menu) for i in letter_choices()]
|
||||
ch = letter_choices(prefix)
|
||||
if menu_cbf:
|
||||
items = [MenuItem(i, f=menu_cbf) for i in ch]
|
||||
else:
|
||||
items = [MenuItem(i, menu=self.next_menu) for i in ch]
|
||||
|
||||
self.is_commit = is_commit
|
||||
|
||||
super(WordNestMenu, self).__init__(items)
|
||||
|
||||
@classmethod
|
||||
async def get_n_words(cls, num_words):
|
||||
rv = []
|
||||
for _ in range(num_words):
|
||||
rv = await cls.get_word(rv, num_words)
|
||||
|
||||
return rv
|
||||
|
||||
@classmethod
|
||||
async def get_word(cls, words=None, target_words=None):
|
||||
# Just block until N words are provided. May only work before menus start?
|
||||
from glob import numpad
|
||||
|
||||
async def menu_done_cbf(menu, b, c):
|
||||
# duplicates some of the logic of next_menu
|
||||
if c.label[-1] == '-':
|
||||
lc = c.label[0:-1]
|
||||
else:
|
||||
cls.words.append(c.label)
|
||||
numpad.abort_ux()
|
||||
return
|
||||
|
||||
m = cls(prefix=lc, menu_cbf=menu_done_cbf)
|
||||
the_ux.push(m)
|
||||
await the_ux.interact()
|
||||
|
||||
m = cls(num_words=target_words, menu_cbf=menu_done_cbf, has_checksum=False, words=words)
|
||||
|
||||
the_ux.push(m)
|
||||
await the_ux.interact()
|
||||
|
||||
return cls.words
|
||||
|
||||
@staticmethod
|
||||
async def next_menu(self, idx, choice):
|
||||
|
||||
@ -215,7 +273,7 @@ class WordNestMenu(MenuSystem):
|
||||
while isinstance(the_ux.top_of_stack(), cls):
|
||||
the_ux.pop()
|
||||
|
||||
def on_cancel(self):
|
||||
async def on_cancel(self):
|
||||
# user pressed cancel on a menu (so he's going upwards)
|
||||
# - if it's a step where we added to the word list, undo that.
|
||||
# - but keep them in our system until:
|
||||
@ -273,9 +331,16 @@ individual words if you wish.''')
|
||||
|
||||
|
||||
async def show_words(words, prompt=None, escape=None, extra='', ephemeral=False):
|
||||
msg = (prompt or 'Record these %d secret words!\n') % len(words)
|
||||
|
||||
from ux import ux_render_words
|
||||
from glob import NFC
|
||||
|
||||
if prompt:
|
||||
title = None
|
||||
msg = prompt
|
||||
else:
|
||||
m = 'Record these %d secret words!' % len(words)
|
||||
title, msg = (m, "") if version.has_qwerty else (None, m+"\n")
|
||||
|
||||
msg += ux_render_words(words)
|
||||
|
||||
msg += '\n\nPlease check and double check your notes.'
|
||||
@ -283,22 +348,30 @@ async def show_words(words, prompt=None, escape=None, extra='', ephemeral=False)
|
||||
# user can skip quiz for ephemeral secrets
|
||||
msg += " There will be a test!"
|
||||
|
||||
escape = (escape or '') + '1'
|
||||
if not version.has_qwerty:
|
||||
escape = (escape or '') + '1'
|
||||
extra += 'Press (1) to view as QR Code. '
|
||||
else:
|
||||
escape = (escape or '') + KEY_QR
|
||||
extra += 'Press '+ KEY_QR + ' to view as QR Code. '
|
||||
title = None
|
||||
extra += 'Press (1) to view as QR Code'
|
||||
if NFC:
|
||||
extra += ", (3) to share via NFC"
|
||||
escape += "3"
|
||||
extra += "."
|
||||
|
||||
if extra:
|
||||
msg += '\n\n'
|
||||
msg += extra
|
||||
|
||||
while 1:
|
||||
ch = await ux_show_story(msg, escape=escape, sensitive=True)
|
||||
if ch == '1' or ch == KEY_QR:
|
||||
await show_qr_code(' '.join(w[0:4] for w in words), True)
|
||||
rv = ' '.join(w[0:4] for w in words)
|
||||
ch = await ux_show_story(msg, title=title, escape=escape, sensitive=True,
|
||||
hint_icons=KEY_QR+(KEY_NFC if NFC else ''))
|
||||
if ch in ('1'+KEY_QR):
|
||||
await show_qr_code(rv, True, is_secret=True)
|
||||
continue
|
||||
if NFC and (ch in "3"+KEY_NFC):
|
||||
await NFC.share_text(rv, is_secret=True)
|
||||
continue
|
||||
|
||||
break
|
||||
|
||||
return ch
|
||||
@ -411,27 +484,35 @@ async def new_from_dice(nwords):
|
||||
await commit_new_words(words)
|
||||
|
||||
def in_seed_vault(encoded):
|
||||
# Test if indicated xfp (or currently active XFP) is in the seed vault already.
|
||||
seeds = settings.master_get("seeds", [])
|
||||
if seeds:
|
||||
ss = stash.SecretStash.storage_serialize(encoded)
|
||||
if ss in [s[1] for s in seeds]:
|
||||
# Test if indicated secret is in the seed vault already.
|
||||
hss = None
|
||||
for rec in seed_vault_iter():
|
||||
if not hss:
|
||||
hss = SecretStash.storage_serialize(encoded)
|
||||
if hss == rec.encoded:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def add_seed_to_vault(encoded, meta=None):
|
||||
async def add_seed_to_vault(encoded, origin=None, label=None):
|
||||
|
||||
if not settings.master_get("seedvault", False):
|
||||
# seed vault disabled
|
||||
# this can be re-enabled by attacker in deltamode
|
||||
return
|
||||
if pa.is_secret_blank():
|
||||
if pa.is_secret_blank() or pa.is_deltamode():
|
||||
# do not save anything if no SE secret yet
|
||||
# do not offer any access to SV in deltamode
|
||||
return
|
||||
|
||||
# do not offer to store secrets that are already in vault
|
||||
if in_seed_vault(encoded):
|
||||
return
|
||||
|
||||
# stay "read only" in hobbled mode
|
||||
if pa.hobbled_mode:
|
||||
return
|
||||
|
||||
main_xfp = settings.master_get("xfp", 0)
|
||||
|
||||
# parse encoded
|
||||
@ -457,10 +538,9 @@ async def add_seed_to_vault(encoded, meta=None):
|
||||
return
|
||||
|
||||
# Save it into master settings
|
||||
seeds.append((new_xfp_str,
|
||||
stash.SecretStash.storage_serialize(encoded),
|
||||
xfp_ui,
|
||||
meta))
|
||||
rec = VaultEntry(xfp=new_xfp_str, encoded=SecretStash.storage_serialize(encoded),
|
||||
label=(label or xfp_ui), origin=origin)
|
||||
seeds.append(list(rec))
|
||||
|
||||
settings.master_set("seeds", seeds)
|
||||
|
||||
@ -469,13 +549,18 @@ async def add_seed_to_vault(encoded, meta=None):
|
||||
return True
|
||||
|
||||
async def set_ephemeral_seed(encoded, chain=None, summarize_ux=True, bip39pw='',
|
||||
is_restore=False, meta=None):
|
||||
if not is_restore:
|
||||
await add_seed_to_vault(encoded, meta=meta)
|
||||
is_restore=False, origin=None, label=None):
|
||||
# Capture tmp seed into vault, if so enabled, and regardless apply it as new tmp.
|
||||
if not is_restore and not_hobbled_mode():
|
||||
await add_seed_to_vault(encoded, origin=origin, label=label)
|
||||
dis.fullscreen("Wait...")
|
||||
|
||||
applied, err_msg = pa.tmp_secret(encoded, chain=chain, bip39pw=bip39pw)
|
||||
|
||||
# FYI: Might need to bounce the USB connection, because our pubkey has changed,
|
||||
# altho if they have already picked a shared session key, no need, and
|
||||
# would only affect MitM test, which has already been done.
|
||||
|
||||
dis.progress_bar_show(1)
|
||||
|
||||
if not applied:
|
||||
@ -484,15 +569,18 @@ async def set_ephemeral_seed(encoded, chain=None, summarize_ux=True, bip39pw='',
|
||||
|
||||
xfp = "[" + xfp2str(settings.get("xfp", 0)) + "]"
|
||||
if summarize_ux:
|
||||
await ux_show_story(title=xfp, msg="New temporary master key is in effect now.")
|
||||
msg = "New temporary master key is in effect now."
|
||||
if bip39pw:
|
||||
msg += "\n\nPassphrase: %s" % bip39pw
|
||||
await ux_show_story(title=xfp, msg=msg)
|
||||
|
||||
return applied
|
||||
|
||||
async def set_ephemeral_seed_words(words, meta):
|
||||
async def set_ephemeral_seed_words(words, origin):
|
||||
dis.progress_bar_show(0.1)
|
||||
encoded = seed_words_to_encoded_secret(words)
|
||||
dis.progress_bar_show(0.5)
|
||||
await set_ephemeral_seed(encoded, meta=meta)
|
||||
await set_ephemeral_seed(encoded, origin=origin)
|
||||
goto_top_menu()
|
||||
|
||||
async def ephemeral_seed_generate_from_dice(nwords):
|
||||
@ -509,7 +597,7 @@ async def ephemeral_seed_generate_from_dice(nwords):
|
||||
words = await approve_word_list(seed, nwords, ephemeral=True)
|
||||
if words:
|
||||
dis.fullscreen("Applying...")
|
||||
await set_ephemeral_seed_words(words, meta='Dice')
|
||||
await set_ephemeral_seed_words(words, origin='Dice')
|
||||
|
||||
def generate_seed():
|
||||
# Generate 32 bytes of best-quality high entropy TRNG bytes.
|
||||
@ -532,7 +620,7 @@ async def make_new_wallet(nwords):
|
||||
async def ephemeral_seed_import(nwords):
|
||||
async def import_done_cb(words):
|
||||
dis.fullscreen("Applying...")
|
||||
await set_ephemeral_seed_words(words, meta='Imported')
|
||||
await set_ephemeral_seed_words(words, origin='Imported')
|
||||
|
||||
if version.has_qwerty:
|
||||
from ux_q1 import seed_word_entry
|
||||
@ -546,17 +634,17 @@ async def ephemeral_seed_generate(nwords):
|
||||
words = await approve_word_list(seed, nwords, ephemeral=True)
|
||||
if words:
|
||||
dis.fullscreen("Applying...")
|
||||
await set_ephemeral_seed_words(words, meta="TRNG Words")
|
||||
await set_ephemeral_seed_words(words, origin="TRNG Words")
|
||||
|
||||
async def set_seed_extended_key(extended_key):
|
||||
encoded, chain = xprv_to_encoded_secret(extended_key)
|
||||
set_seed_value(encoded=encoded, chain=chain)
|
||||
goto_top_menu(first_time=True)
|
||||
|
||||
async def set_ephemeral_seed_extended_key(extended_key, meta=None):
|
||||
async def set_ephemeral_seed_extended_key(extended_key, origin=None):
|
||||
encoded, chain = xprv_to_encoded_secret(extended_key)
|
||||
dis.fullscreen("Applying...")
|
||||
await set_ephemeral_seed(encoded=encoded, chain=chain, meta=meta)
|
||||
await set_ephemeral_seed(encoded=encoded, chain=chain, origin=origin)
|
||||
goto_top_menu()
|
||||
|
||||
async def approve_word_list(seed, nwords, ephemeral=False):
|
||||
@ -634,8 +722,8 @@ def xprv_to_encoded_secret(xprv):
|
||||
|
||||
|
||||
def set_seed_value(words=None, encoded=None, chain=None):
|
||||
# Save the seed words (or other encoded private key) into secure element,
|
||||
# and reboot. BIP-39 passphrase is not set at this point (empty string).
|
||||
# Save the seed words (or other encoded private key) into secure element.
|
||||
# BIP-39 passphrase is not set at this point (empty string).
|
||||
if words:
|
||||
nv = seed_words_to_encoded_secret(words)
|
||||
else:
|
||||
@ -659,14 +747,14 @@ def set_seed_value(words=None, encoded=None, chain=None):
|
||||
|
||||
|
||||
async def calc_bip39_passphrase(pw, bypass_tmp=False):
|
||||
# Returns (new) encoded secret, new xfp, old xfp
|
||||
from glob import dis, settings
|
||||
from pincodes import pa
|
||||
|
||||
dis.fullscreen("Working...")
|
||||
|
||||
current_xfp = settings.get("xfp", 0)
|
||||
|
||||
with stash.SensitiveValues(bip39pw=pw, bypass_tmp=bypass_tmp) as sv:
|
||||
with SensitiveValues(bip39pw=pw, bypass_tmp=bypass_tmp) as sv:
|
||||
# can't do it without original seed words (late, but caller has checked)
|
||||
assert sv.mode == 'words', sv.mode
|
||||
nv = SecretStash.encode(xprv=sv.node)
|
||||
@ -676,14 +764,13 @@ async def calc_bip39_passphrase(pw, bypass_tmp=False):
|
||||
|
||||
async def set_bip39_passphrase(pw, bypass_tmp=False, summarize_ux=True):
|
||||
nv, xfp, parent_xfp = await calc_bip39_passphrase(pw, bypass_tmp=bypass_tmp)
|
||||
ret = await set_ephemeral_seed(nv, summarize_ux=summarize_ux, bip39pw=pw,
|
||||
meta="BIP-39 Passphrase on [%s]" % xfp2str(parent_xfp))
|
||||
dis.draw_status(bip39=int(bool(pw)), xfp=xfp, tmp=1)
|
||||
return ret
|
||||
|
||||
# Might need to bounce the USB connection, because our pubkey has changed,
|
||||
# altho if they have already picked a shared session key, no need, and
|
||||
# would only affect MitM test, which has already been done.
|
||||
ret = await set_ephemeral_seed(nv, summarize_ux=summarize_ux, bip39pw=pw,
|
||||
origin="BIP-39 Passphrase on [%s]" % xfp2str(parent_xfp))
|
||||
|
||||
dis.draw_status(bip39=int(bool(pw)), xfp=xfp, tmp=1)
|
||||
|
||||
return ret
|
||||
|
||||
async def remember_ephemeral_seed():
|
||||
# Compute current xprv and switch to using that as root secret.
|
||||
@ -707,7 +794,7 @@ async def remember_ephemeral_seed():
|
||||
# address cache, settings from tmp seeds / seedvault seeds
|
||||
# rebuild fs as we want to save current tmp settings immediately
|
||||
from files import wipe_flash_filesystem
|
||||
wipe_flash_filesystem(True)
|
||||
wipe_flash_filesystem()
|
||||
|
||||
dis.draw_status(bip39=0, tmp=0)
|
||||
dis.fullscreen('Saving...')
|
||||
@ -738,12 +825,6 @@ def clear_seed():
|
||||
callgate.fast_wipe(True)
|
||||
# NOT REACHED
|
||||
|
||||
utime.sleep(1)
|
||||
|
||||
# security: need to reboot to really be sure to clear the secrets from main memory.
|
||||
from machine import reset
|
||||
reset()
|
||||
|
||||
async def word_quiz(words, limited=None, title='Word %d is?'):
|
||||
# Perform a test, to check they wrote them down
|
||||
# Return X if they cancel early.
|
||||
@ -818,7 +899,7 @@ class SeedVaultMenu(MenuSystem):
|
||||
from glob import dis
|
||||
dis.fullscreen("Applying...")
|
||||
|
||||
xfp, encoded = item.arg
|
||||
encoded = item.arg # 72 bytes binary
|
||||
|
||||
await set_ephemeral_seed(encoded, is_restore=True)
|
||||
|
||||
@ -828,42 +909,40 @@ class SeedVaultMenu(MenuSystem):
|
||||
async def _remove(menu, label, item):
|
||||
from glob import dis, settings
|
||||
|
||||
idx, xfp_str, encoded = item.arg
|
||||
esc = ""
|
||||
tmp_val = False
|
||||
idx, rec, encoded = item.arg
|
||||
current_active = (pa.tmp_value == bytes(encoded))
|
||||
|
||||
msg = ("Remove seed from seed vault and delete its "
|
||||
"settings?\n\nPress %s to continue, press (1) to "
|
||||
"only remove from seed vault and keep "
|
||||
"encrypted settings for later use.\n\n"
|
||||
"WARNING: Funds will be lost if wallet is"
|
||||
" not backed-up elsewhere.") % OK
|
||||
msg = "Remove seed from seed vault"
|
||||
if pa.tmp_value and current_active:
|
||||
tmp_val = True
|
||||
msg += "?\n\n"
|
||||
else:
|
||||
msg += (" and delete its settings?\n\n"
|
||||
"Press %s to continue, press (1) to "
|
||||
"only remove from seed vault and keep "
|
||||
"encrypted settings for later use.\n\n") % OK
|
||||
esc += "1"
|
||||
|
||||
ch = await ux_show_story(title="[" + xfp_str + "]", msg=msg, escape="1")
|
||||
msg += "WARNING: Funds will be lost if wallet is not backed-up elsewhere."
|
||||
|
||||
ch = await ux_show_story(title="[" + rec.xfp + "]", msg=msg, escape=esc)
|
||||
if ch == "x": return
|
||||
|
||||
assert not_hobbled_mode()
|
||||
|
||||
dis.fullscreen("Saving...")
|
||||
|
||||
wipe_slot = (ch != "1")
|
||||
tmp_val = False
|
||||
|
||||
if pa.tmp_value:
|
||||
tmp_val = True
|
||||
wipe_slot = not current_active and (ch != "1")
|
||||
|
||||
if wipe_slot:
|
||||
# are we deleting current active ephemeral wallet
|
||||
# and its settings ?
|
||||
# slot wiping
|
||||
if tmp_val:
|
||||
# wipe current settings
|
||||
settings.blank()
|
||||
pa.tmp_value = False
|
||||
settings.return_to_master_seed()
|
||||
else:
|
||||
# in main settings
|
||||
xs = SettingsObject()
|
||||
xs.set_key(encoded)
|
||||
xs.load()
|
||||
xs.blank()
|
||||
del xs
|
||||
xs = SettingsObject()
|
||||
xs.set_key(encoded)
|
||||
xs.load()
|
||||
xs.blank()
|
||||
del xs
|
||||
|
||||
|
||||
# CAUTION: will get shadow copy if in tmp seed mode already
|
||||
seeds = settings.master_get("seeds", [])
|
||||
@ -885,13 +964,13 @@ class SeedVaultMenu(MenuSystem):
|
||||
|
||||
@staticmethod
|
||||
async def _detail(menu, label, item):
|
||||
xfp_str, encoded, name, meta = item.arg
|
||||
rec, encoded = item.arg
|
||||
|
||||
# - first byte represents type of secret (internal encoding flag)
|
||||
# - first byte represents type of secret (internal encoding flags)
|
||||
txt = SecretStash.summary(encoded[0])
|
||||
|
||||
detail = "Name:\n%s\n\nMaster XFP:\n%s\n\nOrigin:\n%s\n\nSecret Type:\n%s" \
|
||||
% (name, xfp_str, meta, txt)
|
||||
detail = "Name:\n%s\n\nMaster XFP: %s\nSecret Type: %s\n\nOrigin:\n%s\n\n" \
|
||||
% (rec.label, rec.xfp, txt, rec.origin)
|
||||
|
||||
await ux_show_story(detail)
|
||||
|
||||
@ -901,30 +980,30 @@ class SeedVaultMenu(MenuSystem):
|
||||
from glob import dis
|
||||
from ux import ux_input_text
|
||||
|
||||
idx, xfp_str = item.arg
|
||||
assert not_hobbled_mode()
|
||||
|
||||
seeds = settings.master_get("seeds", [])
|
||||
chk_xfp, encoded, old_name, meta = seeds[idx]
|
||||
assert chk_xfp == xfp_str
|
||||
idx, old = item.arg
|
||||
new_label = await ux_input_text(old.label, confirm_exit=False, max_len=40)
|
||||
|
||||
new_name = await ux_input_text(old_name, confirm_exit=False, max_len=40)
|
||||
|
||||
if not new_name:
|
||||
if not new_label:
|
||||
return
|
||||
|
||||
dis.fullscreen("Saving...")
|
||||
seeds = settings.master_get("seeds", [])
|
||||
|
||||
# save it
|
||||
seeds[idx] = (chk_xfp, encoded, new_name, meta)
|
||||
|
||||
seeds[idx] = (old.xfp, old.encoded, new_label, old.origin)
|
||||
# need to load and work on master secrets, will be slow if on tmp seed
|
||||
settings.master_set("seeds", seeds)
|
||||
|
||||
# update label in sub-menu
|
||||
menu.items[0].label = new_name
|
||||
menu.items[0].arg = menu.items[0].arg[0:2] + (new_name,) + menu.items[0].arg[3:]
|
||||
menu.items[0].label = new_label
|
||||
# take old arg, in rename we cannot change encoded value, so it can be used without
|
||||
# the need to deserialize it again
|
||||
_, encoded = menu.items[0].arg
|
||||
menu.items[0].arg = VaultEntry(*seeds[idx]), encoded
|
||||
|
||||
# .. and name in parent menu too
|
||||
# and name in parent menu too
|
||||
parent = the_ux.parent_of(menu)
|
||||
if parent:
|
||||
parent.update_contents()
|
||||
@ -933,6 +1012,8 @@ class SeedVaultMenu(MenuSystem):
|
||||
async def _add_current_tmp(*a):
|
||||
from pincodes import pa
|
||||
|
||||
assert not_hobbled_mode()
|
||||
|
||||
assert pa.tmp_value
|
||||
main_xfp = settings.master_get("xfp", 0)
|
||||
|
||||
@ -952,10 +1033,9 @@ class SeedVaultMenu(MenuSystem):
|
||||
seeds = settings.master_get("seeds", [])
|
||||
|
||||
# Save it into master settings
|
||||
seeds.append((new_xfp_str,
|
||||
stash.SecretStash.storage_serialize(pa.tmp_value),
|
||||
xfp_ui,
|
||||
"unknown origin"))
|
||||
seeds.append(list(VaultEntry(new_xfp_str,
|
||||
SecretStash.storage_serialize(pa.tmp_value),
|
||||
xfp_ui, "unknown origin")))
|
||||
|
||||
settings.master_set("seeds", seeds)
|
||||
|
||||
@ -967,31 +1047,38 @@ class SeedVaultMenu(MenuSystem):
|
||||
@classmethod
|
||||
def construct(cls):
|
||||
# Dynamic menu with user-defined names of seeds shown
|
||||
from glob import settings
|
||||
from pincodes import pa
|
||||
|
||||
rv = []
|
||||
add_current_tmp = MenuItem("Add current tmp", f=cls._add_current_tmp)
|
||||
|
||||
seeds = settings.master_get("seeds", [])
|
||||
seeds = list(seed_vault_iter())
|
||||
|
||||
if not seeds:
|
||||
rv.append(MenuItem('(none saved yet)'))
|
||||
if pa.tmp_value:
|
||||
rv.append(add_current_tmp)
|
||||
rv.append(MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu))
|
||||
if not_hobbled_mode():
|
||||
if pa.tmp_value:
|
||||
rv.append(add_current_tmp)
|
||||
rv.append(MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu))
|
||||
else:
|
||||
wipe_if_deltamode()
|
||||
|
||||
tmp_in_sv = False
|
||||
for i, (xfp_str, encoded, name, meta) in enumerate(seeds):
|
||||
for i, rec in enumerate(seeds):
|
||||
is_active = False
|
||||
encoded = pad_raw_secret(encoded)
|
||||
|
||||
# de-serialize encoded secret
|
||||
encoded = deserialize_secret(rec.encoded)
|
||||
if encoded == pa.tmp_value:
|
||||
is_active = tmp_in_sv = True
|
||||
|
||||
submenu = [
|
||||
MenuItem(name, f=cls._detail, arg=(xfp_str, encoded, name, meta)),
|
||||
MenuItem('Use This Seed', f=cls._set, arg=(xfp_str, encoded)),
|
||||
MenuItem('Rename', f=cls._rename, arg=(i, xfp_str)),
|
||||
MenuItem('Delete', f=cls._remove, arg=(i, xfp_str, encoded)),
|
||||
MenuItem(rec.label, f=cls._detail, arg=(rec, encoded)),
|
||||
MenuItem('Use This Seed', f=cls._set, arg=encoded),
|
||||
MenuItem('Rename', f=cls._rename, arg=(i, rec),
|
||||
predicate=not_hobbled_mode),
|
||||
MenuItem('Delete', f=cls._remove, arg=(i, rec, encoded),
|
||||
predicate=not_hobbled_mode),
|
||||
]
|
||||
if is_active:
|
||||
submenu[1] = MenuItem("Seed In Use")
|
||||
@ -1002,14 +1089,14 @@ class SeedVaultMenu(MenuSystem):
|
||||
# DO NOT offer any modification api (rename/delete)
|
||||
submenu = submenu[:2]
|
||||
|
||||
item = MenuItem('%2d: %s' % (i+1, name), menu=MenuSystem(submenu))
|
||||
item = MenuItem('%2d: %s' % (i+1, rec.label), menu=MenuSystem(submenu))
|
||||
if is_active:
|
||||
item.is_chosen = lambda: True
|
||||
|
||||
rv.append(item)
|
||||
|
||||
if pa.tmp_value:
|
||||
if seeds and (not tmp_in_sv):
|
||||
if seeds and (not tmp_in_sv) and not_hobbled_mode():
|
||||
# give em chance to store current active
|
||||
rv.append(add_current_tmp)
|
||||
|
||||
@ -1024,6 +1111,44 @@ class SeedVaultMenu(MenuSystem):
|
||||
tmp = self.construct()
|
||||
self.replace_items(tmp)
|
||||
|
||||
class SeedVaultChooserMenu(MenuSystem):
|
||||
def __init__(self, words_only=False):
|
||||
self.result = None
|
||||
|
||||
items = []
|
||||
for i, rec in enumerate(seed_vault_iter()):
|
||||
if words_only and not SecretStash.is_words(deserialize_secret(rec.encoded)):
|
||||
continue
|
||||
|
||||
item = MenuItem('%2d: %s' % (i+1, rec.label), arg=rec, f=self.picked)
|
||||
items.append(item)
|
||||
|
||||
if not items:
|
||||
items.append(MenuItem("(none suitable)"))
|
||||
|
||||
super().__init__(items)
|
||||
|
||||
async def picked(self, menu, idx, mi):
|
||||
assert menu == self
|
||||
|
||||
# show as "checked", for a touch
|
||||
menu.chosen = idx
|
||||
menu.show()
|
||||
await sleep_ms(100)
|
||||
|
||||
self.result = mi.arg
|
||||
the_ux.pop() # causes interact to stop
|
||||
|
||||
@classmethod
|
||||
async def pick(cls, **kws):
|
||||
# nice simple blocking menu present and pick
|
||||
m = cls(**kws)
|
||||
|
||||
the_ux.push(m)
|
||||
await m.interact()
|
||||
|
||||
return m.result
|
||||
|
||||
class EphemeralSeedMenu(MenuSystem):
|
||||
|
||||
@staticmethod
|
||||
@ -1042,8 +1167,9 @@ class EphemeralSeedMenu(MenuSystem):
|
||||
def construct(cls):
|
||||
from glob import NFC
|
||||
from actions import nfc_recv_ephemeral, import_xprv
|
||||
from actions import restore_temporary, scan_any_qr
|
||||
from actions import restore_backup, scan_any_qr
|
||||
from tapsigner import import_tapsigner_backup_file
|
||||
from xor_seed import xor_restore_temporary
|
||||
from charcodes import KEY_QR
|
||||
|
||||
import_ephemeral_menu = [
|
||||
@ -1060,32 +1186,31 @@ class EphemeralSeedMenu(MenuSystem):
|
||||
]
|
||||
|
||||
rv = [
|
||||
MenuItem("Generate Words", menu=gen_ephemeral_menu),
|
||||
MenuItem("Generate Words", menu=gen_ephemeral_menu, predicate=not_hobbled_mode),
|
||||
MenuItem('Import from QR Scan', predicate=version.has_qr,
|
||||
shortcut=KEY_QR, f=scan_any_qr, arg=(True, True)),
|
||||
MenuItem("Import Words", menu=import_ephemeral_menu),
|
||||
MenuItem("Import XPRV", f=import_xprv, arg=True), # ephemeral=True
|
||||
MenuItem("Tapsigner Backup", f=import_tapsigner_backup_file, arg=True), # ephemeral=True
|
||||
MenuItem("Coldcard Backup", f=restore_temporary),
|
||||
MenuItem("Coldcard Backup", f=restore_backup, arg=True), # tmp=True
|
||||
MenuItem("Restore Seed XOR", f=xor_restore_temporary),
|
||||
]
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
async def make_ephemeral_seed_menu(*a):
|
||||
|
||||
if (not pa.tmp_value) and (not settings.master_get("seedvault", False)):
|
||||
# force a warning on them, unless they are already doing it.
|
||||
ch = await ux_show_story(
|
||||
if not await ux_confirm(
|
||||
"Temporary seed is a secret completely separate "
|
||||
"from the master seed, typically held in device RAM and "
|
||||
"not persisted between reboots in the Secure Element. "
|
||||
"Enable the Seed Vault feature to store these secrets longer-term."
|
||||
"\n\nPress (4) to prove you read to the end"
|
||||
" of this message and accept all consequences.",
|
||||
"Enable the Seed Vault feature to store these secrets longer-term.",
|
||||
title="WARNING",
|
||||
escape="4"
|
||||
)
|
||||
if ch != "4":
|
||||
confirm_key="4"
|
||||
):
|
||||
return
|
||||
|
||||
rv = EphemeralSeedMenu.construct()
|
||||
@ -1113,10 +1238,10 @@ the passphrase as well, it's okay to put them together.) There is no way for \
|
||||
the Coldcard to know if your entry is correct, and if you have it wrong, \
|
||||
you will be looking at an empty wallet.
|
||||
|
||||
Limitations: 100 characters max length, ASCII characters 32-126 (0x20-0x7e) only.
|
||||
Limitations: %d characters max length, ASCII characters 32-126 (0x20-0x7e) only.
|
||||
|
||||
%s to continue or press (2) to hide this message forever.
|
||||
''' % (howto if not version.has_qwerty else '', OK)
|
||||
''' % (howto if not version.has_qwerty else '', MAX_PASS_LEN, OK)
|
||||
|
||||
ch = await ux_show_story(msg, escape='2')
|
||||
if ch == '2':
|
||||
@ -1126,8 +1251,8 @@ Limitations: 100 characters max length, ASCII characters 32-126 (0x20-0x7e) only
|
||||
|
||||
if version.has_qwerty and not PassphraseSaver.has_file():
|
||||
# no need for any menus if Q and no card present
|
||||
pp = await ux_input_text('', prompt="Your BIP-39 Passphrase",
|
||||
b39_complete=True, scan_ok=True, max_len=100)
|
||||
pp = await ux_input_text('', prompt="Your BIP-39 Passphrase", b39_complete=True,
|
||||
scan_ok=True, max_len=MAX_PASS_LEN)
|
||||
if not pp: return
|
||||
|
||||
await apply_pass_value(pp)
|
||||
@ -1137,7 +1262,7 @@ Limitations: 100 characters max length, ASCII characters 32-126 (0x20-0x7e) only
|
||||
|
||||
|
||||
class PassphraseMenu(MenuSystem):
|
||||
# Collect up to 100 chars as a BIP-39 passphrase
|
||||
# Collect up to MAX_PASS_LEN chars as a BIP-39 passphrase
|
||||
|
||||
# singleton (cls level) vars
|
||||
done_cb = None
|
||||
@ -1191,7 +1316,7 @@ class PassphraseMenu(MenuSystem):
|
||||
|
||||
return PassphraseSaverMenu(items)
|
||||
|
||||
def on_cancel(self):
|
||||
async def on_cancel(self):
|
||||
if not version.has_qwerty:
|
||||
# zip to cancel item when they fail to exit via X button
|
||||
self.goto_idx(self.count - 1)
|
||||
@ -1206,7 +1331,9 @@ class PassphraseMenu(MenuSystem):
|
||||
@classmethod
|
||||
async def add_numbers(cls, *a):
|
||||
# Mk4 only: add some digits (quick, easy)
|
||||
pw = await ux_input_numbers(cls.pp_sofar, cls.check_length)
|
||||
from ux_mk4 import ux_input_digits
|
||||
|
||||
pw = await ux_input_digits(cls.pp_sofar)
|
||||
if pw is not None:
|
||||
cls.pp_sofar = pw
|
||||
cls.check_length()
|
||||
@ -1224,7 +1351,7 @@ class PassphraseMenu(MenuSystem):
|
||||
async def view_edit_phrase(cls, *a):
|
||||
# let them control each character
|
||||
pw = await ux_input_text(cls.pp_sofar, prompt="Your BIP-39 Passphrase",
|
||||
b39_complete=True, scan_ok=True, max_len=100)
|
||||
b39_complete=True, scan_ok=True, max_len=MAX_PASS_LEN)
|
||||
if pw is not None:
|
||||
cls.pp_sofar = pw
|
||||
cls.check_length()
|
||||
@ -1235,8 +1362,8 @@ class PassphraseMenu(MenuSystem):
|
||||
|
||||
@classmethod
|
||||
def check_length(cls):
|
||||
# enforce a limit of 100 chars
|
||||
cls.pp_sofar = cls.pp_sofar[0:100]
|
||||
# enforce a limit of MAX_PASS_LEN chars
|
||||
cls.pp_sofar = cls.pp_sofar[0:MAX_PASS_LEN]
|
||||
|
||||
@classmethod
|
||||
async def add_text(cls, _1, _2, item):
|
||||
@ -1277,15 +1404,16 @@ async def apply_pass_value(new_pp):
|
||||
|
||||
msg = ('Above is the master key fingerprint of the new wallet'
|
||||
' created by adding passphrase to %s.'
|
||||
'\n\nPassphrase: %s'
|
||||
'\n\nPress %s to abort, %s to use the new wallet, (1) to apply'
|
||||
' and save to MicroSD for future.') % (msg, X, OK)
|
||||
' and save to MicroSD for future.') % (msg, new_pp, X, OK)
|
||||
|
||||
ch = await ux_show_story(msg, title="[%s]" % xfp_str, escape='1')
|
||||
if ch == 'x':
|
||||
return
|
||||
|
||||
await set_ephemeral_seed(nv, summarize_ux=False, bip39pw=new_pp,
|
||||
meta="BIP-39 Passphrase on [%s]" % parent_xfp_str)
|
||||
origin="BIP-39 Passphrase on [%s]" % parent_xfp_str)
|
||||
|
||||
if ch == '1':
|
||||
try:
|
||||
|
||||
@ -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_P2TR, AF_BARE_PK
|
||||
|
||||
# single-shot hash functions
|
||||
sha256 = ngu.hash.sha256s
|
||||
@ -27,8 +27,8 @@ ripemd160 = ngu.hash.ripemd160
|
||||
hash256 = ngu.hash.sha256d
|
||||
hash160 = ngu.hash.hash160
|
||||
|
||||
def bytes_to_hex_str(s):
|
||||
return str(b2a_hex(s), 'ascii')
|
||||
#def bytes_to_hex_str(s):
|
||||
# return str(b2a_hex(s), 'ascii')
|
||||
|
||||
SIGHASH_ALL = const(1)
|
||||
SIGHASH_NONE = const(2)
|
||||
@ -60,10 +60,13 @@ def deser_compact_size(f):
|
||||
nit = struct.unpack("<B", f.read(1))[0]
|
||||
if nit == 253:
|
||||
nit = struct.unpack("<H", f.read(2))[0]
|
||||
assert nit >= 253
|
||||
elif nit == 254:
|
||||
nit = struct.unpack("<I", f.read(4))[0]
|
||||
assert nit >= 0x1_0000
|
||||
elif nit == 255:
|
||||
nit = struct.unpack("<Q", f.read(8))[0]
|
||||
assert nit >= 0x1_0000_0000
|
||||
return nit
|
||||
|
||||
def deser_string(f):
|
||||
@ -80,7 +83,6 @@ def deser_uint256(f):
|
||||
r += t << (i * 32)
|
||||
return r
|
||||
|
||||
|
||||
def ser_uint256(u):
|
||||
rs = b""
|
||||
for i in range(8):
|
||||
@ -88,7 +90,6 @@ def ser_uint256(u):
|
||||
u >>= 32
|
||||
return rs
|
||||
|
||||
|
||||
def uint256_from_str(s):
|
||||
r = 0
|
||||
t = struct.unpack("<IIIIIIII", s[:32])
|
||||
@ -96,13 +97,11 @@ def uint256_from_str(s):
|
||||
r += t[i] << (i * 32)
|
||||
return r
|
||||
|
||||
|
||||
def uint256_from_compact(c):
|
||||
nbytes = (c >> 24) & 0xFF
|
||||
v = (c & 0xFFFFFF) << (8 * (nbytes - 3))
|
||||
return v
|
||||
|
||||
|
||||
def deser_vector(f, c):
|
||||
nit = deser_compact_size(f)
|
||||
r = []
|
||||
@ -112,7 +111,6 @@ def deser_vector(f, c):
|
||||
r.append(t)
|
||||
return r
|
||||
|
||||
|
||||
# ser_function_name: Allow for an alternate serialization function on the
|
||||
# entries in the vector (we use this for serializing the vector of transactions
|
||||
# for a witness block).
|
||||
@ -125,7 +123,6 @@ def ser_vector(l, ser_function_name=None):
|
||||
r += i.serialize()
|
||||
return r
|
||||
|
||||
|
||||
def deser_uint256_vector(f):
|
||||
nit = deser_compact_size(f)
|
||||
r = []
|
||||
@ -134,29 +131,22 @@ def deser_uint256_vector(f):
|
||||
r.append(t)
|
||||
return r
|
||||
|
||||
|
||||
def ser_uint256_vector(l):
|
||||
r = ser_compact_size(len(l))
|
||||
for i in l:
|
||||
r += ser_uint256(i)
|
||||
return r
|
||||
|
||||
|
||||
def deser_string_vector(f):
|
||||
nit = deser_compact_size(f)
|
||||
r = []
|
||||
for i in range(nit):
|
||||
t = deser_string(f)
|
||||
r.append(t)
|
||||
return r
|
||||
|
||||
return [deser_string(f) for _ in range(nit)]
|
||||
|
||||
def ser_string_vector(l):
|
||||
r = ser_compact_size(len(l))
|
||||
for sv in l:
|
||||
r += ser_string(sv)
|
||||
return r
|
||||
|
||||
return r
|
||||
|
||||
def deser_int_vector(f):
|
||||
nit = deser_compact_size(f)
|
||||
@ -166,7 +156,6 @@ def deser_int_vector(f):
|
||||
r.append(t)
|
||||
return r
|
||||
|
||||
|
||||
def ser_int_vector(l):
|
||||
r = ser_compact_size(len(l))
|
||||
for i in l:
|
||||
@ -177,16 +166,18 @@ def ser_push_data(dd):
|
||||
# "compile" data to be pushed on the script stack
|
||||
# - will be minimal sized, but only supports size ranges we're likely to see
|
||||
ll = len(dd)
|
||||
assert 2 <= ll <= 255
|
||||
|
||||
if ll <= 75:
|
||||
if ll < 0x4c:
|
||||
return bytes([ll]) + dd # OP_PUSHDATAn + data
|
||||
elif ll <= 0xff:
|
||||
return bytes([0x4c, ll]) + dd # 0x4c = 76 => OP_PUSHDATA1 + size + data
|
||||
elif ll <= 0xffff:
|
||||
return bytes([0x4d]) + struct.pack(b'<H', ll) + dd # # 0x4d = 77 => OP_PUSHDATA2
|
||||
else:
|
||||
return bytes([76, ll]) + dd # 0x4c = 76 => OP_PUSHDATA1 + size + data
|
||||
assert False
|
||||
|
||||
def ser_push_int(n):
|
||||
# push a small integer onto the stack
|
||||
from opcodes import OP_0, OP_1, OP_16, OP_PUSHDATA1
|
||||
from opcodes import OP_0, OP_1
|
||||
|
||||
if n == 0:
|
||||
return bytes([OP_0])
|
||||
@ -204,40 +195,45 @@ def disassemble(script):
|
||||
|
||||
try:
|
||||
offset = 0
|
||||
slen = len(script)
|
||||
while 1:
|
||||
if offset >= len(script):
|
||||
if offset >= slen:
|
||||
#print('dis %d done' % offset)
|
||||
return
|
||||
c = script[offset]
|
||||
offset += 1
|
||||
|
||||
if 1 <= c <= 75:
|
||||
#print('dis %d: bytes=%s' % (offset, b2a_hex(script[offset:offset+c])))
|
||||
yield (script[offset:offset+c], None)
|
||||
offset += c
|
||||
cnt = c
|
||||
elif OP_1 <= c <= OP_16:
|
||||
# OP_1 thru OP_16
|
||||
#print('dis %d: number=%d' % (offset, (c - OP_1 + 1)))
|
||||
yield (c - OP_1 + 1, None)
|
||||
continue
|
||||
elif c == OP_PUSHDATA1:
|
||||
cnt = script[offset]; offset += 1
|
||||
yield (script[offset:offset+cnt], None)
|
||||
offset += cnt
|
||||
cnt = script[offset]
|
||||
offset += 1
|
||||
elif c == OP_PUSHDATA2:
|
||||
cnt = struct.unpack_from("H", script, offset)
|
||||
# up to 65535 bytes
|
||||
cnt, = struct.unpack_from("H", script, offset)
|
||||
offset += 2
|
||||
yield (script[offset:offset+cnt], None)
|
||||
offset += cnt
|
||||
elif c == OP_PUSHDATA4:
|
||||
# no where to put so much data
|
||||
raise NotImplementedError
|
||||
elif c == OP_1NEGATE:
|
||||
yield (-1, None)
|
||||
continue
|
||||
else:
|
||||
# OP_0 included here
|
||||
#print('dis %d: opcode=%d' % (offset, c))
|
||||
yield (None, c)
|
||||
except:
|
||||
continue
|
||||
|
||||
# a data push of `cnt` bytes - reject if it runs off the end
|
||||
if offset + cnt > slen:
|
||||
raise ValueError
|
||||
yield (script[offset:offset+cnt], None)
|
||||
offset += cnt
|
||||
except Exception as e:
|
||||
# import sys;sys.print_exception(e)
|
||||
raise ValueError("bad script")
|
||||
|
||||
|
||||
@ -326,7 +322,6 @@ class CTxIn(object):
|
||||
self.nSequence = nSequence
|
||||
|
||||
def deserialize(self, f):
|
||||
self.prevout = COutPoint()
|
||||
self.prevout.deserialize(f)
|
||||
self.scriptSig = deser_string(f)
|
||||
self.nSequence = struct.unpack("<I", f.read(4))[0]
|
||||
@ -361,30 +356,47 @@ class CTxOut(object):
|
||||
# Detect type of output from scriptPubKey, and return 3-tuple:
|
||||
# (addr_type_code, addr, is_segwit)
|
||||
# 'addr' is byte string, either 20 or 32 long
|
||||
if self.is_p2tr():
|
||||
return AF_P2TR, self.scriptPubKey[2:2+32], True
|
||||
|
||||
if len(self.scriptPubKey) == 22 and \
|
||||
self.scriptPubKey[0] == 0 and self.scriptPubKey[1] == 20:
|
||||
# aka. P2WPKH
|
||||
return 'p2pkh', self.scriptPubKey[2:2+20], True
|
||||
if self.is_p2wpkh():
|
||||
return AF_P2WPKH, self.scriptPubKey[2:2+20], True
|
||||
|
||||
if len(self.scriptPubKey) == 34 and \
|
||||
self.scriptPubKey[0] == 0 and self.scriptPubKey[1] == 32:
|
||||
# aka. P2WSH
|
||||
return 'p2sh', self.scriptPubKey[2:2+32], True
|
||||
if self.is_p2wsh():
|
||||
return AF_P2WSH, self.scriptPubKey[2:2+32], True
|
||||
|
||||
if self.is_p2pkh():
|
||||
return 'p2pkh', self.scriptPubKey[3:3+20], False
|
||||
return AF_CLASSIC, self.scriptPubKey[3:3+20], False
|
||||
|
||||
if self.is_p2sh():
|
||||
return 'p2sh', self.scriptPubKey[2:2+20], False
|
||||
# can be:
|
||||
# * bare P2SH
|
||||
# * P2SH-P2WPKH
|
||||
# * P2SH-P2WSH
|
||||
return AF_P2SH, self.scriptPubKey[2:2+20], False
|
||||
|
||||
if self.is_p2pk():
|
||||
# rare, pay to full pubkey
|
||||
return 'p2pk', self.scriptPubKey[2:2+33], False
|
||||
# rare, pay to full pubkey: <push_op> <pubkey> OP_CHECKSIG
|
||||
# push_op is 0x21 (33) for compressed, 0x41 (65) for uncompressed
|
||||
pk_len = self.scriptPubKey[0]
|
||||
return AF_BARE_PK, self.scriptPubKey[1:1+pk_len], False
|
||||
|
||||
# If this is reached, we do not understand the output well
|
||||
# enough to allow the user to authorize the spend, so fail hard.
|
||||
raise ValueError('scriptPubKey template fail: ' + b2a_hex(self.scriptPubKey).decode())
|
||||
if self.is_op_return():
|
||||
return OP_RETURN, self.scriptPubKey, False
|
||||
|
||||
return None, self.scriptPubKey, None
|
||||
|
||||
def is_p2tr(self):
|
||||
return len(self.scriptPubKey) == 34 and \
|
||||
(OP_1 <= self.scriptPubKey[0] <= OP_16) and self.scriptPubKey[1] == 0x20
|
||||
|
||||
def is_p2wpkh(self):
|
||||
return len(self.scriptPubKey) == 22 and \
|
||||
self.scriptPubKey[0] == 0 and self.scriptPubKey[1] == 0x14
|
||||
|
||||
def is_p2wsh(self):
|
||||
return len(self.scriptPubKey) == 34 and \
|
||||
self.scriptPubKey[0] == 0 and self.scriptPubKey[1] == 0x20
|
||||
|
||||
def is_p2sh(self):
|
||||
return len(self.scriptPubKey) == 23 and self.scriptPubKey[0] == 0xa9 \
|
||||
@ -397,8 +409,11 @@ class CTxOut(object):
|
||||
|
||||
def is_p2pk(self):
|
||||
return (len(self.scriptPubKey) == 35 or len(self.scriptPubKey) == 67) \
|
||||
and (self.scriptPubKey[0] == 0x21 or self.scriptPubKey[0] == 0x41) \
|
||||
and self.scriptPubKey[-1] == 0xac
|
||||
and self.scriptPubKey[0] == len(self.scriptPubKey) - 2 \
|
||||
and self.scriptPubKey[-1] == OP_CHECKSIG
|
||||
|
||||
def is_op_return(self):
|
||||
return self.scriptPubKey and (self.scriptPubKey[0] == OP_RETURN)
|
||||
|
||||
#def __repr__(self):
|
||||
# return "CTxOut(nValue=%d scriptPubKey=%s)" \
|
||||
@ -489,7 +504,7 @@ class CTransaction(object):
|
||||
self.nVersion = struct.unpack("<i", f.read(4))[0]
|
||||
self.vin = deser_vector(f, CTxIn)
|
||||
flags = 0
|
||||
if len(self.vin) == 0:
|
||||
if not self.vin:
|
||||
flags = struct.unpack("<B", f.read(1))[0]
|
||||
# Not sure why flags can't be zero, but this
|
||||
# matches the implementation in bitcoind
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
#
|
||||
# sffile.py - file-like objects stored in PSRAM (Mk4+) (used to be SPI Flash)
|
||||
#
|
||||
# - implements stream IO protoccol
|
||||
# - implements stream IO protocol
|
||||
# - random read, sequential write
|
||||
# - only a few of these are possible
|
||||
# - the offset is the file name
|
||||
@ -26,7 +26,7 @@ class SFFile:
|
||||
self.message = message
|
||||
self.runt = False
|
||||
|
||||
if max_size != None:
|
||||
if max_size is not None:
|
||||
# Write
|
||||
self.max_size = max_size
|
||||
self.readonly = False
|
||||
|
||||
@ -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
|
||||
|
||||
787
shared/teleport.py
Normal file
787
shared/teleport.py
Normal file
@ -0,0 +1,787 @@
|
||||
# (c) Copyright 2025 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
# teleport.py - Magically transport extremely sensitive data between the
|
||||
# secure environment of two Q's.
|
||||
#
|
||||
import ngu, aes256ctr, bip39, json, ndef, chains
|
||||
from utils import xfp2str, deserialize_secret
|
||||
from ubinascii import unhexlify as a2b_hex
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from glob import settings, dis
|
||||
from ux import ux_show_story, ux_confirm, the_ux, ux_dramatic_pause
|
||||
from ux_q1 import show_bbqr_codes, QRScannerInteraction, ux_input_text
|
||||
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
|
||||
from bbqr import b32encode, b32decode
|
||||
from menu import MenuItem, MenuSystem
|
||||
from notes import NoteContentBase
|
||||
from sffile import SFFile
|
||||
from multisig import MultisigWallet
|
||||
from stash import SensitiveValues, SecretStash, blank_object, bip39_passphrase
|
||||
|
||||
# One page github-hosted static website that shows QR based on URL contents pushed by NFC
|
||||
KT_DOMAIN = 'keyteleport.com'
|
||||
|
||||
# No length/size worries with simple secrets, but massive notes and big PSBT,
|
||||
# with lots of UTXO, cannot be passed via NFC URL, because we are limited by
|
||||
# NFC chip (8k) and URL length (4k or less) inside. BBQr is not limited however.
|
||||
# - but the website is ready to make animated BBQr nicely
|
||||
NFC_SIZE_LIMIT = const(4096)
|
||||
|
||||
def short_bbqr(type_code, data):
|
||||
# Short-circuit basic BBQr encoding here: always Base32, single part: 1 of 1
|
||||
# - used only for NFC link, where website may split again into parts
|
||||
hdr = 'B$2%s0100' % type_code
|
||||
|
||||
return hdr + b32encode(data)
|
||||
|
||||
def txt_grouper(txt):
|
||||
# split into 2-char groups and add spaces -- to make it easier to read/remember
|
||||
return ' '.join(txt[n:n+2] for n in range(0, len(txt), 2))
|
||||
|
||||
async def nfc_push_kt(qrdata):
|
||||
# NFC push to send them to our QR-rendering website
|
||||
|
||||
url = KT_DOMAIN + '/#' + qrdata
|
||||
|
||||
n = ndef.ndefMaker()
|
||||
n.add_url(url, https=True)
|
||||
|
||||
from glob import NFC
|
||||
await NFC.share_loop(n, prompt="View QR on web", line2=KT_DOMAIN)
|
||||
|
||||
async def kt_start_rx(*a):
|
||||
# menu item to "start a receive" operation
|
||||
|
||||
rx_key = settings.get("ktrx")
|
||||
|
||||
if rx_key:
|
||||
# Maybe re-use same one? Vaguely risky? Concern is they are confused and
|
||||
# we don't want to lose the pubkey if they should be scanning not here.
|
||||
ch = await ux_show_story('''Looks like last attempt wasn't completed. \
|
||||
You need to do QR scan of data from the sender to move to the next step. \
|
||||
We will re-use same values as last try, unless you press (R) for new values to be picked.''',
|
||||
title='Reuse Pubkey?', escape='r'+KEY_QR, hint_icons=KEY_QR)
|
||||
|
||||
if ch == KEY_QR:
|
||||
# help them scan now!
|
||||
x = QRScannerInteraction()
|
||||
await x.scan_anything(expect_secret=False, tmp=False)
|
||||
return
|
||||
elif ch == 'r':
|
||||
# wipe and restart; sender's work might be lost
|
||||
rx_key = None
|
||||
else:
|
||||
# keep old keypair -- they might be confused
|
||||
kp = ngu.secp256k1.keypair(a2b_hex(rx_key))
|
||||
|
||||
if not rx_key:
|
||||
# pick a random key pair, just for this session
|
||||
kp = ngu.secp256k1.keypair()
|
||||
|
||||
settings.set("ktrx", b2a_hex(kp.privkey()))
|
||||
settings.save()
|
||||
|
||||
short_code, payload = generate_rx_code(kp)
|
||||
|
||||
msg = '''To receive sensitive data from another COLDCARD, \
|
||||
share this Receiver Password with sender:
|
||||
|
||||
%s = %s
|
||||
|
||||
and show the QR on next screen to the sender. ENTER or %s to show here''' % (
|
||||
short_code, txt_grouper(short_code), KEY_QR)
|
||||
|
||||
await tk_show_payload('R', payload, 'Key Teleport: Receive', msg, cta='Show to Sender')
|
||||
|
||||
def generate_rx_code(kp):
|
||||
# Receiver-side password: given a pubkey (33 bytes, compressed format)
|
||||
# - construct an 8-digit decimal "password"
|
||||
# - it's a AES key, but only 26 bits worth
|
||||
pubkey = bytearray(kp.pubkey().to_bytes()) # default: compressed format
|
||||
#assert len(pubkey) == 33
|
||||
|
||||
# - want the code to be deterministic, but I also don't want to save it
|
||||
nk = ngu.hash.sha256d(kp.privkey() + b'COLCARD4EVER')
|
||||
|
||||
# first byte will be 0x02 or 0x03 (Y coord) -- remove those known 7 bits
|
||||
pubkey[0] ^= nk[20] & 0xfe
|
||||
|
||||
num = '%08d' % (int.from_bytes(nk[4:8], 'big') % 1_0000_0000)
|
||||
|
||||
# encryption after baby key stretch
|
||||
kk = ngu.hash.sha256s(num.encode())
|
||||
enc = aes256ctr.new(kk).cipher(pubkey)
|
||||
|
||||
return num, enc
|
||||
|
||||
def decrypt_rx_pubkey(code, payload):
|
||||
# given a 8-digit numeric code, make the key and then decrypt/checksum check
|
||||
# - every value works, there is no fail.
|
||||
kk = ngu.hash.sha256s(code.encode())
|
||||
rx_pubkey = bytearray(aes256ctr.new(kk).cipher(payload))
|
||||
|
||||
# first byte will be 0x02 or 0x03 but other 7 bits are noise
|
||||
rx_pubkey[0] &= 0x01
|
||||
rx_pubkey[0] |= 0x02
|
||||
|
||||
# validate that it's on the curve... otherwise the code is wrong
|
||||
try:
|
||||
ngu.secp256k1.pubkey(rx_pubkey)
|
||||
|
||||
return rx_pubkey
|
||||
except:
|
||||
return None
|
||||
|
||||
async def tk_show_payload(type_code, payload, title, msg, cta=None):
|
||||
# show the QR and/or NFC
|
||||
# - MAYBE: make easier/faster to pick NFC from QR screen and vice-versa
|
||||
from glob import NFC
|
||||
|
||||
hints = KEY_QR
|
||||
if NFC and len(payload) < NFC_SIZE_LIMIT:
|
||||
hints += KEY_NFC
|
||||
msg += ' or %s to view on your phone' % KEY_NFC
|
||||
|
||||
msg += '. CANCEL to stop.'
|
||||
|
||||
# simply show the QR
|
||||
while 1:
|
||||
ch = await ux_show_story(msg, title=title, hint_icons=hints)
|
||||
|
||||
if ch == KEY_NFC and NFC:
|
||||
await nfc_push_kt(short_bbqr(type_code, payload))
|
||||
elif ch == KEY_QR or ch == 'y':
|
||||
# NOTE: CTA rarely seen, but maybe sometimes?
|
||||
await show_bbqr_codes(type_code, payload, msg=cta)
|
||||
elif ch == 'x':
|
||||
return
|
||||
|
||||
async def kt_start_send(rx_data):
|
||||
# a QR was scanned and it held (most of) a pubkey
|
||||
# - they want to send to this guy
|
||||
# - ask them what to send, etc
|
||||
|
||||
while 1:
|
||||
# - ask for the sender's password -- nearly any value will be accepted
|
||||
code = await ux_input_text('', confirm_exit=False, hex_only=True, max_len=8,
|
||||
prompt='Teleport Password (number)', min_len=8, b39_complete=False, scan_ok=False,
|
||||
placeholder='########', funct_keys=None, force_xy=None)
|
||||
if not code: return
|
||||
|
||||
rx_pubkey = decrypt_rx_pubkey(code, rx_data)
|
||||
|
||||
if rx_pubkey:
|
||||
break
|
||||
|
||||
# I think only about 50% odds of catching an incorrect code. Not sure.
|
||||
ch = await ux_show_story(
|
||||
"Incorrect Teleport Password. You can try again or CANCEL to stop.")
|
||||
if ch == 'x': return
|
||||
|
||||
msg = '''You can now Key Teleport secrets! Choose what to share on next screen.\
|
||||
\n
|
||||
WARNING: Receiver will have full access to all Bitcoin controlled by these keys!'''
|
||||
|
||||
ch = await ux_show_story(msg, title="Key Teleport: Send")
|
||||
if ch != 'y': return
|
||||
|
||||
# pick what to send from a series of submenus
|
||||
menu = SecretPickerMenu(rx_pubkey)
|
||||
the_ux.push(menu)
|
||||
|
||||
async def kt_do_send(rx_pubkey, dtype, raw=None, obj=None, prefix=b'', rx_label='the receiver', kp=None):
|
||||
# We are rendering a QR and showing it to them for sending to another Q
|
||||
dis.fullscreen("Wait...")
|
||||
cleartext = dtype.encode() + (raw or json.dumps(obj).encode())
|
||||
dis.progress_bar_show(0.1)
|
||||
|
||||
# Pick and show noid key to sender
|
||||
noid_key, txt = pick_noid_key()
|
||||
|
||||
dis.progress_bar_show(0.25)
|
||||
|
||||
# all new EC key
|
||||
my_keypair = kp or ngu.secp256k1.keypair()
|
||||
|
||||
dis.progress_bar_show(0.75)
|
||||
|
||||
payload = prefix + encode_payload(my_keypair, rx_pubkey, noid_key, cleartext,
|
||||
for_psbt=bool(prefix))
|
||||
|
||||
dis.progress_bar_show(1)
|
||||
|
||||
msg = "Share this password with %s, via some different channel:"\
|
||||
"\n\n %s = %s\n\n" % (rx_label, txt, txt_grouper(txt))
|
||||
msg += "ENTER to view QR"
|
||||
|
||||
await tk_show_payload('S' if not prefix else 'E', payload,
|
||||
'Teleport Password', msg, cta='Show to Receiver')
|
||||
|
||||
if not prefix:
|
||||
# not PSBT case ... reset menus, we are deep!
|
||||
from actions import goto_top_menu
|
||||
goto_top_menu()
|
||||
|
||||
def pick_noid_key():
|
||||
# pick an 40 bit password, shown as base32
|
||||
# - on rx, libngu base32 decoder will convert '018' into 'OLB'
|
||||
# - but a little tempted to removed vowels here?
|
||||
k = ngu.random.bytes(5)
|
||||
txt = b32encode(k).upper()
|
||||
|
||||
return k, txt
|
||||
|
||||
async def kt_decode_rx(is_psbt, payload):
|
||||
# we are getting data back from a sender, decode it.
|
||||
|
||||
prompt = 'Teleport Password (text)'
|
||||
|
||||
if not is_psbt:
|
||||
rx_key = settings.get("ktrx")
|
||||
if not rx_key:
|
||||
await ux_show_story("Not expecting any teleports. You need to start over.")
|
||||
|
||||
await kt_start_rx() # help them to start over? idk maybe not.
|
||||
return
|
||||
|
||||
his_pubkey = payload[0:33]
|
||||
body = payload[33:]
|
||||
pair = ngu.secp256k1.keypair(a2b_hex(rx_key))
|
||||
|
||||
ses_key, body = decode_step1(pair, his_pubkey, body)
|
||||
else:
|
||||
# Multisig PSBT: will need to iterate over a few wallets and each N-1 possible senders
|
||||
if not MultisigWallet.exists():
|
||||
await ux_show_story("Incoming PSBT requires multisig wallet(s) to be already setup, but you have none.")
|
||||
return
|
||||
|
||||
ses_key, body, sender_xfp = MultisigWallet.kt_search_rxkey(payload)
|
||||
|
||||
if sender_xfp is not None:
|
||||
prompt = 'Teleport Password from [%s]' % xfp2str(sender_xfp)
|
||||
|
||||
if not ses_key:
|
||||
# when ECDH fails, it's truncation or wrong RX key (due to sender using old rx key,
|
||||
# or the numeric code the sender entered was wrong, etc)
|
||||
await ux_show_story("QR code was damaged, "+
|
||||
("numeric password was wrong, " if not is_psbt else "")+
|
||||
"or it was sent to a different user. "
|
||||
"Sender must start again.", title="Teleport Fail")
|
||||
return
|
||||
|
||||
while 1:
|
||||
# ask for noid key
|
||||
pw = await ux_input_text('', confirm_exit=False, hex_only=False, max_len=8,
|
||||
prompt=prompt, min_len=8, b39_complete=False, scan_ok=False,
|
||||
placeholder='********', funct_keys=None, force_xy=None)
|
||||
if not pw: return
|
||||
|
||||
dis.fullscreen("Wait...")
|
||||
try:
|
||||
assert len(pw) == 8
|
||||
noid_key = b32decode(pw) # case insenstive, and smart about confused chars
|
||||
final = decode_step2(ses_key, noid_key, body)
|
||||
if final is not None:
|
||||
break
|
||||
except: pass
|
||||
|
||||
ch = await ux_show_story(
|
||||
"Incorrect Teleport Password. You can try again or CANCEL to stop.")
|
||||
if ch == 'x': return
|
||||
# will ask again
|
||||
|
||||
# success w/ decoding. but maybe something goes wrong or they reject a confirm step
|
||||
# so keep the rx key alive still
|
||||
|
||||
await kt_accept_values(chr(final[0]), final[1:])
|
||||
|
||||
async def kt_accept_values(dtype, raw):
|
||||
# We got some secret, decode it more, and save it.
|
||||
'''
|
||||
- `s` - secret, encoded per stash.py
|
||||
- `r` - raw XPRV mode - 64 bytes follow which are the chain code then master privkey
|
||||
- `x` - XPRV mode, full details - 4 bytes (XPRV) + base58 *decoded* binary-XPRV follows
|
||||
- `n` - one or many notes export (JSON array)
|
||||
- `v` - seed vault export (JSON: one secret key but includes includes name, source of key)
|
||||
- `p` - binary PSBT to be signed
|
||||
- `b` - complete system backup file (text, internal format)
|
||||
'''
|
||||
from flow import has_se_secrets, goto_top_menu
|
||||
from pincodes import pa
|
||||
|
||||
enc = None
|
||||
origin = 'Teleported'
|
||||
label = None
|
||||
|
||||
if pa.hobbled_mode and dtype != 'p':
|
||||
await ux_show_story('Only PSBT for multisig accepted in this mode.', title='FAILED')
|
||||
return
|
||||
|
||||
|
||||
if dtype == 's':
|
||||
# words / bip 32 master / xprv, etc
|
||||
enc = bytearray(72)
|
||||
enc[0:len(raw)] = raw
|
||||
|
||||
elif dtype == 'x':
|
||||
# it's an XPRV, but in binary.. some extra data we throw away here; sigh
|
||||
# XXX no way to send this .. but was thinking of address explorer
|
||||
txt = ngu.codecs.b58_encode(raw)
|
||||
node, ch, _, _ = chains.slip132_deserialize(txt)
|
||||
assert ch.name == chains.current_chain().name, 'wrong chain'
|
||||
enc = SecretStash.encode(xprv=node)
|
||||
|
||||
elif dtype == 'p':
|
||||
# raw PSBT -- much bigger more complex
|
||||
from auth import sign_transaction, TXN_INPUT_OFFSET
|
||||
|
||||
psbt_len = len(raw)
|
||||
|
||||
# copy into PSRAM
|
||||
with SFFile(TXN_INPUT_OFFSET, max_size=psbt_len) as out:
|
||||
out.write(raw)
|
||||
|
||||
# This will take over UX w/ the signing process
|
||||
# flags=None --> whether to finalize is decided based on psbt.is_complete
|
||||
sign_transaction(psbt_len, flags=None, input_method="kt")
|
||||
return
|
||||
|
||||
elif dtype == 'b':
|
||||
# full system backup, including master: text lines
|
||||
from backups import text_bk_parser, restore_tmp_from_dict_ll, restore_from_dict, extract_raw_secret
|
||||
|
||||
try:
|
||||
vals = text_bk_parser(raw)
|
||||
assert vals # empty?
|
||||
raw_sec, _ = extract_raw_secret(vals)
|
||||
except Exception as e:
|
||||
await ux_show_story("Invalid backup\n\n" + str(e), title='FAILED')
|
||||
return
|
||||
|
||||
from flow import has_secrets
|
||||
|
||||
if has_secrets():
|
||||
# restores as tmp secret and/or offers to save to SeedVault
|
||||
# need to remove key before I get into tmp seed settings
|
||||
# so even if this errors out, new ktrx is needed
|
||||
settings.remove_key("ktrx")
|
||||
prob = await restore_tmp_from_dict_ll(vals, raw_sec)
|
||||
else:
|
||||
# we have no secret, so... reboot if it works, else errors shown, etc.
|
||||
prob = await restore_from_dict(vals, raw_sec)
|
||||
|
||||
if prob:
|
||||
await ux_show_story(prob, title='FAILED')
|
||||
else:
|
||||
# force new rx key because this tfr worked
|
||||
# only has effect if in master seed settings
|
||||
settings.remove_key("ktrx")
|
||||
return
|
||||
|
||||
elif dtype in 'nv':
|
||||
# all are JSON things
|
||||
js = json.loads(raw)
|
||||
|
||||
if dtype == 'v':
|
||||
# one key export from a seed vault
|
||||
# - watch for incompatibility here if we ever change VaultEntry
|
||||
from seed import VaultEntry
|
||||
rec = VaultEntry(*js)
|
||||
enc = deserialize_secret(rec.encoded)
|
||||
origin = rec.origin
|
||||
label = rec.label
|
||||
elif dtype == 'n':
|
||||
# import secure note(s)
|
||||
from notes import import_from_json, make_notes_menu, NoteContent
|
||||
|
||||
settings.remove_key("ktrx") # force new rx key after this point
|
||||
await import_from_json(dict(coldcard_notes=js))
|
||||
|
||||
await ux_dramatic_pause('Imported.', 2)
|
||||
|
||||
# force them into notes submenu so they can see result right away
|
||||
# - highlight to last note, which should be the just-added one(s)
|
||||
goto_top_menu()
|
||||
nm = await make_notes_menu()
|
||||
nm.goto_idx(NoteContent.count()-1)
|
||||
the_ux.push(nm)
|
||||
|
||||
return
|
||||
else:
|
||||
raise ValueError(dtype)
|
||||
|
||||
# key material is arriving; offer to use as main secret, or tmp, or seed vault?
|
||||
settings.remove_key("ktrx") # force new rx key after this point
|
||||
assert enc
|
||||
|
||||
from seed import set_ephemeral_seed, set_seed_value
|
||||
|
||||
if not has_se_secrets():
|
||||
# unit has nothing, so this will be the master seed
|
||||
set_seed_value(encoded=enc)
|
||||
ok = True
|
||||
else:
|
||||
ok = await set_ephemeral_seed(enc, origin=origin, label=label)
|
||||
|
||||
if ok:
|
||||
goto_top_menu()
|
||||
|
||||
def noid_stretch(session_key, noid_key):
|
||||
# TODO: measure timing of this on real Q
|
||||
return ngu.hash.pbkdf2_sha512(session_key, noid_key, 5000)[0:32]
|
||||
|
||||
def encode_payload(my_keypair, his_pubkey, noid_key, body, for_psbt=False):
|
||||
# do all the encryption for sender
|
||||
assert len(his_pubkey) == 33
|
||||
assert len(noid_key) == 5
|
||||
|
||||
# this can fail with ValueError: secp256k1_ec_pubkey_parse
|
||||
# if the user has provided the wrong value for numeric password
|
||||
# - better to catch this sooner in decrypt_rx_pubkey
|
||||
session_key = my_keypair.ecdh_multiply(his_pubkey)
|
||||
|
||||
# stretch noid key out -- will be slow
|
||||
pk = noid_stretch(session_key, noid_key)
|
||||
|
||||
b1 = aes256ctr.new(pk).cipher(body)
|
||||
b1 += ngu.hash.sha256s(body)[-2:]
|
||||
|
||||
b2 = aes256ctr.new(session_key).cipher(b1)
|
||||
b2 += ngu.hash.sha256s(b1)[-2:]
|
||||
|
||||
if for_psbt:
|
||||
# no need to share pubkey for PSBT files
|
||||
return b2
|
||||
|
||||
return my_keypair.pubkey().to_bytes() + b2
|
||||
|
||||
def decode_step1(my_keypair, his_pubkey, body):
|
||||
# Do ECDH and remove top layer of encryption
|
||||
try:
|
||||
assert len(body) >= 3
|
||||
|
||||
session_key = my_keypair.ecdh_multiply(his_pubkey)
|
||||
|
||||
rv = aes256ctr.new(session_key).cipher(body[:-2])
|
||||
chk = ngu.hash.sha256s(rv)[-2:]
|
||||
|
||||
assert chk == body[-2:] # likely means wrong rx key, or truncation
|
||||
except:
|
||||
return None, None
|
||||
|
||||
return session_key, rv
|
||||
|
||||
def decode_step2(session_key, noid_key, body):
|
||||
# After we have the noid key, can decode true payload
|
||||
assert len(noid_key) == 5
|
||||
|
||||
pk = noid_stretch(session_key, noid_key)
|
||||
|
||||
msg = aes256ctr.new(pk).cipher(body[:-2])
|
||||
chk = ngu.hash.sha256s(msg)[-2:]
|
||||
|
||||
return msg if chk == body[-2:] else None
|
||||
|
||||
|
||||
async def kt_incoming(type_code, payload):
|
||||
# incoming BBQr was scanned (via main menu, etc)
|
||||
|
||||
from pincodes import pa
|
||||
if pa.hobbled_mode and type_code != 'E':
|
||||
# only PSBT rx is supported in hobbled mode
|
||||
# fail silently, this is second check, see decoders.py
|
||||
return
|
||||
|
||||
if type_code == 'R':
|
||||
# they want to send to this guy
|
||||
return await kt_start_send(payload)
|
||||
|
||||
elif type_code == 'S':
|
||||
# we are receiving something, let's try to decode
|
||||
return await kt_decode_rx(False, payload)
|
||||
|
||||
elif type_code == 'E':
|
||||
# incoming PSBT!
|
||||
return await kt_decode_rx(True, payload)
|
||||
|
||||
else:
|
||||
raise ValueError(type_code)
|
||||
|
||||
|
||||
class SecretPickerMenu(MenuSystem):
|
||||
def __init__(self, rx_pubkey):
|
||||
self.rx_pubkey = rx_pubkey
|
||||
|
||||
# this menu should be unreachable in hobbled mode.
|
||||
from pincodes import pa
|
||||
assert not pa.hobbled_mode
|
||||
|
||||
from flow import word_based_seed, is_tmp, has_se_secrets
|
||||
has_notes = bool(NoteContentBase.count())
|
||||
has_sv = bool(settings.get('seedvault', False))
|
||||
|
||||
# Q-only feature, so menu can be W I D E
|
||||
# - in increasing order of importance & sensitivity!
|
||||
# - pinned-virgin mode is supported, so might not have any secrets to share yet,
|
||||
# but can do secret notes still
|
||||
m = [
|
||||
MenuItem('Quick Text Message', f=self.quick_note),
|
||||
MenuItem('Single Note / Password', predicate=has_notes, menu=self.pick_note_submenu),
|
||||
MenuItem('Export All Notes & Passwords', predicate=has_notes, f=self.picked_note),
|
||||
]
|
||||
|
||||
if has_sv:
|
||||
m.append( MenuItem('From Seed Vault', menu=self.pick_vault_submenu) )
|
||||
|
||||
msg = None
|
||||
if is_tmp():
|
||||
# tmp seed, or maybe bip39 is in effect
|
||||
# - share the current master secret, not the real master
|
||||
msg = 'Temp Secret (words)' if word_based_seed() else (
|
||||
'XPRV from Words+Passphrase' if bip39_passphrase else 'Temp XPRV Secret')
|
||||
elif has_se_secrets():
|
||||
# sharing real master secret
|
||||
msg = 'Master Seed Words' if word_based_seed() else 'Master XPRV'
|
||||
|
||||
if msg:
|
||||
m.append( MenuItem(msg, f=self.share_master_secret) )
|
||||
m.append( MenuItem("Full COLDCARD Backup", f=self.share_full_backup) )
|
||||
|
||||
super().__init__(m)
|
||||
|
||||
async def pick_vault_submenu(self, *a):
|
||||
# pick a secret from seed vault
|
||||
from seed import SeedVaultChooserMenu
|
||||
rec = await SeedVaultChooserMenu.pick()
|
||||
if rec:
|
||||
await kt_do_send(self.rx_pubkey, 'v', obj=list(rec))
|
||||
|
||||
async def pick_note_submenu(self, *a):
|
||||
# Make a submenu to select a single note/password
|
||||
rv = []
|
||||
for note in NoteContentBase.get_all():
|
||||
rv.append(MenuItem('%d: %s' % (note.idx+1, note.title), f=self.picked_note, arg=note))
|
||||
|
||||
return rv
|
||||
|
||||
async def quick_note(self, _, _2, item):
|
||||
# accept a text string, and send as a note
|
||||
from notes import NoteContent
|
||||
txt = await ux_input_text('', max_len=100,
|
||||
prompt='Enter your message', min_len=1, b39_complete=True, scan_ok=True,
|
||||
placeholder='Attack at dawn.')
|
||||
|
||||
if not txt: return
|
||||
|
||||
n = NoteContent(dict(title="Quick Note", misc=txt))
|
||||
await kt_do_send(self.rx_pubkey, 'n', obj=[n.serialize()])
|
||||
|
||||
async def picked_note(self, _, _2, item):
|
||||
# exporting note(s)
|
||||
|
||||
if item.arg is None:
|
||||
# export all
|
||||
body = [n.serialize() for n in NoteContentBase.get_all()]
|
||||
else:
|
||||
# single note/password
|
||||
body = [item.arg.serialize()]
|
||||
|
||||
await kt_do_send(self.rx_pubkey, 'n', obj=body)
|
||||
|
||||
async def share_full_backup(self, *a):
|
||||
# context, and warn them
|
||||
ch = await ux_show_story("Sending complete backup, including master secret, "
|
||||
"seed vault (if any), multisig wallets, notes/passwords, and all settings! "
|
||||
"The receiving "
|
||||
"COLDCARD must already have the master seed wiped to be able to install "
|
||||
"everything, otherwise only master secret and multisig are saved into a tmp seed. "
|
||||
"OK to proceed?")
|
||||
if ch != 'y': return
|
||||
|
||||
from backups import render_backup_contents
|
||||
|
||||
dis.fullscreen("Buiding Backup...")
|
||||
|
||||
# renders a text file, with rather a lot of comments; strip them
|
||||
bkup = render_backup_contents(bypass_tmp=True)
|
||||
out = []
|
||||
for ln in bkup.split('\n'):
|
||||
if not ln: continue
|
||||
if ln[0] == '#': continue
|
||||
out.append(ln)
|
||||
|
||||
await kt_do_send(self.rx_pubkey, 'b', raw=b'\n'.join(ln.encode() for ln in out))
|
||||
|
||||
async def share_master_secret(self, _, _2, item):
|
||||
# altho menu items look different we are sharing same thing:
|
||||
# - up to 72 bytes from secure elements
|
||||
|
||||
dis.fullscreen("Wait...")
|
||||
|
||||
with SensitiveValues(bypass_tmp=False, enforce_delta=True) as sv:
|
||||
raw = bytearray(sv.secret)
|
||||
xfp = xfp2str(sv.get_xfp())
|
||||
|
||||
# rtrim zeros
|
||||
while raw[-1] == 0:
|
||||
raw = raw[0:-1]
|
||||
|
||||
summary = SecretStash.summary(raw[0])
|
||||
|
||||
from pincodes import pa
|
||||
scale = 'your MASTER secret' if not pa.tmp_value else 'a temporary secret'
|
||||
|
||||
msg = "Sharing %s [%s] (%s)." % (scale, xfp, summary)
|
||||
msg += "\n\nWARNING: Allows full control over all associated Bitcoin!"
|
||||
|
||||
if not await ux_confirm(msg):
|
||||
blank_object(raw)
|
||||
return
|
||||
|
||||
await kt_do_send(self.rx_pubkey, 's', raw=raw)
|
||||
|
||||
|
||||
async def kt_send_psbt(psbt, psbt_len, psbt_offset):
|
||||
# We just finished adding our signature to an incomplete PSBT.
|
||||
# User wants to send to one or more other senders for them to complete signing.
|
||||
|
||||
# who remains to sign? look at inputs
|
||||
ms = psbt.active_multisig
|
||||
all_xfps = [x for x,*p in ms.get_xfp_paths()]
|
||||
need = [x for x in psbt.multisig_xfps_needed() if x in all_xfps]
|
||||
|
||||
# maybe it's not really a PSBT where we know the other signers? might be
|
||||
# a weird coinjoin we don't fully understand
|
||||
if not need:
|
||||
await ux_show_story("No more signers?")
|
||||
return
|
||||
|
||||
# (TXN_OUTPUT_OFFSET after signing, TXN_INPUT_OFFSET for the file-teleport path)
|
||||
with SFFile(psbt_offset, psbt_len) as fd:
|
||||
bin_psbt = fd.read(psbt_len)
|
||||
|
||||
my_xfp = settings.get('xfp')
|
||||
|
||||
# if my_xfp in need:
|
||||
# - we haven't signed yet? let's do that now .. except we've lost some of the
|
||||
# data we need such as filename to save back into.
|
||||
# - so just keep going instead... maybe they want to be last signer?
|
||||
|
||||
# Make them pick a single next signer. It's not helpful to do multiple at once
|
||||
# here, since we need signatures to be added serially so that last
|
||||
# signer can do finalization. We don't have a general purpose combiner.
|
||||
|
||||
async def done_cb(m, idx, item):
|
||||
m.next_xfp = item.arg
|
||||
the_ux.pop()
|
||||
|
||||
ci = []
|
||||
next_signer = None
|
||||
for idx, x in enumerate(all_xfps):
|
||||
txt = '[%s] Co-signer #%d' % (xfp2str(x), idx+1)
|
||||
f = done_cb
|
||||
if x == my_xfp:
|
||||
txt += ': YOU'
|
||||
f = None
|
||||
if x in need:
|
||||
# we haven't signed ourselves yet, so allow that
|
||||
from auth import sign_transaction
|
||||
|
||||
async def sign_now(*a):
|
||||
# this will reset the UX stack:
|
||||
# flags=None --> whether to finalize is decided based on psbt.is_complete
|
||||
sign_transaction(psbt_len, flags=None, input_method="kt", offset=psbt_offset)
|
||||
|
||||
f = sign_now
|
||||
|
||||
elif x not in need:
|
||||
txt += ': DONE'
|
||||
f = None
|
||||
|
||||
mi = MenuItem(txt, f=f, arg=x)
|
||||
|
||||
if x not in need:
|
||||
# show check if we've got sig
|
||||
mi.is_chosen = lambda: True
|
||||
elif next_signer is None:
|
||||
next_signer = idx
|
||||
|
||||
ci.append(mi)
|
||||
|
||||
m = MenuSystem(ci)
|
||||
m.next_xfp = None
|
||||
m.goto_idx(next_signer) # position cursor on next candidate
|
||||
the_ux.push(m)
|
||||
await m.interact()
|
||||
|
||||
if m.next_xfp:
|
||||
assert m.next_xfp != my_xfp
|
||||
ri, rx_pubkey, kp = ms.kt_make_rxkey(m.next_xfp)
|
||||
await kt_do_send(rx_pubkey, 'p', raw=bin_psbt, prefix=ri, kp=kp,
|
||||
rx_label='[%s] co-signer' % xfp2str(m.next_xfp))
|
||||
|
||||
return True, ms.M - (ms.N - len(need))
|
||||
|
||||
async def kt_send_file_psbt(*a):
|
||||
# Menu item: choose a PSBT file from SD card, and send to co-signers.
|
||||
# Heavy code re-use here. Need to find the multisig wallet associated w/ file,
|
||||
# so we need to parse it and we must be one of the co-signers.
|
||||
|
||||
from actions import is_psbt, file_picker
|
||||
from auth import sign_psbt_file, TXN_INPUT_OFFSET
|
||||
from version import MAX_TXN_LEN
|
||||
from ux import import_export_prompt
|
||||
from psbt import psbtObject
|
||||
|
||||
# choose any PSBT from SD
|
||||
picked = await import_export_prompt("PSBT", is_import=True, no_nfc=True, no_qr=True)
|
||||
if picked == KEY_CANCEL:
|
||||
return
|
||||
choices = await file_picker(suffix='.psbt', min_size=50, ux=False,
|
||||
max_size=MAX_TXN_LEN, taster=is_psbt, **picked)
|
||||
if not choices:
|
||||
# error msg already shown
|
||||
return
|
||||
|
||||
if len(choices) == 1:
|
||||
# single - skip the menu
|
||||
label,path,fn = choices[0]
|
||||
input_psbt = path + '/' + fn
|
||||
else:
|
||||
# multiples - make them pick one
|
||||
input_psbt = await file_picker(choices=choices)
|
||||
if not input_psbt:
|
||||
return
|
||||
|
||||
# read into PSRAM from wherever
|
||||
psbt_len = await sign_psbt_file(input_psbt, just_read=True, **picked)
|
||||
|
||||
dis.fullscreen("Validating...")
|
||||
try:
|
||||
dis.progress_sofar(1, 4)
|
||||
with SFFile(TXN_INPUT_OFFSET, length=psbt_len, message='Reading...') as fd:
|
||||
# NOTE: psbtObject captures the file descriptor and uses it later
|
||||
psbt = psbtObject.read_psbt(fd)
|
||||
|
||||
await psbt.validate() # might do UX: accept multisig import
|
||||
|
||||
dis.progress_sofar(2, 4)
|
||||
psbt.consider_inputs()
|
||||
dis.progress_sofar(3, 4)
|
||||
|
||||
psbt.consider_keys()
|
||||
|
||||
except Exception as exc:
|
||||
# not going to do full reporting here, use our other code for that!
|
||||
await ux_show_story("Cannot validate PSBT?\n\n"+str(exc), "PSBT Load Failed")
|
||||
return
|
||||
finally:
|
||||
dis.progress_bar_show(1)
|
||||
|
||||
if not psbt.active_multisig:
|
||||
await ux_show_story("We are not part of this multisig wallet.", "Cannot Teleport PSBT")
|
||||
return
|
||||
|
||||
await kt_send_psbt(psbt, psbt_len=psbt_len, psbt_offset=TXN_INPUT_OFFSET)
|
||||
|
||||
# EOF
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user