Skip to content

Commit

Permalink
add minimal backup app, minor keystorage improvements for multi-seed
Browse files Browse the repository at this point in the history
  • Loading branch information
stepansnigirev committed Jun 17, 2021
1 parent 40e79dc commit a2e89e3
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 33 deletions.
1 change: 1 addition & 0 deletions src/apps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
"signmessage", # adds bitcoin message signing functionality
"getrandom", # allows to query random bytes from on-board TRNG
"label", # allows settings and getting a label for device
"backup", # creates and loads backups (only loads for now)
]
35 changes: 35 additions & 0 deletions src/apps/backup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""
Backup app - can load secrets from a text file or qr code that starts with
bip39: <recovery phrase>
"""
from app import BaseApp, AppError
from io import BytesIO
from binascii import hexlify
from rng import get_random_bytes
from bitcoin import bip39
from gui.screens import Prompt
from gui.components.mnemonic import MnemonicTable
import lvgl as lv

# Should be called App if you use a single file
class App(BaseApp):
"""Allows to load mnemonic from text file / QR code"""
name = "backup"
prefixes = [b"bip39:"]

async def process_host_command(self, stream, show_fn):
# reads prefix from the stream (until first space)
prefix = self.get_prefix(stream)
if prefix not in self.prefixes:
# WTF? It's not our data...
raise AppError("Prefix is not valid: %s" % prefix.decode())
mnemonic = stream.read().strip().decode()
if not bip39.mnemonic_is_valid(mnemonic):
raise AppError("Invalid mnemonic!")
scr = Prompt("Load this mnemonic to memory?", "Mnemonic:")
table = MnemonicTable(scr)
table.align(scr.message, lv.ALIGN.OUT_BOTTOM_MID, 0, 30)
table.set_mnemonic(mnemonic)
if await show_fn(scr):
self.keystore.set_mnemonic(mnemonic)
return None
2 changes: 1 addition & 1 deletion src/hosts/sd.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class SDHost(Host):
- saving signed transaction to the card
"""

button = "SD card"
button = "Open SD card file"
settings_button = "SD card"

def __init__(self, path, sdpath=fpath("/sd")):
Expand Down
74 changes: 42 additions & 32 deletions src/keystore/sdcard.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,38 +58,47 @@ async def get_keypath(self, title="Select media", only_if_exist=True, **kwargs):
res = await self.show(scr)
return res

async def save_mnemonic(self, filename, path=None):
async def save_mnemonic(self):
if self.is_locked:
raise KeyStoreError("Keystore is locked")
if self.mnemonic is None:
raise KeyStoreError("Recovery phrase is not loaded")

path = await self.get_keypath(
title="Where to save?", only_if_exist=False, note="Select media"
)
if path is None:
path = await self.get_keypath(
title="Where to save?", only_if_exist=False, note="Select media"
)
if path is None:
return False
if path == self.sdpath:
return
filename = await self.get_input(suggestion=self.mnemonic.split()[0])
if filename is None:
return

fullpath = "%s/%s.%s" % (path, self.fileprefix(path), filename)

if fullpath.startswith(self.sdpath):
if not platform.is_sd_present():
raise KeyStoreError("SD card is not present")
platform.mount_sdcard()

if platform.file_exists("%s/%s.%s" % (path, self.fileprefix(path), filename)):
if platform.file_exists(fullpath):
scr = Prompt(
"\n\nFile already exists: %s\n" % filename,
"Would you like to overwrite this file?",
)
res = await self.show(scr)
if res is False:
return None
if fullpath.startswith(self.sdpath):
platform.unmount_sdcard()
return

self.save_aead("%s/%s.%s" % (path, self.fileprefix(path), filename), plaintext=self.mnemonic.encode(),
self.save_aead(fullpath, plaintext=self.mnemonic.encode(),
key=self.enc_secret)
if path == self.sdpath:
if fullpath.startswith(self.sdpath):
platform.unmount_sdcard()
# check it's ok
await self.load_mnemonic(path, "%s.%s" % (self.fileprefix(path), filename))
return True
await self.load_mnemonic(fullpath)
# return the full file name incl. prefix if saved to SD card, just the name if on flash
return fullpath.split("/")[-1] if fullpath.startswith(self.sdpath) else filename

@property
def is_key_saved(self):
Expand Down Expand Up @@ -119,11 +128,14 @@ async def load_mnemonic(self, file=None):
if file is None:
return False

if file.startswith(self.sdpath) and platform.is_sd_present():
platform.mount_sdcard()

if not platform.file_exists(file):
raise KeyStoreError("Key is not saved")
_, data = self.load_aead(file, self.enc_secret)

if platform.is_sd_present():
if file.startswith(self.sdpath) and platform.is_sd_present():
platform.unmount_sdcard()
self.set_mnemonic(data.decode(), "")
return True
Expand All @@ -139,15 +151,15 @@ async def select_file(self):
if platform.is_sd_present():
platform.mount_sdcard()
buttons += self.load_files(self.sdpath)
platform.unmount_sdcard()
else:
buttons += [('None', 'No SD card present')]
buttons += [(None, 'No SD card present')]

return await self.show(Menu(buttons, title="Select a file", last=(None, "Cancel")))

def load_files(self, path):
buttons = []
files = sum(
[[f[0] for f in os.ilistdir(path) if f[0].lower().startswith(self.fileprefix(path))]], [])
files = [f[0] for f in os.ilistdir(path) if f[0].startswith(self.fileprefix(path))]

if len(files) == 0:
buttons += [(None, 'No files found')]
Expand All @@ -167,16 +179,17 @@ async def delete_mnemonic(self):
file = await self.select_file()
if file is None:
return False

if not platform.file_exists(file):
raise KeyStoreError("File not found.")
try:
if platform.is_sd_present() and file.startswith(self.sdpath):
platform.mount_sdcard()
os.remove(file)
except Exception as e:
print(e)
raise KeyStoreError("Failed to delete file '%s'" % file)
finally:
if platform.is_sd_present():
if platform.is_sd_present() and file.startswith(self.sdpath):
platform.unmount_sdcard()
return True

Expand All @@ -195,6 +208,10 @@ async def export_mnemonic(self):
if await self.show(Prompt("Warning",
"You need to confirm your PIN code "
"to export your recovery phrase.\n\n"
"Your recovery phrase will be saved "
"to the SD card as plain text.\n\n"
"Anybody who has access to this SD card "
"will be able to read your recovery phrase!\n\n"
"Continue?")):
self.lock()
await self.unlock()
Expand All @@ -207,7 +224,8 @@ async def export_mnemonic(self):

platform.mount_sdcard()

with open(filepath, "wb") as f:
with open(filepath, "w") as f:
f.write("bip39: ")
f.write(self.mnemonic)

platform.unmount_sdcard()
Expand All @@ -227,8 +245,8 @@ async def storage_menu(self):
(2, "Delete key"),
]

if platform.is_sd_present():
buttons.append((3, "Export recovery phrase to SD"))
# disabled if SD card is not present
buttons.append((3, "Export recovery phrase to SD", platform.is_sd_present()))

# we stay in this menu until back is pressed
while True:
Expand All @@ -239,16 +257,8 @@ async def storage_menu(self):
if menuitem == 255:
return
elif menuitem == 0:
filename = await self.get_input(suggestion=self.mnemonic.split()[0])
if filename is None:
return
if filename is "":
await self.show(
Alert("Error!", "Please provide a valid name!\n\nYour file has NOT been saved.",
button_text="OK")
)
return
if await self.save_mnemonic(filename=filename):
filename = await self.save_mnemonic()
if filename:
await self.show(
Alert("Success!", "Your key is stored now.\n\nName: %s" % filename, button_text="OK")
)
Expand Down

0 comments on commit a2e89e3

Please sign in to comment.