firmware/shared/pincodes.py
2021-04-27 14:53:14 -04:00

429 lines
14 KiB
Python

# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# pincodes.py - manage PIN code (which map to wallet seeds)
#
import ustruct, ckcc, version
from ubinascii import hexlify as b2a_hex
from callgate import enter_dfu
from bip39 import wordlist_en
# See ../stm32/bootloader/pins.h for source of these constants.
#
MAX_PIN_LEN = const(32)
# how many bytes per secret (you don't have to use them all)
AE_SECRET_LEN = const(72)
# on mark3 (608a) we can also store a longer secret
AE_LONG_SECRET_LEN = const(416)
# magic number for struct
PA_MAGIC_V1 = const(0x2eaf6311)
PA_MAGIC_V2 = const(0x2eaf6312)
# For state_flags field: report only covers current wallet (primary vs. secondary)
PA_SUCCESSFUL = const(0x01)
PA_IS_BLANK = const(0x02)
PA_HAS_DURESS = const(0x04)
PA_HAS_BRICKME = const(0x08)
PA_ZERO_SECRET = const(0x10)
# For change_flags field:
CHANGE_WALLET_PIN = const(0x001)
CHANGE_DURESS_PIN = const(0x002)
CHANGE_BRICKME_PIN = const(0x004)
CHANGE_SECRET = const(0x008)
CHANGE_DURESS_SECRET = const(0x010)
CHANGE_SECONDARY_WALLET_PIN = const(0x020)
CHANGE_LS_OFFSET = const(0xf00)
# See below for other direction as well.
PA_ERROR_CODES = {
-100: "HMAC_FAIL",
-101: "HMAC_REQUIRED",
-102: "BAD_MAGIC",
-103: "RANGE_ERR",
-104: "BAD_REQUEST",
-105: "I_AM_BRICK",
-106: "AE_FAIL",
-107: "MUST_WAIT",
-108: "PIN_REQUIRED",
-109: "WRONG_SUCCESS",
-110: "OLD_ATTEMPT",
-111: "AUTH_MISMATCH",
-112: "AUTH_FAIL",
-113: "OLD_AUTH_FAIL",
-114: "PRIMARY_ONLY",
}
# just a few of the likely ones; non-programing errors
EPIN_I_AM_BRICK = const(-105)
EPIN_MUST_WAIT = const(-107)
EPIN_PIN_REQUIRED = const(-108)
EPIN_WRONG_SUCCESS = const(-109)
EPIN_OLD_ATTEMPT = const(-110)
EPIN_AUTH_MISMATCH = const(-111)
EPIN_AUTH_FAIL = const(-112)
EPIN_OLD_AUTH_FAIL = const(-113)
# We are round-tripping this big structure, partially signed by bootloader.
'''
uint32_t magic_value; // = PA_MAGIC_V2 or V1 for older bootroms
int is_secondary; // (bool) primary or secondary
char pin[MAX_PIN_LEN]; // value being attempted
int pin_len; // valid length of pin
uint32_t delay_achieved; // so far, how much time wasted? [508a only]
uint32_t delay_required; // how much will be needed? [508a only]
uint32_t num_fails; // for UI: number of fails PINs
uint32_t attempts_left; // trys left until bricking [608a only]
uint32_t state_flags; // what things have been setup/enabled already
uint32_t private_state; // some internal (encrypted) state
uint8_t hmac[32]; // bootloader's hmac over above, or zeros
// remaining fields are return values, or optional args;
int change_flags; // bitmask of what to do
char old_pin[MAX_PIN_LEN]; // (optional) old PIN value
int old_pin_len; // (optional) valid length of old_pin, can be zero
char new_pin[MAX_PIN_LEN]; // (optional) new PIN value
int new_pin_len; // (optional) valid length of new_pin, can be zero
uint8_t secret[72]; // secret to be changed OR return value
// may grow from here in future versions (V1 bootroms don't expect more)
uint8_t cached_main_pin[32]; // iff they provided right pin already (V2)
'''
PIN_ATTEMPT_FMT_V1 = 'Ii32si6I32si32si32si72s'
PIN_ATTEMPT_FMT_V2_ADDITIONS = '32s'
PIN_ATTEMPT_SIZE_V1 = const(248)
PIN_ATTEMPT_SIZE = const(248+32)
# small cache of pin-prefix to words, for 608a based systems
_word_cache = []
class BootloaderError(RuntimeError):
pass
class PinAttempt:
seconds_per_tick = 0.5
def __init__(self):
self.is_secondary = False
self.pin = None
self.secret = None
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.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
self.private_state = 0 # opaque data, but preserve
self.cached_main_pin = bytearray(32)
assert MAX_PIN_LEN == 32 # update FMT otherwise
assert ustruct.calcsize(PIN_ATTEMPT_FMT_V1) == PIN_ATTEMPT_SIZE_V1, \
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
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)
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
):
# serialize our state, and maybe some arguments
change_flags = 0
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)
else:
new_secret = bytes(AE_SECRET_LEN)
# NOTE: pins should be bytes here.
if get_duress_secret:
# special case for reading duress secret from main wallet
change_flags |= CHANGE_DURESS_SECRET
if new_pin is not None:
if is_duress:
change_flags |= CHANGE_DURESS_PIN
elif is_brickme:
change_flags |= CHANGE_BRICKME_PIN
elif is_secondary:
change_flags |= CHANGE_SECONDARY_WALLET_PIN
else:
change_flags |= CHANGE_WALLET_PIN
assert not old_pin or old_pin == self.pin
old_pin = self.pin
assert len(new_pin) <= MAX_PIN_LEN
assert old_pin != None
assert len(old_pin) <= MAX_PIN_LEN
else:
new_pin = b''
old_pin = old_pin if old_pin is not None else self.pin
if ls_offset is not None:
change_flags |= (ls_offset << 8) # see CHANGE_LS_OFFSET
# can't send the V2 extra stuff if the bootrom isn't expecting it
fields = [self.magic_value,
(1 if self.is_secondary else 0),
self.pin, len(self.pin),
self.delay_achieved,
self.delay_required,
self.num_fails,
self.attempts_left,
self.state_flags,
self.private_state,
self.hmac,
change_flags,
old_pin, len(old_pin),
new_pin, len(new_pin),
new_secret]
if version.has_608:
fmt = PIN_ATTEMPT_FMT_V1 + PIN_ATTEMPT_FMT_V2_ADDITIONS
fields.append(self.cached_main_pin)
else:
fmt = PIN_ATTEMPT_FMT_V1
ustruct.pack_into(fmt, msg, 0, *fields)
def unmarshal(self, msg):
# unpack it and update our state, return other state
x = ustruct.unpack_from(PIN_ATTEMPT_FMT_V1, msg)
(self.magic_value, was_secondary,
self.pin, pin_len,
self.delay_achieved,
self.delay_required,
self.num_fails,
self.attempts_left,
self.state_flags,
self.private_state,
self.hmac,
change_flags,
old_pin, old_pin_len,
new_pin, new_pin_len,
secret) = x
# NOTE: not useful to readback values we sent and it never updates
#new_pin = new_pin[0:new_pin_len]
#old_pin = old_pin[0:old_pin_len]
self.pin = self.pin[0:pin_len]
if self.magic_value == PA_MAGIC_V2:
# pull out V2 extra values
self.cached_main_pin, = ustruct.unpack_from(PIN_ATTEMPT_FMT_V2_ADDITIONS,
msg, PIN_ATTEMPT_SIZE_V1)
return secret
def roundtrip(self, method_num, **kws):
self.marshal(self.buf, **kws)
#print("> tx: %s" % b2a_hex(buf))
err = ckcc.gate(18, self.buf, method_num)
#print("[%d] rx: %s" % (err, b2a_hex(self.buf)))
if err <= -100:
#print("[%d] req: %s" % (err, b2a_hex(self.buf)))
if err == EPIN_I_AM_BRICK:
# don't try to continue!
enter_dfu(3)
raise BootloaderError(PA_ERROR_CODES[err], err)
elif err:
raise RuntimeError(err)
return self.unmarshal(self.buf)
@staticmethod
def prefix_words(pin_prefix):
# take a prefix of the PIN and turn it into a few
# bip39 words for anti-phishing protection
assert 1 <= len(pin_prefix) <= MAX_PIN_LEN, len(pin_prefix)
global _word_cache
if version.has_608:
for k,v in _word_cache:
if pin_prefix == k:
return v
buf = bytearray(pin_prefix + b'\0'*MAX_PIN_LEN)
err = ckcc.gate(16, buf, len(pin_prefix))
if err:
raise RuntimeError(err)
# use just 22 bits of that
bits = ustruct.unpack('I', buf[0:4])[0]
w1 = (bits >> 11) & 0x7ff
w2 = bits & 0x7ff
rv = wordlist_en[w1], wordlist_en[w2]
if version.has_608:
# MRU: keep only a few
if len(_word_cache) > 4:
_word_cache.pop()
_word_cache.insert(0, (pin_prefix, rv))
return rv
def is_delay_needed(self):
return self.delay_achieved < self.delay_required
def is_blank(self):
# device has no PIN at this point
return bool(self.state_flags & PA_IS_BLANK)
def is_successful(self):
# we've got a valid pin
return bool(self.state_flags & PA_SUCCESSFUL)
def is_secret_blank(self):
assert self.state_flags & PA_SUCCESSFUL
return bool(self.state_flags & PA_ZERO_SECRET)
def has_duress_pin(self):
return bool(self.state_flags & PA_HAS_DURESS)
def has_brickme_pin(self):
return bool(self.state_flags & PA_HAS_BRICKME)
def reset(self):
# start over, like when you commit a new seed
return self.setup(self.pin, self.is_secondary)
def setup(self, pin, secondary=False):
self.is_secondary = secondary
self.pin = pin
self.hmac = bytes(32)
_ = self.roundtrip(0)
return self.state_flags
def delay(self):
self.roundtrip(1)
def login(self):
# test we have the PIN code right, and unlock access if so.
chk = self.roundtrip(2)
self.is_empty = (chk[0] == 0)
# IMPORTANT: You will need to re-read settings since the key for that has changed
ok = self.is_successful()
if ok:
# it's a bit sensitive, and no longer useful: wipe.
global _word_cache
_word_cache.clear()
return ok
def change(self, **kws):
# change various values, stored in secure element
if self.tmp_value: return
self.roundtrip(3, **kws)
# IMPORTANT:
# - 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):
if self.tmp_value:
# must make a copy here, and must be mutable instance so not reused
return bytearray(self.tmp_value)
if duress_pin is None:
secret = self.roundtrip(4)
else:
secret = self.roundtrip(4, old_pin=duress_pin, get_duress_secret=True)
return secret
def ls_fetch(self):
# get the "long secret"
#assert (13 * 32) == 416 == AE_LONG_SECRET_LEN
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]
return secret
def ls_change(self, new_long_secret):
# set the "long secret"
assert len(new_long_secret) == AE_LONG_SECRET_LEN
if self.tmp_value: return
for n in range(13):
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
self.roundtrip(5)
ckcc.presume_green()
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
import stash
# capture values we have already
old_values = dict(settings.current)
settings.set_key(raw_secret)
settings.load()
# merge in settings, including what chain to use, timeout, etc.
settings.merge(old_values)
# Recalculate xfp/xpub values (depends both on secret and chain)
with stash.SensitiveValues(raw_secret) as sv:
if chain is not None:
sv.chain = chain
sv.capture_xpub()
# does not call settings.save() but caller should!
def tmp_secret(self, encoded):
# Use indicated secret and stop using the SE; operate like this until reboot
self.tmp_value = bytes(encoded + bytes(AE_SECRET_LEN - len(encoded)))
# We're no longer blank. hard to say about duress secret and stuff tho
self.state_flags = PA_SUCCESSFUL
# Clear bip-39 secret, not applicable anymore.
import stash
stash.bip39_passphrase = ''
# 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)
# singleton
pa = PinAttempt()
# EOF