Skip to content

Commit 20ff492

Browse files
add bip85 app (#265)
* add bip85 app * save to SD, load mnemonic to device
1 parent 02afbf4 commit 20ff492

File tree

7 files changed

+213
-17
lines changed

7 files changed

+213
-17
lines changed

src/apps/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
"backup", # creates and loads backups (only loads for now)
88
"blindingkeys", # blinding keys for liquid wallets
99
"compatibility", # compatibility layer that converts json/files to Specter format
10+
"bip85", # bip85 derivation of new mnemonics, xprvs etc
1011
]

src/apps/bip85.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import lvgl as lv
2+
from binascii import hexlify
3+
from io import BytesIO
4+
5+
from app import BaseApp, AppError
6+
from embit import bip85
7+
from gui.common import add_button, add_button_pair, align_button_pair
8+
from gui.decorators import on_release
9+
from gui.screens import Menu, NumericScreen, QRAlert, Alert
10+
from gui.screens.mnemonic import MnemonicScreen
11+
from helpers import SDCardFile
12+
13+
class QRWithSD(QRAlert):
14+
SAVE = 1
15+
def __init__(self, *args, **kwargs):
16+
super().__init__(*args, **kwargs)
17+
# add save button
18+
btn = add_button("Save to SD card", on_release(self.save), scr=self)
19+
btn.align(self.close_button, lv.ALIGN.OUT_TOP_MID, 0, -20)
20+
21+
def save(self):
22+
self.set_value(self.SAVE)
23+
24+
class Bip85MnemonicScreen(MnemonicScreen):
25+
QR = 1
26+
SD = 2
27+
LOAD = 3
28+
def __init__(self, *args, **kwargs):
29+
super().__init__(*args, **kwargs)
30+
self.load_btn = add_button(
31+
text="Use now (load to device)",
32+
scr=self,
33+
callback=on_release(self.load)
34+
)
35+
self.load_btn.align(self.table, lv.ALIGN.OUT_BOTTOM_MID, 0, 10)
36+
self.show_qr_btn, self.save_sd_btn = add_button_pair(
37+
text1="Show QR code",
38+
callback1=on_release(self.show_qr),
39+
text2="Save to SD card",
40+
callback2=on_release(self.save_sd),
41+
scr=self,
42+
)
43+
self.show_qr_btn.align(self.load_btn, lv.ALIGN.OUT_BOTTOM_MID, 0, 10)
44+
self.save_sd_btn.align(self.load_btn, lv.ALIGN.OUT_BOTTOM_MID, 0, 10)
45+
align_button_pair(self.show_qr_btn, self.save_sd_btn)
46+
47+
def show_qr(self):
48+
self.set_value(self.QR)
49+
50+
def save_sd(self):
51+
self.set_value(self.SD)
52+
53+
def load(self):
54+
self.set_value(self.LOAD)
55+
56+
class App(BaseApp):
57+
"""
58+
WalletManager class manages your wallets.
59+
It stores public information about the wallets
60+
in the folder and signs it with keystore's id key
61+
"""
62+
63+
button = "Deterministic derivation (BIP-85)"
64+
name = "bip85"
65+
66+
async def menu(self, show_screen):
67+
buttons = [
68+
(None, "Mnemonics"),
69+
(0, "12-word mnemonic"),
70+
(1, "18-word mnemonic"),
71+
(2, "24-word mnemonic"),
72+
(None, "Other stuff"),
73+
(3, "WIF key (single private key)"),
74+
(4, "Master private key (xprv)"),
75+
(5, "Raw entropy (16-64 bytes)"),
76+
]
77+
78+
# wait for menu selection
79+
menuitem = await show_screen(
80+
Menu(
81+
buttons,
82+
last=(255, None),
83+
title="What do you want to derive?",
84+
note="",
85+
)
86+
)
87+
88+
# process the menu button:
89+
# back button
90+
if menuitem == 255:
91+
return False
92+
# get derivation index
93+
index = await show_screen(
94+
NumericScreen(title="Enter derivation index", note="Default: 0")
95+
)
96+
if index is None:
97+
return True # stay in the menu
98+
if index == "":
99+
index = 0
100+
index = int(index)
101+
note = "index: %d" % index
102+
103+
fgp = hexlify(self.keystore.fingerprint).decode()
104+
# mnemonic menu items
105+
if menuitem >= 0 and menuitem <=2:
106+
num_words = 12+6*menuitem
107+
mnemonic = bip85.derive_mnemonic(
108+
self.keystore.root, num_words=num_words, index=index
109+
)
110+
title = "Derived %d-word mnemonic" % num_words
111+
action = await show_screen(
112+
Bip85MnemonicScreen(mnemonic=mnemonic, title=title, note=note)
113+
)
114+
if action == Bip85MnemonicScreen.QR:
115+
await show_screen(
116+
QRAlert(title=title, message=mnemonic, note=note)
117+
)
118+
elif action == Bip85MnemonicScreen.SD:
119+
fname = "bip85-%s-mnemonic-%d-%d.txt" % (
120+
fgp, num_words, index
121+
)
122+
with SDCardFile(fname, "w") as f:
123+
f.write(mnemonic)
124+
await show_screen(
125+
Alert(
126+
title="Success",
127+
message="Mnemonic is saved as\n\n%s" % fname,
128+
button_text="Close",
129+
)
130+
)
131+
elif action == Bip85MnemonicScreen.LOAD:
132+
await self.communicate(
133+
BytesIO(b"set_mnemonic "+mnemonic.encode()), app="",
134+
)
135+
return False
136+
return True
137+
# other stuff
138+
if menuitem == 3:
139+
title = "Derived private key"
140+
res = bip85.derive_wif(self.keystore.root, index)
141+
file_suffix = "wif"
142+
elif menuitem == 4:
143+
title = "Derived master private key"
144+
res = bip85.derive_xprv(self.keystore.root, index)
145+
file_suffix = "xprv"
146+
elif menuitem == 5:
147+
num_bytes = await show_screen(
148+
NumericScreen(
149+
title="Number of bytes to generate",
150+
note="16 <= N <= 64. Default: 32",
151+
)
152+
)
153+
if num_bytes is None:
154+
return True
155+
if num_bytes == "":
156+
num_bytes = 32
157+
num_bytes = int(num_bytes)
158+
if num_bytes < 16 or num_bytes > 64:
159+
raise AppError("Only 16-64 bytes can be generated with BIP-85")
160+
title = "Derived %d-byte entropy" % num_bytes
161+
raw = bip85.derive_hex(self.keystore.root, num_bytes, index)
162+
res = hexlify(raw).decode()
163+
file_suffix = "hex-%d" % num_bytes
164+
else:
165+
raise NotImplementedError("Not implemented")
166+
res = str(res)
167+
action = await show_screen(
168+
QRWithSD(title=title, message=res, note=note)
169+
)
170+
if action == QRWithSD.SAVE:
171+
fname = "bip85-%s-%s-%d.txt" % (fgp, file_suffix, index)
172+
with SDCardFile(fname, "w") as f:
173+
f.write(res)
174+
await show_screen(
175+
Alert(
176+
title="Success",
177+
message="Data is saved as\n\n%s" % fname,
178+
button_text="Close",
179+
)
180+
)
181+
return True
182+

src/gui/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ def align_button_pair(btn1, btn2):
169169
w = (HOR_RES - 3 * PADDING) // 2
170170
btn1.set_width(w)
171171
btn2.set_width(w)
172+
btn1.set_x(PADDING)
172173
btn2.set_x(HOR_RES // 2 + PADDING // 2)
173174

174175

src/gui/screens/input.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,12 +414,15 @@ class NumericScreen(Screen):
414414
def __init__(
415415
self,
416416
title="Enter account number",
417+
note=None,
417418
current_val='0'
418419
):
419420
super().__init__()
421+
if note is None:
422+
note = "Current account number: %s" % current_val
420423
self.title = add_label(title, scr=self, y=PADDING, style="title")
421424

422-
self.note = add_label("Current account number: %s" % current_val, scr=self, style="hint")
425+
self.note = add_label(note, scr=self, style="hint")
423426
self.note.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 5)
424427

425428
self.kb = lv.btnm(self)

src/gui/specter.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
RecoverMnemonicScreen,
88
DevSettings,
99
)
10-
import rng
1110
import asyncio
1211

1312

src/keystore/ram.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
from .core import KeyStore, KeyStoreError, PinError
1+
from .core import KeyStore, KeyStoreError
22
from platform import CriticalErrorWipeImmediately
33
import platform
44
from rng import get_random_bytes
5-
import hashlib
65
import hmac
76
from embit import ec, bip39, bip32
87
from embit.liquid import slip77

src/specter.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,8 @@
66

77
from platform import (
88
CriticalErrorWipeImmediately,
9-
set_usb_mode,
109
reboot,
11-
fpath,
1210
maybe_mkdir,
13-
file_exists,
1411
wipe,
1512
get_version,
1613
get_battery_status,
@@ -23,7 +20,7 @@
2320
from gui.screens.mnemonic import MnemonicPrompt
2421

2522
# small helper functions
26-
from helpers import gen_mnemonic, fix_mnemonic, load_apps, is_liquid
23+
from helpers import gen_mnemonic, fix_mnemonic
2724
from errors import BaseError
2825

2926

@@ -143,7 +140,7 @@ async def host_exception_handler(self, e):
143140
b = BytesIO()
144141
sys.print_exception(e, b)
145142
msg = b.getvalue().decode()
146-
res = await self.gui.error(msg, popup=True)
143+
await self.gui.error(msg, popup=True)
147144

148145
async def main(self):
149146
while True:
@@ -166,6 +163,22 @@ def init_apps(self):
166163
app.init(self.keystore, self.network, self.gui.show_loader, self.cross_app_communicate)
167164

168165
async def cross_app_communicate(self, stream, app:str=None, show_fn=None):
166+
if app == "": # root
167+
data = stream.read()
168+
if data.startswith(b"set_mnemonic "):
169+
mnemonic = data[len("set_mnemonic "):].decode()
170+
confirm = await self.gui.prompt(
171+
"Load new mnemonic?",
172+
"\nApp requested to load new mnemonic\n"
173+
"Do you want to continue?\n\n"
174+
"You will need to reboot the device to get back"
175+
" to your current mnemonic.",
176+
)
177+
if confirm:
178+
return self.set_mnemonic(mnemonic)
179+
else:
180+
return True
181+
raise SpecterError("Invalid command '%s'" % data)
169182
return await self.process_host_request(stream, popup=False, appname=app, show_fn=show_fn)
170183

171184
async def initmenu(self):
@@ -194,20 +207,15 @@ async def initmenu(self):
194207
mnemonic = await self.gui.new_mnemonic(gen_mnemonic, bip39.WORDLIST, fix_mnemonic)
195208
if mnemonic is not None:
196209
# load keys using mnemonic and empty password
197-
self.keystore.set_mnemonic(mnemonic.strip(), "")
198-
self.init_apps()
199-
return self.mainmenu
210+
return self.set_mnemonic(mnemonic, "")
200211
# recover
201212
elif menuitem == 1:
202213
mnemonic = await self.gui.recover(
203214
bip39.mnemonic_is_valid, bip39.find_candidates, fix_mnemonic
204215
)
205216
if mnemonic is not None:
206217
# load keys using mnemonic and empty password
207-
self.keystore.set_mnemonic(mnemonic, "")
208-
self.init_apps()
209-
self.current_menu = self.mainmenu
210-
return self.mainmenu
218+
return self.set_mnemonic(mnemonic, "")
211219
elif menuitem == 2:
212220
# try to load key, if user cancels -> return
213221
res = await self.keystore.load_mnemonic()
@@ -256,7 +264,10 @@ async def import_mnemonic(self):
256264
# confirm mnemonic
257265
if not await self.gui.show_screen()(scr):
258266
return
259-
self.keystore.set_mnemonic(mnemonic, "")
267+
return self.set_mnemonic(mnemonic, "")
268+
269+
def set_mnemonic(self, mnemonic, password=""):
270+
self.keystore.set_mnemonic(mnemonic.strip(), password)
260271
self.init_apps()
261272
self.current_menu = self.mainmenu
262273
return self.mainmenu

0 commit comments

Comments
 (0)