Merge branch 'Coldcard:master' into master

This commit is contained in:
scgbckbone 2022-05-09 13:28:55 +02:00 committed by GitHub
commit 50c541c080
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
338 changed files with 256751 additions and 2523 deletions

3
.gitmodules vendored
View File

@ -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

View File

@ -8,8 +8,7 @@ with the latest updates and security alerts.
![coldcard logo](https://coldcard.com/static/images/coldcard-logo-nav.png)
![coldcard picture front](https://coldcard.com/static/images/coldcard-front.png)
![coldcard picture back](https://coldcard.com/static/images/coldcard-back.png)
![Mk4 coldcard picture front](https://coldcard.com/static/images/mk4.png)
## 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

View File

@ -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

View File

@ -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.

View File

@ -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+

View File

@ -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:
![dev-warning screen](dev-warning.png)
- 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.
![custom warning screen](dev-custom.png)
- 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:
![warning screen](dev-warning.png)
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -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)

View File

@ -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`.

View File

@ -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
---

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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
View 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!

View File

@ -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

@ -1 +1 @@
Subproject commit 87b2db968775303a6becc4b460ad867f922c3d14
Subproject commit ef61911047e77d54bc0c6edd5400a424405d814e

2
external/libngu vendored

@ -1 +1 @@
Subproject commit ddfd47644962fe20301466199da90a6f292732af
Subproject commit a28aef463df3e0a21750381edaea989d7bc5679b

@ -1 +1 @@
Subproject commit 5917fc199c7e1dd2ef7f9ada8bc48abe0488e9f3
Subproject commit 6d7527823c3fd9b2c860c8cc1612cc68f6e2cef2

2
external/mpy-qr vendored

@ -1 +1 @@
Subproject commit cb71312dd7369ae6cacd185a81c12683892b971e
Subproject commit 3ccf19ca142e9059904f0c8e53b6baeccb9c6b79

View File

@ -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)

View File

@ -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
View 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
View 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
View 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
View 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
View 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

View File

@ -9,11 +9,17 @@ researchers who wish to analyse the Coldcard more completely.
# Schematic
![](schematic-mark4d.png)
`schematic-mark4d.png`
This is the Mark4 rev D schematic.
![](schematic-mark3b.png)
`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&mdash;no warranties.
- **Coinkite does not grant license of this information for comercial use.**

BIN
hardware/bom-mark4b.xlsx Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 KiB

5
misc/binfonter/README.md Normal file
View 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
View 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
'''),
])

View 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

View File

@ -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

View File

@ -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
View 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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
View 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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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
View 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

View File

@ -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
View File

@ -0,0 +1 @@
../graphics/graphics_mk4.py

View File

@ -5,3 +5,6 @@
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
import uasyncio
arun = uasyncio.run

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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())

View File

@ -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
View 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
View 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)

View File

@ -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
View 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

View File

@ -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
View 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
View 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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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()

View File

@ -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
View 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

View File

@ -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')

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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
View 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

View File

@ -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:

View File

@ -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!

View File

@ -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):

View File

@ -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)

View File

@ -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
View 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

View File

@ -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

View File

@ -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)

View File

@ -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
View File

@ -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

View File

@ -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;
}

View File

@ -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