1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-18 12:28:09 +00:00

refactor(core): convert apps.management.reset_device to layouts

This commit is contained in:
Martin Milata 2021-03-23 23:52:28 +01:00
parent 574dcbc8a3
commit 312876ab67
9 changed files with 591 additions and 465 deletions

View File

@ -110,7 +110,8 @@ mypy:
src/apps/bitcoin \ src/apps/bitcoin \
src/apps/cardano \ src/apps/cardano \
src/apps/misc \ src/apps/misc \
src/apps/webauthn src/apps/webauthn \
src/trezor/ui
## code generation: ## code generation:

View File

@ -523,7 +523,7 @@ if FROZEN:
if TREZOR_MODEL == 'T': if TREZOR_MODEL == 'T':
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/tt/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/tt/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tt.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tt.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/*.py'))
elif TREZOR_MODEL == '1': elif TREZOR_MODEL == '1':
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/t1/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/t1/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/t1.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/t1.py'))

View File

@ -478,7 +478,7 @@ if FROZEN:
if TREZOR_MODEL == 'T': if TREZOR_MODEL == 'T':
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/tt/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/tt/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tt.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tt.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/*.py'))
elif TREZOR_MODEL == '1': elif TREZOR_MODEL == '1':
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/t1/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/t1/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/t1.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/t1.py'))

View File

@ -88,7 +88,7 @@ async def reset_device(ctx: wire.Context, msg: ResetDevice) -> Success:
secret, # for SLIP-39, this is the EMS secret, # for SLIP-39, this is the EMS
msg.backup_type, msg.backup_type,
needs_backup=not perform_backup, needs_backup=not perform_backup,
no_backup=msg.no_backup, no_backup=bool(msg.no_backup),
) )
# if we backed up the wallet, show success message # if we backed up the wallet, show success message
@ -109,12 +109,17 @@ async def backup_slip39_basic(
await layout.slip39_show_checklist(ctx, 1, BackupType.Slip39_Basic) await layout.slip39_show_checklist(ctx, 1, BackupType.Slip39_Basic)
threshold = await layout.slip39_prompt_threshold(ctx, shares_count) threshold = await layout.slip39_prompt_threshold(ctx, shares_count)
identifier = storage.device.get_slip39_identifier()
iteration_exponent = storage.device.get_slip39_iteration_exponent()
if identifier is None or iteration_exponent is None:
raise ValueError
# generate the mnemonics # generate the mnemonics
mnemonics = slip39.split_ems( mnemonics = slip39.split_ems(
1, # Single Group threshold 1, # Single Group threshold
[(threshold, shares_count)], # Single Group threshold/count [(threshold, shares_count)], # Single Group threshold/count
storage.device.get_slip39_identifier(), identifier,
storage.device.get_slip39_iteration_exponent(), iteration_exponent,
encrypted_master_secret, encrypted_master_secret,
)[0] )[0]
@ -144,12 +149,17 @@ async def backup_slip39_advanced(
share_threshold = await layout.slip39_prompt_threshold(ctx, share_count, i) share_threshold = await layout.slip39_prompt_threshold(ctx, share_count, i)
groups.append((share_threshold, share_count)) groups.append((share_threshold, share_count))
identifier = storage.device.get_slip39_identifier()
iteration_exponent = storage.device.get_slip39_iteration_exponent()
if identifier is None or iteration_exponent is None:
raise ValueError
# generate the mnemonics # generate the mnemonics
mnemonics = slip39.split_ems( mnemonics = slip39.split_ems(
group_threshold=group_threshold, group_threshold=group_threshold,
groups=groups, groups=groups,
identifier=storage.device.get_slip39_identifier(), identifier=identifier,
iteration_exponent=storage.device.get_slip39_iteration_exponent(), iteration_exponent=iteration_exponent,
encrypted_master_secret=encrypted_master_secret, encrypted_master_secret=encrypted_master_secret,
) )
@ -193,7 +203,7 @@ def _compute_secret_from_entropy(
async def backup_seed( async def backup_seed(
ctx: wire.Context, backup_type: BackupType, mnemonic_secret: bytes ctx: wire.Context, backup_type: BackupType, mnemonic_secret: bytes
): ) -> None:
if backup_type == BackupType.Slip39_Basic: if backup_type == BackupType.Slip39_Basic:
await backup_slip39_basic(ctx, mnemonic_secret) await backup_slip39_basic(ctx, mnemonic_secret)
elif backup_type == BackupType.Slip39_Advanced: elif backup_type == BackupType.Slip39_Advanced:

View File

@ -1,24 +1,21 @@
from trezor import ui, utils from trezor import ui, utils, wire
from trezor.crypto import random from trezor.enums import ButtonRequestType
from trezor.enums import BackupType, ButtonRequestType
from trezor.ui.components.tt.button import Button, ButtonDefault
from trezor.ui.components.tt.checklist import Checklist
from trezor.ui.components.tt.info import InfoConfirm
from trezor.ui.components.tt.num_input import NumInput
from trezor.ui.components.tt.scroll import Paginated
from trezor.ui.components.tt.text import Text
from trezor.ui.layouts import confirm_action, confirm_blob, show_success, show_warning from trezor.ui.layouts import confirm_action, confirm_blob, show_success, show_warning
from trezor.ui.layouts.tt.reset import ( # noqa: F401
from apps.common.confirm import confirm, require_hold_to_confirm confirm_word,
show_share_words,
slip39_advanced_prompt_group_threshold,
slip39_advanced_prompt_number_of_groups,
slip39_prompt_number_of_shares,
slip39_prompt_threshold,
slip39_show_checklist,
)
if False: if False:
from trezor import loop from typing import Sequence
if __debug__:
from apps import debug
async def show_internal_entropy(ctx, entropy: bytes): async def show_internal_entropy(ctx: wire.GenericContext, entropy: bytes) -> None:
await confirm_blob( await confirm_blob(
ctx, ctx,
"entropy", "entropy",
@ -30,90 +27,12 @@ async def show_internal_entropy(ctx, entropy: bytes):
) )
async def _show_share_words(ctx, share_words, share_index=None, group_index=None): async def _confirm_share_words(
first, chunks, last = _split_share_into_pages(share_words) ctx: wire.GenericContext,
share_index: int | None,
if share_index is None: share_words: Sequence[str],
header_title = "Recovery seed" group_index: int | None = None,
elif group_index is None: ) -> bool:
header_title = "Recovery share #%s" % (share_index + 1)
else:
header_title = "Group %s - Share %s" % ((group_index + 1), (share_index + 1))
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.bold("Write down these")
text.bold("%s words:" % len(share_words))
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)
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.bold("I wrote down all %s" % len(share_words))
text.bold("words in order.")
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
words = [w for _, w in word_pages[paginated.page]]
debug.reset_current_words.publish(words)
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 require_hold_to_confirm(
ctx, paginated, ButtonRequestType.ResetDevice, cancel=False
)
def _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
length = len(share_words)
if length == 12 or length == 20 or length == 24:
middle = share[2:-2]
last = share[-2:] # two words on the last page
elif length == 33 or length == 18:
middle = share[2:]
last = [] # no words at the last page, because it does not add up
else:
# Invalid number of shares. SLIP-39 allows 20 or 33 words, BIP-39 12 or 24
raise RuntimeError
chunks = utils.chunks(middle, 4) # 4 words on the middle pages
return first, list(chunks), last
async def _confirm_share_words(ctx, share_index, share_words, group_index=None):
# divide list into thirds, rounding up, so that chunking by `third` always yields # divide list into thirds, rounding up, so that chunking by `third` always yields
# three parts (the last one might be shorter) # three parts (the last one might be shorter)
third = (len(share_words) + 2) // 3 third = (len(share_words) + 2) // 3
@ -121,41 +40,20 @@ async def _confirm_share_words(ctx, share_index, share_words, group_index=None):
offset = 0 offset = 0
count = len(share_words) count = len(share_words)
for part in utils.chunks(share_words, third): for part in utils.chunks(share_words, third):
if not await _confirm_word(ctx, share_index, part, offset, count, group_index): if not await confirm_word(ctx, share_index, part, offset, count, group_index):
return False return False
offset += len(part) offset += len(part)
return True return True
async def _confirm_word(ctx, share_index, share_words, offset, count, group_index=None):
# remove duplicates
non_duplicates = list(set(share_words))
# shuffle list
random.shuffle(non_duplicates)
# take top NUM_OF_CHOICES words
choices = non_duplicates[: MnemonicWordSelect.NUM_OF_CHOICES]
# select first of them
checked_word = choices[0]
# find its index
checked_index = share_words.index(checked_word) + offset
# shuffle again so the confirmed word is not always the first choice
random.shuffle(choices)
if __debug__:
debug.reset_word_index.publish(checked_index)
# let the user pick a word
select = MnemonicWordSelect(choices, share_index, checked_index, count, group_index)
selected_word = await ctx.wait(select)
# confirm it is the correct one
return selected_word == checked_word
async def _show_confirmation_success( async def _show_confirmation_success(
ctx, share_index=None, num_of_shares=None, group_index=None ctx: wire.GenericContext,
): share_index: int | None = None,
if share_index is None: # it is a BIP39 backup num_of_shares: int | None = None,
group_index: int | None = None,
) -> None:
if share_index is None or num_of_shares is None: # it is a BIP39 backup
subheader = "You have finished\nverifying your\nrecovery seed." subheader = "You have finished\nverifying your\nrecovery seed."
text = "" text = ""
@ -183,7 +81,9 @@ async def _show_confirmation_success(
return await show_success(ctx, "success_recovery", text, subheader=subheader) return await show_success(ctx, "success_recovery", text, subheader=subheader)
async def _show_confirmation_failure(ctx, share_index): async def _show_confirmation_failure(
ctx: wire.GenericContext, share_index: int | None
) -> None:
if share_index is None: if share_index is None:
header = "Recovery seed" header = "Recovery seed"
else: else:
@ -199,7 +99,7 @@ async def _show_confirmation_failure(ctx, share_index):
) )
async def show_backup_warning(ctx, slip39=False): async def show_backup_warning(ctx: wire.GenericContext, slip39: bool = False) -> None:
if slip39: if slip39:
description = "Never make a digital copy of your recovery shares and never upload them online!" description = "Never make a digital copy of your recovery shares and never upload them online!"
else: else:
@ -216,7 +116,7 @@ async def show_backup_warning(ctx, slip39=False):
) )
async def show_backup_success(ctx): async def show_backup_success(ctx: wire.GenericContext) -> None:
text = "Use your backup\nwhen you need to\nrecover your wallet." text = "Use your backup\nwhen you need to\nrecover your wallet."
await show_success(ctx, "success_backup", text, subheader="Your backup is done.") await show_success(ctx, "success_backup", text, subheader="Your backup is done.")
@ -225,7 +125,9 @@ async def show_backup_success(ctx):
# === # ===
async def bip39_show_and_confirm_mnemonic(ctx, mnemonic: str): async def bip39_show_and_confirm_mnemonic(
ctx: wire.GenericContext, mnemonic: str
) -> None:
# warn user about mnemonic safety # warn user about mnemonic safety
await show_backup_warning(ctx) await show_backup_warning(ctx)
@ -233,7 +135,7 @@ async def bip39_show_and_confirm_mnemonic(ctx, mnemonic: str):
while True: while True:
# display paginated mnemonic on the screen # display paginated mnemonic on the screen
await _show_share_words(ctx, share_words=words) await show_share_words(ctx, share_words=words)
# make the user confirm some words from the mnemonic # make the user confirm some words from the mnemonic
if await _confirm_share_words(ctx, None, words): if await _confirm_share_words(ctx, None, words):
@ -247,119 +149,9 @@ async def bip39_show_and_confirm_mnemonic(ctx, mnemonic: str):
# === # ===
async def slip39_show_checklist(ctx, step: int, backup_type: BackupType) -> None: async def slip39_basic_show_and_confirm_shares(
checklist = Checklist("Backup checklist", ui.ICON_RESET) ctx: wire.GenericContext, shares: Sequence[str]
if backup_type is BackupType.Slip39_Basic: ) -> None:
checklist.add("Set number of shares")
checklist.add("Set threshold")
checklist.add(("Write down and check", "all recovery shares"))
elif backup_type is BackupType.Slip39_Advanced:
checklist.add("Set number of groups")
checklist.add("Set group threshold")
checklist.add(("Set size and threshold", "for each group"))
checklist.select(step)
return await confirm(
ctx, checklist, ButtonRequestType.ResetDevice, cancel=None, confirm="Continue"
)
async def slip39_prompt_threshold(ctx, num_of_shares, group_id=None):
count = num_of_shares // 2 + 1
# min value of share threshold is 2 unless the number of shares is 1
# number of shares 1 is possible in advnaced slip39
min_count = min(2, num_of_shares)
max_count = num_of_shares
while True:
shares = Slip39NumInput(
Slip39NumInput.SET_THRESHOLD, count, min_count, max_count, group_id
)
confirmed = await confirm(
ctx,
shares,
ButtonRequestType.ResetDevice,
cancel="Info",
confirm="Continue",
major_confirm=True,
cancel_style=ButtonDefault,
)
count = shares.input.count
if confirmed:
break
text = "The threshold sets the number of shares "
if group_id is None:
text += "needed to recover your wallet. "
text += "Set it to %s and you will need " % count
if num_of_shares == 1:
text += "1 share."
elif num_of_shares == count:
text += "all %s of your %s shares." % (count, num_of_shares)
else:
text += "any %s of your %s shares." % (count, num_of_shares)
else:
text += "needed to form a group. "
text += "Set it to %s and you will " % count
if num_of_shares == 1:
text += "need 1 share "
elif num_of_shares == count:
text += "need all %s of %s shares " % (count, num_of_shares)
else:
text += "need any %s of %s shares " % (count, num_of_shares)
text += "to form Group %s." % (group_id + 1)
info = InfoConfirm(text)
await info
return count
async def slip39_prompt_number_of_shares(ctx, group_id=None):
count = 5
min_count = 1
max_count = 16
while True:
shares = Slip39NumInput(
Slip39NumInput.SET_SHARES, count, min_count, max_count, group_id
)
confirmed = await confirm(
ctx,
shares,
ButtonRequestType.ResetDevice,
cancel="Info",
confirm="Continue",
major_confirm=True,
cancel_style=ButtonDefault,
)
count = shares.input.count
if confirmed:
break
if group_id is None:
info = InfoConfirm(
"Each recovery share is a "
"sequence of 20 words. "
"Next you will choose "
"how many shares you "
"need to recover your "
"wallet."
)
else:
info = InfoConfirm(
"Each recovery share is a "
"sequence of 20 words. "
"Next you will choose "
"the threshold number of "
"shares needed to form "
"Group %s." % (group_id + 1)
)
await info
return count
async def slip39_basic_show_and_confirm_shares(ctx, shares):
# warn user about mnemonic safety # warn user about mnemonic safety
await show_backup_warning(ctx, slip39=True) await show_backup_warning(ctx, slip39=True)
@ -367,7 +159,7 @@ async def slip39_basic_show_and_confirm_shares(ctx, shares):
share_words = share.split(" ") share_words = share.split(" ")
while True: while True:
# display paginated share on the screen # display paginated share on the screen
await _show_share_words(ctx, share_words, index) await show_share_words(ctx, share_words, index)
# make the user confirm words from the share # make the user confirm words from the share
if await _confirm_share_words(ctx, index, share_words): if await _confirm_share_words(ctx, index, share_words):
@ -379,73 +171,9 @@ async def slip39_basic_show_and_confirm_shares(ctx, shares):
await _show_confirmation_failure(ctx, index) await _show_confirmation_failure(ctx, index)
async def slip39_advanced_prompt_number_of_groups(ctx): async def slip39_advanced_show_and_confirm_shares(
count = 5 ctx: wire.GenericContext, shares: Sequence[Sequence[str]]
min_count = 2 ) -> None:
max_count = 16
while True:
shares = Slip39NumInput(Slip39NumInput.SET_GROUPS, count, min_count, max_count)
confirmed = await confirm(
ctx,
shares,
ButtonRequestType.ResetDevice,
cancel="Info",
confirm="Continue",
major_confirm=True,
cancel_style=ButtonDefault,
)
count = shares.input.count
if confirmed:
break
info = InfoConfirm(
"Each group has a set "
"number of shares and "
"its own threshold. In the "
"next steps you will set "
"the numbers of shares "
"and the thresholds."
)
await info
return count
async def slip39_advanced_prompt_group_threshold(ctx, num_of_groups):
count = num_of_groups // 2 + 1
min_count = 1
max_count = num_of_groups
while True:
shares = Slip39NumInput(
Slip39NumInput.SET_GROUP_THRESHOLD, count, min_count, max_count
)
confirmed = await confirm(
ctx,
shares,
ButtonRequestType.ResetDevice,
cancel="Info",
confirm="Continue",
major_confirm=True,
cancel_style=ButtonDefault,
)
count = shares.input.count
if confirmed:
break
else:
info = InfoConfirm(
"The group threshold "
"specifies the number of "
"groups required to "
"recover your wallet. "
)
await info
return count
async def slip39_advanced_show_and_confirm_shares(ctx, shares):
# warn user about mnemonic safety # warn user about mnemonic safety
await show_backup_warning(ctx, slip39=True) await show_backup_warning(ctx, slip39=True)
@ -454,7 +182,7 @@ async def slip39_advanced_show_and_confirm_shares(ctx, shares):
share_words = share.split(" ") share_words = share.split(" ")
while True: while True:
# display paginated share on the screen # display paginated share on the screen
await _show_share_words(ctx, share_words, share_index, group_index) await show_share_words(ctx, share_words, share_index, group_index)
# make the user confirm words from the share # make the user confirm words from the share
if await _confirm_share_words( if await _confirm_share_words(
@ -469,131 +197,3 @@ async def slip39_advanced_show_and_confirm_shares(ctx, shares):
break # this share is confirmed, go to next one break # this share is confirmed, go to next one
else: else:
await _show_confirmation_failure(ctx, share_index) await _show_confirmation_failure(ctx, share_index)
class Slip39NumInput(ui.Component):
SET_SHARES = object()
SET_THRESHOLD = object()
SET_GROUPS = object()
SET_GROUP_THRESHOLD = object()
def __init__(self, step, count, min_count, max_count, group_id=None):
super().__init__()
self.step = step
self.input = NumInput(count, min_count=min_count, max_count=max_count)
self.input.on_change = self.on_change
self.group_id = group_id
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 Slip39NumInput.SET_SHARES:
header = "Set num. of shares"
elif self.step is Slip39NumInput.SET_THRESHOLD:
header = "Set threshold"
elif self.step is Slip39NumInput.SET_GROUPS:
header = "Set num. of groups"
elif self.step is Slip39NumInput.SET_GROUP_THRESHOLD:
header = "Set group threshold"
ui.header(header, ui.ICON_RESET, ui.TITLE_GREY, ui.BG, ui.ORANGE_ICON)
# render the counter
if self.step is Slip39NumInput.SET_SHARES:
if self.group_id is None:
if count == 1:
first_line_text = "Only one share will"
second_line_text = "be created."
else:
first_line_text = "%s people or locations" % count
second_line_text = "will each hold one share."
else:
first_line_text = "Set the total number of"
second_line_text = "shares in Group %s." % (self.group_id + 1)
ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG)
ui.display.text(12, 130, first_line_text, ui.NORMAL, ui.FG, ui.BG)
ui.display.text(12, 156, second_line_text, ui.NORMAL, ui.FG, ui.BG)
elif self.step is Slip39NumInput.SET_THRESHOLD:
if self.group_id is None:
first_line_text = "For recovery you need"
if count == 1:
second_line_text = "1 share."
elif count == self.input.max_count:
second_line_text = "all %s of the shares." % count
else:
second_line_text = "any %s of the shares." % count
else:
first_line_text = "The required number of "
second_line_text = "shares to form Group %s." % (self.group_id + 1)
ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG)
ui.display.text(12, 130, first_line_text, ui.NORMAL, ui.FG, ui.BG)
ui.display.text(12, 156, second_line_text, ui.NORMAL, ui.FG, ui.BG)
elif self.step is Slip39NumInput.SET_GROUPS:
ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG)
ui.display.text(
12, 130, "A group is made up of", ui.NORMAL, ui.FG, ui.BG
)
ui.display.text(12, 156, "recovery shares.", ui.NORMAL, ui.FG, ui.BG)
elif self.step is Slip39NumInput.SET_GROUP_THRESHOLD:
ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG)
ui.display.text(
12, 130, "The required number of", ui.NORMAL, ui.FG, ui.BG
)
ui.display.text(
12, 156, "groups for recovery.", ui.NORMAL, 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, count, group_index=None):
super().__init__()
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("Check seed")
elif group_index is None:
self.text = Text("Check share #%s" % (share_index + 1))
else:
self.text = Text(
"Check G%s - Share %s" % ((group_index + 1), (share_index + 1))
)
self.text.normal("Select word %d of %d:" % (word_index + 1, count))
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
if __debug__:
def read_content(self) -> list[str]:
return self.text.read_content() + [b.text for b in self.buttons]
def create_tasks(self) -> tuple[loop.Task, ...]:
return super().create_tasks() + (debug.input_signal(),)

View File

@ -15,7 +15,7 @@ LINE_WIDTH = ui.WIDTH - TEXT_MARGIN_LEFT
LINE_WIDTH_PAGINATED = LINE_WIDTH - PAGINATION_MARGIN_RIGHT LINE_WIDTH_PAGINATED = LINE_WIDTH - PAGINATION_MARGIN_RIGHT
if False: if False:
from typing import Any, Union from typing import Any, Sequence, Union
TextContent = Union[str, int] TextContent = Union[str, int]
@ -176,7 +176,7 @@ _WORKING_SPAN = Span()
def render_text( def render_text(
items: list[TextContent], items: Sequence[TextContent],
new_lines: bool, new_lines: bool,
max_lines: int, max_lines: int,
font: int = ui.NORMAL, font: int = ui.NORMAL,

View File

@ -0,0 +1,154 @@
from trezor import ui
from .button import Button
from .num_input import NumInput
from .text import Text
if False:
from trezor import loop
from typing import Callable, NoReturn, Sequence
if __debug__:
from apps import debug
class Slip39NumInput(ui.Component):
SET_SHARES = object()
SET_THRESHOLD = object()
SET_GROUPS = object()
SET_GROUP_THRESHOLD = object()
def __init__(
self,
step: object,
count: int,
min_count: int,
max_count: int,
group_id: int | None = None,
) -> None:
super().__init__()
self.step = step
self.input = NumInput(count, min_count=min_count, max_count=max_count)
self.input.on_change = self.on_change # type: ignore
self.group_id = group_id
def dispatch(self, event: int, x: int, y: int) -> None:
self.input.dispatch(event, x, y)
if event is ui.RENDER:
self.on_render()
def on_render(self) -> None:
if self.repaint:
count = self.input.count
# render the headline
if self.step is Slip39NumInput.SET_SHARES:
header = "Set num. of shares"
elif self.step is Slip39NumInput.SET_THRESHOLD:
header = "Set threshold"
elif self.step is Slip39NumInput.SET_GROUPS:
header = "Set num. of groups"
elif self.step is Slip39NumInput.SET_GROUP_THRESHOLD:
header = "Set group threshold"
ui.header(header, ui.ICON_RESET, ui.TITLE_GREY, ui.BG, ui.ORANGE_ICON)
# render the counter
if self.step is Slip39NumInput.SET_SHARES:
if self.group_id is None:
if count == 1:
first_line_text = "Only one share will"
second_line_text = "be created."
else:
first_line_text = "%s people or locations" % count
second_line_text = "will each hold one share."
else:
first_line_text = "Set the total number of"
second_line_text = "shares in Group %s." % (self.group_id + 1)
ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG)
ui.display.text(12, 130, first_line_text, ui.NORMAL, ui.FG, ui.BG)
ui.display.text(12, 156, second_line_text, ui.NORMAL, ui.FG, ui.BG)
elif self.step is Slip39NumInput.SET_THRESHOLD:
if self.group_id is None:
first_line_text = "For recovery you need"
if count == 1:
second_line_text = "1 share."
elif count == self.input.max_count:
second_line_text = "all %s of the shares." % count
else:
second_line_text = "any %s of the shares." % count
else:
first_line_text = "The required number of "
second_line_text = "shares to form Group %s." % (self.group_id + 1)
ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG)
ui.display.text(12, 130, first_line_text, ui.NORMAL, ui.FG, ui.BG)
ui.display.text(12, 156, second_line_text, ui.NORMAL, ui.FG, ui.BG)
elif self.step is Slip39NumInput.SET_GROUPS:
ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG)
ui.display.text(
12, 130, "A group is made up of", ui.NORMAL, ui.FG, ui.BG
)
ui.display.text(12, 156, "recovery shares.", ui.NORMAL, ui.FG, ui.BG)
elif self.step is Slip39NumInput.SET_GROUP_THRESHOLD:
ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG)
ui.display.text(
12, 130, "The required number of", ui.NORMAL, ui.FG, ui.BG
)
ui.display.text(
12, 156, "groups for recovery.", ui.NORMAL, ui.FG, ui.BG
)
self.repaint = False
def on_change(self, count: int) -> None:
self.repaint = True
class MnemonicWordSelect(ui.Layout):
NUM_OF_CHOICES = 3
def __init__(
self,
words: Sequence[str],
share_index: int | None,
word_index: int,
count: int,
group_index: int | None = None,
) -> None:
super().__init__()
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) # type: ignore
self.buttons.append(btn)
if share_index is None:
self.text: ui.Component = Text("Check seed")
elif group_index is None:
self.text = Text("Check share #%s" % (share_index + 1))
else:
self.text = Text(
"Check G%s - Share %s" % ((group_index + 1), (share_index + 1))
)
self.text.normal("Select word %d of %d:" % (word_index + 1, count))
def dispatch(self, event: int, x: int, y: int) -> None:
for btn in self.buttons:
btn.dispatch(event, x, y)
self.text.dispatch(event, x, y)
def select(self, word: str) -> Callable:
def fn() -> NoReturn:
raise ui.Result(word)
return fn
if __debug__:
def read_content(self) -> list[str]:
return self.text.read_content() + [b.text for b in self.buttons]
def create_tasks(self) -> tuple[loop.Task, ...]:
return super().create_tasks() + (debug.input_signal(),)

View File

@ -9,21 +9,21 @@ from trezor.ui.popup import Popup
from trezor.ui.qr import Qr from trezor.ui.qr import Qr
from trezor.utils import chunks, chunks_intersperse from trezor.utils import chunks, chunks_intersperse
from ..components.common import break_path_to_lines from ...components.common import break_path_to_lines
from ..components.common.confirm import is_confirmed, raise_if_cancelled from ...components.common.confirm import is_confirmed, raise_if_cancelled
from ..components.common.webauthn import ConfirmInfo from ...components.common.webauthn import ConfirmInfo
from ..components.tt import passphrase, pin from ...components.tt import passphrase, pin
from ..components.tt.button import ButtonCancel, ButtonDefault from ...components.tt.button import ButtonCancel, ButtonDefault
from ..components.tt.confirm import Confirm, ConfirmPageable, HoldToConfirm, Pageable from ...components.tt.confirm import Confirm, ConfirmPageable, HoldToConfirm, Pageable
from ..components.tt.scroll import ( from ...components.tt.scroll import (
PAGEBREAK, PAGEBREAK,
Paginated, Paginated,
paginate_paragraphs, paginate_paragraphs,
paginate_text, paginate_text,
) )
from ..components.tt.text import LINE_WIDTH_PAGINATED, Span, Text from ...components.tt.text import LINE_WIDTH_PAGINATED, Span, Text
from ..components.tt.webauthn import ConfirmContent from ...components.tt.webauthn import ConfirmContent
from ..constants.tt import ( from ...constants.tt import (
MONO_ADDR_PER_LINE, MONO_ADDR_PER_LINE,
MONO_HEX_PER_LINE, MONO_HEX_PER_LINE,
QR_SIZE_THRESHOLD, QR_SIZE_THRESHOLD,
@ -31,7 +31,7 @@ from ..constants.tt import (
QR_Y, QR_Y,
TEXT_MAX_LINES, TEXT_MAX_LINES,
) )
from .common import button_request, interact from ..common import button_request, interact
if False: if False:
from typing import ( from typing import (

View File

@ -0,0 +1,361 @@
from trezor import ui, utils, wire
from trezor.crypto import random
from trezor.enums import BackupType, ButtonRequestType
from ...components.common.confirm import is_confirmed, raise_if_cancelled
from ...components.tt.button import ButtonDefault
from ...components.tt.checklist import Checklist
from ...components.tt.confirm import Confirm, HoldToConfirm
from ...components.tt.info import InfoConfirm
from ...components.tt.reset import MnemonicWordSelect, Slip39NumInput
from ...components.tt.scroll import Paginated
from ...components.tt.text import Text
from ..common import interact
if False:
from typing import Sequence
NumberedWords = Sequence[tuple[int, str]]
if __debug__:
from apps import debug
async def show_share_words(
ctx: wire.GenericContext,
share_words: Sequence[str],
share_index: int | None = None,
group_index: int | None = None,
) -> None:
first, chunks, last = _split_share_into_pages(share_words)
if share_index is None:
header_title = "Recovery seed"
elif group_index is None:
header_title = "Recovery share #%s" % (share_index + 1)
else:
header_title = "Group %s - Share %s" % ((group_index + 1), (share_index + 1))
header_icon = ui.ICON_RESET
pages: list[ui.Component] = [] # ui page components
shares_words_check = [] # check we display correct data
# first page
text = Text(header_title, header_icon)
text.bold("Write down these")
text.bold("%s words:" % len(share_words))
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)
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.bold("I wrote down all %s" % len(share_words))
text.bold("words in order.")
pages.append(text)
pages[-1] = HoldToConfirm(pages[-1], cancel=False)
# pagination
paginated = Paginated(pages)
if __debug__:
word_pages = [first] + chunks + [last]
def export_displayed_words() -> None:
# export currently displayed mnemonic words into debuglink
words = [w for _, w in word_pages[paginated.page]]
debug.reset_current_words.publish(words)
paginated.on_change = export_displayed_words # type: ignore
export_displayed_words()
# make sure we display correct data
utils.ensure(share_words == shares_words_check)
# confirm the share
await raise_if_cancelled(
interact(
ctx,
paginated,
"backup_words",
ButtonRequestType.ResetDevice,
)
)
async def confirm_word(
ctx: wire.GenericContext,
share_index: int | None,
share_words: Sequence[str],
offset: int,
count: int,
group_index: int | None = None,
) -> bool:
# remove duplicates
non_duplicates = list(set(share_words))
# shuffle list
random.shuffle(non_duplicates)
# take top NUM_OF_CHOICES words
choices = non_duplicates[: MnemonicWordSelect.NUM_OF_CHOICES]
# select first of them
checked_word = choices[0]
# find its index
checked_index = share_words.index(checked_word) + offset
# shuffle again so the confirmed word is not always the first choice
random.shuffle(choices)
if __debug__:
debug.reset_word_index.publish(checked_index)
# let the user pick a word
select = MnemonicWordSelect(choices, share_index, checked_index, count, group_index)
selected_word: str = await ctx.wait(select)
# confirm it is the correct one
return selected_word == checked_word
def _split_share_into_pages(
share_words: Sequence[str],
) -> tuple[NumberedWords, list[NumberedWords], NumberedWords]:
share = list(enumerate(share_words)) # we need to keep track of the word indices
first = share[:2] # two words on the first page
length = len(share_words)
if length == 12 or length == 20 or length == 24:
middle = share[2:-2]
last = share[-2:] # two words on the last page
elif length == 33 or length == 18:
middle = share[2:]
last = [] # no words at the last page, because it does not add up
else:
# Invalid number of shares. SLIP-39 allows 20 or 33 words, BIP-39 12 or 24
raise RuntimeError
chunks = utils.chunks(middle, 4) # 4 words on the middle pages
return first, list(chunks), last
async def slip39_show_checklist(
ctx: wire.GenericContext, step: int, backup_type: BackupType
) -> None:
checklist = Checklist("Backup checklist", ui.ICON_RESET)
if backup_type is BackupType.Slip39_Basic:
checklist.add("Set number of shares")
checklist.add("Set threshold")
checklist.add(("Write down and check", "all recovery shares"))
elif backup_type is BackupType.Slip39_Advanced:
checklist.add("Set number of groups")
checklist.add("Set group threshold")
checklist.add(("Set size and threshold", "for each group"))
checklist.select(step)
await raise_if_cancelled(
interact(
ctx,
Confirm(checklist, confirm="Continue", cancel=None),
"slip39_checklist",
ButtonRequestType.ResetDevice,
)
)
async def slip39_prompt_threshold(
ctx: wire.GenericContext, num_of_shares: int, group_id: int | None = None
) -> int:
count = num_of_shares // 2 + 1
# min value of share threshold is 2 unless the number of shares is 1
# number of shares 1 is possible in advnaced slip39
min_count = min(2, num_of_shares)
max_count = num_of_shares
while True:
shares = Slip39NumInput(
Slip39NumInput.SET_THRESHOLD, count, min_count, max_count, group_id
)
confirmed = is_confirmed(
await interact(
ctx,
Confirm(
shares,
confirm="Continue",
cancel="Info",
major_confirm=True,
cancel_style=ButtonDefault,
),
"slip39_threshold",
ButtonRequestType.ResetDevice,
)
)
count = shares.input.count
if confirmed:
break
text = "The threshold sets the number of shares "
if group_id is None:
text += "needed to recover your wallet. "
text += "Set it to %s and you will need " % count
if num_of_shares == 1:
text += "1 share."
elif num_of_shares == count:
text += "all %s of your %s shares." % (count, num_of_shares)
else:
text += "any %s of your %s shares." % (count, num_of_shares)
else:
text += "needed to form a group. "
text += "Set it to %s and you will " % count
if num_of_shares == 1:
text += "need 1 share "
elif num_of_shares == count:
text += "need all %s of %s shares " % (count, num_of_shares)
else:
text += "need any %s of %s shares " % (count, num_of_shares)
text += "to form Group %s." % (group_id + 1)
info = InfoConfirm(text)
await info
return count
async def slip39_prompt_number_of_shares(
ctx: wire.GenericContext, group_id: int | None = None
) -> int:
count = 5
min_count = 1
max_count = 16
while True:
shares = Slip39NumInput(
Slip39NumInput.SET_SHARES, count, min_count, max_count, group_id
)
confirmed = is_confirmed(
await interact(
ctx,
Confirm(
shares,
confirm="Continue",
cancel="Info",
major_confirm=True,
cancel_style=ButtonDefault,
),
"slip39_shares",
ButtonRequestType.ResetDevice,
)
)
count = shares.input.count
if confirmed:
break
if group_id is None:
info = InfoConfirm(
"Each recovery share is a "
"sequence of 20 words. "
"Next you will choose "
"how many shares you "
"need to recover your "
"wallet."
)
else:
info = InfoConfirm(
"Each recovery share is a "
"sequence of 20 words. "
"Next you will choose "
"the threshold number of "
"shares needed to form "
"Group %s." % (group_id + 1)
)
await info
return count
async def slip39_advanced_prompt_number_of_groups(ctx: wire.GenericContext) -> int:
count = 5
min_count = 2
max_count = 16
while True:
shares = Slip39NumInput(Slip39NumInput.SET_GROUPS, count, min_count, max_count)
confirmed = is_confirmed(
await interact(
ctx,
Confirm(
shares,
confirm="Continue",
cancel="Info",
major_confirm=True,
cancel_style=ButtonDefault,
),
"slip39_groups",
ButtonRequestType.ResetDevice,
)
)
count = shares.input.count
if confirmed:
break
info = InfoConfirm(
"Each group has a set "
"number of shares and "
"its own threshold. In the "
"next steps you will set "
"the numbers of shares "
"and the thresholds."
)
await info
return count
async def slip39_advanced_prompt_group_threshold(
ctx: wire.GenericContext, num_of_groups: int
) -> int:
count = num_of_groups // 2 + 1
min_count = 1
max_count = num_of_groups
while True:
shares = Slip39NumInput(
Slip39NumInput.SET_GROUP_THRESHOLD, count, min_count, max_count
)
confirmed = is_confirmed(
await interact(
ctx,
Confirm(
shares,
confirm="Continue",
cancel="Info",
major_confirm=True,
cancel_style=ButtonDefault,
),
"slip39_group_threshold",
ButtonRequestType.ResetDevice,
)
)
count = shares.input.count
if confirmed:
break
else:
info = InfoConfirm(
"The group threshold "
"specifies the number of "
"groups required to "
"recover your wallet. "
)
await info
return count