mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-01-13 17:00:59 +00:00
core: refactor slip39
This commit is contained in:
parent
5628d1254d
commit
cefb1cf4fd
@ -7,6 +7,15 @@ option java_outer_classname = "TrezorMessageManagement";
|
||||
|
||||
import "messages-common.proto";
|
||||
|
||||
/**
|
||||
* Type of the mnemonic backup given/received by the device during reset/recovery.
|
||||
*/
|
||||
enum BackupType {
|
||||
Bip39 = 0; // also called "Single Backup", see BIP-0039
|
||||
Slip39_Basic = 1; // also called "Shamir Backup", see SLIP-0039
|
||||
Slip39_Advanced = 2; // also called "Super Shamir" or "Shamir with Groups", see SLIP-0039#two-level-scheme
|
||||
}
|
||||
|
||||
/**
|
||||
* Request: Reset device to default state and ask for device details
|
||||
* @start
|
||||
@ -58,13 +67,13 @@ message Features {
|
||||
optional bool unfinished_backup = 27; // report unfinished backup (equals to Storage.unfinished_backup)
|
||||
optional bool no_backup = 28; // report no backup (equals to Storage.no_backup)
|
||||
optional bool recovery_mode = 29; // is recovery mode in progress
|
||||
repeated Capability capabilities = 30; // list of supported capabilities
|
||||
repeated Capability capabilities = 30; // list of supported capabilities
|
||||
enum Capability {
|
||||
Capability_Bitcoin = 1;
|
||||
Capability_Bitcoin_like = 2; // Altcoins based on the Bitcoin source code
|
||||
Capability_Bitcoin_like = 2; // Altcoins based on the Bitcoin source code
|
||||
Capability_Binance = 3;
|
||||
Capability_Cardano = 4;
|
||||
Capability_Crypto = 5; // generic crypto operations for GPG, SSH, etc.
|
||||
Capability_Crypto = 5; // generic crypto operations for GPG, SSH, etc.
|
||||
Capability_EOS = 6;
|
||||
Capability_Ethereum = 7;
|
||||
Capability_Lisk = 8;
|
||||
@ -77,6 +86,7 @@ message Features {
|
||||
Capability_Shamir = 15;
|
||||
Capability_ShamirGroups = 16;
|
||||
}
|
||||
optional BackupType backup_type = 31; // type of device backup (BIP-39 / SLIP-39 basic / SLIP-39 advanced)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -229,14 +239,8 @@ message ResetDevice {
|
||||
optional uint32 u2f_counter = 7; // U2F counter
|
||||
optional bool skip_backup = 8; // postpone seed backup to BackupDevice workflow
|
||||
optional bool no_backup = 9; // indicate that no backup is going to be made
|
||||
|
||||
// type of the mnemonic backup (BIP-39 vs SLIP-39 single group vs SLIP-39 multiple groups)
|
||||
optional ResetDeviceBackupType backup_type = 10 [default=ResetDeviceBackupType_Bip39];
|
||||
enum ResetDeviceBackupType {
|
||||
ResetDeviceBackupType_Bip39 = 0; // The traditional Single Backup - BIP39
|
||||
ResetDeviceBackupType_Slip39_Single_Group = 1; // Shamir Backup with only single group (one-level Shamir)
|
||||
ResetDeviceBackupType_Slip39_Multiple_Groups = 2; // Shamir Backup with multiple groups (two-level Shamir)
|
||||
}
|
||||
// type of the mnemonic backup
|
||||
optional BackupType backup_type = 10 [default=Bip39];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,6 @@
|
||||
Version 2.x.x [not yet released]
|
||||
* nothing yet
|
||||
* Refactor Shamir related codebase
|
||||
* Introduce BackupType in Features
|
||||
|
||||
Version 2.1.5 [Sep 2019]
|
||||
* Introduce Bitcoin-only firmware
|
||||
|
@ -41,20 +41,17 @@ async def get_keychain(ctx: wire.Context) -> Keychain:
|
||||
if not storage.is_initialized():
|
||||
raise wire.ProcessError("Device is not initialized")
|
||||
|
||||
if (
|
||||
mnemonic.get_type() == mnemonic.TYPE_SLIP39
|
||||
or mnemonic.get_type() == mnemonic.TYPE_SLIP39_GROUP
|
||||
):
|
||||
if mnemonic.is_bip39():
|
||||
# derive the root node from mnemonic and passphrase
|
||||
passphrase = await _get_passphrase(ctx)
|
||||
root = bip32.from_mnemonic_cardano(mnemonic.get_secret().decode(), passphrase)
|
||||
else:
|
||||
seed = cache.get_seed()
|
||||
if seed is None:
|
||||
passphrase = await _get_passphrase(ctx)
|
||||
seed = mnemonic.get_seed(passphrase)
|
||||
cache.set_seed(seed)
|
||||
root = bip32.from_seed(seed, "ed25519 cardano seed")
|
||||
else:
|
||||
# derive the root node from mnemonic and passphrase
|
||||
passphrase = await _get_passphrase(ctx)
|
||||
root = bip32.from_mnemonic_cardano(mnemonic.get_secret().decode(), passphrase)
|
||||
|
||||
# derive the namespaced root node
|
||||
for i in SEED_NAMESPACE:
|
||||
|
@ -2,13 +2,13 @@ from trezor import wire
|
||||
from trezor.messages import ButtonRequestType
|
||||
from trezor.messages.ButtonAck import ButtonAck
|
||||
from trezor.messages.ButtonRequest import ButtonRequest
|
||||
from trezor.ui.confirm import CONFIRMED, Confirm, HoldToConfirm
|
||||
from trezor.ui.confirm import CONFIRMED, INFO, Confirm, HoldToConfirm, InfoConfirm
|
||||
|
||||
if __debug__:
|
||||
from apps.debug import confirm_signal
|
||||
|
||||
if False:
|
||||
from typing import Any
|
||||
from typing import Any, Callable
|
||||
from trezor import ui
|
||||
from trezor.ui.confirm import ButtonContent, ButtonStyleType
|
||||
from trezor.ui.loader import LoaderStyleType
|
||||
@ -47,6 +47,37 @@ async def confirm(
|
||||
return await ctx.wait(dialog) is CONFIRMED
|
||||
|
||||
|
||||
async def info_confirm(
|
||||
ctx: wire.Context,
|
||||
content: ui.Component,
|
||||
info_func: Callable,
|
||||
code: int = ButtonRequestType.Other,
|
||||
confirm: ButtonContent = InfoConfirm.DEFAULT_CONFIRM,
|
||||
confirm_style: ButtonStyleType = InfoConfirm.DEFAULT_CONFIRM_STYLE,
|
||||
cancel: ButtonContent = InfoConfirm.DEFAULT_CANCEL,
|
||||
cancel_style: ButtonStyleType = InfoConfirm.DEFAULT_CANCEL_STYLE,
|
||||
info: ButtonContent = InfoConfirm.DEFAULT_INFO,
|
||||
info_style: ButtonStyleType = InfoConfirm.DEFAULT_INFO_STYLE,
|
||||
) -> bool:
|
||||
await ctx.call(ButtonRequest(code=code), ButtonAck)
|
||||
|
||||
dialog = InfoConfirm(
|
||||
content, confirm, confirm_style, cancel, cancel_style, info, info_style
|
||||
)
|
||||
|
||||
while True:
|
||||
if __debug__:
|
||||
result = await ctx.wait(dialog, confirm_signal())
|
||||
else:
|
||||
result = await ctx.wait(dialog)
|
||||
|
||||
if result is INFO:
|
||||
await info_func(ctx)
|
||||
|
||||
else:
|
||||
return result is CONFIRMED
|
||||
|
||||
|
||||
async def hold_to_confirm(
|
||||
ctx: wire.Context,
|
||||
content: ui.Component,
|
||||
|
@ -1,24 +1,12 @@
|
||||
from micropython import const
|
||||
|
||||
from trezor import ui, workflow
|
||||
from trezor.crypto import bip39, slip39
|
||||
from trezor.messages import BackupType
|
||||
|
||||
from apps.common import storage
|
||||
|
||||
if False:
|
||||
from typing import Optional, Tuple
|
||||
|
||||
TYPE_BIP39 = const(0)
|
||||
TYPE_SLIP39 = const(1)
|
||||
TYPE_SLIP39_GROUP = const(2)
|
||||
|
||||
TYPES_WORD_COUNT = {
|
||||
12: TYPE_BIP39,
|
||||
18: TYPE_BIP39,
|
||||
24: TYPE_BIP39,
|
||||
20: TYPE_SLIP39,
|
||||
33: TYPE_SLIP39,
|
||||
}
|
||||
from trezor.messages.ResetDevice import EnumTypeBackupType
|
||||
|
||||
|
||||
def get() -> Tuple[Optional[bytes], int]:
|
||||
@ -29,15 +17,20 @@ def get_secret() -> Optional[bytes]:
|
||||
return storage.device.get_mnemonic_secret()
|
||||
|
||||
|
||||
def get_type() -> int:
|
||||
mnemonic_type = storage.device.get_mnemonic_type() or TYPE_BIP39
|
||||
if mnemonic_type not in (TYPE_BIP39, TYPE_SLIP39, TYPE_SLIP39_GROUP):
|
||||
raise RuntimeError("Invalid mnemonic type")
|
||||
return mnemonic_type
|
||||
def get_type() -> EnumTypeBackupType:
|
||||
return storage.device.get_backup_type()
|
||||
|
||||
|
||||
def is_bip39() -> bool:
|
||||
"""
|
||||
If False then SLIP-39 (either Basic or Advanced).
|
||||
Other invalid values are checked directly in storage.
|
||||
"""
|
||||
return get_type() == BackupType.Bip39
|
||||
|
||||
|
||||
def get_seed(passphrase: str = "", progress_bar: bool = True) -> bytes:
|
||||
mnemonic_secret, mnemonic_type = get()
|
||||
mnemonic_secret = get_secret()
|
||||
if mnemonic_secret is None:
|
||||
raise ValueError("Mnemonic not set")
|
||||
|
||||
@ -46,10 +39,10 @@ def get_seed(passphrase: str = "", progress_bar: bool = True) -> bytes:
|
||||
_start_progress()
|
||||
render_func = _render_progress
|
||||
|
||||
if mnemonic_type == TYPE_BIP39:
|
||||
if is_bip39():
|
||||
seed = bip39.seed(mnemonic_secret.decode(), passphrase, render_func)
|
||||
|
||||
elif mnemonic_type == TYPE_SLIP39 or mnemonic_type == TYPE_SLIP39_GROUP:
|
||||
else: # SLIP-39
|
||||
identifier = storage.device.get_slip39_identifier()
|
||||
iteration_exponent = storage.device.get_slip39_iteration_exponent()
|
||||
if identifier is None or iteration_exponent is None:
|
||||
@ -59,17 +52,9 @@ def get_seed(passphrase: str = "", progress_bar: bool = True) -> bytes:
|
||||
identifier, iteration_exponent, mnemonic_secret, passphrase.encode()
|
||||
)
|
||||
|
||||
if progress_bar:
|
||||
_stop_progress()
|
||||
return seed
|
||||
|
||||
|
||||
def type_from_word_count(count: int) -> int:
|
||||
if count not in TYPES_WORD_COUNT:
|
||||
raise RuntimeError("Recovery: Unknown words count")
|
||||
return TYPES_WORD_COUNT[count]
|
||||
|
||||
|
||||
def _start_progress() -> None:
|
||||
# Because we are drawing to the screen manually, without a layout, we
|
||||
# should make sure that no other layout is running. At this point, only
|
||||
@ -86,7 +71,3 @@ def _render_progress(progress: int, total: int) -> None:
|
||||
p = 1000 * progress // total
|
||||
ui.display.loader(p, False, 18, ui.WHITE, ui.BG)
|
||||
ui.display.refresh()
|
||||
|
||||
|
||||
def _stop_progress() -> None:
|
||||
pass
|
||||
|
@ -2,11 +2,13 @@ from micropython import const
|
||||
from ubinascii import hexlify
|
||||
|
||||
from trezor.crypto import random
|
||||
from trezor.messages import BackupType
|
||||
|
||||
from apps.common.sd_salt import SD_SALT_AUTH_KEY_LEN_BYTES
|
||||
from apps.common.storage import common
|
||||
|
||||
if False:
|
||||
from trezor.messages.ResetDevice import EnumTypeBackupType
|
||||
from typing import Optional
|
||||
|
||||
# Namespace:
|
||||
@ -28,11 +30,13 @@ _PASSPHRASE_SOURCE = const(0x0A) # int
|
||||
_UNFINISHED_BACKUP = const(0x0B) # bool (0x01 or empty)
|
||||
_AUTOLOCK_DELAY_MS = const(0x0C) # int
|
||||
_NO_BACKUP = const(0x0D) # bool (0x01 or empty)
|
||||
_MNEMONIC_TYPE = const(0x0E) # int
|
||||
_BACKUP_TYPE = const(0x0E) # int
|
||||
_ROTATION = const(0x0F) # int
|
||||
_SLIP39_IDENTIFIER = const(0x10) # bool
|
||||
_SLIP39_ITERATION_EXPONENT = const(0x11) # int
|
||||
_SD_SALT_AUTH_KEY = const(0x12) # bytes
|
||||
|
||||
_DEFAULT_BACKUP_TYPE = BackupType.Bip39
|
||||
# fmt: on
|
||||
|
||||
HOMESCREEN_MAXSIZE = 16384
|
||||
@ -80,8 +84,19 @@ def get_mnemonic_secret() -> Optional[bytes]:
|
||||
return common.get(_NAMESPACE, _MNEMONIC_SECRET)
|
||||
|
||||
|
||||
def get_mnemonic_type() -> Optional[int]:
|
||||
return common.get_uint8(_NAMESPACE, _MNEMONIC_TYPE)
|
||||
def get_backup_type() -> EnumTypeBackupType:
|
||||
backup_type = common.get_uint8(_NAMESPACE, _BACKUP_TYPE)
|
||||
if backup_type is None:
|
||||
backup_type = _DEFAULT_BACKUP_TYPE
|
||||
|
||||
if backup_type not in (
|
||||
BackupType.Bip39,
|
||||
BackupType.Slip39_Basic,
|
||||
BackupType.Slip39_Advanced,
|
||||
):
|
||||
# Invalid backup type
|
||||
raise RuntimeError
|
||||
return backup_type
|
||||
|
||||
|
||||
def has_passphrase() -> bool:
|
||||
@ -94,13 +109,13 @@ def get_homescreen() -> Optional[bytes]:
|
||||
|
||||
def store_mnemonic_secret(
|
||||
secret: bytes,
|
||||
mnemonic_type: int,
|
||||
backup_type: EnumTypeBackupType,
|
||||
needs_backup: bool = False,
|
||||
no_backup: bool = False,
|
||||
) -> None:
|
||||
set_version(common.STORAGE_VERSION_CURRENT)
|
||||
common.set(_NAMESPACE, _MNEMONIC_SECRET, secret)
|
||||
common.set_uint8(_NAMESPACE, _MNEMONIC_TYPE, mnemonic_type)
|
||||
common.set_uint8(_NAMESPACE, _BACKUP_TYPE, backup_type)
|
||||
common.set_true_or_delete(_NAMESPACE, _NO_BACKUP, no_backup)
|
||||
if not no_backup:
|
||||
common.set_true_or_delete(_NAMESPACE, _NEEDS_BACKUP, needs_backup)
|
||||
|
@ -4,6 +4,9 @@ from trezor.crypto import slip39
|
||||
|
||||
from apps.common.storage import common, recovery_shares
|
||||
|
||||
if False:
|
||||
from trezor.messages.ResetDevice import EnumTypeBackupType
|
||||
|
||||
# Namespace:
|
||||
_NAMESPACE = common.APP_RECOVERY
|
||||
|
||||
@ -12,12 +15,13 @@ _NAMESPACE = common.APP_RECOVERY
|
||||
_IN_PROGRESS = const(0x00) # bool
|
||||
_DRY_RUN = const(0x01) # bool
|
||||
_WORD_COUNT = const(0x02) # int
|
||||
_REMAINING = const(0x05) # int
|
||||
_SLIP39_IDENTIFIER = const(0x03) # bytes
|
||||
_SLIP39_THRESHOLD = const(0x04) # int
|
||||
_REMAINING = const(0x05) # int
|
||||
_SLIP39_ITERATION_EXPONENT = const(0x06) # int
|
||||
_SLIP39_GROUP_COUNT = const(0x07) # int
|
||||
_SLIP39_GROUP_THRESHOLD = const(0x08) # int
|
||||
_BACKUP_TYPE = const(0x09) # int
|
||||
# fmt: on
|
||||
|
||||
if False:
|
||||
@ -48,6 +52,14 @@ def get_word_count() -> Optional[int]:
|
||||
return common.get_uint8(_NAMESPACE, _WORD_COUNT)
|
||||
|
||||
|
||||
def set_backup_type(backup_type: EnumTypeBackupType) -> None:
|
||||
common.set_uint8(_NAMESPACE, _BACKUP_TYPE, backup_type)
|
||||
|
||||
|
||||
def get_backup_type() -> Optional[EnumTypeBackupType]:
|
||||
return common.get_uint8(_NAMESPACE, _BACKUP_TYPE)
|
||||
|
||||
|
||||
def set_slip39_identifier(identifier: int) -> None:
|
||||
common.set_uint16(_NAMESPACE, _SLIP39_IDENTIFIER, identifier)
|
||||
|
||||
@ -88,7 +100,7 @@ def get_slip39_group_threshold() -> Optional[int]:
|
||||
return common.get_uint8(_NAMESPACE, _SLIP39_GROUP_THRESHOLD)
|
||||
|
||||
|
||||
def set_slip39_remaining_shares(shares_remaining: int, group_index: int = 0) -> None:
|
||||
def set_slip39_remaining_shares(shares_remaining: int, group_index: int) -> None:
|
||||
"""
|
||||
We store the remaining shares as a bytearray of length group_count.
|
||||
Each byte represents share remaining for group of that group_index.
|
||||
@ -96,16 +108,17 @@ def set_slip39_remaining_shares(shares_remaining: int, group_index: int = 0) ->
|
||||
share count for a group.
|
||||
"""
|
||||
remaining = common.get(_NAMESPACE, _REMAINING)
|
||||
if not get_slip39_group_count():
|
||||
raise RuntimeError()
|
||||
group_count = get_slip39_group_count()
|
||||
if not group_count:
|
||||
raise RuntimeError
|
||||
if remaining is None:
|
||||
remaining = bytearray([slip39.MAX_SHARE_COUNT] * get_slip39_group_count())
|
||||
remaining = bytearray([slip39.MAX_SHARE_COUNT] * group_count)
|
||||
remaining = bytearray(remaining)
|
||||
remaining[group_index] = shares_remaining
|
||||
common.set(_NAMESPACE, _REMAINING, remaining)
|
||||
|
||||
|
||||
def get_slip39_remaining_shares(group_index: int = 0) -> Optional[int]:
|
||||
def get_slip39_remaining_shares(group_index: int) -> Optional[int]:
|
||||
remaining = common.get(_NAMESPACE, _REMAINING)
|
||||
if remaining is None or remaining[group_index] == slip39.MAX_SHARE_COUNT:
|
||||
return None
|
||||
@ -135,4 +148,5 @@ def end_progress() -> None:
|
||||
common.delete(_NAMESPACE, _SLIP39_ITERATION_EXPONENT)
|
||||
common.delete(_NAMESPACE, _SLIP39_GROUP_COUNT)
|
||||
common.delete(_NAMESPACE, _SLIP39_GROUP_THRESHOLD)
|
||||
common.delete(_NAMESPACE, _BACKUP_TYPE)
|
||||
recovery_shares.delete()
|
||||
|
@ -20,20 +20,19 @@ def get(index: int) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def fetch() -> List[str]:
|
||||
def fetch() -> List[List[str]]:
|
||||
mnemonics = []
|
||||
if not recovery.get_slip39_group_count():
|
||||
raise RuntimeError
|
||||
for index in range(0, slip39.MAX_SHARE_COUNT * recovery.get_slip39_group_count()):
|
||||
m = get(index)
|
||||
if m:
|
||||
mnemonics.append(m)
|
||||
return mnemonics
|
||||
for i in range(recovery.get_slip39_group_count()):
|
||||
mnemonics.append(fetch_group(i))
|
||||
|
||||
return mnemonics
|
||||
|
||||
|
||||
def fetch_group(group_index: int) -> List[str]:
|
||||
mnemonics = []
|
||||
starting_index = 0 + group_index * slip39.MAX_SHARE_COUNT
|
||||
starting_index = group_index * slip39.MAX_SHARE_COUNT
|
||||
for index in range(starting_index, starting_index + slip39.MAX_SHARE_COUNT):
|
||||
m = get(index)
|
||||
if m:
|
||||
@ -43,5 +42,5 @@ def fetch_group(group_index: int) -> List[str]:
|
||||
|
||||
|
||||
def delete() -> None:
|
||||
for index in range(0, slip39.MAX_SHARE_COUNT):
|
||||
for index in range(slip39.MAX_SHARE_COUNT * slip39.MAX_GROUP_COUNT):
|
||||
common.delete(common.APP_RECOVERY_SHARES, index)
|
||||
|
@ -4,7 +4,7 @@ from trezor.messages.Features import Features
|
||||
from trezor.messages.Success import Success
|
||||
from trezor.wire import register
|
||||
|
||||
from apps.common import cache, storage
|
||||
from apps.common import cache, mnemonic, storage
|
||||
|
||||
if False:
|
||||
from typing import NoReturn
|
||||
@ -36,6 +36,7 @@ def get_features() -> Features:
|
||||
f.no_backup = storage.device.no_backup()
|
||||
f.flags = storage.device.get_flags()
|
||||
f.recovery_mode = storage.recovery.is_in_progress()
|
||||
f.backup_type = mnemonic.get_type()
|
||||
if utils.BITCOIN_ONLY:
|
||||
f.capabilities = [
|
||||
Capability.Bitcoin,
|
||||
|
@ -2,11 +2,7 @@ from trezor import wire
|
||||
from trezor.messages.Success import Success
|
||||
|
||||
from apps.common import mnemonic, storage
|
||||
from apps.management.common import layout
|
||||
from apps.management.reset_device import (
|
||||
backup_group_slip39_wallet,
|
||||
backup_slip39_wallet,
|
||||
)
|
||||
from apps.management.reset_device import backup_seed, layout
|
||||
|
||||
|
||||
async def backup_device(ctx, msg):
|
||||
@ -20,12 +16,7 @@ async def backup_device(ctx, msg):
|
||||
storage.device.set_unfinished_backup(True)
|
||||
storage.device.set_backed_up()
|
||||
|
||||
if mnemonic_type == mnemonic.TYPE_SLIP39:
|
||||
await backup_slip39_wallet(ctx, mnemonic_secret)
|
||||
elif mnemonic_type == mnemonic.TYPE_SLIP39_GROUP:
|
||||
await backup_group_slip39_wallet(ctx, mnemonic_secret)
|
||||
else:
|
||||
await layout.bip39_show_and_confirm_mnemonic(ctx, mnemonic_secret.decode())
|
||||
await backup_seed(ctx, mnemonic_type, mnemonic_secret)
|
||||
|
||||
storage.device.set_unfinished_backup(False)
|
||||
|
||||
|
25
core/src/apps/management/backup_types.py
Normal file
25
core/src/apps/management/backup_types.py
Normal file
@ -0,0 +1,25 @@
|
||||
from trezor.messages import BackupType
|
||||
|
||||
if False:
|
||||
from trezor.messages.ResetDevice import EnumTypeBackupType
|
||||
|
||||
|
||||
_BIP39_WORD_COUNTS = (12, 18, 24)
|
||||
_SLIP39_WORD_COUNTS = (20, 33)
|
||||
|
||||
|
||||
def is_slip39_word_count(word_count: int) -> bool:
|
||||
"""
|
||||
Returns True for SLIP-39 and False for BIP-39.
|
||||
Raise RuntimeError otherwise.
|
||||
"""
|
||||
if word_count in _SLIP39_WORD_COUNTS:
|
||||
return True
|
||||
elif word_count in _BIP39_WORD_COUNTS:
|
||||
return False
|
||||
# Unknown word count.
|
||||
raise RuntimeError
|
||||
|
||||
|
||||
def is_slip39_backup_type(backup_type: EnumTypeBackupType):
|
||||
return backup_type in (BackupType.Slip39_Basic, BackupType.Slip39_Advanced)
|
@ -1,14 +1,53 @@
|
||||
from trezor import config, wire
|
||||
from trezor.crypto import bip39, slip39
|
||||
from trezor.messages import BackupType
|
||||
from trezor.messages.Success import Success
|
||||
from trezor.pin import pin_to_int
|
||||
from trezor.ui.text import Text
|
||||
|
||||
from apps.common import mnemonic, storage
|
||||
from apps.common import storage
|
||||
from apps.common.confirm import require_confirm
|
||||
from apps.management import backup_types
|
||||
|
||||
|
||||
async def load_device(ctx, msg):
|
||||
word_count = _validate(msg)
|
||||
is_slip39 = backup_types.is_slip39_word_count(word_count)
|
||||
|
||||
if not is_slip39 and not msg.skip_checksum and not bip39.check(msg.mnemonics[0]):
|
||||
raise wire.ProcessError("Mnemonic is not valid")
|
||||
|
||||
await _warn(ctx)
|
||||
|
||||
if not is_slip39: # BIP-39
|
||||
secret = msg.mnemonics[0].encode()
|
||||
backup_type = BackupType.Bip39
|
||||
else:
|
||||
identifier, iteration_exponent, secret, group_count = slip39.combine_mnemonics(
|
||||
msg.mnemonics
|
||||
)
|
||||
if group_count == 1:
|
||||
backup_type = BackupType.Slip39_Basic
|
||||
elif group_count > 1:
|
||||
backup_type = BackupType.Slip39_Advanced
|
||||
else:
|
||||
raise RuntimeError("Invalid group count")
|
||||
storage.device.set_slip39_identifier(identifier)
|
||||
storage.device.set_slip39_iteration_exponent(iteration_exponent)
|
||||
|
||||
storage.device.store_mnemonic_secret(
|
||||
secret, backup_type, needs_backup=True, no_backup=False
|
||||
)
|
||||
storage.device.load_settings(
|
||||
use_passphrase=msg.passphrase_protection, label=msg.label
|
||||
)
|
||||
if msg.pin:
|
||||
config.change_pin(pin_to_int(""), pin_to_int(msg.pin), None, None)
|
||||
|
||||
return Success(message="Device loaded")
|
||||
|
||||
|
||||
def _validate(msg) -> int:
|
||||
if storage.is_initialized():
|
||||
raise wire.UnexpectedMessage("Already initialized")
|
||||
|
||||
@ -25,36 +64,11 @@ async def load_device(ctx, msg):
|
||||
"All shares are required to have the same number of words"
|
||||
)
|
||||
|
||||
mnemonic_type = mnemonic.type_from_word_count(word_count)
|
||||
return word_count
|
||||
|
||||
if (
|
||||
mnemonic_type == mnemonic.TYPE_BIP39
|
||||
and not msg.skip_checksum
|
||||
and not bip39.check(msg.mnemonics[0])
|
||||
):
|
||||
raise wire.ProcessError("Mnemonic is not valid")
|
||||
|
||||
async def _warn(ctx: wire.Context):
|
||||
text = Text("Loading seed")
|
||||
text.bold("Loading private seed", "is not recommended.")
|
||||
text.normal("Continue only if you", "know what you are doing!")
|
||||
await require_confirm(ctx, text)
|
||||
|
||||
if mnemonic_type == mnemonic.TYPE_BIP39:
|
||||
secret = msg.mnemonics[0].encode()
|
||||
elif mnemonic_type == mnemonic.TYPE_SLIP39:
|
||||
identifier, iteration_exponent, secret = slip39.combine_mnemonics(msg.mnemonics)
|
||||
storage.device.set_slip39_identifier(identifier)
|
||||
storage.device.set_slip39_iteration_exponent(iteration_exponent)
|
||||
else:
|
||||
raise RuntimeError("Unknown mnemonic type")
|
||||
|
||||
storage.device.store_mnemonic_secret(
|
||||
secret, mnemonic_type, needs_backup=True, no_backup=False
|
||||
)
|
||||
storage.device.load_settings(
|
||||
use_passphrase=msg.passphrase_protection, label=msg.label
|
||||
)
|
||||
if msg.pin:
|
||||
config.change_pin(pin_to_int(""), pin_to_int(msg.pin), None, None)
|
||||
|
||||
return Success(message="Device loaded")
|
||||
|
@ -1,21 +1,20 @@
|
||||
from trezor import loop, utils, wire
|
||||
from trezor.crypto.hashlib import sha256
|
||||
from trezor.errors import (
|
||||
GroupThresholdReachedError,
|
||||
IdentifierMismatchError,
|
||||
MnemonicError,
|
||||
ShareAlreadyAddedError,
|
||||
)
|
||||
from trezor.crypto.slip39 import MAX_SHARE_COUNT, Share
|
||||
from trezor.errors import MnemonicError
|
||||
from trezor.messages import BackupType
|
||||
from trezor.messages.Success import Success
|
||||
|
||||
from . import recover
|
||||
|
||||
from apps.common import mnemonic, storage
|
||||
from apps.common.layout import show_success
|
||||
from apps.management import backup_types
|
||||
from apps.management.recovery_device import layout
|
||||
|
||||
if False:
|
||||
from typing import List
|
||||
from typing import Optional, Tuple
|
||||
from trezor.messages.ResetDevice import EnumTypeBackupType
|
||||
|
||||
|
||||
async def recovery_homescreen() -> None:
|
||||
@ -45,36 +44,62 @@ async def recovery_process(ctx: wire.Context) -> Success:
|
||||
|
||||
async def _continue_recovery_process(ctx: wire.Context) -> Success:
|
||||
# gather the current recovery state from storage
|
||||
in_progress = storage.recovery.is_in_progress()
|
||||
word_count = storage.recovery.get_word_count()
|
||||
dry_run = storage.recovery.is_dry_run()
|
||||
backup_type = storage.recovery.get_backup_type()
|
||||
|
||||
if not in_progress: # invalid and inconsistent state
|
||||
raise RuntimeError
|
||||
if not word_count: # the first run, prompt word count from the user
|
||||
word_count = await _request_and_store_word_count(ctx, dry_run)
|
||||
|
||||
mnemonic_type = mnemonic.type_from_word_count(word_count)
|
||||
is_slip39 = backup_types.is_slip39_word_count(word_count)
|
||||
await _request_share_first_screen(ctx, word_count, is_slip39)
|
||||
|
||||
secret = await _request_secret(ctx, word_count, mnemonic_type)
|
||||
secret = None
|
||||
while secret is None:
|
||||
# ask for mnemonic words one by one
|
||||
words = await layout.request_mnemonic(ctx, word_count, backup_type)
|
||||
|
||||
# if they were invalid or some checks failed we continue and request them again
|
||||
if not words:
|
||||
continue
|
||||
|
||||
try:
|
||||
secret, backup_type = await _process_words(
|
||||
ctx, words, is_slip39, backup_type
|
||||
)
|
||||
except MnemonicError:
|
||||
await layout.show_invalid_mnemonic(ctx, is_slip39)
|
||||
# If the backup type is not stored, we have processed zero mnemonics.
|
||||
# In that case we prompt the word count again to give the user an
|
||||
# opportunity to change the word count if they've made a mistake.
|
||||
first_mnemonic = storage.recovery.get_backup_type() is None
|
||||
if first_mnemonic:
|
||||
word_count = await _request_and_store_word_count(ctx, dry_run)
|
||||
is_slip39 = backup_types.is_slip39_word_count(word_count)
|
||||
backup_type = None
|
||||
continue
|
||||
|
||||
if dry_run:
|
||||
result = await _finish_recovery_dry_run(ctx, secret, mnemonic_type)
|
||||
result = await _finish_recovery_dry_run(ctx, secret)
|
||||
else:
|
||||
result = await _finish_recovery(ctx, secret, mnemonic_type)
|
||||
result = await _finish_recovery(ctx, secret)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def _finish_recovery_dry_run(
|
||||
ctx: wire.Context, secret: bytes, mnemonic_type: int
|
||||
) -> Success:
|
||||
async def _finish_recovery_dry_run(ctx: wire.Context, secret: bytes) -> Success:
|
||||
backup_type = storage.recovery.get_backup_type()
|
||||
if backup_type is None:
|
||||
raise RuntimeError
|
||||
|
||||
digest_input = sha256(secret).digest()
|
||||
stored = mnemonic.get_secret()
|
||||
digest_stored = sha256(stored).digest()
|
||||
result = utils.consteq(digest_stored, digest_input)
|
||||
|
||||
is_slip39 = backup_types.is_slip39_backup_type(backup_type)
|
||||
# Check that the identifier and iteration exponent match as well
|
||||
if mnemonic_type == mnemonic.TYPE_SLIP39:
|
||||
if is_slip39:
|
||||
result &= (
|
||||
storage.device.get_slip39_identifier()
|
||||
== storage.recovery.get_slip39_identifier()
|
||||
@ -84,7 +109,7 @@ async def _finish_recovery_dry_run(
|
||||
== storage.recovery.get_slip39_iteration_exponent()
|
||||
)
|
||||
|
||||
await layout.show_dry_run_result(ctx, result, mnemonic_type)
|
||||
await layout.show_dry_run_result(ctx, result, is_slip39)
|
||||
|
||||
storage.recovery.end_progress()
|
||||
|
||||
@ -94,19 +119,15 @@ async def _finish_recovery_dry_run(
|
||||
raise wire.ProcessError("The seed does not match the one in the device")
|
||||
|
||||
|
||||
async def _finish_recovery(
|
||||
ctx: wire.Context, secret: bytes, mnemonic_type: int
|
||||
) -> Success:
|
||||
group_count = storage.recovery.get_slip39_group_count()
|
||||
if group_count and group_count > 1:
|
||||
mnemonic_type = mnemonic.TYPE_SLIP39_GROUP
|
||||
async def _finish_recovery(ctx: wire.Context, secret: bytes) -> Success:
|
||||
backup_type = storage.recovery.get_backup_type()
|
||||
if backup_type is None:
|
||||
raise RuntimeError
|
||||
|
||||
storage.device.store_mnemonic_secret(
|
||||
secret, mnemonic_type, needs_backup=False, no_backup=False
|
||||
secret, backup_type, needs_backup=False, no_backup=False
|
||||
)
|
||||
if (
|
||||
mnemonic_type == mnemonic.TYPE_SLIP39
|
||||
or mnemonic_type == mnemonic.TYPE_SLIP39_GROUP
|
||||
):
|
||||
if backup_type in (BackupType.Slip39_Basic, BackupType.Slip39_Advanced):
|
||||
identifier = storage.recovery.get_slip39_identifier()
|
||||
exponent = storage.recovery.get_slip39_iteration_exponent()
|
||||
if identifier is None or exponent is None:
|
||||
@ -134,120 +155,102 @@ async def _request_and_store_word_count(ctx: wire.Context, dry_run: bool) -> int
|
||||
return word_count
|
||||
|
||||
|
||||
async def _request_secret(
|
||||
ctx: wire.Context, word_count: int, mnemonic_type: int
|
||||
) -> bytes:
|
||||
await _request_share_first_screen(ctx, word_count, mnemonic_type)
|
||||
async def _process_words(
|
||||
ctx: wire.Context,
|
||||
words: str,
|
||||
is_slip39: bool,
|
||||
backup_type: Optional[EnumTypeBackupType],
|
||||
) -> Tuple[Optional[bytes], EnumTypeBackupType]:
|
||||
|
||||
mnemonics = None
|
||||
advanced_shamir = False
|
||||
secret = None
|
||||
while secret is None:
|
||||
group_count = storage.recovery.get_slip39_group_count()
|
||||
if group_count:
|
||||
mnemonics = storage.recovery_shares.fetch()
|
||||
advanced_shamir = group_count > 1
|
||||
group_threshold = storage.recovery.get_slip39_group_threshold()
|
||||
shares_remaining = storage.recovery.fetch_slip39_remaining_shares()
|
||||
share = None
|
||||
if not is_slip39: # BIP-39
|
||||
secret = recover.process_bip39(words)
|
||||
else:
|
||||
secret, share = recover.process_slip39(words)
|
||||
|
||||
if advanced_shamir:
|
||||
await _show_remaining_groups_and_shares(
|
||||
ctx, group_threshold, shares_remaining
|
||||
)
|
||||
if backup_type is None:
|
||||
# we have to decide what backup type this is and store it
|
||||
backup_type = _store_backup_type(is_slip39, share)
|
||||
|
||||
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)
|
||||
if secret is None:
|
||||
if share.group_count and share.group_count > 1:
|
||||
await layout.show_group_share_success(ctx, share.index, share.group_index)
|
||||
await _request_share_next_screen(ctx)
|
||||
|
||||
return secret
|
||||
return secret, backup_type
|
||||
|
||||
|
||||
def _store_backup_type(is_slip39: bool, share: Share = None) -> EnumTypeBackupType:
|
||||
if not is_slip39: # BIP-39
|
||||
backup_type = BackupType.Bip39
|
||||
elif not share or share.group_count < 1: # invalid parameters
|
||||
raise RuntimeError
|
||||
elif share.group_count == 1:
|
||||
backup_type = BackupType.Slip39_Basic
|
||||
else:
|
||||
backup_type = BackupType.Slip39_Advanced
|
||||
|
||||
storage.recovery.set_backup_type(backup_type)
|
||||
return backup_type
|
||||
|
||||
|
||||
async def _request_share_first_screen(
|
||||
ctx: wire.Context, word_count: int, mnemonic_type: int
|
||||
ctx: wire.Context, word_count: int, is_slip39: bool
|
||||
) -> None:
|
||||
if mnemonic_type == mnemonic.TYPE_BIP39:
|
||||
content = layout.RecoveryHomescreen(
|
||||
"Enter recovery seed", "(%d words)" % word_count
|
||||
)
|
||||
await layout.homescreen_dialog(ctx, content, "Enter seed")
|
||||
elif mnemonic_type == mnemonic.TYPE_SLIP39:
|
||||
if is_slip39:
|
||||
remaining = storage.recovery.fetch_slip39_remaining_shares()
|
||||
if remaining:
|
||||
await _request_share_next_screen(ctx, mnemonic_type)
|
||||
await _request_share_next_screen(ctx)
|
||||
else:
|
||||
content = layout.RecoveryHomescreen(
|
||||
"Enter any share", "(%d words)" % word_count
|
||||
)
|
||||
await layout.homescreen_dialog(ctx, content, "Enter share")
|
||||
else:
|
||||
else: # BIP-39
|
||||
content = layout.RecoveryHomescreen(
|
||||
"Enter recovery seed", "(%d words)" % word_count
|
||||
)
|
||||
await layout.homescreen_dialog(ctx, content, "Enter seed")
|
||||
|
||||
|
||||
async def _request_share_next_screen(ctx: wire.Context) -> None:
|
||||
remaining = storage.recovery.fetch_slip39_remaining_shares()
|
||||
group_count = storage.recovery.get_slip39_group_count()
|
||||
if not remaining:
|
||||
# 'remaining' should be stored at this point
|
||||
raise RuntimeError
|
||||
|
||||
|
||||
async def _request_share_next_screen(ctx: wire.Context, mnemonic_type: int) -> None:
|
||||
if mnemonic_type == mnemonic.TYPE_SLIP39:
|
||||
remaining = storage.recovery.fetch_slip39_remaining_shares()
|
||||
group_count = storage.recovery.get_slip39_group_count()
|
||||
if not remaining:
|
||||
# 'remaining' should be stored at this point
|
||||
raise RuntimeError
|
||||
|
||||
if group_count > 1:
|
||||
content = layout.RecoveryHomescreen(
|
||||
"More shares needed", "for this recovery"
|
||||
)
|
||||
await layout.homescreen_dialog(ctx, content, "Enter share")
|
||||
if group_count > 1:
|
||||
content = layout.RecoveryHomescreen("More shares needed")
|
||||
await layout.homescreen_dialog(
|
||||
ctx, content, "Enter", _show_remaining_groups_and_shares
|
||||
)
|
||||
else:
|
||||
if remaining[0] == 1:
|
||||
text = "1 more 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:
|
||||
raise RuntimeError
|
||||
text = "%d more shares" % remaining[0]
|
||||
content = layout.RecoveryHomescreen(text, "needed to enter")
|
||||
await layout.homescreen_dialog(ctx, content, "Enter share")
|
||||
|
||||
|
||||
async def _show_remaining_groups_and_shares(
|
||||
ctx: wire.Context, group_threshold: int, shares_remaining: List[int]
|
||||
) -> None:
|
||||
async def _show_remaining_groups_and_shares(ctx: wire.Context) -> None:
|
||||
"""
|
||||
Show info dialog for Slip39 Advanced - what shares are to be entered.
|
||||
"""
|
||||
shares_remaining = storage.recovery.fetch_slip39_remaining_shares()
|
||||
|
||||
identifiers = []
|
||||
|
||||
first_entered_index = -1
|
||||
for i in range(len(shares_remaining)):
|
||||
if shares_remaining[i] < 16:
|
||||
if shares_remaining[i] < MAX_SHARE_COUNT:
|
||||
first_entered_index = i
|
||||
|
||||
for i, r in enumerate(shares_remaining):
|
||||
if 0 < r < 16:
|
||||
if 0 < r < MAX_SHARE_COUNT:
|
||||
identifier = storage.recovery_shares.fetch_group(i)[0].split(" ")[0:3]
|
||||
identifiers.append([r, identifier])
|
||||
elif r == 16:
|
||||
elif r == MAX_SHARE_COUNT:
|
||||
identifier = storage.recovery_shares.fetch_group(first_entered_index)[
|
||||
0
|
||||
].split(" ")[0:2]
|
||||
@ -257,6 +260,4 @@ async def _show_remaining_groups_and_shares(
|
||||
except ValueError:
|
||||
identifiers.append([r, identifier])
|
||||
|
||||
return await layout.show_remaining_shares(
|
||||
ctx, identifiers, group_threshold, shares_remaining
|
||||
)
|
||||
return await layout.show_remaining_shares(ctx, identifiers, shares_remaining)
|
||||
|
@ -1,9 +1,8 @@
|
||||
from trezor import ui, wire
|
||||
from trezor.errors import IdentifierMismatchError, ShareAlreadyAddedError
|
||||
from trezor.messages import ButtonRequestType
|
||||
from trezor.crypto.slip39 import MAX_SHARE_COUNT
|
||||
from trezor.messages import BackupType, ButtonRequestType
|
||||
from trezor.messages.ButtonAck import ButtonAck
|
||||
from trezor.messages.ButtonRequest import ButtonRequest
|
||||
from trezor.ui.info import InfoConfirm
|
||||
from trezor.ui.scroll import Paginated
|
||||
from trezor.ui.text import Text
|
||||
from trezor.ui.word_select import WordSelector
|
||||
@ -12,15 +11,17 @@ from .keyboard_bip39 import Bip39Keyboard
|
||||
from .keyboard_slip39 import Slip39Keyboard
|
||||
from .recover import RecoveryAborted
|
||||
|
||||
from apps.common import mnemonic, storage
|
||||
from apps.common.confirm import confirm, require_confirm
|
||||
from apps.common import storage
|
||||
from apps.common.confirm import confirm, info_confirm, require_confirm
|
||||
from apps.common.layout import show_success, show_warning
|
||||
from apps.management import backup_types
|
||||
|
||||
if __debug__:
|
||||
from apps.debug import input_signal, confirm_signal
|
||||
from apps.debug import input_signal
|
||||
|
||||
if False:
|
||||
from typing import List
|
||||
from typing import List, Optional, Callable
|
||||
from trezor.messages.ResetDevice import EnumTypeBackupType
|
||||
|
||||
|
||||
async def confirm_abort(ctx: wire.Context, dry_run: bool = False) -> bool:
|
||||
@ -53,17 +54,13 @@ async def request_word_count(ctx: wire.Context, dry_run: bool) -> int:
|
||||
|
||||
|
||||
async def request_mnemonic(
|
||||
ctx: wire.Context,
|
||||
word_count: int,
|
||||
mnemonic_type: int,
|
||||
mnemonics: List[str],
|
||||
advanced_shamir: bool = False,
|
||||
) -> str:
|
||||
ctx: wire.Context, word_count: int, backup_type: Optional[EnumTypeBackupType]
|
||||
) -> Optional[str]:
|
||||
await ctx.call(ButtonRequest(code=ButtonRequestType.MnemonicInput), ButtonAck)
|
||||
|
||||
words = []
|
||||
for i in range(word_count):
|
||||
if mnemonic_type == mnemonic.TYPE_SLIP39:
|
||||
if backup_types.is_slip39_word_count(word_count):
|
||||
keyboard = Slip39Keyboard("Type word %s of %s:" % (i + 1, word_count))
|
||||
else:
|
||||
keyboard = Bip39Keyboard("Type word %s of %s:" % (i + 1, word_count))
|
||||
@ -72,41 +69,95 @@ async def request_mnemonic(
|
||||
else:
|
||||
word = await ctx.wait(keyboard)
|
||||
|
||||
if mnemonic_type == mnemonic.TYPE_SLIP39 and mnemonics:
|
||||
if not advanced_shamir:
|
||||
# check if first 3 words of mnemonic match
|
||||
# we can check against the first one, others were checked already
|
||||
if i < 3:
|
||||
share_list = mnemonics[0].split(" ")
|
||||
if share_list[i] != word:
|
||||
raise IdentifierMismatchError()
|
||||
elif i == 3:
|
||||
for share in mnemonics:
|
||||
share_list = share.split(" ")
|
||||
# check if the fourth word is different from previous shares
|
||||
if share_list[i] == word:
|
||||
raise ShareAlreadyAddedError()
|
||||
else:
|
||||
# in case of advanced shamir recovery we only check 2 words
|
||||
if i < 2:
|
||||
share_list = mnemonics[0].split(" ")
|
||||
if share_list[i] != word:
|
||||
raise IdentifierMismatchError()
|
||||
if not await check_word_validity(ctx, i, word, backup_type, words):
|
||||
return None
|
||||
|
||||
words.append(word)
|
||||
|
||||
return " ".join(words)
|
||||
|
||||
|
||||
async def check_word_validity(
|
||||
ctx: wire.Context,
|
||||
current_index: int,
|
||||
current_word: str,
|
||||
backup_type: Optional[EnumTypeBackupType],
|
||||
previous_words: List[str],
|
||||
) -> bool:
|
||||
# we can't perform any checks if the backup type was not yet decided
|
||||
if backup_type is None:
|
||||
return True
|
||||
# there are no "on-the-fly" checks for BIP-39
|
||||
if backup_type is BackupType.Bip39:
|
||||
return True
|
||||
|
||||
previous_mnemonics = storage.recovery_shares.fetch()
|
||||
if not previous_mnemonics:
|
||||
# this function must be called only if some mnemonics are already stored
|
||||
raise RuntimeError
|
||||
|
||||
if backup_type == BackupType.Slip39_Basic:
|
||||
# check if first 3 words of mnemonic match
|
||||
# we can check against the first one, others were checked already
|
||||
if current_index < 3:
|
||||
share_list = previous_mnemonics[0][0].split(" ")
|
||||
if share_list[current_index] != current_word:
|
||||
await show_identifier_mismatch(ctx)
|
||||
return False
|
||||
elif current_index == 3:
|
||||
for share in previous_mnemonics[0]:
|
||||
share_list = share.split(" ")
|
||||
# check if the fourth word is different from previous shares
|
||||
if share_list[current_index] == current_word:
|
||||
await show_share_already_added(ctx)
|
||||
return False
|
||||
elif backup_type == BackupType.Slip39_Advanced:
|
||||
# in case of advanced slip39 recovery we only check 2 words
|
||||
if current_index < 2:
|
||||
share_list = next(s for s in previous_mnemonics if s)[0].split(" ")
|
||||
if share_list[current_index] != current_word:
|
||||
await show_identifier_mismatch(ctx)
|
||||
return False
|
||||
# check if we reached threshold in group
|
||||
elif current_index == 2:
|
||||
for i, group in enumerate(previous_mnemonics):
|
||||
if len(group) > 0:
|
||||
if current_word == group[0].split(" ")[current_index]:
|
||||
remaining_shares = (
|
||||
storage.recovery.fetch_slip39_remaining_shares()
|
||||
)
|
||||
if remaining_shares[i] == 0:
|
||||
await show_group_threshold_reached(ctx)
|
||||
return False
|
||||
# check if share was already added for group
|
||||
elif current_index == 3:
|
||||
# we use the 3rd word from previously entered shares to find the group id
|
||||
group_identifier_word = previous_words[2]
|
||||
group_index = None
|
||||
for i, group in enumerate(previous_mnemonics):
|
||||
if len(group) > 0:
|
||||
if group_identifier_word == group[0].split(" ")[2]:
|
||||
group_index = i
|
||||
|
||||
if group_index:
|
||||
group = previous_mnemonics[group_index]
|
||||
for share in group:
|
||||
if current_word == share.split(" ")[current_index]:
|
||||
await show_share_already_added(ctx)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def show_remaining_shares(
|
||||
ctx: wire.Context,
|
||||
groups: List[[int, List[str]]], # remaining + list 3 words
|
||||
group_threshold: int,
|
||||
shares_remaining: List[int],
|
||||
) -> None:
|
||||
group_threshold = storage.recovery.get_slip39_group_threshold()
|
||||
pages = []
|
||||
for remaining, group in groups:
|
||||
if 0 < remaining < 16:
|
||||
if 0 < remaining < MAX_SHARE_COUNT:
|
||||
text = Text("Remaining Shares")
|
||||
if remaining > 1:
|
||||
text.bold("%s more shares starting" % remaining)
|
||||
@ -115,7 +166,9 @@ async def show_remaining_shares(
|
||||
for word in group:
|
||||
text.normal(word)
|
||||
pages.append(text)
|
||||
elif remaining == 16 and shares_remaining.count(0) < group_threshold:
|
||||
elif (
|
||||
remaining == MAX_SHARE_COUNT and shares_remaining.count(0) < group_threshold
|
||||
):
|
||||
text = Text("Remaining Shares")
|
||||
groups_remaining = group_threshold - shares_remaining.count(0)
|
||||
if groups_remaining > 1:
|
||||
@ -126,7 +179,7 @@ async def show_remaining_shares(
|
||||
text.normal(word)
|
||||
pages.append(text)
|
||||
|
||||
return await confirm(ctx, Paginated(pages), confirm="Continue", cancel=None)
|
||||
return await confirm(ctx, Paginated(pages), cancel=None)
|
||||
|
||||
|
||||
async def show_group_share_success(
|
||||
@ -141,11 +194,9 @@ async def show_group_share_success(
|
||||
return await confirm(ctx, text, confirm="Continue", cancel=None)
|
||||
|
||||
|
||||
async def show_dry_run_result(
|
||||
ctx: wire.Context, result: bool, mnemonic_type: int
|
||||
) -> None:
|
||||
async def show_dry_run_result(ctx: wire.Context, result: bool, is_slip39: bool) -> None:
|
||||
if result:
|
||||
if mnemonic_type == mnemonic.TYPE_SLIP39:
|
||||
if is_slip39:
|
||||
text = (
|
||||
"The entered recovery",
|
||||
"shares are valid and",
|
||||
@ -161,7 +212,7 @@ async def show_dry_run_result(
|
||||
)
|
||||
await show_success(ctx, text, button="Continue")
|
||||
else:
|
||||
if mnemonic_type == mnemonic.TYPE_SLIP39:
|
||||
if is_slip39:
|
||||
text = (
|
||||
"The entered recovery",
|
||||
"shares are valid but",
|
||||
@ -188,25 +239,8 @@ async def show_dry_run_different_type(ctx: wire.Context) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def show_keyboard_info(ctx: wire.Context) -> None:
|
||||
# TODO: do not send ButtonRequestType.Other
|
||||
await ctx.call(ButtonRequest(code=ButtonRequestType.Other), ButtonAck)
|
||||
|
||||
info = InfoConfirm(
|
||||
"Did you know? "
|
||||
"You can type the letters "
|
||||
"one by one or use it like "
|
||||
"a T9 keyboard.",
|
||||
"Great!",
|
||||
)
|
||||
if __debug__:
|
||||
await ctx.wait(info, confirm_signal())
|
||||
else:
|
||||
await ctx.wait(info)
|
||||
|
||||
|
||||
async def show_invalid_mnemonic(ctx: wire.Context, mnemonic_type: int) -> None:
|
||||
if mnemonic_type == mnemonic.TYPE_SLIP39:
|
||||
async def show_invalid_mnemonic(ctx: wire.Context, is_slip39: bool) -> None:
|
||||
if is_slip39:
|
||||
await show_warning(ctx, ("You have entered", "an invalid recovery", "share."))
|
||||
else:
|
||||
await show_warning(ctx, ("You have entered", "an invalid recovery", "seed."))
|
||||
@ -272,16 +306,30 @@ class RecoveryHomescreen(ui.Component):
|
||||
|
||||
|
||||
async def homescreen_dialog(
|
||||
ctx: wire.Context, homepage: RecoveryHomescreen, button_label: str
|
||||
ctx: wire.Context,
|
||||
homepage: RecoveryHomescreen,
|
||||
button_label: str,
|
||||
info_func: Callable = None,
|
||||
) -> None:
|
||||
while True:
|
||||
continue_recovery = await confirm(
|
||||
ctx,
|
||||
homepage,
|
||||
code=ButtonRequestType.RecoveryHomepage,
|
||||
confirm=button_label,
|
||||
major_confirm=True,
|
||||
)
|
||||
if info_func:
|
||||
continue_recovery = await info_confirm(
|
||||
ctx,
|
||||
homepage,
|
||||
code=ButtonRequestType.RecoveryHomepage,
|
||||
confirm=button_label,
|
||||
info_func=info_func,
|
||||
info="Info",
|
||||
cancel="Abort",
|
||||
)
|
||||
else:
|
||||
continue_recovery = await confirm(
|
||||
ctx,
|
||||
homepage,
|
||||
code=ButtonRequestType.RecoveryHomepage,
|
||||
confirm=button_label,
|
||||
major_confirm=True,
|
||||
)
|
||||
if continue_recovery:
|
||||
# go forward in the recovery process
|
||||
break
|
||||
|
@ -1,72 +1,81 @@
|
||||
from trezor.crypto import bip39, slip39
|
||||
from trezor.errors import GroupThresholdReachedError, MnemonicError
|
||||
from trezor.errors import MnemonicError
|
||||
|
||||
from apps.common import storage
|
||||
|
||||
if False:
|
||||
from typing import Optional
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
class RecoveryAborted(Exception):
|
||||
pass
|
||||
|
||||
|
||||
_GROUP_STORAGE_OFFSET = 16
|
||||
|
||||
|
||||
def process_bip39(words: str) -> bytes:
|
||||
"""
|
||||
Receives single mnemonic and processes it. Returns what is then stored
|
||||
in the storage, which is the mnemonic itself for BIP-39.
|
||||
"""
|
||||
if not bip39.check(words):
|
||||
raise MnemonicError()
|
||||
raise MnemonicError
|
||||
return words.encode()
|
||||
|
||||
|
||||
def process_slip39(words: str) -> Optional[bytes, int, int]:
|
||||
def process_slip39(words: str) -> Tuple[Optional[bytes], slip39.Share]:
|
||||
"""
|
||||
Receives single mnemonic and processes it. Returns what is then stored in storage or
|
||||
None if more shares are needed.
|
||||
Processes a single mnemonic share. Returns the encrypted master secret
|
||||
(or None if more shares are needed) and the share's group index and member index.
|
||||
"""
|
||||
identifier, iteration_exponent, group_index, group_threshold, group_count, index, threshold, value = slip39.decode_mnemonic(
|
||||
words
|
||||
) # TODO: use better data structure for this
|
||||
share = slip39.decode_mnemonic(words)
|
||||
|
||||
remaining = storage.recovery.fetch_slip39_remaining_shares()
|
||||
index_with_group_offset = index + group_index * _GROUP_STORAGE_OFFSET
|
||||
# TODO: move this whole logic to storage
|
||||
index_with_group_offset = share.index + share.group_index * slip39.MAX_SHARE_COUNT
|
||||
|
||||
# if this is the first share, parse and store metadata
|
||||
if not remaining:
|
||||
storage.recovery.set_slip39_group_count(group_count)
|
||||
storage.recovery.set_slip39_group_threshold(group_threshold)
|
||||
storage.recovery.set_slip39_iteration_exponent(iteration_exponent)
|
||||
storage.recovery.set_slip39_identifier(identifier)
|
||||
storage.recovery.set_slip39_threshold(threshold)
|
||||
storage.recovery.set_slip39_remaining_shares(threshold - 1, group_index)
|
||||
storage.recovery.set_slip39_group_count(share.group_count)
|
||||
storage.recovery.set_slip39_group_threshold(share.group_threshold)
|
||||
storage.recovery.set_slip39_iteration_exponent(share.iteration_exponent)
|
||||
storage.recovery.set_slip39_identifier(share.identifier)
|
||||
storage.recovery.set_slip39_threshold(share.threshold)
|
||||
storage.recovery.set_slip39_remaining_shares(
|
||||
share.threshold - 1, share.group_index
|
||||
)
|
||||
storage.recovery_shares.set(index_with_group_offset, words)
|
||||
|
||||
return None, group_index, index # we need more shares
|
||||
# if share threshold and group threshold are 1
|
||||
# we can calculate the secret right away
|
||||
if share.threshold == 1 and share.group_threshold == 1:
|
||||
identifier, iteration_exponent, secret, _ = slip39.combine_mnemonics(
|
||||
[words]
|
||||
)
|
||||
return secret, share
|
||||
else:
|
||||
# we need more shares
|
||||
return None, share
|
||||
|
||||
if remaining[group_index] == 0:
|
||||
raise GroupThresholdReachedError()
|
||||
# These should be checked by UI before so it's a Runtime exception otherwise
|
||||
if identifier != storage.recovery.get_slip39_identifier():
|
||||
if share.identifier != storage.recovery.get_slip39_identifier():
|
||||
raise RuntimeError("Slip39: Share identifiers do not match")
|
||||
if storage.recovery_shares.get(index_with_group_offset):
|
||||
raise RuntimeError("Slip39: This mnemonic was already entered")
|
||||
|
||||
remaining_for_share = (
|
||||
storage.recovery.get_slip39_remaining_shares(group_index) or threshold
|
||||
storage.recovery.get_slip39_remaining_shares(share.group_index)
|
||||
or share.threshold
|
||||
)
|
||||
storage.recovery.set_slip39_remaining_shares(remaining_for_share - 1, group_index)
|
||||
remaining[group_index] = remaining_for_share - 1
|
||||
storage.recovery.set_slip39_remaining_shares(
|
||||
remaining_for_share - 1, share.group_index
|
||||
)
|
||||
remaining[share.group_index] = remaining_for_share - 1
|
||||
storage.recovery_shares.set(index_with_group_offset, words)
|
||||
|
||||
if remaining.count(0) < group_threshold:
|
||||
return None, group_index, index # we need more shares
|
||||
if remaining.count(0) < share.group_threshold:
|
||||
# we need more shares
|
||||
return None, share
|
||||
|
||||
if len(remaining) > 1:
|
||||
if share.group_count > 1:
|
||||
mnemonics = []
|
||||
for i, r in enumerate(remaining):
|
||||
# if we have multiple groups pass only the ones with threshold reached
|
||||
@ -74,7 +83,8 @@ def process_slip39(words: str) -> Optional[bytes, int, int]:
|
||||
group = storage.recovery_shares.fetch_group(i)
|
||||
mnemonics.extend(group)
|
||||
else:
|
||||
mnemonics = storage.recovery_shares.fetch()
|
||||
# in case of slip39 basic we only need the first and only group
|
||||
mnemonics = storage.recovery_shares.fetch_group(0)
|
||||
|
||||
identifier, iteration_exponent, secret = slip39.combine_mnemonics(mnemonics)
|
||||
return secret, group_index, index
|
||||
identifier, iteration_exponent, secret, _ = slip39.combine_mnemonics(mnemonics)
|
||||
return secret, share
|
||||
|
@ -1,17 +1,15 @@
|
||||
from trezor import config, ui, wire
|
||||
from trezor import config, wire
|
||||
from trezor.crypto import bip39, hashlib, random, slip39
|
||||
from trezor.messages import ButtonRequestType, ResetDeviceBackupType
|
||||
from trezor.messages import BackupType
|
||||
from trezor.messages.EntropyAck import EntropyAck
|
||||
from trezor.messages.EntropyRequest import EntropyRequest
|
||||
from trezor.messages.Success import Success
|
||||
from trezor.pin import pin_to_int
|
||||
from trezor.ui.loader import LoadingAnimation
|
||||
from trezor.ui.text import Text
|
||||
|
||||
from apps.common import mnemonic, storage
|
||||
from apps.common.confirm import require_confirm
|
||||
from apps.common.request_pin import request_pin_confirm
|
||||
from apps.management.common import layout
|
||||
from apps.common import storage
|
||||
from apps.management import backup_types
|
||||
from apps.management.change_pin import request_pin_confirm
|
||||
from apps.management.reset_device import layout
|
||||
|
||||
if __debug__:
|
||||
from apps import debug
|
||||
@ -19,18 +17,15 @@ if __debug__:
|
||||
if False:
|
||||
from trezor.messages.ResetDevice import ResetDevice
|
||||
|
||||
_DEFAULT_BACKUP_TYPE = ResetDeviceBackupType.Bip39
|
||||
_DEFAULT_BACKUP_TYPE = BackupType.Bip39
|
||||
|
||||
|
||||
async def reset_device(ctx: wire.Context, msg: ResetDevice) -> Success:
|
||||
# validate parameters and device state
|
||||
_validate_reset_device(msg)
|
||||
|
||||
is_slip39_simple = msg.backup_type == ResetDeviceBackupType.Slip39_Single_Group
|
||||
is_slip39_group = msg.backup_type == ResetDeviceBackupType.Slip39_Multiple_Groups
|
||||
|
||||
# make sure user knows he's setting up a new wallet
|
||||
await _show_reset_device_warning(ctx, msg.backup_type)
|
||||
# make sure user knows they're setting up a new wallet
|
||||
await layout.show_reset_device_warning(ctx, msg.backup_type)
|
||||
|
||||
# request new PIN
|
||||
if msg.pin_protection:
|
||||
@ -51,24 +46,21 @@ async def reset_device(ctx: wire.Context, msg: ResetDevice) -> Success:
|
||||
# For SLIP-39 this is the Encrypted Master Secret
|
||||
secret = _compute_secret_from_entropy(int_entropy, ext_entropy, msg.strength)
|
||||
|
||||
if is_slip39_simple or is_slip39_group:
|
||||
if msg.backup_type != BackupType.Bip39:
|
||||
storage.device.set_slip39_identifier(slip39.generate_random_identifier())
|
||||
storage.device.set_slip39_iteration_exponent(slip39.DEFAULT_ITERATION_EXPONENT)
|
||||
|
||||
# should we back up the wallet now?
|
||||
if not msg.no_backup and not msg.skip_backup:
|
||||
if not await layout.confirm_backup(ctx):
|
||||
if not await layout.confirm_backup_again(ctx):
|
||||
msg.skip_backup = True
|
||||
# If either of skip_backup or no_backup is specified, we are not doing backup now.
|
||||
# Otherwise, we try to do it.
|
||||
perform_backup = not msg.no_backup and not msg.skip_backup
|
||||
|
||||
# If doing backup, ask the user to confirm.
|
||||
if perform_backup:
|
||||
perform_backup = await layout.confirm_backup(ctx)
|
||||
|
||||
# generate and display backup information for the master secret
|
||||
if not msg.no_backup and not msg.skip_backup:
|
||||
if is_slip39_simple:
|
||||
await backup_slip39_wallet(ctx, secret)
|
||||
elif is_slip39_group:
|
||||
await backup_group_slip39_wallet(ctx, secret)
|
||||
else:
|
||||
await backup_bip39_wallet(ctx, secret)
|
||||
if perform_backup:
|
||||
await backup_seed(ctx, msg.backup_type, secret)
|
||||
|
||||
# write PIN into storage
|
||||
if not config.change_pin(pin_to_int(""), pin_to_int(newpin), None, None):
|
||||
@ -78,69 +70,70 @@ async def reset_device(ctx: wire.Context, msg: ResetDevice) -> Success:
|
||||
storage.device.load_settings(
|
||||
label=msg.label, use_passphrase=msg.passphrase_protection
|
||||
)
|
||||
if is_slip39_simple or is_slip39_group:
|
||||
storage.device.store_mnemonic_secret(
|
||||
secret, # this is the EMS in SLIP-39 terminology
|
||||
msg.backup_type,
|
||||
needs_backup=msg.skip_backup,
|
||||
no_backup=msg.no_backup,
|
||||
)
|
||||
else:
|
||||
if msg.backup_type == BackupType.Bip39:
|
||||
# in BIP-39 we store mnemonic string instead of the secret
|
||||
storage.device.store_mnemonic_secret(
|
||||
bip39.from_data(secret).encode(),
|
||||
mnemonic.TYPE_BIP39,
|
||||
needs_backup=msg.skip_backup,
|
||||
no_backup=msg.no_backup,
|
||||
)
|
||||
secret = bip39.from_data(secret).encode()
|
||||
elif msg.backup_type not in (BackupType.Slip39_Basic, BackupType.Slip39_Advanced):
|
||||
# Unknown backup type.
|
||||
# This check might seem superfluous, because we are checking
|
||||
# in `_validate_reset_device` already, however, this is critical part,
|
||||
# so just to make sure.
|
||||
raise RuntimeError
|
||||
|
||||
storage.device.store_mnemonic_secret(
|
||||
secret, # for SLIP-39, this is the EMS
|
||||
msg.backup_type,
|
||||
needs_backup=not perform_backup,
|
||||
no_backup=msg.no_backup,
|
||||
)
|
||||
|
||||
# if we backed up the wallet, show success message
|
||||
if not msg.no_backup and not msg.skip_backup:
|
||||
if perform_backup:
|
||||
await layout.show_backup_success(ctx)
|
||||
|
||||
return Success(message="Initialized")
|
||||
|
||||
|
||||
async def backup_slip39_wallet(
|
||||
async def backup_slip39_basic(
|
||||
ctx: wire.Context, encrypted_master_secret: bytes
|
||||
) -> None:
|
||||
# get number of shares
|
||||
await layout.slip39_show_checklist_set_shares(ctx)
|
||||
await layout.slip39_show_checklist(ctx, 0, BackupType.Slip39_Basic)
|
||||
shares_count = await layout.slip39_prompt_number_of_shares(ctx)
|
||||
|
||||
# get threshold
|
||||
await layout.slip39_show_checklist_set_threshold(ctx, shares_count)
|
||||
await layout.slip39_show_checklist(ctx, 1, BackupType.Slip39_Basic)
|
||||
threshold = await layout.slip39_prompt_threshold(ctx, shares_count)
|
||||
|
||||
# generate the mnemonics
|
||||
mnemonics = slip39.generate_single_group_mnemonics_from_data(
|
||||
mnemonics = slip39.generate_mnemonics_from_data(
|
||||
encrypted_master_secret,
|
||||
storage.device.get_slip39_identifier(),
|
||||
threshold,
|
||||
shares_count,
|
||||
1, # Single Group threshold
|
||||
[(threshold, shares_count)], # Single Group threshold/count
|
||||
storage.device.get_slip39_iteration_exponent(),
|
||||
)
|
||||
)[0]
|
||||
|
||||
# show and confirm individual shares
|
||||
await layout.slip39_show_checklist_show_shares(ctx, shares_count, threshold)
|
||||
await layout.slip39_show_and_confirm_shares(ctx, mnemonics)
|
||||
await layout.slip39_show_checklist(ctx, 2, BackupType.Slip39_Basic)
|
||||
await layout.slip39_basic_show_and_confirm_shares(ctx, mnemonics)
|
||||
|
||||
|
||||
async def backup_group_slip39_wallet(
|
||||
async def backup_slip39_advanced(
|
||||
ctx: wire.Context, encrypted_master_secret: bytes
|
||||
) -> None:
|
||||
# get number of groups
|
||||
await layout.slip39_group_show_checklist_set_groups(ctx)
|
||||
groups_count = await layout.slip39_prompt_number_of_groups(ctx)
|
||||
await layout.slip39_show_checklist(ctx, 0, BackupType.Slip39_Advanced)
|
||||
groups_count = await layout.slip39_advanced_prompt_number_of_groups(ctx)
|
||||
|
||||
# get group threshold
|
||||
await layout.slip39_group_show_checklist_set_group_threshold(ctx, groups_count)
|
||||
group_threshold = await layout.slip39_prompt_group_threshold(ctx, groups_count)
|
||||
await layout.slip39_show_checklist(ctx, 1, BackupType.Slip39_Advanced)
|
||||
group_threshold = await layout.slip39_advanced_prompt_group_threshold(
|
||||
ctx, groups_count
|
||||
)
|
||||
|
||||
# get shares and thresholds
|
||||
await layout.slip39_group_show_checklist_set_shares(
|
||||
ctx, groups_count, group_threshold
|
||||
)
|
||||
await layout.slip39_show_checklist(ctx, 2, BackupType.Slip39_Advanced)
|
||||
groups = []
|
||||
for i in range(groups_count):
|
||||
share_count = await layout.slip39_prompt_number_of_shares(ctx, i)
|
||||
@ -157,10 +150,10 @@ async def backup_group_slip39_wallet(
|
||||
)
|
||||
|
||||
# show and confirm individual shares
|
||||
await layout.slip39_group_show_and_confirm_shares(ctx, mnemonics)
|
||||
await layout.slip39_advanced_show_and_confirm_shares(ctx, mnemonics)
|
||||
|
||||
|
||||
async def backup_bip39_wallet(ctx: wire.Context, secret: bytes) -> None:
|
||||
async def backup_bip39(ctx: wire.Context, secret: bytes) -> None:
|
||||
mnemonic = bip39.from_data(secret)
|
||||
await layout.bip39_show_and_confirm_mnemonic(ctx, mnemonic)
|
||||
|
||||
@ -168,15 +161,16 @@ async def backup_bip39_wallet(ctx: wire.Context, secret: bytes) -> None:
|
||||
def _validate_reset_device(msg: ResetDevice) -> None:
|
||||
msg.backup_type = msg.backup_type or _DEFAULT_BACKUP_TYPE
|
||||
if msg.backup_type not in (
|
||||
ResetDeviceBackupType.Bip39,
|
||||
ResetDeviceBackupType.Slip39_Single_Group,
|
||||
ResetDeviceBackupType.Slip39_Multiple_Groups,
|
||||
BackupType.Bip39,
|
||||
BackupType.Slip39_Basic,
|
||||
BackupType.Slip39_Advanced,
|
||||
):
|
||||
raise wire.ProcessError("Backup type not implemented.")
|
||||
if msg.strength not in (128, 256):
|
||||
if msg.backup_type == ResetDeviceBackupType.Slip39_Single_Group:
|
||||
if backup_types.is_slip39_backup_type(msg.backup_type):
|
||||
if msg.strength not in (128, 256):
|
||||
raise wire.ProcessError("Invalid strength (has to be 128 or 256 bits)")
|
||||
elif msg.strength != 192:
|
||||
else: # BIP-39
|
||||
if msg.strength not in (128, 192, 256):
|
||||
raise wire.ProcessError("Invalid strength (has to be 128, 192 or 256 bits)")
|
||||
if msg.display_random and (msg.skip_backup or msg.no_backup):
|
||||
raise wire.ProcessError("Can't show internal entropy when backup is skipped")
|
||||
@ -198,27 +192,12 @@ def _compute_secret_from_entropy(
|
||||
return secret
|
||||
|
||||
|
||||
async def _show_reset_device_warning(
|
||||
ctx, backup_type: ResetDeviceBackupType = ResetDeviceBackupType.Bip39
|
||||
async def backup_seed(
|
||||
ctx: wire.Context, backup_type: BackupType, mnemonic_secret: bytes
|
||||
):
|
||||
text = Text("Create new wallet", ui.ICON_RESET, new_lines=False)
|
||||
if backup_type == ResetDeviceBackupType.Slip39_Single_Group:
|
||||
text.bold("Create a new wallet")
|
||||
text.br()
|
||||
text.bold("with Shamir Backup?")
|
||||
elif backup_type == ResetDeviceBackupType.Slip39_Multiple_Groups:
|
||||
text.bold("Create a new wallet")
|
||||
text.br()
|
||||
text.bold("with Super Shamir?")
|
||||
if backup_type == BackupType.Slip39_Basic:
|
||||
await backup_slip39_basic(ctx, mnemonic_secret)
|
||||
elif backup_type == BackupType.Slip39_Advanced:
|
||||
await backup_slip39_advanced(ctx, mnemonic_secret)
|
||||
else:
|
||||
text.bold("Do you want to create")
|
||||
text.br()
|
||||
text.bold("a new wallet?")
|
||||
text.br()
|
||||
text.br_half()
|
||||
text.normal("By continuing you agree")
|
||||
text.br()
|
||||
text.normal("to")
|
||||
text.bold("https://trezor.io/tos")
|
||||
await require_confirm(ctx, text, ButtonRequestType.ResetDevice, major_confirm=True)
|
||||
await LoadingAnimation()
|
||||
await backup_bip39(ctx, mnemonic_secret)
|
@ -1,14 +1,14 @@
|
||||
import ubinascii
|
||||
from micropython import const
|
||||
|
||||
from trezor import ui, utils
|
||||
from trezor.crypto import random
|
||||
from trezor.messages import ButtonRequestType
|
||||
from trezor.messages import BackupType, ButtonRequestType
|
||||
from trezor.ui.button import Button, ButtonDefault
|
||||
from trezor.ui.checklist import Checklist
|
||||
from trezor.ui.info import InfoConfirm
|
||||
from trezor.ui.loader import LoadingAnimation
|
||||
from trezor.ui.num_input import NumInput
|
||||
from trezor.ui.scroll import Paginated
|
||||
from trezor.ui.shamir import NumInput
|
||||
from trezor.ui.text import Text
|
||||
|
||||
from apps.common.confirm import confirm, hold_to_confirm, require_confirm
|
||||
@ -27,6 +27,7 @@ async def show_internal_entropy(ctx, entropy: bytes):
|
||||
|
||||
|
||||
async def confirm_backup(ctx):
|
||||
# First prompt
|
||||
text = Text("Success", ui.ICON_CONFIRM, ui.GREEN, new_lines=False)
|
||||
text.bold("New wallet created")
|
||||
text.br()
|
||||
@ -36,17 +37,17 @@ async def confirm_backup(ctx):
|
||||
text.normal("You should back up your")
|
||||
text.br()
|
||||
text.normal("new wallet right now.")
|
||||
return await confirm(
|
||||
if await confirm(
|
||||
ctx,
|
||||
text,
|
||||
ButtonRequestType.ResetDevice,
|
||||
cancel="Skip",
|
||||
confirm="Back up",
|
||||
major_confirm=True,
|
||||
)
|
||||
):
|
||||
return True
|
||||
|
||||
|
||||
async def confirm_backup_again(ctx):
|
||||
# If the user selects Skip, ask again
|
||||
text = Text("Warning", ui.ICON_WRONG, ui.RED, new_lines=False)
|
||||
text.bold("Are you sure you want")
|
||||
text.br()
|
||||
@ -66,15 +67,93 @@ async def confirm_backup_again(ctx):
|
||||
)
|
||||
|
||||
|
||||
async def _show_share_words(ctx, share_words, share_index=None, group_index=None):
|
||||
first, chunks, last = _split_share_into_pages(share_words)
|
||||
|
||||
if share_index is None:
|
||||
header_title = "Recovery seed"
|
||||
elif group_index is None:
|
||||
header_title = "Recovery share #%s" % (share_index + 1)
|
||||
else:
|
||||
header_title = "Group %s - Share %s" % ((group_index + 1), (share_index + 1))
|
||||
header_icon = ui.ICON_RESET
|
||||
pages = [] # ui page components
|
||||
shares_words_check = [] # check we display correct data
|
||||
|
||||
# first page
|
||||
text = Text(header_title, header_icon)
|
||||
text.bold("Write down these")
|
||||
text.bold("%s words:" % len(share_words))
|
||||
text.br_half()
|
||||
for index, word in first:
|
||||
text.mono("%s. %s" % (index + 1, word))
|
||||
shares_words_check.append(word)
|
||||
pages.append(text)
|
||||
|
||||
# middle pages
|
||||
for chunk in chunks:
|
||||
text = Text(header_title, header_icon)
|
||||
for index, word in chunk:
|
||||
text.mono("%s. %s" % (index + 1, word))
|
||||
shares_words_check.append(word)
|
||||
pages.append(text)
|
||||
|
||||
# last page
|
||||
text = Text(header_title, header_icon)
|
||||
for index, word in last:
|
||||
text.mono("%s. %s" % (index + 1, word))
|
||||
shares_words_check.append(word)
|
||||
text.br_half()
|
||||
text.bold("I wrote down all %s" % len(share_words))
|
||||
text.bold("words in order.")
|
||||
pages.append(text)
|
||||
|
||||
# pagination
|
||||
paginated = Paginated(pages)
|
||||
|
||||
if __debug__:
|
||||
|
||||
word_pages = [first] + chunks + [last]
|
||||
|
||||
def export_displayed_words():
|
||||
# export currently displayed mnemonic words into debuglink
|
||||
words = [w for _, w in word_pages[paginated.page]]
|
||||
debug.reset_current_words.publish(words)
|
||||
|
||||
paginated.on_change = export_displayed_words
|
||||
export_displayed_words()
|
||||
|
||||
# make sure we display correct data
|
||||
utils.ensure(share_words == shares_words_check)
|
||||
|
||||
# confirm the share
|
||||
await hold_to_confirm(ctx, paginated, ButtonRequestType.ResetDevice)
|
||||
|
||||
|
||||
def _split_share_into_pages(share_words):
|
||||
share = list(enumerate(share_words)) # we need to keep track of the word indices
|
||||
first = share[:2] # two words on the first page
|
||||
length = len(share_words)
|
||||
if length == 12 or length == 20 or length == 24:
|
||||
middle = share[2:-2]
|
||||
last = share[-2:] # two words on the last page
|
||||
elif length == 33:
|
||||
middle = share[2:]
|
||||
last = [] # no words at the last page, because it does not add up
|
||||
else:
|
||||
# Invalid number of shares. SLIP-39 allows 20 or 33 words, BIP-39 12 or 24
|
||||
raise RuntimeError
|
||||
|
||||
chunks = utils.chunks(middle, 4) # 4 words on the middle pages
|
||||
return first, list(chunks), last
|
||||
|
||||
|
||||
async def _confirm_share_words(ctx, share_index, share_words, group_index=None):
|
||||
numbered = list(enumerate(share_words))
|
||||
|
||||
# check three words
|
||||
third = len(numbered) // 3
|
||||
# if the num of words is not dividable by 3 let's add 1
|
||||
# to have more words at the beggining and to check all of them
|
||||
if len(numbered) % 3:
|
||||
third += 1
|
||||
# divide list into thirds, rounding up, so that chunking by `third` always yields
|
||||
# three parts (the last one might be shorter)
|
||||
third = (len(numbered) + 2) // 3
|
||||
|
||||
for part in utils.chunks(numbered, third):
|
||||
if not await _confirm_word(
|
||||
@ -88,8 +167,8 @@ async def _confirm_share_words(ctx, share_index, share_words, group_index=None):
|
||||
async def _confirm_word(
|
||||
ctx, share_index, numbered_share_words, count, group_index=None
|
||||
):
|
||||
# TODO: duplicated words in the choice list
|
||||
|
||||
# TODO: duplicated words in the choice list
|
||||
# shuffle the numbered seed half, slice off the choices we need
|
||||
random.shuffle(numbered_share_words)
|
||||
numbered_choices = numbered_share_words[: MnemonicWordSelect.NUM_OF_CHOICES]
|
||||
@ -115,22 +194,24 @@ async def _confirm_word(
|
||||
|
||||
|
||||
async def _show_confirmation_success(
|
||||
ctx, share_index, num_of_shares=None, slip39=False, group_index=None
|
||||
ctx, share_index=None, num_of_shares=None, group_index=None
|
||||
):
|
||||
if share_index is None or num_of_shares is None or share_index == num_of_shares - 1:
|
||||
if slip39:
|
||||
if group_index is None:
|
||||
subheader = ("You have finished", "verifying your", "recovery shares.")
|
||||
else:
|
||||
subheader = (
|
||||
"You have finished",
|
||||
"verifying your",
|
||||
"recovery shares",
|
||||
"for group %s." % (group_index + 1),
|
||||
)
|
||||
else:
|
||||
subheader = ("You have finished", "verifying your", "recovery seed.")
|
||||
if share_index is None: # it is a BIP39 backup
|
||||
subheader = ("You have finished", "verifying your", "recovery seed.")
|
||||
text = []
|
||||
|
||||
elif share_index == num_of_shares - 1:
|
||||
if group_index is None:
|
||||
subheader = ("You have finished", "verifying your", "recovery shares.")
|
||||
else:
|
||||
subheader = (
|
||||
"You have finished",
|
||||
"verifying your",
|
||||
"recovery shares",
|
||||
"for group %s." % (group_index + 1),
|
||||
)
|
||||
text = []
|
||||
|
||||
else:
|
||||
if group_index is None:
|
||||
subheader = (
|
||||
@ -198,247 +279,47 @@ async def bip39_show_and_confirm_mnemonic(ctx, mnemonic: str):
|
||||
|
||||
while True:
|
||||
# display paginated mnemonic on the screen
|
||||
await _bip39_show_mnemonic(ctx, words)
|
||||
await _show_share_words(ctx, share_words=words)
|
||||
|
||||
# make the user confirm 2 words from the mnemonic
|
||||
# make the user confirm some words from the mnemonic
|
||||
if await _confirm_share_words(ctx, None, words):
|
||||
await _show_confirmation_success(ctx, None)
|
||||
await _show_confirmation_success(ctx)
|
||||
break # this share is confirmed, go to next one
|
||||
else:
|
||||
await _show_confirmation_failure(ctx, None)
|
||||
|
||||
|
||||
async def _bip39_show_mnemonic(ctx, words: list):
|
||||
# split mnemonic words into pages
|
||||
PER_PAGE = const(4)
|
||||
words = list(enumerate(words))
|
||||
words = list(utils.chunks(words, PER_PAGE))
|
||||
|
||||
# display the pages, with a confirmation dialog on the last one
|
||||
pages = [_get_mnemonic_page(page) for page in words]
|
||||
paginated = Paginated(pages)
|
||||
|
||||
if __debug__:
|
||||
|
||||
def export_displayed_words():
|
||||
# export currently displayed mnemonic words into debuglink
|
||||
debug.reset_current_words.publish([w for _, w in words[paginated.page]])
|
||||
|
||||
paginated.on_change = export_displayed_words
|
||||
export_displayed_words()
|
||||
|
||||
await hold_to_confirm(ctx, paginated, ButtonRequestType.ResetDevice)
|
||||
|
||||
|
||||
def _get_mnemonic_page(words: list):
|
||||
text = Text("Recovery seed", ui.ICON_RESET)
|
||||
for index, word in words:
|
||||
text.mono("%2d. %s" % (index + 1, word))
|
||||
return text
|
||||
|
||||
|
||||
# SLIP39
|
||||
# ===
|
||||
|
||||
# TODO: yellow cancel style?
|
||||
# TODO: loading animation style?
|
||||
# TODO: smaller font or tighter rows to fit more text in
|
||||
# TODO: icons in checklist
|
||||
|
||||
# SLIP 39 simple
|
||||
|
||||
|
||||
async def slip39_show_checklist_set_shares(ctx):
|
||||
async def slip39_show_checklist(ctx, step: int, backup_type: BackupType) -> None:
|
||||
checklist = Checklist("Backup checklist", ui.ICON_RESET)
|
||||
checklist.add("Set number of shares")
|
||||
checklist.add("Set threshold")
|
||||
checklist.add(("Write down and check", "all recovery shares"))
|
||||
checklist.select(0)
|
||||
if backup_type is BackupType.Slip39_Basic:
|
||||
checklist.add("Set number of shares")
|
||||
checklist.add("Set threshold")
|
||||
checklist.add(("Write down and check", "all recovery shares"))
|
||||
elif backup_type is BackupType.Slip39_Advanced:
|
||||
checklist.add("Set number of groups")
|
||||
checklist.add("Set group threshold")
|
||||
checklist.add(("Set size and threshold", "for each group"))
|
||||
checklist.select(step)
|
||||
|
||||
return await confirm(
|
||||
ctx, checklist, ButtonRequestType.ResetDevice, cancel=None, confirm="Continue"
|
||||
)
|
||||
|
||||
|
||||
async def slip39_show_checklist_set_threshold(ctx, num_of_shares):
|
||||
checklist = Checklist("Backup checklist", ui.ICON_RESET)
|
||||
checklist.add("Set number of shares")
|
||||
checklist.add("Set threshold")
|
||||
checklist.add(("Write down and check", "all recovery shares"))
|
||||
checklist.select(1)
|
||||
return await confirm(
|
||||
ctx, checklist, ButtonRequestType.ResetDevice, cancel=None, confirm="Continue"
|
||||
)
|
||||
|
||||
|
||||
async def slip39_show_checklist_show_shares(ctx, num_of_shares, threshold):
|
||||
checklist = Checklist("Backup checklist", ui.ICON_RESET)
|
||||
checklist.add("Set number of shares")
|
||||
checklist.add("Set threshold")
|
||||
checklist.add(("Write down and check", "all recovery shares"))
|
||||
checklist.select(2)
|
||||
return await confirm(
|
||||
ctx, checklist, ButtonRequestType.ResetDevice, cancel=None, confirm="Continue"
|
||||
)
|
||||
|
||||
|
||||
# SLIP 39 group
|
||||
|
||||
|
||||
async def slip39_group_show_checklist_set_groups(ctx):
|
||||
checklist = Checklist("Backup checklist", ui.ICON_RESET)
|
||||
checklist.add("Set number of groups")
|
||||
checklist.add("Set group threshold")
|
||||
checklist.add(("Set size and threshold", "for each group"))
|
||||
checklist.select(0)
|
||||
return await confirm(
|
||||
ctx, checklist, ButtonRequestType.ResetDevice, cancel=None, confirm="Continue"
|
||||
)
|
||||
|
||||
|
||||
async def slip39_group_show_checklist_set_group_threshold(ctx, num_of_shares):
|
||||
checklist = Checklist("Backup checklist", ui.ICON_RESET)
|
||||
checklist.add("Set number of groups")
|
||||
checklist.add("Set group threshold")
|
||||
checklist.add(("Set size and threshold", "for each group"))
|
||||
checklist.select(1)
|
||||
return await confirm(
|
||||
ctx, checklist, ButtonRequestType.ResetDevice, cancel=None, confirm="Continue"
|
||||
)
|
||||
|
||||
|
||||
async def slip39_group_show_checklist_set_shares(ctx, num_of_shares, group_threshold):
|
||||
checklist = Checklist("Backup checklist", ui.ICON_RESET)
|
||||
checklist.add("Set number of groups")
|
||||
checklist.add("Set group threshold")
|
||||
checklist.add(("Set size and threshold", "for each group"))
|
||||
checklist.select(2)
|
||||
return await confirm(
|
||||
ctx, checklist, ButtonRequestType.ResetDevice, cancel=None, confirm="Continue"
|
||||
)
|
||||
|
||||
|
||||
async def slip39_prompt_number_of_shares(ctx, group_id=None):
|
||||
count = 5
|
||||
if group_id is not None:
|
||||
min_count = 1
|
||||
else:
|
||||
min_count = 2
|
||||
max_count = 16
|
||||
|
||||
while True:
|
||||
shares = ShamirNumInput(
|
||||
ShamirNumInput.SET_SHARES, 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(
|
||||
"Each recovery share is a "
|
||||
"sequence of 20 words. "
|
||||
"Next you will choose "
|
||||
"how many shares you "
|
||||
"need to recover your "
|
||||
"wallet."
|
||||
)
|
||||
else:
|
||||
info = InfoConfirm(
|
||||
"Each recovery share is a "
|
||||
"sequence of 20 words. "
|
||||
"Next you will choose "
|
||||
"the threshold number of "
|
||||
"shares needed to form "
|
||||
"Group %s." % (group_id + 1)
|
||||
)
|
||||
await info
|
||||
|
||||
return count
|
||||
|
||||
|
||||
async def slip39_prompt_number_of_groups(ctx):
|
||||
count = 5
|
||||
min_count = 2
|
||||
max_count = 16
|
||||
|
||||
while True:
|
||||
shares = ShamirNumInput(ShamirNumInput.SET_GROUPS, count, min_count, max_count)
|
||||
confirmed = await confirm(
|
||||
ctx,
|
||||
shares,
|
||||
ButtonRequestType.ResetDevice,
|
||||
cancel="Info",
|
||||
confirm="Continue",
|
||||
major_confirm=True,
|
||||
cancel_style=ButtonDefault,
|
||||
)
|
||||
count = shares.input.count
|
||||
if confirmed:
|
||||
break
|
||||
|
||||
info = InfoConfirm(
|
||||
"Each group has a set "
|
||||
"number of shares and "
|
||||
"its own threshold. In the "
|
||||
"next steps you will set "
|
||||
"the numbers of shares "
|
||||
"and the thresholds."
|
||||
)
|
||||
await info
|
||||
|
||||
return count
|
||||
|
||||
|
||||
async def slip39_prompt_group_threshold(ctx, num_of_groups):
|
||||
count = num_of_groups // 2 + 1
|
||||
min_count = 1
|
||||
max_count = num_of_groups
|
||||
|
||||
while True:
|
||||
shares = ShamirNumInput(
|
||||
ShamirNumInput.SET_GROUP_THRESHOLD, count, min_count, max_count
|
||||
)
|
||||
confirmed = await confirm(
|
||||
ctx,
|
||||
shares,
|
||||
ButtonRequestType.ResetDevice,
|
||||
cancel="Info",
|
||||
confirm="Continue",
|
||||
major_confirm=True,
|
||||
cancel_style=ButtonDefault,
|
||||
)
|
||||
count = shares.input.count
|
||||
if confirmed:
|
||||
break
|
||||
else:
|
||||
info = InfoConfirm(
|
||||
"The group threshold "
|
||||
"specifies the number of "
|
||||
"groups required to "
|
||||
"recover your wallet. "
|
||||
)
|
||||
await info
|
||||
|
||||
return count
|
||||
|
||||
|
||||
async def slip39_prompt_threshold(ctx, num_of_shares, group_id=None):
|
||||
count = num_of_shares // 2 + 1
|
||||
# min value of share threshold is 2 unless the number of shares is 1
|
||||
# number of shares 1 is possible in advnaced slip39
|
||||
min_count = min(2, num_of_shares)
|
||||
max_count = num_of_shares
|
||||
|
||||
while True:
|
||||
shares = ShamirNumInput(
|
||||
ShamirNumInput.SET_THRESHOLD, count, min_count, max_count, group_id
|
||||
shares = Slip39NumInput(
|
||||
Slip39NumInput.SET_THRESHOLD, count, min_count, max_count, group_id
|
||||
)
|
||||
confirmed = await confirm(
|
||||
ctx,
|
||||
@ -476,7 +357,55 @@ async def slip39_prompt_threshold(ctx, num_of_shares, group_id=None):
|
||||
return count
|
||||
|
||||
|
||||
async def slip39_show_and_confirm_shares(ctx, shares):
|
||||
async def slip39_prompt_number_of_shares(ctx, group_id=None):
|
||||
count = 5
|
||||
if group_id is not None:
|
||||
min_count = 1
|
||||
else:
|
||||
min_count = 2
|
||||
max_count = 16
|
||||
|
||||
while True:
|
||||
shares = Slip39NumInput(
|
||||
Slip39NumInput.SET_SHARES, 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(
|
||||
"Each recovery share is a "
|
||||
"sequence of 20 words. "
|
||||
"Next you will choose "
|
||||
"how many shares you "
|
||||
"need to recover your "
|
||||
"wallet."
|
||||
)
|
||||
else:
|
||||
info = InfoConfirm(
|
||||
"Each recovery share is a "
|
||||
"sequence of 20 words. "
|
||||
"Next you will choose "
|
||||
"the threshold number of "
|
||||
"shares needed to form "
|
||||
"Group %s." % (group_id + 1)
|
||||
)
|
||||
await info
|
||||
|
||||
return count
|
||||
|
||||
|
||||
async def slip39_basic_show_and_confirm_shares(ctx, shares):
|
||||
# warn user about mnemonic safety
|
||||
await show_backup_warning(ctx, slip39=True)
|
||||
|
||||
@ -484,19 +413,85 @@ async def slip39_show_and_confirm_shares(ctx, shares):
|
||||
share_words = share.split(" ")
|
||||
while True:
|
||||
# display paginated share on the screen
|
||||
await _slip39_show_share_words(ctx, index, share_words)
|
||||
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, index, num_of_shares=len(shares), slip39=True
|
||||
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_group_show_and_confirm_shares(ctx, shares):
|
||||
async def slip39_advanced_prompt_number_of_groups(ctx):
|
||||
count = 5
|
||||
min_count = 2
|
||||
max_count = 16
|
||||
|
||||
while True:
|
||||
shares = Slip39NumInput(Slip39NumInput.SET_GROUPS, count, min_count, max_count)
|
||||
confirmed = await confirm(
|
||||
ctx,
|
||||
shares,
|
||||
ButtonRequestType.ResetDevice,
|
||||
cancel="Info",
|
||||
confirm="Continue",
|
||||
major_confirm=True,
|
||||
cancel_style=ButtonDefault,
|
||||
)
|
||||
count = shares.input.count
|
||||
if confirmed:
|
||||
break
|
||||
|
||||
info = InfoConfirm(
|
||||
"Each group has a set "
|
||||
"number of shares and "
|
||||
"its own threshold. In the "
|
||||
"next steps you will set "
|
||||
"the numbers of shares "
|
||||
"and the thresholds."
|
||||
)
|
||||
await info
|
||||
|
||||
return count
|
||||
|
||||
|
||||
async def slip39_advanced_prompt_group_threshold(ctx, num_of_groups):
|
||||
count = num_of_groups // 2 + 1
|
||||
min_count = 1
|
||||
max_count = num_of_groups
|
||||
|
||||
while True:
|
||||
shares = Slip39NumInput(
|
||||
Slip39NumInput.SET_GROUP_THRESHOLD, count, min_count, max_count
|
||||
)
|
||||
confirmed = await confirm(
|
||||
ctx,
|
||||
shares,
|
||||
ButtonRequestType.ResetDevice,
|
||||
cancel="Info",
|
||||
confirm="Continue",
|
||||
major_confirm=True,
|
||||
cancel_style=ButtonDefault,
|
||||
)
|
||||
count = shares.input.count
|
||||
if confirmed:
|
||||
break
|
||||
else:
|
||||
info = InfoConfirm(
|
||||
"The group threshold "
|
||||
"specifies the number of "
|
||||
"groups required to "
|
||||
"recover your wallet. "
|
||||
)
|
||||
await info
|
||||
|
||||
return count
|
||||
|
||||
|
||||
async def slip39_advanced_show_and_confirm_shares(ctx, shares):
|
||||
# warn user about mnemonic safety
|
||||
await show_backup_warning(ctx, slip39=True)
|
||||
|
||||
@ -505,9 +500,7 @@ async def slip39_group_show_and_confirm_shares(ctx, shares):
|
||||
share_words = share.split(" ")
|
||||
while True:
|
||||
# display paginated share on the screen
|
||||
await _slip39_show_share_words(
|
||||
ctx, share_index, share_words, group_index
|
||||
)
|
||||
await _show_share_words(ctx, share_words, share_index, group_index)
|
||||
|
||||
# make the user confirm words from the share
|
||||
if await _confirm_share_words(
|
||||
@ -515,9 +508,8 @@ async def slip39_group_show_and_confirm_shares(ctx, shares):
|
||||
):
|
||||
await _show_confirmation_success(
|
||||
ctx,
|
||||
share_index,
|
||||
share_index=share_index,
|
||||
num_of_shares=len(shares),
|
||||
slip39=True,
|
||||
group_index=group_index,
|
||||
)
|
||||
break # this share is confirmed, go to next one
|
||||
@ -525,88 +517,7 @@ async def slip39_group_show_and_confirm_shares(ctx, shares):
|
||||
await _show_confirmation_failure(ctx, share_index)
|
||||
|
||||
|
||||
async def _slip39_show_share_words(ctx, share_index, share_words, group_index=None):
|
||||
first, chunks, last = _slip39_split_share_into_pages(share_words)
|
||||
|
||||
if share_index is None:
|
||||
header_title = "Recovery seed"
|
||||
elif group_index is None:
|
||||
header_title = "Recovery share #%s" % (share_index + 1)
|
||||
else:
|
||||
header_title = "Group %s - Share %s" % ((group_index + 1), (share_index + 1))
|
||||
header_icon = ui.ICON_RESET
|
||||
pages = [] # ui page components
|
||||
shares_words_check = [] # check we display correct data
|
||||
|
||||
# first page
|
||||
text = Text(header_title, header_icon)
|
||||
text.bold("Write down these")
|
||||
text.bold("%s words:" % len(share_words))
|
||||
text.br_half()
|
||||
for index, word in first:
|
||||
text.mono("%s. %s" % (index + 1, word))
|
||||
shares_words_check.append(word)
|
||||
pages.append(text)
|
||||
|
||||
# middle pages
|
||||
for chunk in chunks:
|
||||
text = Text(header_title, header_icon)
|
||||
for index, word in chunk:
|
||||
text.mono("%s. %s" % (index + 1, word))
|
||||
shares_words_check.append(word)
|
||||
pages.append(text)
|
||||
|
||||
# last page
|
||||
text = Text(header_title, header_icon)
|
||||
for index, word in last:
|
||||
text.mono("%s. %s" % (index + 1, word))
|
||||
shares_words_check.append(word)
|
||||
text.br_half()
|
||||
text.bold("I wrote down all %s" % len(share_words))
|
||||
text.bold("words in order.")
|
||||
pages.append(text)
|
||||
|
||||
# pagination
|
||||
paginated = Paginated(pages)
|
||||
|
||||
if __debug__:
|
||||
|
||||
word_pages = [first] + chunks + [last]
|
||||
|
||||
def export_displayed_words():
|
||||
# export currently displayed mnemonic words into debuglink
|
||||
words = [w for _, w in word_pages[paginated.page]]
|
||||
debug.reset_current_words.publish(words)
|
||||
|
||||
paginated.on_change = export_displayed_words
|
||||
export_displayed_words()
|
||||
|
||||
# make sure we display correct data
|
||||
utils.ensure(share_words == shares_words_check)
|
||||
|
||||
# confirm the share
|
||||
await hold_to_confirm(ctx, paginated) # TODO: customize the loader here
|
||||
|
||||
|
||||
def _slip39_split_share_into_pages(share_words):
|
||||
share = list(enumerate(share_words)) # we need to keep track of the word indices
|
||||
first = share[:2] # two words on the first page
|
||||
length = len(share_words)
|
||||
if length == 20:
|
||||
middle = share[2:-2]
|
||||
last = share[-2:] # two words on the last page
|
||||
elif length == 33:
|
||||
middle = share[2:]
|
||||
last = [] # no words at the last page, because it does not add up
|
||||
else:
|
||||
# Invalid number of shares. SLIP-39 allows 20 or 33 words.
|
||||
raise RuntimeError
|
||||
|
||||
chunks = utils.chunks(middle, 4) # 4 words on the middle pages
|
||||
return first, list(chunks), last
|
||||
|
||||
|
||||
class ShamirNumInput(ui.Component):
|
||||
class Slip39NumInput(ui.Component):
|
||||
SET_SHARES = object()
|
||||
SET_THRESHOLD = object()
|
||||
SET_GROUPS = object()
|
||||
@ -629,18 +540,18 @@ class ShamirNumInput(ui.Component):
|
||||
count = self.input.count
|
||||
|
||||
# render the headline
|
||||
if self.step is ShamirNumInput.SET_SHARES:
|
||||
if self.step is Slip39NumInput.SET_SHARES:
|
||||
header = "Set num. of shares"
|
||||
elif self.step is ShamirNumInput.SET_THRESHOLD:
|
||||
elif self.step is Slip39NumInput.SET_THRESHOLD:
|
||||
header = "Set threshold"
|
||||
elif self.step is ShamirNumInput.SET_GROUPS:
|
||||
elif self.step is Slip39NumInput.SET_GROUPS:
|
||||
header = "Set num. of groups"
|
||||
elif self.step is ShamirNumInput.SET_GROUP_THRESHOLD:
|
||||
elif self.step is Slip39NumInput.SET_GROUP_THRESHOLD:
|
||||
header = "Set group threshold"
|
||||
ui.header(header, ui.ICON_RESET, ui.TITLE_GREY, ui.BG, ui.ORANGE_ICON)
|
||||
|
||||
# render the counter
|
||||
if self.step is ShamirNumInput.SET_SHARES:
|
||||
if self.step is Slip39NumInput.SET_SHARES:
|
||||
if self.group_id is None:
|
||||
first_line_text = "%s people or locations" % count
|
||||
second_line_text = "will each hold one share."
|
||||
@ -651,7 +562,7 @@ class ShamirNumInput(ui.Component):
|
||||
12, 130, first_line_text, ui.NORMAL, ui.FG, ui.BG, ui.WIDTH - 12
|
||||
)
|
||||
ui.display.text(12, 156, second_line_text, ui.NORMAL, ui.FG, ui.BG)
|
||||
elif self.step is ShamirNumInput.SET_THRESHOLD:
|
||||
elif self.step is Slip39NumInput.SET_THRESHOLD:
|
||||
if self.group_id is None:
|
||||
first_line_text = "For recovery you need"
|
||||
second_line_text = "any %s of the shares." % count
|
||||
@ -662,14 +573,14 @@ class ShamirNumInput(ui.Component):
|
||||
ui.display.text(
|
||||
12, 156, second_line_text, ui.NORMAL, ui.FG, ui.BG, ui.WIDTH - 12
|
||||
)
|
||||
elif self.step is ShamirNumInput.SET_GROUPS:
|
||||
elif self.step is Slip39NumInput.SET_GROUPS:
|
||||
ui.display.text(
|
||||
12, 130, "A group is made up of", ui.NORMAL, ui.FG, ui.BG
|
||||
)
|
||||
ui.display.text(
|
||||
12, 156, "recovery shares.", ui.NORMAL, ui.FG, ui.BG, ui.WIDTH - 12
|
||||
)
|
||||
elif self.step is ShamirNumInput.SET_GROUP_THRESHOLD:
|
||||
elif self.step is Slip39NumInput.SET_GROUP_THRESHOLD:
|
||||
ui.display.text(
|
||||
12, 130, "The required number of", ui.NORMAL, ui.FG, ui.BG
|
||||
)
|
||||
@ -722,3 +633,27 @@ class MnemonicWordSelect(ui.Layout):
|
||||
raise ui.Result(word)
|
||||
|
||||
return fn
|
||||
|
||||
|
||||
async def show_reset_device_warning(ctx, backup_type: BackupType = BackupType.Bip39):
|
||||
text = Text("Create new wallet", ui.ICON_RESET, new_lines=False)
|
||||
if backup_type == BackupType.Slip39_Basic:
|
||||
text.bold("Create a new wallet")
|
||||
text.br()
|
||||
text.bold("with Shamir Backup?")
|
||||
elif backup_type == BackupType.Slip39_Advanced:
|
||||
text.bold("Create a new wallet")
|
||||
text.br()
|
||||
text.bold("with Super Shamir?")
|
||||
else:
|
||||
text.bold("Do you want to create")
|
||||
text.br()
|
||||
text.bold("a new wallet?")
|
||||
text.br()
|
||||
text.br_half()
|
||||
text.normal("By continuing you agree")
|
||||
text.br()
|
||||
text.normal("to")
|
||||
text.bold("https://trezor.io/tos")
|
||||
await require_confirm(ctx, text, ButtonRequestType.ResetDevice, major_confirm=True)
|
||||
await LoadingAnimation()
|
@ -18,6 +18,18 @@
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
This implements the high-level functions for SLIP-39, also called "Shamir Backup".
|
||||
It uses crypto/shamir.c for the cryptographic operations and crypto.slip39.c for
|
||||
performance-heavy operations (mostly regarding the wordlist).
|
||||
|
||||
This consideres the Encrypted Master Secret, as defined in SLIP-39, as what is
|
||||
stored in the storage, then "decrypted" using a passphrase into a Master Secret,
|
||||
which is then fed into BIP-32 for example.
|
||||
|
||||
See https://github.com/satoshilabs/slips/blob/master/slip-0039.md.
|
||||
"""
|
||||
|
||||
from micropython import const
|
||||
|
||||
from trezor.crypto import hashlib, hmac, pbkdf2, random
|
||||
@ -28,42 +40,31 @@ if False:
|
||||
from typing import Dict, Iterable, List, Optional, Set, Tuple
|
||||
|
||||
Indices = Tuple[int, ...]
|
||||
MnemonicGroups = Dict[int, Tuple[int, Set[Tuple[int, bytes]]]]
|
||||
|
||||
KEYBOARD_FULL_MASK = const(0x1FF)
|
||||
"""All buttons are allowed. 9-bit bitmap all set to 1."""
|
||||
|
||||
|
||||
def compute_mask(prefix: str) -> int:
|
||||
if not prefix:
|
||||
return KEYBOARD_FULL_MASK
|
||||
return slip39.compute_mask(int(prefix))
|
||||
|
||||
|
||||
def button_sequence_to_word(prefix: str) -> str:
|
||||
if not prefix:
|
||||
return ""
|
||||
return slip39.button_sequence_to_word(int(prefix))
|
||||
|
||||
"""
|
||||
## Simple helpers
|
||||
"""
|
||||
|
||||
_RADIX_BITS = const(10)
|
||||
"""The length of the radix in bits."""
|
||||
|
||||
|
||||
def bits_to_bytes(n: int) -> int:
|
||||
def _bits_to_bytes(n: int) -> int:
|
||||
return (n + 7) // 8
|
||||
|
||||
|
||||
def bits_to_words(n: int) -> int:
|
||||
def _bits_to_words(n: int) -> int:
|
||||
return (n + _RADIX_BITS - 1) // _RADIX_BITS
|
||||
|
||||
|
||||
MAX_SHARE_COUNT = const(16)
|
||||
"""The maximum number of shares that can be created."""
|
||||
def _xor(a: bytes, b: bytes) -> bytes:
|
||||
return bytes(x ^ y for x, y in zip(a, b))
|
||||
|
||||
DEFAULT_ITERATION_EXPONENT = const(1)
|
||||
|
||||
_RADIX = 2 ** _RADIX_BITS
|
||||
"""The number of words in the wordlist."""
|
||||
"""
|
||||
## Constants
|
||||
"""
|
||||
|
||||
_ID_LENGTH_BITS = const(15)
|
||||
"""The length of the random identifier in bits."""
|
||||
@ -71,7 +72,7 @@ _ID_LENGTH_BITS = const(15)
|
||||
_ITERATION_EXP_LENGTH_BITS = const(5)
|
||||
"""The length of the iteration exponent in bits."""
|
||||
|
||||
_ID_EXP_LENGTH_WORDS = bits_to_words(_ID_LENGTH_BITS + _ITERATION_EXP_LENGTH_BITS)
|
||||
_ID_EXP_LENGTH_WORDS = _bits_to_words(_ID_LENGTH_BITS + _ITERATION_EXP_LENGTH_BITS)
|
||||
"""The length of the random identifier and iteration exponent in words."""
|
||||
|
||||
_CHECKSUM_LENGTH_WORDS = const(3)
|
||||
@ -89,7 +90,7 @@ _METADATA_LENGTH_WORDS = _ID_EXP_LENGTH_WORDS + 2 + _CHECKSUM_LENGTH_WORDS
|
||||
_MIN_STRENGTH_BITS = const(128)
|
||||
"""The minimum allowed entropy of the master secret."""
|
||||
|
||||
_MIN_MNEMONIC_LENGTH_WORDS = _METADATA_LENGTH_WORDS + bits_to_words(_MIN_STRENGTH_BITS)
|
||||
_MIN_MNEMONIC_LENGTH_WORDS = _METADATA_LENGTH_WORDS + _bits_to_words(_MIN_STRENGTH_BITS)
|
||||
"""The minimum allowed length of the mnemonic in words."""
|
||||
|
||||
_BASE_ITERATION_COUNT = const(10000)
|
||||
@ -105,6 +106,289 @@ _DIGEST_INDEX = const(254)
|
||||
"""The index of the share containing the digest of the shared secret."""
|
||||
|
||||
|
||||
"""
|
||||
# Keyboard functions
|
||||
"""
|
||||
|
||||
KEYBOARD_FULL_MASK = const(0x1FF)
|
||||
"""All buttons are allowed. 9-bit bitmap all set to 1."""
|
||||
|
||||
|
||||
def compute_mask(prefix: str) -> int:
|
||||
if not prefix:
|
||||
return KEYBOARD_FULL_MASK
|
||||
return slip39.compute_mask(int(prefix))
|
||||
|
||||
|
||||
def button_sequence_to_word(prefix: str) -> str:
|
||||
if not prefix:
|
||||
return ""
|
||||
return slip39.button_sequence_to_word(int(prefix))
|
||||
|
||||
|
||||
"""
|
||||
# External API
|
||||
"""
|
||||
|
||||
MAX_SHARE_COUNT = const(16)
|
||||
"""The maximum number of shares that can be created."""
|
||||
MAX_GROUP_COUNT = const(16)
|
||||
"""The maximum number of groups that can be created."""
|
||||
|
||||
DEFAULT_ITERATION_EXPONENT = const(1)
|
||||
|
||||
|
||||
class Share:
|
||||
"""
|
||||
Represents a single mnemonic and offers its parsed metadata.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
identifier: int,
|
||||
iteration_exponent: int,
|
||||
group_index: int,
|
||||
group_threshold: int,
|
||||
group_count: int,
|
||||
index: int,
|
||||
threshold: int,
|
||||
share_value: bytes,
|
||||
):
|
||||
self.identifier = identifier
|
||||
self.iteration_exponent = iteration_exponent
|
||||
self.group_index = group_index
|
||||
self.group_threshold = group_threshold
|
||||
self.group_count = group_count
|
||||
self.index = index
|
||||
self.threshold = threshold
|
||||
self.share_value = share_value
|
||||
|
||||
|
||||
def decrypt(
|
||||
identifier: int,
|
||||
iteration_exponent: int,
|
||||
encrypted_master_secret: bytes,
|
||||
passphrase: bytes,
|
||||
) -> bytes:
|
||||
"""
|
||||
Converts the Encrypted Master Secret to a Master Secret by applying the passphrase.
|
||||
This is analogous to BIP-39 passphrase derivation. We do not use the term "derive"
|
||||
here, because passphrase function is symmetric in SLIP-39. We are using the terms
|
||||
"encrypt" and "decrypt" instead.
|
||||
"""
|
||||
l = encrypted_master_secret[: len(encrypted_master_secret) // 2]
|
||||
r = encrypted_master_secret[len(encrypted_master_secret) // 2 :]
|
||||
salt = _get_salt(identifier)
|
||||
for i in reversed(range(_ROUND_COUNT)):
|
||||
(l, r) = (
|
||||
r,
|
||||
_xor(l, _round_function(i, passphrase, iteration_exponent, salt, r)),
|
||||
)
|
||||
return r + l
|
||||
|
||||
|
||||
def generate_random_identifier() -> int:
|
||||
"""Returns a randomly generated integer in the range 0, ... , 2**_ID_LENGTH_BITS - 1."""
|
||||
|
||||
identifier = int.from_bytes(random.bytes(_bits_to_bytes(_ID_LENGTH_BITS)), "big")
|
||||
return identifier & ((1 << _ID_LENGTH_BITS) - 1)
|
||||
|
||||
|
||||
def generate_mnemonics_from_data(
|
||||
encrypted_master_secret: bytes, # The encrypted master secret to split.
|
||||
identifier: int,
|
||||
group_threshold: int, # The number of groups required to reconstruct the master secret.
|
||||
groups: List[Tuple[int, int]], # A list of (member_threshold, member_count).
|
||||
iteration_exponent: int,
|
||||
) -> List[List[str]]:
|
||||
"""
|
||||
Splits an encrypted master secret into mnemonic shares using Shamir's secret sharing scheme.
|
||||
|
||||
The `groups` argument takes pairs for each group, where member_count is the number of shares
|
||||
to generate for the group and member_threshold is the number of members required to reconstruct
|
||||
the group secret.
|
||||
|
||||
Returns a list of mnemonics, grouped by the groups.
|
||||
"""
|
||||
|
||||
if group_threshold > len(groups):
|
||||
raise ValueError(
|
||||
"The requested group threshold ({}) must not exceed the number of groups ({}).".format(
|
||||
group_threshold, len(groups)
|
||||
)
|
||||
)
|
||||
|
||||
if any(
|
||||
member_threshold == 1 and member_count > 1
|
||||
for member_threshold, member_count in groups
|
||||
):
|
||||
raise ValueError(
|
||||
"Creating multiple member shares with member threshold 1 is not allowed. Use 1-of-1 member sharing instead."
|
||||
)
|
||||
|
||||
# Split the Encrypted Master Secret on the group level.
|
||||
group_shares = _split_secret(group_threshold, len(groups), encrypted_master_secret)
|
||||
|
||||
mnemonics = [] # type: List[List[str]]
|
||||
for (member_threshold, member_count), (group_index, group_secret) in zip(
|
||||
groups, group_shares
|
||||
):
|
||||
group_mnemonics = []
|
||||
# Split the group's secret between shares.
|
||||
shares = _split_secret(member_threshold, member_count, group_secret)
|
||||
for member_index, value in shares:
|
||||
group_mnemonics.append(
|
||||
_encode_mnemonic(
|
||||
identifier,
|
||||
iteration_exponent,
|
||||
group_index,
|
||||
group_threshold,
|
||||
len(groups),
|
||||
member_index,
|
||||
member_threshold,
|
||||
value,
|
||||
)
|
||||
)
|
||||
mnemonics.append(group_mnemonics)
|
||||
return mnemonics
|
||||
|
||||
|
||||
def combine_mnemonics(mnemonics: List[str]) -> Tuple[int, int, bytes, int]:
|
||||
"""
|
||||
Combines mnemonic shares to obtain the encrypted master secret which was previously
|
||||
split using Shamir's secret sharing scheme.
|
||||
Returns identifier, iteration exponent and the encrypted master secret.
|
||||
"""
|
||||
|
||||
if not mnemonics:
|
||||
raise MnemonicError("The list of mnemonics is empty.")
|
||||
|
||||
identifier, iteration_exponent, group_threshold, group_count, groups = _decode_mnemonics(
|
||||
mnemonics
|
||||
)
|
||||
|
||||
if len(groups) != group_threshold:
|
||||
raise MnemonicError(
|
||||
"Wrong number of mnemonic groups. Expected {} groups, but {} were provided.".format(
|
||||
group_threshold, len(groups)
|
||||
)
|
||||
)
|
||||
|
||||
for group_index, group in groups.items():
|
||||
if len(group[1]) != group[0]: # group[0] is threshold
|
||||
raise MnemonicError(
|
||||
"Wrong number of mnemonics. Expected {} mnemonics, but {} were provided.".format(
|
||||
group[0], len(group[1])
|
||||
)
|
||||
)
|
||||
|
||||
group_shares = [
|
||||
(group_index, _recover_secret(group[0], list(group[1])))
|
||||
for group_index, group in groups.items()
|
||||
]
|
||||
|
||||
encrypted_master_secret = _recover_secret(group_threshold, group_shares)
|
||||
return identifier, iteration_exponent, encrypted_master_secret, group_count
|
||||
|
||||
|
||||
def decode_mnemonic(mnemonic: str) -> Share:
|
||||
"""Converts a share mnemonic to share data."""
|
||||
|
||||
mnemonic_data = tuple(_mnemonic_to_indices(mnemonic))
|
||||
|
||||
if len(mnemonic_data) < _MIN_MNEMONIC_LENGTH_WORDS:
|
||||
raise MnemonicError(
|
||||
"Invalid mnemonic length. The length of each mnemonic must be at least {} words.".format(
|
||||
_MIN_MNEMONIC_LENGTH_WORDS
|
||||
)
|
||||
)
|
||||
|
||||
padding_len = (_RADIX_BITS * (len(mnemonic_data) - _METADATA_LENGTH_WORDS)) % 16
|
||||
if padding_len > 8:
|
||||
raise MnemonicError("Invalid mnemonic length.")
|
||||
|
||||
if not _rs1024_verify_checksum(mnemonic_data):
|
||||
raise MnemonicError("Invalid mnemonic checksum.")
|
||||
|
||||
id_exp_int = _int_from_indices(mnemonic_data[:_ID_EXP_LENGTH_WORDS])
|
||||
identifier = id_exp_int >> _ITERATION_EXP_LENGTH_BITS
|
||||
iteration_exponent = id_exp_int & ((1 << _ITERATION_EXP_LENGTH_BITS) - 1)
|
||||
tmp = _int_from_indices(
|
||||
mnemonic_data[_ID_EXP_LENGTH_WORDS : _ID_EXP_LENGTH_WORDS + 2]
|
||||
)
|
||||
group_index, group_threshold, group_count, member_index, member_threshold = _int_to_indices(
|
||||
tmp, 5, 4
|
||||
)
|
||||
value_data = mnemonic_data[_ID_EXP_LENGTH_WORDS + 2 : -_CHECKSUM_LENGTH_WORDS]
|
||||
|
||||
if group_count < group_threshold:
|
||||
raise MnemonicError(
|
||||
"Invalid mnemonic. Group threshold cannot be greater than group count."
|
||||
)
|
||||
|
||||
value_byte_count = _bits_to_bytes(_RADIX_BITS * len(value_data) - padding_len)
|
||||
value_int = _int_from_indices(value_data)
|
||||
if value_data[0] >= 1 << (_RADIX_BITS - padding_len):
|
||||
raise MnemonicError("Invalid mnemonic padding")
|
||||
value = value_int.to_bytes(value_byte_count, "big")
|
||||
|
||||
return Share(
|
||||
identifier,
|
||||
iteration_exponent,
|
||||
group_index,
|
||||
group_threshold + 1,
|
||||
group_count + 1,
|
||||
member_index,
|
||||
member_threshold + 1,
|
||||
value,
|
||||
)
|
||||
|
||||
|
||||
"""
|
||||
## Convert mnemonics or integers to incices and back
|
||||
"""
|
||||
|
||||
|
||||
def _int_from_indices(indices: Indices) -> int:
|
||||
"""Converts a list of base 1024 indices in big endian order to an integer value."""
|
||||
value = 0
|
||||
for index in indices:
|
||||
value = (value << _RADIX_BITS) + index
|
||||
return value
|
||||
|
||||
|
||||
def _int_to_indices(value: int, length: int, bits: int) -> Iterable[int]:
|
||||
"""Converts an integer value to indices in big endian order."""
|
||||
mask = (1 << bits) - 1
|
||||
return ((value >> (i * bits)) & mask for i in reversed(range(length)))
|
||||
|
||||
|
||||
def _mnemonic_from_indices(indices: Indices) -> str:
|
||||
return " ".join(slip39.get_word(i) for i in indices)
|
||||
|
||||
|
||||
def _mnemonic_to_indices(mnemonic: str) -> Iterable[int]:
|
||||
return (slip39.word_index(word.lower()) for word in mnemonic.split())
|
||||
|
||||
|
||||
"""
|
||||
## Checksum functions
|
||||
"""
|
||||
|
||||
|
||||
def _rs1024_create_checksum(data: Indices) -> Indices:
|
||||
"""
|
||||
This implements the checksum - a Reed-Solomon code over GF(1024) that guarantees
|
||||
detection of any error affecting at most 3 words and has less than a 1 in 10^9
|
||||
chance of failing to detect more errors.
|
||||
"""
|
||||
values = tuple(_CUSTOMIZATION_STRING) + data + _CHECKSUM_LENGTH_WORDS * (0,)
|
||||
polymod = _rs1024_polymod(values) ^ 1
|
||||
return tuple(
|
||||
(polymod >> 10 * i) & 1023 for i in reversed(range(_CHECKSUM_LENGTH_WORDS))
|
||||
)
|
||||
|
||||
|
||||
def _rs1024_polymod(values: Indices) -> int:
|
||||
GEN = (
|
||||
0xE0E040,
|
||||
@ -127,19 +411,18 @@ def _rs1024_polymod(values: Indices) -> int:
|
||||
return chk
|
||||
|
||||
|
||||
def rs1024_create_checksum(data: Indices) -> Indices:
|
||||
values = tuple(_CUSTOMIZATION_STRING) + data + _CHECKSUM_LENGTH_WORDS * (0,)
|
||||
polymod = _rs1024_polymod(values) ^ 1
|
||||
return tuple(
|
||||
(polymod >> 10 * i) & 1023 for i in reversed(range(_CHECKSUM_LENGTH_WORDS))
|
||||
)
|
||||
|
||||
|
||||
def rs1024_verify_checksum(data: Indices) -> bool:
|
||||
def _rs1024_verify_checksum(data: Indices) -> bool:
|
||||
"""
|
||||
Verifies a checksum of the given mnemonic, which was already parsed into Indices.
|
||||
"""
|
||||
return _rs1024_polymod(tuple(_CUSTOMIZATION_STRING) + data) == 1
|
||||
|
||||
|
||||
def rs1024_error_index(data: Indices) -> Optional[int]:
|
||||
def _rs1024_error_index(data: Indices) -> Optional[int]:
|
||||
"""
|
||||
Returns the index where an error possibly occurred.
|
||||
Currently unused.
|
||||
"""
|
||||
GEN = (
|
||||
0x91F9F87,
|
||||
0x122F1F07,
|
||||
@ -166,30 +449,9 @@ def rs1024_error_index(data: Indices) -> Optional[int]:
|
||||
return None
|
||||
|
||||
|
||||
def xor(a: bytes, b: bytes) -> bytes:
|
||||
return bytes(x ^ y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def _int_from_indices(indices: Indices) -> int:
|
||||
"""Converts a list of base 1024 indices in big endian order to an integer value."""
|
||||
value = 0
|
||||
for index in indices:
|
||||
value = (value << _RADIX_BITS) + index
|
||||
return value
|
||||
|
||||
|
||||
def _int_to_indices(value: int, length: int, bits: int) -> Iterable[int]:
|
||||
"""Converts an integer value to indices in big endian order."""
|
||||
mask = (1 << bits) - 1
|
||||
return ((value >> (i * bits)) & mask for i in reversed(range(length)))
|
||||
|
||||
|
||||
def mnemonic_from_indices(indices: Indices) -> str:
|
||||
return " ".join(slip39.get_word(i) for i in indices)
|
||||
|
||||
|
||||
def mnemonic_to_indices(mnemonic: str) -> Iterable[int]:
|
||||
return (slip39.word_index(word.lower()) for word in mnemonic.split())
|
||||
"""
|
||||
## Internal functions
|
||||
"""
|
||||
|
||||
|
||||
def _round_function(i: int, passphrase: bytes, e: int, salt: bytes, r: bytes) -> bytes:
|
||||
@ -204,27 +466,10 @@ def _round_function(i: int, passphrase: bytes, e: int, salt: bytes, r: bytes) ->
|
||||
|
||||
def _get_salt(identifier: int) -> bytes:
|
||||
return _CUSTOMIZATION_STRING + identifier.to_bytes(
|
||||
bits_to_bytes(_ID_LENGTH_BITS), "big"
|
||||
_bits_to_bytes(_ID_LENGTH_BITS), "big"
|
||||
)
|
||||
|
||||
|
||||
def decrypt(
|
||||
identifier: int,
|
||||
iteration_exponent: int,
|
||||
encrypted_master_secret: bytes,
|
||||
passphrase: bytes,
|
||||
) -> bytes:
|
||||
l = encrypted_master_secret[: len(encrypted_master_secret) // 2]
|
||||
r = encrypted_master_secret[len(encrypted_master_secret) // 2 :]
|
||||
salt = _get_salt(identifier)
|
||||
for i in reversed(range(_ROUND_COUNT)):
|
||||
(l, r) = (
|
||||
r,
|
||||
xor(l, _round_function(i, passphrase, iteration_exponent, salt, r)),
|
||||
)
|
||||
return r + l
|
||||
|
||||
|
||||
def _create_digest(random_data: bytes, shared_secret: bytes) -> bytes:
|
||||
return hmac.new(random_data, shared_secret, hashlib.sha256).digest()[
|
||||
:_DIGEST_LENGTH_BYTES
|
||||
@ -304,33 +549,23 @@ def _group_prefix(
|
||||
)
|
||||
|
||||
|
||||
def encode_mnemonic(
|
||||
def _encode_mnemonic(
|
||||
identifier: int,
|
||||
iteration_exponent: int,
|
||||
group_index: int,
|
||||
group_threshold: int,
|
||||
group_count: int,
|
||||
member_index: int,
|
||||
member_threshold: int,
|
||||
value: bytes,
|
||||
group_index: int, # The x coordinate of the group share.
|
||||
group_threshold: int, # The number of group shares needed to reconstruct the encrypted master secret.
|
||||
group_count: int, # The total number of groups in existence.
|
||||
member_index: int, # The x coordinate of the member share in the given group.
|
||||
member_threshold: int, # The number of member shares needed to reconstruct the group share.
|
||||
value: bytes, # The share value representing the y coordinates of the share.
|
||||
) -> str:
|
||||
"""
|
||||
Converts share data to a share mnemonic.
|
||||
:param int identifier: The random identifier.
|
||||
:param int iteration_exponent: The iteration exponent.
|
||||
:param int group_index: The x coordinate of the group share.
|
||||
:param int group_threshold: The number of group shares needed to reconstruct the encrypted master secret.
|
||||
:param int group_count: The total number of groups in existence.
|
||||
:param int member_index: The x coordinate of the member share in the given group.
|
||||
:param int member_threshold: The number of member shares needed to reconstruct the group share.
|
||||
:param value: The share value representing the y coordinates of the share.
|
||||
:type value: Array of bytes.
|
||||
:return: The share mnemonic.
|
||||
:rtype: Array of bytes.
|
||||
Takes the metadata and the value to be encoded and converts it into a mnemonic words.
|
||||
Also appends a checksum.
|
||||
"""
|
||||
|
||||
# Convert the share value from bytes to wordlist indices.
|
||||
value_word_count = bits_to_words(len(value) * 8)
|
||||
value_word_count = _bits_to_words(len(value) * 8)
|
||||
value_int = int.from_bytes(value, "big")
|
||||
|
||||
share_data = (
|
||||
@ -344,66 +579,9 @@ def encode_mnemonic(
|
||||
)
|
||||
+ tuple(_int_to_indices(value_int, value_word_count, _RADIX_BITS))
|
||||
)
|
||||
checksum = rs1024_create_checksum(share_data)
|
||||
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]:
|
||||
"""Converts a share mnemonic to share data."""
|
||||
|
||||
mnemonic_data = tuple(mnemonic_to_indices(mnemonic))
|
||||
|
||||
if len(mnemonic_data) < _MIN_MNEMONIC_LENGTH_WORDS:
|
||||
raise MnemonicError(
|
||||
"Invalid mnemonic length. The length of each mnemonic must be at least {} words.".format(
|
||||
_MIN_MNEMONIC_LENGTH_WORDS
|
||||
)
|
||||
)
|
||||
|
||||
padding_len = (_RADIX_BITS * (len(mnemonic_data) - _METADATA_LENGTH_WORDS)) % 16
|
||||
if padding_len > 8:
|
||||
raise MnemonicError("Invalid mnemonic length.")
|
||||
|
||||
if not rs1024_verify_checksum(mnemonic_data):
|
||||
raise MnemonicError("Invalid mnemonic checksum.")
|
||||
|
||||
id_exp_int = _int_from_indices(mnemonic_data[:_ID_EXP_LENGTH_WORDS])
|
||||
identifier = id_exp_int >> _ITERATION_EXP_LENGTH_BITS
|
||||
iteration_exponent = id_exp_int & ((1 << _ITERATION_EXP_LENGTH_BITS) - 1)
|
||||
tmp = _int_from_indices(
|
||||
mnemonic_data[_ID_EXP_LENGTH_WORDS : _ID_EXP_LENGTH_WORDS + 2]
|
||||
)
|
||||
group_index, group_threshold, group_count, member_index, member_threshold = _int_to_indices(
|
||||
tmp, 5, 4
|
||||
)
|
||||
value_data = mnemonic_data[_ID_EXP_LENGTH_WORDS + 2 : -_CHECKSUM_LENGTH_WORDS]
|
||||
|
||||
if group_count < group_threshold:
|
||||
raise MnemonicError(
|
||||
"Invalid mnemonic. Group threshold cannot be greater than group count."
|
||||
)
|
||||
|
||||
value_byte_count = bits_to_bytes(_RADIX_BITS * len(value_data) - padding_len)
|
||||
value_int = _int_from_indices(value_data)
|
||||
if value_data[0] >= 1 << (_RADIX_BITS - padding_len):
|
||||
raise MnemonicError("Invalid mnemonic padding")
|
||||
value = value_int.to_bytes(value_byte_count, "big")
|
||||
|
||||
return (
|
||||
identifier,
|
||||
iteration_exponent,
|
||||
group_index,
|
||||
group_threshold + 1,
|
||||
group_count + 1,
|
||||
member_index,
|
||||
member_threshold + 1,
|
||||
value,
|
||||
)
|
||||
|
||||
|
||||
if False:
|
||||
MnemonicGroups = Dict[int, Tuple[int, Set[Tuple[int, bytes]]]]
|
||||
return _mnemonic_from_indices(share_data + checksum)
|
||||
|
||||
|
||||
def _decode_mnemonics(
|
||||
@ -414,22 +592,20 @@ def _decode_mnemonics(
|
||||
group_thresholds = set()
|
||||
group_counts = set()
|
||||
|
||||
# { group_index : [member_threshold, set_of_member_shares] }
|
||||
# { group_index : [threshold, set_of_member_shares] }
|
||||
groups = {} # type: MnemonicGroups
|
||||
for mnemonic in mnemonics:
|
||||
identifier, iteration_exponent, group_index, group_threshold, group_count, member_index, member_threshold, share_value = decode_mnemonic(
|
||||
mnemonic
|
||||
)
|
||||
identifiers.add(identifier)
|
||||
iteration_exponents.add(iteration_exponent)
|
||||
group_thresholds.add(group_threshold)
|
||||
group_counts.add(group_count)
|
||||
group = groups.setdefault(group_index, (member_threshold, set()))
|
||||
if group[0] != member_threshold:
|
||||
share = decode_mnemonic(mnemonic)
|
||||
identifiers.add(share.identifier)
|
||||
iteration_exponents.add(share.iteration_exponent)
|
||||
group_thresholds.add(share.group_threshold)
|
||||
group_counts.add(share.group_count)
|
||||
group = groups.setdefault(share.group_index, (share.threshold, set()))
|
||||
if group[0] != share.threshold:
|
||||
raise MnemonicError(
|
||||
"Invalid set of mnemonics. All mnemonics in a group must have the same member threshold."
|
||||
)
|
||||
group[1].add((member_index, share_value))
|
||||
group[1].add((share.index, share.share_value))
|
||||
|
||||
if len(identifiers) != 1 or len(iteration_exponents) != 1:
|
||||
raise MnemonicError(
|
||||
@ -461,143 +637,3 @@ def _decode_mnemonics(
|
||||
group_counts.pop(),
|
||||
groups,
|
||||
)
|
||||
|
||||
|
||||
def generate_random_identifier() -> int:
|
||||
"""Returns a randomly generated integer in the range 0, ... , 2**_ID_LENGTH_BITS - 1."""
|
||||
|
||||
identifier = int.from_bytes(random.bytes(bits_to_bytes(_ID_LENGTH_BITS)), "big")
|
||||
return identifier & ((1 << _ID_LENGTH_BITS) - 1)
|
||||
|
||||
|
||||
def generate_single_group_mnemonics_from_data(
|
||||
encrypted_master_secret: bytes,
|
||||
identifier: int,
|
||||
threshold: int,
|
||||
count: int,
|
||||
iteration_exponent: int = DEFAULT_ITERATION_EXPONENT,
|
||||
) -> List[str]:
|
||||
return generate_mnemonics_from_data(
|
||||
encrypted_master_secret, identifier, 1, [(threshold, count)], iteration_exponent
|
||||
)[0]
|
||||
|
||||
|
||||
def generate_mnemonics_from_data(
|
||||
encrypted_master_secret: bytes,
|
||||
identifier: int,
|
||||
group_threshold: int,
|
||||
groups: List[Tuple[int, int]],
|
||||
iteration_exponent: int = DEFAULT_ITERATION_EXPONENT,
|
||||
) -> List[List[str]]:
|
||||
"""
|
||||
Splits an encrypted master secret into mnemonic shares using Shamir's secret sharing scheme.
|
||||
:param encrypted_master_secret: The encrypted master secret to split.
|
||||
:type encrypted_master_secret: Array of bytes.
|
||||
:param int identifier
|
||||
:param int group_threshold: The number of groups required to reconstruct the master secret.
|
||||
:param groups: A list of (member_threshold, member_count) pairs for each group, where member_count
|
||||
is the number of shares to generate for the group and member_threshold is the number of members required to
|
||||
reconstruct the group secret.
|
||||
:type groups: List of pairs of integers.
|
||||
:param int iteration_exponent: The iteration exponent.
|
||||
:return: List of mnemonics.
|
||||
:rtype: List of byte arrays.
|
||||
:return: Identifier.
|
||||
:rtype: int.
|
||||
"""
|
||||
|
||||
if len(encrypted_master_secret) * 8 < _MIN_STRENGTH_BITS:
|
||||
raise ValueError(
|
||||
"The length of the encrypted master secret ({} bytes) must be at least {} bytes.".format(
|
||||
len(encrypted_master_secret), bits_to_bytes(_MIN_STRENGTH_BITS)
|
||||
)
|
||||
)
|
||||
|
||||
if len(encrypted_master_secret) % 2 != 0:
|
||||
raise ValueError(
|
||||
"The length of the encrypted master secret in bytes must be an even number."
|
||||
)
|
||||
|
||||
if group_threshold > len(groups):
|
||||
raise ValueError(
|
||||
"The requested group threshold ({}) must not exceed the number of groups ({}).".format(
|
||||
group_threshold, len(groups)
|
||||
)
|
||||
)
|
||||
|
||||
if any(
|
||||
member_threshold == 1 and member_count > 1
|
||||
for member_threshold, member_count in groups
|
||||
):
|
||||
raise ValueError(
|
||||
"Creating multiple member shares with member threshold 1 is not allowed. Use 1-of-1 member sharing instead."
|
||||
)
|
||||
|
||||
group_shares = _split_secret(group_threshold, len(groups), encrypted_master_secret)
|
||||
|
||||
mnemonics = [] # type: List[List[str]]
|
||||
for (member_threshold, member_count), (group_index, group_secret) in zip(
|
||||
groups, group_shares
|
||||
):
|
||||
group_mnemonics = []
|
||||
for member_index, value in _split_secret(
|
||||
member_threshold, member_count, group_secret
|
||||
):
|
||||
group_mnemonics.append(
|
||||
encode_mnemonic(
|
||||
identifier,
|
||||
iteration_exponent,
|
||||
group_index,
|
||||
group_threshold,
|
||||
len(groups),
|
||||
member_index,
|
||||
member_threshold,
|
||||
value,
|
||||
)
|
||||
)
|
||||
mnemonics.append(group_mnemonics)
|
||||
return mnemonics
|
||||
|
||||
|
||||
def combine_mnemonics(mnemonics: List[str]) -> Tuple[int, int, bytes]:
|
||||
"""
|
||||
Combines mnemonic shares to obtain the master secret which was previously split using
|
||||
Shamir's secret sharing scheme.
|
||||
:param mnemonics: List of mnemonics.
|
||||
:type mnemonics: List of strings.
|
||||
:return: Identifier, iteration exponent, the encrypted master secret.
|
||||
:rtype: Integer, integer, array of bytes.
|
||||
"""
|
||||
|
||||
if not mnemonics:
|
||||
raise MnemonicError("The list of mnemonics is empty.")
|
||||
|
||||
identifier, iteration_exponent, group_threshold, group_count, groups = _decode_mnemonics(
|
||||
mnemonics
|
||||
)
|
||||
|
||||
if len(groups) != group_threshold:
|
||||
raise MnemonicError(
|
||||
"Wrong number of mnemonic groups. Expected {} groups, but {} were provided.".format(
|
||||
group_threshold, len(groups)
|
||||
)
|
||||
)
|
||||
|
||||
for group_index, group in groups.items():
|
||||
if len(group[1]) != group[0]:
|
||||
raise MnemonicError(
|
||||
"Wrong number of mnemonics. Expected {} mnemonics, but {} were provided.".format(
|
||||
group[0], len(group[1])
|
||||
)
|
||||
)
|
||||
|
||||
group_shares = [
|
||||
(group_index, _recover_secret(group[0], list(group[1])))
|
||||
for group_index, group in groups.items()
|
||||
]
|
||||
|
||||
return (
|
||||
identifier,
|
||||
iteration_exponent,
|
||||
_recover_secret(group_threshold, group_shares),
|
||||
)
|
||||
|
@ -5,15 +5,3 @@
|
||||
|
||||
class MnemonicError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class IdentifierMismatchError(MnemonicError):
|
||||
pass
|
||||
|
||||
|
||||
class ShareAlreadyAddedError(MnemonicError):
|
||||
pass
|
||||
|
||||
|
||||
class GroupThresholdReachedError(MnemonicError):
|
||||
pass
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Automatically generated by pb2py
|
||||
# fmt: off
|
||||
Bip39 = 0
|
||||
Slip39_Single_Group = 1
|
||||
Slip39_Multiple_Groups = 2
|
||||
Slip39_Basic = 1
|
||||
Slip39_Advanced = 2
|
@ -7,9 +7,11 @@ if __debug__:
|
||||
from typing import Dict, List, Optional
|
||||
from typing_extensions import Literal # noqa: F401
|
||||
EnumTypeCapability = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
|
||||
EnumTypeBackupType = Literal[0, 1, 2]
|
||||
except ImportError:
|
||||
Dict, List, Optional = None, None, None # type: ignore
|
||||
EnumTypeCapability = None # type: ignore
|
||||
EnumTypeBackupType = None # type: ignore
|
||||
|
||||
|
||||
class Features(p.MessageType):
|
||||
@ -46,6 +48,7 @@ class Features(p.MessageType):
|
||||
no_backup: bool = None,
|
||||
recovery_mode: bool = None,
|
||||
capabilities: List[EnumTypeCapability] = None,
|
||||
backup_type: EnumTypeBackupType = None,
|
||||
) -> None:
|
||||
self.vendor = vendor
|
||||
self.major_version = major_version
|
||||
@ -76,6 +79,7 @@ class Features(p.MessageType):
|
||||
self.no_backup = no_backup
|
||||
self.recovery_mode = recovery_mode
|
||||
self.capabilities = capabilities if capabilities is not None else []
|
||||
self.backup_type = backup_type
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
@ -109,4 +113,5 @@ class Features(p.MessageType):
|
||||
28: ('no_backup', p.BoolType, 0),
|
||||
29: ('recovery_mode', p.BoolType, 0),
|
||||
30: ('capabilities', p.EnumType("Capability", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)), p.FLAG_REPEATED),
|
||||
31: ('backup_type', p.EnumType("BackupType", (0, 1, 2)), 0),
|
||||
}
|
||||
|
@ -6,10 +6,10 @@ if __debug__:
|
||||
try:
|
||||
from typing import Dict, List, Optional
|
||||
from typing_extensions import Literal # noqa: F401
|
||||
EnumTypeResetDeviceBackupType = Literal[0, 1, 2]
|
||||
EnumTypeBackupType = Literal[0, 1, 2]
|
||||
except ImportError:
|
||||
Dict, List, Optional = None, None, None # type: ignore
|
||||
EnumTypeResetDeviceBackupType = None # type: ignore
|
||||
EnumTypeBackupType = None # type: ignore
|
||||
|
||||
|
||||
class ResetDevice(p.MessageType):
|
||||
@ -26,7 +26,7 @@ class ResetDevice(p.MessageType):
|
||||
u2f_counter: int = None,
|
||||
skip_backup: bool = None,
|
||||
no_backup: bool = None,
|
||||
backup_type: EnumTypeResetDeviceBackupType = None,
|
||||
backup_type: EnumTypeBackupType = None,
|
||||
) -> None:
|
||||
self.display_random = display_random
|
||||
self.strength = strength
|
||||
@ -51,5 +51,5 @@ class ResetDevice(p.MessageType):
|
||||
7: ('u2f_counter', p.UVarintType, 0),
|
||||
8: ('skip_backup', p.BoolType, 0),
|
||||
9: ('no_backup', p.BoolType, 0),
|
||||
10: ('backup_type', p.EnumType("ResetDeviceBackupType", (0, 1, 2)), 0), # default=ResetDeviceBackupType_Bip39
|
||||
10: ('backup_type', p.EnumType("BackupType", (0, 1, 2)), 0), # default=Bip39
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
from micropython import const
|
||||
|
||||
from trezor import loop, res, ui
|
||||
from trezor.ui.button import Button, ButtonCancel, ButtonConfirm
|
||||
from trezor.ui.button import Button, ButtonCancel, ButtonConfirm, ButtonDefault
|
||||
from trezor.ui.loader import Loader, LoaderDefault
|
||||
|
||||
if __debug__:
|
||||
@ -14,6 +14,7 @@ if False:
|
||||
|
||||
CONFIRMED = object()
|
||||
CANCELLED = object()
|
||||
INFO = object()
|
||||
|
||||
|
||||
class Confirm(ui.Layout):
|
||||
@ -153,6 +154,54 @@ class ConfirmPageable(Confirm):
|
||||
ui.display.icon(205, 68, icon, c, ui.BG)
|
||||
|
||||
|
||||
class InfoConfirm(ui.Layout):
|
||||
DEFAULT_CONFIRM = res.load(ui.ICON_CONFIRM)
|
||||
DEFAULT_CONFIRM_STYLE = ButtonConfirm
|
||||
DEFAULT_CANCEL = res.load(ui.ICON_CANCEL)
|
||||
DEFAULT_CANCEL_STYLE = ButtonCancel
|
||||
DEFAULT_INFO = res.load(ui.ICON_CLICK) # TODO: this should be (i) icon, not click
|
||||
DEFAULT_INFO_STYLE = ButtonDefault
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
content: ui.Component,
|
||||
confirm: ButtonContent = DEFAULT_CONFIRM,
|
||||
confirm_style: ButtonStyleType = DEFAULT_CONFIRM_STYLE,
|
||||
cancel: ButtonContent = DEFAULT_CANCEL,
|
||||
cancel_style: ButtonStyleType = DEFAULT_CANCEL_STYLE,
|
||||
info: ButtonContent = DEFAULT_INFO,
|
||||
info_style: ButtonStyleType = DEFAULT_INFO_STYLE,
|
||||
) -> None:
|
||||
self.content = content
|
||||
|
||||
self.confirm = Button(ui.grid(14), confirm, confirm_style)
|
||||
self.confirm.on_click = self.on_confirm # type: ignore
|
||||
|
||||
self.info = Button(ui.grid(13), info, info_style)
|
||||
self.info.on_click = self.on_info
|
||||
|
||||
self.cancel = Button(ui.grid(12), cancel, cancel_style)
|
||||
self.cancel.on_click = self.on_cancel # type: ignore
|
||||
|
||||
def dispatch(self, event: int, x: int, y: int) -> None:
|
||||
self.content.dispatch(event, x, y)
|
||||
if self.confirm is not None:
|
||||
self.confirm.dispatch(event, x, y)
|
||||
if self.cancel is not None:
|
||||
self.cancel.dispatch(event, x, y)
|
||||
if self.info is not None:
|
||||
self.info.dispatch(event, x, y)
|
||||
|
||||
def on_confirm(self) -> None:
|
||||
raise ui.Result(CONFIRMED)
|
||||
|
||||
def on_cancel(self) -> None:
|
||||
raise ui.Result(CANCELLED)
|
||||
|
||||
def on_info(self) -> None:
|
||||
raise ui.Result(INFO)
|
||||
|
||||
|
||||
class HoldToConfirm(ui.Layout):
|
||||
DEFAULT_CONFIRM = "Hold To Confirm"
|
||||
DEFAULT_CONFIRM_STYLE = ButtonConfirm
|
||||
|
@ -188,7 +188,7 @@ class TestCardanoAddress(unittest.TestCase):
|
||||
"talent drug much home firefly toxic analysis idea umbrella slice"
|
||||
]
|
||||
passphrase = b"TREZOR"
|
||||
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics)
|
||||
identifier, exponent, ems, _ = slip39.combine_mnemonics(mnemonics)
|
||||
master_secret = slip39.decrypt(identifier, exponent, ems, passphrase)
|
||||
|
||||
node = bip32.from_seed(master_secret, "ed25519 cardano seed")
|
||||
@ -252,7 +252,7 @@ class TestCardanoAddress(unittest.TestCase):
|
||||
"quick silent downtown oral critical step remove says rhythm venture aunt"
|
||||
]
|
||||
passphrase = b"TREZOR"
|
||||
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics)
|
||||
identifier, exponent, ems, _ = slip39.combine_mnemonics(mnemonics)
|
||||
master_secret = slip39.decrypt(identifier, exponent, ems, passphrase)
|
||||
|
||||
node = bip32.from_seed(master_secret, "ed25519 cardano seed")
|
||||
|
@ -60,7 +60,7 @@ class TestCardanoGetPublicKey(unittest.TestCase):
|
||||
"talent drug much home firefly toxic analysis idea umbrella slice"
|
||||
]
|
||||
passphrase = b"TREZOR"
|
||||
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics)
|
||||
identifier, exponent, ems, _ = slip39.combine_mnemonics(mnemonics)
|
||||
master_secret = slip39.decrypt(identifier, exponent, ems, passphrase)
|
||||
|
||||
node = bip32.from_seed(master_secret, "ed25519 cardano seed")
|
||||
@ -111,7 +111,7 @@ class TestCardanoGetPublicKey(unittest.TestCase):
|
||||
"quick silent downtown oral critical step remove says rhythm venture aunt"
|
||||
]
|
||||
passphrase = b"TREZOR"
|
||||
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics)
|
||||
identifier, exponent, ems, _ = slip39.combine_mnemonics(mnemonics)
|
||||
master_secret = slip39.decrypt(identifier, exponent, ems, passphrase)
|
||||
|
||||
node = bip32.from_seed(master_secret, "ed25519 cardano seed")
|
||||
|
@ -8,7 +8,7 @@ from trezor.crypto.hashlib import sha256
|
||||
class TestCredential(unittest.TestCase):
|
||||
def test_fido2_credential_decode(self):
|
||||
mnemonic_secret = b"all all all all all all all all all all all all"
|
||||
mnemonic.get = lambda: (mnemonic_secret, mnemonic.TYPE_BIP39)
|
||||
mnemonic.get_secret = lambda: mnemonic_secret
|
||||
storage.is_initialized = lambda: True
|
||||
|
||||
cred_id = (
|
||||
|
@ -25,21 +25,22 @@ class TestCryptoSlip39(unittest.TestCase):
|
||||
EMS = b"ABCDEFGHIJKLMNOP"
|
||||
|
||||
def test_basic_sharing_random(self):
|
||||
ms = random.bytes(32)
|
||||
ems = random.bytes(32)
|
||||
identifier = slip39.generate_random_identifier()
|
||||
mnemonics = slip39.generate_mnemonics_from_data(ms, identifier, 1, [(3, 5)])
|
||||
mnemonics = slip39.generate_mnemonics_from_data(ems, identifier, 1, [(3, 5)], 1)
|
||||
mnemonics = mnemonics[0]
|
||||
self.assertEqual(slip39.combine_mnemonics(mnemonics[:3]), slip39.combine_mnemonics(mnemonics[2:]))
|
||||
|
||||
|
||||
def test_basic_sharing_fixed(self):
|
||||
generated_identifier = slip39.generate_random_identifier()
|
||||
mnemonics = slip39.generate_mnemonics_from_data(self.EMS, generated_identifier, 1, [(3, 5)])
|
||||
mnemonics = slip39.generate_mnemonics_from_data(self.EMS, generated_identifier, 1, [(3, 5)], 1)
|
||||
mnemonics = mnemonics[0]
|
||||
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics[:3])
|
||||
identifier, exponent, ems, group_count = slip39.combine_mnemonics(mnemonics[:3])
|
||||
self.assertEqual(ems, self.EMS)
|
||||
self.assertEqual(generated_identifier, identifier)
|
||||
self.assertEqual(slip39.combine_mnemonics(mnemonics[1:4])[2], ems)
|
||||
self.assertEqual(group_count, 1)
|
||||
with self.assertRaises(slip39.MnemonicError):
|
||||
slip39.combine_mnemonics(mnemonics[1:3])
|
||||
|
||||
@ -48,13 +49,15 @@ class TestCryptoSlip39(unittest.TestCase):
|
||||
identifier = slip39.generate_random_identifier()
|
||||
mnemonics = slip39.generate_mnemonics_from_data(self.EMS, identifier, 1, [(3, 5)], 1)
|
||||
mnemonics = mnemonics[0]
|
||||
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics[1:4])
|
||||
identifier, exponent, ems, group_count = slip39.combine_mnemonics(mnemonics[1:4])
|
||||
self.assertEqual(ems, self.EMS)
|
||||
self.assertEqual(group_count, 1)
|
||||
|
||||
identifier = slip39.generate_random_identifier()
|
||||
mnemonics = slip39.generate_mnemonics_from_data(self.EMS, identifier, 1, [(3, 5)], 2)
|
||||
mnemonics = mnemonics[0]
|
||||
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics[1:4])
|
||||
identifier, exponent, ems, group_count = slip39.combine_mnemonics(mnemonics[1:4])
|
||||
self.assertEqual(group_count, 1)
|
||||
self.assertEqual(ems, self.EMS)
|
||||
|
||||
|
||||
@ -64,7 +67,7 @@ class TestCryptoSlip39(unittest.TestCase):
|
||||
member_thresholds = (3, 2, 2, 1)
|
||||
identifier = slip39.generate_random_identifier()
|
||||
mnemonics = slip39.generate_mnemonics_from_data(
|
||||
self.EMS, identifier, group_threshold, list(zip(member_thresholds, group_sizes))
|
||||
self.EMS, identifier, group_threshold, list(zip(member_thresholds, group_sizes)), 1
|
||||
)
|
||||
|
||||
# Test all valid combinations of mnemonics.
|
||||
@ -73,14 +76,16 @@ class TestCryptoSlip39(unittest.TestCase):
|
||||
for group2_subset in combinations(groups[1][0], groups[1][1]):
|
||||
mnemonic_subset = list(group1_subset + group2_subset)
|
||||
random.shuffle(mnemonic_subset)
|
||||
identifier, exponent, ems = slip39.combine_mnemonics(mnemonic_subset)
|
||||
identifier, exponent, ems, group_count = slip39.combine_mnemonics(mnemonic_subset)
|
||||
self.assertEqual(group_count, len(group_sizes))
|
||||
self.assertEqual(ems, self.EMS)
|
||||
|
||||
|
||||
# Minimal sets of mnemonics.
|
||||
identifier, exponent, ems = slip39.combine_mnemonics([mnemonics[2][0], mnemonics[2][2], mnemonics[3][0]])
|
||||
identifier, exponent, ems, group_count = slip39.combine_mnemonics([mnemonics[2][0], mnemonics[2][2], mnemonics[3][0]])
|
||||
self.assertEqual(ems, self.EMS)
|
||||
self.assertEqual(slip39.combine_mnemonics([mnemonics[2][3], mnemonics[3][0], mnemonics[2][4]])[2], ems)
|
||||
self.assertEqual(group_count, len(group_sizes))
|
||||
|
||||
# One complete group and one incomplete group out of two groups required.
|
||||
with self.assertRaises(slip39.MnemonicError):
|
||||
@ -97,7 +102,7 @@ class TestCryptoSlip39(unittest.TestCase):
|
||||
member_thresholds = (3, 2, 2, 1)
|
||||
identifier = slip39.generate_random_identifier()
|
||||
mnemonics = slip39.generate_mnemonics_from_data(
|
||||
self.EMS, identifier, group_threshold, list(zip(member_thresholds, group_sizes))
|
||||
self.EMS, identifier, group_threshold, list(zip(member_thresholds, group_sizes)), 1
|
||||
)
|
||||
|
||||
# Test all valid combinations of mnemonics.
|
||||
@ -105,7 +110,8 @@ class TestCryptoSlip39(unittest.TestCase):
|
||||
for group_subset in combinations(group, threshold):
|
||||
mnemonic_subset = list(group_subset)
|
||||
random.shuffle(mnemonic_subset)
|
||||
identifier, exponent, ems = slip39.combine_mnemonics(mnemonic_subset)
|
||||
identifier, exponent, ems, group_count = slip39.combine_mnemonics(mnemonic_subset)
|
||||
self.assertEqual(group_count, len(group_sizes))
|
||||
self.assertEqual(ems, self.EMS)
|
||||
|
||||
|
||||
@ -113,7 +119,7 @@ class TestCryptoSlip39(unittest.TestCase):
|
||||
for group_threshold in (1, 2, 5):
|
||||
identifier = slip39.generate_random_identifier()
|
||||
mnemonics = slip39.generate_mnemonics_from_data(
|
||||
self.EMS, identifier, group_threshold, [(3, 5), (1, 1), (2, 3), (2, 5), (3, 5)]
|
||||
self.EMS, identifier, group_threshold, [(3, 5), (1, 1), (2, 3), (2, 5), (3, 5)], 1
|
||||
)
|
||||
self.assertEqual(len(mnemonics), 5)
|
||||
self.assertEqual(len(sum(mnemonics, [])), 19)
|
||||
@ -121,39 +127,32 @@ class TestCryptoSlip39(unittest.TestCase):
|
||||
|
||||
def test_invalid_sharing(self):
|
||||
identifier = slip39.generate_random_identifier()
|
||||
# Short master secret.
|
||||
with self.assertRaises(ValueError):
|
||||
slip39.generate_mnemonics_from_data(self.EMS[:14], identifier, 1, [(2, 3)])
|
||||
|
||||
# Odd length master secret.
|
||||
with self.assertRaises(ValueError):
|
||||
slip39.generate_mnemonics_from_data(self.EMS + b"X", identifier,1, [(2, 3)])
|
||||
|
||||
# Group threshold exceeds number of groups.
|
||||
with self.assertRaises(ValueError):
|
||||
slip39.generate_mnemonics_from_data(self.EMS, identifier, 3, [(3, 5), (2, 5)])
|
||||
slip39.generate_mnemonics_from_data(self.EMS, identifier, 3, [(3, 5), (2, 5)], 1)
|
||||
|
||||
# Invalid group threshold.
|
||||
with self.assertRaises(ValueError):
|
||||
slip39.generate_mnemonics_from_data(self.EMS, identifier, 0, [(3, 5), (2, 5)])
|
||||
slip39.generate_mnemonics_from_data(self.EMS, identifier, 0, [(3, 5), (2, 5)], 1)
|
||||
|
||||
# Member threshold exceeds number of members.
|
||||
with self.assertRaises(ValueError):
|
||||
slip39.generate_mnemonics_from_data(self.EMS, identifier, 2, [(3, 2), (2, 5)])
|
||||
slip39.generate_mnemonics_from_data(self.EMS, identifier, 2, [(3, 2), (2, 5)], 1)
|
||||
|
||||
# Invalid member threshold.
|
||||
with self.assertRaises(ValueError):
|
||||
slip39.generate_mnemonics_from_data(self.EMS, identifier, 2, [(0, 2), (2, 5)])
|
||||
slip39.generate_mnemonics_from_data(self.EMS, identifier, 2, [(0, 2), (2, 5)], 1)
|
||||
|
||||
# Group with multiple members and threshold 1.
|
||||
with self.assertRaises(ValueError):
|
||||
slip39.generate_mnemonics_from_data(self.EMS, identifier, 2, [(3, 5), (1, 3), (2, 5)])
|
||||
slip39.generate_mnemonics_from_data(self.EMS, identifier, 2, [(3, 5), (1, 3), (2, 5)], 1)
|
||||
|
||||
|
||||
def test_vectors(self):
|
||||
for mnemonics, secret in vectors:
|
||||
if secret:
|
||||
identifier, exponent, ems = slip39.combine_mnemonics(mnemonics)
|
||||
identifier, exponent, ems, _ = slip39.combine_mnemonics(mnemonics)
|
||||
self.assertEqual(slip39.decrypt(identifier, exponent, ems, b"TREZOR"), unhexlify(secret))
|
||||
else:
|
||||
with self.assertRaises(slip39.MnemonicError):
|
||||
@ -166,12 +165,12 @@ class TestCryptoSlip39(unittest.TestCase):
|
||||
"theory painting academic academic armed sweater year military elder discuss acne wildlife boring employer fused large satoshi bundle carbon diagnose anatomy hamster leaves tracks paces beyond phantom capital marvel lips brave detect luck",
|
||||
]
|
||||
for mnemonic in mnemonics:
|
||||
data = tuple(slip39.mnemonic_to_indices(mnemonic))
|
||||
self.assertEqual(slip39.rs1024_error_index(data), None)
|
||||
data = tuple(slip39._mnemonic_to_indices(mnemonic))
|
||||
self.assertEqual(slip39._rs1024_error_index(data), None)
|
||||
for i in range(len(data)):
|
||||
for _ in range(50):
|
||||
error_data = error_data = data[:i] + (data[i] ^ (random.uniform(1023) + 1), ) + data[i + 1:]
|
||||
self.assertEqual(slip39.rs1024_error_index(error_data), i)
|
||||
self.assertEqual(slip39._rs1024_error_index(error_data), i)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -117,9 +117,9 @@ CHOICE_OUTPUT_SCRIPT_TYPE = ChoiceType(
|
||||
|
||||
CHOICE_RESET_DEVICE_TYPE = ChoiceType(
|
||||
{
|
||||
"single": proto.ResetDeviceBackupType.Bip39,
|
||||
"shamir": proto.ResetDeviceBackupType.Slip39_Single_Group,
|
||||
"advanced": proto.ResetDeviceBackupType.Slip39_Multiple_Groups,
|
||||
"single": proto.BackupType.Bip39,
|
||||
"shamir": proto.BackupType.Slip39_Basic,
|
||||
"advanced": proto.BackupType.Slip39_Advanced,
|
||||
}
|
||||
)
|
||||
|
||||
@ -563,10 +563,7 @@ def reset_device(
|
||||
strength = int(strength)
|
||||
|
||||
client = connect()
|
||||
if (
|
||||
client.features.model == "1"
|
||||
and backup_type != proto.ResetDeviceBackupType.Bip39
|
||||
):
|
||||
if client.features.model == "1" and backup_type != proto.BackupType.Bip39:
|
||||
click.echo(
|
||||
"WARNING: Trezor One currently does not support Shamir backup.\n"
|
||||
"Traditional single-seed backup will be generated instead."
|
||||
|
@ -173,7 +173,7 @@ def reset(
|
||||
u2f_counter=0,
|
||||
skip_backup=False,
|
||||
no_backup=False,
|
||||
backup_type=proto.ResetDeviceBackupType.Bip39,
|
||||
backup_type=proto.BackupType.Bip39,
|
||||
):
|
||||
if client.features.initialized:
|
||||
raise RuntimeError(
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Automatically generated by pb2py
|
||||
# fmt: off
|
||||
Bip39 = 0
|
||||
Slip39_Single_Group = 1
|
||||
Slip39_Multiple_Groups = 2
|
||||
Slip39_Basic = 1
|
||||
Slip39_Advanced = 2
|
@ -7,9 +7,11 @@ if __debug__:
|
||||
from typing import Dict, List, Optional
|
||||
from typing_extensions import Literal # noqa: F401
|
||||
EnumTypeCapability = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
|
||||
EnumTypeBackupType = Literal[0, 1, 2]
|
||||
except ImportError:
|
||||
Dict, List, Optional = None, None, None # type: ignore
|
||||
EnumTypeCapability = None # type: ignore
|
||||
EnumTypeBackupType = None # type: ignore
|
||||
|
||||
|
||||
class Features(p.MessageType):
|
||||
@ -46,6 +48,7 @@ class Features(p.MessageType):
|
||||
no_backup: bool = None,
|
||||
recovery_mode: bool = None,
|
||||
capabilities: List[EnumTypeCapability] = None,
|
||||
backup_type: EnumTypeBackupType = None,
|
||||
) -> None:
|
||||
self.vendor = vendor
|
||||
self.major_version = major_version
|
||||
@ -76,6 +79,7 @@ class Features(p.MessageType):
|
||||
self.no_backup = no_backup
|
||||
self.recovery_mode = recovery_mode
|
||||
self.capabilities = capabilities if capabilities is not None else []
|
||||
self.backup_type = backup_type
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
@ -109,4 +113,5 @@ class Features(p.MessageType):
|
||||
28: ('no_backup', p.BoolType, 0),
|
||||
29: ('recovery_mode', p.BoolType, 0),
|
||||
30: ('capabilities', p.EnumType("Capability", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)), p.FLAG_REPEATED),
|
||||
31: ('backup_type', p.EnumType("BackupType", (0, 1, 2)), 0),
|
||||
}
|
||||
|
@ -6,10 +6,10 @@ if __debug__:
|
||||
try:
|
||||
from typing import Dict, List, Optional
|
||||
from typing_extensions import Literal # noqa: F401
|
||||
EnumTypeResetDeviceBackupType = Literal[0, 1, 2]
|
||||
EnumTypeBackupType = Literal[0, 1, 2]
|
||||
except ImportError:
|
||||
Dict, List, Optional = None, None, None # type: ignore
|
||||
EnumTypeResetDeviceBackupType = None # type: ignore
|
||||
EnumTypeBackupType = None # type: ignore
|
||||
|
||||
|
||||
class ResetDevice(p.MessageType):
|
||||
@ -26,7 +26,7 @@ class ResetDevice(p.MessageType):
|
||||
u2f_counter: int = None,
|
||||
skip_backup: bool = None,
|
||||
no_backup: bool = None,
|
||||
backup_type: EnumTypeResetDeviceBackupType = None,
|
||||
backup_type: EnumTypeBackupType = None,
|
||||
) -> None:
|
||||
self.display_random = display_random
|
||||
self.strength = strength
|
||||
@ -51,5 +51,5 @@ class ResetDevice(p.MessageType):
|
||||
7: ('u2f_counter', p.UVarintType, 0),
|
||||
8: ('skip_backup', p.BoolType, 0),
|
||||
9: ('no_backup', p.BoolType, 0),
|
||||
10: ('backup_type', p.EnumType("ResetDeviceBackupType", (0, 1, 2)), 0), # default=ResetDeviceBackupType_Bip39
|
||||
10: ('backup_type', p.EnumType("BackupType", (0, 1, 2)), 0), # default=Bip39
|
||||
}
|
||||
|
@ -256,6 +256,7 @@ from .WebAuthnRemoveResidentCredential import WebAuthnRemoveResidentCredential
|
||||
from .WipeDevice import WipeDevice
|
||||
from .WordAck import WordAck
|
||||
from .WordRequest import WordRequest
|
||||
from . import BackupType
|
||||
from . import BinanceOrderSide
|
||||
from . import BinanceOrderType
|
||||
from . import BinanceTimeInForce
|
||||
@ -274,7 +275,6 @@ from . import PassphraseSourceType
|
||||
from . import PinMatrixRequestType
|
||||
from . import RecoveryDeviceType
|
||||
from . import RequestType
|
||||
from . import ResetDeviceBackupType
|
||||
from . import SdProtectOperationType
|
||||
from . import TezosBallotType
|
||||
from . import TezosContractType
|
||||
|
@ -19,20 +19,24 @@ from trezorlib.messages import ButtonRequestType as B
|
||||
# fmt: off
|
||||
# 1 2 3 4 5 6 7 8 9 10 11 12
|
||||
MNEMONIC12 = "alcohol woman abuse must during monitor noble actual mixed trade anger aisle"
|
||||
# fmt: on
|
||||
|
||||
MNEMONIC_SHAMIR_20_3of6 = [
|
||||
MNEMONIC_SLIP39_BASIC_20_3of6 = [
|
||||
"extra extend academic bishop cricket bundle tofu goat apart victim enlarge program behavior permit course armed jerky faint language modern",
|
||||
"extra extend academic acne away best indicate impact square oasis prospect painting voting guest either argue username racism enemy eclipse",
|
||||
"extra extend academic arcade born dive legal hush gross briefing talent drug much home firefly toxic analysis idea umbrella slice",
|
||||
]
|
||||
MNEMONIC_SHAMIR_20_2of3_2of3_GROUPS = [
|
||||
"gesture negative ceramic leaf device fantasy style ceramic safari keyboard thumb total smug cage plunge aunt favorite lizard intend peanut",
|
||||
"gesture negative acrobat leaf craft sidewalk adorn spider submit bumpy alcohol cards salon making prune decorate smoking image corner method",
|
||||
"gesture negative acrobat lily bishop voting humidity rhyme parcel crunch elephant victim dish mailman triumph agree episode wealthy mayor beam",
|
||||
"gesture negative beard leaf deadline stadium vegan employer armed marathon alien lunar broken edge justice military endorse diet sweater either",
|
||||
"gesture negative beard lily desert belong speak realize explain bolt diet believe response counter medal luck wits glance remove ending",
|
||||
# Shamir shares (128 bits, 2 groups from 1 of 1, 1 of 1, 3 of 5, 2 of 6)
|
||||
MNEMONIC_SLIP39_ADVANCED_20 = [
|
||||
"eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice",
|
||||
"eraser senior ceramic snake clay various huge numb argue hesitate auction category timber browser greatest hanger petition script leaf pickup",
|
||||
"eraser senior ceramic shaft dynamic become junior wrist silver peasant force math alto coal amazing segment yelp velvet image paces",
|
||||
"eraser senior ceramic round column hawk trust auction smug shame alive greatest sheriff living perfect corner chest sled fumes adequate",
|
||||
]
|
||||
# Shamir shares (256 bits, 2 groups from 1 of 1, 1 of 1, 3 of 5, 2 of 6):
|
||||
MNEMONIC_SLIP39_ADVANCED_33 = [
|
||||
"wildlife deal beard romp alcohol space mild usual clothes union nuclear testify course research heat listen task location thank hospital slice smell failure fawn helpful priest ambition average recover lecture process dough stadium",
|
||||
"wildlife deal acrobat romp anxiety axis starting require metric flexible geology game drove editor edge screw helpful have huge holy making pitch unknown carve holiday numb glasses survive already tenant adapt goat fangs",
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
|
||||
def generate_entropy(strength, internal_entropy, external_entropy):
|
||||
@ -91,13 +95,6 @@ def recovery_enter_shares(debug, shares, groups=False):
|
||||
debug.press_yes()
|
||||
# Enter shares
|
||||
for index, share in enumerate(shares):
|
||||
if groups and index >= 1:
|
||||
# confirm remaining shares
|
||||
debug.swipe_down()
|
||||
code = yield
|
||||
assert code == B.Other
|
||||
debug.press_yes()
|
||||
|
||||
code = yield
|
||||
assert code == B.MnemonicInput
|
||||
# Enter mnemonic words
|
||||
|
@ -19,7 +19,7 @@ import pytest
|
||||
from trezorlib.cardano import get_address
|
||||
from trezorlib.tools import parse_path
|
||||
|
||||
from ..common import MNEMONIC_SHAMIR_20_3of6
|
||||
from ..common import MNEMONIC_SLIP39_BASIC_20_3of6
|
||||
|
||||
|
||||
@pytest.mark.altcoin
|
||||
@ -42,7 +42,7 @@ from ..common import MNEMONIC_SHAMIR_20_3of6
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.setup_client(mnemonic=MNEMONIC_SHAMIR_20_3of6, passphrase=True)
|
||||
@pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6, passphrase=True)
|
||||
def test_cardano_get_address(client, path, expected_address):
|
||||
# enter passphrase
|
||||
assert client.features.passphrase_protection is True
|
@ -19,13 +19,13 @@ import pytest
|
||||
from trezorlib.cardano import get_public_key
|
||||
from trezorlib.tools import parse_path
|
||||
|
||||
from ..common import MNEMONIC_SHAMIR_20_3of6
|
||||
from ..common import MNEMONIC_SLIP39_BASIC_20_3of6
|
||||
|
||||
|
||||
@pytest.mark.altcoin
|
||||
@pytest.mark.cardano
|
||||
@pytest.mark.skip_t1 # T1 support is not planned
|
||||
@pytest.mark.setup_client(mnemonic=MNEMONIC_SHAMIR_20_3of6, passphrase=True)
|
||||
@pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6, passphrase=True)
|
||||
@pytest.mark.parametrize(
|
||||
"path,public_key,chain_code",
|
||||
[
|
@ -18,7 +18,7 @@ import pytest
|
||||
|
||||
from trezorlib import cardano, messages
|
||||
|
||||
from ..common import MNEMONIC_SHAMIR_20_3of6
|
||||
from ..common import MNEMONIC_SLIP39_BASIC_20_3of6
|
||||
|
||||
PROTOCOL_MAGICS = {"mainnet": 764824073, "testnet": 1097911063}
|
||||
|
||||
@ -109,7 +109,7 @@ VALID_VECTORS = [
|
||||
@pytest.mark.altcoin
|
||||
@pytest.mark.cardano
|
||||
@pytest.mark.skip_t1 # T1 support is not planned
|
||||
@pytest.mark.setup_client(mnemonic=MNEMONIC_SHAMIR_20_3of6, passphrase=True)
|
||||
@pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6, passphrase=True)
|
||||
@pytest.mark.parametrize(
|
||||
"protocol_magic,inputs,outputs,transactions,tx_hash,tx_body", VALID_VECTORS
|
||||
)
|
@ -17,9 +17,14 @@
|
||||
import pytest
|
||||
|
||||
from trezorlib import btc, debuglink, device
|
||||
from trezorlib.messages import BackupType
|
||||
from trezorlib.messages.PassphraseSourceType import HOST as PASSPHRASE_ON_HOST
|
||||
|
||||
from ..common import MNEMONIC12
|
||||
from ..common import (
|
||||
MNEMONIC12,
|
||||
MNEMONIC_SLIP39_ADVANCED_20,
|
||||
MNEMONIC_SLIP39_BASIC_20_3of6,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
@ -62,6 +67,28 @@ class TestDeviceLoad:
|
||||
address = btc.get_address(client, "Bitcoin", [])
|
||||
assert address == "15fiTDFwZd2kauHYYseifGi9daH2wniDHH"
|
||||
|
||||
@pytest.mark.skip_t1
|
||||
def test_load_device_slip39_basic(self, client):
|
||||
debuglink.load_device_by_mnemonic(
|
||||
client,
|
||||
mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6,
|
||||
pin="",
|
||||
passphrase_protection=False,
|
||||
label="test",
|
||||
)
|
||||
assert client.features.backup_type == BackupType.Slip39_Basic
|
||||
|
||||
@pytest.mark.skip_t1
|
||||
def test_load_device_slip39_advanced(self, client):
|
||||
debuglink.load_device_by_mnemonic(
|
||||
client,
|
||||
mnemonic=MNEMONIC_SLIP39_ADVANCED_20,
|
||||
pin="",
|
||||
passphrase_protection=False,
|
||||
label="test",
|
||||
)
|
||||
assert client.features.backup_type == BackupType.Slip39_Advanced
|
||||
|
||||
def test_load_device_utf(self, client):
|
||||
words_nfkd = u"Pr\u030ci\u0301s\u030cerne\u030c z\u030clut\u030couc\u030cky\u0301 ku\u030an\u030c u\u0301pe\u030cl d\u030ca\u0301belske\u0301 o\u0301dy za\u0301ker\u030cny\u0301 uc\u030cen\u030c be\u030cz\u030ci\u0301 pode\u0301l zo\u0301ny u\u0301lu\u030a"
|
||||
words_nfc = u"P\u0159\xed\u0161ern\u011b \u017elu\u0165ou\u010dk\xfd k\u016f\u0148 \xfap\u011bl \u010f\xe1belsk\xe9 \xf3dy z\xe1ke\u0159n\xfd u\u010de\u0148 b\u011b\u017e\xed pod\xe9l z\xf3ny \xfal\u016f"
|
||||
|
@ -144,6 +144,7 @@ class TestMsgRecoverydeviceT2:
|
||||
|
||||
assert client.features.pin_protection is False
|
||||
assert client.features.passphrase_protection is False
|
||||
assert client.features.backup_type is proto.BackupType.Bip39
|
||||
|
||||
def test_already_initialized(self, client):
|
||||
with pytest.raises(RuntimeError):
|
215
tests/device_tests/test_msg_recoverydevice_slip39_advanced.py
Normal file
215
tests/device_tests/test_msg_recoverydevice_slip39_advanced.py
Normal file
@ -0,0 +1,215 @@
|
||||
# This file is part of the Trezor project.
|
||||
#
|
||||
# Copyright (C) 2012-2019 SatoshiLabs and contributors
|
||||
#
|
||||
# This library is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License version 3
|
||||
# as published by the Free Software Foundation.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the License along with this library.
|
||||
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||
|
||||
import pytest
|
||||
|
||||
from trezorlib import device, exceptions, messages
|
||||
|
||||
from ..common import (
|
||||
MNEMONIC_SLIP39_ADVANCED_20,
|
||||
MNEMONIC_SLIP39_ADVANCED_33,
|
||||
recovery_enter_shares,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.skip_t1
|
||||
|
||||
EXTRA_GROUP_SHARE = [
|
||||
"eraser senior decision smug corner ruin rescue cubic angel tackle skin skunk program roster trash rumor slush angel flea amazing"
|
||||
]
|
||||
|
||||
# secrets generated using model T
|
||||
VECTORS = (
|
||||
(MNEMONIC_SLIP39_ADVANCED_20, "c2d2e26ad06023c60145f150abe2dd2b"),
|
||||
(
|
||||
MNEMONIC_SLIP39_ADVANCED_33,
|
||||
"c41d5cf80fed71a008a3a0ae0458ff0c6d621b1a5522bccbfedbcfad87005c06",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shares, secret", VECTORS)
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
def test_secret(client, shares, secret):
|
||||
debug = client.debug
|
||||
|
||||
def input_flow():
|
||||
yield # Confirm Recovery
|
||||
debug.press_yes()
|
||||
# Proceed with recovery
|
||||
yield from recovery_enter_shares(debug, shares, groups=True)
|
||||
|
||||
with client:
|
||||
client.set_input_flow(input_flow)
|
||||
ret = device.recover(
|
||||
client, pin_protection=False, passphrase_protection=False, label="label"
|
||||
)
|
||||
|
||||
# Workflow succesfully ended
|
||||
assert ret == messages.Success(message="Device recovered")
|
||||
assert client.features.initialized is True
|
||||
assert client.features.pin_protection is False
|
||||
assert client.features.passphrase_protection is False
|
||||
assert client.features.backup_type is messages.BackupType.Slip39_Advanced
|
||||
assert debug.read_mnemonic_secret().hex() == secret
|
||||
|
||||
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
def test_extra_share_entered(client):
|
||||
debug = client.debug
|
||||
|
||||
def input_flow():
|
||||
yield # Confirm Recovery
|
||||
debug.press_yes()
|
||||
# Proceed with recovery
|
||||
yield from recovery_enter_shares(
|
||||
debug, EXTRA_GROUP_SHARE + MNEMONIC_SLIP39_ADVANCED_20, groups=True
|
||||
)
|
||||
|
||||
with client:
|
||||
client.set_input_flow(input_flow)
|
||||
ret = device.recover(
|
||||
client, pin_protection=False, passphrase_protection=False, label="label"
|
||||
)
|
||||
|
||||
# Workflow succesfully ended
|
||||
assert ret == messages.Success(message="Device recovered")
|
||||
assert client.features.initialized is True
|
||||
assert client.features.pin_protection is False
|
||||
assert client.features.passphrase_protection is False
|
||||
assert client.features.backup_type is messages.BackupType.Slip39_Advanced
|
||||
|
||||
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
def test_abort(client):
|
||||
debug = client.debug
|
||||
|
||||
def input_flow():
|
||||
yield # Confirm Recovery
|
||||
debug.press_yes()
|
||||
yield # Homescreen - abort process
|
||||
debug.press_no()
|
||||
yield # Homescreen - confirm abort
|
||||
debug.press_yes()
|
||||
|
||||
with client:
|
||||
client.set_input_flow(input_flow)
|
||||
with pytest.raises(exceptions.Cancelled):
|
||||
device.recover(client, pin_protection=False, label="label")
|
||||
client.init_device()
|
||||
assert client.features.initialized is False
|
||||
|
||||
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
def test_noabort(client):
|
||||
debug = client.debug
|
||||
|
||||
def input_flow():
|
||||
yield # Confirm Recovery
|
||||
debug.press_yes()
|
||||
yield # Homescreen - abort process
|
||||
debug.press_no()
|
||||
yield # Homescreen - go back to process
|
||||
debug.press_no()
|
||||
yield from recovery_enter_shares(
|
||||
debug, EXTRA_GROUP_SHARE + MNEMONIC_SLIP39_ADVANCED_20, groups=True
|
||||
)
|
||||
|
||||
with client:
|
||||
client.set_input_flow(input_flow)
|
||||
device.recover(client, pin_protection=False, label="label")
|
||||
client.init_device()
|
||||
assert client.features.initialized is True
|
||||
|
||||
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
def test_same_share(client):
|
||||
debug = client.debug
|
||||
# we choose the second share from the fixture because
|
||||
# the 1st is 1of1 and group threshold condition is reached first
|
||||
first_share = MNEMONIC_SLIP39_ADVANCED_20[1].split(" ")
|
||||
# second share is first 4 words of first
|
||||
second_share = MNEMONIC_SLIP39_ADVANCED_20[1].split(" ")[:4]
|
||||
|
||||
def input_flow():
|
||||
yield # Confirm Recovery
|
||||
debug.press_yes()
|
||||
yield # Homescreen - start process
|
||||
debug.press_yes()
|
||||
yield # Enter number of words
|
||||
debug.input(str(len(first_share)))
|
||||
yield # Homescreen - proceed to share entry
|
||||
debug.press_yes()
|
||||
yield # Enter first share
|
||||
for word in first_share:
|
||||
debug.input(word)
|
||||
|
||||
yield # Continue to next share
|
||||
debug.press_yes()
|
||||
yield # Homescreen - next share
|
||||
debug.press_yes()
|
||||
yield # Enter next share
|
||||
for word in second_share:
|
||||
debug.input(word)
|
||||
|
||||
code = yield
|
||||
assert code == messages.ButtonRequestType.Warning
|
||||
|
||||
client.cancel()
|
||||
|
||||
with client:
|
||||
client.set_input_flow(input_flow)
|
||||
with pytest.raises(exceptions.Cancelled):
|
||||
device.recover(client, pin_protection=False, label="label")
|
||||
|
||||
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
def test_group_threshold_reached(client):
|
||||
debug = client.debug
|
||||
# first share in the fixture is 1of1 so we choose that
|
||||
first_share = MNEMONIC_SLIP39_ADVANCED_20[0].split(" ")
|
||||
# second share is first 3 words of first
|
||||
second_share = MNEMONIC_SLIP39_ADVANCED_20[0].split(" ")[:3]
|
||||
|
||||
def input_flow():
|
||||
yield # Confirm Recovery
|
||||
debug.press_yes()
|
||||
yield # Homescreen - start process
|
||||
debug.press_yes()
|
||||
yield # Enter number of words
|
||||
debug.input(str(len(first_share)))
|
||||
yield # Homescreen - proceed to share entry
|
||||
debug.press_yes()
|
||||
yield # Enter first share
|
||||
for word in first_share:
|
||||
debug.input(word)
|
||||
|
||||
yield # Continue to next share
|
||||
debug.press_yes()
|
||||
yield # Homescreen - next share
|
||||
debug.press_yes()
|
||||
yield # Enter next share
|
||||
for word in second_share:
|
||||
debug.input(word)
|
||||
|
||||
code = yield
|
||||
assert code == messages.ButtonRequestType.Warning
|
||||
|
||||
client.cancel()
|
||||
|
||||
with client:
|
||||
client.set_input_flow(input_flow)
|
||||
with pytest.raises(exceptions.Cancelled):
|
||||
device.recover(client, pin_protection=False, label="label")
|
@ -19,21 +19,24 @@ import pytest
|
||||
from trezorlib import device, messages
|
||||
from trezorlib.exceptions import TrezorFailure
|
||||
|
||||
from ..common import MNEMONIC_SHAMIR_20_2of3_2of3_GROUPS, recovery_enter_shares
|
||||
from ..common import MNEMONIC_SLIP39_ADVANCED_20, recovery_enter_shares
|
||||
|
||||
pytestmark = pytest.mark.skip_t1
|
||||
|
||||
INVALID_SHARES_20_2of3_2of3_GROUPS = [
|
||||
INVALID_SHARES_SLIP39_ADVANCED_20 = [
|
||||
"chest garlic acrobat leaf diploma thank soul predator grant laundry camera license language likely slim twice amount rich total carve",
|
||||
"chest garlic acrobat lily adequate dwarf genius wolf faint nylon scroll national necklace leader pants literary lift axle watch midst",
|
||||
"chest garlic beard leaf coastal album dramatic learn identify angry dismiss goat plan describe round writing primary surprise sprinkle orbit",
|
||||
"chest garlic beard lily burden pistol retreat pickup emphasis large gesture hand eyebrow season pleasure genuine election skunk champion income",
|
||||
]
|
||||
|
||||
# Extra share from another group to make sure it does not matter.
|
||||
EXTRA_GROUP_SHARE = [
|
||||
"eraser senior decision smug corner ruin rescue cubic angel tackle skin skunk program roster trash rumor slush angel flea amazing"
|
||||
]
|
||||
|
||||
@pytest.mark.setup_client(
|
||||
mnemonic=MNEMONIC_SHAMIR_20_2of3_2of3_GROUPS[1:5], passphrase=False
|
||||
)
|
||||
|
||||
@pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_ADVANCED_20, passphrase=False)
|
||||
def test_2of3_dryrun(client):
|
||||
debug = client.debug
|
||||
|
||||
@ -42,7 +45,7 @@ def test_2of3_dryrun(client):
|
||||
debug.press_yes()
|
||||
# run recovery flow
|
||||
yield from recovery_enter_shares(
|
||||
debug, MNEMONIC_SHAMIR_20_2of3_2of3_GROUPS, groups=True
|
||||
debug, EXTRA_GROUP_SHARE + MNEMONIC_SLIP39_ADVANCED_20, groups=True
|
||||
)
|
||||
|
||||
with client:
|
||||
@ -62,9 +65,7 @@ def test_2of3_dryrun(client):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.setup_client(
|
||||
mnemonic=MNEMONIC_SHAMIR_20_2of3_2of3_GROUPS[1:5], passphrase=True
|
||||
)
|
||||
@pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_ADVANCED_20, passphrase=True)
|
||||
def test_2of3_invalid_seed_dryrun(client):
|
||||
debug = client.debug
|
||||
|
||||
@ -73,7 +74,7 @@ def test_2of3_invalid_seed_dryrun(client):
|
||||
debug.press_yes()
|
||||
# run recovery flow
|
||||
yield from recovery_enter_shares(
|
||||
debug, INVALID_SHARES_20_2of3_2of3_GROUPS, groups=True
|
||||
debug, INVALID_SHARES_SLIP39_ADVANCED_20, groups=True
|
||||
)
|
||||
|
||||
# test fails because of different seed on device
|
@ -18,20 +18,24 @@ import pytest
|
||||
|
||||
from trezorlib import device, exceptions, messages
|
||||
|
||||
from ..common import MNEMONIC_SHAMIR_20_3of6, recovery_enter_shares
|
||||
from ..common import MNEMONIC_SLIP39_BASIC_20_3of6, recovery_enter_shares
|
||||
|
||||
pytestmark = pytest.mark.skip_t1
|
||||
|
||||
MNEMONIC_SLIP39_BASIC_20_1of1 = [
|
||||
"academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic rebuild aquatic spew"
|
||||
]
|
||||
|
||||
MNEMONIC_SHAMIR_33_2of5 = [
|
||||
|
||||
MNEMONIC_SLIP39_BASIC_33_2of5 = [
|
||||
"hobo romp academic axis august founder knife legal recover alien expect emphasis loan kitchen involve teacher capture rebuild trial numb spider forward ladle lying voter typical security quantity hawk legs idle leaves gasoline",
|
||||
"hobo romp academic agency ancestor industry argue sister scene midst graduate profile numb paid headset airport daisy flame express scene usual welcome quick silent downtown oral critical step remove says rhythm venture aunt",
|
||||
]
|
||||
|
||||
VECTORS = (
|
||||
(MNEMONIC_SHAMIR_20_3of6, "491b795b80fc21ccdf466c0fbc98c8fc"),
|
||||
(MNEMONIC_SLIP39_BASIC_20_3of6, "491b795b80fc21ccdf466c0fbc98c8fc"),
|
||||
(
|
||||
MNEMONIC_SHAMIR_33_2of5,
|
||||
MNEMONIC_SLIP39_BASIC_33_2of5,
|
||||
"b770e0da1363247652de97a39bdbf2463be087848d709ecbf28e84508e31202a",
|
||||
),
|
||||
)
|
||||
@ -56,6 +60,7 @@ def test_secret(client, shares, secret):
|
||||
assert ret == messages.Success(message="Device recovered")
|
||||
assert client.features.pin_protection is False
|
||||
assert client.features.passphrase_protection is False
|
||||
assert client.features.backup_type is messages.BackupType.Slip39_Basic
|
||||
|
||||
# Check mnemonic
|
||||
assert debug.read_mnemonic_secret().hex() == secret
|
||||
@ -73,7 +78,7 @@ def test_recover_with_pin_passphrase(client):
|
||||
yield # Enter PIN again
|
||||
debug.input("654")
|
||||
# Proceed with recovery
|
||||
yield from recovery_enter_shares(debug, MNEMONIC_SHAMIR_20_3of6)
|
||||
yield from recovery_enter_shares(debug, MNEMONIC_SLIP39_BASIC_20_3of6)
|
||||
|
||||
with client:
|
||||
client.set_input_flow(input_flow)
|
||||
@ -85,6 +90,7 @@ def test_recover_with_pin_passphrase(client):
|
||||
assert ret == messages.Success(message="Device recovered")
|
||||
assert client.features.pin_protection is True
|
||||
assert client.features.passphrase_protection is True
|
||||
assert client.features.backup_type is messages.BackupType.Slip39_Basic
|
||||
|
||||
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
@ -118,7 +124,7 @@ def test_noabort(client):
|
||||
debug.press_no()
|
||||
yield # Homescreen - go back to process
|
||||
debug.press_no()
|
||||
yield from recovery_enter_shares(debug, MNEMONIC_SHAMIR_20_3of6)
|
||||
yield from recovery_enter_shares(debug, MNEMONIC_SLIP39_BASIC_20_3of6)
|
||||
|
||||
with client:
|
||||
client.set_input_flow(input_flow)
|
||||
@ -131,7 +137,7 @@ def test_noabort(client):
|
||||
@pytest.mark.parametrize("nth_word", range(3))
|
||||
def test_wrong_nth_word(client, nth_word):
|
||||
debug = client.debug
|
||||
share = MNEMONIC_SHAMIR_20_3of6[0].split(" ")
|
||||
share = MNEMONIC_SLIP39_BASIC_20_3of6[0].split(" ")
|
||||
|
||||
def input_flow():
|
||||
yield # Confirm Recovery
|
||||
@ -170,9 +176,9 @@ def test_wrong_nth_word(client, nth_word):
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
def test_same_share(client):
|
||||
debug = client.debug
|
||||
first_share = MNEMONIC_SHAMIR_20_3of6[0].split(" ")
|
||||
first_share = MNEMONIC_SLIP39_BASIC_20_3of6[0].split(" ")
|
||||
# second share is first 4 words of first
|
||||
second_share = MNEMONIC_SHAMIR_20_3of6[0].split(" ")[:4]
|
||||
second_share = MNEMONIC_SLIP39_BASIC_20_3of6[0].split(" ")[:4]
|
||||
|
||||
def input_flow():
|
||||
yield # Confirm Recovery
|
||||
@ -202,3 +208,29 @@ def test_same_share(client):
|
||||
client.set_input_flow(input_flow)
|
||||
with pytest.raises(exceptions.Cancelled):
|
||||
device.recover(client, pin_protection=False, label="label")
|
||||
|
||||
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
def test_1of1(client):
|
||||
debug = client.debug
|
||||
|
||||
def input_flow():
|
||||
yield # Confirm Recovery
|
||||
debug.press_yes()
|
||||
# Proceed with recovery
|
||||
yield from recovery_enter_shares(
|
||||
debug, MNEMONIC_SLIP39_BASIC_20_1of1, groups=False
|
||||
)
|
||||
|
||||
with client:
|
||||
client.set_input_flow(input_flow)
|
||||
ret = device.recover(
|
||||
client, pin_protection=False, passphrase_protection=False, label="label"
|
||||
)
|
||||
|
||||
# Workflow succesfully ended
|
||||
assert ret == messages.Success(message="Device recovered")
|
||||
assert client.features.initialized is True
|
||||
assert client.features.pin_protection is False
|
||||
assert client.features.passphrase_protection is False
|
||||
assert client.features.backup_type is messages.BackupType.Slip39_Basic
|
@ -54,7 +54,6 @@ def test_2of3_dryrun(client):
|
||||
label="label",
|
||||
language="english",
|
||||
dry_run=True,
|
||||
type=messages.ResetDeviceBackupType.Slip39_Single_Group,
|
||||
)
|
||||
|
||||
# Dry run was successful
|
||||
@ -85,5 +84,4 @@ def test_2of3_invalid_seed_dryrun(client):
|
||||
label="label",
|
||||
language="english",
|
||||
dry_run=True,
|
||||
type=messages.ResetDeviceBackupType.Slip39_Single_Group,
|
||||
)
|
@ -1,90 +0,0 @@
|
||||
# This file is part of the Trezor project.
|
||||
#
|
||||
# Copyright (C) 2012-2019 SatoshiLabs and contributors
|
||||
#
|
||||
# This library is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License version 3
|
||||
# as published by the Free Software Foundation.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the License along with this library.
|
||||
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||
|
||||
import pytest
|
||||
|
||||
from trezorlib import device, exceptions, messages
|
||||
|
||||
from ..common import MNEMONIC_SHAMIR_20_2of3_2of3_GROUPS, recovery_enter_shares
|
||||
|
||||
pytestmark = pytest.mark.skip_t1
|
||||
|
||||
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
def test_recover_no_pin_no_passphrase(client):
|
||||
debug = client.debug
|
||||
|
||||
def input_flow():
|
||||
yield # Confirm Recovery
|
||||
debug.press_yes()
|
||||
# Proceed with recovery
|
||||
yield from recovery_enter_shares(
|
||||
debug, MNEMONIC_SHAMIR_20_2of3_2of3_GROUPS, groups=True
|
||||
)
|
||||
|
||||
with client:
|
||||
client.set_input_flow(input_flow)
|
||||
ret = device.recover(
|
||||
client, pin_protection=False, passphrase_protection=False, label="label"
|
||||
)
|
||||
|
||||
# Workflow succesfully ended
|
||||
assert ret == messages.Success(message="Device recovered")
|
||||
assert client.features.initialized is True
|
||||
assert client.features.pin_protection is False
|
||||
assert client.features.passphrase_protection is False
|
||||
|
||||
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
def test_abort(client):
|
||||
debug = client.debug
|
||||
|
||||
def input_flow():
|
||||
yield # Confirm Recovery
|
||||
debug.press_yes()
|
||||
yield # Homescreen - abort process
|
||||
debug.press_no()
|
||||
yield # Homescreen - confirm abort
|
||||
debug.press_yes()
|
||||
|
||||
with client:
|
||||
client.set_input_flow(input_flow)
|
||||
with pytest.raises(exceptions.Cancelled):
|
||||
device.recover(client, pin_protection=False, label="label")
|
||||
client.init_device()
|
||||
assert client.features.initialized is False
|
||||
|
||||
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
def test_noabort(client):
|
||||
debug = client.debug
|
||||
|
||||
def input_flow():
|
||||
yield # Confirm Recovery
|
||||
debug.press_yes()
|
||||
yield # Homescreen - abort process
|
||||
debug.press_no()
|
||||
yield # Homescreen - go back to process
|
||||
debug.press_no()
|
||||
yield from recovery_enter_shares(
|
||||
debug, MNEMONIC_SHAMIR_20_2of3_2of3_GROUPS, groups=True
|
||||
)
|
||||
|
||||
with client:
|
||||
client.set_input_flow(input_flow)
|
||||
device.recover(client, pin_protection=False, label="label")
|
||||
client.init_device()
|
||||
assert client.features.initialized is True
|
@ -103,6 +103,7 @@ class TestMsgResetDeviceT2:
|
||||
assert resp.needs_backup is False
|
||||
assert resp.pin_protection is False
|
||||
assert resp.passphrase_protection is False
|
||||
assert resp.backup_type is proto.BackupType.Bip39
|
||||
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
def test_reset_device_pin(self, client):
|
@ -20,7 +20,7 @@ import pytest
|
||||
import shamir_mnemonic as shamir
|
||||
|
||||
from trezorlib import device, messages as proto
|
||||
from trezorlib.messages import ButtonRequestType as B, ResetDeviceBackupType
|
||||
from trezorlib.messages import BackupType, ButtonRequestType as B
|
||||
|
||||
from ..common import click_through, generate_entropy, read_and_confirm_mnemonic
|
||||
|
||||
@ -31,7 +31,7 @@ EXTERNAL_ENTROPY = b"zlutoucky kun upel divoke ody" * 2
|
||||
class TestMsgResetDeviceT2:
|
||||
# TODO: test with different options
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
def test_reset_device_supershamir(self, client):
|
||||
def test_reset_device_slip39_advanced(self, client):
|
||||
strength = 128
|
||||
word_count = 20
|
||||
member_threshold = 3
|
||||
@ -56,7 +56,7 @@ class TestMsgResetDeviceT2:
|
||||
for h in range(5):
|
||||
# mnemonic phrases
|
||||
btn_code = yield
|
||||
assert btn_code == B.Other
|
||||
assert btn_code == B.ResetDevice
|
||||
mnemonic = read_and_confirm_mnemonic(client.debug, words=word_count)
|
||||
all_mnemonics.append(mnemonic)
|
||||
|
||||
@ -93,55 +93,55 @@ class TestMsgResetDeviceT2:
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.ResetDevice), # group #5 counts
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Other), # show seeds
|
||||
proto.ButtonRequest(code=B.ResetDevice), # show seeds
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success), # show seeds ends here
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.Success(),
|
||||
@ -159,7 +159,7 @@ class TestMsgResetDeviceT2:
|
||||
pin_protection=False,
|
||||
label="test",
|
||||
language="english",
|
||||
backup_type=ResetDeviceBackupType.Slip39_Multiple_Groups,
|
||||
backup_type=BackupType.Slip39_Advanced,
|
||||
)
|
||||
|
||||
# generate secret locally
|
||||
@ -174,6 +174,7 @@ class TestMsgResetDeviceT2:
|
||||
assert client.features.needs_backup is False
|
||||
assert client.features.pin_protection is False
|
||||
assert client.features.passphrase_protection is False
|
||||
assert client.features.backup_type is BackupType.Slip39_Advanced
|
||||
|
||||
|
||||
def validate_mnemonics(mnemonics, threshold, expected_ems):
|
@ -22,7 +22,7 @@ import shamir_mnemonic as shamir
|
||||
from shamir_mnemonic import MnemonicError
|
||||
|
||||
from trezorlib import device, messages as proto
|
||||
from trezorlib.messages import ButtonRequestType as B, ResetDeviceBackupType
|
||||
from trezorlib.messages import BackupType, ButtonRequestType as B
|
||||
|
||||
from ..common import click_through, generate_entropy, read_and_confirm_mnemonic
|
||||
|
||||
@ -33,7 +33,7 @@ EXTERNAL_ENTROPY = b"zlutoucky kun upel divoke ody" * 2
|
||||
class TestMsgResetDeviceT2:
|
||||
# TODO: test with different options
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
def test_reset_device_shamir(self, client):
|
||||
def test_reset_device_slip39_basic(self, client):
|
||||
strength = 128
|
||||
member_threshold = 3
|
||||
all_mnemonics = []
|
||||
@ -53,7 +53,7 @@ class TestMsgResetDeviceT2:
|
||||
for h in range(5):
|
||||
# mnemonic phrases
|
||||
btn_code = yield
|
||||
assert btn_code == B.Other
|
||||
assert btn_code == B.ResetDevice
|
||||
mnemonic = read_and_confirm_mnemonic(client.debug, words=20)
|
||||
all_mnemonics.append(mnemonic)
|
||||
|
||||
@ -80,15 +80,15 @@ class TestMsgResetDeviceT2:
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Other),
|
||||
proto.ButtonRequest(code=B.ResetDevice),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.ButtonRequest(code=B.Success),
|
||||
proto.Success(),
|
||||
@ -106,7 +106,7 @@ class TestMsgResetDeviceT2:
|
||||
pin_protection=False,
|
||||
label="test",
|
||||
language="english",
|
||||
backup_type=ResetDeviceBackupType.Slip39_Single_Group,
|
||||
backup_type=BackupType.Slip39_Basic,
|
||||
)
|
||||
|
||||
# generate secret locally
|
||||
@ -121,6 +121,7 @@ class TestMsgResetDeviceT2:
|
||||
assert client.features.needs_backup is False
|
||||
assert client.features.pin_protection is False
|
||||
assert client.features.passphrase_protection is False
|
||||
assert client.features.backup_type is BackupType.Slip39_Basic
|
||||
|
||||
|
||||
def validate_mnemonics(mnemonics, threshold, expected_ems):
|
59
tests/device_tests/test_passphrase_slip39_advanced.py
Normal file
59
tests/device_tests/test_passphrase_slip39_advanced.py
Normal file
@ -0,0 +1,59 @@
|
||||
# This file is part of the Trezor project.
|
||||
#
|
||||
# Copyright (C) 2012-2019 SatoshiLabs and contributors
|
||||
#
|
||||
# This library is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License version 3
|
||||
# as published by the Free Software Foundation.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the License along with this library.
|
||||
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||
|
||||
import pytest
|
||||
|
||||
from trezorlib import btc
|
||||
|
||||
from ..common import MNEMONIC_SLIP39_ADVANCED_20, MNEMONIC_SLIP39_ADVANCED_33
|
||||
|
||||
|
||||
@pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_ADVANCED_20, passphrase=True)
|
||||
@pytest.mark.skip_t1
|
||||
def test_128bit_passphrase(client):
|
||||
"""
|
||||
BIP32 Root Key for passphrase TREZOR:
|
||||
provided by Andrew, address calculated using Model T
|
||||
xprv9s21ZrQH143K3dzDLfeY3cMp23u5vDeFYftu5RPYZPucKc99mNEddU4w99GxdgUGcSfMpVDxhnR1XpJzZNXRN1m6xNgnzFS5MwMP6QyBRKV
|
||||
"""
|
||||
assert client.features.passphrase_protection is True
|
||||
client.set_passphrase("TREZOR")
|
||||
address = btc.get_address(client, "Bitcoin", [])
|
||||
assert address == "1CX5rv2vbSV8YFAZEAdMwRVqbxxswPnSPw"
|
||||
client.state = None
|
||||
client.clear_session()
|
||||
client.set_passphrase("ROZERT")
|
||||
address_compare = btc.get_address(client, "Bitcoin", [])
|
||||
assert address != address_compare
|
||||
|
||||
|
||||
@pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_ADVANCED_33, passphrase=True)
|
||||
@pytest.mark.skip_t1
|
||||
def test_256bit_passphrase(client):
|
||||
"""
|
||||
BIP32 Root Key for passphrase TREZOR:
|
||||
provided by Andrew, address calculated using Model T
|
||||
xprv9s21ZrQH143K2UspC9FRPfQC9NcDB4HPkx1XG9UEtuceYtpcCZ6ypNZWdgfxQ9dAFVeD1F4Zg4roY7nZm2LB7THPD6kaCege3M7EuS8v85c
|
||||
"""
|
||||
assert client.features.passphrase_protection is True
|
||||
client.set_passphrase("TREZOR")
|
||||
address = btc.get_address(client, "Bitcoin", [])
|
||||
assert address == "18oNx6UczHWASBQXc5XQqdSdAAZyhUwdQU"
|
||||
client.state = None
|
||||
client.clear_session()
|
||||
client.set_passphrase("ROZERT")
|
||||
address_compare = btc.get_address(client, "Bitcoin", [])
|
||||
assert address != address_compare
|
@ -18,10 +18,10 @@ import pytest
|
||||
|
||||
from trezorlib import btc
|
||||
|
||||
from ..common import MNEMONIC_SHAMIR_20_3of6
|
||||
from ..common import MNEMONIC_SLIP39_BASIC_20_3of6
|
||||
|
||||
|
||||
@pytest.mark.setup_client(mnemonic=MNEMONIC_SHAMIR_20_3of6, passphrase=True)
|
||||
@pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6, passphrase=True)
|
||||
@pytest.mark.skip_t1
|
||||
def test_3of6_passphrase(client):
|
||||
"""
|
@ -17,7 +17,7 @@
|
||||
import pytest
|
||||
|
||||
from trezorlib import btc, device, messages
|
||||
from trezorlib.messages import ButtonRequestType as B, ResetDeviceBackupType
|
||||
from trezorlib.messages import BackupType, ButtonRequestType as B
|
||||
from trezorlib.tools import parse_path
|
||||
|
||||
from ..common import click_through, read_and_confirm_mnemonic, recovery_enter_shares
|
||||
@ -28,22 +28,26 @@ from ..common import click_through, read_and_confirm_mnemonic, recovery_enter_sh
|
||||
def test_reset_recovery(client):
|
||||
mnemonics = reset(client)
|
||||
address_before = btc.get_address(client, "Bitcoin", parse_path("44'/0'/0'/0/0"))
|
||||
# TODO: more combinations
|
||||
selected_mnemonics = [
|
||||
mnemonics[0],
|
||||
mnemonics[1],
|
||||
mnemonics[2],
|
||||
mnemonics[5],
|
||||
mnemonics[6],
|
||||
mnemonics[7],
|
||||
mnemonics[10],
|
||||
mnemonics[11],
|
||||
mnemonics[12],
|
||||
# we're generating 3of5 groups 3of5 shares each
|
||||
test_combinations = [
|
||||
mnemonics[0:3] # shares 1-3 from groups 1-3
|
||||
+ mnemonics[5:8]
|
||||
+ mnemonics[10:13],
|
||||
mnemonics[2:5] # shares 3-5 from groups 1-3
|
||||
+ mnemonics[7:10]
|
||||
+ mnemonics[12:15],
|
||||
mnemonics[10:13] # shares 1-3 from groups 3-5
|
||||
+ mnemonics[15:18]
|
||||
+ mnemonics[20:23],
|
||||
mnemonics[12:15] # shares 3-5 from groups 3-5
|
||||
+ mnemonics[17:20]
|
||||
+ mnemonics[22:25],
|
||||
]
|
||||
device.wipe(client)
|
||||
recover(client, selected_mnemonics)
|
||||
address_after = btc.get_address(client, "Bitcoin", parse_path("44'/0'/0'/0/0"))
|
||||
assert address_before == address_after
|
||||
for combination in test_combinations:
|
||||
device.wipe(client)
|
||||
recover(client, combination)
|
||||
address_after = btc.get_address(client, "Bitcoin", parse_path("44'/0'/0'/0/0"))
|
||||
assert address_before == address_after
|
||||
|
||||
|
||||
def reset(client, strength=128):
|
||||
@ -71,7 +75,7 @@ def reset(client, strength=128):
|
||||
for h in range(5):
|
||||
# mnemonic phrases
|
||||
btn_code = yield
|
||||
assert btn_code == B.Other
|
||||
assert btn_code == B.ResetDevice
|
||||
mnemonic = read_and_confirm_mnemonic(client.debug, words=word_count)
|
||||
all_mnemonics.append(mnemonic)
|
||||
|
||||
@ -107,55 +111,55 @@ def reset(client, strength=128):
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.ResetDevice), # group #5 counts
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Other), # show seeds
|
||||
messages.ButtonRequest(code=B.ResetDevice), # show seeds
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success), # show seeds ends here
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.Success(),
|
||||
@ -173,7 +177,7 @@ def reset(client, strength=128):
|
||||
pin_protection=False,
|
||||
label="test",
|
||||
language="english",
|
||||
backup_type=ResetDeviceBackupType.Slip39_Multiple_Groups,
|
||||
backup_type=BackupType.Slip39_Advanced,
|
||||
)
|
||||
client.set_input_flow(None)
|
||||
|
@ -19,7 +19,7 @@ import itertools
|
||||
import pytest
|
||||
|
||||
from trezorlib import btc, device, messages
|
||||
from trezorlib.messages import ButtonRequestType as B, ResetDeviceBackupType
|
||||
from trezorlib.messages import BackupType, ButtonRequestType as B
|
||||
from trezorlib.tools import parse_path
|
||||
|
||||
from ..common import click_through, read_and_confirm_mnemonic, recovery_enter_shares
|
||||
@ -60,7 +60,7 @@ def reset(client, strength=128):
|
||||
for h in range(5):
|
||||
# mnemonic phrases
|
||||
btn_code = yield
|
||||
assert btn_code == B.Other
|
||||
assert btn_code == B.ResetDevice
|
||||
mnemonic = read_and_confirm_mnemonic(client.debug, words=word_count)
|
||||
all_mnemonics.append(mnemonic)
|
||||
|
||||
@ -86,15 +86,15 @@ def reset(client, strength=128):
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Other),
|
||||
messages.ButtonRequest(code=B.ResetDevice),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.ButtonRequest(code=B.Success),
|
||||
messages.Success(),
|
||||
@ -112,7 +112,7 @@ def reset(client, strength=128):
|
||||
pin_protection=False,
|
||||
label="test",
|
||||
language="english",
|
||||
backup_type=ResetDeviceBackupType.Slip39_Single_Group,
|
||||
backup_type=BackupType.Slip39_Basic,
|
||||
)
|
||||
|
||||
client.set_input_flow(None)
|
Loading…
Reference in New Issue
Block a user