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
|
||||
|
||||
@ -28,11 +28,9 @@ has been automated using Docker. Steps are as follows:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/Coldcard/firmware.git
|
||||
cd firmware
|
||||
# DOWNLOAD https://coldcard.com/downloads
|
||||
# get a copy of binary into ./releases/2026-03-05T2052-v5.5.0-mk-coldcard.dfu
|
||||
git checkout 2026-03-05T2052-v5.5.0
|
||||
cd stm32
|
||||
git checkout 2023-12-21T1526-v5.2.2
|
||||
# get a copy of that binary into ./releases/2023-12-21T1526-v5.2.2-mk4-coldcard.dfu
|
||||
cd firmware/stm32
|
||||
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
|
||||
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`
|
||||
and `shared/manifest_q1.py`. Common files are in `shared/manifest.py`.
|
||||
Firmware built for Mk5, supports the Mk4 without any functional differences.
|
||||
|
||||
|
||||
## Check-out and Setup
|
||||
|
||||
**NOTE** This is the `master` branch and covers the latest hardware (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.
|
||||
|
||||
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
|
||||
- expected to be largely in python, and higher-level
|
||||
- code exclusive to the Mk4 or Mk5 will be listed in `manifest_mk4.py`, and
|
||||
to the Q will be listed in `manifest_q1.py`
|
||||
- new code found only on the Mk4 will be listed in `manifest_mk4.py` code exclusive
|
||||
to earlier hardware is in `manifest_mk3.py`
|
||||
|
||||
`unix`
|
||||
|
||||
@ -268,7 +265,7 @@ Top-level dirs:
|
||||
`stm32/mk4-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.
|
||||
|
||||
`hardware`
|
||||
|
||||
@ -208,9 +208,8 @@ def readback(fname):
|
||||
if v & MK_2_OK: d.append('Mk2')
|
||||
if v & MK_3_OK: d.append('Mk3')
|
||||
if v & MK_4_OK: d.append('Mk4')
|
||||
if v & MK_5_OK: d.append('Mk5')
|
||||
if v & MK_Q1_OK: d.append('Q1')
|
||||
if v & ~(MK_1_OK | MK_2_OK | MK_3_OK | MK_4_OK | MK_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?')
|
||||
v = nv + '+'.join(d)
|
||||
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('--high_water', '-h', is_flag=True, help='Mark version as new highwater mark (no downgrades below this version)')
|
||||
@click.option('--verbose', '-v', default=False, is_flag=True, help='Show numbers related to signature')
|
||||
@click.option('--hw-compat', '-m', type=str, metavar='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',
|
||||
help='Make downgrade attack test version', default=0)
|
||||
@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()
|
||||
body = open(build_dir + '/firmware1.bin', 'rb').read()
|
||||
|
||||
if hw_compat in { 'mk4', '4', 'mk5', '5', 'mk' }:
|
||||
# Mk4 and 5 can run the same firmware, once Mk5 support was added
|
||||
hw_compat = MK_4_OK | MK_5_OK
|
||||
if hw_compat in { 'mk4', '4'}:
|
||||
hw_compat = MK_4_OK
|
||||
elif hw_compat == 'q1':
|
||||
hw_compat = MK_Q1_OK
|
||||
elif hw_compat in { 'mk3', '3'}:
|
||||
|
||||
@ -3,32 +3,14 @@
|
||||
These docs are meant for you hackers out there... but also for anyone who
|
||||
wants to understand why it's safe to put your moneys into Coldcard.
|
||||
|
||||
- [`security-model.md`](security-model.md) The COLDCARD Mk4/Mk5/Q security model.
|
||||
- [`pin-entry.md`](pin-entry.md) Huge and detailed discussion of PIN codes and the security element that holds the secrets.
|
||||
- [`secure-elements.md`](secure-elements.md) How the dual secure elements work together.
|
||||
- [`dev-access.md`](dev-access.md) How developers can modify Coldcard to extend it.
|
||||
- [`memory-map.md`](memory-map.md) Memory map highlights
|
||||
- [`notes-on-repro.md`](notes-on-repro.md) Detailed breakdown of the reproducible build process.
|
||||
- [`upgrade-recovery.md`](upgrade-recovery.md) Firmware upgrade and recovery process.
|
||||
- [`backup-files.md`](backup-files.md) Some details of our encrypted backup files.
|
||||
- [`temporary-seeds.md`](temporary-seeds.md) Temporary (ephemeral) seeds and the Seed Vault.
|
||||
- [`seed-xor.md`](seed-xor.md) More about _Seed XOR_ feature, including fully worked Seed XOR example, and useful XOR lookup chart.
|
||||
- [`key-teleport.md`](key-teleport.md) Key Teleport: encrypted transfer of seeds and secrets between Q devices.
|
||||
- [`spending-policy.md`](spending-policy.md) Spending policy: autonomous signing with configurable limits.
|
||||
- [`microsd-2fa.md`](microsd-2fa.md) Using a MicroSD card as a second factor for login.
|
||||
- [`web2fa.md`](web2fa.md) Web 2FA authentication.
|
||||
- [`bip85-passwords.md`](bip85-passwords.md) Deriving deterministic passwords via BIP-85.
|
||||
- [`msg-signing.md`](msg-signing.md) COLDCARD message signing.
|
||||
- [`proof-of-reserves-bip-322.md`](proof-of-reserves-bip-322.md) BIP-322 generic signed message format and proof of reserves.
|
||||
- [`generic-wallet-export.md`](generic-wallet-export.md) Generic JSON wallet export file format.
|
||||
- [`bip-21-extensions.md`](bip-21-extensions.md) Coldcard's BIP-21 URI extensions, including multisig ownership address check.
|
||||
- [`nfc-coldcard.md`](nfc-coldcard.md) NFC support on Coldcard Mk4 and Q.
|
||||
- [`nfc-pushtx.md`](nfc-pushtx.md) NFC Push Transaction: broadcast a signed transaction via your phone.
|
||||
- [`usb-batteries.md`](usb-batteries.md) Using USB battery packs with Coldcard.
|
||||
- [`electrum-usage.md`](electrum-usage.md) Importing seed words into Electrum for funds usage (and other tips).
|
||||
- [`bitcoin-core-usage.md`](bitcoin-core-usage.md) How to use with Bitcoin Core.
|
||||
- [`bitcoin-core2of2desc.md`](bitcoin-core2of2desc.md) Airgapped 2-of-2 multisig with Bitcoin Core using descriptors.
|
||||
- [`limitations.md`](limitations.md) Documented limitations, policy choices, and TODO items.
|
||||
- [`paperwallet.pdf`](paperwallet.pdf) Example paper wallet template file.
|
||||
- [`seed-xor.md`](seed-xor.md) More about _Seed XOR_ feature, including fully worked Seed XOR example, and useful XOR lookup chart.
|
||||
- [`menu-tree.txt`](menu-tree.txt) Dump of the menu system. Incomplete, may be out of date.
|
||||
|
||||
|
||||
@ -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,
|
||||
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
|
||||
keystrokes needed to type the password.
|
||||
|
||||
#### 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)
|
||||
|
||||
## 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
|
||||
2. Choose "Password/Index number" (BIP-85 index) and press OK to generate password.
|
||||
3. Screen shows generated password, path, and entropy from which password was derived
|
||||
4. A few different options are available at this point (on Mk; on Q the NFC and
|
||||
QR buttons are used instead of (3)/(4)):
|
||||
1. press (1) to save password backup file on MicroSD card (cleartext!)
|
||||
2. press (2) to save to Virtual Disk (only when available)
|
||||
3. press (3) to send over NFC (only appears when NFC is enabled)
|
||||
4. press (4) to view password as QR code
|
||||
5. press (6) to send keystrokes over USB (this enables keyboard emulation, sends keystrokes + enter, then disables keyboard emulation)
|
||||
4. A few different options are available at this point:
|
||||
1. press 1 to save password backup file on MicroSD card (cleartext!)
|
||||
2. press 2 to send keystrokes (this will first of all enable keyboard emulation, then send keystrokes + enter, and finally disables keyboard emulation)
|
||||
3. press 3 to view password as QR code
|
||||
4. press 4 to send over NFC (only appears when NFC is enabled)
|
||||
|
||||
## 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.
|
||||
|
||||
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
|
||||
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).
|
||||
position of BIP44, BIP84 and BIP49.
|
||||
|
||||
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
|
||||
more in this file.
|
||||
@ -21,51 +18,32 @@ Here is an example, produced by the Simulator for account number 123.
|
||||
|
||||
```javascript
|
||||
{
|
||||
"chain": "BTC",
|
||||
"chain": "XTN",
|
||||
"xfp": "0F056943",
|
||||
"xpub": "tpubD6NzVbkrYhZ4XzL5Dhayo67Gorv1YMS7j8pRUvVMd5odC2LBPLAygka9p7748JtSq82FNGPppFEz5xxZUdasBRCqJqXvUHq6xpnsMcYJzeh",
|
||||
"account": 123,
|
||||
"xpub": "xpub661MyMwAqRbcGC9DmWbtbAmuUjpMYxw4BWE88NSDHB3jSjfUK7KtYJuKa52GbowD3DVLkgsxH9QwPnTx5mjdHykYFEncnmAsNsCTbWzBhA7",
|
||||
"bip44": {
|
||||
"deriv": "m/44'/1'/123'",
|
||||
"first": "n44vs1Rv7T8SANrg2PFGQhzVkhr5Q6jMMD",
|
||||
"name": "p2pkh",
|
||||
"xfp": "5F898064",
|
||||
"deriv": "m/44h/0h/123h",
|
||||
"xpub": "xpub6DStQXfAgHuLbMpCf86ruVkF4yT9pSLyWsFiqQTWY9osuinq8Dyee4W5jCjMfyku5LNkRB9oFinrY5ufn9XXEn8Vvzc2jnifKMaQCNV7RBZ",
|
||||
"desc": "pkh([0f056943/44h/0h/123h]xpub6DStQXfAgHuLbMpCf86ruVkF4yT9pSLyWsFiqQTWY9osuinq8Dyee4W5jCjMfyku5LNkRB9oFinrY5ufn9XXEn8Vvzc2jnifKMaQCNV7RBZ/<0;1>/*)#4tl8jryn",
|
||||
"first": "1GTNtzG5xX2UhdD5e3Nu7i1WPxFdjxQMJt"
|
||||
"xfp": "B7908B26",
|
||||
"xpub": "tpubDCiHGUNYdRRGoSH22j8YnruUKgguCK1CC2NFQUf9PApeZh8ewAJJWGMUrhggDNK73iCTanWXv1RN5FYemUH8UrVUBjqDb8WF2VoKmDh9UTo"
|
||||
},
|
||||
"bip49": {
|
||||
"name": "p2sh-p2wpkh",
|
||||
"xfp": "A748B1FC",
|
||||
"deriv": "m/49h/0h/123h",
|
||||
"xpub": "xpub6DDm8WzH5a9qjKkttzqSB3uGofNohU9D3n3UG8WMxkUZzJEMPTYiQRf1dvTFCQR82MjGW4LUMVuTtnW4hF17RpzCqVwhf6Z2fnJPWtjG164",
|
||||
"desc": "sh(wpkh([0f056943/49h/0h/123h]xpub6DDm8WzH5a9qjKkttzqSB3uGofNohU9D3n3UG8WMxkUZzJEMPTYiQRf1dvTFCQR82MjGW4LUMVuTtnW4hF17RpzCqVwhf6Z2fnJPWtjG164/<0;1>/*))#5j7t2n2u",
|
||||
"_pub": "ypub6Y42SBfCEFhKacx1jMd4P8zmydXFe68hxtZh3XQFLkrT3Q3ae7iH2VK9f8QqCK53Rzr5FXw2pAG1n57dQwR8E4fohqe8F1NWwWN2uVRfBry",
|
||||
"first": "3CeBRbJKCpg7BpJME2vM8ZxhCjBnhG4toy"
|
||||
"_pub": "upub5DMRSsh6mNak9KbcVjJ7xAgHJvbE3Nx22CBTier5C35kv8j7g2q58ywxskBe6JCcAE2VH86CE2aL4MifJyKbRw8Gj9ay7SWvUBkp2DJ7y52",
|
||||
"deriv": "m/49'/1'/123'",
|
||||
"first": "2N87V39riUUCd4vmXfDjMWAu9gUCiBji5jB",
|
||||
"name": "p2wpkh-p2sh",
|
||||
"xfp": "CEE1D809",
|
||||
"xpub": "tpubDCDqt7XXvhAdy1MpSze5nMJA9x8DrdRaKALRRPasfxyHpiqWWEAr9cbDBQ9BcX7cB3up98Pk97U2QQ3xrvQsi5dNPmRYYhdcsKY9wwEY87T"
|
||||
},
|
||||
"bip84": {
|
||||
"_pub": "vpub5Y5a91QvDT45EnXQaKeuvJupVvX8f9BiywDcadSTtaeJ1VgJPPXMitnYsqd9k7GnEqh44FKJ5McJfu6KrihFXhAmvSWgm7BAVVK8Gupu4fL",
|
||||
"deriv": "m/84'/1'/123'",
|
||||
"first": "tb1qc58ys2dphtphg6yuugdf3d0kufmk0tye044g3l",
|
||||
"name": "p2wpkh",
|
||||
"xfp": "2C5207AA",
|
||||
"deriv": "m/84h/0h/123h",
|
||||
"xpub": "xpub6CaWStGvcXqSW9BzU2vpCoP7aWjz9VfR5DS2nuYWVvKV2nug2dESg3HdFsaWHeoZaxuAhNcPB3TH2gq8MugS3JX1yGuhB4QbC2BneaYqB16",
|
||||
"desc": "wpkh([0f056943/84h/0h/123h]xpub6CaWStGvcXqSW9BzU2vpCoP7aWjz9VfR5DS2nuYWVvKV2nug2dESg3HdFsaWHeoZaxuAhNcPB3TH2gq8MugS3JX1yGuhB4QbC2BneaYqB16/<0;1>/*)#yk84tprf",
|
||||
"_pub": "zpub6rF34DckutvQCjaE8kW4cya7vT2t2jeQuSUUMhLHFw5F8zY8XwZZvAbuJHVgHU7QQF8nCKoW6NANoG4FoJWTdmtDhxJYLt3ZjUK5RqUSMdF",
|
||||
"first": "bc1qhj6avwmp5lhpgqwm6dgxrf3v5lf67rjm99a8an"
|
||||
},
|
||||
"bip48_1": {
|
||||
"name": "p2sh-p2wsh",
|
||||
"xfp": "845A3542",
|
||||
"deriv": "m/48h/0h/123h/1h",
|
||||
"xpub": "xpub6EkcQSTygvxVnBP2X2fM6HY5D7wv46tWbBc54ADaypuCr47vQh1GPdPAZFdx81ou5Rp4vBnzeJT5MDWDZstzijxkHfrofXRycpt1ASfg1La",
|
||||
"desc": "sh(wsh(sortedmulti(M,[0f056943/48h/0h/123h/1h]xpub6EkcQSTygvxVnBP2X2fM6HY5D7wv46tWbBc54ADaypuCr47vQh1GPdPAZFdx81ou5Rp4vBnzeJT5MDWDZstzijxkHfrofXRycpt1ASfg1La/0/*,...)))",
|
||||
"_pub": "Ypub6kUxqLsLQa4M43jXJ3ux8SyP6t8dD5ZbpZmxkpP1jc7VXLW4RkZ76ouEPAZ1gMgiiXzrYFPfzBC8MfjYaoTxfTm1zUfdeqiTnHDX8raCfeg"
|
||||
},
|
||||
"bip48_2": {
|
||||
"name": "p2wsh",
|
||||
"xfp": "2A01C6B0",
|
||||
"deriv": "m/48h/0h/123h/2h",
|
||||
"xpub": "xpub6EkcQSTygvxVneXmk3ywiS2PFhBdiPxeMxYf6RFxHCHH36NxdcN7DjUpudCppAAxs58CG6DQLjtqZNmyC3MpgVob6wpdeATjpZZ1woX92EF",
|
||||
"desc": "wsh(sortedmulti(M,[0f056943/48h/0h/123h/2h]xpub6EkcQSTygvxVneXmk3ywiS2PFhBdiPxeMxYf6RFxHCHH36NxdcN7DjUpudCppAAxs58CG6DQLjtqZNmyC3MpgVob6wpdeATjpZZ1woX92EF/0/*,...))",
|
||||
"_pub": "Zpub75KE91YFZFbpup5PMS2AxgZCKRWnozdEWTEmaUKGQysSmUaKuL5WYyf2kk5UNQhhupRnddQe9GzST7crvfLoRTHTg6KtDPZiFjxBJobzcUz"
|
||||
"xfp": "78CF94E5",
|
||||
"xpub": "tpubDC7jGaaSE66VDB6VhEDFYQSCAyugXmfnMnrMVyHNzW9wryyTxvha7TmfAHd7GRXrr2TaAn2HXn9T8ep4gyNX1bzGiieqcTUNcu2poyntrET"
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -73,23 +51,16 @@ Here is an example, produced by the Simulator for account number 123.
|
||||
## Notes
|
||||
|
||||
1. The `first` address is formed by added `/0/0` onto the given derivation, and is assumed
|
||||
to be the first (non-change) receive address for the wallet. It is only present on the
|
||||
single-signature sections (`bip44`, `bip49`, `bip84`); multisig sections omit it.
|
||||
|
||||
1a. Each section includes a `desc` field: a ready-to-import Bitcoin output descriptor
|
||||
(with `#checksum`). Single-sig descriptors use the `<0;1>/*` multipath form. Multisig
|
||||
sections (`bip48_1`, `bip48_2`, and `bip45` when present) emit a `sortedmulti(...)`
|
||||
template with `M` and a trailing `...` as placeholders, to be completed with your
|
||||
threshold and the other co-signers' keys.
|
||||
to be the first (non-change) receive address for the wallet.
|
||||
|
||||
2. The user may specify any value (up to 9999) for the account number, and it's meant to
|
||||
segregate funds into sub-wallets. Don't assume it's zero.
|
||||
|
||||
3. When making your PSBT files to spend these amounts, remember that the XFP of the master
|
||||
(`0F056943` in this example) is 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,
|
||||
to spend a UTXO on `bc1qhj6avwmp5lhpgqwm6dgxrf3v5lf67rjm99a8an`, the input section
|
||||
of your PSBT would need to specify `(m=0F056943)/84'/0'/123'/0/0`.
|
||||
to spend a UTXO on `tb1qc58ys2dphtphg6yuugdf3d0kufmk0tye044g3l`, the input section
|
||||
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
|
||||
a specific address format.
|
||||
|
||||
@ -14,12 +14,11 @@
|
||||
# PIN Codes
|
||||
|
||||
- 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
|
||||
|
||||
- we don't know what day it is, so meta data on files will not have correct date/time
|
||||
- release date of the firmware version that made the file is used instead of true date
|
||||
- encrypted files produced cannot be changed, and we don't support other tools making them
|
||||
|
||||
# Micro SD
|
||||
@ -66,8 +65,7 @@
|
||||
that to the user for approval.
|
||||
- during USB "show address" for multisig, we limit subkey paths to
|
||||
16 levels deep (including master fingerprint)
|
||||
- max of 15 co-signers due to 1650 byte `scriptSig` limitation in policy with classic P2SH (same limit applies to segwit even though consensus allows up to 20 co-signers).
|
||||
note: the consensus layer sets an upper bound of 520 bytes for the length of each stack element
|
||||
- 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)
|
||||
- (mk3) we have space for up to 8 M-of-3 wallets, or a single M-of-15 wallet. YMMV
|
||||
- only a single multisig wallet can be involved in a PSBT; can't sign inputs from two different
|
||||
multisig wallets at the same time.
|
||||
@ -79,7 +77,6 @@
|
||||
- multisig wallet `name` can only contain printable ASCII characters `range(32, 127)`
|
||||
|
||||
### BIP-67
|
||||
|
||||
- importing multisig from PSBT can ONLY create `sortedmulti(...)` multisig according to BIP-67, DO NOT use with `multi(...)`
|
||||
- creating airgapped multisig using COLDCARD as coordinator always produces `sortedmulti(...)` multisig according to BIP-67
|
||||
- COLDCARD import/export [format](https://coldcard.com/docs/multisig/#configuration-text-file-for-multisig) only supports `sortedmulti(...)` multisig according to BIP-67. To import multisig wallet with `multi(...)` use descriptor import [format](https://github.com/bitcoin/bips/blob/master/bip-0383.mediawiki)
|
||||
@ -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`)
|
||||
|
||||
# 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
|
||||
|
||||
@ -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
|
||||
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:
|
||||
- based on a max magnitude per txn, and a required minimum block height
|
||||
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)
|
||||
- maximum of 25 whitelisted addresses can be stored
|
||||
- Web2FA: any number of mobile devices can be enrolled, but all will have the same shared secret
|
||||
- any warning from the PSBT, such as huge fees, will cause the transaction to be rejected
|
||||
- any warning from the PSBT, such as huge fees, will prevent CCC cosign.
|
||||
|
||||
|
||||
@ -38,7 +38,7 @@ directly from python programs.
|
||||
|
||||
| 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 e000 | 8k | MCU keys, consumable; 256 32-bit write-once slots.
|
||||
| 0x0802 0000 | 16k | Interrupt handlers, file header (Micropython and Coldcard code)
|
||||
|
||||
@ -30,7 +30,6 @@
|
||||
Tapsigner Backup
|
||||
Seed XOR
|
||||
Migrate Coldcard
|
||||
Key Teleport (start)
|
||||
Help
|
||||
Advanced/Tools
|
||||
View Identity
|
||||
@ -49,13 +48,13 @@
|
||||
Import XPRV
|
||||
Tapsigner Backup
|
||||
Coldcard Backup
|
||||
Restore Seed XOR
|
||||
Upgrade Firmware [IF NOT TMP SEED]
|
||||
Show Version
|
||||
From MicroSD
|
||||
From VirtDisk [IF VIRTDISK ENABLED]
|
||||
File Management
|
||||
Verify Backup
|
||||
Teleport Multisig PSBT [IF QR AND SECRET]
|
||||
List Files
|
||||
Verify Sig File
|
||||
NFC File Share [IF NFC ENABLED]
|
||||
@ -158,25 +157,29 @@
|
||||
Delete PSBTs
|
||||
Default Keep
|
||||
Delete PSBTs
|
||||
Buried Settings
|
||||
Home Menu XFP [IF SECRET AND NOT TMP SEED]
|
||||
Only Tmp
|
||||
Always Show
|
||||
Menu Wrapping
|
||||
Default
|
||||
Always Wrap
|
||||
[QR key shortcut] [IF QR SCANNER]
|
||||
Menu Wrapping
|
||||
Default Off
|
||||
Enable
|
||||
Home Menu XFP [IF SECRET AND NOT TMP SEED]
|
||||
Only Tmp
|
||||
Always Show
|
||||
---
|
||||
|
||||
[NORMAL OPERATION]
|
||||
Ready To Sign
|
||||
Passphrase [IF WORD BASED SEED]
|
||||
Restore Saved
|
||||
c*******
|
||||
[3A14F788]
|
||||
Restore Saved [MAYBE]
|
||||
A***********
|
||||
[0C52BAD4]
|
||||
Restore
|
||||
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]
|
||||
Start HSM Mode [IF HSM POLICY]
|
||||
Address Explorer
|
||||
@ -194,44 +197,35 @@
|
||||
Account Number
|
||||
Custom Path
|
||||
CC-2-of-4
|
||||
Secure Notes & Passwords [IF ENBALED] [MAYBE]
|
||||
1: note0
|
||||
"note0"
|
||||
Secure Notes & Passwords [IF ENBALED]
|
||||
1: note1
|
||||
"note1"
|
||||
View Note
|
||||
Edit
|
||||
Delete
|
||||
Export
|
||||
Sign Note Text
|
||||
2: secret-PWD
|
||||
"secret-PWD"
|
||||
↳ satoshi
|
||||
↳ abc.org
|
||||
SHORTCUT
|
||||
SHORTCUT
|
||||
2: nostr
|
||||
"nostr"
|
||||
↳ scg
|
||||
↳ brb.io
|
||||
View Password
|
||||
Send Password [MAYBE]
|
||||
Export
|
||||
Edit Metadata
|
||||
Delete
|
||||
Change Password
|
||||
Sign Note Text
|
||||
SHORTCUT
|
||||
SHORTCUT
|
||||
New Note
|
||||
New Password
|
||||
Export All
|
||||
Sort By Title
|
||||
Import
|
||||
Type Passwords [MAYBE]
|
||||
Seed Vault [MAYBE]
|
||||
1: [7126EB3C]
|
||||
[7126EB3C]
|
||||
Use This Seed
|
||||
Rename
|
||||
Delete
|
||||
2: [CCEE13B9]
|
||||
[CCEE13B9]
|
||||
Use This Seed
|
||||
Rename
|
||||
Delete
|
||||
3: [03EE9989]
|
||||
[03EE9989]
|
||||
1: [B14E9AE0]
|
||||
[B14E9AE0]
|
||||
Use This Seed
|
||||
Rename
|
||||
Delete
|
||||
@ -242,19 +236,17 @@
|
||||
Restore Backup
|
||||
Clone Coldcard
|
||||
Export Wallet
|
||||
Sparrow
|
||||
Cove
|
||||
Bitcoin Core
|
||||
Nunchuk
|
||||
Bull Bitcoin
|
||||
Blue Wallet
|
||||
Electrum Wallet
|
||||
Wasabi Wallet
|
||||
Fully Noded
|
||||
Unchained
|
||||
Sparrow Wallet
|
||||
Nunchuk
|
||||
Zeus
|
||||
Electrum Wallet
|
||||
Theya
|
||||
Bitcoin Safe
|
||||
Zeus
|
||||
Wasabi Wallet
|
||||
Unchained
|
||||
Lily Wallet
|
||||
Samourai Postmix
|
||||
Samourai Premix
|
||||
Descriptor
|
||||
@ -265,7 +257,6 @@
|
||||
P2WPKH/P2SH (BIP-49)
|
||||
Master XPUB
|
||||
Current XFP
|
||||
Key Expression
|
||||
Dump Summary
|
||||
Upgrade Firmware [IF NOT TMP SEED]
|
||||
Show Version
|
||||
@ -275,19 +266,17 @@
|
||||
Verify Backup
|
||||
Backup System
|
||||
Export Wallet
|
||||
Sparrow
|
||||
Cove
|
||||
Bitcoin Core
|
||||
Nunchuk
|
||||
Bull Bitcoin
|
||||
Blue Wallet
|
||||
Electrum Wallet
|
||||
Wasabi Wallet
|
||||
Fully Noded
|
||||
Unchained
|
||||
Sparrow Wallet
|
||||
Nunchuk
|
||||
Zeus
|
||||
Electrum Wallet
|
||||
Theya
|
||||
Bitcoin Safe
|
||||
Zeus
|
||||
Wasabi Wallet
|
||||
Unchained
|
||||
Lily Wallet
|
||||
Samourai Postmix
|
||||
Samourai Premix
|
||||
Descriptor
|
||||
@ -298,11 +287,10 @@
|
||||
P2WPKH/P2SH (BIP-49)
|
||||
Master XPUB
|
||||
Current XFP
|
||||
Key Expression
|
||||
Dump Summary
|
||||
Sign Text File
|
||||
Batch Sign PSBT
|
||||
Teleport Multisig PSBT
|
||||
Teleport Multisig PSBT [IF QR AND SECRET]
|
||||
List Files
|
||||
Verify Sig File
|
||||
NFC File Share [IF NFC ENABLED]
|
||||
@ -312,28 +300,29 @@
|
||||
Format SD Card
|
||||
Format RAM Disk [IF VIRTDISK ENABLED]
|
||||
Secure Notes & Passwords [IF QWERTY KEYBOARD]
|
||||
1: note0
|
||||
"note0"
|
||||
1: note1
|
||||
"note1"
|
||||
View Note
|
||||
Edit
|
||||
Delete
|
||||
Export
|
||||
Sign Note Text
|
||||
2: secret-PWD
|
||||
"secret-PWD"
|
||||
↳ satoshi
|
||||
↳ abc.org
|
||||
SHORTCUT
|
||||
SHORTCUT
|
||||
2: nostr
|
||||
"nostr"
|
||||
↳ scg
|
||||
↳ brb.io
|
||||
View Password
|
||||
Send Password [MAYBE]
|
||||
Export
|
||||
Edit Metadata
|
||||
Delete
|
||||
Change Password
|
||||
Sign Note Text
|
||||
SHORTCUT
|
||||
SHORTCUT
|
||||
New Note
|
||||
New Password
|
||||
Export All
|
||||
Sort By Title
|
||||
Import
|
||||
Derive Seeds (BIP-85)
|
||||
View Identity
|
||||
@ -352,17 +341,14 @@
|
||||
Import XPRV
|
||||
Tapsigner Backup
|
||||
Coldcard Backup
|
||||
Restore Seed XOR
|
||||
Key Teleport (start)
|
||||
Spending Policy [IF SECRET AND NOT TMP SEED]
|
||||
Single-Signer [IF SECRET AND NOT TMP SEED]
|
||||
Co-Sign Multisig (CCC) [IF NOT TMP SEED]
|
||||
HSM Mode [IF HSM AND SECRET]
|
||||
Default Off
|
||||
Enable
|
||||
User Management [MAYBE]
|
||||
Paper Wallets
|
||||
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]
|
||||
Sign PSBT
|
||||
Show Address
|
||||
@ -371,7 +357,7 @@
|
||||
Verify Address
|
||||
File Share
|
||||
Import Multisig
|
||||
Push Transaction [IF PUSHTX ENABLED]
|
||||
Push Transaction [IF ENBALED]
|
||||
Danger Zone
|
||||
Debug Functions
|
||||
Seed Functions
|
||||
@ -412,33 +398,22 @@
|
||||
Settings Space
|
||||
MCU Key Slots
|
||||
Bless Firmware
|
||||
Reflash GPU [IF QWERTY KEYBOARD]
|
||||
Wipe LFS
|
||||
Nuke Device
|
||||
Settings
|
||||
Login Settings
|
||||
Change Main PIN
|
||||
Trick PINs [IF SECRET AND NOT TMP SEED]
|
||||
Trick PINs:
|
||||
↳11-11
|
||||
PIN 11-11
|
||||
↳Bricks CC
|
||||
Hide Trick
|
||||
Delete Trick
|
||||
Change PIN
|
||||
↳333-3334
|
||||
PIN 333-3334
|
||||
↳123-254
|
||||
PIN 123-254
|
||||
↳Duress Wallet
|
||||
Activate Wallet
|
||||
Hide Trick
|
||||
Delete Trick
|
||||
Change PIN
|
||||
↳WRONG PIN
|
||||
After 3 wrong:
|
||||
↳Wipes seed
|
||||
↳Reboots
|
||||
Hide Trick
|
||||
Delete Trick
|
||||
Add New Trick
|
||||
Add If Wrong
|
||||
Delete All
|
||||
Set Nickname
|
||||
Scramble Keys
|
||||
@ -483,12 +458,14 @@
|
||||
View Details
|
||||
Delete
|
||||
Coldcard Export
|
||||
Electrum Wallet
|
||||
Descriptors
|
||||
View Descriptor
|
||||
Export
|
||||
Bitcoin Core
|
||||
Import
|
||||
Electrum Wallet
|
||||
Import from File
|
||||
Import from QR [IF QR SCANNER]
|
||||
Import via NFC [IF NFC ENABLED]
|
||||
Export XPUB
|
||||
Create Airgapped
|
||||
Trust PSBT?
|
||||
@ -543,18 +520,17 @@
|
||||
Delete PSBTs
|
||||
Default Keep
|
||||
Delete PSBTs
|
||||
Menu Wrapping
|
||||
Default Off
|
||||
Enable
|
||||
Home Menu XFP [IF SECRET AND NOT TMP SEED]
|
||||
Only Tmp
|
||||
Always Show
|
||||
Keyboard EMU
|
||||
Default Off
|
||||
Enable
|
||||
Buried Settings
|
||||
Home Menu XFP [IF SECRET AND NOT TMP SEED]
|
||||
Only Tmp
|
||||
Always Show
|
||||
Menu Wrapping
|
||||
Default
|
||||
Always Wrap
|
||||
Secure Logout
|
||||
[NFC key shortcut] [IF NFC ENABLED]
|
||||
SHORTCUT [IF NFC ENABLED]
|
||||
Sign PSBT
|
||||
Show Address
|
||||
Sign Message
|
||||
@ -562,7 +538,7 @@
|
||||
Verify Address
|
||||
File Share
|
||||
Import Multisig
|
||||
Push Transaction [IF PUSHTX ENABLED]
|
||||
Push Transaction [IF ENBALED]
|
||||
---
|
||||
|
||||
[FACTORY MODE]
|
||||
@ -574,151 +550,3 @@
|
||||
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,
|
||||
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.
|
||||
The resulting signature can be returned over SD card/Vdisk, NFC, or — on Q — as a QR code.
|
||||
and Mk4 can also sign messages sent to COLDCARD via NFC.
|
||||
|
||||
Signature format follows [BIP-0137](https://github.com/bitcoin/bips/blob/master/bip-0137.mediawiki) specification.
|
||||
COLDCARD Mk3 and COLDCARD Mk4 up to version `5.1.0` used compressed P2PKH header byte for all script types.
|
||||
From 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
|
||||
|
||||
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
|
||||
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.
|
||||
@ -22,7 +21,7 @@ Bitcoin core can only verify P2PKH.
|
||||
|
||||
## 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`.
|
||||
|
||||
### Message construction and signature file format
|
||||
@ -40,6 +39,8 @@ IFOvGVJrm31S0j+F4dVfQ5kbRKWKcmhmXIn/Lw8iIgaCG5QNZswjrN4X673R7jTZo1kvLmiD4hlIrbuL
|
||||
-----END BITCOIN SIGNATURE-----
|
||||
```
|
||||
|
||||
### 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.
|
||||
@ -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`
|
||||
* Generic XPUBs export: signed by the key corresponding to the first external address of own standard P2WSH derivation `m/48h/<coin_type>h/<account>h/2h/0/0`
|
||||
* Multisig address explorer export: Signed by own key at the same derivation as first (0th) row on exported list. `my_key/<change>/<start_index>`
|
||||
|
||||
### What is NOT signed
|
||||
|
||||
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
|
||||
|
||||
The NFC antenna location depends on the hardware:
|
||||
|
||||
- **Mk4**: a PCB trace loop, centered under number `8` on the keypad.
|
||||
- **Mk5**: a discrete coil (`L6`) in the **top-right corner** of the device
|
||||
- **Q1**: a flexible "sticker" antenna behind the display. The green LED below the
|
||||
bottom-right of the display (`D12`) lights up while an NFC transfer is active —
|
||||
it is the activity indicator, not the antenna.
|
||||
|
||||

|
||||
|
||||
Before using NFC,
|
||||
Mk4 NFC antenna is centered under number `8` on the keypad. Before using NFC,
|
||||
it is important to locate the position of NFC antenna on your device and point it
|
||||
correctly towards the Coldcard NFC antenna. Picture below shows an example with iPhone
|
||||
that has NFC antenna located at the top right edge. The NFC smartphone antenna
|
||||
@ -46,7 +36,7 @@ in general. Good interoperability is critical with radio standards.
|
||||
|
||||
## 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
|
||||
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
|
||||
the Coldcard is powered-down, regardless of the NFC setting.
|
||||
|
||||
If the above is not enough for you, the antenna can be destroyed:
|
||||
|
||||
- **Mk4**: cut the trace labeled "NFC" inside the hole for the MicroSD card,
|
||||
using the point of a sharp knife to cut and peel up the trace.
|
||||
- **Mk5**: has no such trace — its antenna is the discrete coil `L6` in the
|
||||
top-right corner, which would have to be physically removed instead.
|
||||
- **Q1**: cut the trace labeled "NFC DATA" under the batteries.
|
||||
If the above is not enough for you, the antenna can be destroyed
|
||||
by cutting the trace labeled "NFC" inside the hole for the MicroSD
|
||||
card. Use the point of a sharp knife to cut and peel up the trace.
|
||||
|
||||
The NFC traffic is not encrypted and is subject to eavesdropping.
|
||||
While the NFC feature is active, your Coldcard can be uniquely
|
||||
|
||||
@ -34,7 +34,7 @@ The COLDCARD needs a URL prefix. To that it appends some values:
|
||||
- when RegTest is enabled, the value will be `XRT`
|
||||
|
||||
We provide a few default URL values to our customers, including one backend we
|
||||
will operate on `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.
|
||||
|
||||
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.
|
||||
|
||||
```makefile
|
||||
repro: submods-match code-committed
|
||||
repro:
|
||||
docker build -t coldcard-build - < dockerfile.build
|
||||
(cd ..; docker run $(DOCK_RUN_ARGS) sh src/stm32/repro-build.sh $(VERSION_STRING) $(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:
|
||||
|
||||
```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!
|
||||
...
|
||||
|
||||
@ -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."
|
||||
|
||||
- `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.
|
||||
|
||||
|
||||
@ -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
|
||||
| `firmware` | slot 14 | SHA256d | SE1 | Firmware checksum, controls green/red LEDs
|
||||
| `nonce/chksum` | slot 10 | data | SE1 | AES nonce and GMAC tag, protected by PIN
|
||||
| `SE2 easy key` | page 14 | AES via HMAC | SE2 | Another SE2 part of AES seed key
|
||||
| `SE2 hard key` | page 15 | AES via ECC | SE2 | SE2's part of AES seed key; ECC used to unlock
|
||||
| `SE2 easy key` | page 15 | AES via HMAC | SE2 | Another SE2 part of AES seed key
|
||||
| `SE2 hard key` | page 14 | AES via ECC | SE2 | SE2's part of AES seed key; ECC used to unlock
|
||||
| `tpin key` | `tpin_key` | HMAC(key) | MCU | Key for HMAC used to encrypt trick PINs
|
||||
| `trick PIN slots` | pages 0-12 | HMAC | SE2 | Protect duress wallet seeds and pins (6 spots)
|
||||
| `SE2 trash` | secret B | HMAC | SE2 | Used to destroy values (only SE2 knows the value)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# COLDCARD Mk4/Mk5/Q Security Model
|
||||
# COLDCARD Mk4/Q Security Model
|
||||
|
||||
## 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.
|
||||
|
||||
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
|
||||
(or 2001, 2002, 2003 for a 12-word one). Because this is BIP-85
|
||||
based, it behaves exactly like a normal wallet. Defining a passphrase
|
||||
for the wallet is also possible.
|
||||
based on account numbers 1001, 1002, or 1003. Because this is BIP-85
|
||||
based and uses a 24-word seed, it behaves exactly like a normal
|
||||
wallet. Defining a passphrase for the wallet is also possible.
|
||||
|
||||
The Mk4 also supports older COLDCARD duress wallets and their UTXOs
|
||||
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
|
||||
|
||||
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
|
||||
COLDCARD was powered down during the upgrade process. At that point,
|
||||
the intended firmware image has been lost because it it held in
|
||||
|
||||
@ -12,7 +12,7 @@ are not discrete and you could be compelled to produce the passphrase.
|
||||
|
||||
Enter [_Seed XOR_](https://seedxor.com), a plausibly deniable means
|
||||
of storing secrets in two or more parts that look and behave just
|
||||
like the original secret. One 12-, 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
|
||||
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
|
||||
@ -78,12 +78,10 @@ words right the next day.
|
||||
|
||||
When the parts are made deterministically, we take a double-SHA256 over
|
||||
a fixed string (`Batshitoshi`), your master secret, and the text
|
||||
`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
|
||||
them) from the Coldcard's True Random Number Generator (TRNG). The number
|
||||
of bytes matches your secret length: 16, 24, or 32 bytes for a 12-, 18-,
|
||||
or 24-word seed respectively.
|
||||
In random mode, we simply pick 32 random bytes (and then double-SHA256
|
||||
them) from the Coldcard's True Random Number Generator (TRNG)..
|
||||
|
||||
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
|
||||
@ -159,12 +157,6 @@ with the others on a SEEDPLATE.
|
||||
- right to A, down to B ... take that number, and go to that column
|
||||
- down to C, that is answer: a ⊕ b ⊕ c
|
||||
|
||||
## Open Standard
|
||||
Seed XOR is an open standard. Other software and hardware wallets are encouraged to
|
||||
implement support. No license or permission is required, including usage of the term
|
||||
"Seed XOR" when referring to implementations of this feature. Such implementations
|
||||
should match the process described in this documentation and be fully interoperable.
|
||||
|
||||
---
|
||||
|
||||
# 24 Words XOR Seed Example Using 3 Parts
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
[_(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
|
||||
@ -42,7 +42,7 @@ Read more about `Seed Vault` feature below.
|
||||
- `24 words`
|
||||
- `XPRV (BIP-32)`
|
||||
- pick derivation `Index` in next prompt, or just press OK for index 0
|
||||
- Press (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
|
||||
- go to `Settings -> Login Settings -> Trick Pins`
|
||||
@ -66,7 +66,7 @@ Ability to generate and use **Temporary seed** is available on Coldcard when:
|
||||
|
||||
# Restore Master
|
||||
|
||||
[_(new in v5.2.0, requires Mk4, 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
|
||||
to their "master seed" (one stored in SE2). Once COLDCARD has temporary
|
||||
@ -84,7 +84,7 @@ Seed Vault entries can only be deleted in Seed Vault menu.
|
||||
|
||||
# Seed Vault
|
||||
|
||||
[_(new in v5.2.0, requires Mk4, 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
|
||||
recall and later use (AES-256-CTR encrypted with your master seed's key).
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
|
||||
# Firmware Upgrade and Recovery Process
|
||||
|
||||
_This document applies 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
|
||||
(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,
|
||||
but are not as robust as time-based tokens.
|
||||
|
||||
Web2FA is available to be enabled as part of a Spending Policy,
|
||||
both in Multisig and Single Signer modes. When enabled, you will be
|
||||
prompted complete 2FA authentication after viewing the details of
|
||||
the transaction to be signed. You will not be able to sign without
|
||||
the correct code.
|
||||
For now, Web2FA is only being used as part of CCC spending policy (opt-in),
|
||||
but we may find other uses for it.
|
||||
|
||||
## 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)
|
||||
- CC creates URL encrypted to the pubkey of server, containing args:
|
||||
- shared secret for TOTP (same value as held in user's phone)
|
||||
- the response nonce (32 bytes, shown as 64 hex chars, on Q; or 8 digits on Mk4)
|
||||
to be revealed to the user on successful auth
|
||||
- the response nonce (16 bytes, or 8 digits for Mk4) to be revealed to the user
|
||||
on successful auth
|
||||
- flag if Q model, so can provide a QR to be scanned in that case (rather than digits)
|
||||
- some text label for what's being approved, which is presented to user so they can pick
|
||||
correct 2fa shared secret.
|
||||
@ -65,7 +62,7 @@ the correct code.
|
||||
- multiplies that private key by server's known public key
|
||||
- apply sha256(resulting coordinate) => the session key
|
||||
- apply AES-256-CTR over URL contents (ascii text)
|
||||
- prepend 33 bytes of pubkey, and then base64url encode all of it
|
||||
- prepend 33 bytes of pubkey, and base64url encode all of it
|
||||
- full url is: `https://coldcard.com/2fa?{base64 encoded binary}`
|
||||
|
||||
## Trust Issues
|
||||
@ -82,15 +79,12 @@ the correct code.
|
||||
|
||||
## 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
|
||||
- `nm`: human readable label for the transaction/purpose
|
||||
- `is_q`: flag indicating use of QR to provide nonce back to user
|
||||
- `nonce`: text string that is either 8 digits for Mk4, or hex digits for QR
|
||||
- `nm`: human readable label for the transaction/purpose
|
||||
|
||||
Server will accept plaintext arguments as above, but normally everything
|
||||
after the question mark is encrypted.
|
||||
|
||||
2
external/ckcc-protocol
vendored
2
external/ckcc-protocol
vendored
@ -1 +1 @@
|
||||
Subproject commit 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')
|
||||
|
||||
mk5_nfc_1 = (126, 49, 16, 0, b'\x00\x7f\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0\xf0\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x0e\x00\xe0\x0e\x03\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\xe0\x0e\x00\xe0\x0e\x07\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xe0\x0e\x00\xe0\x0e\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\xe0\x0e\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x000D\x04\x10\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x000D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00(D\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00(D\x04\x00\x00\x00xp\x00\xff\xff\xff\xff\xfe0\x00$G\xe4\x00\x00\x00xp\x00\xff\xff\xff\xff\xfe0\x00$G\xe4\x00\x00\x00xp\x00\xe0\x0e\x00\xe0\x0e0\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e\x00\xe0\x0e0\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e\x00\xe0\x0e0\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xff\xff\xff\xff\xfe0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0\x0e?\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xff\xff\xff\xff\xfe\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\xff\xff\xff\xff\xfe\x0f\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x7f\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
|
||||
|
||||
mk5_nfc_2 = (118, 49, 15, 0, b'\x00\x7f\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0\xf0\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x0e\x00\xe0\x03\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\xe0\x0e\x00\xe0\x07\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xe0\x0e\x00\xe0\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\xe0\x0e\x00\xe00\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x000D\x04\x10\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x000D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00(D\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00(D\x04\x00\x00\x00xp\x00\xff\xff\xff\xff0\x00$G\xe4\x00\x00\x00xp\x00\xff\xff\xff\xff0\x00$G\xe4\x00\x00\x00xp\x00\xe0\x0e\x00\xe00\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e\x00\xe00\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e\x00\xe00\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xff\xff\xff\xff0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe00\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00\xe0?\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xff\xff\xff\xff\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\xff\xff\xff\xff\x0f\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x7f\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00')
|
||||
|
||||
mk5_nfc_3 = (110, 49, 14, 0, b'\x00\x7f\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0\xf0\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x0e\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\xe0\x0e\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xe0\x0e\x00\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\xe0\x0e\x000\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x000D\x04\x10\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x000D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00(D\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00(D\x04\x00\x00\x00xp\x00\xff\xff\xff0\x00$G\xe4\x00\x00\x00xp\x00\xff\xff\xff0\x00$G\xe4\x00\x00\x00xp\x00\xe0\x0e\x000\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e\x000\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e\x000\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xff\xff\xff0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x000\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e\x00?\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xff\xff\xff\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\xff\xff\xff\x0f\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x7f\xff\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00')
|
||||
|
||||
mk5_nfc_4 = (102, 49, 13, 0, b'\x00\x7f\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\xf0\xf0\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x0e\x03\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\xe0\x0e\x07\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xe0\x0e\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e0\x00 G\xe3\xe0\x00\x00\x7f\xf0\x00\xe0\x0e0\x000D\x04\x10\x00\x00\x7f\xf0\x00\xe0\x0e0\x000D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00(D\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00(D\x04\x00\x00\x00xp\x00\xff\xff\xb0\x00$G\xe4\x00\x00\x00xp\x00\xff\xff\xb0\x00$G\xe4\x00\x00\x00xp\x00\xe0\x0e0\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e0\x00"D\x04\x00\x00\x00xp\x00\xe0\x0e0\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00 \xc4\x04\x00\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x04\x10\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xff\xff\xb0\x00 D\x03\xe0\x00\x00\x7f\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e0\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x00\xe0\x0e?\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xff\xff\x9f\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\xff\xff\x8f\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x7f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00')
|
||||
|
||||
scroll = (3, 61, 1, 0, b'@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@@\xe0@')
|
||||
|
||||
selected = (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')
|
||||
|
||||
|
||||
@ -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
|
||||
X
|
||||
XX
|
||||
X X
|
||||
XX XX
|
||||
XX X
|
||||
XXXX
|
||||
XX
|
||||
xx
|
||||
xx
|
||||
xx
|
||||
xx
|
||||
xx xx
|
||||
xx xx
|
||||
xx xx
|
||||
xxx
|
||||
x
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
|
||||
# Coldcard Hardware Details
|
||||
|
||||
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
|
||||
researchers who wish to analyse the Coldcard more completely.
|
||||
|
||||
|
||||
# Schematic
|
||||
|
||||

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

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

|
||||
|
||||
`schematic-mark4d.png`
|
||||
@ -34,20 +30,27 @@ This is the Mark3 rev B schematic.
|
||||
|
||||
# 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.
|
||||
|
||||
- BOM for Q rev D: `bom-q1d.xlsx`
|
||||
- BOM for Mk5 rev F: `bom-mark5f.xlsx`
|
||||
- BOM for Mk4 rev D: `bom-mark4d.xlsx`
|
||||
- BOM for Mk3 rev B: `bom-mark3b.xlsx`
|
||||
|
||||
Not included are these minor bits:
|
||||
|
||||
- the plastic case (custom)
|
||||
- the secure bag (with barcode serial number)
|
||||
- pin-recovery card
|
||||
|
||||
`bom-q1d.xlsx`
|
||||
|
||||
- BOM for Q rev D.
|
||||
|
||||
`bom-mark4d.xlsx`
|
||||
|
||||
- BOM for Mk3 rev D.
|
||||
|
||||
`bom-mark3b.xlsx`
|
||||
|
||||
- BOM for Mk3 rev B.
|
||||
|
||||
# Important
|
||||
|
||||
- No promises that these files are 100% current because we constantly make quality improvements.
|
||||
|
||||
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.
|
||||
|
||||
# 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.
|
||||
- Requires a carefully crafted PSBT that does not represent a monetary transaction, but instead is demonstrating
|
||||
control over the keys for a list of UTXO, and commits to a short text message.
|
||||
- Read more [here](https://github.com/Coldcard/firmware/blob/master/docs/proof-of-reserves-bip-322.md).
|
||||
- New Feature: WIF Store. Ability to import foreign WIF keys (Wallet Import Format) and use them for PSBT signing.
|
||||
- New Feature: Export [BIP-380](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki) extended key expression.
|
||||
Navigate to "Advanced/Tools -> Export Wallet -> Key Expression"
|
||||
- New Feature: Transaction Input Explorer. Shows data about UTXO(s) being spent. Press (2) before approving
|
||||
transaction to enter Transaction Explorer.
|
||||
- New Feature: Support for v3 transactions in PSBT files.
|
||||
- New Feature: Option to type a derived BIP-85 secret as an emulated USB keyboard.
|
||||
- New Feature: Nuke Device: purges all sensitive data and makes your COLDCARD e-waste.
|
||||
- Enhancement: CCC debug menu allows you to reset block height.
|
||||
- Enhancement: Show the BIP-39 passphrase on-screen (must scroll down) once new key is in effect.
|
||||
- Enhancement: New "Buried Settings" menu, inside Settings menu, for rarely-applied settings.
|
||||
- Enhancement: Add "Blue Wallet" option to "Export Wallet"
|
||||
- Enhancement: Detect duplicated inputs in PSBT file.
|
||||
- Bugfix: Replace `/` with `-` in exported file names of multisig wallet export artifacts.
|
||||
- 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.
|
||||
|
||||
# Mk Specific Changes
|
||||
|
||||
## 5.5.0 - 2065-03-05
|
||||
# Mk4 Specific Changes
|
||||
|
||||
- This release supports both the newer Mk5 hardware and existing Mk4.
|
||||
- Enhancement: Show QR of XOR-split seeds.
|
||||
## 5.4.3 - 2025-05-14
|
||||
|
||||
- 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
|
||||
|
||||
## 1.4.0Q - 2065-03-05
|
||||
|
||||
- Bugfix: Empty notes in hobbled mode caused yikes upon menu entry.
|
||||
## 1.3.3Q - 2025-05-14
|
||||
|
||||
- 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
|
||||
|
||||
@ -1,58 +1,5 @@
|
||||
*See ChangeLog.md for more recent changes, these are historic versions*
|
||||
|
||||
|
||||
## 5.4.5 - 2025-11-03
|
||||
|
||||
- Enhancement: Address format guessing changed away from using PSBT XPUB's derivation paths.
|
||||
Now based on witness/redeem script of first PSBT input instead.
|
||||
- Enhancement: Show master XFP of backup secret and ask user for confirmation before loading backup.
|
||||
- Enhancement: Show firmware version added to hobbled Advanced/Tools menu.
|
||||
- Bugfix: Exiting text input of Custom Backup Password caused yikes.
|
||||
- Bugfix: Temporary seeds in SSSP mode were not able to update block height.
|
||||
|
||||
## 5.4.4 - 2025-09-30
|
||||
|
||||
- Spending policies for "Single Signers" adds new spending policy options:
|
||||
- limit your Coldcard so it refuses to sign transactions that are "too big"
|
||||
- require 2FA authentication before signing any transaction (NFC+web)
|
||||
- velocity limits can restrict how often new transactions can be signed
|
||||
- see `docs/spending-policy.md` for more details
|
||||
- "Enable HSM" and "User Management" have moved into `Advanced > Spending Policy`.
|
||||
- Old "CCC" feature has been renamed and moved into that menu as well: "Co-Sign Multisig"
|
||||
- Added `Bull Bitcoin` export to `Export Wallet` menu.
|
||||
- Enhancement: Added warning for zero value outputs if not `OP_RETURN`.
|
||||
- Enhancement: Show QR codes of output addresses in transaction output explorer. Explorer is
|
||||
now offered for transactions of all sizes, not just complex ones.
|
||||
- Enhancement: Added file rename, when listing contents of SD card.
|
||||
- Enhancement: Added ability to restore Coldcard backup via USB (needs latest of ckcc version)
|
||||
- Enhancement: Address ownership allows to specify particular multisig wallet in which to search,
|
||||
if `wallet` query parameter is provided via trivial extension to
|
||||
[BIP-21](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki).
|
||||
Example: `tb1q4d67p7stxml3kdudrgkg5mgaxsrgzcqzjrrj4gg62nxtvnsnvqjsxjkej0?wallet=Haystack`
|
||||
- Bugfix: If all change outputs have `nValue=0`, they were not shown in UX.
|
||||
- Bugfix: Disallow negative input/output amounts in PSBT.
|
||||
- Bugfix: Fix filesystem initialization after Wipe LFS or Destroy Seed.
|
||||
- Bugfix: Fix MicroSD selftest code.
|
||||
- Bugfix: NFC loop exporting secrets would not work after first value exported.
|
||||
- Bugfix: Multisig address format handling.
|
||||
- Bugfix: Ownership check failing to find addresses near max (~760), needed to be re-run to succeed
|
||||
- (Mk4 only) Bugfix: Part of extended keys (xpubs) were not always visible.
|
||||
- (Mk4 only) Change: Mk4 default menu wrap-around lowered from 16 to 10 items.
|
||||
|
||||
|
||||
## 5.4.3 - 2025-05-14
|
||||
|
||||
- Enhancement: Text word-wrap done more carefully so never cuts off any text, and yet
|
||||
doesn't waste space.
|
||||
- Bugfix: `Add current tmp` option, which could be shown in `Seed Vault` menu under
|
||||
specific circumstances, would corrupt master settings if selected.
|
||||
- Bugfix: PUSHDATA2 in bitcoin script caused yikes.
|
||||
- Bugfix: Warning for unknown scripts was not shown at the top of the signing story.
|
||||
- Bugfix: With both NFC & Virtual Disk OFF, user cannot exit `Export Wallet` menu. Gets stuck
|
||||
in export loop and needs reboot to escape.
|
||||
- Bugfix: Part of extended keys in stories were not always visible.
|
||||
|
||||
|
||||
## 5.4.2 - 2025-04-16
|
||||
|
||||
- Huge new feature: CCC - ColdCard Cosign
|
||||
|
||||
@ -1,62 +1,5 @@
|
||||
*See ChangeLog.md for more recent changes, these are historic versions*
|
||||
|
||||
|
||||
## 1.3.5Q - 2025-11-03
|
||||
|
||||
- Enhancement: Address format guessing changed away from using PSBT XPUB's derivation paths.
|
||||
Now based on witness/redeem script of first PSBT input instead.
|
||||
- Enhancement: Show master XFP of backup secret and ask user for confirmation before loading backup.
|
||||
- Enhancement: Show firmware version added to hobbled Advanced/Tools menu.
|
||||
- Bugfix: Exiting text input of Custom Backup Password caused yikes.
|
||||
- Bugfix: Temporary seeds in SSSP mode were not able to update block height.
|
||||
- Enhancement: Show backup filename at the top of the screen during backup password entry.
|
||||
|
||||
|
||||
## 1.3.4Q - 2025-09-30
|
||||
|
||||
- Spending policies for "Single Signers" adds new spending policy options:
|
||||
- limit your Coldcard so it refuses to sign transactions that are "too big"
|
||||
- require 2FA authentication before signing any transaction (NFC+web)
|
||||
- velocity limits can restrict how often new transactions can be signed
|
||||
- see `docs/spending-policy.md` for more details
|
||||
- "Enable HSM" and "User Management" have moved into `Advanced > Spending Policy`.
|
||||
- Old "CCC" feature has been renamed and moved into that menu as well: "Co-Sign Multisig"
|
||||
- Added `Bull Bitcoin` export to `Export Wallet` menu.
|
||||
- Enhancement: Added warning for zero value outputs if not `OP_RETURN`.
|
||||
- Enhancement: Show QR codes of output addresses in transaction output explorer. Explorer is
|
||||
now offered for transactions of all sizes, not just complex ones.
|
||||
- Enhancement: Added file rename, when listing contents of SD card.
|
||||
- Enhancement: Added ability to restore Coldcard backup via USB (needs latest of ckcc version)
|
||||
- Enhancement: Address ownership allows to specify particular multisig wallet in which to search,
|
||||
if `wallet` query parameter is provided via trivial extension to
|
||||
[BIP-21](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki).
|
||||
Example: `tb1q4d67p7stxml3kdudrgkg5mgaxsrgzcqzjrrj4gg62nxtvnsnvqjsxjkej0?wallet=Haystack`
|
||||
- Bugfix: If all change outputs have `nValue=0`, they were not shown in UX.
|
||||
- Bugfix: Disallow negative input/output amounts in PSBT.
|
||||
- Bugfix: Fix filesystem initialization after Wipe LFS or Destroy Seed.
|
||||
- Bugfix: Fix MicroSD selftest code.
|
||||
- Bugfix: NFC loop exporting secrets would not work after first value exported.
|
||||
- Bugfix: Multisig address format handling.
|
||||
- Bugfix: Ownership check failing to find addresses near max (~760), needed to be re-run to succeed
|
||||
- (Q only) Enhancement: Enters "forever calculator" mode when Q would otherwise be electronic waste
|
||||
(ie. after 13 PIN failures). Always enabled, regardless of "login calculator" setting.
|
||||
- (Q only) Bugfix: Correct line positioning when 24 seed words displayed.
|
||||
|
||||
|
||||
## 1.3.3Q - 2025-05-14
|
||||
|
||||
- Enhancement: Text word-wrap done more carefully so never cuts off any text, and yet
|
||||
doesn't waste space.
|
||||
- Bugfix: `Add current tmp` option, which could be shown in `Seed Vault` menu under
|
||||
specific circumstances, would corrupt master settings if selected.
|
||||
- Bugfix: PUSHDATA2 in bitcoin script caused yikes.
|
||||
- Bugfix: Warning for unknown scripts was not shown at the top of the signing story.
|
||||
|
||||
- Bugfix: Do not allow to teleport PSBTs from SD card when CC has no secrets.
|
||||
- Bugfix: Calculator login mode: added "rand()" command, removed support
|
||||
for variables/assignments.
|
||||
|
||||
|
||||
## 1.3.2Q - 2025-04-16
|
||||
|
||||
- Feature: Key Teleport -- Easily and securely move seed phrases, secure notes/passwords,
|
||||
|
||||
@ -2,73 +2,23 @@
|
||||
|
||||
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
|
||||
(read more [BIP-322 Proof of Reserves documentation](../docs/proof-of-reserves-bip-322.md) )
|
||||
- Enhancement: WIF Store export watch-only descriptor
|
||||
- Enhancement: WIF Store address detection without the need for PSBT_IN_BIP32_DERIVATION (Electrum support)
|
||||
- Enhancement: Improve USB length validation
|
||||
- Bugfix: Fixes legacy input amount spoofing by rejecting witness-utxo-only PSBT inputs when Coldcard is expected to sign a non-segwit input.
|
||||
When both UTXO fields are present the full non_witness_utxo is now preferred for amount/script lookup. Thanks, @Damir
|
||||
- Bugfix: Emit warning and do not calculate fee for legacy UTXOs with only witness utxo
|
||||
- Bugfix: Disable Virtual Disk and NFC before activating HSM
|
||||
- Bugfix: P2PK signing was broken. Now supports both compressed and uncompressed P2PK spend
|
||||
- Bugfix: Custom address default menu position wrong
|
||||
- Bugfix: Delta Mode Trick PIN was never restored from backup
|
||||
- Bugfix: Proper error message for incorrect 7z headers
|
||||
- Bugfix: Exiting nickname entry with nickname already saved deleted previous nickname
|
||||
- Bugfix: "Send Password" menu item inside Notes & Passwords visibility reversed
|
||||
- Bugfix: Yikes when using "Send Password" on entry with password None field
|
||||
- Bugfix: Do not show "Saving..." UX after failed Notes & Passwords import
|
||||
- Bugfix: Incorrect error message caused by error in Verify/Decrypt Backup
|
||||
- Bugfix: NFC Verify Address raised incorrect error message
|
||||
- Bugfix: Notes & Passwords bulk import JSON with BBQr encoded as text
|
||||
- Bugfix: CCC key C challenge handled bad BIP-39 checksum by crashing the UX; now treated as a wrong attempt (counts toward 3-strike lockout)
|
||||
- Bugfix: CCC magnitude reset from CANCEL on empty input
|
||||
- Bugfix: OP_RETURN in CCC with whitelist enabled caused yikes
|
||||
- Bugfix: TX Explorer crashed on foreign input with non-standard sighash
|
||||
- Bugfix: Malformed JSON message-sign request crashed signing UX
|
||||
- Bugfix: Reject UI-control bytes in JSON / QR text message-signing
|
||||
- Bugfix: Non-standard OP_RETURN outputs shown as "null-data", hiding part of the script
|
||||
- Bugfix: Over-limit CCC address-whitelist import was rejected but still modified the policy
|
||||
- Bugfix: Deleting a file right after renaming it (List Files) blanked the old name, leaving the renamed file
|
||||
- Bugfix: Reordered `multi(...)` multisig with same keys was misreported as name-only change. Now blocked as duplicate.
|
||||
- Bugfix: Max WIF store capacity limit was ignored if saving via QR WIF visualization
|
||||
- Bugfix: Force Seed XOR restore from Temporary Seed menu to remain temporary even when master seed is blank
|
||||
- Bugfix: Q1 seed word entry cursor alignment for 12-word seeds and preserve visible words after failed QR scans
|
||||
- Bugfix: Binary signed-transaction (.txn) failed in NFC/QR file share
|
||||
- Bugfix: yikes in transaction explorer for goto index for tx with only one output
|
||||
- Bugfix: Sending `signmessage` payload encoded as BBQr caused yikes
|
||||
- Bugfix: CCC/SSSP NFC whitelist import caused Yikes
|
||||
- Bugfix: Stricter address ownership validation rejects unrecognized payment addresses before wallet search
|
||||
- Bugfix: Handle malformed NDEF records robustly. Thanks, @Damir
|
||||
- Bugfix: Ignore `bkpw` if added to backup. Thanks [@dmonakhov](https://github.com/dmonakhov)
|
||||
- Bugfix: Keep NFC export tag live for repeated probes
|
||||
- Bugfix: Fix 1of1 multisig signing failure
|
||||
- Bugfix: If all change outputs have `nValue=0` they're not shown in UX
|
||||
- Bugfix: Disallow negative input/output amounts in PSBT
|
||||
- Enhancement: Add warning for zero value outputs if not OP_RETURNs
|
||||
- Enhancement: Show QR codes of output addresses in Txn output explorer. Output explorer is offered for txns of all sizes.
|
||||
|
||||
# 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
|
||||
|
||||
## 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
|
||||
|
||||
95eff9e044cdb6b3d00961ae72d450684d5441c6a3661ab550a3c3aa0882e754 README.md
|
||||
412597a0e30684400cb61ee04650c13ef9fc3dc16fc2570bd5e33a1dc0085d7a Next-ChangeLog.md
|
||||
72458ab9eb2872d263bf4d3f4ca0fbf0ff9c6186f08d27f13fd600cb511ed2a7 History-Q.md
|
||||
d4891b509915800650a881556cca37604caab7a268afc0b1ed31021cea125891 History-Mk4.md
|
||||
3ba92e73d5260656641828e962e8eae4590f59774150d14276818a5229daf734 Next-ChangeLog.md
|
||||
0173cade759704320e7a43810dabd5f18cf2034b447c6c7996f447c8d3ad21de History-Q.md
|
||||
e6192bd7c2b27df7c9d8e58ae9a41bda4ef0615991c3159fb05ff60dc3cfedd1 History-Mk4.md
|
||||
c8ad43b4e3f9d77777026da6d1210c6fc5cfe435bcfcd241c0f67c9392ad7b82 History-Mk3.md
|
||||
9ebab063b57ff07e5d8df20c266ac94736a6ad0e4c71ad1f1db46ec16b0c94be ChangeLog.md
|
||||
b2fa9f4b9a9778b71cf4b09ad79732192fdb457214f868a3af5234094deea33f 2026-03-25T1408-v6.5.0X-mk-coldcard.dfu
|
||||
f7bed9f1d2d49a35e7c53c8208e73ceaccbee2ab3e7fcd7c020fbd4923140313 2026-03-25T1407-v6.5.0QX-q1-coldcard.dfu
|
||||
2b7b4d95cd5d606b0a32e692db7a27c1d860140c6e919e20ea6672ad6afc3088 2026-03-05T2052-v5.5.0-mk-coldcard.dfu
|
||||
15f26aa0b8fe33e29e338b74acc52d5532922af56bcf486e38085de55c86b82a 2026-03-05T2052-v5.5.0-mk-coldcard-factory.dfu
|
||||
b7ce3ba55ae4bb1e1ebe0090507bcada5a4439e57a19552c78da7ef103bd144c 2026-03-05T2051-v1.4.0Q-q1-coldcard.dfu
|
||||
82c2ed5ee5cf75cc8af0f54839b9a1bc8e4a329174657d61861a6576fb6e31d9 2026-03-05T2051-v1.4.0Q-q1-coldcard-factory.dfu
|
||||
372fa1f82e54f632574c56a695a1ed332464bf029bd733b2db2131a591d8f126 2025-11-25T1618-v6.4.1X-mk4-coldcard.dfu
|
||||
1059560fb598e5e8fd6aed0164aa4cad166552bf8e47a0365e986429c9a15346 2025-11-25T1617-v6.4.1QX-q1-coldcard.dfu
|
||||
f04617b52fc0db6e95cac0dddd9ddd90754219f38b63a26d08c848e208069edb 2025-11-20T1602-v6.4.0X-mk4-coldcard.dfu
|
||||
371f13f3e1a5ef28d14933daf03820f0e51d26ffa96008dd5595da0dfac646cf 2025-11-20T1601-v6.4.0QX-q1-coldcard.dfu
|
||||
7076ae29c509d3120db0fae434c132e6abd3fb79c1a2a2f1383ab3b2acaba27c 2025-11-03T1527-v5.4.5-mk4-coldcard.dfu
|
||||
00a337888ff86bf875bcfdab7a734981bce29a49f94f3df9f932924765848ab0 2025-11-03T1527-v5.4.5-mk4-coldcard-factory.dfu
|
||||
ff6371545943518eb4eb00ba73b6aa3a5ac4e63459621ecec8a300c28c281b3c 2025-11-03T1525-v1.3.5Q-q1-coldcard.dfu
|
||||
0ce02c8e549cb67b682d621b4a628f3fba2c56350a9ab090b9f08532f49e7afa 2025-11-03T1525-v1.3.5Q-q1-coldcard-factory.dfu
|
||||
8a8c94e5f64d0bfe4914a236fb8a779f956989fe8de998133b85b23920f46283 2025-09-30T1238-v5.4.4-mk4-coldcard.dfu
|
||||
0d0aba89027d5127f74b2a2b777a7c592cba12903a3c4c3ce9b0e060c09dddb7 2025-09-30T1238-v5.4.4-mk4-coldcard-factory.dfu
|
||||
bc9918968b67fefe634342c77513c9c354e7821e9ff002c7e5c8c356d7507892 2025-09-30T1237-v1.3.4Q-q1-coldcard.dfu
|
||||
00cb1fc2ef360aacf48ba8c9dd2167b3f5c5f1241ba1b2b17d61ea1b7bff0a45 2025-09-30T1237-v1.3.4Q-q1-coldcard-factory.dfu
|
||||
6e8b95855e05dc7889b1476acfb1854107b4e8df6f12cdf4a643a9776e60c798 ChangeLog.md
|
||||
be166b3bb3ec2259991db998c20c3d44e88eeaa73c2b8114f31cb14cab5e66e6 2025-05-14T1344-v5.4.3-mk4-coldcard.dfu
|
||||
876932d4ea7634d268145d5bf45577c7198c9d60e8a271b5079faba4d4c91acd 2025-05-14T1344-v5.4.3-mk4-coldcard-factory.dfu
|
||||
aaed0b90be5de310c8ac9f2d0cb3a7eea58923a53d349eb4b9ac8a902e5cba4e 2025-05-14T1343-v1.3.3Q-q1-coldcard.dfu
|
||||
@ -127,12 +109,12 @@ f05bc8dfed047c0c0abe5ed60621d2d14899b10717221c4af0942d96a1754f33 2021-03-29T192
|
||||
bea27f263b524a66b3ed0a58c16805e98be0d7c3db20c2f7aab3238f2c6a6995 2019-12-19T1623-v3.0.6-coldcard.dfu
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmnD9ncACgkQo6MbrVoq
|
||||
WxD9RAf+JkP/XVUPMDyfz+79AxBWFNU9r6RuYzXdzX3Z/XCKomZCZDtV7Ak6XlZi
|
||||
GTNfsUNHaPC8WP6smFzYg07NoY2U1fVdY7+qeOi7UXF0hBBDJw7Gsa49P2zmt+DB
|
||||
lfzivQG2n+mT4cM64Z0WF3BYBWmCuDJdctqUAnLJe2p8bh6S8n5hFeKqndRhffNK
|
||||
773amkUrDW3RkHkIuevH4MQlR4ozWBmHzcehFDlTYT8BVLR8gg6hBzBEylxyDJNO
|
||||
Ld4W5SzsT6We0RGX2uOpMERDjkizqT9t5J63drzpuPrUQA8XVQPaOc07vpFHRbbZ
|
||||
BhA61XO8yazNLVvata611pSTikNnDQ==
|
||||
=8Ti0
|
||||
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmgknkYACgkQo6MbrVoq
|
||||
WxBkuggAqTFP4YJdkzdNPbPDxtnCL4ZFJ+Rtnybp9JigTazbMvA/pjR+uODPFI3M
|
||||
Pm8I6kNPY8lMOPptEiFpNHn8EL8i2jOdH4NcmSP9OYInCRWyknm8fbmboSkOueAp
|
||||
SG3irwVXf/XWMMpBdXvALPPvttPzlVOLYowYnervDPiINiQDkd5jRP+Kd0AStVEt
|
||||
/QNq3ocmYHj4AUhJ5YSkyyVnnmGrZzKpcJ1q0XxXFCMJnyBrkjkJ60SgDx+ucy7c
|
||||
vTVk+W8QyLfqFkbhv4OT7YBITNGHEwk8sZ6V3N98r2/8Hx5PI42QOKEARYtOTpip
|
||||
oj0LNnPFnAIkOTwZVazuc+vtG/GgSA==
|
||||
=IRUs
|
||||
-----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 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 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 public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
|
||||
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
|
||||
|
||||
# do the actual picking
|
||||
pin = await lll.get_new_pin()
|
||||
pin = await lll.get_new_pin(title)
|
||||
del lll
|
||||
|
||||
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
|
||||
# which are encrypted with zero-key.
|
||||
s = SettingsObject.prelogin()
|
||||
k = "nick"
|
||||
nick = s.get(k, '')
|
||||
nick = s.get('nick', '')
|
||||
|
||||
if not nick:
|
||||
ch = await ux_show_story("You can give this Coldcard a nickname"
|
||||
" and it will be shown before login.")
|
||||
ch = await ux_show_story('''\
|
||||
You can give this Coldcard a nickname and it will be shown before login.''')
|
||||
if ch != 'y': return
|
||||
|
||||
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
|
||||
dis.fullscreen("Saving...")
|
||||
dis.busy_bar(True)
|
||||
|
||||
if not nn:
|
||||
s.remove_key(k)
|
||||
else:
|
||||
s.set(k, nn.strip())
|
||||
|
||||
nn = nn.strip() if nn else None
|
||||
s.set('nick', nn)
|
||||
s.save()
|
||||
dis.busy_bar(False)
|
||||
del s
|
||||
@ -579,11 +573,8 @@ async def clear_seed(*a):
|
||||
# This is super dangerous for the customer's money.
|
||||
import seed
|
||||
|
||||
# in hobble mode, they cannot reach duress wallets and/or maybe we don't
|
||||
# want to reveal them? So don't block them based on that.
|
||||
if not pa.hobbled_mode:
|
||||
if await any_active_duress_ux():
|
||||
return await ux_aborted()
|
||||
if await any_active_duress_ux():
|
||||
return await ux_aborted()
|
||||
|
||||
if not await ux_confirm('Wipe seed words and reset wallet. '
|
||||
'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\
|
||||
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 \
|
||||
new wallet.''', 'AGAIN...', confirm_key='4'):
|
||||
new wallet.''', confirm_key='4'):
|
||||
return await ux_aborted()
|
||||
|
||||
# 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
|
||||
# them to login successfully.
|
||||
sp_unlock = False
|
||||
try:
|
||||
from trick_pins import tp
|
||||
|
||||
try:
|
||||
# Get a PIN and try to use it to login
|
||||
# - does warnings about attempt usage counts
|
||||
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)
|
||||
# - 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
|
||||
from trick_pins import tp
|
||||
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:
|
||||
delay = settings.get('lgto', 0)
|
||||
|
||||
if delay:
|
||||
# kill some time, with countdown, and get "the" PIN again for real login
|
||||
pa.reset()
|
||||
|
||||
await ux_login_countdown(delay * (60 if not version.is_devmode else 1))
|
||||
|
||||
# keep it simple for Mk4+: just challenge again for any PIN
|
||||
@ -867,32 +847,14 @@ async def start_login_sequence():
|
||||
# handle upgrades/downgrade issues
|
||||
try:
|
||||
await version_migration()
|
||||
except: pass
|
||||
except:
|
||||
pass
|
||||
|
||||
# Maybe insist on the "right" microSD being already installed?
|
||||
try:
|
||||
from pwsave import MicroSD2FA
|
||||
MicroSD2FA.enforce_policy()
|
||||
except: pass
|
||||
|
||||
# apply the hobbling for the spending policy, if appropriate
|
||||
try:
|
||||
from ccc import sssp_spending_policy, sssp_word_challenge
|
||||
|
||||
if sp_unlock and sssp_spending_policy('words'):
|
||||
# challenge them also for first and last seed word! (will reboot on fail)
|
||||
await sssp_word_challenge()
|
||||
dis.fullscreen("Startup...")
|
||||
|
||||
if sp_unlock:
|
||||
# Disable spending policy going forward; user has to re-enable.
|
||||
pa.hobbled_mode = False
|
||||
sssp_spending_policy('en', set_value=False)
|
||||
else:
|
||||
# normal entry mode, but might have policy enabled, if so enable it now.
|
||||
pa.hobbled_mode = sssp_spending_policy('en')
|
||||
|
||||
except: pass
|
||||
except: pass # robustness: keep going!
|
||||
|
||||
# implement idle timeout now that we are logged-in
|
||||
IMPT.start_task('idle', idle_logout())
|
||||
@ -938,14 +900,12 @@ async def start_login_sequence():
|
||||
settings.master_set("seedvault", False)
|
||||
except: pass
|
||||
|
||||
|
||||
from glob import hsm_active
|
||||
if version.has_nfc and settings.get('nfc', 0) and not hsm_active:
|
||||
if version.has_nfc and settings.get('nfc', 0):
|
||||
# Maybe allow NFC now
|
||||
import nfc
|
||||
nfc.NFCHandler.startup()
|
||||
|
||||
if settings.get('vidsk', 0) and not hsm_active:
|
||||
if settings.get('vidsk', 0):
|
||||
# Maybe start virtual disk
|
||||
import vdisk
|
||||
vdisk.VirtDisk()
|
||||
@ -983,7 +943,7 @@ async def restore_main_secret(*a):
|
||||
goto_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 pincodes import pa
|
||||
|
||||
@ -999,9 +959,7 @@ def make_top_menu():
|
||||
assert pa.is_successful(), "nonblank but wrong pin"
|
||||
|
||||
if pa.has_secrets():
|
||||
# let them do a few things, but not all the things, when "hobbled"
|
||||
_cls = HobbledTopMenu[:] if pa.hobbled_mode else NormalSystem[:]
|
||||
|
||||
_cls = NormalSystem[:]
|
||||
if pa.tmp_value or settings.get("hmx", False):
|
||||
active_xfp = settings.get("xfp", 0)
|
||||
sl, sr = ("[", "]") if pa.tmp_value else ("<", ">")
|
||||
@ -1103,10 +1061,8 @@ async def export_xpub(label, _2, item):
|
||||
if ch == "2":
|
||||
slip132 = not slip132
|
||||
continue
|
||||
|
||||
if ch == '1':
|
||||
acct = await ux_enter_bip32_index('Account Number:')
|
||||
if acct is None: continue
|
||||
acct = await ux_enter_bip32_index('Account Number:') or 0
|
||||
pth_split = path.split("/")
|
||||
pth_split[-1] = ("%dh" % acct)
|
||||
path = "/".join(pth_split)
|
||||
@ -1132,32 +1088,29 @@ async def export_xpub(label, _2, item):
|
||||
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
|
||||
return ('''\
|
||||
This saves a skeleton %s wallet file. \
|
||||
You can then open that file in the wallet without ever connecting this Coldcard to a computer.\n
|
||||
''' % noun
|
||||
This saves a skeleton Electrum wallet file. \
|
||||
You can then open that file in Electrum without ever connecting this Coldcard to a computer.\n
|
||||
'''
|
||||
+ (background or 'Choose an address type for the wallet on the next screen.'+PICK_ACCOUNT)
|
||||
+ 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
|
||||
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':
|
||||
acct = await ux_enter_bip32_index('Account Number:')
|
||||
|
||||
if (ch not in '1y') or acct is None:
|
||||
account_num = await ux_enter_bip32_index('Account Number:') or 0
|
||||
elif ch != 'y':
|
||||
return
|
||||
|
||||
rv = [
|
||||
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
|
||||
]
|
||||
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):
|
||||
# Export of descriptor data (wallet)
|
||||
addition, f_pattern = "", "descriptor.txt"
|
||||
int_ext = direct_way = None
|
||||
int_ext, addition, f_pattern = None, "", "descriptor.txt"
|
||||
allowed_af = chains.SINGLESIG_AF
|
||||
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
|
||||
|
||||
acct = 0
|
||||
if not direct_way:
|
||||
ch = await ux_show_story(ss_descriptor_export_story(addition), escape='1')
|
||||
ch = await ux_show_story(ss_descriptor_export_story(addition), escape='1')
|
||||
|
||||
if ch == '1':
|
||||
acct = await ux_enter_bip32_index('Account Number:', unlimited=True)
|
||||
|
||||
if (ch not in '1y') or acct is None:
|
||||
return
|
||||
account_num = 0
|
||||
if ch == '1':
|
||||
account_num = await ux_enter_bip32_index('Account Number:', unlimited=True) or 0
|
||||
elif ch != 'y':
|
||||
return
|
||||
|
||||
if int_ext is None:
|
||||
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
|
||||
|
||||
if len(allowed_af) == 1:
|
||||
await make_descriptor_wallet_export(allowed_af[0], acct, int_ext=int_ext,
|
||||
fname_pattern=f_pattern, direct_way=direct_way)
|
||||
await make_descriptor_wallet_export(allowed_af[0], account_num,
|
||||
int_ext=int_ext,
|
||||
fname_pattern=f_pattern)
|
||||
else:
|
||||
rv = [
|
||||
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
|
||||
]
|
||||
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):
|
||||
name = "POST-MIX"
|
||||
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):
|
||||
# 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,
|
||||
fname_pattern=f_pattern, direct_way=dw)
|
||||
fname_pattern=f_pattern)
|
||||
|
||||
|
||||
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.\
|
||||
''' + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape='1')
|
||||
|
||||
acct = 0
|
||||
account_num = 0
|
||||
if ch == '1':
|
||||
acct = await ux_enter_bip32_index('Account Number:')
|
||||
|
||||
if (ch not in '1y') or acct is None:
|
||||
account_num = await ux_enter_bip32_index('Account Number:') or 0
|
||||
elif ch != 'y':
|
||||
return
|
||||
|
||||
# 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):
|
||||
# pick a semi-random file name, render and save it.
|
||||
addr_fmt, account_num, title, fname_pat = item.arg
|
||||
await export_contents(title + " wallet",
|
||||
addr_fmt, account_num = item.arg
|
||||
await export_contents('Electrum wallet',
|
||||
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):
|
||||
# like the Multisig export, make a single JSON file with
|
||||
# basically all useful XPUB's in it.
|
||||
ch = await ux_show_story(prompt + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape="1")
|
||||
acct = 0
|
||||
account_num = 0
|
||||
if ch == '1':
|
||||
acct = await ux_enter_bip32_index('Account Number:')
|
||||
|
||||
if (ch not in '1y') or acct is None:
|
||||
account_num = await ux_enter_bip32_index('Account Number:') or 0
|
||||
elif ch != 'y':
|
||||
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)
|
||||
|
||||
async def generic_skeleton(*A):
|
||||
@ -1366,17 +1274,16 @@ async def unchained_capital_export(*a):
|
||||
ch = await ux_show_story('''\
|
||||
This saves multisig XPUB information required to setup on the Unchained platform. \
|
||||
''' + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape="1")
|
||||
acct = 0
|
||||
account_num = 0
|
||||
if ch == '1':
|
||||
acct = await ux_enter_bip32_index('Account Number:')
|
||||
|
||||
if (ch not in '1y') or acct is None:
|
||||
account_num = await ux_enter_bip32_index('Account Number:') or 0
|
||||
elif ch != 'y':
|
||||
return
|
||||
|
||||
xfp = xfp2str(settings.get('xfp', 0))
|
||||
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)
|
||||
|
||||
|
||||
@ -1455,7 +1362,7 @@ async def import_xprv(_1, _2, item):
|
||||
else:
|
||||
# only get here if NFC was not chosen
|
||||
# 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)
|
||||
|
||||
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 \
|
||||
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. \
|
||||
Does not affect MicroSD card, if any.''', confirm_key="4"):
|
||||
Does not affect MicroSD card, if any.'''):
|
||||
return
|
||||
|
||||
from files import 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):
|
||||
if not await ux_confirm('''\
|
||||
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
|
||||
data = data.decode()
|
||||
else:
|
||||
assert data[1:4] == bytes(3)
|
||||
assert data[2:8] == bytes(6)
|
||||
data = b2a_hex(data).decode()
|
||||
elif data[0:5] == b'psbt\xff':
|
||||
tc = "P"
|
||||
@ -1755,40 +1648,30 @@ async def list_files(*A):
|
||||
from pincodes import pa
|
||||
|
||||
digest = chk.digest()
|
||||
path, basename = fn.rsplit('/', 1)
|
||||
msg_base = 'SHA256(%s)\n\n' + B2A(digest) + '\n\nPress (1) to rename file, '
|
||||
escape = "61"
|
||||
basename = fn.rsplit('/', 1)[-1]
|
||||
msg_base = 'SHA256(%s)\n\n%s\n\nPress ' % (basename, B2A(digest))
|
||||
escape = "6"
|
||||
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"
|
||||
msg_base += '(6) to delete.'
|
||||
|
||||
else:
|
||||
msg_sign = ""
|
||||
msg_delete = '(6) to delete.'
|
||||
msg = msg_base + msg_sign + msg_delete
|
||||
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 in '461':
|
||||
if ch in '46':
|
||||
with CardSlot() as card:
|
||||
if ch == '6':
|
||||
card.securely_blank_file(fn)
|
||||
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:
|
||||
from msgsign import write_sig_file
|
||||
|
||||
sig_nice = write_sig_file([(digest, fn)])
|
||||
await ux_show_story("Signature file %s written." % sig_nice)
|
||||
msg = msg_base + msg_delete
|
||||
return
|
||||
|
||||
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
|
||||
# - 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)
|
||||
# - 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:
|
||||
choices = []
|
||||
@ -1821,13 +1697,13 @@ async def file_picker(suffix=None, min_size=1, max_size=1000000, taster=None,
|
||||
# ignore subdirs
|
||||
continue
|
||||
|
||||
if fn[0] == '.':
|
||||
# unix-style hidden files
|
||||
continue
|
||||
if suffix:
|
||||
if not isinstance(suffix, list):
|
||||
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):
|
||||
# wrong suffix, skip
|
||||
continue
|
||||
if fn[0] == '.': continue
|
||||
|
||||
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:
|
||||
msg += none_msg
|
||||
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?'
|
||||
|
||||
@ -1953,7 +1829,7 @@ async def _batch_sign(choices=None):
|
||||
return
|
||||
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)
|
||||
|
||||
if not choices:
|
||||
@ -1991,7 +1867,7 @@ async def ready2sign(*a):
|
||||
opt = {}
|
||||
|
||||
# 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)
|
||||
|
||||
if pa.tmp_value:
|
||||
@ -2018,7 +1894,7 @@ from your desktop wallet software or command line tools.'''
|
||||
title=title)
|
||||
if isinstance(picked, dict):
|
||||
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,
|
||||
**opt)
|
||||
if not choices:
|
||||
@ -2060,7 +1936,7 @@ async def sign_message_on_sd(*a):
|
||||
# min 1 line max 3 lines
|
||||
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 '
|
||||
'followed by a subkey derivation path on a second line '
|
||||
'and/or address format on third line. JSON msg signing '
|
||||
@ -2136,7 +2012,7 @@ Write it down.'''
|
||||
while 1:
|
||||
lll.reset()
|
||||
lll.subtitle = "New " + title
|
||||
pin = await lll.get_new_pin()
|
||||
pin = await lll.get_new_pin(title)
|
||||
|
||||
if pin is None:
|
||||
return await ux_aborted()
|
||||
@ -2218,6 +2094,7 @@ Coldcard Firmware
|
||||
{rel}
|
||||
{built}
|
||||
|
||||
|
||||
Bootloader:
|
||||
{bl}
|
||||
{chk}
|
||||
@ -2313,7 +2190,7 @@ async def wipe_address_cache(*a):
|
||||
async def wipe_ovc(*a):
|
||||
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 \
|
||||
has occurred in the detection logic.''')
|
||||
has occured in the detection logic.''')
|
||||
if not ok: return
|
||||
|
||||
import history
|
||||
@ -2382,8 +2259,6 @@ async def change_seed_vault(is_enabled):
|
||||
|
||||
async def change_which_chain(*a):
|
||||
# setting already changed, but reflect that value in other settings
|
||||
from glob import dis
|
||||
dis.fullscreen("Wait...")
|
||||
try:
|
||||
# update xpub stored in settings
|
||||
import stash
|
||||
@ -2415,23 +2290,9 @@ async def microsd_2fa(*a):
|
||||
|
||||
async def keyboard_test(*a):
|
||||
# to aid keyboard testing/dev
|
||||
if version.has_qwerty:
|
||||
await ux_input_text('', max_len=128, scan_ok=True, confirm_exit=False,
|
||||
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()
|
||||
from ux import ux_input_text
|
||||
await ux_input_text('', max_len=128, scan_ok=True, confirm_exit=False,
|
||||
prompt='Keyboard Test', placeholder='(type whatever)')
|
||||
|
||||
#
|
||||
# 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):
|
||||
from ux_q1 import QRScannerInteraction
|
||||
x = QRScannerInteraction()
|
||||
try:
|
||||
await x.scan_anything(expect_secret=expect_secret, tmp=tmp)
|
||||
except Exception as e:
|
||||
await ux_show_story(msg="Failed to import from QR.\n\n%s\n%s" % (e, problem_file_line(e)),
|
||||
title="ERROR")
|
||||
await x.scan_anything(expect_secret=expect_secret, tmp=tmp)
|
||||
|
||||
|
||||
PUSHTX_SUPPLIERS = [
|
||||
@ -2488,7 +2345,7 @@ async def pushtx_setup_menu(*a):
|
||||
"transaction will be immediately broadcast on the public network.\n\n"
|
||||
"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. "
|
||||
"Make sure your phone is not in airplane mode. Requires NFC.",
|
||||
"Requires NFC.",
|
||||
title="PUSH TX",
|
||||
)
|
||||
if ch != "y":
|
||||
|
||||
@ -30,10 +30,8 @@ def censor_address(addr):
|
||||
return addr[0:12] + '___' + addr[12+3:]
|
||||
|
||||
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.done_fn = done_fn
|
||||
self.ranged = ranged
|
||||
|
||||
if path is None:
|
||||
# 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/49h/⋯", f=self.deeper),
|
||||
MenuItem("m/84h/⋯", f=self.deeper),
|
||||
MenuItem("m/0/{idx}", menu=self.done),
|
||||
MenuItem("m/{idx}", menu=self.done),
|
||||
MenuItem("m", f=self.done),
|
||||
]
|
||||
if self.ranged:
|
||||
items += [
|
||||
MenuItem("m/0/{idx}", menu=self.done),
|
||||
MenuItem("m/{idx}", menu=self.done),
|
||||
]
|
||||
else:
|
||||
# drill down one layer: (nl) is the current leaf
|
||||
# - hardened choice first
|
||||
@ -58,14 +53,11 @@ class KeypathMenu(MenuSystem):
|
||||
MenuItem(p+"/⋯", menu=self.deeper),
|
||||
MenuItem(p+"h", menu=self.done),
|
||||
MenuItem(p, menu=self.done),
|
||||
MenuItem(p+"h/0/{idx}", menu=self.done),
|
||||
MenuItem(p+"/0/{idx}", menu=self.done), #useful shortcut?
|
||||
MenuItem(p+"h/{idx}", menu=self.done),
|
||||
MenuItem(p+"/{idx}", menu=self.done),
|
||||
]
|
||||
if self.ranged:
|
||||
items += [
|
||||
MenuItem(p + "h/0/{idx}", menu=self.done),
|
||||
MenuItem(p + "/0/{idx}", menu=self.done), # useful shortcut?
|
||||
MenuItem(p + "h/{idx}", menu=self.done),
|
||||
MenuItem(p + "/{idx}", menu=self.done),
|
||||
]
|
||||
|
||||
# simple consistent truncation when needed
|
||||
max_wide = max(len(mi.label) for mi in items)
|
||||
@ -103,20 +95,17 @@ class KeypathMenu(MenuSystem):
|
||||
if isinstance(top, KeypathMenu):
|
||||
the_ux.pop()
|
||||
continue
|
||||
# assert isinstance(top, AddressListMenu), type(top)
|
||||
assert isinstance(top, AddressListMenu)
|
||||
break
|
||||
|
||||
if self.done_fn:
|
||||
return await self.done_fn(final_path)
|
||||
|
||||
return PickAddrFmtMenu(final_path, top)
|
||||
|
||||
async def deeper(self, _1, _2, item):
|
||||
val = item.arg or item.label
|
||||
assert val.endswith('/⋯')
|
||||
cpath = val[:-2]
|
||||
nl = await ux_enter_bip32_index('%s/' % cpath, unlimited=True, can_cancel=False)
|
||||
return KeypathMenu(cpath, nl, ranged=self.ranged, done_fn=self.done_fn)
|
||||
nl = await ux_enter_bip32_index('%s/' % cpath, unlimited=True)
|
||||
return KeypathMenu(cpath, nl)
|
||||
|
||||
class PickAddrFmtMenu(MenuSystem):
|
||||
def __init__(self, path, parent):
|
||||
@ -126,10 +115,9 @@ class PickAddrFmtMenu(MenuSystem):
|
||||
for af in chains.SINGLESIG_AF
|
||||
]
|
||||
super().__init__(items)
|
||||
# below is sensitive to order in chains.SINGLESIG_AF
|
||||
if path.startswith("m/44h"):
|
||||
if path.startswith("m/84h"):
|
||||
self.goto_idx(1)
|
||||
elif path.startswith("m/49h"):
|
||||
if path.startswith("m/49h"):
|
||||
self.goto_idx(2)
|
||||
|
||||
async def done(self, _1, _2, item):
|
||||
@ -242,15 +230,11 @@ class AddressListMenu(MenuSystem):
|
||||
self.goto_idx(axi)
|
||||
|
||||
async def change_account(self, *a):
|
||||
acct = await ux_enter_bip32_index('Account Number:')
|
||||
if acct is None: return
|
||||
self.account_num = acct
|
||||
self.account_num = await ux_enter_bip32_index('Account Number:') or 0
|
||||
await self.render()
|
||||
|
||||
async def change_start_idx(self, *a):
|
||||
idx = await ux_enter_bip32_index("Start index:", unlimited=True)
|
||||
if idx is None: return
|
||||
self.start = idx
|
||||
self.start = await ux_enter_bip32_index("Start index:", unlimited=True)
|
||||
await self.render()
|
||||
|
||||
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)]
|
||||
) + '"\n'
|
||||
|
||||
# saver will be None if we don't think it worth saving these addresses
|
||||
saver = OWNERSHIP.saver(ms_wallet, change, start, n)
|
||||
if (start == 0) and (n > 100) and change in (0, 1):
|
||||
saver = OWNERSHIP.saver(ms_wallet, change, start)
|
||||
else:
|
||||
saver = None
|
||||
|
||||
for (idx, addr, derivs, script) in ms_wallet.yield_addresses(start, n, change_idx=change):
|
||||
if saver:
|
||||
saver(addr, idx)
|
||||
saver(addr)
|
||||
|
||||
# policy choice: never provide a complete multisig address to user.
|
||||
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
|
||||
|
||||
if saver:
|
||||
saver(None, 0) # close cache file
|
||||
saver(None) # close file
|
||||
|
||||
return
|
||||
|
||||
@ -470,18 +456,20 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
|
||||
from wallet import MasterSingleSigWallet
|
||||
main = MasterSingleSigWallet(addr_fmt, path, account_num)
|
||||
|
||||
# saver will be None if we don't think it worth saving these addresses
|
||||
saver = OWNERSHIP.saver(main, change, start, n)
|
||||
if n and (start == 0) and (n > 100) and change in (0, 1):
|
||||
saver = OWNERSHIP.saver(main, change, start)
|
||||
else:
|
||||
saver = None
|
||||
|
||||
yield '"Index","Payment Address","Derivation"\n'
|
||||
for (idx, addr, deriv) in main.yield_addresses(start, n, change_idx=change):
|
||||
if saver:
|
||||
saver(addr, idx)
|
||||
saver(addr)
|
||||
|
||||
yield '%d,"%s","%s"\n' % (idx, addr, deriv)
|
||||
|
||||
if saver:
|
||||
saver(None, 0) # close cache file
|
||||
saver(None) # close
|
||||
|
||||
async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num,
|
||||
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:
|
||||
await needs_microsd()
|
||||
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):
|
||||
|
||||
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 uhashlib import sha256
|
||||
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 menu import MenuSystem, MenuItem
|
||||
from serializations import ser_uint256, SIGHASH_ALL
|
||||
from ux import ux_show_story, abort_and_goto, ux_dramatic_pause, ux_clear_keys, ux_confirm, the_ux
|
||||
from ux import show_qr_code, OK, X, abort_and_push, AbortInteraction, ux_input_text, ux_enter_number
|
||||
from ux import ux_show_story, abort_and_goto, ux_dramatic_pause, ux_clear_keys
|
||||
from ux import show_qr_code, OK, X, abort_and_push, AbortInteraction
|
||||
from usb import CCBusyError
|
||||
from utils import (HexWriter, xfp2str, problem_file_line, cleanup_deriv_path, B2A, node_from_privkey,
|
||||
show_single_address, keypath_to_str, seconds2human_readable)
|
||||
from utils import HexWriter, xfp2str, problem_file_line, cleanup_deriv_path, B2A, show_single_address
|
||||
from psbt import psbtObject, FatalPSBTIssue, FraudulentChangeOutput
|
||||
from files import CardSlot, CardMissingError
|
||||
from exceptions import HSMDenied, QRTooBigError
|
||||
from exceptions import HSMDenied
|
||||
from version import MAX_TXN_LEN
|
||||
from charcodes import KEY_QR, KEY_NFC, KEY_ENTER, KEY_CANCEL, KEY_LEFT, KEY_RIGHT
|
||||
from msgsign import sign_message_digest
|
||||
@ -131,7 +128,7 @@ Press %s to continue, otherwise %s to cancel.''' % (OK, X)
|
||||
|
||||
class ApproveMessageSign(UserAuthorizedAction):
|
||||
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__()
|
||||
is_json = False
|
||||
|
||||
@ -141,23 +138,18 @@ class ApproveMessageSign(UserAuthorizedAction):
|
||||
text, subpath, addr_fmt, is_json = parse_msg_sign_request(msg_sign_request)
|
||||
|
||||
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.addr_fmt = chains.parse_addr_fmt_str(addr_fmt)
|
||||
self.approved_cb = approved_cb
|
||||
self.privkey = privkey
|
||||
|
||||
from glob import dis
|
||||
dis.fullscreen('Wait...')
|
||||
|
||||
if self.privkey:
|
||||
node = node_from_privkey(self.privkey)
|
||||
self.address = chains.current_chain().address(node, self.addr_fmt)
|
||||
else:
|
||||
with stash.SensitiveValues() as sv:
|
||||
node = sv.derive_path(self.subpath)
|
||||
self.address = sv.chain.address(node, self.addr_fmt)
|
||||
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)
|
||||
|
||||
@ -178,8 +170,7 @@ class ApproveMessageSign(UserAuthorizedAction):
|
||||
else:
|
||||
# perform signing (progress bar shown)
|
||||
digest = chains.current_chain().hash_message(self.text.encode())
|
||||
self.result, _ = sign_message_digest(digest, self.subpath, "Signing...",
|
||||
self.addr_fmt, pk=self.privkey)
|
||||
self.result, _ = sign_message_digest(digest, self.subpath, "Signing...", self.addr_fmt)
|
||||
|
||||
if self.approved_cb:
|
||||
# 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,
|
||||
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.
|
||||
UserAuthorizedAction.cleanup()
|
||||
@ -213,8 +204,7 @@ async def approve_msg_sign(text, subpath, addr_fmt, approved_cb=None,
|
||||
text, subpath, addr_fmt,
|
||||
approved_cb=approved_cb,
|
||||
msg_sign_request=msg_sign_request,
|
||||
allow_tab_nl=allow_tab_nl,
|
||||
privkey=privkey
|
||||
only_printable=only_printable,
|
||||
)
|
||||
|
||||
if kill_menu:
|
||||
@ -235,6 +225,8 @@ async def sign_txt_file(filename):
|
||||
|
||||
async def done(signature, address, text):
|
||||
# complete. write out result
|
||||
from glob import dis
|
||||
|
||||
orig_path, basename = filename.rsplit('/', 1)
|
||||
orig_path += '/'
|
||||
base = basename.rsplit('.', 1)[0]
|
||||
@ -271,9 +263,8 @@ async def try_push_tx(data, txid, txn_sha=None):
|
||||
|
||||
class ApproveTransaction(UserAuthorizedAction):
|
||||
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__()
|
||||
self.offset = offset
|
||||
self.psbt_len = psbt_len
|
||||
|
||||
# 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.
|
||||
# - expects CTxOut object
|
||||
# - gives user-visible string
|
||||
# returns: tuple(ux_output_rendition, address_or_script_str_for_qr_display)
|
||||
#
|
||||
val = ' '.join(self.chain.render_value(o.nValue))
|
||||
try:
|
||||
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
|
||||
except ValueError:
|
||||
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
|
||||
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 None:
|
||||
rv = '%s\n - to script -\n%s\n' % (val, dest)
|
||||
else:
|
||||
if data is not None:
|
||||
base = '%s\n - OP_RETURN -\n%s'
|
||||
if not data:
|
||||
dest = ""
|
||||
rv = base % (val, "null-data\n")
|
||||
return base % (val, "null-data\n"), ""
|
||||
else:
|
||||
data_ascii = None
|
||||
if len(data) > 160:
|
||||
if len(data) > 200:
|
||||
# completely arbitrary limit, prevents huge stories
|
||||
# anchor data are not relevant for verification - can be hidden
|
||||
ss = b2a_hex(data[:80]).decode() + "\n ⋯\n" + b2a_hex(data[-80:]).decode()
|
||||
# but we show empty QR in txn explorer for these big, modified data
|
||||
data_hex = b2a_hex(data[:100]).decode() + "\n ⋯\n" + b2a_hex(data[-100:]).decode()
|
||||
else:
|
||||
ss = b2a_hex(data).decode()
|
||||
data_hex = b2a_hex(data).decode()
|
||||
if (min(data) >= 32) and (max(data) < 127): # printable & not huge
|
||||
try:
|
||||
data_ascii = data.decode("ascii")
|
||||
except: pass
|
||||
|
||||
rv = base % (val, ss)
|
||||
to_ret = base % (val, data_hex)
|
||||
if data_ascii:
|
||||
rv += " (ascii: %s)" % data_ascii
|
||||
rv += "\n"
|
||||
to_ret += " (ascii: %s)" % data_ascii
|
||||
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):
|
||||
# Prompt user w/ details and get approval
|
||||
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.
|
||||
|
||||
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
|
||||
self.psbt = psbtObject.read_psbt(fd)
|
||||
except BaseException as exc:
|
||||
@ -370,14 +351,13 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
await self.psbt.validate() # might do UX: accept multisig import
|
||||
dis.progress_sofar(10, 100)
|
||||
|
||||
if not self.psbt.wif_store:
|
||||
self.psbt.consider_keys()
|
||||
# consider_keys only needs num_our_keys to be set
|
||||
# it set during psbt.validate()
|
||||
self.psbt.consider_keys()
|
||||
dis.progress_sofar(20, 100)
|
||||
|
||||
ccc_c_xfp = CCCFeature.get_xfp() # can be None
|
||||
self.psbt.consider_inputs(cosign_xfp=ccc_c_xfp)
|
||||
if self.psbt.wif_store:
|
||||
self.psbt.consider_keys()
|
||||
dis.progress_sofar(50, 100)
|
||||
|
||||
self.psbt.consider_outputs()
|
||||
@ -407,13 +387,7 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
|
||||
# early test for spending policy; not an error if violates policy
|
||||
# - might add warnings
|
||||
could_ccc_sign, ccc_needs_2fa = CCCFeature.could_cosign(self.psbt)
|
||||
|
||||
# test for allowing any signature when in single-signer mode
|
||||
# - but CCC will override it.
|
||||
should_block, ss_needs_2fa = SSSPFeature.can_allow(self.psbt)
|
||||
if should_block and not could_ccc_sign:
|
||||
return await self.failure('Spending Policy violation.')
|
||||
could_ccc_sign, needs_2fa = CCCFeature.could_sign(self.psbt)
|
||||
|
||||
# step 2: figure out what we are approving, so we can get sign-off
|
||||
# - outputs, amounts
|
||||
@ -428,7 +402,6 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
#
|
||||
try:
|
||||
msg = uio.StringIO()
|
||||
is_por = self.psbt.por322 and (self.psbt.num_inputs > 1)
|
||||
|
||||
# mention warning at top
|
||||
wl= len(self.psbt.warnings)
|
||||
@ -437,41 +410,27 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
elif wl >= 2:
|
||||
msg.write('(%d warnings below)\n\n' % wl)
|
||||
|
||||
if self.psbt.por322:
|
||||
msg.write("%s\n\n" % ("Proof of Reserves" if is_por else "BIP-322 Message"))
|
||||
msg.write("Message:\n%s\n\n" % self.psbt.por322_msg)
|
||||
if is_por:
|
||||
msg.write("Amount %s %s\n\n" % self.chain.render_value(self.psbt.total_value_in))
|
||||
try:
|
||||
addr = self.chain.render_address(self.psbt.por322_msg_challenge)
|
||||
msg.write("Challenge Address:\n%s\n\n" % show_single_address(addr))
|
||||
except ValueError:
|
||||
msg.write("Message Challenge:\n%s\n\n" % b2a_hex(self.psbt.por322_msg_challenge).decode())
|
||||
if self.psbt.consolidation_tx:
|
||||
# consolidating txn that doesn't change balance of account.
|
||||
msg.write("Consolidating %s %s\nwithin wallet.\n\n" %
|
||||
self.chain.render_value(self.psbt.total_value_out))
|
||||
else:
|
||||
if self.psbt.consolidation_tx:
|
||||
# consolidating txn that doesn't change balance of account.
|
||||
msg.write("Consolidating %s %s\nwithin wallet.\n\n" %
|
||||
self.chain.render_value(self.psbt.total_value_out))
|
||||
else:
|
||||
msg.write("Sending %s %s\n" % self.chain.render_value(
|
||||
self.psbt.total_value_out - self.psbt.total_change_value))
|
||||
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()
|
||||
if fee is not None:
|
||||
msg.write("Network fee %s %s\n\n" % self.chain.render_value(fee))
|
||||
fee = self.psbt.calculate_fee()
|
||||
if fee is not None:
|
||||
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" % (
|
||||
self.psbt.num_inputs,
|
||||
"input" if self.psbt.num_inputs == 1 else "inputs",
|
||||
self.psbt.num_outputs,
|
||||
"output" if self.psbt.num_outputs == 1 else "outputs",
|
||||
))
|
||||
|
||||
if not self.psbt.por322:
|
||||
# outputs + change story created here
|
||||
self.output_summary_text(msg)
|
||||
msg.write(" %d %s\n %d %s\n\n" % (
|
||||
self.psbt.num_inputs,
|
||||
"input" if self.psbt.num_inputs == 1 else "inputs",
|
||||
self.psbt.num_outputs,
|
||||
"output" if self.psbt.num_outputs == 1 else "outputs",
|
||||
))
|
||||
|
||||
# outputs + change story created here
|
||||
self.output_summary_text(msg)
|
||||
gc.collect()
|
||||
|
||||
if self.psbt.ux_notes:
|
||||
@ -499,23 +458,17 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
|
||||
if not hsm_active:
|
||||
esc = "2"
|
||||
noun = "transaction"
|
||||
if self.psbt.por322:
|
||||
noun = "proof of reserves" if is_por else "message"
|
||||
|
||||
msg.write("Press %s to approve and sign %s."
|
||||
" Press (2) to explore transaction." % (OK, noun))
|
||||
|
||||
msg.write("Press %s to approve and sign transaction."
|
||||
" Press (2) to explore txn outputs." % OK)
|
||||
if (self.input_method == "sd") and CardSlot.both_inserted():
|
||||
esc += "b"
|
||||
msg.write(" (B) to write to lower SD slot.")
|
||||
msg.write(" %s to abort." % X)
|
||||
|
||||
title = "OK TO %s?" % ("SIGN" if self.psbt.por322 else "SEND")
|
||||
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":
|
||||
await TXExplorer.start(self)
|
||||
await self.txn_explorer()
|
||||
continue
|
||||
else:
|
||||
msg.close()
|
||||
@ -547,7 +500,7 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
self.done()
|
||||
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)
|
||||
try:
|
||||
await CCCFeature.web2fa_challenge()
|
||||
@ -557,13 +510,6 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
if ch2 != 'y':
|
||||
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.
|
||||
try:
|
||||
dis.fullscreen('Wait...')
|
||||
@ -571,15 +517,10 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
self.psbt.sign_it()
|
||||
|
||||
if could_ccc_sign:
|
||||
# this is where the CCC co-signing happens.
|
||||
dis.fullscreen('Co-Signing...')
|
||||
dis.fullscreen('CCC Sign...')
|
||||
gc.collect()
|
||||
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:
|
||||
return await self.failure(exc.args[0], title='Change Fraud')
|
||||
except MemoryError:
|
||||
@ -589,9 +530,8 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
return await self.failure("Signing failed late", exc)
|
||||
|
||||
try:
|
||||
await done_signing(self.psbt, self, self.input_method,
|
||||
self.filename, self.output_encoder,
|
||||
slot_b=(ch == "b"), finalize=self.do_finalize)
|
||||
await done_signing(self.psbt, self, self.input_method, self.filename, self.output_encoder,
|
||||
slot_b=True if ch == "b" else False, finalize=self.do_finalize)
|
||||
self.done()
|
||||
except AbortInteraction:
|
||||
# user might have sent new sign cmd, while we still at export prompt
|
||||
@ -600,6 +540,73 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
# sys.print_exception(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):
|
||||
# write story text out, maybe signing it as we go
|
||||
# - return length and checksum
|
||||
@ -651,8 +658,7 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
has_change = True
|
||||
total_change += tx_out.nValue
|
||||
if len(largest_change) < MAX_VISIBLE_CHANGE:
|
||||
_, addr = self.render_output(tx_out)
|
||||
largest_change.append((tx_out.nValue, addr))
|
||||
largest_change.append((tx_out.nValue, self.chain.render_address(tx_out.scriptPubKey)))
|
||||
if len(largest_change) == MAX_VISIBLE_CHANGE:
|
||||
largest_change = sorted(largest_change, key=lambda x: x[0], reverse=True)
|
||||
continue
|
||||
@ -677,9 +683,12 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
continue # too small
|
||||
|
||||
largest.pop(-1)
|
||||
|
||||
rendered, dest = self.render_output(tx_out)
|
||||
largest.insert(keep, (here, dest if outp.is_change else rendered))
|
||||
if outp.is_change:
|
||||
ret = (here, self.chain.render_address(tx_out.scriptPubKey))
|
||||
else:
|
||||
rendered, _ = self.render_output(tx_out)
|
||||
ret = (here, rendered)
|
||||
largest.insert(keep, ret)
|
||||
|
||||
# foreign outputs (soon to be other people's coins)
|
||||
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))
|
||||
|
||||
|
||||
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
|
||||
UserAuthorizedAction.check_busy(ApproveTransaction)
|
||||
UserAuthorizedAction.active_request = ApproveTransaction(
|
||||
psbt_len, flags, psbt_sha=psbt_sha, input_method=input_method,
|
||||
offset=offset
|
||||
psbt_len, flags, psbt_sha=psbt_sha, input_method="usb",
|
||||
)
|
||||
|
||||
# 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
|
||||
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:
|
||||
if is_complete:
|
||||
txid = psbt.finalize(psram)
|
||||
noun = "Finalized TX ready for broadcast"
|
||||
else:
|
||||
psbt.serialize(psram)
|
||||
noun = "Signed BIP-322 PSBT" if psbt.por322 else "Partly Signed PSBT"
|
||||
noun = "Partly Signed PSBT"
|
||||
txid = None
|
||||
|
||||
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."
|
||||
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):
|
||||
# go directly to reexport menu after pushTX
|
||||
first_time = False
|
||||
@ -822,6 +820,8 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
|
||||
ch = KEY_QR
|
||||
elif input_method == "nfc":
|
||||
ch = KEY_NFC
|
||||
elif input_method == "kt":
|
||||
ch = 't'
|
||||
else:
|
||||
# SD/VDisk
|
||||
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:
|
||||
# show all possible export options (based on hardware enabled, features)
|
||||
intro = []
|
||||
key6 = None
|
||||
if msg:
|
||||
intro.append(msg)
|
||||
if txid:
|
||||
key6 = "for QR Code of TXID"
|
||||
intro.append('TXID:\n' + txid)
|
||||
|
||||
# "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
|
||||
# files on SD infinitely (would never actually prompt).
|
||||
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)
|
||||
if ch == KEY_CANCEL:
|
||||
UserAuthorizedAction.cleanup()
|
||||
@ -853,14 +851,14 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
|
||||
|
||||
elif ch == KEY_QR:
|
||||
here = PSRAM.read_at(TXN_OUTPUT_OFFSET, data_len)
|
||||
msg = txid or noun
|
||||
msg = txid or 'Partly Signed PSBT'
|
||||
try:
|
||||
if len(here) > 920:
|
||||
# too big for simple QR - use BBQr instead
|
||||
raise QRTooBigError
|
||||
raise ValueError
|
||||
hex_here = b2a_hex(here).upper().decode()
|
||||
await show_qr_code(hex_here, is_alnum=True, msg=msg)
|
||||
except QRTooBigError:
|
||||
except (ValueError, RuntimeError, TypeError):
|
||||
from ux_q1 import show_bbqr_codes
|
||||
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:
|
||||
# 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
|
||||
# updated PSBT is at TXN_OUTPUT_OFFSET (at TXN_INPUT_OFFSET is PSBT that is NOT updated)
|
||||
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:
|
||||
title = "Failed to Teleport"
|
||||
else:
|
||||
@ -1121,51 +1118,6 @@ class RemoteBackup(UserAuthorizedAction):
|
||||
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():
|
||||
# tell the local user the secret words, and then save to SPI flash
|
||||
# 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
|
||||
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):
|
||||
def __init__(self, pw):
|
||||
@ -1550,230 +1496,4 @@ def authorize_upgrade(hdr, length, **kws):
|
||||
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
|
||||
|
||||
@ -5,11 +5,10 @@
|
||||
import compat7z, stash, ckcc, chains, gc, sys, bip39, uos, ngu
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from ubinascii import unhexlify as a2b_hex
|
||||
from utils import deserialize_secret, swab32, xfp2str
|
||||
from sffile import SFFile
|
||||
from utils import deserialize_secret
|
||||
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, OK, X, ux_input_text
|
||||
import version, ujson
|
||||
from uio import StringIO, BytesIO
|
||||
from uio import StringIO
|
||||
import seed
|
||||
from glob import settings
|
||||
from pincodes import pa
|
||||
@ -49,7 +48,7 @@ def render_backup_contents(bypass_tmp=False):
|
||||
if sv.mode == 'words':
|
||||
ADD('mnemonic', bip39.b2a_words(sv.raw))
|
||||
|
||||
elif sv.mode == 'master':
|
||||
if sv.mode == 'master':
|
||||
ADD('bip32_master_key', b2a_hex(sv.raw))
|
||||
|
||||
ADD('chain', chain.ctype)
|
||||
@ -76,12 +75,7 @@ def render_backup_contents(bypass_tmp=False):
|
||||
current_tmp = pa.tmp_value[:]
|
||||
pa.tmp_value = None
|
||||
# we also need correct settings from main seed
|
||||
if sv.mode == 'words':
|
||||
nv = stash.SecretStash.encode(seed_phrase=sv.raw)
|
||||
else:
|
||||
assert sv.mode == "xprv"
|
||||
nv = stash.SecretStash.encode(xprv=sv.node)
|
||||
|
||||
nv = stash.SecretStash.encode(seed_phrase=sv.raw)
|
||||
settings.set_key(nv)
|
||||
settings.load()
|
||||
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 == 'ccc': continue # not supported, security issue
|
||||
if k == 'ktrx': continue # not useful after the fact
|
||||
if k == 'lfr': continue # temporary error msg value
|
||||
if k == 'seedvault' and not v: continue
|
||||
if k == 'seeds' and not v: continue
|
||||
ADD('setting.' + k, v)
|
||||
@ -128,7 +121,7 @@ def render_backup_contents(bypass_tmp=False):
|
||||
|
||||
return rv.getvalue()
|
||||
|
||||
def extract_raw_secret(vals):
|
||||
def extract_raw_secret(chain, vals):
|
||||
# step1: the private key
|
||||
# - prefer raw_secret over 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)
|
||||
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'
|
||||
|
||||
return raw, node
|
||||
return raw
|
||||
|
||||
def extract_long_secret(vals):
|
||||
ls = None
|
||||
@ -159,7 +152,7 @@ def extract_long_secret(vals):
|
||||
pass
|
||||
return ls
|
||||
|
||||
def restore_from_dict_ll(vals, raw):
|
||||
def restore_from_dict_ll(vals):
|
||||
# Restore from a dict of values. Already JSON decoded.
|
||||
# Need a Reboot on success, return string on failure
|
||||
# - low-level version, factored out for better testing
|
||||
@ -170,6 +163,12 @@ def restore_from_dict_ll(vals, raw):
|
||||
#print("Restoring from: %r" % vals)
|
||||
chain = chains.get_chain(vals.get('chain', 'BTC'))
|
||||
|
||||
try:
|
||||
raw = extract_raw_secret(chain, vals)
|
||||
except Exception as e:
|
||||
return ('Unable to decode raw_secret and '
|
||||
'restore the seed value!\n\n\n'+str(e)), None
|
||||
|
||||
dis.fullscreen("Saving...")
|
||||
dis.progress_bar_show(.1)
|
||||
|
||||
@ -206,13 +205,6 @@ def restore_from_dict_ll(vals, raw):
|
||||
|
||||
k = key[8:]
|
||||
|
||||
if k == 'bkpw':
|
||||
# never import a cached backup password from a backup file.
|
||||
# write-side (render_backup_contents) strips bkpw, so a present
|
||||
# value means a tampered/crafted file trying to fixate the
|
||||
# password used for all FUTURE backups - drop it.
|
||||
continue
|
||||
|
||||
if k == 'sd2fa':
|
||||
# do NOT restore sd2fa as SD card can be lost or damaged
|
||||
# new version of firmware 5.1.3+ will not back sd2fa
|
||||
@ -289,10 +281,15 @@ def text_bk_parser(contents):
|
||||
|
||||
return vals
|
||||
|
||||
async def restore_tmp_from_dict_ll(vals, raw):
|
||||
async def restore_tmp_from_dict_ll(vals):
|
||||
from glob import dis
|
||||
|
||||
chain = chains.get_chain(vals.get('chain', 'BTC'))
|
||||
try:
|
||||
raw = extract_raw_secret(chain, vals)
|
||||
except Exception as e:
|
||||
return ('Unable to decode raw_secret and '
|
||||
'restore the seed value!\n\n\n' + str(e))
|
||||
|
||||
dis.fullscreen("Applying...")
|
||||
from seed import set_ephemeral_seed
|
||||
@ -309,11 +306,11 @@ async def restore_tmp_from_dict_ll(vals, raw):
|
||||
|
||||
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).
|
||||
# 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 need_ftux:
|
||||
@ -459,6 +456,8 @@ async def write_complete_backup(pwd, fname_pattern, write_sflash=False,
|
||||
|
||||
if write_sflash:
|
||||
# for use over USB and unit testing: commit file into PSRAM
|
||||
from sffile import SFFile
|
||||
|
||||
with SFFile(0, max_size=MAX_BACKUP_FILE_SIZE, message='Saving...') as fd:
|
||||
if zz:
|
||||
fd.write(hdr)
|
||||
@ -552,14 +551,10 @@ async def verify_backup_file(fname):
|
||||
# might be already closed on vdisk case due to filesystem unmount/mount
|
||||
pass
|
||||
|
||||
await ux_show_story("Backup file CRC checks out okay.\n\n"
|
||||
"Please note this is only a check against accidental truncation and similar."
|
||||
" Targeted modifications can still pass this test. You may further verify"
|
||||
" this backup file by starting the normal restore process (Restore Backup)"
|
||||
" and aborting it once decryption has been achieved.")
|
||||
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.")
|
||||
|
||||
|
||||
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
|
||||
|
||||
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,
|
||||
temporary=temporary)
|
||||
|
||||
if prob:
|
||||
await ux_show_story(prob, title='FAILED')
|
||||
|
||||
if words:
|
||||
if version.has_qwerty:
|
||||
from ux_q1 import seed_word_entry, CHARS_W
|
||||
|
||||
basename = None
|
||||
if isinstance(fname_or_fd, str):
|
||||
basename = fname_or_fd.split('/')[-1]
|
||||
if len(basename) > CHARS_W:
|
||||
basename = basename[:16] + "⋯" + basename[-16:]
|
||||
|
||||
return await seed_word_entry("Enter Password%s:" % (" for" if basename else ""),
|
||||
num_pw_words, done_cb=done, has_checksum=False,
|
||||
line2=basename)
|
||||
|
||||
from ux_q1 import seed_word_entry
|
||||
return await seed_word_entry('Enter Password:', num_pw_words,
|
||||
done_cb=done, has_checksum=False)
|
||||
# give them a menu to pick from, and start picking
|
||||
if usb:
|
||||
# we're not originating from a menu
|
||||
words = await seed.WordNestMenu.get_n_words(num_pw_words)
|
||||
if len(words) != num_pw_words:
|
||||
seed.WordNestMenu.pop_all()
|
||||
return
|
||||
m = seed.WordNestMenu(num_words=num_pw_words, has_checksum=False, done_cb=done)
|
||||
|
||||
await done(words)
|
||||
else:
|
||||
m = seed.WordNestMenu(num_words=num_pw_words, has_checksum=False, done_cb=done)
|
||||
the_ux.push(m)
|
||||
the_ux.push(m)
|
||||
|
||||
else:
|
||||
pwd = [] # cleartext if words=None
|
||||
if words is False:
|
||||
ipw = await ux_input_text("", prompt="Your Backup Password",
|
||||
min_len=bkpw_min_len, max_len=128)
|
||||
if not ipw: return
|
||||
pwd.append(ipw)
|
||||
|
||||
await done(pwd)
|
||||
|
||||
|
||||
def check_and_decrypt(fd, password):
|
||||
try:
|
||||
compat7z.check_file_headers(fd)
|
||||
except Exception as e:
|
||||
raise RuntimeError('Unable to read backup file.'
|
||||
' Has it been touched?\n\nError: '+str(e))
|
||||
|
||||
from glob import dis
|
||||
dis.fullscreen("Decrypting...")
|
||||
try:
|
||||
zz = compat7z.Builder()
|
||||
fname, contents = zz.read_file(fd, password, MAX_BACKUP_FILE_SIZE,
|
||||
progress_fcn=dis.progress_bar_show)
|
||||
|
||||
# simple quick sanity checks
|
||||
assert fname.endswith('.txt') # was == 'ckcc-backup.txt'
|
||||
assert contents[0:1] == b'#' and contents[-1:] == b'\n'
|
||||
return contents
|
||||
|
||||
except Exception as e:
|
||||
# assume everything here is "password wrong" errors
|
||||
raise RuntimeError('Unable to decrypt backup file. Incorrect password?'
|
||||
'\n\nTried:\n\n' + password)
|
||||
|
||||
|
||||
async def restore_complete_doit(fname_or_fd, words, file_cleanup=None, temporary=False,
|
||||
ux_confirm=True):
|
||||
async def restore_complete_doit(fname_or_fd, words, file_cleanup=None, temporary=False):
|
||||
# Open file, read it, maybe decrypt it; return string if any error
|
||||
# - some errors will be shown, None return in that case
|
||||
# - no return if successful (due to reboot)
|
||||
from glob import dis
|
||||
from files import CardSlot, CardMissingError, needs_microsd
|
||||
|
||||
# build password
|
||||
password = ' '.join(words)
|
||||
prob = None
|
||||
|
||||
if isinstance(fname_or_fd, int):
|
||||
# USB restore - backup is already in PSRAM, fname of fd is length
|
||||
# TXN_INPUT_OFFSET = 0
|
||||
with SFFile(0, length=fname_or_fd) as fd:
|
||||
if not words:
|
||||
contents = fd.read(fname_or_fd)
|
||||
else:
|
||||
# read full size, then decrypt
|
||||
fd = BytesIO(fd.read(fname_or_fd))
|
||||
try:
|
||||
contents = check_and_decrypt(fd, password)
|
||||
except RuntimeError as e:
|
||||
return str(e)
|
||||
else:
|
||||
try:
|
||||
with CardSlot(readonly=True) as card:
|
||||
# filename already picked, taste it and maybe consider using its data.
|
||||
try:
|
||||
fd = open(fname_or_fd, 'rb')
|
||||
except:
|
||||
return 'Unable to open backup file.\n\n' + str(fname_or_fd)
|
||||
try:
|
||||
with CardSlot(readonly=True) as card:
|
||||
# filename already picked, taste it and maybe consider using its data.
|
||||
try:
|
||||
fd = open(fname_or_fd, 'rb') if isinstance(fname_or_fd, str) else fname_or_fd
|
||||
except:
|
||||
return 'Unable to open backup file.\n\n' + str(fname_or_fd)
|
||||
|
||||
try:
|
||||
if words:
|
||||
contents = check_and_decrypt(fd, password)
|
||||
else:
|
||||
contents = fd.read()
|
||||
try:
|
||||
if not words:
|
||||
contents = fd.read()
|
||||
else:
|
||||
try:
|
||||
compat7z.check_file_headers(fd)
|
||||
except Exception as e:
|
||||
return 'Unable to read backup file. Has it been touched?\n\nError: ' \
|
||||
+ str(e)
|
||||
|
||||
except RuntimeError as e:
|
||||
return str(e)
|
||||
finally:
|
||||
fd.close()
|
||||
dis.fullscreen("Decrypting...")
|
||||
try:
|
||||
zz = compat7z.Builder()
|
||||
fname, contents = zz.read_file(fd, password, MAX_BACKUP_FILE_SIZE,
|
||||
progress_fcn=dis.progress_bar_show)
|
||||
|
||||
# simple quick sanity checks
|
||||
assert fname.endswith('.txt') # was == 'ckcc-backup.txt'
|
||||
assert contents[0:1] == b'#' and contents[-1:] == b'\n'
|
||||
|
||||
except Exception as e:
|
||||
# assume everything here is "password wrong" errors
|
||||
#print("pw wrong? %s" % e)
|
||||
|
||||
return ('Unable to decrypt backup file. Incorrect password?'
|
||||
'\n\nTried:\n\n' + password)
|
||||
finally:
|
||||
fd.close()
|
||||
|
||||
if file_cleanup:
|
||||
file_cleanup(fname_or_fd)
|
||||
|
||||
except CardMissingError:
|
||||
await needs_microsd()
|
||||
return
|
||||
except CardMissingError:
|
||||
await needs_microsd()
|
||||
return
|
||||
|
||||
try:
|
||||
vals = text_bk_parser(contents)
|
||||
except:
|
||||
return "Invalid backup file."
|
||||
|
||||
try:
|
||||
raw, node = extract_raw_secret(vals)
|
||||
except Exception as e:
|
||||
return ('Unable to decode raw_secret and '
|
||||
'restore the seed value!\n\n\n'+str(e))
|
||||
|
||||
if ux_confirm:
|
||||
# check master fingerprint from raw secret that is actually being loaded
|
||||
# master extended public keys can be wrong & is unverified
|
||||
xfp_str = xfp2str(swab32(node.my_fp()))
|
||||
ch = await ux_show_story("Above is the master fingerprint of the seed stored in the backup."
|
||||
" Press %s to continue, and load backup as %s seed. Press %s"
|
||||
" to abort." % (OK, "temporary" if temporary else "master", X),
|
||||
title="["+xfp_str+"]")
|
||||
if ch != "y":
|
||||
await ux_dramatic_pause('Aborted.', 2)
|
||||
return
|
||||
vals = text_bk_parser(contents)
|
||||
|
||||
# this leads to reboot if it works, else errors shown, etc.
|
||||
if temporary:
|
||||
return await restore_tmp_from_dict_ll(vals, raw)
|
||||
return await restore_tmp_from_dict_ll(vals)
|
||||
else:
|
||||
return await restore_from_dict(vals, raw)
|
||||
return await restore_from_dict(vals)
|
||||
|
||||
async def clone_start(*a):
|
||||
# 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
|
||||
|
||||
# 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,
|
||||
ux_confirm=False)
|
||||
prob = await restore_complete_doit(incoming, words, file_cleanup=delme)
|
||||
|
||||
if prob:
|
||||
await ux_show_story(prob, title='FAILED')
|
||||
|
||||
|
||||
@ -138,15 +138,12 @@ async def batt_idle_logout():
|
||||
# - even before login
|
||||
import glob
|
||||
from uasyncio import sleep_ms
|
||||
from glob import settings, dis, SCAN
|
||||
from glob import settings, dis
|
||||
import utime
|
||||
|
||||
while True:
|
||||
await sleep_ms(20000) # 20 seconds
|
||||
|
||||
if SCAN.busy_scanning:
|
||||
continue
|
||||
|
||||
if get_batt_level() is None:
|
||||
# on USB power
|
||||
continue
|
||||
|
||||
@ -54,7 +54,7 @@ def calc_num_qr(char_capacity, char_len, split_mod):
|
||||
if char_len > actual:
|
||||
need += 1
|
||||
|
||||
# Challenge: the final QR might have just a 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
|
||||
# and be more robust. Must respect split_mod alignment tho.
|
||||
level = ceil(char_len / need)
|
||||
@ -439,18 +439,5 @@ class BBQrPsramStorage(BBQrStorage):
|
||||
from glob import PSRAM
|
||||
return PSRAM.read_at(0, self.final_size)
|
||||
|
||||
def finalize(self):
|
||||
self._finalize()
|
||||
|
||||
if self.hdr.encoding == 'Z':
|
||||
self.zlib_decompress()
|
||||
|
||||
# PSBT-typed BBQrs end up at PSRAM[0..size]
|
||||
# skip a redundant PSRAM->heap->PSRAM round-trip
|
||||
if self.hdr.file_type == 'P':
|
||||
return self.hdr.file_type, self.final_size, 'PSRAM'
|
||||
|
||||
return self.hdr.file_type, self.final_size, self.get_buffer()
|
||||
|
||||
|
||||
# EOF
|
||||
|
||||
@ -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
|
||||
|
||||
async def login_repl():
|
||||
from glob import dis
|
||||
from glob import dis, settings
|
||||
from pincodes import pa
|
||||
|
||||
NUM_LINES = 7 # 10 - title - 2 for prompt
|
||||
@ -65,11 +65,11 @@ Example Commands:
|
||||
elif ln in ('help', 'cls', 'rand'):
|
||||
# no need for () for these commands
|
||||
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
|
||||
m = re_pin.match(ln)
|
||||
ln = m.group(1)+ '-' + m.group(2)
|
||||
|
||||
print(ln)
|
||||
try:
|
||||
pa.setup(ln)
|
||||
ok = pa.login()
|
||||
@ -83,7 +83,7 @@ Example Commands:
|
||||
else:
|
||||
ans = 'Error: ' + repr(exc.args)
|
||||
|
||||
elif re_prefix.match(ln) and (len(ln) <= 7):
|
||||
elif re_prefix.match(ln) and len(ln) <= 7:
|
||||
# show words
|
||||
ans = pa.prefix_words(ln[:-1].encode())
|
||||
else:
|
||||
|
||||
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
|
||||
from uhashlib import sha256
|
||||
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 AFC_PUBKEY, AFC_SEGWIT, AFC_BECH32, AFC_SCRIPT
|
||||
from block_height import BLOCK_HEIGHT
|
||||
from serializations import hash160, ser_compact_size, disassemble
|
||||
from ucollections import namedtuple
|
||||
from opcodes import OP_RETURN, OP_1, OP_16
|
||||
|
||||
# DO NOT CHANGE ORDER! PickAddrFmtMenu.__init__ expects correct order
|
||||
|
||||
SINGLESIG_AF = (AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH)
|
||||
|
||||
# See SLIP 132 <https://github.com/satoshilabs/slips/blob/master/slip-0132.md>
|
||||
@ -23,6 +22,8 @@ Slip132Version = namedtuple('Slip132Version', ('pub', 'priv', 'hint'))
|
||||
# See also:
|
||||
# - <https://github.com/satoshilabs/slips/blob/master/slip-0132.md>
|
||||
# - defines ypub/zpub/Xprc variants
|
||||
# - <https://github.com/satoshilabs/slips/blob/master/slip-0032.md>
|
||||
# - nice bech32 encoded scheme for going forward
|
||||
# - <https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-September/014907.html>
|
||||
# - mailing list post proposed ypub, etc.
|
||||
# - from <https://github.com/Bit-Wasp/bitcoin-php/issues/576>
|
||||
@ -81,41 +82,6 @@ class ChainsBase:
|
||||
or (version == cls.slip132[addr_fmt].priv)
|
||||
return node
|
||||
|
||||
@classmethod
|
||||
def script_pubkey(cls, addr_fmt, pubkey=None, script=None):
|
||||
digest = None
|
||||
if addr_fmt & AFC_SCRIPT:
|
||||
assert script, "need witness/redeem script"
|
||||
|
||||
if addr_fmt in [AF_P2WSH, AF_P2WSH_P2SH]:
|
||||
digest = ngu.hash.sha256s(script)
|
||||
# bech32 encoded segwit p2sh
|
||||
spk = b'\x00\x20' + digest
|
||||
if addr_fmt == AF_P2WSH_P2SH:
|
||||
# segwit p2wsh encoded as classic P2SH
|
||||
digest = hash160(spk)
|
||||
spk = b'\xA9\x14' + digest + b'\x87'
|
||||
|
||||
else:
|
||||
assert addr_fmt == AF_P2SH
|
||||
digest = hash160(script)
|
||||
spk = b'\xA9\x14' + digest + b'\x87'
|
||||
|
||||
else:
|
||||
assert pubkey
|
||||
keyhash = ngu.hash.hash160(pubkey)
|
||||
if addr_fmt == AF_CLASSIC:
|
||||
spk = b'\x76\xA9\x14' + keyhash + b'\x88\xAC'
|
||||
elif addr_fmt == AF_P2WPKH_P2SH:
|
||||
redeem_script = b'\x00\x14' + keyhash
|
||||
spk = b'\xA9\x14' + ngu.hash.hash160(redeem_script) + b'\x87'
|
||||
elif addr_fmt == AF_P2WPKH:
|
||||
spk = b'\x00\x14' + keyhash
|
||||
else:
|
||||
raise ValueError('bad address template: %s' % addr_fmt)
|
||||
|
||||
return spk, digest
|
||||
|
||||
@classmethod
|
||||
def p2sh_address(cls, addr_fmt, witdeem_script):
|
||||
# Multisig and general P2SH support
|
||||
@ -127,14 +93,21 @@ class ChainsBase:
|
||||
# - returns: str(address)
|
||||
|
||||
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
|
||||
addr = ngu.codecs.segwit_encode(cls.bech32_hrp, 0, digest)
|
||||
else:
|
||||
elif addr_fmt == AF_P2WSH_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)
|
||||
|
||||
return addr
|
||||
@ -144,8 +117,20 @@ class ChainsBase:
|
||||
# - renders a pubkey to an address
|
||||
# - works only with single-key addresses
|
||||
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
|
||||
def address(cls, node, addr_fmt):
|
||||
@ -254,7 +239,7 @@ class ChainsBase:
|
||||
return ngu.codecs.b58_encode(cls.b58_script + script[2:2+20])
|
||||
|
||||
# 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:])
|
||||
|
||||
# segwit v1 (P2TR) and later segwit version
|
||||
@ -265,40 +250,56 @@ class ChainsBase:
|
||||
|
||||
@classmethod
|
||||
def op_return(cls, script):
|
||||
try:
|
||||
gen = disassemble(script)
|
||||
item, opcode = next(gen)
|
||||
except (StopIteration, ValueError):
|
||||
return None
|
||||
|
||||
if opcode != OP_RETURN:
|
||||
return None
|
||||
# returns decoded string op return data if script is op return otherwise None
|
||||
gen = disassemble(script)
|
||||
script_type = next(gen)
|
||||
if OP_RETURN not in script_type:
|
||||
return
|
||||
|
||||
try:
|
||||
try:
|
||||
data, opcode = next(gen)
|
||||
except StopIteration:
|
||||
return b"" # bare OP_RETURN
|
||||
data = next(gen)[0]
|
||||
if data:
|
||||
return data
|
||||
except StopIteration:
|
||||
pass
|
||||
|
||||
try:
|
||||
next(gen)
|
||||
return None # extra ops/pushes -> raw script display
|
||||
except StopIteration: pass
|
||||
return b""
|
||||
|
||||
@classmethod
|
||||
def possible_address_fmt(cls, addr):
|
||||
# Given a text (serialized) address, return what
|
||||
# address format applies to the address, but
|
||||
# for AF_P2SH case, could be: AF_P2SH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH. .. we don't know
|
||||
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:
|
||||
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):
|
||||
# see <https://github.com/bitcoin/bitcoin/blob/master/src/chainparams.cpp#L140>
|
||||
ctype = 'BTC'
|
||||
name = 'Bitcoin Mainnet'
|
||||
ccc_min_block = BLOCK_HEIGHT
|
||||
ccc_min_block = 892714 # Apr 16/2025
|
||||
|
||||
slip132 = {
|
||||
AF_CLASSIC: Slip132Version(0x0488B21E, 0x0488ADE4, 'x'),
|
||||
@ -338,11 +339,26 @@ class BitcoinTestnet(ChainsBase):
|
||||
b44_cointype = 1
|
||||
|
||||
|
||||
class BitcoinRegtest(BitcoinTestnet):
|
||||
class BitcoinRegtest(ChainsBase):
|
||||
ctype = 'XRT'
|
||||
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'
|
||||
|
||||
b58_addr = bytes([111])
|
||||
b58_script = bytes([196])
|
||||
b58_privkey = bytes([239])
|
||||
|
||||
b44_cointype = 1
|
||||
|
||||
|
||||
def get_chain(short_name):
|
||||
# lookup object from name: 'BTC' or 'XTN'
|
||||
@ -370,7 +386,7 @@ def current_chain():
|
||||
# Overbuilt: will only be testnet and mainchain.
|
||||
AllChains = [BitcoinMain, BitcoinTestnet, BitcoinRegtest]
|
||||
|
||||
def slip132_deserialize(xp):
|
||||
def slip32_deserialize(xp):
|
||||
# .. and classify chain and addr-type, as implied by prefix
|
||||
node = ngu.hdnode.HDNode()
|
||||
version = node.deserialize(xp)
|
||||
@ -433,31 +449,17 @@ def parse_addr_fmt_str(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,
|
||||
AF_P2WPKH_P2SH: 49,
|
||||
AF_P2WPKH: 84}[addr_fmt]
|
||||
|
||||
|
||||
def addr_fmt_label(addr_fmt):
|
||||
# Text used in menus
|
||||
return {AF_CLASSIC: "Classic P2PKH",
|
||||
AF_P2WPKH_P2SH: "P2SH-Segwit",
|
||||
AF_P2WPKH: "Segwit P2WPKH"}[addr_fmt]
|
||||
|
||||
|
||||
def addr_fmt_str(addr_fmt):
|
||||
# Short string codes used for address format (industry standard)
|
||||
return {AF_CLASSIC: "p2pkh",
|
||||
AF_BARE_PK: "p2pk",
|
||||
AF_P2SH: "p2sh",
|
||||
AF_P2TR: "p2tr",
|
||||
AF_P2WPKH: "p2wpkh",
|
||||
AF_P2WSH: "p2wsh",
|
||||
AF_P2WPKH_P2SH: "p2sh-p2wpkh",
|
||||
AF_P2WSH_P2SH: "p2sh-p2wsh"}[addr_fmt]
|
||||
|
||||
def verify_recover_pubkey(sig, digest):
|
||||
# verifies a message digest against a signature and recovers
|
||||
# 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
|
||||
# characters on the output side of things, not input.
|
||||
# - must be first char in line
|
||||
OUT_CTRL_TITLE = '\x01' # be a title line
|
||||
OUT_CTRL_ADDRESS = '\x02' # it's a payment address
|
||||
OUT_CTRL_NOWRAP = '\x03' # do not word wrap this line
|
||||
OUT_CTRL_TITLE = '\x01' # must be first char in line: be a title line
|
||||
OUT_CTRL_ADDRESS = '\x02' # must be first char in line: it's a payment address
|
||||
|
||||
# EOF
|
||||
|
||||
@ -51,7 +51,9 @@ def decode_utf_16_le(s):
|
||||
'''
|
||||
|
||||
def read_var64(f):
|
||||
# Decode their silly 64-bit encoding.
|
||||
'''
|
||||
Decode their silly 64-bit encoding.
|
||||
'''
|
||||
first = ord(f.read(1))
|
||||
if first < 128:
|
||||
return first
|
||||
@ -98,7 +100,7 @@ def check_file_headers(f):
|
||||
# assume f is seekable
|
||||
fh = FileHeader.read(f)
|
||||
|
||||
if not fh.has_good_magic():
|
||||
if not fh.has_good_magic:
|
||||
raise ValueError("Bad magic bytes")
|
||||
|
||||
# read only first header
|
||||
@ -111,21 +113,22 @@ def check_file_headers(f):
|
||||
if sh.size > 10000:
|
||||
raise ValueError("Second header too big")
|
||||
|
||||
# FileHeader.read() always reads exactly calcsize('<6sBBL') = 12 bytes
|
||||
# SectionHeader.read() always reads exactly calcsize('<QQL') = 20 bytes
|
||||
# after those two calls, f.tell() is always start_pos + 32
|
||||
# assert f.tell() == 0x20 # expect 0x20
|
||||
# capture this spot
|
||||
# TODO 'data_start' unused
|
||||
data_start = f.tell() # expect 0x20
|
||||
|
||||
try:
|
||||
f.seek(sh.offset, 1)
|
||||
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
|
||||
# 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:
|
||||
raise ValueError("Confused file? %s" % e)
|
||||
raise ValueError("Confused file? %s" % e.message)
|
||||
|
||||
if masked_crc(th) != sh.crc:
|
||||
raise ValueError("Trailing header has wrong CRC")
|
||||
@ -173,6 +176,7 @@ class FileHeader(object):
|
||||
return masked_crc(self.bits)
|
||||
|
||||
|
||||
|
||||
class SectionHeader(namedtuple('SectionHeader', ['offset', 'size', 'crc' ])):
|
||||
@classmethod
|
||||
def read(cls, f):
|
||||
@ -209,7 +213,6 @@ class SectionHeader(namedtuple('SectionHeader', ['offset', 'size', 'crc' ])):
|
||||
def actual_crc(self):
|
||||
return masked_crc(self.bits)
|
||||
|
||||
|
||||
class Builder(object):
|
||||
def __init__(self, password=None, salt_len=16, iv_len=16, rounds_pow=13, progress_fcn=None):
|
||||
self.rounds_pow = rounds_pow # standard is 19, 16 and 17 work fine
|
||||
|
||||
@ -11,15 +11,6 @@ from bbqr import TYPE_LABELS
|
||||
from utils import decode_bip21_text
|
||||
|
||||
|
||||
def decode_qr_text(got):
|
||||
if isinstance(got, str):
|
||||
return got
|
||||
|
||||
try:
|
||||
return got.decode()
|
||||
except UnicodeError:
|
||||
raise QRDecodeExplained('UTF-8 decode failed')
|
||||
|
||||
def decode_seed_qr(data):
|
||||
# SeedQR: 4 digit groups of index into word list
|
||||
parts = [data[pos:pos + 4] for pos in range(0, len(data), 4)]
|
||||
@ -48,8 +39,6 @@ def decode_secret(got):
|
||||
# - xprv / tprv
|
||||
# - words (either full or prefixes, case insensitive)
|
||||
# - SeedQR (github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md)
|
||||
# - word lists are NOT BIP-39-checksum-validated here. Callers that
|
||||
# require a valid seed must run bip39.a2b_words(...)
|
||||
|
||||
if len(got) > 300:
|
||||
raise ValueError("Too big.")
|
||||
@ -62,7 +51,7 @@ def decode_secret(got):
|
||||
# xprv or tprv: private key import for sure
|
||||
# - verify checksum is right
|
||||
try:
|
||||
ngu.codecs.b58_decode(got)
|
||||
raw = ngu.codecs.b58_decode(got)
|
||||
except:
|
||||
raise ValueError('corrupt xprv?')
|
||||
|
||||
@ -70,9 +59,17 @@ def decode_secret(got):
|
||||
|
||||
if len(got) in (51, 52):
|
||||
try:
|
||||
from wif import decode_wif
|
||||
kp, testnet, compressed = decode_wif(got)
|
||||
return 'wif', (got, kp, compressed, testnet)
|
||||
raw = ngu.codecs.b58_decode(got)
|
||||
if raw[0] in (0xef, 0x80):
|
||||
testnet = True if raw[0] == 0xef else False
|
||||
if len(raw) in (33, 34): # uncompressed pubkey
|
||||
compressed = False
|
||||
if len(raw) == 34: # compressed pubkey
|
||||
assert raw[33] == 0x01
|
||||
compressed = True
|
||||
sk = raw[1:33]
|
||||
kp = ngu.secp256k1.keypair(sk)
|
||||
return 'wif', (got, kp, compressed, testnet)
|
||||
except: pass
|
||||
|
||||
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()
|
||||
|
||||
if ty == 'P':
|
||||
# `got` is the literal 'PSRAM' from BBQrPsramStorage when data already there
|
||||
# otherwise it's real bytes
|
||||
# may already be in PSRAM, avoid a copy here
|
||||
from glob import PSRAM
|
||||
if PSRAM.is_at(got, 0):
|
||||
got = 'PSRAM' # see qr_psbt_sign()
|
||||
|
||||
return 'psbt', (None, final_size, got)
|
||||
|
||||
elif ty == 'T':
|
||||
@ -128,10 +128,9 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
|
||||
|
||||
elif ty == 'U':
|
||||
# continue thru code below for TEXT
|
||||
got = decode_qr_text(got)
|
||||
pass
|
||||
|
||||
elif ty == 'J':
|
||||
got = decode_qr_text(got)
|
||||
what = "json"
|
||||
if "msg" in got:
|
||||
what = "smsg"
|
||||
@ -140,11 +139,6 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
|
||||
|
||||
elif ty in 'RSE':
|
||||
# key-teleport related
|
||||
|
||||
from pincodes import pa
|
||||
if pa.hobbled_mode and ty != 'E':
|
||||
raise QRDecodeExplained("KT Blocked")
|
||||
|
||||
if ty == 'R' and len(got) != 33:
|
||||
raise QRDecodeExplained("Truncated KT RX")
|
||||
|
||||
@ -196,7 +190,12 @@ def decode_short_text(got):
|
||||
# - if bad checksum on bitcoin addr, we treat as text... since might be
|
||||
# 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?
|
||||
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]+"
|
||||
rgx = ure.compile(cc_ms_pat)
|
||||
# go line by line and match above, once 2 matches observed - considered multisig
|
||||
# important to not use ure.search for big strings (can run out of stack);
|
||||
# a real line here is a "<8-hex xfp>: <xpub>" key (~121 chars)
|
||||
# important to not use ure.search for big strings (can run out of stack)
|
||||
c = 0 # match count
|
||||
for l in got.split("\n"):
|
||||
if len(l) <= 150 and rgx.search(l):
|
||||
if rgx.search(l):
|
||||
c += 1
|
||||
if c > 1:
|
||||
return 'multi', (got,)
|
||||
|
||||
@ -2,8 +2,9 @@
|
||||
#
|
||||
# display.py - OLED rendering
|
||||
#
|
||||
import machine, uzlib, ckcc, utime, version
|
||||
import machine, uzlib, ckcc, utime
|
||||
from ssd1306 import SSD1306_SPI
|
||||
from version import is_devmode
|
||||
import framebuf
|
||||
from graphics_mk4 import Graphics
|
||||
from charcodes import OUT_CTRL_TITLE, OUT_CTRL_ADDRESS
|
||||
@ -34,14 +35,11 @@ class Display:
|
||||
dc_pin = Pin('PA8', Pin.OUT)
|
||||
cs_pin = Pin('PA4', Pin.OUT)
|
||||
|
||||
if version.mk_num == 5:
|
||||
# Early revs (A-D) needed this pin asserted to enable +12v to OLED
|
||||
# - removed in rev E and later boards, but keep here for dev boards
|
||||
# - remove this in 2027
|
||||
vcc_en = Pin('V12EN', Pin.OUT) # aka PC1
|
||||
vcc_en(1)
|
||||
|
||||
self.dis = SSD1306_SPI(128, 64, spi, dc_pin, reset_pin, cs_pin, is_mk5=(version.mk_num==5))
|
||||
try:
|
||||
self.dis = SSD1306_SPI(128, 64, spi, dc_pin, reset_pin, cs_pin)
|
||||
except OSError:
|
||||
print("OLED unplugged?")
|
||||
raise
|
||||
|
||||
self.last_bar_update = 0
|
||||
self.clear()
|
||||
@ -144,7 +142,7 @@ class Display:
|
||||
self.icon(128-3, 1, 'scroll')
|
||||
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.text(-2, 21, 'D', 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):
|
||||
# show a simple message "fullscreen".
|
||||
# - 'line2' not supported on smaller screen sizes, ignore
|
||||
self.clear()
|
||||
y = 14
|
||||
self.text(None, y, msg, font=FontLarge)
|
||||
|
||||
if line2:
|
||||
# 21 + 6 ie. FontLarge.height of above text + FontTiny.height as space between
|
||||
self.text(None, y + 27, line2, font=FontSmall)
|
||||
|
||||
if percent is not None:
|
||||
self.progress_bar(percent)
|
||||
self.show()
|
||||
@ -206,20 +201,61 @@ class Display:
|
||||
|
||||
def busy_bar(self, enable):
|
||||
# Render a continuous activity (not progress) bar in lower 8 lines of display
|
||||
#
|
||||
# - using OLED itself to do the animation, so smooth and CPU free
|
||||
# - cannot preserve bottom 8 lines, since we have to destructively write there
|
||||
# - assumes normal horz addr mode: 0x20, 0x00
|
||||
# - speed_code=>framedelay: 0=5fr, 1=64fr, 2=128, 3=256, 4=3, 5=4, 6=25, 7=2frames
|
||||
# unused: assert 0 <= speed_code <= 7
|
||||
|
||||
setup = bytes([
|
||||
0x21, 0x00, 0x7f, # setup column address range (start, end): 0-127
|
||||
0x22, 7, 7, # setup page start/end address: page 7=last 8 lines
|
||||
])
|
||||
animate = bytes([
|
||||
0x2e, # stop animations in progress
|
||||
0x26, # scroll leftwards (stock ticker mode)
|
||||
0, # placeholder
|
||||
7, # start 'page' (vertical)
|
||||
5, # "speed_code" # scroll speed: 7=fastest, but no order to it
|
||||
7, # end 'page'
|
||||
0, 0xff, # placeholders
|
||||
0x2f # start
|
||||
])
|
||||
|
||||
cleanup = bytes([
|
||||
0x2e, # stop animation
|
||||
0x20, 0x00, # horz addr-ing mode
|
||||
0x21, 0x00, 0x7f, # setup column address range (start, end): 0-127
|
||||
0x22, 7, 7, # setup page start/end address: page 7=last 8 lines
|
||||
])
|
||||
|
||||
if not enable:
|
||||
self.dis.busy_bar(False, None)
|
||||
# stop animation, and redraw old (new) screen
|
||||
self.write_cmds(cleanup)
|
||||
self.show()
|
||||
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):
|
||||
# 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):
|
||||
# draw a menu item, perhaps selected, checked.
|
||||
@ -230,18 +266,17 @@ class Display:
|
||||
if is_sel:
|
||||
self.dis.fill_rect(0, y, Display.WIDTH, h-1, 1)
|
||||
self.icon(2, y, 'wedge', invert=1)
|
||||
nx = self.text(x, y, msg, invert=1)
|
||||
self.text(x, y, msg, invert=1)
|
||||
else:
|
||||
nx = self.text(x, y, msg)
|
||||
self.text(x, y, msg)
|
||||
|
||||
# LATER: removed because caused confusion w/ underscore
|
||||
#if msg[0] == ' ' and space_indicators:
|
||||
# see also graphics/mono/space.txt
|
||||
#self.icon(x-2, y+9, 'space', invert=is_sel)
|
||||
|
||||
if is_checked and nx <= 113:
|
||||
# omit checkmark if it doesn't fit
|
||||
self.icon(113, y, 'selected', invert=is_sel)
|
||||
if is_checked:
|
||||
self.icon(108, y, 'selected', invert=is_sel)
|
||||
|
||||
def menu_show(self, *a):
|
||||
self.show()
|
||||
@ -293,25 +328,15 @@ class Display:
|
||||
# no status bar on Mk4
|
||||
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,
|
||||
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
|
||||
# - 'msg' will appear to right if very short, else under in tiny
|
||||
# - ignores "is_addr" because exactly zero space to do anything special
|
||||
from utils import word_wrap
|
||||
|
||||
self.clear()
|
||||
|
||||
w = qr_data.width()
|
||||
if w <= 29:
|
||||
# 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)
|
||||
self.dis.blit(gly, XO, YO, 1)
|
||||
|
||||
self._draw_qr_display(bw, lm, msg, is_alnum, sidebar, idx_hint, invert, is_addr, side_msg)
|
||||
|
||||
def _draw_qr_display(self, bw, lm, msg, is_alnum, sidebar, idx_hint, invert,
|
||||
is_addr=False, side_msg=None):
|
||||
# does not draw actual QR, but all other things in the screen
|
||||
from utils import word_wrap
|
||||
|
||||
if not sidebar and not msg:
|
||||
pass
|
||||
elif not sidebar and ((len(msg) > (5*7)) 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)
|
||||
# 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
|
||||
ww = ((128 - x)//4) - 1 # char width avail
|
||||
y = 1
|
||||
@ -379,11 +397,8 @@ class Display:
|
||||
self.text(x, y, line, FontTiny)
|
||||
y += 8
|
||||
|
||||
if side_msg and (len(side_msg) < 15):
|
||||
y_pos = y + 8
|
||||
# only render if there is space
|
||||
if (self.HEIGHT - y_pos) >= FontTiny.height:
|
||||
self.text(x+4, y+8, side_msg, FontTiny)
|
||||
if is_addr and is_change:
|
||||
self.text(x+4, y+8, "CHANGE BACK", FontTiny)
|
||||
else:
|
||||
# hand-positioned for known cases
|
||||
# - 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 b2a_base64
|
||||
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
|
||||
|
||||
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?"
|
||||
index = await ux_enter_bip32_index(msg, unlimited=settings.get("b85max", False))
|
||||
if index is None: return
|
||||
if index is None:
|
||||
return
|
||||
|
||||
dis.fullscreen("Working...")
|
||||
new_secret, width, s_mode, path = bip85_derive(picked, index)
|
||||
@ -179,7 +180,7 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
|
||||
elif s_mode == 'xprv':
|
||||
# Raw XPRV value.
|
||||
ch, pk = new_secret[0:32], new_secret[32:64]
|
||||
master_node = node_from_privkey(pk, ch)
|
||||
master_node = ngu.hdnode.HDNode().from_chaincode_privkey(ch, pk)
|
||||
node = 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:
|
||||
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
|
||||
if encoded is not None:
|
||||
key0 = 'to switch to derived secret'
|
||||
|
||||
prompt, escape = export_prompt_builder('data', key0=key0, key6=key6,
|
||||
elif s_mode == 'pw':
|
||||
key0 = 'to type password over USB'
|
||||
prompt, escape = export_prompt_builder('data', key0=key0,
|
||||
no_qr=(not qr), force_prompt=True)
|
||||
title = None
|
||||
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,
|
||||
strict_escape=True, sensitive=True)
|
||||
choice = import_export_prompt_decode(ch)
|
||||
if choice == KEY_CANCEL:
|
||||
break
|
||||
elif isinstance(choice, dict):
|
||||
if isinstance(choice, dict):
|
||||
# write to SD card or Virtual Disk: simple text file
|
||||
dis.fullscreen("Saving...")
|
||||
try:
|
||||
@ -240,33 +241,33 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
|
||||
await needs_microsd()
|
||||
continue
|
||||
except Exception as e:
|
||||
await ux_show_story('Failed to write!\n\n'+str(e))
|
||||
await ux_show_story('Failed to write!\n\n\n'+str(e))
|
||||
continue
|
||||
|
||||
story = "Filename is:\n\n%s" % out_fn
|
||||
story += "\n\nSignature filename is:\n\n%s" % sig_nice
|
||||
await ux_show_story(story, title='Saved')
|
||||
|
||||
elif choice == KEY_CANCEL:
|
||||
break
|
||||
elif choice == KEY_QR:
|
||||
from ux import show_qr_code
|
||||
await show_qr_code(qr, qr_alnum, is_secret=True)
|
||||
|
||||
elif (choice == '0') and (encoded is not None):
|
||||
# switch over to new secret!
|
||||
dis.fullscreen("Applying...")
|
||||
from actions import goto_top_menu
|
||||
from glob import settings
|
||||
xfp_str = xfp2str(settings.get("xfp", 0))
|
||||
await seed.set_ephemeral_seed(
|
||||
encoded,
|
||||
origin='BIP85 Derived from [%s], index=%d' % (xfp_str, index)
|
||||
)
|
||||
goto_top_menu()
|
||||
break
|
||||
|
||||
elif choice == "6":
|
||||
# gets confirmation then types it
|
||||
await single_send_keystrokes(qr, path)
|
||||
elif choice == '0':
|
||||
if s_mode == 'pw':
|
||||
# gets confirmation then types it
|
||||
await single_send_keystrokes(qr, path)
|
||||
elif encoded is not None:
|
||||
# switch over to new secret!
|
||||
dis.fullscreen("Applying...")
|
||||
from actions import goto_top_menu
|
||||
from glob import settings
|
||||
xfp_str = xfp2str(settings.get("xfp", 0))
|
||||
await seed.set_ephemeral_seed(
|
||||
encoded,
|
||||
origin='BIP85 Derived from [%s], index=%d' % (xfp_str, index)
|
||||
)
|
||||
goto_top_menu()
|
||||
break
|
||||
|
||||
elif NFC and choice == KEY_NFC:
|
||||
# Share any of these over NFC
|
||||
@ -291,7 +292,7 @@ async def password_entry(*args, **kwargs):
|
||||
|
||||
while True:
|
||||
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:
|
||||
break
|
||||
|
||||
|
||||
@ -19,10 +19,10 @@ class CCBusyError(RuntimeError):
|
||||
# HSM is blocking your action
|
||||
class HSMDenied(RuntimeError):
|
||||
pass
|
||||
|
||||
class HSMCMDDisabled(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
# PSBT / transaction related
|
||||
class FatalPSBTIssue(RuntimeError):
|
||||
pass
|
||||
@ -51,12 +51,8 @@ class QRDecodeExplained(ValueError):
|
||||
class UnknownAddressExplained(ValueError):
|
||||
pass
|
||||
|
||||
# We're not going to (co-)sign using spending policy features
|
||||
class SpendPolicyViolation(RuntimeError):
|
||||
pass
|
||||
|
||||
# data too big for simple QR
|
||||
class QRTooBigError(ValueError):
|
||||
# We're not going to co-sign using CCC feature
|
||||
class CCCPolicyViolationError(RuntimeError):
|
||||
pass
|
||||
|
||||
# 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 charcodes import KEY_NFC, KEY_CANCEL, KEY_QR
|
||||
from ownership import OWNERSHIP
|
||||
from exceptions import QRTooBigError
|
||||
|
||||
async def export_by_qr(body, label, type_code, force_bbqr=False):
|
||||
# render as QR and show on-screen
|
||||
@ -20,10 +19,10 @@ async def export_by_qr(body, label, type_code, force_bbqr=False):
|
||||
|
||||
try:
|
||||
if force_bbqr or len(body) > 2000:
|
||||
raise QRTooBigError
|
||||
raise ValueError
|
||||
|
||||
await show_qr_code(body)
|
||||
except QRTooBigError:
|
||||
except (ValueError, RuntimeError, TypeError):
|
||||
if version.has_qwerty:
|
||||
# do BBQr on Q
|
||||
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,
|
||||
is_json=False, force_bbqr=False, force_prompt=False, direct_way=None,
|
||||
intro="", footer="", ux_title=None):
|
||||
is_json=False, force_bbqr=False, force_prompt=False):
|
||||
# export text and json files while offering NFC, QR & Vdisk
|
||||
# produces signed export in case of SD/Vdisk (signed with key at deriv and addr_fmt)
|
||||
# checks if suitable to offer QR export on Mk4
|
||||
# argument contents can support function that generates content
|
||||
# argument direct way can be KEY_{NFC,QR}, any other truth value is SD/Vdisk,
|
||||
# if None ask for way via UX story
|
||||
from glob import dis, NFC, VD
|
||||
from files import CardSlot, CardMissingError, needs_microsd
|
||||
from qrs import MAX_V11_CHAR_LIMIT
|
||||
@ -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)
|
||||
|
||||
ch = direct_way # set it to direct way only once, outside the loop
|
||||
while True:
|
||||
if direct_way is None:
|
||||
ch = await import_export_prompt("%s file" % title, intro=intro, footnotes=footer,
|
||||
force_prompt=force_prompt, no_qr=no_qr, title=ux_title)
|
||||
ch = await import_export_prompt("%s file" % title,
|
||||
force_prompt=force_prompt, no_qr=no_qr)
|
||||
if ch == KEY_CANCEL:
|
||||
break
|
||||
elif ch == KEY_QR:
|
||||
await export_by_qr(contents, title, "J" if is_json else "U", force_bbqr=force_bbqr)
|
||||
continue
|
||||
elif ch == KEY_NFC:
|
||||
if is_json:
|
||||
await NFC.share_json(contents)
|
||||
else:
|
||||
await NFC.share_text(contents)
|
||||
else:
|
||||
# SD/VDisk
|
||||
# choose a filename
|
||||
try:
|
||||
dis.fullscreen("Saving...")
|
||||
with CardSlot(**ch) as card:
|
||||
fname, nice = card.pick_filename(fname_pattern)
|
||||
continue
|
||||
|
||||
# do actual write
|
||||
with open(fname, 'wt' if is_json else 'wb') as fd:
|
||||
fd.write(contents)
|
||||
# choose a filename
|
||||
try:
|
||||
dis.fullscreen("Saving...")
|
||||
with CardSlot(**ch) as card:
|
||||
fname, nice = card.pick_filename(fname_pattern)
|
||||
|
||||
if sig:
|
||||
h = ngu.hash.sha256s(contents.encode())
|
||||
sig_nice = write_sig_file([(h, fname)], derive, addr_fmt)
|
||||
# do actual write
|
||||
with open(fname, 'wt' if is_json else 'wb') as fd:
|
||||
fd.write(contents)
|
||||
|
||||
msg = '%s file written:\n\n%s' % (title, nice)
|
||||
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 needs_microsd()
|
||||
except Exception as e:
|
||||
await ux_show_story('Failed to write!\n\n' + str(e))
|
||||
await ux_show_story(msg)
|
||||
|
||||
except CardMissingError:
|
||||
await needs_microsd()
|
||||
except Exception as e:
|
||||
await ux_show_story('Failed to write!\n\n\n' + str(e))
|
||||
|
||||
# both exceptions & success gets here
|
||||
if no_qr and (NFC is None) and (VD is None) and not force_prompt:
|
||||
# user has no other ways enabled, we already exported to SD - done
|
||||
return
|
||||
|
||||
if direct_way:
|
||||
return
|
||||
|
||||
def generate_public_contents():
|
||||
# Generate public details about wallet.
|
||||
#
|
||||
@ -425,14 +417,14 @@ def generate_generic_export(account_num=0):
|
||||
def generate_electrum_wallet(addr_type, account_num):
|
||||
# Generate line-by-line JSON details about wallet.
|
||||
#
|
||||
# Much reverse engineering of Electrum here. It's a complex
|
||||
# Much reverse enginerring of Electrum here. It's a complex
|
||||
# legacy file format.
|
||||
|
||||
chain = chains.current_chain()
|
||||
|
||||
xfp = settings.get('xfp')
|
||||
|
||||
# Must get the derivation path, and the SLIP132 version bytes right!
|
||||
# Must get the derivation path, and the SLIP32 version bytes right!
|
||||
mode = chains.af_to_bip44_purpose(addr_type)
|
||||
|
||||
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,
|
||||
fname_pattern="descriptor.txt", direct_way=None):
|
||||
fname_pattern="descriptor.txt"):
|
||||
from descriptor import Descriptor
|
||||
from glob import dis
|
||||
|
||||
dis.fullscreen('Generating...')
|
||||
chain = chains.current_chain()
|
||||
|
||||
xfp = settings.get('xfp', 0)
|
||||
xfp = settings.get('xfp')
|
||||
dis.progress_bar_show(0.1)
|
||||
if mode is None:
|
||||
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)
|
||||
|
||||
intro, footer = (body, "") if version.has_qwerty else ("", body)
|
||||
title = "Descriptor"
|
||||
await export_contents(title, body, fname_pattern, derive + "/0/0", addr_type,
|
||||
force_prompt=True, direct_way=direct_way, intro=intro, footer=footer,
|
||||
ux_title=title if version.has_qwerty else None)
|
||||
|
||||
|
||||
async def make_key_expression_export(orig_der, addr_fmt=AF_CLASSIC, fname_pattern="key_expr.txt"):
|
||||
from glob import dis
|
||||
|
||||
dis.fullscreen('Generating...')
|
||||
|
||||
xfp = xfp2str(settings.get('xfp', 0)).lower()
|
||||
|
||||
with stash.SensitiveValues() as sv:
|
||||
ek = chains.current_chain().serialize_public(sv.derive_path(orig_der))
|
||||
|
||||
body = "[%s/%s]%s" % (xfp, orig_der.replace("m/", ""), ek)
|
||||
|
||||
intro, footer = (body, "") if version.has_qwerty else ("", body)
|
||||
title = "Key Expression"
|
||||
await export_contents(title, body, fname_pattern, orig_der + "/0/0", addr_fmt,
|
||||
force_prompt=True, intro=intro, footer=footer,
|
||||
ux_title=title if version.has_qwerty else None)
|
||||
await export_contents("Descriptor", body, fname_pattern, derive + "/0/0",
|
||||
addr_type, force_prompt=True)
|
||||
|
||||
# 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 trick_pins import TrickPinMenu
|
||||
from tapsigner import import_tapsigner_backup_file
|
||||
from ccc import toggle_ccc_feature, sssp_spending_policy, sssp_feature_menu
|
||||
from wif import WIFStoreMenu
|
||||
from ccc import toggle_ccc_feature
|
||||
|
||||
# useful shortcut keys
|
||||
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)
|
||||
return version.supports_hsm and has_real_secret()
|
||||
|
||||
def qr_and_ms():
|
||||
# has QR scanner, and at least one MS wallet
|
||||
if not version.has_qr: return False
|
||||
return bool(settings.get('multisig', False))
|
||||
|
||||
def has_pushtx_url():
|
||||
# they want to use PushTX feature
|
||||
return bool(settings.get("ptxurl", False))
|
||||
|
||||
# Spending Policy (Hobbled mode) predicates.
|
||||
#
|
||||
def is_hobble_testdrive():
|
||||
from pincodes import pa
|
||||
return (pa.hobbled_mode == 2)
|
||||
|
||||
def sssp_related_keys():
|
||||
return sssp_spending_policy('okeys')
|
||||
|
||||
def sssp_allow_passphrase():
|
||||
return word_based_seed() and sssp_related_keys()
|
||||
|
||||
def sssp_allow_notes():
|
||||
return settings.get("secnap", False) and sssp_spending_policy('notes')
|
||||
|
||||
def sssp_allow_vault():
|
||||
return settings.master_get('seedvault') and sssp_related_keys()
|
||||
|
||||
async def goto_home(*a):
|
||||
goto_top_menu()
|
||||
|
||||
@ -165,21 +137,6 @@ LoginPrefsMenu = [
|
||||
MenuItem('Test Login Now', f=login_now, arg=1),
|
||||
]
|
||||
|
||||
|
||||
# obscure settings, not more dangerous, just more personal
|
||||
BuriedSettingsMenu = [
|
||||
ToggleMenuItem('Home Menu XFP', 'hmx', ['Only Tmp', 'Always Show'],
|
||||
story=('Forces display of XFP (seed fingerprint) '
|
||||
'at top of main menu. Normally, XFP is shown only when '
|
||||
'temporary seed is active.\n\n'
|
||||
'Master seed is displayed as <XFP>, temporary seeds as [XFP].'),
|
||||
predicate=has_real_secret,
|
||||
on_change=goto_home),
|
||||
ToggleMenuItem('Menu Wrapping', 'wa', ['Default', 'Always Wrap'],
|
||||
story='''When enabled, allows scrolling past menu top/bottom \
|
||||
(wrap around). By default, this only happens in menus whose length is greater than 10.'''),
|
||||
]
|
||||
|
||||
SettingsMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem('Login Settings', menu=LoginPrefsMenu),
|
||||
@ -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 \
|
||||
which take apart the flash chips of the SDCard may still be able to find the \
|
||||
data or filenames.'''),
|
||||
ToggleMenuItem('Menu Wrapping', 'wa', ['Default Off', 'Enable'],
|
||||
story='''When enabled, allows scrolling past menu top/bottom \
|
||||
(wrap around). By default, this only happens in very large menus.'''),
|
||||
ToggleMenuItem('Home Menu XFP', 'hmx', ['Only Tmp', 'Always Show'],
|
||||
story=('Forces display of XFP (seed fingerprint) '
|
||||
'at top of main menu. Normally, XFP is shown only when '
|
||||
'temporary seed is active.\n\n'
|
||||
'Master seed is displayed as <XFP>, temporary seeds as [XFP].'),
|
||||
predicate=has_real_secret,
|
||||
on_change=goto_home),
|
||||
ToggleMenuItem('Keyboard EMU', 'emu', ['Default Off', 'Enable'],
|
||||
on_change=usb_keyboard_emulation,
|
||||
predicate=has_secrets, # cannot generate BIP85 passwords without secret
|
||||
story='''This mode adds a top-level menu item for typing \
|
||||
deterministically-generated passwords (BIP-85), directly into an \
|
||||
attached USB computer (as an emulated keyboard).'''),
|
||||
MenuItem('Buried Settings', menu=BuriedSettingsMenu),
|
||||
]
|
||||
|
||||
XpubExportMenu = [
|
||||
@ -226,24 +192,20 @@ WalletExportMenu = [
|
||||
MenuItem("Cove", f=named_generic_skeleton, arg="Cove"),
|
||||
MenuItem("Bitcoin Core", f=bitcoin_core_skeleton),
|
||||
MenuItem("Nunchuk", f=named_generic_skeleton, arg="Nunchuk"),
|
||||
MenuItem("Bull Bitcoin", f=ss_descriptor_skeleton,
|
||||
arg=(True, [AF_P2WPKH], "", "bull-bitcoin.txt", KEY_QR)),
|
||||
MenuItem("Blue Wallet", f=electrum_skeleton, arg="Blue"),
|
||||
MenuItem("Electrum Wallet", f=electrum_skeleton, arg="Electrum"),
|
||||
MenuItem("Zeus", f=ss_descriptor_skeleton,
|
||||
arg=(True, [AF_P2WPKH, AF_P2WPKH_P2SH], "Zeus Wallet", "zeus-export.txt")),
|
||||
MenuItem("Electrum Wallet", f=electrum_skeleton),
|
||||
MenuItem("Wasabi Wallet", f=wasabi_skeleton),
|
||||
MenuItem("Fully Noded", f=named_generic_skeleton, arg="Fully Noded"),
|
||||
MenuItem("Unchained", f=unchained_capital_export),
|
||||
MenuItem("Theya", f=named_generic_skeleton, arg="Theya"),
|
||||
MenuItem("Bitcoin Safe", f=named_generic_skeleton, arg="Bitcoin Safe"),
|
||||
MenuItem("Zeus", f=ss_descriptor_skeleton,
|
||||
arg=(True, [AF_P2WPKH, AF_P2WPKH_P2SH], "Zeus Wallet", "zeus-export.txt", None)),
|
||||
MenuItem("Samourai Postmix", f=samourai_post_mix_descriptor_export),
|
||||
MenuItem("Samourai Premix", f=samourai_pre_mix_descriptor_export),
|
||||
# MenuItem("Samourai BadBank", f=samourai_bad_bank_descriptor_export), # not released yet
|
||||
MenuItem("Descriptor", f=ss_descriptor_skeleton),
|
||||
MenuItem("Generic JSON", f=generic_skeleton),
|
||||
MenuItem("Export XPUB", menu=XpubExportMenu),
|
||||
MenuItem("Key Expression", f=key_expression_skeleton),
|
||||
MenuItem("Dump Summary", predicate=has_secrets, f=dump_summary),
|
||||
]
|
||||
|
||||
@ -307,8 +269,6 @@ DebugFunctionsMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem("Keyboard Test", f=keyboard_test),
|
||||
MenuItem('BBQr Demo', f=debug_bbqr_test, predicate=version.has_qwerty),
|
||||
MenuItem("NFC Test", f=quick_nfc_test),
|
||||
MenuItem('Clear Tested', f=clear_tested_flag),
|
||||
MenuItem('Debug: assert', f=debug_assert),
|
||||
MenuItem('Debug: except', f=debug_except),
|
||||
MenuItem('Check: BL FW', f=check_firewall_read),
|
||||
@ -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('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("Nuke Device", f=nuke_device),
|
||||
]
|
||||
|
||||
BackupStuffMenu = [
|
||||
@ -394,20 +353,7 @@ NFCToolsMenu = [
|
||||
MenuItem('Verify Address', f=nfc_address_verify),
|
||||
MenuItem('File Share', f=nfc_share_file),
|
||||
MenuItem('Import Multisig', f=import_multisig_nfc),
|
||||
MenuItem('Push Transaction', f=nfc_pushtx_file, predicate=has_pushtx_url),
|
||||
]
|
||||
|
||||
|
||||
SpendingPolicySubMenu = [
|
||||
NonDefaultMenuItem('Single-Signer', 'sssp', f=sssp_feature_menu, predicate=has_real_secret),
|
||||
NonDefaultMenuItem('Co-Sign Multi.' if not version.has_qwerty else 'Co-Sign Multisig (CCC)',
|
||||
'ccc', f=toggle_ccc_feature, predicate=is_not_tmp),
|
||||
ToggleMenuItem('HSM Mode', 'hsmcmd', ['Default Off', 'Enable'],
|
||||
story=("Enable HSM? Enables all user management commands, and other HSM-only USB commands. "
|
||||
"By default these commands are disabled."),
|
||||
predicate=hsm_available),
|
||||
MenuItem('User Management', menu=make_users_menu,
|
||||
predicate=lambda: hsm_available() and settings.get('hsmcmd', False)),
|
||||
MenuItem('Push Transaction', f=nfc_pushtx_file, predicate=lambda: settings.get("ptxurl", False)),
|
||||
]
|
||||
|
||||
AdvancedNormalMenu = [
|
||||
@ -423,9 +369,14 @@ AdvancedNormalMenu = [
|
||||
MenuItem("View Identity", f=view_ident),
|
||||
MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu),
|
||||
MenuItem("Key Teleport (start)", f=kt_start_rx, predicate=version.has_qr),
|
||||
MenuItem("Spending Policy", menu=SpendingPolicySubMenu,shortcut='s',predicate=has_real_secret),
|
||||
MenuItem('Paper Wallets', f=make_paper_wallet),
|
||||
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("Danger Zone", menu=DangerZoneMenu, shortcut='z'),
|
||||
]
|
||||
@ -469,7 +420,6 @@ EmptyWallet = [
|
||||
MenuItem('New Seed Words', menu=NewSeedMenu),
|
||||
MenuItem('Import Existing', menu=ImportWallet),
|
||||
MenuItem("Migrate Coldcard", menu=clone_start),
|
||||
MenuItem("Key Teleport (start)", f=kt_start_rx, predicate=version.has_qr),
|
||||
MenuItem('Help', f=virgin_help, predicate=not version.has_qwerty),
|
||||
MenuItem('Advanced/Tools', menu=AdvancedPinnedVirginMenu, shortcut='t'),
|
||||
MenuItem('Settings', menu=SettingsMenu),
|
||||
@ -488,7 +438,7 @@ NormalSystem = [
|
||||
MenuItem("Address Explorer", menu=address_explore, shortcut='x'),
|
||||
MenuItem('Secure Notes & Passwords', menu=make_notes_menu, shortcut='n',
|
||||
predicate=lambda: version.has_qwerty and settings.get("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()),
|
||||
MenuItem('Seed Vault', menu=make_seed_vault_menu, shortcut='v',
|
||||
predicate=lambda: settings.master_get('seedvault') and has_secrets()),
|
||||
@ -507,72 +457,3 @@ FactoryMenu = [
|
||||
MenuItem("Debug Functions", menu=DebugFunctionsMenu, shortcut='f'),
|
||||
MenuItem("Perform Selftest", f=start_selftest, shortcut='s'),
|
||||
]
|
||||
|
||||
# Special menus for hobbled mode where we have a (single signer) spending policy in effect.
|
||||
# - no access to secrets, backups, firmware up/downgrades.
|
||||
# - secure notes, but readonly; can be disabled completely.
|
||||
# - key teleport, but only for PSBT & multisig purposes.
|
||||
# - can only be enabled after we have secrets, so no need for has_secrets tests here
|
||||
#
|
||||
|
||||
# Slightly limited file menu when hobbled.
|
||||
# - no backup/restore
|
||||
HobbledFileMgmtMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem('Sign Text File', f=sign_message_on_sd),
|
||||
MenuItem('Batch Sign PSBT', f=batch_sign),
|
||||
MenuItem('List Files', f=list_files),
|
||||
MenuItem('Export Wallet', menu=WalletExportMenu), # dup under Adv/Tools
|
||||
MenuItem('Verify Sig File', f=verify_sig_file),
|
||||
MenuItem('NFC File Share', predicate=nfc_enabled, f=nfc_share_file, shortcut=KEY_NFC),
|
||||
MenuItem('BBQr File Share', predicate=version.has_qr, f=qr_share_file, arg=True),
|
||||
MenuItem('QR File Share', predicate=version.has_qr, f=qr_share_file, shortcut=KEY_QR),
|
||||
MenuItem('Format SD Card', f=wipe_sd_card),
|
||||
MenuItem('Format RAM Disk', predicate=vdisk_enabled, f=wipe_vdisk),
|
||||
]
|
||||
|
||||
# NFC tools when hobbled: not much different.
|
||||
HobbledNFCToolsMenu = [
|
||||
MenuItem('Sign PSBT', f=nfc_sign_psbt),
|
||||
MenuItem('Show Address', f=nfc_show_address),
|
||||
MenuItem('Sign Message', f=nfc_sign_msg),
|
||||
MenuItem('Verify Sig File', f=nfc_sign_verify),
|
||||
MenuItem('Verify Address', f=nfc_address_verify),
|
||||
MenuItem('File Share', f=nfc_share_file),
|
||||
MenuItem('Push Transaction', f=nfc_pushtx_file, predicate=has_pushtx_url),
|
||||
]
|
||||
|
||||
# Very limited advanced menu when hobbled.
|
||||
HobbledAdvancedMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem("File Management", menu=HobbledFileMgmtMenu),
|
||||
MenuItem('Export Wallet', menu=WalletExportMenu, shortcut='x'), # also inside FileMgmt
|
||||
MenuItem('Teleport Multisig PSBT', predicate=qr_and_ms, f=kt_send_file_psbt),
|
||||
MenuItem("View Identity", f=view_ident),
|
||||
MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu, predicate=sssp_related_keys),
|
||||
MenuItem('Paper Wallets', f=make_paper_wallet),
|
||||
MenuItem('NFC Tools', predicate=nfc_enabled, menu=HobbledNFCToolsMenu, shortcut=KEY_NFC),
|
||||
MenuItem('WIF Store', menu=WIFStoreMenu.make, predicate=sssp_related_keys),
|
||||
MenuItem('Show %s Version' % ("Firmware" if version.has_qwerty else "FW"), f=show_version),
|
||||
MenuItem("Destroy Seed", f=clear_seed, predicate=has_real_secret),
|
||||
]
|
||||
|
||||
# Main menu when a spending policy (hobbled) is in effect.
|
||||
HobbledTopMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem('Ready To Sign', f=ready2sign, shortcut='r'),
|
||||
MenuItem('Passphrase', menu=start_b39_pw, predicate=sssp_allow_passphrase, shortcut='p'),
|
||||
MenuItem('Scan Any QR Code', predicate=version.has_qr, f=scan_any_qr, arg=(False, True),
|
||||
shortcut=KEY_QR),
|
||||
MenuItem("Address Explorer", menu=address_explore, shortcut='x'),
|
||||
MenuItem('Secure Notes & Passwords', menu=make_notes_menu, predicate=sssp_allow_notes,
|
||||
shortcut='n'),
|
||||
MenuItem('Type Passwords', f=password_entry, shortcut='t',
|
||||
predicate=lambda: settings.get("emu", False) and sssp_related_keys()),
|
||||
MenuItem('Seed Vault', menu=make_seed_vault_menu, predicate=sssp_allow_vault,
|
||||
shortcut='v'),
|
||||
MenuItem('Advanced/Tools', menu=HobbledAdvancedMenu, shortcut='t'),
|
||||
MenuItem('Secure Logout', f=logout_now, predicate=not version.has_battery),
|
||||
MenuItem('EXIT TEST DRIVE', f=sssp_feature_menu, predicate=is_hobble_testdrive),
|
||||
ShortcutItem(KEY_NFC, predicate=nfc_enabled, menu=HobbledNFCToolsMenu),
|
||||
]
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
#
|
||||
import utime, struct
|
||||
import uasyncio as asyncio
|
||||
from utils import B2A
|
||||
from machine import Pin
|
||||
from ustruct import pack
|
||||
|
||||
|
||||
@ -4,8 +4,9 @@
|
||||
#
|
||||
# Unattended signing of transactions and messages, subject to a set of rules.
|
||||
#
|
||||
import ustruct, chains, sys, gc, uio, ujson, uos, utime, ckcc, ngu
|
||||
from utils import problem_file_line, cleanup_deriv_path, match_deriv_path, keypath_to_str
|
||||
import stash, ustruct, chains, sys, gc, uio, ujson, uos, utime, ckcc, ngu, version
|
||||
from sffile import SFFile
|
||||
from utils import problem_file_line, cleanup_deriv_path, match_deriv_path
|
||||
from utils import cleanup_payment_address
|
||||
from pincodes import AE_LONG_SECRET_LEN
|
||||
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'
|
||||
% plist(self.share_xpubs))
|
||||
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))
|
||||
if self.priv_over_ux:
|
||||
fd.write('- Status responses optimized for privacy.\n')
|
||||
@ -656,15 +657,6 @@ class HSMPolicy:
|
||||
assert not glob.hsm_active
|
||||
glob.hsm_active = self
|
||||
|
||||
# HSM is the locked-down operating mode: shut down peripherals
|
||||
# that enlarge the USB-stack interaction surface.
|
||||
# - VDisk: MSC bulk OUT and HID OUT share the STM32 OTG_FS RX FIFO;
|
||||
# under load this can wedge the HID OUT endpoint permanently
|
||||
if glob.VD is not None:
|
||||
glob.VD.shutdown()
|
||||
if glob.NFC is not None:
|
||||
glob.NFC.shutdown()
|
||||
|
||||
self.start_time = utime.ticks_ms()
|
||||
|
||||
if new_file:
|
||||
@ -882,6 +874,9 @@ class HSMPolicy:
|
||||
# do this super early so always cleared even if other issues
|
||||
local_ok = self.consume_local_code(psbt_sha)
|
||||
|
||||
if not self.rules:
|
||||
raise ValueError("no txn signing allowed")
|
||||
|
||||
# reject anything with warning, probably
|
||||
if psbt.warnings:
|
||||
if self.warnings_ok:
|
||||
@ -889,32 +884,6 @@ class HSMPolicy:
|
||||
else:
|
||||
raise ValueError("has %d warning(s)" % len(psbt.warnings))
|
||||
|
||||
if psbt.por322:
|
||||
if not self.msg_paths:
|
||||
raise ValueError("Message signing not permitted")
|
||||
|
||||
for inp in psbt.inputs:
|
||||
if not inp.required_key:
|
||||
continue
|
||||
|
||||
if inp.is_multisig:
|
||||
paths = [
|
||||
keypath_to_str(inp.subpaths[pk])
|
||||
for pk in inp.required_key
|
||||
if pk in inp.subpaths
|
||||
]
|
||||
else:
|
||||
paths = [keypath_to_str(inp.subpaths[inp.required_key])]
|
||||
|
||||
if not any(match_deriv_path(self.msg_paths, p) for p in paths):
|
||||
raise ValueError("Message signing not enabled for that path")
|
||||
|
||||
self.approve(log, "BIP-322 message signing allowed")
|
||||
return 'y'
|
||||
|
||||
if not self.rules:
|
||||
raise ValueError("no txn signing allowed")
|
||||
|
||||
# See who has entered creditials already (all must be valid).
|
||||
users = []
|
||||
for u, (token, counter) in auth.items():
|
||||
|
||||
@ -59,7 +59,7 @@ class ApproveHSMPolicy(UserAuthorizedAction):
|
||||
msg = '''Last chance. You are defining a new policy which \
|
||||
allows the Coldcard to sign specific transactions without any further user approval.\n\n\
|
||||
Policy hash:\n%s\n\n
|
||||
Press (%s) to save policy and enable HSM mode.''' % (self.policy.hash(), confirm_char)
|
||||
Press %s to save policy and enable HSM mode.''' % (self.policy.hash(), confirm_char)
|
||||
|
||||
ch = await ux_show_story(msg, title=self.title,
|
||||
escape='x'+confirm_char, strict_escape=True)
|
||||
|
||||
@ -58,8 +58,8 @@ class ImportantTask:
|
||||
else:
|
||||
# uncaught exception in an unnamed (and unimportant) task
|
||||
print("UNNAMED: " + context["message"])
|
||||
sys.print_exception(context["exception"]) # VERY USEFUL on sim
|
||||
#print("... future: %r" % context.get("future", '?'))
|
||||
# sys.print_exception(context["exception"])
|
||||
print("... future: %r" % context.get("future", '?'))
|
||||
|
||||
def start_task(self, name, awaitable):
|
||||
# start a critical task and watch for it to never die
|
||||
|
||||
@ -134,7 +134,7 @@ class FullKeyboard(NumpadBase):
|
||||
if self._history[kn] == NUM_SAMPLES:
|
||||
self.is_pressed[kn] = 1
|
||||
new_presses.add(kn)
|
||||
elif self._history[kn] == 0:
|
||||
elif self._history[i] == 0:
|
||||
self.is_pressed[kn] = 0
|
||||
self._history[kn] = 0
|
||||
|
||||
|
||||
@ -656,91 +656,8 @@ class Display:
|
||||
|
||||
return prev_x
|
||||
|
||||
@staticmethod
|
||||
def handle_qr_msg(msg, max_lines=False):
|
||||
if len(msg) <= CHARS_W:
|
||||
parts = [msg]
|
||||
elif ' ' not in msg and (len(msg) <= (CHARS_W * 2)):
|
||||
# fits in two lines, but has no spaces
|
||||
hh = len(msg) // 2
|
||||
parts = [msg[0:hh], msg[hh:]]
|
||||
else:
|
||||
if not max_lines:
|
||||
# do word wrap
|
||||
parts = list(word_wrap(msg, CHARS_W))
|
||||
else:
|
||||
# 2 lines max
|
||||
parts = [msg[:30] + "⋯", "⋯" + msg[-30:]]
|
||||
|
||||
return parts
|
||||
|
||||
def draw_qr_lines(self, lines, is_addr):
|
||||
y = CHARS_H - len(lines)
|
||||
prev_x = 0
|
||||
for line in lines:
|
||||
if not is_addr:
|
||||
self.text(None, y, line)
|
||||
else:
|
||||
prev_x = self._draw_addr(y, line, prev_x=prev_x)
|
||||
y += 1
|
||||
|
||||
def draw_qr_idx_hint(self, str_idx):
|
||||
lh = len(str_idx)
|
||||
assert lh <= 10
|
||||
if lh > 5:
|
||||
# needs 2 lines
|
||||
self.text(-1, 0, str_idx[:5])
|
||||
self.text(-1, 1, str_idx[5:])
|
||||
else:
|
||||
self.text(-1, 0, str_idx)
|
||||
|
||||
def draw_side_msg(self, msg, has_idx):
|
||||
right_sub = 2 if has_idx else 0
|
||||
start_right = right_msg = None
|
||||
if len(msg) <= CHARS_H:
|
||||
# we only need left side
|
||||
start_left = CHARS_H - len(msg)
|
||||
left_msg = msg
|
||||
else:
|
||||
split_msg = msg.split()
|
||||
if len(split_msg) == 1 or len(split_msg) > 2:
|
||||
return # not possible
|
||||
|
||||
left_msg, right_msg = split_msg
|
||||
if len(left_msg) > CHARS_H:
|
||||
return
|
||||
if len(right_msg) > (CHARS_H - right_sub):
|
||||
return
|
||||
|
||||
start_left = CHARS_H - len(left_msg)
|
||||
start_right = CHARS_H - len(right_msg)
|
||||
|
||||
for i, c in enumerate(left_msg, start=start_left):
|
||||
self.text(1, i, c)
|
||||
|
||||
if start_right:
|
||||
for i, c in enumerate(right_msg, start=start_right):
|
||||
self.text(-1, i, c)
|
||||
|
||||
def draw_qr_error(self, idx_hint, msg=None):
|
||||
x = 85
|
||||
y = 30
|
||||
w = 150
|
||||
self.clear()
|
||||
self.dis.fill_rect(x, y, w, w, COL_TEXT)
|
||||
self.dis.fill_rect(x + 1, y + 1, w - 2, w - 2) # Black
|
||||
self.text(12, 3, "QR too big")
|
||||
if msg:
|
||||
lines = self.handle_qr_msg(msg, max_lines=True)
|
||||
self.draw_qr_lines(lines, False)
|
||||
|
||||
if idx_hint:
|
||||
self.draw_qr_idx_hint(idx_hint)
|
||||
|
||||
self.show()
|
||||
|
||||
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert, partial_bar=None,
|
||||
is_addr=False, force_msg=False, side_msg=None):
|
||||
is_addr=False, force_msg=False, is_change=False):
|
||||
# Show a QR code on screen w/ some text under it
|
||||
# - invert not supported on Q1
|
||||
# - 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
|
||||
num_lines = 0
|
||||
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)
|
||||
else:
|
||||
num_lines = 0
|
||||
@ -829,13 +755,31 @@ class Display:
|
||||
|
||||
if num_lines:
|
||||
# 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:
|
||||
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:
|
||||
self.draw_side_msg(side_msg, idx_hint)
|
||||
if is_addr and is_change:
|
||||
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
|
||||
self.show(max_bright=True)
|
||||
|
||||
@ -181,22 +181,14 @@ class LoginUX:
|
||||
async def we_are_ewaste(self, num_fails):
|
||||
msg = '''After %d failed PIN attempts this Coldcard is locked forever. \
|
||||
By design, there is no way to reset or recover the secure element, and its contents \
|
||||
are now forever inaccessible.\n\n''' % num_fails
|
||||
are now forever inaccessible.
|
||||
|
||||
if has_qwerty:
|
||||
msg += 'Calculator mode starts now.'
|
||||
else:
|
||||
msg += 'Restore your seed words onto a new Coldcard.'
|
||||
Restore your seed words onto a new Coldcard.''' % num_fails
|
||||
|
||||
while 1:
|
||||
ch = await ux_show_story(msg, title='I Am Brick!', escape='6')
|
||||
if ch == '6': break
|
||||
|
||||
if has_qwerty:
|
||||
from calc import login_repl
|
||||
await login_repl()
|
||||
|
||||
|
||||
async def confirm_attempt(self, attempts_left, value):
|
||||
|
||||
ch = await ux_show_story('''You have %d attempts left before this Coldcard BRICKS \
|
||||
@ -278,7 +270,7 @@ suffix break point is correct.\n\n'''
|
||||
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
|
||||
self.is_setting = True
|
||||
|
||||
|
||||
@ -81,6 +81,7 @@ glob.settings = settings
|
||||
|
||||
async def more_setup():
|
||||
# Boot up code; splash screen is being shown
|
||||
|
||||
try:
|
||||
from files import CardSlot
|
||||
CardSlot.setup()
|
||||
@ -88,10 +89,6 @@ async def more_setup():
|
||||
# This "pa" object holds some state shared w/ bootloader about the PIN
|
||||
try:
|
||||
from pincodes import pa
|
||||
# check for bricked system early
|
||||
# bricked CC not going past this point
|
||||
await pa.enforce_brick()
|
||||
|
||||
pa.setup(b'') # just to see where we stand.
|
||||
is_blank = pa.is_blank()
|
||||
except RuntimeError as e:
|
||||
|
||||
@ -6,7 +6,6 @@ freeze_as_mpy('', [
|
||||
'address_explorer.py',
|
||||
'auth.py',
|
||||
'backups.py',
|
||||
'block_height.py',
|
||||
'callgate.py',
|
||||
'ccc.py',
|
||||
'chains.py',
|
||||
@ -14,6 +13,8 @@ freeze_as_mpy('', [
|
||||
'compat7z.py',
|
||||
'countdowns.py',
|
||||
'descriptor.py',
|
||||
'dev_helper.py',
|
||||
'display.py',
|
||||
'drv_entro.py',
|
||||
'exceptions.py',
|
||||
'export.py',
|
||||
@ -47,6 +48,7 @@ freeze_as_mpy('', [
|
||||
'selftest.py',
|
||||
'serializations.py',
|
||||
'sffile.py',
|
||||
'ssd1306.py',
|
||||
'stash.py',
|
||||
'tapsigner.py',
|
||||
'trick_pins.py',
|
||||
@ -57,7 +59,6 @@ freeze_as_mpy('', [
|
||||
'version.py',
|
||||
'wallet.py',
|
||||
'web2fa.py',
|
||||
'wif.py',
|
||||
'xor_seed.py'
|
||||
], opt=0)
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
# Mk4 only files; would not be needed on Mk3 or earlier.
|
||||
freeze_as_mpy('', [
|
||||
'display.py',
|
||||
'hsm.py',
|
||||
'hsm_ux.py',
|
||||
'mempad.py',
|
||||
|
||||
@ -119,7 +119,7 @@ class ShortcutItem(MenuItem):
|
||||
super().__init__('SHORTCUT', shortcut=key, **kws)
|
||||
|
||||
class NonDefaultMenuItem(MenuItem):
|
||||
# Show a checkmark if setting is defined and not the default
|
||||
# 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):
|
||||
super().__init__(label, **kws)
|
||||
self.nvkey = nvkey
|
||||
@ -290,7 +290,7 @@ class MenuSystem:
|
||||
dis.clear()
|
||||
|
||||
cursor_y = None
|
||||
for n in range(PER_M+1):
|
||||
for n in range(self.ypos+PER_M+1):
|
||||
real_idx = n+self.ypos
|
||||
if real_idx >= self.count: break
|
||||
|
||||
@ -306,6 +306,10 @@ class MenuSystem:
|
||||
if fcn and fcn():
|
||||
checked = True
|
||||
|
||||
if not has_qwerty and checked and (len(msg) > 14):
|
||||
# on mk4 every label longer than 14 will overlap with checkmark
|
||||
checked = False
|
||||
|
||||
if self.multi_selected is not None and (real_idx in self.multi_selected):
|
||||
# ignore length constraint above, we need to visually show that
|
||||
# smthg is selected - in any case
|
||||
@ -331,8 +335,9 @@ class MenuSystem:
|
||||
if wrap: return True
|
||||
|
||||
# Do wrap-around (by request from NVK) if longer than the screen itself (on Q),
|
||||
# Mk4: same limit
|
||||
return self.count > 10
|
||||
# for mk4, limit is 16 which hits mostly the seed word menus.
|
||||
limit = 10 if has_qwerty else 16
|
||||
return self.count > limit
|
||||
|
||||
def down(self):
|
||||
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.
|
||||
#
|
||||
# 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
|
||||
@ -11,8 +11,7 @@ def make_flash_fs():
|
||||
os.VfsLfs2.mkfs(fl)
|
||||
|
||||
os.mount(fl, '/flash')
|
||||
os.chdir('/flash')
|
||||
os.mkdir('settings')
|
||||
os.mkdir('/flash/settings')
|
||||
|
||||
def make_psram_fs():
|
||||
# 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 ux import (ux_show_story, OK, ux_enter_bip32_index, ux_input_text, the_ux,
|
||||
import_export_prompt, ux_aborted)
|
||||
from utils import problem_file_line, to_ascii_printable, show_single_address, node_from_privkey
|
||||
from utils import problem_file_line, to_ascii_printable, show_single_address
|
||||
from files import CardSlot, CardMissingError, needs_microsd
|
||||
|
||||
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)
|
||||
chain_n = chains.current_chain().b44_cointype
|
||||
|
||||
acct = await ux_enter_bip32_index('Account Number:')
|
||||
if acct is None: return
|
||||
acct = await ux_enter_bip32_index('Account Number:') or 0
|
||||
|
||||
ch = await ux_show_story(title="Change?",
|
||||
msg="Press (0) to use internal/change address,"
|
||||
" %s to use external/receive address." % OK, escape="0")
|
||||
change = 1 if ch == '0' else 0
|
||||
|
||||
idx = await ux_enter_bip32_index('Index Number:')
|
||||
if idx is None: return
|
||||
idx = await ux_enter_bip32_index('Index Number:') or 0
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
# - messages must be short and ascii only. Our charset is limited
|
||||
# - too many spaces, leading/trailing can be an issue
|
||||
# MSG_MAX_SPACES = 4 # impt. compared to -=- positioning
|
||||
text = str(text, "ascii") # handle memoryview coming from USB
|
||||
result = to_ascii_printable(text, allow_tab_nl=allow_tab_nl)
|
||||
|
||||
result = to_ascii_printable(text, only_printable=only_printable)
|
||||
|
||||
length = len(result)
|
||||
assert length >= 2, "msg too short (min. 2)"
|
||||
@ -315,7 +313,6 @@ def parse_msg_sign_request(data):
|
||||
if text is None:
|
||||
raise AssertionError("MSG required")
|
||||
subpath = data_dict.get("subpath", subpath)
|
||||
assert isinstance(subpath, str), "subpath"
|
||||
addr_fmt = data_dict.get("addr_fmt", addr_fmt)
|
||||
is_json = True
|
||||
except ValueError:
|
||||
@ -334,13 +331,11 @@ def parse_msg_sign_request(data):
|
||||
addr_fmt = addr_fmt_from_subpath(subpath)
|
||||
|
||||
if not subpath:
|
||||
try:
|
||||
subpath = chains.STD_DERIVATIONS[addr_fmt]
|
||||
subpath = subpath.format(
|
||||
coin_type=chains.current_chain().b44_cointype,
|
||||
account=0, change=0, idx=0
|
||||
)
|
||||
except: pass
|
||||
subpath = chains.STD_DERIVATIONS[addr_fmt]
|
||||
subpath = subpath.format(
|
||||
coin_type=chains.current_chain().b44_cointype,
|
||||
account=0, change=0, idx=0
|
||||
)
|
||||
|
||||
return text, subpath, addr_fmt, is_json
|
||||
|
||||
@ -386,7 +381,7 @@ def sign_message_digest(digest, subpath, prompt, addr_fmt=AF_CLASSIC, pk=None):
|
||||
else:
|
||||
# if private key is provided, derivation subpath is ignored
|
||||
# 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)
|
||||
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
|
||||
subpath = await msg_sign_ux_get_subpath(af)
|
||||
if subpath is None: return
|
||||
|
||||
await approve_msg_sign(text, subpath, af, approved_cb=approved_cb,
|
||||
kill_menu=kill_menu, allow_tab_nl=True)
|
||||
kill_menu=kill_menu, only_printable=False)
|
||||
|
||||
# pick address format
|
||||
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 descriptor import MultisigDescriptor, multisig_descriptor_template
|
||||
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 exceptions import FatalPSBTIssue
|
||||
from glob import settings
|
||||
@ -245,11 +245,9 @@ class MultisigWallet(WalletABC):
|
||||
return rv
|
||||
|
||||
@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.
|
||||
# - 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', [])
|
||||
|
||||
for idx, rec in enumerate(lst):
|
||||
@ -257,19 +255,16 @@ class MultisigWallet(WalletABC):
|
||||
# ignore one by index
|
||||
continue
|
||||
|
||||
if name and (rec[0] != name):
|
||||
continue
|
||||
|
||||
if M or N:
|
||||
# peek at M/N
|
||||
has_m, has_n = tuple(rec[1])
|
||||
if M is not None and has_m != M: continue
|
||||
if N is not None and has_n != N: continue
|
||||
|
||||
if addr_fmts:
|
||||
if addr_fmt is not None:
|
||||
opts = rec[3]
|
||||
af = opts.get('ft', AF_P2SH)
|
||||
if af not in addr_fmts: continue
|
||||
if af != addr_fmt: continue
|
||||
|
||||
yield cls.deserialize(rec, idx)
|
||||
|
||||
@ -278,23 +273,28 @@ class MultisigWallet(WalletABC):
|
||||
return list(self.xfp_paths.values())
|
||||
|
||||
@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
|
||||
# - xfp_paths is list of lists: [xfp, *path] like in psbt files
|
||||
# - M and N must be known
|
||||
# - 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):
|
||||
return rv
|
||||
|
||||
return None
|
||||
|
||||
@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.
|
||||
# - 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 = []
|
||||
for rv in cls.iter_wallets():
|
||||
for rv in cls.iter_wallets(M=M, addr_fmt=addr_fmt):
|
||||
if rv.matching_subpaths(xfp_paths):
|
||||
matches.append(rv)
|
||||
|
||||
@ -327,12 +327,11 @@ class MultisigWallet(WalletABC):
|
||||
|
||||
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
|
||||
# - xfp_paths must be sorted already
|
||||
assert (self.M, self.N) == (M, N), "M/N mismatch"
|
||||
assert len(xfp_paths) == N, "XFP count"
|
||||
assert self.addr_fmt == addr_fmt, "addr fmt"
|
||||
if self.disable_checks: return
|
||||
assert self.matching_subpaths(xfp_paths), "wrong XFP/derivs"
|
||||
|
||||
@ -409,7 +408,7 @@ class MultisigWallet(WalletABC):
|
||||
# - count_similar: same N, same 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:
|
||||
# All details are same: M/N, paths, addr fmt
|
||||
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
|
||||
# already imported and vice-versa
|
||||
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:
|
||||
return None, [], 1
|
||||
else:
|
||||
@ -460,7 +454,7 @@ class MultisigWallet(WalletABC):
|
||||
assert self.storage_idx >= 0
|
||||
|
||||
# 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
|
||||
break
|
||||
else:
|
||||
@ -588,7 +582,7 @@ class MultisigWallet(WalletABC):
|
||||
found_pk = node.pubkey()
|
||||
|
||||
# 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.
|
||||
here = '[%s]' % xfp2str(xfp)
|
||||
if dp != len(path):
|
||||
@ -870,8 +864,7 @@ class MultisigWallet(WalletABC):
|
||||
|
||||
def make_fname(self, prefix, suffix='txt'):
|
||||
rv = '%s-%s.%s' % (prefix, self.name, suffix)
|
||||
rv = rv.replace(' ', '_')
|
||||
return rv.replace('/', '-')
|
||||
return rv.replace(' ', '_')
|
||||
|
||||
async def export_electrum(self):
|
||||
# Generate and save an Electrum JSON file.
|
||||
@ -975,7 +968,34 @@ class MultisigWallet(WalletABC):
|
||||
print('%s: %s' % (xfp2str(xfp), val), file=fp)
|
||||
|
||||
@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
|
||||
# the details, and/or bypass that all and just trust the data.
|
||||
# - xpubs_list is a list of (xfp+path, binary BIP-32 xpub)
|
||||
@ -1004,13 +1024,14 @@ class MultisigWallet(WalletABC):
|
||||
expect_chain, my_xfp, xpubs)
|
||||
if is_mine:
|
||||
has_mine += 1
|
||||
addr_fmt = cls.guess_addr_fmt(path)
|
||||
|
||||
assert has_mine == 1 # 'my key not included'
|
||||
|
||||
name = 'PSBT-%d-of-%d' % (M, N)
|
||||
# this will always create sortedmulti multisig (BIP-67)
|
||||
# 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
|
||||
# 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?'
|
||||
elif num_dups and isinstance(diff_items, list):
|
||||
# failures only
|
||||
story = "Duplicate wallet. "
|
||||
story = "Duplicate wallet."
|
||||
if diff_items:
|
||||
story += diff_items[0]
|
||||
else:
|
||||
story += 'All details are the same as existing!'
|
||||
story += ' All details are the same as existing!'
|
||||
is_dup = True
|
||||
elif diff_items:
|
||||
# Concern here is overwrite when similar, but we don't overwrite anymore, so
|
||||
@ -1386,7 +1407,7 @@ class MultisigMenu(MenuSystem):
|
||||
@classmethod
|
||||
def construct(cls):
|
||||
# Dynamic menu with user-defined names of wallets shown
|
||||
from flow import nfc_enabled
|
||||
from glob import NFC
|
||||
|
||||
if not MultisigWallet.exists():
|
||||
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),
|
||||
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('Create Airgapped', f=create_ms_step1))
|
||||
rv.append(MenuItem('Trust PSBT?', f=trust_psbt_menu))
|
||||
@ -1410,9 +1435,6 @@ class MultisigMenu(MenuSystem):
|
||||
rv.append(NonDefaultMenuItem(
|
||||
'Unsorted Multisig?' if version.has_qwerty else 'Unsorted Multi?',
|
||||
'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
|
||||
|
||||
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' % (
|
||||
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
|
||||
|
||||
await ms.export_electrum()
|
||||
@ -1571,8 +1593,7 @@ P2WSH:
|
||||
if ch != "y":
|
||||
return
|
||||
|
||||
acct = await ux_enter_bip32_index('Account Number:')
|
||||
if acct is None: return
|
||||
acct = await ux_enter_bip32_index('Account Number:') or 0
|
||||
|
||||
def render(acct_num):
|
||||
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
|
||||
# Always include 2 keys from CCC: own master (key A) and key C
|
||||
# - force them to same derivation.
|
||||
acct = await ux_enter_bip32_index('CCC Account Number:')
|
||||
if acct is None: return
|
||||
acct = await ux_enter_bip32_index('CCC Account Number:') or 0
|
||||
|
||||
dis.fullscreen("Wait...")
|
||||
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 ?",
|
||||
title="[%s]" % xfp2str(my_xfp))
|
||||
if ch == "y":
|
||||
acct = await ux_enter_bip32_index('Account Number:')
|
||||
if acct is None: return
|
||||
acct = await ux_enter_bip32_index('Account Number:') or 0
|
||||
dis.fullscreen("Wait...")
|
||||
xpubs.append(add_own_xpub(chain, acct, addr_fmt))
|
||||
num_mine += 1
|
||||
@ -1827,8 +1846,10 @@ async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH, is_qr=False,
|
||||
M = 2
|
||||
else:
|
||||
# pick useful M value to start
|
||||
M = await ux_enter_number("How many need to sign?(M)", N)
|
||||
if M is None: return
|
||||
M = await ux_enter_number("How many need to sign?(M)", N, can_cancel=True)
|
||||
if not M:
|
||||
await ux_dramatic_pause('Aborted.', 2)
|
||||
return # user cancel
|
||||
|
||||
dis.fullscreen("Wait...")
|
||||
|
||||
@ -1919,19 +1940,26 @@ async def import_multisig_qr(*a):
|
||||
except Exception as e:
|
||||
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
|
||||
|
||||
|
||||
async def import_multisig(*a):
|
||||
# pick text file from SD card, import as multisig setup file
|
||||
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)
|
||||
if isinstance(ch, str):
|
||||
if ch == KEY_QR:
|
||||
await import_multisig_qr()
|
||||
elif ch == KEY_NFC:
|
||||
await import_multisig_nfc()
|
||||
return
|
||||
force_vdisk = False
|
||||
if VD:
|
||||
prompt = "Press (1) to import multisig wallet file from SD Card"
|
||||
escape = "1"
|
||||
if VD is not None:
|
||||
prompt += ", press (2) to import from Virtual Disk"
|
||||
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):
|
||||
with open(filename, 'rt') as fd:
|
||||
@ -1943,11 +1971,11 @@ async def import_multisig(*a):
|
||||
return True
|
||||
|
||||
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
|
||||
|
||||
try:
|
||||
with CardSlot(**ch) as card:
|
||||
with CardSlot(force_vdisk=force_vdisk) as card:
|
||||
with open(fn, 'rt') as fp:
|
||||
data = fp.read()
|
||||
except CardMissingError:
|
||||
@ -1958,8 +1986,8 @@ async def import_multisig(*a):
|
||||
try:
|
||||
possible_name = (fn.split('/')[-1].split('.'))[0]
|
||||
maybe_enroll_xpub(config=data, name=possible_name)
|
||||
except BaseException as 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)))
|
||||
except Exception as e:
|
||||
#import sys; sys.print_exception(e)
|
||||
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
|
||||
|
||||
# EOF
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
#
|
||||
# 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
|
||||
#
|
||||
from struct import pack, unpack
|
||||
|
||||
@ -107,14 +107,13 @@ class NFCHandler:
|
||||
from glob import dis
|
||||
here = bytes(256)
|
||||
end = 8196
|
||||
for pos in range(0, end, 256):
|
||||
for pos in range(0, end, 256) :
|
||||
self.i2c.writeto_mem(I2C_ADDR_USER, pos, here, addrsize=16)
|
||||
if (pos == 256) and not full_wipe: break
|
||||
if pos == 256 and not full_wipe: break
|
||||
|
||||
# 6ms per 16 byte row, worst case, so ~100ms here per iter! 3.2seconds total
|
||||
if full_wipe:
|
||||
dis.progress_bar_show(pos / end)
|
||||
|
||||
await self.wait_ready()
|
||||
|
||||
# system config area (flash cells, but affect operation): table 12
|
||||
@ -226,15 +225,9 @@ class NFCHandler:
|
||||
self.set_rf_disable(1)
|
||||
|
||||
async def share_loop(self, n, **kws):
|
||||
# Keep one fully-written tag image live until the user exits. Some
|
||||
# phones perform multiple probes while deciding if a tag is NDEF.
|
||||
await self.big_write(n.bytes())
|
||||
|
||||
while 1:
|
||||
aborted = await self.ux_animation(exit_after_activity=False, **kws)
|
||||
if aborted:
|
||||
await self.wipe(kws.get("is_secret", False))
|
||||
break
|
||||
done = await self.share_start(n, **kws)
|
||||
if done: break
|
||||
|
||||
async def share_signed_txn(self, txid, file_offset, txn_len, txn_sha):
|
||||
# we just signed something, share it over NFC
|
||||
@ -404,13 +397,12 @@ class NFCHandler:
|
||||
self.write_dyn(GPO_CTRL_Dyn, 0x01) # GPO_EN
|
||||
self.read_dyn(IT_STS_Dyn) # clear interrupt
|
||||
|
||||
async def ux_animation(self, allow_enter=True, prompt=None, line2=None,
|
||||
is_secret=False, exit_after_activity=True,
|
||||
min_delay=1000):
|
||||
async def ux_animation(self, write_mode, allow_enter=True, prompt=None, line2=None,
|
||||
is_secret=False):
|
||||
# Run the pretty animation, and detect both when we are written, and/or key to exit/abort.
|
||||
# - similar when "read" and then removed from field
|
||||
# - return T if aborted by user
|
||||
from glob import dis
|
||||
from glob import dis, numpad
|
||||
|
||||
await self.wait_ready()
|
||||
self.set_rf_disable(0)
|
||||
@ -423,8 +415,7 @@ class NFCHandler:
|
||||
dis.text(None, -3, line2)
|
||||
else:
|
||||
from graphics_mk4 import Graphics
|
||||
from version import mk_num
|
||||
frames = [getattr(Graphics, 'mk%d_nfc_%d'%(mk_num, i)) for i in range(1, 5)]
|
||||
frames = [getattr(Graphics, 'mk4_nfc_%d'%i) for i in range(1, 5)]
|
||||
|
||||
aborted = True
|
||||
phase = -1
|
||||
@ -432,6 +423,7 @@ class NFCHandler:
|
||||
|
||||
# (ms) How long to wait after RF field comes and goes
|
||||
# - user can press OK during this period if they know they are done
|
||||
min_delay = (3000 if write_mode else 1000)
|
||||
|
||||
while 1:
|
||||
if dis.has_lcd:
|
||||
@ -470,7 +462,7 @@ class NFCHandler:
|
||||
aborted = False
|
||||
break
|
||||
|
||||
if exit_after_activity and last_activity:
|
||||
if last_activity:
|
||||
dt = utime.ticks_diff(utime.ticks_ms(), last_activity)
|
||||
if dt >= min_delay:
|
||||
# They acheived some RF activity and then nothing for some time, so
|
||||
@ -479,6 +471,9 @@ class NFCHandler:
|
||||
break
|
||||
|
||||
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
|
||||
|
||||
@ -486,15 +481,17 @@ class NFCHandler:
|
||||
# do the UX while we are sharing a value over NFC
|
||||
# - assumpting is people know what they are scanning
|
||||
# - x key to abort early, but also self-clears
|
||||
|
||||
await self.big_write(ndef_obj.bytes())
|
||||
return await self.ux_animation(**kws)
|
||||
|
||||
return await self.ux_animation(False, **kws)
|
||||
|
||||
async def start_nfc_rx(self, **kws):
|
||||
# Pretend to be a big warm empty tag ready to be stuffed with data
|
||||
await self.big_write(ndef.CC_WR_FILE)
|
||||
|
||||
# wait until something is written
|
||||
aborted = await self.ux_animation(min_delay=3000, **kws)
|
||||
aborted = await self.ux_animation(True, **kws)
|
||||
if aborted: return
|
||||
|
||||
# read CCFILE area (header)
|
||||
@ -621,7 +618,7 @@ class NFCHandler:
|
||||
# it's a txn, and we wrote as hex
|
||||
data = a2b_hex(data)
|
||||
else:
|
||||
assert data[1:4] == bytes(3)
|
||||
assert data[2:8] == bytes(6)
|
||||
sha = ngu.hash.sha256s(data)
|
||||
await self.share_signed_txn(txid, data, len(data), sha)
|
||||
elif ext == 'psbt':
|
||||
@ -742,7 +739,7 @@ class NFCHandler:
|
||||
m = m.decode()
|
||||
what, vals = decode_bip21_text(m)
|
||||
if what == 'addr':
|
||||
return vals
|
||||
return vals[1]
|
||||
|
||||
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):
|
||||
# Get an address or complete bip-21 url even and search it... slow.
|
||||
res = await self.read_address()
|
||||
if not res: return
|
||||
_, addr, args = res
|
||||
from ownership import OWNERSHIP
|
||||
await OWNERSHIP.search_ux(addr, args)
|
||||
winner = await self.read_address()
|
||||
if winner:
|
||||
from ownership import OWNERSHIP
|
||||
await OWNERSHIP.search_ux(winner)
|
||||
|
||||
async def read_extended_private_key(self):
|
||||
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
|
||||
return await self._nfc_reader(f, 'Unable to find base64 encoded TAPSIGNER backup.')
|
||||
|
||||
async def read_bip322_msg(self):
|
||||
f = lambda x: x.decode()
|
||||
return await self._nfc_reader(f, 'Unable to find BIP-322 message.')
|
||||
|
||||
async def read_wif(self):
|
||||
# only compressed WIFs allowed
|
||||
f = lambda x: x.decode() if len(x) >= 51 else None
|
||||
return await self._nfc_reader(f, 'Unable to find WIF key(s).')
|
||||
|
||||
async def _nfc_reader(self, func, fail_msg):
|
||||
data = await self.start_nfc_rx()
|
||||
if not data: return
|
||||
|
||||
winner = None
|
||||
try:
|
||||
for urn, msg, meta in ndef.record_parser(data):
|
||||
msg = bytes(msg)
|
||||
try:
|
||||
r = func(msg)
|
||||
if r is not None:
|
||||
winner = r
|
||||
break
|
||||
except:
|
||||
pass
|
||||
except Exception: pass # dont crash when given garbage
|
||||
for urn, msg, meta in ndef.record_parser(data):
|
||||
msg = bytes(msg)
|
||||
try:
|
||||
r = func(msg)
|
||||
if r is not None:
|
||||
winner = r
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
if not winner:
|
||||
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 glob import settings, dis
|
||||
from files import CardMissingError, needs_microsd, CardSlot
|
||||
from public_constants import MSG_SIGNING_MAX_LENGTH
|
||||
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
|
||||
from charcodes import KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6
|
||||
from lcd_display import CHARS_W
|
||||
from utils import problem_file_line, url_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
|
||||
# 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
|
||||
|
||||
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):
|
||||
# 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):
|
||||
|
||||
readonly = False
|
||||
|
||||
@classmethod
|
||||
def construct(cls):
|
||||
# Dynamic menu with user-defined names of notes shown
|
||||
@ -131,7 +119,9 @@ class NotesMenu(MenuSystem):
|
||||
else:
|
||||
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)
|
||||
|
||||
@ -144,39 +134,6 @@ class NotesMenu(MenuSystem):
|
||||
|
||||
return rv
|
||||
|
||||
@classmethod
|
||||
def construct_readonly(cls):
|
||||
# When only allowed to view, no export/add new/delete.
|
||||
wipe_if_deltamode()
|
||||
|
||||
rv = cls.construct_note_items(readonly=True)
|
||||
|
||||
if not rv:
|
||||
rv.append(MenuItem('(none saved yet)'))
|
||||
|
||||
return rv
|
||||
|
||||
@classmethod
|
||||
def construct_note_items(cls, readonly=False):
|
||||
rv = []
|
||||
by_group = {}
|
||||
|
||||
for note in NoteContent.get_all():
|
||||
item = MenuItem('%d: %s' % (note.idx+1, note.title),
|
||||
menu=note.make_menu, arg=readonly)
|
||||
group = note.group
|
||||
if group:
|
||||
if group not in by_group:
|
||||
by_group[group] = []
|
||||
by_group[group].append(item)
|
||||
else:
|
||||
rv.append(item)
|
||||
|
||||
for group in sorted(by_group):
|
||||
rv.append(MenuItem('↳ ' + group, menu=NoteGroupMenu(group, readonly)))
|
||||
|
||||
return rv
|
||||
|
||||
@classmethod
|
||||
async def export_all(cls, *a):
|
||||
await start_export(NoteContent.get_all())
|
||||
@ -228,7 +185,7 @@ class NotesMenu(MenuSystem):
|
||||
@classmethod
|
||||
async def disable_notes(cls, *a):
|
||||
# they don't want feature anymore; already checked no notes in effect
|
||||
# - no need for confirm, they aren't losing anything
|
||||
# - no need for confirm, they aren't loosing anything
|
||||
settings.remove_key('secnap')
|
||||
settings.remove_key('notes')
|
||||
settings.save()
|
||||
@ -247,27 +204,9 @@ class NotesMenu(MenuSystem):
|
||||
@classmethod
|
||||
async def drill_to(cls, menu, item):
|
||||
# make it so looks like we drilled down into the new note
|
||||
label = '%d: %s' % (item.idx+1, item.title)
|
||||
group = item.group
|
||||
if group:
|
||||
cls.goto_exact_label(menu, '↳ ' + group)
|
||||
gm = NoteGroupMenu(group)
|
||||
cls.goto_exact_label(gm, label)
|
||||
the_ux.push(gm)
|
||||
else:
|
||||
cls.goto_exact_label(menu, label)
|
||||
|
||||
m = await item._make_menu()
|
||||
the_ux.push(MenuSystem(m))
|
||||
|
||||
@staticmethod
|
||||
def goto_exact_label(menu, label):
|
||||
for i, mi in enumerate(menu.items):
|
||||
if mi.label == label:
|
||||
menu.goto_idx(i)
|
||||
return True
|
||||
|
||||
return False
|
||||
menu.goto_idx(item.idx)
|
||||
m = MenuSystem(await item.make_menu())
|
||||
the_ux.push(m)
|
||||
|
||||
|
||||
class NoteContentBase:
|
||||
@ -284,15 +223,9 @@ class NoteContentBase:
|
||||
return PasswordContent(j, idx) if 'user' in j else NoteContent(j, idx)
|
||||
|
||||
def serialize(self):
|
||||
res = {}
|
||||
for fld in self.flds:
|
||||
val = getattr(self, fld, '')
|
||||
# user field is necessary for proper password identification in constructor
|
||||
if not val and (fld != "user"):
|
||||
continue
|
||||
res[fld] = val
|
||||
return {fld:getattr(self, fld, '') for fld in self.flds}
|
||||
|
||||
return res
|
||||
to_json = serialize
|
||||
|
||||
@classmethod
|
||||
def get_all(cls):
|
||||
@ -318,15 +251,6 @@ class NoteContentBase:
|
||||
settings.put('notes', [n.serialize() for n in notes])
|
||||
settings.save()
|
||||
|
||||
@classmethod
|
||||
def get_groups(cls):
|
||||
groups = set()
|
||||
for note in cls.get_all():
|
||||
if note.group:
|
||||
groups.add(note.group)
|
||||
|
||||
return sorted(groups)
|
||||
|
||||
async def delete(self, *a):
|
||||
# Remove note
|
||||
ok = await ux_confirm("Everything about this note/password will be lost.")
|
||||
@ -347,11 +271,6 @@ class NoteContentBase:
|
||||
the_ux.pop()
|
||||
m = the_ux.top_of_stack()
|
||||
m.update_contents()
|
||||
parent = the_ux.parent_of(m)
|
||||
if parent:
|
||||
parent.update_contents()
|
||||
if isinstance(m, NoteGroupMenu) and not m.has_notes():
|
||||
the_ux.pop()
|
||||
|
||||
await ux_dramatic_pause('Deleted.', 3)
|
||||
|
||||
@ -383,17 +302,11 @@ class NoteContentBase:
|
||||
|
||||
if not is_new:
|
||||
# change our own menu contents
|
||||
mi = await self._make_menu()
|
||||
menu.replace_items(mi)
|
||||
menu.replace_items(await self.make_menu())
|
||||
|
||||
# update parent
|
||||
parent = the_ux.parent_of(menu)
|
||||
parent.update_contents()
|
||||
grandparent = the_ux.parent_of(parent)
|
||||
if grandparent:
|
||||
grandparent.update_contents()
|
||||
if isinstance(parent, NoteGroupMenu) and not parent.has_notes():
|
||||
the_ux.stack.remove(parent)
|
||||
else:
|
||||
menu.update_contents()
|
||||
|
||||
@ -423,132 +336,33 @@ class NoteContentBase:
|
||||
await ux_sign_msg(txt, approved_cb=msg_signing_done, kill_menu=False)
|
||||
|
||||
def sign_misc_menu_item(self):
|
||||
return MenuItem("Sign Note Text", f=self.sign_txt_msg, arg=self.misc,
|
||||
predicate=2 <= len(self.misc) <= MSG_SIGNING_MAX_LENGTH)
|
||||
|
||||
@staticmethod
|
||||
def is_b39pass_applicable(data, read_only):
|
||||
from seed import MAX_PASS_LEN
|
||||
from ccc import sssp_spending_policy
|
||||
if read_only and not sssp_spending_policy('okeys'):
|
||||
return False
|
||||
return (len(data) <= MAX_PASS_LEN) and is_printable(data) and settings.get("words", True)
|
||||
|
||||
async def apply_as_b39_pass(self, a, b, item):
|
||||
data, readonly = item.arg
|
||||
# rstrip just trailing whitespaces/tabs/newlines
|
||||
data = data.rstrip()
|
||||
# do not allow any more tabs/newlines
|
||||
assert self.is_b39pass_applicable(data, readonly)
|
||||
from seed import apply_pass_value
|
||||
await apply_pass_value(data)
|
||||
|
||||
|
||||
class NoteGroupMenu(MenuSystem):
|
||||
def __init__(self, group, readonly=False):
|
||||
self.group = group
|
||||
self.readonly = readonly
|
||||
super().__init__(self.construct())
|
||||
|
||||
def construct(self):
|
||||
items = []
|
||||
for note in NoteContent.get_all():
|
||||
if note.group == self.group:
|
||||
items.append(MenuItem('%d: %s' % (note.idx+1, note.title),
|
||||
menu=note.make_menu, arg=self.readonly))
|
||||
|
||||
return items or [MenuItem('(none)')]
|
||||
|
||||
def has_notes(self):
|
||||
return any(note.group == self.group for note in NoteContent.get_all())
|
||||
|
||||
def update_contents(self):
|
||||
self.replace_items(self.construct())
|
||||
|
||||
|
||||
class GroupPickerMenu(MenuSystem):
|
||||
def __init__(self, current=''):
|
||||
self.result = None
|
||||
self.current = current
|
||||
|
||||
groups = NoteContentBase.get_groups()
|
||||
chosen = 0
|
||||
items = [MenuItem('(none)', f=self.picked, arg='')]
|
||||
|
||||
for group in groups:
|
||||
if group == self.current:
|
||||
chosen = len(items)
|
||||
items.append(MenuItem(group, f=self.picked, arg=group))
|
||||
|
||||
items.append(MenuItem('New Group', f=self.new_group))
|
||||
|
||||
super().__init__(items, chosen=chosen)
|
||||
|
||||
async def picked(self, menu, idx, mi):
|
||||
assert menu == self
|
||||
self.result = mi.arg
|
||||
the_ux.pop()
|
||||
|
||||
async def new_group(self, menu, idx, mi):
|
||||
group = await ux_input_text('', max_len=ONE_LINE, confirm_exit=False,
|
||||
prompt='Group', placeholder='(optional)')
|
||||
if group is None:
|
||||
self.result = None
|
||||
else:
|
||||
self.result = group
|
||||
|
||||
the_ux.pop()
|
||||
|
||||
@classmethod
|
||||
async def pick(cls, current=''):
|
||||
m = cls(current)
|
||||
the_ux.push(m)
|
||||
await m.interact()
|
||||
|
||||
return current if m.result is None else m.result
|
||||
return MenuItem("Sign Note Text", f=self.sign_txt_msg, arg=self.misc)
|
||||
|
||||
|
||||
class PasswordContent(NoteContentBase):
|
||||
# "Passwords" have a few more fields and are more structured
|
||||
flds = ['title', 'user', 'password', 'site', 'misc', 'group']
|
||||
flds = ['title', 'user', 'password', 'site', 'misc' ]
|
||||
type_label = 'password'
|
||||
|
||||
async def _make_menu(self, readonly=False):
|
||||
async def make_menu(self, *a):
|
||||
rv = [MenuItem('"%s"' % self.title, f=self.view)]
|
||||
if self.user:
|
||||
rv.append(MenuItem('↳ %s' % self.user, f=self.view))
|
||||
if self.site:
|
||||
rv.append(MenuItem('↳ %s' % self.site, f=self.view))
|
||||
# if self.misc: rv.append(MenuItem('↳ (notes)', f=self.view))
|
||||
rv += [
|
||||
#if self.misc: rv.append(MenuItem('↳ (notes)', f=self.view))
|
||||
return rv + [
|
||||
MenuItem('View Password', f=self.view_pw),
|
||||
MenuItem('Send Password', f=self.send_pw, predicate=lambda: not settings.get('du', 0)),
|
||||
]
|
||||
if not readonly:
|
||||
rv += [
|
||||
MenuItem('Export', f=self.export),
|
||||
MenuItem('Edit Metadata', f=self.edit),
|
||||
MenuItem('Delete', f=self.delete),
|
||||
MenuItem('Change Password', f=self.change_pw),
|
||||
]
|
||||
rv += [
|
||||
MenuItem('Send Password', f=self.send_pw, predicate=lambda: settings.get('du', True)),
|
||||
MenuItem('Export', f=self.export),
|
||||
MenuItem('Edit Metadata', f=self.edit),
|
||||
MenuItem('Delete', f=self.delete),
|
||||
MenuItem('Change Password', f=self.change_pw),
|
||||
self.sign_misc_menu_item(),
|
||||
ShortcutItem(KEY_QR, f=self.view_qr_menu, arg=self.type_label),
|
||||
ShortcutItem(KEY_NFC, f=self.share_nfc, arg=self.type_label),
|
||||
]
|
||||
|
||||
# if password is less than MAX_PASS_LEN and only consist of printable ASCII characters
|
||||
# and current seed (master or tmp) is word based - offer to apply pwd text as BIP-39 passphrase
|
||||
if self.is_b39pass_applicable(self.password, readonly):
|
||||
rv += [MenuItem('Apply as BIP-39 Passphrase',
|
||||
f=self.apply_as_b39_pass, arg=(self.password, readonly))]
|
||||
|
||||
return rv
|
||||
|
||||
async def make_menu(self, a, b, item):
|
||||
items = await self._make_menu(readonly=item.arg)
|
||||
return MenuSystem(items)
|
||||
|
||||
async def view(self, *a):
|
||||
pl = len(self.password)
|
||||
m = ''
|
||||
@ -616,8 +430,7 @@ class PasswordContent(NoteContentBase):
|
||||
|
||||
if self.idx == -1:
|
||||
# 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) or ""
|
||||
self.password = await get_a_password(self.password)
|
||||
|
||||
site = await ux_input_text(self.site, max_len=ONE_LINE, scan_ok=True, confirm_exit=False,
|
||||
prompt='Website', placeholder='(optional)')
|
||||
@ -629,8 +442,6 @@ class PasswordContent(NoteContentBase):
|
||||
if misc is None:
|
||||
misc = self.misc
|
||||
|
||||
group = await GroupPickerMenu.pick(self.group)
|
||||
|
||||
if self.idx != -1:
|
||||
# confirm changes, don't for new records
|
||||
chgs = []
|
||||
@ -642,8 +453,6 @@ class PasswordContent(NoteContentBase):
|
||||
chgs.append('Username')
|
||||
if self.misc != misc:
|
||||
chgs.append('Other Notes')
|
||||
if self.group != group:
|
||||
chgs.append('Group')
|
||||
|
||||
if not chgs:
|
||||
await ux_dramatic_pause('No changes.', 3)
|
||||
@ -657,7 +466,6 @@ class PasswordContent(NoteContentBase):
|
||||
self.user = user
|
||||
self.site = site
|
||||
self.misc = misc
|
||||
self.group = group
|
||||
|
||||
await self._save_ux(menu)
|
||||
return self
|
||||
@ -665,41 +473,22 @@ class PasswordContent(NoteContentBase):
|
||||
|
||||
class NoteContent(NoteContentBase):
|
||||
# Pure "notes" have just a title and free-form text
|
||||
flds = ['title', 'misc', 'group']
|
||||
flds = ['title', 'misc']
|
||||
type_label = 'note'
|
||||
|
||||
async def _make_menu(self, readonly=False):
|
||||
async def make_menu(self, *a):
|
||||
# Details and actions for this Note
|
||||
|
||||
rv = [
|
||||
return [
|
||||
MenuItem('"%s"' % self.title, f=self.view),
|
||||
MenuItem('View Note', f=self.view),
|
||||
]
|
||||
if not readonly:
|
||||
rv += [
|
||||
MenuItem('Edit', f=self.edit),
|
||||
MenuItem('Delete', f=self.delete),
|
||||
MenuItem('Export', f=self.export),
|
||||
]
|
||||
|
||||
rv += [
|
||||
MenuItem('Edit', f=self.edit),
|
||||
MenuItem('Delete', f=self.delete),
|
||||
MenuItem('Export', f=self.export),
|
||||
self.sign_misc_menu_item(),
|
||||
ShortcutItem(KEY_QR, f=self.view_qr_menu, arg="misc"),
|
||||
ShortcutItem(KEY_NFC, f=self.share_nfc, arg='misc'),
|
||||
]
|
||||
|
||||
# if misc is less than MAX_PASS_LEN and only consist of printable ASCII characters
|
||||
# and current seed (master or tmp) is word based - offer to apply note text as BIP-39 passphrase
|
||||
if self.is_b39pass_applicable(self.misc, readonly):
|
||||
rv += [MenuItem('Apply as BIP-39 Passphrase',
|
||||
f=self.apply_as_b39_pass, arg=(self.misc, readonly))]
|
||||
|
||||
return rv
|
||||
|
||||
async def make_menu(self, a, b, item):
|
||||
items = await self._make_menu(readonly=item.arg)
|
||||
return MenuSystem(items)
|
||||
|
||||
async def view(self, *a):
|
||||
ch = await ux_show_story(self.misc, title=self.title, escape=KEY_QR,
|
||||
hint_icons=KEY_QR)
|
||||
@ -720,8 +509,6 @@ class NoteContent(NoteContentBase):
|
||||
if misc is None:
|
||||
misc = self.misc
|
||||
|
||||
group = await GroupPickerMenu.pick(self.group)
|
||||
|
||||
if self.idx != -1:
|
||||
# confirm changes, don't for new records
|
||||
chgs = []
|
||||
@ -729,8 +516,6 @@ class NoteContent(NoteContentBase):
|
||||
chgs.append('Title')
|
||||
if self.misc != misc:
|
||||
chgs.append('Note Text')
|
||||
if self.group != group:
|
||||
chgs.append('Group')
|
||||
|
||||
if not chgs:
|
||||
await ux_dramatic_pause('No changes.', 3)
|
||||
@ -743,7 +528,6 @@ class NoteContent(NoteContentBase):
|
||||
|
||||
self.title = title
|
||||
self.misc = misc
|
||||
self.group = group
|
||||
|
||||
await self._save_ux(menu)
|
||||
|
||||
@ -790,7 +574,7 @@ async def start_export(notes):
|
||||
await needs_microsd()
|
||||
return
|
||||
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
|
||||
|
||||
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'))
|
||||
|
||||
# We have some JSON, parsed now.
|
||||
ok = await import_from_json(records)
|
||||
if not ok: return
|
||||
await import_from_json(records)
|
||||
|
||||
await ux_dramatic_pause('Saved.', 3)
|
||||
menu.update_contents()
|
||||
@ -852,7 +635,6 @@ async def import_from_json(records):
|
||||
settings.set('notes', was)
|
||||
settings.set('secnap', True)
|
||||
settings.save()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
await ux_show_story(title="Failure", msg=str(e) + '\n\n' + problem_file_line(e))
|
||||
|
||||
@ -67,9 +67,6 @@ from utils import call_later_ms
|
||||
# msas = multisig address show (do not censor multisig addresses)
|
||||
# ccc = (complex) If present, CCC feature is enabled and key details stored here.
|
||||
# ktrx = (privkey) Key teleport Rx has been started, this will be our keypair
|
||||
# sssp = (complex) If present, a (single signer) spending-policy is defined (maybe disabled)
|
||||
# lfr = (string) If present, the reason why Spending Policy blocked last transaction
|
||||
# wifs = (list) List of tuples (public/private key)
|
||||
|
||||
# Stored w/ key=00 for access before login
|
||||
# _skip_pin = hard code a PIN value (dangerous, only for debug)
|
||||
@ -93,9 +90,7 @@ KEEP_IF_BLANK_SETTINGS = ["wa", "sighshchk", "emu", "rz", "b39skip",
|
||||
"axskip", "del", "pms", "idle_to", "batt_to",
|
||||
"bright", "msas"]
|
||||
|
||||
# key value pairs saved directly to master seed settings
|
||||
# held in RAM for tmp seed sessions
|
||||
MASTER_FIELDS = ['seeds', 'seedvault', 'xfp', 'words', "bkpw", "sssp"]
|
||||
SEEDVAULT_FIELDS = ['seeds', 'seedvault', 'xfp', 'words', "bkpw"]
|
||||
|
||||
NUM_SLOTS = const(100)
|
||||
SLOTS = range(NUM_SLOTS)
|
||||
@ -289,7 +284,7 @@ class SettingsObject:
|
||||
|
||||
SettingsObject.master_nvram_key = self.nvram_key
|
||||
|
||||
for fn in MASTER_FIELDS:
|
||||
for fn in SEEDVAULT_FIELDS:
|
||||
curr = self.current.get(fn, None)
|
||||
if curr is not None:
|
||||
SettingsObject.master_sv_data[fn] = curr
|
||||
@ -305,7 +300,7 @@ class SettingsObject:
|
||||
SettingsObject.master_sv_data.clear()
|
||||
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
|
||||
# Concern is we may be changing a setting from a tmp seed mode
|
||||
# - always does a save
|
||||
@ -316,7 +311,6 @@ class SettingsObject:
|
||||
self.set(key, value)
|
||||
self.save()
|
||||
else:
|
||||
assert not master_only
|
||||
# harder, slower: have to load, change and write
|
||||
master = SettingsObject(nvram_key=SettingsObject.master_nvram_key)
|
||||
master.load()
|
||||
@ -325,7 +319,7 @@ class SettingsObject:
|
||||
del master
|
||||
|
||||
# track our copies
|
||||
if key in MASTER_FIELDS:
|
||||
if key in SEEDVAULT_FIELDS:
|
||||
SettingsObject.master_sv_data[key] = value
|
||||
|
||||
def master_get(self, kn, default=None):
|
||||
@ -337,7 +331,7 @@ class SettingsObject:
|
||||
return self.get(kn, default)
|
||||
|
||||
# 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)
|
||||
if res is None:
|
||||
return default
|
||||
|
||||
@ -2,18 +2,17 @@
|
||||
#
|
||||
# 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 ucollections import namedtuple
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from ubinascii import unhexlify as a2b_hex
|
||||
from exceptions import UnknownAddressExplained
|
||||
from utils import problem_file_line, show_single_address, validate_own_address
|
||||
from public_constants import AFC_SCRIPT, AF_P2WPKH_P2SH, AF_P2SH, AF_P2WSH_P2SH, AF_P2TR, AF_P2WSH
|
||||
from utils import problem_file_line, show_single_address
|
||||
|
||||
# Track many addresses, but in compressed form
|
||||
# - 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
|
||||
# - try to keep private between different duress wallets, and seed vaults
|
||||
# - 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
|
||||
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):
|
||||
# Convert text address to something we can store while preserving privacy.
|
||||
@ -57,7 +56,6 @@ class AddressCacheFile:
|
||||
self.salt = h[32:]
|
||||
self.count = 0
|
||||
self.hdr = None
|
||||
self.fd = None
|
||||
|
||||
self.peek()
|
||||
|
||||
@ -67,6 +65,9 @@ class AddressCacheFile:
|
||||
rv += ' (change)'
|
||||
return rv
|
||||
|
||||
def exists(self):
|
||||
return bool(self.count)
|
||||
|
||||
def peek(self):
|
||||
# see what we have on-disk; just reads header.
|
||||
try:
|
||||
@ -104,13 +105,14 @@ class AddressCacheFile:
|
||||
self.fd.write(hdr)
|
||||
|
||||
def append(self, addr):
|
||||
self.fd.write(encode_addr(addr, self.salt))
|
||||
|
||||
def close(self):
|
||||
# close file, done
|
||||
if self.fd is not None:
|
||||
if addr is None:
|
||||
# close file, done
|
||||
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):
|
||||
# Do the easy part of the searching, using the existing file's contents.
|
||||
@ -119,7 +121,6 @@ class AddressCacheFile:
|
||||
from glob import dis
|
||||
|
||||
if not self.hdr or not self.count:
|
||||
# cache empty
|
||||
return
|
||||
|
||||
with open(self.fname, 'rb') as fd:
|
||||
@ -131,7 +132,7 @@ class AddressCacheFile:
|
||||
chk = encode_addr(addr, self.salt)
|
||||
for idx in range(self.count):
|
||||
if buf[idx*HASH_ENC_LEN : (idx*HASH_ENC_LEN)+HASH_ENC_LEN] == chk:
|
||||
yield self.change_idx, idx
|
||||
yield (self.change_idx, idx)
|
||||
|
||||
dis.progress_sofar(idx, self.count)
|
||||
|
||||
@ -147,106 +148,92 @@ class AddressCacheFile:
|
||||
# - return subpath for a hit or None
|
||||
from glob import dis
|
||||
|
||||
bonus = 0
|
||||
match = None
|
||||
|
||||
start_idx = self.count
|
||||
count = MAX_ADDRS_STORED - start_idx
|
||||
|
||||
if count <= 0:
|
||||
return match
|
||||
return None
|
||||
|
||||
self.setup(self.change_idx, start_idx)
|
||||
|
||||
bonus = None
|
||||
for idx,here,*_ in self.wallet.yield_addresses(start_idx, count,
|
||||
change_idx=self.change_idx):
|
||||
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
|
||||
|
||||
change_idx=self.change_idx):
|
||||
|
||||
if here == addr:
|
||||
# match but keep going
|
||||
# Found it! But keep going a little for next time.
|
||||
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()
|
||||
return match
|
||||
if match and bonus >= BONUS_GAP_LIMIT:
|
||||
self.append(None)
|
||||
return match
|
||||
|
||||
dis.progress_sofar(idx-start_idx, count)
|
||||
|
||||
self.append(None)
|
||||
|
||||
return None
|
||||
|
||||
class OwnershipCache:
|
||||
|
||||
@classmethod
|
||||
def saver(cls, wallet, change_idx, start_idx, count):
|
||||
# when we are generating many addresses for export, capture them (if suitable)
|
||||
def saver(cls, wallet, change_idx, start_idx):
|
||||
# when we are generating many addresses for export, capture them
|
||||
# as we go with this function
|
||||
if not count:
|
||||
return
|
||||
if change_idx not in (0, 1):
|
||||
return
|
||||
if start_idx >= MAX_ADDRS_STORED:
|
||||
return
|
||||
|
||||
# - not change -- only main addrs
|
||||
file = AddressCacheFile(wallet, change_idx)
|
||||
current_pos = file.count
|
||||
|
||||
if start_idx > current_pos:
|
||||
# nothing to do here, we are missing some addresses in the middle
|
||||
return
|
||||
if (start_idx + count) <= current_pos:
|
||||
# we already have all these addresses
|
||||
return
|
||||
if file.exists():
|
||||
# don't save to existing file, has some already
|
||||
return None
|
||||
|
||||
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):
|
||||
if addr is None:
|
||||
file.close()
|
||||
elif (idx < MAX_ADDRS_STORED) and idx >= current_pos:
|
||||
file.append(addr)
|
||||
|
||||
return doit
|
||||
return file.append
|
||||
|
||||
@classmethod
|
||||
def filter(cls, addr_fmt, args):
|
||||
# Filter possible candidates!
|
||||
def search(cls, addr):
|
||||
# Find it!
|
||||
# - returns wallet object, and tuple2 of final 2 subpath components
|
||||
# - if you start w/ testnet, we'll follow that
|
||||
from multisig import MultisigWallet
|
||||
from public_constants import AFC_SCRIPT, AF_P2WPKH_P2SH, AF_P2SH, AF_P2WSH_P2SH
|
||||
from glob import dis
|
||||
|
||||
args = args or {}
|
||||
ch = chains.current_chain()
|
||||
|
||||
# user has specified specific (named) wallet
|
||||
named_wal = args.get("wallet", None)
|
||||
if named_wal:
|
||||
# quick search without deserialization
|
||||
res = list(MultisigWallet.iter_wallets(name=named_wal))
|
||||
if not res:
|
||||
raise UnknownAddressExplained("Wallet '%s' not defined." % named_wal)
|
||||
|
||||
# only return desired named wallet, no other wallets are searched
|
||||
return res
|
||||
addr_fmt = ch.possible_address_fmt(addr)
|
||||
if not addr_fmt:
|
||||
# might be valid address over on testnet vs mainnet
|
||||
raise UnknownAddressExplained('That address is not valid on ' + ch.name)
|
||||
|
||||
possibles = []
|
||||
|
||||
if addr_fmt & AFC_SCRIPT:
|
||||
# multisig or script at least... must exist already
|
||||
afs = [addr_fmt]
|
||||
# multisig or script at least.. must exist already
|
||||
possibles.extend(MultisigWallet.iter_wallets(addr_fmt=addr_fmt))
|
||||
|
||||
if addr_fmt == AF_P2SH:
|
||||
# might look like P2SH but actually be AF_P2WSH_P2SH
|
||||
# wrapped segwit is more used than legacy
|
||||
afs = [AF_P2WSH_P2SH, AF_P2SH]
|
||||
possibles.extend(MultisigWallet.iter_wallets(addr_fmt=AF_P2WSH_P2SH))
|
||||
|
||||
# Might be single-sig p2wpkh wrapped in p2sh ... but that was a transition
|
||||
# thing that hopefully is going away, so if they have any multisig wallets,
|
||||
# defined, assume that that's the only p2sh address source.
|
||||
addr_fmt = AF_P2WPKH_P2SH
|
||||
|
||||
possibles.extend(MultisigWallet.iter_wallets(addr_fmts=afs))
|
||||
# TODO: add tapscript and such fancy stuff here
|
||||
|
||||
try:
|
||||
# Construct possible single-signer wallets, always at least account=0 case
|
||||
@ -260,97 +247,63 @@ class OwnershipCache:
|
||||
if af == addr_fmt and acct_num:
|
||||
w = MasterSingleSigWallet(addr_fmt, account_idx=acct_num)
|
||||
possibles.append(w)
|
||||
except (KeyError, ValueError):
|
||||
pass # if not single sig address format
|
||||
except (KeyError, ValueError): pass # if not single sig address format
|
||||
|
||||
if not possibles:
|
||||
# can only happen w/ scripts; for single-signer we have things to check
|
||||
raise UnknownAddressExplained(
|
||||
"No suitable multisig wallets are currently defined.")
|
||||
"No suitable multisig wallets are currently defined.")
|
||||
|
||||
# ordering here
|
||||
return possibles
|
||||
|
||||
@classmethod
|
||||
def search_wallet_cache(cls, addr, cf):
|
||||
# - returns wallet object, and tuple2 of final 2 subpath components
|
||||
# "quick" check first, before doing any generations
|
||||
# external chain first, then internal (change)
|
||||
for maybe in cf.fast_search(addr):
|
||||
ok = cf.check_match(addr, maybe)
|
||||
if ok:
|
||||
return cf.wallet, maybe
|
||||
return None, None
|
||||
|
||||
count = 0
|
||||
phase2 = []
|
||||
for change_idx in (0, 1):
|
||||
files = [AddressCacheFile(w, change_idx) for w in possibles]
|
||||
for f in files:
|
||||
if dis.has_lcd:
|
||||
dis.fullscreen('Searching wallet(s)...', line2=f.nice_name())
|
||||
else:
|
||||
dis.fullscreen('Searching...')
|
||||
|
||||
for maybe in f.fast_search(addr):
|
||||
ok = f.check_match(addr, maybe)
|
||||
if not ok: continue # false positive - will happen
|
||||
|
||||
# found winner.
|
||||
return f.wallet, maybe
|
||||
|
||||
if f.count < MAX_ADDRS_STORED:
|
||||
phase2.append(f)
|
||||
|
||||
count += f.count
|
||||
|
||||
@classmethod
|
||||
def search_build_wallet(cls, addr, cf):
|
||||
# maybe we haven't calculated all the addresses yet, so do that
|
||||
# - very slow, but only needed once; any negative (failed) search causes this
|
||||
# - could stop when match found, but we go a bit beyond that for next time
|
||||
# - we could search all in parallel, rather than serially because
|
||||
# more likely to find a match with low index... but seen as too much memory
|
||||
result = cf.build_and_search(addr)
|
||||
if result:
|
||||
# found it, so report it and stop
|
||||
return cf.wallet, result
|
||||
|
||||
for f in phase2:
|
||||
b4 = f.count
|
||||
if dis.has_lcd:
|
||||
dis.fullscreen("Generating addresses...", line2=f.nice_name())
|
||||
else:
|
||||
dis.fullscreen("Generating...")
|
||||
|
||||
result = f.build_and_search(addr)
|
||||
if result:
|
||||
# found it, so report it and stop
|
||||
return f.wallet, result
|
||||
|
||||
count += f.count - b4
|
||||
|
||||
# possible phase 3: other seedvault... slow, rare and not implemented
|
||||
return None, None
|
||||
|
||||
raise UnknownAddressExplained('Searched %d candidates without finding a match.' % count)
|
||||
|
||||
@classmethod
|
||||
def search(cls, addr, args=None):
|
||||
from glob import dis
|
||||
|
||||
dis.fullscreen("Wait...")
|
||||
|
||||
try:
|
||||
addr, addr_fmt = validate_own_address(addr)
|
||||
except Exception as e:
|
||||
raise UnknownAddressExplained('That address is not valid on ' + e.args[0])
|
||||
|
||||
matches = OWNERSHIP.filter(addr_fmt, args)
|
||||
|
||||
# build cache files for both external & internal chain
|
||||
cachefs = []
|
||||
for w in matches:
|
||||
cachefs.append(AddressCacheFile(w, 0))
|
||||
cachefs.append(AddressCacheFile(w, 1))
|
||||
|
||||
for cf in cachefs:
|
||||
msg = "Searching wallet(s)..." if dis.has_lcd else "Searching..."
|
||||
dis.fullscreen(msg, line2=cf.nice_name())
|
||||
wallet, subpath = OWNERSHIP.search_wallet_cache(addr, cf)
|
||||
if wallet:
|
||||
# first arg from_cache=True
|
||||
return True, wallet, subpath
|
||||
|
||||
# nothing found in existing cache files
|
||||
c = 0
|
||||
for cf in cachefs:
|
||||
msg = "Generating addresses..." if dis.has_lcd else "Generating..."
|
||||
dis.fullscreen(msg, line2=cf.nice_name())
|
||||
wallet, subpath = OWNERSHIP.search_build_wallet(addr, cf)
|
||||
c += cf.count
|
||||
if wallet:
|
||||
# first arg from_cache=False
|
||||
return False, wallet, subpath
|
||||
|
||||
# nothing found among singlesig & registered multisig wallets
|
||||
# check WIF store (single sig only)
|
||||
if addr_fmt not in [AF_P2TR, AF_P2WSH]:
|
||||
dis.fullscreen("WIF Store...")
|
||||
from wif import iter_wif_store_addresses
|
||||
target_af = AF_P2WPKH_P2SH if addr_fmt == AF_P2SH else addr_fmt
|
||||
for i, store_addr in iter_wif_store_addresses(target_af):
|
||||
if store_addr == addr:
|
||||
return False, ("wif", target_af), i+1
|
||||
|
||||
raise UnknownAddressExplained('Searched %d candidate addresses in %d wallet(s)'
|
||||
' without finding a match.' % (c, len(matches)))
|
||||
|
||||
@classmethod
|
||||
async def search_ux(cls, addr, args):
|
||||
async def search_ux(cls, addr):
|
||||
# Provide a simple UX. Called functions do fullscreen, progress bar stuff.
|
||||
from ux import ux_show_story, show_qr_code
|
||||
from charcodes import KEY_QR
|
||||
@ -358,28 +311,25 @@ class OwnershipCache:
|
||||
from public_constants import AFC_BECH32, AFC_BECH32M
|
||||
|
||||
try:
|
||||
_, wallet, subpath = cls.search(addr, args)
|
||||
wallet, subpath = OWNERSHIP.search(addr)
|
||||
is_ms = isinstance(wallet, MultisigWallet)
|
||||
sp = wallet.render_path(*subpath)
|
||||
|
||||
msg = show_single_address(addr)
|
||||
esc = ""
|
||||
if isinstance(wallet, tuple) and (wallet[0] == "wif"):
|
||||
msg += '\n\nFound in WIF store at index %d' % subpath
|
||||
addr_fmt = wallet[1]
|
||||
msg += '\n\nFound in wallet:\n ' + wallet.name
|
||||
msg += '\nDerivation path:\n ' + sp
|
||||
if is_ms:
|
||||
esc = ""
|
||||
else:
|
||||
sp = wallet.render_path(*subpath)
|
||||
msg += '\n\nFound in wallet:\n ' + wallet.name
|
||||
msg += '\nDerivation path:\n ' + sp
|
||||
addr_fmt = wallet.addr_fmt
|
||||
if not is_ms:
|
||||
esc = "0"
|
||||
msg += "\n\nPress (0) to sign message with this key."
|
||||
esc = "0"
|
||||
msg += "\n\nPress (0) to sign message with this key."
|
||||
|
||||
title = "Verified"
|
||||
if version.has_qwerty:
|
||||
esc += KEY_QR
|
||||
title += " Address"
|
||||
else:
|
||||
msg += ' Press (1) for address QR.'
|
||||
msg += ' (1) for address QR'
|
||||
esc += '1'
|
||||
title += "!"
|
||||
|
||||
@ -387,10 +337,10 @@ class OwnershipCache:
|
||||
ch = await ux_show_story(msg, title=title, escape=esc, hint_icons=KEY_QR)
|
||||
if ch in ("1"+KEY_QR):
|
||||
await show_qr_code(addr, msg=addr, is_addrs=True,
|
||||
is_alnum=(addr_fmt & (AFC_BECH32 | AFC_BECH32M)))
|
||||
is_alnum=(wallet.addr_fmt & (AFC_BECH32 | AFC_BECH32M)))
|
||||
elif not is_ms and (ch == "0"): # only singlesig
|
||||
from msgsign import sign_with_own_address
|
||||
await sign_with_own_address(sp, addr_fmt)
|
||||
await sign_with_own_address(sp, wallet.addr_fmt)
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
@ -179,7 +179,7 @@ class PaperWalletMaker:
|
||||
return
|
||||
except Exception as e:
|
||||
from utils import problem_file_line
|
||||
await ux_show_story('Failed to write!\n\n'+problem_file_line(e))
|
||||
await ux_show_story('Failed to write!\n\n\n'+problem_file_line(e))
|
||||
return
|
||||
|
||||
story = "Done! Created file(s):\n\n%s" % nice_txt
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
# pincodes.py - manage PIN code (which map to wallet seeds)
|
||||
#
|
||||
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
|
||||
|
||||
# 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.cached_main_pin = bytearray(32)
|
||||
|
||||
# If set, a spending policy is in effect, and so even tho we know the master
|
||||
# seed, we are not going to let them see it, nor sign things we dont like, etc.
|
||||
self.hobbled_mode = False
|
||||
|
||||
#assert MAX_PIN_LEN == 32 # update FMT otherwise
|
||||
#assert ustruct.calcsize(PIN_ATTEMPT_FMT_V1) == PIN_ATTEMPT_SIZE_V1
|
||||
#assert ustruct.calcsize(PIN_ATTEMPT_FMT_V2_ADDITIONS) \
|
||||
# == PIN_ATTEMPT_SIZE - PIN_ATTEMPT_SIZE_V1
|
||||
assert MAX_PIN_LEN == 32 # update FMT otherwise
|
||||
assert ustruct.calcsize(PIN_ATTEMPT_FMT_V1) == PIN_ATTEMPT_SIZE_V1
|
||||
assert ustruct.calcsize(PIN_ATTEMPT_FMT_V2_ADDITIONS) == PIN_ATTEMPT_SIZE - PIN_ATTEMPT_SIZE_V1
|
||||
|
||||
# check for bricked system early
|
||||
import callgate
|
||||
if callgate.get_is_bricked():
|
||||
# die right away if it's not going to work
|
||||
print("SE bricked")
|
||||
callgate.enter_dfu(3)
|
||||
|
||||
def __repr__(self):
|
||||
return '<PinAttempt: fails/left=%d/%d tc_flag/arg=0x%x/0x%x>' % (
|
||||
@ -335,6 +339,10 @@ class PinAttempt:
|
||||
|
||||
return self.state_flags
|
||||
|
||||
def delay(self):
|
||||
# obsolete since Mk3, but called from login.py
|
||||
self.roundtrip(1)
|
||||
|
||||
def login(self):
|
||||
# test we have the PIN code right, and unlock access if so.
|
||||
chk = self.roundtrip(2)
|
||||
@ -525,25 +533,11 @@ class PinAttempt:
|
||||
from trick_pins import TC_DELTA_MODE
|
||||
return bool(self.delay_required & TC_DELTA_MODE)
|
||||
|
||||
|
||||
def get_tc_values(self):
|
||||
# Mk4 only
|
||||
# return (tc_flags, tc_arg)
|
||||
return self.delay_required, self.delay_achieved
|
||||
|
||||
@staticmethod
|
||||
async def enforce_brick():
|
||||
# check for bricked system early
|
||||
if get_is_bricked():
|
||||
try:
|
||||
# regardless of settings, become a forever calculator after brickage.
|
||||
while version.has_qwerty:
|
||||
from calc import login_repl
|
||||
await login_repl()
|
||||
finally:
|
||||
# die right away if it's not going to work
|
||||
enter_dfu(3)
|
||||
|
||||
|
||||
# singleton
|
||||
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]
|
||||
|
||||
def is_at(self, ptr, offset):
|
||||
# is bytes() object really one we created at read_at
|
||||
return uctypes.addressof(ptr) == self.base+offset
|
||||
|
||||
# Be compatible with SPIFlash class...
|
||||
|
||||
def read(self, address, buf, cmd=None):
|
||||
|
||||
@ -4,10 +4,10 @@
|
||||
#
|
||||
import framebuf, uqr
|
||||
from ux import UserInteraction, ux_wait_keyup, the_ux
|
||||
from version import has_qwerty
|
||||
from exceptions import QRTooBigError
|
||||
from charcodes import (KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_NFC,
|
||||
KEY_END, KEY_ENTER, KEY_CANCEL)
|
||||
KEY_END, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_ENTER, KEY_CANCEL)
|
||||
from version import has_qwerty
|
||||
|
||||
|
||||
# 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,
|
||||
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.idx = 0 # start with first address
|
||||
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
|
||||
self.is_secret = is_secret
|
||||
self.change_idxs = change_idxs or []
|
||||
self.can_raise = can_raise
|
||||
self.qr_msgs = qr_msgs
|
||||
self.no_index = no_index
|
||||
|
||||
def calc_qr(self, msg):
|
||||
# Version 2 would be nice, but can't hold what we need, even at min error correction,
|
||||
@ -65,20 +62,12 @@ class QRDisplaySingle(UserInteraction):
|
||||
# draw_qr_display takes this and renders hint in the top right corner
|
||||
# this member function decides what type of hint will be shown
|
||||
# numbers, letters, etc.
|
||||
if self.no_index:
|
||||
return None
|
||||
return str(self.start_n + self.idx) if len(self.addrs) > 1 else None
|
||||
|
||||
def side_msg(self):
|
||||
def is_change(self):
|
||||
if self.idx in self.change_idxs:
|
||||
return "CHANGE BACK"
|
||||
|
||||
elif self.qr_msgs:
|
||||
try:
|
||||
return self.qr_msgs[self.idx]
|
||||
except IndexError: pass
|
||||
|
||||
return None
|
||||
return True
|
||||
return False
|
||||
|
||||
def redraw(self):
|
||||
# Redraw screen.
|
||||
@ -87,15 +76,6 @@ class QRDisplaySingle(UserInteraction):
|
||||
|
||||
# what we are showing inside the QR
|
||||
body = self.addrs[self.idx]
|
||||
idx_hint = self.idx_hint()
|
||||
|
||||
msg = None
|
||||
if self.msg:
|
||||
msg = self.msg
|
||||
else:
|
||||
if isinstance(body, str):
|
||||
# sanity check
|
||||
msg = body
|
||||
|
||||
# make the QR, if needed.
|
||||
if not self.qr_data:
|
||||
@ -104,19 +84,23 @@ class QRDisplaySingle(UserInteraction):
|
||||
self.calc_qr(body)
|
||||
except Exception:
|
||||
dis.busy_bar(False)
|
||||
if not self.can_raise:
|
||||
dis.draw_qr_error(idx_hint, msg)
|
||||
return
|
||||
|
||||
# other code paths require raise to switch to BBQr
|
||||
raise QRTooBigError
|
||||
raise
|
||||
|
||||
# draw display
|
||||
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,
|
||||
self.sidebar, idx_hint, self.invert,
|
||||
self.sidebar, self.idx_hint(), self.invert,
|
||||
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):
|
||||
from glob import NFC, dis
|
||||
|
||||
@ -57,8 +57,9 @@ SLOW_BAUD = const(9600)
|
||||
FAST_BAUD = const(57600)
|
||||
RX_BUF_SIZE = const(4350) # big enough for full v40 decoded
|
||||
|
||||
# TODO: constructor should avoid full setup until after login; after setup,
|
||||
# command sleep is the known low-power state.
|
||||
# TODO: constructor should leave it in reset for simple lower-power usage; then after
|
||||
# login we can do full setup (2+ seconds) and then sleep again until needed.
|
||||
|
||||
class QRScanner:
|
||||
|
||||
def __init__(self):
|
||||
@ -67,8 +68,6 @@ class QRScanner:
|
||||
self.scan_light = False # is light on during scanning?
|
||||
self.version = None
|
||||
self.setup_done = False
|
||||
self.needs_reinit = False
|
||||
self.sleep_seq = 0
|
||||
|
||||
# hodl this lock when communicating w/ QR scanner
|
||||
self.lock = asyncio.Lock()
|
||||
@ -85,21 +84,16 @@ class QRScanner:
|
||||
# setup hardware, reset scanner and return time to delay until ready
|
||||
from machine import UART, Pin
|
||||
self.serial = UART(2, SLOW_BAUD, rxbuf=RX_BUF_SIZE)
|
||||
self.reset = Pin('QR_RESET', Pin.OUT_OD, value=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.pulse_reset()
|
||||
|
||||
# needs full 2 seconds of recovery time after reset
|
||||
return 2
|
||||
|
||||
def pulse_reset(self):
|
||||
# RESET is active low (open drain). Keep it as a pulse; module docs
|
||||
# describe low on this pin as wake-up, so don't use it as parking state.
|
||||
# NOTE: reset is active low (open drain)
|
||||
self.reset(0)
|
||||
utime.sleep_ms(10)
|
||||
self.reset(1)
|
||||
self.needs_reinit = False
|
||||
|
||||
# needs full 2 seconds of recovery time
|
||||
return 2
|
||||
|
||||
def set_baud(self, br):
|
||||
# change serial port baud rate
|
||||
@ -124,104 +118,56 @@ class QRScanner:
|
||||
|
||||
async def setup_task(self, start_delay):
|
||||
# Task to setup device, and then die.
|
||||
await asyncio.sleep(start_delay)
|
||||
|
||||
async with self.lock:
|
||||
for attempt in range(3):
|
||||
await asyncio.sleep(start_delay)
|
||||
|
||||
try:
|
||||
await self._configure()
|
||||
except Exception:
|
||||
# a step failed or timed out (would have left scanner dead
|
||||
# until next boot); reset module and start over
|
||||
await self.blind_shutdown()
|
||||
if attempt == 2:
|
||||
break
|
||||
start_delay = self.reset_stream()
|
||||
continue
|
||||
|
||||
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)
|
||||
# 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:
|
||||
raise RuntimeError('no contact after S_CMD_FFFF')
|
||||
#print("QR Scanner: missing")
|
||||
return
|
||||
|
||||
# go to high speed!
|
||||
if baud != FAST_BAUD:
|
||||
await self.txrx('S_CMD_H3BR%d' % FAST_BAUD)
|
||||
self.set_baud(FAST_BAUD)
|
||||
await self.txrx('S_CMD_FFFF') # factory reset of settings
|
||||
|
||||
# configure it like we want it
|
||||
await self.txrx('S_CMD_MTRS5000') # 5s to read before fail (unused)
|
||||
await self.txrx('S_CMD_MT11') # trigger is edge-based (not level)
|
||||
await self.txrx('S_CMD_MT30') # Same code reading without delay
|
||||
await self.txrx('S_CMD_MT20') # Enable automatic sleep when idle
|
||||
await self.txrx('S_CMD_MTRF500') # Idle time: 500ms
|
||||
await self.txrx('S_CMD_059A') # add CR LF after QR data (important)
|
||||
await self.txrx('S_CMD_03L0') # light off all the time by default
|
||||
await self.txrx('S_CMD_0407') # turn on signal for our yellow led
|
||||
# go to high speed!
|
||||
if baud != FAST_BAUD:
|
||||
await self.txrx('S_CMD_H3BR%d' % FAST_BAUD)
|
||||
self.set_baud(FAST_BAUD)
|
||||
|
||||
# settings under continuous scan mode
|
||||
await self.txrx('S_CMD_MARS0000') # "Modify the duration of single code reading" (ms)
|
||||
await self.txrx('S_CMD_MARR000') # "Modify the time of the reading interval 0ms"
|
||||
await self.txrx('S_CMD_MA31') # Enable "Same code reading delay"
|
||||
await self.txrx('S_CMD_MARI0050') # "Modify the same code reading delay 50ms"
|
||||
# configure it like we want it
|
||||
await self.txrx('S_CMD_MTRS5000') # 5s to read before fail (unused)
|
||||
await self.txrx('S_CMD_MT11') # trigger is edge-based (not level)
|
||||
await self.txrx('S_CMD_MT30') # Same code reading without delay
|
||||
await self.txrx('S_CMD_MT20') # Enable automatic sleep when idle
|
||||
await self.txrx('S_CMD_MTRF500') # Idle time: 500ms
|
||||
await self.txrx('S_CMD_059A') # add CR LF after QR data (important)
|
||||
await self.txrx('S_CMD_03L0') # light off all the time by default
|
||||
await self.txrx('S_CMD_0407') # turn on signal for our yellow led
|
||||
|
||||
# these aren't useful (yet?) and just make things harder to decode.
|
||||
#await self.txrx('S_CMD_05F1') # add all information on
|
||||
#await self.txrx('S_CMD_05L1') # output decoding length info on
|
||||
#await self.txrx('S_CMD_05S1') # STX start char
|
||||
#await self.txrx('S_CMD_05C1') # CodeID+prefix
|
||||
#await self.txrx('S_CMD_0501') # prefix on
|
||||
#await self.txrx('S_CMD_0506') # suffix
|
||||
#await self.txrx('S_CMD_05D0') # tx total data
|
||||
# settings under continuous scan mode
|
||||
await self.txrx('S_CMD_MARS0000') # "Modify the duration of single code reading" (ms)
|
||||
await self.txrx('S_CMD_MARR000') # "Modify the time of the reading interval 0ms"
|
||||
await self.txrx('S_CMD_MA31') # Enable "Same code reading delay"
|
||||
await self.txrx('S_CMD_MARI0050') # "Modify the same code reading delay 50ms"
|
||||
|
||||
# prevent scanning magic QR to affect settings
|
||||
await self.txrx('S_CMD_0000') # close setting codes
|
||||
# these aren't useful (yet?) and just make things harder to decode.
|
||||
#await self.txrx('S_CMD_05F1') # add all information on
|
||||
#await self.txrx('S_CMD_05L1') # output decoding length info on
|
||||
#await self.txrx('S_CMD_05S1') # STX start char
|
||||
#await self.txrx('S_CMD_05C1') # CodeID+prefix
|
||||
#await self.txrx('S_CMD_0501') # prefix on
|
||||
#await self.txrx('S_CMD_0506') # suffix
|
||||
#await self.txrx('S_CMD_05D0') # tx total data
|
||||
|
||||
# prevent scanning magic QR to affect settings
|
||||
await self.txrx('S_CMD_0000') # close setting codes
|
||||
|
||||
self.setup_done = True
|
||||
|
||||
await self.goto_sleep()
|
||||
|
||||
async def scan_once(self):
|
||||
# Blocks until something is scanned. Returns it as string
|
||||
@ -230,16 +176,6 @@ class QRScanner:
|
||||
# - returns a BBQr object at that point
|
||||
self.scan_light = False
|
||||
|
||||
if self.needs_reinit:
|
||||
try:
|
||||
await self.setup_task(self.reset_stream())
|
||||
if self.setup_done:
|
||||
await asyncio.sleep_ms(200)
|
||||
except asyncio.CancelledError:
|
||||
await self.blind_shutdown()
|
||||
self.mark_needs_reinit()
|
||||
return None
|
||||
|
||||
# wait for reset process to complete (can be an issue right after boot)
|
||||
# - few seconds of boot time needed
|
||||
for retry in range(10):
|
||||
@ -275,22 +211,19 @@ class QRScanner:
|
||||
finally:
|
||||
# Problem: another valid scan can come in just as we are trying
|
||||
# to get out of scanner mode
|
||||
for retry in range(3):
|
||||
for retry in range(10):
|
||||
try:
|
||||
await self.txrx('S_CMD_020D', timeout=1000) # return to "Command mode"
|
||||
await self.txrx('S_CMD_03L0', timeout=1000) # turn off bright light
|
||||
await self.txrx('S_CMD_020D') # return to "Command mode"
|
||||
await self.txrx('S_CMD_03L0') # turn off bright light
|
||||
#print('rest after %d retries' % retry)
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.sleep_ms(50)
|
||||
except: pass
|
||||
await asyncio.sleep_ms(25)
|
||||
else:
|
||||
pass
|
||||
#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
|
||||
|
||||
# return BBQr object or string if simple QR
|
||||
@ -321,14 +254,13 @@ class QRScanner:
|
||||
# send specific command until it responds
|
||||
# - it will wake on any command, but not instant
|
||||
# - first one seems to fail 100%
|
||||
self.sleep_seq += 1
|
||||
await self.tx('SRDF0051') # blindly at first
|
||||
|
||||
for retry in range(5):
|
||||
try:
|
||||
await self.txrx('SRDF0051', timeout=50) # 50 ok, 20 too short
|
||||
return
|
||||
except Exception:
|
||||
except:
|
||||
# first try usually fails, that's okay... its asleep and groggy
|
||||
pass
|
||||
|
||||
@ -338,13 +270,9 @@ class QRScanner:
|
||||
# - need blind retries here
|
||||
# - might be two layers of sleep, and we need this second command after the first
|
||||
# - helps to turn off the yellow LED, and save power as well
|
||||
self.sleep_seq += 1
|
||||
sleep_seq = self.sleep_seq
|
||||
await self.tx('SRDF0050')
|
||||
async def later():
|
||||
await asyncio.sleep_ms(150)
|
||||
if sleep_seq != self.sleep_seq or self.busy_scanning:
|
||||
return
|
||||
await self.tx('SRDF0050')
|
||||
asyncio.create_task(later())
|
||||
|
||||
@ -362,22 +290,6 @@ class QRScanner:
|
||||
#print('tx >> ' + msg)
|
||||
self.serial.write(msg)
|
||||
|
||||
async def readexactly_timeout(self, num, timeout, msg=None):
|
||||
# Avoid asyncio.wait_for_ms here: it can leave the scanner setup task
|
||||
# stuck after a CancelledError. Convert scanner silence into a normal
|
||||
# retryable command failure instead.
|
||||
if timeout is None:
|
||||
return await self.stream.readexactly(num)
|
||||
|
||||
start = utime.ticks_ms()
|
||||
while self.stream.s.any() < num:
|
||||
if utime.ticks_diff(utime.ticks_ms(), start) >= timeout:
|
||||
#print("no rx after %s" % msg)
|
||||
raise RuntimeError
|
||||
await asyncio.sleep_ms(5)
|
||||
|
||||
return await self.stream.readexactly(num)
|
||||
|
||||
async def txrx(self, msg, timeout=250):
|
||||
# Send a command, get the corresponding response.
|
||||
# - has a long timeout, collects rx based on framing
|
||||
@ -398,8 +310,13 @@ class QRScanner:
|
||||
expect = LEN_OKAY
|
||||
rx = b''
|
||||
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))
|
||||
|
||||
|
||||
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
|
||||
VALID_LENGTHS = (24, 18, 12)
|
||||
|
||||
# maximum length for BIP-39 passphrase
|
||||
MAX_PASS_LEN = 100
|
||||
|
||||
# bit flag that means "also include bare prefix as a valid word"
|
||||
_PREFIX_MARKER = const(1<<26)
|
||||
|
||||
@ -43,10 +40,6 @@ _PREFIX_MARKER = const(1<<26)
|
||||
# - 'encoded' is hex, and has is trimmed of right side zeros
|
||||
VaultEntry = namedtuple('VaultEntry', 'xfp encoded label origin')
|
||||
|
||||
def not_hobbled_mode():
|
||||
# used as menu predicate and similar
|
||||
return not pa.hobbled_mode
|
||||
|
||||
def seed_vault_iter():
|
||||
# iterate over all seeds in the vault; returns VaultEntry instances.
|
||||
# raw vault entries are list type when json.loaded from flash
|
||||
@ -157,62 +150,23 @@ class WordNestMenu(MenuSystem):
|
||||
done_cb = None
|
||||
|
||||
def __init__(self, num_words=None, has_checksum=True, done_cb=commit_new_words,
|
||||
items=None, is_commit=False, menu_cbf=None, prefix="", words=None):
|
||||
items=None, is_commit=False):
|
||||
|
||||
if num_words is not None:
|
||||
WordNestMenu.target_words = num_words
|
||||
WordNestMenu.has_checksum = has_checksum
|
||||
WordNestMenu.words = []
|
||||
assert done_cb
|
||||
WordNestMenu.done_cb = done_cb
|
||||
is_commit = True
|
||||
|
||||
if words:
|
||||
WordNestMenu.words = words
|
||||
|
||||
if not items:
|
||||
ch = letter_choices(prefix)
|
||||
if menu_cbf:
|
||||
items = [MenuItem(i, f=menu_cbf) for i in ch]
|
||||
else:
|
||||
items = [MenuItem(i, menu=self.next_menu) for i in ch]
|
||||
items = [MenuItem(i, menu=self.next_menu) for i in letter_choices()]
|
||||
|
||||
self.is_commit = is_commit
|
||||
|
||||
super(WordNestMenu, self).__init__(items)
|
||||
|
||||
@classmethod
|
||||
async def get_n_words(cls, num_words):
|
||||
rv = []
|
||||
for _ in range(num_words):
|
||||
rv = await cls.get_word(rv, num_words)
|
||||
|
||||
return rv
|
||||
|
||||
@classmethod
|
||||
async def get_word(cls, words=None, target_words=None):
|
||||
# Just block until N words are provided. May only work before menus start?
|
||||
from glob import numpad
|
||||
|
||||
async def menu_done_cbf(menu, b, c):
|
||||
# duplicates some of the logic of next_menu
|
||||
if c.label[-1] == '-':
|
||||
lc = c.label[0:-1]
|
||||
else:
|
||||
cls.words.append(c.label)
|
||||
numpad.abort_ux()
|
||||
return
|
||||
|
||||
m = cls(prefix=lc, menu_cbf=menu_done_cbf)
|
||||
the_ux.push(m)
|
||||
await the_ux.interact()
|
||||
|
||||
m = cls(num_words=target_words, menu_cbf=menu_done_cbf, has_checksum=False, words=words)
|
||||
|
||||
the_ux.push(m)
|
||||
await the_ux.interact()
|
||||
|
||||
return cls.words
|
||||
|
||||
@staticmethod
|
||||
async def next_menu(self, idx, choice):
|
||||
|
||||
@ -509,10 +463,6 @@ async def add_seed_to_vault(encoded, origin=None, label=None):
|
||||
if in_seed_vault(encoded):
|
||||
return
|
||||
|
||||
# stay "read only" in hobbled mode
|
||||
if pa.hobbled_mode:
|
||||
return
|
||||
|
||||
main_xfp = settings.master_get("xfp", 0)
|
||||
|
||||
# parse encoded
|
||||
@ -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='',
|
||||
is_restore=False, origin=None, label=None):
|
||||
# Capture tmp seed into vault, if so enabled, and regardless apply it as new tmp.
|
||||
if not is_restore and not_hobbled_mode():
|
||||
if not is_restore:
|
||||
await add_seed_to_vault(encoded, origin=origin, label=label)
|
||||
dis.fullscreen("Wait...")
|
||||
|
||||
applied, err_msg = pa.tmp_secret(encoded, chain=chain, bip39pw=bip39pw)
|
||||
|
||||
# FYI: Might need to bounce the USB connection, because our pubkey has changed,
|
||||
# altho if they have already picked a shared session key, no need, and
|
||||
# would only affect MitM test, which has already been done.
|
||||
|
||||
dis.progress_bar_show(1)
|
||||
|
||||
if not applied:
|
||||
@ -569,10 +515,7 @@ async def set_ephemeral_seed(encoded, chain=None, summarize_ux=True, bip39pw='',
|
||||
|
||||
xfp = "[" + xfp2str(settings.get("xfp", 0)) + "]"
|
||||
if summarize_ux:
|
||||
msg = "New temporary master key is in effect now."
|
||||
if bip39pw:
|
||||
msg += "\n\nPassphrase: %s" % bip39pw
|
||||
await ux_show_story(title=xfp, msg=msg)
|
||||
await ux_show_story(title=xfp, msg="New temporary master key is in effect now.")
|
||||
|
||||
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):
|
||||
# Returns (new) encoded secret, new xfp, old xfp
|
||||
from glob import dis, settings
|
||||
|
||||
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):
|
||||
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,
|
||||
origin="BIP-39 Passphrase on [%s]" % xfp2str(parent_xfp))
|
||||
|
||||
dis.draw_status(bip39=int(bool(pw)), xfp=xfp, tmp=1)
|
||||
|
||||
return ret
|
||||
|
||||
# Might need to bounce the USB connection, because our pubkey has changed,
|
||||
# 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():
|
||||
# Compute current xprv and switch to using that as root secret.
|
||||
from nvstore import SettingsObject
|
||||
@ -794,7 +737,7 @@ async def remember_ephemeral_seed():
|
||||
# address cache, settings from tmp seeds / seedvault seeds
|
||||
# rebuild fs as we want to save current tmp settings immediately
|
||||
from files import wipe_flash_filesystem
|
||||
wipe_flash_filesystem()
|
||||
wipe_flash_filesystem(True)
|
||||
|
||||
dis.draw_status(bip39=0, tmp=0)
|
||||
dis.fullscreen('Saving...')
|
||||
@ -825,6 +768,12 @@ def clear_seed():
|
||||
callgate.fast_wipe(True)
|
||||
# NOT REACHED
|
||||
|
||||
utime.sleep(1)
|
||||
|
||||
# security: need to reboot to really be sure to clear the secrets from main memory.
|
||||
from machine import reset
|
||||
reset()
|
||||
|
||||
async def word_quiz(words, limited=None, title='Word %d is?'):
|
||||
# Perform a test, to check they wrote them down
|
||||
# Return X if they cancel early.
|
||||
@ -930,8 +879,6 @@ class SeedVaultMenu(MenuSystem):
|
||||
ch = await ux_show_story(title="[" + rec.xfp + "]", msg=msg, escape=esc)
|
||||
if ch == "x": return
|
||||
|
||||
assert not_hobbled_mode()
|
||||
|
||||
dis.fullscreen("Saving...")
|
||||
|
||||
wipe_slot = not current_active and (ch != "1")
|
||||
@ -943,7 +890,6 @@ class SeedVaultMenu(MenuSystem):
|
||||
xs.blank()
|
||||
del xs
|
||||
|
||||
|
||||
# CAUTION: will get shadow copy if in tmp seed mode already
|
||||
seeds = settings.master_get("seeds", [])
|
||||
try:
|
||||
@ -980,8 +926,6 @@ class SeedVaultMenu(MenuSystem):
|
||||
from glob import dis
|
||||
from ux import ux_input_text
|
||||
|
||||
assert not_hobbled_mode()
|
||||
|
||||
idx, old = item.arg
|
||||
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):
|
||||
from pincodes import pa
|
||||
|
||||
assert not_hobbled_mode()
|
||||
|
||||
assert pa.tmp_value
|
||||
main_xfp = settings.master_get("xfp", 0)
|
||||
|
||||
@ -1056,10 +998,9 @@ class SeedVaultMenu(MenuSystem):
|
||||
|
||||
if not seeds:
|
||||
rv.append(MenuItem('(none saved yet)'))
|
||||
if not_hobbled_mode():
|
||||
if pa.tmp_value:
|
||||
rv.append(add_current_tmp)
|
||||
rv.append(MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu))
|
||||
if pa.tmp_value:
|
||||
rv.append(add_current_tmp)
|
||||
rv.append(MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu))
|
||||
else:
|
||||
wipe_if_deltamode()
|
||||
|
||||
@ -1075,10 +1016,8 @@ class SeedVaultMenu(MenuSystem):
|
||||
submenu = [
|
||||
MenuItem(rec.label, f=cls._detail, arg=(rec, encoded)),
|
||||
MenuItem('Use This Seed', f=cls._set, arg=encoded),
|
||||
MenuItem('Rename', f=cls._rename, arg=(i, rec),
|
||||
predicate=not_hobbled_mode),
|
||||
MenuItem('Delete', f=cls._remove, arg=(i, rec, encoded),
|
||||
predicate=not_hobbled_mode),
|
||||
MenuItem('Rename', f=cls._rename, arg=(i, rec)),
|
||||
MenuItem('Delete', f=cls._remove, arg=(i, rec, encoded)),
|
||||
]
|
||||
if is_active:
|
||||
submenu[1] = MenuItem("Seed In Use")
|
||||
@ -1096,7 +1035,7 @@ class SeedVaultMenu(MenuSystem):
|
||||
rv.append(item)
|
||||
|
||||
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
|
||||
rv.append(add_current_tmp)
|
||||
|
||||
@ -1169,7 +1108,6 @@ class EphemeralSeedMenu(MenuSystem):
|
||||
from actions import nfc_recv_ephemeral, import_xprv
|
||||
from actions import restore_backup, scan_any_qr
|
||||
from tapsigner import import_tapsigner_backup_file
|
||||
from xor_seed import xor_restore_temporary
|
||||
from charcodes import KEY_QR
|
||||
|
||||
import_ephemeral_menu = [
|
||||
@ -1186,21 +1124,19 @@ class EphemeralSeedMenu(MenuSystem):
|
||||
]
|
||||
|
||||
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,
|
||||
shortcut=KEY_QR, f=scan_any_qr, arg=(True, True)),
|
||||
MenuItem("Import Words", menu=import_ephemeral_menu),
|
||||
MenuItem("Import XPRV", f=import_xprv, arg=True), # ephemeral=True
|
||||
MenuItem("Tapsigner Backup", f=import_tapsigner_backup_file, arg=True), # ephemeral=True
|
||||
MenuItem("Coldcard Backup", f=restore_backup, arg=True), # tmp=True
|
||||
MenuItem("Restore Seed XOR", f=xor_restore_temporary),
|
||||
]
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
async def make_ephemeral_seed_menu(*a):
|
||||
|
||||
if (not pa.tmp_value) and (not settings.master_get("seedvault", False)):
|
||||
# force a warning on them, unless they are already doing it.
|
||||
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, \
|
||||
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.
|
||||
''' % (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')
|
||||
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():
|
||||
# no need for any menus if Q and no card present
|
||||
pp = await ux_input_text('', prompt="Your BIP-39 Passphrase", b39_complete=True,
|
||||
scan_ok=True, max_len=MAX_PASS_LEN)
|
||||
pp = await ux_input_text('', prompt="Your BIP-39 Passphrase",
|
||||
b39_complete=True, scan_ok=True, max_len=100)
|
||||
if not pp: return
|
||||
|
||||
await apply_pass_value(pp)
|
||||
@ -1262,7 +1198,7 @@ Limitations: %d characters max length, ASCII characters 32-126 (0x20-0x7e) only.
|
||||
|
||||
|
||||
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
|
||||
done_cb = None
|
||||
@ -1351,7 +1287,7 @@ class PassphraseMenu(MenuSystem):
|
||||
async def view_edit_phrase(cls, *a):
|
||||
# let them control each character
|
||||
pw = await ux_input_text(cls.pp_sofar, prompt="Your BIP-39 Passphrase",
|
||||
b39_complete=True, scan_ok=True, max_len=MAX_PASS_LEN)
|
||||
b39_complete=True, scan_ok=True, max_len=100)
|
||||
if pw is not None:
|
||||
cls.pp_sofar = pw
|
||||
cls.check_length()
|
||||
@ -1362,8 +1298,8 @@ class PassphraseMenu(MenuSystem):
|
||||
|
||||
@classmethod
|
||||
def check_length(cls):
|
||||
# enforce a limit of MAX_PASS_LEN chars
|
||||
cls.pp_sofar = cls.pp_sofar[0:MAX_PASS_LEN]
|
||||
# enforce a limit of 100 chars
|
||||
cls.pp_sofar = cls.pp_sofar[0:100]
|
||||
|
||||
@classmethod
|
||||
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'
|
||||
' created by adding passphrase to %s.'
|
||||
'\n\nPassphrase: %s'
|
||||
'\n\nPress %s to abort, %s to use the new wallet, (1) to apply'
|
||||
' and save to MicroSD for future.') % (msg, 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')
|
||||
if ch == 'x':
|
||||
|
||||
@ -171,7 +171,7 @@ async def test_secure_element():
|
||||
|
||||
dis.clear()
|
||||
|
||||
if version.has_qwerty or version.mk_num == 5:
|
||||
if version.has_qwerty:
|
||||
dis.text(0, 0, "^^-- Green? " if gg else " ^^-- Red?")
|
||||
else:
|
||||
if gg:
|
||||
@ -364,7 +364,7 @@ async def test_microsd():
|
||||
|
||||
with CardSlot(slot_b=slot_num) as card:
|
||||
|
||||
fn, _ = card.pick_filename('test-delme.txt')
|
||||
_, fn = card.pick_filename('test-delme.txt')
|
||||
|
||||
with open(fn, 'wt') as fd:
|
||||
fd.write("Hello")
|
||||
|
||||
@ -19,7 +19,6 @@ from ubinascii import hexlify as b2a_hex
|
||||
import ustruct as struct
|
||||
import ngu
|
||||
from opcodes import *
|
||||
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2SH, AF_P2WSH, AF_P2TR, AF_BARE_PK
|
||||
|
||||
# single-shot hash functions
|
||||
sha256 = ngu.hash.sha256s
|
||||
@ -27,8 +26,8 @@ ripemd160 = ngu.hash.ripemd160
|
||||
hash256 = ngu.hash.sha256d
|
||||
hash160 = ngu.hash.hash160
|
||||
|
||||
#def bytes_to_hex_str(s):
|
||||
# return str(b2a_hex(s), 'ascii')
|
||||
def bytes_to_hex_str(s):
|
||||
return str(b2a_hex(s), 'ascii')
|
||||
|
||||
SIGHASH_ALL = const(1)
|
||||
SIGHASH_NONE = const(2)
|
||||
@ -195,43 +194,41 @@ def disassemble(script):
|
||||
|
||||
try:
|
||||
offset = 0
|
||||
slen = len(script)
|
||||
while 1:
|
||||
if offset >= slen:
|
||||
if offset >= len(script):
|
||||
#print('dis %d done' % offset)
|
||||
return
|
||||
c = script[offset]
|
||||
offset += 1
|
||||
|
||||
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:
|
||||
# OP_1 thru OP_16
|
||||
#print('dis %d: number=%d' % (offset, (c - OP_1 + 1)))
|
||||
yield (c - OP_1 + 1, None)
|
||||
continue
|
||||
elif c == OP_PUSHDATA1:
|
||||
cnt = script[offset]
|
||||
offset += 1
|
||||
yield (script[offset:offset+cnt], None)
|
||||
offset += cnt
|
||||
elif c == OP_PUSHDATA2:
|
||||
# up to 65535 bytes
|
||||
cnt, = struct.unpack_from("H", script, offset)
|
||||
offset += 2
|
||||
yield (script[offset:offset+cnt], None)
|
||||
offset += cnt
|
||||
elif c == OP_PUSHDATA4:
|
||||
# no where to put so much data
|
||||
raise NotImplementedError
|
||||
elif c == OP_1NEGATE:
|
||||
yield (-1, None)
|
||||
continue
|
||||
else:
|
||||
# OP_0 included here
|
||||
#print('dis %d: opcode=%d' % (offset, c))
|
||||
yield (None, c)
|
||||
continue
|
||||
|
||||
# a data push of `cnt` bytes - reject if it runs off the end
|
||||
if offset + cnt > slen:
|
||||
raise ValueError
|
||||
yield (script[offset:offset+cnt], None)
|
||||
offset += cnt
|
||||
except Exception as e:
|
||||
# import sys;sys.print_exception(e)
|
||||
raise ValueError("bad script")
|
||||
@ -322,6 +319,7 @@ class CTxIn(object):
|
||||
self.nSequence = nSequence
|
||||
|
||||
def deserialize(self, f):
|
||||
self.prevout = COutPoint()
|
||||
self.prevout.deserialize(f)
|
||||
self.scriptSig = deser_string(f)
|
||||
self.nSequence = struct.unpack("<I", f.read(4))[0]
|
||||
@ -357,32 +355,26 @@ class CTxOut(object):
|
||||
# (addr_type_code, addr, is_segwit)
|
||||
# 'addr' is byte string, either 20 or 32 long
|
||||
if self.is_p2tr():
|
||||
return AF_P2TR, self.scriptPubKey[2:2+32], True
|
||||
return 'p2tr', self.scriptPubKey[2:2+32], True
|
||||
|
||||
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():
|
||||
return AF_P2WSH, self.scriptPubKey[2:2+32], True
|
||||
return 'p2sh', self.scriptPubKey[2:2+32], True
|
||||
|
||||
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():
|
||||
# can be:
|
||||
# * bare P2SH
|
||||
# * P2SH-P2WPKH
|
||||
# * P2SH-P2WSH
|
||||
return AF_P2SH, self.scriptPubKey[2:2+20], False
|
||||
return 'p2sh', self.scriptPubKey[2:2+20], False
|
||||
|
||||
if self.is_p2pk():
|
||||
# rare, pay to full pubkey: <push_op> <pubkey> OP_CHECKSIG
|
||||
# push_op is 0x21 (33) for compressed, 0x41 (65) for uncompressed
|
||||
pk_len = self.scriptPubKey[0]
|
||||
return AF_BARE_PK, self.scriptPubKey[1:1+pk_len], False
|
||||
# rare, pay to full pubkey
|
||||
return 'p2pk', self.scriptPubKey[2:2+33], False
|
||||
|
||||
if self.is_op_return():
|
||||
return OP_RETURN, self.scriptPubKey, False
|
||||
if self.scriptPubKey[0] == OP_RETURN:
|
||||
return 'op_return', self.scriptPubKey, False
|
||||
|
||||
return None, self.scriptPubKey, None
|
||||
|
||||
@ -409,11 +401,8 @@ class CTxOut(object):
|
||||
|
||||
def is_p2pk(self):
|
||||
return (len(self.scriptPubKey) == 35 or len(self.scriptPubKey) == 67) \
|
||||
and self.scriptPubKey[0] == len(self.scriptPubKey) - 2 \
|
||||
and self.scriptPubKey[-1] == OP_CHECKSIG
|
||||
|
||||
def is_op_return(self):
|
||||
return self.scriptPubKey and (self.scriptPubKey[0] == OP_RETURN)
|
||||
and (self.scriptPubKey[0] == 0x21 or self.scriptPubKey[0] == 0x41) \
|
||||
and self.scriptPubKey[-1] == 0xac
|
||||
|
||||
#def __repr__(self):
|
||||
# return "CTxOut(nValue=%d scriptPubKey=%s)" \
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
#
|
||||
# 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
|
||||
# - only a few of these are possible
|
||||
# - 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.
|
||||
#
|
||||
# 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
|
||||
#
|
||||
@ -28,81 +28,49 @@ SET_VCOM_DESEL = const(0xdb)
|
||||
SET_CHARGE_PUMP = const(0x8d)
|
||||
|
||||
# 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):
|
||||
def __init__(self, width, height, is_mk5):
|
||||
def __init__(self, width, height, external_vcc):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.is_mk5 = is_mk5
|
||||
self.external_vcc = external_vcc
|
||||
self.pages = self.height // 8
|
||||
|
||||
#self.buffer = bytearray(self.pages * self.width)
|
||||
|
||||
self.buffer = bytearray(1024)
|
||||
#assert len(self.buffer) == self.pages * self.width
|
||||
assert len(self.buffer) == self.pages * self.width
|
||||
|
||||
super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
|
||||
self.init_display()
|
||||
|
||||
def init_display(self):
|
||||
if not self.is_mk5:
|
||||
# Mk4 and earlier
|
||||
cmds = (
|
||||
SET_DISP | 0x00, # display off
|
||||
# address setting
|
||||
SET_MEM_ADDR, 0x00, # horizontal
|
||||
# resolution and layout
|
||||
SET_DISP_START_LINE | 0x00,
|
||||
SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0
|
||||
SET_MUX_RATIO, self.height - 1,
|
||||
SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0
|
||||
SET_DISP_OFFSET, 0x00,
|
||||
SET_COM_PIN_CFG, 0x12,
|
||||
# timing and driving scheme
|
||||
SET_DISP_CLK_DIV, 0xF0,
|
||||
SET_PRECHARGE, 0xf1,
|
||||
SET_VCOM_DESEL, 0x30, # 0.83*Vcc
|
||||
# display
|
||||
SET_CONTRAST, 0xff, # maximum
|
||||
SET_ENTIRE_ON, # output follows RAM contents
|
||||
SET_NORM_INV, # not inverted
|
||||
# charge pump
|
||||
SET_CHARGE_PUMP, 0x14)
|
||||
else:
|
||||
# Mk5 has external +12v power supply, and different setup protocol
|
||||
|
||||
cmds = (
|
||||
SET_DISP | 0x00, # display off
|
||||
# address setting
|
||||
SET_MEM_ADDR, 0x00, # horizontal
|
||||
# resolution and layout
|
||||
SET_DISP_START_LINE | 0x00,
|
||||
SET_SEG_REMAP | 0x00, # column addr 0 mapped to SEG127
|
||||
SET_MUX_RATIO, self.height - 1,
|
||||
SET_COM_OUT_DIR | 0x00, # scan from COM[8] to COM[N]
|
||||
SET_DISP_OFFSET, 0x00,
|
||||
SET_COM_PIN_CFG, 0x12,
|
||||
# timing and driving scheme
|
||||
SET_DISP_CLK_DIV, 0xF0,
|
||||
SET_PRECHARGE, 0x22,
|
||||
SET_VCOM_DESEL, 0x40, # per spec sheet
|
||||
# display
|
||||
SET_CONTRAST, 0x85, # NOT maximum, because spec sheet
|
||||
SET_ENTIRE_ON, # output follows RAM contents
|
||||
SET_NORM_INV, # not inverted
|
||||
SET_CHARGE_PUMP, 0x10, # charge pump: DISABLE
|
||||
)
|
||||
|
||||
self.write_cmds(cmds)
|
||||
|
||||
for cmd in (
|
||||
SET_DISP | 0x00, # off
|
||||
# address setting
|
||||
SET_MEM_ADDR, 0x00, # horizontal
|
||||
# resolution and layout
|
||||
SET_DISP_START_LINE | 0x00,
|
||||
SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0
|
||||
SET_MUX_RATIO, self.height - 1,
|
||||
SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0
|
||||
SET_DISP_OFFSET, 0x00,
|
||||
SET_COM_PIN_CFG, 0x02 if self.height == 32 else 0x12,
|
||||
# timing and driving scheme
|
||||
SET_DISP_CLK_DIV, 0xF0,
|
||||
SET_PRECHARGE, 0x22 if self.external_vcc else 0xf1,
|
||||
SET_VCOM_DESEL, 0x30, # 0.83*Vcc
|
||||
# display
|
||||
SET_CONTRAST, 0xff, # maximum
|
||||
SET_ENTIRE_ON, # output follows RAM contents
|
||||
SET_NORM_INV, # not inverted
|
||||
# charge pump
|
||||
SET_CHARGE_PUMP, 0x10 if self.external_vcc else 0x14,
|
||||
SET_DISP | 0x01): # on
|
||||
self.write_cmd(cmd)
|
||||
self.fill(0)
|
||||
self.show()
|
||||
|
||||
self.write_cmd(SET_DISP | 0x01)
|
||||
|
||||
def write_cmds(self, cmds):
|
||||
for c in cmds:
|
||||
self.write_cmd(c)
|
||||
|
||||
def poweroff(self):
|
||||
self.write_cmd(SET_DISP | 0x00)
|
||||
|
||||
@ -110,10 +78,6 @@ class SSD1306(framebuf.FrameBuffer):
|
||||
self.write_cmd(SET_DISP | 0x01)
|
||||
|
||||
def contrast(self, contrast):
|
||||
# brightness: normal = 0x7f, brightness=0xff, dim=0x00 (but they are all very similar)
|
||||
if self.is_mk5:
|
||||
# - limit to a specific max value from OLED specs used on Mk5
|
||||
contrast = max(contrast, 0x85)
|
||||
self.write_cmd(SET_CONTRAST)
|
||||
self.write_cmd(contrast)
|
||||
|
||||
@ -121,113 +85,56 @@ class SSD1306(framebuf.FrameBuffer):
|
||||
self.write_cmd(SET_NORM_INV | (invert & 1))
|
||||
|
||||
def show(self):
|
||||
x0 = 0
|
||||
x1 = self.width - 1
|
||||
if self.width == 64:
|
||||
# displays with width of 64 pixels are shifted by 32
|
||||
x0 += 32
|
||||
x1 += 32
|
||||
self.write_cmd(SET_COL_ADDR)
|
||||
self.write_cmd(0)
|
||||
self.write_cmd(self.width - 1)
|
||||
|
||||
self.write_cmd(x0)
|
||||
self.write_cmd(x1)
|
||||
self.write_cmd(SET_PAGE_ADDR)
|
||||
self.write_cmd(0)
|
||||
self.write_cmd(self.pages - 1)
|
||||
|
||||
self.write_data(self.buffer)
|
||||
|
||||
def busy_bar(self, enable, pattern):
|
||||
# Render a continuous activity (not progress) bar in lower 8 lines of display
|
||||
# - using OLED itself to do the animation, so smooth and CPU free
|
||||
# - cannot preserve bottom 8 lines, since we have to destructively write there
|
||||
# - assumes normal horz addr mode: 0x20, 0x00
|
||||
# - speed_code=>framedelay: 0=5fr, 1=64fr, 2=128, 3=256, 4=3, 5=4, 6=25, 7=2frames
|
||||
# unused: assert 0 <= speed_code <= 7
|
||||
|
||||
setup = bytes([
|
||||
0x21, 0x00, 0x7f, # setup column address range (start, end): 0-127
|
||||
0x22, 7, 7, # setup page start/end address: page 7=last 8 lines
|
||||
])
|
||||
if not self.is_mk5:
|
||||
animate = bytes([
|
||||
0x2e, # stop animations in progress
|
||||
0x26, # scroll leftwards (stock ticker mode)
|
||||
0, # placeholder
|
||||
7, # start 'page' (vertical)
|
||||
5, # "speed_code" # scroll speed: 7=fastest, but no order to it
|
||||
7, # end 'page'
|
||||
0, 0x7f, # start/end columns
|
||||
0x2f # start
|
||||
])
|
||||
else:
|
||||
# SSD1309? doesn't implement 0x26 but has other commands
|
||||
animate = bytes([
|
||||
0x2e, # stop animations in progress
|
||||
0x29, # Vert+Right horz animation setup
|
||||
1, # A: enable horz scroll
|
||||
7, # B: start 'page' (vertical)
|
||||
5, # C: "speed_code" # scroll speed: 7=fastest, but no order to it
|
||||
7, # D: end 'page'
|
||||
1, # E: vert scrolling offset (unused)
|
||||
0, 0x7f, # F,G: start/end columns
|
||||
0xa3, # Set Vertical scroll Area
|
||||
0, 0, # A, B: # of rows in fixed vs. scroll area
|
||||
0x2f # start animating
|
||||
])
|
||||
|
||||
cleanup = bytes([
|
||||
0x2e, # stop animation
|
||||
0x20, 0x00, # horz addr-ing mode
|
||||
0x21, 0x00, 0x7f, # setup column address range (start, end): 0-127
|
||||
0x22, 7, 7, # setup page start/end address: page 7=last 8 lines
|
||||
])
|
||||
|
||||
if not enable:
|
||||
# stop animation, and redraw old (new) screen
|
||||
self.write_cmds(cleanup)
|
||||
else:
|
||||
# needs a pattern that repeats nicely mod 128
|
||||
self.write_cmds(setup)
|
||||
self.write_data(pattern)
|
||||
self.write_cmds(animate)
|
||||
SPI_RATE = const(40000000) # max chip can do, still slower than display limit tho
|
||||
|
||||
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.dc = dc
|
||||
self.cs = cs
|
||||
self.res = res
|
||||
|
||||
# initial states
|
||||
dc(0)
|
||||
cs(1)
|
||||
|
||||
# reset sequence
|
||||
res(1)
|
||||
self.cs = cs
|
||||
self.res(1)
|
||||
time.sleep_ms(1)
|
||||
res(0)
|
||||
self.res(0)
|
||||
time.sleep_ms(10)
|
||||
res(1)
|
||||
|
||||
super().__init__(width, height, is_mk5)
|
||||
|
||||
def _setup_spi(self):
|
||||
# need to re-do this constantly
|
||||
# max chip can do, still slower than display limit tho
|
||||
# - 40Mhz (target) is fine for short-cabled Mk4 (actual is lower?)
|
||||
# - max spec is 10Mhz on Mk5
|
||||
rate = 40_000_000 if not self.is_mk5 else 10_000_000
|
||||
self.spi.init(baudrate=rate, polarity=0, phase=0)
|
||||
self.res(1)
|
||||
super().__init__(width, height, external_vcc)
|
||||
|
||||
def write_cmd(self, cmd):
|
||||
self._setup_spi()
|
||||
self.spi.init(baudrate=SPI_RATE, polarity=0, phase=0)
|
||||
self.cs(1)
|
||||
self.dc(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)
|
||||
|
||||
def write_data(self, buf):
|
||||
self._setup_spi()
|
||||
self.spi.init(baudrate=SPI_RATE, polarity=0, phase=0)
|
||||
self.cs(1)
|
||||
self.dc(1)
|
||||
self.cs(0)
|
||||
self.spi.write(buf)
|
||||
try:
|
||||
self.spi.write(buf)
|
||||
except:
|
||||
print("SPI[data]: %r" % self.spi)
|
||||
self.cs(1)
|
||||
|
||||
# EOF
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
#
|
||||
import ngu, uctypes, gc, bip39, utime
|
||||
from uhashlib import sha256
|
||||
from utils import swab32, call_later_ms, B2A, node_from_privkey
|
||||
from utils import swab32, call_later_ms, B2A
|
||||
|
||||
|
||||
SEED_LEN_OPTS = [12, 18, 24]
|
||||
@ -104,7 +104,7 @@ class SecretStash:
|
||||
ch, pk = secret[1:33], secret[33:65]
|
||||
assert not _bip39pw
|
||||
|
||||
hd = node_from_privkey(pk, ch)
|
||||
hd.from_chaincode_privkey(ch, pk)
|
||||
return 'xprv', ch+pk, hd
|
||||
|
||||
elif marker & 0x80:
|
||||
@ -403,7 +403,8 @@ class SensitiveValues:
|
||||
self.register(cc)
|
||||
self.register(pk)
|
||||
|
||||
rv = node_from_privkey(pk, cc)
|
||||
rv = ngu.hdnode.HDNode()
|
||||
rv.from_chaincode_privkey(cc, pk)
|
||||
self.register(rv)
|
||||
|
||||
return rv, p
|
||||
|
||||
@ -67,7 +67,7 @@ async def import_tapsigner_backup_file(_1, _2, item):
|
||||
continue
|
||||
break
|
||||
else:
|
||||
fn = await file_picker(suffix=".aes", min_size=100, max_size=160, **choice)
|
||||
fn = await file_picker(suffix="aes", min_size=100, max_size=160, **choice)
|
||||
if not fn: return
|
||||
origin += (" (%s)" % fn)
|
||||
try:
|
||||
|
||||
@ -307,17 +307,11 @@ async def kt_accept_values(dtype, raw):
|
||||
- `b` - complete system backup file (text, internal format)
|
||||
'''
|
||||
from flow import has_se_secrets, goto_top_menu
|
||||
from pincodes import pa
|
||||
|
||||
enc = None
|
||||
origin = 'Teleported'
|
||||
label = None
|
||||
|
||||
if pa.hobbled_mode and dtype != 'p':
|
||||
await ux_show_story('Only PSBT for multisig accepted in this mode.', title='FAILED')
|
||||
return
|
||||
|
||||
|
||||
if dtype == 's':
|
||||
# words / bip 32 master / xprv, etc
|
||||
enc = bytearray(72)
|
||||
@ -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
|
||||
# XXX no way to send this .. but was thinking of address explorer
|
||||
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'
|
||||
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
|
||||
# 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
|
||||
|
||||
elif dtype == 'b':
|
||||
# full system backup, including master: text lines
|
||||
from backups import text_bk_parser, restore_tmp_from_dict_ll, restore_from_dict, extract_raw_secret
|
||||
from backups import text_bk_parser, restore_tmp_from_dict_ll, restore_from_dict
|
||||
|
||||
try:
|
||||
vals = text_bk_parser(raw)
|
||||
assert vals # empty?
|
||||
raw_sec, _ = extract_raw_secret(vals)
|
||||
except Exception as e:
|
||||
await ux_show_story("Invalid backup\n\n" + str(e), title='FAILED')
|
||||
return
|
||||
vals = text_bk_parser(raw)
|
||||
assert vals # empty?
|
||||
|
||||
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
|
||||
# so even if this errors out, new ktrx is needed
|
||||
settings.remove_key("ktrx")
|
||||
prob = await restore_tmp_from_dict_ll(vals, raw_sec)
|
||||
prob = await restore_tmp_from_dict_ll(vals)
|
||||
else:
|
||||
# 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:
|
||||
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):
|
||||
# incoming BBQr was scanned (via main menu, etc)
|
||||
|
||||
from pincodes import pa
|
||||
if pa.hobbled_mode and type_code != 'E':
|
||||
# only PSBT rx is supported in hobbled mode
|
||||
# fail silently, this is second check, see decoders.py
|
||||
return
|
||||
|
||||
if type_code == 'R':
|
||||
# they want to send to this guy
|
||||
return await kt_start_send(payload)
|
||||
@ -512,10 +495,6 @@ class SecretPickerMenu(MenuSystem):
|
||||
def __init__(self, rx_pubkey):
|
||||
self.rx_pubkey = rx_pubkey
|
||||
|
||||
# this menu should be unreachable in hobbled mode.
|
||||
from pincodes import pa
|
||||
assert not pa.hobbled_mode
|
||||
|
||||
from flow import word_based_seed, is_tmp, has_se_secrets
|
||||
has_notes = bool(NoteContentBase.count())
|
||||
has_sv = bool(settings.get('seedvault', False))
|
||||
@ -641,7 +620,7 @@ class SecretPickerMenu(MenuSystem):
|
||||
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.
|
||||
# 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?")
|
||||
return
|
||||
|
||||
# (TXN_OUTPUT_OFFSET after signing, TXN_INPUT_OFFSET for the file-teleport path)
|
||||
with SFFile(psbt_offset, psbt_len) as fd:
|
||||
# move out of PSRAM
|
||||
from auth import TXN_OUTPUT_OFFSET
|
||||
|
||||
with SFFile(TXN_OUTPUT_OFFSET, psbt_len) as fd:
|
||||
bin_psbt = fd.read(psbt_len)
|
||||
|
||||
my_xfp = settings.get('xfp')
|
||||
@ -685,12 +666,12 @@ async def kt_send_psbt(psbt, psbt_len, psbt_offset):
|
||||
f = None
|
||||
if x in need:
|
||||
# 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):
|
||||
# this will reset the UX stack:
|
||||
# flags=None --> whether to finalize is decided based on psbt.is_complete
|
||||
sign_transaction(psbt_len, flags=None, input_method="kt", offset=psbt_offset)
|
||||
sign_transaction(psbt_len, flags=None)
|
||||
|
||||
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)
|
||||
if picked == KEY_CANCEL:
|
||||
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)
|
||||
if not choices:
|
||||
# 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")
|
||||
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
|
||||
|
||||
@ -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 stash import SecretStash
|
||||
from drv_entro import bip85_derive
|
||||
from utils import node_from_privkey
|
||||
|
||||
# see from mk4-bootloader/se2.h
|
||||
NUM_TRICKS = const(14)
|
||||
@ -33,7 +32,7 @@ TC_WORD_WALLET = const(0x1000)
|
||||
TC_XPRV_WALLET = const(0x0800)
|
||||
TC_DELTA_MODE = const(0x0400)
|
||||
TC_REBOOT = const(0x0200)
|
||||
TC_FW_DEFINED = const(0x0100)
|
||||
TC_RFU = const(0x0100)
|
||||
# for our use, not implemented in bootrom
|
||||
TC_BLANK_WALLET = const(0x0080)
|
||||
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_WORD_WALLET -> BIP-85 index, 1001..1003 for 24 words, 2001..2003 for 12-words
|
||||
|
||||
# If TC_FW_DEFINED is true, then we can do anything with this PIN at the firmware
|
||||
# level. First application is to unlock spending stuff.
|
||||
TCA_SP_UNLOCK = const(0x0001) # spending policy unlock
|
||||
|
||||
# special "pin" used as catch-all for wrong pins
|
||||
WRONG_PIN_CODE = '!p'
|
||||
|
||||
@ -211,14 +206,14 @@ class TrickPinMgmt:
|
||||
def update_slot(self, pin, new=False, new_pin=None, tc_flags=None, tc_arg=None, secret=None):
|
||||
# create or update a trick pin
|
||||
# - 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)
|
||||
|
||||
b, slot = self.get_by_pin(pin)
|
||||
if not slot:
|
||||
assert new, "wrong pin"
|
||||
if not new: raise KeyError("wrong pin")
|
||||
|
||||
# Making a new entry
|
||||
b, slot = make_slot()
|
||||
@ -279,10 +274,6 @@ class TrickPinMgmt:
|
||||
# put them in order, with "wrong" last
|
||||
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):
|
||||
# was the trick pin just used? if so how much delay needed (or zero if not)
|
||||
from pincodes import pa
|
||||
@ -293,32 +284,6 @@ class TrickPinMgmt:
|
||||
else:
|
||||
return 0
|
||||
|
||||
def was_sp_unlock(self):
|
||||
# was a trick pin just used that enables acess to spending policy?
|
||||
# - ok if it's also a trick PIN .. a wiping bypass for example
|
||||
from pincodes import pa
|
||||
tc_flags, tc_arg = pa.get_tc_values()
|
||||
return bool(tc_flags & TC_FW_DEFINED) and (tc_arg == TCA_SP_UNLOCK)
|
||||
|
||||
def has_sp_unlock(self):
|
||||
# if spending policy defined, this PIN allows adjustment
|
||||
# - not TRICK bypass choices, like ones that wipe
|
||||
# - could be multiple, but only first returned.
|
||||
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):
|
||||
# iterate over all delta-mode PIN's defined.
|
||||
for k, (sn,flags,args) in self.tp.items():
|
||||
@ -398,7 +363,7 @@ class TrickPinMgmt:
|
||||
continue
|
||||
|
||||
if flags & TC_DELTA_MODE:
|
||||
prob, arg = validate_delta_pin(true_pin, pin)
|
||||
prob = validate_delta_pin(true_pin, pin)
|
||||
if prob:
|
||||
# just forget it, no UI here to report issue
|
||||
continue
|
||||
@ -407,16 +372,10 @@ class TrickPinMgmt:
|
||||
# might need to construct a BIP-85 or XPRV secret to match
|
||||
path, new_secret = construct_duress_secret(flags, arg)
|
||||
|
||||
tp.update_slot(pin.encode(), new=True, secret=new_secret,
|
||||
tc_flags=flags, tc_arg=arg)
|
||||
b, slot = tp.update_slot(pin.encode(), new=True,
|
||||
tc_flags=flags, tc_arg=arg, secret=new_secret)
|
||||
except: pass
|
||||
|
||||
@staticmethod
|
||||
async def err_unique_pin(pin):
|
||||
# standardized error UX
|
||||
return await ux_show_story(
|
||||
"That PIN (%s) is already in use. All PIN codes must be unique." % pin)
|
||||
|
||||
|
||||
tp = TrickPinMgmt()
|
||||
|
||||
@ -561,7 +520,8 @@ class TrickPinMenu(MenuSystem):
|
||||
have.remove(existing_pin)
|
||||
|
||||
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.
|
||||
# - 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 \
|
||||
differ only in final 4 positions (ignoring dash).\
|
||||
''', flags=TC_DELTA_MODE),
|
||||
StoryMenuItem('Policy Unlock', "Adds (another?) Spending Policy unlock PIN.", flags=TC_FW_DEFINED, arg=TCA_SP_UNLOCK),
|
||||
StoryMenuItem('Policy Unlock & Wipe' if version.has_qwerty else 'P.U. & Wipe',
|
||||
"Pretends correct Spending Policy unlock PIN given, but silently wipes seed before asking for main PIN.", flags=TC_FW_DEFINED|TC_WIPE, arg=TCA_SP_UNLOCK),
|
||||
]
|
||||
m = MenuSystem(FirstMenu)
|
||||
m.goto_idx(1)
|
||||
@ -694,14 +651,9 @@ setting) the Coldcard will always brick after 13 failed PIN attempts.''')
|
||||
the_ux.push(m)
|
||||
|
||||
async def clear_all(self, m,l,item):
|
||||
|
||||
if not await ux_confirm("Remove ALL TRICK PIN codes and special wrong-pin handling?"):
|
||||
return
|
||||
|
||||
if tp.has_sp_unlock():
|
||||
if not await ux_confirm("You will not be able to bypass spending policy anymore."):
|
||||
return
|
||||
|
||||
if any(tp.get_duress_pins()):
|
||||
if not await ux_confirm("Any funds on the duress wallet(s) have been moved already?"):
|
||||
return
|
||||
@ -710,7 +662,7 @@ setting) the Coldcard will always brick after 13 failed PIN attempts.''')
|
||||
m.update_contents()
|
||||
|
||||
async def hide_pin(self, m,l, item):
|
||||
pin, slot_num, flags, arg = item.arg
|
||||
pin, slot_num, flags = item.arg
|
||||
|
||||
if flags & TC_DELTA_MODE:
|
||||
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.''')
|
||||
return
|
||||
|
||||
if (flags == TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
|
||||
msg = "It will still be possible to change or disable the spending policy if this PIN is known."
|
||||
elif pin == WRONG_PIN_CODE:
|
||||
msg = "This will hide what happens with wrong PINs from the menus but it will still be in effect."
|
||||
else:
|
||||
if pin != WRONG_PIN_CODE:
|
||||
msg = '''This will hide the PIN from the menus but it will still be in effect.
|
||||
|
||||
You can restore it by trying to re-add the same PIN (%s) again later.''' % pin
|
||||
else:
|
||||
msg = "This will hide what happens with wrong PINs from the menus but it will still be in effect."
|
||||
|
||||
if not await ux_confirm(msg): return
|
||||
|
||||
@ -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)
|
||||
|
||||
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 not await ux_confirm("Any funds on this duress wallet have been moved already?"):
|
||||
return
|
||||
|
||||
if (flags == TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
|
||||
if not await ux_confirm("Changes to the spending policy will not be possible anymore."):
|
||||
return
|
||||
|
||||
if pin == WRONG_PIN_CODE:
|
||||
msg = "Remove special handling of wrong PINs?"
|
||||
else:
|
||||
@ -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('''\
|
||||
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
|
||||
|
||||
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
|
||||
if flags & TC_XPRV_WALLET:
|
||||
node = ngu.hdnode.HDNode()
|
||||
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)
|
||||
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"))
|
||||
elif flags & TC_DELTA_MODE:
|
||||
rv.append(MenuItem("↳Delta Mode"))
|
||||
elif (flags & TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
|
||||
rv.append(MenuItem("↳Unlock Policy")) # width issues on Mk4
|
||||
|
||||
for m, msg in [
|
||||
(TC_WIPE, '↳Wipes seed'),
|
||||
@ -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.extend([
|
||||
MenuItem('Hide Trick', f=self.hide_pin, arg=(pin, slot_num, flags, arg)),
|
||||
MenuItem('Delete Trick', f=self.delete_pin, arg=(pin, slot_num, flags, arg)),
|
||||
MenuItem('Hide Trick', f=self.hide_pin, arg=(pin, slot_num, flags)),
|
||||
MenuItem('Delete Trick', f=self.delete_pin, arg=(pin, slot_num, flags)),
|
||||
])
|
||||
if pin != WRONG_PIN_CODE:
|
||||
rv.append(
|
||||
@ -961,7 +907,6 @@ Wallet is XPRV-based and derived from a fixed path.''' % pin
|
||||
|
||||
class StoryMenuItem(MenuItem):
|
||||
def __init__(self, label, story, flags=0, **kws):
|
||||
# arg= .. handled by super
|
||||
self.story = story
|
||||
self.flags = flags
|
||||
super().__init__(label, **kws)
|
||||
|
||||
@ -11,8 +11,7 @@ from ustruct import pack, unpack_from
|
||||
from ckcc import watchpoint, is_simulator
|
||||
from utils import problem_file_line, call_later_ms
|
||||
from version import supports_hsm, is_devmode, MAX_TXN_LEN, MAX_UPLOAD_LEN
|
||||
from exceptions import FramingError, CCBusyError, HSMDenied, HSMCMDDisabled, SpendPolicyViolation
|
||||
from pincodes import pa
|
||||
from exceptions import FramingError, CCBusyError, HSMDenied, HSMCMDDisabled
|
||||
|
||||
# Unofficial, unpermissioned... numbers
|
||||
COINKITE_VID = 0xd13e
|
||||
@ -69,14 +68,6 @@ HSM_DISABLE_CMDS = frozenset({
|
||||
"hsms",
|
||||
})
|
||||
|
||||
# spending policy active: blacklist some commands
|
||||
# - 'pass' may be allowed if 'okeys' is enabled
|
||||
HOBBLED_CMDS = frozenset({
|
||||
'enrl', # no new multisigs during policy enforcement
|
||||
'back', # no backups
|
||||
'bagi', 'dfu_', # just in case
|
||||
}) | HSM_DISABLE_CMDS
|
||||
|
||||
# singleton instance of USBHandler()
|
||||
handler = None
|
||||
|
||||
@ -226,8 +217,6 @@ class USBHandler:
|
||||
except CCBusyError:
|
||||
# auth UX is doing something else
|
||||
resp = b'busy'
|
||||
except SpendPolicyViolation:
|
||||
resp = b'err_Spending policy in effect'
|
||||
except HSMDenied:
|
||||
resp = b'err_Not allowed in HSM mode'
|
||||
except HSMCMDDisabled:
|
||||
@ -251,7 +240,7 @@ class USBHandler:
|
||||
# catch bugs and fuzzing too
|
||||
if is_simulator() or is_devmode:
|
||||
print("USB request caused this: ", end='')
|
||||
sys.print_exception(exc)
|
||||
# sys.print_exception(exc)
|
||||
resp = b'err_Confused ' + problem_file_line(exc)
|
||||
|
||||
if not success:
|
||||
@ -356,7 +345,7 @@ class USBHandler:
|
||||
except:
|
||||
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
|
||||
try:
|
||||
from usb_test_commands import do_usb_command
|
||||
@ -369,18 +358,7 @@ class USBHandler:
|
||||
if cmd not in HSM_WHITELIST:
|
||||
raise HSMDenied
|
||||
|
||||
if pa.hobbled_mode:
|
||||
# block some commands when we are hobbled.
|
||||
if cmd in HOBBLED_CMDS:
|
||||
raise SpendPolicyViolation
|
||||
|
||||
if cmd in {'pwok', 'pass'}:
|
||||
from ccc import sssp_spending_policy
|
||||
if not sssp_spending_policy('okeys'):
|
||||
raise SpendPolicyViolation
|
||||
|
||||
elif not settings.get('hsmcmd', False):
|
||||
# block these HSM-related command if not using feature
|
||||
if not settings.get('hsmcmd', False):
|
||||
if cmd in HSM_DISABLE_CMDS:
|
||||
raise HSMCMDDisabled
|
||||
|
||||
@ -416,12 +394,10 @@ class USBHandler:
|
||||
|
||||
if cmd == 'dwld':
|
||||
offset, length, fileno = unpack_from('<III', args)
|
||||
assert len(args) == 12, 'badlen'
|
||||
return await self.handle_download(offset, length, fileno)
|
||||
|
||||
if cmd == 'ncry':
|
||||
version, his_pubkey = unpack_from('<I64s', args)
|
||||
assert len(args) == 68, 'badlen'
|
||||
|
||||
return self.handle_crypto_setup(version, his_pubkey)
|
||||
|
||||
@ -451,9 +427,9 @@ class USBHandler:
|
||||
if cmd == 'smsg':
|
||||
# sign message
|
||||
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]
|
||||
msg = args[12+len_subpath:]
|
||||
assert len(msg) == len_msg, "badlen"
|
||||
|
||||
from auth import sign_msg
|
||||
sign_msg(msg, subpath, addr_fmt)
|
||||
@ -482,7 +458,6 @@ class USBHandler:
|
||||
|
||||
xfp_paths = []
|
||||
for i in range(N):
|
||||
assert offset < len(args), 'badlen'
|
||||
ln = args[offset]
|
||||
assert 1 <= ln <= 16, 'badlen'
|
||||
xfp_paths.append(unpack_from('<%dI' % ln, args, offset+1))
|
||||
@ -498,7 +473,6 @@ class USBHandler:
|
||||
from auth import usb_show_address
|
||||
|
||||
addr_fmt, = unpack_from('<I', args)
|
||||
assert len(args) >= 4, 'badlen'
|
||||
# regression patch of AFC_BECH32M flag
|
||||
# fixed here https://github.com/Coldcard/ckcc-protocol/commit/a6d901f9fca50755835eca895586ca74d0ca81ed
|
||||
if addr_fmt == 0x17: # old P2TR
|
||||
@ -510,7 +484,6 @@ class USBHandler:
|
||||
# - text config file must already be uploaded
|
||||
|
||||
file_len, file_sha = unpack_from('<I32s', args)
|
||||
assert len(args) == 36, 'badlen'
|
||||
if file_sha != self.file_checksum.digest():
|
||||
return b'err_Checksum'
|
||||
assert 100 < file_len <= (20*200), "badlen"
|
||||
@ -525,20 +498,19 @@ class USBHandler:
|
||||
# Quick check to test if we have a wallet already installed.
|
||||
from multisig import MultisigWallet
|
||||
M, N, xfp_xor = unpack_from('<3I', args)
|
||||
assert len(args) == 12, 'badlen'
|
||||
|
||||
return int(MultisigWallet.quick_check(M, N, xfp_xor))
|
||||
|
||||
if cmd == 'stxn':
|
||||
# sign transaction
|
||||
txn_len, flags, txn_sha = unpack_from('<II32s', args)
|
||||
assert len(args) == 40, 'badlen'
|
||||
if txn_sha != self.file_checksum.digest():
|
||||
return b'err_Checksum'
|
||||
|
||||
assert 50 < txn_len <= MAX_TXN_LEN, "badlen"
|
||||
|
||||
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
|
||||
|
||||
if cmd == 'stok' or cmd == 'bkok' or cmd == 'smok' or cmd == 'pwok':
|
||||
@ -563,6 +535,7 @@ class USBHandler:
|
||||
# STILL waiting on user
|
||||
return None
|
||||
|
||||
|
||||
if cmd == 'pwok':
|
||||
# return new root xpub
|
||||
xpub = req.result
|
||||
@ -588,7 +561,6 @@ class USBHandler:
|
||||
assert settings.get("words", True), 'no seed'
|
||||
assert len(args) < 400, 'too long'
|
||||
pw = str(args, 'utf8')
|
||||
assert len(pw), 'too short'
|
||||
assert len(pw) < 100, 'too long'
|
||||
|
||||
return start_bip39_passphrase(pw)
|
||||
@ -598,17 +570,6 @@ class USBHandler:
|
||||
from auth import start_remote_backup
|
||||
return start_remote_backup()
|
||||
|
||||
if cmd == 'rest':
|
||||
# restore backup from what is already uploaded in PSRAM
|
||||
file_len, file_sha, bf = unpack_from('<I32sB', args)
|
||||
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':
|
||||
# report which blockchain we are configured for
|
||||
from chains import current_chain
|
||||
@ -625,7 +586,6 @@ class USBHandler:
|
||||
# HSM mode "start" -- requires user approval
|
||||
if args:
|
||||
file_len, file_sha = unpack_from('<I32s', args)
|
||||
assert len(args) == 36, 'badlen'
|
||||
if file_sha != self.file_checksum.digest():
|
||||
return b'err_Checksum'
|
||||
assert 2 <= file_len <= (200*1000), "badlen"
|
||||
@ -653,8 +613,6 @@ class USBHandler:
|
||||
if cmd == 'nwur': # new user
|
||||
from users import Users
|
||||
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')
|
||||
secret = bytes(args[3+ul:3+ul+sl])
|
||||
|
||||
@ -663,8 +621,6 @@ class USBHandler:
|
||||
if cmd == 'rmur': # delete user
|
||||
from users import Users
|
||||
ul, = unpack_from('<B', args)
|
||||
assert len(args) == (1 + ul), 'badlen'
|
||||
assert ul, "badlen"
|
||||
username = bytes(args[1:1+ul]).decode('ascii')
|
||||
|
||||
return Users.delete(username)
|
||||
@ -672,8 +628,6 @@ class USBHandler:
|
||||
if cmd == 'user': # auth user (HSM mode)
|
||||
from users import Users
|
||||
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')
|
||||
token = bytes(args[6+ul:6+ul+tl])
|
||||
|
||||
@ -762,8 +716,7 @@ class USBHandler:
|
||||
length = min(length, MAX_BLK_LEN)
|
||||
|
||||
assert 0 <= file_number < 2, 'bad fnum'
|
||||
assert 0 <= offset < MAX_TXN_LEN, "bad offset"
|
||||
assert offset + length <= MAX_TXN_LEN, "bad offset"
|
||||
assert 0 <= offset <= MAX_TXN_LEN, "bad offset"
|
||||
assert 1 <= length, 'len'
|
||||
|
||||
# maintain a running SHA256 over what's sent
|
||||
@ -788,6 +741,7 @@ class USBHandler:
|
||||
from glob import dis, hsm_active
|
||||
from utils import check_firmware_hdr
|
||||
from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE, FW_HEADER_MAGIC
|
||||
from pincodes import pa
|
||||
|
||||
# maintain a running SHA256 over what's received
|
||||
if offset == 0:
|
||||
@ -798,11 +752,10 @@ class USBHandler:
|
||||
dis.progress_sofar(offset, total_size)
|
||||
|
||||
assert offset % 256 == 0, 'alignment'
|
||||
assert 1 <= total_size <= MAX_UPLOAD_LEN, 'long'
|
||||
assert offset + len(data) <= total_size, 'long'
|
||||
assert offset+len(data) <= total_size <= MAX_UPLOAD_LEN, 'long'
|
||||
|
||||
if hsm_active or pa.hobbled_mode:
|
||||
# additional restriction in HSM mode or hobbled: must be PSBT
|
||||
if hsm_active:
|
||||
# additional restrictions in HSM mode
|
||||
assert offset+len(data) <= total_size <= MAX_TXN_LEN, 'psbt'
|
||||
if offset == 0:
|
||||
assert data[0:5] == b'psbt\xff', 'psbt'
|
||||
@ -881,6 +834,7 @@ class USBHandler:
|
||||
def handle_bag_number(self, bag_num):
|
||||
import version, callgate
|
||||
from glob import dis, settings
|
||||
from pincodes import pa
|
||||
|
||||
if bag_num and version.is_factory_mode and not version.has_qr:
|
||||
# check state first
|
||||
|
||||
@ -78,7 +78,7 @@ KEY = 'usr'
|
||||
UserInfo = namedtuple('UserInfo', 'auth_mode secret last_counter')
|
||||
|
||||
class Users:
|
||||
'''Track users and their TOTP secrets or hashed passwords'''
|
||||
'''Track users and thier TOTP secrets or hashed passwords'''
|
||||
|
||||
@classmethod
|
||||
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 hexlify as b2a_hex
|
||||
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 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')
|
||||
|
||||
@ -193,31 +193,34 @@ def str2xfp(txt):
|
||||
# Inverse of xfp2str
|
||||
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):
|
||||
PRINTABLE = range(32, 127)
|
||||
for ch in s:
|
||||
o = ord(ch)
|
||||
if o < 32 or o > 126:
|
||||
if ord(ch) not in PRINTABLE:
|
||||
return False
|
||||
return True
|
||||
|
||||
def to_ascii_printable(s, allow_tab_nl=False):
|
||||
def to_ascii_printable(s, strip=False, only_printable=True):
|
||||
try:
|
||||
# s must be a string!
|
||||
assert len(s) == len(s.encode())
|
||||
if not allow_tab_nl:
|
||||
s = str(s, 'ascii')
|
||||
if strip:
|
||||
s = s.strip()
|
||||
assert is_ascii(s)
|
||||
if only_printable:
|
||||
assert is_printable(s)
|
||||
else:
|
||||
for ch in s:
|
||||
o = ord(ch)
|
||||
assert 32 <= o <= 126 or o == 9 or o == 10
|
||||
return s
|
||||
except:
|
||||
err = "must be ascii printable" + (", tab, or newline" if allow_tab_nl else "")
|
||||
raise AssertionError(err)
|
||||
raise AssertionError("must be ascii" + (" printable" if only_printable else ""))
|
||||
|
||||
|
||||
def problem_file_line(exc):
|
||||
# return a string of just the filename.py and line number where
|
||||
# an exception occurred. Best used on AssertionError.
|
||||
# an exception occured. Best used on AssertionError.
|
||||
|
||||
tmp = uio.StringIO()
|
||||
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
|
||||
# - 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
|
||||
if s == '': return 'm'
|
||||
@ -380,7 +383,7 @@ def check_firmware_hdr(hdr, binary_size):
|
||||
# - hdr must be a bytearray(FW_HEADER_SIZE+more)
|
||||
|
||||
from sigheader import FW_HEADER_SIZE, FW_HEADER_MAGIC, FWH_PY_FORMAT
|
||||
from sigheader import MK_1_OK, MK_2_OK, MK_3_OK, MK_4_OK, MK_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 version import hw_label
|
||||
import callgate
|
||||
@ -409,8 +412,6 @@ def check_firmware_hdr(hdr, binary_size):
|
||||
ok = (hw_compat & MK_3_OK)
|
||||
elif hw_label == 'mk4':
|
||||
ok = (hw_compat & MK_4_OK)
|
||||
elif hw_label == 'mk5':
|
||||
ok = (hw_compat & MK_5_OK)
|
||||
elif hw_label == 'q1':
|
||||
ok = (hw_compat & MK_Q1_OK)
|
||||
|
||||
@ -428,8 +429,6 @@ def clean_shutdown(style=0):
|
||||
# wipe SPI flash and shutdown (wiping main memory)
|
||||
# - mk4: SPI flash not used, but NFC may hold data (PSRAM cleared by bootrom)
|
||||
# - bootrom wipes every byte of SRAM, so no need to repeat here
|
||||
# - style=2 => reboot and try login again
|
||||
# - default is logout and (if applicable) power down.
|
||||
import callgate
|
||||
|
||||
# save if anything pending
|
||||
@ -465,11 +464,6 @@ def call_later_ms(delay, cb, *args, **kws):
|
||||
def word_wrap(ln, w):
|
||||
# Generate the lines needed to wrap one line into X "width"-long lines.
|
||||
# - tests in testing/test_unit.py
|
||||
if ln and (ln[0] == OUT_CTRL_NOWRAP):
|
||||
# no need to wrap this line - as requested by caller
|
||||
yield ln[1:]
|
||||
return
|
||||
|
||||
while True:
|
||||
# ln_len considers DOUBLE_WIDTH chars
|
||||
ln_len = 0
|
||||
@ -545,7 +539,7 @@ def parse_extended_key(ln, private=False):
|
||||
found = pat.search(ln)
|
||||
# serialize, and note version code
|
||||
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:
|
||||
pass
|
||||
|
||||
@ -643,10 +637,13 @@ def decode_bip21_text(got):
|
||||
|
||||
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
|
||||
if '?' in got:
|
||||
got, args = got.split('?', 1)
|
||||
addr, args = got.split('?', 1)
|
||||
|
||||
# full URL decode here, but assuming no repeated keys
|
||||
parts = args.split('&')
|
||||
@ -655,12 +652,7 @@ def decode_bip21_text(got):
|
||||
k, v = p.split('=', 1)
|
||||
args[k] = url_unquote(v)
|
||||
|
||||
# remove URL protocol: if present
|
||||
if ':' in got:
|
||||
proto, got = got.split(':', 1)
|
||||
assert proto.lower() == "bitcoin"
|
||||
|
||||
# assume it's a bare address for now
|
||||
# assume it's an bare address for now
|
||||
if not addr:
|
||||
addr = got
|
||||
|
||||
@ -688,35 +680,6 @@ def decode_bip21_text(got):
|
||||
|
||||
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):
|
||||
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
|
||||
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
|
||||
|
||||
39
shared/ux.py
39
shared/ux.py
@ -7,8 +7,8 @@ from queues import QueueEmpty
|
||||
import utime, gc, version
|
||||
from utils import word_wrap
|
||||
from version import has_qwerty, num_sd_slots, has_qr
|
||||
from charcodes import (KEY_UP, KEY_DOWN, KEY_HOME, KEY_NFC, KEY_QR, KEY_END, KEY_PAGE_UP,
|
||||
KEY_PAGE_DOWN, KEY_ENTER, KEY_CANCEL, OUT_CTRL_TITLE)
|
||||
from charcodes import (KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_NFC, KEY_QR,
|
||||
KEY_END, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_ENTER, KEY_CANCEL, OUT_CTRL_TITLE)
|
||||
|
||||
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
|
||||
n = seconds * 8
|
||||
dis.fullscreen(msg)
|
||||
for i in range(1, n+1):
|
||||
for i in range(n):
|
||||
dis.progress_bar_show(i/n)
|
||||
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)
|
||||
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:
|
||||
max_value = (2 ** 31) - 1 # we handle hardened
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
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):
|
||||
prompt = "Press (B) to import %s from lower slot SD Card" % title
|
||||
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"
|
||||
escape += KEY_QR
|
||||
|
||||
if key6:
|
||||
prompt += ', (6) ' + key6
|
||||
escape += '6'
|
||||
|
||||
if key0:
|
||||
prompt += ', (0) ' + key0
|
||||
escape += '0'
|
||||
|
||||
prompt += "."
|
||||
|
||||
return prompt, escape
|
||||
|
||||
|
||||
def export_prompt_builder(what_it_is, no_qr=False, no_nfc=False, key0=None, offer_kt=False,
|
||||
force_prompt=False, key6=None):
|
||||
force_prompt=False, txid=None):
|
||||
# Build the prompt for export
|
||||
# - key0 can be for special stuff
|
||||
from glob import NFC, VD
|
||||
|
||||
prompt, escape = None, KEY_CANCEL+"x"
|
||||
|
||||
if (NFC or VD) or (num_sd_slots>1) or key0 or force_prompt 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
|
||||
|
||||
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"
|
||||
escape += '4'
|
||||
|
||||
if txid:
|
||||
prompt += ", (6) for QR Code of TXID"
|
||||
escape += "6"
|
||||
|
||||
if offer_kt:
|
||||
prompt += ", (T) to " + offer_kt
|
||||
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
|
||||
escape += '0'
|
||||
|
||||
if key6:
|
||||
prompt += ", (6) " + key6
|
||||
escape += "6"
|
||||
|
||||
prompt += "."
|
||||
|
||||
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,
|
||||
no_nfc=False, title=None, intro='', footnotes='',
|
||||
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
|
||||
# - 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
|
||||
|
||||
if is_import:
|
||||
prompt, escape = _import_prompt_builder(what_it_is, no_qr, no_nfc, slot_b_only,
|
||||
key0=key0, key6=key6)
|
||||
prompt, escape = _import_prompt_builder(what_it_is, no_qr, no_nfc, slot_b_only)
|
||||
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)
|
||||
|
||||
# TODO: detect if we're only asking A or B, when just one card is inserted
|
||||
|
||||
@ -60,7 +60,7 @@ class PressRelease:
|
||||
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
|
||||
# - default/blank value assumed to be zero
|
||||
# - clamps large values to the max
|
||||
|
||||
@ -76,7 +76,7 @@ class PressRelease:
|
||||
self.last_key = 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
|
||||
# - default/blank value assumed to be zero
|
||||
# - 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)
|
||||
elif ch == KEY_CANCEL:
|
||||
if can_cancel:
|
||||
# quit if they press CANCEL on any screen
|
||||
# quit if they press X on empty screen
|
||||
return None
|
||||
elif '0' <= ch <= '9':
|
||||
if len(value) == max_w:
|
||||
@ -537,14 +537,13 @@ def ux_render_words(words, leading_blanks=0):
|
||||
num_words = len(words)
|
||||
if num_words == 12:
|
||||
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]))
|
||||
else:
|
||||
lines = 6 if num_words == 18 else 8
|
||||
for y in range(lines):
|
||||
rv.append(OUT_CTRL_NOWRAP+'%d:%-8s %2d:%-8s %2d:%s' % (
|
||||
y+1, words[y], y+lines+1, words[y+lines],
|
||||
y+(lines*2)+1, words[y+(lines*2)]))
|
||||
rv.append('%d:%-8s %2d:%-8s %2d:%s' % (y+1, words[y],
|
||||
y+lines+1, words[y+lines],
|
||||
y+(lines*2)+1, words[y+(lines*2)]))
|
||||
|
||||
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
|
||||
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:
|
||||
cols = 2
|
||||
xpos = [2, 18]
|
||||
@ -578,7 +567,7 @@ def ux_draw_words(y, num_words, words):
|
||||
if num_words == 12:
|
||||
# luxious space after colon
|
||||
msg = ('%2d: ' % n) + word
|
||||
x_off = 4
|
||||
x_off = 3
|
||||
else:
|
||||
if n <= n_per_c:
|
||||
# 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
|
||||
|
||||
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
|
||||
# - replaces WordNestMenu on Q1
|
||||
# - 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
|
||||
|
||||
not24 = (num_words != 24)
|
||||
|
||||
def redraw_words(wrds=None):
|
||||
if not wrds:
|
||||
wrds = ['' for _ in range(num_words)]
|
||||
|
||||
dis.clear()
|
||||
dis.text(None, 0, prompt, invert=1)
|
||||
|
||||
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)
|
||||
p = ux_draw_words(2 if num_words != 24 else 1, num_words, wrds)
|
||||
return wrds, p
|
||||
|
||||
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)
|
||||
except QRDecodeExplained as e:
|
||||
err_msg = str(e)
|
||||
redraw_words(words)
|
||||
redraw_words()
|
||||
continue
|
||||
|
||||
if what != "words":
|
||||
@ -825,6 +804,7 @@ class QRScannerInteraction:
|
||||
while 1:
|
||||
if task.done():
|
||||
data = await task
|
||||
#print("Scanned: %r" % data)
|
||||
break
|
||||
|
||||
dis.image(None, 40, 'scan_%d' % frames[ph])
|
||||
@ -837,12 +817,7 @@ class QRScannerInteraction:
|
||||
data = None
|
||||
break
|
||||
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
task.cancel()
|
||||
|
||||
# clear screen right away so user knows we got it
|
||||
dis.clear()
|
||||
@ -885,7 +860,7 @@ class QRScannerInteraction:
|
||||
file_type, _, data = decode_qr_result(got, expect_bbqr=True)
|
||||
if file_type == 'U':
|
||||
data = data.strip()
|
||||
if data[:1] == b'{' and data[-1:] == b'}':
|
||||
if data[0] == '{' and data[-1] == '}':
|
||||
file_type = 'J'
|
||||
if file_type != 'J':
|
||||
raise QRDecodeExplained('Expected JSON data')
|
||||
@ -926,8 +901,6 @@ class QRScannerInteraction:
|
||||
async def scan_anything(self, expect_secret=False, tmp=False):
|
||||
# start a QR scan, and act on what we find, whatever it may be.
|
||||
from ux import ux_show_story
|
||||
from pincodes import pa
|
||||
|
||||
problem = None
|
||||
while 1:
|
||||
prompt = 'Scan any QR code, or CANCEL' if not expect_secret else \
|
||||
@ -949,21 +922,6 @@ class QRScannerInteraction:
|
||||
problem = "Unable to decode QR"
|
||||
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':
|
||||
from actions import import_extended_key_as_secret
|
||||
text_xprv, = vals
|
||||
@ -1004,7 +962,6 @@ class QRScannerInteraction:
|
||||
elif what == "wif":
|
||||
data, = vals
|
||||
wif_str, key_pair, compressed, testnet = data
|
||||
from wif import ux_visualize_wif
|
||||
await ux_visualize_wif(wif_str, key_pair, compressed, testnet)
|
||||
|
||||
elif what == "vmsg":
|
||||
@ -1061,7 +1018,7 @@ async def qr_psbt_sign(decoder, psbt_len, raw):
|
||||
psbt_len = total
|
||||
|
||||
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)
|
||||
_, 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
|
||||
# - validate address ownership on request
|
||||
from ux import ux_show_story
|
||||
from chains import current_chain
|
||||
|
||||
msg = show_single_address(addr) + '\n\n'
|
||||
args = args or {}
|
||||
|
||||
if 'amount' in args:
|
||||
msg += 'Amount: '
|
||||
try:
|
||||
amt = args.pop('amount')
|
||||
whole, _, frac = amt.partition('.')
|
||||
assert whole.isdigit()
|
||||
assert len(whole) <= 8
|
||||
assert len(frac) <= 8
|
||||
sats = int((whole or '0') + (frac + '00000000')[:8])
|
||||
msg += 'Amount: %s %s\n' % current_chain().render_value(sats)
|
||||
whole, frac = amt.split('.', 1)
|
||||
frac = int(frac) if frac else 0
|
||||
whole = int(whole) if whole else 0
|
||||
msg += '%d.%08d BTC\n' % (whole, frac)
|
||||
except:
|
||||
msg += 'Amount: (corrupt)\n'
|
||||
msg += '(corrupt)\n'
|
||||
|
||||
for fn in ['label', 'message', 'lightning']:
|
||||
if fn in args:
|
||||
@ -1145,8 +1100,15 @@ async def ux_visualize_bip21(proto, addr, args):
|
||||
|
||||
if ch == '1':
|
||||
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):
|
||||
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
|
||||
import uqr
|
||||
|
||||
assert not PSRAM.is_at(data, 0) # input data would be overwritten with our work
|
||||
assert type_code in TYPE_LABELS
|
||||
|
||||
dis.fullscreen('Generating BBQr...', .1)
|
||||
@ -1215,11 +1178,6 @@ async def show_bbqr_codes(type_code, data, msg, already_hex=False):
|
||||
else:
|
||||
# default to Base32, because always best option
|
||||
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)
|
||||
|
||||
# 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
|
||||
import ckcc, callgate, machine
|
||||
from machine import Pin
|
||||
|
||||
hw_label = 'mk4'
|
||||
has_608 = True
|
||||
@ -98,7 +97,7 @@ def probe_system():
|
||||
|
||||
# detect Q1 based on pins.csv
|
||||
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
|
||||
num_sd_slots = 2
|
||||
hw_label = 'q1'
|
||||
@ -109,15 +108,6 @@ def probe_system():
|
||||
except ValueError:
|
||||
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:
|
||||
# - did we just install a new version, for example (obsolete in mk4)
|
||||
# - are we running in "factory mode" with flash un-secured?
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
#
|
||||
# wallet.py - A place you find UTXO, addresses and descriptors.
|
||||
#
|
||||
import chains, version
|
||||
import chains
|
||||
from descriptor import Descriptor
|
||||
from stash import SensitiveValues
|
||||
|
||||
@ -38,16 +38,9 @@ class MasterSingleSigWallet(WalletABC):
|
||||
def __init__(self, addr_fmt, path=None, account_idx=0, chain_name=None):
|
||||
# Construct a wallet based on current master secret, and chain.
|
||||
# - 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)
|
||||
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)
|
||||
prefix = path or 'm/%dh/{coin_type}h/{account}h' % purpose
|
||||
|
||||
@ -57,13 +50,12 @@ class MasterSingleSigWallet(WalletABC):
|
||||
self.chain = chains.current_chain()
|
||||
|
||||
if account_idx != 0:
|
||||
rv = " Account#%d" if version.has_qwerty else " Acct#%d"
|
||||
n += rv % account_idx
|
||||
n += ' Account#%d' % account_idx
|
||||
|
||||
if self.chain.ctype == 'XTN':
|
||||
n += ' (Testnet)' if version.has_qwerty else " XTN"
|
||||
n += ' (Testnet)'
|
||||
if self.chain.ctype == 'XRT':
|
||||
n += ' (Regtest)' if version.has_qwerty else " XRT"
|
||||
n += ' (Regtest)'
|
||||
|
||||
|
||||
self.name = n
|
||||
|
||||
@ -95,7 +95,7 @@ async def perform_web2fa(label, shared_secret):
|
||||
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.
|
||||
#
|
||||
@ -115,21 +115,22 @@ async def web2fa_enroll(ss=None):
|
||||
# - can't fit any metadata, like username or our serial # in there
|
||||
# - 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, nm=nm)
|
||||
qr = 'otpauth://totp/{nm}?secret={ss}'.format(ss=ss,
|
||||
nm=url_quote(label if has_qr else label[0:4]))
|
||||
|
||||
while 1:
|
||||
# show QR for enroll
|
||||
await show_qr_code(qr, is_alnum=False, msg="Import into 2FA Mobile App",
|
||||
force_msg=True)
|
||||
|
||||
# important: force them to prove they stored it correctly
|
||||
ok = await perform_web2fa('Enroll: COLDCARD', ss)
|
||||
# important: force them to prove they store it correctly
|
||||
ok = await perform_web2fa('Enroll: ' + label, ss)
|
||||
if ok: break
|
||||
|
||||
ch = await ux_show_story("That isn't correct. Please re-import and/or "
|
||||
"try again or %s to give up." % X)
|
||||
if ch == 'x':
|
||||
# mk4 only?
|
||||
return None
|
||||
|
||||
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