forked from Foundation-Devices/passport-firmware
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpincodes.py
374 lines (301 loc) · 12.5 KB
/
pincodes.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. <[email protected]>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2018 Coinkite, Inc. <coldcardwallet.com>
# SPDX-License-Identifier: GPL-3.0-only
#
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
#
# pincodes.py - manage PIN code (which map to wallet seeds)
#
import trezorcrypto
import ustruct
import version
from callgate import get_anti_phishing_words,get_supply_chain_validation_words
from ubinascii import hexlify as b2a_hex
from se_commands import *
from common import system
# 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)
SE_SECRET_LEN = const(72)
# magic number for struct
PA_MAGIC_V1 = const(0xc50b61a7)
# 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: "SE_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_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_V1
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
uint8_t cached_main_pin[32]; // iff they provided right pin already (V2)
'''
PIN_ATTEMPT_FMT_V1 = 'I32si6I32si32si32si72s32s'
PIN_ATTEMPT_SIZE_V1 = const(276)
# 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.pin = None
self.secret = None
self.is_empty = None
self.magic_value = 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.max_attempts = 21 # Numbger of attempts allowed
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_V1)
self.buf = bytearray(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('I AM I BRICKED!!!')
pass
def __repr__(self):
return '<PinAttempt: num_fails={} delay={}/{} attempts_left={} state=0x{} hmac={}>'.format(
self.num_fails,
self.delay_achieved, self.delay_required, self.attempts_left, hex(self.state_flags),
b2a_hex(self.hmac))
def marshal(self, msg, new_secret=None, new_pin=None, old_pin=None, ls_offset=None):
# serialize our state, and maybe some arguments
change_flags = 0
if new_secret is not None:
change_flags |= CHANGE_SECRET
assert len(new_secret) in (32, SE_SECRET_LEN)
else:
new_secret = bytes(SE_SECRET_LEN)
# NOTE: pins should be bytes here.
if new_pin is not None:
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,
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,
self.cached_main_pin]
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,
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,
self.cached_main_pin) = 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]
return secret
def pin_control(self, method_num, **kws):
self.marshal(self.buf, **kws)
# print("> tx: %s" % b2a_hex(self.buf))
err = system.dispatch(CMD_PIN_CONTROL, 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:
raise RuntimeError(err)
# Unpack the updated attempts_left and num_fails if the pin was wrong so the UI updates correctly
if err == EPIN_AUTH_FAIL:
self.unmarshal(self.buf)
# print('Unmarshalled: {}'.format(self.attempts_left))
# print('ERROR: {} ({})'.format(PA_ERROR_CODES[err], err))
raise BootloaderError(PA_ERROR_CODES[err], err)
elif err:
raise RuntimeError(err)
return self.unmarshal(self.buf)
@staticmethod
def anti_phishing_words(pin_prefix):
# Take a prefix of the PIN and turn it into two
# bip39 words for anti-phishing protection.
assert 1 <= len(pin_prefix) <= MAX_PIN_LEN, len(pin_prefix)
# global _word_cache
# for k, v in _word_cache:
# if pin_prefix == k:
# return v
padding = MAX_PIN_LEN - len(pin_prefix)
buf = bytearray(pin_prefix + b'\0'*padding)
err = get_anti_phishing_words(buf)
if err:
raise RuntimeError(err)
# Get a mnemonic from the 32 bytes in the buffer
s = trezorcrypto.bip39.from_data(buf)
rv = s.split()
return rv[0:2] # Only keep 2 words for anti-phishing prefix
@staticmethod
def supply_chain_validation_words(challenge_str):
# Take the validation string and turn it into 4
# bip39 words for supply chain tampering detection.
# print('challenge_str={}'.format(challenge_str))
buf = bytearray(challenge_str)
# This actually gets the HMAC bytes, not the words
err = get_supply_chain_validation_words(buf)
if err:
raise RuntimeError(err)
# print('hmac buf = {}'.format(buf))
# Get a mnemonic from the 32 bytes in the buffer
buf = buf[:32]
if len(buf) < 32:
padding = 32 - len(buf)
buf = buf + b'\0'*padding
s = trezorcrypto.bip39.from_data(buf)
rv = s.split()
# print('supply chain validation words = {}'.format(rv[0:4]))
return rv[0:4] # Only keep 4 words for supply chain validation
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
# print('self.state_flags = {:04x}'.format(self.state_flags))
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 reset(self):
# start over, like when you commit a new seed
return self.setup(self.pin)
# Prepare the class for a PIN operation (first pin, login, change)
def setup(self, pin, secondary=False):
# print('Setting up SE hmac')
self.pin = pin
self.hmac = bytes(32)
_ = self.pin_control(PIN_SETUP)
return self.state_flags
def login(self):
# test we have the PIN code right, and unlock access if so.
chk = self.pin_control(PIN_ATTEMPT)
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
self.pin_control(PIN_CHANGE, **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):
secret = self.pin_control(PIN_GET_SECRET)
return secret
async def new_main_secret(self, raw_secret, chain=None):
from common import settings, flash_cache
import stash
# 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()
# We shouldn't need to save this anymore since we are dynamically managing xfp and xpub
# await settings.save()
# Set the key for the flash cache (cache was inaccessible prior to user logging in)
flash_cache.set_key(new_secret=raw_secret)
# Need to save out flash cache with the new secret or else we won't be able to read it back later
flash_cache.save()
# EOF