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:
parent
49d6a35249
commit
80f8f7900d
35
core/mocks/generated/trezorcrypto/slip39.py
Normal file
35
core/mocks/generated/trezorcrypto/slip39.py
Normal 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'.
|
||||
"""
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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()
|
59
core/src/apps/common/mnemonic/__init__.py
Normal file
59
core/src/apps/common/mnemonic/__init__.py
Normal 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()
|
36
core/src/apps/common/mnemonic/bip39.py
Normal file
36
core/src/apps/common/mnemonic/bip39.py
Normal 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)
|
87
core/src/apps/common/mnemonic/slip39.py
Normal file
87
core/src/apps/common/mnemonic/slip39.py
Normal 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
|
||||
)
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
519
core/src/apps/management/common/layout.py
Normal file
519
core/src/apps/management/common/layout.py
Normal 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
|
@ -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,
|
||||
)
|
||||
|
@ -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"
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
@ -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),
|
||||
}
|
||||
|
55
core/src/trezor/ui/checklist.py
Normal file
55
core/src/trezor/ui/checklist.py
Normal 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
|
@ -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)
|
||||
|
63
core/src/trezor/ui/info.py
Normal file
63
core/src/trezor/ui/info.py
Normal 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)
|
@ -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)
|
||||
|
49
core/src/trezor/ui/shamir.py
Normal file
49
core/src/trezor/ui/shamir.py
Normal 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
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
@ -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),
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user