SLIP-39 MVP (#195)

SLIP-39 MVP
pull/258/head
Tomas Susanka 5 years ago committed by GitHub
commit 435d00114c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -191,6 +191,7 @@ message ResetDevice {
optional uint32 u2f_counter = 7; // U2F counter
optional bool skip_backup = 8; // postpone seed backup to BackupDevice workflow
optional bool no_backup = 9; // indicate that no backup is going to be made
optional bool slip39 = 10; // indicate that we want SLIP-39 backup not BIP-39
}
/**

@ -88,6 +88,7 @@ SOURCE_MOD += [
'vendor/trezor-crypto/sha2.c',
'vendor/trezor-crypto/sha3.c',
'vendor/trezor-crypto/shamir.c',
'vendor/trezor-crypto/slip39.c',
]
# libsecp256k1-zkp

@ -86,6 +86,7 @@ SOURCE_MOD += [
'vendor/trezor-crypto/sha2.c',
'vendor/trezor-crypto/sha3.c',
'vendor/trezor-crypto/shamir.c',
'vendor/trezor-crypto/slip39.c',
]
# libsecp256k1-zkp

@ -0,0 +1,122 @@
/*
* This file is part of the TREZOR project, https://trezor.io/
*
* Copyright (c) SatoshiLabs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "py/obj.h"
#include "py/runtime.h"
#include "slip39.h"
/// package: trezorcrypto.slip39
/// 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.
/// """
STATIC mp_obj_t mod_trezorcrypto_slip39_compute_mask(mp_obj_t _prefix) {
uint16_t prefix = mp_obj_get_int(_prefix);
if (prefix < 1 || prefix > 9999) {
mp_raise_ValueError(
"Invalid button prefix (range between 1 and 9999 is allowed)");
}
return mp_obj_new_int_from_uint(compute_mask(prefix));
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_slip39_compute_mask_obj,
mod_trezorcrypto_slip39_compute_mask);
/// def button_sequence_to_word(prefix: int) -> str:
/// """
/// Finds the first word that fits the given button prefix.
/// """
STATIC mp_obj_t
mod_trezorcrypto_slip39_button_sequence_to_word(mp_obj_t _prefix) {
uint16_t prefix = mp_obj_get_int(_prefix);
if (prefix < 1 || prefix > 9999) {
mp_raise_ValueError(
"Invalid button prefix (range between 1 and 9999 is allowed)");
}
const char *word = button_sequence_to_word(prefix);
return mp_obj_new_str_copy(&mp_type_str, (const uint8_t *)word, strlen(word));
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(
mod_trezorcrypto_slip39_button_sequence_to_word_obj,
mod_trezorcrypto_slip39_button_sequence_to_word);
/// def word_index(word: str) -> int:
/// """
/// Finds index of given word.
/// Raises ValueError if not found.
/// """
STATIC mp_obj_t mod_trezorcrypto_slip39_word_index(mp_obj_t _word) {
mp_buffer_info_t word;
mp_get_buffer_raise(_word, &word, MP_BUFFER_READ);
uint16_t result = 0;
if (word_index(&result, word.buf, word.len) == false) {
mp_raise_ValueError("Invalid mnemonic word");
}
return mp_obj_new_int_from_uint(result);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_slip39_word_index_obj,
mod_trezorcrypto_slip39_word_index);
/// def get_word(index: int) -> str:
/// """
/// Returns word on position 'index'.
/// """
STATIC mp_obj_t mod_trezorcrypto_slip39_get_word(mp_obj_t _index) {
uint16_t index = mp_obj_get_int(_index);
if (index > 1023) {
mp_raise_ValueError(
"Invalid wordlist index (range between 0 and 1024 is allowed)");
}
const char *word = get_word(index);
return mp_obj_new_str_copy(&mp_type_str, (const uint8_t *)word, strlen(word));
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_slip39_get_word_obj,
mod_trezorcrypto_slip39_get_word);
STATIC const mp_rom_map_elem_t mod_trezorcrypto_slip39_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_slip39)},
{MP_ROM_QSTR(MP_QSTR_compute_mask),
MP_ROM_PTR(&mod_trezorcrypto_slip39_compute_mask_obj)},
{MP_ROM_QSTR(MP_QSTR_button_sequence_to_word),
MP_ROM_PTR(&mod_trezorcrypto_slip39_button_sequence_to_word_obj)},
{MP_ROM_QSTR(MP_QSTR_word_index),
MP_ROM_PTR(&mod_trezorcrypto_slip39_word_index_obj)},
{MP_ROM_QSTR(MP_QSTR_get_word),
MP_ROM_PTR(&mod_trezorcrypto_slip39_get_word_obj)},
};
STATIC MP_DEFINE_CONST_DICT(mod_trezorcrypto_slip39_globals,
mod_trezorcrypto_slip39_globals_table);
STATIC const mp_obj_module_t mod_trezorcrypto_slip39_module = {
.base = {&mp_type_module},
.globals = (mp_obj_dict_t *)&mod_trezorcrypto_slip39_globals,
};

@ -51,6 +51,7 @@
#include "modtrezorcrypto-sha3-512.h"
#include "modtrezorcrypto-sha512.h"
#include "modtrezorcrypto-shamir.h"
#include "modtrezorcrypto-slip39.h"
STATIC const mp_rom_map_elem_t mp_module_trezorcrypto_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_trezorcrypto)},
@ -91,6 +92,7 @@ STATIC const mp_rom_map_elem_t mp_module_trezorcrypto_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR_sha3_512),
MP_ROM_PTR(&mod_trezorcrypto_Sha3_512_type)},
{MP_ROM_QSTR(MP_QSTR_shamir), MP_ROM_PTR(&mod_trezorcrypto_shamir_module)},
{MP_ROM_QSTR(MP_QSTR_slip39), MP_ROM_PTR(&mod_trezorcrypto_slip39_module)},
};
STATIC MP_DEFINE_CONST_DICT(mp_module_trezorcrypto_globals,
mp_module_trezorcrypto_globals_table);

@ -0,0 +1,35 @@
from typing import *
# extmod/modtrezorcrypto/modtrezorcrypto-slip39.h
def compute_mask(prefix: int) -> int:
"""
Calculates which buttons still can be pressed after some already were.
Returns a 9-bit bitmask, where each bit specifies which buttons
can be further pressed (there are still words in this combination).
LSB denotes first button.
Example: 110000110 - second, third, eighth and ninth button still can be
pressed.
"""
# extmod/modtrezorcrypto/modtrezorcrypto-slip39.h
def button_sequence_to_word(prefix: int) -> str:
"""
Finds the first word that fits the given button prefix.
"""
# extmod/modtrezorcrypto/modtrezorcrypto-slip39.h
def word_index(word: str) -> int:
"""
Finds index of given word.
Raises ValueError if not found.
"""
# extmod/modtrezorcrypto/modtrezorcrypto-slip39.h
def get_word(index: int) -> str:
"""
Returns word on position 'index'.
"""

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

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

@ -1,56 +0,0 @@
from trezor import ui, workflow
from trezor.crypto import bip39
from apps.common import storage
TYPE_BIP39 = 0
def get() -> (bytes, int):
mnemonic_secret = storage.get_mnemonic_secret()
mnemonic_type = storage.get_mnemonic_type() or TYPE_BIP39
return mnemonic_secret, mnemonic_type
def get_seed(passphrase: str = "", progress_bar=True):
secret, mnemonic_type = get()
if mnemonic_type == TYPE_BIP39:
module = bip39
if progress_bar:
_start_progress()
result = module.seed(secret.decode(), passphrase, _render_progress)
_stop_progress()
else:
result = module.seed(secret.decode(), passphrase)
return result
def process(mnemonics: list, mnemonic_type: int):
if mnemonic_type == TYPE_BIP39:
return mnemonics[0].encode()
else:
raise RuntimeError("Unknown mnemonic type")
def restore() -> str:
secret, mnemonic_type = get()
if mnemonic_type == TYPE_BIP39:
return secret.decode()
def _start_progress():
ui.backlight_fade(ui.BACKLIGHT_DIM)
ui.display.clear()
ui.header("Please wait")
ui.display.refresh()
ui.backlight_fade(ui.BACKLIGHT_NORMAL)
def _render_progress(progress: int, total: int):
p = 1000 * progress // total
ui.display.loader(p, False, 18, ui.WHITE, ui.BG)
ui.display.refresh()
def _stop_progress():
workflow.restartdefault()

@ -0,0 +1,59 @@
from micropython import const
from trezor import ui, wire
from trezor.crypto.hashlib import sha256
from trezor.messages.Success import Success
from trezor.utils import consteq
from . import bip39, slip39
from apps.common import storage
TYPE_BIP39 = const(0)
TYPE_SLIP39 = const(1)
TYPES_WORD_COUNT = {12: bip39, 18: bip39, 24: bip39, 20: slip39, 33: slip39}
def get() -> (bytes, int):
mnemonic_secret = storage.get_mnemonic_secret()
mnemonic_type = storage.get_mnemonic_type() or TYPE_BIP39
return mnemonic_secret, mnemonic_type
def get_seed(passphrase: str = ""):
mnemonic_secret, mnemonic_type = get()
if mnemonic_type == TYPE_BIP39:
return bip39.get_seed(mnemonic_secret, passphrase)
elif mnemonic_type == TYPE_SLIP39:
return slip39.get_seed(mnemonic_secret, passphrase)
def dry_run(secret: bytes):
digest_input = sha256(secret).digest()
stored, _ = get()
digest_stored = sha256(stored).digest()
if consteq(digest_stored, digest_input):
return Success(message="The seed is valid and matches the one in the device")
else:
raise wire.ProcessError(
"The seed is valid but does not match the one in the device"
)
def module_from_words_count(count: int):
return TYPES_WORD_COUNT[count]
def _start_progress():
ui.backlight_fade(ui.BACKLIGHT_DIM)
ui.display.clear()
ui.header("Please wait")
ui.display.refresh()
ui.backlight_fade(ui.BACKLIGHT_NORMAL)
def _render_progress(progress: int, total: int):
p = 1000 * progress // total
ui.display.loader(p, False, 18, ui.WHITE, ui.BG)
ui.display.refresh()

@ -0,0 +1,36 @@
from trezor.crypto import bip39
from apps.common import mnemonic, storage
def get_type():
return mnemonic.TYPE_BIP39
def process_single(mnemonic: str) -> bytes:
"""
Receives single mnemonic and processes it. Returns what is then stored in storage or
None if more shares are needed.
"""
return mnemonic.encode()
def process_all(mnemonics: list) -> bytes:
"""
Receives all mnemonics (just one in case of BIP-39) and processes it into a master
secret which is usually then stored in storage.
"""
return mnemonics[0].encode()
def store(secret: bytes, needs_backup: bool, no_backup: bool):
storage.store_mnemonic(secret, mnemonic.TYPE_BIP39, needs_backup, no_backup)
def get_seed(secret: bytes, passphrase: str):
mnemonic._start_progress()
return bip39.seed(secret.decode(), passphrase, mnemonic._render_progress)
def check(secret: bytes):
return bip39.check(secret)

@ -0,0 +1,87 @@
from trezor.crypto import slip39
from apps.common import mnemonic, storage
def generate_from_secret(master_secret: bytes, count: int, threshold: int) -> str:
"""
Generates new Shamir backup for 'master_secret'. Multiple groups are not yet supported.
"""
identifier, group_mnemonics = slip39.generate_single_group_mnemonics_from_data(
master_secret, threshold, count
)
storage.set_slip39_iteration_exponent(slip39.DEFAULT_ITERATION_EXPONENT)
storage.set_slip39_identifier(identifier)
return group_mnemonics
def get_type():
return mnemonic.TYPE_SLIP39
def process_single(mnemonic: str) -> bytes:
"""
Receives single mnemonic and processes it. Returns what is then stored in storage or
None if more shares are needed.
"""
identifier, iteration_exponent, _, _, _, index, threshold, value = slip39.decode_mnemonic(
mnemonic
) # TODO: use better data structure for this
if threshold == 1:
raise ValueError("Threshold equal to 1 is not allowed.")
# if recovery is not in progress already, start it and wait for more mnemonics
if not storage.is_slip39_in_progress():
storage.set_slip39_in_progress(True)
storage.set_slip39_iteration_exponent(iteration_exponent)
storage.set_slip39_identifier(identifier)
storage.set_slip39_threshold(threshold)
storage.set_slip39_remaining(threshold - 1)
storage.set_slip39_words_count(len(mnemonic.split()))
storage.set_slip39_mnemonic(index, mnemonic)
return None # we need more shares
# check identifier and member index of this share against stored values
if identifier != storage.get_slip39_identifier():
# TODO: improve UX (tell user)
raise ValueError("Share identifiers do not match")
if storage.get_slip39_mnemonic(index):
# TODO: improve UX (tell user)
raise ValueError("This mnemonic was already entered")
# append to storage
remaining = storage.get_slip39_remaining() - 1
storage.set_slip39_remaining(remaining)
storage.set_slip39_mnemonic(index, mnemonic)
if remaining != 0:
return None # we need more shares
# combine shares and return the master secret
mnemonics = storage.get_slip39_mnemonics()
if len(mnemonics) != threshold:
raise ValueError("Some mnemonics are still missing.")
_, _, secret = slip39.combine_mnemonics(mnemonics)
return secret
def process_all(mnemonics: list) -> bytes:
"""
Receives all mnemonics and processes it into pre-master secret which is usually then
stored in the storage.
"""
_, _, secret = slip39.combine_mnemonics(mnemonics)
return secret
def store(secret: bytes, needs_backup: bool, no_backup: bool):
storage.store_mnemonic(secret, mnemonic.TYPE_SLIP39, needs_backup, no_backup)
storage.clear_slip39_data()
def get_seed(encrypted_master_secret: bytes, passphrase: str):
mnemonic._start_progress()
identifier = storage.get_slip39_identifier()
iteration_exponent = storage.get_slip39_iteration_exponent()
return slip39.decrypt(
identifier, iteration_exponent, encrypted_master_secret, passphrase
)

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

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

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

@ -0,0 +1,519 @@
import ubinascii
from micropython import const
from trezor import ui, utils
from trezor.crypto import random
from trezor.messages import ButtonRequestType
from trezor.ui.button import Button, ButtonDefault
from trezor.ui.checklist import Checklist
from trezor.ui.info import InfoConfirm
from trezor.ui.loader import LoadingAnimation
from trezor.ui.scroll import Paginated
from trezor.ui.shamir import NumInput
from trezor.ui.text import Text
from apps.common.confirm import confirm, hold_to_confirm, require_confirm
if __debug__:
from apps import debug
async def show_reset_device_warning(ctx, use_slip39: bool):
text = Text("Create new wallet", ui.ICON_RESET, new_lines=False)
text.bold("Do you want to create")
text.br()
if use_slip39:
text.bold("a new Shamir wallet?")
else:
text.bold("a new wallet?")
text.br()
text.br_half()
text.normal("By continuing you agree")
text.br()
text.normal("to")
text.bold("https://trezor.io/tos")
await require_confirm(ctx, text, ButtonRequestType.ResetDevice, major_confirm=True)
await LoadingAnimation()
async def show_internal_entropy(ctx, entropy: bytes):
entropy_str = ubinascii.hexlify(entropy).decode()
lines = utils.chunks(entropy_str, 16)
text = Text("Internal entropy", ui.ICON_RESET)
text.mono(*lines)
await require_confirm(ctx, text, ButtonRequestType.ResetDevice)
async def show_backup_success(ctx):
text = Text("Backup is done!", ui.ICON_CONFIRM, ui.GREEN)
text.normal(
"Never make a digital",
"copy of your recovery",
"seed and never upload",
"it online!",
)
await require_confirm(
ctx, text, ButtonRequestType.ResetDevice, confirm="Finish setup", cancel=None
)
async def confirm_backup(ctx):
text = Text("Backup wallet", ui.ICON_RESET, new_lines=False)
text.bold("New wallet created")
text.br()
text.bold("successfully!")
text.br()
text.br_half()
text.normal("You should back your")
text.br()
text.normal("new wallet right now.")
return await confirm(
ctx,
text,
ButtonRequestType.ResetDevice,
cancel="Skip",
confirm="Backup",
major_confirm=True,
)
async def confirm_backup_again(ctx):
text = Text("Backup wallet", ui.ICON_RESET, new_lines=False)
text.bold("Are you sure you want")
text.br()
text.bold("to skip the backup?")
text.br()
text.br_half()
text.normal("You can backup Trezor")
text.br()
text.normal("anytime later.")
return await confirm(
ctx,
text,
ButtonRequestType.ResetDevice,
cancel="Skip",
confirm="Backup",
major_confirm=True,
)
async def _confirm_share_words(ctx, share_index, share_words):
numbered = list(enumerate(share_words))
# check a word from the first half
first_half = numbered[: len(numbered) // 2]
if not await _confirm_word(ctx, share_index, first_half):
return False
# check a word from the second half
second_half = numbered[len(numbered) // 2 :]
if not await _confirm_word(ctx, share_index, second_half):
return False
return True
async def _confirm_word(ctx, share_index, numbered_share_words):
# TODO: duplicated words in the choice list
# shuffle the numbered seed half, slice off the choices we need
random.shuffle(numbered_share_words)
numbered_choices = numbered_share_words[: MnemonicWordSelect.NUM_OF_CHOICES]
# we always confirm the first (random) word index
checked_index, checked_word = numbered_choices[0]
if __debug__:
debug.reset_word_index = checked_index
# shuffle again so the confirmed word is not always the first choice
random.shuffle(numbered_choices)
# let the user pick a word
choices = [word for _, word in numbered_choices]
select = MnemonicWordSelect(choices, share_index, checked_index)
if __debug__:
selected_word = await ctx.wait(select, debug.input_signal)
else:
selected_word = await ctx.wait(select)
# confirm it is the correct one
return selected_word == checked_word
async def _show_confirmation_success(ctx, share_index):
if share_index is None:
text = Text("Recovery seed", ui.ICON_RESET)
text.bold("Recovery seed")
text.bold("checked successfully.")
else:
text = Text("Recovery share #%s" % (share_index + 1), ui.ICON_RESET)
text.bold("Seed share #%s" % (share_index + 1))
text.bold("checked successfully.")
text.normal("Let's continue with")
text.normal("share #%s." % (share_index + 2))
return await confirm(
ctx, text, ButtonRequestType.ResetDevice, cancel=None, confirm="Continue"
)
async def _show_confirmation_failure(ctx, share_index):
if share_index is None:
text = Text("Recovery seed", ui.ICON_WRONG, ui.RED)
else:
text = Text("Recovery share #%s" % (share_index + 1), ui.ICON_WRONG, ui.RED)
text.bold("You have entered")
text.bold("wrong seed word.")
text.bold("Please check again.")
await require_confirm(
ctx, text, ButtonRequestType.ResetDevice, confirm="Check again", cancel=None
)
# BIP39
# ===
async def bip39_show_and_confirm_mnemonic(ctx, mnemonic: str):
words = mnemonic.split()
# require confirmation of the mnemonic safety
await bip39_show_backup_warning(ctx)
while True:
# display paginated mnemonic on the screen
await _bip39_show_mnemonic(ctx, words)
# make the user confirm 2 words from the mnemonic
if await _confirm_share_words(ctx, None, words):
await _show_confirmation_success(ctx, None)
break # this share is confirmed, go to next one
else:
await _show_confirmation_failure(ctx, None)
async def bip39_show_backup_warning(ctx):
text = Text("Backup your seed", ui.ICON_NOCOPY)
text.normal(
"Never make a digital",
"copy of your recovery",
"seed and never upload",
"it online!",
)
await require_confirm(
ctx, text, ButtonRequestType.ResetDevice, confirm="I understand", cancel=None
)
async def _bip39_show_mnemonic(ctx, words: list):
# split mnemonic words into pages
PER_PAGE = const(4)
words = list(enumerate(words))
words = list(utils.chunks(words, PER_PAGE))
# display the pages, with a confirmation dialog on the last one
pages = [_get_mnemonic_page(page) for page in words]
paginated = Paginated(pages)
if __debug__:
def export_displayed_words():
# export currently displayed mnemonic words into debuglink
debug.reset_current_words = [w for _, w in words[paginated.page]]
paginated.on_change = export_displayed_words
export_displayed_words()
await hold_to_confirm(ctx, paginated, ButtonRequestType.ResetDevice)
def _get_mnemonic_page(words: list):
text = Text("Recovery seed", ui.ICON_RESET)
for index, word in words:
text.mono("%2d. %s" % (index + 1, word))
return text
# SLIP39
# ===
# TODO: yellow cancel style?
# TODO: loading animation style?
# TODO: smaller font or tighter rows to fit more text in
# TODO: icons in checklist
async def slip39_show_checklist_set_shares(ctx):
checklist = Checklist("Backup checklist", ui.ICON_RESET)
checklist.add("Set number of shares")
checklist.add("Set the threshold")
checklist.add(("Write down and check", "all seed shares"))
checklist.select(0)
checklist.process()
return await confirm(
ctx, checklist, ButtonRequestType.ResetDevice, cancel=None, confirm="Set shares"
)
async def slip39_show_checklist_set_threshold(ctx, num_of_shares):
checklist = Checklist("Backup checklist", ui.ICON_RESET)
checklist.add("Set number of shares")
checklist.add("Set the threshold")
checklist.add(("Write down and check", "all seed shares"))
checklist.select(1)
checklist.process()
return await confirm(
ctx,
checklist,
ButtonRequestType.ResetDevice,
cancel=None,
confirm="Set threshold",
)
async def slip39_show_checklist_show_shares(ctx, num_of_shares, threshold):
checklist = Checklist("Backup checklist", ui.ICON_RESET)
checklist.add("Set number of shares")
checklist.add("Set the threshold")
checklist.add(("Write down and check", "all seed shares"))
checklist.select(2)
checklist.process()
return await confirm(
ctx,
checklist,
ButtonRequestType.ResetDevice,
cancel=None,
confirm="Show seed shares",
)
async def slip39_prompt_number_of_shares(ctx):
count = 5
min_count = 2
max_count = 16
while True:
shares = ShamirNumInput(ShamirNumInput.SET_SHARES, count, min_count, max_count)
info = InfoConfirm(
"Shares are parts of "
"the recovery seed, "
"each containing 20 "
"words. You can later set "
"how many shares you "
"need to recover your "
"wallet."
)
confirmed = await confirm(
ctx,
shares,
ButtonRequestType.ResetDevice,
cancel="Info",
confirm="Set",
major_confirm=True,
cancel_style=ButtonDefault,
)
count = shares.input.count
if confirmed:
break
else:
await info
return count
async def slip39_prompt_threshold(ctx, num_of_shares):
count = num_of_shares // 2
min_count = 2
max_count = num_of_shares
while True:
shares = ShamirNumInput(
ShamirNumInput.SET_THRESHOLD, count, min_count, max_count
)
info = InfoConfirm(
"Threshold sets number "
"shares that you need "
"to recover your wallet. "
"i.e. Set it to %s and "
"you'll need any %s shares "
"of the total number." % (count, count)
)
confirmed = await confirm(
ctx,
shares,
ButtonRequestType.ResetDevice,
cancel="Info",
confirm="Set",
major_confirm=True,
cancel_style=ButtonDefault,
)
count = shares.input.count
if confirmed:
break
else:
await info
return count
async def slip39_show_and_confirm_shares(ctx, shares):
for index, share in enumerate(shares):
share_words = share.split(" ")
while True:
# display paginated share on the screen
await _slip39_show_share_words(ctx, index, share_words)
# make the user confirm 2 words from the share
if await _confirm_share_words(ctx, index, share_words):
await _show_confirmation_success(ctx, index)
break # this share is confirmed, go to next one
else:
await _show_confirmation_failure(ctx, index)
async def _slip39_show_share_words(ctx, share_index, share_words):
first, chunks, last = _slip39_split_share_into_pages(share_words)
if share_index is None:
header_title = "Recovery share #%s" % (share_index + 1)
else:
header_title = "Recovery seed"
header_icon = ui.ICON_RESET
pages = [] # ui page components
shares_words_check = [] # check we display correct data
# first page
text = Text(header_title, header_icon)
text.normal("Write down %s words" % len(share_words))
text.normal("onto paper booklet:")
text.br_half()
for index, word in first:
text.mono("%s. %s" % (index + 1, word))
shares_words_check.append(word)
pages.append(text)
# middle pages
for chunk in chunks:
text = Text(header_title, header_icon)
text.br_half()
for index, word in chunk:
text.mono("%s. %s" % (index + 1, word))
shares_words_check.append(word)
pages.append(text)
# last page
text = Text(header_title, header_icon)
for index, word in last:
text.mono("%s. %s" % (index + 1, word))
shares_words_check.append(word)
text.br_half()
text.normal("I confirm that I wrote")
text.normal("down all %s words." % len(share_words))
pages.append(text)
# pagination
paginated = Paginated(pages)
if __debug__:
word_pages = [first] + chunks + [last]
def export_displayed_words():
# export currently displayed mnemonic words into debuglink
debug.reset_current_words = word_pages[paginated.page]
paginated.on_change = export_displayed_words
export_displayed_words()
# make sure we display correct data
utils.ensure(share_words == shares_words_check)
# confirm the share
await hold_to_confirm(ctx, paginated) # TODO: customize the loader here
def _slip39_split_share_into_pages(share_words):
share = list(enumerate(share_words)) # we need to keep track of the word indices
first = share[:2] # two words on the first page
middle = share[2:-2]
last = share[-2:] # two words on the last page
chunks = utils.chunks(middle, 4) # 4 words on the middle pages
return first, list(chunks), last
class ShamirNumInput(ui.Control):
SET_SHARES = object()
SET_THRESHOLD = object()
def __init__(self, step, count, min_count, max_count):
self.step = step
self.input = NumInput(count, min_count=min_count, max_count=max_count)
self.input.on_change = self.on_change
self.repaint = True
def dispatch(self, event, x, y):
self.input.dispatch(event, x, y)
if event is ui.RENDER:
self.on_render()
def on_render(self):
if self.repaint:
count = self.input.count
# render the headline
if self.step is ShamirNumInput.SET_SHARES:
header = "Set num. of shares"
elif self.step is ShamirNumInput.SET_THRESHOLD:
header = "Set the threshold"
ui.header(header, ui.ICON_RESET, ui.TITLE_GREY, ui.BG, ui.ORANGE_ICON)
# render the counter
if self.step is ShamirNumInput.SET_SHARES:
ui.display.text(
12, 130, "%s people or locations" % count, ui.BOLD, ui.FG, ui.BG
)
ui.display.text(
12, 156, "will each host one share.", ui.NORMAL, ui.FG, ui.BG
)
elif self.step is ShamirNumInput.SET_THRESHOLD:
ui.display.text(
12, 130, "For recovery you'll need", ui.NORMAL, ui.FG, ui.BG
)
ui.display.text(
12, 156, "any %s of shares." % count, ui.BOLD, ui.FG, ui.BG
)
self.repaint = False
def on_change(self, count):
self.repaint = True
class MnemonicWordSelect(ui.Layout):
NUM_OF_CHOICES = 3
def __init__(self, words, share_index, word_index):
self.words = words
self.share_index = share_index
self.word_index = word_index
self.buttons = []
for i, word in enumerate(words):
area = ui.grid(i + 2, n_x=1)
btn = Button(area, word)
btn.on_click = self.select(word)
self.buttons.append(btn)
if share_index is None:
self.text = Text("Recovery seed")
else:
self.text = Text("Recovery share #%s" % (share_index + 1))
self.text.normal("Choose the %s word:" % utils.format_ordinal(word_index + 1))
def dispatch(self, event, x, y):
for btn in self.buttons:
btn.dispatch(event, x, y)
self.text.dispatch(event, x, y)
def select(self, word):
def fn():
raise ui.Result(word)
return fn

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

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

@ -1,20 +1,13 @@
from micropython import const
from ubinascii import hexlify
from trezor import config, ui, wire
from trezor import config, wire
from trezor.crypto import bip39, hashlib, random
from trezor.messages import ButtonRequestType, MessageType
from trezor.messages import MessageType
from trezor.messages.EntropyRequest import EntropyRequest
from trezor.messages.Success import Success
from trezor.pin import pin_to_int
from trezor.ui.mnemonic import MnemonicKeyboard
from trezor.ui.scroll import Paginated
from trezor.ui.text import Text
from trezor.utils import chunks, format_ordinal
from apps.common import mnemonic, storage
from apps.common.confirm import hold_to_confirm, require_confirm
from apps.management.change_pin import request_pin_confirm
from apps.management.common import layout
if __debug__:
from apps import debug
@ -22,15 +15,10 @@ if __debug__:
async def reset_device(ctx, msg):
# validate parameters and device state
if msg.strength not in (128, 192, 256):
raise wire.ProcessError("Invalid strength (has to be 128, 192 or 256 bits)")
if msg.display_random and (msg.skip_backup or msg.no_backup):
raise wire.ProcessError("Can't show internal entropy when backup is skipped")
if storage.is_initialized():
raise wire.UnexpectedMessage("Already initialized")
_validate_reset_device(msg)
# make sure use knows he's setting up a new wallet
await show_reset_warning(ctx)
# make sure user knows he's setting up a new wallet
await layout.show_reset_device_warning(ctx, msg.slip39)
# request new PIN
if msg.pin_protection:
@ -39,165 +27,100 @@ async def reset_device(ctx, msg):
newpin = ""
# generate and display internal entropy
internal_ent = random.bytes(32)
int_entropy = random.bytes(32)
if __debug__:
debug.reset_internal_entropy = internal_ent
debug.reset_internal_entropy = int_entropy
if msg.display_random:
await show_entropy(ctx, internal_ent)
# request external entropy and compute mnemonic
ent_ack = await ctx.call(EntropyRequest(), MessageType.EntropyAck)
words = generate_mnemonic(msg.strength, internal_ent, ent_ack.entropy)
if not msg.skip_backup and not msg.no_backup:
# require confirmation of the mnemonic safety
await show_backup_warning(ctx)
# show mnemonic and require confirmation of a random word
while True:
await show_mnemonic(ctx, words)
if await check_mnemonic(ctx, words):
break
await show_wrong_entry(ctx)
await layout.show_internal_entropy(ctx, int_entropy)
# request external entropy and compute the master secret
entropy_ack = await ctx.call(EntropyRequest(), MessageType.EntropyAck)
ext_entropy = entropy_ack.entropy
secret = _compute_secret_from_entropy(int_entropy, ext_entropy, msg.strength)
# should we back up the wallet now?
if not msg.no_backup and not msg.skip_backup:
if not await layout.confirm_backup(ctx):
if not await layout.confirm_backup_again(ctx):
msg.skip_backup = True
# generate and display backup information for the master secret
if not msg.no_backup and not msg.skip_backup:
if msg.slip39:
await backup_slip39_wallet(ctx, secret)
else:
await backup_bip39_wallet(ctx, secret)
# write PIN into storage
if newpin:
if not config.change_pin(pin_to_int(""), pin_to_int(newpin)):
raise wire.ProcessError("Could not change PIN")
if not config.change_pin(pin_to_int(""), pin_to_int(newpin)):
raise wire.ProcessError("Could not change PIN")
secret = mnemonic.process([words], mnemonic.TYPE_BIP39)
# write settings and mnemonic into storage
# write settings and master secret into storage
storage.load_settings(label=msg.label, use_passphrase=msg.passphrase_protection)
storage.store_mnemonic(
secret=secret,
mnemonic_type=mnemonic.TYPE_BIP39,
needs_backup=msg.skip_backup,
no_backup=msg.no_backup,
)
if msg.slip39:
mnemonic.slip39.store(
secret=secret, needs_backup=msg.skip_backup, no_backup=msg.no_backup
)
else:
# in BIP-39 we store mnemonic string instead of the secret
mnemonic.bip39.store(
secret=bip39.from_data(secret).encode(),
needs_backup=msg.skip_backup,
no_backup=msg.no_backup,
)
# show success message
if not msg.skip_backup and not msg.no_backup:
await show_success(ctx)
# if we backed up the wallet, show success message
if not msg.no_backup and not msg.skip_backup:
await layout.show_backup_success(ctx)
return Success(message="Initialized")
def generate_mnemonic(strength: int, int_entropy: bytes, ext_entropy: bytes) -> bytes:
ehash = hashlib.sha256()
ehash.update(int_entropy)
ehash.update(ext_entropy)
entropy = ehash.digest()
return bip39.from_data(entropy[: strength // 8])
async def show_reset_warning(ctx):
text = Text("Create a new wallet", ui.ICON_RESET, new_lines=False)
text.normal("Do you really want to")
text.br()
text.normal("create a new wallet?")
text.br()
text.br_half()
text.normal("By continuing you agree")
text.br()
text.normal("to")
text.bold("https://trezor.io/tos")
await require_confirm(ctx, text, code=ButtonRequestType.ResetDevice)
async def show_backup_warning(ctx):
text = Text("Backup your seed", ui.ICON_NOCOPY)
text.normal(
"Never make a digital",
"copy of your recovery",
"seed and never upload",
"it online!",
)
await require_confirm(
ctx, text, ButtonRequestType.ResetDevice, confirm="I understand", cancel=None
)
async def show_wrong_entry(ctx):
text = Text("Wrong entry!", ui.ICON_WRONG, ui.RED)
text.normal("You have entered", "wrong seed word.", "Please check again.")
await require_confirm(
ctx, text, ButtonRequestType.ResetDevice, confirm="Check again", cancel=None
)
async def show_success(ctx):
text = Text("Backup is done!", ui.ICON_CONFIRM, ui.GREEN)
text.normal(
"Never make a digital",
"copy of your recovery",
"seed and never upload",
"it online!",
)
await require_confirm(
ctx, text, ButtonRequestType.ResetDevice, confirm="Finish setup", cancel=None
)
async def show_entropy(ctx, entropy: bytes):
entropy_str = hexlify(entropy).decode()
lines = chunks(entropy_str, 16)
text = Text("Internal entropy", ui.ICON_RESET)
text.mono(*lines)
await require_confirm(ctx, text, ButtonRequestType.ResetDevice)
async def show_mnemonic(ctx, mnemonic: str):
# split mnemonic words into pages
PER_PAGE = const(4)
words = mnemonic.split()
words = list(enumerate(words))
words = list(chunks(words, PER_PAGE))
# display the pages, with a confirmation dialog on the last one
pages = [get_mnemonic_page(page) for page in words]
paginated = Paginated(pages)
if __debug__:
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)
def export_displayed_words():
# export currently displayed mnemonic words into debuglink
debug.reset_current_words = [w for _, w in words[paginated.page]]
# get threshold
await layout.slip39_show_checklist_set_threshold(ctx, shares_count)
threshold = await layout.slip39_prompt_threshold(ctx, shares_count)
paginated.on_change = export_displayed_words
export_displayed_words()
# generate the mnemonics
mnemonics = mnemonic.slip39.generate_from_secret(secret, shares_count, threshold)
await hold_to_confirm(ctx, paginated, ButtonRequestType.ResetDevice)
# 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)
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 backup_bip39_wallet(ctx, secret: bytes):
mnemonic = bip39.from_data(secret)
await layout.bip39_show_and_confirm_mnemonic(ctx, mnemonic)
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
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.")
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]
def _compute_secret_from_entropy(
int_entropy: bytes, ext_entropy: bytes, strength_in_bytes: int
) -> bytes:
# combine internal and external entropy
ehash = hashlib.sha256()
ehash.update(int_entropy)
ehash.update(ext_entropy)
entropy = ehash.digest()
# take a required number of bytes
strength = strength_in_bytes // 8
secret = entropy[:strength]
return secret

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

File diff suppressed because one or more lines are too long

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

@ -0,0 +1,55 @@
from micropython import const
from trezor import ui
from trezor.ui import text
_CHECKLIST_MAX_LINES = const(5)
_CHECKLIST_OFFSET_X = const(24)
class Checklist(ui.Control):
def __init__(self, title, icon):
self.title = title
self.icon = icon
self.choices = []
self.words = []
self.active = 0
self.repaint = False
def add(self, choice):
self.choices.append(choice)
def select(self, active):
self.active = active
def process(self):
w = self.words
w.clear()
for index, choice in enumerate(self.choices):
if index < self.active:
w.append(ui.BOLD)
w.append(ui.GREEN)
elif index == self.active:
w.append(ui.BOLD)
w.append(ui.FG)
else: # index > self.active
w.append(ui.NORMAL)
w.append(ui.GREY)
if isinstance(choice, str):
w.append(choice)
else: # choice is iterable
w.extend(choice)
w.append(text.BR)
self.words = w
self.repaint = True
def on_render(self):
if self.repaint:
ui.header(self.title, self.icon)
text.render_text(
self.words,
new_lines=False, # we are adding line breaks manually
max_lines=_CHECKLIST_MAX_LINES,
offset_x=_CHECKLIST_OFFSET_X,
)
self.repaint = False

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

@ -0,0 +1,63 @@
from trezor import res, ui
from trezor.ui.button import Button, ButtonConfirm
from trezor.ui.confirm import CONFIRMED
from trezor.ui.text import TEXT_LINE_HEIGHT, TEXT_MARGIN_LEFT, render_text
class DefaultInfoConfirm:
fg_color = ui.LIGHT_GREY
bg_color = ui.BLACKISH
class button(ButtonConfirm):
class normal(ButtonConfirm.normal):
border_color = ui.BLACKISH
class disabled(ButtonConfirm.disabled):
border_color = ui.BLACKISH
class InfoConfirm(ui.Layout):
DEFAULT_CONFIRM = res.load(ui.ICON_CONFIRM)
DEFAULT_STYLE = DefaultInfoConfirm
def __init__(self, text, confirm=DEFAULT_CONFIRM, style=DEFAULT_STYLE):
self.text = text.split()
self.style = style
panel_area = ui.grid(0, n_x=1, n_y=1)
self.panel_area = panel_area
confirm_area = ui.grid(4, n_x=1)
self.confirm = Button(confirm_area, confirm, style.button)
self.confirm.on_click = self.on_confirm
self.repaint = True
def dispatch(self, event, x, y):
if event == ui.RENDER:
self.on_render()
self.confirm.dispatch(event, x, y)
def on_render(self):
if self.repaint:
x, y, w, h = self.panel_area
fg_color = self.style.fg_color
bg_color = self.style.bg_color
# render the background panel
ui.display.bar_radius(x, y, w, h, bg_color, ui.BG, ui.RADIUS)
# render the info text
render_text(
self.text,
new_lines=False,
max_lines=6,
offset_y=y + TEXT_LINE_HEIGHT,
offset_x=x + TEXT_MARGIN_LEFT - ui.VIEWX,
offset_x_max=x + w - ui.VIEWX,
fg=fg_color,
bg=bg_color,
)
self.repaint = False
def on_confirm(self):
raise ui.Result(CONFIRMED)

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

@ -90,7 +90,7 @@ class Prompt(ui.Control):
self.repaint = False
class MnemonicKeyboard(ui.Layout):
class Bip39Keyboard(ui.Layout):
def __init__(self, prompt):
self.prompt = Prompt(prompt)

@ -0,0 +1,205 @@
from trezor import io, loop, res, ui
from trezor.crypto import slip39
from trezor.ui import display
from trezor.ui.button import Button, ButtonClear, ButtonMono, ButtonMonoConfirm
def check_mask(mask: int, index: int) -> bool:
return bool((1 << (index - 1)) & mask)
# TODO: ask UX if we want to finish sooner than 4 words
# example: 'hairy'
def _is_final(content: str) -> bool:
return len(content) > 3
class KeyButton(Button):
def __init__(self, area, content, keyboard, index):
self.keyboard = keyboard
self.index = index
super().__init__(area, content)
def on_click(self):
self.keyboard.on_key_click(self)
class InputButton(Button):
def __init__(self, area, content, word):
super().__init__(area, content)
self.word = word
self.pending_button = None
self.pending_index = None
self.icon = None # rendered icon
self.disable()
def edit(self, content, word, pending_button, pending_index):
self.word = word
self.content = content
self.pending_button = pending_button
self.pending_index = pending_index
self.repaint = True
if word:
self.enable()
self.normal_style = ButtonMonoConfirm.normal
self.active_style = ButtonMonoConfirm.active
self.icon = ui.ICON_CONFIRM
else: # disabled button
self.disabled_style = ButtonMono.normal
self.disable()
self.icon = None
def render_content(self, s, ax, ay, aw, ah):
text_style = s.text_style
fg_color = s.fg_color
bg_color = s.bg_color
tx = ax + 24 # x-offset of the content
ty = ay + ah // 2 + 8 # y-offset of the content
if not _is_final(self.content):
to_display = len(self.content) * "*"
if self.pending_button:
to_display = (
to_display[:-1] + self.pending_button.content[self.pending_index]
)
else:
to_display = self.word
display.text(tx, ty, to_display, text_style, fg_color, bg_color)
if self.pending_button and not _is_final(self.content):
width = display.text_width(to_display, text_style)
pw = display.text_width(self.content[-1:], text_style)
px = tx + width - pw
display.bar(px, ty + 2, pw + 1, 3, fg_color)
if self.icon:
ix = ax + aw - 16 * 2
iy = ty - 16
display.icon(ix, iy, res.load(self.icon), fg_color, bg_color)
class Prompt(ui.Control):
def __init__(self, prompt):
self.prompt = prompt
self.repaint = True
def on_render(self):
if self.repaint:
display.bar(0, 8, ui.WIDTH, 60, ui.BG)
display.text(20, 40, self.prompt, ui.BOLD, ui.GREY, ui.BG)
self.repaint = False
class Slip39Keyboard(ui.Layout):
def __init__(self, prompt):
self.prompt = Prompt(prompt)
icon_back = res.load(ui.ICON_BACK)
self.back = Button(ui.grid(0, n_x=4, n_y=4), icon_back, ButtonClear)
self.back.on_click = self.on_back_click
self.input = InputButton(ui.grid(1, n_x=4, n_y=4, cells_x=3), "", "")
self.input.on_click = self.on_input_click
self.keys = [
KeyButton(ui.grid(i + 3, n_y=4), k, self, i + 1)
for i, k in enumerate(
("ab", "cd", "ef", "ghij", "klm", "nopq", "rs", "tuv", "wxyz")
)
]
self.pending_button = None
self.pending_index = 0
self.button_sequence = ""
def dispatch(self, event: int, x: int, y: int):
for btn in self.keys:
btn.dispatch(event, x, y)
if self.input.content:
self.input.dispatch(event, x, y)
self.back.dispatch(event, x, y)
else:
self.prompt.dispatch(event, x, y)
def on_back_click(self):
# Backspace was clicked, let's delete the last character of input.
self.button_sequence = self.button_sequence[:-1]
self.edit()
def on_input_click(self):
# Input button was clicked. If the content matches the suggested word,
# let's confirm it, otherwise just auto-complete.
result = self.input.word
if _is_final(self.input.content):
self.button_sequence = ""
self.edit()
self.on_confirm(result)
def on_key_click(self, btn: KeyButton):
# Key button was clicked. If this button is pending, let's cycle the
# pending character in input. If not, let's just append the first
# character.
if self.pending_button is btn:
index = (self.pending_index + 1) % len(btn.content)
else:
index = 0
self.button_sequence += str(btn.index)
self.edit(btn, index)
def on_timeout(self):
# Timeout occurred. Let's redraw to draw asterisks.
self.edit()
def on_confirm(self, word):
# Word was confirmed by the user.
raise ui.Result(word)
def edit(self, button: KeyButton = None, index: int = 0):
self.pending_button = button
self.pending_index = index
# find the completions
mask = 0
word = ""
if _is_final(self.button_sequence):
word = slip39.button_sequence_to_word(self.button_sequence)
else:
mask = slip39.compute_mask(self.button_sequence)
# modify the input state
self.input.edit(
self.button_sequence, word, self.pending_button, self.pending_index
)
# enable or disable key buttons
for btn in self.keys:
if (not _is_final(self.button_sequence) and btn is button) or check_mask(
mask, btn.index
):
btn.enable()
else:
btn.disable()
# invalidate the prompt if we display it next frame
if not self.input.content:
self.prompt.repaint = True
async def handle_input(self):
touch = loop.wait(io.TOUCH)
timeout = loop.sleep(1000 * 1000 * 1)
spawn_touch = loop.spawn(touch)
spawn_timeout = loop.spawn(touch, timeout)
while True:
if self.pending_button is not None:
spawn = spawn_timeout
else:
spawn = spawn_touch
result = await spawn
if touch in spawn.finished:
event, x, y = result
self.dispatch(event, x, y)
else:
self.on_timeout()

@ -0,0 +1,49 @@
from trezor import ui
from trezor.ui.button import Button
from trezor.ui.text import LABEL_CENTER, Label
class NumInput(ui.Control):
def __init__(self, count=5, max_count=16, min_count=1):
self.count = count
self.max_count = max_count
self.min_count = min_count
self.minus = Button(ui.grid(3), "-")
self.minus.on_click = self.on_minus
self.plus = Button(ui.grid(5), "+")
self.plus.on_click = self.on_plus
self.text = Label(ui.grid(4), "", LABEL_CENTER, ui.BOLD)
self.edit(count)
def dispatch(self, event, x, y):
self.minus.dispatch(event, x, y)
self.plus.dispatch(event, x, y)
self.text.dispatch(event, x, y)
def on_minus(self):
self.edit(self.count - 1)
def on_plus(self):
self.edit(self.count + 1)
def edit(self, count):
count = max(count, self.min_count)
count = min(count, self.max_count)
if self.count != count:
self.on_change(count)
self.count = count
self.text.content = str(count)
self.text.repaint = True
if self.count == self.min_count:
self.minus.disable()
else:
self.minus.enable()
if self.count == self.max_count:
self.plus.disable()
else:
self.plus.enable()
def on_change(self, count):
pass

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

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

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

@ -66,6 +66,7 @@ SRCS += memzero.c
SRCS += shamir.c
SRCS += hmac_drbg.c
SRCS += rfc6979.c
SRCS += slip39.c
OBJS = $(SRCS:.c=.o)

@ -0,0 +1,136 @@
/**
* This file is part of the TREZOR project, https://trezor.io/
*
* Copyright (c) SatoshiLabs
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
* OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#include "slip39.h"
#include <stdio.h>
#include <string.h>
#include "slip39_wordlist.h"
/**
* Returns word on position `index`.
*/
const char* get_word(uint16_t index) { return wordlist[index]; }
/**
* Finds index of given word, if found.
* Returns true on success and stores result in `index`.
*/
bool word_index(uint16_t* index, const char* word, uint8_t word_length) {
uint16_t lo = 0;
uint16_t hi = WORDS_COUNT;
uint16_t mid = 0;
while ((hi - lo) > 1) {
mid = (hi + lo) / 2;
if (strncmp(wordlist[mid], word, word_length) > 0) {
hi = mid;
} else {
lo = mid;
}
}
if (strncmp(wordlist[lo], word, word_length) != 0) {
return false;
}
*index = lo;
return true;
}
/**
* 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.
*/
uint16_t compute_mask(uint16_t prefix) { return find(prefix, false); }
/**
* Converts sequence to word index.
*/
const char* button_sequence_to_word(uint16_t prefix) {
return wordlist[find(prefix, true)];
}
/**
* Computes mask if find_index is false.
* Finds the first word index that suits the prefix otherwise.
*/
uint16_t find(uint16_t prefix, bool find_index) {
uint16_t min = prefix;
uint16_t max = 0;
uint16_t for_max = 0;
uint8_t multiplier = 0;
uint8_t divider = 0;
uint16_t bitmap = 0;
uint8_t digit = 0;
uint16_t i = 0;
max = prefix + 1;
while (min < 1000) {
min = min * 10;
max = max * 10;
multiplier++;
}
// Four char prefix -> the mask is zero
if (!multiplier && !find_index) {
return 0;
}
for_max = min - (min % 1000) + 1000;
// We can't use binary search because the numbers are not sorted.
// They are sorted using the words' alphabet (so we can use the index).
// Example: axle (1953), beam (1315)
// The first digit is sorted so we can loop only upto `for_max`.
while (words_button_seq[i] < for_max) {
if (words_button_seq[i] >= min && words_button_seq[i] < max) {
if (find_index) {
return i;
}
switch (multiplier) {
case 1:
divider = 1;
break;
case 2:
divider = 10;
break;
case 3:
divider = 100;
break;
default:
divider = 1;
break;
}
digit = (words_button_seq[i] / divider) % 10;
bitmap |= 1 << (digit - 1);
}
i++;
}
return bitmap;
}

@ -0,0 +1,36 @@
/**
* This file is part of the TREZOR project, https://trezor.io/
*
* Copyright (c) SatoshiLabs
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
* OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#include <stdbool.h>
#include <stdint.h>
const char* get_word(uint16_t index);
bool word_index(uint16_t* index, const char* word, uint8_t word_length);
uint16_t compute_mask(uint16_t prefix);
const char* button_sequence_to_word(uint16_t prefix);
uint16_t find(uint16_t prefix, bool find_index);

File diff suppressed because it is too large Load Diff

@ -67,6 +67,7 @@
#include "sha2.h"
#include "sha3.h"
#include "shamir.h"
#include "slip39.h"
#if VALGRIND
/*
@ -5096,6 +5097,96 @@ START_TEST(test_mnemonic_to_entropy) {
}
END_TEST
START_TEST(test_slip39_get_word) {
static const struct {
const int index;
const char *expected_word;
} vectors[] = {{573, "member"},
{0, "academic"},
{1023, "zero"},
{245, "drove"},
{781, "satoshi"}};
for (size_t i = 0; i < (sizeof(vectors) / sizeof(*vectors)); i++) {
const char *a = get_word(vectors[i].index);
ck_assert_str_eq(a, vectors[i].expected_word);
}
}
END_TEST
START_TEST(test_slip39_word_index) {
uint16_t index;
static const struct {
const char *word;
bool expected_result;
uint16_t expected_index;
} vectors[] = {{"academic", true, 0},
{"zero", true, 1023},
{"drove", true, 245},
{"satoshi", true, 781},
{"member", true, 573},
// 9999 value is never checked since the word is not in list
{"fakeword", false, 9999}};
for (size_t i = 0; i < (sizeof(vectors) / sizeof(*vectors)); i++) {
bool result = word_index(&index, vectors[i].word, sizeof(vectors[i].word));
ck_assert_int_eq(result, vectors[i].expected_result);
if (result) {
ck_assert_int_eq(index, vectors[i].expected_index);
}
}
}
END_TEST
START_TEST(test_slip39_compute_mask) {
static const struct {
const uint16_t prefix;
const uint16_t expected_mask;
} vectors[] = {{
12,
0xFD // 011111101
},
{
21,
0xF8 // 011111000
},
{
75,
0xAD // 010101101
},
{
4,
0x1F7 // 111110111
},
{
738,
0x6D // 001101101
},
{
9,
0x6D // 001101101
}};
for (size_t i = 0; i < (sizeof(vectors) / sizeof(*vectors)); i++) {
uint16_t mask = compute_mask(vectors[i].prefix);
ck_assert_int_eq(mask, vectors[i].expected_mask);
}
}
END_TEST
START_TEST(test_slip39_sequence_to_word) {
static const struct {
const uint16_t prefix;
const char *expected_word;
} vectors[] = {{7945, "swimming"},
{646, "photo"},
{5, "kernel"},
{34, "either"},
{62, "ocean"}};
for (size_t i = 0; i < (sizeof(vectors) / sizeof(*vectors)); i++) {
const char *word = button_sequence_to_word(vectors[i].prefix);
ck_assert_str_eq(word, vectors[i].expected_word);
}
}
END_TEST
START_TEST(test_shamir) {
#define SHAMIR_MAX_COUNT 16
static const struct {
@ -8676,6 +8767,13 @@ Suite *test_suite(void) {
tcase_add_test(tc, test_mnemonic_to_entropy);
suite_add_tcase(s, tc);
tc = tcase_create("slip39");
tcase_add_test(tc, test_slip39_get_word);
tcase_add_test(tc, test_slip39_word_index);
tcase_add_test(tc, test_slip39_compute_mask);
tcase_add_test(tc, test_slip39_sequence_to_word);
suite_add_tcase(s, tc);
tc = tcase_create("shamir");
tcase_add_test(tc, test_shamir);
suite_add_tcase(s, tc);

@ -505,6 +505,7 @@ def recovery_device(
@click.option("-u", "--u2f-counter", default=0)
@click.option("-s", "--skip-backup", is_flag=True)
@click.option("-n", "--no-backup", is_flag=True)
@click.option("-x", "--slip39", is_flag=True)
@click.pass_obj
def reset_device(
connect,
@ -516,6 +517,7 @@ def reset_device(
u2f_counter,
skip_backup,
no_backup,
slip39,
):
if strength:
strength = int(strength)
@ -530,6 +532,7 @@ def reset_device(
u2f_counter=u2f_counter,
skip_backup=skip_backup,
no_backup=no_backup,
slip39=slip39,
)

File diff suppressed because one or more lines are too long

@ -103,7 +103,6 @@ def wipe(client):
return ret
@expect(proto.Success, field="message")
def recover(
client,
word_count=24,
@ -168,6 +167,7 @@ def reset(
u2f_counter=0,
skip_backup=False,
no_backup=False,
slip39=False,
):
if client.features.initialized:
raise RuntimeError(
@ -191,6 +191,7 @@ def reset(
u2f_counter=u2f_counter,
skip_backup=bool(skip_backup),
no_backup=bool(no_backup),
slip39=bool(slip39),
)
resp = client.call(msg)

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

@ -0,0 +1,193 @@
# This file is part of the Trezor project.
#
# Copyright (C) 2012-2019 SatoshiLabs and contributors
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3
# as published by the Free Software Foundation.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the License along with this library.
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
import time
import pytest
from trezorlib import btc, device, messages as proto
from trezorlib.messages.PassphraseSourceType import HOST as PASSPHRASE_ON_HOST
from .common import TrezorTest
@pytest.mark.skip_t1
class TestMsgRecoveryDeviceShamir(TrezorTest):
def test_3of6_nopin_nopassphrase(self):
# 128 bits security, 3 of 6
mnemonics = [
"extra extend academic bishop cricket bundle tofu goat apart victim enlarge program behavior permit course armed jerky faint language modern",
"extra extend academic acne away best indicate impact square oasis prospect painting voting guest either argue username racism enemy eclipse",
"extra extend academic arcade born dive legal hush gross briefing talent drug much home firefly toxic analysis idea umbrella slice",
]
word_count = len(mnemonics[0].split(" "))
ret = self.client.call_raw(
proto.RecoveryDevice(
passphrase_protection=False, pin_protection=False, label="label"
)
)
# Confirm Recovery
assert isinstance(ret, proto.ButtonRequest)
self.client.debug.press_yes()
ret = self.client.call_raw(proto.ButtonAck())
# Enter word count
assert ret == proto.ButtonRequest(
code=proto.ButtonRequestType.MnemonicWordCount
)
self.client.debug.input(str(word_count))
ret = self.client.call_raw(proto.ButtonAck())
# Confirm T9 keyboard
assert isinstance(ret, proto.ButtonRequest)
self.client.debug.press_yes()
ret = self.client.call_raw(proto.ButtonAck())
# Enter shares
for mnemonic in mnemonics:
# Enter mnemonic words
assert ret == proto.ButtonRequest(
code=proto.ButtonRequestType.MnemonicInput
)
self.client.transport.write(proto.ButtonAck())
for word in mnemonic.split(" "):
time.sleep(1)
self.client.debug.input(word)
ret = self.client.transport.read()
if mnemonic != mnemonics[-1]:
# Confirm status
assert isinstance(ret, proto.ButtonRequest)
self.client.debug.press_yes()
ret = self.client.call_raw(proto.ButtonAck())
# Workflow succesfully ended
assert ret == proto.Success(message="Device recovered")
assert self.client.features.pin_protection is False
assert self.client.features.passphrase_protection is False
# Check mnemonic
assert (
self.client.debug.read_mnemonic_secret().hex()
== "491b795b80fc21ccdf466c0fbc98c8fc"
)
# BIP32 Root Key for empty passphrase
# provided by Andrew, address calculated using T1
# xprv9s21ZrQH143K3reExTJbGTHPu6mGuUx6yN1H1KkqoiAcw6j1t6hBiwwnXYxNQXxU8T7pANSv1rJYQPXn1LMQk1gbWes5BjSz3rJ5ZfH1cc3
address = btc.get_address(self.client, "Bitcoin", [])
assert address == "1G1MwH5sLVxKQ7yKYasfE5pxWaABLo7VK7"
# TODO: enable passphrase and test
# ask @jpochyla how to do this - see below
# # BIP32 Root Key for passphrase TREZOR
# # provided by Andrew, address calculated using T1
# # xprv9s21ZrQH143K2pMWi8jrTawHaj16uKk4CSbvo4Zt61tcrmuUDMx2o1Byzcr3saXNGNvHP8zZgXVdJHsXVdzYFPavxvCyaGyGr1WkAYG83ce
# self.client.set_passphrase("TREZOR")
# address = btc.get_address(self.client, "Bitcoin", [])
# assert address == "18oZEMRWurCZW1FeK8sWYyXuWx2bFqEKyX"
def test_2of5_pin_passphrase(self):
# 256 bits security, 2 of 5
mnemonics = [
"hobo romp academic axis august founder knife legal recover alien expect emphasis loan kitchen involve teacher capture rebuild trial numb spider forward ladle lying voter typical security quantity hawk legs idle leaves gasoline",
"hobo romp academic agency ancestor industry argue sister scene midst graduate profile numb paid headset airport daisy flame express scene usual welcome quick silent downtown oral critical step remove says rhythm venture aunt",
]
word_count = len(mnemonics[0].split(" "))
ret = self.client.call_raw(
proto.RecoveryDevice(
passphrase_protection=True, pin_protection=True, label="label"
)
)
# Confirm Recovery
assert isinstance(ret, proto.ButtonRequest)
self.client.debug.press_yes()
ret = self.client.call_raw(proto.ButtonAck())
# Enter word count
assert ret == proto.ButtonRequest(
code=proto.ButtonRequestType.MnemonicWordCount
)
self.client.debug.input(str(word_count))
ret = self.client.call_raw(proto.ButtonAck())
# Confirm T9 keyboard
assert isinstance(ret, proto.ButtonRequest)
self.client.debug.press_yes()
ret = self.client.call_raw(proto.ButtonAck())
# Enter shares
for mnemonic in mnemonics:
# Enter mnemonic words
assert ret == proto.ButtonRequest(
code=proto.ButtonRequestType.MnemonicInput
)
self.client.transport.write(proto.ButtonAck())
for word in mnemonic.split(" "):
time.sleep(1)
self.client.debug.input(word)
ret = self.client.transport.read()
if mnemonic != mnemonics[-1]:
# Confirm status
assert isinstance(ret, proto.ButtonRequest)
self.client.debug.press_yes()
ret = self.client.call_raw(proto.ButtonAck())
# Enter PIN for first time
assert ret == proto.ButtonRequest(code=proto.ButtonRequestType.Other)
self.client.debug.input("654")
ret = self.client.call_raw(proto.ButtonAck())
# Enter PIN for second time
assert ret == proto.ButtonRequest(code=proto.ButtonRequestType.Other)
self.client.debug.input("654")
ret = self.client.call_raw(proto.ButtonAck())
# Workflow succesfully ended
assert ret == proto.Success(message="Device recovered")
# Check mnemonic
self.client.init_device()
assert (
self.client.debug.read_mnemonic_secret().hex()
== "b770e0da1363247652de97a39bdbf2463be087848d709ecbf28e84508e31202a"
)
assert self.client.features.pin_protection is True
assert self.client.features.passphrase_protection is True
device.apply_settings(self.client, passphrase_source=PASSPHRASE_ON_HOST)
# BIP32 Root Key for empty passphrase
# provided by Andrew, address calculated using T1
# xprv9s21ZrQH143K2kP9RYJE5AFggTHLs8PbDaaTYtvh238THxDyXqyqQV6H1QpFr3aaQ7CFusFMYyGZ6VcK7aLADyCaCJrszovxtzVZmnRfca4
address = btc.get_address(self.client, "Bitcoin", [])
assert address == "1BmqXKM8M1gWA4bgkbPeCtJruRnrY2qYKP"
# BIP32 Root Key for empty passphrase
# provided by Andrew, address calculated using T1
# xprv9s21ZrQH143K2o6EXEHpVy8TCYoMmkBnDCCESLdR2ieKwmcNG48ck2XJQY4waS7RUQcXqR9N7HnQbUVEDMWYyREdF1idQqxFHuCfK7fqFni
# self.client.set_passphrase("TREZOR")
# TODO: @jpochyla this does not get send to Trezor :( How should I enter passphrase?
# address = btc.get_address(self.client, "Bitcoin", [])
# assert address == "19Fjs9AvT13Y2Nx8GtoVfADmFWnccsPinQ"

@ -45,6 +45,11 @@ class TestMsgResetDeviceT2(TrezorTest):
assert btn_code == B.ResetDevice
self.client.debug.press_yes()
# safety warning
btn_code = yield
assert btn_code == B.ResetDevice
self.client.debug.press_yes()
# mnemonic phrases
btn_code = yield
assert btn_code == B.ResetDevice
@ -64,7 +69,12 @@ class TestMsgResetDeviceT2(TrezorTest):
index = self.client.debug.state().reset_word_pos
self.client.debug.input(words[index])
# safety warning
# confirm recovery seed check
btn_code = yield
assert btn_code == B.ResetDevice
self.client.debug.press_yes()
# confirm success
btn_code = yield
assert btn_code == B.ResetDevice
self.client.debug.press_yes()
@ -78,6 +88,8 @@ class TestMsgResetDeviceT2(TrezorTest):
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.ResetDevice),
proto.Success(),
proto.Features(),
]
@ -138,6 +150,11 @@ class TestMsgResetDeviceT2(TrezorTest):
assert btn_code == B.ResetDevice
self.client.debug.press_yes()
# safety warning
btn_code = yield
assert btn_code == B.ResetDevice
self.client.debug.press_yes()
# mnemonic phrases
btn_code = yield
assert btn_code == B.ResetDevice
@ -157,7 +174,12 @@ class TestMsgResetDeviceT2(TrezorTest):
index = self.client.debug.state().reset_word_pos
self.client.debug.input(words[index])
# safety warning
# confirm recovery seed check
btn_code = yield
assert btn_code == B.ResetDevice
self.client.debug.press_yes()
# confirm success
btn_code = yield
assert btn_code == B.ResetDevice
self.client.debug.press_yes()
@ -174,6 +196,8 @@ class TestMsgResetDeviceT2(TrezorTest):
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.ResetDevice),
proto.Success(),
proto.Features(),
]

Loading…
Cancel
Save