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

core: add slip39 support to reset and recovery device

This commit is contained in:
Tomas Susanka 2019-06-23 12:18:59 +02:00
parent 49d6a35249
commit 80f8f7900d
27 changed files with 1448 additions and 461 deletions

View File

@ -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'.
"""

View File

@ -37,7 +37,13 @@ async def get_keychain(ctx: wire.Context) -> Keychain:
if passphrase is None:
passphrase = await protect_by_passphrase(ctx)
cache.set_passphrase(passphrase)
root = bip32.from_mnemonic_cardano(mnemonic.restore(), passphrase)
# TODO fix for SLIP-39!
mnemonic_secret, mnemonic_type = mnemonic.get()
if mnemonic_type == mnemonic.TYPE_SLIP39:
# TODO: we need to modify bip32.from_mnemonic_cardano to accept entropy directly
raise NotImplementedError("SLIP-39 currently does not support Cardano")
else:
root = bip32.from_mnemonic_cardano(mnemonic_secret.decode(), passphrase)
# derive the namespaced root node
for i in SEED_NAMESPACE:

View File

@ -15,16 +15,24 @@ async def confirm(
confirm_style=Confirm.DEFAULT_CONFIRM_STYLE,
cancel=Confirm.DEFAULT_CANCEL,
cancel_style=Confirm.DEFAULT_CANCEL_STYLE,
major_confirm=None,
):
await ctx.call(ButtonRequest(code=code), MessageType.ButtonAck)
if content.__class__.__name__ == "Paginated":
content.pages[-1] = Confirm(
content.pages[-1], confirm, confirm_style, cancel, cancel_style
content.pages[-1],
confirm,
confirm_style,
cancel,
cancel_style,
major_confirm,
)
dialog = content
else:
dialog = Confirm(content, confirm, confirm_style, cancel, cancel_style)
dialog = Confirm(
content, confirm, confirm_style, cancel, cancel_style, major_confirm
)
if __debug__:
return await ctx.wait(dialog, confirm_signal) is CONFIRMED

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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
)

View File

@ -2,7 +2,7 @@ from micropython import const
from ubinascii import hexlify
from trezor import config
from trezor.crypto import random
from trezor.crypto import random, slip39
from apps.common import cache
@ -32,9 +32,100 @@ _AUTOLOCK_DELAY_MS = const(0x0C) # int
_NO_BACKUP = const(0x0D) # bool (0x01 or empty)
_MNEMONIC_TYPE = const(0x0E) # int
_ROTATION = const(0x0F) # int
_SLIP39 = const(0x02) # SLIP-39 namespace
_SLIP39_IN_PROGRESS = const(0x00) # bool
_SLIP39_IDENTIFIER = const(0x01) # bytes
_SLIP39_THRESHOLD = const(0x02) # int
_SLIP39_REMAINING = const(0x03) # int
_SLIP39_WORDS_COUNT = const(0x04) # int
_SLIP39_ITERATION_EXPONENT = const(0x05) # int
# Mnemonics stored during SLIP-39 recovery process.
# Each mnemonic is stored under key = index.
_SLIP39_MNEMONICS = const(0x03) # SLIP-39 mnemonics namespace
# fmt: on
def set_slip39_in_progress(val: bool):
_set_bool(_SLIP39, _SLIP39_IN_PROGRESS, val)
def is_slip39_in_progress():
return _get_bool(_SLIP39, _SLIP39_IN_PROGRESS)
def set_slip39_identifier(identifier: int):
_set_uint16(_SLIP39, _SLIP39_IDENTIFIER, identifier)
def get_slip39_identifier() -> int:
return _get_uint16(_SLIP39, _SLIP39_IDENTIFIER)
def set_slip39_threshold(threshold: int):
_set_uint8(_SLIP39, _SLIP39_THRESHOLD, threshold)
def get_slip39_threshold() -> int:
return _get_uint8(_SLIP39, _SLIP39_THRESHOLD)
def set_slip39_remaining(remaining: int):
_set_uint8(_SLIP39, _SLIP39_REMAINING, remaining)
def get_slip39_remaining() -> int:
return _get_uint8(_SLIP39, _SLIP39_REMAINING)
def set_slip39_words_count(count: int):
_set_uint8(_SLIP39, _SLIP39_WORDS_COUNT, count)
def get_slip39_words_count() -> int:
return _get_uint8(_SLIP39, _SLIP39_WORDS_COUNT)
def set_slip39_iteration_exponent(exponent: int):
# TODO: check if not > 5 bits
_set_uint8(_SLIP39, _SLIP39_ITERATION_EXPONENT, exponent)
def get_slip39_iteration_exponent() -> int:
return _get_uint8(_SLIP39, _SLIP39_ITERATION_EXPONENT)
def set_slip39_mnemonic(index: int, mnemonic: str):
config.set(_SLIP39_MNEMONICS, index, mnemonic.encode())
def get_slip39_mnemonic(index: int) -> str:
m = config.get(_SLIP39_MNEMONICS, index)
if m:
return m.decode()
return False
def get_slip39_mnemonics() -> list:
mnemonics = []
for index in range(0, slip39.MAX_SHARE_COUNT):
m = get_slip39_mnemonic(index)
if m:
mnemonics.append(m)
return mnemonics
def clear_slip39_data():
config.delete(_SLIP39, _SLIP39_IN_PROGRESS)
config.delete(_SLIP39, _SLIP39_REMAINING)
config.delete(_SLIP39, _SLIP39_THRESHOLD)
config.delete(_SLIP39, _SLIP39_WORDS_COUNT)
for index in (0, slip39.MAX_SHARE_COUNT):
config.delete(_SLIP39_MNEMONICS, index)
def _set_bool(app: int, key: int, value: bool, public: bool = False) -> None:
if value:
config.set(app, key, _TRUE_BYTE, public)
@ -57,6 +148,17 @@ def _get_uint8(app: int, key: int) -> int:
return int.from_bytes(val, "big")
def _set_uint16(app: int, key: int, val: int):
config.set(app, key, val.to_bytes(2, "big"))
def _get_uint16(app: int, key: int) -> int:
val = config.get(app, key)
if not val:
return None
return int.from_bytes(val, "big")
def _new_device_id() -> str:
return hexlify(random.bytes(12)).decode().upper()
@ -77,7 +179,9 @@ def get_rotation() -> int:
def is_initialized() -> bool:
return bool(config.get(_APP, _VERSION))
return bool(config.get(_APP, _VERSION)) and not bool(
config.get(_SLIP39, _SLIP39_IN_PROGRESS)
)
def get_label() -> str:
@ -88,10 +192,7 @@ def get_label() -> str:
def get_mnemonic_secret() -> bytes:
mnemonic = config.get(_APP, _MNEMONIC_SECRET)
if mnemonic is None:
return None
return mnemonic
return config.get(_APP, _MNEMONIC_SECRET)
def get_mnemonic_type() -> int:
@ -107,10 +208,17 @@ def get_homescreen() -> bytes:
def store_mnemonic(
secret: bytes, mnemonic_type: int, needs_backup: bool, no_backup: bool
secret: bytes,
mnemonic_type: int,
needs_backup: bool = False,
no_backup: bool = False,
) -> None:
config.set(_APP, _MNEMONIC_SECRET, secret)
_set_uint8(_APP, _MNEMONIC_TYPE, mnemonic_type)
_init(needs_backup, no_backup)
def _init(needs_backup=False, no_backup=False):
config.set(_APP, _VERSION, _STORAGE_VERSION)
_set_bool(_APP, _NO_BACKUP, no_backup)
if not no_backup:

View File

@ -16,9 +16,11 @@ async def homescreen():
def display_homescreen():
if not storage.is_initialized():
image = None
if storage.is_slip39_in_progress():
label = "Waiting for other shares"
elif not storage.is_initialized():
label = "Go to trezor.io/start"
image = None
else:
label = storage.get_label() or "My Trezor"
image = storage.get_homescreen()
@ -27,28 +29,28 @@ def display_homescreen():
image = res.load("apps/homescreen/res/bg.toif")
if storage.is_initialized() and storage.no_backup():
ui.display.bar(0, 0, ui.WIDTH, 30, ui.RED)
ui.display.text_center(ui.WIDTH // 2, 22, "SEEDLESS", ui.BOLD, ui.WHITE, ui.RED)
ui.display.bar(0, 30, ui.WIDTH, ui.HEIGHT - 30, ui.BG)
_err("SEEDLESS")
elif storage.is_initialized() and storage.unfinished_backup():
ui.display.bar(0, 0, ui.WIDTH, 30, ui.RED)
ui.display.text_center(
ui.WIDTH // 2, 22, "BACKUP FAILED!", ui.BOLD, ui.WHITE, ui.RED
)
ui.display.bar(0, 30, ui.WIDTH, ui.HEIGHT - 30, ui.BG)
_err("BACKUP FAILED!")
elif storage.is_initialized() and storage.needs_backup():
ui.display.bar(0, 0, ui.WIDTH, 30, ui.YELLOW)
ui.display.text_center(
ui.WIDTH // 2, 22, "NEEDS BACKUP!", ui.BOLD, ui.BLACK, ui.YELLOW
)
ui.display.bar(0, 30, ui.WIDTH, ui.HEIGHT - 30, ui.BG)
_warn("NEEDS BACKUP!")
elif storage.is_initialized() and not config.has_pin():
ui.display.bar(0, 0, ui.WIDTH, 30, ui.YELLOW)
ui.display.text_center(
ui.WIDTH // 2, 22, "PIN NOT SET!", ui.BOLD, ui.BLACK, ui.YELLOW
)
ui.display.bar(0, 30, ui.WIDTH, ui.HEIGHT - 30, ui.BG)
_warn("PIN NOT SET!")
elif storage.is_slip39_in_progress():
_warn("SHAMIR IN PROGRESS!")
else:
ui.display.bar(0, 0, ui.WIDTH, ui.HEIGHT, ui.BG)
ui.display.avatar(48, 48 - 10, image, ui.WHITE, ui.BLACK)
ui.display.text_center(ui.WIDTH // 2, 220, label, ui.BOLD, ui.FG, ui.BG)
def _warn(message: str):
ui.display.bar(0, 0, ui.WIDTH, 30, ui.YELLOW)
ui.display.text_center(ui.WIDTH // 2, 22, message, ui.BOLD, ui.BLACK, ui.YELLOW)
ui.display.bar(0, 30, ui.WIDTH, ui.HEIGHT - 30, ui.BG)
def _err(message: str):
ui.display.bar(0, 0, ui.WIDTH, 30, ui.RED)
ui.display.text_center(ui.WIDTH // 2, 22, message, ui.BOLD, ui.WHITE, ui.RED)
ui.display.bar(0, 30, ui.WIDTH, ui.HEIGHT - 30, ui.BG)

View File

@ -2,12 +2,8 @@ from trezor import wire
from trezor.messages.Success import Success
from apps.common import mnemonic, storage
from apps.management.reset_device import (
check_mnemonic,
show_backup_warning,
show_mnemonic,
show_wrong_entry,
)
from apps.management.common import layout
from apps.management.reset_device import backup_slip39_wallet
async def backup_device(ctx, msg):
@ -16,20 +12,18 @@ async def backup_device(ctx, msg):
if not storage.needs_backup():
raise wire.ProcessError("Seed already backed up")
words = mnemonic.restore()
# warn user about mnemonic safety
await show_backup_warning(ctx)
await layout.bip39_show_backup_warning(ctx)
storage.set_unfinished_backup(True)
storage.set_backed_up()
while True:
# show mnemonic and require confirmation of a random word
await show_mnemonic(ctx, words)
if await check_mnemonic(ctx, words):
break
await show_wrong_entry(ctx)
mnemonic_secret, mnemonic_type = mnemonic.get()
if mnemonic_type == mnemonic.TYPE_BIP39:
await layout.bip39_show_and_confirm_mnemonic(ctx, mnemonic_secret.decode())
elif mnemonic_type == mnemonic.TYPE_SLIP39:
await backup_slip39_wallet(ctx, mnemonic_secret)
storage.set_unfinished_backup(False)

View File

@ -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

View File

@ -1,15 +1,15 @@
from trezor import config, wire
from trezor.crypto import bip39
from trezor.messages.Success import Success
from trezor.pin import pin_to_int
from trezor.ui.text import Text
from apps.common import mnemonic, storage
from apps.common import storage
from apps.common.confirm import require_confirm
from apps.common.mnemonic import bip39
async def load_device(ctx, msg):
# TODO implement SLIP-39
if storage.is_initialized():
raise wire.UnexpectedMessage("Already initialized")
@ -24,10 +24,10 @@ async def load_device(ctx, msg):
text.normal("Continue only if you", "know what you are doing!")
await require_confirm(ctx, text)
secret = mnemonic.process([msg.mnemonic], mnemonic.TYPE_BIP39)
secret = bip39.process_all([msg.mnemonic])
storage.store_mnemonic(
secret=secret,
mnemonic_type=mnemonic.TYPE_BIP39,
mnemonic_type=bip39.get_type(),
needs_backup=True,
no_backup=False,
)

View File

@ -1,31 +1,28 @@
from trezor import config, ui, wire
from trezor.crypto import bip39
from trezor.crypto.hashlib import sha256
from trezor.messages import ButtonRequestType
from trezor.messages.ButtonRequest import ButtonRequest
from trezor.messages.ButtonRequestType import (
MnemonicInput,
MnemonicWordCount,
ProtectCall,
)
from trezor.messages.MessageType import ButtonAck
from trezor.messages.Success import Success
from trezor.pin import pin_to_int
from trezor.ui.mnemonic import MnemonicKeyboard
from trezor.ui.info import InfoConfirm
from trezor.ui.mnemonic_bip39 import Bip39Keyboard
from trezor.ui.mnemonic_slip39 import Slip39Keyboard
from trezor.ui.text import Text
from trezor.ui.word_select import WordSelector
from trezor.utils import consteq, format_ordinal
from trezor.utils import format_ordinal
from apps.common import mnemonic, storage
from apps.common.confirm import require_confirm
from apps.homescreen.homescreen import display_homescreen
from apps.management.change_pin import request_pin_ack, request_pin_confirm
if __debug__:
from apps.debug import input_signal
from apps.debug import confirm_signal, input_signal
async def recovery_device(ctx, msg):
"""
Recover BIP39 seed into empty device.
Recover BIP39/SLIP39 seed into empty device.
1. Ask for the number of words in recovered seed.
2. Let user type in the mnemonic words one by one.
@ -36,34 +33,53 @@ async def recovery_device(ctx, msg):
if not msg.dry_run and storage.is_initialized():
raise wire.UnexpectedMessage("Already initialized")
if not msg.dry_run:
title = "Device recovery"
text = Text(title, ui.ICON_RECOVERY)
text.normal("Do you really want to", "recover the device?", "")
else:
title = "Simulated recovery"
text = Text(title, ui.ICON_RECOVERY)
text.normal("Do you really want to", "check the recovery", "seed?")
await require_confirm(ctx, text, code=ProtectCall)
if msg.dry_run:
if config.has_pin():
curpin = await request_pin_ack(ctx, "Enter PIN", config.get_pin_rem())
if not storage.is_slip39_in_progress():
if not msg.dry_run:
title = "Device recovery"
text = Text(title, ui.ICON_RECOVERY)
text.normal("Do you really want to", "recover the device?", "")
else:
curpin = ""
if not config.check_pin(pin_to_int(curpin)):
raise wire.PinInvalid("PIN invalid")
title = "Simulated recovery"
text = Text(title, ui.ICON_RECOVERY)
text.normal("Do you really want to", "check the recovery", "seed?")
await require_confirm(ctx, text, code=ButtonRequestType.ProtectCall)
# ask for the number of words
wordcount = await request_wordcount(ctx, title)
if msg.dry_run:
if config.has_pin():
curpin = await request_pin_ack(ctx, "Enter PIN", config.get_pin_rem())
else:
curpin = ""
if not config.check_pin(pin_to_int(curpin)):
raise wire.PinInvalid("PIN invalid")
# ask for mnemonic words one by one
words = await request_mnemonic(ctx, wordcount)
# ask for the number of words
wordcount = await request_wordcount(ctx, title)
mnemonic_module = mnemonic.module_from_words_count(wordcount)
else:
wordcount = storage.get_slip39_words_count()
mnemonic_module = mnemonic.slip39
if mnemonic_module == mnemonic.slip39:
# show a note about the keyboard
await show_keyboard_info(ctx)
secret = None
while secret is None:
# ask for mnemonic words one by one
words = await request_mnemonic(
ctx, wordcount, mnemonic_module == mnemonic.slip39
)
secret = mnemonic_module.process_single(words)
# show a number of remaining mnemonics for SLIP39
if secret is None and mnemonic_module == mnemonic.slip39:
await show_remaining_slip39_mnemonics(
ctx, title, storage.get_slip39_remaining()
)
# check mnemonic validity
if msg.enforce_wordlist or msg.dry_run:
if not bip39.check(words):
# it is checked automatically in SLIP-39
if mnemonic_module == mnemonic.bip39 and (msg.enforce_wordlist or msg.dry_run):
if not mnemonic_module.check(secret):
raise wire.ProcessError("Mnemonic is not valid")
# ask for pin repeatedly
@ -72,39 +88,24 @@ async def recovery_device(ctx, msg):
else:
newpin = ""
secret = mnemonic.process([words], mnemonic.TYPE_BIP39)
# dry run
if msg.dry_run:
digest_input = sha256(secret).digest()
stored, _ = mnemonic.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"
)
mnemonic.dry_run(secret)
# save into storage
if newpin:
if msg.pin_protection:
config.change_pin(pin_to_int(""), pin_to_int(newpin))
storage.set_u2f_counter(msg.u2f_counter)
storage.load_settings(label=msg.label, use_passphrase=msg.passphrase_protection)
storage.store_mnemonic(
secret=secret,
mnemonic_type=mnemonic.TYPE_BIP39,
needs_backup=False,
no_backup=False,
)
mnemonic_module.store(secret=secret, needs_backup=False, no_backup=False)
display_homescreen()
return Success(message="Device recovered")
async def request_wordcount(ctx, title: str) -> int:
await ctx.call(ButtonRequest(code=MnemonicWordCount), ButtonAck)
await ctx.call(ButtonRequest(code=ButtonRequestType.MnemonicWordCount), ButtonAck)
text = Text(title, ui.ICON_RECOVERY)
text.normal("Number of words?")
@ -118,12 +119,15 @@ async def request_wordcount(ctx, title: str) -> int:
return count
async def request_mnemonic(ctx, count: int) -> str:
await ctx.call(ButtonRequest(code=MnemonicInput), ButtonAck)
async def request_mnemonic(ctx, count: int, slip39: bool) -> str:
await ctx.call(ButtonRequest(code=ButtonRequestType.MnemonicInput), ButtonAck)
words = []
for i in range(count):
keyboard = MnemonicKeyboard("Type the %s word:" % format_ordinal(i + 1))
if slip39:
keyboard = Slip39Keyboard("Type the %s word:" % format_ordinal(i + 1))
else:
keyboard = Bip39Keyboard("Type the %s word:" % format_ordinal(i + 1))
if __debug__:
word = await ctx.wait(keyboard, input_signal)
else:
@ -131,3 +135,33 @@ async def request_mnemonic(ctx, count: int) -> str:
words.append(word)
return " ".join(words)
async def show_keyboard_info(ctx):
await ctx.call(ButtonRequest(code=ButtonRequestType.Other), ButtonAck)
info = InfoConfirm(
"One more thing. "
"You can type the letters "
"the old-fashioned way "
"one by one or use our "
"T9 keyboard and press "
"buttons only once."
)
if __debug__:
await ctx.wait(info, confirm_signal)
else:
await ctx.wait(info)
async def show_remaining_slip39_mnemonics(ctx, title, remaining: int):
text = Text(title, ui.ICON_RECOVERY)
text.bold("Good job!")
if remaining > 1:
text.normal("%s more shares" % remaining)
else:
text.normal("%s more share" % remaining)
text.normal("needed to enter.")
await require_confirm(
ctx, text, ButtonRequestType.ProtectCall, cancel=None, confirm="Continue"
)

View File

@ -1,20 +1,13 @@
from micropython import const
from ubinascii import hexlify
from trezor import config, ui, wire
from trezor import config, wire
from trezor.crypto import bip39, hashlib, random
from trezor.messages import ButtonRequestType, MessageType
from trezor.messages import MessageType
from trezor.messages.EntropyRequest import EntropyRequest
from trezor.messages.Success import Success
from trezor.pin import pin_to_int
from trezor.ui.mnemonic import MnemonicKeyboard
from trezor.ui.scroll import Paginated
from trezor.ui.text import Text
from trezor.utils import chunks, format_ordinal
from apps.common import mnemonic, storage
from apps.common.confirm import hold_to_confirm, require_confirm
from apps.management.change_pin import request_pin_confirm
from apps.management.common import layout
if __debug__:
from apps import debug
@ -22,15 +15,10 @@ if __debug__:
async def reset_device(ctx, msg):
# validate parameters and device state
if msg.strength not in (128, 192, 256):
raise wire.ProcessError("Invalid strength (has to be 128, 192 or 256 bits)")
if msg.display_random and (msg.skip_backup or msg.no_backup):
raise wire.ProcessError("Can't show internal entropy when backup is skipped")
if storage.is_initialized():
raise wire.UnexpectedMessage("Already initialized")
_validate_reset_device(msg)
# make sure use knows he's setting up a new wallet
await show_reset_warning(ctx)
# make sure user knows he's setting up a new wallet
await layout.show_reset_device_warning(ctx, msg.slip39)
# request new PIN
if msg.pin_protection:
@ -39,165 +27,100 @@ async def reset_device(ctx, msg):
newpin = ""
# generate and display internal entropy
internal_ent = random.bytes(32)
int_entropy = random.bytes(32)
if __debug__:
debug.reset_internal_entropy = internal_ent
debug.reset_internal_entropy = int_entropy
if msg.display_random:
await show_entropy(ctx, internal_ent)
await layout.show_internal_entropy(ctx, int_entropy)
# request external entropy and compute mnemonic
ent_ack = await ctx.call(EntropyRequest(), MessageType.EntropyAck)
words = generate_mnemonic(msg.strength, internal_ent, ent_ack.entropy)
# request external entropy and compute the master secret
entropy_ack = await ctx.call(EntropyRequest(), MessageType.EntropyAck)
ext_entropy = entropy_ack.entropy
secret = _compute_secret_from_entropy(int_entropy, ext_entropy, msg.strength)
if not msg.skip_backup and not msg.no_backup:
# require confirmation of the mnemonic safety
await show_backup_warning(ctx)
# should we back up the wallet now?
if not msg.no_backup and not msg.skip_backup:
if not await layout.confirm_backup(ctx):
if not await layout.confirm_backup_again(ctx):
msg.skip_backup = True
# show mnemonic and require confirmation of a random word
while True:
await show_mnemonic(ctx, words)
if await check_mnemonic(ctx, words):
break
await show_wrong_entry(ctx)
# generate and display backup information for the master secret
if not msg.no_backup and not msg.skip_backup:
if msg.slip39:
await backup_slip39_wallet(ctx, secret)
else:
await backup_bip39_wallet(ctx, secret)
# write PIN into storage
if newpin:
if not config.change_pin(pin_to_int(""), pin_to_int(newpin)):
raise wire.ProcessError("Could not change PIN")
if not config.change_pin(pin_to_int(""), pin_to_int(newpin)):
raise wire.ProcessError("Could not change PIN")
secret = mnemonic.process([words], mnemonic.TYPE_BIP39)
# write settings and mnemonic into storage
# write settings and master secret into storage
storage.load_settings(label=msg.label, use_passphrase=msg.passphrase_protection)
storage.store_mnemonic(
secret=secret,
mnemonic_type=mnemonic.TYPE_BIP39,
needs_backup=msg.skip_backup,
no_backup=msg.no_backup,
)
if msg.slip39:
mnemonic.slip39.store(
secret=secret, needs_backup=msg.skip_backup, no_backup=msg.no_backup
)
else:
# in BIP-39 we store mnemonic string instead of the secret
mnemonic.bip39.store(
secret=bip39.from_data(secret).encode(),
needs_backup=msg.skip_backup,
no_backup=msg.no_backup,
)
# show success message
if not msg.skip_backup and not msg.no_backup:
await show_success(ctx)
# if we backed up the wallet, show success message
if not msg.no_backup and not msg.skip_backup:
await layout.show_backup_success(ctx)
return Success(message="Initialized")
def generate_mnemonic(strength: int, int_entropy: bytes, ext_entropy: bytes) -> bytes:
async def backup_slip39_wallet(ctx, secret: bytes):
# get number of shares
await layout.slip39_show_checklist_set_shares(ctx)
shares_count = await layout.slip39_prompt_number_of_shares(ctx)
# get threshold
await layout.slip39_show_checklist_set_threshold(ctx, shares_count)
threshold = await layout.slip39_prompt_threshold(ctx, shares_count)
# generate the mnemonics
mnemonics = mnemonic.slip39.generate_from_secret(secret, shares_count, threshold)
# show and confirm individual shares
await layout.slip39_show_checklist_show_shares(ctx, shares_count, threshold)
await layout.slip39_show_and_confirm_shares(ctx, mnemonics)
async def backup_bip39_wallet(ctx, secret: bytes):
mnemonic = bip39.from_data(secret)
await layout.bip39_show_and_confirm_mnemonic(ctx, mnemonic)
def _validate_reset_device(msg):
if msg.strength not in (128, 256):
if msg.slip39:
raise wire.ProcessError("Invalid strength (has to be 128 or 256 bits)")
elif msg.strength != 192:
raise wire.ProcessError("Invalid strength (has to be 128, 192 or 256 bits)")
if msg.display_random and (msg.skip_backup or msg.no_backup):
raise wire.ProcessError("Can't show internal entropy when backup is skipped")
if storage.is_initialized():
raise wire.UnexpectedMessage("Already initialized")
if (msg.skip_backup or msg.no_backup) and msg.slip39:
raise wire.ProcessError("Both no/skip backup flag and Shamir SLIP-39 required.")
def _compute_secret_from_entropy(
int_entropy: bytes, ext_entropy: bytes, strength_in_bytes: int
) -> bytes:
# combine internal and external entropy
ehash = hashlib.sha256()
ehash.update(int_entropy)
ehash.update(ext_entropy)
entropy = ehash.digest()
return bip39.from_data(entropy[: strength // 8])
async def show_reset_warning(ctx):
text = Text("Create a new wallet", ui.ICON_RESET, new_lines=False)
text.normal("Do you really want to")
text.br()
text.normal("create 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, code=ButtonRequestType.ResetDevice)
async def 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 show_wrong_entry(ctx):
text = Text("Wrong entry!", ui.ICON_WRONG, ui.RED)
text.normal("You have entered", "wrong seed word.", "Please check again.")
await require_confirm(
ctx, text, ButtonRequestType.ResetDevice, confirm="Check again", cancel=None
)
async def show_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 show_entropy(ctx, entropy: bytes):
entropy_str = hexlify(entropy).decode()
lines = chunks(entropy_str, 16)
text = Text("Internal entropy", ui.ICON_RESET)
text.mono(*lines)
await require_confirm(ctx, text, ButtonRequestType.ResetDevice)
async def show_mnemonic(ctx, mnemonic: str):
# split mnemonic words into pages
PER_PAGE = const(4)
words = mnemonic.split()
words = list(enumerate(words))
words = list(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
async def check_mnemonic(ctx, mnemonic: str) -> bool:
words = mnemonic.split()
# check a word from the first half
index = random.uniform(len(words) // 2)
if not await check_word(ctx, words, index):
return False
# check a word from the second half
index = random.uniform(len(words) // 2) + len(words) // 2
if not await check_word(ctx, words, index):
return False
return True
async def check_word(ctx, words: list, index: int):
if __debug__:
debug.reset_word_index = index
keyboard = MnemonicKeyboard("Type the %s word:" % format_ordinal(index + 1))
if __debug__:
result = await ctx.wait(keyboard, debug.input_signal)
else:
result = await ctx.wait(keyboard)
return result == words[index]
# take a required number of bytes
strength = strength_in_bytes // 8
secret = entropy[:strength]
return secret

View File

@ -21,8 +21,23 @@
from micropython import const
from trezor.crypto import hashlib, hmac, pbkdf2, random
from trezor.crypto.slip39_wordlist import wordlist
from trezorcrypto import shamir
from trezorcrypto import shamir, slip39
_KEYBOARD_FULL_MASK = const(0x1FF)
"""All buttons are allowed. 9-bit bitmap all set to 1."""
def compute_mask(prefix: str) -> int:
if not prefix:
return _KEYBOARD_FULL_MASK
return slip39.compute_mask(int(prefix))
def button_sequence_to_word(prefix: str) -> str:
if not prefix:
return _KEYBOARD_FULL_MASK
return slip39.button_sequence_to_word(int(prefix))
_RADIX_BITS = const(10)
"""The length of the radix in bits."""
@ -36,6 +51,11 @@ def bits_to_words(n):
return (n + _RADIX_BITS - 1) // _RADIX_BITS
MAX_SHARE_COUNT = const(16)
"""The maximum number of shares that can be created."""
DEFAULT_ITERATION_EXPONENT = const(0)
_RADIX = 2 ** _RADIX_BITS
"""The number of words in the wordlist."""
@ -48,9 +68,6 @@ _ITERATION_EXP_LENGTH_BITS = const(5)
_ID_EXP_LENGTH_WORDS = bits_to_words(_ID_LENGTH_BITS + _ITERATION_EXP_LENGTH_BITS)
"""The length of the random identifier and iteration exponent in words."""
_MAX_SHARE_COUNT = const(16)
"""The maximum number of shares that can be created."""
_CHECKSUM_LENGTH_WORDS = const(3)
"""The length of the RS1024 checksum in words."""
@ -86,21 +103,6 @@ class MnemonicError(Exception):
pass
def word_index(word):
word = word + " " * (8 - len(word))
lo = 0
hi = _RADIX
while hi - lo > 1:
mid = (hi + lo) // 2
if wordlist[mid * 8 : mid * 8 + 8] > word:
hi = mid
else:
lo = mid
if wordlist[lo * 8 : lo * 8 + 8] != word:
raise MnemonicError('Invalid mnemonic word "{}".'.format(word))
return lo
def _rs1024_polymod(values):
GEN = (
0xE0E040,
@ -181,11 +183,11 @@ def _int_to_indices(value, length, bits):
def mnemonic_from_indices(indices):
return " ".join(wordlist[i * 8 : i * 8 + 8].strip() for i in indices)
return " ".join(slip39.get_word(i) for i in indices)
def mnemonic_to_indices(mnemonic):
return (word_index(word.lower()) for word in mnemonic.split())
return (slip39.word_index(word.lower()) for word in mnemonic.split())
def _round_function(i, passphrase, e, salt, r):
@ -247,10 +249,10 @@ def _split_secret(threshold, share_count, shared_secret):
)
)
if share_count > _MAX_SHARE_COUNT:
if share_count > MAX_SHARE_COUNT:
raise ValueError(
"The requested number of shares ({}) must not exceed {}.".format(
share_count, _MAX_SHARE_COUNT
share_count, MAX_SHARE_COUNT
)
)
@ -462,16 +464,33 @@ def _decode_mnemonics(mnemonics):
)
def _generate_random_identifier():
def _generate_random_identifier() -> int:
"""Returns a randomly generated integer in the range 0, ... , 2**_ID_LENGTH_BITS - 1."""
identifier = int.from_bytes(random.bytes(bits_to_bytes(_ID_LENGTH_BITS)), "big")
return identifier & ((1 << _ID_LENGTH_BITS) - 1)
def generate_mnemonics(
group_threshold, groups, master_secret, passphrase=b"", iteration_exponent=0
):
def generate_single_group_mnemonics_from_data(
master_secret,
threshold,
count,
passphrase=b"",
iteration_exponent=DEFAULT_ITERATION_EXPONENT,
) -> (int, list):
identifier, mnemonics = generate_mnemonics_from_data(
master_secret, 1, [(threshold, count)], passphrase, iteration_exponent
)
return identifier, mnemonics[0]
def generate_mnemonics_from_data(
master_secret,
group_threshold,
groups,
passphrase=b"",
iteration_exponent=DEFAULT_ITERATION_EXPONENT,
) -> (int, list):
"""
Splits a master secret into mnemonic shares using Shamir's secret sharing scheme.
:param int group_threshold: The number of groups required to reconstruct the master secret.
@ -486,6 +505,8 @@ def generate_mnemonics(
:param int iteration_exponent: The iteration exponent.
:return: List of mnemonics.
:rtype: List of byte arrays.
:return: Identifier.
:rtype: int.
"""
identifier = _generate_random_identifier()
@ -528,68 +549,28 @@ def generate_mnemonics(
group_shares = _split_secret(group_threshold, len(groups), encrypted_master_secret)
return [
[
encode_mnemonic(
identifier,
iteration_exponent,
group_index,
group_threshold,
len(groups),
member_index,
member_threshold,
value,
mnemonics = []
for (member_threshold, member_count), (group_index, group_secret) in zip(
groups, group_shares
):
group_mnemonics = []
for member_index, value in _split_secret(
member_threshold, member_count, group_secret
):
group_mnemonics.append(
encode_mnemonic(
identifier,
iteration_exponent,
group_index,
group_threshold,
len(groups),
member_index,
member_threshold,
value,
)
)
for member_index, value in _split_secret(
member_threshold, member_count, group_secret
)
]
for (member_threshold, member_count), (group_index, group_secret) in zip(
groups, group_shares
)
]
def generate_mnemonics_random(
group_threshold, groups, strength_bits=128, passphrase=b"", iteration_exponent=0
):
"""
Generates a random master secret and splits it into mnemonic shares using Shamir's secret
sharing scheme.
:param int group_threshold: The number of groups required to reconstruct the master secret.
:param groups: A list of (member_threshold, member_count) pairs for each group, where member_count
is the number of shares to generate for the group and member_threshold is the number of members required to
reconstruct the group secret.
:type groups: List of pairs of integers.
:param int strength_bits: The entropy of the randomly generated master secret in bits.
:param passphrase: The passphrase used to encrypt the master secret.
:type passphrase: Array of bytes.
:param int iteration_exponent: The iteration exponent.
:return: List of mnemonics.
:rtype: List of byte arrays.
"""
if strength_bits < _MIN_STRENGTH_BITS:
raise ValueError(
"The requested strength of the master secret ({} bits) must be at least {} bits.".format(
strength_bits, _MIN_STRENGTH_BITS
)
)
if strength_bits % 16 != 0:
raise ValueError(
"The requested strength of the master secret ({} bits) must be a multiple of 16 bits.".format(
strength_bits
)
)
return generate_mnemonics(
group_threshold,
groups,
random.bytes(strength_bits // 8),
passphrase,
iteration_exponent,
)
mnemonics.append(group_mnemonics)
return identifier, mnemonics
def combine_mnemonics(mnemonics):
@ -597,7 +578,7 @@ def combine_mnemonics(mnemonics):
Combines mnemonic shares to obtain the master secret which was previously split using
Shamir's secret sharing scheme.
:param mnemonics: List of mnemonics.
:type mnemonics: List of byte arrays.
:type mnemonics: List of strings.
:return: Identifier, iteration exponent, the encrypted master secret.
:rtype: Integer, integer, array of bytes.
"""

File diff suppressed because one or more lines are too long

View File

@ -17,6 +17,7 @@ class ResetDevice(p.MessageType):
u2f_counter: int = None,
skip_backup: bool = None,
no_backup: bool = None,
slip39: bool = None,
) -> None:
self.display_random = display_random
self.strength = strength
@ -27,6 +28,7 @@ class ResetDevice(p.MessageType):
self.u2f_counter = u2f_counter
self.skip_backup = skip_backup
self.no_backup = no_backup
self.slip39 = slip39
@classmethod
def get_fields(cls):
@ -40,4 +42,5 @@ class ResetDevice(p.MessageType):
7: ('u2f_counter', p.UVarintType, 0),
8: ('skip_backup', p.BoolType, 0),
9: ('no_backup', p.BoolType, 0),
10: ('slip39', p.BoolType, 0),
}

View File

@ -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

View File

@ -19,12 +19,15 @@ class Confirm(ui.Layout):
confirm_style=DEFAULT_CONFIRM_STYLE,
cancel=DEFAULT_CANCEL,
cancel_style=DEFAULT_CANCEL_STYLE,
major_confirm=False,
):
self.content = content
if confirm is not None:
if cancel is None:
area = ui.grid(4, n_x=1)
elif major_confirm:
area = ui.grid(13, cells_x=2)
else:
area = ui.grid(9, n_x=2)
self.confirm = Button(area, confirm, confirm_style)
@ -35,6 +38,8 @@ class Confirm(ui.Layout):
if cancel is not None:
if confirm is None:
area = ui.grid(4, n_x=1)
elif major_confirm:
area = ui.grid(12, cells_x=1)
else:
area = ui.grid(8, n_x=2)
self.cancel = Button(area, cancel, cancel_style)

View File

@ -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)

View File

@ -77,6 +77,26 @@ class Loader(ui.Control):
self.start_ms = None
self.stop_ms = None
self.on_start()
if r == target:
self.on_finish()
def on_start(self):
pass
def on_finish(self):
pass
class LoadingAnimation(ui.Layout):
def __init__(self, style=LoaderDefault):
self.loader = Loader(style)
self.loader.on_finish = self.on_finish
self.loader.start()
def dispatch(self, event, x, y):
if not self.loader.elapsed_ms():
self.loader.start()
self.loader.dispatch(event, x, y)
def on_finish(self):
raise ui.Result(None)

View File

@ -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

View File

@ -13,15 +13,21 @@ BR = const(-256)
BR_HALF = const(-257)
def render_text(words: list, new_lines: bool, max_lines: int) -> None:
def render_text(
words: list,
new_lines: bool,
max_lines: int,
font: int = ui.NORMAL,
fg: int = ui.FG,
bg: int = ui.BG,
offset_x: int = TEXT_MARGIN_LEFT,
offset_y: int = TEXT_HEADER_HEIGHT + TEXT_LINE_HEIGHT,
offset_x_max: int = ui.WIDTH,
) -> None:
# initial rendering state
font = ui.NORMAL
fg = ui.FG
bg = ui.BG
offset_x = TEXT_MARGIN_LEFT
offset_y = TEXT_HEADER_HEIGHT + TEXT_LINE_HEIGHT
OFFSET_X_MAX = ui.WIDTH
OFFSET_Y_MAX = TEXT_HEADER_HEIGHT + TEXT_LINE_HEIGHT * max_lines
INITIAL_OFFSET_X = offset_x
offset_y_max = offset_y * max_lines
FONTS = (ui.NORMAL, ui.BOLD, ui.MONO, ui.MONO_BOLD)
# sizes of common glyphs
@ -35,10 +41,10 @@ def render_text(words: list, new_lines: bool, max_lines: int) -> None:
if isinstance(word, int):
if word is BR or word is BR_HALF:
# line break or half-line break
if offset_y >= OFFSET_Y_MAX:
if offset_y >= offset_y_max:
ui.display.text(offset_x, offset_y, "...", ui.BOLD, ui.GREY, bg)
return
offset_x = TEXT_MARGIN_LEFT
offset_x = INITIAL_OFFSET_X
offset_y += TEXT_LINE_HEIGHT if word is BR else TEXT_LINE_HEIGHT_HALF
elif word in FONTS:
# change of font style
@ -50,22 +56,22 @@ def render_text(words: list, new_lines: bool, max_lines: int) -> None:
width = ui.display.text_width(word, font)
while offset_x + width > OFFSET_X_MAX or (
has_next_word and offset_y >= OFFSET_Y_MAX
while offset_x + width > offset_x_max or (
has_next_word and offset_y >= offset_y_max
):
beginning_of_line = offset_x == TEXT_MARGIN_LEFT
word_fits_in_one_line = width < (OFFSET_X_MAX - TEXT_MARGIN_LEFT)
beginning_of_line = offset_x == INITIAL_OFFSET_X
word_fits_in_one_line = width < (offset_x_max - INITIAL_OFFSET_X)
if (
offset_y < OFFSET_Y_MAX
offset_y < offset_y_max
and word_fits_in_one_line
and not beginning_of_line
):
# line break
offset_x = TEXT_MARGIN_LEFT
offset_x = INITIAL_OFFSET_X
offset_y += TEXT_LINE_HEIGHT
break
# word split
if offset_y < OFFSET_Y_MAX:
if offset_y < offset_y_max:
split = "-"
splitw = DASH
else:
@ -75,7 +81,7 @@ def render_text(words: list, new_lines: bool, max_lines: int) -> None:
for index in range(len(word) - 1, 0, -1):
letter = word[index]
width -= ui.display.text_width(letter, font)
if offset_x + width + splitw < OFFSET_X_MAX:
if offset_x + width + splitw < offset_x_max:
break
else:
index = 0
@ -84,9 +90,9 @@ def render_text(words: list, new_lines: bool, max_lines: int) -> None:
ui.display.text(offset_x, offset_y, span, font, fg, bg)
ui.display.text(offset_x + width, offset_y, split, ui.BOLD, ui.GREY, bg)
# line break
if offset_y >= OFFSET_Y_MAX:
if offset_y >= offset_y_max:
return
offset_x = TEXT_MARGIN_LEFT
offset_x = INITIAL_OFFSET_X
offset_y += TEXT_LINE_HEIGHT
# continue with the rest
word = word[index:]
@ -97,10 +103,10 @@ def render_text(words: list, new_lines: bool, max_lines: int) -> None:
if new_lines and has_next_word:
# line break
if offset_y >= OFFSET_Y_MAX:
if offset_y >= offset_y_max:
ui.display.text(offset_x, offset_y, "...", ui.BOLD, ui.GREY, bg)
return
offset_x = TEXT_MARGIN_LEFT
offset_x = INITIAL_OFFSET_X
offset_y += TEXT_LINE_HEIGHT
else:
# shift cursor
@ -158,3 +164,31 @@ class Text(ui.Control):
)
render_text(self.content, self.new_lines, self.max_lines)
self.repaint = False
LABEL_LEFT = const(0)
LABEL_CENTER = const(1)
LABEL_RIGHT = const(2)
class Label(ui.Control):
def __init__(self, area, content, align=LABEL_LEFT, style=ui.NORMAL):
self.area = area
self.content = content
self.align = align
self.style = style
self.repaint = True
def on_render(self):
if self.repaint:
align = self.align
ax, ay, aw, ah = self.area
tx = ax + aw // 2
ty = ay + ah // 2 + 8
if align is LABEL_LEFT:
ui.display.text_left(tx, ty, self.content, self.style, ui.FG, ui.BG)
elif align is LABEL_CENTER:
ui.display.text_center(tx, ty, self.content, self.style, ui.FG, ui.BG)
elif align is LABEL_RIGHT:
ui.display.text_right(tx, ty, self.content, self.style, ui.FG, ui.BG)
self.repaint = False

View File

@ -1,22 +1,30 @@
from trezor import ui
from trezor.ui.button import Button
# todo improve?
class WordSelector(ui.Layout):
def __init__(self, content):
self.content = content
self.w12 = Button(ui.grid(6, n_y=4, n_x=3, cells_y=2), "12")
self.w12 = Button(ui.grid(6, n_y=4), "12")
self.w12.on_click = self.on_w12
self.w18 = Button(ui.grid(7, n_y=4, n_x=3, cells_y=2), "18")
self.w18 = Button(ui.grid(7, n_y=4), "18")
self.w18.on_click = self.on_w18
self.w24 = Button(ui.grid(8, n_y=4, n_x=3, cells_y=2), "24")
self.w20 = Button(ui.grid(8, n_y=4), "20")
self.w20.on_click = self.on_w20
self.w24 = Button(ui.grid(9, n_y=4), "24")
self.w24.on_click = self.on_w24
self.w33 = Button(ui.grid(10, n_y=4), "33")
self.w33.on_click = self.on_w33
def dispatch(self, event, x, y):
self.content.dispatch(event, x, y)
self.w12.dispatch(event, x, y)
self.w18.dispatch(event, x, y)
self.w20.dispatch(event, x, y)
self.w24.dispatch(event, x, y)
self.w33.dispatch(event, x, y)
def on_w12(self):
raise ui.Result(12)
@ -24,5 +32,11 @@ class WordSelector(ui.Layout):
def on_w18(self):
raise ui.Result(18)
def on_w20(self):
raise ui.Result(20)
def on_w24(self):
raise ui.Result(24)
def on_w33(self):
raise ui.Result(33)

View File

@ -25,12 +25,15 @@ class TestCryptoSlip39(unittest.TestCase):
MS = b"ABCDEFGHIJKLMNOP"
def test_basic_sharing_random(self):
mnemonics = slip39.generate_mnemonics_random(1, [(3, 5)])[0]
ms = random.bytes(32)
_, mnemonics = slip39.generate_mnemonics_from_data(ms, 1, [(3, 5)])
mnemonics = mnemonics[0]
self.assertEqual(slip39.combine_mnemonics(mnemonics[:3]), slip39.combine_mnemonics(mnemonics[2:]))
def test_basic_sharing_fixed(self):
mnemonics = slip39.generate_mnemonics(1, [(3, 5)], self.MS)[0]
_, mnemonics = slip39.generate_mnemonics_from_data(self.MS, 1, [(3, 5)])
mnemonics = mnemonics[0]
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics[:3])
self.assertEqual(slip39.decrypt(identifier, exponent, ems, b""), self.MS)
self.assertEqual(slip39.combine_mnemonics(mnemonics[1:4])[2], ems)
@ -39,19 +42,22 @@ class TestCryptoSlip39(unittest.TestCase):
def test_passphrase(self):
mnemonics = slip39.generate_mnemonics(1, [(3, 5)], self.MS, b"TREZOR")[0]
_, mnemonics = slip39.generate_mnemonics_from_data(self.MS, 1, [(3, 5)], b"TREZOR")
mnemonics = mnemonics[0]
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics[1:4])
self.assertEqual(slip39.decrypt(identifier, exponent, ems, b"TREZOR"), self.MS)
self.assertNotEqual(slip39.decrypt(identifier, exponent, ems, b""), self.MS)
def test_iteration_exponent(self):
mnemonics = slip39.generate_mnemonics(1, [(3, 5)], self.MS, b"TREZOR", 1)[0]
_, mnemonics = slip39.generate_mnemonics_from_data(self.MS, 1, [(3, 5)], b"TREZOR", 1)
mnemonics = mnemonics[0]
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics[1:4])
self.assertEqual(slip39.decrypt(identifier, exponent, ems, b"TREZOR"), self.MS)
self.assertNotEqual(slip39.decrypt(identifier, exponent, ems, b""), self.MS)
mnemonics = slip39.generate_mnemonics(1, [(3, 5)], self.MS, b"TREZOR", 2)[0]
_, mnemonics = slip39.generate_mnemonics_from_data(self.MS, 1, [(3, 5)], b"TREZOR", 2)
mnemonics = mnemonics[0]
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics[1:4])
self.assertEqual(slip39.decrypt(identifier, exponent, ems, b"TREZOR"), self.MS)
self.assertNotEqual(slip39.decrypt(identifier, exponent, ems, b""), self.MS)
@ -61,8 +67,8 @@ class TestCryptoSlip39(unittest.TestCase):
group_threshold = 2
group_sizes = (5, 3, 5, 1)
member_thresholds = (3, 2, 2, 1)
mnemonics = slip39.generate_mnemonics(
group_threshold, list(zip(member_thresholds, group_sizes)), self.MS
_, mnemonics = slip39.generate_mnemonics_from_data(
self.MS, group_threshold, list(zip(member_thresholds, group_sizes))
)
# Test all valid combinations of mnemonics.
@ -93,8 +99,8 @@ class TestCryptoSlip39(unittest.TestCase):
group_threshold = 1
group_sizes = (5, 3, 5, 1)
member_thresholds = (3, 2, 2, 1)
mnemonics = slip39.generate_mnemonics(
group_threshold, list(zip(member_thresholds, group_sizes)), self.MS
_, mnemonics = slip39.generate_mnemonics_from_data(
self.MS, group_threshold, list(zip(member_thresholds, group_sizes))
)
# Test all valid combinations of mnemonics.
@ -108,8 +114,8 @@ class TestCryptoSlip39(unittest.TestCase):
def test_all_groups_exist(self):
for group_threshold in (1, 2, 5):
mnemonics = slip39.generate_mnemonics(
group_threshold, [(3, 5), (1, 1), (2, 3), (2, 5), (3, 5)], self.MS
_, mnemonics = slip39.generate_mnemonics_from_data(
self.MS, group_threshold, [(3, 5), (1, 1), (2, 3), (2, 5), (3, 5)]
)
self.assertEqual(len(mnemonics), 5)
self.assertEqual(len(sum(mnemonics, [])), 19)
@ -118,31 +124,31 @@ class TestCryptoSlip39(unittest.TestCase):
def test_invalid_sharing(self):
# Short master secret.
with self.assertRaises(ValueError):
slip39.generate_mnemonics(1, [(2, 3)], self.MS[:14])
slip39.generate_mnemonics_from_data(self.MS[:14], 1, [(2, 3)])
# Odd length master secret.
with self.assertRaises(ValueError):
slip39.generate_mnemonics(1, [(2, 3)], self.MS + b"X")
slip39.generate_mnemonics_from_data(self.MS + b"X", 1, [(2, 3)])
# Group threshold exceeds number of groups.
with self.assertRaises(ValueError):
slip39.generate_mnemonics(3, [(3, 5), (2, 5)], self.MS)
slip39.generate_mnemonics_from_data(self.MS, 3, [(3, 5), (2, 5)])
# Invalid group threshold.
with self.assertRaises(ValueError):
slip39.generate_mnemonics(0, [(3, 5), (2, 5)], self.MS)
slip39.generate_mnemonics_from_data(self.MS, 0, [(3, 5), (2, 5)])
# Member threshold exceeds number of members.
with self.assertRaises(ValueError):
slip39.generate_mnemonics(2, [(3, 2), (2, 5)], self.MS)
slip39.generate_mnemonics_from_data(self.MS, 2, [(3, 2), (2, 5)])
# Invalid member threshold.
with self.assertRaises(ValueError):
slip39.generate_mnemonics(2, [(0, 2), (2, 5)], self.MS)
slip39.generate_mnemonics_from_data(self.MS, 2, [(0, 2), (2, 5)])
# Group with multiple members and threshold 1.
with self.assertRaises(ValueError):
slip39.generate_mnemonics(2, [(3, 5), (1, 3), (2, 5)], self.MS)
slip39.generate_mnemonics_from_data(self.MS, 2, [(3, 5), (1, 3), (2, 5)])
def test_vectors(self):

File diff suppressed because one or more lines are too long

View File

@ -17,6 +17,7 @@ class ResetDevice(p.MessageType):
u2f_counter: int = None,
skip_backup: bool = None,
no_backup: bool = None,
slip39: bool = None,
) -> None:
self.display_random = display_random
self.strength = strength
@ -27,6 +28,7 @@ class ResetDevice(p.MessageType):
self.u2f_counter = u2f_counter
self.skip_backup = skip_backup
self.no_backup = no_backup
self.slip39 = slip39
@classmethod
def get_fields(cls):
@ -40,4 +42,5 @@ class ResetDevice(p.MessageType):
7: ('u2f_counter', p.UVarintType, 0),
8: ('skip_backup', p.BoolType, 0),
9: ('no_backup', p.BoolType, 0),
10: ('slip39', p.BoolType, 0),
}