Merge pull request #803 from trezor/passphrase

Passphrase Redesign
pull/846/head
Tomas Susanka 4 years ago committed by GitHub
commit 2c0504ad1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -62,11 +62,12 @@ message ButtonRequest {
ButtonRequest_PublicKey = 11;
ButtonRequest_MnemonicWordCount = 12;
ButtonRequest_MnemonicInput = 13;
ButtonRequest_PassphraseType = 14;
// ButtonRequest_PassphraseType = 14; DEPRECATED
ButtonRequest_UnknownDerivationPath = 15;
ButtonRequest_RecoveryHomepage = 16;
ButtonRequest_Success = 17;
ButtonRequest_Warning = 18;
ButtonRequest_PassphraseEntry = 19;
}
}
@ -110,31 +111,36 @@ message PinMatrixAck {
* @next PassphraseAck
*/
message PassphraseRequest {
optional bool on_device = 1; // passphrase is being entered on the device
optional bool _on_device = 1 [deprecated=true]; // <2.3.0
}
/**
* Request: Send passphrase back
* @next PassphraseStateRequest
* @auxend
*/
message PassphraseAck {
optional string passphrase = 1;
optional bytes state = 2; // expected device state
optional bytes _state = 2 [deprecated=true]; // <2.3.0
optional bool on_device = 3; // user wants to enter passphrase on the device
}
/**
* Response: Device awaits passphrase state
* @next PassphraseStateAck
* Deprecated in 2.3.0
* @next Deprecated_PassphraseStateAck
*/
message PassphraseStateRequest {
message Deprecated_PassphraseStateRequest {
option deprecated = true;
optional bytes state = 1; // actual device state
}
/**
* Request: Send passphrase state back
* Deprecated in 2.3.0
* @auxend
*/
message PassphraseStateAck {
message Deprecated_PassphraseStateAck {
option deprecated = true;
}
/**

@ -20,8 +20,7 @@ enum BackupType {
* @next Features
*/
message Initialize {
optional bytes state = 1; // assumed device state, clear session if set and different
optional bool skip_passphrase = 2; // this session should always assume empty passphrase
optional bytes session_id = 1; // assumed device session id; Trezor clears caches if it is different or empty
}
/**
@ -52,7 +51,7 @@ message Features {
optional bytes bootloader_hash = 14; // hash of the bootloader
optional bool imported = 15; // was storage imported from an external source?
optional bool pin_cached = 16; // is PIN already cached in session?
optional bool passphrase_cached = 17; // is passphrase already cached in session?
// optional bool passphrase_cached = 17; // is passphrase already cached in session? DEPRECATED
optional bool firmware_present = 18; // is valid firmware loaded?
optional bool needs_backup = 19; // does storage need backup? (equals to Storage.needs_backup)
optional uint32 flags = 20; // device flags (equals to Storage.flags)
@ -83,11 +82,14 @@ message Features {
Capability_U2F = 14;
Capability_Shamir = 15;
Capability_ShamirGroups = 16;
Capability_PassphraseEntry = 17; // the device is capable of passphrase entry directly on the device
}
optional BackupType backup_type = 31; // type of device backup (BIP-39 / SLIP-39 basic / SLIP-39 advanced)
optional bool sd_card_present = 32; // is SD card present
optional bool sd_protection = 33; // is SD Protect enabled
optional bool wipe_code_protection = 34; // is wipe code protection enabled
optional bytes session_id = 35;
optional bool passphrase_always_on_device = 36; // device enforces passphrase entry on Trezor
}
/**
@ -109,17 +111,10 @@ message ApplySettings {
optional string label = 2;
optional bool use_passphrase = 3;
optional bytes homescreen = 4;
optional PassphraseSourceType passphrase_source = 5;
// optional PassphraseSourceType passphrase_source = 5; DEPRECATED
optional uint32 auto_lock_delay_ms = 6;
optional uint32 display_rotation = 7; // in degrees from North
/**
* Structure representing passphrase source
*/
enum PassphraseSourceType {
ASK = 0;
DEVICE = 1;
HOST = 2;
}
optional bool passphrase_always_on_device = 8; // do not prompt for passphrase, enforce device entry
}
/**
@ -178,8 +173,6 @@ message SdProtect {
message Ping {
optional string message = 1; // message to send back in Success message
optional bool button_protection = 2; // ask for button press
optional bool pin_protection = 3; // ask for PIN if set in device
optional bool passphrase_protection = 4; // ask for passphrase if set in device
}
/**

@ -54,8 +54,6 @@ enum MessageType {
MessageType_EntropyAck = 36 [(wire_in) = true];
MessageType_PassphraseRequest = 41 [(wire_out) = true];
MessageType_PassphraseAck = 42 [(wire_in) = true, (wire_tiny) = true, (wire_no_fsm) = true];
MessageType_PassphraseStateRequest = 77 [(wire_out) = true];
MessageType_PassphraseStateAck = 78 [(wire_in) = true, (wire_tiny) = true, (wire_no_fsm) = true];
MessageType_RecoveryDevice = 45 [(wire_in) = true];
MessageType_WordRequest = 46 [(wire_out) = true];
MessageType_WordAck = 47 [(wire_in) = true];
@ -66,6 +64,11 @@ enum MessageType {
MessageType_NextU2FCounter = 81 [(wire_out) = true];
MessageType_ChangeWipeCode = 82 [(wire_in) = true];
// Deprecated messages, kept for protobuf compatibility.
// Both are marked wire_out so that we don't need to implement incoming handler for legacy
MessageType_Deprecated_PassphraseStateRequest = 77 [deprecated = true];
MessageType_Deprecated_PassphraseStateAck = 78 [deprecated = true];
// Bootloader
MessageType_FirmwareErase = 6 [(wire_in) = true, (wire_bootloader) = true];
MessageType_FirmwareUpload = 7 [(wire_in) = true, (wire_bootloader) = true];

@ -2,6 +2,9 @@ Version 2.x.x [not yet released]
* SD card protection
* Upgrade MicroPython to 1.12
Version 2.3.0 [not yet released]
* Passphrase redesign
Version 2.2.0 [Jan 2020]
* Remove unused ButtonRequest.data field
* Rework Recovery persistence internally

@ -1,6 +1,6 @@
#define VERSION_MAJOR 2
#define VERSION_MINOR 2
#define VERSION_PATCH 1
#define VERSION_MINOR 3
#define VERSION_PATCH 0
#define VERSION_BUILD 0
#define FIX_VERSION_MAJOR 2

@ -1,11 +1,11 @@
import storage
import storage.cache
from storage import cache
from trezor import wire
from trezor.crypto import bip32
from apps.cardano import CURVE, SEED_NAMESPACE
from apps.common import mnemonic
from apps.common.request_passphrase import protect_by_passphrase
from apps.common.passphrase import get as get_passphrase
class Keychain:
@ -30,34 +30,27 @@ class Keychain:
return node
async def _get_passphrase(ctx: wire.Context) -> bytes:
passphrase = storage.cache.get_passphrase()
if passphrase is None:
passphrase = await protect_by_passphrase(ctx)
storage.cache.set_passphrase(passphrase)
return passphrase
async def get_keychain(ctx: wire.Context) -> Keychain:
root = cache.get(cache.APP_CARDANO_ROOT)
if not storage.is_initialized():
raise wire.NotInitialized("Device is not initialized")
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 = storage.cache.get_seed()
if seed is None:
passphrase = await _get_passphrase(ctx)
if root is None:
passphrase = await get_passphrase(ctx)
if mnemonic.is_bip39():
# derive the root node from mnemonic and passphrase
root = bip32.from_mnemonic_cardano(
mnemonic.get_secret().decode(), passphrase
)
else:
seed = mnemonic.get_seed(passphrase)
storage.cache.set_seed(seed)
root = bip32.from_seed(seed, "ed25519 cardano seed")
root = bip32.from_seed(seed, "ed25519 cardano seed")
# derive the namespaced root node
for i in SEED_NAMESPACE:
root.derive_cardano(i)
# derive the namespaced root node
for i in SEED_NAMESPACE:
root.derive_cardano(i)
storage.cache.set(cache.APP_CARDANO_ROOT, root)
keychain = Keychain(SEED_NAMESPACE, root)
return keychain

@ -2,6 +2,7 @@ import storage.device
from trezor import ui, utils, workflow
from trezor.crypto import bip39, slip39
from trezor.messages import BackupType
from trezor.ui.text import Text
if False:
from typing import Optional, Tuple
@ -59,11 +60,8 @@ def _start_progress() -> None:
# should make sure that no other layout is running. At this point, only
# the homescreen should be on, so shut it down.
workflow.kill_default()
ui.backlight_fade(ui.BACKLIGHT_DIM)
ui.display.clear()
ui.header("Please wait")
ui.refresh()
ui.backlight_fade(ui.BACKLIGHT_NORMAL)
t = Text("Please wait", ui.ICON_CONFIG)
ui.draw_simple(t)
def _render_progress(progress: int, total: int) -> None:

@ -0,0 +1,78 @@
from micropython import const
import storage.device
from trezor import wire
from trezor.messages import ButtonRequestType
from trezor.messages.ButtonAck import ButtonAck
from trezor.messages.ButtonRequest import ButtonRequest
from trezor.messages.PassphraseAck import PassphraseAck
from trezor.messages.PassphraseRequest import PassphraseRequest
from trezor.ui import ICON_CONFIG, draw_simple
from trezor.ui.passphrase import CANCELLED, PassphraseKeyboard
from trezor.ui.text import Text
if __debug__:
from apps.debug import input_signal
_MAX_PASSPHRASE_LEN = const(50)
def is_enabled() -> bool:
return storage.device.is_passphrase_enabled()
async def get(ctx: wire.Context) -> str:
if is_enabled():
return await _request_from_user(ctx)
else:
return ""
async def _request_from_user(ctx: wire.Context) -> str:
if storage.device.get_passphrase_always_on_device():
passphrase = await _request_on_device(ctx)
else:
passphrase = await _request_on_host(ctx)
if len(passphrase) > _MAX_PASSPHRASE_LEN:
raise wire.DataError("Maximum passphrase length is %d" % _MAX_PASSPHRASE_LEN)
return passphrase
async def _request_on_host(ctx: wire.Context) -> str:
_entry_dialog()
request = PassphraseRequest()
ack = await ctx.call(request, PassphraseAck)
if ack.on_device:
if ack.passphrase is not None:
raise wire.DataError("Passphrase provided when it should not be")
return await _request_on_device(ctx)
if ack.passphrase is None:
raise wire.DataError(
"Passphrase not provided and on_device is False. Use empty string to set an empty passphrase."
)
return ack.passphrase
async def _request_on_device(ctx: wire.Context) -> str:
await ctx.call(ButtonRequest(code=ButtonRequestType.PassphraseEntry), ButtonAck)
keyboard = PassphraseKeyboard("Enter passphrase", _MAX_PASSPHRASE_LEN)
if __debug__:
passphrase = await ctx.wait(keyboard, input_signal())
else:
passphrase = await ctx.wait(keyboard)
if passphrase is CANCELLED:
raise wire.ActionCancelled("Passphrase entry cancelled")
assert isinstance(passphrase, str)
return passphrase
def _entry_dialog() -> None:
text = Text("Passphrase entry", ICON_CONFIG)
text.normal("Please type your", "passphrase on the", "connected host.")
draw_simple(text)

@ -1,86 +0,0 @@
from micropython import const
import storage.device
from storage import cache
from trezor import ui, wire
from trezor.messages import ButtonRequestType, PassphraseSourceType
from trezor.messages.ButtonAck import ButtonAck
from trezor.messages.ButtonRequest import ButtonRequest
from trezor.messages.PassphraseAck import PassphraseAck
from trezor.messages.PassphraseRequest import PassphraseRequest
from trezor.messages.PassphraseStateAck import PassphraseStateAck
from trezor.messages.PassphraseStateRequest import PassphraseStateRequest
from trezor.ui.passphrase import CANCELLED, PassphraseKeyboard, PassphraseSource
from trezor.ui.popup import Popup
from trezor.ui.text import Text
if __debug__:
from apps.debug import input_signal
_MAX_PASSPHRASE_LEN = const(50)
async def protect_by_passphrase(ctx: wire.Context) -> str:
if storage.device.has_passphrase():
return await request_passphrase(ctx)
else:
return ""
async def request_passphrase(ctx: wire.Context) -> str:
source = storage.device.get_passphrase_source()
if source == PassphraseSourceType.ASK:
source = await request_passphrase_source(ctx)
passphrase = await request_passphrase_ack(
ctx, source == PassphraseSourceType.DEVICE
)
if len(passphrase) > _MAX_PASSPHRASE_LEN:
raise wire.DataError("Maximum passphrase length is %d" % _MAX_PASSPHRASE_LEN)
return passphrase
async def request_passphrase_source(ctx: wire.Context) -> int:
req = ButtonRequest(code=ButtonRequestType.PassphraseType)
await ctx.call(req, ButtonAck)
text = Text("Enter passphrase", ui.ICON_CONFIG)
text.normal("Where do you want to", "enter your passphrase?")
source = PassphraseSource(text)
response = await ctx.wait(source)
assert isinstance(response, int)
return response
async def request_passphrase_ack(ctx: wire.Context, on_device: bool) -> str:
if not on_device:
text = Text("Passphrase entry", ui.ICON_CONFIG)
text.normal("Please type your", "passphrase on the", "connected host.")
await Popup(text)
passphrase_request = PassphraseRequest(on_device=on_device)
ack = await ctx.call(passphrase_request, PassphraseAck)
if on_device:
if ack.passphrase is not None:
raise wire.ProcessError("Passphrase provided when it should not be")
keyboard = PassphraseKeyboard("Enter passphrase", _MAX_PASSPHRASE_LEN)
if __debug__:
passphrase = await ctx.wait(keyboard, input_signal())
else:
passphrase = await ctx.wait(keyboard)
if passphrase is CANCELLED:
raise wire.ActionCancelled("Passphrase cancelled")
else:
if ack.passphrase is None:
raise wire.ProcessError("Passphrase not provided")
passphrase = ack.passphrase
assert isinstance(passphrase, str)
state = cache.get_state(prev_state=ack.state, passphrase=passphrase)
state_request = PassphraseStateRequest(state=state)
await ctx.call(state_request, PassphraseStateAck)
return passphrase

@ -1,11 +1,11 @@
import storage
import storage.cache
from storage import cache
from trezor import wire
from trezor.crypto import bip32, hashlib, hmac
from trezor.crypto.curve import secp256k1
from apps.common import HARDENED, mnemonic
from apps.common.request_passphrase import protect_by_passphrase
from apps.common.passphrase import get as get_passphrase
if False:
from typing import List, Union
@ -112,14 +112,11 @@ class Keychain:
async def get_keychain(ctx: wire.Context, namespaces: list) -> Keychain:
if not storage.is_initialized():
raise wire.NotInitialized("Device is not initialized")
seed = storage.cache.get_seed()
seed = cache.get(cache.APP_COMMON_SEED)
if seed is None:
passphrase = storage.cache.get_passphrase()
if passphrase is None:
passphrase = await protect_by_passphrase(ctx)
storage.cache.set_passphrase(passphrase)
passphrase = await get_passphrase(ctx)
seed = mnemonic.get_seed(passphrase)
storage.cache.set_seed(seed)
cache.set(cache.APP_COMMON_SEED, seed)
keychain = Keychain(seed, namespaces)
return keychain
@ -129,10 +126,10 @@ def derive_node_without_passphrase(
) -> bip32.HDNode:
if not storage.is_initialized():
raise Exception("Device is not initialized")
seed = storage.cache.get_seed_without_passphrase()
seed = cache.get(cache.APP_COMMON_SEED_WITHOUT_PASSPHRASE)
if seed is None:
seed = mnemonic.get_seed(progress_bar=False)
storage.cache.set_seed_without_passphrase(seed)
cache.set(cache.APP_COMMON_SEED_WITHOUT_PASSPHRASE, seed)
node = bip32.from_seed(seed, curve_name)
node.derive_path(path)
return node
@ -141,10 +138,10 @@ def derive_node_without_passphrase(
def derive_slip21_node_without_passphrase(path: list) -> Slip21Node:
if not storage.is_initialized():
raise Exception("Device is not initialized")
seed = storage.cache.get_seed_without_passphrase()
seed = cache.get(cache.APP_COMMON_SEED_WITHOUT_PASSPHRASE)
if seed is None:
seed = mnemonic.get_seed(progress_bar=False)
storage.cache.set_seed_without_passphrase(seed)
cache.set(cache.APP_COMMON_SEED_WITHOUT_PASSPHRASE, seed)
node = Slip21Node(seed)
node.derive_path(path)
return node

@ -97,13 +97,12 @@ if __debug__:
ctx: wire.Context, msg: DebugLinkGetState
) -> DebugLinkState:
from trezor.messages.DebugLinkState import DebugLinkState
from storage.device import has_passphrase
from apps.common import mnemonic
from apps.common import mnemonic, passphrase
m = DebugLinkState()
m.mnemonic_secret = mnemonic.get_secret()
m.mnemonic_type = mnemonic.get_type()
m.passphrase_protection = has_passphrase()
m.passphrase_protection = passphrase.is_enabled()
m.reset_entropy = reset_internal_entropy
if msg.wait_layout or current_content is None:

@ -34,8 +34,7 @@ def get_features() -> Features:
f.initialized = storage.is_initialized()
f.pin_protection = config.has_pin()
f.pin_cached = config.has_pin()
f.passphrase_protection = storage.device.has_passphrase()
f.passphrase_cached = cache.has_passphrase()
f.passphrase_protection = storage.device.is_passphrase_enabled()
f.needs_backup = storage.device.needs_backup()
f.unfinished_backup = storage.device.unfinished_backup()
f.no_backup = storage.device.no_backup()
@ -48,6 +47,7 @@ def get_features() -> Features:
Capability.Crypto,
Capability.Shamir,
Capability.ShamirGroups,
Capability.PassphraseEntry,
]
else:
f.capabilities = [
@ -67,18 +67,19 @@ def get_features() -> Features:
Capability.U2F,
Capability.Shamir,
Capability.ShamirGroups,
Capability.PassphraseEntry,
]
f.sd_card_present = io.SDCard().present()
f.sd_protection = storage.sd_salt.is_enabled()
f.wipe_code_protection = config.has_wipe_code()
f.session_id = cache.get_session_id()
f.passphrase_always_on_device = storage.device.get_passphrase_always_on_device()
return f
async def handle_Initialize(ctx: wire.Context, msg: Initialize) -> Features:
if msg.state is None or msg.state != cache.get_state(prev_state=bytes(msg.state)):
if msg.session_id is None or msg.session_id != cache.get_session_id():
cache.clear()
if msg.skip_passphrase:
cache.set_passphrase("")
return get_features()
@ -91,8 +92,12 @@ async def handle_Cancel(ctx: wire.Context, msg: Cancel) -> NoReturn:
async def handle_ClearSession(ctx: wire.Context, msg: ClearSession) -> Success:
cache.clear(keep_passphrase=True)
return Success(message="Session cleared")
"""
This is currently a no-op on T. This should be called LockSession/LockDevice
and lock the device. In other words the cache should stay but the PIN should
be forgotten and required again.
"""
return Success()
async def handle_Ping(ctx: wire.Context, msg: Ping) -> Success:
@ -102,10 +107,6 @@ async def handle_Ping(ctx: wire.Context, msg: Ping) -> Success:
from trezor.ui.text import Text
await require_confirm(ctx, Text("Confirm"), ProtectCall)
if msg.passphrase_protection:
from apps.common.request_passphrase import protect_by_passphrase
await protect_by_passphrase(ctx)
return Success(message=msg.message)

@ -1,6 +1,6 @@
import storage.device
from trezor import ui, wire
from trezor.messages import ButtonRequestType, PassphraseSourceType
from trezor.messages import ButtonRequestType
from trezor.messages.Success import Success
from trezor.ui.text import Text
@ -12,7 +12,7 @@ async def apply_settings(ctx, msg):
msg.homescreen is None
and msg.label is None
and msg.use_passphrase is None
and msg.passphrase_source is None
and msg.passphrase_always_on_device is None
and msg.display_rotation is None
):
raise wire.ProcessError("No setting provided")
@ -28,8 +28,10 @@ async def apply_settings(ctx, msg):
if msg.use_passphrase is not None:
await require_confirm_change_passphrase(ctx, msg.use_passphrase)
if msg.passphrase_source is not None:
await require_confirm_change_passphrase_source(ctx, msg.passphrase_source)
if msg.passphrase_always_on_device is not None:
await require_confirm_change_passphrase_source(
ctx, msg.passphrase_always_on_device
)
if msg.display_rotation is not None:
await require_confirm_change_display_rotation(ctx, msg.display_rotation)
@ -38,7 +40,7 @@ async def apply_settings(ctx, msg):
label=msg.label,
use_passphrase=msg.use_passphrase,
homescreen=msg.homescreen,
passphrase_source=msg.passphrase_source,
passphrase_always_on_device=msg.passphrase_always_on_device,
display_rotation=msg.display_rotation,
)
@ -69,16 +71,16 @@ async def require_confirm_change_passphrase(ctx, use):
await require_confirm(ctx, text, ButtonRequestType.ProtectCall)
async def require_confirm_change_passphrase_source(ctx, source):
if source == PassphraseSourceType.DEVICE:
desc = "ON DEVICE"
elif source == PassphraseSourceType.HOST:
desc = "ON HOST"
else:
desc = "ASK"
async def require_confirm_change_passphrase_source(
ctx, passphrase_always_on_device: bool
):
text = Text("Passphrase source", ui.ICON_CONFIG)
text.normal("Do you really want to", "change the passphrase", "source to")
text.bold("ALWAYS %s?" % desc)
if passphrase_always_on_device:
text.normal(
"Do you really want to", "enter passphrase always", "on the device?"
)
else:
text.normal("Do you want to revoke", "the passphrase on device", "setting?")
await require_confirm(ctx, text, ButtonRequestType.ProtectCall)

@ -28,7 +28,7 @@ async def recovery_homescreen() -> None:
return
# recovery process does not communicate on the wire
ctx = wire.DummyContext()
ctx = wire.DUMMY_CONTEXT
await recovery_process(ctx)

@ -4,7 +4,6 @@ from trezor.messages import MessageType
from apps.common import HARDENED
CURVE = "ed25519"
_LIVE_REFRESH_TOKEN = None # live-refresh permission token
def boot() -> None:
@ -20,11 +19,3 @@ def boot() -> None:
if __debug__ and hasattr(MessageType, "DebugMoneroDiagRequest"):
wire.add(MessageType.DebugMoneroDiagRequest, __name__, "diag")
def live_refresh_token(token: bytes = None) -> None:
global _LIVE_REFRESH_TOKEN
if token is None:
return _LIVE_REFRESH_TOKEN
else:
_LIVE_REFRESH_TOKEN = token

@ -1,6 +1,6 @@
import gc
from storage.cache import get_passphrase_fprint
import storage.cache
from trezor import log
from trezor.messages import MessageType
from trezor.messages.MoneroLiveRefreshFinalAck import MoneroLiveRefreshFinalAck
@ -10,7 +10,7 @@ from trezor.messages.MoneroLiveRefreshStepAck import MoneroLiveRefreshStepAck
from trezor.messages.MoneroLiveRefreshStepRequest import MoneroLiveRefreshStepRequest
from apps.common import paths
from apps.monero import CURVE, live_refresh_token, misc
from apps.monero import CURVE, misc
from apps.monero.layout import confirms
from apps.monero.xmr import crypto, key_image, monero
from apps.monero.xmr.crypto import chacha_poly
@ -49,10 +49,9 @@ async def _init_step(
ctx, misc.validate_full_path, keychain, msg.address_n, CURVE
)
passphrase_fprint = get_passphrase_fprint()
if live_refresh_token() != passphrase_fprint:
if not storage.cache.get(storage.cache.APP_MONERO_LIVE_REFRESH):
await confirms.require_confirm_live_refresh(ctx)
live_refresh_token(passphrase_fprint)
storage.cache.set(storage.cache.APP_MONERO_LIVE_REFRESH, True)
s.creds = misc.get_creds(keychain, msg.address_n, msg.network_type)

@ -1,75 +1,36 @@
from storage.device import get_device_id
from trezor.crypto import hashlib, hmac, random
from trezor.crypto import random
if False:
from typing import Optional
_cached_seed = None # type: Optional[bytes]
_cached_seed_without_passphrase = None # type: Optional[bytes]
_cached_passphrase = None # type: Optional[str]
_cached_passphrase_fprint = b"\x00\x00\x00\x00" # type: bytes
APP_COMMON_SEED = 0
APP_COMMON_SEED_WITHOUT_PASSPHRASE = 1
APP_CARDANO_ROOT = 2
APP_MONERO_LIVE_REFRESH = 3
_cache_session_id = None # type: Optional[bytes]
_cache = {}
def get_state(prev_state: bytes = None, passphrase: str = None) -> Optional[bytes]:
if prev_state is None:
salt = random.bytes(32) # generate a random salt if no state provided
else:
salt = prev_state[:32] # use salt from provided state
if len(salt) != 32:
return None # invalid state
if passphrase is None:
if _cached_passphrase is None:
return None # we don't have any passphrase to compute the state
else:
passphrase = _cached_passphrase # use cached passphrase
return _compute_state(salt, passphrase)
def _compute_state(salt: bytes, passphrase: str) -> bytes:
# state = HMAC(passphrase, salt || device_id)
message = salt + get_device_id().encode()
state = hmac.new(passphrase.encode(), message, hashlib.sha256).digest()
return salt + state
def get_seed() -> Optional[bytes]:
return _cached_seed
def get_seed_without_passphrase() -> Optional[bytes]:
return _cached_seed_without_passphrase
def get_passphrase() -> Optional[str]:
return _cached_passphrase
def get_passphrase_fprint() -> bytes:
return _cached_passphrase_fprint
def has_passphrase() -> bool:
return _cached_passphrase is not None
if False:
from typing import Any
def set_seed(seed: Optional[bytes]) -> None:
global _cached_seed
_cached_seed = seed
def get_session_id() -> bytes:
global _cache_session_id
if not _cache_session_id:
_cache_session_id = random.bytes(32)
return _cache_session_id
def set_seed_without_passphrase(seed: Optional[bytes]) -> None:
global _cached_seed_without_passphrase
_cached_seed_without_passphrase = seed
def set(key: int, value: Any) -> None:
_cache[key] = value
def set_passphrase(passphrase: Optional[str]) -> None:
global _cached_passphrase, _cached_passphrase_fprint
_cached_passphrase = passphrase
_cached_passphrase_fprint = _compute_state(b"FPRINT", passphrase or "")[:4]
def get(key: int) -> Any:
return _cache.get(key)
def clear(keep_passphrase: bool = False) -> None:
set_seed(None)
set_seed_without_passphrase(None)
if not keep_passphrase:
set_passphrase(None)
def clear() -> None:
global _cache_session_id
_cache_session_id = None
_cache.clear()

@ -24,7 +24,7 @@ _HOMESCREEN = const(0x06) # bytes
_NEEDS_BACKUP = const(0x07) # bool (0x01 or empty)
_FLAGS = const(0x08) # int
U2F_COUNTER = const(0x09) # int
_PASSPHRASE_SOURCE = const(0x0A) # int
_PASSPHRASE_ALWAYS_ON_DEVICE = const(0x0A) # bool (0x01 or empty)
_UNFINISHED_BACKUP = const(0x0B) # bool (0x01 or empty)
_AUTOLOCK_DELAY_MS = const(0x0C) # int
_NO_BACKUP = const(0x0D) # bool (0x01 or empty)
@ -101,7 +101,7 @@ def get_backup_type() -> EnumTypeBackupType:
return backup_type # type: ignore
def has_passphrase() -> bool:
def is_passphrase_enabled() -> bool:
return common.get_bool(_NAMESPACE, _USE_PASSPHRASE)
@ -143,21 +143,21 @@ def no_backup() -> bool:
return common.get_bool(_NAMESPACE, _NO_BACKUP)
def get_passphrase_source() -> int:
b = common.get(_NAMESPACE, _PASSPHRASE_SOURCE)
if b == b"\x01":
return 1
elif b == b"\x02":
return 2
else:
return 0
def get_passphrase_always_on_device() -> bool:
"""
This is backwards compatible with _PASSPHRASE_SOURCE:
- If ASK(0) => returns False, the check against b"\x01" in get_bool fails.
- If DEVICE(1) => returns True, the check against b"\x01" in get_bool succeeds.
- If HOST(2) => returns False, the check against b"\x01" in get_bool fails.
"""
return common.get_bool(_NAMESPACE, _PASSPHRASE_ALWAYS_ON_DEVICE)
def load_settings(
label: str = None,
use_passphrase: bool = None,
homescreen: bytes = None,
passphrase_source: int = None,
passphrase_always_on_device: bool = None,
display_rotation: int = None,
) -> None:
if label is not None:
@ -170,9 +170,10 @@ def load_settings(
common.set(_NAMESPACE, _HOMESCREEN, homescreen, True) # public
else:
common.set(_NAMESPACE, _HOMESCREEN, b"", True) # public
if passphrase_source is not None:
if passphrase_source in (0, 1, 2):
common.set(_NAMESPACE, _PASSPHRASE_SOURCE, bytes([passphrase_source]))
if passphrase_always_on_device is not None:
common.set_bool(
_NAMESPACE, _PASSPHRASE_ALWAYS_ON_DEVICE, passphrase_always_on_device
)
if display_rotation is not None:
if display_rotation not in (0, 90, 180, 270):
raise ValueError(

@ -6,7 +6,6 @@ if __debug__:
try:
from typing import Dict, List # noqa: F401
from typing_extensions import Literal # noqa: F401
EnumTypePassphraseSourceType = Literal[0, 1, 2]
except ImportError:
pass
@ -20,17 +19,17 @@ class ApplySettings(p.MessageType):
label: str = None,
use_passphrase: bool = None,
homescreen: bytes = None,
passphrase_source: EnumTypePassphraseSourceType = None,
auto_lock_delay_ms: int = None,
display_rotation: int = None,
passphrase_always_on_device: bool = None,
) -> None:
self.language = language
self.label = label
self.use_passphrase = use_passphrase
self.homescreen = homescreen
self.passphrase_source = passphrase_source
self.auto_lock_delay_ms = auto_lock_delay_ms
self.display_rotation = display_rotation
self.passphrase_always_on_device = passphrase_always_on_device
@classmethod
def get_fields(cls) -> Dict:
@ -39,7 +38,7 @@ class ApplySettings(p.MessageType):
2: ('label', p.UnicodeType, 0),
3: ('use_passphrase', p.BoolType, 0),
4: ('homescreen', p.BytesType, 0),
5: ('passphrase_source', p.EnumType("PassphraseSourceType", (0, 1, 2)), 0),
6: ('auto_lock_delay_ms', p.UVarintType, 0),
7: ('display_rotation', p.UVarintType, 0),
8: ('passphrase_always_on_device', p.BoolType, 0),
}

@ -6,7 +6,7 @@ if __debug__:
try:
from typing import Dict, List # noqa: F401
from typing_extensions import Literal # noqa: F401
EnumTypeButtonRequestType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
EnumTypeButtonRequestType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19]
except ImportError:
pass
@ -23,5 +23,5 @@ class ButtonRequest(p.MessageType):
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('code', p.EnumType("ButtonRequestType", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18)), 0),
1: ('code', p.EnumType("ButtonRequestType", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19)), 0),
}

@ -16,8 +16,8 @@ Address = 10 # type: Literal[10]
PublicKey = 11 # type: Literal[11]
MnemonicWordCount = 12 # type: Literal[12]
MnemonicInput = 13 # type: Literal[13]
PassphraseType = 14 # type: Literal[14]
UnknownDerivationPath = 15 # type: Literal[15]
RecoveryHomepage = 16 # type: Literal[16]
Success = 17 # type: Literal[17]
Warning = 18 # type: Literal[18]
PassphraseEntry = 19 # type: Literal[19]

@ -23,3 +23,4 @@ if not utils.BITCOIN_ONLY:
U2F = 14 # type: Literal[14]
Shamir = 15 # type: Literal[15]
ShamirGroups = 16 # type: Literal[16]
PassphraseEntry = 17 # type: Literal[17]

@ -10,5 +10,5 @@ if __debug__:
pass
class PassphraseStateAck(p.MessageType):
class Deprecated_PassphraseStateAck(p.MessageType):
MESSAGE_WIRE_TYPE = 78

@ -10,7 +10,7 @@ if __debug__:
pass
class PassphraseStateRequest(p.MessageType):
class Deprecated_PassphraseStateRequest(p.MessageType):
MESSAGE_WIRE_TYPE = 77
def __init__(

@ -6,7 +6,7 @@ if __debug__:
try:
from typing import Dict, List # noqa: F401
from typing_extensions import Literal # noqa: F401
EnumTypeCapability = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
EnumTypeCapability = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
EnumTypeBackupType = Literal[0, 1, 2]
except ImportError:
pass
@ -32,7 +32,6 @@ class Features(p.MessageType):
bootloader_hash: bytes = None,
imported: bool = None,
pin_cached: bool = None,
passphrase_cached: bool = None,
firmware_present: bool = None,
needs_backup: bool = None,
flags: int = None,
@ -50,6 +49,8 @@ class Features(p.MessageType):
sd_card_present: bool = None,
sd_protection: bool = None,
wipe_code_protection: bool = None,
session_id: bytes = None,
passphrase_always_on_device: bool = None,
) -> None:
self.vendor = vendor
self.major_version = major_version
@ -66,7 +67,6 @@ class Features(p.MessageType):
self.bootloader_hash = bootloader_hash
self.imported = imported
self.pin_cached = pin_cached
self.passphrase_cached = passphrase_cached
self.firmware_present = firmware_present
self.needs_backup = needs_backup
self.flags = flags
@ -84,6 +84,8 @@ class Features(p.MessageType):
self.sd_card_present = sd_card_present
self.sd_protection = sd_protection
self.wipe_code_protection = wipe_code_protection
self.session_id = session_id
self.passphrase_always_on_device = passphrase_always_on_device
@classmethod
def get_fields(cls) -> Dict:
@ -103,7 +105,6 @@ class Features(p.MessageType):
14: ('bootloader_hash', p.BytesType, 0),
15: ('imported', p.BoolType, 0),
16: ('pin_cached', p.BoolType, 0),
17: ('passphrase_cached', p.BoolType, 0),
18: ('firmware_present', p.BoolType, 0),
19: ('needs_backup', p.BoolType, 0),
20: ('flags', p.UVarintType, 0),
@ -116,9 +117,11 @@ class Features(p.MessageType):
27: ('unfinished_backup', p.BoolType, 0),
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),
30: ('capabilities', p.EnumType("Capability", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)), p.FLAG_REPEATED),
31: ('backup_type', p.EnumType("BackupType", (0, 1, 2)), 0),
32: ('sd_card_present', p.BoolType, 0),
33: ('sd_protection', p.BoolType, 0),
34: ('wipe_code_protection', p.BoolType, 0),
35: ('session_id', p.BytesType, 0),
36: ('passphrase_always_on_device', p.BoolType, 0),
}

@ -15,15 +15,12 @@ class Initialize(p.MessageType):
def __init__(
self,
state: bytes = None,
skip_passphrase: bool = None,
session_id: bytes = None,
) -> None:
self.state = state
self.skip_passphrase = skip_passphrase
self.session_id = session_id
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('state', p.BytesType, 0),
2: ('skip_passphrase', p.BoolType, 0),
1: ('session_id', p.BytesType, 0),
}

@ -29,8 +29,6 @@ EntropyRequest = 35 # type: Literal[35]
EntropyAck = 36 # type: Literal[36]
PassphraseRequest = 41 # type: Literal[41]
PassphraseAck = 42 # type: Literal[42]
PassphraseStateRequest = 77 # type: Literal[77]
PassphraseStateAck = 78 # type: Literal[78]
RecoveryDevice = 45 # type: Literal[45]
WordRequest = 46 # type: Literal[46]
WordAck = 47 # type: Literal[47]
@ -40,6 +38,8 @@ SdProtect = 79 # type: Literal[79]
GetNextU2FCounter = 80 # type: Literal[80]
NextU2FCounter = 81 # type: Literal[81]
ChangeWipeCode = 82 # type: Literal[82]
Deprecated_PassphraseStateRequest = 77 # type: Literal[77]
Deprecated_PassphraseStateAck = 78 # type: Literal[78]
FirmwareErase = 6 # type: Literal[6]
FirmwareUpload = 7 # type: Literal[7]
FirmwareRequest = 8 # type: Literal[8]

@ -16,14 +16,17 @@ class PassphraseAck(p.MessageType):
def __init__(
self,
passphrase: str = None,
state: bytes = None,
_state: bytes = None,
on_device: bool = None,
) -> None:
self.passphrase = passphrase
self.state = state
self._state = _state
self.on_device = on_device
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('passphrase', p.UnicodeType, 0),
2: ('state', p.BytesType, 0),
2: ('_state', p.BytesType, 0),
3: ('on_device', p.BoolType, 0),
}

@ -15,12 +15,12 @@ class PassphraseRequest(p.MessageType):
def __init__(
self,
on_device: bool = None,
_on_device: bool = None,
) -> None:
self.on_device = on_device
self._on_device = _on_device
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('on_device', p.BoolType, 0),
1: ('_on_device', p.BoolType, 0),
}

@ -1,8 +0,0 @@
# Automatically generated by pb2py
# fmt: off
if False:
from typing_extensions import Literal
ASK = 0 # type: Literal[0]
DEVICE = 1 # type: Literal[1]
HOST = 2 # type: Literal[2]

@ -17,19 +17,13 @@ class Ping(p.MessageType):
self,
message: str = None,
button_protection: bool = None,
pin_protection: bool = None,
passphrase_protection: bool = None,
) -> None:
self.message = message
self.button_protection = button_protection
self.pin_protection = pin_protection
self.passphrase_protection = passphrase_protection
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('message', p.UnicodeType, 0),
2: ('button_protection', p.BoolType, 0),
3: ('pin_protection', p.BoolType, 0),
4: ('passphrase_protection', p.BoolType, 0),
}

@ -160,6 +160,14 @@ def header_error(message: str, clear: bool = True) -> None:
display.bar(0, 30, WIDTH, HEIGHT - 30, style.BG)
def draw_simple(t: Component) -> None: # noqa: F405
backlight_fade(style.BACKLIGHT_DIM)
display.clear()
t.on_render()
refresh()
backlight_fade(style.BACKLIGHT_NORMAL)
def grid(
i: int, # i-th cell of the table of which we wish to return Area (snake-like starting with 0)
n_x: int = 3, # number of rows in the table

@ -1,7 +1,6 @@
from micropython import const
from trezor import io, loop, res, ui
from trezor.messages import PassphraseSourceType
from trezor.ui import display
from trezor.ui.button import Button, ButtonClear, ButtonConfirm
from trezor.ui.swipe import SWIPE_HORIZONTAL, SWIPE_LEFT, Swipe
@ -246,25 +245,3 @@ class PassphraseKeyboard(ui.Layout):
def create_tasks(self) -> Tuple[loop.Task, ...]:
return self.handle_input(), self.handle_rendering(), self.handle_paging()
class PassphraseSource(ui.Layout):
def __init__(self, content: ui.Component) -> None:
self.content = content
self.device = Button(ui.grid(8, n_y=4, n_x=4, cells_x=4), "Device")
self.device.on_click = self.on_device # type: ignore
self.host = Button(ui.grid(12, n_y=4, n_x=4, cells_x=4), "Host")
self.host.on_click = self.on_host # type: ignore
def dispatch(self, event: int, x: int, y: int) -> None:
self.content.dispatch(event, x, y)
self.device.dispatch(event, x, y)
self.host.dispatch(event, x, y)
def on_device(self) -> None:
raise ui.Result(PassphraseSourceType.DEVICE)
def on_host(self) -> None:
raise ui.Result(PassphraseSourceType.HOST)

@ -1,9 +1,23 @@
import sys
sys.path.append('../src')
sys.path.append("../src")
from ubinascii import hexlify, unhexlify # noqa: F401
import unittest # noqa: F401
from trezor import utils # noqa: F401
def await_result(task: Awaitable) -> Any:
value = None
while True:
try:
result = task.send(value)
except StopIteration as e:
return e.value
if result:
value = await_result(result)
else:
value = None

@ -44,28 +44,14 @@ class ByteArrayWriter:
return len(buf)
def run_until_complete(task: Awaitable) -> Any:
value = None
while True:
try:
result = task.send(value)
except StopIteration as e:
return e.value
if result:
value = run_until_complete(result)
else:
value = None
def load_uvarint(data: bytes) -> int:
reader = ByteReader(data)
return run_until_complete(protobuf.load_uvarint(reader))
return await_result(protobuf.load_uvarint(reader))
def dump_uvarint(value: int) -> bytearray:
writer = ByteArrayWriter()
run_until_complete(protobuf.dump_uvarint(writer, value))
await_result(protobuf.dump_uvarint(writer, value))
return writer.buf
@ -106,9 +92,9 @@ class TestProtobuf(unittest.TestCase):
# ok message:
msg = Message(-42, 5)
writer = ByteArrayWriter()
run_until_complete(protobuf.dump_message(writer, msg))
await_result(protobuf.dump_message(writer, msg))
reader = ByteReader(bytes(writer.buf))
nmsg = run_until_complete(protobuf.load_message(reader, Message))
nmsg = await_result(protobuf.load_message(reader, Message))
self.assertEqual(msg.sint_field, nmsg.sint_field)
self.assertEqual(msg.enum_field, nmsg.enum_field)
@ -116,10 +102,10 @@ class TestProtobuf(unittest.TestCase):
# bad enum value:
msg = Message(-42, 42)
writer = ByteArrayWriter()
run_until_complete(protobuf.dump_message(writer, msg))
await_result(protobuf.dump_message(writer, msg))
reader = ByteReader(bytes(writer.buf))
with self.assertRaises(TypeError):
run_until_complete(protobuf.load_message(reader, Message))
await_result(protobuf.load_message(reader, Message))
if __name__ == "__main__":

@ -0,0 +1,81 @@
from common import *
from mock import patch
from mock_storage import mock_storage
import storage
from storage import cache
from trezor.messages.Initialize import Initialize
from trezor.messages.ClearSession import ClearSession
from trezor.wire import DUMMY_CONTEXT
from apps.homescreen import handle_Initialize, handle_ClearSession
KEY = 99
class TestStorageCache(unittest.TestCase):
def test_session_id(self):
session_id_a = cache.get_session_id()
self.assertIsNotNone(session_id_a)
session_id_b = cache.get_session_id()
self.assertEqual(session_id_a, session_id_b)
cache.clear()
session_id_c = cache.get_session_id()
self.assertIsNotNone(session_id_c)
self.assertNotEqual(session_id_a, session_id_c)
def test_get_set(self):
value = cache.get(KEY)
self.assertIsNone(value)
cache.set(KEY, "hello")
value = cache.get(KEY)
self.assertEqual(value, "hello")
cache.clear()
value = cache.get(KEY)
self.assertIsNone(value)
@mock_storage
def test_Initialize(self):
def call_Initialize(**kwargs):
msg = Initialize(**kwargs)
return await_result(handle_Initialize(DUMMY_CONTEXT, msg))
# calling Initialize without an ID allocates a new one
session_id = cache.get_session_id()
features = call_Initialize()
new_session_id = cache.get_session_id()
self.assertNotEqual(session_id, new_session_id)
self.assertEqual(new_session_id, features.session_id)
# calling Initialize with the current ID does not allocate a new one
features = call_Initialize(session_id=new_session_id)
same_session_id = cache.get_session_id()
self.assertEqual(new_session_id, same_session_id)
self.assertEqual(same_session_id, features.session_id)
call_Initialize()
# calling Initialize with a non-current ID returns a different one
features = call_Initialize(session_id=new_session_id)
self.assertNotEqual(new_session_id, features.session_id)
# allocating a new session ID clears the cache
cache.set(KEY, "hello")
features = call_Initialize()
self.assertIsNone(cache.get(KEY))
# resuming a session does not clear the cache
cache.set(KEY, "hello")
call_Initialize(session_id=features.session_id)
self.assertEqual(cache.get(KEY), "hello")
# supplying a different session ID clears the cache
self.assertNotEqual(new_session_id, features.session_id)
call_Initialize(session_id=new_session_id)
self.assertIsNone(cache.get(KEY))
if __name__ == "__main__":
unittest.main()

@ -1,5 +0,0 @@
# Communication
We use [Protobuf v2](https://developers.google.com/protocol-buffers/) for host-device communication. The communication cycle is very simple, Trezor receives a message, acts on it and responds with another message. Trezor on its own is incapable of initiating the communication.
The Protobuf definitions can be found in `common/protob`.

@ -0,0 +1,9 @@
# Communication
_Note: In this section we describe the internal functioning of the communication protocol. If you wish to implement Trezor support you should use [Connect](https://github.com/trezor/connect/) or [python-trezor](https://pypi.org/project/trezor/), which will do all this hard work for you._
We use [Protobuf v2](https://developers.google.com/protocol-buffers/) for host-device communication. The communication cycle is very simple, Trezor receives a message (request), acts on it and responds with another one (response). Trezor on its own is incapable of initiating the communication.
## Definitions
Protobuf messages are defined in the [Common](https://github.com/trezor/trezor-firmware/tree/master/common) project, which is part of this monorepo. This repository is also exported to [trezor/trezor-common](https://github.com/trezor/trezor-common) to be used by third parties, which prefer not to include the whole monorepo. That copy is read-only mirror and all changes are happening in this monorepo.

@ -0,0 +1,64 @@
# Passphrase
As of \[versions TBD\] we have changed how [passphrase](https://wiki.trezor.io/Passphrase) is communicated between the Host and the Device.
Passphrase is very tightly coupled with _sessions_. The reader is encouraged to read on that topic first in the [sessions.md](sessions.md) section.
## Scheme
As soon as Trezor needs the passphrase to do BIP-39/SLIP-39 derivations it prompts the user for passphrase.
```
GetAddress(...)
---------> PassphraseRequest()
<---------
PassphraseAck
(str passphrase, bool on_device)
---------> Address(...)
<---------
```
In the default Trezor setting, the passphrase is obtained from the Host. Trezor sends a PassphraseRequest message and awaits PassphraseAck as a response. This message contains field `passphrase` to transmit it or it has `on_device` boolean flag to indicate that the user wishes to enter the passphrase on Trezor instead. Setting both `passphrase` and `on_device` to true is forbidden.
Note that this has changed as of TBD. In previous firmware versions the `on_device` flag was in the PassphraseRequest message, since this decision has been made on Trezor. We also had two additional messages PassphraseStateRequest and PassphraseStateAck which were removed.
## Example
On an initialized device with passphrase enabled a common communication starts like this:
```
Initialize()
---------> Features(..., session_id)
<---------
GetAddress(...)
---------> PassphraseRequest()
<---------
PassphraseAck(...)
---------> Address(...)
<---------
```
The device requested the passphrase since the BIP-39/SLIP-39 seed is not yet cached. After this workflow the seed is cached and the passphrase will therefore never be requested again unless the session is cleared*.
Since we do not have sessions, the Host can not be sure that someone else has not used the device and applied another session id (e.g. changed the Passphrase). To work around this we send the session id again on every subsequent message. See more on that in [session.md]().
```
Initialize(session_id)
---------> Features(..., session_id)
<---------
GetPublicKey(...)
---------> PublicKey(...)
<---------
```
As long as the session_id in `Initialize` is the same as the one Trezor stores internally, Trezor guarantees the same passphrase is being used.
----
\* There is one exception and that is Cardano. Because Cardano has a different BIP-39/SLIP-39 derivation scheme for passphrase we can not use the cached seed. As a workaround we prompt for the passphrase again in such case and cache the cardano seed in the cardano app directly.
## Passphrase always on device
User might want to enforce the passphrase entry on the device every time without the hassle of instructing the Host to do so.
For such cases the user may apply the *Passphrase always on device* setting. As the name suggests, with this setting the passphrase is prompted on the device right away and no PassphraseRequest/PassphraseAck messages are exchanged. Note that the passphrase is prompted only once for given session id. If the user wishes to enter another passphrase they need to either send Initialize(session_id=None) or replug the device.

@ -0,0 +1,36 @@
# Sessions (the lack of them)
Currently the communication protocol lacks sessions, which are planned to be introduced in the near future (see [#79](https://github.com/trezor/trezor-firmware/issues/79)).
To ensure the device is in the expected state we use something called _session_id_. Session id is a 32 bytes long random blob which identifies the internal device state (mainly its caches). This is primarily useful for passphrase to make sure the same passphrase is cached in the device as the one the user entered a few minutes ago. See [passphrase.md](passphrase.md) for more on that.
On first initialization the Host does not have a session id and starts the communication with an empty Initialize:
```
Initialize()
---------> Features(..., session_id)
<---------
```
After the first Features message is received the Host might store the session_id. To ensure the device state Host must send the Initialize message again including that particular session_id:
```
Initialize(session_id)
---------> Features(..., session_id)
<---------
Request
---------> Response
<---------
```
So to make sure the device state has not changed, the Host must send the Initialize message with the correctly stored session_id before each request. Yes, this is stupid.
As mentioned, sessions will be introduced soon™ and fix that. We will probably take the first few bytes of the session_id, declare it a session id, and the rest will remain, without the annoying requirement of sending Initialize before every message.
----
The session is terminated and therefore the caches are cleared if:
- Initialize.session_id is empty.
- Initialize.session_id is different then the one cached in Trezor.
- Trezor is replugged (session is not persistent).
- ClearSession is received.

@ -1,8 +1,9 @@
Version 1.x.x [not yet released]
Version 1.9.0 [not yet released]
* Disallow changing of settings via dry-run recovery
* Wipe code
* Make LoadDevice debug only and drop its XPRV feature
* Add feature to retrieve the next U2F counter
* Passphrase redesign
Version 1.8.3 [Sep 2019]
* Small code improvements

@ -31,6 +31,7 @@
#include "config.h"
#include "curves.h"
#include "debug.h"
#include "fsm.h"
#include "gettext.h"
#include "hmac.h"
#include "layout2.h"
@ -119,11 +120,11 @@ be added to the storage u2f_counter to get the real counter value.
* storage.u2f_counter + config_u2f_offset.
* This corresponds to the number of cleared bits in the U2FAREA.
*/
static secbool sessionSeedCached, sessionSeedUsesPassphrase;
static secbool sessionSeedCached;
static uint8_t CONFIDENTIAL sessionSeed[64];
static secbool sessionPassphraseCached = secfalse;
static char CONFIDENTIAL sessionPassphrase[51];
static secbool sessionIdCached;
static uint8_t sessionId[32];
#define autoLockDelayMsDefault (10 * 60 * 1000U) // 10 minutes
static secbool autoLockDelayMsCached = secfalse;
@ -407,8 +408,8 @@ void config_init(void) {
void session_clear(bool lock) {
sessionSeedCached = secfalse;
memzero(&sessionSeed, sizeof(sessionSeed));
sessionPassphraseCached = secfalse;
memzero(&sessionPassphrase, sizeof(sessionPassphrase));
sessionIdCached = secfalse;
memzero(&sessionId, sizeof(sessionId));
if (lock) {
storage_lock();
}
@ -527,8 +528,6 @@ void config_setLanguage(const char *lang) {
}
void config_setPassphraseProtection(bool passphrase_protection) {
sessionSeedCached = secfalse;
sessionPassphraseCached = secfalse;
config_set_bool(KEY_PASSPHRASE_PROTECTION, passphrase_protection);
}
@ -550,18 +549,19 @@ static void get_root_node_callback(uint32_t iter, uint32_t total) {
layoutProgress(_("Waking up"), 1000 * iter / total);
}
const uint8_t *config_getSeed(bool usePassphrase) {
const uint8_t *config_getSeed(void) {
// root node is properly cached
if (usePassphrase == (sectrue == sessionSeedUsesPassphrase) &&
sectrue == sessionSeedCached) {
if (sectrue == sessionSeedCached) {
return sessionSeed;
}
// if storage has mnemonic, convert it to node and use it
char mnemonic[MAX_MNEMONIC_LEN + 1] = {0};
if (config_getMnemonic(mnemonic, sizeof(mnemonic))) {
if (usePassphrase && !protectPassphrase()) {
char passphrase[MAX_PASSPHRASE_LEN + 1] = {0};
if (!protectPassphrase(passphrase)) {
memzero(mnemonic, sizeof(mnemonic));
memzero(passphrase, sizeof(passphrase));
return NULL;
}
// if storage was not imported (i.e. it was properly generated or recovered)
@ -575,13 +575,16 @@ const uint8_t *config_getSeed(bool usePassphrase) {
}
}
char oldTiny = usbTiny(1);
mnemonic_to_seed(mnemonic, usePassphrase ? sessionPassphrase : "",
sessionSeed, get_root_node_callback); // BIP-0039
mnemonic_to_seed(mnemonic, passphrase, sessionSeed,
get_root_node_callback); // BIP-0039
memzero(mnemonic, sizeof(mnemonic));
memzero(passphrase, sizeof(passphrase));
usbTiny(oldTiny);
sessionSeedCached = sectrue;
sessionSeedUsesPassphrase = usePassphrase ? sectrue : secfalse;
return sessionSeed;
} else {
fsm_sendFailure(FailureType_Failure_NotInitialized,
_("Device not initialized"));
}
return NULL;
@ -606,58 +609,16 @@ bool config_getU2FRoot(HDNode *node) {
return ret;
}
bool config_getRootNode(HDNode *node, const char *curve, bool usePassphrase) {
// if storage has node, decrypt and use it
StorageHDNode storageHDNode = {0};
uint16_t len = 0;
if (strcmp(curve, SECP256K1_NAME) == 0 &&
sectrue ==
storage_get(KEY_NODE, &storageHDNode, sizeof(storageHDNode), &len) &&
len == sizeof(StorageHDNode)) {
if (!protectPassphrase()) {
memzero(&storageHDNode, sizeof(storageHDNode));
return false;
}
if (!config_loadNode(&storageHDNode, curve, node)) {
memzero(&storageHDNode, sizeof(storageHDNode));
return false;
}
bool passphrase_protection = false;
config_getPassphraseProtection(&passphrase_protection);
if (passphrase_protection && sectrue == sessionPassphraseCached &&
sessionPassphrase[0] != '\0') {
// decrypt hd node
uint8_t secret[64] = {0};
PBKDF2_HMAC_SHA512_CTX pctx = {0};
char oldTiny = usbTiny(1);
pbkdf2_hmac_sha512_Init(&pctx, (const uint8_t *)sessionPassphrase,
strlen(sessionPassphrase),
(const uint8_t *)"TREZORHD", 8, 1);
get_root_node_callback(0, BIP39_PBKDF2_ROUNDS);
for (int i = 0; i < 8; i++) {
pbkdf2_hmac_sha512_Update(&pctx, BIP39_PBKDF2_ROUNDS / 8);
get_root_node_callback((i + 1) * BIP39_PBKDF2_ROUNDS / 8,
BIP39_PBKDF2_ROUNDS);
}
pbkdf2_hmac_sha512_Final(&pctx, secret);
usbTiny(oldTiny);
aes_decrypt_ctx ctx = {0};
aes_decrypt_key256(secret, &ctx);
aes_cbc_decrypt(node->chain_code, node->chain_code, 32, secret + 32,
&ctx);
aes_cbc_decrypt(node->private_key, node->private_key, 32, secret + 32,
&ctx);
}
return true;
}
memzero(&storageHDNode, sizeof(storageHDNode));
const uint8_t *seed = config_getSeed(usePassphrase);
bool config_getRootNode(HDNode *node, const char *curve) {
const uint8_t *seed = config_getSeed();
if (seed == NULL) {
return false;
}
return hdnode_from_seed(seed, 64, curve, node);
int result = hdnode_from_seed(seed, 64, curve, node);
if (result == 0) {
fsm_sendFailure(FailureType_Failure_NotInitialized, _("Unsupported curve"));
}
return result;
}
bool config_getLabel(char *dest, uint16_t dest_size) {
@ -811,40 +772,12 @@ bool config_changeWipeCode(const char *pin, const char *wipe_code) {
return sectrue == ret;
}
void session_cachePassphrase(const char *passphrase) {
strlcpy(sessionPassphrase, passphrase, sizeof(sessionPassphrase));
sessionPassphraseCached = sectrue;
}
bool session_isPassphraseCached(void) {
return sectrue == sessionPassphraseCached;
}
bool session_getState(const uint8_t *salt, uint8_t *state,
const char *passphrase) {
if (!passphrase && sectrue != sessionPassphraseCached) {
return false;
} else {
passphrase = sessionPassphrase;
const uint8_t *session_getSessionId(void) {
if (!sessionIdCached) {
random_buffer(sessionId, 32);
}
if (!salt) {
// if salt is not provided fill the first half of the state with random data
random_buffer(state, 32);
} else {
// if salt is provided fill the first half of the state with salt
memcpy(state, salt, 32);
}
// state[0:32] = salt
// state[32:64] = HMAC(passphrase, salt || device_id)
HMAC_SHA256_CTX ctx = {0};
hmac_sha256_Init(&ctx, (const uint8_t *)passphrase, strlen(passphrase));
hmac_sha256_Update(&ctx, state, 32);
hmac_sha256_Update(&ctx, (const uint8_t *)config_uuid, sizeof(config_uuid));
hmac_sha256_Final(&ctx, state + 32);
memzero(&ctx, sizeof(ctx));
return true;
sessionIdCached = sectrue;
return sessionId;
}
bool session_isUnlocked(void) { return sectrue == storage_is_unlocked(); }

@ -91,10 +91,10 @@ void session_clear(bool lock);
void config_loadDevice(const LoadDevice *msg);
const uint8_t *config_getSeed(bool usePassphrase);
const uint8_t *config_getSeed(void);
bool config_getU2FRoot(HDNode *node);
bool config_getRootNode(HDNode *node, const char *curve, bool usePassphrase);
bool config_getRootNode(HDNode *node, const char *curve);
bool config_getLabel(char *dest, uint16_t dest_size);
void config_setLabel(const char *label);
@ -108,10 +108,7 @@ bool config_getPassphraseProtection(bool *passphrase_protection);
bool config_getHomescreen(uint8_t *dest, uint16_t dest_size);
void config_setHomescreen(const uint8_t *data, uint32_t size);
void session_cachePassphrase(const char *passphrase);
bool session_isPassphraseCached(void);
bool session_getState(const uint8_t *salt, uint8_t *state,
const char *passphrase);
const uint8_t *session_getSessionId(void);
bool config_setMnemonic(const char *mnemonic);
bool config_containsMnemonic(const char *mnemonic);

@ -212,10 +212,7 @@ static HDNode *fsm_getDerivedNode(const char *curve, const uint32_t *address_n,
if (fingerprint) {
*fingerprint = 0;
}
if (!config_getRootNode(&node, curve, true)) {
fsm_sendFailure(FailureType_Failure_NotInitialized,
_("Device not initialized or passphrase request cancelled "
"or unsupported curve"));
if (!config_getRootNode(&node, curve)) {
layoutHome();
return 0;
}

@ -20,17 +20,14 @@
void fsm_msgInitialize(const Initialize *msg) {
recovery_abort();
signing_abort();
if (msg && msg->has_state && msg->state.size == 64) {
uint8_t i_state[64];
if (!session_getState(msg->state.bytes, i_state, NULL)) {
session_clear(false); // do not clear PIN
} else {
if (0 != memcmp(msg->state.bytes, i_state, 64)) {
session_clear(false); // do not clear PIN
}
if (msg && msg->has_session_id && msg->session_id.size == 32) {
if (0 != memcmp(session_getSessionId(), msg->session_id.bytes, 32)) {
// If session id was specified but does not match -> clear the cache.
session_clear(false); // do not lock
}
} else {
session_clear(false); // do not clear PIN
// If session id was not specified -> clear the cache.
session_clear(false); // do not lock
}
layoutHome();
fsm_msgGetFeatures(0);
@ -39,6 +36,12 @@ void fsm_msgInitialize(const Initialize *msg) {
void fsm_msgGetFeatures(const GetFeatures *msg) {
(void)msg;
RESP_INIT(Features);
resp->has_session_id = true;
memcpy(resp->session_id.bytes, session_getSessionId(),
sizeof(resp->session_id.bytes));
resp->session_id.size = sizeof(resp->session_id.bytes);
resp->has_vendor = true;
strlcpy(resp->vendor, "trezor.io", sizeof(resp->vendor));
resp->has_major_version = true;
@ -71,8 +74,6 @@ void fsm_msgGetFeatures(const GetFeatures *msg) {
resp->has_imported = config_getImported(&(resp->imported));
resp->has_pin_cached = true;
resp->pin_cached = session_isUnlocked() && config_hasPin();
resp->has_passphrase_cached = true;
resp->passphrase_cached = session_isPassphraseCached();
resp->has_needs_backup = true;
config_getNeedsBackup(&(resp->needs_backup));
resp->has_unfinished_backup = true;
@ -120,17 +121,6 @@ void fsm_msgPing(const Ping *msg) {
}
}
if (msg->has_pin_protection && msg->pin_protection) {
CHECK_PIN
}
if (msg->has_passphrase_protection && msg->passphrase_protection) {
if (!protectPassphrase()) {
fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL);
return;
}
}
if (msg->has_message) {
resp->has_message = true;
memcpy(&(resp->message), &(msg->message), sizeof(resp->message));
@ -359,6 +349,10 @@ void fsm_msgClearSession(const ClearSession *msg) {
}
void fsm_msgApplySettings(const ApplySettings *msg) {
CHECK_PARAM(
!msg->has_passphrase_always_on_device,
_("This firmware is incapable of passphrase entry on the device."));
CHECK_PARAM(msg->has_label || msg->has_language || msg->has_use_passphrase ||
msg->has_homescreen || msg->has_auto_lock_delay_ms,
_("No setting provided"));

@ -350,10 +350,12 @@ bool protectChangeWipeCode(bool removal) {
return ret;
}
bool protectPassphrase(void) {
bool protectPassphrase(char *passphrase) {
memzero(passphrase, MAX_PASSPHRASE_LEN + 1);
bool passphrase_protection = false;
config_getPassphraseProtection(&passphrase_protection);
if (!passphrase_protection || session_isPassphraseCached()) {
if (!passphrase_protection) {
// passphrase already set to empty by memzero above
return true;
}
@ -369,12 +371,24 @@ bool protectPassphrase(void) {
bool result;
for (;;) {
usbPoll();
// TODO: correctly process PassphraseAck with state field set (mismatch =>
// Failure)
if (msg_tiny_id == MessageType_MessageType_PassphraseAck) {
msg_tiny_id = 0xFFFF;
PassphraseAck *ppa = (PassphraseAck *)msg_tiny;
session_cachePassphrase(ppa->has_passphrase ? ppa->passphrase : "");
if (ppa->has_on_device && ppa->on_device == true) {
fsm_sendFailure(
FailureType_Failure_DataError,
_("This firmware is incapable of passphrase entry on the device."));
result = false;
break;
}
if (!ppa->has_passphrase) {
fsm_sendFailure(FailureType_Failure_DataError,
_("No passphrase provided. Use empty string to set an "
"empty passphrase."));
result = false;
break;
}
strlcpy(passphrase, ppa->passphrase, sizeof(ppa->passphrase));
result = true;
break;
}
@ -383,8 +397,9 @@ bool protectPassphrase(void) {
protectAbortedByInitialize =
(msg_tiny_id == MessageType_MessageType_Initialize);
if (protectAbortedByCancel || protectAbortedByInitialize) {
msg_tiny_id = 0xFFFF;
fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL);
result = false;
msg_tiny_id = 0xFFFF;
break;
}
}

@ -24,13 +24,15 @@
#include "messages-common.pb.h"
#include "secbool.h"
#define MAX_PASSPHRASE_LEN 50
bool protectButton(ButtonRequestType type, bool confirm_only);
secbool protectPinUiCallback(uint32_t wait, uint32_t progress,
const char* message);
bool protectPin(bool use_cached);
bool protectChangePin(bool removal);
bool protectChangeWipeCode(bool removal);
bool protectPassphrase(void);
bool protectPassphrase(char* passphrase);
extern bool protectAbortedByCancel;
extern bool protectAbortedByInitialize;

@ -5,9 +5,6 @@ Failure.message max_size:256
PinMatrixAck.pin max_size:10
PassphraseAck.passphrase max_size:51
PassphraseAck.state max_size:64
PassphraseStateRequest.state max_size:64
HDNodeType.chain_code max_size:32
HDNodeType.private_key max_size:32

@ -1,4 +1,4 @@
Initialize.state max_size:64
Initialize.session_id max_size:32
Features.vendor max_size:33
Features.device_id max_size:25
@ -10,6 +10,7 @@ Features.model max_size:17
Features.fw_vendor max_size:256
Features.fw_vendor_keys max_size:32
Features.capabilities max_count:32
Features.session_id max_size:32
ApplySettings.language max_size:17
ApplySettings.label max_size:33

@ -109,6 +109,8 @@ fl.write(
messages = defaultdict(list)
for message in MessageType.DESCRIPTOR.values:
if message.GetOptions().deprecated:
continue
extension = get_wire_extension(message)
messages[extension].append(message)

@ -1460,7 +1460,7 @@ const HDNode *stellar_deriveNode(const uint32_t *address_n,
const char *curve = "ed25519";
// Device not initialized, passphrase request cancelled, or unsupported curve
if (!config_getRootNode(&node, curve, true)) {
if (!config_getRootNode(&node, curve)) {
return 0;
}
// Failed to derive private key

@ -1,7 +1,7 @@
#define VERSION_MAJOR 1
#define VERSION_MINOR 8
#define VERSION_PATCH 4
#define VERSION_MINOR 9
#define VERSION_PATCH 0
#define FIX_VERSION_MAJOR 1
#define FIX_VERSION_MINOR 8
#define FIX_VERSION_MINOR 9
#define FIX_VERSION_PATCH 0

@ -6,13 +6,38 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
_At the moment, the project does **not** adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). That is expected to change with version 1.0._
## [0.11.7] - Unreleased
## [0.12.0] - Unreleased
### Added
- support for firmwares 1.9.0 and 2.3.0
- Model T now defaults to entering passphrase on device. New trezorctl option `-P`
enforces entering passphrase on host.
- support for "passphrase always on device" mode on model T
- new trezorctl command `get-session` and option `-s` allows entering passphrase once
for multiple subsequent trezorctl operations
- built-in functionality of UdpTransport to wait until an emulator comes up, and the
related command `trezorctl wait-for-emulator`
### Changed
- API for `cosi` module was streamlined: `verify_m_of_n` is now `verify`, the old
`verify` is `verify_combined`
- internals of firmware parsing were reworked to support signing firmware headers
- `get_default_client` respects `TREZOR_PATH` environment variable
- UI callback `get_passphrase` has an additional argument `available_on_device`,
indicating that the connected Trezor is capable of on-device entry
### Fixed
- trezorctl does not print empty line when there is no output
### Removed
- `btc.sign_tx` will not preload transaction data from `prev_txes`, as usage with TxApi
is being phased out
- PIN protection and passphrase protection for `ping()` command was removed
## [0.11.6] - 2019-12-30
[0.11.6]: https://github.com/trezor/trezor-firmware/compare/python/v0.11.5...python/v0.11.6

@ -1,4 +1,4 @@
__version__ = "0.11.6"
__version__ = "0.12.0"
# fmt: off
MINIMUM_FIRMWARE_VERSION = {

@ -16,15 +16,9 @@
import click
from .. import device, messages
from .. import device
from . import ChoiceType
PASSPHRASE_SOURCE = {
"ask": messages.PassphraseSourceType.ASK,
"device": messages.PassphraseSourceType.DEVICE,
"host": messages.PassphraseSourceType.HOST,
}
ROTATION = {"north": 0, "east": 90, "south": 180, "west": 270}
@ -145,10 +139,13 @@ def passphrase():
@passphrase.command(name="enabled")
@click.option("-f/-F", "--force-on-device/--no-force-on-device", default=None)
@click.pass_obj
def passphrase_enable(connect):
def passphrase_enable(connect, force_on_device: bool):
"""Enable passphrase."""
return device.apply_settings(connect(), use_passphrase=True)
return device.apply_settings(
connect(), use_passphrase=True, passphrase_always_on_device=force_on_device
)
@passphrase.command(name="disabled")
@ -156,19 +153,3 @@ def passphrase_enable(connect):
def passphrase_disable(connect):
"""Disable passphrase."""
return device.apply_settings(connect(), use_passphrase=False)
@passphrase.command(name="source")
@click.argument("source", type=ChoiceType(PASSPHRASE_SOURCE))
@click.pass_obj
def passphrase_source(connect, source):
"""Set passphrase source.
Configure how to enter passphrase on Trezor Model T. The options are:
\b
ask - always ask where to enter passphrase
device - always enter passphrase on device
host - always enter passphrase on host
"""
return device.apply_settings(connect(), passphrase_source=source)

@ -52,7 +52,6 @@ COMMAND_ALIASES = {
"change-pin": settings.pin,
"enable-passphrase": settings.passphrase_enable,
"disable-passphrase": settings.passphrase_disable,
"set-passphrase-source": settings.passphrase_source,
"wipe-device": device.wipe,
"reset-device": device.setup,
"recovery-device": device.recover,
@ -143,11 +142,26 @@ def configure_logging(verbose: int):
@click.option(
"-j", "--json", "is_json", is_flag=True, help="Print result as JSON object"
)
@click.option(
"-P", "--passphrase-on-host", is_flag=True, help="Enter passphrase on host.",
)
@click.option(
"-s",
"--session-id",
help="Resume given session ID.",
default=os.environ.get("TREZOR_SESSION_ID"),
)
@click.version_option()
@click.pass_context
def cli(ctx, path, verbose, is_json):
def cli(ctx, path, verbose, is_json, passphrase_on_host, session_id):
configure_logging(verbose)
if session_id:
try:
session_id = bytes.fromhex(session_id)
except ValueError:
raise click.ClickException("Not a valid session id: {}".format(session_id))
def get_device():
try:
device = get_transport(path, prefix_search=False)
@ -159,13 +173,17 @@ def cli(ctx, path, verbose, is_json):
if path is not None:
click.echo("Using path: {}".format(path))
sys.exit(1)
return TrezorClient(transport=device, ui=ui.ClickUI())
return TrezorClient(
transport=device,
ui=ui.ClickUI(passphrase_on_host=passphrase_on_host),
session_id=session_id,
)
ctx.obj = get_device
@cli.resultcallback()
def print_result(res, path, verbose, is_json):
def print_result(res, is_json, **kwargs):
if is_json:
if isinstance(res, protobuf.MessageType):
click.echo(json.dumps({res.__class__.__name__: res.__dict__}))
@ -202,7 +220,7 @@ def list_devices():
@cli.command()
def version():
"""Show version of trezorctl/trezorlib."""
from trezorlib import __version__ as VERSION
from .. import __version__ as VERSION
return VERSION
@ -215,17 +233,33 @@ def version():
@cli.command()
@click.argument("message")
@click.option("-b", "--button-protection", is_flag=True)
@click.option("-p", "--pin-protection", is_flag=True)
@click.option("-r", "--passphrase-protection", is_flag=True)
@click.pass_obj
def ping(connect, message, button_protection, pin_protection, passphrase_protection):
def ping(connect, message, button_protection):
"""Send ping message."""
return connect().ping(
message,
button_protection=button_protection,
pin_protection=pin_protection,
passphrase_protection=passphrase_protection,
)
return connect().ping(message, button_protection=button_protection)
@cli.command()
@click.pass_obj
def get_session(connect):
"""Get a session ID for subsequent commands.
Unlocks Trezor with a passphrase and returns a session ID. Use this session ID with
`trezorctl -s SESSION_ID`, or set it to an environment variable `TREZOR_SESSION_ID`,
to avoid having to enter passphrase for subsequent commands.
The session ID is valid until another client starts using Trezor, until the next
get-session call, or until Trezor is disconnected.
"""
from ..btc import get_address
from ..client import PASSPHRASE_TEST_PATH
client = connect()
get_address(client, "Testnet", PASSPHRASE_TEST_PATH)
if client.session_id is None:
raise click.ClickException("Passphrase not enabled or firmware too old.")
else:
return client.session_id.hex()
@cli.command()

@ -23,6 +23,7 @@ from types import SimpleNamespace
from mnemonic import Mnemonic
from . import MINIMUM_FIRMWARE_VERSION, exceptions, messages, tools
from .messages import Capability
if sys.version_info.major < 3:
raise Exception("Trezorlib does not support Python 2 anymore.")
@ -32,6 +33,9 @@ LOG = logging.getLogger(__name__)
VENDORS = ("bitcointrezor.com", "trezor.io")
MAX_PASSPHRASE_LENGTH = 50
PASSPHRASE_ON_DEVICE = object()
PASSPHRASE_TEST_PATH = tools.parse_path("44h/1h/19h/0/1337")
DEPRECATION_ERROR = """
Incompatible Trezor library detected.
@ -107,15 +111,17 @@ class TrezorClient:
- passphrase request (ask the user to enter a passphrase)
See `trezorlib.ui` for details.
You can supply a `state` you saved in the previous session. If you do,
the user might not need to enter their passphrase again.
You can supply a `session_id` you might have saved in the previous session.
If you do, the user might not need to enter their passphrase again.
"""
def __init__(self, transport, ui=_NO_UI_OBJECT, state=None):
def __init__(
self, transport, ui=_NO_UI_OBJECT, session_id=None,
):
LOG.info("creating client instance for device: {}".format(transport.get_path()))
self.transport = transport
self.ui = ui
self.state = state
self.session_id = session_id
# XXX remove when old Electrum has been cycled out.
# explanation: We changed the API in 0.11 and this broke older versions
@ -177,30 +183,47 @@ class TrezorClient:
else:
return resp
def _callback_passphrase(self, msg):
if msg.on_device:
passphrase = None
else:
try:
passphrase = self.ui.get_passphrase()
except exceptions.Cancelled:
self.call_raw(messages.Cancel())
raise
def _callback_passphrase(self, msg: messages.PassphraseRequest):
available_on_device = Capability.PassphraseEntry in self.features.capabilities
passphrase = Mnemonic.normalize_string(passphrase)
if len(passphrase) > MAX_PASSPHRASE_LENGTH:
def send_passphrase(passphrase=None, on_device=None):
if self.features.model == "1":
state = None
else:
state = self.session_id
msg = messages.PassphraseAck(
_state=state, passphrase=passphrase, on_device=on_device
)
resp = self.call_raw(msg)
if isinstance(resp, messages.Deprecated_PassphraseStateRequest):
self.session_id = resp.state
resp = self.call_raw(messages.Deprecated_PassphraseStateAck())
return resp
# short-circuit old style entry
if msg._on_device is True:
return send_passphrase(None, None)
try:
passphrase = self.ui.get_passphrase(available_on_device=available_on_device)
except exceptions.Cancelled:
self.call_raw(messages.Cancel())
raise
if passphrase is PASSPHRASE_ON_DEVICE:
if not available_on_device:
self.call_raw(messages.Cancel())
raise ValueError("Passphrase too long")
raise RuntimeError("Device is not capable of entering passphrase")
else:
return send_passphrase(on_device=True)
resp = self.call_raw(
messages.PassphraseAck(passphrase=passphrase, state=self.state)
)
if isinstance(resp, messages.PassphraseStateRequest):
# TODO report to the user that the passphrase has changed?
self.state = resp.state
return self.call_raw(messages.PassphraseStateAck())
else:
return resp
# else process host-entered passphrase
passphrase = Mnemonic.normalize_string(passphrase)
if len(passphrase) > MAX_PASSPHRASE_LENGTH:
self.call_raw(messages.Cancel())
raise ValueError("Passphrase too long")
return send_passphrase(passphrase, on_device=False)
def _callback_button(self, msg):
__tracebackhide__ = True # for pytest # pylint: disable=W0612
@ -229,7 +252,7 @@ class TrezorClient:
@tools.session
def init_device(self):
resp = self.call_raw(messages.Initialize(state=self.state))
resp = self.call_raw(messages.Initialize(session_id=self.session_id))
if not isinstance(resp, messages.Features):
raise exceptions.TrezorException("Unexpected initial response")
else:
@ -245,6 +268,8 @@ class TrezorClient:
self.features.patch_version,
)
self.check_firmware_version(warn_only=True)
if self.features.session_id is not None:
self.session_id = self.features.session_id
def is_outdated(self):
if self.features.bootloader_mode:
@ -262,17 +287,13 @@ class TrezorClient:
@tools.expect(messages.Success, field="message")
def ping(
self,
msg,
button_protection=False,
pin_protection=False,
passphrase_protection=False,
self, msg, button_protection=False,
):
# We would like ping to work on any valid TrezorClient instance, but
# due to the protection modes, we need to go through self.call, and that will
# raise an exception if the firmware is too old.
# So we short-circuit the simplest variant of ping with call_raw.
if not button_protection and not pin_protection and not passphrase_protection:
if not button_protection:
# XXX this should be: `with self:`
try:
self.open()
@ -280,12 +301,7 @@ class TrezorClient:
finally:
self.close()
msg = messages.Ping(
message=msg,
button_protection=button_protection,
pin_protection=pin_protection,
passphrase_protection=passphrase_protection,
)
msg = messages.Ping(message=msg, button_protection=button_protection,)
return self.call(msg)
def get_device_id(self):
@ -295,7 +311,7 @@ class TrezorClient:
def clear_session(self):
resp = self.call_raw(messages.ClearSession())
if isinstance(resp, messages.Success):
self.state = None
self.session_id = None
self.init_device()
return resp.message
else:

@ -226,7 +226,7 @@ class DebugUI:
else:
return self.debuglink.read_pin_encoded()
def get_passphrase(self):
def get_passphrase(self, available_on_device):
return self.passphrase

@ -51,7 +51,7 @@ def apply_settings(
language=None,
use_passphrase=None,
homescreen=None,
passphrase_source=None,
passphrase_always_on_device=None,
auto_lock_delay_ms=None,
display_rotation=None,
):
@ -64,8 +64,8 @@ def apply_settings(
settings.use_passphrase = use_passphrase
if homescreen is not None:
settings.homescreen = homescreen
if passphrase_source is not None:
settings.passphrase_source = passphrase_source
if passphrase_always_on_device is not None:
settings.passphrase_always_on_device = passphrase_always_on_device
if auto_lock_delay_ms is not None:
settings.auto_lock_delay_ms = auto_lock_delay_ms
if display_rotation is not None:

@ -6,7 +6,6 @@ if __debug__:
try:
from typing import Dict, List # noqa: F401
from typing_extensions import Literal # noqa: F401
EnumTypePassphraseSourceType = Literal[0, 1, 2]
except ImportError:
pass
@ -20,17 +19,17 @@ class ApplySettings(p.MessageType):
label: str = None,
use_passphrase: bool = None,
homescreen: bytes = None,
passphrase_source: EnumTypePassphraseSourceType = None,
auto_lock_delay_ms: int = None,
display_rotation: int = None,
passphrase_always_on_device: bool = None,
) -> None:
self.language = language
self.label = label
self.use_passphrase = use_passphrase
self.homescreen = homescreen
self.passphrase_source = passphrase_source
self.auto_lock_delay_ms = auto_lock_delay_ms
self.display_rotation = display_rotation
self.passphrase_always_on_device = passphrase_always_on_device
@classmethod
def get_fields(cls) -> Dict:
@ -39,7 +38,7 @@ class ApplySettings(p.MessageType):
2: ('label', p.UnicodeType, 0),
3: ('use_passphrase', p.BoolType, 0),
4: ('homescreen', p.BytesType, 0),
5: ('passphrase_source', p.EnumType("PassphraseSourceType", (0, 1, 2)), 0),
6: ('auto_lock_delay_ms', p.UVarintType, 0),
7: ('display_rotation', p.UVarintType, 0),
8: ('passphrase_always_on_device', p.BoolType, 0),
}

@ -6,7 +6,7 @@ if __debug__:
try:
from typing import Dict, List # noqa: F401
from typing_extensions import Literal # noqa: F401
EnumTypeButtonRequestType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
EnumTypeButtonRequestType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19]
except ImportError:
pass
@ -23,5 +23,5 @@ class ButtonRequest(p.MessageType):
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('code', p.EnumType("ButtonRequestType", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18)), 0),
1: ('code', p.EnumType("ButtonRequestType", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19)), 0),
}

@ -16,8 +16,8 @@ Address = 10 # type: Literal[10]
PublicKey = 11 # type: Literal[11]
MnemonicWordCount = 12 # type: Literal[12]
MnemonicInput = 13 # type: Literal[13]
PassphraseType = 14 # type: Literal[14]
UnknownDerivationPath = 15 # type: Literal[15]
RecoveryHomepage = 16 # type: Literal[16]
Success = 17 # type: Literal[17]
Warning = 18 # type: Literal[18]
PassphraseEntry = 19 # type: Literal[19]

@ -19,3 +19,4 @@ Tezos = 13 # type: Literal[13]
U2F = 14 # type: Literal[14]
Shamir = 15 # type: Literal[15]
ShamirGroups = 16 # type: Literal[16]
PassphraseEntry = 17 # type: Literal[17]

@ -10,5 +10,5 @@ if __debug__:
pass
class PassphraseStateAck(p.MessageType):
class Deprecated_PassphraseStateAck(p.MessageType):
MESSAGE_WIRE_TYPE = 78

@ -10,7 +10,7 @@ if __debug__:
pass
class PassphraseStateRequest(p.MessageType):
class Deprecated_PassphraseStateRequest(p.MessageType):
MESSAGE_WIRE_TYPE = 77
def __init__(

@ -6,7 +6,7 @@ if __debug__:
try:
from typing import Dict, List # noqa: F401
from typing_extensions import Literal # noqa: F401
EnumTypeCapability = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
EnumTypeCapability = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
EnumTypeBackupType = Literal[0, 1, 2]
except ImportError:
pass
@ -32,7 +32,6 @@ class Features(p.MessageType):
bootloader_hash: bytes = None,
imported: bool = None,
pin_cached: bool = None,
passphrase_cached: bool = None,
firmware_present: bool = None,
needs_backup: bool = None,
flags: int = None,
@ -50,6 +49,8 @@ class Features(p.MessageType):
sd_card_present: bool = None,
sd_protection: bool = None,
wipe_code_protection: bool = None,
session_id: bytes = None,
passphrase_always_on_device: bool = None,
) -> None:
self.vendor = vendor
self.major_version = major_version
@ -66,7 +67,6 @@ class Features(p.MessageType):
self.bootloader_hash = bootloader_hash
self.imported = imported
self.pin_cached = pin_cached
self.passphrase_cached = passphrase_cached
self.firmware_present = firmware_present
self.needs_backup = needs_backup
self.flags = flags
@ -84,6 +84,8 @@ class Features(p.MessageType):
self.sd_card_present = sd_card_present
self.sd_protection = sd_protection
self.wipe_code_protection = wipe_code_protection
self.session_id = session_id
self.passphrase_always_on_device = passphrase_always_on_device
@classmethod
def get_fields(cls) -> Dict:
@ -103,7 +105,6 @@ class Features(p.MessageType):
14: ('bootloader_hash', p.BytesType, 0),
15: ('imported', p.BoolType, 0),
16: ('pin_cached', p.BoolType, 0),
17: ('passphrase_cached', p.BoolType, 0),
18: ('firmware_present', p.BoolType, 0),
19: ('needs_backup', p.BoolType, 0),
20: ('flags', p.UVarintType, 0),
@ -116,9 +117,11 @@ class Features(p.MessageType):
27: ('unfinished_backup', p.BoolType, 0),
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),
30: ('capabilities', p.EnumType("Capability", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)), p.FLAG_REPEATED),
31: ('backup_type', p.EnumType("BackupType", (0, 1, 2)), 0),
32: ('sd_card_present', p.BoolType, 0),
33: ('sd_protection', p.BoolType, 0),
34: ('wipe_code_protection', p.BoolType, 0),
35: ('session_id', p.BytesType, 0),
36: ('passphrase_always_on_device', p.BoolType, 0),
}

@ -15,15 +15,12 @@ class Initialize(p.MessageType):
def __init__(
self,
state: bytes = None,
skip_passphrase: bool = None,
session_id: bytes = None,
) -> None:
self.state = state
self.skip_passphrase = skip_passphrase
self.session_id = session_id
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('state', p.BytesType, 0),
2: ('skip_passphrase', p.BoolType, 0),
1: ('session_id', p.BytesType, 0),
}

@ -27,8 +27,6 @@ EntropyRequest = 35 # type: Literal[35]
EntropyAck = 36 # type: Literal[36]
PassphraseRequest = 41 # type: Literal[41]
PassphraseAck = 42 # type: Literal[42]
PassphraseStateRequest = 77 # type: Literal[77]
PassphraseStateAck = 78 # type: Literal[78]
RecoveryDevice = 45 # type: Literal[45]
WordRequest = 46 # type: Literal[46]
WordAck = 47 # type: Literal[47]
@ -38,6 +36,8 @@ SdProtect = 79 # type: Literal[79]
GetNextU2FCounter = 80 # type: Literal[80]
NextU2FCounter = 81 # type: Literal[81]
ChangeWipeCode = 82 # type: Literal[82]
Deprecated_PassphraseStateRequest = 77 # type: Literal[77]
Deprecated_PassphraseStateAck = 78 # type: Literal[78]
FirmwareErase = 6 # type: Literal[6]
FirmwareUpload = 7 # type: Literal[7]
FirmwareRequest = 8 # type: Literal[8]

@ -16,14 +16,17 @@ class PassphraseAck(p.MessageType):
def __init__(
self,
passphrase: str = None,
state: bytes = None,
_state: bytes = None,
on_device: bool = None,
) -> None:
self.passphrase = passphrase
self.state = state
self._state = _state
self.on_device = on_device
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('passphrase', p.UnicodeType, 0),
2: ('state', p.BytesType, 0),
2: ('_state', p.BytesType, 0),
3: ('on_device', p.BoolType, 0),
}

@ -15,12 +15,12 @@ class PassphraseRequest(p.MessageType):
def __init__(
self,
on_device: bool = None,
_on_device: bool = None,
) -> None:
self.on_device = on_device
self._on_device = _on_device
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('on_device', p.BoolType, 0),
1: ('_on_device', p.BoolType, 0),
}

@ -1,8 +0,0 @@
# Automatically generated by pb2py
# fmt: off
if False:
from typing_extensions import Literal
ASK = 0 # type: Literal[0]
DEVICE = 1 # type: Literal[1]
HOST = 2 # type: Literal[2]

@ -17,19 +17,13 @@ class Ping(p.MessageType):
self,
message: str = None,
button_protection: bool = None,
pin_protection: bool = None,
passphrase_protection: bool = None,
) -> None:
self.message = message
self.button_protection = button_protection
self.pin_protection = pin_protection
self.passphrase_protection = passphrase_protection
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('message', p.UnicodeType, 0),
2: ('button_protection', p.BoolType, 0),
3: ('pin_protection', p.BoolType, 0),
4: ('passphrase_protection', p.BoolType, 0),
}

@ -55,6 +55,8 @@ from .DebugLinkState import DebugLinkState
from .DebugLinkStop import DebugLinkStop
from .DebugMoneroDiagAck import DebugMoneroDiagAck
from .DebugMoneroDiagRequest import DebugMoneroDiagRequest
from .Deprecated_PassphraseStateAck import Deprecated_PassphraseStateAck
from .Deprecated_PassphraseStateRequest import Deprecated_PassphraseStateRequest
from .ECDHSessionKey import ECDHSessionKey
from .Entropy import Entropy
from .EntropyAck import EntropyAck
@ -196,8 +198,6 @@ from .NEMTransfer import NEMTransfer
from .NextU2FCounter import NextU2FCounter
from .PassphraseAck import PassphraseAck
from .PassphraseRequest import PassphraseRequest
from .PassphraseStateAck import PassphraseStateAck
from .PassphraseStateRequest import PassphraseStateRequest
from .PinMatrixAck import PinMatrixAck
from .PinMatrixRequest import PinMatrixRequest
from .Ping import Ping
@ -283,7 +283,6 @@ from . import NEMModificationType
from . import NEMMosaicLevy
from . import NEMSupplyChangeType
from . import OutputScriptType
from . import PassphraseSourceType
from . import PinMatrixRequestType
from . import RecoveryDeviceType
from . import RequestType

@ -20,6 +20,7 @@ import click
from mnemonic import Mnemonic
from . import device
from .client import PASSPHRASE_ON_DEVICE
from .exceptions import Cancelled
from .messages import PinMatrixRequestType, WordRequestType
@ -58,10 +59,11 @@ def prompt(*args, **kwargs):
class ClickUI:
def __init__(self, always_prompt=False):
def __init__(self, always_prompt=False, passphrase_on_host=False):
self.pinmatrix_shown = False
self.prompt_shown = False
self.always_prompt = always_prompt
self.passphrase_on_host = passphrase_on_host
def button_request(self, code):
if not self.prompt_shown:
@ -98,7 +100,10 @@ class ClickUI:
else:
return pin
def get_passphrase(self):
def get_passphrase(self, available_on_device):
if available_on_device and not self.passphrase_on_host:
return PASSPHRASE_ON_DEVICE
if os.getenv("PASSPHRASE") is not None:
echo("Passphrase required. Using PASSPHRASE environment variable.")
return os.getenv("PASSPHRASE")

1
tests/.gitignore vendored

@ -1 +1,2 @@
junit.xml
trezor.log

@ -20,8 +20,7 @@ import pytest
from trezorlib import debuglink, log
from trezorlib.debuglink import TrezorClientDebugLink
from trezorlib.device import apply_settings, wipe as wipe_device
from trezorlib.messages.PassphraseSourceType import HOST as PASSPHRASE_ON_HOST
from trezorlib.device import wipe as wipe_device
from trezorlib.transport import enumerate_devices, get_transport
from . import ui_tests
@ -36,8 +35,8 @@ def get_device():
try:
transport = get_transport(path)
return TrezorClientDebugLink(transport, auto_interact=not interact)
except Exception as e:
raise RuntimeError("Failed to open debuglink for {}".format(path)) from e
except Exception:
pytest.exit("Failed to open debuglink for {}".format(path), 3)
else:
devices = enumerate_devices()
@ -47,7 +46,7 @@ def get_device():
except Exception:
pass
else:
raise RuntimeError("No debuggable device found")
pytest.exit("No debuggable device found", 3)
@pytest.fixture(scope="function")
@ -130,8 +129,6 @@ def client(request):
needs_backup=setup_params["needs_backup"],
no_backup=setup_params["no_backup"],
)
if setup_params["passphrase"] and client.features.model != "1":
apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST)
if setup_params["pin"]:
# ClearSession locks the device. We only do that if the PIN is set.

@ -20,7 +20,7 @@ from trezorlib import device, messages
class TestBasic:
def test_features(self, client):
f0 = client.features
f1 = client.call(messages.Initialize())
f1 = client.call(messages.Initialize(f0.session_id))
assert f0 == f1
def test_ping(self, client):

@ -34,7 +34,7 @@ class TestDebuglink:
@pytest.mark.setup_client(mnemonic=MNEMONIC12, pin="1234", passphrase=True)
def test_pin(self, client):
resp = client.call_raw(messages.Ping(message="test", pin_protection=True))
resp = client.call_raw(messages.GetAddress())
assert isinstance(resp, messages.PinMatrixRequest)
pin, matrix = client.debug.read_pin()
@ -43,4 +43,7 @@ class TestDebuglink:
pin_encoded = client.debug.read_pin_encoded()
resp = client.call_raw(messages.PinMatrixAck(pin=pin_encoded))
assert isinstance(resp, messages.Success)
assert isinstance(resp, messages.PassphraseRequest)
resp = client.call_raw(messages.PassphraseAck(passphrase=""))
assert isinstance(resp, messages.Address)

@ -18,7 +18,7 @@ import time
import pytest
from trezorlib import device, messages as proto
from trezorlib import btc, device, messages as proto
EXPECTED_RESPONSES_NOPIN = [proto.ButtonRequest(), proto.Success(), proto.Features()]
EXPECTED_RESPONSES_PIN = [proto.PinMatrixRequest()] + EXPECTED_RESPONSES_NOPIN
@ -91,13 +91,13 @@ class TestMsgApplysettings:
time.sleep(0.1) # sleep less than auto-lock delay
with client:
# No PIN protection is required.
client.set_expected_responses([proto.Success()])
client.ping(msg="", pin_protection=True)
client.set_expected_responses([proto.Address()])
btc.get_address(client, "Testnet", [0])
time.sleep(10.1) # sleep more than auto-lock delay
with client:
client.set_expected_responses([proto.PinMatrixRequest(), proto.Success()])
client.ping(msg="", pin_protection=True)
client.set_expected_responses([proto.PinMatrixRequest(), proto.Address()])
btc.get_address(client, "Testnet", [0])
@pytest.mark.skip_t2
def test_apply_minimal_auto_lock_delay(self, client):
@ -114,16 +114,16 @@ class TestMsgApplysettings:
time.sleep(0.1) # sleep less than auto-lock delay
with client:
# No PIN protection is required.
client.set_expected_responses([proto.Success()])
client.ping(msg="", pin_protection=True)
client.set_expected_responses([proto.Address()])
btc.get_address(client, "Testnet", [0])
time.sleep(2) # sleep less than the minimal auto-lock delay
with client:
# No PIN protection is required.
client.set_expected_responses([proto.Success()])
client.ping(msg="", pin_protection=True)
client.set_expected_responses([proto.Address()])
btc.get_address(client, "Testnet", [0])
time.sleep(10.1) # sleep more than the minimal auto-lock delay
with client:
client.set_expected_responses([proto.PinMatrixRequest(), proto.Success()])
client.ping(msg="", pin_protection=True)
client.set_expected_responses([proto.PinMatrixRequest(), proto.Address()])
btc.get_address(client, "Testnet", [0])

@ -119,10 +119,7 @@ def test_cardano_sign_tx(
inputs = [cardano.create_input(i) for i in inputs]
outputs = [cardano.create_output(o) for o in outputs]
expected_responses = [
messages.PassphraseRequest(),
messages.PassphraseStateRequest(),
]
expected_responses = [messages.PassphraseRequest()]
expected_responses += [
messages.CardanoTxRequest(tx_index=i) for i in range(len(transactions))
]

@ -225,5 +225,5 @@ def test_set_pin_to_wipe_code(client):
# Check that there is no PIN protection.
client.init_device()
assert client.features.pin_protection is False
ret = client.call_raw(messages.Ping(pin_protection=True))
assert isinstance(ret, messages.Success)
resp = client.call_raw(messages.GetAddress())
assert isinstance(resp, messages.Address)

@ -29,8 +29,8 @@ class TestMsgChangepin:
assert features.pin_protection is False
# Check that there's no PIN protection
ret = client.call_raw(proto.Ping(pin_protection=True))
assert isinstance(ret, proto.Success)
ret = client.call_raw(proto.GetAddress())
assert isinstance(ret, proto.Address)
# Let's set new PIN
ret = client.call_raw(proto.ChangePin())
@ -66,7 +66,7 @@ class TestMsgChangepin:
assert features.pin_protection is True
# Check that there's PIN protection
ret = client.call_raw(proto.Ping(pin_protection=True))
ret = client.call_raw(proto.GetAddress())
assert isinstance(ret, proto.PinMatrixRequest)
client.call_raw(proto.Cancel())
@ -112,7 +112,7 @@ class TestMsgChangepin:
assert features.pin_protection is True
# Check that there's PIN protection
ret = client.call_raw(proto.Ping(pin_protection=True))
ret = client.call_raw(proto.GetAddress())
assert isinstance(ret, proto.PinMatrixRequest)
client.call_raw(proto.Cancel())
@ -135,16 +135,16 @@ class TestMsgChangepin:
# Check that there's no PIN protection now
features = client.call_raw(proto.Initialize())
assert features.pin_protection is False
ret = client.call_raw(proto.Ping(pin_protection=True))
assert isinstance(ret, proto.Success)
ret = client.call_raw(proto.GetAddress())
assert isinstance(ret, proto.Address)
def test_set_failed(self, client):
features = client.call_raw(proto.Initialize())
assert features.pin_protection is False
# Check that there's no PIN protection
ret = client.call_raw(proto.Ping(pin_protection=True))
assert isinstance(ret, proto.Success)
ret = client.call_raw(proto.GetAddress())
assert isinstance(ret, proto.Address)
# Let's set new PIN
ret = client.call_raw(proto.ChangePin())
@ -170,8 +170,8 @@ class TestMsgChangepin:
# Check that there's still no PIN protection now
features = client.call_raw(proto.Initialize())
assert features.pin_protection is False
ret = client.call_raw(proto.Ping(pin_protection=True))
assert isinstance(ret, proto.Success)
ret = client.call_raw(proto.GetAddress())
assert isinstance(ret, proto.Address)
@pytest.mark.setup_client(pin=True)
def test_set_failed_2(self, client):
@ -211,8 +211,8 @@ class TestMsgChangepin:
def check_pin(self, client, pin):
client.clear_session()
ret = client.call_raw(proto.Ping(pin_protection=True))
ret = client.call_raw(proto.GetAddress())
assert isinstance(ret, proto.PinMatrixRequest)
pin_encoded = client.debug.encode_pin(pin)
ret = client.call_raw(proto.PinMatrixAck(pin=pin_encoded))
assert isinstance(ret, proto.Success)
assert isinstance(ret, proto.Address)

@ -16,78 +16,41 @@
import pytest
from trezorlib import messages as proto
from trezorlib import messages
from trezorlib.btc import get_public_node
from trezorlib.tools import parse_path
ADDRESS_N = parse_path("44'/0'/0'")
XPUB = "xpub6BiVtCpG9fQPxnPmHXG8PhtzQdWC2Su4qWu6XW9tpWFYhxydCLJGrWBJZ5H6qTAHdPQ7pQhtpjiYZVZARo14qHiay2fvrX996oEP42u8wZy"
@pytest.mark.skip_t2
class TestMsgClearsession:
@pytest.mark.setup_client(pin=True, passphrase=True)
def test_clearsession(self, client):
with client:
client.set_expected_responses(
[
proto.ButtonRequest(code=proto.ButtonRequestType.ProtectCall),
proto.PinMatrixRequest(),
proto.PassphraseRequest(),
proto.Success(),
]
)
res = client.ping(
"random data",
button_protection=True,
pin_protection=True,
passphrase_protection=True,
)
assert res == "random data"
with client:
# pin and passphrase are cached
client.set_expected_responses(
[
proto.ButtonRequest(code=proto.ButtonRequestType.ProtectCall),
proto.Success(),
]
)
res = client.ping(
"random data",
button_protection=True,
pin_protection=True,
passphrase_protection=True,
)
assert res == "random data"
@pytest.mark.skip_ui
@pytest.mark.setup_client(pin=True, passphrase=True)
def test_clear_session(client):
if client.features.model == "1":
init_responses = [messages.PinMatrixRequest(), messages.PassphraseRequest()]
else:
init_responses = [messages.PassphraseRequest()]
client.clear_session()
cached_responses = [messages.PublicKey()]
# session cache is cleared
with client:
client.set_expected_responses(
[
proto.ButtonRequest(code=proto.ButtonRequestType.ProtectCall),
proto.PinMatrixRequest(),
proto.PassphraseRequest(),
proto.Success(),
]
)
res = client.ping(
"random data",
button_protection=True,
pin_protection=True,
passphrase_protection=True,
)
assert res == "random data"
with client:
client.set_expected_responses(init_responses + cached_responses)
assert get_public_node(client, ADDRESS_N).xpub == XPUB
with client:
# pin and passphrase are cached
client.set_expected_responses(
[
proto.ButtonRequest(code=proto.ButtonRequestType.ProtectCall),
proto.Success(),
]
)
res = client.ping(
"random data",
button_protection=True,
pin_protection=True,
passphrase_protection=True,
)
assert res == "random data"
with client:
# pin and passphrase are cached
client.set_expected_responses(cached_responses)
assert get_public_node(client, ADDRESS_N).xpub == XPUB
client.clear_session()
# session cache is cleared
with client:
client.set_expected_responses(init_responses + cached_responses)
assert get_public_node(client, ADDRESS_N).xpub == XPUB
with client:
# pin and passphrase are cached
client.set_expected_responses(cached_responses)
assert get_public_node(client, ADDRESS_N).xpub == XPUB

@ -18,7 +18,6 @@ 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,
@ -53,8 +52,6 @@ class TestDeviceLoad:
passphrase_protection=True,
label="test",
)
if client.features.model == "T":
device.apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST)
client.set_passphrase("passphrase")
state = client.debug.state()
assert state.mnemonic_secret == MNEMONIC12.encode()
@ -108,7 +105,6 @@ class TestDeviceLoad:
u"Neuve\u030cr\u030citelne\u030c bezpec\u030cne\u0301 hesli\u0301c\u030cko"
)
device.wipe(client)
debuglink.load_device(
client,
mnemonic=words_nfkd,
@ -118,8 +114,6 @@ class TestDeviceLoad:
language="en-US",
skip_checksum=True,
)
if client.features.model == "T":
device.apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST)
client.set_passphrase(passphrase_nfkd)
address_nfkd = btc.get_address(client, "Bitcoin", [])
@ -133,8 +127,6 @@ class TestDeviceLoad:
language="en-US",
skip_checksum=True,
)
if client.features.model == "T":
device.apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST)
client.set_passphrase(passphrase_nfc)
address_nfc = btc.get_address(client, "Bitcoin", [])
@ -148,8 +140,6 @@ class TestDeviceLoad:
language="en-US",
skip_checksum=True,
)
if client.features.model == "T":
device.apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST)
client.set_passphrase(passphrase_nfkc)
address_nfkc = btc.get_address(client, "Bitcoin", [])
@ -163,8 +153,6 @@ class TestDeviceLoad:
language="en-US",
skip_checksum=True,
)
if client.features.model == "T":
device.apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST)
client.set_passphrase(passphrase_nfd)
address_nfd = btc.get_address(client, "Bitcoin", [])

@ -19,8 +19,8 @@ import pytest
from trezorlib import messages as proto
@pytest.mark.skip_t2
class TestMsgPing:
@pytest.mark.skip_ui
@pytest.mark.setup_client(pin=True, passphrase=True)
def test_ping(self, client):
with client:
@ -37,48 +37,3 @@ class TestMsgPing:
)
res = client.ping("random data", button_protection=True)
assert res == "random data"
with client:
client.set_expected_responses([proto.PinMatrixRequest(), proto.Success()])
res = client.ping("random data", pin_protection=True)
assert res == "random data"
with client:
client.set_expected_responses([proto.PassphraseRequest(), proto.Success()])
res = client.ping("random data", passphrase_protection=True)
assert res == "random data"
@pytest.mark.setup_client(pin=True, passphrase=True)
def test_ping_caching(self, client):
with client:
client.set_expected_responses(
[
proto.ButtonRequest(code=proto.ButtonRequestType.ProtectCall),
proto.PinMatrixRequest(),
proto.PassphraseRequest(),
proto.Success(),
]
)
res = client.ping(
"random data",
button_protection=True,
pin_protection=True,
passphrase_protection=True,
)
assert res == "random data"
with client:
# pin and passphrase are cached
client.set_expected_responses(
[
proto.ButtonRequest(code=proto.ButtonRequestType.ProtectCall),
proto.Success(),
]
)
res = client.ping(
"random data",
button_protection=True,
pin_protection=True,
passphrase_protection=True,
)
assert res == "random data"

@ -85,7 +85,7 @@ class TestMsgRecoverydevice:
assert client.features.passphrase_protection is True
# Do passphrase-protected action, PassphraseRequest should be raised
resp = client.call_raw(proto.Ping(passphrase_protection=True))
resp = client.call_raw(proto.GetAddress())
assert isinstance(resp, proto.PassphraseRequest)
client.call_raw(proto.Cancel())
@ -136,13 +136,9 @@ class TestMsgRecoverydevice:
assert client.features.pin_protection is False
assert client.features.passphrase_protection is False
# Do passphrase-protected action, PassphraseRequest should NOT be raised
resp = client.call_raw(proto.Ping(passphrase_protection=True))
assert isinstance(resp, proto.Success)
# Do PIN-protected action, PinRequest should NOT be raised
resp = client.call_raw(proto.Ping(pin_protection=True))
assert isinstance(resp, proto.Success)
# Do pin & passphrase-protected action, PassphraseRequest should NOT be raised
resp = client.call_raw(proto.GetAddress())
assert isinstance(resp, proto.Address)
@pytest.mark.setup_client(uninitialized=True)
def test_word_fail(self, client):

@ -87,13 +87,9 @@ class TestMsgResetDevice:
assert resp.pin_protection is False
assert resp.passphrase_protection is False
# Do passphrase-protected action, PassphraseRequest should NOT be raised
resp = client.call_raw(proto.Ping(passphrase_protection=True))
assert isinstance(resp, proto.Success)
# Do PIN-protected action, PinRequest should NOT be raised
resp = client.call_raw(proto.Ping(pin_protection=True))
assert isinstance(resp, proto.Success)
# Do pin & passphrase-protected action, PassphraseRequest should NOT be raised
resp = client.call_raw(proto.GetAddress())
assert isinstance(resp, proto.Address)
@pytest.mark.setup_client(uninitialized=True)
def test_reset_device_pin(self, client):
@ -180,7 +176,7 @@ class TestMsgResetDevice:
assert resp.passphrase_protection is True
# Do passphrase-protected action, PassphraseRequest should be raised
resp = client.call_raw(proto.Ping(passphrase_protection=True))
resp = client.call_raw(proto.GetAddress())
assert isinstance(resp, proto.PassphraseRequest)
client.call_raw(proto.Cancel())

@ -18,7 +18,7 @@ import time
import pytest
from trezorlib import messages as proto
from trezorlib import btc, messages as proto
from trezorlib.exceptions import PinException
# FIXME TODO Add passphrase tests
@ -26,84 +26,36 @@ from trezorlib.exceptions import PinException
@pytest.mark.skip_t2
class TestProtectCall:
def _some_protected_call(self, client, button, pin, passphrase):
def _some_protected_call(self, client):
# This method perform any call which have protection in the device
res = client.ping(
"random data",
button_protection=button,
pin_protection=pin,
passphrase_protection=passphrase,
)
assert res == "random data"
@pytest.mark.setup_client(pin="1234", passphrase=True)
def test_expected_responses(self, client):
# This is low-level test of set_expected_responses()
# feature of debugging client
with pytest.raises(AssertionError), client:
# Scenario 1 - Received unexpected message
client.set_expected_responses([])
self._some_protected_call(client, True, True, True)
with pytest.raises(AssertionError), client:
# Scenario 2 - Received other than expected message
client.set_expected_responses([proto.Success()])
self._some_protected_call(client, True, True, True)
with pytest.raises(AssertionError), client:
# Scenario 3 - Not received expected message
client.set_expected_responses(
[proto.ButtonRequest(), proto.Success(), proto.Success()]
) # This is expected, but not received
self._some_protected_call(client, True, False, False)
with pytest.raises(AssertionError), client:
# Scenario 4 - Received what expected
client.set_expected_responses(
[
proto.ButtonRequest(),
proto.PinMatrixRequest(),
proto.PassphraseRequest(),
proto.Success(message="random data"),
]
)
self._some_protected_call(client, True, True, True)
with pytest.raises(AssertionError), client:
# Scenario 5 - Failed message by field filter
client.set_expected_responses(
[proto.ButtonRequest(), proto.Success(message="wrong data")]
)
self._some_protected_call(client, True, True, True)
res = btc.get_address(client, "Testnet", [0])
assert res == "mndoQDWatQhfeQbprzZxD43mZ75Z94D6vz"
def test_no_protection(self, client):
with client:
assert client.debug.read_pin()[0] is None
client.set_expected_responses([proto.Success()])
self._some_protected_call(client, False, True, True)
client.set_expected_responses([proto.Address()])
self._some_protected_call(client)
@pytest.mark.setup_client(pin="1234", passphrase=True)
@pytest.mark.setup_client(pin="1234")
def test_pin(self, client):
with client:
assert client.debug.read_pin()[0] == "1234"
client.setup_debuglink(button=True, pin_correct=True)
client.set_expected_responses(
[proto.ButtonRequest(), proto.PinMatrixRequest(), proto.Success()]
)
self._some_protected_call(client, True, True, False)
client.set_expected_responses([proto.PinMatrixRequest(), proto.Address()])
self._some_protected_call(client)
@pytest.mark.setup_client(pin="1234", passphrase=True)
@pytest.mark.setup_client(pin="1234")
def test_incorrect_pin(self, client):
client.setup_debuglink(button=True, pin_correct=False)
with pytest.raises(PinException):
self._some_protected_call(client, False, True, False)
self._some_protected_call(client)
@pytest.mark.setup_client(pin="1234", passphrase=True)
@pytest.mark.setup_client(pin="1234")
def test_cancelled_pin(self, client):
client.setup_debuglink(button=True, pin_correct=False) # PIN cancel
with pytest.raises(PinException):
self._some_protected_call(client, False, True, False)
self._some_protected_call(client)
@pytest.mark.setup_client(pin="1234", passphrase=True)
def test_exponential_backoff_with_reboot(self, client):
@ -126,5 +78,5 @@ class TestProtectCall:
for attempt in range(1, 4):
start = time.time()
with pytest.raises(PinException):
self._some_protected_call(client, False, True, False)
self._some_protected_call(client)
test_backoff(attempt, start)

@ -63,18 +63,10 @@ class TestProtectionLevels:
)
device.change_pin(client)
@pytest.mark.setup_client(pin=True, passphrase=True)
def test_ping(self, client):
with client:
client.set_expected_responses(
[
proto.ButtonRequest(),
proto.PinMatrixRequest(),
proto.PassphraseRequest(),
proto.Success(),
]
)
client.ping("msg", True, True, True)
client.set_expected_responses([proto.ButtonRequest(), proto.Success()])
client.ping("msg", True)
@pytest.mark.setup_client(pin=True, passphrase=True)
def test_get_entropy(self, client):
@ -269,29 +261,21 @@ class TestProtectionLevels:
assert client.features.pin_cached is False
with client:
client.set_expected_responses(
[proto.ButtonRequest(), proto.PinMatrixRequest(), proto.Success()]
)
client.ping("msg", True, True, True)
client.set_expected_responses([proto.PinMatrixRequest(), proto.Address()])
btc.get_address(client, "Testnet", [0])
client.init_device()
assert client.features.pin_cached is True
with client:
client.set_expected_responses([proto.ButtonRequest(), proto.Success()])
client.ping("msg", True, True, True)
client.set_expected_responses([proto.Address()])
btc.get_address(client, "Testnet", [0])
@pytest.mark.setup_client(passphrase=True)
def test_passphrase_cached(self, client):
assert client.features.passphrase_cached is False
with client:
client.set_expected_responses(
[proto.ButtonRequest(), proto.PassphraseRequest(), proto.Success()]
)
client.ping("msg", True, True, True)
client.set_expected_responses([proto.PassphraseRequest(), proto.Address()])
btc.get_address(client, "Testnet", [0])
features = client.call(proto.GetFeatures())
assert features.passphrase_cached is True
with client:
client.set_expected_responses([proto.ButtonRequest(), proto.Success()])
client.ping("msg", True, True, True)
client.set_expected_responses([proto.Address()])
btc.get_address(client, "Testnet", [0])

@ -0,0 +1,280 @@
# 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 messages
from trezorlib.messages import FailureType
from trezorlib.tools import parse_path
XPUB_PASSPHRASE_A = "xpub6CekxGcnqnJ6osfY4Rrq7W5ogFtR54KUvz4H16XzaQuukMFZCGebEpVznfq4yFcKEmYyShwj2UKjL7CazuNSuhdkofF4mHabHkLxCMVvsqG"
XPUB_PASSPHRASE_NONE = "xpub6BiVtCpG9fQPxnPmHXG8PhtzQdWC2Su4qWu6XW9tpWFYhxydCLJGrWBJZ5H6qTAHdPQ7pQhtpjiYZVZARo14qHiay2fvrX996oEP42u8wZy"
XPUB_CARDANO_PASSPHRASE_B = "d80e770f6dfc3edb58eaab68aa091b2c27b08a47583471e93437ac5f8baa61880c7af4938a941c084c19731e6e57a5710e6ad1196263291aea297ce0eec0f177"
ADDRESS_N = parse_path("44h/0h/0h")
XPUB_REQUEST = messages.GetPublicKey(address_n=ADDRESS_N, coin_name="Bitcoin")
def _init_session(client, session_id=None):
"""Call Initialize, check and return the session ID."""
response = client.call(messages.Initialize(session_id=session_id))
assert isinstance(response, messages.Features)
assert len(response.session_id) == 32
return response.session_id
def _get_xpub(client, passphrase=None):
"""Get XPUB and check that the appropriate passphrase flow has happened."""
response = client.call_raw(XPUB_REQUEST)
if passphrase is not None:
assert isinstance(response, messages.PassphraseRequest)
response = client.call_raw(messages.PassphraseAck(passphrase=passphrase))
assert isinstance(response, messages.PublicKey)
return response.xpub
@pytest.mark.skip_ui
@pytest.mark.setup_client(passphrase=True)
def test_session_with_passphrase(client):
# Let's start the communication by calling Initialize.
session_id = _init_session(client)
# GetPublicKey requires passphrase and since it is not cached,
# Trezor will prompt for it.
assert _get_xpub(client, passphrase="A") == XPUB_PASSPHRASE_A
# Call Initialize again, this time with the received session id and then call
# GetPublicKey. The passphrase should be cached now so Trezor must
# not ask for it again, whilst returning the same xpub.
new_session_id = _init_session(client, session_id=session_id)
assert new_session_id == session_id
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASE_A
# If we set session id in Initialize to None, the cache will be cleared
# and Trezor will ask for the passphrase again.
new_session_id = _init_session(client)
assert new_session_id != session_id
assert _get_xpub(client, passphrase="A") == XPUB_PASSPHRASE_A
# Unknown session id is the same as setting it to None.
_init_session(client, session_id=b"X" * 32)
assert _get_xpub(client, passphrase="A") == XPUB_PASSPHRASE_A
@pytest.mark.skip_ui
def test_session_enable_passphrase(client):
# Let's start the communication by calling Initialize.
session_id = _init_session(client)
# Trezor will not prompt for passphrase because it is turned off.
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASE_NONE
# Turn on passphrase.
# Emit the call explicitly to avoid ClearSession done by the library function
response = client.call(messages.ApplySettings(use_passphrase=True))
assert isinstance(response, messages.Success)
# The session id is unchanged, therefore we do not prompt for the passphrase.
new_session_id = _init_session(client, session_id=session_id)
assert session_id == new_session_id
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASE_NONE
# We clear the session id now, so the passphrase should be asked.
new_session_id = _init_session(client)
assert session_id != new_session_id
assert _get_xpub(client, passphrase="A") == XPUB_PASSPHRASE_A
@pytest.mark.skip_ui
@pytest.mark.setup_client(passphrase=True)
def test_clear_session_passphrase(client):
# at first attempt, we are prompted for passphrase
assert _get_xpub(client, passphrase="A") == XPUB_PASSPHRASE_A
# now the passphrase is cached
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASE_A
# Erase the cached passphrase
response = client.call(messages.Initialize())
assert isinstance(response, messages.Features)
# we have to enter passphrase again
assert _get_xpub(client, passphrase="A") == XPUB_PASSPHRASE_A
@pytest.mark.skip_ui
@pytest.mark.skip_t1
@pytest.mark.setup_client(passphrase=True)
def test_passphrase_on_device(client):
_init_session(client)
# try to get xpub with passphrase on host:
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.PassphraseRequest)
response = client.call_raw(messages.PassphraseAck(passphrase="A", on_device=False))
assert isinstance(response, messages.PublicKey)
assert response.xpub == XPUB_PASSPHRASE_A
# try to get xpub again, passphrase should be cached
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.PublicKey)
assert response.xpub == XPUB_PASSPHRASE_A
# make a new session
_init_session(client)
# try to get xpub with passphrase on device:
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.PassphraseRequest)
response = client.call_raw(messages.PassphraseAck(on_device=True))
assert isinstance(response, messages.ButtonRequest)
client.debug.input("A")
response = client.call_raw(messages.ButtonAck())
assert isinstance(response, messages.PublicKey)
assert response.xpub == XPUB_PASSPHRASE_A
# try to get xpub again, passphrase should be cached
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.PublicKey)
assert response.xpub == XPUB_PASSPHRASE_A
@pytest.mark.skip_ui
@pytest.mark.skip_t1
@pytest.mark.setup_client(passphrase=True)
def test_passphrase_always_on_device(client):
# Let's start the communication by calling Initialize.
session_id = _init_session(client)
# Force passphrase entry on Trezor.
response = client.call(messages.ApplySettings(passphrase_always_on_device=True))
assert isinstance(response, messages.Success)
# Since we enabled the always_on_device setting, Trezor will send ButtonRequests and ask for it on the device.
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.ButtonRequest)
client.debug.input("") # Input empty passphrase.
response = client.call_raw(messages.ButtonAck())
assert isinstance(response, messages.PublicKey)
assert response.xpub == XPUB_PASSPHRASE_NONE
# Passphrase will not be prompted. The session id stays the same and the passphrase is cached.
_init_session(client, session_id=session_id)
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.PublicKey)
assert response.xpub == XPUB_PASSPHRASE_NONE
# In case we want to add a new passphrase we need to send session_id = None.
_init_session(client)
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.ButtonRequest)
client.debug.input("A") # Input empty passphrase.
response = client.call_raw(messages.ButtonAck())
assert isinstance(response, messages.PublicKey)
assert response.xpub == XPUB_PASSPHRASE_A
@pytest.mark.skip_ui
@pytest.mark.skip_t2
@pytest.mark.setup_client(passphrase=True)
def test_passphrase_on_device_not_possible_on_t1(client):
# This setting makes no sense on T1.
response = client.call_raw(messages.ApplySettings(passphrase_always_on_device=True))
assert isinstance(response, messages.Failure)
assert response.code == FailureType.DataError
# T1 should not accept on_device request
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.PassphraseRequest)
response = client.call_raw(messages.PassphraseAck(on_device=True))
assert isinstance(response, messages.Failure)
assert response.code == FailureType.DataError
@pytest.mark.skip_ui
@pytest.mark.setup_client(passphrase=True)
def test_passphrase_ack_mismatch(client):
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.PassphraseRequest)
response = client.call_raw(messages.PassphraseAck(passphrase="A", on_device=True))
assert isinstance(response, messages.Failure)
assert response.code == FailureType.DataError
@pytest.mark.skip_ui
@pytest.mark.setup_client(passphrase=True)
def test_passphrase_missing(client):
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.PassphraseRequest)
response = client.call_raw(messages.PassphraseAck(passphrase=None))
assert isinstance(response, messages.Failure)
assert response.code == FailureType.DataError
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.PassphraseRequest)
response = client.call_raw(messages.PassphraseAck(passphrase=None, on_device=False))
assert isinstance(response, messages.Failure)
assert response.code == FailureType.DataError
def _get_xpub_cardano(client, passphrase):
msg = messages.CardanoGetPublicKey(address_n=parse_path("44'/1815'/0'/0/0"))
response = client.call_raw(msg)
if passphrase is not None:
assert isinstance(response, messages.PassphraseRequest)
response = client.call_raw(messages.PassphraseAck(passphrase=passphrase))
assert isinstance(response, messages.CardanoPublicKey)
return response.xpub
@pytest.mark.skip_ui
@pytest.mark.skip_t1
@pytest.mark.altcoin
@pytest.mark.setup_client(passphrase=True)
def test_cardano_passphrase(client):
# Cardano uses a variation of BIP-39 so we need to ask for the passphrase again.
session_id = _init_session(client)
# GetPublicKey requires passphrase and since it is not cached,
# Trezor will prompt for it.
assert _get_xpub(client, passphrase="A") == XPUB_PASSPHRASE_A
# The passphrase is now cached for non-Cardano coins.
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASE_A
# Cardano will prompt for it again.
assert _get_xpub_cardano(client, passphrase="B") == XPUB_CARDANO_PASSPHRASE_B
# But now also Cardano has it cached.
assert _get_xpub_cardano(client, passphrase=None) == XPUB_CARDANO_PASSPHRASE_B
# And others behaviour did not change.
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASE_A
# Initialize with the session id does not destroy the state
_init_session(client, session_id=session_id)
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASE_A
assert _get_xpub_cardano(client, passphrase=None) == XPUB_CARDANO_PASSPHRASE_B
# New session will destroy the state
_init_session(client)
# GetPublicKey must ask for passphrase again
assert _get_xpub(client, passphrase="A") == XPUB_PASSPHRASE_A
# Cardano must also ask for passphrase again
assert _get_xpub_cardano(client, passphrase="B") == XPUB_CARDANO_PASSPHRASE_B

@ -9,9 +9,9 @@
"test_cancel.py::test_cancel_message_via_initialize[message1]": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_applysettings.py-test_apply_settings": "b698654871541258f97d58ada0f010b2d77b74829791566746cad619d3740a94",
"test_msg_applysettings.py-test_apply_settings_passphrase": "fb38537b921f8064f7ea6e1a584e70a8be74968a3be6726b7d36cf57de0d7865",
"test_msg_backup_device.py::test_backup_bip39": "68d22900d8f9130bf325342b6b32b1785f28cefd256ea1cee7fd32109c817c23",
"test_msg_backup_device.py::test_backup_slip39_advanced": "aec6a663f8f75ff73d2f2377d12347b062b831ce0d5fcfe9d7f3c975a05aef32",
"test_msg_backup_device.py::test_backup_slip39_basic": "a4c44785873509bd4ebffa6ec82f2fe732349eb0bd23631dbc3876e956b567c7",
"test_msg_backup_device.py::test_backup_bip39": "dfdbd0ae6774177d43f2f11d026c4d8679dd994b508c1d850d9cfee5dd1118ac",
"test_msg_backup_device.py::test_backup_slip39_advanced": "244b31044a25e44847a6efb79ea4cb67c246a971bf828d46aa79d14151934a04",
"test_msg_backup_device.py::test_backup_slip39_basic": "81a56c307342e46ad261d6a6c6da0a75224af8e72f40da7c630c1e03c0643e1c",
"test_msg_backup_device.py::test_interrupt_backup_fails": "225b3da1acac6e9a65106fcc4a01de8a44de035aedb4dcc21c09f439199fdf40",
"test_msg_backup_device.py::test_no_backup_fails": "93039a9472cfc9058563bd56e4a3dbe2e41af64744a61f6ee3255a04bd3a9366",
"test_msg_backup_device.py::test_no_backup_show_entropy_fails": "14fcdd2ded299ca099a35966cc9f21204b31de8d6bab9ec91cb64537bd70440c",
@ -24,16 +24,16 @@
"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-0-Ae2tdPwUPEZLCq3sFv4wVYx": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-1-Ae2tdPwUPEZEY6pVJoyuNNd": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-2-Ae2tdPwUPEZ3gZD1QeUHvAq": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address_slip39_basic.py::test_cardano_get_address[m-44'-1815'-0'-0-0-Ae2tdPwUPE": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0",
"test_msg_cardano_get_address_slip39_basic.py::test_cardano_get_address[m-44'-1815'-0'-0-1-Ae2tdPwUPE": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0",
"test_msg_cardano_get_address_slip39_basic.py::test_cardano_get_address[m-44'-1815'-0'-0-2-Ae2tdPwUPE": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0",
"test_msg_cardano_get_address_slip39_basic.py::test_cardano_get_address[m-44'-1815'-0'-0-0-Ae2tdPwUPE": "d2d6aac0a4605f1a961580f0fc156e7b69b993f908906281313429fa3222f349",
"test_msg_cardano_get_address_slip39_basic.py::test_cardano_get_address[m-44'-1815'-0'-0-1-Ae2tdPwUPE": "d2d6aac0a4605f1a961580f0fc156e7b69b993f908906281313429fa3222f349",
"test_msg_cardano_get_address_slip39_basic.py::test_cardano_get_address[m-44'-1815'-0'-0-2-Ae2tdPwUPE": "d2d6aac0a4605f1a961580f0fc156e7b69b993f908906281313429fa3222f349",
"test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-44'-1815'-0'-c0fce1839f1a84c4e7702": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-44'-1815'-1'-ea5dde31b9f551e08a5b6": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-44'-1815'-2'-076338cee5ab3dae19f06": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-44'-1815'-3'-5f769380dc6fd17a4e0f2": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_public_key_slip39_basic.py::test_cardano_get_public_key[m-44'-1815'-0'-0-0-bc04": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0",
"test_msg_cardano_get_public_key_slip39_basic.py::test_cardano_get_public_key[m-44'-1815'-0'-0-1-24c4": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0",
"test_msg_cardano_get_public_key_slip39_basic.py::test_cardano_get_public_key[m-44'-1815'-0'-0-2-831a": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0",
"test_msg_cardano_get_public_key_slip39_basic.py::test_cardano_get_public_key[m-44'-1815'-0'-0-0-bc04": "d2d6aac0a4605f1a961580f0fc156e7b69b993f908906281313429fa3222f349",
"test_msg_cardano_get_public_key_slip39_basic.py::test_cardano_get_public_key[m-44'-1815'-0'-0-1-24c4": "d2d6aac0a4605f1a961580f0fc156e7b69b993f908906281313429fa3222f349",
"test_msg_cardano_get_public_key_slip39_basic.py::test_cardano_get_public_key[m-44'-1815'-0'-0-2-831a": "d2d6aac0a4605f1a961580f0fc156e7b69b993f908906281313429fa3222f349",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[1097911063-inputs2-outputs2-transactions2": "1a8eade03d9c0ecbbb01567e5e9d46187a2ffe7fa42d59eb711347a7fe3b5bb7",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-inputs0-outputs0-transactions0-": "07eceef966cb8069381dc5105b732bce6de8d207a1d27e56e8abddd57c307b22",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-inputs1-outputs1-transactions1-": "4c74546cacb2b99b9aeb6e134d99c0d1d6f0ba1818d1182364cfdb94b938ccc5",
@ -43,16 +43,16 @@
"test_msg_cardano_sign_tx_slip39_basic.py::test_cardano_sign_tx[1097911063-inputs2-outputs2-transacti": "e6813a7fd973f49b02ef28cee15deb48d0389d1cb2696194848e4d690281361f",
"test_msg_cardano_sign_tx_slip39_basic.py::test_cardano_sign_tx[764824073-inputs0-outputs0-transactio": "bc88a23280c0234860ccbb5e96d5cc3a851e2f2f9928c400f6c0907c68172d39",
"test_msg_cardano_sign_tx_slip39_basic.py::test_cardano_sign_tx[764824073-inputs1-outputs1-transactio": "6956bb359388186b4c127ae88f4d86527381caf8098f55b4fa05c343640c081f",
"test_msg_change_wipe_code_t2.py::test_set_pin_to_wipe_code": "b294554434cd454b6c53845256d3637bb83f0f7f96e39608c91018bb345f359e",
"test_msg_change_wipe_code_t2.py::test_set_remove_wipe_code": "74e8309c74ca9e599de91296cfcef63a744713acad3f3ea10b7b461eba69c30e",
"test_msg_change_wipe_code_t2.py::test_set_wipe_code_mismatch": "fdbd58fb31323cfc32a9a7684d4dd45a5bd2b098156f2abf9cca5b5a481ecd10",
"test_msg_change_wipe_code_t2.py::test_set_wipe_code_to_pin": "7b3599a5582b566c65c337b50fb6e38fc577b67310fc731ed756b7d143f4727d",
"test_msg_change_wipe_code_t2.py::test_wipe_code_activate": "b25571c82be5f00b1f0c0bca95154dbae6a6ac092b249b79f805a126e1580037",
"test_msg_changepin_t2.py::test_change_failed": "d39ba207aab689c53adde211dc8e189809ffe4f3a3c547bd8c0422ad20663dc2",
"test_msg_changepin_t2.py::test_change_pin": "9e43d9f05c96f61e5ba7723e7d3ceb26ed813fd5be95d2cfa7e01573ab87b531",
"test_msg_changepin_t2.py::test_remove_pin": "249a84500e2cac217f4b3c7cfbb4d4212781019191b21d1386f81317f6d699e7",
"test_msg_changepin_t2.py::test_set_failed": "5aa994de8e571d08512705b04e0c55b60411d7d23491f75688f16d255369363e",
"test_msg_changepin_t2.py::test_set_pin": "9e30570a9063a9820d423dd000d5ba70f30bf32de78cce9160643f60d5fe4eb2",
"test_msg_change_wipe_code_t2.py::test_set_pin_to_wipe_code": "9519b574e3f749810977ac55f851553f2051b51ef7799ffaa155410d89e1cd63",
"test_msg_change_wipe_code_t2.py::test_set_remove_wipe_code": "82c0d1acbf5ff344189761f808d3cf0e632726341231c20b2c0925ab5549b6af",
"test_msg_change_wipe_code_t2.py::test_set_wipe_code_mismatch": "1642d2d15920a3bb2c666b39beca9943ba39adb59289ebc40b97d7088a4d7abf",
"test_msg_change_wipe_code_t2.py::test_set_wipe_code_to_pin": "15289574ceb002b5161305b0595dcd20e437d1dd4e7561332e1aba4c1615e9ea",
"test_msg_change_wipe_code_t2.py::test_wipe_code_activate": "348ff6811029253d7d520b37a2d3ff219516a7401c8b65ab088c7a7d39bd8b2b",
"test_msg_changepin_t2.py::test_change_failed": "370c59da62a84aaefa242562c36a6facac89c7f819e37d1ae8cbe2c44a2de256",
"test_msg_changepin_t2.py::test_change_pin": "c42fca9bf8f3b4c330516d90231ae0cfa7419d83370be9cfcf6a81cca3f3b06c",
"test_msg_changepin_t2.py::test_remove_pin": "d049eaa6cd11e88b7af193b080cf868b62271266ad6f2973bfd82944b523741d",
"test_msg_changepin_t2.py::test_set_failed": "59beeec1a00817f664a5fd93234012588613aac93c45d53c27550fe5d0ef8380",
"test_msg_changepin_t2.py::test_set_pin": "3aafe16a451f928c9bfff2a3ff7e3c23ce4948c9a044ebd04834df045670183f",
"test_msg_cipherkeyvalue.py-test_decrypt": "166d85b1bf11aeaeb5b93ef5d047b6f8910c28b8fce1d853e6912d89d7bfca2f",
"test_msg_cipherkeyvalue.py-test_decrypt_badlen": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cipherkeyvalue.py-test_encrypt": "3a37e4004c87bc6df6a8fa7c93b6fe3e3524986914709fda2f9c99ba0ff69775",
@ -166,10 +166,10 @@
"test_msg_lisk_verifymessage.py-test_verify": "5e9cf05f6ccf10f697cae9f780042db934892e1d7c68fb2f19a40319a687ea50",
"test_msg_lisk_verifymessage.py-test_verify_long": "26adab7e31f388e5b034a865f9c010d57e67fd855d44839d2f2600d8317bd98e",
"test_msg_loaddevice.py-test_load_device_1": "114d7e07b00f8a9fc60e0888ce3e39f79805c577b99f4d25967fcf7cf6367664",
"test_msg_loaddevice.py-test_load_device_2": "35797984a814cf7e48ee827993269b43524bf91462c9c694ad7dd7d6bb7e6bbb",
"test_msg_loaddevice.py-test_load_device_2": "9947760ad56ea110b6f3937883c37701c866dd57b6c342806bd8e8b3aa889887",
"test_msg_loaddevice.py-test_load_device_slip39_advanced": "1c6db0d592b1d22b3c9fce3ddab8a9fd138f11d83e5d4e64431a02bf4ffed605",
"test_msg_loaddevice.py-test_load_device_slip39_basic": "1c6db0d592b1d22b3c9fce3ddab8a9fd138f11d83e5d4e64431a02bf4ffed605",
"test_msg_loaddevice.py-test_load_device_utf": "51fe82ea3081d06e69af1283bf9e32ea91ddb75ac87173a3d1f861c2b27a0635",
"test_msg_loaddevice.py-test_load_device_utf": "65d570c16a561831e017c447736749d855f0bf6e0c3cfe7a9b186493c01f187b",
"test_msg_monero_getaddress.py-test_monero_getaddress": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_monero_getwatchkey.py-test_monero_getwatchkey": "d77fa4d4322e145c41f1ce07526ff59f8b58d8854aeffaa5266e14cd572350e7",
"test_msg_nem_getaddress.py-test_nem_getaddress": "e726f99401a20eb74c33d755cecea2a3f69b7ae5b541302677ee05f80f5aef19",
@ -200,7 +200,7 @@
"test_msg_recoverydevice_bip39_dryrun.py::test_uninitialized": "14fcdd2ded299ca099a35966cc9f21204b31de8d6bab9ec91cb64537bd70440c",
"test_msg_recoverydevice_bip39_t2.py-test_already_initialized": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_recoverydevice_bip39_t2.py-test_nopin_nopassphrase": "9769cde3e3951a76364973ade753682e2acc67d1633a9f982f0604b5702aa895",
"test_msg_recoverydevice_bip39_t2.py-test_pin_passphrase": "761b17d28d589eb4d4d6d73beb6061087828c469f60694893c94b301a37d34ad",
"test_msg_recoverydevice_bip39_t2.py-test_pin_passphrase": "d746fda5234be75cded559817d0fdfb4397ac757e9847d4dfbef44ac031381d1",
"test_msg_recoverydevice_slip39_advanced.py::test_abort": "793dde7fb47e9c4ad36369be396da20332560f29083d7f9a2b0582173371c9ed",
"test_msg_recoverydevice_slip39_advanced.py::test_extra_share_entered": "00a94e20b786346c45f987860b2465f299075d7c6de4971f42a4749e1cc8bfc0",
"test_msg_recoverydevice_slip39_advanced.py::test_group_threshold_reached": "3b075a276c4e0d53fbc51ce1f29594bbd474d25f47c0f6a32caac41ba0ba2138",
@ -214,7 +214,7 @@
"test_msg_recoverydevice_slip39_basic.py::test_abort": "793dde7fb47e9c4ad36369be396da20332560f29083d7f9a2b0582173371c9ed",
"test_msg_recoverydevice_slip39_basic.py::test_ask_word_number": "8e9d9fd75e17f6b44829ae2d7b0eb9e60b48577f975abc6d75116f8365241082",
"test_msg_recoverydevice_slip39_basic.py::test_noabort": "d374a9b85c03a0cc1bbb59130e454406513fc35f4f43b968db4920414de1bb72",
"test_msg_recoverydevice_slip39_basic.py::test_recover_with_pin_passphrase": "3b49ab7cd70cf4e2576c842948029046bc6a686b3763983985e3d3965e8652d8",
"test_msg_recoverydevice_slip39_basic.py::test_recover_with_pin_passphrase": "ee0edd912b913d31b8308b64ceed7fa3e58f5b63c8a4d4d85f3651d4b121a202",
"test_msg_recoverydevice_slip39_basic.py::test_same_share": "e6a54429fdbedea9efca9cbed736aada07f95f3b20f895f9c1c5ec056a2be014",
"test_msg_recoverydevice_slip39_basic.py::test_secret[shares0-491b795b80fc21ccdf466c0fbc98c8fc]": "54581a91b55ab531b215cba61052fd77c505232c510f170080760605eb9b8c46",
"test_msg_recoverydevice_slip39_basic.py::test_secret[shares1-b770e0da1363247652de97a39bdbf2463be0878": "f73bef254762d761db27df46bff62641a9d2ac0602c34fc4e465262bf26ed08f",
@ -224,11 +224,11 @@
"test_msg_recoverydevice_slip39_basic_dryrun.py::test_2of3_dryrun": "d84427489f691ecc222b62f83af3e97fa09097404dcba07772a43b5eb0c689e8",
"test_msg_recoverydevice_slip39_basic_dryrun.py::test_2of3_invalid_seed_dryrun": "55f2dd6b4958659f071c3f57e06286f872ac38af4828f446a0f4e91c657dfccc",
"test_msg_resetdevice_bip39_t2.py-test_already_initialized": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_resetdevice_bip39_t2.py-test_failed_pin": "a6ba803a7572dc8e29c821433a067c3a990185a8eb7b95d0106cd8f4b82e9da2",
"test_msg_resetdevice_bip39_t2.py-test_reset_device": "cbe1e439c2e9810ff6ee1031268810188b3cd07e029a7aa1d4b227e33707c727",
"test_msg_resetdevice_bip39_t2.py-test_reset_device_pin": "af378bff7e8fdf16324715f640e8b17bebd69f779758ab33cd7025c557a1d5c0",
"test_msg_resetdevice_slip39_advanced.py-test_reset_device_slip39_advanced": "7daabeb21a17a8c8e8adb800a8b6b0504e5d2e7cf0bbfcb14770fb33df1426a2",
"test_msg_resetdevice_slip39_basic.py-test_reset_device_slip39_basic": "1b56e3335876c01ec684839158bc0aa3a31aac03d2beef8d3ee63a0a441e3f1f",
"test_msg_resetdevice_bip39_t2.py-test_failed_pin": "a52f3479f7d8e14c7f89af9b305a50f3bd244fee28f17ebc0abedba701c32811",
"test_msg_resetdevice_bip39_t2.py-test_reset_device": "13d739a387a80aa96f533bb6f3b0f2ff12c7ba84608d0797ac603208d9aed796",
"test_msg_resetdevice_bip39_t2.py-test_reset_device_pin": "ed64165674816dfb408ead89abb8d1a5743f50f63933aceba6701149522c3866",
"test_msg_resetdevice_slip39_advanced.py-test_reset_device_slip39_advanced": "005622513c517610a4a1af529ef94760b4d0406b971f0b2f2557c93ce0dac8c9",
"test_msg_resetdevice_slip39_basic.py-test_reset_device_slip39_basic": "ebc62d2c49136c6ce75c8e026e51157d96a6f2440881dca0cdd8bc357e2c9354",
"test_msg_ripple_get_address.py-test_ripple_get_address": "2bb7d7bf48f1218530b4d7045d48480cad6411e110df537551b2f80b342007f2",
"test_msg_ripple_get_address.py-test_ripple_get_address_other": "2bb7d7bf48f1218530b4d7045d48480cad6411e110df537551b2f80b342007f2",
"test_msg_ripple_sign_tx.py-test_ripple_sign_invalid_fee": "1c0ca08b857da6121f43cfb1632c7f7e1d189ef1fdb665db7ba2cdfa7a59ea7c",
@ -380,12 +380,12 @@
"test_passphrase_slip39_advanced.py::test_256bit_passphrase": "69b6b8b22c819e1282d7d2c14b31bf8d015c81ac05fe034540dbb11c8a20dbdb",
"test_passphrase_slip39_basic.py::test_2of5_passphrase": "1e00b1a7840bc144b98b7bce26f74fc913a0abf9d1c500571d7803b6b2e0943c",
"test_passphrase_slip39_basic.py::test_3of6_passphrase": "1e00b1a7840bc144b98b7bce26f74fc913a0abf9d1c500571d7803b6b2e0943c",
"test_reset_backup.py::test_skip_backup_manual[0-backup_flow_bip39]": "84ee487acfff37417e4d6862ab1b166be1165ae3eb4e19f828ae3363866edb39",
"test_reset_backup.py::test_skip_backup_manual[1-backup_flow_slip39_basic]": "55224486083a1268c76ffe1d2b47ca7fba29ca6955e3601dae9e2abd44fb9d62",
"test_reset_backup.py::test_skip_backup_manual[2-backup_flow_slip39_advanced]": "c40d7894875393bc093e23d02dab8ad668929767a542a27ff89c6dd5b7b963de",
"test_reset_backup.py::test_skip_backup_msg[0-backup_flow_bip39]": "68eb02ad320eee49d3a6aac891e5dc4c35ee09a1e98a2480c2cb1b672cbb0696",
"test_reset_backup.py::test_skip_backup_msg[1-backup_flow_slip39_basic]": "34674c8b9f4b6a836940fa347bacfd6d31dd681d41bf8e1ec129e9deef590588",
"test_reset_backup.py::test_skip_backup_msg[2-backup_flow_slip39_advanced]": "24c641cde46665219de99978c3e27088fd4ade55f2665535a95e7dce1f2b3148",
"test_reset_backup.py::test_skip_backup_manual[0-backup_flow_bip39]": "a73fcd9af54d3b55cc7c21e68b1637a6e29829bfd57d47b0503e67fa22e4106a",
"test_reset_backup.py::test_skip_backup_manual[1-backup_flow_slip39_basic]": "4b117541a58e7c209786a728ebc1406720cbd7d5a234f20a550b1ad3970b4e76",
"test_reset_backup.py::test_skip_backup_manual[2-backup_flow_slip39_advanced]": "2d1d55f25b21c2be2e7aeaa17eeb3a4b9ee4d7763c4a65ae2fd59da2d9dbbcc0",
"test_reset_backup.py::test_skip_backup_msg[0-backup_flow_bip39]": "f448153452b154c0be153e18340f22c853430267316229548ad8f4f47cf946f7",
"test_reset_backup.py::test_skip_backup_msg[1-backup_flow_slip39_basic]": "9fbcd0ab293de21ab3a24e0c29315d152c1bc5da4902ec054cc8800c0d855aa6",
"test_reset_backup.py::test_skip_backup_msg[2-backup_flow_slip39_advanced]": "0bafb944169d0ab608513deadba15411d1756ef2658253740c5d8792f4527e9d",
"test_u2f_counter.py::test_u2f_counter": "7d96a4d262b9d8a2c1158ac1e5f0f7b2c3ed5f2ba9d6235a014320313f9488fe",
"test_zerosig.py-test_one_zero_signature": "401aeaf7b2f565e2064a3c1a57a8ee3afe1e9bf251fba0874390685e7e0f178f",
"test_zerosig.py-test_two_zero_signature": "7a01a057fb5dd3e6e38e7986875c5d07f0700bd80b519660e0b42973a9afd664"

Loading…
Cancel
Save