mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-01-03 12:00:59 +00:00
commit
435d00114c
@ -191,6 +191,7 @@ message ResetDevice {
|
|||||||
optional uint32 u2f_counter = 7; // U2F counter
|
optional uint32 u2f_counter = 7; // U2F counter
|
||||||
optional bool skip_backup = 8; // postpone seed backup to BackupDevice workflow
|
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 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/sha2.c',
|
||||||
'vendor/trezor-crypto/sha3.c',
|
'vendor/trezor-crypto/sha3.c',
|
||||||
'vendor/trezor-crypto/shamir.c',
|
'vendor/trezor-crypto/shamir.c',
|
||||||
|
'vendor/trezor-crypto/slip39.c',
|
||||||
]
|
]
|
||||||
|
|
||||||
# libsecp256k1-zkp
|
# libsecp256k1-zkp
|
||||||
|
@ -86,6 +86,7 @@ SOURCE_MOD += [
|
|||||||
'vendor/trezor-crypto/sha2.c',
|
'vendor/trezor-crypto/sha2.c',
|
||||||
'vendor/trezor-crypto/sha3.c',
|
'vendor/trezor-crypto/sha3.c',
|
||||||
'vendor/trezor-crypto/shamir.c',
|
'vendor/trezor-crypto/shamir.c',
|
||||||
|
'vendor/trezor-crypto/slip39.c',
|
||||||
]
|
]
|
||||||
|
|
||||||
# libsecp256k1-zkp
|
# libsecp256k1-zkp
|
||||||
|
122
core/embed/extmod/modtrezorcrypto/modtrezorcrypto-slip39.h
Normal file
122
core/embed/extmod/modtrezorcrypto/modtrezorcrypto-slip39.h
Normal file
@ -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-sha3-512.h"
|
||||||
#include "modtrezorcrypto-sha512.h"
|
#include "modtrezorcrypto-sha512.h"
|
||||||
#include "modtrezorcrypto-shamir.h"
|
#include "modtrezorcrypto-shamir.h"
|
||||||
|
#include "modtrezorcrypto-slip39.h"
|
||||||
|
|
||||||
STATIC const mp_rom_map_elem_t mp_module_trezorcrypto_globals_table[] = {
|
STATIC const mp_rom_map_elem_t mp_module_trezorcrypto_globals_table[] = {
|
||||||
{MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_trezorcrypto)},
|
{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_QSTR(MP_QSTR_sha3_512),
|
||||||
MP_ROM_PTR(&mod_trezorcrypto_Sha3_512_type)},
|
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_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,
|
STATIC MP_DEFINE_CONST_DICT(mp_module_trezorcrypto_globals,
|
||||||
mp_module_trezorcrypto_globals_table);
|
mp_module_trezorcrypto_globals_table);
|
||||||
|
35
core/mocks/generated/trezorcrypto/slip39.py
Normal file
35
core/mocks/generated/trezorcrypto/slip39.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from typing import *
|
||||||
|
|
||||||
|
|
||||||
|
# extmod/modtrezorcrypto/modtrezorcrypto-slip39.h
|
||||||
|
def compute_mask(prefix: int) -> int:
|
||||||
|
"""
|
||||||
|
Calculates which buttons still can be pressed after some already were.
|
||||||
|
Returns a 9-bit bitmask, where each bit specifies which buttons
|
||||||
|
can be further pressed (there are still words in this combination).
|
||||||
|
LSB denotes first button.
|
||||||
|
Example: 110000110 - second, third, eighth and ninth button still can be
|
||||||
|
pressed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# extmod/modtrezorcrypto/modtrezorcrypto-slip39.h
|
||||||
|
def button_sequence_to_word(prefix: int) -> str:
|
||||||
|
"""
|
||||||
|
Finds the first word that fits the given button prefix.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# extmod/modtrezorcrypto/modtrezorcrypto-slip39.h
|
||||||
|
def word_index(word: str) -> int:
|
||||||
|
"""
|
||||||
|
Finds index of given word.
|
||||||
|
Raises ValueError if not found.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# extmod/modtrezorcrypto/modtrezorcrypto-slip39.h
|
||||||
|
def get_word(index: int) -> str:
|
||||||
|
"""
|
||||||
|
Returns word on position 'index'.
|
||||||
|
"""
|
@ -37,7 +37,13 @@ async def get_keychain(ctx: wire.Context) -> Keychain:
|
|||||||
if passphrase is None:
|
if passphrase is None:
|
||||||
passphrase = await protect_by_passphrase(ctx)
|
passphrase = await protect_by_passphrase(ctx)
|
||||||
cache.set_passphrase(passphrase)
|
cache.set_passphrase(passphrase)
|
||||||
root = bip32.from_mnemonic_cardano(mnemonic.restore(), passphrase)
|
# TODO fix for SLIP-39!
|
||||||
|
mnemonic_secret, mnemonic_type = mnemonic.get()
|
||||||
|
if mnemonic_type == mnemonic.TYPE_SLIP39:
|
||||||
|
# TODO: we need to modify bip32.from_mnemonic_cardano to accept entropy directly
|
||||||
|
raise NotImplementedError("SLIP-39 currently does not support Cardano")
|
||||||
|
else:
|
||||||
|
root = bip32.from_mnemonic_cardano(mnemonic_secret.decode(), passphrase)
|
||||||
|
|
||||||
# derive the namespaced root node
|
# derive the namespaced root node
|
||||||
for i in SEED_NAMESPACE:
|
for i in SEED_NAMESPACE:
|
||||||
|
@ -15,16 +15,24 @@ async def confirm(
|
|||||||
confirm_style=Confirm.DEFAULT_CONFIRM_STYLE,
|
confirm_style=Confirm.DEFAULT_CONFIRM_STYLE,
|
||||||
cancel=Confirm.DEFAULT_CANCEL,
|
cancel=Confirm.DEFAULT_CANCEL,
|
||||||
cancel_style=Confirm.DEFAULT_CANCEL_STYLE,
|
cancel_style=Confirm.DEFAULT_CANCEL_STYLE,
|
||||||
|
major_confirm=None,
|
||||||
):
|
):
|
||||||
await ctx.call(ButtonRequest(code=code), MessageType.ButtonAck)
|
await ctx.call(ButtonRequest(code=code), MessageType.ButtonAck)
|
||||||
|
|
||||||
if content.__class__.__name__ == "Paginated":
|
if content.__class__.__name__ == "Paginated":
|
||||||
content.pages[-1] = Confirm(
|
content.pages[-1] = Confirm(
|
||||||
content.pages[-1], confirm, confirm_style, cancel, cancel_style
|
content.pages[-1],
|
||||||
|
confirm,
|
||||||
|
confirm_style,
|
||||||
|
cancel,
|
||||||
|
cancel_style,
|
||||||
|
major_confirm,
|
||||||
)
|
)
|
||||||
dialog = content
|
dialog = content
|
||||||
else:
|
else:
|
||||||
dialog = Confirm(content, confirm, confirm_style, cancel, cancel_style)
|
dialog = Confirm(
|
||||||
|
content, confirm, confirm_style, cancel, cancel_style, major_confirm
|
||||||
|
)
|
||||||
|
|
||||||
if __debug__:
|
if __debug__:
|
||||||
return await ctx.wait(dialog, confirm_signal) is CONFIRMED
|
return await ctx.wait(dialog, confirm_signal) is CONFIRMED
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
from trezor import ui, workflow
|
|
||||||
from trezor.crypto import bip39
|
|
||||||
|
|
||||||
from apps.common import storage
|
|
||||||
|
|
||||||
TYPE_BIP39 = 0
|
|
||||||
|
|
||||||
|
|
||||||
def get() -> (bytes, int):
|
|
||||||
mnemonic_secret = storage.get_mnemonic_secret()
|
|
||||||
mnemonic_type = storage.get_mnemonic_type() or TYPE_BIP39
|
|
||||||
return mnemonic_secret, mnemonic_type
|
|
||||||
|
|
||||||
|
|
||||||
def get_seed(passphrase: str = "", progress_bar=True):
|
|
||||||
secret, mnemonic_type = get()
|
|
||||||
if mnemonic_type == TYPE_BIP39:
|
|
||||||
module = bip39
|
|
||||||
if progress_bar:
|
|
||||||
_start_progress()
|
|
||||||
result = module.seed(secret.decode(), passphrase, _render_progress)
|
|
||||||
_stop_progress()
|
|
||||||
else:
|
|
||||||
result = module.seed(secret.decode(), passphrase)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def process(mnemonics: list, mnemonic_type: int):
|
|
||||||
if mnemonic_type == TYPE_BIP39:
|
|
||||||
return mnemonics[0].encode()
|
|
||||||
else:
|
|
||||||
raise RuntimeError("Unknown mnemonic type")
|
|
||||||
|
|
||||||
|
|
||||||
def restore() -> str:
|
|
||||||
secret, mnemonic_type = get()
|
|
||||||
if mnemonic_type == TYPE_BIP39:
|
|
||||||
return secret.decode()
|
|
||||||
|
|
||||||
|
|
||||||
def _start_progress():
|
|
||||||
ui.backlight_fade(ui.BACKLIGHT_DIM)
|
|
||||||
ui.display.clear()
|
|
||||||
ui.header("Please wait")
|
|
||||||
ui.display.refresh()
|
|
||||||
ui.backlight_fade(ui.BACKLIGHT_NORMAL)
|
|
||||||
|
|
||||||
|
|
||||||
def _render_progress(progress: int, total: int):
|
|
||||||
p = 1000 * progress // total
|
|
||||||
ui.display.loader(p, False, 18, ui.WHITE, ui.BG)
|
|
||||||
ui.display.refresh()
|
|
||||||
|
|
||||||
|
|
||||||
def _stop_progress():
|
|
||||||
workflow.restartdefault()
|
|
59
core/src/apps/common/mnemonic/__init__.py
Normal file
59
core/src/apps/common/mnemonic/__init__.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
from micropython import const
|
||||||
|
|
||||||
|
from trezor import ui, wire
|
||||||
|
from trezor.crypto.hashlib import sha256
|
||||||
|
from trezor.messages.Success import Success
|
||||||
|
from trezor.utils import consteq
|
||||||
|
|
||||||
|
from . import bip39, slip39
|
||||||
|
|
||||||
|
from apps.common import storage
|
||||||
|
|
||||||
|
TYPE_BIP39 = const(0)
|
||||||
|
TYPE_SLIP39 = const(1)
|
||||||
|
|
||||||
|
TYPES_WORD_COUNT = {12: bip39, 18: bip39, 24: bip39, 20: slip39, 33: slip39}
|
||||||
|
|
||||||
|
|
||||||
|
def get() -> (bytes, int):
|
||||||
|
mnemonic_secret = storage.get_mnemonic_secret()
|
||||||
|
mnemonic_type = storage.get_mnemonic_type() or TYPE_BIP39
|
||||||
|
return mnemonic_secret, mnemonic_type
|
||||||
|
|
||||||
|
|
||||||
|
def get_seed(passphrase: str = ""):
|
||||||
|
mnemonic_secret, mnemonic_type = get()
|
||||||
|
if mnemonic_type == TYPE_BIP39:
|
||||||
|
return bip39.get_seed(mnemonic_secret, passphrase)
|
||||||
|
elif mnemonic_type == TYPE_SLIP39:
|
||||||
|
return slip39.get_seed(mnemonic_secret, passphrase)
|
||||||
|
|
||||||
|
|
||||||
|
def dry_run(secret: bytes):
|
||||||
|
digest_input = sha256(secret).digest()
|
||||||
|
stored, _ = get()
|
||||||
|
digest_stored = sha256(stored).digest()
|
||||||
|
if consteq(digest_stored, digest_input):
|
||||||
|
return Success(message="The seed is valid and matches the one in the device")
|
||||||
|
else:
|
||||||
|
raise wire.ProcessError(
|
||||||
|
"The seed is valid but does not match the one in the device"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def module_from_words_count(count: int):
|
||||||
|
return TYPES_WORD_COUNT[count]
|
||||||
|
|
||||||
|
|
||||||
|
def _start_progress():
|
||||||
|
ui.backlight_fade(ui.BACKLIGHT_DIM)
|
||||||
|
ui.display.clear()
|
||||||
|
ui.header("Please wait")
|
||||||
|
ui.display.refresh()
|
||||||
|
ui.backlight_fade(ui.BACKLIGHT_NORMAL)
|
||||||
|
|
||||||
|
|
||||||
|
def _render_progress(progress: int, total: int):
|
||||||
|
p = 1000 * progress // total
|
||||||
|
ui.display.loader(p, False, 18, ui.WHITE, ui.BG)
|
||||||
|
ui.display.refresh()
|
36
core/src/apps/common/mnemonic/bip39.py
Normal file
36
core/src/apps/common/mnemonic/bip39.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from trezor.crypto import bip39
|
||||||
|
|
||||||
|
from apps.common import mnemonic, storage
|
||||||
|
|
||||||
|
|
||||||
|
def get_type():
|
||||||
|
return mnemonic.TYPE_BIP39
|
||||||
|
|
||||||
|
|
||||||
|
def process_single(mnemonic: str) -> bytes:
|
||||||
|
"""
|
||||||
|
Receives single mnemonic and processes it. Returns what is then stored in storage or
|
||||||
|
None if more shares are needed.
|
||||||
|
"""
|
||||||
|
return mnemonic.encode()
|
||||||
|
|
||||||
|
|
||||||
|
def process_all(mnemonics: list) -> bytes:
|
||||||
|
"""
|
||||||
|
Receives all mnemonics (just one in case of BIP-39) and processes it into a master
|
||||||
|
secret which is usually then stored in storage.
|
||||||
|
"""
|
||||||
|
return mnemonics[0].encode()
|
||||||
|
|
||||||
|
|
||||||
|
def store(secret: bytes, needs_backup: bool, no_backup: bool):
|
||||||
|
storage.store_mnemonic(secret, mnemonic.TYPE_BIP39, needs_backup, no_backup)
|
||||||
|
|
||||||
|
|
||||||
|
def get_seed(secret: bytes, passphrase: str):
|
||||||
|
mnemonic._start_progress()
|
||||||
|
return bip39.seed(secret.decode(), passphrase, mnemonic._render_progress)
|
||||||
|
|
||||||
|
|
||||||
|
def check(secret: bytes):
|
||||||
|
return bip39.check(secret)
|
87
core/src/apps/common/mnemonic/slip39.py
Normal file
87
core/src/apps/common/mnemonic/slip39.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
from trezor.crypto import slip39
|
||||||
|
|
||||||
|
from apps.common import mnemonic, storage
|
||||||
|
|
||||||
|
|
||||||
|
def generate_from_secret(master_secret: bytes, count: int, threshold: int) -> str:
|
||||||
|
"""
|
||||||
|
Generates new Shamir backup for 'master_secret'. Multiple groups are not yet supported.
|
||||||
|
"""
|
||||||
|
identifier, group_mnemonics = slip39.generate_single_group_mnemonics_from_data(
|
||||||
|
master_secret, threshold, count
|
||||||
|
)
|
||||||
|
storage.set_slip39_iteration_exponent(slip39.DEFAULT_ITERATION_EXPONENT)
|
||||||
|
storage.set_slip39_identifier(identifier)
|
||||||
|
return group_mnemonics
|
||||||
|
|
||||||
|
|
||||||
|
def get_type():
|
||||||
|
return mnemonic.TYPE_SLIP39
|
||||||
|
|
||||||
|
|
||||||
|
def process_single(mnemonic: str) -> bytes:
|
||||||
|
"""
|
||||||
|
Receives single mnemonic and processes it. Returns what is then stored in storage or
|
||||||
|
None if more shares are needed.
|
||||||
|
"""
|
||||||
|
identifier, iteration_exponent, _, _, _, index, threshold, value = slip39.decode_mnemonic(
|
||||||
|
mnemonic
|
||||||
|
) # TODO: use better data structure for this
|
||||||
|
if threshold == 1:
|
||||||
|
raise ValueError("Threshold equal to 1 is not allowed.")
|
||||||
|
|
||||||
|
# if recovery is not in progress already, start it and wait for more mnemonics
|
||||||
|
if not storage.is_slip39_in_progress():
|
||||||
|
storage.set_slip39_in_progress(True)
|
||||||
|
storage.set_slip39_iteration_exponent(iteration_exponent)
|
||||||
|
storage.set_slip39_identifier(identifier)
|
||||||
|
storage.set_slip39_threshold(threshold)
|
||||||
|
storage.set_slip39_remaining(threshold - 1)
|
||||||
|
storage.set_slip39_words_count(len(mnemonic.split()))
|
||||||
|
storage.set_slip39_mnemonic(index, mnemonic)
|
||||||
|
return None # we need more shares
|
||||||
|
|
||||||
|
# check identifier and member index of this share against stored values
|
||||||
|
if identifier != storage.get_slip39_identifier():
|
||||||
|
# TODO: improve UX (tell user)
|
||||||
|
raise ValueError("Share identifiers do not match")
|
||||||
|
if storage.get_slip39_mnemonic(index):
|
||||||
|
# TODO: improve UX (tell user)
|
||||||
|
raise ValueError("This mnemonic was already entered")
|
||||||
|
|
||||||
|
# append to storage
|
||||||
|
remaining = storage.get_slip39_remaining() - 1
|
||||||
|
storage.set_slip39_remaining(remaining)
|
||||||
|
storage.set_slip39_mnemonic(index, mnemonic)
|
||||||
|
if remaining != 0:
|
||||||
|
return None # we need more shares
|
||||||
|
|
||||||
|
# combine shares and return the master secret
|
||||||
|
mnemonics = storage.get_slip39_mnemonics()
|
||||||
|
if len(mnemonics) != threshold:
|
||||||
|
raise ValueError("Some mnemonics are still missing.")
|
||||||
|
_, _, secret = slip39.combine_mnemonics(mnemonics)
|
||||||
|
return secret
|
||||||
|
|
||||||
|
|
||||||
|
def process_all(mnemonics: list) -> bytes:
|
||||||
|
"""
|
||||||
|
Receives all mnemonics and processes it into pre-master secret which is usually then
|
||||||
|
stored in the storage.
|
||||||
|
"""
|
||||||
|
_, _, secret = slip39.combine_mnemonics(mnemonics)
|
||||||
|
return secret
|
||||||
|
|
||||||
|
|
||||||
|
def store(secret: bytes, needs_backup: bool, no_backup: bool):
|
||||||
|
storage.store_mnemonic(secret, mnemonic.TYPE_SLIP39, needs_backup, no_backup)
|
||||||
|
storage.clear_slip39_data()
|
||||||
|
|
||||||
|
|
||||||
|
def get_seed(encrypted_master_secret: bytes, passphrase: str):
|
||||||
|
mnemonic._start_progress()
|
||||||
|
identifier = storage.get_slip39_identifier()
|
||||||
|
iteration_exponent = storage.get_slip39_iteration_exponent()
|
||||||
|
return slip39.decrypt(
|
||||||
|
identifier, iteration_exponent, encrypted_master_secret, passphrase
|
||||||
|
)
|
@ -2,7 +2,7 @@ from micropython import const
|
|||||||
from ubinascii import hexlify
|
from ubinascii import hexlify
|
||||||
|
|
||||||
from trezor import config
|
from trezor import config
|
||||||
from trezor.crypto import random
|
from trezor.crypto import random, slip39
|
||||||
|
|
||||||
from apps.common import cache
|
from apps.common import cache
|
||||||
|
|
||||||
@ -32,9 +32,100 @@ _AUTOLOCK_DELAY_MS = const(0x0C) # int
|
|||||||
_NO_BACKUP = const(0x0D) # bool (0x01 or empty)
|
_NO_BACKUP = const(0x0D) # bool (0x01 or empty)
|
||||||
_MNEMONIC_TYPE = const(0x0E) # int
|
_MNEMONIC_TYPE = const(0x0E) # int
|
||||||
_ROTATION = const(0x0F) # int
|
_ROTATION = const(0x0F) # int
|
||||||
|
|
||||||
|
_SLIP39 = const(0x02) # SLIP-39 namespace
|
||||||
|
_SLIP39_IN_PROGRESS = const(0x00) # bool
|
||||||
|
_SLIP39_IDENTIFIER = const(0x01) # bytes
|
||||||
|
_SLIP39_THRESHOLD = const(0x02) # int
|
||||||
|
_SLIP39_REMAINING = const(0x03) # int
|
||||||
|
_SLIP39_WORDS_COUNT = const(0x04) # int
|
||||||
|
_SLIP39_ITERATION_EXPONENT = const(0x05) # int
|
||||||
|
|
||||||
|
# Mnemonics stored during SLIP-39 recovery process.
|
||||||
|
# Each mnemonic is stored under key = index.
|
||||||
|
_SLIP39_MNEMONICS = const(0x03) # SLIP-39 mnemonics namespace
|
||||||
|
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
|
def set_slip39_in_progress(val: bool):
|
||||||
|
_set_bool(_SLIP39, _SLIP39_IN_PROGRESS, val)
|
||||||
|
|
||||||
|
|
||||||
|
def is_slip39_in_progress():
|
||||||
|
return _get_bool(_SLIP39, _SLIP39_IN_PROGRESS)
|
||||||
|
|
||||||
|
|
||||||
|
def set_slip39_identifier(identifier: int):
|
||||||
|
_set_uint16(_SLIP39, _SLIP39_IDENTIFIER, identifier)
|
||||||
|
|
||||||
|
|
||||||
|
def get_slip39_identifier() -> int:
|
||||||
|
return _get_uint16(_SLIP39, _SLIP39_IDENTIFIER)
|
||||||
|
|
||||||
|
|
||||||
|
def set_slip39_threshold(threshold: int):
|
||||||
|
_set_uint8(_SLIP39, _SLIP39_THRESHOLD, threshold)
|
||||||
|
|
||||||
|
|
||||||
|
def get_slip39_threshold() -> int:
|
||||||
|
return _get_uint8(_SLIP39, _SLIP39_THRESHOLD)
|
||||||
|
|
||||||
|
|
||||||
|
def set_slip39_remaining(remaining: int):
|
||||||
|
_set_uint8(_SLIP39, _SLIP39_REMAINING, remaining)
|
||||||
|
|
||||||
|
|
||||||
|
def get_slip39_remaining() -> int:
|
||||||
|
return _get_uint8(_SLIP39, _SLIP39_REMAINING)
|
||||||
|
|
||||||
|
|
||||||
|
def set_slip39_words_count(count: int):
|
||||||
|
_set_uint8(_SLIP39, _SLIP39_WORDS_COUNT, count)
|
||||||
|
|
||||||
|
|
||||||
|
def get_slip39_words_count() -> int:
|
||||||
|
return _get_uint8(_SLIP39, _SLIP39_WORDS_COUNT)
|
||||||
|
|
||||||
|
|
||||||
|
def set_slip39_iteration_exponent(exponent: int):
|
||||||
|
# TODO: check if not > 5 bits
|
||||||
|
_set_uint8(_SLIP39, _SLIP39_ITERATION_EXPONENT, exponent)
|
||||||
|
|
||||||
|
|
||||||
|
def get_slip39_iteration_exponent() -> int:
|
||||||
|
return _get_uint8(_SLIP39, _SLIP39_ITERATION_EXPONENT)
|
||||||
|
|
||||||
|
|
||||||
|
def set_slip39_mnemonic(index: int, mnemonic: str):
|
||||||
|
config.set(_SLIP39_MNEMONICS, index, mnemonic.encode())
|
||||||
|
|
||||||
|
|
||||||
|
def get_slip39_mnemonic(index: int) -> str:
|
||||||
|
m = config.get(_SLIP39_MNEMONICS, index)
|
||||||
|
if m:
|
||||||
|
return m.decode()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_slip39_mnemonics() -> list:
|
||||||
|
mnemonics = []
|
||||||
|
for index in range(0, slip39.MAX_SHARE_COUNT):
|
||||||
|
m = get_slip39_mnemonic(index)
|
||||||
|
if m:
|
||||||
|
mnemonics.append(m)
|
||||||
|
return mnemonics
|
||||||
|
|
||||||
|
|
||||||
|
def clear_slip39_data():
|
||||||
|
config.delete(_SLIP39, _SLIP39_IN_PROGRESS)
|
||||||
|
config.delete(_SLIP39, _SLIP39_REMAINING)
|
||||||
|
config.delete(_SLIP39, _SLIP39_THRESHOLD)
|
||||||
|
config.delete(_SLIP39, _SLIP39_WORDS_COUNT)
|
||||||
|
for index in (0, slip39.MAX_SHARE_COUNT):
|
||||||
|
config.delete(_SLIP39_MNEMONICS, index)
|
||||||
|
|
||||||
|
|
||||||
def _set_bool(app: int, key: int, value: bool, public: bool = False) -> None:
|
def _set_bool(app: int, key: int, value: bool, public: bool = False) -> None:
|
||||||
if value:
|
if value:
|
||||||
config.set(app, key, _TRUE_BYTE, public)
|
config.set(app, key, _TRUE_BYTE, public)
|
||||||
@ -57,6 +148,17 @@ def _get_uint8(app: int, key: int) -> int:
|
|||||||
return int.from_bytes(val, "big")
|
return int.from_bytes(val, "big")
|
||||||
|
|
||||||
|
|
||||||
|
def _set_uint16(app: int, key: int, val: int):
|
||||||
|
config.set(app, key, val.to_bytes(2, "big"))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_uint16(app: int, key: int) -> int:
|
||||||
|
val = config.get(app, key)
|
||||||
|
if not val:
|
||||||
|
return None
|
||||||
|
return int.from_bytes(val, "big")
|
||||||
|
|
||||||
|
|
||||||
def _new_device_id() -> str:
|
def _new_device_id() -> str:
|
||||||
return hexlify(random.bytes(12)).decode().upper()
|
return hexlify(random.bytes(12)).decode().upper()
|
||||||
|
|
||||||
@ -77,7 +179,9 @@ def get_rotation() -> int:
|
|||||||
|
|
||||||
|
|
||||||
def is_initialized() -> bool:
|
def is_initialized() -> bool:
|
||||||
return bool(config.get(_APP, _VERSION))
|
return bool(config.get(_APP, _VERSION)) and not bool(
|
||||||
|
config.get(_SLIP39, _SLIP39_IN_PROGRESS)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_label() -> str:
|
def get_label() -> str:
|
||||||
@ -88,10 +192,7 @@ def get_label() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def get_mnemonic_secret() -> bytes:
|
def get_mnemonic_secret() -> bytes:
|
||||||
mnemonic = config.get(_APP, _MNEMONIC_SECRET)
|
return config.get(_APP, _MNEMONIC_SECRET)
|
||||||
if mnemonic is None:
|
|
||||||
return None
|
|
||||||
return mnemonic
|
|
||||||
|
|
||||||
|
|
||||||
def get_mnemonic_type() -> int:
|
def get_mnemonic_type() -> int:
|
||||||
@ -107,10 +208,17 @@ def get_homescreen() -> bytes:
|
|||||||
|
|
||||||
|
|
||||||
def store_mnemonic(
|
def store_mnemonic(
|
||||||
secret: bytes, mnemonic_type: int, needs_backup: bool, no_backup: bool
|
secret: bytes,
|
||||||
|
mnemonic_type: int,
|
||||||
|
needs_backup: bool = False,
|
||||||
|
no_backup: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
config.set(_APP, _MNEMONIC_SECRET, secret)
|
config.set(_APP, _MNEMONIC_SECRET, secret)
|
||||||
_set_uint8(_APP, _MNEMONIC_TYPE, mnemonic_type)
|
_set_uint8(_APP, _MNEMONIC_TYPE, mnemonic_type)
|
||||||
|
_init(needs_backup, no_backup)
|
||||||
|
|
||||||
|
|
||||||
|
def _init(needs_backup=False, no_backup=False):
|
||||||
config.set(_APP, _VERSION, _STORAGE_VERSION)
|
config.set(_APP, _VERSION, _STORAGE_VERSION)
|
||||||
_set_bool(_APP, _NO_BACKUP, no_backup)
|
_set_bool(_APP, _NO_BACKUP, no_backup)
|
||||||
if not no_backup:
|
if not no_backup:
|
||||||
|
@ -16,9 +16,11 @@ async def homescreen():
|
|||||||
|
|
||||||
|
|
||||||
def display_homescreen():
|
def display_homescreen():
|
||||||
if not storage.is_initialized():
|
image = None
|
||||||
|
if storage.is_slip39_in_progress():
|
||||||
|
label = "Waiting for other shares"
|
||||||
|
elif not storage.is_initialized():
|
||||||
label = "Go to trezor.io/start"
|
label = "Go to trezor.io/start"
|
||||||
image = None
|
|
||||||
else:
|
else:
|
||||||
label = storage.get_label() or "My Trezor"
|
label = storage.get_label() or "My Trezor"
|
||||||
image = storage.get_homescreen()
|
image = storage.get_homescreen()
|
||||||
@ -27,28 +29,28 @@ def display_homescreen():
|
|||||||
image = res.load("apps/homescreen/res/bg.toif")
|
image = res.load("apps/homescreen/res/bg.toif")
|
||||||
|
|
||||||
if storage.is_initialized() and storage.no_backup():
|
if storage.is_initialized() and storage.no_backup():
|
||||||
ui.display.bar(0, 0, ui.WIDTH, 30, ui.RED)
|
_err("SEEDLESS")
|
||||||
ui.display.text_center(ui.WIDTH // 2, 22, "SEEDLESS", ui.BOLD, ui.WHITE, ui.RED)
|
|
||||||
ui.display.bar(0, 30, ui.WIDTH, ui.HEIGHT - 30, ui.BG)
|
|
||||||
elif storage.is_initialized() and storage.unfinished_backup():
|
elif storage.is_initialized() and storage.unfinished_backup():
|
||||||
ui.display.bar(0, 0, ui.WIDTH, 30, ui.RED)
|
_err("BACKUP FAILED!")
|
||||||
ui.display.text_center(
|
|
||||||
ui.WIDTH // 2, 22, "BACKUP FAILED!", ui.BOLD, ui.WHITE, ui.RED
|
|
||||||
)
|
|
||||||
ui.display.bar(0, 30, ui.WIDTH, ui.HEIGHT - 30, ui.BG)
|
|
||||||
elif storage.is_initialized() and storage.needs_backup():
|
elif storage.is_initialized() and storage.needs_backup():
|
||||||
ui.display.bar(0, 0, ui.WIDTH, 30, ui.YELLOW)
|
_warn("NEEDS BACKUP!")
|
||||||
ui.display.text_center(
|
|
||||||
ui.WIDTH // 2, 22, "NEEDS BACKUP!", ui.BOLD, ui.BLACK, ui.YELLOW
|
|
||||||
)
|
|
||||||
ui.display.bar(0, 30, ui.WIDTH, ui.HEIGHT - 30, ui.BG)
|
|
||||||
elif storage.is_initialized() and not config.has_pin():
|
elif storage.is_initialized() and not config.has_pin():
|
||||||
ui.display.bar(0, 0, ui.WIDTH, 30, ui.YELLOW)
|
_warn("PIN NOT SET!")
|
||||||
ui.display.text_center(
|
elif storage.is_slip39_in_progress():
|
||||||
ui.WIDTH // 2, 22, "PIN NOT SET!", ui.BOLD, ui.BLACK, ui.YELLOW
|
_warn("SHAMIR IN PROGRESS!")
|
||||||
)
|
|
||||||
ui.display.bar(0, 30, ui.WIDTH, ui.HEIGHT - 30, ui.BG)
|
|
||||||
else:
|
else:
|
||||||
ui.display.bar(0, 0, ui.WIDTH, ui.HEIGHT, ui.BG)
|
ui.display.bar(0, 0, ui.WIDTH, ui.HEIGHT, ui.BG)
|
||||||
ui.display.avatar(48, 48 - 10, image, ui.WHITE, ui.BLACK)
|
ui.display.avatar(48, 48 - 10, image, ui.WHITE, ui.BLACK)
|
||||||
ui.display.text_center(ui.WIDTH // 2, 220, label, ui.BOLD, ui.FG, ui.BG)
|
ui.display.text_center(ui.WIDTH // 2, 220, label, ui.BOLD, ui.FG, ui.BG)
|
||||||
|
|
||||||
|
|
||||||
|
def _warn(message: str):
|
||||||
|
ui.display.bar(0, 0, ui.WIDTH, 30, ui.YELLOW)
|
||||||
|
ui.display.text_center(ui.WIDTH // 2, 22, message, ui.BOLD, ui.BLACK, ui.YELLOW)
|
||||||
|
ui.display.bar(0, 30, ui.WIDTH, ui.HEIGHT - 30, ui.BG)
|
||||||
|
|
||||||
|
|
||||||
|
def _err(message: str):
|
||||||
|
ui.display.bar(0, 0, ui.WIDTH, 30, ui.RED)
|
||||||
|
ui.display.text_center(ui.WIDTH // 2, 22, message, ui.BOLD, ui.WHITE, ui.RED)
|
||||||
|
ui.display.bar(0, 30, ui.WIDTH, ui.HEIGHT - 30, ui.BG)
|
||||||
|
@ -2,12 +2,8 @@ from trezor import wire
|
|||||||
from trezor.messages.Success import Success
|
from trezor.messages.Success import Success
|
||||||
|
|
||||||
from apps.common import mnemonic, storage
|
from apps.common import mnemonic, storage
|
||||||
from apps.management.reset_device import (
|
from apps.management.common import layout
|
||||||
check_mnemonic,
|
from apps.management.reset_device import backup_slip39_wallet
|
||||||
show_backup_warning,
|
|
||||||
show_mnemonic,
|
|
||||||
show_wrong_entry,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def backup_device(ctx, msg):
|
async def backup_device(ctx, msg):
|
||||||
@ -16,20 +12,18 @@ async def backup_device(ctx, msg):
|
|||||||
if not storage.needs_backup():
|
if not storage.needs_backup():
|
||||||
raise wire.ProcessError("Seed already backed up")
|
raise wire.ProcessError("Seed already backed up")
|
||||||
|
|
||||||
words = mnemonic.restore()
|
|
||||||
|
|
||||||
# warn user about mnemonic safety
|
# warn user about mnemonic safety
|
||||||
await show_backup_warning(ctx)
|
await layout.bip39_show_backup_warning(ctx)
|
||||||
|
|
||||||
storage.set_unfinished_backup(True)
|
storage.set_unfinished_backup(True)
|
||||||
storage.set_backed_up()
|
storage.set_backed_up()
|
||||||
|
|
||||||
while True:
|
mnemonic_secret, mnemonic_type = mnemonic.get()
|
||||||
# show mnemonic and require confirmation of a random word
|
if mnemonic_type == mnemonic.TYPE_BIP39:
|
||||||
await show_mnemonic(ctx, words)
|
await layout.bip39_show_and_confirm_mnemonic(ctx, mnemonic_secret.decode())
|
||||||
if await check_mnemonic(ctx, words):
|
|
||||||
break
|
elif mnemonic_type == mnemonic.TYPE_SLIP39:
|
||||||
await show_wrong_entry(ctx)
|
await backup_slip39_wallet(ctx, mnemonic_secret)
|
||||||
|
|
||||||
storage.set_unfinished_backup(False)
|
storage.set_unfinished_backup(False)
|
||||||
|
|
||||||
|
519
core/src/apps/management/common/layout.py
Normal file
519
core/src/apps/management/common/layout.py
Normal file
@ -0,0 +1,519 @@
|
|||||||
|
import ubinascii
|
||||||
|
from micropython import const
|
||||||
|
|
||||||
|
from trezor import ui, utils
|
||||||
|
from trezor.crypto import random
|
||||||
|
from trezor.messages import ButtonRequestType
|
||||||
|
from trezor.ui.button import Button, ButtonDefault
|
||||||
|
from trezor.ui.checklist import Checklist
|
||||||
|
from trezor.ui.info import InfoConfirm
|
||||||
|
from trezor.ui.loader import LoadingAnimation
|
||||||
|
from trezor.ui.scroll import Paginated
|
||||||
|
from trezor.ui.shamir import NumInput
|
||||||
|
from trezor.ui.text import Text
|
||||||
|
|
||||||
|
from apps.common.confirm import confirm, hold_to_confirm, require_confirm
|
||||||
|
|
||||||
|
if __debug__:
|
||||||
|
from apps import debug
|
||||||
|
|
||||||
|
|
||||||
|
async def show_reset_device_warning(ctx, use_slip39: bool):
|
||||||
|
text = Text("Create new wallet", ui.ICON_RESET, new_lines=False)
|
||||||
|
text.bold("Do you want to create")
|
||||||
|
text.br()
|
||||||
|
if use_slip39:
|
||||||
|
text.bold("a new Shamir wallet?")
|
||||||
|
else:
|
||||||
|
text.bold("a new wallet?")
|
||||||
|
text.br()
|
||||||
|
text.br_half()
|
||||||
|
text.normal("By continuing you agree")
|
||||||
|
text.br()
|
||||||
|
text.normal("to")
|
||||||
|
text.bold("https://trezor.io/tos")
|
||||||
|
await require_confirm(ctx, text, ButtonRequestType.ResetDevice, major_confirm=True)
|
||||||
|
await LoadingAnimation()
|
||||||
|
|
||||||
|
|
||||||
|
async def show_internal_entropy(ctx, entropy: bytes):
|
||||||
|
entropy_str = ubinascii.hexlify(entropy).decode()
|
||||||
|
lines = utils.chunks(entropy_str, 16)
|
||||||
|
text = Text("Internal entropy", ui.ICON_RESET)
|
||||||
|
text.mono(*lines)
|
||||||
|
await require_confirm(ctx, text, ButtonRequestType.ResetDevice)
|
||||||
|
|
||||||
|
|
||||||
|
async def show_backup_success(ctx):
|
||||||
|
text = Text("Backup is done!", ui.ICON_CONFIRM, ui.GREEN)
|
||||||
|
text.normal(
|
||||||
|
"Never make a digital",
|
||||||
|
"copy of your recovery",
|
||||||
|
"seed and never upload",
|
||||||
|
"it online!",
|
||||||
|
)
|
||||||
|
await require_confirm(
|
||||||
|
ctx, text, ButtonRequestType.ResetDevice, confirm="Finish setup", cancel=None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def confirm_backup(ctx):
|
||||||
|
text = Text("Backup wallet", ui.ICON_RESET, new_lines=False)
|
||||||
|
text.bold("New wallet created")
|
||||||
|
text.br()
|
||||||
|
text.bold("successfully!")
|
||||||
|
text.br()
|
||||||
|
text.br_half()
|
||||||
|
text.normal("You should back your")
|
||||||
|
text.br()
|
||||||
|
text.normal("new wallet right now.")
|
||||||
|
return await confirm(
|
||||||
|
ctx,
|
||||||
|
text,
|
||||||
|
ButtonRequestType.ResetDevice,
|
||||||
|
cancel="Skip",
|
||||||
|
confirm="Backup",
|
||||||
|
major_confirm=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def confirm_backup_again(ctx):
|
||||||
|
text = Text("Backup wallet", ui.ICON_RESET, new_lines=False)
|
||||||
|
text.bold("Are you sure you want")
|
||||||
|
text.br()
|
||||||
|
text.bold("to skip the backup?")
|
||||||
|
text.br()
|
||||||
|
text.br_half()
|
||||||
|
text.normal("You can backup Trezor")
|
||||||
|
text.br()
|
||||||
|
text.normal("anytime later.")
|
||||||
|
return await confirm(
|
||||||
|
ctx,
|
||||||
|
text,
|
||||||
|
ButtonRequestType.ResetDevice,
|
||||||
|
cancel="Skip",
|
||||||
|
confirm="Backup",
|
||||||
|
major_confirm=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _confirm_share_words(ctx, share_index, share_words):
|
||||||
|
numbered = list(enumerate(share_words))
|
||||||
|
|
||||||
|
# check a word from the first half
|
||||||
|
first_half = numbered[: len(numbered) // 2]
|
||||||
|
if not await _confirm_word(ctx, share_index, first_half):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# check a word from the second half
|
||||||
|
second_half = numbered[len(numbered) // 2 :]
|
||||||
|
if not await _confirm_word(ctx, share_index, second_half):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def _confirm_word(ctx, share_index, numbered_share_words):
|
||||||
|
# TODO: duplicated words in the choice list
|
||||||
|
|
||||||
|
# shuffle the numbered seed half, slice off the choices we need
|
||||||
|
random.shuffle(numbered_share_words)
|
||||||
|
numbered_choices = numbered_share_words[: MnemonicWordSelect.NUM_OF_CHOICES]
|
||||||
|
|
||||||
|
# we always confirm the first (random) word index
|
||||||
|
checked_index, checked_word = numbered_choices[0]
|
||||||
|
if __debug__:
|
||||||
|
debug.reset_word_index = checked_index
|
||||||
|
|
||||||
|
# shuffle again so the confirmed word is not always the first choice
|
||||||
|
random.shuffle(numbered_choices)
|
||||||
|
|
||||||
|
# let the user pick a word
|
||||||
|
choices = [word for _, word in numbered_choices]
|
||||||
|
select = MnemonicWordSelect(choices, share_index, checked_index)
|
||||||
|
if __debug__:
|
||||||
|
selected_word = await ctx.wait(select, debug.input_signal)
|
||||||
|
else:
|
||||||
|
selected_word = await ctx.wait(select)
|
||||||
|
|
||||||
|
# confirm it is the correct one
|
||||||
|
return selected_word == checked_word
|
||||||
|
|
||||||
|
|
||||||
|
async def _show_confirmation_success(ctx, share_index):
|
||||||
|
if share_index is None:
|
||||||
|
text = Text("Recovery seed", ui.ICON_RESET)
|
||||||
|
text.bold("Recovery seed")
|
||||||
|
text.bold("checked successfully.")
|
||||||
|
else:
|
||||||
|
text = Text("Recovery share #%s" % (share_index + 1), ui.ICON_RESET)
|
||||||
|
text.bold("Seed share #%s" % (share_index + 1))
|
||||||
|
text.bold("checked successfully.")
|
||||||
|
text.normal("Let's continue with")
|
||||||
|
text.normal("share #%s." % (share_index + 2))
|
||||||
|
return await confirm(
|
||||||
|
ctx, text, ButtonRequestType.ResetDevice, cancel=None, confirm="Continue"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _show_confirmation_failure(ctx, share_index):
|
||||||
|
if share_index is None:
|
||||||
|
text = Text("Recovery seed", ui.ICON_WRONG, ui.RED)
|
||||||
|
else:
|
||||||
|
text = Text("Recovery share #%s" % (share_index + 1), ui.ICON_WRONG, ui.RED)
|
||||||
|
text.bold("You have entered")
|
||||||
|
text.bold("wrong seed word.")
|
||||||
|
text.bold("Please check again.")
|
||||||
|
await require_confirm(
|
||||||
|
ctx, text, ButtonRequestType.ResetDevice, confirm="Check again", cancel=None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# BIP39
|
||||||
|
# ===
|
||||||
|
|
||||||
|
|
||||||
|
async def bip39_show_and_confirm_mnemonic(ctx, mnemonic: str):
|
||||||
|
words = mnemonic.split()
|
||||||
|
|
||||||
|
# require confirmation of the mnemonic safety
|
||||||
|
await bip39_show_backup_warning(ctx)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# display paginated mnemonic on the screen
|
||||||
|
await _bip39_show_mnemonic(ctx, words)
|
||||||
|
|
||||||
|
# make the user confirm 2 words from the mnemonic
|
||||||
|
if await _confirm_share_words(ctx, None, words):
|
||||||
|
await _show_confirmation_success(ctx, None)
|
||||||
|
break # this share is confirmed, go to next one
|
||||||
|
else:
|
||||||
|
await _show_confirmation_failure(ctx, None)
|
||||||
|
|
||||||
|
|
||||||
|
async def bip39_show_backup_warning(ctx):
|
||||||
|
text = Text("Backup your seed", ui.ICON_NOCOPY)
|
||||||
|
text.normal(
|
||||||
|
"Never make a digital",
|
||||||
|
"copy of your recovery",
|
||||||
|
"seed and never upload",
|
||||||
|
"it online!",
|
||||||
|
)
|
||||||
|
await require_confirm(
|
||||||
|
ctx, text, ButtonRequestType.ResetDevice, confirm="I understand", cancel=None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _bip39_show_mnemonic(ctx, words: list):
|
||||||
|
# split mnemonic words into pages
|
||||||
|
PER_PAGE = const(4)
|
||||||
|
words = list(enumerate(words))
|
||||||
|
words = list(utils.chunks(words, PER_PAGE))
|
||||||
|
|
||||||
|
# display the pages, with a confirmation dialog on the last one
|
||||||
|
pages = [_get_mnemonic_page(page) for page in words]
|
||||||
|
paginated = Paginated(pages)
|
||||||
|
|
||||||
|
if __debug__:
|
||||||
|
|
||||||
|
def export_displayed_words():
|
||||||
|
# export currently displayed mnemonic words into debuglink
|
||||||
|
debug.reset_current_words = [w for _, w in words[paginated.page]]
|
||||||
|
|
||||||
|
paginated.on_change = export_displayed_words
|
||||||
|
export_displayed_words()
|
||||||
|
|
||||||
|
await hold_to_confirm(ctx, paginated, ButtonRequestType.ResetDevice)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_mnemonic_page(words: list):
|
||||||
|
text = Text("Recovery seed", ui.ICON_RESET)
|
||||||
|
for index, word in words:
|
||||||
|
text.mono("%2d. %s" % (index + 1, word))
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
# SLIP39
|
||||||
|
# ===
|
||||||
|
|
||||||
|
# TODO: yellow cancel style?
|
||||||
|
# TODO: loading animation style?
|
||||||
|
# TODO: smaller font or tighter rows to fit more text in
|
||||||
|
# TODO: icons in checklist
|
||||||
|
|
||||||
|
|
||||||
|
async def slip39_show_checklist_set_shares(ctx):
|
||||||
|
checklist = Checklist("Backup checklist", ui.ICON_RESET)
|
||||||
|
checklist.add("Set number of shares")
|
||||||
|
checklist.add("Set the threshold")
|
||||||
|
checklist.add(("Write down and check", "all seed shares"))
|
||||||
|
checklist.select(0)
|
||||||
|
checklist.process()
|
||||||
|
return await confirm(
|
||||||
|
ctx, checklist, ButtonRequestType.ResetDevice, cancel=None, confirm="Set shares"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def slip39_show_checklist_set_threshold(ctx, num_of_shares):
|
||||||
|
checklist = Checklist("Backup checklist", ui.ICON_RESET)
|
||||||
|
checklist.add("Set number of shares")
|
||||||
|
checklist.add("Set the threshold")
|
||||||
|
checklist.add(("Write down and check", "all seed shares"))
|
||||||
|
checklist.select(1)
|
||||||
|
checklist.process()
|
||||||
|
return await confirm(
|
||||||
|
ctx,
|
||||||
|
checklist,
|
||||||
|
ButtonRequestType.ResetDevice,
|
||||||
|
cancel=None,
|
||||||
|
confirm="Set threshold",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def slip39_show_checklist_show_shares(ctx, num_of_shares, threshold):
|
||||||
|
checklist = Checklist("Backup checklist", ui.ICON_RESET)
|
||||||
|
checklist.add("Set number of shares")
|
||||||
|
checklist.add("Set the threshold")
|
||||||
|
checklist.add(("Write down and check", "all seed shares"))
|
||||||
|
checklist.select(2)
|
||||||
|
checklist.process()
|
||||||
|
return await confirm(
|
||||||
|
ctx,
|
||||||
|
checklist,
|
||||||
|
ButtonRequestType.ResetDevice,
|
||||||
|
cancel=None,
|
||||||
|
confirm="Show seed shares",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def slip39_prompt_number_of_shares(ctx):
|
||||||
|
count = 5
|
||||||
|
min_count = 2
|
||||||
|
max_count = 16
|
||||||
|
|
||||||
|
while True:
|
||||||
|
shares = ShamirNumInput(ShamirNumInput.SET_SHARES, count, min_count, max_count)
|
||||||
|
info = InfoConfirm(
|
||||||
|
"Shares are parts of "
|
||||||
|
"the recovery seed, "
|
||||||
|
"each containing 20 "
|
||||||
|
"words. You can later set "
|
||||||
|
"how many shares you "
|
||||||
|
"need to recover your "
|
||||||
|
"wallet."
|
||||||
|
)
|
||||||
|
confirmed = await confirm(
|
||||||
|
ctx,
|
||||||
|
shares,
|
||||||
|
ButtonRequestType.ResetDevice,
|
||||||
|
cancel="Info",
|
||||||
|
confirm="Set",
|
||||||
|
major_confirm=True,
|
||||||
|
cancel_style=ButtonDefault,
|
||||||
|
)
|
||||||
|
count = shares.input.count
|
||||||
|
if confirmed:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
await info
|
||||||
|
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
async def slip39_prompt_threshold(ctx, num_of_shares):
|
||||||
|
count = num_of_shares // 2
|
||||||
|
min_count = 2
|
||||||
|
max_count = num_of_shares
|
||||||
|
|
||||||
|
while True:
|
||||||
|
shares = ShamirNumInput(
|
||||||
|
ShamirNumInput.SET_THRESHOLD, count, min_count, max_count
|
||||||
|
)
|
||||||
|
info = InfoConfirm(
|
||||||
|
"Threshold sets number "
|
||||||
|
"shares that you need "
|
||||||
|
"to recover your wallet. "
|
||||||
|
"i.e. Set it to %s and "
|
||||||
|
"you'll need any %s shares "
|
||||||
|
"of the total number." % (count, count)
|
||||||
|
)
|
||||||
|
confirmed = await confirm(
|
||||||
|
ctx,
|
||||||
|
shares,
|
||||||
|
ButtonRequestType.ResetDevice,
|
||||||
|
cancel="Info",
|
||||||
|
confirm="Set",
|
||||||
|
major_confirm=True,
|
||||||
|
cancel_style=ButtonDefault,
|
||||||
|
)
|
||||||
|
count = shares.input.count
|
||||||
|
if confirmed:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
await info
|
||||||
|
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
async def slip39_show_and_confirm_shares(ctx, shares):
|
||||||
|
for index, share in enumerate(shares):
|
||||||
|
share_words = share.split(" ")
|
||||||
|
while True:
|
||||||
|
# display paginated share on the screen
|
||||||
|
await _slip39_show_share_words(ctx, index, share_words)
|
||||||
|
|
||||||
|
# make the user confirm 2 words from the share
|
||||||
|
if await _confirm_share_words(ctx, index, share_words):
|
||||||
|
await _show_confirmation_success(ctx, index)
|
||||||
|
break # this share is confirmed, go to next one
|
||||||
|
else:
|
||||||
|
await _show_confirmation_failure(ctx, index)
|
||||||
|
|
||||||
|
|
||||||
|
async def _slip39_show_share_words(ctx, share_index, share_words):
|
||||||
|
first, chunks, last = _slip39_split_share_into_pages(share_words)
|
||||||
|
|
||||||
|
if share_index is None:
|
||||||
|
header_title = "Recovery share #%s" % (share_index + 1)
|
||||||
|
else:
|
||||||
|
header_title = "Recovery seed"
|
||||||
|
header_icon = ui.ICON_RESET
|
||||||
|
pages = [] # ui page components
|
||||||
|
shares_words_check = [] # check we display correct data
|
||||||
|
|
||||||
|
# first page
|
||||||
|
text = Text(header_title, header_icon)
|
||||||
|
text.normal("Write down %s words" % len(share_words))
|
||||||
|
text.normal("onto paper booklet:")
|
||||||
|
text.br_half()
|
||||||
|
for index, word in first:
|
||||||
|
text.mono("%s. %s" % (index + 1, word))
|
||||||
|
shares_words_check.append(word)
|
||||||
|
pages.append(text)
|
||||||
|
|
||||||
|
# middle pages
|
||||||
|
for chunk in chunks:
|
||||||
|
text = Text(header_title, header_icon)
|
||||||
|
text.br_half()
|
||||||
|
for index, word in chunk:
|
||||||
|
text.mono("%s. %s" % (index + 1, word))
|
||||||
|
shares_words_check.append(word)
|
||||||
|
pages.append(text)
|
||||||
|
|
||||||
|
# last page
|
||||||
|
text = Text(header_title, header_icon)
|
||||||
|
for index, word in last:
|
||||||
|
text.mono("%s. %s" % (index + 1, word))
|
||||||
|
shares_words_check.append(word)
|
||||||
|
text.br_half()
|
||||||
|
text.normal("I confirm that I wrote")
|
||||||
|
text.normal("down all %s words." % len(share_words))
|
||||||
|
pages.append(text)
|
||||||
|
|
||||||
|
# pagination
|
||||||
|
paginated = Paginated(pages)
|
||||||
|
|
||||||
|
if __debug__:
|
||||||
|
|
||||||
|
word_pages = [first] + chunks + [last]
|
||||||
|
|
||||||
|
def export_displayed_words():
|
||||||
|
# export currently displayed mnemonic words into debuglink
|
||||||
|
debug.reset_current_words = word_pages[paginated.page]
|
||||||
|
|
||||||
|
paginated.on_change = export_displayed_words
|
||||||
|
export_displayed_words()
|
||||||
|
|
||||||
|
# make sure we display correct data
|
||||||
|
utils.ensure(share_words == shares_words_check)
|
||||||
|
|
||||||
|
# confirm the share
|
||||||
|
await hold_to_confirm(ctx, paginated) # TODO: customize the loader here
|
||||||
|
|
||||||
|
|
||||||
|
def _slip39_split_share_into_pages(share_words):
|
||||||
|
share = list(enumerate(share_words)) # we need to keep track of the word indices
|
||||||
|
first = share[:2] # two words on the first page
|
||||||
|
middle = share[2:-2]
|
||||||
|
last = share[-2:] # two words on the last page
|
||||||
|
chunks = utils.chunks(middle, 4) # 4 words on the middle pages
|
||||||
|
return first, list(chunks), last
|
||||||
|
|
||||||
|
|
||||||
|
class ShamirNumInput(ui.Control):
|
||||||
|
SET_SHARES = object()
|
||||||
|
SET_THRESHOLD = object()
|
||||||
|
|
||||||
|
def __init__(self, step, count, min_count, max_count):
|
||||||
|
self.step = step
|
||||||
|
self.input = NumInput(count, min_count=min_count, max_count=max_count)
|
||||||
|
self.input.on_change = self.on_change
|
||||||
|
self.repaint = True
|
||||||
|
|
||||||
|
def dispatch(self, event, x, y):
|
||||||
|
self.input.dispatch(event, x, y)
|
||||||
|
if event is ui.RENDER:
|
||||||
|
self.on_render()
|
||||||
|
|
||||||
|
def on_render(self):
|
||||||
|
if self.repaint:
|
||||||
|
count = self.input.count
|
||||||
|
|
||||||
|
# render the headline
|
||||||
|
if self.step is ShamirNumInput.SET_SHARES:
|
||||||
|
header = "Set num. of shares"
|
||||||
|
elif self.step is ShamirNumInput.SET_THRESHOLD:
|
||||||
|
header = "Set the threshold"
|
||||||
|
ui.header(header, ui.ICON_RESET, ui.TITLE_GREY, ui.BG, ui.ORANGE_ICON)
|
||||||
|
|
||||||
|
# render the counter
|
||||||
|
if self.step is ShamirNumInput.SET_SHARES:
|
||||||
|
ui.display.text(
|
||||||
|
12, 130, "%s people or locations" % count, ui.BOLD, ui.FG, ui.BG
|
||||||
|
)
|
||||||
|
ui.display.text(
|
||||||
|
12, 156, "will each host one share.", ui.NORMAL, ui.FG, ui.BG
|
||||||
|
)
|
||||||
|
elif self.step is ShamirNumInput.SET_THRESHOLD:
|
||||||
|
ui.display.text(
|
||||||
|
12, 130, "For recovery you'll need", ui.NORMAL, ui.FG, ui.BG
|
||||||
|
)
|
||||||
|
ui.display.text(
|
||||||
|
12, 156, "any %s of shares." % count, ui.BOLD, ui.FG, ui.BG
|
||||||
|
)
|
||||||
|
|
||||||
|
self.repaint = False
|
||||||
|
|
||||||
|
def on_change(self, count):
|
||||||
|
self.repaint = True
|
||||||
|
|
||||||
|
|
||||||
|
class MnemonicWordSelect(ui.Layout):
|
||||||
|
NUM_OF_CHOICES = 3
|
||||||
|
|
||||||
|
def __init__(self, words, share_index, word_index):
|
||||||
|
self.words = words
|
||||||
|
self.share_index = share_index
|
||||||
|
self.word_index = word_index
|
||||||
|
self.buttons = []
|
||||||
|
for i, word in enumerate(words):
|
||||||
|
area = ui.grid(i + 2, n_x=1)
|
||||||
|
btn = Button(area, word)
|
||||||
|
btn.on_click = self.select(word)
|
||||||
|
self.buttons.append(btn)
|
||||||
|
if share_index is None:
|
||||||
|
self.text = Text("Recovery seed")
|
||||||
|
else:
|
||||||
|
self.text = Text("Recovery share #%s" % (share_index + 1))
|
||||||
|
self.text.normal("Choose the %s word:" % utils.format_ordinal(word_index + 1))
|
||||||
|
|
||||||
|
def dispatch(self, event, x, y):
|
||||||
|
for btn in self.buttons:
|
||||||
|
btn.dispatch(event, x, y)
|
||||||
|
self.text.dispatch(event, x, y)
|
||||||
|
|
||||||
|
def select(self, word):
|
||||||
|
def fn():
|
||||||
|
raise ui.Result(word)
|
||||||
|
|
||||||
|
return fn
|
@ -1,15 +1,15 @@
|
|||||||
from trezor import config, wire
|
from trezor import config, wire
|
||||||
from trezor.crypto import bip39
|
|
||||||
from trezor.messages.Success import Success
|
from trezor.messages.Success import Success
|
||||||
from trezor.pin import pin_to_int
|
from trezor.pin import pin_to_int
|
||||||
from trezor.ui.text import Text
|
from trezor.ui.text import Text
|
||||||
|
|
||||||
from apps.common import mnemonic, storage
|
from apps.common import storage
|
||||||
from apps.common.confirm import require_confirm
|
from apps.common.confirm import require_confirm
|
||||||
|
from apps.common.mnemonic import bip39
|
||||||
|
|
||||||
|
|
||||||
async def load_device(ctx, msg):
|
async def load_device(ctx, msg):
|
||||||
|
# TODO implement SLIP-39
|
||||||
if storage.is_initialized():
|
if storage.is_initialized():
|
||||||
raise wire.UnexpectedMessage("Already initialized")
|
raise wire.UnexpectedMessage("Already initialized")
|
||||||
|
|
||||||
@ -24,10 +24,10 @@ async def load_device(ctx, msg):
|
|||||||
text.normal("Continue only if you", "know what you are doing!")
|
text.normal("Continue only if you", "know what you are doing!")
|
||||||
await require_confirm(ctx, text)
|
await require_confirm(ctx, text)
|
||||||
|
|
||||||
secret = mnemonic.process([msg.mnemonic], mnemonic.TYPE_BIP39)
|
secret = bip39.process_all([msg.mnemonic])
|
||||||
storage.store_mnemonic(
|
storage.store_mnemonic(
|
||||||
secret=secret,
|
secret=secret,
|
||||||
mnemonic_type=mnemonic.TYPE_BIP39,
|
mnemonic_type=bip39.get_type(),
|
||||||
needs_backup=True,
|
needs_backup=True,
|
||||||
no_backup=False,
|
no_backup=False,
|
||||||
)
|
)
|
||||||
|
@ -1,31 +1,28 @@
|
|||||||
from trezor import config, ui, wire
|
from trezor import config, ui, wire
|
||||||
from trezor.crypto import bip39
|
from trezor.messages import ButtonRequestType
|
||||||
from trezor.crypto.hashlib import sha256
|
|
||||||
from trezor.messages.ButtonRequest import ButtonRequest
|
from trezor.messages.ButtonRequest import ButtonRequest
|
||||||
from trezor.messages.ButtonRequestType import (
|
|
||||||
MnemonicInput,
|
|
||||||
MnemonicWordCount,
|
|
||||||
ProtectCall,
|
|
||||||
)
|
|
||||||
from trezor.messages.MessageType import ButtonAck
|
from trezor.messages.MessageType import ButtonAck
|
||||||
from trezor.messages.Success import Success
|
from trezor.messages.Success import Success
|
||||||
from trezor.pin import pin_to_int
|
from trezor.pin import pin_to_int
|
||||||
from trezor.ui.mnemonic import MnemonicKeyboard
|
from trezor.ui.info import InfoConfirm
|
||||||
|
from trezor.ui.mnemonic_bip39 import Bip39Keyboard
|
||||||
|
from trezor.ui.mnemonic_slip39 import Slip39Keyboard
|
||||||
from trezor.ui.text import Text
|
from trezor.ui.text import Text
|
||||||
from trezor.ui.word_select import WordSelector
|
from trezor.ui.word_select import WordSelector
|
||||||
from trezor.utils import consteq, format_ordinal
|
from trezor.utils import format_ordinal
|
||||||
|
|
||||||
from apps.common import mnemonic, storage
|
from apps.common import mnemonic, storage
|
||||||
from apps.common.confirm import require_confirm
|
from apps.common.confirm import require_confirm
|
||||||
|
from apps.homescreen.homescreen import display_homescreen
|
||||||
from apps.management.change_pin import request_pin_ack, request_pin_confirm
|
from apps.management.change_pin import request_pin_ack, request_pin_confirm
|
||||||
|
|
||||||
if __debug__:
|
if __debug__:
|
||||||
from apps.debug import input_signal
|
from apps.debug import confirm_signal, input_signal
|
||||||
|
|
||||||
|
|
||||||
async def recovery_device(ctx, msg):
|
async def recovery_device(ctx, msg):
|
||||||
"""
|
"""
|
||||||
Recover BIP39 seed into empty device.
|
Recover BIP39/SLIP39 seed into empty device.
|
||||||
|
|
||||||
1. Ask for the number of words in recovered seed.
|
1. Ask for the number of words in recovered seed.
|
||||||
2. Let user type in the mnemonic words one by one.
|
2. Let user type in the mnemonic words one by one.
|
||||||
@ -36,34 +33,53 @@ async def recovery_device(ctx, msg):
|
|||||||
if not msg.dry_run and storage.is_initialized():
|
if not msg.dry_run and storage.is_initialized():
|
||||||
raise wire.UnexpectedMessage("Already initialized")
|
raise wire.UnexpectedMessage("Already initialized")
|
||||||
|
|
||||||
if not msg.dry_run:
|
if not storage.is_slip39_in_progress():
|
||||||
title = "Device recovery"
|
if not msg.dry_run:
|
||||||
text = Text(title, ui.ICON_RECOVERY)
|
title = "Device recovery"
|
||||||
text.normal("Do you really want to", "recover the device?", "")
|
text = Text(title, ui.ICON_RECOVERY)
|
||||||
else:
|
text.normal("Do you really want to", "recover the device?", "")
|
||||||
title = "Simulated recovery"
|
|
||||||
text = Text(title, ui.ICON_RECOVERY)
|
|
||||||
text.normal("Do you really want to", "check the recovery", "seed?")
|
|
||||||
|
|
||||||
await require_confirm(ctx, text, code=ProtectCall)
|
|
||||||
|
|
||||||
if msg.dry_run:
|
|
||||||
if config.has_pin():
|
|
||||||
curpin = await request_pin_ack(ctx, "Enter PIN", config.get_pin_rem())
|
|
||||||
else:
|
else:
|
||||||
curpin = ""
|
title = "Simulated recovery"
|
||||||
if not config.check_pin(pin_to_int(curpin)):
|
text = Text(title, ui.ICON_RECOVERY)
|
||||||
raise wire.PinInvalid("PIN invalid")
|
text.normal("Do you really want to", "check the recovery", "seed?")
|
||||||
|
await require_confirm(ctx, text, code=ButtonRequestType.ProtectCall)
|
||||||
|
|
||||||
# ask for the number of words
|
if msg.dry_run:
|
||||||
wordcount = await request_wordcount(ctx, title)
|
if config.has_pin():
|
||||||
|
curpin = await request_pin_ack(ctx, "Enter PIN", config.get_pin_rem())
|
||||||
|
else:
|
||||||
|
curpin = ""
|
||||||
|
if not config.check_pin(pin_to_int(curpin)):
|
||||||
|
raise wire.PinInvalid("PIN invalid")
|
||||||
|
|
||||||
# ask for mnemonic words one by one
|
# ask for the number of words
|
||||||
words = await request_mnemonic(ctx, wordcount)
|
wordcount = await request_wordcount(ctx, title)
|
||||||
|
mnemonic_module = mnemonic.module_from_words_count(wordcount)
|
||||||
|
else:
|
||||||
|
wordcount = storage.get_slip39_words_count()
|
||||||
|
mnemonic_module = mnemonic.slip39
|
||||||
|
|
||||||
|
if mnemonic_module == mnemonic.slip39:
|
||||||
|
# show a note about the keyboard
|
||||||
|
await show_keyboard_info(ctx)
|
||||||
|
|
||||||
|
secret = None
|
||||||
|
while secret is None:
|
||||||
|
# ask for mnemonic words one by one
|
||||||
|
words = await request_mnemonic(
|
||||||
|
ctx, wordcount, mnemonic_module == mnemonic.slip39
|
||||||
|
)
|
||||||
|
secret = mnemonic_module.process_single(words)
|
||||||
|
# show a number of remaining mnemonics for SLIP39
|
||||||
|
if secret is None and mnemonic_module == mnemonic.slip39:
|
||||||
|
await show_remaining_slip39_mnemonics(
|
||||||
|
ctx, title, storage.get_slip39_remaining()
|
||||||
|
)
|
||||||
|
|
||||||
# check mnemonic validity
|
# check mnemonic validity
|
||||||
if msg.enforce_wordlist or msg.dry_run:
|
# it is checked automatically in SLIP-39
|
||||||
if not bip39.check(words):
|
if mnemonic_module == mnemonic.bip39 and (msg.enforce_wordlist or msg.dry_run):
|
||||||
|
if not mnemonic_module.check(secret):
|
||||||
raise wire.ProcessError("Mnemonic is not valid")
|
raise wire.ProcessError("Mnemonic is not valid")
|
||||||
|
|
||||||
# ask for pin repeatedly
|
# ask for pin repeatedly
|
||||||
@ -72,39 +88,24 @@ async def recovery_device(ctx, msg):
|
|||||||
else:
|
else:
|
||||||
newpin = ""
|
newpin = ""
|
||||||
|
|
||||||
secret = mnemonic.process([words], mnemonic.TYPE_BIP39)
|
|
||||||
|
|
||||||
# dry run
|
# dry run
|
||||||
if msg.dry_run:
|
if msg.dry_run:
|
||||||
digest_input = sha256(secret).digest()
|
mnemonic.dry_run(secret)
|
||||||
stored, _ = mnemonic.get()
|
|
||||||
digest_stored = sha256(stored).digest()
|
|
||||||
if consteq(digest_stored, digest_input):
|
|
||||||
return Success(
|
|
||||||
message="The seed is valid and matches the one in the device"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise wire.ProcessError(
|
|
||||||
"The seed is valid but does not match the one in the device"
|
|
||||||
)
|
|
||||||
|
|
||||||
# save into storage
|
# save into storage
|
||||||
if newpin:
|
if msg.pin_protection:
|
||||||
config.change_pin(pin_to_int(""), pin_to_int(newpin))
|
config.change_pin(pin_to_int(""), pin_to_int(newpin))
|
||||||
storage.set_u2f_counter(msg.u2f_counter)
|
storage.set_u2f_counter(msg.u2f_counter)
|
||||||
storage.load_settings(label=msg.label, use_passphrase=msg.passphrase_protection)
|
storage.load_settings(label=msg.label, use_passphrase=msg.passphrase_protection)
|
||||||
storage.store_mnemonic(
|
mnemonic_module.store(secret=secret, needs_backup=False, no_backup=False)
|
||||||
secret=secret,
|
|
||||||
mnemonic_type=mnemonic.TYPE_BIP39,
|
display_homescreen()
|
||||||
needs_backup=False,
|
|
||||||
no_backup=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
return Success(message="Device recovered")
|
return Success(message="Device recovered")
|
||||||
|
|
||||||
|
|
||||||
async def request_wordcount(ctx, title: str) -> int:
|
async def request_wordcount(ctx, title: str) -> int:
|
||||||
await ctx.call(ButtonRequest(code=MnemonicWordCount), ButtonAck)
|
await ctx.call(ButtonRequest(code=ButtonRequestType.MnemonicWordCount), ButtonAck)
|
||||||
|
|
||||||
text = Text(title, ui.ICON_RECOVERY)
|
text = Text(title, ui.ICON_RECOVERY)
|
||||||
text.normal("Number of words?")
|
text.normal("Number of words?")
|
||||||
@ -118,12 +119,15 @@ async def request_wordcount(ctx, title: str) -> int:
|
|||||||
return count
|
return count
|
||||||
|
|
||||||
|
|
||||||
async def request_mnemonic(ctx, count: int) -> str:
|
async def request_mnemonic(ctx, count: int, slip39: bool) -> str:
|
||||||
await ctx.call(ButtonRequest(code=MnemonicInput), ButtonAck)
|
await ctx.call(ButtonRequest(code=ButtonRequestType.MnemonicInput), ButtonAck)
|
||||||
|
|
||||||
words = []
|
words = []
|
||||||
for i in range(count):
|
for i in range(count):
|
||||||
keyboard = MnemonicKeyboard("Type the %s word:" % format_ordinal(i + 1))
|
if slip39:
|
||||||
|
keyboard = Slip39Keyboard("Type the %s word:" % format_ordinal(i + 1))
|
||||||
|
else:
|
||||||
|
keyboard = Bip39Keyboard("Type the %s word:" % format_ordinal(i + 1))
|
||||||
if __debug__:
|
if __debug__:
|
||||||
word = await ctx.wait(keyboard, input_signal)
|
word = await ctx.wait(keyboard, input_signal)
|
||||||
else:
|
else:
|
||||||
@ -131,3 +135,33 @@ async def request_mnemonic(ctx, count: int) -> str:
|
|||||||
words.append(word)
|
words.append(word)
|
||||||
|
|
||||||
return " ".join(words)
|
return " ".join(words)
|
||||||
|
|
||||||
|
|
||||||
|
async def show_keyboard_info(ctx):
|
||||||
|
await ctx.call(ButtonRequest(code=ButtonRequestType.Other), ButtonAck)
|
||||||
|
|
||||||
|
info = InfoConfirm(
|
||||||
|
"One more thing. "
|
||||||
|
"You can type the letters "
|
||||||
|
"the old-fashioned way "
|
||||||
|
"one by one or use our "
|
||||||
|
"T9 keyboard and press "
|
||||||
|
"buttons only once."
|
||||||
|
)
|
||||||
|
if __debug__:
|
||||||
|
await ctx.wait(info, confirm_signal)
|
||||||
|
else:
|
||||||
|
await ctx.wait(info)
|
||||||
|
|
||||||
|
|
||||||
|
async def show_remaining_slip39_mnemonics(ctx, title, remaining: int):
|
||||||
|
text = Text(title, ui.ICON_RECOVERY)
|
||||||
|
text.bold("Good job!")
|
||||||
|
if remaining > 1:
|
||||||
|
text.normal("%s more shares" % remaining)
|
||||||
|
else:
|
||||||
|
text.normal("%s more share" % remaining)
|
||||||
|
text.normal("needed to enter.")
|
||||||
|
await require_confirm(
|
||||||
|
ctx, text, ButtonRequestType.ProtectCall, cancel=None, confirm="Continue"
|
||||||
|
)
|
||||||
|
@ -1,20 +1,13 @@
|
|||||||
from micropython import const
|
from trezor import config, wire
|
||||||
from ubinascii import hexlify
|
|
||||||
|
|
||||||
from trezor import config, ui, wire
|
|
||||||
from trezor.crypto import bip39, hashlib, random
|
from trezor.crypto import bip39, hashlib, random
|
||||||
from trezor.messages import ButtonRequestType, MessageType
|
from trezor.messages import MessageType
|
||||||
from trezor.messages.EntropyRequest import EntropyRequest
|
from trezor.messages.EntropyRequest import EntropyRequest
|
||||||
from trezor.messages.Success import Success
|
from trezor.messages.Success import Success
|
||||||
from trezor.pin import pin_to_int
|
from trezor.pin import pin_to_int
|
||||||
from trezor.ui.mnemonic import MnemonicKeyboard
|
|
||||||
from trezor.ui.scroll import Paginated
|
|
||||||
from trezor.ui.text import Text
|
|
||||||
from trezor.utils import chunks, format_ordinal
|
|
||||||
|
|
||||||
from apps.common import mnemonic, storage
|
from apps.common import mnemonic, storage
|
||||||
from apps.common.confirm import hold_to_confirm, require_confirm
|
|
||||||
from apps.management.change_pin import request_pin_confirm
|
from apps.management.change_pin import request_pin_confirm
|
||||||
|
from apps.management.common import layout
|
||||||
|
|
||||||
if __debug__:
|
if __debug__:
|
||||||
from apps import debug
|
from apps import debug
|
||||||
@ -22,15 +15,10 @@ if __debug__:
|
|||||||
|
|
||||||
async def reset_device(ctx, msg):
|
async def reset_device(ctx, msg):
|
||||||
# validate parameters and device state
|
# validate parameters and device state
|
||||||
if msg.strength not in (128, 192, 256):
|
_validate_reset_device(msg)
|
||||||
raise wire.ProcessError("Invalid strength (has to be 128, 192 or 256 bits)")
|
|
||||||
if msg.display_random and (msg.skip_backup or msg.no_backup):
|
|
||||||
raise wire.ProcessError("Can't show internal entropy when backup is skipped")
|
|
||||||
if storage.is_initialized():
|
|
||||||
raise wire.UnexpectedMessage("Already initialized")
|
|
||||||
|
|
||||||
# make sure use knows he's setting up a new wallet
|
# make sure user knows he's setting up a new wallet
|
||||||
await show_reset_warning(ctx)
|
await layout.show_reset_device_warning(ctx, msg.slip39)
|
||||||
|
|
||||||
# request new PIN
|
# request new PIN
|
||||||
if msg.pin_protection:
|
if msg.pin_protection:
|
||||||
@ -39,165 +27,100 @@ async def reset_device(ctx, msg):
|
|||||||
newpin = ""
|
newpin = ""
|
||||||
|
|
||||||
# generate and display internal entropy
|
# generate and display internal entropy
|
||||||
internal_ent = random.bytes(32)
|
int_entropy = random.bytes(32)
|
||||||
if __debug__:
|
if __debug__:
|
||||||
debug.reset_internal_entropy = internal_ent
|
debug.reset_internal_entropy = int_entropy
|
||||||
if msg.display_random:
|
if msg.display_random:
|
||||||
await show_entropy(ctx, internal_ent)
|
await layout.show_internal_entropy(ctx, int_entropy)
|
||||||
|
|
||||||
# request external entropy and compute mnemonic
|
# request external entropy and compute the master secret
|
||||||
ent_ack = await ctx.call(EntropyRequest(), MessageType.EntropyAck)
|
entropy_ack = await ctx.call(EntropyRequest(), MessageType.EntropyAck)
|
||||||
words = generate_mnemonic(msg.strength, internal_ent, ent_ack.entropy)
|
ext_entropy = entropy_ack.entropy
|
||||||
|
secret = _compute_secret_from_entropy(int_entropy, ext_entropy, msg.strength)
|
||||||
|
|
||||||
if not msg.skip_backup and not msg.no_backup:
|
# should we back up the wallet now?
|
||||||
# require confirmation of the mnemonic safety
|
if not msg.no_backup and not msg.skip_backup:
|
||||||
await show_backup_warning(ctx)
|
if not await layout.confirm_backup(ctx):
|
||||||
|
if not await layout.confirm_backup_again(ctx):
|
||||||
|
msg.skip_backup = True
|
||||||
|
|
||||||
# show mnemonic and require confirmation of a random word
|
# generate and display backup information for the master secret
|
||||||
while True:
|
if not msg.no_backup and not msg.skip_backup:
|
||||||
await show_mnemonic(ctx, words)
|
if msg.slip39:
|
||||||
if await check_mnemonic(ctx, words):
|
await backup_slip39_wallet(ctx, secret)
|
||||||
break
|
else:
|
||||||
await show_wrong_entry(ctx)
|
await backup_bip39_wallet(ctx, secret)
|
||||||
|
|
||||||
# write PIN into storage
|
# write PIN into storage
|
||||||
if newpin:
|
if not config.change_pin(pin_to_int(""), pin_to_int(newpin)):
|
||||||
if not config.change_pin(pin_to_int(""), pin_to_int(newpin)):
|
raise wire.ProcessError("Could not change PIN")
|
||||||
raise wire.ProcessError("Could not change PIN")
|
|
||||||
|
|
||||||
secret = mnemonic.process([words], mnemonic.TYPE_BIP39)
|
# write settings and master secret into storage
|
||||||
# write settings and mnemonic into storage
|
|
||||||
storage.load_settings(label=msg.label, use_passphrase=msg.passphrase_protection)
|
storage.load_settings(label=msg.label, use_passphrase=msg.passphrase_protection)
|
||||||
storage.store_mnemonic(
|
if msg.slip39:
|
||||||
secret=secret,
|
mnemonic.slip39.store(
|
||||||
mnemonic_type=mnemonic.TYPE_BIP39,
|
secret=secret, needs_backup=msg.skip_backup, no_backup=msg.no_backup
|
||||||
needs_backup=msg.skip_backup,
|
)
|
||||||
no_backup=msg.no_backup,
|
else:
|
||||||
)
|
# in BIP-39 we store mnemonic string instead of the secret
|
||||||
|
mnemonic.bip39.store(
|
||||||
|
secret=bip39.from_data(secret).encode(),
|
||||||
|
needs_backup=msg.skip_backup,
|
||||||
|
no_backup=msg.no_backup,
|
||||||
|
)
|
||||||
|
|
||||||
# show success message
|
# if we backed up the wallet, show success message
|
||||||
if not msg.skip_backup and not msg.no_backup:
|
if not msg.no_backup and not msg.skip_backup:
|
||||||
await show_success(ctx)
|
await layout.show_backup_success(ctx)
|
||||||
|
|
||||||
return Success(message="Initialized")
|
return Success(message="Initialized")
|
||||||
|
|
||||||
|
|
||||||
def generate_mnemonic(strength: int, int_entropy: bytes, ext_entropy: bytes) -> bytes:
|
async def backup_slip39_wallet(ctx, secret: bytes):
|
||||||
|
# get number of shares
|
||||||
|
await layout.slip39_show_checklist_set_shares(ctx)
|
||||||
|
shares_count = await layout.slip39_prompt_number_of_shares(ctx)
|
||||||
|
|
||||||
|
# get threshold
|
||||||
|
await layout.slip39_show_checklist_set_threshold(ctx, shares_count)
|
||||||
|
threshold = await layout.slip39_prompt_threshold(ctx, shares_count)
|
||||||
|
|
||||||
|
# generate the mnemonics
|
||||||
|
mnemonics = mnemonic.slip39.generate_from_secret(secret, shares_count, threshold)
|
||||||
|
|
||||||
|
# show and confirm individual shares
|
||||||
|
await layout.slip39_show_checklist_show_shares(ctx, shares_count, threshold)
|
||||||
|
await layout.slip39_show_and_confirm_shares(ctx, mnemonics)
|
||||||
|
|
||||||
|
|
||||||
|
async def backup_bip39_wallet(ctx, secret: bytes):
|
||||||
|
mnemonic = bip39.from_data(secret)
|
||||||
|
await layout.bip39_show_and_confirm_mnemonic(ctx, mnemonic)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_reset_device(msg):
|
||||||
|
if msg.strength not in (128, 256):
|
||||||
|
if msg.slip39:
|
||||||
|
raise wire.ProcessError("Invalid strength (has to be 128 or 256 bits)")
|
||||||
|
elif msg.strength != 192:
|
||||||
|
raise wire.ProcessError("Invalid strength (has to be 128, 192 or 256 bits)")
|
||||||
|
if msg.display_random and (msg.skip_backup or msg.no_backup):
|
||||||
|
raise wire.ProcessError("Can't show internal entropy when backup is skipped")
|
||||||
|
if storage.is_initialized():
|
||||||
|
raise wire.UnexpectedMessage("Already initialized")
|
||||||
|
if (msg.skip_backup or msg.no_backup) and msg.slip39:
|
||||||
|
raise wire.ProcessError("Both no/skip backup flag and Shamir SLIP-39 required.")
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_secret_from_entropy(
|
||||||
|
int_entropy: bytes, ext_entropy: bytes, strength_in_bytes: int
|
||||||
|
) -> bytes:
|
||||||
|
# combine internal and external entropy
|
||||||
ehash = hashlib.sha256()
|
ehash = hashlib.sha256()
|
||||||
ehash.update(int_entropy)
|
ehash.update(int_entropy)
|
||||||
ehash.update(ext_entropy)
|
ehash.update(ext_entropy)
|
||||||
entropy = ehash.digest()
|
entropy = ehash.digest()
|
||||||
return bip39.from_data(entropy[: strength // 8])
|
# take a required number of bytes
|
||||||
|
strength = strength_in_bytes // 8
|
||||||
|
secret = entropy[:strength]
|
||||||
async def show_reset_warning(ctx):
|
return secret
|
||||||
text = Text("Create a new wallet", ui.ICON_RESET, new_lines=False)
|
|
||||||
text.normal("Do you really want to")
|
|
||||||
text.br()
|
|
||||||
text.normal("create a new wallet?")
|
|
||||||
text.br()
|
|
||||||
text.br_half()
|
|
||||||
text.normal("By continuing you agree")
|
|
||||||
text.br()
|
|
||||||
text.normal("to")
|
|
||||||
text.bold("https://trezor.io/tos")
|
|
||||||
await require_confirm(ctx, text, code=ButtonRequestType.ResetDevice)
|
|
||||||
|
|
||||||
|
|
||||||
async def show_backup_warning(ctx):
|
|
||||||
text = Text("Backup your seed", ui.ICON_NOCOPY)
|
|
||||||
text.normal(
|
|
||||||
"Never make a digital",
|
|
||||||
"copy of your recovery",
|
|
||||||
"seed and never upload",
|
|
||||||
"it online!",
|
|
||||||
)
|
|
||||||
await require_confirm(
|
|
||||||
ctx, text, ButtonRequestType.ResetDevice, confirm="I understand", cancel=None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def show_wrong_entry(ctx):
|
|
||||||
text = Text("Wrong entry!", ui.ICON_WRONG, ui.RED)
|
|
||||||
text.normal("You have entered", "wrong seed word.", "Please check again.")
|
|
||||||
await require_confirm(
|
|
||||||
ctx, text, ButtonRequestType.ResetDevice, confirm="Check again", cancel=None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def show_success(ctx):
|
|
||||||
text = Text("Backup is done!", ui.ICON_CONFIRM, ui.GREEN)
|
|
||||||
text.normal(
|
|
||||||
"Never make a digital",
|
|
||||||
"copy of your recovery",
|
|
||||||
"seed and never upload",
|
|
||||||
"it online!",
|
|
||||||
)
|
|
||||||
await require_confirm(
|
|
||||||
ctx, text, ButtonRequestType.ResetDevice, confirm="Finish setup", cancel=None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def show_entropy(ctx, entropy: bytes):
|
|
||||||
entropy_str = hexlify(entropy).decode()
|
|
||||||
lines = chunks(entropy_str, 16)
|
|
||||||
text = Text("Internal entropy", ui.ICON_RESET)
|
|
||||||
text.mono(*lines)
|
|
||||||
await require_confirm(ctx, text, ButtonRequestType.ResetDevice)
|
|
||||||
|
|
||||||
|
|
||||||
async def show_mnemonic(ctx, mnemonic: str):
|
|
||||||
# split mnemonic words into pages
|
|
||||||
PER_PAGE = const(4)
|
|
||||||
words = mnemonic.split()
|
|
||||||
words = list(enumerate(words))
|
|
||||||
words = list(chunks(words, PER_PAGE))
|
|
||||||
|
|
||||||
# display the pages, with a confirmation dialog on the last one
|
|
||||||
pages = [get_mnemonic_page(page) for page in words]
|
|
||||||
paginated = Paginated(pages)
|
|
||||||
|
|
||||||
if __debug__:
|
|
||||||
|
|
||||||
def export_displayed_words():
|
|
||||||
# export currently displayed mnemonic words into debuglink
|
|
||||||
debug.reset_current_words = [w for _, w in words[paginated.page]]
|
|
||||||
|
|
||||||
paginated.on_change = export_displayed_words
|
|
||||||
export_displayed_words()
|
|
||||||
|
|
||||||
await hold_to_confirm(ctx, paginated, ButtonRequestType.ResetDevice)
|
|
||||||
|
|
||||||
|
|
||||||
def get_mnemonic_page(words: list):
|
|
||||||
text = Text("Recovery seed", ui.ICON_RESET)
|
|
||||||
for index, word in words:
|
|
||||||
text.mono("%2d. %s" % (index + 1, word))
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
async def check_mnemonic(ctx, mnemonic: str) -> bool:
|
|
||||||
words = mnemonic.split()
|
|
||||||
|
|
||||||
# check a word from the first half
|
|
||||||
index = random.uniform(len(words) // 2)
|
|
||||||
if not await check_word(ctx, words, index):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# check a word from the second half
|
|
||||||
index = random.uniform(len(words) // 2) + len(words) // 2
|
|
||||||
if not await check_word(ctx, words, index):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def check_word(ctx, words: list, index: int):
|
|
||||||
if __debug__:
|
|
||||||
debug.reset_word_index = index
|
|
||||||
keyboard = MnemonicKeyboard("Type the %s word:" % format_ordinal(index + 1))
|
|
||||||
if __debug__:
|
|
||||||
result = await ctx.wait(keyboard, debug.input_signal)
|
|
||||||
else:
|
|
||||||
result = await ctx.wait(keyboard)
|
|
||||||
return result == words[index]
|
|
||||||
|
@ -21,8 +21,23 @@
|
|||||||
from micropython import const
|
from micropython import const
|
||||||
|
|
||||||
from trezor.crypto import hashlib, hmac, pbkdf2, random
|
from trezor.crypto import hashlib, hmac, pbkdf2, random
|
||||||
from trezor.crypto.slip39_wordlist import wordlist
|
from trezorcrypto import shamir, slip39
|
||||||
from trezorcrypto import shamir
|
|
||||||
|
_KEYBOARD_FULL_MASK = const(0x1FF)
|
||||||
|
"""All buttons are allowed. 9-bit bitmap all set to 1."""
|
||||||
|
|
||||||
|
|
||||||
|
def compute_mask(prefix: str) -> int:
|
||||||
|
if not prefix:
|
||||||
|
return _KEYBOARD_FULL_MASK
|
||||||
|
return slip39.compute_mask(int(prefix))
|
||||||
|
|
||||||
|
|
||||||
|
def button_sequence_to_word(prefix: str) -> str:
|
||||||
|
if not prefix:
|
||||||
|
return _KEYBOARD_FULL_MASK
|
||||||
|
return slip39.button_sequence_to_word(int(prefix))
|
||||||
|
|
||||||
|
|
||||||
_RADIX_BITS = const(10)
|
_RADIX_BITS = const(10)
|
||||||
"""The length of the radix in bits."""
|
"""The length of the radix in bits."""
|
||||||
@ -36,6 +51,11 @@ def bits_to_words(n):
|
|||||||
return (n + _RADIX_BITS - 1) // _RADIX_BITS
|
return (n + _RADIX_BITS - 1) // _RADIX_BITS
|
||||||
|
|
||||||
|
|
||||||
|
MAX_SHARE_COUNT = const(16)
|
||||||
|
"""The maximum number of shares that can be created."""
|
||||||
|
|
||||||
|
DEFAULT_ITERATION_EXPONENT = const(0)
|
||||||
|
|
||||||
_RADIX = 2 ** _RADIX_BITS
|
_RADIX = 2 ** _RADIX_BITS
|
||||||
"""The number of words in the wordlist."""
|
"""The number of words in the wordlist."""
|
||||||
|
|
||||||
@ -48,9 +68,6 @@ _ITERATION_EXP_LENGTH_BITS = const(5)
|
|||||||
_ID_EXP_LENGTH_WORDS = bits_to_words(_ID_LENGTH_BITS + _ITERATION_EXP_LENGTH_BITS)
|
_ID_EXP_LENGTH_WORDS = bits_to_words(_ID_LENGTH_BITS + _ITERATION_EXP_LENGTH_BITS)
|
||||||
"""The length of the random identifier and iteration exponent in words."""
|
"""The length of the random identifier and iteration exponent in words."""
|
||||||
|
|
||||||
_MAX_SHARE_COUNT = const(16)
|
|
||||||
"""The maximum number of shares that can be created."""
|
|
||||||
|
|
||||||
_CHECKSUM_LENGTH_WORDS = const(3)
|
_CHECKSUM_LENGTH_WORDS = const(3)
|
||||||
"""The length of the RS1024 checksum in words."""
|
"""The length of the RS1024 checksum in words."""
|
||||||
|
|
||||||
@ -86,21 +103,6 @@ class MnemonicError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def word_index(word):
|
|
||||||
word = word + " " * (8 - len(word))
|
|
||||||
lo = 0
|
|
||||||
hi = _RADIX
|
|
||||||
while hi - lo > 1:
|
|
||||||
mid = (hi + lo) // 2
|
|
||||||
if wordlist[mid * 8 : mid * 8 + 8] > word:
|
|
||||||
hi = mid
|
|
||||||
else:
|
|
||||||
lo = mid
|
|
||||||
if wordlist[lo * 8 : lo * 8 + 8] != word:
|
|
||||||
raise MnemonicError('Invalid mnemonic word "{}".'.format(word))
|
|
||||||
return lo
|
|
||||||
|
|
||||||
|
|
||||||
def _rs1024_polymod(values):
|
def _rs1024_polymod(values):
|
||||||
GEN = (
|
GEN = (
|
||||||
0xE0E040,
|
0xE0E040,
|
||||||
@ -181,11 +183,11 @@ def _int_to_indices(value, length, bits):
|
|||||||
|
|
||||||
|
|
||||||
def mnemonic_from_indices(indices):
|
def mnemonic_from_indices(indices):
|
||||||
return " ".join(wordlist[i * 8 : i * 8 + 8].strip() for i in indices)
|
return " ".join(slip39.get_word(i) for i in indices)
|
||||||
|
|
||||||
|
|
||||||
def mnemonic_to_indices(mnemonic):
|
def mnemonic_to_indices(mnemonic):
|
||||||
return (word_index(word.lower()) for word in mnemonic.split())
|
return (slip39.word_index(word.lower()) for word in mnemonic.split())
|
||||||
|
|
||||||
|
|
||||||
def _round_function(i, passphrase, e, salt, r):
|
def _round_function(i, passphrase, e, salt, r):
|
||||||
@ -247,10 +249,10 @@ def _split_secret(threshold, share_count, shared_secret):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if share_count > _MAX_SHARE_COUNT:
|
if share_count > MAX_SHARE_COUNT:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"The requested number of shares ({}) must not exceed {}.".format(
|
"The requested number of shares ({}) must not exceed {}.".format(
|
||||||
share_count, _MAX_SHARE_COUNT
|
share_count, MAX_SHARE_COUNT
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -462,16 +464,33 @@ def _decode_mnemonics(mnemonics):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _generate_random_identifier():
|
def _generate_random_identifier() -> int:
|
||||||
"""Returns a randomly generated integer in the range 0, ... , 2**_ID_LENGTH_BITS - 1."""
|
"""Returns a randomly generated integer in the range 0, ... , 2**_ID_LENGTH_BITS - 1."""
|
||||||
|
|
||||||
identifier = int.from_bytes(random.bytes(bits_to_bytes(_ID_LENGTH_BITS)), "big")
|
identifier = int.from_bytes(random.bytes(bits_to_bytes(_ID_LENGTH_BITS)), "big")
|
||||||
return identifier & ((1 << _ID_LENGTH_BITS) - 1)
|
return identifier & ((1 << _ID_LENGTH_BITS) - 1)
|
||||||
|
|
||||||
|
|
||||||
def generate_mnemonics(
|
def generate_single_group_mnemonics_from_data(
|
||||||
group_threshold, groups, master_secret, passphrase=b"", iteration_exponent=0
|
master_secret,
|
||||||
):
|
threshold,
|
||||||
|
count,
|
||||||
|
passphrase=b"",
|
||||||
|
iteration_exponent=DEFAULT_ITERATION_EXPONENT,
|
||||||
|
) -> (int, list):
|
||||||
|
identifier, mnemonics = generate_mnemonics_from_data(
|
||||||
|
master_secret, 1, [(threshold, count)], passphrase, iteration_exponent
|
||||||
|
)
|
||||||
|
return identifier, mnemonics[0]
|
||||||
|
|
||||||
|
|
||||||
|
def generate_mnemonics_from_data(
|
||||||
|
master_secret,
|
||||||
|
group_threshold,
|
||||||
|
groups,
|
||||||
|
passphrase=b"",
|
||||||
|
iteration_exponent=DEFAULT_ITERATION_EXPONENT,
|
||||||
|
) -> (int, list):
|
||||||
"""
|
"""
|
||||||
Splits a master secret into mnemonic shares using Shamir's secret sharing scheme.
|
Splits a master secret into mnemonic shares using Shamir's secret sharing scheme.
|
||||||
:param int group_threshold: The number of groups required to reconstruct the master secret.
|
:param int group_threshold: The number of groups required to reconstruct the master secret.
|
||||||
@ -486,6 +505,8 @@ def generate_mnemonics(
|
|||||||
:param int iteration_exponent: The iteration exponent.
|
:param int iteration_exponent: The iteration exponent.
|
||||||
:return: List of mnemonics.
|
:return: List of mnemonics.
|
||||||
:rtype: List of byte arrays.
|
:rtype: List of byte arrays.
|
||||||
|
:return: Identifier.
|
||||||
|
:rtype: int.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
identifier = _generate_random_identifier()
|
identifier = _generate_random_identifier()
|
||||||
@ -528,68 +549,28 @@ def generate_mnemonics(
|
|||||||
|
|
||||||
group_shares = _split_secret(group_threshold, len(groups), encrypted_master_secret)
|
group_shares = _split_secret(group_threshold, len(groups), encrypted_master_secret)
|
||||||
|
|
||||||
return [
|
mnemonics = []
|
||||||
[
|
for (member_threshold, member_count), (group_index, group_secret) in zip(
|
||||||
encode_mnemonic(
|
groups, group_shares
|
||||||
identifier,
|
):
|
||||||
iteration_exponent,
|
group_mnemonics = []
|
||||||
group_index,
|
for member_index, value in _split_secret(
|
||||||
group_threshold,
|
member_threshold, member_count, group_secret
|
||||||
len(groups),
|
):
|
||||||
member_index,
|
group_mnemonics.append(
|
||||||
member_threshold,
|
encode_mnemonic(
|
||||||
value,
|
identifier,
|
||||||
|
iteration_exponent,
|
||||||
|
group_index,
|
||||||
|
group_threshold,
|
||||||
|
len(groups),
|
||||||
|
member_index,
|
||||||
|
member_threshold,
|
||||||
|
value,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
for member_index, value in _split_secret(
|
mnemonics.append(group_mnemonics)
|
||||||
member_threshold, member_count, group_secret
|
return identifier, mnemonics
|
||||||
)
|
|
||||||
]
|
|
||||||
for (member_threshold, member_count), (group_index, group_secret) in zip(
|
|
||||||
groups, group_shares
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def generate_mnemonics_random(
|
|
||||||
group_threshold, groups, strength_bits=128, passphrase=b"", iteration_exponent=0
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Generates a random master secret and splits it into mnemonic shares using Shamir's secret
|
|
||||||
sharing scheme.
|
|
||||||
:param int group_threshold: The number of groups required to reconstruct the master secret.
|
|
||||||
:param groups: A list of (member_threshold, member_count) pairs for each group, where member_count
|
|
||||||
is the number of shares to generate for the group and member_threshold is the number of members required to
|
|
||||||
reconstruct the group secret.
|
|
||||||
:type groups: List of pairs of integers.
|
|
||||||
:param int strength_bits: The entropy of the randomly generated master secret in bits.
|
|
||||||
:param passphrase: The passphrase used to encrypt the master secret.
|
|
||||||
:type passphrase: Array of bytes.
|
|
||||||
:param int iteration_exponent: The iteration exponent.
|
|
||||||
:return: List of mnemonics.
|
|
||||||
:rtype: List of byte arrays.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if strength_bits < _MIN_STRENGTH_BITS:
|
|
||||||
raise ValueError(
|
|
||||||
"The requested strength of the master secret ({} bits) must be at least {} bits.".format(
|
|
||||||
strength_bits, _MIN_STRENGTH_BITS
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if strength_bits % 16 != 0:
|
|
||||||
raise ValueError(
|
|
||||||
"The requested strength of the master secret ({} bits) must be a multiple of 16 bits.".format(
|
|
||||||
strength_bits
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return generate_mnemonics(
|
|
||||||
group_threshold,
|
|
||||||
groups,
|
|
||||||
random.bytes(strength_bits // 8),
|
|
||||||
passphrase,
|
|
||||||
iteration_exponent,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def combine_mnemonics(mnemonics):
|
def combine_mnemonics(mnemonics):
|
||||||
@ -597,7 +578,7 @@ def combine_mnemonics(mnemonics):
|
|||||||
Combines mnemonic shares to obtain the master secret which was previously split using
|
Combines mnemonic shares to obtain the master secret which was previously split using
|
||||||
Shamir's secret sharing scheme.
|
Shamir's secret sharing scheme.
|
||||||
:param mnemonics: List of mnemonics.
|
:param mnemonics: List of mnemonics.
|
||||||
:type mnemonics: List of byte arrays.
|
:type mnemonics: List of strings.
|
||||||
:return: Identifier, iteration exponent, the encrypted master secret.
|
:return: Identifier, iteration exponent, the encrypted master secret.
|
||||||
:rtype: Integer, integer, array of bytes.
|
:rtype: Integer, integer, array of bytes.
|
||||||
"""
|
"""
|
||||||
|
File diff suppressed because one or more lines are too long
@ -17,6 +17,7 @@ class ResetDevice(p.MessageType):
|
|||||||
u2f_counter: int = None,
|
u2f_counter: int = None,
|
||||||
skip_backup: bool = None,
|
skip_backup: bool = None,
|
||||||
no_backup: bool = None,
|
no_backup: bool = None,
|
||||||
|
slip39: bool = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.display_random = display_random
|
self.display_random = display_random
|
||||||
self.strength = strength
|
self.strength = strength
|
||||||
@ -27,6 +28,7 @@ class ResetDevice(p.MessageType):
|
|||||||
self.u2f_counter = u2f_counter
|
self.u2f_counter = u2f_counter
|
||||||
self.skip_backup = skip_backup
|
self.skip_backup = skip_backup
|
||||||
self.no_backup = no_backup
|
self.no_backup = no_backup
|
||||||
|
self.slip39 = slip39
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_fields(cls):
|
def get_fields(cls):
|
||||||
@ -40,4 +42,5 @@ class ResetDevice(p.MessageType):
|
|||||||
7: ('u2f_counter', p.UVarintType, 0),
|
7: ('u2f_counter', p.UVarintType, 0),
|
||||||
8: ('skip_backup', p.BoolType, 0),
|
8: ('skip_backup', p.BoolType, 0),
|
||||||
9: ('no_backup', p.BoolType, 0),
|
9: ('no_backup', p.BoolType, 0),
|
||||||
|
10: ('slip39', p.BoolType, 0),
|
||||||
}
|
}
|
||||||
|
55
core/src/trezor/ui/checklist.py
Normal file
55
core/src/trezor/ui/checklist.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
from micropython import const
|
||||||
|
|
||||||
|
from trezor import ui
|
||||||
|
from trezor.ui import text
|
||||||
|
|
||||||
|
_CHECKLIST_MAX_LINES = const(5)
|
||||||
|
_CHECKLIST_OFFSET_X = const(24)
|
||||||
|
|
||||||
|
|
||||||
|
class Checklist(ui.Control):
|
||||||
|
def __init__(self, title, icon):
|
||||||
|
self.title = title
|
||||||
|
self.icon = icon
|
||||||
|
self.choices = []
|
||||||
|
self.words = []
|
||||||
|
self.active = 0
|
||||||
|
self.repaint = False
|
||||||
|
|
||||||
|
def add(self, choice):
|
||||||
|
self.choices.append(choice)
|
||||||
|
|
||||||
|
def select(self, active):
|
||||||
|
self.active = active
|
||||||
|
|
||||||
|
def process(self):
|
||||||
|
w = self.words
|
||||||
|
w.clear()
|
||||||
|
for index, choice in enumerate(self.choices):
|
||||||
|
if index < self.active:
|
||||||
|
w.append(ui.BOLD)
|
||||||
|
w.append(ui.GREEN)
|
||||||
|
elif index == self.active:
|
||||||
|
w.append(ui.BOLD)
|
||||||
|
w.append(ui.FG)
|
||||||
|
else: # index > self.active
|
||||||
|
w.append(ui.NORMAL)
|
||||||
|
w.append(ui.GREY)
|
||||||
|
if isinstance(choice, str):
|
||||||
|
w.append(choice)
|
||||||
|
else: # choice is iterable
|
||||||
|
w.extend(choice)
|
||||||
|
w.append(text.BR)
|
||||||
|
self.words = w
|
||||||
|
self.repaint = True
|
||||||
|
|
||||||
|
def on_render(self):
|
||||||
|
if self.repaint:
|
||||||
|
ui.header(self.title, self.icon)
|
||||||
|
text.render_text(
|
||||||
|
self.words,
|
||||||
|
new_lines=False, # we are adding line breaks manually
|
||||||
|
max_lines=_CHECKLIST_MAX_LINES,
|
||||||
|
offset_x=_CHECKLIST_OFFSET_X,
|
||||||
|
)
|
||||||
|
self.repaint = False
|
@ -19,12 +19,15 @@ class Confirm(ui.Layout):
|
|||||||
confirm_style=DEFAULT_CONFIRM_STYLE,
|
confirm_style=DEFAULT_CONFIRM_STYLE,
|
||||||
cancel=DEFAULT_CANCEL,
|
cancel=DEFAULT_CANCEL,
|
||||||
cancel_style=DEFAULT_CANCEL_STYLE,
|
cancel_style=DEFAULT_CANCEL_STYLE,
|
||||||
|
major_confirm=False,
|
||||||
):
|
):
|
||||||
self.content = content
|
self.content = content
|
||||||
|
|
||||||
if confirm is not None:
|
if confirm is not None:
|
||||||
if cancel is None:
|
if cancel is None:
|
||||||
area = ui.grid(4, n_x=1)
|
area = ui.grid(4, n_x=1)
|
||||||
|
elif major_confirm:
|
||||||
|
area = ui.grid(13, cells_x=2)
|
||||||
else:
|
else:
|
||||||
area = ui.grid(9, n_x=2)
|
area = ui.grid(9, n_x=2)
|
||||||
self.confirm = Button(area, confirm, confirm_style)
|
self.confirm = Button(area, confirm, confirm_style)
|
||||||
@ -35,6 +38,8 @@ class Confirm(ui.Layout):
|
|||||||
if cancel is not None:
|
if cancel is not None:
|
||||||
if confirm is None:
|
if confirm is None:
|
||||||
area = ui.grid(4, n_x=1)
|
area = ui.grid(4, n_x=1)
|
||||||
|
elif major_confirm:
|
||||||
|
area = ui.grid(12, cells_x=1)
|
||||||
else:
|
else:
|
||||||
area = ui.grid(8, n_x=2)
|
area = ui.grid(8, n_x=2)
|
||||||
self.cancel = Button(area, cancel, cancel_style)
|
self.cancel = Button(area, cancel, cancel_style)
|
||||||
|
63
core/src/trezor/ui/info.py
Normal file
63
core/src/trezor/ui/info.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
from trezor import res, ui
|
||||||
|
from trezor.ui.button import Button, ButtonConfirm
|
||||||
|
from trezor.ui.confirm import CONFIRMED
|
||||||
|
from trezor.ui.text import TEXT_LINE_HEIGHT, TEXT_MARGIN_LEFT, render_text
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultInfoConfirm:
|
||||||
|
|
||||||
|
fg_color = ui.LIGHT_GREY
|
||||||
|
bg_color = ui.BLACKISH
|
||||||
|
|
||||||
|
class button(ButtonConfirm):
|
||||||
|
class normal(ButtonConfirm.normal):
|
||||||
|
border_color = ui.BLACKISH
|
||||||
|
|
||||||
|
class disabled(ButtonConfirm.disabled):
|
||||||
|
border_color = ui.BLACKISH
|
||||||
|
|
||||||
|
|
||||||
|
class InfoConfirm(ui.Layout):
|
||||||
|
DEFAULT_CONFIRM = res.load(ui.ICON_CONFIRM)
|
||||||
|
DEFAULT_STYLE = DefaultInfoConfirm
|
||||||
|
|
||||||
|
def __init__(self, text, confirm=DEFAULT_CONFIRM, style=DEFAULT_STYLE):
|
||||||
|
self.text = text.split()
|
||||||
|
self.style = style
|
||||||
|
panel_area = ui.grid(0, n_x=1, n_y=1)
|
||||||
|
self.panel_area = panel_area
|
||||||
|
confirm_area = ui.grid(4, n_x=1)
|
||||||
|
self.confirm = Button(confirm_area, confirm, style.button)
|
||||||
|
self.confirm.on_click = self.on_confirm
|
||||||
|
self.repaint = True
|
||||||
|
|
||||||
|
def dispatch(self, event, x, y):
|
||||||
|
if event == ui.RENDER:
|
||||||
|
self.on_render()
|
||||||
|
self.confirm.dispatch(event, x, y)
|
||||||
|
|
||||||
|
def on_render(self):
|
||||||
|
if self.repaint:
|
||||||
|
x, y, w, h = self.panel_area
|
||||||
|
fg_color = self.style.fg_color
|
||||||
|
bg_color = self.style.bg_color
|
||||||
|
|
||||||
|
# render the background panel
|
||||||
|
ui.display.bar_radius(x, y, w, h, bg_color, ui.BG, ui.RADIUS)
|
||||||
|
|
||||||
|
# render the info text
|
||||||
|
render_text(
|
||||||
|
self.text,
|
||||||
|
new_lines=False,
|
||||||
|
max_lines=6,
|
||||||
|
offset_y=y + TEXT_LINE_HEIGHT,
|
||||||
|
offset_x=x + TEXT_MARGIN_LEFT - ui.VIEWX,
|
||||||
|
offset_x_max=x + w - ui.VIEWX,
|
||||||
|
fg=fg_color,
|
||||||
|
bg=bg_color,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.repaint = False
|
||||||
|
|
||||||
|
def on_confirm(self):
|
||||||
|
raise ui.Result(CONFIRMED)
|
@ -77,6 +77,26 @@ class Loader(ui.Control):
|
|||||||
self.start_ms = None
|
self.start_ms = None
|
||||||
self.stop_ms = None
|
self.stop_ms = None
|
||||||
self.on_start()
|
self.on_start()
|
||||||
|
if r == target:
|
||||||
|
self.on_finish()
|
||||||
|
|
||||||
def on_start(self):
|
def on_start(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def on_finish(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class LoadingAnimation(ui.Layout):
|
||||||
|
def __init__(self, style=LoaderDefault):
|
||||||
|
self.loader = Loader(style)
|
||||||
|
self.loader.on_finish = self.on_finish
|
||||||
|
self.loader.start()
|
||||||
|
|
||||||
|
def dispatch(self, event, x, y):
|
||||||
|
if not self.loader.elapsed_ms():
|
||||||
|
self.loader.start()
|
||||||
|
self.loader.dispatch(event, x, y)
|
||||||
|
|
||||||
|
def on_finish(self):
|
||||||
|
raise ui.Result(None)
|
||||||
|
@ -90,7 +90,7 @@ class Prompt(ui.Control):
|
|||||||
self.repaint = False
|
self.repaint = False
|
||||||
|
|
||||||
|
|
||||||
class MnemonicKeyboard(ui.Layout):
|
class Bip39Keyboard(ui.Layout):
|
||||||
def __init__(self, prompt):
|
def __init__(self, prompt):
|
||||||
self.prompt = Prompt(prompt)
|
self.prompt = Prompt(prompt)
|
||||||
|
|
205
core/src/trezor/ui/mnemonic_slip39.py
Normal file
205
core/src/trezor/ui/mnemonic_slip39.py
Normal file
@ -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()
|
49
core/src/trezor/ui/shamir.py
Normal file
49
core/src/trezor/ui/shamir.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
from trezor import ui
|
||||||
|
from trezor.ui.button import Button
|
||||||
|
from trezor.ui.text import LABEL_CENTER, Label
|
||||||
|
|
||||||
|
|
||||||
|
class NumInput(ui.Control):
|
||||||
|
def __init__(self, count=5, max_count=16, min_count=1):
|
||||||
|
self.count = count
|
||||||
|
self.max_count = max_count
|
||||||
|
self.min_count = min_count
|
||||||
|
|
||||||
|
self.minus = Button(ui.grid(3), "-")
|
||||||
|
self.minus.on_click = self.on_minus
|
||||||
|
self.plus = Button(ui.grid(5), "+")
|
||||||
|
self.plus.on_click = self.on_plus
|
||||||
|
self.text = Label(ui.grid(4), "", LABEL_CENTER, ui.BOLD)
|
||||||
|
|
||||||
|
self.edit(count)
|
||||||
|
|
||||||
|
def dispatch(self, event, x, y):
|
||||||
|
self.minus.dispatch(event, x, y)
|
||||||
|
self.plus.dispatch(event, x, y)
|
||||||
|
self.text.dispatch(event, x, y)
|
||||||
|
|
||||||
|
def on_minus(self):
|
||||||
|
self.edit(self.count - 1)
|
||||||
|
|
||||||
|
def on_plus(self):
|
||||||
|
self.edit(self.count + 1)
|
||||||
|
|
||||||
|
def edit(self, count):
|
||||||
|
count = max(count, self.min_count)
|
||||||
|
count = min(count, self.max_count)
|
||||||
|
if self.count != count:
|
||||||
|
self.on_change(count)
|
||||||
|
self.count = count
|
||||||
|
self.text.content = str(count)
|
||||||
|
self.text.repaint = True
|
||||||
|
if self.count == self.min_count:
|
||||||
|
self.minus.disable()
|
||||||
|
else:
|
||||||
|
self.minus.enable()
|
||||||
|
if self.count == self.max_count:
|
||||||
|
self.plus.disable()
|
||||||
|
else:
|
||||||
|
self.plus.enable()
|
||||||
|
|
||||||
|
def on_change(self, count):
|
||||||
|
pass
|
@ -13,15 +13,21 @@ BR = const(-256)
|
|||||||
BR_HALF = const(-257)
|
BR_HALF = const(-257)
|
||||||
|
|
||||||
|
|
||||||
def render_text(words: list, new_lines: bool, max_lines: int) -> None:
|
def render_text(
|
||||||
|
words: list,
|
||||||
|
new_lines: bool,
|
||||||
|
max_lines: int,
|
||||||
|
font: int = ui.NORMAL,
|
||||||
|
fg: int = ui.FG,
|
||||||
|
bg: int = ui.BG,
|
||||||
|
offset_x: int = TEXT_MARGIN_LEFT,
|
||||||
|
offset_y: int = TEXT_HEADER_HEIGHT + TEXT_LINE_HEIGHT,
|
||||||
|
offset_x_max: int = ui.WIDTH,
|
||||||
|
) -> None:
|
||||||
# initial rendering state
|
# initial rendering state
|
||||||
font = ui.NORMAL
|
INITIAL_OFFSET_X = offset_x
|
||||||
fg = ui.FG
|
offset_y_max = offset_y * max_lines
|
||||||
bg = ui.BG
|
|
||||||
offset_x = TEXT_MARGIN_LEFT
|
|
||||||
offset_y = TEXT_HEADER_HEIGHT + TEXT_LINE_HEIGHT
|
|
||||||
OFFSET_X_MAX = ui.WIDTH
|
|
||||||
OFFSET_Y_MAX = TEXT_HEADER_HEIGHT + TEXT_LINE_HEIGHT * max_lines
|
|
||||||
FONTS = (ui.NORMAL, ui.BOLD, ui.MONO, ui.MONO_BOLD)
|
FONTS = (ui.NORMAL, ui.BOLD, ui.MONO, ui.MONO_BOLD)
|
||||||
|
|
||||||
# sizes of common glyphs
|
# sizes of common glyphs
|
||||||
@ -35,10 +41,10 @@ def render_text(words: list, new_lines: bool, max_lines: int) -> None:
|
|||||||
if isinstance(word, int):
|
if isinstance(word, int):
|
||||||
if word is BR or word is BR_HALF:
|
if word is BR or word is BR_HALF:
|
||||||
# line break or half-line break
|
# line break or half-line break
|
||||||
if offset_y >= OFFSET_Y_MAX:
|
if offset_y >= offset_y_max:
|
||||||
ui.display.text(offset_x, offset_y, "...", ui.BOLD, ui.GREY, bg)
|
ui.display.text(offset_x, offset_y, "...", ui.BOLD, ui.GREY, bg)
|
||||||
return
|
return
|
||||||
offset_x = TEXT_MARGIN_LEFT
|
offset_x = INITIAL_OFFSET_X
|
||||||
offset_y += TEXT_LINE_HEIGHT if word is BR else TEXT_LINE_HEIGHT_HALF
|
offset_y += TEXT_LINE_HEIGHT if word is BR else TEXT_LINE_HEIGHT_HALF
|
||||||
elif word in FONTS:
|
elif word in FONTS:
|
||||||
# change of font style
|
# change of font style
|
||||||
@ -50,22 +56,22 @@ def render_text(words: list, new_lines: bool, max_lines: int) -> None:
|
|||||||
|
|
||||||
width = ui.display.text_width(word, font)
|
width = ui.display.text_width(word, font)
|
||||||
|
|
||||||
while offset_x + width > OFFSET_X_MAX or (
|
while offset_x + width > offset_x_max or (
|
||||||
has_next_word and offset_y >= OFFSET_Y_MAX
|
has_next_word and offset_y >= offset_y_max
|
||||||
):
|
):
|
||||||
beginning_of_line = offset_x == TEXT_MARGIN_LEFT
|
beginning_of_line = offset_x == INITIAL_OFFSET_X
|
||||||
word_fits_in_one_line = width < (OFFSET_X_MAX - TEXT_MARGIN_LEFT)
|
word_fits_in_one_line = width < (offset_x_max - INITIAL_OFFSET_X)
|
||||||
if (
|
if (
|
||||||
offset_y < OFFSET_Y_MAX
|
offset_y < offset_y_max
|
||||||
and word_fits_in_one_line
|
and word_fits_in_one_line
|
||||||
and not beginning_of_line
|
and not beginning_of_line
|
||||||
):
|
):
|
||||||
# line break
|
# line break
|
||||||
offset_x = TEXT_MARGIN_LEFT
|
offset_x = INITIAL_OFFSET_X
|
||||||
offset_y += TEXT_LINE_HEIGHT
|
offset_y += TEXT_LINE_HEIGHT
|
||||||
break
|
break
|
||||||
# word split
|
# word split
|
||||||
if offset_y < OFFSET_Y_MAX:
|
if offset_y < offset_y_max:
|
||||||
split = "-"
|
split = "-"
|
||||||
splitw = DASH
|
splitw = DASH
|
||||||
else:
|
else:
|
||||||
@ -75,7 +81,7 @@ def render_text(words: list, new_lines: bool, max_lines: int) -> None:
|
|||||||
for index in range(len(word) - 1, 0, -1):
|
for index in range(len(word) - 1, 0, -1):
|
||||||
letter = word[index]
|
letter = word[index]
|
||||||
width -= ui.display.text_width(letter, font)
|
width -= ui.display.text_width(letter, font)
|
||||||
if offset_x + width + splitw < OFFSET_X_MAX:
|
if offset_x + width + splitw < offset_x_max:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
index = 0
|
index = 0
|
||||||
@ -84,9 +90,9 @@ def render_text(words: list, new_lines: bool, max_lines: int) -> None:
|
|||||||
ui.display.text(offset_x, offset_y, span, font, fg, bg)
|
ui.display.text(offset_x, offset_y, span, font, fg, bg)
|
||||||
ui.display.text(offset_x + width, offset_y, split, ui.BOLD, ui.GREY, bg)
|
ui.display.text(offset_x + width, offset_y, split, ui.BOLD, ui.GREY, bg)
|
||||||
# line break
|
# line break
|
||||||
if offset_y >= OFFSET_Y_MAX:
|
if offset_y >= offset_y_max:
|
||||||
return
|
return
|
||||||
offset_x = TEXT_MARGIN_LEFT
|
offset_x = INITIAL_OFFSET_X
|
||||||
offset_y += TEXT_LINE_HEIGHT
|
offset_y += TEXT_LINE_HEIGHT
|
||||||
# continue with the rest
|
# continue with the rest
|
||||||
word = word[index:]
|
word = word[index:]
|
||||||
@ -97,10 +103,10 @@ def render_text(words: list, new_lines: bool, max_lines: int) -> None:
|
|||||||
|
|
||||||
if new_lines and has_next_word:
|
if new_lines and has_next_word:
|
||||||
# line break
|
# line break
|
||||||
if offset_y >= OFFSET_Y_MAX:
|
if offset_y >= offset_y_max:
|
||||||
ui.display.text(offset_x, offset_y, "...", ui.BOLD, ui.GREY, bg)
|
ui.display.text(offset_x, offset_y, "...", ui.BOLD, ui.GREY, bg)
|
||||||
return
|
return
|
||||||
offset_x = TEXT_MARGIN_LEFT
|
offset_x = INITIAL_OFFSET_X
|
||||||
offset_y += TEXT_LINE_HEIGHT
|
offset_y += TEXT_LINE_HEIGHT
|
||||||
else:
|
else:
|
||||||
# shift cursor
|
# shift cursor
|
||||||
@ -158,3 +164,31 @@ class Text(ui.Control):
|
|||||||
)
|
)
|
||||||
render_text(self.content, self.new_lines, self.max_lines)
|
render_text(self.content, self.new_lines, self.max_lines)
|
||||||
self.repaint = False
|
self.repaint = False
|
||||||
|
|
||||||
|
|
||||||
|
LABEL_LEFT = const(0)
|
||||||
|
LABEL_CENTER = const(1)
|
||||||
|
LABEL_RIGHT = const(2)
|
||||||
|
|
||||||
|
|
||||||
|
class Label(ui.Control):
|
||||||
|
def __init__(self, area, content, align=LABEL_LEFT, style=ui.NORMAL):
|
||||||
|
self.area = area
|
||||||
|
self.content = content
|
||||||
|
self.align = align
|
||||||
|
self.style = style
|
||||||
|
self.repaint = True
|
||||||
|
|
||||||
|
def on_render(self):
|
||||||
|
if self.repaint:
|
||||||
|
align = self.align
|
||||||
|
ax, ay, aw, ah = self.area
|
||||||
|
tx = ax + aw // 2
|
||||||
|
ty = ay + ah // 2 + 8
|
||||||
|
if align is LABEL_LEFT:
|
||||||
|
ui.display.text_left(tx, ty, self.content, self.style, ui.FG, ui.BG)
|
||||||
|
elif align is LABEL_CENTER:
|
||||||
|
ui.display.text_center(tx, ty, self.content, self.style, ui.FG, ui.BG)
|
||||||
|
elif align is LABEL_RIGHT:
|
||||||
|
ui.display.text_right(tx, ty, self.content, self.style, ui.FG, ui.BG)
|
||||||
|
self.repaint = False
|
||||||
|
@ -1,22 +1,30 @@
|
|||||||
from trezor import ui
|
from trezor import ui
|
||||||
from trezor.ui.button import Button
|
from trezor.ui.button import Button
|
||||||
|
|
||||||
|
# todo improve?
|
||||||
|
|
||||||
|
|
||||||
class WordSelector(ui.Layout):
|
class WordSelector(ui.Layout):
|
||||||
def __init__(self, content):
|
def __init__(self, content):
|
||||||
self.content = content
|
self.content = content
|
||||||
self.w12 = Button(ui.grid(6, n_y=4, n_x=3, cells_y=2), "12")
|
self.w12 = Button(ui.grid(6, n_y=4), "12")
|
||||||
self.w12.on_click = self.on_w12
|
self.w12.on_click = self.on_w12
|
||||||
self.w18 = Button(ui.grid(7, n_y=4, n_x=3, cells_y=2), "18")
|
self.w18 = Button(ui.grid(7, n_y=4), "18")
|
||||||
self.w18.on_click = self.on_w18
|
self.w18.on_click = self.on_w18
|
||||||
self.w24 = Button(ui.grid(8, n_y=4, n_x=3, cells_y=2), "24")
|
self.w20 = Button(ui.grid(8, n_y=4), "20")
|
||||||
|
self.w20.on_click = self.on_w20
|
||||||
|
self.w24 = Button(ui.grid(9, n_y=4), "24")
|
||||||
self.w24.on_click = self.on_w24
|
self.w24.on_click = self.on_w24
|
||||||
|
self.w33 = Button(ui.grid(10, n_y=4), "33")
|
||||||
|
self.w33.on_click = self.on_w33
|
||||||
|
|
||||||
def dispatch(self, event, x, y):
|
def dispatch(self, event, x, y):
|
||||||
self.content.dispatch(event, x, y)
|
self.content.dispatch(event, x, y)
|
||||||
self.w12.dispatch(event, x, y)
|
self.w12.dispatch(event, x, y)
|
||||||
self.w18.dispatch(event, x, y)
|
self.w18.dispatch(event, x, y)
|
||||||
|
self.w20.dispatch(event, x, y)
|
||||||
self.w24.dispatch(event, x, y)
|
self.w24.dispatch(event, x, y)
|
||||||
|
self.w33.dispatch(event, x, y)
|
||||||
|
|
||||||
def on_w12(self):
|
def on_w12(self):
|
||||||
raise ui.Result(12)
|
raise ui.Result(12)
|
||||||
@ -24,5 +32,11 @@ class WordSelector(ui.Layout):
|
|||||||
def on_w18(self):
|
def on_w18(self):
|
||||||
raise ui.Result(18)
|
raise ui.Result(18)
|
||||||
|
|
||||||
|
def on_w20(self):
|
||||||
|
raise ui.Result(20)
|
||||||
|
|
||||||
def on_w24(self):
|
def on_w24(self):
|
||||||
raise ui.Result(24)
|
raise ui.Result(24)
|
||||||
|
|
||||||
|
def on_w33(self):
|
||||||
|
raise ui.Result(33)
|
||||||
|
@ -25,12 +25,15 @@ class TestCryptoSlip39(unittest.TestCase):
|
|||||||
MS = b"ABCDEFGHIJKLMNOP"
|
MS = b"ABCDEFGHIJKLMNOP"
|
||||||
|
|
||||||
def test_basic_sharing_random(self):
|
def test_basic_sharing_random(self):
|
||||||
mnemonics = slip39.generate_mnemonics_random(1, [(3, 5)])[0]
|
ms = random.bytes(32)
|
||||||
|
_, mnemonics = slip39.generate_mnemonics_from_data(ms, 1, [(3, 5)])
|
||||||
|
mnemonics = mnemonics[0]
|
||||||
self.assertEqual(slip39.combine_mnemonics(mnemonics[:3]), slip39.combine_mnemonics(mnemonics[2:]))
|
self.assertEqual(slip39.combine_mnemonics(mnemonics[:3]), slip39.combine_mnemonics(mnemonics[2:]))
|
||||||
|
|
||||||
|
|
||||||
def test_basic_sharing_fixed(self):
|
def test_basic_sharing_fixed(self):
|
||||||
mnemonics = slip39.generate_mnemonics(1, [(3, 5)], self.MS)[0]
|
_, mnemonics = slip39.generate_mnemonics_from_data(self.MS, 1, [(3, 5)])
|
||||||
|
mnemonics = mnemonics[0]
|
||||||
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics[:3])
|
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics[:3])
|
||||||
self.assertEqual(slip39.decrypt(identifier, exponent, ems, b""), self.MS)
|
self.assertEqual(slip39.decrypt(identifier, exponent, ems, b""), self.MS)
|
||||||
self.assertEqual(slip39.combine_mnemonics(mnemonics[1:4])[2], ems)
|
self.assertEqual(slip39.combine_mnemonics(mnemonics[1:4])[2], ems)
|
||||||
@ -39,19 +42,22 @@ class TestCryptoSlip39(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
def test_passphrase(self):
|
def test_passphrase(self):
|
||||||
mnemonics = slip39.generate_mnemonics(1, [(3, 5)], self.MS, b"TREZOR")[0]
|
_, mnemonics = slip39.generate_mnemonics_from_data(self.MS, 1, [(3, 5)], b"TREZOR")
|
||||||
|
mnemonics = mnemonics[0]
|
||||||
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics[1:4])
|
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics[1:4])
|
||||||
self.assertEqual(slip39.decrypt(identifier, exponent, ems, b"TREZOR"), self.MS)
|
self.assertEqual(slip39.decrypt(identifier, exponent, ems, b"TREZOR"), self.MS)
|
||||||
self.assertNotEqual(slip39.decrypt(identifier, exponent, ems, b""), self.MS)
|
self.assertNotEqual(slip39.decrypt(identifier, exponent, ems, b""), self.MS)
|
||||||
|
|
||||||
|
|
||||||
def test_iteration_exponent(self):
|
def test_iteration_exponent(self):
|
||||||
mnemonics = slip39.generate_mnemonics(1, [(3, 5)], self.MS, b"TREZOR", 1)[0]
|
_, mnemonics = slip39.generate_mnemonics_from_data(self.MS, 1, [(3, 5)], b"TREZOR", 1)
|
||||||
|
mnemonics = mnemonics[0]
|
||||||
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics[1:4])
|
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics[1:4])
|
||||||
self.assertEqual(slip39.decrypt(identifier, exponent, ems, b"TREZOR"), self.MS)
|
self.assertEqual(slip39.decrypt(identifier, exponent, ems, b"TREZOR"), self.MS)
|
||||||
self.assertNotEqual(slip39.decrypt(identifier, exponent, ems, b""), self.MS)
|
self.assertNotEqual(slip39.decrypt(identifier, exponent, ems, b""), self.MS)
|
||||||
|
|
||||||
mnemonics = slip39.generate_mnemonics(1, [(3, 5)], self.MS, b"TREZOR", 2)[0]
|
_, mnemonics = slip39.generate_mnemonics_from_data(self.MS, 1, [(3, 5)], b"TREZOR", 2)
|
||||||
|
mnemonics = mnemonics[0]
|
||||||
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics[1:4])
|
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics[1:4])
|
||||||
self.assertEqual(slip39.decrypt(identifier, exponent, ems, b"TREZOR"), self.MS)
|
self.assertEqual(slip39.decrypt(identifier, exponent, ems, b"TREZOR"), self.MS)
|
||||||
self.assertNotEqual(slip39.decrypt(identifier, exponent, ems, b""), self.MS)
|
self.assertNotEqual(slip39.decrypt(identifier, exponent, ems, b""), self.MS)
|
||||||
@ -61,8 +67,8 @@ class TestCryptoSlip39(unittest.TestCase):
|
|||||||
group_threshold = 2
|
group_threshold = 2
|
||||||
group_sizes = (5, 3, 5, 1)
|
group_sizes = (5, 3, 5, 1)
|
||||||
member_thresholds = (3, 2, 2, 1)
|
member_thresholds = (3, 2, 2, 1)
|
||||||
mnemonics = slip39.generate_mnemonics(
|
_, mnemonics = slip39.generate_mnemonics_from_data(
|
||||||
group_threshold, list(zip(member_thresholds, group_sizes)), self.MS
|
self.MS, group_threshold, list(zip(member_thresholds, group_sizes))
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test all valid combinations of mnemonics.
|
# Test all valid combinations of mnemonics.
|
||||||
@ -93,8 +99,8 @@ class TestCryptoSlip39(unittest.TestCase):
|
|||||||
group_threshold = 1
|
group_threshold = 1
|
||||||
group_sizes = (5, 3, 5, 1)
|
group_sizes = (5, 3, 5, 1)
|
||||||
member_thresholds = (3, 2, 2, 1)
|
member_thresholds = (3, 2, 2, 1)
|
||||||
mnemonics = slip39.generate_mnemonics(
|
_, mnemonics = slip39.generate_mnemonics_from_data(
|
||||||
group_threshold, list(zip(member_thresholds, group_sizes)), self.MS
|
self.MS, group_threshold, list(zip(member_thresholds, group_sizes))
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test all valid combinations of mnemonics.
|
# Test all valid combinations of mnemonics.
|
||||||
@ -108,8 +114,8 @@ class TestCryptoSlip39(unittest.TestCase):
|
|||||||
|
|
||||||
def test_all_groups_exist(self):
|
def test_all_groups_exist(self):
|
||||||
for group_threshold in (1, 2, 5):
|
for group_threshold in (1, 2, 5):
|
||||||
mnemonics = slip39.generate_mnemonics(
|
_, mnemonics = slip39.generate_mnemonics_from_data(
|
||||||
group_threshold, [(3, 5), (1, 1), (2, 3), (2, 5), (3, 5)], self.MS
|
self.MS, group_threshold, [(3, 5), (1, 1), (2, 3), (2, 5), (3, 5)]
|
||||||
)
|
)
|
||||||
self.assertEqual(len(mnemonics), 5)
|
self.assertEqual(len(mnemonics), 5)
|
||||||
self.assertEqual(len(sum(mnemonics, [])), 19)
|
self.assertEqual(len(sum(mnemonics, [])), 19)
|
||||||
@ -118,31 +124,31 @@ class TestCryptoSlip39(unittest.TestCase):
|
|||||||
def test_invalid_sharing(self):
|
def test_invalid_sharing(self):
|
||||||
# Short master secret.
|
# Short master secret.
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
slip39.generate_mnemonics(1, [(2, 3)], self.MS[:14])
|
slip39.generate_mnemonics_from_data(self.MS[:14], 1, [(2, 3)])
|
||||||
|
|
||||||
# Odd length master secret.
|
# Odd length master secret.
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
slip39.generate_mnemonics(1, [(2, 3)], self.MS + b"X")
|
slip39.generate_mnemonics_from_data(self.MS + b"X", 1, [(2, 3)])
|
||||||
|
|
||||||
# Group threshold exceeds number of groups.
|
# Group threshold exceeds number of groups.
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
slip39.generate_mnemonics(3, [(3, 5), (2, 5)], self.MS)
|
slip39.generate_mnemonics_from_data(self.MS, 3, [(3, 5), (2, 5)])
|
||||||
|
|
||||||
# Invalid group threshold.
|
# Invalid group threshold.
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
slip39.generate_mnemonics(0, [(3, 5), (2, 5)], self.MS)
|
slip39.generate_mnemonics_from_data(self.MS, 0, [(3, 5), (2, 5)])
|
||||||
|
|
||||||
# Member threshold exceeds number of members.
|
# Member threshold exceeds number of members.
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
slip39.generate_mnemonics(2, [(3, 2), (2, 5)], self.MS)
|
slip39.generate_mnemonics_from_data(self.MS, 2, [(3, 2), (2, 5)])
|
||||||
|
|
||||||
# Invalid member threshold.
|
# Invalid member threshold.
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
slip39.generate_mnemonics(2, [(0, 2), (2, 5)], self.MS)
|
slip39.generate_mnemonics_from_data(self.MS, 2, [(0, 2), (2, 5)])
|
||||||
|
|
||||||
# Group with multiple members and threshold 1.
|
# Group with multiple members and threshold 1.
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
slip39.generate_mnemonics(2, [(3, 5), (1, 3), (2, 5)], self.MS)
|
slip39.generate_mnemonics_from_data(self.MS, 2, [(3, 5), (1, 3), (2, 5)])
|
||||||
|
|
||||||
|
|
||||||
def test_vectors(self):
|
def test_vectors(self):
|
||||||
|
@ -66,6 +66,7 @@ SRCS += memzero.c
|
|||||||
SRCS += shamir.c
|
SRCS += shamir.c
|
||||||
SRCS += hmac_drbg.c
|
SRCS += hmac_drbg.c
|
||||||
SRCS += rfc6979.c
|
SRCS += rfc6979.c
|
||||||
|
SRCS += slip39.c
|
||||||
|
|
||||||
OBJS = $(SRCS:.c=.o)
|
OBJS = $(SRCS:.c=.o)
|
||||||
|
|
||||||
|
136
crypto/slip39.c
Normal file
136
crypto/slip39.c
Normal file
@ -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;
|
||||||
|
}
|
36
crypto/slip39.h
Normal file
36
crypto/slip39.h
Normal file
@ -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);
|
1238
crypto/slip39_wordlist.h
Normal file
1238
crypto/slip39_wordlist.h
Normal file
File diff suppressed because it is too large
Load Diff
@ -67,6 +67,7 @@
|
|||||||
#include "sha2.h"
|
#include "sha2.h"
|
||||||
#include "sha3.h"
|
#include "sha3.h"
|
||||||
#include "shamir.h"
|
#include "shamir.h"
|
||||||
|
#include "slip39.h"
|
||||||
|
|
||||||
#if VALGRIND
|
#if VALGRIND
|
||||||
/*
|
/*
|
||||||
@ -5096,6 +5097,96 @@ START_TEST(test_mnemonic_to_entropy) {
|
|||||||
}
|
}
|
||||||
END_TEST
|
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) {
|
START_TEST(test_shamir) {
|
||||||
#define SHAMIR_MAX_COUNT 16
|
#define SHAMIR_MAX_COUNT 16
|
||||||
static const struct {
|
static const struct {
|
||||||
@ -8676,6 +8767,13 @@ Suite *test_suite(void) {
|
|||||||
tcase_add_test(tc, test_mnemonic_to_entropy);
|
tcase_add_test(tc, test_mnemonic_to_entropy);
|
||||||
suite_add_tcase(s, tc);
|
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");
|
tc = tcase_create("shamir");
|
||||||
tcase_add_test(tc, test_shamir);
|
tcase_add_test(tc, test_shamir);
|
||||||
suite_add_tcase(s, tc);
|
suite_add_tcase(s, tc);
|
||||||
|
@ -505,6 +505,7 @@ def recovery_device(
|
|||||||
@click.option("-u", "--u2f-counter", default=0)
|
@click.option("-u", "--u2f-counter", default=0)
|
||||||
@click.option("-s", "--skip-backup", is_flag=True)
|
@click.option("-s", "--skip-backup", is_flag=True)
|
||||||
@click.option("-n", "--no-backup", is_flag=True)
|
@click.option("-n", "--no-backup", is_flag=True)
|
||||||
|
@click.option("-x", "--slip39", is_flag=True)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def reset_device(
|
def reset_device(
|
||||||
connect,
|
connect,
|
||||||
@ -516,6 +517,7 @@ def reset_device(
|
|||||||
u2f_counter,
|
u2f_counter,
|
||||||
skip_backup,
|
skip_backup,
|
||||||
no_backup,
|
no_backup,
|
||||||
|
slip39,
|
||||||
):
|
):
|
||||||
if strength:
|
if strength:
|
||||||
strength = int(strength)
|
strength = int(strength)
|
||||||
@ -530,6 +532,7 @@ def reset_device(
|
|||||||
u2f_counter=u2f_counter,
|
u2f_counter=u2f_counter,
|
||||||
skip_backup=skip_backup,
|
skip_backup=skip_backup,
|
||||||
no_backup=no_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
|
return ret
|
||||||
|
|
||||||
|
|
||||||
@expect(proto.Success, field="message")
|
|
||||||
def recover(
|
def recover(
|
||||||
client,
|
client,
|
||||||
word_count=24,
|
word_count=24,
|
||||||
@ -168,6 +167,7 @@ def reset(
|
|||||||
u2f_counter=0,
|
u2f_counter=0,
|
||||||
skip_backup=False,
|
skip_backup=False,
|
||||||
no_backup=False,
|
no_backup=False,
|
||||||
|
slip39=False,
|
||||||
):
|
):
|
||||||
if client.features.initialized:
|
if client.features.initialized:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
@ -191,6 +191,7 @@ def reset(
|
|||||||
u2f_counter=u2f_counter,
|
u2f_counter=u2f_counter,
|
||||||
skip_backup=bool(skip_backup),
|
skip_backup=bool(skip_backup),
|
||||||
no_backup=bool(no_backup),
|
no_backup=bool(no_backup),
|
||||||
|
slip39=bool(slip39),
|
||||||
)
|
)
|
||||||
|
|
||||||
resp = client.call(msg)
|
resp = client.call(msg)
|
||||||
|
@ -17,6 +17,7 @@ class ResetDevice(p.MessageType):
|
|||||||
u2f_counter: int = None,
|
u2f_counter: int = None,
|
||||||
skip_backup: bool = None,
|
skip_backup: bool = None,
|
||||||
no_backup: bool = None,
|
no_backup: bool = None,
|
||||||
|
slip39: bool = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.display_random = display_random
|
self.display_random = display_random
|
||||||
self.strength = strength
|
self.strength = strength
|
||||||
@ -27,6 +28,7 @@ class ResetDevice(p.MessageType):
|
|||||||
self.u2f_counter = u2f_counter
|
self.u2f_counter = u2f_counter
|
||||||
self.skip_backup = skip_backup
|
self.skip_backup = skip_backup
|
||||||
self.no_backup = no_backup
|
self.no_backup = no_backup
|
||||||
|
self.slip39 = slip39
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_fields(cls):
|
def get_fields(cls):
|
||||||
@ -40,4 +42,5 @@ class ResetDevice(p.MessageType):
|
|||||||
7: ('u2f_counter', p.UVarintType, 0),
|
7: ('u2f_counter', p.UVarintType, 0),
|
||||||
8: ('skip_backup', p.BoolType, 0),
|
8: ('skip_backup', p.BoolType, 0),
|
||||||
9: ('no_backup', p.BoolType, 0),
|
9: ('no_backup', p.BoolType, 0),
|
||||||
|
10: ('slip39', p.BoolType, 0),
|
||||||
}
|
}
|
||||||
|
@ -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
|
assert btn_code == B.ResetDevice
|
||||||
self.client.debug.press_yes()
|
self.client.debug.press_yes()
|
||||||
|
|
||||||
|
# safety warning
|
||||||
|
btn_code = yield
|
||||||
|
assert btn_code == B.ResetDevice
|
||||||
|
self.client.debug.press_yes()
|
||||||
|
|
||||||
# mnemonic phrases
|
# mnemonic phrases
|
||||||
btn_code = yield
|
btn_code = yield
|
||||||
assert btn_code == B.ResetDevice
|
assert btn_code == B.ResetDevice
|
||||||
@ -64,7 +69,12 @@ class TestMsgResetDeviceT2(TrezorTest):
|
|||||||
index = self.client.debug.state().reset_word_pos
|
index = self.client.debug.state().reset_word_pos
|
||||||
self.client.debug.input(words[index])
|
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
|
btn_code = yield
|
||||||
assert btn_code == B.ResetDevice
|
assert btn_code == B.ResetDevice
|
||||||
self.client.debug.press_yes()
|
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.ButtonRequest(code=B.ResetDevice),
|
||||||
|
proto.ButtonRequest(code=B.ResetDevice),
|
||||||
|
proto.ButtonRequest(code=B.ResetDevice),
|
||||||
proto.Success(),
|
proto.Success(),
|
||||||
proto.Features(),
|
proto.Features(),
|
||||||
]
|
]
|
||||||
@ -138,6 +150,11 @@ class TestMsgResetDeviceT2(TrezorTest):
|
|||||||
assert btn_code == B.ResetDevice
|
assert btn_code == B.ResetDevice
|
||||||
self.client.debug.press_yes()
|
self.client.debug.press_yes()
|
||||||
|
|
||||||
|
# safety warning
|
||||||
|
btn_code = yield
|
||||||
|
assert btn_code == B.ResetDevice
|
||||||
|
self.client.debug.press_yes()
|
||||||
|
|
||||||
# mnemonic phrases
|
# mnemonic phrases
|
||||||
btn_code = yield
|
btn_code = yield
|
||||||
assert btn_code == B.ResetDevice
|
assert btn_code == B.ResetDevice
|
||||||
@ -157,7 +174,12 @@ class TestMsgResetDeviceT2(TrezorTest):
|
|||||||
index = self.client.debug.state().reset_word_pos
|
index = self.client.debug.state().reset_word_pos
|
||||||
self.client.debug.input(words[index])
|
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
|
btn_code = yield
|
||||||
assert btn_code == B.ResetDevice
|
assert btn_code == B.ResetDevice
|
||||||
self.client.debug.press_yes()
|
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.ButtonRequest(code=B.ResetDevice),
|
||||||
|
proto.ButtonRequest(code=B.ResetDevice),
|
||||||
|
proto.ButtonRequest(code=B.ResetDevice),
|
||||||
proto.Success(),
|
proto.Success(),
|
||||||
proto.Features(),
|
proto.Features(),
|
||||||
]
|
]
|
||||||
|
Loading…
Reference in New Issue
Block a user