1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-22 15:38:11 +00:00

Merge pull request #803 from trezor/passphrase

Passphrase Redesign
This commit is contained in:
Tomas Susanka 2020-02-11 16:01:59 +01:00 committed by GitHub
commit 2c0504ad1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
91 changed files with 1142 additions and 944 deletions

View File

@ -62,11 +62,12 @@ message ButtonRequest {
ButtonRequest_PublicKey = 11; ButtonRequest_PublicKey = 11;
ButtonRequest_MnemonicWordCount = 12; ButtonRequest_MnemonicWordCount = 12;
ButtonRequest_MnemonicInput = 13; ButtonRequest_MnemonicInput = 13;
ButtonRequest_PassphraseType = 14; // ButtonRequest_PassphraseType = 14; DEPRECATED
ButtonRequest_UnknownDerivationPath = 15; ButtonRequest_UnknownDerivationPath = 15;
ButtonRequest_RecoveryHomepage = 16; ButtonRequest_RecoveryHomepage = 16;
ButtonRequest_Success = 17; ButtonRequest_Success = 17;
ButtonRequest_Warning = 18; ButtonRequest_Warning = 18;
ButtonRequest_PassphraseEntry = 19;
} }
} }
@ -110,31 +111,36 @@ message PinMatrixAck {
* @next PassphraseAck * @next PassphraseAck
*/ */
message PassphraseRequest { 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 * Request: Send passphrase back
* @next PassphraseStateRequest * @auxend
*/ */
message PassphraseAck { message PassphraseAck {
optional string passphrase = 1; 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 * 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 optional bytes state = 1; // actual device state
} }
/** /**
* Request: Send passphrase state back * Request: Send passphrase state back
* Deprecated in 2.3.0
* @auxend * @auxend
*/ */
message PassphraseStateAck { message Deprecated_PassphraseStateAck {
option deprecated = true;
} }
/** /**

View File

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

View File

@ -54,8 +54,6 @@ enum MessageType {
MessageType_EntropyAck = 36 [(wire_in) = true]; MessageType_EntropyAck = 36 [(wire_in) = true];
MessageType_PassphraseRequest = 41 [(wire_out) = true]; MessageType_PassphraseRequest = 41 [(wire_out) = true];
MessageType_PassphraseAck = 42 [(wire_in) = true, (wire_tiny) = true, (wire_no_fsm) = 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_RecoveryDevice = 45 [(wire_in) = true];
MessageType_WordRequest = 46 [(wire_out) = true]; MessageType_WordRequest = 46 [(wire_out) = true];
MessageType_WordAck = 47 [(wire_in) = true]; MessageType_WordAck = 47 [(wire_in) = true];
@ -66,6 +64,11 @@ enum MessageType {
MessageType_NextU2FCounter = 81 [(wire_out) = true]; MessageType_NextU2FCounter = 81 [(wire_out) = true];
MessageType_ChangeWipeCode = 82 [(wire_in) = 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 // Bootloader
MessageType_FirmwareErase = 6 [(wire_in) = true, (wire_bootloader) = true]; MessageType_FirmwareErase = 6 [(wire_in) = true, (wire_bootloader) = true];
MessageType_FirmwareUpload = 7 [(wire_in) = true, (wire_bootloader) = true]; MessageType_FirmwareUpload = 7 [(wire_in) = true, (wire_bootloader) = true];

View File

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

View File

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

View File

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

View File

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

View File

@ -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)

View File

@ -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

View File

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

View File

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

View File

@ -34,8 +34,7 @@ def get_features() -> Features:
f.initialized = storage.is_initialized() f.initialized = storage.is_initialized()
f.pin_protection = config.has_pin() f.pin_protection = config.has_pin()
f.pin_cached = config.has_pin() f.pin_cached = config.has_pin()
f.passphrase_protection = storage.device.has_passphrase() f.passphrase_protection = storage.device.is_passphrase_enabled()
f.passphrase_cached = cache.has_passphrase()
f.needs_backup = storage.device.needs_backup() f.needs_backup = storage.device.needs_backup()
f.unfinished_backup = storage.device.unfinished_backup() f.unfinished_backup = storage.device.unfinished_backup()
f.no_backup = storage.device.no_backup() f.no_backup = storage.device.no_backup()
@ -48,6 +47,7 @@ def get_features() -> Features:
Capability.Crypto, Capability.Crypto,
Capability.Shamir, Capability.Shamir,
Capability.ShamirGroups, Capability.ShamirGroups,
Capability.PassphraseEntry,
] ]
else: else:
f.capabilities = [ f.capabilities = [
@ -67,18 +67,19 @@ def get_features() -> Features:
Capability.U2F, Capability.U2F,
Capability.Shamir, Capability.Shamir,
Capability.ShamirGroups, Capability.ShamirGroups,
Capability.PassphraseEntry,
] ]
f.sd_card_present = io.SDCard().present() f.sd_card_present = io.SDCard().present()
f.sd_protection = storage.sd_salt.is_enabled() f.sd_protection = storage.sd_salt.is_enabled()
f.wipe_code_protection = config.has_wipe_code() 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 return f
async def handle_Initialize(ctx: wire.Context, msg: Initialize) -> Features: 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() cache.clear()
if msg.skip_passphrase:
cache.set_passphrase("")
return get_features() 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: 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: 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 from trezor.ui.text import Text
await require_confirm(ctx, Text("Confirm"), ProtectCall) 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) return Success(message=msg.message)

View File

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

View File

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

View File

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

View File

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

View File

@ -1,75 +1,36 @@
from storage.device import get_device_id from trezor.crypto import random
from trezor.crypto import hashlib, hmac, random
if False: if False:
from typing import Optional from typing import Optional
_cached_seed = None # type: Optional[bytes] APP_COMMON_SEED = 0
_cached_seed_without_passphrase = None # type: Optional[bytes] APP_COMMON_SEED_WITHOUT_PASSPHRASE = 1
_cached_passphrase = None # type: Optional[str] APP_CARDANO_ROOT = 2
_cached_passphrase_fprint = b"\x00\x00\x00\x00" # type: bytes APP_MONERO_LIVE_REFRESH = 3
_cache_session_id = None # type: Optional[bytes]
_cache = {}
if False:
from typing import Any
def get_state(prev_state: bytes = None, passphrase: str = None) -> Optional[bytes]: def get_session_id() -> bytes:
if prev_state is None: global _cache_session_id
salt = random.bytes(32) # generate a random salt if no state provided if not _cache_session_id:
else: _cache_session_id = random.bytes(32)
salt = prev_state[:32] # use salt from provided state return _cache_session_id
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: def set(key: int, value: Any) -> None:
# state = HMAC(passphrase, salt || device_id) _cache[key] = value
message = salt + get_device_id().encode()
state = hmac.new(passphrase.encode(), message, hashlib.sha256).digest()
return salt + state
def get_seed() -> Optional[bytes]: def get(key: int) -> Any:
return _cached_seed return _cache.get(key)
def get_seed_without_passphrase() -> Optional[bytes]: def clear() -> None:
return _cached_seed_without_passphrase global _cache_session_id
_cache_session_id = None
_cache.clear()
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
def set_seed(seed: Optional[bytes]) -> None:
global _cached_seed
_cached_seed = seed
def set_seed_without_passphrase(seed: Optional[bytes]) -> None:
global _cached_seed_without_passphrase
_cached_seed_without_passphrase = seed
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 clear(keep_passphrase: bool = False) -> None:
set_seed(None)
set_seed_without_passphrase(None)
if not keep_passphrase:
set_passphrase(None)

View File

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

View File

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

View File

@ -6,7 +6,7 @@ if __debug__:
try: try:
from typing import Dict, List # noqa: F401 from typing import Dict, List # noqa: F401
from typing_extensions import Literal # 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: except ImportError:
pass pass
@ -23,5 +23,5 @@ class ButtonRequest(p.MessageType):
@classmethod @classmethod
def get_fields(cls) -> Dict: def get_fields(cls) -> Dict:
return { 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),
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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]

View File

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

View File

@ -160,6 +160,14 @@ def header_error(message: str, clear: bool = True) -> None:
display.bar(0, 30, WIDTH, HEIGHT - 30, style.BG) 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( def grid(
i: int, # i-th cell of the table of which we wish to return Area (snake-like starting with 0) 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 n_x: int = 3, # number of rows in the table

View File

@ -1,7 +1,6 @@
from micropython import const from micropython import const
from trezor import io, loop, res, ui from trezor import io, loop, res, ui
from trezor.messages import PassphraseSourceType
from trezor.ui import display from trezor.ui import display
from trezor.ui.button import Button, ButtonClear, ButtonConfirm from trezor.ui.button import Button, ButtonClear, ButtonConfirm
from trezor.ui.swipe import SWIPE_HORIZONTAL, SWIPE_LEFT, Swipe 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, ...]: def create_tasks(self) -> Tuple[loop.Task, ...]:
return self.handle_input(), self.handle_rendering(), self.handle_paging() 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)

View File

@ -1,9 +1,23 @@
import sys import sys
sys.path.append('../src') sys.path.append("../src")
from ubinascii import hexlify, unhexlify # noqa: F401 from ubinascii import hexlify, unhexlify # noqa: F401
import unittest # noqa: F401 import unittest # noqa: F401
from trezor import utils # 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

View File

@ -44,28 +44,14 @@ class ByteArrayWriter:
return len(buf) 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: def load_uvarint(data: bytes) -> int:
reader = ByteReader(data) reader = ByteReader(data)
return run_until_complete(protobuf.load_uvarint(reader)) return await_result(protobuf.load_uvarint(reader))
def dump_uvarint(value: int) -> bytearray: def dump_uvarint(value: int) -> bytearray:
writer = ByteArrayWriter() writer = ByteArrayWriter()
run_until_complete(protobuf.dump_uvarint(writer, value)) await_result(protobuf.dump_uvarint(writer, value))
return writer.buf return writer.buf
@ -106,9 +92,9 @@ class TestProtobuf(unittest.TestCase):
# ok message: # ok message:
msg = Message(-42, 5) msg = Message(-42, 5)
writer = ByteArrayWriter() writer = ByteArrayWriter()
run_until_complete(protobuf.dump_message(writer, msg)) await_result(protobuf.dump_message(writer, msg))
reader = ByteReader(bytes(writer.buf)) 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.sint_field, nmsg.sint_field)
self.assertEqual(msg.enum_field, nmsg.enum_field) self.assertEqual(msg.enum_field, nmsg.enum_field)
@ -116,10 +102,10 @@ class TestProtobuf(unittest.TestCase):
# bad enum value: # bad enum value:
msg = Message(-42, 42) msg = Message(-42, 42)
writer = ByteArrayWriter() writer = ByteArrayWriter()
run_until_complete(protobuf.dump_message(writer, msg)) await_result(protobuf.dump_message(writer, msg))
reader = ByteReader(bytes(writer.buf)) reader = ByteReader(bytes(writer.buf))
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
run_until_complete(protobuf.load_message(reader, Message)) await_result(protobuf.load_message(reader, Message))
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -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()

View File

@ -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`.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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 * Disallow changing of settings via dry-run recovery
* Wipe code * Wipe code
* Make LoadDevice debug only and drop its XPRV feature * Make LoadDevice debug only and drop its XPRV feature
* Add feature to retrieve the next U2F counter * Add feature to retrieve the next U2F counter
* Passphrase redesign
Version 1.8.3 [Sep 2019] Version 1.8.3 [Sep 2019]
* Small code improvements * Small code improvements

View File

@ -31,6 +31,7 @@
#include "config.h" #include "config.h"
#include "curves.h" #include "curves.h"
#include "debug.h" #include "debug.h"
#include "fsm.h"
#include "gettext.h" #include "gettext.h"
#include "hmac.h" #include "hmac.h"
#include "layout2.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. * storage.u2f_counter + config_u2f_offset.
* This corresponds to the number of cleared bits in the U2FAREA. * 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 uint8_t CONFIDENTIAL sessionSeed[64];
static secbool sessionPassphraseCached = secfalse; static secbool sessionIdCached;
static char CONFIDENTIAL sessionPassphrase[51]; static uint8_t sessionId[32];
#define autoLockDelayMsDefault (10 * 60 * 1000U) // 10 minutes #define autoLockDelayMsDefault (10 * 60 * 1000U) // 10 minutes
static secbool autoLockDelayMsCached = secfalse; static secbool autoLockDelayMsCached = secfalse;
@ -407,8 +408,8 @@ void config_init(void) {
void session_clear(bool lock) { void session_clear(bool lock) {
sessionSeedCached = secfalse; sessionSeedCached = secfalse;
memzero(&sessionSeed, sizeof(sessionSeed)); memzero(&sessionSeed, sizeof(sessionSeed));
sessionPassphraseCached = secfalse; sessionIdCached = secfalse;
memzero(&sessionPassphrase, sizeof(sessionPassphrase)); memzero(&sessionId, sizeof(sessionId));
if (lock) { if (lock) {
storage_lock(); storage_lock();
} }
@ -527,8 +528,6 @@ void config_setLanguage(const char *lang) {
} }
void config_setPassphraseProtection(bool passphrase_protection) { void config_setPassphraseProtection(bool passphrase_protection) {
sessionSeedCached = secfalse;
sessionPassphraseCached = secfalse;
config_set_bool(KEY_PASSPHRASE_PROTECTION, passphrase_protection); 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); layoutProgress(_("Waking up"), 1000 * iter / total);
} }
const uint8_t *config_getSeed(bool usePassphrase) { const uint8_t *config_getSeed(void) {
// root node is properly cached // root node is properly cached
if (usePassphrase == (sectrue == sessionSeedUsesPassphrase) && if (sectrue == sessionSeedCached) {
sectrue == sessionSeedCached) {
return sessionSeed; return sessionSeed;
} }
// if storage has mnemonic, convert it to node and use it // if storage has mnemonic, convert it to node and use it
char mnemonic[MAX_MNEMONIC_LEN + 1] = {0}; char mnemonic[MAX_MNEMONIC_LEN + 1] = {0};
if (config_getMnemonic(mnemonic, sizeof(mnemonic))) { if (config_getMnemonic(mnemonic, sizeof(mnemonic))) {
if (usePassphrase && !protectPassphrase()) { char passphrase[MAX_PASSPHRASE_LEN + 1] = {0};
if (!protectPassphrase(passphrase)) {
memzero(mnemonic, sizeof(mnemonic)); memzero(mnemonic, sizeof(mnemonic));
memzero(passphrase, sizeof(passphrase));
return NULL; return NULL;
} }
// if storage was not imported (i.e. it was properly generated or recovered) // 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); char oldTiny = usbTiny(1);
mnemonic_to_seed(mnemonic, usePassphrase ? sessionPassphrase : "", mnemonic_to_seed(mnemonic, passphrase, sessionSeed,
sessionSeed, get_root_node_callback); // BIP-0039 get_root_node_callback); // BIP-0039
memzero(mnemonic, sizeof(mnemonic)); memzero(mnemonic, sizeof(mnemonic));
memzero(passphrase, sizeof(passphrase));
usbTiny(oldTiny); usbTiny(oldTiny);
sessionSeedCached = sectrue; sessionSeedCached = sectrue;
sessionSeedUsesPassphrase = usePassphrase ? sectrue : secfalse;
return sessionSeed; return sessionSeed;
} else {
fsm_sendFailure(FailureType_Failure_NotInitialized,
_("Device not initialized"));
} }
return NULL; return NULL;
@ -606,58 +609,16 @@ bool config_getU2FRoot(HDNode *node) {
return ret; return ret;
} }
bool config_getRootNode(HDNode *node, const char *curve, bool usePassphrase) { bool config_getRootNode(HDNode *node, const char *curve) {
// if storage has node, decrypt and use it const uint8_t *seed = config_getSeed();
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);
if (seed == NULL) { if (seed == NULL) {
return false; return false;
} }
int result = hdnode_from_seed(seed, 64, curve, node);
return 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) { 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; return sectrue == ret;
} }
void session_cachePassphrase(const char *passphrase) { const uint8_t *session_getSessionId(void) {
strlcpy(sessionPassphrase, passphrase, sizeof(sessionPassphrase)); if (!sessionIdCached) {
sessionPassphraseCached = sectrue; random_buffer(sessionId, 32);
}
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;
} }
if (!salt) { sessionIdCached = sectrue;
// if salt is not provided fill the first half of the state with random data return sessionId;
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;
} }
bool session_isUnlocked(void) { return sectrue == storage_is_unlocked(); } bool session_isUnlocked(void) { return sectrue == storage_is_unlocked(); }

View File

@ -91,10 +91,10 @@ void session_clear(bool lock);
void config_loadDevice(const LoadDevice *msg); 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_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); bool config_getLabel(char *dest, uint16_t dest_size);
void config_setLabel(const char *label); 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); bool config_getHomescreen(uint8_t *dest, uint16_t dest_size);
void config_setHomescreen(const uint8_t *data, uint32_t size); void config_setHomescreen(const uint8_t *data, uint32_t size);
void session_cachePassphrase(const char *passphrase); const uint8_t *session_getSessionId(void);
bool session_isPassphraseCached(void);
bool session_getState(const uint8_t *salt, uint8_t *state,
const char *passphrase);
bool config_setMnemonic(const char *mnemonic); bool config_setMnemonic(const char *mnemonic);
bool config_containsMnemonic(const char *mnemonic); bool config_containsMnemonic(const char *mnemonic);

View File

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

View File

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

View File

@ -350,10 +350,12 @@ bool protectChangeWipeCode(bool removal) {
return ret; return ret;
} }
bool protectPassphrase(void) { bool protectPassphrase(char *passphrase) {
memzero(passphrase, MAX_PASSPHRASE_LEN + 1);
bool passphrase_protection = false; bool passphrase_protection = false;
config_getPassphraseProtection(&passphrase_protection); config_getPassphraseProtection(&passphrase_protection);
if (!passphrase_protection || session_isPassphraseCached()) { if (!passphrase_protection) {
// passphrase already set to empty by memzero above
return true; return true;
} }
@ -369,12 +371,24 @@ bool protectPassphrase(void) {
bool result; bool result;
for (;;) { for (;;) {
usbPoll(); usbPoll();
// TODO: correctly process PassphraseAck with state field set (mismatch =>
// Failure)
if (msg_tiny_id == MessageType_MessageType_PassphraseAck) { if (msg_tiny_id == MessageType_MessageType_PassphraseAck) {
msg_tiny_id = 0xFFFF; msg_tiny_id = 0xFFFF;
PassphraseAck *ppa = (PassphraseAck *)msg_tiny; 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; result = true;
break; break;
} }
@ -383,8 +397,9 @@ bool protectPassphrase(void) {
protectAbortedByInitialize = protectAbortedByInitialize =
(msg_tiny_id == MessageType_MessageType_Initialize); (msg_tiny_id == MessageType_MessageType_Initialize);
if (protectAbortedByCancel || protectAbortedByInitialize) { if (protectAbortedByCancel || protectAbortedByInitialize) {
msg_tiny_id = 0xFFFF; fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL);
result = false; result = false;
msg_tiny_id = 0xFFFF;
break; break;
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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._ _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 ### 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 - built-in functionality of UdpTransport to wait until an emulator comes up, and the
related command `trezorctl wait-for-emulator` 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] - 2019-12-30
[0.11.6]: https://github.com/trezor/trezor-firmware/compare/python/v0.11.5...python/v0.11.6 [0.11.6]: https://github.com/trezor/trezor-firmware/compare/python/v0.11.5...python/v0.11.6

View File

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

View File

@ -16,15 +16,9 @@
import click import click
from .. import device, messages from .. import device
from . import ChoiceType 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} ROTATION = {"north": 0, "east": 90, "south": 180, "west": 270}
@ -145,10 +139,13 @@ def passphrase():
@passphrase.command(name="enabled") @passphrase.command(name="enabled")
@click.option("-f/-F", "--force-on-device/--no-force-on-device", default=None)
@click.pass_obj @click.pass_obj
def passphrase_enable(connect): def passphrase_enable(connect, force_on_device: bool):
"""Enable passphrase.""" """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") @passphrase.command(name="disabled")
@ -156,19 +153,3 @@ def passphrase_enable(connect):
def passphrase_disable(connect): def passphrase_disable(connect):
"""Disable passphrase.""" """Disable passphrase."""
return device.apply_settings(connect(), use_passphrase=False) 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)

View File

@ -52,7 +52,6 @@ COMMAND_ALIASES = {
"change-pin": settings.pin, "change-pin": settings.pin,
"enable-passphrase": settings.passphrase_enable, "enable-passphrase": settings.passphrase_enable,
"disable-passphrase": settings.passphrase_disable, "disable-passphrase": settings.passphrase_disable,
"set-passphrase-source": settings.passphrase_source,
"wipe-device": device.wipe, "wipe-device": device.wipe,
"reset-device": device.setup, "reset-device": device.setup,
"recovery-device": device.recover, "recovery-device": device.recover,
@ -143,11 +142,26 @@ def configure_logging(verbose: int):
@click.option( @click.option(
"-j", "--json", "is_json", is_flag=True, help="Print result as JSON object" "-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.version_option()
@click.pass_context @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) 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(): def get_device():
try: try:
device = get_transport(path, prefix_search=False) device = get_transport(path, prefix_search=False)
@ -159,13 +173,17 @@ def cli(ctx, path, verbose, is_json):
if path is not None: if path is not None:
click.echo("Using path: {}".format(path)) click.echo("Using path: {}".format(path))
sys.exit(1) 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 ctx.obj = get_device
@cli.resultcallback() @cli.resultcallback()
def print_result(res, path, verbose, is_json): def print_result(res, is_json, **kwargs):
if is_json: if is_json:
if isinstance(res, protobuf.MessageType): if isinstance(res, protobuf.MessageType):
click.echo(json.dumps({res.__class__.__name__: res.__dict__})) click.echo(json.dumps({res.__class__.__name__: res.__dict__}))
@ -202,7 +220,7 @@ def list_devices():
@cli.command() @cli.command()
def version(): def version():
"""Show version of trezorctl/trezorlib.""" """Show version of trezorctl/trezorlib."""
from trezorlib import __version__ as VERSION from .. import __version__ as VERSION
return VERSION return VERSION
@ -215,17 +233,33 @@ def version():
@cli.command() @cli.command()
@click.argument("message") @click.argument("message")
@click.option("-b", "--button-protection", is_flag=True) @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 @click.pass_obj
def ping(connect, message, button_protection, pin_protection, passphrase_protection): def ping(connect, message, button_protection):
"""Send ping message.""" """Send ping message."""
return connect().ping( return connect().ping(message, button_protection=button_protection)
message,
button_protection=button_protection,
pin_protection=pin_protection, @cli.command()
passphrase_protection=passphrase_protection, @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() @cli.command()

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ if __debug__:
try: try:
from typing import Dict, List # noqa: F401 from typing import Dict, List # noqa: F401
from typing_extensions import Literal # 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: except ImportError:
pass pass
@ -23,5 +23,5 @@ class ButtonRequest(p.MessageType):
@classmethod @classmethod
def get_fields(cls) -> Dict: def get_fields(cls) -> Dict:
return { 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),
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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]

View File

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

View File

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

View File

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

1
tests/.gitignore vendored
View File

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

View File

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

View File

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

View File

@ -34,7 +34,7 @@ class TestDebuglink:
@pytest.mark.setup_client(mnemonic=MNEMONIC12, pin="1234", passphrase=True) @pytest.mark.setup_client(mnemonic=MNEMONIC12, pin="1234", passphrase=True)
def test_pin(self, client): 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) assert isinstance(resp, messages.PinMatrixRequest)
pin, matrix = client.debug.read_pin() pin, matrix = client.debug.read_pin()
@ -43,4 +43,7 @@ class TestDebuglink:
pin_encoded = client.debug.read_pin_encoded() pin_encoded = client.debug.read_pin_encoded()
resp = client.call_raw(messages.PinMatrixAck(pin=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)

View File

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

View File

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

View File

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

View File

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

View File

@ -16,78 +16,41 @@
import pytest 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 @pytest.mark.skip_ui
class TestMsgClearsession: @pytest.mark.setup_client(pin=True, passphrase=True)
@pytest.mark.setup_client(pin=True, passphrase=True) def test_clear_session(client):
def test_clearsession(self, client): if client.features.model == "1":
with client: init_responses = [messages.PinMatrixRequest(), messages.PassphraseRequest()]
client.set_expected_responses( else:
[ init_responses = [messages.PassphraseRequest()]
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: cached_responses = [messages.PublicKey()]
# 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"
client.clear_session() with client:
client.set_expected_responses(init_responses + cached_responses)
assert get_public_node(client, ADDRESS_N).xpub == XPUB
# session cache is cleared with client:
with client: # pin and passphrase are cached
client.set_expected_responses( client.set_expected_responses(cached_responses)
[ assert get_public_node(client, ADDRESS_N).xpub == XPUB
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.clear_session()
# pin and passphrase are cached
client.set_expected_responses( # session cache is cleared
[ with client:
proto.ButtonRequest(code=proto.ButtonRequestType.ProtectCall), client.set_expected_responses(init_responses + cached_responses)
proto.Success(), assert get_public_node(client, ADDRESS_N).xpub == XPUB
]
) with client:
res = client.ping( # pin and passphrase are cached
"random data", client.set_expected_responses(cached_responses)
button_protection=True, assert get_public_node(client, ADDRESS_N).xpub == XPUB
pin_protection=True,
passphrase_protection=True,
)
assert res == "random data"

View File

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

View File

@ -19,8 +19,8 @@ import pytest
from trezorlib import messages as proto from trezorlib import messages as proto
@pytest.mark.skip_t2
class TestMsgPing: class TestMsgPing:
@pytest.mark.skip_ui
@pytest.mark.setup_client(pin=True, passphrase=True) @pytest.mark.setup_client(pin=True, passphrase=True)
def test_ping(self, client): def test_ping(self, client):
with client: with client:
@ -37,48 +37,3 @@ class TestMsgPing:
) )
res = client.ping("random data", button_protection=True) res = client.ping("random data", button_protection=True)
assert res == "random data" 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"

View File

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

View File

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

View File

@ -18,7 +18,7 @@ import time
import pytest import pytest
from trezorlib import messages as proto from trezorlib import btc, messages as proto
from trezorlib.exceptions import PinException from trezorlib.exceptions import PinException
# FIXME TODO Add passphrase tests # FIXME TODO Add passphrase tests
@ -26,84 +26,36 @@ from trezorlib.exceptions import PinException
@pytest.mark.skip_t2 @pytest.mark.skip_t2
class TestProtectCall: 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 # This method perform any call which have protection in the device
res = client.ping( res = btc.get_address(client, "Testnet", [0])
"random data", assert res == "mndoQDWatQhfeQbprzZxD43mZ75Z94D6vz"
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)
def test_no_protection(self, client): def test_no_protection(self, client):
with client: with client:
assert client.debug.read_pin()[0] is None assert client.debug.read_pin()[0] is None
client.set_expected_responses([proto.Success()]) client.set_expected_responses([proto.Address()])
self._some_protected_call(client, False, True, True) self._some_protected_call(client)
@pytest.mark.setup_client(pin="1234", passphrase=True) @pytest.mark.setup_client(pin="1234")
def test_pin(self, client): def test_pin(self, client):
with client: with client:
assert client.debug.read_pin()[0] == "1234" assert client.debug.read_pin()[0] == "1234"
client.setup_debuglink(button=True, pin_correct=True) client.setup_debuglink(button=True, pin_correct=True)
client.set_expected_responses( client.set_expected_responses([proto.PinMatrixRequest(), proto.Address()])
[proto.ButtonRequest(), proto.PinMatrixRequest(), proto.Success()] self._some_protected_call(client)
)
self._some_protected_call(client, True, True, False)
@pytest.mark.setup_client(pin="1234", passphrase=True) @pytest.mark.setup_client(pin="1234")
def test_incorrect_pin(self, client): def test_incorrect_pin(self, client):
client.setup_debuglink(button=True, pin_correct=False) client.setup_debuglink(button=True, pin_correct=False)
with pytest.raises(PinException): 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): def test_cancelled_pin(self, client):
client.setup_debuglink(button=True, pin_correct=False) # PIN cancel client.setup_debuglink(button=True, pin_correct=False) # PIN cancel
with pytest.raises(PinException): 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", passphrase=True)
def test_exponential_backoff_with_reboot(self, client): def test_exponential_backoff_with_reboot(self, client):
@ -126,5 +78,5 @@ class TestProtectCall:
for attempt in range(1, 4): for attempt in range(1, 4):
start = time.time() start = time.time()
with pytest.raises(PinException): with pytest.raises(PinException):
self._some_protected_call(client, False, True, False) self._some_protected_call(client)
test_backoff(attempt, start) test_backoff(attempt, start)

View File

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

View File

@ -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

View File

@ -9,9 +9,9 @@
"test_cancel.py::test_cancel_message_via_initialize[message1]": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "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": "b698654871541258f97d58ada0f010b2d77b74829791566746cad619d3740a94",
"test_msg_applysettings.py-test_apply_settings_passphrase": "fb38537b921f8064f7ea6e1a584e70a8be74968a3be6726b7d36cf57de0d7865", "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_bip39": "dfdbd0ae6774177d43f2f11d026c4d8679dd994b508c1d850d9cfee5dd1118ac",
"test_msg_backup_device.py::test_backup_slip39_advanced": "aec6a663f8f75ff73d2f2377d12347b062b831ce0d5fcfe9d7f3c975a05aef32", "test_msg_backup_device.py::test_backup_slip39_advanced": "244b31044a25e44847a6efb79ea4cb67c246a971bf828d46aa79d14151934a04",
"test_msg_backup_device.py::test_backup_slip39_basic": "a4c44785873509bd4ebffa6ec82f2fe732349eb0bd23631dbc3876e956b567c7", "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_interrupt_backup_fails": "225b3da1acac6e9a65106fcc4a01de8a44de035aedb4dcc21c09f439199fdf40",
"test_msg_backup_device.py::test_no_backup_fails": "93039a9472cfc9058563bd56e4a3dbe2e41af64744a61f6ee3255a04bd3a9366", "test_msg_backup_device.py::test_no_backup_fails": "93039a9472cfc9058563bd56e4a3dbe2e41af64744a61f6ee3255a04bd3a9366",
"test_msg_backup_device.py::test_no_backup_show_entropy_fails": "14fcdd2ded299ca099a35966cc9f21204b31de8d6bab9ec91cb64537bd70440c", "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-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-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.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-0-Ae2tdPwUPE": "d2d6aac0a4605f1a961580f0fc156e7b69b993f908906281313429fa3222f349",
"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-1-Ae2tdPwUPE": "d2d6aac0a4605f1a961580f0fc156e7b69b993f908906281313429fa3222f349",
"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-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'-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'-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'-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.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-0-bc04": "d2d6aac0a4605f1a961580f0fc156e7b69b993f908906281313429fa3222f349",
"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-1-24c4": "d2d6aac0a4605f1a961580f0fc156e7b69b993f908906281313429fa3222f349",
"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-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[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-inputs0-outputs0-transactions0-": "07eceef966cb8069381dc5105b732bce6de8d207a1d27e56e8abddd57c307b22",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-inputs1-outputs1-transactions1-": "4c74546cacb2b99b9aeb6e134d99c0d1d6f0ba1818d1182364cfdb94b938ccc5", "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[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-inputs0-outputs0-transactio": "bc88a23280c0234860ccbb5e96d5cc3a851e2f2f9928c400f6c0907c68172d39",
"test_msg_cardano_sign_tx_slip39_basic.py::test_cardano_sign_tx[764824073-inputs1-outputs1-transactio": "6956bb359388186b4c127ae88f4d86527381caf8098f55b4fa05c343640c081f", "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_pin_to_wipe_code": "9519b574e3f749810977ac55f851553f2051b51ef7799ffaa155410d89e1cd63",
"test_msg_change_wipe_code_t2.py::test_set_remove_wipe_code": "74e8309c74ca9e599de91296cfcef63a744713acad3f3ea10b7b461eba69c30e", "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": "fdbd58fb31323cfc32a9a7684d4dd45a5bd2b098156f2abf9cca5b5a481ecd10", "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": "7b3599a5582b566c65c337b50fb6e38fc577b67310fc731ed756b7d143f4727d", "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": "b25571c82be5f00b1f0c0bca95154dbae6a6ac092b249b79f805a126e1580037", "test_msg_change_wipe_code_t2.py::test_wipe_code_activate": "348ff6811029253d7d520b37a2d3ff219516a7401c8b65ab088c7a7d39bd8b2b",
"test_msg_changepin_t2.py::test_change_failed": "d39ba207aab689c53adde211dc8e189809ffe4f3a3c547bd8c0422ad20663dc2", "test_msg_changepin_t2.py::test_change_failed": "370c59da62a84aaefa242562c36a6facac89c7f819e37d1ae8cbe2c44a2de256",
"test_msg_changepin_t2.py::test_change_pin": "9e43d9f05c96f61e5ba7723e7d3ceb26ed813fd5be95d2cfa7e01573ab87b531", "test_msg_changepin_t2.py::test_change_pin": "c42fca9bf8f3b4c330516d90231ae0cfa7419d83370be9cfcf6a81cca3f3b06c",
"test_msg_changepin_t2.py::test_remove_pin": "249a84500e2cac217f4b3c7cfbb4d4212781019191b21d1386f81317f6d699e7", "test_msg_changepin_t2.py::test_remove_pin": "d049eaa6cd11e88b7af193b080cf868b62271266ad6f2973bfd82944b523741d",
"test_msg_changepin_t2.py::test_set_failed": "5aa994de8e571d08512705b04e0c55b60411d7d23491f75688f16d255369363e", "test_msg_changepin_t2.py::test_set_failed": "59beeec1a00817f664a5fd93234012588613aac93c45d53c27550fe5d0ef8380",
"test_msg_changepin_t2.py::test_set_pin": "9e30570a9063a9820d423dd000d5ba70f30bf32de78cce9160643f60d5fe4eb2", "test_msg_changepin_t2.py::test_set_pin": "3aafe16a451f928c9bfff2a3ff7e3c23ce4948c9a044ebd04834df045670183f",
"test_msg_cipherkeyvalue.py-test_decrypt": "166d85b1bf11aeaeb5b93ef5d047b6f8910c28b8fce1d853e6912d89d7bfca2f", "test_msg_cipherkeyvalue.py-test_decrypt": "166d85b1bf11aeaeb5b93ef5d047b6f8910c28b8fce1d853e6912d89d7bfca2f",
"test_msg_cipherkeyvalue.py-test_decrypt_badlen": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_msg_cipherkeyvalue.py-test_decrypt_badlen": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cipherkeyvalue.py-test_encrypt": "3a37e4004c87bc6df6a8fa7c93b6fe3e3524986914709fda2f9c99ba0ff69775", "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": "5e9cf05f6ccf10f697cae9f780042db934892e1d7c68fb2f19a40319a687ea50",
"test_msg_lisk_verifymessage.py-test_verify_long": "26adab7e31f388e5b034a865f9c010d57e67fd855d44839d2f2600d8317bd98e", "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_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_advanced": "1c6db0d592b1d22b3c9fce3ddab8a9fd138f11d83e5d4e64431a02bf4ffed605",
"test_msg_loaddevice.py-test_load_device_slip39_basic": "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_getaddress.py-test_monero_getaddress": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_monero_getwatchkey.py-test_monero_getwatchkey": "d77fa4d4322e145c41f1ce07526ff59f8b58d8854aeffaa5266e14cd572350e7", "test_msg_monero_getwatchkey.py-test_monero_getwatchkey": "d77fa4d4322e145c41f1ce07526ff59f8b58d8854aeffaa5266e14cd572350e7",
"test_msg_nem_getaddress.py-test_nem_getaddress": "e726f99401a20eb74c33d755cecea2a3f69b7ae5b541302677ee05f80f5aef19", "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_dryrun.py::test_uninitialized": "14fcdd2ded299ca099a35966cc9f21204b31de8d6bab9ec91cb64537bd70440c",
"test_msg_recoverydevice_bip39_t2.py-test_already_initialized": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "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_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_abort": "793dde7fb47e9c4ad36369be396da20332560f29083d7f9a2b0582173371c9ed",
"test_msg_recoverydevice_slip39_advanced.py::test_extra_share_entered": "00a94e20b786346c45f987860b2465f299075d7c6de4971f42a4749e1cc8bfc0", "test_msg_recoverydevice_slip39_advanced.py::test_extra_share_entered": "00a94e20b786346c45f987860b2465f299075d7c6de4971f42a4749e1cc8bfc0",
"test_msg_recoverydevice_slip39_advanced.py::test_group_threshold_reached": "3b075a276c4e0d53fbc51ce1f29594bbd474d25f47c0f6a32caac41ba0ba2138", "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_abort": "793dde7fb47e9c4ad36369be396da20332560f29083d7f9a2b0582173371c9ed",
"test_msg_recoverydevice_slip39_basic.py::test_ask_word_number": "8e9d9fd75e17f6b44829ae2d7b0eb9e60b48577f975abc6d75116f8365241082", "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_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_same_share": "e6a54429fdbedea9efca9cbed736aada07f95f3b20f895f9c1c5ec056a2be014",
"test_msg_recoverydevice_slip39_basic.py::test_secret[shares0-491b795b80fc21ccdf466c0fbc98c8fc]": "54581a91b55ab531b215cba61052fd77c505232c510f170080760605eb9b8c46", "test_msg_recoverydevice_slip39_basic.py::test_secret[shares0-491b795b80fc21ccdf466c0fbc98c8fc]": "54581a91b55ab531b215cba61052fd77c505232c510f170080760605eb9b8c46",
"test_msg_recoverydevice_slip39_basic.py::test_secret[shares1-b770e0da1363247652de97a39bdbf2463be0878": "f73bef254762d761db27df46bff62641a9d2ac0602c34fc4e465262bf26ed08f", "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_dryrun": "d84427489f691ecc222b62f83af3e97fa09097404dcba07772a43b5eb0c689e8",
"test_msg_recoverydevice_slip39_basic_dryrun.py::test_2of3_invalid_seed_dryrun": "55f2dd6b4958659f071c3f57e06286f872ac38af4828f446a0f4e91c657dfccc", "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_already_initialized": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_resetdevice_bip39_t2.py-test_failed_pin": "a6ba803a7572dc8e29c821433a067c3a990185a8eb7b95d0106cd8f4b82e9da2", "test_msg_resetdevice_bip39_t2.py-test_failed_pin": "a52f3479f7d8e14c7f89af9b305a50f3bd244fee28f17ebc0abedba701c32811",
"test_msg_resetdevice_bip39_t2.py-test_reset_device": "cbe1e439c2e9810ff6ee1031268810188b3cd07e029a7aa1d4b227e33707c727", "test_msg_resetdevice_bip39_t2.py-test_reset_device": "13d739a387a80aa96f533bb6f3b0f2ff12c7ba84608d0797ac603208d9aed796",
"test_msg_resetdevice_bip39_t2.py-test_reset_device_pin": "af378bff7e8fdf16324715f640e8b17bebd69f779758ab33cd7025c557a1d5c0", "test_msg_resetdevice_bip39_t2.py-test_reset_device_pin": "ed64165674816dfb408ead89abb8d1a5743f50f63933aceba6701149522c3866",
"test_msg_resetdevice_slip39_advanced.py-test_reset_device_slip39_advanced": "7daabeb21a17a8c8e8adb800a8b6b0504e5d2e7cf0bbfcb14770fb33df1426a2", "test_msg_resetdevice_slip39_advanced.py-test_reset_device_slip39_advanced": "005622513c517610a4a1af529ef94760b4d0406b971f0b2f2557c93ce0dac8c9",
"test_msg_resetdevice_slip39_basic.py-test_reset_device_slip39_basic": "1b56e3335876c01ec684839158bc0aa3a31aac03d2beef8d3ee63a0a441e3f1f", "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": "2bb7d7bf48f1218530b4d7045d48480cad6411e110df537551b2f80b342007f2",
"test_msg_ripple_get_address.py-test_ripple_get_address_other": "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", "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_advanced.py::test_256bit_passphrase": "69b6b8b22c819e1282d7d2c14b31bf8d015c81ac05fe034540dbb11c8a20dbdb",
"test_passphrase_slip39_basic.py::test_2of5_passphrase": "1e00b1a7840bc144b98b7bce26f74fc913a0abf9d1c500571d7803b6b2e0943c", "test_passphrase_slip39_basic.py::test_2of5_passphrase": "1e00b1a7840bc144b98b7bce26f74fc913a0abf9d1c500571d7803b6b2e0943c",
"test_passphrase_slip39_basic.py::test_3of6_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[0-backup_flow_bip39]": "a73fcd9af54d3b55cc7c21e68b1637a6e29829bfd57d47b0503e67fa22e4106a",
"test_reset_backup.py::test_skip_backup_manual[1-backup_flow_slip39_basic]": "55224486083a1268c76ffe1d2b47ca7fba29ca6955e3601dae9e2abd44fb9d62", "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]": "c40d7894875393bc093e23d02dab8ad668929767a542a27ff89c6dd5b7b963de", "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]": "68eb02ad320eee49d3a6aac891e5dc4c35ee09a1e98a2480c2cb1b672cbb0696", "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]": "34674c8b9f4b6a836940fa347bacfd6d31dd681d41bf8e1ec129e9deef590588", "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]": "24c641cde46665219de99978c3e27088fd4ade55f2665535a95e7dce1f2b3148", "test_reset_backup.py::test_skip_backup_msg[2-backup_flow_slip39_advanced]": "0bafb944169d0ab608513deadba15411d1756ef2658253740c5d8792f4527e9d",
"test_u2f_counter.py::test_u2f_counter": "7d96a4d262b9d8a2c1158ac1e5f0f7b2c3ed5f2ba9d6235a014320313f9488fe", "test_u2f_counter.py::test_u2f_counter": "7d96a4d262b9d8a2c1158ac1e5f0f7b2c3ed5f2ba9d6235a014320313f9488fe",
"test_zerosig.py-test_one_zero_signature": "401aeaf7b2f565e2064a3c1a57a8ee3afe1e9bf251fba0874390685e7e0f178f", "test_zerosig.py-test_one_zero_signature": "401aeaf7b2f565e2064a3c1a57a8ee3afe1e9bf251fba0874390685e7e0f178f",
"test_zerosig.py-test_two_zero_signature": "7a01a057fb5dd3e6e38e7986875c5d07f0700bd80b519660e0b42973a9afd664" "test_zerosig.py-test_two_zero_signature": "7a01a057fb5dd3e6e38e7986875c5d07f0700bd80b519660e0b42973a9afd664"