Merge branch 'Coldcard:master' into master
This commit is contained in:
commit
50c541c080
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -11,3 +11,6 @@
|
||||
[submodule "external/libngu"]
|
||||
path = external/libngu
|
||||
url = https://github.com/switck/libngu.git
|
||||
[submodule "stm32/mk4-bootloader/hal"]
|
||||
path = stm32/mk4-bootloader/hal
|
||||
url = https://github.com/STMicroelectronics/STM32CubeL4.git
|
||||
|
||||
44
README.md
44
README.md
@ -8,8 +8,7 @@ with the latest updates and security alerts.
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## Reproducible Builds
|
||||
|
||||
@ -22,9 +21,9 @@ has been automated using Docker. Steps are as follows:
|
||||
3. Checkout the code, and start the process.
|
||||
|
||||
git clone https://github.com/Coldcard/firmware.git
|
||||
|
||||
|
||||
cd firmware/stm32
|
||||
|
||||
|
||||
make repro
|
||||
|
||||
4. At the end of the process a clear confirmation message is shown, or the differences.
|
||||
@ -32,6 +31,9 @@ has been automated using Docker. Steps are as follows:
|
||||
|
||||
## Check-out and Setup
|
||||
|
||||
**NOTE** This is the `master` branch and covers the latest hardware (Mk4).
|
||||
See branch `v4-legacy` for firmware which supports only Mk3/Mk2 and earlier.
|
||||
|
||||
Do a checkout, recursively to get all the submodules:
|
||||
|
||||
git clone --recursive https://github.com/Coldcard/firmware.git
|
||||
@ -41,6 +43,9 @@ Already checked-out and getting git errors? Do this:
|
||||
git fetch
|
||||
git reset --hard origin/master
|
||||
|
||||
Do not use a path with any spaces in it. The Makefiles do not handle
|
||||
that well, and we're not planning to fix it.
|
||||
|
||||
Then:
|
||||
|
||||
- `cd firmware`
|
||||
@ -85,6 +90,13 @@ Used to be these were needed as well:
|
||||
|
||||
You may need to reboot to avoid a `DISPLAY is not set` error.
|
||||
|
||||
You may need to `brew upgrade gcc-arm-embedded` because we need 10.2 or higher.
|
||||
|
||||
#### Big Sur Issues
|
||||
|
||||
- `defaults write org.python.python ApplePersistenceIgnoreState NO` will supress a warning
|
||||
about `Python[22580:10101559] ApplePersistenceIgnoreState: Existing state will not be touched. New state will be written to...` see <https://bugs.python.org/issue32909>
|
||||
|
||||
### Linux
|
||||
|
||||
You'll probably need to install these (Ubuntu 16):
|
||||
@ -108,6 +120,8 @@ Top-level dirs:
|
||||
|
||||
- shared code between desktop test version and real-deal
|
||||
- expected to be largely in python, and higher-level
|
||||
- new code found only on the Mk4 will be listed in `manifest_mk4.py` code exclusive
|
||||
to earlier hardware is in `manifest_mk3.py`
|
||||
|
||||
`unix`
|
||||
|
||||
@ -121,25 +135,39 @@ Top-level dirs:
|
||||
|
||||
`stm32`
|
||||
|
||||
- embedded micro version, for actual product
|
||||
- embedded binaries (and building), for actual product hardware
|
||||
- final target is a binary file for loading onto hardware
|
||||
|
||||
`external`
|
||||
|
||||
- code from other projects, ie. the dreaded submodules
|
||||
|
||||
`graphics`
|
||||
|
||||
- images which ship as part of the final product (icons)
|
||||
|
||||
`stm32/bootloader`
|
||||
|
||||
- 32k of factory-set code that you cannot change
|
||||
- 32k of factory-set code that you cannot change (Mk3)
|
||||
- however, you can inspect what code is on your coldcard and compare to this.
|
||||
|
||||
`stm32/mk4-bootloader`
|
||||
|
||||
- 128k of factory-set code that you cannot change for Mk4
|
||||
- however, you can inspect what code is on your coldcard and compare to this.
|
||||
|
||||
`hardware`
|
||||
|
||||
- schematic and bill of materials for the Coldcard
|
||||
|
||||
`unix/work/MicroSD`
|
||||
`unix/work/...`
|
||||
|
||||
- `/MicroSD/*` files on "simulated" microSD card
|
||||
|
||||
- `/VirtDisk/*` simulated emulated virtual Disk files.
|
||||
|
||||
- `/settings/*.aes` persistant settings for Simulator
|
||||
|
||||
- files on "simulated" microSD card
|
||||
|
||||
|
||||
## Support
|
||||
|
||||
@ -12,9 +12,6 @@ from ecdsa import SigningKey, VerifyingKey
|
||||
from ecdsa.curves import SECP256k1
|
||||
from sigheader import *
|
||||
|
||||
# list of hardware we are presently supporting
|
||||
CURRENT_HARDWARE = MK_2_OK | MK_3_OK
|
||||
|
||||
# more details about header
|
||||
header = namedtuple('header', FWH_PY_VALUES)
|
||||
packed_len = struct.calcsize(FWH_PY_FORMAT)
|
||||
@ -210,7 +207,8 @@ def readback(fname):
|
||||
if v & MK_1_OK: d.append('Mk1')
|
||||
if v & MK_2_OK: d.append('Mk2')
|
||||
if v & MK_3_OK: d.append('Mk3')
|
||||
if v & ~(MK_1_OK | MK_2_OK | MK_3_OK):
|
||||
if v & MK_4_OK: d.append('Mk4')
|
||||
if v & ~(MK_1_OK | MK_2_OK | MK_3_OK | MK_4_OK):
|
||||
d.append('?other?')
|
||||
v = nv + '+'.join(d)
|
||||
elif fld == 'timestamp':
|
||||
@ -228,7 +226,7 @@ def readback(fname):
|
||||
a.update(data[FW_HEADER_OFFSET+FW_HEADER_SIZE:])
|
||||
chk = sha256(a.digest()).digest()
|
||||
|
||||
#print("sha256^2: %s" % b2a_hex(chk).decode('ascii'))
|
||||
print("sha256^2: %s" % b2a_hex(chk).decode('ascii'))
|
||||
|
||||
# from pubkey
|
||||
vk = VerifyingKey.from_pem(open("keys/%02d.pubkey.pem" % vals['pubkey_num']).read())
|
||||
@ -246,16 +244,17 @@ 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('--force-hw-compat', type=str, metavar='BITMASK', help="Override HW compat field (hex)")
|
||||
@click.option('--hw-compat', '-m', type=int, metavar='BITMASK', help="Set HW compat field (mk number)")
|
||||
@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')
|
||||
@click.option('--resign_file', '-r', type=click.File('rb'),
|
||||
help='Replace existing signature', default=None)
|
||||
@click.option('--outfn', '-o', type=click.Path(),
|
||||
help='Output filename', default='firmware-signed.bin')
|
||||
@click.option('--keydir', type=str, metavar='DIRPATH', help="Where to find priv keys for signing", default='keys')
|
||||
def doit(keydir, outfn=None, build_dir='l-port/build-COLDCARD', high_water=False,
|
||||
current=False, force_hw_compat=None,
|
||||
def doit(keydir, outfn=None, build_dir=None, high_water=False,
|
||||
current=False, hw_compat=None,
|
||||
version='0.1a', pubkey_num=0, backdate=0, verbose=False, resign_file=None):
|
||||
"Add signature into binary file before it becomes a DFU file."
|
||||
|
||||
@ -278,15 +277,16 @@ def doit(keydir, outfn=None, build_dir='l-port/build-COLDCARD', high_water=False
|
||||
vectors = open(build_dir + '/firmware0.bin', 'rb').read()
|
||||
body = open(build_dir + '/firmware1.bin', 'rb').read()
|
||||
|
||||
if hw_compat == 4:
|
||||
hw_compat = MK_4_OK
|
||||
elif hw_compat in {3, None}:
|
||||
hw_compat = MK_2_OK | MK_3_OK
|
||||
else:
|
||||
assert not "known"
|
||||
|
||||
assert len(vectors) <= FW_HEADER_OFFSET, "isr vectors area is too big!"
|
||||
assert len(body) >= FW_MIN_LENGTH, "main firmware is too small: %d" % len(body)
|
||||
|
||||
if force_hw_compat is not None:
|
||||
hw_compat = int(force_hw_compat, 16)
|
||||
click.echo("Overriding hw_compat field: 0x%02x" % hw_compat)
|
||||
else:
|
||||
hw_compat = CURRENT_HARDWARE
|
||||
|
||||
body_len = align_to(len(body), 512)
|
||||
assert body_len % 512 == 0, body_len
|
||||
|
||||
@ -300,6 +300,7 @@ def doit(keydir, outfn=None, build_dir='l-port/build-COLDCARD', high_water=False
|
||||
firmware_length=FW_HEADER_OFFSET+FW_HEADER_SIZE+body_len,
|
||||
install_flags=(FWHIF_HIGH_WATER if high_water else 0x0),
|
||||
hw_compat=hw_compat,
|
||||
best_ts=bytes(8),
|
||||
future=b'\0'*(4*FWH_NUM_FUTURE),
|
||||
signature=b'\xff'*64,
|
||||
pubkey_num=pubkey_num,
|
||||
@ -307,8 +308,13 @@ def doit(keydir, outfn=None, build_dir='l-port/build-COLDCARD', high_water=False
|
||||
|
||||
assert FW_MIN_LENGTH <= hdr.firmware_length <= FW_MAX_LENGTH, hdr.firmware_length
|
||||
|
||||
# actual file length limited by size of SPI flash area reserved to txn data/uploads
|
||||
USB_MAX_LEN = (786432-128)
|
||||
if hw_compat & MK_4_OK:
|
||||
# new value for Mk4: limited only by final binary size, not SPI flash
|
||||
USB_MAX_LEN = 1472 * 1024
|
||||
else:
|
||||
# actual file length limited by size of SPI flash area reserved to txn data/uploads
|
||||
USB_MAX_LEN = (786432-128)
|
||||
|
||||
assert hdr.firmware_length <= USB_MAX_LEN, \
|
||||
"too big for our USB upgrades: %d = %d bytes too big" % (
|
||||
hdr.firmware_length, hdr.firmware_length-USB_MAX_LEN)
|
||||
@ -317,8 +323,6 @@ def doit(keydir, outfn=None, build_dir='l-port/build-COLDCARD', high_water=False
|
||||
|
||||
binhdr = struct.pack(FWH_PY_FORMAT, *hdr)
|
||||
assert len(binhdr) == FW_HEADER_SIZE
|
||||
|
||||
assert len(vectors + binhdr[:-64]) == 0x3fc0
|
||||
assert len(vectors + binhdr[:-64]) == 0x3fc0
|
||||
|
||||
hashable = vectors + binhdr[:-64] + body
|
||||
|
||||
@ -13,5 +13,4 @@ wants to understand why it's safe to put your moneys into Coldcard.
|
||||
- [`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.
|
||||
- [`nfc-coldcard.md`](https://github.com/Coldcard/firmware/blob/master/docs/nfc-coldcard.md) NFC Specification for COLDCARD interoperability.
|
||||
|
||||
|
||||
@ -27,10 +27,9 @@ Step 2: Export descriptor from Coldcard to Core
|
||||
- select your newly created descriptor wallet in the wallet pulldown (top left)
|
||||
- paste the `importdescriptor` command. It should respond with a success message
|
||||
|
||||
NOTE: If you are importing an existing wallet this way, with UTXO
|
||||
already on the blockchain, you may need to rescan by changing
|
||||
"timestamp=now" to "timestamp=0" for enable a full rescan. If the balance
|
||||
is zero this is probably why.
|
||||
NOTE: If you are importing an existing wallet this way, with UTXO on the blockchain,
|
||||
you may need to rescan and/or delete "timestamp=now" from the command. If the
|
||||
balance is zero this is why.
|
||||
|
||||
### Bitcoin Core v0.19.0+
|
||||
|
||||
|
||||
@ -1,93 +1,81 @@
|
||||
# Developers on Coldcard
|
||||
# Developing on COLDCARD
|
||||
|
||||
Yes, external developers can modify Coldcard... We've saved 128k of flash just for you!
|
||||
Yes, external developers can modify COLDCARD and make their own versions!
|
||||
|
||||
## Approaches
|
||||
|
||||
### Hard Core
|
||||
|
||||
- build a new image, all the way to a DFU file (see `../stm32/Makefile`)
|
||||
- sign with non-production key, provided in github tree
|
||||
- install your DFU file using existing upgrade methods (microSD, usb upload)
|
||||
- sign with non-production key, provided in github tree (key zero)
|
||||
- install your DFU file using existing upgrade methods (microSD, usb upload, VirtDisk)
|
||||
- you can replace any part of the python code, and even the mpy interpreter itself
|
||||
- you cannot change the bootrom, and it still runs first
|
||||
- your code will not be signed by the factory key, so warning and delay, is shown:
|
||||
- since your code is not signed by a factory key, a warning and forced delay always occurs:
|
||||
|
||||

|
||||
|
||||
- to get green light, the user (who knows the main PIN) must do the "bless" operation
|
||||
- you can distrubute your DFU file to the world
|
||||
- you can take factory-fresh Coldcards, destroy the tamper-evident bag, and load your
|
||||
firmware onto them before shipping to your customers.
|
||||

|
||||
|
||||
- in versions before the Mk4, if you had the green light set, via blessing the custom firmware,
|
||||
this delay/warning could be avoided, but that is no longer the case.
|
||||
- you can distribute your DFU file to the world, but everyone who runs it will see above warning
|
||||
- remember the main PIN has to be set and provided correctly before new firmware can be installed
|
||||
- your COLDCARD will be bricked if your code crashes before it gets running "enough" that you
|
||||
can upload a corrected version. Bugs in the boot & login sequence are fatal in that sense.
|
||||
|
||||
### Medium Core
|
||||
|
||||
- install any published dev version (or build your own, see above)
|
||||
- enable the virtual disk feature:
|
||||
Advanced > Danger Zone > I Am Developer. > Enable USB Disk
|
||||
- REPL and mass-storage emulation will now be active
|
||||
- or you can just enable the REPL, and connect there and experiment first
|
||||
- expect to find helpful file: `/Volumes/COLDCARD/README.txt`
|
||||
- write code and save into `.../COLDCARD/lib`
|
||||
- any same-named file will replace built-in stock module
|
||||
- `lib/boot2.py` and `lib/main2.py` are loaded if they exist during boot sequence
|
||||
- the only python you cannot override is `main.py`
|
||||
- RAM limits complexity of the override you can do (but freezing your micropython helps)
|
||||
- you cannot change the bootrom, and it still runs first
|
||||
- changes to the virtual disk cause the light to go red, you'll need to bless after
|
||||
each change you make.
|
||||
- distribute your version by capturing an image of your working virtual disk drive,
|
||||
and then putting just that into a DFU file (128k)
|
||||
- users will have to have a similar dev version of main firmware, but
|
||||
you would probably give it to them, as separate DFU file.
|
||||
- both files could be put into a single DFU, but that's not supported by
|
||||
MicroSD upgrade method
|
||||
- develop your changes using the Simulator (see `../unix`)
|
||||
- submit a PR (pull request) explaining your new feature or fix.
|
||||
- Coinkite team will review for security and other code-quality issues
|
||||
- your PR could get merged into the next Coinkite firmware release for all to use.
|
||||
|
||||
### Soft Core
|
||||
|
||||
- Send an email to support asking for your altcoin to be supported. Await reply patiently.
|
||||
- send an email to support asking for your improvements to be implemented.
|
||||
- await reply patiently.
|
||||
|
||||
## Corrupt Flash
|
||||
|
||||
If the red/green light is red, this means some part of flash was
|
||||
changed without the secure checksum inside SE1 being first updated.
|
||||
The upgrade process does this correctly in Mk4, and there is no
|
||||
point time the checksum is wrong, so there should be no way to see this
|
||||
screen:
|
||||
|
||||

|
||||
|
||||
But it will be shown if the COLDCARD finds its flash checksum does
|
||||
not match the checksum held in SE1, secured by the main PIN. This
|
||||
can be false positive, but in Mk4 we've worked hard to avoid those cases.
|
||||
|
||||
A checksum error on the firmware itself (the main code) will always
|
||||
fail with a "(lemon icon) Firmware?" screen. The broken firmware is not
|
||||
started, but it's possible to recover the COLDCARD using a firmware loaded
|
||||
from an SD Card.
|
||||
|
||||
You cannot load *new* code via the SD Card firmware recovery mode.
|
||||
It requires the new firmware (based on whatever is found on SD Card)
|
||||
to have a checksum that already matches the value found in SE1.
|
||||
This means only the signed firmware that was attempting to be
|
||||
installed during the power-fail can be loaded, and not new code you
|
||||
may have written.
|
||||
|
||||
|
||||
## Shortcuts and Accerations
|
||||
## Shortcuts and Accelerations
|
||||
|
||||
- You can enable USB, or USB disk emulation, automatically at the end of `boot2.py`.
|
||||
Mount the emulated disk, and create this file, as `/Volumes/COLDCARD/lib/boot2.py`
|
||||
- You can access a micropython REPL if you are willing to break your case
|
||||
and attach to the test points along right edge of board, marked: G=Gnd, R=Rx, T=Tx.
|
||||
It's a serial port with 3.3v TTL signals running
|
||||
at 115,200 bps. Enter the REPL by pressing `^C` after enabling the REPL in
|
||||
Advanced > Danger Zone > I Am Developer. > Serial REPL
|
||||
|
||||
- To skip the prompts for the PIN, assuming correct PIN is '12-12'... run this code
|
||||
in the REPL:
|
||||
|
||||
```python
|
||||
# start the REPL very early
|
||||
import uasyncio.core as asyncio
|
||||
from usb import enable_usb
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
enable_usb(loop, True)
|
||||
from nvstore import SettingsObject
|
||||
s=SettingsObject()
|
||||
s.set('_skip_pin', '12-12')
|
||||
s.save()
|
||||
```
|
||||
|
||||
- To skip the prompts for the PIN, assuming correct PIN is '12-12'... add this code
|
||||
to `boot2.py` or run once when the system hasn't yet logged in.
|
||||
|
||||
```python
|
||||
from main import settings
|
||||
settings.set('_skip_pin', '12-12')
|
||||
```
|
||||
|
||||
- For max crash-change-rerun speed, enable the mass storage all the
|
||||
time, and work directly in `/COLDCARD/lib` files. After making a
|
||||
change, you just need to do a warm reboot (^D in REPL, 'warm reset'
|
||||
on menus). At that point, `main.py` runs, and your code will be
|
||||
used again. The USB doesn't disconnect, and the drive will still
|
||||
be mounted, ready for more changes.
|
||||
|
||||
## Limitations
|
||||
|
||||
- You cannot enable mass storage, virtual comm port (VCP = REPL access) and also HID
|
||||
for the Coldcard protocol at the same time. Pick any two.
|
||||
|
||||
- `.py` files in `/lib` will be interpreted at runtime. This is slow, and bytecode
|
||||
takes large amounts of RAM. Some files in the normal code are too big to even
|
||||
fit in RAM. The solution is to freeze your code before copying onto Coldcard. See the
|
||||
`stm32/Makefile` target `up` which does a build (freezing all the files, using
|
||||
`mpy-cross`) and then rsyncs the changed `.mpy` files into place. You'll want to do
|
||||
this on a smaller scale, and probably only for the files you are working on.
|
||||
|
||||
|
||||
|
||||
BIN
docs/dev-custom.png
Normal file
BIN
docs/dev-custom.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
@ -33,16 +33,21 @@
|
||||
- however, each wallet must be of a single address type; cannot be mixed (their limitation)
|
||||
- the same Coldcard could be used in each of the three modes (we don't care about address format)
|
||||
- with Bitcoin Core (version 0.17?), we can do PSBT transactions, which support all address types
|
||||
- we don't support coinbase transactions, so don't mine directly into a Coldcard wallet
|
||||
- we don't support signing coinbase transactions, so don't mine directly into a Coldcard wallet
|
||||
|
||||
# Max Transaction Size
|
||||
|
||||
- we support transactions up to 384k-bytes in size when serialized into PSBT format
|
||||
- mk3:
|
||||
- we support transactions up to 384k-bytes in size when serialized into PSBT format
|
||||
- we can handle transactions with up to 20 inputs to be signed at one time.
|
||||
- a maximum of 250 outputs per transaction is supported (will attempt more if memory allows)
|
||||
- mk4:
|
||||
- we support PSBT files up to 2M bytes in size.
|
||||
- any number of inputs and outputs are supported, limited only by final transaction size (100k)
|
||||
- tested with: 250 inputs, 2000 outputs
|
||||
- bitcoin limits transactions to 100k, but there could be large input transactions
|
||||
inside the PSBT. Reduce this by using segwit signatures and provide only the
|
||||
individual UTXO ("out points").
|
||||
- we can handle transactions with up to 20 inputs to be signed at one time.
|
||||
- a maximum of 250 outputs per transaction is supported (will attempt more if memory allows)
|
||||
|
||||
|
||||
# P2SH / Multisig
|
||||
@ -57,7 +62,7 @@
|
||||
- during USB "show address" for multisig, we limit subkey paths to
|
||||
16 levels deep (including master fingerprint)
|
||||
- max of 15 co-signers due to 520 byte script limitation in consensus layer with classic P2SH
|
||||
- we have space for up to 8 M-of-3 wallets, or a single M-of-15 wallet. YMMV
|
||||
- (mk3) we have space for up to 8 M-of-3 wallets, or a single M-of-15 wallet. YMMV
|
||||
- only a single multisig wallet can be involved in a PSBT; can't sign inputs from two different
|
||||
multisig wallets at the same time.
|
||||
- we always store xpubs in BIP32 format, although we can read SLIP132 format (Ypub/Zpub/etc)
|
||||
@ -113,4 +118,30 @@ We will hide transaction outputs if they are "change" back into same wallet, how
|
||||
- key derivatation paths must be 12 or less in depth (`MAX_PATH_DEPTH`)
|
||||
|
||||
|
||||
# NFC Feature (Mk4)
|
||||
|
||||
- can share up to 8000 bytes of PSBT or signed transaction data.
|
||||
- NFC-V (ISO-15693) radio/modulation is common on mobile phones but very rare on desktops
|
||||
|
||||
# Fast Wipe (Mk4)
|
||||
|
||||
- each use of "fast wipe" feature consumes a MCU key slot, of which there are 256.
|
||||
- use _Advanced > Danger Zone > MCU Key Slots_ to view usage
|
||||
|
||||
# Trick Pins (Mk4)
|
||||
|
||||
- "deltamode" PIN must be same length as true pin, and differ only in final 4 positions.
|
||||
- there are 14 trick "slots"
|
||||
- duress wallets consume 2 slots (or 3 slots for legacy duress wallet)
|
||||
- when restoring trick pins from backup files, "forgotten" pins are not restored,
|
||||
and any trick pin which matches the true PIN of the restored system will be dropped
|
||||
- deltamode PIN requirements are checked during wallet restore, and if the new true PIN
|
||||
is not compatible, the deltamode trick PIN is dropped and not restored
|
||||
- duress wallets are supported when derived from 24- or 12-word seed phrases
|
||||
|
||||
# Debug Serial Port (Mk4)
|
||||
|
||||
- virtual USB serial port disabled completely by default, and even if enabled
|
||||
in Danger Zone, only echos output, and does not accept any input
|
||||
- use hardware serial port for interactive REPL access (3.3v TTL levels)
|
||||
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
|
||||
## Background
|
||||
|
||||
The microprocess on the Coldcard is the STM32L476R?. It comes with
|
||||
one megabyte of flash, and 128k of SRAM. All types of memory share
|
||||
the same 32-bit address space.
|
||||
The microprocess on the Coldcard is from the STM32L4 family. It comes with
|
||||
one or 2 megabytes of flash, and 128k to 512k of SRAM depending on Mk2/3/4.
|
||||
All types of memory share the same 32-bit address space.
|
||||
|
||||
The bootloader code runs first, and enables specific hardware
|
||||
firewall features, which cover various parts of the address space.
|
||||
@ -13,11 +13,15 @@ so for example, you cannot see any of the flash used by the boot loader.
|
||||
|
||||
If you want to verify the contents of the boot loader, you can give
|
||||
it a 32-bit nonce and it will provide a SHA256 of itself with that
|
||||
nonce as a prefix. That hash covers `0x0800 0000` to `0x0800 7800`.
|
||||
Flash above `0x0800 8000` can be examined directly from python programs.
|
||||
nonce as a prefix. That hash covers `0x0800 0000` to `0x0800 7800`
|
||||
(to `0x0801 c000` for mk4).
|
||||
Flash above `0x0800 8000` (Mk4: `0x0802 0000`) can be examined
|
||||
directly from python programs.
|
||||
|
||||
## Memory Layout
|
||||
|
||||
(Mk3)
|
||||
|
||||
| Start | Size | Notes
|
||||
|---------------|-----------|--------------------------
|
||||
| 0x0800 0000 | 0x7800 | Mapped at zero briefly at boot time. Code. see `stm32/bootloader`
|
||||
@ -30,18 +34,33 @@ Flash above `0x0800 8000` can be examined directly from python programs.
|
||||
| 0x1000 7c00 | 0x0400 | Read-only. "Root seed" (once per bootup nonce), copy of firmware sig
|
||||
| 0x2000 0000 | 96k | SRAM1: heap and working SRAM for micropython
|
||||
|
||||
(Mk4)
|
||||
|
||||
| Start | Size | Notes
|
||||
|---------------|-----------|--------------------------
|
||||
| 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)
|
||||
| 0x0802 4000 | (~2m) | Main flash area for Micropython / Coldcard C code.
|
||||
| 0x0818 0000 | 512k | Internal LFS filesystem (holds settings)
|
||||
| 0x2000 0000 | 640k | SRAM1,2,3: used by micropython code: disk caches, byte arrays, stack
|
||||
| 0x2009 e000 | 8k | Top of SRAM3 reserved for bootloader
|
||||
|
||||
|
||||
## Security Measures
|
||||
|
||||
- On entry the bootloader always wipes its entire working SRAM2 area. You may change
|
||||
- (Mk1-3) On entry the bootloader always wipes its entire working SRAM2 area. You may change
|
||||
it, or even use it for very temporary storage, but it will be destroyed once the callgate
|
||||
into the bootloader is accessed.
|
||||
- All of SRAM1 and SRAM2 is cleared on boot up, and when the "secure logout" feature is used.
|
||||
- DFU firmware updates can only affect areas at and above 0x08008000. Upgrade process will
|
||||
crash (harmlessly) if you give a DFU file which changes another area. DFU is disabled
|
||||
- (Mk4) On entry the bootloader wipes the SRAM it's allocated before and after use.
|
||||
- All of SRAM is cleared on boot up, and when the "secure logout" feature is used.
|
||||
- DFU firmware updates can only affect areas at and above the bootrom. Upgrade process will
|
||||
have not effect if you give a DFU file which changes another area. Built-in DFU is disabled
|
||||
once the system leaves the factory.
|
||||
- If you manage to erase the entire chip's flash (not clear that's possible), then you will
|
||||
lose the pairing secret (0x0800 7800) and be unable to communicate with the secure element.
|
||||
lose the pairing secret (0x0800 7800 / 0x0800 c000) and be unable to communicate with the
|
||||
secure element(s).
|
||||
- Boot up verification process does a double-SHA256 over all of flash (including the pairing
|
||||
secret area) and also a few registers that are loaded from flash cells.
|
||||
See `verify.c` in `stm32/bootloader`.
|
||||
|
||||
@ -1,41 +1,115 @@
|
||||
|
||||
No PIN Set
|
||||
[IF NO PIN SET]
|
||||
Choose PIN Code
|
||||
Advanced
|
||||
Advanced/Tools
|
||||
View Identity
|
||||
Upgrade firmware
|
||||
Upgrade Firmware
|
||||
Show Version
|
||||
From MicroSD
|
||||
From VirtDisk [IF VIRTDISK ENABLED]
|
||||
Bless Firmware
|
||||
Paper Wallets [MAYBE]
|
||||
Paper Wallets
|
||||
Perform Selftest
|
||||
Secure Logout
|
||||
Bag Number
|
||||
Help
|
||||
---
|
||||
|
||||
Empty Wallet
|
||||
New Wallet
|
||||
[IF BLANK WALLET]
|
||||
New Seed Words
|
||||
24 Word (default)
|
||||
12 Word
|
||||
24 Word Dice Roll
|
||||
12 Word Dice Roll
|
||||
Import Existing
|
||||
24 Words
|
||||
[SEED WORD MENUS]
|
||||
18 Words
|
||||
[SEED WORD MENUS]
|
||||
12 Words
|
||||
[SEED WORD MENUS]
|
||||
Restore Backup
|
||||
Clone Coldcard
|
||||
Import XPRV
|
||||
Dice Rolls
|
||||
Seed XOR
|
||||
Help
|
||||
Advanced
|
||||
Advanced/Tools
|
||||
View Identity
|
||||
Upgrade
|
||||
Upgrade Firmware
|
||||
Show Version
|
||||
From MicroSD
|
||||
From VirtDisk [IF VIRTDISK ENABLED]
|
||||
Bless Firmware
|
||||
Paper Wallets [MAYBE]
|
||||
File Management
|
||||
Verify Backup
|
||||
List Files
|
||||
NFC File Share [IF NFC ENABLED]
|
||||
Format SD Card
|
||||
Format RAM Disk [IF VIRTDISK ENABLED]
|
||||
Paper Wallets
|
||||
Perform Selftest
|
||||
I Am Developer.
|
||||
Serial REPL
|
||||
Wipe LFS
|
||||
Warm Reset
|
||||
Restore Txt Bkup
|
||||
Secure Logout
|
||||
Settings
|
||||
Login Settings
|
||||
Change Main PIN
|
||||
PIN Options
|
||||
Trick PINs
|
||||
Trick PINs:
|
||||
↳22-22
|
||||
Add New Trick
|
||||
Add If Wrong
|
||||
Delete All
|
||||
Set Nickname
|
||||
Scramble Keypad
|
||||
Kill Key
|
||||
Login Countdown
|
||||
Disabled
|
||||
15 minutes
|
||||
30 minutes
|
||||
4 hours
|
||||
1 hour
|
||||
5 minutes
|
||||
8 hours
|
||||
12 hours
|
||||
2 hours
|
||||
3 days
|
||||
48 hours
|
||||
1 week
|
||||
24 hours
|
||||
28 days later
|
||||
Test Login Now
|
||||
Hardware On/Off
|
||||
USB Port
|
||||
Default On
|
||||
Disable USB
|
||||
Virtual Disk
|
||||
Default Off
|
||||
Enable
|
||||
Enable & Auto
|
||||
NFC Sharing
|
||||
Default Off
|
||||
Enable NFC
|
||||
Multisig Wallets
|
||||
(none setup yet)
|
||||
Import from SD
|
||||
Export XPUB
|
||||
Create Airgapped
|
||||
Trust PSBT?
|
||||
Skip Checks?
|
||||
Display Units
|
||||
BTC
|
||||
mBTC
|
||||
bits
|
||||
sats
|
||||
Max Network Fee
|
||||
10% (default)
|
||||
25%
|
||||
50%
|
||||
no limit
|
||||
Idle Timeout
|
||||
2 minutes
|
||||
5 minutes
|
||||
@ -44,81 +118,70 @@ Empty Wallet
|
||||
4 hours
|
||||
8 hours
|
||||
Never
|
||||
Login Countdown
|
||||
Disabled
|
||||
5 minutes
|
||||
15 minutes
|
||||
30 minutes
|
||||
1 hour
|
||||
2 hours
|
||||
4 hours
|
||||
8 hours
|
||||
12 hours
|
||||
24 hours
|
||||
48 hours
|
||||
3 days
|
||||
1 week
|
||||
28 days later
|
||||
Max Network Fee
|
||||
10% (default)
|
||||
25%
|
||||
50%
|
||||
no limit
|
||||
PIN Options
|
||||
Multisig Wallets
|
||||
Set Nickname
|
||||
Scramble Keypad
|
||||
Delete PSBTs
|
||||
Disable USB
|
||||
Normal
|
||||
Disable USB
|
||||
Display Units
|
||||
BTC
|
||||
mBTC
|
||||
bits
|
||||
sats
|
||||
Default Keep
|
||||
Delete PSBTs
|
||||
---
|
||||
|
||||
Normal
|
||||
[NORMAL OPERATION]
|
||||
Ready To Sign
|
||||
Passphrase [MAYBE]
|
||||
Start HSM Mode [MAYBE]
|
||||
Passphrase
|
||||
Start HSM Mode [IF HSM POLICY]
|
||||
Address Explorer
|
||||
Secure Logout
|
||||
Advanced
|
||||
View Identity
|
||||
Upgrade
|
||||
Show Version
|
||||
From MicroSD
|
||||
Bless Firmware
|
||||
Advanced/Tools
|
||||
Backup
|
||||
Backup System
|
||||
Verify Backup
|
||||
Restore Backup
|
||||
Clone Coldcard [MAYBE]
|
||||
Clone Coldcard
|
||||
Export Wallet
|
||||
Bitcoin Core
|
||||
Electrum Wallet
|
||||
Wasabi Wallet
|
||||
Unchained Capital
|
||||
Generic JSON
|
||||
Export XPUB
|
||||
Segwit (BIP-84)
|
||||
Classic (BIP-44)
|
||||
P2WPKH/P2SH (49)
|
||||
Master XPUB
|
||||
Current XFP
|
||||
Dump Summary
|
||||
MicroSD Card
|
||||
Upgrade Firmware
|
||||
Show Version
|
||||
From MicroSD
|
||||
From VirtDisk [IF VIRTDISK ENABLED]
|
||||
Bless Firmware
|
||||
File Management
|
||||
Verify Backup
|
||||
Backup System
|
||||
Dump Summary
|
||||
Export Wallet
|
||||
Bitcoin Core
|
||||
Electrum Wallet
|
||||
Wasabi Wallet
|
||||
Unchained Capital
|
||||
Generic JSON
|
||||
Sign Text File [MAYBE]
|
||||
Upgrade From SD
|
||||
Clone Coldcard [MAYBE]
|
||||
Export XPUB
|
||||
Segwit (BIP-84)
|
||||
Classic (BIP-44)
|
||||
P2WPKH/P2SH (49)
|
||||
Master XPUB
|
||||
Current XFP
|
||||
Dump Summary
|
||||
Sign Text File
|
||||
Clone Coldcard
|
||||
List Files
|
||||
Format Card
|
||||
Paper Wallets [MAYBE]
|
||||
User Management [MAYBE]
|
||||
Derive Entropy
|
||||
NFC File Share [IF NFC ENABLED]
|
||||
Format SD Card
|
||||
Format RAM Disk [IF VIRTDISK ENABLED]
|
||||
Derive Seed B85
|
||||
View Identity
|
||||
Paper Wallets
|
||||
User Management
|
||||
(no users yet)
|
||||
Danger Zone
|
||||
Debug Functions
|
||||
Debug: assert
|
||||
Debug: except
|
||||
Check: BL FW
|
||||
Warm Reset
|
||||
Seed Functions
|
||||
View Seed Words
|
||||
Seed XOR
|
||||
@ -127,14 +190,76 @@ Normal
|
||||
Destroy Seed
|
||||
Lock Down Seed
|
||||
I Am Developer.
|
||||
Wipe Patch Area
|
||||
Serial REPL
|
||||
Wipe LFS
|
||||
Warm Reset
|
||||
Restore Txt Bkup
|
||||
Perform Selftest
|
||||
Set High-Water
|
||||
Wipe HSM Policy [MAYBE]
|
||||
Wipe HSM Policy [IF HSM POLICY]
|
||||
Clear OV cache
|
||||
Testnet Mode
|
||||
Settings space
|
||||
Bitcoin
|
||||
Testnet3
|
||||
Settings Space
|
||||
MCU Key Slots
|
||||
Settings
|
||||
Login Settings
|
||||
Change Main PIN
|
||||
PIN Options
|
||||
Trick PINs
|
||||
Trick PINs:
|
||||
↳22-22
|
||||
Add New Trick
|
||||
Add If Wrong
|
||||
Delete All
|
||||
Set Nickname
|
||||
Scramble Keypad
|
||||
Kill Key
|
||||
Login Countdown
|
||||
Disabled
|
||||
15 minutes
|
||||
30 minutes
|
||||
4 hours
|
||||
1 hour
|
||||
5 minutes
|
||||
8 hours
|
||||
12 hours
|
||||
2 hours
|
||||
3 days
|
||||
48 hours
|
||||
1 week
|
||||
24 hours
|
||||
28 days later
|
||||
Test Login Now
|
||||
Hardware On/Off
|
||||
USB Port
|
||||
Default On
|
||||
Disable USB
|
||||
Virtual Disk
|
||||
Default Off
|
||||
Enable
|
||||
Enable & Auto
|
||||
NFC Sharing
|
||||
Default Off
|
||||
Enable NFC
|
||||
Multisig Wallets
|
||||
(none setup yet)
|
||||
Import from SD
|
||||
Export XPUB
|
||||
Create Airgapped
|
||||
Trust PSBT?
|
||||
Skip Checks?
|
||||
Display Units
|
||||
BTC
|
||||
mBTC
|
||||
bits
|
||||
sats
|
||||
Max Network Fee
|
||||
10% (default)
|
||||
25%
|
||||
50%
|
||||
no limit
|
||||
Idle Timeout
|
||||
2 minutes
|
||||
5 minutes
|
||||
@ -143,51 +268,17 @@ Normal
|
||||
4 hours
|
||||
8 hours
|
||||
Never
|
||||
Login Countdown
|
||||
Disabled
|
||||
5 minutes
|
||||
15 minutes
|
||||
30 minutes
|
||||
1 hour
|
||||
2 hours
|
||||
4 hours
|
||||
8 hours
|
||||
12 hours
|
||||
24 hours
|
||||
48 hours
|
||||
3 days
|
||||
1 week
|
||||
28 days later
|
||||
Max Network Fee
|
||||
10% (default)
|
||||
25%
|
||||
50%
|
||||
no limit
|
||||
PIN Options
|
||||
Multisig Wallets
|
||||
Set Nickname
|
||||
Scramble Keypad
|
||||
Delete PSBTs
|
||||
Disable USB
|
||||
Normal
|
||||
Disable USB
|
||||
Display Units
|
||||
BTC
|
||||
mBTC
|
||||
bits
|
||||
sats
|
||||
Default Keep
|
||||
Delete PSBTs
|
||||
---
|
||||
|
||||
Factory Mode
|
||||
[FACTORY MODE]
|
||||
Bag Me Now
|
||||
DFU Upgrade
|
||||
Show Version
|
||||
Ship W/O Bag
|
||||
Debug Functions
|
||||
Debug: assert
|
||||
Debug: except
|
||||
Check: BL FW
|
||||
Warm Reset
|
||||
Perform Selftest
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -153,7 +153,7 @@ but that's all.
|
||||
## TXID Value
|
||||
|
||||
When sharing a fully-signed transaction, the TXID, if known, will be
|
||||
shared in binary (32 bytes).
|
||||
shared in hex.
|
||||
|
||||
Type: `urn:nfc:ext:bitcoin.org:txid`
|
||||
|
||||
@ -187,6 +187,11 @@ When the Coldcard has signed and finalized a transaction, it can
|
||||
share it in this format. Typically the user will want to broadcast
|
||||
this new transaction on the Bitcoin P2P network.
|
||||
|
||||
## JSON Files
|
||||
|
||||
When exporting wallet details, we need to share a JSON file in most
|
||||
cases. These are marked as "application/json".
|
||||
|
||||
# Examples
|
||||
|
||||
This section will include a number of examples, with analysis of the content.
|
||||
|
||||
@ -273,9 +273,11 @@ Here's what the warning screen looks like:
|
||||
- Dev signs binary release with private "zero key" published in our Github
|
||||
- Give firmware binary file to users (via web download probably)
|
||||
- They upgrade via normal process (copy to MicroSD, or USB upgrade)
|
||||
- On first reboot, big "unauthorized firmware" warning is shown, with delay
|
||||
- On first reboot, big "unauthorized firmware" warning is shown, with delay.
|
||||
- If they know the main PIN (since they are the owner), they follow process to set green light
|
||||
- Next reboot and following, as long as "genuine" mode is maintained, they boot without warnings
|
||||
- Next reboot and following, as long as "genuine" mode is maintained, they boot without
|
||||
warnings (Mk3 and earlier)
|
||||
- Mk4 will always alert on boot-up when running code not approved by Coinkite.
|
||||
|
||||
### Benefits
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
# echo 123456123456 | python3 rolls.py
|
||||
#
|
||||
# - Requires python3 and nothing else!
|
||||
# - This file is <https://coldcard.com/docs/rolls.py>
|
||||
# - This file is <https://coldcardwallet.com/docs/rolls.py>
|
||||
# - Public domain.
|
||||
#
|
||||
from hashlib import sha256
|
||||
|
||||
@ -35,7 +35,7 @@ bit-by-bit into a new phrase.
|
||||
The last word (in 24-word case, which is the only width we support) has
|
||||
8 bits of checksum. For the "parts" (sometimes called "shares") this checksum
|
||||
is calculated as normal for BIP-39, but those final 8-bits are not used in
|
||||
the XOR process. But the checksums still protect the integrity of the
|
||||
the XOR process. But the checksums still protects the integrity of the
|
||||
individual parts.
|
||||
|
||||
Useful properties of this approach:
|
||||
|
||||
117
docs/upgrade-recovery.md
Normal file
117
docs/upgrade-recovery.md
Normal file
@ -0,0 +1,117 @@
|
||||
|
||||
# Firmware Upgrade and Recovery Process
|
||||
|
||||
_This document applies only to the Mk4. Earlier COLDCARDs did not use this approach._
|
||||
|
||||
On the new Mk4 COLDCARD, we have done away with the slow external
|
||||
SPI flash (serial flash) chip entirely. In it's place we use a much
|
||||
faster and huge 64 Mbit PSRAM chip (quad SPI RAM chip: ESP-PSRAM64H).
|
||||
|
||||
This chip is volatile and forgets its contents at power down.
|
||||
|
||||
For working space of PSBT files during signing, that's okay but it
|
||||
can be a problem during firmware upgrades. This document explains
|
||||
how we've solved the risks of firmware upgrades and possible bricking
|
||||
that can happen with power fails at just the wrong time.
|
||||
|
||||
## Firmware Upgrade Process on Mk4
|
||||
|
||||
Steps:
|
||||
|
||||
- firmware image (DFU file) is copied onto the COLDCARD, either by USB or SDCard
|
||||
- the proposed firmware image (up to about 1.5Mbytes) is stored in PSRAM
|
||||
- the user approves the upgrade, and they must process the main PIN code to do that.
|
||||
- firmware image is checked for correct signature from factory (nothing proceeds if
|
||||
not signed by a legit key)
|
||||
- a checksum is calculated over the new firmware, and the current contents of
|
||||
flash, including the bootloader code, its secrets, unique identity bits
|
||||
(for the main chip). We call this the "world checksum".
|
||||
- before anything else happens, we update the main secure element (608B) with
|
||||
the world checksum, and during boot, knowledge of the world checksum is required
|
||||
to light the green genuine light.
|
||||
- the light stays green at this point, and the system could still boot the old firmware
|
||||
- flash erasing and writing of the new firmware starts
|
||||
- this takes about 15 seconds because flash is relatively slow
|
||||
- once that is done, the system resets, and the normal bootup sequence will
|
||||
re-verify the flash for its signature
|
||||
- the green light will be active because the world checksum was already written earlier
|
||||
|
||||
## Recovery Cases
|
||||
|
||||
When the system boots up, it always checks the firmware's signature. If it's
|
||||
corrupt or missing, then we attempt a few different recovery stpes.
|
||||
|
||||
### PSRAM Holds New Firmware
|
||||
|
||||
If the system resets before the flash is erased and programmed completely, we
|
||||
still have enough information in the PSRAM to start over. If main firmware
|
||||
is corrupt, then we look in PSRAM for an image that we might have been
|
||||
burning when we got interrupted. A full signature check is done (so any bitrot
|
||||
will be detected).
|
||||
|
||||
Importantly, the firmware we find in PSRAM at this point must also reconstruct
|
||||
the right "world checksum" as is already stored in the 608. If it does not,
|
||||
then we do not use the contents of the PSRAM and continue with other options.
|
||||
|
||||
We need this policy because the PSRAM is an external chip, and an attacker
|
||||
might try this:
|
||||
|
||||
1) Corrupt the main firmware slightly. Perhaps by shining a UV-C light source
|
||||
at the bare die. Only one bit flip is required. This is done only to trigger
|
||||
recovery mode.
|
||||
|
||||
2) Replace the PSRAM contents with a special firmware image. (It would need
|
||||
to factory signed, but perhaps it has some feature they want to abuse or
|
||||
something.)
|
||||
|
||||
3) Power up the COLDCARD, and it would try to restore the firmware image in PSRAM.
|
||||
|
||||
Because of the world checksum, only the intended firmware can be
|
||||
restored, not any other version. There is no way to for the attacker
|
||||
to change the other parts of the firmware based just corrupting a few
|
||||
bits using UV-C.
|
||||
|
||||
### Recovering from Power Fails
|
||||
|
||||
The most likely way to make the upgrade fail is a power failure
|
||||
during the 15-second period while the firmware is copied from PSRAM
|
||||
to main flash. The PSRAM will forget it's contents, and the COLDCARD
|
||||
no longer has a complete copy of firmware anywhere.
|
||||
|
||||
Most products would be a "brick" at this point, and the docs would
|
||||
warn against power fails during upgrade. However, the Mk4 can read
|
||||
SD Cards to load replacement firmware. The card does not need to
|
||||
be specially prepared, but we recommend erasing it, formating with
|
||||
FAT32 and then copying just the firmware onto the card.
|
||||
|
||||
If the main firmware is corrupt or missing, and the PSRAM does not
|
||||
hold a suitable firmware upgrade, then the screen will show "Insert Card".
|
||||
Once a card is inserted, a search is made for a suitable firmware file.
|
||||
|
||||
All DFU files will be considered, but you must provide the firmware
|
||||
file that you were attempting to upgrade to during the power failure,
|
||||
because the "world checksum" is calculated for each image found on
|
||||
the card. You will not be able to substitue a newer version of firmware.
|
||||
Of course, firmware factory signatures are checked as well.
|
||||
|
||||
|
||||
## Key Entry Sequence
|
||||
|
||||
We do **not** provide a key sequence to enter recovery mode. This
|
||||
would be a nice to have to recover from major bugs in the main firmware,
|
||||
but our security model does not allow it: Since the recovery methods
|
||||
will only replace the bits you used to have with the exact same
|
||||
bits you had previously, there is no need to "recover" if the
|
||||
firmware is already there.
|
||||
|
||||
Attackers would not need a sequence, since they can gitch the clock
|
||||
or use UV-C light on the bare die to change bits.
|
||||
|
||||
|
||||
## Reality of Flipping Bits in Flash
|
||||
|
||||
It's not actually possible to flip a few flash bits because this
|
||||
chip has ECC for all flash cells. It will auto-correct single-bit
|
||||
errors, and for double-bit errors it stops execution completely.
|
||||
So good luck attackers, have a nice day!
|
||||
|
||||
2
external/c-modules/aes256ctr/micropython.mk
vendored
2
external/c-modules/aes256ctr/micropython.mk
vendored
@ -2,7 +2,7 @@
|
||||
# Pure ASM version of AES-256 for CTR mode only
|
||||
#
|
||||
|
||||
ifeq ($(BOARD), COLDCARD)
|
||||
ifdef BOARD
|
||||
|
||||
SRC_USERMOD += $(USERMOD_DIR)/aes_256_ctr.o
|
||||
SRC_USERMOD += $(USERMOD_DIR)/module.c
|
||||
|
||||
2
external/ckcc-protocol
vendored
2
external/ckcc-protocol
vendored
@ -1 +1 @@
|
||||
Subproject commit 87b2db968775303a6becc4b460ad867f922c3d14
|
||||
Subproject commit ef61911047e77d54bc0c6edd5400a424405d814e
|
||||
2
external/libngu
vendored
2
external/libngu
vendored
@ -1 +1 @@
|
||||
Subproject commit ddfd47644962fe20301466199da90a6f292732af
|
||||
Subproject commit a28aef463df3e0a21750381edaea989d7bc5679b
|
||||
2
external/micropython
vendored
2
external/micropython
vendored
@ -1 +1 @@
|
||||
Subproject commit 5917fc199c7e1dd2ef7f9ada8bc48abe0488e9f3
|
||||
Subproject commit 6d7527823c3fd9b2c860c8cc1612cc68f6e2cef2
|
||||
2
external/mpy-qr
vendored
2
external/mpy-qr
vendored
@ -1 +1 @@
|
||||
Subproject commit cb71312dd7369ae6cacd185a81c12683892b971e
|
||||
Subproject commit 3ccf19ca142e9059904f0c8e53b6baeccb9c6b79
|
||||
@ -1,11 +1,15 @@
|
||||
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
|
||||
all: graphics.py
|
||||
all: graphics.py graphics_mk4.py
|
||||
|
||||
SOURCES = $(wildcard *.txt) $(wildcard *.png)
|
||||
SOURCES = $(filter-out mk4_%, $(wildcard *.txt) $(wildcard *.png))
|
||||
MK4_SOURCES = $(wildcard mk4_*.txt) $(wildcard mk4_*.png)
|
||||
|
||||
graphics.py: Makefile $(SOURCES) build.py
|
||||
./build.py $(SOURCES)
|
||||
./build.py graphics.py $(SOURCES)
|
||||
|
||||
graphics_mk4.py: Makefile $(MK4_SOURCES) build.py
|
||||
./build.py graphics_mk4.py $(MK4_SOURCES)
|
||||
|
||||
up: all
|
||||
(cd ../shared; make up)
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
|
||||
# and is covered by GPLv3 license found in COPYING.
|
||||
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
import os, sys, pdb
|
||||
from PIL import Image, ImageOps
|
||||
@ -66,6 +65,8 @@ def crunch(n):
|
||||
|
||||
def doit(outfname, fnames):
|
||||
|
||||
assert outfname.endswith('.py')
|
||||
assert outfname != 'build.py'
|
||||
assert fnames, "need some files"
|
||||
|
||||
fp = open(outfname, 'wt')
|
||||
@ -108,4 +109,4 @@ class Graphics:
|
||||
fp.write("\n# EOF\n")
|
||||
|
||||
if 1:
|
||||
doit('graphics.py', sys.argv[1:])
|
||||
doit(sys.argv[1], sys.argv[2:])
|
||||
|
||||
15
graphics/graphics_mk4.py
Normal file
15
graphics/graphics_mk4.py
Normal file
@ -0,0 +1,15 @@
|
||||
# autogenerated; don't edit
|
||||
#
|
||||
class Graphics:
|
||||
# (w,h, w_bytes, wbits, data)
|
||||
|
||||
mk4_nfc_1 = (126, 49, 16, 0, b'\x00\x7f\xff\xff\xff\xfc\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\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\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\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')
|
||||
|
||||
mk4_nfc_2 = (118, 49, 15, 0, b'\x00\x7f\xff\xff\xff\xfc\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\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\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\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')
|
||||
|
||||
mk4_nfc_3 = (110, 49, 14, 0, b'\x00\x7f\xff\xff\xff\xfc\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\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\xcf\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x03\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\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')
|
||||
|
||||
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')
|
||||
|
||||
|
||||
# EOF
|
||||
49
graphics/mk4_nfc_1.txt
Normal file
49
graphics/mk4_nfc_1.txt
Normal file
@ -0,0 +1,49 @@
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxx xxxx xxxxxxxxxxx
|
||||
xxx xxxx xxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxx xxxx xxxxxxxxxxx
|
||||
xxx xxxx xxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxx xxx xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||
xxx xxx xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||
xxx xxx xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||
xxx xxx xxx xxx yy yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
|
||||
xxx xxx xxx xxx yy n n ffffff ccccc yyyyyyyyyyy
|
||||
xxx xxx xxx xxx yy n n ffffff ccccc yyyyyyyyyyy
|
||||
xxx xxx xxx xxx yy nn n f c c yyyyyyyyyyy
|
||||
xxx xxx xxx xxx yy nn n f c c yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n n f c yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n n f c yyyy yyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n n ffffff c yyyy yyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n n ffffff c yyyy yyy
|
||||
xxx xxx xxx xxx yy n n n f c yyyy yyy
|
||||
xxx xxx xxx xxx yy n n n f c yyyy yyy
|
||||
xxx xxx xxx xxx yy n nn f c yyyyyyyyyyy
|
||||
xxx xxx xxx xxx yy n nn f c yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n f c c yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n f c c yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n f ccccc yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n f ccccc yyyyyyyyyyy
|
||||
xxx xxx xxx xxx yy yyyyyyyyyyy
|
||||
xxx xxx xxx xxx yy yyyyyyyyyyy
|
||||
xxx xxx xxx xxx yy yyyyyyyyyyy
|
||||
xxx xxx xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
49
graphics/mk4_nfc_2.txt
Normal file
49
graphics/mk4_nfc_2.txt
Normal file
@ -0,0 +1,49 @@
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxx xxxx xxxxxxxxxxx
|
||||
xxx xxxx xxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxx xxxx xxxxxxxxxxx
|
||||
xxx xxxx xxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxx xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||
xxx xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||
xxx xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||
xxx xxx xxx yy yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
|
||||
xxx xxx xxx yy n n ffffff ccccc yyyyyyyyyyy
|
||||
xxx xxx xxx yy n n ffffff ccccc yyyyyyyyyyy
|
||||
xxx xxx xxx yy nn n f c c yyyyyyyyyyy
|
||||
xxx xxx xxx yy nn n f c c yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n n f c yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n n f c yyyy yyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n n ffffff c yyyy yyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n n ffffff c yyyy yyy
|
||||
xxx xxx xxx yy n n n f c yyyy yyy
|
||||
xxx xxx xxx yy n n n f c yyyy yyy
|
||||
xxx xxx xxx yy n nn f c yyyyyyyyyyy
|
||||
xxx xxx xxx yy n nn f c yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n f c c yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n f c c yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n f ccccc yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yy n n f ccccc yyyyyyyyyyy
|
||||
xxx xxx xxx yy yyyyyyyyyyy
|
||||
xxx xxx xxx yy yyyyyyyyyyy
|
||||
xxx xxx xxx yy yyyyyyyyyyy
|
||||
xxx xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
49
graphics/mk4_nfc_3.txt
Normal file
49
graphics/mk4_nfc_3.txt
Normal file
@ -0,0 +1,49 @@
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxx xxxx xxxxxxxxxxx
|
||||
xxx xxxx xxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxx xxxx xxxxxxxxxxx
|
||||
xxx xxxx xxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||
xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||
xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||
xxx xxx yy yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxx yy yyyyyyyyyyy
|
||||
xxx xxx yy n n ffffff ccccc yyyyyyyyyyy
|
||||
xxx xxx yy n n ffffff ccccc yyyyyyyyyyy
|
||||
xxx xxx yy nn n f c c yyyyyyyyyyy
|
||||
xxx xxx yy nn n f c c yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxx yy n n n f c yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxx yy n n n f c yyyy yyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxx yy n n n ffffff c yyyy yyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxx yy n n n ffffff c yyyy yyy
|
||||
xxx xxx yy n n n f c yyyy yyy
|
||||
xxx xxx yy n n n f c yyyy yyy
|
||||
xxx xxx yy n nn f c yyyyyyyyyyy
|
||||
xxx xxx yy n nn f c yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxx yy n n f c c yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxx yy n n f c c yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxx yy n n f ccccc yyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxx yy n n f ccccc yyyyyyyyyyy
|
||||
xxx xxx yy yyyyyyyyyyy
|
||||
xxx xxx yy yyyyyyyyyyy
|
||||
xxx xxx yy yyyyyyyyyyy
|
||||
xxx xxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxx yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
49
graphics/mk4_nfc_4.txt
Normal file
49
graphics/mk4_nfc_4.txt
Normal file
@ -0,0 +1,49 @@
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxx xxxx xxxxxxxxxxx
|
||||
xxx xxxx xxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
xxx xxxx xxxxxxxxxxx
|
||||
xxx xxxx xxxxxxxxxxx
|
||||
xxx xxxxxxxxxxxxxxxxx
|
||||
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
|
||||
@ -9,11 +9,17 @@ researchers who wish to analyse the Coldcard more completely.
|
||||
|
||||
# Schematic
|
||||
|
||||

|
||||
|
||||
`schematic-mark4d.png`
|
||||
|
||||
This is the Mark4 rev D schematic.
|
||||
|
||||

|
||||
|
||||
`schematic-mark3b.png`
|
||||
|
||||
This is the Mark3 rev B schematic. It's just one page and pretty simple.
|
||||
This is the Mark3 rev B schematic.
|
||||
|
||||
|
||||
# BOM - Bill of Materials
|
||||
@ -30,11 +36,14 @@ Not included are these minor bits:
|
||||
- the secure bag (with barcode serial number)
|
||||
- pin-recovery card
|
||||
|
||||
`bom-mark4d.xlsx`
|
||||
|
||||
- Same for Mk4 rev D.
|
||||
|
||||
# Important
|
||||
|
||||
- No promises that these files are 100% current because we do make quality improvements.
|
||||
- Copyright of these files, and all design elements of the Coldcard remain with Coinkite Inc.
|
||||
- This information is for research and testing purposes only no warranties.
|
||||
- This information is for research and testing purposes only—no warranties.
|
||||
- **Coinkite does not grant license of this information for comercial use.**
|
||||
|
||||
|
||||
BIN
hardware/bom-mark4b.xlsx
Normal file
BIN
hardware/bom-mark4b.xlsx
Normal file
Binary file not shown.
BIN
hardware/schematic-mark4d.png
Normal file
BIN
hardware/schematic-mark4d.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 443 KiB |
5
misc/binfonter/README.md
Normal file
5
misc/binfonter/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
# Binfonter
|
||||
|
||||
- see <https://github.com/doc-hex/binfonter>
|
||||
- this config file is needed
|
||||
69
misc/binfonter/config.py
Normal file
69
misc/binfonter/config.py
Normal file
@ -0,0 +1,69 @@
|
||||
#
|
||||
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
|
||||
font_files = {
|
||||
'small': 'assets/zevv-peep-iso8859-15-07x14.bdf',
|
||||
'large': 'assets/zevv-peep-iso8859-15-10x20.bdf',
|
||||
'tiny': 'assets/4x6.bdf',
|
||||
}
|
||||
|
||||
# test with:
|
||||
#
|
||||
# ./build.py build --portable && ./testit.py --msg "hello→world←\n↳this\n•Bullet\n•Text" -f small
|
||||
#
|
||||
special_chars = dict(small=[
|
||||
('→', dict(y=0), '''\
|
||||
|
||||
|
||||
|
||||
|
||||
x
|
||||
xx
|
||||
xxxxxxxxxxx
|
||||
xx
|
||||
x
|
||||
'''),
|
||||
('←', dict(y=0), '''\
|
||||
|
||||
|
||||
|
||||
|
||||
x
|
||||
xx
|
||||
xxxxxxxxxxx
|
||||
xx
|
||||
x
|
||||
'''),
|
||||
('↳', dict(y=0), '''\
|
||||
|
||||
x
|
||||
x
|
||||
x
|
||||
x x
|
||||
x xx
|
||||
xxxxxxxxxx
|
||||
xx
|
||||
x
|
||||
'''),
|
||||
('•', dict(y=0), '''\
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
xxx
|
||||
xxx
|
||||
xxx
|
||||
'''),
|
||||
('-', dict(y=0), '''\
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
xxxxx
|
||||
'''),
|
||||
|
||||
])
|
||||
445
misc/obsolete-code/ds28c36b.py
Normal file
445
misc/obsolete-code/ds28c36b.py
Normal file
@ -0,0 +1,445 @@
|
||||
# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
# ds28c36b.py -- Talk to DS28C36B Secure Element (SE2) on Mk4
|
||||
#
|
||||
'''
|
||||
About this chip.
|
||||
|
||||
- 32 pages, 16 general purpose, others are keys: all 32 bytes wide
|
||||
- up to 3 keys
|
||||
- SHA-256 (HMAC) auth or ECC (P256) auth via ECDH
|
||||
- RNG, one useless dec counter
|
||||
- pages individually lockable (nice)
|
||||
- slow: 1ms to read page, 15ms to write, 50-150 for ECC ops, 3ms for HMAC
|
||||
|
||||
Challenges:
|
||||
- reading auth'd data via SHA method is insecure against replay by emulated device
|
||||
- HMAC+SHA auth methods do not allow us to inject nonce/random values, but rely on
|
||||
device serial number and 8 bytes picked by the device (or the attacker/mitm)
|
||||
- concerned it can sign/HMAC user-provided value in a way that might be used to
|
||||
fake other responses, but I'm probably missing something
|
||||
|
||||
Plan:
|
||||
- in factory, random privkey/pubkey generated on-device, locked.
|
||||
- bootrom stores pubkey
|
||||
- use ECDH to generate S value when we need to get auth data in/out
|
||||
'''
|
||||
from utime import sleep_ms
|
||||
from utils import B2A
|
||||
import ngu, ckcc
|
||||
|
||||
# i2c address (7-bits)
|
||||
SE2_ADDR = const(0x1b)
|
||||
|
||||
# page numbers (Table 1)
|
||||
PGN_PUBKEY_A = const(16) # also +1
|
||||
PGN_PUBKEY_B = const(18) # also +1
|
||||
PGN_PUBKEY_C = const(20) # also +1
|
||||
PGN_PRIVKEY_A = const(22)
|
||||
PGN_PRIVKEY_B = const(23)
|
||||
PGN_PRIVKEY_C = const(24)
|
||||
PGN_SECRET_A = const(25)
|
||||
PGN_SECRET_B = const(26)
|
||||
PGN_DEC_COUNTER = const(27)
|
||||
PGN_ROM_OPTIONS = const(28)
|
||||
PGN_GPIO = const(29)
|
||||
PGN_PUBKEY_S = const(30) # also 31, volatile
|
||||
|
||||
# page protection bitmask (Table 11)
|
||||
PROT_RP = const(0x01)
|
||||
PROT_WP = const(0x02)
|
||||
PROT_EM = const(0x04)
|
||||
PROT_APH = const(0x08)
|
||||
PROT_EPH = const(0x10)
|
||||
PROT_AUTH = const(0x20)
|
||||
PROT_ECH = const(0x40)
|
||||
PROT_ECW = const(0x80)
|
||||
|
||||
# random example
|
||||
T_pubkey = b'\xf0\xa5\xba6A\xe1\xd5/\n\xed\xbc8b\xfe\xca\xfe7\xe0\xdd\xbd\xad\x1f\xfb%\xb2cL\xd9>\x13\xfd5 &8\xe0l$/\x14\x94dLT,\x92X.;\x95\xe4\x13\xc3\x03\xc7\n\xc6\x15\xa6\xd2\xfd\x8a\xbe\xba'
|
||||
T_privkey = b'\x8ev\xa4\xce\xb5B\xe2\x90\x06\x925\xa9\xc3\x90\xe61s^\xa9\x10q\x17\x82\\r$\xc0\xbe\xb7\xfc\xa5>'
|
||||
|
||||
|
||||
class SE2Handler:
|
||||
def __init__(self):
|
||||
from machine import I2C
|
||||
self.i2c = I2C(2, freq=500000)
|
||||
|
||||
try:
|
||||
self.read_ident()
|
||||
#self.verify_page(1, 0, bytes(32), b'a'*32)
|
||||
except: pass
|
||||
|
||||
def _write(self, cmd, data=None):
|
||||
# chip will not ack bytes that are past end of command+args, etc.
|
||||
# data can be one int (a byte, will be prefixed w/1) or a sequence of bytes/ints
|
||||
if data is not None:
|
||||
if isinstance(data, int):
|
||||
b = bytes([cmd, 1, data])
|
||||
else:
|
||||
b = bytearray([cmd, len(data)])
|
||||
b.extend(data)
|
||||
else:
|
||||
b = bytes([cmd])
|
||||
|
||||
txl = self.i2c.writeto(SE2_ADDR, b)
|
||||
assert txl == len(b), 'chip nak'
|
||||
|
||||
# all commands need at least tRM recovery time
|
||||
sleep_ms(2) # tRM*2
|
||||
|
||||
def _read(self, num):
|
||||
# responses usually have length in first byte, then status byte, then data
|
||||
# - chip provides 0xff when reading past end of response (does not nak)
|
||||
# - poll until it starts responding again, might take >200ms
|
||||
for retry in range(200):
|
||||
try:
|
||||
rx = self.i2c.readfrom(SE2_ADDR, num)
|
||||
assert len(rx) == num, 'short read'
|
||||
|
||||
return rx
|
||||
except OSError:
|
||||
# expect OSError: [Errno 19] ENODEV
|
||||
pass
|
||||
sleep_ms(2)
|
||||
raise RuntimeError('se2 timeout')
|
||||
|
||||
def _read1(self):
|
||||
# when expecting a single-byte status byte back
|
||||
rx = self._read(2)
|
||||
assert rx[0] == 1
|
||||
return rx[1]
|
||||
|
||||
def _check_result(self, rv):
|
||||
if rv == 0xAA: return
|
||||
raise RuntimeError("bad response: 0x%x" % rv)
|
||||
|
||||
def write_buffer(self, data):
|
||||
# write up to 80 bytes into a RAM buffer on device
|
||||
# - remainder of buffer is set to 0xff, valid length is remembered
|
||||
assert 1 <= len(data) <= 80
|
||||
self._write(0x87, data)
|
||||
|
||||
def read_buffer(self):
|
||||
# length implied from previous write buffer (1..80)
|
||||
self._write(0x5a)
|
||||
rx = self._read(81)
|
||||
assert 0 < rx[0] <= 80
|
||||
assert len(rx) >= rx[0]+1
|
||||
return rx[1:1+rx[0]]
|
||||
|
||||
def read_rng(self, num):
|
||||
# Read RNG, and allow any MiTM to modulate as needed.
|
||||
# - do not use for any purpose
|
||||
assert 1 <= num <= 63
|
||||
self._write(0xd2, num)
|
||||
rx = self._read(1+num)
|
||||
return rx[1:]
|
||||
|
||||
def load_thash(self, buf):
|
||||
# Perform SHA256 and store result into THASH register of chip
|
||||
assert 1<= len(buf) <= 64 # zero not supported by chip, this code only 64
|
||||
tmp = bytes([0xc0]) + bytes(buf)
|
||||
|
||||
self._write(0x33, tmp)
|
||||
self._check_result(self._read1())
|
||||
|
||||
def page_protection(self, page):
|
||||
# return active page protection for page
|
||||
assert 0 <= page < 32
|
||||
self._write(0xaa, page)
|
||||
return self._read1()
|
||||
|
||||
def set_page_protection(self, page, bitmap):
|
||||
# set the protection for one page
|
||||
assert 0 <= page < 32
|
||||
self._write(0xc3, bytes([page, bitmap]))
|
||||
self._check_result(self._read1())
|
||||
|
||||
def read_page(self, page):
|
||||
# unauth version, mitm vulnerable, no encryption
|
||||
assert 0 <= page < 32
|
||||
self._write(0x69, page)
|
||||
rx = self._read(34)
|
||||
if rx[1] == 0x55:
|
||||
raise RuntimeError('read protected')
|
||||
self._check_result(rx[1])
|
||||
|
||||
return rx[2:]
|
||||
|
||||
def write_page(self, page, value):
|
||||
# unauth version, mitm vulnerable
|
||||
assert 0 <= page < 32
|
||||
assert len(value) == 32
|
||||
self._write(0x96, bytes([page])+bytes(value))
|
||||
self._check_result(self._read1())
|
||||
|
||||
def read_enc_page(self, page_num, secret_num, secret=None):
|
||||
# Use secret key, and read encrypted contents of page (XOR w/ HMAC output)
|
||||
# - key for HMAC is pre-shared secret, either key A, B or secret S established by ECDH
|
||||
# - EPH for keys A/B, ECH forces key S
|
||||
# - IMPORTANT: not secure against simple replay, so we always verify
|
||||
assert 0 <= page_num <= 32
|
||||
if secret_num == 2:
|
||||
secret = self.shared_secret
|
||||
else:
|
||||
assert 0 <= secret_num <= 1
|
||||
self._write(0x4b, (secret_num << 6) | page_num)
|
||||
|
||||
rx = self._read(42)
|
||||
self._check_result(rx[1])
|
||||
|
||||
# do decryption
|
||||
chal = rx[2:2+8]
|
||||
enc = rx[2+8:]
|
||||
assert len(enc) == 32
|
||||
|
||||
msg = chal + self.rom_id + bytes([page_num]) + self.manid
|
||||
assert len(msg) == 19
|
||||
|
||||
chk = ngu.hmac.hmac_sha256(secret, msg)
|
||||
readback = bytes(a^b for a,b in zip(chk, enc))
|
||||
|
||||
# Must always verify the response because it can be replayed w/o
|
||||
# knowing any secrets
|
||||
# - also catches wrong decryption if key/secret wrong
|
||||
ok = self.verify_page(page_num, secret_num, readback, secret)
|
||||
if not ok:
|
||||
raise RuntimeError("wrong key/MitM")
|
||||
|
||||
return readback
|
||||
|
||||
def write_enc_page(self, page_num, secret_num, secret, old_data, new_data):
|
||||
# Authenticated write to a page.
|
||||
# - only for pages with APH or EPH
|
||||
# - assume EPH here, with encrypted data tx
|
||||
assert 0 <= page_num <= 32
|
||||
assert 0 <= secret_num <= 2
|
||||
assert len(new_data) == len(old_data) == 32
|
||||
|
||||
PGDV = bytes([page_num | 0x80])
|
||||
|
||||
# This is used for encryption: hmac w/ nonce we pick
|
||||
chal = ngu.random.bytes(8)
|
||||
msg = chal + self.rom_id + PGDV + self.manid
|
||||
assert len(msg) == 19
|
||||
otp = ngu.hmac.hmac_sha256(secret, msg)
|
||||
|
||||
# Must know old data to authenticate change.
|
||||
msg2 = self.rom_id + old_data + new_data + PGDV + self.manid
|
||||
assert len(msg2) == 75
|
||||
auth_chk = ngu.hmac.hmac_sha256(secret, msg2)
|
||||
|
||||
# write that + our nonce into buffer
|
||||
self.write_buffer(auth_chk + chal)
|
||||
|
||||
# encrypt new data
|
||||
args = bytearray(33)
|
||||
args[0] = (secret_num << 6) | page_num
|
||||
for i in range(32):
|
||||
args[i+1] = otp[i] ^ new_data[i]
|
||||
|
||||
self._write(0x99, args)
|
||||
self._check_result(self._read1())
|
||||
|
||||
def read_ident(self):
|
||||
# identity details needed for auth setup
|
||||
b = self.read_page(28)
|
||||
self.rom_id = b[24:24+8]
|
||||
self.manid = b[22:22+2]
|
||||
assert self.rom_id[0] == 0x4c # for this device family
|
||||
|
||||
def pick_keypair(self, kn, lock=False):
|
||||
# use device RNG to pick a EC keypair
|
||||
assert 0 <= kn <= 2 # A,B, or C
|
||||
wpe = 0x1 if lock else 0x0
|
||||
self._write(0xcb, (wpe<<6) | kn)
|
||||
self._check_result(self._read1())
|
||||
|
||||
def verify_page(self, page_num, secret_num, expected, secret=None, hmac=True):
|
||||
# See if chip is holding expected value in a page.
|
||||
# - if this fails, you have the secret wrong, or the data is wrong
|
||||
assert 0 <= secret_num <= 2 # Secret A,B, or S (or PrivkeyA/B/C)
|
||||
assert 0 <= page_num < 32
|
||||
assert len(expected) == 32
|
||||
assert not secret or len(secret) == 32
|
||||
|
||||
chal = ngu.random.bytes(32)
|
||||
self.write_buffer(chal)
|
||||
|
||||
if hmac:
|
||||
arg = (secret_num << 5) | page_num
|
||||
else:
|
||||
assert 0 <= secret_num <= 1 # privkey A,B only
|
||||
arg = ((0x3 + secret_num) << 5) | page_num
|
||||
|
||||
self._write(0xa5, arg)
|
||||
if hmac:
|
||||
rx = self._read(2+32)
|
||||
else:
|
||||
rx = self._read(2+64)
|
||||
|
||||
self._check_result(rx[1])
|
||||
|
||||
msg = self.rom_id + expected + chal + bytes([page_num]) + self.manid
|
||||
assert len(msg) == 75
|
||||
|
||||
if hmac:
|
||||
# response will be HMAC-SHA256 output
|
||||
chk = ngu.hmac.hmac_sha256(secret, msg)
|
||||
return rx[2:] == chk
|
||||
else:
|
||||
# response will be signature over SHA256(msg)
|
||||
# - need p256r1 code to be able to verify here
|
||||
md = ngu.hash.sha256s(msg)
|
||||
|
||||
pn = PGN_PUBKEY_A + (2*secret_num)
|
||||
pubkey = self.read_page(pn) + self.read_page(pn+1)
|
||||
# R and S are swapped in the new signature
|
||||
sig = rx[2+32:2+32+32] + rx[2:2+32]
|
||||
|
||||
args = bytearray(pubkey + md + sig)
|
||||
rv = ckcc.gate(130, args, 0)
|
||||
|
||||
return rv == 0
|
||||
|
||||
def setup_auth(self, ecdh_kn=0):
|
||||
# do "Authenticate ECDSA Public Key" proving we know the privkey for
|
||||
# pubkey held in slot C. Set volatile state: AUTH and maybe W_PUB_KEY and S
|
||||
# - must enable ECDH because we want to read using this authority
|
||||
# - lengths/offsets are all messed in spec
|
||||
# - only supporting READ; we will do our writes before locking page(s)
|
||||
|
||||
# this is remembered in SRAM, but needed in general
|
||||
self.write_page(PGN_PUBKEY_S+0, T_pubkey[:32])
|
||||
self.write_page(PGN_PUBKEY_S+1, T_pubkey[32:])
|
||||
|
||||
chal = ngu.random.bytes(32+32)
|
||||
self.write_buffer(chal)
|
||||
|
||||
cs_offset = 32 # very confusing, might be implied by buffer length?
|
||||
|
||||
md = ngu.hash.sha256s(T_pubkey + chal[0:32])
|
||||
|
||||
# sign md with our privkey
|
||||
args = bytearray(T_privkey + md + bytes(64))
|
||||
rv = ckcc.gate(132, args, 0)
|
||||
assert rv == 0
|
||||
|
||||
sig = bytes(args[-64:])
|
||||
|
||||
args = bytearray()
|
||||
args.append( ((cs_offset-1) << 3) | (ecdh_kn << 2) | 0x2 )
|
||||
args.extend(sig)
|
||||
|
||||
self._write(0xa8, args)
|
||||
self._check_result(self._read1())
|
||||
|
||||
print('auth ok')
|
||||
|
||||
# ecdh multi
|
||||
pubkey_pn = PGN_PUBKEY_A + (ecdh_kn*2)
|
||||
their_pubkey = self.read_page(pubkey_pn) + self.read_page(pubkey_pn+1)
|
||||
|
||||
args = bytearray(their_pubkey + T_privkey + bytes(32))
|
||||
rv = ckcc.gate(133, args, 0)
|
||||
assert rv == 0
|
||||
x = args[-32:]
|
||||
|
||||
# shared secret S will be SHA over X of shared ECDH point + chal[32:]
|
||||
s = ngu.hash.sha256s(x + chal[32:])
|
||||
|
||||
self.shared_secret = s
|
||||
|
||||
return True
|
||||
|
||||
def load_s(self, s):
|
||||
# take string dumped by ROM
|
||||
self.shared_secret = bytes(int(s[i:i+2], 16) for i in range(0, 64, 2))
|
||||
|
||||
def clear_state(self):
|
||||
# No command to reset the volatile state on this chip! Could
|
||||
# be sensitive at times. 608 has a watchdog for this!!
|
||||
self.write_page(PGN_PUBKEY_S+0, bytes(32))
|
||||
self.write_page(PGN_PUBKEY_S+1, bytes(32))
|
||||
|
||||
chal = ngu.random.bytes(32)
|
||||
self.write_buffer(chal)
|
||||
|
||||
# rotate the secret S ... not ideal but only way I've got to change it
|
||||
# - also clears ECDH_SECRET_S flag
|
||||
self._write(0x3c, bytes([ (2<<6), 0 ]))
|
||||
self._read1()
|
||||
|
||||
|
||||
|
||||
|
||||
def selftest_sig(self):
|
||||
# SELFTEST
|
||||
# make sig, check on device
|
||||
md = b'm'*32
|
||||
args = bytearray(T_privkey + md + bytes(64))
|
||||
rv = ckcc.gate(132, args, 0)
|
||||
assert rv == 0
|
||||
|
||||
sig = bytes(args[-64:])
|
||||
|
||||
# check we like our own work
|
||||
args = bytearray(T_pubkey + md + sig)
|
||||
rv = ckcc.gate(130, args, 0)
|
||||
assert rv == 0
|
||||
|
||||
# try against the chip
|
||||
self.write_page(PGN_PUBKEY_S+0, T_pubkey[:32])
|
||||
self.write_page(PGN_PUBKEY_S+1, T_pubkey[32:])
|
||||
|
||||
self.write_buffer(md)
|
||||
|
||||
b = bytearray([0x03])
|
||||
b.extend(sig)
|
||||
self._write(0x59, b)
|
||||
self._check_result(self._read1())
|
||||
|
||||
def first_time(self):
|
||||
# reset and lock the ANON flag == 0, so request/responses require serial number
|
||||
prot = self.page_protection(PGN_ROM_OPTIONS)
|
||||
b = bytearray(self.read_page(PGN_ROM_OPTIONS))
|
||||
if prot != 0:
|
||||
# after first run, should be protected and in right state.
|
||||
assert b[1] == 0x0
|
||||
else:
|
||||
b[1] = 0x00 # same as default
|
||||
self.write_page(PGN_ROM_OPTIONS, b)
|
||||
|
||||
self.read_ident()
|
||||
assert self.manid[1] & 0xc0 == 0x80, 'not B rev?'
|
||||
assert self.rom_id != b'\xff\xff\xff\xff\xff\xff\xff\xff'
|
||||
|
||||
if prot != PROT_APH:
|
||||
# set write lock, except WP isn't possible on this page?! So use APH
|
||||
self.set_page_protection(PGN_ROM_OPTIONS, PROT_APH)
|
||||
|
||||
# pick a keypair for communications (key C, no choice)
|
||||
#self.pick_keypair(kn=2)
|
||||
|
||||
self.write_page(PGN_SECRET_A, b'a'*32)
|
||||
self.write_page(PGN_SECRET_B, b'b'*32)
|
||||
|
||||
if self.page_protection(PGN_PUBKEY_C) == 0:
|
||||
# write a pubkey for AUTH purposes
|
||||
self.write_page(PGN_PUBKEY_C, T_pubkey[:32])
|
||||
self.write_page(PGN_PUBKEY_C+1, T_pubkey[32:])
|
||||
self.set_page_protection(PGN_PUBKEY_C, PROT_AUTH|PROT_RP|PROT_WP)
|
||||
|
||||
# known values in all pages
|
||||
for i in range(0, 16):
|
||||
try:
|
||||
SE2.write_page(i, (b'%x'%i)*32)
|
||||
except: pass
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# EOF
|
||||
@ -1,4 +1,5 @@
|
||||
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
|
||||
# and is covered by GPLv3 license found in COPYING.
|
||||
#
|
||||
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
Thanks for contributing to COLDCARD source code, please be as descriptive as possible.
|
||||
|
||||
- If you haven't yet, please check out our docs https://coldcard.com/docs/
|
||||
- If you haven't yet, please check out our docs https://coldcardwallet.com/docs/
|
||||
- Have you tested your work yet? You can do it with the simulator https://github.com/Coldcard/firmware/blob/master/unix/simulator.py
|
||||
|
||||
*By submitting this work you agree to the [Individual Contributor License Agreement (CLA)](https://raw.githubusercontent.com/Coldcard/firmware/master/CONTRIBUTING.md)*
|
||||
|
||||
47
releases/ChangeLog-mk4.md
Normal file
47
releases/ChangeLog-mk4.md
Normal file
@ -0,0 +1,47 @@
|
||||
## 5.0.3 - 2022-05-04
|
||||
|
||||
- Enhancement: Support P2TR outputs (pay to Taproot) in PSBT files. Allows
|
||||
on-screen verification of P2TR destination addresses (`bc1p..`) so you can send
|
||||
your BTC to them. Does **not** support signing, so you cannot operate a Taproot
|
||||
wallet with COLDCARD as the signing device... yet.
|
||||
|
||||
## 5.0.2 - 2022-04-19
|
||||
|
||||
- Adds NFC support for exporting to all the various wallet-types.
|
||||
- Multisig wallet specs can be exported via NFC, and new multisig wallet can be imported over NFC.
|
||||
- Menu re-org:
|
||||
- "Export Wallet" now directly under Advanced Menu and
|
||||
duplicate link remains under File Management.
|
||||
- "Dump Summary" moved from Backup menu to Export
|
||||
- "Advanced" now "Advanced/Tools"
|
||||
- shuffled contents of Advanced menu
|
||||
- "New Wallet" renamed "New Seed Words"
|
||||
- Text changes to match reality that writing "files" can happen to SD card or VirtDisk or NFC.
|
||||
- New users will see some prompts to help them get started, after seed is set.
|
||||
- 12 word seeds are now an option from the start, either by TRNG or Dice Roll
|
||||
- Dice rolls (for new seed) moved from Import (a misnomer) to "New Seed Words"
|
||||
- Duress wallet (from trick pin) will be 12-words if your true seed is 12-words
|
||||
- Bugfix: allow sending to scripts that we cannot parse, with a warning, to support
|
||||
`OP_RETURN` and other outputs we don't understand well (yet).
|
||||
- Bugfix: sending NFC things into the Coldcard was not working, fixed.
|
||||
|
||||
|
||||
## 5.0.1 - 2022-03-24
|
||||
|
||||
- bugfix: red light whenever MCU keys changed or seed installed first time.
|
||||
|
||||
## 5.0.0 - 2022-03-14
|
||||
|
||||
Mk4 - New hardware
|
||||
|
||||
- (Mk3&Mk4) Performance improved: some internal objects cached to reduce delays when
|
||||
accessing master secret. Helps address explorer, many USB commands and signing.
|
||||
- Enhancement: Power-down during the login countdown now resets the time delay to force
|
||||
attacker (or yourself) to start over with full delay time.
|
||||
- Enhancement: if an XFP of zero is seen in a PSBT file, assume that should be replaced by
|
||||
our current XFP value and try to sign the input (same for change outputs and change-fraud
|
||||
checks). This makes building a workable PSBT file easier and could be used to preserve
|
||||
privacy of XFP value itself. A warning is shown when this happens.
|
||||
- Enhancement: "Advanced > Export XPUB" provides direct way to show XPUB (or ZPUB/YPUB) for
|
||||
BIP-84 / BIP-44 / BIP-49 standard derivations, as a QR. Also can show XFP and master XPUB.
|
||||
- (Mk4) PSBT files up to 2 megabytes now supported
|
||||
@ -1,4 +1,4 @@
|
||||
## 4.1.4 - Sep ??, 2021
|
||||
## 4.1.4 - Apr 26, 2022
|
||||
|
||||
- Enhancement: if an XFP of zero is seen in a PSBT file, assume that should be replaced by
|
||||
our current XFP value and try to sign the input (same for change outputs and change-fraud
|
||||
@ -8,6 +8,8 @@
|
||||
BIP-84 / BIP-44 / BIP-49 standard derivations, as a QR. Also can show XFP and master XPUB.
|
||||
- Bugfix: Updated domain name from `coldcardwallet.com` to `coldcard.com` in docs and few
|
||||
on-screen messages.
|
||||
- Bugfix: allow sending to scripts that we cannot parse, with a warning, to support
|
||||
`OP_RETURN` and other outputs we don't understand well (yet).
|
||||
|
||||
## 4.1.3 - Sep 2, 2021
|
||||
|
||||
|
||||
@ -16,6 +16,6 @@
|
||||
|
||||
## Looking for Firmware Binaries?
|
||||
|
||||
[Go to our website for the latest binaries.](https://coldcard.com/docs/upgrade)
|
||||
[Go to our website for the latest binaries.](https://coldcardwallet.com/docs/upgrade)
|
||||
|
||||
Please do check the signatures using the files here.
|
||||
|
||||
@ -3,7 +3,16 @@ Hash: SHA256
|
||||
|
||||
715a3ec7a91d2366788b14a243e7343de875714b647d1e2bc906ecc5b752d8d9 README.md
|
||||
a0c4d0ac3881a36704f0b620a13c72704531f656ee29368d5aac87dc5f21c7a1 History.md
|
||||
90dc85f0bedbda32270af1aed97ac7356fe15dc4eeb95db11af4bb037c000018 ChangeLog.md
|
||||
7537973f584fffc11d3d258469c0d04201e5b310a868dfe03a7a73abeab84b7f ChangeLog.md
|
||||
b112bc079c687896b686cda087b11182df19b336b0f9c11b625ba3a76de24ee0 ChangeLog-mk4.md
|
||||
5aa2ccc65e2e5279db78b3068b9f3c60c34dd7cc330c2cc1243160db31a2d0f0 2022-05-04T1258-v4.1.5-coldcard.dfu
|
||||
6dbf0aca0f98fb7bdc761eeead4786617b804dad4afb42ee02febf23d31b5e9b 2022-05-04T1254-v5.0.3-mk3-coldcard.dfu
|
||||
d5d9bf50892a4aab6e2ffb106a3d206853a60f879daa94a6f90d68a69bf4fa33 2022-05-04T1252-v5.0.3-mk4-coldcard.dfu
|
||||
9bb028d3e60239f0fcdb3b1f91075785e2c21795789b38c4c619c1f64c2950ef 2022-04-25T1618-v4.1.4-coldcard.dfu
|
||||
a363b1f0d1b27b8f21dbaac32844a59dacab8c2fee126815cda84c4df31fd7cd 2022-04-19T1805-v5.0.2-mk4-coldcard.dfu
|
||||
afb6048397af4093e63567563544098e1cfb45b7ca673536253eb6494d60125c 2022-03-24T1645-v5.0.1-mk3-coldcard.dfu
|
||||
605807bd448711d54e14057892a100bac299a103f5b5fb6466d73f9a36d0694b 2022-03-24T1643-v5.0.1-mk4-coldcard.dfu
|
||||
badd10c078996516c6464c9bfa5f696747dd7206c97d1e6a75d6f5ee0436619a 2022-03-14T1907-v5.0.0-mk4-coldcard.dfu
|
||||
dedfcf8385e35dbdbb26b92f8c0667105404062ad83c8830d809cf9193434d9c 2021-09-02T1752-v4.1.3-coldcard.dfu
|
||||
d01d81305b209dadcf960b9e9d20affb8d4f11e9f9f916c5a06be29298c80dc2 2021-07-28T1347-v4.1.2-coldcard.dfu
|
||||
08e1ec1fd073afbbc9014db6da07fd96c6b20a6710fe491eb805afeba865fe3f 2021-04-30T1748-v4.1.1-coldcard.dfu
|
||||
@ -15,12 +24,12 @@ f05bc8dfed047c0c0abe5ed60621d2d14899b10717221c4af0942d96a1754f33 2021-03-29T192
|
||||
bea27f263b524a66b3ed0a58c16805e98be0d7c3db20c2f7aab3238f2c6a6995 2019-12-19T1623-v3.0.6-coldcard.dfu
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmExD3kACgkQo6MbrVoq
|
||||
WxDE5gf/Sm8Rf278CyD2z1P6nQgqgYK3sIOL+aF68JpjQ8WXzCGA/KIHSVCC8GSt
|
||||
aIJyR0O2F9wiHpAC9f1KHQySqC9VuJ/3z+H8LnLJ7j218O4Y3ojylXw7q61c7FD9
|
||||
327emLD6OuX8L4l6JDlunqb+BJGsTwtpVpdQhWdZjrdYMDxvMdqWqoBuzIgegHJr
|
||||
1tstoM9py7ygzyqXTF2bXMYGiWmZC/Ak7J0TbZYU9dixAF+m/OPQC9nvKYRRiKFM
|
||||
Mcc3ylIdWYEC3sygUodtcItrdSYKBMQ47ubMBewk/Dp8fJwvnIV/cH4pZ5s/2pV7
|
||||
c59R52ZY0vC68Ql0cr5h6+AmikbdNA==
|
||||
=9L7S
|
||||
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmJyl2cACgkQo6MbrVoq
|
||||
WxAyKQf/Wi3erAb8XxcPtdPbggfLvQPRALCNQlbq2pFbpIYS0+uPsbqgGWiM56FU
|
||||
IMptXLx7oGFMlAzrJWA55kGSyngBcjHvxnX/2/9/HmB2G3oWNyfYcwe1pIe+uIZg
|
||||
KjnAkhTGdjjhUbjOCfBgQX/EDufXxlBH01/I847YfaWO7ABRop7cHBWiGOPQlOmN
|
||||
JTkl/KbbxYbCYa8bZFEekCBHcwdfI7gBeI/enNE3ZInTkBPK+57YPNdh16gFtOTm
|
||||
RpNcI2gw8y8kr5pZVrxJkJjfdWGdtLIcJR4sp/B8yS43gXZSOw7Y3O38a9E0VQ9C
|
||||
Ayb9TpbxCbwtf98dsQfG2ojp1nKo2w==
|
||||
=KcSG
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -11,7 +11,7 @@ from menu import MenuSystem, MenuItem, start_chooser
|
||||
from public_constants import AFC_BECH32, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
|
||||
from multisig import MultisigWallet
|
||||
from uasyncio import sleep_ms
|
||||
from nvstore import settings
|
||||
from glob import settings
|
||||
|
||||
def truncate_address(addr):
|
||||
# Truncates address to width of screen, replacing middle chars
|
||||
@ -203,25 +203,30 @@ Press 3 if you really understand and accept these risks.
|
||||
# Displays n addresses by replacing {idx} in path format.
|
||||
# - also for other {account} numbers
|
||||
# - or multisig case
|
||||
from glob import dis
|
||||
from glob import dis, NFC
|
||||
import version
|
||||
|
||||
def make_msg():
|
||||
msg = ''
|
||||
if n > 1:
|
||||
if start == 0:
|
||||
msg = "Press 1 to save to MicroSD."
|
||||
msg = "Press 1 to save into a file."
|
||||
if version.has_fatram and not ms_wallet:
|
||||
msg += " 4 to view QR Codes."
|
||||
msg += " 2 to view QR Codes."
|
||||
if NFC:
|
||||
msg += " Press 3 to share over NFC."
|
||||
msg += '\n\n'
|
||||
msg += "Addresses %d..%d:\n\n" % (start, start + n - 1)
|
||||
else:
|
||||
# single address, from deep path given by user
|
||||
msg += "Showing single address."
|
||||
if version.has_fatram:
|
||||
msg += " Press 4 to view QR Codes."
|
||||
msg += " Press 2 to view QR Codes."
|
||||
if NFC:
|
||||
msg += " Press 3 to share over NFC."
|
||||
msg += '\n\n'
|
||||
|
||||
|
||||
addrs = []
|
||||
chain = chains.current_chain()
|
||||
|
||||
@ -268,19 +273,19 @@ Press 3 if you really understand and accept these risks.
|
||||
msg, addrs = make_msg()
|
||||
|
||||
while 1:
|
||||
ch = await ux_show_story(msg, escape='1479')
|
||||
ch = await ux_show_story(msg, escape='12379')
|
||||
|
||||
if ch == '1':
|
||||
# save addresses to MicroSD signal
|
||||
if ch == 'x':
|
||||
return
|
||||
|
||||
elif ch == '1':
|
||||
# save addresses to MicroSD/VirtDisk
|
||||
await make_address_summary_file(path, addr_fmt, ms_wallet, self.account_num,
|
||||
count=(250 if n!=1 else 1))
|
||||
# .. continue on same screen in case they want to write to multiple cards
|
||||
continue
|
||||
|
||||
if ch == 'x':
|
||||
return
|
||||
|
||||
if ch == '4':
|
||||
elif ch == '2':
|
||||
# switch into a mode that shows them as QR codes
|
||||
if not version.has_fatram or ms_wallet:
|
||||
# requires mk3 and not multisig
|
||||
@ -290,12 +295,22 @@ Press 3 if you really understand and accept these risks.
|
||||
await show_qr_codes(addrs, bool(addr_fmt & AFC_BECH32), start)
|
||||
continue
|
||||
|
||||
if ch == '7' and start>0:
|
||||
elif ch == '3' and NFC:
|
||||
# share table over NFC
|
||||
if n > 1:
|
||||
await NFC.share_text('\n'.join(addrs))
|
||||
else:
|
||||
await NFC.share_deposit_address(addrs[self.idx])
|
||||
continue
|
||||
|
||||
elif ch == '7' and start>0:
|
||||
# go backwards in explorer
|
||||
start -= n
|
||||
elif ch == '9':
|
||||
# go forwards
|
||||
start += n
|
||||
else:
|
||||
continue # 3 in non-NFC mode
|
||||
|
||||
msg, addrs = make_msg()
|
||||
|
||||
@ -332,10 +347,9 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0):
|
||||
stash.blank_object(node)
|
||||
|
||||
async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num, count=250):
|
||||
# write addresses into a text file on the MicroSD
|
||||
# write addresses into a text file on the MicroSD/VirtDisk
|
||||
from glob import dis
|
||||
from files import CardSlot, CardMissingError
|
||||
from actions import needs_microsd
|
||||
from files import CardSlot, CardMissingError, needs_microsd
|
||||
|
||||
# simple: always set number of addresses.
|
||||
# - takes 60 seconds to write 250 addresses on actual hardware
|
||||
|
||||
194
shared/auth.py
194
shared/auth.py
@ -4,7 +4,7 @@
|
||||
# and signing bitcoin transactions.
|
||||
#
|
||||
import stash, ure, ux, chains, sys, gc, uio, version, ngu
|
||||
from public_constants import MAX_TXN_LEN, MSG_SIGNING_MAX_LENGTH, SUPPORTED_ADDR_FORMATS
|
||||
from public_constants import MSG_SIGNING_MAX_LENGTH, SUPPORTED_ADDR_FORMATS
|
||||
from public_constants import AFC_SCRIPT, AF_CLASSIC, AFC_BECH32, AF_P2WPKH
|
||||
from public_constants import STXN_FLAGS_MASK, STXN_FINALIZE, STXN_VISUALIZE, STXN_SIGNED
|
||||
from sffile import SFFile
|
||||
@ -14,8 +14,9 @@ from usb import CCBusyError
|
||||
from utils import HexWriter, xfp2str, problem_file_line, cleanup_deriv_path, B2A
|
||||
from psbt import psbtObject, FatalPSBTIssue, FraudulentChangeOutput
|
||||
from exceptions import HSMDenied
|
||||
from version import has_psram, has_fatram, MAX_TXN_LEN
|
||||
|
||||
# Where in SPI flash the two transactions are (in and out)
|
||||
# Where in SPI flash/PSRAM the two PSBT files are (in and out)
|
||||
TXN_INPUT_OFFSET = 0
|
||||
TXN_OUTPUT_OFFSET = MAX_TXN_LEN
|
||||
|
||||
@ -262,7 +263,7 @@ def sign_txt_file(filename):
|
||||
|
||||
# copy message into memory
|
||||
with CardSlot() as card:
|
||||
with open(filename, 'rt') as fd:
|
||||
with card.open(filename, 'rt') as fd:
|
||||
text = fd.readline().strip()
|
||||
subpath = fd.readline().strip()
|
||||
|
||||
@ -311,7 +312,7 @@ def sign_txt_file(filename):
|
||||
|
||||
for path in [orig_path, None]:
|
||||
try:
|
||||
with CardSlot() as card:
|
||||
with CardSlot(readonly=True) as card:
|
||||
out_full, out_fn = card.pick_filename(target_fname, path)
|
||||
out_path = path
|
||||
if out_full: break
|
||||
@ -326,7 +327,7 @@ def sign_txt_file(filename):
|
||||
# attempt write-out
|
||||
try:
|
||||
with CardSlot() as card:
|
||||
with open(out_full, 'wt') as fd:
|
||||
with card.open(out_full, 'wt') as fd:
|
||||
# save in full RFC style
|
||||
fd.write(RFC_SIGNATURE_TEMPLATE.format(addr=address, msg=text,
|
||||
blockchain='BITCOIN', sig=sig))
|
||||
@ -377,10 +378,19 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
# - gives user-visible string
|
||||
#
|
||||
val = ' '.join(self.chain.render_value(o.nValue))
|
||||
dest = self.chain.render_address(o.scriptPubKey)
|
||||
try:
|
||||
dest = self.chain.render_address(o.scriptPubKey)
|
||||
|
||||
return '%s\n - to address -\n%s\n' % (val, dest)
|
||||
return '%s\n - to address -\n%s\n' % (val, dest)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Handle future things better: allow them to happen at least.
|
||||
self.psbt.warnings.append(
|
||||
('Output?', 'Sending to a script that is not well understood.'))
|
||||
dest = B2A(o.scriptPubKey)
|
||||
|
||||
return '%s\n - to script -\n%s\n' % (val, dest)
|
||||
|
||||
async def interact(self):
|
||||
# Prompt user w/ details and get approval
|
||||
@ -389,8 +399,8 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
# step 1: parse PSBT from sflash into in-memory objects.
|
||||
|
||||
try:
|
||||
dis.fullscreen("Reading...")
|
||||
with SFFile(TXN_INPUT_OFFSET, length=self.psbt_len) 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:
|
||||
if isinstance(exc, MemoryError):
|
||||
@ -407,8 +417,14 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
try:
|
||||
await self.psbt.validate() # might do UX: accept multisig import
|
||||
self.psbt.consider_inputs()
|
||||
|
||||
dis.fullscreen("Validating...", percent=0.33)
|
||||
self.psbt.consider_keys()
|
||||
|
||||
dis.progress_bar(0.66)
|
||||
self.psbt.consider_outputs()
|
||||
|
||||
dis.progress_bar(0.85)
|
||||
except FraudulentChangeOutput as exc:
|
||||
print('FraudulentChangeOutput: ' + exc.args[0])
|
||||
return await self.failure(exc.args[0], title='Change Fraud')
|
||||
@ -505,7 +521,7 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
# do the actual signing.
|
||||
try:
|
||||
dis.fullscreen('Wait...')
|
||||
gc.collect() # visible delay causes by this but also sign_it() below
|
||||
gc.collect() # visible delay caused by this but also sign_it() below
|
||||
self.psbt.sign_it()
|
||||
except FraudulentChangeOutput as exc:
|
||||
return await self.failure(exc.args[0], title='Change Fraud')
|
||||
@ -532,6 +548,7 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
else:
|
||||
self.psbt.serialize(fd)
|
||||
|
||||
fd.close()
|
||||
self.result = (fd.tell(), fd.checksum.digest())
|
||||
|
||||
self.done(redraw=(not txid))
|
||||
@ -539,18 +556,34 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
except BaseException as exc:
|
||||
return await self.failure("PSBT output failed", exc)
|
||||
|
||||
from glob import NFC
|
||||
|
||||
if self.do_finalize and txid and not hsm_active:
|
||||
# Show txid when we can; advisory
|
||||
# - maybe even as QR, hex-encoded in alnum mode
|
||||
tmsg = txid
|
||||
while 1:
|
||||
# Show txid when we can; advisory
|
||||
# - maybe even as QR, hex-encoded in alnum mode
|
||||
tmsg = txid + '\n\n'
|
||||
|
||||
if version.has_fatram:
|
||||
tmsg += '\n\nPress 1 for QR Code of TXID.'
|
||||
if has_fatram:
|
||||
tmsg += 'Press 1 for QR Code of TXID. '
|
||||
if NFC:
|
||||
tmsg += 'Press 3 to share signed txn over NFC.'
|
||||
|
||||
ch = await ux_show_story(tmsg, "Final TXID", escape='1')
|
||||
ch = await ux_show_story(tmsg, "Final TXID", escape='13')
|
||||
|
||||
if version.has_fatram and ch=='1':
|
||||
await show_qr_code(txid, True)
|
||||
if ch=='1' and has_fatram:
|
||||
await show_qr_code(txid, True)
|
||||
continue
|
||||
|
||||
if ch == '3' and NFC:
|
||||
await NFC.share_signed_txn(txid, TXN_OUTPUT_OFFSET,
|
||||
self.result[0], self.result[1])
|
||||
continue
|
||||
break
|
||||
|
||||
# TODO ofter to share / or auto-share over NFC if that seems appropraite
|
||||
#if NFC:
|
||||
#NFC.share_signed_psbt(TXN_OUTPUT_OFFSET, self.result[0], self.result[1])
|
||||
|
||||
def save_visualization(self, msg, sign_text=False):
|
||||
# write text into spi flash, maybe signing it as we go
|
||||
@ -672,7 +705,7 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
|
||||
left = self.psbt.num_outputs - len(largest) - num_change
|
||||
if left > 0:
|
||||
msg.write('.. plus %d more smaller output(s), not shown here, which total: ' % left)
|
||||
msg.write('.. plus %d smaller output(s), not shown here, which total: ' % left)
|
||||
|
||||
# calculate left over value
|
||||
mtot = self.psbt.total_value_out - sum(v for v,t in largest)
|
||||
@ -683,31 +716,50 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
|
||||
|
||||
def sign_transaction(psbt_len, flags=0x0, psbt_sha=None):
|
||||
# transaction (binary) loaded into sflash already, checksum checked
|
||||
# transaction (binary) loaded into sflash/PSRAM already, checksum checked
|
||||
UserAuthorizedAction.check_busy(ApproveTransaction)
|
||||
UserAuthorizedAction.active_request = ApproveTransaction(psbt_len, flags, psbt_sha=psbt_sha)
|
||||
|
||||
# kill any menu stack, and put our thing at the top
|
||||
abort_and_goto(UserAuthorizedAction.active_request)
|
||||
|
||||
def psbt_encoding_taster(taste, psbt_len):
|
||||
# look at first 10 bytes, and detect file encoding (binary, hex, base64)
|
||||
# - return len is upper bound on size because of unknown whitespace
|
||||
from utils import HexStreamer, Base64Streamer, HexWriter, Base64Writer
|
||||
|
||||
if taste[0:5] == b'psbt\xff':
|
||||
decoder = None
|
||||
output_encoder = lambda x: x
|
||||
elif taste[0:10] == b'70736274ff' or taste[0:10] == b'70736274FF':
|
||||
decoder = HexStreamer()
|
||||
output_encoder = HexWriter
|
||||
psbt_len //= 2
|
||||
elif taste[0:6] == b'cHNidP':
|
||||
decoder = Base64Streamer()
|
||||
output_encoder = Base64Writer
|
||||
psbt_len = (psbt_len * 3 // 4) + 10
|
||||
else:
|
||||
raise ValueError("not psbt")
|
||||
|
||||
return decoder, output_encoder, psbt_len
|
||||
|
||||
def sign_psbt_file(filename):
|
||||
async def sign_psbt_file(filename, force_vdisk=False):
|
||||
# sign a PSBT file found on a MicroSD card
|
||||
from files import CardSlot, CardMissingError, securely_blank_file
|
||||
# - or from VirtualDisk (mk4)
|
||||
from files import CardSlot, CardMissingError
|
||||
from glob import dis
|
||||
from sram2 import tmp_buf
|
||||
from utils import HexStreamer, Base64Streamer, HexWriter, Base64Writer
|
||||
|
||||
UserAuthorizedAction.cleanup()
|
||||
|
||||
#print("sign: %s" % filename)
|
||||
|
||||
|
||||
# copy file into our spiflash
|
||||
# - can't work in-place on the card because we want to support writing out to different card
|
||||
# - accepts hex or base64 encoding, but binary prefered
|
||||
with CardSlot() as card:
|
||||
with open(filename, 'rb') as fd:
|
||||
with CardSlot(force_vdisk, readonly=True) as card:
|
||||
with card.open(filename, 'rb') as fd:
|
||||
dis.fullscreen('Reading...')
|
||||
|
||||
# see how long it is
|
||||
@ -718,17 +770,7 @@ def sign_psbt_file(filename):
|
||||
taste = fd.read(10)
|
||||
fd.seek(0)
|
||||
|
||||
if taste[0:5] == b'psbt\xff':
|
||||
decoder = None
|
||||
output_encoder = lambda x: x
|
||||
elif taste[0:10] == b'70736274ff':
|
||||
decoder = HexStreamer()
|
||||
output_encoder = HexWriter
|
||||
psbt_len //= 2
|
||||
elif taste[0:6] == b'cHNidP':
|
||||
decoder = Base64Streamer()
|
||||
output_encoder = Base64Writer
|
||||
psbt_len = (psbt_len * 3 // 4) + 10
|
||||
decoder, output_encoder, psbt_len = psbt_encoding_taster(taste, psbt_len)
|
||||
|
||||
total = 0
|
||||
with SFFile(TXN_INPUT_OFFSET, max_size=psbt_len) as out:
|
||||
@ -766,7 +808,7 @@ def sign_psbt_file(filename):
|
||||
out_fn = None
|
||||
txid = None
|
||||
|
||||
from nvstore import settings
|
||||
from glob import settings
|
||||
import os
|
||||
del_after = settings.get('del', 0)
|
||||
|
||||
@ -782,7 +824,7 @@ def sign_psbt_file(filename):
|
||||
|
||||
for path in [orig_path, None]:
|
||||
try:
|
||||
with CardSlot() as card:
|
||||
with CardSlot(force_vdisk, readonly=True) as card:
|
||||
out_full, out_fn = card.pick_filename(target_fname, path)
|
||||
out_path = path
|
||||
if out_full: break
|
||||
@ -796,12 +838,12 @@ def sign_psbt_file(filename):
|
||||
else:
|
||||
# attempt write-out
|
||||
try:
|
||||
with CardSlot() as card:
|
||||
with CardSlot(force_vdisk) as card:
|
||||
if is_comp and del_after:
|
||||
# don't write signed PSBT if we'd just delete it anyway
|
||||
out_fn = None
|
||||
else:
|
||||
with output_encoder(open(out_full, 'wb')) as fd:
|
||||
with output_encoder(card.open(out_full, 'wb')) as fd:
|
||||
# save as updated PSBT
|
||||
psbt.serialize(fd)
|
||||
|
||||
@ -811,7 +853,7 @@ def sign_psbt_file(filename):
|
||||
base+'-final.txn' if not del_after else 'tmp.txn', out_path)
|
||||
|
||||
if out2_full:
|
||||
with HexWriter(open(out2_full, 'w+t')) as fd:
|
||||
with HexWriter(card.open(out2_full, 'w+t')) as fd:
|
||||
# save transaction, in hex
|
||||
txid = psbt.finalize(fd)
|
||||
|
||||
@ -821,13 +863,13 @@ def sign_psbt_file(filename):
|
||||
txid+'.txn', out_path, overwrite=True)
|
||||
os.rename(out2_full, after_full)
|
||||
|
||||
if del_after:
|
||||
# this can do nothing if they swapped SDCard between steps, which is ok,
|
||||
# but if the original file is still there, this blows it away.
|
||||
# - if not yet final, the foo-part.psbt file stays
|
||||
try:
|
||||
securely_blank_file(filename)
|
||||
except: pass
|
||||
if del_after:
|
||||
# this can do nothing if they swapped SDCard between steps, which is ok,
|
||||
# but if the original file is still there, this blows it away.
|
||||
# - if not yet final, the foo-part.psbt file stays
|
||||
try:
|
||||
card.securely_blank_file(filename)
|
||||
except: pass
|
||||
|
||||
# success and done!
|
||||
break
|
||||
@ -837,6 +879,10 @@ def sign_psbt_file(filename):
|
||||
sys.print_exception(exc)
|
||||
# fall thru to try again
|
||||
|
||||
if force_vdisk:
|
||||
await ux_show_story(prob, title='Error')
|
||||
return
|
||||
|
||||
# prompt them to input another card?
|
||||
ch = await ux_show_story(prob+"Please insert an SDCard to receive signed transaction, "
|
||||
"and press OK.", title="Need Card")
|
||||
@ -912,7 +958,7 @@ class NewPassphrase(UserAuthorizedAction):
|
||||
|
||||
async def interact(self):
|
||||
# prompt them
|
||||
from nvstore import settings
|
||||
from glob import settings
|
||||
|
||||
showit = False
|
||||
while 1:
|
||||
@ -985,13 +1031,13 @@ class ShowAddressBase(UserAuthorizedAction):
|
||||
if not hsm_active:
|
||||
msg = self.get_msg()
|
||||
msg += '\n\nCompare this payment address to the one shown on your other, less-trusted, software.'
|
||||
if version.has_fatram:
|
||||
if has_fatram:
|
||||
msg += ' Press 4 to view QR Code.'
|
||||
|
||||
while 1:
|
||||
ch = await ux_show_story(msg, title=self.title, escape='4')
|
||||
|
||||
if ch == '4' and version.has_fatram:
|
||||
if ch == '4' and has_fatram:
|
||||
await show_qr_code(self.address, (self.addr_fmt & AFC_BECH32))
|
||||
continue
|
||||
|
||||
@ -1163,15 +1209,30 @@ def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False):
|
||||
the_ux.push(UserAuthorizedAction.active_request)
|
||||
|
||||
class FirmwareUpgradeRequest(UserAuthorizedAction):
|
||||
def __init__(self, hdr, length):
|
||||
def __init__(self, hdr, length, hdr_check=False, psram_offset=None):
|
||||
super().__init__()
|
||||
self.hdr = hdr
|
||||
self.length = length
|
||||
self.hdr_check = hdr_check
|
||||
self.psram_offset = psram_offset
|
||||
|
||||
async def interact(self):
|
||||
from version import decode_firmware_header
|
||||
from sflash import SF
|
||||
from utils import check_firmware_hdr
|
||||
|
||||
# check header values
|
||||
if self.hdr_check:
|
||||
# when coming in via USB, this part already done
|
||||
# so the error can be sent back over USB port
|
||||
failed = check_firmware_hdr(self.hdr, self.length)
|
||||
if failed:
|
||||
await ux_show_story(failed, 'Sorry!')
|
||||
|
||||
UserAuthorizedAction.cleanup()
|
||||
self.pop_menu()
|
||||
return
|
||||
|
||||
# Get informed consent to upgrade.
|
||||
date, version, _ = decode_firmware_header(self.hdr)
|
||||
|
||||
msg = '''\
|
||||
@ -1191,16 +1252,27 @@ Binary checksum and signature will be further verified before any changes are ma
|
||||
# - write final file header, so bootloader will see it
|
||||
# - reboot to start process
|
||||
from glob import dis
|
||||
import callgate
|
||||
SF.write(self.length, self.hdr)
|
||||
|
||||
dis.fullscreen('Upgrading...', percent=1)
|
||||
|
||||
callgate.show_logout(2)
|
||||
if not has_psram:
|
||||
from sflash import SF
|
||||
import callgate
|
||||
SF.write(self.length, self.hdr)
|
||||
|
||||
callgate.show_logout(2)
|
||||
else:
|
||||
# Mk4 copies from PSRAM to flash inside bootrom, we have
|
||||
# nothing to do here except start that process.
|
||||
from pincodes import pa
|
||||
pa.firmware_upgrade(self.psram_offset, self.length)
|
||||
# not reached, unless issue?
|
||||
raise RuntimeError("bootrom fail")
|
||||
else:
|
||||
# they don't want to!
|
||||
self.refused = True
|
||||
SF.block_erase(0) # just in case, but not required
|
||||
if not has_psram:
|
||||
from sflash import SF
|
||||
SF.block_erase(0) # just in case, but not required
|
||||
await ux_dramatic_pause("Refused.", 2)
|
||||
|
||||
except BaseException as exc:
|
||||
@ -1210,12 +1282,12 @@ Binary checksum and signature will be further verified before any changes are ma
|
||||
UserAuthorizedAction.cleanup() # because no results to store
|
||||
self.pop_menu()
|
||||
|
||||
def authorize_upgrade(hdr, length):
|
||||
def authorize_upgrade(hdr, length, **kws):
|
||||
# final USB write has come in, get buy-in
|
||||
|
||||
# Do some verification before we even show to the local user
|
||||
UserAuthorizedAction.check_busy()
|
||||
UserAuthorizedAction.active_request = FirmwareUpgradeRequest(hdr, length)
|
||||
UserAuthorizedAction.active_request = FirmwareUpgradeRequest(hdr, length, **kws)
|
||||
|
||||
# kill any menu stack, and put our thing at the top
|
||||
abort_and_goto(UserAuthorizedAction.active_request)
|
||||
|
||||
@ -10,7 +10,7 @@ from ux import ux_show_story, ux_confirm, ux_dramatic_pause
|
||||
import version, ujson
|
||||
from uio import StringIO
|
||||
import seed
|
||||
from nvstore import settings
|
||||
from glob import settings
|
||||
from pincodes import pa, AE_SECRET_LEN
|
||||
|
||||
# we make passwords with this number of words
|
||||
@ -43,6 +43,10 @@ def render_backup_contents():
|
||||
COMMENT('Private key details: ' + chain.name)
|
||||
|
||||
with stash.SensitiveValues(bypass_pw=True) as sv:
|
||||
if sv.deltamode:
|
||||
# die rather than give up our secrets
|
||||
import callgate
|
||||
callgate.fast_wipe()
|
||||
|
||||
if sv.mode == 'words':
|
||||
ADD('mnemonic', bip39.b2a_words(sv.raw))
|
||||
@ -57,15 +61,26 @@ def render_backup_contents():
|
||||
# BTW: everything is really a duplicate of this value
|
||||
ADD('raw_secret', b2a_hex(sv.secret).rstrip(b'0'))
|
||||
|
||||
if pa.has_duress_pin():
|
||||
COMMENT('Duress Wallet (informational)')
|
||||
dpk = sv.duress_root()
|
||||
ADD('duress_xprv', chain.serialize_private(dpk))
|
||||
ADD('duress_xpub', chain.serialize_public(dpk))
|
||||
|
||||
if version.has_608:
|
||||
# save the so-called long-secret
|
||||
ADD('long_secret', b2a_hex(pa.ls_fetch()))
|
||||
|
||||
# Duress wallets (somewhat optional, since derived)
|
||||
if version.mk_num <= 3:
|
||||
if pa.has_duress_pin():
|
||||
COMMENT('Duress Wallet (informational)')
|
||||
dpk, p = sv.duress_root()
|
||||
COMMENT('path = %s' % p)
|
||||
ADD('duress_xprv', chain.serialize_private(dpk))
|
||||
ADD('duress_xpub', chain.serialize_public(dpk))
|
||||
else:
|
||||
from trick_pins import tp
|
||||
for label, path, pairs in tp.backup_duress_wallets(sv):
|
||||
COMMENT()
|
||||
COMMENT(label + ' (informational)')
|
||||
COMMENT(path)
|
||||
for k,v in pairs:
|
||||
ADD(k, v)
|
||||
|
||||
COMMENT('Firmware version (informational)')
|
||||
date, vers, timestamp = version.get_mpy_version()[0:3]
|
||||
@ -92,9 +107,10 @@ def render_backup_contents():
|
||||
|
||||
return rv.getvalue()
|
||||
|
||||
async def restore_from_dict(vals):
|
||||
def restore_from_dict_ll(vals):
|
||||
# Restore from a dict of values. Already JSON decoded.
|
||||
# Reboot on success, return string on failure
|
||||
# Need a Reboot on success, return string on failure
|
||||
# - low-level version, factored out for better testing
|
||||
from glob import dis
|
||||
|
||||
#print("Restoring from: %r" % vals)
|
||||
@ -122,7 +138,6 @@ async def restore_from_dict(vals):
|
||||
check_xprv = chain.serialize_private(node)
|
||||
assert check_xprv == vals['xprv'], 'xprv mismatch'
|
||||
|
||||
|
||||
except Exception as e:
|
||||
return ('Unable to decode raw_secret and '
|
||||
'restore the seed value!\n\n\n'+str(e))
|
||||
@ -164,6 +179,16 @@ async def restore_from_dict(vals):
|
||||
|
||||
if k == 'xfp' or k == 'xpub': continue
|
||||
|
||||
if k == 'tp':
|
||||
# restore trick pins, which may involve many ops
|
||||
if version.mk_num >= 4:
|
||||
from trick_pins import tp
|
||||
try:
|
||||
tp.restore_backup(vals[k])
|
||||
except Exception as exc:
|
||||
sys.print_exception(exc)
|
||||
continue
|
||||
|
||||
settings.set(k[8:], vals[k])
|
||||
|
||||
# write out
|
||||
@ -173,6 +198,14 @@ async def restore_from_dict(vals):
|
||||
import hsm
|
||||
hsm.restore_backup(vals['hsm_policy'])
|
||||
|
||||
|
||||
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 = restore_from_dict_ll(vals)
|
||||
if prob: return prob
|
||||
|
||||
await ux_show_story('Everything has been successfully restored. '
|
||||
'We must now reboot to install the '
|
||||
'updated settings and seed.', title='Success!')
|
||||
@ -276,7 +309,7 @@ async def write_complete_backup(words, fname_pattern, write_sflash=False, allow_
|
||||
fname, nice = card.pick_filename(fname_pattern)
|
||||
|
||||
# do actual write
|
||||
with open(fname, 'wb') as fd:
|
||||
with card.open(fname, 'wb') as fd:
|
||||
if zz:
|
||||
fd.write(hdr)
|
||||
fd.write(zz.body)
|
||||
@ -313,20 +346,19 @@ Insert another SD card and press 2 to make another copy.''' % (nice)
|
||||
Press OK for another copy, or press X to stop.''' % (copy+1, nice), escape='2')
|
||||
if ch == 'x': break
|
||||
|
||||
async def verify_backup_file(fname_or_fd):
|
||||
async def verify_backup_file(fname):
|
||||
# read 7z header, and measure checksums
|
||||
# - no password is wanted/required
|
||||
# - really just checking CRC32, but that's enough against truncated files
|
||||
from files import CardSlot, CardMissingError
|
||||
from actions import needs_microsd
|
||||
from files import CardSlot, CardMissingError, needs_microsd
|
||||
prob = None
|
||||
fd = None
|
||||
|
||||
# filename already picked, open it.
|
||||
try:
|
||||
with CardSlot() as card:
|
||||
with CardSlot(readonly=True) as card:
|
||||
prob = 'Unable to open backup file.'
|
||||
fd = open(fname_or_fd, 'rb') if isinstance(fname_or_fd, str) else fname_or_fd
|
||||
fd = card.open(fname, 'rb')
|
||||
|
||||
prob = 'Unable to read backup file headers. Might be truncated.'
|
||||
compat7z.check_file_headers(fd)
|
||||
@ -347,8 +379,12 @@ async def verify_backup_file(fname_or_fd):
|
||||
await ux_show_story(prob + '\n\nError: ' + str(e))
|
||||
return
|
||||
finally:
|
||||
if fd:
|
||||
fd.close()
|
||||
if fd is not None:
|
||||
try:
|
||||
fd.close()
|
||||
except OSError:
|
||||
# might be already closed on vdisk case due to filesystem unmount/mount
|
||||
pass
|
||||
|
||||
await ux_show_story("Backup file CRC checks out okay.\n\nPlease note this is only a check against accidental truncation and similar. Targeted modifications can still pass this test.")
|
||||
|
||||
@ -375,8 +411,7 @@ async def restore_complete_doit(fname_or_fd, words, file_cleanup=None):
|
||||
# - 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
|
||||
from actions import needs_microsd
|
||||
from files import CardSlot, CardMissingError, needs_microsd
|
||||
|
||||
# build password
|
||||
password = ' '.join(words)
|
||||
@ -384,7 +419,7 @@ async def restore_complete_doit(fname_or_fd, words, file_cleanup=None):
|
||||
prob = None
|
||||
|
||||
try:
|
||||
with CardSlot() as card:
|
||||
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
|
||||
@ -461,7 +496,7 @@ file with an ephemeral public key will be written.''')
|
||||
with CardSlot() as card:
|
||||
fname, nice = card.pick_filename('ccbk-start.json', overwrite=True)
|
||||
|
||||
with open(fname, 'wb') as fd:
|
||||
with card.open(fname, 'wb') as fd:
|
||||
fd.write(ujson.dumps(dict(pubkey=b2a_hex(my_pubkey))))
|
||||
|
||||
except CardMissingError:
|
||||
|
||||
@ -41,18 +41,6 @@ def set_genuine():
|
||||
# does checksum over firmware, and might set green
|
||||
return ckcc.gate(4, None, 3)
|
||||
|
||||
def get_dfu_button():
|
||||
# read current state
|
||||
rv = bytearray(1)
|
||||
ckcc.gate(12, rv, 0)
|
||||
return (rv[0] == 1)
|
||||
|
||||
def get_bl_rng():
|
||||
# read 32 bytes of RNG (test)
|
||||
rv = bytearray(32)
|
||||
assert ckcc.gate(17, rv, 0) == 0
|
||||
return rv
|
||||
|
||||
def get_is_bricked():
|
||||
# see if we are a brick?
|
||||
return ckcc.gate(5, None, 0) != 0
|
||||
@ -68,6 +56,12 @@ def set_rdp_level(n):
|
||||
assert n in {0,1,2}
|
||||
return ckcc.gate(19, None, 100+n)
|
||||
|
||||
def get_factory_mode():
|
||||
# are we in normal RDP=2 mode (else in factory setup time)
|
||||
arg = bytearray(1)
|
||||
ckcc.gate(19, arg, 2)
|
||||
return (arg[0] != 2)
|
||||
|
||||
def get_bag_number():
|
||||
arg = bytearray(32)
|
||||
ckcc.gate(19, arg, 0)
|
||||
@ -96,4 +90,39 @@ def has_608b():
|
||||
ckcc.gate(20, config, 0)
|
||||
return (config[7] >= 0x3)
|
||||
|
||||
def fast_wipe(silent=True):
|
||||
# mk4: wipe seed, also reboots immediately: can stop and show a screen or not
|
||||
ckcc.oneway(23, 0xBeef if silent else 0xdead);
|
||||
|
||||
def fast_brick():
|
||||
# mk4: brick and reboot. Near instant. Shows brick screen.
|
||||
ckcc.oneway(24, 0xDead);
|
||||
|
||||
def mcu_key_usage():
|
||||
# mk4: avail/consumed/total stats, one will be in use typically
|
||||
from ustruct import unpack
|
||||
arg = bytearray(3*4)
|
||||
ckcc.gate(25, arg, 0);
|
||||
return unpack('3I', arg)
|
||||
|
||||
def read_rng(source=2):
|
||||
# return random bytes from a secure source
|
||||
# - first byte is # of valid random bytes
|
||||
arg = bytearray(33)
|
||||
rv = ckcc.gate(26, arg, source);
|
||||
assert not rv
|
||||
return arg[1:1+arg[0]]
|
||||
|
||||
def get_se_parts():
|
||||
# mk4: report part names
|
||||
# - gets a nul-terminated string, w/ newline between them
|
||||
arg = bytearray(80)
|
||||
rv = ckcc.gate(27, arg, 0);
|
||||
if rv:
|
||||
# happens w/ obsolete versions of bootrom that never left Toronto
|
||||
return ['SE1', 'SE2']
|
||||
ln = bytes(arg).find(b'\0')
|
||||
return arg[0:ln].decode().split('\n')
|
||||
|
||||
|
||||
# EOF
|
||||
|
||||
@ -150,7 +150,7 @@ class ChainsBase:
|
||||
# convert nValue from a transaction into human form.
|
||||
# - always be precise
|
||||
# - return (string, units label)
|
||||
from nvstore import settings
|
||||
from glob import settings
|
||||
rz = settings.get('rz', 8)
|
||||
|
||||
if rz == 8:
|
||||
@ -201,9 +201,9 @@ class ChainsBase:
|
||||
if ll == 22 and script[0:2] == b'\x00\x14':
|
||||
return ngu.codecs.segwit_encode(cls.bech32_hrp, 0, script[2:])
|
||||
|
||||
# P2WSH
|
||||
if ll == 34 and script[0:2] == b'\x00\x20':
|
||||
return ngu.codecs.segwit_encode(cls.bech32_hrp, 0, script[2:])
|
||||
# P2WSH, P2TR and later
|
||||
if ll == 34 and script[0] <= 16 and script[1] == 0x20:
|
||||
return ngu.codecs.segwit_encode(cls.bech32_hrp, script[0], script[2:])
|
||||
|
||||
raise ValueError('Unknown payment script', repr(script))
|
||||
|
||||
@ -250,35 +250,28 @@ class BitcoinTestnet(BitcoinMain):
|
||||
|
||||
b44_cointype = 1
|
||||
|
||||
# Add to this list of all choices; keep testnet stuff near bottom
|
||||
# because this order matches UI as presented to users.
|
||||
#
|
||||
AllChains = [
|
||||
BitcoinMain,
|
||||
BitcoinTestnet,
|
||||
]
|
||||
|
||||
|
||||
def get_chain(short_name, btc_default=False):
|
||||
# lookup 'LTC' for example
|
||||
|
||||
for c in AllChains:
|
||||
if c.ctype == short_name:
|
||||
return c
|
||||
|
||||
if btc_default:
|
||||
def get_chain(short_name):
|
||||
# lookup object from name: 'BTC' or 'XTN'
|
||||
if short_name == 'BTC':
|
||||
return BitcoinMain
|
||||
elif short_name == 'XTN':
|
||||
return BitcoinTestnet
|
||||
else:
|
||||
raise KeyError(short_name)
|
||||
|
||||
def current_chain():
|
||||
# return chain matching current setting
|
||||
from nvstore import settings
|
||||
from glob import settings
|
||||
|
||||
chain = settings.get('chain', 'BTC')
|
||||
chain = settings.get('chain', None)
|
||||
if chain is None:
|
||||
return BitcoinMain
|
||||
|
||||
return get_chain(chain)
|
||||
|
||||
# Overbuilt: will only be testnet and mainchain.
|
||||
AllChains = [BitcoinMain, BitcoinTestnet]
|
||||
|
||||
def slip32_deserialize(xp):
|
||||
# .. and classify chain and addr-type, as implied by prefix
|
||||
|
||||
@ -2,7 +2,8 @@
|
||||
#
|
||||
# choosers.py - various interactive menus for setting config values.
|
||||
#
|
||||
from nvstore import settings, SettingsObject
|
||||
from glob import settings
|
||||
from nvstore import SettingsObject
|
||||
|
||||
def max_fee_chooser():
|
||||
from psbt import DEFAULT_MAX_FEE_PERCENTAGE
|
||||
@ -63,82 +64,6 @@ def value_resolution_chooser():
|
||||
|
||||
return which, ch, doit
|
||||
|
||||
def real_countdown_chooser(tag, offset, def_to):
|
||||
# Login countdown length, stored in minutes
|
||||
#
|
||||
lgto_ch = [ 'Disabled',
|
||||
' 5 minutes',
|
||||
'15 minutes',
|
||||
'30 minutes',
|
||||
' 1 hour',
|
||||
' 2 hours',
|
||||
' 4 hours',
|
||||
' 8 hours',
|
||||
'12 hours',
|
||||
'24 hours',
|
||||
'48 hours',
|
||||
' 3 days',
|
||||
' 1 week',
|
||||
'28 days later',
|
||||
]
|
||||
lgto_va = [ 0, 5, 15, 30, 60, 2*60, 4*60, 8*60, 12*60, 24*60, 48*60, 72*60, 7*24*60, 28*24*60]
|
||||
|
||||
# 'disabled' choice not appropriate for cd_lgto case
|
||||
ch = lgto_ch[offset:]
|
||||
va = lgto_va[offset:]
|
||||
|
||||
s = SettingsObject()
|
||||
timeout = s.get(tag, def_to) # in minutes
|
||||
try:
|
||||
which = va.index(timeout)
|
||||
except ValueError:
|
||||
which = 0
|
||||
|
||||
def set_it(idx, text):
|
||||
# save on key0, not normal settings
|
||||
s = SettingsObject()
|
||||
s.set(tag, va[idx])
|
||||
s.save()
|
||||
del s
|
||||
|
||||
return which, ch, set_it
|
||||
|
||||
def countdown_chooser():
|
||||
return real_countdown_chooser('lgto', 0, 0)
|
||||
def cd_countdown_chooser():
|
||||
return real_countdown_chooser('cd_lgto', 1, 60)
|
||||
|
||||
|
||||
def chain_chooser():
|
||||
# Pick Bitcoin or Testnet3 blockchains
|
||||
from chains import AllChains
|
||||
|
||||
chain = settings.get('chain', 'BTC')
|
||||
|
||||
ch = [(i.ctype, i.menu_name or i.name) for i in AllChains ]
|
||||
|
||||
# find index of current choice
|
||||
try:
|
||||
which = [n for n, (k,v) in enumerate(ch) if k == chain][0]
|
||||
except IndexError:
|
||||
which = 0
|
||||
|
||||
def set_chain(idx, text):
|
||||
val = ch[idx][0]
|
||||
assert ch[idx][1] == text
|
||||
settings.set('chain', val)
|
||||
|
||||
try:
|
||||
# update xpub stored in settings
|
||||
import stash
|
||||
with stash.SensitiveValues() as sv:
|
||||
sv.capture_xpub()
|
||||
except ValueError:
|
||||
# no secrets yet, not an error
|
||||
pass
|
||||
|
||||
return which, [t for _,t in ch], set_chain
|
||||
|
||||
def scramble_keypad_chooser():
|
||||
# rngk = randomize keypad for PIN entry
|
||||
|
||||
@ -157,52 +82,28 @@ def scramble_keypad_chooser():
|
||||
|
||||
return which, ch, set
|
||||
|
||||
def kill_key_chooser():
|
||||
# kbtn = single keypress after anti-phishing words will wipe seed
|
||||
|
||||
def set_countdown_pin_mode():
|
||||
# cd_mode = various harm levels
|
||||
s = SettingsObject()
|
||||
which = s.get('cd_mode', 0) # default is brick
|
||||
which = s.get('kbtn', -1)
|
||||
del s
|
||||
which = int(which) + 1
|
||||
|
||||
ch = ['Brick', 'Final PIN', 'Test Mode']
|
||||
ch = ['Disable'] + [str(d) for d in range(10)]
|
||||
|
||||
def set(idx, text):
|
||||
# save it, but "outside" of login PIN
|
||||
s = SettingsObject()
|
||||
s.set('cd_mode', idx)
|
||||
if idx == 0:
|
||||
s.remove_key('kbtn')
|
||||
else:
|
||||
s.set('kbtn', str(idx-1))
|
||||
s.save()
|
||||
del s
|
||||
|
||||
return which, ch, set
|
||||
|
||||
def disable_usb_chooser():
|
||||
value = settings.get('du', 0)
|
||||
ch = [ 'Normal', 'Disable USB']
|
||||
def set_it(idx, text):
|
||||
settings.set('du', idx)
|
||||
|
||||
import pyb
|
||||
from usb import enable_usb, disable_usb
|
||||
cur = pyb.usb_mode()
|
||||
if cur and idx:
|
||||
# usb enabled, but should not be now
|
||||
disable_usb()
|
||||
elif not cur and not idx:
|
||||
# USB disabled, but now should be
|
||||
enable_usb()
|
||||
|
||||
return value, ch, set_it
|
||||
|
||||
def delete_inputs_chooser():
|
||||
# del = (int) 0=normal 1=overwrite+delete input PSBT's, rename outputs
|
||||
del_psbt = settings.get('del', 0)
|
||||
|
||||
ch = [ 'Normal', 'Delete PSBTs']
|
||||
|
||||
def set_del_psbt(idx, text):
|
||||
settings.set('del', idx)
|
||||
|
||||
return del_psbt, ch, set_del_psbt
|
||||
|
||||
|
||||
# EOF
|
||||
|
||||
@ -107,7 +107,7 @@ def check_file_headers(f):
|
||||
sh = SectionHeader.read(f)
|
||||
|
||||
if sh.actual_crc() != fh.crc:
|
||||
print('act=%r expect=%r bits=%r' % (sh.actual_crc(), fh.crc, fh.bits))
|
||||
#print('act=%r expect=%r bits=%r' % (sh.actual_crc(), fh.crc, fh.bits))
|
||||
raise ValueError("Second header has wrong CRC")
|
||||
|
||||
if sh.size > 10000:
|
||||
|
||||
131
shared/countdowns.py
Normal file
131
shared/countdowns.py
Normal file
@ -0,0 +1,131 @@
|
||||
# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
# countdowns.py - various details and chooser menus for setting/showing countdown times
|
||||
#
|
||||
from glob import settings
|
||||
from nvstore import SettingsObject
|
||||
from menu import MenuItem
|
||||
from ux import ux_show_story, ux_dramatic_pause
|
||||
|
||||
# Login countdown length, stored in minutes
|
||||
#
|
||||
lgto_map = {
|
||||
0: 'Disabled',
|
||||
5: ' 5 minutes',
|
||||
15: '15 minutes',
|
||||
30: '30 minutes',
|
||||
60: ' 1 hour',
|
||||
2*60: ' 2 hours',
|
||||
4*60: ' 4 hours',
|
||||
8*60: ' 8 hours',
|
||||
12*60: '12 hours',
|
||||
24*60: '24 hours',
|
||||
48*60: '48 hours',
|
||||
3*24*60: ' 3 days',
|
||||
7*24*60: ' 1 week',
|
||||
28*24*60: '28 days later' }
|
||||
lgto_va = list(lgto_map.keys())
|
||||
lgto_ch = list(lgto_map.values())
|
||||
|
||||
def real_countdown_chooser(tag, offset, def_to):
|
||||
|
||||
# 'disabled' choice not appropriate for cd_lgto case
|
||||
ch = lgto_ch[offset:]
|
||||
va = lgto_va[offset:]
|
||||
|
||||
s = SettingsObject()
|
||||
timeout = s.get(tag, def_to) # in minutes
|
||||
try:
|
||||
which = va.index(timeout)
|
||||
except ValueError:
|
||||
which = 0
|
||||
|
||||
def set_it(idx, text):
|
||||
# save on key0, not normal settings
|
||||
s = SettingsObject()
|
||||
s.set(tag, va[idx])
|
||||
s.save()
|
||||
del s
|
||||
|
||||
return which, ch, set_it
|
||||
|
||||
def countdown_chooser():
|
||||
return real_countdown_chooser('lgto', 0, 0)
|
||||
def cd_countdown_chooser():
|
||||
return real_countdown_chooser('cd_lgto', 1, 60)
|
||||
|
||||
# Mk3 only
|
||||
async def set_countdown_pin(_1, _2, menu_item):
|
||||
# Accept a new PIN to be used to enable this feature
|
||||
from login import LoginUX
|
||||
|
||||
lll = LoginUX()
|
||||
lll.reset()
|
||||
lll.subtitle = "Countdown PIN"
|
||||
|
||||
pin = await lll.get_new_pin(None, allow_clear=True) # a string
|
||||
|
||||
s = SettingsObject()
|
||||
|
||||
from pincodes import pa
|
||||
if pin == pa.pin.decode():
|
||||
# can't compare to others like duress/brickme but will override them
|
||||
await ux_show_story("Must be a unique PIN value!")
|
||||
return
|
||||
elif not pin:
|
||||
# X on first screen does this (better than CLEAR_PIN thing)
|
||||
s.remove_key('cd_pin')
|
||||
msg = 'PIN Cleared.'
|
||||
menu_item.label = "Enable Feature"
|
||||
else:
|
||||
s.set('cd_pin', pin)
|
||||
msg = 'PIN Set.'
|
||||
menu_item.label = "PIN is Set!"
|
||||
|
||||
s.save()
|
||||
|
||||
await ux_dramatic_pause(msg, 3)
|
||||
|
||||
# Mk3 only
|
||||
def set_countdown_pin_mode():
|
||||
# cd_mode = various harm levels
|
||||
s = SettingsObject()
|
||||
which = s.get('cd_mode', 0) # default is brick
|
||||
del s
|
||||
|
||||
ch = ['Brick', 'Final PIN', 'Test Mode']
|
||||
|
||||
def set(idx, text):
|
||||
# save it, but "outside" of login PIN
|
||||
s = SettingsObject()
|
||||
s.set('cd_mode', idx)
|
||||
s.save()
|
||||
del s
|
||||
|
||||
return which, ch, set
|
||||
|
||||
# Mk3 only
|
||||
async def countdown_pin_submenu(*a):
|
||||
# Background and settings for duress-countdown pin
|
||||
s = SettingsObject()
|
||||
pin_set = bool(s.get('cd_pin', 0))
|
||||
|
||||
if not pin_set:
|
||||
ok = await ux_show_story('''\
|
||||
This special PIN will immediately and silently brick the Coldcard, \
|
||||
but as it does that, it shows a normal-looking countdown timer for login. \
|
||||
At the end of the countdown, the Coldcard crashes with a vague error. \
|
||||
|
||||
Instead of complete brick, you may select a test mode (no harm done) or \
|
||||
to consume all but the final PIN attempt.\
|
||||
''')
|
||||
if not ok: return
|
||||
|
||||
|
||||
return [
|
||||
MenuItem('PIN is Set!' if pin_set else 'Enable Feature', f=set_countdown_pin),
|
||||
MenuItem('Countdown Time', chooser=cd_countdown_chooser),
|
||||
MenuItem('Brick Mode', chooser=set_countdown_pin_mode),
|
||||
]
|
||||
|
||||
# EOF
|
||||
@ -1,30 +1,9 @@
|
||||
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
# dev_helper.py - Debug and similar code.
|
||||
# dev_helper.py - Debug code, not shipped.
|
||||
#
|
||||
import ckcc, pyb
|
||||
from uasyncio import sleep_ms
|
||||
|
||||
async def monitor_usb():
|
||||
# Provide a helpful message when virtual serial port is connected.
|
||||
# Without this, they have to press enter or something to see they are connected.
|
||||
from usb import is_vcp_active
|
||||
|
||||
u = pyb.USB_VCP()
|
||||
was_connected = u.isconnected()
|
||||
|
||||
while 1:
|
||||
await sleep_ms(100)
|
||||
|
||||
conn = u.isconnected()
|
||||
if conn and not was_connected:
|
||||
# this direct write bypasses vcp_lockdown code.
|
||||
if is_vcp_active():
|
||||
u.write(b"\r\nWelcome to Coldcard! Press ^C to stop GUI and enter REPL.\r\n")
|
||||
else:
|
||||
u.write(b"\r\nColdcard developer features disabled.\r\n")
|
||||
|
||||
was_connected = conn
|
||||
|
||||
async def usb_keypad_emu():
|
||||
# Take keypresses on USB virtual serial port (when not in REPL mode)
|
||||
@ -32,7 +11,7 @@ async def usb_keypad_emu():
|
||||
#
|
||||
# IMPORTANT:
|
||||
# - code is **not** used in real product, but left here for devs to use
|
||||
# - this code isn't even called; unless you add code to do so, see ../stm32/my_lib_boot2.py
|
||||
# - this code isn't even included in the build normally
|
||||
#
|
||||
await sleep_ms(1000) # avoid slowing the startup
|
||||
|
||||
@ -79,4 +58,8 @@ async def usb_keypad_emu():
|
||||
numpad.inject(k)
|
||||
continue
|
||||
|
||||
def setup():
|
||||
from imptask import IMPT
|
||||
IMPT.start_task('usb_keypad_emu', usb_keypad_emu())
|
||||
|
||||
# EOF
|
||||
|
||||
@ -2,8 +2,9 @@
|
||||
#
|
||||
# display.py - OLED rendering
|
||||
#
|
||||
import machine, ssd1306, uzlib, ckcc
|
||||
import machine, ssd1306, uzlib, ckcc, utime
|
||||
from ssd1306 import SSD1306_SPI
|
||||
from version import is_devmode
|
||||
import framebuf
|
||||
import uasyncio
|
||||
from uasyncio import sleep_ms
|
||||
@ -31,7 +32,13 @@ class Display:
|
||||
dc_pin = Pin('PA8', Pin.OUT)
|
||||
cs_pin = Pin('PA4', Pin.OUT)
|
||||
|
||||
self.dis = SSD1306_SPI(128, 64, spi, dc_pin, reset_pin, cs_pin)
|
||||
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()
|
||||
self.show()
|
||||
|
||||
@ -42,8 +49,11 @@ class Display:
|
||||
return sum(font.lookup(ord(ch)).w for ch in msg)
|
||||
|
||||
def icon(self, x, y, name, invert=0):
|
||||
# see graphics.py (auto generated file) for names
|
||||
w,h, bw, wbits, data = getattr(Graphics, name)
|
||||
if isinstance(name, tuple):
|
||||
w,h, bw, wbits, data = name
|
||||
else:
|
||||
# see graphics.py (auto generated file) for names
|
||||
w,h, bw, wbits, data = getattr(Graphics, name)
|
||||
|
||||
if wbits:
|
||||
data = uzlib.decompress(data, wbits)
|
||||
@ -123,6 +133,12 @@ class Display:
|
||||
pos = min(int(mm*fraction), mm)
|
||||
self.dis.fill_rect(128-2, pos, 1, 8, 1)
|
||||
|
||||
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)
|
||||
self.text(-2, 35, 'V', font=FontTiny, invert=1)
|
||||
|
||||
def fullscreen(self, msg, percent=None, line2=None):
|
||||
# show a simple message "fullscreen".
|
||||
self.clear()
|
||||
@ -142,7 +158,7 @@ class Display:
|
||||
# display a splash screen with some version numbers
|
||||
self.clear()
|
||||
y = 4
|
||||
self.text(None, y, 'Coldcard', font=FontLarge)
|
||||
self.text(None, y, 'COLDCARD', font=FontLarge)
|
||||
self.text(None, y+20, 'Wallet', font=FontLarge)
|
||||
|
||||
from version import get_mpy_version
|
||||
@ -160,6 +176,14 @@ class Display:
|
||||
percent = max(0, min(1.0, percent))
|
||||
self.dis.hline(0, self.HEIGHT-1, int(self.WIDTH * percent), 1)
|
||||
|
||||
def progress_sofar(self, done, total):
|
||||
# Update progress bar, but only if it's been a while since last update
|
||||
if utime.ticks_diff(utime.ticks_ms(), self.last_bar_update) < 100:
|
||||
return
|
||||
self.last_bar_update = utime.ticks_ms()
|
||||
self.progress_bar(done / total)
|
||||
self.show()
|
||||
|
||||
def progress_bar_show(self, percent):
|
||||
# useful as a callback
|
||||
self.progress_bar(percent)
|
||||
|
||||
@ -41,13 +41,8 @@ still backed-up.''')
|
||||
m = MenuSystem([MenuItem(c, f=drv_entro_step2) for c in choices])
|
||||
the_ux.push(m)
|
||||
|
||||
def drv_entro_step2(_1, picked, _2):
|
||||
from glob import dis
|
||||
from files import CardSlot, CardMissingError
|
||||
|
||||
the_ux.pop()
|
||||
|
||||
index = await ux_enter_number("Index Number?", 9999)
|
||||
def bip85_derive(picked, index):
|
||||
# implement the core step of BIP85 from our master secret
|
||||
|
||||
if picked in (0,1,2):
|
||||
# BIP-39 seed phrases (we only support English)
|
||||
@ -72,9 +67,6 @@ def drv_entro_step2(_1, picked, _2):
|
||||
else:
|
||||
raise ValueError(picked)
|
||||
|
||||
dis.fullscreen("Working...")
|
||||
encoded = None
|
||||
|
||||
with stash.SensitiveValues() as sv:
|
||||
node = sv.derive_path(path)
|
||||
entropy = ngu.hmac.hmac_sha512(b'bip-entropy-from-k', node.privkey())
|
||||
@ -84,11 +76,21 @@ def drv_entro_step2(_1, picked, _2):
|
||||
# truncate for this application
|
||||
new_secret = entropy[0:width]
|
||||
|
||||
return new_secret, width, s_mode, path
|
||||
|
||||
# only "new_secret" is interesting past here (node already blanked at this point)
|
||||
del node
|
||||
async def drv_entro_step2(_1, picked, _2):
|
||||
from glob import dis
|
||||
from files import CardSlot, CardMissingError, needs_microsd
|
||||
|
||||
the_ux.pop()
|
||||
|
||||
index = await ux_enter_number("Index Number?", 9999)
|
||||
|
||||
dis.fullscreen("Working...")
|
||||
new_secret, width, s_mode, path = bip85_derive(picked, index)
|
||||
|
||||
# Reveal to user!
|
||||
encoded = None
|
||||
chain = chains.current_chain()
|
||||
qr = None
|
||||
qr_alnum = False
|
||||
|
||||
@ -9,7 +9,7 @@ from utils import xfp2str, swab32
|
||||
from ux import ux_show_story
|
||||
import version, ujson
|
||||
from uio import StringIO
|
||||
from nvstore import settings
|
||||
from glob import settings
|
||||
|
||||
def generate_public_contents():
|
||||
# Generate public details about wallet.
|
||||
@ -118,8 +118,7 @@ be needed for different systems.
|
||||
async def write_text_file(fname_pattern, body, title, total_parts=72):
|
||||
# - total_parts does need not be precise
|
||||
from glob import dis
|
||||
from files import CardSlot, CardMissingError
|
||||
from actions import needs_microsd
|
||||
from files import CardSlot, CardMissingError, needs_microsd
|
||||
|
||||
# choose a filename
|
||||
try:
|
||||
@ -411,16 +410,24 @@ def generate_electrum_wallet(addr_type, account_num=0):
|
||||
async def make_json_wallet(label, generator, fname_pattern='new-wallet.json'):
|
||||
# Record **public** values and helpful data into a JSON file
|
||||
|
||||
from glob import dis
|
||||
from files import CardSlot, CardMissingError
|
||||
from actions import needs_microsd
|
||||
from glob import dis, NFC
|
||||
from files import CardSlot, CardMissingError, needs_microsd
|
||||
|
||||
dis.fullscreen('Generating...')
|
||||
|
||||
body = generator()
|
||||
|
||||
# choose a filename
|
||||
|
||||
if NFC:
|
||||
# Offer to share over NFC regardless of if card inserted, virtdisk active, etc.
|
||||
ch = await ux_show_story('''Press (3) to share %s file over NFC. \
|
||||
Otherwise, OK to proceed normally.''' % label, escape='3')
|
||||
if ch == '3':
|
||||
await NFC.share_json(ujson.dumps(body))
|
||||
return
|
||||
if ch != 'y':
|
||||
return
|
||||
|
||||
# choose a filename and save
|
||||
try:
|
||||
with CardSlot() as card:
|
||||
fname, nice = card.pick_filename(fname_pattern)
|
||||
|
||||
127
shared/files.py
127
shared/files.py
@ -2,9 +2,18 @@
|
||||
#
|
||||
# files.py - MicroSD and related functions.
|
||||
#
|
||||
import pyb, ckcc, os, sys, utime
|
||||
import pyb, ckcc, os, sys, utime, glob
|
||||
from uerrno import ENOENT
|
||||
|
||||
async def needs_microsd():
|
||||
# Standard msg shown if no SD card detected when we need one.
|
||||
from ux import ux_show_story
|
||||
return await ux_show_story("Please insert a MicroSD card before attempting this operation.")
|
||||
|
||||
def _is_ejected():
|
||||
sd = pyb.SDCard()
|
||||
return not sd.present()
|
||||
|
||||
def _try_microsd(bad_fs_ok=False):
|
||||
# Power up, mount the SD card, return False if we can't for some reason.
|
||||
#
|
||||
@ -38,11 +47,11 @@ def _try_microsd(bad_fs_ok=False):
|
||||
#sys.print_exception(exc)
|
||||
return False
|
||||
|
||||
|
||||
def wipe_flash_filesystem():
|
||||
# erase and re-format the flash filesystem (/flash/)
|
||||
# erase and re-format the flash filesystem (/flash/**)
|
||||
import ckcc, pyb
|
||||
from glob import dis
|
||||
from version import mk_num
|
||||
|
||||
dis.fullscreen('Erasing...')
|
||||
os.umount('/flash')
|
||||
@ -52,33 +61,36 @@ def wipe_flash_filesystem():
|
||||
BP_IOCTL_SEC_SIZE = (5)
|
||||
|
||||
# block-level erase
|
||||
fl = pyb.Flash()
|
||||
fl = pyb.Flash(start=0) # start=0 does magic things
|
||||
bsize = fl.ioctl(BP_IOCTL_SEC_SIZE, 0)
|
||||
assert bsize == 512
|
||||
bcount = fl.ioctl(BP_IOCTL_SEC_COUNT, 0)
|
||||
|
||||
blk = bytearray(bsize)
|
||||
ckcc.rng_bytes(blk)
|
||||
|
||||
# trickiness: actual flash blocks are offset by 0x100 (FLASH_PART1_START_BLOCK)
|
||||
# so fake MBR can be inserted. Count also inflated by 2X, but not from ioctl above.
|
||||
for n in range(bcount):
|
||||
fl.writeblocks(n + 0x100, blk)
|
||||
ckcc.rng_bytes(blk)
|
||||
|
||||
dis.progress_bar_show(n*2/bcount)
|
||||
for n in range(bcount):
|
||||
fl.writeblocks(n, blk)
|
||||
ckcc.rng_bytes(blk)
|
||||
dis.progress_bar_show(n/bcount)
|
||||
|
||||
# rebuild and mount /flash
|
||||
dis.fullscreen('Rebuilding...')
|
||||
ckcc.wipe_fs()
|
||||
|
||||
if mk_num == 4:
|
||||
# no need to erase, we just put new FS on top
|
||||
import mk4
|
||||
mk4.make_flash_fs()
|
||||
else:
|
||||
ckcc.wipe_fs()
|
||||
|
||||
# remount it
|
||||
os.mount(fl, '/flash')
|
||||
|
||||
# re-store current settings
|
||||
from nvstore import settings
|
||||
from glob import settings
|
||||
settings.save()
|
||||
|
||||
# remount it
|
||||
os.mount(fl, '/flash')
|
||||
|
||||
def wipe_microsd_card():
|
||||
# Erase and re-format SD card. Not secure erase, because that is too slow.
|
||||
import ckcc, pyb
|
||||
@ -92,7 +104,9 @@ def wipe_microsd_card():
|
||||
sd = pyb.SDCard()
|
||||
assert sd
|
||||
|
||||
if not sd.present(): return
|
||||
if not sd.present():
|
||||
|
||||
return
|
||||
|
||||
# power cycle so card details (like size) are re-read from current card
|
||||
sd.power(0)
|
||||
@ -193,10 +207,23 @@ class CardSlot:
|
||||
from machine import Pin
|
||||
cls.active_led = Pin('SD_ACTIVE', Pin.OUT)
|
||||
|
||||
def __init__(self):
|
||||
self.active = False
|
||||
@classmethod
|
||||
def is_inserted(cls):
|
||||
# debounce?
|
||||
return not _is_ejected()
|
||||
|
||||
def __init__(self, force_vdisk=False, readonly=False):
|
||||
self.mountpt = None
|
||||
self.force_vdisk = force_vdisk
|
||||
self.readonly = readonly
|
||||
self.wrote_files = set()
|
||||
|
||||
def __enter__(self):
|
||||
# Mk4: maybe use our virtual disk in preference to SD Card
|
||||
if glob.VD and (_is_ejected() or self.force_vdisk):
|
||||
self.mountpt = glob.VD.mount(self.readonly)
|
||||
return self
|
||||
|
||||
# Get ready!
|
||||
self.active_led.on()
|
||||
|
||||
@ -211,29 +238,42 @@ class CardSlot:
|
||||
ok = _try_microsd()
|
||||
|
||||
if not ok:
|
||||
self.recover()
|
||||
self._recover()
|
||||
|
||||
raise CardMissingError
|
||||
|
||||
self.active = True
|
||||
self.mountpt = self.get_sd_root() # probably /sd
|
||||
|
||||
return self
|
||||
|
||||
def __exit__(self, *a):
|
||||
self.recover()
|
||||
if self.mountpt == '/sd':
|
||||
self._recover()
|
||||
elif glob.VD:
|
||||
glob.VD.unmount(self.wrote_files)
|
||||
|
||||
self.mountpt = None
|
||||
return False
|
||||
|
||||
def open(self, fname, mode='r', **kw):
|
||||
# open a file for read/write
|
||||
# - track new files for virtdisk case
|
||||
if 'w' in mode:
|
||||
assert not self.readonly
|
||||
self.wrote_files.add(fname)
|
||||
|
||||
return open(fname, mode, **kw)
|
||||
|
||||
def recover(self):
|
||||
def _recover(self):
|
||||
# done using the microSD -- unpower it
|
||||
self.active_led.off()
|
||||
|
||||
self.active = False
|
||||
|
||||
try:
|
||||
assert self.mountpt == '/sd'
|
||||
os.umount('/sd')
|
||||
except: pass
|
||||
|
||||
# important: turn off power so touch can work again
|
||||
# previously important: turn off power so touch can work again (Mk1)
|
||||
sd = pyb.SDCard()
|
||||
sd.power(0)
|
||||
|
||||
@ -246,9 +286,9 @@ class CardSlot:
|
||||
|
||||
def get_paths(self):
|
||||
# (full) paths to check on the card
|
||||
root = self.get_sd_root()
|
||||
|
||||
return [root]
|
||||
#root = self.get_sd_root()
|
||||
#return [root]
|
||||
return [self.mountpt]
|
||||
|
||||
def get_id_hash(self):
|
||||
# hash over card config and serial # details
|
||||
@ -273,10 +313,10 @@ class CardSlot:
|
||||
# - no UI here please
|
||||
import ure
|
||||
|
||||
assert self.active # used out of context mgr
|
||||
assert self.mountpt # else: we got used out of context mgr
|
||||
|
||||
# prefer SD card if we can
|
||||
path = path or (self.get_sd_root() + '/')
|
||||
# put it back where we found it
|
||||
path = path or (self.mountpt + '/')
|
||||
|
||||
assert '/' not in pattern
|
||||
assert '.' in pattern
|
||||
@ -311,19 +351,20 @@ class CardSlot:
|
||||
|
||||
return fname, fname[len(path):]
|
||||
|
||||
def securely_blank_file(full_path):
|
||||
# input PSBT file no longer required; so delete it
|
||||
# - blank with zeros
|
||||
# - rename to garbage (to hide filename after undelete)
|
||||
# - delete
|
||||
# - ok if file missing already (card maybe have been swapped)
|
||||
#
|
||||
# NOTE: we know the FAT filesystem code is simple, see
|
||||
# ../external/micropython/extmod/vfs_fat.[ch]
|
||||
def securely_blank_file(self, full_path):
|
||||
# input PSBT file no longer required; so delete it
|
||||
# - blank with zeros
|
||||
# - rename to garbage (to hide filename after undelete)
|
||||
# - delete
|
||||
# - ok if file missing already (card maybe have been swapped)
|
||||
#
|
||||
# NOTE: we know the FAT filesystem code is simple, see
|
||||
# ../external/micropython/extmod/vfs_fat.[ch]
|
||||
|
||||
path, basename = full_path.rsplit('/', 1)
|
||||
self.wrote_files.discard(full_path)
|
||||
|
||||
path, basename = full_path.rsplit('/', 1)
|
||||
|
||||
with CardSlot() as card:
|
||||
try:
|
||||
blk = bytes(64)
|
||||
|
||||
|
||||
174
shared/flow.py
174
shared/flow.py
@ -2,9 +2,9 @@
|
||||
#
|
||||
# flow.py - Menu structure
|
||||
#
|
||||
from menu import MenuItem
|
||||
from menu import MenuItem, ToggleMenuItem
|
||||
import version
|
||||
from nvstore import settings
|
||||
from glob import settings
|
||||
|
||||
from actions import *
|
||||
from choosers import *
|
||||
@ -14,6 +14,7 @@ from users import make_users_menu
|
||||
from drv_entro import drv_entro_start
|
||||
from backups import clone_start, clone_write_data
|
||||
from xor_seed import xor_split_start, xor_restore_start
|
||||
from countdowns import countdown_pin_submenu, countdown_chooser
|
||||
|
||||
# Optional feature: HSM
|
||||
if version.has_fatram:
|
||||
@ -27,11 +28,18 @@ try:
|
||||
except:
|
||||
make_paper_wallet = None
|
||||
|
||||
if version.mk_num >= 4:
|
||||
from trick_pins import TrickPinMenu
|
||||
trick_pin_menu = TrickPinMenu.make_menu
|
||||
else:
|
||||
trick_pin_menu = None
|
||||
|
||||
#
|
||||
# NOTE: "Always In Title Case"
|
||||
#
|
||||
# - try to keep harmless things as first item: so double-tap of OK does no harm
|
||||
|
||||
# Mk3 and earlier: see Trick Pins for Mk4
|
||||
PinChangesMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem('Change Main PIN', f=pin_changer, arg='main'),
|
||||
@ -54,26 +62,76 @@ if not version.has_608:
|
||||
]
|
||||
|
||||
async def which_pin_menu(_1,_2, item):
|
||||
if version.has_608: return PinChangesMenu
|
||||
from pincodes import pa
|
||||
return PinChangesMenu if not pa.is_secondary else SecondaryPinChangesMenu
|
||||
assert version.mk_num < 4
|
||||
if version.has_608:
|
||||
# mk3
|
||||
return PinChangesMenu
|
||||
else:
|
||||
# mk2 only
|
||||
from pincodes import pa
|
||||
return PinChangesMenu if not pa.is_secondary else SecondaryPinChangesMenu
|
||||
|
||||
#
|
||||
# Predicates
|
||||
#
|
||||
|
||||
def has_secrets():
|
||||
from pincodes import pa
|
||||
return not pa.is_secret_blank()
|
||||
|
||||
SettingsMenu = [
|
||||
def nfc_enabled():
|
||||
from glob import NFC
|
||||
return bool(NFC)
|
||||
|
||||
def vdisk_enabled():
|
||||
return bool(settings.get('vdsk', 0))
|
||||
|
||||
|
||||
HWTogglesMenu = [
|
||||
ToggleMenuItem('USB Port', 'du', ['Default On', 'Disable USB'], invert=True,
|
||||
on_change=change_usb_disable, story='''\
|
||||
Blocks any data over USB port. Useful when your plan is air-gap usage.'''),
|
||||
ToggleMenuItem('Virtual Disk', 'vdsk', ['Default Off', 'Enable', 'Enable & Auto'],
|
||||
predicate=lambda: version.has_psram, on_change=change_virtdisk_enable,
|
||||
story='''Coldcard can emulate a virtual disk drive (4MB) where new PSBT files \
|
||||
can be saved. Signed PSBT files (transactions) will also be saved here. \n\
|
||||
In "auto" mode, selects PSBT as soon as written.'''),
|
||||
ToggleMenuItem('NFC Sharing', 'nfc', ['Default Off', 'Enable NFC'], on_change=change_nfc_enable,
|
||||
story='''\
|
||||
NFC (Near Field Communications) allows a phone to "tap" to send and receive data \
|
||||
with the Coldcard.''',
|
||||
predicate=lambda: version.has_nfc),
|
||||
]
|
||||
|
||||
# all pre-login values
|
||||
LoginPrefsMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem('Idle Timeout', chooser=idle_timeout_chooser),
|
||||
MenuItem('Login Countdown', chooser=countdown_chooser),
|
||||
MenuItem('Max Network Fee', chooser=max_fee_chooser),
|
||||
MenuItem('PIN Options', menu=which_pin_menu),
|
||||
MenuItem('Multisig Wallets', menu=make_multisig_menu),
|
||||
MenuItem('Change Main PIN', f=pin_changer, arg='main'),
|
||||
MenuItem('PIN Options', predicate=lambda: not version.has_se2, menu=which_pin_menu),
|
||||
MenuItem('Trick PINs', predicate=lambda: version.has_se2, menu=trick_pin_menu),
|
||||
MenuItem('Set Nickname', f=pick_nickname),
|
||||
MenuItem('Scramble Keypad', f=pick_scramble),
|
||||
MenuItem('Delete PSBTs', f=pick_inputs_delete),
|
||||
MenuItem('Disable USB', chooser=disable_usb_chooser),
|
||||
MenuItem('Kill Key', f=pick_killkey, predicate=lambda: version.has_se2),
|
||||
MenuItem('Login Countdown', chooser=countdown_chooser),
|
||||
MenuItem('Test Login Now', f=login_now, arg=1),
|
||||
]
|
||||
|
||||
SettingsMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem('Login Settings', menu=LoginPrefsMenu),
|
||||
MenuItem('Hardware On/Off', menu=HWTogglesMenu),
|
||||
MenuItem('Multisig Wallets', menu=make_multisig_menu),
|
||||
MenuItem('Display Units', chooser=value_resolution_chooser),
|
||||
MenuItem('Max Network Fee', chooser=max_fee_chooser),
|
||||
MenuItem('Idle Timeout', chooser=idle_timeout_chooser),
|
||||
ToggleMenuItem('Delete PSBTs', 'del', ['Default Keep', 'Delete PSBTs'],
|
||||
story='''\
|
||||
PSBT files (on SDCard) will be blanked & deleted after they are used. \
|
||||
The signed transaction will be named <TXID>.txn, so the file name does not leak information.
|
||||
|
||||
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.'''),
|
||||
]
|
||||
|
||||
XpubExportMenu = [
|
||||
@ -93,42 +151,57 @@ WalletExportMenu = [
|
||||
MenuItem("Unchained Capital", f=unchained_capital_export),
|
||||
MenuItem("Generic JSON", f=generic_skeleton),
|
||||
MenuItem("Export XPUB", menu=XpubExportMenu),
|
||||
MenuItem("Dump Summary", predicate=has_secrets, f=dump_summary),
|
||||
]
|
||||
|
||||
SDCardMenu = [
|
||||
# useful even if no secrets, may operate on VDisk or SDCard when inserted
|
||||
FileMgmtMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem("Verify Backup", f=verify_backup),
|
||||
MenuItem("Backup System", f=backup_everything),
|
||||
MenuItem("Dump Summary", f=dump_summary),
|
||||
MenuItem('Export Wallet', menu=WalletExportMenu),
|
||||
MenuItem("Backup System", predicate=has_secrets, f=backup_everything),
|
||||
MenuItem('Export Wallet', predicate=has_secrets, menu=WalletExportMenu), #dup elsewhere
|
||||
MenuItem('Sign Text File', predicate=has_secrets, f=sign_message_on_sd),
|
||||
MenuItem('Upgrade From SD', f=microsd_upgrade),
|
||||
#MenuItem('Upgrade Firmware', f=microsd_upgrade),
|
||||
MenuItem('Clone Coldcard', predicate=has_secrets, f=clone_write_data),
|
||||
MenuItem('List Files', f=list_files),
|
||||
MenuItem('Format Card', f=wipe_sd_card),
|
||||
MenuItem('NFC File Share', predicate=nfc_enabled, f=nfc_share_file),
|
||||
MenuItem('Format SD Card', f=wipe_sd_card),
|
||||
MenuItem('Format RAM Disk', predicate=vdisk_enabled, f=wipe_vdisk),
|
||||
]
|
||||
|
||||
UpgradeMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem('Show Version', f=show_version),
|
||||
MenuItem('From MicroSD', f=microsd_upgrade),
|
||||
MenuItem('From MicroSD', f=microsd_upgrade), # mk4: misnomer, could be vdisk too
|
||||
MenuItem('From VirtDisk', predicate=vdisk_enabled, f=microsd_upgrade),
|
||||
MenuItem('Bless Firmware', f=bless_flash),
|
||||
]
|
||||
|
||||
DevelopersMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem("Normal USB Mode", f=dev_enable_protocol),
|
||||
MenuItem("Enable USB REPL", f=dev_enable_vcp),
|
||||
MenuItem("Enable USB Disk", f=dev_enable_disk),
|
||||
MenuItem("Wipe Patch Area", f=wipe_filesystem),
|
||||
MenuItem('Warm Reset', f=reset_self),
|
||||
MenuItem("Restore Txt Bkup", f=restore_everything_cleartext),
|
||||
]
|
||||
if version.mk_num < 4:
|
||||
DevelopersMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem("Normal USB Mode", f=dev_enable_protocol),
|
||||
MenuItem("Enable USB REPL", f=dev_enable_vcp),
|
||||
MenuItem("Enable USB Disk", f=dev_enable_disk),
|
||||
MenuItem("Wipe Patch Area", f=wipe_filesystem), # needs better label
|
||||
MenuItem('Warm Reset', f=reset_self),
|
||||
MenuItem("Restore Txt Bkup", f=restore_everything_cleartext),
|
||||
]
|
||||
else:
|
||||
# Mk4 and later
|
||||
from mk4 import dev_enable_repl
|
||||
DevelopersMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem("Serial REPL", f=dev_enable_repl),
|
||||
MenuItem("Wipe LFS", f=wipe_filesystem), # kills settings, HSM stuff
|
||||
MenuItem('Warm Reset', f=reset_self),
|
||||
MenuItem("Restore Txt Bkup", f=restore_everything_cleartext),
|
||||
]
|
||||
|
||||
AdvancedVirginMenu = [ # No PIN, no secrets yet (factory fresh)
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem("View Identity", f=view_ident),
|
||||
MenuItem('Upgrade firmware', menu=UpgradeMenu),
|
||||
MenuItem('Upgrade Firmware', menu=UpgradeMenu),
|
||||
MenuItem('Paper Wallets', f=make_paper_wallet, predicate=lambda: make_paper_wallet),
|
||||
MenuItem('Perform Selftest', f=start_selftest),
|
||||
MenuItem('Secure Logout', f=logout_now),
|
||||
@ -137,7 +210,8 @@ AdvancedVirginMenu = [ # No PIN, no secrets yet (factory fresh)
|
||||
AdvancedPinnedVirginMenu = [ # Has PIN but no secrets yet
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem("View Identity", f=view_ident),
|
||||
MenuItem("Upgrade", menu=UpgradeMenu),
|
||||
MenuItem("Upgrade Firmware", menu=UpgradeMenu),
|
||||
MenuItem("File Management", menu=FileMgmtMenu),
|
||||
MenuItem('Paper Wallets', f=make_paper_wallet, predicate=lambda: make_paper_wallet),
|
||||
MenuItem('Perform Selftest', f=start_selftest),
|
||||
MenuItem("I Am Developer.", menu=maybe_dev_menu),
|
||||
@ -171,13 +245,16 @@ DangerZoneMenu = [
|
||||
MenuItem("Debug Functions", menu=DebugFunctionsMenu), # actually harmless
|
||||
MenuItem("Seed Functions", menu=SeedFunctionsMenu),
|
||||
MenuItem("I Am Developer.", menu=maybe_dev_menu),
|
||||
MenuItem("Wipe Patch Area", f=wipe_filesystem), # needs better label
|
||||
MenuItem('Perform Selftest', f=start_selftest), # little harmful
|
||||
MenuItem("Set High-Water", f=set_highwater),
|
||||
MenuItem('Wipe HSM Policy', f=wipe_hsm_policy, predicate=hsm_policy_available),
|
||||
MenuItem('Clear OV cache', f=wipe_ovc),
|
||||
MenuItem('Testnet Mode', f=confirm_testnet_mode),
|
||||
MenuItem('Settings space', f=show_settings_space),
|
||||
ToggleMenuItem('Testnet Mode', 'chain', ['Bitcoin', 'Testnet3'],
|
||||
value_map=['BTC', 'XTN'],
|
||||
story="Testnet must only be used by developers because \
|
||||
correctly- crafted transactions signed on Testnet could be broadcast on Mainnet."),
|
||||
MenuItem('Settings Space', f=show_settings_space),
|
||||
MenuItem('MCU Key Slots', predicate=lambda: version.has_se2, f=show_mcu_keys_left),
|
||||
]
|
||||
|
||||
BackupStuffMenu = [
|
||||
@ -186,19 +263,18 @@ BackupStuffMenu = [
|
||||
MenuItem("Verify Backup", f=verify_backup),
|
||||
MenuItem("Restore Backup", f=restore_everything), # just a redirect really
|
||||
MenuItem('Clone Coldcard', predicate=has_secrets, f=clone_write_data),
|
||||
MenuItem("Dump Summary", f=dump_summary),
|
||||
]
|
||||
|
||||
AdvancedNormalMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem("View Identity", f=view_ident),
|
||||
MenuItem("Upgrade", menu=UpgradeMenu),
|
||||
MenuItem("Backup", menu=BackupStuffMenu),
|
||||
MenuItem("MicroSD Card", menu=SDCardMenu),
|
||||
MenuItem('Export Wallet', predicate=has_secrets, menu=WalletExportMenu), # also inside FileMgmt
|
||||
MenuItem("Upgrade Firmware", menu=UpgradeMenu),
|
||||
MenuItem("File Management", menu=FileMgmtMenu),
|
||||
MenuItem('Derive Seed B85', f=drv_entro_start),
|
||||
MenuItem("View Identity", f=view_ident),
|
||||
MenuItem('Paper Wallets', f=make_paper_wallet, predicate=lambda: make_paper_wallet),
|
||||
MenuItem('User Management', menu=make_users_menu, predicate=lambda: version.has_fatram),
|
||||
MenuItem('Derive Entropy', f=drv_entro_start),
|
||||
MenuItem("Export XPUB", menu=XpubExportMenu),
|
||||
MenuItem("Danger Zone", menu=DangerZoneMenu),
|
||||
]
|
||||
|
||||
@ -206,7 +282,7 @@ AdvancedNormalMenu = [
|
||||
VirginSystem = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem('Choose PIN Code', f=initial_pin_setup),
|
||||
MenuItem('Advanced', menu=AdvancedVirginMenu),
|
||||
MenuItem('Advanced/Tools', menu=AdvancedVirginMenu),
|
||||
MenuItem('Bag Number', f=show_bag_number),
|
||||
MenuItem('Help', f=virgin_help),
|
||||
]
|
||||
@ -219,17 +295,25 @@ ImportWallet = [
|
||||
MenuItem("Restore Backup", f=restore_everything),
|
||||
MenuItem("Clone Coldcard", menu=clone_start),
|
||||
MenuItem("Import XPRV", f=import_xprv),
|
||||
MenuItem("Dice Rolls", f=import_from_dice),
|
||||
MenuItem("Seed XOR", f=xor_restore_start),
|
||||
]
|
||||
|
||||
|
||||
NewSeedMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem("24 Word (default)", f=pick_new_seed_24),
|
||||
MenuItem("12 Word", f=pick_new_seed_12),
|
||||
MenuItem("24 Word Dice Roll", f=new_from_dice_24),
|
||||
MenuItem("12 Word Dice Roll", f=new_from_dice_12),
|
||||
]
|
||||
|
||||
# has PIN, but no secret seed yet
|
||||
EmptyWallet = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem('New Wallet', f=pick_new_wallet),
|
||||
MenuItem('New Seed Words', menu=NewSeedMenu),
|
||||
MenuItem('Import Existing', menu=ImportWallet),
|
||||
MenuItem('Help', f=virgin_help),
|
||||
MenuItem('Advanced', menu=AdvancedPinnedVirginMenu),
|
||||
MenuItem('Advanced/Tools', menu=AdvancedPinnedVirginMenu),
|
||||
MenuItem('Settings', menu=SettingsMenu),
|
||||
]
|
||||
|
||||
@ -242,7 +326,7 @@ NormalSystem = [
|
||||
MenuItem('Start HSM Mode', f=start_hsm_menu_item, predicate=hsm_policy_available),
|
||||
MenuItem("Address Explorer", f=address_explore),
|
||||
MenuItem('Secure Logout', f=logout_now),
|
||||
MenuItem('Advanced', menu=AdvancedNormalMenu),
|
||||
MenuItem('Advanced/Tools', menu=AdvancedNormalMenu),
|
||||
MenuItem('Settings', menu=SettingsMenu),
|
||||
]
|
||||
|
||||
|
||||
56
shared/ftux.py
Normal file
56
shared/ftux.py
Normal file
@ -0,0 +1,56 @@
|
||||
# (c) Copyright 2022 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
# ftux.py - First Time User Experience! A new ride at the waterpark.
|
||||
#
|
||||
import version
|
||||
from glob import settings
|
||||
from ux import ux_show_story, the_ux, ux_dramatic_pause
|
||||
from actions import change_nfc_enable, change_virtdisk_enable, change_usb_disable
|
||||
|
||||
COMMON = '''\
|
||||
\n
|
||||
You can change this later under Settings > Hardware On/Off.'''
|
||||
|
||||
class FirstTimeUX:
|
||||
async def interact(self):
|
||||
# Help them enable the good stuff.
|
||||
# - they might have already enabled things
|
||||
# - some features not on mk3
|
||||
|
||||
if version.has_nfc and not settings.get('nfc', 0):
|
||||
msg = '''Enable NFC/Tap?\n\n\
|
||||
Lets you Tap your mobile phone on the COLDCARD and \
|
||||
transfer data easily via NFC.''' + COMMON
|
||||
ch = await ux_show_story(msg)
|
||||
if ch == 'y':
|
||||
settings.set('nfc', 1)
|
||||
await change_nfc_enable(1)
|
||||
await ux_dramatic_pause('Enabled.', 1)
|
||||
|
||||
if version.has_psram and not settings.get('vdsk', 0):
|
||||
msg = '''Enable USB Drive?\n\n\
|
||||
Connect your COLDCARD directly as a USB flash drive \
|
||||
to your phone or desktop. You will be able to drag-n-drop or \
|
||||
save PSBT files like other drives/volumes.''' + COMMON
|
||||
ch = await ux_show_story(msg)
|
||||
if ch == 'y':
|
||||
# put them into full-auto mode: 2
|
||||
settings.set('vdsk', 2)
|
||||
await change_virtdisk_enable(2)
|
||||
await ux_dramatic_pause('Enabled.', 1)
|
||||
|
||||
if not settings.get('vdsk', 0) and not settings.get('du', 0):
|
||||
msg = '''Disable USB port?\n\n\
|
||||
If you intend to operate in Air-Gap mode, where this COLDCARD \
|
||||
is never connected to anything but power, then this will disable the USB port.''' + COMMON
|
||||
ch = await ux_show_story(msg)
|
||||
|
||||
if ch == 'y':
|
||||
settings.set('du', 1)
|
||||
await change_usb_disable(1)
|
||||
await ux_dramatic_pause('Disabled.', 1)
|
||||
|
||||
# done
|
||||
the_ux.pop()
|
||||
|
||||
# EOF
|
||||
@ -14,5 +14,17 @@ numpad = None
|
||||
# global ptr to HSM policy, if any (supported on Mk3+ only)
|
||||
hsm_active = None
|
||||
|
||||
# setup by main.py, expected to always be present
|
||||
settings = None
|
||||
|
||||
# PSRAM (on Mk4 only)
|
||||
PSRAM = None
|
||||
|
||||
# Virtual Disk (Mk4)
|
||||
VD = None
|
||||
|
||||
# NFC interface (Mk4, and can be disabled)
|
||||
NFC = None
|
||||
|
||||
|
||||
# EOF
|
||||
|
||||
1
shared/graphics_mk4.py
Symbolic link
1
shared/graphics_mk4.py
Symbolic link
@ -0,0 +1 @@
|
||||
../graphics/graphics_mk4.py
|
||||
@ -5,3 +5,6 @@
|
||||
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from ubinascii import unhexlify as a2b_hex
|
||||
|
||||
import uasyncio
|
||||
arun = uasyncio.run
|
||||
|
||||
@ -9,7 +9,7 @@ from ustruct import pack, unpack
|
||||
from exceptions import IncorrectUTXOAmount
|
||||
from ubinascii import b2a_base64, a2b_base64
|
||||
from serializations import COutPoint, uint256_from_str
|
||||
from nvstore import settings
|
||||
from glob import settings
|
||||
|
||||
# Very limited space in serial flash, so we compress as much as possible:
|
||||
# - would be bad for privacy to store these **UTXO amounts** in plaintext
|
||||
|
||||
@ -820,8 +820,7 @@ def hsm_status_report():
|
||||
# Return a JSON-able object. Documented and external programs
|
||||
# rely on this output... and yet, don't overshare either.
|
||||
from auth import UserAuthorizedAction
|
||||
from glob import hsm_active
|
||||
from nvstore import settings
|
||||
from glob import hsm_active, settings
|
||||
from hsm_ux import ApproveHSMPolicy
|
||||
|
||||
rv = dict()
|
||||
|
||||
@ -2,12 +2,11 @@
|
||||
#
|
||||
# imptask.py -- important async tasks that shouldn't die
|
||||
#
|
||||
import sys, uasyncio
|
||||
import sys, uasyncio, ckcc
|
||||
|
||||
def die_with_debug(exc):
|
||||
try:
|
||||
from usb import is_vcp_active
|
||||
is_debug = is_vcp_active()
|
||||
is_debug = ckcc.vcp_enabled(None) or ckcc.is_debug_build()
|
||||
except:
|
||||
# robustness
|
||||
is_debug = False
|
||||
@ -63,6 +62,7 @@ class ImportantTask:
|
||||
|
||||
def start_task(self, name, awaitable):
|
||||
# start a critical task and watch for it to never die
|
||||
print("Start: %s" % name)
|
||||
task = uasyncio.create_task(awaitable)
|
||||
self.tasks[name] = task
|
||||
return task
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
#
|
||||
# login.py - UX related to PIN code entry/login.
|
||||
#
|
||||
# NOTE: Mark3 hardware does not support secondary wallet concept.
|
||||
# NOTE: Mark3+ hardware does not support secondary wallet concept.
|
||||
#
|
||||
import pincodes, version, random
|
||||
from glob import dis
|
||||
from display import FontLarge, FontTiny
|
||||
from ux import PressRelease, ux_wait_keyup, ux_poll_once, ux_show_story
|
||||
from ux import PressRelease, ux_wait_keyup, ux_show_story
|
||||
from utils import pretty_delay
|
||||
from callgate import show_logout
|
||||
from pincodes import pa
|
||||
@ -18,10 +18,11 @@ MIN_PIN_PART_LEN = 2
|
||||
|
||||
class LoginUX:
|
||||
|
||||
def __init__(self, randomize=False):
|
||||
def __init__(self, randomize=False, kill_btn=None):
|
||||
self.is_setting = False
|
||||
self.is_repeat = False
|
||||
self.subtitle = False
|
||||
self.kill_btn = kill_btn
|
||||
self.offer_second = not version.has_608
|
||||
self.reset()
|
||||
self.randomize = randomize
|
||||
@ -169,7 +170,7 @@ class LoginUX:
|
||||
# X on blank first screen: stop
|
||||
return None
|
||||
|
||||
# do a delete-one
|
||||
# do a backspace
|
||||
if self.pin:
|
||||
self.pin = self.pin[:-1]
|
||||
self.show_pin()
|
||||
@ -185,7 +186,20 @@ class LoginUX:
|
||||
|
||||
self._show_words()
|
||||
|
||||
nxt = await ux_wait_keyup('xy2' if self.offer_second else 'xy')
|
||||
pattern = 'xy'
|
||||
if self.offer_second:
|
||||
pattern += '2'
|
||||
if self.kill_btn:
|
||||
pattern += self.kill_btn
|
||||
|
||||
nxt = await ux_wait_keyup(pattern)
|
||||
|
||||
if not self.is_setting and nxt == self.kill_btn:
|
||||
# wipe the seed if they press a special key
|
||||
import callgate
|
||||
callgate.fast_wipe(False)
|
||||
# not reached
|
||||
|
||||
if nxt == 'y' or nxt == '2':
|
||||
self.pin_prefix = self.pin
|
||||
self.pin = ''
|
||||
@ -199,8 +213,7 @@ class LoginUX:
|
||||
self.show_pin(True)
|
||||
|
||||
else:
|
||||
#assert ch in '0123456789' or ch == ''
|
||||
|
||||
# digit pressed
|
||||
if self.randomize and ch:
|
||||
ch = self.randomize[int(ch)]
|
||||
|
||||
@ -353,7 +366,7 @@ suffix break point is correct.'''
|
||||
while 1:
|
||||
self.reset()
|
||||
second_pin = await self.interact()
|
||||
if first_pin is None: return None
|
||||
if second_pin is None: return None
|
||||
|
||||
if first_pin == second_pin:
|
||||
return first_pin
|
||||
|
||||
@ -20,11 +20,15 @@ if 0:
|
||||
# useful for debug: keep this stub!
|
||||
import ckcc
|
||||
ckcc.vcp_enabled(True)
|
||||
#pyb.usb_mode('VCP+MSC') # handy but annoying disk issues
|
||||
pyb.usb_mode('VCP')
|
||||
from h import *
|
||||
if 0:
|
||||
raise SystemExit
|
||||
|
||||
print("---\nColdcard Wallet from Coinkite Inc. (c) 2018-2021.\n")
|
||||
print("---\nColdcard Wallet from Coinkite Inc. (c) 2018-2022.")
|
||||
|
||||
import version
|
||||
datestamp,vers,_ = version.get_mpy_version()
|
||||
print("Version: %s / %s\n" % (vers, datestamp))
|
||||
|
||||
# Setup OLED and get something onto it.
|
||||
from display import Display
|
||||
@ -33,29 +37,33 @@ dis.splash()
|
||||
glob.dis = dis
|
||||
|
||||
# slowish imports, some with side-effects
|
||||
import version, ckcc, uasyncio
|
||||
import ckcc, uasyncio
|
||||
|
||||
if version.is_devmode:
|
||||
# For devs only: allow code in this directory to overide compiled-in stuff. Dangerous!
|
||||
# - using relative paths here so works better on simulator
|
||||
# - you must boot w/ non-production-signed firmware to get here
|
||||
sys.path.insert(0, 'flash/lib')
|
||||
|
||||
# Give external devs a way to start stuff early
|
||||
if version.mk_num >= 4:
|
||||
# early setup code needed on Mk4
|
||||
try:
|
||||
import boot2
|
||||
except: pass
|
||||
import mk4
|
||||
mk4.init0()
|
||||
|
||||
from psram import PSRAMWrapper
|
||||
glob.PSRAM = PSRAMWrapper()
|
||||
|
||||
except BaseException as exc:
|
||||
sys.print_exception(exc)
|
||||
# continue tho
|
||||
else:
|
||||
# Serial Flash memory
|
||||
from sflash import SF
|
||||
|
||||
# Setup membrane numpad (mark 2+)
|
||||
from mempad import MembraneNumpad
|
||||
numpad = MembraneNumpad()
|
||||
glob.numpad = numpad
|
||||
|
||||
# Serial Flash memory
|
||||
from sflash import SF
|
||||
|
||||
# NV settings
|
||||
from nvstore import settings
|
||||
from nvstore import SettingsObject
|
||||
settings = SettingsObject(glob.dis)
|
||||
glob.settings = settings
|
||||
|
||||
async def more_setup():
|
||||
# Boot up code; splash screen is being shown
|
||||
@ -63,11 +71,6 @@ async def more_setup():
|
||||
# MAYBE: check if we're a brick and die again? Or show msg?
|
||||
|
||||
try:
|
||||
# Some background "tasks"
|
||||
#
|
||||
from dev_helper import monitor_usb
|
||||
IMPT.start_task('vcp', monitor_usb())
|
||||
|
||||
from files import CardSlot
|
||||
CardSlot.setup()
|
||||
|
||||
@ -79,6 +82,7 @@ async def more_setup():
|
||||
print("Problem: %r" % e)
|
||||
|
||||
if version.is_factory_mode:
|
||||
print("factory mode")
|
||||
# in factory mode, turn on USB early to allow debug/setup
|
||||
from usb import enable_usb
|
||||
enable_usb()
|
||||
@ -114,7 +118,7 @@ async def mainline():
|
||||
goto_top_menu()
|
||||
|
||||
gc.collect()
|
||||
#print("Free mem: %d" % gc.mem_free())
|
||||
#print("Free mem: %d" % gc.mem_free()) # 532656 on mk4!
|
||||
|
||||
while 1:
|
||||
await the_ux.interact()
|
||||
@ -130,9 +134,15 @@ def go():
|
||||
die_with_debug(exc)
|
||||
|
||||
if version.is_devmode:
|
||||
# Give external devs a way to start semi-early.
|
||||
# Start some debug-only code.
|
||||
try:
|
||||
import main2
|
||||
import dev_helper
|
||||
dev_helper.setup()
|
||||
except: pass
|
||||
|
||||
# Simulator code
|
||||
try:
|
||||
import sim_quickstart
|
||||
except: pass
|
||||
|
||||
uasyncio.create_task(more_setup())
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
# freeze everything in this directoy
|
||||
# Freeze everything in this list.
|
||||
# - not optimized because we need asserts to work
|
||||
# - for mk3 vs mk4, see manifest_mk[34].py
|
||||
freeze_as_mpy('', [
|
||||
'actions.py',
|
||||
'address_explorer.py',
|
||||
@ -8,6 +10,7 @@ freeze_as_mpy('', [
|
||||
'chains.py',
|
||||
'choosers.py',
|
||||
'compat7z.py',
|
||||
'countdowns.py',
|
||||
'descriptor.py',
|
||||
'dev_helper.py',
|
||||
'display.py',
|
||||
@ -17,7 +20,6 @@ freeze_as_mpy('', [
|
||||
'files.py',
|
||||
'flow.py',
|
||||
'glob.py',
|
||||
'h.py',
|
||||
'history.py',
|
||||
'hsm.py',
|
||||
'hsm_ux.py',
|
||||
@ -41,7 +43,6 @@ freeze_as_mpy('', [
|
||||
'selftest.py',
|
||||
'serializations.py',
|
||||
'sffile.py',
|
||||
'sflash.py',
|
||||
'sram2.py',
|
||||
'ssd1306.py',
|
||||
'stash.py',
|
||||
@ -51,12 +52,25 @@ freeze_as_mpy('', [
|
||||
'ux.py',
|
||||
'version.py',
|
||||
'xor_seed.py',
|
||||
'ftux.py',
|
||||
], opt=0)
|
||||
|
||||
# Data-like files, since no need to debug them
|
||||
# Optimize data-like files, since no need to debug them.
|
||||
freeze_as_mpy('', [
|
||||
'sigheader.py',
|
||||
'graphics.py',
|
||||
'zevvpeep.py',
|
||||
'public_constants.py',
|
||||
], opt=3)
|
||||
|
||||
# Maybe include test code.
|
||||
import os
|
||||
if int(os.environ.get('DEBUG_BUILD', 0)):
|
||||
freeze_as_mpy('', [
|
||||
'h.py',
|
||||
'dev_helper.py',
|
||||
'usb_test_commands.py',
|
||||
'sim_display.py',
|
||||
], opt=0)
|
||||
|
||||
include("$(MPY_DIR)/extmod/uasyncio/manifest.py")
|
||||
|
||||
5
shared/manifest_mk3.py
Normal file
5
shared/manifest_mk3.py
Normal file
@ -0,0 +1,5 @@
|
||||
# Mk3 and earlier only files; would not be needed on Mk4 or later
|
||||
freeze_as_mpy('', [
|
||||
'sflash.py',
|
||||
], opt=0)
|
||||
|
||||
10
shared/manifest_mk4.py
Normal file
10
shared/manifest_mk4.py
Normal file
@ -0,0 +1,10 @@
|
||||
# Mk4 only files; would not be needed on Mk3 or earlier.
|
||||
freeze_as_mpy('', [
|
||||
'psram.py',
|
||||
'mk4.py',
|
||||
'vdisk.py',
|
||||
'nfc.py',
|
||||
'ndef.py',
|
||||
'trick_pins.py',
|
||||
'graphics_mk4.py',
|
||||
], opt=0)
|
||||
@ -68,6 +68,71 @@ class MenuItem:
|
||||
if m:
|
||||
the_ux.push(m)
|
||||
|
||||
class ToggleMenuItem(MenuItem):
|
||||
# Handle toggles: must use undefined (missing) as default
|
||||
# - can remap values a little, but default is to store 0/1/2
|
||||
def __init__(self, label, nvkey, choices, predicate=None, story=None, on_change=None, invert=False, value_map=None):
|
||||
self.label = label
|
||||
self.story = story
|
||||
self.nvkey = nvkey
|
||||
self.choices = choices # list of strings, at least 2
|
||||
self.on_change = on_change # optional, since some are just settings
|
||||
if invert:
|
||||
self.invert = True
|
||||
if value_map:
|
||||
self.value_map = value_map
|
||||
if predicate:
|
||||
self.predicate = predicate
|
||||
|
||||
def is_chosen(self):
|
||||
# should we show a check in parent menu?
|
||||
from glob import settings
|
||||
rv = bool(settings.get(self.nvkey, 0))
|
||||
if getattr(self, 'invert', False):
|
||||
rv = not rv
|
||||
return rv
|
||||
|
||||
async def activate(self, menu, idx):
|
||||
from glob import settings
|
||||
from ux import ux_show_story
|
||||
|
||||
# skip story if default value has been changed
|
||||
if self.story and settings.get(self.nvkey, None) == None:
|
||||
ch = await ux_show_story(self.story)
|
||||
if ch == 'x': return
|
||||
|
||||
value = settings.get(self.nvkey, 0)
|
||||
if hasattr(self, 'value_map'):
|
||||
for n,v in enumerate(self.value_map):
|
||||
if value == v:
|
||||
value = n
|
||||
break
|
||||
else:
|
||||
value = 0 # robustness
|
||||
|
||||
m = MenuSystem([MenuItem(c, f=self.picked) for c in self.choices], chosen=value)
|
||||
the_ux.push(m)
|
||||
|
||||
async def picked(self, menu, picked, xx_self):
|
||||
from glob import settings
|
||||
|
||||
menu.chosen = picked
|
||||
menu.show()
|
||||
await sleep_ms(100) # visual feedback that we changed it
|
||||
|
||||
if picked == 0:
|
||||
settings.remove_key(self.nvkey)
|
||||
else:
|
||||
if hasattr(self, 'value_map'):
|
||||
picked = self.value_map[picked] # want IndexError if wrong here
|
||||
settings.set(self.nvkey, picked)
|
||||
|
||||
if self.on_change:
|
||||
await self.on_change(picked)
|
||||
|
||||
the_ux.pop()
|
||||
|
||||
|
||||
class MenuSystem:
|
||||
|
||||
def __init__(self, menu_items, chosen=None, should_cont=None, space_indicators=False):
|
||||
@ -125,7 +190,14 @@ class MenuSystem:
|
||||
if msg[0] == ' ' and self.space_indicators:
|
||||
dis.icon(x-2, y+11, 'space', invert=is_sel)
|
||||
|
||||
if self.chosen is not None and (n+self.ypos) == self.chosen:
|
||||
# show check?
|
||||
checked = (self.chosen is not None and (n+self.ypos) == self.chosen)
|
||||
|
||||
fcn = getattr(self.items[n+self.ypos], 'is_chosen', None)
|
||||
if fcn and fcn():
|
||||
checked = True
|
||||
|
||||
if checked:
|
||||
dis.icon(108, y, 'selected', invert=is_sel)
|
||||
|
||||
y += h
|
||||
|
||||
102
shared/mk4.py
Normal file
102
shared/mk4.py
Normal file
@ -0,0 +1,102 @@
|
||||
# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
# mk4.py - Mk4 specific code, not needed on earlier devices.
|
||||
#
|
||||
#
|
||||
import os, sys, pyb, ckcc, version, glob
|
||||
|
||||
def make_flash_fs():
|
||||
print("Rebuild /flash")
|
||||
|
||||
# create our fav filesystem, and mount it
|
||||
fl = pyb.Flash(start=0)
|
||||
os.VfsLfs2.mkfs(fl)
|
||||
|
||||
os.mount(fl, '/flash')
|
||||
|
||||
open('/flash/README.txt', 'wt').write("LFS Virt disk")
|
||||
os.mkdir('/flash/settings')
|
||||
|
||||
def make_psram_fs():
|
||||
# Filesystem is wiped and rebuild on each boot before this point, but
|
||||
# add some more files.
|
||||
ps = ckcc.PSRAM()
|
||||
os.mount(ps, '/psram')
|
||||
|
||||
# need DOS-style newlines for best compatibility
|
||||
open('/psram/README.txt', 'wt').write('''
|
||||
COLDCARD Virtual Disk
|
||||
|
||||
1) copy your PSBT file here.
|
||||
2) select from Coldcard menu & approve transaction.
|
||||
3) signed transaction file(s) will be saved here.
|
||||
|
||||
'''.replace('\n', '\r\n'))
|
||||
|
||||
date, ver, *_ = version.get_mpy_version()
|
||||
open('/psram/ident/version.txt', 'wt').write('\r\n'.join([ver, date, '']))
|
||||
|
||||
# generally, leave it unmounted
|
||||
os.umount('/psram')
|
||||
|
||||
def rng_seeding():
|
||||
# seed our RNG with entropy from secure elements
|
||||
import callgate, ngu, ustruct
|
||||
|
||||
a = callgate.read_rng(1) # SE1
|
||||
b = callgate.read_rng(2) # SE2
|
||||
|
||||
n = ngu.hash.sha256d(a+b)
|
||||
n, = ustruct.unpack('I', n[0:4])
|
||||
|
||||
ngu.random.reseed(n)
|
||||
|
||||
|
||||
def init0():
|
||||
# called very early
|
||||
try:
|
||||
os.statvfs('/flash')
|
||||
except OSError:
|
||||
make_flash_fs()
|
||||
|
||||
try:
|
||||
make_psram_fs()
|
||||
except BaseException as exc:
|
||||
sys.print_exception(exc)
|
||||
|
||||
if version.is_devmode:
|
||||
try:
|
||||
# need to import this early so it can monkey-patch itself in place
|
||||
import sim_display
|
||||
except: pass
|
||||
|
||||
# seed RNGs with entropy from secure elements
|
||||
rng_seeding()
|
||||
|
||||
async def dev_enable_repl(*a):
|
||||
# Mk4: Enable serial port connection. You'll have to break case open.
|
||||
from ux import ux_show_story
|
||||
|
||||
wipe_if_deltamode()
|
||||
|
||||
# allow REPL access
|
||||
ckcc.vcp_enabled(True)
|
||||
|
||||
print("REPL enabled.")
|
||||
await ux_show_story("""\
|
||||
The serial port has now been enabled.\n\n3.3v TTL on Tx/Rx/Gnd pads @ 115,200 bps.""")
|
||||
|
||||
def wipe_if_deltamode():
|
||||
# If in deltamode, give up and wipe self rather do
|
||||
# a thing that might reveal true master secret...
|
||||
|
||||
from pincodes import pa
|
||||
import callgate
|
||||
|
||||
if not pa.is_deltamode():
|
||||
return
|
||||
|
||||
import callgate
|
||||
callgate.fast_wipe()
|
||||
|
||||
# EOF
|
||||
@ -6,13 +6,12 @@ import stash, chains, ustruct, ure, uio, sys, ngu
|
||||
#from ubinascii import hexlify as b2a_hex
|
||||
from utils import xfp2str, str2xfp, swab32, cleanup_deriv_path, keypath_to_str, str_to_keypath
|
||||
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_clear_keys, ux_enter_number
|
||||
from files import CardSlot, CardMissingError
|
||||
from files import CardSlot, CardMissingError, needs_microsd
|
||||
from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AFC_SCRIPT, MAX_PATH_DEPTH
|
||||
from menu import MenuSystem, MenuItem
|
||||
from opcodes import OP_CHECKMULTISIG
|
||||
from actions import needs_microsd
|
||||
from exceptions import FatalPSBTIssue
|
||||
from nvstore import settings
|
||||
from glob import settings
|
||||
|
||||
# Bitcoin limitation: max number of signatures in CHECK_MULTISIG
|
||||
# - 520 byte redeem script limit <= 15*34 bytes per pubkey == 510 bytes
|
||||
@ -804,9 +803,26 @@ class MultisigWallet:
|
||||
|
||||
async def export_wallet_file(self, mode="exported from", extra_msg=None):
|
||||
# create a text file with the details; ready for import to next Coldcard
|
||||
from glob import NFC
|
||||
|
||||
my_xfp = xfp2str(settings.get('xfp'))
|
||||
|
||||
fname_pattern = self.make_fname('export')
|
||||
hdr = '%s %s' % (mode, my_xfp)
|
||||
|
||||
if NFC:
|
||||
# Offer to share over NFC regardless of if card inserted, virtdisk active, etc.
|
||||
ch = await ux_show_story('''Press (3) to share file over NFC. \
|
||||
Otherwise, OK to proceed normally.''', escape='3')
|
||||
if ch == '3':
|
||||
with uio.StringIO() as fp:
|
||||
self.render_export(fp, hdr_comment=hdr)
|
||||
await NFC.share_text(fp.getvalue())
|
||||
|
||||
return
|
||||
|
||||
if ch != 'y':
|
||||
return
|
||||
|
||||
try:
|
||||
with CardSlot() as card:
|
||||
@ -814,8 +830,7 @@ class MultisigWallet:
|
||||
|
||||
# do actual write
|
||||
with open(fname, 'wt') as fp:
|
||||
print("# Coldcard Multisig setup file (%s %s)\n#" % (mode, my_xfp), file=fp)
|
||||
self.render_export(fp)
|
||||
self.render_export(fp, hdr_comment=hdr)
|
||||
|
||||
msg = '''Coldcard multisig setup file written:\n\n%s''' % nice
|
||||
if extra_msg:
|
||||
@ -830,7 +845,10 @@ class MultisigWallet:
|
||||
await ux_show_story('Failed to write!\n\n\n'+str(e))
|
||||
return
|
||||
|
||||
def render_export(self, fp):
|
||||
def render_export(self, fp, hdr_comment=None):
|
||||
if hdr_comment:
|
||||
print("# Coldcard Multisig setup file (%s)\n#" % hdr_comment, file=fp)
|
||||
|
||||
print("Name: %s\nPolicy: %d of %d" % (self.name, self.M, self.N), file=fp)
|
||||
|
||||
if self.addr_fmt != AF_P2SH:
|
||||
@ -1141,8 +1159,6 @@ class MultisigMenu(MenuSystem):
|
||||
@classmethod
|
||||
def construct(cls):
|
||||
# Dynamic menu with user-defined names of wallets shown
|
||||
#from menu import MenuSystem, MenuItem
|
||||
from actions import import_multisig
|
||||
|
||||
if not MultisigWallet.exists():
|
||||
rv = [MenuItem('(none setup yet)', f=no_ms_yet)]
|
||||
@ -1152,7 +1168,7 @@ 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 from SD', f=import_multisig))
|
||||
rv.append(MenuItem('Import from File', f=import_multisig))
|
||||
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))
|
||||
@ -1259,8 +1275,10 @@ async def export_multisig_xpubs(*a):
|
||||
# NOW: Export JSON with one xpub per useful address type and semi-standard derivation path
|
||||
#
|
||||
# Consumer for this file is supposed to be ourselves, when we build on-device multisig.
|
||||
# - however some 3rd parts are making use of it as well.
|
||||
# - however some 3rd parties are making use of it as well.
|
||||
#
|
||||
from glob import NFC
|
||||
|
||||
xfp = xfp2str(settings.get('xfp', 0))
|
||||
chain = chains.current_chain()
|
||||
|
||||
@ -1278,11 +1296,13 @@ P2SH-P2WSH:
|
||||
P2WSH:
|
||||
m/48'/{coin}'/acct'/2'
|
||||
|
||||
OK to continue. X to abort.
|
||||
'''.format(coin = chain.b44_cointype)
|
||||
OK to continue. X to abort.'''.format(coin = chain.b44_cointype)
|
||||
|
||||
resp = await ux_show_story(msg)
|
||||
if resp != 'y': return
|
||||
if NFC:
|
||||
msg += ' Press 3 to share over NFC.'
|
||||
|
||||
ch = await ux_show_story(msg, escape='3')
|
||||
if ch == 'x': return
|
||||
|
||||
acct_num = await ux_enter_number('Account Number:', 9999)
|
||||
|
||||
@ -1292,24 +1312,33 @@ OK to continue. X to abort.
|
||||
( "m/48'/{coin}'/{acct_num}'/2'", 'p2wsh', AF_P2WSH ),
|
||||
]
|
||||
|
||||
def render(fp):
|
||||
fp.write('{\n')
|
||||
with stash.SensitiveValues() as sv:
|
||||
for deriv, name, fmt in todo:
|
||||
if fmt == AF_P2SH and acct_num:
|
||||
continue
|
||||
dd = deriv.format(coin=chain.b44_cointype, acct_num=acct_num)
|
||||
node = sv.derive_path(dd)
|
||||
xp = chain.serialize_public(node, fmt)
|
||||
fp.write(' "%s_deriv": "%s",\n' % (name, dd))
|
||||
fp.write(' "%s": "%s",\n' % (name, xp))
|
||||
|
||||
fp.write(' "account": "%d",\n' % acct_num)
|
||||
fp.write(' "xfp": "%s"\n}\n' % xfp)
|
||||
|
||||
if NFC and ch == '3':
|
||||
with uio.StringIO() as fp:
|
||||
render(fp)
|
||||
await NFC.share_json(fp.getvalue())
|
||||
return
|
||||
|
||||
try:
|
||||
with CardSlot() as card:
|
||||
fname, nice = card.pick_filename(fname_pattern)
|
||||
# do actual write: manual JSON here so more human-readable.
|
||||
with open(fname, 'wt') as fp:
|
||||
fp.write('{\n')
|
||||
with stash.SensitiveValues() as sv:
|
||||
for deriv, name, fmt in todo:
|
||||
if fmt == AF_P2SH and acct_num:
|
||||
continue
|
||||
dd = deriv.format(coin=chain.b44_cointype, acct_num=acct_num)
|
||||
node = sv.derive_path(dd)
|
||||
xp = chain.serialize_public(node, fmt)
|
||||
fp.write(' "%s_deriv": "%s",\n' % (name, dd))
|
||||
fp.write(' "%s": "%s",\n' % (name, xp))
|
||||
|
||||
fp.write(' "account": "%d",\n' % acct_num)
|
||||
fp.write(' "xfp": "%s"\n}\n' % xfp)
|
||||
render(fp)
|
||||
|
||||
except CardMissingError:
|
||||
await needs_microsd()
|
||||
@ -1506,5 +1535,43 @@ Default is P2WSH addresses (segwit) or press (1) for P2SH-P2WSH.''', escape='1')
|
||||
|
||||
return await ondevice_multisig_create(n, f)
|
||||
|
||||
async def import_multisig(*a):
|
||||
# pick text file from SD card, import as multisig setup file
|
||||
from actions import file_picker
|
||||
from glob import NFC
|
||||
|
||||
if NFC and not CardSlot.is_inserted():
|
||||
# prompt them use NFC?
|
||||
ch = await ux_show_story("Press 3 to use NFC to send the multisig wallet file. Otherwise use file.", escape='3')
|
||||
if ch == '3':
|
||||
return await NFC.import_multisig_nfc()
|
||||
if ch == 'x':
|
||||
return
|
||||
|
||||
def possible(filename):
|
||||
with open(filename, 'rt') as fd:
|
||||
for ln in fd:
|
||||
if 'pub' in ln:
|
||||
return True
|
||||
|
||||
fn = await file_picker('Pick multisig wallet file to import (.txt)', suffix='.txt',
|
||||
min_size=100, max_size=20*200, taster=possible)
|
||||
if not fn: return
|
||||
|
||||
try:
|
||||
with CardSlot() as card:
|
||||
with open(fn, 'rt') as fp:
|
||||
data = fp.read()
|
||||
except CardMissingError:
|
||||
await needs_microsd()
|
||||
return
|
||||
|
||||
from auth import maybe_enroll_xpub
|
||||
try:
|
||||
possible_name = (fn.split('/')[-1].split('.'))[0]
|
||||
maybe_enroll_xpub(config=data, name=possible_name)
|
||||
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
|
||||
|
||||
241
shared/ndef.py
Normal file
241
shared/ndef.py
Normal file
@ -0,0 +1,241 @@
|
||||
# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
# ndef.py -- NDEF records: making them and parsing them.
|
||||
#
|
||||
# - see ../docs/nfc-on-coldcard.md for background.
|
||||
# - cross platform file
|
||||
#
|
||||
from struct import pack, unpack
|
||||
from binascii import hexlify as b2a_hex
|
||||
|
||||
# From ST AN4911 - Fixed CC file that uses E2 to indicate 2-byte lengths
|
||||
# - allocates entire memory (64k) to tag usage, read only
|
||||
# - followed the "NDEF File Control TLV" tag (0x03) but not the length
|
||||
CC_FILE = bytes([0xE2, 0x43, 0x00, 0x01, 0x00, 0x00, 0x04, 0x00, 0x03])
|
||||
|
||||
# When we are writable, empty file is given
|
||||
CC_WR_FILE = bytes([0xE2, 0x40, 0x00, 0x01, 0x00, 0x00, 0x04, 0x00,
|
||||
0x03, 0x00, # empty
|
||||
0xfe, # end marker
|
||||
])
|
||||
|
||||
class ndefMaker:
|
||||
'''
|
||||
make a few different types of NDEF records, very limited and only
|
||||
for our use-cases
|
||||
'''
|
||||
def __init__(self):
|
||||
# ndef records: len, TNF value, type (byte string), payload (bytes)
|
||||
self.lst = []
|
||||
|
||||
def add_text(self, msg):
|
||||
# assume: english, utf-8
|
||||
ln = len(msg) + 3
|
||||
self.lst.append( (ln, 0x1, b'T', b'\x02en' + msg.encode()) )
|
||||
|
||||
def add_url(self, url, https=True):
|
||||
# always https since we're in Bitcoin, or else full URL
|
||||
# - ascii only
|
||||
proto_code = b'\x04' if https else b'\x00'
|
||||
self.lst.append( (len(url)+1, 0x1, b'U', proto_code + url.encode()) )
|
||||
|
||||
def add_large_object(self, ext_type, offset, obj_len):
|
||||
# zero-copy a binary file from PSRAM into NFC flash
|
||||
# - or accept bytes
|
||||
if isinstance(offset, int):
|
||||
from glob import PSRAM
|
||||
self.lst.append( (obj_len, 0x4, ext_type.encode(),
|
||||
PSRAM.read_at(offset, obj_len)) )
|
||||
else:
|
||||
self.add_custom(ext_type, offset)
|
||||
|
||||
def add_custom(self, ext_type, payload):
|
||||
# "NFC Forum external Type" using bitcoin.org domain
|
||||
self.lst.append( (len(payload), 0x4, ext_type.encode(), payload) )
|
||||
|
||||
def add_mime_data(self, mime_type, payload):
|
||||
# "image/png" or other RFC mime types, including application/json
|
||||
self.lst.append( (len(payload), 0x2, mime_type.encode(), payload) )
|
||||
|
||||
def bytes(self):
|
||||
# Walk list of records, and set various framing bits to first bytes of each
|
||||
# and concat.
|
||||
# - resist urge to make this a generator, it's not worth it.
|
||||
rv = bytearray(CC_FILE)
|
||||
|
||||
# calc total length of all records
|
||||
ln = sum((3 if ln <= 255 else 6) + len(ntype) + len(rec)
|
||||
for (ln, _, ntype, rec) in self.lst)
|
||||
if ln <= 0xfe:
|
||||
rv.append(ln)
|
||||
else:
|
||||
rv.append(0xff)
|
||||
rv.extend(pack('>H', ln))
|
||||
|
||||
last = len(self.lst) - 1
|
||||
for n, (ln, tnf, ntype, rec) in enumerate(self.lst):
|
||||
# First byte of the NDEF record: it's a bitmask + TNF 3-bit value
|
||||
|
||||
# TNF=1 => well-known type
|
||||
# TNF=2 => mime-type from RFC 2046
|
||||
# TNF=4 => NFC Forum external type
|
||||
assert 0 < tnf < 7
|
||||
first = tnf
|
||||
|
||||
if ln <= 255:
|
||||
first |= 0x10 # SR=1
|
||||
|
||||
if n == 0:
|
||||
first |= 0x80 # = MB Message Begin
|
||||
if n == last:
|
||||
first |= 0x40 # = ME Message End
|
||||
|
||||
rv.append(first) # NDEF header byte
|
||||
rv.append(len(ntype)) # type-length always one, if well-known
|
||||
if ln <= 255:
|
||||
rv.append(ln) # value-length
|
||||
else:
|
||||
rv.extend(pack('>I', ln))
|
||||
rv.extend(ntype)
|
||||
rv.extend(rec)
|
||||
|
||||
rv.append(0xfe) # Terminator TLV
|
||||
|
||||
return rv
|
||||
|
||||
def ccfile_decode(taste):
|
||||
# Given first 16 bytes of tag's memory (user memory):
|
||||
# - returns start and length of real Ndef records
|
||||
# - and is_writable flag
|
||||
# - and max size of tag memory capacity, in bytes (poorly spec'ed / compat issues)
|
||||
ex, b1, b2 = taste[0:3]
|
||||
assert b1 & 0xf0 == 0x40 # bad version.
|
||||
if ex == 0xE1:
|
||||
# "one byte addressing mode" -- max of 2040 bytes, 4-6 byte header
|
||||
if b2 != 0x00:
|
||||
st = 4
|
||||
mlen = b2 # aka MLEN
|
||||
else:
|
||||
st = 6
|
||||
mlen = unpack('>H', taste[4:4+2])[0]
|
||||
elif ex == 0xE2:
|
||||
# 8-byte CC Field, allows 2 byte address mode
|
||||
st = 8
|
||||
mlen = unpack('>H', taste[6:6+2])[0]
|
||||
else:
|
||||
raise ValueError("bad first byte") # not one of 2 magic values we support
|
||||
|
||||
assert taste[st] == 0x03 # special first TLV
|
||||
st += 1
|
||||
ll = taste[st:st+3]
|
||||
if ll[0] == 0xff:
|
||||
ll = unpack('>H', ll[1:])[0]
|
||||
st += 3
|
||||
else:
|
||||
ll = ll[0]
|
||||
st += 1
|
||||
|
||||
assert 0 <= ll < 8196 # 64kbit max part
|
||||
|
||||
return st, ll, ((b1 & 3) == 0), mlen*4
|
||||
|
||||
def record_parser(msg):
|
||||
# Given body of ndef records, yield a tuple for each record:
|
||||
# - type info, as urn string
|
||||
# - bytes of body
|
||||
# - dict of meta data, appropriate to type
|
||||
# - we gag on chunks
|
||||
pos = 0
|
||||
while 1:
|
||||
meta = {}
|
||||
hdr = msg[pos]
|
||||
|
||||
MB = hdr & 0x80
|
||||
ME = hdr & 0x40
|
||||
CF = hdr & 0x20
|
||||
SR = hdr & 0x10
|
||||
IL = hdr & 0x08
|
||||
TNF = hdr & 0x7
|
||||
|
||||
assert not CF # no chunks please
|
||||
assert (pos == 0) == bool(MB) # first one needs MB set
|
||||
|
||||
ty_len = msg[pos+1]
|
||||
pos += 2
|
||||
|
||||
if SR: # short record: one byte for payload length
|
||||
pl_len = msg[pos]
|
||||
pos += 1
|
||||
else:
|
||||
pl_len = unpack('>I', msg[pos:pos+4])[0]
|
||||
pos += 4
|
||||
|
||||
id_len = 0
|
||||
if IL:
|
||||
id_len = msg[pos]
|
||||
pos += 1
|
||||
|
||||
urn = None
|
||||
|
||||
# type is next
|
||||
ty = msg[pos:pos+ty_len]
|
||||
pos += ty_len
|
||||
|
||||
if TNF == 0x0: # empty
|
||||
assert ty_len == pl_len == 0
|
||||
urn = None
|
||||
elif TNF == 0x1: # WKT
|
||||
urn = 'urn:nfc:wkt:'
|
||||
urn += ty.decode()
|
||||
|
||||
if ty == b'T':
|
||||
# unwrap Text
|
||||
hdr2 = msg[pos]
|
||||
assert hdr2 & 0xc0 == 0x00 # only UTF supported
|
||||
lang_len = hdr2 & 0x3f
|
||||
|
||||
meta['lang'] = msg[pos+1:pos+1 + lang_len].decode()
|
||||
skip = 1 + lang_len
|
||||
pl_len -= skip
|
||||
pos += skip
|
||||
|
||||
if ty == b'U':
|
||||
# limited URL support
|
||||
meta['prefix'] = msg[pos]
|
||||
pos += 1
|
||||
pl_len -= 1
|
||||
|
||||
elif TNF == 0x2: # mime-type, like 'image/png'
|
||||
urn = ty.decode()
|
||||
elif TNF == 0x3: # absolute URI??
|
||||
urn = 'uri'
|
||||
elif TNF == 0x4: # NFC forum external type
|
||||
urn = 'urn:nfc:ext:'
|
||||
urn += ty.decode()
|
||||
else:
|
||||
raise ValueError("TNF") # unknown/reserved/not handled.
|
||||
|
||||
if IL:
|
||||
meta['ident'] = bytes(msg[pos:pos+id_len])
|
||||
pos += id_len
|
||||
|
||||
yield urn, memoryview(msg)[pos:pos+pl_len], meta
|
||||
|
||||
if ME: return
|
||||
|
||||
pos += pl_len
|
||||
assert pos < len(msg) # missing ME/truncated
|
||||
|
||||
|
||||
# EOF
|
||||
|
||||
# from NXP:
|
||||
# E1 40 80 09 03 10 D1 01 0C 55 01 6E 78 70 2E 63 6F 6D 2F 6E 66 63 FE 00
|
||||
|
||||
# ST AN5439 -- works
|
||||
# 4-byte CCfile then "NDEF File Control TLV":
|
||||
# E1 40 40 00 03 2A
|
||||
# NDef records:
|
||||
# D1012655016578616D706C652E636F6D2F74656D703D303030302F746170636F756E7465723D30303030FE000000
|
||||
#
|
||||
# m=b'\xe1@@\x00\x03*\xd1\x01&U\x01example.com/temp=0000/tapcounter=0000\xfe\x00\x00\x00'
|
||||
559
shared/nfc.py
Normal file
559
shared/nfc.py
Normal file
@ -0,0 +1,559 @@
|
||||
# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
# nfc.py -- Add some NFC tag-like features to Mk4
|
||||
#
|
||||
# - using ST ST25DV64KC
|
||||
# - on it's own I2C bus (not shared)
|
||||
# - has GPIO signal "??" which is multipurpose on its own pin
|
||||
# - this chip chosen because it can disable RF interaction
|
||||
#
|
||||
import ngu, ckcc, utime
|
||||
from uasyncio import sleep_ms
|
||||
from utils import B2A, problem_file_line
|
||||
from ustruct import pack, unpack
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from ubinascii import unhexlify as a2b_hex
|
||||
from ux import ux_wait_keyup, ux_show_story, ux_poll_key
|
||||
import ndef
|
||||
|
||||
# practical limit for things to share: 8k part, minus overhead
|
||||
MAX_NFC_SIZE = const(8000)
|
||||
|
||||
# (ms) How long to wait after RF field comes and goes (or tag is written)
|
||||
POST_SCAN_DELAY = const(1000)
|
||||
|
||||
# i2c address (7-bits) is not simple...
|
||||
# - assume defaults of E0=1 and I2C_DEVICE_CODE=0xa
|
||||
# - also 0x2d which isn't documented and no idea what it is
|
||||
I2C_ADDR_USER = const(0x53)
|
||||
I2C_ADDR_SYS = const(0x57)
|
||||
I2C_ADDR_RF_ON = const(0x51)
|
||||
I2C_ADDR_RF_OFF = const(0x55)
|
||||
|
||||
# Dynamic regs
|
||||
GPO_CTRL_Dyn = const(0x2000) # GPO control
|
||||
EH_CTRL_Dyn = const(0x2002) # Energy Harvesting management & usage status
|
||||
RF_MNGT_Dyn = const(0x2003) # RF interface usage management
|
||||
I2C_SSO_Dyn = const(0x2004) # I2C security session status
|
||||
IT_STS_Dyn = const(0x2005) # Interrupt Status
|
||||
MB_CTRL_Dyn = const(0x2006) # Fast transfer mode control and status
|
||||
MB_LEN_Dyn = const(0x2007) # Length of fast transfer mode message
|
||||
|
||||
# Sys config area
|
||||
GPO1_CFG = const(0x00) # GPIO config 1
|
||||
GPO2_CFG = const(0x01) # GPIO config 2
|
||||
RF_MNGT = const(0x03) # RF interface state after Power ON
|
||||
I2C_CFG = const(0x0e)
|
||||
I2C_PWD = const(0x900) # I2C security session password, 8 bytes
|
||||
|
||||
class NFCHandler:
|
||||
def __init__(self):
|
||||
from machine import I2C, Pin
|
||||
self.i2c = I2C(1, freq=400000)
|
||||
self.last_edge = 0
|
||||
self.pin_ed = Pin('NFC_ED', mode=Pin.IN, pull=Pin.PULL_UP)
|
||||
|
||||
# track time of last edge
|
||||
def _irq(x):
|
||||
self.last_edge = utime.ticks_ms()
|
||||
self.pin_ed.irq(_irq, Pin.IRQ_FALLING)
|
||||
|
||||
|
||||
@classmethod
|
||||
def startup(cls):
|
||||
import glob
|
||||
n = cls()
|
||||
try:
|
||||
n.setup()
|
||||
glob.NFC = n
|
||||
except BaseException as exc:
|
||||
# i2c comms errors probably
|
||||
#sys.print_exception(exc) # debug only remove me
|
||||
print("NFC absent/disabled")
|
||||
del n
|
||||
|
||||
def shutdown(self):
|
||||
# we aren't wanted anymore
|
||||
self.set_rf_disable(True)
|
||||
import glob
|
||||
glob.NFC = None
|
||||
|
||||
# flash memory access (fixed tag data): 0x0 to 0x2000
|
||||
def read(self, offset, count):
|
||||
return self.i2c.readfrom_mem(I2C_ADDR_USER, offset, count, addrsize=16)
|
||||
def write(self, offset, data):
|
||||
# various limits in place here? Not clear
|
||||
self.i2c.writeto_mem(I2C_ADDR_USER, offset, data, addrsize=16)
|
||||
|
||||
async def big_write(self, data):
|
||||
# write lots to start of flash (new ndef records)
|
||||
for pos in range(0, len(data), 256):
|
||||
here = memoryview(data)[pos:pos+256]
|
||||
self.i2c.writeto_mem(I2C_ADDR_USER, pos, here, addrsize=16)
|
||||
# 6ms per 16 byte row, worst case, so ~100ms here!
|
||||
await self.wait_ready()
|
||||
|
||||
async def wipe(self, full_wipe):
|
||||
# Tag value is stored in flash cells, so want to clear
|
||||
# once we're done in case it's sensitive. But too slow to
|
||||
# clear entire chip most of time, just do first 512 bytes,
|
||||
# and dont wait for last to complete
|
||||
from glob import dis
|
||||
here = bytes(256)
|
||||
end = 8196
|
||||
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
|
||||
|
||||
# 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
|
||||
def read_config(self, offset, count):
|
||||
return self.i2c.readfrom_mem(I2C_ADDR_SYS, offset, count, addrsize=16)
|
||||
def write_config(self, offset, data):
|
||||
# not all areas are writable
|
||||
self.i2c.writeto_mem(I2C_ADDR_SYS, offset, data, addrsize=16)
|
||||
def read_config1(self, offset):
|
||||
return self.i2c.readfrom_mem(I2C_ADDR_SYS, offset, 1, addrsize=16)[0]
|
||||
def write_config1(self, offset, value):
|
||||
self.i2c.writeto_mem(I2C_ADDR_SYS, offset, bytes([value]), addrsize=16)
|
||||
|
||||
# dynamic registers (state control, bytes): table 13
|
||||
def read_dyn(self, offset):
|
||||
assert 0x2000 <= offset < 0x2008
|
||||
return self.i2c.readfrom_mem(I2C_ADDR_USER, offset, 1, addrsize=16)[0]
|
||||
def write_dyn(self, offset, val):
|
||||
assert 0x2000 <= offset < 0x2008
|
||||
m = bytes([val])
|
||||
self.i2c.writeto_mem(I2C_ADDR_USER, offset, m, addrsize=16)
|
||||
|
||||
def is_rf_disabled(self):
|
||||
# not checking if disable/sleep vs. off
|
||||
return (self.read_dyn(RF_MNGT_Dyn) != 0)
|
||||
|
||||
def set_rf_disable(self, val):
|
||||
# using stronger "off" rather than sleep/disable
|
||||
if val:
|
||||
self.i2c.writeto(I2C_ADDR_RF_OFF, b'')
|
||||
assert self.read_dyn(RF_MNGT_Dyn) & 0x4
|
||||
return
|
||||
|
||||
# re-enable (turn on)
|
||||
for i in range(10):
|
||||
try:
|
||||
self.i2c.writeto(I2C_ADDR_RF_ON, b'')
|
||||
self.write_dyn(RF_MNGT_Dyn, 0)
|
||||
assert self.read_dyn(RF_MNGT_Dyn) == 0x0
|
||||
|
||||
return
|
||||
except: # assertion, OSError(ENODEV)
|
||||
# handle no-ACK cases (sometimes, after bigger write to flash)
|
||||
utime.sleep_ms(25)
|
||||
else:
|
||||
raise RuntimeError("timeout")
|
||||
|
||||
def send_pw(self, pw=None):
|
||||
# show we know a password (but sent cleartext, very lame)
|
||||
# - keeping as zeros for now, so pointless anyway
|
||||
pw = pw or bytes(8)
|
||||
assert len(pw) == 8
|
||||
|
||||
msg = pw + b'\x09' + pw
|
||||
self.write_config(I2C_PWD, msg)
|
||||
|
||||
return (self.read_dyn(I2C_SSO_Dyn) & 0x1 == 0x1) # else "wrong pw"
|
||||
|
||||
def get_uid(self):
|
||||
# Unique id for chip. Required for RF protocol.
|
||||
return ':'.join('%02x'% i for i in reversed(self.uid))
|
||||
|
||||
def dump_ndef(self):
|
||||
# dump what we are showing, skipping the CCFILE and wrapping
|
||||
# - used in test cases, and psbt rx
|
||||
taste = self.read(0, 16)
|
||||
st, ll, _, _ = ndef.ccfile_decode(taste)
|
||||
return self.read(st, ll)
|
||||
|
||||
def firsttime_setup(self):
|
||||
# always setup IC_RF_SWITCHOFF_EN bit in I2C_CFG register
|
||||
# - so we can module RF support with special i2c addresses
|
||||
# - keep default other bits: 0x1a (i2c base address)
|
||||
self.write_config1(I2C_CFG, 0x3a)
|
||||
|
||||
utime.sleep_ms(10) # required
|
||||
|
||||
# set to no RF when first powered up (so CC is quiet when system unpowered)
|
||||
# - side-effect: sets rf to sleep now too
|
||||
self.write_config1(RF_MNGT, 2)
|
||||
|
||||
utime.sleep_ms(10) # might be needed?
|
||||
|
||||
# XXX locking stuff?
|
||||
|
||||
def setup(self):
|
||||
# check if present, alive
|
||||
self.uid = self.read_config(0x18, 8)
|
||||
|
||||
assert self.uid[-1] == 0xe0 # ST manu code
|
||||
|
||||
# read size of memory
|
||||
mem_size = (unpack('<H', self.read_config(0x14, 2))[0] + 1) * 4
|
||||
assert mem_size == 8192 # require 64kbit part
|
||||
|
||||
# chip revision, saw 0x11 perhaps means "1.1"?
|
||||
#rev = self.read_config(0x20, 1)[0]
|
||||
#print("NFC: uid=%s size=%d rev=%x" % (self.get_uid(), mem_size, rev))
|
||||
|
||||
self.send_pw()
|
||||
|
||||
if self.read_config1(I2C_CFG) != 0x3a:
|
||||
# chip probably blank...
|
||||
self.firsttime_setup()
|
||||
|
||||
self.set_rf_disable(1)
|
||||
|
||||
async def share_signed_txn(self, txid, file_offset, txn_len, txn_sha):
|
||||
# we just signed something, share it over NFC
|
||||
if txn_len >= MAX_NFC_SIZE:
|
||||
await ux_show_story("Transaction is too large to share over NFC")
|
||||
return
|
||||
|
||||
n = ndef.ndefMaker()
|
||||
if txid is not None:
|
||||
n.add_text('Signed Transaction: ' + txid)
|
||||
n.add_custom('bitcoin.org:txid', a2b_hex(txid)) # want binary
|
||||
n.add_custom('bitcoin.org:sha256', txn_sha)
|
||||
n.add_large_object('bitcoin.org:txn', file_offset, txn_len)
|
||||
|
||||
return await self.share_start(n)
|
||||
|
||||
async def share_psbt(self, file_offset, psbt_len, psbt_sha, label=None):
|
||||
# we just signed something, share it over NFC
|
||||
if psbt_len >= MAX_NFC_SIZE:
|
||||
await ux_show_story("PSBT is too large to share over NFC")
|
||||
return
|
||||
|
||||
n = ndef.ndefMaker()
|
||||
n.add_text(label or 'Partly signed PSBT')
|
||||
n.add_custom('bitcoin.org:sha256', psbt_sha)
|
||||
n.add_large_object('bitcoin.org:psbt', file_offset, psbt_len)
|
||||
|
||||
return await self.share_start(n)
|
||||
|
||||
async def share_deposit_address(self, addr):
|
||||
n = ndef.ndefMaker()
|
||||
n.add_text('Deposit Address')
|
||||
n.add_custom('bitcoin.org:addr', addr.encode())
|
||||
return await self.share_start(n)
|
||||
|
||||
async def share_json(self, json_data):
|
||||
# a text file of JSON for programs to read
|
||||
n = ndef.ndefMaker()
|
||||
n.add_mime_data('application/json', json_data)
|
||||
|
||||
return await self.share_start(n)
|
||||
|
||||
async def share_text(self, data):
|
||||
# share text from a list of values
|
||||
# - just a text file, no multiple records; max usability!
|
||||
n = ndef.ndefMaker()
|
||||
n.add_text(data)
|
||||
|
||||
return await self.share_start(n)
|
||||
|
||||
async def wait_ready(self):
|
||||
# block until chip ready to continue (ACK happens)
|
||||
# - especially after any flash write, which is very slow: 5.5ms per 16byte
|
||||
while 1:
|
||||
try:
|
||||
self.i2c.readfrom_mem(I2C_ADDR_USER, 0, 0, addrsize=16)
|
||||
return
|
||||
except OSError:
|
||||
await sleep_ms(3)
|
||||
|
||||
async def setup_gpio(self):
|
||||
# setup GPIO (ED) signal for detecting activity
|
||||
# - GPO1_CFG seems to be a flash cell, and takes time to write
|
||||
want = 0x1 | 0x80 | 0x04 # enable, and RF_ACTIVITY_EN | RF_WRITE_EN
|
||||
|
||||
if self.read_config1(GPO1_CFG) != want:
|
||||
self.write_config1(GPO1_CFG, want)
|
||||
# not clear how much delay is needed, but need some
|
||||
await self.wait_ready()
|
||||
|
||||
self.last_edge = 0
|
||||
self.write_dyn(GPO_CTRL_Dyn, 0x01) # GPO_EN
|
||||
self.read_dyn(IT_STS_Dyn) # clear interrupt
|
||||
|
||||
async def ux_animation(self, write_mode):
|
||||
# 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 graphics_mk4 import Graphics
|
||||
|
||||
await self.wait_ready()
|
||||
self.set_rf_disable(0)
|
||||
await self.setup_gpio()
|
||||
|
||||
frames = [getattr(Graphics, 'mk4_nfc_%d'%i) for i in range(1, 5)]
|
||||
|
||||
aborted = True
|
||||
phase = -1
|
||||
last_activity = None
|
||||
while 1:
|
||||
phase = (phase + 1) % 4
|
||||
dis.clear()
|
||||
dis.icon(0, 8, frames[phase])
|
||||
dis.show()
|
||||
await sleep_ms(250)
|
||||
|
||||
if self.last_edge:
|
||||
self.last_edge = 0
|
||||
|
||||
# detect various types of RF activity, so we can clear screen automatically
|
||||
await self.wait_ready()
|
||||
try:
|
||||
events = self.read_dyn(IT_STS_Dyn)
|
||||
except OSError: # ENODEV
|
||||
#print("r_dyn fail")
|
||||
events = 0
|
||||
|
||||
if write_mode:
|
||||
# in write mode, ignore simple read/scan activity: wait for write
|
||||
if events & 0x80:
|
||||
last_activity = utime.ticks_ms()
|
||||
else:
|
||||
if events & 0x02:
|
||||
last_activity = utime.ticks_ms()
|
||||
|
||||
if last_activity is not None \
|
||||
and utime.ticks_diff(utime.ticks_ms(), last_activity) > POST_SCAN_DELAY:
|
||||
# They acheived a read/write and then nothing for some time. We are done w/ success.
|
||||
aborted = False
|
||||
break
|
||||
|
||||
# X or OK to quit, with slightly different meanings
|
||||
ch = ux_poll_key()
|
||||
if ch and ch in 'xy':
|
||||
aborted = (ch == 'x')
|
||||
break
|
||||
|
||||
self.set_rf_disable(1)
|
||||
if not write_mode:
|
||||
await self.wipe(False)
|
||||
|
||||
return aborted
|
||||
|
||||
async def share_start(self, ndef_obj):
|
||||
# do the UX while we are sharing a value over NFC
|
||||
# - assumpting is people know what they are scanning
|
||||
# - x key to abort early, but also self-clears
|
||||
|
||||
await self.big_write(ndef_obj.bytes())
|
||||
|
||||
return await self.ux_animation(False)
|
||||
|
||||
async def start_nfc_rx(self):
|
||||
# Pretend to be a big warm empty tag ready to be stuffed with data
|
||||
await self.big_write(ndef.CC_WR_FILE)
|
||||
|
||||
# wait until something is written
|
||||
aborted = await self.ux_animation(True)
|
||||
if aborted: return
|
||||
|
||||
# read CCFILE area (header)
|
||||
try:
|
||||
taste = self.read(0, 16)
|
||||
st, ll, _, _ = ndef.ccfile_decode(taste)
|
||||
except Exception as e:
|
||||
# robustness; need to handle all failures here
|
||||
import sys; sys.print_exception(e)
|
||||
print("taste = " + B2A(taste))
|
||||
ll = None
|
||||
|
||||
if not ll:
|
||||
# they wrote nothing / failed to do anything
|
||||
await ux_show_story("No tag data was written?\n\n" + B2A(taste), title="Sorry!")
|
||||
return
|
||||
|
||||
# copy to ram, wipe
|
||||
rv = self.read(st, ll)
|
||||
await self.wipe(False)
|
||||
return rv
|
||||
|
||||
|
||||
async def start_psbt_rx(self):
|
||||
from auth import psbt_encoding_taster, TXN_INPUT_OFFSET
|
||||
from auth import UserAuthorizedAction, ApproveTransaction
|
||||
from ux import abort_and_goto
|
||||
from sffile import SFFile
|
||||
|
||||
data = await self.start_nfc_rx()
|
||||
if not data: return
|
||||
|
||||
psbt_in = None
|
||||
psbt_sha = None
|
||||
try:
|
||||
for urn, msg, meta in ndef.record_parser(data):
|
||||
if len(msg) > 100:
|
||||
# attempt to decode any large object, ignore type for max compat
|
||||
try:
|
||||
decoder, output_encoder, psbt_len = \
|
||||
psbt_encoding_taster(msg[0:10], len(msg))
|
||||
psbt_in = msg
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if urn == 'urn:nfc:ext:bitcoin.org:sha256' and len(msg) == 32:
|
||||
# probably produced by another Coldcard: SHA256 over expected contents
|
||||
psbt_sha = bytes(msg)
|
||||
except Exception as e:
|
||||
# dont crash when given garbage
|
||||
import sys; sys.print_exception(e)
|
||||
pass
|
||||
|
||||
if psbt_in is None:
|
||||
await ux_show_story("Could not find PSBT", title="Sorry!")
|
||||
return
|
||||
|
||||
# decode into PSRAM
|
||||
total = 0
|
||||
with SFFile(TXN_INPUT_OFFSET, max_size=psbt_len) as out:
|
||||
if not decoder:
|
||||
total = out.write(psbt_in)
|
||||
else:
|
||||
for here in decoder.more(psbt_in):
|
||||
total += out.write(here)
|
||||
|
||||
# might have been whitespace inflating initial estimate of PSBT size, adjust
|
||||
assert total <= psbt_len
|
||||
psbt_len = total
|
||||
|
||||
# start signing UX
|
||||
UserAuthorizedAction.cleanup()
|
||||
UserAuthorizedAction.active_request = ApproveTransaction(psbt_len, 0x0, psbt_sha=psbt_sha,
|
||||
approved_cb=self.signing_done)
|
||||
# kill any menu stack, and put our thing at the top
|
||||
abort_and_goto(UserAuthorizedAction.active_request)
|
||||
|
||||
async def signing_done(self, psbt):
|
||||
# User approved the PSBT, and signing worked... share result over NFC (only)
|
||||
from auth import TXN_OUTPUT_OFFSET
|
||||
from version import MAX_TXN_LEN
|
||||
from sffile import SFFile
|
||||
|
||||
txid = None
|
||||
|
||||
# asssume they want final transaction when possible, else PSBT output
|
||||
is_comp = psbt.is_complete()
|
||||
|
||||
# re-serialize the PSBT back out (into PSRAM)
|
||||
with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as fd:
|
||||
if is_comp:
|
||||
txid = psbt.finalize(fd)
|
||||
else:
|
||||
psbt.serialize(fd)
|
||||
|
||||
fd.close()
|
||||
self.result = (fd.tell(), fd.checksum.digest())
|
||||
|
||||
out_len, out_sha = self.result
|
||||
|
||||
if is_comp:
|
||||
await self.share_signed_txn(txid, TXN_OUTPUT_OFFSET, out_len, out_sha)
|
||||
else:
|
||||
await self.share_psbt(TXN_OUTPUT_OFFSET, out_len, out_sha)
|
||||
|
||||
# ? show txid on screen ?
|
||||
# thank them?
|
||||
|
||||
@classmethod
|
||||
async def selftest(cls):
|
||||
# check for chip present, field present .. and that it works
|
||||
n = cls()
|
||||
n.setup()
|
||||
assert n.uid
|
||||
|
||||
aborted = await n.share_text("NFC is working: %s" % n.get_uid())
|
||||
assert not aborted, "Aborted"
|
||||
|
||||
|
||||
async def share_file(self):
|
||||
# Pick file from SD card and share over NFC...
|
||||
from actions import file_picker
|
||||
from ubinascii import unhexlify as a2b_hex
|
||||
from files import CardSlot, CardMissingError
|
||||
import ngu
|
||||
|
||||
def is_suitable(fname):
|
||||
f = fname.lower()
|
||||
return f.endswith('.psbt') or f.endswith('.txn') or f.endswith('.txt')
|
||||
|
||||
msg = "Lists PSBT, text, and TXN files on MicroSD. Select to share contents over NFC."
|
||||
|
||||
while 1:
|
||||
fn = await file_picker(msg, min_size=10, max_size=MAX_NFC_SIZE, taster=is_suitable)
|
||||
if not fn: return
|
||||
|
||||
basename = fn.split('/')[-1]
|
||||
ctype = fn.split('.')[-1].lower()
|
||||
|
||||
try:
|
||||
with CardSlot() as card:
|
||||
with open(fn, 'rb') as fp:
|
||||
data = fp.read(MAX_NFC_SIZE)
|
||||
|
||||
except CardMissingError:
|
||||
await needs_microsd()
|
||||
return
|
||||
|
||||
if data[2:6] == b'000000' and ctype == 'txn':
|
||||
# it's a txn, and we wrote as hex
|
||||
data = a2b_hex(data)
|
||||
|
||||
if ctype == 'psbt':
|
||||
sha = ngu.hash.sha256s(data)
|
||||
await self.share_psbt(data, len(data), sha, label="PSBT file: " + basename)
|
||||
elif ctype == 'txn':
|
||||
sha = ngu.hash.sha256s(data)
|
||||
txid = basename[0:64]
|
||||
if len(txid) != 64:
|
||||
# maybe some other txn file?
|
||||
txid = None
|
||||
await self.share_signed_txn(txid, data, len(data), sha)
|
||||
elif ctype == 'txt':
|
||||
await self.share_text(data.decode())
|
||||
else:
|
||||
raise ValueError(ctype)
|
||||
|
||||
async def import_multisig_nfc(self, *a):
|
||||
# user is pushing a file downloaded from another CC over NFC
|
||||
# - would need an NFC app in between for the sneakernet step
|
||||
# get some data
|
||||
data = await self.start_nfc_rx()
|
||||
if not data: return
|
||||
|
||||
winner = None
|
||||
for urn, msg, meta in ndef.record_parser(data):
|
||||
if len(msg) < 70: continue
|
||||
msg = bytes(msg).decode() # from memory view
|
||||
if 'pub' in msg:
|
||||
winner = msg
|
||||
break
|
||||
|
||||
if not winner:
|
||||
await ux_show_story('Unable to find data expected in NDEF')
|
||||
return
|
||||
|
||||
from auth import maybe_enroll_xpub
|
||||
try:
|
||||
maybe_enroll_xpub(config=winner)
|
||||
except Exception as e:
|
||||
#import sys; sys.print_exception(e)
|
||||
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
|
||||
|
||||
# EOF
|
||||
@ -14,7 +14,7 @@ class NumpadBase:
|
||||
|
||||
def __init__(self):
|
||||
# once pressed, and released; keys show up in this queue
|
||||
self._changes = Queue(24)
|
||||
self._changes = Queue(64)
|
||||
self.key_pressed = ''
|
||||
|
||||
self.debug = 0 # 0..2
|
||||
|
||||
@ -15,21 +15,24 @@
|
||||
# - to support multiple wallets and plausible deniablity, we
|
||||
# will preserve any noise already there, and only replace our own stuff
|
||||
# - you cannot move data between slots because AES-CTR with CTR seed based on slot #
|
||||
# - SHA check on decrypted data
|
||||
# - SHA-256 check on decrypted data
|
||||
# - (Mk4) each slot is a file on /flash/settings
|
||||
#
|
||||
import os, ujson, ustruct, ckcc, gc, ngu, aes256ctr
|
||||
import os, sys, ujson, ustruct, ckcc, gc, ngu, aes256ctr
|
||||
from uio import BytesIO
|
||||
from sffile import SFFile
|
||||
from sflash import SF
|
||||
from uhashlib import sha256
|
||||
from random import shuffle
|
||||
from random import shuffle, randbelow
|
||||
from utils import call_later_ms
|
||||
from version import mk_num, is_devmode
|
||||
from glob import PSRAM
|
||||
|
||||
# TODO fs.sync
|
||||
|
||||
# Setting values:
|
||||
# xfp = master xpub's fingerprint (32 bit unsigned)
|
||||
# xpub = master xpub in base58
|
||||
# chain = 3-letter codename for chain we are working on (BTC)
|
||||
# words = (bool) BIP-39 seed words exist (else XPRV or master secret based)
|
||||
# words = {0/12/18/24} nummber of BIP-39 seed words exist (default: 24, 0=XPRV, etc)
|
||||
# b39skip = (bool) skip discussion about use of BIP-39 passphrase
|
||||
# idle_to = idle timeout period (seconds)
|
||||
# _age = internal verison number for data (see below)
|
||||
@ -45,32 +48,44 @@ from utils import call_later_ms
|
||||
# axskip = (bool) skip warning about addr explorer
|
||||
# du = (bool) if set, disable the USB port at all times
|
||||
# rz = (int) display value resolution/units: 8=BTC 5=mBTC 2=bits 0=sats
|
||||
# tp = (complex) trick pins' config on Mk4
|
||||
# nfc = (bool) if set, enable the NFC feature; default is OFF=>DISABLED (mk4+)
|
||||
# vdsk = (bool) if set, enable the Virtual Disk features; default is OFF=>DISABLED (mk4+)
|
||||
# Stored w/ key=00 for access before login
|
||||
# _skip_pin = hard code a PIN value (dangerous, only for debug)
|
||||
# nick = optional nickname for this coldcard (personalization)
|
||||
# rngk = randomize keypad for PIN entry
|
||||
# delay_left = seconds remaining on login countdown, if defined
|
||||
# delay_left = [REMOVED-obsolete] seconds remaining on login countdown, if defined
|
||||
# lgto = (minutes) how long to wait for Login Countdown feature [in v4.0.2+]
|
||||
# cd_lgto = minutes to show in countdown (in countdown-to-brick mode)
|
||||
# cd_mode = set to enable some less-destructive modes
|
||||
# cd_pin = pin code which enables "countdown to brick" mode
|
||||
# cd_lgto = [<=mk3] minutes to show in countdown (in countdown-to-brick mode)
|
||||
# cd_mode = [<=mk3] set to enable some less-destructive modes
|
||||
# cd_pin = [<=mk3] pin code which enables "countdown to brick" mode
|
||||
# kbtn = (1 char) '1'-'9' that will wipe seed during login process (mk4+)
|
||||
|
||||
|
||||
# where in SPI Flash we work (last 128k)
|
||||
SLOTS = range((1024-128)*1024, 1024*1024, 4096)
|
||||
if mk_num <= 3:
|
||||
# where in SPI Flash we work (last 128k)
|
||||
SLOTS = range((1024-128)*1024, 1024*1024, 4096)
|
||||
NUM_SLOTS = 32
|
||||
|
||||
# Altho seems bad to statically alloc this big block, it solves
|
||||
# concerns with heap fragmentation, and saving settings is clearly
|
||||
# core to our mission!
|
||||
# 4k, but last 32 bytes are a SHA (itself encrypted)
|
||||
from sram2 import nvstore_buf
|
||||
_tmp = nvstore_buf
|
||||
from sffile import SFFile
|
||||
from sflash import SF
|
||||
else:
|
||||
# work in LFS2 filesystem instead, but same terminology
|
||||
SLOTS = range(0, 64)
|
||||
NUM_SLOTS = 64
|
||||
|
||||
MK4_WORKDIR = '/flash/settings/'
|
||||
|
||||
# for mk4: we store binary files on LFS2 filesystem
|
||||
def MK4_FILENAME(slot):
|
||||
return MK4_WORKDIR + ('%03x.aes' % slot)
|
||||
|
||||
class SettingsObject:
|
||||
|
||||
def __init__(self, dis=None):
|
||||
self.is_dirty = 0
|
||||
self.my_pos = 0
|
||||
self.my_pos = None
|
||||
|
||||
self.nvram_key = b'\0'*32
|
||||
self.capacity = 0
|
||||
@ -86,11 +101,11 @@ class SettingsObject:
|
||||
return aes256ctr.new(self.nvram_key, ctr)
|
||||
|
||||
def set_key(self, new_secret=None):
|
||||
# System settings (not secrets) are stored in SPI Flash, encrypted with this
|
||||
# System settings (not secrets) are stored in flash, encrypted with this
|
||||
# key that is derived from main wallet secret. Call this method when the secret
|
||||
# is first loaded, or changes for some reason.
|
||||
from pincodes import pa
|
||||
from stash import blank_object
|
||||
from stash import blank_object, SensitiveValues
|
||||
|
||||
key = None
|
||||
mine = False
|
||||
@ -98,11 +113,12 @@ class SettingsObject:
|
||||
if not new_secret:
|
||||
if not pa.is_successful() or pa.is_secret_blank():
|
||||
# simple fixed key allows us to store a few things when logged out
|
||||
key = b'\0'*32
|
||||
key = bytes(32)
|
||||
else:
|
||||
# read secret and use it.
|
||||
new_secret = pa.fetch()
|
||||
mine = True
|
||||
SensitiveValues.cache_secret(new_secret)
|
||||
|
||||
if new_secret:
|
||||
# hash up the secret... without decoding it or similar
|
||||
@ -123,66 +139,226 @@ class SettingsObject:
|
||||
# for restore from backup case, or when changing (created) the seed
|
||||
self.nvram_key = key
|
||||
|
||||
def load(self, dis=None):
|
||||
# Search all slots for any we can read, decrypt that,
|
||||
# and pick the newest one (in unlikely case of dups)
|
||||
# reset
|
||||
self.current.clear()
|
||||
self.overrides.clear()
|
||||
self.my_pos = 0
|
||||
self.is_dirty = 0
|
||||
self.capacity = 0
|
||||
def get_capacity(self):
|
||||
# percent space used (0.0=>empty)
|
||||
if mk_num <= 3:
|
||||
return self.capacity
|
||||
|
||||
# 4k, but last 32 bytes are a SHA (itself encrypted)
|
||||
global _tmp
|
||||
# could use whole filesystem, so use that as imprecise proxy
|
||||
_, _, blocks, bfree, *_ = os.statvfs(MK4_WORKDIR)
|
||||
|
||||
buf = bytearray(4)
|
||||
empty = 0
|
||||
for pos in SLOTS:
|
||||
if dis:
|
||||
dis.progress_bar_show((pos-SLOTS.start) / (SLOTS.stop-SLOTS.start))
|
||||
gc.collect()
|
||||
return (blocks-bfree) / blocks
|
||||
|
||||
|
||||
def _open_file(self, pos, mode='rb'):
|
||||
return open(MK4_FILENAME(pos), mode)
|
||||
|
||||
def _slot_is_blank(self, pos, buf):
|
||||
# read a few bytes from start of slot
|
||||
if mk_num <= 3:
|
||||
SF.read(pos, buf)
|
||||
if buf[0] == buf[1] == buf[2] == buf[3] == 0xff:
|
||||
# erased (probably)
|
||||
empty += 1
|
||||
continue
|
||||
return (buf[0] == buf[1] == buf[2] == buf[3] == 0xff)
|
||||
|
||||
# check if first 2 bytes makes sense for JSON
|
||||
aes = self.get_aes(pos)
|
||||
chk = aes.copy().cipher(b'{"')
|
||||
try:
|
||||
with self._open_file(pos) as fd:
|
||||
fd.readinto(buf)
|
||||
return False
|
||||
except:
|
||||
return True
|
||||
|
||||
if chk != buf[0:2]:
|
||||
# doesn't look like JSON meant for me
|
||||
continue
|
||||
def _wipe_slot(self, pos):
|
||||
# blank out a slot
|
||||
if mk_num <= 3:
|
||||
SF.wait_done()
|
||||
SF.sector_erase(pos)
|
||||
SF.wait_done()
|
||||
else:
|
||||
fn = MK4_FILENAME(pos)
|
||||
try:
|
||||
os.remove(fn)
|
||||
except BaseException as exc:
|
||||
# Error (ENOENT) expected here when saving first time, because the
|
||||
# "old" slot was not in use
|
||||
pass
|
||||
|
||||
# probably good, read it
|
||||
def _deny_slot(self, pos):
|
||||
# write garbage to look legit in a slot
|
||||
if mk_num <= 3:
|
||||
for i in range(0, 4096, 256):
|
||||
h = ngu.random.bytes(256)
|
||||
SF.wait_done()
|
||||
SF.write(pos+i, h)
|
||||
else:
|
||||
with self._open_file(pos, 'wb') as fd:
|
||||
for i in range(0, 4096, 256):
|
||||
h = ngu.random.bytes(256)
|
||||
fd.write(h)
|
||||
|
||||
def _read_slot(self, pos, decryptor):
|
||||
# return decrypted (json text) and last 32-bytes which is SHA-256 over that
|
||||
if mk_num <= 3:
|
||||
chk = sha256()
|
||||
aes = aes.cipher
|
||||
expect = None
|
||||
|
||||
from sram2 import nvstore_buf as _tmp
|
||||
|
||||
with SFFile(pos, length=4096, pre_erased=True) as fd:
|
||||
for i in range(4096/32):
|
||||
b = aes(fd.read(32))
|
||||
b = decryptor(fd.read(32))
|
||||
if i != 127:
|
||||
_tmp[i*32:(i*32)+32] = b
|
||||
chk.update(b)
|
||||
else:
|
||||
expect = b
|
||||
|
||||
# check how much space was used for encoded JSON
|
||||
try:
|
||||
end = bytes(_tmp).index(b'\0')
|
||||
self.capacity = end / (4096-32)
|
||||
except ValueError:
|
||||
self.capacity = 1.0
|
||||
|
||||
return _tmp, expect, chk.digest()
|
||||
else:
|
||||
# Mk4 is just reading a binary file and decrypt as we go.
|
||||
with self._open_file(pos) as fd:
|
||||
# missing ftell(), so emulate
|
||||
ln = fd.seek(0, 2)
|
||||
fd.seek(0, 0)
|
||||
|
||||
buf = fd.read(ln - 32)
|
||||
assert len(buf) == ln-32
|
||||
|
||||
rv = decryptor(buf)
|
||||
digest = ngu.hash.sha256s(rv)
|
||||
|
||||
expect = decryptor(fd.read(32))
|
||||
assert len(expect) == 32
|
||||
|
||||
return rv, expect, digest
|
||||
|
||||
def _write_slot(self, pos, aes):
|
||||
# SHA-256 over plaintext
|
||||
chk = sha256()
|
||||
|
||||
# serialize the data into JSON
|
||||
d = ujson.dumps(self.current)
|
||||
|
||||
if mk_num <= 3:
|
||||
with SFFile(pos, max_size=4096, pre_erased=True) as fd:
|
||||
# pad w/ zeros
|
||||
dat_len = len(d)
|
||||
pad_len = (4096-32) - dat_len
|
||||
assert pad_len >= 0, 'too big'
|
||||
|
||||
self.capacity = dat_len / 4096
|
||||
|
||||
fd.write(aes(d))
|
||||
chk.update(d)
|
||||
del d
|
||||
|
||||
while pad_len > 0:
|
||||
here = min(32, pad_len)
|
||||
|
||||
pad = bytes(here)
|
||||
fd.write(aes(pad))
|
||||
chk.update(pad)
|
||||
|
||||
pad_len -= here
|
||||
|
||||
fd.write(aes(chk.digest()))
|
||||
assert fd.tell() == 4096
|
||||
else:
|
||||
with self._open_file(pos, 'wb') as fd:
|
||||
# pad w/ zeros at least to 4k, but allow larger
|
||||
dat_len = len(d)
|
||||
pad_len = (4096-32) - dat_len
|
||||
|
||||
fd.write(aes(d))
|
||||
assert fd.tell() == dat_len
|
||||
chk.update(d)
|
||||
del d
|
||||
|
||||
while pad_len > 0:
|
||||
here = min(32, pad_len)
|
||||
|
||||
pad = bytes(here)
|
||||
fd.write(aes(pad))
|
||||
chk.update(pad)
|
||||
|
||||
pad_len -= here
|
||||
|
||||
fd.write(aes(chk.digest()))
|
||||
|
||||
def _used_slots(self):
|
||||
# mk4: faster list of slots in use; doesn't open them
|
||||
files = os.listdir(MK4_WORKDIR)
|
||||
return [int(fn[0:-4], 16) for fn in files if fn.endswith('.aes')]
|
||||
|
||||
def _nonempty_slots(self, dis=None):
|
||||
# generate slots that are non-empty
|
||||
taste = bytearray(4)
|
||||
|
||||
if mk_num <= 3:
|
||||
self.num_empty = 0
|
||||
|
||||
for pos in SLOTS:
|
||||
if dis:
|
||||
dis.progress_bar_show((pos-SLOTS.start) / (SLOTS.stop-SLOTS.start))
|
||||
gc.collect()
|
||||
|
||||
if self._slot_is_blank(pos, taste):
|
||||
# erased (probably)
|
||||
self.num_empty += 1
|
||||
continue
|
||||
|
||||
yield pos, taste
|
||||
else:
|
||||
# use directory listing
|
||||
files = self._used_slots()
|
||||
self.num_empty = NUM_SLOTS - len(files)
|
||||
|
||||
for i, pos in enumerate(files):
|
||||
if dis:
|
||||
dis.progress_bar_show(i / len(files))
|
||||
|
||||
if self._slot_is_blank(pos, taste):
|
||||
# unlikely case, but easy to handle
|
||||
continue
|
||||
|
||||
yield pos, taste
|
||||
|
||||
def load(self, dis=None):
|
||||
# Search all slots for any we can read, decrypt that,
|
||||
# and pick the newest one (in unlikely case of dups)
|
||||
# reset
|
||||
self.current.clear()
|
||||
self.overrides.clear()
|
||||
self.my_pos = None
|
||||
self.is_dirty = 0
|
||||
self.capacity = 0
|
||||
nonempty = set()
|
||||
|
||||
for pos, taste in self._nonempty_slots(dis):
|
||||
# check if first 2 bytes makes sense for JSON
|
||||
aes = self.get_aes(pos)
|
||||
chk = aes.copy().cipher(b'{"')
|
||||
nonempty.add(pos)
|
||||
|
||||
if chk != taste[0:2]:
|
||||
# doesn't look like JSON meant for me
|
||||
continue
|
||||
|
||||
# probably good, read it
|
||||
aes = aes.cipher
|
||||
json_data, expect, actual = self._read_slot(pos, aes)
|
||||
|
||||
try:
|
||||
# verify checksum in last 32 bytes
|
||||
assert expect == chk.digest()
|
||||
assert expect == actual
|
||||
|
||||
# loads() can't work from a byte array, and converting to
|
||||
# bytes here would copy it; better to use file emulation.
|
||||
fd = BytesIO(_tmp)
|
||||
d = ujson.load(fd)
|
||||
self.capacity = fd.seek(0,1) / 4096 # .tell() is missing
|
||||
d = ujson.loads(json_data)
|
||||
except:
|
||||
# One in 65k or so chance to come here w/ garbage decoded, so
|
||||
# not an error.
|
||||
# Good chance to come here w/ garbage decoded, so not an error.
|
||||
continue
|
||||
|
||||
got_age = d.get('_age', 0)
|
||||
@ -190,36 +366,37 @@ class SettingsObject:
|
||||
# likely winner
|
||||
self.current = d
|
||||
self.my_pos = pos
|
||||
#print("NV: data @ %d w/ age=%d" % (pos, got_age))
|
||||
#print("NV: data @ 0x%x w/ age=%d" % (pos, got_age))
|
||||
else:
|
||||
# stale data seen; clean it up.
|
||||
#print("NV: cleanup @ 0x%x" % pos)
|
||||
assert self.current['_age'] > 0
|
||||
#print("NV: cleanup @ %d" % pos)
|
||||
SF.sector_erase(pos)
|
||||
SF.wait_done()
|
||||
self._wipe_slot(pos)
|
||||
|
||||
# 4k is a large object, sigh, for us right now. cleanup
|
||||
gc.collect()
|
||||
|
||||
# done, if we found something
|
||||
if self.my_pos:
|
||||
if self.my_pos is not None:
|
||||
#print("NV: load done")
|
||||
return
|
||||
|
||||
# nothing found.
|
||||
self.my_pos = 0
|
||||
# nothing found, use defaults
|
||||
self.current = self.default_values()
|
||||
|
||||
if empty == len(SLOTS):
|
||||
# Whole thing is blank. Bad for plausible deniability. Write 3 slots
|
||||
# with garbage. They will be wasted space until it fills.
|
||||
blks = list(SLOTS)
|
||||
shuffle(blks)
|
||||
# pick a (new) random home
|
||||
self.my_pos = self.find_spot(-1)
|
||||
#print("NV: empty")
|
||||
|
||||
for pos in blks[0:3]:
|
||||
for i in range(0, 4096, 256):
|
||||
h = ngu.random.bytes(256)
|
||||
SF.wait_done()
|
||||
SF.write(pos+i, h)
|
||||
if is_devmode:
|
||||
self.current['chain'] = 'XTN'
|
||||
|
||||
if self.num_empty == NUM_SLOTS:
|
||||
# Whole thing is blank. Bad for plausible deniability. Write 3 slots
|
||||
# with white noise. They will be wasted space until it fills up.
|
||||
for _ in range(4):
|
||||
pos = self.find_spot(-1)
|
||||
self._deny_slot(pos)
|
||||
|
||||
def get(self, kn, default=None):
|
||||
if kn in self.overrides:
|
||||
@ -232,6 +409,11 @@ class SettingsObject:
|
||||
if self.is_dirty < 2:
|
||||
call_later_ms(250, self.write_out)
|
||||
|
||||
def save_if_dirty(self):
|
||||
# call when system is about to stop
|
||||
if self.is_dirty:
|
||||
self.save()
|
||||
|
||||
def put(self, kn, v):
|
||||
self.current[kn] = v
|
||||
self.changed()
|
||||
@ -274,25 +456,33 @@ class SettingsObject:
|
||||
# - check randomly and pick first blank one (wear leveling, deniability)
|
||||
# - we will write and then erase old slot
|
||||
# - if "full", blow away a random one
|
||||
options = [s for s in SLOTS if s != not_here]
|
||||
shuffle(options)
|
||||
if mk_num <= 3:
|
||||
options = [s for s in SLOTS if s != not_here]
|
||||
shuffle(options)
|
||||
|
||||
buf = bytearray(16)
|
||||
for pos in options:
|
||||
SF.read(pos, buf)
|
||||
if set(buf) == {0xff}:
|
||||
# blank
|
||||
return pos
|
||||
buf = bytearray(4)
|
||||
for pos in options:
|
||||
if self._slot_is_blank(pos, buf):
|
||||
# found a blank area
|
||||
return pos
|
||||
|
||||
# No where to write! (probably a bug because we have lots of slots)
|
||||
# ... so pick a random slot and kill what it had
|
||||
#print("ERROR: nvram full?")
|
||||
# No-where to write! (probably a bug because we have lots of slots)
|
||||
# ... so pick a random slot and kill what it had
|
||||
victim = options[0]
|
||||
else:
|
||||
# on mk4, use the filesystem to see what's already taken
|
||||
avail = set(SLOTS) - set(self._used_slots())
|
||||
avail.discard(not_here)
|
||||
|
||||
victem = options[0]
|
||||
SF.sector_erase(victem)
|
||||
SF.wait_done()
|
||||
if avail:
|
||||
return avail.pop()
|
||||
|
||||
return victem
|
||||
victim = randbelow(NUM_SLOTS)
|
||||
|
||||
#print("ERROR: nvram full")
|
||||
self._wipe_slot(victim)
|
||||
|
||||
return victim
|
||||
|
||||
def save(self):
|
||||
# render as JSON, encrypt and write it.
|
||||
@ -303,40 +493,11 @@ class SettingsObject:
|
||||
|
||||
aes = self.get_aes(pos).cipher
|
||||
|
||||
with SFFile(pos, max_size=4096, pre_erased=True) as fd:
|
||||
chk = sha256()
|
||||
|
||||
# first the json data
|
||||
d = ujson.dumps(self.current)
|
||||
|
||||
# pad w/ zeros
|
||||
dat_len = len(d)
|
||||
pad_len = (4096-32) - dat_len
|
||||
assert pad_len >= 0, 'too big'
|
||||
|
||||
self.capacity = dat_len / 4096
|
||||
|
||||
fd.write(aes(d))
|
||||
chk.update(d)
|
||||
del d
|
||||
|
||||
while pad_len > 0:
|
||||
here = min(32, pad_len)
|
||||
|
||||
pad = bytes(here)
|
||||
fd.write(aes(pad))
|
||||
chk.update(pad)
|
||||
|
||||
pad_len -= here
|
||||
|
||||
fd.write(aes(chk.digest()))
|
||||
assert fd.tell() == 4096
|
||||
self._write_slot(pos, aes)
|
||||
|
||||
# erase old copy of data
|
||||
if self.my_pos and self.my_pos != pos:
|
||||
SF.wait_done()
|
||||
SF.sector_erase(self.my_pos)
|
||||
SF.wait_done()
|
||||
if (self.my_pos is not None) and (self.my_pos != pos):
|
||||
self._wipe_slot(self.my_pos)
|
||||
|
||||
self.my_pos = pos
|
||||
self.is_dirty = 0
|
||||
@ -348,9 +509,8 @@ class SettingsObject:
|
||||
def blank(self):
|
||||
# erase current copy of values in nvram; older ones may exist still
|
||||
# - use when clearing the seed value
|
||||
if self.my_pos:
|
||||
SF.wait_done()
|
||||
SF.sector_erase(self.my_pos)
|
||||
if self.my_pos is not None:
|
||||
self._wipe_slot(self.my_pos)
|
||||
self.my_pos = 0
|
||||
|
||||
# act blank too, just in case.
|
||||
@ -365,8 +525,4 @@ class SettingsObject:
|
||||
# where value is used, and treat undefined as the default state.
|
||||
return dict(_age=0)
|
||||
|
||||
# not a singleton, but default widely-used object
|
||||
from glob import dis
|
||||
settings = SettingsObject(dis)
|
||||
|
||||
# EOF
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
|
||||
# and is covered by GPLv3 license found in COPYING.
|
||||
#
|
||||
# paper.py - generate paper wallets, based on random values (not linked to wallet)
|
||||
#
|
||||
from utils import imported
|
||||
from actions import needs_microsd
|
||||
from ux import ux_show_story, ux_dramatic_pause
|
||||
from files import CardSlot, CardMissingError
|
||||
from files import CardSlot, CardMissingError, needs_microsd
|
||||
from actions import file_picker
|
||||
from menu import MenuSystem, MenuItem
|
||||
|
||||
@ -137,7 +137,7 @@ class PaperWalletMaker:
|
||||
fname, nice_txt = card.pick_filename(basename +
|
||||
('-note.txt' if self.template_fn else '.txt'))
|
||||
|
||||
with open(fname, 'wt') as fp:
|
||||
with card.open(fname, 'wt') as fp:
|
||||
self.make_txt(fp, addr, wif, privkey, qr_addr, qr_wif)
|
||||
|
||||
if self.template_fn:
|
||||
|
||||
@ -35,6 +35,7 @@ CHANGE_BRICKME_PIN = const(0x004)
|
||||
CHANGE_SECRET = const(0x008)
|
||||
CHANGE_DURESS_SECRET = const(0x010)
|
||||
CHANGE_SECONDARY_WALLET_PIN = const(0x020)
|
||||
CHANGE_FIRMWARE = const(0x040) # Mk4+
|
||||
CHANGE_LS_OFFSET = const(0xf00)
|
||||
|
||||
# See below for other direction as well.
|
||||
@ -45,7 +46,7 @@ PA_ERROR_CODES = {
|
||||
-103: "RANGE_ERR",
|
||||
-104: "BAD_REQUEST",
|
||||
-105: "I_AM_BRICK",
|
||||
-106: "AE_FAIL",
|
||||
-106: "AE_FAIL", # SE1 on Mk4
|
||||
-107: "MUST_WAIT",
|
||||
-108: "PIN_REQUIRED",
|
||||
-109: "WRONG_SUCCESS",
|
||||
@ -54,6 +55,7 @@ PA_ERROR_CODES = {
|
||||
-112: "AUTH_FAIL",
|
||||
-113: "OLD_AUTH_FAIL",
|
||||
-114: "PRIMARY_ONLY",
|
||||
-115: "SE2_FAIL",
|
||||
}
|
||||
|
||||
# just a few of the likely ones; non-programing errors
|
||||
@ -111,8 +113,8 @@ class PinAttempt:
|
||||
self.is_empty = None
|
||||
self.tmp_value = False # simulated SE, in-ram only
|
||||
self.magic_value = PA_MAGIC_V2 if version.has_608 else PA_MAGIC_V1
|
||||
self.delay_achieved = 0 # so far, how much time wasted?
|
||||
self.delay_required = 0 # how much will be needed?
|
||||
self.delay_achieved = 0 # so far, how much time wasted?: mk4: tc_arg
|
||||
self.delay_required = 0 # how much will be needed? mk4: tc_flags
|
||||
self.num_fails = 0 # for UI: number of fails PINs
|
||||
self.attempts_left = 0 # ignore in mk1/2 case, only valid for mk3
|
||||
self.state_flags = 0 # useful readback
|
||||
@ -125,22 +127,21 @@ class PinAttempt:
|
||||
ustruct.calcsize(PIN_ATTEMPT_FMT)
|
||||
assert ustruct.calcsize(PIN_ATTEMPT_FMT_V2_ADDITIONS) == PIN_ATTEMPT_SIZE - PIN_ATTEMPT_SIZE_V1
|
||||
|
||||
self.buf = bytearray(PIN_ATTEMPT_SIZE if version.has_608 else 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: secondary=%d num_fails=%d delay=%d/%d state=0x%x>' % (
|
||||
self.is_secondary, self.num_fails,
|
||||
self.delay_achieved, self.delay_required, self.state_flags)
|
||||
return '<PinAttempt: fails/left=%d/%d tc_flag/arg=0x%x/0x%x>' % (
|
||||
self.num_fails, self.attempts_left,
|
||||
self.delay_required, self.delay_achieved)
|
||||
|
||||
def marshal(self, msg, is_duress=False, is_brickme=False, new_secret=None,
|
||||
new_pin=None, old_pin=None, get_duress_secret=False, is_secondary=False,
|
||||
ls_offset=None
|
||||
ls_offset=None, fw_upgrade=None, spare_num=None
|
||||
):
|
||||
# serialize our state, and maybe some arguments
|
||||
change_flags = 0
|
||||
@ -148,6 +149,8 @@ class PinAttempt:
|
||||
if new_secret is not None:
|
||||
change_flags |= CHANGE_SECRET if not is_duress else CHANGE_DURESS_SECRET
|
||||
assert len(new_secret) in (32, AE_SECRET_LEN)
|
||||
import stash
|
||||
stash.SensitiveValues.clear_cache()
|
||||
else:
|
||||
new_secret = bytes(AE_SECRET_LEN)
|
||||
|
||||
@ -178,6 +181,13 @@ class PinAttempt:
|
||||
|
||||
if ls_offset is not None:
|
||||
change_flags |= (ls_offset << 8) # see CHANGE_LS_OFFSET
|
||||
if spare_num is not None:
|
||||
assert 0 <= spare_num <= 3
|
||||
change_flags |= (spare_num << 8) # useful for fetch/change secret on Mk4
|
||||
|
||||
if fw_upgrade:
|
||||
change_flags = CHANGE_FIRMWARE
|
||||
new_secret = ustruct.pack('2I', *fw_upgrade) + bytes(AE_SECRET_LEN-8)
|
||||
|
||||
# can't send the V2 extra stuff if the bootrom isn't expecting it
|
||||
fields = [self.magic_value,
|
||||
@ -233,18 +243,23 @@ class PinAttempt:
|
||||
|
||||
return secret
|
||||
|
||||
def roundtrip(self, method_num, **kws):
|
||||
def roundtrip(self, method_num, after_buf=None, **kws):
|
||||
|
||||
self.marshal(self.buf, **kws)
|
||||
buf = bytearray(PIN_ATTEMPT_SIZE if version.has_608 else PIN_ATTEMPT_SIZE_V1)
|
||||
|
||||
self.marshal(buf, **kws)
|
||||
|
||||
if after_buf is not None:
|
||||
buf.extend(after_buf)
|
||||
|
||||
#print("> tx: %s" % b2a_hex(buf))
|
||||
|
||||
err = ckcc.gate(18, self.buf, method_num)
|
||||
err = ckcc.gate(18, buf, method_num)
|
||||
|
||||
#print("[%d] rx: %s" % (err, b2a_hex(self.buf)))
|
||||
#print("[%d] rx: %s" % (err, b2a_hex(buf)))
|
||||
|
||||
if err <= -100:
|
||||
#print("[%d] req: %s" % (err, b2a_hex(self.buf)))
|
||||
#print("[%d] req: %s" % (err, b2a_hex(buf)))
|
||||
if err == EPIN_I_AM_BRICK:
|
||||
# don't try to continue!
|
||||
enter_dfu(3)
|
||||
@ -252,7 +267,10 @@ class PinAttempt:
|
||||
elif err:
|
||||
raise RuntimeError(err)
|
||||
|
||||
return self.unmarshal(self.buf)
|
||||
if after_buf is not None:
|
||||
return buf[PIN_ATTEMPT_SIZE:]
|
||||
else:
|
||||
return self.unmarshal(buf)
|
||||
|
||||
@staticmethod
|
||||
def prefix_words(pin_prefix):
|
||||
@ -287,6 +305,9 @@ class PinAttempt:
|
||||
return rv
|
||||
|
||||
def is_delay_needed(self):
|
||||
# obsolete starting w/ mk3 and values re-used for other stuff
|
||||
if version.has_608:
|
||||
return False
|
||||
return self.delay_achieved < self.delay_required
|
||||
|
||||
def is_blank(self):
|
||||
@ -301,6 +322,7 @@ class PinAttempt:
|
||||
assert self.state_flags & PA_SUCCESSFUL
|
||||
return bool(self.state_flags & PA_ZERO_SECRET)
|
||||
|
||||
# Mk1/2/3 concepts, not used in Mk4
|
||||
def has_duress_pin(self):
|
||||
return bool(self.state_flags & PA_HAS_DURESS)
|
||||
def has_brickme_pin(self):
|
||||
@ -320,6 +342,7 @@ class PinAttempt:
|
||||
return self.state_flags
|
||||
|
||||
def delay(self):
|
||||
# obsolete since Mk3, but called from login.py
|
||||
self.roundtrip(1)
|
||||
|
||||
def login(self):
|
||||
@ -347,14 +370,17 @@ class PinAttempt:
|
||||
# - call new_main_secret() when main secret changes!
|
||||
# - is_secret_blank and is_successful may be wrong now, re-login to get again
|
||||
|
||||
def fetch(self, duress_pin=None):
|
||||
def fetch(self, duress_pin=None, spare_num=0):
|
||||
if self.tmp_value:
|
||||
# must make a copy here, and must be mutable instance so not reused
|
||||
if spare_num:
|
||||
return bytearray(AE_SECRET_LEN)
|
||||
return bytearray(self.tmp_value)
|
||||
|
||||
if duress_pin is None:
|
||||
secret = self.roundtrip(4)
|
||||
secret = self.roundtrip(4, spare_num=spare_num)
|
||||
else:
|
||||
# mk3 and earlier
|
||||
secret = self.roundtrip(4, old_pin=duress_pin, get_duress_secret=True)
|
||||
|
||||
return secret
|
||||
@ -365,11 +391,15 @@ class PinAttempt:
|
||||
if self.tmp_value:
|
||||
return bytes(AE_LONG_SECRET_LEN)
|
||||
|
||||
secret = b''
|
||||
for n in range(13):
|
||||
secret += self.roundtrip(6, ls_offset=n)[0:32]
|
||||
if version.mk_num < 4:
|
||||
secret = b''
|
||||
for n in range(13):
|
||||
secret += self.roundtrip(6, ls_offset=n)[0:32]
|
||||
|
||||
return secret
|
||||
return secret
|
||||
else:
|
||||
# faster method for Mk4
|
||||
return self.roundtrip(8, after_buf=bytes(AE_LONG_SECRET_LEN))
|
||||
|
||||
def ls_change(self, new_long_secret):
|
||||
# set the "long secret"
|
||||
@ -380,15 +410,23 @@ class PinAttempt:
|
||||
self.roundtrip(6, ls_offset=n, new_secret=new_long_secret[n*32:(n*32)+32])
|
||||
|
||||
def greenlight_firmware(self):
|
||||
# hash all of flash and commit value to 508a/608a
|
||||
# hash all of flash and commit value to SE1
|
||||
self.roundtrip(5)
|
||||
ckcc.presume_green()
|
||||
|
||||
def firmware_upgrade(self, start, length):
|
||||
# tell the bootrom to use data in PSRAM to upgrade now.
|
||||
# - requires main pin because it writes expected world check value before upgrade
|
||||
# - will fail if not self.is_successful() already (ie. right PIN entered)
|
||||
self.roundtrip(7, fw_upgrade=(start, length))
|
||||
# not-reached
|
||||
|
||||
def new_main_secret(self, raw_secret, chain=None):
|
||||
# Main secret has changed: reset the settings+their key,
|
||||
# and capture xfp/xpub
|
||||
from nvstore import settings
|
||||
from glob import settings
|
||||
import stash
|
||||
stash.SensitiveValues.clear_cache()
|
||||
|
||||
# capture values we have already
|
||||
old_values = dict(settings.current)
|
||||
@ -417,11 +455,41 @@ class PinAttempt:
|
||||
# Clear bip-39 secret, not applicable anymore.
|
||||
import stash
|
||||
stash.bip39_passphrase = ''
|
||||
stash.SensitiveValues.clear_cache()
|
||||
|
||||
# Copies system settings to new encrypted-key value, calculates
|
||||
# XFP, XPUB and saves into that, and starts using them.
|
||||
self.new_main_secret(self.tmp_value)
|
||||
|
||||
def trick_request(self, method_num, data):
|
||||
# send/recv a trick-pin related request (mk4 only)
|
||||
buf = bytearray(PIN_ATTEMPT_SIZE)
|
||||
self.marshal(buf)
|
||||
buf.extend(data)
|
||||
|
||||
err = ckcc.gate(22, buf, method_num)
|
||||
|
||||
#print("[%d] rx: %s" % (err, b2a_hex(buf)))
|
||||
|
||||
if err <= -100:
|
||||
raise BootloaderError(PA_ERROR_CODES[err], err)
|
||||
|
||||
return err, buf[PIN_ATTEMPT_SIZE:]
|
||||
|
||||
def is_deltamode(self):
|
||||
# (mk4 only) are we operating w/ a slightly wrong PIN code?
|
||||
if version.mk_num < 4:
|
||||
return False
|
||||
|
||||
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
|
||||
|
||||
|
||||
# singleton
|
||||
pa = PinAttempt()
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ from serializations import ser_compact_size, deser_compact_size, hash160, hash25
|
||||
from serializations import CTxIn, CTxInWitness, CTxOut, SIGHASH_ALL, ser_uint256
|
||||
from serializations import ser_sig_der, uint256_from_str, ser_push_data, uint256_from_str
|
||||
from serializations import ser_string
|
||||
from nvstore import settings
|
||||
from glob import settings
|
||||
|
||||
from public_constants import (
|
||||
PSBT_GLOBAL_UNSIGNED_TX, PSBT_GLOBAL_XPUB, PSBT_IN_NON_WITNESS_UTXO, PSBT_IN_WITNESS_UTXO,
|
||||
@ -138,7 +138,7 @@ def get_hash256(fd, poslen, hasher=None):
|
||||
|
||||
fd.seek(pos)
|
||||
while ll:
|
||||
here = fd.read_into(psbt_tmp256)
|
||||
here = fd.readinto(psbt_tmp256)
|
||||
if not here: break
|
||||
if here > ll:
|
||||
here = ll
|
||||
@ -918,7 +918,8 @@ class psbtObject(psbtProxy):
|
||||
if self.total_value_out is None:
|
||||
self.total_value_out = total_out
|
||||
else:
|
||||
assert self.total_value_out == total_out
|
||||
assert self.total_value_out == total_out, \
|
||||
'%s != %s' % (self.total_value_out, total_out)
|
||||
|
||||
def parse_txn(self):
|
||||
# Need to semi-parse in unsigned transaction.
|
||||
@ -1385,7 +1386,7 @@ class psbtObject(psbtProxy):
|
||||
# write out the ready-to-transmit txn
|
||||
# - means we are also a PSBT combiner in this case
|
||||
# - hard tho, due to variable length data.
|
||||
# - XXX probably a bad idea, so disabled for now
|
||||
# - probably a bad idea, so disabled for now
|
||||
out_fd.write(b'\x01\x00') # keylength=1, key=b'', PSBT_GLOBAL_UNSIGNED_TX
|
||||
|
||||
with SizerFile() as fd:
|
||||
@ -1495,6 +1496,12 @@ class psbtObject(psbtProxy):
|
||||
digest = self.make_txn_segwit_sighash(in_idx, txi,
|
||||
inp.amount, inp.scriptCode, inp.sighash)
|
||||
|
||||
if sv.deltamode:
|
||||
# Current user is actually a thug with a slightly wrong PIN, so we
|
||||
# do have access to the private keys and could sign txn, but we
|
||||
# are going to silently corrupt our signatures.
|
||||
digest = bytes(range(32))
|
||||
|
||||
if inp.is_multisig:
|
||||
# need to consider a set of possible keys, since xfp may not be unique
|
||||
for which_key in inp.required_key:
|
||||
|
||||
65
shared/psram.py
Normal file
65
shared/psram.py
Normal file
@ -0,0 +1,65 @@
|
||||
# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
# psram.py -- access PSRAM chip on Mk4
|
||||
#
|
||||
import version, uctypes
|
||||
|
||||
# already started and memory mapped by bootrom.
|
||||
|
||||
class PSRAMWrapper:
|
||||
base = 0x9000_0000 # OCTOSPI1
|
||||
length = 0x40_0000 # 4 meg (lower half)
|
||||
|
||||
def __init__(self):
|
||||
self._wr = uctypes.bytearray_at(self.base, self.length)
|
||||
|
||||
def read_at(self, offset, ln):
|
||||
# one-copy byte-wise access
|
||||
return uctypes.bytes_at(self.base+offset, ln)
|
||||
|
||||
def write_at(self, offset, ln):
|
||||
# word-aligned writes only
|
||||
assert offset % 4 == 0, offset
|
||||
assert ln % 4 == 0, ln
|
||||
assert offset + ln <= self.length, (offset+ln)
|
||||
|
||||
return memoryview(self._wr)[offset:offset+ln]
|
||||
|
||||
# Be compatible with SPIFlash class...
|
||||
|
||||
def read(self, address, buf, cmd=None):
|
||||
buf[:] = self.read_at(address, len(buf))
|
||||
|
||||
def write(self, address, buf):
|
||||
ln = len(buf)
|
||||
if ln % 4:
|
||||
assert address % 4 == 0, address
|
||||
runt = ln % 4
|
||||
tb = buf + bytes(4-runt)
|
||||
self.write_at(address, len(tb))[:] = tb
|
||||
else:
|
||||
self.write_at(address, ln)[:] = buf
|
||||
|
||||
def is_busy(self):
|
||||
return False
|
||||
def wait_done(self):
|
||||
return
|
||||
|
||||
# we are not flash
|
||||
def chip_erase(self):
|
||||
return
|
||||
def sector_erase(self, address):
|
||||
return
|
||||
def block_erase(self, address):
|
||||
return
|
||||
|
||||
def wipe_all(self):
|
||||
# works, but code in bootrom is much faster and better (rng values used)
|
||||
|
||||
z = bytes(16384)
|
||||
for pos in range(0, self.length, len(z)):
|
||||
self.write_at(pos, len(z))[:] = z
|
||||
|
||||
dis.progress_bar_show(pos / self.length)
|
||||
|
||||
# EOF
|
||||
@ -3,7 +3,7 @@
|
||||
# pwsave.py - Save bip39 passphrases into encrypted file on MicroSD (if desired)
|
||||
#
|
||||
import sys, stash, ujson, os, ngu
|
||||
from files import CardSlot, CardMissingError
|
||||
from files import CardSlot, CardMissingError, needs_microsd
|
||||
|
||||
class PassphraseSaver:
|
||||
# Encrypts BIP-39 passphrase very carefully, and appends
|
||||
@ -46,7 +46,6 @@ class PassphraseSaver:
|
||||
# encrypt and save; always appends.
|
||||
from ux import ux_dramatic_pause
|
||||
from glob import dis
|
||||
from actions import needs_microsd
|
||||
|
||||
while 1:
|
||||
dis.fullscreen('Saving...')
|
||||
@ -130,7 +129,7 @@ class PassphraseSaver:
|
||||
pw, expect_xfp = item.arg
|
||||
set_bip39_passphrase(pw)
|
||||
|
||||
from nvstore import settings
|
||||
from glob import settings
|
||||
from utils import xfp2str
|
||||
xfp = settings.get('xfp')
|
||||
|
||||
|
||||
@ -133,6 +133,7 @@ class QRDisplaySingle(UserInteraction):
|
||||
|
||||
|
||||
async def interact_bare(self):
|
||||
from glob import NFC
|
||||
self.redraw()
|
||||
|
||||
while 1:
|
||||
@ -143,6 +144,11 @@ class QRDisplaySingle(UserInteraction):
|
||||
self.invert = not self.invert
|
||||
self.redraw()
|
||||
continue
|
||||
elif NFC and ch == '3':
|
||||
# Share any QR over NFC!
|
||||
await NFC.share_text(self.addrs[self.idx])
|
||||
self.redraw()
|
||||
continue
|
||||
elif ch in 'xy':
|
||||
break
|
||||
elif len(self.addrs) == 1:
|
||||
@ -165,5 +171,4 @@ class QRDisplaySingle(UserInteraction):
|
||||
await self.interact_bare()
|
||||
the_ux.pop()
|
||||
|
||||
|
||||
# EOF
|
||||
|
||||
@ -21,7 +21,7 @@ from actions import goto_top_menu
|
||||
from stash import SecretStash, SensitiveValues
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from pwsave import PassphraseSaver
|
||||
from nvstore import settings
|
||||
from glob import settings
|
||||
from pincodes import pa
|
||||
|
||||
# seed words lengths we support: 24=>256 bits, and recommended
|
||||
@ -225,7 +225,7 @@ class WordNestMenu(MenuSystem):
|
||||
set_seed_value(new_words)
|
||||
|
||||
# clear menu stack
|
||||
goto_top_menu()
|
||||
goto_top_menu(first_time=True)
|
||||
|
||||
return None
|
||||
|
||||
@ -347,7 +347,7 @@ for 256-bits of security, 99 rolls.''' % count)
|
||||
|
||||
return count, seed
|
||||
|
||||
async def import_from_dice():
|
||||
async def new_from_dice(nwords):
|
||||
# Use lots of (D6) dice rolls to create seed entropy.
|
||||
# Note: only 2.585 bits of entropy per roll, so need lots!
|
||||
# 50 => 128bits, 99 => 256bits
|
||||
@ -359,32 +359,34 @@ async def import_from_dice():
|
||||
|
||||
if count == 0: return
|
||||
|
||||
await approve_word_list(seed)
|
||||
await approve_word_list(seed, nwords)
|
||||
|
||||
async def make_new_wallet():
|
||||
async def make_new_wallet(nwords):
|
||||
# Pick a new random seed.
|
||||
|
||||
await ux_dramatic_pause('Generating...', 4)
|
||||
await ux_dramatic_pause('Generating...', 3)
|
||||
|
||||
# always full 24-word (256 bit) entropy
|
||||
# starting point
|
||||
seed = random.bytes(32)
|
||||
|
||||
assert len(set(seed)) > 4 # TRNG failure
|
||||
|
||||
# hash to mitigate possible bias in TRNG
|
||||
seed = ngu.hash.sha256s(seed)
|
||||
|
||||
await approve_word_list(seed)
|
||||
await approve_word_list(seed, nwords)
|
||||
|
||||
async def approve_word_list(seed):
|
||||
async def approve_word_list(seed, nwords):
|
||||
# Force the user to write the seeds words down, give a quiz, then save them.
|
||||
|
||||
# LESSON LEARNED: if the user is writting down the words, as we have
|
||||
# vividly instructed, then it's a big deal to lose those words and have to start
|
||||
# over. So confirm that action, and don't volunteer it.
|
||||
|
||||
if nwords == 12:
|
||||
seed = seed[0:16]
|
||||
|
||||
words = bip39.b2a_words(seed).split(' ')
|
||||
assert len(words) == 24
|
||||
assert len(words) == nwords
|
||||
|
||||
while 1:
|
||||
# show the seed words
|
||||
@ -402,7 +404,7 @@ async def approve_word_list(seed):
|
||||
# dice roll mode
|
||||
count, new_seed = await add_dice_rolls(0, seed, False)
|
||||
if count:
|
||||
seed = new_seed
|
||||
seed = new_seed[0:16] if nwords == 12 else new_seed
|
||||
words = bip39.b2a_words(seed).split(' ')
|
||||
|
||||
continue
|
||||
@ -430,7 +432,7 @@ async def approve_word_list(seed):
|
||||
set_seed_value(words)
|
||||
|
||||
# send them to home menu, now with a wallet enabled
|
||||
goto_top_menu()
|
||||
goto_top_menu(first_time=True)
|
||||
|
||||
def set_seed_value(words=None, encoded=None):
|
||||
# Save the seed words into secure element, and reboot. BIP-39 password
|
||||
@ -461,16 +463,20 @@ def set_seed_value(words=None, encoded=None):
|
||||
nv = encoded
|
||||
|
||||
from glob import dis
|
||||
dis.fullscreen('Applying...')
|
||||
pa.change(new_secret=nv)
|
||||
try:
|
||||
dis.fullscreen('Applying...')
|
||||
dis.busy_bar(True)
|
||||
pa.change(new_secret=nv)
|
||||
|
||||
# re-read settings since key is now different
|
||||
# - also captures xfp, xpub at this point
|
||||
pa.new_main_secret(nv)
|
||||
# re-read settings since key is now different
|
||||
# - also captures xfp, xpub at this point
|
||||
pa.new_main_secret(nv)
|
||||
|
||||
# check and reload secret
|
||||
pa.reset()
|
||||
pa.login()
|
||||
# check and reload secret
|
||||
pa.reset()
|
||||
pa.login()
|
||||
finally:
|
||||
dis.busy_bar(False)
|
||||
|
||||
def set_bip39_passphrase(pw):
|
||||
# apply bip39 passphrase for now (volatile)
|
||||
@ -520,22 +526,28 @@ async def remember_bip39_passphrase():
|
||||
|
||||
def clear_seed():
|
||||
from glob import dis
|
||||
import utime
|
||||
import utime, callgate
|
||||
|
||||
dis.fullscreen('Clearing...')
|
||||
dis.busy_bar(True)
|
||||
|
||||
# clear settings associated with this key, since it will be no more
|
||||
settings.blank()
|
||||
|
||||
# save a blank secret (all zeros is a special case, detected by bootloader)
|
||||
nv = bytes(AE_SECRET_LEN)
|
||||
pa.change(new_secret=nv)
|
||||
if version.mk_num >= 4:
|
||||
callgate.fast_wipe(True)
|
||||
# NOT REACHED
|
||||
else:
|
||||
# save a blank secret (all zeros is a special case, detected by bootloader)
|
||||
nv = bytes(AE_SECRET_LEN)
|
||||
pa.change(new_secret=nv)
|
||||
|
||||
if version.has_608:
|
||||
# wipe the long secret too
|
||||
nv = bytes(AE_LONG_SECRET_LEN)
|
||||
pa.ls_change(nv)
|
||||
if version.has_608:
|
||||
# wipe the long secret too
|
||||
nv = bytes(AE_LONG_SECRET_LEN)
|
||||
pa.ls_change(nv)
|
||||
|
||||
dis.busy_bar(False)
|
||||
dis.fullscreen('Reboot...')
|
||||
utime.sleep(1)
|
||||
|
||||
|
||||
@ -6,12 +6,12 @@ import ckcc
|
||||
from uasyncio import sleep_ms
|
||||
from glob import dis
|
||||
from display import FontLarge
|
||||
from ux import ux_wait_keyup, ux_clear_keys, ux_poll_once
|
||||
from ux import ux_wait_keyup, ux_clear_keys, ux_poll_key
|
||||
from ux import ux_show_story
|
||||
from callgate import get_dfu_button, get_is_bricked, get_genuine, clear_genuine
|
||||
from utils import imported
|
||||
from callgate import get_is_bricked, get_genuine, clear_genuine
|
||||
from utils import problem_file_line
|
||||
import version
|
||||
from nvstore import settings
|
||||
from glob import settings
|
||||
|
||||
async def test_numpad():
|
||||
# do an interactive self test
|
||||
@ -56,13 +56,11 @@ async def test_secure_element():
|
||||
assert not get_is_bricked() # bricked already
|
||||
|
||||
# test right chips installed
|
||||
is_fat = ckcc.is_stm32l496()
|
||||
if is_fat:
|
||||
assert version.has_608 # expect 608a
|
||||
assert version.hw_label == 'mk3'
|
||||
if version.has_fatram:
|
||||
assert version.has_608 # expect 608
|
||||
else:
|
||||
assert not version.has_608 # expect 508a
|
||||
assert version.hw_label != 'mk3'
|
||||
assert version.hw_label == 'mk2'
|
||||
|
||||
if ckcc.is_simulator(): return
|
||||
|
||||
@ -115,7 +113,64 @@ async def test_sd_active():
|
||||
k = await ux_wait_keyup('xy')
|
||||
assert k == 'y' # "SD Active LED bust"
|
||||
|
||||
async def test_usb_light():
|
||||
# Mk4's new USB activity light (right by connector)
|
||||
|
||||
if version.mk_num < 4: return
|
||||
|
||||
from machine import Pin
|
||||
p = Pin('USB_ACTIVE', Pin.OUT)
|
||||
|
||||
try:
|
||||
p.value(1)
|
||||
dis.clear()
|
||||
dis.text(0,0, "USB light on? ^^")
|
||||
dis.show()
|
||||
|
||||
k = await ux_wait_keyup('xy')
|
||||
assert k == 'y' # "USB Active LED bust"
|
||||
finally:
|
||||
p.value(0)
|
||||
|
||||
async def test_nfc():
|
||||
# Mk4: NFC chip and field
|
||||
if not version.has_nfc: return
|
||||
from nfc import NFCHandler
|
||||
await NFCHandler.selftest()
|
||||
|
||||
async def test_psram():
|
||||
if not version.has_psram: return
|
||||
|
||||
from glob import PSRAM
|
||||
from ustruct import pack
|
||||
import ngu
|
||||
|
||||
dis.clear()
|
||||
dis.text(None, 18, 'PSRAM Test')
|
||||
dis.show()
|
||||
|
||||
test_len = PSRAM.length * 2
|
||||
chk = bytearray(32)
|
||||
spots = set()
|
||||
for pos in range(0, PSRAM.length, 800 * 17):
|
||||
if pos >= PSRAM.length: break
|
||||
rnd = ngu.hash.sha256s(pack('I', pos))
|
||||
|
||||
PSRAM.write(pos, rnd)
|
||||
PSRAM.read(pos, chk)
|
||||
assert chk == rnd, "bad @ 0x%x" % pos
|
||||
dis.progress_bar_show(pos / test_len)
|
||||
spots.add(pos)
|
||||
|
||||
for pos in spots:
|
||||
rnd = ngu.hash.sha256s(pack('I', pos))
|
||||
PSRAM.read(pos, chk)
|
||||
assert chk == rnd, "RB bad @ 0x%x" % pos
|
||||
dis.progress_bar_show((PSRAM.length + pos) / test_len)
|
||||
|
||||
async def test_sflash():
|
||||
if version.has_psram: return
|
||||
|
||||
dis.clear()
|
||||
dis.text(None, 18, 'Serial Flash')
|
||||
dis.show()
|
||||
@ -148,6 +203,7 @@ async def test_sflash():
|
||||
|
||||
rnd = ngu.hash.sha256s(pack('I', addr))
|
||||
SF.write(addr, rnd)
|
||||
SF.wait_done()
|
||||
SF.read(addr, buf)
|
||||
assert buf == rnd # "write failed"
|
||||
|
||||
@ -186,7 +242,7 @@ async def test_microsd():
|
||||
while 1:
|
||||
if want == sd.present(): return
|
||||
await sleep_ms(100)
|
||||
if ux_poll_once():
|
||||
if ux_poll_key():
|
||||
raise RuntimeError("MicroSD test aborted")
|
||||
|
||||
try:
|
||||
@ -202,7 +258,7 @@ async def test_microsd():
|
||||
# debounce
|
||||
await sleep_ms(100)
|
||||
if sd.present(): break
|
||||
if ux_poll_once():
|
||||
if ux_poll_key():
|
||||
raise RuntimeError("MicroSD test aborted")
|
||||
|
||||
dis.clear()
|
||||
@ -214,13 +270,16 @@ async def test_microsd():
|
||||
assert sd.present() #, "SD not present?"
|
||||
|
||||
# power up?
|
||||
sd.power(1)
|
||||
await sleep_ms(100)
|
||||
await sleep_ms(100) # required
|
||||
ok = sd.power(1)
|
||||
assert ok # "sd.power() fail"
|
||||
await sleep_ms(100) # prob'ly not required
|
||||
|
||||
try:
|
||||
blks, bsize, *unused = sd.info()
|
||||
assert bsize == 512
|
||||
except:
|
||||
# sd.info() returns None if problem
|
||||
assert 0 # , "card info"
|
||||
|
||||
# just read it a bit, writing would prove little
|
||||
@ -245,11 +304,14 @@ async def start_selftest():
|
||||
|
||||
try:
|
||||
await test_oled()
|
||||
await test_psram()
|
||||
await test_nfc()
|
||||
await test_sflash()
|
||||
await test_microsd()
|
||||
await test_numpad()
|
||||
await test_sflash()
|
||||
await test_secure_element()
|
||||
await test_sd_active()
|
||||
await test_usb_light()
|
||||
|
||||
# add more tests here
|
||||
|
||||
@ -257,6 +319,7 @@ async def start_selftest():
|
||||
await ux_show_story("Selftest complete", 'PASS')
|
||||
|
||||
except (RuntimeError, AssertionError) as e:
|
||||
e = str(e) or problem_file_line(e)
|
||||
await ux_show_story("Test failed:\n" + str(e), 'FAIL')
|
||||
|
||||
|
||||
|
||||
150
shared/sffile.py
150
shared/sffile.py
@ -1,25 +1,37 @@
|
||||
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
# sffile.py - file-like objects stored in SPI Flash
|
||||
# sffile.py - file-like objects stored in SPI Flash (Mk1-3) or PSRAM (Mk4+)
|
||||
#
|
||||
# - implements stream IO protoccol
|
||||
# - does erasing for you
|
||||
# - random read, sequential write
|
||||
# - only a few of these are possible
|
||||
# - the offset is the file name
|
||||
# - last 64k of memory reserved for settings
|
||||
# - (<Mk3) last 64k of memory reserved for settings
|
||||
#
|
||||
from uasyncio import sleep_ms
|
||||
from uio import BytesIO
|
||||
from uhashlib import sha256
|
||||
from sflash import SF
|
||||
from version import has_psram
|
||||
|
||||
# this code works on large "blocks" defined by the chip as 64k
|
||||
blksize = const(65536)
|
||||
if not has_psram:
|
||||
# Use SPI flash chip
|
||||
from sflash import SF
|
||||
|
||||
def PADOUT(n):
|
||||
# rounds up
|
||||
return (n + blksize - 1) & ~(blksize-1)
|
||||
# this code works on large "blocks" defined by the chip as 64k
|
||||
blksize = 65536
|
||||
|
||||
def PADOUT(n):
|
||||
# rounds up
|
||||
return (n + blksize - 1) & ~(blksize-1)
|
||||
else:
|
||||
# Use PSRAM chip
|
||||
from glob import PSRAM
|
||||
blksize = 4
|
||||
PADOUT = lambda n: n
|
||||
|
||||
def ALIGN4(n):
|
||||
return n & ~0x3
|
||||
|
||||
class SFFile:
|
||||
def __init__(self, start, length=0, max_size=None, message=None, pre_erased=False):
|
||||
@ -29,12 +41,20 @@ class SFFile:
|
||||
self.pos = 0
|
||||
self.length = length # byte-wise length
|
||||
self.message = message
|
||||
self.runt = False
|
||||
|
||||
if max_size != None:
|
||||
# Write
|
||||
self.max_size = PADOUT(max_size) if not pre_erased else max_size
|
||||
self.readonly = False
|
||||
self.checksum = sha256()
|
||||
|
||||
if has_psram:
|
||||
# up to 3 bytes that haven't been written-out yet
|
||||
self.runt = bytearray()
|
||||
self._pos = 0
|
||||
else:
|
||||
# Read
|
||||
self.readonly = True
|
||||
|
||||
def tell(self):
|
||||
@ -70,6 +90,8 @@ class SFFile:
|
||||
assert not self.readonly
|
||||
assert self.length == 0 # 'already wrote?'
|
||||
|
||||
if has_psram: return
|
||||
|
||||
for i in range(0, self.max_size, blksize):
|
||||
SF.block_erase(self.start + i)
|
||||
|
||||
@ -88,6 +110,8 @@ class SFFile:
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
|
||||
if self.message:
|
||||
from glob import dis
|
||||
dis.progress_bar_show(1)
|
||||
@ -95,49 +119,83 @@ class SFFile:
|
||||
return False
|
||||
|
||||
def wait_writable(self):
|
||||
# TODO: timeouts here
|
||||
if has_psram: return
|
||||
|
||||
while SF.is_busy():
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
# PSRAM might leave a little behind
|
||||
if self.runt:
|
||||
# write final runt, might be up to 3 bytes (padding w/ zeros)
|
||||
assert len(self.runt) <= 3 # , 'rl=%d'%len(self.runt)
|
||||
assert self._pos + len(self.runt) == self.pos
|
||||
self.runt.extend(bytes(4-len(self.runt)))
|
||||
PSRAM.write(self.start + self._pos, self.runt)
|
||||
|
||||
self.runt = None
|
||||
self._pos = self.pos
|
||||
|
||||
def write(self, b):
|
||||
# immediate write, no buffering
|
||||
assert not self.readonly
|
||||
assert self.pos == self.length # "can only append"
|
||||
assert self.pos + len(b) <= self.max_size # "past end: %r" % [self.pos, len(b), self.max_size]
|
||||
assert self.pos == self.length # "can only append"
|
||||
assert self.pos + len(b) <= self.max_size # "past end"
|
||||
|
||||
left = len(b)
|
||||
|
||||
# must perform page-aligned (256) writes, but can start
|
||||
# anywhere in the page, and can write just one byte
|
||||
sofar = 0
|
||||
|
||||
while left:
|
||||
if (self.pos + sofar) % 256 != 0:
|
||||
# start is unaligned, do a partial write to align
|
||||
assert sofar == 0 #, (sofar, (self.pos+sofar)) # can only happen on first page
|
||||
runt = min(left, 256 - (self.pos % 256))
|
||||
here = memoryview(b)[0:runt]
|
||||
assert len(here) == runt
|
||||
else:
|
||||
# write full pages, or final runt
|
||||
here = memoryview(b)[sofar:sofar+256]
|
||||
assert 1 <= len(here) <= 256
|
||||
if has_psram:
|
||||
# Mk4: memory-mapped, but can only do word-aligned writes
|
||||
self.checksum.update(b)
|
||||
|
||||
self.wait_writable()
|
||||
self.runt.extend(b)
|
||||
here = ALIGN4(len(self.runt))
|
||||
if here:
|
||||
PSRAM.write(self.start + self._pos, self.runt[0:here])
|
||||
self._pos += here
|
||||
self.runt = self.runt[here:]
|
||||
|
||||
SF.write(self.start + self.pos + sofar, here)
|
||||
|
||||
left -= len(here)
|
||||
sofar += len(here)
|
||||
self.checksum.update(here)
|
||||
self.pos += left
|
||||
self.length = self.pos
|
||||
|
||||
assert left >= 0
|
||||
if self.message:
|
||||
from glob import dis
|
||||
dis.progress_sofar(self.pos, self.length)
|
||||
|
||||
assert sofar == len(b)
|
||||
self.pos += sofar
|
||||
self.length = self.pos
|
||||
return left
|
||||
|
||||
return sofar
|
||||
else:
|
||||
# must perform page-aligned (256) writes, but can start
|
||||
# anywhere in the page, and can write just one byte
|
||||
sofar = 0
|
||||
|
||||
while left:
|
||||
if (self.pos + sofar) % 256 != 0:
|
||||
# start is unaligned, do a partial write to align
|
||||
assert sofar == 0 #, (sofar, (self.pos+sofar)) # can only happen on first page
|
||||
runt = min(left, 256 - (self.pos % 256))
|
||||
here = memoryview(b)[0:runt]
|
||||
assert len(here) == runt
|
||||
else:
|
||||
# write full pages, or final runt
|
||||
here = memoryview(b)[sofar:sofar+256]
|
||||
assert 1 <= len(here) <= 256
|
||||
|
||||
self.wait_writable()
|
||||
|
||||
SF.write(self.start + self.pos + sofar, here)
|
||||
|
||||
left -= len(here)
|
||||
sofar += len(here)
|
||||
self.checksum.update(here)
|
||||
|
||||
assert left >= 0
|
||||
|
||||
assert sofar == len(b)
|
||||
self.pos += sofar
|
||||
self.length = self.pos
|
||||
return sofar
|
||||
|
||||
def read(self, ll=None):
|
||||
if ll == 0:
|
||||
@ -152,32 +210,32 @@ class SFFile:
|
||||
return b''
|
||||
|
||||
rv = bytearray(ll)
|
||||
SF.read(self.start + self.pos, rv)
|
||||
if has_psram:
|
||||
PSRAM.read(self.start + self.pos, rv)
|
||||
else:
|
||||
SF.read(self.start + self.pos, rv)
|
||||
|
||||
self.pos += ll
|
||||
|
||||
if self.message and ll > 1:
|
||||
from glob import dis
|
||||
dis.progress_bar_show(self.pos / self.length)
|
||||
|
||||
# altho tempting to return a bytearray (which we already have) many
|
||||
# callers expect return to be bytes and have those methods, like "find"
|
||||
return bytes(rv)
|
||||
|
||||
def read_into(self, b):
|
||||
def readinto(self, b):
|
||||
# limitation: this will read past end of file, but not tell the caller
|
||||
actual = min(self.length - self.pos, len(b))
|
||||
if actual <= 0:
|
||||
return 0
|
||||
|
||||
SF.read(self.start + self.pos, b)
|
||||
if has_psram:
|
||||
PSRAM.read(self.start + self.pos, b)
|
||||
else:
|
||||
SF.read(self.start + self.pos, b)
|
||||
|
||||
self.pos += actual
|
||||
|
||||
return actual
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
class SizerFile(SFFile):
|
||||
# looks like a file, but forgets everything except file position
|
||||
@ -211,7 +269,7 @@ class SizerFile(SFFile):
|
||||
def read(self, ll=None):
|
||||
raise ValueError
|
||||
|
||||
def read_into(self, b):
|
||||
def readinto(self, b):
|
||||
raise ValueError
|
||||
|
||||
def close(self):
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
# During firmware updates, entire flash, starting at zero may be used.
|
||||
#
|
||||
import machine
|
||||
from version import mk_num
|
||||
|
||||
CMD_WRSR = const(0x01)
|
||||
CMD_WRITE = const(0x02)
|
||||
@ -39,7 +40,11 @@ class SPIFlash:
|
||||
def __init__(self):
|
||||
from machine import Pin
|
||||
|
||||
self.spi = machine.SPI(2, baudrate=8000000)
|
||||
# chip can do 80Mhz, but very limited prescaler-only baudrate generation, so
|
||||
# sysclk/2 or /4 will happen depending on Mk3 vs. 4
|
||||
# - Mk4: 120Mhz => 60Mhz result (div 2)
|
||||
# - Mk3: 80Mhz => 40Mhz result (div 2)
|
||||
self.spi = machine.SPI(2, baudrate=80_000_000)
|
||||
self.cs = Pin('SF_CS', Pin.OUT)
|
||||
|
||||
def cmd(self, cmd, addr=None, complete=True, pad=False):
|
||||
@ -112,12 +117,13 @@ class SPIFlash:
|
||||
|
||||
def wipe_most(self):
|
||||
# erase everything except settings: takes 5 seconds at least
|
||||
from glob import dis
|
||||
|
||||
assert mk_num <= 3 # obsolete in mk4
|
||||
|
||||
from nvstore import SLOTS
|
||||
end = SLOTS[0]
|
||||
|
||||
from glob import dis
|
||||
dis.fullscreen("Cleanup...")
|
||||
|
||||
for addr in range(0, end, self.BLOCK_SIZE):
|
||||
self.block_erase(addr)
|
||||
dis.progress_bar_show(addr/end)
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
# DEBUG ONLY -- only installed for debug builds
|
||||
# Hack to monitor screen contents, as text.
|
||||
# Import this file to install the hacks.
|
||||
import ux
|
||||
|
||||
global contents, full_contents, story
|
||||
|
||||
@ -51,13 +55,10 @@ def hack_show(themself, *a, **kw):
|
||||
|
||||
|
||||
# Also monitor "UX stories"
|
||||
import ux
|
||||
|
||||
orig_show_story = ux.ux_show_story
|
||||
|
||||
ux.ux_show_story = lambda *a, **kw: hack_story(*a, **kw)
|
||||
|
||||
def hack_story(msg, title=None, **kw):
|
||||
async def hack_story(msg, title=None, **kw):
|
||||
global story
|
||||
|
||||
if hasattr(msg, 'readline'):
|
||||
@ -67,22 +68,16 @@ def hack_story(msg, title=None, **kw):
|
||||
|
||||
#print("Story: %s: %s" % (title, msg))
|
||||
|
||||
return orig_show_story(msg, title, **kw)
|
||||
rv = await orig_show_story(msg, title, **kw)
|
||||
|
||||
# And menus
|
||||
|
||||
def read_menu():
|
||||
# helper: return contents of current menu
|
||||
from ux import the_ux
|
||||
from menu import MenuSystem
|
||||
|
||||
top = the_ux.top_of_stack()
|
||||
if not top: return None
|
||||
|
||||
if not isinstance(top, MenuSystem):
|
||||
return repr(top)
|
||||
|
||||
return list(it.label for it in top.items)
|
||||
story = None
|
||||
|
||||
return rv
|
||||
ux.ux_show_story = hack_story
|
||||
|
||||
# remove pauses that lengthen test case times...
|
||||
async def no_drama(msg, seconds):
|
||||
print("Pause (%ds): %s" % (seconds, msg))
|
||||
ux.ux_dramatic_pause = no_drama
|
||||
|
||||
# EOF
|
||||
@ -8,20 +8,27 @@
|
||||
# - top page of that is specially marked to cause reset if any attempt to change
|
||||
# - 2k at bottom reserved for code in `flashbdev.c` to use as cache data for flash writing
|
||||
# - keep this file in sync with simulated version
|
||||
# - none of the above is true anymore on mk4
|
||||
#
|
||||
import uctypes, ckcc
|
||||
from version import mk_num
|
||||
|
||||
# see stm32/COLDCARD/layout.ld where this is effectively defined
|
||||
SRAM2_START = const(0x10000800)
|
||||
SRAM2_LENGTH = const(0x5800)
|
||||
if mk_num < 4:
|
||||
# see stm32/COLDCARD/layout.ld where this is effectively defined
|
||||
SRAM2_START = const(0x10000800)
|
||||
SRAM2_LENGTH = const(0x5800)
|
||||
|
||||
_start = SRAM2_START
|
||||
_start = SRAM2_START
|
||||
|
||||
def _alloc(ln):
|
||||
global _start
|
||||
rv = uctypes.bytearray_at(_start, ln)
|
||||
_start += ln
|
||||
return rv
|
||||
def _alloc(ln):
|
||||
global _start
|
||||
rv = uctypes.bytearray_at(_start, ln)
|
||||
_start += ln
|
||||
return rv
|
||||
else:
|
||||
# Mk4 has tons of memory, so just use it
|
||||
def _alloc(ln):
|
||||
return bytearray(ln)
|
||||
|
||||
nvstore_buf = _alloc(4096-32)
|
||||
display_buf = _alloc(1024)
|
||||
@ -30,9 +37,10 @@ usb_buf = _alloc(2048+12) # 2060 @ 0x10001be0
|
||||
tmp_buf = _alloc(1024)
|
||||
psbt_tmp256 = _alloc(256)
|
||||
|
||||
assert _start <= 0x10006000
|
||||
if mk_num < 4:
|
||||
assert _start <= 0x10006000
|
||||
|
||||
# observed: about 22k on Mk2
|
||||
ckcc.stack_limit(SRAM2_LENGTH - (_start - SRAM2_START))
|
||||
# observed: about 22k on Mk2
|
||||
ckcc.stack_limit(SRAM2_LENGTH - (_start - SRAM2_START))
|
||||
|
||||
# EOF
|
||||
|
||||
@ -123,7 +123,10 @@ class SSD1306_SPI(SSD1306):
|
||||
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):
|
||||
@ -131,5 +134,8 @@ class SSD1306_SPI(SSD1306):
|
||||
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)
|
||||
|
||||
166
shared/stash.py
166
shared/stash.py
@ -10,10 +10,10 @@
|
||||
# - 'abandon' * 17 + 'agent'
|
||||
# - 'abandon' * 11 + 'about'
|
||||
#
|
||||
import ngu, uctypes, gc, bip39
|
||||
import ngu, uctypes, gc, bip39, utime
|
||||
from uhashlib import sha256
|
||||
from pincodes import AE_SECRET_LEN
|
||||
from utils import swab32
|
||||
from utils import swab32, call_later_ms
|
||||
|
||||
def blank_object(item):
|
||||
# Use/abuse uctypes to blank objects under python. Will likely
|
||||
@ -27,15 +27,20 @@ def blank_object(item):
|
||||
buf[i] = 0
|
||||
elif isinstance(item, ngu.hdnode.HDNode):
|
||||
item.blank()
|
||||
elif item is None:
|
||||
pass
|
||||
else:
|
||||
raise TypeError(item)
|
||||
|
||||
|
||||
# Chip can hold 72-bytes as a secret: we need to store either
|
||||
# a list of seed words (packed), of various lengths, or maybe
|
||||
# a raw master secret, and so on
|
||||
def len_to_numwords(vlen):
|
||||
# map length of binary secret to number of BIP-39 seed words
|
||||
assert vlen in [16, 24, 32]
|
||||
return 6 * (vlen // 8)
|
||||
|
||||
class SecretStash:
|
||||
# Chip can hold 72-bytes as a secret: we need to store either
|
||||
# a list of seed words (packed), of various lengths, or maybe
|
||||
# a raw master secret, and so on.
|
||||
|
||||
@staticmethod
|
||||
def encode(seed_phrase=None, master_secret=None, xprv=None):
|
||||
@ -95,12 +100,17 @@ class SecretStash:
|
||||
|
||||
# make master secret, using the memonic words, and passphrase (or empty string)
|
||||
seed_bits = secret[1:1+ll]
|
||||
|
||||
# slow: 2+ seconds
|
||||
ms = bip39.master_secret(bip39.b2a_words(seed_bits), _bip39pw)
|
||||
|
||||
hd.from_master(ms)
|
||||
|
||||
return 'words', seed_bits, hd
|
||||
|
||||
elif marker == 0x00:
|
||||
# probably all zeros, which we don't normally store, and represents "no secret"
|
||||
raise ValueError('actually zero secret')
|
||||
else:
|
||||
# variable-length master secret for BIP-32
|
||||
vlen = secret[0]
|
||||
@ -115,38 +125,127 @@ class SecretStash:
|
||||
# optional global value: user-supplied passphrase to salt BIP-39 seed process
|
||||
bip39_passphrase = ''
|
||||
|
||||
CACHE_CHECK_RATE = const(10*1000) # 10 seconds
|
||||
CACHE_MAX_LIFE = const(60*1000) # one minute
|
||||
|
||||
class SensitiveValues:
|
||||
# be a context manager, and holder to secrets in-memory
|
||||
# be a context manager, and holder of secrets in-memory
|
||||
|
||||
# class-level cache, key is bip39 pass
|
||||
_cache = {}
|
||||
_cache_secret = None
|
||||
_cache_used = None
|
||||
|
||||
def __init__(self, secret=None, bypass_pw=False):
|
||||
if secret is None:
|
||||
# fetch the secret from bootloader/atecc508a
|
||||
from pincodes import pa
|
||||
|
||||
if pa.is_secret_blank():
|
||||
raise ValueError('no secrets yet')
|
||||
|
||||
self.secret = pa.fetch()
|
||||
self.spots = [ self.secret ]
|
||||
else:
|
||||
# sometimes we already know it
|
||||
#assert set(secret) != {0}
|
||||
self.secret = secret
|
||||
self.spots = []
|
||||
self.spots = []
|
||||
|
||||
# backup during volatile bip39 encryption: do not use passphrase
|
||||
self._bip39pw = '' if bypass_pw else str(bip39_passphrase)
|
||||
|
||||
def __enter__(self):
|
||||
import chains
|
||||
if secret is not None:
|
||||
# sometimes we already know the secret
|
||||
self.secret = secret
|
||||
self.deltamode = False
|
||||
|
||||
self.mode, self.raw, self.node = SecretStash.decode(self.secret, self._bip39pw)
|
||||
self.mode, self.raw, self.node = SecretStash.decode(self.secret, self._bip39pw)
|
||||
else:
|
||||
# More typical: fetch the secret from bootloader and SE
|
||||
# - but that's real slow, so avoid if possible
|
||||
from pincodes import pa
|
||||
|
||||
if pa.is_secret_blank():
|
||||
raise ValueError('no secrets yet')
|
||||
self.deltamode = pa.is_deltamode()
|
||||
|
||||
if self._bip39pw in self._cache:
|
||||
# cache hit
|
||||
self.secret = bytearray(self._cache_secret)
|
||||
self.mode, r, n = self._cache[self._bip39pw]
|
||||
self.raw = bytearray(r)
|
||||
self.node = n.copy()
|
||||
self.__class__._cache_used = utime.ticks_ms()
|
||||
else:
|
||||
if self._cache_secret:
|
||||
# they are using new BIP39 passphrase but we already have raw secret
|
||||
self.secret = bytearray(self._cache_secret)
|
||||
else:
|
||||
# slow: read from secure element(s)
|
||||
self.secret = pa.fetch()
|
||||
|
||||
# slow: do bip39 key stretching (typically)
|
||||
self.mode, self.raw, self.node = SecretStash.decode(self.secret, self._bip39pw)
|
||||
|
||||
self.save_to_cache()
|
||||
|
||||
self.spots.append(self.secret)
|
||||
|
||||
self.spots.append(self.node)
|
||||
self.spots.append(self.raw)
|
||||
self.spots.append(self.node)
|
||||
|
||||
import chains
|
||||
self.chain = chains.current_chain()
|
||||
|
||||
@classmethod
|
||||
def clear_cache(cls):
|
||||
# clear cached secrets we have
|
||||
# - call any time, certainly when main secret changes
|
||||
# - will be called after 2 minutes of idle keypad
|
||||
blank_object(cls._cache_secret)
|
||||
cls._cache_secret = None
|
||||
|
||||
for _,raw,node in cls._cache.values():
|
||||
blank_object(raw)
|
||||
blank_object(node)
|
||||
|
||||
cls._cache.clear()
|
||||
cls._cache_used = None
|
||||
|
||||
def save_to_cache(self):
|
||||
# add to cache, must copy here to avoid wipe
|
||||
if not self._cache_secret:
|
||||
SensitiveValues._cache_secret = bytearray(self.secret)
|
||||
else:
|
||||
assert SensitiveValues._cache_secret == self.secret
|
||||
|
||||
SensitiveValues._cache[self._bip39pw] = ( self.mode, bytearray(self.raw), self.node.copy() )
|
||||
SensitiveValues._cache_used = utime.ticks_ms()
|
||||
|
||||
call_later_ms(CACHE_CHECK_RATE, self.cache_check)
|
||||
|
||||
@classmethod
|
||||
def cache_secret(cls, main_secret):
|
||||
# During login we learn the main secret so we can decrypt
|
||||
# the settings, so want to catch that in cache since user is likely
|
||||
# to do something useful immediately after login
|
||||
SensitiveValues._cache_used = utime.ticks_ms()
|
||||
|
||||
if cls._cache_secret:
|
||||
assert SensitiveValues._cache_secret == main_secret
|
||||
return
|
||||
|
||||
SensitiveValues._cache_secret = bytearray(main_secret)
|
||||
call_later_ms(CACHE_CHECK_RATE, cls.cache_check)
|
||||
|
||||
@classmethod
|
||||
async def cache_check(cls):
|
||||
# verify the cache has been used recently, else clear it.
|
||||
|
||||
if not cls._cache_used:
|
||||
# called after already cleared
|
||||
return
|
||||
|
||||
now = utime.ticks_ms()
|
||||
dt = utime.ticks_diff(now, cls._cache_used)
|
||||
|
||||
if dt >= CACHE_MAX_LIFE:
|
||||
# clear cached secrets after 1 minute if unused
|
||||
cls.clear_cache()
|
||||
else:
|
||||
# keep waiting
|
||||
call_later_ms(CACHE_CHECK_RATE, cls.cache_check)
|
||||
|
||||
def __enter__(self):
|
||||
# complexity moved to __init__
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
@ -177,9 +276,9 @@ class SensitiveValues:
|
||||
return True
|
||||
|
||||
def capture_xpub(self):
|
||||
# track my xpubkey fingerprint & value in settings (not sensitive really)
|
||||
# track my xpubkey fingerprint & xpub value in settings (not sensitive really)
|
||||
# - we share these on any USB connection
|
||||
from nvstore import settings
|
||||
from glob import settings
|
||||
|
||||
# Implicit in the values is the BIP-39 encryption passphrase,
|
||||
# which we might not want to actually store.
|
||||
@ -195,7 +294,12 @@ class SensitiveValues:
|
||||
settings.put('xpub', xpub)
|
||||
|
||||
settings.put('chain', self.chain.ctype)
|
||||
settings.put('words', (self.mode == 'words'))
|
||||
|
||||
# calc num words in seed, or zero
|
||||
nw = 0
|
||||
if self.mode == 'words':
|
||||
nw = len_to_numwords(len(self.raw))
|
||||
settings.put('words', nw)
|
||||
|
||||
def register(self, item):
|
||||
# Caller can add his own sensitive (derived?) data to our wiper
|
||||
@ -230,7 +334,9 @@ class SensitiveValues:
|
||||
def duress_root(self):
|
||||
# Return a bip32 node for the duress wallet linked to this wallet.
|
||||
# 0x80000000 - 0xCC10 = 2147431408
|
||||
dirty = self.derive_path("m/2147431408'/0'/0'")
|
||||
# Obsoleted in Mk4: use BIP-85 instead
|
||||
p = "m/2147431408'/0'/0'"
|
||||
dirty = self.derive_path(p)
|
||||
|
||||
# clear the parent linkage by rebuilding it.
|
||||
cc, pk = dirty.chain_code(), dirty.privkey()
|
||||
@ -241,7 +347,7 @@ class SensitiveValues:
|
||||
rv.from_chaincode_privkey(cc, pk)
|
||||
self.register(rv)
|
||||
|
||||
return rv
|
||||
return rv, p
|
||||
|
||||
def encryption_key(self, salt):
|
||||
# Return a 32-byte derived secret to be used for our own internal encryption purposes
|
||||
|
||||
925
shared/trick_pins.py
Normal file
925
shared/trick_pins.py
Normal file
@ -0,0 +1,925 @@
|
||||
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
# trick_pins.py - manage the "trick" PIN codes, which can do anything but let you in!
|
||||
#
|
||||
# - mk4+ only
|
||||
# - uses SE2 to store PIN codes (hashed) and what actions to perform for each
|
||||
# - replaces old "duress wallet" and "brickme" features
|
||||
# - changes require knowledge of real PIN code (it is checked)
|
||||
#
|
||||
import version, uctypes, errno, ngu, sys, ckcc, stash, bip39
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from menu import MenuSystem, MenuItem
|
||||
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_enter_number, the_ux, ux_aborted
|
||||
from stash import SecretStash
|
||||
from drv_entro import bip85_derive
|
||||
|
||||
# see from mk4-bootloader/se2.h
|
||||
NUM_TRICKS = const(14)
|
||||
TRICK_SLOT_LAYOUT = {
|
||||
"slot_num": 0 | uctypes.INT32,
|
||||
"tc_flags": 4 | uctypes.UINT16,
|
||||
"tc_arg": 6 | uctypes.UINT16,
|
||||
"xdata": (8 | uctypes.ARRAY, 64 | uctypes.UINT8),
|
||||
"pin": (8+64 | uctypes.ARRAY, 16 | uctypes.UINT8),
|
||||
"pin_len": (8+64+16) | uctypes.INT32,
|
||||
"blank_slots": (8+64+16+4) | uctypes.UINT32,
|
||||
"spare": ((8+64+16+4+4) | uctypes.ARRAY, 8|uctypes.INT32),
|
||||
}
|
||||
TC_WIPE = const(0x8000)
|
||||
TC_BRICK = const(0x4000)
|
||||
TC_FAKE_OUT = const(0x2000)
|
||||
TC_WORD_WALLET = const(0x1000)
|
||||
TC_XPRV_WALLET = const(0x0800)
|
||||
TC_DELTA_MODE = const(0x0400)
|
||||
TC_REBOOT = const(0x0200)
|
||||
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
|
||||
|
||||
# tc_args encoding:
|
||||
# TC_WORD_WALLET -> BIP-85 index, 1001..1003 for 24 words, 2001..2003 for 12-words
|
||||
|
||||
# special "pin" used as catch-all for wrong pins
|
||||
WRONG_PIN_CODE = '!p'
|
||||
|
||||
def validate_delta_pin(true_pin, proposed_delta_pin):
|
||||
# Check delta pin proposal works w/ limitations and
|
||||
# provide error msg, and/or calc required tc_arg value.
|
||||
right = true_pin.replace('-', '')
|
||||
fake = proposed_delta_pin.replace('-', '')
|
||||
|
||||
if (len(right) != len(fake)) or (right[0:-4] != fake[0:-4]):
|
||||
prob = '''\
|
||||
Trick PIN must be same length (%d) as true PIN and \
|
||||
up to last four digits can be different between true PIN and trick.''' % len(right)
|
||||
return prob, 0
|
||||
|
||||
a = 0
|
||||
for i in range(4):
|
||||
dx = -(1+i)
|
||||
if right[dx] == fake[dx]:
|
||||
# no need to reveal this digit to SE2 hacker if same
|
||||
a |= 0xf << (i*4)
|
||||
else:
|
||||
a |= (ord(right[-(1+i)]) - 0x30) << (i*4)
|
||||
|
||||
return None, a
|
||||
|
||||
def construct_duress_secret(flags, tc_arg):
|
||||
# is duress wallet required and if so, what are the secret values (32 or 64 bytes)
|
||||
if flags & TC_WORD_WALLET:
|
||||
# derive the secret via BIP-85
|
||||
nwords = 24 if (tc_arg//1000 == 1) else 12
|
||||
mmode = 0 if (nwords == 12) else 2 # weak: based on menu design
|
||||
new_secret, _, _, path = bip85_derive(mmode, tc_arg)
|
||||
path = "BIP85(words=%d, index=%d)" % (nwords, tc_arg)
|
||||
|
||||
elif flags & TC_XPRV_WALLET:
|
||||
# use old method for duress wallets
|
||||
with stash.SensitiveValues() as sv:
|
||||
node, path = sv.duress_root()
|
||||
new_secret = SecretStash.encode(xprv=node)[1:65]
|
||||
assert len(new_secret) == 64
|
||||
else:
|
||||
return (None, None)
|
||||
|
||||
return path, new_secret
|
||||
|
||||
def make_slot():
|
||||
b = bytearray(uctypes.sizeof(TRICK_SLOT_LAYOUT))
|
||||
return b, uctypes.struct(uctypes.addressof(b), TRICK_SLOT_LAYOUT)
|
||||
|
||||
class TrickPinMgmt:
|
||||
|
||||
def __init__(self):
|
||||
assert uctypes.sizeof(TRICK_SLOT_LAYOUT) == 128
|
||||
self.reload()
|
||||
|
||||
def reload(self):
|
||||
# we track known PINS as a dictionary:
|
||||
# pin (in ascii) => (slot_num, tc_flags, arg)
|
||||
from glob import settings
|
||||
self.tp = settings.get('tp', {})
|
||||
|
||||
def save_record(self):
|
||||
# commit changes back to settings
|
||||
from glob import settings
|
||||
settings.set('tp', self.tp)
|
||||
settings.save()
|
||||
|
||||
def roundtrip(self, method_num, slot_buf=None):
|
||||
from pincodes import pa
|
||||
|
||||
if slot_buf is not None:
|
||||
arg = slot_buf
|
||||
else:
|
||||
# use zeros
|
||||
assert method_num == 0
|
||||
arg = bytes(uctypes.sizeof(TRICK_SLOT_LAYOUT))
|
||||
|
||||
rc, data = pa.trick_request(method_num, arg)
|
||||
|
||||
if slot_buf is not None:
|
||||
# overwrite request w/ result (works inplace)
|
||||
slot_buf[:] = data
|
||||
|
||||
return rc
|
||||
|
||||
def clear_all(self):
|
||||
# get rid of them all
|
||||
self.roundtrip(0)
|
||||
self.tp = {}
|
||||
self.save_record()
|
||||
|
||||
def forget_pin(self, pin):
|
||||
# forget about settings for a PIN
|
||||
self.tp.pop(pin, None)
|
||||
self.save_record()
|
||||
|
||||
def restore_pin(self, new_pin):
|
||||
# remember/restore PIN that we "forgot", return T if worked
|
||||
b, slot = tp.get_by_pin(new_pin)
|
||||
if slot is None: return False
|
||||
|
||||
record = (slot.slot_num, slot.tc_flags,
|
||||
0xffff if slot.tc_flags & TC_DELTA_MODE else slot.tc_arg)
|
||||
self.tp[new_pin] = record
|
||||
self.save_record()
|
||||
|
||||
return True
|
||||
|
||||
def clear_slots(self, slot_nums):
|
||||
# remove some slots, not all
|
||||
b, slot = make_slot()
|
||||
slot.blank_slots = sum(1<<s for s in slot_nums)
|
||||
self.roundtrip(2, b)
|
||||
|
||||
def get_available_slots(self):
|
||||
# do an impossible search, so we can get blank_slots field back
|
||||
b, slot = make_slot()
|
||||
slot.pin_len = 1
|
||||
self.roundtrip(1, b) # expects ENOENT=2
|
||||
|
||||
blk = slot.blank_slots
|
||||
return [i for i in range(NUM_TRICKS) if (1<<i & blk)]
|
||||
|
||||
def find_empty_slots(self, qty_needed):
|
||||
# locate a slot (or 3) that are available for use
|
||||
avail = self.get_available_slots()
|
||||
if qty_needed == 1:
|
||||
return avail[0] if avail else None
|
||||
else:
|
||||
for sn in avail:
|
||||
if all((sn+i in avail) for i in range(1, qty_needed)):
|
||||
return sn
|
||||
return None
|
||||
|
||||
def get_by_pin(self, pin):
|
||||
# fetch slot details based on a PIN code (which must be known already somehow)
|
||||
b, slot = make_slot()
|
||||
|
||||
if isinstance(pin, str):
|
||||
pin = pin.encode()
|
||||
|
||||
slot.pin_len = len(pin)
|
||||
slot.pin[0:slot.pin_len] = pin
|
||||
|
||||
rc = self.roundtrip(1, b)
|
||||
if rc == errno.ENOENT:
|
||||
return None, None
|
||||
|
||||
# these fields are zeros on return, but we need them for CRUD
|
||||
slot.pin_len = len(pin)
|
||||
slot.pin[0:slot.pin_len] = pin
|
||||
|
||||
return b, slot
|
||||
|
||||
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 *
|
||||
'''
|
||||
assert isinstance(pin, bytes)
|
||||
|
||||
b, slot = self.get_by_pin(pin)
|
||||
if not slot:
|
||||
if not new: raise KeyError("wrong pin")
|
||||
|
||||
# Making a new entry
|
||||
b, slot = make_slot()
|
||||
new_pin = pin
|
||||
|
||||
# pick a free slot
|
||||
sn = self.find_empty_slots(1 if not secret else 1+(len(secret)//32))
|
||||
if sn == None:
|
||||
# we are full
|
||||
raise RuntimeError("no space left")
|
||||
|
||||
slot.slot_num = sn
|
||||
|
||||
if new_pin is not None:
|
||||
slot.pin_len = len(new_pin)
|
||||
slot.pin[0:slot.pin_len] = new_pin
|
||||
if new_pin != pin:
|
||||
self.tp.pop(pin.decode(), None)
|
||||
pin = new_pin
|
||||
|
||||
if tc_flags is not None:
|
||||
assert 0 <= tc_flags <= 65536
|
||||
slot.tc_flags = tc_flags
|
||||
|
||||
if tc_arg is not None:
|
||||
assert 0 <= tc_arg <= 65536
|
||||
slot.tc_arg = tc_arg
|
||||
|
||||
if secret is not None:
|
||||
# expecting an encoded secret
|
||||
if len(secret) <= 32:
|
||||
# words.
|
||||
assert slot.tc_flags & TC_WORD_WALLET
|
||||
slot.xdata[0:len(secret)] = secret
|
||||
elif len(secret) == 64:
|
||||
# expecting 64 bytes encoded already
|
||||
assert slot.tc_flags & TC_XPRV_WALLET
|
||||
slot.xdata[0:64] = secret
|
||||
else:
|
||||
raise ValueError()
|
||||
|
||||
# Save config for later
|
||||
# - deltamode: don't document real pin digits
|
||||
record = (slot.slot_num, slot.tc_flags,
|
||||
0xffff if slot.tc_flags & TC_DELTA_MODE else slot.tc_arg)
|
||||
|
||||
slot.blank_slots = 0
|
||||
rc = self.roundtrip(2, b)
|
||||
assert rc == 0
|
||||
|
||||
# record key details.
|
||||
self.tp[pin.decode()] = record
|
||||
self.save_record()
|
||||
|
||||
return b, slot
|
||||
|
||||
def all_tricks(self):
|
||||
# put them in order, with "wrong" last
|
||||
return sorted(self.tp.keys(), key=lambda i: i if (i != WRONG_PIN_CODE) else 'Z')
|
||||
|
||||
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
|
||||
tc_flags, tc_arg = pa.get_tc_values()
|
||||
|
||||
if tc_flags & TC_COUNTDOWN:
|
||||
return tc_arg or 60
|
||||
else:
|
||||
return 0
|
||||
|
||||
def get_deltamode_pins(self):
|
||||
# iterate over all delta-mode PIN's defined.
|
||||
for k, (sn,flags,args) in self.tp.items():
|
||||
if flags & TC_DELTA_MODE:
|
||||
yield k
|
||||
|
||||
def get_duress_pins(self):
|
||||
# iterate over all duress wallets
|
||||
for k, (sn,flags,args) in self.tp.items():
|
||||
if flags & (TC_WORD_WALLET | TC_XPRV_WALLET):
|
||||
yield k
|
||||
|
||||
def check_new_main_pin(self, pin):
|
||||
# user is trying to change main PIN to new value; check for issues
|
||||
# - dups bad but also: delta mode pin might not work w/ longer main true pin
|
||||
# - return error msg or None
|
||||
assert isinstance(pin, str)
|
||||
if pin in self.tp:
|
||||
return 'That PIN is already in use as a Trick PIN.'
|
||||
|
||||
for d_pin in self.get_deltamode_pins():
|
||||
prob, _ = validate_delta_pin(pin, d_pin)
|
||||
if prob:
|
||||
return 'That PIN value makes problems with a Delta Mode Trick PIN.'
|
||||
|
||||
def main_pin_has_changed(self, new_main_pin):
|
||||
# update any delta-mode entries we have
|
||||
for d_pin in self.get_deltamode_pins():
|
||||
prob, arg = validate_delta_pin(new_main_pin, d_pin)
|
||||
assert not prob # see check_new_main_pin() above
|
||||
self.update_slot(d_pin.encode(), tc_arg=arg)
|
||||
|
||||
def backup_duress_wallets(self, sv):
|
||||
# for backup file, yield (label, path, pairs-of-data)
|
||||
done = set()
|
||||
for pin in self.get_duress_pins():
|
||||
sn, flags, arg = self.tp[pin]
|
||||
|
||||
if (flags, arg) in done:
|
||||
continue
|
||||
done.add( (flags, arg) )
|
||||
|
||||
if flags & TC_WORD_WALLET:
|
||||
label = "Duress: BIP-85 Derived wallet"
|
||||
nwords = 12 if ((arg // 1000) == 2) else 24
|
||||
path = "BIP85(words=%d, index=%d)" % (nwords, arg)
|
||||
b, slot = tp.get_by_pin(pin)
|
||||
words = bip39.b2a_words(slot.xdata[0:(32 if nwords==24 else 16)])
|
||||
|
||||
d = [ ('duress_%d_words' % arg, words) ]
|
||||
elif flags & TC_XPRV_WALLET:
|
||||
label = "Duress: XPRV Wallet"
|
||||
node, path = sv.duress_root()
|
||||
path = 'path = ' + path
|
||||
# backwards compat name, but skipping xpub this time
|
||||
d = [ ('duress_xprv', sv.chain.serialize_private(node)) ]
|
||||
|
||||
yield (label, path, d)
|
||||
|
||||
def restore_backup(self, vals):
|
||||
# restoring backup value
|
||||
# - need to re-populate SE2 w/ these values, including duress wallets
|
||||
# - being restored: vals=self.tp
|
||||
# - CAUTION: new true-pin may not match old true-pin; skip any that would
|
||||
# not work w/ new pin (conflicting value, or deltamode issues)
|
||||
from pincodes import pa
|
||||
true_pin = pa.pin.decode()
|
||||
|
||||
for pin in vals:
|
||||
(sn, flags, arg) = vals[pin]
|
||||
|
||||
if pin == true_pin:
|
||||
# drop conflicting trick pin vs. (new) true pin
|
||||
continue
|
||||
|
||||
if flags & TC_DELTA_MODE:
|
||||
prob = validate_delta_pin(true_pin, pin)
|
||||
if prob:
|
||||
# just forget it, no UI here to report issue
|
||||
continue
|
||||
|
||||
try:
|
||||
# might need to construct a BIP-85 or XPRV secret to match
|
||||
path, new_secret = construct_duress_secret(flags, arg)
|
||||
|
||||
b, slot = tp.update_slot(pin.encode(), new=True,
|
||||
tc_flags=flags, tc_arg=arg, secret=new_secret)
|
||||
except Exception as exc:
|
||||
sys.print_exception(exc) # not visible
|
||||
|
||||
|
||||
tp = TrickPinMgmt()
|
||||
|
||||
class TrickPinMenu(MenuSystem):
|
||||
|
||||
def __init__(self):
|
||||
self.WillWipeMenu = None
|
||||
super().__init__(self.construct())
|
||||
|
||||
@classmethod
|
||||
async def make_menu(cls, *unused):
|
||||
# used to build menu at runtime, in response to parent menu item
|
||||
return cls()
|
||||
|
||||
@property
|
||||
def current_pin(self):
|
||||
from pincodes import pa
|
||||
return pa.pin.decode()
|
||||
|
||||
def construct(self):
|
||||
# Dynamic menu with PIN codes as the items, plus a few static choices
|
||||
|
||||
# not going to work well if tmp secret in effect
|
||||
from pincodes import pa
|
||||
if bool(pa.tmp_value):
|
||||
return [MenuItem('Not Available')]
|
||||
|
||||
tp.reload()
|
||||
tricks = tp.all_tricks()
|
||||
|
||||
if self.current_pin in tricks:
|
||||
# They got into here with a trick PIN, so it must be
|
||||
# a deltamode pin, or something else tricky ... hide it from menu
|
||||
# since it reveals that fact to attacker.
|
||||
tricks.remove(self.current_pin)
|
||||
|
||||
has_wrong = False
|
||||
|
||||
rv = []
|
||||
|
||||
if tricks:
|
||||
rv.append(MenuItem('Trick PINs:'))
|
||||
for pin in tricks:
|
||||
if pin == WRONG_PIN_CODE:
|
||||
rv.append(MenuItem('↳WRONG PIN', menu=self.pin_submenu, arg=pin))
|
||||
else:
|
||||
rv.append(MenuItem('↳'+pin, menu=self.pin_submenu, arg=pin))
|
||||
|
||||
|
||||
rv.append(MenuItem('Add New Trick', f=self.add_new))
|
||||
has_wrong = any(pin == WRONG_PIN_CODE for pin in tricks)
|
||||
if not has_wrong:
|
||||
rv.append(MenuItem('Add If Wrong', f=self.set_any_wrong))
|
||||
|
||||
# even if menu "looks" empty, many times we need this anyway
|
||||
rv.append(MenuItem('Delete All', f=self.clear_all))
|
||||
|
||||
return rv
|
||||
|
||||
def update_contents(self):
|
||||
tmp = self.construct()
|
||||
self.replace_items(tmp)
|
||||
|
||||
async def done_picking(self, item, parents):
|
||||
# done picking/drilling down tree.
|
||||
# - shows point-form summary and gets confirmation
|
||||
wants_wipe = (self.WillWipeMenu in parents)
|
||||
self.WillWipeMenu = None # memory free
|
||||
|
||||
flags = item.flags
|
||||
tc_arg = item.arg
|
||||
|
||||
if self.proposed_pin == WRONG_PIN_CODE:
|
||||
if tc_arg == 0:
|
||||
msg = "Any Wrong PIN\n↳%s" % item.label
|
||||
else:
|
||||
msg = "%d Wrong PINs\n↳%s" % (tc_arg, item.label)
|
||||
else:
|
||||
msg = "PIN %s\n↳%s" % (self.proposed_pin, item.label)
|
||||
|
||||
if wants_wipe:
|
||||
msg += " (after wiping secret)"
|
||||
flags |= TC_WIPE
|
||||
|
||||
msg += '\n\n'
|
||||
|
||||
path, new_secret = construct_duress_secret(flags, tc_arg)
|
||||
|
||||
|
||||
if path:
|
||||
msg += "Duress wallet will use path:\n\n%s\n\n" % path
|
||||
|
||||
if flags & TC_DELTA_MODE:
|
||||
# Calculate the value needed for args: BCD encoded final 4 digits
|
||||
# of the true PIN!
|
||||
prob, a = validate_delta_pin(self.current_pin, self.proposed_pin)
|
||||
if prob:
|
||||
await ux_show_story(prob, 'Sorry!')
|
||||
return
|
||||
tc_arg = a
|
||||
|
||||
msg += "Ok?"
|
||||
ch = await ux_show_story(msg)
|
||||
if ch != 'y': return
|
||||
|
||||
# save it
|
||||
try:
|
||||
bpin = self.proposed_pin.encode()
|
||||
b, slot = tp.update_slot(bpin, new=True, tc_flags=flags,
|
||||
tc_arg=tc_arg, secret=new_secret)
|
||||
await ux_dramatic_pause("Saved.", 1)
|
||||
except BaseException as exc:
|
||||
sys.print_exception(exc)
|
||||
await ux_show_story("Failed: %s" % exc)
|
||||
|
||||
self.update_contents()
|
||||
|
||||
|
||||
async def get_new_pin(self, existing_pin=None):
|
||||
# get a new PIN code and check not a dup
|
||||
# - show msg if aborted
|
||||
# - recover "forgotten" pins
|
||||
|
||||
from login import LoginUX
|
||||
lll = LoginUX()
|
||||
lll.is_setting = True
|
||||
lll.subtitle = "New Trick PIN"
|
||||
new_pin = await lll.prompt_pin()
|
||||
|
||||
if new_pin is None:
|
||||
return
|
||||
|
||||
if new_pin == existing_pin:
|
||||
await ux_show_story("That isn't a new value")
|
||||
return
|
||||
|
||||
have = tp.all_tricks()
|
||||
if existing_pin and (existing_pin in have):
|
||||
have.remove(existing_pin)
|
||||
|
||||
if (new_pin == self.current_pin) or (new_pin in have):
|
||||
await ux_show_story("That PIN (%s) is already in use. All PIN codes must be unique." % new_pin);
|
||||
return
|
||||
|
||||
# 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
|
||||
# to get here
|
||||
if tp.restore_pin(new_pin):
|
||||
await ux_show_story("Hmm. I remember that PIN now.")
|
||||
self.update_contents()
|
||||
return
|
||||
|
||||
return new_pin
|
||||
|
||||
async def add_new(self, *a):
|
||||
# Add a new PIN code
|
||||
from pincodes import pa
|
||||
from glob import settings
|
||||
|
||||
if pa.is_secret_blank() or pa.is_blank() or not pa.pin:
|
||||
await ux_show_story("Please set true PIN and wallet seed before creating trick pins.")
|
||||
return
|
||||
|
||||
# get the new pin
|
||||
self.proposed_pin = await self.get_new_pin()
|
||||
if not self.proposed_pin: return
|
||||
nwords = settings.get('words', 24)
|
||||
if nwords == 12:
|
||||
dbase = 2000
|
||||
else:
|
||||
# 24-word typical duress wallet
|
||||
# - cannot handle 18-word seeds exactly, so map to 24
|
||||
# - also XPRV -> duress word wallet will be 24-word type
|
||||
dbase = 1000
|
||||
|
||||
b85 = "This PIN will lead to a functional 'duress' wallet using seed words produced by the standard BIP-85 process. Index number is %d...%d for #1..#3 duress wallets. Same number of seed words as your true seed." \
|
||||
% (dbase+1, dbase+3)
|
||||
|
||||
DuressOptions = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
StoryMenuItem('BIP-85 Wallet #1', b85, arg=dbase+1, flags=TC_WORD_WALLET),
|
||||
StoryMenuItem('BIP-85 Wallet #2', b85, arg=dbase+2, flags=TC_WORD_WALLET),
|
||||
StoryMenuItem('BIP-85 Wallet #3', b85, arg=dbase+3, flags=TC_WORD_WALLET),
|
||||
StoryMenuItem('Legacy Wallet', "Uses duress wallet created on Mk3 Coldcard, using a fixed derivation.\n\nRecommended only for existing UTXO compatibility.", flags=TC_XPRV_WALLET),
|
||||
StoryMenuItem('Blank Coldcard', "Look and act like a freshly wiped Coldcard", flags=TC_BLANK_WALLET),
|
||||
]
|
||||
self.WillWipeMenu = MenuSystem([
|
||||
# xxxxxxxxxxxxxxxx
|
||||
StoryMenuItem('Wipe & Reboot', "Seed is wiped and Coldcard reboots without notice.",
|
||||
flags=TC_WIPE|TC_REBOOT),
|
||||
StoryMenuItem('Silent Wipe', "Seed is silently wiped and Coldcard acts as if PIN code was just wrong.",
|
||||
flags=TC_WIPE|TC_FAKE_OUT),
|
||||
StoryMenuItem('Wipe -> Wallet', "Seed is silently wiped, and Coldcard logs into a duress wallet. Select type of wallet on next menu.", menu=DuressOptions),
|
||||
StoryMenuItem('Say Wiped, Stop', "Seed is wiped and a message is shown.",
|
||||
flags=TC_WIPE),
|
||||
])
|
||||
from countdowns import lgto_map
|
||||
def_to = settings.get('lgto', 0) or 60 # use 1hour or current countdown length as default
|
||||
|
||||
countdown_menu = MenuSystem([
|
||||
# xxxxxxxxxxxxxxxx
|
||||
StoryMenuItem('Wipe & Countdown', "Seed is wiped at start of countdown.",
|
||||
flags=TC_WIPE|TC_COUNTDOWN, arg=def_to),
|
||||
StoryMenuItem('Countdown & Brick', "Does the countdown, then system is bricked.",
|
||||
flags=TC_WIPE|TC_BRICK|TC_COUNTDOWN, arg=def_to),
|
||||
StoryMenuItem('Just Countdown', "Shows countdown, has no effect on seed.",
|
||||
flags=TC_COUNTDOWN, arg=def_to),
|
||||
])
|
||||
FirstMenu = [
|
||||
#MenuItem('"%s" =>' % self.proposed_pin),
|
||||
MenuItem('[%s]' % self.proposed_pin),
|
||||
StoryMenuItem('Brick Self', "Become a brick instantly and forever.", flags=TC_BRICK),
|
||||
StoryMenuItem('Wipe Seed', "Wipe the seed and maybe do more. See next menu.",
|
||||
menu=self.WillWipeMenu),
|
||||
StoryMenuItem('Duress Wallet', "Goes directly to a specific duress wallet. No side effects.", menu=DuressOptions),
|
||||
StoryMenuItem('Login Countdown', "Pretends a login countdown timer (%s) is in effect but wipes seed first. Resets system at end of countdown or bricks it." % lgto_map[def_to].strip(),
|
||||
menu=countdown_menu),
|
||||
StoryMenuItem('Look Blank', "Look and act like a freshly- wiped Coldcard but don't affect actual seed.", flags=TC_BLANK_WALLET),
|
||||
StoryMenuItem('Just Reboot', "Reboot when this PIN is entered. Doesn't do anything else.", flags=TC_REBOOT),
|
||||
StoryMenuItem('Delta Mode', '''\
|
||||
Advanced! Logs into REAL seed and allows attacker to do most things, \
|
||||
but will produce incorrect signatures when signing PSBT files. \
|
||||
Wipes seed if they try to do certain actions that might reveal \
|
||||
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),
|
||||
]
|
||||
m = MenuSystem(FirstMenu)
|
||||
m.goto_idx(1)
|
||||
the_ux.push(m)
|
||||
|
||||
|
||||
async def set_any_wrong(self, *a):
|
||||
ch = await ux_show_story('''\
|
||||
After X incorrect PIN attempts, this feature will be triggered. It can wipe \
|
||||
the seed phrase, and/or brick the Coldcard. Regardless of this (or any other \
|
||||
setting) the Coldcard will always brick after 13 failed PIN attempts.''')
|
||||
if ch == 'x': return
|
||||
|
||||
self.proposed_pin = WRONG_PIN_CODE
|
||||
num = await ux_enter_number("#of wrong attempts", 12)
|
||||
if num is None: return
|
||||
|
||||
# - can't do countdown here because of only one tc_arg value per slot
|
||||
# - zero and one effectively the same
|
||||
if num == 0:
|
||||
num = 1
|
||||
|
||||
rel = ['', 'ANY', '2nd', '3rd'][num] if num <= 3 else ('%dth' % num)
|
||||
|
||||
m = MenuSystem([
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem('[%s WRONG PIN]' % rel),
|
||||
StoryMenuItem('Wipe, Stop', "Seed is wiped and a message is shown.",
|
||||
arg=num, flags=TC_WIPE),
|
||||
StoryMenuItem('Wipe & Reboot', "Seed is wiped and Coldcard reboots without notice.",
|
||||
arg=num, flags=TC_WIPE|TC_REBOOT),
|
||||
StoryMenuItem('Silent Wipe', "Seed is silently wiped and Coldcard acts as if PIN code was just wrong.",
|
||||
arg=num, flags=TC_WIPE|TC_FAKE_OUT),
|
||||
StoryMenuItem('Brick Self', "Become a brick instantly and forever.", flags=TC_BRICK),
|
||||
StoryMenuItem('Last Chance', "Wipe seed, then give one more try and then brick if wrong PIN.", arg=num, flags=TC_WIPE|TC_BRICK),
|
||||
StoryMenuItem('Look Blank', "Look and act like a freshly- wiped Coldcard but don't affect actual seed.", arg=num, flags=TC_BLANK_WALLET),
|
||||
StoryMenuItem('Just Reboot', "Reboot when this happens. Doesn't do anything else.", arg=num, flags=TC_REBOOT),
|
||||
])
|
||||
|
||||
m.goto_idx(1)
|
||||
the_ux.push(m)
|
||||
|
||||
async def clear_all(self, m,l,item):
|
||||
if not await ux_confirm("Remove ALL TRICK PIN codes and special wrong-pin handling?"):
|
||||
return
|
||||
|
||||
if any(tp.get_duress_pins()):
|
||||
if not await ux_confirm("Any funds on the duress wallet(s) have been moved already?"):
|
||||
return
|
||||
|
||||
tp.clear_all()
|
||||
m.update_contents()
|
||||
|
||||
async def hide_pin(self, m,l, item):
|
||||
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 \
|
||||
to attacker, and we need to update this record if the main PIN is changed, so we don't support \
|
||||
hiding this item.''')
|
||||
return
|
||||
|
||||
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
|
||||
|
||||
# just a settings change
|
||||
tp.forget_pin(pin)
|
||||
|
||||
self.pop_submenu()
|
||||
|
||||
def pop_submenu(self):
|
||||
the_ux.pop()
|
||||
m = the_ux.top_of_stack()
|
||||
m.update_contents()
|
||||
|
||||
async def change_pin(self, m,l, item):
|
||||
# Change existing PIN code.
|
||||
old_pin, slot_num, flags, tc_arg = item.arg
|
||||
|
||||
new_pin = await self.get_new_pin(old_pin)
|
||||
if new_pin is None:
|
||||
return
|
||||
|
||||
if flags & TC_DELTA_MODE:
|
||||
# if delta mode ... must apply rules to new PIN
|
||||
prob, a = validate_delta_pin(self.current_pin, new_pin)
|
||||
if prob:
|
||||
await ux_show_story(prob, 'Sorry!')
|
||||
return
|
||||
tc_arg = a
|
||||
|
||||
try:
|
||||
tp.update_slot(old_pin.encode(), new_pin=new_pin.encode(), tc_arg=tc_arg)
|
||||
await ux_dramatic_pause("Changed.", 1)
|
||||
|
||||
self.pop_submenu() # too lazy to get redraw right
|
||||
except BaseException as exc:
|
||||
sys.print_exception(exc)
|
||||
await ux_show_story("Failed: %s" % exc)
|
||||
|
||||
async def delete_pin(self, m,l, item):
|
||||
pin, slot_num, flags = item.arg
|
||||
|
||||
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 pin == WRONG_PIN_CODE:
|
||||
msg = "Remove special handling of wrong PINs?"
|
||||
else:
|
||||
msg = "Removing trick PIN:\n %s\n\nOk?" % pin
|
||||
|
||||
if not await ux_confirm(msg):
|
||||
return
|
||||
|
||||
if flags & TC_WORD_WALLET:
|
||||
nslots = 2
|
||||
elif flags & TC_XPRV_WALLET:
|
||||
nslots = 3
|
||||
else:
|
||||
nslots = 1
|
||||
|
||||
tp.clear_slots(range(slot_num, slot_num+nslots))
|
||||
tp.forget_pin(pin)
|
||||
|
||||
self.pop_submenu()
|
||||
|
||||
async def activate_wallet(self, m, l, item):
|
||||
# load the secrets of a wallet for immediate use
|
||||
# - duress or blank wallet
|
||||
pin, flags, arg = item.arg
|
||||
|
||||
ch = await ux_show_story('''\
|
||||
This will temporarily load the secrets associated with this trick wallet \
|
||||
so you may perform transactions with it. Reboot the Coldcard to restore \
|
||||
normal operation.''')
|
||||
if ch != 'y': return
|
||||
|
||||
from pincodes import pa, AE_SECRET_LEN
|
||||
b, slot = tp.get_by_pin(pin)
|
||||
assert slot
|
||||
|
||||
# TC_BLANK_WALLET here would be nice, but no support working w/ fake empty secret
|
||||
|
||||
# emulate stash.py encoding
|
||||
if flags & TC_XPRV_WALLET:
|
||||
encoded = b'\x01' + slot.xdata[0:64]
|
||||
elif flags & TC_WORD_WALLET and (arg // 1000 == 1):
|
||||
encoded = b'\x82' + slot.xdata[0:32]
|
||||
elif flags & TC_WORD_WALLET and (arg // 1000 == 2):
|
||||
encoded = b'\x80' + slot.xdata[0:16]
|
||||
else:
|
||||
raise ValueError('f=0x%x a=%d' % (flags, args))
|
||||
|
||||
from glob import dis
|
||||
|
||||
# switch over to new secret!
|
||||
dis.fullscreen("Applying...")
|
||||
pa.tmp_secret(encoded)
|
||||
tp.reload()
|
||||
|
||||
await ux_show_story("New master key in effect until next power down.")
|
||||
|
||||
from actions import goto_top_menu
|
||||
goto_top_menu()
|
||||
|
||||
async def countdown_details(self, m, l, item):
|
||||
# explain details of the countdown case
|
||||
# - allow change of time period
|
||||
from countdowns import lgto_map, lgto_va, lgto_ch
|
||||
from menu import start_chooser
|
||||
|
||||
pin, flags, arg = item.arg
|
||||
|
||||
# "arg" can be out-of-date, if they edited timer value after parent was
|
||||
# rendered, where arg was captured into item.arg ... so don't use it.
|
||||
cd_val = tp.tp[pin][2]
|
||||
|
||||
msg = 'Shows login countdown (%s)' % lgto_map.get(cd_val, '???').strip()
|
||||
if flags & TC_WIPE:
|
||||
msg += ', wipes the seed'
|
||||
else:
|
||||
msg += ' and reboots at end of countdown'
|
||||
if flags & TC_BRICK:
|
||||
msg += ' and bricks system at end of countdown'
|
||||
|
||||
msg += '.\n\nPress 4 to change time.'
|
||||
ch = await ux_show_story(msg, escape='4')
|
||||
if ch != '4': return
|
||||
|
||||
def adjust_countdown_chooser():
|
||||
# 'disabled' choice not appropriate for this case
|
||||
ch = lgto_ch[1:]
|
||||
va = lgto_va[1:]
|
||||
|
||||
def set_it(idx, text):
|
||||
new_val = va[idx+1]
|
||||
# save it
|
||||
try:
|
||||
b, slot = tp.update_slot(pin.encode(), tc_flags=flags, tc_arg=new_val)
|
||||
except BaseException as exc:
|
||||
sys.print_exception(exc)
|
||||
|
||||
return va.index(cd_val), lgto_ch[1:], set_it
|
||||
|
||||
start_chooser(adjust_countdown_chooser)
|
||||
|
||||
async def duress_details(self, m, l, item):
|
||||
# explain details of a duress wallet
|
||||
pin, flags, arg = item.arg
|
||||
if flags & TC_XPRV_WALLET:
|
||||
msg = '''The legacy duress wallet will be activated if '%s' is provded. \
|
||||
You probably created this on an older Mk2 or Mk3 Coldcard. \
|
||||
Wallet is XPRV-based and derived from a fixed path.''' % pin
|
||||
elif flags & TC_WORD_WALLET:
|
||||
nwords = 12 if (arg // 1000 == 2) else 24
|
||||
msg = '''BIP-85 derived wallet (%d words), with index #%d, is provided if '%s'.''' \
|
||||
% (nwords, arg, pin)
|
||||
else:
|
||||
raise ValueError(hex(flags))
|
||||
|
||||
ch = await ux_show_story(msg + '\n\nPress 6 to view associated secrets.', escape='6')
|
||||
if ch != '6': return
|
||||
|
||||
b, s = tp.get_by_pin(pin)
|
||||
if s == None:
|
||||
# could not find in SE2. Our settings vs. SE2 are not in sync.
|
||||
msg = "Not found in SE2. Delete and remake."
|
||||
else:
|
||||
from actions import render_master_secrets
|
||||
|
||||
assert s.tc_flags == flags
|
||||
if flags & TC_XPRV_WALLET:
|
||||
node = ngu.hdnode.HDNode()
|
||||
ch, pk = s.xdata[0:32], s.xdata[32:64]
|
||||
node.from_chaincode_privkey(ch, pk)
|
||||
|
||||
msg, *_ = render_master_secrets('xprv', None, node)
|
||||
elif flags & TC_WORD_WALLET:
|
||||
raw = s.xdata[0:(32 if nwords == 24 else 16)]
|
||||
msg, *_ = render_master_secrets('words', raw, None)
|
||||
else:
|
||||
raise ValueError(hex(flags))
|
||||
|
||||
await ux_show_story(msg, sensitive=True)
|
||||
|
||||
|
||||
async def pin_submenu(self, menu, label, item):
|
||||
# drill down into a sub-menu per existing PIN
|
||||
# - data display only, no editing; just clear and redo
|
||||
pin = item.arg
|
||||
slot_num, flags, arg = tp.tp[pin] if (pin in tp.tp) else (-1, 0, 0)
|
||||
|
||||
rv = []
|
||||
|
||||
if pin != WRONG_PIN_CODE:
|
||||
rv.append(MenuItem('PIN %s' % pin))
|
||||
else:
|
||||
rv.append(MenuItem("After %d wrong:" % arg))
|
||||
|
||||
if flags & (TC_WORD_WALLET | TC_XPRV_WALLET):
|
||||
rv.append(MenuItem("↳Duress Wallet", f=self.duress_details, arg=(pin, flags, arg)))
|
||||
elif flags & TC_BLANK_WALLET:
|
||||
rv.append(MenuItem("↳Blank Wallet"))
|
||||
elif flags & TC_COUNTDOWN:
|
||||
rv.append(MenuItem("↳Countdown", f=self.countdown_details, arg=(pin, flags, arg)))
|
||||
elif flags & TC_FAKE_OUT:
|
||||
rv.append(MenuItem("↳Pretends Wrong"))
|
||||
elif flags & TC_DELTA_MODE:
|
||||
rv.append(MenuItem("↳Delta Mode"))
|
||||
|
||||
for m, msg in [
|
||||
(TC_WIPE, '↳Wipes seed'),
|
||||
(TC_BRICK, '↳Bricks CC'),
|
||||
(TC_REBOOT, '↳Reboots'),
|
||||
]:
|
||||
if flags & m:
|
||||
rv.append(MenuItem(msg))
|
||||
|
||||
if flags & (TC_WORD_WALLET | TC_XPRV_WALLET):
|
||||
rv.append(MenuItem("Activate Wallet", f=self.activate_wallet, arg=(pin, flags, arg)))
|
||||
|
||||
rv.extend([
|
||||
MenuItem('Hide Trick', f=self.hide_pin, arg=(pin, slot_num, flags)),
|
||||
MenuItem('Delete Trick', f=self.delete_pin, arg=(pin, slot_num, flags)),
|
||||
])
|
||||
if pin != WRONG_PIN_CODE:
|
||||
rv.append(
|
||||
MenuItem('Change PIN', f=self.change_pin, arg=(pin, slot_num, flags, arg)),
|
||||
)
|
||||
|
||||
return rv
|
||||
|
||||
class StoryMenuItem(MenuItem):
|
||||
def __init__(self, label, story, flags=0, **kws):
|
||||
self.story = story
|
||||
self.flags = flags
|
||||
super().__init__(label, **kws)
|
||||
|
||||
async def activate(self, menu, idx):
|
||||
ch = await ux_show_story(self.story)
|
||||
if ch == 'x':
|
||||
return
|
||||
|
||||
if getattr(self, 'next_menu', None):
|
||||
# drill down more
|
||||
return await super().activate(menu, idx)
|
||||
|
||||
# pop some levels, and note the drill-down path that was used
|
||||
parents = []
|
||||
while 1:
|
||||
the_ux.pop()
|
||||
parent = the_ux.top_of_stack()
|
||||
assert parent
|
||||
|
||||
parents.insert(0, parent)
|
||||
|
||||
if isinstance(parent, TrickPinMenu):
|
||||
await parent.done_picking(self, parents)
|
||||
return
|
||||
|
||||
# EOF
|
||||
@ -5,16 +5,15 @@
|
||||
import ckcc, pyb, callgate, sys, ux, ngu, stash, aes256ctr
|
||||
from uasyncio import sleep_ms, core
|
||||
from uhashlib import sha256
|
||||
from public_constants import MAX_MSG_LEN, MAX_TXN_LEN, MAX_BLK_LEN, MAX_UPLOAD_LEN, AFC_SCRIPT
|
||||
from public_constants import MAX_MSG_LEN, MAX_BLK_LEN, AFC_SCRIPT
|
||||
from public_constants import STXN_FLAGS_MASK
|
||||
from ustruct import pack, unpack_from
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from ckcc import watchpoint, is_simulator
|
||||
import uselect as select
|
||||
from utils import problem_file_line, call_later_ms
|
||||
from version import has_fatram, is_devmode
|
||||
from version import has_fatram, is_devmode, has_psram, MAX_TXN_LEN, MAX_UPLOAD_LEN
|
||||
from exceptions import FramingError, CCBusyError, HSMDenied
|
||||
from nvstore import settings
|
||||
|
||||
# Unofficial, unpermissioned... numbers
|
||||
COINKITE_VID = 0xd13e
|
||||
@ -51,7 +50,7 @@ hid_descp = bytes([
|
||||
HSM_WHITELIST = frozenset({
|
||||
'logo', 'ping', 'vers', # harmless/boring
|
||||
'upld', 'sha2', 'dwld', 'stxn', # up/download/sign PSBT needed
|
||||
'mitm','ncry', # maybe limited by policy tho
|
||||
'mitm', 'ncry', # maybe limited by policy tho
|
||||
'smsg', # limited by policy
|
||||
'blkc', 'hsts', # report status values
|
||||
'stok', 'smok', # completion check: sign txn or msg
|
||||
@ -61,20 +60,20 @@ HSM_WHITELIST = frozenset({
|
||||
'gslr', # read storage locker; hsm mode only, limited usage
|
||||
})
|
||||
|
||||
|
||||
|
||||
# singleton instance of USBHandler()
|
||||
handler = None
|
||||
|
||||
def enable_usb():
|
||||
# We can't change it on the fly; must be disabled before here
|
||||
# - only one combo of subclasses can be used during a single power-up cycle
|
||||
cur = pyb.usb_mode()
|
||||
if cur:
|
||||
print("USB already enabled: %s" % cur)
|
||||
else:
|
||||
# subclass, protocol, max packet length, polling interval, report descriptor
|
||||
hid_info = (0x0, 0x0, 64, 5, hid_descp )
|
||||
pyb.usb_mode('VCP+HID', vid=COINKITE_VID, pid=CKCC_PID, hid=hid_info)
|
||||
classes = 'VCP+HID' if not has_psram else 'VCP+MSC+HID'
|
||||
pyb.usb_mode(classes, vid=COINKITE_VID, pid=CKCC_PID, hid=hid_info)
|
||||
|
||||
global handler
|
||||
if not handler:
|
||||
@ -105,7 +104,6 @@ class USBHandler:
|
||||
# handle simulator
|
||||
self.blockable = getattr(self.dev, 'pipe', self.dev)
|
||||
|
||||
#self.msg = bytearray(MAX_MSG_LEN)
|
||||
from sram2 import usb_buf
|
||||
self.msg = usb_buf
|
||||
assert len(self.msg) == MAX_MSG_LEN
|
||||
@ -120,6 +118,7 @@ class USBHandler:
|
||||
# read next packet (64 bytes) waiting on the wire. Unframe it and return
|
||||
# active part of packet, flags associated.
|
||||
buf = self.dev.recv(64, timeout=5000)
|
||||
ckcc.usb_active()
|
||||
|
||||
if not buf:
|
||||
raise FramingError('timeout')
|
||||
@ -282,6 +281,7 @@ class USBHandler:
|
||||
left -= here
|
||||
pos += here
|
||||
|
||||
ckcc.usb_active()
|
||||
for retries in range(100):
|
||||
chk = self.dev.send(msg)
|
||||
if chk == 64: break
|
||||
@ -306,7 +306,7 @@ class USBHandler:
|
||||
except:
|
||||
raise FramingError('decode')
|
||||
|
||||
if cmd[0].isupper() and (is_simulator() or is_devmode):
|
||||
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
|
||||
@ -511,6 +511,7 @@ class USBHandler:
|
||||
# bip39 passphrase provided, maybe use it if authorized
|
||||
assert self.encrypted_req, 'must encrypt'
|
||||
from auth import start_bip39_passphrase
|
||||
from glob import settings
|
||||
|
||||
assert settings.get('words', True), 'no seed'
|
||||
assert len(args) < 400, 'too long'
|
||||
@ -627,6 +628,7 @@ class USBHandler:
|
||||
self.encrypt = ctr.cipher
|
||||
self.decrypt = ctr.copy().cipher
|
||||
|
||||
from glob import settings
|
||||
xfp = settings.get('xfp', 0)
|
||||
xpub = settings.get('xpub', '')
|
||||
|
||||
@ -654,7 +656,6 @@ class USBHandler:
|
||||
async def handle_download(self, offset, length, file_number):
|
||||
# let them read from where we store the signed txn
|
||||
# - filenumber can be 0 or 1: uploaded txn, or result
|
||||
from sflash import SF
|
||||
|
||||
# limiting memory use here, should be MAX_BLK_LEN really
|
||||
length = min(length, MAX_BLK_LEN)
|
||||
@ -667,18 +668,28 @@ class USBHandler:
|
||||
if offset == 0:
|
||||
self.file_checksum = sha256()
|
||||
|
||||
pos = (MAX_TXN_LEN * file_number) + offset
|
||||
|
||||
resp = bytearray(4 + length)
|
||||
resp[0:4] = b'biny'
|
||||
SF.read(pos, memoryview(resp)[4:])
|
||||
buf = memoryview(resp)[4:]
|
||||
|
||||
self.file_checksum.update(memoryview(resp)[4:])
|
||||
pos = (MAX_TXN_LEN * file_number) + offset
|
||||
|
||||
if has_psram:
|
||||
from glob import PSRAM
|
||||
PSRAM.read(pos, buf)
|
||||
else:
|
||||
from sflash import SF
|
||||
SF.read(pos, buf)
|
||||
|
||||
self.file_checksum.update(buf)
|
||||
|
||||
return resp
|
||||
|
||||
async def handle_upload(self, offset, total_size, data):
|
||||
from sflash import SF
|
||||
if has_psram:
|
||||
from glob import PSRAM
|
||||
else:
|
||||
from sflash import SF
|
||||
from glob import dis, hsm_active
|
||||
from utils import check_firmware_hdr
|
||||
from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE, FW_HEADER_MAGIC
|
||||
@ -699,15 +710,16 @@ class USBHandler:
|
||||
|
||||
for pos in range(offset, offset+len(data), 256):
|
||||
if pos % 4096 == 0:
|
||||
# erase here
|
||||
dis.fullscreen("Receiving...", offset/total_size)
|
||||
|
||||
SF.sector_erase(pos)
|
||||
if not has_psram:
|
||||
# erase here
|
||||
SF.sector_erase(pos)
|
||||
|
||||
# expect 10-22 ms delay here
|
||||
await sleep_ms(12)
|
||||
while SF.is_busy():
|
||||
await sleep_ms(2)
|
||||
# expect 10-22 ms delay here
|
||||
await sleep_ms(12)
|
||||
while SF.is_busy():
|
||||
await sleep_ms(2)
|
||||
|
||||
# write up to 256 bytes
|
||||
here = data[pos-offset:pos-offset+256]
|
||||
@ -724,11 +736,10 @@ class USBHandler:
|
||||
hdr = memoryview(here)[-128:]
|
||||
magic, = unpack_from('<I', hdr[0:4])
|
||||
if magic == FW_HEADER_MAGIC:
|
||||
self.is_fw_upgrade = bytes(hdr)
|
||||
|
||||
prob = check_firmware_hdr(hdr, total_size)
|
||||
if prob:
|
||||
raise ValueError(prob)
|
||||
self.is_fw_upgrade = bytes(hdr)
|
||||
|
||||
if is_trailer and self.is_fw_upgrade:
|
||||
# expect the trailer to exactly match the original one
|
||||
@ -738,17 +749,20 @@ class USBHandler:
|
||||
|
||||
# but don't write it, instead offer user a chance to abort
|
||||
from auth import authorize_upgrade
|
||||
authorize_upgrade(self.is_fw_upgrade, pos)
|
||||
authorize_upgrade(self.is_fw_upgrade, pos, psram_offset=0)
|
||||
|
||||
# pretend we wrote it, so ckcc-protocol or whatever gives normal feedback
|
||||
return offset
|
||||
|
||||
SF.write(pos, here)
|
||||
|
||||
# full page write: 0.6 to 3ms
|
||||
while SF.is_busy():
|
||||
await sleep_ms(1)
|
||||
# write to SPI Flash / PSRAM
|
||||
if has_psram:
|
||||
PSRAM.write(pos, here)
|
||||
else:
|
||||
SF.write(pos, here)
|
||||
|
||||
# full page write: 0.6 to 3ms
|
||||
while SF.is_busy():
|
||||
await sleep_ms(1)
|
||||
|
||||
if offset+len(data) >= total_size and not hsm_active:
|
||||
# probably done
|
||||
@ -783,7 +797,7 @@ class USBHandler:
|
||||
|
||||
def handle_bag_number(self, bag_num):
|
||||
import version, callgate
|
||||
from glob import dis
|
||||
from glob import dis, settings
|
||||
from pincodes import pa
|
||||
|
||||
if version.is_factory_mode and bag_num:
|
||||
|
||||
@ -1,6 +1,16 @@
|
||||
import uio, sys, version, nvstore
|
||||
#from pincodes import pa
|
||||
from sflash import SF
|
||||
# items imported here may be useful to EVAL and EXEC commands, which test cases depend on.
|
||||
import uio, sys, version, nvstore, glob, callgate
|
||||
try:
|
||||
from sflash import SF
|
||||
except: pass
|
||||
try:
|
||||
import sim_display
|
||||
except: pass
|
||||
try:
|
||||
from pincodes import pa
|
||||
except: pass
|
||||
|
||||
from glob import *
|
||||
|
||||
def do_usb_command(cmd, args):
|
||||
# TESTING commands!
|
||||
@ -11,7 +11,7 @@ from public_constants import MAX_USERNAME_LEN, PBKDF2_ITER_COUNT
|
||||
from menu import MenuSystem, MenuItem
|
||||
from ucollections import namedtuple
|
||||
from ux import ux_dramatic_pause, ux_show_story, ux_confirm
|
||||
from nvstore import settings
|
||||
from glob import settings
|
||||
|
||||
# accepting strings and strings, returning bytes when decoding, str when encoding (ie. correct)
|
||||
b32encode = ngu.codecs.b32_encode
|
||||
@ -257,8 +257,6 @@ class UsersMenu(MenuSystem):
|
||||
@classmethod
|
||||
def construct(cls):
|
||||
# Dynamic menu with user-defined user names
|
||||
from actions import import_multisig
|
||||
|
||||
async def no_users_yet(*a):
|
||||
# action for 'no wallets yet' menu item
|
||||
await ux_show_story("You don't have any user accounts defined yet. USB is used to define new users, and their associated secrets.")
|
||||
@ -271,9 +269,6 @@ class UsersMenu(MenuSystem):
|
||||
for u in users:
|
||||
rv.append(MenuItem('"%s"' % u, menu=make_user_sub_menu, arg=u))
|
||||
|
||||
# other static items?
|
||||
#rv.append(MenuItem('', f=import_multisig))
|
||||
|
||||
return rv
|
||||
|
||||
def update_contents(self):
|
||||
|
||||
@ -128,7 +128,7 @@ class HexWriter:
|
||||
self.pos += len(b)//2
|
||||
return a2b_hex(b)
|
||||
|
||||
def read_into(self, buf):
|
||||
def readinto(self, buf):
|
||||
b = self.read(len(buf))
|
||||
buf[0:len(b)] = b
|
||||
return len(b)
|
||||
@ -336,7 +336,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
|
||||
from sigheader import MK_1_OK, MK_2_OK, MK_3_OK, MK_4_OK
|
||||
from ustruct import unpack_from
|
||||
from version import hw_label
|
||||
import callgate
|
||||
@ -363,9 +363,11 @@ def check_firmware_hdr(hdr, binary_size):
|
||||
ok = (hw_compat & MK_2_OK)
|
||||
elif hw_label == 'mk3':
|
||||
ok = (hw_compat & MK_3_OK)
|
||||
elif hw_label == 'mk4':
|
||||
ok = (hw_compat & MK_4_OK)
|
||||
|
||||
if not ok:
|
||||
return "New firmware doesn't support this version of Coldcard hardware (%s)."%hw_label
|
||||
return "That firmware doesn't support this version of Coldcard hardware (%s)."%hw_label
|
||||
|
||||
water = callgate.get_highwater()
|
||||
if water[0] and timestamp < water:
|
||||
@ -376,11 +378,27 @@ def check_firmware_hdr(hdr, binary_size):
|
||||
|
||||
def clean_shutdown(style=0):
|
||||
# wipe SPI flash and shutdown (wiping main memory)
|
||||
import callgate
|
||||
from sflash import SF
|
||||
# - mk4: SPI flash not used, but NFC may hold data (PSRAM cleared by bootrom)
|
||||
# - bootrom wipes every byte of SRAM, so no need to repeat here
|
||||
import callgate, version, uasyncio
|
||||
|
||||
# save if anything pending
|
||||
from glob import settings
|
||||
settings.save_if_dirty()
|
||||
|
||||
try:
|
||||
SF.wipe_most()
|
||||
from glob import dis
|
||||
dis.fullscreen("Cleanup...")
|
||||
|
||||
if not version.has_psram:
|
||||
from sflash import SF
|
||||
SF.wipe_most()
|
||||
else:
|
||||
from glob import NFC
|
||||
|
||||
if NFC:
|
||||
uasyncio.run(NFC.wipe(True))
|
||||
|
||||
except: pass
|
||||
|
||||
callgate.show_logout(style)
|
||||
|
||||
51
shared/ux.py
51
shared/ux.py
@ -95,27 +95,20 @@ async def ux_wait_keyup(expected=None):
|
||||
|
||||
armed = ch
|
||||
|
||||
def ux_poll_once(expected='x'):
|
||||
# non-blocking check if key is pressed
|
||||
# - ignore and suppress any key not in expected
|
||||
def ux_poll_key():
|
||||
# non-blocking check if any key is pressed
|
||||
# - responds to key down only
|
||||
# - eats any existing key presses
|
||||
from glob import numpad
|
||||
|
||||
while 1:
|
||||
try:
|
||||
ch = numpad.key_pressed
|
||||
while not ch:
|
||||
ch = numpad.get_nowait()
|
||||
try:
|
||||
ch = numpad.get_nowait()
|
||||
|
||||
if ch == numpad.ABORT_KEY:
|
||||
raise AbortInteraction()
|
||||
except QueueEmpty:
|
||||
return None
|
||||
if ch == numpad.ABORT_KEY:
|
||||
raise AbortInteraction()
|
||||
except QueueEmpty:
|
||||
return None
|
||||
|
||||
for c in ch:
|
||||
if c in expected:
|
||||
return c
|
||||
return ch
|
||||
|
||||
class PressRelease:
|
||||
def __init__(self, need_release='xy'):
|
||||
@ -217,9 +210,6 @@ async def ux_show_story(msg, title=None, escape=None, sensitive=False, strict_es
|
||||
|
||||
lines.append('EOT')
|
||||
|
||||
#print("story:\n\n\"" + '"\n"'.join(lines))
|
||||
#lines[0] = '111111111121234567893'
|
||||
|
||||
top = 0
|
||||
H = 5
|
||||
ch = None
|
||||
@ -269,23 +259,22 @@ async def ux_show_story(msg, title=None, escape=None, sensitive=False, strict_es
|
||||
|
||||
async def idle_logout():
|
||||
import glob
|
||||
from nvstore import settings
|
||||
from glob import settings
|
||||
|
||||
while not glob.hsm_active:
|
||||
await sleep_ms(250)
|
||||
|
||||
# they may have changed setting recently
|
||||
timeout = settings.get('idle_to', DEFAULT_IDLE_TIMEOUT)*1000 # ms
|
||||
if timeout == 0:
|
||||
continue
|
||||
|
||||
now = utime.ticks_ms()
|
||||
await sleep_ms(5000)
|
||||
|
||||
if not glob.numpad.last_event_time:
|
||||
continue
|
||||
|
||||
if now > glob.numpad.last_event_time + timeout:
|
||||
# do a logout now.
|
||||
now = utime.ticks_ms()
|
||||
dt = utime.ticks_diff(now, glob.numpad.last_event_time)
|
||||
|
||||
# they may have changed setting recently
|
||||
timeout = settings.get('idle_to', DEFAULT_IDLE_TIMEOUT)*1000 # ms
|
||||
|
||||
if timeout and dt > timeout:
|
||||
# user has been idle for too long: do a logout
|
||||
print("Idle!")
|
||||
|
||||
from actions import logout_now
|
||||
@ -425,6 +414,6 @@ async def ux_enter_number(prompt, max_value):
|
||||
value += ch
|
||||
|
||||
# cleanup leading zeros and such
|
||||
value = str(int(value))
|
||||
value = str(min(int(value), max_value))
|
||||
|
||||
# EOF
|
||||
|
||||
219
shared/vdisk.py
Normal file
219
shared/vdisk.py
Normal file
@ -0,0 +1,219 @@
|
||||
# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
# vdisk.py - Share a virtual RAM disk with a USB host.
|
||||
#
|
||||
#
|
||||
import os, sys, pyb, ckcc, version, glob, uasyncio, utime
|
||||
from sigheader import FW_MIN_LENGTH
|
||||
from version import MAX_UPLOAD_LEN, is_devmode
|
||||
from usb import enable_usb, disable_usb
|
||||
from uasyncio import sleep_ms
|
||||
|
||||
MIN_QUIET_TIME = 250 # (ms) delay after host writes disk, before we look at it.
|
||||
|
||||
def _host_done_cb(_psram):
|
||||
# get back into the singleton
|
||||
assert glob.VD
|
||||
if glob.VD:
|
||||
glob.VD.host_done_handler()
|
||||
|
||||
# singleton: block device implemented on half of the PSRAM
|
||||
VBLKDEV = ckcc.PSRAM()
|
||||
|
||||
class VirtDisk:
|
||||
def __init__(self):
|
||||
# Feature is enabled, altho USB might be off.
|
||||
glob.VD = self
|
||||
|
||||
self.ignore = set()
|
||||
self.contents = self.sample()
|
||||
|
||||
assert ckcc.PSRAM
|
||||
VBLKDEV.callback(_host_done_cb)
|
||||
VBLKDEV.set_inserted(True)
|
||||
|
||||
def shutdown(self):
|
||||
# we've been disabled, stop
|
||||
VBLKDEV.set_inserted(False)
|
||||
VBLKDEV.callback(None)
|
||||
glob.VD = None
|
||||
|
||||
def unmount(self, written_files):
|
||||
# just unmount; ignore errors
|
||||
try:
|
||||
os.umount('/vdisk')
|
||||
except:
|
||||
pass
|
||||
|
||||
# ignore the files we write ourselves
|
||||
for fn in written_files:
|
||||
if fn.startswith('/vdisk/'):
|
||||
self.ignore.add(fn)
|
||||
|
||||
# allow host to change again
|
||||
enable_usb()
|
||||
if glob.VD:
|
||||
VBLKDEV.set_inserted(True)
|
||||
|
||||
def mount(self, readonly=False):
|
||||
# Prepare to read the filesystem. Block host. Return mount pt.
|
||||
for _ in range(10):
|
||||
# wait until it's been idle for a little bit
|
||||
host = VBLKDEV.get_time()
|
||||
if utime.ticks_diff(utime.ticks_ms(), host) > MIN_QUIET_TIME:
|
||||
break
|
||||
utime.sleep_ms(MIN_QUIET_TIME//5)
|
||||
else:
|
||||
print("busy disk?")
|
||||
|
||||
try:
|
||||
if not readonly:
|
||||
disable_usb()
|
||||
VBLKDEV.set_inserted(False)
|
||||
os.mount(VBLKDEV, '/vdisk', readonly=readonly)
|
||||
st = os.statvfs('/vdisk')
|
||||
|
||||
return '/vdisk'
|
||||
except OSError as exc:
|
||||
# corrupt or unformated?
|
||||
# XXX incomplete error handling here; needs work
|
||||
VBLKDEV.set_inserted(True)
|
||||
sys.print_exception(exc)
|
||||
|
||||
return None
|
||||
|
||||
def sample(self):
|
||||
# Peek at the contents of the disk right now
|
||||
# - only root directory
|
||||
# - only files, and capture their sizes
|
||||
try:
|
||||
os.mount(VBLKDEV, '/vdisk', readonly=True)
|
||||
|
||||
return list(sorted(('/vdisk/'+fn, sz) for (fn,ty,_,sz) in os.ilistdir('/vdisk')
|
||||
if ty == 0x8000))
|
||||
except BaseException as exc:
|
||||
sys.print_exception(exc)
|
||||
|
||||
return []
|
||||
finally:
|
||||
os.umount('/vdisk')
|
||||
|
||||
def import_file(self, filename, sz):
|
||||
# copy file into another area of PSRAM where rest of system can use it
|
||||
assert sz <= MAX_UPLOAD_LEN # too big
|
||||
|
||||
# I could not resist doing this in C... since we already have the
|
||||
# data in memory, why mess around with file concepts?
|
||||
actual = VBLKDEV.copy_file(0, filename.split('/')[-1])
|
||||
|
||||
assert actual == sz
|
||||
|
||||
return actual
|
||||
|
||||
def new_psbt(self, filename, sz):
|
||||
# New incoming PSBT has been detected, start to sign it.
|
||||
from auth import sign_psbt_file
|
||||
uasyncio.create_task(sign_psbt_file(filename, force_vdisk=True))
|
||||
|
||||
def new_firmware(self, filename, sz):
|
||||
# potential new firmware file detected
|
||||
# - copy to start of PSRAM, begin upgrade confirm
|
||||
self.import_file(filename, sz)
|
||||
uasyncio.create_task(psram_upgrade(filename, sz))
|
||||
|
||||
def host_done_handler(self):
|
||||
from glob import settings
|
||||
|
||||
if settings.get('vdsk', 0) != 2:
|
||||
# auto mode not enabled, so ignore changes
|
||||
return
|
||||
|
||||
now = self.sample()
|
||||
if now == self.contents:
|
||||
# no-op change, common, ignore
|
||||
# - timestamp changes, hidden files, MacOS BS, etc.
|
||||
return
|
||||
|
||||
# clear ignored items once they are deleted
|
||||
self.ignore.intersection_update(fn for fn,_ in now)
|
||||
|
||||
self.contents = now
|
||||
|
||||
# Look for files we want to taste; assume they have
|
||||
# been fully written-out because we are called after a
|
||||
# fairly long timeout
|
||||
for fn, sz in now:
|
||||
|
||||
if fn in self.ignore:
|
||||
continue
|
||||
|
||||
if fn[0] == '.' or not sz:
|
||||
continue
|
||||
|
||||
if sz > MAX_UPLOAD_LEN: # == MAX_UPLOAD_LEN_MK4, see version.py
|
||||
#print("%s: too big" % fn)
|
||||
continue
|
||||
|
||||
lfn = fn.lower()
|
||||
|
||||
if lfn.endswith('.psbt') and sz > 100:
|
||||
self.ignore.add(fn)
|
||||
self.new_psbt(fn, sz)
|
||||
break
|
||||
|
||||
if lfn.endswith('.dfu') and sz > FW_MIN_LENGTH:
|
||||
self.ignore.add(fn) # in case they decline it
|
||||
self.new_firmware(fn, sz)
|
||||
break
|
||||
|
||||
async def wipe_disk(self):
|
||||
# Reformat. Near instant.
|
||||
from glob import dis
|
||||
from mk4 import make_psram_fs
|
||||
|
||||
dis.fullscreen('Formatting...')
|
||||
dis.progress_bar_show(0.1)
|
||||
|
||||
disable_usb()
|
||||
VBLKDEV.wipe()
|
||||
make_psram_fs()
|
||||
enable_usb()
|
||||
|
||||
await sleep_ms(50)
|
||||
dis.progress_bar_show(1)
|
||||
await sleep_ms(250)
|
||||
|
||||
|
||||
async def psram_upgrade(filename, size):
|
||||
# Upgrade to firmware image already in PSRAM at offset zero.
|
||||
from glob import dis, PSRAM
|
||||
from files import dfu_parse
|
||||
from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE
|
||||
from ux import ux_show_story, the_ux
|
||||
from sffile import SFFile
|
||||
|
||||
if PSRAM.read_at(0, 3) == b'Dfu':
|
||||
with SFFile(0, size) as fp:
|
||||
offset, size = dfu_parse(fp)
|
||||
else:
|
||||
# handle raw binary file
|
||||
offset = 0
|
||||
|
||||
# pull out firmware header
|
||||
hdr = PSRAM.read_at(offset+FW_HEADER_OFFSET, FW_HEADER_SIZE)
|
||||
|
||||
if filename == '/vdisk/dev.dfu' and is_devmode:
|
||||
# skip the checking and display for us devs and "just do it"
|
||||
# - the bootrom still does the checks, you just can't see useful errors
|
||||
from pincodes import pa
|
||||
assert pa.is_successful()
|
||||
print("dev.dfu being installed")
|
||||
pa.firmware_upgrade(offset, size)
|
||||
return
|
||||
|
||||
# get user buy-in and approval of the change.
|
||||
from auth import authorize_upgrade
|
||||
authorize_upgrade(hdr, size, hdr_check=True, psram_offset=offset)
|
||||
|
||||
|
||||
# EOF
|
||||
@ -4,6 +4,7 @@
|
||||
#
|
||||
# REMINDER: update simulator version of this file if API changes are made.
|
||||
#
|
||||
from public_constants import MAX_TXN_LEN, MAX_UPLOAD_LEN
|
||||
|
||||
def decode_firmware_header(hdr):
|
||||
from sigheader import FWH_PY_FORMAT
|
||||
@ -18,17 +19,22 @@ def decode_firmware_header(hdr):
|
||||
|
||||
return date, vers, ''.join(parts[:-2])
|
||||
|
||||
def get_fw_header():
|
||||
# located in our own flash
|
||||
from sigheader import FLASH_HEADER_BASE, FLASH_HEADER_BASE_MK4, FW_HEADER_SIZE
|
||||
import uctypes
|
||||
|
||||
global mk_num
|
||||
|
||||
return uctypes.bytes_at(FLASH_HEADER_BASE_MK4 if mk_num == 4 else FLASH_HEADER_BASE,
|
||||
FW_HEADER_SIZE)
|
||||
|
||||
def get_mpy_version():
|
||||
# read my own file header
|
||||
# see stm32/bootloader/sigheader.h
|
||||
|
||||
try:
|
||||
# located in flash, but could also use RAM version
|
||||
from sigheader import FLASH_HEADER_BASE, FW_HEADER_SIZE
|
||||
import uctypes
|
||||
|
||||
hdr = uctypes.bytes_at(FLASH_HEADER_BASE, FW_HEADER_SIZE)
|
||||
|
||||
hdr = get_fw_header()
|
||||
return decode_firmware_header(hdr)
|
||||
except:
|
||||
# this is early in boot process, so don't fail!
|
||||
@ -36,17 +42,28 @@ def get_mpy_version():
|
||||
|
||||
def get_header_value(fld_name):
|
||||
# get a single value, raw, from header; based on field name
|
||||
from sigheader import FLASH_HEADER_BASE, FW_HEADER_SIZE, FWH_PY_FORMAT, FWH_PY_VALUES
|
||||
import ustruct, uctypes
|
||||
from sigheader import FWH_PY_FORMAT, FWH_PY_VALUES
|
||||
import ustruct
|
||||
|
||||
idx = FWH_PY_VALUES.split().index(fld_name)
|
||||
|
||||
hdr = uctypes.bytes_at(FLASH_HEADER_BASE, FW_HEADER_SIZE)
|
||||
hdr = get_fw_header()
|
||||
|
||||
return ustruct.unpack_from(FWH_PY_FORMAT, hdr)[idx]
|
||||
|
||||
def nfc_presence_check():
|
||||
# Does NFC hardware exist on this board?
|
||||
# SDA/SCL will be tied low
|
||||
from machine import Pin
|
||||
return Pin('NFC_SDA', mode=Pin.IN).value() or Pin('NFC_SCL', mode=Pin.IN).value()
|
||||
|
||||
def get_is_devmode():
|
||||
# what firmware signing key did we boot with? are we in dev mode?
|
||||
|
||||
if mk_num == 4:
|
||||
# mk4: are we built differently?
|
||||
import ckcc
|
||||
return ckcc.is_debug_build()
|
||||
|
||||
from sigheader import RAM_HEADER_BASE, FWH_PK_NUM_OFFSET
|
||||
import stm
|
||||
|
||||
@ -61,6 +78,10 @@ def get_is_devmode():
|
||||
|
||||
|
||||
def is_fresh_version():
|
||||
# Did we just boot into a new firmware for the first time?
|
||||
# - mk4+ does not use this approach, light will be solid green during upgrade
|
||||
if mk_num >= 4: return False
|
||||
|
||||
from sigheader import RAM_BOOT_FLAGS, RBF_FRESH_VERSION
|
||||
import stm
|
||||
|
||||
@ -81,7 +102,9 @@ def serial_number():
|
||||
|
||||
def probe_system():
|
||||
# run-once code to determine what hardware we are running on
|
||||
global hw_label, has_608, has_fatram, is_factory_mode, is_devmode
|
||||
global hw_label, has_608, has_fatram, is_factory_mode, is_devmode, has_psram
|
||||
global has_se2, mk_num, has_nfc
|
||||
global MAX_UPLOAD_LEN, MAX_TXN_LEN
|
||||
|
||||
from sigheader import RAM_BOOT_FLAGS, RBF_FACTORY_MODE
|
||||
import ckcc, callgate, stm
|
||||
@ -90,24 +113,39 @@ def probe_system():
|
||||
# NOTE: mk1 not supported anymore.
|
||||
# PA10 is pulled-down in Mark2, open in previous revs
|
||||
#mark2 = (Pin('MARK2', Pin.IN, pull=Pin.PULL_UP).value() == 0)
|
||||
#
|
||||
#if not mark2:
|
||||
# has_membrane = False
|
||||
# hw_label = 'mk1'
|
||||
|
||||
hw_label = 'mk2'
|
||||
has_fatram = False
|
||||
has_psram = False
|
||||
has_608 = True
|
||||
has_se2 = False
|
||||
has_nfc = False # hardware present; they might not be using it
|
||||
mk_num = 2
|
||||
|
||||
has_fatram = ckcc.is_stm32l496()
|
||||
|
||||
if has_fatram:
|
||||
cpuid = ckcc.get_cpu_id()
|
||||
if cpuid == 0x461: # STM32L496RG6
|
||||
hw_label = 'mk3'
|
||||
|
||||
has_608 = callgate.has_608()
|
||||
has_fatram = True
|
||||
mk_num = 3
|
||||
elif cpuid == 0x470: # STM32L4S5VI
|
||||
hw_label = 'mk4'
|
||||
has_fatram = True
|
||||
has_psram = True
|
||||
has_se2 = True
|
||||
mk_num = 4
|
||||
has_nfc = nfc_presence_check()
|
||||
else:
|
||||
# mark 2
|
||||
has_608 = callgate.has_608()
|
||||
|
||||
# Boot loader needs to tell us stuff about how we were booted, sometimes:
|
||||
# - did we just install a new version, for example
|
||||
# - did we just install a new version, for example (obsolete in mk4)
|
||||
# - are we running in "factory mode" with flash un-secured?
|
||||
is_factory_mode = bool(stm.mem32[RAM_BOOT_FLAGS] & RBF_FACTORY_MODE)
|
||||
if mk_num < 4:
|
||||
is_factory_mode = bool(stm.mem32[RAM_BOOT_FLAGS] & RBF_FACTORY_MODE)
|
||||
else:
|
||||
is_factory_mode = callgate.get_factory_mode()
|
||||
|
||||
bn = callgate.get_bag_number()
|
||||
if bn:
|
||||
# this path supports testing/dev with RDP!=2, which normal production bootroms enforce
|
||||
@ -116,6 +154,12 @@ def probe_system():
|
||||
# what firmware signing key did we boot with? are we in dev mode?
|
||||
is_devmode = get_is_devmode()
|
||||
|
||||
# increase size limits for mk4
|
||||
if has_psram:
|
||||
from public_constants import MAX_TXN_LEN_MK4, MAX_UPLOAD_LEN_MK4
|
||||
MAX_UPLOAD_LEN = MAX_UPLOAD_LEN_MK4
|
||||
MAX_TXN_LEN = MAX_TXN_LEN_MK4
|
||||
|
||||
probe_system()
|
||||
|
||||
# EOF
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
import stash, ngu, chains, bip39, random
|
||||
from ux import ux_show_story, ux_enter_number, the_ux, ux_confirm, ux_dramatic_pause
|
||||
from seed import word_quiz, WordNestMenu, set_seed_value
|
||||
from nvstore import settings
|
||||
from glob import settings
|
||||
from actions import goto_top_menu
|
||||
|
||||
def xor32(*args):
|
||||
@ -165,7 +165,7 @@ class XORWordNestMenu(WordNestMenu):
|
||||
set_seed_value(encoded=enc)
|
||||
|
||||
# update menu contents now that wallet defined
|
||||
goto_top_menu()
|
||||
goto_top_menu(first_time=True)
|
||||
else:
|
||||
pa.tmp_secret(enc)
|
||||
await ux_show_story("New master key in effect until next power down.")
|
||||
@ -209,7 +209,7 @@ any time and you will have a valid wallet.''')
|
||||
|
||||
if not pa.is_secret_blank():
|
||||
msg = "Since you have a seed already on this Coldcard, the reconstructed XOR seed will be temporary and not saved. Wipe the seed first if you want to commit the new value into the secure element."
|
||||
if settings.get('words', True):
|
||||
if settings.get('words', 24) == 24:
|
||||
msg += '''\n
|
||||
Press (1) to include this Coldcard's seed words into the XOR seed set, or OK to continue without.'''
|
||||
|
||||
@ -221,7 +221,8 @@ Press (1) to include this Coldcard's seed words into the XOR seed set, or OK to
|
||||
with stash.SensitiveValues() as sv:
|
||||
if sv.mode == 'words':
|
||||
words = bip39.b2a_words(sv.raw).split(' ')
|
||||
import_xor_parts.append(words)
|
||||
if len(words) == 24:
|
||||
import_xor_parts.append(words)
|
||||
|
||||
return XORWordNestMenu(num_words=24)
|
||||
|
||||
|
||||
@ -2,9 +2,6 @@
|
||||
#
|
||||
# cmdline: ./build.py build
|
||||
#
|
||||
# special chars (if present):
|
||||
# √ ± µ ×
|
||||
#
|
||||
# 'assets/zevv-peep-iso8859-15-07x14.bdf' => FontSmall
|
||||
# 'assets/zevv-peep-iso8859-15-10x20.bdf' => FontLarge
|
||||
# 'assets/4x6.bdf' => FontTiny
|
||||
@ -23,36 +20,37 @@ class FontBase:
|
||||
if cp not in r: continue
|
||||
ptr = d[cp-r.start]
|
||||
if not ptr: return None
|
||||
|
||||
|
||||
x,y, w,h, dlen = cls._bboxes[cls._bitmaps[ptr]]
|
||||
bits = cls._bitmaps[ptr+1:ptr+1+dlen]
|
||||
|
||||
|
||||
return GlyphInfo(x,y, w,h, bits)
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class FontSmall(FontBase):
|
||||
height = 14
|
||||
code_range = range(32, 215)
|
||||
code_range = range(32, 8627)
|
||||
|
||||
|
||||
_bboxes = [None, (0, -3, 7, 14, 0), (0, -3, 7, 14, 4), (0, -3,
|
||||
7, 14, 5), (0, -3, 7, 14, 7), (0, -3, 7, 14, 9), (0, -3, 7, 14, 10),
|
||||
_bboxes = [None, (0, -3, 7, 14, 0), (0, -3, 7, 14, 4), (0, -3, 7,
|
||||
14, 5), (0, -3, 7, 14, 7), (0, -3, 7, 14, 9), (0, -3, 7, 14, 10),
|
||||
(0, -3, 7, 14, 11), (0, -3, 7, 14, 12), (0, -3, 7, 14, 13), (0, -3,
|
||||
7, 14, 14)]
|
||||
7, 14, 14), (0, 0, 8, 8, 8), (0, 0, 11, 9, 18), (0, 0, 14, 10, 20)]
|
||||
|
||||
_code_points = [
|
||||
(range(32, 127), [1, 2, 14, 20, 31, 43, 55, 67, 73, 87, 101, 111, 122,
|
||||
136, 144, 157, 170, 182, 194, 206, 218, 230, 242, 254, 266, 278,
|
||||
290, 303, 317, 329, 339, 351, 363, 375, 387, 399, 411, 423, 435,
|
||||
447, 459, 471, 483, 495, 507, 519, 531, 543, 555, 567, 580, 592,
|
||||
604, 616, 628, 640, 652, 664, 676, 688, 702, 715, 729, 734, 748,
|
||||
754, 766, 778, 790, 802, 814, 826, 841, 853, 865, 879, 891, 903,
|
||||
915, 927, 939, 953, 967, 979, 991, 1003, 1015, 1027, 1039, 1051,
|
||||
1066, 1078, 1092, 1105, 1119]),
|
||||
(range(177, 182), [1125, 0, 0, 0, 1138]), # ± µ
|
||||
(range(215, 216), [1151]), # ×
|
||||
136, 145, 158, 171, 183, 195, 207, 219, 231, 243, 255, 267, 279,
|
||||
291, 304, 318, 330, 340, 352, 364, 376, 388, 400, 412, 424, 436,
|
||||
448, 460, 472, 484, 496, 508, 520, 532, 544, 556, 568, 581, 593,
|
||||
605, 617, 629, 641, 653, 665, 677, 689, 703, 716, 730, 735, 749,
|
||||
755, 767, 779, 791, 803, 815, 827, 842, 854, 866, 880, 892, 904,
|
||||
916, 928, 940, 954, 968, 980, 992, 1004, 1016, 1028, 1040, 1052,
|
||||
1067, 1079, 1093, 1106, 1120]),
|
||||
(range(8226, 8227), [1126]), # •
|
||||
(range(8592, 8593), [1145]), # ←
|
||||
(range(8594, 8595), [1166]), # →
|
||||
(range(8627, 8628), [1187]), # ↳
|
||||
]
|
||||
|
||||
_bitmaps = b"""\
|
||||
@ -63,76 +61,79 @@ class FontSmall(FontBase):
|
||||
\x10\x09\x04\x08\x10\x10\x20\x20\x20\x20\x20\x10\x10\x08\x04\x09\x20\x10\
|
||||
\x08\x08\x04\x04\x04\x04\x04\x08\x08\x10\x20\x05\x00\x00\x00\x00\x24\x18\
|
||||
\x7e\x18\x24\x06\x00\x00\x00\x08\x08\x08\x3e\x08\x08\x08\x09\x00\x00\x00\
|
||||
\x00\x00\x00\x00\x00\x00\x18\x30\x20\x40\x04\x00\x00\x00\x00\x00\x00\x7e\
|
||||
\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x38\x10\x08\x02\x02\x04\x04\
|
||||
\x08\x08\x10\x10\x20\x20\x40\x40\x07\x00\x18\x24\x42\x42\x4a\x52\x42\x42\
|
||||
\x24\x18\x07\x00\x08\x18\x28\x48\x08\x08\x08\x08\x08\x08\x07\x00\x3c\x42\
|
||||
\x42\x02\x04\x08\x10\x20\x40\x7e\x07\x00\x3c\x42\x02\x02\x1c\x02\x02\x42\
|
||||
\x42\x3c\x07\x00\x0c\x14\x14\x24\x24\x44\x7e\x04\x04\x04\x07\x00\x7e\x40\
|
||||
\x40\x7c\x42\x02\x02\x02\x42\x3c\x07\x00\x3c\x42\x40\x40\x5c\x62\x42\x42\
|
||||
\x42\x3c\x07\x00\x7e\x02\x04\x04\x08\x08\x10\x10\x20\x20\x07\x00\x3c\x42\
|
||||
\x42\x42\x3c\x42\x42\x42\x42\x3c\x07\x00\x3c\x42\x42\x42\x46\x3a\x02\x02\
|
||||
\x42\x3c\x08\x00\x00\x00\x10\x38\x10\x00\x00\x00\x10\x38\x10\x09\x00\x00\
|
||||
\x00\x10\x38\x10\x00\x00\x00\x18\x30\x20\x40\x07\x00\x00\x04\x08\x10\x20\
|
||||
\x40\x20\x10\x08\x04\x05\x00\x00\x00\x00\x00\x7e\x00\x00\x7e\x07\x00\x00\
|
||||
\x20\x10\x08\x04\x02\x04\x08\x10\x20\x07\x00\x78\x04\x04\x08\x10\x20\x20\
|
||||
\x00\x20\x20\x07\x00\x1c\x22\x42\x4e\x52\x52\x52\x4e\x20\x1c\x07\x00\x3c\
|
||||
\x42\x42\x42\x42\x7e\x42\x42\x42\x42\x07\x00\x78\x44\x44\x44\x7c\x42\x42\
|
||||
\x42\x42\x7c\x07\x00\x3c\x42\x42\x40\x40\x40\x40\x42\x42\x3c\x07\x00\x78\
|
||||
\x44\x42\x42\x42\x42\x42\x42\x44\x78\x07\x00\x7e\x40\x40\x40\x7c\x40\x40\
|
||||
\x40\x40\x7e\x07\x00\x7e\x40\x40\x40\x7c\x40\x40\x40\x40\x40\x07\x00\x3c\
|
||||
\x42\x42\x40\x40\x4e\x42\x42\x42\x3c\x07\x00\x42\x42\x42\x42\x7e\x42\x42\
|
||||
\x42\x42\x42\x07\x00\x7c\x10\x10\x10\x10\x10\x10\x10\x10\x7c\x07\x00\x1e\
|
||||
\x02\x02\x02\x02\x02\x02\x42\x42\x3c\x07\x00\x42\x44\x48\x50\x68\x48\x44\
|
||||
\x44\x42\x42\x07\x00\x40\x40\x40\x40\x40\x40\x40\x40\x40\x7e\x07\x00\x42\
|
||||
\x66\x66\x5a\x5a\x5a\x42\x42\x42\x42\x07\x00\x42\x62\x62\x52\x52\x4a\x4a\
|
||||
\x46\x46\x42\x07\x00\x3c\x42\x42\x42\x42\x42\x42\x42\x42\x3c\x07\x00\x7c\
|
||||
\x42\x42\x42\x42\x7c\x40\x40\x40\x40\x08\x00\x3c\x42\x42\x42\x42\x42\x42\
|
||||
\x4a\x4a\x3c\x06\x07\x00\x7c\x42\x42\x42\x42\x7c\x50\x48\x44\x42\x07\x00\
|
||||
\x3c\x42\x40\x40\x30\x0c\x02\x02\x42\x3c\x07\x00\x7e\x08\x08\x08\x08\x08\
|
||||
\x08\x08\x08\x08\x07\x00\x42\x42\x42\x42\x42\x42\x42\x42\x42\x3c\x07\x00\
|
||||
\x42\x42\x42\x42\x42\x42\x24\x24\x18\x18\x07\x00\x42\x42\x42\x42\x42\x5a\
|
||||
\x5a\x5a\x24\x24\x07\x00\x42\x42\x24\x24\x18\x18\x24\x24\x42\x42\x07\x00\
|
||||
\x42\x42\x42\x42\x24\x18\x08\x08\x08\x08\x07\x00\x7e\x02\x04\x04\x08\x10\
|
||||
\x10\x20\x40\x7e\x09\x1e\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x1e\
|
||||
\x08\x40\x40\x20\x20\x10\x10\x08\x08\x04\x04\x02\x02\x09\x78\x08\x08\x08\
|
||||
\x08\x08\x08\x08\x08\x08\x08\x08\x78\x02\x00\x18\x24\x42\x09\x00\x00\x00\
|
||||
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7e\x03\x00\x30\x18\x08\x04\x07\x00\
|
||||
\x00\x00\x00\x3c\x02\x3e\x42\x42\x46\x3a\x07\x00\x40\x40\x40\x5c\x62\x42\
|
||||
\x42\x42\x62\x5c\x07\x00\x00\x00\x00\x3c\x42\x40\x40\x40\x42\x3c\x07\x00\
|
||||
\x02\x02\x02\x3a\x46\x42\x42\x42\x46\x3a\x07\x00\x00\x00\x00\x3c\x42\x42\
|
||||
\x7e\x40\x40\x3e\x07\x00\x1c\x22\x20\x20\x20\x7c\x20\x20\x20\x20\x0a\x00\
|
||||
\x00\x00\x00\x3a\x46\x42\x42\x46\x3a\x02\x02\x42\x3c\x07\x00\x40\x40\x40\
|
||||
\x5c\x62\x42\x42\x42\x42\x42\x07\x00\x08\x08\x00\x38\x08\x08\x08\x08\x08\
|
||||
\x08\x09\x00\x04\x04\x00\x1c\x04\x04\x04\x04\x04\x04\x44\x38\x07\x00\x40\
|
||||
\x40\x40\x44\x48\x50\x70\x48\x44\x42\x07\x00\x20\x20\x20\x20\x20\x20\x20\
|
||||
\x20\x20\x1c\x07\x00\x00\x00\x00\x74\x4a\x4a\x4a\x4a\x42\x42\x07\x00\x00\
|
||||
\x00\x00\x5c\x62\x42\x42\x42\x42\x42\x07\x00\x00\x00\x00\x3c\x42\x42\x42\
|
||||
\x42\x42\x3c\x09\x00\x00\x00\x00\x5c\x62\x42\x42\x62\x5c\x40\x40\x40\x09\
|
||||
\x00\x00\x00\x00\x3a\x46\x42\x42\x46\x3a\x02\x02\x02\x07\x00\x00\x00\x00\
|
||||
\x5c\x62\x40\x40\x40\x40\x40\x07\x00\x00\x00\x00\x3c\x42\x40\x3c\x02\x42\
|
||||
\x3c\x07\x00\x00\x20\x20\x7c\x20\x20\x20\x20\x22\x1c\x07\x00\x00\x00\x00\
|
||||
\x42\x42\x42\x42\x42\x46\x3a\x07\x00\x00\x00\x00\x42\x42\x42\x24\x24\x18\
|
||||
\x18\x07\x00\x00\x00\x00\x42\x42\x4a\x4a\x4a\x5a\x24\x07\x00\x00\x00\x00\
|
||||
\x42\x24\x18\x00\x18\x24\x42\x0a\x00\x00\x00\x00\x42\x42\x42\x42\x46\x3a\
|
||||
\x02\x02\x42\x3c\x07\x00\x00\x00\x00\x7e\x02\x04\x08\x10\x20\x7e\x09\x06\
|
||||
\x08\x08\x08\x08\x08\x30\x08\x08\x08\x08\x08\x06\x08\x10\x10\x10\x10\x10\
|
||||
\x10\x10\x10\x10\x10\x10\x10\x09\x60\x10\x10\x10\x10\x10\x0c\x10\x10\x10\
|
||||
\x10\x10\x60\x03\x00\x00\x32\x4a\x44\x08\x00\x00\x00\x08\x08\x08\x7e\x08\
|
||||
\x08\x08\x00\x7e\x08\x00\x00\x00\x00\x44\x44\x44\x44\x4c\x7a\x40\x40\x06\
|
||||
\x00\x00\x00\x00\x42\x24\x18\x18\x24\x42\
|
||||
\x00\x00\x00\x00\x00\x00\x18\x30\x20\x40\x0b\x00\x00\x00\x00\x00\x00\x3e\
|
||||
\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x38\x10\x08\x02\x02\x04\
|
||||
\x04\x08\x08\x10\x10\x20\x20\x40\x40\x07\x00\x18\x24\x42\x42\x4a\x52\x42\
|
||||
\x42\x24\x18\x07\x00\x08\x18\x28\x48\x08\x08\x08\x08\x08\x08\x07\x00\x3c\
|
||||
\x42\x42\x02\x04\x08\x10\x20\x40\x7e\x07\x00\x3c\x42\x02\x02\x1c\x02\x02\
|
||||
\x42\x42\x3c\x07\x00\x0c\x14\x14\x24\x24\x44\x7e\x04\x04\x04\x07\x00\x7e\
|
||||
\x40\x40\x7c\x42\x02\x02\x02\x42\x3c\x07\x00\x3c\x42\x40\x40\x5c\x62\x42\
|
||||
\x42\x42\x3c\x07\x00\x7e\x02\x04\x04\x08\x08\x10\x10\x20\x20\x07\x00\x3c\
|
||||
\x42\x42\x42\x3c\x42\x42\x42\x42\x3c\x07\x00\x3c\x42\x42\x42\x46\x3a\x02\
|
||||
\x02\x42\x3c\x08\x00\x00\x00\x10\x38\x10\x00\x00\x00\x10\x38\x10\x09\x00\
|
||||
\x00\x00\x10\x38\x10\x00\x00\x00\x18\x30\x20\x40\x07\x00\x00\x04\x08\x10\
|
||||
\x20\x40\x20\x10\x08\x04\x05\x00\x00\x00\x00\x00\x7e\x00\x00\x7e\x07\x00\
|
||||
\x00\x20\x10\x08\x04\x02\x04\x08\x10\x20\x07\x00\x78\x04\x04\x08\x10\x20\
|
||||
\x20\x00\x20\x20\x07\x00\x1c\x22\x42\x4e\x52\x52\x52\x4e\x20\x1c\x07\x00\
|
||||
\x3c\x42\x42\x42\x42\x7e\x42\x42\x42\x42\x07\x00\x78\x44\x44\x44\x7c\x42\
|
||||
\x42\x42\x42\x7c\x07\x00\x3c\x42\x42\x40\x40\x40\x40\x42\x42\x3c\x07\x00\
|
||||
\x78\x44\x42\x42\x42\x42\x42\x42\x44\x78\x07\x00\x7e\x40\x40\x40\x7c\x40\
|
||||
\x40\x40\x40\x7e\x07\x00\x7e\x40\x40\x40\x7c\x40\x40\x40\x40\x40\x07\x00\
|
||||
\x3c\x42\x42\x40\x40\x4e\x42\x42\x42\x3c\x07\x00\x42\x42\x42\x42\x7e\x42\
|
||||
\x42\x42\x42\x42\x07\x00\x7c\x10\x10\x10\x10\x10\x10\x10\x10\x7c\x07\x00\
|
||||
\x1e\x02\x02\x02\x02\x02\x02\x42\x42\x3c\x07\x00\x42\x44\x48\x50\x68\x48\
|
||||
\x44\x44\x42\x42\x07\x00\x40\x40\x40\x40\x40\x40\x40\x40\x40\x7e\x07\x00\
|
||||
\x42\x66\x66\x5a\x5a\x5a\x42\x42\x42\x42\x07\x00\x42\x62\x62\x52\x52\x4a\
|
||||
\x4a\x46\x46\x42\x07\x00\x3c\x42\x42\x42\x42\x42\x42\x42\x42\x3c\x07\x00\
|
||||
\x7c\x42\x42\x42\x42\x7c\x40\x40\x40\x40\x08\x00\x3c\x42\x42\x42\x42\x42\
|
||||
\x42\x4a\x4a\x3c\x06\x07\x00\x7c\x42\x42\x42\x42\x7c\x50\x48\x44\x42\x07\
|
||||
\x00\x3c\x42\x40\x40\x30\x0c\x02\x02\x42\x3c\x07\x00\x7e\x08\x08\x08\x08\
|
||||
\x08\x08\x08\x08\x08\x07\x00\x42\x42\x42\x42\x42\x42\x42\x42\x42\x3c\x07\
|
||||
\x00\x42\x42\x42\x42\x42\x42\x24\x24\x18\x18\x07\x00\x42\x42\x42\x42\x42\
|
||||
\x5a\x5a\x5a\x24\x24\x07\x00\x42\x42\x24\x24\x18\x18\x24\x24\x42\x42\x07\
|
||||
\x00\x42\x42\x42\x42\x24\x18\x08\x08\x08\x08\x07\x00\x7e\x02\x04\x04\x08\
|
||||
\x10\x10\x20\x40\x7e\x09\x1e\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\
|
||||
\x1e\x08\x40\x40\x20\x20\x10\x10\x08\x08\x04\x04\x02\x02\x09\x78\x08\x08\
|
||||
\x08\x08\x08\x08\x08\x08\x08\x08\x08\x78\x02\x00\x18\x24\x42\x09\x00\x00\
|
||||
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7e\x03\x00\x30\x18\x08\x04\x07\
|
||||
\x00\x00\x00\x00\x3c\x02\x3e\x42\x42\x46\x3a\x07\x00\x40\x40\x40\x5c\x62\
|
||||
\x42\x42\x42\x62\x5c\x07\x00\x00\x00\x00\x3c\x42\x40\x40\x40\x42\x3c\x07\
|
||||
\x00\x02\x02\x02\x3a\x46\x42\x42\x42\x46\x3a\x07\x00\x00\x00\x00\x3c\x42\
|
||||
\x42\x7e\x40\x40\x3e\x07\x00\x1c\x22\x20\x20\x20\x7c\x20\x20\x20\x20\x0a\
|
||||
\x00\x00\x00\x00\x3a\x46\x42\x42\x46\x3a\x02\x02\x42\x3c\x07\x00\x40\x40\
|
||||
\x40\x5c\x62\x42\x42\x42\x42\x42\x07\x00\x08\x08\x00\x38\x08\x08\x08\x08\
|
||||
\x08\x08\x09\x00\x04\x04\x00\x1c\x04\x04\x04\x04\x04\x04\x44\x38\x07\x00\
|
||||
\x40\x40\x40\x44\x48\x50\x70\x48\x44\x42\x07\x00\x20\x20\x20\x20\x20\x20\
|
||||
\x20\x20\x20\x1c\x07\x00\x00\x00\x00\x74\x4a\x4a\x4a\x4a\x42\x42\x07\x00\
|
||||
\x00\x00\x00\x5c\x62\x42\x42\x42\x42\x42\x07\x00\x00\x00\x00\x3c\x42\x42\
|
||||
\x42\x42\x42\x3c\x09\x00\x00\x00\x00\x5c\x62\x42\x42\x62\x5c\x40\x40\x40\
|
||||
\x09\x00\x00\x00\x00\x3a\x46\x42\x42\x46\x3a\x02\x02\x02\x07\x00\x00\x00\
|
||||
\x00\x5c\x62\x40\x40\x40\x40\x40\x07\x00\x00\x00\x00\x3c\x42\x40\x3c\x02\
|
||||
\x42\x3c\x07\x00\x00\x20\x20\x7c\x20\x20\x20\x20\x22\x1c\x07\x00\x00\x00\
|
||||
\x00\x42\x42\x42\x42\x42\x46\x3a\x07\x00\x00\x00\x00\x42\x42\x42\x24\x24\
|
||||
\x18\x18\x07\x00\x00\x00\x00\x42\x42\x4a\x4a\x4a\x5a\x24\x07\x00\x00\x00\
|
||||
\x00\x42\x24\x18\x00\x18\x24\x42\x0a\x00\x00\x00\x00\x42\x42\x42\x42\x46\
|
||||
\x3a\x02\x02\x42\x3c\x07\x00\x00\x00\x00\x7e\x02\x04\x08\x10\x20\x7e\x09\
|
||||
\x06\x08\x08\x08\x08\x08\x30\x08\x08\x08\x08\x08\x06\x08\x10\x10\x10\x10\
|
||||
\x10\x10\x10\x10\x10\x10\x10\x10\x09\x60\x10\x10\x10\x10\x10\x0c\x10\x10\
|
||||
\x10\x10\x10\x60\x03\x00\x00\x32\x4a\x44\x0c\x00\x00\x00\x00\x00\x00\x00\
|
||||
\x00\x00\x00\x03\x80\x03\x80\x03\x80\x00\x00\x0d\x00\x00\x00\x00\x00\x00\
|
||||
\x00\x00\x08\x00\x18\x00\x3f\xf8\x18\x00\x08\x00\x00\x00\x0d\x00\x00\x00\
|
||||
\x00\x00\x00\x00\x00\x00\x20\x00\x30\x3f\xf8\x00\x30\x00\x20\x00\x00\x0d\
|
||||
\x00\x00\x10\x00\x10\x00\x10\x00\x10\x20\x10\x30\x1f\xf8\x00\x30\x00\x20\
|
||||
\x00\x00\
|
||||
"""
|
||||
|
||||
|
||||
class FontLarge(FontBase):
|
||||
height = 21
|
||||
code_range = range(32, 215)
|
||||
code_range = range(32, 126)
|
||||
|
||||
_bboxes = [None, (0, -5, 10, 21, 0), (0, -5, 10, 21, 14), (0, -5,
|
||||
10, 21, 16), (0, -5, 10, 21, 22), (0, -5, 10, 21, 26), (0, -5, 10,
|
||||
21, 30), (0, -5, 10, 21, 32), (0, -5, 10, 21, 34), (0, -5, 10, 21,
|
||||
36), (0, -5, 10, 21, 38), (0, -5, 10, 21, 40)]
|
||||
|
||||
_bboxes = [None, (0, -5, 10, 21, 0), (0, -5, 10, 21, 14), (0,
|
||||
-5, 10, 21, 16), (0, -5, 10, 21, 22), (0, -5, 10, 21, 26), (0, -5,
|
||||
10, 21, 30), (0, -5, 10, 21, 32), (0, -5, 10, 21, 34), (0, -5, 10,
|
||||
21, 36), (0, -5, 10, 21, 38), (0, -5, 10, 21, 40)]
|
||||
|
||||
_code_points = [
|
||||
(range(32, 127), [1, 2, 35, 50, 81, 118, 151, 184, 199, 238, 277, 304,
|
||||
335, 372, 395, 428, 463, 496, 529, 562, 595, 628, 661, 694, 727,
|
||||
@ -143,8 +144,6 @@ class FontLarge(FontBase):
|
||||
2223, 2256, 2297, 2330, 2363, 2402, 2435, 2468, 2501, 2534, 2567,
|
||||
2608, 2649, 2682, 2715, 2748, 2781, 2814, 2847, 2880, 2921, 2954,
|
||||
2991, 3026, 3063]),
|
||||
(range(177, 182), [3080, 0, 0, 0, 3115]), # ± µ
|
||||
(range(215, 216), [3150]), # ×
|
||||
]
|
||||
|
||||
_bitmaps = b"""\
|
||||
@ -319,20 +318,16 @@ class FontLarge(FontBase):
|
||||
\x08\x00\x09\x00\x00\x00\x00\x38\x00\x04\x00\x04\x00\x04\x00\x04\x00\x04\
|
||||
\x00\x04\x00\x04\x00\x03\x80\x04\x00\x04\x00\x04\x00\x04\x00\x04\x00\x04\
|
||||
\x00\x38\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18\x40\x24\x40\
|
||||
\x23\x80\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\
|
||||
\x00\x04\x00\x04\x00\x3f\x80\x04\x00\x04\x00\x04\x00\x00\x00\x00\x00\x3f\
|
||||
\x80\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x21\x00\
|
||||
\x21\x00\x21\x00\x21\x00\x21\x00\x21\x00\x33\x00\x2c\x80\x20\x00\x20\x00\
|
||||
\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x40\x80\x21\
|
||||
\x00\x12\x00\x0c\x00\x0c\x00\x12\x00\x21\x00\x40\x80\
|
||||
\x23\x80\
|
||||
"""
|
||||
|
||||
|
||||
class FontTiny(FontBase):
|
||||
height = 6
|
||||
code_range = range(32, 8730)
|
||||
code_range = range(32, 126)
|
||||
|
||||
_bboxes = [None, (0, -1, 4, 6, 0), (0, -1, 4, 6, 2), (0, -1, 4,
|
||||
6, 3), (0, -1, 4, 6, 4), (0, -1, 4, 6, 5), (0, -1, 4, 6, 6)]
|
||||
_bboxes = [None, (0, -1, 4, 6, 0), (0, -1, 4, 6, 2), (0, -1, 4, 6,
|
||||
3), (0, -1, 4, 6, 4), (0, -1, 4, 6, 5), (0, -1, 4, 6, 6)]
|
||||
|
||||
_code_points = [
|
||||
(range(32, 127), [1, 2, 8, 11, 17, 24, 30, 36, 39, 46, 53, 59, 65, 72,
|
||||
@ -343,9 +338,6 @@ class FontTiny(FontBase):
|
||||
390, 396, 402, 408, 414, 421, 427, 433, 440, 446, 452, 458, 464,
|
||||
470, 477, 484, 490, 496, 502, 508, 514, 520, 526, 533, 539, 546,
|
||||
552, 559]),
|
||||
(range(177, 182), [562, 0, 0, 0, 568]), # ± µ
|
||||
(range(215, 216), [575]), # ×
|
||||
(range(8730, 8731), [580]), # √
|
||||
]
|
||||
|
||||
_bitmaps = b"""\
|
||||
@ -380,7 +372,7 @@ class FontTiny(FontBase):
|
||||
\xa0\xa0\xa0\x60\x05\x00\xa0\xa0\xa0\x40\x05\x00\xa0\xa0\xe0\xa0\x05\x00\
|
||||
\xa0\x40\x40\xa0\x06\x00\xa0\xa0\x60\x20\xc0\x05\x00\xe0\x20\x40\xe0\x06\
|
||||
\x20\x40\xc0\x40\x40\x20\x05\x40\x40\x40\x40\x40\x06\x80\x40\x60\x40\x40\
|
||||
\x80\x02\x50\xa0\x05\x40\xe0\x40\x00\xe0\x06\x00\xa0\xa0\xa0\xc0\x80\x04\
|
||||
\x00\xa0\x40\xa0\x05\x30\x20\x20\xa0\x60\
|
||||
\x80\x02\x50\xa0\
|
||||
"""
|
||||
|
||||
|
||||
|
||||
4
stm32/.gitignore
vendored
4
stm32/.gitignore
vendored
@ -8,6 +8,8 @@ mpy-files.timestamp
|
||||
firmware.lss
|
||||
firmware-signed.*
|
||||
firmware.elf
|
||||
file_time.c
|
||||
*-RC1-coldcard.dfu
|
||||
|
||||
# somewhat useful binary snapshots
|
||||
mostly.dfu
|
||||
@ -21,3 +23,5 @@ check-fw.bin
|
||||
check-bootrom.bin
|
||||
repro-got.txt
|
||||
repro-want.txt
|
||||
|
||||
C
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
|
||||
// (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
// (c) Copyright 2020-2022 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
//
|
||||
// AUTO-generated.
|
||||
//
|
||||
// built: 2021-09-02
|
||||
// version: 4.1.3
|
||||
// built: 2022-05-04
|
||||
// version: 5.0.3
|
||||
//
|
||||
#include <stdint.h>
|
||||
|
||||
// this overrides ports/stm32/fatfs_port.c
|
||||
uint32_t get_fattime(void) {
|
||||
return 0x53222020UL;
|
||||
return 0x54a42800UL;
|
||||
}
|
||||
|
||||
@ -179,13 +179,12 @@ STATIC mp_obj_t is_simulator(void)
|
||||
}
|
||||
MP_DEFINE_CONST_FUN_OBJ_0(is_simulator_obj, is_simulator);
|
||||
|
||||
STATIC mp_obj_t is_stm32l496(void)
|
||||
STATIC mp_obj_t get_cpu_id(void)
|
||||
{
|
||||
// Are we running on a STM32L496RG6?
|
||||
return ((DBGMCU->IDCODE & 0xfff) == 0x461) ? mp_const_true : mp_const_false;
|
||||
// Are we running on a STM32L496RG6? If so, expect 0x461
|
||||
return MP_OBJ_NEW_SMALL_INT(DBGMCU->IDCODE & 0xfff);
|
||||
}
|
||||
MP_DEFINE_CONST_FUN_OBJ_0(is_stm32l496_obj, is_stm32l496);
|
||||
|
||||
MP_DEFINE_CONST_FUN_OBJ_0(get_cpu_id_obj, get_cpu_id);
|
||||
|
||||
|
||||
STATIC mp_obj_t vcp_enabled(mp_obj_t new_val)
|
||||
@ -253,6 +252,12 @@ STATIC mp_obj_t watchpoint(volatile mp_obj_t arg1)
|
||||
}
|
||||
MP_DEFINE_CONST_FUN_OBJ_1(watchpoint_obj, watchpoint);
|
||||
|
||||
STATIC mp_obj_t usb_active(void)
|
||||
{
|
||||
// NOP on mk3 but here for compatibility
|
||||
return mp_const_none;
|
||||
}
|
||||
MP_DEFINE_CONST_FUN_OBJ_0(usb_active_obj, usb_active);
|
||||
|
||||
STATIC const mp_rom_map_elem_t ckcc_module_globals_table[] = {
|
||||
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_ckcc) },
|
||||
@ -261,13 +266,14 @@ STATIC const mp_rom_map_elem_t ckcc_module_globals_table[] = {
|
||||
{ MP_ROM_QSTR(MP_QSTR_gate), MP_ROM_PTR(&sec_gate_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_oneway), MP_ROM_PTR(&sec_oneway_gate_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_is_simulator), MP_ROM_PTR(&is_simulator_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_is_stm32l496), MP_ROM_PTR(&is_stm32l496_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_get_cpu_id), MP_ROM_PTR(&get_cpu_id_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_vcp_enabled), MP_ROM_PTR(&vcp_enabled_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_wipe_fs), MP_ROM_PTR(&wipe_fs_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_presume_green), MP_ROM_PTR(&presume_green_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_breakpoint), MP_ROM_PTR(&breakpoint_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_watchpoint), MP_ROM_PTR(&watchpoint_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_stack_limit), MP_ROM_PTR(&stack_limit_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_usb_active), MP_ROM_PTR(&usb_active_obj) },
|
||||
};
|
||||
|
||||
STATIC MP_DEFINE_CONST_DICT(ckcc_module_globals, ckcc_module_globals_table);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user