core: refactor slip39

pull/541/head
Tomas Susanka 5 years ago committed by matejcik
parent 5628d1254d
commit cefb1cf4fd

@ -7,6 +7,15 @@ option java_outer_classname = "TrezorMessageManagement";
import "messages-common.proto";
/**
* Type of the mnemonic backup given/received by the device during reset/recovery.
*/
enum BackupType {
Bip39 = 0; // also called "Single Backup", see BIP-0039
Slip39_Basic = 1; // also called "Shamir Backup", see SLIP-0039
Slip39_Advanced = 2; // also called "Super Shamir" or "Shamir with Groups", see SLIP-0039#two-level-scheme
}
/**
* Request: Reset device to default state and ask for device details
* @start
@ -58,13 +67,13 @@ message Features {
optional bool unfinished_backup = 27; // report unfinished backup (equals to Storage.unfinished_backup)
optional bool no_backup = 28; // report no backup (equals to Storage.no_backup)
optional bool recovery_mode = 29; // is recovery mode in progress
repeated Capability capabilities = 30; // list of supported capabilities
repeated Capability capabilities = 30; // list of supported capabilities
enum Capability {
Capability_Bitcoin = 1;
Capability_Bitcoin_like = 2; // Altcoins based on the Bitcoin source code
Capability_Bitcoin_like = 2; // Altcoins based on the Bitcoin source code
Capability_Binance = 3;
Capability_Cardano = 4;
Capability_Crypto = 5; // generic crypto operations for GPG, SSH, etc.
Capability_Crypto = 5; // generic crypto operations for GPG, SSH, etc.
Capability_EOS = 6;
Capability_Ethereum = 7;
Capability_Lisk = 8;
@ -77,6 +86,7 @@ message Features {
Capability_Shamir = 15;
Capability_ShamirGroups = 16;
}
optional BackupType backup_type = 31; // type of device backup (BIP-39 / SLIP-39 basic / SLIP-39 advanced)
}
/**
@ -229,14 +239,8 @@ message ResetDevice {
optional uint32 u2f_counter = 7; // U2F counter
optional bool skip_backup = 8; // postpone seed backup to BackupDevice workflow
optional bool no_backup = 9; // indicate that no backup is going to be made
// type of the mnemonic backup (BIP-39 vs SLIP-39 single group vs SLIP-39 multiple groups)
optional ResetDeviceBackupType backup_type = 10 [default=ResetDeviceBackupType_Bip39];
enum ResetDeviceBackupType {
ResetDeviceBackupType_Bip39 = 0; // The traditional Single Backup - BIP39
ResetDeviceBackupType_Slip39_Single_Group = 1; // Shamir Backup with only single group (one-level Shamir)
ResetDeviceBackupType_Slip39_Multiple_Groups = 2; // Shamir Backup with multiple groups (two-level Shamir)
}
// type of the mnemonic backup
optional BackupType backup_type = 10 [default=Bip39];
}
/**

@ -1,5 +1,6 @@
Version 2.x.x [not yet released]
* nothing yet
* Refactor Shamir related codebase
* Introduce BackupType in Features
Version 2.1.5 [Sep 2019]
* Introduce Bitcoin-only firmware

@ -41,20 +41,17 @@ async def get_keychain(ctx: wire.Context) -> Keychain:
if not storage.is_initialized():
raise wire.ProcessError("Device is not initialized")
if (
mnemonic.get_type() == mnemonic.TYPE_SLIP39
or mnemonic.get_type() == mnemonic.TYPE_SLIP39_GROUP
):
if mnemonic.is_bip39():
# derive the root node from mnemonic and passphrase
passphrase = await _get_passphrase(ctx)
root = bip32.from_mnemonic_cardano(mnemonic.get_secret().decode(), passphrase)
else:
seed = cache.get_seed()
if seed is None:
passphrase = await _get_passphrase(ctx)
seed = mnemonic.get_seed(passphrase)
cache.set_seed(seed)
root = bip32.from_seed(seed, "ed25519 cardano seed")
else:
# derive the root node from mnemonic and passphrase
passphrase = await _get_passphrase(ctx)
root = bip32.from_mnemonic_cardano(mnemonic.get_secret().decode(), passphrase)
# derive the namespaced root node
for i in SEED_NAMESPACE:

@ -2,13 +2,13 @@ from trezor import wire
from trezor.messages import ButtonRequestType
from trezor.messages.ButtonAck import ButtonAck
from trezor.messages.ButtonRequest import ButtonRequest
from trezor.ui.confirm import CONFIRMED, Confirm, HoldToConfirm
from trezor.ui.confirm import CONFIRMED, INFO, Confirm, HoldToConfirm, InfoConfirm
if __debug__:
from apps.debug import confirm_signal
if False:
from typing import Any
from typing import Any, Callable
from trezor import ui
from trezor.ui.confirm import ButtonContent, ButtonStyleType
from trezor.ui.loader import LoaderStyleType
@ -47,6 +47,37 @@ async def confirm(
return await ctx.wait(dialog) is CONFIRMED
async def info_confirm(
ctx: wire.Context,
content: ui.Component,
info_func: Callable,
code: int = ButtonRequestType.Other,
confirm: ButtonContent = InfoConfirm.DEFAULT_CONFIRM,
confirm_style: ButtonStyleType = InfoConfirm.DEFAULT_CONFIRM_STYLE,
cancel: ButtonContent = InfoConfirm.DEFAULT_CANCEL,
cancel_style: ButtonStyleType = InfoConfirm.DEFAULT_CANCEL_STYLE,
info: ButtonContent = InfoConfirm.DEFAULT_INFO,
info_style: ButtonStyleType = InfoConfirm.DEFAULT_INFO_STYLE,
) -> bool:
await ctx.call(ButtonRequest(code=code), ButtonAck)
dialog = InfoConfirm(
content, confirm, confirm_style, cancel, cancel_style, info, info_style
)
while True:
if __debug__:
result = await ctx.wait(dialog, confirm_signal())
else:
result = await ctx.wait(dialog)
if result is INFO:
await info_func(ctx)
else:
return result is CONFIRMED
async def hold_to_confirm(
ctx: wire.Context,
content: ui.Component,

@ -1,24 +1,12 @@
from micropython import const
from trezor import ui, workflow
from trezor.crypto import bip39, slip39
from trezor.messages import BackupType
from apps.common import storage
if False:
from typing import Optional, Tuple
TYPE_BIP39 = const(0)
TYPE_SLIP39 = const(1)
TYPE_SLIP39_GROUP = const(2)
TYPES_WORD_COUNT = {
12: TYPE_BIP39,
18: TYPE_BIP39,
24: TYPE_BIP39,
20: TYPE_SLIP39,
33: TYPE_SLIP39,
}
from trezor.messages.ResetDevice import EnumTypeBackupType
def get() -> Tuple[Optional[bytes], int]:
@ -29,15 +17,20 @@ def get_secret() -> Optional[bytes]:
return storage.device.get_mnemonic_secret()
def get_type() -> int:
mnemonic_type = storage.device.get_mnemonic_type() or TYPE_BIP39
if mnemonic_type not in (TYPE_BIP39, TYPE_SLIP39, TYPE_SLIP39_GROUP):
raise RuntimeError("Invalid mnemonic type")
return mnemonic_type
def get_type() -> EnumTypeBackupType:
return storage.device.get_backup_type()
def is_bip39() -> bool:
"""
If False then SLIP-39 (either Basic or Advanced).
Other invalid values are checked directly in storage.
"""
return get_type() == BackupType.Bip39
def get_seed(passphrase: str = "", progress_bar: bool = True) -> bytes:
mnemonic_secret, mnemonic_type = get()
mnemonic_secret = get_secret()
if mnemonic_secret is None:
raise ValueError("Mnemonic not set")
@ -46,10 +39,10 @@ def get_seed(passphrase: str = "", progress_bar: bool = True) -> bytes:
_start_progress()
render_func = _render_progress
if mnemonic_type == TYPE_BIP39:
if is_bip39():
seed = bip39.seed(mnemonic_secret.decode(), passphrase, render_func)
elif mnemonic_type == TYPE_SLIP39 or mnemonic_type == TYPE_SLIP39_GROUP:
else: # SLIP-39
identifier = storage.device.get_slip39_identifier()
iteration_exponent = storage.device.get_slip39_iteration_exponent()
if identifier is None or iteration_exponent is None:
@ -59,17 +52,9 @@ def get_seed(passphrase: str = "", progress_bar: bool = True) -> bytes:
identifier, iteration_exponent, mnemonic_secret, passphrase.encode()
)
if progress_bar:
_stop_progress()
return seed
def type_from_word_count(count: int) -> int:
if count not in TYPES_WORD_COUNT:
raise RuntimeError("Recovery: Unknown words count")
return TYPES_WORD_COUNT[count]
def _start_progress() -> None:
# Because we are drawing to the screen manually, without a layout, we
# should make sure that no other layout is running. At this point, only
@ -86,7 +71,3 @@ def _render_progress(progress: int, total: int) -> None:
p = 1000 * progress // total
ui.display.loader(p, False, 18, ui.WHITE, ui.BG)
ui.display.refresh()
def _stop_progress() -> None:
pass

@ -2,11 +2,13 @@ from micropython import const
from ubinascii import hexlify
from trezor.crypto import random
from trezor.messages import BackupType
from apps.common.sd_salt import SD_SALT_AUTH_KEY_LEN_BYTES
from apps.common.storage import common
if False:
from trezor.messages.ResetDevice import EnumTypeBackupType
from typing import Optional
# Namespace:
@ -28,11 +30,13 @@ _PASSPHRASE_SOURCE = const(0x0A) # int
_UNFINISHED_BACKUP = const(0x0B) # bool (0x01 or empty)
_AUTOLOCK_DELAY_MS = const(0x0C) # int
_NO_BACKUP = const(0x0D) # bool (0x01 or empty)
_MNEMONIC_TYPE = const(0x0E) # int
_BACKUP_TYPE = const(0x0E) # int
_ROTATION = const(0x0F) # int
_SLIP39_IDENTIFIER = const(0x10) # bool
_SLIP39_ITERATION_EXPONENT = const(0x11) # int
_SD_SALT_AUTH_KEY = const(0x12) # bytes
_DEFAULT_BACKUP_TYPE = BackupType.Bip39
# fmt: on
HOMESCREEN_MAXSIZE = 16384
@ -80,8 +84,19 @@ def get_mnemonic_secret() -> Optional[bytes]:
return common.get(_NAMESPACE, _MNEMONIC_SECRET)
def get_mnemonic_type() -> Optional[int]:
return common.get_uint8(_NAMESPACE, _MNEMONIC_TYPE)
def get_backup_type() -> EnumTypeBackupType:
backup_type = common.get_uint8(_NAMESPACE, _BACKUP_TYPE)
if backup_type is None:
backup_type = _DEFAULT_BACKUP_TYPE
if backup_type not in (
BackupType.Bip39,
BackupType.Slip39_Basic,
BackupType.Slip39_Advanced,
):
# Invalid backup type
raise RuntimeError
return backup_type
def has_passphrase() -> bool:
@ -94,13 +109,13 @@ def get_homescreen() -> Optional[bytes]:
def store_mnemonic_secret(
secret: bytes,
mnemonic_type: int,
backup_type: EnumTypeBackupType,
needs_backup: bool = False,
no_backup: bool = False,
) -> None:
set_version(common.STORAGE_VERSION_CURRENT)
common.set(_NAMESPACE, _MNEMONIC_SECRET, secret)
common.set_uint8(_NAMESPACE, _MNEMONIC_TYPE, mnemonic_type)
common.set_uint8(_NAMESPACE, _BACKUP_TYPE, backup_type)
common.set_true_or_delete(_NAMESPACE, _NO_BACKUP, no_backup)
if not no_backup:
common.set_true_or_delete(_NAMESPACE, _NEEDS_BACKUP, needs_backup)

@ -4,6 +4,9 @@ from trezor.crypto import slip39
from apps.common.storage import common, recovery_shares
if False:
from trezor.messages.ResetDevice import EnumTypeBackupType
# Namespace:
_NAMESPACE = common.APP_RECOVERY
@ -12,12 +15,13 @@ _NAMESPACE = common.APP_RECOVERY
_IN_PROGRESS = const(0x00) # bool
_DRY_RUN = const(0x01) # bool
_WORD_COUNT = const(0x02) # int
_REMAINING = const(0x05) # int
_SLIP39_IDENTIFIER = const(0x03) # bytes
_SLIP39_THRESHOLD = const(0x04) # int
_REMAINING = const(0x05) # int
_SLIP39_ITERATION_EXPONENT = const(0x06) # int
_SLIP39_GROUP_COUNT = const(0x07) # int
_SLIP39_GROUP_THRESHOLD = const(0x08) # int
_BACKUP_TYPE = const(0x09) # int
# fmt: on
if False:
@ -48,6 +52,14 @@ def get_word_count() -> Optional[int]:
return common.get_uint8(_NAMESPACE, _WORD_COUNT)
def set_backup_type(backup_type: EnumTypeBackupType) -> None:
common.set_uint8(_NAMESPACE, _BACKUP_TYPE, backup_type)
def get_backup_type() -> Optional[EnumTypeBackupType]:
return common.get_uint8(_NAMESPACE, _BACKUP_TYPE)
def set_slip39_identifier(identifier: int) -> None:
common.set_uint16(_NAMESPACE, _SLIP39_IDENTIFIER, identifier)
@ -88,7 +100,7 @@ def get_slip39_group_threshold() -> Optional[int]:
return common.get_uint8(_NAMESPACE, _SLIP39_GROUP_THRESHOLD)
def set_slip39_remaining_shares(shares_remaining: int, group_index: int = 0) -> None:
def set_slip39_remaining_shares(shares_remaining: int, group_index: int) -> None:
"""
We store the remaining shares as a bytearray of length group_count.
Each byte represents share remaining for group of that group_index.
@ -96,16 +108,17 @@ def set_slip39_remaining_shares(shares_remaining: int, group_index: int = 0) ->
share count for a group.
"""
remaining = common.get(_NAMESPACE, _REMAINING)
if not get_slip39_group_count():
raise RuntimeError()
group_count = get_slip39_group_count()
if not group_count:
raise RuntimeError
if remaining is None:
remaining = bytearray([slip39.MAX_SHARE_COUNT] * get_slip39_group_count())
remaining = bytearray([slip39.MAX_SHARE_COUNT] * group_count)
remaining = bytearray(remaining)
remaining[group_index] = shares_remaining
common.set(_NAMESPACE, _REMAINING, remaining)
def get_slip39_remaining_shares(group_index: int = 0) -> Optional[int]:
def get_slip39_remaining_shares(group_index: int) -> Optional[int]:
remaining = common.get(_NAMESPACE, _REMAINING)
if remaining is None or remaining[group_index] == slip39.MAX_SHARE_COUNT:
return None
@ -135,4 +148,5 @@ def end_progress() -> None:
common.delete(_NAMESPACE, _SLIP39_ITERATION_EXPONENT)
common.delete(_NAMESPACE, _SLIP39_GROUP_COUNT)
common.delete(_NAMESPACE, _SLIP39_GROUP_THRESHOLD)
common.delete(_NAMESPACE, _BACKUP_TYPE)
recovery_shares.delete()

@ -20,20 +20,19 @@ def get(index: int) -> Optional[str]:
return None
def fetch() -> List[str]:
def fetch() -> List[List[str]]:
mnemonics = []
if not recovery.get_slip39_group_count():
raise RuntimeError
for index in range(0, slip39.MAX_SHARE_COUNT * recovery.get_slip39_group_count()):
m = get(index)
if m:
mnemonics.append(m)
return mnemonics
for i in range(recovery.get_slip39_group_count()):
mnemonics.append(fetch_group(i))
return mnemonics
def fetch_group(group_index: int) -> List[str]:
mnemonics = []
starting_index = 0 + group_index * slip39.MAX_SHARE_COUNT
starting_index = group_index * slip39.MAX_SHARE_COUNT
for index in range(starting_index, starting_index + slip39.MAX_SHARE_COUNT):
m = get(index)
if m:
@ -43,5 +42,5 @@ def fetch_group(group_index: int) -> List[str]:
def delete() -> None:
for index in range(0, slip39.MAX_SHARE_COUNT):
for index in range(slip39.MAX_SHARE_COUNT * slip39.MAX_GROUP_COUNT):
common.delete(common.APP_RECOVERY_SHARES, index)

@ -4,7 +4,7 @@ from trezor.messages.Features import Features
from trezor.messages.Success import Success
from trezor.wire import register
from apps.common import cache, storage
from apps.common import cache, mnemonic, storage
if False:
from typing import NoReturn
@ -36,6 +36,7 @@ def get_features() -> Features:
f.no_backup = storage.device.no_backup()
f.flags = storage.device.get_flags()
f.recovery_mode = storage.recovery.is_in_progress()
f.backup_type = mnemonic.get_type()
if utils.BITCOIN_ONLY:
f.capabilities = [
Capability.Bitcoin,

@ -2,11 +2,7 @@ from trezor import wire
from trezor.messages.Success import Success
from apps.common import mnemonic, storage
from apps.management.common import layout
from apps.management.reset_device import (
backup_group_slip39_wallet,
backup_slip39_wallet,
)
from apps.management.reset_device import backup_seed, layout
async def backup_device(ctx, msg):
@ -20,12 +16,7 @@ async def backup_device(ctx, msg):
storage.device.set_unfinished_backup(True)
storage.device.set_backed_up()
if mnemonic_type == mnemonic.TYPE_SLIP39:
await backup_slip39_wallet(ctx, mnemonic_secret)
elif mnemonic_type == mnemonic.TYPE_SLIP39_GROUP:
await backup_group_slip39_wallet(ctx, mnemonic_secret)
else:
await layout.bip39_show_and_confirm_mnemonic(ctx, mnemonic_secret.decode())
await backup_seed(ctx, mnemonic_type, mnemonic_secret)
storage.device.set_unfinished_backup(False)

@ -0,0 +1,25 @@
from trezor.messages import BackupType
if False:
from trezor.messages.ResetDevice import EnumTypeBackupType
_BIP39_WORD_COUNTS = (12, 18, 24)
_SLIP39_WORD_COUNTS = (20, 33)
def is_slip39_word_count(word_count: int) -> bool:
"""
Returns True for SLIP-39 and False for BIP-39.
Raise RuntimeError otherwise.
"""
if word_count in _SLIP39_WORD_COUNTS:
return True
elif word_count in _BIP39_WORD_COUNTS:
return False
# Unknown word count.
raise RuntimeError
def is_slip39_backup_type(backup_type: EnumTypeBackupType):
return backup_type in (BackupType.Slip39_Basic, BackupType.Slip39_Advanced)

@ -1,14 +1,53 @@
from trezor import config, wire
from trezor.crypto import bip39, slip39
from trezor.messages import BackupType
from trezor.messages.Success import Success
from trezor.pin import pin_to_int
from trezor.ui.text import Text
from apps.common import mnemonic, storage
from apps.common import storage
from apps.common.confirm import require_confirm
from apps.management import backup_types
async def load_device(ctx, msg):
word_count = _validate(msg)
is_slip39 = backup_types.is_slip39_word_count(word_count)
if not is_slip39 and not msg.skip_checksum and not bip39.check(msg.mnemonics[0]):
raise wire.ProcessError("Mnemonic is not valid")
await _warn(ctx)
if not is_slip39: # BIP-39
secret = msg.mnemonics[0].encode()
backup_type = BackupType.Bip39
else:
identifier, iteration_exponent, secret, group_count = slip39.combine_mnemonics(
msg.mnemonics
)
if group_count == 1:
backup_type = BackupType.Slip39_Basic
elif group_count > 1:
backup_type = BackupType.Slip39_Advanced
else:
raise RuntimeError("Invalid group count")
storage.device.set_slip39_identifier(identifier)
storage.device.set_slip39_iteration_exponent(iteration_exponent)
storage.device.store_mnemonic_secret(
secret, backup_type, needs_backup=True, no_backup=False
)
storage.device.load_settings(
use_passphrase=msg.passphrase_protection, label=msg.label
)
if msg.pin:
config.change_pin(pin_to_int(""), pin_to_int(msg.pin), None, None)
return Success(message="Device loaded")
def _validate(msg) -> int:
if storage.is_initialized():
raise wire.UnexpectedMessage("Already initialized")
@ -25,36 +64,11 @@ async def load_device(ctx, msg):
"All shares are required to have the same number of words"
)
mnemonic_type = mnemonic.type_from_word_count(word_count)
return word_count
if (
mnemonic_type == mnemonic.TYPE_BIP39
and not msg.skip_checksum
and not bip39.check(msg.mnemonics[0])
):
raise wire.ProcessError("Mnemonic is not valid")
async def _warn(ctx: wire.Context):
text = Text("Loading seed")
text.bold("Loading private seed", "is not recommended.")
text.normal("Continue only if you", "know what you are doing!")
await require_confirm(ctx, text)
if mnemonic_type == mnemonic.TYPE_BIP39:
secret = msg.mnemonics[0].encode()
elif mnemonic_type == mnemonic.TYPE_SLIP39:
identifier, iteration_exponent, secret = slip39.combine_mnemonics(msg.mnemonics)
storage.device.set_slip39_identifier(identifier)
storage.device.set_slip39_iteration_exponent(iteration_exponent)
else:
raise RuntimeError("Unknown mnemonic type")
storage.device.store_mnemonic_secret(
secret, mnemonic_type, needs_backup=True, no_backup=False
)
storage.device.load_settings(
use_passphrase=msg.passphrase_protection, label=msg.label
)
if msg.pin:
config.change_pin(pin_to_int(""), pin_to_int(msg.pin), None, None)
return Success(message="Device loaded")

@ -1,21 +1,20 @@
from trezor import loop, utils, wire
from trezor.crypto.hashlib import sha256
from trezor.errors import (
GroupThresholdReachedError,
IdentifierMismatchError,
MnemonicError,
ShareAlreadyAddedError,
)
from trezor.crypto.slip39 import MAX_SHARE_COUNT, Share
from trezor.errors import MnemonicError
from trezor.messages import BackupType
from trezor.messages.Success import Success
from . import recover
from apps.common import mnemonic, storage
from apps.common.layout import show_success
from apps.management import backup_types
from apps.management.recovery_device import layout
if False:
from typing import List
from typing import Optional, Tuple
from trezor.messages.ResetDevice import EnumTypeBackupType
async def recovery_homescreen() -> None:
@ -45,36 +44,62 @@ async def recovery_process(ctx: wire.Context) -> Success:
async def _continue_recovery_process(ctx: wire.Context) -> Success:
# gather the current recovery state from storage
in_progress = storage.recovery.is_in_progress()
word_count = storage.recovery.get_word_count()
dry_run = storage.recovery.is_dry_run()
backup_type = storage.recovery.get_backup_type()
if not in_progress: # invalid and inconsistent state
raise RuntimeError
if not word_count: # the first run, prompt word count from the user
word_count = await _request_and_store_word_count(ctx, dry_run)
mnemonic_type = mnemonic.type_from_word_count(word_count)
is_slip39 = backup_types.is_slip39_word_count(word_count)
await _request_share_first_screen(ctx, word_count, is_slip39)
secret = None
while secret is None:
# ask for mnemonic words one by one
words = await layout.request_mnemonic(ctx, word_count, backup_type)
secret = await _request_secret(ctx, word_count, mnemonic_type)
# if they were invalid or some checks failed we continue and request them again
if not words:
continue
try:
secret, backup_type = await _process_words(
ctx, words, is_slip39, backup_type
)
except MnemonicError:
await layout.show_invalid_mnemonic(ctx, is_slip39)
# If the backup type is not stored, we have processed zero mnemonics.
# In that case we prompt the word count again to give the user an
# opportunity to change the word count if they've made a mistake.
first_mnemonic = storage.recovery.get_backup_type() is None
if first_mnemonic:
word_count = await _request_and_store_word_count(ctx, dry_run)
is_slip39 = backup_types.is_slip39_word_count(word_count)
backup_type = None
continue
if dry_run:
result = await _finish_recovery_dry_run(ctx, secret, mnemonic_type)
result = await _finish_recovery_dry_run(ctx, secret)
else:
result = await _finish_recovery(ctx, secret, mnemonic_type)
result = await _finish_recovery(ctx, secret)
return result
async def _finish_recovery_dry_run(
ctx: wire.Context, secret: bytes, mnemonic_type: int
) -> Success:
async def _finish_recovery_dry_run(ctx: wire.Context, secret: bytes) -> Success:
backup_type = storage.recovery.get_backup_type()
if backup_type is None:
raise RuntimeError
digest_input = sha256(secret).digest()
stored = mnemonic.get_secret()
digest_stored = sha256(stored).digest()
result = utils.consteq(digest_stored, digest_input)
is_slip39 = backup_types.is_slip39_backup_type(backup_type)
# Check that the identifier and iteration exponent match as well
if mnemonic_type == mnemonic.TYPE_SLIP39:
if is_slip39:
result &= (
storage.device.get_slip39_identifier()
== storage.recovery.get_slip39_identifier()
@ -84,7 +109,7 @@ async def _finish_recovery_dry_run(
== storage.recovery.get_slip39_iteration_exponent()
)
await layout.show_dry_run_result(ctx, result, mnemonic_type)
await layout.show_dry_run_result(ctx, result, is_slip39)
storage.recovery.end_progress()
@ -94,19 +119,15 @@ async def _finish_recovery_dry_run(
raise wire.ProcessError("The seed does not match the one in the device")
async def _finish_recovery(
ctx: wire.Context, secret: bytes, mnemonic_type: int
) -> Success:
group_count = storage.recovery.get_slip39_group_count()
if group_count and group_count > 1:
mnemonic_type = mnemonic.TYPE_SLIP39_GROUP
async def _finish_recovery(ctx: wire.Context, secret: bytes) -> Success:
backup_type = storage.recovery.get_backup_type()
if backup_type is None:
raise RuntimeError
storage.device.store_mnemonic_secret(
secret, mnemonic_type, needs_backup=False, no_backup=False
secret, backup_type, needs_backup=False, no_backup=False
)
if (
mnemonic_type == mnemonic.TYPE_SLIP39
or mnemonic_type == mnemonic.TYPE_SLIP39_GROUP
):
if backup_type in (BackupType.Slip39_Basic, BackupType.Slip39_Advanced):
identifier = storage.recovery.get_slip39_identifier()
exponent = storage.recovery.get_slip39_iteration_exponent()
if identifier is None or exponent is None:
@ -134,120 +155,102 @@ async def _request_and_store_word_count(ctx: wire.Context, dry_run: bool) -> int
return word_count
async def _request_secret(
ctx: wire.Context, word_count: int, mnemonic_type: int
) -> bytes:
await _request_share_first_screen(ctx, word_count, mnemonic_type)
async def _process_words(
ctx: wire.Context,
words: str,
is_slip39: bool,
backup_type: Optional[EnumTypeBackupType],
) -> Tuple[Optional[bytes], EnumTypeBackupType]:
mnemonics = None
advanced_shamir = False
secret = None
while secret is None:
group_count = storage.recovery.get_slip39_group_count()
if group_count:
mnemonics = storage.recovery_shares.fetch()
advanced_shamir = group_count > 1
group_threshold = storage.recovery.get_slip39_group_threshold()
shares_remaining = storage.recovery.fetch_slip39_remaining_shares()
if advanced_shamir:
await _show_remaining_groups_and_shares(
ctx, group_threshold, shares_remaining
)
share = None
if not is_slip39: # BIP-39
secret = recover.process_bip39(words)
else:
secret, share = recover.process_slip39(words)
if backup_type is None:
# we have to decide what backup type this is and store it
backup_type = _store_backup_type(is_slip39, share)
if secret is None:
if share.group_count and share.group_count > 1:
await layout.show_group_share_success(ctx, share.index, share.group_index)
await _request_share_next_screen(ctx)
return secret, backup_type
try:
# ask for mnemonic words one by one
words = await layout.request_mnemonic(
ctx, word_count, mnemonic_type, mnemonics, advanced_shamir
)
except IdentifierMismatchError:
await layout.show_identifier_mismatch(ctx)
continue
except ShareAlreadyAddedError:
await layout.show_share_already_added(ctx)
continue
# process this seed share
try:
if mnemonic_type == mnemonic.TYPE_BIP39:
secret = recover.process_bip39(words)
else:
try:
secret, group_index, share_index = recover.process_slip39(words)
except GroupThresholdReachedError:
await layout.show_group_threshold_reached(ctx)
continue
except MnemonicError:
await layout.show_invalid_mnemonic(ctx, mnemonic_type)
continue
if secret is None:
group_count = storage.recovery.get_slip39_group_count()
if group_count and group_count > 1:
await layout.show_group_share_success(ctx, share_index, group_index)
await _request_share_next_screen(ctx, mnemonic_type)
return secret
def _store_backup_type(is_slip39: bool, share: Share = None) -> EnumTypeBackupType:
if not is_slip39: # BIP-39
backup_type = BackupType.Bip39
elif not share or share.group_count < 1: # invalid parameters
raise RuntimeError
elif share.group_count == 1:
backup_type = BackupType.Slip39_Basic
else:
backup_type = BackupType.Slip39_Advanced
storage.recovery.set_backup_type(backup_type)
return backup_type
async def _request_share_first_screen(
ctx: wire.Context, word_count: int, mnemonic_type: int
ctx: wire.Context, word_count: int, is_slip39: bool
) -> None:
if mnemonic_type == mnemonic.TYPE_BIP39:
content = layout.RecoveryHomescreen(
"Enter recovery seed", "(%d words)" % word_count
)
await layout.homescreen_dialog(ctx, content, "Enter seed")
elif mnemonic_type == mnemonic.TYPE_SLIP39:
if is_slip39:
remaining = storage.recovery.fetch_slip39_remaining_shares()
if remaining:
await _request_share_next_screen(ctx, mnemonic_type)
await _request_share_next_screen(ctx)
else:
content = layout.RecoveryHomescreen(
"Enter any share", "(%d words)" % word_count
)
await layout.homescreen_dialog(ctx, content, "Enter share")
else:
raise RuntimeError
else: # BIP-39
content = layout.RecoveryHomescreen(
"Enter recovery seed", "(%d words)" % word_count
)
await layout.homescreen_dialog(ctx, content, "Enter seed")
async def _request_share_next_screen(ctx: wire.Context, mnemonic_type: int) -> None:
if mnemonic_type == mnemonic.TYPE_SLIP39:
remaining = storage.recovery.fetch_slip39_remaining_shares()
group_count = storage.recovery.get_slip39_group_count()
if not remaining:
# 'remaining' should be stored at this point
raise RuntimeError
async def _request_share_next_screen(ctx: wire.Context) -> None:
remaining = storage.recovery.fetch_slip39_remaining_shares()
group_count = storage.recovery.get_slip39_group_count()
if not remaining:
# 'remaining' should be stored at this point
raise RuntimeError
if group_count > 1:
content = layout.RecoveryHomescreen(
"More shares needed", "for this recovery"
)
await layout.homescreen_dialog(ctx, content, "Enter share")
else:
if remaining[0] == 1:
text = "1 more share"
else:
text = "%d more shares" % remaining[0]
content = layout.RecoveryHomescreen(text, "needed to enter")
await layout.homescreen_dialog(ctx, content, "Enter share")
if group_count > 1:
content = layout.RecoveryHomescreen("More shares needed")
await layout.homescreen_dialog(
ctx, content, "Enter", _show_remaining_groups_and_shares
)
else:
raise RuntimeError
if remaining[0] == 1:
text = "1 more share"
else:
text = "%d more shares" % remaining[0]
content = layout.RecoveryHomescreen(text, "needed to enter")
await layout.homescreen_dialog(ctx, content, "Enter share")
async def _show_remaining_groups_and_shares(
ctx: wire.Context, group_threshold: int, shares_remaining: List[int]
) -> None:
identifiers = []
async def _show_remaining_groups_and_shares(ctx: wire.Context) -> None:
"""
Show info dialog for Slip39 Advanced - what shares are to be entered.
"""
shares_remaining = storage.recovery.fetch_slip39_remaining_shares()
identifiers = []
first_entered_index = -1
for i in range(len(shares_remaining)):
if shares_remaining[i] < 16:
if shares_remaining[i] < MAX_SHARE_COUNT:
first_entered_index = i
for i, r in enumerate(shares_remaining):
if 0 < r < 16:
if 0 < r < MAX_SHARE_COUNT:
identifier = storage.recovery_shares.fetch_group(i)[0].split(" ")[0:3]
identifiers.append([r, identifier])
elif r == 16:
elif r == MAX_SHARE_COUNT:
identifier = storage.recovery_shares.fetch_group(first_entered_index)[
0
].split(" ")[0:2]
@ -257,6 +260,4 @@ async def _show_remaining_groups_and_shares(
except ValueError:
identifiers.append([r, identifier])
return await layout.show_remaining_shares(
ctx, identifiers, group_threshold, shares_remaining
)
return await layout.show_remaining_shares(ctx, identifiers, shares_remaining)

@ -1,9 +1,8 @@
from trezor import ui, wire
from trezor.errors import IdentifierMismatchError, ShareAlreadyAddedError
from trezor.messages import ButtonRequestType
from trezor.crypto.slip39 import MAX_SHARE_COUNT
from trezor.messages import BackupType, ButtonRequestType
from trezor.messages.ButtonAck import ButtonAck
from trezor.messages.ButtonRequest import ButtonRequest
from trezor.ui.info import InfoConfirm
from trezor.ui.scroll import Paginated
from trezor.ui.text import Text
from trezor.ui.word_select import WordSelector
@ -12,15 +11,17 @@ from .keyboard_bip39 import Bip39Keyboard
from .keyboard_slip39 import Slip39Keyboard
from .recover import RecoveryAborted
from apps.common import mnemonic, storage
from apps.common.confirm import confirm, require_confirm
from apps.common import storage
from apps.common.confirm import confirm, info_confirm, require_confirm
from apps.common.layout import show_success, show_warning
from apps.management import backup_types
if __debug__:
from apps.debug import input_signal, confirm_signal
from apps.debug import input_signal
if False:
from typing import List
from typing import List, Optional, Callable
from trezor.messages.ResetDevice import EnumTypeBackupType
async def confirm_abort(ctx: wire.Context, dry_run: bool = False) -> bool:
@ -53,17 +54,13 @@ async def request_word_count(ctx: wire.Context, dry_run: bool) -> int:
async def request_mnemonic(
ctx: wire.Context,
word_count: int,
mnemonic_type: int,
mnemonics: List[str],
advanced_shamir: bool = False,
) -> str:
ctx: wire.Context, word_count: int, backup_type: Optional[EnumTypeBackupType]
) -> Optional[str]:
await ctx.call(ButtonRequest(code=ButtonRequestType.MnemonicInput), ButtonAck)
words = []
for i in range(word_count):
if mnemonic_type == mnemonic.TYPE_SLIP39:
if backup_types.is_slip39_word_count(word_count):
keyboard = Slip39Keyboard("Type word %s of %s:" % (i + 1, word_count))
else:
keyboard = Bip39Keyboard("Type word %s of %s:" % (i + 1, word_count))
@ -72,41 +69,95 @@ async def request_mnemonic(
else:
word = await ctx.wait(keyboard)
if mnemonic_type == mnemonic.TYPE_SLIP39 and mnemonics:
if not advanced_shamir:
# check if first 3 words of mnemonic match
# we can check against the first one, others were checked already
if i < 3:
share_list = mnemonics[0].split(" ")
if share_list[i] != word:
raise IdentifierMismatchError()
elif i == 3:
for share in mnemonics:
share_list = share.split(" ")
# check if the fourth word is different from previous shares
if share_list[i] == word:
raise ShareAlreadyAddedError()
else:
# in case of advanced shamir recovery we only check 2 words
if i < 2:
share_list = mnemonics[0].split(" ")
if share_list[i] != word:
raise IdentifierMismatchError()
if not await check_word_validity(ctx, i, word, backup_type, words):
return None
words.append(word)
return " ".join(words)
async def check_word_validity(
ctx: wire.Context,
current_index: int,
current_word: str,
backup_type: Optional[EnumTypeBackupType],
previous_words: List[str],
) -> bool:
# we can't perform any checks if the backup type was not yet decided
if backup_type is None:
return True
# there are no "on-the-fly" checks for BIP-39
if backup_type is BackupType.Bip39:
return True
previous_mnemonics = storage.recovery_shares.fetch()
if not previous_mnemonics:
# this function must be called only if some mnemonics are already stored
raise RuntimeError
if backup_type == BackupType.Slip39_Basic:
# check if first 3 words of mnemonic match
# we can check against the first one, others were checked already
if current_index < 3:
share_list = previous_mnemonics[0][0].split(" ")
if share_list[current_index] != current_word:
await show_identifier_mismatch(ctx)
return False
elif current_index == 3:
for share in previous_mnemonics[0]:
share_list = share.split(" ")
# check if the fourth word is different from previous shares
if share_list[current_index] == current_word:
await show_share_already_added(ctx)
return False
elif backup_type == BackupType.Slip39_Advanced:
# in case of advanced slip39 recovery we only check 2 words
if current_index < 2:
share_list = next(s for s in previous_mnemonics if s)[0].split(" ")
if share_list[current_index] != current_word:
await show_identifier_mismatch(ctx)
return False
# check if we reached threshold in group
elif current_index == 2:
for i, group in enumerate(previous_mnemonics):
if len(group) > 0:
if current_word == group[0].split(" ")[current_index]:
remaining_shares = (
storage.recovery.fetch_slip39_remaining_shares()
)
if remaining_shares[i] == 0:
await show_group_threshold_reached(ctx)
return False
# check if share was already added for group
elif current_index == 3:
# we use the 3rd word from previously entered shares to find the group id
group_identifier_word = previous_words[2]
group_index = None
for i, group in enumerate(previous_mnemonics):
if len(group) > 0:
if group_identifier_word == group[0].split(" ")[2]:
group_index = i
if group_index:
group = previous_mnemonics[group_index]
for share in group:
if current_word == share.split(" ")[current_index]:
await show_share_already_added(ctx)
return False
return True
async def show_remaining_shares(
ctx: wire.Context,
groups: List[[int, List[str]]], # remaining + list 3 words
group_threshold: int,
shares_remaining: List[int],
) -> None:
group_threshold = storage.recovery.get_slip39_group_threshold()
pages = []
for remaining, group in groups:
if 0 < remaining < 16:
if 0 < remaining < MAX_SHARE_COUNT:
text = Text("Remaining Shares")
if remaining > 1:
text.bold("%s more shares starting" % remaining)
@ -115,7 +166,9 @@ async def show_remaining_shares(
for word in group:
text.normal(word)
pages.append(text)
elif remaining == 16 and shares_remaining.count(0) < group_threshold:
elif (
remaining == MAX_SHARE_COUNT and shares_remaining.count(0) < group_threshold
):
text = Text("Remaining Shares")
groups_remaining = group_threshold - shares_remaining.count(0)
if groups_remaining > 1:
@ -126,7 +179,7 @@ async def show_remaining_shares(
text.normal(word)
pages.append(text)
return await confirm(ctx, Paginated(pages), confirm="Continue", cancel=None)
return await confirm(ctx, Paginated(pages), cancel=None)
async def show_group_share_success(
@ -141,11 +194,9 @@ async def show_group_share_success(
return await confirm(ctx, text, confirm="Continue", cancel=None)
async def show_dry_run_result(
ctx: wire.Context, result: bool, mnemonic_type: int
) -> None:
async def show_dry_run_result(ctx: wire.Context, result: bool, is_slip39: bool) -> None:
if result:
if mnemonic_type == mnemonic.TYPE_SLIP39:
if is_slip39:
text = (
"The entered recovery",
"shares are valid and",
@ -161,7 +212,7 @@ async def show_dry_run_result(
)
await show_success(ctx, text, button="Continue")
else:
if mnemonic_type == mnemonic.TYPE_SLIP39:
if is_slip39:
text = (
"The entered recovery",
"shares are valid but",
@ -188,25 +239,8 @@ async def show_dry_run_different_type(ctx: wire.Context) -> None:
)
async def show_keyboard_info(ctx: wire.Context) -> None:
# TODO: do not send ButtonRequestType.Other
await ctx.call(ButtonRequest(code=ButtonRequestType.Other), ButtonAck)
info = InfoConfirm(
"Did you know? "
"You can type the letters "
"one by one or use it like "
"a T9 keyboard.",
"Great!",
)
if __debug__:
await ctx.wait(info, confirm_signal())
else:
await ctx.wait(info)
async def show_invalid_mnemonic(ctx: wire.Context, mnemonic_type: int) -> None:
if mnemonic_type == mnemonic.TYPE_SLIP39:
async def show_invalid_mnemonic(ctx: wire.Context, is_slip39: bool) -> None:
if is_slip39:
await show_warning(ctx, ("You have entered", "an invalid recovery", "share."))
else:
await show_warning(ctx, ("You have entered", "an invalid recovery", "seed."))
@ -272,16 +306,30 @@ class RecoveryHomescreen(ui.Component):
async def homescreen_dialog(
ctx: wire.Context, homepage: RecoveryHomescreen, button_label: str
ctx: wire.Context,
homepage: RecoveryHomescreen,
button_label: str,
info_func: Callable = None,
) -> None:
while True:
continue_recovery = await confirm(
ctx,
homepage,
code=ButtonRequestType.RecoveryHomepage,
confirm=button_label,
major_confirm=True,
)
if info_func:
continue_recovery = await info_confirm(
ctx,
homepage,
code=ButtonRequestType.RecoveryHomepage,
confirm=button_label,
info_func=info_func,
info="Info",
cancel="Abort",
)
else:
continue_recovery = await confirm(
ctx,
homepage,
code=ButtonRequestType.RecoveryHomepage,
confirm=button_label,
major_confirm=True,
)
if continue_recovery:
# go forward in the recovery process
break

@ -1,72 +1,81 @@
from trezor.crypto import bip39, slip39
from trezor.errors import GroupThresholdReachedError, MnemonicError
from trezor.errors import MnemonicError
from apps.common import storage
if False:
from typing import Optional
from typing import Optional, Tuple
class RecoveryAborted(Exception):
pass
_GROUP_STORAGE_OFFSET = 16
def process_bip39(words: str) -> bytes:
"""
Receives single mnemonic and processes it. Returns what is then stored
in the storage, which is the mnemonic itself for BIP-39.
"""
if not bip39.check(words):
raise MnemonicError()
raise MnemonicError
return words.encode()
def process_slip39(words: str) -> Optional[bytes, int, int]:
def process_slip39(words: str) -> Tuple[Optional[bytes], slip39.Share]:
"""
Receives single mnemonic and processes it. Returns what is then stored in storage or
None if more shares are needed.
Processes a single mnemonic share. Returns the encrypted master secret
(or None if more shares are needed) and the share's group index and member index.
"""
identifier, iteration_exponent, group_index, group_threshold, group_count, index, threshold, value = slip39.decode_mnemonic(
words
) # TODO: use better data structure for this
share = slip39.decode_mnemonic(words)
remaining = storage.recovery.fetch_slip39_remaining_shares()
index_with_group_offset = index + group_index * _GROUP_STORAGE_OFFSET
# TODO: move this whole logic to storage
index_with_group_offset = share.index + share.group_index * slip39.MAX_SHARE_COUNT
# if this is the first share, parse and store metadata
if not remaining:
storage.recovery.set_slip39_group_count(group_count)
storage.recovery.set_slip39_group_threshold(group_threshold)
storage.recovery.set_slip39_iteration_exponent(iteration_exponent)
storage.recovery.set_slip39_identifier(identifier)
storage.recovery.set_slip39_threshold(threshold)
storage.recovery.set_slip39_remaining_shares(threshold - 1, group_index)
storage.recovery.set_slip39_group_count(share.group_count)
storage.recovery.set_slip39_group_threshold(share.group_threshold)
storage.recovery.set_slip39_iteration_exponent(share.iteration_exponent)
storage.recovery.set_slip39_identifier(share.identifier)
storage.recovery.set_slip39_threshold(share.threshold)
storage.recovery.set_slip39_remaining_shares(
share.threshold - 1, share.group_index
)
storage.recovery_shares.set(index_with_group_offset, words)
return None, group_index, index # we need more shares
# if share threshold and group threshold are 1
# we can calculate the secret right away
if share.threshold == 1 and share.group_threshold == 1:
identifier, iteration_exponent, secret, _ = slip39.combine_mnemonics(
[words]
)
return secret, share
else:
# we need more shares
return None, share
if remaining[group_index] == 0:
raise GroupThresholdReachedError()
# These should be checked by UI before so it's a Runtime exception otherwise
if identifier != storage.recovery.get_slip39_identifier():
if share.identifier != storage.recovery.get_slip39_identifier():
raise RuntimeError("Slip39: Share identifiers do not match")
if storage.recovery_shares.get(index_with_group_offset):
raise RuntimeError("Slip39: This mnemonic was already entered")
remaining_for_share = (
storage.recovery.get_slip39_remaining_shares(group_index) or threshold
storage.recovery.get_slip39_remaining_shares(share.group_index)
or share.threshold
)
storage.recovery.set_slip39_remaining_shares(
remaining_for_share - 1, share.group_index
)
storage.recovery.set_slip39_remaining_shares(remaining_for_share - 1, group_index)
remaining[group_index] = remaining_for_share - 1
remaining[share.group_index] = remaining_for_share - 1
storage.recovery_shares.set(index_with_group_offset, words)
if remaining.count(0) < group_threshold:
return None, group_index, index # we need more shares
if remaining.count(0) < share.group_threshold:
# we need more shares
return None, share
if len(remaining) > 1:
if share.group_count > 1:
mnemonics = []
for i, r in enumerate(remaining):
# if we have multiple groups pass only the ones with threshold reached
@ -74,7 +83,8 @@ def process_slip39(words: str) -> Optional[bytes, int, int]:
group = storage.recovery_shares.fetch_group(i)
mnemonics.extend(group)
else:
mnemonics = storage.recovery_shares.fetch()
# in case of slip39 basic we only need the first and only group
mnemonics = storage.recovery_shares.fetch_group(0)
identifier, iteration_exponent, secret = slip39.combine_mnemonics(mnemonics)
return secret, group_index, index
identifier, iteration_exponent, secret, _ = slip39.combine_mnemonics(mnemonics)
return secret, share

@ -1,17 +1,15 @@
from trezor import config, ui, wire
from trezor import config, wire
from trezor.crypto import bip39, hashlib, random, slip39
from trezor.messages import ButtonRequestType, ResetDeviceBackupType
from trezor.messages import BackupType
from trezor.messages.EntropyAck import EntropyAck
from trezor.messages.EntropyRequest import EntropyRequest
from trezor.messages.Success import Success
from trezor.pin import pin_to_int
from trezor.ui.loader import LoadingAnimation
from trezor.ui.text import Text
from apps.common import mnemonic, storage
from apps.common.confirm import require_confirm
from apps.common.request_pin import request_pin_confirm
from apps.management.common import layout
from apps.common import storage
from apps.management import backup_types
from apps.management.change_pin import request_pin_confirm
from apps.management.reset_device import layout
if __debug__:
from apps import debug
@ -19,18 +17,15 @@ if __debug__:
if False:
from trezor.messages.ResetDevice import ResetDevice
_DEFAULT_BACKUP_TYPE = ResetDeviceBackupType.Bip39
_DEFAULT_BACKUP_TYPE = BackupType.Bip39
async def reset_device(ctx: wire.Context, msg: ResetDevice) -> Success:
# validate parameters and device state
_validate_reset_device(msg)
is_slip39_simple = msg.backup_type == ResetDeviceBackupType.Slip39_Single_Group
is_slip39_group = msg.backup_type == ResetDeviceBackupType.Slip39_Multiple_Groups
# make sure user knows he's setting up a new wallet
await _show_reset_device_warning(ctx, msg.backup_type)
# make sure user knows they're setting up a new wallet
await layout.show_reset_device_warning(ctx, msg.backup_type)
# request new PIN
if msg.pin_protection:
@ -51,24 +46,21 @@ async def reset_device(ctx: wire.Context, msg: ResetDevice) -> Success:
# For SLIP-39 this is the Encrypted Master Secret
secret = _compute_secret_from_entropy(int_entropy, ext_entropy, msg.strength)
if is_slip39_simple or is_slip39_group:
if msg.backup_type != BackupType.Bip39:
storage.device.set_slip39_identifier(slip39.generate_random_identifier())
storage.device.set_slip39_iteration_exponent(slip39.DEFAULT_ITERATION_EXPONENT)
# should we back up the wallet now?
if not msg.no_backup and not msg.skip_backup:
if not await layout.confirm_backup(ctx):
if not await layout.confirm_backup_again(ctx):
msg.skip_backup = True
# If either of skip_backup or no_backup is specified, we are not doing backup now.
# Otherwise, we try to do it.
perform_backup = not msg.no_backup and not msg.skip_backup
# If doing backup, ask the user to confirm.
if perform_backup:
perform_backup = await layout.confirm_backup(ctx)
# generate and display backup information for the master secret
if not msg.no_backup and not msg.skip_backup:
if is_slip39_simple:
await backup_slip39_wallet(ctx, secret)
elif is_slip39_group:
await backup_group_slip39_wallet(ctx, secret)
else:
await backup_bip39_wallet(ctx, secret)
if perform_backup:
await backup_seed(ctx, msg.backup_type, secret)
# write PIN into storage
if not config.change_pin(pin_to_int(""), pin_to_int(newpin), None, None):
@ -78,69 +70,70 @@ async def reset_device(ctx: wire.Context, msg: ResetDevice) -> Success:
storage.device.load_settings(
label=msg.label, use_passphrase=msg.passphrase_protection
)
if is_slip39_simple or is_slip39_group:
storage.device.store_mnemonic_secret(
secret, # this is the EMS in SLIP-39 terminology
msg.backup_type,
needs_backup=msg.skip_backup,
no_backup=msg.no_backup,
)
else:
if msg.backup_type == BackupType.Bip39:
# in BIP-39 we store mnemonic string instead of the secret
storage.device.store_mnemonic_secret(
bip39.from_data(secret).encode(),
mnemonic.TYPE_BIP39,
needs_backup=msg.skip_backup,
no_backup=msg.no_backup,
)
secret = bip39.from_data(secret).encode()
elif msg.backup_type not in (BackupType.Slip39_Basic, BackupType.Slip39_Advanced):
# Unknown backup type.
# This check might seem superfluous, because we are checking
# in `_validate_reset_device` already, however, this is critical part,
# so just to make sure.
raise RuntimeError
storage.device.store_mnemonic_secret(
secret, # for SLIP-39, this is the EMS
msg.backup_type,
needs_backup=not perform_backup,
no_backup=msg.no_backup,
)
# if we backed up the wallet, show success message
if not msg.no_backup and not msg.skip_backup:
if perform_backup:
await layout.show_backup_success(ctx)
return Success(message="Initialized")
async def backup_slip39_wallet(
async def backup_slip39_basic(
ctx: wire.Context, encrypted_master_secret: bytes
) -> None:
# get number of shares
await layout.slip39_show_checklist_set_shares(ctx)
await layout.slip39_show_checklist(ctx, 0, BackupType.Slip39_Basic)
shares_count = await layout.slip39_prompt_number_of_shares(ctx)
# get threshold
await layout.slip39_show_checklist_set_threshold(ctx, shares_count)
await layout.slip39_show_checklist(ctx, 1, BackupType.Slip39_Basic)
threshold = await layout.slip39_prompt_threshold(ctx, shares_count)
# generate the mnemonics
mnemonics = slip39.generate_single_group_mnemonics_from_data(
mnemonics = slip39.generate_mnemonics_from_data(
encrypted_master_secret,
storage.device.get_slip39_identifier(),
threshold,
shares_count,
1, # Single Group threshold
[(threshold, shares_count)], # Single Group threshold/count
storage.device.get_slip39_iteration_exponent(),
)
)[0]
# 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)
await layout.slip39_show_checklist(ctx, 2, BackupType.Slip39_Basic)
await layout.slip39_basic_show_and_confirm_shares(ctx, mnemonics)
async def backup_group_slip39_wallet(
async def backup_slip39_advanced(
ctx: wire.Context, encrypted_master_secret: bytes
) -> None:
# get number of groups
await layout.slip39_group_show_checklist_set_groups(ctx)
groups_count = await layout.slip39_prompt_number_of_groups(ctx)
await layout.slip39_show_checklist(ctx, 0, BackupType.Slip39_Advanced)
groups_count = await layout.slip39_advanced_prompt_number_of_groups(ctx)
# get group threshold
await layout.slip39_group_show_checklist_set_group_threshold(ctx, groups_count)
group_threshold = await layout.slip39_prompt_group_threshold(ctx, groups_count)
await layout.slip39_show_checklist(ctx, 1, BackupType.Slip39_Advanced)
group_threshold = await layout.slip39_advanced_prompt_group_threshold(
ctx, groups_count
)
# get shares and thresholds
await layout.slip39_group_show_checklist_set_shares(
ctx, groups_count, group_threshold
)
await layout.slip39_show_checklist(ctx, 2, BackupType.Slip39_Advanced)
groups = []
for i in range(groups_count):
share_count = await layout.slip39_prompt_number_of_shares(ctx, i)
@ -157,10 +150,10 @@ async def backup_group_slip39_wallet(
)
# show and confirm individual shares
await layout.slip39_group_show_and_confirm_shares(ctx, mnemonics)
await layout.slip39_advanced_show_and_confirm_shares(ctx, mnemonics)
async def backup_bip39_wallet(ctx: wire.Context, secret: bytes) -> None:
async def backup_bip39(ctx: wire.Context, secret: bytes) -> None:
mnemonic = bip39.from_data(secret)
await layout.bip39_show_and_confirm_mnemonic(ctx, mnemonic)
@ -168,15 +161,16 @@ async def backup_bip39_wallet(ctx: wire.Context, secret: bytes) -> None:
def _validate_reset_device(msg: ResetDevice) -> None:
msg.backup_type = msg.backup_type or _DEFAULT_BACKUP_TYPE
if msg.backup_type not in (
ResetDeviceBackupType.Bip39,
ResetDeviceBackupType.Slip39_Single_Group,
ResetDeviceBackupType.Slip39_Multiple_Groups,
BackupType.Bip39,
BackupType.Slip39_Basic,
BackupType.Slip39_Advanced,
):
raise wire.ProcessError("Backup type not implemented.")
if msg.strength not in (128, 256):
if msg.backup_type == ResetDeviceBackupType.Slip39_Single_Group:
if backup_types.is_slip39_backup_type(msg.backup_type):
if msg.strength not in (128, 256):
raise wire.ProcessError("Invalid strength (has to be 128 or 256 bits)")
elif msg.strength != 192:
else: # BIP-39
if msg.strength not in (128, 192, 256):
raise wire.ProcessError("Invalid strength (has to be 128, 192 or 256 bits)")
if msg.display_random and (msg.skip_backup or msg.no_backup):
raise wire.ProcessError("Can't show internal entropy when backup is skipped")
@ -198,27 +192,12 @@ def _compute_secret_from_entropy(
return secret
async def _show_reset_device_warning(
ctx, backup_type: ResetDeviceBackupType = ResetDeviceBackupType.Bip39
async def backup_seed(
ctx: wire.Context, backup_type: BackupType, mnemonic_secret: bytes
):
text = Text("Create new wallet", ui.ICON_RESET, new_lines=False)
if backup_type == ResetDeviceBackupType.Slip39_Single_Group:
text.bold("Create a new wallet")
text.br()
text.bold("with Shamir Backup?")
elif backup_type == ResetDeviceBackupType.Slip39_Multiple_Groups:
text.bold("Create a new wallet")
text.br()
text.bold("with Super Shamir?")
if backup_type == BackupType.Slip39_Basic:
await backup_slip39_basic(ctx, mnemonic_secret)
elif backup_type == BackupType.Slip39_Advanced:
await backup_slip39_advanced(ctx, mnemonic_secret)
else:
text.bold("Do you want to create")
text.br()
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()
await backup_bip39(ctx, mnemonic_secret)

@ -1,14 +1,14 @@
import ubinascii
from micropython import const
from trezor import ui, utils
from trezor.crypto import random
from trezor.messages import ButtonRequestType
from trezor.messages import BackupType, 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.num_input import NumInput
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
@ -27,6 +27,7 @@ async def show_internal_entropy(ctx, entropy: bytes):
async def confirm_backup(ctx):
# First prompt
text = Text("Success", ui.ICON_CONFIRM, ui.GREEN, new_lines=False)
text.bold("New wallet created")
text.br()
@ -36,17 +37,17 @@ async def confirm_backup(ctx):
text.normal("You should back up your")
text.br()
text.normal("new wallet right now.")
return await confirm(
if await confirm(
ctx,
text,
ButtonRequestType.ResetDevice,
cancel="Skip",
confirm="Back up",
major_confirm=True,
)
):
return True
async def confirm_backup_again(ctx):
# If the user selects Skip, ask again
text = Text("Warning", ui.ICON_WRONG, ui.RED, new_lines=False)
text.bold("Are you sure you want")
text.br()
@ -66,15 +67,93 @@ async def confirm_backup_again(ctx):
)
async def _show_share_words(ctx, share_words, share_index=None, group_index=None):
first, chunks, last = _split_share_into_pages(share_words)
if share_index is None:
header_title = "Recovery seed"
elif group_index is None:
header_title = "Recovery share #%s" % (share_index + 1)
else:
header_title = "Group %s - Share %s" % ((group_index + 1), (share_index + 1))
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.bold("Write down these")
text.bold("%s words:" % len(share_words))
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)
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.bold("I wrote down all %s" % len(share_words))
text.bold("words in order.")
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
words = [w for _, w in word_pages[paginated.page]]
debug.reset_current_words.publish(words)
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, ButtonRequestType.ResetDevice)
def _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
length = len(share_words)
if length == 12 or length == 20 or length == 24:
middle = share[2:-2]
last = share[-2:] # two words on the last page
elif length == 33:
middle = share[2:]
last = [] # no words at the last page, because it does not add up
else:
# Invalid number of shares. SLIP-39 allows 20 or 33 words, BIP-39 12 or 24
raise RuntimeError
chunks = utils.chunks(middle, 4) # 4 words on the middle pages
return first, list(chunks), last
async def _confirm_share_words(ctx, share_index, share_words, group_index=None):
numbered = list(enumerate(share_words))
# check three words
third = len(numbered) // 3
# if the num of words is not dividable by 3 let's add 1
# to have more words at the beggining and to check all of them
if len(numbered) % 3:
third += 1
# divide list into thirds, rounding up, so that chunking by `third` always yields
# three parts (the last one might be shorter)
third = (len(numbered) + 2) // 3
for part in utils.chunks(numbered, third):
if not await _confirm_word(
@ -88,8 +167,8 @@ async def _confirm_share_words(ctx, share_index, share_words, group_index=None):
async def _confirm_word(
ctx, share_index, numbered_share_words, count, group_index=None
):
# TODO: duplicated words in the choice list
# 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]
@ -115,22 +194,24 @@ async def _confirm_word(
async def _show_confirmation_success(
ctx, share_index, num_of_shares=None, slip39=False, group_index=None
ctx, share_index=None, num_of_shares=None, group_index=None
):
if share_index is None or num_of_shares is None or share_index == num_of_shares - 1:
if slip39:
if group_index is None:
subheader = ("You have finished", "verifying your", "recovery shares.")
else:
subheader = (
"You have finished",
"verifying your",
"recovery shares",
"for group %s." % (group_index + 1),
)
if share_index is None: # it is a BIP39 backup
subheader = ("You have finished", "verifying your", "recovery seed.")
text = []
elif share_index == num_of_shares - 1:
if group_index is None:
subheader = ("You have finished", "verifying your", "recovery shares.")
else:
subheader = ("You have finished", "verifying your", "recovery seed.")
subheader = (
"You have finished",
"verifying your",
"recovery shares",
"for group %s." % (group_index + 1),
)
text = []
else:
if group_index is None:
subheader = (
@ -198,123 +279,82 @@ async def bip39_show_and_confirm_mnemonic(ctx, mnemonic: str):
while True:
# display paginated mnemonic on the screen
await _bip39_show_mnemonic(ctx, words)
await _show_share_words(ctx, share_words=words)
# make the user confirm 2 words from the mnemonic
# make the user confirm some words from the mnemonic
if await _confirm_share_words(ctx, None, words):
await _show_confirmation_success(ctx, None)
await _show_confirmation_success(ctx)
break # this share is confirmed, go to next one
else:
await _show_confirmation_failure(ctx, 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.publish([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
# SLIP 39 simple
async def slip39_show_checklist_set_shares(ctx):
async def slip39_show_checklist(ctx, step: int, backup_type: BackupType) -> None:
checklist = Checklist("Backup checklist", ui.ICON_RESET)
checklist.add("Set number of shares")
checklist.add("Set threshold")
checklist.add(("Write down and check", "all recovery shares"))
checklist.select(0)
return await confirm(
ctx, checklist, ButtonRequestType.ResetDevice, cancel=None, confirm="Continue"
)
if backup_type is BackupType.Slip39_Basic:
checklist.add("Set number of shares")
checklist.add("Set threshold")
checklist.add(("Write down and check", "all recovery shares"))
elif backup_type is BackupType.Slip39_Advanced:
checklist.add("Set number of groups")
checklist.add("Set group threshold")
checklist.add(("Set size and threshold", "for each group"))
checklist.select(step)
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 threshold")
checklist.add(("Write down and check", "all recovery shares"))
checklist.select(1)
return await confirm(
ctx, checklist, ButtonRequestType.ResetDevice, cancel=None, confirm="Continue"
)
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 threshold")
checklist.add(("Write down and check", "all recovery shares"))
checklist.select(2)
return await confirm(
ctx, checklist, ButtonRequestType.ResetDevice, cancel=None, confirm="Continue"
)
# SLIP 39 group
async def slip39_group_show_checklist_set_groups(ctx):
checklist = Checklist("Backup checklist", ui.ICON_RESET)
checklist.add("Set number of groups")
checklist.add("Set group threshold")
checklist.add(("Set size and threshold", "for each group"))
checklist.select(0)
return await confirm(
ctx, checklist, ButtonRequestType.ResetDevice, cancel=None, confirm="Continue"
)
async def slip39_prompt_threshold(ctx, num_of_shares, group_id=None):
count = num_of_shares // 2 + 1
# min value of share threshold is 2 unless the number of shares is 1
# number of shares 1 is possible in advnaced slip39
min_count = min(2, num_of_shares)
max_count = num_of_shares
async def slip39_group_show_checklist_set_group_threshold(ctx, num_of_shares):
checklist = Checklist("Backup checklist", ui.ICON_RESET)
checklist.add("Set number of groups")
checklist.add("Set group threshold")
checklist.add(("Set size and threshold", "for each group"))
checklist.select(1)
return await confirm(
ctx, checklist, ButtonRequestType.ResetDevice, cancel=None, confirm="Continue"
)
while True:
shares = Slip39NumInput(
Slip39NumInput.SET_THRESHOLD, count, min_count, max_count, group_id
)
confirmed = await confirm(
ctx,
shares,
ButtonRequestType.ResetDevice,
cancel="Info",
confirm="Continue",
major_confirm=True,
cancel_style=ButtonDefault,
)
count = shares.input.count
if confirmed:
break
if group_id is None:
info = InfoConfirm(
"The threshold sets the "
"number of shares "
"needed to recover your "
"wallet. Set it to %s and "
"you will need any %s "
"of your %s shares." % (count, count, num_of_shares)
)
else:
info = InfoConfirm(
"The threshold sets the "
"number of shares "
"needed to form a group. "
"Set it to %s and you will "
"need any %s of %s shares "
"to form Group %s." % (count, count, num_of_shares, group_id + 1)
)
await info
async def slip39_group_show_checklist_set_shares(ctx, num_of_shares, group_threshold):
checklist = Checklist("Backup checklist", ui.ICON_RESET)
checklist.add("Set number of groups")
checklist.add("Set group threshold")
checklist.add(("Set size and threshold", "for each group"))
checklist.select(2)
return await confirm(
ctx, checklist, ButtonRequestType.ResetDevice, cancel=None, confirm="Continue"
)
return count
async def slip39_prompt_number_of_shares(ctx, group_id=None):
@ -326,8 +366,8 @@ async def slip39_prompt_number_of_shares(ctx, group_id=None):
max_count = 16
while True:
shares = ShamirNumInput(
ShamirNumInput.SET_SHARES, count, min_count, max_count, group_id
shares = Slip39NumInput(
Slip39NumInput.SET_SHARES, count, min_count, max_count, group_id
)
confirmed = await confirm(
ctx,
@ -365,13 +405,33 @@ async def slip39_prompt_number_of_shares(ctx, group_id=None):
return count
async def slip39_prompt_number_of_groups(ctx):
async def slip39_basic_show_and_confirm_shares(ctx, shares):
# warn user about mnemonic safety
await show_backup_warning(ctx, slip39=True)
for index, share in enumerate(shares):
share_words = share.split(" ")
while True:
# display paginated share on the screen
await _show_share_words(ctx, share_words, index)
# make the user confirm words from the share
if await _confirm_share_words(ctx, index, share_words):
await _show_confirmation_success(
ctx, share_index=index, num_of_shares=len(shares)
)
break # this share is confirmed, go to next one
else:
await _show_confirmation_failure(ctx, index)
async def slip39_advanced_prompt_number_of_groups(ctx):
count = 5
min_count = 2
max_count = 16
while True:
shares = ShamirNumInput(ShamirNumInput.SET_GROUPS, count, min_count, max_count)
shares = Slip39NumInput(Slip39NumInput.SET_GROUPS, count, min_count, max_count)
confirmed = await confirm(
ctx,
shares,
@ -398,14 +458,14 @@ async def slip39_prompt_number_of_groups(ctx):
return count
async def slip39_prompt_group_threshold(ctx, num_of_groups):
async def slip39_advanced_prompt_group_threshold(ctx, num_of_groups):
count = num_of_groups // 2 + 1
min_count = 1
max_count = num_of_groups
while True:
shares = ShamirNumInput(
ShamirNumInput.SET_GROUP_THRESHOLD, count, min_count, max_count
shares = Slip39NumInput(
Slip39NumInput.SET_GROUP_THRESHOLD, count, min_count, max_count
)
confirmed = await confirm(
ctx,
@ -431,72 +491,7 @@ async def slip39_prompt_group_threshold(ctx, num_of_groups):
return count
async def slip39_prompt_threshold(ctx, num_of_shares, group_id=None):
count = num_of_shares // 2 + 1
min_count = min(2, num_of_shares)
max_count = num_of_shares
while True:
shares = ShamirNumInput(
ShamirNumInput.SET_THRESHOLD, count, min_count, max_count, group_id
)
confirmed = await confirm(
ctx,
shares,
ButtonRequestType.ResetDevice,
cancel="Info",
confirm="Continue",
major_confirm=True,
cancel_style=ButtonDefault,
)
count = shares.input.count
if confirmed:
break
if group_id is None:
info = InfoConfirm(
"The threshold sets the "
"number of shares "
"needed to recover your "
"wallet. Set it to %s and "
"you will need any %s "
"of your %s shares." % (count, count, num_of_shares)
)
else:
info = InfoConfirm(
"The threshold sets the "
"number of shares "
"needed to form a group. "
"Set it to %s and you will "
"need any %s of %s shares "
"to form Group %s." % (count, count, num_of_shares, group_id + 1)
)
await info
return count
async def slip39_show_and_confirm_shares(ctx, shares):
# warn user about mnemonic safety
await show_backup_warning(ctx, slip39=True)
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 words from the share
if await _confirm_share_words(ctx, index, share_words):
await _show_confirmation_success(
ctx, index, num_of_shares=len(shares), slip39=True
)
break # this share is confirmed, go to next one
else:
await _show_confirmation_failure(ctx, index)
async def slip39_group_show_and_confirm_shares(ctx, shares):
async def slip39_advanced_show_and_confirm_shares(ctx, shares):
# warn user about mnemonic safety
await show_backup_warning(ctx, slip39=True)
@ -505,9 +500,7 @@ async def slip39_group_show_and_confirm_shares(ctx, shares):
share_words = share.split(" ")
while True:
# display paginated share on the screen
await _slip39_show_share_words(
ctx, share_index, share_words, group_index
)
await _show_share_words(ctx, share_words, share_index, group_index)
# make the user confirm words from the share
if await _confirm_share_words(
@ -515,9 +508,8 @@ async def slip39_group_show_and_confirm_shares(ctx, shares):
):
await _show_confirmation_success(
ctx,
share_index,
share_index=share_index,
num_of_shares=len(shares),
slip39=True,
group_index=group_index,
)
break # this share is confirmed, go to next one
@ -525,88 +517,7 @@ async def slip39_group_show_and_confirm_shares(ctx, shares):
await _show_confirmation_failure(ctx, share_index)
async def _slip39_show_share_words(ctx, share_index, share_words, group_index=None):
first, chunks, last = _slip39_split_share_into_pages(share_words)
if share_index is None:
header_title = "Recovery seed"
elif group_index is None:
header_title = "Recovery share #%s" % (share_index + 1)
else:
header_title = "Group %s - Share %s" % ((group_index + 1), (share_index + 1))
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.bold("Write down these")
text.bold("%s words:" % len(share_words))
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)
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.bold("I wrote down all %s" % len(share_words))
text.bold("words in order.")
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
words = [w for _, w in word_pages[paginated.page]]
debug.reset_current_words.publish(words)
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
length = len(share_words)
if length == 20:
middle = share[2:-2]
last = share[-2:] # two words on the last page
elif length == 33:
middle = share[2:]
last = [] # no words at the last page, because it does not add up
else:
# Invalid number of shares. SLIP-39 allows 20 or 33 words.
raise RuntimeError
chunks = utils.chunks(middle, 4) # 4 words on the middle pages
return first, list(chunks), last
class ShamirNumInput(ui.Component):
class Slip39NumInput(ui.Component):
SET_SHARES = object()
SET_THRESHOLD = object()
SET_GROUPS = object()
@ -629,18 +540,18 @@ class ShamirNumInput(ui.Component):
count = self.input.count
# render the headline
if self.step is ShamirNumInput.SET_SHARES:
if self.step is Slip39NumInput.SET_SHARES:
header = "Set num. of shares"
elif self.step is ShamirNumInput.SET_THRESHOLD:
elif self.step is Slip39NumInput.SET_THRESHOLD:
header = "Set threshold"
elif self.step is ShamirNumInput.SET_GROUPS:
elif self.step is Slip39NumInput.SET_GROUPS:
header = "Set num. of groups"
elif self.step is ShamirNumInput.SET_GROUP_THRESHOLD:
elif self.step is Slip39NumInput.SET_GROUP_THRESHOLD:
header = "Set group 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:
if self.step is Slip39NumInput.SET_SHARES:
if self.group_id is None:
first_line_text = "%s people or locations" % count
second_line_text = "will each hold one share."
@ -651,7 +562,7 @@ class ShamirNumInput(ui.Component):
12, 130, first_line_text, ui.NORMAL, ui.FG, ui.BG, ui.WIDTH - 12
)
ui.display.text(12, 156, second_line_text, ui.NORMAL, ui.FG, ui.BG)
elif self.step is ShamirNumInput.SET_THRESHOLD:
elif self.step is Slip39NumInput.SET_THRESHOLD:
if self.group_id is None:
first_line_text = "For recovery you need"
second_line_text = "any %s of the shares." % count
@ -662,14 +573,14 @@ class ShamirNumInput(ui.Component):
ui.display.text(
12, 156, second_line_text, ui.NORMAL, ui.FG, ui.BG, ui.WIDTH - 12
)
elif self.step is ShamirNumInput.SET_GROUPS:
elif self.step is Slip39NumInput.SET_GROUPS:
ui.display.text(
12, 130, "A group is made up of", ui.NORMAL, ui.FG, ui.BG
)
ui.display.text(
12, 156, "recovery shares.", ui.NORMAL, ui.FG, ui.BG, ui.WIDTH - 12
)
elif self.step is ShamirNumInput.SET_GROUP_THRESHOLD:
elif self.step is Slip39NumInput.SET_GROUP_THRESHOLD:
ui.display.text(
12, 130, "The required number of", ui.NORMAL, ui.FG, ui.BG
)
@ -722,3 +633,27 @@ class MnemonicWordSelect(ui.Layout):
raise ui.Result(word)
return fn
async def show_reset_device_warning(ctx, backup_type: BackupType = BackupType.Bip39):
text = Text("Create new wallet", ui.ICON_RESET, new_lines=False)
if backup_type == BackupType.Slip39_Basic:
text.bold("Create a new wallet")
text.br()
text.bold("with Shamir Backup?")
elif backup_type == BackupType.Slip39_Advanced:
text.bold("Create a new wallet")
text.br()
text.bold("with Super Shamir?")
else:
text.bold("Do you want to create")
text.br()
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()

@ -18,6 +18,18 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
"""
This implements the high-level functions for SLIP-39, also called "Shamir Backup".
It uses crypto/shamir.c for the cryptographic operations and crypto.slip39.c for
performance-heavy operations (mostly regarding the wordlist).
This consideres the Encrypted Master Secret, as defined in SLIP-39, as what is
stored in the storage, then "decrypted" using a passphrase into a Master Secret,
which is then fed into BIP-32 for example.
See https://github.com/satoshilabs/slips/blob/master/slip-0039.md.
"""
from micropython import const
from trezor.crypto import hashlib, hmac, pbkdf2, random
@ -28,42 +40,31 @@ if False:
from typing import Dict, Iterable, List, Optional, Set, Tuple
Indices = Tuple[int, ...]
MnemonicGroups = Dict[int, Tuple[int, Set[Tuple[int, bytes]]]]
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 ""
return slip39.button_sequence_to_word(int(prefix))
"""
## Simple helpers
"""
_RADIX_BITS = const(10)
"""The length of the radix in bits."""
def bits_to_bytes(n: int) -> int:
def _bits_to_bytes(n: int) -> int:
return (n + 7) // 8
def bits_to_words(n: int) -> int:
def _bits_to_words(n: int) -> int:
return (n + _RADIX_BITS - 1) // _RADIX_BITS
MAX_SHARE_COUNT = const(16)
"""The maximum number of shares that can be created."""
def _xor(a: bytes, b: bytes) -> bytes:
return bytes(x ^ y for x, y in zip(a, b))
DEFAULT_ITERATION_EXPONENT = const(1)
_RADIX = 2 ** _RADIX_BITS
"""The number of words in the wordlist."""
"""
## Constants
"""
_ID_LENGTH_BITS = const(15)
"""The length of the random identifier in bits."""
@ -71,7 +72,7 @@ _ID_LENGTH_BITS = const(15)
_ITERATION_EXP_LENGTH_BITS = const(5)
"""The length of the iteration exponent in bits."""
_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."""
_CHECKSUM_LENGTH_WORDS = const(3)
@ -89,7 +90,7 @@ _METADATA_LENGTH_WORDS = _ID_EXP_LENGTH_WORDS + 2 + _CHECKSUM_LENGTH_WORDS
_MIN_STRENGTH_BITS = const(128)
"""The minimum allowed entropy of the master secret."""
_MIN_MNEMONIC_LENGTH_WORDS = _METADATA_LENGTH_WORDS + bits_to_words(_MIN_STRENGTH_BITS)
_MIN_MNEMONIC_LENGTH_WORDS = _METADATA_LENGTH_WORDS + _bits_to_words(_MIN_STRENGTH_BITS)
"""The minimum allowed length of the mnemonic in words."""
_BASE_ITERATION_COUNT = const(10000)
@ -105,6 +106,289 @@ _DIGEST_INDEX = const(254)
"""The index of the share containing the digest of the shared secret."""
"""
# Keyboard functions
"""
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 ""
return slip39.button_sequence_to_word(int(prefix))
"""
# External API
"""
MAX_SHARE_COUNT = const(16)
"""The maximum number of shares that can be created."""
MAX_GROUP_COUNT = const(16)
"""The maximum number of groups that can be created."""
DEFAULT_ITERATION_EXPONENT = const(1)
class Share:
"""
Represents a single mnemonic and offers its parsed metadata.
"""
def __init__(
self,
identifier: int,
iteration_exponent: int,
group_index: int,
group_threshold: int,
group_count: int,
index: int,
threshold: int,
share_value: bytes,
):
self.identifier = identifier
self.iteration_exponent = iteration_exponent
self.group_index = group_index
self.group_threshold = group_threshold
self.group_count = group_count
self.index = index
self.threshold = threshold
self.share_value = share_value
def decrypt(
identifier: int,
iteration_exponent: int,
encrypted_master_secret: bytes,
passphrase: bytes,
) -> bytes:
"""
Converts the Encrypted Master Secret to a Master Secret by applying the passphrase.
This is analogous to BIP-39 passphrase derivation. We do not use the term "derive"
here, because passphrase function is symmetric in SLIP-39. We are using the terms
"encrypt" and "decrypt" instead.
"""
l = encrypted_master_secret[: len(encrypted_master_secret) // 2]
r = encrypted_master_secret[len(encrypted_master_secret) // 2 :]
salt = _get_salt(identifier)
for i in reversed(range(_ROUND_COUNT)):
(l, r) = (
r,
_xor(l, _round_function(i, passphrase, iteration_exponent, salt, r)),
)
return r + l
def generate_random_identifier() -> int:
"""Returns a randomly generated integer in the range 0, ... , 2**_ID_LENGTH_BITS - 1."""
identifier = int.from_bytes(random.bytes(_bits_to_bytes(_ID_LENGTH_BITS)), "big")
return identifier & ((1 << _ID_LENGTH_BITS) - 1)
def generate_mnemonics_from_data(
encrypted_master_secret: bytes, # The encrypted master secret to split.
identifier: int,
group_threshold: int, # The number of groups required to reconstruct the master secret.
groups: List[Tuple[int, int]], # A list of (member_threshold, member_count).
iteration_exponent: int,
) -> List[List[str]]:
"""
Splits an encrypted master secret into mnemonic shares using Shamir's secret sharing scheme.
The `groups` argument takes 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.
Returns a list of mnemonics, grouped by the groups.
"""
if group_threshold > len(groups):
raise ValueError(
"The requested group threshold ({}) must not exceed the number of groups ({}).".format(
group_threshold, len(groups)
)
)
if any(
member_threshold == 1 and member_count > 1
for member_threshold, member_count in groups
):
raise ValueError(
"Creating multiple member shares with member threshold 1 is not allowed. Use 1-of-1 member sharing instead."
)
# Split the Encrypted Master Secret on the group level.
group_shares = _split_secret(group_threshold, len(groups), encrypted_master_secret)
mnemonics = [] # type: List[List[str]]
for (member_threshold, member_count), (group_index, group_secret) in zip(
groups, group_shares
):
group_mnemonics = []
# Split the group's secret between shares.
shares = _split_secret(member_threshold, member_count, group_secret)
for member_index, value in shares:
group_mnemonics.append(
_encode_mnemonic(
identifier,
iteration_exponent,
group_index,
group_threshold,
len(groups),
member_index,
member_threshold,
value,
)
)
mnemonics.append(group_mnemonics)
return mnemonics
def combine_mnemonics(mnemonics: List[str]) -> Tuple[int, int, bytes, int]:
"""
Combines mnemonic shares to obtain the encrypted master secret which was previously
split using Shamir's secret sharing scheme.
Returns identifier, iteration exponent and the encrypted master secret.
"""
if not mnemonics:
raise MnemonicError("The list of mnemonics is empty.")
identifier, iteration_exponent, group_threshold, group_count, groups = _decode_mnemonics(
mnemonics
)
if len(groups) != group_threshold:
raise MnemonicError(
"Wrong number of mnemonic groups. Expected {} groups, but {} were provided.".format(
group_threshold, len(groups)
)
)
for group_index, group in groups.items():
if len(group[1]) != group[0]: # group[0] is threshold
raise MnemonicError(
"Wrong number of mnemonics. Expected {} mnemonics, but {} were provided.".format(
group[0], len(group[1])
)
)
group_shares = [
(group_index, _recover_secret(group[0], list(group[1])))
for group_index, group in groups.items()
]
encrypted_master_secret = _recover_secret(group_threshold, group_shares)
return identifier, iteration_exponent, encrypted_master_secret, group_count
def decode_mnemonic(mnemonic: str) -> Share:
"""Converts a share mnemonic to share data."""
mnemonic_data = tuple(_mnemonic_to_indices(mnemonic))
if len(mnemonic_data) < _MIN_MNEMONIC_LENGTH_WORDS:
raise MnemonicError(
"Invalid mnemonic length. The length of each mnemonic must be at least {} words.".format(
_MIN_MNEMONIC_LENGTH_WORDS
)
)
padding_len = (_RADIX_BITS * (len(mnemonic_data) - _METADATA_LENGTH_WORDS)) % 16
if padding_len > 8:
raise MnemonicError("Invalid mnemonic length.")
if not _rs1024_verify_checksum(mnemonic_data):
raise MnemonicError("Invalid mnemonic checksum.")
id_exp_int = _int_from_indices(mnemonic_data[:_ID_EXP_LENGTH_WORDS])
identifier = id_exp_int >> _ITERATION_EXP_LENGTH_BITS
iteration_exponent = id_exp_int & ((1 << _ITERATION_EXP_LENGTH_BITS) - 1)
tmp = _int_from_indices(
mnemonic_data[_ID_EXP_LENGTH_WORDS : _ID_EXP_LENGTH_WORDS + 2]
)
group_index, group_threshold, group_count, member_index, member_threshold = _int_to_indices(
tmp, 5, 4
)
value_data = mnemonic_data[_ID_EXP_LENGTH_WORDS + 2 : -_CHECKSUM_LENGTH_WORDS]
if group_count < group_threshold:
raise MnemonicError(
"Invalid mnemonic. Group threshold cannot be greater than group count."
)
value_byte_count = _bits_to_bytes(_RADIX_BITS * len(value_data) - padding_len)
value_int = _int_from_indices(value_data)
if value_data[0] >= 1 << (_RADIX_BITS - padding_len):
raise MnemonicError("Invalid mnemonic padding")
value = value_int.to_bytes(value_byte_count, "big")
return Share(
identifier,
iteration_exponent,
group_index,
group_threshold + 1,
group_count + 1,
member_index,
member_threshold + 1,
value,
)
"""
## Convert mnemonics or integers to incices and back
"""
def _int_from_indices(indices: Indices) -> int:
"""Converts a list of base 1024 indices in big endian order to an integer value."""
value = 0
for index in indices:
value = (value << _RADIX_BITS) + index
return value
def _int_to_indices(value: int, length: int, bits: int) -> Iterable[int]:
"""Converts an integer value to indices in big endian order."""
mask = (1 << bits) - 1
return ((value >> (i * bits)) & mask for i in reversed(range(length)))
def _mnemonic_from_indices(indices: Indices) -> str:
return " ".join(slip39.get_word(i) for i in indices)
def _mnemonic_to_indices(mnemonic: str) -> Iterable[int]:
return (slip39.word_index(word.lower()) for word in mnemonic.split())
"""
## Checksum functions
"""
def _rs1024_create_checksum(data: Indices) -> Indices:
"""
This implements the checksum - a Reed-Solomon code over GF(1024) that guarantees
detection of any error affecting at most 3 words and has less than a 1 in 10^9
chance of failing to detect more errors.
"""
values = tuple(_CUSTOMIZATION_STRING) + data + _CHECKSUM_LENGTH_WORDS * (0,)
polymod = _rs1024_polymod(values) ^ 1
return tuple(
(polymod >> 10 * i) & 1023 for i in reversed(range(_CHECKSUM_LENGTH_WORDS))
)
def _rs1024_polymod(values: Indices) -> int:
GEN = (
0xE0E040,
@ -127,19 +411,18 @@ def _rs1024_polymod(values: Indices) -> int:
return chk
def rs1024_create_checksum(data: Indices) -> Indices:
values = tuple(_CUSTOMIZATION_STRING) + data + _CHECKSUM_LENGTH_WORDS * (0,)
polymod = _rs1024_polymod(values) ^ 1
return tuple(
(polymod >> 10 * i) & 1023 for i in reversed(range(_CHECKSUM_LENGTH_WORDS))
)
def rs1024_verify_checksum(data: Indices) -> bool:
def _rs1024_verify_checksum(data: Indices) -> bool:
"""
Verifies a checksum of the given mnemonic, which was already parsed into Indices.
"""
return _rs1024_polymod(tuple(_CUSTOMIZATION_STRING) + data) == 1
def rs1024_error_index(data: Indices) -> Optional[int]:
def _rs1024_error_index(data: Indices) -> Optional[int]:
"""
Returns the index where an error possibly occurred.
Currently unused.
"""
GEN = (
0x91F9F87,
0x122F1F07,
@ -166,30 +449,9 @@ def rs1024_error_index(data: Indices) -> Optional[int]:
return None
def xor(a: bytes, b: bytes) -> bytes:
return bytes(x ^ y for x, y in zip(a, b))
def _int_from_indices(indices: Indices) -> int:
"""Converts a list of base 1024 indices in big endian order to an integer value."""
value = 0
for index in indices:
value = (value << _RADIX_BITS) + index
return value
def _int_to_indices(value: int, length: int, bits: int) -> Iterable[int]:
"""Converts an integer value to indices in big endian order."""
mask = (1 << bits) - 1
return ((value >> (i * bits)) & mask for i in reversed(range(length)))
def mnemonic_from_indices(indices: Indices) -> str:
return " ".join(slip39.get_word(i) for i in indices)
def mnemonic_to_indices(mnemonic: str) -> Iterable[int]:
return (slip39.word_index(word.lower()) for word in mnemonic.split())
"""
## Internal functions
"""
def _round_function(i: int, passphrase: bytes, e: int, salt: bytes, r: bytes) -> bytes:
@ -204,27 +466,10 @@ def _round_function(i: int, passphrase: bytes, e: int, salt: bytes, r: bytes) ->
def _get_salt(identifier: int) -> bytes:
return _CUSTOMIZATION_STRING + identifier.to_bytes(
bits_to_bytes(_ID_LENGTH_BITS), "big"
_bits_to_bytes(_ID_LENGTH_BITS), "big"
)
def decrypt(
identifier: int,
iteration_exponent: int,
encrypted_master_secret: bytes,
passphrase: bytes,
) -> bytes:
l = encrypted_master_secret[: len(encrypted_master_secret) // 2]
r = encrypted_master_secret[len(encrypted_master_secret) // 2 :]
salt = _get_salt(identifier)
for i in reversed(range(_ROUND_COUNT)):
(l, r) = (
r,
xor(l, _round_function(i, passphrase, iteration_exponent, salt, r)),
)
return r + l
def _create_digest(random_data: bytes, shared_secret: bytes) -> bytes:
return hmac.new(random_data, shared_secret, hashlib.sha256).digest()[
:_DIGEST_LENGTH_BYTES
@ -304,33 +549,23 @@ def _group_prefix(
)
def encode_mnemonic(
def _encode_mnemonic(
identifier: int,
iteration_exponent: int,
group_index: int,
group_threshold: int,
group_count: int,
member_index: int,
member_threshold: int,
value: bytes,
group_index: int, # The x coordinate of the group share.
group_threshold: int, # The number of group shares needed to reconstruct the encrypted master secret.
group_count: int, # The total number of groups in existence.
member_index: int, # The x coordinate of the member share in the given group.
member_threshold: int, # The number of member shares needed to reconstruct the group share.
value: bytes, # The share value representing the y coordinates of the share.
) -> str:
"""
Converts share data to a share mnemonic.
:param int identifier: The random identifier.
:param int iteration_exponent: The iteration exponent.
:param int group_index: The x coordinate of the group share.
:param int group_threshold: The number of group shares needed to reconstruct the encrypted master secret.
:param int group_count: The total number of groups in existence.
:param int member_index: The x coordinate of the member share in the given group.
:param int member_threshold: The number of member shares needed to reconstruct the group share.
:param value: The share value representing the y coordinates of the share.
:type value: Array of bytes.
:return: The share mnemonic.
:rtype: Array of bytes.
Takes the metadata and the value to be encoded and converts it into a mnemonic words.
Also appends a checksum.
"""
# Convert the share value from bytes to wordlist indices.
value_word_count = bits_to_words(len(value) * 8)
value_word_count = _bits_to_words(len(value) * 8)
value_int = int.from_bytes(value, "big")
share_data = (
@ -344,66 +579,9 @@ def encode_mnemonic(
)
+ tuple(_int_to_indices(value_int, value_word_count, _RADIX_BITS))
)
checksum = rs1024_create_checksum(share_data)
return mnemonic_from_indices(share_data + checksum)
checksum = _rs1024_create_checksum(share_data)
def decode_mnemonic(mnemonic: str) -> Tuple[int, int, int, int, int, int, int, bytes]:
"""Converts a share mnemonic to share data."""
mnemonic_data = tuple(mnemonic_to_indices(mnemonic))
if len(mnemonic_data) < _MIN_MNEMONIC_LENGTH_WORDS:
raise MnemonicError(
"Invalid mnemonic length. The length of each mnemonic must be at least {} words.".format(
_MIN_MNEMONIC_LENGTH_WORDS
)
)
padding_len = (_RADIX_BITS * (len(mnemonic_data) - _METADATA_LENGTH_WORDS)) % 16
if padding_len > 8:
raise MnemonicError("Invalid mnemonic length.")
if not rs1024_verify_checksum(mnemonic_data):
raise MnemonicError("Invalid mnemonic checksum.")
id_exp_int = _int_from_indices(mnemonic_data[:_ID_EXP_LENGTH_WORDS])
identifier = id_exp_int >> _ITERATION_EXP_LENGTH_BITS
iteration_exponent = id_exp_int & ((1 << _ITERATION_EXP_LENGTH_BITS) - 1)
tmp = _int_from_indices(
mnemonic_data[_ID_EXP_LENGTH_WORDS : _ID_EXP_LENGTH_WORDS + 2]
)
group_index, group_threshold, group_count, member_index, member_threshold = _int_to_indices(
tmp, 5, 4
)
value_data = mnemonic_data[_ID_EXP_LENGTH_WORDS + 2 : -_CHECKSUM_LENGTH_WORDS]
if group_count < group_threshold:
raise MnemonicError(
"Invalid mnemonic. Group threshold cannot be greater than group count."
)
value_byte_count = bits_to_bytes(_RADIX_BITS * len(value_data) - padding_len)
value_int = _int_from_indices(value_data)
if value_data[0] >= 1 << (_RADIX_BITS - padding_len):
raise MnemonicError("Invalid mnemonic padding")
value = value_int.to_bytes(value_byte_count, "big")
return (
identifier,
iteration_exponent,
group_index,
group_threshold + 1,
group_count + 1,
member_index,
member_threshold + 1,
value,
)
if False:
MnemonicGroups = Dict[int, Tuple[int, Set[Tuple[int, bytes]]]]
return _mnemonic_from_indices(share_data + checksum)
def _decode_mnemonics(
@ -414,22 +592,20 @@ def _decode_mnemonics(
group_thresholds = set()
group_counts = set()
# { group_index : [member_threshold, set_of_member_shares] }
# { group_index : [threshold, set_of_member_shares] }
groups = {} # type: MnemonicGroups
for mnemonic in mnemonics:
identifier, iteration_exponent, group_index, group_threshold, group_count, member_index, member_threshold, share_value = decode_mnemonic(
mnemonic
)
identifiers.add(identifier)
iteration_exponents.add(iteration_exponent)
group_thresholds.add(group_threshold)
group_counts.add(group_count)
group = groups.setdefault(group_index, (member_threshold, set()))
if group[0] != member_threshold:
share = decode_mnemonic(mnemonic)
identifiers.add(share.identifier)
iteration_exponents.add(share.iteration_exponent)
group_thresholds.add(share.group_threshold)
group_counts.add(share.group_count)
group = groups.setdefault(share.group_index, (share.threshold, set()))
if group[0] != share.threshold:
raise MnemonicError(
"Invalid set of mnemonics. All mnemonics in a group must have the same member threshold."
)
group[1].add((member_index, share_value))
group[1].add((share.index, share.share_value))
if len(identifiers) != 1 or len(iteration_exponents) != 1:
raise MnemonicError(
@ -461,143 +637,3 @@ def _decode_mnemonics(
group_counts.pop(),
groups,
)
def generate_random_identifier() -> int:
"""Returns a randomly generated integer in the range 0, ... , 2**_ID_LENGTH_BITS - 1."""
identifier = int.from_bytes(random.bytes(bits_to_bytes(_ID_LENGTH_BITS)), "big")
return identifier & ((1 << _ID_LENGTH_BITS) - 1)
def generate_single_group_mnemonics_from_data(
encrypted_master_secret: bytes,
identifier: int,
threshold: int,
count: int,
iteration_exponent: int = DEFAULT_ITERATION_EXPONENT,
) -> List[str]:
return generate_mnemonics_from_data(
encrypted_master_secret, identifier, 1, [(threshold, count)], iteration_exponent
)[0]
def generate_mnemonics_from_data(
encrypted_master_secret: bytes,
identifier: int,
group_threshold: int,
groups: List[Tuple[int, int]],
iteration_exponent: int = DEFAULT_ITERATION_EXPONENT,
) -> List[List[str]]:
"""
Splits an encrypted master secret into mnemonic shares using Shamir's secret sharing scheme.
:param encrypted_master_secret: The encrypted master secret to split.
:type encrypted_master_secret: Array of bytes.
:param int identifier
: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 iteration_exponent: The iteration exponent.
:return: List of mnemonics.
:rtype: List of byte arrays.
:return: Identifier.
:rtype: int.
"""
if len(encrypted_master_secret) * 8 < _MIN_STRENGTH_BITS:
raise ValueError(
"The length of the encrypted master secret ({} bytes) must be at least {} bytes.".format(
len(encrypted_master_secret), bits_to_bytes(_MIN_STRENGTH_BITS)
)
)
if len(encrypted_master_secret) % 2 != 0:
raise ValueError(
"The length of the encrypted master secret in bytes must be an even number."
)
if group_threshold > len(groups):
raise ValueError(
"The requested group threshold ({}) must not exceed the number of groups ({}).".format(
group_threshold, len(groups)
)
)
if any(
member_threshold == 1 and member_count > 1
for member_threshold, member_count in groups
):
raise ValueError(
"Creating multiple member shares with member threshold 1 is not allowed. Use 1-of-1 member sharing instead."
)
group_shares = _split_secret(group_threshold, len(groups), encrypted_master_secret)
mnemonics = [] # type: List[List[str]]
for (member_threshold, member_count), (group_index, group_secret) in zip(
groups, group_shares
):
group_mnemonics = []
for member_index, value in _split_secret(
member_threshold, member_count, group_secret
):
group_mnemonics.append(
encode_mnemonic(
identifier,
iteration_exponent,
group_index,
group_threshold,
len(groups),
member_index,
member_threshold,
value,
)
)
mnemonics.append(group_mnemonics)
return mnemonics
def combine_mnemonics(mnemonics: List[str]) -> Tuple[int, int, bytes]:
"""
Combines mnemonic shares to obtain the master secret which was previously split using
Shamir's secret sharing scheme.
:param mnemonics: List of mnemonics.
:type mnemonics: List of strings.
:return: Identifier, iteration exponent, the encrypted master secret.
:rtype: Integer, integer, array of bytes.
"""
if not mnemonics:
raise MnemonicError("The list of mnemonics is empty.")
identifier, iteration_exponent, group_threshold, group_count, groups = _decode_mnemonics(
mnemonics
)
if len(groups) != group_threshold:
raise MnemonicError(
"Wrong number of mnemonic groups. Expected {} groups, but {} were provided.".format(
group_threshold, len(groups)
)
)
for group_index, group in groups.items():
if len(group[1]) != group[0]:
raise MnemonicError(
"Wrong number of mnemonics. Expected {} mnemonics, but {} were provided.".format(
group[0], len(group[1])
)
)
group_shares = [
(group_index, _recover_secret(group[0], list(group[1])))
for group_index, group in groups.items()
]
return (
identifier,
iteration_exponent,
_recover_secret(group_threshold, group_shares),
)

@ -5,15 +5,3 @@
class MnemonicError(RuntimeError):
pass
class IdentifierMismatchError(MnemonicError):
pass
class ShareAlreadyAddedError(MnemonicError):
pass
class GroupThresholdReachedError(MnemonicError):
pass

@ -1,5 +1,5 @@
# Automatically generated by pb2py
# fmt: off
Bip39 = 0
Slip39_Single_Group = 1
Slip39_Multiple_Groups = 2
Slip39_Basic = 1
Slip39_Advanced = 2

@ -7,9 +7,11 @@ if __debug__:
from typing import Dict, List, Optional
from typing_extensions import Literal # noqa: F401
EnumTypeCapability = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
EnumTypeBackupType = Literal[0, 1, 2]
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
EnumTypeCapability = None # type: ignore
EnumTypeBackupType = None # type: ignore
class Features(p.MessageType):
@ -46,6 +48,7 @@ class Features(p.MessageType):
no_backup: bool = None,
recovery_mode: bool = None,
capabilities: List[EnumTypeCapability] = None,
backup_type: EnumTypeBackupType = None,
) -> None:
self.vendor = vendor
self.major_version = major_version
@ -76,6 +79,7 @@ class Features(p.MessageType):
self.no_backup = no_backup
self.recovery_mode = recovery_mode
self.capabilities = capabilities if capabilities is not None else []
self.backup_type = backup_type
@classmethod
def get_fields(cls) -> Dict:
@ -109,4 +113,5 @@ class Features(p.MessageType):
28: ('no_backup', p.BoolType, 0),
29: ('recovery_mode', p.BoolType, 0),
30: ('capabilities', p.EnumType("Capability", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)), p.FLAG_REPEATED),
31: ('backup_type', p.EnumType("BackupType", (0, 1, 2)), 0),
}

@ -6,10 +6,10 @@ if __debug__:
try:
from typing import Dict, List, Optional
from typing_extensions import Literal # noqa: F401
EnumTypeResetDeviceBackupType = Literal[0, 1, 2]
EnumTypeBackupType = Literal[0, 1, 2]
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
EnumTypeResetDeviceBackupType = None # type: ignore
EnumTypeBackupType = None # type: ignore
class ResetDevice(p.MessageType):
@ -26,7 +26,7 @@ class ResetDevice(p.MessageType):
u2f_counter: int = None,
skip_backup: bool = None,
no_backup: bool = None,
backup_type: EnumTypeResetDeviceBackupType = None,
backup_type: EnumTypeBackupType = None,
) -> None:
self.display_random = display_random
self.strength = strength
@ -51,5 +51,5 @@ class ResetDevice(p.MessageType):
7: ('u2f_counter', p.UVarintType, 0),
8: ('skip_backup', p.BoolType, 0),
9: ('no_backup', p.BoolType, 0),
10: ('backup_type', p.EnumType("ResetDeviceBackupType", (0, 1, 2)), 0), # default=ResetDeviceBackupType_Bip39
10: ('backup_type', p.EnumType("BackupType", (0, 1, 2)), 0), # default=Bip39
}

@ -1,7 +1,7 @@
from micropython import const
from trezor import loop, res, ui
from trezor.ui.button import Button, ButtonCancel, ButtonConfirm
from trezor.ui.button import Button, ButtonCancel, ButtonConfirm, ButtonDefault
from trezor.ui.loader import Loader, LoaderDefault
if __debug__:
@ -14,6 +14,7 @@ if False:
CONFIRMED = object()
CANCELLED = object()
INFO = object()
class Confirm(ui.Layout):
@ -153,6 +154,54 @@ class ConfirmPageable(Confirm):
ui.display.icon(205, 68, icon, c, ui.BG)
class InfoConfirm(ui.Layout):
DEFAULT_CONFIRM = res.load(ui.ICON_CONFIRM)
DEFAULT_CONFIRM_STYLE = ButtonConfirm
DEFAULT_CANCEL = res.load(ui.ICON_CANCEL)
DEFAULT_CANCEL_STYLE = ButtonCancel
DEFAULT_INFO = res.load(ui.ICON_CLICK) # TODO: this should be (i) icon, not click
DEFAULT_INFO_STYLE = ButtonDefault
def __init__(
self,
content: ui.Component,
confirm: ButtonContent = DEFAULT_CONFIRM,
confirm_style: ButtonStyleType = DEFAULT_CONFIRM_STYLE,
cancel: ButtonContent = DEFAULT_CANCEL,
cancel_style: ButtonStyleType = DEFAULT_CANCEL_STYLE,
info: ButtonContent = DEFAULT_INFO,
info_style: ButtonStyleType = DEFAULT_INFO_STYLE,
) -> None:
self.content = content
self.confirm = Button(ui.grid(14), confirm, confirm_style)
self.confirm.on_click = self.on_confirm # type: ignore
self.info = Button(ui.grid(13), info, info_style)
self.info.on_click = self.on_info
self.cancel = Button(ui.grid(12), cancel, cancel_style)
self.cancel.on_click = self.on_cancel # type: ignore
def dispatch(self, event: int, x: int, y: int) -> None:
self.content.dispatch(event, x, y)
if self.confirm is not None:
self.confirm.dispatch(event, x, y)
if self.cancel is not None:
self.cancel.dispatch(event, x, y)
if self.info is not None:
self.info.dispatch(event, x, y)
def on_confirm(self) -> None:
raise ui.Result(CONFIRMED)
def on_cancel(self) -> None:
raise ui.Result(CANCELLED)
def on_info(self) -> None:
raise ui.Result(INFO)
class HoldToConfirm(ui.Layout):
DEFAULT_CONFIRM = "Hold To Confirm"
DEFAULT_CONFIRM_STYLE = ButtonConfirm

@ -188,7 +188,7 @@ class TestCardanoAddress(unittest.TestCase):
"talent drug much home firefly toxic analysis idea umbrella slice"
]
passphrase = b"TREZOR"
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics)
identifier, exponent, ems, _ = slip39.combine_mnemonics(mnemonics)
master_secret = slip39.decrypt(identifier, exponent, ems, passphrase)
node = bip32.from_seed(master_secret, "ed25519 cardano seed")
@ -252,7 +252,7 @@ class TestCardanoAddress(unittest.TestCase):
"quick silent downtown oral critical step remove says rhythm venture aunt"
]
passphrase = b"TREZOR"
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics)
identifier, exponent, ems, _ = slip39.combine_mnemonics(mnemonics)
master_secret = slip39.decrypt(identifier, exponent, ems, passphrase)
node = bip32.from_seed(master_secret, "ed25519 cardano seed")

@ -60,7 +60,7 @@ class TestCardanoGetPublicKey(unittest.TestCase):
"talent drug much home firefly toxic analysis idea umbrella slice"
]
passphrase = b"TREZOR"
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics)
identifier, exponent, ems, _ = slip39.combine_mnemonics(mnemonics)
master_secret = slip39.decrypt(identifier, exponent, ems, passphrase)
node = bip32.from_seed(master_secret, "ed25519 cardano seed")
@ -111,7 +111,7 @@ class TestCardanoGetPublicKey(unittest.TestCase):
"quick silent downtown oral critical step remove says rhythm venture aunt"
]
passphrase = b"TREZOR"
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics)
identifier, exponent, ems, _ = slip39.combine_mnemonics(mnemonics)
master_secret = slip39.decrypt(identifier, exponent, ems, passphrase)
node = bip32.from_seed(master_secret, "ed25519 cardano seed")

@ -8,7 +8,7 @@ from trezor.crypto.hashlib import sha256
class TestCredential(unittest.TestCase):
def test_fido2_credential_decode(self):
mnemonic_secret = b"all all all all all all all all all all all all"
mnemonic.get = lambda: (mnemonic_secret, mnemonic.TYPE_BIP39)
mnemonic.get_secret = lambda: mnemonic_secret
storage.is_initialized = lambda: True
cred_id = (

@ -25,21 +25,22 @@ class TestCryptoSlip39(unittest.TestCase):
EMS = b"ABCDEFGHIJKLMNOP"
def test_basic_sharing_random(self):
ms = random.bytes(32)
ems = random.bytes(32)
identifier = slip39.generate_random_identifier()
mnemonics = slip39.generate_mnemonics_from_data(ms, identifier, 1, [(3, 5)])
mnemonics = slip39.generate_mnemonics_from_data(ems, identifier, 1, [(3, 5)], 1)
mnemonics = mnemonics[0]
self.assertEqual(slip39.combine_mnemonics(mnemonics[:3]), slip39.combine_mnemonics(mnemonics[2:]))
def test_basic_sharing_fixed(self):
generated_identifier = slip39.generate_random_identifier()
mnemonics = slip39.generate_mnemonics_from_data(self.EMS, generated_identifier, 1, [(3, 5)])
mnemonics = slip39.generate_mnemonics_from_data(self.EMS, generated_identifier, 1, [(3, 5)], 1)
mnemonics = mnemonics[0]
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics[:3])
identifier, exponent, ems, group_count = slip39.combine_mnemonics(mnemonics[:3])
self.assertEqual(ems, self.EMS)
self.assertEqual(generated_identifier, identifier)
self.assertEqual(slip39.combine_mnemonics(mnemonics[1:4])[2], ems)
self.assertEqual(group_count, 1)
with self.assertRaises(slip39.MnemonicError):
slip39.combine_mnemonics(mnemonics[1:3])
@ -48,13 +49,15 @@ class TestCryptoSlip39(unittest.TestCase):
identifier = slip39.generate_random_identifier()
mnemonics = slip39.generate_mnemonics_from_data(self.EMS, identifier, 1, [(3, 5)], 1)
mnemonics = mnemonics[0]
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics[1:4])
identifier, exponent, ems, group_count = slip39.combine_mnemonics(mnemonics[1:4])
self.assertEqual(ems, self.EMS)
self.assertEqual(group_count, 1)
identifier = slip39.generate_random_identifier()
mnemonics = slip39.generate_mnemonics_from_data(self.EMS, identifier, 1, [(3, 5)], 2)
mnemonics = mnemonics[0]
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics[1:4])
identifier, exponent, ems, group_count = slip39.combine_mnemonics(mnemonics[1:4])
self.assertEqual(group_count, 1)
self.assertEqual(ems, self.EMS)
@ -64,7 +67,7 @@ class TestCryptoSlip39(unittest.TestCase):
member_thresholds = (3, 2, 2, 1)
identifier = slip39.generate_random_identifier()
mnemonics = slip39.generate_mnemonics_from_data(
self.EMS, identifier, group_threshold, list(zip(member_thresholds, group_sizes))
self.EMS, identifier, group_threshold, list(zip(member_thresholds, group_sizes)), 1
)
# Test all valid combinations of mnemonics.
@ -73,14 +76,16 @@ class TestCryptoSlip39(unittest.TestCase):
for group2_subset in combinations(groups[1][0], groups[1][1]):
mnemonic_subset = list(group1_subset + group2_subset)
random.shuffle(mnemonic_subset)
identifier, exponent, ems = slip39.combine_mnemonics(mnemonic_subset)
identifier, exponent, ems, group_count = slip39.combine_mnemonics(mnemonic_subset)
self.assertEqual(group_count, len(group_sizes))
self.assertEqual(ems, self.EMS)
# Minimal sets of mnemonics.
identifier, exponent, ems = slip39.combine_mnemonics([mnemonics[2][0], mnemonics[2][2], mnemonics[3][0]])
identifier, exponent, ems, group_count = slip39.combine_mnemonics([mnemonics[2][0], mnemonics[2][2], mnemonics[3][0]])
self.assertEqual(ems, self.EMS)
self.assertEqual(slip39.combine_mnemonics([mnemonics[2][3], mnemonics[3][0], mnemonics[2][4]])[2], ems)
self.assertEqual(group_count, len(group_sizes))
# One complete group and one incomplete group out of two groups required.
with self.assertRaises(slip39.MnemonicError):
@ -97,7 +102,7 @@ class TestCryptoSlip39(unittest.TestCase):
member_thresholds = (3, 2, 2, 1)
identifier = slip39.generate_random_identifier()
mnemonics = slip39.generate_mnemonics_from_data(
self.EMS, identifier, group_threshold, list(zip(member_thresholds, group_sizes))
self.EMS, identifier, group_threshold, list(zip(member_thresholds, group_sizes)), 1
)
# Test all valid combinations of mnemonics.
@ -105,7 +110,8 @@ class TestCryptoSlip39(unittest.TestCase):
for group_subset in combinations(group, threshold):
mnemonic_subset = list(group_subset)
random.shuffle(mnemonic_subset)
identifier, exponent, ems = slip39.combine_mnemonics(mnemonic_subset)
identifier, exponent, ems, group_count = slip39.combine_mnemonics(mnemonic_subset)
self.assertEqual(group_count, len(group_sizes))
self.assertEqual(ems, self.EMS)
@ -113,7 +119,7 @@ class TestCryptoSlip39(unittest.TestCase):
for group_threshold in (1, 2, 5):
identifier = slip39.generate_random_identifier()
mnemonics = slip39.generate_mnemonics_from_data(
self.EMS, identifier, group_threshold, [(3, 5), (1, 1), (2, 3), (2, 5), (3, 5)]
self.EMS, identifier, group_threshold, [(3, 5), (1, 1), (2, 3), (2, 5), (3, 5)], 1
)
self.assertEqual(len(mnemonics), 5)
self.assertEqual(len(sum(mnemonics, [])), 19)
@ -121,39 +127,32 @@ class TestCryptoSlip39(unittest.TestCase):
def test_invalid_sharing(self):
identifier = slip39.generate_random_identifier()
# Short master secret.
with self.assertRaises(ValueError):
slip39.generate_mnemonics_from_data(self.EMS[:14], identifier, 1, [(2, 3)])
# Odd length master secret.
with self.assertRaises(ValueError):
slip39.generate_mnemonics_from_data(self.EMS + b"X", identifier,1, [(2, 3)])
# Group threshold exceeds number of groups.
with self.assertRaises(ValueError):
slip39.generate_mnemonics_from_data(self.EMS, identifier, 3, [(3, 5), (2, 5)])
slip39.generate_mnemonics_from_data(self.EMS, identifier, 3, [(3, 5), (2, 5)], 1)
# Invalid group threshold.
with self.assertRaises(ValueError):
slip39.generate_mnemonics_from_data(self.EMS, identifier, 0, [(3, 5), (2, 5)])
slip39.generate_mnemonics_from_data(self.EMS, identifier, 0, [(3, 5), (2, 5)], 1)
# Member threshold exceeds number of members.
with self.assertRaises(ValueError):
slip39.generate_mnemonics_from_data(self.EMS, identifier, 2, [(3, 2), (2, 5)])
slip39.generate_mnemonics_from_data(self.EMS, identifier, 2, [(3, 2), (2, 5)], 1)
# Invalid member threshold.
with self.assertRaises(ValueError):
slip39.generate_mnemonics_from_data(self.EMS, identifier, 2, [(0, 2), (2, 5)])
slip39.generate_mnemonics_from_data(self.EMS, identifier, 2, [(0, 2), (2, 5)], 1)
# Group with multiple members and threshold 1.
with self.assertRaises(ValueError):
slip39.generate_mnemonics_from_data(self.EMS, identifier, 2, [(3, 5), (1, 3), (2, 5)])
slip39.generate_mnemonics_from_data(self.EMS, identifier, 2, [(3, 5), (1, 3), (2, 5)], 1)
def test_vectors(self):
for mnemonics, secret in vectors:
if secret:
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics)
identifier, exponent, ems, _ = slip39.combine_mnemonics(mnemonics)
self.assertEqual(slip39.decrypt(identifier, exponent, ems, b"TREZOR"), unhexlify(secret))
else:
with self.assertRaises(slip39.MnemonicError):
@ -166,12 +165,12 @@ class TestCryptoSlip39(unittest.TestCase):
"theory painting academic academic armed sweater year military elder discuss acne wildlife boring employer fused large satoshi bundle carbon diagnose anatomy hamster leaves tracks paces beyond phantom capital marvel lips brave detect luck",
]
for mnemonic in mnemonics:
data = tuple(slip39.mnemonic_to_indices(mnemonic))
self.assertEqual(slip39.rs1024_error_index(data), None)
data = tuple(slip39._mnemonic_to_indices(mnemonic))
self.assertEqual(slip39._rs1024_error_index(data), None)
for i in range(len(data)):
for _ in range(50):
error_data = error_data = data[:i] + (data[i] ^ (random.uniform(1023) + 1), ) + data[i + 1:]
self.assertEqual(slip39.rs1024_error_index(error_data), i)
self.assertEqual(slip39._rs1024_error_index(error_data), i)
if __name__ == '__main__':

@ -117,9 +117,9 @@ CHOICE_OUTPUT_SCRIPT_TYPE = ChoiceType(
CHOICE_RESET_DEVICE_TYPE = ChoiceType(
{
"single": proto.ResetDeviceBackupType.Bip39,
"shamir": proto.ResetDeviceBackupType.Slip39_Single_Group,
"advanced": proto.ResetDeviceBackupType.Slip39_Multiple_Groups,
"single": proto.BackupType.Bip39,
"shamir": proto.BackupType.Slip39_Basic,
"advanced": proto.BackupType.Slip39_Advanced,
}
)
@ -563,10 +563,7 @@ def reset_device(
strength = int(strength)
client = connect()
if (
client.features.model == "1"
and backup_type != proto.ResetDeviceBackupType.Bip39
):
if client.features.model == "1" and backup_type != proto.BackupType.Bip39:
click.echo(
"WARNING: Trezor One currently does not support Shamir backup.\n"
"Traditional single-seed backup will be generated instead."

@ -173,7 +173,7 @@ def reset(
u2f_counter=0,
skip_backup=False,
no_backup=False,
backup_type=proto.ResetDeviceBackupType.Bip39,
backup_type=proto.BackupType.Bip39,
):
if client.features.initialized:
raise RuntimeError(

@ -1,5 +1,5 @@
# Automatically generated by pb2py
# fmt: off
Bip39 = 0
Slip39_Single_Group = 1
Slip39_Multiple_Groups = 2
Slip39_Basic = 1
Slip39_Advanced = 2

@ -7,9 +7,11 @@ if __debug__:
from typing import Dict, List, Optional
from typing_extensions import Literal # noqa: F401
EnumTypeCapability = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
EnumTypeBackupType = Literal[0, 1, 2]
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
EnumTypeCapability = None # type: ignore
EnumTypeBackupType = None # type: ignore
class Features(p.MessageType):
@ -46,6 +48,7 @@ class Features(p.MessageType):
no_backup: bool = None,
recovery_mode: bool = None,
capabilities: List[EnumTypeCapability] = None,
backup_type: EnumTypeBackupType = None,
) -> None:
self.vendor = vendor
self.major_version = major_version
@ -76,6 +79,7 @@ class Features(p.MessageType):
self.no_backup = no_backup
self.recovery_mode = recovery_mode
self.capabilities = capabilities if capabilities is not None else []
self.backup_type = backup_type
@classmethod
def get_fields(cls) -> Dict:
@ -109,4 +113,5 @@ class Features(p.MessageType):
28: ('no_backup', p.BoolType, 0),
29: ('recovery_mode', p.BoolType, 0),
30: ('capabilities', p.EnumType("Capability", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)), p.FLAG_REPEATED),
31: ('backup_type', p.EnumType("BackupType", (0, 1, 2)), 0),
}

@ -6,10 +6,10 @@ if __debug__:
try:
from typing import Dict, List, Optional
from typing_extensions import Literal # noqa: F401
EnumTypeResetDeviceBackupType = Literal[0, 1, 2]
EnumTypeBackupType = Literal[0, 1, 2]
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
EnumTypeResetDeviceBackupType = None # type: ignore
EnumTypeBackupType = None # type: ignore
class ResetDevice(p.MessageType):
@ -26,7 +26,7 @@ class ResetDevice(p.MessageType):
u2f_counter: int = None,
skip_backup: bool = None,
no_backup: bool = None,
backup_type: EnumTypeResetDeviceBackupType = None,
backup_type: EnumTypeBackupType = None,
) -> None:
self.display_random = display_random
self.strength = strength
@ -51,5 +51,5 @@ class ResetDevice(p.MessageType):
7: ('u2f_counter', p.UVarintType, 0),
8: ('skip_backup', p.BoolType, 0),
9: ('no_backup', p.BoolType, 0),
10: ('backup_type', p.EnumType("ResetDeviceBackupType", (0, 1, 2)), 0), # default=ResetDeviceBackupType_Bip39
10: ('backup_type', p.EnumType("BackupType", (0, 1, 2)), 0), # default=Bip39
}

@ -256,6 +256,7 @@ from .WebAuthnRemoveResidentCredential import WebAuthnRemoveResidentCredential
from .WipeDevice import WipeDevice
from .WordAck import WordAck
from .WordRequest import WordRequest
from . import BackupType
from . import BinanceOrderSide
from . import BinanceOrderType
from . import BinanceTimeInForce
@ -274,7 +275,6 @@ from . import PassphraseSourceType
from . import PinMatrixRequestType
from . import RecoveryDeviceType
from . import RequestType
from . import ResetDeviceBackupType
from . import SdProtectOperationType
from . import TezosBallotType
from . import TezosContractType

@ -19,20 +19,24 @@ from trezorlib.messages import ButtonRequestType as B
# fmt: off
# 1 2 3 4 5 6 7 8 9 10 11 12
MNEMONIC12 = "alcohol woman abuse must during monitor noble actual mixed trade anger aisle"
# fmt: on
MNEMONIC_SHAMIR_20_3of6 = [
MNEMONIC_SLIP39_BASIC_20_3of6 = [
"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",
]
MNEMONIC_SHAMIR_20_2of3_2of3_GROUPS = [
"gesture negative ceramic leaf device fantasy style ceramic safari keyboard thumb total smug cage plunge aunt favorite lizard intend peanut",
"gesture negative acrobat leaf craft sidewalk adorn spider submit bumpy alcohol cards salon making prune decorate smoking image corner method",
"gesture negative acrobat lily bishop voting humidity rhyme parcel crunch elephant victim dish mailman triumph agree episode wealthy mayor beam",
"gesture negative beard leaf deadline stadium vegan employer armed marathon alien lunar broken edge justice military endorse diet sweater either",
"gesture negative beard lily desert belong speak realize explain bolt diet believe response counter medal luck wits glance remove ending",
# Shamir shares (128 bits, 2 groups from 1 of 1, 1 of 1, 3 of 5, 2 of 6)
MNEMONIC_SLIP39_ADVANCED_20 = [
"eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice",
"eraser senior ceramic snake clay various huge numb argue hesitate auction category timber browser greatest hanger petition script leaf pickup",
"eraser senior ceramic shaft dynamic become junior wrist silver peasant force math alto coal amazing segment yelp velvet image paces",
"eraser senior ceramic round column hawk trust auction smug shame alive greatest sheriff living perfect corner chest sled fumes adequate",
]
# Shamir shares (256 bits, 2 groups from 1 of 1, 1 of 1, 3 of 5, 2 of 6):
MNEMONIC_SLIP39_ADVANCED_33 = [
"wildlife deal beard romp alcohol space mild usual clothes union nuclear testify course research heat listen task location thank hospital slice smell failure fawn helpful priest ambition average recover lecture process dough stadium",
"wildlife deal acrobat romp anxiety axis starting require metric flexible geology game drove editor edge screw helpful have huge holy making pitch unknown carve holiday numb glasses survive already tenant adapt goat fangs",
]
# fmt: on
def generate_entropy(strength, internal_entropy, external_entropy):
@ -91,13 +95,6 @@ def recovery_enter_shares(debug, shares, groups=False):
debug.press_yes()
# Enter shares
for index, share in enumerate(shares):
if groups and index >= 1:
# confirm remaining shares
debug.swipe_down()
code = yield
assert code == B.Other
debug.press_yes()
code = yield
assert code == B.MnemonicInput
# Enter mnemonic words

@ -19,7 +19,7 @@ import pytest
from trezorlib.cardano import get_address
from trezorlib.tools import parse_path
from ..common import MNEMONIC_SHAMIR_20_3of6
from ..common import MNEMONIC_SLIP39_BASIC_20_3of6
@pytest.mark.altcoin
@ -42,7 +42,7 @@ from ..common import MNEMONIC_SHAMIR_20_3of6
),
],
)
@pytest.mark.setup_client(mnemonic=MNEMONIC_SHAMIR_20_3of6, passphrase=True)
@pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6, passphrase=True)
def test_cardano_get_address(client, path, expected_address):
# enter passphrase
assert client.features.passphrase_protection is True

@ -19,13 +19,13 @@ import pytest
from trezorlib.cardano import get_public_key
from trezorlib.tools import parse_path
from ..common import MNEMONIC_SHAMIR_20_3of6
from ..common import MNEMONIC_SLIP39_BASIC_20_3of6
@pytest.mark.altcoin
@pytest.mark.cardano
@pytest.mark.skip_t1 # T1 support is not planned
@pytest.mark.setup_client(mnemonic=MNEMONIC_SHAMIR_20_3of6, passphrase=True)
@pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6, passphrase=True)
@pytest.mark.parametrize(
"path,public_key,chain_code",
[

@ -18,7 +18,7 @@ import pytest
from trezorlib import cardano, messages
from ..common import MNEMONIC_SHAMIR_20_3of6
from ..common import MNEMONIC_SLIP39_BASIC_20_3of6
PROTOCOL_MAGICS = {"mainnet": 764824073, "testnet": 1097911063}
@ -109,7 +109,7 @@ VALID_VECTORS = [
@pytest.mark.altcoin
@pytest.mark.cardano
@pytest.mark.skip_t1 # T1 support is not planned
@pytest.mark.setup_client(mnemonic=MNEMONIC_SHAMIR_20_3of6, passphrase=True)
@pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6, passphrase=True)
@pytest.mark.parametrize(
"protocol_magic,inputs,outputs,transactions,tx_hash,tx_body", VALID_VECTORS
)

@ -17,9 +17,14 @@
import pytest
from trezorlib import btc, debuglink, device
from trezorlib.messages import BackupType
from trezorlib.messages.PassphraseSourceType import HOST as PASSPHRASE_ON_HOST
from ..common import MNEMONIC12
from ..common import (
MNEMONIC12,
MNEMONIC_SLIP39_ADVANCED_20,
MNEMONIC_SLIP39_BASIC_20_3of6,
)
@pytest.mark.setup_client(uninitialized=True)
@ -62,6 +67,28 @@ class TestDeviceLoad:
address = btc.get_address(client, "Bitcoin", [])
assert address == "15fiTDFwZd2kauHYYseifGi9daH2wniDHH"
@pytest.mark.skip_t1
def test_load_device_slip39_basic(self, client):
debuglink.load_device_by_mnemonic(
client,
mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6,
pin="",
passphrase_protection=False,
label="test",
)
assert client.features.backup_type == BackupType.Slip39_Basic
@pytest.mark.skip_t1
def test_load_device_slip39_advanced(self, client):
debuglink.load_device_by_mnemonic(
client,
mnemonic=MNEMONIC_SLIP39_ADVANCED_20,
pin="",
passphrase_protection=False,
label="test",
)
assert client.features.backup_type == BackupType.Slip39_Advanced
def test_load_device_utf(self, client):
words_nfkd = u"Pr\u030ci\u0301s\u030cerne\u030c z\u030clut\u030couc\u030cky\u0301 ku\u030an\u030c u\u0301pe\u030cl d\u030ca\u0301belske\u0301 o\u0301dy za\u0301ker\u030cny\u0301 uc\u030cen\u030c be\u030cz\u030ci\u0301 pode\u0301l zo\u0301ny u\u0301lu\u030a"
words_nfc = u"P\u0159\xed\u0161ern\u011b \u017elu\u0165ou\u010dk\xfd k\u016f\u0148 \xfap\u011bl \u010f\xe1belsk\xe9 \xf3dy z\xe1ke\u0159n\xfd u\u010de\u0148 b\u011b\u017e\xed pod\xe9l z\xf3ny \xfal\u016f"

@ -144,6 +144,7 @@ class TestMsgRecoverydeviceT2:
assert client.features.pin_protection is False
assert client.features.passphrase_protection is False
assert client.features.backup_type is proto.BackupType.Bip39
def test_already_initialized(self, client):
with pytest.raises(RuntimeError):

@ -0,0 +1,215 @@
# 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 pytest
from trezorlib import device, exceptions, messages
from ..common import (
MNEMONIC_SLIP39_ADVANCED_20,
MNEMONIC_SLIP39_ADVANCED_33,
recovery_enter_shares,
)
pytestmark = pytest.mark.skip_t1
EXTRA_GROUP_SHARE = [
"eraser senior decision smug corner ruin rescue cubic angel tackle skin skunk program roster trash rumor slush angel flea amazing"
]
# secrets generated using model T
VECTORS = (
(MNEMONIC_SLIP39_ADVANCED_20, "c2d2e26ad06023c60145f150abe2dd2b"),
(
MNEMONIC_SLIP39_ADVANCED_33,
"c41d5cf80fed71a008a3a0ae0458ff0c6d621b1a5522bccbfedbcfad87005c06",
),
)
@pytest.mark.parametrize("shares, secret", VECTORS)
@pytest.mark.setup_client(uninitialized=True)
def test_secret(client, shares, secret):
debug = client.debug
def input_flow():
yield # Confirm Recovery
debug.press_yes()
# Proceed with recovery
yield from recovery_enter_shares(debug, shares, groups=True)
with client:
client.set_input_flow(input_flow)
ret = device.recover(
client, pin_protection=False, passphrase_protection=False, label="label"
)
# Workflow succesfully ended
assert ret == messages.Success(message="Device recovered")
assert client.features.initialized is True
assert client.features.pin_protection is False
assert client.features.passphrase_protection is False
assert client.features.backup_type is messages.BackupType.Slip39_Advanced
assert debug.read_mnemonic_secret().hex() == secret
@pytest.mark.setup_client(uninitialized=True)
def test_extra_share_entered(client):
debug = client.debug
def input_flow():
yield # Confirm Recovery
debug.press_yes()
# Proceed with recovery
yield from recovery_enter_shares(
debug, EXTRA_GROUP_SHARE + MNEMONIC_SLIP39_ADVANCED_20, groups=True
)
with client:
client.set_input_flow(input_flow)
ret = device.recover(
client, pin_protection=False, passphrase_protection=False, label="label"
)
# Workflow succesfully ended
assert ret == messages.Success(message="Device recovered")
assert client.features.initialized is True
assert client.features.pin_protection is False
assert client.features.passphrase_protection is False
assert client.features.backup_type is messages.BackupType.Slip39_Advanced
@pytest.mark.setup_client(uninitialized=True)
def test_abort(client):
debug = client.debug
def input_flow():
yield # Confirm Recovery
debug.press_yes()
yield # Homescreen - abort process
debug.press_no()
yield # Homescreen - confirm abort
debug.press_yes()
with client:
client.set_input_flow(input_flow)
with pytest.raises(exceptions.Cancelled):
device.recover(client, pin_protection=False, label="label")
client.init_device()
assert client.features.initialized is False
@pytest.mark.setup_client(uninitialized=True)
def test_noabort(client):
debug = client.debug
def input_flow():
yield # Confirm Recovery
debug.press_yes()
yield # Homescreen - abort process
debug.press_no()
yield # Homescreen - go back to process
debug.press_no()
yield from recovery_enter_shares(
debug, EXTRA_GROUP_SHARE + MNEMONIC_SLIP39_ADVANCED_20, groups=True
)
with client:
client.set_input_flow(input_flow)
device.recover(client, pin_protection=False, label="label")
client.init_device()
assert client.features.initialized is True
@pytest.mark.setup_client(uninitialized=True)
def test_same_share(client):
debug = client.debug
# we choose the second share from the fixture because
# the 1st is 1of1 and group threshold condition is reached first
first_share = MNEMONIC_SLIP39_ADVANCED_20[1].split(" ")
# second share is first 4 words of first
second_share = MNEMONIC_SLIP39_ADVANCED_20[1].split(" ")[:4]
def input_flow():
yield # Confirm Recovery
debug.press_yes()
yield # Homescreen - start process
debug.press_yes()
yield # Enter number of words
debug.input(str(len(first_share)))
yield # Homescreen - proceed to share entry
debug.press_yes()
yield # Enter first share
for word in first_share:
debug.input(word)
yield # Continue to next share
debug.press_yes()
yield # Homescreen - next share
debug.press_yes()
yield # Enter next share
for word in second_share:
debug.input(word)
code = yield
assert code == messages.ButtonRequestType.Warning
client.cancel()
with client:
client.set_input_flow(input_flow)
with pytest.raises(exceptions.Cancelled):
device.recover(client, pin_protection=False, label="label")
@pytest.mark.setup_client(uninitialized=True)
def test_group_threshold_reached(client):
debug = client.debug
# first share in the fixture is 1of1 so we choose that
first_share = MNEMONIC_SLIP39_ADVANCED_20[0].split(" ")
# second share is first 3 words of first
second_share = MNEMONIC_SLIP39_ADVANCED_20[0].split(" ")[:3]
def input_flow():
yield # Confirm Recovery
debug.press_yes()
yield # Homescreen - start process
debug.press_yes()
yield # Enter number of words
debug.input(str(len(first_share)))
yield # Homescreen - proceed to share entry
debug.press_yes()
yield # Enter first share
for word in first_share:
debug.input(word)
yield # Continue to next share
debug.press_yes()
yield # Homescreen - next share
debug.press_yes()
yield # Enter next share
for word in second_share:
debug.input(word)
code = yield
assert code == messages.ButtonRequestType.Warning
client.cancel()
with client:
client.set_input_flow(input_flow)
with pytest.raises(exceptions.Cancelled):
device.recover(client, pin_protection=False, label="label")

@ -19,21 +19,24 @@ import pytest
from trezorlib import device, messages
from trezorlib.exceptions import TrezorFailure
from ..common import MNEMONIC_SHAMIR_20_2of3_2of3_GROUPS, recovery_enter_shares
from ..common import MNEMONIC_SLIP39_ADVANCED_20, recovery_enter_shares
pytestmark = pytest.mark.skip_t1
INVALID_SHARES_20_2of3_2of3_GROUPS = [
INVALID_SHARES_SLIP39_ADVANCED_20 = [
"chest garlic acrobat leaf diploma thank soul predator grant laundry camera license language likely slim twice amount rich total carve",
"chest garlic acrobat lily adequate dwarf genius wolf faint nylon scroll national necklace leader pants literary lift axle watch midst",
"chest garlic beard leaf coastal album dramatic learn identify angry dismiss goat plan describe round writing primary surprise sprinkle orbit",
"chest garlic beard lily burden pistol retreat pickup emphasis large gesture hand eyebrow season pleasure genuine election skunk champion income",
]
# Extra share from another group to make sure it does not matter.
EXTRA_GROUP_SHARE = [
"eraser senior decision smug corner ruin rescue cubic angel tackle skin skunk program roster trash rumor slush angel flea amazing"
]
@pytest.mark.setup_client(
mnemonic=MNEMONIC_SHAMIR_20_2of3_2of3_GROUPS[1:5], passphrase=False
)
@pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_ADVANCED_20, passphrase=False)
def test_2of3_dryrun(client):
debug = client.debug
@ -42,7 +45,7 @@ def test_2of3_dryrun(client):
debug.press_yes()
# run recovery flow
yield from recovery_enter_shares(
debug, MNEMONIC_SHAMIR_20_2of3_2of3_GROUPS, groups=True
debug, EXTRA_GROUP_SHARE + MNEMONIC_SLIP39_ADVANCED_20, groups=True
)
with client:
@ -62,9 +65,7 @@ def test_2of3_dryrun(client):
)
@pytest.mark.setup_client(
mnemonic=MNEMONIC_SHAMIR_20_2of3_2of3_GROUPS[1:5], passphrase=True
)
@pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_ADVANCED_20, passphrase=True)
def test_2of3_invalid_seed_dryrun(client):
debug = client.debug
@ -73,7 +74,7 @@ def test_2of3_invalid_seed_dryrun(client):
debug.press_yes()
# run recovery flow
yield from recovery_enter_shares(
debug, INVALID_SHARES_20_2of3_2of3_GROUPS, groups=True
debug, INVALID_SHARES_SLIP39_ADVANCED_20, groups=True
)
# test fails because of different seed on device

@ -18,20 +18,24 @@ import pytest
from trezorlib import device, exceptions, messages
from ..common import MNEMONIC_SHAMIR_20_3of6, recovery_enter_shares
from ..common import MNEMONIC_SLIP39_BASIC_20_3of6, recovery_enter_shares
pytestmark = pytest.mark.skip_t1
MNEMONIC_SLIP39_BASIC_20_1of1 = [
"academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic rebuild aquatic spew"
]
MNEMONIC_SHAMIR_33_2of5 = [
MNEMONIC_SLIP39_BASIC_33_2of5 = [
"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",
]
VECTORS = (
(MNEMONIC_SHAMIR_20_3of6, "491b795b80fc21ccdf466c0fbc98c8fc"),
(MNEMONIC_SLIP39_BASIC_20_3of6, "491b795b80fc21ccdf466c0fbc98c8fc"),
(
MNEMONIC_SHAMIR_33_2of5,
MNEMONIC_SLIP39_BASIC_33_2of5,
"b770e0da1363247652de97a39bdbf2463be087848d709ecbf28e84508e31202a",
),
)
@ -56,6 +60,7 @@ def test_secret(client, shares, secret):
assert ret == messages.Success(message="Device recovered")
assert client.features.pin_protection is False
assert client.features.passphrase_protection is False
assert client.features.backup_type is messages.BackupType.Slip39_Basic
# Check mnemonic
assert debug.read_mnemonic_secret().hex() == secret
@ -73,7 +78,7 @@ def test_recover_with_pin_passphrase(client):
yield # Enter PIN again
debug.input("654")
# Proceed with recovery
yield from recovery_enter_shares(debug, MNEMONIC_SHAMIR_20_3of6)
yield from recovery_enter_shares(debug, MNEMONIC_SLIP39_BASIC_20_3of6)
with client:
client.set_input_flow(input_flow)
@ -85,6 +90,7 @@ def test_recover_with_pin_passphrase(client):
assert ret == messages.Success(message="Device recovered")
assert client.features.pin_protection is True
assert client.features.passphrase_protection is True
assert client.features.backup_type is messages.BackupType.Slip39_Basic
@pytest.mark.setup_client(uninitialized=True)
@ -118,7 +124,7 @@ def test_noabort(client):
debug.press_no()
yield # Homescreen - go back to process
debug.press_no()
yield from recovery_enter_shares(debug, MNEMONIC_SHAMIR_20_3of6)
yield from recovery_enter_shares(debug, MNEMONIC_SLIP39_BASIC_20_3of6)
with client:
client.set_input_flow(input_flow)
@ -131,7 +137,7 @@ def test_noabort(client):
@pytest.mark.parametrize("nth_word", range(3))
def test_wrong_nth_word(client, nth_word):
debug = client.debug
share = MNEMONIC_SHAMIR_20_3of6[0].split(" ")
share = MNEMONIC_SLIP39_BASIC_20_3of6[0].split(" ")
def input_flow():
yield # Confirm Recovery
@ -170,9 +176,9 @@ def test_wrong_nth_word(client, nth_word):
@pytest.mark.setup_client(uninitialized=True)
def test_same_share(client):
debug = client.debug
first_share = MNEMONIC_SHAMIR_20_3of6[0].split(" ")
first_share = MNEMONIC_SLIP39_BASIC_20_3of6[0].split(" ")
# second share is first 4 words of first
second_share = MNEMONIC_SHAMIR_20_3of6[0].split(" ")[:4]
second_share = MNEMONIC_SLIP39_BASIC_20_3of6[0].split(" ")[:4]
def input_flow():
yield # Confirm Recovery
@ -202,3 +208,29 @@ def test_same_share(client):
client.set_input_flow(input_flow)
with pytest.raises(exceptions.Cancelled):
device.recover(client, pin_protection=False, label="label")
@pytest.mark.setup_client(uninitialized=True)
def test_1of1(client):
debug = client.debug
def input_flow():
yield # Confirm Recovery
debug.press_yes()
# Proceed with recovery
yield from recovery_enter_shares(
debug, MNEMONIC_SLIP39_BASIC_20_1of1, groups=False
)
with client:
client.set_input_flow(input_flow)
ret = device.recover(
client, pin_protection=False, passphrase_protection=False, label="label"
)
# Workflow succesfully ended
assert ret == messages.Success(message="Device recovered")
assert client.features.initialized is True
assert client.features.pin_protection is False
assert client.features.passphrase_protection is False
assert client.features.backup_type is messages.BackupType.Slip39_Basic

@ -54,7 +54,6 @@ def test_2of3_dryrun(client):
label="label",
language="english",
dry_run=True,
type=messages.ResetDeviceBackupType.Slip39_Single_Group,
)
# Dry run was successful
@ -85,5 +84,4 @@ def test_2of3_invalid_seed_dryrun(client):
label="label",
language="english",
dry_run=True,
type=messages.ResetDeviceBackupType.Slip39_Single_Group,
)

@ -1,90 +0,0 @@
# 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 pytest
from trezorlib import device, exceptions, messages
from ..common import MNEMONIC_SHAMIR_20_2of3_2of3_GROUPS, recovery_enter_shares
pytestmark = pytest.mark.skip_t1
@pytest.mark.setup_client(uninitialized=True)
def test_recover_no_pin_no_passphrase(client):
debug = client.debug
def input_flow():
yield # Confirm Recovery
debug.press_yes()
# Proceed with recovery
yield from recovery_enter_shares(
debug, MNEMONIC_SHAMIR_20_2of3_2of3_GROUPS, groups=True
)
with client:
client.set_input_flow(input_flow)
ret = device.recover(
client, pin_protection=False, passphrase_protection=False, label="label"
)
# Workflow succesfully ended
assert ret == messages.Success(message="Device recovered")
assert client.features.initialized is True
assert client.features.pin_protection is False
assert client.features.passphrase_protection is False
@pytest.mark.setup_client(uninitialized=True)
def test_abort(client):
debug = client.debug
def input_flow():
yield # Confirm Recovery
debug.press_yes()
yield # Homescreen - abort process
debug.press_no()
yield # Homescreen - confirm abort
debug.press_yes()
with client:
client.set_input_flow(input_flow)
with pytest.raises(exceptions.Cancelled):
device.recover(client, pin_protection=False, label="label")
client.init_device()
assert client.features.initialized is False
@pytest.mark.setup_client(uninitialized=True)
def test_noabort(client):
debug = client.debug
def input_flow():
yield # Confirm Recovery
debug.press_yes()
yield # Homescreen - abort process
debug.press_no()
yield # Homescreen - go back to process
debug.press_no()
yield from recovery_enter_shares(
debug, MNEMONIC_SHAMIR_20_2of3_2of3_GROUPS, groups=True
)
with client:
client.set_input_flow(input_flow)
device.recover(client, pin_protection=False, label="label")
client.init_device()
assert client.features.initialized is True

@ -103,6 +103,7 @@ class TestMsgResetDeviceT2:
assert resp.needs_backup is False
assert resp.pin_protection is False
assert resp.passphrase_protection is False
assert resp.backup_type is proto.BackupType.Bip39
@pytest.mark.setup_client(uninitialized=True)
def test_reset_device_pin(self, client):

@ -20,7 +20,7 @@ import pytest
import shamir_mnemonic as shamir
from trezorlib import device, messages as proto
from trezorlib.messages import ButtonRequestType as B, ResetDeviceBackupType
from trezorlib.messages import BackupType, ButtonRequestType as B
from ..common import click_through, generate_entropy, read_and_confirm_mnemonic
@ -31,7 +31,7 @@ EXTERNAL_ENTROPY = b"zlutoucky kun upel divoke ody" * 2
class TestMsgResetDeviceT2:
# TODO: test with different options
@pytest.mark.setup_client(uninitialized=True)
def test_reset_device_supershamir(self, client):
def test_reset_device_slip39_advanced(self, client):
strength = 128
word_count = 20
member_threshold = 3
@ -56,7 +56,7 @@ class TestMsgResetDeviceT2:
for h in range(5):
# mnemonic phrases
btn_code = yield
assert btn_code == B.Other
assert btn_code == B.ResetDevice
mnemonic = read_and_confirm_mnemonic(client.debug, words=word_count)
all_mnemonics.append(mnemonic)
@ -93,55 +93,55 @@ class TestMsgResetDeviceT2:
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.ResetDevice), # group #5 counts
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Other), # show seeds
proto.ButtonRequest(code=B.ResetDevice), # show seeds
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success), # show seeds ends here
proto.ButtonRequest(code=B.Success),
proto.Success(),
@ -159,7 +159,7 @@ class TestMsgResetDeviceT2:
pin_protection=False,
label="test",
language="english",
backup_type=ResetDeviceBackupType.Slip39_Multiple_Groups,
backup_type=BackupType.Slip39_Advanced,
)
# generate secret locally
@ -174,6 +174,7 @@ class TestMsgResetDeviceT2:
assert client.features.needs_backup is False
assert client.features.pin_protection is False
assert client.features.passphrase_protection is False
assert client.features.backup_type is BackupType.Slip39_Advanced
def validate_mnemonics(mnemonics, threshold, expected_ems):

@ -22,7 +22,7 @@ import shamir_mnemonic as shamir
from shamir_mnemonic import MnemonicError
from trezorlib import device, messages as proto
from trezorlib.messages import ButtonRequestType as B, ResetDeviceBackupType
from trezorlib.messages import BackupType, ButtonRequestType as B
from ..common import click_through, generate_entropy, read_and_confirm_mnemonic
@ -33,7 +33,7 @@ EXTERNAL_ENTROPY = b"zlutoucky kun upel divoke ody" * 2
class TestMsgResetDeviceT2:
# TODO: test with different options
@pytest.mark.setup_client(uninitialized=True)
def test_reset_device_shamir(self, client):
def test_reset_device_slip39_basic(self, client):
strength = 128
member_threshold = 3
all_mnemonics = []
@ -53,7 +53,7 @@ class TestMsgResetDeviceT2:
for h in range(5):
# mnemonic phrases
btn_code = yield
assert btn_code == B.Other
assert btn_code == B.ResetDevice
mnemonic = read_and_confirm_mnemonic(client.debug, words=20)
all_mnemonics.append(mnemonic)
@ -80,15 +80,15 @@ class TestMsgResetDeviceT2:
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Other),
proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success),
proto.ButtonRequest(code=B.Success),
proto.Success(),
@ -106,7 +106,7 @@ class TestMsgResetDeviceT2:
pin_protection=False,
label="test",
language="english",
backup_type=ResetDeviceBackupType.Slip39_Single_Group,
backup_type=BackupType.Slip39_Basic,
)
# generate secret locally
@ -121,6 +121,7 @@ class TestMsgResetDeviceT2:
assert client.features.needs_backup is False
assert client.features.pin_protection is False
assert client.features.passphrase_protection is False
assert client.features.backup_type is BackupType.Slip39_Basic
def validate_mnemonics(mnemonics, threshold, expected_ems):

@ -0,0 +1,59 @@
# 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 pytest
from trezorlib import btc
from ..common import MNEMONIC_SLIP39_ADVANCED_20, MNEMONIC_SLIP39_ADVANCED_33
@pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_ADVANCED_20, passphrase=True)
@pytest.mark.skip_t1
def test_128bit_passphrase(client):
"""
BIP32 Root Key for passphrase TREZOR:
provided by Andrew, address calculated using Model T
xprv9s21ZrQH143K3dzDLfeY3cMp23u5vDeFYftu5RPYZPucKc99mNEddU4w99GxdgUGcSfMpVDxhnR1XpJzZNXRN1m6xNgnzFS5MwMP6QyBRKV
"""
assert client.features.passphrase_protection is True
client.set_passphrase("TREZOR")
address = btc.get_address(client, "Bitcoin", [])
assert address == "1CX5rv2vbSV8YFAZEAdMwRVqbxxswPnSPw"
client.state = None
client.clear_session()
client.set_passphrase("ROZERT")
address_compare = btc.get_address(client, "Bitcoin", [])
assert address != address_compare
@pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_ADVANCED_33, passphrase=True)
@pytest.mark.skip_t1
def test_256bit_passphrase(client):
"""
BIP32 Root Key for passphrase TREZOR:
provided by Andrew, address calculated using Model T
xprv9s21ZrQH143K2UspC9FRPfQC9NcDB4HPkx1XG9UEtuceYtpcCZ6ypNZWdgfxQ9dAFVeD1F4Zg4roY7nZm2LB7THPD6kaCege3M7EuS8v85c
"""
assert client.features.passphrase_protection is True
client.set_passphrase("TREZOR")
address = btc.get_address(client, "Bitcoin", [])
assert address == "18oNx6UczHWASBQXc5XQqdSdAAZyhUwdQU"
client.state = None
client.clear_session()
client.set_passphrase("ROZERT")
address_compare = btc.get_address(client, "Bitcoin", [])
assert address != address_compare

@ -18,10 +18,10 @@ import pytest
from trezorlib import btc
from ..common import MNEMONIC_SHAMIR_20_3of6
from ..common import MNEMONIC_SLIP39_BASIC_20_3of6
@pytest.mark.setup_client(mnemonic=MNEMONIC_SHAMIR_20_3of6, passphrase=True)
@pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6, passphrase=True)
@pytest.mark.skip_t1
def test_3of6_passphrase(client):
"""

@ -17,7 +17,7 @@
import pytest
from trezorlib import btc, device, messages
from trezorlib.messages import ButtonRequestType as B, ResetDeviceBackupType
from trezorlib.messages import BackupType, ButtonRequestType as B
from trezorlib.tools import parse_path
from ..common import click_through, read_and_confirm_mnemonic, recovery_enter_shares
@ -28,22 +28,26 @@ from ..common import click_through, read_and_confirm_mnemonic, recovery_enter_sh
def test_reset_recovery(client):
mnemonics = reset(client)
address_before = btc.get_address(client, "Bitcoin", parse_path("44'/0'/0'/0/0"))
# TODO: more combinations
selected_mnemonics = [
mnemonics[0],
mnemonics[1],
mnemonics[2],
mnemonics[5],
mnemonics[6],
mnemonics[7],
mnemonics[10],
mnemonics[11],
mnemonics[12],
# we're generating 3of5 groups 3of5 shares each
test_combinations = [
mnemonics[0:3] # shares 1-3 from groups 1-3
+ mnemonics[5:8]
+ mnemonics[10:13],
mnemonics[2:5] # shares 3-5 from groups 1-3
+ mnemonics[7:10]
+ mnemonics[12:15],
mnemonics[10:13] # shares 1-3 from groups 3-5
+ mnemonics[15:18]
+ mnemonics[20:23],
mnemonics[12:15] # shares 3-5 from groups 3-5
+ mnemonics[17:20]
+ mnemonics[22:25],
]
device.wipe(client)
recover(client, selected_mnemonics)
address_after = btc.get_address(client, "Bitcoin", parse_path("44'/0'/0'/0/0"))
assert address_before == address_after
for combination in test_combinations:
device.wipe(client)
recover(client, combination)
address_after = btc.get_address(client, "Bitcoin", parse_path("44'/0'/0'/0/0"))
assert address_before == address_after
def reset(client, strength=128):
@ -71,7 +75,7 @@ def reset(client, strength=128):
for h in range(5):
# mnemonic phrases
btn_code = yield
assert btn_code == B.Other
assert btn_code == B.ResetDevice
mnemonic = read_and_confirm_mnemonic(client.debug, words=word_count)
all_mnemonics.append(mnemonic)
@ -107,55 +111,55 @@ def reset(client, strength=128):
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.ResetDevice), # group #5 counts
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Other), # show seeds
messages.ButtonRequest(code=B.ResetDevice), # show seeds
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success), # show seeds ends here
messages.ButtonRequest(code=B.Success),
messages.Success(),
@ -173,7 +177,7 @@ def reset(client, strength=128):
pin_protection=False,
label="test",
language="english",
backup_type=ResetDeviceBackupType.Slip39_Multiple_Groups,
backup_type=BackupType.Slip39_Advanced,
)
client.set_input_flow(None)

@ -19,7 +19,7 @@ import itertools
import pytest
from trezorlib import btc, device, messages
from trezorlib.messages import ButtonRequestType as B, ResetDeviceBackupType
from trezorlib.messages import BackupType, ButtonRequestType as B
from trezorlib.tools import parse_path
from ..common import click_through, read_and_confirm_mnemonic, recovery_enter_shares
@ -60,7 +60,7 @@ def reset(client, strength=128):
for h in range(5):
# mnemonic phrases
btn_code = yield
assert btn_code == B.Other
assert btn_code == B.ResetDevice
mnemonic = read_and_confirm_mnemonic(client.debug, words=word_count)
all_mnemonics.append(mnemonic)
@ -86,15 +86,15 @@ def reset(client, strength=128):
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Other),
messages.ButtonRequest(code=B.ResetDevice),
messages.ButtonRequest(code=B.Success),
messages.ButtonRequest(code=B.Success),
messages.Success(),
@ -112,7 +112,7 @@ def reset(client, strength=128):
pin_protection=False,
label="test",
language="english",
backup_type=ResetDeviceBackupType.Slip39_Single_Group,
backup_type=BackupType.Slip39_Basic,
)
client.set_input_flow(None)
Loading…
Cancel
Save