mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-28 17:18:29 +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:
|
if passphrase is None:
|
||||||
passphrase = await protect_by_passphrase(ctx)
|
passphrase = await protect_by_passphrase(ctx)
|
||||||
cache.set_passphrase(passphrase)
|
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
|
# derive the namespaced root node
|
||||||
for i in SEED_NAMESPACE:
|
for i in SEED_NAMESPACE:
|
||||||
|
@ -15,16 +15,24 @@ async def confirm(
|
|||||||
confirm_style=Confirm.DEFAULT_CONFIRM_STYLE,
|
confirm_style=Confirm.DEFAULT_CONFIRM_STYLE,
|
||||||
cancel=Confirm.DEFAULT_CANCEL,
|
cancel=Confirm.DEFAULT_CANCEL,
|
||||||
cancel_style=Confirm.DEFAULT_CANCEL_STYLE,
|
cancel_style=Confirm.DEFAULT_CANCEL_STYLE,
|
||||||
|
major_confirm=None,
|
||||||
):
|
):
|
||||||
await ctx.call(ButtonRequest(code=code), MessageType.ButtonAck)
|
await ctx.call(ButtonRequest(code=code), MessageType.ButtonAck)
|
||||||
|
|
||||||
if content.__class__.__name__ == "Paginated":
|
if content.__class__.__name__ == "Paginated":
|
||||||
content.pages[-1] = Confirm(
|
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
|
dialog = content
|
||||||
else:
|
else:
|
||||||
dialog = Confirm(content, confirm, confirm_style, cancel, cancel_style)
|
dialog = Confirm(
|
||||||
|
content, confirm, confirm_style, cancel, cancel_style, major_confirm
|
||||||
|
)
|
||||||
|
|
||||||
if __debug__:
|
if __debug__:
|
||||||
return await ctx.wait(dialog, confirm_signal) is CONFIRMED
|
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 ubinascii import hexlify
|
||||||
|
|
||||||
from trezor import config
|
from trezor import config
|
||||||
from trezor.crypto import random
|
from trezor.crypto import random, slip39
|
||||||
|
|
||||||
from apps.common import cache
|
from apps.common import cache
|
||||||
|
|
||||||
@ -32,9 +32,100 @@ _AUTOLOCK_DELAY_MS = const(0x0C) # int
|
|||||||
_NO_BACKUP = const(0x0D) # bool (0x01 or empty)
|
_NO_BACKUP = const(0x0D) # bool (0x01 or empty)
|
||||||
_MNEMONIC_TYPE = const(0x0E) # int
|
_MNEMONIC_TYPE = const(0x0E) # int
|
||||||
_ROTATION = const(0x0F) # 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
|
# 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:
|
def _set_bool(app: int, key: int, value: bool, public: bool = False) -> None:
|
||||||
if value:
|
if value:
|
||||||
config.set(app, key, _TRUE_BYTE, public)
|
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")
|
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:
|
def _new_device_id() -> str:
|
||||||
return hexlify(random.bytes(12)).decode().upper()
|
return hexlify(random.bytes(12)).decode().upper()
|
||||||
|
|
||||||
@ -77,7 +179,9 @@ def get_rotation() -> int:
|
|||||||
|
|
||||||
|
|
||||||
def is_initialized() -> bool:
|
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:
|
def get_label() -> str:
|
||||||
@ -88,10 +192,7 @@ def get_label() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def get_mnemonic_secret() -> bytes:
|
def get_mnemonic_secret() -> bytes:
|
||||||
mnemonic = config.get(_APP, _MNEMONIC_SECRET)
|
return config.get(_APP, _MNEMONIC_SECRET)
|
||||||
if mnemonic is None:
|
|
||||||
return None
|
|
||||||
return mnemonic
|
|
||||||
|
|
||||||
|
|
||||||
def get_mnemonic_type() -> int:
|
def get_mnemonic_type() -> int:
|
||||||
@ -107,10 +208,17 @@ def get_homescreen() -> bytes:
|
|||||||
|
|
||||||
|
|
||||||
def store_mnemonic(
|
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:
|
) -> None:
|
||||||
config.set(_APP, _MNEMONIC_SECRET, secret)
|
config.set(_APP, _MNEMONIC_SECRET, secret)
|
||||||
_set_uint8(_APP, _MNEMONIC_TYPE, mnemonic_type)
|
_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)
|
config.set(_APP, _VERSION, _STORAGE_VERSION)
|
||||||
_set_bool(_APP, _NO_BACKUP, no_backup)
|
_set_bool(_APP, _NO_BACKUP, no_backup)
|
||||||
if not no_backup:
|
if not no_backup:
|
||||||
|
@ -16,9 +16,11 @@ async def homescreen():
|
|||||||
|
|
||||||
|
|
||||||
def display_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"
|
label = "Go to trezor.io/start"
|
||||||
image = None
|
|
||||||
else:
|
else:
|
||||||
label = storage.get_label() or "My Trezor"
|
label = storage.get_label() or "My Trezor"
|
||||||
image = storage.get_homescreen()
|
image = storage.get_homescreen()
|
||||||
@ -27,28 +29,28 @@ def display_homescreen():
|
|||||||
image = res.load("apps/homescreen/res/bg.toif")
|
image = res.load("apps/homescreen/res/bg.toif")
|
||||||
|
|
||||||
if storage.is_initialized() and storage.no_backup():
|
if storage.is_initialized() and storage.no_backup():
|
||||||
ui.display.bar(0, 0, ui.WIDTH, 30, ui.RED)
|
_err("SEEDLESS")
|
||||||
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)
|
|
||||||
elif storage.is_initialized() and storage.unfinished_backup():
|
elif storage.is_initialized() and storage.unfinished_backup():
|
||||||
ui.display.bar(0, 0, ui.WIDTH, 30, ui.RED)
|
_err("BACKUP FAILED!")
|
||||||
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)
|
|
||||||
elif storage.is_initialized() and storage.needs_backup():
|
elif storage.is_initialized() and storage.needs_backup():
|
||||||
ui.display.bar(0, 0, ui.WIDTH, 30, ui.YELLOW)
|
_warn("NEEDS BACKUP!")
|
||||||
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)
|
|
||||||
elif storage.is_initialized() and not config.has_pin():
|
elif storage.is_initialized() and not config.has_pin():
|
||||||
ui.display.bar(0, 0, ui.WIDTH, 30, ui.YELLOW)
|
_warn("PIN NOT SET!")
|
||||||
ui.display.text_center(
|
elif storage.is_slip39_in_progress():
|
||||||
ui.WIDTH // 2, 22, "PIN NOT SET!", ui.BOLD, ui.BLACK, ui.YELLOW
|
_warn("SHAMIR IN PROGRESS!")
|
||||||
)
|
|
||||||
ui.display.bar(0, 30, ui.WIDTH, ui.HEIGHT - 30, ui.BG)
|
|
||||||
else:
|
else:
|
||||||
ui.display.bar(0, 0, ui.WIDTH, ui.HEIGHT, ui.BG)
|
ui.display.bar(0, 0, ui.WIDTH, ui.HEIGHT, ui.BG)
|
||||||
ui.display.avatar(48, 48 - 10, image, ui.WHITE, ui.BLACK)
|
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)
|
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 trezor.messages.Success import Success
|
||||||
|
|
||||||
from apps.common import mnemonic, storage
|
from apps.common import mnemonic, storage
|
||||||
from apps.management.reset_device import (
|
from apps.management.common import layout
|
||||||
check_mnemonic,
|
from apps.management.reset_device import backup_slip39_wallet
|
||||||
show_backup_warning,
|
|
||||||
show_mnemonic,
|
|
||||||
show_wrong_entry,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def backup_device(ctx, msg):
|
async def backup_device(ctx, msg):
|
||||||
@ -16,20 +12,18 @@ async def backup_device(ctx, msg):
|
|||||||
if not storage.needs_backup():
|
if not storage.needs_backup():
|
||||||
raise wire.ProcessError("Seed already backed up")
|
raise wire.ProcessError("Seed already backed up")
|
||||||
|
|
||||||
words = mnemonic.restore()
|
|
||||||
|
|
||||||
# warn user about mnemonic safety
|
# warn user about mnemonic safety
|
||||||
await show_backup_warning(ctx)
|
await layout.bip39_show_backup_warning(ctx)
|
||||||
|
|
||||||
storage.set_unfinished_backup(True)
|
storage.set_unfinished_backup(True)
|
||||||
storage.set_backed_up()
|
storage.set_backed_up()
|
||||||
|
|
||||||
while True:
|
mnemonic_secret, mnemonic_type = mnemonic.get()
|
||||||
# show mnemonic and require confirmation of a random word
|
if mnemonic_type == mnemonic.TYPE_BIP39:
|
||||||
await show_mnemonic(ctx, words)
|
await layout.bip39_show_and_confirm_mnemonic(ctx, mnemonic_secret.decode())
|
||||||
if await check_mnemonic(ctx, words):
|
|
||||||
break
|
elif mnemonic_type == mnemonic.TYPE_SLIP39:
|
||||||
await show_wrong_entry(ctx)
|
await backup_slip39_wallet(ctx, mnemonic_secret)
|
||||||
|
|
||||||
storage.set_unfinished_backup(False)
|
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 import config, wire
|
||||||
from trezor.crypto import bip39
|
|
||||||
from trezor.messages.Success import Success
|
from trezor.messages.Success import Success
|
||||||
from trezor.pin import pin_to_int
|
from trezor.pin import pin_to_int
|
||||||
from trezor.ui.text import Text
|
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.confirm import require_confirm
|
||||||
|
from apps.common.mnemonic import bip39
|
||||||
|
|
||||||
|
|
||||||
async def load_device(ctx, msg):
|
async def load_device(ctx, msg):
|
||||||
|
# TODO implement SLIP-39
|
||||||
if storage.is_initialized():
|
if storage.is_initialized():
|
||||||
raise wire.UnexpectedMessage("Already 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!")
|
text.normal("Continue only if you", "know what you are doing!")
|
||||||
await require_confirm(ctx, text)
|
await require_confirm(ctx, text)
|
||||||
|
|
||||||
secret = mnemonic.process([msg.mnemonic], mnemonic.TYPE_BIP39)
|
secret = bip39.process_all([msg.mnemonic])
|
||||||
storage.store_mnemonic(
|
storage.store_mnemonic(
|
||||||
secret=secret,
|
secret=secret,
|
||||||
mnemonic_type=mnemonic.TYPE_BIP39,
|
mnemonic_type=bip39.get_type(),
|
||||||
needs_backup=True,
|
needs_backup=True,
|
||||||
no_backup=False,
|
no_backup=False,
|
||||||
)
|
)
|
||||||
|
@ -1,31 +1,28 @@
|
|||||||
from trezor import config, ui, wire
|
from trezor import config, ui, wire
|
||||||
from trezor.crypto import bip39
|
from trezor.messages import ButtonRequestType
|
||||||
from trezor.crypto.hashlib import sha256
|
|
||||||
from trezor.messages.ButtonRequest import ButtonRequest
|
from trezor.messages.ButtonRequest import ButtonRequest
|
||||||
from trezor.messages.ButtonRequestType import (
|
|
||||||
MnemonicInput,
|
|
||||||
MnemonicWordCount,
|
|
||||||
ProtectCall,
|
|
||||||
)
|
|
||||||
from trezor.messages.MessageType import ButtonAck
|
from trezor.messages.MessageType import ButtonAck
|
||||||
from trezor.messages.Success import Success
|
from trezor.messages.Success import Success
|
||||||
from trezor.pin import pin_to_int
|
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.text import Text
|
||||||
from trezor.ui.word_select import WordSelector
|
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 import mnemonic, storage
|
||||||
from apps.common.confirm import require_confirm
|
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
|
from apps.management.change_pin import request_pin_ack, request_pin_confirm
|
||||||
|
|
||||||
if __debug__:
|
if __debug__:
|
||||||
from apps.debug import input_signal
|
from apps.debug import confirm_signal, input_signal
|
||||||
|
|
||||||
|
|
||||||
async def recovery_device(ctx, msg):
|
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.
|
1. Ask for the number of words in recovered seed.
|
||||||
2. Let user type in the mnemonic words one by one.
|
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():
|
if not msg.dry_run and storage.is_initialized():
|
||||||
raise wire.UnexpectedMessage("Already initialized")
|
raise wire.UnexpectedMessage("Already initialized")
|
||||||
|
|
||||||
if not msg.dry_run:
|
if not storage.is_slip39_in_progress():
|
||||||
title = "Device recovery"
|
if not msg.dry_run:
|
||||||
text = Text(title, ui.ICON_RECOVERY)
|
title = "Device recovery"
|
||||||
text.normal("Do you really want to", "recover the device?", "")
|
text = Text(title, ui.ICON_RECOVERY)
|
||||||
else:
|
text.normal("Do you really want to", "recover the device?", "")
|
||||||
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())
|
|
||||||
else:
|
else:
|
||||||
curpin = ""
|
title = "Simulated recovery"
|
||||||
if not config.check_pin(pin_to_int(curpin)):
|
text = Text(title, ui.ICON_RECOVERY)
|
||||||
raise wire.PinInvalid("PIN invalid")
|
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
|
if msg.dry_run:
|
||||||
wordcount = await request_wordcount(ctx, title)
|
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
|
# ask for the number of words
|
||||||
words = await request_mnemonic(ctx, wordcount)
|
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
|
# check mnemonic validity
|
||||||
if msg.enforce_wordlist or msg.dry_run:
|
# it is checked automatically in SLIP-39
|
||||||
if not bip39.check(words):
|
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")
|
raise wire.ProcessError("Mnemonic is not valid")
|
||||||
|
|
||||||
# ask for pin repeatedly
|
# ask for pin repeatedly
|
||||||
@ -72,39 +88,24 @@ async def recovery_device(ctx, msg):
|
|||||||
else:
|
else:
|
||||||
newpin = ""
|
newpin = ""
|
||||||
|
|
||||||
secret = mnemonic.process([words], mnemonic.TYPE_BIP39)
|
|
||||||
|
|
||||||
# dry run
|
# dry run
|
||||||
if msg.dry_run:
|
if msg.dry_run:
|
||||||
digest_input = sha256(secret).digest()
|
mnemonic.dry_run(secret)
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
# save into storage
|
# save into storage
|
||||||
if newpin:
|
if msg.pin_protection:
|
||||||
config.change_pin(pin_to_int(""), pin_to_int(newpin))
|
config.change_pin(pin_to_int(""), pin_to_int(newpin))
|
||||||
storage.set_u2f_counter(msg.u2f_counter)
|
storage.set_u2f_counter(msg.u2f_counter)
|
||||||
storage.load_settings(label=msg.label, use_passphrase=msg.passphrase_protection)
|
storage.load_settings(label=msg.label, use_passphrase=msg.passphrase_protection)
|
||||||
storage.store_mnemonic(
|
mnemonic_module.store(secret=secret, needs_backup=False, no_backup=False)
|
||||||
secret=secret,
|
|
||||||
mnemonic_type=mnemonic.TYPE_BIP39,
|
display_homescreen()
|
||||||
needs_backup=False,
|
|
||||||
no_backup=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
return Success(message="Device recovered")
|
return Success(message="Device recovered")
|
||||||
|
|
||||||
|
|
||||||
async def request_wordcount(ctx, title: str) -> int:
|
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 = Text(title, ui.ICON_RECOVERY)
|
||||||
text.normal("Number of words?")
|
text.normal("Number of words?")
|
||||||
@ -118,12 +119,15 @@ async def request_wordcount(ctx, title: str) -> int:
|
|||||||
return count
|
return count
|
||||||
|
|
||||||
|
|
||||||
async def request_mnemonic(ctx, count: int) -> str:
|
async def request_mnemonic(ctx, count: int, slip39: bool) -> str:
|
||||||
await ctx.call(ButtonRequest(code=MnemonicInput), ButtonAck)
|
await ctx.call(ButtonRequest(code=ButtonRequestType.MnemonicInput), ButtonAck)
|
||||||
|
|
||||||
words = []
|
words = []
|
||||||
for i in range(count):
|
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__:
|
if __debug__:
|
||||||
word = await ctx.wait(keyboard, input_signal)
|
word = await ctx.wait(keyboard, input_signal)
|
||||||
else:
|
else:
|
||||||
@ -131,3 +135,33 @@ async def request_mnemonic(ctx, count: int) -> str:
|
|||||||
words.append(word)
|
words.append(word)
|
||||||
|
|
||||||
return " ".join(words)
|
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 trezor import config, wire
|
||||||
from ubinascii import hexlify
|
|
||||||
|
|
||||||
from trezor import config, ui, wire
|
|
||||||
from trezor.crypto import bip39, hashlib, random
|
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.EntropyRequest import EntropyRequest
|
||||||
from trezor.messages.Success import Success
|
from trezor.messages.Success import Success
|
||||||
from trezor.pin import pin_to_int
|
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 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.change_pin import request_pin_confirm
|
||||||
|
from apps.management.common import layout
|
||||||
|
|
||||||
if __debug__:
|
if __debug__:
|
||||||
from apps import debug
|
from apps import debug
|
||||||
@ -22,15 +15,10 @@ if __debug__:
|
|||||||
|
|
||||||
async def reset_device(ctx, msg):
|
async def reset_device(ctx, msg):
|
||||||
# validate parameters and device state
|
# validate parameters and device state
|
||||||
if msg.strength not in (128, 192, 256):
|
_validate_reset_device(msg)
|
||||||
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")
|
|
||||||
|
|
||||||
# make sure use knows he's setting up a new wallet
|
# make sure user knows he's setting up a new wallet
|
||||||
await show_reset_warning(ctx)
|
await layout.show_reset_device_warning(ctx, msg.slip39)
|
||||||
|
|
||||||
# request new PIN
|
# request new PIN
|
||||||
if msg.pin_protection:
|
if msg.pin_protection:
|
||||||
@ -39,165 +27,100 @@ async def reset_device(ctx, msg):
|
|||||||
newpin = ""
|
newpin = ""
|
||||||
|
|
||||||
# generate and display internal entropy
|
# generate and display internal entropy
|
||||||
internal_ent = random.bytes(32)
|
int_entropy = random.bytes(32)
|
||||||
if __debug__:
|
if __debug__:
|
||||||
debug.reset_internal_entropy = internal_ent
|
debug.reset_internal_entropy = int_entropy
|
||||||
if msg.display_random:
|
if msg.display_random:
|
||||||
await show_entropy(ctx, internal_ent)
|
await layout.show_internal_entropy(ctx, int_entropy)
|
||||||
|
|
||||||
# request external entropy and compute mnemonic
|
# request external entropy and compute the master secret
|
||||||
ent_ack = await ctx.call(EntropyRequest(), MessageType.EntropyAck)
|
entropy_ack = await ctx.call(EntropyRequest(), MessageType.EntropyAck)
|
||||||
words = generate_mnemonic(msg.strength, internal_ent, ent_ack.entropy)
|
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:
|
# should we back up the wallet now?
|
||||||
# require confirmation of the mnemonic safety
|
if not msg.no_backup and not msg.skip_backup:
|
||||||
await show_backup_warning(ctx)
|
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
|
# generate and display backup information for the master secret
|
||||||
while True:
|
if not msg.no_backup and not msg.skip_backup:
|
||||||
await show_mnemonic(ctx, words)
|
if msg.slip39:
|
||||||
if await check_mnemonic(ctx, words):
|
await backup_slip39_wallet(ctx, secret)
|
||||||
break
|
else:
|
||||||
await show_wrong_entry(ctx)
|
await backup_bip39_wallet(ctx, secret)
|
||||||
|
|
||||||
# write PIN into storage
|
# write PIN into storage
|
||||||
if newpin:
|
if not config.change_pin(pin_to_int(""), pin_to_int(newpin)):
|
||||||
if not config.change_pin(pin_to_int(""), pin_to_int(newpin)):
|
raise wire.ProcessError("Could not change PIN")
|
||||||
raise wire.ProcessError("Could not change PIN")
|
|
||||||
|
|
||||||
secret = mnemonic.process([words], mnemonic.TYPE_BIP39)
|
# write settings and master secret into storage
|
||||||
# write settings and mnemonic into storage
|
|
||||||
storage.load_settings(label=msg.label, use_passphrase=msg.passphrase_protection)
|
storage.load_settings(label=msg.label, use_passphrase=msg.passphrase_protection)
|
||||||
storage.store_mnemonic(
|
if msg.slip39:
|
||||||
secret=secret,
|
mnemonic.slip39.store(
|
||||||
mnemonic_type=mnemonic.TYPE_BIP39,
|
secret=secret, needs_backup=msg.skip_backup, no_backup=msg.no_backup
|
||||||
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 we backed up the wallet, show success message
|
||||||
if not msg.skip_backup and not msg.no_backup:
|
if not msg.no_backup and not msg.skip_backup:
|
||||||
await show_success(ctx)
|
await layout.show_backup_success(ctx)
|
||||||
|
|
||||||
return Success(message="Initialized")
|
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 = hashlib.sha256()
|
||||||
ehash.update(int_entropy)
|
ehash.update(int_entropy)
|
||||||
ehash.update(ext_entropy)
|
ehash.update(ext_entropy)
|
||||||
entropy = ehash.digest()
|
entropy = ehash.digest()
|
||||||
return bip39.from_data(entropy[: strength // 8])
|
# take a required number of bytes
|
||||||
|
strength = strength_in_bytes // 8
|
||||||
|
secret = entropy[:strength]
|
||||||
async def show_reset_warning(ctx):
|
return secret
|
||||||
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]
|
|
||||||
|
@ -21,8 +21,23 @@
|
|||||||
from micropython import const
|
from micropython import const
|
||||||
|
|
||||||
from trezor.crypto import hashlib, hmac, pbkdf2, random
|
from trezor.crypto import hashlib, hmac, pbkdf2, random
|
||||||
from trezor.crypto.slip39_wordlist import wordlist
|
from trezorcrypto import shamir, slip39
|
||||||
from trezorcrypto import shamir
|
|
||||||
|
_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)
|
_RADIX_BITS = const(10)
|
||||||
"""The length of the radix in bits."""
|
"""The length of the radix in bits."""
|
||||||
@ -36,6 +51,11 @@ def bits_to_words(n):
|
|||||||
return (n + _RADIX_BITS - 1) // _RADIX_BITS
|
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
|
_RADIX = 2 ** _RADIX_BITS
|
||||||
"""The number of words in the wordlist."""
|
"""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)
|
_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."""
|
"""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)
|
_CHECKSUM_LENGTH_WORDS = const(3)
|
||||||
"""The length of the RS1024 checksum in words."""
|
"""The length of the RS1024 checksum in words."""
|
||||||
|
|
||||||
@ -86,21 +103,6 @@ class MnemonicError(Exception):
|
|||||||
pass
|
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):
|
def _rs1024_polymod(values):
|
||||||
GEN = (
|
GEN = (
|
||||||
0xE0E040,
|
0xE0E040,
|
||||||
@ -181,11 +183,11 @@ def _int_to_indices(value, length, bits):
|
|||||||
|
|
||||||
|
|
||||||
def mnemonic_from_indices(indices):
|
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):
|
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):
|
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(
|
raise ValueError(
|
||||||
"The requested number of shares ({}) must not exceed {}.".format(
|
"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."""
|
"""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")
|
identifier = int.from_bytes(random.bytes(bits_to_bytes(_ID_LENGTH_BITS)), "big")
|
||||||
return identifier & ((1 << _ID_LENGTH_BITS) - 1)
|
return identifier & ((1 << _ID_LENGTH_BITS) - 1)
|
||||||
|
|
||||||
|
|
||||||
def generate_mnemonics(
|
def generate_single_group_mnemonics_from_data(
|
||||||
group_threshold, groups, master_secret, passphrase=b"", iteration_exponent=0
|
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.
|
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.
|
: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.
|
:param int iteration_exponent: The iteration exponent.
|
||||||
:return: List of mnemonics.
|
:return: List of mnemonics.
|
||||||
:rtype: List of byte arrays.
|
:rtype: List of byte arrays.
|
||||||
|
:return: Identifier.
|
||||||
|
:rtype: int.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
identifier = _generate_random_identifier()
|
identifier = _generate_random_identifier()
|
||||||
@ -528,68 +549,28 @@ def generate_mnemonics(
|
|||||||
|
|
||||||
group_shares = _split_secret(group_threshold, len(groups), encrypted_master_secret)
|
group_shares = _split_secret(group_threshold, len(groups), encrypted_master_secret)
|
||||||
|
|
||||||
return [
|
mnemonics = []
|
||||||
[
|
for (member_threshold, member_count), (group_index, group_secret) in zip(
|
||||||
encode_mnemonic(
|
groups, group_shares
|
||||||
identifier,
|
):
|
||||||
iteration_exponent,
|
group_mnemonics = []
|
||||||
group_index,
|
for member_index, value in _split_secret(
|
||||||
group_threshold,
|
member_threshold, member_count, group_secret
|
||||||
len(groups),
|
):
|
||||||
member_index,
|
group_mnemonics.append(
|
||||||
member_threshold,
|
encode_mnemonic(
|
||||||
value,
|
identifier,
|
||||||
|
iteration_exponent,
|
||||||
|
group_index,
|
||||||
|
group_threshold,
|
||||||
|
len(groups),
|
||||||
|
member_index,
|
||||||
|
member_threshold,
|
||||||
|
value,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
for member_index, value in _split_secret(
|
mnemonics.append(group_mnemonics)
|
||||||
member_threshold, member_count, group_secret
|
return identifier, mnemonics
|
||||||
)
|
|
||||||
]
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def combine_mnemonics(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
|
Combines mnemonic shares to obtain the master secret which was previously split using
|
||||||
Shamir's secret sharing scheme.
|
Shamir's secret sharing scheme.
|
||||||
:param mnemonics: List of mnemonics.
|
:param mnemonics: List of mnemonics.
|
||||||
:type mnemonics: List of byte arrays.
|
:type mnemonics: List of strings.
|
||||||
:return: Identifier, iteration exponent, the encrypted master secret.
|
:return: Identifier, iteration exponent, the encrypted master secret.
|
||||||
:rtype: Integer, integer, array of bytes.
|
: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,
|
u2f_counter: int = None,
|
||||||
skip_backup: bool = None,
|
skip_backup: bool = None,
|
||||||
no_backup: bool = None,
|
no_backup: bool = None,
|
||||||
|
slip39: bool = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.display_random = display_random
|
self.display_random = display_random
|
||||||
self.strength = strength
|
self.strength = strength
|
||||||
@ -27,6 +28,7 @@ class ResetDevice(p.MessageType):
|
|||||||
self.u2f_counter = u2f_counter
|
self.u2f_counter = u2f_counter
|
||||||
self.skip_backup = skip_backup
|
self.skip_backup = skip_backup
|
||||||
self.no_backup = no_backup
|
self.no_backup = no_backup
|
||||||
|
self.slip39 = slip39
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_fields(cls):
|
def get_fields(cls):
|
||||||
@ -40,4 +42,5 @@ class ResetDevice(p.MessageType):
|
|||||||
7: ('u2f_counter', p.UVarintType, 0),
|
7: ('u2f_counter', p.UVarintType, 0),
|
||||||
8: ('skip_backup', p.BoolType, 0),
|
8: ('skip_backup', p.BoolType, 0),
|
||||||
9: ('no_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,
|
confirm_style=DEFAULT_CONFIRM_STYLE,
|
||||||
cancel=DEFAULT_CANCEL,
|
cancel=DEFAULT_CANCEL,
|
||||||
cancel_style=DEFAULT_CANCEL_STYLE,
|
cancel_style=DEFAULT_CANCEL_STYLE,
|
||||||
|
major_confirm=False,
|
||||||
):
|
):
|
||||||
self.content = content
|
self.content = content
|
||||||
|
|
||||||
if confirm is not None:
|
if confirm is not None:
|
||||||
if cancel is None:
|
if cancel is None:
|
||||||
area = ui.grid(4, n_x=1)
|
area = ui.grid(4, n_x=1)
|
||||||
|
elif major_confirm:
|
||||||
|
area = ui.grid(13, cells_x=2)
|
||||||
else:
|
else:
|
||||||
area = ui.grid(9, n_x=2)
|
area = ui.grid(9, n_x=2)
|
||||||
self.confirm = Button(area, confirm, confirm_style)
|
self.confirm = Button(area, confirm, confirm_style)
|
||||||
@ -35,6 +38,8 @@ class Confirm(ui.Layout):
|
|||||||
if cancel is not None:
|
if cancel is not None:
|
||||||
if confirm is None:
|
if confirm is None:
|
||||||
area = ui.grid(4, n_x=1)
|
area = ui.grid(4, n_x=1)
|
||||||
|
elif major_confirm:
|
||||||
|
area = ui.grid(12, cells_x=1)
|
||||||
else:
|
else:
|
||||||
area = ui.grid(8, n_x=2)
|
area = ui.grid(8, n_x=2)
|
||||||
self.cancel = Button(area, cancel, cancel_style)
|
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.start_ms = None
|
||||||
self.stop_ms = None
|
self.stop_ms = None
|
||||||
self.on_start()
|
self.on_start()
|
||||||
|
if r == target:
|
||||||
|
self.on_finish()
|
||||||
|
|
||||||
def on_start(self):
|
def on_start(self):
|
||||||
pass
|
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)
|
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
|
# initial rendering state
|
||||||
font = ui.NORMAL
|
INITIAL_OFFSET_X = offset_x
|
||||||
fg = ui.FG
|
offset_y_max = offset_y * max_lines
|
||||||
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
|
|
||||||
FONTS = (ui.NORMAL, ui.BOLD, ui.MONO, ui.MONO_BOLD)
|
FONTS = (ui.NORMAL, ui.BOLD, ui.MONO, ui.MONO_BOLD)
|
||||||
|
|
||||||
# sizes of common glyphs
|
# 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 isinstance(word, int):
|
||||||
if word is BR or word is BR_HALF:
|
if word is BR or word is BR_HALF:
|
||||||
# line break or half-line break
|
# 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)
|
ui.display.text(offset_x, offset_y, "...", ui.BOLD, ui.GREY, bg)
|
||||||
return
|
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
|
offset_y += TEXT_LINE_HEIGHT if word is BR else TEXT_LINE_HEIGHT_HALF
|
||||||
elif word in FONTS:
|
elif word in FONTS:
|
||||||
# change of font style
|
# 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)
|
width = ui.display.text_width(word, font)
|
||||||
|
|
||||||
while offset_x + width > OFFSET_X_MAX or (
|
while offset_x + width > offset_x_max or (
|
||||||
has_next_word and offset_y >= OFFSET_Y_MAX
|
has_next_word and offset_y >= offset_y_max
|
||||||
):
|
):
|
||||||
beginning_of_line = offset_x == TEXT_MARGIN_LEFT
|
beginning_of_line = offset_x == INITIAL_OFFSET_X
|
||||||
word_fits_in_one_line = width < (OFFSET_X_MAX - TEXT_MARGIN_LEFT)
|
word_fits_in_one_line = width < (offset_x_max - INITIAL_OFFSET_X)
|
||||||
if (
|
if (
|
||||||
offset_y < OFFSET_Y_MAX
|
offset_y < offset_y_max
|
||||||
and word_fits_in_one_line
|
and word_fits_in_one_line
|
||||||
and not beginning_of_line
|
and not beginning_of_line
|
||||||
):
|
):
|
||||||
# line break
|
# line break
|
||||||
offset_x = TEXT_MARGIN_LEFT
|
offset_x = INITIAL_OFFSET_X
|
||||||
offset_y += TEXT_LINE_HEIGHT
|
offset_y += TEXT_LINE_HEIGHT
|
||||||
break
|
break
|
||||||
# word split
|
# word split
|
||||||
if offset_y < OFFSET_Y_MAX:
|
if offset_y < offset_y_max:
|
||||||
split = "-"
|
split = "-"
|
||||||
splitw = DASH
|
splitw = DASH
|
||||||
else:
|
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):
|
for index in range(len(word) - 1, 0, -1):
|
||||||
letter = word[index]
|
letter = word[index]
|
||||||
width -= ui.display.text_width(letter, font)
|
width -= ui.display.text_width(letter, font)
|
||||||
if offset_x + width + splitw < OFFSET_X_MAX:
|
if offset_x + width + splitw < offset_x_max:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
index = 0
|
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, offset_y, span, font, fg, bg)
|
||||||
ui.display.text(offset_x + width, offset_y, split, ui.BOLD, ui.GREY, bg)
|
ui.display.text(offset_x + width, offset_y, split, ui.BOLD, ui.GREY, bg)
|
||||||
# line break
|
# line break
|
||||||
if offset_y >= OFFSET_Y_MAX:
|
if offset_y >= offset_y_max:
|
||||||
return
|
return
|
||||||
offset_x = TEXT_MARGIN_LEFT
|
offset_x = INITIAL_OFFSET_X
|
||||||
offset_y += TEXT_LINE_HEIGHT
|
offset_y += TEXT_LINE_HEIGHT
|
||||||
# continue with the rest
|
# continue with the rest
|
||||||
word = word[index:]
|
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:
|
if new_lines and has_next_word:
|
||||||
# line break
|
# 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)
|
ui.display.text(offset_x, offset_y, "...", ui.BOLD, ui.GREY, bg)
|
||||||
return
|
return
|
||||||
offset_x = TEXT_MARGIN_LEFT
|
offset_x = INITIAL_OFFSET_X
|
||||||
offset_y += TEXT_LINE_HEIGHT
|
offset_y += TEXT_LINE_HEIGHT
|
||||||
else:
|
else:
|
||||||
# shift cursor
|
# shift cursor
|
||||||
@ -158,3 +164,31 @@ class Text(ui.Control):
|
|||||||
)
|
)
|
||||||
render_text(self.content, self.new_lines, self.max_lines)
|
render_text(self.content, self.new_lines, self.max_lines)
|
||||||
self.repaint = False
|
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 import ui
|
||||||
from trezor.ui.button import Button
|
from trezor.ui.button import Button
|
||||||
|
|
||||||
|
# todo improve?
|
||||||
|
|
||||||
|
|
||||||
class WordSelector(ui.Layout):
|
class WordSelector(ui.Layout):
|
||||||
def __init__(self, content):
|
def __init__(self, content):
|
||||||
self.content = 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.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.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.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):
|
def dispatch(self, event, x, y):
|
||||||
self.content.dispatch(event, x, y)
|
self.content.dispatch(event, x, y)
|
||||||
self.w12.dispatch(event, x, y)
|
self.w12.dispatch(event, x, y)
|
||||||
self.w18.dispatch(event, x, y)
|
self.w18.dispatch(event, x, y)
|
||||||
|
self.w20.dispatch(event, x, y)
|
||||||
self.w24.dispatch(event, x, y)
|
self.w24.dispatch(event, x, y)
|
||||||
|
self.w33.dispatch(event, x, y)
|
||||||
|
|
||||||
def on_w12(self):
|
def on_w12(self):
|
||||||
raise ui.Result(12)
|
raise ui.Result(12)
|
||||||
@ -24,5 +32,11 @@ class WordSelector(ui.Layout):
|
|||||||
def on_w18(self):
|
def on_w18(self):
|
||||||
raise ui.Result(18)
|
raise ui.Result(18)
|
||||||
|
|
||||||
|
def on_w20(self):
|
||||||
|
raise ui.Result(20)
|
||||||
|
|
||||||
def on_w24(self):
|
def on_w24(self):
|
||||||
raise ui.Result(24)
|
raise ui.Result(24)
|
||||||
|
|
||||||
|
def on_w33(self):
|
||||||
|
raise ui.Result(33)
|
||||||
|
@ -25,12 +25,15 @@ class TestCryptoSlip39(unittest.TestCase):
|
|||||||
MS = b"ABCDEFGHIJKLMNOP"
|
MS = b"ABCDEFGHIJKLMNOP"
|
||||||
|
|
||||||
def test_basic_sharing_random(self):
|
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:]))
|
self.assertEqual(slip39.combine_mnemonics(mnemonics[:3]), slip39.combine_mnemonics(mnemonics[2:]))
|
||||||
|
|
||||||
|
|
||||||
def test_basic_sharing_fixed(self):
|
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])
|
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics[:3])
|
||||||
self.assertEqual(slip39.decrypt(identifier, exponent, ems, b""), self.MS)
|
self.assertEqual(slip39.decrypt(identifier, exponent, ems, b""), self.MS)
|
||||||
self.assertEqual(slip39.combine_mnemonics(mnemonics[1:4])[2], ems)
|
self.assertEqual(slip39.combine_mnemonics(mnemonics[1:4])[2], ems)
|
||||||
@ -39,19 +42,22 @@ class TestCryptoSlip39(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
def test_passphrase(self):
|
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])
|
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics[1:4])
|
||||||
self.assertEqual(slip39.decrypt(identifier, exponent, ems, b"TREZOR"), self.MS)
|
self.assertEqual(slip39.decrypt(identifier, exponent, ems, b"TREZOR"), self.MS)
|
||||||
self.assertNotEqual(slip39.decrypt(identifier, exponent, ems, b""), self.MS)
|
self.assertNotEqual(slip39.decrypt(identifier, exponent, ems, b""), self.MS)
|
||||||
|
|
||||||
|
|
||||||
def test_iteration_exponent(self):
|
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])
|
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics[1:4])
|
||||||
self.assertEqual(slip39.decrypt(identifier, exponent, ems, b"TREZOR"), self.MS)
|
self.assertEqual(slip39.decrypt(identifier, exponent, ems, b"TREZOR"), self.MS)
|
||||||
self.assertNotEqual(slip39.decrypt(identifier, exponent, ems, b""), 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])
|
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics[1:4])
|
||||||
self.assertEqual(slip39.decrypt(identifier, exponent, ems, b"TREZOR"), self.MS)
|
self.assertEqual(slip39.decrypt(identifier, exponent, ems, b"TREZOR"), self.MS)
|
||||||
self.assertNotEqual(slip39.decrypt(identifier, exponent, ems, b""), self.MS)
|
self.assertNotEqual(slip39.decrypt(identifier, exponent, ems, b""), self.MS)
|
||||||
@ -61,8 +67,8 @@ class TestCryptoSlip39(unittest.TestCase):
|
|||||||
group_threshold = 2
|
group_threshold = 2
|
||||||
group_sizes = (5, 3, 5, 1)
|
group_sizes = (5, 3, 5, 1)
|
||||||
member_thresholds = (3, 2, 2, 1)
|
member_thresholds = (3, 2, 2, 1)
|
||||||
mnemonics = slip39.generate_mnemonics(
|
_, mnemonics = slip39.generate_mnemonics_from_data(
|
||||||
group_threshold, list(zip(member_thresholds, group_sizes)), self.MS
|
self.MS, group_threshold, list(zip(member_thresholds, group_sizes))
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test all valid combinations of mnemonics.
|
# Test all valid combinations of mnemonics.
|
||||||
@ -93,8 +99,8 @@ class TestCryptoSlip39(unittest.TestCase):
|
|||||||
group_threshold = 1
|
group_threshold = 1
|
||||||
group_sizes = (5, 3, 5, 1)
|
group_sizes = (5, 3, 5, 1)
|
||||||
member_thresholds = (3, 2, 2, 1)
|
member_thresholds = (3, 2, 2, 1)
|
||||||
mnemonics = slip39.generate_mnemonics(
|
_, mnemonics = slip39.generate_mnemonics_from_data(
|
||||||
group_threshold, list(zip(member_thresholds, group_sizes)), self.MS
|
self.MS, group_threshold, list(zip(member_thresholds, group_sizes))
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test all valid combinations of mnemonics.
|
# Test all valid combinations of mnemonics.
|
||||||
@ -108,8 +114,8 @@ class TestCryptoSlip39(unittest.TestCase):
|
|||||||
|
|
||||||
def test_all_groups_exist(self):
|
def test_all_groups_exist(self):
|
||||||
for group_threshold in (1, 2, 5):
|
for group_threshold in (1, 2, 5):
|
||||||
mnemonics = slip39.generate_mnemonics(
|
_, mnemonics = slip39.generate_mnemonics_from_data(
|
||||||
group_threshold, [(3, 5), (1, 1), (2, 3), (2, 5), (3, 5)], self.MS
|
self.MS, group_threshold, [(3, 5), (1, 1), (2, 3), (2, 5), (3, 5)]
|
||||||
)
|
)
|
||||||
self.assertEqual(len(mnemonics), 5)
|
self.assertEqual(len(mnemonics), 5)
|
||||||
self.assertEqual(len(sum(mnemonics, [])), 19)
|
self.assertEqual(len(sum(mnemonics, [])), 19)
|
||||||
@ -118,31 +124,31 @@ class TestCryptoSlip39(unittest.TestCase):
|
|||||||
def test_invalid_sharing(self):
|
def test_invalid_sharing(self):
|
||||||
# Short master secret.
|
# Short master secret.
|
||||||
with self.assertRaises(ValueError):
|
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.
|
# Odd length master secret.
|
||||||
with self.assertRaises(ValueError):
|
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.
|
# Group threshold exceeds number of groups.
|
||||||
with self.assertRaises(ValueError):
|
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.
|
# Invalid group threshold.
|
||||||
with self.assertRaises(ValueError):
|
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.
|
# Member threshold exceeds number of members.
|
||||||
with self.assertRaises(ValueError):
|
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.
|
# Invalid member threshold.
|
||||||
with self.assertRaises(ValueError):
|
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.
|
# Group with multiple members and threshold 1.
|
||||||
with self.assertRaises(ValueError):
|
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):
|
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,
|
u2f_counter: int = None,
|
||||||
skip_backup: bool = None,
|
skip_backup: bool = None,
|
||||||
no_backup: bool = None,
|
no_backup: bool = None,
|
||||||
|
slip39: bool = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.display_random = display_random
|
self.display_random = display_random
|
||||||
self.strength = strength
|
self.strength = strength
|
||||||
@ -27,6 +28,7 @@ class ResetDevice(p.MessageType):
|
|||||||
self.u2f_counter = u2f_counter
|
self.u2f_counter = u2f_counter
|
||||||
self.skip_backup = skip_backup
|
self.skip_backup = skip_backup
|
||||||
self.no_backup = no_backup
|
self.no_backup = no_backup
|
||||||
|
self.slip39 = slip39
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_fields(cls):
|
def get_fields(cls):
|
||||||
@ -40,4 +42,5 @@ class ResetDevice(p.MessageType):
|
|||||||
7: ('u2f_counter', p.UVarintType, 0),
|
7: ('u2f_counter', p.UVarintType, 0),
|
||||||
8: ('skip_backup', p.BoolType, 0),
|
8: ('skip_backup', p.BoolType, 0),
|
||||||
9: ('no_backup', p.BoolType, 0),
|
9: ('no_backup', p.BoolType, 0),
|
||||||
|
10: ('slip39', p.BoolType, 0),
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user