-
Notifications
You must be signed in to change notification settings - Fork 145
/
stash.py
240 lines (184 loc) · 7.42 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
# (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 tcc, uctypes, gc
from pincodes import AE_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, tcc.bip32.HDNode):
item.blank()
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_phrase=None, master_secret=None, xprv=None):
nv = bytearray(AE_SECRET_LEN)
if seed_phrase:
# typical: packed version of memonic phrase
vlen = len(seed_phrase)
assert vlen in [16, 24, 32]
nv[0] = 0x80 | ((vlen // 8) - 2)
nv[1:1+vlen] = seed_phrase
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, tcc.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, tcc.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 = tcc.bip39.seed(tcc.bip39.from_data(seed_bits), _bip39pw)
hd = tcc.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 = tcc.bip32.from_seed(ms, 'secp256k1')
return 'master', ms, hd
# optional global value: user-supplied passphrase to salt BIP39 seed process
bip39_passphrase = ''
class SensitiveValues:
# be a context manager, and holder to secrets in-memory
def __init__(self, secret=None, for_backup=False):
if secret is None:
# fetch the secret from bootloader/atecc508a
from main import pa
if pa.is_secret_blank():
raise ValueError('no secrets yet')
self.secret = pa.fetch()
else:
# sometimes we already know it
assert set(secret) != {0}
self.secret = secret
# backup during volatile bip39 encryption: do not use passphrase
self._bip39pw = '' if for_backup else str(bip39_passphrase)
def __enter__(self):
import chains
self.mode, self.raw, self.node = SecretStash.decode(self.secret, self._bip39pw)
self.chain = chains.current_chain()
self.spots = [ self.secret, self.node, self.raw ]
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
assert self.secret == bytes(AE_SECRET_LEN)
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 capture_xpub(self):
# track my xpubkey fingerprint & value in settings (not sensitive really)
# - we share these on any USB connection
from main import settings
# Implicit in the values is the BIP39 encryption passphrase,
# which we might not want to actually store.
xfp = self.node.my_fingerprint()
xpub = self.chain.serialize_public(self.node)
if self._bip39pw:
settings.put_volatile('xfp', xfp)
settings.put_volatile('xpub', xpub)
else:
settings.overrides.clear()
settings.put('xfp', xfp)
settings.put('xpub', xpub)
settings.put('chain', self.chain.ctype)
settings.put('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]) | 0x80000000
else:
here = int(i)
assert 0 <= here < 0x80000000, here
rv.derive(here)
return rv
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'")
# clear the parent linkage by rebuilding it.
cc, pk = dirty.chain_code(), dirty.private_key()
self.register(cc)
self.register(pk)
rv = tcc.bip32.HDNode(chain_code=cc, private_key=pk,
child_num=0, depth=0, fingerprint=0)
self.register(rv)
return rv
# EOF