forked from Foundation-Devices/passport-firmware
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathstash.py
257 lines (202 loc) · 8.35 KB
/
stash.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
# 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.
#
# stash.py - encoding the ultrasecrets: bip39 seeds and words
#
# references:
# - <https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki>
# - <https://iancoleman.io/bip39/#english>
# - zero values:
# - 'abandon' * 23 + 'art'
# - 'abandon' * 17 + 'agent'
# - 'abandon' * 11 + 'about'
#
import trezorcrypto, uctypes, gc
from pincodes import SE_SECRET_LEN
def blank_object(item):
# Use/abuse uctypes to blank objects until python. Will likely
# even work on immutable types, so be careful. Also works
# well to kill references to sensitive values (but not copies).
#
if isinstance(item, (bytearray, bytes, str)):
addr, ln = uctypes.addressof(item), len(item)
buf = uctypes.bytearray_at(addr, ln)
for i in range(ln):
buf[i] = 0
elif isinstance(item, trezorcrypto.bip32.HDNode):
pass
# item.blank() # node.blank() elsewhere
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
class SecretStash:
@staticmethod
def encode(seed_bits=None, master_secret=None, xprv=None):
nv = bytearray(SE_SECRET_LEN)
if seed_bits:
# typical: seed bits without checksum bits
vlen = len(seed_bits)
# TODO: Do we support all of these?s
assert vlen in [16, 24, 32]
nv[0] = 0x80 | ((vlen // 8) - 2)
nv[1:1+vlen] = seed_bits
elif master_secret:
# between 128 and 512 bits of master secret for BIP32 key derivation
vlen = len(master_secret)
assert 16 <= vlen <= 64
nv[0] = vlen
nv[1:1+vlen] = master_secret
elif xprv:
# master xprivkey, which could be a subkey of something we don't know
# - we record only the minimum
assert isinstance(xprv, trezorcrypto.bip32.HDNode)
nv[0] = 0x01
nv[1:33] = xprv.chain_code()
nv[33:65] = xprv.private_key()
return nv
@staticmethod
def decode(secret, _bip39pw=''):
# expecting 72-bytes of secret payload; decode meaning
# returns:
# type, secrets bytes, HDNode(root)
#
marker = secret[0]
if marker == 0x01:
# xprv => BIP32 private key values
ch, pk = secret[1:33], secret[33:65]
assert not _bip39pw
return 'xprv', ch+pk, trezorcrypto.bip32.HDNode(chain_code=ch, private_key=pk,
child_num=0, depth=0, fingerprint=0)
if marker & 0x80:
# seed phrase
ll = ((marker & 0x3) + 2) * 8
# note:
# - byte length > number of words
# - not storing checksum
assert ll in [16, 24, 32]
# make master secret, using the memonic words, and passphrase (or empty string)
seed_bits = secret[1:1+ll]
ms = trezorcrypto.bip39.seed(trezorcrypto.bip39.from_data(seed_bits), _bip39pw)
hd = trezorcrypto.bip32.from_seed(ms, 'secp256k1')
return 'words', seed_bits, hd
else:
# variable-length master secret for BIP32
vlen = secret[0]
assert 16 <= vlen <= 64
assert not _bip39pw
ms = secret[1:1+vlen]
hd = trezorcrypto.bip32.from_seed(ms, 'secp256k1')
return 'master', ms, hd
# optional global value: user-supplied passphrase to salt BIP39 seed process
bip39_passphrase = ''
bip39_hash = ''
class SensitiveValues:
# be a context manager, and holder to secrets in-memory
def __init__(self, secret=None, for_backup=False):
from common import system
if secret is None:
# fetch the secret from bootloader/atecc508a
from common 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 = []
# backup during volatile bip39 encryption: do not use passphrase
self._bip39pw = '' if for_backup else str(bip39_passphrase)
# print('self._bip39pw={}'.format(self._bip39pw))
def __enter__(self):
import chains
self.mode, self.raw, self.node = SecretStash.decode(self.secret, self._bip39pw)
self.spots.append(self.node)
self.spots.append(self.raw)
self.chain = chains.current_chain()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# Clear secrets from memory ... yes, they could have been
# copied elsewhere, but in normal case, at least we blanked them.
for item in self.spots:
blank_object(item)
if hasattr(self, 'secret'):
# will be blanked from above
del self.secret
if hasattr(self, 'node'):
# specialized blanking code already above
del self.node
# just in case this holds some pointers?
del self.spots
# .. and some GC will help too!
gc.collect()
if exc_val:
# An exception happened, but we've done cleanup already now, so
# not a big deal. Cause it be raised again.
return False
return True
def get_xfp(self):
return self.node.my_fingerprint()
def capture_xpub(self):
# track my xpubkey fingerprint & value in settings (not sensitive really)
# - we share these on any USB connection
import common
from common import settings
# # Set the master values if no account selected yet
# if common.active_account:
# # Derive xfp and xpub based on the current active account
# # The BIP39 passphrase is already taken into account by SensitiveValues
# # print('deriving from path: {}'.format(common.active_account.deriv_path))
# if not common.active_account.deriv_path:
# return
#
# node = self.derive_path(common.active_account.deriv_path)
#
# xfp = node.my_fingerprint()
# print('capture_xpub(): xfp={}'.format(hex(xfp)))
# xpub = self.chain.serialize_public(node, common.active_account.addr_type)
# print('capture_xpub(): xpub={}'.format(xpub))
# else:
xfp = self.node.my_fingerprint()
# print('capture_xpub(): xfp={}'.format(hex(xfp)))
xpub = self.chain.serialize_public(self.node)
# print('capture_xpub(): xpub={}'.format(xpub))
# Always store these volatile - Takes less than 1 second to recreate, and it will change whenever
# a passphrase is entered, so no need to waste flash cycles on storing it.
settings.set_volatile('xfp', xfp)
settings.set_volatile('xpub', xpub)
settings.set_volatile('chain', self.chain.ctype)
settings.set('words', (self.mode == 'words'))
def register(self, item):
# Caller can add his own sensitive (derived?) data to our wiper
# typically would be byte arrays or byte strings, but also
# supports bip32 nodes
self.spots.append(item)
def derive_path(self, path, master=None, register=True):
# Given a string path, derive the related subkey
rv = (master or self.node).clone()
if register:
self.register(rv)
for i in path.split('/'):
if i == 'm': continue
if not i: continue # trailing or duplicated slashes
if i[-1] == "'":
assert len(i) >= 2, i
here = int(i[:-1])
assert 0 <= here < 0x80000000, here
here |= 0x80000000
else:
here = int(i)
assert 0 <= here < 0x80000000, here
rv.derive(here)
return rv
# EOF