commit
435d00114c
@ -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,
|
||||
};
|
@ -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'.
|
||||
"""
|
@ -1,56 +0,0 @@
|
||||
from trezor import ui, workflow
|
||||
from trezor.crypto import bip39
|
||||
|
||||
from apps.common import storage
|
||||
|
||||
TYPE_BIP39 = 0
|
||||
|
||||
|
||||
def get() -> (bytes, int):
|
||||
mnemonic_secret = storage.get_mnemonic_secret()
|
||||
mnemonic_type = storage.get_mnemonic_type() or TYPE_BIP39
|
||||
return mnemonic_secret, mnemonic_type
|
||||
|
||||
|
||||
def get_seed(passphrase: str = "", progress_bar=True):
|
||||
secret, mnemonic_type = get()
|
||||
if mnemonic_type == TYPE_BIP39:
|
||||
module = bip39
|
||||
if progress_bar:
|
||||
_start_progress()
|
||||
result = module.seed(secret.decode(), passphrase, _render_progress)
|
||||
_stop_progress()
|
||||
else:
|
||||
result = module.seed(secret.decode(), passphrase)
|
||||
return result
|
||||
|
||||
|
||||
def process(mnemonics: list, mnemonic_type: int):
|
||||
if mnemonic_type == TYPE_BIP39:
|
||||
return mnemonics[0].encode()
|
||||
else:
|
||||
raise RuntimeError("Unknown mnemonic type")
|
||||
|
||||
|
||||
def restore() -> str:
|
||||
secret, mnemonic_type = get()
|
||||
if mnemonic_type == TYPE_BIP39:
|
||||
return secret.decode()
|
||||
|
||||
|
||||
def _start_progress():
|
||||
ui.backlight_fade(ui.BACKLIGHT_DIM)
|
||||
ui.display.clear()
|
||||
ui.header("Please wait")
|
||||
ui.display.refresh()
|
||||
ui.backlight_fade(ui.BACKLIGHT_NORMAL)
|
||||
|
||||
|
||||
def _render_progress(progress: int, total: int):
|
||||
p = 1000 * progress // total
|
||||
ui.display.loader(p, False, 18, ui.WHITE, ui.BG)
|
||||
ui.display.refresh()
|
||||
|
||||
|
||||
def _stop_progress():
|
||||
workflow.restartdefault()
|
@ -0,0 +1,59 @@
|
||||
from micropython import const
|
||||
|
||||
from trezor import ui, wire
|
||||
from trezor.crypto.hashlib import sha256
|
||||
from trezor.messages.Success import Success
|
||||
from trezor.utils import consteq
|
||||
|
||||
from . import bip39, slip39
|
||||
|
||||
from apps.common import storage
|
||||
|
||||
TYPE_BIP39 = const(0)
|
||||
TYPE_SLIP39 = const(1)
|
||||
|
||||
TYPES_WORD_COUNT = {12: bip39, 18: bip39, 24: bip39, 20: slip39, 33: slip39}
|
||||
|
||||
|
||||
def get() -> (bytes, int):
|
||||
mnemonic_secret = storage.get_mnemonic_secret()
|
||||
mnemonic_type = storage.get_mnemonic_type() or TYPE_BIP39
|
||||
return mnemonic_secret, mnemonic_type
|
||||
|
||||
|
||||
def get_seed(passphrase: str = ""):
|
||||
mnemonic_secret, mnemonic_type = get()
|
||||
if mnemonic_type == TYPE_BIP39:
|
||||
return bip39.get_seed(mnemonic_secret, passphrase)
|
||||
elif mnemonic_type == TYPE_SLIP39:
|
||||
return slip39.get_seed(mnemonic_secret, passphrase)
|
||||
|
||||
|
||||
def dry_run(secret: bytes):
|
||||
digest_input = sha256(secret).digest()
|
||||
stored, _ = get()
|
||||
digest_stored = sha256(stored).digest()
|
||||
if consteq(digest_stored, digest_input):
|
||||
return Success(message="The seed is valid and matches the one in the device")
|
||||
else:
|
||||
raise wire.ProcessError(
|
||||
"The seed is valid but does not match the one in the device"
|
||||
)
|
||||
|
||||
|
||||
def module_from_words_count(count: int):
|
||||
return TYPES_WORD_COUNT[count]
|
||||
|
||||
|
||||
def _start_progress():
|
||||
ui.backlight_fade(ui.BACKLIGHT_DIM)
|
||||
ui.display.clear()
|
||||
ui.header("Please wait")
|
||||
ui.display.refresh()
|
||||
ui.backlight_fade(ui.BACKLIGHT_NORMAL)
|
||||
|
||||
|
||||
def _render_progress(progress: int, total: int):
|
||||
p = 1000 * progress // total
|
||||
ui.display.loader(p, False, 18, ui.WHITE, ui.BG)
|
||||
ui.display.refresh()
|
@ -0,0 +1,36 @@
|
||||
from trezor.crypto import bip39
|
||||
|
||||
from apps.common import mnemonic, storage
|
||||
|
||||
|
||||
def get_type():
|
||||
return mnemonic.TYPE_BIP39
|
||||
|
||||
|
||||
def process_single(mnemonic: str) -> bytes:
|
||||
"""
|
||||
Receives single mnemonic and processes it. Returns what is then stored in storage or
|
||||
None if more shares are needed.
|
||||
"""
|
||||
return mnemonic.encode()
|
||||
|
||||
|
||||
def process_all(mnemonics: list) -> bytes:
|
||||
"""
|
||||
Receives all mnemonics (just one in case of BIP-39) and processes it into a master
|
||||
secret which is usually then stored in storage.
|
||||
"""
|
||||
return mnemonics[0].encode()
|
||||
|
||||
|
||||
def store(secret: bytes, needs_backup: bool, no_backup: bool):
|
||||
storage.store_mnemonic(secret, mnemonic.TYPE_BIP39, needs_backup, no_backup)
|
||||
|
||||
|
||||
def get_seed(secret: bytes, passphrase: str):
|
||||
mnemonic._start_progress()
|
||||
return bip39.seed(secret.decode(), passphrase, mnemonic._render_progress)
|
||||
|
||||
|
||||
def check(secret: bytes):
|
||||
return bip39.check(secret)
|
@ -0,0 +1,87 @@
|
||||
from trezor.crypto import slip39
|
||||
|
||||
from apps.common import mnemonic, storage
|
||||
|
||||
|
||||
def generate_from_secret(master_secret: bytes, count: int, threshold: int) -> str:
|
||||
"""
|
||||
Generates new Shamir backup for 'master_secret'. Multiple groups are not yet supported.
|
||||
"""
|
||||
identifier, group_mnemonics = slip39.generate_single_group_mnemonics_from_data(
|
||||
master_secret, threshold, count
|
||||
)
|
||||
storage.set_slip39_iteration_exponent(slip39.DEFAULT_ITERATION_EXPONENT)
|
||||
storage.set_slip39_identifier(identifier)
|
||||
return group_mnemonics
|
||||
|
||||
|
||||
def get_type():
|
||||
return mnemonic.TYPE_SLIP39
|
||||
|
||||
|
||||
def process_single(mnemonic: str) -> bytes:
|
||||
"""
|
||||
Receives single mnemonic and processes it. Returns what is then stored in storage or
|
||||
None if more shares are needed.
|
||||
"""
|
||||
identifier, iteration_exponent, _, _, _, index, threshold, value = slip39.decode_mnemonic(
|
||||
mnemonic
|
||||
) # TODO: use better data structure for this
|
||||
if threshold == 1:
|
||||
raise ValueError("Threshold equal to 1 is not allowed.")
|
||||
|
||||
# if recovery is not in progress already, start it and wait for more mnemonics
|
||||
if not storage.is_slip39_in_progress():
|
||||
storage.set_slip39_in_progress(True)
|
||||
storage.set_slip39_iteration_exponent(iteration_exponent)
|
||||
storage.set_slip39_identifier(identifier)
|
||||
storage.set_slip39_threshold(threshold)
|
||||
storage.set_slip39_remaining(threshold - 1)
|
||||
storage.set_slip39_words_count(len(mnemonic.split()))
|
||||
storage.set_slip39_mnemonic(index, mnemonic)
|
||||
return None # we need more shares
|
||||
|
||||
# check identifier and member index of this share against stored values
|
||||
if identifier != storage.get_slip39_identifier():
|
||||
# TODO: improve UX (tell user)
|
||||
raise ValueError("Share identifiers do not match")
|
||||
if storage.get_slip39_mnemonic(index):
|
||||
# TODO: improve UX (tell user)
|
||||
raise ValueError("This mnemonic was already entered")
|
||||
|
||||
# append to storage
|
||||
remaining = storage.get_slip39_remaining() - 1
|
||||
storage.set_slip39_remaining(remaining)
|
||||
storage.set_slip39_mnemonic(index, mnemonic)
|
||||
if remaining != 0:
|
||||
return None # we need more shares
|
||||
|
||||
# combine shares and return the master secret
|
||||
mnemonics = storage.get_slip39_mnemonics()
|
||||
if len(mnemonics) != threshold:
|
||||
raise ValueError("Some mnemonics are still missing.")
|
||||
_, _, secret = slip39.combine_mnemonics(mnemonics)
|
||||
return secret
|
||||
|
||||
|
||||
def process_all(mnemonics: list) -> bytes:
|
||||
"""
|
||||
Receives all mnemonics and processes it into pre-master secret which is usually then
|
||||
stored in the storage.
|
||||
"""
|
||||
_, _, secret = slip39.combine_mnemonics(mnemonics)
|
||||
return secret
|
||||
|
||||
|
||||
def store(secret: bytes, needs_backup: bool, no_backup: bool):
|
||||
storage.store_mnemonic(secret, mnemonic.TYPE_SLIP39, needs_backup, no_backup)
|
||||
storage.clear_slip39_data()
|
||||
|
||||
|
||||
def get_seed(encrypted_master_secret: bytes, passphrase: str):
|
||||
mnemonic._start_progress()
|
||||
identifier = storage.get_slip39_identifier()
|
||||
iteration_exponent = storage.get_slip39_iteration_exponent()
|
||||
return slip39.decrypt(
|
||||
identifier, iteration_exponent, encrypted_master_secret, passphrase
|
||||
)
|
@ -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
|
File diff suppressed because one or more lines are too long
@ -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
|
@ -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)
|
@ -0,0 +1,205 @@
|
||||
from trezor import io, loop, res, ui
|
||||
from trezor.crypto import slip39
|
||||
from trezor.ui import display
|
||||
from trezor.ui.button import Button, ButtonClear, ButtonMono, ButtonMonoConfirm
|
||||
|
||||
|
||||
def check_mask(mask: int, index: int) -> bool:
|
||||
return bool((1 << (index - 1)) & mask)
|
||||
|
||||
|
||||
# TODO: ask UX if we want to finish sooner than 4 words
|
||||
# example: 'hairy'
|
||||
def _is_final(content: str) -> bool:
|
||||
return len(content) > 3
|
||||
|
||||
|
||||
class KeyButton(Button):
|
||||
def __init__(self, area, content, keyboard, index):
|
||||
self.keyboard = keyboard
|
||||
self.index = index
|
||||
super().__init__(area, content)
|
||||
|
||||
def on_click(self):
|
||||
self.keyboard.on_key_click(self)
|
||||
|
||||
|
||||
class InputButton(Button):
|
||||
def __init__(self, area, content, word):
|
||||
super().__init__(area, content)
|
||||
self.word = word
|
||||
self.pending_button = None
|
||||
self.pending_index = None
|
||||
self.icon = None # rendered icon
|
||||
self.disable()
|
||||
|
||||
def edit(self, content, word, pending_button, pending_index):
|
||||
self.word = word
|
||||
self.content = content
|
||||
self.pending_button = pending_button
|
||||
self.pending_index = pending_index
|
||||
self.repaint = True
|
||||
if word:
|
||||
self.enable()
|
||||
self.normal_style = ButtonMonoConfirm.normal
|
||||
self.active_style = ButtonMonoConfirm.active
|
||||
self.icon = ui.ICON_CONFIRM
|
||||
else: # disabled button
|
||||
self.disabled_style = ButtonMono.normal
|
||||
self.disable()
|
||||
self.icon = None
|
||||
|
||||
def render_content(self, s, ax, ay, aw, ah):
|
||||
text_style = s.text_style
|
||||
fg_color = s.fg_color
|
||||
bg_color = s.bg_color
|
||||
|
||||
tx = ax + 24 # x-offset of the content
|
||||
ty = ay + ah // 2 + 8 # y-offset of the content
|
||||
|
||||
if not _is_final(self.content):
|
||||
to_display = len(self.content) * "*"
|
||||
if self.pending_button:
|
||||
to_display = (
|
||||
to_display[:-1] + self.pending_button.content[self.pending_index]
|
||||
)
|
||||
else:
|
||||
to_display = self.word
|
||||
|
||||
display.text(tx, ty, to_display, text_style, fg_color, bg_color)
|
||||
|
||||
if self.pending_button and not _is_final(self.content):
|
||||
width = display.text_width(to_display, text_style)
|
||||
pw = display.text_width(self.content[-1:], text_style)
|
||||
px = tx + width - pw
|
||||
display.bar(px, ty + 2, pw + 1, 3, fg_color)
|
||||
|
||||
if self.icon:
|
||||
ix = ax + aw - 16 * 2
|
||||
iy = ty - 16
|
||||
display.icon(ix, iy, res.load(self.icon), fg_color, bg_color)
|
||||
|
||||
|
||||
class Prompt(ui.Control):
|
||||
def __init__(self, prompt):
|
||||
self.prompt = prompt
|
||||
self.repaint = True
|
||||
|
||||
def on_render(self):
|
||||
if self.repaint:
|
||||
display.bar(0, 8, ui.WIDTH, 60, ui.BG)
|
||||
display.text(20, 40, self.prompt, ui.BOLD, ui.GREY, ui.BG)
|
||||
self.repaint = False
|
||||
|
||||
|
||||
class Slip39Keyboard(ui.Layout):
|
||||
def __init__(self, prompt):
|
||||
self.prompt = Prompt(prompt)
|
||||
|
||||
icon_back = res.load(ui.ICON_BACK)
|
||||
self.back = Button(ui.grid(0, n_x=4, n_y=4), icon_back, ButtonClear)
|
||||
self.back.on_click = self.on_back_click
|
||||
|
||||
self.input = InputButton(ui.grid(1, n_x=4, n_y=4, cells_x=3), "", "")
|
||||
self.input.on_click = self.on_input_click
|
||||
|
||||
self.keys = [
|
||||
KeyButton(ui.grid(i + 3, n_y=4), k, self, i + 1)
|
||||
for i, k in enumerate(
|
||||
("ab", "cd", "ef", "ghij", "klm", "nopq", "rs", "tuv", "wxyz")
|
||||
)
|
||||
]
|
||||
self.pending_button = None
|
||||
self.pending_index = 0
|
||||
self.button_sequence = ""
|
||||
|
||||
def dispatch(self, event: int, x: int, y: int):
|
||||
for btn in self.keys:
|
||||
btn.dispatch(event, x, y)
|
||||
if self.input.content:
|
||||
self.input.dispatch(event, x, y)
|
||||
self.back.dispatch(event, x, y)
|
||||
else:
|
||||
self.prompt.dispatch(event, x, y)
|
||||
|
||||
def on_back_click(self):
|
||||
# Backspace was clicked, let's delete the last character of input.
|
||||
self.button_sequence = self.button_sequence[:-1]
|
||||
self.edit()
|
||||
|
||||
def on_input_click(self):
|
||||
# Input button was clicked. If the content matches the suggested word,
|
||||
# let's confirm it, otherwise just auto-complete.
|
||||
result = self.input.word
|
||||
if _is_final(self.input.content):
|
||||
self.button_sequence = ""
|
||||
self.edit()
|
||||
self.on_confirm(result)
|
||||
|
||||
def on_key_click(self, btn: KeyButton):
|
||||
# Key button was clicked. If this button is pending, let's cycle the
|
||||
# pending character in input. If not, let's just append the first
|
||||
# character.
|
||||
if self.pending_button is btn:
|
||||
index = (self.pending_index + 1) % len(btn.content)
|
||||
else:
|
||||
index = 0
|
||||
self.button_sequence += str(btn.index)
|
||||
self.edit(btn, index)
|
||||
|
||||
def on_timeout(self):
|
||||
# Timeout occurred. Let's redraw to draw asterisks.
|
||||
self.edit()
|
||||
|
||||
def on_confirm(self, word):
|
||||
# Word was confirmed by the user.
|
||||
raise ui.Result(word)
|
||||
|
||||
def edit(self, button: KeyButton = None, index: int = 0):
|
||||
self.pending_button = button
|
||||
self.pending_index = index
|
||||
|
||||
# find the completions
|
||||
mask = 0
|
||||
word = ""
|
||||
if _is_final(self.button_sequence):
|
||||
word = slip39.button_sequence_to_word(self.button_sequence)
|
||||
else:
|
||||
mask = slip39.compute_mask(self.button_sequence)
|
||||
|
||||
# modify the input state
|
||||
self.input.edit(
|
||||
self.button_sequence, word, self.pending_button, self.pending_index
|
||||
)
|
||||
|
||||
# enable or disable key buttons
|
||||
for btn in self.keys:
|
||||
if (not _is_final(self.button_sequence) and btn is button) or check_mask(
|
||||
mask, btn.index
|
||||
):
|
||||
btn.enable()
|
||||
else:
|
||||
btn.disable()
|
||||
|
||||
# invalidate the prompt if we display it next frame
|
||||
if not self.input.content:
|
||||
self.prompt.repaint = True
|
||||
|
||||
async def handle_input(self):
|
||||
touch = loop.wait(io.TOUCH)
|
||||
timeout = loop.sleep(1000 * 1000 * 1)
|
||||
spawn_touch = loop.spawn(touch)
|
||||
spawn_timeout = loop.spawn(touch, timeout)
|
||||
|
||||
while True:
|
||||
if self.pending_button is not None:
|
||||
spawn = spawn_timeout
|
||||
else:
|
||||
spawn = spawn_touch
|
||||
result = await spawn
|
||||
|
||||
if touch in spawn.finished:
|
||||
event, x, y = result
|
||||
self.dispatch(event, x, y)
|
||||
else:
|
||||
self.on_timeout()
|
@ -0,0 +1,49 @@
|
||||
from trezor import ui
|
||||
from trezor.ui.button import Button
|
||||
from trezor.ui.text import LABEL_CENTER, Label
|
||||
|
||||
|
||||
class NumInput(ui.Control):
|
||||
def __init__(self, count=5, max_count=16, min_count=1):
|
||||
self.count = count
|
||||
self.max_count = max_count
|
||||
self.min_count = min_count
|
||||
|
||||
self.minus = Button(ui.grid(3), "-")
|
||||
self.minus.on_click = self.on_minus
|
||||
self.plus = Button(ui.grid(5), "+")
|
||||
self.plus.on_click = self.on_plus
|
||||
self.text = Label(ui.grid(4), "", LABEL_CENTER, ui.BOLD)
|
||||
|
||||
self.edit(count)
|
||||
|
||||
def dispatch(self, event, x, y):
|
||||
self.minus.dispatch(event, x, y)
|
||||
self.plus.dispatch(event, x, y)
|
||||
self.text.dispatch(event, x, y)
|
||||
|
||||
def on_minus(self):
|
||||
self.edit(self.count - 1)
|
||||
|
||||
def on_plus(self):
|
||||
self.edit(self.count + 1)
|
||||
|
||||
def edit(self, count):
|
||||
count = max(count, self.min_count)
|
||||
count = min(count, self.max_count)
|
||||
if self.count != count:
|
||||
self.on_change(count)
|
||||
self.count = count
|
||||
self.text.content = str(count)
|
||||
self.text.repaint = True
|
||||
if self.count == self.min_count:
|
||||
self.minus.disable()
|
||||
else:
|
||||
self.minus.enable()
|
||||
if self.count == self.max_count:
|
||||
self.plus.disable()
|
||||
else:
|
||||
self.plus.enable()
|
||||
|
||||
def on_change(self, count):
|
||||
pass
|
@ -0,0 +1,136 @@
|
||||
/**
|
||||
* This file is part of the TREZOR project, https://trezor.io/
|
||||
*
|
||||
* Copyright (c) SatoshiLabs
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the "Software"),
|
||||
* to deal in the Software without restriction, including without limitation
|
||||
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included
|
||||
* in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
||||
* OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "slip39.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "slip39_wordlist.h"
|
||||
|
||||
/**
|
||||
* Returns word on position `index`.
|
||||
*/
|
||||
const char* get_word(uint16_t index) { return wordlist[index]; }
|
||||
|
||||
/**
|
||||
* Finds index of given word, if found.
|
||||
* Returns true on success and stores result in `index`.
|
||||
*/
|
||||
bool word_index(uint16_t* index, const char* word, uint8_t word_length) {
|
||||
uint16_t lo = 0;
|
||||
uint16_t hi = WORDS_COUNT;
|
||||
uint16_t mid = 0;
|
||||
|
||||
while ((hi - lo) > 1) {
|
||||
mid = (hi + lo) / 2;
|
||||
if (strncmp(wordlist[mid], word, word_length) > 0) {
|
||||
hi = mid;
|
||||
} else {
|
||||
lo = mid;
|
||||
}
|
||||
}
|
||||
if (strncmp(wordlist[lo], word, word_length) != 0) {
|
||||
return false;
|
||||
}
|
||||
*index = lo;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates which buttons still can be pressed after some already were.
|
||||
* Returns a 9-bit bitmask, where each bit specifies which buttons
|
||||
* can be further pressed (there are still words in this combination).
|
||||
* LSB denotes first button.
|
||||
*
|
||||
* Example: 110000110 - second, third, eighth and ninth button still can be
|
||||
* pressed.
|
||||
*/
|
||||
uint16_t compute_mask(uint16_t prefix) { return find(prefix, false); }
|
||||
|
||||
/**
|
||||
* Converts sequence to word index.
|
||||
*/
|
||||
const char* button_sequence_to_word(uint16_t prefix) {
|
||||
return wordlist[find(prefix, true)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes mask if find_index is false.
|
||||
* Finds the first word index that suits the prefix otherwise.
|
||||
*/
|
||||
uint16_t find(uint16_t prefix, bool find_index) {
|
||||
uint16_t min = prefix;
|
||||
uint16_t max = 0;
|
||||
uint16_t for_max = 0;
|
||||
uint8_t multiplier = 0;
|
||||
uint8_t divider = 0;
|
||||
uint16_t bitmap = 0;
|
||||
uint8_t digit = 0;
|
||||
uint16_t i = 0;
|
||||
|
||||
max = prefix + 1;
|
||||
while (min < 1000) {
|
||||
min = min * 10;
|
||||
max = max * 10;
|
||||
multiplier++;
|
||||
}
|
||||
|
||||
// Four char prefix -> the mask is zero
|
||||
if (!multiplier && !find_index) {
|
||||
return 0;
|
||||
}
|
||||
for_max = min - (min % 1000) + 1000;
|
||||
|
||||
// We can't use binary search because the numbers are not sorted.
|
||||
// They are sorted using the words' alphabet (so we can use the index).
|
||||
// Example: axle (1953), beam (1315)
|
||||
// The first digit is sorted so we can loop only upto `for_max`.
|
||||
while (words_button_seq[i] < for_max) {
|
||||
if (words_button_seq[i] >= min && words_button_seq[i] < max) {
|
||||
if (find_index) {
|
||||
return i;
|
||||
}
|
||||
|
||||
switch (multiplier) {
|
||||
case 1:
|
||||
divider = 1;
|
||||
break;
|
||||
case 2:
|
||||
divider = 10;
|
||||
break;
|
||||
case 3:
|
||||
divider = 100;
|
||||
break;
|
||||
default:
|
||||
divider = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
digit = (words_button_seq[i] / divider) % 10;
|
||||
bitmap |= 1 << (digit - 1);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* This file is part of the TREZOR project, https://trezor.io/
|
||||
*
|
||||
* Copyright (c) SatoshiLabs
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the "Software"),
|
||||
* to deal in the Software without restriction, including without limitation
|
||||
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included
|
||||
* in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
||||
* OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
const char* get_word(uint16_t index);
|
||||
|
||||
bool word_index(uint16_t* index, const char* word, uint8_t word_length);
|
||||
|
||||
uint16_t compute_mask(uint16_t prefix);
|
||||
|
||||
const char* button_sequence_to_word(uint16_t prefix);
|
||||
|
||||
uint16_t find(uint16_t prefix, bool find_index);
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -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"
|
Loading…
Reference in new issue