Compare commits
1 Commits
master
...
nvk-patch-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2dba93a38 |
21
README.md
21
README.md
@ -8,7 +8,7 @@ with the latest updates and security alerts.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Quick Links
|
## Quick Links
|
||||||
|
|
||||||
@ -28,11 +28,9 @@ has been automated using Docker. Steps are as follows:
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
git clone https://github.com/Coldcard/firmware.git
|
git clone https://github.com/Coldcard/firmware.git
|
||||||
cd firmware
|
git checkout 2023-12-21T1526-v5.2.2
|
||||||
# DOWNLOAD https://coldcard.com/downloads
|
# get a copy of that binary into ./releases/2023-12-21T1526-v5.2.2-mk4-coldcard.dfu
|
||||||
# get a copy of binary into ./releases/2026-03-05T2052-v5.5.0-mk-coldcard.dfu
|
cd firmware/stm32
|
||||||
git checkout 2026-03-05T2052-v5.5.0
|
|
||||||
cd stm32
|
|
||||||
make -f MK4-Makefile repro
|
make -f MK4-Makefile repro
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -51,15 +49,14 @@ such as Taproot or Miniscript. Our standards for releasing new Edge
|
|||||||
versions are lower, so we can iterate faster and get these advancements
|
versions are lower, so we can iterate faster and get these advancements
|
||||||
out to other developers.
|
out to other developers.
|
||||||
|
|
||||||
Q and Mk series share the same code base. Individual files that are added,
|
Q and Mk4 share the same code base. Individual files that are added,
|
||||||
or removed, can be see in differences between `shared/manifest_mk4.py`
|
or removed, can be see in differences between `shared/manifest_mk4.py`
|
||||||
and `shared/manifest_q1.py`. Common files are in `shared/manifest.py`.
|
and `shared/manifest_q1.py`. Common files are in `shared/manifest.py`.
|
||||||
Firmware built for Mk5, supports the Mk4 without any functional differences.
|
|
||||||
|
|
||||||
|
|
||||||
## Check-out and Setup
|
## Check-out and Setup
|
||||||
|
|
||||||
**NOTE** This is the `master` branch and covers the latest hardware (Mk and Q).
|
**NOTE** This is the `master` branch and covers the latest hardware (Mk4 and Q).
|
||||||
See branch `v4-legacy` for firmware which supports only Mk3/Mk2 and earlier.
|
See branch `v4-legacy` for firmware which supports only Mk3/Mk2 and earlier.
|
||||||
|
|
||||||
Do a checkout, recursively, to get all the submodules:
|
Do a checkout, recursively, to get all the submodules:
|
||||||
@ -235,8 +232,8 @@ Top-level dirs:
|
|||||||
|
|
||||||
- shared code between desktop test version and real-deal
|
- shared code between desktop test version and real-deal
|
||||||
- expected to be largely in python, and higher-level
|
- expected to be largely in python, and higher-level
|
||||||
- code exclusive to the Mk4 or Mk5 will be listed in `manifest_mk4.py`, and
|
- new code found only on the Mk4 will be listed in `manifest_mk4.py` code exclusive
|
||||||
to the Q will be listed in `manifest_q1.py`
|
to earlier hardware is in `manifest_mk3.py`
|
||||||
|
|
||||||
`unix`
|
`unix`
|
||||||
|
|
||||||
@ -268,7 +265,7 @@ Top-level dirs:
|
|||||||
`stm32/mk4-bootloader`
|
`stm32/mk4-bootloader`
|
||||||
`stm32/q1-bootloader`
|
`stm32/q1-bootloader`
|
||||||
|
|
||||||
- 128k of factory-set code that you cannot change
|
- 128k of factory-set code that you cannot change for Mk4 or Q
|
||||||
- however, you can inspect what code is on your coldcard and compare to this.
|
- however, you can inspect what code is on your coldcard and compare to this.
|
||||||
|
|
||||||
`hardware`
|
`hardware`
|
||||||
|
|||||||
@ -208,9 +208,8 @@ def readback(fname):
|
|||||||
if v & MK_2_OK: d.append('Mk2')
|
if v & MK_2_OK: d.append('Mk2')
|
||||||
if v & MK_3_OK: d.append('Mk3')
|
if v & MK_3_OK: d.append('Mk3')
|
||||||
if v & MK_4_OK: d.append('Mk4')
|
if v & MK_4_OK: d.append('Mk4')
|
||||||
if v & MK_5_OK: d.append('Mk5')
|
|
||||||
if v & MK_Q1_OK: d.append('Q1')
|
if v & MK_Q1_OK: d.append('Q1')
|
||||||
if v & ~(MK_1_OK | MK_2_OK | MK_3_OK | MK_4_OK | MK_5_OK | MK_Q1_OK):
|
if v & ~(MK_1_OK | MK_2_OK | MK_3_OK | MK_4_OK | MK_Q1_OK):
|
||||||
d.append('?other?')
|
d.append('?other?')
|
||||||
v = nv + '+'.join(d)
|
v = nv + '+'.join(d)
|
||||||
elif fld == 'timestamp':
|
elif fld == 'timestamp':
|
||||||
@ -246,7 +245,7 @@ def readback(fname):
|
|||||||
@click.option('--pubkey-num', '-k', type=int, help='Which key # to use for signing', default=0)
|
@click.option('--pubkey-num', '-k', type=int, help='Which key # to use for signing', default=0)
|
||||||
@click.option('--high_water', '-h', is_flag=True, help='Mark version as new highwater mark (no downgrades below this version)')
|
@click.option('--high_water', '-h', is_flag=True, help='Mark version as new highwater mark (no downgrades below this version)')
|
||||||
@click.option('--verbose', '-v', default=False, is_flag=True, help='Show numbers related to signature')
|
@click.option('--verbose', '-v', default=False, is_flag=True, help='Show numbers related to signature')
|
||||||
@click.option('--hw-compat', '-m', type=str, metavar='mk', help="Set HW compat field (hw_label value)")
|
@click.option('--hw-compat', '-m', type=str, metavar='Mk4', help="Set HW compat field (hw_label value)")
|
||||||
@click.option('--backdate', type=int, metavar='DAYS',
|
@click.option('--backdate', type=int, metavar='DAYS',
|
||||||
help='Make downgrade attack test version', default=0)
|
help='Make downgrade attack test version', default=0)
|
||||||
@click.option('--build_dir', '-b', default='l-port/build-COLDCARD')
|
@click.option('--build_dir', '-b', default='l-port/build-COLDCARD')
|
||||||
@ -279,9 +278,8 @@ def doit(keydir, outfn=None, build_dir=None, high_water=False,
|
|||||||
vectors = open(build_dir + '/firmware0.bin', 'rb').read()
|
vectors = open(build_dir + '/firmware0.bin', 'rb').read()
|
||||||
body = open(build_dir + '/firmware1.bin', 'rb').read()
|
body = open(build_dir + '/firmware1.bin', 'rb').read()
|
||||||
|
|
||||||
if hw_compat in { 'mk4', '4', 'mk5', '5', 'mk' }:
|
if hw_compat in { 'mk4', '4'}:
|
||||||
# Mk4 and 5 can run the same firmware, once Mk5 support was added
|
hw_compat = MK_4_OK
|
||||||
hw_compat = MK_4_OK | MK_5_OK
|
|
||||||
elif hw_compat == 'q1':
|
elif hw_compat == 'q1':
|
||||||
hw_compat = MK_Q1_OK
|
hw_compat = MK_Q1_OK
|
||||||
elif hw_compat in { 'mk3', '3'}:
|
elif hw_compat in { 'mk3', '3'}:
|
||||||
|
|||||||
@ -3,32 +3,14 @@
|
|||||||
These docs are meant for you hackers out there... but also for anyone who
|
These docs are meant for you hackers out there... but also for anyone who
|
||||||
wants to understand why it's safe to put your moneys into Coldcard.
|
wants to understand why it's safe to put your moneys into Coldcard.
|
||||||
|
|
||||||
- [`security-model.md`](security-model.md) The COLDCARD Mk4/Mk5/Q security model.
|
|
||||||
- [`pin-entry.md`](pin-entry.md) Huge and detailed discussion of PIN codes and the security element that holds the secrets.
|
- [`pin-entry.md`](pin-entry.md) Huge and detailed discussion of PIN codes and the security element that holds the secrets.
|
||||||
- [`secure-elements.md`](secure-elements.md) How the dual secure elements work together.
|
|
||||||
- [`dev-access.md`](dev-access.md) How developers can modify Coldcard to extend it.
|
- [`dev-access.md`](dev-access.md) How developers can modify Coldcard to extend it.
|
||||||
- [`memory-map.md`](memory-map.md) Memory map highlights
|
- [`memory-map.md`](memory-map.md) Memory map highlights
|
||||||
- [`notes-on-repro.md`](notes-on-repro.md) Detailed breakdown of the reproducible build process.
|
|
||||||
- [`upgrade-recovery.md`](upgrade-recovery.md) Firmware upgrade and recovery process.
|
|
||||||
- [`backup-files.md`](backup-files.md) Some details of our encrypted backup files.
|
- [`backup-files.md`](backup-files.md) Some details of our encrypted backup files.
|
||||||
- [`temporary-seeds.md`](temporary-seeds.md) Temporary (ephemeral) seeds and the Seed Vault.
|
|
||||||
- [`seed-xor.md`](seed-xor.md) More about _Seed XOR_ feature, including fully worked Seed XOR example, and useful XOR lookup chart.
|
|
||||||
- [`key-teleport.md`](key-teleport.md) Key Teleport: encrypted transfer of seeds and secrets between Q devices.
|
|
||||||
- [`spending-policy.md`](spending-policy.md) Spending policy: autonomous signing with configurable limits.
|
|
||||||
- [`microsd-2fa.md`](microsd-2fa.md) Using a MicroSD card as a second factor for login.
|
|
||||||
- [`web2fa.md`](web2fa.md) Web 2FA authentication.
|
|
||||||
- [`bip85-passwords.md`](bip85-passwords.md) Deriving deterministic passwords via BIP-85.
|
|
||||||
- [`msg-signing.md`](msg-signing.md) COLDCARD message signing.
|
|
||||||
- [`proof-of-reserves-bip-322.md`](proof-of-reserves-bip-322.md) BIP-322 generic signed message format and proof of reserves.
|
|
||||||
- [`generic-wallet-export.md`](generic-wallet-export.md) Generic JSON wallet export file format.
|
|
||||||
- [`bip-21-extensions.md`](bip-21-extensions.md) Coldcard's BIP-21 URI extensions, including multisig ownership address check.
|
|
||||||
- [`nfc-coldcard.md`](nfc-coldcard.md) NFC support on Coldcard Mk4 and Q.
|
|
||||||
- [`nfc-pushtx.md`](nfc-pushtx.md) NFC Push Transaction: broadcast a signed transaction via your phone.
|
|
||||||
- [`usb-batteries.md`](usb-batteries.md) Using USB battery packs with Coldcard.
|
|
||||||
- [`electrum-usage.md`](electrum-usage.md) Importing seed words into Electrum for funds usage (and other tips).
|
- [`electrum-usage.md`](electrum-usage.md) Importing seed words into Electrum for funds usage (and other tips).
|
||||||
- [`bitcoin-core-usage.md`](bitcoin-core-usage.md) How to use with Bitcoin Core.
|
- [`bitcoin-core-usage.md`](bitcoin-core-usage.md) How to use with Bitcoin Core.
|
||||||
- [`bitcoin-core2of2desc.md`](bitcoin-core2of2desc.md) Airgapped 2-of-2 multisig with Bitcoin Core using descriptors.
|
|
||||||
- [`limitations.md`](limitations.md) Documented limitations, policy choices, and TODO items.
|
- [`limitations.md`](limitations.md) Documented limitations, policy choices, and TODO items.
|
||||||
- [`paperwallet.pdf`](paperwallet.pdf) Example paper wallet template file.
|
- [`paperwallet.pdf`](paperwallet.pdf) Example paper wallet template file.
|
||||||
|
- [`seed-xor.md`](seed-xor.md) More about _Seed XOR_ feature, including fully worked Seed XOR example, and useful XOR lookup chart.
|
||||||
- [`menu-tree.txt`](menu-tree.txt) Dump of the menu system. Incomplete, may be out of date.
|
- [`menu-tree.txt`](menu-tree.txt) Dump of the menu system. Incomplete, may be out of date.
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
## 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,
|
Generated passwords can be sent as keystrokes via USB to the host computer,
|
||||||
effectively using Coldcard as specialized password manager.
|
effectively using Coldcard as specialized password manager.
|
||||||
|
|
||||||
In addition to deriving up to 10,000 distinct secure passwords, the Coldcard
|
In addition to deriving up to 10,000 distinct secure passwords, the Coldcard Mk4
|
||||||
can also type them into a computer by emulating a USB keyboard, and simulating the
|
can also type them into a computer by emulating a USB keyboard, and simulating the
|
||||||
keystrokes needed to type the password.
|
keystrokes needed to type the password.
|
||||||
|
|
||||||
#### Requirements
|
#### Requirements
|
||||||
|
|
||||||
* Coldcard Mk4 or Mk5 (firmware 5.0.5 or newer), or any Q
|
* Coldcard Mk4 with version 5.0.5 or newer
|
||||||
* USB-C with data link (won't work with power only cable from Coinkite)
|
* USB-C with data link (won't work with power only cable from Coinkite)
|
||||||
|
|
||||||
## Type Passwords over USB
|
## Type Passwords over USB
|
||||||
@ -32,13 +32,11 @@ to exit. Exiting from "Type Passwords" will cause Coldcard to turn off keyboard
|
|||||||
1. Go to Advanced/Tools -> Derive Seed B85 -> Passwords
|
1. Go to Advanced/Tools -> Derive Seed B85 -> Passwords
|
||||||
2. Choose "Password/Index number" (BIP-85 index) and press OK to generate password.
|
2. Choose "Password/Index number" (BIP-85 index) and press OK to generate password.
|
||||||
3. Screen shows generated password, path, and entropy from which password was derived
|
3. Screen shows generated password, path, and entropy from which password was derived
|
||||||
4. A few different options are available at this point (on Mk; on Q the NFC and
|
4. A few different options are available at this point:
|
||||||
QR buttons are used instead of (3)/(4)):
|
1. press 1 to save password backup file on MicroSD card (cleartext!)
|
||||||
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)
|
||||||
2. press (2) to save to Virtual Disk (only when available)
|
3. press 3 to view password as QR code
|
||||||
3. press (3) to send over NFC (only appears when NFC is enabled)
|
4. press 4 to send over NFC (only appears when NFC is enabled)
|
||||||
4. press (4) to view password as QR code
|
|
||||||
5. press (6) to send keystrokes over USB (this enables keyboard emulation, sends keystrokes + enter, then disables keyboard emulation)
|
|
||||||
|
|
||||||
## Keyboard language settings
|
## Keyboard language settings
|
||||||
|
|
||||||
|
|||||||
@ -5,12 +5,9 @@ wallet systems, but we also have a file format for general purpose
|
|||||||
exports, which we hope future wallet makers will leverage.
|
exports, which we hope future wallet makers will leverage.
|
||||||
|
|
||||||
It contains master XPUB, XFP for that, and derived values for the top hardened
|
It contains master XPUB, XFP for that, and derived values for the top hardened
|
||||||
position of the single-signature schemes BIP44, BIP49 and BIP84, plus the
|
position of BIP44, BIP84 and BIP49.
|
||||||
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/Tools > Export Wallet > Generic JSON_
|
The feature can be found here: _Advanced > MicroSD > Export Wallet > Generic JSON_
|
||||||
|
|
||||||
Please contact us (or better yet, make a pull request), if you need something
|
Please contact us (or better yet, make a pull request), if you need something
|
||||||
more in this file.
|
more in this file.
|
||||||
@ -21,51 +18,32 @@ Here is an example, produced by the Simulator for account number 123.
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
"chain": "BTC",
|
"chain": "XTN",
|
||||||
"xfp": "0F056943",
|
"xfp": "0F056943",
|
||||||
|
"xpub": "tpubD6NzVbkrYhZ4XzL5Dhayo67Gorv1YMS7j8pRUvVMd5odC2LBPLAygka9p7748JtSq82FNGPppFEz5xxZUdasBRCqJqXvUHq6xpnsMcYJzeh",
|
||||||
"account": 123,
|
"account": 123,
|
||||||
"xpub": "xpub661MyMwAqRbcGC9DmWbtbAmuUjpMYxw4BWE88NSDHB3jSjfUK7KtYJuKa52GbowD3DVLkgsxH9QwPnTx5mjdHykYFEncnmAsNsCTbWzBhA7",
|
|
||||||
"bip44": {
|
"bip44": {
|
||||||
|
"deriv": "m/44'/1'/123'",
|
||||||
|
"first": "n44vs1Rv7T8SANrg2PFGQhzVkhr5Q6jMMD",
|
||||||
"name": "p2pkh",
|
"name": "p2pkh",
|
||||||
"xfp": "5F898064",
|
"xfp": "B7908B26",
|
||||||
"deriv": "m/44h/0h/123h",
|
"xpub": "tpubDCiHGUNYdRRGoSH22j8YnruUKgguCK1CC2NFQUf9PApeZh8ewAJJWGMUrhggDNK73iCTanWXv1RN5FYemUH8UrVUBjqDb8WF2VoKmDh9UTo"
|
||||||
"xpub": "xpub6DStQXfAgHuLbMpCf86ruVkF4yT9pSLyWsFiqQTWY9osuinq8Dyee4W5jCjMfyku5LNkRB9oFinrY5ufn9XXEn8Vvzc2jnifKMaQCNV7RBZ",
|
|
||||||
"desc": "pkh([0f056943/44h/0h/123h]xpub6DStQXfAgHuLbMpCf86ruVkF4yT9pSLyWsFiqQTWY9osuinq8Dyee4W5jCjMfyku5LNkRB9oFinrY5ufn9XXEn8Vvzc2jnifKMaQCNV7RBZ/<0;1>/*)#4tl8jryn",
|
|
||||||
"first": "1GTNtzG5xX2UhdD5e3Nu7i1WPxFdjxQMJt"
|
|
||||||
},
|
},
|
||||||
"bip49": {
|
"bip49": {
|
||||||
"name": "p2sh-p2wpkh",
|
"_pub": "upub5DMRSsh6mNak9KbcVjJ7xAgHJvbE3Nx22CBTier5C35kv8j7g2q58ywxskBe6JCcAE2VH86CE2aL4MifJyKbRw8Gj9ay7SWvUBkp2DJ7y52",
|
||||||
"xfp": "A748B1FC",
|
"deriv": "m/49'/1'/123'",
|
||||||
"deriv": "m/49h/0h/123h",
|
"first": "2N87V39riUUCd4vmXfDjMWAu9gUCiBji5jB",
|
||||||
"xpub": "xpub6DDm8WzH5a9qjKkttzqSB3uGofNohU9D3n3UG8WMxkUZzJEMPTYiQRf1dvTFCQR82MjGW4LUMVuTtnW4hF17RpzCqVwhf6Z2fnJPWtjG164",
|
"name": "p2wpkh-p2sh",
|
||||||
"desc": "sh(wpkh([0f056943/49h/0h/123h]xpub6DDm8WzH5a9qjKkttzqSB3uGofNohU9D3n3UG8WMxkUZzJEMPTYiQRf1dvTFCQR82MjGW4LUMVuTtnW4hF17RpzCqVwhf6Z2fnJPWtjG164/<0;1>/*))#5j7t2n2u",
|
"xfp": "CEE1D809",
|
||||||
"_pub": "ypub6Y42SBfCEFhKacx1jMd4P8zmydXFe68hxtZh3XQFLkrT3Q3ae7iH2VK9f8QqCK53Rzr5FXw2pAG1n57dQwR8E4fohqe8F1NWwWN2uVRfBry",
|
"xpub": "tpubDCDqt7XXvhAdy1MpSze5nMJA9x8DrdRaKALRRPasfxyHpiqWWEAr9cbDBQ9BcX7cB3up98Pk97U2QQ3xrvQsi5dNPmRYYhdcsKY9wwEY87T"
|
||||||
"first": "3CeBRbJKCpg7BpJME2vM8ZxhCjBnhG4toy"
|
|
||||||
},
|
},
|
||||||
"bip84": {
|
"bip84": {
|
||||||
|
"_pub": "vpub5Y5a91QvDT45EnXQaKeuvJupVvX8f9BiywDcadSTtaeJ1VgJPPXMitnYsqd9k7GnEqh44FKJ5McJfu6KrihFXhAmvSWgm7BAVVK8Gupu4fL",
|
||||||
|
"deriv": "m/84'/1'/123'",
|
||||||
|
"first": "tb1qc58ys2dphtphg6yuugdf3d0kufmk0tye044g3l",
|
||||||
"name": "p2wpkh",
|
"name": "p2wpkh",
|
||||||
"xfp": "2C5207AA",
|
"xfp": "78CF94E5",
|
||||||
"deriv": "m/84h/0h/123h",
|
"xpub": "tpubDC7jGaaSE66VDB6VhEDFYQSCAyugXmfnMnrMVyHNzW9wryyTxvha7TmfAHd7GRXrr2TaAn2HXn9T8ep4gyNX1bzGiieqcTUNcu2poyntrET"
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -73,23 +51,16 @@ Here is an example, produced by the Simulator for account number 123.
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
1. The `first` address is formed by added `/0/0` onto the given derivation, and is assumed
|
1. The `first` address is formed by added `/0/0` onto the given derivation, and is assumed
|
||||||
to be the first (non-change) receive address for the wallet. It is only present on the
|
to be the first (non-change) receive address for the wallet.
|
||||||
single-signature sections (`bip44`, `bip49`, `bip84`); multisig sections omit it.
|
|
||||||
|
|
||||||
1a. Each section includes a `desc` field: a ready-to-import Bitcoin output descriptor
|
|
||||||
(with `#checksum`). Single-sig descriptors use the `<0;1>/*` multipath form. Multisig
|
|
||||||
sections (`bip48_1`, `bip48_2`, and `bip45` when present) emit a `sortedmulti(...)`
|
|
||||||
template with `M` and a trailing `...` as placeholders, to be completed with your
|
|
||||||
threshold and the other co-signers' keys.
|
|
||||||
|
|
||||||
2. The user may specify any value (up to 9999) for the account number, and it's meant to
|
2. The user may specify any value (up to 9999) for the account number, and it's meant to
|
||||||
segregate funds into sub-wallets. Don't assume it's zero.
|
segregate funds into sub-wallets. Don't assume it's zero.
|
||||||
|
|
||||||
3. When making your PSBT files to spend these amounts, remember that the XFP of the master
|
3. When making your PSBT files to spend these amounts, remember that the XFP of the master
|
||||||
(`0F056943` in this example) is the root of the subkey paths found in the file, and
|
(`0F056943` in this example) is is the root of the subkey paths found in the file, and
|
||||||
you must include the full derivation path from master. So based on this example,
|
you must include the full derivation path from master. So based on this example,
|
||||||
to spend a UTXO on `bc1qhj6avwmp5lhpgqwm6dgxrf3v5lf67rjm99a8an`, the input section
|
to spend a UTXO on `tb1qc58ys2dphtphg6yuugdf3d0kufmk0tye044g3l`, the input section
|
||||||
of your PSBT would need to specify `(m=0F056943)/84'/0'/123'/0/0`.
|
of your PSBT would need to specify `(m=0F056943)/84'/1'/123'/0/0`.
|
||||||
|
|
||||||
4. The `_pub` value is the [SLIP-132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) style "ypub/zpub/etc" which some systems might want. It implies
|
4. The `_pub` value is the [SLIP-132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) style "ypub/zpub/etc" which some systems might want. It implies
|
||||||
a specific address format.
|
a specific address format.
|
||||||
|
|||||||
@ -14,12 +14,11 @@
|
|||||||
# PIN Codes
|
# PIN Codes
|
||||||
|
|
||||||
- 2-2 through 6-6 in size, numeric digits only
|
- 2-2 through 6-6 in size, numeric digits only
|
||||||
- pin code 999999-999999 was reserved (meaning 'clear pin'), but now available again
|
- pin code 999999-999999 is reserved (means 'clear pin')
|
||||||
|
|
||||||
# Backup Files
|
# Backup Files
|
||||||
|
|
||||||
- we don't know what day it is, so meta data on files will not have correct date/time
|
- we don't know what day it is, so meta data on files will not have correct date/time
|
||||||
- release date of the firmware version that made the file is used instead of true date
|
|
||||||
- encrypted files produced cannot be changed, and we don't support other tools making them
|
- encrypted files produced cannot be changed, and we don't support other tools making them
|
||||||
|
|
||||||
# Micro SD
|
# Micro SD
|
||||||
@ -66,8 +65,7 @@
|
|||||||
that to the user for approval.
|
that to the user for approval.
|
||||||
- during USB "show address" for multisig, we limit subkey paths to
|
- during USB "show address" for multisig, we limit subkey paths to
|
||||||
16 levels deep (including master fingerprint)
|
16 levels deep (including master fingerprint)
|
||||||
- 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).
|
- 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)
|
||||||
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
|
- (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
|
- only a single multisig wallet can be involved in a PSBT; can't sign inputs from two different
|
||||||
multisig wallets at the same time.
|
multisig wallets at the same time.
|
||||||
@ -79,7 +77,6 @@
|
|||||||
- multisig wallet `name` can only contain printable ASCII characters `range(32, 127)`
|
- multisig wallet `name` can only contain printable ASCII characters `range(32, 127)`
|
||||||
|
|
||||||
### BIP-67
|
### BIP-67
|
||||||
|
|
||||||
- importing multisig from PSBT can ONLY create `sortedmulti(...)` multisig according to BIP-67, DO NOT use with `multi(...)`
|
- importing multisig from PSBT can ONLY create `sortedmulti(...)` multisig according to BIP-67, DO NOT use with `multi(...)`
|
||||||
- creating airgapped multisig using COLDCARD as coordinator always produces `sortedmulti(...)` multisig according to BIP-67
|
- creating airgapped multisig using COLDCARD as coordinator always produces `sortedmulti(...)` multisig according to BIP-67
|
||||||
- COLDCARD import/export [format](https://coldcard.com/docs/multisig/#configuration-text-file-for-multisig) only supports `sortedmulti(...)` multisig according to BIP-67. To import multisig wallet with `multi(...)` use descriptor import [format](https://github.com/bitcoin/bips/blob/master/bip-0383.mediawiki)
|
- COLDCARD import/export [format](https://coldcard.com/docs/multisig/#configuration-text-file-for-multisig) only supports `sortedmulti(...)` multisig according to BIP-67. To import multisig wallet with `multi(...)` use descriptor import [format](https://github.com/bitcoin/bips/blob/master/bip-0383.mediawiki)
|
||||||
@ -140,10 +137,6 @@ We will summarize transaction outputs as "change" back into same wallet, however
|
|||||||
|
|
||||||
- key derivatation paths must be 12 or less in depth (`MAX_PATH_DEPTH`)
|
- key derivatation paths must be 12 or less in depth (`MAX_PATH_DEPTH`)
|
||||||
|
|
||||||
# Pay-to-Pubkey
|
|
||||||
|
|
||||||
- although we have some code for "pay to pubkey" (P2PK not P2PKH), it is untested
|
|
||||||
and unused since this style of payment address is obsolete and largely unused today
|
|
||||||
|
|
||||||
# NFC Feature
|
# NFC Feature
|
||||||
|
|
||||||
@ -208,9 +201,9 @@ We will summarize transaction outputs as "change" back into same wallet, however
|
|||||||
- if you have an XFP collision between multiple wallets in SeedVault (ie. two wallets
|
- if you have an XFP collision between multiple wallets in SeedVault (ie. two wallets
|
||||||
with same descriptors, but different seeds) you will get false negatives
|
with same descriptors, but different seeds) you will get false negatives
|
||||||
|
|
||||||
# Spending Policy
|
# CCC Feature (ColdCard Cosigning)
|
||||||
|
|
||||||
- (Cosign mode) only 12 or 24 word seeds (not XPRV) are accepted for "key C"
|
- only 12 or 24 word seeds (not XPRV) are accepted for "key C"
|
||||||
- velocity limit:
|
- velocity limit:
|
||||||
- based on a max magnitude per txn, and a required minimum block height
|
- based on a max magnitude per txn, and a required minimum block height
|
||||||
gap, based on previous `nLockTime` value in last-signed PSBT.
|
gap, based on previous `nLockTime` value in last-signed PSBT.
|
||||||
@ -219,5 +212,5 @@ We will summarize transaction outputs as "change" back into same wallet, however
|
|||||||
- PSBT creator must put in `nLockTime` block heights (most already do to avoid fee sniping)
|
- PSBT creator must put in `nLockTime` block heights (most already do to avoid fee sniping)
|
||||||
- maximum of 25 whitelisted addresses can be stored
|
- maximum of 25 whitelisted addresses can be stored
|
||||||
- Web2FA: any number of mobile devices can be enrolled, but all will have the same shared secret
|
- Web2FA: any number of mobile devices can be enrolled, but all will have the same shared secret
|
||||||
- any warning from the PSBT, such as huge fees, will cause the transaction to be rejected
|
- any warning from the PSBT, such as huge fees, will prevent CCC cosign.
|
||||||
|
|
||||||
|
|||||||
@ -38,7 +38,7 @@ directly from python programs.
|
|||||||
|
|
||||||
| Start | Size | Notes
|
| Start | Size | Notes
|
||||||
|---------------|-----------|--------------------------
|
|---------------|-----------|--------------------------
|
||||||
| 0x0800 0000 | 112k | Bootloader code, including reset vector. See `stm32/mk4-bootloader`
|
| 0x0800 0000 | 128k | Bootloader code, including reset vector. See `stm32/mk4-bootloader`
|
||||||
| 0x0801 c000 | 8k | Sensitive "pairing secrets" for SE1 and SE2
|
| 0x0801 c000 | 8k | Sensitive "pairing secrets" for SE1 and SE2
|
||||||
| 0x0801 e000 | 8k | MCU keys, consumable; 256 32-bit write-once slots.
|
| 0x0801 e000 | 8k | MCU keys, consumable; 256 32-bit write-once slots.
|
||||||
| 0x0802 0000 | 16k | Interrupt handlers, file header (Micropython and Coldcard code)
|
| 0x0802 0000 | 16k | Interrupt handlers, file header (Micropython and Coldcard code)
|
||||||
|
|||||||
@ -30,7 +30,6 @@
|
|||||||
Tapsigner Backup
|
Tapsigner Backup
|
||||||
Seed XOR
|
Seed XOR
|
||||||
Migrate Coldcard
|
Migrate Coldcard
|
||||||
Key Teleport (start)
|
|
||||||
Help
|
Help
|
||||||
Advanced/Tools
|
Advanced/Tools
|
||||||
View Identity
|
View Identity
|
||||||
@ -49,13 +48,13 @@
|
|||||||
Import XPRV
|
Import XPRV
|
||||||
Tapsigner Backup
|
Tapsigner Backup
|
||||||
Coldcard Backup
|
Coldcard Backup
|
||||||
Restore Seed XOR
|
|
||||||
Upgrade Firmware [IF NOT TMP SEED]
|
Upgrade Firmware [IF NOT TMP SEED]
|
||||||
Show Version
|
Show Version
|
||||||
From MicroSD
|
From MicroSD
|
||||||
From VirtDisk [IF VIRTDISK ENABLED]
|
From VirtDisk [IF VIRTDISK ENABLED]
|
||||||
File Management
|
File Management
|
||||||
Verify Backup
|
Verify Backup
|
||||||
|
Teleport Multisig PSBT [IF QR AND SECRET]
|
||||||
List Files
|
List Files
|
||||||
Verify Sig File
|
Verify Sig File
|
||||||
NFC File Share [IF NFC ENABLED]
|
NFC File Share [IF NFC ENABLED]
|
||||||
@ -158,25 +157,29 @@
|
|||||||
Delete PSBTs
|
Delete PSBTs
|
||||||
Default Keep
|
Default Keep
|
||||||
Delete PSBTs
|
Delete PSBTs
|
||||||
Buried Settings
|
Menu Wrapping
|
||||||
Home Menu XFP [IF SECRET AND NOT TMP SEED]
|
Default Off
|
||||||
Only Tmp
|
Enable
|
||||||
Always Show
|
Home Menu XFP [IF SECRET AND NOT TMP SEED]
|
||||||
Menu Wrapping
|
Only Tmp
|
||||||
Default
|
Always Show
|
||||||
Always Wrap
|
|
||||||
[QR key shortcut] [IF QR SCANNER]
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[NORMAL OPERATION]
|
[NORMAL OPERATION]
|
||||||
Ready To Sign
|
Ready To Sign
|
||||||
Passphrase [IF WORD BASED SEED]
|
Passphrase [IF WORD BASED SEED]
|
||||||
Restore Saved
|
Restore Saved [MAYBE]
|
||||||
c*******
|
A***********
|
||||||
[3A14F788]
|
[0C52BAD4]
|
||||||
Restore
|
Restore
|
||||||
Delete
|
Delete
|
||||||
Edit Phrase
|
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]
|
||||||
Scan Any QR Code [IF QR SCANNER]
|
Scan Any QR Code [IF QR SCANNER]
|
||||||
Start HSM Mode [IF HSM POLICY]
|
Start HSM Mode [IF HSM POLICY]
|
||||||
Address Explorer
|
Address Explorer
|
||||||
@ -194,44 +197,35 @@
|
|||||||
Account Number
|
Account Number
|
||||||
Custom Path
|
Custom Path
|
||||||
CC-2-of-4
|
CC-2-of-4
|
||||||
Secure Notes & Passwords [IF ENBALED] [MAYBE]
|
Secure Notes & Passwords [IF ENBALED]
|
||||||
1: note0
|
1: note1
|
||||||
"note0"
|
"note1"
|
||||||
View Note
|
View Note
|
||||||
Edit
|
Edit
|
||||||
Delete
|
Delete
|
||||||
Export
|
Export
|
||||||
Sign Note Text
|
SHORTCUT
|
||||||
2: secret-PWD
|
SHORTCUT
|
||||||
"secret-PWD"
|
2: nostr
|
||||||
↳ satoshi
|
"nostr"
|
||||||
↳ abc.org
|
↳ scg
|
||||||
|
↳ brb.io
|
||||||
View Password
|
View Password
|
||||||
Send Password [MAYBE]
|
Send Password [MAYBE]
|
||||||
Export
|
Export
|
||||||
Edit Metadata
|
Edit Metadata
|
||||||
Delete
|
Delete
|
||||||
Change Password
|
Change Password
|
||||||
Sign Note Text
|
SHORTCUT
|
||||||
|
SHORTCUT
|
||||||
New Note
|
New Note
|
||||||
New Password
|
New Password
|
||||||
Export All
|
Export All
|
||||||
Sort By Title
|
|
||||||
Import
|
Import
|
||||||
Type Passwords [MAYBE]
|
Type Passwords [MAYBE]
|
||||||
Seed Vault [MAYBE]
|
Seed Vault [MAYBE]
|
||||||
1: [7126EB3C]
|
1: [B14E9AE0]
|
||||||
[7126EB3C]
|
[B14E9AE0]
|
||||||
Use This Seed
|
|
||||||
Rename
|
|
||||||
Delete
|
|
||||||
2: [CCEE13B9]
|
|
||||||
[CCEE13B9]
|
|
||||||
Use This Seed
|
|
||||||
Rename
|
|
||||||
Delete
|
|
||||||
3: [03EE9989]
|
|
||||||
[03EE9989]
|
|
||||||
Use This Seed
|
Use This Seed
|
||||||
Rename
|
Rename
|
||||||
Delete
|
Delete
|
||||||
@ -242,19 +236,17 @@
|
|||||||
Restore Backup
|
Restore Backup
|
||||||
Clone Coldcard
|
Clone Coldcard
|
||||||
Export Wallet
|
Export Wallet
|
||||||
Sparrow
|
|
||||||
Cove
|
|
||||||
Bitcoin Core
|
Bitcoin Core
|
||||||
Nunchuk
|
|
||||||
Bull Bitcoin
|
|
||||||
Blue Wallet
|
|
||||||
Electrum Wallet
|
|
||||||
Wasabi Wallet
|
|
||||||
Fully Noded
|
Fully Noded
|
||||||
Unchained
|
Sparrow Wallet
|
||||||
|
Nunchuk
|
||||||
|
Zeus
|
||||||
|
Electrum Wallet
|
||||||
Theya
|
Theya
|
||||||
Bitcoin Safe
|
Bitcoin Safe
|
||||||
Zeus
|
Wasabi Wallet
|
||||||
|
Unchained
|
||||||
|
Lily Wallet
|
||||||
Samourai Postmix
|
Samourai Postmix
|
||||||
Samourai Premix
|
Samourai Premix
|
||||||
Descriptor
|
Descriptor
|
||||||
@ -265,7 +257,6 @@
|
|||||||
P2WPKH/P2SH (BIP-49)
|
P2WPKH/P2SH (BIP-49)
|
||||||
Master XPUB
|
Master XPUB
|
||||||
Current XFP
|
Current XFP
|
||||||
Key Expression
|
|
||||||
Dump Summary
|
Dump Summary
|
||||||
Upgrade Firmware [IF NOT TMP SEED]
|
Upgrade Firmware [IF NOT TMP SEED]
|
||||||
Show Version
|
Show Version
|
||||||
@ -275,19 +266,17 @@
|
|||||||
Verify Backup
|
Verify Backup
|
||||||
Backup System
|
Backup System
|
||||||
Export Wallet
|
Export Wallet
|
||||||
Sparrow
|
|
||||||
Cove
|
|
||||||
Bitcoin Core
|
Bitcoin Core
|
||||||
Nunchuk
|
|
||||||
Bull Bitcoin
|
|
||||||
Blue Wallet
|
|
||||||
Electrum Wallet
|
|
||||||
Wasabi Wallet
|
|
||||||
Fully Noded
|
Fully Noded
|
||||||
Unchained
|
Sparrow Wallet
|
||||||
|
Nunchuk
|
||||||
|
Zeus
|
||||||
|
Electrum Wallet
|
||||||
Theya
|
Theya
|
||||||
Bitcoin Safe
|
Bitcoin Safe
|
||||||
Zeus
|
Wasabi Wallet
|
||||||
|
Unchained
|
||||||
|
Lily Wallet
|
||||||
Samourai Postmix
|
Samourai Postmix
|
||||||
Samourai Premix
|
Samourai Premix
|
||||||
Descriptor
|
Descriptor
|
||||||
@ -298,11 +287,10 @@
|
|||||||
P2WPKH/P2SH (BIP-49)
|
P2WPKH/P2SH (BIP-49)
|
||||||
Master XPUB
|
Master XPUB
|
||||||
Current XFP
|
Current XFP
|
||||||
Key Expression
|
|
||||||
Dump Summary
|
Dump Summary
|
||||||
Sign Text File
|
Sign Text File
|
||||||
Batch Sign PSBT
|
Batch Sign PSBT
|
||||||
Teleport Multisig PSBT
|
Teleport Multisig PSBT [IF QR AND SECRET]
|
||||||
List Files
|
List Files
|
||||||
Verify Sig File
|
Verify Sig File
|
||||||
NFC File Share [IF NFC ENABLED]
|
NFC File Share [IF NFC ENABLED]
|
||||||
@ -312,28 +300,29 @@
|
|||||||
Format SD Card
|
Format SD Card
|
||||||
Format RAM Disk [IF VIRTDISK ENABLED]
|
Format RAM Disk [IF VIRTDISK ENABLED]
|
||||||
Secure Notes & Passwords [IF QWERTY KEYBOARD]
|
Secure Notes & Passwords [IF QWERTY KEYBOARD]
|
||||||
1: note0
|
1: note1
|
||||||
"note0"
|
"note1"
|
||||||
View Note
|
View Note
|
||||||
Edit
|
Edit
|
||||||
Delete
|
Delete
|
||||||
Export
|
Export
|
||||||
Sign Note Text
|
SHORTCUT
|
||||||
2: secret-PWD
|
SHORTCUT
|
||||||
"secret-PWD"
|
2: nostr
|
||||||
↳ satoshi
|
"nostr"
|
||||||
↳ abc.org
|
↳ scg
|
||||||
|
↳ brb.io
|
||||||
View Password
|
View Password
|
||||||
Send Password [MAYBE]
|
Send Password [MAYBE]
|
||||||
Export
|
Export
|
||||||
Edit Metadata
|
Edit Metadata
|
||||||
Delete
|
Delete
|
||||||
Change Password
|
Change Password
|
||||||
Sign Note Text
|
SHORTCUT
|
||||||
|
SHORTCUT
|
||||||
New Note
|
New Note
|
||||||
New Password
|
New Password
|
||||||
Export All
|
Export All
|
||||||
Sort By Title
|
|
||||||
Import
|
Import
|
||||||
Derive Seeds (BIP-85)
|
Derive Seeds (BIP-85)
|
||||||
View Identity
|
View Identity
|
||||||
@ -352,17 +341,14 @@
|
|||||||
Import XPRV
|
Import XPRV
|
||||||
Tapsigner Backup
|
Tapsigner Backup
|
||||||
Coldcard Backup
|
Coldcard Backup
|
||||||
Restore Seed XOR
|
|
||||||
Key Teleport (start)
|
Key Teleport (start)
|
||||||
Spending Policy [IF SECRET AND NOT TMP SEED]
|
|
||||||
Single-Signer [IF SECRET AND NOT TMP SEED]
|
|
||||||
Co-Sign Multisig (CCC) [IF NOT TMP SEED]
|
|
||||||
HSM Mode [IF HSM AND SECRET]
|
|
||||||
Default Off
|
|
||||||
Enable
|
|
||||||
User Management [MAYBE]
|
|
||||||
Paper Wallets
|
Paper Wallets
|
||||||
WIF Store
|
Enable HSM [IF HSM AND SECRET]
|
||||||
|
Default Off
|
||||||
|
Enable
|
||||||
|
Coldcard Co-Signing [IF NOT TMP SEED]
|
||||||
|
User Management [IF HSM AND SECRET]
|
||||||
|
(no users yet)
|
||||||
NFC Tools [IF NFC ENABLED]
|
NFC Tools [IF NFC ENABLED]
|
||||||
Sign PSBT
|
Sign PSBT
|
||||||
Show Address
|
Show Address
|
||||||
@ -371,7 +357,7 @@
|
|||||||
Verify Address
|
Verify Address
|
||||||
File Share
|
File Share
|
||||||
Import Multisig
|
Import Multisig
|
||||||
Push Transaction [IF PUSHTX ENABLED]
|
Push Transaction [IF ENBALED]
|
||||||
Danger Zone
|
Danger Zone
|
||||||
Debug Functions
|
Debug Functions
|
||||||
Seed Functions
|
Seed Functions
|
||||||
@ -412,33 +398,22 @@
|
|||||||
Settings Space
|
Settings Space
|
||||||
MCU Key Slots
|
MCU Key Slots
|
||||||
Bless Firmware
|
Bless Firmware
|
||||||
|
Reflash GPU [IF QWERTY KEYBOARD]
|
||||||
Wipe LFS
|
Wipe LFS
|
||||||
Nuke Device
|
|
||||||
Settings
|
Settings
|
||||||
Login Settings
|
Login Settings
|
||||||
Change Main PIN
|
Change Main PIN
|
||||||
Trick PINs [IF SECRET AND NOT TMP SEED]
|
Trick PINs [IF SECRET AND NOT TMP SEED]
|
||||||
Trick PINs:
|
Trick PINs:
|
||||||
↳11-11
|
↳123-254
|
||||||
PIN 11-11
|
PIN 123-254
|
||||||
↳Bricks CC
|
|
||||||
Hide Trick
|
|
||||||
Delete Trick
|
|
||||||
Change PIN
|
|
||||||
↳333-3334
|
|
||||||
PIN 333-3334
|
|
||||||
↳Duress Wallet
|
↳Duress Wallet
|
||||||
Activate Wallet
|
Activate Wallet
|
||||||
Hide Trick
|
Hide Trick
|
||||||
Delete Trick
|
Delete Trick
|
||||||
Change PIN
|
Change PIN
|
||||||
↳WRONG PIN
|
|
||||||
After 3 wrong:
|
|
||||||
↳Wipes seed
|
|
||||||
↳Reboots
|
|
||||||
Hide Trick
|
|
||||||
Delete Trick
|
|
||||||
Add New Trick
|
Add New Trick
|
||||||
|
Add If Wrong
|
||||||
Delete All
|
Delete All
|
||||||
Set Nickname
|
Set Nickname
|
||||||
Scramble Keys
|
Scramble Keys
|
||||||
@ -483,12 +458,14 @@
|
|||||||
View Details
|
View Details
|
||||||
Delete
|
Delete
|
||||||
Coldcard Export
|
Coldcard Export
|
||||||
Electrum Wallet
|
|
||||||
Descriptors
|
Descriptors
|
||||||
View Descriptor
|
View Descriptor
|
||||||
Export
|
Export
|
||||||
Bitcoin Core
|
Bitcoin Core
|
||||||
Import
|
Electrum Wallet
|
||||||
|
Import from File
|
||||||
|
Import from QR [IF QR SCANNER]
|
||||||
|
Import via NFC [IF NFC ENABLED]
|
||||||
Export XPUB
|
Export XPUB
|
||||||
Create Airgapped
|
Create Airgapped
|
||||||
Trust PSBT?
|
Trust PSBT?
|
||||||
@ -543,18 +520,17 @@
|
|||||||
Delete PSBTs
|
Delete PSBTs
|
||||||
Default Keep
|
Default Keep
|
||||||
Delete PSBTs
|
Delete PSBTs
|
||||||
|
Menu Wrapping
|
||||||
|
Default Off
|
||||||
|
Enable
|
||||||
|
Home Menu XFP [IF SECRET AND NOT TMP SEED]
|
||||||
|
Only Tmp
|
||||||
|
Always Show
|
||||||
Keyboard EMU
|
Keyboard EMU
|
||||||
Default Off
|
Default Off
|
||||||
Enable
|
Enable
|
||||||
Buried Settings
|
|
||||||
Home Menu XFP [IF SECRET AND NOT TMP SEED]
|
|
||||||
Only Tmp
|
|
||||||
Always Show
|
|
||||||
Menu Wrapping
|
|
||||||
Default
|
|
||||||
Always Wrap
|
|
||||||
Secure Logout
|
Secure Logout
|
||||||
[NFC key shortcut] [IF NFC ENABLED]
|
SHORTCUT [IF NFC ENABLED]
|
||||||
Sign PSBT
|
Sign PSBT
|
||||||
Show Address
|
Show Address
|
||||||
Sign Message
|
Sign Message
|
||||||
@ -562,7 +538,7 @@
|
|||||||
Verify Address
|
Verify Address
|
||||||
File Share
|
File Share
|
||||||
Import Multisig
|
Import Multisig
|
||||||
Push Transaction [IF PUSHTX ENABLED]
|
Push Transaction [IF ENBALED]
|
||||||
---
|
---
|
||||||
|
|
||||||
[FACTORY MODE]
|
[FACTORY MODE]
|
||||||
@ -574,151 +550,3 @@
|
|||||||
Perform Selftest
|
Perform Selftest
|
||||||
---
|
---
|
||||||
|
|
||||||
[SSSP]
|
|
||||||
Ready To Sign
|
|
||||||
Passphrase [IF WORD BASED SEED & SSSP RELATED KEYS ENABLED]
|
|
||||||
Restore Saved
|
|
||||||
c*******
|
|
||||||
[3A14F788]
|
|
||||||
Restore
|
|
||||||
Delete
|
|
||||||
Edit Phrase
|
|
||||||
Scan Any QR Code [IF QR SCANNER]
|
|
||||||
Address Explorer
|
|
||||||
Classic P2PKH
|
|
||||||
↳ mtHSVByP9EYZ⋯Vm19gvpecb5R
|
|
||||||
P2SH-Segwit
|
|
||||||
↳ 2NCAJ5wD4Gvm⋯NphNU8UYoEJv
|
|
||||||
Segwit P2WPKH
|
|
||||||
↳ tb1qupyd58nd⋯vu9jtdyws9n9
|
|
||||||
Applications
|
|
||||||
Samourai
|
|
||||||
Post-mix
|
|
||||||
Pre-mix
|
|
||||||
Wasabi
|
|
||||||
Account Number
|
|
||||||
Custom Path
|
|
||||||
CC-2-of-4
|
|
||||||
Secure Notes & Passwords[IF ENABLED & SSSP ALLOW NOTES]
|
|
||||||
1: note0
|
|
||||||
"note0"
|
|
||||||
View Note
|
|
||||||
Sign Note Text
|
|
||||||
2: secret-PWD
|
|
||||||
"secret-PWD"
|
|
||||||
↳ satoshi
|
|
||||||
↳ abc.org
|
|
||||||
View Password
|
|
||||||
Send Password [MAYBE]
|
|
||||||
Sign Note Text
|
|
||||||
Type Passwords [MAYBE]
|
|
||||||
Seed Vault[IF ENABLED & SSSP RELATED KEYS ENABLED]
|
|
||||||
1: [7126EB3C]
|
|
||||||
[7126EB3C]
|
|
||||||
Use This Seed
|
|
||||||
2: [CCEE13B9]
|
|
||||||
[CCEE13B9]
|
|
||||||
Use This Seed
|
|
||||||
3: [03EE9989]
|
|
||||||
[03EE9989]
|
|
||||||
Use This Seed
|
|
||||||
Advanced/Tools
|
|
||||||
File Management
|
|
||||||
Sign Text File
|
|
||||||
Batch Sign PSBT
|
|
||||||
List Files
|
|
||||||
Export Wallet
|
|
||||||
Sparrow
|
|
||||||
Cove
|
|
||||||
Bitcoin Core
|
|
||||||
Nunchuk
|
|
||||||
Bull Bitcoin
|
|
||||||
Blue Wallet
|
|
||||||
Electrum Wallet
|
|
||||||
Wasabi Wallet
|
|
||||||
Fully Noded
|
|
||||||
Unchained
|
|
||||||
Theya
|
|
||||||
Bitcoin Safe
|
|
||||||
Zeus
|
|
||||||
Samourai Postmix
|
|
||||||
Samourai Premix
|
|
||||||
Descriptor
|
|
||||||
Generic JSON
|
|
||||||
Export XPUB
|
|
||||||
Segwit (BIP-84)
|
|
||||||
Classic (BIP-44)
|
|
||||||
P2WPKH/P2SH (BIP-49)
|
|
||||||
Master XPUB
|
|
||||||
Current XFP
|
|
||||||
Key Expression
|
|
||||||
Dump Summary
|
|
||||||
Verify Sig File
|
|
||||||
NFC File Share [IF NFC ENABLED]
|
|
||||||
BBQr File Share [IF QR SCANNER]
|
|
||||||
QR File Share [IF QR SCANNER]
|
|
||||||
Format SD Card
|
|
||||||
Format RAM Disk [IF VIRTDISK ENABLED]
|
|
||||||
Export Wallet
|
|
||||||
Sparrow
|
|
||||||
Cove
|
|
||||||
Bitcoin Core
|
|
||||||
Nunchuk
|
|
||||||
Bull Bitcoin
|
|
||||||
Blue Wallet
|
|
||||||
Electrum Wallet
|
|
||||||
Wasabi Wallet
|
|
||||||
Fully Noded
|
|
||||||
Unchained
|
|
||||||
Theya
|
|
||||||
Bitcoin Safe
|
|
||||||
Zeus
|
|
||||||
Samourai Postmix
|
|
||||||
Samourai Premix
|
|
||||||
Descriptor
|
|
||||||
Generic JSON
|
|
||||||
Export XPUB
|
|
||||||
Segwit (BIP-84)
|
|
||||||
Classic (BIP-44)
|
|
||||||
P2WPKH/P2SH (BIP-49)
|
|
||||||
Master XPUB
|
|
||||||
Current XFP
|
|
||||||
Key Expression
|
|
||||||
Dump Summary
|
|
||||||
Teleport Multisig PSBT [MAYBE]
|
|
||||||
View Identity
|
|
||||||
Temporary Seed [IF SSSP RELATED KEYS ENABLED]
|
|
||||||
Import from QR Scan [IF QR SCANNER]
|
|
||||||
Import Words
|
|
||||||
12 Words
|
|
||||||
18 Words
|
|
||||||
24 Words
|
|
||||||
Import via NFC [IF NFC ENABLED]
|
|
||||||
Import XPRV
|
|
||||||
Tapsigner Backup
|
|
||||||
Coldcard Backup
|
|
||||||
Restore Seed XOR
|
|
||||||
Paper Wallets
|
|
||||||
WIF Store
|
|
||||||
NFC Tools [IF NFC ENABLED]
|
|
||||||
Sign PSBT
|
|
||||||
Show Address
|
|
||||||
Sign Message
|
|
||||||
Verify Sig File
|
|
||||||
Verify Address
|
|
||||||
File Share
|
|
||||||
Push Transaction [IF PUSHTX ENABLED]
|
|
||||||
Show Firmware Version
|
|
||||||
Destroy Seed [IF SECRET AND NOT TMP SEED]
|
|
||||||
Secure Logout
|
|
||||||
EXIT TEST DRIVE [MAYBE]
|
|
||||||
[NFC key shortcut] [IF NFC ENABLED]
|
|
||||||
Sign PSBT
|
|
||||||
Show Address
|
|
||||||
Sign Message
|
|
||||||
Verify Sig File
|
|
||||||
Verify Address
|
|
||||||
File Share
|
|
||||||
Push Transaction [IF PUSHTX ENABLED]
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|||||||
@ -2,16 +2,15 @@
|
|||||||
|
|
||||||
COLDCARD can sign messages send to it via USB with the help of `ckcc` utility,
|
COLDCARD can sign messages send to it via USB with the help of `ckcc` utility,
|
||||||
sign messages provided via specially crafted file on SD card or Vdisk,
|
sign messages provided via specially crafted file on SD card or Vdisk,
|
||||||
and NFC-equipped models (Mk4, Mk5, and Q) can also sign messages sent to COLDCARD via NFC.
|
and Mk4 can also sign messages sent to COLDCARD via NFC.
|
||||||
The resulting signature can be returned over SD card/Vdisk, NFC, or — on Q — as a QR code.
|
|
||||||
|
|
||||||
Signature format follows [BIP-0137](https://github.com/bitcoin/bips/blob/master/bip-0137.mediawiki) specification.
|
Signature format follows [BIP-0137](https://github.com/bitcoin/bips/blob/master/bip-0137.mediawiki) specification.
|
||||||
COLDCARD Mk3 and COLDCARD Mk4 up to version `5.1.0` used compressed P2PKH header byte for all script types.
|
COLDCARD Mk3 and COLDCARD Mk4 up to version `5.1.0` used compressed P2PKH header byte for all script types.
|
||||||
From version `5.1.0` correct header byte is used for corresponding script type.
|
From Mk4 `5.1.0` correct header byte is used for corresponding script type.
|
||||||
|
|
||||||
### Verification
|
### Verification
|
||||||
|
|
||||||
From version `5.1.0` users can verify signed messages directly on the device.
|
From COLDCARD Mk4 version `5.1.0` users can verify signed messages directly on the device.
|
||||||
If signature file is on SD card or Virtual disk `Advanced/Tools -> File Management -> Verify Sig File`. In case
|
If signature file is on SD card or Virtual disk `Advanced/Tools -> File Management -> Verify Sig File`. In case
|
||||||
signature file is detached signature of signed export (or any other file), COLDCARD can check if digest of file
|
signature file is detached signature of signed export (or any other file), COLDCARD can check if digest of file
|
||||||
specified in the message matches contents of file. This requires file signed to be available on SD card or Vdisk.
|
specified in the message matches contents of file. This requires file signed to be available on SD card or Vdisk.
|
||||||
@ -22,7 +21,7 @@ Bitcoin core can only verify P2PKH.
|
|||||||
|
|
||||||
## Signed Exports
|
## Signed Exports
|
||||||
|
|
||||||
From version `5.1.0` most of SD card and Virtual disk exports are accompanied by detached signature file.
|
From Mk4 version `5.1.0` most of SD card and Virtual disk exports are accompanied by detached signature file.
|
||||||
If exported file name is `addresses.csv` signature file name will be `addresses.sig`.
|
If exported file name is `addresses.csv` signature file name will be `addresses.sig`.
|
||||||
|
|
||||||
### Message construction and signature file format
|
### Message construction and signature file format
|
||||||
@ -40,6 +39,8 @@ IFOvGVJrm31S0j+F4dVfQ5kbRKWKcmhmXIn/Lw8iIgaCG5QNZswjrN4X673R7jTZo1kvLmiD4hlIrbuL
|
|||||||
-----END BITCOIN SIGNATURE-----
|
-----END BITCOIN SIGNATURE-----
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### What is signed
|
||||||
|
|
||||||
### What Is Signed
|
### What Is Signed
|
||||||
|
|
||||||
1. **Single sig address explorer exports:** Signed by the key corresponding to the first (0th) address on the exported list.
|
1. **Single sig address explorer exports:** Signed by the key corresponding to the first (0th) address on the exported list.
|
||||||
@ -60,3 +61,13 @@ IFOvGVJrm31S0j+F4dVfQ5kbRKWKcmhmXIn/Lw8iIgaCG5QNZswjrN4X673R7jTZo1kvLmiD4hlIrbuL
|
|||||||
* Multisig wallet descriptor: signed by the key corresponding to the first external address of own enrolled extended key `my_key/0/0`
|
* Multisig wallet descriptor: signed by the key corresponding to the first external address of own enrolled extended key `my_key/0/0`
|
||||||
* Generic XPUBs export: signed by the key corresponding to the first external address of own standard P2WSH derivation `m/48h/<coin_type>h/<account>h/2h/0/0`
|
* Generic XPUBs export: signed by the key corresponding to the first external address of own standard P2WSH derivation `m/48h/<coin_type>h/<account>h/2h/0/0`
|
||||||
* Multisig address explorer export: Signed by own key at the same derivation as first (0th) row on exported list. `my_key/<change>/<start_index>`
|
* Multisig address explorer export: Signed by own key at the same derivation as first (0th) row on exported list. `my_key/<change>/<start_index>`
|
||||||
|
|
||||||
|
### What is NOT signed
|
||||||
|
|
||||||
|
Multisig exports and generic multisig xpub exports are not signed. It is not clear at this point
|
||||||
|
whether to sign these exports with some generic single signature key (i.e. `m/44'/<coin_type>'/0'/0/0`)
|
||||||
|
or with our portion (leg) of script. In both cases script type (address format) would not match as multisignature
|
||||||
|
message signing is not standardized.
|
||||||
|
|
||||||
|
1. **Multisig exports**
|
||||||
|
2. **Generic multisig exports**
|
||||||
@ -1,20 +1,10 @@
|
|||||||
# NFC and Coldcard
|
# NFC and Coldcard Mk4
|
||||||
|
|
||||||
(Applies to NFC-equipped models: Mk4, Mk5, and Q)
|
(Applies to Coldcard Mk4 only)
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
The NFC antenna location depends on the hardware:
|
Mk4 NFC antenna is centered under number `8` on the keypad. Before using NFC,
|
||||||
|
|
||||||
- **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
|
it is important to locate the position of NFC antenna on your device and point it
|
||||||
correctly towards the Coldcard NFC antenna. Picture below shows an example with iPhone
|
correctly towards the Coldcard NFC antenna. Picture below shows an example with iPhone
|
||||||
that has NFC antenna located at the top right edge. The NFC smartphone antenna
|
that has NFC antenna located at the top right edge. The NFC smartphone antenna
|
||||||
@ -46,7 +36,7 @@ in general. Good interoperability is critical with radio standards.
|
|||||||
|
|
||||||
## Lower Layers
|
## Lower Layers
|
||||||
|
|
||||||
The Coldcard has a chip that acts as a Type 5 NFC tag. The
|
The Coldcard Mk4 has an chip that acts as a Type 5 NFC tag. The
|
||||||
radio standard is called "NFC-V" or ISO-15693, and operates on a
|
radio standard is called "NFC-V" or ISO-15693, and operates on a
|
||||||
13.56 Mhz carrier wave.
|
13.56 Mhz carrier wave.
|
||||||
|
|
||||||
@ -68,13 +58,9 @@ unless we are actively sharing something. We disable the "energy
|
|||||||
harvesting" features of the chip, so it will not do anything when
|
harvesting" features of the chip, so it will not do anything when
|
||||||
the Coldcard is powered-down, regardless of the NFC setting.
|
the Coldcard is powered-down, regardless of the NFC setting.
|
||||||
|
|
||||||
If the above is not enough for you, the antenna can be destroyed:
|
If the above is not enough for you, the antenna can be destroyed
|
||||||
|
by cutting the trace labeled "NFC" inside the hole for the MicroSD
|
||||||
- **Mk4**: cut the trace labeled "NFC" inside the hole for the MicroSD card,
|
card. Use the point of a sharp knife to cut and peel up the trace.
|
||||||
using the point of a sharp knife to cut and peel up the trace.
|
|
||||||
- **Mk5**: has no such trace — its antenna is the discrete coil `L6` in the
|
|
||||||
top-right corner, which would have to be physically removed instead.
|
|
||||||
- **Q1**: cut the trace labeled "NFC DATA" under the batteries.
|
|
||||||
|
|
||||||
The NFC traffic is not encrypted and is subject to eavesdropping.
|
The NFC traffic is not encrypted and is subject to eavesdropping.
|
||||||
While the NFC feature is active, your Coldcard can be uniquely
|
While the NFC feature is active, your Coldcard can be uniquely
|
||||||
|
|||||||
@ -34,7 +34,7 @@ The COLDCARD needs a URL prefix. To that it appends some values:
|
|||||||
- when RegTest is enabled, the value will be `XRT`
|
- when RegTest is enabled, the value will be `XRT`
|
||||||
|
|
||||||
We provide a few default URL values to our customers, including one backend we
|
We provide a few default URL values to our customers, including one backend we
|
||||||
will operate on `coldcard.com`. The URL can also be directly entered by the
|
will operate on `colcard.com`. The URL can also be directly entered by the
|
||||||
customer. On the Q, it can be scanned from a QR code.
|
customer. On the Q, it can be scanned from a QR code.
|
||||||
|
|
||||||
For COLDCARD backend, the url used is:
|
For COLDCARD backend, the url used is:
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 258 KiB |
@ -11,17 +11,11 @@ The entrypoint makefile for repro builds.
|
|||||||
The `repro` command in `shared.mk` is the first step in the repro build process, which triggers a docker build and run process.
|
The `repro` command in `shared.mk` is the first step in the repro build process, which triggers a docker build and run process.
|
||||||
|
|
||||||
```makefile
|
```makefile
|
||||||
repro: submods-match code-committed
|
|
||||||
repro:
|
repro:
|
||||||
docker build -t coldcard-build - < dockerfile.build
|
docker build -t coldcard-build - < dockerfile.build
|
||||||
(cd ..; docker run $(DOCK_RUN_ARGS) sh src/stm32/repro-build.sh $(VERSION_STRING) $(HW_MODEL) $(PARENT_MKFILE))
|
(cd ..; docker run $(DOCK_RUN_ARGS) sh src/stm32/repro-build.sh $(VERSION_STRING) $(MK_NUM))
|
||||||
```
|
```
|
||||||
|
|
||||||
`$(HW_MODEL)` is the model string (e.g. `mk4`, `q1`) and `$(PARENT_MKFILE)` is the
|
|
||||||
top-level makefile being used (`MK-Makefile` or `Q1-Makefile`). The `submods-match`
|
|
||||||
and `code-committed` prerequisites ensure the submodules and working tree are clean
|
|
||||||
before a repro build.
|
|
||||||
|
|
||||||
Below are interesting sections from the docker logs that give an idea as to what is going on in build process:
|
Below are interesting sections from the docker logs that give an idea as to what is going on in build process:
|
||||||
|
|
||||||
```stdout
|
```stdout
|
||||||
@ -67,19 +61,19 @@ Successfully installed signit-1.0
|
|||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
+ make -f MK-Makefile setup
|
+ make -f MK4-Makefile setup
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
+ make -f MK-Makefile firmware-signed.bin firmware-signed.dfu production.bin dev.dfu firmware.lss firmware.elf
|
+ make -f MK4-Makefile firmware-signed.bin firmware-signed.dfu production.bin dev.dfu firmware.lss firmware.elf
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
signit sign -b l-port/build-COLDCARD_MK4 -m mk4 5.0.7 -o firmware-signed.bin
|
signit sign -b l-port/build-COLDCARD_MK4 -m 4 5.0.7 -o firmware-signed.bin
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
signit sign -m mk4 5.0.7 -r firmware-signed.bin -k 1 -o production.bin
|
signit sign -m 4 5.0.7 -r firmware-signed.bin -k 1 -o production.bin
|
||||||
You don't have that key (1), so using key zero instead!
|
You don't have that key (1), so using key zero instead!
|
||||||
...
|
...
|
||||||
|
|
||||||
@ -102,7 +96,7 @@ production.bin
|
|||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
+ make -f MK-Makefile 'PUBLISHED_BIN=/tmp/checkout/firmware/releases/2022-10-05T1724-v5.0.7-mk4-coldcard.dfu' check-repro
|
+ make -f MK4-Makefile 'PUBLISHED_BIN=/tmp/checkout/firmware/releases/2022-10-05T1724-v5.0.7-mk4-coldcard.dfu' check-repro
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
@ -189,7 +183,7 @@ To summarize `check-repro`:
|
|||||||
|
|
||||||
- `split` (cli/signit.py: Line 153-175) is run against the release `*.dfu` resulting in a `check-fw.bin` and `check-bootrom.bin`. "This splits the DFU file into the two parts it contains: the main firmware (COLDCARD application) and the boot loader code."
|
- `split` (cli/signit.py: Line 153-175) is run against the release `*.dfu` resulting in a `check-fw.bin` and `check-bootrom.bin`. "This splits the DFU file into the two parts it contains: the main firmware (COLDCARD application) and the boot loader code."
|
||||||
|
|
||||||
- `check` (cli/signit.py: Line 176-243) is run against each the release `check-fw.bin` and our built `firmware-signed.bin`.
|
- `check` (cli/signit.py: Line 176-241) is run against each the release `check-fw.bin` and our built `firmware-signed.bin`.
|
||||||
|
|
||||||
- a hexdump is taken of each the release `check-fw.bin` and our built `firmware-signed.bin` piped through $TRIM_SIG which removes 64 bytes of signature data and subsitutes it with a common string.
|
- a hexdump is taken of each the release `check-fw.bin` and our built `firmware-signed.bin` piped through $TRIM_SIG which removes 64 bytes of signature data and subsitutes it with a common string.
|
||||||
|
|
||||||
|
|||||||
@ -1,112 +0,0 @@
|
|||||||
# 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
|
| `pin stretch` | slot 2 | HMAC | SE1 | Key stretching for PIN entry and anti-phish words
|
||||||
| `firmware` | slot 14 | SHA256d | SE1 | Firmware checksum, controls green/red LEDs
|
| `firmware` | slot 14 | SHA256d | SE1 | Firmware checksum, controls green/red LEDs
|
||||||
| `nonce/chksum` | slot 10 | data | SE1 | AES nonce and GMAC tag, protected by PIN
|
| `nonce/chksum` | slot 10 | data | SE1 | AES nonce and GMAC tag, protected by PIN
|
||||||
| `SE2 easy key` | page 14 | AES via HMAC | SE2 | Another SE2 part of AES seed key
|
| `SE2 easy key` | page 15 | 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
|
| `SE2 hard key` | page 14 | AES via ECC | SE2 | SE2's part of AES seed key; ECC used to unlock
|
||||||
| `tpin key` | `tpin_key` | HMAC(key) | MCU | Key for HMAC used to encrypt trick PINs
|
| `tpin key` | `tpin_key` | HMAC(key) | MCU | Key for HMAC used to encrypt trick PINs
|
||||||
| `trick PIN slots` | pages 0-12 | HMAC | SE2 | Protect duress wallet seeds and pins (6 spots)
|
| `trick PIN slots` | pages 0-12 | HMAC | SE2 | Protect duress wallet seeds and pins (6 spots)
|
||||||
| `SE2 trash` | secret B | HMAC | SE2 | Used to destroy values (only SE2 knows the value)
|
| `SE2 trash` | secret B | HMAC | SE2 | Used to destroy values (only SE2 knows the value)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# COLDCARD Mk4/Mk5/Q Security Model
|
# COLDCARD Mk4/Q Security Model
|
||||||
|
|
||||||
## Abstract
|
## Abstract
|
||||||
|
|
||||||
@ -96,10 +96,9 @@ user entered the True PIN. An attacker will only have access to the
|
|||||||
duress wallet. They won't have access to steal the main stash.
|
duress wallet. They won't have access to steal the main stash.
|
||||||
|
|
||||||
The private key can be automatically derived using BIP-85 methods,
|
The private key can be automatically derived using BIP-85 methods,
|
||||||
based on account numbers 1001, 1002, or 1003 for a 24-word duress wallet
|
based on account numbers 1001, 1002, or 1003. Because this is BIP-85
|
||||||
(or 2001, 2002, 2003 for a 12-word one). Because this is BIP-85
|
based and uses a 24-word seed, it behaves exactly like a normal
|
||||||
based, it behaves exactly like a normal wallet. Defining a passphrase
|
wallet. Defining a passphrase for the wallet is also possible.
|
||||||
for the wallet is also possible.
|
|
||||||
|
|
||||||
The Mk4 also supports older COLDCARD duress wallets and their UTXOs
|
The Mk4 also supports older COLDCARD duress wallets and their UTXOs
|
||||||
on the blockchain. There is an option to create compatible wallets
|
on the blockchain. There is an option to create compatible wallets
|
||||||
@ -244,7 +243,7 @@ COLDCARD's case to do so, but the option is there if needed.
|
|||||||
|
|
||||||
## SD Card Recovery Mode
|
## SD Card Recovery Mode
|
||||||
|
|
||||||
Mk4/Mk5/Q bootloader is smart enough to be able to read an SD card. You
|
Mk4/Q bootloader is smart enough to be able to read an SD card. You
|
||||||
will only be able to trigger the SD card loading code, if the
|
will only be able to trigger the SD card loading code, if the
|
||||||
COLDCARD was powered down during the upgrade process. At that point,
|
COLDCARD was powered down during the upgrade process. At that point,
|
||||||
the intended firmware image has been lost because it it held in
|
the intended firmware image has been lost because it it held in
|
||||||
|
|||||||
@ -12,7 +12,7 @@ are not discrete and you could be compelled to produce the passphrase.
|
|||||||
|
|
||||||
Enter [_Seed XOR_](https://seedxor.com), a plausibly deniable means
|
Enter [_Seed XOR_](https://seedxor.com), a plausibly deniable means
|
||||||
of storing secrets in two or more parts that look and behave just
|
of storing secrets in two or more parts that look and behave just
|
||||||
like the original secret. One 12-, 18-, or 24-word seed phrase becomes two or more parts
|
like the original secret. One 12 or 24-word seed phrase becomes two or more parts
|
||||||
that are also BIP-39 compatible seeds phrases. These should be backed up in your
|
that are also BIP-39 compatible seeds phrases. These should be backed up in your
|
||||||
preferred method, metal or otherwise. These parts can be individually loaded
|
preferred method, metal or otherwise. These parts can be individually loaded
|
||||||
with honeypot funds as each one has same word length, with the last being
|
with honeypot funds as each one has same word length, with the last being
|
||||||
@ -78,12 +78,10 @@ words right the next day.
|
|||||||
|
|
||||||
When the parts are made deterministically, we take a double-SHA256 over
|
When the parts are made deterministically, we take a double-SHA256 over
|
||||||
a fixed string (`Batshitoshi`), your master secret, and the text
|
a fixed string (`Batshitoshi`), your master secret, and the text
|
||||||
`0 of 4 parts` which changes for each part (the index is 0-based).
|
`1 of 4 parts` which changes for each part.
|
||||||
|
|
||||||
In random mode, we simply pick random bytes (and then double-SHA256
|
In random mode, we simply pick 32 random bytes (and then double-SHA256
|
||||||
them) from the Coldcard's True Random Number Generator (TRNG). The number
|
them) from the Coldcard's True Random Number Generator (TRNG)..
|
||||||
of bytes matches your secret length: 16, 24, or 32 bytes for a 12-, 18-,
|
|
||||||
or 24-word seed respectively.
|
|
||||||
|
|
||||||
This is done to make all but the one part. The final part is the
|
This is done to make all but the one part. The final part is the
|
||||||
value needed to get back to your secret, so it's the XOR of the
|
value needed to get back to your secret, so it's the XOR of the
|
||||||
@ -159,12 +157,6 @@ with the others on a SEEDPLATE.
|
|||||||
- right to A, down to B ... take that number, and go to that column
|
- right to A, down to B ... take that number, and go to that column
|
||||||
- down to C, that is answer: a ⊕ b ⊕ c
|
- 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
|
# 24 Words XOR Seed Example Using 3 Parts
|
||||||
|
|||||||
@ -1,209 +0,0 @@
|
|||||||
# 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
|
# Temporary Seeds
|
||||||
|
|
||||||
|
|
||||||
[_(new in v5.0.7, requires Mk4, Mk5, or Q)_](upgrade.md)
|
[_(new in v5.0.7, requires Mk4)_](upgrade.md)
|
||||||
|
|
||||||
|
|
||||||
Temporary seed (renamed in `5.2.0` from Ephemeral seed) is a temporary secret completely separate
|
Temporary seed (renamed in `5.2.0` from Ephemeral seed) is a temporary secret completely separate
|
||||||
@ -42,7 +42,7 @@ Read more about `Seed Vault` feature below.
|
|||||||
- `24 words`
|
- `24 words`
|
||||||
- `XPRV (BIP-32)`
|
- `XPRV (BIP-32)`
|
||||||
- pick derivation `Index` in next prompt, or just press OK for index 0
|
- pick derivation `Index` in next prompt, or just press OK for index 0
|
||||||
- Press (0) in next prompt to activate derived secret as a temporary seed
|
- Press (2) in next prompt to activate derived secret as a temporary seed
|
||||||
|
|
||||||
* temporary seed can be activated from Duress Wallet
|
* temporary seed can be activated from Duress Wallet
|
||||||
- go to `Settings -> Login Settings -> Trick Pins`
|
- go to `Settings -> Login Settings -> Trick Pins`
|
||||||
@ -66,7 +66,7 @@ Ability to generate and use **Temporary seed** is available on Coldcard when:
|
|||||||
|
|
||||||
# Restore Master
|
# Restore Master
|
||||||
|
|
||||||
[_(new in v5.2.0, requires Mk4, Mk5, or Q)_](upgrade.md)
|
[_(new in v5.2.0, requires Mk4)_](upgrade.md)
|
||||||
|
|
||||||
From version `5.2.0` users no longer need to reboot COLDCARD to return
|
From version `5.2.0` users no longer need to reboot COLDCARD to return
|
||||||
to their "master seed" (one stored in SE2). Once COLDCARD has temporary
|
to their "master seed" (one stored in SE2). Once COLDCARD has temporary
|
||||||
@ -84,7 +84,7 @@ Seed Vault entries can only be deleted in Seed Vault menu.
|
|||||||
|
|
||||||
# Seed Vault
|
# Seed Vault
|
||||||
|
|
||||||
[_(new in v5.2.0, requires Mk4, Mk5, or Q)_](upgrade.md)
|
[_(new in v5.2.0, requires Mk4)_](upgrade.md)
|
||||||
|
|
||||||
Seed Vault adds the ability to store multiple temporary secrets into encrypted settings for simple
|
Seed Vault adds the ability to store multiple temporary secrets into encrypted settings for simple
|
||||||
recall and later use (AES-256-CTR encrypted with your master seed's key).
|
recall and later use (AES-256-CTR encrypted with your master seed's key).
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
# Firmware Upgrade and Recovery Process
|
# Firmware Upgrade and Recovery Process
|
||||||
|
|
||||||
_This document applies to the Mk4, Mk5, and Q. Earlier COLDCARDs did not use this approach._
|
_This document applies only to the Mk4. Earlier COLDCARDs did not use this approach._
|
||||||
|
|
||||||
On the COLDCARD, we have done away with the slow external SPI flash
|
On the COLDCARD, we have done away with the slow external SPI flash
|
||||||
(serial flash) chip entirely (used in Mk1-Mk3). In it's place we
|
(serial flash) chip entirely (used in Mk1-Mk3). In it's place we
|
||||||
|
|||||||
@ -18,11 +18,8 @@ for the COLDCARD Q, it is a QR code to be scanned.
|
|||||||
The HSM feature uses HOTP tokens, which do not require a backend,
|
The HSM feature uses HOTP tokens, which do not require a backend,
|
||||||
but are not as robust as time-based tokens.
|
but are not as robust as time-based tokens.
|
||||||
|
|
||||||
Web2FA is available to be enabled as part of a Spending Policy,
|
For now, Web2FA is only being used as part of CCC spending policy (opt-in),
|
||||||
both in Multisig and Single Signer modes. When enabled, you will be
|
but we may find other uses for it.
|
||||||
prompted complete 2FA authentication after viewing the details of
|
|
||||||
the transaction to be signed. You will not be able to sign without
|
|
||||||
the correct code.
|
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
@ -30,8 +27,8 @@ the correct code.
|
|||||||
- Usual 2fa base32 secret is picked by CC and stored in CC (so that server is stateless)
|
- Usual 2fa base32 secret is picked by CC and stored in CC (so that server is stateless)
|
||||||
- CC creates URL encrypted to the pubkey of server, containing args:
|
- CC creates URL encrypted to the pubkey of server, containing args:
|
||||||
- shared secret for TOTP (same value as held in user's phone)
|
- shared secret for TOTP (same value as held in user's phone)
|
||||||
- the response nonce (32 bytes, shown as 64 hex chars, on Q; or 8 digits on Mk4)
|
- the response nonce (16 bytes, or 8 digits for Mk4) to be revealed to the user
|
||||||
to be revealed to the user on successful auth
|
on successful auth
|
||||||
- flag if Q model, so can provide a QR to be scanned in that case (rather than digits)
|
- flag if Q model, so can provide a QR to be scanned in that case (rather than digits)
|
||||||
- some text label for what's being approved, which is presented to user so they can pick
|
- some text label for what's being approved, which is presented to user so they can pick
|
||||||
correct 2fa shared secret.
|
correct 2fa shared secret.
|
||||||
@ -65,7 +62,7 @@ the correct code.
|
|||||||
- multiplies that private key by server's known public key
|
- multiplies that private key by server's known public key
|
||||||
- apply sha256(resulting coordinate) => the session key
|
- apply sha256(resulting coordinate) => the session key
|
||||||
- apply AES-256-CTR over URL contents (ascii text)
|
- apply AES-256-CTR over URL contents (ascii text)
|
||||||
- prepend 33 bytes of pubkey, and then base64url encode all of it
|
- prepend 33 bytes of pubkey, and base64url encode all of it
|
||||||
- full url is: `https://coldcard.com/2fa?{base64 encoded binary}`
|
- full url is: `https://coldcard.com/2fa?{base64 encoded binary}`
|
||||||
|
|
||||||
## Trust Issues
|
## Trust Issues
|
||||||
@ -82,15 +79,12 @@ the correct code.
|
|||||||
|
|
||||||
## URL Format
|
## URL Format
|
||||||
|
|
||||||
https://coldcard.com/2fa?g={nonce}&ss={shared_secret}&nm={label_text}&q={is_q}
|
https://coldcard.com/2fa?ss={shared_secret}&q={is_q}&g={nonce}&nm={label_text}
|
||||||
|
|
||||||
(the query string is then encrypted to the server's pubkey, so the args above
|
|
||||||
are what is inside the encrypted payload.)
|
|
||||||
|
|
||||||
- `nonce`: text string that is either 8 digits on Mk4, or 64 hex chars on Q
|
|
||||||
- `shared_secret`: 16 chars of Base32-encoded pre-shared secret
|
- `shared_secret`: 16 chars of Base32-encoded pre-shared secret
|
||||||
- `nm`: human readable label for the transaction/purpose
|
|
||||||
- `is_q`: flag indicating use of QR to provide nonce back to user
|
- `is_q`: flag indicating use of QR to provide nonce back to user
|
||||||
|
- `nonce`: text string that is either 8 digits for Mk4, or hex digits for QR
|
||||||
|
- `nm`: human readable label for the transaction/purpose
|
||||||
|
|
||||||
Server will accept plaintext arguments as above, but normally everything
|
Server will accept plaintext arguments as above, but normally everything
|
||||||
after the question mark is encrypted.
|
after the question mark is encrypted.
|
||||||
|
|||||||
2
external/ckcc-protocol
vendored
2
external/ckcc-protocol
vendored
@ -1 +1 @@
|
|||||||
Subproject commit 3d1dfa858beb58b8dac37d8c66d7aed2909812f2
|
Subproject commit f87d30f220cb6334eb3c4ace93c1b62e04942022
|
||||||
2
external/libngu
vendored
2
external/libngu
vendored
@ -1 +1 @@
|
|||||||
Subproject commit 537519a829259622ea6b0334fbafd6cae852852f
|
Subproject commit 1cccb25ef7736efae4a1de83d5dbdc13a2db0e80
|
||||||
@ -17,17 +17,9 @@ class Graphics:
|
|||||||
|
|
||||||
mk4_nfc_4 = (102, 49, 13, 0, b'\x00\x7f\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x0e\x03\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\xe0\x0e\x07\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xe0\x0e\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e0\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e0\x000D\x04\x10\x00\x00\x7f\xf0\x00\xe0\x0e0\x000D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00(D\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00(D\x04\x00\x00\x00xp\x00\xff\xff\xb0\x00$G\xe4\x00\x00\x00xp\x00\xff\xff\xb0\x00$G\xe4\x00\x00\x00xp\x00\xe0\x0e0\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e0\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e0\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e?\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xff\xff\x9f\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\xff\xff\x8f\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x7f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00')
|
mk4_nfc_4 = (102, 49, 13, 0, b'\x00\x7f\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x0e\x03\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\xe0\x0e\x07\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xe0\x0e\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e0\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e0\x000D\x04\x10\x00\x00\x7f\xf0\x00\xe0\x0e0\x000D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00(D\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00(D\x04\x00\x00\x00xp\x00\xff\xff\xb0\x00$G\xe4\x00\x00\x00xp\x00\xff\xff\xb0\x00$G\xe4\x00\x00\x00xp\x00\xe0\x0e0\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e0\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e0\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e?\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xff\xff\x9f\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\xff\xff\x8f\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x7f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00')
|
||||||
|
|
||||||
mk5_nfc_1 = (126, 49, 16, 0, b'\x00\x7f\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0\xf0\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x0e\x00\xe0\x0e\x03\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\xe0\x0e\x00\xe0\x0e\x07\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xe0\x0e\x00\xe0\x0e\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\xe0\x0e\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x000D\x04\x10\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x000D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00(D\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00(D\x04\x00\x00\x00xp\x00\xff\xff\xff\xff\xfe0\x00$G\xe4\x00\x00\x00xp\x00\xff\xff\xff\xff\xfe0\x00$G\xe4\x00\x00\x00xp\x00\xe0\x0e\x00\xe0\x0e0\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e\x00\xe0\x0e0\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e\x00\xe0\x0e0\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e?\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xff\xff\xff\xff\xfe\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\xff\xff\xff\xff\xfe\x0f\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x7f\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
|
|
||||||
|
|
||||||
mk5_nfc_2 = (118, 49, 15, 0, b'\x00\x7f\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0\xf0\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x0e\x00\xe0\x03\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\xe0\x0e\x00\xe0\x07\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xe0\x0e\x00\xe0\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\xe0\x0e\x00\xe00\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x000D\x04\x10\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x000D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00(D\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00(D\x04\x00\x00\x00xp\x00\xff\xff\xff\xff0\x00$G\xe4\x00\x00\x00xp\x00\xff\xff\xff\xff0\x00$G\xe4\x00\x00\x00xp\x00\xe0\x0e\x00\xe00\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e\x00\xe00\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e\x00\xe00\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0?\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xff\xff\xff\xff\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\xff\xff\xff\xff\x0f\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x7f\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00')
|
|
||||||
|
|
||||||
mk5_nfc_3 = (110, 49, 14, 0, b'\x00\x7f\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0\xf0\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x0e\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\xe0\x0e\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xe0\x0e\x00\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\xe0\x0e\x000\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x000D\x04\x10\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x000D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00(D\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00(D\x04\x00\x00\x00xp\x00\xff\xff\xff0\x00$G\xe4\x00\x00\x00xp\x00\xff\xff\xff0\x00$G\xe4\x00\x00\x00xp\x00\xe0\x0e\x000\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e\x000\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e\x000\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00?\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xff\xff\xff\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\xff\xff\xff\x0f\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x7f\xff\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00')
|
|
||||||
|
|
||||||
mk5_nfc_4 = (102, 49, 13, 0, b'\x00\x7f\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\xf0\xf0\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x0e\x03\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\xe0\x0e\x07\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xe0\x0e\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e0\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e0\x000D\x04\x10\x00\x00\x7f\xf0\x00\xe0\x0e0\x000D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00(D\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00(D\x04\x00\x00\x00xp\x00\xff\xff\xb0\x00$G\xe4\x00\x00\x00xp\x00\xff\xff\xb0\x00$G\xe4\x00\x00\x00xp\x00\xe0\x0e0\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e0\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e0\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e?\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xff\xff\x9f\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\xff\xff\x8f\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x7f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00')
|
|
||||||
|
|
||||||
scroll = (3, 61, 1, 0, b'@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@@\xe0@')
|
scroll = (3, 61, 1, 0, b'@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@@\xe0@')
|
||||||
|
|
||||||
selected = (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')
|
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')
|
||||||
|
|
||||||
sm_box = (11, 17, 2, 0, b'\xe4\xe0\x80 \x80 \x80 \x00\x00\x00\x00\x80 \x00\x00\x00\x00\x00\x00\x80 \x00\x00\x00\x00\x80 \x80 \x80 \xe4\xe0')
|
sm_box = (11, 17, 2, 0, b'\xe4\xe0\x80 \x80 \x80 \x00\x00\x00\x00\x80 \x00\x00\x00\x00\x00\x00\x80 \x00\x00\x00\x00\x80 \x80 \x80 \xe4\xe0')
|
||||||
|
|
||||||
|
|||||||
@ -1,49 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
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 @@
|
|||||||
|
|
||||||
|
|
||||||
X
|
xx
|
||||||
XX
|
xx
|
||||||
X
|
xx
|
||||||
XX
|
xx
|
||||||
X X
|
xx xx
|
||||||
XX XX
|
xx xx
|
||||||
XX X
|
xx xx
|
||||||
XXXX
|
xxx
|
||||||
XX
|
x
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
# Coldcard Hardware Details
|
# Coldcard Hardware Details
|
||||||
|
|
||||||
This directory contains enough information for you to be able to
|
This directory contains enough information for you to be able to
|
||||||
@ -5,6 +6,7 @@ build your own Coldcard from off-the-shelf parts.
|
|||||||
We are sharing this information for the benefit of security
|
We are sharing this information for the benefit of security
|
||||||
researchers who wish to analyse the Coldcard more completely.
|
researchers who wish to analyse the Coldcard more completely.
|
||||||
|
|
||||||
|
|
||||||
# Schematic
|
# Schematic
|
||||||
|
|
||||||

|

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

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

|

|
||||||
|
|
||||||
`schematic-mark4d.png`
|
`schematic-mark4d.png`
|
||||||
@ -34,20 +30,27 @@ This is the Mark3 rev B schematic.
|
|||||||
|
|
||||||
# BOM - Bill of Materials
|
# BOM - Bill of Materials
|
||||||
|
|
||||||
The parts used in the Coldcard are detailed in these spreadsheets.
|
The parts used in the Coldcard are detailed in this spreadsheet file.
|
||||||
Most of them could be bought on Digikey, but some are direct from suppliers.
|
Most of them could be bought on Digikey, but some are direct from suppliers.
|
||||||
|
|
||||||
- BOM for Q rev D: `bom-q1d.xlsx`
|
|
||||||
- BOM for Mk5 rev F: `bom-mark5f.xlsx`
|
|
||||||
- BOM for Mk4 rev D: `bom-mark4d.xlsx`
|
|
||||||
- BOM for Mk3 rev B: `bom-mark3b.xlsx`
|
|
||||||
|
|
||||||
Not included are these minor bits:
|
Not included are these minor bits:
|
||||||
|
|
||||||
- the plastic case (custom)
|
- the plastic case (custom)
|
||||||
- the secure bag (with barcode serial number)
|
- the secure bag (with barcode serial number)
|
||||||
- pin-recovery card
|
- pin-recovery card
|
||||||
|
|
||||||
|
`bom-q1d.xlsx`
|
||||||
|
|
||||||
|
- BOM for Q rev D.
|
||||||
|
|
||||||
|
`bom-mark4d.xlsx`
|
||||||
|
|
||||||
|
- BOM for Mk3 rev D.
|
||||||
|
|
||||||
|
`bom-mark3b.xlsx`
|
||||||
|
|
||||||
|
- BOM for Mk3 rev B.
|
||||||
|
|
||||||
# Important
|
# Important
|
||||||
|
|
||||||
- No promises that these files are 100% current because we constantly make quality improvements.
|
- No promises that these files are 100% current because we constantly make quality improvements.
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 428 KiB |
@ -1,2 +1,2 @@
|
|||||||
Pillow==12.1.1
|
Pillow==10.3.0
|
||||||
|
|
||||||
|
|||||||
@ -2,41 +2,32 @@
|
|||||||
|
|
||||||
This lists the changes in the most recent firmware, for each hardware platform.
|
This lists the changes in the most recent firmware, for each hardware platform.
|
||||||
|
|
||||||
# Shared Improvements - Both Mk and Q
|
# Shared Improvements - Both Mk4 and Q
|
||||||
|
|
||||||
- New Feature: Sign [BIP-322](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki) Proof of Reserve PSBT files.
|
- Enhancement: Text word-wrap done more carefully so never cuts off any text, and yet
|
||||||
- Requires a carefully crafted PSBT that does not represent a monetary transaction, but instead is demonstrating
|
doesn't waste space.
|
||||||
control over the keys for a list of UTXO, and commits to a short text message.
|
- Bugfix: `Add current tmp` option, which could be shown in `Seed Vault` menu under
|
||||||
- Read more [here](https://github.com/Coldcard/firmware/blob/master/docs/proof-of-reserves-bip-322.md).
|
specific circumstances, would corrupt master settings if selected.
|
||||||
- New Feature: WIF Store. Ability to import foreign WIF keys (Wallet Import Format) and use them for PSBT signing.
|
- Bugfix: PUSHDATA2 in bitcoin script caused yikes.
|
||||||
- New Feature: Export [BIP-380](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki) extended key expression.
|
- Bugfix: Warning for unknown scripts was not shown at the top of the signing story.
|
||||||
Navigate to "Advanced/Tools -> Export Wallet -> Key Expression"
|
|
||||||
- New Feature: Transaction Input Explorer. Shows data about UTXO(s) being spent. Press (2) before approving
|
|
||||||
transaction to enter Transaction Explorer.
|
|
||||||
- New Feature: Support for v3 transactions in PSBT files.
|
|
||||||
- New Feature: Option to type a derived BIP-85 secret as an emulated USB keyboard.
|
|
||||||
- New Feature: Nuke Device: purges all sensitive data and makes your COLDCARD e-waste.
|
|
||||||
- Enhancement: CCC debug menu allows you to reset block height.
|
|
||||||
- Enhancement: Show the BIP-39 passphrase on-screen (must scroll down) once new key is in effect.
|
|
||||||
- Enhancement: New "Buried Settings" menu, inside Settings menu, for rarely-applied settings.
|
|
||||||
- Enhancement: Add "Blue Wallet" option to "Export Wallet"
|
|
||||||
- Enhancement: Detect duplicated inputs in PSBT file.
|
|
||||||
- Bugfix: Replace `/` with `-` in exported file names of multisig wallet export artifacts.
|
|
||||||
|
|
||||||
# Mk Specific Changes
|
|
||||||
|
|
||||||
## 5.5.0 - 2065-03-05
|
# Mk4 Specific Changes
|
||||||
|
|
||||||
- This release supports both the newer Mk5 hardware and existing Mk4.
|
## 5.4.3 - 2025-05-14
|
||||||
- Enhancement: Show QR of XOR-split seeds.
|
|
||||||
|
- Bugfix: With both NFC & Virtual Disk OFF, user cannot exit `Export Wallet` menu. Gets stuck
|
||||||
|
in export loop and needs reboot to escape.
|
||||||
|
- Bugfix: Part of extended keys in stories were not always visible.
|
||||||
|
|
||||||
|
|
||||||
# Q Specific Changes
|
# Q Specific Changes
|
||||||
|
|
||||||
## 1.4.0Q - 2065-03-05
|
## 1.3.3Q - 2025-05-14
|
||||||
|
|
||||||
- Bugfix: Empty notes in hobbled mode caused yikes upon menu entry.
|
|
||||||
|
|
||||||
|
- Bugfix: Do not allow to teleport PSBTs from SD card when CC has no secrets.
|
||||||
|
- Bugfix: Calculator login mode: added "rand()" command, removed support
|
||||||
|
for variables/assignments.
|
||||||
|
|
||||||
|
|
||||||
# Release History
|
# Release History
|
||||||
|
|||||||
@ -1,58 +1,5 @@
|
|||||||
*See ChangeLog.md for more recent changes, these are historic versions*
|
*See ChangeLog.md for more recent changes, these are historic versions*
|
||||||
|
|
||||||
|
|
||||||
## 5.4.5 - 2025-11-03
|
|
||||||
|
|
||||||
- Enhancement: Address format guessing changed away from using PSBT XPUB's derivation paths.
|
|
||||||
Now based on witness/redeem script of first PSBT input instead.
|
|
||||||
- Enhancement: Show master XFP of backup secret and ask user for confirmation before loading backup.
|
|
||||||
- Enhancement: Show firmware version added to hobbled Advanced/Tools menu.
|
|
||||||
- Bugfix: Exiting text input of Custom Backup Password caused yikes.
|
|
||||||
- Bugfix: Temporary seeds in SSSP mode were not able to update block height.
|
|
||||||
|
|
||||||
## 5.4.4 - 2025-09-30
|
|
||||||
|
|
||||||
- Spending policies for "Single Signers" adds new spending policy options:
|
|
||||||
- limit your Coldcard so it refuses to sign transactions that are "too big"
|
|
||||||
- require 2FA authentication before signing any transaction (NFC+web)
|
|
||||||
- velocity limits can restrict how often new transactions can be signed
|
|
||||||
- see `docs/spending-policy.md` for more details
|
|
||||||
- "Enable HSM" and "User Management" have moved into `Advanced > Spending Policy`.
|
|
||||||
- Old "CCC" feature has been renamed and moved into that menu as well: "Co-Sign Multisig"
|
|
||||||
- Added `Bull Bitcoin` export to `Export Wallet` menu.
|
|
||||||
- Enhancement: Added warning for zero value outputs if not `OP_RETURN`.
|
|
||||||
- Enhancement: Show QR codes of output addresses in transaction output explorer. Explorer is
|
|
||||||
now offered for transactions of all sizes, not just complex ones.
|
|
||||||
- Enhancement: Added file rename, when listing contents of SD card.
|
|
||||||
- Enhancement: Added ability to restore Coldcard backup via USB (needs latest of ckcc version)
|
|
||||||
- Enhancement: Address ownership allows to specify particular multisig wallet in which to search,
|
|
||||||
if `wallet` query parameter is provided via trivial extension to
|
|
||||||
[BIP-21](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki).
|
|
||||||
Example: `tb1q4d67p7stxml3kdudrgkg5mgaxsrgzcqzjrrj4gg62nxtvnsnvqjsxjkej0?wallet=Haystack`
|
|
||||||
- Bugfix: If all change outputs have `nValue=0`, they were not shown in UX.
|
|
||||||
- Bugfix: Disallow negative input/output amounts in PSBT.
|
|
||||||
- Bugfix: Fix filesystem initialization after Wipe LFS or Destroy Seed.
|
|
||||||
- Bugfix: Fix MicroSD selftest code.
|
|
||||||
- Bugfix: NFC loop exporting secrets would not work after first value exported.
|
|
||||||
- Bugfix: Multisig address format handling.
|
|
||||||
- Bugfix: Ownership check failing to find addresses near max (~760), needed to be re-run to succeed
|
|
||||||
- (Mk4 only) Bugfix: Part of extended keys (xpubs) were not always visible.
|
|
||||||
- (Mk4 only) Change: Mk4 default menu wrap-around lowered from 16 to 10 items.
|
|
||||||
|
|
||||||
|
|
||||||
## 5.4.3 - 2025-05-14
|
|
||||||
|
|
||||||
- Enhancement: Text word-wrap done more carefully so never cuts off any text, and yet
|
|
||||||
doesn't waste space.
|
|
||||||
- Bugfix: `Add current tmp` option, which could be shown in `Seed Vault` menu under
|
|
||||||
specific circumstances, would corrupt master settings if selected.
|
|
||||||
- Bugfix: PUSHDATA2 in bitcoin script caused yikes.
|
|
||||||
- Bugfix: Warning for unknown scripts was not shown at the top of the signing story.
|
|
||||||
- Bugfix: With both NFC & Virtual Disk OFF, user cannot exit `Export Wallet` menu. Gets stuck
|
|
||||||
in export loop and needs reboot to escape.
|
|
||||||
- Bugfix: Part of extended keys in stories were not always visible.
|
|
||||||
|
|
||||||
|
|
||||||
## 5.4.2 - 2025-04-16
|
## 5.4.2 - 2025-04-16
|
||||||
|
|
||||||
- Huge new feature: CCC - ColdCard Cosign
|
- Huge new feature: CCC - ColdCard Cosign
|
||||||
|
|||||||
@ -1,62 +1,5 @@
|
|||||||
*See ChangeLog.md for more recent changes, these are historic versions*
|
*See ChangeLog.md for more recent changes, these are historic versions*
|
||||||
|
|
||||||
|
|
||||||
## 1.3.5Q - 2025-11-03
|
|
||||||
|
|
||||||
- Enhancement: Address format guessing changed away from using PSBT XPUB's derivation paths.
|
|
||||||
Now based on witness/redeem script of first PSBT input instead.
|
|
||||||
- Enhancement: Show master XFP of backup secret and ask user for confirmation before loading backup.
|
|
||||||
- Enhancement: Show firmware version added to hobbled Advanced/Tools menu.
|
|
||||||
- Bugfix: Exiting text input of Custom Backup Password caused yikes.
|
|
||||||
- Bugfix: Temporary seeds in SSSP mode were not able to update block height.
|
|
||||||
- Enhancement: Show backup filename at the top of the screen during backup password entry.
|
|
||||||
|
|
||||||
|
|
||||||
## 1.3.4Q - 2025-09-30
|
|
||||||
|
|
||||||
- Spending policies for "Single Signers" adds new spending policy options:
|
|
||||||
- limit your Coldcard so it refuses to sign transactions that are "too big"
|
|
||||||
- require 2FA authentication before signing any transaction (NFC+web)
|
|
||||||
- velocity limits can restrict how often new transactions can be signed
|
|
||||||
- see `docs/spending-policy.md` for more details
|
|
||||||
- "Enable HSM" and "User Management" have moved into `Advanced > Spending Policy`.
|
|
||||||
- Old "CCC" feature has been renamed and moved into that menu as well: "Co-Sign Multisig"
|
|
||||||
- Added `Bull Bitcoin` export to `Export Wallet` menu.
|
|
||||||
- Enhancement: Added warning for zero value outputs if not `OP_RETURN`.
|
|
||||||
- Enhancement: Show QR codes of output addresses in transaction output explorer. Explorer is
|
|
||||||
now offered for transactions of all sizes, not just complex ones.
|
|
||||||
- Enhancement: Added file rename, when listing contents of SD card.
|
|
||||||
- Enhancement: Added ability to restore Coldcard backup via USB (needs latest of ckcc version)
|
|
||||||
- Enhancement: Address ownership allows to specify particular multisig wallet in which to search,
|
|
||||||
if `wallet` query parameter is provided via trivial extension to
|
|
||||||
[BIP-21](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki).
|
|
||||||
Example: `tb1q4d67p7stxml3kdudrgkg5mgaxsrgzcqzjrrj4gg62nxtvnsnvqjsxjkej0?wallet=Haystack`
|
|
||||||
- Bugfix: If all change outputs have `nValue=0`, they were not shown in UX.
|
|
||||||
- Bugfix: Disallow negative input/output amounts in PSBT.
|
|
||||||
- Bugfix: Fix filesystem initialization after Wipe LFS or Destroy Seed.
|
|
||||||
- Bugfix: Fix MicroSD selftest code.
|
|
||||||
- Bugfix: NFC loop exporting secrets would not work after first value exported.
|
|
||||||
- Bugfix: Multisig address format handling.
|
|
||||||
- Bugfix: Ownership check failing to find addresses near max (~760), needed to be re-run to succeed
|
|
||||||
- (Q only) Enhancement: Enters "forever calculator" mode when Q would otherwise be electronic waste
|
|
||||||
(ie. after 13 PIN failures). Always enabled, regardless of "login calculator" setting.
|
|
||||||
- (Q only) Bugfix: Correct line positioning when 24 seed words displayed.
|
|
||||||
|
|
||||||
|
|
||||||
## 1.3.3Q - 2025-05-14
|
|
||||||
|
|
||||||
- Enhancement: Text word-wrap done more carefully so never cuts off any text, and yet
|
|
||||||
doesn't waste space.
|
|
||||||
- Bugfix: `Add current tmp` option, which could be shown in `Seed Vault` menu under
|
|
||||||
specific circumstances, would corrupt master settings if selected.
|
|
||||||
- Bugfix: PUSHDATA2 in bitcoin script caused yikes.
|
|
||||||
- Bugfix: Warning for unknown scripts was not shown at the top of the signing story.
|
|
||||||
|
|
||||||
- Bugfix: Do not allow to teleport PSBTs from SD card when CC has no secrets.
|
|
||||||
- Bugfix: Calculator login mode: added "rand()" command, removed support
|
|
||||||
for variables/assignments.
|
|
||||||
|
|
||||||
|
|
||||||
## 1.3.2Q - 2025-04-16
|
## 1.3.2Q - 2025-04-16
|
||||||
|
|
||||||
- Feature: Key Teleport -- Easily and securely move seed phrases, secure notes/passwords,
|
- Feature: Key Teleport -- Easily and securely move seed phrases, secure notes/passwords,
|
||||||
|
|||||||
@ -2,73 +2,23 @@
|
|||||||
|
|
||||||
This lists the new changes that have not yet been published in a normal release.
|
This lists the new changes that have not yet been published in a normal release.
|
||||||
|
|
||||||
# Shared Improvements - Both Mk and Q
|
# Shared Improvements - Both Mk4 and Q
|
||||||
|
|
||||||
- Change: BIP-322 Proof of Reserves & message signing PSBT requires PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE
|
- Bugfix: If all change outputs have `nValue=0` they're not shown in UX
|
||||||
(read more [BIP-322 Proof of Reserves documentation](../docs/proof-of-reserves-bip-322.md) )
|
- Bugfix: Disallow negative input/output amounts in PSBT
|
||||||
- Enhancement: WIF Store export watch-only descriptor
|
- Enhancement: Add warning for zero value outputs if not OP_RETURNs
|
||||||
- Enhancement: WIF Store address detection without the need for PSBT_IN_BIP32_DERIVATION (Electrum support)
|
- Enhancement: Show QR codes of output addresses in Txn output explorer. Output explorer is offered for txns of all sizes.
|
||||||
- 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
|
# Mk4 Specific Changes
|
||||||
|
|
||||||
- tbd
|
## 5.4.? - 2025-06-
|
||||||
|
|
||||||
|
- Bugfix: Part of extended keys in stories were not always visible.
|
||||||
|
|
||||||
|
|
||||||
# Q Specific Changes
|
# Q Specific Changes
|
||||||
|
|
||||||
## 1.4.xQ - 2065-04-xx
|
## 1.3.?Q - 2025-06
|
||||||
|
|
||||||
|
|
||||||
- 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,29 +2,11 @@
|
|||||||
Hash: SHA256
|
Hash: SHA256
|
||||||
|
|
||||||
95eff9e044cdb6b3d00961ae72d450684d5441c6a3661ab550a3c3aa0882e754 README.md
|
95eff9e044cdb6b3d00961ae72d450684d5441c6a3661ab550a3c3aa0882e754 README.md
|
||||||
412597a0e30684400cb61ee04650c13ef9fc3dc16fc2570bd5e33a1dc0085d7a Next-ChangeLog.md
|
3ba92e73d5260656641828e962e8eae4590f59774150d14276818a5229daf734 Next-ChangeLog.md
|
||||||
72458ab9eb2872d263bf4d3f4ca0fbf0ff9c6186f08d27f13fd600cb511ed2a7 History-Q.md
|
0173cade759704320e7a43810dabd5f18cf2034b447c6c7996f447c8d3ad21de History-Q.md
|
||||||
d4891b509915800650a881556cca37604caab7a268afc0b1ed31021cea125891 History-Mk4.md
|
e6192bd7c2b27df7c9d8e58ae9a41bda4ef0615991c3159fb05ff60dc3cfedd1 History-Mk4.md
|
||||||
c8ad43b4e3f9d77777026da6d1210c6fc5cfe435bcfcd241c0f67c9392ad7b82 History-Mk3.md
|
c8ad43b4e3f9d77777026da6d1210c6fc5cfe435bcfcd241c0f67c9392ad7b82 History-Mk3.md
|
||||||
9ebab063b57ff07e5d8df20c266ac94736a6ad0e4c71ad1f1db46ec16b0c94be ChangeLog.md
|
6e8b95855e05dc7889b1476acfb1854107b4e8df6f12cdf4a643a9776e60c798 ChangeLog.md
|
||||||
b2fa9f4b9a9778b71cf4b09ad79732192fdb457214f868a3af5234094deea33f 2026-03-25T1408-v6.5.0X-mk-coldcard.dfu
|
|
||||||
f7bed9f1d2d49a35e7c53c8208e73ceaccbee2ab3e7fcd7c020fbd4923140313 2026-03-25T1407-v6.5.0QX-q1-coldcard.dfu
|
|
||||||
2b7b4d95cd5d606b0a32e692db7a27c1d860140c6e919e20ea6672ad6afc3088 2026-03-05T2052-v5.5.0-mk-coldcard.dfu
|
|
||||||
15f26aa0b8fe33e29e338b74acc52d5532922af56bcf486e38085de55c86b82a 2026-03-05T2052-v5.5.0-mk-coldcard-factory.dfu
|
|
||||||
b7ce3ba55ae4bb1e1ebe0090507bcada5a4439e57a19552c78da7ef103bd144c 2026-03-05T2051-v1.4.0Q-q1-coldcard.dfu
|
|
||||||
82c2ed5ee5cf75cc8af0f54839b9a1bc8e4a329174657d61861a6576fb6e31d9 2026-03-05T2051-v1.4.0Q-q1-coldcard-factory.dfu
|
|
||||||
372fa1f82e54f632574c56a695a1ed332464bf029bd733b2db2131a591d8f126 2025-11-25T1618-v6.4.1X-mk4-coldcard.dfu
|
|
||||||
1059560fb598e5e8fd6aed0164aa4cad166552bf8e47a0365e986429c9a15346 2025-11-25T1617-v6.4.1QX-q1-coldcard.dfu
|
|
||||||
f04617b52fc0db6e95cac0dddd9ddd90754219f38b63a26d08c848e208069edb 2025-11-20T1602-v6.4.0X-mk4-coldcard.dfu
|
|
||||||
371f13f3e1a5ef28d14933daf03820f0e51d26ffa96008dd5595da0dfac646cf 2025-11-20T1601-v6.4.0QX-q1-coldcard.dfu
|
|
||||||
7076ae29c509d3120db0fae434c132e6abd3fb79c1a2a2f1383ab3b2acaba27c 2025-11-03T1527-v5.4.5-mk4-coldcard.dfu
|
|
||||||
00a337888ff86bf875bcfdab7a734981bce29a49f94f3df9f932924765848ab0 2025-11-03T1527-v5.4.5-mk4-coldcard-factory.dfu
|
|
||||||
ff6371545943518eb4eb00ba73b6aa3a5ac4e63459621ecec8a300c28c281b3c 2025-11-03T1525-v1.3.5Q-q1-coldcard.dfu
|
|
||||||
0ce02c8e549cb67b682d621b4a628f3fba2c56350a9ab090b9f08532f49e7afa 2025-11-03T1525-v1.3.5Q-q1-coldcard-factory.dfu
|
|
||||||
8a8c94e5f64d0bfe4914a236fb8a779f956989fe8de998133b85b23920f46283 2025-09-30T1238-v5.4.4-mk4-coldcard.dfu
|
|
||||||
0d0aba89027d5127f74b2a2b777a7c592cba12903a3c4c3ce9b0e060c09dddb7 2025-09-30T1238-v5.4.4-mk4-coldcard-factory.dfu
|
|
||||||
bc9918968b67fefe634342c77513c9c354e7821e9ff002c7e5c8c356d7507892 2025-09-30T1237-v1.3.4Q-q1-coldcard.dfu
|
|
||||||
00cb1fc2ef360aacf48ba8c9dd2167b3f5c5f1241ba1b2b17d61ea1b7bff0a45 2025-09-30T1237-v1.3.4Q-q1-coldcard-factory.dfu
|
|
||||||
be166b3bb3ec2259991db998c20c3d44e88eeaa73c2b8114f31cb14cab5e66e6 2025-05-14T1344-v5.4.3-mk4-coldcard.dfu
|
be166b3bb3ec2259991db998c20c3d44e88eeaa73c2b8114f31cb14cab5e66e6 2025-05-14T1344-v5.4.3-mk4-coldcard.dfu
|
||||||
876932d4ea7634d268145d5bf45577c7198c9d60e8a271b5079faba4d4c91acd 2025-05-14T1344-v5.4.3-mk4-coldcard-factory.dfu
|
876932d4ea7634d268145d5bf45577c7198c9d60e8a271b5079faba4d4c91acd 2025-05-14T1344-v5.4.3-mk4-coldcard-factory.dfu
|
||||||
aaed0b90be5de310c8ac9f2d0cb3a7eea58923a53d349eb4b9ac8a902e5cba4e 2025-05-14T1343-v1.3.3Q-q1-coldcard.dfu
|
aaed0b90be5de310c8ac9f2d0cb3a7eea58923a53d349eb4b9ac8a902e5cba4e 2025-05-14T1343-v1.3.3Q-q1-coldcard.dfu
|
||||||
@ -127,12 +109,12 @@ f05bc8dfed047c0c0abe5ed60621d2d14899b10717221c4af0942d96a1754f33 2021-03-29T192
|
|||||||
bea27f263b524a66b3ed0a58c16805e98be0d7c3db20c2f7aab3238f2c6a6995 2019-12-19T1623-v3.0.6-coldcard.dfu
|
bea27f263b524a66b3ed0a58c16805e98be0d7c3db20c2f7aab3238f2c6a6995 2019-12-19T1623-v3.0.6-coldcard.dfu
|
||||||
-----BEGIN PGP SIGNATURE-----
|
-----BEGIN PGP SIGNATURE-----
|
||||||
|
|
||||||
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmnD9ncACgkQo6MbrVoq
|
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmgknkYACgkQo6MbrVoq
|
||||||
WxD9RAf+JkP/XVUPMDyfz+79AxBWFNU9r6RuYzXdzX3Z/XCKomZCZDtV7Ak6XlZi
|
WxBkuggAqTFP4YJdkzdNPbPDxtnCL4ZFJ+Rtnybp9JigTazbMvA/pjR+uODPFI3M
|
||||||
GTNfsUNHaPC8WP6smFzYg07NoY2U1fVdY7+qeOi7UXF0hBBDJw7Gsa49P2zmt+DB
|
Pm8I6kNPY8lMOPptEiFpNHn8EL8i2jOdH4NcmSP9OYInCRWyknm8fbmboSkOueAp
|
||||||
lfzivQG2n+mT4cM64Z0WF3BYBWmCuDJdctqUAnLJe2p8bh6S8n5hFeKqndRhffNK
|
SG3irwVXf/XWMMpBdXvALPPvttPzlVOLYowYnervDPiINiQDkd5jRP+Kd0AStVEt
|
||||||
773amkUrDW3RkHkIuevH4MQlR4ozWBmHzcehFDlTYT8BVLR8gg6hBzBEylxyDJNO
|
/QNq3ocmYHj4AUhJ5YSkyyVnnmGrZzKpcJ1q0XxXFCMJnyBrkjkJ60SgDx+ucy7c
|
||||||
Ld4W5SzsT6We0RGX2uOpMERDjkizqT9t5J63drzpuPrUQA8XVQPaOc07vpFHRbbZ
|
vTVk+W8QyLfqFkbhv4OT7YBITNGHEwk8sZ6V3N98r2/8Hx5PI42QOKEARYtOTpip
|
||||||
BhA61XO8yazNLVvata611pSTikNnDQ==
|
oj0LNnPFnAIkOTwZVazuc+vtG/GgSA==
|
||||||
=8Ti0
|
=IRUs
|
||||||
-----END PGP SIGNATURE-----
|
-----END PGP SIGNATURE-----
|
||||||
|
|||||||
@ -14,7 +14,7 @@ from ux import ux_show_story, the_ux, ux_confirm, ux_dramatic_pause, ux_aborted
|
|||||||
from ux import ux_enter_bip32_index, ux_input_text, import_export_prompt, OK, X, ux_render_words
|
from ux import ux_enter_bip32_index, ux_input_text, import_export_prompt, OK, X, ux_render_words
|
||||||
from export import export_contents, make_summary_file, make_descriptor_wallet_export
|
from export import export_contents, make_summary_file, make_descriptor_wallet_export
|
||||||
from export import make_bitcoin_core_wallet, generate_wasabi_wallet, generate_generic_export
|
from export import make_bitcoin_core_wallet, generate_wasabi_wallet, generate_generic_export
|
||||||
from export import generate_unchained_export, generate_electrum_wallet, make_key_expression_export
|
from export import generate_unchained_export, generate_electrum_wallet
|
||||||
from files import CardSlot, CardMissingError, needs_microsd
|
from files import CardSlot, CardMissingError, needs_microsd
|
||||||
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
|
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
|
||||||
from glob import settings
|
from glob import settings
|
||||||
@ -319,7 +319,7 @@ Press (6) to prove you read to the end of this message.''', title='WARNING', esc
|
|||||||
if ch == '6': break
|
if ch == '6': break
|
||||||
|
|
||||||
# do the actual picking
|
# do the actual picking
|
||||||
pin = await lll.get_new_pin()
|
pin = await lll.get_new_pin(title)
|
||||||
del lll
|
del lll
|
||||||
|
|
||||||
if pin is None: return
|
if pin is None: return
|
||||||
@ -459,27 +459,21 @@ async def pick_nickname(*a):
|
|||||||
# Value is not stored with normal settings, it's part of "prelogin" settings
|
# Value is not stored with normal settings, it's part of "prelogin" settings
|
||||||
# which are encrypted with zero-key.
|
# which are encrypted with zero-key.
|
||||||
s = SettingsObject.prelogin()
|
s = SettingsObject.prelogin()
|
||||||
k = "nick"
|
nick = s.get('nick', '')
|
||||||
nick = s.get(k, '')
|
|
||||||
|
|
||||||
if not nick:
|
if not nick:
|
||||||
ch = await ux_show_story("You can give this Coldcard a nickname"
|
ch = await ux_show_story('''\
|
||||||
" and it will be shown before login.")
|
You can give this Coldcard a nickname and it will be shown before login.''')
|
||||||
if ch != 'y': return
|
if ch != 'y': return
|
||||||
|
|
||||||
nn = await ux_input_text(nick, confirm_exit=False, prompt="Enter Nickname")
|
nn = await ux_input_text(nick, confirm_exit=False, prompt="Enter Nickname")
|
||||||
|
|
||||||
if nn is None or (nick == nn): return # user exit & same value - noop
|
|
||||||
|
|
||||||
from glob import dis
|
from glob import dis
|
||||||
dis.fullscreen("Saving...")
|
dis.fullscreen("Saving...")
|
||||||
dis.busy_bar(True)
|
dis.busy_bar(True)
|
||||||
|
|
||||||
if not nn:
|
nn = nn.strip() if nn else None
|
||||||
s.remove_key(k)
|
s.set('nick', nn)
|
||||||
else:
|
|
||||||
s.set(k, nn.strip())
|
|
||||||
|
|
||||||
s.save()
|
s.save()
|
||||||
dis.busy_bar(False)
|
dis.busy_bar(False)
|
||||||
del s
|
del s
|
||||||
@ -579,11 +573,8 @@ async def clear_seed(*a):
|
|||||||
# This is super dangerous for the customer's money.
|
# This is super dangerous for the customer's money.
|
||||||
import seed
|
import seed
|
||||||
|
|
||||||
# in hobble mode, they cannot reach duress wallets and/or maybe we don't
|
if await any_active_duress_ux():
|
||||||
# want to reveal them? So don't block them based on that.
|
return await ux_aborted()
|
||||||
if not pa.hobbled_mode:
|
|
||||||
if await any_active_duress_ux():
|
|
||||||
return await ux_aborted()
|
|
||||||
|
|
||||||
if not await ux_confirm('Wipe seed words and reset wallet. '
|
if not await ux_confirm('Wipe seed words and reset wallet. '
|
||||||
'All funds will be lost. '
|
'All funds will be lost. '
|
||||||
@ -596,7 +587,7 @@ async def clear_seed(*a):
|
|||||||
if not await ux_confirm('''Are you REALLY sure though???\n\n\
|
if not await ux_confirm('''Are you REALLY sure though???\n\n\
|
||||||
This action will certainly cause you to lose all funds associated with this wallet, \
|
This action will certainly cause you to lose all funds associated with this wallet, \
|
||||||
unless you have a backup of the seed words and know how to import them into a \
|
unless you have a backup of the seed words and know how to import them into a \
|
||||||
new wallet.''', 'AGAIN...', confirm_key='4'):
|
new wallet.''', confirm_key='4'):
|
||||||
return await ux_aborted()
|
return await ux_aborted()
|
||||||
|
|
||||||
# clear all trick PINs from SE2
|
# clear all trick PINs from SE2
|
||||||
@ -809,37 +800,26 @@ async def start_login_sequence():
|
|||||||
|
|
||||||
# If that didn't work, or no skip defined, force
|
# If that didn't work, or no skip defined, force
|
||||||
# them to login successfully.
|
# them to login successfully.
|
||||||
sp_unlock = False
|
|
||||||
try:
|
|
||||||
from trick_pins import tp
|
|
||||||
|
|
||||||
|
try:
|
||||||
# Get a PIN and try to use it to login
|
# Get a PIN and try to use it to login
|
||||||
# - does warnings about attempt usage counts
|
# - does warnings about attempt usage counts
|
||||||
await block_until_login()
|
await block_until_login()
|
||||||
|
|
||||||
sp_unlock = tp.was_sp_unlock()
|
|
||||||
if sp_unlock:
|
|
||||||
# Trying to unlock spending policy: ask for main PIN next.
|
|
||||||
await ux_show_story("Spending Policy Unlock: Please provide Main PIN next.")
|
|
||||||
pa.reset()
|
|
||||||
await block_until_login()
|
|
||||||
|
|
||||||
# we don't really know if that was the Main PIN (could easily be the bypass
|
|
||||||
# PIN again) and if it's a duress wallet, that's cool...
|
|
||||||
|
|
||||||
# Do we need to do countdown delay? (real or otherwise)
|
# Do we need to do countdown delay? (real or otherwise)
|
||||||
# - wiping has already occurred if that was selected by trick details
|
# Q/Mk4 approach:
|
||||||
|
# - wiping has already occured if that was picked
|
||||||
# - delay is variable, stored in tc_arg
|
# - delay is variable, stored in tc_arg
|
||||||
|
from trick_pins import tp
|
||||||
delay = tp.was_countdown_pin()
|
delay = tp.was_countdown_pin()
|
||||||
|
|
||||||
# Maybe they do know the right PIN, but always do a delay anyway, because they wanted that
|
# Maybe they do know the right PIN, but do a delay anyway, because they wanted that
|
||||||
if not delay:
|
if not delay:
|
||||||
delay = settings.get('lgto', 0)
|
delay = settings.get('lgto', 0)
|
||||||
|
|
||||||
if delay:
|
if delay:
|
||||||
# kill some time, with countdown, and get "the" PIN again for real login
|
# kill some time, with countdown, and get "the" PIN again for real login
|
||||||
pa.reset()
|
pa.reset()
|
||||||
|
|
||||||
await ux_login_countdown(delay * (60 if not version.is_devmode else 1))
|
await ux_login_countdown(delay * (60 if not version.is_devmode else 1))
|
||||||
|
|
||||||
# keep it simple for Mk4+: just challenge again for any PIN
|
# keep it simple for Mk4+: just challenge again for any PIN
|
||||||
@ -867,32 +847,14 @@ async def start_login_sequence():
|
|||||||
# handle upgrades/downgrade issues
|
# handle upgrades/downgrade issues
|
||||||
try:
|
try:
|
||||||
await version_migration()
|
await version_migration()
|
||||||
except: pass
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
# Maybe insist on the "right" microSD being already installed?
|
# Maybe insist on the "right" microSD being already installed?
|
||||||
try:
|
try:
|
||||||
from pwsave import MicroSD2FA
|
from pwsave import MicroSD2FA
|
||||||
MicroSD2FA.enforce_policy()
|
MicroSD2FA.enforce_policy()
|
||||||
except: pass
|
except: pass # robustness: keep going!
|
||||||
|
|
||||||
# apply the hobbling for the spending policy, if appropriate
|
|
||||||
try:
|
|
||||||
from ccc import sssp_spending_policy, sssp_word_challenge
|
|
||||||
|
|
||||||
if sp_unlock and sssp_spending_policy('words'):
|
|
||||||
# challenge them also for first and last seed word! (will reboot on fail)
|
|
||||||
await sssp_word_challenge()
|
|
||||||
dis.fullscreen("Startup...")
|
|
||||||
|
|
||||||
if sp_unlock:
|
|
||||||
# Disable spending policy going forward; user has to re-enable.
|
|
||||||
pa.hobbled_mode = False
|
|
||||||
sssp_spending_policy('en', set_value=False)
|
|
||||||
else:
|
|
||||||
# normal entry mode, but might have policy enabled, if so enable it now.
|
|
||||||
pa.hobbled_mode = sssp_spending_policy('en')
|
|
||||||
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
# implement idle timeout now that we are logged-in
|
# implement idle timeout now that we are logged-in
|
||||||
IMPT.start_task('idle', idle_logout())
|
IMPT.start_task('idle', idle_logout())
|
||||||
@ -938,14 +900,12 @@ async def start_login_sequence():
|
|||||||
settings.master_set("seedvault", False)
|
settings.master_set("seedvault", False)
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
|
if version.has_nfc and settings.get('nfc', 0):
|
||||||
from glob import hsm_active
|
|
||||||
if version.has_nfc and settings.get('nfc', 0) and not hsm_active:
|
|
||||||
# Maybe allow NFC now
|
# Maybe allow NFC now
|
||||||
import nfc
|
import nfc
|
||||||
nfc.NFCHandler.startup()
|
nfc.NFCHandler.startup()
|
||||||
|
|
||||||
if settings.get('vidsk', 0) and not hsm_active:
|
if settings.get('vidsk', 0):
|
||||||
# Maybe start virtual disk
|
# Maybe start virtual disk
|
||||||
import vdisk
|
import vdisk
|
||||||
vdisk.VirtDisk()
|
vdisk.VirtDisk()
|
||||||
@ -983,7 +943,7 @@ async def restore_main_secret(*a):
|
|||||||
goto_top_menu()
|
goto_top_menu()
|
||||||
|
|
||||||
def make_top_menu():
|
def make_top_menu():
|
||||||
from flow import VirginSystem, NormalSystem, EmptyWallet, FactoryMenu, HobbledTopMenu
|
from flow import VirginSystem, NormalSystem, EmptyWallet, FactoryMenu
|
||||||
from glob import hsm_active, settings
|
from glob import hsm_active, settings
|
||||||
from pincodes import pa
|
from pincodes import pa
|
||||||
|
|
||||||
@ -999,9 +959,7 @@ def make_top_menu():
|
|||||||
assert pa.is_successful(), "nonblank but wrong pin"
|
assert pa.is_successful(), "nonblank but wrong pin"
|
||||||
|
|
||||||
if pa.has_secrets():
|
if pa.has_secrets():
|
||||||
# let them do a few things, but not all the things, when "hobbled"
|
_cls = NormalSystem[:]
|
||||||
_cls = HobbledTopMenu[:] if pa.hobbled_mode else NormalSystem[:]
|
|
||||||
|
|
||||||
if pa.tmp_value or settings.get("hmx", False):
|
if pa.tmp_value or settings.get("hmx", False):
|
||||||
active_xfp = settings.get("xfp", 0)
|
active_xfp = settings.get("xfp", 0)
|
||||||
sl, sr = ("[", "]") if pa.tmp_value else ("<", ">")
|
sl, sr = ("[", "]") if pa.tmp_value else ("<", ">")
|
||||||
@ -1103,10 +1061,8 @@ async def export_xpub(label, _2, item):
|
|||||||
if ch == "2":
|
if ch == "2":
|
||||||
slip132 = not slip132
|
slip132 = not slip132
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if ch == '1':
|
if ch == '1':
|
||||||
acct = await ux_enter_bip32_index('Account Number:')
|
acct = await ux_enter_bip32_index('Account Number:') or 0
|
||||||
if acct is None: continue
|
|
||||||
pth_split = path.split("/")
|
pth_split = path.split("/")
|
||||||
pth_split[-1] = ("%dh" % acct)
|
pth_split[-1] = ("%dh" % acct)
|
||||||
path = "/".join(pth_split)
|
path = "/".join(pth_split)
|
||||||
@ -1132,32 +1088,29 @@ async def export_xpub(label, _2, item):
|
|||||||
await show_qr_code(xpub, False)
|
await show_qr_code(xpub, False)
|
||||||
|
|
||||||
|
|
||||||
def electrum_export_story(noun="Electrum", background=False):
|
def electrum_export_story(background=False):
|
||||||
# saves memory being in a function
|
# saves memory being in a function
|
||||||
return ('''\
|
return ('''\
|
||||||
This saves a skeleton %s wallet file. \
|
This saves a skeleton Electrum wallet file. \
|
||||||
You can then open that file in the wallet without ever connecting this Coldcard to a computer.\n
|
You can then open that file in Electrum without ever connecting this Coldcard to a computer.\n
|
||||||
''' % noun
|
'''
|
||||||
+ (background or 'Choose an address type for the wallet on the next screen.'+PICK_ACCOUNT)
|
+ (background or 'Choose an address type for the wallet on the next screen.'+PICK_ACCOUNT)
|
||||||
+ SENSITIVE_NOT_SECRET)
|
+ SENSITIVE_NOT_SECRET)
|
||||||
|
|
||||||
async def electrum_skeleton(a, b, item):
|
async def electrum_skeleton(*a):
|
||||||
# save xpub, and some other public details into a file: NOT MULTISIG
|
# save xpub, and some other public details into a file: NOT MULTISIG
|
||||||
title = item.arg
|
|
||||||
fname_pat = "new-%s.json" % title.lower()
|
|
||||||
|
|
||||||
ch = await ux_show_story(electrum_export_story(title), escape='1')
|
ch = await ux_show_story(electrum_export_story(), escape='1')
|
||||||
|
|
||||||
acct = 0
|
account_num = 0
|
||||||
if ch == '1':
|
if ch == '1':
|
||||||
acct = await ux_enter_bip32_index('Account Number:')
|
account_num = await ux_enter_bip32_index('Account Number:') or 0
|
||||||
|
elif ch != 'y':
|
||||||
if (ch not in '1y') or acct is None:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
rv = [
|
rv = [
|
||||||
MenuItem(chains.addr_fmt_label(af), f=electrum_skeleton_step2,
|
MenuItem(chains.addr_fmt_label(af), f=electrum_skeleton_step2,
|
||||||
arg=(af, acct, title, fname_pat))
|
arg=(af, account_num))
|
||||||
for af in chains.SINGLESIG_AF
|
for af in chains.SINGLESIG_AF
|
||||||
]
|
]
|
||||||
the_ux.push(MenuSystem(rv))
|
the_ux.push(MenuSystem(rv))
|
||||||
@ -1171,22 +1124,19 @@ def ss_descriptor_export_story(addition="", background="", acct=True):
|
|||||||
|
|
||||||
async def ss_descriptor_skeleton(_0, _1, item):
|
async def ss_descriptor_skeleton(_0, _1, item):
|
||||||
# Export of descriptor data (wallet)
|
# Export of descriptor data (wallet)
|
||||||
addition, f_pattern = "", "descriptor.txt"
|
int_ext, addition, f_pattern = None, "", "descriptor.txt"
|
||||||
int_ext = direct_way = None
|
|
||||||
allowed_af = chains.SINGLESIG_AF
|
allowed_af = chains.SINGLESIG_AF
|
||||||
if item.arg:
|
if item.arg:
|
||||||
int_ext, allowed_af, ll, f_pattern, direct_way = item.arg
|
int_ext, allowed_af, ll, f_pattern = item.arg
|
||||||
addition = " for " + ll
|
addition = " for " + ll
|
||||||
|
|
||||||
acct = 0
|
ch = await ux_show_story(ss_descriptor_export_story(addition), escape='1')
|
||||||
if not direct_way:
|
|
||||||
ch = await ux_show_story(ss_descriptor_export_story(addition), escape='1')
|
|
||||||
|
|
||||||
if ch == '1':
|
account_num = 0
|
||||||
acct = await ux_enter_bip32_index('Account Number:', unlimited=True)
|
if ch == '1':
|
||||||
|
account_num = await ux_enter_bip32_index('Account Number:', unlimited=True) or 0
|
||||||
if (ch not in '1y') or acct is None:
|
elif ch != 'y':
|
||||||
return
|
return
|
||||||
|
|
||||||
if int_ext is None:
|
if int_ext is None:
|
||||||
ch = await ux_show_story(
|
ch = await ux_show_story(
|
||||||
@ -1197,57 +1147,17 @@ async def ss_descriptor_skeleton(_0, _1, item):
|
|||||||
int_ext = False if ch == "1" else True
|
int_ext = False if ch == "1" else True
|
||||||
|
|
||||||
if len(allowed_af) == 1:
|
if len(allowed_af) == 1:
|
||||||
await make_descriptor_wallet_export(allowed_af[0], acct, int_ext=int_ext,
|
await make_descriptor_wallet_export(allowed_af[0], account_num,
|
||||||
fname_pattern=f_pattern, direct_way=direct_way)
|
int_ext=int_ext,
|
||||||
|
fname_pattern=f_pattern)
|
||||||
else:
|
else:
|
||||||
rv = [
|
rv = [
|
||||||
MenuItem(chains.addr_fmt_label(af), f=descriptor_skeleton_step2,
|
MenuItem(chains.addr_fmt_label(af), f=descriptor_skeleton_step2,
|
||||||
arg=(af, acct, int_ext, f_pattern, direct_way))
|
arg=(af, account_num, int_ext, f_pattern))
|
||||||
for af in allowed_af
|
for af in allowed_af
|
||||||
]
|
]
|
||||||
the_ux.push(MenuSystem(rv))
|
the_ux.push(MenuSystem(rv))
|
||||||
|
|
||||||
|
|
||||||
async def key_expression_skeleton_step2(_1, _2, item):
|
|
||||||
# pick a semi-random file name, render and save it.
|
|
||||||
orig_path, addr_fmt = item.arg
|
|
||||||
await make_key_expression_export(orig_path, addr_fmt)
|
|
||||||
|
|
||||||
async def key_expression_skeleton(_0, _1, item):
|
|
||||||
# Export key expression -> [xfp/d/e/r]xpub
|
|
||||||
|
|
||||||
acct = 0
|
|
||||||
ch = await ux_show_story("This saves a extended key expression."
|
|
||||||
+ PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape='1')
|
|
||||||
if ch == '1':
|
|
||||||
acct = await ux_enter_bip32_index('Account Number:', unlimited=True)
|
|
||||||
|
|
||||||
if (ch not in '1y') or acct is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
# element on 2nd index is address format for signed exports
|
|
||||||
# if multisig key use p2pkh
|
|
||||||
todo = [
|
|
||||||
("Segwit P2WPKH", "m/84h/%dh/%dh", AF_P2WPKH),
|
|
||||||
("Classic P2PKH", "m/44h/%dh/%dh", AF_CLASSIC),
|
|
||||||
("P2SH-Segwit", "m/49h/%dh/%dh", AF_P2WPKH_P2SH),
|
|
||||||
("Multi P2WSH", "m/48h/%dh/%dh/2h", AF_CLASSIC),
|
|
||||||
("Multi P2SH-P2WSH", "m/48h/%dh/%dh/1h", AF_CLASSIC),
|
|
||||||
]
|
|
||||||
|
|
||||||
from address_explorer import KeypathMenu
|
|
||||||
|
|
||||||
async def doit(*a):
|
|
||||||
return KeypathMenu(ranged=False, done_fn=make_key_expression_export)
|
|
||||||
|
|
||||||
ct = chains.current_chain().b44_cointype
|
|
||||||
|
|
||||||
rv = [ MenuItem(label, f=key_expression_skeleton_step2, arg=(orig_der % (ct, acct), af))
|
|
||||||
for label, orig_der, af in todo ]
|
|
||||||
rv += [ MenuItem("Custom Path", menu=doit) ]
|
|
||||||
|
|
||||||
the_ux.push(MenuSystem(rv))
|
|
||||||
|
|
||||||
async def samourai_post_mix_descriptor_export(*a):
|
async def samourai_post_mix_descriptor_export(*a):
|
||||||
name = "POST-MIX"
|
name = "POST-MIX"
|
||||||
post_mix_acct_num = 2147483646
|
post_mix_acct_num = 2147483646
|
||||||
@ -1277,9 +1187,9 @@ async def samourai_account_descriptor(name, account_num):
|
|||||||
|
|
||||||
async def descriptor_skeleton_step2(_1, _2, item):
|
async def descriptor_skeleton_step2(_1, _2, item):
|
||||||
# pick a semi-random file name, render and save it.
|
# pick a semi-random file name, render and save it.
|
||||||
addr_fmt, account_num, int_ext, f_pattern, dw = item.arg
|
addr_fmt, account_num, int_ext, f_pattern = item.arg
|
||||||
await make_descriptor_wallet_export(addr_fmt, account_num, int_ext=int_ext,
|
await make_descriptor_wallet_export(addr_fmt, account_num, int_ext=int_ext,
|
||||||
fname_pattern=f_pattern, direct_way=dw)
|
fname_pattern=f_pattern)
|
||||||
|
|
||||||
|
|
||||||
async def bitcoin_core_skeleton(*A):
|
async def bitcoin_core_skeleton(*A):
|
||||||
@ -1292,36 +1202,34 @@ You can then run the commands in Bitcoin Core's console window, \
|
|||||||
without ever connecting this Coldcard to a computer.\
|
without ever connecting this Coldcard to a computer.\
|
||||||
''' + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape='1')
|
''' + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape='1')
|
||||||
|
|
||||||
acct = 0
|
account_num = 0
|
||||||
if ch == '1':
|
if ch == '1':
|
||||||
acct = await ux_enter_bip32_index('Account Number:')
|
account_num = await ux_enter_bip32_index('Account Number:') or 0
|
||||||
|
elif ch != 'y':
|
||||||
if (ch not in '1y') or acct is None:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# no choices to be made, just do it.
|
# no choices to be made, just do it.
|
||||||
await make_bitcoin_core_wallet(acct)
|
await make_bitcoin_core_wallet(account_num)
|
||||||
|
|
||||||
|
|
||||||
async def electrum_skeleton_step2(_1, _2, item):
|
async def electrum_skeleton_step2(_1, _2, item):
|
||||||
# pick a semi-random file name, render and save it.
|
# pick a semi-random file name, render and save it.
|
||||||
addr_fmt, account_num, title, fname_pat = item.arg
|
addr_fmt, account_num = item.arg
|
||||||
await export_contents(title + " wallet",
|
await export_contents('Electrum wallet',
|
||||||
lambda: generate_electrum_wallet(addr_fmt, account_num),
|
lambda: generate_electrum_wallet(addr_fmt, account_num),
|
||||||
fname_pat, is_json=True)
|
"new-electrum.json", is_json=True)
|
||||||
|
|
||||||
async def _generic_export(prompt, label, f_pattern):
|
async def _generic_export(prompt, label, f_pattern):
|
||||||
# like the Multisig export, make a single JSON file with
|
# like the Multisig export, make a single JSON file with
|
||||||
# basically all useful XPUB's in it.
|
# basically all useful XPUB's in it.
|
||||||
ch = await ux_show_story(prompt + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape="1")
|
ch = await ux_show_story(prompt + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape="1")
|
||||||
acct = 0
|
account_num = 0
|
||||||
if ch == '1':
|
if ch == '1':
|
||||||
acct = await ux_enter_bip32_index('Account Number:')
|
account_num = await ux_enter_bip32_index('Account Number:') or 0
|
||||||
|
elif ch != 'y':
|
||||||
if (ch not in '1y') or acct is None:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
await export_contents(label, lambda: generate_generic_export(acct),
|
await export_contents(label, lambda: generate_generic_export(account_num),
|
||||||
f_pattern, is_json=True)
|
f_pattern, is_json=True)
|
||||||
|
|
||||||
async def generic_skeleton(*A):
|
async def generic_skeleton(*A):
|
||||||
@ -1366,17 +1274,16 @@ async def unchained_capital_export(*a):
|
|||||||
ch = await ux_show_story('''\
|
ch = await ux_show_story('''\
|
||||||
This saves multisig XPUB information required to setup on the Unchained platform. \
|
This saves multisig XPUB information required to setup on the Unchained platform. \
|
||||||
''' + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape="1")
|
''' + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape="1")
|
||||||
acct = 0
|
account_num = 0
|
||||||
if ch == '1':
|
if ch == '1':
|
||||||
acct = await ux_enter_bip32_index('Account Number:')
|
account_num = await ux_enter_bip32_index('Account Number:') or 0
|
||||||
|
elif ch != 'y':
|
||||||
if (ch not in '1y') or acct is None:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
xfp = xfp2str(settings.get('xfp', 0))
|
xfp = xfp2str(settings.get('xfp', 0))
|
||||||
fname = 'unchained-%s.json' % xfp
|
fname = 'unchained-%s.json' % xfp
|
||||||
|
|
||||||
await export_contents('Unchained', lambda: generate_unchained_export(acct),
|
await export_contents('Unchained', lambda: generate_unchained_export(account_num),
|
||||||
fname, is_json=True)
|
fname, is_json=True)
|
||||||
|
|
||||||
|
|
||||||
@ -1455,7 +1362,7 @@ async def import_xprv(_1, _2, item):
|
|||||||
else:
|
else:
|
||||||
# only get here if NFC was not chosen
|
# only get here if NFC was not chosen
|
||||||
# pick a likely-looking file.
|
# pick a likely-looking file.
|
||||||
fn = await file_picker(suffix='.txt', min_size=50, max_size=2000, taster=contains_xprv,
|
fn = await file_picker(suffix='txt', min_size=50, max_size=2000, taster=contains_xprv,
|
||||||
none_msg="Must contain " + label + ".", **choice)
|
none_msg="Must contain " + label + ".", **choice)
|
||||||
|
|
||||||
if not fn: return
|
if not fn: return
|
||||||
@ -1551,26 +1458,12 @@ async def wipe_filesystem(*A):
|
|||||||
Erase internal filesystem and rebuild it. Resets contents of internal flash area \
|
Erase internal filesystem and rebuild it. Resets contents of internal flash area \
|
||||||
used for settings, address search cache, and HSM config file. Does not affect funds, \
|
used for settings, address search cache, and HSM config file. Does not affect funds, \
|
||||||
or seed words but will reset settings used with other temporary seeds & BIP-39 passphrases. \
|
or seed words but will reset settings used with other temporary seeds & BIP-39 passphrases. \
|
||||||
Does not affect MicroSD card, if any.''', confirm_key="4"):
|
Does not affect MicroSD card, if any.'''):
|
||||||
return
|
return
|
||||||
|
|
||||||
from files import wipe_flash_filesystem
|
from files import wipe_flash_filesystem
|
||||||
wipe_flash_filesystem()
|
wipe_flash_filesystem()
|
||||||
|
|
||||||
async def nuke_device(*a):
|
|
||||||
if not await ux_confirm("Wipe Seed & Brick device? This will wipe the seed, purge"
|
|
||||||
" all related settings, and makes ewaste from this device."):
|
|
||||||
return
|
|
||||||
|
|
||||||
if not await ux_confirm("Brick device?\n\nBy design, there is no way to reset or recover"
|
|
||||||
" the secure element, and its contents become forever inaccessible.",
|
|
||||||
confirm_key="1"):
|
|
||||||
return
|
|
||||||
|
|
||||||
import callgate
|
|
||||||
callgate.fast_brick()
|
|
||||||
# NOT REACHED
|
|
||||||
|
|
||||||
async def wipe_vdisk(*A):
|
async def wipe_vdisk(*A):
|
||||||
if not await ux_confirm('''\
|
if not await ux_confirm('''\
|
||||||
Erases and reformats shared RAM disk. This is a secure erase that blanks every byte.'''):
|
Erases and reformats shared RAM disk. This is a secure erase that blanks every byte.'''):
|
||||||
@ -1625,7 +1518,7 @@ async def qr_share_file(_1, _2, item):
|
|||||||
# it's a txn, and we wrote as hex
|
# it's a txn, and we wrote as hex
|
||||||
data = data.decode()
|
data = data.decode()
|
||||||
else:
|
else:
|
||||||
assert data[1:4] == bytes(3)
|
assert data[2:8] == bytes(6)
|
||||||
data = b2a_hex(data).decode()
|
data = b2a_hex(data).decode()
|
||||||
elif data[0:5] == b'psbt\xff':
|
elif data[0:5] == b'psbt\xff':
|
||||||
tc = "P"
|
tc = "P"
|
||||||
@ -1755,40 +1648,30 @@ async def list_files(*A):
|
|||||||
from pincodes import pa
|
from pincodes import pa
|
||||||
|
|
||||||
digest = chk.digest()
|
digest = chk.digest()
|
||||||
path, basename = fn.rsplit('/', 1)
|
basename = fn.rsplit('/', 1)[-1]
|
||||||
msg_base = 'SHA256(%s)\n\n' + B2A(digest) + '\n\nPress (1) to rename file, '
|
msg_base = 'SHA256(%s)\n\n%s\n\nPress ' % (basename, B2A(digest))
|
||||||
escape = "61"
|
escape = "6"
|
||||||
if pa.has_secrets():
|
if pa.has_secrets():
|
||||||
msg_base += '(4) to sign file digest and export detached signature, '
|
msg_sign = '(4) to sign file digest and export detached signature, '
|
||||||
escape += "4"
|
escape += "4"
|
||||||
msg_base += '(6) to delete.'
|
else:
|
||||||
|
msg_sign = ""
|
||||||
|
msg_delete = '(6) to delete.'
|
||||||
|
msg = msg_base + msg_sign + msg_delete
|
||||||
while True:
|
while True:
|
||||||
ch = await ux_show_story(msg_base % basename, escape=escape)
|
ch = await ux_show_story(msg, escape=escape)
|
||||||
if ch == "x": break
|
if ch == "x": break
|
||||||
if ch in '461':
|
if ch in '46':
|
||||||
with CardSlot() as card:
|
with CardSlot() as card:
|
||||||
if ch == '6':
|
if ch == '6':
|
||||||
card.securely_blank_file(fn)
|
card.securely_blank_file(fn)
|
||||||
break
|
break
|
||||||
elif ch == '1':
|
|
||||||
new_basename = await ux_input_text(basename, max_len=32, min_len=3)
|
|
||||||
if new_basename:
|
|
||||||
try:
|
|
||||||
# prohibit both slashes and space in filenames
|
|
||||||
for s in "\/ ":
|
|
||||||
assert s not in new_basename, "illegal char"
|
|
||||||
uos.rename(path + "/" + basename, path + "/" + new_basename)
|
|
||||||
basename = new_basename
|
|
||||||
fn = path + "/" + basename # keep full path in sync (delete/sign use it)
|
|
||||||
except Exception as e:
|
|
||||||
await ux_show_story("Failed to rename the file. " + str(e),
|
|
||||||
title="Failure")
|
|
||||||
else:
|
else:
|
||||||
from msgsign import write_sig_file
|
from msgsign import write_sig_file
|
||||||
|
|
||||||
sig_nice = write_sig_file([(digest, fn)])
|
sig_nice = write_sig_file([(digest, fn)])
|
||||||
await ux_show_story("Signature file %s written." % sig_nice)
|
await ux_show_story("Signature file %s written." % sig_nice)
|
||||||
|
msg = msg_base + msg_delete
|
||||||
return
|
return
|
||||||
|
|
||||||
async def file_picker(suffix=None, min_size=1, max_size=1000000, taster=None,
|
async def file_picker(suffix=None, min_size=1, max_size=1000000, taster=None,
|
||||||
@ -1801,13 +1684,6 @@ async def file_picker(suffix=None, min_size=1, max_size=1000000, taster=None,
|
|||||||
# - escape: allow these chars to skip picking process
|
# - escape: allow these chars to skip picking process
|
||||||
# - slot_b: None=>pick slot w/ card in it, or A if both.
|
# - slot_b: None=>pick slot w/ card in it, or A if both.
|
||||||
# - allow_batch: adds an "all of the above" choice: ("menu label", menu_handler)
|
# - allow_batch: adds an "all of the above" choice: ("menu label", menu_handler)
|
||||||
# - suffix argument MUST contain the dot (.txt), if list of suffixes, all MUST
|
|
||||||
|
|
||||||
if suffix:
|
|
||||||
# actually make it a list of "suffixes"
|
|
||||||
if not isinstance(suffix, list):
|
|
||||||
suffix = [suffix]
|
|
||||||
assert all(s[0] == '.' for s in suffix)
|
|
||||||
|
|
||||||
if choices is None:
|
if choices is None:
|
||||||
choices = []
|
choices = []
|
||||||
@ -1821,13 +1697,13 @@ async def file_picker(suffix=None, min_size=1, max_size=1000000, taster=None,
|
|||||||
# ignore subdirs
|
# ignore subdirs
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if fn[0] == '.':
|
if suffix:
|
||||||
# unix-style hidden files
|
if not isinstance(suffix, list):
|
||||||
continue
|
suffix = [suffix]
|
||||||
|
if not any([fn.lower().endswith(s) for s in suffix]):
|
||||||
|
continue
|
||||||
|
|
||||||
if suffix and not any(fn.lower().endswith(s) for s in suffix):
|
if fn[0] == '.': continue
|
||||||
# wrong suffix, skip
|
|
||||||
continue
|
|
||||||
|
|
||||||
full_fname = path + '/' + fn
|
full_fname = path + '/' + fn
|
||||||
|
|
||||||
@ -1873,7 +1749,7 @@ async def file_picker(suffix=None, min_size=1, max_size=1000000, taster=None,
|
|||||||
if none_msg:
|
if none_msg:
|
||||||
msg += none_msg
|
msg += none_msg
|
||||||
if suffix:
|
if suffix:
|
||||||
msg += '\n\nThe filename must end in: ' + ' OR '.join(suffix)
|
msg += '\n\nThe filename must end in %r. ' % suffix
|
||||||
|
|
||||||
msg += '\n\nMaybe insert (another) SD card and try again?'
|
msg += '\n\nMaybe insert (another) SD card and try again?'
|
||||||
|
|
||||||
@ -1953,7 +1829,7 @@ async def _batch_sign(choices=None):
|
|||||||
return
|
return
|
||||||
assert isinstance(picked, dict)
|
assert isinstance(picked, dict)
|
||||||
|
|
||||||
choices = await file_picker(suffix='.psbt', min_size=50, ux=False,
|
choices = await file_picker(suffix='psbt', min_size=50, ux=False,
|
||||||
max_size=MAX_TXN_LEN, taster=is_psbt, **picked)
|
max_size=MAX_TXN_LEN, taster=is_psbt, **picked)
|
||||||
|
|
||||||
if not choices:
|
if not choices:
|
||||||
@ -1991,7 +1867,7 @@ async def ready2sign(*a):
|
|||||||
opt = {}
|
opt = {}
|
||||||
|
|
||||||
# just check if we have candidates, no UI
|
# just check if we have candidates, no UI
|
||||||
choices = await file_picker(suffix='.psbt', min_size=50, ux=False,
|
choices = await file_picker(suffix='psbt', min_size=50, ux=False,
|
||||||
max_size=MAX_TXN_LEN, taster=is_psbt)
|
max_size=MAX_TXN_LEN, taster=is_psbt)
|
||||||
|
|
||||||
if pa.tmp_value:
|
if pa.tmp_value:
|
||||||
@ -2018,7 +1894,7 @@ from your desktop wallet software or command line tools.'''
|
|||||||
title=title)
|
title=title)
|
||||||
if isinstance(picked, dict):
|
if isinstance(picked, dict):
|
||||||
opt = picked # reset options to what was chosen by user
|
opt = picked # reset options to what was chosen by user
|
||||||
choices = await file_picker(suffix='.psbt', min_size=50, ux=False,
|
choices = await file_picker(suffix='psbt', min_size=50, ux=False,
|
||||||
max_size=MAX_TXN_LEN, taster=is_psbt,
|
max_size=MAX_TXN_LEN, taster=is_psbt,
|
||||||
**opt)
|
**opt)
|
||||||
if not choices:
|
if not choices:
|
||||||
@ -2060,7 +1936,7 @@ async def sign_message_on_sd(*a):
|
|||||||
# min 1 line max 3 lines
|
# min 1 line max 3 lines
|
||||||
return 1 <= len(lines) <= 3
|
return 1 <= len(lines) <= 3
|
||||||
|
|
||||||
fn = await file_picker(suffix=['.txt', ".json"], min_size=2, max_size=500, taster=is_signable,
|
fn = await file_picker(suffix=['txt', "json"], min_size=2, max_size=500, taster=is_signable,
|
||||||
none_msg=('Must be txt file with one msg line, optionally '
|
none_msg=('Must be txt file with one msg line, optionally '
|
||||||
'followed by a subkey derivation path on a second line '
|
'followed by a subkey derivation path on a second line '
|
||||||
'and/or address format on third line. JSON msg signing '
|
'and/or address format on third line. JSON msg signing '
|
||||||
@ -2136,7 +2012,7 @@ Write it down.'''
|
|||||||
while 1:
|
while 1:
|
||||||
lll.reset()
|
lll.reset()
|
||||||
lll.subtitle = "New " + title
|
lll.subtitle = "New " + title
|
||||||
pin = await lll.get_new_pin()
|
pin = await lll.get_new_pin(title)
|
||||||
|
|
||||||
if pin is None:
|
if pin is None:
|
||||||
return await ux_aborted()
|
return await ux_aborted()
|
||||||
@ -2218,6 +2094,7 @@ Coldcard Firmware
|
|||||||
{rel}
|
{rel}
|
||||||
{built}
|
{built}
|
||||||
|
|
||||||
|
|
||||||
Bootloader:
|
Bootloader:
|
||||||
{bl}
|
{bl}
|
||||||
{chk}
|
{chk}
|
||||||
@ -2313,7 +2190,7 @@ async def wipe_address_cache(*a):
|
|||||||
async def wipe_ovc(*a):
|
async def wipe_ovc(*a):
|
||||||
ok = await ux_confirm('''Clear history of segwit UTXO input values we have seen already. \
|
ok = await ux_confirm('''Clear history of segwit UTXO input values we have seen already. \
|
||||||
This data protects you against specific attacks. Use this only if certain a false-positive \
|
This data protects you against specific attacks. Use this only if certain a false-positive \
|
||||||
has occurred in the detection logic.''')
|
has occured in the detection logic.''')
|
||||||
if not ok: return
|
if not ok: return
|
||||||
|
|
||||||
import history
|
import history
|
||||||
@ -2382,8 +2259,6 @@ async def change_seed_vault(is_enabled):
|
|||||||
|
|
||||||
async def change_which_chain(*a):
|
async def change_which_chain(*a):
|
||||||
# setting already changed, but reflect that value in other settings
|
# setting already changed, but reflect that value in other settings
|
||||||
from glob import dis
|
|
||||||
dis.fullscreen("Wait...")
|
|
||||||
try:
|
try:
|
||||||
# update xpub stored in settings
|
# update xpub stored in settings
|
||||||
import stash
|
import stash
|
||||||
@ -2415,23 +2290,9 @@ async def microsd_2fa(*a):
|
|||||||
|
|
||||||
async def keyboard_test(*a):
|
async def keyboard_test(*a):
|
||||||
# to aid keyboard testing/dev
|
# to aid keyboard testing/dev
|
||||||
if version.has_qwerty:
|
from ux import ux_input_text
|
||||||
await ux_input_text('', max_len=128, scan_ok=True, confirm_exit=False,
|
await ux_input_text('', max_len=128, scan_ok=True, confirm_exit=False,
|
||||||
prompt='Keyboard Test', placeholder='(type whatever)')
|
prompt='Keyboard Test', placeholder='(type whatever)')
|
||||||
else:
|
|
||||||
from ux_mk4 import ux_input_digits
|
|
||||||
await ux_input_digits('')
|
|
||||||
|
|
||||||
async def quick_nfc_test(*a):
|
|
||||||
from selftest import test_nfc
|
|
||||||
await test_nfc()
|
|
||||||
|
|
||||||
async def clear_tested_flag(*a):
|
|
||||||
# so can re-create first time power up in
|
|
||||||
# factory case (direct to selftest)
|
|
||||||
settings.remove_key('tested')
|
|
||||||
settings.save()
|
|
||||||
await reset_self()
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Q wrappers; these will be present, but are very short on mk4
|
# Q wrappers; these will be present, but are very short on mk4
|
||||||
@ -2447,11 +2308,7 @@ async def scan_any_qr(menu, label, item):
|
|||||||
async def _scan_any_qr(expect_secret=False, tmp=False):
|
async def _scan_any_qr(expect_secret=False, tmp=False):
|
||||||
from ux_q1 import QRScannerInteraction
|
from ux_q1 import QRScannerInteraction
|
||||||
x = QRScannerInteraction()
|
x = QRScannerInteraction()
|
||||||
try:
|
await x.scan_anything(expect_secret=expect_secret, tmp=tmp)
|
||||||
await x.scan_anything(expect_secret=expect_secret, tmp=tmp)
|
|
||||||
except Exception as e:
|
|
||||||
await ux_show_story(msg="Failed to import from QR.\n\n%s\n%s" % (e, problem_file_line(e)),
|
|
||||||
title="ERROR")
|
|
||||||
|
|
||||||
|
|
||||||
PUSHTX_SUPPLIERS = [
|
PUSHTX_SUPPLIERS = [
|
||||||
@ -2488,7 +2345,7 @@ async def pushtx_setup_menu(*a):
|
|||||||
"transaction will be immediately broadcast on the public network.\n\n"
|
"transaction will be immediately broadcast on the public network.\n\n"
|
||||||
"You must choose a provider by URL here, or give your own URL. "
|
"You must choose a provider by URL here, or give your own URL. "
|
||||||
"\n\nYour phone's IP address vs. transaction details could be linked by the service. "
|
"\n\nYour phone's IP address vs. transaction details could be linked by the service. "
|
||||||
"Make sure your phone is not in airplane mode. Requires NFC.",
|
"Requires NFC.",
|
||||||
title="PUSH TX",
|
title="PUSH TX",
|
||||||
)
|
)
|
||||||
if ch != "y":
|
if ch != "y":
|
||||||
|
|||||||
@ -30,10 +30,8 @@ def censor_address(addr):
|
|||||||
return addr[0:12] + '___' + addr[12+3:]
|
return addr[0:12] + '___' + addr[12+3:]
|
||||||
|
|
||||||
class KeypathMenu(MenuSystem):
|
class KeypathMenu(MenuSystem):
|
||||||
def __init__(self, path=None, nl=0, ranged=True, done_fn=None):
|
def __init__(self, path=None, nl=0):
|
||||||
self.prefix = None
|
self.prefix = None
|
||||||
self.done_fn = done_fn
|
|
||||||
self.ranged = ranged
|
|
||||||
|
|
||||||
if path is None:
|
if path is None:
|
||||||
# Top level menu; useful shortcuts, and special case just "m"
|
# Top level menu; useful shortcuts, and special case just "m"
|
||||||
@ -42,13 +40,10 @@ class KeypathMenu(MenuSystem):
|
|||||||
MenuItem("m/44h/⋯", f=self.deeper),
|
MenuItem("m/44h/⋯", f=self.deeper),
|
||||||
MenuItem("m/49h/⋯", f=self.deeper),
|
MenuItem("m/49h/⋯", f=self.deeper),
|
||||||
MenuItem("m/84h/⋯", f=self.deeper),
|
MenuItem("m/84h/⋯", f=self.deeper),
|
||||||
|
MenuItem("m/0/{idx}", menu=self.done),
|
||||||
|
MenuItem("m/{idx}", menu=self.done),
|
||||||
MenuItem("m", f=self.done),
|
MenuItem("m", f=self.done),
|
||||||
]
|
]
|
||||||
if self.ranged:
|
|
||||||
items += [
|
|
||||||
MenuItem("m/0/{idx}", menu=self.done),
|
|
||||||
MenuItem("m/{idx}", menu=self.done),
|
|
||||||
]
|
|
||||||
else:
|
else:
|
||||||
# drill down one layer: (nl) is the current leaf
|
# drill down one layer: (nl) is the current leaf
|
||||||
# - hardened choice first
|
# - hardened choice first
|
||||||
@ -58,14 +53,11 @@ class KeypathMenu(MenuSystem):
|
|||||||
MenuItem(p+"/⋯", menu=self.deeper),
|
MenuItem(p+"/⋯", menu=self.deeper),
|
||||||
MenuItem(p+"h", menu=self.done),
|
MenuItem(p+"h", menu=self.done),
|
||||||
MenuItem(p, menu=self.done),
|
MenuItem(p, menu=self.done),
|
||||||
|
MenuItem(p+"h/0/{idx}", menu=self.done),
|
||||||
|
MenuItem(p+"/0/{idx}", menu=self.done), #useful shortcut?
|
||||||
|
MenuItem(p+"h/{idx}", menu=self.done),
|
||||||
|
MenuItem(p+"/{idx}", menu=self.done),
|
||||||
]
|
]
|
||||||
if self.ranged:
|
|
||||||
items += [
|
|
||||||
MenuItem(p + "h/0/{idx}", menu=self.done),
|
|
||||||
MenuItem(p + "/0/{idx}", menu=self.done), # useful shortcut?
|
|
||||||
MenuItem(p + "h/{idx}", menu=self.done),
|
|
||||||
MenuItem(p + "/{idx}", menu=self.done),
|
|
||||||
]
|
|
||||||
|
|
||||||
# simple consistent truncation when needed
|
# simple consistent truncation when needed
|
||||||
max_wide = max(len(mi.label) for mi in items)
|
max_wide = max(len(mi.label) for mi in items)
|
||||||
@ -103,20 +95,17 @@ class KeypathMenu(MenuSystem):
|
|||||||
if isinstance(top, KeypathMenu):
|
if isinstance(top, KeypathMenu):
|
||||||
the_ux.pop()
|
the_ux.pop()
|
||||||
continue
|
continue
|
||||||
# assert isinstance(top, AddressListMenu), type(top)
|
assert isinstance(top, AddressListMenu)
|
||||||
break
|
break
|
||||||
|
|
||||||
if self.done_fn:
|
|
||||||
return await self.done_fn(final_path)
|
|
||||||
|
|
||||||
return PickAddrFmtMenu(final_path, top)
|
return PickAddrFmtMenu(final_path, top)
|
||||||
|
|
||||||
async def deeper(self, _1, _2, item):
|
async def deeper(self, _1, _2, item):
|
||||||
val = item.arg or item.label
|
val = item.arg or item.label
|
||||||
assert val.endswith('/⋯')
|
assert val.endswith('/⋯')
|
||||||
cpath = val[:-2]
|
cpath = val[:-2]
|
||||||
nl = await ux_enter_bip32_index('%s/' % cpath, unlimited=True, can_cancel=False)
|
nl = await ux_enter_bip32_index('%s/' % cpath, unlimited=True)
|
||||||
return KeypathMenu(cpath, nl, ranged=self.ranged, done_fn=self.done_fn)
|
return KeypathMenu(cpath, nl)
|
||||||
|
|
||||||
class PickAddrFmtMenu(MenuSystem):
|
class PickAddrFmtMenu(MenuSystem):
|
||||||
def __init__(self, path, parent):
|
def __init__(self, path, parent):
|
||||||
@ -126,10 +115,9 @@ class PickAddrFmtMenu(MenuSystem):
|
|||||||
for af in chains.SINGLESIG_AF
|
for af in chains.SINGLESIG_AF
|
||||||
]
|
]
|
||||||
super().__init__(items)
|
super().__init__(items)
|
||||||
# below is sensitive to order in chains.SINGLESIG_AF
|
if path.startswith("m/84h"):
|
||||||
if path.startswith("m/44h"):
|
|
||||||
self.goto_idx(1)
|
self.goto_idx(1)
|
||||||
elif path.startswith("m/49h"):
|
if path.startswith("m/49h"):
|
||||||
self.goto_idx(2)
|
self.goto_idx(2)
|
||||||
|
|
||||||
async def done(self, _1, _2, item):
|
async def done(self, _1, _2, item):
|
||||||
@ -242,15 +230,11 @@ class AddressListMenu(MenuSystem):
|
|||||||
self.goto_idx(axi)
|
self.goto_idx(axi)
|
||||||
|
|
||||||
async def change_account(self, *a):
|
async def change_account(self, *a):
|
||||||
acct = await ux_enter_bip32_index('Account Number:')
|
self.account_num = await ux_enter_bip32_index('Account Number:') or 0
|
||||||
if acct is None: return
|
|
||||||
self.account_num = acct
|
|
||||||
await self.render()
|
await self.render()
|
||||||
|
|
||||||
async def change_start_idx(self, *a):
|
async def change_start_idx(self, *a):
|
||||||
idx = await ux_enter_bip32_index("Start index:", unlimited=True)
|
self.start = await ux_enter_bip32_index("Start index:", unlimited=True)
|
||||||
if idx is None: return
|
|
||||||
self.start = idx
|
|
||||||
await self.render()
|
await self.render()
|
||||||
|
|
||||||
async def pick_single(self, _1, _2, item):
|
async def pick_single(self, _1, _2, item):
|
||||||
@ -445,12 +429,14 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
|
|||||||
+ ['Derivation (%d of %d)' % (i+1, ms_wallet.N) for i in range(ms_wallet.N)]
|
+ ['Derivation (%d of %d)' % (i+1, ms_wallet.N) for i in range(ms_wallet.N)]
|
||||||
) + '"\n'
|
) + '"\n'
|
||||||
|
|
||||||
# saver will be None if we don't think it worth saving these addresses
|
if (start == 0) and (n > 100) and change in (0, 1):
|
||||||
saver = OWNERSHIP.saver(ms_wallet, change, start, n)
|
saver = OWNERSHIP.saver(ms_wallet, change, start)
|
||||||
|
else:
|
||||||
|
saver = None
|
||||||
|
|
||||||
for (idx, addr, derivs, script) in ms_wallet.yield_addresses(start, n, change_idx=change):
|
for (idx, addr, derivs, script) in ms_wallet.yield_addresses(start, n, change_idx=change):
|
||||||
if saver:
|
if saver:
|
||||||
saver(addr, idx)
|
saver(addr)
|
||||||
|
|
||||||
# policy choice: never provide a complete multisig address to user.
|
# policy choice: never provide a complete multisig address to user.
|
||||||
addr = censor_address(addr)
|
addr = censor_address(addr)
|
||||||
@ -462,7 +448,7 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
|
|||||||
yield ln
|
yield ln
|
||||||
|
|
||||||
if saver:
|
if saver:
|
||||||
saver(None, 0) # close cache file
|
saver(None) # close file
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -470,18 +456,20 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
|
|||||||
from wallet import MasterSingleSigWallet
|
from wallet import MasterSingleSigWallet
|
||||||
main = MasterSingleSigWallet(addr_fmt, path, account_num)
|
main = MasterSingleSigWallet(addr_fmt, path, account_num)
|
||||||
|
|
||||||
# saver will be None if we don't think it worth saving these addresses
|
if n and (start == 0) and (n > 100) and change in (0, 1):
|
||||||
saver = OWNERSHIP.saver(main, change, start, n)
|
saver = OWNERSHIP.saver(main, change, start)
|
||||||
|
else:
|
||||||
|
saver = None
|
||||||
|
|
||||||
yield '"Index","Payment Address","Derivation"\n'
|
yield '"Index","Payment Address","Derivation"\n'
|
||||||
for (idx, addr, deriv) in main.yield_addresses(start, n, change_idx=change):
|
for (idx, addr, deriv) in main.yield_addresses(start, n, change_idx=change):
|
||||||
if saver:
|
if saver:
|
||||||
saver(addr, idx)
|
saver(addr)
|
||||||
|
|
||||||
yield '%d,"%s","%s"\n' % (idx, addr, deriv)
|
yield '%d,"%s","%s"\n' % (idx, addr, deriv)
|
||||||
|
|
||||||
if saver:
|
if saver:
|
||||||
saver(None, 0) # close cache file
|
saver(None) # close
|
||||||
|
|
||||||
async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num,
|
async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num,
|
||||||
start=0, count=250, change=0, **save_opts):
|
start=0, count=250, change=0, **save_opts):
|
||||||
@ -528,7 +516,7 @@ async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num,
|
|||||||
except CardMissingError:
|
except CardMissingError:
|
||||||
await needs_microsd()
|
await needs_microsd()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await ux_show_story('Failed to write!\n\n%s\n%s' % (e, problem_file_line(e)))
|
await ux_show_story('Failed to write!\n\n\n%s\n%s' % (e, problem_file_line(e)))
|
||||||
|
|
||||||
|
|
||||||
async def address_explore(*a):
|
async def address_explore(*a):
|
||||||
|
|||||||
568
shared/auth.py
568
shared/auth.py
@ -9,18 +9,15 @@ from ubinascii import hexlify as b2a_hex
|
|||||||
from ubinascii import unhexlify as a2b_hex
|
from ubinascii import unhexlify as a2b_hex
|
||||||
from uhashlib import sha256
|
from uhashlib import sha256
|
||||||
from public_constants import AFC_SCRIPT, AF_CLASSIC, AFC_BECH32, SUPPORTED_ADDR_FORMATS
|
from public_constants import AFC_SCRIPT, AF_CLASSIC, AFC_BECH32, SUPPORTED_ADDR_FORMATS
|
||||||
from public_constants import STXN_FINALIZE, STXN_VISUALIZE, STXN_SIGNED, AF_P2SH, AF_P2WPKH_P2SH
|
from public_constants import STXN_FINALIZE, STXN_VISUALIZE, STXN_SIGNED
|
||||||
from sffile import SFFile
|
from sffile import SFFile
|
||||||
from menu import MenuSystem, MenuItem
|
from ux import ux_show_story, abort_and_goto, ux_dramatic_pause, ux_clear_keys
|
||||||
from serializations import ser_uint256, SIGHASH_ALL
|
from ux import show_qr_code, OK, X, abort_and_push, AbortInteraction
|
||||||
from ux import ux_show_story, abort_and_goto, ux_dramatic_pause, ux_clear_keys, ux_confirm, the_ux
|
|
||||||
from ux import show_qr_code, OK, X, abort_and_push, AbortInteraction, ux_input_text, ux_enter_number
|
|
||||||
from usb import CCBusyError
|
from usb import CCBusyError
|
||||||
from utils import (HexWriter, xfp2str, problem_file_line, cleanup_deriv_path, B2A, node_from_privkey,
|
from utils import HexWriter, xfp2str, problem_file_line, cleanup_deriv_path, B2A, show_single_address
|
||||||
show_single_address, keypath_to_str, seconds2human_readable)
|
|
||||||
from psbt import psbtObject, FatalPSBTIssue, FraudulentChangeOutput
|
from psbt import psbtObject, FatalPSBTIssue, FraudulentChangeOutput
|
||||||
from files import CardSlot, CardMissingError
|
from files import CardSlot, CardMissingError
|
||||||
from exceptions import HSMDenied, QRTooBigError
|
from exceptions import HSMDenied
|
||||||
from version import MAX_TXN_LEN
|
from version import MAX_TXN_LEN
|
||||||
from charcodes import KEY_QR, KEY_NFC, KEY_ENTER, KEY_CANCEL, KEY_LEFT, KEY_RIGHT
|
from charcodes import KEY_QR, KEY_NFC, KEY_ENTER, KEY_CANCEL, KEY_LEFT, KEY_RIGHT
|
||||||
from msgsign import sign_message_digest
|
from msgsign import sign_message_digest
|
||||||
@ -131,7 +128,7 @@ Press %s to continue, otherwise %s to cancel.''' % (OK, X)
|
|||||||
|
|
||||||
class ApproveMessageSign(UserAuthorizedAction):
|
class ApproveMessageSign(UserAuthorizedAction):
|
||||||
def __init__(self, text, subpath, addr_fmt, approved_cb=None,
|
def __init__(self, text, subpath, addr_fmt, approved_cb=None,
|
||||||
msg_sign_request=None, allow_tab_nl=False, privkey=None):
|
msg_sign_request=None, only_printable=True):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
is_json = False
|
is_json = False
|
||||||
|
|
||||||
@ -141,23 +138,18 @@ class ApproveMessageSign(UserAuthorizedAction):
|
|||||||
text, subpath, addr_fmt, is_json = parse_msg_sign_request(msg_sign_request)
|
text, subpath, addr_fmt, is_json = parse_msg_sign_request(msg_sign_request)
|
||||||
|
|
||||||
self.text = validate_text_for_signing(
|
self.text = validate_text_for_signing(
|
||||||
text, allow_tab_nl=is_json and allow_tab_nl
|
text, only_printable=not is_json and only_printable
|
||||||
)
|
)
|
||||||
self.subpath = cleanup_deriv_path(subpath)
|
self.subpath = cleanup_deriv_path(subpath)
|
||||||
self.addr_fmt = chains.parse_addr_fmt_str(addr_fmt)
|
self.addr_fmt = chains.parse_addr_fmt_str(addr_fmt)
|
||||||
self.approved_cb = approved_cb
|
self.approved_cb = approved_cb
|
||||||
self.privkey = privkey
|
|
||||||
|
|
||||||
from glob import dis
|
from glob import dis
|
||||||
dis.fullscreen('Wait...')
|
dis.fullscreen('Wait...')
|
||||||
|
|
||||||
if self.privkey:
|
with stash.SensitiveValues() as sv:
|
||||||
node = node_from_privkey(self.privkey)
|
node = sv.derive_path(self.subpath)
|
||||||
self.address = chains.current_chain().address(node, self.addr_fmt)
|
self.address = sv.chain.address(node, self.addr_fmt)
|
||||||
else:
|
|
||||||
with stash.SensitiveValues() as sv:
|
|
||||||
node = sv.derive_path(self.subpath)
|
|
||||||
self.address = sv.chain.address(node, self.addr_fmt)
|
|
||||||
|
|
||||||
dis.progress_bar_show(1)
|
dis.progress_bar_show(1)
|
||||||
|
|
||||||
@ -178,8 +170,7 @@ class ApproveMessageSign(UserAuthorizedAction):
|
|||||||
else:
|
else:
|
||||||
# perform signing (progress bar shown)
|
# perform signing (progress bar shown)
|
||||||
digest = chains.current_chain().hash_message(self.text.encode())
|
digest = chains.current_chain().hash_message(self.text.encode())
|
||||||
self.result, _ = sign_message_digest(digest, self.subpath, "Signing...",
|
self.result, _ = sign_message_digest(digest, self.subpath, "Signing...", self.addr_fmt)
|
||||||
self.addr_fmt, pk=self.privkey)
|
|
||||||
|
|
||||||
if self.approved_cb:
|
if self.approved_cb:
|
||||||
# for micro sd case
|
# for micro sd case
|
||||||
@ -203,7 +194,7 @@ def sign_msg(text, subpath, addr_fmt):
|
|||||||
|
|
||||||
async def approve_msg_sign(text, subpath, addr_fmt, approved_cb=None,
|
async def approve_msg_sign(text, subpath, addr_fmt, approved_cb=None,
|
||||||
msg_sign_request=None, kill_menu=False,
|
msg_sign_request=None, kill_menu=False,
|
||||||
allow_tab_nl=False, privkey=None):
|
only_printable=True):
|
||||||
|
|
||||||
# Ask user if they want to sign some short text message.
|
# Ask user if they want to sign some short text message.
|
||||||
UserAuthorizedAction.cleanup()
|
UserAuthorizedAction.cleanup()
|
||||||
@ -213,8 +204,7 @@ async def approve_msg_sign(text, subpath, addr_fmt, approved_cb=None,
|
|||||||
text, subpath, addr_fmt,
|
text, subpath, addr_fmt,
|
||||||
approved_cb=approved_cb,
|
approved_cb=approved_cb,
|
||||||
msg_sign_request=msg_sign_request,
|
msg_sign_request=msg_sign_request,
|
||||||
allow_tab_nl=allow_tab_nl,
|
only_printable=only_printable,
|
||||||
privkey=privkey
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if kill_menu:
|
if kill_menu:
|
||||||
@ -235,6 +225,8 @@ async def sign_txt_file(filename):
|
|||||||
|
|
||||||
async def done(signature, address, text):
|
async def done(signature, address, text):
|
||||||
# complete. write out result
|
# complete. write out result
|
||||||
|
from glob import dis
|
||||||
|
|
||||||
orig_path, basename = filename.rsplit('/', 1)
|
orig_path, basename = filename.rsplit('/', 1)
|
||||||
orig_path += '/'
|
orig_path += '/'
|
||||||
base = basename.rsplit('.', 1)[0]
|
base = basename.rsplit('.', 1)[0]
|
||||||
@ -271,9 +263,8 @@ async def try_push_tx(data, txid, txn_sha=None):
|
|||||||
|
|
||||||
class ApproveTransaction(UserAuthorizedAction):
|
class ApproveTransaction(UserAuthorizedAction):
|
||||||
def __init__(self, psbt_len, flags=None, psbt_sha=None, input_method=None,
|
def __init__(self, psbt_len, flags=None, psbt_sha=None, input_method=None,
|
||||||
output_encoder=None, filename=None, offset=TXN_INPUT_OFFSET):
|
output_encoder=None, filename=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.offset = offset
|
|
||||||
self.psbt_len = psbt_len
|
self.psbt_len = psbt_len
|
||||||
|
|
||||||
# do finalize is None if not USB, None = decide based on is_complete
|
# do finalize is None if not USB, None = decide based on is_complete
|
||||||
@ -296,62 +287,52 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
# Pretty-print a transactions output.
|
# Pretty-print a transactions output.
|
||||||
# - expects CTxOut object
|
# - expects CTxOut object
|
||||||
# - gives user-visible string
|
# - gives user-visible string
|
||||||
# returns: tuple(ux_output_rendition, address_or_script_str_for_qr_display)
|
|
||||||
#
|
#
|
||||||
val = ' '.join(self.chain.render_value(o.nValue))
|
val = ' '.join(self.chain.render_value(o.nValue))
|
||||||
try:
|
try:
|
||||||
dest = self.chain.render_address(o.scriptPubKey)
|
dest = self.chain.render_address(o.scriptPubKey)
|
||||||
# known script types are short enough that we can display QR on both hw versions
|
|
||||||
return '%s\n - to address -\n%s\n' % (val, show_single_address(dest)), dest
|
return '%s\n - to address -\n%s\n' % (val, show_single_address(dest)), dest
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Handle future things better: allow them to happen at least.
|
|
||||||
# sending to some unknown script, possibly very long
|
|
||||||
# but full-show required for verification
|
|
||||||
# OP_RETURN dest contains also OP_RETURN itself (for PSBT qr explorer)
|
|
||||||
dest = B2A(o.scriptPubKey)
|
|
||||||
|
|
||||||
# check for OP_RETURN
|
# check for OP_RETURN
|
||||||
data = self.chain.op_return(o.scriptPubKey)
|
data = self.chain.op_return(o.scriptPubKey)
|
||||||
# In UX story only data are shown as OP_RETURN is part of base msg
|
if data is not None:
|
||||||
if data is None:
|
|
||||||
rv = '%s\n - to script -\n%s\n' % (val, dest)
|
|
||||||
else:
|
|
||||||
base = '%s\n - OP_RETURN -\n%s'
|
base = '%s\n - OP_RETURN -\n%s'
|
||||||
if not data:
|
if not data:
|
||||||
dest = ""
|
return base % (val, "null-data\n"), ""
|
||||||
rv = base % (val, "null-data\n")
|
|
||||||
else:
|
else:
|
||||||
data_ascii = None
|
data_ascii = None
|
||||||
if len(data) > 160:
|
if len(data) > 200:
|
||||||
# completely arbitrary limit, prevents huge stories
|
# completely arbitrary limit, prevents huge stories
|
||||||
# anchor data are not relevant for verification - can be hidden
|
data_hex = b2a_hex(data[:100]).decode() + "\n ⋯\n" + b2a_hex(data[-100:]).decode()
|
||||||
ss = b2a_hex(data[:80]).decode() + "\n ⋯\n" + b2a_hex(data[-80:]).decode()
|
|
||||||
# but we show empty QR in txn explorer for these big, modified data
|
|
||||||
else:
|
else:
|
||||||
ss = b2a_hex(data).decode()
|
data_hex = b2a_hex(data).decode()
|
||||||
if (min(data) >= 32) and (max(data) < 127): # printable & not huge
|
if (min(data) >= 32) and (max(data) < 127): # printable & not huge
|
||||||
try:
|
try:
|
||||||
data_ascii = data.decode("ascii")
|
data_ascii = data.decode("ascii")
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
rv = base % (val, ss)
|
to_ret = base % (val, data_hex)
|
||||||
if data_ascii:
|
if data_ascii:
|
||||||
rv += " (ascii: %s)" % data_ascii
|
to_ret += " (ascii: %s)" % data_ascii
|
||||||
rv += "\n"
|
return to_ret + "\n", data_hex
|
||||||
|
|
||||||
return rv, dest
|
# Handle future things better: allow them to happen at least.
|
||||||
|
dest = B2A(o.scriptPubKey)
|
||||||
|
|
||||||
|
return '%s\n - to script -\n%s\n' % (val, dest), dest
|
||||||
|
|
||||||
async def interact(self):
|
async def interact(self):
|
||||||
# Prompt user w/ details and get approval
|
# Prompt user w/ details and get approval
|
||||||
from glob import dis, hsm_active
|
from glob import dis, hsm_active
|
||||||
from ccc import CCCFeature, SSSPFeature
|
from ccc import CCCFeature
|
||||||
|
|
||||||
# step 1: parse PSBT from PSRAM into in-memory objects.
|
# step 1: parse PSBT from PSRAM into in-memory objects.
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with SFFile(self.offset, length=self.psbt_len, message='Reading...') as fd:
|
with SFFile(TXN_INPUT_OFFSET, length=self.psbt_len, message='Reading...') as fd:
|
||||||
# NOTE: psbtObject captures the file descriptor and uses it later
|
# NOTE: psbtObject captures the file descriptor and uses it later
|
||||||
self.psbt = psbtObject.read_psbt(fd)
|
self.psbt = psbtObject.read_psbt(fd)
|
||||||
except BaseException as exc:
|
except BaseException as exc:
|
||||||
@ -370,14 +351,13 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
await self.psbt.validate() # might do UX: accept multisig import
|
await self.psbt.validate() # might do UX: accept multisig import
|
||||||
dis.progress_sofar(10, 100)
|
dis.progress_sofar(10, 100)
|
||||||
|
|
||||||
if not self.psbt.wif_store:
|
# consider_keys only needs num_our_keys to be set
|
||||||
self.psbt.consider_keys()
|
# it set during psbt.validate()
|
||||||
|
self.psbt.consider_keys()
|
||||||
dis.progress_sofar(20, 100)
|
dis.progress_sofar(20, 100)
|
||||||
|
|
||||||
ccc_c_xfp = CCCFeature.get_xfp() # can be None
|
ccc_c_xfp = CCCFeature.get_xfp() # can be None
|
||||||
self.psbt.consider_inputs(cosign_xfp=ccc_c_xfp)
|
self.psbt.consider_inputs(cosign_xfp=ccc_c_xfp)
|
||||||
if self.psbt.wif_store:
|
|
||||||
self.psbt.consider_keys()
|
|
||||||
dis.progress_sofar(50, 100)
|
dis.progress_sofar(50, 100)
|
||||||
|
|
||||||
self.psbt.consider_outputs()
|
self.psbt.consider_outputs()
|
||||||
@ -407,13 +387,7 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
|
|
||||||
# early test for spending policy; not an error if violates policy
|
# early test for spending policy; not an error if violates policy
|
||||||
# - might add warnings
|
# - might add warnings
|
||||||
could_ccc_sign, ccc_needs_2fa = CCCFeature.could_cosign(self.psbt)
|
could_ccc_sign, needs_2fa = CCCFeature.could_sign(self.psbt)
|
||||||
|
|
||||||
# test for allowing any signature when in single-signer mode
|
|
||||||
# - but CCC will override it.
|
|
||||||
should_block, ss_needs_2fa = SSSPFeature.can_allow(self.psbt)
|
|
||||||
if should_block and not could_ccc_sign:
|
|
||||||
return await self.failure('Spending Policy violation.')
|
|
||||||
|
|
||||||
# step 2: figure out what we are approving, so we can get sign-off
|
# step 2: figure out what we are approving, so we can get sign-off
|
||||||
# - outputs, amounts
|
# - outputs, amounts
|
||||||
@ -428,7 +402,6 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
#
|
#
|
||||||
try:
|
try:
|
||||||
msg = uio.StringIO()
|
msg = uio.StringIO()
|
||||||
is_por = self.psbt.por322 and (self.psbt.num_inputs > 1)
|
|
||||||
|
|
||||||
# mention warning at top
|
# mention warning at top
|
||||||
wl= len(self.psbt.warnings)
|
wl= len(self.psbt.warnings)
|
||||||
@ -437,41 +410,27 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
elif wl >= 2:
|
elif wl >= 2:
|
||||||
msg.write('(%d warnings below)\n\n' % wl)
|
msg.write('(%d warnings below)\n\n' % wl)
|
||||||
|
|
||||||
if self.psbt.por322:
|
if self.psbt.consolidation_tx:
|
||||||
msg.write("%s\n\n" % ("Proof of Reserves" if is_por else "BIP-322 Message"))
|
# consolidating txn that doesn't change balance of account.
|
||||||
msg.write("Message:\n%s\n\n" % self.psbt.por322_msg)
|
msg.write("Consolidating %s %s\nwithin wallet.\n\n" %
|
||||||
if is_por:
|
self.chain.render_value(self.psbt.total_value_out))
|
||||||
msg.write("Amount %s %s\n\n" % self.chain.render_value(self.psbt.total_value_in))
|
|
||||||
try:
|
|
||||||
addr = self.chain.render_address(self.psbt.por322_msg_challenge)
|
|
||||||
msg.write("Challenge Address:\n%s\n\n" % show_single_address(addr))
|
|
||||||
except ValueError:
|
|
||||||
msg.write("Message Challenge:\n%s\n\n" % b2a_hex(self.psbt.por322_msg_challenge).decode())
|
|
||||||
else:
|
else:
|
||||||
if self.psbt.consolidation_tx:
|
msg.write("Sending %s %s\n" % self.chain.render_value(
|
||||||
# consolidating txn that doesn't change balance of account.
|
self.psbt.total_value_out - self.psbt.total_change_value))
|
||||||
msg.write("Consolidating %s %s\nwithin wallet.\n\n" %
|
|
||||||
self.chain.render_value(self.psbt.total_value_out))
|
|
||||||
else:
|
|
||||||
msg.write("Sending %s %s\n" % self.chain.render_value(
|
|
||||||
self.psbt.total_value_out - self.psbt.total_change_value))
|
|
||||||
|
|
||||||
fee = self.psbt.calculate_fee()
|
fee = self.psbt.calculate_fee()
|
||||||
if fee is not None:
|
if fee is not None:
|
||||||
msg.write("Network fee %s %s\n\n" % self.chain.render_value(fee))
|
msg.write("Network fee %s %s\n\n" % self.chain.render_value(fee))
|
||||||
|
|
||||||
if not self.psbt.por322 or is_por:
|
msg.write(" %d %s\n %d %s\n\n" % (
|
||||||
msg.write(" %d %s\n %d %s\n\n" % (
|
self.psbt.num_inputs,
|
||||||
self.psbt.num_inputs,
|
"input" if self.psbt.num_inputs == 1 else "inputs",
|
||||||
"input" if self.psbt.num_inputs == 1 else "inputs",
|
self.psbt.num_outputs,
|
||||||
self.psbt.num_outputs,
|
"output" if self.psbt.num_outputs == 1 else "outputs",
|
||||||
"output" if self.psbt.num_outputs == 1 else "outputs",
|
))
|
||||||
))
|
|
||||||
|
|
||||||
if not self.psbt.por322:
|
|
||||||
# outputs + change story created here
|
|
||||||
self.output_summary_text(msg)
|
|
||||||
|
|
||||||
|
# outputs + change story created here
|
||||||
|
self.output_summary_text(msg)
|
||||||
gc.collect()
|
gc.collect()
|
||||||
|
|
||||||
if self.psbt.ux_notes:
|
if self.psbt.ux_notes:
|
||||||
@ -499,23 +458,17 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
|
|
||||||
if not hsm_active:
|
if not hsm_active:
|
||||||
esc = "2"
|
esc = "2"
|
||||||
noun = "transaction"
|
msg.write("Press %s to approve and sign transaction."
|
||||||
if self.psbt.por322:
|
" Press (2) to explore txn outputs." % OK)
|
||||||
noun = "proof of reserves" if is_por else "message"
|
|
||||||
|
|
||||||
msg.write("Press %s to approve and sign %s."
|
|
||||||
" Press (2) to explore transaction." % (OK, noun))
|
|
||||||
|
|
||||||
if (self.input_method == "sd") and CardSlot.both_inserted():
|
if (self.input_method == "sd") and CardSlot.both_inserted():
|
||||||
esc += "b"
|
esc += "b"
|
||||||
msg.write(" (B) to write to lower SD slot.")
|
msg.write(" (B) to write to lower SD slot.")
|
||||||
msg.write(" %s to abort." % X)
|
msg.write(" %s to abort." % X)
|
||||||
|
|
||||||
title = "OK TO %s?" % ("SIGN" if self.psbt.por322 else "SEND")
|
|
||||||
while True:
|
while True:
|
||||||
ch = await ux_show_story(msg, title=title, escape=esc)
|
ch = await ux_show_story(msg, title="OK TO SEND?", escape=esc)
|
||||||
if ch == "2":
|
if ch == "2":
|
||||||
await TXExplorer.start(self)
|
await self.txn_explorer()
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
msg.close()
|
msg.close()
|
||||||
@ -547,7 +500,7 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
self.done()
|
self.done()
|
||||||
return
|
return
|
||||||
|
|
||||||
if ccc_needs_2fa and could_ccc_sign:
|
if needs_2fa and could_ccc_sign:
|
||||||
# They still need to pass web2fa challenge (but it meets other specs ok)
|
# They still need to pass web2fa challenge (but it meets other specs ok)
|
||||||
try:
|
try:
|
||||||
await CCCFeature.web2fa_challenge()
|
await CCCFeature.web2fa_challenge()
|
||||||
@ -557,13 +510,6 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
if ch2 != 'y':
|
if ch2 != 'y':
|
||||||
return await self.failure("2FA Failed")
|
return await self.failure("2FA Failed")
|
||||||
|
|
||||||
elif ss_needs_2fa:
|
|
||||||
# Need 2FA for single-sig case .. refuse to sign if it fails.
|
|
||||||
try:
|
|
||||||
await SSSPFeature.web2fa_challenge()
|
|
||||||
except:
|
|
||||||
return await self.failure("2FA Failed")
|
|
||||||
|
|
||||||
# do the actual signing.
|
# do the actual signing.
|
||||||
try:
|
try:
|
||||||
dis.fullscreen('Wait...')
|
dis.fullscreen('Wait...')
|
||||||
@ -571,15 +517,10 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
self.psbt.sign_it()
|
self.psbt.sign_it()
|
||||||
|
|
||||||
if could_ccc_sign:
|
if could_ccc_sign:
|
||||||
# this is where the CCC co-signing happens.
|
dis.fullscreen('CCC Sign...')
|
||||||
dis.fullscreen('Co-Signing...')
|
|
||||||
gc.collect()
|
gc.collect()
|
||||||
CCCFeature.sign_psbt(self.psbt)
|
CCCFeature.sign_psbt(self.psbt)
|
||||||
|
|
||||||
if SSSPFeature.is_enabled():
|
|
||||||
# update SSSP block_h even if SSSP blocks and overridden by CCC
|
|
||||||
SSSPFeature.update_last_signed(self.psbt)
|
|
||||||
|
|
||||||
except FraudulentChangeOutput as exc:
|
except FraudulentChangeOutput as exc:
|
||||||
return await self.failure(exc.args[0], title='Change Fraud')
|
return await self.failure(exc.args[0], title='Change Fraud')
|
||||||
except MemoryError:
|
except MemoryError:
|
||||||
@ -589,9 +530,8 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
return await self.failure("Signing failed late", exc)
|
return await self.failure("Signing failed late", exc)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await done_signing(self.psbt, self, self.input_method,
|
await done_signing(self.psbt, self, self.input_method, self.filename, self.output_encoder,
|
||||||
self.filename, self.output_encoder,
|
slot_b=True if ch == "b" else False, finalize=self.do_finalize)
|
||||||
slot_b=(ch == "b"), finalize=self.do_finalize)
|
|
||||||
self.done()
|
self.done()
|
||||||
except AbortInteraction:
|
except AbortInteraction:
|
||||||
# user might have sent new sign cmd, while we still at export prompt
|
# user might have sent new sign cmd, while we still at export prompt
|
||||||
@ -600,6 +540,73 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
# sys.print_exception(exc)
|
# sys.print_exception(exc)
|
||||||
return await self.failure("PSBT output failed", exc)
|
return await self.failure("PSBT output failed", exc)
|
||||||
|
|
||||||
|
|
||||||
|
async def txn_explorer(self):
|
||||||
|
# Page through unlimited-sized transaction details
|
||||||
|
# - shows all outputs (including change): their address and amounts.
|
||||||
|
from glob import dis
|
||||||
|
|
||||||
|
def make_msg(offset, count):
|
||||||
|
dis.fullscreen('Wait...')
|
||||||
|
rv = ""
|
||||||
|
end = min(offset + count, self.psbt.num_outputs)
|
||||||
|
addrs = []
|
||||||
|
change = []
|
||||||
|
for i, (idx, out) in enumerate(self.psbt.output_iter(offset, end)):
|
||||||
|
outp = self.psbt.outputs[idx]
|
||||||
|
item = "Output %d%s:\n\n" % (idx, " (change)" if outp.is_change else "")
|
||||||
|
msg, addr_or_script = self.render_output(out)
|
||||||
|
item += msg
|
||||||
|
addrs.append(addr_or_script)
|
||||||
|
if outp.is_change:
|
||||||
|
change.append(i)
|
||||||
|
item += "\n"
|
||||||
|
rv += item
|
||||||
|
dis.progress_sofar(idx-offset+1, count)
|
||||||
|
|
||||||
|
rv += 'Press RIGHT to see next group'
|
||||||
|
if offset:
|
||||||
|
rv += ', LEFT to go back'
|
||||||
|
|
||||||
|
if not version.has_qwerty:
|
||||||
|
# Q has hint key
|
||||||
|
rv += ", (4) to show QR code"
|
||||||
|
rv += ('. %s to quit.' % X)
|
||||||
|
|
||||||
|
return rv, addrs, change, end
|
||||||
|
|
||||||
|
start = 0
|
||||||
|
n = 10
|
||||||
|
msg, addrs, change, end = make_msg(start, n)
|
||||||
|
while True:
|
||||||
|
ch = await ux_show_story(msg, title="%d-%d" % (start, end-1),
|
||||||
|
escape='479'+KEY_RIGHT+KEY_LEFT+KEY_QR,
|
||||||
|
hint_icons=KEY_QR)
|
||||||
|
if ch == 'x':
|
||||||
|
del msg
|
||||||
|
return
|
||||||
|
elif ch in "4"+KEY_QR:
|
||||||
|
from ux import show_qr_codes
|
||||||
|
await show_qr_codes(addrs, False, start, is_addrs=True, change_idxs=change)
|
||||||
|
continue
|
||||||
|
elif (ch in KEY_LEFT+"7"):
|
||||||
|
if (start - n) < 0:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# go backwards in explorer
|
||||||
|
start -= n
|
||||||
|
elif (ch in KEY_RIGHT+"9"):
|
||||||
|
if (start + n) >= self.psbt.num_outputs:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# go forwards
|
||||||
|
start += n
|
||||||
|
else:
|
||||||
|
# nothing changed - do not recalc msg
|
||||||
|
continue
|
||||||
|
|
||||||
|
msg, addrs, change, end = make_msg(start, n)
|
||||||
|
|
||||||
async def save_visualization(self, msg, sign_text=False):
|
async def save_visualization(self, msg, sign_text=False):
|
||||||
# write story text out, maybe signing it as we go
|
# write story text out, maybe signing it as we go
|
||||||
# - return length and checksum
|
# - return length and checksum
|
||||||
@ -651,8 +658,7 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
has_change = True
|
has_change = True
|
||||||
total_change += tx_out.nValue
|
total_change += tx_out.nValue
|
||||||
if len(largest_change) < MAX_VISIBLE_CHANGE:
|
if len(largest_change) < MAX_VISIBLE_CHANGE:
|
||||||
_, addr = self.render_output(tx_out)
|
largest_change.append((tx_out.nValue, self.chain.render_address(tx_out.scriptPubKey)))
|
||||||
largest_change.append((tx_out.nValue, addr))
|
|
||||||
if len(largest_change) == MAX_VISIBLE_CHANGE:
|
if len(largest_change) == MAX_VISIBLE_CHANGE:
|
||||||
largest_change = sorted(largest_change, key=lambda x: x[0], reverse=True)
|
largest_change = sorted(largest_change, key=lambda x: x[0], reverse=True)
|
||||||
continue
|
continue
|
||||||
@ -677,9 +683,12 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
continue # too small
|
continue # too small
|
||||||
|
|
||||||
largest.pop(-1)
|
largest.pop(-1)
|
||||||
|
if outp.is_change:
|
||||||
rendered, dest = self.render_output(tx_out)
|
ret = (here, self.chain.render_address(tx_out.scriptPubKey))
|
||||||
largest.insert(keep, (here, dest if outp.is_change else rendered))
|
else:
|
||||||
|
rendered, _ = self.render_output(tx_out)
|
||||||
|
ret = (here, rendered)
|
||||||
|
largest.insert(keep, ret)
|
||||||
|
|
||||||
# foreign outputs (soon to be other people's coins)
|
# foreign outputs (soon to be other people's coins)
|
||||||
visible_out_sum = 0
|
visible_out_sum = 0
|
||||||
@ -718,12 +727,11 @@ class ApproveTransaction(UserAuthorizedAction):
|
|||||||
msg.write('%s %s\n\n' % self.chain.render_value(total_change - visible_change_sum))
|
msg.write('%s %s\n\n' % self.chain.render_value(total_change - visible_change_sum))
|
||||||
|
|
||||||
|
|
||||||
def sign_transaction(psbt_len, flags=0x0, psbt_sha=None, input_method="usb", offset=TXN_INPUT_OFFSET):
|
def sign_transaction(psbt_len, flags=0x0, psbt_sha=None):
|
||||||
# transaction (binary) loaded into PSRAM already, checksum checked
|
# transaction (binary) loaded into PSRAM already, checksum checked
|
||||||
UserAuthorizedAction.check_busy(ApproveTransaction)
|
UserAuthorizedAction.check_busy(ApproveTransaction)
|
||||||
UserAuthorizedAction.active_request = ApproveTransaction(
|
UserAuthorizedAction.active_request = ApproveTransaction(
|
||||||
psbt_len, flags, psbt_sha=psbt_sha, input_method=input_method,
|
psbt_len, flags, psbt_sha=psbt_sha, input_method="usb",
|
||||||
offset=offset
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# kill any menu stack, and put our thing at the top
|
# kill any menu stack, and put our thing at the top
|
||||||
@ -770,19 +778,13 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
|
|||||||
# USB case - user can choose whether to attempt finalization
|
# USB case - user can choose whether to attempt finalization
|
||||||
is_complete = finalize
|
is_complete = finalize
|
||||||
|
|
||||||
if psbt.por322:
|
|
||||||
# network txn strips PSBT BIP-32 with paths with pubkey required for verification
|
|
||||||
# overrides --finalize from USB
|
|
||||||
# disable pushTX for BIP-322
|
|
||||||
is_complete = False
|
|
||||||
|
|
||||||
with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as psram:
|
with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as psram:
|
||||||
if is_complete:
|
if is_complete:
|
||||||
txid = psbt.finalize(psram)
|
txid = psbt.finalize(psram)
|
||||||
noun = "Finalized TX ready for broadcast"
|
noun = "Finalized TX ready for broadcast"
|
||||||
else:
|
else:
|
||||||
psbt.serialize(psram)
|
psbt.serialize(psram)
|
||||||
noun = "Signed BIP-322 PSBT" if psbt.por322 else "Partly Signed PSBT"
|
noun = "Partly Signed PSBT"
|
||||||
txid = None
|
txid = None
|
||||||
|
|
||||||
data_len = psram.tell()
|
data_len = psram.tell()
|
||||||
@ -800,10 +802,6 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
|
|||||||
msg = noun + " shared via USB."
|
msg = noun + " shared via USB."
|
||||||
title = "PSBT Signed"
|
title = "PSBT Signed"
|
||||||
|
|
||||||
elif input_method == "kt":
|
|
||||||
first_time = False
|
|
||||||
title = "PSBT Signed"
|
|
||||||
|
|
||||||
if txid and await try_push_tx(data_len, txid, data_sha2):
|
if txid and await try_push_tx(data_len, txid, data_sha2):
|
||||||
# go directly to reexport menu after pushTX
|
# go directly to reexport menu after pushTX
|
||||||
first_time = False
|
first_time = False
|
||||||
@ -822,6 +820,8 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
|
|||||||
ch = KEY_QR
|
ch = KEY_QR
|
||||||
elif input_method == "nfc":
|
elif input_method == "nfc":
|
||||||
ch = KEY_NFC
|
ch = KEY_NFC
|
||||||
|
elif input_method == "kt":
|
||||||
|
ch = 't'
|
||||||
else:
|
else:
|
||||||
# SD/VDisk
|
# SD/VDisk
|
||||||
ch = {"force_vdisk": input_method == "vdisk", "slot_b": slot_b}
|
ch = {"force_vdisk": input_method == "vdisk", "slot_b": slot_b}
|
||||||
@ -829,11 +829,9 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
|
|||||||
if not ch:
|
if not ch:
|
||||||
# show all possible export options (based on hardware enabled, features)
|
# show all possible export options (based on hardware enabled, features)
|
||||||
intro = []
|
intro = []
|
||||||
key6 = None
|
|
||||||
if msg:
|
if msg:
|
||||||
intro.append(msg)
|
intro.append(msg)
|
||||||
if txid:
|
if txid:
|
||||||
key6 = "for QR Code of TXID"
|
|
||||||
intro.append('TXID:\n' + txid)
|
intro.append('TXID:\n' + txid)
|
||||||
|
|
||||||
# "force_prompt" is needed after first iteration as we can be Mk4, with NFC,Vdisk off,
|
# "force_prompt" is needed after first iteration as we can be Mk4, with NFC,Vdisk off,
|
||||||
@ -841,7 +839,7 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
|
|||||||
# In that case this would just return dict and keep producing signed
|
# In that case this would just return dict and keep producing signed
|
||||||
# files on SD infinitely (would never actually prompt).
|
# files on SD infinitely (would never actually prompt).
|
||||||
ch = await import_export_prompt(noun, intro="\n\n".join(intro), offer_kt=offer_kt,
|
ch = await import_export_prompt(noun, intro="\n\n".join(intro), offer_kt=offer_kt,
|
||||||
key6=key6, title=title, force_prompt=not first_time,
|
txid=txid, title=title, force_prompt=not first_time,
|
||||||
no_qr=not version.has_qwerty)
|
no_qr=not version.has_qwerty)
|
||||||
if ch == KEY_CANCEL:
|
if ch == KEY_CANCEL:
|
||||||
UserAuthorizedAction.cleanup()
|
UserAuthorizedAction.cleanup()
|
||||||
@ -853,14 +851,14 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
|
|||||||
|
|
||||||
elif ch == KEY_QR:
|
elif ch == KEY_QR:
|
||||||
here = PSRAM.read_at(TXN_OUTPUT_OFFSET, data_len)
|
here = PSRAM.read_at(TXN_OUTPUT_OFFSET, data_len)
|
||||||
msg = txid or noun
|
msg = txid or 'Partly Signed PSBT'
|
||||||
try:
|
try:
|
||||||
if len(here) > 920:
|
if len(here) > 920:
|
||||||
# too big for simple QR - use BBQr instead
|
# too big for simple QR - use BBQr instead
|
||||||
raise QRTooBigError
|
raise ValueError
|
||||||
hex_here = b2a_hex(here).upper().decode()
|
hex_here = b2a_hex(here).upper().decode()
|
||||||
await show_qr_code(hex_here, is_alnum=True, msg=msg)
|
await show_qr_code(hex_here, is_alnum=True, msg=msg)
|
||||||
except QRTooBigError:
|
except (ValueError, RuntimeError, TypeError):
|
||||||
from ux_q1 import show_bbqr_codes
|
from ux_q1 import show_bbqr_codes
|
||||||
await show_bbqr_codes('T' if txid else 'P', here, msg)
|
await show_bbqr_codes('T' if txid else 'P', here, msg)
|
||||||
|
|
||||||
@ -879,9 +877,8 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
|
|||||||
elif (ch == 't') and not is_complete:
|
elif (ch == 't') and not is_complete:
|
||||||
# they might want to teleport it, but only if we have PSBT
|
# they might want to teleport it, but only if we have PSBT
|
||||||
# there is no need to teleport PSBT if txn is already complete & ready to be broadcast
|
# there is no need to teleport PSBT if txn is already complete & ready to be broadcast
|
||||||
# updated PSBT is at TXN_OUTPUT_OFFSET (at TXN_INPUT_OFFSET is PSBT that is NOT updated)
|
|
||||||
from teleport import kt_send_psbt
|
from teleport import kt_send_psbt
|
||||||
ok = await kt_send_psbt(psbt, data_len, psbt_offset=TXN_OUTPUT_OFFSET)
|
ok = await kt_send_psbt(psbt, data_len)
|
||||||
if ok is None:
|
if ok is None:
|
||||||
title = "Failed to Teleport"
|
title = "Failed to Teleport"
|
||||||
else:
|
else:
|
||||||
@ -1121,51 +1118,6 @@ class RemoteBackup(UserAuthorizedAction):
|
|||||||
self.done()
|
self.done()
|
||||||
|
|
||||||
|
|
||||||
class RemoteRestoreBackup(UserAuthorizedAction):
|
|
||||||
def __init__(self, file_len, bitflag):
|
|
||||||
super().__init__()
|
|
||||||
self.file_len = file_len
|
|
||||||
self.custom_pwd = bitflag & 1
|
|
||||||
self.plaintext = bitflag & 2
|
|
||||||
self.force_tmp = bitflag & 4
|
|
||||||
|
|
||||||
def to_words(self):
|
|
||||||
# conversion to "words" argument of "restore_complete" function
|
|
||||||
if self.plaintext:
|
|
||||||
return None
|
|
||||||
elif self.custom_pwd:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def to_tmp(self):
|
|
||||||
# conversion to "temporary" argument of "restore_complete" function
|
|
||||||
from pincodes import pa
|
|
||||||
if pa.is_secret_blank() and not self.force_tmp:
|
|
||||||
# no master secret & not forcing tmp
|
|
||||||
# will load backup as master seed
|
|
||||||
return False, "master"
|
|
||||||
|
|
||||||
# has master secret --> load backup as tmp
|
|
||||||
# secret is blank but user forcing tmp
|
|
||||||
return True, "temporary"
|
|
||||||
|
|
||||||
async def interact(self):
|
|
||||||
try:
|
|
||||||
# requires confirm from user
|
|
||||||
tmp, noun = self.to_tmp()
|
|
||||||
if await ux_confirm("Restore uploaded backup as a %s seed?" % noun):
|
|
||||||
from backups import restore_complete
|
|
||||||
await restore_complete(self.file_len, tmp, self.to_words(), usb=True)
|
|
||||||
else:
|
|
||||||
self.refused = True
|
|
||||||
|
|
||||||
except BaseException as exc:
|
|
||||||
self.failed = "Error during backup restore."
|
|
||||||
# sys.print_exception(exc)
|
|
||||||
finally:
|
|
||||||
self.done()
|
|
||||||
|
|
||||||
|
|
||||||
def start_remote_backup():
|
def start_remote_backup():
|
||||||
# tell the local user the secret words, and then save to SPI flash
|
# tell the local user the secret words, and then save to SPI flash
|
||||||
# USB caller has to come back and download encrypted contents.
|
# USB caller has to come back and download encrypted contents.
|
||||||
@ -1176,12 +1128,6 @@ def start_remote_backup():
|
|||||||
# kill any menu stack, and put our thing at the top
|
# kill any menu stack, and put our thing at the top
|
||||||
abort_and_goto(UserAuthorizedAction.active_request)
|
abort_and_goto(UserAuthorizedAction.active_request)
|
||||||
|
|
||||||
def start_remote_restore_backup(file_len, bitflag):
|
|
||||||
UserAuthorizedAction.cleanup()
|
|
||||||
UserAuthorizedAction.active_request = RemoteRestoreBackup(file_len, bitflag)
|
|
||||||
# kill any menu stack, and put our thing at the top
|
|
||||||
abort_and_goto(UserAuthorizedAction.active_request)
|
|
||||||
|
|
||||||
|
|
||||||
class NewPassphrase(UserAuthorizedAction):
|
class NewPassphrase(UserAuthorizedAction):
|
||||||
def __init__(self, pw):
|
def __init__(self, pw):
|
||||||
@ -1550,230 +1496,4 @@ def authorize_upgrade(hdr, length, **kws):
|
|||||||
abort_and_goto(UserAuthorizedAction.active_request)
|
abort_and_goto(UserAuthorizedAction.active_request)
|
||||||
|
|
||||||
|
|
||||||
class TXExplorer:
|
|
||||||
def __init__(self, n, user_auth_action, max_items):
|
|
||||||
self.n = n
|
|
||||||
self.user_auth_action = user_auth_action
|
|
||||||
self.max_items = max_items
|
|
||||||
self.chain = chains.current_chain()
|
|
||||||
self.qr_msgs = []
|
|
||||||
self.title = None
|
|
||||||
|
|
||||||
def can_goto_idx(self):
|
|
||||||
return self.max_items > 1
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def start(cls, user_auth_action):
|
|
||||||
rv = [
|
|
||||||
MenuItem("Inputs", f=TXInpExplorer(user_auth_action).explore),
|
|
||||||
MenuItem("Outputs", f=TXOutExplorer(user_auth_action).explore),
|
|
||||||
]
|
|
||||||
the_ux.push(MenuSystem(rv))
|
|
||||||
await the_ux.interact()
|
|
||||||
|
|
||||||
def make_ux_msg(self, offset, count):
|
|
||||||
from glob import dis
|
|
||||||
dis.fullscreen('Wait...')
|
|
||||||
esc = "4"+KEY_QR
|
|
||||||
rv = ""
|
|
||||||
qrs = []
|
|
||||||
change = []
|
|
||||||
end = min(offset + count, self.max_items)
|
|
||||||
for idx, item in self.yield_item(offset, end, qrs, change):
|
|
||||||
rv += item
|
|
||||||
dis.progress_sofar(idx-offset+1, count)
|
|
||||||
|
|
||||||
hints = []
|
|
||||||
if end < self.max_items:
|
|
||||||
hints.append('RIGHT to see next group')
|
|
||||||
esc += KEY_RIGHT + "9"
|
|
||||||
if offset:
|
|
||||||
hints.append('LEFT to go back')
|
|
||||||
esc += KEY_LEFT + "7"
|
|
||||||
|
|
||||||
if self.can_goto_idx():
|
|
||||||
hints.append("(2) to go to index")
|
|
||||||
esc += "2"
|
|
||||||
|
|
||||||
if not version.has_qwerty:
|
|
||||||
# Q has hint key
|
|
||||||
hints.append("(4) to show QR code")
|
|
||||||
|
|
||||||
if hints:
|
|
||||||
rv += 'Press ' + ', '.join(hints)
|
|
||||||
rv += ('. %s to quit.' % X)
|
|
||||||
else:
|
|
||||||
rv += 'Press %s to quit.' % X
|
|
||||||
|
|
||||||
return rv, qrs, change, end, esc
|
|
||||||
|
|
||||||
|
|
||||||
async def explore(self, *a):
|
|
||||||
# Page through unlimited-sized transaction details
|
|
||||||
# - shows all outputs (including change): their address and amounts.
|
|
||||||
# - shows all inputs: utxo amount and address, txid & tx index.
|
|
||||||
|
|
||||||
start = 0
|
|
||||||
msg, addrs, change, end, esc = self.make_ux_msg(start, self.n)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
ch = await ux_show_story(msg, title=self.title, hint_icons=KEY_QR, escape=esc)
|
|
||||||
if ch == 'x':
|
|
||||||
del msg
|
|
||||||
return
|
|
||||||
elif (ch in "4"+KEY_QR) and addrs:
|
|
||||||
from ux import show_qr_codes
|
|
||||||
# showing addresses from PSBT, no idea what is in there
|
|
||||||
# handle QR code failures gracefully
|
|
||||||
await show_qr_codes(addrs, False, start, is_addrs=True,
|
|
||||||
change_idxs=change, can_raise=False,
|
|
||||||
qr_msgs=self.qr_msgs, no_index=bool(self.qr_msgs))
|
|
||||||
continue
|
|
||||||
elif ch in (KEY_LEFT+"7"):
|
|
||||||
if not start: continue # 0
|
|
||||||
start = max(start - self.n, 0)
|
|
||||||
|
|
||||||
elif ch in (KEY_RIGHT+"9"):
|
|
||||||
if (start + self.n) >= self.max_items:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
# go forwards
|
|
||||||
start += self.n
|
|
||||||
elif (ch == "2") and (self.max_items > 1):
|
|
||||||
max_v = self.max_items - 1
|
|
||||||
res = await ux_enter_number("Start Idx (0-%d):" % max_v, max_value=max_v)
|
|
||||||
if res is None: continue
|
|
||||||
start = res
|
|
||||||
else:
|
|
||||||
# nothing changed - do not recalc msg
|
|
||||||
continue
|
|
||||||
|
|
||||||
msg, addrs, change, end, esc = self.make_ux_msg(start, self.n)
|
|
||||||
|
|
||||||
|
|
||||||
class TXOutExplorer(TXExplorer):
|
|
||||||
def __init__(self, user_auth_action):
|
|
||||||
super().__init__(10, user_auth_action, user_auth_action.psbt.num_outputs)
|
|
||||||
|
|
||||||
def yield_item(self, offset, end, qr_items, change_idxs):
|
|
||||||
# showing 10 outputs per UX page (just address/script + whether change)
|
|
||||||
self.title = "%d-%d" % (offset, end - 1)
|
|
||||||
for i, (idx, out) in enumerate(self.user_auth_action.psbt.output_iter(offset, end)):
|
|
||||||
outp = self.user_auth_action.psbt.outputs[idx]
|
|
||||||
item = "Output %d%s:\n\n" % (idx, " (change)" if outp.is_change else "")
|
|
||||||
msg, addr_or_script = self.user_auth_action.render_output(out)
|
|
||||||
item += msg
|
|
||||||
qr_items.append(addr_or_script)
|
|
||||||
if outp.is_change:
|
|
||||||
change_idxs.append(i)
|
|
||||||
item += "\n"
|
|
||||||
yield idx, item
|
|
||||||
|
|
||||||
|
|
||||||
class TXInpExplorer(TXExplorer):
|
|
||||||
def __init__(self, user_auth_action):
|
|
||||||
super().__init__(1, user_auth_action, user_auth_action.psbt.num_inputs)
|
|
||||||
self.qr_msgs = ["TXID", "UTXO ADDR"]
|
|
||||||
|
|
||||||
def yield_item(self, offset, end, qr_items, change_idxs):
|
|
||||||
# showing just one input per UX page
|
|
||||||
i, (idx, txin) = next(enumerate(self.user_auth_action.psbt.input_iter(offset, offset+1)))
|
|
||||||
self.title = "Input %d" % idx
|
|
||||||
inp = self.user_auth_action.psbt.inputs[idx]
|
|
||||||
|
|
||||||
txid = b2a_hex(ser_uint256(txin.prevout.hash)).decode()
|
|
||||||
qr_items.append(txid)
|
|
||||||
item = "%s:%d\n\n" % (txid, txin.prevout.n)
|
|
||||||
|
|
||||||
has_utxo = inp.has_utxo()
|
|
||||||
if has_utxo:
|
|
||||||
utxo = inp.get_utxo(txin.prevout.n)
|
|
||||||
spk = b2a_hex(utxo.scriptPubKey).decode()
|
|
||||||
try:
|
|
||||||
addr = self.chain.render_address(utxo.scriptPubKey)
|
|
||||||
except:
|
|
||||||
# some script we do not understand
|
|
||||||
addr = None
|
|
||||||
|
|
||||||
val, unit = self.chain.render_value(utxo.nValue)
|
|
||||||
item += "=== UTXO ===\n\n%s %s\n\n%s\n\n" % (val, unit, spk)
|
|
||||||
if addr:
|
|
||||||
item += show_single_address(addr) + "\n\n"
|
|
||||||
qr_items.append(addr)
|
|
||||||
|
|
||||||
if inp.addr_fmt is not None:
|
|
||||||
item += "Address Format: %s\n\n" % chains.addr_fmt_str(inp.addr_fmt)
|
|
||||||
|
|
||||||
if self.user_auth_action.psbt.txn_version >= 2:
|
|
||||||
has_rtl = inp.has_relative_timelock(txin)
|
|
||||||
if has_rtl:
|
|
||||||
if has_rtl[0]:
|
|
||||||
val = seconds2human_readable(has_rtl[1])
|
|
||||||
msg = "time-based timelock of:\n %s" % val
|
|
||||||
else:
|
|
||||||
msg = "block height timelock of %d blocks" % (has_rtl[1])
|
|
||||||
|
|
||||||
item += "Input has relative %s\n\n" % msg
|
|
||||||
|
|
||||||
|
|
||||||
psbt_item = ""
|
|
||||||
if inp.required_key:
|
|
||||||
ws = self.user_auth_action.psbt.wif_store
|
|
||||||
our = [inp.required_key] if isinstance(inp.required_key, bytes) else inp.required_key
|
|
||||||
psbt_item += "Our key%s:\n\n" % ("s" if len(our) > 1 else "")
|
|
||||||
wif_note = "(WIF Store)"
|
|
||||||
for k in our:
|
|
||||||
pubkey = b2a_hex(k).decode()
|
|
||||||
pth = inp.subpaths.get(k)
|
|
||||||
note = ""
|
|
||||||
if pth:
|
|
||||||
label = keypath_to_str(pth, prefix="%s/" % xfp2str(pth[0]))
|
|
||||||
if ws and k in ws:
|
|
||||||
note = "\n" + wif_note
|
|
||||||
psbt_item += "%s:\n%s%s\n\n" % (label, pubkey, note)
|
|
||||||
else:
|
|
||||||
psbt_item += "%s\n%s\n\n" % (pubkey, wif_note)
|
|
||||||
|
|
||||||
if inp.is_multisig:
|
|
||||||
ks_coord = inp.witness_script or inp.redeem_script
|
|
||||||
if ks_coord:
|
|
||||||
ks = self.user_auth_action.psbt.get(ks_coord)
|
|
||||||
|
|
||||||
from multisig import disassemble_multisig_mn
|
|
||||||
try:
|
|
||||||
M, N = disassemble_multisig_mn(ks)
|
|
||||||
psbt_item += "Multisig: %dof%d\n\n" % (M, N)
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
if inp.part_sigs:
|
|
||||||
# do not show XFPs in case input is fully signed --> elif
|
|
||||||
# only part_sig should be available, as we haven't signed yet so added_sigs empty
|
|
||||||
done = []
|
|
||||||
for pk, pth in inp.subpaths.items():
|
|
||||||
if pk in inp.part_sigs:
|
|
||||||
done.append(xfp2str(pth[0]))
|
|
||||||
|
|
||||||
if inp.fully_signed:
|
|
||||||
psbt_item += "Input fully signed.\n\n"
|
|
||||||
else:
|
|
||||||
psbt_item += "Already signed:\n"
|
|
||||||
for xfp in done:
|
|
||||||
psbt_item += " %s\n" % xfp
|
|
||||||
psbt_item += "\n"
|
|
||||||
|
|
||||||
if inp.sighash and (inp.sighash != SIGHASH_ALL):
|
|
||||||
# only show sighash value to the user if it is non-standard
|
|
||||||
psbt_item += "sighash: %s\n\n" % {
|
|
||||||
1: "ALL", 2: "NONE", 3: "SINGLE",
|
|
||||||
1 | 0x80: "ALL|ANYONECANPAY",
|
|
||||||
2 | 0x80: "NONE|ANYONECANPAY",
|
|
||||||
3 | 0x80: "SINGLE|ANYONECANPAY",
|
|
||||||
}.get(inp.sighash, "0x%02x (non-standard)" % inp.sighash)
|
|
||||||
|
|
||||||
if psbt_item:
|
|
||||||
psbt_item = "=== PSBT ===\n\n" + psbt_item
|
|
||||||
item += psbt_item
|
|
||||||
|
|
||||||
yield idx, item
|
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
@ -5,11 +5,10 @@
|
|||||||
import compat7z, stash, ckcc, chains, gc, sys, bip39, uos, ngu
|
import compat7z, stash, ckcc, chains, gc, sys, bip39, uos, ngu
|
||||||
from ubinascii import hexlify as b2a_hex
|
from ubinascii import hexlify as b2a_hex
|
||||||
from ubinascii import unhexlify as a2b_hex
|
from ubinascii import unhexlify as a2b_hex
|
||||||
from utils import deserialize_secret, swab32, xfp2str
|
from utils import deserialize_secret
|
||||||
from sffile import SFFile
|
|
||||||
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, OK, X, ux_input_text
|
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, OK, X, ux_input_text
|
||||||
import version, ujson
|
import version, ujson
|
||||||
from uio import StringIO, BytesIO
|
from uio import StringIO
|
||||||
import seed
|
import seed
|
||||||
from glob import settings
|
from glob import settings
|
||||||
from pincodes import pa
|
from pincodes import pa
|
||||||
@ -49,7 +48,7 @@ def render_backup_contents(bypass_tmp=False):
|
|||||||
if sv.mode == 'words':
|
if sv.mode == 'words':
|
||||||
ADD('mnemonic', bip39.b2a_words(sv.raw))
|
ADD('mnemonic', bip39.b2a_words(sv.raw))
|
||||||
|
|
||||||
elif sv.mode == 'master':
|
if sv.mode == 'master':
|
||||||
ADD('bip32_master_key', b2a_hex(sv.raw))
|
ADD('bip32_master_key', b2a_hex(sv.raw))
|
||||||
|
|
||||||
ADD('chain', chain.ctype)
|
ADD('chain', chain.ctype)
|
||||||
@ -76,12 +75,7 @@ def render_backup_contents(bypass_tmp=False):
|
|||||||
current_tmp = pa.tmp_value[:]
|
current_tmp = pa.tmp_value[:]
|
||||||
pa.tmp_value = None
|
pa.tmp_value = None
|
||||||
# we also need correct settings from main seed
|
# we also need correct settings from main seed
|
||||||
if sv.mode == 'words':
|
nv = stash.SecretStash.encode(seed_phrase=sv.raw)
|
||||||
nv = stash.SecretStash.encode(seed_phrase=sv.raw)
|
|
||||||
else:
|
|
||||||
assert sv.mode == "xprv"
|
|
||||||
nv = stash.SecretStash.encode(xprv=sv.node)
|
|
||||||
|
|
||||||
settings.set_key(nv)
|
settings.set_key(nv)
|
||||||
settings.load()
|
settings.load()
|
||||||
stash.blank_object(nv)
|
stash.blank_object(nv)
|
||||||
@ -107,7 +101,6 @@ def render_backup_contents(bypass_tmp=False):
|
|||||||
if k == 'words': continue # words length is recalculated from secret
|
if k == 'words': continue # words length is recalculated from secret
|
||||||
if k == 'ccc': continue # not supported, security issue
|
if k == 'ccc': continue # not supported, security issue
|
||||||
if k == 'ktrx': continue # not useful after the fact
|
if k == 'ktrx': continue # not useful after the fact
|
||||||
if k == 'lfr': continue # temporary error msg value
|
|
||||||
if k == 'seedvault' and not v: continue
|
if k == 'seedvault' and not v: continue
|
||||||
if k == 'seeds' and not v: continue
|
if k == 'seeds' and not v: continue
|
||||||
ADD('setting.' + k, v)
|
ADD('setting.' + k, v)
|
||||||
@ -128,7 +121,7 @@ def render_backup_contents(bypass_tmp=False):
|
|||||||
|
|
||||||
return rv.getvalue()
|
return rv.getvalue()
|
||||||
|
|
||||||
def extract_raw_secret(vals):
|
def extract_raw_secret(chain, vals):
|
||||||
# step1: the private key
|
# step1: the private key
|
||||||
# - prefer raw_secret over other values
|
# - prefer raw_secret over other values
|
||||||
# - TODO: fail back to other values
|
# - TODO: fail back to other values
|
||||||
@ -143,10 +136,10 @@ def extract_raw_secret(vals):
|
|||||||
|
|
||||||
# verify against xprv value (if we have it)
|
# verify against xprv value (if we have it)
|
||||||
if 'xprv' in vals:
|
if 'xprv' in vals:
|
||||||
check_xprv = chains.get_chain(vals.get('chain', 'BTC')).serialize_private(node)
|
check_xprv = chain.serialize_private(node)
|
||||||
assert check_xprv == vals['xprv'], 'xprv mismatch'
|
assert check_xprv == vals['xprv'], 'xprv mismatch'
|
||||||
|
|
||||||
return raw, node
|
return raw
|
||||||
|
|
||||||
def extract_long_secret(vals):
|
def extract_long_secret(vals):
|
||||||
ls = None
|
ls = None
|
||||||
@ -159,7 +152,7 @@ def extract_long_secret(vals):
|
|||||||
pass
|
pass
|
||||||
return ls
|
return ls
|
||||||
|
|
||||||
def restore_from_dict_ll(vals, raw):
|
def restore_from_dict_ll(vals):
|
||||||
# Restore from a dict of values. Already JSON decoded.
|
# Restore from a dict of values. Already JSON decoded.
|
||||||
# Need a Reboot on success, return string on failure
|
# Need a Reboot on success, return string on failure
|
||||||
# - low-level version, factored out for better testing
|
# - low-level version, factored out for better testing
|
||||||
@ -170,6 +163,12 @@ def restore_from_dict_ll(vals, raw):
|
|||||||
#print("Restoring from: %r" % vals)
|
#print("Restoring from: %r" % vals)
|
||||||
chain = chains.get_chain(vals.get('chain', 'BTC'))
|
chain = chains.get_chain(vals.get('chain', 'BTC'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw = extract_raw_secret(chain, vals)
|
||||||
|
except Exception as e:
|
||||||
|
return ('Unable to decode raw_secret and '
|
||||||
|
'restore the seed value!\n\n\n'+str(e)), None
|
||||||
|
|
||||||
dis.fullscreen("Saving...")
|
dis.fullscreen("Saving...")
|
||||||
dis.progress_bar_show(.1)
|
dis.progress_bar_show(.1)
|
||||||
|
|
||||||
@ -206,13 +205,6 @@ def restore_from_dict_ll(vals, raw):
|
|||||||
|
|
||||||
k = key[8:]
|
k = key[8:]
|
||||||
|
|
||||||
if k == 'bkpw':
|
|
||||||
# never import a cached backup password from a backup file.
|
|
||||||
# write-side (render_backup_contents) strips bkpw, so a present
|
|
||||||
# value means a tampered/crafted file trying to fixate the
|
|
||||||
# password used for all FUTURE backups - drop it.
|
|
||||||
continue
|
|
||||||
|
|
||||||
if k == 'sd2fa':
|
if k == 'sd2fa':
|
||||||
# do NOT restore sd2fa as SD card can be lost or damaged
|
# do NOT restore sd2fa as SD card can be lost or damaged
|
||||||
# new version of firmware 5.1.3+ will not back sd2fa
|
# new version of firmware 5.1.3+ will not back sd2fa
|
||||||
@ -289,10 +281,15 @@ def text_bk_parser(contents):
|
|||||||
|
|
||||||
return vals
|
return vals
|
||||||
|
|
||||||
async def restore_tmp_from_dict_ll(vals, raw):
|
async def restore_tmp_from_dict_ll(vals):
|
||||||
from glob import dis
|
from glob import dis
|
||||||
|
|
||||||
chain = chains.get_chain(vals.get('chain', 'BTC'))
|
chain = chains.get_chain(vals.get('chain', 'BTC'))
|
||||||
|
try:
|
||||||
|
raw = extract_raw_secret(chain, vals)
|
||||||
|
except Exception as e:
|
||||||
|
return ('Unable to decode raw_secret and '
|
||||||
|
'restore the seed value!\n\n\n' + str(e))
|
||||||
|
|
||||||
dis.fullscreen("Applying...")
|
dis.fullscreen("Applying...")
|
||||||
from seed import set_ephemeral_seed
|
from seed import set_ephemeral_seed
|
||||||
@ -309,11 +306,11 @@ async def restore_tmp_from_dict_ll(vals, raw):
|
|||||||
|
|
||||||
goto_top_menu()
|
goto_top_menu()
|
||||||
|
|
||||||
async def restore_from_dict(vals, raw):
|
async def restore_from_dict(vals):
|
||||||
# Restore from a dict of values. Already JSON decoded (ie. dict object).
|
# Restore from a dict of values. Already JSON decoded (ie. dict object).
|
||||||
# Need a Reboot on success, return string on failure
|
# Need a Reboot on success, return string on failure
|
||||||
|
|
||||||
prob, need_ftux = restore_from_dict_ll(vals, raw)
|
prob, need_ftux = restore_from_dict_ll(vals)
|
||||||
if prob: return prob
|
if prob: return prob
|
||||||
|
|
||||||
if need_ftux:
|
if need_ftux:
|
||||||
@ -459,6 +456,8 @@ async def write_complete_backup(pwd, fname_pattern, write_sflash=False,
|
|||||||
|
|
||||||
if write_sflash:
|
if write_sflash:
|
||||||
# for use over USB and unit testing: commit file into PSRAM
|
# for use over USB and unit testing: commit file into PSRAM
|
||||||
|
from sffile import SFFile
|
||||||
|
|
||||||
with SFFile(0, max_size=MAX_BACKUP_FILE_SIZE, message='Saving...') as fd:
|
with SFFile(0, max_size=MAX_BACKUP_FILE_SIZE, message='Saving...') as fd:
|
||||||
if zz:
|
if zz:
|
||||||
fd.write(hdr)
|
fd.write(hdr)
|
||||||
@ -552,14 +551,10 @@ async def verify_backup_file(fname):
|
|||||||
# might be already closed on vdisk case due to filesystem unmount/mount
|
# might be already closed on vdisk case due to filesystem unmount/mount
|
||||||
pass
|
pass
|
||||||
|
|
||||||
await ux_show_story("Backup file CRC checks out okay.\n\n"
|
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.")
|
||||||
"Please note this is only a check against accidental truncation and similar."
|
|
||||||
" Targeted modifications can still pass this test. You may further verify"
|
|
||||||
" this backup file by starting the normal restore process (Restore Backup)"
|
|
||||||
" and aborting it once decryption has been achieved.")
|
|
||||||
|
|
||||||
|
|
||||||
async def restore_complete(fname_or_fd, temporary=False, words=True, usb=False):
|
async def restore_complete(fname_or_fd, temporary=False, words=True):
|
||||||
from ux import the_ux
|
from ux import the_ux
|
||||||
|
|
||||||
async def done(words):
|
async def done(words):
|
||||||
@ -569,151 +564,91 @@ async def restore_complete(fname_or_fd, temporary=False, words=True, usb=False):
|
|||||||
|
|
||||||
prob = await restore_complete_doit(fname_or_fd, words,
|
prob = await restore_complete_doit(fname_or_fd, words,
|
||||||
temporary=temporary)
|
temporary=temporary)
|
||||||
|
|
||||||
if prob:
|
if prob:
|
||||||
await ux_show_story(prob, title='FAILED')
|
await ux_show_story(prob, title='FAILED')
|
||||||
|
|
||||||
if words:
|
if words:
|
||||||
if version.has_qwerty:
|
if version.has_qwerty:
|
||||||
from ux_q1 import seed_word_entry, CHARS_W
|
from ux_q1 import seed_word_entry
|
||||||
|
return await seed_word_entry('Enter Password:', num_pw_words,
|
||||||
basename = None
|
done_cb=done, has_checksum=False)
|
||||||
if isinstance(fname_or_fd, str):
|
|
||||||
basename = fname_or_fd.split('/')[-1]
|
|
||||||
if len(basename) > CHARS_W:
|
|
||||||
basename = basename[:16] + "⋯" + basename[-16:]
|
|
||||||
|
|
||||||
return await seed_word_entry("Enter Password%s:" % (" for" if basename else ""),
|
|
||||||
num_pw_words, done_cb=done, has_checksum=False,
|
|
||||||
line2=basename)
|
|
||||||
|
|
||||||
# give them a menu to pick from, and start picking
|
# give them a menu to pick from, and start picking
|
||||||
if usb:
|
m = seed.WordNestMenu(num_words=num_pw_words, has_checksum=False, done_cb=done)
|
||||||
# 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)
|
the_ux.push(m)
|
||||||
else:
|
|
||||||
m = seed.WordNestMenu(num_words=num_pw_words, has_checksum=False, done_cb=done)
|
|
||||||
the_ux.push(m)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
pwd = [] # cleartext if words=None
|
pwd = [] # cleartext if words=None
|
||||||
if words is False:
|
if words is False:
|
||||||
ipw = await ux_input_text("", prompt="Your Backup Password",
|
ipw = await ux_input_text("", prompt="Your Backup Password",
|
||||||
min_len=bkpw_min_len, max_len=128)
|
min_len=bkpw_min_len, max_len=128)
|
||||||
if not ipw: return
|
|
||||||
pwd.append(ipw)
|
pwd.append(ipw)
|
||||||
|
|
||||||
await done(pwd)
|
await done(pwd)
|
||||||
|
|
||||||
|
async def restore_complete_doit(fname_or_fd, words, file_cleanup=None, temporary=False):
|
||||||
def check_and_decrypt(fd, password):
|
|
||||||
try:
|
|
||||||
compat7z.check_file_headers(fd)
|
|
||||||
except Exception as e:
|
|
||||||
raise RuntimeError('Unable to read backup file.'
|
|
||||||
' Has it been touched?\n\nError: '+str(e))
|
|
||||||
|
|
||||||
from glob import dis
|
|
||||||
dis.fullscreen("Decrypting...")
|
|
||||||
try:
|
|
||||||
zz = compat7z.Builder()
|
|
||||||
fname, contents = zz.read_file(fd, password, MAX_BACKUP_FILE_SIZE,
|
|
||||||
progress_fcn=dis.progress_bar_show)
|
|
||||||
|
|
||||||
# simple quick sanity checks
|
|
||||||
assert fname.endswith('.txt') # was == 'ckcc-backup.txt'
|
|
||||||
assert contents[0:1] == b'#' and contents[-1:] == b'\n'
|
|
||||||
return contents
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# assume everything here is "password wrong" errors
|
|
||||||
raise RuntimeError('Unable to decrypt backup file. Incorrect password?'
|
|
||||||
'\n\nTried:\n\n' + password)
|
|
||||||
|
|
||||||
|
|
||||||
async def restore_complete_doit(fname_or_fd, words, file_cleanup=None, temporary=False,
|
|
||||||
ux_confirm=True):
|
|
||||||
# Open file, read it, maybe decrypt it; return string if any error
|
# Open file, read it, maybe decrypt it; return string if any error
|
||||||
# - some errors will be shown, None return in that case
|
# - some errors will be shown, None return in that case
|
||||||
# - no return if successful (due to reboot)
|
# - no return if successful (due to reboot)
|
||||||
|
from glob import dis
|
||||||
from files import CardSlot, CardMissingError, needs_microsd
|
from files import CardSlot, CardMissingError, needs_microsd
|
||||||
|
|
||||||
# build password
|
# build password
|
||||||
password = ' '.join(words)
|
password = ' '.join(words)
|
||||||
prob = None
|
prob = None
|
||||||
|
|
||||||
if isinstance(fname_or_fd, int):
|
try:
|
||||||
# USB restore - backup is already in PSRAM, fname of fd is length
|
with CardSlot(readonly=True) as card:
|
||||||
# TXN_INPUT_OFFSET = 0
|
# filename already picked, taste it and maybe consider using its data.
|
||||||
with SFFile(0, length=fname_or_fd) as fd:
|
try:
|
||||||
if not words:
|
fd = open(fname_or_fd, 'rb') if isinstance(fname_or_fd, str) else fname_or_fd
|
||||||
contents = fd.read(fname_or_fd)
|
except:
|
||||||
else:
|
return 'Unable to open backup file.\n\n' + str(fname_or_fd)
|
||||||
# read full size, then decrypt
|
|
||||||
fd = BytesIO(fd.read(fname_or_fd))
|
|
||||||
try:
|
|
||||||
contents = check_and_decrypt(fd, password)
|
|
||||||
except RuntimeError as e:
|
|
||||||
return str(e)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
with CardSlot(readonly=True) as card:
|
|
||||||
# filename already picked, taste it and maybe consider using its data.
|
|
||||||
try:
|
|
||||||
fd = open(fname_or_fd, 'rb')
|
|
||||||
except:
|
|
||||||
return 'Unable to open backup file.\n\n' + str(fname_or_fd)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if words:
|
if not words:
|
||||||
contents = check_and_decrypt(fd, password)
|
contents = fd.read()
|
||||||
else:
|
else:
|
||||||
contents = fd.read()
|
try:
|
||||||
|
compat7z.check_file_headers(fd)
|
||||||
|
except Exception as e:
|
||||||
|
return 'Unable to read backup file. Has it been touched?\n\nError: ' \
|
||||||
|
+ str(e)
|
||||||
|
|
||||||
except RuntimeError as e:
|
dis.fullscreen("Decrypting...")
|
||||||
return str(e)
|
try:
|
||||||
finally:
|
zz = compat7z.Builder()
|
||||||
fd.close()
|
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()
|
||||||
|
|
||||||
if file_cleanup:
|
if file_cleanup:
|
||||||
file_cleanup(fname_or_fd)
|
file_cleanup(fname_or_fd)
|
||||||
|
|
||||||
except CardMissingError:
|
except CardMissingError:
|
||||||
await needs_microsd()
|
await needs_microsd()
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
vals = text_bk_parser(contents)
|
||||||
vals = text_bk_parser(contents)
|
|
||||||
except:
|
|
||||||
return "Invalid backup file."
|
|
||||||
|
|
||||||
try:
|
|
||||||
raw, node = extract_raw_secret(vals)
|
|
||||||
except Exception as e:
|
|
||||||
return ('Unable to decode raw_secret and '
|
|
||||||
'restore the seed value!\n\n\n'+str(e))
|
|
||||||
|
|
||||||
if ux_confirm:
|
|
||||||
# check master fingerprint from raw secret that is actually being loaded
|
|
||||||
# master extended public keys can be wrong & is unverified
|
|
||||||
xfp_str = xfp2str(swab32(node.my_fp()))
|
|
||||||
ch = await ux_show_story("Above is the master fingerprint of the seed stored in the backup."
|
|
||||||
" Press %s to continue, and load backup as %s seed. Press %s"
|
|
||||||
" to abort." % (OK, "temporary" if temporary else "master", X),
|
|
||||||
title="["+xfp_str+"]")
|
|
||||||
if ch != "y":
|
|
||||||
await ux_dramatic_pause('Aborted.', 2)
|
|
||||||
return
|
|
||||||
|
|
||||||
# this leads to reboot if it works, else errors shown, etc.
|
# this leads to reboot if it works, else errors shown, etc.
|
||||||
if temporary:
|
if temporary:
|
||||||
return await restore_tmp_from_dict_ll(vals, raw)
|
return await restore_tmp_from_dict_ll(vals)
|
||||||
else:
|
else:
|
||||||
return await restore_from_dict(vals, raw)
|
return await restore_from_dict(vals)
|
||||||
|
|
||||||
async def clone_start(*a):
|
async def clone_start(*a):
|
||||||
# Begins cloning process, on target device.
|
# Begins cloning process, on target device.
|
||||||
@ -796,9 +731,8 @@ back and press %s to complete clone process.''' % OK)
|
|||||||
uos.remove(fname) # ccbk-start.json
|
uos.remove(fname) # ccbk-start.json
|
||||||
|
|
||||||
# this will reset in successful case, no return (but delme is called)
|
# this will reset in successful case, no return (but delme is called)
|
||||||
# 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)
|
||||||
prob = await restore_complete_doit(incoming, words, file_cleanup=delme,
|
|
||||||
ux_confirm=False)
|
|
||||||
if prob:
|
if prob:
|
||||||
await ux_show_story(prob, title='FAILED')
|
await ux_show_story(prob, title='FAILED')
|
||||||
|
|
||||||
|
|||||||
@ -138,15 +138,12 @@ async def batt_idle_logout():
|
|||||||
# - even before login
|
# - even before login
|
||||||
import glob
|
import glob
|
||||||
from uasyncio import sleep_ms
|
from uasyncio import sleep_ms
|
||||||
from glob import settings, dis, SCAN
|
from glob import settings, dis
|
||||||
import utime
|
import utime
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
await sleep_ms(20000) # 20 seconds
|
await sleep_ms(20000) # 20 seconds
|
||||||
|
|
||||||
if SCAN.busy_scanning:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if get_batt_level() is None:
|
if get_batt_level() is None:
|
||||||
# on USB power
|
# on USB power
|
||||||
continue
|
continue
|
||||||
|
|||||||
@ -54,7 +54,7 @@ def calc_num_qr(char_capacity, char_len, split_mod):
|
|||||||
if char_len > actual:
|
if char_len > actual:
|
||||||
need += 1
|
need += 1
|
||||||
|
|
||||||
# Challenge: the final QR might have just a few chars in it, if we redistribute
|
# Challenge: the final QR might have just a a few chars in it, if we redistribute
|
||||||
# the data into the other parts, then each QR can have more forward error correction
|
# the data into the other parts, then each QR can have more forward error correction
|
||||||
# and be more robust. Must respect split_mod alignment tho.
|
# and be more robust. Must respect split_mod alignment tho.
|
||||||
level = ceil(char_len / need)
|
level = ceil(char_len / need)
|
||||||
@ -439,18 +439,5 @@ class BBQrPsramStorage(BBQrStorage):
|
|||||||
from glob import PSRAM
|
from glob import PSRAM
|
||||||
return PSRAM.read_at(0, self.final_size)
|
return PSRAM.read_at(0, self.final_size)
|
||||||
|
|
||||||
def finalize(self):
|
|
||||||
self._finalize()
|
|
||||||
|
|
||||||
if self.hdr.encoding == 'Z':
|
|
||||||
self.zlib_decompress()
|
|
||||||
|
|
||||||
# PSBT-typed BBQrs end up at PSRAM[0..size]
|
|
||||||
# skip a redundant PSRAM->heap->PSRAM round-trip
|
|
||||||
if self.hdr.file_type == 'P':
|
|
||||||
return self.hdr.file_type, self.final_size, 'PSRAM'
|
|
||||||
|
|
||||||
return self.hdr.file_type, self.final_size, self.get_buffer()
|
|
||||||
|
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
# (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
|
|
||||||
@ -9,7 +9,7 @@ from utils import B2A, word_wrap
|
|||||||
from ux_q1 import ux_input_text
|
from ux_q1 import ux_input_text
|
||||||
|
|
||||||
async def login_repl():
|
async def login_repl():
|
||||||
from glob import dis
|
from glob import dis, settings
|
||||||
from pincodes import pa
|
from pincodes import pa
|
||||||
|
|
||||||
NUM_LINES = 7 # 10 - title - 2 for prompt
|
NUM_LINES = 7 # 10 - title - 2 for prompt
|
||||||
@ -65,11 +65,11 @@ Example Commands:
|
|||||||
elif ln in ('help', 'cls', 'rand'):
|
elif ln in ('help', 'cls', 'rand'):
|
||||||
# no need for () for these commands
|
# no need for () for these commands
|
||||||
ans = state[ln]()
|
ans = state[ln]()
|
||||||
elif pa.attempts_left and re_pin.match(ln) and (len(ln) <= 13):
|
elif re_pin.match(ln) and len(ln) <= 13:
|
||||||
# try login
|
# try login
|
||||||
m = re_pin.match(ln)
|
m = re_pin.match(ln)
|
||||||
ln = m.group(1)+ '-' + m.group(2)
|
ln = m.group(1)+ '-' + m.group(2)
|
||||||
|
print(ln)
|
||||||
try:
|
try:
|
||||||
pa.setup(ln)
|
pa.setup(ln)
|
||||||
ok = pa.login()
|
ok = pa.login()
|
||||||
@ -83,7 +83,7 @@ Example Commands:
|
|||||||
else:
|
else:
|
||||||
ans = 'Error: ' + repr(exc.args)
|
ans = 'Error: ' + repr(exc.args)
|
||||||
|
|
||||||
elif re_prefix.match(ln) and (len(ln) <= 7):
|
elif re_prefix.match(ln) and len(ln) <= 7:
|
||||||
# show words
|
# show words
|
||||||
ans = pa.prefix_words(ln[:-1].encode())
|
ans = pa.prefix_words(ln[:-1].encode())
|
||||||
else:
|
else:
|
||||||
|
|||||||
778
shared/ccc.py
778
shared/ccc.py
File diff suppressed because it is too large
Load Diff
172
shared/chains.py
172
shared/chains.py
@ -5,15 +5,14 @@
|
|||||||
import ngu
|
import ngu
|
||||||
from uhashlib import sha256
|
from uhashlib import sha256
|
||||||
from ubinascii import hexlify as b2a_hex
|
from ubinascii import hexlify as b2a_hex
|
||||||
from public_constants import AF_BARE_PK, AF_CLASSIC, AF_P2WPKH, AF_P2TR
|
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2TR
|
||||||
from public_constants import AF_P2SH, AF_P2WSH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH
|
from public_constants import AF_P2SH, AF_P2WSH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH
|
||||||
from public_constants import AFC_PUBKEY, AFC_SEGWIT, AFC_BECH32, AFC_SCRIPT
|
from public_constants import AFC_PUBKEY, AFC_SEGWIT, AFC_BECH32, AFC_SCRIPT
|
||||||
from block_height import BLOCK_HEIGHT
|
|
||||||
from serializations import hash160, ser_compact_size, disassemble
|
from serializations import hash160, ser_compact_size, disassemble
|
||||||
from ucollections import namedtuple
|
from ucollections import namedtuple
|
||||||
from opcodes import OP_RETURN, OP_1, OP_16
|
from opcodes import OP_RETURN, OP_1, OP_16
|
||||||
|
|
||||||
# DO NOT CHANGE ORDER! PickAddrFmtMenu.__init__ expects correct order
|
|
||||||
SINGLESIG_AF = (AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH)
|
SINGLESIG_AF = (AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH)
|
||||||
|
|
||||||
# See SLIP 132 <https://github.com/satoshilabs/slips/blob/master/slip-0132.md>
|
# See SLIP 132 <https://github.com/satoshilabs/slips/blob/master/slip-0132.md>
|
||||||
@ -23,6 +22,8 @@ Slip132Version = namedtuple('Slip132Version', ('pub', 'priv', 'hint'))
|
|||||||
# See also:
|
# See also:
|
||||||
# - <https://github.com/satoshilabs/slips/blob/master/slip-0132.md>
|
# - <https://github.com/satoshilabs/slips/blob/master/slip-0132.md>
|
||||||
# - defines ypub/zpub/Xprc variants
|
# - defines ypub/zpub/Xprc variants
|
||||||
|
# - <https://github.com/satoshilabs/slips/blob/master/slip-0032.md>
|
||||||
|
# - nice bech32 encoded scheme for going forward
|
||||||
# - <https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-September/014907.html>
|
# - <https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-September/014907.html>
|
||||||
# - mailing list post proposed ypub, etc.
|
# - mailing list post proposed ypub, etc.
|
||||||
# - from <https://github.com/Bit-Wasp/bitcoin-php/issues/576>
|
# - from <https://github.com/Bit-Wasp/bitcoin-php/issues/576>
|
||||||
@ -81,41 +82,6 @@ class ChainsBase:
|
|||||||
or (version == cls.slip132[addr_fmt].priv)
|
or (version == cls.slip132[addr_fmt].priv)
|
||||||
return node
|
return node
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def script_pubkey(cls, addr_fmt, pubkey=None, script=None):
|
|
||||||
digest = None
|
|
||||||
if addr_fmt & AFC_SCRIPT:
|
|
||||||
assert script, "need witness/redeem script"
|
|
||||||
|
|
||||||
if addr_fmt in [AF_P2WSH, AF_P2WSH_P2SH]:
|
|
||||||
digest = ngu.hash.sha256s(script)
|
|
||||||
# bech32 encoded segwit p2sh
|
|
||||||
spk = b'\x00\x20' + digest
|
|
||||||
if addr_fmt == AF_P2WSH_P2SH:
|
|
||||||
# segwit p2wsh encoded as classic P2SH
|
|
||||||
digest = hash160(spk)
|
|
||||||
spk = b'\xA9\x14' + digest + b'\x87'
|
|
||||||
|
|
||||||
else:
|
|
||||||
assert addr_fmt == AF_P2SH
|
|
||||||
digest = hash160(script)
|
|
||||||
spk = b'\xA9\x14' + digest + b'\x87'
|
|
||||||
|
|
||||||
else:
|
|
||||||
assert pubkey
|
|
||||||
keyhash = ngu.hash.hash160(pubkey)
|
|
||||||
if addr_fmt == AF_CLASSIC:
|
|
||||||
spk = b'\x76\xA9\x14' + keyhash + b'\x88\xAC'
|
|
||||||
elif addr_fmt == AF_P2WPKH_P2SH:
|
|
||||||
redeem_script = b'\x00\x14' + keyhash
|
|
||||||
spk = b'\xA9\x14' + ngu.hash.hash160(redeem_script) + b'\x87'
|
|
||||||
elif addr_fmt == AF_P2WPKH:
|
|
||||||
spk = b'\x00\x14' + keyhash
|
|
||||||
else:
|
|
||||||
raise ValueError('bad address template: %s' % addr_fmt)
|
|
||||||
|
|
||||||
return spk, digest
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def p2sh_address(cls, addr_fmt, witdeem_script):
|
def p2sh_address(cls, addr_fmt, witdeem_script):
|
||||||
# Multisig and general P2SH support
|
# Multisig and general P2SH support
|
||||||
@ -127,14 +93,21 @@ class ChainsBase:
|
|||||||
# - returns: str(address)
|
# - returns: str(address)
|
||||||
|
|
||||||
assert addr_fmt & AFC_SCRIPT, 'for p2sh only'
|
assert addr_fmt & AFC_SCRIPT, 'for p2sh only'
|
||||||
_, digest = cls.script_pubkey(addr_fmt, script=witdeem_script)
|
assert witdeem_script, "need witness/redeem script"
|
||||||
|
|
||||||
if addr_fmt == AF_P2WSH:
|
if addr_fmt & AFC_SEGWIT:
|
||||||
|
digest = ngu.hash.sha256s(witdeem_script)
|
||||||
|
else:
|
||||||
|
digest = hash160(witdeem_script)
|
||||||
|
|
||||||
|
if addr_fmt & AFC_BECH32:
|
||||||
# bech32 encoded segwit p2sh
|
# bech32 encoded segwit p2sh
|
||||||
addr = ngu.codecs.segwit_encode(cls.bech32_hrp, 0, digest)
|
addr = ngu.codecs.segwit_encode(cls.bech32_hrp, 0, digest)
|
||||||
else:
|
elif addr_fmt == AF_P2WSH_P2SH:
|
||||||
# segwit p2wsh encoded as classic P2SH
|
# segwit p2wsh encoded as classic P2SH
|
||||||
# and P2SH classic
|
addr = ngu.codecs.b58_encode(cls.b58_script + hash160(b'\x00\x20' + digest))
|
||||||
|
else:
|
||||||
|
# P2SH classic
|
||||||
addr = ngu.codecs.b58_encode(cls.b58_script + digest)
|
addr = ngu.codecs.b58_encode(cls.b58_script + digest)
|
||||||
|
|
||||||
return addr
|
return addr
|
||||||
@ -144,8 +117,20 @@ class ChainsBase:
|
|||||||
# - renders a pubkey to an address
|
# - renders a pubkey to an address
|
||||||
# - works only with single-key addresses
|
# - works only with single-key addresses
|
||||||
assert not addr_fmt & AFC_SCRIPT
|
assert not addr_fmt & AFC_SCRIPT
|
||||||
spk, _ = cls.script_pubkey(addr_fmt, pubkey=pubkey)
|
|
||||||
return cls.render_address(spk)
|
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)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def address(cls, node, addr_fmt):
|
def address(cls, node, addr_fmt):
|
||||||
@ -254,7 +239,7 @@ class ChainsBase:
|
|||||||
return ngu.codecs.b58_encode(cls.b58_script + script[2:2+20])
|
return ngu.codecs.b58_encode(cls.b58_script + script[2:2+20])
|
||||||
|
|
||||||
# segwit v0 (P2WPKH, P2WSH)
|
# segwit v0 (P2WPKH, P2WSH)
|
||||||
if ll in (22, 34) and script[0] == 0 and script[1] in (0x14, 0x20) and (ll-2) == script[1]:
|
if script[0] == 0 and script[1] in (0x14, 0x20) and (ll-2) == script[1]:
|
||||||
return ngu.codecs.segwit_encode(cls.bech32_hrp, script[0], script[2:])
|
return ngu.codecs.segwit_encode(cls.bech32_hrp, script[0], script[2:])
|
||||||
|
|
||||||
# segwit v1 (P2TR) and later segwit version
|
# segwit v1 (P2TR) and later segwit version
|
||||||
@ -265,40 +250,56 @@ class ChainsBase:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def op_return(cls, script):
|
def op_return(cls, script):
|
||||||
try:
|
# returns decoded string op return data if script is op return otherwise None
|
||||||
gen = disassemble(script)
|
gen = disassemble(script)
|
||||||
item, opcode = next(gen)
|
script_type = next(gen)
|
||||||
except (StopIteration, ValueError):
|
if OP_RETURN not in script_type:
|
||||||
return None
|
return
|
||||||
|
|
||||||
if opcode != OP_RETURN:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
try:
|
data = next(gen)[0]
|
||||||
data, opcode = next(gen)
|
if data:
|
||||||
except StopIteration:
|
return data
|
||||||
return b"" # bare OP_RETURN
|
except StopIteration:
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
return b""
|
||||||
next(gen)
|
|
||||||
return None # extra ops/pushes -> raw script display
|
|
||||||
except StopIteration: pass
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
hrp = cls.bech32_hrp + "1"
|
||||||
|
if addr.startswith(hrp):
|
||||||
|
if addr.startswith(hrp+'p'):
|
||||||
|
# segwit v1 (any ver=1 script or address, but for now just taproot...)
|
||||||
|
return AF_P2TR
|
||||||
|
elif addr.startswith(hrp+'q'):
|
||||||
|
# segwit v0
|
||||||
|
return AF_P2WPKH if len(addr) < 55 else AF_P2WSH
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw = ngu.codecs.b58_decode(addr)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
# not base58, not an error
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if raw[0] == cls.b58_addr[0]:
|
||||||
|
return AF_CLASSIC
|
||||||
|
if raw[0] == cls.b58_script[0]:
|
||||||
|
return AF_P2SH
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
if isinstance(data, bytes):
|
|
||||||
return data
|
|
||||||
if data is None and opcode == 0:
|
|
||||||
return b"" # OP_RETURN OP_0
|
|
||||||
return None
|
|
||||||
|
|
||||||
class BitcoinMain(ChainsBase):
|
class BitcoinMain(ChainsBase):
|
||||||
# see <https://github.com/bitcoin/bitcoin/blob/master/src/chainparams.cpp#L140>
|
# see <https://github.com/bitcoin/bitcoin/blob/master/src/chainparams.cpp#L140>
|
||||||
ctype = 'BTC'
|
ctype = 'BTC'
|
||||||
name = 'Bitcoin Mainnet'
|
name = 'Bitcoin Mainnet'
|
||||||
ccc_min_block = BLOCK_HEIGHT
|
ccc_min_block = 892714 # Apr 16/2025
|
||||||
|
|
||||||
slip132 = {
|
slip132 = {
|
||||||
AF_CLASSIC: Slip132Version(0x0488B21E, 0x0488ADE4, 'x'),
|
AF_CLASSIC: Slip132Version(0x0488B21E, 0x0488ADE4, 'x'),
|
||||||
@ -338,11 +339,26 @@ class BitcoinTestnet(ChainsBase):
|
|||||||
b44_cointype = 1
|
b44_cointype = 1
|
||||||
|
|
||||||
|
|
||||||
class BitcoinRegtest(BitcoinTestnet):
|
class BitcoinRegtest(ChainsBase):
|
||||||
ctype = 'XRT'
|
ctype = 'XRT'
|
||||||
name = 'Bitcoin Regtest'
|
name = 'Bitcoin Regtest'
|
||||||
|
|
||||||
|
slip132 = {
|
||||||
|
AF_CLASSIC: Slip132Version(0x043587cf, 0x04358394, 't'),
|
||||||
|
AF_P2WPKH_P2SH: Slip132Version(0x044a5262, 0x044a4e28, 'u'),
|
||||||
|
AF_P2WPKH: Slip132Version(0x045f1cf6, 0x045f18bc, 'v'),
|
||||||
|
AF_P2WSH_P2SH: Slip132Version(0x024289ef, 0x024285b5, 'U'),
|
||||||
|
AF_P2WSH: Slip132Version(0x02575483, 0x02575048, 'V'),
|
||||||
|
}
|
||||||
|
|
||||||
bech32_hrp = 'bcrt'
|
bech32_hrp = 'bcrt'
|
||||||
|
|
||||||
|
b58_addr = bytes([111])
|
||||||
|
b58_script = bytes([196])
|
||||||
|
b58_privkey = bytes([239])
|
||||||
|
|
||||||
|
b44_cointype = 1
|
||||||
|
|
||||||
|
|
||||||
def get_chain(short_name):
|
def get_chain(short_name):
|
||||||
# lookup object from name: 'BTC' or 'XTN'
|
# lookup object from name: 'BTC' or 'XTN'
|
||||||
@ -370,7 +386,7 @@ def current_chain():
|
|||||||
# Overbuilt: will only be testnet and mainchain.
|
# Overbuilt: will only be testnet and mainchain.
|
||||||
AllChains = [BitcoinMain, BitcoinTestnet, BitcoinRegtest]
|
AllChains = [BitcoinMain, BitcoinTestnet, BitcoinRegtest]
|
||||||
|
|
||||||
def slip132_deserialize(xp):
|
def slip32_deserialize(xp):
|
||||||
# .. and classify chain and addr-type, as implied by prefix
|
# .. and classify chain and addr-type, as implied by prefix
|
||||||
node = ngu.hdnode.HDNode()
|
node = ngu.hdnode.HDNode()
|
||||||
version = node.deserialize(xp)
|
version = node.deserialize(xp)
|
||||||
@ -433,31 +449,17 @@ def parse_addr_fmt_str(addr_fmt):
|
|||||||
|
|
||||||
|
|
||||||
def af_to_bip44_purpose(addr_fmt):
|
def af_to_bip44_purpose(addr_fmt):
|
||||||
# Address format to BIP-44 "purpose" number
|
# single signature only
|
||||||
# - single signature only
|
|
||||||
return {AF_CLASSIC: 44,
|
return {AF_CLASSIC: 44,
|
||||||
AF_P2WPKH_P2SH: 49,
|
AF_P2WPKH_P2SH: 49,
|
||||||
AF_P2WPKH: 84}[addr_fmt]
|
AF_P2WPKH: 84}[addr_fmt]
|
||||||
|
|
||||||
|
|
||||||
def addr_fmt_label(addr_fmt):
|
def addr_fmt_label(addr_fmt):
|
||||||
# Text used in menus
|
|
||||||
return {AF_CLASSIC: "Classic P2PKH",
|
return {AF_CLASSIC: "Classic P2PKH",
|
||||||
AF_P2WPKH_P2SH: "P2SH-Segwit",
|
AF_P2WPKH_P2SH: "P2SH-Segwit",
|
||||||
AF_P2WPKH: "Segwit P2WPKH"}[addr_fmt]
|
AF_P2WPKH: "Segwit P2WPKH"}[addr_fmt]
|
||||||
|
|
||||||
|
|
||||||
def addr_fmt_str(addr_fmt):
|
|
||||||
# Short string codes used for address format (industry standard)
|
|
||||||
return {AF_CLASSIC: "p2pkh",
|
|
||||||
AF_BARE_PK: "p2pk",
|
|
||||||
AF_P2SH: "p2sh",
|
|
||||||
AF_P2TR: "p2tr",
|
|
||||||
AF_P2WPKH: "p2wpkh",
|
|
||||||
AF_P2WSH: "p2wsh",
|
|
||||||
AF_P2WPKH_P2SH: "p2sh-p2wpkh",
|
|
||||||
AF_P2WSH_P2SH: "p2sh-p2wsh"}[addr_fmt]
|
|
||||||
|
|
||||||
def verify_recover_pubkey(sig, digest):
|
def verify_recover_pubkey(sig, digest):
|
||||||
# verifies a message digest against a signature and recovers
|
# verifies a message digest against a signature and recovers
|
||||||
# the address type and public key that did the signing
|
# the address type and public key that did the signing
|
||||||
|
|||||||
@ -109,9 +109,7 @@ if has_qwerty:
|
|||||||
|
|
||||||
# These affect how 'ux stories' are rendered; they are control
|
# These affect how 'ux stories' are rendered; they are control
|
||||||
# characters on the output side of things, not input.
|
# characters on the output side of things, not input.
|
||||||
# - must be first char in line
|
OUT_CTRL_TITLE = '\x01' # must be first char in line: be a title line
|
||||||
OUT_CTRL_TITLE = '\x01' # be a title line
|
OUT_CTRL_ADDRESS = '\x02' # must be first char in line: it's a payment address
|
||||||
OUT_CTRL_ADDRESS = '\x02' # it's a payment address
|
|
||||||
OUT_CTRL_NOWRAP = '\x03' # do not word wrap this line
|
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
@ -51,7 +51,9 @@ def decode_utf_16_le(s):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
def read_var64(f):
|
def read_var64(f):
|
||||||
# Decode their silly 64-bit encoding.
|
'''
|
||||||
|
Decode their silly 64-bit encoding.
|
||||||
|
'''
|
||||||
first = ord(f.read(1))
|
first = ord(f.read(1))
|
||||||
if first < 128:
|
if first < 128:
|
||||||
return first
|
return first
|
||||||
@ -98,7 +100,7 @@ def check_file_headers(f):
|
|||||||
# assume f is seekable
|
# assume f is seekable
|
||||||
fh = FileHeader.read(f)
|
fh = FileHeader.read(f)
|
||||||
|
|
||||||
if not fh.has_good_magic():
|
if not fh.has_good_magic:
|
||||||
raise ValueError("Bad magic bytes")
|
raise ValueError("Bad magic bytes")
|
||||||
|
|
||||||
# read only first header
|
# read only first header
|
||||||
@ -111,21 +113,22 @@ def check_file_headers(f):
|
|||||||
if sh.size > 10000:
|
if sh.size > 10000:
|
||||||
raise ValueError("Second header too big")
|
raise ValueError("Second header too big")
|
||||||
|
|
||||||
# FileHeader.read() always reads exactly calcsize('<6sBBL') = 12 bytes
|
# capture this spot
|
||||||
# SectionHeader.read() always reads exactly calcsize('<QQL') = 20 bytes
|
# TODO 'data_start' unused
|
||||||
# after those two calls, f.tell() is always start_pos + 32
|
data_start = f.tell() # expect 0x20
|
||||||
# assert f.tell() == 0x20 # expect 0x20
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
f.seek(sh.offset, 1)
|
f.seek(sh.offset, 1)
|
||||||
th = f.read(sh.size)
|
th = f.read(sh.size)
|
||||||
assert len(th) == sh.size, "Truncated file?"
|
if len(th) != sh.size:
|
||||||
|
raise IndexError("Truncated file?")
|
||||||
|
|
||||||
# Look for properties about compression. this could be
|
# Look for properties about compression. this could be
|
||||||
# faked-out but good enough for now
|
# faked-out but good enough for now
|
||||||
assert b'\x24\x06\xf1\x07\x01' in th, "Not marked as AES+SHA encrypted?"
|
if b'\x24\x06\xf1\x07\x01' not in th:
|
||||||
|
raise RuntimeError("Not marked as AES+SHA encrypted?")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError("Confused file? %s" % e)
|
raise ValueError("Confused file? %s" % e.message)
|
||||||
|
|
||||||
if masked_crc(th) != sh.crc:
|
if masked_crc(th) != sh.crc:
|
||||||
raise ValueError("Trailing header has wrong CRC")
|
raise ValueError("Trailing header has wrong CRC")
|
||||||
@ -173,6 +176,7 @@ class FileHeader(object):
|
|||||||
return masked_crc(self.bits)
|
return masked_crc(self.bits)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SectionHeader(namedtuple('SectionHeader', ['offset', 'size', 'crc' ])):
|
class SectionHeader(namedtuple('SectionHeader', ['offset', 'size', 'crc' ])):
|
||||||
@classmethod
|
@classmethod
|
||||||
def read(cls, f):
|
def read(cls, f):
|
||||||
@ -209,7 +213,6 @@ class SectionHeader(namedtuple('SectionHeader', ['offset', 'size', 'crc' ])):
|
|||||||
def actual_crc(self):
|
def actual_crc(self):
|
||||||
return masked_crc(self.bits)
|
return masked_crc(self.bits)
|
||||||
|
|
||||||
|
|
||||||
class Builder(object):
|
class Builder(object):
|
||||||
def __init__(self, password=None, salt_len=16, iv_len=16, rounds_pow=13, progress_fcn=None):
|
def __init__(self, password=None, salt_len=16, iv_len=16, rounds_pow=13, progress_fcn=None):
|
||||||
self.rounds_pow = rounds_pow # standard is 19, 16 and 17 work fine
|
self.rounds_pow = rounds_pow # standard is 19, 16 and 17 work fine
|
||||||
|
|||||||
@ -11,15 +11,6 @@ from bbqr import TYPE_LABELS
|
|||||||
from utils import decode_bip21_text
|
from utils import decode_bip21_text
|
||||||
|
|
||||||
|
|
||||||
def decode_qr_text(got):
|
|
||||||
if isinstance(got, str):
|
|
||||||
return got
|
|
||||||
|
|
||||||
try:
|
|
||||||
return got.decode()
|
|
||||||
except UnicodeError:
|
|
||||||
raise QRDecodeExplained('UTF-8 decode failed')
|
|
||||||
|
|
||||||
def decode_seed_qr(data):
|
def decode_seed_qr(data):
|
||||||
# SeedQR: 4 digit groups of index into word list
|
# SeedQR: 4 digit groups of index into word list
|
||||||
parts = [data[pos:pos + 4] for pos in range(0, len(data), 4)]
|
parts = [data[pos:pos + 4] for pos in range(0, len(data), 4)]
|
||||||
@ -48,8 +39,6 @@ def decode_secret(got):
|
|||||||
# - xprv / tprv
|
# - xprv / tprv
|
||||||
# - words (either full or prefixes, case insensitive)
|
# - words (either full or prefixes, case insensitive)
|
||||||
# - SeedQR (github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md)
|
# - SeedQR (github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md)
|
||||||
# - word lists are NOT BIP-39-checksum-validated here. Callers that
|
|
||||||
# require a valid seed must run bip39.a2b_words(...)
|
|
||||||
|
|
||||||
if len(got) > 300:
|
if len(got) > 300:
|
||||||
raise ValueError("Too big.")
|
raise ValueError("Too big.")
|
||||||
@ -62,7 +51,7 @@ def decode_secret(got):
|
|||||||
# xprv or tprv: private key import for sure
|
# xprv or tprv: private key import for sure
|
||||||
# - verify checksum is right
|
# - verify checksum is right
|
||||||
try:
|
try:
|
||||||
ngu.codecs.b58_decode(got)
|
raw = ngu.codecs.b58_decode(got)
|
||||||
except:
|
except:
|
||||||
raise ValueError('corrupt xprv?')
|
raise ValueError('corrupt xprv?')
|
||||||
|
|
||||||
@ -70,9 +59,17 @@ def decode_secret(got):
|
|||||||
|
|
||||||
if len(got) in (51, 52):
|
if len(got) in (51, 52):
|
||||||
try:
|
try:
|
||||||
from wif import decode_wif
|
raw = ngu.codecs.b58_decode(got)
|
||||||
kp, testnet, compressed = decode_wif(got)
|
if raw[0] in (0xef, 0x80):
|
||||||
return 'wif', (got, kp, compressed, testnet)
|
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)
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
taste = got.strip().lower()
|
taste = got.strip().lower()
|
||||||
@ -119,8 +116,11 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
|
|||||||
return got.decode()
|
return got.decode()
|
||||||
|
|
||||||
if ty == 'P':
|
if ty == 'P':
|
||||||
# `got` is the literal 'PSRAM' from BBQrPsramStorage when data already there
|
# may already be in PSRAM, avoid a copy here
|
||||||
# otherwise it's real bytes
|
from glob import PSRAM
|
||||||
|
if PSRAM.is_at(got, 0):
|
||||||
|
got = 'PSRAM' # see qr_psbt_sign()
|
||||||
|
|
||||||
return 'psbt', (None, final_size, got)
|
return 'psbt', (None, final_size, got)
|
||||||
|
|
||||||
elif ty == 'T':
|
elif ty == 'T':
|
||||||
@ -128,10 +128,9 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
|
|||||||
|
|
||||||
elif ty == 'U':
|
elif ty == 'U':
|
||||||
# continue thru code below for TEXT
|
# continue thru code below for TEXT
|
||||||
got = decode_qr_text(got)
|
pass
|
||||||
|
|
||||||
elif ty == 'J':
|
elif ty == 'J':
|
||||||
got = decode_qr_text(got)
|
|
||||||
what = "json"
|
what = "json"
|
||||||
if "msg" in got:
|
if "msg" in got:
|
||||||
what = "smsg"
|
what = "smsg"
|
||||||
@ -140,11 +139,6 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
|
|||||||
|
|
||||||
elif ty in 'RSE':
|
elif ty in 'RSE':
|
||||||
# key-teleport related
|
# key-teleport related
|
||||||
|
|
||||||
from pincodes import pa
|
|
||||||
if pa.hobbled_mode and ty != 'E':
|
|
||||||
raise QRDecodeExplained("KT Blocked")
|
|
||||||
|
|
||||||
if ty == 'R' and len(got) != 33:
|
if ty == 'R' and len(got) != 33:
|
||||||
raise QRDecodeExplained("Truncated KT RX")
|
raise QRDecodeExplained("Truncated KT RX")
|
||||||
|
|
||||||
@ -196,7 +190,12 @@ def decode_short_text(got):
|
|||||||
# - if bad checksum on bitcoin addr, we treat as text... since might be
|
# - if bad checksum on bitcoin addr, we treat as text... since might be
|
||||||
# return: what-it-is, (tuple)
|
# return: what-it-is, (tuple)
|
||||||
|
|
||||||
got = decode_qr_text(got)
|
if not isinstance(got, str):
|
||||||
|
# decode utf-8
|
||||||
|
try:
|
||||||
|
got = got.decode()
|
||||||
|
except UnicodeError:
|
||||||
|
raise QRDecodeExplained('UTF-8 decode failed')
|
||||||
|
|
||||||
# might be a PSBT?
|
# might be a PSBT?
|
||||||
if len(got) > 100:
|
if len(got) > 100:
|
||||||
@ -231,11 +230,10 @@ def decode_short_text(got):
|
|||||||
cc_ms_pat = r"[0-9a-fA-F]+\s*:\s*[xtyYzZuUvV]pub[1-9A-HJ-NP-Za-km-z]+"
|
cc_ms_pat = r"[0-9a-fA-F]+\s*:\s*[xtyYzZuUvV]pub[1-9A-HJ-NP-Za-km-z]+"
|
||||||
rgx = ure.compile(cc_ms_pat)
|
rgx = ure.compile(cc_ms_pat)
|
||||||
# go line by line and match above, once 2 matches observed - considered multisig
|
# go line by line and match above, once 2 matches observed - considered multisig
|
||||||
# important to not use ure.search for big strings (can run out of stack);
|
# important to not use ure.search for big strings (can run out of stack)
|
||||||
# a real line here is a "<8-hex xfp>: <xpub>" key (~121 chars)
|
|
||||||
c = 0 # match count
|
c = 0 # match count
|
||||||
for l in got.split("\n"):
|
for l in got.split("\n"):
|
||||||
if len(l) <= 150 and rgx.search(l):
|
if rgx.search(l):
|
||||||
c += 1
|
c += 1
|
||||||
if c > 1:
|
if c > 1:
|
||||||
return 'multi', (got,)
|
return 'multi', (got,)
|
||||||
|
|||||||
@ -2,8 +2,9 @@
|
|||||||
#
|
#
|
||||||
# display.py - OLED rendering
|
# display.py - OLED rendering
|
||||||
#
|
#
|
||||||
import machine, uzlib, ckcc, utime, version
|
import machine, uzlib, ckcc, utime
|
||||||
from ssd1306 import SSD1306_SPI
|
from ssd1306 import SSD1306_SPI
|
||||||
|
from version import is_devmode
|
||||||
import framebuf
|
import framebuf
|
||||||
from graphics_mk4 import Graphics
|
from graphics_mk4 import Graphics
|
||||||
from charcodes import OUT_CTRL_TITLE, OUT_CTRL_ADDRESS
|
from charcodes import OUT_CTRL_TITLE, OUT_CTRL_ADDRESS
|
||||||
@ -34,14 +35,11 @@ class Display:
|
|||||||
dc_pin = Pin('PA8', Pin.OUT)
|
dc_pin = Pin('PA8', Pin.OUT)
|
||||||
cs_pin = Pin('PA4', Pin.OUT)
|
cs_pin = Pin('PA4', Pin.OUT)
|
||||||
|
|
||||||
if version.mk_num == 5:
|
try:
|
||||||
# Early revs (A-D) needed this pin asserted to enable +12v to OLED
|
self.dis = SSD1306_SPI(128, 64, spi, dc_pin, reset_pin, cs_pin)
|
||||||
# - removed in rev E and later boards, but keep here for dev boards
|
except OSError:
|
||||||
# - remove this in 2027
|
print("OLED unplugged?")
|
||||||
vcc_en = Pin('V12EN', Pin.OUT) # aka PC1
|
raise
|
||||||
vcc_en(1)
|
|
||||||
|
|
||||||
self.dis = SSD1306_SPI(128, 64, spi, dc_pin, reset_pin, cs_pin, is_mk5=(version.mk_num==5))
|
|
||||||
|
|
||||||
self.last_bar_update = 0
|
self.last_bar_update = 0
|
||||||
self.clear()
|
self.clear()
|
||||||
@ -144,7 +142,7 @@ class Display:
|
|||||||
self.icon(128-3, 1, 'scroll')
|
self.icon(128-3, 1, 'scroll')
|
||||||
self.dis.fill_rect(128-2, pos, 1, bh, 1)
|
self.dis.fill_rect(128-2, pos, 1, bh, 1)
|
||||||
|
|
||||||
if version.is_devmode and not ckcc.is_simulator():
|
if is_devmode and not ckcc.is_simulator():
|
||||||
self.dis.fill_rect(128-6, 20, 5, 21, 1)
|
self.dis.fill_rect(128-6, 20, 5, 21, 1)
|
||||||
self.text(-2, 21, 'D', font=FontTiny, invert=1)
|
self.text(-2, 21, 'D', font=FontTiny, invert=1)
|
||||||
self.text(-2, 28, 'E', font=FontTiny, invert=1)
|
self.text(-2, 28, 'E', font=FontTiny, invert=1)
|
||||||
@ -152,14 +150,11 @@ class Display:
|
|||||||
|
|
||||||
def fullscreen(self, msg, percent=None, line2=None):
|
def fullscreen(self, msg, percent=None, line2=None):
|
||||||
# show a simple message "fullscreen".
|
# show a simple message "fullscreen".
|
||||||
|
# - 'line2' not supported on smaller screen sizes, ignore
|
||||||
self.clear()
|
self.clear()
|
||||||
y = 14
|
y = 14
|
||||||
self.text(None, y, msg, font=FontLarge)
|
self.text(None, y, msg, font=FontLarge)
|
||||||
|
|
||||||
if line2:
|
|
||||||
# 21 + 6 ie. FontLarge.height of above text + FontTiny.height as space between
|
|
||||||
self.text(None, y + 27, line2, font=FontSmall)
|
|
||||||
|
|
||||||
if percent is not None:
|
if percent is not None:
|
||||||
self.progress_bar(percent)
|
self.progress_bar(percent)
|
||||||
self.show()
|
self.show()
|
||||||
@ -206,20 +201,61 @@ class Display:
|
|||||||
|
|
||||||
def busy_bar(self, enable):
|
def busy_bar(self, enable):
|
||||||
# Render a continuous activity (not progress) bar in lower 8 lines of display
|
# Render a continuous activity (not progress) bar in lower 8 lines of display
|
||||||
#
|
# - using OLED itself to do the animation, so smooth and CPU free
|
||||||
|
# - cannot preserve bottom 8 lines, since we have to destructively write there
|
||||||
|
# - assumes normal horz addr mode: 0x20, 0x00
|
||||||
|
# - speed_code=>framedelay: 0=5fr, 1=64fr, 2=128, 3=256, 4=3, 5=4, 6=25, 7=2frames
|
||||||
|
# unused: assert 0 <= speed_code <= 7
|
||||||
|
|
||||||
|
setup = bytes([
|
||||||
|
0x21, 0x00, 0x7f, # setup column address range (start, end): 0-127
|
||||||
|
0x22, 7, 7, # setup page start/end address: page 7=last 8 lines
|
||||||
|
])
|
||||||
|
animate = bytes([
|
||||||
|
0x2e, # stop animations in progress
|
||||||
|
0x26, # scroll leftwards (stock ticker mode)
|
||||||
|
0, # placeholder
|
||||||
|
7, # start 'page' (vertical)
|
||||||
|
5, # "speed_code" # scroll speed: 7=fastest, but no order to it
|
||||||
|
7, # end 'page'
|
||||||
|
0, 0xff, # placeholders
|
||||||
|
0x2f # start
|
||||||
|
])
|
||||||
|
|
||||||
|
cleanup = bytes([
|
||||||
|
0x2e, # stop animation
|
||||||
|
0x20, 0x00, # horz addr-ing mode
|
||||||
|
0x21, 0x00, 0x7f, # setup column address range (start, end): 0-127
|
||||||
|
0x22, 7, 7, # setup page start/end address: page 7=last 8 lines
|
||||||
|
])
|
||||||
|
|
||||||
if not enable:
|
if not enable:
|
||||||
self.dis.busy_bar(False, None)
|
# stop animation, and redraw old (new) screen
|
||||||
|
self.write_cmds(cleanup)
|
||||||
self.show()
|
self.show()
|
||||||
else:
|
else:
|
||||||
# Need a pattern that repeats nicely mod 128
|
|
||||||
# - each byte here is a vertical column, 8 pixels tall, MSB at bottom
|
|
||||||
pat = bytes(0x80 if (x%4)<2 else 0x0 for x in range(128))
|
|
||||||
|
|
||||||
self.dis.busy_bar(True, pat)
|
# 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))
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
def set_brightness(self, val):
|
def set_brightness(self, val):
|
||||||
# normal = 0x7f, brightness=0xff, dim=0x00 (but they are all very similar)
|
# normal = 0x7f, brightness=0xff, dim=0x00 (but they are all very similar)
|
||||||
return self.dis.contrast(val)
|
self.dis.write_cmd(0x81) # Set Contrast Control
|
||||||
|
self.dis.write_cmd(val)
|
||||||
|
|
||||||
def menu_draw(self, ry, msg, is_sel, is_checked, space_indicators):
|
def menu_draw(self, ry, msg, is_sel, is_checked, space_indicators):
|
||||||
# draw a menu item, perhaps selected, checked.
|
# draw a menu item, perhaps selected, checked.
|
||||||
@ -230,18 +266,17 @@ class Display:
|
|||||||
if is_sel:
|
if is_sel:
|
||||||
self.dis.fill_rect(0, y, Display.WIDTH, h-1, 1)
|
self.dis.fill_rect(0, y, Display.WIDTH, h-1, 1)
|
||||||
self.icon(2, y, 'wedge', invert=1)
|
self.icon(2, y, 'wedge', invert=1)
|
||||||
nx = self.text(x, y, msg, invert=1)
|
self.text(x, y, msg, invert=1)
|
||||||
else:
|
else:
|
||||||
nx = self.text(x, y, msg)
|
self.text(x, y, msg)
|
||||||
|
|
||||||
# LATER: removed because caused confusion w/ underscore
|
# LATER: removed because caused confusion w/ underscore
|
||||||
#if msg[0] == ' ' and space_indicators:
|
#if msg[0] == ' ' and space_indicators:
|
||||||
# see also graphics/mono/space.txt
|
# see also graphics/mono/space.txt
|
||||||
#self.icon(x-2, y+9, 'space', invert=is_sel)
|
#self.icon(x-2, y+9, 'space', invert=is_sel)
|
||||||
|
|
||||||
if is_checked and nx <= 113:
|
if is_checked:
|
||||||
# omit checkmark if it doesn't fit
|
self.icon(108, y, 'selected', invert=is_sel)
|
||||||
self.icon(113, y, 'selected', invert=is_sel)
|
|
||||||
|
|
||||||
def menu_show(self, *a):
|
def menu_show(self, *a):
|
||||||
self.show()
|
self.show()
|
||||||
@ -293,25 +328,15 @@ class Display:
|
|||||||
# no status bar on Mk4
|
# no status bar on Mk4
|
||||||
return
|
return
|
||||||
|
|
||||||
def draw_qr_error(self, idx_hint, msg):
|
|
||||||
self.clear()
|
|
||||||
lm = 4
|
|
||||||
bw = 54
|
|
||||||
y = (self.HEIGHT - bw) // 2
|
|
||||||
# empty rectangle
|
|
||||||
self.dis.fill_rect(lm, y, bw, bw, 1)
|
|
||||||
self.dis.fill_rect(lm+1, y+1, bw-2, bw-2, 0)
|
|
||||||
# error in rectangle - handpicked position
|
|
||||||
self.text(lm+5,y+10, "QR too")
|
|
||||||
self.text(lm+16,y+24, "big")
|
|
||||||
self._draw_qr_display(bw, lm, msg, False, None, idx_hint, False)
|
|
||||||
|
|
||||||
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert,
|
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert,
|
||||||
is_addr=False, force_msg=False, side_msg=None):
|
is_addr=False, force_msg=False, is_change=False):
|
||||||
# 'sidebar' is a pre-formated obj to show to right of QR -- oled life
|
# 'sidebar' is a pre-formated obj to show to right of QR -- oled life
|
||||||
# - 'msg' will appear to right if very short, else under in tiny
|
# - 'msg' will appear to right if very short, else under in tiny
|
||||||
# - ignores "is_addr" because exactly zero space to do anything special
|
# - ignores "is_addr" because exactly zero space to do anything special
|
||||||
|
from utils import word_wrap
|
||||||
|
|
||||||
self.clear()
|
self.clear()
|
||||||
|
|
||||||
w = qr_data.width()
|
w = qr_data.width()
|
||||||
if w <= 29:
|
if w <= 29:
|
||||||
# version 1,2,3 => we can double-up the pixels
|
# version 1,2,3 => we can double-up the pixels
|
||||||
@ -351,19 +376,12 @@ class Display:
|
|||||||
gly = framebuf.FrameBuffer(bytearray(packed), w, w, framebuf.MONO_HLSB)
|
gly = framebuf.FrameBuffer(bytearray(packed), w, w, framebuf.MONO_HLSB)
|
||||||
self.dis.blit(gly, XO, YO, 1)
|
self.dis.blit(gly, XO, YO, 1)
|
||||||
|
|
||||||
self._draw_qr_display(bw, lm, msg, is_alnum, sidebar, idx_hint, invert, is_addr, side_msg)
|
|
||||||
|
|
||||||
def _draw_qr_display(self, bw, lm, msg, is_alnum, sidebar, idx_hint, invert,
|
|
||||||
is_addr=False, side_msg=None):
|
|
||||||
# does not draw actual QR, but all other things in the screen
|
|
||||||
from utils import word_wrap
|
|
||||||
|
|
||||||
if not sidebar and not msg:
|
if not sidebar and not msg:
|
||||||
pass
|
pass
|
||||||
elif not sidebar and ((len(msg) > (5*7)) or side_msg):
|
elif not sidebar and ((len(msg) > (5*7)) or is_change):
|
||||||
# use FontTiny and word wrap (will just split if no spaces)
|
# use FontTiny and word wrap (will just split if no spaces)
|
||||||
# native segwit addresses and taproot
|
# native segwit addresses and taproot
|
||||||
# if 'side_msg' also p2pkh and p2sh fall into this category as space is needed for "CHANGE BACK" text
|
# if is_change=True also p2pkh and p2sh fall into this category as space is needed for "CHANGE"
|
||||||
x = bw + lm + 4
|
x = bw + lm + 4
|
||||||
ww = ((128 - x)//4) - 1 # char width avail
|
ww = ((128 - x)//4) - 1 # char width avail
|
||||||
y = 1
|
y = 1
|
||||||
@ -379,11 +397,8 @@ class Display:
|
|||||||
self.text(x, y, line, FontTiny)
|
self.text(x, y, line, FontTiny)
|
||||||
y += 8
|
y += 8
|
||||||
|
|
||||||
if side_msg and (len(side_msg) < 15):
|
if is_addr and is_change:
|
||||||
y_pos = y + 8
|
self.text(x+4, y+8, "CHANGE BACK", FontTiny)
|
||||||
# only render if there is space
|
|
||||||
if (self.HEIGHT - y_pos) >= FontTiny.height:
|
|
||||||
self.text(x+4, y+8, side_msg, FontTiny)
|
|
||||||
else:
|
else:
|
||||||
# hand-positioned for known cases
|
# hand-positioned for known cases
|
||||||
# - sidebar = (text, #of char per line)
|
# - sidebar = (text, #of char per line)
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from menu import MenuItem, MenuSystem
|
|||||||
from ubinascii import hexlify as b2a_hex
|
from ubinascii import hexlify as b2a_hex
|
||||||
from ubinascii import b2a_base64
|
from ubinascii import b2a_base64
|
||||||
from msgsign import write_sig_file
|
from msgsign import write_sig_file
|
||||||
from utils import xfp2str, swab32, node_from_privkey
|
from utils import xfp2str, swab32
|
||||||
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
|
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
|
||||||
|
|
||||||
BIP85_PWD_LEN = 21
|
BIP85_PWD_LEN = 21
|
||||||
@ -124,7 +124,8 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
|
|||||||
|
|
||||||
msg = "Password Index?" if picked == 7 else "Index Number?"
|
msg = "Password Index?" if picked == 7 else "Index Number?"
|
||||||
index = await ux_enter_bip32_index(msg, unlimited=settings.get("b85max", False))
|
index = await ux_enter_bip32_index(msg, unlimited=settings.get("b85max", False))
|
||||||
if index is None: return
|
if index is None:
|
||||||
|
return
|
||||||
|
|
||||||
dis.fullscreen("Working...")
|
dis.fullscreen("Working...")
|
||||||
new_secret, width, s_mode, path = bip85_derive(picked, index)
|
new_secret, width, s_mode, path = bip85_derive(picked, index)
|
||||||
@ -179,7 +180,7 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
|
|||||||
elif s_mode == 'xprv':
|
elif s_mode == 'xprv':
|
||||||
# Raw XPRV value.
|
# Raw XPRV value.
|
||||||
ch, pk = new_secret[0:32], new_secret[32:64]
|
ch, pk = new_secret[0:32], new_secret[32:64]
|
||||||
master_node = node_from_privkey(pk, ch)
|
master_node = ngu.hdnode.HDNode().from_chaincode_privkey(ch, pk)
|
||||||
node = master_node
|
node = master_node
|
||||||
|
|
||||||
encoded = stash.SecretStash.encode(xprv=master_node)
|
encoded = stash.SecretStash.encode(xprv=master_node)
|
||||||
@ -204,12 +205,14 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
|
|||||||
if new_secret:
|
if new_secret:
|
||||||
msg += '\n\nRaw Entropy:\n' + str(b2a_hex(new_secret), 'ascii')
|
msg += '\n\nRaw Entropy:\n' + str(b2a_hex(new_secret), 'ascii')
|
||||||
|
|
||||||
key6 = 'to type %s over USB' % s_mode
|
# Add the standard export prompt at the end, with extra (5) option sometimes.
|
||||||
|
|
||||||
key0 = None
|
key0 = None
|
||||||
if encoded is not None:
|
if encoded is not None:
|
||||||
key0 = 'to switch to derived secret'
|
key0 = 'to switch to derived secret'
|
||||||
|
elif s_mode == 'pw':
|
||||||
prompt, escape = export_prompt_builder('data', key0=key0, key6=key6,
|
key0 = 'to type password over USB'
|
||||||
|
prompt, escape = export_prompt_builder('data', key0=key0,
|
||||||
no_qr=(not qr), force_prompt=True)
|
no_qr=(not qr), force_prompt=True)
|
||||||
title = None
|
title = None
|
||||||
if node:
|
if node:
|
||||||
@ -221,9 +224,7 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
|
|||||||
ch = await ux_show_story(msg+'\n\n'+prompt, title=title, escape=escape,
|
ch = await ux_show_story(msg+'\n\n'+prompt, title=title, escape=escape,
|
||||||
strict_escape=True, sensitive=True)
|
strict_escape=True, sensitive=True)
|
||||||
choice = import_export_prompt_decode(ch)
|
choice = import_export_prompt_decode(ch)
|
||||||
if choice == KEY_CANCEL:
|
if isinstance(choice, dict):
|
||||||
break
|
|
||||||
elif isinstance(choice, dict):
|
|
||||||
# write to SD card or Virtual Disk: simple text file
|
# write to SD card or Virtual Disk: simple text file
|
||||||
dis.fullscreen("Saving...")
|
dis.fullscreen("Saving...")
|
||||||
try:
|
try:
|
||||||
@ -240,33 +241,33 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
|
|||||||
await needs_microsd()
|
await needs_microsd()
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await ux_show_story('Failed to write!\n\n'+str(e))
|
await ux_show_story('Failed to write!\n\n\n'+str(e))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
story = "Filename is:\n\n%s" % out_fn
|
story = "Filename is:\n\n%s" % out_fn
|
||||||
story += "\n\nSignature filename is:\n\n%s" % sig_nice
|
story += "\n\nSignature filename is:\n\n%s" % sig_nice
|
||||||
await ux_show_story(story, title='Saved')
|
await ux_show_story(story, title='Saved')
|
||||||
|
elif choice == KEY_CANCEL:
|
||||||
|
break
|
||||||
elif choice == KEY_QR:
|
elif choice == KEY_QR:
|
||||||
from ux import show_qr_code
|
from ux import show_qr_code
|
||||||
await show_qr_code(qr, qr_alnum, is_secret=True)
|
await show_qr_code(qr, qr_alnum, is_secret=True)
|
||||||
|
elif choice == '0':
|
||||||
elif (choice == '0') and (encoded is not None):
|
if s_mode == 'pw':
|
||||||
# switch over to new secret!
|
# gets confirmation then types it
|
||||||
dis.fullscreen("Applying...")
|
await single_send_keystrokes(qr, path)
|
||||||
from actions import goto_top_menu
|
elif encoded is not None:
|
||||||
from glob import settings
|
# switch over to new secret!
|
||||||
xfp_str = xfp2str(settings.get("xfp", 0))
|
dis.fullscreen("Applying...")
|
||||||
await seed.set_ephemeral_seed(
|
from actions import goto_top_menu
|
||||||
encoded,
|
from glob import settings
|
||||||
origin='BIP85 Derived from [%s], index=%d' % (xfp_str, index)
|
xfp_str = xfp2str(settings.get("xfp", 0))
|
||||||
)
|
await seed.set_ephemeral_seed(
|
||||||
goto_top_menu()
|
encoded,
|
||||||
break
|
origin='BIP85 Derived from [%s], index=%d' % (xfp_str, index)
|
||||||
|
)
|
||||||
elif choice == "6":
|
goto_top_menu()
|
||||||
# gets confirmation then types it
|
break
|
||||||
await single_send_keystrokes(qr, path)
|
|
||||||
|
|
||||||
elif NFC and choice == KEY_NFC:
|
elif NFC and choice == KEY_NFC:
|
||||||
# Share any of these over NFC
|
# Share any of these over NFC
|
||||||
@ -291,7 +292,7 @@ async def password_entry(*args, **kwargs):
|
|||||||
|
|
||||||
while True:
|
while True:
|
||||||
the_ux.pop()
|
the_ux.pop()
|
||||||
index = await ux_enter_bip32_index("Password Index?")
|
index = await ux_enter_bip32_index("Password Index?", can_cancel=True)
|
||||||
if index is None:
|
if index is None:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@ -19,10 +19,10 @@ class CCBusyError(RuntimeError):
|
|||||||
# HSM is blocking your action
|
# HSM is blocking your action
|
||||||
class HSMDenied(RuntimeError):
|
class HSMDenied(RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class HSMCMDDisabled(RuntimeError):
|
class HSMCMDDisabled(RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# PSBT / transaction related
|
# PSBT / transaction related
|
||||||
class FatalPSBTIssue(RuntimeError):
|
class FatalPSBTIssue(RuntimeError):
|
||||||
pass
|
pass
|
||||||
@ -51,12 +51,8 @@ class QRDecodeExplained(ValueError):
|
|||||||
class UnknownAddressExplained(ValueError):
|
class UnknownAddressExplained(ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# We're not going to (co-)sign using spending policy features
|
# We're not going to co-sign using CCC feature
|
||||||
class SpendPolicyViolation(RuntimeError):
|
class CCCPolicyViolationError(RuntimeError):
|
||||||
pass
|
|
||||||
|
|
||||||
# data too big for simple QR
|
|
||||||
class QRTooBigError(ValueError):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
@ -12,7 +12,6 @@ from msgsign import write_sig_file
|
|||||||
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH
|
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH
|
||||||
from charcodes import KEY_NFC, KEY_CANCEL, KEY_QR
|
from charcodes import KEY_NFC, KEY_CANCEL, KEY_QR
|
||||||
from ownership import OWNERSHIP
|
from ownership import OWNERSHIP
|
||||||
from exceptions import QRTooBigError
|
|
||||||
|
|
||||||
async def export_by_qr(body, label, type_code, force_bbqr=False):
|
async def export_by_qr(body, label, type_code, force_bbqr=False):
|
||||||
# render as QR and show on-screen
|
# render as QR and show on-screen
|
||||||
@ -20,10 +19,10 @@ async def export_by_qr(body, label, type_code, force_bbqr=False):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if force_bbqr or len(body) > 2000:
|
if force_bbqr or len(body) > 2000:
|
||||||
raise QRTooBigError
|
raise ValueError
|
||||||
|
|
||||||
await show_qr_code(body)
|
await show_qr_code(body)
|
||||||
except QRTooBigError:
|
except (ValueError, RuntimeError, TypeError):
|
||||||
if version.has_qwerty:
|
if version.has_qwerty:
|
||||||
# do BBQr on Q
|
# do BBQr on Q
|
||||||
from ux_q1 import show_bbqr_codes
|
from ux_q1 import show_bbqr_codes
|
||||||
@ -35,14 +34,11 @@ async def export_by_qr(body, label, type_code, force_bbqr=False):
|
|||||||
|
|
||||||
|
|
||||||
async def export_contents(title, contents, fname_pattern, derive=None, addr_fmt=None,
|
async def export_contents(title, contents, fname_pattern, derive=None, addr_fmt=None,
|
||||||
is_json=False, force_bbqr=False, force_prompt=False, direct_way=None,
|
is_json=False, force_bbqr=False, force_prompt=False):
|
||||||
intro="", footer="", ux_title=None):
|
|
||||||
# export text and json files while offering NFC, QR & Vdisk
|
# export text and json files while offering NFC, QR & Vdisk
|
||||||
# produces signed export in case of SD/Vdisk (signed with key at deriv and addr_fmt)
|
# produces signed export in case of SD/Vdisk (signed with key at deriv and addr_fmt)
|
||||||
# checks if suitable to offer QR export on Mk4
|
# checks if suitable to offer QR export on Mk4
|
||||||
# argument contents can support function that generates content
|
# 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 glob import dis, NFC, VD
|
||||||
from files import CardSlot, CardMissingError, needs_microsd
|
from files import CardSlot, CardMissingError, needs_microsd
|
||||||
from qrs import MAX_V11_CHAR_LIMIT
|
from qrs import MAX_V11_CHAR_LIMIT
|
||||||
@ -57,55 +53,51 @@ async def export_contents(title, contents, fname_pattern, derive=None, addr_fmt=
|
|||||||
|
|
||||||
sig = not (derive is None and addr_fmt is None)
|
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:
|
while True:
|
||||||
if direct_way is None:
|
ch = await import_export_prompt("%s file" % title,
|
||||||
ch = await import_export_prompt("%s file" % title, intro=intro, footnotes=footer,
|
force_prompt=force_prompt, no_qr=no_qr)
|
||||||
force_prompt=force_prompt, no_qr=no_qr, title=ux_title)
|
|
||||||
if ch == KEY_CANCEL:
|
if ch == KEY_CANCEL:
|
||||||
break
|
break
|
||||||
elif ch == KEY_QR:
|
elif ch == KEY_QR:
|
||||||
await export_by_qr(contents, title, "J" if is_json else "U", force_bbqr=force_bbqr)
|
await export_by_qr(contents, title, "J" if is_json else "U", force_bbqr=force_bbqr)
|
||||||
|
continue
|
||||||
elif ch == KEY_NFC:
|
elif ch == KEY_NFC:
|
||||||
if is_json:
|
if is_json:
|
||||||
await NFC.share_json(contents)
|
await NFC.share_json(contents)
|
||||||
else:
|
else:
|
||||||
await NFC.share_text(contents)
|
await NFC.share_text(contents)
|
||||||
else:
|
continue
|
||||||
# SD/VDisk
|
|
||||||
# choose a filename
|
|
||||||
try:
|
|
||||||
dis.fullscreen("Saving...")
|
|
||||||
with CardSlot(**ch) as card:
|
|
||||||
fname, nice = card.pick_filename(fname_pattern)
|
|
||||||
|
|
||||||
# do actual write
|
# choose a filename
|
||||||
with open(fname, 'wt' if is_json else 'wb') as fd:
|
try:
|
||||||
fd.write(contents)
|
dis.fullscreen("Saving...")
|
||||||
|
with CardSlot(**ch) as card:
|
||||||
|
fname, nice = card.pick_filename(fname_pattern)
|
||||||
|
|
||||||
if sig:
|
# do actual write
|
||||||
h = ngu.hash.sha256s(contents.encode())
|
with open(fname, 'wt' if is_json else 'wb') as fd:
|
||||||
sig_nice = write_sig_file([(h, fname)], derive, addr_fmt)
|
fd.write(contents)
|
||||||
|
|
||||||
msg = '%s file written:\n\n%s' % (title, nice)
|
|
||||||
if sig:
|
if sig:
|
||||||
msg += "\n\n%s signature file written:\n\n%s" % (title, sig_nice)
|
h = ngu.hash.sha256s(contents.encode())
|
||||||
|
sig_nice = write_sig_file([(h, fname)], derive, addr_fmt)
|
||||||
|
|
||||||
await ux_show_story(msg)
|
msg = '%s file written:\n\n%s' % (title, nice)
|
||||||
|
if sig:
|
||||||
|
msg += "\n\n%s signature file written:\n\n%s" % (title, sig_nice)
|
||||||
|
|
||||||
except CardMissingError:
|
await ux_show_story(msg)
|
||||||
await needs_microsd()
|
|
||||||
except Exception as e:
|
except CardMissingError:
|
||||||
await ux_show_story('Failed to write!\n\n' + str(e))
|
await needs_microsd()
|
||||||
|
except Exception as e:
|
||||||
|
await ux_show_story('Failed to write!\n\n\n' + str(e))
|
||||||
|
|
||||||
# both exceptions & success gets here
|
# both exceptions & success gets here
|
||||||
if no_qr and (NFC is None) and (VD is None) and not force_prompt:
|
if no_qr and (NFC is None) and (VD is None) and not force_prompt:
|
||||||
# user has no other ways enabled, we already exported to SD - done
|
# user has no other ways enabled, we already exported to SD - done
|
||||||
return
|
return
|
||||||
|
|
||||||
if direct_way:
|
|
||||||
return
|
|
||||||
|
|
||||||
def generate_public_contents():
|
def generate_public_contents():
|
||||||
# Generate public details about wallet.
|
# Generate public details about wallet.
|
||||||
#
|
#
|
||||||
@ -425,14 +417,14 @@ def generate_generic_export(account_num=0):
|
|||||||
def generate_electrum_wallet(addr_type, account_num):
|
def generate_electrum_wallet(addr_type, account_num):
|
||||||
# Generate line-by-line JSON details about wallet.
|
# Generate line-by-line JSON details about wallet.
|
||||||
#
|
#
|
||||||
# Much reverse engineering of Electrum here. It's a complex
|
# Much reverse enginerring of Electrum here. It's a complex
|
||||||
# legacy file format.
|
# legacy file format.
|
||||||
|
|
||||||
chain = chains.current_chain()
|
chain = chains.current_chain()
|
||||||
|
|
||||||
xfp = settings.get('xfp')
|
xfp = settings.get('xfp')
|
||||||
|
|
||||||
# Must get the derivation path, and the SLIP132 version bytes right!
|
# Must get the derivation path, and the SLIP32 version bytes right!
|
||||||
mode = chains.af_to_bip44_purpose(addr_type)
|
mode = chains.af_to_bip44_purpose(addr_type)
|
||||||
|
|
||||||
OWNERSHIP.note_wallet_used(addr_type, account_num)
|
OWNERSHIP.note_wallet_used(addr_type, account_num)
|
||||||
@ -465,14 +457,14 @@ def generate_electrum_wallet(addr_type, account_num):
|
|||||||
|
|
||||||
|
|
||||||
async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int_ext=True,
|
async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int_ext=True,
|
||||||
fname_pattern="descriptor.txt", direct_way=None):
|
fname_pattern="descriptor.txt"):
|
||||||
from descriptor import Descriptor
|
from descriptor import Descriptor
|
||||||
from glob import dis
|
from glob import dis
|
||||||
|
|
||||||
dis.fullscreen('Generating...')
|
dis.fullscreen('Generating...')
|
||||||
chain = chains.current_chain()
|
chain = chains.current_chain()
|
||||||
|
|
||||||
xfp = settings.get('xfp', 0)
|
xfp = settings.get('xfp')
|
||||||
dis.progress_bar_show(0.1)
|
dis.progress_bar_show(0.1)
|
||||||
if mode is None:
|
if mode is None:
|
||||||
mode = chains.af_to_bip44_purpose(addr_type)
|
mode = chains.af_to_bip44_purpose(addr_type)
|
||||||
@ -501,31 +493,8 @@ async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int
|
|||||||
)
|
)
|
||||||
|
|
||||||
dis.progress_bar_show(1)
|
dis.progress_bar_show(1)
|
||||||
|
await export_contents("Descriptor", body, fname_pattern, derive + "/0/0",
|
||||||
intro, footer = (body, "") if version.has_qwerty else ("", body)
|
addr_type, force_prompt=True)
|
||||||
title = "Descriptor"
|
|
||||||
await export_contents(title, body, fname_pattern, derive + "/0/0", addr_type,
|
|
||||||
force_prompt=True, direct_way=direct_way, intro=intro, footer=footer,
|
|
||||||
ux_title=title if version.has_qwerty else None)
|
|
||||||
|
|
||||||
|
|
||||||
async def make_key_expression_export(orig_der, addr_fmt=AF_CLASSIC, fname_pattern="key_expr.txt"):
|
|
||||||
from glob import dis
|
|
||||||
|
|
||||||
dis.fullscreen('Generating...')
|
|
||||||
|
|
||||||
xfp = xfp2str(settings.get('xfp', 0)).lower()
|
|
||||||
|
|
||||||
with stash.SensitiveValues() as sv:
|
|
||||||
ek = chains.current_chain().serialize_public(sv.derive_path(orig_der))
|
|
||||||
|
|
||||||
body = "[%s/%s]%s" % (xfp, orig_der.replace("m/", ""), ek)
|
|
||||||
|
|
||||||
intro, footer = (body, "") if version.has_qwerty else ("", body)
|
|
||||||
title = "Key Expression"
|
|
||||||
await export_contents(title, body, fname_pattern, orig_der + "/0/0", addr_fmt,
|
|
||||||
force_prompt=True, intro=intro, footer=footer,
|
|
||||||
ux_title=title if version.has_qwerty else None)
|
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|
||||||
|
|||||||
165
shared/flow.py
165
shared/flow.py
@ -19,8 +19,7 @@ from countdowns import countdown_chooser
|
|||||||
from paper import make_paper_wallet
|
from paper import make_paper_wallet
|
||||||
from trick_pins import TrickPinMenu
|
from trick_pins import TrickPinMenu
|
||||||
from tapsigner import import_tapsigner_backup_file
|
from tapsigner import import_tapsigner_backup_file
|
||||||
from ccc import toggle_ccc_feature, sssp_spending_policy, sssp_feature_menu
|
from ccc import toggle_ccc_feature
|
||||||
from wif import WIFStoreMenu
|
|
||||||
|
|
||||||
# useful shortcut keys
|
# useful shortcut keys
|
||||||
from charcodes import KEY_QR, KEY_NFC
|
from charcodes import KEY_QR, KEY_NFC
|
||||||
@ -101,33 +100,6 @@ def hsm_available():
|
|||||||
# contains hsm feature + can it be used (needs se2 secret and no tmp active)
|
# contains hsm feature + can it be used (needs se2 secret and no tmp active)
|
||||||
return version.supports_hsm and has_real_secret()
|
return version.supports_hsm and has_real_secret()
|
||||||
|
|
||||||
def qr_and_ms():
|
|
||||||
# has QR scanner, and at least one MS wallet
|
|
||||||
if not version.has_qr: return False
|
|
||||||
return bool(settings.get('multisig', False))
|
|
||||||
|
|
||||||
def has_pushtx_url():
|
|
||||||
# they want to use PushTX feature
|
|
||||||
return bool(settings.get("ptxurl", False))
|
|
||||||
|
|
||||||
# Spending Policy (Hobbled mode) predicates.
|
|
||||||
#
|
|
||||||
def is_hobble_testdrive():
|
|
||||||
from pincodes import pa
|
|
||||||
return (pa.hobbled_mode == 2)
|
|
||||||
|
|
||||||
def sssp_related_keys():
|
|
||||||
return sssp_spending_policy('okeys')
|
|
||||||
|
|
||||||
def sssp_allow_passphrase():
|
|
||||||
return word_based_seed() and sssp_related_keys()
|
|
||||||
|
|
||||||
def sssp_allow_notes():
|
|
||||||
return settings.get("secnap", False) and sssp_spending_policy('notes')
|
|
||||||
|
|
||||||
def sssp_allow_vault():
|
|
||||||
return settings.master_get('seedvault') and sssp_related_keys()
|
|
||||||
|
|
||||||
async def goto_home(*a):
|
async def goto_home(*a):
|
||||||
goto_top_menu()
|
goto_top_menu()
|
||||||
|
|
||||||
@ -165,21 +137,6 @@ LoginPrefsMenu = [
|
|||||||
MenuItem('Test Login Now', f=login_now, arg=1),
|
MenuItem('Test Login Now', f=login_now, arg=1),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# obscure settings, not more dangerous, just more personal
|
|
||||||
BuriedSettingsMenu = [
|
|
||||||
ToggleMenuItem('Home Menu XFP', 'hmx', ['Only Tmp', 'Always Show'],
|
|
||||||
story=('Forces display of XFP (seed fingerprint) '
|
|
||||||
'at top of main menu. Normally, XFP is shown only when '
|
|
||||||
'temporary seed is active.\n\n'
|
|
||||||
'Master seed is displayed as <XFP>, temporary seeds as [XFP].'),
|
|
||||||
predicate=has_real_secret,
|
|
||||||
on_change=goto_home),
|
|
||||||
ToggleMenuItem('Menu Wrapping', 'wa', ['Default', 'Always Wrap'],
|
|
||||||
story='''When enabled, allows scrolling past menu top/bottom \
|
|
||||||
(wrap around). By default, this only happens in menus whose length is greater than 10.'''),
|
|
||||||
]
|
|
||||||
|
|
||||||
SettingsMenu = [
|
SettingsMenu = [
|
||||||
# xxxxxxxxxxxxxxxx
|
# xxxxxxxxxxxxxxxx
|
||||||
MenuItem('Login Settings', menu=LoginPrefsMenu),
|
MenuItem('Login Settings', menu=LoginPrefsMenu),
|
||||||
@ -202,13 +159,22 @@ The signed transaction will be named <TXID>.txn, so the file name does not leak
|
|||||||
MS-DOS tools should not be able to find the PSBT data (ie. undelete), but forensic tools \
|
MS-DOS tools should not be able to find the PSBT data (ie. undelete), but forensic tools \
|
||||||
which take apart the flash chips of the SDCard may still be able to find the \
|
which take apart the flash chips of the SDCard may still be able to find the \
|
||||||
data or filenames.'''),
|
data or filenames.'''),
|
||||||
|
ToggleMenuItem('Menu Wrapping', 'wa', ['Default Off', 'Enable'],
|
||||||
|
story='''When enabled, allows scrolling past menu top/bottom \
|
||||||
|
(wrap around). By default, this only happens in very large menus.'''),
|
||||||
|
ToggleMenuItem('Home Menu XFP', 'hmx', ['Only Tmp', 'Always Show'],
|
||||||
|
story=('Forces display of XFP (seed fingerprint) '
|
||||||
|
'at top of main menu. Normally, XFP is shown only when '
|
||||||
|
'temporary seed is active.\n\n'
|
||||||
|
'Master seed is displayed as <XFP>, temporary seeds as [XFP].'),
|
||||||
|
predicate=has_real_secret,
|
||||||
|
on_change=goto_home),
|
||||||
ToggleMenuItem('Keyboard EMU', 'emu', ['Default Off', 'Enable'],
|
ToggleMenuItem('Keyboard EMU', 'emu', ['Default Off', 'Enable'],
|
||||||
on_change=usb_keyboard_emulation,
|
on_change=usb_keyboard_emulation,
|
||||||
predicate=has_secrets, # cannot generate BIP85 passwords without secret
|
predicate=has_secrets, # cannot generate BIP85 passwords without secret
|
||||||
story='''This mode adds a top-level menu item for typing \
|
story='''This mode adds a top-level menu item for typing \
|
||||||
deterministically-generated passwords (BIP-85), directly into an \
|
deterministically-generated passwords (BIP-85), directly into an \
|
||||||
attached USB computer (as an emulated keyboard).'''),
|
attached USB computer (as an emulated keyboard).'''),
|
||||||
MenuItem('Buried Settings', menu=BuriedSettingsMenu),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
XpubExportMenu = [
|
XpubExportMenu = [
|
||||||
@ -226,24 +192,20 @@ WalletExportMenu = [
|
|||||||
MenuItem("Cove", f=named_generic_skeleton, arg="Cove"),
|
MenuItem("Cove", f=named_generic_skeleton, arg="Cove"),
|
||||||
MenuItem("Bitcoin Core", f=bitcoin_core_skeleton),
|
MenuItem("Bitcoin Core", f=bitcoin_core_skeleton),
|
||||||
MenuItem("Nunchuk", f=named_generic_skeleton, arg="Nunchuk"),
|
MenuItem("Nunchuk", f=named_generic_skeleton, arg="Nunchuk"),
|
||||||
MenuItem("Bull Bitcoin", f=ss_descriptor_skeleton,
|
MenuItem("Zeus", f=ss_descriptor_skeleton,
|
||||||
arg=(True, [AF_P2WPKH], "", "bull-bitcoin.txt", KEY_QR)),
|
arg=(True, [AF_P2WPKH, AF_P2WPKH_P2SH], "Zeus Wallet", "zeus-export.txt")),
|
||||||
MenuItem("Blue Wallet", f=electrum_skeleton, arg="Blue"),
|
MenuItem("Electrum Wallet", f=electrum_skeleton),
|
||||||
MenuItem("Electrum Wallet", f=electrum_skeleton, arg="Electrum"),
|
|
||||||
MenuItem("Wasabi Wallet", f=wasabi_skeleton),
|
MenuItem("Wasabi Wallet", f=wasabi_skeleton),
|
||||||
MenuItem("Fully Noded", f=named_generic_skeleton, arg="Fully Noded"),
|
MenuItem("Fully Noded", f=named_generic_skeleton, arg="Fully Noded"),
|
||||||
MenuItem("Unchained", f=unchained_capital_export),
|
MenuItem("Unchained", f=unchained_capital_export),
|
||||||
MenuItem("Theya", f=named_generic_skeleton, arg="Theya"),
|
MenuItem("Theya", f=named_generic_skeleton, arg="Theya"),
|
||||||
MenuItem("Bitcoin Safe", f=named_generic_skeleton, arg="Bitcoin Safe"),
|
MenuItem("Bitcoin Safe", f=named_generic_skeleton, arg="Bitcoin Safe"),
|
||||||
MenuItem("Zeus", f=ss_descriptor_skeleton,
|
|
||||||
arg=(True, [AF_P2WPKH, AF_P2WPKH_P2SH], "Zeus Wallet", "zeus-export.txt", None)),
|
|
||||||
MenuItem("Samourai Postmix", f=samourai_post_mix_descriptor_export),
|
MenuItem("Samourai Postmix", f=samourai_post_mix_descriptor_export),
|
||||||
MenuItem("Samourai Premix", f=samourai_pre_mix_descriptor_export),
|
MenuItem("Samourai Premix", f=samourai_pre_mix_descriptor_export),
|
||||||
# MenuItem("Samourai BadBank", f=samourai_bad_bank_descriptor_export), # not released yet
|
# MenuItem("Samourai BadBank", f=samourai_bad_bank_descriptor_export), # not released yet
|
||||||
MenuItem("Descriptor", f=ss_descriptor_skeleton),
|
MenuItem("Descriptor", f=ss_descriptor_skeleton),
|
||||||
MenuItem("Generic JSON", f=generic_skeleton),
|
MenuItem("Generic JSON", f=generic_skeleton),
|
||||||
MenuItem("Export XPUB", menu=XpubExportMenu),
|
MenuItem("Export XPUB", menu=XpubExportMenu),
|
||||||
MenuItem("Key Expression", f=key_expression_skeleton),
|
|
||||||
MenuItem("Dump Summary", predicate=has_secrets, f=dump_summary),
|
MenuItem("Dump Summary", predicate=has_secrets, f=dump_summary),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -307,8 +269,6 @@ DebugFunctionsMenu = [
|
|||||||
# xxxxxxxxxxxxxxxx
|
# xxxxxxxxxxxxxxxx
|
||||||
MenuItem("Keyboard Test", f=keyboard_test),
|
MenuItem("Keyboard Test", f=keyboard_test),
|
||||||
MenuItem('BBQr Demo', f=debug_bbqr_test, predicate=version.has_qwerty),
|
MenuItem('BBQr Demo', f=debug_bbqr_test, predicate=version.has_qwerty),
|
||||||
MenuItem("NFC Test", f=quick_nfc_test),
|
|
||||||
MenuItem('Clear Tested', f=clear_tested_flag),
|
|
||||||
MenuItem('Debug: assert', f=debug_assert),
|
MenuItem('Debug: assert', f=debug_assert),
|
||||||
MenuItem('Debug: except', f=debug_except),
|
MenuItem('Debug: except', f=debug_except),
|
||||||
MenuItem('Check: BL FW', f=check_firewall_read),
|
MenuItem('Check: BL FW', f=check_firewall_read),
|
||||||
@ -375,7 +335,6 @@ correctly- crafted transactions signed on Testnet could be broadcast on Mainnet.
|
|||||||
MenuItem('MCU Key Slots', f=show_mcu_keys_left),
|
MenuItem('MCU Key Slots', f=show_mcu_keys_left),
|
||||||
MenuItem('Bless Firmware', f=bless_flash), # no need for this anymore?
|
MenuItem('Bless Firmware', f=bless_flash), # no need for this anymore?
|
||||||
MenuItem("Wipe LFS", f=wipe_filesystem), # kills other-seed settings, HSM stuff, addr cache
|
MenuItem("Wipe LFS", f=wipe_filesystem), # kills other-seed settings, HSM stuff, addr cache
|
||||||
MenuItem("Nuke Device", f=nuke_device),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
BackupStuffMenu = [
|
BackupStuffMenu = [
|
||||||
@ -394,20 +353,7 @@ NFCToolsMenu = [
|
|||||||
MenuItem('Verify Address', f=nfc_address_verify),
|
MenuItem('Verify Address', f=nfc_address_verify),
|
||||||
MenuItem('File Share', f=nfc_share_file),
|
MenuItem('File Share', f=nfc_share_file),
|
||||||
MenuItem('Import Multisig', f=import_multisig_nfc),
|
MenuItem('Import Multisig', f=import_multisig_nfc),
|
||||||
MenuItem('Push Transaction', f=nfc_pushtx_file, predicate=has_pushtx_url),
|
MenuItem('Push Transaction', f=nfc_pushtx_file, predicate=lambda: settings.get("ptxurl", False)),
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
SpendingPolicySubMenu = [
|
|
||||||
NonDefaultMenuItem('Single-Signer', 'sssp', f=sssp_feature_menu, predicate=has_real_secret),
|
|
||||||
NonDefaultMenuItem('Co-Sign Multi.' if not version.has_qwerty else 'Co-Sign Multisig (CCC)',
|
|
||||||
'ccc', f=toggle_ccc_feature, predicate=is_not_tmp),
|
|
||||||
ToggleMenuItem('HSM Mode', 'hsmcmd', ['Default Off', 'Enable'],
|
|
||||||
story=("Enable HSM? Enables all user management commands, and other HSM-only USB commands. "
|
|
||||||
"By default these commands are disabled."),
|
|
||||||
predicate=hsm_available),
|
|
||||||
MenuItem('User Management', menu=make_users_menu,
|
|
||||||
predicate=lambda: hsm_available() and settings.get('hsmcmd', False)),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
AdvancedNormalMenu = [
|
AdvancedNormalMenu = [
|
||||||
@ -423,9 +369,14 @@ AdvancedNormalMenu = [
|
|||||||
MenuItem("View Identity", f=view_ident),
|
MenuItem("View Identity", f=view_ident),
|
||||||
MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu),
|
MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu),
|
||||||
MenuItem("Key Teleport (start)", f=kt_start_rx, predicate=version.has_qr),
|
MenuItem("Key Teleport (start)", f=kt_start_rx, predicate=version.has_qr),
|
||||||
MenuItem("Spending Policy", menu=SpendingPolicySubMenu,shortcut='s',predicate=has_real_secret),
|
|
||||||
MenuItem('Paper Wallets', f=make_paper_wallet),
|
MenuItem('Paper Wallets', f=make_paper_wallet),
|
||||||
MenuItem('WIF Store', menu=WIFStoreMenu.make),
|
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),
|
||||||
|
NonDefaultMenuItem('Coldcard Co-Signing', 'ccc', f=toggle_ccc_feature, predicate=is_not_tmp),
|
||||||
|
MenuItem('User Management', menu=make_users_menu,
|
||||||
|
predicate=hsm_available),
|
||||||
MenuItem('NFC Tools', predicate=nfc_enabled, menu=NFCToolsMenu, shortcut=KEY_NFC),
|
MenuItem('NFC Tools', predicate=nfc_enabled, menu=NFCToolsMenu, shortcut=KEY_NFC),
|
||||||
MenuItem("Danger Zone", menu=DangerZoneMenu, shortcut='z'),
|
MenuItem("Danger Zone", menu=DangerZoneMenu, shortcut='z'),
|
||||||
]
|
]
|
||||||
@ -469,7 +420,6 @@ EmptyWallet = [
|
|||||||
MenuItem('New Seed Words', menu=NewSeedMenu),
|
MenuItem('New Seed Words', menu=NewSeedMenu),
|
||||||
MenuItem('Import Existing', menu=ImportWallet),
|
MenuItem('Import Existing', menu=ImportWallet),
|
||||||
MenuItem("Migrate Coldcard", menu=clone_start),
|
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('Help', f=virgin_help, predicate=not version.has_qwerty),
|
||||||
MenuItem('Advanced/Tools', menu=AdvancedPinnedVirginMenu, shortcut='t'),
|
MenuItem('Advanced/Tools', menu=AdvancedPinnedVirginMenu, shortcut='t'),
|
||||||
MenuItem('Settings', menu=SettingsMenu),
|
MenuItem('Settings', menu=SettingsMenu),
|
||||||
@ -488,7 +438,7 @@ NormalSystem = [
|
|||||||
MenuItem("Address Explorer", menu=address_explore, shortcut='x'),
|
MenuItem("Address Explorer", menu=address_explore, shortcut='x'),
|
||||||
MenuItem('Secure Notes & Passwords', menu=make_notes_menu, shortcut='n',
|
MenuItem('Secure Notes & Passwords', menu=make_notes_menu, shortcut='n',
|
||||||
predicate=lambda: version.has_qwerty and settings.get("secnap", False)),
|
predicate=lambda: version.has_qwerty and settings.get("secnap", False)),
|
||||||
MenuItem('Type Passwords', f=password_entry, shortcut='e',
|
MenuItem('Type Passwords', f=password_entry, shortcut='t',
|
||||||
predicate=lambda: settings.get("emu", False) and has_secrets()),
|
predicate=lambda: settings.get("emu", False) and has_secrets()),
|
||||||
MenuItem('Seed Vault', menu=make_seed_vault_menu, shortcut='v',
|
MenuItem('Seed Vault', menu=make_seed_vault_menu, shortcut='v',
|
||||||
predicate=lambda: settings.master_get('seedvault') and has_secrets()),
|
predicate=lambda: settings.master_get('seedvault') and has_secrets()),
|
||||||
@ -507,72 +457,3 @@ FactoryMenu = [
|
|||||||
MenuItem("Debug Functions", menu=DebugFunctionsMenu, shortcut='f'),
|
MenuItem("Debug Functions", menu=DebugFunctionsMenu, shortcut='f'),
|
||||||
MenuItem("Perform Selftest", f=start_selftest, shortcut='s'),
|
MenuItem("Perform Selftest", f=start_selftest, shortcut='s'),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Special menus for hobbled mode where we have a (single signer) spending policy in effect.
|
|
||||||
# - no access to secrets, backups, firmware up/downgrades.
|
|
||||||
# - secure notes, but readonly; can be disabled completely.
|
|
||||||
# - key teleport, but only for PSBT & multisig purposes.
|
|
||||||
# - can only be enabled after we have secrets, so no need for has_secrets tests here
|
|
||||||
#
|
|
||||||
|
|
||||||
# Slightly limited file menu when hobbled.
|
|
||||||
# - no backup/restore
|
|
||||||
HobbledFileMgmtMenu = [
|
|
||||||
# xxxxxxxxxxxxxxxx
|
|
||||||
MenuItem('Sign Text File', f=sign_message_on_sd),
|
|
||||||
MenuItem('Batch Sign PSBT', f=batch_sign),
|
|
||||||
MenuItem('List Files', f=list_files),
|
|
||||||
MenuItem('Export Wallet', menu=WalletExportMenu), # dup under Adv/Tools
|
|
||||||
MenuItem('Verify Sig File', f=verify_sig_file),
|
|
||||||
MenuItem('NFC File Share', predicate=nfc_enabled, f=nfc_share_file, shortcut=KEY_NFC),
|
|
||||||
MenuItem('BBQr File Share', predicate=version.has_qr, f=qr_share_file, arg=True),
|
|
||||||
MenuItem('QR File Share', predicate=version.has_qr, f=qr_share_file, shortcut=KEY_QR),
|
|
||||||
MenuItem('Format SD Card', f=wipe_sd_card),
|
|
||||||
MenuItem('Format RAM Disk', predicate=vdisk_enabled, f=wipe_vdisk),
|
|
||||||
]
|
|
||||||
|
|
||||||
# NFC tools when hobbled: not much different.
|
|
||||||
HobbledNFCToolsMenu = [
|
|
||||||
MenuItem('Sign PSBT', f=nfc_sign_psbt),
|
|
||||||
MenuItem('Show Address', f=nfc_show_address),
|
|
||||||
MenuItem('Sign Message', f=nfc_sign_msg),
|
|
||||||
MenuItem('Verify Sig File', f=nfc_sign_verify),
|
|
||||||
MenuItem('Verify Address', f=nfc_address_verify),
|
|
||||||
MenuItem('File Share', f=nfc_share_file),
|
|
||||||
MenuItem('Push Transaction', f=nfc_pushtx_file, predicate=has_pushtx_url),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Very limited advanced menu when hobbled.
|
|
||||||
HobbledAdvancedMenu = [
|
|
||||||
# xxxxxxxxxxxxxxxx
|
|
||||||
MenuItem("File Management", menu=HobbledFileMgmtMenu),
|
|
||||||
MenuItem('Export Wallet', menu=WalletExportMenu, shortcut='x'), # also inside FileMgmt
|
|
||||||
MenuItem('Teleport Multisig PSBT', predicate=qr_and_ms, f=kt_send_file_psbt),
|
|
||||||
MenuItem("View Identity", f=view_ident),
|
|
||||||
MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu, predicate=sssp_related_keys),
|
|
||||||
MenuItem('Paper Wallets', f=make_paper_wallet),
|
|
||||||
MenuItem('NFC Tools', predicate=nfc_enabled, menu=HobbledNFCToolsMenu, shortcut=KEY_NFC),
|
|
||||||
MenuItem('WIF Store', menu=WIFStoreMenu.make, predicate=sssp_related_keys),
|
|
||||||
MenuItem('Show %s Version' % ("Firmware" if version.has_qwerty else "FW"), f=show_version),
|
|
||||||
MenuItem("Destroy Seed", f=clear_seed, predicate=has_real_secret),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Main menu when a spending policy (hobbled) is in effect.
|
|
||||||
HobbledTopMenu = [
|
|
||||||
# xxxxxxxxxxxxxxxx
|
|
||||||
MenuItem('Ready To Sign', f=ready2sign, shortcut='r'),
|
|
||||||
MenuItem('Passphrase', menu=start_b39_pw, predicate=sssp_allow_passphrase, shortcut='p'),
|
|
||||||
MenuItem('Scan Any QR Code', predicate=version.has_qr, f=scan_any_qr, arg=(False, True),
|
|
||||||
shortcut=KEY_QR),
|
|
||||||
MenuItem("Address Explorer", menu=address_explore, shortcut='x'),
|
|
||||||
MenuItem('Secure Notes & Passwords', menu=make_notes_menu, predicate=sssp_allow_notes,
|
|
||||||
shortcut='n'),
|
|
||||||
MenuItem('Type Passwords', f=password_entry, shortcut='t',
|
|
||||||
predicate=lambda: settings.get("emu", False) and sssp_related_keys()),
|
|
||||||
MenuItem('Seed Vault', menu=make_seed_vault_menu, predicate=sssp_allow_vault,
|
|
||||||
shortcut='v'),
|
|
||||||
MenuItem('Advanced/Tools', menu=HobbledAdvancedMenu, shortcut='t'),
|
|
||||||
MenuItem('Secure Logout', f=logout_now, predicate=not version.has_battery),
|
|
||||||
MenuItem('EXIT TEST DRIVE', f=sssp_feature_menu, predicate=is_hobble_testdrive),
|
|
||||||
ShortcutItem(KEY_NFC, predicate=nfc_enabled, menu=HobbledNFCToolsMenu),
|
|
||||||
]
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
#
|
#
|
||||||
import utime, struct
|
import utime, struct
|
||||||
import uasyncio as asyncio
|
import uasyncio as asyncio
|
||||||
|
from utils import B2A
|
||||||
from machine import Pin
|
from machine import Pin
|
||||||
from ustruct import pack
|
from ustruct import pack
|
||||||
|
|
||||||
|
|||||||
@ -4,8 +4,9 @@
|
|||||||
#
|
#
|
||||||
# Unattended signing of transactions and messages, subject to a set of rules.
|
# Unattended signing of transactions and messages, subject to a set of rules.
|
||||||
#
|
#
|
||||||
import ustruct, chains, sys, gc, uio, ujson, uos, utime, ckcc, ngu
|
import stash, ustruct, chains, sys, gc, uio, ujson, uos, utime, ckcc, ngu, version
|
||||||
from utils import problem_file_line, cleanup_deriv_path, match_deriv_path, keypath_to_str
|
from sffile import SFFile
|
||||||
|
from utils import problem_file_line, cleanup_deriv_path, match_deriv_path
|
||||||
from utils import cleanup_payment_address
|
from utils import cleanup_payment_address
|
||||||
from pincodes import AE_LONG_SECRET_LEN
|
from pincodes import AE_LONG_SECRET_LEN
|
||||||
from stash import blank_object
|
from stash import blank_object
|
||||||
@ -605,7 +606,7 @@ class HSMPolicy:
|
|||||||
fd.write('- XPUB values will be shared, if path matches: m OR %s.\n'
|
fd.write('- XPUB values will be shared, if path matches: m OR %s.\n'
|
||||||
% plist(self.share_xpubs))
|
% plist(self.share_xpubs))
|
||||||
if self.share_addrs:
|
if self.share_addrs:
|
||||||
fd.write('- Address values will be shared, if path matches: %s.\n'
|
fd.write('- Address values values will be shared, if path matches: %s.\n'
|
||||||
% plist(self.share_addrs))
|
% plist(self.share_addrs))
|
||||||
if self.priv_over_ux:
|
if self.priv_over_ux:
|
||||||
fd.write('- Status responses optimized for privacy.\n')
|
fd.write('- Status responses optimized for privacy.\n')
|
||||||
@ -656,15 +657,6 @@ class HSMPolicy:
|
|||||||
assert not glob.hsm_active
|
assert not glob.hsm_active
|
||||||
glob.hsm_active = self
|
glob.hsm_active = self
|
||||||
|
|
||||||
# HSM is the locked-down operating mode: shut down peripherals
|
|
||||||
# that enlarge the USB-stack interaction surface.
|
|
||||||
# - VDisk: MSC bulk OUT and HID OUT share the STM32 OTG_FS RX FIFO;
|
|
||||||
# under load this can wedge the HID OUT endpoint permanently
|
|
||||||
if glob.VD is not None:
|
|
||||||
glob.VD.shutdown()
|
|
||||||
if glob.NFC is not None:
|
|
||||||
glob.NFC.shutdown()
|
|
||||||
|
|
||||||
self.start_time = utime.ticks_ms()
|
self.start_time = utime.ticks_ms()
|
||||||
|
|
||||||
if new_file:
|
if new_file:
|
||||||
@ -882,6 +874,9 @@ class HSMPolicy:
|
|||||||
# do this super early so always cleared even if other issues
|
# do this super early so always cleared even if other issues
|
||||||
local_ok = self.consume_local_code(psbt_sha)
|
local_ok = self.consume_local_code(psbt_sha)
|
||||||
|
|
||||||
|
if not self.rules:
|
||||||
|
raise ValueError("no txn signing allowed")
|
||||||
|
|
||||||
# reject anything with warning, probably
|
# reject anything with warning, probably
|
||||||
if psbt.warnings:
|
if psbt.warnings:
|
||||||
if self.warnings_ok:
|
if self.warnings_ok:
|
||||||
@ -889,32 +884,6 @@ class HSMPolicy:
|
|||||||
else:
|
else:
|
||||||
raise ValueError("has %d warning(s)" % len(psbt.warnings))
|
raise ValueError("has %d warning(s)" % len(psbt.warnings))
|
||||||
|
|
||||||
if psbt.por322:
|
|
||||||
if not self.msg_paths:
|
|
||||||
raise ValueError("Message signing not permitted")
|
|
||||||
|
|
||||||
for inp in psbt.inputs:
|
|
||||||
if not inp.required_key:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if inp.is_multisig:
|
|
||||||
paths = [
|
|
||||||
keypath_to_str(inp.subpaths[pk])
|
|
||||||
for pk in inp.required_key
|
|
||||||
if pk in inp.subpaths
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
paths = [keypath_to_str(inp.subpaths[inp.required_key])]
|
|
||||||
|
|
||||||
if not any(match_deriv_path(self.msg_paths, p) for p in paths):
|
|
||||||
raise ValueError("Message signing not enabled for that path")
|
|
||||||
|
|
||||||
self.approve(log, "BIP-322 message signing allowed")
|
|
||||||
return 'y'
|
|
||||||
|
|
||||||
if not self.rules:
|
|
||||||
raise ValueError("no txn signing allowed")
|
|
||||||
|
|
||||||
# See who has entered creditials already (all must be valid).
|
# See who has entered creditials already (all must be valid).
|
||||||
users = []
|
users = []
|
||||||
for u, (token, counter) in auth.items():
|
for u, (token, counter) in auth.items():
|
||||||
|
|||||||
@ -59,7 +59,7 @@ class ApproveHSMPolicy(UserAuthorizedAction):
|
|||||||
msg = '''Last chance. You are defining a new policy which \
|
msg = '''Last chance. You are defining a new policy which \
|
||||||
allows the Coldcard to sign specific transactions without any further user approval.\n\n\
|
allows the Coldcard to sign specific transactions without any further user approval.\n\n\
|
||||||
Policy hash:\n%s\n\n
|
Policy hash:\n%s\n\n
|
||||||
Press (%s) to save policy and enable HSM mode.''' % (self.policy.hash(), confirm_char)
|
Press %s to save policy and enable HSM mode.''' % (self.policy.hash(), confirm_char)
|
||||||
|
|
||||||
ch = await ux_show_story(msg, title=self.title,
|
ch = await ux_show_story(msg, title=self.title,
|
||||||
escape='x'+confirm_char, strict_escape=True)
|
escape='x'+confirm_char, strict_escape=True)
|
||||||
|
|||||||
@ -58,8 +58,8 @@ class ImportantTask:
|
|||||||
else:
|
else:
|
||||||
# uncaught exception in an unnamed (and unimportant) task
|
# uncaught exception in an unnamed (and unimportant) task
|
||||||
print("UNNAMED: " + context["message"])
|
print("UNNAMED: " + context["message"])
|
||||||
sys.print_exception(context["exception"]) # VERY USEFUL on sim
|
# sys.print_exception(context["exception"])
|
||||||
#print("... future: %r" % context.get("future", '?'))
|
print("... future: %r" % context.get("future", '?'))
|
||||||
|
|
||||||
def start_task(self, name, awaitable):
|
def start_task(self, name, awaitable):
|
||||||
# start a critical task and watch for it to never die
|
# start a critical task and watch for it to never die
|
||||||
|
|||||||
@ -134,7 +134,7 @@ class FullKeyboard(NumpadBase):
|
|||||||
if self._history[kn] == NUM_SAMPLES:
|
if self._history[kn] == NUM_SAMPLES:
|
||||||
self.is_pressed[kn] = 1
|
self.is_pressed[kn] = 1
|
||||||
new_presses.add(kn)
|
new_presses.add(kn)
|
||||||
elif self._history[kn] == 0:
|
elif self._history[i] == 0:
|
||||||
self.is_pressed[kn] = 0
|
self.is_pressed[kn] = 0
|
||||||
self._history[kn] = 0
|
self._history[kn] = 0
|
||||||
|
|
||||||
|
|||||||
@ -656,91 +656,8 @@ class Display:
|
|||||||
|
|
||||||
return prev_x
|
return prev_x
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def handle_qr_msg(msg, max_lines=False):
|
|
||||||
if len(msg) <= CHARS_W:
|
|
||||||
parts = [msg]
|
|
||||||
elif ' ' not in msg and (len(msg) <= (CHARS_W * 2)):
|
|
||||||
# fits in two lines, but has no spaces
|
|
||||||
hh = len(msg) // 2
|
|
||||||
parts = [msg[0:hh], msg[hh:]]
|
|
||||||
else:
|
|
||||||
if not max_lines:
|
|
||||||
# do word wrap
|
|
||||||
parts = list(word_wrap(msg, CHARS_W))
|
|
||||||
else:
|
|
||||||
# 2 lines max
|
|
||||||
parts = [msg[:30] + "⋯", "⋯" + msg[-30:]]
|
|
||||||
|
|
||||||
return parts
|
|
||||||
|
|
||||||
def draw_qr_lines(self, lines, is_addr):
|
|
||||||
y = CHARS_H - len(lines)
|
|
||||||
prev_x = 0
|
|
||||||
for line in lines:
|
|
||||||
if not is_addr:
|
|
||||||
self.text(None, y, line)
|
|
||||||
else:
|
|
||||||
prev_x = self._draw_addr(y, line, prev_x=prev_x)
|
|
||||||
y += 1
|
|
||||||
|
|
||||||
def draw_qr_idx_hint(self, str_idx):
|
|
||||||
lh = len(str_idx)
|
|
||||||
assert lh <= 10
|
|
||||||
if lh > 5:
|
|
||||||
# needs 2 lines
|
|
||||||
self.text(-1, 0, str_idx[:5])
|
|
||||||
self.text(-1, 1, str_idx[5:])
|
|
||||||
else:
|
|
||||||
self.text(-1, 0, str_idx)
|
|
||||||
|
|
||||||
def draw_side_msg(self, msg, has_idx):
|
|
||||||
right_sub = 2 if has_idx else 0
|
|
||||||
start_right = right_msg = None
|
|
||||||
if len(msg) <= CHARS_H:
|
|
||||||
# we only need left side
|
|
||||||
start_left = CHARS_H - len(msg)
|
|
||||||
left_msg = msg
|
|
||||||
else:
|
|
||||||
split_msg = msg.split()
|
|
||||||
if len(split_msg) == 1 or len(split_msg) > 2:
|
|
||||||
return # not possible
|
|
||||||
|
|
||||||
left_msg, right_msg = split_msg
|
|
||||||
if len(left_msg) > CHARS_H:
|
|
||||||
return
|
|
||||||
if len(right_msg) > (CHARS_H - right_sub):
|
|
||||||
return
|
|
||||||
|
|
||||||
start_left = CHARS_H - len(left_msg)
|
|
||||||
start_right = CHARS_H - len(right_msg)
|
|
||||||
|
|
||||||
for i, c in enumerate(left_msg, start=start_left):
|
|
||||||
self.text(1, i, c)
|
|
||||||
|
|
||||||
if start_right:
|
|
||||||
for i, c in enumerate(right_msg, start=start_right):
|
|
||||||
self.text(-1, i, c)
|
|
||||||
|
|
||||||
def draw_qr_error(self, idx_hint, msg=None):
|
|
||||||
x = 85
|
|
||||||
y = 30
|
|
||||||
w = 150
|
|
||||||
self.clear()
|
|
||||||
self.dis.fill_rect(x, y, w, w, COL_TEXT)
|
|
||||||
self.dis.fill_rect(x + 1, y + 1, w - 2, w - 2) # Black
|
|
||||||
self.text(12, 3, "QR too big")
|
|
||||||
if msg:
|
|
||||||
lines = self.handle_qr_msg(msg, max_lines=True)
|
|
||||||
self.draw_qr_lines(lines, False)
|
|
||||||
|
|
||||||
if idx_hint:
|
|
||||||
self.draw_qr_idx_hint(idx_hint)
|
|
||||||
|
|
||||||
self.show()
|
|
||||||
|
|
||||||
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert, partial_bar=None,
|
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert, partial_bar=None,
|
||||||
is_addr=False, force_msg=False, side_msg=None):
|
is_addr=False, force_msg=False, is_change=False):
|
||||||
# Show a QR code on screen w/ some text under it
|
# Show a QR code on screen w/ some text under it
|
||||||
# - invert not supported on Q1
|
# - invert not supported on Q1
|
||||||
# - sidebar not supported here (see users.py)
|
# - sidebar not supported here (see users.py)
|
||||||
@ -760,7 +677,16 @@ class Display:
|
|||||||
# p2wsh address would need 3 lines to show, so we won't
|
# p2wsh address would need 3 lines to show, so we won't
|
||||||
num_lines = 0
|
num_lines = 0
|
||||||
elif msg:
|
elif msg:
|
||||||
parts = self.handle_qr_msg(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
|
||||||
|
hh = len(msg) // 2
|
||||||
|
parts = [msg[0:hh], msg[hh:]]
|
||||||
|
else:
|
||||||
|
# do word wrap
|
||||||
|
parts = list(word_wrap(msg, CHARS_W))
|
||||||
|
|
||||||
num_lines = len(parts)
|
num_lines = len(parts)
|
||||||
else:
|
else:
|
||||||
num_lines = 0
|
num_lines = 0
|
||||||
@ -829,13 +755,31 @@ class Display:
|
|||||||
|
|
||||||
if num_lines:
|
if num_lines:
|
||||||
# centered text under that
|
# centered text under that
|
||||||
self.draw_qr_lines(parts, is_addr)
|
y = CHARS_H - num_lines
|
||||||
|
prev_x = 0
|
||||||
|
for line in parts:
|
||||||
|
if not is_addr:
|
||||||
|
self.text(None, y, line)
|
||||||
|
else:
|
||||||
|
prev_x = self._draw_addr(y, line, prev_x=prev_x)
|
||||||
|
y += 1
|
||||||
|
|
||||||
if idx_hint:
|
if idx_hint:
|
||||||
self.draw_qr_idx_hint(idx_hint)
|
lh = len(idx_hint)
|
||||||
|
assert lh <= 10
|
||||||
|
if lh > 5:
|
||||||
|
# needs 2 lines
|
||||||
|
self.text(-1, 0, idx_hint[:5])
|
||||||
|
self.text(-1, 1, idx_hint[5:])
|
||||||
|
else:
|
||||||
|
self.text(-1, 0, idx_hint)
|
||||||
|
|
||||||
if side_msg:
|
if is_addr and is_change:
|
||||||
self.draw_side_msg(side_msg, idx_hint)
|
for i, c in enumerate("CHANGE", start=4):
|
||||||
|
self.text(1, i, c)
|
||||||
|
|
||||||
|
for i, c in enumerate("BACK", start=6):
|
||||||
|
self.text(-1, i, c)
|
||||||
|
|
||||||
# pass a max brightness flag here, which will be cleared after next show
|
# pass a max brightness flag here, which will be cleared after next show
|
||||||
self.show(max_bright=True)
|
self.show(max_bright=True)
|
||||||
|
|||||||
@ -181,22 +181,14 @@ class LoginUX:
|
|||||||
async def we_are_ewaste(self, num_fails):
|
async def we_are_ewaste(self, num_fails):
|
||||||
msg = '''After %d failed PIN attempts this Coldcard is locked forever. \
|
msg = '''After %d failed PIN attempts this Coldcard is locked forever. \
|
||||||
By design, there is no way to reset or recover the secure element, and its contents \
|
By design, there is no way to reset or recover the secure element, and its contents \
|
||||||
are now forever inaccessible.\n\n''' % num_fails
|
are now forever inaccessible.
|
||||||
|
|
||||||
if has_qwerty:
|
Restore your seed words onto a new Coldcard.''' % num_fails
|
||||||
msg += 'Calculator mode starts now.'
|
|
||||||
else:
|
|
||||||
msg += 'Restore your seed words onto a new Coldcard.'
|
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
ch = await ux_show_story(msg, title='I Am Brick!', escape='6')
|
ch = await ux_show_story(msg, title='I Am Brick!', escape='6')
|
||||||
if ch == '6': break
|
if ch == '6': break
|
||||||
|
|
||||||
if has_qwerty:
|
|
||||||
from calc import login_repl
|
|
||||||
await login_repl()
|
|
||||||
|
|
||||||
|
|
||||||
async def confirm_attempt(self, attempts_left, value):
|
async def confirm_attempt(self, attempts_left, value):
|
||||||
|
|
||||||
ch = await ux_show_story('''You have %d attempts left before this Coldcard BRICKS \
|
ch = await ux_show_story('''You have %d attempts left before this Coldcard BRICKS \
|
||||||
@ -278,7 +270,7 @@ suffix break point is correct.\n\n'''
|
|||||||
return await self.interact()
|
return await self.interact()
|
||||||
|
|
||||||
|
|
||||||
async def get_new_pin(self, title=None, story=None):
|
async def get_new_pin(self, title, story=None):
|
||||||
# Do UX flow to get new (or change) PIN. Always does the double-entry thing
|
# Do UX flow to get new (or change) PIN. Always does the double-entry thing
|
||||||
self.is_setting = True
|
self.is_setting = True
|
||||||
|
|
||||||
|
|||||||
@ -81,6 +81,7 @@ glob.settings = settings
|
|||||||
|
|
||||||
async def more_setup():
|
async def more_setup():
|
||||||
# Boot up code; splash screen is being shown
|
# Boot up code; splash screen is being shown
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from files import CardSlot
|
from files import CardSlot
|
||||||
CardSlot.setup()
|
CardSlot.setup()
|
||||||
@ -88,10 +89,6 @@ async def more_setup():
|
|||||||
# This "pa" object holds some state shared w/ bootloader about the PIN
|
# This "pa" object holds some state shared w/ bootloader about the PIN
|
||||||
try:
|
try:
|
||||||
from pincodes import pa
|
from pincodes import pa
|
||||||
# check for bricked system early
|
|
||||||
# bricked CC not going past this point
|
|
||||||
await pa.enforce_brick()
|
|
||||||
|
|
||||||
pa.setup(b'') # just to see where we stand.
|
pa.setup(b'') # just to see where we stand.
|
||||||
is_blank = pa.is_blank()
|
is_blank = pa.is_blank()
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
|
|||||||
@ -6,7 +6,6 @@ freeze_as_mpy('', [
|
|||||||
'address_explorer.py',
|
'address_explorer.py',
|
||||||
'auth.py',
|
'auth.py',
|
||||||
'backups.py',
|
'backups.py',
|
||||||
'block_height.py',
|
|
||||||
'callgate.py',
|
'callgate.py',
|
||||||
'ccc.py',
|
'ccc.py',
|
||||||
'chains.py',
|
'chains.py',
|
||||||
@ -14,6 +13,8 @@ freeze_as_mpy('', [
|
|||||||
'compat7z.py',
|
'compat7z.py',
|
||||||
'countdowns.py',
|
'countdowns.py',
|
||||||
'descriptor.py',
|
'descriptor.py',
|
||||||
|
'dev_helper.py',
|
||||||
|
'display.py',
|
||||||
'drv_entro.py',
|
'drv_entro.py',
|
||||||
'exceptions.py',
|
'exceptions.py',
|
||||||
'export.py',
|
'export.py',
|
||||||
@ -47,6 +48,7 @@ freeze_as_mpy('', [
|
|||||||
'selftest.py',
|
'selftest.py',
|
||||||
'serializations.py',
|
'serializations.py',
|
||||||
'sffile.py',
|
'sffile.py',
|
||||||
|
'ssd1306.py',
|
||||||
'stash.py',
|
'stash.py',
|
||||||
'tapsigner.py',
|
'tapsigner.py',
|
||||||
'trick_pins.py',
|
'trick_pins.py',
|
||||||
@ -57,7 +59,6 @@ freeze_as_mpy('', [
|
|||||||
'version.py',
|
'version.py',
|
||||||
'wallet.py',
|
'wallet.py',
|
||||||
'web2fa.py',
|
'web2fa.py',
|
||||||
'wif.py',
|
|
||||||
'xor_seed.py'
|
'xor_seed.py'
|
||||||
], opt=0)
|
], opt=0)
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
# Mk4 only files; would not be needed on Mk3 or earlier.
|
# Mk4 only files; would not be needed on Mk3 or earlier.
|
||||||
freeze_as_mpy('', [
|
freeze_as_mpy('', [
|
||||||
'display.py',
|
|
||||||
'hsm.py',
|
'hsm.py',
|
||||||
'hsm_ux.py',
|
'hsm_ux.py',
|
||||||
'mempad.py',
|
'mempad.py',
|
||||||
|
|||||||
@ -119,7 +119,7 @@ class ShortcutItem(MenuItem):
|
|||||||
super().__init__('SHORTCUT', shortcut=key, **kws)
|
super().__init__('SHORTCUT', shortcut=key, **kws)
|
||||||
|
|
||||||
class NonDefaultMenuItem(MenuItem):
|
class NonDefaultMenuItem(MenuItem):
|
||||||
# Show a checkmark if setting is defined and not the default
|
# Show a checkmark if setting is defined and not the default ... so know know it's set
|
||||||
def __init__(self, label, nvkey, prelogin=False, default_value=None, **kws):
|
def __init__(self, label, nvkey, prelogin=False, default_value=None, **kws):
|
||||||
super().__init__(label, **kws)
|
super().__init__(label, **kws)
|
||||||
self.nvkey = nvkey
|
self.nvkey = nvkey
|
||||||
@ -290,7 +290,7 @@ class MenuSystem:
|
|||||||
dis.clear()
|
dis.clear()
|
||||||
|
|
||||||
cursor_y = None
|
cursor_y = None
|
||||||
for n in range(PER_M+1):
|
for n in range(self.ypos+PER_M+1):
|
||||||
real_idx = n+self.ypos
|
real_idx = n+self.ypos
|
||||||
if real_idx >= self.count: break
|
if real_idx >= self.count: break
|
||||||
|
|
||||||
@ -306,6 +306,10 @@ class MenuSystem:
|
|||||||
if fcn and fcn():
|
if fcn and fcn():
|
||||||
checked = True
|
checked = True
|
||||||
|
|
||||||
|
if not has_qwerty and checked and (len(msg) > 14):
|
||||||
|
# on mk4 every label longer than 14 will overlap with checkmark
|
||||||
|
checked = False
|
||||||
|
|
||||||
if self.multi_selected is not None and (real_idx in self.multi_selected):
|
if self.multi_selected is not None and (real_idx in self.multi_selected):
|
||||||
# ignore length constraint above, we need to visually show that
|
# ignore length constraint above, we need to visually show that
|
||||||
# smthg is selected - in any case
|
# smthg is selected - in any case
|
||||||
@ -331,8 +335,9 @@ class MenuSystem:
|
|||||||
if wrap: return True
|
if wrap: return True
|
||||||
|
|
||||||
# Do wrap-around (by request from NVK) if longer than the screen itself (on Q),
|
# Do wrap-around (by request from NVK) if longer than the screen itself (on Q),
|
||||||
# Mk4: same limit
|
# for mk4, limit is 16 which hits mostly the seed word menus.
|
||||||
return self.count > 10
|
limit = 10 if has_qwerty else 16
|
||||||
|
return self.count > limit
|
||||||
|
|
||||||
def down(self):
|
def down(self):
|
||||||
if self.cursor < self.count-1:
|
if self.cursor < self.count-1:
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||||
#
|
#
|
||||||
# mk4.py - Mk4 and Mk5 specific code, not needed on earlier devices.
|
# mk4.py - Mk4 specific code, not needed on earlier devices.
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
import os, sys, pyb, ckcc, version, glob
|
import os, sys, pyb, ckcc, version, glob
|
||||||
@ -11,8 +11,7 @@ def make_flash_fs():
|
|||||||
os.VfsLfs2.mkfs(fl)
|
os.VfsLfs2.mkfs(fl)
|
||||||
|
|
||||||
os.mount(fl, '/flash')
|
os.mount(fl, '/flash')
|
||||||
os.chdir('/flash')
|
os.mkdir('/flash/settings')
|
||||||
os.mkdir('settings')
|
|
||||||
|
|
||||||
def make_psram_fs():
|
def make_psram_fs():
|
||||||
# Filesystem is wiped and rebuilt on each boot before this point, but
|
# Filesystem is wiped and rebuilt on each boot before this point, but
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
|
|||||||
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
|
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
|
||||||
from ux import (ux_show_story, OK, ux_enter_bip32_index, ux_input_text, the_ux,
|
from ux import (ux_show_story, OK, ux_enter_bip32_index, ux_input_text, the_ux,
|
||||||
import_export_prompt, ux_aborted)
|
import_export_prompt, ux_aborted)
|
||||||
from utils import problem_file_line, to_ascii_printable, show_single_address, node_from_privkey
|
from utils import problem_file_line, to_ascii_printable, show_single_address
|
||||||
from files import CardSlot, CardMissingError, needs_microsd
|
from files import CardSlot, CardMissingError, needs_microsd
|
||||||
|
|
||||||
def rfc_signature_template(msg, addr, sig):
|
def rfc_signature_template(msg, addr, sig):
|
||||||
@ -179,16 +179,14 @@ async def msg_sign_ux_get_subpath(addr_fmt):
|
|||||||
purpose = chains.af_to_bip44_purpose(addr_fmt)
|
purpose = chains.af_to_bip44_purpose(addr_fmt)
|
||||||
chain_n = chains.current_chain().b44_cointype
|
chain_n = chains.current_chain().b44_cointype
|
||||||
|
|
||||||
acct = await ux_enter_bip32_index('Account Number:')
|
acct = await ux_enter_bip32_index('Account Number:') or 0
|
||||||
if acct is None: return
|
|
||||||
|
|
||||||
ch = await ux_show_story(title="Change?",
|
ch = await ux_show_story(title="Change?",
|
||||||
msg="Press (0) to use internal/change address,"
|
msg="Press (0) to use internal/change address,"
|
||||||
" %s to use external/receive address." % OK, escape="0")
|
" %s to use external/receive address." % OK, escape="0")
|
||||||
change = 1 if ch == '0' else 0
|
change = 1 if ch == '0' else 0
|
||||||
|
|
||||||
idx = await ux_enter_bip32_index('Index Number:')
|
idx = await ux_enter_bip32_index('Index Number:') or 0
|
||||||
if idx is None: return
|
|
||||||
|
|
||||||
return "m/%dh/%dh/%dh/%d/%d" % (purpose, chain_n, acct, change, idx)
|
return "m/%dh/%dh/%dh/%d/%d" % (purpose, chain_n, acct, change, idx)
|
||||||
|
|
||||||
@ -262,13 +260,13 @@ def write_sig_file(content_list, derive=None, addr_fmt=AF_CLASSIC, pk=None, sig_
|
|||||||
|
|
||||||
return sig_nice
|
return sig_nice
|
||||||
|
|
||||||
def validate_text_for_signing(text, allow_tab_nl=False):
|
def validate_text_for_signing(text, only_printable=True):
|
||||||
# Check for some UX/UI traps in the message itself.
|
# Check for some UX/UI traps in the message itself.
|
||||||
# - messages must be short and ascii only. Our charset is limited
|
# - messages must be short and ascii only. Our charset is limited
|
||||||
# - too many spaces, leading/trailing can be an issue
|
# - too many spaces, leading/trailing can be an issue
|
||||||
# MSG_MAX_SPACES = 4 # impt. compared to -=- positioning
|
# MSG_MAX_SPACES = 4 # impt. compared to -=- positioning
|
||||||
text = str(text, "ascii") # handle memoryview coming from USB
|
|
||||||
result = to_ascii_printable(text, allow_tab_nl=allow_tab_nl)
|
result = to_ascii_printable(text, only_printable=only_printable)
|
||||||
|
|
||||||
length = len(result)
|
length = len(result)
|
||||||
assert length >= 2, "msg too short (min. 2)"
|
assert length >= 2, "msg too short (min. 2)"
|
||||||
@ -315,7 +313,6 @@ def parse_msg_sign_request(data):
|
|||||||
if text is None:
|
if text is None:
|
||||||
raise AssertionError("MSG required")
|
raise AssertionError("MSG required")
|
||||||
subpath = data_dict.get("subpath", subpath)
|
subpath = data_dict.get("subpath", subpath)
|
||||||
assert isinstance(subpath, str), "subpath"
|
|
||||||
addr_fmt = data_dict.get("addr_fmt", addr_fmt)
|
addr_fmt = data_dict.get("addr_fmt", addr_fmt)
|
||||||
is_json = True
|
is_json = True
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -334,13 +331,11 @@ def parse_msg_sign_request(data):
|
|||||||
addr_fmt = addr_fmt_from_subpath(subpath)
|
addr_fmt = addr_fmt_from_subpath(subpath)
|
||||||
|
|
||||||
if not subpath:
|
if not subpath:
|
||||||
try:
|
subpath = chains.STD_DERIVATIONS[addr_fmt]
|
||||||
subpath = chains.STD_DERIVATIONS[addr_fmt]
|
subpath = subpath.format(
|
||||||
subpath = subpath.format(
|
coin_type=chains.current_chain().b44_cointype,
|
||||||
coin_type=chains.current_chain().b44_cointype,
|
account=0, change=0, idx=0
|
||||||
account=0, change=0, idx=0
|
)
|
||||||
)
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
return text, subpath, addr_fmt, is_json
|
return text, subpath, addr_fmt, is_json
|
||||||
|
|
||||||
@ -386,7 +381,7 @@ def sign_message_digest(digest, subpath, prompt, addr_fmt=AF_CLASSIC, pk=None):
|
|||||||
else:
|
else:
|
||||||
# if private key is provided, derivation subpath is ignored
|
# if private key is provided, derivation subpath is ignored
|
||||||
# and given private key is used for signing.
|
# and given private key is used for signing.
|
||||||
node = node_from_privkey(pk)
|
node = ngu.hdnode.HDNode().from_chaincode_privkey(bytes(32), pk)
|
||||||
dis.progress_sofar(50, 100)
|
dis.progress_sofar(50, 100)
|
||||||
addr = ch.address(node, addr_fmt)
|
addr = ch.address(node, addr_fmt)
|
||||||
|
|
||||||
@ -413,10 +408,9 @@ async def ux_sign_msg(txt, approved_cb=None, kill_menu=True):
|
|||||||
|
|
||||||
text, af = item.arg
|
text, af = item.arg
|
||||||
subpath = await msg_sign_ux_get_subpath(af)
|
subpath = await msg_sign_ux_get_subpath(af)
|
||||||
if subpath is None: return
|
|
||||||
|
|
||||||
await approve_msg_sign(text, subpath, af, approved_cb=approved_cb,
|
await approve_msg_sign(text, subpath, af, approved_cb=approved_cb,
|
||||||
kill_menu=kill_menu, allow_tab_nl=True)
|
kill_menu=kill_menu, only_printable=False)
|
||||||
|
|
||||||
# pick address format
|
# pick address format
|
||||||
rv = [
|
rv = [
|
||||||
|
|||||||
@ -10,7 +10,7 @@ from ux import ux_enter_bip32_index, ux_enter_number, OK, X
|
|||||||
from files import CardSlot, CardMissingError, needs_microsd
|
from files import CardSlot, CardMissingError, needs_microsd
|
||||||
from descriptor import MultisigDescriptor, multisig_descriptor_template
|
from descriptor import MultisigDescriptor, multisig_descriptor_template
|
||||||
from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AFC_SCRIPT, MAX_SIGNERS, AF_CLASSIC
|
from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AFC_SCRIPT, MAX_SIGNERS, AF_CLASSIC
|
||||||
from menu import MenuSystem, MenuItem, NonDefaultMenuItem, start_chooser, ToggleMenuItem, ShortcutItem
|
from menu import MenuSystem, MenuItem, NonDefaultMenuItem, start_chooser, ToggleMenuItem
|
||||||
from opcodes import OP_CHECKMULTISIG
|
from opcodes import OP_CHECKMULTISIG
|
||||||
from exceptions import FatalPSBTIssue
|
from exceptions import FatalPSBTIssue
|
||||||
from glob import settings
|
from glob import settings
|
||||||
@ -245,11 +245,9 @@ class MultisigWallet(WalletABC):
|
|||||||
return rv
|
return rv
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def iter_wallets(cls, M=None, N=None, not_idx=None, addr_fmts=None, name=None):
|
def iter_wallets(cls, M=None, N=None, not_idx=None, addr_fmt=None):
|
||||||
# yield MS wallets we know about, that match at least right M,N if known.
|
# yield MS wallets we know about, that match at least right M,N if known.
|
||||||
# - this is only place we should be searching this list, please!!
|
# - this is only place we should be searching this list, please!!
|
||||||
# addr_fmts: list of address formats we're interested in
|
|
||||||
# name: string ms wallet name
|
|
||||||
lst = settings.get('multisig', [])
|
lst = settings.get('multisig', [])
|
||||||
|
|
||||||
for idx, rec in enumerate(lst):
|
for idx, rec in enumerate(lst):
|
||||||
@ -257,19 +255,16 @@ class MultisigWallet(WalletABC):
|
|||||||
# ignore one by index
|
# ignore one by index
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if name and (rec[0] != name):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if M or N:
|
if M or N:
|
||||||
# peek at M/N
|
# peek at M/N
|
||||||
has_m, has_n = tuple(rec[1])
|
has_m, has_n = tuple(rec[1])
|
||||||
if M is not None and has_m != M: continue
|
if M is not None and has_m != M: continue
|
||||||
if N is not None and has_n != N: continue
|
if N is not None and has_n != N: continue
|
||||||
|
|
||||||
if addr_fmts:
|
if addr_fmt is not None:
|
||||||
opts = rec[3]
|
opts = rec[3]
|
||||||
af = opts.get('ft', AF_P2SH)
|
af = opts.get('ft', AF_P2SH)
|
||||||
if af not in addr_fmts: continue
|
if af != addr_fmt: continue
|
||||||
|
|
||||||
yield cls.deserialize(rec, idx)
|
yield cls.deserialize(rec, idx)
|
||||||
|
|
||||||
@ -278,23 +273,28 @@ class MultisigWallet(WalletABC):
|
|||||||
return list(self.xfp_paths.values())
|
return list(self.xfp_paths.values())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_match(cls, M, N, xfp_paths, addr_fmts=None):
|
def find_match(cls, M, N, xfp_paths, addr_fmt=None):
|
||||||
# Find index of matching wallet
|
# Find index of matching wallet
|
||||||
# - xfp_paths is list of lists: [xfp, *path] like in psbt files
|
# - xfp_paths is list of lists: [xfp, *path] like in psbt files
|
||||||
# - M and N must be known
|
# - M and N must be known
|
||||||
# - returns instance, or None if not found
|
# - returns instance, or None if not found
|
||||||
for rv in cls.iter_wallets(M, N, addr_fmts=addr_fmts):
|
for rv in cls.iter_wallets(M, N, addr_fmt=addr_fmt):
|
||||||
if rv.matching_subpaths(xfp_paths):
|
if rv.matching_subpaths(xfp_paths):
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_candidates(cls, xfp_paths):
|
def find_candidates(cls, xfp_paths, addr_fmt=None, M=None):
|
||||||
# Return a list of matching wallets for various M values.
|
# Return a list of matching wallets for various M values.
|
||||||
# - xpfs_paths should already be sorted
|
# - xpfs_paths should already be sorted
|
||||||
|
# - returns set of matches, of any M value
|
||||||
|
|
||||||
|
# we know N, but not M at this point.
|
||||||
|
N = len(xfp_paths)
|
||||||
|
|
||||||
matches = []
|
matches = []
|
||||||
for rv in cls.iter_wallets():
|
for rv in cls.iter_wallets(M=M, addr_fmt=addr_fmt):
|
||||||
if rv.matching_subpaths(xfp_paths):
|
if rv.matching_subpaths(xfp_paths):
|
||||||
matches.append(rv)
|
matches.append(rv)
|
||||||
|
|
||||||
@ -327,12 +327,11 @@ class MultisigWallet(WalletABC):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def assert_matching(self, M, N, xfp_paths, addr_fmt):
|
def assert_matching(self, M, N, xfp_paths):
|
||||||
# compare in-memory wallet with details recovered from PSBT
|
# compare in-memory wallet with details recovered from PSBT
|
||||||
# - xfp_paths must be sorted already
|
# - xfp_paths must be sorted already
|
||||||
assert (self.M, self.N) == (M, N), "M/N mismatch"
|
assert (self.M, self.N) == (M, N), "M/N mismatch"
|
||||||
assert len(xfp_paths) == N, "XFP count"
|
assert len(xfp_paths) == N, "XFP count"
|
||||||
assert self.addr_fmt == addr_fmt, "addr fmt"
|
|
||||||
if self.disable_checks: return
|
if self.disable_checks: return
|
||||||
assert self.matching_subpaths(xfp_paths), "wrong XFP/derivs"
|
assert self.matching_subpaths(xfp_paths), "wrong XFP/derivs"
|
||||||
|
|
||||||
@ -409,7 +408,7 @@ class MultisigWallet(WalletABC):
|
|||||||
# - count_similar: same N, same xfp+paths
|
# - count_similar: same N, same xfp+paths
|
||||||
|
|
||||||
lst = self.get_xfp_paths()
|
lst = self.get_xfp_paths()
|
||||||
c = self.find_match(self.M, self.N, lst, addr_fmts=[self.addr_fmt])
|
c = self.find_match(self.M, self.N, lst, addr_fmt=self.addr_fmt)
|
||||||
if c:
|
if c:
|
||||||
# All details are same: M/N, paths, addr fmt
|
# All details are same: M/N, paths, addr fmt
|
||||||
if sorted(self.xpubs) != sorted(c.xpubs):
|
if sorted(self.xpubs) != sorted(c.xpubs):
|
||||||
@ -424,11 +423,6 @@ class MultisigWallet(WalletABC):
|
|||||||
# do not allow to import multi if sortedmulti with the same set of keys
|
# do not allow to import multi if sortedmulti with the same set of keys
|
||||||
# already imported and vice-versa
|
# already imported and vice-versa
|
||||||
return None, ["BIP-67 clash"], 1
|
return None, ["BIP-67 clash"], 1
|
||||||
elif not self.bip67 and self.xpubs != c.xpubs:
|
|
||||||
# multi(2,A,B) and multi(2,B,A) are consensus-different scripts;
|
|
||||||
# treat as duplicates -- don't allow either if a same-keys variant
|
|
||||||
# in a different order is already enrolled
|
|
||||||
return None, ["key order"], 1
|
|
||||||
elif self.name == c.name:
|
elif self.name == c.name:
|
||||||
return None, [], 1
|
return None, [], 1
|
||||||
else:
|
else:
|
||||||
@ -460,7 +454,7 @@ class MultisigWallet(WalletABC):
|
|||||||
assert self.storage_idx >= 0
|
assert self.storage_idx >= 0
|
||||||
|
|
||||||
# safety check
|
# safety check
|
||||||
for existing in self.iter_wallets(M=self.M, N=self.N, addr_fmts=[self.addr_fmt]):
|
for existing in self.iter_wallets(M=self.M, N=self.N, addr_fmt=self.addr_fmt):
|
||||||
if existing.storage_idx != self.storage_idx: continue
|
if existing.storage_idx != self.storage_idx: continue
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@ -588,7 +582,7 @@ class MultisigWallet(WalletABC):
|
|||||||
found_pk = node.pubkey()
|
found_pk = node.pubkey()
|
||||||
|
|
||||||
# Document path(s) used. Not sure this is useful info to user tho.
|
# Document path(s) used. Not sure this is useful info to user tho.
|
||||||
# - Do not show what we can't verify: we don't really know the hardened
|
# - Do not show what we can't verify: we don't really know the hardeneded
|
||||||
# part of the path from fingerprint to here.
|
# part of the path from fingerprint to here.
|
||||||
here = '[%s]' % xfp2str(xfp)
|
here = '[%s]' % xfp2str(xfp)
|
||||||
if dp != len(path):
|
if dp != len(path):
|
||||||
@ -870,8 +864,7 @@ class MultisigWallet(WalletABC):
|
|||||||
|
|
||||||
def make_fname(self, prefix, suffix='txt'):
|
def make_fname(self, prefix, suffix='txt'):
|
||||||
rv = '%s-%s.%s' % (prefix, self.name, suffix)
|
rv = '%s-%s.%s' % (prefix, self.name, suffix)
|
||||||
rv = rv.replace(' ', '_')
|
return rv.replace(' ', '_')
|
||||||
return rv.replace('/', '-')
|
|
||||||
|
|
||||||
async def export_electrum(self):
|
async def export_electrum(self):
|
||||||
# Generate and save an Electrum JSON file.
|
# Generate and save an Electrum JSON file.
|
||||||
@ -975,7 +968,34 @@ class MultisigWallet(WalletABC):
|
|||||||
print('%s: %s' % (xfp2str(xfp), val), file=fp)
|
print('%s: %s' % (xfp2str(xfp), val), file=fp)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def import_from_psbt(cls, af, M, N, xpubs_list):
|
def guess_addr_fmt(cls, npath):
|
||||||
|
# Assuming the bips are being respected, what address format will be used,
|
||||||
|
# based on indicated numeric subkey path observed.
|
||||||
|
# - return None if unsure, no errors
|
||||||
|
#
|
||||||
|
#( "m/45h", 'p2sh', AF_P2SH),
|
||||||
|
#( "m/48h/{coin}h/0h/1h", 'p2sh_p2wsh', AF_P2WSH_P2SH),
|
||||||
|
#( "m/48h/{coin}h/0h/2h", 'p2wsh', AF_P2WSH)
|
||||||
|
|
||||||
|
top = npath[0] & 0x7fffffff
|
||||||
|
if top == npath[0]:
|
||||||
|
# non-hardened top? rare/bad
|
||||||
|
return
|
||||||
|
|
||||||
|
if top == 45:
|
||||||
|
return AF_P2SH
|
||||||
|
|
||||||
|
if top == 48:
|
||||||
|
if len(npath) < 4: return
|
||||||
|
|
||||||
|
last = npath[3] & 0x7fffffff
|
||||||
|
if last == 1:
|
||||||
|
return AF_P2WSH_P2SH
|
||||||
|
if last == 2:
|
||||||
|
return AF_P2WSH
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def import_from_psbt(cls, M, N, xpubs_list):
|
||||||
# given the raw data from PSBT global header, offer the user
|
# given the raw data from PSBT global header, offer the user
|
||||||
# the details, and/or bypass that all and just trust the data.
|
# the details, and/or bypass that all and just trust the data.
|
||||||
# - xpubs_list is a list of (xfp+path, binary BIP-32 xpub)
|
# - xpubs_list is a list of (xfp+path, binary BIP-32 xpub)
|
||||||
@ -1004,13 +1024,14 @@ class MultisigWallet(WalletABC):
|
|||||||
expect_chain, my_xfp, xpubs)
|
expect_chain, my_xfp, xpubs)
|
||||||
if is_mine:
|
if is_mine:
|
||||||
has_mine += 1
|
has_mine += 1
|
||||||
|
addr_fmt = cls.guess_addr_fmt(path)
|
||||||
|
|
||||||
assert has_mine == 1 # 'my key not included'
|
assert has_mine == 1 # 'my key not included'
|
||||||
|
|
||||||
name = 'PSBT-%d-of-%d' % (M, N)
|
name = 'PSBT-%d-of-%d' % (M, N)
|
||||||
# this will always create sortedmulti multisig (BIP-67)
|
# this will always create sortedmulti multisig (BIP-67)
|
||||||
# because BIP-174 came years after wide spread acceptance of BIP-67 policy
|
# because BIP-174 came years after wide spread acceptance of BIP-67 policy
|
||||||
ms = cls(name, (M, N), xpubs, chain_type=expect_chain, addr_fmt=af)
|
ms = cls(name, (M, N), xpubs, chain_type=expect_chain, addr_fmt=addr_fmt or AF_P2SH)
|
||||||
|
|
||||||
# may just keep in-memory version, no approval required, if we are
|
# may just keep in-memory version, no approval required, if we are
|
||||||
# trusting PSBT's today, otherwise caller will need to handle UX w.r.t new wallet
|
# trusting PSBT's today, otherwise caller will need to handle UX w.r.t new wallet
|
||||||
@ -1087,11 +1108,11 @@ class MultisigWallet(WalletABC):
|
|||||||
story = 'Update NAME only of existing multisig wallet?'
|
story = 'Update NAME only of existing multisig wallet?'
|
||||||
elif num_dups and isinstance(diff_items, list):
|
elif num_dups and isinstance(diff_items, list):
|
||||||
# failures only
|
# failures only
|
||||||
story = "Duplicate wallet. "
|
story = "Duplicate wallet."
|
||||||
if diff_items:
|
if diff_items:
|
||||||
story += diff_items[0]
|
story += diff_items[0]
|
||||||
else:
|
else:
|
||||||
story += 'All details are the same as existing!'
|
story += ' All details are the same as existing!'
|
||||||
is_dup = True
|
is_dup = True
|
||||||
elif diff_items:
|
elif diff_items:
|
||||||
# Concern here is overwrite when similar, but we don't overwrite anymore, so
|
# Concern here is overwrite when similar, but we don't overwrite anymore, so
|
||||||
@ -1386,7 +1407,7 @@ class MultisigMenu(MenuSystem):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def construct(cls):
|
def construct(cls):
|
||||||
# Dynamic menu with user-defined names of wallets shown
|
# Dynamic menu with user-defined names of wallets shown
|
||||||
from flow import nfc_enabled
|
from glob import NFC
|
||||||
|
|
||||||
if not MultisigWallet.exists():
|
if not MultisigWallet.exists():
|
||||||
rv = [MenuItem('(none setup yet)', f=no_ms_yet)]
|
rv = [MenuItem('(none setup yet)', f=no_ms_yet)]
|
||||||
@ -1396,7 +1417,11 @@ class MultisigMenu(MenuSystem):
|
|||||||
rv.append(MenuItem('%d/%d: %s' % (ms.M, ms.N, ms.name),
|
rv.append(MenuItem('%d/%d: %s' % (ms.M, ms.N, ms.name),
|
||||||
menu=make_ms_wallet_menu, arg=ms.storage_idx))
|
menu=make_ms_wallet_menu, arg=ms.storage_idx))
|
||||||
|
|
||||||
rv.append(MenuItem('Import', f=import_multisig))
|
rv.append(MenuItem('Import from File', f=import_multisig))
|
||||||
|
rv.append(MenuItem('Import from QR', f=import_multisig_qr,
|
||||||
|
predicate=version.has_qwerty, shortcut=KEY_QR))
|
||||||
|
rv.append(MenuItem('Import via NFC', f=import_multisig_nfc,
|
||||||
|
predicate=bool(NFC), shortcut=KEY_NFC))
|
||||||
rv.append(MenuItem('Export XPUB', f=export_multisig_xpubs))
|
rv.append(MenuItem('Export XPUB', f=export_multisig_xpubs))
|
||||||
rv.append(MenuItem('Create Airgapped', f=create_ms_step1))
|
rv.append(MenuItem('Create Airgapped', f=create_ms_step1))
|
||||||
rv.append(MenuItem('Trust PSBT?', f=trust_psbt_menu))
|
rv.append(MenuItem('Trust PSBT?', f=trust_psbt_menu))
|
||||||
@ -1410,9 +1435,6 @@ class MultisigMenu(MenuSystem):
|
|||||||
rv.append(NonDefaultMenuItem(
|
rv.append(NonDefaultMenuItem(
|
||||||
'Unsorted Multisig?' if version.has_qwerty else 'Unsorted Multi?',
|
'Unsorted Multisig?' if version.has_qwerty else 'Unsorted Multi?',
|
||||||
'unsort_ms', f=unsorted_ms_menu))
|
'unsort_ms', f=unsorted_ms_menu))
|
||||||
|
|
||||||
rv.append(ShortcutItem(KEY_NFC, predicate=nfc_enabled, f=import_multisig_nfc))
|
|
||||||
rv.append(ShortcutItem(KEY_QR, predicate=version.has_qwerty, f=import_multisig_qr))
|
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def update_contents(self):
|
def update_contents(self):
|
||||||
@ -1523,7 +1545,7 @@ async def ms_wallet_electrum_export(menu, label, item):
|
|||||||
msg = 'The new wallet will have derivation path:\n %s\n and use %s addresses.\n' % (
|
msg = 'The new wallet will have derivation path:\n %s\n and use %s addresses.\n' % (
|
||||||
dsum, MultisigWallet.render_addr_fmt(ms.addr_fmt) )
|
dsum, MultisigWallet.render_addr_fmt(ms.addr_fmt) )
|
||||||
|
|
||||||
if await ux_show_story(electrum_export_story("Electrum", msg)) != 'y':
|
if await ux_show_story(electrum_export_story(msg)) != 'y':
|
||||||
return
|
return
|
||||||
|
|
||||||
await ms.export_electrum()
|
await ms.export_electrum()
|
||||||
@ -1571,8 +1593,7 @@ P2WSH:
|
|||||||
if ch != "y":
|
if ch != "y":
|
||||||
return
|
return
|
||||||
|
|
||||||
acct = await ux_enter_bip32_index('Account Number:')
|
acct = await ux_enter_bip32_index('Account Number:') or 0
|
||||||
if acct is None: return
|
|
||||||
|
|
||||||
def render(acct_num):
|
def render(acct_num):
|
||||||
sign_der = None
|
sign_der = None
|
||||||
@ -1785,8 +1806,7 @@ async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH, is_qr=False,
|
|||||||
secret, ccc_ms_count = for_ccc
|
secret, ccc_ms_count = for_ccc
|
||||||
# Always include 2 keys from CCC: own master (key A) and key C
|
# Always include 2 keys from CCC: own master (key A) and key C
|
||||||
# - force them to same derivation.
|
# - force them to same derivation.
|
||||||
acct = await ux_enter_bip32_index('CCC Account Number:')
|
acct = await ux_enter_bip32_index('CCC Account Number:') or 0
|
||||||
if acct is None: return
|
|
||||||
|
|
||||||
dis.fullscreen("Wait...")
|
dis.fullscreen("Wait...")
|
||||||
a = add_own_xpub(chain, acct, addr_fmt) # master: key A
|
a = add_own_xpub(chain, acct, addr_fmt) # master: key A
|
||||||
@ -1811,8 +1831,7 @@ async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH, is_qr=False,
|
|||||||
ch = await ux_show_story("Add current Coldcard with above XFP ?",
|
ch = await ux_show_story("Add current Coldcard with above XFP ?",
|
||||||
title="[%s]" % xfp2str(my_xfp))
|
title="[%s]" % xfp2str(my_xfp))
|
||||||
if ch == "y":
|
if ch == "y":
|
||||||
acct = await ux_enter_bip32_index('Account Number:')
|
acct = await ux_enter_bip32_index('Account Number:') or 0
|
||||||
if acct is None: return
|
|
||||||
dis.fullscreen("Wait...")
|
dis.fullscreen("Wait...")
|
||||||
xpubs.append(add_own_xpub(chain, acct, addr_fmt))
|
xpubs.append(add_own_xpub(chain, acct, addr_fmt))
|
||||||
num_mine += 1
|
num_mine += 1
|
||||||
@ -1827,8 +1846,10 @@ async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH, is_qr=False,
|
|||||||
M = 2
|
M = 2
|
||||||
else:
|
else:
|
||||||
# pick useful M value to start
|
# pick useful M value to start
|
||||||
M = await ux_enter_number("How many need to sign?(M)", N)
|
M = await ux_enter_number("How many need to sign?(M)", N, can_cancel=True)
|
||||||
if M is None: return
|
if not M:
|
||||||
|
await ux_dramatic_pause('Aborted.', 2)
|
||||||
|
return # user cancel
|
||||||
|
|
||||||
dis.fullscreen("Wait...")
|
dis.fullscreen("Wait...")
|
||||||
|
|
||||||
@ -1919,19 +1940,26 @@ async def import_multisig_qr(*a):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
|
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
|
||||||
|
|
||||||
|
|
||||||
async def import_multisig(*a):
|
async def import_multisig(*a):
|
||||||
# pick text file from SD card, import as multisig setup file
|
# pick text file from SD card, import as multisig setup file
|
||||||
from actions import file_picker
|
from actions import file_picker
|
||||||
from ux import import_export_prompt
|
from glob import VD
|
||||||
|
|
||||||
ch = await import_export_prompt("multisig wallet file", is_import=True)
|
force_vdisk = False
|
||||||
if isinstance(ch, str):
|
if VD:
|
||||||
if ch == KEY_QR:
|
prompt = "Press (1) to import multisig wallet file from SD Card"
|
||||||
await import_multisig_qr()
|
escape = "1"
|
||||||
elif ch == KEY_NFC:
|
if VD is not None:
|
||||||
await import_multisig_nfc()
|
prompt += ", press (2) to import from Virtual Disk"
|
||||||
return
|
escape += "2"
|
||||||
|
prompt += "."
|
||||||
|
ch = await ux_show_story(prompt, escape=escape)
|
||||||
|
if ch == "1":
|
||||||
|
force_vdisk=False
|
||||||
|
elif ch == "2":
|
||||||
|
force_vdisk = True
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
def possible(filename):
|
def possible(filename):
|
||||||
with open(filename, 'rt') as fd:
|
with open(filename, 'rt') as fd:
|
||||||
@ -1943,11 +1971,11 @@ async def import_multisig(*a):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
fn = await file_picker(suffix=['.txt', '.json'], min_size=100, max_size=20*200,
|
fn = await file_picker(suffix=['.txt', '.json'], min_size=100, max_size=20*200,
|
||||||
taster=possible, **ch)
|
taster=possible, force_vdisk=force_vdisk)
|
||||||
if not fn: return
|
if not fn: return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with CardSlot(**ch) as card:
|
with CardSlot(force_vdisk=force_vdisk) as card:
|
||||||
with open(fn, 'rt') as fp:
|
with open(fn, 'rt') as fp:
|
||||||
data = fp.read()
|
data = fp.read()
|
||||||
except CardMissingError:
|
except CardMissingError:
|
||||||
@ -1958,8 +1986,8 @@ async def import_multisig(*a):
|
|||||||
try:
|
try:
|
||||||
possible_name = (fn.split('/')[-1].split('.'))[0]
|
possible_name = (fn.split('/')[-1].split('.'))[0]
|
||||||
maybe_enroll_xpub(config=data, name=possible_name)
|
maybe_enroll_xpub(config=data, name=possible_name)
|
||||||
except BaseException as e:
|
except Exception as e:
|
||||||
# import sys; sys.print_exception(e)
|
#import sys; sys.print_exception(e)
|
||||||
await ux_show_story('Failed to import multisig.\n\n%s\n%s' % (e, problem_file_line(e)))
|
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
# ndef.py -- NDEF records: making them and parsing them.
|
# ndef.py -- NDEF records: making them and parsing them.
|
||||||
#
|
#
|
||||||
# - see ../docs/nfc-coldcard.md for background.
|
# - see ../docs/nfc-on-coldcard.md for background.
|
||||||
# - cross platform file
|
# - cross platform file
|
||||||
#
|
#
|
||||||
from struct import pack, unpack
|
from struct import pack, unpack
|
||||||
|
|||||||
@ -107,14 +107,13 @@ class NFCHandler:
|
|||||||
from glob import dis
|
from glob import dis
|
||||||
here = bytes(256)
|
here = bytes(256)
|
||||||
end = 8196
|
end = 8196
|
||||||
for pos in range(0, end, 256):
|
for pos in range(0, end, 256) :
|
||||||
self.i2c.writeto_mem(I2C_ADDR_USER, pos, here, addrsize=16)
|
self.i2c.writeto_mem(I2C_ADDR_USER, pos, here, addrsize=16)
|
||||||
if (pos == 256) and not full_wipe: break
|
if pos == 256 and not full_wipe: break
|
||||||
|
|
||||||
# 6ms per 16 byte row, worst case, so ~100ms here per iter! 3.2seconds total
|
# 6ms per 16 byte row, worst case, so ~100ms here per iter! 3.2seconds total
|
||||||
if full_wipe:
|
if full_wipe:
|
||||||
dis.progress_bar_show(pos / end)
|
dis.progress_bar_show(pos / end)
|
||||||
|
|
||||||
await self.wait_ready()
|
await self.wait_ready()
|
||||||
|
|
||||||
# system config area (flash cells, but affect operation): table 12
|
# system config area (flash cells, but affect operation): table 12
|
||||||
@ -226,15 +225,9 @@ class NFCHandler:
|
|||||||
self.set_rf_disable(1)
|
self.set_rf_disable(1)
|
||||||
|
|
||||||
async def share_loop(self, n, **kws):
|
async def share_loop(self, n, **kws):
|
||||||
# Keep one fully-written tag image live until the user exits. Some
|
|
||||||
# phones perform multiple probes while deciding if a tag is NDEF.
|
|
||||||
await self.big_write(n.bytes())
|
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
aborted = await self.ux_animation(exit_after_activity=False, **kws)
|
done = await self.share_start(n, **kws)
|
||||||
if aborted:
|
if done: break
|
||||||
await self.wipe(kws.get("is_secret", False))
|
|
||||||
break
|
|
||||||
|
|
||||||
async def share_signed_txn(self, txid, file_offset, txn_len, txn_sha):
|
async def share_signed_txn(self, txid, file_offset, txn_len, txn_sha):
|
||||||
# we just signed something, share it over NFC
|
# we just signed something, share it over NFC
|
||||||
@ -404,13 +397,12 @@ class NFCHandler:
|
|||||||
self.write_dyn(GPO_CTRL_Dyn, 0x01) # GPO_EN
|
self.write_dyn(GPO_CTRL_Dyn, 0x01) # GPO_EN
|
||||||
self.read_dyn(IT_STS_Dyn) # clear interrupt
|
self.read_dyn(IT_STS_Dyn) # clear interrupt
|
||||||
|
|
||||||
async def ux_animation(self, allow_enter=True, prompt=None, line2=None,
|
async def ux_animation(self, write_mode, allow_enter=True, prompt=None, line2=None,
|
||||||
is_secret=False, exit_after_activity=True,
|
is_secret=False):
|
||||||
min_delay=1000):
|
|
||||||
# Run the pretty animation, and detect both when we are written, and/or key to exit/abort.
|
# Run the pretty animation, and detect both when we are written, and/or key to exit/abort.
|
||||||
# - similar when "read" and then removed from field
|
# - similar when "read" and then removed from field
|
||||||
# - return T if aborted by user
|
# - return T if aborted by user
|
||||||
from glob import dis
|
from glob import dis, numpad
|
||||||
|
|
||||||
await self.wait_ready()
|
await self.wait_ready()
|
||||||
self.set_rf_disable(0)
|
self.set_rf_disable(0)
|
||||||
@ -423,8 +415,7 @@ class NFCHandler:
|
|||||||
dis.text(None, -3, line2)
|
dis.text(None, -3, line2)
|
||||||
else:
|
else:
|
||||||
from graphics_mk4 import Graphics
|
from graphics_mk4 import Graphics
|
||||||
from version import mk_num
|
frames = [getattr(Graphics, 'mk4_nfc_%d'%i) for i in range(1, 5)]
|
||||||
frames = [getattr(Graphics, 'mk%d_nfc_%d'%(mk_num, i)) for i in range(1, 5)]
|
|
||||||
|
|
||||||
aborted = True
|
aborted = True
|
||||||
phase = -1
|
phase = -1
|
||||||
@ -432,6 +423,7 @@ class NFCHandler:
|
|||||||
|
|
||||||
# (ms) How long to wait after RF field comes and goes
|
# (ms) How long to wait after RF field comes and goes
|
||||||
# - user can press OK during this period if they know they are done
|
# - user can press OK during this period if they know they are done
|
||||||
|
min_delay = (3000 if write_mode else 1000)
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
if dis.has_lcd:
|
if dis.has_lcd:
|
||||||
@ -470,7 +462,7 @@ class NFCHandler:
|
|||||||
aborted = False
|
aborted = False
|
||||||
break
|
break
|
||||||
|
|
||||||
if exit_after_activity and last_activity:
|
if last_activity:
|
||||||
dt = utime.ticks_diff(utime.ticks_ms(), last_activity)
|
dt = utime.ticks_diff(utime.ticks_ms(), last_activity)
|
||||||
if dt >= min_delay:
|
if dt >= min_delay:
|
||||||
# They acheived some RF activity and then nothing for some time, so
|
# They acheived some RF activity and then nothing for some time, so
|
||||||
@ -479,6 +471,9 @@ class NFCHandler:
|
|||||||
break
|
break
|
||||||
|
|
||||||
self.set_rf_disable(1)
|
self.set_rf_disable(1)
|
||||||
|
if not write_mode:
|
||||||
|
# function argument secret decides whether to do full wipe after writing to chip
|
||||||
|
await self.wipe(is_secret)
|
||||||
|
|
||||||
return aborted
|
return aborted
|
||||||
|
|
||||||
@ -486,15 +481,17 @@ class NFCHandler:
|
|||||||
# do the UX while we are sharing a value over NFC
|
# do the UX while we are sharing a value over NFC
|
||||||
# - assumpting is people know what they are scanning
|
# - assumpting is people know what they are scanning
|
||||||
# - x key to abort early, but also self-clears
|
# - x key to abort early, but also self-clears
|
||||||
|
|
||||||
await self.big_write(ndef_obj.bytes())
|
await self.big_write(ndef_obj.bytes())
|
||||||
return await self.ux_animation(**kws)
|
|
||||||
|
return await self.ux_animation(False, **kws)
|
||||||
|
|
||||||
async def start_nfc_rx(self, **kws):
|
async def start_nfc_rx(self, **kws):
|
||||||
# Pretend to be a big warm empty tag ready to be stuffed with data
|
# Pretend to be a big warm empty tag ready to be stuffed with data
|
||||||
await self.big_write(ndef.CC_WR_FILE)
|
await self.big_write(ndef.CC_WR_FILE)
|
||||||
|
|
||||||
# wait until something is written
|
# wait until something is written
|
||||||
aborted = await self.ux_animation(min_delay=3000, **kws)
|
aborted = await self.ux_animation(True, **kws)
|
||||||
if aborted: return
|
if aborted: return
|
||||||
|
|
||||||
# read CCFILE area (header)
|
# read CCFILE area (header)
|
||||||
@ -621,7 +618,7 @@ class NFCHandler:
|
|||||||
# it's a txn, and we wrote as hex
|
# it's a txn, and we wrote as hex
|
||||||
data = a2b_hex(data)
|
data = a2b_hex(data)
|
||||||
else:
|
else:
|
||||||
assert data[1:4] == bytes(3)
|
assert data[2:8] == bytes(6)
|
||||||
sha = ngu.hash.sha256s(data)
|
sha = ngu.hash.sha256s(data)
|
||||||
await self.share_signed_txn(txid, data, len(data), sha)
|
await self.share_signed_txn(txid, data, len(data), sha)
|
||||||
elif ext == 'psbt':
|
elif ext == 'psbt':
|
||||||
@ -742,7 +739,7 @@ class NFCHandler:
|
|||||||
m = m.decode()
|
m = m.decode()
|
||||||
what, vals = decode_bip21_text(m)
|
what, vals = decode_bip21_text(m)
|
||||||
if what == 'addr':
|
if what == 'addr':
|
||||||
return vals
|
return vals[1]
|
||||||
|
|
||||||
winner = await self._nfc_reader(f, 'Unable to find address from NFC data.')
|
winner = await self._nfc_reader(f, 'Unable to find address from NFC data.')
|
||||||
|
|
||||||
@ -750,11 +747,10 @@ class NFCHandler:
|
|||||||
|
|
||||||
async def verify_address_nfc(self):
|
async def verify_address_nfc(self):
|
||||||
# Get an address or complete bip-21 url even and search it... slow.
|
# Get an address or complete bip-21 url even and search it... slow.
|
||||||
res = await self.read_address()
|
winner = await self.read_address()
|
||||||
if not res: return
|
if winner:
|
||||||
_, addr, args = res
|
from ownership import OWNERSHIP
|
||||||
from ownership import OWNERSHIP
|
await OWNERSHIP.search_ux(winner)
|
||||||
await OWNERSHIP.search_ux(addr, args)
|
|
||||||
|
|
||||||
async def read_extended_private_key(self):
|
async def read_extended_private_key(self):
|
||||||
f = lambda x: x.decode().strip() if b"prv" in x else None
|
f = lambda x: x.decode().strip() if b"prv" in x else None
|
||||||
@ -764,31 +760,20 @@ class NFCHandler:
|
|||||||
f = lambda x: a2b_base64(x.decode()) if 150 <= len(x) <= 280 else None
|
f = lambda x: a2b_base64(x.decode()) if 150 <= len(x) <= 280 else None
|
||||||
return await self._nfc_reader(f, 'Unable to find base64 encoded TAPSIGNER backup.')
|
return await self._nfc_reader(f, 'Unable to find base64 encoded TAPSIGNER backup.')
|
||||||
|
|
||||||
async def read_bip322_msg(self):
|
|
||||||
f = lambda x: x.decode()
|
|
||||||
return await self._nfc_reader(f, 'Unable to find BIP-322 message.')
|
|
||||||
|
|
||||||
async def read_wif(self):
|
|
||||||
# only compressed WIFs allowed
|
|
||||||
f = lambda x: x.decode() if len(x) >= 51 else None
|
|
||||||
return await self._nfc_reader(f, 'Unable to find WIF key(s).')
|
|
||||||
|
|
||||||
async def _nfc_reader(self, func, fail_msg):
|
async def _nfc_reader(self, func, fail_msg):
|
||||||
data = await self.start_nfc_rx()
|
data = await self.start_nfc_rx()
|
||||||
if not data: return
|
if not data: return
|
||||||
|
|
||||||
winner = None
|
winner = None
|
||||||
try:
|
for urn, msg, meta in ndef.record_parser(data):
|
||||||
for urn, msg, meta in ndef.record_parser(data):
|
msg = bytes(msg)
|
||||||
msg = bytes(msg)
|
try:
|
||||||
try:
|
r = func(msg)
|
||||||
r = func(msg)
|
if r is not None:
|
||||||
if r is not None:
|
winner = r
|
||||||
winner = r
|
break
|
||||||
break
|
except:
|
||||||
except:
|
pass
|
||||||
pass
|
|
||||||
except Exception: pass # dont crash when given garbage
|
|
||||||
|
|
||||||
if not winner:
|
if not winner:
|
||||||
await ux_show_story(fail_msg)
|
await ux_show_story(fail_msg)
|
||||||
|
|||||||
278
shared/notes.py
278
shared/notes.py
@ -10,11 +10,10 @@ from ux_q1 import QRScannerInteraction
|
|||||||
from actions import goto_top_menu
|
from actions import goto_top_menu
|
||||||
from glob import settings, dis
|
from glob import settings, dis
|
||||||
from files import CardMissingError, needs_microsd, CardSlot
|
from files import CardMissingError, needs_microsd, CardSlot
|
||||||
from public_constants import MSG_SIGNING_MAX_LENGTH
|
|
||||||
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
|
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
|
||||||
from charcodes import KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6
|
from charcodes import KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6
|
||||||
from lcd_display import CHARS_W
|
from lcd_display import CHARS_W
|
||||||
from utils import problem_file_line, url_unquote, wipe_if_deltamode, is_printable
|
from utils import problem_file_line, url_unquote, wipe_if_deltamode
|
||||||
|
|
||||||
# title, username and such are limited that they fit on the one line both in
|
# title, username and such are limited that they fit on the one line both in
|
||||||
# text entry (W-2) and also in menu display (W-3)
|
# text entry (W-2) and also in menu display (W-3)
|
||||||
@ -22,15 +21,6 @@ from utils import problem_file_line, url_unquote, wipe_if_deltamode, is_printabl
|
|||||||
ONE_LINE = CHARS_W-2
|
ONE_LINE = CHARS_W-2
|
||||||
|
|
||||||
async def make_notes_menu(*a):
|
async def make_notes_menu(*a):
|
||||||
from pincodes import pa
|
|
||||||
|
|
||||||
if pa.hobbled_mode:
|
|
||||||
# Read only version of menu system
|
|
||||||
# - used when spending policy in effect
|
|
||||||
# - must have some notes already, or unreachable
|
|
||||||
rv = NotesMenu(NotesMenu.construct_readonly())
|
|
||||||
rv.readonly = True
|
|
||||||
return rv
|
|
||||||
|
|
||||||
if not settings.get('secnap', False):
|
if not settings.get('secnap', False):
|
||||||
# Explain feature, and then enable if interested. Drop them into menu.
|
# Explain feature, and then enable if interested. Drop them into menu.
|
||||||
@ -115,8 +105,6 @@ async def get_a_password(old_value, min_len=0, max_len=128):
|
|||||||
|
|
||||||
class NotesMenu(MenuSystem):
|
class NotesMenu(MenuSystem):
|
||||||
|
|
||||||
readonly = False
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def construct(cls):
|
def construct(cls):
|
||||||
# Dynamic menu with user-defined names of notes shown
|
# Dynamic menu with user-defined names of notes shown
|
||||||
@ -131,7 +119,9 @@ class NotesMenu(MenuSystem):
|
|||||||
else:
|
else:
|
||||||
wipe_if_deltamode()
|
wipe_if_deltamode()
|
||||||
|
|
||||||
rv = cls.construct_note_items(readonly=False)
|
rv = []
|
||||||
|
for note in NoteContent.get_all():
|
||||||
|
rv.append(MenuItem('%d: %s' % (note.idx+1, note.title), menu=note.make_menu))
|
||||||
|
|
||||||
rv.extend(news)
|
rv.extend(news)
|
||||||
|
|
||||||
@ -144,39 +134,6 @@ class NotesMenu(MenuSystem):
|
|||||||
|
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def construct_readonly(cls):
|
|
||||||
# When only allowed to view, no export/add new/delete.
|
|
||||||
wipe_if_deltamode()
|
|
||||||
|
|
||||||
rv = cls.construct_note_items(readonly=True)
|
|
||||||
|
|
||||||
if not rv:
|
|
||||||
rv.append(MenuItem('(none saved yet)'))
|
|
||||||
|
|
||||||
return rv
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def construct_note_items(cls, readonly=False):
|
|
||||||
rv = []
|
|
||||||
by_group = {}
|
|
||||||
|
|
||||||
for note in NoteContent.get_all():
|
|
||||||
item = MenuItem('%d: %s' % (note.idx+1, note.title),
|
|
||||||
menu=note.make_menu, arg=readonly)
|
|
||||||
group = note.group
|
|
||||||
if group:
|
|
||||||
if group not in by_group:
|
|
||||||
by_group[group] = []
|
|
||||||
by_group[group].append(item)
|
|
||||||
else:
|
|
||||||
rv.append(item)
|
|
||||||
|
|
||||||
for group in sorted(by_group):
|
|
||||||
rv.append(MenuItem('↳ ' + group, menu=NoteGroupMenu(group, readonly)))
|
|
||||||
|
|
||||||
return rv
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def export_all(cls, *a):
|
async def export_all(cls, *a):
|
||||||
await start_export(NoteContent.get_all())
|
await start_export(NoteContent.get_all())
|
||||||
@ -228,7 +185,7 @@ class NotesMenu(MenuSystem):
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def disable_notes(cls, *a):
|
async def disable_notes(cls, *a):
|
||||||
# they don't want feature anymore; already checked no notes in effect
|
# they don't want feature anymore; already checked no notes in effect
|
||||||
# - no need for confirm, they aren't losing anything
|
# - no need for confirm, they aren't loosing anything
|
||||||
settings.remove_key('secnap')
|
settings.remove_key('secnap')
|
||||||
settings.remove_key('notes')
|
settings.remove_key('notes')
|
||||||
settings.save()
|
settings.save()
|
||||||
@ -247,27 +204,9 @@ class NotesMenu(MenuSystem):
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def drill_to(cls, menu, item):
|
async def drill_to(cls, menu, item):
|
||||||
# make it so looks like we drilled down into the new note
|
# make it so looks like we drilled down into the new note
|
||||||
label = '%d: %s' % (item.idx+1, item.title)
|
menu.goto_idx(item.idx)
|
||||||
group = item.group
|
m = MenuSystem(await item.make_menu())
|
||||||
if group:
|
the_ux.push(m)
|
||||||
cls.goto_exact_label(menu, '↳ ' + group)
|
|
||||||
gm = NoteGroupMenu(group)
|
|
||||||
cls.goto_exact_label(gm, label)
|
|
||||||
the_ux.push(gm)
|
|
||||||
else:
|
|
||||||
cls.goto_exact_label(menu, label)
|
|
||||||
|
|
||||||
m = await item._make_menu()
|
|
||||||
the_ux.push(MenuSystem(m))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def goto_exact_label(menu, label):
|
|
||||||
for i, mi in enumerate(menu.items):
|
|
||||||
if mi.label == label:
|
|
||||||
menu.goto_idx(i)
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class NoteContentBase:
|
class NoteContentBase:
|
||||||
@ -284,15 +223,9 @@ class NoteContentBase:
|
|||||||
return PasswordContent(j, idx) if 'user' in j else NoteContent(j, idx)
|
return PasswordContent(j, idx) if 'user' in j else NoteContent(j, idx)
|
||||||
|
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
res = {}
|
return {fld:getattr(self, fld, '') for fld in self.flds}
|
||||||
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
|
|
||||||
|
|
||||||
return res
|
to_json = serialize
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all(cls):
|
def get_all(cls):
|
||||||
@ -318,15 +251,6 @@ class NoteContentBase:
|
|||||||
settings.put('notes', [n.serialize() for n in notes])
|
settings.put('notes', [n.serialize() for n in notes])
|
||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_groups(cls):
|
|
||||||
groups = set()
|
|
||||||
for note in cls.get_all():
|
|
||||||
if note.group:
|
|
||||||
groups.add(note.group)
|
|
||||||
|
|
||||||
return sorted(groups)
|
|
||||||
|
|
||||||
async def delete(self, *a):
|
async def delete(self, *a):
|
||||||
# Remove note
|
# Remove note
|
||||||
ok = await ux_confirm("Everything about this note/password will be lost.")
|
ok = await ux_confirm("Everything about this note/password will be lost.")
|
||||||
@ -347,11 +271,6 @@ class NoteContentBase:
|
|||||||
the_ux.pop()
|
the_ux.pop()
|
||||||
m = the_ux.top_of_stack()
|
m = the_ux.top_of_stack()
|
||||||
m.update_contents()
|
m.update_contents()
|
||||||
parent = the_ux.parent_of(m)
|
|
||||||
if parent:
|
|
||||||
parent.update_contents()
|
|
||||||
if isinstance(m, NoteGroupMenu) and not m.has_notes():
|
|
||||||
the_ux.pop()
|
|
||||||
|
|
||||||
await ux_dramatic_pause('Deleted.', 3)
|
await ux_dramatic_pause('Deleted.', 3)
|
||||||
|
|
||||||
@ -383,17 +302,11 @@ class NoteContentBase:
|
|||||||
|
|
||||||
if not is_new:
|
if not is_new:
|
||||||
# change our own menu contents
|
# change our own menu contents
|
||||||
mi = await self._make_menu()
|
menu.replace_items(await self.make_menu())
|
||||||
menu.replace_items(mi)
|
|
||||||
|
|
||||||
# update parent
|
# update parent
|
||||||
parent = the_ux.parent_of(menu)
|
parent = the_ux.parent_of(menu)
|
||||||
parent.update_contents()
|
parent.update_contents()
|
||||||
grandparent = the_ux.parent_of(parent)
|
|
||||||
if grandparent:
|
|
||||||
grandparent.update_contents()
|
|
||||||
if isinstance(parent, NoteGroupMenu) and not parent.has_notes():
|
|
||||||
the_ux.stack.remove(parent)
|
|
||||||
else:
|
else:
|
||||||
menu.update_contents()
|
menu.update_contents()
|
||||||
|
|
||||||
@ -423,132 +336,33 @@ class NoteContentBase:
|
|||||||
await ux_sign_msg(txt, approved_cb=msg_signing_done, kill_menu=False)
|
await ux_sign_msg(txt, approved_cb=msg_signing_done, kill_menu=False)
|
||||||
|
|
||||||
def sign_misc_menu_item(self):
|
def sign_misc_menu_item(self):
|
||||||
return MenuItem("Sign Note Text", f=self.sign_txt_msg, arg=self.misc,
|
return MenuItem("Sign Note Text", f=self.sign_txt_msg, arg=self.misc)
|
||||||
predicate=2 <= len(self.misc) <= MSG_SIGNING_MAX_LENGTH)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_b39pass_applicable(data, read_only):
|
|
||||||
from seed import MAX_PASS_LEN
|
|
||||||
from ccc import sssp_spending_policy
|
|
||||||
if read_only and not sssp_spending_policy('okeys'):
|
|
||||||
return False
|
|
||||||
return (len(data) <= MAX_PASS_LEN) and is_printable(data) and settings.get("words", True)
|
|
||||||
|
|
||||||
async def apply_as_b39_pass(self, a, b, item):
|
|
||||||
data, readonly = item.arg
|
|
||||||
# rstrip just trailing whitespaces/tabs/newlines
|
|
||||||
data = data.rstrip()
|
|
||||||
# do not allow any more tabs/newlines
|
|
||||||
assert self.is_b39pass_applicable(data, readonly)
|
|
||||||
from seed import apply_pass_value
|
|
||||||
await apply_pass_value(data)
|
|
||||||
|
|
||||||
|
|
||||||
class NoteGroupMenu(MenuSystem):
|
|
||||||
def __init__(self, group, readonly=False):
|
|
||||||
self.group = group
|
|
||||||
self.readonly = readonly
|
|
||||||
super().__init__(self.construct())
|
|
||||||
|
|
||||||
def construct(self):
|
|
||||||
items = []
|
|
||||||
for note in NoteContent.get_all():
|
|
||||||
if note.group == self.group:
|
|
||||||
items.append(MenuItem('%d: %s' % (note.idx+1, note.title),
|
|
||||||
menu=note.make_menu, arg=self.readonly))
|
|
||||||
|
|
||||||
return items or [MenuItem('(none)')]
|
|
||||||
|
|
||||||
def has_notes(self):
|
|
||||||
return any(note.group == self.group for note in NoteContent.get_all())
|
|
||||||
|
|
||||||
def update_contents(self):
|
|
||||||
self.replace_items(self.construct())
|
|
||||||
|
|
||||||
|
|
||||||
class GroupPickerMenu(MenuSystem):
|
|
||||||
def __init__(self, current=''):
|
|
||||||
self.result = None
|
|
||||||
self.current = current
|
|
||||||
|
|
||||||
groups = NoteContentBase.get_groups()
|
|
||||||
chosen = 0
|
|
||||||
items = [MenuItem('(none)', f=self.picked, arg='')]
|
|
||||||
|
|
||||||
for group in groups:
|
|
||||||
if group == self.current:
|
|
||||||
chosen = len(items)
|
|
||||||
items.append(MenuItem(group, f=self.picked, arg=group))
|
|
||||||
|
|
||||||
items.append(MenuItem('New Group', f=self.new_group))
|
|
||||||
|
|
||||||
super().__init__(items, chosen=chosen)
|
|
||||||
|
|
||||||
async def picked(self, menu, idx, mi):
|
|
||||||
assert menu == self
|
|
||||||
self.result = mi.arg
|
|
||||||
the_ux.pop()
|
|
||||||
|
|
||||||
async def new_group(self, menu, idx, mi):
|
|
||||||
group = await ux_input_text('', max_len=ONE_LINE, confirm_exit=False,
|
|
||||||
prompt='Group', placeholder='(optional)')
|
|
||||||
if group is None:
|
|
||||||
self.result = None
|
|
||||||
else:
|
|
||||||
self.result = group
|
|
||||||
|
|
||||||
the_ux.pop()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def pick(cls, current=''):
|
|
||||||
m = cls(current)
|
|
||||||
the_ux.push(m)
|
|
||||||
await m.interact()
|
|
||||||
|
|
||||||
return current if m.result is None else m.result
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordContent(NoteContentBase):
|
class PasswordContent(NoteContentBase):
|
||||||
# "Passwords" have a few more fields and are more structured
|
# "Passwords" have a few more fields and are more structured
|
||||||
flds = ['title', 'user', 'password', 'site', 'misc', 'group']
|
flds = ['title', 'user', 'password', 'site', 'misc' ]
|
||||||
type_label = 'password'
|
type_label = 'password'
|
||||||
|
|
||||||
async def _make_menu(self, readonly=False):
|
async def make_menu(self, *a):
|
||||||
rv = [MenuItem('"%s"' % self.title, f=self.view)]
|
rv = [MenuItem('"%s"' % self.title, f=self.view)]
|
||||||
if self.user:
|
if self.user:
|
||||||
rv.append(MenuItem('↳ %s' % self.user, f=self.view))
|
rv.append(MenuItem('↳ %s' % self.user, f=self.view))
|
||||||
if self.site:
|
if self.site:
|
||||||
rv.append(MenuItem('↳ %s' % self.site, f=self.view))
|
rv.append(MenuItem('↳ %s' % self.site, f=self.view))
|
||||||
# if self.misc: rv.append(MenuItem('↳ (notes)', f=self.view))
|
#if self.misc: rv.append(MenuItem('↳ (notes)', f=self.view))
|
||||||
rv += [
|
return rv + [
|
||||||
MenuItem('View Password', f=self.view_pw),
|
MenuItem('View Password', f=self.view_pw),
|
||||||
MenuItem('Send Password', f=self.send_pw, predicate=lambda: not settings.get('du', 0)),
|
MenuItem('Send Password', f=self.send_pw, predicate=lambda: settings.get('du', True)),
|
||||||
]
|
MenuItem('Export', f=self.export),
|
||||||
if not readonly:
|
MenuItem('Edit Metadata', f=self.edit),
|
||||||
rv += [
|
MenuItem('Delete', f=self.delete),
|
||||||
MenuItem('Export', f=self.export),
|
MenuItem('Change Password', f=self.change_pw),
|
||||||
MenuItem('Edit Metadata', f=self.edit),
|
|
||||||
MenuItem('Delete', f=self.delete),
|
|
||||||
MenuItem('Change Password', f=self.change_pw),
|
|
||||||
]
|
|
||||||
rv += [
|
|
||||||
self.sign_misc_menu_item(),
|
self.sign_misc_menu_item(),
|
||||||
ShortcutItem(KEY_QR, f=self.view_qr_menu, arg=self.type_label),
|
ShortcutItem(KEY_QR, f=self.view_qr_menu, arg=self.type_label),
|
||||||
ShortcutItem(KEY_NFC, f=self.share_nfc, arg=self.type_label),
|
ShortcutItem(KEY_NFC, f=self.share_nfc, arg=self.type_label),
|
||||||
]
|
]
|
||||||
|
|
||||||
# if password is less than MAX_PASS_LEN and only consist of printable ASCII characters
|
|
||||||
# and current seed (master or tmp) is word based - offer to apply pwd text as BIP-39 passphrase
|
|
||||||
if self.is_b39pass_applicable(self.password, readonly):
|
|
||||||
rv += [MenuItem('Apply as BIP-39 Passphrase',
|
|
||||||
f=self.apply_as_b39_pass, arg=(self.password, readonly))]
|
|
||||||
|
|
||||||
return rv
|
|
||||||
|
|
||||||
async def make_menu(self, a, b, item):
|
|
||||||
items = await self._make_menu(readonly=item.arg)
|
|
||||||
return MenuSystem(items)
|
|
||||||
|
|
||||||
async def view(self, *a):
|
async def view(self, *a):
|
||||||
pl = len(self.password)
|
pl = len(self.password)
|
||||||
m = ''
|
m = ''
|
||||||
@ -616,8 +430,7 @@ class PasswordContent(NoteContentBase):
|
|||||||
|
|
||||||
if self.idx == -1:
|
if self.idx == -1:
|
||||||
# prompt for password only on new records.
|
# prompt for password only on new records.
|
||||||
# can be None if CANCEL is pressed - handle, Send Password requires string
|
self.password = await get_a_password(self.password)
|
||||||
self.password = await get_a_password(self.password) or ""
|
|
||||||
|
|
||||||
site = await ux_input_text(self.site, max_len=ONE_LINE, scan_ok=True, confirm_exit=False,
|
site = await ux_input_text(self.site, max_len=ONE_LINE, scan_ok=True, confirm_exit=False,
|
||||||
prompt='Website', placeholder='(optional)')
|
prompt='Website', placeholder='(optional)')
|
||||||
@ -629,8 +442,6 @@ class PasswordContent(NoteContentBase):
|
|||||||
if misc is None:
|
if misc is None:
|
||||||
misc = self.misc
|
misc = self.misc
|
||||||
|
|
||||||
group = await GroupPickerMenu.pick(self.group)
|
|
||||||
|
|
||||||
if self.idx != -1:
|
if self.idx != -1:
|
||||||
# confirm changes, don't for new records
|
# confirm changes, don't for new records
|
||||||
chgs = []
|
chgs = []
|
||||||
@ -642,8 +453,6 @@ class PasswordContent(NoteContentBase):
|
|||||||
chgs.append('Username')
|
chgs.append('Username')
|
||||||
if self.misc != misc:
|
if self.misc != misc:
|
||||||
chgs.append('Other Notes')
|
chgs.append('Other Notes')
|
||||||
if self.group != group:
|
|
||||||
chgs.append('Group')
|
|
||||||
|
|
||||||
if not chgs:
|
if not chgs:
|
||||||
await ux_dramatic_pause('No changes.', 3)
|
await ux_dramatic_pause('No changes.', 3)
|
||||||
@ -657,7 +466,6 @@ class PasswordContent(NoteContentBase):
|
|||||||
self.user = user
|
self.user = user
|
||||||
self.site = site
|
self.site = site
|
||||||
self.misc = misc
|
self.misc = misc
|
||||||
self.group = group
|
|
||||||
|
|
||||||
await self._save_ux(menu)
|
await self._save_ux(menu)
|
||||||
return self
|
return self
|
||||||
@ -665,41 +473,22 @@ class PasswordContent(NoteContentBase):
|
|||||||
|
|
||||||
class NoteContent(NoteContentBase):
|
class NoteContent(NoteContentBase):
|
||||||
# Pure "notes" have just a title and free-form text
|
# Pure "notes" have just a title and free-form text
|
||||||
flds = ['title', 'misc', 'group']
|
flds = ['title', 'misc']
|
||||||
type_label = 'note'
|
type_label = 'note'
|
||||||
|
|
||||||
async def _make_menu(self, readonly=False):
|
async def make_menu(self, *a):
|
||||||
# Details and actions for this Note
|
# Details and actions for this Note
|
||||||
|
return [
|
||||||
rv = [
|
|
||||||
MenuItem('"%s"' % self.title, f=self.view),
|
MenuItem('"%s"' % self.title, f=self.view),
|
||||||
MenuItem('View Note', f=self.view),
|
MenuItem('View Note', f=self.view),
|
||||||
]
|
MenuItem('Edit', f=self.edit),
|
||||||
if not readonly:
|
MenuItem('Delete', f=self.delete),
|
||||||
rv += [
|
MenuItem('Export', f=self.export),
|
||||||
MenuItem('Edit', f=self.edit),
|
|
||||||
MenuItem('Delete', f=self.delete),
|
|
||||||
MenuItem('Export', f=self.export),
|
|
||||||
]
|
|
||||||
|
|
||||||
rv += [
|
|
||||||
self.sign_misc_menu_item(),
|
self.sign_misc_menu_item(),
|
||||||
ShortcutItem(KEY_QR, f=self.view_qr_menu, arg="misc"),
|
ShortcutItem(KEY_QR, f=self.view_qr_menu, arg="misc"),
|
||||||
ShortcutItem(KEY_NFC, f=self.share_nfc, arg='misc'),
|
ShortcutItem(KEY_NFC, f=self.share_nfc, arg='misc'),
|
||||||
]
|
]
|
||||||
|
|
||||||
# if misc is less than MAX_PASS_LEN and only consist of printable ASCII characters
|
|
||||||
# and current seed (master or tmp) is word based - offer to apply note text as BIP-39 passphrase
|
|
||||||
if self.is_b39pass_applicable(self.misc, readonly):
|
|
||||||
rv += [MenuItem('Apply as BIP-39 Passphrase',
|
|
||||||
f=self.apply_as_b39_pass, arg=(self.misc, readonly))]
|
|
||||||
|
|
||||||
return rv
|
|
||||||
|
|
||||||
async def make_menu(self, a, b, item):
|
|
||||||
items = await self._make_menu(readonly=item.arg)
|
|
||||||
return MenuSystem(items)
|
|
||||||
|
|
||||||
async def view(self, *a):
|
async def view(self, *a):
|
||||||
ch = await ux_show_story(self.misc, title=self.title, escape=KEY_QR,
|
ch = await ux_show_story(self.misc, title=self.title, escape=KEY_QR,
|
||||||
hint_icons=KEY_QR)
|
hint_icons=KEY_QR)
|
||||||
@ -720,8 +509,6 @@ class NoteContent(NoteContentBase):
|
|||||||
if misc is None:
|
if misc is None:
|
||||||
misc = self.misc
|
misc = self.misc
|
||||||
|
|
||||||
group = await GroupPickerMenu.pick(self.group)
|
|
||||||
|
|
||||||
if self.idx != -1:
|
if self.idx != -1:
|
||||||
# confirm changes, don't for new records
|
# confirm changes, don't for new records
|
||||||
chgs = []
|
chgs = []
|
||||||
@ -729,8 +516,6 @@ class NoteContent(NoteContentBase):
|
|||||||
chgs.append('Title')
|
chgs.append('Title')
|
||||||
if self.misc != misc:
|
if self.misc != misc:
|
||||||
chgs.append('Note Text')
|
chgs.append('Note Text')
|
||||||
if self.group != group:
|
|
||||||
chgs.append('Group')
|
|
||||||
|
|
||||||
if not chgs:
|
if not chgs:
|
||||||
await ux_dramatic_pause('No changes.', 3)
|
await ux_dramatic_pause('No changes.', 3)
|
||||||
@ -743,7 +528,6 @@ class NoteContent(NoteContentBase):
|
|||||||
|
|
||||||
self.title = title
|
self.title = title
|
||||||
self.misc = misc
|
self.misc = misc
|
||||||
self.group = group
|
|
||||||
|
|
||||||
await self._save_ux(menu)
|
await self._save_ux(menu)
|
||||||
|
|
||||||
@ -790,7 +574,7 @@ async def start_export(notes):
|
|||||||
await needs_microsd()
|
await needs_microsd()
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await ux_show_story('Failed to write!\n\n'+str(e))
|
await ux_show_story('Failed to write!\n\n\n'+str(e))
|
||||||
return
|
return
|
||||||
|
|
||||||
msg = 'Export file written:\n\n%s\n\nSignature file written:\n\n%s' % (
|
msg = 'Export file written:\n\n%s\n\nSignature file written:\n\n%s' % (
|
||||||
@ -832,8 +616,7 @@ async def import_from_other(menu, *a):
|
|||||||
records = json.load(open(fn, 'rt'))
|
records = json.load(open(fn, 'rt'))
|
||||||
|
|
||||||
# We have some JSON, parsed now.
|
# We have some JSON, parsed now.
|
||||||
ok = await import_from_json(records)
|
await import_from_json(records)
|
||||||
if not ok: return
|
|
||||||
|
|
||||||
await ux_dramatic_pause('Saved.', 3)
|
await ux_dramatic_pause('Saved.', 3)
|
||||||
menu.update_contents()
|
menu.update_contents()
|
||||||
@ -852,7 +635,6 @@ async def import_from_json(records):
|
|||||||
settings.set('notes', was)
|
settings.set('notes', was)
|
||||||
settings.set('secnap', True)
|
settings.set('secnap', True)
|
||||||
settings.save()
|
settings.save()
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await ux_show_story(title="Failure", msg=str(e) + '\n\n' + problem_file_line(e))
|
await ux_show_story(title="Failure", msg=str(e) + '\n\n' + problem_file_line(e))
|
||||||
|
|||||||
@ -67,9 +67,6 @@ from utils import call_later_ms
|
|||||||
# msas = multisig address show (do not censor multisig addresses)
|
# msas = multisig address show (do not censor multisig addresses)
|
||||||
# ccc = (complex) If present, CCC feature is enabled and key details stored here.
|
# ccc = (complex) If present, CCC feature is enabled and key details stored here.
|
||||||
# ktrx = (privkey) Key teleport Rx has been started, this will be our keypair
|
# ktrx = (privkey) Key teleport Rx has been started, this will be our keypair
|
||||||
# sssp = (complex) If present, a (single signer) spending-policy is defined (maybe disabled)
|
|
||||||
# lfr = (string) If present, the reason why Spending Policy blocked last transaction
|
|
||||||
# wifs = (list) List of tuples (public/private key)
|
|
||||||
|
|
||||||
# Stored w/ key=00 for access before login
|
# Stored w/ key=00 for access before login
|
||||||
# _skip_pin = hard code a PIN value (dangerous, only for debug)
|
# _skip_pin = hard code a PIN value (dangerous, only for debug)
|
||||||
@ -93,9 +90,7 @@ KEEP_IF_BLANK_SETTINGS = ["wa", "sighshchk", "emu", "rz", "b39skip",
|
|||||||
"axskip", "del", "pms", "idle_to", "batt_to",
|
"axskip", "del", "pms", "idle_to", "batt_to",
|
||||||
"bright", "msas"]
|
"bright", "msas"]
|
||||||
|
|
||||||
# key value pairs saved directly to master seed settings
|
SEEDVAULT_FIELDS = ['seeds', 'seedvault', 'xfp', 'words', "bkpw"]
|
||||||
# held in RAM for tmp seed sessions
|
|
||||||
MASTER_FIELDS = ['seeds', 'seedvault', 'xfp', 'words', "bkpw", "sssp"]
|
|
||||||
|
|
||||||
NUM_SLOTS = const(100)
|
NUM_SLOTS = const(100)
|
||||||
SLOTS = range(NUM_SLOTS)
|
SLOTS = range(NUM_SLOTS)
|
||||||
@ -289,7 +284,7 @@ class SettingsObject:
|
|||||||
|
|
||||||
SettingsObject.master_nvram_key = self.nvram_key
|
SettingsObject.master_nvram_key = self.nvram_key
|
||||||
|
|
||||||
for fn in MASTER_FIELDS:
|
for fn in SEEDVAULT_FIELDS:
|
||||||
curr = self.current.get(fn, None)
|
curr = self.current.get(fn, None)
|
||||||
if curr is not None:
|
if curr is not None:
|
||||||
SettingsObject.master_sv_data[fn] = curr
|
SettingsObject.master_sv_data[fn] = curr
|
||||||
@ -305,7 +300,7 @@ class SettingsObject:
|
|||||||
SettingsObject.master_sv_data.clear()
|
SettingsObject.master_sv_data.clear()
|
||||||
SettingsObject.master_nvram_key = None
|
SettingsObject.master_nvram_key = None
|
||||||
|
|
||||||
def master_set(self, key, value, master_only=False):
|
def master_set(self, key, value):
|
||||||
# Set a value, and it must be saved under the master seed's
|
# Set a value, and it must be saved under the master seed's
|
||||||
# Concern is we may be changing a setting from a tmp seed mode
|
# Concern is we may be changing a setting from a tmp seed mode
|
||||||
# - always does a save
|
# - always does a save
|
||||||
@ -316,7 +311,6 @@ class SettingsObject:
|
|||||||
self.set(key, value)
|
self.set(key, value)
|
||||||
self.save()
|
self.save()
|
||||||
else:
|
else:
|
||||||
assert not master_only
|
|
||||||
# harder, slower: have to load, change and write
|
# harder, slower: have to load, change and write
|
||||||
master = SettingsObject(nvram_key=SettingsObject.master_nvram_key)
|
master = SettingsObject(nvram_key=SettingsObject.master_nvram_key)
|
||||||
master.load()
|
master.load()
|
||||||
@ -325,7 +319,7 @@ class SettingsObject:
|
|||||||
del master
|
del master
|
||||||
|
|
||||||
# track our copies
|
# track our copies
|
||||||
if key in MASTER_FIELDS:
|
if key in SEEDVAULT_FIELDS:
|
||||||
SettingsObject.master_sv_data[key] = value
|
SettingsObject.master_sv_data[key] = value
|
||||||
|
|
||||||
def master_get(self, kn, default=None):
|
def master_get(self, kn, default=None):
|
||||||
@ -337,7 +331,7 @@ class SettingsObject:
|
|||||||
return self.get(kn, default)
|
return self.get(kn, default)
|
||||||
|
|
||||||
# LIMITATION: only supporting a few values we know we will need
|
# LIMITATION: only supporting a few values we know we will need
|
||||||
assert kn in MASTER_FIELDS
|
assert kn in SEEDVAULT_FIELDS
|
||||||
res = SettingsObject.master_sv_data.get(kn, default)
|
res = SettingsObject.master_sv_data.get(kn, default)
|
||||||
if res is None:
|
if res is None:
|
||||||
return default
|
return default
|
||||||
|
|||||||
@ -2,18 +2,17 @@
|
|||||||
#
|
#
|
||||||
# ownership.py - store a cache of hashes related to addresses we might control.
|
# ownership.py - store a cache of hashes related to addresses we might control.
|
||||||
#
|
#
|
||||||
import os, chains, ngu, struct, version
|
import os, sys, chains, ngu, struct, version
|
||||||
from glob import settings
|
from glob import settings
|
||||||
from ucollections import namedtuple
|
from ucollections import namedtuple
|
||||||
from ubinascii import hexlify as b2a_hex
|
from ubinascii import hexlify as b2a_hex
|
||||||
from ubinascii import unhexlify as a2b_hex
|
|
||||||
from exceptions import UnknownAddressExplained
|
from exceptions import UnknownAddressExplained
|
||||||
from utils import problem_file_line, show_single_address, validate_own_address
|
from utils import problem_file_line, show_single_address
|
||||||
from public_constants import AFC_SCRIPT, AF_P2WPKH_P2SH, AF_P2SH, AF_P2WSH_P2SH, AF_P2TR, AF_P2WSH
|
|
||||||
|
|
||||||
# Track many addresses, but in compressed form
|
# Track many addresses, but in compressed form
|
||||||
# - map from random Bech32/Base58 payment address to (wallet) + keypath
|
# - map from random Bech32/Base58 payment address to (wallet) + keypath
|
||||||
# - won't consider any keypath that does not end in <0;1>/*
|
# - only normal (external, not change) addresses, and won't consider
|
||||||
|
# any keypath that does not end in 0/*
|
||||||
# - store only hints, since we can re-construct any address and want to fully verify
|
# - store only hints, since we can re-construct any address and want to fully verify
|
||||||
# - try to keep private between different duress wallets, and seed vaults
|
# - try to keep private between different duress wallets, and seed vaults
|
||||||
# - storing bulk data into LFS, not settings
|
# - storing bulk data into LFS, not settings
|
||||||
@ -40,7 +39,7 @@ OWNERSHIP_MAGIC = 0x10A0 # "Address Ownership" v1.0
|
|||||||
|
|
||||||
# target 3 flash blocks, max file size => 764 addresses
|
# target 3 flash blocks, max file size => 764 addresses
|
||||||
MAX_ADDRS_STORED = const(764) # =((3*512) - OWNERSHIP_FILE_HDR_LEN) // HASH_ENC_LEN
|
MAX_ADDRS_STORED = const(764) # =((3*512) - OWNERSHIP_FILE_HDR_LEN) // HASH_ENC_LEN
|
||||||
BONUS_AFTER_MATCH = const(20) # number of addresses to still generate after match found
|
BONUS_GAP_LIMIT = const(20)
|
||||||
|
|
||||||
def encode_addr(addr, salt):
|
def encode_addr(addr, salt):
|
||||||
# Convert text address to something we can store while preserving privacy.
|
# Convert text address to something we can store while preserving privacy.
|
||||||
@ -57,7 +56,6 @@ class AddressCacheFile:
|
|||||||
self.salt = h[32:]
|
self.salt = h[32:]
|
||||||
self.count = 0
|
self.count = 0
|
||||||
self.hdr = None
|
self.hdr = None
|
||||||
self.fd = None
|
|
||||||
|
|
||||||
self.peek()
|
self.peek()
|
||||||
|
|
||||||
@ -67,6 +65,9 @@ class AddressCacheFile:
|
|||||||
rv += ' (change)'
|
rv += ' (change)'
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
|
def exists(self):
|
||||||
|
return bool(self.count)
|
||||||
|
|
||||||
def peek(self):
|
def peek(self):
|
||||||
# see what we have on-disk; just reads header.
|
# see what we have on-disk; just reads header.
|
||||||
try:
|
try:
|
||||||
@ -104,13 +105,14 @@ class AddressCacheFile:
|
|||||||
self.fd.write(hdr)
|
self.fd.write(hdr)
|
||||||
|
|
||||||
def append(self, addr):
|
def append(self, addr):
|
||||||
self.fd.write(encode_addr(addr, self.salt))
|
if addr is None:
|
||||||
|
# close file, done
|
||||||
def close(self):
|
|
||||||
# close file, done
|
|
||||||
if self.fd is not None:
|
|
||||||
self.fd.close()
|
self.fd.close()
|
||||||
self.fd = None
|
del self.fd
|
||||||
|
return
|
||||||
|
|
||||||
|
assert '_' not in addr
|
||||||
|
self.fd.write(encode_addr(addr, self.salt))
|
||||||
|
|
||||||
def fast_search(self, addr):
|
def fast_search(self, addr):
|
||||||
# Do the easy part of the searching, using the existing file's contents.
|
# Do the easy part of the searching, using the existing file's contents.
|
||||||
@ -119,7 +121,6 @@ class AddressCacheFile:
|
|||||||
from glob import dis
|
from glob import dis
|
||||||
|
|
||||||
if not self.hdr or not self.count:
|
if not self.hdr or not self.count:
|
||||||
# cache empty
|
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(self.fname, 'rb') as fd:
|
with open(self.fname, 'rb') as fd:
|
||||||
@ -131,7 +132,7 @@ class AddressCacheFile:
|
|||||||
chk = encode_addr(addr, self.salt)
|
chk = encode_addr(addr, self.salt)
|
||||||
for idx in range(self.count):
|
for idx in range(self.count):
|
||||||
if buf[idx*HASH_ENC_LEN : (idx*HASH_ENC_LEN)+HASH_ENC_LEN] == chk:
|
if buf[idx*HASH_ENC_LEN : (idx*HASH_ENC_LEN)+HASH_ENC_LEN] == chk:
|
||||||
yield self.change_idx, idx
|
yield (self.change_idx, idx)
|
||||||
|
|
||||||
dis.progress_sofar(idx, self.count)
|
dis.progress_sofar(idx, self.count)
|
||||||
|
|
||||||
@ -147,106 +148,92 @@ class AddressCacheFile:
|
|||||||
# - return subpath for a hit or None
|
# - return subpath for a hit or None
|
||||||
from glob import dis
|
from glob import dis
|
||||||
|
|
||||||
|
bonus = 0
|
||||||
match = None
|
match = None
|
||||||
|
|
||||||
start_idx = self.count
|
start_idx = self.count
|
||||||
count = MAX_ADDRS_STORED - start_idx
|
count = MAX_ADDRS_STORED - start_idx
|
||||||
|
|
||||||
if count <= 0:
|
if count <= 0:
|
||||||
return match
|
return None
|
||||||
|
|
||||||
self.setup(self.change_idx, start_idx)
|
self.setup(self.change_idx, start_idx)
|
||||||
|
|
||||||
bonus = None
|
|
||||||
for idx,here,*_ in self.wallet.yield_addresses(start_idx, count,
|
for idx,here,*_ in self.wallet.yield_addresses(start_idx, count,
|
||||||
change_idx=self.change_idx):
|
change_idx=self.change_idx):
|
||||||
self.append(here)
|
|
||||||
self.count += 1
|
|
||||||
|
|
||||||
if bonus:
|
|
||||||
if bonus >= BONUS_AFTER_MATCH:
|
|
||||||
# do (at most) 20 more - limited by 'start_idx' & 'count'
|
|
||||||
break
|
|
||||||
bonus += 1
|
|
||||||
|
|
||||||
|
|
||||||
if here == addr:
|
if here == addr:
|
||||||
# match but keep going
|
# Found it! But keep going a little for next time.
|
||||||
match = (self.change_idx, idx)
|
match = (self.change_idx, idx)
|
||||||
bonus = 1
|
|
||||||
|
|
||||||
dis.progress_sofar(idx - start_idx, count)
|
self.append(here)
|
||||||
|
self.count += 1
|
||||||
|
if match:
|
||||||
|
bonus += 1
|
||||||
|
|
||||||
self.close()
|
if match and bonus >= BONUS_GAP_LIMIT:
|
||||||
return match
|
self.append(None)
|
||||||
|
return match
|
||||||
|
|
||||||
|
dis.progress_sofar(idx-start_idx, count)
|
||||||
|
|
||||||
|
self.append(None)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
class OwnershipCache:
|
class OwnershipCache:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def saver(cls, wallet, change_idx, start_idx, count):
|
def saver(cls, wallet, change_idx, start_idx):
|
||||||
# when we are generating many addresses for export, capture them (if suitable)
|
# when we are generating many addresses for export, capture them
|
||||||
# as we go with this function
|
# as we go with this function
|
||||||
if not count:
|
# - not change -- only main addrs
|
||||||
return
|
|
||||||
if change_idx not in (0, 1):
|
|
||||||
return
|
|
||||||
if start_idx >= MAX_ADDRS_STORED:
|
|
||||||
return
|
|
||||||
|
|
||||||
file = AddressCacheFile(wallet, change_idx)
|
file = AddressCacheFile(wallet, change_idx)
|
||||||
current_pos = file.count
|
|
||||||
|
|
||||||
if start_idx > current_pos:
|
if file.exists():
|
||||||
# nothing to do here, we are missing some addresses in the middle
|
# don't save to existing file, has some already
|
||||||
return
|
return None
|
||||||
if (start_idx + count) <= current_pos:
|
|
||||||
# we already have all these addresses
|
|
||||||
return
|
|
||||||
|
|
||||||
file.setup(change_idx, current_pos)
|
try:
|
||||||
|
file.setup(change_idx, start_idx)
|
||||||
|
except:
|
||||||
|
# in some cases we don't want to save anything, not an error
|
||||||
|
return None
|
||||||
|
|
||||||
def doit(addr, idx):
|
return file.append
|
||||||
if addr is None:
|
|
||||||
file.close()
|
|
||||||
elif (idx < MAX_ADDRS_STORED) and idx >= current_pos:
|
|
||||||
file.append(addr)
|
|
||||||
|
|
||||||
return doit
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def filter(cls, addr_fmt, args):
|
def search(cls, addr):
|
||||||
# Filter possible candidates!
|
# Find it!
|
||||||
|
# - returns wallet object, and tuple2 of final 2 subpath components
|
||||||
# - if you start w/ testnet, we'll follow that
|
# - if you start w/ testnet, we'll follow that
|
||||||
from multisig import MultisigWallet
|
from multisig import MultisigWallet
|
||||||
|
from public_constants import AFC_SCRIPT, AF_P2WPKH_P2SH, AF_P2SH, AF_P2WSH_P2SH
|
||||||
|
from glob import dis
|
||||||
|
|
||||||
args = args or {}
|
ch = chains.current_chain()
|
||||||
|
|
||||||
# user has specified specific (named) wallet
|
addr_fmt = ch.possible_address_fmt(addr)
|
||||||
named_wal = args.get("wallet", None)
|
if not addr_fmt:
|
||||||
if named_wal:
|
# might be valid address over on testnet vs mainnet
|
||||||
# quick search without deserialization
|
raise UnknownAddressExplained('That address is not valid on ' + ch.name)
|
||||||
res = list(MultisigWallet.iter_wallets(name=named_wal))
|
|
||||||
if not res:
|
|
||||||
raise UnknownAddressExplained("Wallet '%s' not defined." % named_wal)
|
|
||||||
|
|
||||||
# only return desired named wallet, no other wallets are searched
|
|
||||||
return res
|
|
||||||
|
|
||||||
possibles = []
|
possibles = []
|
||||||
|
|
||||||
if addr_fmt & AFC_SCRIPT:
|
if addr_fmt & AFC_SCRIPT:
|
||||||
# multisig or script at least... must exist already
|
# multisig or script at least.. must exist already
|
||||||
afs = [addr_fmt]
|
possibles.extend(MultisigWallet.iter_wallets(addr_fmt=addr_fmt))
|
||||||
|
|
||||||
if addr_fmt == AF_P2SH:
|
if addr_fmt == AF_P2SH:
|
||||||
# might look like P2SH but actually be AF_P2WSH_P2SH
|
# might look like P2SH but actually be AF_P2WSH_P2SH
|
||||||
# wrapped segwit is more used than legacy
|
possibles.extend(MultisigWallet.iter_wallets(addr_fmt=AF_P2WSH_P2SH))
|
||||||
afs = [AF_P2WSH_P2SH, AF_P2SH]
|
|
||||||
|
|
||||||
# Might be single-sig p2wpkh wrapped in p2sh ... but that was a transition
|
# Might be single-sig p2wpkh wrapped in p2sh ... but that was a transition
|
||||||
# thing that hopefully is going away, so if they have any multisig wallets,
|
# thing that hopefully is going away, so if they have any multisig wallets,
|
||||||
# defined, assume that that's the only p2sh address source.
|
# defined, assume that that's the only p2sh address source.
|
||||||
addr_fmt = AF_P2WPKH_P2SH
|
addr_fmt = AF_P2WPKH_P2SH
|
||||||
|
|
||||||
possibles.extend(MultisigWallet.iter_wallets(addr_fmts=afs))
|
# TODO: add tapscript and such fancy stuff here
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Construct possible single-signer wallets, always at least account=0 case
|
# Construct possible single-signer wallets, always at least account=0 case
|
||||||
@ -260,97 +247,63 @@ class OwnershipCache:
|
|||||||
if af == addr_fmt and acct_num:
|
if af == addr_fmt and acct_num:
|
||||||
w = MasterSingleSigWallet(addr_fmt, account_idx=acct_num)
|
w = MasterSingleSigWallet(addr_fmt, account_idx=acct_num)
|
||||||
possibles.append(w)
|
possibles.append(w)
|
||||||
except (KeyError, ValueError):
|
except (KeyError, ValueError): pass # if not single sig address format
|
||||||
pass # if not single sig address format
|
|
||||||
|
|
||||||
if not possibles:
|
if not possibles:
|
||||||
# can only happen w/ scripts; for single-signer we have things to check
|
# can only happen w/ scripts; for single-signer we have things to check
|
||||||
raise UnknownAddressExplained(
|
raise UnknownAddressExplained(
|
||||||
"No suitable multisig wallets are currently defined.")
|
"No suitable multisig wallets are currently defined.")
|
||||||
|
|
||||||
# ordering here
|
|
||||||
return possibles
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def search_wallet_cache(cls, addr, cf):
|
|
||||||
# - returns wallet object, and tuple2 of final 2 subpath components
|
|
||||||
# "quick" check first, before doing any generations
|
# "quick" check first, before doing any generations
|
||||||
# external chain first, then internal (change)
|
|
||||||
for maybe in cf.fast_search(addr):
|
|
||||||
ok = cf.check_match(addr, maybe)
|
|
||||||
if ok:
|
|
||||||
return cf.wallet, maybe
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
phase2 = []
|
||||||
|
for change_idx in (0, 1):
|
||||||
|
files = [AddressCacheFile(w, change_idx) for w in possibles]
|
||||||
|
for f in files:
|
||||||
|
if dis.has_lcd:
|
||||||
|
dis.fullscreen('Searching wallet(s)...', line2=f.nice_name())
|
||||||
|
else:
|
||||||
|
dis.fullscreen('Searching...')
|
||||||
|
|
||||||
|
for maybe in f.fast_search(addr):
|
||||||
|
ok = f.check_match(addr, maybe)
|
||||||
|
if not ok: continue # false positive - will happen
|
||||||
|
|
||||||
|
# found winner.
|
||||||
|
return f.wallet, maybe
|
||||||
|
|
||||||
|
if f.count < MAX_ADDRS_STORED:
|
||||||
|
phase2.append(f)
|
||||||
|
|
||||||
|
count += f.count
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def search_build_wallet(cls, addr, cf):
|
|
||||||
# maybe we haven't calculated all the addresses yet, so do that
|
# maybe we haven't calculated all the addresses yet, so do that
|
||||||
# - very slow, but only needed once; any negative (failed) search causes this
|
# - very slow, but only needed once; any negative (failed) search causes this
|
||||||
# - could stop when match found, but we go a bit beyond that for next time
|
# - could stop when match found, but we go a bit beyond that for next time
|
||||||
# - we could search all in parallel, rather than serially because
|
# - we could search all in parallel, rather than serially because
|
||||||
# more likely to find a match with low index... but seen as too much memory
|
# more likely to find a match with low index... but seen as too much memory
|
||||||
result = cf.build_and_search(addr)
|
|
||||||
if result:
|
for f in phase2:
|
||||||
# found it, so report it and stop
|
b4 = f.count
|
||||||
return cf.wallet, result
|
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
|
||||||
|
|
||||||
# possible phase 3: other seedvault... slow, rare and not implemented
|
# possible phase 3: other seedvault... slow, rare and not implemented
|
||||||
return None, None
|
|
||||||
|
raise UnknownAddressExplained('Searched %d candidates without finding a match.' % count)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def search(cls, addr, args=None):
|
async def search_ux(cls, addr):
|
||||||
from glob import dis
|
|
||||||
|
|
||||||
dis.fullscreen("Wait...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
addr, addr_fmt = validate_own_address(addr)
|
|
||||||
except Exception as e:
|
|
||||||
raise UnknownAddressExplained('That address is not valid on ' + e.args[0])
|
|
||||||
|
|
||||||
matches = OWNERSHIP.filter(addr_fmt, args)
|
|
||||||
|
|
||||||
# build cache files for both external & internal chain
|
|
||||||
cachefs = []
|
|
||||||
for w in matches:
|
|
||||||
cachefs.append(AddressCacheFile(w, 0))
|
|
||||||
cachefs.append(AddressCacheFile(w, 1))
|
|
||||||
|
|
||||||
for cf in cachefs:
|
|
||||||
msg = "Searching wallet(s)..." if dis.has_lcd else "Searching..."
|
|
||||||
dis.fullscreen(msg, line2=cf.nice_name())
|
|
||||||
wallet, subpath = OWNERSHIP.search_wallet_cache(addr, cf)
|
|
||||||
if wallet:
|
|
||||||
# first arg from_cache=True
|
|
||||||
return True, wallet, subpath
|
|
||||||
|
|
||||||
# nothing found in existing cache files
|
|
||||||
c = 0
|
|
||||||
for cf in cachefs:
|
|
||||||
msg = "Generating addresses..." if dis.has_lcd else "Generating..."
|
|
||||||
dis.fullscreen(msg, line2=cf.nice_name())
|
|
||||||
wallet, subpath = OWNERSHIP.search_build_wallet(addr, cf)
|
|
||||||
c += cf.count
|
|
||||||
if wallet:
|
|
||||||
# first arg from_cache=False
|
|
||||||
return False, wallet, subpath
|
|
||||||
|
|
||||||
# nothing found among singlesig & registered multisig wallets
|
|
||||||
# check WIF store (single sig only)
|
|
||||||
if addr_fmt not in [AF_P2TR, AF_P2WSH]:
|
|
||||||
dis.fullscreen("WIF Store...")
|
|
||||||
from wif import iter_wif_store_addresses
|
|
||||||
target_af = AF_P2WPKH_P2SH if addr_fmt == AF_P2SH else addr_fmt
|
|
||||||
for i, store_addr in iter_wif_store_addresses(target_af):
|
|
||||||
if store_addr == addr:
|
|
||||||
return False, ("wif", target_af), i+1
|
|
||||||
|
|
||||||
raise UnknownAddressExplained('Searched %d candidate addresses in %d wallet(s)'
|
|
||||||
' without finding a match.' % (c, len(matches)))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def search_ux(cls, addr, args):
|
|
||||||
# Provide a simple UX. Called functions do fullscreen, progress bar stuff.
|
# Provide a simple UX. Called functions do fullscreen, progress bar stuff.
|
||||||
from ux import ux_show_story, show_qr_code
|
from ux import ux_show_story, show_qr_code
|
||||||
from charcodes import KEY_QR
|
from charcodes import KEY_QR
|
||||||
@ -358,28 +311,25 @@ class OwnershipCache:
|
|||||||
from public_constants import AFC_BECH32, AFC_BECH32M
|
from public_constants import AFC_BECH32, AFC_BECH32M
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_, wallet, subpath = cls.search(addr, args)
|
wallet, subpath = OWNERSHIP.search(addr)
|
||||||
is_ms = isinstance(wallet, MultisigWallet)
|
is_ms = isinstance(wallet, MultisigWallet)
|
||||||
|
sp = wallet.render_path(*subpath)
|
||||||
|
|
||||||
msg = show_single_address(addr)
|
msg = show_single_address(addr)
|
||||||
esc = ""
|
msg += '\n\nFound in wallet:\n ' + wallet.name
|
||||||
if isinstance(wallet, tuple) and (wallet[0] == "wif"):
|
msg += '\nDerivation path:\n ' + sp
|
||||||
msg += '\n\nFound in WIF store at index %d' % subpath
|
if is_ms:
|
||||||
addr_fmt = wallet[1]
|
esc = ""
|
||||||
else:
|
else:
|
||||||
sp = wallet.render_path(*subpath)
|
esc = "0"
|
||||||
msg += '\n\nFound in wallet:\n ' + wallet.name
|
msg += "\n\nPress (0) to sign message with this key."
|
||||||
msg += '\nDerivation path:\n ' + sp
|
|
||||||
addr_fmt = wallet.addr_fmt
|
|
||||||
if not is_ms:
|
|
||||||
esc = "0"
|
|
||||||
msg += "\n\nPress (0) to sign message with this key."
|
|
||||||
|
|
||||||
title = "Verified"
|
title = "Verified"
|
||||||
if version.has_qwerty:
|
if version.has_qwerty:
|
||||||
esc += KEY_QR
|
esc += KEY_QR
|
||||||
title += " Address"
|
title += " Address"
|
||||||
else:
|
else:
|
||||||
msg += ' Press (1) for address QR.'
|
msg += ' (1) for address QR'
|
||||||
esc += '1'
|
esc += '1'
|
||||||
title += "!"
|
title += "!"
|
||||||
|
|
||||||
@ -387,10 +337,10 @@ class OwnershipCache:
|
|||||||
ch = await ux_show_story(msg, title=title, escape=esc, hint_icons=KEY_QR)
|
ch = await ux_show_story(msg, title=title, escape=esc, hint_icons=KEY_QR)
|
||||||
if ch in ("1"+KEY_QR):
|
if ch in ("1"+KEY_QR):
|
||||||
await show_qr_code(addr, msg=addr, is_addrs=True,
|
await show_qr_code(addr, msg=addr, is_addrs=True,
|
||||||
is_alnum=(addr_fmt & (AFC_BECH32 | AFC_BECH32M)))
|
is_alnum=(wallet.addr_fmt & (AFC_BECH32 | AFC_BECH32M)))
|
||||||
elif not is_ms and (ch == "0"): # only singlesig
|
elif not is_ms and (ch == "0"): # only singlesig
|
||||||
from msgsign import sign_with_own_address
|
from msgsign import sign_with_own_address
|
||||||
await sign_with_own_address(sp, addr_fmt)
|
await sign_with_own_address(sp, wallet.addr_fmt)
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@ -179,7 +179,7 @@ class PaperWalletMaker:
|
|||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
from utils import problem_file_line
|
from utils import problem_file_line
|
||||||
await ux_show_story('Failed to write!\n\n'+problem_file_line(e))
|
await ux_show_story('Failed to write!\n\n\n'+problem_file_line(e))
|
||||||
return
|
return
|
||||||
|
|
||||||
story = "Done! Created file(s):\n\n%s" % nice_txt
|
story = "Done! Created file(s):\n\n%s" % nice_txt
|
||||||
|
|||||||
@ -3,7 +3,8 @@
|
|||||||
# pincodes.py - manage PIN code (which map to wallet seeds)
|
# pincodes.py - manage PIN code (which map to wallet seeds)
|
||||||
#
|
#
|
||||||
import ustruct, ckcc, version, chains, stash
|
import ustruct, ckcc, version, chains, stash
|
||||||
from callgate import enter_dfu, get_is_bricked
|
# from ubinascii import hexlify as b2a_hex
|
||||||
|
from callgate import enter_dfu
|
||||||
from bip39 import wordlist_en
|
from bip39 import wordlist_en
|
||||||
|
|
||||||
# See ../stm32/bootloader/pins.h for source of these constants.
|
# See ../stm32/bootloader/pins.h for source of these constants.
|
||||||
@ -126,14 +127,17 @@ class PinAttempt:
|
|||||||
self.private_state = 0 # opaque data, but preserve
|
self.private_state = 0 # opaque data, but preserve
|
||||||
self.cached_main_pin = bytearray(32)
|
self.cached_main_pin = bytearray(32)
|
||||||
|
|
||||||
# If set, a spending policy is in effect, and so even tho we know the master
|
|
||||||
# seed, we are not going to let them see it, nor sign things we dont like, etc.
|
|
||||||
self.hobbled_mode = False
|
|
||||||
|
|
||||||
#assert MAX_PIN_LEN == 32 # update FMT otherwise
|
assert MAX_PIN_LEN == 32 # update FMT otherwise
|
||||||
#assert ustruct.calcsize(PIN_ATTEMPT_FMT_V1) == PIN_ATTEMPT_SIZE_V1
|
assert ustruct.calcsize(PIN_ATTEMPT_FMT_V1) == PIN_ATTEMPT_SIZE_V1
|
||||||
#assert ustruct.calcsize(PIN_ATTEMPT_FMT_V2_ADDITIONS) \
|
assert ustruct.calcsize(PIN_ATTEMPT_FMT_V2_ADDITIONS) == PIN_ATTEMPT_SIZE - PIN_ATTEMPT_SIZE_V1
|
||||||
# == PIN_ATTEMPT_SIZE - PIN_ATTEMPT_SIZE_V1
|
|
||||||
|
# check for bricked system early
|
||||||
|
import callgate
|
||||||
|
if callgate.get_is_bricked():
|
||||||
|
# die right away if it's not going to work
|
||||||
|
print("SE bricked")
|
||||||
|
callgate.enter_dfu(3)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<PinAttempt: fails/left=%d/%d tc_flag/arg=0x%x/0x%x>' % (
|
return '<PinAttempt: fails/left=%d/%d tc_flag/arg=0x%x/0x%x>' % (
|
||||||
@ -335,6 +339,10 @@ class PinAttempt:
|
|||||||
|
|
||||||
return self.state_flags
|
return self.state_flags
|
||||||
|
|
||||||
|
def delay(self):
|
||||||
|
# obsolete since Mk3, but called from login.py
|
||||||
|
self.roundtrip(1)
|
||||||
|
|
||||||
def login(self):
|
def login(self):
|
||||||
# test we have the PIN code right, and unlock access if so.
|
# test we have the PIN code right, and unlock access if so.
|
||||||
chk = self.roundtrip(2)
|
chk = self.roundtrip(2)
|
||||||
@ -525,25 +533,11 @@ class PinAttempt:
|
|||||||
from trick_pins import TC_DELTA_MODE
|
from trick_pins import TC_DELTA_MODE
|
||||||
return bool(self.delay_required & TC_DELTA_MODE)
|
return bool(self.delay_required & TC_DELTA_MODE)
|
||||||
|
|
||||||
|
|
||||||
def get_tc_values(self):
|
def get_tc_values(self):
|
||||||
# Mk4 only
|
# Mk4 only
|
||||||
# return (tc_flags, tc_arg)
|
# return (tc_flags, tc_arg)
|
||||||
return self.delay_required, self.delay_achieved
|
return self.delay_required, self.delay_achieved
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def enforce_brick():
|
|
||||||
# check for bricked system early
|
|
||||||
if get_is_bricked():
|
|
||||||
try:
|
|
||||||
# regardless of settings, become a forever calculator after brickage.
|
|
||||||
while version.has_qwerty:
|
|
||||||
from calc import login_repl
|
|
||||||
await login_repl()
|
|
||||||
finally:
|
|
||||||
# die right away if it's not going to work
|
|
||||||
enter_dfu(3)
|
|
||||||
|
|
||||||
|
|
||||||
# singleton
|
# singleton
|
||||||
pa = PinAttempt()
|
pa = PinAttempt()
|
||||||
|
|||||||
514
shared/psbt.py
514
shared/psbt.py
File diff suppressed because it is too large
Load Diff
@ -25,6 +25,10 @@ class PSRAMWrapper:
|
|||||||
|
|
||||||
return memoryview(self._wr)[offset:offset+ln]
|
return memoryview(self._wr)[offset:offset+ln]
|
||||||
|
|
||||||
|
def is_at(self, ptr, offset):
|
||||||
|
# is bytes() object really one we created at read_at
|
||||||
|
return uctypes.addressof(ptr) == self.base+offset
|
||||||
|
|
||||||
# Be compatible with SPIFlash class...
|
# Be compatible with SPIFlash class...
|
||||||
|
|
||||||
def read(self, address, buf, cmd=None):
|
def read(self, address, buf, cmd=None):
|
||||||
|
|||||||
@ -4,10 +4,10 @@
|
|||||||
#
|
#
|
||||||
import framebuf, uqr
|
import framebuf, uqr
|
||||||
from ux import UserInteraction, ux_wait_keyup, the_ux
|
from ux import UserInteraction, ux_wait_keyup, the_ux
|
||||||
from version import has_qwerty
|
|
||||||
from exceptions import QRTooBigError
|
|
||||||
from charcodes import (KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_NFC,
|
from charcodes import (KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_NFC,
|
||||||
KEY_END, KEY_ENTER, KEY_CANCEL)
|
KEY_END, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_ENTER, KEY_CANCEL)
|
||||||
|
from version import has_qwerty
|
||||||
|
|
||||||
|
|
||||||
# TODO: This class has a terrible API!
|
# TODO: This class has a terrible API!
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ class QRDisplaySingle(UserInteraction):
|
|||||||
|
|
||||||
def __init__(self, addrs, is_alnum, start_n=0, sidebar=None, msg=None,
|
def __init__(self, addrs, is_alnum, start_n=0, sidebar=None, msg=None,
|
||||||
is_addrs=False, force_msg=False, allow_nfc=True, is_secret=False,
|
is_addrs=False, force_msg=False, allow_nfc=True, is_secret=False,
|
||||||
change_idxs=None, can_raise=True, qr_msgs=None, no_index=None):
|
change_idxs=None):
|
||||||
self.is_alnum = is_alnum
|
self.is_alnum = is_alnum
|
||||||
self.idx = 0 # start with first address
|
self.idx = 0 # start with first address
|
||||||
self.invert = False # looks better, but neither mode is ideal
|
self.invert = False # looks better, but neither mode is ideal
|
||||||
@ -34,9 +34,6 @@ class QRDisplaySingle(UserInteraction):
|
|||||||
# only used for NFC sharing secret material - full chip wipe if is_secret=True
|
# only used for NFC sharing secret material - full chip wipe if is_secret=True
|
||||||
self.is_secret = is_secret
|
self.is_secret = is_secret
|
||||||
self.change_idxs = change_idxs or []
|
self.change_idxs = change_idxs or []
|
||||||
self.can_raise = can_raise
|
|
||||||
self.qr_msgs = qr_msgs
|
|
||||||
self.no_index = no_index
|
|
||||||
|
|
||||||
def calc_qr(self, msg):
|
def calc_qr(self, msg):
|
||||||
# Version 2 would be nice, but can't hold what we need, even at min error correction,
|
# Version 2 would be nice, but can't hold what we need, even at min error correction,
|
||||||
@ -65,20 +62,12 @@ class QRDisplaySingle(UserInteraction):
|
|||||||
# draw_qr_display takes this and renders hint in the top right corner
|
# draw_qr_display takes this and renders hint in the top right corner
|
||||||
# this member function decides what type of hint will be shown
|
# this member function decides what type of hint will be shown
|
||||||
# numbers, letters, etc.
|
# numbers, letters, etc.
|
||||||
if self.no_index:
|
|
||||||
return None
|
|
||||||
return str(self.start_n + self.idx) if len(self.addrs) > 1 else None
|
return str(self.start_n + self.idx) if len(self.addrs) > 1 else None
|
||||||
|
|
||||||
def side_msg(self):
|
def is_change(self):
|
||||||
if self.idx in self.change_idxs:
|
if self.idx in self.change_idxs:
|
||||||
return "CHANGE BACK"
|
return True
|
||||||
|
return False
|
||||||
elif self.qr_msgs:
|
|
||||||
try:
|
|
||||||
return self.qr_msgs[self.idx]
|
|
||||||
except IndexError: pass
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def redraw(self):
|
def redraw(self):
|
||||||
# Redraw screen.
|
# Redraw screen.
|
||||||
@ -87,15 +76,6 @@ class QRDisplaySingle(UserInteraction):
|
|||||||
|
|
||||||
# what we are showing inside the QR
|
# what we are showing inside the QR
|
||||||
body = self.addrs[self.idx]
|
body = self.addrs[self.idx]
|
||||||
idx_hint = self.idx_hint()
|
|
||||||
|
|
||||||
msg = None
|
|
||||||
if self.msg:
|
|
||||||
msg = self.msg
|
|
||||||
else:
|
|
||||||
if isinstance(body, str):
|
|
||||||
# sanity check
|
|
||||||
msg = body
|
|
||||||
|
|
||||||
# make the QR, if needed.
|
# make the QR, if needed.
|
||||||
if not self.qr_data:
|
if not self.qr_data:
|
||||||
@ -104,19 +84,23 @@ class QRDisplaySingle(UserInteraction):
|
|||||||
self.calc_qr(body)
|
self.calc_qr(body)
|
||||||
except Exception:
|
except Exception:
|
||||||
dis.busy_bar(False)
|
dis.busy_bar(False)
|
||||||
if not self.can_raise:
|
raise
|
||||||
dis.draw_qr_error(idx_hint, msg)
|
|
||||||
return
|
|
||||||
|
|
||||||
# other code paths require raise to switch to BBQr
|
|
||||||
raise QRTooBigError
|
|
||||||
|
|
||||||
# draw display
|
# draw display
|
||||||
dis.busy_bar(False)
|
dis.busy_bar(False)
|
||||||
|
|
||||||
|
if self.msg:
|
||||||
|
msg = self.msg
|
||||||
|
else:
|
||||||
|
msg = None
|
||||||
|
if isinstance(body, str):
|
||||||
|
# sanity check
|
||||||
|
msg = body
|
||||||
|
|
||||||
dis.draw_qr_display(self.qr_data, msg, self.is_alnum,
|
dis.draw_qr_display(self.qr_data, msg, self.is_alnum,
|
||||||
self.sidebar, idx_hint, self.invert,
|
self.sidebar, self.idx_hint(), self.invert,
|
||||||
is_addr=self.is_addrs, force_msg=self.force_msg,
|
is_addr=self.is_addrs, force_msg=self.force_msg,
|
||||||
side_msg=self.side_msg())
|
is_change=self.is_change())
|
||||||
|
|
||||||
async def interact_bare(self):
|
async def interact_bare(self):
|
||||||
from glob import NFC, dis
|
from glob import NFC, dis
|
||||||
|
|||||||
@ -57,8 +57,9 @@ SLOW_BAUD = const(9600)
|
|||||||
FAST_BAUD = const(57600)
|
FAST_BAUD = const(57600)
|
||||||
RX_BUF_SIZE = const(4350) # big enough for full v40 decoded
|
RX_BUF_SIZE = const(4350) # big enough for full v40 decoded
|
||||||
|
|
||||||
# TODO: constructor should avoid full setup until after login; after setup,
|
# TODO: constructor should leave it in reset for simple lower-power usage; then after
|
||||||
# command sleep is the known low-power state.
|
# login we can do full setup (2+ seconds) and then sleep again until needed.
|
||||||
|
|
||||||
class QRScanner:
|
class QRScanner:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -67,8 +68,6 @@ class QRScanner:
|
|||||||
self.scan_light = False # is light on during scanning?
|
self.scan_light = False # is light on during scanning?
|
||||||
self.version = None
|
self.version = None
|
||||||
self.setup_done = False
|
self.setup_done = False
|
||||||
self.needs_reinit = False
|
|
||||||
self.sleep_seq = 0
|
|
||||||
|
|
||||||
# hodl this lock when communicating w/ QR scanner
|
# hodl this lock when communicating w/ QR scanner
|
||||||
self.lock = asyncio.Lock()
|
self.lock = asyncio.Lock()
|
||||||
@ -85,21 +84,16 @@ class QRScanner:
|
|||||||
# setup hardware, reset scanner and return time to delay until ready
|
# setup hardware, reset scanner and return time to delay until ready
|
||||||
from machine import UART, Pin
|
from machine import UART, Pin
|
||||||
self.serial = UART(2, SLOW_BAUD, rxbuf=RX_BUF_SIZE)
|
self.serial = UART(2, SLOW_BAUD, rxbuf=RX_BUF_SIZE)
|
||||||
self.reset = Pin('QR_RESET', Pin.OUT_OD, value=1)
|
self.reset = Pin('QR_RESET', Pin.OUT_OD, value=0)
|
||||||
self.trigger = Pin('QR_TRIG', Pin.OUT_OD, value=1) # wasn't needed
|
self.trigger = Pin('QR_TRIG', Pin.OUT_OD, value=1) # wasn't needed
|
||||||
|
|
||||||
self.pulse_reset()
|
# NOTE: reset is active low (open drain)
|
||||||
|
|
||||||
# needs full 2 seconds of recovery time after reset
|
|
||||||
return 2
|
|
||||||
|
|
||||||
def pulse_reset(self):
|
|
||||||
# RESET is active low (open drain). Keep it as a pulse; module docs
|
|
||||||
# describe low on this pin as wake-up, so don't use it as parking state.
|
|
||||||
self.reset(0)
|
self.reset(0)
|
||||||
utime.sleep_ms(10)
|
utime.sleep_ms(10)
|
||||||
self.reset(1)
|
self.reset(1)
|
||||||
self.needs_reinit = False
|
|
||||||
|
# needs full 2 seconds of recovery time
|
||||||
|
return 2
|
||||||
|
|
||||||
def set_baud(self, br):
|
def set_baud(self, br):
|
||||||
# change serial port baud rate
|
# change serial port baud rate
|
||||||
@ -124,104 +118,56 @@ class QRScanner:
|
|||||||
|
|
||||||
async def setup_task(self, start_delay):
|
async def setup_task(self, start_delay):
|
||||||
# Task to setup device, and then die.
|
# Task to setup device, and then die.
|
||||||
|
await asyncio.sleep(start_delay)
|
||||||
|
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
for attempt in range(3):
|
|
||||||
await asyncio.sleep(start_delay)
|
|
||||||
|
|
||||||
try:
|
# might need to repeat a few time to get into right state
|
||||||
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
|
|
||||||
|
|
||||||
self.setup_done = True
|
|
||||||
await self.goto_sleep()
|
|
||||||
return
|
|
||||||
self.mark_needs_reinit()
|
|
||||||
|
|
||||||
def reset_stream(self):
|
|
||||||
self.sleep_seq += 1
|
|
||||||
start_delay = self.hardware_setup()
|
|
||||||
self.stream = asyncio.StreamReader(self.serial, {})
|
|
||||||
return start_delay
|
|
||||||
|
|
||||||
def mark_needs_reinit(self):
|
|
||||||
self.setup_done = False
|
|
||||||
self.version = None
|
|
||||||
self.needs_reinit = True
|
|
||||||
if hasattr(self, 'reset'):
|
|
||||||
self.reset(1)
|
|
||||||
|
|
||||||
async def blind_shutdown(self):
|
|
||||||
for baud in (SLOW_BAUD, FAST_BAUD):
|
|
||||||
self.set_baud(baud)
|
|
||||||
await self.tx('S_CMD_020D') # return to "Command mode"
|
|
||||||
await asyncio.sleep_ms(20)
|
|
||||||
await self.tx('S_CMD_03L0') # turn off bright light
|
|
||||||
await asyncio.sleep_ms(20)
|
|
||||||
await self.tx('SRDF0050') # sleep scanner
|
|
||||||
await asyncio.sleep_ms(150)
|
|
||||||
await self.tx('SRDF0050')
|
|
||||||
await asyncio.sleep_ms(20)
|
|
||||||
|
|
||||||
async def _configure(self):
|
|
||||||
# full config sequence; any step may raise on timeout/framing error
|
|
||||||
|
|
||||||
# might need to repeat a few time to get into right state
|
|
||||||
for retry in range(5):
|
|
||||||
baud = await self.probe_baud()
|
|
||||||
if baud: break
|
|
||||||
else:
|
|
||||||
#print("QR Scanner: missing")
|
|
||||||
raise RuntimeError('no contact')
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.txrx('S_CMD_FFFF') # factory reset of settings
|
|
||||||
except RuntimeError:
|
|
||||||
await asyncio.sleep_ms(1000)
|
|
||||||
for retry in range(5):
|
for retry in range(5):
|
||||||
baud = await self.probe_baud()
|
baud = await self.probe_baud()
|
||||||
if baud: break
|
if baud: break
|
||||||
else:
|
else:
|
||||||
raise RuntimeError('no contact after S_CMD_FFFF')
|
#print("QR Scanner: missing")
|
||||||
|
return
|
||||||
|
|
||||||
# go to high speed!
|
await self.txrx('S_CMD_FFFF') # factory reset of settings
|
||||||
if baud != FAST_BAUD:
|
|
||||||
await self.txrx('S_CMD_H3BR%d' % FAST_BAUD)
|
|
||||||
self.set_baud(FAST_BAUD)
|
|
||||||
|
|
||||||
# configure it like we want it
|
# go to high speed!
|
||||||
await self.txrx('S_CMD_MTRS5000') # 5s to read before fail (unused)
|
if baud != FAST_BAUD:
|
||||||
await self.txrx('S_CMD_MT11') # trigger is edge-based (not level)
|
await self.txrx('S_CMD_H3BR%d' % FAST_BAUD)
|
||||||
await self.txrx('S_CMD_MT30') # Same code reading without delay
|
self.set_baud(FAST_BAUD)
|
||||||
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
|
# configure it like we want it
|
||||||
await self.txrx('S_CMD_MARS0000') # "Modify the duration of single code reading" (ms)
|
await self.txrx('S_CMD_MTRS5000') # 5s to read before fail (unused)
|
||||||
await self.txrx('S_CMD_MARR000') # "Modify the time of the reading interval 0ms"
|
await self.txrx('S_CMD_MT11') # trigger is edge-based (not level)
|
||||||
await self.txrx('S_CMD_MA31') # Enable "Same code reading delay"
|
await self.txrx('S_CMD_MT30') # Same code reading without delay
|
||||||
await self.txrx('S_CMD_MARI0050') # "Modify the same code reading delay 50ms"
|
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
|
||||||
|
|
||||||
# these aren't useful (yet?) and just make things harder to decode.
|
# settings under continuous scan mode
|
||||||
#await self.txrx('S_CMD_05F1') # add all information on
|
await self.txrx('S_CMD_MARS0000') # "Modify the duration of single code reading" (ms)
|
||||||
#await self.txrx('S_CMD_05L1') # output decoding length info on
|
await self.txrx('S_CMD_MARR000') # "Modify the time of the reading interval 0ms"
|
||||||
#await self.txrx('S_CMD_05S1') # STX start char
|
await self.txrx('S_CMD_MA31') # Enable "Same code reading delay"
|
||||||
#await self.txrx('S_CMD_05C1') # CodeID+prefix
|
await self.txrx('S_CMD_MARI0050') # "Modify the same code reading delay 50ms"
|
||||||
#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
|
# these aren't useful (yet?) and just make things harder to decode.
|
||||||
await self.txrx('S_CMD_0000') # close setting codes
|
#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()
|
||||||
|
|
||||||
async def scan_once(self):
|
async def scan_once(self):
|
||||||
# Blocks until something is scanned. Returns it as string
|
# Blocks until something is scanned. Returns it as string
|
||||||
@ -230,16 +176,6 @@ class QRScanner:
|
|||||||
# - returns a BBQr object at that point
|
# - returns a BBQr object at that point
|
||||||
self.scan_light = False
|
self.scan_light = False
|
||||||
|
|
||||||
if self.needs_reinit:
|
|
||||||
try:
|
|
||||||
await self.setup_task(self.reset_stream())
|
|
||||||
if self.setup_done:
|
|
||||||
await asyncio.sleep_ms(200)
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
await self.blind_shutdown()
|
|
||||||
self.mark_needs_reinit()
|
|
||||||
return None
|
|
||||||
|
|
||||||
# wait for reset process to complete (can be an issue right after boot)
|
# wait for reset process to complete (can be an issue right after boot)
|
||||||
# - few seconds of boot time needed
|
# - few seconds of boot time needed
|
||||||
for retry in range(10):
|
for retry in range(10):
|
||||||
@ -275,22 +211,19 @@ class QRScanner:
|
|||||||
finally:
|
finally:
|
||||||
# Problem: another valid scan can come in just as we are trying
|
# Problem: another valid scan can come in just as we are trying
|
||||||
# to get out of scanner mode
|
# to get out of scanner mode
|
||||||
for retry in range(3):
|
for retry in range(10):
|
||||||
try:
|
try:
|
||||||
await self.txrx('S_CMD_020D', timeout=1000) # return to "Command mode"
|
await self.txrx('S_CMD_020D') # return to "Command mode"
|
||||||
await self.txrx('S_CMD_03L0', timeout=1000) # turn off bright light
|
await self.txrx('S_CMD_03L0') # turn off bright light
|
||||||
#print('rest after %d retries' % retry)
|
#print('rest after %d retries' % retry)
|
||||||
break
|
break
|
||||||
except Exception:
|
except: pass
|
||||||
pass
|
await asyncio.sleep_ms(25)
|
||||||
await asyncio.sleep_ms(50)
|
|
||||||
else:
|
else:
|
||||||
|
pass
|
||||||
#print('reset failed')
|
#print('reset failed')
|
||||||
await self.blind_shutdown()
|
|
||||||
self.mark_needs_reinit()
|
|
||||||
|
|
||||||
if self.setup_done:
|
await self.goto_sleep()
|
||||||
await self.goto_sleep()
|
|
||||||
self.busy_scanning = False
|
self.busy_scanning = False
|
||||||
|
|
||||||
# return BBQr object or string if simple QR
|
# return BBQr object or string if simple QR
|
||||||
@ -321,14 +254,13 @@ class QRScanner:
|
|||||||
# send specific command until it responds
|
# send specific command until it responds
|
||||||
# - it will wake on any command, but not instant
|
# - it will wake on any command, but not instant
|
||||||
# - first one seems to fail 100%
|
# - first one seems to fail 100%
|
||||||
self.sleep_seq += 1
|
|
||||||
await self.tx('SRDF0051') # blindly at first
|
await self.tx('SRDF0051') # blindly at first
|
||||||
|
|
||||||
for retry in range(5):
|
for retry in range(5):
|
||||||
try:
|
try:
|
||||||
await self.txrx('SRDF0051', timeout=50) # 50 ok, 20 too short
|
await self.txrx('SRDF0051', timeout=50) # 50 ok, 20 too short
|
||||||
return
|
return
|
||||||
except Exception:
|
except:
|
||||||
# first try usually fails, that's okay... its asleep and groggy
|
# first try usually fails, that's okay... its asleep and groggy
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -338,13 +270,9 @@ class QRScanner:
|
|||||||
# - need blind retries here
|
# - need blind retries here
|
||||||
# - might be two layers of sleep, and we need this second command after the first
|
# - might be two layers of sleep, and we need this second command after the first
|
||||||
# - helps to turn off the yellow LED, and save power as well
|
# - helps to turn off the yellow LED, and save power as well
|
||||||
self.sleep_seq += 1
|
|
||||||
sleep_seq = self.sleep_seq
|
|
||||||
await self.tx('SRDF0050')
|
await self.tx('SRDF0050')
|
||||||
async def later():
|
async def later():
|
||||||
await asyncio.sleep_ms(150)
|
await asyncio.sleep_ms(150)
|
||||||
if sleep_seq != self.sleep_seq or self.busy_scanning:
|
|
||||||
return
|
|
||||||
await self.tx('SRDF0050')
|
await self.tx('SRDF0050')
|
||||||
asyncio.create_task(later())
|
asyncio.create_task(later())
|
||||||
|
|
||||||
@ -362,22 +290,6 @@ class QRScanner:
|
|||||||
#print('tx >> ' + msg)
|
#print('tx >> ' + msg)
|
||||||
self.serial.write(msg)
|
self.serial.write(msg)
|
||||||
|
|
||||||
async def readexactly_timeout(self, num, timeout, msg=None):
|
|
||||||
# Avoid asyncio.wait_for_ms here: it can leave the scanner setup task
|
|
||||||
# stuck after a CancelledError. Convert scanner silence into a normal
|
|
||||||
# retryable command failure instead.
|
|
||||||
if timeout is None:
|
|
||||||
return await self.stream.readexactly(num)
|
|
||||||
|
|
||||||
start = utime.ticks_ms()
|
|
||||||
while self.stream.s.any() < num:
|
|
||||||
if utime.ticks_diff(utime.ticks_ms(), start) >= timeout:
|
|
||||||
#print("no rx after %s" % msg)
|
|
||||||
raise RuntimeError
|
|
||||||
await asyncio.sleep_ms(5)
|
|
||||||
|
|
||||||
return await self.stream.readexactly(num)
|
|
||||||
|
|
||||||
async def txrx(self, msg, timeout=250):
|
async def txrx(self, msg, timeout=250):
|
||||||
# Send a command, get the corresponding response.
|
# Send a command, get the corresponding response.
|
||||||
# - has a long timeout, collects rx based on framing
|
# - has a long timeout, collects rx based on framing
|
||||||
@ -398,8 +310,13 @@ class QRScanner:
|
|||||||
expect = LEN_OKAY
|
expect = LEN_OKAY
|
||||||
rx = b''
|
rx = b''
|
||||||
while 1:
|
while 1:
|
||||||
rx += await self.readexactly_timeout(expect, timeout, msg)
|
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
|
||||||
|
|
||||||
#print('txrx << ' + B2A(rx))
|
#print('txrx << ' + B2A(rx))
|
||||||
|
|
||||||
|
|||||||
129
shared/seed.py
129
shared/seed.py
@ -33,9 +33,6 @@ from ucollections import namedtuple
|
|||||||
# seed words lengths we support: 24=>256 bits, and recommended
|
# seed words lengths we support: 24=>256 bits, and recommended
|
||||||
VALID_LENGTHS = (24, 18, 12)
|
VALID_LENGTHS = (24, 18, 12)
|
||||||
|
|
||||||
# maximum length for BIP-39 passphrase
|
|
||||||
MAX_PASS_LEN = 100
|
|
||||||
|
|
||||||
# bit flag that means "also include bare prefix as a valid word"
|
# bit flag that means "also include bare prefix as a valid word"
|
||||||
_PREFIX_MARKER = const(1<<26)
|
_PREFIX_MARKER = const(1<<26)
|
||||||
|
|
||||||
@ -43,10 +40,6 @@ _PREFIX_MARKER = const(1<<26)
|
|||||||
# - 'encoded' is hex, and has is trimmed of right side zeros
|
# - 'encoded' is hex, and has is trimmed of right side zeros
|
||||||
VaultEntry = namedtuple('VaultEntry', 'xfp encoded label origin')
|
VaultEntry = namedtuple('VaultEntry', 'xfp encoded label origin')
|
||||||
|
|
||||||
def not_hobbled_mode():
|
|
||||||
# used as menu predicate and similar
|
|
||||||
return not pa.hobbled_mode
|
|
||||||
|
|
||||||
def seed_vault_iter():
|
def seed_vault_iter():
|
||||||
# iterate over all seeds in the vault; returns VaultEntry instances.
|
# iterate over all seeds in the vault; returns VaultEntry instances.
|
||||||
# raw vault entries are list type when json.loaded from flash
|
# raw vault entries are list type when json.loaded from flash
|
||||||
@ -157,62 +150,23 @@ class WordNestMenu(MenuSystem):
|
|||||||
done_cb = None
|
done_cb = None
|
||||||
|
|
||||||
def __init__(self, num_words=None, has_checksum=True, done_cb=commit_new_words,
|
def __init__(self, num_words=None, has_checksum=True, done_cb=commit_new_words,
|
||||||
items=None, is_commit=False, menu_cbf=None, prefix="", words=None):
|
items=None, is_commit=False):
|
||||||
|
|
||||||
if num_words is not None:
|
if num_words is not None:
|
||||||
WordNestMenu.target_words = num_words
|
WordNestMenu.target_words = num_words
|
||||||
WordNestMenu.has_checksum = has_checksum
|
WordNestMenu.has_checksum = has_checksum
|
||||||
WordNestMenu.words = []
|
WordNestMenu.words = []
|
||||||
|
assert done_cb
|
||||||
WordNestMenu.done_cb = done_cb
|
WordNestMenu.done_cb = done_cb
|
||||||
is_commit = True
|
is_commit = True
|
||||||
|
|
||||||
if words:
|
|
||||||
WordNestMenu.words = words
|
|
||||||
|
|
||||||
if not items:
|
if not items:
|
||||||
ch = letter_choices(prefix)
|
items = [MenuItem(i, menu=self.next_menu) for i in letter_choices()]
|
||||||
if menu_cbf:
|
|
||||||
items = [MenuItem(i, f=menu_cbf) for i in ch]
|
|
||||||
else:
|
|
||||||
items = [MenuItem(i, menu=self.next_menu) for i in ch]
|
|
||||||
|
|
||||||
self.is_commit = is_commit
|
self.is_commit = is_commit
|
||||||
|
|
||||||
super(WordNestMenu, self).__init__(items)
|
super(WordNestMenu, self).__init__(items)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def get_n_words(cls, num_words):
|
|
||||||
rv = []
|
|
||||||
for _ in range(num_words):
|
|
||||||
rv = await cls.get_word(rv, num_words)
|
|
||||||
|
|
||||||
return rv
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def get_word(cls, words=None, target_words=None):
|
|
||||||
# Just block until N words are provided. May only work before menus start?
|
|
||||||
from glob import numpad
|
|
||||||
|
|
||||||
async def menu_done_cbf(menu, b, c):
|
|
||||||
# duplicates some of the logic of next_menu
|
|
||||||
if c.label[-1] == '-':
|
|
||||||
lc = c.label[0:-1]
|
|
||||||
else:
|
|
||||||
cls.words.append(c.label)
|
|
||||||
numpad.abort_ux()
|
|
||||||
return
|
|
||||||
|
|
||||||
m = cls(prefix=lc, menu_cbf=menu_done_cbf)
|
|
||||||
the_ux.push(m)
|
|
||||||
await the_ux.interact()
|
|
||||||
|
|
||||||
m = cls(num_words=target_words, menu_cbf=menu_done_cbf, has_checksum=False, words=words)
|
|
||||||
|
|
||||||
the_ux.push(m)
|
|
||||||
await the_ux.interact()
|
|
||||||
|
|
||||||
return cls.words
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def next_menu(self, idx, choice):
|
async def next_menu(self, idx, choice):
|
||||||
|
|
||||||
@ -509,10 +463,6 @@ async def add_seed_to_vault(encoded, origin=None, label=None):
|
|||||||
if in_seed_vault(encoded):
|
if in_seed_vault(encoded):
|
||||||
return
|
return
|
||||||
|
|
||||||
# stay "read only" in hobbled mode
|
|
||||||
if pa.hobbled_mode:
|
|
||||||
return
|
|
||||||
|
|
||||||
main_xfp = settings.master_get("xfp", 0)
|
main_xfp = settings.master_get("xfp", 0)
|
||||||
|
|
||||||
# parse encoded
|
# parse encoded
|
||||||
@ -551,16 +501,12 @@ async def add_seed_to_vault(encoded, origin=None, label=None):
|
|||||||
async def set_ephemeral_seed(encoded, chain=None, summarize_ux=True, bip39pw='',
|
async def set_ephemeral_seed(encoded, chain=None, summarize_ux=True, bip39pw='',
|
||||||
is_restore=False, origin=None, label=None):
|
is_restore=False, origin=None, label=None):
|
||||||
# Capture tmp seed into vault, if so enabled, and regardless apply it as new tmp.
|
# Capture tmp seed into vault, if so enabled, and regardless apply it as new tmp.
|
||||||
if not is_restore and not_hobbled_mode():
|
if not is_restore:
|
||||||
await add_seed_to_vault(encoded, origin=origin, label=label)
|
await add_seed_to_vault(encoded, origin=origin, label=label)
|
||||||
dis.fullscreen("Wait...")
|
dis.fullscreen("Wait...")
|
||||||
|
|
||||||
applied, err_msg = pa.tmp_secret(encoded, chain=chain, bip39pw=bip39pw)
|
applied, err_msg = pa.tmp_secret(encoded, chain=chain, bip39pw=bip39pw)
|
||||||
|
|
||||||
# FYI: Might need to bounce the USB connection, because our pubkey has changed,
|
|
||||||
# altho if they have already picked a shared session key, no need, and
|
|
||||||
# would only affect MitM test, which has already been done.
|
|
||||||
|
|
||||||
dis.progress_bar_show(1)
|
dis.progress_bar_show(1)
|
||||||
|
|
||||||
if not applied:
|
if not applied:
|
||||||
@ -569,10 +515,7 @@ async def set_ephemeral_seed(encoded, chain=None, summarize_ux=True, bip39pw='',
|
|||||||
|
|
||||||
xfp = "[" + xfp2str(settings.get("xfp", 0)) + "]"
|
xfp = "[" + xfp2str(settings.get("xfp", 0)) + "]"
|
||||||
if summarize_ux:
|
if summarize_ux:
|
||||||
msg = "New temporary master key is in effect now."
|
await ux_show_story(title=xfp, msg="New temporary master key is in effect now.")
|
||||||
if bip39pw:
|
|
||||||
msg += "\n\nPassphrase: %s" % bip39pw
|
|
||||||
await ux_show_story(title=xfp, msg=msg)
|
|
||||||
|
|
||||||
return applied
|
return applied
|
||||||
|
|
||||||
@ -747,7 +690,6 @@ def set_seed_value(words=None, encoded=None, chain=None):
|
|||||||
|
|
||||||
|
|
||||||
async def calc_bip39_passphrase(pw, bypass_tmp=False):
|
async def calc_bip39_passphrase(pw, bypass_tmp=False):
|
||||||
# Returns (new) encoded secret, new xfp, old xfp
|
|
||||||
from glob import dis, settings
|
from glob import dis, settings
|
||||||
|
|
||||||
dis.fullscreen("Working...")
|
dis.fullscreen("Working...")
|
||||||
@ -764,14 +706,15 @@ async def calc_bip39_passphrase(pw, bypass_tmp=False):
|
|||||||
|
|
||||||
async def set_bip39_passphrase(pw, bypass_tmp=False, summarize_ux=True):
|
async def set_bip39_passphrase(pw, bypass_tmp=False, summarize_ux=True):
|
||||||
nv, xfp, parent_xfp = await calc_bip39_passphrase(pw, bypass_tmp=bypass_tmp)
|
nv, xfp, parent_xfp = await calc_bip39_passphrase(pw, bypass_tmp=bypass_tmp)
|
||||||
|
|
||||||
ret = await set_ephemeral_seed(nv, summarize_ux=summarize_ux, bip39pw=pw,
|
ret = await set_ephemeral_seed(nv, summarize_ux=summarize_ux, bip39pw=pw,
|
||||||
origin="BIP-39 Passphrase on [%s]" % xfp2str(parent_xfp))
|
origin="BIP-39 Passphrase on [%s]" % xfp2str(parent_xfp))
|
||||||
|
|
||||||
dis.draw_status(bip39=int(bool(pw)), xfp=xfp, tmp=1)
|
dis.draw_status(bip39=int(bool(pw)), xfp=xfp, tmp=1)
|
||||||
|
|
||||||
return ret
|
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.
|
||||||
|
|
||||||
async def remember_ephemeral_seed():
|
async def remember_ephemeral_seed():
|
||||||
# Compute current xprv and switch to using that as root secret.
|
# Compute current xprv and switch to using that as root secret.
|
||||||
from nvstore import SettingsObject
|
from nvstore import SettingsObject
|
||||||
@ -794,7 +737,7 @@ async def remember_ephemeral_seed():
|
|||||||
# address cache, settings from tmp seeds / seedvault seeds
|
# address cache, settings from tmp seeds / seedvault seeds
|
||||||
# rebuild fs as we want to save current tmp settings immediately
|
# rebuild fs as we want to save current tmp settings immediately
|
||||||
from files import wipe_flash_filesystem
|
from files import wipe_flash_filesystem
|
||||||
wipe_flash_filesystem()
|
wipe_flash_filesystem(True)
|
||||||
|
|
||||||
dis.draw_status(bip39=0, tmp=0)
|
dis.draw_status(bip39=0, tmp=0)
|
||||||
dis.fullscreen('Saving...')
|
dis.fullscreen('Saving...')
|
||||||
@ -825,6 +768,12 @@ def clear_seed():
|
|||||||
callgate.fast_wipe(True)
|
callgate.fast_wipe(True)
|
||||||
# NOT REACHED
|
# NOT REACHED
|
||||||
|
|
||||||
|
utime.sleep(1)
|
||||||
|
|
||||||
|
# security: need to reboot to really be sure to clear the secrets from main memory.
|
||||||
|
from machine import reset
|
||||||
|
reset()
|
||||||
|
|
||||||
async def word_quiz(words, limited=None, title='Word %d is?'):
|
async def word_quiz(words, limited=None, title='Word %d is?'):
|
||||||
# Perform a test, to check they wrote them down
|
# Perform a test, to check they wrote them down
|
||||||
# Return X if they cancel early.
|
# Return X if they cancel early.
|
||||||
@ -930,8 +879,6 @@ class SeedVaultMenu(MenuSystem):
|
|||||||
ch = await ux_show_story(title="[" + rec.xfp + "]", msg=msg, escape=esc)
|
ch = await ux_show_story(title="[" + rec.xfp + "]", msg=msg, escape=esc)
|
||||||
if ch == "x": return
|
if ch == "x": return
|
||||||
|
|
||||||
assert not_hobbled_mode()
|
|
||||||
|
|
||||||
dis.fullscreen("Saving...")
|
dis.fullscreen("Saving...")
|
||||||
|
|
||||||
wipe_slot = not current_active and (ch != "1")
|
wipe_slot = not current_active and (ch != "1")
|
||||||
@ -943,7 +890,6 @@ class SeedVaultMenu(MenuSystem):
|
|||||||
xs.blank()
|
xs.blank()
|
||||||
del xs
|
del xs
|
||||||
|
|
||||||
|
|
||||||
# CAUTION: will get shadow copy if in tmp seed mode already
|
# CAUTION: will get shadow copy if in tmp seed mode already
|
||||||
seeds = settings.master_get("seeds", [])
|
seeds = settings.master_get("seeds", [])
|
||||||
try:
|
try:
|
||||||
@ -980,8 +926,6 @@ class SeedVaultMenu(MenuSystem):
|
|||||||
from glob import dis
|
from glob import dis
|
||||||
from ux import ux_input_text
|
from ux import ux_input_text
|
||||||
|
|
||||||
assert not_hobbled_mode()
|
|
||||||
|
|
||||||
idx, old = item.arg
|
idx, old = item.arg
|
||||||
new_label = await ux_input_text(old.label, confirm_exit=False, max_len=40)
|
new_label = await ux_input_text(old.label, confirm_exit=False, max_len=40)
|
||||||
|
|
||||||
@ -1012,8 +956,6 @@ class SeedVaultMenu(MenuSystem):
|
|||||||
async def _add_current_tmp(*a):
|
async def _add_current_tmp(*a):
|
||||||
from pincodes import pa
|
from pincodes import pa
|
||||||
|
|
||||||
assert not_hobbled_mode()
|
|
||||||
|
|
||||||
assert pa.tmp_value
|
assert pa.tmp_value
|
||||||
main_xfp = settings.master_get("xfp", 0)
|
main_xfp = settings.master_get("xfp", 0)
|
||||||
|
|
||||||
@ -1056,10 +998,9 @@ class SeedVaultMenu(MenuSystem):
|
|||||||
|
|
||||||
if not seeds:
|
if not seeds:
|
||||||
rv.append(MenuItem('(none saved yet)'))
|
rv.append(MenuItem('(none saved yet)'))
|
||||||
if not_hobbled_mode():
|
if pa.tmp_value:
|
||||||
if pa.tmp_value:
|
rv.append(add_current_tmp)
|
||||||
rv.append(add_current_tmp)
|
rv.append(MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu))
|
||||||
rv.append(MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu))
|
|
||||||
else:
|
else:
|
||||||
wipe_if_deltamode()
|
wipe_if_deltamode()
|
||||||
|
|
||||||
@ -1075,10 +1016,8 @@ class SeedVaultMenu(MenuSystem):
|
|||||||
submenu = [
|
submenu = [
|
||||||
MenuItem(rec.label, f=cls._detail, arg=(rec, encoded)),
|
MenuItem(rec.label, f=cls._detail, arg=(rec, encoded)),
|
||||||
MenuItem('Use This Seed', f=cls._set, arg=encoded),
|
MenuItem('Use This Seed', f=cls._set, arg=encoded),
|
||||||
MenuItem('Rename', f=cls._rename, arg=(i, rec),
|
MenuItem('Rename', f=cls._rename, arg=(i, rec)),
|
||||||
predicate=not_hobbled_mode),
|
MenuItem('Delete', f=cls._remove, arg=(i, rec, encoded)),
|
||||||
MenuItem('Delete', f=cls._remove, arg=(i, rec, encoded),
|
|
||||||
predicate=not_hobbled_mode),
|
|
||||||
]
|
]
|
||||||
if is_active:
|
if is_active:
|
||||||
submenu[1] = MenuItem("Seed In Use")
|
submenu[1] = MenuItem("Seed In Use")
|
||||||
@ -1096,7 +1035,7 @@ class SeedVaultMenu(MenuSystem):
|
|||||||
rv.append(item)
|
rv.append(item)
|
||||||
|
|
||||||
if pa.tmp_value:
|
if pa.tmp_value:
|
||||||
if seeds and (not tmp_in_sv) and not_hobbled_mode():
|
if seeds and (not tmp_in_sv):
|
||||||
# give em chance to store current active
|
# give em chance to store current active
|
||||||
rv.append(add_current_tmp)
|
rv.append(add_current_tmp)
|
||||||
|
|
||||||
@ -1169,7 +1108,6 @@ class EphemeralSeedMenu(MenuSystem):
|
|||||||
from actions import nfc_recv_ephemeral, import_xprv
|
from actions import nfc_recv_ephemeral, import_xprv
|
||||||
from actions import restore_backup, scan_any_qr
|
from actions import restore_backup, scan_any_qr
|
||||||
from tapsigner import import_tapsigner_backup_file
|
from tapsigner import import_tapsigner_backup_file
|
||||||
from xor_seed import xor_restore_temporary
|
|
||||||
from charcodes import KEY_QR
|
from charcodes import KEY_QR
|
||||||
|
|
||||||
import_ephemeral_menu = [
|
import_ephemeral_menu = [
|
||||||
@ -1186,21 +1124,19 @@ class EphemeralSeedMenu(MenuSystem):
|
|||||||
]
|
]
|
||||||
|
|
||||||
rv = [
|
rv = [
|
||||||
MenuItem("Generate Words", menu=gen_ephemeral_menu, predicate=not_hobbled_mode),
|
MenuItem("Generate Words", menu=gen_ephemeral_menu),
|
||||||
MenuItem('Import from QR Scan', predicate=version.has_qr,
|
MenuItem('Import from QR Scan', predicate=version.has_qr,
|
||||||
shortcut=KEY_QR, f=scan_any_qr, arg=(True, True)),
|
shortcut=KEY_QR, f=scan_any_qr, arg=(True, True)),
|
||||||
MenuItem("Import Words", menu=import_ephemeral_menu),
|
MenuItem("Import Words", menu=import_ephemeral_menu),
|
||||||
MenuItem("Import XPRV", f=import_xprv, arg=True), # ephemeral=True
|
MenuItem("Import XPRV", f=import_xprv, arg=True), # ephemeral=True
|
||||||
MenuItem("Tapsigner Backup", f=import_tapsigner_backup_file, arg=True), # ephemeral=True
|
MenuItem("Tapsigner Backup", f=import_tapsigner_backup_file, arg=True), # ephemeral=True
|
||||||
MenuItem("Coldcard Backup", f=restore_backup, arg=True), # tmp=True
|
MenuItem("Coldcard Backup", f=restore_backup, arg=True), # tmp=True
|
||||||
MenuItem("Restore Seed XOR", f=xor_restore_temporary),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
|
|
||||||
async def make_ephemeral_seed_menu(*a):
|
async def make_ephemeral_seed_menu(*a):
|
||||||
|
|
||||||
if (not pa.tmp_value) and (not settings.master_get("seedvault", False)):
|
if (not pa.tmp_value) and (not settings.master_get("seedvault", False)):
|
||||||
# force a warning on them, unless they are already doing it.
|
# force a warning on them, unless they are already doing it.
|
||||||
if not await ux_confirm(
|
if not await ux_confirm(
|
||||||
@ -1238,10 +1174,10 @@ the passphrase as well, it's okay to put them together.) There is no way for \
|
|||||||
the Coldcard to know if your entry is correct, and if you have it wrong, \
|
the Coldcard to know if your entry is correct, and if you have it wrong, \
|
||||||
you will be looking at an empty wallet.
|
you will be looking at an empty wallet.
|
||||||
|
|
||||||
Limitations: %d characters max length, ASCII characters 32-126 (0x20-0x7e) only.
|
Limitations: 100 characters max length, ASCII characters 32-126 (0x20-0x7e) only.
|
||||||
|
|
||||||
%s to continue or press (2) to hide this message forever.
|
%s to continue or press (2) to hide this message forever.
|
||||||
''' % (howto if not version.has_qwerty else '', MAX_PASS_LEN, OK)
|
''' % (howto if not version.has_qwerty else '', OK)
|
||||||
|
|
||||||
ch = await ux_show_story(msg, escape='2')
|
ch = await ux_show_story(msg, escape='2')
|
||||||
if ch == '2':
|
if ch == '2':
|
||||||
@ -1251,8 +1187,8 @@ Limitations: %d characters max length, ASCII characters 32-126 (0x20-0x7e) only.
|
|||||||
|
|
||||||
if version.has_qwerty and not PassphraseSaver.has_file():
|
if version.has_qwerty and not PassphraseSaver.has_file():
|
||||||
# no need for any menus if Q and no card present
|
# no need for any menus if Q and no card present
|
||||||
pp = await ux_input_text('', prompt="Your BIP-39 Passphrase", b39_complete=True,
|
pp = await ux_input_text('', prompt="Your BIP-39 Passphrase",
|
||||||
scan_ok=True, max_len=MAX_PASS_LEN)
|
b39_complete=True, scan_ok=True, max_len=100)
|
||||||
if not pp: return
|
if not pp: return
|
||||||
|
|
||||||
await apply_pass_value(pp)
|
await apply_pass_value(pp)
|
||||||
@ -1262,7 +1198,7 @@ Limitations: %d characters max length, ASCII characters 32-126 (0x20-0x7e) only.
|
|||||||
|
|
||||||
|
|
||||||
class PassphraseMenu(MenuSystem):
|
class PassphraseMenu(MenuSystem):
|
||||||
# Collect up to MAX_PASS_LEN chars as a BIP-39 passphrase
|
# Collect up to 100 chars as a BIP-39 passphrase
|
||||||
|
|
||||||
# singleton (cls level) vars
|
# singleton (cls level) vars
|
||||||
done_cb = None
|
done_cb = None
|
||||||
@ -1351,7 +1287,7 @@ class PassphraseMenu(MenuSystem):
|
|||||||
async def view_edit_phrase(cls, *a):
|
async def view_edit_phrase(cls, *a):
|
||||||
# let them control each character
|
# let them control each character
|
||||||
pw = await ux_input_text(cls.pp_sofar, prompt="Your BIP-39 Passphrase",
|
pw = await ux_input_text(cls.pp_sofar, prompt="Your BIP-39 Passphrase",
|
||||||
b39_complete=True, scan_ok=True, max_len=MAX_PASS_LEN)
|
b39_complete=True, scan_ok=True, max_len=100)
|
||||||
if pw is not None:
|
if pw is not None:
|
||||||
cls.pp_sofar = pw
|
cls.pp_sofar = pw
|
||||||
cls.check_length()
|
cls.check_length()
|
||||||
@ -1362,8 +1298,8 @@ class PassphraseMenu(MenuSystem):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check_length(cls):
|
def check_length(cls):
|
||||||
# enforce a limit of MAX_PASS_LEN chars
|
# enforce a limit of 100 chars
|
||||||
cls.pp_sofar = cls.pp_sofar[0:MAX_PASS_LEN]
|
cls.pp_sofar = cls.pp_sofar[0:100]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def add_text(cls, _1, _2, item):
|
async def add_text(cls, _1, _2, item):
|
||||||
@ -1404,9 +1340,8 @@ async def apply_pass_value(new_pp):
|
|||||||
|
|
||||||
msg = ('Above is the master key fingerprint of the new wallet'
|
msg = ('Above is the master key fingerprint of the new wallet'
|
||||||
' created by adding passphrase to %s.'
|
' created by adding passphrase to %s.'
|
||||||
'\n\nPassphrase: %s'
|
|
||||||
'\n\nPress %s to abort, %s to use the new wallet, (1) to apply'
|
'\n\nPress %s to abort, %s to use the new wallet, (1) to apply'
|
||||||
' and save to MicroSD for future.') % (msg, new_pp, X, OK)
|
' and save to MicroSD for future.') % (msg, X, OK)
|
||||||
|
|
||||||
ch = await ux_show_story(msg, title="[%s]" % xfp_str, escape='1')
|
ch = await ux_show_story(msg, title="[%s]" % xfp_str, escape='1')
|
||||||
if ch == 'x':
|
if ch == 'x':
|
||||||
|
|||||||
@ -171,7 +171,7 @@ async def test_secure_element():
|
|||||||
|
|
||||||
dis.clear()
|
dis.clear()
|
||||||
|
|
||||||
if version.has_qwerty or version.mk_num == 5:
|
if version.has_qwerty:
|
||||||
dis.text(0, 0, "^^-- Green? " if gg else " ^^-- Red?")
|
dis.text(0, 0, "^^-- Green? " if gg else " ^^-- Red?")
|
||||||
else:
|
else:
|
||||||
if gg:
|
if gg:
|
||||||
@ -364,7 +364,7 @@ async def test_microsd():
|
|||||||
|
|
||||||
with CardSlot(slot_b=slot_num) as card:
|
with CardSlot(slot_b=slot_num) as card:
|
||||||
|
|
||||||
fn, _ = card.pick_filename('test-delme.txt')
|
_, fn = card.pick_filename('test-delme.txt')
|
||||||
|
|
||||||
with open(fn, 'wt') as fd:
|
with open(fn, 'wt') as fd:
|
||||||
fd.write("Hello")
|
fd.write("Hello")
|
||||||
|
|||||||
@ -19,7 +19,6 @@ from ubinascii import hexlify as b2a_hex
|
|||||||
import ustruct as struct
|
import ustruct as struct
|
||||||
import ngu
|
import ngu
|
||||||
from opcodes import *
|
from opcodes import *
|
||||||
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2SH, AF_P2WSH, AF_P2TR, AF_BARE_PK
|
|
||||||
|
|
||||||
# single-shot hash functions
|
# single-shot hash functions
|
||||||
sha256 = ngu.hash.sha256s
|
sha256 = ngu.hash.sha256s
|
||||||
@ -27,8 +26,8 @@ ripemd160 = ngu.hash.ripemd160
|
|||||||
hash256 = ngu.hash.sha256d
|
hash256 = ngu.hash.sha256d
|
||||||
hash160 = ngu.hash.hash160
|
hash160 = ngu.hash.hash160
|
||||||
|
|
||||||
#def bytes_to_hex_str(s):
|
def bytes_to_hex_str(s):
|
||||||
# return str(b2a_hex(s), 'ascii')
|
return str(b2a_hex(s), 'ascii')
|
||||||
|
|
||||||
SIGHASH_ALL = const(1)
|
SIGHASH_ALL = const(1)
|
||||||
SIGHASH_NONE = const(2)
|
SIGHASH_NONE = const(2)
|
||||||
@ -195,43 +194,41 @@ def disassemble(script):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
offset = 0
|
offset = 0
|
||||||
slen = len(script)
|
|
||||||
while 1:
|
while 1:
|
||||||
if offset >= slen:
|
if offset >= len(script):
|
||||||
#print('dis %d done' % offset)
|
#print('dis %d done' % offset)
|
||||||
return
|
return
|
||||||
c = script[offset]
|
c = script[offset]
|
||||||
offset += 1
|
offset += 1
|
||||||
|
|
||||||
if 1 <= c <= 75:
|
if 1 <= c <= 75:
|
||||||
cnt = c
|
#print('dis %d: bytes=%s' % (offset, b2a_hex(script[offset:offset+c])))
|
||||||
|
yield (script[offset:offset+c], None)
|
||||||
|
offset += c
|
||||||
elif OP_1 <= c <= OP_16:
|
elif OP_1 <= c <= OP_16:
|
||||||
# OP_1 thru OP_16
|
# OP_1 thru OP_16
|
||||||
|
#print('dis %d: number=%d' % (offset, (c - OP_1 + 1)))
|
||||||
yield (c - OP_1 + 1, None)
|
yield (c - OP_1 + 1, None)
|
||||||
continue
|
|
||||||
elif c == OP_PUSHDATA1:
|
elif c == OP_PUSHDATA1:
|
||||||
cnt = script[offset]
|
cnt = script[offset]
|
||||||
offset += 1
|
offset += 1
|
||||||
|
yield (script[offset:offset+cnt], None)
|
||||||
|
offset += cnt
|
||||||
elif c == OP_PUSHDATA2:
|
elif c == OP_PUSHDATA2:
|
||||||
# up to 65535 bytes
|
# up to 65535 bytes
|
||||||
cnt, = struct.unpack_from("H", script, offset)
|
cnt, = struct.unpack_from("H", script, offset)
|
||||||
offset += 2
|
offset += 2
|
||||||
|
yield (script[offset:offset+cnt], None)
|
||||||
|
offset += cnt
|
||||||
elif c == OP_PUSHDATA4:
|
elif c == OP_PUSHDATA4:
|
||||||
# no where to put so much data
|
# no where to put so much data
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
elif c == OP_1NEGATE:
|
elif c == OP_1NEGATE:
|
||||||
yield (-1, None)
|
yield (-1, None)
|
||||||
continue
|
|
||||||
else:
|
else:
|
||||||
# OP_0 included here
|
# OP_0 included here
|
||||||
|
#print('dis %d: opcode=%d' % (offset, c))
|
||||||
yield (None, c)
|
yield (None, c)
|
||||||
continue
|
|
||||||
|
|
||||||
# a data push of `cnt` bytes - reject if it runs off the end
|
|
||||||
if offset + cnt > slen:
|
|
||||||
raise ValueError
|
|
||||||
yield (script[offset:offset+cnt], None)
|
|
||||||
offset += cnt
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# import sys;sys.print_exception(e)
|
# import sys;sys.print_exception(e)
|
||||||
raise ValueError("bad script")
|
raise ValueError("bad script")
|
||||||
@ -322,6 +319,7 @@ class CTxIn(object):
|
|||||||
self.nSequence = nSequence
|
self.nSequence = nSequence
|
||||||
|
|
||||||
def deserialize(self, f):
|
def deserialize(self, f):
|
||||||
|
self.prevout = COutPoint()
|
||||||
self.prevout.deserialize(f)
|
self.prevout.deserialize(f)
|
||||||
self.scriptSig = deser_string(f)
|
self.scriptSig = deser_string(f)
|
||||||
self.nSequence = struct.unpack("<I", f.read(4))[0]
|
self.nSequence = struct.unpack("<I", f.read(4))[0]
|
||||||
@ -357,32 +355,26 @@ class CTxOut(object):
|
|||||||
# (addr_type_code, addr, is_segwit)
|
# (addr_type_code, addr, is_segwit)
|
||||||
# 'addr' is byte string, either 20 or 32 long
|
# 'addr' is byte string, either 20 or 32 long
|
||||||
if self.is_p2tr():
|
if self.is_p2tr():
|
||||||
return AF_P2TR, self.scriptPubKey[2:2+32], True
|
return 'p2tr', self.scriptPubKey[2:2+32], True
|
||||||
|
|
||||||
if self.is_p2wpkh():
|
if self.is_p2wpkh():
|
||||||
return AF_P2WPKH, self.scriptPubKey[2:2+20], True
|
return 'p2pkh', self.scriptPubKey[2:2+20], True
|
||||||
|
|
||||||
if self.is_p2wsh():
|
if self.is_p2wsh():
|
||||||
return AF_P2WSH, self.scriptPubKey[2:2+32], True
|
return 'p2sh', self.scriptPubKey[2:2+32], True
|
||||||
|
|
||||||
if self.is_p2pkh():
|
if self.is_p2pkh():
|
||||||
return AF_CLASSIC, self.scriptPubKey[3:3+20], False
|
return 'p2pkh', self.scriptPubKey[3:3+20], False
|
||||||
|
|
||||||
if self.is_p2sh():
|
if self.is_p2sh():
|
||||||
# can be:
|
return 'p2sh', self.scriptPubKey[2:2+20], False
|
||||||
# * bare P2SH
|
|
||||||
# * P2SH-P2WPKH
|
|
||||||
# * P2SH-P2WSH
|
|
||||||
return AF_P2SH, self.scriptPubKey[2:2+20], False
|
|
||||||
|
|
||||||
if self.is_p2pk():
|
if self.is_p2pk():
|
||||||
# rare, pay to full pubkey: <push_op> <pubkey> OP_CHECKSIG
|
# rare, pay to full pubkey
|
||||||
# push_op is 0x21 (33) for compressed, 0x41 (65) for uncompressed
|
return 'p2pk', self.scriptPubKey[2:2+33], False
|
||||||
pk_len = self.scriptPubKey[0]
|
|
||||||
return AF_BARE_PK, self.scriptPubKey[1:1+pk_len], False
|
|
||||||
|
|
||||||
if self.is_op_return():
|
if self.scriptPubKey[0] == OP_RETURN:
|
||||||
return OP_RETURN, self.scriptPubKey, False
|
return 'op_return', self.scriptPubKey, False
|
||||||
|
|
||||||
return None, self.scriptPubKey, None
|
return None, self.scriptPubKey, None
|
||||||
|
|
||||||
@ -409,11 +401,8 @@ class CTxOut(object):
|
|||||||
|
|
||||||
def is_p2pk(self):
|
def is_p2pk(self):
|
||||||
return (len(self.scriptPubKey) == 35 or len(self.scriptPubKey) == 67) \
|
return (len(self.scriptPubKey) == 35 or len(self.scriptPubKey) == 67) \
|
||||||
and self.scriptPubKey[0] == len(self.scriptPubKey) - 2 \
|
and (self.scriptPubKey[0] == 0x21 or self.scriptPubKey[0] == 0x41) \
|
||||||
and self.scriptPubKey[-1] == OP_CHECKSIG
|
and self.scriptPubKey[-1] == 0xac
|
||||||
|
|
||||||
def is_op_return(self):
|
|
||||||
return self.scriptPubKey and (self.scriptPubKey[0] == OP_RETURN)
|
|
||||||
|
|
||||||
#def __repr__(self):
|
#def __repr__(self):
|
||||||
# return "CTxOut(nValue=%d scriptPubKey=%s)" \
|
# return "CTxOut(nValue=%d scriptPubKey=%s)" \
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
# sffile.py - file-like objects stored in PSRAM (Mk4+) (used to be SPI Flash)
|
# sffile.py - file-like objects stored in PSRAM (Mk4+) (used to be SPI Flash)
|
||||||
#
|
#
|
||||||
# - implements stream IO protocol
|
# - implements stream IO protoccol
|
||||||
# - random read, sequential write
|
# - random read, sequential write
|
||||||
# - only a few of these are possible
|
# - only a few of these are possible
|
||||||
# - the offset is the file name
|
# - the offset is the file name
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||||
#
|
#
|
||||||
# ssd1306.py - MicroPython SSD1306 OLED driver, with SPI interface
|
# ssd1306.py - MicroPython SSD1306 OLED driver, I2C and SPI interfaces
|
||||||
#
|
#
|
||||||
# Copied from ../external/micropython/drivers/display/ssd1306.py
|
# Copied from ../external/micropython/drivers/display/ssd1306.py
|
||||||
#
|
#
|
||||||
@ -28,81 +28,49 @@ SET_VCOM_DESEL = const(0xdb)
|
|||||||
SET_CHARGE_PUMP = const(0x8d)
|
SET_CHARGE_PUMP = const(0x8d)
|
||||||
|
|
||||||
# Subclassing FrameBuffer provides support for graphics primitives
|
# Subclassing FrameBuffer provides support for graphics primitives
|
||||||
# see <http://docs.micropython.org/en/latest/pyboard/library/framebuf.html>
|
# http://docs.micropython.org/en/latest/pyboard/library/framebuf.html
|
||||||
#
|
|
||||||
class SSD1306(framebuf.FrameBuffer):
|
class SSD1306(framebuf.FrameBuffer):
|
||||||
def __init__(self, width, height, is_mk5):
|
def __init__(self, width, height, external_vcc):
|
||||||
self.width = width
|
self.width = width
|
||||||
self.height = height
|
self.height = height
|
||||||
self.is_mk5 = is_mk5
|
self.external_vcc = external_vcc
|
||||||
self.pages = self.height // 8
|
self.pages = self.height // 8
|
||||||
|
|
||||||
|
#self.buffer = bytearray(self.pages * self.width)
|
||||||
|
|
||||||
self.buffer = bytearray(1024)
|
self.buffer = bytearray(1024)
|
||||||
#assert len(self.buffer) == self.pages * self.width
|
assert len(self.buffer) == self.pages * self.width
|
||||||
|
|
||||||
super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
|
super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
|
||||||
self.init_display()
|
self.init_display()
|
||||||
|
|
||||||
def init_display(self):
|
def init_display(self):
|
||||||
if not self.is_mk5:
|
for cmd in (
|
||||||
# Mk4 and earlier
|
SET_DISP | 0x00, # off
|
||||||
cmds = (
|
# address setting
|
||||||
SET_DISP | 0x00, # display off
|
SET_MEM_ADDR, 0x00, # horizontal
|
||||||
# address setting
|
# resolution and layout
|
||||||
SET_MEM_ADDR, 0x00, # horizontal
|
SET_DISP_START_LINE | 0x00,
|
||||||
# resolution and layout
|
SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0
|
||||||
SET_DISP_START_LINE | 0x00,
|
SET_MUX_RATIO, self.height - 1,
|
||||||
SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0
|
SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0
|
||||||
SET_MUX_RATIO, self.height - 1,
|
SET_DISP_OFFSET, 0x00,
|
||||||
SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0
|
SET_COM_PIN_CFG, 0x02 if self.height == 32 else 0x12,
|
||||||
SET_DISP_OFFSET, 0x00,
|
# timing and driving scheme
|
||||||
SET_COM_PIN_CFG, 0x12,
|
SET_DISP_CLK_DIV, 0xF0,
|
||||||
# timing and driving scheme
|
SET_PRECHARGE, 0x22 if self.external_vcc else 0xf1,
|
||||||
SET_DISP_CLK_DIV, 0xF0,
|
SET_VCOM_DESEL, 0x30, # 0.83*Vcc
|
||||||
SET_PRECHARGE, 0xf1,
|
# display
|
||||||
SET_VCOM_DESEL, 0x30, # 0.83*Vcc
|
SET_CONTRAST, 0xff, # maximum
|
||||||
# display
|
SET_ENTIRE_ON, # output follows RAM contents
|
||||||
SET_CONTRAST, 0xff, # maximum
|
SET_NORM_INV, # not inverted
|
||||||
SET_ENTIRE_ON, # output follows RAM contents
|
# charge pump
|
||||||
SET_NORM_INV, # not inverted
|
SET_CHARGE_PUMP, 0x10 if self.external_vcc else 0x14,
|
||||||
# charge pump
|
SET_DISP | 0x01): # on
|
||||||
SET_CHARGE_PUMP, 0x14)
|
self.write_cmd(cmd)
|
||||||
else:
|
|
||||||
# Mk5 has external +12v power supply, and different setup protocol
|
|
||||||
|
|
||||||
cmds = (
|
|
||||||
SET_DISP | 0x00, # display off
|
|
||||||
# address setting
|
|
||||||
SET_MEM_ADDR, 0x00, # horizontal
|
|
||||||
# resolution and layout
|
|
||||||
SET_DISP_START_LINE | 0x00,
|
|
||||||
SET_SEG_REMAP | 0x00, # column addr 0 mapped to SEG127
|
|
||||||
SET_MUX_RATIO, self.height - 1,
|
|
||||||
SET_COM_OUT_DIR | 0x00, # scan from COM[8] to COM[N]
|
|
||||||
SET_DISP_OFFSET, 0x00,
|
|
||||||
SET_COM_PIN_CFG, 0x12,
|
|
||||||
# timing and driving scheme
|
|
||||||
SET_DISP_CLK_DIV, 0xF0,
|
|
||||||
SET_PRECHARGE, 0x22,
|
|
||||||
SET_VCOM_DESEL, 0x40, # per spec sheet
|
|
||||||
# display
|
|
||||||
SET_CONTRAST, 0x85, # NOT maximum, because spec sheet
|
|
||||||
SET_ENTIRE_ON, # output follows RAM contents
|
|
||||||
SET_NORM_INV, # not inverted
|
|
||||||
SET_CHARGE_PUMP, 0x10, # charge pump: DISABLE
|
|
||||||
)
|
|
||||||
|
|
||||||
self.write_cmds(cmds)
|
|
||||||
|
|
||||||
self.fill(0)
|
self.fill(0)
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
self.write_cmd(SET_DISP | 0x01)
|
|
||||||
|
|
||||||
def write_cmds(self, cmds):
|
|
||||||
for c in cmds:
|
|
||||||
self.write_cmd(c)
|
|
||||||
|
|
||||||
def poweroff(self):
|
def poweroff(self):
|
||||||
self.write_cmd(SET_DISP | 0x00)
|
self.write_cmd(SET_DISP | 0x00)
|
||||||
|
|
||||||
@ -110,10 +78,6 @@ class SSD1306(framebuf.FrameBuffer):
|
|||||||
self.write_cmd(SET_DISP | 0x01)
|
self.write_cmd(SET_DISP | 0x01)
|
||||||
|
|
||||||
def contrast(self, contrast):
|
def contrast(self, contrast):
|
||||||
# brightness: normal = 0x7f, brightness=0xff, dim=0x00 (but they are all very similar)
|
|
||||||
if self.is_mk5:
|
|
||||||
# - limit to a specific max value from OLED specs used on Mk5
|
|
||||||
contrast = max(contrast, 0x85)
|
|
||||||
self.write_cmd(SET_CONTRAST)
|
self.write_cmd(SET_CONTRAST)
|
||||||
self.write_cmd(contrast)
|
self.write_cmd(contrast)
|
||||||
|
|
||||||
@ -121,113 +85,56 @@ class SSD1306(framebuf.FrameBuffer):
|
|||||||
self.write_cmd(SET_NORM_INV | (invert & 1))
|
self.write_cmd(SET_NORM_INV | (invert & 1))
|
||||||
|
|
||||||
def show(self):
|
def show(self):
|
||||||
|
x0 = 0
|
||||||
|
x1 = self.width - 1
|
||||||
|
if self.width == 64:
|
||||||
|
# displays with width of 64 pixels are shifted by 32
|
||||||
|
x0 += 32
|
||||||
|
x1 += 32
|
||||||
self.write_cmd(SET_COL_ADDR)
|
self.write_cmd(SET_COL_ADDR)
|
||||||
self.write_cmd(0)
|
self.write_cmd(x0)
|
||||||
self.write_cmd(self.width - 1)
|
self.write_cmd(x1)
|
||||||
|
|
||||||
self.write_cmd(SET_PAGE_ADDR)
|
self.write_cmd(SET_PAGE_ADDR)
|
||||||
self.write_cmd(0)
|
self.write_cmd(0)
|
||||||
self.write_cmd(self.pages - 1)
|
self.write_cmd(self.pages - 1)
|
||||||
|
|
||||||
self.write_data(self.buffer)
|
self.write_data(self.buffer)
|
||||||
|
|
||||||
def busy_bar(self, enable, pattern):
|
SPI_RATE = const(40000000) # max chip can do, still slower than display limit tho
|
||||||
# Render a continuous activity (not progress) bar in lower 8 lines of display
|
|
||||||
# - using OLED itself to do the animation, so smooth and CPU free
|
|
||||||
# - cannot preserve bottom 8 lines, since we have to destructively write there
|
|
||||||
# - assumes normal horz addr mode: 0x20, 0x00
|
|
||||||
# - speed_code=>framedelay: 0=5fr, 1=64fr, 2=128, 3=256, 4=3, 5=4, 6=25, 7=2frames
|
|
||||||
# unused: assert 0 <= speed_code <= 7
|
|
||||||
|
|
||||||
setup = bytes([
|
|
||||||
0x21, 0x00, 0x7f, # setup column address range (start, end): 0-127
|
|
||||||
0x22, 7, 7, # setup page start/end address: page 7=last 8 lines
|
|
||||||
])
|
|
||||||
if not self.is_mk5:
|
|
||||||
animate = bytes([
|
|
||||||
0x2e, # stop animations in progress
|
|
||||||
0x26, # scroll leftwards (stock ticker mode)
|
|
||||||
0, # placeholder
|
|
||||||
7, # start 'page' (vertical)
|
|
||||||
5, # "speed_code" # scroll speed: 7=fastest, but no order to it
|
|
||||||
7, # end 'page'
|
|
||||||
0, 0x7f, # start/end columns
|
|
||||||
0x2f # start
|
|
||||||
])
|
|
||||||
else:
|
|
||||||
# SSD1309? doesn't implement 0x26 but has other commands
|
|
||||||
animate = bytes([
|
|
||||||
0x2e, # stop animations in progress
|
|
||||||
0x29, # Vert+Right horz animation setup
|
|
||||||
1, # A: enable horz scroll
|
|
||||||
7, # B: start 'page' (vertical)
|
|
||||||
5, # C: "speed_code" # scroll speed: 7=fastest, but no order to it
|
|
||||||
7, # D: end 'page'
|
|
||||||
1, # E: vert scrolling offset (unused)
|
|
||||||
0, 0x7f, # F,G: start/end columns
|
|
||||||
0xa3, # Set Vertical scroll Area
|
|
||||||
0, 0, # A, B: # of rows in fixed vs. scroll area
|
|
||||||
0x2f # start animating
|
|
||||||
])
|
|
||||||
|
|
||||||
cleanup = bytes([
|
|
||||||
0x2e, # stop animation
|
|
||||||
0x20, 0x00, # horz addr-ing mode
|
|
||||||
0x21, 0x00, 0x7f, # setup column address range (start, end): 0-127
|
|
||||||
0x22, 7, 7, # setup page start/end address: page 7=last 8 lines
|
|
||||||
])
|
|
||||||
|
|
||||||
if not enable:
|
|
||||||
# stop animation, and redraw old (new) screen
|
|
||||||
self.write_cmds(cleanup)
|
|
||||||
else:
|
|
||||||
# needs a pattern that repeats nicely mod 128
|
|
||||||
self.write_cmds(setup)
|
|
||||||
self.write_data(pattern)
|
|
||||||
self.write_cmds(animate)
|
|
||||||
|
|
||||||
class SSD1306_SPI(SSD1306):
|
class SSD1306_SPI(SSD1306):
|
||||||
def __init__(self, width, height, spi, dc, res, cs, is_mk5=False):
|
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)
|
||||||
self.spi = spi
|
self.spi = spi
|
||||||
self.dc = dc
|
self.dc = dc
|
||||||
self.cs = cs
|
|
||||||
self.res = res
|
self.res = res
|
||||||
|
self.cs = cs
|
||||||
# initial states
|
self.res(1)
|
||||||
dc(0)
|
|
||||||
cs(1)
|
|
||||||
|
|
||||||
# reset sequence
|
|
||||||
res(1)
|
|
||||||
time.sleep_ms(1)
|
time.sleep_ms(1)
|
||||||
res(0)
|
self.res(0)
|
||||||
time.sleep_ms(10)
|
time.sleep_ms(10)
|
||||||
res(1)
|
self.res(1)
|
||||||
|
super().__init__(width, height, external_vcc)
|
||||||
super().__init__(width, height, is_mk5)
|
|
||||||
|
|
||||||
def _setup_spi(self):
|
|
||||||
# need to re-do this constantly
|
|
||||||
# max chip can do, still slower than display limit tho
|
|
||||||
# - 40Mhz (target) is fine for short-cabled Mk4 (actual is lower?)
|
|
||||||
# - max spec is 10Mhz on Mk5
|
|
||||||
rate = 40_000_000 if not self.is_mk5 else 10_000_000
|
|
||||||
self.spi.init(baudrate=rate, polarity=0, phase=0)
|
|
||||||
|
|
||||||
def write_cmd(self, cmd):
|
def write_cmd(self, cmd):
|
||||||
self._setup_spi()
|
self.spi.init(baudrate=SPI_RATE, polarity=0, phase=0)
|
||||||
self.cs(1)
|
self.cs(1)
|
||||||
self.dc(0)
|
self.dc(0)
|
||||||
self.cs(0)
|
self.cs(0)
|
||||||
self.spi.write(bytearray([cmd]))
|
try:
|
||||||
|
self.spi.write(bytearray([cmd]))
|
||||||
|
except:
|
||||||
|
print("SPI[cmd]: %r" % self.spi)
|
||||||
self.cs(1)
|
self.cs(1)
|
||||||
|
|
||||||
def write_data(self, buf):
|
def write_data(self, buf):
|
||||||
self._setup_spi()
|
self.spi.init(baudrate=SPI_RATE, polarity=0, phase=0)
|
||||||
self.cs(1)
|
self.cs(1)
|
||||||
self.dc(1)
|
self.dc(1)
|
||||||
self.cs(0)
|
self.cs(0)
|
||||||
self.spi.write(buf)
|
try:
|
||||||
|
self.spi.write(buf)
|
||||||
|
except:
|
||||||
|
print("SPI[data]: %r" % self.spi)
|
||||||
self.cs(1)
|
self.cs(1)
|
||||||
|
|
||||||
# EOF
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
#
|
#
|
||||||
import ngu, uctypes, gc, bip39, utime
|
import ngu, uctypes, gc, bip39, utime
|
||||||
from uhashlib import sha256
|
from uhashlib import sha256
|
||||||
from utils import swab32, call_later_ms, B2A, node_from_privkey
|
from utils import swab32, call_later_ms, B2A
|
||||||
|
|
||||||
|
|
||||||
SEED_LEN_OPTS = [12, 18, 24]
|
SEED_LEN_OPTS = [12, 18, 24]
|
||||||
@ -104,7 +104,7 @@ class SecretStash:
|
|||||||
ch, pk = secret[1:33], secret[33:65]
|
ch, pk = secret[1:33], secret[33:65]
|
||||||
assert not _bip39pw
|
assert not _bip39pw
|
||||||
|
|
||||||
hd = node_from_privkey(pk, ch)
|
hd.from_chaincode_privkey(ch, pk)
|
||||||
return 'xprv', ch+pk, hd
|
return 'xprv', ch+pk, hd
|
||||||
|
|
||||||
elif marker & 0x80:
|
elif marker & 0x80:
|
||||||
@ -403,7 +403,8 @@ class SensitiveValues:
|
|||||||
self.register(cc)
|
self.register(cc)
|
||||||
self.register(pk)
|
self.register(pk)
|
||||||
|
|
||||||
rv = node_from_privkey(pk, cc)
|
rv = ngu.hdnode.HDNode()
|
||||||
|
rv.from_chaincode_privkey(cc, pk)
|
||||||
self.register(rv)
|
self.register(rv)
|
||||||
|
|
||||||
return rv, p
|
return rv, p
|
||||||
|
|||||||
@ -67,7 +67,7 @@ async def import_tapsigner_backup_file(_1, _2, item):
|
|||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
fn = await file_picker(suffix=".aes", min_size=100, max_size=160, **choice)
|
fn = await file_picker(suffix="aes", min_size=100, max_size=160, **choice)
|
||||||
if not fn: return
|
if not fn: return
|
||||||
origin += (" (%s)" % fn)
|
origin += (" (%s)" % fn)
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -307,17 +307,11 @@ async def kt_accept_values(dtype, raw):
|
|||||||
- `b` - complete system backup file (text, internal format)
|
- `b` - complete system backup file (text, internal format)
|
||||||
'''
|
'''
|
||||||
from flow import has_se_secrets, goto_top_menu
|
from flow import has_se_secrets, goto_top_menu
|
||||||
from pincodes import pa
|
|
||||||
|
|
||||||
enc = None
|
enc = None
|
||||||
origin = 'Teleported'
|
origin = 'Teleported'
|
||||||
label = None
|
label = None
|
||||||
|
|
||||||
if pa.hobbled_mode and dtype != 'p':
|
|
||||||
await ux_show_story('Only PSBT for multisig accepted in this mode.', title='FAILED')
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
if dtype == 's':
|
if dtype == 's':
|
||||||
# words / bip 32 master / xprv, etc
|
# words / bip 32 master / xprv, etc
|
||||||
enc = bytearray(72)
|
enc = bytearray(72)
|
||||||
@ -327,7 +321,7 @@ async def kt_accept_values(dtype, raw):
|
|||||||
# it's an XPRV, but in binary.. some extra data we throw away here; sigh
|
# it's an XPRV, but in binary.. some extra data we throw away here; sigh
|
||||||
# XXX no way to send this .. but was thinking of address explorer
|
# XXX no way to send this .. but was thinking of address explorer
|
||||||
txt = ngu.codecs.b58_encode(raw)
|
txt = ngu.codecs.b58_encode(raw)
|
||||||
node, ch, _, _ = chains.slip132_deserialize(txt)
|
node, ch, _, _ = chains.slip32_deserialize(txt)
|
||||||
assert ch.name == chains.current_chain().name, 'wrong chain'
|
assert ch.name == chains.current_chain().name, 'wrong chain'
|
||||||
enc = SecretStash.encode(xprv=node)
|
enc = SecretStash.encode(xprv=node)
|
||||||
|
|
||||||
@ -343,20 +337,15 @@ async def kt_accept_values(dtype, raw):
|
|||||||
|
|
||||||
# This will take over UX w/ the signing process
|
# This will take over UX w/ the signing process
|
||||||
# flags=None --> whether to finalize is decided based on psbt.is_complete
|
# flags=None --> whether to finalize is decided based on psbt.is_complete
|
||||||
sign_transaction(psbt_len, flags=None, input_method="kt")
|
sign_transaction(psbt_len, flags=None)
|
||||||
return
|
return
|
||||||
|
|
||||||
elif dtype == 'b':
|
elif dtype == 'b':
|
||||||
# full system backup, including master: text lines
|
# full system backup, including master: text lines
|
||||||
from backups import text_bk_parser, restore_tmp_from_dict_ll, restore_from_dict, extract_raw_secret
|
from backups import text_bk_parser, restore_tmp_from_dict_ll, restore_from_dict
|
||||||
|
|
||||||
try:
|
vals = text_bk_parser(raw)
|
||||||
vals = text_bk_parser(raw)
|
assert vals # empty?
|
||||||
assert vals # empty?
|
|
||||||
raw_sec, _ = extract_raw_secret(vals)
|
|
||||||
except Exception as e:
|
|
||||||
await ux_show_story("Invalid backup\n\n" + str(e), title='FAILED')
|
|
||||||
return
|
|
||||||
|
|
||||||
from flow import has_secrets
|
from flow import has_secrets
|
||||||
|
|
||||||
@ -365,10 +354,10 @@ async def kt_accept_values(dtype, raw):
|
|||||||
# need to remove key before I get into tmp seed settings
|
# need to remove key before I get into tmp seed settings
|
||||||
# so even if this errors out, new ktrx is needed
|
# so even if this errors out, new ktrx is needed
|
||||||
settings.remove_key("ktrx")
|
settings.remove_key("ktrx")
|
||||||
prob = await restore_tmp_from_dict_ll(vals, raw_sec)
|
prob = await restore_tmp_from_dict_ll(vals)
|
||||||
else:
|
else:
|
||||||
# we have no secret, so... reboot if it works, else errors shown, etc.
|
# we have no secret, so... reboot if it works, else errors shown, etc.
|
||||||
prob = await restore_from_dict(vals, raw_sec)
|
prob = await restore_from_dict(vals)
|
||||||
|
|
||||||
if prob:
|
if prob:
|
||||||
await ux_show_story(prob, title='FAILED')
|
await ux_show_story(prob, title='FAILED')
|
||||||
@ -486,12 +475,6 @@ def decode_step2(session_key, noid_key, body):
|
|||||||
async def kt_incoming(type_code, payload):
|
async def kt_incoming(type_code, payload):
|
||||||
# incoming BBQr was scanned (via main menu, etc)
|
# incoming BBQr was scanned (via main menu, etc)
|
||||||
|
|
||||||
from pincodes import pa
|
|
||||||
if pa.hobbled_mode and type_code != 'E':
|
|
||||||
# only PSBT rx is supported in hobbled mode
|
|
||||||
# fail silently, this is second check, see decoders.py
|
|
||||||
return
|
|
||||||
|
|
||||||
if type_code == 'R':
|
if type_code == 'R':
|
||||||
# they want to send to this guy
|
# they want to send to this guy
|
||||||
return await kt_start_send(payload)
|
return await kt_start_send(payload)
|
||||||
@ -512,10 +495,6 @@ class SecretPickerMenu(MenuSystem):
|
|||||||
def __init__(self, rx_pubkey):
|
def __init__(self, rx_pubkey):
|
||||||
self.rx_pubkey = rx_pubkey
|
self.rx_pubkey = rx_pubkey
|
||||||
|
|
||||||
# this menu should be unreachable in hobbled mode.
|
|
||||||
from pincodes import pa
|
|
||||||
assert not pa.hobbled_mode
|
|
||||||
|
|
||||||
from flow import word_based_seed, is_tmp, has_se_secrets
|
from flow import word_based_seed, is_tmp, has_se_secrets
|
||||||
has_notes = bool(NoteContentBase.count())
|
has_notes = bool(NoteContentBase.count())
|
||||||
has_sv = bool(settings.get('seedvault', False))
|
has_sv = bool(settings.get('seedvault', False))
|
||||||
@ -641,7 +620,7 @@ class SecretPickerMenu(MenuSystem):
|
|||||||
await kt_do_send(self.rx_pubkey, 's', raw=raw)
|
await kt_do_send(self.rx_pubkey, 's', raw=raw)
|
||||||
|
|
||||||
|
|
||||||
async def kt_send_psbt(psbt, psbt_len, psbt_offset):
|
async def kt_send_psbt(psbt, psbt_len):
|
||||||
# We just finished adding our signature to an incomplete PSBT.
|
# We just finished adding our signature to an incomplete PSBT.
|
||||||
# User wants to send to one or more other senders for them to complete signing.
|
# User wants to send to one or more other senders for them to complete signing.
|
||||||
|
|
||||||
@ -656,8 +635,10 @@ async def kt_send_psbt(psbt, psbt_len, psbt_offset):
|
|||||||
await ux_show_story("No more signers?")
|
await ux_show_story("No more signers?")
|
||||||
return
|
return
|
||||||
|
|
||||||
# (TXN_OUTPUT_OFFSET after signing, TXN_INPUT_OFFSET for the file-teleport path)
|
# move out of PSRAM
|
||||||
with SFFile(psbt_offset, psbt_len) as fd:
|
from auth import TXN_OUTPUT_OFFSET
|
||||||
|
|
||||||
|
with SFFile(TXN_OUTPUT_OFFSET, psbt_len) as fd:
|
||||||
bin_psbt = fd.read(psbt_len)
|
bin_psbt = fd.read(psbt_len)
|
||||||
|
|
||||||
my_xfp = settings.get('xfp')
|
my_xfp = settings.get('xfp')
|
||||||
@ -685,12 +666,12 @@ async def kt_send_psbt(psbt, psbt_len, psbt_offset):
|
|||||||
f = None
|
f = None
|
||||||
if x in need:
|
if x in need:
|
||||||
# we haven't signed ourselves yet, so allow that
|
# we haven't signed ourselves yet, so allow that
|
||||||
from auth import sign_transaction
|
from auth import sign_transaction, TXN_INPUT_OFFSET
|
||||||
|
|
||||||
async def sign_now(*a):
|
async def sign_now(*a):
|
||||||
# this will reset the UX stack:
|
# this will reset the UX stack:
|
||||||
# flags=None --> whether to finalize is decided based on psbt.is_complete
|
# flags=None --> whether to finalize is decided based on psbt.is_complete
|
||||||
sign_transaction(psbt_len, flags=None, input_method="kt", offset=psbt_offset)
|
sign_transaction(psbt_len, flags=None)
|
||||||
|
|
||||||
f = sign_now
|
f = sign_now
|
||||||
|
|
||||||
@ -737,7 +718,7 @@ async def kt_send_file_psbt(*a):
|
|||||||
picked = await import_export_prompt("PSBT", is_import=True, no_nfc=True, no_qr=True)
|
picked = await import_export_prompt("PSBT", is_import=True, no_nfc=True, no_qr=True)
|
||||||
if picked == KEY_CANCEL:
|
if picked == KEY_CANCEL:
|
||||||
return
|
return
|
||||||
choices = await file_picker(suffix='.psbt', min_size=50, ux=False,
|
choices = await file_picker(suffix='psbt', min_size=50, ux=False,
|
||||||
max_size=MAX_TXN_LEN, taster=is_psbt, **picked)
|
max_size=MAX_TXN_LEN, taster=is_psbt, **picked)
|
||||||
if not choices:
|
if not choices:
|
||||||
# error msg already shown
|
# error msg already shown
|
||||||
@ -782,6 +763,6 @@ async def kt_send_file_psbt(*a):
|
|||||||
await ux_show_story("We are not part of this multisig wallet.", "Cannot Teleport PSBT")
|
await ux_show_story("We are not part of this multisig wallet.", "Cannot Teleport PSBT")
|
||||||
return
|
return
|
||||||
|
|
||||||
await kt_send_psbt(psbt, psbt_len=psbt_len, psbt_offset=TXN_INPUT_OFFSET)
|
await kt_send_psbt(psbt, psbt_len=psbt_len)
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
@ -12,7 +12,6 @@ from menu import MenuSystem, MenuItem
|
|||||||
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_enter_number, the_ux
|
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_enter_number, the_ux
|
||||||
from stash import SecretStash
|
from stash import SecretStash
|
||||||
from drv_entro import bip85_derive
|
from drv_entro import bip85_derive
|
||||||
from utils import node_from_privkey
|
|
||||||
|
|
||||||
# see from mk4-bootloader/se2.h
|
# see from mk4-bootloader/se2.h
|
||||||
NUM_TRICKS = const(14)
|
NUM_TRICKS = const(14)
|
||||||
@ -33,7 +32,7 @@ TC_WORD_WALLET = const(0x1000)
|
|||||||
TC_XPRV_WALLET = const(0x0800)
|
TC_XPRV_WALLET = const(0x0800)
|
||||||
TC_DELTA_MODE = const(0x0400)
|
TC_DELTA_MODE = const(0x0400)
|
||||||
TC_REBOOT = const(0x0200)
|
TC_REBOOT = const(0x0200)
|
||||||
TC_FW_DEFINED = const(0x0100)
|
TC_RFU = const(0x0100)
|
||||||
# for our use, not implemented in bootrom
|
# for our use, not implemented in bootrom
|
||||||
TC_BLANK_WALLET = const(0x0080)
|
TC_BLANK_WALLET = const(0x0080)
|
||||||
TC_COUNTDOWN = const(0x0040) # tc_arg = minutes of delay
|
TC_COUNTDOWN = const(0x0040) # tc_arg = minutes of delay
|
||||||
@ -41,10 +40,6 @@ TC_COUNTDOWN = const(0x0040) # tc_arg = minutes of delay
|
|||||||
# tc_args encoding:
|
# tc_args encoding:
|
||||||
# TC_WORD_WALLET -> BIP-85 index, 1001..1003 for 24 words, 2001..2003 for 12-words
|
# TC_WORD_WALLET -> BIP-85 index, 1001..1003 for 24 words, 2001..2003 for 12-words
|
||||||
|
|
||||||
# If TC_FW_DEFINED is true, then we can do anything with this PIN at the firmware
|
|
||||||
# level. First application is to unlock spending stuff.
|
|
||||||
TCA_SP_UNLOCK = const(0x0001) # spending policy unlock
|
|
||||||
|
|
||||||
# special "pin" used as catch-all for wrong pins
|
# special "pin" used as catch-all for wrong pins
|
||||||
WRONG_PIN_CODE = '!p'
|
WRONG_PIN_CODE = '!p'
|
||||||
|
|
||||||
@ -211,14 +206,14 @@ class TrickPinMgmt:
|
|||||||
def update_slot(self, pin, new=False, new_pin=None, tc_flags=None, tc_arg=None, secret=None):
|
def update_slot(self, pin, new=False, new_pin=None, tc_flags=None, tc_arg=None, secret=None):
|
||||||
# create or update a trick pin
|
# create or update a trick pin
|
||||||
# - doesn't support wallet to no-wallet transitions
|
# - doesn't support wallet to no-wallet transitions
|
||||||
#
|
'''
|
||||||
# from pincodes import pa; pa.setup(b'12-12'); pa.login(); from trick_pins import *
|
>>> from pincodes import pa; pa.setup(b'12-12'); pa.login(); from trick_pins import *
|
||||||
#
|
'''
|
||||||
assert isinstance(pin, bytes)
|
assert isinstance(pin, bytes)
|
||||||
|
|
||||||
b, slot = self.get_by_pin(pin)
|
b, slot = self.get_by_pin(pin)
|
||||||
if not slot:
|
if not slot:
|
||||||
assert new, "wrong pin"
|
if not new: raise KeyError("wrong pin")
|
||||||
|
|
||||||
# Making a new entry
|
# Making a new entry
|
||||||
b, slot = make_slot()
|
b, slot = make_slot()
|
||||||
@ -279,10 +274,6 @@ class TrickPinMgmt:
|
|||||||
# put them in order, with "wrong" last
|
# put them in order, with "wrong" last
|
||||||
return sorted(self.tp.keys(), key=lambda i: i if (i != WRONG_PIN_CODE) else 'Z')
|
return sorted(self.tp.keys(), key=lambda i: i if (i != WRONG_PIN_CODE) else 'Z')
|
||||||
|
|
||||||
def define_unlock_pin(self, new_pin):
|
|
||||||
# user is setting the bypass PIN for first time.
|
|
||||||
self.update_slot(new_pin.encode(), new=True, tc_flags=TC_FW_DEFINED, tc_arg=TCA_SP_UNLOCK)
|
|
||||||
|
|
||||||
def was_countdown_pin(self):
|
def was_countdown_pin(self):
|
||||||
# was the trick pin just used? if so how much delay needed (or zero if not)
|
# was the trick pin just used? if so how much delay needed (or zero if not)
|
||||||
from pincodes import pa
|
from pincodes import pa
|
||||||
@ -293,32 +284,6 @@ class TrickPinMgmt:
|
|||||||
else:
|
else:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def was_sp_unlock(self):
|
|
||||||
# was a trick pin just used that enables acess to spending policy?
|
|
||||||
# - ok if it's also a trick PIN .. a wiping bypass for example
|
|
||||||
from pincodes import pa
|
|
||||||
tc_flags, tc_arg = pa.get_tc_values()
|
|
||||||
return bool(tc_flags & TC_FW_DEFINED) and (tc_arg == TCA_SP_UNLOCK)
|
|
||||||
|
|
||||||
def has_sp_unlock(self):
|
|
||||||
# if spending policy defined, this PIN allows adjustment
|
|
||||||
# - not TRICK bypass choices, like ones that wipe
|
|
||||||
# - could be multiple, but only first returned.
|
|
||||||
self.reload()
|
|
||||||
for k, (sn,flags,arg) in self.tp.items():
|
|
||||||
if (flags == TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
|
|
||||||
return k
|
|
||||||
return None
|
|
||||||
|
|
||||||
def delete_sp_unlock_pins(self):
|
|
||||||
# remove all bypass pins, they are done w/ feature
|
|
||||||
self.reload()
|
|
||||||
for k, (sn,flags,arg) in self.tp.items():
|
|
||||||
if (flags & TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
|
|
||||||
self.clear_slots([sn])
|
|
||||||
self.forget_pin(k)
|
|
||||||
|
|
||||||
|
|
||||||
def get_deltamode_pins(self):
|
def get_deltamode_pins(self):
|
||||||
# iterate over all delta-mode PIN's defined.
|
# iterate over all delta-mode PIN's defined.
|
||||||
for k, (sn,flags,args) in self.tp.items():
|
for k, (sn,flags,args) in self.tp.items():
|
||||||
@ -398,7 +363,7 @@ class TrickPinMgmt:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if flags & TC_DELTA_MODE:
|
if flags & TC_DELTA_MODE:
|
||||||
prob, arg = validate_delta_pin(true_pin, pin)
|
prob = validate_delta_pin(true_pin, pin)
|
||||||
if prob:
|
if prob:
|
||||||
# just forget it, no UI here to report issue
|
# just forget it, no UI here to report issue
|
||||||
continue
|
continue
|
||||||
@ -407,16 +372,10 @@ class TrickPinMgmt:
|
|||||||
# might need to construct a BIP-85 or XPRV secret to match
|
# might need to construct a BIP-85 or XPRV secret to match
|
||||||
path, new_secret = construct_duress_secret(flags, arg)
|
path, new_secret = construct_duress_secret(flags, arg)
|
||||||
|
|
||||||
tp.update_slot(pin.encode(), new=True, secret=new_secret,
|
b, slot = tp.update_slot(pin.encode(), new=True,
|
||||||
tc_flags=flags, tc_arg=arg)
|
tc_flags=flags, tc_arg=arg, secret=new_secret)
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def err_unique_pin(pin):
|
|
||||||
# standardized error UX
|
|
||||||
return await ux_show_story(
|
|
||||||
"That PIN (%s) is already in use. All PIN codes must be unique." % pin)
|
|
||||||
|
|
||||||
|
|
||||||
tp = TrickPinMgmt()
|
tp = TrickPinMgmt()
|
||||||
|
|
||||||
@ -561,7 +520,8 @@ class TrickPinMenu(MenuSystem):
|
|||||||
have.remove(existing_pin)
|
have.remove(existing_pin)
|
||||||
|
|
||||||
if (new_pin == self.current_pin) or (new_pin in have):
|
if (new_pin == self.current_pin) or (new_pin in have):
|
||||||
return await tp.err_unique_pin(new_pin)
|
await ux_show_story("That PIN (%s) is already in use. All PIN codes must be unique." % new_pin)
|
||||||
|
return
|
||||||
|
|
||||||
# check if we "forgot" this pin, and read it back if we did.
|
# check if we "forgot" this pin, and read it back if we did.
|
||||||
# - important this is after the above checks so we don't reveal any trick pin used
|
# - important this is after the above checks so we don't reveal any trick pin used
|
||||||
@ -646,9 +606,6 @@ the seed phrase, but still a somewhat riskier mode.
|
|||||||
For this mode only, trick PIN must be same length as true PIN and \
|
For this mode only, trick PIN must be same length as true PIN and \
|
||||||
differ only in final 4 positions (ignoring dash).\
|
differ only in final 4 positions (ignoring dash).\
|
||||||
''', flags=TC_DELTA_MODE),
|
''', flags=TC_DELTA_MODE),
|
||||||
StoryMenuItem('Policy Unlock', "Adds (another?) Spending Policy unlock PIN.", flags=TC_FW_DEFINED, arg=TCA_SP_UNLOCK),
|
|
||||||
StoryMenuItem('Policy Unlock & Wipe' if version.has_qwerty else 'P.U. & Wipe',
|
|
||||||
"Pretends correct Spending Policy unlock PIN given, but silently wipes seed before asking for main PIN.", flags=TC_FW_DEFINED|TC_WIPE, arg=TCA_SP_UNLOCK),
|
|
||||||
]
|
]
|
||||||
m = MenuSystem(FirstMenu)
|
m = MenuSystem(FirstMenu)
|
||||||
m.goto_idx(1)
|
m.goto_idx(1)
|
||||||
@ -694,14 +651,9 @@ setting) the Coldcard will always brick after 13 failed PIN attempts.''')
|
|||||||
the_ux.push(m)
|
the_ux.push(m)
|
||||||
|
|
||||||
async def clear_all(self, m,l,item):
|
async def clear_all(self, m,l,item):
|
||||||
|
|
||||||
if not await ux_confirm("Remove ALL TRICK PIN codes and special wrong-pin handling?"):
|
if not await ux_confirm("Remove ALL TRICK PIN codes and special wrong-pin handling?"):
|
||||||
return
|
return
|
||||||
|
|
||||||
if tp.has_sp_unlock():
|
|
||||||
if not await ux_confirm("You will not be able to bypass spending policy anymore."):
|
|
||||||
return
|
|
||||||
|
|
||||||
if any(tp.get_duress_pins()):
|
if any(tp.get_duress_pins()):
|
||||||
if not await ux_confirm("Any funds on the duress wallet(s) have been moved already?"):
|
if not await ux_confirm("Any funds on the duress wallet(s) have been moved already?"):
|
||||||
return
|
return
|
||||||
@ -710,7 +662,7 @@ setting) the Coldcard will always brick after 13 failed PIN attempts.''')
|
|||||||
m.update_contents()
|
m.update_contents()
|
||||||
|
|
||||||
async def hide_pin(self, m,l, item):
|
async def hide_pin(self, m,l, item):
|
||||||
pin, slot_num, flags, arg = item.arg
|
pin, slot_num, flags = item.arg
|
||||||
|
|
||||||
if flags & TC_DELTA_MODE:
|
if flags & TC_DELTA_MODE:
|
||||||
await ux_show_story('''Delta mode PIN will be hidden if trick PIN menu is shown \
|
await ux_show_story('''Delta mode PIN will be hidden if trick PIN menu is shown \
|
||||||
@ -718,14 +670,12 @@ to attacker, and we need to update this record if the main PIN is changed, so we
|
|||||||
hiding this item.''')
|
hiding this item.''')
|
||||||
return
|
return
|
||||||
|
|
||||||
if (flags == TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
|
if pin != WRONG_PIN_CODE:
|
||||||
msg = "It will still be possible to change or disable the spending policy if this PIN is known."
|
|
||||||
elif pin == WRONG_PIN_CODE:
|
|
||||||
msg = "This will hide what happens with wrong PINs from the menus but it will still be in effect."
|
|
||||||
else:
|
|
||||||
msg = '''This will hide the PIN from the menus but it will still be in effect.
|
msg = '''This will hide the PIN from the menus but it will still be in effect.
|
||||||
|
|
||||||
You can restore it by trying to re-add the same PIN (%s) again later.''' % pin
|
You can restore it by trying to re-add the same PIN (%s) again later.''' % pin
|
||||||
|
else:
|
||||||
|
msg = "This will hide what happens with wrong PINs from the menus but it will still be in effect."
|
||||||
|
|
||||||
if not await ux_confirm(msg): return
|
if not await ux_confirm(msg): return
|
||||||
|
|
||||||
@ -765,16 +715,12 @@ You can restore it by trying to re-add the same PIN (%s) again later.''' % pin
|
|||||||
await ux_show_story("Failed: %s" % exc)
|
await ux_show_story("Failed: %s" % exc)
|
||||||
|
|
||||||
async def delete_pin(self, m,l, item):
|
async def delete_pin(self, m,l, item):
|
||||||
pin, slot_num, flags, arg = item.arg
|
pin, slot_num, flags = item.arg
|
||||||
|
|
||||||
if flags & (TC_WORD_WALLET | TC_XPRV_WALLET):
|
if flags & (TC_WORD_WALLET | TC_XPRV_WALLET):
|
||||||
if not await ux_confirm("Any funds on this duress wallet have been moved already?"):
|
if not await ux_confirm("Any funds on this duress wallet have been moved already?"):
|
||||||
return
|
return
|
||||||
|
|
||||||
if (flags == TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
|
|
||||||
if not await ux_confirm("Changes to the spending policy will not be possible anymore."):
|
|
||||||
return
|
|
||||||
|
|
||||||
if pin == WRONG_PIN_CODE:
|
if pin == WRONG_PIN_CODE:
|
||||||
msg = "Remove special handling of wrong PINs?"
|
msg = "Remove special handling of wrong PINs?"
|
||||||
else:
|
else:
|
||||||
@ -802,7 +748,8 @@ You can restore it by trying to re-add the same PIN (%s) again later.''' % pin
|
|||||||
|
|
||||||
ch = await ux_show_story('''\
|
ch = await ux_show_story('''\
|
||||||
This will temporarily load the secrets associated with this trick wallet \
|
This will temporarily load the secrets associated with this trick wallet \
|
||||||
so you may perform transactions with it.''')
|
so you may perform transactions with it. Reboot the Coldcard to restore \
|
||||||
|
normal operation.''')
|
||||||
if ch != 'y': return
|
if ch != 'y': return
|
||||||
|
|
||||||
b, slot = tp.get_by_pin(pin)
|
b, slot = tp.get_by_pin(pin)
|
||||||
@ -898,8 +845,9 @@ Wallet is XPRV-based and derived from a fixed path.''' % pin
|
|||||||
|
|
||||||
assert s.tc_flags == flags
|
assert s.tc_flags == flags
|
||||||
if flags & TC_XPRV_WALLET:
|
if flags & TC_XPRV_WALLET:
|
||||||
|
node = ngu.hdnode.HDNode()
|
||||||
ch, pk = s.xdata[0:32], s.xdata[32:64]
|
ch, pk = s.xdata[0:32], s.xdata[32:64]
|
||||||
node = node_from_privkey(pk, ch)
|
node.from_chaincode_privkey(ch, pk)
|
||||||
|
|
||||||
title, msg, *_ = render_master_secrets('xprv', None, node)
|
title, msg, *_ = render_master_secrets('xprv', None, node)
|
||||||
elif flags & TC_WORD_WALLET:
|
elif flags & TC_WORD_WALLET:
|
||||||
@ -934,8 +882,6 @@ Wallet is XPRV-based and derived from a fixed path.''' % pin
|
|||||||
rv.append(MenuItem("↳Pretends Wrong"))
|
rv.append(MenuItem("↳Pretends Wrong"))
|
||||||
elif flags & TC_DELTA_MODE:
|
elif flags & TC_DELTA_MODE:
|
||||||
rv.append(MenuItem("↳Delta Mode"))
|
rv.append(MenuItem("↳Delta Mode"))
|
||||||
elif (flags & TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
|
|
||||||
rv.append(MenuItem("↳Unlock Policy")) # width issues on Mk4
|
|
||||||
|
|
||||||
for m, msg in [
|
for m, msg in [
|
||||||
(TC_WIPE, '↳Wipes seed'),
|
(TC_WIPE, '↳Wipes seed'),
|
||||||
@ -949,8 +895,8 @@ Wallet is XPRV-based and derived from a fixed path.''' % pin
|
|||||||
rv.append(MenuItem("Activate Wallet", f=self.activate_wallet, arg=(pin, flags, arg)))
|
rv.append(MenuItem("Activate Wallet", f=self.activate_wallet, arg=(pin, flags, arg)))
|
||||||
|
|
||||||
rv.extend([
|
rv.extend([
|
||||||
MenuItem('Hide Trick', f=self.hide_pin, arg=(pin, slot_num, flags, arg)),
|
MenuItem('Hide Trick', f=self.hide_pin, arg=(pin, slot_num, flags)),
|
||||||
MenuItem('Delete Trick', f=self.delete_pin, arg=(pin, slot_num, flags, arg)),
|
MenuItem('Delete Trick', f=self.delete_pin, arg=(pin, slot_num, flags)),
|
||||||
])
|
])
|
||||||
if pin != WRONG_PIN_CODE:
|
if pin != WRONG_PIN_CODE:
|
||||||
rv.append(
|
rv.append(
|
||||||
@ -961,7 +907,6 @@ Wallet is XPRV-based and derived from a fixed path.''' % pin
|
|||||||
|
|
||||||
class StoryMenuItem(MenuItem):
|
class StoryMenuItem(MenuItem):
|
||||||
def __init__(self, label, story, flags=0, **kws):
|
def __init__(self, label, story, flags=0, **kws):
|
||||||
# arg= .. handled by super
|
|
||||||
self.story = story
|
self.story = story
|
||||||
self.flags = flags
|
self.flags = flags
|
||||||
super().__init__(label, **kws)
|
super().__init__(label, **kws)
|
||||||
|
|||||||
@ -11,8 +11,7 @@ from ustruct import pack, unpack_from
|
|||||||
from ckcc import watchpoint, is_simulator
|
from ckcc import watchpoint, is_simulator
|
||||||
from utils import problem_file_line, call_later_ms
|
from utils import problem_file_line, call_later_ms
|
||||||
from version import supports_hsm, is_devmode, MAX_TXN_LEN, MAX_UPLOAD_LEN
|
from version import supports_hsm, is_devmode, MAX_TXN_LEN, MAX_UPLOAD_LEN
|
||||||
from exceptions import FramingError, CCBusyError, HSMDenied, HSMCMDDisabled, SpendPolicyViolation
|
from exceptions import FramingError, CCBusyError, HSMDenied, HSMCMDDisabled
|
||||||
from pincodes import pa
|
|
||||||
|
|
||||||
# Unofficial, unpermissioned... numbers
|
# Unofficial, unpermissioned... numbers
|
||||||
COINKITE_VID = 0xd13e
|
COINKITE_VID = 0xd13e
|
||||||
@ -69,14 +68,6 @@ HSM_DISABLE_CMDS = frozenset({
|
|||||||
"hsms",
|
"hsms",
|
||||||
})
|
})
|
||||||
|
|
||||||
# spending policy active: blacklist some commands
|
|
||||||
# - 'pass' may be allowed if 'okeys' is enabled
|
|
||||||
HOBBLED_CMDS = frozenset({
|
|
||||||
'enrl', # no new multisigs during policy enforcement
|
|
||||||
'back', # no backups
|
|
||||||
'bagi', 'dfu_', # just in case
|
|
||||||
}) | HSM_DISABLE_CMDS
|
|
||||||
|
|
||||||
# singleton instance of USBHandler()
|
# singleton instance of USBHandler()
|
||||||
handler = None
|
handler = None
|
||||||
|
|
||||||
@ -226,8 +217,6 @@ class USBHandler:
|
|||||||
except CCBusyError:
|
except CCBusyError:
|
||||||
# auth UX is doing something else
|
# auth UX is doing something else
|
||||||
resp = b'busy'
|
resp = b'busy'
|
||||||
except SpendPolicyViolation:
|
|
||||||
resp = b'err_Spending policy in effect'
|
|
||||||
except HSMDenied:
|
except HSMDenied:
|
||||||
resp = b'err_Not allowed in HSM mode'
|
resp = b'err_Not allowed in HSM mode'
|
||||||
except HSMCMDDisabled:
|
except HSMCMDDisabled:
|
||||||
@ -251,7 +240,7 @@ class USBHandler:
|
|||||||
# catch bugs and fuzzing too
|
# catch bugs and fuzzing too
|
||||||
if is_simulator() or is_devmode:
|
if is_simulator() or is_devmode:
|
||||||
print("USB request caused this: ", end='')
|
print("USB request caused this: ", end='')
|
||||||
sys.print_exception(exc)
|
# sys.print_exception(exc)
|
||||||
resp = b'err_Confused ' + problem_file_line(exc)
|
resp = b'err_Confused ' + problem_file_line(exc)
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
@ -356,7 +345,7 @@ class USBHandler:
|
|||||||
except:
|
except:
|
||||||
raise FramingError('decode')
|
raise FramingError('decode')
|
||||||
|
|
||||||
if is_devmode and cmd[0].isupper():
|
if cmd[0].isupper() and is_devmode:
|
||||||
# special hacky commands to support testing w/ the simulator
|
# special hacky commands to support testing w/ the simulator
|
||||||
try:
|
try:
|
||||||
from usb_test_commands import do_usb_command
|
from usb_test_commands import do_usb_command
|
||||||
@ -369,18 +358,7 @@ class USBHandler:
|
|||||||
if cmd not in HSM_WHITELIST:
|
if cmd not in HSM_WHITELIST:
|
||||||
raise HSMDenied
|
raise HSMDenied
|
||||||
|
|
||||||
if pa.hobbled_mode:
|
if not settings.get('hsmcmd', False):
|
||||||
# block some commands when we are hobbled.
|
|
||||||
if cmd in HOBBLED_CMDS:
|
|
||||||
raise SpendPolicyViolation
|
|
||||||
|
|
||||||
if cmd in {'pwok', 'pass'}:
|
|
||||||
from ccc import sssp_spending_policy
|
|
||||||
if not sssp_spending_policy('okeys'):
|
|
||||||
raise SpendPolicyViolation
|
|
||||||
|
|
||||||
elif not settings.get('hsmcmd', False):
|
|
||||||
# block these HSM-related command if not using feature
|
|
||||||
if cmd in HSM_DISABLE_CMDS:
|
if cmd in HSM_DISABLE_CMDS:
|
||||||
raise HSMCMDDisabled
|
raise HSMCMDDisabled
|
||||||
|
|
||||||
@ -416,12 +394,10 @@ class USBHandler:
|
|||||||
|
|
||||||
if cmd == 'dwld':
|
if cmd == 'dwld':
|
||||||
offset, length, fileno = unpack_from('<III', args)
|
offset, length, fileno = unpack_from('<III', args)
|
||||||
assert len(args) == 12, 'badlen'
|
|
||||||
return await self.handle_download(offset, length, fileno)
|
return await self.handle_download(offset, length, fileno)
|
||||||
|
|
||||||
if cmd == 'ncry':
|
if cmd == 'ncry':
|
||||||
version, his_pubkey = unpack_from('<I64s', args)
|
version, his_pubkey = unpack_from('<I64s', args)
|
||||||
assert len(args) == 68, 'badlen'
|
|
||||||
|
|
||||||
return self.handle_crypto_setup(version, his_pubkey)
|
return self.handle_crypto_setup(version, his_pubkey)
|
||||||
|
|
||||||
@ -451,9 +427,9 @@ class USBHandler:
|
|||||||
if cmd == 'smsg':
|
if cmd == 'smsg':
|
||||||
# sign message
|
# sign message
|
||||||
addr_fmt, len_subpath, len_msg = unpack_from('<III', args)
|
addr_fmt, len_subpath, len_msg = unpack_from('<III', args)
|
||||||
assert len(args) == (12 + len_subpath + len_msg), 'badlen'
|
|
||||||
subpath = args[12:12+len_subpath]
|
subpath = args[12:12+len_subpath]
|
||||||
msg = args[12+len_subpath:]
|
msg = args[12+len_subpath:]
|
||||||
|
assert len(msg) == len_msg, "badlen"
|
||||||
|
|
||||||
from auth import sign_msg
|
from auth import sign_msg
|
||||||
sign_msg(msg, subpath, addr_fmt)
|
sign_msg(msg, subpath, addr_fmt)
|
||||||
@ -482,7 +458,6 @@ class USBHandler:
|
|||||||
|
|
||||||
xfp_paths = []
|
xfp_paths = []
|
||||||
for i in range(N):
|
for i in range(N):
|
||||||
assert offset < len(args), 'badlen'
|
|
||||||
ln = args[offset]
|
ln = args[offset]
|
||||||
assert 1 <= ln <= 16, 'badlen'
|
assert 1 <= ln <= 16, 'badlen'
|
||||||
xfp_paths.append(unpack_from('<%dI' % ln, args, offset+1))
|
xfp_paths.append(unpack_from('<%dI' % ln, args, offset+1))
|
||||||
@ -498,7 +473,6 @@ class USBHandler:
|
|||||||
from auth import usb_show_address
|
from auth import usb_show_address
|
||||||
|
|
||||||
addr_fmt, = unpack_from('<I', args)
|
addr_fmt, = unpack_from('<I', args)
|
||||||
assert len(args) >= 4, 'badlen'
|
|
||||||
# regression patch of AFC_BECH32M flag
|
# regression patch of AFC_BECH32M flag
|
||||||
# fixed here https://github.com/Coldcard/ckcc-protocol/commit/a6d901f9fca50755835eca895586ca74d0ca81ed
|
# fixed here https://github.com/Coldcard/ckcc-protocol/commit/a6d901f9fca50755835eca895586ca74d0ca81ed
|
||||||
if addr_fmt == 0x17: # old P2TR
|
if addr_fmt == 0x17: # old P2TR
|
||||||
@ -510,7 +484,6 @@ class USBHandler:
|
|||||||
# - text config file must already be uploaded
|
# - text config file must already be uploaded
|
||||||
|
|
||||||
file_len, file_sha = unpack_from('<I32s', args)
|
file_len, file_sha = unpack_from('<I32s', args)
|
||||||
assert len(args) == 36, 'badlen'
|
|
||||||
if file_sha != self.file_checksum.digest():
|
if file_sha != self.file_checksum.digest():
|
||||||
return b'err_Checksum'
|
return b'err_Checksum'
|
||||||
assert 100 < file_len <= (20*200), "badlen"
|
assert 100 < file_len <= (20*200), "badlen"
|
||||||
@ -525,20 +498,19 @@ class USBHandler:
|
|||||||
# Quick check to test if we have a wallet already installed.
|
# Quick check to test if we have a wallet already installed.
|
||||||
from multisig import MultisigWallet
|
from multisig import MultisigWallet
|
||||||
M, N, xfp_xor = unpack_from('<3I', args)
|
M, N, xfp_xor = unpack_from('<3I', args)
|
||||||
assert len(args) == 12, 'badlen'
|
|
||||||
return int(MultisigWallet.quick_check(M, N, xfp_xor))
|
return int(MultisigWallet.quick_check(M, N, xfp_xor))
|
||||||
|
|
||||||
if cmd == 'stxn':
|
if cmd == 'stxn':
|
||||||
# sign transaction
|
# sign transaction
|
||||||
txn_len, flags, txn_sha = unpack_from('<II32s', args)
|
txn_len, flags, txn_sha = unpack_from('<II32s', args)
|
||||||
assert len(args) == 40, 'badlen'
|
|
||||||
if txn_sha != self.file_checksum.digest():
|
if txn_sha != self.file_checksum.digest():
|
||||||
return b'err_Checksum'
|
return b'err_Checksum'
|
||||||
|
|
||||||
assert 50 < txn_len <= MAX_TXN_LEN, "badlen"
|
assert 50 < txn_len <= MAX_TXN_LEN, "badlen"
|
||||||
|
|
||||||
from auth import sign_transaction
|
from auth import sign_transaction
|
||||||
sign_transaction(txn_len, (flags & STXN_FLAGS_MASK), txn_sha, input_method="usb")
|
sign_transaction(txn_len, (flags & STXN_FLAGS_MASK), txn_sha)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if cmd == 'stok' or cmd == 'bkok' or cmd == 'smok' or cmd == 'pwok':
|
if cmd == 'stok' or cmd == 'bkok' or cmd == 'smok' or cmd == 'pwok':
|
||||||
@ -563,6 +535,7 @@ class USBHandler:
|
|||||||
# STILL waiting on user
|
# STILL waiting on user
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
if cmd == 'pwok':
|
if cmd == 'pwok':
|
||||||
# return new root xpub
|
# return new root xpub
|
||||||
xpub = req.result
|
xpub = req.result
|
||||||
@ -588,7 +561,6 @@ class USBHandler:
|
|||||||
assert settings.get("words", True), 'no seed'
|
assert settings.get("words", True), 'no seed'
|
||||||
assert len(args) < 400, 'too long'
|
assert len(args) < 400, 'too long'
|
||||||
pw = str(args, 'utf8')
|
pw = str(args, 'utf8')
|
||||||
assert len(pw), 'too short'
|
|
||||||
assert len(pw) < 100, 'too long'
|
assert len(pw) < 100, 'too long'
|
||||||
|
|
||||||
return start_bip39_passphrase(pw)
|
return start_bip39_passphrase(pw)
|
||||||
@ -598,17 +570,6 @@ class USBHandler:
|
|||||||
from auth import start_remote_backup
|
from auth import start_remote_backup
|
||||||
return start_remote_backup()
|
return start_remote_backup()
|
||||||
|
|
||||||
if cmd == 'rest':
|
|
||||||
# restore backup from what is already uploaded in PSRAM
|
|
||||||
file_len, file_sha, bf = unpack_from('<I32sB', args)
|
|
||||||
assert len(args) == 37, 'badlen'
|
|
||||||
assert 0 < file_len <= MAX_TXN_LEN, "badlen"
|
|
||||||
if file_sha != self.file_checksum.digest():
|
|
||||||
return b'err_Checksum'
|
|
||||||
|
|
||||||
from auth import start_remote_restore_backup
|
|
||||||
return start_remote_restore_backup(file_len, bf)
|
|
||||||
|
|
||||||
if cmd == 'blkc':
|
if cmd == 'blkc':
|
||||||
# report which blockchain we are configured for
|
# report which blockchain we are configured for
|
||||||
from chains import current_chain
|
from chains import current_chain
|
||||||
@ -625,7 +586,6 @@ class USBHandler:
|
|||||||
# HSM mode "start" -- requires user approval
|
# HSM mode "start" -- requires user approval
|
||||||
if args:
|
if args:
|
||||||
file_len, file_sha = unpack_from('<I32s', args)
|
file_len, file_sha = unpack_from('<I32s', args)
|
||||||
assert len(args) == 36, 'badlen'
|
|
||||||
if file_sha != self.file_checksum.digest():
|
if file_sha != self.file_checksum.digest():
|
||||||
return b'err_Checksum'
|
return b'err_Checksum'
|
||||||
assert 2 <= file_len <= (200*1000), "badlen"
|
assert 2 <= file_len <= (200*1000), "badlen"
|
||||||
@ -653,8 +613,6 @@ class USBHandler:
|
|||||||
if cmd == 'nwur': # new user
|
if cmd == 'nwur': # new user
|
||||||
from users import Users
|
from users import Users
|
||||||
auth_mode, ul, sl = unpack_from('<BBB', args)
|
auth_mode, ul, sl = unpack_from('<BBB', args)
|
||||||
assert len(args) == (3 + ul + sl), 'badlen'
|
|
||||||
assert ul and sl, "badlen"
|
|
||||||
username = bytes(args[3:3+ul]).decode('ascii')
|
username = bytes(args[3:3+ul]).decode('ascii')
|
||||||
secret = bytes(args[3+ul:3+ul+sl])
|
secret = bytes(args[3+ul:3+ul+sl])
|
||||||
|
|
||||||
@ -663,8 +621,6 @@ class USBHandler:
|
|||||||
if cmd == 'rmur': # delete user
|
if cmd == 'rmur': # delete user
|
||||||
from users import Users
|
from users import Users
|
||||||
ul, = unpack_from('<B', args)
|
ul, = unpack_from('<B', args)
|
||||||
assert len(args) == (1 + ul), 'badlen'
|
|
||||||
assert ul, "badlen"
|
|
||||||
username = bytes(args[1:1+ul]).decode('ascii')
|
username = bytes(args[1:1+ul]).decode('ascii')
|
||||||
|
|
||||||
return Users.delete(username)
|
return Users.delete(username)
|
||||||
@ -672,8 +628,6 @@ class USBHandler:
|
|||||||
if cmd == 'user': # auth user (HSM mode)
|
if cmd == 'user': # auth user (HSM mode)
|
||||||
from users import Users
|
from users import Users
|
||||||
totp_time, ul, tl = unpack_from('<IBB', args)
|
totp_time, ul, tl = unpack_from('<IBB', args)
|
||||||
assert len(args) == (6 + ul + tl), 'badlen'
|
|
||||||
assert ul and tl, "badlen"
|
|
||||||
username = bytes(args[6:6+ul]).decode('ascii')
|
username = bytes(args[6:6+ul]).decode('ascii')
|
||||||
token = bytes(args[6+ul:6+ul+tl])
|
token = bytes(args[6+ul:6+ul+tl])
|
||||||
|
|
||||||
@ -762,8 +716,7 @@ class USBHandler:
|
|||||||
length = min(length, MAX_BLK_LEN)
|
length = min(length, MAX_BLK_LEN)
|
||||||
|
|
||||||
assert 0 <= file_number < 2, 'bad fnum'
|
assert 0 <= file_number < 2, 'bad fnum'
|
||||||
assert 0 <= offset < MAX_TXN_LEN, "bad offset"
|
assert 0 <= offset <= MAX_TXN_LEN, "bad offset"
|
||||||
assert offset + length <= MAX_TXN_LEN, "bad offset"
|
|
||||||
assert 1 <= length, 'len'
|
assert 1 <= length, 'len'
|
||||||
|
|
||||||
# maintain a running SHA256 over what's sent
|
# maintain a running SHA256 over what's sent
|
||||||
@ -788,6 +741,7 @@ class USBHandler:
|
|||||||
from glob import dis, hsm_active
|
from glob import dis, hsm_active
|
||||||
from utils import check_firmware_hdr
|
from utils import check_firmware_hdr
|
||||||
from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE, FW_HEADER_MAGIC
|
from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE, FW_HEADER_MAGIC
|
||||||
|
from pincodes import pa
|
||||||
|
|
||||||
# maintain a running SHA256 over what's received
|
# maintain a running SHA256 over what's received
|
||||||
if offset == 0:
|
if offset == 0:
|
||||||
@ -798,11 +752,10 @@ class USBHandler:
|
|||||||
dis.progress_sofar(offset, total_size)
|
dis.progress_sofar(offset, total_size)
|
||||||
|
|
||||||
assert offset % 256 == 0, 'alignment'
|
assert offset % 256 == 0, 'alignment'
|
||||||
assert 1 <= total_size <= MAX_UPLOAD_LEN, 'long'
|
assert offset+len(data) <= total_size <= MAX_UPLOAD_LEN, 'long'
|
||||||
assert offset + len(data) <= total_size, 'long'
|
|
||||||
|
|
||||||
if hsm_active or pa.hobbled_mode:
|
if hsm_active:
|
||||||
# additional restriction in HSM mode or hobbled: must be PSBT
|
# additional restrictions in HSM mode
|
||||||
assert offset+len(data) <= total_size <= MAX_TXN_LEN, 'psbt'
|
assert offset+len(data) <= total_size <= MAX_TXN_LEN, 'psbt'
|
||||||
if offset == 0:
|
if offset == 0:
|
||||||
assert data[0:5] == b'psbt\xff', 'psbt'
|
assert data[0:5] == b'psbt\xff', 'psbt'
|
||||||
@ -881,6 +834,7 @@ class USBHandler:
|
|||||||
def handle_bag_number(self, bag_num):
|
def handle_bag_number(self, bag_num):
|
||||||
import version, callgate
|
import version, callgate
|
||||||
from glob import dis, settings
|
from glob import dis, settings
|
||||||
|
from pincodes import pa
|
||||||
|
|
||||||
if bag_num and version.is_factory_mode and not version.has_qr:
|
if bag_num and version.is_factory_mode and not version.has_qr:
|
||||||
# check state first
|
# check state first
|
||||||
|
|||||||
@ -78,7 +78,7 @@ KEY = 'usr'
|
|||||||
UserInfo = namedtuple('UserInfo', 'auth_mode secret last_counter')
|
UserInfo = namedtuple('UserInfo', 'auth_mode secret last_counter')
|
||||||
|
|
||||||
class Users:
|
class Users:
|
||||||
'''Track users and their TOTP secrets or hashed passwords'''
|
'''Track users and thier TOTP secrets or hashed passwords'''
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls):
|
def get(cls):
|
||||||
|
|||||||
@ -6,9 +6,9 @@ import gc, sys, ustruct, ngu, chains, ure, uos, uio, time, bip39, version, uasyn
|
|||||||
from ubinascii import unhexlify as a2b_hex
|
from ubinascii import unhexlify as a2b_hex
|
||||||
from ubinascii import hexlify as b2a_hex
|
from ubinascii import hexlify as b2a_hex
|
||||||
from ubinascii import a2b_base64, b2a_base64
|
from ubinascii import a2b_base64, b2a_base64
|
||||||
from charcodes import OUT_CTRL_ADDRESS, OUT_CTRL_NOWRAP
|
from charcodes import OUT_CTRL_ADDRESS
|
||||||
from uhashlib import sha256
|
from uhashlib import sha256
|
||||||
from public_constants import MAX_PATH_DEPTH, AF_CLASSIC, AF_P2SH, AF_P2WPKH, AF_P2WSH, AF_P2TR
|
from public_constants import MAX_PATH_DEPTH, AF_CLASSIC
|
||||||
|
|
||||||
B2A = lambda x: str(b2a_hex(x), 'ascii')
|
B2A = lambda x: str(b2a_hex(x), 'ascii')
|
||||||
|
|
||||||
@ -193,31 +193,34 @@ def str2xfp(txt):
|
|||||||
# Inverse of xfp2str
|
# Inverse of xfp2str
|
||||||
return ustruct.unpack('<I', a2b_hex(txt))[0]
|
return ustruct.unpack('<I', a2b_hex(txt))[0]
|
||||||
|
|
||||||
|
def is_ascii(s):
|
||||||
|
if len(s) == len(s.encode()):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def is_printable(s):
|
def is_printable(s):
|
||||||
|
PRINTABLE = range(32, 127)
|
||||||
for ch in s:
|
for ch in s:
|
||||||
o = ord(ch)
|
if ord(ch) not in PRINTABLE:
|
||||||
if o < 32 or o > 126:
|
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def to_ascii_printable(s, allow_tab_nl=False):
|
def to_ascii_printable(s, strip=False, only_printable=True):
|
||||||
try:
|
try:
|
||||||
# s must be a string!
|
s = str(s, 'ascii')
|
||||||
assert len(s) == len(s.encode())
|
if strip:
|
||||||
if not allow_tab_nl:
|
s = s.strip()
|
||||||
|
assert is_ascii(s)
|
||||||
|
if only_printable:
|
||||||
assert is_printable(s)
|
assert is_printable(s)
|
||||||
else:
|
|
||||||
for ch in s:
|
|
||||||
o = ord(ch)
|
|
||||||
assert 32 <= o <= 126 or o == 9 or o == 10
|
|
||||||
return s
|
return s
|
||||||
except:
|
except:
|
||||||
err = "must be ascii printable" + (", tab, or newline" if allow_tab_nl else "")
|
raise AssertionError("must be ascii" + (" printable" if only_printable else ""))
|
||||||
raise AssertionError(err)
|
|
||||||
|
|
||||||
def problem_file_line(exc):
|
def problem_file_line(exc):
|
||||||
# return a string of just the filename.py and line number where
|
# return a string of just the filename.py and line number where
|
||||||
# an exception occurred. Best used on AssertionError.
|
# an exception occured. Best used on AssertionError.
|
||||||
|
|
||||||
tmp = uio.StringIO()
|
tmp = uio.StringIO()
|
||||||
sys.print_exception(exc, tmp)
|
sys.print_exception(exc, tmp)
|
||||||
@ -249,7 +252,7 @@ def cleanup_deriv_path(bin_path, allow_star=False):
|
|||||||
# - do not assume /// is m/0/0/0
|
# - do not assume /// is m/0/0/0
|
||||||
# - if allow_star, then final position can be * or *h (wildcard)
|
# - if allow_star, then final position can be * or *h (wildcard)
|
||||||
|
|
||||||
s = to_ascii_printable(str(bin_path, "ascii").strip()).lower()
|
s = to_ascii_printable(bin_path, strip=True).lower()
|
||||||
|
|
||||||
# empty string is valid
|
# empty string is valid
|
||||||
if s == '': return 'm'
|
if s == '': return 'm'
|
||||||
@ -380,7 +383,7 @@ def check_firmware_hdr(hdr, binary_size):
|
|||||||
# - hdr must be a bytearray(FW_HEADER_SIZE+more)
|
# - hdr must be a bytearray(FW_HEADER_SIZE+more)
|
||||||
|
|
||||||
from sigheader import FW_HEADER_SIZE, FW_HEADER_MAGIC, FWH_PY_FORMAT
|
from sigheader import FW_HEADER_SIZE, FW_HEADER_MAGIC, FWH_PY_FORMAT
|
||||||
from sigheader import MK_1_OK, MK_2_OK, MK_3_OK, MK_4_OK, MK_5_OK, MK_Q1_OK
|
from sigheader import MK_1_OK, MK_2_OK, MK_3_OK, MK_4_OK, MK_Q1_OK
|
||||||
from ustruct import unpack_from
|
from ustruct import unpack_from
|
||||||
from version import hw_label
|
from version import hw_label
|
||||||
import callgate
|
import callgate
|
||||||
@ -409,8 +412,6 @@ def check_firmware_hdr(hdr, binary_size):
|
|||||||
ok = (hw_compat & MK_3_OK)
|
ok = (hw_compat & MK_3_OK)
|
||||||
elif hw_label == 'mk4':
|
elif hw_label == 'mk4':
|
||||||
ok = (hw_compat & MK_4_OK)
|
ok = (hw_compat & MK_4_OK)
|
||||||
elif hw_label == 'mk5':
|
|
||||||
ok = (hw_compat & MK_5_OK)
|
|
||||||
elif hw_label == 'q1':
|
elif hw_label == 'q1':
|
||||||
ok = (hw_compat & MK_Q1_OK)
|
ok = (hw_compat & MK_Q1_OK)
|
||||||
|
|
||||||
@ -428,8 +429,6 @@ def clean_shutdown(style=0):
|
|||||||
# wipe SPI flash and shutdown (wiping main memory)
|
# wipe SPI flash and shutdown (wiping main memory)
|
||||||
# - mk4: SPI flash not used, but NFC may hold data (PSRAM cleared by bootrom)
|
# - mk4: SPI flash not used, but NFC may hold data (PSRAM cleared by bootrom)
|
||||||
# - bootrom wipes every byte of SRAM, so no need to repeat here
|
# - bootrom wipes every byte of SRAM, so no need to repeat here
|
||||||
# - style=2 => reboot and try login again
|
|
||||||
# - default is logout and (if applicable) power down.
|
|
||||||
import callgate
|
import callgate
|
||||||
|
|
||||||
# save if anything pending
|
# save if anything pending
|
||||||
@ -465,11 +464,6 @@ def call_later_ms(delay, cb, *args, **kws):
|
|||||||
def word_wrap(ln, w):
|
def word_wrap(ln, w):
|
||||||
# Generate the lines needed to wrap one line into X "width"-long lines.
|
# Generate the lines needed to wrap one line into X "width"-long lines.
|
||||||
# - tests in testing/test_unit.py
|
# - tests in testing/test_unit.py
|
||||||
if ln and (ln[0] == OUT_CTRL_NOWRAP):
|
|
||||||
# no need to wrap this line - as requested by caller
|
|
||||||
yield ln[1:]
|
|
||||||
return
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# ln_len considers DOUBLE_WIDTH chars
|
# ln_len considers DOUBLE_WIDTH chars
|
||||||
ln_len = 0
|
ln_len = 0
|
||||||
@ -545,7 +539,7 @@ def parse_extended_key(ln, private=False):
|
|||||||
found = pat.search(ln)
|
found = pat.search(ln)
|
||||||
# serialize, and note version code
|
# serialize, and note version code
|
||||||
try:
|
try:
|
||||||
node, chain, addr_fmt, is_private = chains.slip132_deserialize(found.group(0))
|
node, chain, addr_fmt, is_private = chains.slip32_deserialize(found.group(0))
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -643,10 +637,13 @@ def decode_bip21_text(got):
|
|||||||
|
|
||||||
proto, args, addr = None, None, None
|
proto, args, addr = None, None, None
|
||||||
|
|
||||||
# remove query params first - if any
|
# remove URL protocol: if present
|
||||||
|
if ':' in got:
|
||||||
|
proto, got = got.split(':', 1)
|
||||||
|
|
||||||
# looks like BIP-21 payment URL
|
# looks like BIP-21 payment URL
|
||||||
if '?' in got:
|
if '?' in got:
|
||||||
got, args = got.split('?', 1)
|
addr, args = got.split('?', 1)
|
||||||
|
|
||||||
# full URL decode here, but assuming no repeated keys
|
# full URL decode here, but assuming no repeated keys
|
||||||
parts = args.split('&')
|
parts = args.split('&')
|
||||||
@ -655,12 +652,7 @@ def decode_bip21_text(got):
|
|||||||
k, v = p.split('=', 1)
|
k, v = p.split('=', 1)
|
||||||
args[k] = url_unquote(v)
|
args[k] = url_unquote(v)
|
||||||
|
|
||||||
# remove URL protocol: if present
|
# assume it's an bare address for now
|
||||||
if ':' in got:
|
|
||||||
proto, got = got.split(':', 1)
|
|
||||||
assert proto.lower() == "bitcoin"
|
|
||||||
|
|
||||||
# assume it's a bare address for now
|
|
||||||
if not addr:
|
if not addr:
|
||||||
addr = got
|
addr = got
|
||||||
|
|
||||||
@ -688,35 +680,6 @@ def decode_bip21_text(got):
|
|||||||
|
|
||||||
raise ValueError('not bip-21')
|
raise ValueError('not bip-21')
|
||||||
|
|
||||||
def validate_own_address(addr):
|
|
||||||
ch = chains.current_chain()
|
|
||||||
addr_l = addr.lower()
|
|
||||||
|
|
||||||
if addr_l[:3] in ("bc1", "tb1") or addr_l[:5] == 'bcrt1':
|
|
||||||
try:
|
|
||||||
hrp, witver, data = ngu.codecs.segwit_decode(addr)
|
|
||||||
|
|
||||||
assert hrp == ch.bech32_hrp
|
|
||||||
assert witver == 0
|
|
||||||
if len(data) == 20:
|
|
||||||
return addr_l, AF_P2WPKH
|
|
||||||
if len(data) == 32:
|
|
||||||
return addr_l, AF_P2WSH
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
# Bitcoin main/test/reg base58 address prefixes.
|
|
||||||
elif addr and addr[0] in '123mn':
|
|
||||||
try:
|
|
||||||
raw = ngu.codecs.b58_decode(addr)
|
|
||||||
assert len(raw) == 21
|
|
||||||
if raw[0] == ch.b58_addr[0]:
|
|
||||||
return addr, AF_CLASSIC
|
|
||||||
if raw[0] == ch.b58_script[0]:
|
|
||||||
return addr, AF_P2SH
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
assert False, ch.name
|
|
||||||
|
|
||||||
def encode_seed_qr(words):
|
def encode_seed_qr(words):
|
||||||
return ''.join('%04d' % bip39.get_word_index(w) for w in words)
|
return ''.join('%04d' % bip39.get_word_index(w) for w in words)
|
||||||
|
|
||||||
@ -816,11 +779,4 @@ def extract_cosigner(data, af_str):
|
|||||||
# emulate coldcard export xpubs
|
# emulate coldcard export xpubs
|
||||||
return {"xfp": xfp, af_str: ek, key_deriv: deriv}
|
return {"xfp": xfp, af_str: ek, key_deriv: deriv}
|
||||||
|
|
||||||
|
|
||||||
def node_from_privkey(privkey, chain_code=bytes(32)):
|
|
||||||
return ngu.hdnode.HDNode().from_chaincode_privkey(chain_code, privkey)
|
|
||||||
|
|
||||||
def node_from_pubkey(pubkey, chain_code=bytes(32)):
|
|
||||||
return ngu.hdnode.HDNode().from_chaincode_pubkey(chain_code, pubkey)
|
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
39
shared/ux.py
39
shared/ux.py
@ -7,8 +7,8 @@ from queues import QueueEmpty
|
|||||||
import utime, gc, version
|
import utime, gc, version
|
||||||
from utils import word_wrap
|
from utils import word_wrap
|
||||||
from version import has_qwerty, num_sd_slots, has_qr
|
from version import has_qwerty, num_sd_slots, has_qr
|
||||||
from charcodes import (KEY_UP, KEY_DOWN, KEY_HOME, KEY_NFC, KEY_QR, KEY_END, KEY_PAGE_UP,
|
from charcodes import (KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_NFC, KEY_QR,
|
||||||
KEY_PAGE_DOWN, KEY_ENTER, KEY_CANCEL, OUT_CTRL_TITLE)
|
KEY_END, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_ENTER, KEY_CANCEL, OUT_CTRL_TITLE)
|
||||||
|
|
||||||
from exceptions import AbortInteraction
|
from exceptions import AbortInteraction
|
||||||
|
|
||||||
@ -299,7 +299,7 @@ async def ux_dramatic_pause(msg, seconds):
|
|||||||
# show a full-screen msg, with a dramatic pause + progress bar
|
# show a full-screen msg, with a dramatic pause + progress bar
|
||||||
n = seconds * 8
|
n = seconds * 8
|
||||||
dis.fullscreen(msg)
|
dis.fullscreen(msg)
|
||||||
for i in range(1, n+1):
|
for i in range(n):
|
||||||
dis.progress_bar_show(i/n)
|
dis.progress_bar_show(i/n)
|
||||||
await sleep_ms(125)
|
await sleep_ms(125)
|
||||||
|
|
||||||
@ -349,7 +349,7 @@ async def show_qr_code(data, is_alnum=False, msg=None, **kw):
|
|||||||
o = QRDisplaySingle([data], is_alnum, msg=msg, **kw)
|
o = QRDisplaySingle([data], is_alnum, msg=msg, **kw)
|
||||||
await o.interact_bare()
|
await o.interact_bare()
|
||||||
|
|
||||||
async def ux_enter_bip32_index(prompt, can_cancel=True, unlimited=False):
|
async def ux_enter_bip32_index(prompt, can_cancel=False, unlimited=False):
|
||||||
if unlimited:
|
if unlimited:
|
||||||
max_value = (2 ** 31) - 1 # we handle hardened
|
max_value = (2 ** 31) - 1 # we handle hardened
|
||||||
else:
|
else:
|
||||||
@ -357,12 +357,12 @@ async def ux_enter_bip32_index(prompt, can_cancel=True, unlimited=False):
|
|||||||
|
|
||||||
return await ux_enter_number(prompt=prompt, max_value=max_value, can_cancel=can_cancel)
|
return await ux_enter_number(prompt=prompt, max_value=max_value, can_cancel=can_cancel)
|
||||||
|
|
||||||
def _import_prompt_builder(title, no_qr, no_nfc, slot_b_only=False, key0=None, key6=None):
|
def _import_prompt_builder(title, no_qr, no_nfc, slot_b_only=False):
|
||||||
from glob import NFC, VD
|
from glob import NFC, VD
|
||||||
|
|
||||||
prompt, escape = None, KEY_CANCEL+"x"
|
prompt, escape = None, KEY_CANCEL+"x"
|
||||||
|
|
||||||
if (NFC or VD) or (num_sd_slots > 1) or key0 or key6:
|
if (NFC or VD) or num_sd_slots>1:
|
||||||
if slot_b_only and (num_sd_slots>1):
|
if slot_b_only and (num_sd_slots>1):
|
||||||
prompt = "Press (B) to import %s from lower slot SD Card" % title
|
prompt = "Press (B) to import %s from lower slot SD Card" % title
|
||||||
escape += "b"
|
escape += "b"
|
||||||
@ -388,28 +388,20 @@ def _import_prompt_builder(title, no_qr, no_nfc, slot_b_only=False, key0=None, k
|
|||||||
prompt += ", " + KEY_QR + " to scan QR code"
|
prompt += ", " + KEY_QR + " to scan QR code"
|
||||||
escape += KEY_QR
|
escape += KEY_QR
|
||||||
|
|
||||||
if key6:
|
|
||||||
prompt += ', (6) ' + key6
|
|
||||||
escape += '6'
|
|
||||||
|
|
||||||
if key0:
|
|
||||||
prompt += ', (0) ' + key0
|
|
||||||
escape += '0'
|
|
||||||
|
|
||||||
prompt += "."
|
prompt += "."
|
||||||
|
|
||||||
return prompt, escape
|
return prompt, escape
|
||||||
|
|
||||||
|
|
||||||
def export_prompt_builder(what_it_is, no_qr=False, no_nfc=False, key0=None, offer_kt=False,
|
def export_prompt_builder(what_it_is, no_qr=False, no_nfc=False, key0=None, offer_kt=False,
|
||||||
force_prompt=False, key6=None):
|
force_prompt=False, txid=None):
|
||||||
# Build the prompt for export
|
# Build the prompt for export
|
||||||
# - key0 can be for special stuff
|
# - key0 can be for special stuff
|
||||||
from glob import NFC, VD
|
from glob import NFC, VD
|
||||||
|
|
||||||
prompt, escape = None, KEY_CANCEL+"x"
|
prompt, escape = None, KEY_CANCEL+"x"
|
||||||
|
|
||||||
if (NFC or VD) or (num_sd_slots>1) or key0 or force_prompt or offer_kt or key6 or (not no_qr):
|
if (NFC or VD) or (num_sd_slots>1) or key0 or force_prompt or offer_kt or txid or (not no_qr):
|
||||||
# no need to spam with another prompt, only option is SD card
|
# no need to spam with another prompt, only option is SD card
|
||||||
|
|
||||||
prompt = "Press (1) to save %s to SD Card" % what_it_is
|
prompt = "Press (1) to save %s to SD Card" % what_it_is
|
||||||
@ -439,6 +431,10 @@ def export_prompt_builder(what_it_is, no_qr=False, no_nfc=False, key0=None, offe
|
|||||||
prompt += ", (4) to show QR code"
|
prompt += ", (4) to show QR code"
|
||||||
escape += '4'
|
escape += '4'
|
||||||
|
|
||||||
|
if txid:
|
||||||
|
prompt += ", (6) for QR Code of TXID"
|
||||||
|
escape += "6"
|
||||||
|
|
||||||
if offer_kt:
|
if offer_kt:
|
||||||
prompt += ", (T) to " + offer_kt
|
prompt += ", (T) to " + offer_kt
|
||||||
escape += 't'
|
escape += 't'
|
||||||
@ -447,10 +443,6 @@ def export_prompt_builder(what_it_is, no_qr=False, no_nfc=False, key0=None, offe
|
|||||||
prompt += ', (0) ' + key0
|
prompt += ', (0) ' + key0
|
||||||
escape += '0'
|
escape += '0'
|
||||||
|
|
||||||
if key6:
|
|
||||||
prompt += ", (6) " + key6
|
|
||||||
escape += "6"
|
|
||||||
|
|
||||||
prompt += "."
|
prompt += "."
|
||||||
|
|
||||||
return prompt, escape
|
return prompt, escape
|
||||||
@ -489,7 +481,7 @@ def import_export_prompt_decode(ch):
|
|||||||
async def import_export_prompt(what_it_is, is_import=False, no_qr=False,
|
async def import_export_prompt(what_it_is, is_import=False, no_qr=False,
|
||||||
no_nfc=False, title=None, intro='', footnotes='',
|
no_nfc=False, title=None, intro='', footnotes='',
|
||||||
offer_kt=False, slot_b_only=False, force_prompt=False,
|
offer_kt=False, slot_b_only=False, force_prompt=False,
|
||||||
key0=None, key6=None):
|
txid=None):
|
||||||
|
|
||||||
# Show story allowing user to select source for importing/exporting
|
# Show story allowing user to select source for importing/exporting
|
||||||
# - return either str(mode) OR dict(file_args)
|
# - return either str(mode) OR dict(file_args)
|
||||||
@ -500,10 +492,9 @@ async def import_export_prompt(what_it_is, is_import=False, no_qr=False,
|
|||||||
from glob import NFC
|
from glob import NFC
|
||||||
|
|
||||||
if is_import:
|
if is_import:
|
||||||
prompt, escape = _import_prompt_builder(what_it_is, no_qr, no_nfc, slot_b_only,
|
prompt, escape = _import_prompt_builder(what_it_is, no_qr, no_nfc, slot_b_only)
|
||||||
key0=key0, key6=key6)
|
|
||||||
else:
|
else:
|
||||||
prompt, escape = export_prompt_builder(what_it_is, no_qr, no_nfc, key6=key6, key0=key0,
|
prompt, escape = export_prompt_builder(what_it_is, no_qr, no_nfc, txid=txid,
|
||||||
force_prompt=force_prompt, offer_kt=offer_kt)
|
force_prompt=force_prompt, offer_kt=offer_kt)
|
||||||
|
|
||||||
# TODO: detect if we're only asking A or B, when just one card is inserted
|
# TODO: detect if we're only asking A or B, when just one card is inserted
|
||||||
|
|||||||
@ -60,7 +60,7 @@ class PressRelease:
|
|||||||
return ch
|
return ch
|
||||||
|
|
||||||
|
|
||||||
async def ux_enter_number(prompt, max_value, can_cancel=True, value=''):
|
async def ux_enter_number(prompt, max_value, can_cancel=False, value=''):
|
||||||
# return the decimal number which the user has entered
|
# return the decimal number which the user has entered
|
||||||
# - default/blank value assumed to be zero
|
# - default/blank value assumed to be zero
|
||||||
# - clamps large values to the max
|
# - clamps large values to the max
|
||||||
|
|||||||
@ -76,7 +76,7 @@ class PressRelease:
|
|||||||
self.last_key = ch
|
self.last_key = ch
|
||||||
return ch
|
return ch
|
||||||
|
|
||||||
async def ux_enter_number(prompt, max_value, can_cancel=True, value=''):
|
async def ux_enter_number(prompt, max_value, can_cancel=False, value=''):
|
||||||
# return the decimal number which the user has entered
|
# return the decimal number which the user has entered
|
||||||
# - default/blank value assumed to be zero
|
# - default/blank value assumed to be zero
|
||||||
# - clamps large values to the max
|
# - clamps large values to the max
|
||||||
@ -121,7 +121,7 @@ async def ux_enter_number(prompt, max_value, can_cancel=True, value=''):
|
|||||||
dis.text(0, 4, ' '*CHARS_W)
|
dis.text(0, 4, ' '*CHARS_W)
|
||||||
elif ch == KEY_CANCEL:
|
elif ch == KEY_CANCEL:
|
||||||
if can_cancel:
|
if can_cancel:
|
||||||
# quit if they press CANCEL on any screen
|
# quit if they press X on empty screen
|
||||||
return None
|
return None
|
||||||
elif '0' <= ch <= '9':
|
elif '0' <= ch <= '9':
|
||||||
if len(value) == max_w:
|
if len(value) == max_w:
|
||||||
@ -537,14 +537,13 @@ def ux_render_words(words, leading_blanks=0):
|
|||||||
num_words = len(words)
|
num_words = len(words)
|
||||||
if num_words == 12:
|
if num_words == 12:
|
||||||
for y in range(6):
|
for y in range(6):
|
||||||
# no need to use NOWRAP here, will always fit (2 word columns)
|
|
||||||
rv.append('%2d: %-8s %2d: %s' % (y+1, words[y], y+7, words[y+6]))
|
rv.append('%2d: %-8s %2d: %s' % (y+1, words[y], y+7, words[y+6]))
|
||||||
else:
|
else:
|
||||||
lines = 6 if num_words == 18 else 8
|
lines = 6 if num_words == 18 else 8
|
||||||
for y in range(lines):
|
for y in range(lines):
|
||||||
rv.append(OUT_CTRL_NOWRAP+'%d:%-8s %2d:%-8s %2d:%s' % (
|
rv.append('%d:%-8s %2d:%-8s %2d:%s' % (y+1, words[y],
|
||||||
y+1, words[y], y+lines+1, words[y+lines],
|
y+lines+1, words[y+lines],
|
||||||
y+(lines*2)+1, words[y+(lines*2)]))
|
y+(lines*2)+1, words[y+(lines*2)]))
|
||||||
|
|
||||||
return '\n'.join(rv)
|
return '\n'.join(rv)
|
||||||
|
|
||||||
@ -553,16 +552,6 @@ def ux_draw_words(y, num_words, words):
|
|||||||
# Draw seed words on single screen (hard) and return x/y position of start of each
|
# Draw seed words on single screen (hard) and return x/y position of start of each
|
||||||
from glob import dis
|
from glob import dis
|
||||||
|
|
||||||
if num_words == 2:
|
|
||||||
# simple version for first & last words, used only during login to spending policy
|
|
||||||
X = 14
|
|
||||||
Y = y+1
|
|
||||||
dis.text(X-7, Y, 'FIRST: %s' % words[0])
|
|
||||||
dis.text(X-4, Y+1, '⋯')
|
|
||||||
dis.text(X-6, Y+2, 'LAST: %s' % words[-1])
|
|
||||||
|
|
||||||
return [ (X, Y), (X, Y+2) ]
|
|
||||||
|
|
||||||
if num_words == 12:
|
if num_words == 12:
|
||||||
cols = 2
|
cols = 2
|
||||||
xpos = [2, 18]
|
xpos = [2, 18]
|
||||||
@ -578,7 +567,7 @@ def ux_draw_words(y, num_words, words):
|
|||||||
if num_words == 12:
|
if num_words == 12:
|
||||||
# luxious space after colon
|
# luxious space after colon
|
||||||
msg = ('%2d: ' % n) + word
|
msg = ('%2d: ' % n) + word
|
||||||
x_off = 4
|
x_off = 3
|
||||||
else:
|
else:
|
||||||
if n <= n_per_c:
|
if n <= n_per_c:
|
||||||
# no space in front of 1: thru N: in leftmost column of 3
|
# no space in front of 1: thru N: in leftmost column of 3
|
||||||
@ -594,7 +583,7 @@ def ux_draw_words(y, num_words, words):
|
|||||||
|
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
async def seed_word_entry(prompt, num_words, has_checksum=True, done_cb=None, line2=None):
|
async def seed_word_entry(prompt, num_words, has_checksum=True, done_cb=None):
|
||||||
# Accept a seed phrase, only
|
# Accept a seed phrase, only
|
||||||
# - replaces WordNestMenu on Q1
|
# - replaces WordNestMenu on Q1
|
||||||
# - max word length is 8, min is 3
|
# - max word length is 8, min is 3
|
||||||
@ -604,23 +593,13 @@ async def seed_word_entry(prompt, num_words, has_checksum=True, done_cb=None, li
|
|||||||
|
|
||||||
assert num_words and prompt
|
assert num_words and prompt
|
||||||
|
|
||||||
not24 = (num_words != 24)
|
|
||||||
|
|
||||||
def redraw_words(wrds=None):
|
def redraw_words(wrds=None):
|
||||||
if not wrds:
|
if not wrds:
|
||||||
wrds = ['' for _ in range(num_words)]
|
wrds = ['' for _ in range(num_words)]
|
||||||
|
|
||||||
dis.clear()
|
dis.clear()
|
||||||
dis.text(None, 0, prompt, invert=1)
|
dis.text(None, 0, prompt, invert=1)
|
||||||
|
p = ux_draw_words(2 if num_words != 24 else 1, num_words, wrds)
|
||||||
Y = 2 if not24 else 1
|
|
||||||
if line2 and not24:
|
|
||||||
# add second line, if provided, but only if words length < 24
|
|
||||||
# currently only used to show backup filename during backup pwd entry
|
|
||||||
dis.text(None, 1, line2, invert=1)
|
|
||||||
Y += 1
|
|
||||||
|
|
||||||
p = ux_draw_words(Y, num_words, wrds)
|
|
||||||
return wrds, p
|
return wrds, p
|
||||||
|
|
||||||
words, pos = redraw_words()
|
words, pos = redraw_words()
|
||||||
@ -667,7 +646,7 @@ async def seed_word_entry(prompt, num_words, has_checksum=True, done_cb=None, li
|
|||||||
what, vals = decode_qr_result(got, expect_secret=True)
|
what, vals = decode_qr_result(got, expect_secret=True)
|
||||||
except QRDecodeExplained as e:
|
except QRDecodeExplained as e:
|
||||||
err_msg = str(e)
|
err_msg = str(e)
|
||||||
redraw_words(words)
|
redraw_words()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if what != "words":
|
if what != "words":
|
||||||
@ -825,6 +804,7 @@ class QRScannerInteraction:
|
|||||||
while 1:
|
while 1:
|
||||||
if task.done():
|
if task.done():
|
||||||
data = await task
|
data = await task
|
||||||
|
#print("Scanned: %r" % data)
|
||||||
break
|
break
|
||||||
|
|
||||||
dis.image(None, 40, 'scan_%d' % frames[ph])
|
dis.image(None, 40, 'scan_%d' % frames[ph])
|
||||||
@ -837,12 +817,7 @@ class QRScannerInteraction:
|
|||||||
data = None
|
data = None
|
||||||
break
|
break
|
||||||
|
|
||||||
if not task.done():
|
task.cancel()
|
||||||
task.cancel()
|
|
||||||
try:
|
|
||||||
await task
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# clear screen right away so user knows we got it
|
# clear screen right away so user knows we got it
|
||||||
dis.clear()
|
dis.clear()
|
||||||
@ -885,7 +860,7 @@ class QRScannerInteraction:
|
|||||||
file_type, _, data = decode_qr_result(got, expect_bbqr=True)
|
file_type, _, data = decode_qr_result(got, expect_bbqr=True)
|
||||||
if file_type == 'U':
|
if file_type == 'U':
|
||||||
data = data.strip()
|
data = data.strip()
|
||||||
if data[:1] == b'{' and data[-1:] == b'}':
|
if data[0] == '{' and data[-1] == '}':
|
||||||
file_type = 'J'
|
file_type = 'J'
|
||||||
if file_type != 'J':
|
if file_type != 'J':
|
||||||
raise QRDecodeExplained('Expected JSON data')
|
raise QRDecodeExplained('Expected JSON data')
|
||||||
@ -926,8 +901,6 @@ class QRScannerInteraction:
|
|||||||
async def scan_anything(self, expect_secret=False, tmp=False):
|
async def scan_anything(self, expect_secret=False, tmp=False):
|
||||||
# start a QR scan, and act on what we find, whatever it may be.
|
# start a QR scan, and act on what we find, whatever it may be.
|
||||||
from ux import ux_show_story
|
from ux import ux_show_story
|
||||||
from pincodes import pa
|
|
||||||
|
|
||||||
problem = None
|
problem = None
|
||||||
while 1:
|
while 1:
|
||||||
prompt = 'Scan any QR code, or CANCEL' if not expect_secret else \
|
prompt = 'Scan any QR code, or CANCEL' if not expect_secret else \
|
||||||
@ -949,21 +922,6 @@ class QRScannerInteraction:
|
|||||||
problem = "Unable to decode QR"
|
problem = "Unable to decode QR"
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if pa.hobbled_mode:
|
|
||||||
# block most imports in hobbled mode.
|
|
||||||
# - specific checks in place for teleport (PSBT is okay)
|
|
||||||
from ccc import sssp_spending_policy
|
|
||||||
whitelist = {'psbt', 'addr', 'vmsg', 'text', 'xpub', 'teleport' }
|
|
||||||
|
|
||||||
sv_ok = sssp_spending_policy('okeys')
|
|
||||||
if sv_ok:
|
|
||||||
# seed vault, and tmp seeds are okay with user, even in hobble mode
|
|
||||||
whitelist.update({'xprv', 'words'})
|
|
||||||
|
|
||||||
if what not in whitelist:
|
|
||||||
await ux_show_story("Blocked when Spending Policy is in force.", title='Sorry')
|
|
||||||
return
|
|
||||||
|
|
||||||
if what == 'xprv':
|
if what == 'xprv':
|
||||||
from actions import import_extended_key_as_secret
|
from actions import import_extended_key_as_secret
|
||||||
text_xprv, = vals
|
text_xprv, = vals
|
||||||
@ -1004,7 +962,6 @@ class QRScannerInteraction:
|
|||||||
elif what == "wif":
|
elif what == "wif":
|
||||||
data, = vals
|
data, = vals
|
||||||
wif_str, key_pair, compressed, testnet = data
|
wif_str, key_pair, compressed, testnet = data
|
||||||
from wif import ux_visualize_wif
|
|
||||||
await ux_visualize_wif(wif_str, key_pair, compressed, testnet)
|
await ux_visualize_wif(wif_str, key_pair, compressed, testnet)
|
||||||
|
|
||||||
elif what == "vmsg":
|
elif what == "vmsg":
|
||||||
@ -1061,7 +1018,7 @@ async def qr_psbt_sign(decoder, psbt_len, raw):
|
|||||||
psbt_len = total
|
psbt_len = total
|
||||||
|
|
||||||
else:
|
else:
|
||||||
with SFFile(TXN_INPUT_OFFSET, length=psbt_len) as out:
|
with SFFile(TXN_INPUT_OFFSET, max_size=psbt_len) as out:
|
||||||
taste = out.read(10)
|
taste = out.read(10)
|
||||||
_, output_encoder, _ = psbt_encoding_taster(taste, psbt_len)
|
_, output_encoder, _ = psbt_encoding_taster(taste, psbt_len)
|
||||||
|
|
||||||
@ -1113,22 +1070,20 @@ async def ux_visualize_bip21(proto, addr, args):
|
|||||||
# - imho, a bare address is a valid BIP-21 URL so we come here too
|
# - imho, a bare address is a valid BIP-21 URL so we come here too
|
||||||
# - validate address ownership on request
|
# - validate address ownership on request
|
||||||
from ux import ux_show_story
|
from ux import ux_show_story
|
||||||
from chains import current_chain
|
|
||||||
|
|
||||||
msg = show_single_address(addr) + '\n\n'
|
msg = show_single_address(addr) + '\n\n'
|
||||||
args = args or {}
|
args = args or {}
|
||||||
|
|
||||||
if 'amount' in args:
|
if 'amount' in args:
|
||||||
|
msg += 'Amount: '
|
||||||
try:
|
try:
|
||||||
amt = args.pop('amount')
|
amt = args.pop('amount')
|
||||||
whole, _, frac = amt.partition('.')
|
whole, frac = amt.split('.', 1)
|
||||||
assert whole.isdigit()
|
frac = int(frac) if frac else 0
|
||||||
assert len(whole) <= 8
|
whole = int(whole) if whole else 0
|
||||||
assert len(frac) <= 8
|
msg += '%d.%08d BTC\n' % (whole, frac)
|
||||||
sats = int((whole or '0') + (frac + '00000000')[:8])
|
|
||||||
msg += 'Amount: %s %s\n' % current_chain().render_value(sats)
|
|
||||||
except:
|
except:
|
||||||
msg += 'Amount: (corrupt)\n'
|
msg += '(corrupt)\n'
|
||||||
|
|
||||||
for fn in ['label', 'message', 'lightning']:
|
for fn in ['label', 'message', 'lightning']:
|
||||||
if fn in args:
|
if fn in args:
|
||||||
@ -1145,8 +1100,15 @@ async def ux_visualize_bip21(proto, addr, args):
|
|||||||
|
|
||||||
if ch == '1':
|
if ch == '1':
|
||||||
from ownership import OWNERSHIP
|
from ownership import OWNERSHIP
|
||||||
await OWNERSHIP.search_ux(addr, args)
|
await OWNERSHIP.search_ux(addr)
|
||||||
|
|
||||||
|
async def ux_visualize_wif(wif_str, kp, compressed, testnet):
|
||||||
|
from ux import ux_show_story
|
||||||
|
msg = wif_str + "\n\n"
|
||||||
|
msg += "chain: %s\n\n" % ("XTN" if testnet else "BTC")
|
||||||
|
msg += "private key hex:\n" + b2a_hex(kp.privkey()).decode() + "\n\n"
|
||||||
|
msg += "public key sec:\n" + b2a_hex(kp.pubkey().to_bytes(not compressed)).decode() + "\n\n"
|
||||||
|
await ux_show_story(msg, title="WIF")
|
||||||
|
|
||||||
async def qr_msg_sign_done(signature, address, text):
|
async def qr_msg_sign_done(signature, address, text):
|
||||||
from ux import ux_show_story
|
from ux import ux_show_story
|
||||||
@ -1205,6 +1167,7 @@ async def show_bbqr_codes(type_code, data, msg, already_hex=False):
|
|||||||
from ux import ux_wait_keydown
|
from ux import ux_wait_keydown
|
||||||
import uqr
|
import uqr
|
||||||
|
|
||||||
|
assert not PSRAM.is_at(data, 0) # input data would be overwritten with our work
|
||||||
assert type_code in TYPE_LABELS
|
assert type_code in TYPE_LABELS
|
||||||
|
|
||||||
dis.fullscreen('Generating BBQr...', .1)
|
dis.fullscreen('Generating BBQr...', .1)
|
||||||
@ -1215,11 +1178,6 @@ async def show_bbqr_codes(type_code, data, msg, already_hex=False):
|
|||||||
else:
|
else:
|
||||||
# default to Base32, because always best option
|
# default to Base32, because always best option
|
||||||
encoding = '2'
|
encoding = '2'
|
||||||
if isinstance(data, str):
|
|
||||||
# 'U'/'J' payloads are UTF-8 text; b32encode consumes the UTF-8
|
|
||||||
# bytes, so convert now to keep length/slicing consistent (else
|
|
||||||
# multi-byte chars overflow target_vers -> assert below trips)
|
|
||||||
data = data.encode()
|
|
||||||
data_len = len(data)
|
data_len = len(data)
|
||||||
|
|
||||||
# try a few select resolutions (sizes) in order such that we use either single QR
|
# try a few select resolutions (sizes) in order such that we use either single QR
|
||||||
|
|||||||
@ -80,7 +80,6 @@ def probe_system():
|
|||||||
|
|
||||||
from sigheader import RAM_BOOT_FLAGS, RBF_FACTORY_MODE
|
from sigheader import RAM_BOOT_FLAGS, RBF_FACTORY_MODE
|
||||||
import ckcc, callgate, machine
|
import ckcc, callgate, machine
|
||||||
from machine import Pin
|
|
||||||
|
|
||||||
hw_label = 'mk4'
|
hw_label = 'mk4'
|
||||||
has_608 = True
|
has_608 = True
|
||||||
@ -98,7 +97,7 @@ def probe_system():
|
|||||||
|
|
||||||
# detect Q1 based on pins.csv
|
# detect Q1 based on pins.csv
|
||||||
try:
|
try:
|
||||||
Pin('LCD_TEAR') # only defined on Q1 build, will error otherwise
|
machine.Pin('LCD_TEAR') # only defined on Q1 build, will error otherwise
|
||||||
has_qr = True
|
has_qr = True
|
||||||
num_sd_slots = 2
|
num_sd_slots = 2
|
||||||
hw_label = 'q1'
|
hw_label = 'q1'
|
||||||
@ -109,15 +108,6 @@ def probe_system():
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
|
||||||
# only defined on Mk4/5 build, will error otherwise; was open on Mk1-4, low on Mk5
|
|
||||||
s0 = Pin('STRAP_MK5', mode=Pin.IN, pull=Pin.PULL_UP)
|
|
||||||
if s0() == 0:
|
|
||||||
hw_label = 'mk5'
|
|
||||||
mk_num = 5
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Boot loader needs to tell us stuff about how we were booted, sometimes:
|
# Boot loader needs to tell us stuff about how we were booted, sometimes:
|
||||||
# - did we just install a new version, for example (obsolete in mk4)
|
# - did we just install a new version, for example (obsolete in mk4)
|
||||||
# - are we running in "factory mode" with flash un-secured?
|
# - are we running in "factory mode" with flash un-secured?
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
# wallet.py - A place you find UTXO, addresses and descriptors.
|
# wallet.py - A place you find UTXO, addresses and descriptors.
|
||||||
#
|
#
|
||||||
import chains, version
|
import chains
|
||||||
from descriptor import Descriptor
|
from descriptor import Descriptor
|
||||||
from stash import SensitiveValues
|
from stash import SensitiveValues
|
||||||
|
|
||||||
@ -38,16 +38,9 @@ class MasterSingleSigWallet(WalletABC):
|
|||||||
def __init__(self, addr_fmt, path=None, account_idx=0, chain_name=None):
|
def __init__(self, addr_fmt, path=None, account_idx=0, chain_name=None):
|
||||||
# Construct a wallet based on current master secret, and chain.
|
# Construct a wallet based on current master secret, and chain.
|
||||||
# - path is optional, and then we use standard path for addr_fmt
|
# - path is optional, and then we use standard path for addr_fmt
|
||||||
# - path can be overridden when we come here via address explorer
|
# - path can be overriden when we come here via address explorer
|
||||||
|
|
||||||
n = chains.addr_fmt_label(addr_fmt)
|
n = chains.addr_fmt_label(addr_fmt)
|
||||||
if not version.has_qwerty:
|
|
||||||
# Mk4 tiny display
|
|
||||||
# Classic P2PKH -> P2PKH
|
|
||||||
# Segwit P2WPKH -> P2WPKH
|
|
||||||
# P2SH-Segwit -> no change (should not be used that much)
|
|
||||||
n = n.split(" ")[-1]
|
|
||||||
|
|
||||||
purpose = chains.af_to_bip44_purpose(addr_fmt)
|
purpose = chains.af_to_bip44_purpose(addr_fmt)
|
||||||
prefix = path or 'm/%dh/{coin_type}h/{account}h' % purpose
|
prefix = path or 'm/%dh/{coin_type}h/{account}h' % purpose
|
||||||
|
|
||||||
@ -57,13 +50,12 @@ class MasterSingleSigWallet(WalletABC):
|
|||||||
self.chain = chains.current_chain()
|
self.chain = chains.current_chain()
|
||||||
|
|
||||||
if account_idx != 0:
|
if account_idx != 0:
|
||||||
rv = " Account#%d" if version.has_qwerty else " Acct#%d"
|
n += ' Account#%d' % account_idx
|
||||||
n += rv % account_idx
|
|
||||||
|
|
||||||
if self.chain.ctype == 'XTN':
|
if self.chain.ctype == 'XTN':
|
||||||
n += ' (Testnet)' if version.has_qwerty else " XTN"
|
n += ' (Testnet)'
|
||||||
if self.chain.ctype == 'XRT':
|
if self.chain.ctype == 'XRT':
|
||||||
n += ' (Regtest)' if version.has_qwerty else " XRT"
|
n += ' (Regtest)'
|
||||||
|
|
||||||
|
|
||||||
self.name = n
|
self.name = n
|
||||||
|
|||||||
@ -95,7 +95,7 @@ async def perform_web2fa(label, shared_secret):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def web2fa_enroll(ss=None):
|
async def web2fa_enroll(label, ss=None):
|
||||||
#
|
#
|
||||||
# Enroll: Pick a secret and test they have loaded it into their phone.
|
# Enroll: Pick a secret and test they have loaded it into their phone.
|
||||||
#
|
#
|
||||||
@ -115,21 +115,22 @@ async def web2fa_enroll(ss=None):
|
|||||||
# - can't fit any metadata, like username or our serial # in there
|
# - can't fit any metadata, like username or our serial # in there
|
||||||
# - better on Q1 where no limitations for this size of QR
|
# - better on Q1 where no limitations for this size of QR
|
||||||
|
|
||||||
nm = 'COLDCARD' if has_qr else 'CC' # must be url-safe
|
qr = 'otpauth://totp/{nm}?secret={ss}'.format(ss=ss,
|
||||||
qr = 'otpauth://totp/{nm}?secret={ss}'.format(ss=ss, nm=nm)
|
nm=url_quote(label if has_qr else label[0:4]))
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
# show QR for enroll
|
# show QR for enroll
|
||||||
await show_qr_code(qr, is_alnum=False, msg="Import into 2FA Mobile App",
|
await show_qr_code(qr, is_alnum=False, msg="Import into 2FA Mobile App",
|
||||||
force_msg=True)
|
force_msg=True)
|
||||||
|
|
||||||
# important: force them to prove they stored it correctly
|
# important: force them to prove they store it correctly
|
||||||
ok = await perform_web2fa('Enroll: COLDCARD', ss)
|
ok = await perform_web2fa('Enroll: ' + label, ss)
|
||||||
if ok: break
|
if ok: break
|
||||||
|
|
||||||
ch = await ux_show_story("That isn't correct. Please re-import and/or "
|
ch = await ux_show_story("That isn't correct. Please re-import and/or "
|
||||||
"try again or %s to give up." % X)
|
"try again or %s to give up." % X)
|
||||||
if ch == 'x':
|
if ch == 'x':
|
||||||
|
# mk4 only?
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return ss
|
return ss
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user