parent
49d6a35249
commit
80f8f7900d
@ -0,0 +1,35 @@
|
||||
from typing import *
|
||||
|
||||
|
||||
# extmod/modtrezorcrypto/modtrezorcrypto-slip39.h
|
||||
def compute_mask(prefix: int) -> int:
|
||||
"""
|
||||
Calculates which buttons still can be pressed after some already were.
|
||||
Returns a 9-bit bitmask, where each bit specifies which buttons
|
||||
can be further pressed (there are still words in this combination).
|
||||
LSB denotes first button.
|
||||
Example: 110000110 - second, third, eighth and ninth button still can be
|
||||
pressed.
|
||||
"""
|
||||
|
||||
|
||||
# extmod/modtrezorcrypto/modtrezorcrypto-slip39.h
|
||||
def button_sequence_to_word(prefix: int) -> str:
|
||||
"""
|
||||
Finds the first word that fits the given button prefix.
|
||||
"""
|
||||
|
||||
|
||||
# extmod/modtrezorcrypto/modtrezorcrypto-slip39.h
|
||||
def word_index(word: str) -> int:
|
||||
"""
|
||||
Finds index of given word.
|
||||
Raises ValueError if not found.
|
||||
"""
|
||||
|
||||
|
||||
# extmod/modtrezorcrypto/modtrezorcrypto-slip39.h
|
||||
def get_word(index: int) -> str:
|
||||
"""
|
||||
Returns word on position 'index'.
|
||||
"""
|
@ -1,56 +0,0 @@
|
||||
from trezor import ui, workflow
|
||||
from trezor.crypto import bip39
|
||||
|
||||
from apps.common import storage
|
||||
|
||||
TYPE_BIP39 = 0
|
||||
|
||||
|
||||
def get() -> (bytes, int):
|
||||
mnemonic_secret = storage.get_mnemonic_secret()
|
||||
mnemonic_type = storage.get_mnemonic_type() or TYPE_BIP39
|
||||
return mnemonic_secret, mnemonic_type
|
||||
|
||||
|
||||
def get_seed(passphrase: str = "", progress_bar=True):
|
||||
secret, mnemonic_type = get()
|
||||
if mnemonic_type == TYPE_BIP39:
|
||||
module = bip39
|
||||
if progress_bar:
|
||||
_start_progress()
|
||||
result = module.seed(secret.decode(), passphrase, _render_progress)
|
||||
_stop_progress()
|
||||
else:
|
||||
result = module.seed(secret.decode(), passphrase)
|
||||
return result
|
||||
|
||||
|
||||
def process(mnemonics: list, mnemonic_type: int):
|
||||
if mnemonic_type == TYPE_BIP39:
|
||||
return mnemonics[0].encode()
|
||||
else:
|
||||
raise RuntimeError("Unknown mnemonic type")
|
||||
|
||||
|
||||
def restore() -> str:
|
||||
secret, mnemonic_type = get()
|
||||
if mnemonic_type == TYPE_BIP39:
|
||||
return secret.decode()
|
||||
|
||||
|
||||
def _start_progress():
|
||||
ui.backlight_fade(ui.BACKLIGHT_DIM)
|
||||
ui.display.clear()
|
||||
ui.header("Please wait")
|
||||
ui.display.refresh()
|
||||
ui.backlight_fade(ui.BACKLIGHT_NORMAL)
|
||||
|
||||
|
||||
def _render_progress(progress: int, total: int):
|
||||
p = 1000 * progress // total
|
||||
ui.display.loader(p, False, 18, ui.WHITE, ui.BG)
|
||||
ui.display.refresh()
|
||||
|
||||
|
||||
def _stop_progress():
|
||||
workflow.restartdefault()
|
@ -0,0 +1,59 @@
|
||||
from micropython import const
|
||||
|
||||
from trezor import ui, wire
|
||||
from trezor.crypto.hashlib import sha256
|
||||
from trezor.messages.Success import Success
|
||||
from trezor.utils import consteq
|
||||
|
||||
from . import bip39, slip39
|
||||
|
||||
from apps.common import storage
|
||||
|
||||
TYPE_BIP39 = const(0)
|
||||
TYPE_SLIP39 = const(1)
|
||||
|
||||
TYPES_WORD_COUNT = {12: bip39, 18: bip39, 24: bip39, 20: slip39, 33: slip39}
|
||||
|
||||
|
||||
def get() -> (bytes, int):
|
||||
mnemonic_secret = storage.get_mnemonic_secret()
|
||||
mnemonic_type = storage.get_mnemonic_type() or TYPE_BIP39
|
||||
return mnemonic_secret, mnemonic_type
|
||||
|
||||
|
||||
def get_seed(passphrase: str = ""):
|
||||
mnemonic_secret, mnemonic_type = get()
|
||||
if mnemonic_type == TYPE_BIP39:
|
||||
return bip39.get_seed(mnemonic_secret, passphrase)
|
||||
elif mnemonic_type == TYPE_SLIP39:
|
||||
return slip39.get_seed(mnemonic_secret, passphrase)
|
||||
|
||||
|
||||
def dry_run(secret: bytes):
|
||||
digest_input = sha256(secret).digest()
|
||||
stored, _ = get()
|
||||
digest_stored = sha256(stored).digest()
|
||||
if consteq(digest_stored, digest_input):
|
||||
return Success(message="The seed is valid and matches the one in the device")
|
||||
else:
|
||||
raise wire.ProcessError(
|
||||
"The seed is valid but does not match the one in the device"
|
||||
)
|
||||
|
||||
|
||||
def module_from_words_count(count: int):
|
||||
return TYPES_WORD_COUNT[count]
|
||||
|
||||
|
||||
def _start_progress():
|
||||
ui.backlight_fade(ui.BACKLIGHT_DIM)
|
||||
ui.display.clear()
|
||||
ui.header("Please wait")
|
||||
ui.display.refresh()
|
||||
ui.backlight_fade(ui.BACKLIGHT_NORMAL)
|
||||
|
||||
|
||||
def _render_progress(progress: int, total: int):
|
||||
p = 1000 * progress // total
|
||||
ui.display.loader(p, False, 18, ui.WHITE, ui.BG)
|
||||
ui.display.refresh()
|
@ -0,0 +1,36 @@
|
||||
from trezor.crypto import bip39
|
||||
|
||||
from apps.common import mnemonic, storage
|
||||
|
||||
|
||||
def get_type():
|
||||
return mnemonic.TYPE_BIP39
|
||||
|
||||
|
||||
def process_single(mnemonic: str) -> bytes:
|
||||
"""
|
||||
Receives single mnemonic and processes it. Returns what is then stored in storage or
|
||||
None if more shares are needed.
|
||||
"""
|
||||
return mnemonic.encode()
|
||||
|
||||
|
||||
def process_all(mnemonics: list) -> bytes:
|
||||
"""
|
||||
Receives all mnemonics (just one in case of BIP-39) and processes it into a master
|
||||
secret which is usually then stored in storage.
|
||||
"""
|
||||
return mnemonics[0].encode()
|
||||
|
||||
|
||||
def store(secret: bytes, needs_backup: bool, no_backup: bool):
|
||||
storage.store_mnemonic(secret, mnemonic.TYPE_BIP39, needs_backup, no_backup)
|
||||
|
||||
|
||||
def get_seed(secret: bytes, passphrase: str):
|
||||
mnemonic._start_progress()
|
||||
return bip39.seed(secret.decode(), passphrase, mnemonic._render_progress)
|
||||
|
||||
|
||||
def check(secret: bytes):
|
||||
return bip39.check(secret)
|
@ -0,0 +1,87 @@
|
||||
from trezor.crypto import slip39
|
||||
|
||||
from apps.common import mnemonic, storage
|
||||
|
||||
|
||||
def generate_from_secret(master_secret: bytes, count: int, threshold: int) -> str:
|
||||
"""
|
||||
Generates new Shamir backup for 'master_secret'. Multiple groups are not yet supported.
|
||||
"""
|
||||
identifier, group_mnemonics = slip39.generate_single_group_mnemonics_from_data(
|
||||
master_secret, threshold, count
|
||||
)
|
||||
storage.set_slip39_iteration_exponent(slip39.DEFAULT_ITERATION_EXPONENT)
|
||||
storage.set_slip39_identifier(identifier)
|
||||
return group_mnemonics
|
||||
|
||||
|
||||
def get_type():
|
||||
return mnemonic.TYPE_SLIP39
|
||||
|
||||
|
||||
def process_single(mnemonic: str) -> bytes:
|
||||
"""
|
||||
Receives single mnemonic and processes it. Returns what is then stored in storage or
|
||||
None if more shares are needed.
|
||||
"""
|
||||
identifier, iteration_exponent, _, _, _, index, threshold, value = slip39.decode_mnemonic(
|
||||
mnemonic
|
||||
) # TODO: use better data structure for this
|
||||
if threshold == 1:
|
||||
raise ValueError("Threshold equal to 1 is not allowed.")
|
||||
|
||||
# if recovery is not in progress already, start it and wait for more mnemonics
|
||||
if not storage.is_slip39_in_progress():
|
||||
storage.set_slip39_in_progress(True)
|
||||
storage.set_slip39_iteration_exponent(iteration_exponent)
|
||||
storage.set_slip39_identifier(identifier)
|
||||
storage.set_slip39_threshold(threshold)
|
||||
storage.set_slip39_remaining(threshold - 1)
|
||||
storage.set_slip39_words_count(len(mnemonic.split()))
|
||||
storage.set_slip39_mnemonic(index, mnemonic)
|
||||
return None # we need more shares
|
||||
|
||||
# check identifier and member index of this share against stored values
|
||||
if identifier != storage.get_slip39_identifier():
|
||||
# TODO: improve UX (tell user)
|
||||
raise ValueError("Share identifiers do not match")
|
||||
if storage.get_slip39_mnemonic(index):
|
||||
# TODO: improve UX (tell user)
|
||||
raise ValueError("This mnemonic was already entered")
|
||||
|
||||
# append to storage
|
||||
remaining = storage.get_slip39_remaining() - 1
|
||||
storage.set_slip39_remaining(remaining)
|
||||
storage.set_slip39_mnemonic(index, mnemonic)
|
||||
if remaining != 0:
|
||||
return None # we need more shares
|
||||
|
||||
# combine shares and return the master secret
|
||||
mnemonics = storage.get_slip39_mnemonics()
|
||||
if len(mnemonics) != threshold:
|
||||
raise ValueError("Some mnemonics are still missing.")
|
||||
_, _, secret = slip39.combine_mnemonics(mnemonics)
|
||||
return secret
|
||||
|
||||
|
||||
def process_all(mnemonics: list) -> bytes:
|
||||
"""
|
||||
Receives all mnemonics and processes it into pre-master secret which is usually then
|
||||
stored in the storage.
|
||||
"""
|
||||
_, _, secret = slip39.combine_mnemonics(mnemonics)
|
||||
return secret
|
||||
|
||||
|
||||
def store(secret: bytes, needs_backup: bool, no_backup: bool):
|
||||
storage.store_mnemonic(secret, mnemonic.TYPE_SLIP39, needs_backup, no_backup)
|
||||
storage.clear_slip39_data()
|
||||
|
||||
|
||||
def get_seed(encrypted_master_secret: bytes, passphrase: str):
|
||||
mnemonic._start_progress()
|
||||
identifier = storage.get_slip39_identifier()
|
||||
iteration_exponent = storage.get_slip39_iteration_exponent()
|
||||
return slip39.decrypt(
|
||||
identifier, iteration_exponent, encrypted_master_secret, passphrase
|
||||
)
|
@ -0,0 +1,519 @@
|
||||
import ubinascii
|
||||
from micropython import const
|
||||
|
||||
from trezor import ui, utils
|
||||
from trezor.crypto import random
|
||||
from trezor.messages import ButtonRequestType
|
||||
from trezor.ui.button import Button, ButtonDefault
|
||||
from trezor.ui.checklist import Checklist
|
||||
from trezor.ui.info import InfoConfirm
|
||||
from trezor.ui.loader import LoadingAnimation
|
||||
from trezor.ui.scroll import Paginated
|
||||
from trezor.ui.shamir import NumInput
|
||||
from trezor.ui.text import Text
|
||||
|
||||
from apps.common.confirm import confirm, hold_to_confirm, require_confirm
|
||||
|
||||
if __debug__:
|
||||
from apps import debug
|
||||
|
||||
|
||||
async def show_reset_device_warning(ctx, use_slip39: bool):
|
||||
text = Text("Create new wallet", ui.ICON_RESET, new_lines=False)
|
||||
text.bold("Do you want to create")
|
||||
text.br()
|
||||
if use_slip39:
|
||||
text.bold("a new Shamir wallet?")
|
||||
else:
|
||||
text.bold("a new wallet?")
|
||||
text.br()
|
||||
text.br_half()
|
||||
text.normal("By continuing you agree")
|
||||
text.br()
|
||||
text.normal("to")
|
||||
text.bold("https://trezor.io/tos")
|
||||
await require_confirm(ctx, text, ButtonRequestType.ResetDevice, major_confirm=True)
|
||||
await LoadingAnimation()
|
||||
|
||||
|
||||
async def show_internal_entropy(ctx, entropy: bytes):
|
||||
entropy_str = ubinascii.hexlify(entropy).decode()
|
||||
lines = utils.chunks(entropy_str, 16)
|
||||
text = Text("Internal entropy", ui.ICON_RESET)
|
||||
text.mono(*lines)
|
||||
await require_confirm(ctx, text, ButtonRequestType.ResetDevice)
|
||||
|
||||
|
||||
async def show_backup_success(ctx):
|
||||
text = Text("Backup is done!", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.normal(
|
||||
"Never make a digital",
|
||||
"copy of your recovery",
|
||||
"seed and never upload",
|
||||
"it online!",
|
||||
)
|
||||
await require_confirm(
|
||||
ctx, text, ButtonRequestType.ResetDevice, confirm="Finish setup", cancel=None
|
||||
)
|
||||
|
||||
|
||||
async def confirm_backup(ctx):
|
||||
text = Text("Backup wallet", ui.ICON_RESET, new_lines=False)
|
||||
text.bold("New wallet created")
|
||||
text.br()
|
||||
text.bold("successfully!")
|
||||
text.br()
|
||||
text.br_half()
|
||||
text.normal("You should back your")
|
||||
text.br()
|
||||
text.normal("new wallet right now.")
|
||||
return await confirm(
|
||||
ctx,
|
||||
text,
|
||||
ButtonRequestType.ResetDevice,
|
||||
cancel="Skip",
|
||||
confirm="Backup",
|
||||
major_confirm=True,
|
||||
)
|
||||
|
||||
|
||||
async def confirm_backup_again(ctx):
|
||||
text = Text("Backup wallet", ui.ICON_RESET, new_lines=False)
|
||||
text.bold("Are you sure you want")
|
||||
text.br()
|
||||
text.bold("to skip the backup?")
|
||||
text.br()
|
||||
text.br_half()
|
||||
text.normal("You can backup Trezor")
|
||||
text.br()
|
||||
text.normal("anytime later.")
|
||||
return await confirm(
|
||||
ctx,
|
||||
text,
|
||||
ButtonRequestType.ResetDevice,
|
||||
cancel="Skip",
|
||||
confirm="Backup",
|
||||
major_confirm=True,
|
||||
)
|
||||
|
||||
|
||||
async def _confirm_share_words(ctx, share_index, share_words):
|
||||
numbered = list(enumerate(share_words))
|
||||
|
||||
# check a word from the first half
|
||||
first_half = numbered[: len(numbered) // 2]
|
||||
if not await _confirm_word(ctx, share_index, first_half):
|
||||
return False
|
||||
|
||||
# check a word from the second half
|
||||
second_half = numbered[len(numbered) // 2 :]
|
||||
if not await _confirm_word(ctx, share_index, second_half):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _confirm_word(ctx, share_index, numbered_share_words):
|
||||
# TODO: duplicated words in the choice list
|
||||
|
||||
# shuffle the numbered seed half, slice off the choices we need
|
||||
random.shuffle(numbered_share_words)
|
||||
numbered_choices = numbered_share_words[: MnemonicWordSelect.NUM_OF_CHOICES]
|
||||
|
||||
# we always confirm the first (random) word index
|
||||
checked_index, checked_word = numbered_choices[0]
|
||||
if __debug__:
|
||||
debug.reset_word_index = checked_index
|
||||
|
||||
# shuffle again so the confirmed word is not always the first choice
|
||||
random.shuffle(numbered_choices)
|
||||
|
||||
# let the user pick a word
|
||||
choices = [word for _, word in numbered_choices]
|
||||
select = MnemonicWordSelect(choices, share_index, checked_index)
|
||||
if __debug__:
|
||||
selected_word = await ctx.wait(select, debug.input_signal)
|
||||
else:
|
||||
selected_word = await ctx.wait(select)
|
||||
|
||||
# confirm it is the correct one
|
||||
return selected_word == checked_word
|
||||
|
||||
|
||||
async def _show_confirmation_success(ctx, share_index):
|
||||
if share_index is None:
|
||||
text = Text("Recovery seed", ui.ICON_RESET)
|
||||
text.bold("Recovery seed")
|
||||
text.bold("checked successfully.")
|
||||
else:
|
||||
text = Text("Recovery share #%s" % (share_index + 1), ui.ICON_RESET)
|
||||
text.bold("Seed share #%s" % (share_index + 1))
|
||||
text.bold("checked successfully.")
|
||||
text.normal("Let's continue with")
|
||||
text.normal("share #%s." % (share_index + 2))
|
||||
return await confirm(
|
||||
ctx, text, ButtonRequestType.ResetDevice, cancel=None, confirm="Continue"
|
||||
)
|
||||
|
||||
|
||||
async def _show_confirmation_failure(ctx, share_index):
|
||||
if share_index is None:
|
||||
text = Text("Recovery seed", ui.ICON_WRONG, ui.RED)
|
||||
else:
|
||||
text = Text("Recovery share #%s" % (share_index + 1), ui.ICON_WRONG, ui.RED)
|
||||
text.bold("You have entered")
|
||||
text.bold("wrong seed word.")
|
||||
text.bold("Please check again.")
|
||||
await require_confirm(
|
||||
ctx, text, ButtonRequestType.ResetDevice, confirm="Check again", cancel=None
|
||||
)
|
||||
|
||||
|
||||
# BIP39
|
||||
# ===
|
||||
|
||||
|
||||
async def bip39_show_and_confirm_mnemonic(ctx, mnemonic: str):
|
||||
words = mnemonic.split()
|
||||
|
||||
# require confirmation of the mnemonic safety
|
||||
await bip39_show_backup_warning(ctx)
|
||||
|
||||
while True:
|
||||
# display paginated mnemonic on the screen
|
||||
await _bip39_show_mnemonic(ctx, words)
|
||||
|
||||
# make the user confirm 2 words from the mnemonic
|
||||
if await _confirm_share_words(ctx, None, words):
|
||||
await _show_confirmation_success(ctx, None)
|
||||
break # this share is confirmed, go to next one
|
||||
else:
|
||||
await _show_confirmation_failure(ctx, None)
|
||||
|
||||
|
||||
async def bip39_show_backup_warning(ctx):
|
||||
text = Text("Backup your seed", ui.ICON_NOCOPY)
|
||||
text.normal(
|
||||
"Never make a digital",
|
||||
"copy of your recovery",
|
||||
"seed and never upload",
|
||||
"it online!",
|
||||
)
|
||||
await require_confirm(
|
||||
ctx, text, ButtonRequestType.ResetDevice, confirm="I understand", cancel=None
|
||||
)
|
||||
|
||||
|
||||
async def _bip39_show_mnemonic(ctx, words: list):
|
||||
# split mnemonic words into pages
|
||||
PER_PAGE = const(4)
|
||||
words = list(enumerate(words))
|
||||
words = list(utils.chunks(words, PER_PAGE))
|
||||
|
||||
# display the pages, with a confirmation dialog on the last one
|
||||
pages = [_get_mnemonic_page(page) for page in words]
|
||||
paginated = Paginated(pages)
|
||||
|
||||
if __debug__:
|
||||
|
||||
def export_displayed_words():
|
||||
# export currently displayed mnemonic words into debuglink
|
||||
debug.reset_current_words = [w for _, w in words[paginated.page]]
|
||||
|
||||
paginated.on_change = export_displayed_words
|
||||
export_displayed_words()
|
||||
|
||||
await hold_to_confirm(ctx, paginated, ButtonRequestType.ResetDevice)
|
||||
|
||||
|
||||
def _get_mnemonic_page(words: list):
|
||||
text = Text("Recovery seed", ui.ICON_RESET)
|
||||
for index, word in words:
|
||||
text.mono("%2d. %s" % (index + 1, word))
|
||||
return text
|
||||
|
||||
|
||||
# SLIP39
|
||||
# ===
|
||||
|
||||
# TODO: yellow cancel style?
|
||||
# TODO: loading animation style?
|
||||
# TODO: smaller font or tighter rows to fit more text in
|
||||
# TODO: icons in checklist
|
||||
|
||||
|
||||
async def slip39_show_checklist_set_shares(ctx):
|
||||
checklist = Checklist("Backup checklist", ui.ICON_RESET)
|
||||
checklist.add("Set number of shares")
|
||||
checklist.add("Set the threshold")
|
||||
checklist.add(("Write down and check", "all seed shares"))
|
||||
checklist.select(0)
|
||||
checklist.process()
|
||||
return await confirm(
|
||||
ctx, checklist, ButtonRequestType.ResetDevice, cancel=None, confirm="Set shares"
|
||||
)
|
||||
|
||||
|
||||
async def slip39_show_checklist_set_threshold(ctx, num_of_shares):
|
||||
checklist = Checklist("Backup checklist", ui.ICON_RESET)
|
||||
checklist.add("Set number of shares")
|
||||
checklist.add("Set the threshold")
|
||||
checklist.add(("Write down and check", "all seed shares"))
|
||||
checklist.select(1)
|
||||
checklist.process()
|
||||
return await confirm(
|
||||
ctx,
|
||||
checklist,
|
||||
ButtonRequestType.ResetDevice,
|
||||
cancel=None,
|
||||
confirm="Set threshold",
|
||||
)
|
||||
|
||||
|
||||
async def slip39_show_checklist_show_shares(ctx, num_of_shares, threshold):
|
||||
checklist = Checklist("Backup checklist", ui.ICON_RESET)
|
||||
checklist.add("Set number of shares")
|
||||
checklist.add("Set the threshold")
|
||||
checklist.add(("Write down and check", "all seed shares"))
|
||||
checklist.select(2)
|
||||
checklist.process()
|
||||
return await confirm(
|
||||
ctx,
|
||||
checklist,
|
||||
ButtonRequestType.ResetDevice,
|
||||
cancel=None,
|
||||
confirm="Show seed shares",
|
||||
)
|
||||
|
||||
|
||||
async def slip39_prompt_number_of_shares(ctx):
|
||||
count = 5
|
||||
min_count = 2
|
||||
max_count = 16
|
||||
|
||||
while True:
|
||||
shares = ShamirNumInput(ShamirNumInput.SET_SHARES, count, min_count, max_count)
|
||||
info = InfoConfirm(
|
||||
"Shares are parts of "
|
||||
"the recovery seed, "
|
||||
"each containing 20 "
|
||||
"words. You can later set "
|
||||
"how many shares you "
|
||||
"need to recover your "
|
||||
"wallet."
|
||||
)
|
||||
confirmed = await confirm(
|
||||
ctx,
|
||||
shares,
|
||||
ButtonRequestType.ResetDevice,
|
||||
cancel="Info",
|
||||
confirm="Set",
|
||||
major_confirm=True,
|
||||
cancel_style=ButtonDefault,
|
||||
)
|
||||
count = shares.input.count
|
||||
if confirmed:
|
||||
break
|
||||
else:
|
||||
await info
|
||||
|
||||
return count
|
||||
|
||||
|
||||
async def slip39_prompt_threshold(ctx, num_of_shares):
|
||||
count = num_of_shares // 2
|
||||
min_count = 2
|
||||
max_count = num_of_shares
|
||||
|
||||
while True:
|
||||
shares = ShamirNumInput(
|
||||
ShamirNumInput.SET_THRESHOLD, count, min_count, max_count
|
||||
)
|
||||
info = InfoConfirm(
|
||||
"Threshold sets number "
|
||||
"shares that you need "
|
||||
"to recover your wallet. "
|
||||
"i.e. Set it to %s and "
|
||||
"you'll need any %s shares "
|
||||
"of the total number." % (count, count)
|
||||
)
|
||||
confirmed = await confirm(
|
||||
ctx,
|
||||
shares,
|
||||
ButtonRequestType.ResetDevice,
|
||||
cancel="Info",
|
||||
confirm="Set",
|
||||
major_confirm=True,
|
||||
cancel_style=ButtonDefault,
|
||||
)
|
||||
count = shares.input.count
|
||||
if confirmed:
|
||||
break
|
||||
else:
|
||||
await info
|
||||
|
||||
return count
|
||||
|
||||
|
||||
async def slip39_show_and_confirm_shares(ctx, shares):
|
||||
for index, share in enumerate(shares):
|
||||
share_words = share.split(" ")
|
||||
while True:
|
||||
# display paginated share on the screen
|
||||
await _slip39_show_share_words(ctx, index, share_words)
|
||||
|
||||
# make the user confirm 2 words from the share
|
||||
if await _confirm_share_words(ctx, index, share_words):
|
||||
await _show_confirmation_success(ctx, index)
|
||||
break # this share is confirmed, go to next one
|
||||
else:
|
||||
await _show_confirmation_failure(ctx, index)
|
||||
|
||||
|
||||
async def _slip39_show_share_words(ctx, share_index, share_words):
|
||||
first, chunks, last = _slip39_split_share_into_pages(share_words)
|
||||
|
||||
if share_index is None:
|
||||
header_title = "Recovery share #%s" % (share_index + 1)
|
||||
else:
|
||||
header_title = "Recovery seed"
|
||||
header_icon = ui.ICON_RESET
|
||||
pages = [] # ui page components
|
||||
shares_words_check = [] # check we display correct data
|
||||
|
||||
# first page
|
||||
text = Text(header_title, header_icon)
|
||||
text.normal("Write down %s words" % len(share_words))
|
||||
text.normal("onto paper booklet:")
|
||||
text.br_half()
|
||||
for index, word in first:
|
||||
text.mono("%s. %s" % (index + 1, word))
|
||||
shares_words_check.append(word)
|
||||
pages.append(text)
|
||||
|
||||
# middle pages
|
||||
for chunk in chunks:
|
||||
text = Text(header_title, header_icon)
|
||||
text.br_half()
|
||||
for index, word in chunk:
|
||||
text.mono("%s. %s" % (index + 1, word))
|
||||
shares_words_check.append(word)
|
||||
pages.append(text)
|
||||
|
||||
# last page
|
||||
text = Text(header_title, header_icon)
|
||||
for index, word in last:
|
||||
text.mono("%s. %s" % (index + 1, word))
|
||||
shares_words_check.append(word)
|
||||
text.br_half()
|
||||
text.normal("I confirm that I wrote")
|
||||
text.normal("down all %s words." % len(share_words))
|
||||
pages.append(text)
|
||||
|
||||
# pagination
|
||||
paginated = Paginated(pages)
|
||||
|
||||
if __debug__:
|
||||
|
||||
word_pages = [first] + chunks + [last]
|
||||
|
||||
def export_displayed_words():
|
||||
# export currently displayed mnemonic words into debuglink
|
||||
debug.reset_current_words = word_pages[paginated.page]
|
||||
|
||||
paginated.on_change = export_displayed_words
|
||||
export_displayed_words()
|
||||
|
||||
# make sure we display correct data
|
||||
utils.ensure(share_words == shares_words_check)
|
||||
|
||||
# confirm the share
|
||||
await hold_to_confirm(ctx, paginated) # TODO: customize the loader here
|
||||
|
||||
|
||||
def _slip39_split_share_into_pages(share_words):
|
||||
share = list(enumerate(share_words)) # we need to keep track of the word indices
|
||||
first = share[:2] # two words on the first page
|
||||
middle = share[2:-2]
|
||||
last = share[-2:] # two words on the last page
|
||||
chunks = utils.chunks(middle, 4) # 4 words on the middle pages
|
||||
return first, list(chunks), last
|
||||
|
||||
|
||||
class ShamirNumInput(ui.Control):
|
||||
SET_SHARES = object()
|
||||
SET_THRESHOLD = object()
|
||||
|
||||
def __init__(self, step, count, min_count, max_count):
|
||||
self.step = step
|
||||
self.input = NumInput(count, min_count=min_count, max_count=max_count)
|
||||
self.input.on_change = self.on_change
|
||||
self.repaint = True
|
||||
|
||||
def dispatch(self, event, x, y):
|
||||
self.input.dispatch(event, x, y)
|
||||
if event is ui.RENDER:
|
||||
self.on_render()
|
||||
|
||||
def on_render(self):
|
||||
if self.repaint:
|
||||
count = self.input.count
|
||||
|
||||
# render the headline
|
||||
if self.step is ShamirNumInput.SET_SHARES:
|
||||
header = "Set num. of shares"
|
||||
elif self.step is ShamirNumInput.SET_THRESHOLD:
|
||||
header = "Set the threshold"
|
||||
ui.header(header, ui.ICON_RESET, ui.TITLE_GREY, ui.BG, ui.ORANGE_ICON)
|
||||
|
||||
# render the counter
|
||||
if self.step is ShamirNumInput.SET_SHARES:
|
||||
ui.display.text(
|
||||
12, 130, "%s people or locations" % count, ui.BOLD, ui.FG, ui.BG
|
||||
)
|
||||
ui.display.text(
|
||||
12, 156, "will each host one share.", ui.NORMAL, ui.FG, ui.BG
|
||||
)
|
||||
elif self.step is ShamirNumInput.SET_THRESHOLD:
|
||||
ui.display.text(
|
||||
12, 130, "For recovery you'll need", ui.NORMAL, ui.FG, ui.BG
|
||||
)
|
||||
ui.display.text(
|
||||
12, 156, "any %s of shares." % count, ui.BOLD, ui.FG, ui.BG
|
||||
)
|
||||
|
||||
self.repaint = False
|
||||
|
||||
def on_change(self, count):
|
||||
self.repaint = True
|
||||
|
||||
|
||||
class MnemonicWordSelect(ui.Layout):
|
||||
NUM_OF_CHOICES = 3
|
||||
|
||||
def __init__(self, words, share_index, word_index):
|
||||
self.words = words
|
||||
self.share_index = share_index
|
||||
self.word_index = word_index
|
||||
self.buttons = []
|
||||
for i, word in enumerate(words):
|
||||
area = ui.grid(i + 2, n_x=1)
|
||||
btn = Button(area, word)
|
||||
btn.on_click = self.select(word)
|
||||
self.buttons.append(btn)
|
||||
if share_index is None:
|
||||
self.text = Text("Recovery seed")
|
||||
else:
|
||||
self.text = Text("Recovery share #%s" % (share_index + 1))
|
||||
self.text.normal("Choose the %s word:" % utils.format_ordinal(word_index + 1))
|
||||
|
||||
def dispatch(self, event, x, y):
|
||||
for btn in self.buttons:
|
||||
btn.dispatch(event, x, y)
|
||||
self.text.dispatch(event, x, y)
|
||||
|
||||
def select(self, word):
|
||||
def fn():
|
||||
raise ui.Result(word)
|
||||
|
||||
return fn
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,55 @@
|
||||
from micropython import const
|
||||
|
||||
from trezor import ui
|
||||
from trezor.ui import text
|
||||
|
||||
_CHECKLIST_MAX_LINES = const(5)
|
||||
_CHECKLIST_OFFSET_X = const(24)
|
||||
|
||||
|
||||
class Checklist(ui.Control):
|
||||
def __init__(self, title, icon):
|
||||
self.title = title
|
||||
self.icon = icon
|
||||
self.choices = []
|
||||
self.words = []
|
||||
self.active = 0
|
||||
self.repaint = False
|
||||
|
||||
def add(self, choice):
|
||||
self.choices.append(choice)
|
||||
|
||||
def select(self, active):
|
||||
self.active = active
|
||||
|
||||
def process(self):
|
||||
w = self.words
|
||||
w.clear()
|
||||
for index, choice in enumerate(self.choices):
|
||||
if index < self.active:
|
||||
w.append(ui.BOLD)
|
||||
w.append(ui.GREEN)
|
||||
elif index == self.active:
|
||||
w.append(ui.BOLD)
|
||||
w.append(ui.FG)
|
||||
else: # index > self.active
|
||||
w.append(ui.NORMAL)
|
||||
w.append(ui.GREY)
|
||||
if isinstance(choice, str):
|
||||
w.append(choice)
|
||||
else: # choice is iterable
|
||||
w.extend(choice)
|
||||
w.append(text.BR)
|
||||
self.words = w
|
||||
self.repaint = True
|
||||
|
||||
def on_render(self):
|
||||
if self.repaint:
|
||||
ui.header(self.title, self.icon)
|
||||
text.render_text(
|
||||
self.words,
|
||||
new_lines=False, # we are adding line breaks manually
|
||||
max_lines=_CHECKLIST_MAX_LINES,
|
||||
offset_x=_CHECKLIST_OFFSET_X,
|
||||
)
|
||||
self.repaint = False
|
@ -0,0 +1,63 @@
|
||||
from trezor import res, ui
|
||||
from trezor.ui.button import Button, ButtonConfirm
|
||||
from trezor.ui.confirm import CONFIRMED
|
||||
from trezor.ui.text import TEXT_LINE_HEIGHT, TEXT_MARGIN_LEFT, render_text
|
||||
|
||||
|
||||
class DefaultInfoConfirm:
|
||||
|
||||
fg_color = ui.LIGHT_GREY
|
||||
bg_color = ui.BLACKISH
|
||||
|
||||
class button(ButtonConfirm):
|
||||
class normal(ButtonConfirm.normal):
|
||||
border_color = ui.BLACKISH
|
||||
|
||||
class disabled(ButtonConfirm.disabled):
|
||||
border_color = ui.BLACKISH
|
||||
|
||||
|
||||
class InfoConfirm(ui.Layout):
|
||||
DEFAULT_CONFIRM = res.load(ui.ICON_CONFIRM)
|
||||
DEFAULT_STYLE = DefaultInfoConfirm
|
||||
|
||||
def __init__(self, text, confirm=DEFAULT_CONFIRM, style=DEFAULT_STYLE):
|
||||
self.text = text.split()
|
||||
self.style = style
|
||||
panel_area = ui.grid(0, n_x=1, n_y=1)
|
||||
self.panel_area = panel_area
|
||||
confirm_area = ui.grid(4, n_x=1)
|
||||
self.confirm = Button(confirm_area, confirm, style.button)
|
||||
self.confirm.on_click = self.on_confirm
|
||||
self.repaint = True
|
||||
|
||||
def dispatch(self, event, x, y):
|
||||
if event == ui.RENDER:
|
||||
self.on_render()
|
||||
self.confirm.dispatch(event, x, y)
|
||||
|
||||
def on_render(self):
|
||||
if self.repaint:
|
||||
x, y, w, h = self.panel_area
|
||||
fg_color = self.style.fg_color
|
||||
bg_color = self.style.bg_color
|
||||
|
||||
# render the background panel
|
||||
ui.display.bar_radius(x, y, w, h, bg_color, ui.BG, ui.RADIUS)
|
||||
|
||||
# render the info text
|
||||
render_text(
|
||||
self.text,
|
||||
new_lines=False,
|
||||
max_lines=6,
|
||||
offset_y=y + TEXT_LINE_HEIGHT,
|
||||
offset_x=x + TEXT_MARGIN_LEFT - ui.VIEWX,
|
||||
offset_x_max=x + w - ui.VIEWX,
|
||||
fg=fg_color,
|
||||
bg=bg_color,
|
||||
)
|
||||
|
||||
self.repaint = False
|
||||
|
||||
def on_confirm(self):
|
||||
raise ui.Result(CONFIRMED)
|
@ -0,0 +1,49 @@
|
||||
from trezor import ui
|
||||
from trezor.ui.button import Button
|
||||
from trezor.ui.text import LABEL_CENTER, Label
|
||||
|
||||
|
||||
class NumInput(ui.Control):
|
||||
def __init__(self, count=5, max_count=16, min_count=1):
|
||||
self.count = count
|
||||
self.max_count = max_count
|
||||
self.min_count = min_count
|
||||
|
||||
self.minus = Button(ui.grid(3), "-")
|
||||
self.minus.on_click = self.on_minus
|
||||
self.plus = Button(ui.grid(5), "+")
|
||||
self.plus.on_click = self.on_plus
|
||||
self.text = Label(ui.grid(4), "", LABEL_CENTER, ui.BOLD)
|
||||
|
||||
self.edit(count)
|
||||
|
||||
def dispatch(self, event, x, y):
|
||||
self.minus.dispatch(event, x, y)
|
||||
self.plus.dispatch(event, x, y)
|
||||
self.text.dispatch(event, x, y)
|
||||
|
||||
def on_minus(self):
|
||||
self.edit(self.count - 1)
|
||||
|
||||
def on_plus(self):
|
||||
self.edit(self.count + 1)
|
||||
|
||||
def edit(self, count):
|
||||
count = max(count, self.min_count)
|
||||
count = min(count, self.max_count)
|
||||
if self.count != count:
|
||||
self.on_change(count)
|
||||
self.count = count
|
||||
self.text.content = str(count)
|
||||
self.text.repaint = True
|
||||
if self.count == self.min_count:
|
||||
self.minus.disable()
|
||||
else:
|
||||
self.minus.enable()
|
||||
if self.count == self.max_count:
|
||||
self.plus.disable()
|
||||
else:
|
||||
self.plus.enable()
|
||||
|
||||
def on_change(self, count):
|
||||
pass
|
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue