diff --git a/common/protob/messages-common.proto b/common/protob/messages-common.proto index cb89d8a9a..607784078 100644 --- a/common/protob/messages-common.proto +++ b/common/protob/messages-common.proto @@ -62,11 +62,12 @@ message ButtonRequest { ButtonRequest_PublicKey = 11; ButtonRequest_MnemonicWordCount = 12; ButtonRequest_MnemonicInput = 13; - ButtonRequest_PassphraseType = 14; +// ButtonRequest_PassphraseType = 14; DEPRECATED ButtonRequest_UnknownDerivationPath = 15; ButtonRequest_RecoveryHomepage = 16; ButtonRequest_Success = 17; ButtonRequest_Warning = 18; + ButtonRequest_PassphraseEntry = 19; } } @@ -110,31 +111,36 @@ message PinMatrixAck { * @next PassphraseAck */ message PassphraseRequest { - optional bool on_device = 1; // passphrase is being entered on the device + optional bool _on_device = 1 [deprecated=true]; // <2.3.0 } /** * Request: Send passphrase back - * @next PassphraseStateRequest + * @auxend */ message PassphraseAck { optional string passphrase = 1; - optional bytes state = 2; // expected device state + optional bytes _state = 2 [deprecated=true]; // <2.3.0 + optional bool on_device = 3; // user wants to enter passphrase on the device } /** * Response: Device awaits passphrase state - * @next PassphraseStateAck + * Deprecated in 2.3.0 + * @next Deprecated_PassphraseStateAck */ -message PassphraseStateRequest { +message Deprecated_PassphraseStateRequest { + option deprecated = true; optional bytes state = 1; // actual device state } /** * Request: Send passphrase state back + * Deprecated in 2.3.0 * @auxend */ -message PassphraseStateAck { +message Deprecated_PassphraseStateAck { + option deprecated = true; } /** diff --git a/common/protob/messages-management.proto b/common/protob/messages-management.proto index 8b7f0b215..be936f60f 100644 --- a/common/protob/messages-management.proto +++ b/common/protob/messages-management.proto @@ -20,8 +20,7 @@ enum BackupType { * @next Features */ message Initialize { - optional bytes state = 1; // assumed device state, clear session if set and different - optional bool skip_passphrase = 2; // this session should always assume empty passphrase + optional bytes session_id = 1; // assumed device session id; Trezor clears caches if it is different or empty } /** @@ -52,7 +51,7 @@ message Features { optional bytes bootloader_hash = 14; // hash of the bootloader optional bool imported = 15; // was storage imported from an external source? optional bool pin_cached = 16; // is PIN already cached in session? - optional bool passphrase_cached = 17; // is passphrase already cached in session? +// optional bool passphrase_cached = 17; // is passphrase already cached in session? DEPRECATED optional bool firmware_present = 18; // is valid firmware loaded? optional bool needs_backup = 19; // does storage need backup? (equals to Storage.needs_backup) optional uint32 flags = 20; // device flags (equals to Storage.flags) @@ -83,11 +82,14 @@ message Features { Capability_U2F = 14; Capability_Shamir = 15; Capability_ShamirGroups = 16; + Capability_PassphraseEntry = 17; // the device is capable of passphrase entry directly on the device } optional BackupType backup_type = 31; // type of device backup (BIP-39 / SLIP-39 basic / SLIP-39 advanced) optional bool sd_card_present = 32; // is SD card present optional bool sd_protection = 33; // is SD Protect enabled optional bool wipe_code_protection = 34; // is wipe code protection enabled + optional bytes session_id = 35; + optional bool passphrase_always_on_device = 36; // device enforces passphrase entry on Trezor } /** @@ -109,17 +111,10 @@ message ApplySettings { optional string label = 2; optional bool use_passphrase = 3; optional bytes homescreen = 4; - optional PassphraseSourceType passphrase_source = 5; +// optional PassphraseSourceType passphrase_source = 5; DEPRECATED optional uint32 auto_lock_delay_ms = 6; optional uint32 display_rotation = 7; // in degrees from North - /** - * Structure representing passphrase source - */ - enum PassphraseSourceType { - ASK = 0; - DEVICE = 1; - HOST = 2; - } + optional bool passphrase_always_on_device = 8; // do not prompt for passphrase, enforce device entry } /** @@ -178,8 +173,6 @@ message SdProtect { message Ping { optional string message = 1; // message to send back in Success message optional bool button_protection = 2; // ask for button press - optional bool pin_protection = 3; // ask for PIN if set in device - optional bool passphrase_protection = 4; // ask for passphrase if set in device } /** diff --git a/common/protob/messages.proto b/common/protob/messages.proto index 38a39ed71..0f4af5e79 100644 --- a/common/protob/messages.proto +++ b/common/protob/messages.proto @@ -54,8 +54,6 @@ enum MessageType { MessageType_EntropyAck = 36 [(wire_in) = true]; MessageType_PassphraseRequest = 41 [(wire_out) = true]; MessageType_PassphraseAck = 42 [(wire_in) = true, (wire_tiny) = true, (wire_no_fsm) = true]; - MessageType_PassphraseStateRequest = 77 [(wire_out) = true]; - MessageType_PassphraseStateAck = 78 [(wire_in) = true, (wire_tiny) = true, (wire_no_fsm) = true]; MessageType_RecoveryDevice = 45 [(wire_in) = true]; MessageType_WordRequest = 46 [(wire_out) = true]; MessageType_WordAck = 47 [(wire_in) = true]; @@ -66,6 +64,11 @@ enum MessageType { MessageType_NextU2FCounter = 81 [(wire_out) = true]; MessageType_ChangeWipeCode = 82 [(wire_in) = true]; + // Deprecated messages, kept for protobuf compatibility. + // Both are marked wire_out so that we don't need to implement incoming handler for legacy + MessageType_Deprecated_PassphraseStateRequest = 77 [deprecated = true]; + MessageType_Deprecated_PassphraseStateAck = 78 [deprecated = true]; + // Bootloader MessageType_FirmwareErase = 6 [(wire_in) = true, (wire_bootloader) = true]; MessageType_FirmwareUpload = 7 [(wire_in) = true, (wire_bootloader) = true]; diff --git a/core/ChangeLog b/core/ChangeLog index 7be5a24d1..135c11d43 100644 --- a/core/ChangeLog +++ b/core/ChangeLog @@ -2,6 +2,9 @@ Version 2.x.x [not yet released] * SD card protection * Upgrade MicroPython to 1.12 +Version 2.3.0 [not yet released] +* Passphrase redesign + Version 2.2.0 [Jan 2020] * Remove unused ButtonRequest.data field * Rework Recovery persistence internally diff --git a/core/embed/firmware/version.h b/core/embed/firmware/version.h index da58213ff..0654e2dd1 100644 --- a/core/embed/firmware/version.h +++ b/core/embed/firmware/version.h @@ -1,6 +1,6 @@ #define VERSION_MAJOR 2 -#define VERSION_MINOR 2 -#define VERSION_PATCH 1 +#define VERSION_MINOR 3 +#define VERSION_PATCH 0 #define VERSION_BUILD 0 #define FIX_VERSION_MAJOR 2 diff --git a/core/src/apps/cardano/seed.py b/core/src/apps/cardano/seed.py index 672daf778..d09033c54 100644 --- a/core/src/apps/cardano/seed.py +++ b/core/src/apps/cardano/seed.py @@ -1,11 +1,11 @@ import storage -import storage.cache +from storage import cache from trezor import wire from trezor.crypto import bip32 from apps.cardano import CURVE, SEED_NAMESPACE from apps.common import mnemonic -from apps.common.request_passphrase import protect_by_passphrase +from apps.common.passphrase import get as get_passphrase class Keychain: @@ -30,34 +30,27 @@ class Keychain: return node -async def _get_passphrase(ctx: wire.Context) -> bytes: - passphrase = storage.cache.get_passphrase() - if passphrase is None: - passphrase = await protect_by_passphrase(ctx) - storage.cache.set_passphrase(passphrase) - - return passphrase - - async def get_keychain(ctx: wire.Context) -> Keychain: + root = cache.get(cache.APP_CARDANO_ROOT) + if not storage.is_initialized(): raise wire.NotInitialized("Device is not initialized") - if mnemonic.is_bip39(): - # derive the root node from mnemonic and passphrase - passphrase = await _get_passphrase(ctx) - root = bip32.from_mnemonic_cardano(mnemonic.get_secret().decode(), passphrase) - else: - seed = storage.cache.get_seed() - if seed is None: - passphrase = await _get_passphrase(ctx) + if root is None: + passphrase = await get_passphrase(ctx) + if mnemonic.is_bip39(): + # derive the root node from mnemonic and passphrase + root = bip32.from_mnemonic_cardano( + mnemonic.get_secret().decode(), passphrase + ) + else: seed = mnemonic.get_seed(passphrase) - storage.cache.set_seed(seed) - root = bip32.from_seed(seed, "ed25519 cardano seed") + root = bip32.from_seed(seed, "ed25519 cardano seed") - # derive the namespaced root node - for i in SEED_NAMESPACE: - root.derive_cardano(i) + # derive the namespaced root node + for i in SEED_NAMESPACE: + root.derive_cardano(i) + storage.cache.set(cache.APP_CARDANO_ROOT, root) keychain = Keychain(SEED_NAMESPACE, root) return keychain diff --git a/core/src/apps/common/mnemonic.py b/core/src/apps/common/mnemonic.py index cf0c2ed0e..5fe54ef06 100644 --- a/core/src/apps/common/mnemonic.py +++ b/core/src/apps/common/mnemonic.py @@ -2,6 +2,7 @@ import storage.device from trezor import ui, utils, workflow from trezor.crypto import bip39, slip39 from trezor.messages import BackupType +from trezor.ui.text import Text if False: from typing import Optional, Tuple @@ -59,11 +60,8 @@ def _start_progress() -> None: # should make sure that no other layout is running. At this point, only # the homescreen should be on, so shut it down. workflow.kill_default() - ui.backlight_fade(ui.BACKLIGHT_DIM) - ui.display.clear() - ui.header("Please wait") - ui.refresh() - ui.backlight_fade(ui.BACKLIGHT_NORMAL) + t = Text("Please wait", ui.ICON_CONFIG) + ui.draw_simple(t) def _render_progress(progress: int, total: int) -> None: diff --git a/core/src/apps/common/passphrase.py b/core/src/apps/common/passphrase.py new file mode 100644 index 000000000..9f59f5af5 --- /dev/null +++ b/core/src/apps/common/passphrase.py @@ -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) diff --git a/core/src/apps/common/request_passphrase.py b/core/src/apps/common/request_passphrase.py deleted file mode 100644 index 476d149a6..000000000 --- a/core/src/apps/common/request_passphrase.py +++ /dev/null @@ -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 diff --git a/core/src/apps/common/seed.py b/core/src/apps/common/seed.py index 8b74880e9..4a022b9ff 100644 --- a/core/src/apps/common/seed.py +++ b/core/src/apps/common/seed.py @@ -1,11 +1,11 @@ import storage -import storage.cache +from storage import cache from trezor import wire from trezor.crypto import bip32, hashlib, hmac from trezor.crypto.curve import secp256k1 from apps.common import HARDENED, mnemonic -from apps.common.request_passphrase import protect_by_passphrase +from apps.common.passphrase import get as get_passphrase if False: from typing import List, Union @@ -112,14 +112,11 @@ class Keychain: async def get_keychain(ctx: wire.Context, namespaces: list) -> Keychain: if not storage.is_initialized(): raise wire.NotInitialized("Device is not initialized") - seed = storage.cache.get_seed() + seed = cache.get(cache.APP_COMMON_SEED) if seed is None: - passphrase = storage.cache.get_passphrase() - if passphrase is None: - passphrase = await protect_by_passphrase(ctx) - storage.cache.set_passphrase(passphrase) + passphrase = await get_passphrase(ctx) seed = mnemonic.get_seed(passphrase) - storage.cache.set_seed(seed) + cache.set(cache.APP_COMMON_SEED, seed) keychain = Keychain(seed, namespaces) return keychain @@ -129,10 +126,10 @@ def derive_node_without_passphrase( ) -> bip32.HDNode: if not storage.is_initialized(): raise Exception("Device is not initialized") - seed = storage.cache.get_seed_without_passphrase() + seed = cache.get(cache.APP_COMMON_SEED_WITHOUT_PASSPHRASE) if seed is None: seed = mnemonic.get_seed(progress_bar=False) - storage.cache.set_seed_without_passphrase(seed) + cache.set(cache.APP_COMMON_SEED_WITHOUT_PASSPHRASE, seed) node = bip32.from_seed(seed, curve_name) node.derive_path(path) return node @@ -141,10 +138,10 @@ def derive_node_without_passphrase( def derive_slip21_node_without_passphrase(path: list) -> Slip21Node: if not storage.is_initialized(): raise Exception("Device is not initialized") - seed = storage.cache.get_seed_without_passphrase() + seed = cache.get(cache.APP_COMMON_SEED_WITHOUT_PASSPHRASE) if seed is None: seed = mnemonic.get_seed(progress_bar=False) - storage.cache.set_seed_without_passphrase(seed) + cache.set(cache.APP_COMMON_SEED_WITHOUT_PASSPHRASE, seed) node = Slip21Node(seed) node.derive_path(path) return node diff --git a/core/src/apps/debug/__init__.py b/core/src/apps/debug/__init__.py index 93bdced3a..c0293772f 100644 --- a/core/src/apps/debug/__init__.py +++ b/core/src/apps/debug/__init__.py @@ -97,13 +97,12 @@ if __debug__: ctx: wire.Context, msg: DebugLinkGetState ) -> DebugLinkState: from trezor.messages.DebugLinkState import DebugLinkState - from storage.device import has_passphrase - from apps.common import mnemonic + from apps.common import mnemonic, passphrase m = DebugLinkState() m.mnemonic_secret = mnemonic.get_secret() m.mnemonic_type = mnemonic.get_type() - m.passphrase_protection = has_passphrase() + m.passphrase_protection = passphrase.is_enabled() m.reset_entropy = reset_internal_entropy if msg.wait_layout or current_content is None: diff --git a/core/src/apps/homescreen/__init__.py b/core/src/apps/homescreen/__init__.py index db711fd11..1eabc8719 100644 --- a/core/src/apps/homescreen/__init__.py +++ b/core/src/apps/homescreen/__init__.py @@ -34,8 +34,7 @@ def get_features() -> Features: f.initialized = storage.is_initialized() f.pin_protection = config.has_pin() f.pin_cached = config.has_pin() - f.passphrase_protection = storage.device.has_passphrase() - f.passphrase_cached = cache.has_passphrase() + f.passphrase_protection = storage.device.is_passphrase_enabled() f.needs_backup = storage.device.needs_backup() f.unfinished_backup = storage.device.unfinished_backup() f.no_backup = storage.device.no_backup() @@ -48,6 +47,7 @@ def get_features() -> Features: Capability.Crypto, Capability.Shamir, Capability.ShamirGroups, + Capability.PassphraseEntry, ] else: f.capabilities = [ @@ -67,18 +67,19 @@ def get_features() -> Features: Capability.U2F, Capability.Shamir, Capability.ShamirGroups, + Capability.PassphraseEntry, ] f.sd_card_present = io.SDCard().present() f.sd_protection = storage.sd_salt.is_enabled() f.wipe_code_protection = config.has_wipe_code() + f.session_id = cache.get_session_id() + f.passphrase_always_on_device = storage.device.get_passphrase_always_on_device() return f async def handle_Initialize(ctx: wire.Context, msg: Initialize) -> Features: - if msg.state is None or msg.state != cache.get_state(prev_state=bytes(msg.state)): + if msg.session_id is None or msg.session_id != cache.get_session_id(): cache.clear() - if msg.skip_passphrase: - cache.set_passphrase("") return get_features() @@ -91,8 +92,12 @@ async def handle_Cancel(ctx: wire.Context, msg: Cancel) -> NoReturn: async def handle_ClearSession(ctx: wire.Context, msg: ClearSession) -> Success: - cache.clear(keep_passphrase=True) - return Success(message="Session cleared") + """ + This is currently a no-op on T. This should be called LockSession/LockDevice + and lock the device. In other words the cache should stay but the PIN should + be forgotten and required again. + """ + return Success() async def handle_Ping(ctx: wire.Context, msg: Ping) -> Success: @@ -102,10 +107,6 @@ async def handle_Ping(ctx: wire.Context, msg: Ping) -> Success: from trezor.ui.text import Text await require_confirm(ctx, Text("Confirm"), ProtectCall) - if msg.passphrase_protection: - from apps.common.request_passphrase import protect_by_passphrase - - await protect_by_passphrase(ctx) return Success(message=msg.message) diff --git a/core/src/apps/management/apply_settings.py b/core/src/apps/management/apply_settings.py index f72a0255b..12cff9f75 100644 --- a/core/src/apps/management/apply_settings.py +++ b/core/src/apps/management/apply_settings.py @@ -1,6 +1,6 @@ import storage.device from trezor import ui, wire -from trezor.messages import ButtonRequestType, PassphraseSourceType +from trezor.messages import ButtonRequestType from trezor.messages.Success import Success from trezor.ui.text import Text @@ -12,7 +12,7 @@ async def apply_settings(ctx, msg): msg.homescreen is None and msg.label is None and msg.use_passphrase is None - and msg.passphrase_source is None + and msg.passphrase_always_on_device is None and msg.display_rotation is None ): raise wire.ProcessError("No setting provided") @@ -28,8 +28,10 @@ async def apply_settings(ctx, msg): if msg.use_passphrase is not None: await require_confirm_change_passphrase(ctx, msg.use_passphrase) - if msg.passphrase_source is not None: - await require_confirm_change_passphrase_source(ctx, msg.passphrase_source) + if msg.passphrase_always_on_device is not None: + await require_confirm_change_passphrase_source( + ctx, msg.passphrase_always_on_device + ) if msg.display_rotation is not None: await require_confirm_change_display_rotation(ctx, msg.display_rotation) @@ -38,7 +40,7 @@ async def apply_settings(ctx, msg): label=msg.label, use_passphrase=msg.use_passphrase, homescreen=msg.homescreen, - passphrase_source=msg.passphrase_source, + passphrase_always_on_device=msg.passphrase_always_on_device, display_rotation=msg.display_rotation, ) @@ -69,16 +71,16 @@ async def require_confirm_change_passphrase(ctx, use): await require_confirm(ctx, text, ButtonRequestType.ProtectCall) -async def require_confirm_change_passphrase_source(ctx, source): - if source == PassphraseSourceType.DEVICE: - desc = "ON DEVICE" - elif source == PassphraseSourceType.HOST: - desc = "ON HOST" - else: - desc = "ASK" +async def require_confirm_change_passphrase_source( + ctx, passphrase_always_on_device: bool +): text = Text("Passphrase source", ui.ICON_CONFIG) - text.normal("Do you really want to", "change the passphrase", "source to") - text.bold("ALWAYS %s?" % desc) + if passphrase_always_on_device: + text.normal( + "Do you really want to", "enter passphrase always", "on the device?" + ) + else: + text.normal("Do you want to revoke", "the passphrase on device", "setting?") await require_confirm(ctx, text, ButtonRequestType.ProtectCall) diff --git a/core/src/apps/management/recovery_device/homescreen.py b/core/src/apps/management/recovery_device/homescreen.py index 95dc3554d..a14b9c25b 100644 --- a/core/src/apps/management/recovery_device/homescreen.py +++ b/core/src/apps/management/recovery_device/homescreen.py @@ -28,7 +28,7 @@ async def recovery_homescreen() -> None: return # recovery process does not communicate on the wire - ctx = wire.DummyContext() + ctx = wire.DUMMY_CONTEXT await recovery_process(ctx) diff --git a/core/src/apps/monero/__init__.py b/core/src/apps/monero/__init__.py index 94f08884e..1eb8f0a18 100644 --- a/core/src/apps/monero/__init__.py +++ b/core/src/apps/monero/__init__.py @@ -4,7 +4,6 @@ from trezor.messages import MessageType from apps.common import HARDENED CURVE = "ed25519" -_LIVE_REFRESH_TOKEN = None # live-refresh permission token def boot() -> None: @@ -20,11 +19,3 @@ def boot() -> None: if __debug__ and hasattr(MessageType, "DebugMoneroDiagRequest"): wire.add(MessageType.DebugMoneroDiagRequest, __name__, "diag") - - -def live_refresh_token(token: bytes = None) -> None: - global _LIVE_REFRESH_TOKEN - if token is None: - return _LIVE_REFRESH_TOKEN - else: - _LIVE_REFRESH_TOKEN = token diff --git a/core/src/apps/monero/live_refresh.py b/core/src/apps/monero/live_refresh.py index b192de52b..5e8f65c39 100644 --- a/core/src/apps/monero/live_refresh.py +++ b/core/src/apps/monero/live_refresh.py @@ -1,6 +1,6 @@ import gc -from storage.cache import get_passphrase_fprint +import storage.cache from trezor import log from trezor.messages import MessageType from trezor.messages.MoneroLiveRefreshFinalAck import MoneroLiveRefreshFinalAck @@ -10,7 +10,7 @@ from trezor.messages.MoneroLiveRefreshStepAck import MoneroLiveRefreshStepAck from trezor.messages.MoneroLiveRefreshStepRequest import MoneroLiveRefreshStepRequest from apps.common import paths -from apps.monero import CURVE, live_refresh_token, misc +from apps.monero import CURVE, misc from apps.monero.layout import confirms from apps.monero.xmr import crypto, key_image, monero from apps.monero.xmr.crypto import chacha_poly @@ -49,10 +49,9 @@ async def _init_step( ctx, misc.validate_full_path, keychain, msg.address_n, CURVE ) - passphrase_fprint = get_passphrase_fprint() - if live_refresh_token() != passphrase_fprint: + if not storage.cache.get(storage.cache.APP_MONERO_LIVE_REFRESH): await confirms.require_confirm_live_refresh(ctx) - live_refresh_token(passphrase_fprint) + storage.cache.set(storage.cache.APP_MONERO_LIVE_REFRESH, True) s.creds = misc.get_creds(keychain, msg.address_n, msg.network_type) diff --git a/core/src/storage/cache.py b/core/src/storage/cache.py index 0e4f15b0e..9411e8f23 100644 --- a/core/src/storage/cache.py +++ b/core/src/storage/cache.py @@ -1,75 +1,36 @@ -from storage.device import get_device_id -from trezor.crypto import hashlib, hmac, random +from trezor.crypto import random if False: from typing import Optional -_cached_seed = None # type: Optional[bytes] -_cached_seed_without_passphrase = None # type: Optional[bytes] -_cached_passphrase = None # type: Optional[str] -_cached_passphrase_fprint = b"\x00\x00\x00\x00" # type: bytes +APP_COMMON_SEED = 0 +APP_COMMON_SEED_WITHOUT_PASSPHRASE = 1 +APP_CARDANO_ROOT = 2 +APP_MONERO_LIVE_REFRESH = 3 +_cache_session_id = None # type: Optional[bytes] +_cache = {} -def get_state(prev_state: bytes = None, passphrase: str = None) -> Optional[bytes]: - if prev_state is None: - salt = random.bytes(32) # generate a random salt if no state provided - else: - salt = prev_state[:32] # use salt from provided state - if len(salt) != 32: - return None # invalid state - if passphrase is None: - if _cached_passphrase is None: - return None # we don't have any passphrase to compute the state - else: - passphrase = _cached_passphrase # use cached passphrase - return _compute_state(salt, passphrase) - - -def _compute_state(salt: bytes, passphrase: str) -> bytes: - # state = HMAC(passphrase, salt || device_id) - message = salt + get_device_id().encode() - state = hmac.new(passphrase.encode(), message, hashlib.sha256).digest() - return salt + state - - -def get_seed() -> Optional[bytes]: - return _cached_seed - - -def get_seed_without_passphrase() -> Optional[bytes]: - return _cached_seed_without_passphrase - - -def get_passphrase() -> Optional[str]: - return _cached_passphrase - - -def get_passphrase_fprint() -> bytes: - return _cached_passphrase_fprint - - -def has_passphrase() -> bool: - return _cached_passphrase is not None +if False: + from typing import Any -def set_seed(seed: Optional[bytes]) -> None: - global _cached_seed - _cached_seed = seed +def get_session_id() -> bytes: + global _cache_session_id + if not _cache_session_id: + _cache_session_id = random.bytes(32) + return _cache_session_id -def set_seed_without_passphrase(seed: Optional[bytes]) -> None: - global _cached_seed_without_passphrase - _cached_seed_without_passphrase = seed +def set(key: int, value: Any) -> None: + _cache[key] = value -def set_passphrase(passphrase: Optional[str]) -> None: - global _cached_passphrase, _cached_passphrase_fprint - _cached_passphrase = passphrase - _cached_passphrase_fprint = _compute_state(b"FPRINT", passphrase or "")[:4] +def get(key: int) -> Any: + return _cache.get(key) -def clear(keep_passphrase: bool = False) -> None: - set_seed(None) - set_seed_without_passphrase(None) - if not keep_passphrase: - set_passphrase(None) +def clear() -> None: + global _cache_session_id + _cache_session_id = None + _cache.clear() diff --git a/core/src/storage/device.py b/core/src/storage/device.py index 224910b20..d028a5c9a 100644 --- a/core/src/storage/device.py +++ b/core/src/storage/device.py @@ -24,7 +24,7 @@ _HOMESCREEN = const(0x06) # bytes _NEEDS_BACKUP = const(0x07) # bool (0x01 or empty) _FLAGS = const(0x08) # int U2F_COUNTER = const(0x09) # int -_PASSPHRASE_SOURCE = const(0x0A) # int +_PASSPHRASE_ALWAYS_ON_DEVICE = const(0x0A) # bool (0x01 or empty) _UNFINISHED_BACKUP = const(0x0B) # bool (0x01 or empty) _AUTOLOCK_DELAY_MS = const(0x0C) # int _NO_BACKUP = const(0x0D) # bool (0x01 or empty) @@ -101,7 +101,7 @@ def get_backup_type() -> EnumTypeBackupType: return backup_type # type: ignore -def has_passphrase() -> bool: +def is_passphrase_enabled() -> bool: return common.get_bool(_NAMESPACE, _USE_PASSPHRASE) @@ -143,21 +143,21 @@ def no_backup() -> bool: return common.get_bool(_NAMESPACE, _NO_BACKUP) -def get_passphrase_source() -> int: - b = common.get(_NAMESPACE, _PASSPHRASE_SOURCE) - if b == b"\x01": - return 1 - elif b == b"\x02": - return 2 - else: - return 0 +def get_passphrase_always_on_device() -> bool: + """ + This is backwards compatible with _PASSPHRASE_SOURCE: + - If ASK(0) => returns False, the check against b"\x01" in get_bool fails. + - If DEVICE(1) => returns True, the check against b"\x01" in get_bool succeeds. + - If HOST(2) => returns False, the check against b"\x01" in get_bool fails. + """ + return common.get_bool(_NAMESPACE, _PASSPHRASE_ALWAYS_ON_DEVICE) def load_settings( label: str = None, use_passphrase: bool = None, homescreen: bytes = None, - passphrase_source: int = None, + passphrase_always_on_device: bool = None, display_rotation: int = None, ) -> None: if label is not None: @@ -170,9 +170,10 @@ def load_settings( common.set(_NAMESPACE, _HOMESCREEN, homescreen, True) # public else: common.set(_NAMESPACE, _HOMESCREEN, b"", True) # public - if passphrase_source is not None: - if passphrase_source in (0, 1, 2): - common.set(_NAMESPACE, _PASSPHRASE_SOURCE, bytes([passphrase_source])) + if passphrase_always_on_device is not None: + common.set_bool( + _NAMESPACE, _PASSPHRASE_ALWAYS_ON_DEVICE, passphrase_always_on_device + ) if display_rotation is not None: if display_rotation not in (0, 90, 180, 270): raise ValueError( diff --git a/core/src/trezor/messages/ApplySettings.py b/core/src/trezor/messages/ApplySettings.py index 986d10591..74f06c92d 100644 --- a/core/src/trezor/messages/ApplySettings.py +++ b/core/src/trezor/messages/ApplySettings.py @@ -6,7 +6,6 @@ if __debug__: try: from typing import Dict, List # noqa: F401 from typing_extensions import Literal # noqa: F401 - EnumTypePassphraseSourceType = Literal[0, 1, 2] except ImportError: pass @@ -20,17 +19,17 @@ class ApplySettings(p.MessageType): label: str = None, use_passphrase: bool = None, homescreen: bytes = None, - passphrase_source: EnumTypePassphraseSourceType = None, auto_lock_delay_ms: int = None, display_rotation: int = None, + passphrase_always_on_device: bool = None, ) -> None: self.language = language self.label = label self.use_passphrase = use_passphrase self.homescreen = homescreen - self.passphrase_source = passphrase_source self.auto_lock_delay_ms = auto_lock_delay_ms self.display_rotation = display_rotation + self.passphrase_always_on_device = passphrase_always_on_device @classmethod def get_fields(cls) -> Dict: @@ -39,7 +38,7 @@ class ApplySettings(p.MessageType): 2: ('label', p.UnicodeType, 0), 3: ('use_passphrase', p.BoolType, 0), 4: ('homescreen', p.BytesType, 0), - 5: ('passphrase_source', p.EnumType("PassphraseSourceType", (0, 1, 2)), 0), 6: ('auto_lock_delay_ms', p.UVarintType, 0), 7: ('display_rotation', p.UVarintType, 0), + 8: ('passphrase_always_on_device', p.BoolType, 0), } diff --git a/core/src/trezor/messages/ButtonRequest.py b/core/src/trezor/messages/ButtonRequest.py index 69d1b390b..334ff5954 100644 --- a/core/src/trezor/messages/ButtonRequest.py +++ b/core/src/trezor/messages/ButtonRequest.py @@ -6,7 +6,7 @@ if __debug__: try: from typing import Dict, List # noqa: F401 from typing_extensions import Literal # noqa: F401 - EnumTypeButtonRequestType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18] + EnumTypeButtonRequestType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19] except ImportError: pass @@ -23,5 +23,5 @@ class ButtonRequest(p.MessageType): @classmethod def get_fields(cls) -> Dict: return { - 1: ('code', p.EnumType("ButtonRequestType", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18)), 0), + 1: ('code', p.EnumType("ButtonRequestType", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19)), 0), } diff --git a/core/src/trezor/messages/ButtonRequestType.py b/core/src/trezor/messages/ButtonRequestType.py index f29e98d1e..c973ce8aa 100644 --- a/core/src/trezor/messages/ButtonRequestType.py +++ b/core/src/trezor/messages/ButtonRequestType.py @@ -16,8 +16,8 @@ Address = 10 # type: Literal[10] PublicKey = 11 # type: Literal[11] MnemonicWordCount = 12 # type: Literal[12] MnemonicInput = 13 # type: Literal[13] -PassphraseType = 14 # type: Literal[14] UnknownDerivationPath = 15 # type: Literal[15] RecoveryHomepage = 16 # type: Literal[16] Success = 17 # type: Literal[17] Warning = 18 # type: Literal[18] +PassphraseEntry = 19 # type: Literal[19] diff --git a/core/src/trezor/messages/Capability.py b/core/src/trezor/messages/Capability.py index ae1c5abb5..848c2aafe 100644 --- a/core/src/trezor/messages/Capability.py +++ b/core/src/trezor/messages/Capability.py @@ -23,3 +23,4 @@ if not utils.BITCOIN_ONLY: U2F = 14 # type: Literal[14] Shamir = 15 # type: Literal[15] ShamirGroups = 16 # type: Literal[16] +PassphraseEntry = 17 # type: Literal[17] diff --git a/core/src/trezor/messages/PassphraseStateAck.py b/core/src/trezor/messages/Deprecated_PassphraseStateAck.py similarity index 83% rename from core/src/trezor/messages/PassphraseStateAck.py rename to core/src/trezor/messages/Deprecated_PassphraseStateAck.py index 3846fc5f8..5bd97159e 100644 --- a/core/src/trezor/messages/PassphraseStateAck.py +++ b/core/src/trezor/messages/Deprecated_PassphraseStateAck.py @@ -10,5 +10,5 @@ if __debug__: pass -class PassphraseStateAck(p.MessageType): +class Deprecated_PassphraseStateAck(p.MessageType): MESSAGE_WIRE_TYPE = 78 diff --git a/core/src/trezor/messages/PassphraseStateRequest.py b/core/src/trezor/messages/Deprecated_PassphraseStateRequest.py similarity index 89% rename from core/src/trezor/messages/PassphraseStateRequest.py rename to core/src/trezor/messages/Deprecated_PassphraseStateRequest.py index 5dbac3b33..3699eab13 100644 --- a/core/src/trezor/messages/PassphraseStateRequest.py +++ b/core/src/trezor/messages/Deprecated_PassphraseStateRequest.py @@ -10,7 +10,7 @@ if __debug__: pass -class PassphraseStateRequest(p.MessageType): +class Deprecated_PassphraseStateRequest(p.MessageType): MESSAGE_WIRE_TYPE = 77 def __init__( diff --git a/core/src/trezor/messages/Features.py b/core/src/trezor/messages/Features.py index 17586cc24..ce5462e2c 100644 --- a/core/src/trezor/messages/Features.py +++ b/core/src/trezor/messages/Features.py @@ -6,7 +6,7 @@ if __debug__: try: from typing import Dict, List # noqa: F401 from typing_extensions import Literal # noqa: F401 - EnumTypeCapability = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + EnumTypeCapability = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17] EnumTypeBackupType = Literal[0, 1, 2] except ImportError: pass @@ -32,7 +32,6 @@ class Features(p.MessageType): bootloader_hash: bytes = None, imported: bool = None, pin_cached: bool = None, - passphrase_cached: bool = None, firmware_present: bool = None, needs_backup: bool = None, flags: int = None, @@ -50,6 +49,8 @@ class Features(p.MessageType): sd_card_present: bool = None, sd_protection: bool = None, wipe_code_protection: bool = None, + session_id: bytes = None, + passphrase_always_on_device: bool = None, ) -> None: self.vendor = vendor self.major_version = major_version @@ -66,7 +67,6 @@ class Features(p.MessageType): self.bootloader_hash = bootloader_hash self.imported = imported self.pin_cached = pin_cached - self.passphrase_cached = passphrase_cached self.firmware_present = firmware_present self.needs_backup = needs_backup self.flags = flags @@ -84,6 +84,8 @@ class Features(p.MessageType): self.sd_card_present = sd_card_present self.sd_protection = sd_protection self.wipe_code_protection = wipe_code_protection + self.session_id = session_id + self.passphrase_always_on_device = passphrase_always_on_device @classmethod def get_fields(cls) -> Dict: @@ -103,7 +105,6 @@ class Features(p.MessageType): 14: ('bootloader_hash', p.BytesType, 0), 15: ('imported', p.BoolType, 0), 16: ('pin_cached', p.BoolType, 0), - 17: ('passphrase_cached', p.BoolType, 0), 18: ('firmware_present', p.BoolType, 0), 19: ('needs_backup', p.BoolType, 0), 20: ('flags', p.UVarintType, 0), @@ -116,9 +117,11 @@ class Features(p.MessageType): 27: ('unfinished_backup', p.BoolType, 0), 28: ('no_backup', p.BoolType, 0), 29: ('recovery_mode', p.BoolType, 0), - 30: ('capabilities', p.EnumType("Capability", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)), p.FLAG_REPEATED), + 30: ('capabilities', p.EnumType("Capability", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)), p.FLAG_REPEATED), 31: ('backup_type', p.EnumType("BackupType", (0, 1, 2)), 0), 32: ('sd_card_present', p.BoolType, 0), 33: ('sd_protection', p.BoolType, 0), 34: ('wipe_code_protection', p.BoolType, 0), + 35: ('session_id', p.BytesType, 0), + 36: ('passphrase_always_on_device', p.BoolType, 0), } diff --git a/core/src/trezor/messages/Initialize.py b/core/src/trezor/messages/Initialize.py index 3e4176999..73dd3fdc9 100644 --- a/core/src/trezor/messages/Initialize.py +++ b/core/src/trezor/messages/Initialize.py @@ -15,15 +15,12 @@ class Initialize(p.MessageType): def __init__( self, - state: bytes = None, - skip_passphrase: bool = None, + session_id: bytes = None, ) -> None: - self.state = state - self.skip_passphrase = skip_passphrase + self.session_id = session_id @classmethod def get_fields(cls) -> Dict: return { - 1: ('state', p.BytesType, 0), - 2: ('skip_passphrase', p.BoolType, 0), + 1: ('session_id', p.BytesType, 0), } diff --git a/core/src/trezor/messages/MessageType.py b/core/src/trezor/messages/MessageType.py index d2a40123a..4f6497015 100644 --- a/core/src/trezor/messages/MessageType.py +++ b/core/src/trezor/messages/MessageType.py @@ -29,8 +29,6 @@ EntropyRequest = 35 # type: Literal[35] EntropyAck = 36 # type: Literal[36] PassphraseRequest = 41 # type: Literal[41] PassphraseAck = 42 # type: Literal[42] -PassphraseStateRequest = 77 # type: Literal[77] -PassphraseStateAck = 78 # type: Literal[78] RecoveryDevice = 45 # type: Literal[45] WordRequest = 46 # type: Literal[46] WordAck = 47 # type: Literal[47] @@ -40,6 +38,8 @@ SdProtect = 79 # type: Literal[79] GetNextU2FCounter = 80 # type: Literal[80] NextU2FCounter = 81 # type: Literal[81] ChangeWipeCode = 82 # type: Literal[82] +Deprecated_PassphraseStateRequest = 77 # type: Literal[77] +Deprecated_PassphraseStateAck = 78 # type: Literal[78] FirmwareErase = 6 # type: Literal[6] FirmwareUpload = 7 # type: Literal[7] FirmwareRequest = 8 # type: Literal[8] diff --git a/core/src/trezor/messages/PassphraseAck.py b/core/src/trezor/messages/PassphraseAck.py index 3e965b0e2..84d134c93 100644 --- a/core/src/trezor/messages/PassphraseAck.py +++ b/core/src/trezor/messages/PassphraseAck.py @@ -16,14 +16,17 @@ class PassphraseAck(p.MessageType): def __init__( self, passphrase: str = None, - state: bytes = None, + _state: bytes = None, + on_device: bool = None, ) -> None: self.passphrase = passphrase - self.state = state + self._state = _state + self.on_device = on_device @classmethod def get_fields(cls) -> Dict: return { 1: ('passphrase', p.UnicodeType, 0), - 2: ('state', p.BytesType, 0), + 2: ('_state', p.BytesType, 0), + 3: ('on_device', p.BoolType, 0), } diff --git a/core/src/trezor/messages/PassphraseRequest.py b/core/src/trezor/messages/PassphraseRequest.py index 21395653a..a2d28848c 100644 --- a/core/src/trezor/messages/PassphraseRequest.py +++ b/core/src/trezor/messages/PassphraseRequest.py @@ -15,12 +15,12 @@ class PassphraseRequest(p.MessageType): def __init__( self, - on_device: bool = None, + _on_device: bool = None, ) -> None: - self.on_device = on_device + self._on_device = _on_device @classmethod def get_fields(cls) -> Dict: return { - 1: ('on_device', p.BoolType, 0), + 1: ('_on_device', p.BoolType, 0), } diff --git a/core/src/trezor/messages/PassphraseSourceType.py b/core/src/trezor/messages/PassphraseSourceType.py deleted file mode 100644 index d62ea047d..000000000 --- a/core/src/trezor/messages/PassphraseSourceType.py +++ /dev/null @@ -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] diff --git a/core/src/trezor/messages/Ping.py b/core/src/trezor/messages/Ping.py index 3a7134b71..9aff85c6c 100644 --- a/core/src/trezor/messages/Ping.py +++ b/core/src/trezor/messages/Ping.py @@ -17,19 +17,13 @@ class Ping(p.MessageType): self, message: str = None, button_protection: bool = None, - pin_protection: bool = None, - passphrase_protection: bool = None, ) -> None: self.message = message self.button_protection = button_protection - self.pin_protection = pin_protection - self.passphrase_protection = passphrase_protection @classmethod def get_fields(cls) -> Dict: return { 1: ('message', p.UnicodeType, 0), 2: ('button_protection', p.BoolType, 0), - 3: ('pin_protection', p.BoolType, 0), - 4: ('passphrase_protection', p.BoolType, 0), } diff --git a/core/src/trezor/ui/__init__.py b/core/src/trezor/ui/__init__.py index 7eefd76ad..256e7a534 100644 --- a/core/src/trezor/ui/__init__.py +++ b/core/src/trezor/ui/__init__.py @@ -160,6 +160,14 @@ def header_error(message: str, clear: bool = True) -> None: display.bar(0, 30, WIDTH, HEIGHT - 30, style.BG) +def draw_simple(t: Component) -> None: # noqa: F405 + backlight_fade(style.BACKLIGHT_DIM) + display.clear() + t.on_render() + refresh() + backlight_fade(style.BACKLIGHT_NORMAL) + + def grid( i: int, # i-th cell of the table of which we wish to return Area (snake-like starting with 0) n_x: int = 3, # number of rows in the table diff --git a/core/src/trezor/ui/passphrase.py b/core/src/trezor/ui/passphrase.py index 7dc66e637..c2713fce1 100644 --- a/core/src/trezor/ui/passphrase.py +++ b/core/src/trezor/ui/passphrase.py @@ -1,7 +1,6 @@ from micropython import const from trezor import io, loop, res, ui -from trezor.messages import PassphraseSourceType from trezor.ui import display from trezor.ui.button import Button, ButtonClear, ButtonConfirm from trezor.ui.swipe import SWIPE_HORIZONTAL, SWIPE_LEFT, Swipe @@ -246,25 +245,3 @@ class PassphraseKeyboard(ui.Layout): def create_tasks(self) -> Tuple[loop.Task, ...]: return self.handle_input(), self.handle_rendering(), self.handle_paging() - - -class PassphraseSource(ui.Layout): - def __init__(self, content: ui.Component) -> None: - self.content = content - - self.device = Button(ui.grid(8, n_y=4, n_x=4, cells_x=4), "Device") - self.device.on_click = self.on_device # type: ignore - - self.host = Button(ui.grid(12, n_y=4, n_x=4, cells_x=4), "Host") - self.host.on_click = self.on_host # type: ignore - - def dispatch(self, event: int, x: int, y: int) -> None: - self.content.dispatch(event, x, y) - self.device.dispatch(event, x, y) - self.host.dispatch(event, x, y) - - def on_device(self) -> None: - raise ui.Result(PassphraseSourceType.DEVICE) - - def on_host(self) -> None: - raise ui.Result(PassphraseSourceType.HOST) diff --git a/core/tests/common.py b/core/tests/common.py index 1e699e382..fce79b33c 100644 --- a/core/tests/common.py +++ b/core/tests/common.py @@ -1,9 +1,23 @@ import sys -sys.path.append('../src') +sys.path.append("../src") from ubinascii import hexlify, unhexlify # noqa: F401 import unittest # noqa: F401 from trezor import utils # noqa: F401 + + +def await_result(task: Awaitable) -> Any: + value = None + while True: + try: + result = task.send(value) + except StopIteration as e: + return e.value + + if result: + value = await_result(result) + else: + value = None diff --git a/core/tests/test_protobuf.py b/core/tests/test_protobuf.py index d362401e5..c6de12331 100644 --- a/core/tests/test_protobuf.py +++ b/core/tests/test_protobuf.py @@ -44,28 +44,14 @@ class ByteArrayWriter: return len(buf) -def run_until_complete(task: Awaitable) -> Any: - value = None - while True: - try: - result = task.send(value) - except StopIteration as e: - return e.value - - if result: - value = run_until_complete(result) - else: - value = None - - def load_uvarint(data: bytes) -> int: reader = ByteReader(data) - return run_until_complete(protobuf.load_uvarint(reader)) + return await_result(protobuf.load_uvarint(reader)) def dump_uvarint(value: int) -> bytearray: writer = ByteArrayWriter() - run_until_complete(protobuf.dump_uvarint(writer, value)) + await_result(protobuf.dump_uvarint(writer, value)) return writer.buf @@ -106,9 +92,9 @@ class TestProtobuf(unittest.TestCase): # ok message: msg = Message(-42, 5) writer = ByteArrayWriter() - run_until_complete(protobuf.dump_message(writer, msg)) + await_result(protobuf.dump_message(writer, msg)) reader = ByteReader(bytes(writer.buf)) - nmsg = run_until_complete(protobuf.load_message(reader, Message)) + nmsg = await_result(protobuf.load_message(reader, Message)) self.assertEqual(msg.sint_field, nmsg.sint_field) self.assertEqual(msg.enum_field, nmsg.enum_field) @@ -116,10 +102,10 @@ class TestProtobuf(unittest.TestCase): # bad enum value: msg = Message(-42, 42) writer = ByteArrayWriter() - run_until_complete(protobuf.dump_message(writer, msg)) + await_result(protobuf.dump_message(writer, msg)) reader = ByteReader(bytes(writer.buf)) with self.assertRaises(TypeError): - run_until_complete(protobuf.load_message(reader, Message)) + await_result(protobuf.load_message(reader, Message)) if __name__ == "__main__": diff --git a/core/tests/test_storage.cache.py b/core/tests/test_storage.cache.py new file mode 100644 index 000000000..6a290c1c9 --- /dev/null +++ b/core/tests/test_storage.cache.py @@ -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() diff --git a/docs/common/communication.md b/docs/common/communication.md deleted file mode 100644 index c93505917..000000000 --- a/docs/common/communication.md +++ /dev/null @@ -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`. diff --git a/docs/common/communication/index.md b/docs/common/communication/index.md new file mode 100644 index 000000000..4eb8a2880 --- /dev/null +++ b/docs/common/communication/index.md @@ -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. diff --git a/docs/common/communication/passphrase.md b/docs/common/communication/passphrase.md new file mode 100644 index 000000000..064f3f516 --- /dev/null +++ b/docs/common/communication/passphrase.md @@ -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. diff --git a/docs/common/communication/sessions.md b/docs/common/communication/sessions.md new file mode 100644 index 000000000..963f8b472 --- /dev/null +++ b/docs/common/communication/sessions.md @@ -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. diff --git a/legacy/firmware/ChangeLog b/legacy/firmware/ChangeLog index f70d06420..f76f4cc3e 100644 --- a/legacy/firmware/ChangeLog +++ b/legacy/firmware/ChangeLog @@ -1,8 +1,9 @@ -Version 1.x.x [not yet released] +Version 1.9.0 [not yet released] * Disallow changing of settings via dry-run recovery * Wipe code * Make LoadDevice debug only and drop its XPRV feature * Add feature to retrieve the next U2F counter +* Passphrase redesign Version 1.8.3 [Sep 2019] * Small code improvements diff --git a/legacy/firmware/config.c b/legacy/firmware/config.c index 485ac8616..daba0e222 100644 --- a/legacy/firmware/config.c +++ b/legacy/firmware/config.c @@ -31,6 +31,7 @@ #include "config.h" #include "curves.h" #include "debug.h" +#include "fsm.h" #include "gettext.h" #include "hmac.h" #include "layout2.h" @@ -119,11 +120,11 @@ be added to the storage u2f_counter to get the real counter value. * storage.u2f_counter + config_u2f_offset. * This corresponds to the number of cleared bits in the U2FAREA. */ -static secbool sessionSeedCached, sessionSeedUsesPassphrase; +static secbool sessionSeedCached; static uint8_t CONFIDENTIAL sessionSeed[64]; -static secbool sessionPassphraseCached = secfalse; -static char CONFIDENTIAL sessionPassphrase[51]; +static secbool sessionIdCached; +static uint8_t sessionId[32]; #define autoLockDelayMsDefault (10 * 60 * 1000U) // 10 minutes static secbool autoLockDelayMsCached = secfalse; @@ -407,8 +408,8 @@ void config_init(void) { void session_clear(bool lock) { sessionSeedCached = secfalse; memzero(&sessionSeed, sizeof(sessionSeed)); - sessionPassphraseCached = secfalse; - memzero(&sessionPassphrase, sizeof(sessionPassphrase)); + sessionIdCached = secfalse; + memzero(&sessionId, sizeof(sessionId)); if (lock) { storage_lock(); } @@ -527,8 +528,6 @@ void config_setLanguage(const char *lang) { } void config_setPassphraseProtection(bool passphrase_protection) { - sessionSeedCached = secfalse; - sessionPassphraseCached = secfalse; config_set_bool(KEY_PASSPHRASE_PROTECTION, passphrase_protection); } @@ -550,18 +549,19 @@ static void get_root_node_callback(uint32_t iter, uint32_t total) { layoutProgress(_("Waking up"), 1000 * iter / total); } -const uint8_t *config_getSeed(bool usePassphrase) { +const uint8_t *config_getSeed(void) { // root node is properly cached - if (usePassphrase == (sectrue == sessionSeedUsesPassphrase) && - sectrue == sessionSeedCached) { + if (sectrue == sessionSeedCached) { return sessionSeed; } // if storage has mnemonic, convert it to node and use it char mnemonic[MAX_MNEMONIC_LEN + 1] = {0}; if (config_getMnemonic(mnemonic, sizeof(mnemonic))) { - if (usePassphrase && !protectPassphrase()) { + char passphrase[MAX_PASSPHRASE_LEN + 1] = {0}; + if (!protectPassphrase(passphrase)) { memzero(mnemonic, sizeof(mnemonic)); + memzero(passphrase, sizeof(passphrase)); return NULL; } // if storage was not imported (i.e. it was properly generated or recovered) @@ -575,13 +575,16 @@ const uint8_t *config_getSeed(bool usePassphrase) { } } char oldTiny = usbTiny(1); - mnemonic_to_seed(mnemonic, usePassphrase ? sessionPassphrase : "", - sessionSeed, get_root_node_callback); // BIP-0039 + mnemonic_to_seed(mnemonic, passphrase, sessionSeed, + get_root_node_callback); // BIP-0039 memzero(mnemonic, sizeof(mnemonic)); + memzero(passphrase, sizeof(passphrase)); usbTiny(oldTiny); sessionSeedCached = sectrue; - sessionSeedUsesPassphrase = usePassphrase ? sectrue : secfalse; return sessionSeed; + } else { + fsm_sendFailure(FailureType_Failure_NotInitialized, + _("Device not initialized")); } return NULL; @@ -606,58 +609,16 @@ bool config_getU2FRoot(HDNode *node) { return ret; } -bool config_getRootNode(HDNode *node, const char *curve, bool usePassphrase) { - // if storage has node, decrypt and use it - StorageHDNode storageHDNode = {0}; - uint16_t len = 0; - if (strcmp(curve, SECP256K1_NAME) == 0 && - sectrue == - storage_get(KEY_NODE, &storageHDNode, sizeof(storageHDNode), &len) && - len == sizeof(StorageHDNode)) { - if (!protectPassphrase()) { - memzero(&storageHDNode, sizeof(storageHDNode)); - return false; - } - if (!config_loadNode(&storageHDNode, curve, node)) { - memzero(&storageHDNode, sizeof(storageHDNode)); - return false; - } - bool passphrase_protection = false; - config_getPassphraseProtection(&passphrase_protection); - if (passphrase_protection && sectrue == sessionPassphraseCached && - sessionPassphrase[0] != '\0') { - // decrypt hd node - uint8_t secret[64] = {0}; - PBKDF2_HMAC_SHA512_CTX pctx = {0}; - char oldTiny = usbTiny(1); - pbkdf2_hmac_sha512_Init(&pctx, (const uint8_t *)sessionPassphrase, - strlen(sessionPassphrase), - (const uint8_t *)"TREZORHD", 8, 1); - get_root_node_callback(0, BIP39_PBKDF2_ROUNDS); - for (int i = 0; i < 8; i++) { - pbkdf2_hmac_sha512_Update(&pctx, BIP39_PBKDF2_ROUNDS / 8); - get_root_node_callback((i + 1) * BIP39_PBKDF2_ROUNDS / 8, - BIP39_PBKDF2_ROUNDS); - } - pbkdf2_hmac_sha512_Final(&pctx, secret); - usbTiny(oldTiny); - aes_decrypt_ctx ctx = {0}; - aes_decrypt_key256(secret, &ctx); - aes_cbc_decrypt(node->chain_code, node->chain_code, 32, secret + 32, - &ctx); - aes_cbc_decrypt(node->private_key, node->private_key, 32, secret + 32, - &ctx); - } - return true; - } - memzero(&storageHDNode, sizeof(storageHDNode)); - - const uint8_t *seed = config_getSeed(usePassphrase); +bool config_getRootNode(HDNode *node, const char *curve) { + const uint8_t *seed = config_getSeed(); if (seed == NULL) { return false; } - - return hdnode_from_seed(seed, 64, curve, node); + int result = hdnode_from_seed(seed, 64, curve, node); + if (result == 0) { + fsm_sendFailure(FailureType_Failure_NotInitialized, _("Unsupported curve")); + } + return result; } bool config_getLabel(char *dest, uint16_t dest_size) { @@ -811,40 +772,12 @@ bool config_changeWipeCode(const char *pin, const char *wipe_code) { return sectrue == ret; } -void session_cachePassphrase(const char *passphrase) { - strlcpy(sessionPassphrase, passphrase, sizeof(sessionPassphrase)); - sessionPassphraseCached = sectrue; -} - -bool session_isPassphraseCached(void) { - return sectrue == sessionPassphraseCached; -} - -bool session_getState(const uint8_t *salt, uint8_t *state, - const char *passphrase) { - if (!passphrase && sectrue != sessionPassphraseCached) { - return false; - } else { - passphrase = sessionPassphrase; +const uint8_t *session_getSessionId(void) { + if (!sessionIdCached) { + random_buffer(sessionId, 32); } - if (!salt) { - // if salt is not provided fill the first half of the state with random data - random_buffer(state, 32); - } else { - // if salt is provided fill the first half of the state with salt - memcpy(state, salt, 32); - } - // state[0:32] = salt - // state[32:64] = HMAC(passphrase, salt || device_id) - HMAC_SHA256_CTX ctx = {0}; - hmac_sha256_Init(&ctx, (const uint8_t *)passphrase, strlen(passphrase)); - hmac_sha256_Update(&ctx, state, 32); - hmac_sha256_Update(&ctx, (const uint8_t *)config_uuid, sizeof(config_uuid)); - hmac_sha256_Final(&ctx, state + 32); - - memzero(&ctx, sizeof(ctx)); - - return true; + sessionIdCached = sectrue; + return sessionId; } bool session_isUnlocked(void) { return sectrue == storage_is_unlocked(); } diff --git a/legacy/firmware/config.h b/legacy/firmware/config.h index d061dea71..41464cab1 100644 --- a/legacy/firmware/config.h +++ b/legacy/firmware/config.h @@ -91,10 +91,10 @@ void session_clear(bool lock); void config_loadDevice(const LoadDevice *msg); -const uint8_t *config_getSeed(bool usePassphrase); +const uint8_t *config_getSeed(void); bool config_getU2FRoot(HDNode *node); -bool config_getRootNode(HDNode *node, const char *curve, bool usePassphrase); +bool config_getRootNode(HDNode *node, const char *curve); bool config_getLabel(char *dest, uint16_t dest_size); void config_setLabel(const char *label); @@ -108,10 +108,7 @@ bool config_getPassphraseProtection(bool *passphrase_protection); bool config_getHomescreen(uint8_t *dest, uint16_t dest_size); void config_setHomescreen(const uint8_t *data, uint32_t size); -void session_cachePassphrase(const char *passphrase); -bool session_isPassphraseCached(void); -bool session_getState(const uint8_t *salt, uint8_t *state, - const char *passphrase); +const uint8_t *session_getSessionId(void); bool config_setMnemonic(const char *mnemonic); bool config_containsMnemonic(const char *mnemonic); diff --git a/legacy/firmware/fsm.c b/legacy/firmware/fsm.c index 4c82442b3..efc4a88ef 100644 --- a/legacy/firmware/fsm.c +++ b/legacy/firmware/fsm.c @@ -212,10 +212,7 @@ static HDNode *fsm_getDerivedNode(const char *curve, const uint32_t *address_n, if (fingerprint) { *fingerprint = 0; } - if (!config_getRootNode(&node, curve, true)) { - fsm_sendFailure(FailureType_Failure_NotInitialized, - _("Device not initialized or passphrase request cancelled " - "or unsupported curve")); + if (!config_getRootNode(&node, curve)) { layoutHome(); return 0; } diff --git a/legacy/firmware/fsm_msg_common.h b/legacy/firmware/fsm_msg_common.h index 9de5b2ec4..a8834430e 100644 --- a/legacy/firmware/fsm_msg_common.h +++ b/legacy/firmware/fsm_msg_common.h @@ -20,17 +20,14 @@ void fsm_msgInitialize(const Initialize *msg) { recovery_abort(); signing_abort(); - if (msg && msg->has_state && msg->state.size == 64) { - uint8_t i_state[64]; - if (!session_getState(msg->state.bytes, i_state, NULL)) { - session_clear(false); // do not clear PIN - } else { - if (0 != memcmp(msg->state.bytes, i_state, 64)) { - session_clear(false); // do not clear PIN - } + if (msg && msg->has_session_id && msg->session_id.size == 32) { + if (0 != memcmp(session_getSessionId(), msg->session_id.bytes, 32)) { + // If session id was specified but does not match -> clear the cache. + session_clear(false); // do not lock } } else { - session_clear(false); // do not clear PIN + // If session id was not specified -> clear the cache. + session_clear(false); // do not lock } layoutHome(); fsm_msgGetFeatures(0); @@ -39,6 +36,12 @@ void fsm_msgInitialize(const Initialize *msg) { void fsm_msgGetFeatures(const GetFeatures *msg) { (void)msg; RESP_INIT(Features); + + resp->has_session_id = true; + memcpy(resp->session_id.bytes, session_getSessionId(), + sizeof(resp->session_id.bytes)); + resp->session_id.size = sizeof(resp->session_id.bytes); + resp->has_vendor = true; strlcpy(resp->vendor, "trezor.io", sizeof(resp->vendor)); resp->has_major_version = true; @@ -71,8 +74,6 @@ void fsm_msgGetFeatures(const GetFeatures *msg) { resp->has_imported = config_getImported(&(resp->imported)); resp->has_pin_cached = true; resp->pin_cached = session_isUnlocked() && config_hasPin(); - resp->has_passphrase_cached = true; - resp->passphrase_cached = session_isPassphraseCached(); resp->has_needs_backup = true; config_getNeedsBackup(&(resp->needs_backup)); resp->has_unfinished_backup = true; @@ -120,17 +121,6 @@ void fsm_msgPing(const Ping *msg) { } } - if (msg->has_pin_protection && msg->pin_protection) { - CHECK_PIN - } - - if (msg->has_passphrase_protection && msg->passphrase_protection) { - if (!protectPassphrase()) { - fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); - return; - } - } - if (msg->has_message) { resp->has_message = true; memcpy(&(resp->message), &(msg->message), sizeof(resp->message)); @@ -359,6 +349,10 @@ void fsm_msgClearSession(const ClearSession *msg) { } void fsm_msgApplySettings(const ApplySettings *msg) { + CHECK_PARAM( + !msg->has_passphrase_always_on_device, + _("This firmware is incapable of passphrase entry on the device.")); + CHECK_PARAM(msg->has_label || msg->has_language || msg->has_use_passphrase || msg->has_homescreen || msg->has_auto_lock_delay_ms, _("No setting provided")); diff --git a/legacy/firmware/protect.c b/legacy/firmware/protect.c index 2b77b291d..5169e3324 100644 --- a/legacy/firmware/protect.c +++ b/legacy/firmware/protect.c @@ -350,10 +350,12 @@ bool protectChangeWipeCode(bool removal) { return ret; } -bool protectPassphrase(void) { +bool protectPassphrase(char *passphrase) { + memzero(passphrase, MAX_PASSPHRASE_LEN + 1); bool passphrase_protection = false; config_getPassphraseProtection(&passphrase_protection); - if (!passphrase_protection || session_isPassphraseCached()) { + if (!passphrase_protection) { + // passphrase already set to empty by memzero above return true; } @@ -369,12 +371,24 @@ bool protectPassphrase(void) { bool result; for (;;) { usbPoll(); - // TODO: correctly process PassphraseAck with state field set (mismatch => - // Failure) if (msg_tiny_id == MessageType_MessageType_PassphraseAck) { msg_tiny_id = 0xFFFF; PassphraseAck *ppa = (PassphraseAck *)msg_tiny; - session_cachePassphrase(ppa->has_passphrase ? ppa->passphrase : ""); + if (ppa->has_on_device && ppa->on_device == true) { + fsm_sendFailure( + FailureType_Failure_DataError, + _("This firmware is incapable of passphrase entry on the device.")); + result = false; + break; + } + if (!ppa->has_passphrase) { + fsm_sendFailure(FailureType_Failure_DataError, + _("No passphrase provided. Use empty string to set an " + "empty passphrase.")); + result = false; + break; + } + strlcpy(passphrase, ppa->passphrase, sizeof(ppa->passphrase)); result = true; break; } @@ -383,8 +397,9 @@ bool protectPassphrase(void) { protectAbortedByInitialize = (msg_tiny_id == MessageType_MessageType_Initialize); if (protectAbortedByCancel || protectAbortedByInitialize) { - msg_tiny_id = 0xFFFF; + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); result = false; + msg_tiny_id = 0xFFFF; break; } } diff --git a/legacy/firmware/protect.h b/legacy/firmware/protect.h index 4d9af7c6e..f94742302 100644 --- a/legacy/firmware/protect.h +++ b/legacy/firmware/protect.h @@ -24,13 +24,15 @@ #include "messages-common.pb.h" #include "secbool.h" +#define MAX_PASSPHRASE_LEN 50 + bool protectButton(ButtonRequestType type, bool confirm_only); secbool protectPinUiCallback(uint32_t wait, uint32_t progress, const char* message); bool protectPin(bool use_cached); bool protectChangePin(bool removal); bool protectChangeWipeCode(bool removal); -bool protectPassphrase(void); +bool protectPassphrase(char* passphrase); extern bool protectAbortedByCancel; extern bool protectAbortedByInitialize; diff --git a/legacy/firmware/protob/messages-common.options b/legacy/firmware/protob/messages-common.options index cbb39f1cf..6060e696b 100644 --- a/legacy/firmware/protob/messages-common.options +++ b/legacy/firmware/protob/messages-common.options @@ -5,9 +5,6 @@ Failure.message max_size:256 PinMatrixAck.pin max_size:10 PassphraseAck.passphrase max_size:51 -PassphraseAck.state max_size:64 - -PassphraseStateRequest.state max_size:64 HDNodeType.chain_code max_size:32 HDNodeType.private_key max_size:32 diff --git a/legacy/firmware/protob/messages-management.options b/legacy/firmware/protob/messages-management.options index bd7971d7c..c77537736 100644 --- a/legacy/firmware/protob/messages-management.options +++ b/legacy/firmware/protob/messages-management.options @@ -1,4 +1,4 @@ -Initialize.state max_size:64 +Initialize.session_id max_size:32 Features.vendor max_size:33 Features.device_id max_size:25 @@ -10,6 +10,7 @@ Features.model max_size:17 Features.fw_vendor max_size:256 Features.fw_vendor_keys max_size:32 Features.capabilities max_count:32 +Features.session_id max_size:32 ApplySettings.language max_size:17 ApplySettings.label max_size:33 diff --git a/legacy/firmware/protob/messages_map.py b/legacy/firmware/protob/messages_map.py index f82e17b10..231ee9d47 100755 --- a/legacy/firmware/protob/messages_map.py +++ b/legacy/firmware/protob/messages_map.py @@ -109,6 +109,8 @@ fl.write( messages = defaultdict(list) for message in MessageType.DESCRIPTOR.values: + if message.GetOptions().deprecated: + continue extension = get_wire_extension(message) messages[extension].append(message) diff --git a/legacy/firmware/stellar.c b/legacy/firmware/stellar.c index ae7498204..e7b081ea4 100644 --- a/legacy/firmware/stellar.c +++ b/legacy/firmware/stellar.c @@ -1460,7 +1460,7 @@ const HDNode *stellar_deriveNode(const uint32_t *address_n, const char *curve = "ed25519"; // Device not initialized, passphrase request cancelled, or unsupported curve - if (!config_getRootNode(&node, curve, true)) { + if (!config_getRootNode(&node, curve)) { return 0; } // Failed to derive private key diff --git a/legacy/firmware/version.h b/legacy/firmware/version.h index 8e9b78709..ead46c13e 100644 --- a/legacy/firmware/version.h +++ b/legacy/firmware/version.h @@ -1,7 +1,7 @@ #define VERSION_MAJOR 1 -#define VERSION_MINOR 8 -#define VERSION_PATCH 4 +#define VERSION_MINOR 9 +#define VERSION_PATCH 0 #define FIX_VERSION_MAJOR 1 -#define FIX_VERSION_MINOR 8 +#define FIX_VERSION_MINOR 9 #define FIX_VERSION_PATCH 0 diff --git a/python/CHANGELOG.md b/python/CHANGELOG.md index 4ba30f8a0..82ce983de 100644 --- a/python/CHANGELOG.md +++ b/python/CHANGELOG.md @@ -6,13 +6,38 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). _At the moment, the project does **not** adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). That is expected to change with version 1.0._ -## [0.11.7] - Unreleased +## [0.12.0] - Unreleased ### Added +- support for firmwares 1.9.0 and 2.3.0 +- Model T now defaults to entering passphrase on device. New trezorctl option `-P` + enforces entering passphrase on host. +- support for "passphrase always on device" mode on model T +- new trezorctl command `get-session` and option `-s` allows entering passphrase once + for multiple subsequent trezorctl operations - built-in functionality of UdpTransport to wait until an emulator comes up, and the related command `trezorctl wait-for-emulator` +### Changed + +- API for `cosi` module was streamlined: `verify_m_of_n` is now `verify`, the old + `verify` is `verify_combined` +- internals of firmware parsing were reworked to support signing firmware headers +- `get_default_client` respects `TREZOR_PATH` environment variable +- UI callback `get_passphrase` has an additional argument `available_on_device`, + indicating that the connected Trezor is capable of on-device entry + +### Fixed + +- trezorctl does not print empty line when there is no output + +### Removed + +- `btc.sign_tx` will not preload transaction data from `prev_txes`, as usage with TxApi + is being phased out +- PIN protection and passphrase protection for `ping()` command was removed + ## [0.11.6] - 2019-12-30 [0.11.6]: https://github.com/trezor/trezor-firmware/compare/python/v0.11.5...python/v0.11.6 diff --git a/python/src/trezorlib/__init__.py b/python/src/trezorlib/__init__.py index d26316a2b..ceb5048e8 100644 --- a/python/src/trezorlib/__init__.py +++ b/python/src/trezorlib/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.11.6" +__version__ = "0.12.0" # fmt: off MINIMUM_FIRMWARE_VERSION = { diff --git a/python/src/trezorlib/cli/settings.py b/python/src/trezorlib/cli/settings.py index 2bf72cc38..404711275 100644 --- a/python/src/trezorlib/cli/settings.py +++ b/python/src/trezorlib/cli/settings.py @@ -16,15 +16,9 @@ import click -from .. import device, messages +from .. import device from . import ChoiceType -PASSPHRASE_SOURCE = { - "ask": messages.PassphraseSourceType.ASK, - "device": messages.PassphraseSourceType.DEVICE, - "host": messages.PassphraseSourceType.HOST, -} - ROTATION = {"north": 0, "east": 90, "south": 180, "west": 270} @@ -145,10 +139,13 @@ def passphrase(): @passphrase.command(name="enabled") +@click.option("-f/-F", "--force-on-device/--no-force-on-device", default=None) @click.pass_obj -def passphrase_enable(connect): +def passphrase_enable(connect, force_on_device: bool): """Enable passphrase.""" - return device.apply_settings(connect(), use_passphrase=True) + return device.apply_settings( + connect(), use_passphrase=True, passphrase_always_on_device=force_on_device + ) @passphrase.command(name="disabled") @@ -156,19 +153,3 @@ def passphrase_enable(connect): def passphrase_disable(connect): """Disable passphrase.""" return device.apply_settings(connect(), use_passphrase=False) - - -@passphrase.command(name="source") -@click.argument("source", type=ChoiceType(PASSPHRASE_SOURCE)) -@click.pass_obj -def passphrase_source(connect, source): - """Set passphrase source. - - Configure how to enter passphrase on Trezor Model T. The options are: - - \b - ask - always ask where to enter passphrase - device - always enter passphrase on device - host - always enter passphrase on host - """ - return device.apply_settings(connect(), passphrase_source=source) diff --git a/python/src/trezorlib/cli/trezorctl.py b/python/src/trezorlib/cli/trezorctl.py index baf9a28a0..6b7000527 100755 --- a/python/src/trezorlib/cli/trezorctl.py +++ b/python/src/trezorlib/cli/trezorctl.py @@ -52,7 +52,6 @@ COMMAND_ALIASES = { "change-pin": settings.pin, "enable-passphrase": settings.passphrase_enable, "disable-passphrase": settings.passphrase_disable, - "set-passphrase-source": settings.passphrase_source, "wipe-device": device.wipe, "reset-device": device.setup, "recovery-device": device.recover, @@ -143,11 +142,26 @@ def configure_logging(verbose: int): @click.option( "-j", "--json", "is_json", is_flag=True, help="Print result as JSON object" ) +@click.option( + "-P", "--passphrase-on-host", is_flag=True, help="Enter passphrase on host.", +) +@click.option( + "-s", + "--session-id", + help="Resume given session ID.", + default=os.environ.get("TREZOR_SESSION_ID"), +) @click.version_option() @click.pass_context -def cli(ctx, path, verbose, is_json): +def cli(ctx, path, verbose, is_json, passphrase_on_host, session_id): configure_logging(verbose) + if session_id: + try: + session_id = bytes.fromhex(session_id) + except ValueError: + raise click.ClickException("Not a valid session id: {}".format(session_id)) + def get_device(): try: device = get_transport(path, prefix_search=False) @@ -159,13 +173,17 @@ def cli(ctx, path, verbose, is_json): if path is not None: click.echo("Using path: {}".format(path)) sys.exit(1) - return TrezorClient(transport=device, ui=ui.ClickUI()) + return TrezorClient( + transport=device, + ui=ui.ClickUI(passphrase_on_host=passphrase_on_host), + session_id=session_id, + ) ctx.obj = get_device @cli.resultcallback() -def print_result(res, path, verbose, is_json): +def print_result(res, is_json, **kwargs): if is_json: if isinstance(res, protobuf.MessageType): click.echo(json.dumps({res.__class__.__name__: res.__dict__})) @@ -202,7 +220,7 @@ def list_devices(): @cli.command() def version(): """Show version of trezorctl/trezorlib.""" - from trezorlib import __version__ as VERSION + from .. import __version__ as VERSION return VERSION @@ -215,17 +233,33 @@ def version(): @cli.command() @click.argument("message") @click.option("-b", "--button-protection", is_flag=True) -@click.option("-p", "--pin-protection", is_flag=True) -@click.option("-r", "--passphrase-protection", is_flag=True) @click.pass_obj -def ping(connect, message, button_protection, pin_protection, passphrase_protection): +def ping(connect, message, button_protection): """Send ping message.""" - return connect().ping( - message, - button_protection=button_protection, - pin_protection=pin_protection, - passphrase_protection=passphrase_protection, - ) + return connect().ping(message, button_protection=button_protection) + + +@cli.command() +@click.pass_obj +def get_session(connect): + """Get a session ID for subsequent commands. + + Unlocks Trezor with a passphrase and returns a session ID. Use this session ID with + `trezorctl -s SESSION_ID`, or set it to an environment variable `TREZOR_SESSION_ID`, + to avoid having to enter passphrase for subsequent commands. + + The session ID is valid until another client starts using Trezor, until the next + get-session call, or until Trezor is disconnected. + """ + from ..btc import get_address + from ..client import PASSPHRASE_TEST_PATH + + client = connect() + get_address(client, "Testnet", PASSPHRASE_TEST_PATH) + if client.session_id is None: + raise click.ClickException("Passphrase not enabled or firmware too old.") + else: + return client.session_id.hex() @cli.command() diff --git a/python/src/trezorlib/client.py b/python/src/trezorlib/client.py index 24b4ccbb4..5d00fe401 100644 --- a/python/src/trezorlib/client.py +++ b/python/src/trezorlib/client.py @@ -23,6 +23,7 @@ from types import SimpleNamespace from mnemonic import Mnemonic from . import MINIMUM_FIRMWARE_VERSION, exceptions, messages, tools +from .messages import Capability if sys.version_info.major < 3: raise Exception("Trezorlib does not support Python 2 anymore.") @@ -32,6 +33,9 @@ LOG = logging.getLogger(__name__) VENDORS = ("bitcointrezor.com", "trezor.io") MAX_PASSPHRASE_LENGTH = 50 +PASSPHRASE_ON_DEVICE = object() +PASSPHRASE_TEST_PATH = tools.parse_path("44h/1h/19h/0/1337") + DEPRECATION_ERROR = """ Incompatible Trezor library detected. @@ -107,15 +111,17 @@ class TrezorClient: - passphrase request (ask the user to enter a passphrase) See `trezorlib.ui` for details. - You can supply a `state` you saved in the previous session. If you do, - the user might not need to enter their passphrase again. + You can supply a `session_id` you might have saved in the previous session. + If you do, the user might not need to enter their passphrase again. """ - def __init__(self, transport, ui=_NO_UI_OBJECT, state=None): + def __init__( + self, transport, ui=_NO_UI_OBJECT, session_id=None, + ): LOG.info("creating client instance for device: {}".format(transport.get_path())) self.transport = transport self.ui = ui - self.state = state + self.session_id = session_id # XXX remove when old Electrum has been cycled out. # explanation: We changed the API in 0.11 and this broke older versions @@ -177,30 +183,47 @@ class TrezorClient: else: return resp - def _callback_passphrase(self, msg): - if msg.on_device: - passphrase = None - else: - try: - passphrase = self.ui.get_passphrase() - except exceptions.Cancelled: - self.call_raw(messages.Cancel()) - raise + def _callback_passphrase(self, msg: messages.PassphraseRequest): + available_on_device = Capability.PassphraseEntry in self.features.capabilities - passphrase = Mnemonic.normalize_string(passphrase) - if len(passphrase) > MAX_PASSPHRASE_LENGTH: + def send_passphrase(passphrase=None, on_device=None): + if self.features.model == "1": + state = None + else: + state = self.session_id + msg = messages.PassphraseAck( + _state=state, passphrase=passphrase, on_device=on_device + ) + resp = self.call_raw(msg) + if isinstance(resp, messages.Deprecated_PassphraseStateRequest): + self.session_id = resp.state + resp = self.call_raw(messages.Deprecated_PassphraseStateAck()) + return resp + + # short-circuit old style entry + if msg._on_device is True: + return send_passphrase(None, None) + + try: + passphrase = self.ui.get_passphrase(available_on_device=available_on_device) + except exceptions.Cancelled: + self.call_raw(messages.Cancel()) + raise + + if passphrase is PASSPHRASE_ON_DEVICE: + if not available_on_device: self.call_raw(messages.Cancel()) - raise ValueError("Passphrase too long") + raise RuntimeError("Device is not capable of entering passphrase") + else: + return send_passphrase(on_device=True) - resp = self.call_raw( - messages.PassphraseAck(passphrase=passphrase, state=self.state) - ) - if isinstance(resp, messages.PassphraseStateRequest): - # TODO report to the user that the passphrase has changed? - self.state = resp.state - return self.call_raw(messages.PassphraseStateAck()) - else: - return resp + # else process host-entered passphrase + passphrase = Mnemonic.normalize_string(passphrase) + if len(passphrase) > MAX_PASSPHRASE_LENGTH: + self.call_raw(messages.Cancel()) + raise ValueError("Passphrase too long") + + return send_passphrase(passphrase, on_device=False) def _callback_button(self, msg): __tracebackhide__ = True # for pytest # pylint: disable=W0612 @@ -229,7 +252,7 @@ class TrezorClient: @tools.session def init_device(self): - resp = self.call_raw(messages.Initialize(state=self.state)) + resp = self.call_raw(messages.Initialize(session_id=self.session_id)) if not isinstance(resp, messages.Features): raise exceptions.TrezorException("Unexpected initial response") else: @@ -245,6 +268,8 @@ class TrezorClient: self.features.patch_version, ) self.check_firmware_version(warn_only=True) + if self.features.session_id is not None: + self.session_id = self.features.session_id def is_outdated(self): if self.features.bootloader_mode: @@ -262,17 +287,13 @@ class TrezorClient: @tools.expect(messages.Success, field="message") def ping( - self, - msg, - button_protection=False, - pin_protection=False, - passphrase_protection=False, + self, msg, button_protection=False, ): # We would like ping to work on any valid TrezorClient instance, but # due to the protection modes, we need to go through self.call, and that will # raise an exception if the firmware is too old. # So we short-circuit the simplest variant of ping with call_raw. - if not button_protection and not pin_protection and not passphrase_protection: + if not button_protection: # XXX this should be: `with self:` try: self.open() @@ -280,12 +301,7 @@ class TrezorClient: finally: self.close() - msg = messages.Ping( - message=msg, - button_protection=button_protection, - pin_protection=pin_protection, - passphrase_protection=passphrase_protection, - ) + msg = messages.Ping(message=msg, button_protection=button_protection,) return self.call(msg) def get_device_id(self): @@ -295,7 +311,7 @@ class TrezorClient: def clear_session(self): resp = self.call_raw(messages.ClearSession()) if isinstance(resp, messages.Success): - self.state = None + self.session_id = None self.init_device() return resp.message else: diff --git a/python/src/trezorlib/debuglink.py b/python/src/trezorlib/debuglink.py index d88043d53..b9848a081 100644 --- a/python/src/trezorlib/debuglink.py +++ b/python/src/trezorlib/debuglink.py @@ -226,7 +226,7 @@ class DebugUI: else: return self.debuglink.read_pin_encoded() - def get_passphrase(self): + def get_passphrase(self, available_on_device): return self.passphrase diff --git a/python/src/trezorlib/device.py b/python/src/trezorlib/device.py index ddd89ba2b..0f51c2ddd 100644 --- a/python/src/trezorlib/device.py +++ b/python/src/trezorlib/device.py @@ -51,7 +51,7 @@ def apply_settings( language=None, use_passphrase=None, homescreen=None, - passphrase_source=None, + passphrase_always_on_device=None, auto_lock_delay_ms=None, display_rotation=None, ): @@ -64,8 +64,8 @@ def apply_settings( settings.use_passphrase = use_passphrase if homescreen is not None: settings.homescreen = homescreen - if passphrase_source is not None: - settings.passphrase_source = passphrase_source + if passphrase_always_on_device is not None: + settings.passphrase_always_on_device = passphrase_always_on_device if auto_lock_delay_ms is not None: settings.auto_lock_delay_ms = auto_lock_delay_ms if display_rotation is not None: diff --git a/python/src/trezorlib/messages/ApplySettings.py b/python/src/trezorlib/messages/ApplySettings.py index 329a179f2..8a05ae1d6 100644 --- a/python/src/trezorlib/messages/ApplySettings.py +++ b/python/src/trezorlib/messages/ApplySettings.py @@ -6,7 +6,6 @@ if __debug__: try: from typing import Dict, List # noqa: F401 from typing_extensions import Literal # noqa: F401 - EnumTypePassphraseSourceType = Literal[0, 1, 2] except ImportError: pass @@ -20,17 +19,17 @@ class ApplySettings(p.MessageType): label: str = None, use_passphrase: bool = None, homescreen: bytes = None, - passphrase_source: EnumTypePassphraseSourceType = None, auto_lock_delay_ms: int = None, display_rotation: int = None, + passphrase_always_on_device: bool = None, ) -> None: self.language = language self.label = label self.use_passphrase = use_passphrase self.homescreen = homescreen - self.passphrase_source = passphrase_source self.auto_lock_delay_ms = auto_lock_delay_ms self.display_rotation = display_rotation + self.passphrase_always_on_device = passphrase_always_on_device @classmethod def get_fields(cls) -> Dict: @@ -39,7 +38,7 @@ class ApplySettings(p.MessageType): 2: ('label', p.UnicodeType, 0), 3: ('use_passphrase', p.BoolType, 0), 4: ('homescreen', p.BytesType, 0), - 5: ('passphrase_source', p.EnumType("PassphraseSourceType", (0, 1, 2)), 0), 6: ('auto_lock_delay_ms', p.UVarintType, 0), 7: ('display_rotation', p.UVarintType, 0), + 8: ('passphrase_always_on_device', p.BoolType, 0), } diff --git a/python/src/trezorlib/messages/ButtonRequest.py b/python/src/trezorlib/messages/ButtonRequest.py index 57f4b524d..5592eb638 100644 --- a/python/src/trezorlib/messages/ButtonRequest.py +++ b/python/src/trezorlib/messages/ButtonRequest.py @@ -6,7 +6,7 @@ if __debug__: try: from typing import Dict, List # noqa: F401 from typing_extensions import Literal # noqa: F401 - EnumTypeButtonRequestType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18] + EnumTypeButtonRequestType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19] except ImportError: pass @@ -23,5 +23,5 @@ class ButtonRequest(p.MessageType): @classmethod def get_fields(cls) -> Dict: return { - 1: ('code', p.EnumType("ButtonRequestType", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18)), 0), + 1: ('code', p.EnumType("ButtonRequestType", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19)), 0), } diff --git a/python/src/trezorlib/messages/ButtonRequestType.py b/python/src/trezorlib/messages/ButtonRequestType.py index f29e98d1e..c973ce8aa 100644 --- a/python/src/trezorlib/messages/ButtonRequestType.py +++ b/python/src/trezorlib/messages/ButtonRequestType.py @@ -16,8 +16,8 @@ Address = 10 # type: Literal[10] PublicKey = 11 # type: Literal[11] MnemonicWordCount = 12 # type: Literal[12] MnemonicInput = 13 # type: Literal[13] -PassphraseType = 14 # type: Literal[14] UnknownDerivationPath = 15 # type: Literal[15] RecoveryHomepage = 16 # type: Literal[16] Success = 17 # type: Literal[17] Warning = 18 # type: Literal[18] +PassphraseEntry = 19 # type: Literal[19] diff --git a/python/src/trezorlib/messages/Capability.py b/python/src/trezorlib/messages/Capability.py index 4a4f6d93f..6cf40cd19 100644 --- a/python/src/trezorlib/messages/Capability.py +++ b/python/src/trezorlib/messages/Capability.py @@ -19,3 +19,4 @@ Tezos = 13 # type: Literal[13] U2F = 14 # type: Literal[14] Shamir = 15 # type: Literal[15] ShamirGroups = 16 # type: Literal[16] +PassphraseEntry = 17 # type: Literal[17] diff --git a/python/src/trezorlib/messages/PassphraseStateAck.py b/python/src/trezorlib/messages/Deprecated_PassphraseStateAck.py similarity index 84% rename from python/src/trezorlib/messages/PassphraseStateAck.py rename to python/src/trezorlib/messages/Deprecated_PassphraseStateAck.py index 1faedc1c5..fa178ddf5 100644 --- a/python/src/trezorlib/messages/PassphraseStateAck.py +++ b/python/src/trezorlib/messages/Deprecated_PassphraseStateAck.py @@ -10,5 +10,5 @@ if __debug__: pass -class PassphraseStateAck(p.MessageType): +class Deprecated_PassphraseStateAck(p.MessageType): MESSAGE_WIRE_TYPE = 78 diff --git a/python/src/trezorlib/messages/PassphraseStateRequest.py b/python/src/trezorlib/messages/Deprecated_PassphraseStateRequest.py similarity index 89% rename from python/src/trezorlib/messages/PassphraseStateRequest.py rename to python/src/trezorlib/messages/Deprecated_PassphraseStateRequest.py index 6cbcbe58b..4ca29bb51 100644 --- a/python/src/trezorlib/messages/PassphraseStateRequest.py +++ b/python/src/trezorlib/messages/Deprecated_PassphraseStateRequest.py @@ -10,7 +10,7 @@ if __debug__: pass -class PassphraseStateRequest(p.MessageType): +class Deprecated_PassphraseStateRequest(p.MessageType): MESSAGE_WIRE_TYPE = 77 def __init__( diff --git a/python/src/trezorlib/messages/Features.py b/python/src/trezorlib/messages/Features.py index 23720aa22..5a2390b02 100644 --- a/python/src/trezorlib/messages/Features.py +++ b/python/src/trezorlib/messages/Features.py @@ -6,7 +6,7 @@ if __debug__: try: from typing import Dict, List # noqa: F401 from typing_extensions import Literal # noqa: F401 - EnumTypeCapability = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + EnumTypeCapability = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17] EnumTypeBackupType = Literal[0, 1, 2] except ImportError: pass @@ -32,7 +32,6 @@ class Features(p.MessageType): bootloader_hash: bytes = None, imported: bool = None, pin_cached: bool = None, - passphrase_cached: bool = None, firmware_present: bool = None, needs_backup: bool = None, flags: int = None, @@ -50,6 +49,8 @@ class Features(p.MessageType): sd_card_present: bool = None, sd_protection: bool = None, wipe_code_protection: bool = None, + session_id: bytes = None, + passphrase_always_on_device: bool = None, ) -> None: self.vendor = vendor self.major_version = major_version @@ -66,7 +67,6 @@ class Features(p.MessageType): self.bootloader_hash = bootloader_hash self.imported = imported self.pin_cached = pin_cached - self.passphrase_cached = passphrase_cached self.firmware_present = firmware_present self.needs_backup = needs_backup self.flags = flags @@ -84,6 +84,8 @@ class Features(p.MessageType): self.sd_card_present = sd_card_present self.sd_protection = sd_protection self.wipe_code_protection = wipe_code_protection + self.session_id = session_id + self.passphrase_always_on_device = passphrase_always_on_device @classmethod def get_fields(cls) -> Dict: @@ -103,7 +105,6 @@ class Features(p.MessageType): 14: ('bootloader_hash', p.BytesType, 0), 15: ('imported', p.BoolType, 0), 16: ('pin_cached', p.BoolType, 0), - 17: ('passphrase_cached', p.BoolType, 0), 18: ('firmware_present', p.BoolType, 0), 19: ('needs_backup', p.BoolType, 0), 20: ('flags', p.UVarintType, 0), @@ -116,9 +117,11 @@ class Features(p.MessageType): 27: ('unfinished_backup', p.BoolType, 0), 28: ('no_backup', p.BoolType, 0), 29: ('recovery_mode', p.BoolType, 0), - 30: ('capabilities', p.EnumType("Capability", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)), p.FLAG_REPEATED), + 30: ('capabilities', p.EnumType("Capability", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)), p.FLAG_REPEATED), 31: ('backup_type', p.EnumType("BackupType", (0, 1, 2)), 0), 32: ('sd_card_present', p.BoolType, 0), 33: ('sd_protection', p.BoolType, 0), 34: ('wipe_code_protection', p.BoolType, 0), + 35: ('session_id', p.BytesType, 0), + 36: ('passphrase_always_on_device', p.BoolType, 0), } diff --git a/python/src/trezorlib/messages/Initialize.py b/python/src/trezorlib/messages/Initialize.py index dacf75889..3d8ca3810 100644 --- a/python/src/trezorlib/messages/Initialize.py +++ b/python/src/trezorlib/messages/Initialize.py @@ -15,15 +15,12 @@ class Initialize(p.MessageType): def __init__( self, - state: bytes = None, - skip_passphrase: bool = None, + session_id: bytes = None, ) -> None: - self.state = state - self.skip_passphrase = skip_passphrase + self.session_id = session_id @classmethod def get_fields(cls) -> Dict: return { - 1: ('state', p.BytesType, 0), - 2: ('skip_passphrase', p.BoolType, 0), + 1: ('session_id', p.BytesType, 0), } diff --git a/python/src/trezorlib/messages/MessageType.py b/python/src/trezorlib/messages/MessageType.py index bc8df2258..f24095c89 100644 --- a/python/src/trezorlib/messages/MessageType.py +++ b/python/src/trezorlib/messages/MessageType.py @@ -27,8 +27,6 @@ EntropyRequest = 35 # type: Literal[35] EntropyAck = 36 # type: Literal[36] PassphraseRequest = 41 # type: Literal[41] PassphraseAck = 42 # type: Literal[42] -PassphraseStateRequest = 77 # type: Literal[77] -PassphraseStateAck = 78 # type: Literal[78] RecoveryDevice = 45 # type: Literal[45] WordRequest = 46 # type: Literal[46] WordAck = 47 # type: Literal[47] @@ -38,6 +36,8 @@ SdProtect = 79 # type: Literal[79] GetNextU2FCounter = 80 # type: Literal[80] NextU2FCounter = 81 # type: Literal[81] ChangeWipeCode = 82 # type: Literal[82] +Deprecated_PassphraseStateRequest = 77 # type: Literal[77] +Deprecated_PassphraseStateAck = 78 # type: Literal[78] FirmwareErase = 6 # type: Literal[6] FirmwareUpload = 7 # type: Literal[7] FirmwareRequest = 8 # type: Literal[8] diff --git a/python/src/trezorlib/messages/PassphraseAck.py b/python/src/trezorlib/messages/PassphraseAck.py index 65f7be27b..ef00fa78e 100644 --- a/python/src/trezorlib/messages/PassphraseAck.py +++ b/python/src/trezorlib/messages/PassphraseAck.py @@ -16,14 +16,17 @@ class PassphraseAck(p.MessageType): def __init__( self, passphrase: str = None, - state: bytes = None, + _state: bytes = None, + on_device: bool = None, ) -> None: self.passphrase = passphrase - self.state = state + self._state = _state + self.on_device = on_device @classmethod def get_fields(cls) -> Dict: return { 1: ('passphrase', p.UnicodeType, 0), - 2: ('state', p.BytesType, 0), + 2: ('_state', p.BytesType, 0), + 3: ('on_device', p.BoolType, 0), } diff --git a/python/src/trezorlib/messages/PassphraseRequest.py b/python/src/trezorlib/messages/PassphraseRequest.py index 94a5bdbac..903c0dfc2 100644 --- a/python/src/trezorlib/messages/PassphraseRequest.py +++ b/python/src/trezorlib/messages/PassphraseRequest.py @@ -15,12 +15,12 @@ class PassphraseRequest(p.MessageType): def __init__( self, - on_device: bool = None, + _on_device: bool = None, ) -> None: - self.on_device = on_device + self._on_device = _on_device @classmethod def get_fields(cls) -> Dict: return { - 1: ('on_device', p.BoolType, 0), + 1: ('_on_device', p.BoolType, 0), } diff --git a/python/src/trezorlib/messages/PassphraseSourceType.py b/python/src/trezorlib/messages/PassphraseSourceType.py deleted file mode 100644 index d62ea047d..000000000 --- a/python/src/trezorlib/messages/PassphraseSourceType.py +++ /dev/null @@ -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] diff --git a/python/src/trezorlib/messages/Ping.py b/python/src/trezorlib/messages/Ping.py index 5f34b81ba..763c8ea98 100644 --- a/python/src/trezorlib/messages/Ping.py +++ b/python/src/trezorlib/messages/Ping.py @@ -17,19 +17,13 @@ class Ping(p.MessageType): self, message: str = None, button_protection: bool = None, - pin_protection: bool = None, - passphrase_protection: bool = None, ) -> None: self.message = message self.button_protection = button_protection - self.pin_protection = pin_protection - self.passphrase_protection = passphrase_protection @classmethod def get_fields(cls) -> Dict: return { 1: ('message', p.UnicodeType, 0), 2: ('button_protection', p.BoolType, 0), - 3: ('pin_protection', p.BoolType, 0), - 4: ('passphrase_protection', p.BoolType, 0), } diff --git a/python/src/trezorlib/messages/__init__.py b/python/src/trezorlib/messages/__init__.py index 8fdb40269..28e4bfd33 100644 --- a/python/src/trezorlib/messages/__init__.py +++ b/python/src/trezorlib/messages/__init__.py @@ -55,6 +55,8 @@ from .DebugLinkState import DebugLinkState from .DebugLinkStop import DebugLinkStop from .DebugMoneroDiagAck import DebugMoneroDiagAck from .DebugMoneroDiagRequest import DebugMoneroDiagRequest +from .Deprecated_PassphraseStateAck import Deprecated_PassphraseStateAck +from .Deprecated_PassphraseStateRequest import Deprecated_PassphraseStateRequest from .ECDHSessionKey import ECDHSessionKey from .Entropy import Entropy from .EntropyAck import EntropyAck @@ -196,8 +198,6 @@ from .NEMTransfer import NEMTransfer from .NextU2FCounter import NextU2FCounter from .PassphraseAck import PassphraseAck from .PassphraseRequest import PassphraseRequest -from .PassphraseStateAck import PassphraseStateAck -from .PassphraseStateRequest import PassphraseStateRequest from .PinMatrixAck import PinMatrixAck from .PinMatrixRequest import PinMatrixRequest from .Ping import Ping @@ -283,7 +283,6 @@ from . import NEMModificationType from . import NEMMosaicLevy from . import NEMSupplyChangeType from . import OutputScriptType -from . import PassphraseSourceType from . import PinMatrixRequestType from . import RecoveryDeviceType from . import RequestType diff --git a/python/src/trezorlib/ui.py b/python/src/trezorlib/ui.py index 9d486b67b..55e0e349c 100644 --- a/python/src/trezorlib/ui.py +++ b/python/src/trezorlib/ui.py @@ -20,6 +20,7 @@ import click from mnemonic import Mnemonic from . import device +from .client import PASSPHRASE_ON_DEVICE from .exceptions import Cancelled from .messages import PinMatrixRequestType, WordRequestType @@ -58,10 +59,11 @@ def prompt(*args, **kwargs): class ClickUI: - def __init__(self, always_prompt=False): + def __init__(self, always_prompt=False, passphrase_on_host=False): self.pinmatrix_shown = False self.prompt_shown = False self.always_prompt = always_prompt + self.passphrase_on_host = passphrase_on_host def button_request(self, code): if not self.prompt_shown: @@ -98,7 +100,10 @@ class ClickUI: else: return pin - def get_passphrase(self): + def get_passphrase(self, available_on_device): + if available_on_device and not self.passphrase_on_host: + return PASSPHRASE_ON_DEVICE + if os.getenv("PASSPHRASE") is not None: echo("Passphrase required. Using PASSPHRASE environment variable.") return os.getenv("PASSPHRASE") diff --git a/tests/.gitignore b/tests/.gitignore index 9d6ffe384..fd6fe0b1a 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1 +1,2 @@ junit.xml +trezor.log diff --git a/tests/conftest.py b/tests/conftest.py index fecfa8d8b..b9309bc39 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,8 +20,7 @@ import pytest from trezorlib import debuglink, log from trezorlib.debuglink import TrezorClientDebugLink -from trezorlib.device import apply_settings, wipe as wipe_device -from trezorlib.messages.PassphraseSourceType import HOST as PASSPHRASE_ON_HOST +from trezorlib.device import wipe as wipe_device from trezorlib.transport import enumerate_devices, get_transport from . import ui_tests @@ -36,8 +35,8 @@ def get_device(): try: transport = get_transport(path) return TrezorClientDebugLink(transport, auto_interact=not interact) - except Exception as e: - raise RuntimeError("Failed to open debuglink for {}".format(path)) from e + except Exception: + pytest.exit("Failed to open debuglink for {}".format(path), 3) else: devices = enumerate_devices() @@ -47,7 +46,7 @@ def get_device(): except Exception: pass else: - raise RuntimeError("No debuggable device found") + pytest.exit("No debuggable device found", 3) @pytest.fixture(scope="function") @@ -130,8 +129,6 @@ def client(request): needs_backup=setup_params["needs_backup"], no_backup=setup_params["no_backup"], ) - if setup_params["passphrase"] and client.features.model != "1": - apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST) if setup_params["pin"]: # ClearSession locks the device. We only do that if the PIN is set. diff --git a/tests/device_tests/test_basic.py b/tests/device_tests/test_basic.py index 6a503d143..ff59c181f 100644 --- a/tests/device_tests/test_basic.py +++ b/tests/device_tests/test_basic.py @@ -20,7 +20,7 @@ from trezorlib import device, messages class TestBasic: def test_features(self, client): f0 = client.features - f1 = client.call(messages.Initialize()) + f1 = client.call(messages.Initialize(f0.session_id)) assert f0 == f1 def test_ping(self, client): diff --git a/tests/device_tests/test_debuglink.py b/tests/device_tests/test_debuglink.py index 64848873a..edfeff820 100644 --- a/tests/device_tests/test_debuglink.py +++ b/tests/device_tests/test_debuglink.py @@ -34,7 +34,7 @@ class TestDebuglink: @pytest.mark.setup_client(mnemonic=MNEMONIC12, pin="1234", passphrase=True) def test_pin(self, client): - resp = client.call_raw(messages.Ping(message="test", pin_protection=True)) + resp = client.call_raw(messages.GetAddress()) assert isinstance(resp, messages.PinMatrixRequest) pin, matrix = client.debug.read_pin() @@ -43,4 +43,7 @@ class TestDebuglink: pin_encoded = client.debug.read_pin_encoded() resp = client.call_raw(messages.PinMatrixAck(pin=pin_encoded)) - assert isinstance(resp, messages.Success) + assert isinstance(resp, messages.PassphraseRequest) + + resp = client.call_raw(messages.PassphraseAck(passphrase="")) + assert isinstance(resp, messages.Address) diff --git a/tests/device_tests/test_msg_applysettings.py b/tests/device_tests/test_msg_applysettings.py index c9f83063a..2656330aa 100644 --- a/tests/device_tests/test_msg_applysettings.py +++ b/tests/device_tests/test_msg_applysettings.py @@ -18,7 +18,7 @@ import time import pytest -from trezorlib import device, messages as proto +from trezorlib import btc, device, messages as proto EXPECTED_RESPONSES_NOPIN = [proto.ButtonRequest(), proto.Success(), proto.Features()] EXPECTED_RESPONSES_PIN = [proto.PinMatrixRequest()] + EXPECTED_RESPONSES_NOPIN @@ -91,13 +91,13 @@ class TestMsgApplysettings: time.sleep(0.1) # sleep less than auto-lock delay with client: # No PIN protection is required. - client.set_expected_responses([proto.Success()]) - client.ping(msg="", pin_protection=True) + client.set_expected_responses([proto.Address()]) + btc.get_address(client, "Testnet", [0]) time.sleep(10.1) # sleep more than auto-lock delay with client: - client.set_expected_responses([proto.PinMatrixRequest(), proto.Success()]) - client.ping(msg="", pin_protection=True) + client.set_expected_responses([proto.PinMatrixRequest(), proto.Address()]) + btc.get_address(client, "Testnet", [0]) @pytest.mark.skip_t2 def test_apply_minimal_auto_lock_delay(self, client): @@ -114,16 +114,16 @@ class TestMsgApplysettings: time.sleep(0.1) # sleep less than auto-lock delay with client: # No PIN protection is required. - client.set_expected_responses([proto.Success()]) - client.ping(msg="", pin_protection=True) + client.set_expected_responses([proto.Address()]) + btc.get_address(client, "Testnet", [0]) time.sleep(2) # sleep less than the minimal auto-lock delay with client: # No PIN protection is required. - client.set_expected_responses([proto.Success()]) - client.ping(msg="", pin_protection=True) + client.set_expected_responses([proto.Address()]) + btc.get_address(client, "Testnet", [0]) time.sleep(10.1) # sleep more than the minimal auto-lock delay with client: - client.set_expected_responses([proto.PinMatrixRequest(), proto.Success()]) - client.ping(msg="", pin_protection=True) + client.set_expected_responses([proto.PinMatrixRequest(), proto.Address()]) + btc.get_address(client, "Testnet", [0]) diff --git a/tests/device_tests/test_msg_cardano_sign_tx_slip39_basic.py b/tests/device_tests/test_msg_cardano_sign_tx_slip39_basic.py index 79aca76bf..75e5d62c0 100644 --- a/tests/device_tests/test_msg_cardano_sign_tx_slip39_basic.py +++ b/tests/device_tests/test_msg_cardano_sign_tx_slip39_basic.py @@ -119,10 +119,7 @@ def test_cardano_sign_tx( inputs = [cardano.create_input(i) for i in inputs] outputs = [cardano.create_output(o) for o in outputs] - expected_responses = [ - messages.PassphraseRequest(), - messages.PassphraseStateRequest(), - ] + expected_responses = [messages.PassphraseRequest()] expected_responses += [ messages.CardanoTxRequest(tx_index=i) for i in range(len(transactions)) ] diff --git a/tests/device_tests/test_msg_change_wipe_code_t1.py b/tests/device_tests/test_msg_change_wipe_code_t1.py index d4152bba0..8468324b9 100644 --- a/tests/device_tests/test_msg_change_wipe_code_t1.py +++ b/tests/device_tests/test_msg_change_wipe_code_t1.py @@ -225,5 +225,5 @@ def test_set_pin_to_wipe_code(client): # Check that there is no PIN protection. client.init_device() assert client.features.pin_protection is False - ret = client.call_raw(messages.Ping(pin_protection=True)) - assert isinstance(ret, messages.Success) + resp = client.call_raw(messages.GetAddress()) + assert isinstance(resp, messages.Address) diff --git a/tests/device_tests/test_msg_changepin.py b/tests/device_tests/test_msg_changepin.py index ab7a9ac60..62aacad92 100644 --- a/tests/device_tests/test_msg_changepin.py +++ b/tests/device_tests/test_msg_changepin.py @@ -29,8 +29,8 @@ class TestMsgChangepin: assert features.pin_protection is False # Check that there's no PIN protection - ret = client.call_raw(proto.Ping(pin_protection=True)) - assert isinstance(ret, proto.Success) + ret = client.call_raw(proto.GetAddress()) + assert isinstance(ret, proto.Address) # Let's set new PIN ret = client.call_raw(proto.ChangePin()) @@ -66,7 +66,7 @@ class TestMsgChangepin: assert features.pin_protection is True # Check that there's PIN protection - ret = client.call_raw(proto.Ping(pin_protection=True)) + ret = client.call_raw(proto.GetAddress()) assert isinstance(ret, proto.PinMatrixRequest) client.call_raw(proto.Cancel()) @@ -112,7 +112,7 @@ class TestMsgChangepin: assert features.pin_protection is True # Check that there's PIN protection - ret = client.call_raw(proto.Ping(pin_protection=True)) + ret = client.call_raw(proto.GetAddress()) assert isinstance(ret, proto.PinMatrixRequest) client.call_raw(proto.Cancel()) @@ -135,16 +135,16 @@ class TestMsgChangepin: # Check that there's no PIN protection now features = client.call_raw(proto.Initialize()) assert features.pin_protection is False - ret = client.call_raw(proto.Ping(pin_protection=True)) - assert isinstance(ret, proto.Success) + ret = client.call_raw(proto.GetAddress()) + assert isinstance(ret, proto.Address) def test_set_failed(self, client): features = client.call_raw(proto.Initialize()) assert features.pin_protection is False # Check that there's no PIN protection - ret = client.call_raw(proto.Ping(pin_protection=True)) - assert isinstance(ret, proto.Success) + ret = client.call_raw(proto.GetAddress()) + assert isinstance(ret, proto.Address) # Let's set new PIN ret = client.call_raw(proto.ChangePin()) @@ -170,8 +170,8 @@ class TestMsgChangepin: # Check that there's still no PIN protection now features = client.call_raw(proto.Initialize()) assert features.pin_protection is False - ret = client.call_raw(proto.Ping(pin_protection=True)) - assert isinstance(ret, proto.Success) + ret = client.call_raw(proto.GetAddress()) + assert isinstance(ret, proto.Address) @pytest.mark.setup_client(pin=True) def test_set_failed_2(self, client): @@ -211,8 +211,8 @@ class TestMsgChangepin: def check_pin(self, client, pin): client.clear_session() - ret = client.call_raw(proto.Ping(pin_protection=True)) + ret = client.call_raw(proto.GetAddress()) assert isinstance(ret, proto.PinMatrixRequest) pin_encoded = client.debug.encode_pin(pin) ret = client.call_raw(proto.PinMatrixAck(pin=pin_encoded)) - assert isinstance(ret, proto.Success) + assert isinstance(ret, proto.Address) diff --git a/tests/device_tests/test_msg_clearsession.py b/tests/device_tests/test_msg_clearsession.py index ce1e492c6..e19ce317c 100644 --- a/tests/device_tests/test_msg_clearsession.py +++ b/tests/device_tests/test_msg_clearsession.py @@ -16,78 +16,41 @@ import pytest -from trezorlib import messages as proto +from trezorlib import messages +from trezorlib.btc import get_public_node +from trezorlib.tools import parse_path +ADDRESS_N = parse_path("44'/0'/0'") +XPUB = "xpub6BiVtCpG9fQPxnPmHXG8PhtzQdWC2Su4qWu6XW9tpWFYhxydCLJGrWBJZ5H6qTAHdPQ7pQhtpjiYZVZARo14qHiay2fvrX996oEP42u8wZy" -@pytest.mark.skip_t2 -class TestMsgClearsession: - @pytest.mark.setup_client(pin=True, passphrase=True) - def test_clearsession(self, client): - with client: - client.set_expected_responses( - [ - proto.ButtonRequest(code=proto.ButtonRequestType.ProtectCall), - proto.PinMatrixRequest(), - proto.PassphraseRequest(), - proto.Success(), - ] - ) - res = client.ping( - "random data", - button_protection=True, - pin_protection=True, - passphrase_protection=True, - ) - assert res == "random data" - with client: - # pin and passphrase are cached - client.set_expected_responses( - [ - proto.ButtonRequest(code=proto.ButtonRequestType.ProtectCall), - proto.Success(), - ] - ) - res = client.ping( - "random data", - button_protection=True, - pin_protection=True, - passphrase_protection=True, - ) - assert res == "random data" +@pytest.mark.skip_ui +@pytest.mark.setup_client(pin=True, passphrase=True) +def test_clear_session(client): + if client.features.model == "1": + init_responses = [messages.PinMatrixRequest(), messages.PassphraseRequest()] + else: + init_responses = [messages.PassphraseRequest()] - client.clear_session() + cached_responses = [messages.PublicKey()] - # session cache is cleared - with client: - client.set_expected_responses( - [ - proto.ButtonRequest(code=proto.ButtonRequestType.ProtectCall), - proto.PinMatrixRequest(), - proto.PassphraseRequest(), - proto.Success(), - ] - ) - res = client.ping( - "random data", - button_protection=True, - pin_protection=True, - passphrase_protection=True, - ) - assert res == "random data" + with client: + client.set_expected_responses(init_responses + cached_responses) + assert get_public_node(client, ADDRESS_N).xpub == XPUB - with client: - # pin and passphrase are cached - client.set_expected_responses( - [ - proto.ButtonRequest(code=proto.ButtonRequestType.ProtectCall), - proto.Success(), - ] - ) - res = client.ping( - "random data", - button_protection=True, - pin_protection=True, - passphrase_protection=True, - ) - assert res == "random data" + with client: + # pin and passphrase are cached + client.set_expected_responses(cached_responses) + assert get_public_node(client, ADDRESS_N).xpub == XPUB + + client.clear_session() + + # session cache is cleared + with client: + client.set_expected_responses(init_responses + cached_responses) + assert get_public_node(client, ADDRESS_N).xpub == XPUB + + with client: + # pin and passphrase are cached + client.set_expected_responses(cached_responses) + assert get_public_node(client, ADDRESS_N).xpub == XPUB diff --git a/tests/device_tests/test_msg_loaddevice.py b/tests/device_tests/test_msg_loaddevice.py index e308a4216..6ff5f0d75 100644 --- a/tests/device_tests/test_msg_loaddevice.py +++ b/tests/device_tests/test_msg_loaddevice.py @@ -18,7 +18,6 @@ import pytest from trezorlib import btc, debuglink, device from trezorlib.messages import BackupType -from trezorlib.messages.PassphraseSourceType import HOST as PASSPHRASE_ON_HOST from ..common import ( MNEMONIC12, @@ -53,8 +52,6 @@ class TestDeviceLoad: passphrase_protection=True, label="test", ) - if client.features.model == "T": - device.apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST) client.set_passphrase("passphrase") state = client.debug.state() assert state.mnemonic_secret == MNEMONIC12.encode() @@ -108,7 +105,6 @@ class TestDeviceLoad: u"Neuve\u030cr\u030citelne\u030c bezpec\u030cne\u0301 hesli\u0301c\u030cko" ) - device.wipe(client) debuglink.load_device( client, mnemonic=words_nfkd, @@ -118,8 +114,6 @@ class TestDeviceLoad: language="en-US", skip_checksum=True, ) - if client.features.model == "T": - device.apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST) client.set_passphrase(passphrase_nfkd) address_nfkd = btc.get_address(client, "Bitcoin", []) @@ -133,8 +127,6 @@ class TestDeviceLoad: language="en-US", skip_checksum=True, ) - if client.features.model == "T": - device.apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST) client.set_passphrase(passphrase_nfc) address_nfc = btc.get_address(client, "Bitcoin", []) @@ -148,8 +140,6 @@ class TestDeviceLoad: language="en-US", skip_checksum=True, ) - if client.features.model == "T": - device.apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST) client.set_passphrase(passphrase_nfkc) address_nfkc = btc.get_address(client, "Bitcoin", []) @@ -163,8 +153,6 @@ class TestDeviceLoad: language="en-US", skip_checksum=True, ) - if client.features.model == "T": - device.apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST) client.set_passphrase(passphrase_nfd) address_nfd = btc.get_address(client, "Bitcoin", []) diff --git a/tests/device_tests/test_msg_ping.py b/tests/device_tests/test_msg_ping.py index 29c5f1c5a..5bd1d6266 100644 --- a/tests/device_tests/test_msg_ping.py +++ b/tests/device_tests/test_msg_ping.py @@ -19,8 +19,8 @@ import pytest from trezorlib import messages as proto -@pytest.mark.skip_t2 class TestMsgPing: + @pytest.mark.skip_ui @pytest.mark.setup_client(pin=True, passphrase=True) def test_ping(self, client): with client: @@ -37,48 +37,3 @@ class TestMsgPing: ) res = client.ping("random data", button_protection=True) assert res == "random data" - - with client: - client.set_expected_responses([proto.PinMatrixRequest(), proto.Success()]) - res = client.ping("random data", pin_protection=True) - assert res == "random data" - - with client: - client.set_expected_responses([proto.PassphraseRequest(), proto.Success()]) - res = client.ping("random data", passphrase_protection=True) - assert res == "random data" - - @pytest.mark.setup_client(pin=True, passphrase=True) - def test_ping_caching(self, client): - with client: - client.set_expected_responses( - [ - proto.ButtonRequest(code=proto.ButtonRequestType.ProtectCall), - proto.PinMatrixRequest(), - proto.PassphraseRequest(), - proto.Success(), - ] - ) - res = client.ping( - "random data", - button_protection=True, - pin_protection=True, - passphrase_protection=True, - ) - assert res == "random data" - - with client: - # pin and passphrase are cached - client.set_expected_responses( - [ - proto.ButtonRequest(code=proto.ButtonRequestType.ProtectCall), - proto.Success(), - ] - ) - res = client.ping( - "random data", - button_protection=True, - pin_protection=True, - passphrase_protection=True, - ) - assert res == "random data" diff --git a/tests/device_tests/test_msg_recoverydevice_bip39.py b/tests/device_tests/test_msg_recoverydevice_bip39.py index 69e78f137..07782f05c 100644 --- a/tests/device_tests/test_msg_recoverydevice_bip39.py +++ b/tests/device_tests/test_msg_recoverydevice_bip39.py @@ -85,7 +85,7 @@ class TestMsgRecoverydevice: assert client.features.passphrase_protection is True # Do passphrase-protected action, PassphraseRequest should be raised - resp = client.call_raw(proto.Ping(passphrase_protection=True)) + resp = client.call_raw(proto.GetAddress()) assert isinstance(resp, proto.PassphraseRequest) client.call_raw(proto.Cancel()) @@ -136,13 +136,9 @@ class TestMsgRecoverydevice: assert client.features.pin_protection is False assert client.features.passphrase_protection is False - # Do passphrase-protected action, PassphraseRequest should NOT be raised - resp = client.call_raw(proto.Ping(passphrase_protection=True)) - assert isinstance(resp, proto.Success) - - # Do PIN-protected action, PinRequest should NOT be raised - resp = client.call_raw(proto.Ping(pin_protection=True)) - assert isinstance(resp, proto.Success) + # Do pin & passphrase-protected action, PassphraseRequest should NOT be raised + resp = client.call_raw(proto.GetAddress()) + assert isinstance(resp, proto.Address) @pytest.mark.setup_client(uninitialized=True) def test_word_fail(self, client): diff --git a/tests/device_tests/test_msg_resetdevice_bip39.py b/tests/device_tests/test_msg_resetdevice_bip39.py index 4606efed9..eab5c0d34 100644 --- a/tests/device_tests/test_msg_resetdevice_bip39.py +++ b/tests/device_tests/test_msg_resetdevice_bip39.py @@ -87,13 +87,9 @@ class TestMsgResetDevice: assert resp.pin_protection is False assert resp.passphrase_protection is False - # Do passphrase-protected action, PassphraseRequest should NOT be raised - resp = client.call_raw(proto.Ping(passphrase_protection=True)) - assert isinstance(resp, proto.Success) - - # Do PIN-protected action, PinRequest should NOT be raised - resp = client.call_raw(proto.Ping(pin_protection=True)) - assert isinstance(resp, proto.Success) + # Do pin & passphrase-protected action, PassphraseRequest should NOT be raised + resp = client.call_raw(proto.GetAddress()) + assert isinstance(resp, proto.Address) @pytest.mark.setup_client(uninitialized=True) def test_reset_device_pin(self, client): @@ -180,7 +176,7 @@ class TestMsgResetDevice: assert resp.passphrase_protection is True # Do passphrase-protected action, PassphraseRequest should be raised - resp = client.call_raw(proto.Ping(passphrase_protection=True)) + resp = client.call_raw(proto.GetAddress()) assert isinstance(resp, proto.PassphraseRequest) client.call_raw(proto.Cancel()) diff --git a/tests/device_tests/test_protect_call.py b/tests/device_tests/test_protect_call.py index 7a40642da..15d284680 100644 --- a/tests/device_tests/test_protect_call.py +++ b/tests/device_tests/test_protect_call.py @@ -18,7 +18,7 @@ import time import pytest -from trezorlib import messages as proto +from trezorlib import btc, messages as proto from trezorlib.exceptions import PinException # FIXME TODO Add passphrase tests @@ -26,84 +26,36 @@ from trezorlib.exceptions import PinException @pytest.mark.skip_t2 class TestProtectCall: - def _some_protected_call(self, client, button, pin, passphrase): + def _some_protected_call(self, client): # This method perform any call which have protection in the device - res = client.ping( - "random data", - button_protection=button, - pin_protection=pin, - passphrase_protection=passphrase, - ) - assert res == "random data" - - @pytest.mark.setup_client(pin="1234", passphrase=True) - def test_expected_responses(self, client): - # This is low-level test of set_expected_responses() - # feature of debugging client - - with pytest.raises(AssertionError), client: - # Scenario 1 - Received unexpected message - client.set_expected_responses([]) - self._some_protected_call(client, True, True, True) - - with pytest.raises(AssertionError), client: - # Scenario 2 - Received other than expected message - client.set_expected_responses([proto.Success()]) - self._some_protected_call(client, True, True, True) - - with pytest.raises(AssertionError), client: - # Scenario 3 - Not received expected message - client.set_expected_responses( - [proto.ButtonRequest(), proto.Success(), proto.Success()] - ) # This is expected, but not received - self._some_protected_call(client, True, False, False) - - with pytest.raises(AssertionError), client: - # Scenario 4 - Received what expected - client.set_expected_responses( - [ - proto.ButtonRequest(), - proto.PinMatrixRequest(), - proto.PassphraseRequest(), - proto.Success(message="random data"), - ] - ) - self._some_protected_call(client, True, True, True) - - with pytest.raises(AssertionError), client: - # Scenario 5 - Failed message by field filter - client.set_expected_responses( - [proto.ButtonRequest(), proto.Success(message="wrong data")] - ) - self._some_protected_call(client, True, True, True) + res = btc.get_address(client, "Testnet", [0]) + assert res == "mndoQDWatQhfeQbprzZxD43mZ75Z94D6vz" def test_no_protection(self, client): with client: assert client.debug.read_pin()[0] is None - client.set_expected_responses([proto.Success()]) - self._some_protected_call(client, False, True, True) + client.set_expected_responses([proto.Address()]) + self._some_protected_call(client) - @pytest.mark.setup_client(pin="1234", passphrase=True) + @pytest.mark.setup_client(pin="1234") def test_pin(self, client): with client: assert client.debug.read_pin()[0] == "1234" client.setup_debuglink(button=True, pin_correct=True) - client.set_expected_responses( - [proto.ButtonRequest(), proto.PinMatrixRequest(), proto.Success()] - ) - self._some_protected_call(client, True, True, False) + client.set_expected_responses([proto.PinMatrixRequest(), proto.Address()]) + self._some_protected_call(client) - @pytest.mark.setup_client(pin="1234", passphrase=True) + @pytest.mark.setup_client(pin="1234") def test_incorrect_pin(self, client): client.setup_debuglink(button=True, pin_correct=False) with pytest.raises(PinException): - self._some_protected_call(client, False, True, False) + self._some_protected_call(client) - @pytest.mark.setup_client(pin="1234", passphrase=True) + @pytest.mark.setup_client(pin="1234") def test_cancelled_pin(self, client): client.setup_debuglink(button=True, pin_correct=False) # PIN cancel with pytest.raises(PinException): - self._some_protected_call(client, False, True, False) + self._some_protected_call(client) @pytest.mark.setup_client(pin="1234", passphrase=True) def test_exponential_backoff_with_reboot(self, client): @@ -126,5 +78,5 @@ class TestProtectCall: for attempt in range(1, 4): start = time.time() with pytest.raises(PinException): - self._some_protected_call(client, False, True, False) + self._some_protected_call(client) test_backoff(attempt, start) diff --git a/tests/device_tests/test_protection_levels.py b/tests/device_tests/test_protection_levels.py index 67004270f..d4db71f3e 100644 --- a/tests/device_tests/test_protection_levels.py +++ b/tests/device_tests/test_protection_levels.py @@ -63,18 +63,10 @@ class TestProtectionLevels: ) device.change_pin(client) - @pytest.mark.setup_client(pin=True, passphrase=True) def test_ping(self, client): with client: - client.set_expected_responses( - [ - proto.ButtonRequest(), - proto.PinMatrixRequest(), - proto.PassphraseRequest(), - proto.Success(), - ] - ) - client.ping("msg", True, True, True) + client.set_expected_responses([proto.ButtonRequest(), proto.Success()]) + client.ping("msg", True) @pytest.mark.setup_client(pin=True, passphrase=True) def test_get_entropy(self, client): @@ -269,29 +261,21 @@ class TestProtectionLevels: assert client.features.pin_cached is False with client: - client.set_expected_responses( - [proto.ButtonRequest(), proto.PinMatrixRequest(), proto.Success()] - ) - client.ping("msg", True, True, True) + client.set_expected_responses([proto.PinMatrixRequest(), proto.Address()]) + btc.get_address(client, "Testnet", [0]) client.init_device() assert client.features.pin_cached is True with client: - client.set_expected_responses([proto.ButtonRequest(), proto.Success()]) - client.ping("msg", True, True, True) + client.set_expected_responses([proto.Address()]) + btc.get_address(client, "Testnet", [0]) @pytest.mark.setup_client(passphrase=True) def test_passphrase_cached(self, client): - assert client.features.passphrase_cached is False - with client: - client.set_expected_responses( - [proto.ButtonRequest(), proto.PassphraseRequest(), proto.Success()] - ) - client.ping("msg", True, True, True) + client.set_expected_responses([proto.PassphraseRequest(), proto.Address()]) + btc.get_address(client, "Testnet", [0]) - features = client.call(proto.GetFeatures()) - assert features.passphrase_cached is True with client: - client.set_expected_responses([proto.ButtonRequest(), proto.Success()]) - client.ping("msg", True, True, True) + client.set_expected_responses([proto.Address()]) + btc.get_address(client, "Testnet", [0]) diff --git a/tests/device_tests/test_session_id_and_passphrase.py b/tests/device_tests/test_session_id_and_passphrase.py new file mode 100644 index 000000000..0ec35e44d --- /dev/null +++ b/tests/device_tests/test_session_id_and_passphrase.py @@ -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 . + +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 diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index 7f0190c4c..3a37dcede 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -9,9 +9,9 @@ "test_cancel.py::test_cancel_message_via_initialize[message1]": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_msg_applysettings.py-test_apply_settings": "b698654871541258f97d58ada0f010b2d77b74829791566746cad619d3740a94", "test_msg_applysettings.py-test_apply_settings_passphrase": "fb38537b921f8064f7ea6e1a584e70a8be74968a3be6726b7d36cf57de0d7865", -"test_msg_backup_device.py::test_backup_bip39": "68d22900d8f9130bf325342b6b32b1785f28cefd256ea1cee7fd32109c817c23", -"test_msg_backup_device.py::test_backup_slip39_advanced": "aec6a663f8f75ff73d2f2377d12347b062b831ce0d5fcfe9d7f3c975a05aef32", -"test_msg_backup_device.py::test_backup_slip39_basic": "a4c44785873509bd4ebffa6ec82f2fe732349eb0bd23631dbc3876e956b567c7", +"test_msg_backup_device.py::test_backup_bip39": "dfdbd0ae6774177d43f2f11d026c4d8679dd994b508c1d850d9cfee5dd1118ac", +"test_msg_backup_device.py::test_backup_slip39_advanced": "244b31044a25e44847a6efb79ea4cb67c246a971bf828d46aa79d14151934a04", +"test_msg_backup_device.py::test_backup_slip39_basic": "81a56c307342e46ad261d6a6c6da0a75224af8e72f40da7c630c1e03c0643e1c", "test_msg_backup_device.py::test_interrupt_backup_fails": "225b3da1acac6e9a65106fcc4a01de8a44de035aedb4dcc21c09f439199fdf40", "test_msg_backup_device.py::test_no_backup_fails": "93039a9472cfc9058563bd56e4a3dbe2e41af64744a61f6ee3255a04bd3a9366", "test_msg_backup_device.py::test_no_backup_show_entropy_fails": "14fcdd2ded299ca099a35966cc9f21204b31de8d6bab9ec91cb64537bd70440c", @@ -24,16 +24,16 @@ "test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-0-Ae2tdPwUPEZLCq3sFv4wVYx": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-1-Ae2tdPwUPEZEY6pVJoyuNNd": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-2-Ae2tdPwUPEZ3gZD1QeUHvAq": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_cardano_get_address_slip39_basic.py::test_cardano_get_address[m-44'-1815'-0'-0-0-Ae2tdPwUPE": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", -"test_msg_cardano_get_address_slip39_basic.py::test_cardano_get_address[m-44'-1815'-0'-0-1-Ae2tdPwUPE": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", -"test_msg_cardano_get_address_slip39_basic.py::test_cardano_get_address[m-44'-1815'-0'-0-2-Ae2tdPwUPE": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", +"test_msg_cardano_get_address_slip39_basic.py::test_cardano_get_address[m-44'-1815'-0'-0-0-Ae2tdPwUPE": "d2d6aac0a4605f1a961580f0fc156e7b69b993f908906281313429fa3222f349", +"test_msg_cardano_get_address_slip39_basic.py::test_cardano_get_address[m-44'-1815'-0'-0-1-Ae2tdPwUPE": "d2d6aac0a4605f1a961580f0fc156e7b69b993f908906281313429fa3222f349", +"test_msg_cardano_get_address_slip39_basic.py::test_cardano_get_address[m-44'-1815'-0'-0-2-Ae2tdPwUPE": "d2d6aac0a4605f1a961580f0fc156e7b69b993f908906281313429fa3222f349", "test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-44'-1815'-0'-c0fce1839f1a84c4e7702": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-44'-1815'-1'-ea5dde31b9f551e08a5b6": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-44'-1815'-2'-076338cee5ab3dae19f06": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-44'-1815'-3'-5f769380dc6fd17a4e0f2": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_cardano_get_public_key_slip39_basic.py::test_cardano_get_public_key[m-44'-1815'-0'-0-0-bc04": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", -"test_msg_cardano_get_public_key_slip39_basic.py::test_cardano_get_public_key[m-44'-1815'-0'-0-1-24c4": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", -"test_msg_cardano_get_public_key_slip39_basic.py::test_cardano_get_public_key[m-44'-1815'-0'-0-2-831a": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", +"test_msg_cardano_get_public_key_slip39_basic.py::test_cardano_get_public_key[m-44'-1815'-0'-0-0-bc04": "d2d6aac0a4605f1a961580f0fc156e7b69b993f908906281313429fa3222f349", +"test_msg_cardano_get_public_key_slip39_basic.py::test_cardano_get_public_key[m-44'-1815'-0'-0-1-24c4": "d2d6aac0a4605f1a961580f0fc156e7b69b993f908906281313429fa3222f349", +"test_msg_cardano_get_public_key_slip39_basic.py::test_cardano_get_public_key[m-44'-1815'-0'-0-2-831a": "d2d6aac0a4605f1a961580f0fc156e7b69b993f908906281313429fa3222f349", "test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[1097911063-inputs2-outputs2-transactions2": "1a8eade03d9c0ecbbb01567e5e9d46187a2ffe7fa42d59eb711347a7fe3b5bb7", "test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-inputs0-outputs0-transactions0-": "07eceef966cb8069381dc5105b732bce6de8d207a1d27e56e8abddd57c307b22", "test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-inputs1-outputs1-transactions1-": "4c74546cacb2b99b9aeb6e134d99c0d1d6f0ba1818d1182364cfdb94b938ccc5", @@ -43,16 +43,16 @@ "test_msg_cardano_sign_tx_slip39_basic.py::test_cardano_sign_tx[1097911063-inputs2-outputs2-transacti": "e6813a7fd973f49b02ef28cee15deb48d0389d1cb2696194848e4d690281361f", "test_msg_cardano_sign_tx_slip39_basic.py::test_cardano_sign_tx[764824073-inputs0-outputs0-transactio": "bc88a23280c0234860ccbb5e96d5cc3a851e2f2f9928c400f6c0907c68172d39", "test_msg_cardano_sign_tx_slip39_basic.py::test_cardano_sign_tx[764824073-inputs1-outputs1-transactio": "6956bb359388186b4c127ae88f4d86527381caf8098f55b4fa05c343640c081f", -"test_msg_change_wipe_code_t2.py::test_set_pin_to_wipe_code": "b294554434cd454b6c53845256d3637bb83f0f7f96e39608c91018bb345f359e", -"test_msg_change_wipe_code_t2.py::test_set_remove_wipe_code": "74e8309c74ca9e599de91296cfcef63a744713acad3f3ea10b7b461eba69c30e", -"test_msg_change_wipe_code_t2.py::test_set_wipe_code_mismatch": "fdbd58fb31323cfc32a9a7684d4dd45a5bd2b098156f2abf9cca5b5a481ecd10", -"test_msg_change_wipe_code_t2.py::test_set_wipe_code_to_pin": "7b3599a5582b566c65c337b50fb6e38fc577b67310fc731ed756b7d143f4727d", -"test_msg_change_wipe_code_t2.py::test_wipe_code_activate": "b25571c82be5f00b1f0c0bca95154dbae6a6ac092b249b79f805a126e1580037", -"test_msg_changepin_t2.py::test_change_failed": "d39ba207aab689c53adde211dc8e189809ffe4f3a3c547bd8c0422ad20663dc2", -"test_msg_changepin_t2.py::test_change_pin": "9e43d9f05c96f61e5ba7723e7d3ceb26ed813fd5be95d2cfa7e01573ab87b531", -"test_msg_changepin_t2.py::test_remove_pin": "249a84500e2cac217f4b3c7cfbb4d4212781019191b21d1386f81317f6d699e7", -"test_msg_changepin_t2.py::test_set_failed": "5aa994de8e571d08512705b04e0c55b60411d7d23491f75688f16d255369363e", -"test_msg_changepin_t2.py::test_set_pin": "9e30570a9063a9820d423dd000d5ba70f30bf32de78cce9160643f60d5fe4eb2", +"test_msg_change_wipe_code_t2.py::test_set_pin_to_wipe_code": "9519b574e3f749810977ac55f851553f2051b51ef7799ffaa155410d89e1cd63", +"test_msg_change_wipe_code_t2.py::test_set_remove_wipe_code": "82c0d1acbf5ff344189761f808d3cf0e632726341231c20b2c0925ab5549b6af", +"test_msg_change_wipe_code_t2.py::test_set_wipe_code_mismatch": "1642d2d15920a3bb2c666b39beca9943ba39adb59289ebc40b97d7088a4d7abf", +"test_msg_change_wipe_code_t2.py::test_set_wipe_code_to_pin": "15289574ceb002b5161305b0595dcd20e437d1dd4e7561332e1aba4c1615e9ea", +"test_msg_change_wipe_code_t2.py::test_wipe_code_activate": "348ff6811029253d7d520b37a2d3ff219516a7401c8b65ab088c7a7d39bd8b2b", +"test_msg_changepin_t2.py::test_change_failed": "370c59da62a84aaefa242562c36a6facac89c7f819e37d1ae8cbe2c44a2de256", +"test_msg_changepin_t2.py::test_change_pin": "c42fca9bf8f3b4c330516d90231ae0cfa7419d83370be9cfcf6a81cca3f3b06c", +"test_msg_changepin_t2.py::test_remove_pin": "d049eaa6cd11e88b7af193b080cf868b62271266ad6f2973bfd82944b523741d", +"test_msg_changepin_t2.py::test_set_failed": "59beeec1a00817f664a5fd93234012588613aac93c45d53c27550fe5d0ef8380", +"test_msg_changepin_t2.py::test_set_pin": "3aafe16a451f928c9bfff2a3ff7e3c23ce4948c9a044ebd04834df045670183f", "test_msg_cipherkeyvalue.py-test_decrypt": "166d85b1bf11aeaeb5b93ef5d047b6f8910c28b8fce1d853e6912d89d7bfca2f", "test_msg_cipherkeyvalue.py-test_decrypt_badlen": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_msg_cipherkeyvalue.py-test_encrypt": "3a37e4004c87bc6df6a8fa7c93b6fe3e3524986914709fda2f9c99ba0ff69775", @@ -166,10 +166,10 @@ "test_msg_lisk_verifymessage.py-test_verify": "5e9cf05f6ccf10f697cae9f780042db934892e1d7c68fb2f19a40319a687ea50", "test_msg_lisk_verifymessage.py-test_verify_long": "26adab7e31f388e5b034a865f9c010d57e67fd855d44839d2f2600d8317bd98e", "test_msg_loaddevice.py-test_load_device_1": "114d7e07b00f8a9fc60e0888ce3e39f79805c577b99f4d25967fcf7cf6367664", -"test_msg_loaddevice.py-test_load_device_2": "35797984a814cf7e48ee827993269b43524bf91462c9c694ad7dd7d6bb7e6bbb", +"test_msg_loaddevice.py-test_load_device_2": "9947760ad56ea110b6f3937883c37701c866dd57b6c342806bd8e8b3aa889887", "test_msg_loaddevice.py-test_load_device_slip39_advanced": "1c6db0d592b1d22b3c9fce3ddab8a9fd138f11d83e5d4e64431a02bf4ffed605", "test_msg_loaddevice.py-test_load_device_slip39_basic": "1c6db0d592b1d22b3c9fce3ddab8a9fd138f11d83e5d4e64431a02bf4ffed605", -"test_msg_loaddevice.py-test_load_device_utf": "51fe82ea3081d06e69af1283bf9e32ea91ddb75ac87173a3d1f861c2b27a0635", +"test_msg_loaddevice.py-test_load_device_utf": "65d570c16a561831e017c447736749d855f0bf6e0c3cfe7a9b186493c01f187b", "test_msg_monero_getaddress.py-test_monero_getaddress": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_msg_monero_getwatchkey.py-test_monero_getwatchkey": "d77fa4d4322e145c41f1ce07526ff59f8b58d8854aeffaa5266e14cd572350e7", "test_msg_nem_getaddress.py-test_nem_getaddress": "e726f99401a20eb74c33d755cecea2a3f69b7ae5b541302677ee05f80f5aef19", @@ -200,7 +200,7 @@ "test_msg_recoverydevice_bip39_dryrun.py::test_uninitialized": "14fcdd2ded299ca099a35966cc9f21204b31de8d6bab9ec91cb64537bd70440c", "test_msg_recoverydevice_bip39_t2.py-test_already_initialized": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_msg_recoverydevice_bip39_t2.py-test_nopin_nopassphrase": "9769cde3e3951a76364973ade753682e2acc67d1633a9f982f0604b5702aa895", -"test_msg_recoverydevice_bip39_t2.py-test_pin_passphrase": "761b17d28d589eb4d4d6d73beb6061087828c469f60694893c94b301a37d34ad", +"test_msg_recoverydevice_bip39_t2.py-test_pin_passphrase": "d746fda5234be75cded559817d0fdfb4397ac757e9847d4dfbef44ac031381d1", "test_msg_recoverydevice_slip39_advanced.py::test_abort": "793dde7fb47e9c4ad36369be396da20332560f29083d7f9a2b0582173371c9ed", "test_msg_recoverydevice_slip39_advanced.py::test_extra_share_entered": "00a94e20b786346c45f987860b2465f299075d7c6de4971f42a4749e1cc8bfc0", "test_msg_recoverydevice_slip39_advanced.py::test_group_threshold_reached": "3b075a276c4e0d53fbc51ce1f29594bbd474d25f47c0f6a32caac41ba0ba2138", @@ -214,7 +214,7 @@ "test_msg_recoverydevice_slip39_basic.py::test_abort": "793dde7fb47e9c4ad36369be396da20332560f29083d7f9a2b0582173371c9ed", "test_msg_recoverydevice_slip39_basic.py::test_ask_word_number": "8e9d9fd75e17f6b44829ae2d7b0eb9e60b48577f975abc6d75116f8365241082", "test_msg_recoverydevice_slip39_basic.py::test_noabort": "d374a9b85c03a0cc1bbb59130e454406513fc35f4f43b968db4920414de1bb72", -"test_msg_recoverydevice_slip39_basic.py::test_recover_with_pin_passphrase": "3b49ab7cd70cf4e2576c842948029046bc6a686b3763983985e3d3965e8652d8", +"test_msg_recoverydevice_slip39_basic.py::test_recover_with_pin_passphrase": "ee0edd912b913d31b8308b64ceed7fa3e58f5b63c8a4d4d85f3651d4b121a202", "test_msg_recoverydevice_slip39_basic.py::test_same_share": "e6a54429fdbedea9efca9cbed736aada07f95f3b20f895f9c1c5ec056a2be014", "test_msg_recoverydevice_slip39_basic.py::test_secret[shares0-491b795b80fc21ccdf466c0fbc98c8fc]": "54581a91b55ab531b215cba61052fd77c505232c510f170080760605eb9b8c46", "test_msg_recoverydevice_slip39_basic.py::test_secret[shares1-b770e0da1363247652de97a39bdbf2463be0878": "f73bef254762d761db27df46bff62641a9d2ac0602c34fc4e465262bf26ed08f", @@ -224,11 +224,11 @@ "test_msg_recoverydevice_slip39_basic_dryrun.py::test_2of3_dryrun": "d84427489f691ecc222b62f83af3e97fa09097404dcba07772a43b5eb0c689e8", "test_msg_recoverydevice_slip39_basic_dryrun.py::test_2of3_invalid_seed_dryrun": "55f2dd6b4958659f071c3f57e06286f872ac38af4828f446a0f4e91c657dfccc", "test_msg_resetdevice_bip39_t2.py-test_already_initialized": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_resetdevice_bip39_t2.py-test_failed_pin": "a6ba803a7572dc8e29c821433a067c3a990185a8eb7b95d0106cd8f4b82e9da2", -"test_msg_resetdevice_bip39_t2.py-test_reset_device": "cbe1e439c2e9810ff6ee1031268810188b3cd07e029a7aa1d4b227e33707c727", -"test_msg_resetdevice_bip39_t2.py-test_reset_device_pin": "af378bff7e8fdf16324715f640e8b17bebd69f779758ab33cd7025c557a1d5c0", -"test_msg_resetdevice_slip39_advanced.py-test_reset_device_slip39_advanced": "7daabeb21a17a8c8e8adb800a8b6b0504e5d2e7cf0bbfcb14770fb33df1426a2", -"test_msg_resetdevice_slip39_basic.py-test_reset_device_slip39_basic": "1b56e3335876c01ec684839158bc0aa3a31aac03d2beef8d3ee63a0a441e3f1f", +"test_msg_resetdevice_bip39_t2.py-test_failed_pin": "a52f3479f7d8e14c7f89af9b305a50f3bd244fee28f17ebc0abedba701c32811", +"test_msg_resetdevice_bip39_t2.py-test_reset_device": "13d739a387a80aa96f533bb6f3b0f2ff12c7ba84608d0797ac603208d9aed796", +"test_msg_resetdevice_bip39_t2.py-test_reset_device_pin": "ed64165674816dfb408ead89abb8d1a5743f50f63933aceba6701149522c3866", +"test_msg_resetdevice_slip39_advanced.py-test_reset_device_slip39_advanced": "005622513c517610a4a1af529ef94760b4d0406b971f0b2f2557c93ce0dac8c9", +"test_msg_resetdevice_slip39_basic.py-test_reset_device_slip39_basic": "ebc62d2c49136c6ce75c8e026e51157d96a6f2440881dca0cdd8bc357e2c9354", "test_msg_ripple_get_address.py-test_ripple_get_address": "2bb7d7bf48f1218530b4d7045d48480cad6411e110df537551b2f80b342007f2", "test_msg_ripple_get_address.py-test_ripple_get_address_other": "2bb7d7bf48f1218530b4d7045d48480cad6411e110df537551b2f80b342007f2", "test_msg_ripple_sign_tx.py-test_ripple_sign_invalid_fee": "1c0ca08b857da6121f43cfb1632c7f7e1d189ef1fdb665db7ba2cdfa7a59ea7c", @@ -380,12 +380,12 @@ "test_passphrase_slip39_advanced.py::test_256bit_passphrase": "69b6b8b22c819e1282d7d2c14b31bf8d015c81ac05fe034540dbb11c8a20dbdb", "test_passphrase_slip39_basic.py::test_2of5_passphrase": "1e00b1a7840bc144b98b7bce26f74fc913a0abf9d1c500571d7803b6b2e0943c", "test_passphrase_slip39_basic.py::test_3of6_passphrase": "1e00b1a7840bc144b98b7bce26f74fc913a0abf9d1c500571d7803b6b2e0943c", -"test_reset_backup.py::test_skip_backup_manual[0-backup_flow_bip39]": "84ee487acfff37417e4d6862ab1b166be1165ae3eb4e19f828ae3363866edb39", -"test_reset_backup.py::test_skip_backup_manual[1-backup_flow_slip39_basic]": "55224486083a1268c76ffe1d2b47ca7fba29ca6955e3601dae9e2abd44fb9d62", -"test_reset_backup.py::test_skip_backup_manual[2-backup_flow_slip39_advanced]": "c40d7894875393bc093e23d02dab8ad668929767a542a27ff89c6dd5b7b963de", -"test_reset_backup.py::test_skip_backup_msg[0-backup_flow_bip39]": "68eb02ad320eee49d3a6aac891e5dc4c35ee09a1e98a2480c2cb1b672cbb0696", -"test_reset_backup.py::test_skip_backup_msg[1-backup_flow_slip39_basic]": "34674c8b9f4b6a836940fa347bacfd6d31dd681d41bf8e1ec129e9deef590588", -"test_reset_backup.py::test_skip_backup_msg[2-backup_flow_slip39_advanced]": "24c641cde46665219de99978c3e27088fd4ade55f2665535a95e7dce1f2b3148", +"test_reset_backup.py::test_skip_backup_manual[0-backup_flow_bip39]": "a73fcd9af54d3b55cc7c21e68b1637a6e29829bfd57d47b0503e67fa22e4106a", +"test_reset_backup.py::test_skip_backup_manual[1-backup_flow_slip39_basic]": "4b117541a58e7c209786a728ebc1406720cbd7d5a234f20a550b1ad3970b4e76", +"test_reset_backup.py::test_skip_backup_manual[2-backup_flow_slip39_advanced]": "2d1d55f25b21c2be2e7aeaa17eeb3a4b9ee4d7763c4a65ae2fd59da2d9dbbcc0", +"test_reset_backup.py::test_skip_backup_msg[0-backup_flow_bip39]": "f448153452b154c0be153e18340f22c853430267316229548ad8f4f47cf946f7", +"test_reset_backup.py::test_skip_backup_msg[1-backup_flow_slip39_basic]": "9fbcd0ab293de21ab3a24e0c29315d152c1bc5da4902ec054cc8800c0d855aa6", +"test_reset_backup.py::test_skip_backup_msg[2-backup_flow_slip39_advanced]": "0bafb944169d0ab608513deadba15411d1756ef2658253740c5d8792f4527e9d", "test_u2f_counter.py::test_u2f_counter": "7d96a4d262b9d8a2c1158ac1e5f0f7b2c3ed5f2ba9d6235a014320313f9488fe", "test_zerosig.py-test_one_zero_signature": "401aeaf7b2f565e2064a3c1a57a8ee3afe1e9bf251fba0874390685e7e0f178f", "test_zerosig.py-test_two_zero_signature": "7a01a057fb5dd3e6e38e7986875c5d07f0700bd80b519660e0b42973a9afd664"