forked from Foundation-Devices/passport-firmware
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsettings.py
389 lines (321 loc) · 15.4 KB
/
settings.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
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# 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.
#
# settings.py - manage a few key values that aren't super secrets
#
# Goals:
# - Single wallet settings
# - Wear leveling of the flash
# - If no settings are readable, erase flash and start over
#
# Result:
# - up to 4k of values supported (after json encoding)
# - encrypted and stored in SPI flash, in last 128k area
# - AES encryption key is derived from actual wallet secret
# - if logged out, then use fixed key instead (ie. it's public)
# - 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
#
import os
import ujson
import ustruct
import uctypes
import gc
import trezorcrypto
from uio import BytesIO
from uasyncio import sleep_ms
from ubinascii import hexlify as b2a_hex
from utils import to_str
# Base address for internal memory-mapped flash used for settings: 0x81E0000
SETTINGS_FLASH_START = const(0x81E0000)
SETTINGS_FLASH_LENGTH = const(0x20000) # 128K
SETTINGS_FLASH_END = SETTINGS_FLASH_START + SETTINGS_FLASH_LENGTH - 1
DATA_SIZE = const(8192 - 32)
BLOCK_SIZE = const(8192)
# 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) BIP39 seed words exist (else XPRV or master secret based)
# shutdown_timeout = idle timeout period (seconds)
# _revision = internal version number for data - incremented every time the data is saved
# terms_ok = customer has signed-off on the terms of sale
# multisig = list of defined multisig wallets (complex)
# multisig_policy = trust/import/distrust xpubs found in PSBT files
# accounts = array of accounts configured on this device
# screen_brightness = 0 to 100, 999 for automatic
# enable_passphrase = True to show Set Passphrase item in main menu, False to hide it
# backup_quiz = True if backup password quiz was passed; False if not
# These are the data slots available to use. We have 32 slots
# for flash wear leveling.
SLOT_ADDRS = range(SETTINGS_FLASH_START, SETTINGS_FLASH_END, BLOCK_SIZE)
class Settings:
def __init__(self, loop=None, serial=None):
from foundation import SettingsFlash
from common import system # This is defined before Settings is created, so OK to use here
self.loop = loop
self.is_dirty = 0
# AES key is based on the serial number now instead of the PIN
# We don't store anything critical in the settings, so this level of protection is fine,
# and avoids having 2 sets of settings (one with a zero AES key and one with the PIN-based key).
serial = system.get_serial_number()
# print('Settings: serial={}'.format(serial))
self.aes_key = trezorcrypto.sha256(serial).digest()
# print('Settings: aes_key={}'.format(self.aes_key))
self.curr_dict = self.default_values()
self.overrides = {} # volatile overide values
self.flash = SettingsFlash()
self.load()
def get_aes(self, flash_offset):
# Build AES key for en/decrypt of specific block.
# Include the slot number as part of the initial counter (CTR)
return trezorcrypto.aes(trezorcrypto.aes.CTR, self.aes_key, ustruct.pack('<4I', 4, 3, 2, flash_offset))
def load(self):
# Search all slots for any we can read, decrypt them and pick the newest one
from common import system
system.turbo(True)
try:
# reset
self.curr_dict.clear()
self.overrides.clear()
self.addr = 0
self.is_dirty = 0
for addr in SLOT_ADDRS:
# print('Trying to load at {}'.format(hex(addr)))
buf = uctypes.bytearray_at(addr, 4)
if buf[0] == buf[1] == buf[2] == buf[3] == 0xff:
# print(' Slot is ERASED')
# erased (probably)
continue
# check if first 2 bytes makes sense for JSON
flash_offset = (addr - SETTINGS_FLASH_START) // BLOCK_SIZE
aes = self.get_aes(flash_offset)
chk = aes.decrypt(b'{"')
if chk != buf[0:2]:
# doesn't look like JSON, so skip it
# print(' Slot does not contain JSON')
continue
# probably good, so prepare to read it
aes = self.get_aes(flash_offset)
chk = trezorcrypto.sha256()
expect = None
# Our flash is memory mapped, so we read directly by address
buf = uctypes.bytearray_at(addr, DATA_SIZE)
# Get a bytearray for the SHA256 at the end
expected_sha = uctypes.bytearray_at(addr + DATA_SIZE, 32)
# Decrypt and check hash
b = aes.decrypt(buf)
# Add the decrypted result to the SHA
chk.update(b)
try:
# verify hash in last 32 bytes
assert expected_sha == chk.digest()
# FOUNDATION
# loads() can't work from a byte array, and converting to
# bytes here would copy it; better to use file emulation.
# print('json = {}'.format(b))
d = ujson.load(BytesIO(b))
except:
# One in 65k or so chance to come here w/ garbage decoded, so
# not an error.
# print('ERROR? Unable to decode JSON')
continue
curr_revision = d.get('_revision', 0)
if curr_revision > self.curr_dict.get('_revision', -1):
# print('Found candidate JSON: {}'.format(d))
# A newer entry was found
self.curr_dict = d
self.addr = addr
# If we loaded settings, then we're done
if self.addr:
# print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
# print('LOADED SETTINGS! _revision={} addr={}'.format(self.curr_dict.get('_revision'), hex(addr)))
# print('values: {}'.format(to_str(self.curr_dict)))
# print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
system.turbo(False)
return
# print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
# print(' UNABLE TO LOAD SETTINGS: key={}'.format(self.aes_key))
# print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
# If no entries were found, which means this is either the first boot or we have corrupt settings, so raise an exception so we erase and set default
# raise ValueError('Flash is either blank or corrupt, so me must reset to recover to avoid a crash!')
self.curr_dict = self.default_values()
self.overrides.clear()
self.addr = 0
except Exception as e:
# print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
# print('Exception in settings.load(): e={}'.format(e))
# print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
self.reset()
self.is_dirty = True
self.write_out()
system.turbo(False)
def get(self, kn, default=None):
if kn in self.overrides:
return self.overrides.get(kn)
else:
# Special case for xfp and xpub -- make sure they exist and create if not
if kn not in self.curr_dict:
if kn == 'xfp' or kn == 'xpub':
try:
# Update xpub/xfp in settings after creating new wallet
import stash
from common import system
system.show_busy_bar()
with stash.SensitiveValues() as sv:
sv.capture_xpub()
except Exception as e:
# print('ERROR: Cannot create xfp/xpub: e={}'.format(e))
# We tried to create it, but if creation fails, just let the caller handle the error
pass
finally:
system.hide_busy_bar()
# These are overrides, so return them from there
return self.overrides.get(kn)
return self.curr_dict.get(kn, default)
def changed(self):
self.is_dirty += 1
if self.is_dirty < 2 and self.loop:
self.loop.call_later_ms(250, self.write_out())
def set(self, kn, v):
# print('Settings: Set {} to {}'.format(kn, to_str(v)))
if isinstance(v, dict) or self.curr_dict.get(kn, '!~$~!') != v: # So that None can be set
self.curr_dict[kn] = v
self.changed()
def remove(self, kn):
self.curr_dict.pop(kn, None)
# print('Settings: Remove {}'.format(kn))
self.changed()
def set_volatile(self, kn, v):
self.overrides[kn] = v
def reset(self):
# print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
# print(' RESET SETTINGS FLASH')
# print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
self.flash.erase()
self.curr_dict = self.default_values()
self.overrides.clear()
self.addr = 0
self.is_dirty = False
def erase_settings_flash(self):
self.flash.erase()
async def write_out(self):
# delayed write handler
if not self.is_dirty:
# someone beat me to it
return
# Was sometimes running low on memory in this area: recover
try:
import common
# Don't save settings in the demo loop
if not common.demo_active:
gc.collect()
await self.save()
except MemoryError as e:
# NOTE: This would be an infinite async loop if it throws an exception every time -- be aware!
self.loop.call_later_ms(250, self.write_out())
def is_erased(self, addr):
buf = uctypes.bytearray_at(addr, 32)
for i in range(32):
if buf[i] != 0xFF:
return False
return True
def find_first_erased_addr(self):
for addr in SLOT_ADDRS:
buf = uctypes.bytearray_at(addr, 4)
if self.is_erased(addr):
return addr
return 0
# We use chunks sequentially since there is no benefit to randomness here.
def next_addr(self):
# If no entries were found on load, addr will be zero
if self.addr == 0:
addr = self.find_first_erased_addr()
if addr == 0:
# Everything is full, so we must erase and start again
self.flash.erase()
return SETTINGS_FLASH_START
else:
return addr
# Go to next address
if self.addr < SETTINGS_FLASH_START + SETTINGS_FLASH_LENGTH - BLOCK_SIZE:
# Sanity check - if the block we want to write to is not erased, then
# something has gone wrong and we better erase and start again!
if not self.is_erased(self.addr + BLOCK_SIZE):
# print('===============================================================')
# print('UNERASED MEMORY FOUND AT {}'.format(hex(self.addr)))
# print('Aborting save')
# print('===============================================================')
self.flash.erase()
return SETTINGS_FLASH_START
return self.addr + BLOCK_SIZE
# We reached the end of the bank -- we need to erase it so
# the new settings can be written.
# print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
# print(' ERASE WHEN WRAPPING AROUND')
# print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
self.flash.erase()
return SETTINGS_FLASH_START
async def save(self):
from export import auto_backup
# Render as JSON, encrypt and write it
self.curr_dict['_revision'] = self.curr_dict.get('_revision', 0) + 1
addr = self.next_addr()
# print('===============================================================')
# print('SAVING SETTINGS! _revision={} addr={}'.format(self.curr_dict.get('_revision'), hex(addr)))
# print('values to save: {}'.format(to_str(self.curr_dict)))
# print('===============================================================')
flash_offset = (addr - SETTINGS_FLASH_START) // BLOCK_SIZE
aes = self.get_aes(flash_offset)
chk = trezorcrypto.sha256()
# Create the JSON string as bytes
json_buf = ujson.dumps(self.curr_dict).encode('utf8')
# Ensure data is not too big
if len(json_buf) > DATA_SIZE:
# print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
# print(' JSON TOO BIG!')
# print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
assert false, 'JSON data is larger than {}.'.format(DATA_SIZE)
return
# Create a zero-filled byte buf
padded_buf = bytearray(DATA_SIZE)
# Copy the json data into the padded buffer
for i in range(len(json_buf)):
padded_buf[i] = json_buf[i]
del json_buf
# Add the data and padding to the AES and SHA
encrypted_buf = aes.encrypt(padded_buf)
chk.update(padded_buf)
# Build the final buf for writing to flash
save_buf = bytearray(BLOCK_SIZE)
for i in range(len(encrypted_buf)):
save_buf[i] = encrypted_buf[i]
digest = chk.digest()
for i in range(32):
save_buf[DATA_SIZE + i] = digest[i]
# print('addr={}\nbuf={}'.format(hex(addr),b2a_hex(save_buf)))
self.flash.write(addr, save_buf)
# We don't overwrite the old entry here, even though it's now useless, as that can
# cause flash to have ECC errors.
self.addr = addr
self.is_dirty = 0
# print("Settings.save(): wrote @ {}".format(hex(addr)))
def merge(self, prev):
# take a dict of previous values and merge them into what we have
self.curr_dict.update(prev)
@staticmethod
def default_values():
# Please try to avoid defaults here. It's better to put into code
# where value is used, and treat undefined as the default state.
# _schema indicates what version of settings "schema" is in use
# Used to help auto-update code that might run after a firmware update.
return dict(_revision=0, _schema=1)
# EOF