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"; 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 * Request: Reset device to default state and ask for device details
* @start * @start
@ -58,13 +67,13 @@ message Features {
optional bool unfinished_backup = 27; // report unfinished backup (equals to Storage.unfinished_backup) 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 no_backup = 28; // report no backup (equals to Storage.no_backup)
optional bool recovery_mode = 29; // is recovery mode in progress 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 { enum Capability {
Capability_Bitcoin = 1; 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_Binance = 3;
Capability_Cardano = 4; 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_EOS = 6;
Capability_Ethereum = 7; Capability_Ethereum = 7;
Capability_Lisk = 8; Capability_Lisk = 8;
@ -77,6 +86,7 @@ message Features {
Capability_Shamir = 15; Capability_Shamir = 15;
Capability_ShamirGroups = 16; 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 uint32 u2f_counter = 7; // U2F counter
optional bool skip_backup = 8; // postpone seed backup to BackupDevice workflow optional bool skip_backup = 8; // postpone seed backup to BackupDevice workflow
optional bool no_backup = 9; // indicate that no backup is going to be made optional bool no_backup = 9; // indicate that no backup is going to be made
// type of the mnemonic backup
// type of the mnemonic backup (BIP-39 vs SLIP-39 single group vs SLIP-39 multiple groups) optional BackupType backup_type = 10 [default=Bip39];
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)
}
} }
/** /**

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

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

@ -2,13 +2,13 @@ from trezor import wire
from trezor.messages import ButtonRequestType from trezor.messages import ButtonRequestType
from trezor.messages.ButtonAck import ButtonAck from trezor.messages.ButtonAck import ButtonAck
from trezor.messages.ButtonRequest import ButtonRequest 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__: if __debug__:
from apps.debug import confirm_signal from apps.debug import confirm_signal
if False: if False:
from typing import Any from typing import Any, Callable
from trezor import ui from trezor import ui
from trezor.ui.confirm import ButtonContent, ButtonStyleType from trezor.ui.confirm import ButtonContent, ButtonStyleType
from trezor.ui.loader import LoaderStyleType from trezor.ui.loader import LoaderStyleType
@ -47,6 +47,37 @@ async def confirm(
return await ctx.wait(dialog) is CONFIRMED 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( async def hold_to_confirm(
ctx: wire.Context, ctx: wire.Context,
content: ui.Component, content: ui.Component,

@ -1,24 +1,12 @@
from micropython import const
from trezor import ui, workflow from trezor import ui, workflow
from trezor.crypto import bip39, slip39 from trezor.crypto import bip39, slip39
from trezor.messages import BackupType
from apps.common import storage from apps.common import storage
if False: if False:
from typing import Optional, Tuple from typing import Optional, Tuple
from trezor.messages.ResetDevice import EnumTypeBackupType
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,
}
def get() -> Tuple[Optional[bytes], int]: def get() -> Tuple[Optional[bytes], int]:
@ -29,15 +17,20 @@ def get_secret() -> Optional[bytes]:
return storage.device.get_mnemonic_secret() return storage.device.get_mnemonic_secret()
def get_type() -> int: def get_type() -> EnumTypeBackupType:
mnemonic_type = storage.device.get_mnemonic_type() or TYPE_BIP39 return storage.device.get_backup_type()
if mnemonic_type not in (TYPE_BIP39, TYPE_SLIP39, TYPE_SLIP39_GROUP):
raise RuntimeError("Invalid mnemonic type")
return mnemonic_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: def get_seed(passphrase: str = "", progress_bar: bool = True) -> bytes:
mnemonic_secret, mnemonic_type = get() mnemonic_secret = get_secret()
if mnemonic_secret is None: if mnemonic_secret is None:
raise ValueError("Mnemonic not set") raise ValueError("Mnemonic not set")
@ -46,10 +39,10 @@ def get_seed(passphrase: str = "", progress_bar: bool = True) -> bytes:
_start_progress() _start_progress()
render_func = _render_progress render_func = _render_progress
if mnemonic_type == TYPE_BIP39: if is_bip39():
seed = bip39.seed(mnemonic_secret.decode(), passphrase, render_func) 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() identifier = storage.device.get_slip39_identifier()
iteration_exponent = storage.device.get_slip39_iteration_exponent() iteration_exponent = storage.device.get_slip39_iteration_exponent()
if identifier is None or iteration_exponent is None: 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() identifier, iteration_exponent, mnemonic_secret, passphrase.encode()
) )
if progress_bar:
_stop_progress()
return seed 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: def _start_progress() -> None:
# Because we are drawing to the screen manually, without a layout, we # 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 # 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 p = 1000 * progress // total
ui.display.loader(p, False, 18, ui.WHITE, ui.BG) ui.display.loader(p, False, 18, ui.WHITE, ui.BG)
ui.display.refresh() ui.display.refresh()
def _stop_progress() -> None:
pass

@ -2,11 +2,13 @@ from micropython import const
from ubinascii import hexlify from ubinascii import hexlify
from trezor.crypto import random 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.sd_salt import SD_SALT_AUTH_KEY_LEN_BYTES
from apps.common.storage import common from apps.common.storage import common
if False: if False:
from trezor.messages.ResetDevice import EnumTypeBackupType
from typing import Optional from typing import Optional
# Namespace: # Namespace:
@ -28,11 +30,13 @@ _PASSPHRASE_SOURCE = const(0x0A) # int
_UNFINISHED_BACKUP = const(0x0B) # bool (0x01 or empty) _UNFINISHED_BACKUP = const(0x0B) # bool (0x01 or empty)
_AUTOLOCK_DELAY_MS = const(0x0C) # int _AUTOLOCK_DELAY_MS = const(0x0C) # int
_NO_BACKUP = const(0x0D) # bool (0x01 or empty) _NO_BACKUP = const(0x0D) # bool (0x01 or empty)
_MNEMONIC_TYPE = const(0x0E) # int _BACKUP_TYPE = const(0x0E) # int
_ROTATION = const(0x0F) # int _ROTATION = const(0x0F) # int
_SLIP39_IDENTIFIER = const(0x10) # bool _SLIP39_IDENTIFIER = const(0x10) # bool
_SLIP39_ITERATION_EXPONENT = const(0x11) # int _SLIP39_ITERATION_EXPONENT = const(0x11) # int
_SD_SALT_AUTH_KEY = const(0x12) # bytes _SD_SALT_AUTH_KEY = const(0x12) # bytes
_DEFAULT_BACKUP_TYPE = BackupType.Bip39
# fmt: on # fmt: on
HOMESCREEN_MAXSIZE = 16384 HOMESCREEN_MAXSIZE = 16384
@ -80,8 +84,19 @@ def get_mnemonic_secret() -> Optional[bytes]:
return common.get(_NAMESPACE, _MNEMONIC_SECRET) return common.get(_NAMESPACE, _MNEMONIC_SECRET)
def get_mnemonic_type() -> Optional[int]: def get_backup_type() -> EnumTypeBackupType:
return common.get_uint8(_NAMESPACE, _MNEMONIC_TYPE) 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: def has_passphrase() -> bool:
@ -94,13 +109,13 @@ def get_homescreen() -> Optional[bytes]:
def store_mnemonic_secret( def store_mnemonic_secret(
secret: bytes, secret: bytes,
mnemonic_type: int, backup_type: EnumTypeBackupType,
needs_backup: bool = False, needs_backup: bool = False,
no_backup: bool = False, no_backup: bool = False,
) -> None: ) -> None:
set_version(common.STORAGE_VERSION_CURRENT) set_version(common.STORAGE_VERSION_CURRENT)
common.set(_NAMESPACE, _MNEMONIC_SECRET, secret) 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) common.set_true_or_delete(_NAMESPACE, _NO_BACKUP, no_backup)
if not no_backup: if not no_backup:
common.set_true_or_delete(_NAMESPACE, _NEEDS_BACKUP, needs_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 from apps.common.storage import common, recovery_shares
if False:
from trezor.messages.ResetDevice import EnumTypeBackupType
# Namespace: # Namespace:
_NAMESPACE = common.APP_RECOVERY _NAMESPACE = common.APP_RECOVERY
@ -12,12 +15,13 @@ _NAMESPACE = common.APP_RECOVERY
_IN_PROGRESS = const(0x00) # bool _IN_PROGRESS = const(0x00) # bool
_DRY_RUN = const(0x01) # bool _DRY_RUN = const(0x01) # bool
_WORD_COUNT = const(0x02) # int _WORD_COUNT = const(0x02) # int
_REMAINING = const(0x05) # int
_SLIP39_IDENTIFIER = const(0x03) # bytes _SLIP39_IDENTIFIER = const(0x03) # bytes
_SLIP39_THRESHOLD = const(0x04) # int _SLIP39_THRESHOLD = const(0x04) # int
_REMAINING = const(0x05) # int
_SLIP39_ITERATION_EXPONENT = const(0x06) # int _SLIP39_ITERATION_EXPONENT = const(0x06) # int
_SLIP39_GROUP_COUNT = const(0x07) # int _SLIP39_GROUP_COUNT = const(0x07) # int
_SLIP39_GROUP_THRESHOLD = const(0x08) # int _SLIP39_GROUP_THRESHOLD = const(0x08) # int
_BACKUP_TYPE = const(0x09) # int
# fmt: on # fmt: on
if False: if False:
@ -48,6 +52,14 @@ def get_word_count() -> Optional[int]:
return common.get_uint8(_NAMESPACE, _WORD_COUNT) 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: def set_slip39_identifier(identifier: int) -> None:
common.set_uint16(_NAMESPACE, _SLIP39_IDENTIFIER, identifier) 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) 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. We store the remaining shares as a bytearray of length group_count.
Each byte represents share remaining for group of that group_index. 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. share count for a group.
""" """
remaining = common.get(_NAMESPACE, _REMAINING) remaining = common.get(_NAMESPACE, _REMAINING)
if not get_slip39_group_count(): group_count = get_slip39_group_count()
raise RuntimeError() if not group_count:
raise RuntimeError
if remaining is None: 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 = bytearray(remaining)
remaining[group_index] = shares_remaining remaining[group_index] = shares_remaining
common.set(_NAMESPACE, _REMAINING, 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) remaining = common.get(_NAMESPACE, _REMAINING)
if remaining is None or remaining[group_index] == slip39.MAX_SHARE_COUNT: if remaining is None or remaining[group_index] == slip39.MAX_SHARE_COUNT:
return None return None
@ -135,4 +148,5 @@ def end_progress() -> None:
common.delete(_NAMESPACE, _SLIP39_ITERATION_EXPONENT) common.delete(_NAMESPACE, _SLIP39_ITERATION_EXPONENT)
common.delete(_NAMESPACE, _SLIP39_GROUP_COUNT) common.delete(_NAMESPACE, _SLIP39_GROUP_COUNT)
common.delete(_NAMESPACE, _SLIP39_GROUP_THRESHOLD) common.delete(_NAMESPACE, _SLIP39_GROUP_THRESHOLD)
common.delete(_NAMESPACE, _BACKUP_TYPE)
recovery_shares.delete() recovery_shares.delete()

@ -20,20 +20,19 @@ def get(index: int) -> Optional[str]:
return None return None
def fetch() -> List[str]: def fetch() -> List[List[str]]:
mnemonics = [] mnemonics = []
if not recovery.get_slip39_group_count(): if not recovery.get_slip39_group_count():
raise RuntimeError return mnemonics
for index in range(0, slip39.MAX_SHARE_COUNT * recovery.get_slip39_group_count()): for i in range(recovery.get_slip39_group_count()):
m = get(index) mnemonics.append(fetch_group(i))
if m:
mnemonics.append(m)
return mnemonics return mnemonics
def fetch_group(group_index: int) -> List[str]: def fetch_group(group_index: int) -> List[str]:
mnemonics = [] 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): for index in range(starting_index, starting_index + slip39.MAX_SHARE_COUNT):
m = get(index) m = get(index)
if m: if m:
@ -43,5 +42,5 @@ def fetch_group(group_index: int) -> List[str]:
def delete() -> None: 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) 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.messages.Success import Success
from trezor.wire import register from trezor.wire import register
from apps.common import cache, storage from apps.common import cache, mnemonic, storage
if False: if False:
from typing import NoReturn from typing import NoReturn
@ -36,6 +36,7 @@ def get_features() -> Features:
f.no_backup = storage.device.no_backup() f.no_backup = storage.device.no_backup()
f.flags = storage.device.get_flags() f.flags = storage.device.get_flags()
f.recovery_mode = storage.recovery.is_in_progress() f.recovery_mode = storage.recovery.is_in_progress()
f.backup_type = mnemonic.get_type()
if utils.BITCOIN_ONLY: if utils.BITCOIN_ONLY:
f.capabilities = [ f.capabilities = [
Capability.Bitcoin, Capability.Bitcoin,

@ -2,11 +2,7 @@ from trezor import wire
from trezor.messages.Success import Success from trezor.messages.Success import Success
from apps.common import mnemonic, storage from apps.common import mnemonic, storage
from apps.management.common import layout from apps.management.reset_device import backup_seed, layout
from apps.management.reset_device import (
backup_group_slip39_wallet,
backup_slip39_wallet,
)
async def backup_device(ctx, msg): 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_unfinished_backup(True)
storage.device.set_backed_up() storage.device.set_backed_up()
if mnemonic_type == mnemonic.TYPE_SLIP39: await backup_seed(ctx, mnemonic_type, mnemonic_secret)
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())
storage.device.set_unfinished_backup(False) 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 import config, wire
from trezor.crypto import bip39, slip39 from trezor.crypto import bip39, slip39
from trezor.messages import BackupType
from trezor.messages.Success import Success from trezor.messages.Success import Success
from trezor.pin import pin_to_int from trezor.pin import pin_to_int
from trezor.ui.text import Text from trezor.ui.text import Text
from apps.common import mnemonic, storage from apps.common import storage
from apps.common.confirm import require_confirm from apps.common.confirm import require_confirm
from apps.management import backup_types
async def load_device(ctx, msg): 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(): if storage.is_initialized():
raise wire.UnexpectedMessage("Already 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" "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 = Text("Loading seed")
text.bold("Loading private seed", "is not recommended.") text.bold("Loading private seed", "is not recommended.")
text.normal("Continue only if you", "know what you are doing!") text.normal("Continue only if you", "know what you are doing!")
await require_confirm(ctx, text) await require_confirm(ctx, text)
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 import loop, utils, wire
from trezor.crypto.hashlib import sha256 from trezor.crypto.hashlib import sha256
from trezor.errors import ( from trezor.crypto.slip39 import MAX_SHARE_COUNT, Share
GroupThresholdReachedError, from trezor.errors import MnemonicError
IdentifierMismatchError, from trezor.messages import BackupType
MnemonicError,
ShareAlreadyAddedError,
)
from trezor.messages.Success import Success from trezor.messages.Success import Success
from . import recover from . import recover
from apps.common import mnemonic, storage from apps.common import mnemonic, storage
from apps.common.layout import show_success from apps.common.layout import show_success
from apps.management import backup_types
from apps.management.recovery_device import layout from apps.management.recovery_device import layout
if False: if False:
from typing import List from typing import Optional, Tuple
from trezor.messages.ResetDevice import EnumTypeBackupType
async def recovery_homescreen() -> None: 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: async def _continue_recovery_process(ctx: wire.Context) -> Success:
# gather the current recovery state from storage # gather the current recovery state from storage
in_progress = storage.recovery.is_in_progress()
word_count = storage.recovery.get_word_count() word_count = storage.recovery.get_word_count()
dry_run = storage.recovery.is_dry_run() 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 if not word_count: # the first run, prompt word count from the user
word_count = await _request_and_store_word_count(ctx, dry_run) 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: if dry_run:
result = await _finish_recovery_dry_run(ctx, secret, mnemonic_type) result = await _finish_recovery_dry_run(ctx, secret)
else: else:
result = await _finish_recovery(ctx, secret, mnemonic_type) result = await _finish_recovery(ctx, secret)
return result return result
async def _finish_recovery_dry_run( async def _finish_recovery_dry_run(ctx: wire.Context, secret: bytes) -> Success:
ctx: wire.Context, secret: bytes, mnemonic_type: int backup_type = storage.recovery.get_backup_type()
) -> Success: if backup_type is None:
raise RuntimeError
digest_input = sha256(secret).digest() digest_input = sha256(secret).digest()
stored = mnemonic.get_secret() stored = mnemonic.get_secret()
digest_stored = sha256(stored).digest() digest_stored = sha256(stored).digest()
result = utils.consteq(digest_stored, digest_input) 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 # Check that the identifier and iteration exponent match as well
if mnemonic_type == mnemonic.TYPE_SLIP39: if is_slip39:
result &= ( result &= (
storage.device.get_slip39_identifier() storage.device.get_slip39_identifier()
== storage.recovery.get_slip39_identifier() == storage.recovery.get_slip39_identifier()
@ -84,7 +109,7 @@ async def _finish_recovery_dry_run(
== storage.recovery.get_slip39_iteration_exponent() == 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() 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") raise wire.ProcessError("The seed does not match the one in the device")
async def _finish_recovery( async def _finish_recovery(ctx: wire.Context, secret: bytes) -> Success:
ctx: wire.Context, secret: bytes, mnemonic_type: int backup_type = storage.recovery.get_backup_type()
) -> Success: if backup_type is None:
group_count = storage.recovery.get_slip39_group_count() raise RuntimeError
if group_count and group_count > 1:
mnemonic_type = mnemonic.TYPE_SLIP39_GROUP
storage.device.store_mnemonic_secret( storage.device.store_mnemonic_secret(
secret, mnemonic_type, needs_backup=False, no_backup=False secret, backup_type, needs_backup=False, no_backup=False
) )
if ( if backup_type in (BackupType.Slip39_Basic, BackupType.Slip39_Advanced):
mnemonic_type == mnemonic.TYPE_SLIP39
or mnemonic_type == mnemonic.TYPE_SLIP39_GROUP
):
identifier = storage.recovery.get_slip39_identifier() identifier = storage.recovery.get_slip39_identifier()
exponent = storage.recovery.get_slip39_iteration_exponent() exponent = storage.recovery.get_slip39_iteration_exponent()
if identifier is None or exponent is None: 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 return word_count
async def _request_secret( async def _process_words(
ctx: wire.Context, word_count: int, mnemonic_type: int ctx: wire.Context,
) -> bytes: words: str,
await _request_share_first_screen(ctx, word_count, mnemonic_type) is_slip39: bool,
backup_type: Optional[EnumTypeBackupType],
) -> Tuple[Optional[bytes], EnumTypeBackupType]:
mnemonics = None share = None
advanced_shamir = False if not is_slip39: # BIP-39
secret = None secret = recover.process_bip39(words)
while secret is None: else:
group_count = storage.recovery.get_slip39_group_count() secret, share = recover.process_slip39(words)
if group_count:
mnemonics = storage.recovery_shares.fetch() if backup_type is None:
advanced_shamir = group_count > 1 # we have to decide what backup type this is and store it
group_threshold = storage.recovery.get_slip39_group_threshold() backup_type = _store_backup_type(is_slip39, share)
shares_remaining = storage.recovery.fetch_slip39_remaining_shares()
if secret is None:
if advanced_shamir: if share.group_count and share.group_count > 1:
await _show_remaining_groups_and_shares( await layout.show_group_share_success(ctx, share.index, share.group_index)
ctx, group_threshold, shares_remaining 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( 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: ) -> None:
if mnemonic_type == mnemonic.TYPE_BIP39: if is_slip39:
content = layout.RecoveryHomescreen(
"Enter recovery seed", "(%d words)" % word_count
)
await layout.homescreen_dialog(ctx, content, "Enter seed")
elif mnemonic_type == mnemonic.TYPE_SLIP39:
remaining = storage.recovery.fetch_slip39_remaining_shares() remaining = storage.recovery.fetch_slip39_remaining_shares()
if remaining: if remaining:
await _request_share_next_screen(ctx, mnemonic_type) await _request_share_next_screen(ctx)
else: else:
content = layout.RecoveryHomescreen( content = layout.RecoveryHomescreen(
"Enter any share", "(%d words)" % word_count "Enter any share", "(%d words)" % word_count
) )
await layout.homescreen_dialog(ctx, content, "Enter share") await layout.homescreen_dialog(ctx, content, "Enter share")
else: else: # BIP-39
raise RuntimeError 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: async def _request_share_next_screen(ctx: wire.Context) -> None:
if mnemonic_type == mnemonic.TYPE_SLIP39: remaining = storage.recovery.fetch_slip39_remaining_shares()
remaining = storage.recovery.fetch_slip39_remaining_shares() group_count = storage.recovery.get_slip39_group_count()
group_count = storage.recovery.get_slip39_group_count() if not remaining:
if not remaining: # 'remaining' should be stored at this point
# 'remaining' should be stored at this point raise RuntimeError
raise RuntimeError
if group_count > 1: if group_count > 1:
content = layout.RecoveryHomescreen( content = layout.RecoveryHomescreen("More shares needed")
"More shares needed", "for this recovery" await layout.homescreen_dialog(
) ctx, content, "Enter", _show_remaining_groups_and_shares
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")
else: 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( async def _show_remaining_groups_and_shares(ctx: wire.Context) -> None:
ctx: wire.Context, group_threshold: int, shares_remaining: List[int] """
) -> None: Show info dialog for Slip39 Advanced - what shares are to be entered.
identifiers = [] """
shares_remaining = storage.recovery.fetch_slip39_remaining_shares()
identifiers = []
first_entered_index = -1 first_entered_index = -1
for i in range(len(shares_remaining)): for i in range(len(shares_remaining)):
if shares_remaining[i] < 16: if shares_remaining[i] < MAX_SHARE_COUNT:
first_entered_index = i first_entered_index = i
for i, r in enumerate(shares_remaining): 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] identifier = storage.recovery_shares.fetch_group(i)[0].split(" ")[0:3]
identifiers.append([r, identifier]) identifiers.append([r, identifier])
elif r == 16: elif r == MAX_SHARE_COUNT:
identifier = storage.recovery_shares.fetch_group(first_entered_index)[ identifier = storage.recovery_shares.fetch_group(first_entered_index)[
0 0
].split(" ")[0:2] ].split(" ")[0:2]
@ -257,6 +260,4 @@ async def _show_remaining_groups_and_shares(
except ValueError: except ValueError:
identifiers.append([r, identifier]) identifiers.append([r, identifier])
return await layout.show_remaining_shares( return await layout.show_remaining_shares(ctx, identifiers, shares_remaining)
ctx, identifiers, group_threshold, shares_remaining
)

@ -1,9 +1,8 @@
from trezor import ui, wire from trezor import ui, wire
from trezor.errors import IdentifierMismatchError, ShareAlreadyAddedError from trezor.crypto.slip39 import MAX_SHARE_COUNT
from trezor.messages import ButtonRequestType from trezor.messages import BackupType, ButtonRequestType
from trezor.messages.ButtonAck import ButtonAck from trezor.messages.ButtonAck import ButtonAck
from trezor.messages.ButtonRequest import ButtonRequest from trezor.messages.ButtonRequest import ButtonRequest
from trezor.ui.info import InfoConfirm
from trezor.ui.scroll import Paginated from trezor.ui.scroll import Paginated
from trezor.ui.text import Text from trezor.ui.text import Text
from trezor.ui.word_select import WordSelector from trezor.ui.word_select import WordSelector
@ -12,15 +11,17 @@ from .keyboard_bip39 import Bip39Keyboard
from .keyboard_slip39 import Slip39Keyboard from .keyboard_slip39 import Slip39Keyboard
from .recover import RecoveryAborted from .recover import RecoveryAborted
from apps.common import mnemonic, storage from apps.common import storage
from apps.common.confirm import confirm, require_confirm from apps.common.confirm import confirm, info_confirm, require_confirm
from apps.common.layout import show_success, show_warning from apps.common.layout import show_success, show_warning
from apps.management import backup_types
if __debug__: if __debug__:
from apps.debug import input_signal, confirm_signal from apps.debug import input_signal
if False: 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: 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( async def request_mnemonic(
ctx: wire.Context, ctx: wire.Context, word_count: int, backup_type: Optional[EnumTypeBackupType]
word_count: int, ) -> Optional[str]:
mnemonic_type: int,
mnemonics: List[str],
advanced_shamir: bool = False,
) -> str:
await ctx.call(ButtonRequest(code=ButtonRequestType.MnemonicInput), ButtonAck) await ctx.call(ButtonRequest(code=ButtonRequestType.MnemonicInput), ButtonAck)
words = [] words = []
for i in range(word_count): 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)) keyboard = Slip39Keyboard("Type word %s of %s:" % (i + 1, word_count))
else: else:
keyboard = Bip39Keyboard("Type word %s of %s:" % (i + 1, word_count)) keyboard = Bip39Keyboard("Type word %s of %s:" % (i + 1, word_count))
@ -72,41 +69,95 @@ async def request_mnemonic(
else: else:
word = await ctx.wait(keyboard) word = await ctx.wait(keyboard)
if mnemonic_type == mnemonic.TYPE_SLIP39 and mnemonics: if not await check_word_validity(ctx, i, word, backup_type, words):
if not advanced_shamir: return None
# 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()
words.append(word) words.append(word)
return " ".join(words) 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( async def show_remaining_shares(
ctx: wire.Context, ctx: wire.Context,
groups: List[[int, List[str]]], # remaining + list 3 words groups: List[[int, List[str]]], # remaining + list 3 words
group_threshold: int,
shares_remaining: List[int], shares_remaining: List[int],
) -> None: ) -> None:
group_threshold = storage.recovery.get_slip39_group_threshold()
pages = [] pages = []
for remaining, group in groups: for remaining, group in groups:
if 0 < remaining < 16: if 0 < remaining < MAX_SHARE_COUNT:
text = Text("Remaining Shares") text = Text("Remaining Shares")
if remaining > 1: if remaining > 1:
text.bold("%s more shares starting" % remaining) text.bold("%s more shares starting" % remaining)
@ -115,7 +166,9 @@ async def show_remaining_shares(
for word in group: for word in group:
text.normal(word) text.normal(word)
pages.append(text) 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") text = Text("Remaining Shares")
groups_remaining = group_threshold - shares_remaining.count(0) groups_remaining = group_threshold - shares_remaining.count(0)
if groups_remaining > 1: if groups_remaining > 1:
@ -126,7 +179,7 @@ async def show_remaining_shares(
text.normal(word) text.normal(word)
pages.append(text) 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( 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) return await confirm(ctx, text, confirm="Continue", cancel=None)
async def show_dry_run_result( async def show_dry_run_result(ctx: wire.Context, result: bool, is_slip39: bool) -> None:
ctx: wire.Context, result: bool, mnemonic_type: int
) -> None:
if result: if result:
if mnemonic_type == mnemonic.TYPE_SLIP39: if is_slip39:
text = ( text = (
"The entered recovery", "The entered recovery",
"shares are valid and", "shares are valid and",
@ -161,7 +212,7 @@ async def show_dry_run_result(
) )
await show_success(ctx, text, button="Continue") await show_success(ctx, text, button="Continue")
else: else:
if mnemonic_type == mnemonic.TYPE_SLIP39: if is_slip39:
text = ( text = (
"The entered recovery", "The entered recovery",
"shares are valid but", "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: async def show_invalid_mnemonic(ctx: wire.Context, is_slip39: bool) -> None:
# TODO: do not send ButtonRequestType.Other if is_slip39:
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:
await show_warning(ctx, ("You have entered", "an invalid recovery", "share.")) await show_warning(ctx, ("You have entered", "an invalid recovery", "share."))
else: else:
await show_warning(ctx, ("You have entered", "an invalid recovery", "seed.")) await show_warning(ctx, ("You have entered", "an invalid recovery", "seed."))
@ -272,16 +306,30 @@ class RecoveryHomescreen(ui.Component):
async def homescreen_dialog( 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: ) -> None:
while True: while True:
continue_recovery = await confirm( if info_func:
ctx, continue_recovery = await info_confirm(
homepage, ctx,
code=ButtonRequestType.RecoveryHomepage, homepage,
confirm=button_label, code=ButtonRequestType.RecoveryHomepage,
major_confirm=True, 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: if continue_recovery:
# go forward in the recovery process # go forward in the recovery process
break break

@ -1,72 +1,81 @@
from trezor.crypto import bip39, slip39 from trezor.crypto import bip39, slip39
from trezor.errors import GroupThresholdReachedError, MnemonicError from trezor.errors import MnemonicError
from apps.common import storage from apps.common import storage
if False: if False:
from typing import Optional from typing import Optional, Tuple
class RecoveryAborted(Exception): class RecoveryAborted(Exception):
pass pass
_GROUP_STORAGE_OFFSET = 16
def process_bip39(words: str) -> bytes: def process_bip39(words: str) -> bytes:
""" """
Receives single mnemonic and processes it. Returns what is then stored Receives single mnemonic and processes it. Returns what is then stored
in the storage, which is the mnemonic itself for BIP-39. in the storage, which is the mnemonic itself for BIP-39.
""" """
if not bip39.check(words): if not bip39.check(words):
raise MnemonicError() raise MnemonicError
return words.encode() 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 Processes a single mnemonic share. Returns the encrypted master secret
None if more shares are needed. (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( share = slip39.decode_mnemonic(words)
words
) # TODO: use better data structure for this
remaining = storage.recovery.fetch_slip39_remaining_shares() 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 this is the first share, parse and store metadata
if not remaining: if not remaining:
storage.recovery.set_slip39_group_count(group_count) storage.recovery.set_slip39_group_count(share.group_count)
storage.recovery.set_slip39_group_threshold(group_threshold) storage.recovery.set_slip39_group_threshold(share.group_threshold)
storage.recovery.set_slip39_iteration_exponent(iteration_exponent) storage.recovery.set_slip39_iteration_exponent(share.iteration_exponent)
storage.recovery.set_slip39_identifier(identifier) storage.recovery.set_slip39_identifier(share.identifier)
storage.recovery.set_slip39_threshold(threshold) storage.recovery.set_slip39_threshold(share.threshold)
storage.recovery.set_slip39_remaining_shares(threshold - 1, group_index) storage.recovery.set_slip39_remaining_shares(
share.threshold - 1, share.group_index
)
storage.recovery_shares.set(index_with_group_offset, words) 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 # 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") raise RuntimeError("Slip39: Share identifiers do not match")
if storage.recovery_shares.get(index_with_group_offset): if storage.recovery_shares.get(index_with_group_offset):
raise RuntimeError("Slip39: This mnemonic was already entered") raise RuntimeError("Slip39: This mnemonic was already entered")
remaining_for_share = ( 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[share.group_index] = remaining_for_share - 1
remaining[group_index] = remaining_for_share - 1
storage.recovery_shares.set(index_with_group_offset, words) storage.recovery_shares.set(index_with_group_offset, words)
if remaining.count(0) < group_threshold: if remaining.count(0) < share.group_threshold:
return None, group_index, index # we need more shares # we need more shares
return None, share
if len(remaining) > 1: if share.group_count > 1:
mnemonics = [] mnemonics = []
for i, r in enumerate(remaining): for i, r in enumerate(remaining):
# if we have multiple groups pass only the ones with threshold reached # 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) group = storage.recovery_shares.fetch_group(i)
mnemonics.extend(group) mnemonics.extend(group)
else: 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) identifier, iteration_exponent, secret, _ = slip39.combine_mnemonics(mnemonics)
return secret, group_index, index 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.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.EntropyAck import EntropyAck
from trezor.messages.EntropyRequest import EntropyRequest from trezor.messages.EntropyRequest import EntropyRequest
from trezor.messages.Success import Success from trezor.messages.Success import Success
from trezor.pin import pin_to_int from trezor.pin import pin_to_int
from trezor.ui.loader import LoadingAnimation
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
from apps.common.request_pin import request_pin_confirm from apps.management.change_pin import request_pin_confirm
from apps.management.common import layout from apps.management.reset_device import layout
if __debug__: if __debug__:
from apps import debug from apps import debug
@ -19,18 +17,15 @@ if __debug__:
if False: if False:
from trezor.messages.ResetDevice import ResetDevice 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: async def reset_device(ctx: wire.Context, msg: ResetDevice) -> Success:
# validate parameters and device state # validate parameters and device state
_validate_reset_device(msg) _validate_reset_device(msg)
is_slip39_simple = msg.backup_type == ResetDeviceBackupType.Slip39_Single_Group # make sure user knows they're setting up a new wallet
is_slip39_group = msg.backup_type == ResetDeviceBackupType.Slip39_Multiple_Groups await layout.show_reset_device_warning(ctx, msg.backup_type)
# make sure user knows he's setting up a new wallet
await _show_reset_device_warning(ctx, msg.backup_type)
# request new PIN # request new PIN
if msg.pin_protection: 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 # For SLIP-39 this is the Encrypted Master Secret
secret = _compute_secret_from_entropy(int_entropy, ext_entropy, msg.strength) 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_identifier(slip39.generate_random_identifier())
storage.device.set_slip39_iteration_exponent(slip39.DEFAULT_ITERATION_EXPONENT) storage.device.set_slip39_iteration_exponent(slip39.DEFAULT_ITERATION_EXPONENT)
# should we back up the wallet now? # If either of skip_backup or no_backup is specified, we are not doing backup now.
if not msg.no_backup and not msg.skip_backup: # Otherwise, we try to do it.
if not await layout.confirm_backup(ctx): perform_backup = not msg.no_backup and not msg.skip_backup
if not await layout.confirm_backup_again(ctx):
msg.skip_backup = True # 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 # generate and display backup information for the master secret
if not msg.no_backup and not msg.skip_backup: if perform_backup:
if is_slip39_simple: await backup_seed(ctx, msg.backup_type, secret)
await backup_slip39_wallet(ctx, secret)
elif is_slip39_group:
await backup_group_slip39_wallet(ctx, secret)
else:
await backup_bip39_wallet(ctx, secret)
# write PIN into storage # write PIN into storage
if not config.change_pin(pin_to_int(""), pin_to_int(newpin), None, None): 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( storage.device.load_settings(
label=msg.label, use_passphrase=msg.passphrase_protection label=msg.label, use_passphrase=msg.passphrase_protection
) )
if is_slip39_simple or is_slip39_group: if msg.backup_type == BackupType.Bip39:
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:
# in BIP-39 we store mnemonic string instead of the secret # in BIP-39 we store mnemonic string instead of the secret
storage.device.store_mnemonic_secret( secret = bip39.from_data(secret).encode()
bip39.from_data(secret).encode(), elif msg.backup_type not in (BackupType.Slip39_Basic, BackupType.Slip39_Advanced):
mnemonic.TYPE_BIP39, # Unknown backup type.
needs_backup=msg.skip_backup, # This check might seem superfluous, because we are checking
no_backup=msg.no_backup, # 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 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) await layout.show_backup_success(ctx)
return Success(message="Initialized") return Success(message="Initialized")
async def backup_slip39_wallet( async def backup_slip39_basic(
ctx: wire.Context, encrypted_master_secret: bytes ctx: wire.Context, encrypted_master_secret: bytes
) -> None: ) -> None:
# get number of shares # 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) shares_count = await layout.slip39_prompt_number_of_shares(ctx)
# get threshold # 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) threshold = await layout.slip39_prompt_threshold(ctx, shares_count)
# generate the mnemonics # generate the mnemonics
mnemonics = slip39.generate_single_group_mnemonics_from_data( mnemonics = slip39.generate_mnemonics_from_data(
encrypted_master_secret, encrypted_master_secret,
storage.device.get_slip39_identifier(), storage.device.get_slip39_identifier(),
threshold, 1, # Single Group threshold
shares_count, [(threshold, shares_count)], # Single Group threshold/count
storage.device.get_slip39_iteration_exponent(), storage.device.get_slip39_iteration_exponent(),
) )[0]
# show and confirm individual shares # show and confirm individual shares
await layout.slip39_show_checklist_show_shares(ctx, shares_count, threshold) await layout.slip39_show_checklist(ctx, 2, BackupType.Slip39_Basic)
await layout.slip39_show_and_confirm_shares(ctx, mnemonics) 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 ctx: wire.Context, encrypted_master_secret: bytes
) -> None: ) -> None:
# get number of groups # get number of groups
await layout.slip39_group_show_checklist_set_groups(ctx) await layout.slip39_show_checklist(ctx, 0, BackupType.Slip39_Advanced)
groups_count = await layout.slip39_prompt_number_of_groups(ctx) groups_count = await layout.slip39_advanced_prompt_number_of_groups(ctx)
# get group threshold # get group threshold
await layout.slip39_group_show_checklist_set_group_threshold(ctx, groups_count) await layout.slip39_show_checklist(ctx, 1, BackupType.Slip39_Advanced)
group_threshold = await layout.slip39_prompt_group_threshold(ctx, groups_count) group_threshold = await layout.slip39_advanced_prompt_group_threshold(
ctx, groups_count
)
# get shares and thresholds # get shares and thresholds
await layout.slip39_group_show_checklist_set_shares( await layout.slip39_show_checklist(ctx, 2, BackupType.Slip39_Advanced)
ctx, groups_count, group_threshold
)
groups = [] groups = []
for i in range(groups_count): for i in range(groups_count):
share_count = await layout.slip39_prompt_number_of_shares(ctx, i) 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 # 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) mnemonic = bip39.from_data(secret)
await layout.bip39_show_and_confirm_mnemonic(ctx, mnemonic) 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: def _validate_reset_device(msg: ResetDevice) -> None:
msg.backup_type = msg.backup_type or _DEFAULT_BACKUP_TYPE msg.backup_type = msg.backup_type or _DEFAULT_BACKUP_TYPE
if msg.backup_type not in ( if msg.backup_type not in (
ResetDeviceBackupType.Bip39, BackupType.Bip39,
ResetDeviceBackupType.Slip39_Single_Group, BackupType.Slip39_Basic,
ResetDeviceBackupType.Slip39_Multiple_Groups, BackupType.Slip39_Advanced,
): ):
raise wire.ProcessError("Backup type not implemented.") raise wire.ProcessError("Backup type not implemented.")
if msg.strength not in (128, 256): if backup_types.is_slip39_backup_type(msg.backup_type):
if msg.backup_type == ResetDeviceBackupType.Slip39_Single_Group: if msg.strength not in (128, 256):
raise wire.ProcessError("Invalid strength (has to be 128 or 256 bits)") 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)") 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): if msg.display_random and (msg.skip_backup or msg.no_backup):
raise wire.ProcessError("Can't show internal entropy when backup is skipped") raise wire.ProcessError("Can't show internal entropy when backup is skipped")
@ -198,27 +192,12 @@ def _compute_secret_from_entropy(
return secret return secret
async def _show_reset_device_warning( async def backup_seed(
ctx, backup_type: ResetDeviceBackupType = ResetDeviceBackupType.Bip39 ctx: wire.Context, backup_type: BackupType, mnemonic_secret: bytes
): ):
text = Text("Create new wallet", ui.ICON_RESET, new_lines=False) if backup_type == BackupType.Slip39_Basic:
if backup_type == ResetDeviceBackupType.Slip39_Single_Group: await backup_slip39_basic(ctx, mnemonic_secret)
text.bold("Create a new wallet") elif backup_type == BackupType.Slip39_Advanced:
text.br() await backup_slip39_advanced(ctx, mnemonic_secret)
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?")
else: else:
text.bold("Do you want to create") await backup_bip39(ctx, mnemonic_secret)
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()

@ -1,14 +1,14 @@
import ubinascii import ubinascii
from micropython import const
from trezor import ui, utils from trezor import ui, utils
from trezor.crypto import random 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.button import Button, ButtonDefault
from trezor.ui.checklist import Checklist from trezor.ui.checklist import Checklist
from trezor.ui.info import InfoConfirm 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.scroll import Paginated
from trezor.ui.shamir import NumInput
from trezor.ui.text import Text from trezor.ui.text import Text
from apps.common.confirm import confirm, hold_to_confirm, require_confirm 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): async def confirm_backup(ctx):
# First prompt
text = Text("Success", ui.ICON_CONFIRM, ui.GREEN, new_lines=False) text = Text("Success", ui.ICON_CONFIRM, ui.GREEN, new_lines=False)
text.bold("New wallet created") text.bold("New wallet created")
text.br() text.br()
@ -36,17 +37,17 @@ async def confirm_backup(ctx):
text.normal("You should back up your") text.normal("You should back up your")
text.br() text.br()
text.normal("new wallet right now.") text.normal("new wallet right now.")
return await confirm( if await confirm(
ctx, ctx,
text, text,
ButtonRequestType.ResetDevice, ButtonRequestType.ResetDevice,
cancel="Skip", cancel="Skip",
confirm="Back up", confirm="Back up",
major_confirm=True, 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 = Text("Warning", ui.ICON_WRONG, ui.RED, new_lines=False)
text.bold("Are you sure you want") text.bold("Are you sure you want")
text.br() 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): async def _confirm_share_words(ctx, share_index, share_words, group_index=None):
numbered = list(enumerate(share_words)) numbered = list(enumerate(share_words))
# check three words # divide list into thirds, rounding up, so that chunking by `third` always yields
third = len(numbered) // 3 # three parts (the last one might be shorter)
# if the num of words is not dividable by 3 let's add 1 third = (len(numbered) + 2) // 3
# to have more words at the beggining and to check all of them
if len(numbered) % 3:
third += 1
for part in utils.chunks(numbered, third): for part in utils.chunks(numbered, third):
if not await _confirm_word( 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( async def _confirm_word(
ctx, share_index, numbered_share_words, count, group_index=None 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 # shuffle the numbered seed half, slice off the choices we need
random.shuffle(numbered_share_words) random.shuffle(numbered_share_words)
numbered_choices = numbered_share_words[: MnemonicWordSelect.NUM_OF_CHOICES] numbered_choices = numbered_share_words[: MnemonicWordSelect.NUM_OF_CHOICES]
@ -115,22 +194,24 @@ async def _confirm_word(
async def _show_confirmation_success( 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 share_index is None: # it is a BIP39 backup
if slip39: subheader = ("You have finished", "verifying your", "recovery seed.")
if group_index is None: text = []
subheader = ("You have finished", "verifying your", "recovery shares.")
else: elif share_index == num_of_shares - 1:
subheader = ( if group_index is None:
"You have finished", subheader = ("You have finished", "verifying your", "recovery shares.")
"verifying your",
"recovery shares",
"for group %s." % (group_index + 1),
)
else: else:
subheader = ("You have finished", "verifying your", "recovery seed.") subheader = (
"You have finished",
"verifying your",
"recovery shares",
"for group %s." % (group_index + 1),
)
text = [] text = []
else: else:
if group_index is None: if group_index is None:
subheader = ( subheader = (
@ -198,123 +279,82 @@ async def bip39_show_and_confirm_mnemonic(ctx, mnemonic: str):
while True: while True:
# display paginated mnemonic on the screen # 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): 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 break # this share is confirmed, go to next one
else: else:
await _show_confirmation_failure(ctx, None) 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 # 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 = Checklist("Backup checklist", ui.ICON_RESET)
checklist.add("Set number of shares") if backup_type is BackupType.Slip39_Basic:
checklist.add("Set threshold") checklist.add("Set number of shares")
checklist.add(("Write down and check", "all recovery shares")) checklist.add("Set threshold")
checklist.select(0) checklist.add(("Write down and check", "all recovery shares"))
return await confirm( elif backup_type is BackupType.Slip39_Advanced:
ctx, checklist, ButtonRequestType.ResetDevice, cancel=None, confirm="Continue" 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( return await confirm(
ctx, checklist, ButtonRequestType.ResetDevice, cancel=None, confirm="Continue" ctx, checklist, ButtonRequestType.ResetDevice, cancel=None, confirm="Continue"
) )
async def slip39_show_checklist_show_shares(ctx, num_of_shares, threshold): async def slip39_prompt_threshold(ctx, num_of_shares, group_id=None):
checklist = Checklist("Backup checklist", ui.ICON_RESET) count = num_of_shares // 2 + 1
checklist.add("Set number of shares") # min value of share threshold is 2 unless the number of shares is 1
checklist.add("Set threshold") # number of shares 1 is possible in advnaced slip39
checklist.add(("Write down and check", "all recovery shares")) min_count = min(2, num_of_shares)
checklist.select(2) max_count = num_of_shares
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_group_show_checklist_set_group_threshold(ctx, num_of_shares): while True:
checklist = Checklist("Backup checklist", ui.ICON_RESET) shares = Slip39NumInput(
checklist.add("Set number of groups") Slip39NumInput.SET_THRESHOLD, count, min_count, max_count, group_id
checklist.add("Set group threshold") )
checklist.add(("Set size and threshold", "for each group")) confirmed = await confirm(
checklist.select(1) ctx,
return await confirm( shares,
ctx, checklist, ButtonRequestType.ResetDevice, cancel=None, confirm="Continue" 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): return count
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"
)
async def slip39_prompt_number_of_shares(ctx, group_id=None): 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 max_count = 16
while True: while True:
shares = ShamirNumInput( shares = Slip39NumInput(
ShamirNumInput.SET_SHARES, count, min_count, max_count, group_id Slip39NumInput.SET_SHARES, count, min_count, max_count, group_id
) )
confirmed = await confirm( confirmed = await confirm(
ctx, ctx,
@ -365,13 +405,33 @@ async def slip39_prompt_number_of_shares(ctx, group_id=None):
return count 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 count = 5
min_count = 2 min_count = 2
max_count = 16 max_count = 16
while True: 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( confirmed = await confirm(
ctx, ctx,
shares, shares,
@ -398,14 +458,14 @@ async def slip39_prompt_number_of_groups(ctx):
return count 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 count = num_of_groups // 2 + 1
min_count = 1 min_count = 1
max_count = num_of_groups max_count = num_of_groups
while True: while True:
shares = ShamirNumInput( shares = Slip39NumInput(
ShamirNumInput.SET_GROUP_THRESHOLD, count, min_count, max_count Slip39NumInput.SET_GROUP_THRESHOLD, count, min_count, max_count
) )
confirmed = await confirm( confirmed = await confirm(
ctx, ctx,
@ -431,72 +491,7 @@ async def slip39_prompt_group_threshold(ctx, num_of_groups):
return count return count
async def slip39_prompt_threshold(ctx, num_of_shares, group_id=None): async def slip39_advanced_show_and_confirm_shares(ctx, shares):
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):
# warn user about mnemonic safety # warn user about mnemonic safety
await show_backup_warning(ctx, slip39=True) 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(" ") share_words = share.split(" ")
while True: while True:
# display paginated share on the screen # display paginated share on the screen
await _slip39_show_share_words( await _show_share_words(ctx, share_words, share_index, group_index)
ctx, share_index, share_words, group_index
)
# make the user confirm words from the share # make the user confirm words from the share
if await _confirm_share_words( if await _confirm_share_words(
@ -515,9 +508,8 @@ async def slip39_group_show_and_confirm_shares(ctx, shares):
): ):
await _show_confirmation_success( await _show_confirmation_success(
ctx, ctx,
share_index, share_index=share_index,
num_of_shares=len(shares), num_of_shares=len(shares),
slip39=True,
group_index=group_index, group_index=group_index,
) )
break # this share is confirmed, go to next one 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) await _show_confirmation_failure(ctx, share_index)
async def _slip39_show_share_words(ctx, share_index, share_words, group_index=None): class Slip39NumInput(ui.Component):
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):
SET_SHARES = object() SET_SHARES = object()
SET_THRESHOLD = object() SET_THRESHOLD = object()
SET_GROUPS = object() SET_GROUPS = object()
@ -629,18 +540,18 @@ class ShamirNumInput(ui.Component):
count = self.input.count count = self.input.count
# render the headline # render the headline
if self.step is ShamirNumInput.SET_SHARES: if self.step is Slip39NumInput.SET_SHARES:
header = "Set num. of shares" header = "Set num. of shares"
elif self.step is ShamirNumInput.SET_THRESHOLD: elif self.step is Slip39NumInput.SET_THRESHOLD:
header = "Set threshold" header = "Set threshold"
elif self.step is ShamirNumInput.SET_GROUPS: elif self.step is Slip39NumInput.SET_GROUPS:
header = "Set num. of 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" header = "Set group threshold"
ui.header(header, ui.ICON_RESET, ui.TITLE_GREY, ui.BG, ui.ORANGE_ICON) ui.header(header, ui.ICON_RESET, ui.TITLE_GREY, ui.BG, ui.ORANGE_ICON)
# render the counter # render the counter
if self.step is ShamirNumInput.SET_SHARES: if self.step is Slip39NumInput.SET_SHARES:
if self.group_id is None: if self.group_id is None:
first_line_text = "%s people or locations" % count first_line_text = "%s people or locations" % count
second_line_text = "will each hold one share." 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 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) 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: if self.group_id is None:
first_line_text = "For recovery you need" first_line_text = "For recovery you need"
second_line_text = "any %s of the shares." % count second_line_text = "any %s of the shares." % count
@ -662,14 +573,14 @@ class ShamirNumInput(ui.Component):
ui.display.text( ui.display.text(
12, 156, second_line_text, ui.NORMAL, ui.FG, ui.BG, ui.WIDTH - 12 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( ui.display.text(
12, 130, "A group is made up of", ui.NORMAL, ui.FG, ui.BG 12, 130, "A group is made up of", ui.NORMAL, ui.FG, ui.BG
) )
ui.display.text( ui.display.text(
12, 156, "recovery shares.", ui.NORMAL, ui.FG, ui.BG, ui.WIDTH - 12 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( ui.display.text(
12, 130, "The required number of", ui.NORMAL, ui.FG, ui.BG 12, 130, "The required number of", ui.NORMAL, ui.FG, ui.BG
) )
@ -722,3 +633,27 @@ class MnemonicWordSelect(ui.Layout):
raise ui.Result(word) raise ui.Result(word)
return fn 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. # 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 micropython import const
from trezor.crypto import hashlib, hmac, pbkdf2, random from trezor.crypto import hashlib, hmac, pbkdf2, random
@ -28,42 +40,31 @@ if False:
from typing import Dict, Iterable, List, Optional, Set, Tuple from typing import Dict, Iterable, List, Optional, Set, Tuple
Indices = Tuple[int, ...] 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.""" ## Simple helpers
"""
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))
_RADIX_BITS = const(10) _RADIX_BITS = const(10)
"""The length of the radix in bits.""" """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 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 return (n + _RADIX_BITS - 1) // _RADIX_BITS
MAX_SHARE_COUNT = const(16) def _xor(a: bytes, b: bytes) -> bytes:
"""The maximum number of shares that can be created.""" 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) _ID_LENGTH_BITS = const(15)
"""The length of the random identifier in bits.""" """The length of the random identifier in bits."""
@ -71,7 +72,7 @@ _ID_LENGTH_BITS = const(15)
_ITERATION_EXP_LENGTH_BITS = const(5) _ITERATION_EXP_LENGTH_BITS = const(5)
"""The length of the iteration exponent in bits.""" """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.""" """The length of the random identifier and iteration exponent in words."""
_CHECKSUM_LENGTH_WORDS = const(3) _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) _MIN_STRENGTH_BITS = const(128)
"""The minimum allowed entropy of the master secret.""" """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.""" """The minimum allowed length of the mnemonic in words."""
_BASE_ITERATION_COUNT = const(10000) _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.""" """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: def _rs1024_polymod(values: Indices) -> int:
GEN = ( GEN = (
0xE0E040, 0xE0E040,
@ -127,19 +411,18 @@ def _rs1024_polymod(values: Indices) -> int:
return chk return chk
def rs1024_create_checksum(data: Indices) -> Indices: def _rs1024_verify_checksum(data: Indices) -> bool:
values = tuple(_CUSTOMIZATION_STRING) + data + _CHECKSUM_LENGTH_WORDS * (0,) """
polymod = _rs1024_polymod(values) ^ 1 Verifies a checksum of the given mnemonic, which was already parsed into Indices.
return tuple( """
(polymod >> 10 * i) & 1023 for i in reversed(range(_CHECKSUM_LENGTH_WORDS))
)
def rs1024_verify_checksum(data: Indices) -> bool:
return _rs1024_polymod(tuple(_CUSTOMIZATION_STRING) + data) == 1 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 = ( GEN = (
0x91F9F87, 0x91F9F87,
0x122F1F07, 0x122F1F07,
@ -166,30 +449,9 @@ def rs1024_error_index(data: Indices) -> Optional[int]:
return None return None
def xor(a: bytes, b: bytes) -> bytes: """
return bytes(x ^ y for x, y in zip(a, b)) ## Internal functions
"""
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())
def _round_function(i: int, passphrase: bytes, e: int, salt: bytes, r: bytes) -> bytes: 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: def _get_salt(identifier: int) -> bytes:
return _CUSTOMIZATION_STRING + identifier.to_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: def _create_digest(random_data: bytes, shared_secret: bytes) -> bytes:
return hmac.new(random_data, shared_secret, hashlib.sha256).digest()[ return hmac.new(random_data, shared_secret, hashlib.sha256).digest()[
:_DIGEST_LENGTH_BYTES :_DIGEST_LENGTH_BYTES
@ -304,33 +549,23 @@ def _group_prefix(
) )
def encode_mnemonic( def _encode_mnemonic(
identifier: int, identifier: int,
iteration_exponent: int, iteration_exponent: int,
group_index: int, group_index: int, # The x coordinate of the group share.
group_threshold: int, group_threshold: int, # The number of group shares needed to reconstruct the encrypted master secret.
group_count: int, group_count: int, # The total number of groups in existence.
member_index: int, member_index: int, # The x coordinate of the member share in the given group.
member_threshold: int, member_threshold: int, # The number of member shares needed to reconstruct the group share.
value: bytes, value: bytes, # The share value representing the y coordinates of the share.
) -> str: ) -> str:
""" """
Converts share data to a share mnemonic. Takes the metadata and the value to be encoded and converts it into a mnemonic words.
:param int identifier: The random identifier. Also appends a checksum.
: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.
""" """
# Convert the share value from bytes to wordlist indices. # 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") value_int = int.from_bytes(value, "big")
share_data = ( share_data = (
@ -344,66 +579,9 @@ def encode_mnemonic(
) )
+ tuple(_int_to_indices(value_int, value_word_count, _RADIX_BITS)) + tuple(_int_to_indices(value_int, value_word_count, _RADIX_BITS))
) )
checksum = rs1024_create_checksum(share_data) checksum = _rs1024_create_checksum(share_data)
return mnemonic_from_indices(share_data + checksum)
def decode_mnemonic(mnemonic: str) -> Tuple[int, int, int, int, int, int, int, bytes]: return _mnemonic_from_indices(share_data + checksum)
"""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]]]]
def _decode_mnemonics( def _decode_mnemonics(
@ -414,22 +592,20 @@ def _decode_mnemonics(
group_thresholds = set() group_thresholds = set()
group_counts = set() group_counts = set()
# { group_index : [member_threshold, set_of_member_shares] } # { group_index : [threshold, set_of_member_shares] }
groups = {} # type: MnemonicGroups groups = {} # type: MnemonicGroups
for mnemonic in mnemonics: for mnemonic in mnemonics:
identifier, iteration_exponent, group_index, group_threshold, group_count, member_index, member_threshold, share_value = decode_mnemonic( share = decode_mnemonic(mnemonic)
mnemonic identifiers.add(share.identifier)
) iteration_exponents.add(share.iteration_exponent)
identifiers.add(identifier) group_thresholds.add(share.group_threshold)
iteration_exponents.add(iteration_exponent) group_counts.add(share.group_count)
group_thresholds.add(group_threshold) group = groups.setdefault(share.group_index, (share.threshold, set()))
group_counts.add(group_count) if group[0] != share.threshold:
group = groups.setdefault(group_index, (member_threshold, set()))
if group[0] != member_threshold:
raise MnemonicError( raise MnemonicError(
"Invalid set of mnemonics. All mnemonics in a group must have the same member threshold." "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: if len(identifiers) != 1 or len(iteration_exponents) != 1:
raise MnemonicError( raise MnemonicError(
@ -461,143 +637,3 @@ def _decode_mnemonics(
group_counts.pop(), group_counts.pop(),
groups, 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): class MnemonicError(RuntimeError):
pass pass
class IdentifierMismatchError(MnemonicError):
pass
class ShareAlreadyAddedError(MnemonicError):
pass
class GroupThresholdReachedError(MnemonicError):
pass

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

@ -7,9 +7,11 @@ if __debug__:
from typing import Dict, List, Optional from typing import Dict, List, Optional
from typing_extensions import Literal # noqa: F401 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] 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: except ImportError:
Dict, List, Optional = None, None, None # type: ignore Dict, List, Optional = None, None, None # type: ignore
EnumTypeCapability = None # type: ignore EnumTypeCapability = None # type: ignore
EnumTypeBackupType = None # type: ignore
class Features(p.MessageType): class Features(p.MessageType):
@ -46,6 +48,7 @@ class Features(p.MessageType):
no_backup: bool = None, no_backup: bool = None,
recovery_mode: bool = None, recovery_mode: bool = None,
capabilities: List[EnumTypeCapability] = None, capabilities: List[EnumTypeCapability] = None,
backup_type: EnumTypeBackupType = None,
) -> None: ) -> None:
self.vendor = vendor self.vendor = vendor
self.major_version = major_version self.major_version = major_version
@ -76,6 +79,7 @@ class Features(p.MessageType):
self.no_backup = no_backup self.no_backup = no_backup
self.recovery_mode = recovery_mode self.recovery_mode = recovery_mode
self.capabilities = capabilities if capabilities is not None else [] self.capabilities = capabilities if capabilities is not None else []
self.backup_type = backup_type
@classmethod @classmethod
def get_fields(cls) -> Dict: def get_fields(cls) -> Dict:
@ -109,4 +113,5 @@ class Features(p.MessageType):
28: ('no_backup', p.BoolType, 0), 28: ('no_backup', p.BoolType, 0),
29: ('recovery_mode', 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), 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: try:
from typing import Dict, List, Optional from typing import Dict, List, Optional
from typing_extensions import Literal # noqa: F401 from typing_extensions import Literal # noqa: F401
EnumTypeResetDeviceBackupType = Literal[0, 1, 2] EnumTypeBackupType = Literal[0, 1, 2]
except ImportError: except ImportError:
Dict, List, Optional = None, None, None # type: ignore Dict, List, Optional = None, None, None # type: ignore
EnumTypeResetDeviceBackupType = None # type: ignore EnumTypeBackupType = None # type: ignore
class ResetDevice(p.MessageType): class ResetDevice(p.MessageType):
@ -26,7 +26,7 @@ class ResetDevice(p.MessageType):
u2f_counter: int = None, u2f_counter: int = None,
skip_backup: bool = None, skip_backup: bool = None,
no_backup: bool = None, no_backup: bool = None,
backup_type: EnumTypeResetDeviceBackupType = None, backup_type: EnumTypeBackupType = None,
) -> None: ) -> None:
self.display_random = display_random self.display_random = display_random
self.strength = strength self.strength = strength
@ -51,5 +51,5 @@ class ResetDevice(p.MessageType):
7: ('u2f_counter', p.UVarintType, 0), 7: ('u2f_counter', p.UVarintType, 0),
8: ('skip_backup', p.BoolType, 0), 8: ('skip_backup', p.BoolType, 0),
9: ('no_backup', p.BoolType, 0), 9: ('no_backup', p.BoolType, 0),
10: ('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 micropython import const
from trezor import loop, res, ui 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 from trezor.ui.loader import Loader, LoaderDefault
if __debug__: if __debug__:
@ -14,6 +14,7 @@ if False:
CONFIRMED = object() CONFIRMED = object()
CANCELLED = object() CANCELLED = object()
INFO = object()
class Confirm(ui.Layout): class Confirm(ui.Layout):
@ -153,6 +154,54 @@ class ConfirmPageable(Confirm):
ui.display.icon(205, 68, icon, c, ui.BG) 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): class HoldToConfirm(ui.Layout):
DEFAULT_CONFIRM = "Hold To Confirm" DEFAULT_CONFIRM = "Hold To Confirm"
DEFAULT_CONFIRM_STYLE = ButtonConfirm DEFAULT_CONFIRM_STYLE = ButtonConfirm

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

@ -8,7 +8,7 @@ from trezor.crypto.hashlib import sha256
class TestCredential(unittest.TestCase): class TestCredential(unittest.TestCase):
def test_fido2_credential_decode(self): def test_fido2_credential_decode(self):
mnemonic_secret = b"all all all all all all all all all all all all" 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 storage.is_initialized = lambda: True
cred_id = ( cred_id = (

@ -25,21 +25,22 @@ class TestCryptoSlip39(unittest.TestCase):
EMS = b"ABCDEFGHIJKLMNOP" EMS = b"ABCDEFGHIJKLMNOP"
def test_basic_sharing_random(self): def test_basic_sharing_random(self):
ms = random.bytes(32) ems = random.bytes(32)
identifier = slip39.generate_random_identifier() 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] mnemonics = mnemonics[0]
self.assertEqual(slip39.combine_mnemonics(mnemonics[:3]), slip39.combine_mnemonics(mnemonics[2:])) self.assertEqual(slip39.combine_mnemonics(mnemonics[:3]), slip39.combine_mnemonics(mnemonics[2:]))
def test_basic_sharing_fixed(self): def test_basic_sharing_fixed(self):
generated_identifier = slip39.generate_random_identifier() 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] 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(ems, self.EMS)
self.assertEqual(generated_identifier, identifier) self.assertEqual(generated_identifier, identifier)
self.assertEqual(slip39.combine_mnemonics(mnemonics[1:4])[2], ems) self.assertEqual(slip39.combine_mnemonics(mnemonics[1:4])[2], ems)
self.assertEqual(group_count, 1)
with self.assertRaises(slip39.MnemonicError): with self.assertRaises(slip39.MnemonicError):
slip39.combine_mnemonics(mnemonics[1:3]) slip39.combine_mnemonics(mnemonics[1:3])
@ -48,13 +49,15 @@ class TestCryptoSlip39(unittest.TestCase):
identifier = slip39.generate_random_identifier() identifier = slip39.generate_random_identifier()
mnemonics = slip39.generate_mnemonics_from_data(self.EMS, identifier, 1, [(3, 5)], 1) mnemonics = slip39.generate_mnemonics_from_data(self.EMS, identifier, 1, [(3, 5)], 1)
mnemonics = mnemonics[0] 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(ems, self.EMS)
self.assertEqual(group_count, 1)
identifier = slip39.generate_random_identifier() identifier = slip39.generate_random_identifier()
mnemonics = slip39.generate_mnemonics_from_data(self.EMS, identifier, 1, [(3, 5)], 2) mnemonics = slip39.generate_mnemonics_from_data(self.EMS, identifier, 1, [(3, 5)], 2)
mnemonics = mnemonics[0] 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) self.assertEqual(ems, self.EMS)
@ -64,7 +67,7 @@ class TestCryptoSlip39(unittest.TestCase):
member_thresholds = (3, 2, 2, 1) member_thresholds = (3, 2, 2, 1)
identifier = slip39.generate_random_identifier() identifier = slip39.generate_random_identifier()
mnemonics = slip39.generate_mnemonics_from_data( 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. # 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]): for group2_subset in combinations(groups[1][0], groups[1][1]):
mnemonic_subset = list(group1_subset + group2_subset) mnemonic_subset = list(group1_subset + group2_subset)
random.shuffle(mnemonic_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) self.assertEqual(ems, self.EMS)
# Minimal sets of mnemonics. # 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(ems, self.EMS)
self.assertEqual(slip39.combine_mnemonics([mnemonics[2][3], mnemonics[3][0], mnemonics[2][4]])[2], 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. # One complete group and one incomplete group out of two groups required.
with self.assertRaises(slip39.MnemonicError): with self.assertRaises(slip39.MnemonicError):
@ -97,7 +102,7 @@ class TestCryptoSlip39(unittest.TestCase):
member_thresholds = (3, 2, 2, 1) member_thresholds = (3, 2, 2, 1)
identifier = slip39.generate_random_identifier() identifier = slip39.generate_random_identifier()
mnemonics = slip39.generate_mnemonics_from_data( 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. # Test all valid combinations of mnemonics.
@ -105,7 +110,8 @@ class TestCryptoSlip39(unittest.TestCase):
for group_subset in combinations(group, threshold): for group_subset in combinations(group, threshold):
mnemonic_subset = list(group_subset) mnemonic_subset = list(group_subset)
random.shuffle(mnemonic_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) self.assertEqual(ems, self.EMS)
@ -113,7 +119,7 @@ class TestCryptoSlip39(unittest.TestCase):
for group_threshold in (1, 2, 5): for group_threshold in (1, 2, 5):
identifier = slip39.generate_random_identifier() identifier = slip39.generate_random_identifier()
mnemonics = slip39.generate_mnemonics_from_data( 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(mnemonics), 5)
self.assertEqual(len(sum(mnemonics, [])), 19) self.assertEqual(len(sum(mnemonics, [])), 19)
@ -121,39 +127,32 @@ class TestCryptoSlip39(unittest.TestCase):
def test_invalid_sharing(self): def test_invalid_sharing(self):
identifier = slip39.generate_random_identifier() 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. # Group threshold exceeds number of groups.
with self.assertRaises(ValueError): 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. # Invalid group threshold.
with self.assertRaises(ValueError): 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. # Member threshold exceeds number of members.
with self.assertRaises(ValueError): 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. # Invalid member threshold.
with self.assertRaises(ValueError): 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. # Group with multiple members and threshold 1.
with self.assertRaises(ValueError): 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): def test_vectors(self):
for mnemonics, secret in vectors: for mnemonics, secret in vectors:
if secret: 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)) self.assertEqual(slip39.decrypt(identifier, exponent, ems, b"TREZOR"), unhexlify(secret))
else: else:
with self.assertRaises(slip39.MnemonicError): 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", "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: for mnemonic in mnemonics:
data = tuple(slip39.mnemonic_to_indices(mnemonic)) data = tuple(slip39._mnemonic_to_indices(mnemonic))
self.assertEqual(slip39.rs1024_error_index(data), None) self.assertEqual(slip39._rs1024_error_index(data), None)
for i in range(len(data)): for i in range(len(data)):
for _ in range(50): for _ in range(50):
error_data = error_data = data[:i] + (data[i] ^ (random.uniform(1023) + 1), ) + data[i + 1:] 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__': if __name__ == '__main__':

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

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

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

@ -7,9 +7,11 @@ if __debug__:
from typing import Dict, List, Optional from typing import Dict, List, Optional
from typing_extensions import Literal # noqa: F401 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] 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: except ImportError:
Dict, List, Optional = None, None, None # type: ignore Dict, List, Optional = None, None, None # type: ignore
EnumTypeCapability = None # type: ignore EnumTypeCapability = None # type: ignore
EnumTypeBackupType = None # type: ignore
class Features(p.MessageType): class Features(p.MessageType):
@ -46,6 +48,7 @@ class Features(p.MessageType):
no_backup: bool = None, no_backup: bool = None,
recovery_mode: bool = None, recovery_mode: bool = None,
capabilities: List[EnumTypeCapability] = None, capabilities: List[EnumTypeCapability] = None,
backup_type: EnumTypeBackupType = None,
) -> None: ) -> None:
self.vendor = vendor self.vendor = vendor
self.major_version = major_version self.major_version = major_version
@ -76,6 +79,7 @@ class Features(p.MessageType):
self.no_backup = no_backup self.no_backup = no_backup
self.recovery_mode = recovery_mode self.recovery_mode = recovery_mode
self.capabilities = capabilities if capabilities is not None else [] self.capabilities = capabilities if capabilities is not None else []
self.backup_type = backup_type
@classmethod @classmethod
def get_fields(cls) -> Dict: def get_fields(cls) -> Dict:
@ -109,4 +113,5 @@ class Features(p.MessageType):
28: ('no_backup', p.BoolType, 0), 28: ('no_backup', p.BoolType, 0),
29: ('recovery_mode', 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), 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: try:
from typing import Dict, List, Optional from typing import Dict, List, Optional
from typing_extensions import Literal # noqa: F401 from typing_extensions import Literal # noqa: F401
EnumTypeResetDeviceBackupType = Literal[0, 1, 2] EnumTypeBackupType = Literal[0, 1, 2]
except ImportError: except ImportError:
Dict, List, Optional = None, None, None # type: ignore Dict, List, Optional = None, None, None # type: ignore
EnumTypeResetDeviceBackupType = None # type: ignore EnumTypeBackupType = None # type: ignore
class ResetDevice(p.MessageType): class ResetDevice(p.MessageType):
@ -26,7 +26,7 @@ class ResetDevice(p.MessageType):
u2f_counter: int = None, u2f_counter: int = None,
skip_backup: bool = None, skip_backup: bool = None,
no_backup: bool = None, no_backup: bool = None,
backup_type: EnumTypeResetDeviceBackupType = None, backup_type: EnumTypeBackupType = None,
) -> None: ) -> None:
self.display_random = display_random self.display_random = display_random
self.strength = strength self.strength = strength
@ -51,5 +51,5 @@ class ResetDevice(p.MessageType):
7: ('u2f_counter', p.UVarintType, 0), 7: ('u2f_counter', p.UVarintType, 0),
8: ('skip_backup', p.BoolType, 0), 8: ('skip_backup', p.BoolType, 0),
9: ('no_backup', p.BoolType, 0), 9: ('no_backup', p.BoolType, 0),
10: ('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 .WipeDevice import WipeDevice
from .WordAck import WordAck from .WordAck import WordAck
from .WordRequest import WordRequest from .WordRequest import WordRequest
from . import BackupType
from . import BinanceOrderSide from . import BinanceOrderSide
from . import BinanceOrderType from . import BinanceOrderType
from . import BinanceTimeInForce from . import BinanceTimeInForce
@ -274,7 +275,6 @@ from . import PassphraseSourceType
from . import PinMatrixRequestType from . import PinMatrixRequestType
from . import RecoveryDeviceType from . import RecoveryDeviceType
from . import RequestType from . import RequestType
from . import ResetDeviceBackupType
from . import SdProtectOperationType from . import SdProtectOperationType
from . import TezosBallotType from . import TezosBallotType
from . import TezosContractType from . import TezosContractType

@ -19,20 +19,24 @@ from trezorlib.messages import ButtonRequestType as B
# fmt: off # fmt: off
# 1 2 3 4 5 6 7 8 9 10 11 12 # 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" MNEMONIC12 = "alcohol woman abuse must during monitor noble actual mixed trade anger aisle"
# fmt: on MNEMONIC_SLIP39_BASIC_20_3of6 = [
MNEMONIC_SHAMIR_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 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 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", "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 = [ # Shamir shares (128 bits, 2 groups from 1 of 1, 1 of 1, 3 of 5, 2 of 6)
"gesture negative ceramic leaf device fantasy style ceramic safari keyboard thumb total smug cage plunge aunt favorite lizard intend peanut", MNEMONIC_SLIP39_ADVANCED_20 = [
"gesture negative acrobat leaf craft sidewalk adorn spider submit bumpy alcohol cards salon making prune decorate smoking image corner method", "eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice",
"gesture negative acrobat lily bishop voting humidity rhyme parcel crunch elephant victim dish mailman triumph agree episode wealthy mayor beam", "eraser senior ceramic snake clay various huge numb argue hesitate auction category timber browser greatest hanger petition script leaf pickup",
"gesture negative beard leaf deadline stadium vegan employer armed marathon alien lunar broken edge justice military endorse diet sweater either", "eraser senior ceramic shaft dynamic become junior wrist silver peasant force math alto coal amazing segment yelp velvet image paces",
"gesture negative beard lily desert belong speak realize explain bolt diet believe response counter medal luck wits glance remove ending", "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): def generate_entropy(strength, internal_entropy, external_entropy):
@ -91,13 +95,6 @@ def recovery_enter_shares(debug, shares, groups=False):
debug.press_yes() debug.press_yes()
# Enter shares # Enter shares
for index, share in enumerate(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 code = yield
assert code == B.MnemonicInput assert code == B.MnemonicInput
# Enter mnemonic words # Enter mnemonic words

@ -19,7 +19,7 @@ import pytest
from trezorlib.cardano import get_address from trezorlib.cardano import get_address
from trezorlib.tools import parse_path 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.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): def test_cardano_get_address(client, path, expected_address):
# enter passphrase # enter passphrase
assert client.features.passphrase_protection is True assert client.features.passphrase_protection is True

@ -19,13 +19,13 @@ import pytest
from trezorlib.cardano import get_public_key from trezorlib.cardano import get_public_key
from trezorlib.tools import parse_path 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.altcoin
@pytest.mark.cardano @pytest.mark.cardano
@pytest.mark.skip_t1 # T1 support is not planned @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( @pytest.mark.parametrize(
"path,public_key,chain_code", "path,public_key,chain_code",
[ [

@ -18,7 +18,7 @@ import pytest
from trezorlib import cardano, messages 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} PROTOCOL_MAGICS = {"mainnet": 764824073, "testnet": 1097911063}
@ -109,7 +109,7 @@ VALID_VECTORS = [
@pytest.mark.altcoin @pytest.mark.altcoin
@pytest.mark.cardano @pytest.mark.cardano
@pytest.mark.skip_t1 # T1 support is not planned @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( @pytest.mark.parametrize(
"protocol_magic,inputs,outputs,transactions,tx_hash,tx_body", VALID_VECTORS "protocol_magic,inputs,outputs,transactions,tx_hash,tx_body", VALID_VECTORS
) )

@ -17,9 +17,14 @@
import pytest import pytest
from trezorlib import btc, debuglink, device from trezorlib import btc, debuglink, device
from trezorlib.messages import BackupType
from trezorlib.messages.PassphraseSourceType import HOST as PASSPHRASE_ON_HOST 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) @pytest.mark.setup_client(uninitialized=True)
@ -62,6 +67,28 @@ class TestDeviceLoad:
address = btc.get_address(client, "Bitcoin", []) address = btc.get_address(client, "Bitcoin", [])
assert address == "15fiTDFwZd2kauHYYseifGi9daH2wniDHH" 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): 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_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" 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.pin_protection is False
assert client.features.passphrase_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): def test_already_initialized(self, client):
with pytest.raises(RuntimeError): 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 import device, messages
from trezorlib.exceptions import TrezorFailure 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 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 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 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 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", "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( @pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_ADVANCED_20, passphrase=False)
mnemonic=MNEMONIC_SHAMIR_20_2of3_2of3_GROUPS[1:5], passphrase=False
)
def test_2of3_dryrun(client): def test_2of3_dryrun(client):
debug = client.debug debug = client.debug
@ -42,7 +45,7 @@ def test_2of3_dryrun(client):
debug.press_yes() debug.press_yes()
# run recovery flow # run recovery flow
yield from recovery_enter_shares( 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: with client:
@ -62,9 +65,7 @@ def test_2of3_dryrun(client):
) )
@pytest.mark.setup_client( @pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_ADVANCED_20, passphrase=True)
mnemonic=MNEMONIC_SHAMIR_20_2of3_2of3_GROUPS[1:5], passphrase=True
)
def test_2of3_invalid_seed_dryrun(client): def test_2of3_invalid_seed_dryrun(client):
debug = client.debug debug = client.debug
@ -73,7 +74,7 @@ def test_2of3_invalid_seed_dryrun(client):
debug.press_yes() debug.press_yes()
# run recovery flow # run recovery flow
yield from recovery_enter_shares( 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 # test fails because of different seed on device

@ -18,20 +18,24 @@ import pytest
from trezorlib import device, exceptions, messages 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 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 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", "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 = ( VECTORS = (
(MNEMONIC_SHAMIR_20_3of6, "491b795b80fc21ccdf466c0fbc98c8fc"), (MNEMONIC_SLIP39_BASIC_20_3of6, "491b795b80fc21ccdf466c0fbc98c8fc"),
( (
MNEMONIC_SHAMIR_33_2of5, MNEMONIC_SLIP39_BASIC_33_2of5,
"b770e0da1363247652de97a39bdbf2463be087848d709ecbf28e84508e31202a", "b770e0da1363247652de97a39bdbf2463be087848d709ecbf28e84508e31202a",
), ),
) )
@ -56,6 +60,7 @@ def test_secret(client, shares, secret):
assert ret == messages.Success(message="Device recovered") assert ret == messages.Success(message="Device recovered")
assert client.features.pin_protection is False assert client.features.pin_protection is False
assert client.features.passphrase_protection is False assert client.features.passphrase_protection is False
assert client.features.backup_type is messages.BackupType.Slip39_Basic
# Check mnemonic # Check mnemonic
assert debug.read_mnemonic_secret().hex() == secret assert debug.read_mnemonic_secret().hex() == secret
@ -73,7 +78,7 @@ def test_recover_with_pin_passphrase(client):
yield # Enter PIN again yield # Enter PIN again
debug.input("654") debug.input("654")
# Proceed with recovery # 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: with client:
client.set_input_flow(input_flow) 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 ret == messages.Success(message="Device recovered")
assert client.features.pin_protection is True assert client.features.pin_protection is True
assert client.features.passphrase_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) @pytest.mark.setup_client(uninitialized=True)
@ -118,7 +124,7 @@ def test_noabort(client):
debug.press_no() debug.press_no()
yield # Homescreen - go back to process yield # Homescreen - go back to process
debug.press_no() 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: with client:
client.set_input_flow(input_flow) client.set_input_flow(input_flow)
@ -131,7 +137,7 @@ def test_noabort(client):
@pytest.mark.parametrize("nth_word", range(3)) @pytest.mark.parametrize("nth_word", range(3))
def test_wrong_nth_word(client, nth_word): def test_wrong_nth_word(client, nth_word):
debug = client.debug debug = client.debug
share = MNEMONIC_SHAMIR_20_3of6[0].split(" ") share = MNEMONIC_SLIP39_BASIC_20_3of6[0].split(" ")
def input_flow(): def input_flow():
yield # Confirm Recovery yield # Confirm Recovery
@ -170,9 +176,9 @@ def test_wrong_nth_word(client, nth_word):
@pytest.mark.setup_client(uninitialized=True) @pytest.mark.setup_client(uninitialized=True)
def test_same_share(client): def test_same_share(client):
debug = client.debug 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 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(): def input_flow():
yield # Confirm Recovery yield # Confirm Recovery
@ -202,3 +208,29 @@ def test_same_share(client):
client.set_input_flow(input_flow) client.set_input_flow(input_flow)
with pytest.raises(exceptions.Cancelled): with pytest.raises(exceptions.Cancelled):
device.recover(client, pin_protection=False, label="label") 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", label="label",
language="english", language="english",
dry_run=True, dry_run=True,
type=messages.ResetDeviceBackupType.Slip39_Single_Group,
) )
# Dry run was successful # Dry run was successful
@ -85,5 +84,4 @@ def test_2of3_invalid_seed_dryrun(client):
label="label", label="label",
language="english", language="english",
dry_run=True, 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.needs_backup is False
assert resp.pin_protection is False assert resp.pin_protection is False
assert resp.passphrase_protection is False assert resp.passphrase_protection is False
assert resp.backup_type is proto.BackupType.Bip39
@pytest.mark.setup_client(uninitialized=True) @pytest.mark.setup_client(uninitialized=True)
def test_reset_device_pin(self, client): def test_reset_device_pin(self, client):

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

@ -22,7 +22,7 @@ import shamir_mnemonic as shamir
from shamir_mnemonic import MnemonicError from shamir_mnemonic import MnemonicError
from trezorlib import device, messages as proto 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 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: class TestMsgResetDeviceT2:
# TODO: test with different options # TODO: test with different options
@pytest.mark.setup_client(uninitialized=True) @pytest.mark.setup_client(uninitialized=True)
def test_reset_device_shamir(self, client): def test_reset_device_slip39_basic(self, client):
strength = 128 strength = 128
member_threshold = 3 member_threshold = 3
all_mnemonics = [] all_mnemonics = []
@ -53,7 +53,7 @@ class TestMsgResetDeviceT2:
for h in range(5): for h in range(5):
# mnemonic phrases # mnemonic phrases
btn_code = yield btn_code = yield
assert btn_code == B.Other assert btn_code == B.ResetDevice
mnemonic = read_and_confirm_mnemonic(client.debug, words=20) mnemonic = read_and_confirm_mnemonic(client.debug, words=20)
all_mnemonics.append(mnemonic) 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.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.Success),
proto.ButtonRequest(code=B.Other), proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success), 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.ButtonRequest(code=B.Other), proto.ButtonRequest(code=B.ResetDevice),
proto.ButtonRequest(code=B.Success), 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.ButtonRequest(code=B.Success), proto.ButtonRequest(code=B.Success),
proto.Success(), proto.Success(),
@ -106,7 +106,7 @@ class TestMsgResetDeviceT2:
pin_protection=False, pin_protection=False,
label="test", label="test",
language="english", language="english",
backup_type=ResetDeviceBackupType.Slip39_Single_Group, backup_type=BackupType.Slip39_Basic,
) )
# generate secret locally # generate secret locally
@ -121,6 +121,7 @@ class TestMsgResetDeviceT2:
assert client.features.needs_backup is False assert client.features.needs_backup is False
assert client.features.pin_protection is False assert client.features.pin_protection is False
assert client.features.passphrase_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): 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 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 @pytest.mark.skip_t1
def test_3of6_passphrase(client): def test_3of6_passphrase(client):
""" """

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

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