mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-22 07:28:10 +00:00
commit
2c0504ad1c
@ -62,11 +62,12 @@ message ButtonRequest {
|
||||
ButtonRequest_PublicKey = 11;
|
||||
ButtonRequest_MnemonicWordCount = 12;
|
||||
ButtonRequest_MnemonicInput = 13;
|
||||
ButtonRequest_PassphraseType = 14;
|
||||
// ButtonRequest_PassphraseType = 14; DEPRECATED
|
||||
ButtonRequest_UnknownDerivationPath = 15;
|
||||
ButtonRequest_RecoveryHomepage = 16;
|
||||
ButtonRequest_Success = 17;
|
||||
ButtonRequest_Warning = 18;
|
||||
ButtonRequest_PassphraseEntry = 19;
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,31 +111,36 @@ message PinMatrixAck {
|
||||
* @next PassphraseAck
|
||||
*/
|
||||
message PassphraseRequest {
|
||||
optional bool on_device = 1; // passphrase is being entered on the device
|
||||
optional bool _on_device = 1 [deprecated=true]; // <2.3.0
|
||||
}
|
||||
|
||||
/**
|
||||
* Request: Send passphrase back
|
||||
* @next PassphraseStateRequest
|
||||
* @auxend
|
||||
*/
|
||||
message PassphraseAck {
|
||||
optional string passphrase = 1;
|
||||
optional bytes state = 2; // expected device state
|
||||
optional bytes _state = 2 [deprecated=true]; // <2.3.0
|
||||
optional bool on_device = 3; // user wants to enter passphrase on the device
|
||||
}
|
||||
|
||||
/**
|
||||
* Response: Device awaits passphrase state
|
||||
* @next PassphraseStateAck
|
||||
* Deprecated in 2.3.0
|
||||
* @next Deprecated_PassphraseStateAck
|
||||
*/
|
||||
message PassphraseStateRequest {
|
||||
message Deprecated_PassphraseStateRequest {
|
||||
option deprecated = true;
|
||||
optional bytes state = 1; // actual device state
|
||||
}
|
||||
|
||||
/**
|
||||
* Request: Send passphrase state back
|
||||
* Deprecated in 2.3.0
|
||||
* @auxend
|
||||
*/
|
||||
message PassphraseStateAck {
|
||||
message Deprecated_PassphraseStateAck {
|
||||
option deprecated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -20,8 +20,7 @@ enum BackupType {
|
||||
* @next Features
|
||||
*/
|
||||
message Initialize {
|
||||
optional bytes state = 1; // assumed device state, clear session if set and different
|
||||
optional bool skip_passphrase = 2; // this session should always assume empty passphrase
|
||||
optional bytes session_id = 1; // assumed device session id; Trezor clears caches if it is different or empty
|
||||
}
|
||||
|
||||
/**
|
||||
@ -52,7 +51,7 @@ message Features {
|
||||
optional bytes bootloader_hash = 14; // hash of the bootloader
|
||||
optional bool imported = 15; // was storage imported from an external source?
|
||||
optional bool pin_cached = 16; // is PIN already cached in session?
|
||||
optional bool passphrase_cached = 17; // is passphrase already cached in session?
|
||||
// optional bool passphrase_cached = 17; // is passphrase already cached in session? DEPRECATED
|
||||
optional bool firmware_present = 18; // is valid firmware loaded?
|
||||
optional bool needs_backup = 19; // does storage need backup? (equals to Storage.needs_backup)
|
||||
optional uint32 flags = 20; // device flags (equals to Storage.flags)
|
||||
@ -83,11 +82,14 @@ message Features {
|
||||
Capability_U2F = 14;
|
||||
Capability_Shamir = 15;
|
||||
Capability_ShamirGroups = 16;
|
||||
Capability_PassphraseEntry = 17; // the device is capable of passphrase entry directly on the device
|
||||
}
|
||||
optional BackupType backup_type = 31; // type of device backup (BIP-39 / SLIP-39 basic / SLIP-39 advanced)
|
||||
optional bool sd_card_present = 32; // is SD card present
|
||||
optional bool sd_protection = 33; // is SD Protect enabled
|
||||
optional bool wipe_code_protection = 34; // is wipe code protection enabled
|
||||
optional bytes session_id = 35;
|
||||
optional bool passphrase_always_on_device = 36; // device enforces passphrase entry on Trezor
|
||||
}
|
||||
|
||||
/**
|
||||
@ -109,17 +111,10 @@ message ApplySettings {
|
||||
optional string label = 2;
|
||||
optional bool use_passphrase = 3;
|
||||
optional bytes homescreen = 4;
|
||||
optional PassphraseSourceType passphrase_source = 5;
|
||||
// optional PassphraseSourceType passphrase_source = 5; DEPRECATED
|
||||
optional uint32 auto_lock_delay_ms = 6;
|
||||
optional uint32 display_rotation = 7; // in degrees from North
|
||||
/**
|
||||
* Structure representing passphrase source
|
||||
*/
|
||||
enum PassphraseSourceType {
|
||||
ASK = 0;
|
||||
DEVICE = 1;
|
||||
HOST = 2;
|
||||
}
|
||||
optional bool passphrase_always_on_device = 8; // do not prompt for passphrase, enforce device entry
|
||||
}
|
||||
|
||||
/**
|
||||
@ -178,8 +173,6 @@ message SdProtect {
|
||||
message Ping {
|
||||
optional string message = 1; // message to send back in Success message
|
||||
optional bool button_protection = 2; // ask for button press
|
||||
optional bool pin_protection = 3; // ask for PIN if set in device
|
||||
optional bool passphrase_protection = 4; // ask for passphrase if set in device
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -54,8 +54,6 @@ enum MessageType {
|
||||
MessageType_EntropyAck = 36 [(wire_in) = true];
|
||||
MessageType_PassphraseRequest = 41 [(wire_out) = true];
|
||||
MessageType_PassphraseAck = 42 [(wire_in) = true, (wire_tiny) = true, (wire_no_fsm) = true];
|
||||
MessageType_PassphraseStateRequest = 77 [(wire_out) = true];
|
||||
MessageType_PassphraseStateAck = 78 [(wire_in) = true, (wire_tiny) = true, (wire_no_fsm) = true];
|
||||
MessageType_RecoveryDevice = 45 [(wire_in) = true];
|
||||
MessageType_WordRequest = 46 [(wire_out) = true];
|
||||
MessageType_WordAck = 47 [(wire_in) = true];
|
||||
@ -66,6 +64,11 @@ enum MessageType {
|
||||
MessageType_NextU2FCounter = 81 [(wire_out) = true];
|
||||
MessageType_ChangeWipeCode = 82 [(wire_in) = true];
|
||||
|
||||
// Deprecated messages, kept for protobuf compatibility.
|
||||
// Both are marked wire_out so that we don't need to implement incoming handler for legacy
|
||||
MessageType_Deprecated_PassphraseStateRequest = 77 [deprecated = true];
|
||||
MessageType_Deprecated_PassphraseStateAck = 78 [deprecated = true];
|
||||
|
||||
// Bootloader
|
||||
MessageType_FirmwareErase = 6 [(wire_in) = true, (wire_bootloader) = true];
|
||||
MessageType_FirmwareUpload = 7 [(wire_in) = true, (wire_bootloader) = true];
|
||||
|
@ -2,6 +2,9 @@ Version 2.x.x [not yet released]
|
||||
* SD card protection
|
||||
* Upgrade MicroPython to 1.12
|
||||
|
||||
Version 2.3.0 [not yet released]
|
||||
* Passphrase redesign
|
||||
|
||||
Version 2.2.0 [Jan 2020]
|
||||
* Remove unused ButtonRequest.data field
|
||||
* Rework Recovery persistence internally
|
||||
|
@ -1,6 +1,6 @@
|
||||
#define VERSION_MAJOR 2
|
||||
#define VERSION_MINOR 2
|
||||
#define VERSION_PATCH 1
|
||||
#define VERSION_MINOR 3
|
||||
#define VERSION_PATCH 0
|
||||
#define VERSION_BUILD 0
|
||||
|
||||
#define FIX_VERSION_MAJOR 2
|
||||
|
@ -1,11 +1,11 @@
|
||||
import storage
|
||||
import storage.cache
|
||||
from storage import cache
|
||||
from trezor import wire
|
||||
from trezor.crypto import bip32
|
||||
|
||||
from apps.cardano import CURVE, SEED_NAMESPACE
|
||||
from apps.common import mnemonic
|
||||
from apps.common.request_passphrase import protect_by_passphrase
|
||||
from apps.common.passphrase import get as get_passphrase
|
||||
|
||||
|
||||
class Keychain:
|
||||
@ -30,34 +30,27 @@ class Keychain:
|
||||
return node
|
||||
|
||||
|
||||
async def _get_passphrase(ctx: wire.Context) -> bytes:
|
||||
passphrase = storage.cache.get_passphrase()
|
||||
if passphrase is None:
|
||||
passphrase = await protect_by_passphrase(ctx)
|
||||
storage.cache.set_passphrase(passphrase)
|
||||
|
||||
return passphrase
|
||||
|
||||
|
||||
async def get_keychain(ctx: wire.Context) -> Keychain:
|
||||
root = cache.get(cache.APP_CARDANO_ROOT)
|
||||
|
||||
if not storage.is_initialized():
|
||||
raise wire.NotInitialized("Device is not initialized")
|
||||
|
||||
if mnemonic.is_bip39():
|
||||
# derive the root node from mnemonic and passphrase
|
||||
passphrase = await _get_passphrase(ctx)
|
||||
root = bip32.from_mnemonic_cardano(mnemonic.get_secret().decode(), passphrase)
|
||||
else:
|
||||
seed = storage.cache.get_seed()
|
||||
if seed is None:
|
||||
passphrase = await _get_passphrase(ctx)
|
||||
if root is None:
|
||||
passphrase = await get_passphrase(ctx)
|
||||
if mnemonic.is_bip39():
|
||||
# derive the root node from mnemonic and passphrase
|
||||
root = bip32.from_mnemonic_cardano(
|
||||
mnemonic.get_secret().decode(), passphrase
|
||||
)
|
||||
else:
|
||||
seed = mnemonic.get_seed(passphrase)
|
||||
storage.cache.set_seed(seed)
|
||||
root = bip32.from_seed(seed, "ed25519 cardano seed")
|
||||
root = bip32.from_seed(seed, "ed25519 cardano seed")
|
||||
|
||||
# derive the namespaced root node
|
||||
for i in SEED_NAMESPACE:
|
||||
root.derive_cardano(i)
|
||||
# derive the namespaced root node
|
||||
for i in SEED_NAMESPACE:
|
||||
root.derive_cardano(i)
|
||||
storage.cache.set(cache.APP_CARDANO_ROOT, root)
|
||||
|
||||
keychain = Keychain(SEED_NAMESPACE, root)
|
||||
return keychain
|
||||
|
@ -2,6 +2,7 @@ import storage.device
|
||||
from trezor import ui, utils, workflow
|
||||
from trezor.crypto import bip39, slip39
|
||||
from trezor.messages import BackupType
|
||||
from trezor.ui.text import Text
|
||||
|
||||
if False:
|
||||
from typing import Optional, Tuple
|
||||
@ -59,11 +60,8 @@ def _start_progress() -> None:
|
||||
# should make sure that no other layout is running. At this point, only
|
||||
# the homescreen should be on, so shut it down.
|
||||
workflow.kill_default()
|
||||
ui.backlight_fade(ui.BACKLIGHT_DIM)
|
||||
ui.display.clear()
|
||||
ui.header("Please wait")
|
||||
ui.refresh()
|
||||
ui.backlight_fade(ui.BACKLIGHT_NORMAL)
|
||||
t = Text("Please wait", ui.ICON_CONFIG)
|
||||
ui.draw_simple(t)
|
||||
|
||||
|
||||
def _render_progress(progress: int, total: int) -> None:
|
||||
|
78
core/src/apps/common/passphrase.py
Normal file
78
core/src/apps/common/passphrase.py
Normal file
@ -0,0 +1,78 @@
|
||||
from micropython import const
|
||||
|
||||
import storage.device
|
||||
from trezor import wire
|
||||
from trezor.messages import ButtonRequestType
|
||||
from trezor.messages.ButtonAck import ButtonAck
|
||||
from trezor.messages.ButtonRequest import ButtonRequest
|
||||
from trezor.messages.PassphraseAck import PassphraseAck
|
||||
from trezor.messages.PassphraseRequest import PassphraseRequest
|
||||
from trezor.ui import ICON_CONFIG, draw_simple
|
||||
from trezor.ui.passphrase import CANCELLED, PassphraseKeyboard
|
||||
from trezor.ui.text import Text
|
||||
|
||||
if __debug__:
|
||||
from apps.debug import input_signal
|
||||
|
||||
_MAX_PASSPHRASE_LEN = const(50)
|
||||
|
||||
|
||||
def is_enabled() -> bool:
|
||||
return storage.device.is_passphrase_enabled()
|
||||
|
||||
|
||||
async def get(ctx: wire.Context) -> str:
|
||||
if is_enabled():
|
||||
return await _request_from_user(ctx)
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
async def _request_from_user(ctx: wire.Context) -> str:
|
||||
if storage.device.get_passphrase_always_on_device():
|
||||
passphrase = await _request_on_device(ctx)
|
||||
else:
|
||||
passphrase = await _request_on_host(ctx)
|
||||
if len(passphrase) > _MAX_PASSPHRASE_LEN:
|
||||
raise wire.DataError("Maximum passphrase length is %d" % _MAX_PASSPHRASE_LEN)
|
||||
|
||||
return passphrase
|
||||
|
||||
|
||||
async def _request_on_host(ctx: wire.Context) -> str:
|
||||
_entry_dialog()
|
||||
|
||||
request = PassphraseRequest()
|
||||
ack = await ctx.call(request, PassphraseAck)
|
||||
if ack.on_device:
|
||||
if ack.passphrase is not None:
|
||||
raise wire.DataError("Passphrase provided when it should not be")
|
||||
return await _request_on_device(ctx)
|
||||
|
||||
if ack.passphrase is None:
|
||||
raise wire.DataError(
|
||||
"Passphrase not provided and on_device is False. Use empty string to set an empty passphrase."
|
||||
)
|
||||
return ack.passphrase
|
||||
|
||||
|
||||
async def _request_on_device(ctx: wire.Context) -> str:
|
||||
await ctx.call(ButtonRequest(code=ButtonRequestType.PassphraseEntry), ButtonAck)
|
||||
|
||||
keyboard = PassphraseKeyboard("Enter passphrase", _MAX_PASSPHRASE_LEN)
|
||||
if __debug__:
|
||||
passphrase = await ctx.wait(keyboard, input_signal())
|
||||
else:
|
||||
passphrase = await ctx.wait(keyboard)
|
||||
if passphrase is CANCELLED:
|
||||
raise wire.ActionCancelled("Passphrase entry cancelled")
|
||||
|
||||
assert isinstance(passphrase, str)
|
||||
|
||||
return passphrase
|
||||
|
||||
|
||||
def _entry_dialog() -> None:
|
||||
text = Text("Passphrase entry", ICON_CONFIG)
|
||||
text.normal("Please type your", "passphrase on the", "connected host.")
|
||||
draw_simple(text)
|
@ -1,86 +0,0 @@
|
||||
from micropython import const
|
||||
|
||||
import storage.device
|
||||
from storage import cache
|
||||
from trezor import ui, wire
|
||||
from trezor.messages import ButtonRequestType, PassphraseSourceType
|
||||
from trezor.messages.ButtonAck import ButtonAck
|
||||
from trezor.messages.ButtonRequest import ButtonRequest
|
||||
from trezor.messages.PassphraseAck import PassphraseAck
|
||||
from trezor.messages.PassphraseRequest import PassphraseRequest
|
||||
from trezor.messages.PassphraseStateAck import PassphraseStateAck
|
||||
from trezor.messages.PassphraseStateRequest import PassphraseStateRequest
|
||||
from trezor.ui.passphrase import CANCELLED, PassphraseKeyboard, PassphraseSource
|
||||
from trezor.ui.popup import Popup
|
||||
from trezor.ui.text import Text
|
||||
|
||||
if __debug__:
|
||||
from apps.debug import input_signal
|
||||
|
||||
_MAX_PASSPHRASE_LEN = const(50)
|
||||
|
||||
|
||||
async def protect_by_passphrase(ctx: wire.Context) -> str:
|
||||
if storage.device.has_passphrase():
|
||||
return await request_passphrase(ctx)
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
async def request_passphrase(ctx: wire.Context) -> str:
|
||||
source = storage.device.get_passphrase_source()
|
||||
if source == PassphraseSourceType.ASK:
|
||||
source = await request_passphrase_source(ctx)
|
||||
passphrase = await request_passphrase_ack(
|
||||
ctx, source == PassphraseSourceType.DEVICE
|
||||
)
|
||||
if len(passphrase) > _MAX_PASSPHRASE_LEN:
|
||||
raise wire.DataError("Maximum passphrase length is %d" % _MAX_PASSPHRASE_LEN)
|
||||
return passphrase
|
||||
|
||||
|
||||
async def request_passphrase_source(ctx: wire.Context) -> int:
|
||||
req = ButtonRequest(code=ButtonRequestType.PassphraseType)
|
||||
await ctx.call(req, ButtonAck)
|
||||
|
||||
text = Text("Enter passphrase", ui.ICON_CONFIG)
|
||||
text.normal("Where do you want to", "enter your passphrase?")
|
||||
source = PassphraseSource(text)
|
||||
|
||||
response = await ctx.wait(source)
|
||||
assert isinstance(response, int)
|
||||
return response
|
||||
|
||||
|
||||
async def request_passphrase_ack(ctx: wire.Context, on_device: bool) -> str:
|
||||
if not on_device:
|
||||
text = Text("Passphrase entry", ui.ICON_CONFIG)
|
||||
text.normal("Please type your", "passphrase on the", "connected host.")
|
||||
await Popup(text)
|
||||
|
||||
passphrase_request = PassphraseRequest(on_device=on_device)
|
||||
ack = await ctx.call(passphrase_request, PassphraseAck)
|
||||
|
||||
if on_device:
|
||||
if ack.passphrase is not None:
|
||||
raise wire.ProcessError("Passphrase provided when it should not be")
|
||||
|
||||
keyboard = PassphraseKeyboard("Enter passphrase", _MAX_PASSPHRASE_LEN)
|
||||
if __debug__:
|
||||
passphrase = await ctx.wait(keyboard, input_signal())
|
||||
else:
|
||||
passphrase = await ctx.wait(keyboard)
|
||||
if passphrase is CANCELLED:
|
||||
raise wire.ActionCancelled("Passphrase cancelled")
|
||||
else:
|
||||
if ack.passphrase is None:
|
||||
raise wire.ProcessError("Passphrase not provided")
|
||||
passphrase = ack.passphrase
|
||||
|
||||
assert isinstance(passphrase, str)
|
||||
|
||||
state = cache.get_state(prev_state=ack.state, passphrase=passphrase)
|
||||
state_request = PassphraseStateRequest(state=state)
|
||||
await ctx.call(state_request, PassphraseStateAck)
|
||||
|
||||
return passphrase
|
@ -1,11 +1,11 @@
|
||||
import storage
|
||||
import storage.cache
|
||||
from storage import cache
|
||||
from trezor import wire
|
||||
from trezor.crypto import bip32, hashlib, hmac
|
||||
from trezor.crypto.curve import secp256k1
|
||||
|
||||
from apps.common import HARDENED, mnemonic
|
||||
from apps.common.request_passphrase import protect_by_passphrase
|
||||
from apps.common.passphrase import get as get_passphrase
|
||||
|
||||
if False:
|
||||
from typing import List, Union
|
||||
@ -112,14 +112,11 @@ class Keychain:
|
||||
async def get_keychain(ctx: wire.Context, namespaces: list) -> Keychain:
|
||||
if not storage.is_initialized():
|
||||
raise wire.NotInitialized("Device is not initialized")
|
||||
seed = storage.cache.get_seed()
|
||||
seed = cache.get(cache.APP_COMMON_SEED)
|
||||
if seed is None:
|
||||
passphrase = storage.cache.get_passphrase()
|
||||
if passphrase is None:
|
||||
passphrase = await protect_by_passphrase(ctx)
|
||||
storage.cache.set_passphrase(passphrase)
|
||||
passphrase = await get_passphrase(ctx)
|
||||
seed = mnemonic.get_seed(passphrase)
|
||||
storage.cache.set_seed(seed)
|
||||
cache.set(cache.APP_COMMON_SEED, seed)
|
||||
keychain = Keychain(seed, namespaces)
|
||||
return keychain
|
||||
|
||||
@ -129,10 +126,10 @@ def derive_node_without_passphrase(
|
||||
) -> bip32.HDNode:
|
||||
if not storage.is_initialized():
|
||||
raise Exception("Device is not initialized")
|
||||
seed = storage.cache.get_seed_without_passphrase()
|
||||
seed = cache.get(cache.APP_COMMON_SEED_WITHOUT_PASSPHRASE)
|
||||
if seed is None:
|
||||
seed = mnemonic.get_seed(progress_bar=False)
|
||||
storage.cache.set_seed_without_passphrase(seed)
|
||||
cache.set(cache.APP_COMMON_SEED_WITHOUT_PASSPHRASE, seed)
|
||||
node = bip32.from_seed(seed, curve_name)
|
||||
node.derive_path(path)
|
||||
return node
|
||||
@ -141,10 +138,10 @@ def derive_node_without_passphrase(
|
||||
def derive_slip21_node_without_passphrase(path: list) -> Slip21Node:
|
||||
if not storage.is_initialized():
|
||||
raise Exception("Device is not initialized")
|
||||
seed = storage.cache.get_seed_without_passphrase()
|
||||
seed = cache.get(cache.APP_COMMON_SEED_WITHOUT_PASSPHRASE)
|
||||
if seed is None:
|
||||
seed = mnemonic.get_seed(progress_bar=False)
|
||||
storage.cache.set_seed_without_passphrase(seed)
|
||||
cache.set(cache.APP_COMMON_SEED_WITHOUT_PASSPHRASE, seed)
|
||||
node = Slip21Node(seed)
|
||||
node.derive_path(path)
|
||||
return node
|
||||
|
@ -97,13 +97,12 @@ if __debug__:
|
||||
ctx: wire.Context, msg: DebugLinkGetState
|
||||
) -> DebugLinkState:
|
||||
from trezor.messages.DebugLinkState import DebugLinkState
|
||||
from storage.device import has_passphrase
|
||||
from apps.common import mnemonic
|
||||
from apps.common import mnemonic, passphrase
|
||||
|
||||
m = DebugLinkState()
|
||||
m.mnemonic_secret = mnemonic.get_secret()
|
||||
m.mnemonic_type = mnemonic.get_type()
|
||||
m.passphrase_protection = has_passphrase()
|
||||
m.passphrase_protection = passphrase.is_enabled()
|
||||
m.reset_entropy = reset_internal_entropy
|
||||
|
||||
if msg.wait_layout or current_content is None:
|
||||
|
@ -34,8 +34,7 @@ def get_features() -> Features:
|
||||
f.initialized = storage.is_initialized()
|
||||
f.pin_protection = config.has_pin()
|
||||
f.pin_cached = config.has_pin()
|
||||
f.passphrase_protection = storage.device.has_passphrase()
|
||||
f.passphrase_cached = cache.has_passphrase()
|
||||
f.passphrase_protection = storage.device.is_passphrase_enabled()
|
||||
f.needs_backup = storage.device.needs_backup()
|
||||
f.unfinished_backup = storage.device.unfinished_backup()
|
||||
f.no_backup = storage.device.no_backup()
|
||||
@ -48,6 +47,7 @@ def get_features() -> Features:
|
||||
Capability.Crypto,
|
||||
Capability.Shamir,
|
||||
Capability.ShamirGroups,
|
||||
Capability.PassphraseEntry,
|
||||
]
|
||||
else:
|
||||
f.capabilities = [
|
||||
@ -67,18 +67,19 @@ def get_features() -> Features:
|
||||
Capability.U2F,
|
||||
Capability.Shamir,
|
||||
Capability.ShamirGroups,
|
||||
Capability.PassphraseEntry,
|
||||
]
|
||||
f.sd_card_present = io.SDCard().present()
|
||||
f.sd_protection = storage.sd_salt.is_enabled()
|
||||
f.wipe_code_protection = config.has_wipe_code()
|
||||
f.session_id = cache.get_session_id()
|
||||
f.passphrase_always_on_device = storage.device.get_passphrase_always_on_device()
|
||||
return f
|
||||
|
||||
|
||||
async def handle_Initialize(ctx: wire.Context, msg: Initialize) -> Features:
|
||||
if msg.state is None or msg.state != cache.get_state(prev_state=bytes(msg.state)):
|
||||
if msg.session_id is None or msg.session_id != cache.get_session_id():
|
||||
cache.clear()
|
||||
if msg.skip_passphrase:
|
||||
cache.set_passphrase("")
|
||||
return get_features()
|
||||
|
||||
|
||||
@ -91,8 +92,12 @@ async def handle_Cancel(ctx: wire.Context, msg: Cancel) -> NoReturn:
|
||||
|
||||
|
||||
async def handle_ClearSession(ctx: wire.Context, msg: ClearSession) -> Success:
|
||||
cache.clear(keep_passphrase=True)
|
||||
return Success(message="Session cleared")
|
||||
"""
|
||||
This is currently a no-op on T. This should be called LockSession/LockDevice
|
||||
and lock the device. In other words the cache should stay but the PIN should
|
||||
be forgotten and required again.
|
||||
"""
|
||||
return Success()
|
||||
|
||||
|
||||
async def handle_Ping(ctx: wire.Context, msg: Ping) -> Success:
|
||||
@ -102,10 +107,6 @@ async def handle_Ping(ctx: wire.Context, msg: Ping) -> Success:
|
||||
from trezor.ui.text import Text
|
||||
|
||||
await require_confirm(ctx, Text("Confirm"), ProtectCall)
|
||||
if msg.passphrase_protection:
|
||||
from apps.common.request_passphrase import protect_by_passphrase
|
||||
|
||||
await protect_by_passphrase(ctx)
|
||||
return Success(message=msg.message)
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import storage.device
|
||||
from trezor import ui, wire
|
||||
from trezor.messages import ButtonRequestType, PassphraseSourceType
|
||||
from trezor.messages import ButtonRequestType
|
||||
from trezor.messages.Success import Success
|
||||
from trezor.ui.text import Text
|
||||
|
||||
@ -12,7 +12,7 @@ async def apply_settings(ctx, msg):
|
||||
msg.homescreen is None
|
||||
and msg.label is None
|
||||
and msg.use_passphrase is None
|
||||
and msg.passphrase_source is None
|
||||
and msg.passphrase_always_on_device is None
|
||||
and msg.display_rotation is None
|
||||
):
|
||||
raise wire.ProcessError("No setting provided")
|
||||
@ -28,8 +28,10 @@ async def apply_settings(ctx, msg):
|
||||
if msg.use_passphrase is not None:
|
||||
await require_confirm_change_passphrase(ctx, msg.use_passphrase)
|
||||
|
||||
if msg.passphrase_source is not None:
|
||||
await require_confirm_change_passphrase_source(ctx, msg.passphrase_source)
|
||||
if msg.passphrase_always_on_device is not None:
|
||||
await require_confirm_change_passphrase_source(
|
||||
ctx, msg.passphrase_always_on_device
|
||||
)
|
||||
|
||||
if msg.display_rotation is not None:
|
||||
await require_confirm_change_display_rotation(ctx, msg.display_rotation)
|
||||
@ -38,7 +40,7 @@ async def apply_settings(ctx, msg):
|
||||
label=msg.label,
|
||||
use_passphrase=msg.use_passphrase,
|
||||
homescreen=msg.homescreen,
|
||||
passphrase_source=msg.passphrase_source,
|
||||
passphrase_always_on_device=msg.passphrase_always_on_device,
|
||||
display_rotation=msg.display_rotation,
|
||||
)
|
||||
|
||||
@ -69,16 +71,16 @@ async def require_confirm_change_passphrase(ctx, use):
|
||||
await require_confirm(ctx, text, ButtonRequestType.ProtectCall)
|
||||
|
||||
|
||||
async def require_confirm_change_passphrase_source(ctx, source):
|
||||
if source == PassphraseSourceType.DEVICE:
|
||||
desc = "ON DEVICE"
|
||||
elif source == PassphraseSourceType.HOST:
|
||||
desc = "ON HOST"
|
||||
else:
|
||||
desc = "ASK"
|
||||
async def require_confirm_change_passphrase_source(
|
||||
ctx, passphrase_always_on_device: bool
|
||||
):
|
||||
text = Text("Passphrase source", ui.ICON_CONFIG)
|
||||
text.normal("Do you really want to", "change the passphrase", "source to")
|
||||
text.bold("ALWAYS %s?" % desc)
|
||||
if passphrase_always_on_device:
|
||||
text.normal(
|
||||
"Do you really want to", "enter passphrase always", "on the device?"
|
||||
)
|
||||
else:
|
||||
text.normal("Do you want to revoke", "the passphrase on device", "setting?")
|
||||
await require_confirm(ctx, text, ButtonRequestType.ProtectCall)
|
||||
|
||||
|
||||
|
@ -28,7 +28,7 @@ async def recovery_homescreen() -> None:
|
||||
return
|
||||
|
||||
# recovery process does not communicate on the wire
|
||||
ctx = wire.DummyContext()
|
||||
ctx = wire.DUMMY_CONTEXT
|
||||
await recovery_process(ctx)
|
||||
|
||||
|
||||
|
@ -4,7 +4,6 @@ from trezor.messages import MessageType
|
||||
from apps.common import HARDENED
|
||||
|
||||
CURVE = "ed25519"
|
||||
_LIVE_REFRESH_TOKEN = None # live-refresh permission token
|
||||
|
||||
|
||||
def boot() -> None:
|
||||
@ -20,11 +19,3 @@ def boot() -> None:
|
||||
|
||||
if __debug__ and hasattr(MessageType, "DebugMoneroDiagRequest"):
|
||||
wire.add(MessageType.DebugMoneroDiagRequest, __name__, "diag")
|
||||
|
||||
|
||||
def live_refresh_token(token: bytes = None) -> None:
|
||||
global _LIVE_REFRESH_TOKEN
|
||||
if token is None:
|
||||
return _LIVE_REFRESH_TOKEN
|
||||
else:
|
||||
_LIVE_REFRESH_TOKEN = token
|
||||
|
@ -1,6 +1,6 @@
|
||||
import gc
|
||||
|
||||
from storage.cache import get_passphrase_fprint
|
||||
import storage.cache
|
||||
from trezor import log
|
||||
from trezor.messages import MessageType
|
||||
from trezor.messages.MoneroLiveRefreshFinalAck import MoneroLiveRefreshFinalAck
|
||||
@ -10,7 +10,7 @@ from trezor.messages.MoneroLiveRefreshStepAck import MoneroLiveRefreshStepAck
|
||||
from trezor.messages.MoneroLiveRefreshStepRequest import MoneroLiveRefreshStepRequest
|
||||
|
||||
from apps.common import paths
|
||||
from apps.monero import CURVE, live_refresh_token, misc
|
||||
from apps.monero import CURVE, misc
|
||||
from apps.monero.layout import confirms
|
||||
from apps.monero.xmr import crypto, key_image, monero
|
||||
from apps.monero.xmr.crypto import chacha_poly
|
||||
@ -49,10 +49,9 @@ async def _init_step(
|
||||
ctx, misc.validate_full_path, keychain, msg.address_n, CURVE
|
||||
)
|
||||
|
||||
passphrase_fprint = get_passphrase_fprint()
|
||||
if live_refresh_token() != passphrase_fprint:
|
||||
if not storage.cache.get(storage.cache.APP_MONERO_LIVE_REFRESH):
|
||||
await confirms.require_confirm_live_refresh(ctx)
|
||||
live_refresh_token(passphrase_fprint)
|
||||
storage.cache.set(storage.cache.APP_MONERO_LIVE_REFRESH, True)
|
||||
|
||||
s.creds = misc.get_creds(keychain, msg.address_n, msg.network_type)
|
||||
|
||||
|
@ -1,75 +1,36 @@
|
||||
from storage.device import get_device_id
|
||||
from trezor.crypto import hashlib, hmac, random
|
||||
from trezor.crypto import random
|
||||
|
||||
if False:
|
||||
from typing import Optional
|
||||
|
||||
_cached_seed = None # type: Optional[bytes]
|
||||
_cached_seed_without_passphrase = None # type: Optional[bytes]
|
||||
_cached_passphrase = None # type: Optional[str]
|
||||
_cached_passphrase_fprint = b"\x00\x00\x00\x00" # type: bytes
|
||||
APP_COMMON_SEED = 0
|
||||
APP_COMMON_SEED_WITHOUT_PASSPHRASE = 1
|
||||
APP_CARDANO_ROOT = 2
|
||||
APP_MONERO_LIVE_REFRESH = 3
|
||||
|
||||
_cache_session_id = None # type: Optional[bytes]
|
||||
_cache = {}
|
||||
|
||||
if False:
|
||||
from typing import Any
|
||||
|
||||
|
||||
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 get_session_id() -> bytes:
|
||||
global _cache_session_id
|
||||
if not _cache_session_id:
|
||||
_cache_session_id = random.bytes(32)
|
||||
return _cache_session_id
|
||||
|
||||
|
||||
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 set(key: int, value: Any) -> None:
|
||||
_cache[key] = value
|
||||
|
||||
|
||||
def get_seed() -> Optional[bytes]:
|
||||
return _cached_seed
|
||||
def get(key: int) -> Any:
|
||||
return _cache.get(key)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def set_seed(seed: Optional[bytes]) -> None:
|
||||
global _cached_seed
|
||||
_cached_seed = seed
|
||||
|
||||
|
||||
def set_seed_without_passphrase(seed: Optional[bytes]) -> None:
|
||||
global _cached_seed_without_passphrase
|
||||
_cached_seed_without_passphrase = seed
|
||||
|
||||
|
||||
def set_passphrase(passphrase: Optional[str]) -> None:
|
||||
global _cached_passphrase, _cached_passphrase_fprint
|
||||
_cached_passphrase = passphrase
|
||||
_cached_passphrase_fprint = _compute_state(b"FPRINT", passphrase or "")[:4]
|
||||
|
||||
|
||||
def clear(keep_passphrase: bool = False) -> None:
|
||||
set_seed(None)
|
||||
set_seed_without_passphrase(None)
|
||||
if not keep_passphrase:
|
||||
set_passphrase(None)
|
||||
def clear() -> None:
|
||||
global _cache_session_id
|
||||
_cache_session_id = None
|
||||
_cache.clear()
|
||||
|
@ -24,7 +24,7 @@ _HOMESCREEN = const(0x06) # bytes
|
||||
_NEEDS_BACKUP = const(0x07) # bool (0x01 or empty)
|
||||
_FLAGS = const(0x08) # int
|
||||
U2F_COUNTER = const(0x09) # int
|
||||
_PASSPHRASE_SOURCE = const(0x0A) # int
|
||||
_PASSPHRASE_ALWAYS_ON_DEVICE = const(0x0A) # bool (0x01 or empty)
|
||||
_UNFINISHED_BACKUP = const(0x0B) # bool (0x01 or empty)
|
||||
_AUTOLOCK_DELAY_MS = const(0x0C) # int
|
||||
_NO_BACKUP = const(0x0D) # bool (0x01 or empty)
|
||||
@ -101,7 +101,7 @@ def get_backup_type() -> EnumTypeBackupType:
|
||||
return backup_type # type: ignore
|
||||
|
||||
|
||||
def has_passphrase() -> bool:
|
||||
def is_passphrase_enabled() -> bool:
|
||||
return common.get_bool(_NAMESPACE, _USE_PASSPHRASE)
|
||||
|
||||
|
||||
@ -143,21 +143,21 @@ def no_backup() -> bool:
|
||||
return common.get_bool(_NAMESPACE, _NO_BACKUP)
|
||||
|
||||
|
||||
def get_passphrase_source() -> int:
|
||||
b = common.get(_NAMESPACE, _PASSPHRASE_SOURCE)
|
||||
if b == b"\x01":
|
||||
return 1
|
||||
elif b == b"\x02":
|
||||
return 2
|
||||
else:
|
||||
return 0
|
||||
def get_passphrase_always_on_device() -> bool:
|
||||
"""
|
||||
This is backwards compatible with _PASSPHRASE_SOURCE:
|
||||
- If ASK(0) => returns False, the check against b"\x01" in get_bool fails.
|
||||
- If DEVICE(1) => returns True, the check against b"\x01" in get_bool succeeds.
|
||||
- If HOST(2) => returns False, the check against b"\x01" in get_bool fails.
|
||||
"""
|
||||
return common.get_bool(_NAMESPACE, _PASSPHRASE_ALWAYS_ON_DEVICE)
|
||||
|
||||
|
||||
def load_settings(
|
||||
label: str = None,
|
||||
use_passphrase: bool = None,
|
||||
homescreen: bytes = None,
|
||||
passphrase_source: int = None,
|
||||
passphrase_always_on_device: bool = None,
|
||||
display_rotation: int = None,
|
||||
) -> None:
|
||||
if label is not None:
|
||||
@ -170,9 +170,10 @@ def load_settings(
|
||||
common.set(_NAMESPACE, _HOMESCREEN, homescreen, True) # public
|
||||
else:
|
||||
common.set(_NAMESPACE, _HOMESCREEN, b"", True) # public
|
||||
if passphrase_source is not None:
|
||||
if passphrase_source in (0, 1, 2):
|
||||
common.set(_NAMESPACE, _PASSPHRASE_SOURCE, bytes([passphrase_source]))
|
||||
if passphrase_always_on_device is not None:
|
||||
common.set_bool(
|
||||
_NAMESPACE, _PASSPHRASE_ALWAYS_ON_DEVICE, passphrase_always_on_device
|
||||
)
|
||||
if display_rotation is not None:
|
||||
if display_rotation not in (0, 90, 180, 270):
|
||||
raise ValueError(
|
||||
|
@ -6,7 +6,6 @@ if __debug__:
|
||||
try:
|
||||
from typing import Dict, List # noqa: F401
|
||||
from typing_extensions import Literal # noqa: F401
|
||||
EnumTypePassphraseSourceType = Literal[0, 1, 2]
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@ -20,17 +19,17 @@ class ApplySettings(p.MessageType):
|
||||
label: str = None,
|
||||
use_passphrase: bool = None,
|
||||
homescreen: bytes = None,
|
||||
passphrase_source: EnumTypePassphraseSourceType = None,
|
||||
auto_lock_delay_ms: int = None,
|
||||
display_rotation: int = None,
|
||||
passphrase_always_on_device: bool = None,
|
||||
) -> None:
|
||||
self.language = language
|
||||
self.label = label
|
||||
self.use_passphrase = use_passphrase
|
||||
self.homescreen = homescreen
|
||||
self.passphrase_source = passphrase_source
|
||||
self.auto_lock_delay_ms = auto_lock_delay_ms
|
||||
self.display_rotation = display_rotation
|
||||
self.passphrase_always_on_device = passphrase_always_on_device
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
@ -39,7 +38,7 @@ class ApplySettings(p.MessageType):
|
||||
2: ('label', p.UnicodeType, 0),
|
||||
3: ('use_passphrase', p.BoolType, 0),
|
||||
4: ('homescreen', p.BytesType, 0),
|
||||
5: ('passphrase_source', p.EnumType("PassphraseSourceType", (0, 1, 2)), 0),
|
||||
6: ('auto_lock_delay_ms', p.UVarintType, 0),
|
||||
7: ('display_rotation', p.UVarintType, 0),
|
||||
8: ('passphrase_always_on_device', p.BoolType, 0),
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ if __debug__:
|
||||
try:
|
||||
from typing import Dict, List # noqa: F401
|
||||
from typing_extensions import Literal # noqa: F401
|
||||
EnumTypeButtonRequestType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
|
||||
EnumTypeButtonRequestType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19]
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@ -23,5 +23,5 @@ class ButtonRequest(p.MessageType):
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
return {
|
||||
1: ('code', p.EnumType("ButtonRequestType", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18)), 0),
|
||||
1: ('code', p.EnumType("ButtonRequestType", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19)), 0),
|
||||
}
|
||||
|
@ -16,8 +16,8 @@ Address = 10 # type: Literal[10]
|
||||
PublicKey = 11 # type: Literal[11]
|
||||
MnemonicWordCount = 12 # type: Literal[12]
|
||||
MnemonicInput = 13 # type: Literal[13]
|
||||
PassphraseType = 14 # type: Literal[14]
|
||||
UnknownDerivationPath = 15 # type: Literal[15]
|
||||
RecoveryHomepage = 16 # type: Literal[16]
|
||||
Success = 17 # type: Literal[17]
|
||||
Warning = 18 # type: Literal[18]
|
||||
PassphraseEntry = 19 # type: Literal[19]
|
||||
|
@ -23,3 +23,4 @@ if not utils.BITCOIN_ONLY:
|
||||
U2F = 14 # type: Literal[14]
|
||||
Shamir = 15 # type: Literal[15]
|
||||
ShamirGroups = 16 # type: Literal[16]
|
||||
PassphraseEntry = 17 # type: Literal[17]
|
||||
|
@ -10,5 +10,5 @@ if __debug__:
|
||||
pass
|
||||
|
||||
|
||||
class PassphraseStateAck(p.MessageType):
|
||||
class Deprecated_PassphraseStateAck(p.MessageType):
|
||||
MESSAGE_WIRE_TYPE = 78
|
@ -10,7 +10,7 @@ if __debug__:
|
||||
pass
|
||||
|
||||
|
||||
class PassphraseStateRequest(p.MessageType):
|
||||
class Deprecated_PassphraseStateRequest(p.MessageType):
|
||||
MESSAGE_WIRE_TYPE = 77
|
||||
|
||||
def __init__(
|
@ -6,7 +6,7 @@ if __debug__:
|
||||
try:
|
||||
from typing import Dict, List # noqa: F401
|
||||
from typing_extensions import Literal # noqa: F401
|
||||
EnumTypeCapability = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
|
||||
EnumTypeCapability = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
|
||||
EnumTypeBackupType = Literal[0, 1, 2]
|
||||
except ImportError:
|
||||
pass
|
||||
@ -32,7 +32,6 @@ class Features(p.MessageType):
|
||||
bootloader_hash: bytes = None,
|
||||
imported: bool = None,
|
||||
pin_cached: bool = None,
|
||||
passphrase_cached: bool = None,
|
||||
firmware_present: bool = None,
|
||||
needs_backup: bool = None,
|
||||
flags: int = None,
|
||||
@ -50,6 +49,8 @@ class Features(p.MessageType):
|
||||
sd_card_present: bool = None,
|
||||
sd_protection: bool = None,
|
||||
wipe_code_protection: bool = None,
|
||||
session_id: bytes = None,
|
||||
passphrase_always_on_device: bool = None,
|
||||
) -> None:
|
||||
self.vendor = vendor
|
||||
self.major_version = major_version
|
||||
@ -66,7 +67,6 @@ class Features(p.MessageType):
|
||||
self.bootloader_hash = bootloader_hash
|
||||
self.imported = imported
|
||||
self.pin_cached = pin_cached
|
||||
self.passphrase_cached = passphrase_cached
|
||||
self.firmware_present = firmware_present
|
||||
self.needs_backup = needs_backup
|
||||
self.flags = flags
|
||||
@ -84,6 +84,8 @@ class Features(p.MessageType):
|
||||
self.sd_card_present = sd_card_present
|
||||
self.sd_protection = sd_protection
|
||||
self.wipe_code_protection = wipe_code_protection
|
||||
self.session_id = session_id
|
||||
self.passphrase_always_on_device = passphrase_always_on_device
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
@ -103,7 +105,6 @@ class Features(p.MessageType):
|
||||
14: ('bootloader_hash', p.BytesType, 0),
|
||||
15: ('imported', p.BoolType, 0),
|
||||
16: ('pin_cached', p.BoolType, 0),
|
||||
17: ('passphrase_cached', p.BoolType, 0),
|
||||
18: ('firmware_present', p.BoolType, 0),
|
||||
19: ('needs_backup', p.BoolType, 0),
|
||||
20: ('flags', p.UVarintType, 0),
|
||||
@ -116,9 +117,11 @@ class Features(p.MessageType):
|
||||
27: ('unfinished_backup', p.BoolType, 0),
|
||||
28: ('no_backup', p.BoolType, 0),
|
||||
29: ('recovery_mode', p.BoolType, 0),
|
||||
30: ('capabilities', p.EnumType("Capability", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)), p.FLAG_REPEATED),
|
||||
30: ('capabilities', p.EnumType("Capability", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)), p.FLAG_REPEATED),
|
||||
31: ('backup_type', p.EnumType("BackupType", (0, 1, 2)), 0),
|
||||
32: ('sd_card_present', p.BoolType, 0),
|
||||
33: ('sd_protection', p.BoolType, 0),
|
||||
34: ('wipe_code_protection', p.BoolType, 0),
|
||||
35: ('session_id', p.BytesType, 0),
|
||||
36: ('passphrase_always_on_device', p.BoolType, 0),
|
||||
}
|
||||
|
@ -15,15 +15,12 @@ class Initialize(p.MessageType):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
state: bytes = None,
|
||||
skip_passphrase: bool = None,
|
||||
session_id: bytes = None,
|
||||
) -> None:
|
||||
self.state = state
|
||||
self.skip_passphrase = skip_passphrase
|
||||
self.session_id = session_id
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
return {
|
||||
1: ('state', p.BytesType, 0),
|
||||
2: ('skip_passphrase', p.BoolType, 0),
|
||||
1: ('session_id', p.BytesType, 0),
|
||||
}
|
||||
|
@ -29,8 +29,6 @@ EntropyRequest = 35 # type: Literal[35]
|
||||
EntropyAck = 36 # type: Literal[36]
|
||||
PassphraseRequest = 41 # type: Literal[41]
|
||||
PassphraseAck = 42 # type: Literal[42]
|
||||
PassphraseStateRequest = 77 # type: Literal[77]
|
||||
PassphraseStateAck = 78 # type: Literal[78]
|
||||
RecoveryDevice = 45 # type: Literal[45]
|
||||
WordRequest = 46 # type: Literal[46]
|
||||
WordAck = 47 # type: Literal[47]
|
||||
@ -40,6 +38,8 @@ SdProtect = 79 # type: Literal[79]
|
||||
GetNextU2FCounter = 80 # type: Literal[80]
|
||||
NextU2FCounter = 81 # type: Literal[81]
|
||||
ChangeWipeCode = 82 # type: Literal[82]
|
||||
Deprecated_PassphraseStateRequest = 77 # type: Literal[77]
|
||||
Deprecated_PassphraseStateAck = 78 # type: Literal[78]
|
||||
FirmwareErase = 6 # type: Literal[6]
|
||||
FirmwareUpload = 7 # type: Literal[7]
|
||||
FirmwareRequest = 8 # type: Literal[8]
|
||||
|
@ -16,14 +16,17 @@ class PassphraseAck(p.MessageType):
|
||||
def __init__(
|
||||
self,
|
||||
passphrase: str = None,
|
||||
state: bytes = None,
|
||||
_state: bytes = None,
|
||||
on_device: bool = None,
|
||||
) -> None:
|
||||
self.passphrase = passphrase
|
||||
self.state = state
|
||||
self._state = _state
|
||||
self.on_device = on_device
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
return {
|
||||
1: ('passphrase', p.UnicodeType, 0),
|
||||
2: ('state', p.BytesType, 0),
|
||||
2: ('_state', p.BytesType, 0),
|
||||
3: ('on_device', p.BoolType, 0),
|
||||
}
|
||||
|
@ -15,12 +15,12 @@ class PassphraseRequest(p.MessageType):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
on_device: bool = None,
|
||||
_on_device: bool = None,
|
||||
) -> None:
|
||||
self.on_device = on_device
|
||||
self._on_device = _on_device
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
return {
|
||||
1: ('on_device', p.BoolType, 0),
|
||||
1: ('_on_device', p.BoolType, 0),
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
# Automatically generated by pb2py
|
||||
# fmt: off
|
||||
if False:
|
||||
from typing_extensions import Literal
|
||||
|
||||
ASK = 0 # type: Literal[0]
|
||||
DEVICE = 1 # type: Literal[1]
|
||||
HOST = 2 # type: Literal[2]
|
@ -17,19 +17,13 @@ class Ping(p.MessageType):
|
||||
self,
|
||||
message: str = None,
|
||||
button_protection: bool = None,
|
||||
pin_protection: bool = None,
|
||||
passphrase_protection: bool = None,
|
||||
) -> None:
|
||||
self.message = message
|
||||
self.button_protection = button_protection
|
||||
self.pin_protection = pin_protection
|
||||
self.passphrase_protection = passphrase_protection
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
return {
|
||||
1: ('message', p.UnicodeType, 0),
|
||||
2: ('button_protection', p.BoolType, 0),
|
||||
3: ('pin_protection', p.BoolType, 0),
|
||||
4: ('passphrase_protection', p.BoolType, 0),
|
||||
}
|
||||
|
@ -160,6 +160,14 @@ def header_error(message: str, clear: bool = True) -> None:
|
||||
display.bar(0, 30, WIDTH, HEIGHT - 30, style.BG)
|
||||
|
||||
|
||||
def draw_simple(t: Component) -> None: # noqa: F405
|
||||
backlight_fade(style.BACKLIGHT_DIM)
|
||||
display.clear()
|
||||
t.on_render()
|
||||
refresh()
|
||||
backlight_fade(style.BACKLIGHT_NORMAL)
|
||||
|
||||
|
||||
def grid(
|
||||
i: int, # i-th cell of the table of which we wish to return Area (snake-like starting with 0)
|
||||
n_x: int = 3, # number of rows in the table
|
||||
|
@ -1,7 +1,6 @@
|
||||
from micropython import const
|
||||
|
||||
from trezor import io, loop, res, ui
|
||||
from trezor.messages import PassphraseSourceType
|
||||
from trezor.ui import display
|
||||
from trezor.ui.button import Button, ButtonClear, ButtonConfirm
|
||||
from trezor.ui.swipe import SWIPE_HORIZONTAL, SWIPE_LEFT, Swipe
|
||||
@ -246,25 +245,3 @@ class PassphraseKeyboard(ui.Layout):
|
||||
|
||||
def create_tasks(self) -> Tuple[loop.Task, ...]:
|
||||
return self.handle_input(), self.handle_rendering(), self.handle_paging()
|
||||
|
||||
|
||||
class PassphraseSource(ui.Layout):
|
||||
def __init__(self, content: ui.Component) -> None:
|
||||
self.content = content
|
||||
|
||||
self.device = Button(ui.grid(8, n_y=4, n_x=4, cells_x=4), "Device")
|
||||
self.device.on_click = self.on_device # type: ignore
|
||||
|
||||
self.host = Button(ui.grid(12, n_y=4, n_x=4, cells_x=4), "Host")
|
||||
self.host.on_click = self.on_host # type: ignore
|
||||
|
||||
def dispatch(self, event: int, x: int, y: int) -> None:
|
||||
self.content.dispatch(event, x, y)
|
||||
self.device.dispatch(event, x, y)
|
||||
self.host.dispatch(event, x, y)
|
||||
|
||||
def on_device(self) -> None:
|
||||
raise ui.Result(PassphraseSourceType.DEVICE)
|
||||
|
||||
def on_host(self) -> None:
|
||||
raise ui.Result(PassphraseSourceType.HOST)
|
||||
|
@ -1,9 +1,23 @@
|
||||
import sys
|
||||
|
||||
sys.path.append('../src')
|
||||
sys.path.append("../src")
|
||||
|
||||
from ubinascii import hexlify, unhexlify # noqa: F401
|
||||
|
||||
import unittest # noqa: F401
|
||||
|
||||
from trezor import utils # noqa: F401
|
||||
|
||||
|
||||
def await_result(task: Awaitable) -> Any:
|
||||
value = None
|
||||
while True:
|
||||
try:
|
||||
result = task.send(value)
|
||||
except StopIteration as e:
|
||||
return e.value
|
||||
|
||||
if result:
|
||||
value = await_result(result)
|
||||
else:
|
||||
value = None
|
||||
|
@ -44,28 +44,14 @@ class ByteArrayWriter:
|
||||
return len(buf)
|
||||
|
||||
|
||||
def run_until_complete(task: Awaitable) -> Any:
|
||||
value = None
|
||||
while True:
|
||||
try:
|
||||
result = task.send(value)
|
||||
except StopIteration as e:
|
||||
return e.value
|
||||
|
||||
if result:
|
||||
value = run_until_complete(result)
|
||||
else:
|
||||
value = None
|
||||
|
||||
|
||||
def load_uvarint(data: bytes) -> int:
|
||||
reader = ByteReader(data)
|
||||
return run_until_complete(protobuf.load_uvarint(reader))
|
||||
return await_result(protobuf.load_uvarint(reader))
|
||||
|
||||
|
||||
def dump_uvarint(value: int) -> bytearray:
|
||||
writer = ByteArrayWriter()
|
||||
run_until_complete(protobuf.dump_uvarint(writer, value))
|
||||
await_result(protobuf.dump_uvarint(writer, value))
|
||||
return writer.buf
|
||||
|
||||
|
||||
@ -106,9 +92,9 @@ class TestProtobuf(unittest.TestCase):
|
||||
# ok message:
|
||||
msg = Message(-42, 5)
|
||||
writer = ByteArrayWriter()
|
||||
run_until_complete(protobuf.dump_message(writer, msg))
|
||||
await_result(protobuf.dump_message(writer, msg))
|
||||
reader = ByteReader(bytes(writer.buf))
|
||||
nmsg = run_until_complete(protobuf.load_message(reader, Message))
|
||||
nmsg = await_result(protobuf.load_message(reader, Message))
|
||||
|
||||
self.assertEqual(msg.sint_field, nmsg.sint_field)
|
||||
self.assertEqual(msg.enum_field, nmsg.enum_field)
|
||||
@ -116,10 +102,10 @@ class TestProtobuf(unittest.TestCase):
|
||||
# bad enum value:
|
||||
msg = Message(-42, 42)
|
||||
writer = ByteArrayWriter()
|
||||
run_until_complete(protobuf.dump_message(writer, msg))
|
||||
await_result(protobuf.dump_message(writer, msg))
|
||||
reader = ByteReader(bytes(writer.buf))
|
||||
with self.assertRaises(TypeError):
|
||||
run_until_complete(protobuf.load_message(reader, Message))
|
||||
await_result(protobuf.load_message(reader, Message))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
81
core/tests/test_storage.cache.py
Normal file
81
core/tests/test_storage.cache.py
Normal file
@ -0,0 +1,81 @@
|
||||
from common import *
|
||||
from mock import patch
|
||||
from mock_storage import mock_storage
|
||||
|
||||
import storage
|
||||
from storage import cache
|
||||
from trezor.messages.Initialize import Initialize
|
||||
from trezor.messages.ClearSession import ClearSession
|
||||
from trezor.wire import DUMMY_CONTEXT
|
||||
|
||||
from apps.homescreen import handle_Initialize, handle_ClearSession
|
||||
|
||||
KEY = 99
|
||||
|
||||
|
||||
class TestStorageCache(unittest.TestCase):
|
||||
def test_session_id(self):
|
||||
session_id_a = cache.get_session_id()
|
||||
self.assertIsNotNone(session_id_a)
|
||||
session_id_b = cache.get_session_id()
|
||||
self.assertEqual(session_id_a, session_id_b)
|
||||
|
||||
cache.clear()
|
||||
session_id_c = cache.get_session_id()
|
||||
self.assertIsNotNone(session_id_c)
|
||||
self.assertNotEqual(session_id_a, session_id_c)
|
||||
|
||||
def test_get_set(self):
|
||||
value = cache.get(KEY)
|
||||
self.assertIsNone(value)
|
||||
|
||||
cache.set(KEY, "hello")
|
||||
value = cache.get(KEY)
|
||||
self.assertEqual(value, "hello")
|
||||
|
||||
cache.clear()
|
||||
value = cache.get(KEY)
|
||||
self.assertIsNone(value)
|
||||
|
||||
@mock_storage
|
||||
def test_Initialize(self):
|
||||
def call_Initialize(**kwargs):
|
||||
msg = Initialize(**kwargs)
|
||||
return await_result(handle_Initialize(DUMMY_CONTEXT, msg))
|
||||
|
||||
# calling Initialize without an ID allocates a new one
|
||||
session_id = cache.get_session_id()
|
||||
features = call_Initialize()
|
||||
new_session_id = cache.get_session_id()
|
||||
self.assertNotEqual(session_id, new_session_id)
|
||||
self.assertEqual(new_session_id, features.session_id)
|
||||
|
||||
# calling Initialize with the current ID does not allocate a new one
|
||||
features = call_Initialize(session_id=new_session_id)
|
||||
same_session_id = cache.get_session_id()
|
||||
self.assertEqual(new_session_id, same_session_id)
|
||||
self.assertEqual(same_session_id, features.session_id)
|
||||
|
||||
call_Initialize()
|
||||
# calling Initialize with a non-current ID returns a different one
|
||||
features = call_Initialize(session_id=new_session_id)
|
||||
self.assertNotEqual(new_session_id, features.session_id)
|
||||
|
||||
# allocating a new session ID clears the cache
|
||||
cache.set(KEY, "hello")
|
||||
features = call_Initialize()
|
||||
self.assertIsNone(cache.get(KEY))
|
||||
|
||||
# resuming a session does not clear the cache
|
||||
cache.set(KEY, "hello")
|
||||
call_Initialize(session_id=features.session_id)
|
||||
self.assertEqual(cache.get(KEY), "hello")
|
||||
|
||||
# supplying a different session ID clears the cache
|
||||
self.assertNotEqual(new_session_id, features.session_id)
|
||||
call_Initialize(session_id=new_session_id)
|
||||
self.assertIsNone(cache.get(KEY))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
@ -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`.
|
9
docs/common/communication/index.md
Normal file
9
docs/common/communication/index.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Communication
|
||||
|
||||
_Note: In this section we describe the internal functioning of the communication protocol. If you wish to implement Trezor support you should use [Connect](https://github.com/trezor/connect/) or [python-trezor](https://pypi.org/project/trezor/), which will do all this hard work for you._
|
||||
|
||||
We use [Protobuf v2](https://developers.google.com/protocol-buffers/) for host-device communication. The communication cycle is very simple, Trezor receives a message (request), acts on it and responds with another one (response). Trezor on its own is incapable of initiating the communication.
|
||||
|
||||
## Definitions
|
||||
|
||||
Protobuf messages are defined in the [Common](https://github.com/trezor/trezor-firmware/tree/master/common) project, which is part of this monorepo. This repository is also exported to [trezor/trezor-common](https://github.com/trezor/trezor-common) to be used by third parties, which prefer not to include the whole monorepo. That copy is read-only mirror and all changes are happening in this monorepo.
|
64
docs/common/communication/passphrase.md
Normal file
64
docs/common/communication/passphrase.md
Normal file
@ -0,0 +1,64 @@
|
||||
# Passphrase
|
||||
|
||||
As of \[versions TBD\] we have changed how [passphrase](https://wiki.trezor.io/Passphrase) is communicated between the Host and the Device.
|
||||
|
||||
Passphrase is very tightly coupled with _sessions_. The reader is encouraged to read on that topic first in the [sessions.md](sessions.md) section.
|
||||
|
||||
## Scheme
|
||||
|
||||
As soon as Trezor needs the passphrase to do BIP-39/SLIP-39 derivations it prompts the user for passphrase.
|
||||
|
||||
```
|
||||
GetAddress(...)
|
||||
---------> PassphraseRequest()
|
||||
<---------
|
||||
PassphraseAck
|
||||
(str passphrase, bool on_device)
|
||||
---------> Address(...)
|
||||
<---------
|
||||
```
|
||||
|
||||
In the default Trezor setting, the passphrase is obtained from the Host. Trezor sends a PassphraseRequest message and awaits PassphraseAck as a response. This message contains field `passphrase` to transmit it or it has `on_device` boolean flag to indicate that the user wishes to enter the passphrase on Trezor instead. Setting both `passphrase` and `on_device` to true is forbidden.
|
||||
|
||||
Note that this has changed as of TBD. In previous firmware versions the `on_device` flag was in the PassphraseRequest message, since this decision has been made on Trezor. We also had two additional messages PassphraseStateRequest and PassphraseStateAck which were removed.
|
||||
|
||||
## Example
|
||||
|
||||
On an initialized device with passphrase enabled a common communication starts like this:
|
||||
|
||||
```
|
||||
Initialize()
|
||||
---------> Features(..., session_id)
|
||||
<---------
|
||||
GetAddress(...)
|
||||
---------> PassphraseRequest()
|
||||
<---------
|
||||
PassphraseAck(...)
|
||||
---------> Address(...)
|
||||
<---------
|
||||
```
|
||||
|
||||
The device requested the passphrase since the BIP-39/SLIP-39 seed is not yet cached. After this workflow the seed is cached and the passphrase will therefore never be requested again unless the session is cleared*.
|
||||
|
||||
Since we do not have sessions, the Host can not be sure that someone else has not used the device and applied another session id (e.g. changed the Passphrase). To work around this we send the session id again on every subsequent message. See more on that in [session.md]().
|
||||
|
||||
```
|
||||
Initialize(session_id)
|
||||
---------> Features(..., session_id)
|
||||
<---------
|
||||
GetPublicKey(...)
|
||||
---------> PublicKey(...)
|
||||
<---------
|
||||
```
|
||||
|
||||
As long as the session_id in `Initialize` is the same as the one Trezor stores internally, Trezor guarantees the same passphrase is being used.
|
||||
|
||||
----
|
||||
|
||||
\* There is one exception and that is Cardano. Because Cardano has a different BIP-39/SLIP-39 derivation scheme for passphrase we can not use the cached seed. As a workaround we prompt for the passphrase again in such case and cache the cardano seed in the cardano app directly.
|
||||
|
||||
## Passphrase always on device
|
||||
|
||||
User might want to enforce the passphrase entry on the device every time without the hassle of instructing the Host to do so.
|
||||
|
||||
For such cases the user may apply the *Passphrase always on device* setting. As the name suggests, with this setting the passphrase is prompted on the device right away and no PassphraseRequest/PassphraseAck messages are exchanged. Note that the passphrase is prompted only once for given session id. If the user wishes to enter another passphrase they need to either send Initialize(session_id=None) or replug the device.
|
36
docs/common/communication/sessions.md
Normal file
36
docs/common/communication/sessions.md
Normal file
@ -0,0 +1,36 @@
|
||||
# Sessions (the lack of them)
|
||||
|
||||
Currently the communication protocol lacks sessions, which are planned to be introduced in the near future (see [#79](https://github.com/trezor/trezor-firmware/issues/79)).
|
||||
|
||||
To ensure the device is in the expected state we use something called _session_id_. Session id is a 32 bytes long random blob which identifies the internal device state (mainly its caches). This is primarily useful for passphrase to make sure the same passphrase is cached in the device as the one the user entered a few minutes ago. See [passphrase.md](passphrase.md) for more on that.
|
||||
|
||||
On first initialization the Host does not have a session id and starts the communication with an empty Initialize:
|
||||
|
||||
```
|
||||
Initialize()
|
||||
---------> Features(..., session_id)
|
||||
<---------
|
||||
```
|
||||
|
||||
After the first Features message is received the Host might store the session_id. To ensure the device state Host must send the Initialize message again including that particular session_id:
|
||||
|
||||
```
|
||||
Initialize(session_id)
|
||||
---------> Features(..., session_id)
|
||||
<---------
|
||||
Request
|
||||
---------> Response
|
||||
<---------
|
||||
```
|
||||
|
||||
So to make sure the device state has not changed, the Host must send the Initialize message with the correctly stored session_id before each request. Yes, this is stupid.
|
||||
|
||||
As mentioned, sessions will be introduced soon™ and fix that. We will probably take the first few bytes of the session_id, declare it a session id, and the rest will remain, without the annoying requirement of sending Initialize before every message.
|
||||
|
||||
----
|
||||
|
||||
The session is terminated and therefore the caches are cleared if:
|
||||
- Initialize.session_id is empty.
|
||||
- Initialize.session_id is different then the one cached in Trezor.
|
||||
- Trezor is replugged (session is not persistent).
|
||||
- ClearSession is received.
|
@ -1,8 +1,9 @@
|
||||
Version 1.x.x [not yet released]
|
||||
Version 1.9.0 [not yet released]
|
||||
* Disallow changing of settings via dry-run recovery
|
||||
* Wipe code
|
||||
* Make LoadDevice debug only and drop its XPRV feature
|
||||
* Add feature to retrieve the next U2F counter
|
||||
* Passphrase redesign
|
||||
|
||||
Version 1.8.3 [Sep 2019]
|
||||
* Small code improvements
|
||||
|
@ -31,6 +31,7 @@
|
||||
#include "config.h"
|
||||
#include "curves.h"
|
||||
#include "debug.h"
|
||||
#include "fsm.h"
|
||||
#include "gettext.h"
|
||||
#include "hmac.h"
|
||||
#include "layout2.h"
|
||||
@ -119,11 +120,11 @@ be added to the storage u2f_counter to get the real counter value.
|
||||
* storage.u2f_counter + config_u2f_offset.
|
||||
* This corresponds to the number of cleared bits in the U2FAREA.
|
||||
*/
|
||||
static secbool sessionSeedCached, sessionSeedUsesPassphrase;
|
||||
static secbool sessionSeedCached;
|
||||
static uint8_t CONFIDENTIAL sessionSeed[64];
|
||||
|
||||
static secbool sessionPassphraseCached = secfalse;
|
||||
static char CONFIDENTIAL sessionPassphrase[51];
|
||||
static secbool sessionIdCached;
|
||||
static uint8_t sessionId[32];
|
||||
|
||||
#define autoLockDelayMsDefault (10 * 60 * 1000U) // 10 minutes
|
||||
static secbool autoLockDelayMsCached = secfalse;
|
||||
@ -407,8 +408,8 @@ void config_init(void) {
|
||||
void session_clear(bool lock) {
|
||||
sessionSeedCached = secfalse;
|
||||
memzero(&sessionSeed, sizeof(sessionSeed));
|
||||
sessionPassphraseCached = secfalse;
|
||||
memzero(&sessionPassphrase, sizeof(sessionPassphrase));
|
||||
sessionIdCached = secfalse;
|
||||
memzero(&sessionId, sizeof(sessionId));
|
||||
if (lock) {
|
||||
storage_lock();
|
||||
}
|
||||
@ -527,8 +528,6 @@ void config_setLanguage(const char *lang) {
|
||||
}
|
||||
|
||||
void config_setPassphraseProtection(bool passphrase_protection) {
|
||||
sessionSeedCached = secfalse;
|
||||
sessionPassphraseCached = secfalse;
|
||||
config_set_bool(KEY_PASSPHRASE_PROTECTION, passphrase_protection);
|
||||
}
|
||||
|
||||
@ -550,18 +549,19 @@ static void get_root_node_callback(uint32_t iter, uint32_t total) {
|
||||
layoutProgress(_("Waking up"), 1000 * iter / total);
|
||||
}
|
||||
|
||||
const uint8_t *config_getSeed(bool usePassphrase) {
|
||||
const uint8_t *config_getSeed(void) {
|
||||
// root node is properly cached
|
||||
if (usePassphrase == (sectrue == sessionSeedUsesPassphrase) &&
|
||||
sectrue == sessionSeedCached) {
|
||||
if (sectrue == sessionSeedCached) {
|
||||
return sessionSeed;
|
||||
}
|
||||
|
||||
// if storage has mnemonic, convert it to node and use it
|
||||
char mnemonic[MAX_MNEMONIC_LEN + 1] = {0};
|
||||
if (config_getMnemonic(mnemonic, sizeof(mnemonic))) {
|
||||
if (usePassphrase && !protectPassphrase()) {
|
||||
char passphrase[MAX_PASSPHRASE_LEN + 1] = {0};
|
||||
if (!protectPassphrase(passphrase)) {
|
||||
memzero(mnemonic, sizeof(mnemonic));
|
||||
memzero(passphrase, sizeof(passphrase));
|
||||
return NULL;
|
||||
}
|
||||
// if storage was not imported (i.e. it was properly generated or recovered)
|
||||
@ -575,13 +575,16 @@ const uint8_t *config_getSeed(bool usePassphrase) {
|
||||
}
|
||||
}
|
||||
char oldTiny = usbTiny(1);
|
||||
mnemonic_to_seed(mnemonic, usePassphrase ? sessionPassphrase : "",
|
||||
sessionSeed, get_root_node_callback); // BIP-0039
|
||||
mnemonic_to_seed(mnemonic, passphrase, sessionSeed,
|
||||
get_root_node_callback); // BIP-0039
|
||||
memzero(mnemonic, sizeof(mnemonic));
|
||||
memzero(passphrase, sizeof(passphrase));
|
||||
usbTiny(oldTiny);
|
||||
sessionSeedCached = sectrue;
|
||||
sessionSeedUsesPassphrase = usePassphrase ? sectrue : secfalse;
|
||||
return sessionSeed;
|
||||
} else {
|
||||
fsm_sendFailure(FailureType_Failure_NotInitialized,
|
||||
_("Device not initialized"));
|
||||
}
|
||||
|
||||
return NULL;
|
||||
@ -606,58 +609,16 @@ bool config_getU2FRoot(HDNode *node) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool config_getRootNode(HDNode *node, const char *curve, bool usePassphrase) {
|
||||
// if storage has node, decrypt and use it
|
||||
StorageHDNode storageHDNode = {0};
|
||||
uint16_t len = 0;
|
||||
if (strcmp(curve, SECP256K1_NAME) == 0 &&
|
||||
sectrue ==
|
||||
storage_get(KEY_NODE, &storageHDNode, sizeof(storageHDNode), &len) &&
|
||||
len == sizeof(StorageHDNode)) {
|
||||
if (!protectPassphrase()) {
|
||||
memzero(&storageHDNode, sizeof(storageHDNode));
|
||||
return false;
|
||||
}
|
||||
if (!config_loadNode(&storageHDNode, curve, node)) {
|
||||
memzero(&storageHDNode, sizeof(storageHDNode));
|
||||
return false;
|
||||
}
|
||||
bool passphrase_protection = false;
|
||||
config_getPassphraseProtection(&passphrase_protection);
|
||||
if (passphrase_protection && sectrue == sessionPassphraseCached &&
|
||||
sessionPassphrase[0] != '\0') {
|
||||
// decrypt hd node
|
||||
uint8_t secret[64] = {0};
|
||||
PBKDF2_HMAC_SHA512_CTX pctx = {0};
|
||||
char oldTiny = usbTiny(1);
|
||||
pbkdf2_hmac_sha512_Init(&pctx, (const uint8_t *)sessionPassphrase,
|
||||
strlen(sessionPassphrase),
|
||||
(const uint8_t *)"TREZORHD", 8, 1);
|
||||
get_root_node_callback(0, BIP39_PBKDF2_ROUNDS);
|
||||
for (int i = 0; i < 8; i++) {
|
||||
pbkdf2_hmac_sha512_Update(&pctx, BIP39_PBKDF2_ROUNDS / 8);
|
||||
get_root_node_callback((i + 1) * BIP39_PBKDF2_ROUNDS / 8,
|
||||
BIP39_PBKDF2_ROUNDS);
|
||||
}
|
||||
pbkdf2_hmac_sha512_Final(&pctx, secret);
|
||||
usbTiny(oldTiny);
|
||||
aes_decrypt_ctx ctx = {0};
|
||||
aes_decrypt_key256(secret, &ctx);
|
||||
aes_cbc_decrypt(node->chain_code, node->chain_code, 32, secret + 32,
|
||||
&ctx);
|
||||
aes_cbc_decrypt(node->private_key, node->private_key, 32, secret + 32,
|
||||
&ctx);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
memzero(&storageHDNode, sizeof(storageHDNode));
|
||||
|
||||
const uint8_t *seed = config_getSeed(usePassphrase);
|
||||
bool config_getRootNode(HDNode *node, const char *curve) {
|
||||
const uint8_t *seed = config_getSeed();
|
||||
if (seed == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hdnode_from_seed(seed, 64, curve, node);
|
||||
int result = hdnode_from_seed(seed, 64, curve, node);
|
||||
if (result == 0) {
|
||||
fsm_sendFailure(FailureType_Failure_NotInitialized, _("Unsupported curve"));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool config_getLabel(char *dest, uint16_t dest_size) {
|
||||
@ -811,40 +772,12 @@ bool config_changeWipeCode(const char *pin, const char *wipe_code) {
|
||||
return sectrue == ret;
|
||||
}
|
||||
|
||||
void session_cachePassphrase(const char *passphrase) {
|
||||
strlcpy(sessionPassphrase, passphrase, sizeof(sessionPassphrase));
|
||||
sessionPassphraseCached = sectrue;
|
||||
}
|
||||
|
||||
bool session_isPassphraseCached(void) {
|
||||
return sectrue == sessionPassphraseCached;
|
||||
}
|
||||
|
||||
bool session_getState(const uint8_t *salt, uint8_t *state,
|
||||
const char *passphrase) {
|
||||
if (!passphrase && sectrue != sessionPassphraseCached) {
|
||||
return false;
|
||||
} else {
|
||||
passphrase = sessionPassphrase;
|
||||
const uint8_t *session_getSessionId(void) {
|
||||
if (!sessionIdCached) {
|
||||
random_buffer(sessionId, 32);
|
||||
}
|
||||
if (!salt) {
|
||||
// if salt is not provided fill the first half of the state with random data
|
||||
random_buffer(state, 32);
|
||||
} else {
|
||||
// if salt is provided fill the first half of the state with salt
|
||||
memcpy(state, salt, 32);
|
||||
}
|
||||
// state[0:32] = salt
|
||||
// state[32:64] = HMAC(passphrase, salt || device_id)
|
||||
HMAC_SHA256_CTX ctx = {0};
|
||||
hmac_sha256_Init(&ctx, (const uint8_t *)passphrase, strlen(passphrase));
|
||||
hmac_sha256_Update(&ctx, state, 32);
|
||||
hmac_sha256_Update(&ctx, (const uint8_t *)config_uuid, sizeof(config_uuid));
|
||||
hmac_sha256_Final(&ctx, state + 32);
|
||||
|
||||
memzero(&ctx, sizeof(ctx));
|
||||
|
||||
return true;
|
||||
sessionIdCached = sectrue;
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
bool session_isUnlocked(void) { return sectrue == storage_is_unlocked(); }
|
||||
|
@ -91,10 +91,10 @@ void session_clear(bool lock);
|
||||
|
||||
void config_loadDevice(const LoadDevice *msg);
|
||||
|
||||
const uint8_t *config_getSeed(bool usePassphrase);
|
||||
const uint8_t *config_getSeed(void);
|
||||
|
||||
bool config_getU2FRoot(HDNode *node);
|
||||
bool config_getRootNode(HDNode *node, const char *curve, bool usePassphrase);
|
||||
bool config_getRootNode(HDNode *node, const char *curve);
|
||||
|
||||
bool config_getLabel(char *dest, uint16_t dest_size);
|
||||
void config_setLabel(const char *label);
|
||||
@ -108,10 +108,7 @@ bool config_getPassphraseProtection(bool *passphrase_protection);
|
||||
bool config_getHomescreen(uint8_t *dest, uint16_t dest_size);
|
||||
void config_setHomescreen(const uint8_t *data, uint32_t size);
|
||||
|
||||
void session_cachePassphrase(const char *passphrase);
|
||||
bool session_isPassphraseCached(void);
|
||||
bool session_getState(const uint8_t *salt, uint8_t *state,
|
||||
const char *passphrase);
|
||||
const uint8_t *session_getSessionId(void);
|
||||
|
||||
bool config_setMnemonic(const char *mnemonic);
|
||||
bool config_containsMnemonic(const char *mnemonic);
|
||||
|
@ -212,10 +212,7 @@ static HDNode *fsm_getDerivedNode(const char *curve, const uint32_t *address_n,
|
||||
if (fingerprint) {
|
||||
*fingerprint = 0;
|
||||
}
|
||||
if (!config_getRootNode(&node, curve, true)) {
|
||||
fsm_sendFailure(FailureType_Failure_NotInitialized,
|
||||
_("Device not initialized or passphrase request cancelled "
|
||||
"or unsupported curve"));
|
||||
if (!config_getRootNode(&node, curve)) {
|
||||
layoutHome();
|
||||
return 0;
|
||||
}
|
||||
|
@ -20,17 +20,14 @@
|
||||
void fsm_msgInitialize(const Initialize *msg) {
|
||||
recovery_abort();
|
||||
signing_abort();
|
||||
if (msg && msg->has_state && msg->state.size == 64) {
|
||||
uint8_t i_state[64];
|
||||
if (!session_getState(msg->state.bytes, i_state, NULL)) {
|
||||
session_clear(false); // do not clear PIN
|
||||
} else {
|
||||
if (0 != memcmp(msg->state.bytes, i_state, 64)) {
|
||||
session_clear(false); // do not clear PIN
|
||||
}
|
||||
if (msg && msg->has_session_id && msg->session_id.size == 32) {
|
||||
if (0 != memcmp(session_getSessionId(), msg->session_id.bytes, 32)) {
|
||||
// If session id was specified but does not match -> clear the cache.
|
||||
session_clear(false); // do not lock
|
||||
}
|
||||
} else {
|
||||
session_clear(false); // do not clear PIN
|
||||
// If session id was not specified -> clear the cache.
|
||||
session_clear(false); // do not lock
|
||||
}
|
||||
layoutHome();
|
||||
fsm_msgGetFeatures(0);
|
||||
@ -39,6 +36,12 @@ void fsm_msgInitialize(const Initialize *msg) {
|
||||
void fsm_msgGetFeatures(const GetFeatures *msg) {
|
||||
(void)msg;
|
||||
RESP_INIT(Features);
|
||||
|
||||
resp->has_session_id = true;
|
||||
memcpy(resp->session_id.bytes, session_getSessionId(),
|
||||
sizeof(resp->session_id.bytes));
|
||||
resp->session_id.size = sizeof(resp->session_id.bytes);
|
||||
|
||||
resp->has_vendor = true;
|
||||
strlcpy(resp->vendor, "trezor.io", sizeof(resp->vendor));
|
||||
resp->has_major_version = true;
|
||||
@ -71,8 +74,6 @@ void fsm_msgGetFeatures(const GetFeatures *msg) {
|
||||
resp->has_imported = config_getImported(&(resp->imported));
|
||||
resp->has_pin_cached = true;
|
||||
resp->pin_cached = session_isUnlocked() && config_hasPin();
|
||||
resp->has_passphrase_cached = true;
|
||||
resp->passphrase_cached = session_isPassphraseCached();
|
||||
resp->has_needs_backup = true;
|
||||
config_getNeedsBackup(&(resp->needs_backup));
|
||||
resp->has_unfinished_backup = true;
|
||||
@ -120,17 +121,6 @@ void fsm_msgPing(const Ping *msg) {
|
||||
}
|
||||
}
|
||||
|
||||
if (msg->has_pin_protection && msg->pin_protection) {
|
||||
CHECK_PIN
|
||||
}
|
||||
|
||||
if (msg->has_passphrase_protection && msg->passphrase_protection) {
|
||||
if (!protectPassphrase()) {
|
||||
fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (msg->has_message) {
|
||||
resp->has_message = true;
|
||||
memcpy(&(resp->message), &(msg->message), sizeof(resp->message));
|
||||
@ -359,6 +349,10 @@ void fsm_msgClearSession(const ClearSession *msg) {
|
||||
}
|
||||
|
||||
void fsm_msgApplySettings(const ApplySettings *msg) {
|
||||
CHECK_PARAM(
|
||||
!msg->has_passphrase_always_on_device,
|
||||
_("This firmware is incapable of passphrase entry on the device."));
|
||||
|
||||
CHECK_PARAM(msg->has_label || msg->has_language || msg->has_use_passphrase ||
|
||||
msg->has_homescreen || msg->has_auto_lock_delay_ms,
|
||||
_("No setting provided"));
|
||||
|
@ -350,10 +350,12 @@ bool protectChangeWipeCode(bool removal) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool protectPassphrase(void) {
|
||||
bool protectPassphrase(char *passphrase) {
|
||||
memzero(passphrase, MAX_PASSPHRASE_LEN + 1);
|
||||
bool passphrase_protection = false;
|
||||
config_getPassphraseProtection(&passphrase_protection);
|
||||
if (!passphrase_protection || session_isPassphraseCached()) {
|
||||
if (!passphrase_protection) {
|
||||
// passphrase already set to empty by memzero above
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -369,12 +371,24 @@ bool protectPassphrase(void) {
|
||||
bool result;
|
||||
for (;;) {
|
||||
usbPoll();
|
||||
// TODO: correctly process PassphraseAck with state field set (mismatch =>
|
||||
// Failure)
|
||||
if (msg_tiny_id == MessageType_MessageType_PassphraseAck) {
|
||||
msg_tiny_id = 0xFFFF;
|
||||
PassphraseAck *ppa = (PassphraseAck *)msg_tiny;
|
||||
session_cachePassphrase(ppa->has_passphrase ? ppa->passphrase : "");
|
||||
if (ppa->has_on_device && ppa->on_device == true) {
|
||||
fsm_sendFailure(
|
||||
FailureType_Failure_DataError,
|
||||
_("This firmware is incapable of passphrase entry on the device."));
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
if (!ppa->has_passphrase) {
|
||||
fsm_sendFailure(FailureType_Failure_DataError,
|
||||
_("No passphrase provided. Use empty string to set an "
|
||||
"empty passphrase."));
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
strlcpy(passphrase, ppa->passphrase, sizeof(ppa->passphrase));
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
@ -383,8 +397,9 @@ bool protectPassphrase(void) {
|
||||
protectAbortedByInitialize =
|
||||
(msg_tiny_id == MessageType_MessageType_Initialize);
|
||||
if (protectAbortedByCancel || protectAbortedByInitialize) {
|
||||
msg_tiny_id = 0xFFFF;
|
||||
fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL);
|
||||
result = false;
|
||||
msg_tiny_id = 0xFFFF;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -24,13 +24,15 @@
|
||||
#include "messages-common.pb.h"
|
||||
#include "secbool.h"
|
||||
|
||||
#define MAX_PASSPHRASE_LEN 50
|
||||
|
||||
bool protectButton(ButtonRequestType type, bool confirm_only);
|
||||
secbool protectPinUiCallback(uint32_t wait, uint32_t progress,
|
||||
const char* message);
|
||||
bool protectPin(bool use_cached);
|
||||
bool protectChangePin(bool removal);
|
||||
bool protectChangeWipeCode(bool removal);
|
||||
bool protectPassphrase(void);
|
||||
bool protectPassphrase(char* passphrase);
|
||||
|
||||
extern bool protectAbortedByCancel;
|
||||
extern bool protectAbortedByInitialize;
|
||||
|
@ -5,9 +5,6 @@ Failure.message max_size:256
|
||||
PinMatrixAck.pin max_size:10
|
||||
|
||||
PassphraseAck.passphrase max_size:51
|
||||
PassphraseAck.state max_size:64
|
||||
|
||||
PassphraseStateRequest.state max_size:64
|
||||
|
||||
HDNodeType.chain_code max_size:32
|
||||
HDNodeType.private_key max_size:32
|
||||
|
@ -1,4 +1,4 @@
|
||||
Initialize.state max_size:64
|
||||
Initialize.session_id max_size:32
|
||||
|
||||
Features.vendor max_size:33
|
||||
Features.device_id max_size:25
|
||||
@ -10,6 +10,7 @@ Features.model max_size:17
|
||||
Features.fw_vendor max_size:256
|
||||
Features.fw_vendor_keys max_size:32
|
||||
Features.capabilities max_count:32
|
||||
Features.session_id max_size:32
|
||||
|
||||
ApplySettings.language max_size:17
|
||||
ApplySettings.label max_size:33
|
||||
|
@ -109,6 +109,8 @@ fl.write(
|
||||
messages = defaultdict(list)
|
||||
|
||||
for message in MessageType.DESCRIPTOR.values:
|
||||
if message.GetOptions().deprecated:
|
||||
continue
|
||||
extension = get_wire_extension(message)
|
||||
messages[extension].append(message)
|
||||
|
||||
|
@ -1460,7 +1460,7 @@ const HDNode *stellar_deriveNode(const uint32_t *address_n,
|
||||
const char *curve = "ed25519";
|
||||
|
||||
// Device not initialized, passphrase request cancelled, or unsupported curve
|
||||
if (!config_getRootNode(&node, curve, true)) {
|
||||
if (!config_getRootNode(&node, curve)) {
|
||||
return 0;
|
||||
}
|
||||
// Failed to derive private key
|
||||
|
@ -1,7 +1,7 @@
|
||||
#define VERSION_MAJOR 1
|
||||
#define VERSION_MINOR 8
|
||||
#define VERSION_PATCH 4
|
||||
#define VERSION_MINOR 9
|
||||
#define VERSION_PATCH 0
|
||||
|
||||
#define FIX_VERSION_MAJOR 1
|
||||
#define FIX_VERSION_MINOR 8
|
||||
#define FIX_VERSION_MINOR 9
|
||||
#define FIX_VERSION_PATCH 0
|
||||
|
@ -6,13 +6,38 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
_At the moment, the project does **not** adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). That is expected to change with version 1.0._
|
||||
|
||||
## [0.11.7] - Unreleased
|
||||
## [0.12.0] - Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- support for firmwares 1.9.0 and 2.3.0
|
||||
- Model T now defaults to entering passphrase on device. New trezorctl option `-P`
|
||||
enforces entering passphrase on host.
|
||||
- support for "passphrase always on device" mode on model T
|
||||
- new trezorctl command `get-session` and option `-s` allows entering passphrase once
|
||||
for multiple subsequent trezorctl operations
|
||||
- built-in functionality of UdpTransport to wait until an emulator comes up, and the
|
||||
related command `trezorctl wait-for-emulator`
|
||||
|
||||
### Changed
|
||||
|
||||
- API for `cosi` module was streamlined: `verify_m_of_n` is now `verify`, the old
|
||||
`verify` is `verify_combined`
|
||||
- internals of firmware parsing were reworked to support signing firmware headers
|
||||
- `get_default_client` respects `TREZOR_PATH` environment variable
|
||||
- UI callback `get_passphrase` has an additional argument `available_on_device`,
|
||||
indicating that the connected Trezor is capable of on-device entry
|
||||
|
||||
### Fixed
|
||||
|
||||
- trezorctl does not print empty line when there is no output
|
||||
|
||||
### Removed
|
||||
|
||||
- `btc.sign_tx` will not preload transaction data from `prev_txes`, as usage with TxApi
|
||||
is being phased out
|
||||
- PIN protection and passphrase protection for `ping()` command was removed
|
||||
|
||||
## [0.11.6] - 2019-12-30
|
||||
[0.11.6]: https://github.com/trezor/trezor-firmware/compare/python/v0.11.5...python/v0.11.6
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
__version__ = "0.11.6"
|
||||
__version__ = "0.12.0"
|
||||
|
||||
# fmt: off
|
||||
MINIMUM_FIRMWARE_VERSION = {
|
||||
|
@ -16,15 +16,9 @@
|
||||
|
||||
import click
|
||||
|
||||
from .. import device, messages
|
||||
from .. import device
|
||||
from . import ChoiceType
|
||||
|
||||
PASSPHRASE_SOURCE = {
|
||||
"ask": messages.PassphraseSourceType.ASK,
|
||||
"device": messages.PassphraseSourceType.DEVICE,
|
||||
"host": messages.PassphraseSourceType.HOST,
|
||||
}
|
||||
|
||||
ROTATION = {"north": 0, "east": 90, "south": 180, "west": 270}
|
||||
|
||||
|
||||
@ -145,10 +139,13 @@ def passphrase():
|
||||
|
||||
|
||||
@passphrase.command(name="enabled")
|
||||
@click.option("-f/-F", "--force-on-device/--no-force-on-device", default=None)
|
||||
@click.pass_obj
|
||||
def passphrase_enable(connect):
|
||||
def passphrase_enable(connect, force_on_device: bool):
|
||||
"""Enable passphrase."""
|
||||
return device.apply_settings(connect(), use_passphrase=True)
|
||||
return device.apply_settings(
|
||||
connect(), use_passphrase=True, passphrase_always_on_device=force_on_device
|
||||
)
|
||||
|
||||
|
||||
@passphrase.command(name="disabled")
|
||||
@ -156,19 +153,3 @@ def passphrase_enable(connect):
|
||||
def passphrase_disable(connect):
|
||||
"""Disable passphrase."""
|
||||
return device.apply_settings(connect(), use_passphrase=False)
|
||||
|
||||
|
||||
@passphrase.command(name="source")
|
||||
@click.argument("source", type=ChoiceType(PASSPHRASE_SOURCE))
|
||||
@click.pass_obj
|
||||
def passphrase_source(connect, source):
|
||||
"""Set passphrase source.
|
||||
|
||||
Configure how to enter passphrase on Trezor Model T. The options are:
|
||||
|
||||
\b
|
||||
ask - always ask where to enter passphrase
|
||||
device - always enter passphrase on device
|
||||
host - always enter passphrase on host
|
||||
"""
|
||||
return device.apply_settings(connect(), passphrase_source=source)
|
||||
|
@ -52,7 +52,6 @@ COMMAND_ALIASES = {
|
||||
"change-pin": settings.pin,
|
||||
"enable-passphrase": settings.passphrase_enable,
|
||||
"disable-passphrase": settings.passphrase_disable,
|
||||
"set-passphrase-source": settings.passphrase_source,
|
||||
"wipe-device": device.wipe,
|
||||
"reset-device": device.setup,
|
||||
"recovery-device": device.recover,
|
||||
@ -143,11 +142,26 @@ def configure_logging(verbose: int):
|
||||
@click.option(
|
||||
"-j", "--json", "is_json", is_flag=True, help="Print result as JSON object"
|
||||
)
|
||||
@click.option(
|
||||
"-P", "--passphrase-on-host", is_flag=True, help="Enter passphrase on host.",
|
||||
)
|
||||
@click.option(
|
||||
"-s",
|
||||
"--session-id",
|
||||
help="Resume given session ID.",
|
||||
default=os.environ.get("TREZOR_SESSION_ID"),
|
||||
)
|
||||
@click.version_option()
|
||||
@click.pass_context
|
||||
def cli(ctx, path, verbose, is_json):
|
||||
def cli(ctx, path, verbose, is_json, passphrase_on_host, session_id):
|
||||
configure_logging(verbose)
|
||||
|
||||
if session_id:
|
||||
try:
|
||||
session_id = bytes.fromhex(session_id)
|
||||
except ValueError:
|
||||
raise click.ClickException("Not a valid session id: {}".format(session_id))
|
||||
|
||||
def get_device():
|
||||
try:
|
||||
device = get_transport(path, prefix_search=False)
|
||||
@ -159,13 +173,17 @@ def cli(ctx, path, verbose, is_json):
|
||||
if path is not None:
|
||||
click.echo("Using path: {}".format(path))
|
||||
sys.exit(1)
|
||||
return TrezorClient(transport=device, ui=ui.ClickUI())
|
||||
return TrezorClient(
|
||||
transport=device,
|
||||
ui=ui.ClickUI(passphrase_on_host=passphrase_on_host),
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
ctx.obj = get_device
|
||||
|
||||
|
||||
@cli.resultcallback()
|
||||
def print_result(res, path, verbose, is_json):
|
||||
def print_result(res, is_json, **kwargs):
|
||||
if is_json:
|
||||
if isinstance(res, protobuf.MessageType):
|
||||
click.echo(json.dumps({res.__class__.__name__: res.__dict__}))
|
||||
@ -202,7 +220,7 @@ def list_devices():
|
||||
@cli.command()
|
||||
def version():
|
||||
"""Show version of trezorctl/trezorlib."""
|
||||
from trezorlib import __version__ as VERSION
|
||||
from .. import __version__ as VERSION
|
||||
|
||||
return VERSION
|
||||
|
||||
@ -215,17 +233,33 @@ def version():
|
||||
@cli.command()
|
||||
@click.argument("message")
|
||||
@click.option("-b", "--button-protection", is_flag=True)
|
||||
@click.option("-p", "--pin-protection", is_flag=True)
|
||||
@click.option("-r", "--passphrase-protection", is_flag=True)
|
||||
@click.pass_obj
|
||||
def ping(connect, message, button_protection, pin_protection, passphrase_protection):
|
||||
def ping(connect, message, button_protection):
|
||||
"""Send ping message."""
|
||||
return connect().ping(
|
||||
message,
|
||||
button_protection=button_protection,
|
||||
pin_protection=pin_protection,
|
||||
passphrase_protection=passphrase_protection,
|
||||
)
|
||||
return connect().ping(message, button_protection=button_protection)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_obj
|
||||
def get_session(connect):
|
||||
"""Get a session ID for subsequent commands.
|
||||
|
||||
Unlocks Trezor with a passphrase and returns a session ID. Use this session ID with
|
||||
`trezorctl -s SESSION_ID`, or set it to an environment variable `TREZOR_SESSION_ID`,
|
||||
to avoid having to enter passphrase for subsequent commands.
|
||||
|
||||
The session ID is valid until another client starts using Trezor, until the next
|
||||
get-session call, or until Trezor is disconnected.
|
||||
"""
|
||||
from ..btc import get_address
|
||||
from ..client import PASSPHRASE_TEST_PATH
|
||||
|
||||
client = connect()
|
||||
get_address(client, "Testnet", PASSPHRASE_TEST_PATH)
|
||||
if client.session_id is None:
|
||||
raise click.ClickException("Passphrase not enabled or firmware too old.")
|
||||
else:
|
||||
return client.session_id.hex()
|
||||
|
||||
|
||||
@cli.command()
|
||||
|
@ -23,6 +23,7 @@ from types import SimpleNamespace
|
||||
from mnemonic import Mnemonic
|
||||
|
||||
from . import MINIMUM_FIRMWARE_VERSION, exceptions, messages, tools
|
||||
from .messages import Capability
|
||||
|
||||
if sys.version_info.major < 3:
|
||||
raise Exception("Trezorlib does not support Python 2 anymore.")
|
||||
@ -32,6 +33,9 @@ LOG = logging.getLogger(__name__)
|
||||
VENDORS = ("bitcointrezor.com", "trezor.io")
|
||||
MAX_PASSPHRASE_LENGTH = 50
|
||||
|
||||
PASSPHRASE_ON_DEVICE = object()
|
||||
PASSPHRASE_TEST_PATH = tools.parse_path("44h/1h/19h/0/1337")
|
||||
|
||||
DEPRECATION_ERROR = """
|
||||
Incompatible Trezor library detected.
|
||||
|
||||
@ -107,15 +111,17 @@ class TrezorClient:
|
||||
- passphrase request (ask the user to enter a passphrase)
|
||||
See `trezorlib.ui` for details.
|
||||
|
||||
You can supply a `state` you saved in the previous session. If you do,
|
||||
the user might not need to enter their passphrase again.
|
||||
You can supply a `session_id` you might have saved in the previous session.
|
||||
If you do, the user might not need to enter their passphrase again.
|
||||
"""
|
||||
|
||||
def __init__(self, transport, ui=_NO_UI_OBJECT, state=None):
|
||||
def __init__(
|
||||
self, transport, ui=_NO_UI_OBJECT, session_id=None,
|
||||
):
|
||||
LOG.info("creating client instance for device: {}".format(transport.get_path()))
|
||||
self.transport = transport
|
||||
self.ui = ui
|
||||
self.state = state
|
||||
self.session_id = session_id
|
||||
|
||||
# XXX remove when old Electrum has been cycled out.
|
||||
# explanation: We changed the API in 0.11 and this broke older versions
|
||||
@ -177,31 +183,48 @@ 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:
|
||||
self.call_raw(messages.Cancel())
|
||||
raise ValueError("Passphrase too long")
|
||||
|
||||
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:
|
||||
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 RuntimeError("Device is not capable of entering passphrase")
|
||||
else:
|
||||
return send_passphrase(on_device=True)
|
||||
|
||||
# else process host-entered passphrase
|
||||
passphrase = Mnemonic.normalize_string(passphrase)
|
||||
if len(passphrase) > MAX_PASSPHRASE_LENGTH:
|
||||
self.call_raw(messages.Cancel())
|
||||
raise ValueError("Passphrase too long")
|
||||
|
||||
return send_passphrase(passphrase, on_device=False)
|
||||
|
||||
def _callback_button(self, msg):
|
||||
__tracebackhide__ = True # for pytest # pylint: disable=W0612
|
||||
# do this raw - send ButtonAck first, notify UI later
|
||||
@ -229,7 +252,7 @@ class TrezorClient:
|
||||
|
||||
@tools.session
|
||||
def init_device(self):
|
||||
resp = self.call_raw(messages.Initialize(state=self.state))
|
||||
resp = self.call_raw(messages.Initialize(session_id=self.session_id))
|
||||
if not isinstance(resp, messages.Features):
|
||||
raise exceptions.TrezorException("Unexpected initial response")
|
||||
else:
|
||||
@ -245,6 +268,8 @@ class TrezorClient:
|
||||
self.features.patch_version,
|
||||
)
|
||||
self.check_firmware_version(warn_only=True)
|
||||
if self.features.session_id is not None:
|
||||
self.session_id = self.features.session_id
|
||||
|
||||
def is_outdated(self):
|
||||
if self.features.bootloader_mode:
|
||||
@ -262,17 +287,13 @@ class TrezorClient:
|
||||
|
||||
@tools.expect(messages.Success, field="message")
|
||||
def ping(
|
||||
self,
|
||||
msg,
|
||||
button_protection=False,
|
||||
pin_protection=False,
|
||||
passphrase_protection=False,
|
||||
self, msg, button_protection=False,
|
||||
):
|
||||
# We would like ping to work on any valid TrezorClient instance, but
|
||||
# due to the protection modes, we need to go through self.call, and that will
|
||||
# raise an exception if the firmware is too old.
|
||||
# So we short-circuit the simplest variant of ping with call_raw.
|
||||
if not button_protection and not pin_protection and not passphrase_protection:
|
||||
if not button_protection:
|
||||
# XXX this should be: `with self:`
|
||||
try:
|
||||
self.open()
|
||||
@ -280,12 +301,7 @@ class TrezorClient:
|
||||
finally:
|
||||
self.close()
|
||||
|
||||
msg = messages.Ping(
|
||||
message=msg,
|
||||
button_protection=button_protection,
|
||||
pin_protection=pin_protection,
|
||||
passphrase_protection=passphrase_protection,
|
||||
)
|
||||
msg = messages.Ping(message=msg, button_protection=button_protection,)
|
||||
return self.call(msg)
|
||||
|
||||
def get_device_id(self):
|
||||
@ -295,7 +311,7 @@ class TrezorClient:
|
||||
def clear_session(self):
|
||||
resp = self.call_raw(messages.ClearSession())
|
||||
if isinstance(resp, messages.Success):
|
||||
self.state = None
|
||||
self.session_id = None
|
||||
self.init_device()
|
||||
return resp.message
|
||||
else:
|
||||
|
@ -226,7 +226,7 @@ class DebugUI:
|
||||
else:
|
||||
return self.debuglink.read_pin_encoded()
|
||||
|
||||
def get_passphrase(self):
|
||||
def get_passphrase(self, available_on_device):
|
||||
return self.passphrase
|
||||
|
||||
|
||||
|
@ -51,7 +51,7 @@ def apply_settings(
|
||||
language=None,
|
||||
use_passphrase=None,
|
||||
homescreen=None,
|
||||
passphrase_source=None,
|
||||
passphrase_always_on_device=None,
|
||||
auto_lock_delay_ms=None,
|
||||
display_rotation=None,
|
||||
):
|
||||
@ -64,8 +64,8 @@ def apply_settings(
|
||||
settings.use_passphrase = use_passphrase
|
||||
if homescreen is not None:
|
||||
settings.homescreen = homescreen
|
||||
if passphrase_source is not None:
|
||||
settings.passphrase_source = passphrase_source
|
||||
if passphrase_always_on_device is not None:
|
||||
settings.passphrase_always_on_device = passphrase_always_on_device
|
||||
if auto_lock_delay_ms is not None:
|
||||
settings.auto_lock_delay_ms = auto_lock_delay_ms
|
||||
if display_rotation is not None:
|
||||
|
@ -6,7 +6,6 @@ if __debug__:
|
||||
try:
|
||||
from typing import Dict, List # noqa: F401
|
||||
from typing_extensions import Literal # noqa: F401
|
||||
EnumTypePassphraseSourceType = Literal[0, 1, 2]
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@ -20,17 +19,17 @@ class ApplySettings(p.MessageType):
|
||||
label: str = None,
|
||||
use_passphrase: bool = None,
|
||||
homescreen: bytes = None,
|
||||
passphrase_source: EnumTypePassphraseSourceType = None,
|
||||
auto_lock_delay_ms: int = None,
|
||||
display_rotation: int = None,
|
||||
passphrase_always_on_device: bool = None,
|
||||
) -> None:
|
||||
self.language = language
|
||||
self.label = label
|
||||
self.use_passphrase = use_passphrase
|
||||
self.homescreen = homescreen
|
||||
self.passphrase_source = passphrase_source
|
||||
self.auto_lock_delay_ms = auto_lock_delay_ms
|
||||
self.display_rotation = display_rotation
|
||||
self.passphrase_always_on_device = passphrase_always_on_device
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
@ -39,7 +38,7 @@ class ApplySettings(p.MessageType):
|
||||
2: ('label', p.UnicodeType, 0),
|
||||
3: ('use_passphrase', p.BoolType, 0),
|
||||
4: ('homescreen', p.BytesType, 0),
|
||||
5: ('passphrase_source', p.EnumType("PassphraseSourceType", (0, 1, 2)), 0),
|
||||
6: ('auto_lock_delay_ms', p.UVarintType, 0),
|
||||
7: ('display_rotation', p.UVarintType, 0),
|
||||
8: ('passphrase_always_on_device', p.BoolType, 0),
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ if __debug__:
|
||||
try:
|
||||
from typing import Dict, List # noqa: F401
|
||||
from typing_extensions import Literal # noqa: F401
|
||||
EnumTypeButtonRequestType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
|
||||
EnumTypeButtonRequestType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19]
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@ -23,5 +23,5 @@ class ButtonRequest(p.MessageType):
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
return {
|
||||
1: ('code', p.EnumType("ButtonRequestType", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18)), 0),
|
||||
1: ('code', p.EnumType("ButtonRequestType", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19)), 0),
|
||||
}
|
||||
|
@ -16,8 +16,8 @@ Address = 10 # type: Literal[10]
|
||||
PublicKey = 11 # type: Literal[11]
|
||||
MnemonicWordCount = 12 # type: Literal[12]
|
||||
MnemonicInput = 13 # type: Literal[13]
|
||||
PassphraseType = 14 # type: Literal[14]
|
||||
UnknownDerivationPath = 15 # type: Literal[15]
|
||||
RecoveryHomepage = 16 # type: Literal[16]
|
||||
Success = 17 # type: Literal[17]
|
||||
Warning = 18 # type: Literal[18]
|
||||
PassphraseEntry = 19 # type: Literal[19]
|
||||
|
@ -19,3 +19,4 @@ Tezos = 13 # type: Literal[13]
|
||||
U2F = 14 # type: Literal[14]
|
||||
Shamir = 15 # type: Literal[15]
|
||||
ShamirGroups = 16 # type: Literal[16]
|
||||
PassphraseEntry = 17 # type: Literal[17]
|
||||
|
@ -10,5 +10,5 @@ if __debug__:
|
||||
pass
|
||||
|
||||
|
||||
class PassphraseStateAck(p.MessageType):
|
||||
class Deprecated_PassphraseStateAck(p.MessageType):
|
||||
MESSAGE_WIRE_TYPE = 78
|
@ -10,7 +10,7 @@ if __debug__:
|
||||
pass
|
||||
|
||||
|
||||
class PassphraseStateRequest(p.MessageType):
|
||||
class Deprecated_PassphraseStateRequest(p.MessageType):
|
||||
MESSAGE_WIRE_TYPE = 77
|
||||
|
||||
def __init__(
|
@ -6,7 +6,7 @@ if __debug__:
|
||||
try:
|
||||
from typing import Dict, List # noqa: F401
|
||||
from typing_extensions import Literal # noqa: F401
|
||||
EnumTypeCapability = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
|
||||
EnumTypeCapability = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
|
||||
EnumTypeBackupType = Literal[0, 1, 2]
|
||||
except ImportError:
|
||||
pass
|
||||
@ -32,7 +32,6 @@ class Features(p.MessageType):
|
||||
bootloader_hash: bytes = None,
|
||||
imported: bool = None,
|
||||
pin_cached: bool = None,
|
||||
passphrase_cached: bool = None,
|
||||
firmware_present: bool = None,
|
||||
needs_backup: bool = None,
|
||||
flags: int = None,
|
||||
@ -50,6 +49,8 @@ class Features(p.MessageType):
|
||||
sd_card_present: bool = None,
|
||||
sd_protection: bool = None,
|
||||
wipe_code_protection: bool = None,
|
||||
session_id: bytes = None,
|
||||
passphrase_always_on_device: bool = None,
|
||||
) -> None:
|
||||
self.vendor = vendor
|
||||
self.major_version = major_version
|
||||
@ -66,7 +67,6 @@ class Features(p.MessageType):
|
||||
self.bootloader_hash = bootloader_hash
|
||||
self.imported = imported
|
||||
self.pin_cached = pin_cached
|
||||
self.passphrase_cached = passphrase_cached
|
||||
self.firmware_present = firmware_present
|
||||
self.needs_backup = needs_backup
|
||||
self.flags = flags
|
||||
@ -84,6 +84,8 @@ class Features(p.MessageType):
|
||||
self.sd_card_present = sd_card_present
|
||||
self.sd_protection = sd_protection
|
||||
self.wipe_code_protection = wipe_code_protection
|
||||
self.session_id = session_id
|
||||
self.passphrase_always_on_device = passphrase_always_on_device
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
@ -103,7 +105,6 @@ class Features(p.MessageType):
|
||||
14: ('bootloader_hash', p.BytesType, 0),
|
||||
15: ('imported', p.BoolType, 0),
|
||||
16: ('pin_cached', p.BoolType, 0),
|
||||
17: ('passphrase_cached', p.BoolType, 0),
|
||||
18: ('firmware_present', p.BoolType, 0),
|
||||
19: ('needs_backup', p.BoolType, 0),
|
||||
20: ('flags', p.UVarintType, 0),
|
||||
@ -116,9 +117,11 @@ class Features(p.MessageType):
|
||||
27: ('unfinished_backup', p.BoolType, 0),
|
||||
28: ('no_backup', p.BoolType, 0),
|
||||
29: ('recovery_mode', p.BoolType, 0),
|
||||
30: ('capabilities', p.EnumType("Capability", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)), p.FLAG_REPEATED),
|
||||
30: ('capabilities', p.EnumType("Capability", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)), p.FLAG_REPEATED),
|
||||
31: ('backup_type', p.EnumType("BackupType", (0, 1, 2)), 0),
|
||||
32: ('sd_card_present', p.BoolType, 0),
|
||||
33: ('sd_protection', p.BoolType, 0),
|
||||
34: ('wipe_code_protection', p.BoolType, 0),
|
||||
35: ('session_id', p.BytesType, 0),
|
||||
36: ('passphrase_always_on_device', p.BoolType, 0),
|
||||
}
|
||||
|
@ -15,15 +15,12 @@ class Initialize(p.MessageType):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
state: bytes = None,
|
||||
skip_passphrase: bool = None,
|
||||
session_id: bytes = None,
|
||||
) -> None:
|
||||
self.state = state
|
||||
self.skip_passphrase = skip_passphrase
|
||||
self.session_id = session_id
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
return {
|
||||
1: ('state', p.BytesType, 0),
|
||||
2: ('skip_passphrase', p.BoolType, 0),
|
||||
1: ('session_id', p.BytesType, 0),
|
||||
}
|
||||
|
@ -27,8 +27,6 @@ EntropyRequest = 35 # type: Literal[35]
|
||||
EntropyAck = 36 # type: Literal[36]
|
||||
PassphraseRequest = 41 # type: Literal[41]
|
||||
PassphraseAck = 42 # type: Literal[42]
|
||||
PassphraseStateRequest = 77 # type: Literal[77]
|
||||
PassphraseStateAck = 78 # type: Literal[78]
|
||||
RecoveryDevice = 45 # type: Literal[45]
|
||||
WordRequest = 46 # type: Literal[46]
|
||||
WordAck = 47 # type: Literal[47]
|
||||
@ -38,6 +36,8 @@ SdProtect = 79 # type: Literal[79]
|
||||
GetNextU2FCounter = 80 # type: Literal[80]
|
||||
NextU2FCounter = 81 # type: Literal[81]
|
||||
ChangeWipeCode = 82 # type: Literal[82]
|
||||
Deprecated_PassphraseStateRequest = 77 # type: Literal[77]
|
||||
Deprecated_PassphraseStateAck = 78 # type: Literal[78]
|
||||
FirmwareErase = 6 # type: Literal[6]
|
||||
FirmwareUpload = 7 # type: Literal[7]
|
||||
FirmwareRequest = 8 # type: Literal[8]
|
||||
|
@ -16,14 +16,17 @@ class PassphraseAck(p.MessageType):
|
||||
def __init__(
|
||||
self,
|
||||
passphrase: str = None,
|
||||
state: bytes = None,
|
||||
_state: bytes = None,
|
||||
on_device: bool = None,
|
||||
) -> None:
|
||||
self.passphrase = passphrase
|
||||
self.state = state
|
||||
self._state = _state
|
||||
self.on_device = on_device
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
return {
|
||||
1: ('passphrase', p.UnicodeType, 0),
|
||||
2: ('state', p.BytesType, 0),
|
||||
2: ('_state', p.BytesType, 0),
|
||||
3: ('on_device', p.BoolType, 0),
|
||||
}
|
||||
|
@ -15,12 +15,12 @@ class PassphraseRequest(p.MessageType):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
on_device: bool = None,
|
||||
_on_device: bool = None,
|
||||
) -> None:
|
||||
self.on_device = on_device
|
||||
self._on_device = _on_device
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
return {
|
||||
1: ('on_device', p.BoolType, 0),
|
||||
1: ('_on_device', p.BoolType, 0),
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
# Automatically generated by pb2py
|
||||
# fmt: off
|
||||
if False:
|
||||
from typing_extensions import Literal
|
||||
|
||||
ASK = 0 # type: Literal[0]
|
||||
DEVICE = 1 # type: Literal[1]
|
||||
HOST = 2 # type: Literal[2]
|
@ -17,19 +17,13 @@ class Ping(p.MessageType):
|
||||
self,
|
||||
message: str = None,
|
||||
button_protection: bool = None,
|
||||
pin_protection: bool = None,
|
||||
passphrase_protection: bool = None,
|
||||
) -> None:
|
||||
self.message = message
|
||||
self.button_protection = button_protection
|
||||
self.pin_protection = pin_protection
|
||||
self.passphrase_protection = passphrase_protection
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
return {
|
||||
1: ('message', p.UnicodeType, 0),
|
||||
2: ('button_protection', p.BoolType, 0),
|
||||
3: ('pin_protection', p.BoolType, 0),
|
||||
4: ('passphrase_protection', p.BoolType, 0),
|
||||
}
|
||||
|
@ -55,6 +55,8 @@ from .DebugLinkState import DebugLinkState
|
||||
from .DebugLinkStop import DebugLinkStop
|
||||
from .DebugMoneroDiagAck import DebugMoneroDiagAck
|
||||
from .DebugMoneroDiagRequest import DebugMoneroDiagRequest
|
||||
from .Deprecated_PassphraseStateAck import Deprecated_PassphraseStateAck
|
||||
from .Deprecated_PassphraseStateRequest import Deprecated_PassphraseStateRequest
|
||||
from .ECDHSessionKey import ECDHSessionKey
|
||||
from .Entropy import Entropy
|
||||
from .EntropyAck import EntropyAck
|
||||
@ -196,8 +198,6 @@ from .NEMTransfer import NEMTransfer
|
||||
from .NextU2FCounter import NextU2FCounter
|
||||
from .PassphraseAck import PassphraseAck
|
||||
from .PassphraseRequest import PassphraseRequest
|
||||
from .PassphraseStateAck import PassphraseStateAck
|
||||
from .PassphraseStateRequest import PassphraseStateRequest
|
||||
from .PinMatrixAck import PinMatrixAck
|
||||
from .PinMatrixRequest import PinMatrixRequest
|
||||
from .Ping import Ping
|
||||
@ -283,7 +283,6 @@ from . import NEMModificationType
|
||||
from . import NEMMosaicLevy
|
||||
from . import NEMSupplyChangeType
|
||||
from . import OutputScriptType
|
||||
from . import PassphraseSourceType
|
||||
from . import PinMatrixRequestType
|
||||
from . import RecoveryDeviceType
|
||||
from . import RequestType
|
||||
|
@ -20,6 +20,7 @@ import click
|
||||
from mnemonic import Mnemonic
|
||||
|
||||
from . import device
|
||||
from .client import PASSPHRASE_ON_DEVICE
|
||||
from .exceptions import Cancelled
|
||||
from .messages import PinMatrixRequestType, WordRequestType
|
||||
|
||||
@ -58,10 +59,11 @@ def prompt(*args, **kwargs):
|
||||
|
||||
|
||||
class ClickUI:
|
||||
def __init__(self, always_prompt=False):
|
||||
def __init__(self, always_prompt=False, passphrase_on_host=False):
|
||||
self.pinmatrix_shown = False
|
||||
self.prompt_shown = False
|
||||
self.always_prompt = always_prompt
|
||||
self.passphrase_on_host = passphrase_on_host
|
||||
|
||||
def button_request(self, code):
|
||||
if not self.prompt_shown:
|
||||
@ -98,7 +100,10 @@ class ClickUI:
|
||||
else:
|
||||
return pin
|
||||
|
||||
def get_passphrase(self):
|
||||
def get_passphrase(self, available_on_device):
|
||||
if available_on_device and not self.passphrase_on_host:
|
||||
return PASSPHRASE_ON_DEVICE
|
||||
|
||||
if os.getenv("PASSPHRASE") is not None:
|
||||
echo("Passphrase required. Using PASSPHRASE environment variable.")
|
||||
return os.getenv("PASSPHRASE")
|
||||
|
1
tests/.gitignore
vendored
1
tests/.gitignore
vendored
@ -1 +1,2 @@
|
||||
junit.xml
|
||||
trezor.log
|
||||
|
@ -20,8 +20,7 @@ import pytest
|
||||
|
||||
from trezorlib import debuglink, log
|
||||
from trezorlib.debuglink import TrezorClientDebugLink
|
||||
from trezorlib.device import apply_settings, wipe as wipe_device
|
||||
from trezorlib.messages.PassphraseSourceType import HOST as PASSPHRASE_ON_HOST
|
||||
from trezorlib.device import wipe as wipe_device
|
||||
from trezorlib.transport import enumerate_devices, get_transport
|
||||
|
||||
from . import ui_tests
|
||||
@ -36,8 +35,8 @@ def get_device():
|
||||
try:
|
||||
transport = get_transport(path)
|
||||
return TrezorClientDebugLink(transport, auto_interact=not interact)
|
||||
except Exception as e:
|
||||
raise RuntimeError("Failed to open debuglink for {}".format(path)) from e
|
||||
except Exception:
|
||||
pytest.exit("Failed to open debuglink for {}".format(path), 3)
|
||||
|
||||
else:
|
||||
devices = enumerate_devices()
|
||||
@ -47,7 +46,7 @@ def get_device():
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
raise RuntimeError("No debuggable device found")
|
||||
pytest.exit("No debuggable device found", 3)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@ -130,8 +129,6 @@ def client(request):
|
||||
needs_backup=setup_params["needs_backup"],
|
||||
no_backup=setup_params["no_backup"],
|
||||
)
|
||||
if setup_params["passphrase"] and client.features.model != "1":
|
||||
apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST)
|
||||
|
||||
if setup_params["pin"]:
|
||||
# ClearSession locks the device. We only do that if the PIN is set.
|
||||
|
@ -20,7 +20,7 @@ from trezorlib import device, messages
|
||||
class TestBasic:
|
||||
def test_features(self, client):
|
||||
f0 = client.features
|
||||
f1 = client.call(messages.Initialize())
|
||||
f1 = client.call(messages.Initialize(f0.session_id))
|
||||
assert f0 == f1
|
||||
|
||||
def test_ping(self, client):
|
||||
|
@ -34,7 +34,7 @@ class TestDebuglink:
|
||||
|
||||
@pytest.mark.setup_client(mnemonic=MNEMONIC12, pin="1234", passphrase=True)
|
||||
def test_pin(self, client):
|
||||
resp = client.call_raw(messages.Ping(message="test", pin_protection=True))
|
||||
resp = client.call_raw(messages.GetAddress())
|
||||
assert isinstance(resp, messages.PinMatrixRequest)
|
||||
|
||||
pin, matrix = client.debug.read_pin()
|
||||
@ -43,4 +43,7 @@ class TestDebuglink:
|
||||
|
||||
pin_encoded = client.debug.read_pin_encoded()
|
||||
resp = client.call_raw(messages.PinMatrixAck(pin=pin_encoded))
|
||||
assert isinstance(resp, messages.Success)
|
||||
assert isinstance(resp, messages.PassphraseRequest)
|
||||
|
||||
resp = client.call_raw(messages.PassphraseAck(passphrase=""))
|
||||
assert isinstance(resp, messages.Address)
|
||||
|
@ -18,7 +18,7 @@ import time
|
||||
|
||||
import pytest
|
||||
|
||||
from trezorlib import device, messages as proto
|
||||
from trezorlib import btc, device, messages as proto
|
||||
|
||||
EXPECTED_RESPONSES_NOPIN = [proto.ButtonRequest(), proto.Success(), proto.Features()]
|
||||
EXPECTED_RESPONSES_PIN = [proto.PinMatrixRequest()] + EXPECTED_RESPONSES_NOPIN
|
||||
@ -91,13 +91,13 @@ class TestMsgApplysettings:
|
||||
time.sleep(0.1) # sleep less than auto-lock delay
|
||||
with client:
|
||||
# No PIN protection is required.
|
||||
client.set_expected_responses([proto.Success()])
|
||||
client.ping(msg="", pin_protection=True)
|
||||
client.set_expected_responses([proto.Address()])
|
||||
btc.get_address(client, "Testnet", [0])
|
||||
|
||||
time.sleep(10.1) # sleep more than auto-lock delay
|
||||
with client:
|
||||
client.set_expected_responses([proto.PinMatrixRequest(), proto.Success()])
|
||||
client.ping(msg="", pin_protection=True)
|
||||
client.set_expected_responses([proto.PinMatrixRequest(), proto.Address()])
|
||||
btc.get_address(client, "Testnet", [0])
|
||||
|
||||
@pytest.mark.skip_t2
|
||||
def test_apply_minimal_auto_lock_delay(self, client):
|
||||
@ -114,16 +114,16 @@ class TestMsgApplysettings:
|
||||
time.sleep(0.1) # sleep less than auto-lock delay
|
||||
with client:
|
||||
# No PIN protection is required.
|
||||
client.set_expected_responses([proto.Success()])
|
||||
client.ping(msg="", pin_protection=True)
|
||||
client.set_expected_responses([proto.Address()])
|
||||
btc.get_address(client, "Testnet", [0])
|
||||
|
||||
time.sleep(2) # sleep less than the minimal auto-lock delay
|
||||
with client:
|
||||
# No PIN protection is required.
|
||||
client.set_expected_responses([proto.Success()])
|
||||
client.ping(msg="", pin_protection=True)
|
||||
client.set_expected_responses([proto.Address()])
|
||||
btc.get_address(client, "Testnet", [0])
|
||||
|
||||
time.sleep(10.1) # sleep more than the minimal auto-lock delay
|
||||
with client:
|
||||
client.set_expected_responses([proto.PinMatrixRequest(), proto.Success()])
|
||||
client.ping(msg="", pin_protection=True)
|
||||
client.set_expected_responses([proto.PinMatrixRequest(), proto.Address()])
|
||||
btc.get_address(client, "Testnet", [0])
|
||||
|
@ -119,10 +119,7 @@ def test_cardano_sign_tx(
|
||||
inputs = [cardano.create_input(i) for i in inputs]
|
||||
outputs = [cardano.create_output(o) for o in outputs]
|
||||
|
||||
expected_responses = [
|
||||
messages.PassphraseRequest(),
|
||||
messages.PassphraseStateRequest(),
|
||||
]
|
||||
expected_responses = [messages.PassphraseRequest()]
|
||||
expected_responses += [
|
||||
messages.CardanoTxRequest(tx_index=i) for i in range(len(transactions))
|
||||
]
|
||||
|
@ -225,5 +225,5 @@ def test_set_pin_to_wipe_code(client):
|
||||
# Check that there is no PIN protection.
|
||||
client.init_device()
|
||||
assert client.features.pin_protection is False
|
||||
ret = client.call_raw(messages.Ping(pin_protection=True))
|
||||
assert isinstance(ret, messages.Success)
|
||||
resp = client.call_raw(messages.GetAddress())
|
||||
assert isinstance(resp, messages.Address)
|
||||
|
@ -29,8 +29,8 @@ class TestMsgChangepin:
|
||||
assert features.pin_protection is False
|
||||
|
||||
# Check that there's no PIN protection
|
||||
ret = client.call_raw(proto.Ping(pin_protection=True))
|
||||
assert isinstance(ret, proto.Success)
|
||||
ret = client.call_raw(proto.GetAddress())
|
||||
assert isinstance(ret, proto.Address)
|
||||
|
||||
# Let's set new PIN
|
||||
ret = client.call_raw(proto.ChangePin())
|
||||
@ -66,7 +66,7 @@ class TestMsgChangepin:
|
||||
assert features.pin_protection is True
|
||||
|
||||
# Check that there's PIN protection
|
||||
ret = client.call_raw(proto.Ping(pin_protection=True))
|
||||
ret = client.call_raw(proto.GetAddress())
|
||||
assert isinstance(ret, proto.PinMatrixRequest)
|
||||
client.call_raw(proto.Cancel())
|
||||
|
||||
@ -112,7 +112,7 @@ class TestMsgChangepin:
|
||||
assert features.pin_protection is True
|
||||
|
||||
# Check that there's PIN protection
|
||||
ret = client.call_raw(proto.Ping(pin_protection=True))
|
||||
ret = client.call_raw(proto.GetAddress())
|
||||
assert isinstance(ret, proto.PinMatrixRequest)
|
||||
client.call_raw(proto.Cancel())
|
||||
|
||||
@ -135,16 +135,16 @@ class TestMsgChangepin:
|
||||
# Check that there's no PIN protection now
|
||||
features = client.call_raw(proto.Initialize())
|
||||
assert features.pin_protection is False
|
||||
ret = client.call_raw(proto.Ping(pin_protection=True))
|
||||
assert isinstance(ret, proto.Success)
|
||||
ret = client.call_raw(proto.GetAddress())
|
||||
assert isinstance(ret, proto.Address)
|
||||
|
||||
def test_set_failed(self, client):
|
||||
features = client.call_raw(proto.Initialize())
|
||||
assert features.pin_protection is False
|
||||
|
||||
# Check that there's no PIN protection
|
||||
ret = client.call_raw(proto.Ping(pin_protection=True))
|
||||
assert isinstance(ret, proto.Success)
|
||||
ret = client.call_raw(proto.GetAddress())
|
||||
assert isinstance(ret, proto.Address)
|
||||
|
||||
# Let's set new PIN
|
||||
ret = client.call_raw(proto.ChangePin())
|
||||
@ -170,8 +170,8 @@ class TestMsgChangepin:
|
||||
# Check that there's still no PIN protection now
|
||||
features = client.call_raw(proto.Initialize())
|
||||
assert features.pin_protection is False
|
||||
ret = client.call_raw(proto.Ping(pin_protection=True))
|
||||
assert isinstance(ret, proto.Success)
|
||||
ret = client.call_raw(proto.GetAddress())
|
||||
assert isinstance(ret, proto.Address)
|
||||
|
||||
@pytest.mark.setup_client(pin=True)
|
||||
def test_set_failed_2(self, client):
|
||||
@ -211,8 +211,8 @@ class TestMsgChangepin:
|
||||
|
||||
def check_pin(self, client, pin):
|
||||
client.clear_session()
|
||||
ret = client.call_raw(proto.Ping(pin_protection=True))
|
||||
ret = client.call_raw(proto.GetAddress())
|
||||
assert isinstance(ret, proto.PinMatrixRequest)
|
||||
pin_encoded = client.debug.encode_pin(pin)
|
||||
ret = client.call_raw(proto.PinMatrixAck(pin=pin_encoded))
|
||||
assert isinstance(ret, proto.Success)
|
||||
assert isinstance(ret, proto.Address)
|
||||
|
@ -16,78 +16,41 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from trezorlib import messages as proto
|
||||
from trezorlib import messages
|
||||
from trezorlib.btc import get_public_node
|
||||
from trezorlib.tools import parse_path
|
||||
|
||||
ADDRESS_N = parse_path("44'/0'/0'")
|
||||
XPUB = "xpub6BiVtCpG9fQPxnPmHXG8PhtzQdWC2Su4qWu6XW9tpWFYhxydCLJGrWBJZ5H6qTAHdPQ7pQhtpjiYZVZARo14qHiay2fvrX996oEP42u8wZy"
|
||||
|
||||
|
||||
@pytest.mark.skip_t2
|
||||
class TestMsgClearsession:
|
||||
@pytest.mark.setup_client(pin=True, passphrase=True)
|
||||
def test_clearsession(self, client):
|
||||
with client:
|
||||
client.set_expected_responses(
|
||||
[
|
||||
proto.ButtonRequest(code=proto.ButtonRequestType.ProtectCall),
|
||||
proto.PinMatrixRequest(),
|
||||
proto.PassphraseRequest(),
|
||||
proto.Success(),
|
||||
]
|
||||
)
|
||||
res = client.ping(
|
||||
"random data",
|
||||
button_protection=True,
|
||||
pin_protection=True,
|
||||
passphrase_protection=True,
|
||||
)
|
||||
assert res == "random data"
|
||||
@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()]
|
||||
|
||||
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"
|
||||
cached_responses = [messages.PublicKey()]
|
||||
|
||||
client.clear_session()
|
||||
with client:
|
||||
client.set_expected_responses(init_responses + cached_responses)
|
||||
assert get_public_node(client, ADDRESS_N).xpub == XPUB
|
||||
|
||||
# session cache is cleared
|
||||
with client:
|
||||
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(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"
|
||||
client.clear_session()
|
||||
|
||||
# session cache is cleared
|
||||
with client:
|
||||
client.set_expected_responses(init_responses + cached_responses)
|
||||
assert get_public_node(client, ADDRESS_N).xpub == XPUB
|
||||
|
||||
with client:
|
||||
# pin and passphrase are cached
|
||||
client.set_expected_responses(cached_responses)
|
||||
assert get_public_node(client, ADDRESS_N).xpub == XPUB
|
||||
|
@ -18,7 +18,6 @@ import pytest
|
||||
|
||||
from trezorlib import btc, debuglink, device
|
||||
from trezorlib.messages import BackupType
|
||||
from trezorlib.messages.PassphraseSourceType import HOST as PASSPHRASE_ON_HOST
|
||||
|
||||
from ..common import (
|
||||
MNEMONIC12,
|
||||
@ -53,8 +52,6 @@ class TestDeviceLoad:
|
||||
passphrase_protection=True,
|
||||
label="test",
|
||||
)
|
||||
if client.features.model == "T":
|
||||
device.apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST)
|
||||
client.set_passphrase("passphrase")
|
||||
state = client.debug.state()
|
||||
assert state.mnemonic_secret == MNEMONIC12.encode()
|
||||
@ -108,7 +105,6 @@ class TestDeviceLoad:
|
||||
u"Neuve\u030cr\u030citelne\u030c bezpec\u030cne\u0301 hesli\u0301c\u030cko"
|
||||
)
|
||||
|
||||
device.wipe(client)
|
||||
debuglink.load_device(
|
||||
client,
|
||||
mnemonic=words_nfkd,
|
||||
@ -118,8 +114,6 @@ class TestDeviceLoad:
|
||||
language="en-US",
|
||||
skip_checksum=True,
|
||||
)
|
||||
if client.features.model == "T":
|
||||
device.apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST)
|
||||
client.set_passphrase(passphrase_nfkd)
|
||||
address_nfkd = btc.get_address(client, "Bitcoin", [])
|
||||
|
||||
@ -133,8 +127,6 @@ class TestDeviceLoad:
|
||||
language="en-US",
|
||||
skip_checksum=True,
|
||||
)
|
||||
if client.features.model == "T":
|
||||
device.apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST)
|
||||
client.set_passphrase(passphrase_nfc)
|
||||
address_nfc = btc.get_address(client, "Bitcoin", [])
|
||||
|
||||
@ -148,8 +140,6 @@ class TestDeviceLoad:
|
||||
language="en-US",
|
||||
skip_checksum=True,
|
||||
)
|
||||
if client.features.model == "T":
|
||||
device.apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST)
|
||||
client.set_passphrase(passphrase_nfkc)
|
||||
address_nfkc = btc.get_address(client, "Bitcoin", [])
|
||||
|
||||
@ -163,8 +153,6 @@ class TestDeviceLoad:
|
||||
language="en-US",
|
||||
skip_checksum=True,
|
||||
)
|
||||
if client.features.model == "T":
|
||||
device.apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST)
|
||||
client.set_passphrase(passphrase_nfd)
|
||||
address_nfd = btc.get_address(client, "Bitcoin", [])
|
||||
|
||||
|
@ -19,8 +19,8 @@ import pytest
|
||||
from trezorlib import messages as proto
|
||||
|
||||
|
||||
@pytest.mark.skip_t2
|
||||
class TestMsgPing:
|
||||
@pytest.mark.skip_ui
|
||||
@pytest.mark.setup_client(pin=True, passphrase=True)
|
||||
def test_ping(self, client):
|
||||
with client:
|
||||
@ -37,48 +37,3 @@ class TestMsgPing:
|
||||
)
|
||||
res = client.ping("random data", button_protection=True)
|
||||
assert res == "random data"
|
||||
|
||||
with client:
|
||||
client.set_expected_responses([proto.PinMatrixRequest(), proto.Success()])
|
||||
res = client.ping("random data", pin_protection=True)
|
||||
assert res == "random data"
|
||||
|
||||
with client:
|
||||
client.set_expected_responses([proto.PassphraseRequest(), proto.Success()])
|
||||
res = client.ping("random data", passphrase_protection=True)
|
||||
assert res == "random data"
|
||||
|
||||
@pytest.mark.setup_client(pin=True, passphrase=True)
|
||||
def test_ping_caching(self, client):
|
||||
with client:
|
||||
client.set_expected_responses(
|
||||
[
|
||||
proto.ButtonRequest(code=proto.ButtonRequestType.ProtectCall),
|
||||
proto.PinMatrixRequest(),
|
||||
proto.PassphraseRequest(),
|
||||
proto.Success(),
|
||||
]
|
||||
)
|
||||
res = client.ping(
|
||||
"random data",
|
||||
button_protection=True,
|
||||
pin_protection=True,
|
||||
passphrase_protection=True,
|
||||
)
|
||||
assert res == "random data"
|
||||
|
||||
with client:
|
||||
# pin and passphrase are cached
|
||||
client.set_expected_responses(
|
||||
[
|
||||
proto.ButtonRequest(code=proto.ButtonRequestType.ProtectCall),
|
||||
proto.Success(),
|
||||
]
|
||||
)
|
||||
res = client.ping(
|
||||
"random data",
|
||||
button_protection=True,
|
||||
pin_protection=True,
|
||||
passphrase_protection=True,
|
||||
)
|
||||
assert res == "random data"
|
||||
|
@ -85,7 +85,7 @@ class TestMsgRecoverydevice:
|
||||
assert client.features.passphrase_protection is True
|
||||
|
||||
# Do passphrase-protected action, PassphraseRequest should be raised
|
||||
resp = client.call_raw(proto.Ping(passphrase_protection=True))
|
||||
resp = client.call_raw(proto.GetAddress())
|
||||
assert isinstance(resp, proto.PassphraseRequest)
|
||||
client.call_raw(proto.Cancel())
|
||||
|
||||
@ -136,13 +136,9 @@ class TestMsgRecoverydevice:
|
||||
assert client.features.pin_protection is False
|
||||
assert client.features.passphrase_protection is False
|
||||
|
||||
# Do passphrase-protected action, PassphraseRequest should NOT be raised
|
||||
resp = client.call_raw(proto.Ping(passphrase_protection=True))
|
||||
assert isinstance(resp, proto.Success)
|
||||
|
||||
# Do PIN-protected action, PinRequest should NOT be raised
|
||||
resp = client.call_raw(proto.Ping(pin_protection=True))
|
||||
assert isinstance(resp, proto.Success)
|
||||
# Do pin & passphrase-protected action, PassphraseRequest should NOT be raised
|
||||
resp = client.call_raw(proto.GetAddress())
|
||||
assert isinstance(resp, proto.Address)
|
||||
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
def test_word_fail(self, client):
|
||||
|
@ -87,13 +87,9 @@ class TestMsgResetDevice:
|
||||
assert resp.pin_protection is False
|
||||
assert resp.passphrase_protection is False
|
||||
|
||||
# Do passphrase-protected action, PassphraseRequest should NOT be raised
|
||||
resp = client.call_raw(proto.Ping(passphrase_protection=True))
|
||||
assert isinstance(resp, proto.Success)
|
||||
|
||||
# Do PIN-protected action, PinRequest should NOT be raised
|
||||
resp = client.call_raw(proto.Ping(pin_protection=True))
|
||||
assert isinstance(resp, proto.Success)
|
||||
# Do pin & passphrase-protected action, PassphraseRequest should NOT be raised
|
||||
resp = client.call_raw(proto.GetAddress())
|
||||
assert isinstance(resp, proto.Address)
|
||||
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
def test_reset_device_pin(self, client):
|
||||
@ -180,7 +176,7 @@ class TestMsgResetDevice:
|
||||
assert resp.passphrase_protection is True
|
||||
|
||||
# Do passphrase-protected action, PassphraseRequest should be raised
|
||||
resp = client.call_raw(proto.Ping(passphrase_protection=True))
|
||||
resp = client.call_raw(proto.GetAddress())
|
||||
assert isinstance(resp, proto.PassphraseRequest)
|
||||
client.call_raw(proto.Cancel())
|
||||
|
||||
|
@ -18,7 +18,7 @@ import time
|
||||
|
||||
import pytest
|
||||
|
||||
from trezorlib import messages as proto
|
||||
from trezorlib import btc, messages as proto
|
||||
from trezorlib.exceptions import PinException
|
||||
|
||||
# FIXME TODO Add passphrase tests
|
||||
@ -26,84 +26,36 @@ from trezorlib.exceptions import PinException
|
||||
|
||||
@pytest.mark.skip_t2
|
||||
class TestProtectCall:
|
||||
def _some_protected_call(self, client, button, pin, passphrase):
|
||||
def _some_protected_call(self, client):
|
||||
# This method perform any call which have protection in the device
|
||||
res = client.ping(
|
||||
"random data",
|
||||
button_protection=button,
|
||||
pin_protection=pin,
|
||||
passphrase_protection=passphrase,
|
||||
)
|
||||
assert res == "random data"
|
||||
|
||||
@pytest.mark.setup_client(pin="1234", passphrase=True)
|
||||
def test_expected_responses(self, client):
|
||||
# This is low-level test of set_expected_responses()
|
||||
# feature of debugging client
|
||||
|
||||
with pytest.raises(AssertionError), client:
|
||||
# Scenario 1 - Received unexpected message
|
||||
client.set_expected_responses([])
|
||||
self._some_protected_call(client, True, True, True)
|
||||
|
||||
with pytest.raises(AssertionError), client:
|
||||
# Scenario 2 - Received other than expected message
|
||||
client.set_expected_responses([proto.Success()])
|
||||
self._some_protected_call(client, True, True, True)
|
||||
|
||||
with pytest.raises(AssertionError), client:
|
||||
# Scenario 3 - Not received expected message
|
||||
client.set_expected_responses(
|
||||
[proto.ButtonRequest(), proto.Success(), proto.Success()]
|
||||
) # This is expected, but not received
|
||||
self._some_protected_call(client, True, False, False)
|
||||
|
||||
with pytest.raises(AssertionError), client:
|
||||
# Scenario 4 - Received what expected
|
||||
client.set_expected_responses(
|
||||
[
|
||||
proto.ButtonRequest(),
|
||||
proto.PinMatrixRequest(),
|
||||
proto.PassphraseRequest(),
|
||||
proto.Success(message="random data"),
|
||||
]
|
||||
)
|
||||
self._some_protected_call(client, True, True, True)
|
||||
|
||||
with pytest.raises(AssertionError), client:
|
||||
# Scenario 5 - Failed message by field filter
|
||||
client.set_expected_responses(
|
||||
[proto.ButtonRequest(), proto.Success(message="wrong data")]
|
||||
)
|
||||
self._some_protected_call(client, True, True, True)
|
||||
res = btc.get_address(client, "Testnet", [0])
|
||||
assert res == "mndoQDWatQhfeQbprzZxD43mZ75Z94D6vz"
|
||||
|
||||
def test_no_protection(self, client):
|
||||
with client:
|
||||
assert client.debug.read_pin()[0] is None
|
||||
client.set_expected_responses([proto.Success()])
|
||||
self._some_protected_call(client, False, True, True)
|
||||
client.set_expected_responses([proto.Address()])
|
||||
self._some_protected_call(client)
|
||||
|
||||
@pytest.mark.setup_client(pin="1234", passphrase=True)
|
||||
@pytest.mark.setup_client(pin="1234")
|
||||
def test_pin(self, client):
|
||||
with client:
|
||||
assert client.debug.read_pin()[0] == "1234"
|
||||
client.setup_debuglink(button=True, pin_correct=True)
|
||||
client.set_expected_responses(
|
||||
[proto.ButtonRequest(), proto.PinMatrixRequest(), proto.Success()]
|
||||
)
|
||||
self._some_protected_call(client, True, True, False)
|
||||
client.set_expected_responses([proto.PinMatrixRequest(), proto.Address()])
|
||||
self._some_protected_call(client)
|
||||
|
||||
@pytest.mark.setup_client(pin="1234", passphrase=True)
|
||||
@pytest.mark.setup_client(pin="1234")
|
||||
def test_incorrect_pin(self, client):
|
||||
client.setup_debuglink(button=True, pin_correct=False)
|
||||
with pytest.raises(PinException):
|
||||
self._some_protected_call(client, False, True, False)
|
||||
self._some_protected_call(client)
|
||||
|
||||
@pytest.mark.setup_client(pin="1234", passphrase=True)
|
||||
@pytest.mark.setup_client(pin="1234")
|
||||
def test_cancelled_pin(self, client):
|
||||
client.setup_debuglink(button=True, pin_correct=False) # PIN cancel
|
||||
with pytest.raises(PinException):
|
||||
self._some_protected_call(client, False, True, False)
|
||||
self._some_protected_call(client)
|
||||
|
||||
@pytest.mark.setup_client(pin="1234", passphrase=True)
|
||||
def test_exponential_backoff_with_reboot(self, client):
|
||||
@ -126,5 +78,5 @@ class TestProtectCall:
|
||||
for attempt in range(1, 4):
|
||||
start = time.time()
|
||||
with pytest.raises(PinException):
|
||||
self._some_protected_call(client, False, True, False)
|
||||
self._some_protected_call(client)
|
||||
test_backoff(attempt, start)
|
||||
|
@ -63,18 +63,10 @@ class TestProtectionLevels:
|
||||
)
|
||||
device.change_pin(client)
|
||||
|
||||
@pytest.mark.setup_client(pin=True, passphrase=True)
|
||||
def test_ping(self, client):
|
||||
with client:
|
||||
client.set_expected_responses(
|
||||
[
|
||||
proto.ButtonRequest(),
|
||||
proto.PinMatrixRequest(),
|
||||
proto.PassphraseRequest(),
|
||||
proto.Success(),
|
||||
]
|
||||
)
|
||||
client.ping("msg", True, True, True)
|
||||
client.set_expected_responses([proto.ButtonRequest(), proto.Success()])
|
||||
client.ping("msg", True)
|
||||
|
||||
@pytest.mark.setup_client(pin=True, passphrase=True)
|
||||
def test_get_entropy(self, client):
|
||||
@ -269,29 +261,21 @@ class TestProtectionLevels:
|
||||
assert client.features.pin_cached is False
|
||||
|
||||
with client:
|
||||
client.set_expected_responses(
|
||||
[proto.ButtonRequest(), proto.PinMatrixRequest(), proto.Success()]
|
||||
)
|
||||
client.ping("msg", True, True, True)
|
||||
client.set_expected_responses([proto.PinMatrixRequest(), proto.Address()])
|
||||
btc.get_address(client, "Testnet", [0])
|
||||
|
||||
client.init_device()
|
||||
assert client.features.pin_cached is True
|
||||
with client:
|
||||
client.set_expected_responses([proto.ButtonRequest(), proto.Success()])
|
||||
client.ping("msg", True, True, True)
|
||||
client.set_expected_responses([proto.Address()])
|
||||
btc.get_address(client, "Testnet", [0])
|
||||
|
||||
@pytest.mark.setup_client(passphrase=True)
|
||||
def test_passphrase_cached(self, client):
|
||||
assert client.features.passphrase_cached is False
|
||||
with client:
|
||||
client.set_expected_responses([proto.PassphraseRequest(), proto.Address()])
|
||||
btc.get_address(client, "Testnet", [0])
|
||||
|
||||
with client:
|
||||
client.set_expected_responses(
|
||||
[proto.ButtonRequest(), proto.PassphraseRequest(), proto.Success()]
|
||||
)
|
||||
client.ping("msg", True, True, True)
|
||||
|
||||
features = client.call(proto.GetFeatures())
|
||||
assert features.passphrase_cached is True
|
||||
with client:
|
||||
client.set_expected_responses([proto.ButtonRequest(), proto.Success()])
|
||||
client.ping("msg", True, True, True)
|
||||
client.set_expected_responses([proto.Address()])
|
||||
btc.get_address(client, "Testnet", [0])
|
||||
|
280
tests/device_tests/test_session_id_and_passphrase.py
Normal file
280
tests/device_tests/test_session_id_and_passphrase.py
Normal file
@ -0,0 +1,280 @@
|
||||
# This file is part of the Trezor project.
|
||||
#
|
||||
# Copyright (C) 2012-2019 SatoshiLabs and contributors
|
||||
#
|
||||
# This library is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License version 3
|
||||
# as published by the Free Software Foundation.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the License along with this library.
|
||||
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||
|
||||
import pytest
|
||||
|
||||
from trezorlib import messages
|
||||
from trezorlib.messages import FailureType
|
||||
from trezorlib.tools import parse_path
|
||||
|
||||
XPUB_PASSPHRASE_A = "xpub6CekxGcnqnJ6osfY4Rrq7W5ogFtR54KUvz4H16XzaQuukMFZCGebEpVznfq4yFcKEmYyShwj2UKjL7CazuNSuhdkofF4mHabHkLxCMVvsqG"
|
||||
XPUB_PASSPHRASE_NONE = "xpub6BiVtCpG9fQPxnPmHXG8PhtzQdWC2Su4qWu6XW9tpWFYhxydCLJGrWBJZ5H6qTAHdPQ7pQhtpjiYZVZARo14qHiay2fvrX996oEP42u8wZy"
|
||||
XPUB_CARDANO_PASSPHRASE_B = "d80e770f6dfc3edb58eaab68aa091b2c27b08a47583471e93437ac5f8baa61880c7af4938a941c084c19731e6e57a5710e6ad1196263291aea297ce0eec0f177"
|
||||
|
||||
ADDRESS_N = parse_path("44h/0h/0h")
|
||||
XPUB_REQUEST = messages.GetPublicKey(address_n=ADDRESS_N, coin_name="Bitcoin")
|
||||
|
||||
|
||||
def _init_session(client, session_id=None):
|
||||
"""Call Initialize, check and return the session ID."""
|
||||
response = client.call(messages.Initialize(session_id=session_id))
|
||||
assert isinstance(response, messages.Features)
|
||||
assert len(response.session_id) == 32
|
||||
return response.session_id
|
||||
|
||||
|
||||
def _get_xpub(client, passphrase=None):
|
||||
"""Get XPUB and check that the appropriate passphrase flow has happened."""
|
||||
response = client.call_raw(XPUB_REQUEST)
|
||||
if passphrase is not None:
|
||||
assert isinstance(response, messages.PassphraseRequest)
|
||||
response = client.call_raw(messages.PassphraseAck(passphrase=passphrase))
|
||||
assert isinstance(response, messages.PublicKey)
|
||||
return response.xpub
|
||||
|
||||
|
||||
@pytest.mark.skip_ui
|
||||
@pytest.mark.setup_client(passphrase=True)
|
||||
def test_session_with_passphrase(client):
|
||||
# Let's start the communication by calling Initialize.
|
||||
session_id = _init_session(client)
|
||||
|
||||
# GetPublicKey requires passphrase and since it is not cached,
|
||||
# Trezor will prompt for it.
|
||||
assert _get_xpub(client, passphrase="A") == XPUB_PASSPHRASE_A
|
||||
|
||||
# Call Initialize again, this time with the received session id and then call
|
||||
# GetPublicKey. The passphrase should be cached now so Trezor must
|
||||
# not ask for it again, whilst returning the same xpub.
|
||||
new_session_id = _init_session(client, session_id=session_id)
|
||||
assert new_session_id == session_id
|
||||
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASE_A
|
||||
|
||||
# If we set session id in Initialize to None, the cache will be cleared
|
||||
# and Trezor will ask for the passphrase again.
|
||||
new_session_id = _init_session(client)
|
||||
assert new_session_id != session_id
|
||||
assert _get_xpub(client, passphrase="A") == XPUB_PASSPHRASE_A
|
||||
|
||||
# Unknown session id is the same as setting it to None.
|
||||
_init_session(client, session_id=b"X" * 32)
|
||||
assert _get_xpub(client, passphrase="A") == XPUB_PASSPHRASE_A
|
||||
|
||||
|
||||
@pytest.mark.skip_ui
|
||||
def test_session_enable_passphrase(client):
|
||||
# Let's start the communication by calling Initialize.
|
||||
session_id = _init_session(client)
|
||||
|
||||
# Trezor will not prompt for passphrase because it is turned off.
|
||||
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASE_NONE
|
||||
|
||||
# Turn on passphrase.
|
||||
# Emit the call explicitly to avoid ClearSession done by the library function
|
||||
response = client.call(messages.ApplySettings(use_passphrase=True))
|
||||
assert isinstance(response, messages.Success)
|
||||
|
||||
# The session id is unchanged, therefore we do not prompt for the passphrase.
|
||||
new_session_id = _init_session(client, session_id=session_id)
|
||||
assert session_id == new_session_id
|
||||
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASE_NONE
|
||||
|
||||
# We clear the session id now, so the passphrase should be asked.
|
||||
new_session_id = _init_session(client)
|
||||
assert session_id != new_session_id
|
||||
assert _get_xpub(client, passphrase="A") == XPUB_PASSPHRASE_A
|
||||
|
||||
|
||||
@pytest.mark.skip_ui
|
||||
@pytest.mark.setup_client(passphrase=True)
|
||||
def test_clear_session_passphrase(client):
|
||||
# at first attempt, we are prompted for passphrase
|
||||
assert _get_xpub(client, passphrase="A") == XPUB_PASSPHRASE_A
|
||||
|
||||
# now the passphrase is cached
|
||||
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASE_A
|
||||
|
||||
# Erase the cached passphrase
|
||||
response = client.call(messages.Initialize())
|
||||
assert isinstance(response, messages.Features)
|
||||
|
||||
# we have to enter passphrase again
|
||||
assert _get_xpub(client, passphrase="A") == XPUB_PASSPHRASE_A
|
||||
|
||||
|
||||
@pytest.mark.skip_ui
|
||||
@pytest.mark.skip_t1
|
||||
@pytest.mark.setup_client(passphrase=True)
|
||||
def test_passphrase_on_device(client):
|
||||
_init_session(client)
|
||||
|
||||
# try to get xpub with passphrase on host:
|
||||
response = client.call_raw(XPUB_REQUEST)
|
||||
assert isinstance(response, messages.PassphraseRequest)
|
||||
response = client.call_raw(messages.PassphraseAck(passphrase="A", on_device=False))
|
||||
assert isinstance(response, messages.PublicKey)
|
||||
assert response.xpub == XPUB_PASSPHRASE_A
|
||||
|
||||
# try to get xpub again, passphrase should be cached
|
||||
response = client.call_raw(XPUB_REQUEST)
|
||||
assert isinstance(response, messages.PublicKey)
|
||||
assert response.xpub == XPUB_PASSPHRASE_A
|
||||
|
||||
# make a new session
|
||||
_init_session(client)
|
||||
|
||||
# try to get xpub with passphrase on device:
|
||||
response = client.call_raw(XPUB_REQUEST)
|
||||
assert isinstance(response, messages.PassphraseRequest)
|
||||
response = client.call_raw(messages.PassphraseAck(on_device=True))
|
||||
assert isinstance(response, messages.ButtonRequest)
|
||||
client.debug.input("A")
|
||||
response = client.call_raw(messages.ButtonAck())
|
||||
assert isinstance(response, messages.PublicKey)
|
||||
assert response.xpub == XPUB_PASSPHRASE_A
|
||||
|
||||
# try to get xpub again, passphrase should be cached
|
||||
response = client.call_raw(XPUB_REQUEST)
|
||||
assert isinstance(response, messages.PublicKey)
|
||||
assert response.xpub == XPUB_PASSPHRASE_A
|
||||
|
||||
|
||||
@pytest.mark.skip_ui
|
||||
@pytest.mark.skip_t1
|
||||
@pytest.mark.setup_client(passphrase=True)
|
||||
def test_passphrase_always_on_device(client):
|
||||
# Let's start the communication by calling Initialize.
|
||||
session_id = _init_session(client)
|
||||
|
||||
# Force passphrase entry on Trezor.
|
||||
response = client.call(messages.ApplySettings(passphrase_always_on_device=True))
|
||||
assert isinstance(response, messages.Success)
|
||||
|
||||
# Since we enabled the always_on_device setting, Trezor will send ButtonRequests and ask for it on the device.
|
||||
response = client.call_raw(XPUB_REQUEST)
|
||||
assert isinstance(response, messages.ButtonRequest)
|
||||
client.debug.input("") # Input empty passphrase.
|
||||
response = client.call_raw(messages.ButtonAck())
|
||||
assert isinstance(response, messages.PublicKey)
|
||||
assert response.xpub == XPUB_PASSPHRASE_NONE
|
||||
|
||||
# Passphrase will not be prompted. The session id stays the same and the passphrase is cached.
|
||||
_init_session(client, session_id=session_id)
|
||||
response = client.call_raw(XPUB_REQUEST)
|
||||
assert isinstance(response, messages.PublicKey)
|
||||
assert response.xpub == XPUB_PASSPHRASE_NONE
|
||||
|
||||
# In case we want to add a new passphrase we need to send session_id = None.
|
||||
_init_session(client)
|
||||
response = client.call_raw(XPUB_REQUEST)
|
||||
assert isinstance(response, messages.ButtonRequest)
|
||||
client.debug.input("A") # Input empty passphrase.
|
||||
response = client.call_raw(messages.ButtonAck())
|
||||
assert isinstance(response, messages.PublicKey)
|
||||
assert response.xpub == XPUB_PASSPHRASE_A
|
||||
|
||||
|
||||
@pytest.mark.skip_ui
|
||||
@pytest.mark.skip_t2
|
||||
@pytest.mark.setup_client(passphrase=True)
|
||||
def test_passphrase_on_device_not_possible_on_t1(client):
|
||||
# This setting makes no sense on T1.
|
||||
response = client.call_raw(messages.ApplySettings(passphrase_always_on_device=True))
|
||||
assert isinstance(response, messages.Failure)
|
||||
assert response.code == FailureType.DataError
|
||||
|
||||
# T1 should not accept on_device request
|
||||
response = client.call_raw(XPUB_REQUEST)
|
||||
assert isinstance(response, messages.PassphraseRequest)
|
||||
response = client.call_raw(messages.PassphraseAck(on_device=True))
|
||||
assert isinstance(response, messages.Failure)
|
||||
assert response.code == FailureType.DataError
|
||||
|
||||
|
||||
@pytest.mark.skip_ui
|
||||
@pytest.mark.setup_client(passphrase=True)
|
||||
def test_passphrase_ack_mismatch(client):
|
||||
response = client.call_raw(XPUB_REQUEST)
|
||||
assert isinstance(response, messages.PassphraseRequest)
|
||||
response = client.call_raw(messages.PassphraseAck(passphrase="A", on_device=True))
|
||||
assert isinstance(response, messages.Failure)
|
||||
assert response.code == FailureType.DataError
|
||||
|
||||
|
||||
@pytest.mark.skip_ui
|
||||
@pytest.mark.setup_client(passphrase=True)
|
||||
def test_passphrase_missing(client):
|
||||
response = client.call_raw(XPUB_REQUEST)
|
||||
assert isinstance(response, messages.PassphraseRequest)
|
||||
response = client.call_raw(messages.PassphraseAck(passphrase=None))
|
||||
assert isinstance(response, messages.Failure)
|
||||
assert response.code == FailureType.DataError
|
||||
|
||||
response = client.call_raw(XPUB_REQUEST)
|
||||
assert isinstance(response, messages.PassphraseRequest)
|
||||
response = client.call_raw(messages.PassphraseAck(passphrase=None, on_device=False))
|
||||
assert isinstance(response, messages.Failure)
|
||||
assert response.code == FailureType.DataError
|
||||
|
||||
|
||||
def _get_xpub_cardano(client, passphrase):
|
||||
msg = messages.CardanoGetPublicKey(address_n=parse_path("44'/1815'/0'/0/0"))
|
||||
response = client.call_raw(msg)
|
||||
if passphrase is not None:
|
||||
assert isinstance(response, messages.PassphraseRequest)
|
||||
response = client.call_raw(messages.PassphraseAck(passphrase=passphrase))
|
||||
assert isinstance(response, messages.CardanoPublicKey)
|
||||
return response.xpub
|
||||
|
||||
|
||||
@pytest.mark.skip_ui
|
||||
@pytest.mark.skip_t1
|
||||
@pytest.mark.altcoin
|
||||
@pytest.mark.setup_client(passphrase=True)
|
||||
def test_cardano_passphrase(client):
|
||||
# Cardano uses a variation of BIP-39 so we need to ask for the passphrase again.
|
||||
|
||||
session_id = _init_session(client)
|
||||
|
||||
# GetPublicKey requires passphrase and since it is not cached,
|
||||
# Trezor will prompt for it.
|
||||
assert _get_xpub(client, passphrase="A") == XPUB_PASSPHRASE_A
|
||||
|
||||
# The passphrase is now cached for non-Cardano coins.
|
||||
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASE_A
|
||||
|
||||
# Cardano will prompt for it again.
|
||||
assert _get_xpub_cardano(client, passphrase="B") == XPUB_CARDANO_PASSPHRASE_B
|
||||
|
||||
# But now also Cardano has it cached.
|
||||
assert _get_xpub_cardano(client, passphrase=None) == XPUB_CARDANO_PASSPHRASE_B
|
||||
|
||||
# And others behaviour did not change.
|
||||
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASE_A
|
||||
|
||||
# Initialize with the session id does not destroy the state
|
||||
_init_session(client, session_id=session_id)
|
||||
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASE_A
|
||||
assert _get_xpub_cardano(client, passphrase=None) == XPUB_CARDANO_PASSPHRASE_B
|
||||
|
||||
# New session will destroy the state
|
||||
_init_session(client)
|
||||
|
||||
# GetPublicKey must ask for passphrase again
|
||||
assert _get_xpub(client, passphrase="A") == XPUB_PASSPHRASE_A
|
||||
|
||||
# Cardano must also ask for passphrase again
|
||||
assert _get_xpub_cardano(client, passphrase="B") == XPUB_CARDANO_PASSPHRASE_B
|
@ -9,9 +9,9 @@
|
||||
"test_cancel.py::test_cancel_message_via_initialize[message1]": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
|
||||
"test_msg_applysettings.py-test_apply_settings": "b698654871541258f97d58ada0f010b2d77b74829791566746cad619d3740a94",
|
||||
"test_msg_applysettings.py-test_apply_settings_passphrase": "fb38537b921f8064f7ea6e1a584e70a8be74968a3be6726b7d36cf57de0d7865",
|
||||
"test_msg_backup_device.py::test_backup_bip39": "68d22900d8f9130bf325342b6b32b1785f28cefd256ea1cee7fd32109c817c23",
|
||||
"test_msg_backup_device.py::test_backup_slip39_advanced": "aec6a663f8f75ff73d2f2377d12347b062b831ce0d5fcfe9d7f3c975a05aef32",
|
||||
"test_msg_backup_device.py::test_backup_slip39_basic": "a4c44785873509bd4ebffa6ec82f2fe732349eb0bd23631dbc3876e956b567c7",
|
||||
"test_msg_backup_device.py::test_backup_bip39": "dfdbd0ae6774177d43f2f11d026c4d8679dd994b508c1d850d9cfee5dd1118ac",
|
||||
"test_msg_backup_device.py::test_backup_slip39_advanced": "244b31044a25e44847a6efb79ea4cb67c246a971bf828d46aa79d14151934a04",
|
||||
"test_msg_backup_device.py::test_backup_slip39_basic": "81a56c307342e46ad261d6a6c6da0a75224af8e72f40da7c630c1e03c0643e1c",
|
||||
"test_msg_backup_device.py::test_interrupt_backup_fails": "225b3da1acac6e9a65106fcc4a01de8a44de035aedb4dcc21c09f439199fdf40",
|
||||
"test_msg_backup_device.py::test_no_backup_fails": "93039a9472cfc9058563bd56e4a3dbe2e41af64744a61f6ee3255a04bd3a9366",
|
||||
"test_msg_backup_device.py::test_no_backup_show_entropy_fails": "14fcdd2ded299ca099a35966cc9f21204b31de8d6bab9ec91cb64537bd70440c",
|
||||
@ -24,16 +24,16 @@
|
||||
"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-0-Ae2tdPwUPEZLCq3sFv4wVYx": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
|
||||
"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-1-Ae2tdPwUPEZEY6pVJoyuNNd": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
|
||||
"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-2-Ae2tdPwUPEZ3gZD1QeUHvAq": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
|
||||
"test_msg_cardano_get_address_slip39_basic.py::test_cardano_get_address[m-44'-1815'-0'-0-0-Ae2tdPwUPE": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0",
|
||||
"test_msg_cardano_get_address_slip39_basic.py::test_cardano_get_address[m-44'-1815'-0'-0-1-Ae2tdPwUPE": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0",
|
||||
"test_msg_cardano_get_address_slip39_basic.py::test_cardano_get_address[m-44'-1815'-0'-0-2-Ae2tdPwUPE": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0",
|
||||
"test_msg_cardano_get_address_slip39_basic.py::test_cardano_get_address[m-44'-1815'-0'-0-0-Ae2tdPwUPE": "d2d6aac0a4605f1a961580f0fc156e7b69b993f908906281313429fa3222f349",
|
||||
"test_msg_cardano_get_address_slip39_basic.py::test_cardano_get_address[m-44'-1815'-0'-0-1-Ae2tdPwUPE": "d2d6aac0a4605f1a961580f0fc156e7b69b993f908906281313429fa3222f349",
|
||||
"test_msg_cardano_get_address_slip39_basic.py::test_cardano_get_address[m-44'-1815'-0'-0-2-Ae2tdPwUPE": "d2d6aac0a4605f1a961580f0fc156e7b69b993f908906281313429fa3222f349",
|
||||
"test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-44'-1815'-0'-c0fce1839f1a84c4e7702": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
|
||||
"test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-44'-1815'-1'-ea5dde31b9f551e08a5b6": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
|
||||
"test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-44'-1815'-2'-076338cee5ab3dae19f06": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
|
||||
"test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-44'-1815'-3'-5f769380dc6fd17a4e0f2": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
|
||||
"test_msg_cardano_get_public_key_slip39_basic.py::test_cardano_get_public_key[m-44'-1815'-0'-0-0-bc04": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0",
|
||||
"test_msg_cardano_get_public_key_slip39_basic.py::test_cardano_get_public_key[m-44'-1815'-0'-0-1-24c4": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0",
|
||||
"test_msg_cardano_get_public_key_slip39_basic.py::test_cardano_get_public_key[m-44'-1815'-0'-0-2-831a": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0",
|
||||
"test_msg_cardano_get_public_key_slip39_basic.py::test_cardano_get_public_key[m-44'-1815'-0'-0-0-bc04": "d2d6aac0a4605f1a961580f0fc156e7b69b993f908906281313429fa3222f349",
|
||||
"test_msg_cardano_get_public_key_slip39_basic.py::test_cardano_get_public_key[m-44'-1815'-0'-0-1-24c4": "d2d6aac0a4605f1a961580f0fc156e7b69b993f908906281313429fa3222f349",
|
||||
"test_msg_cardano_get_public_key_slip39_basic.py::test_cardano_get_public_key[m-44'-1815'-0'-0-2-831a": "d2d6aac0a4605f1a961580f0fc156e7b69b993f908906281313429fa3222f349",
|
||||
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[1097911063-inputs2-outputs2-transactions2": "1a8eade03d9c0ecbbb01567e5e9d46187a2ffe7fa42d59eb711347a7fe3b5bb7",
|
||||
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-inputs0-outputs0-transactions0-": "07eceef966cb8069381dc5105b732bce6de8d207a1d27e56e8abddd57c307b22",
|
||||
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-inputs1-outputs1-transactions1-": "4c74546cacb2b99b9aeb6e134d99c0d1d6f0ba1818d1182364cfdb94b938ccc5",
|
||||
@ -43,16 +43,16 @@
|
||||
"test_msg_cardano_sign_tx_slip39_basic.py::test_cardano_sign_tx[1097911063-inputs2-outputs2-transacti": "e6813a7fd973f49b02ef28cee15deb48d0389d1cb2696194848e4d690281361f",
|
||||
"test_msg_cardano_sign_tx_slip39_basic.py::test_cardano_sign_tx[764824073-inputs0-outputs0-transactio": "bc88a23280c0234860ccbb5e96d5cc3a851e2f2f9928c400f6c0907c68172d39",
|
||||
"test_msg_cardano_sign_tx_slip39_basic.py::test_cardano_sign_tx[764824073-inputs1-outputs1-transactio": "6956bb359388186b4c127ae88f4d86527381caf8098f55b4fa05c343640c081f",
|
||||
"test_msg_change_wipe_code_t2.py::test_set_pin_to_wipe_code": "b294554434cd454b6c53845256d3637bb83f0f7f96e39608c91018bb345f359e",
|
||||
"test_msg_change_wipe_code_t2.py::test_set_remove_wipe_code": "74e8309c74ca9e599de91296cfcef63a744713acad3f3ea10b7b461eba69c30e",
|
||||
"test_msg_change_wipe_code_t2.py::test_set_wipe_code_mismatch": "fdbd58fb31323cfc32a9a7684d4dd45a5bd2b098156f2abf9cca5b5a481ecd10",
|
||||
"test_msg_change_wipe_code_t2.py::test_set_wipe_code_to_pin": "7b3599a5582b566c65c337b50fb6e38fc577b67310fc731ed756b7d143f4727d",
|
||||
"test_msg_change_wipe_code_t2.py::test_wipe_code_activate": "b25571c82be5f00b1f0c0bca95154dbae6a6ac092b249b79f805a126e1580037",
|
||||
"test_msg_changepin_t2.py::test_change_failed": "d39ba207aab689c53adde211dc8e189809ffe4f3a3c547bd8c0422ad20663dc2",
|
||||
"test_msg_changepin_t2.py::test_change_pin": "9e43d9f05c96f61e5ba7723e7d3ceb26ed813fd5be95d2cfa7e01573ab87b531",
|
||||
"test_msg_changepin_t2.py::test_remove_pin": "249a84500e2cac217f4b3c7cfbb4d4212781019191b21d1386f81317f6d699e7",
|
||||
"test_msg_changepin_t2.py::test_set_failed": "5aa994de8e571d08512705b04e0c55b60411d7d23491f75688f16d255369363e",
|
||||
"test_msg_changepin_t2.py::test_set_pin": "9e30570a9063a9820d423dd000d5ba70f30bf32de78cce9160643f60d5fe4eb2",
|
||||
"test_msg_change_wipe_code_t2.py::test_set_pin_to_wipe_code": "9519b574e3f749810977ac55f851553f2051b51ef7799ffaa155410d89e1cd63",
|
||||
"test_msg_change_wipe_code_t2.py::test_set_remove_wipe_code": "82c0d1acbf5ff344189761f808d3cf0e632726341231c20b2c0925ab5549b6af",
|
||||
"test_msg_change_wipe_code_t2.py::test_set_wipe_code_mismatch": "1642d2d15920a3bb2c666b39beca9943ba39adb59289ebc40b97d7088a4d7abf",
|
||||
"test_msg_change_wipe_code_t2.py::test_set_wipe_code_to_pin": "15289574ceb002b5161305b0595dcd20e437d1dd4e7561332e1aba4c1615e9ea",
|
||||
"test_msg_change_wipe_code_t2.py::test_wipe_code_activate": "348ff6811029253d7d520b37a2d3ff219516a7401c8b65ab088c7a7d39bd8b2b",
|
||||
"test_msg_changepin_t2.py::test_change_failed": "370c59da62a84aaefa242562c36a6facac89c7f819e37d1ae8cbe2c44a2de256",
|
||||
"test_msg_changepin_t2.py::test_change_pin": "c42fca9bf8f3b4c330516d90231ae0cfa7419d83370be9cfcf6a81cca3f3b06c",
|
||||
"test_msg_changepin_t2.py::test_remove_pin": "d049eaa6cd11e88b7af193b080cf868b62271266ad6f2973bfd82944b523741d",
|
||||
"test_msg_changepin_t2.py::test_set_failed": "59beeec1a00817f664a5fd93234012588613aac93c45d53c27550fe5d0ef8380",
|
||||
"test_msg_changepin_t2.py::test_set_pin": "3aafe16a451f928c9bfff2a3ff7e3c23ce4948c9a044ebd04834df045670183f",
|
||||
"test_msg_cipherkeyvalue.py-test_decrypt": "166d85b1bf11aeaeb5b93ef5d047b6f8910c28b8fce1d853e6912d89d7bfca2f",
|
||||
"test_msg_cipherkeyvalue.py-test_decrypt_badlen": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
|
||||
"test_msg_cipherkeyvalue.py-test_encrypt": "3a37e4004c87bc6df6a8fa7c93b6fe3e3524986914709fda2f9c99ba0ff69775",
|
||||
@ -166,10 +166,10 @@
|
||||
"test_msg_lisk_verifymessage.py-test_verify": "5e9cf05f6ccf10f697cae9f780042db934892e1d7c68fb2f19a40319a687ea50",
|
||||
"test_msg_lisk_verifymessage.py-test_verify_long": "26adab7e31f388e5b034a865f9c010d57e67fd855d44839d2f2600d8317bd98e",
|
||||
"test_msg_loaddevice.py-test_load_device_1": "114d7e07b00f8a9fc60e0888ce3e39f79805c577b99f4d25967fcf7cf6367664",
|
||||
"test_msg_loaddevice.py-test_load_device_2": "35797984a814cf7e48ee827993269b43524bf91462c9c694ad7dd7d6bb7e6bbb",
|
||||
"test_msg_loaddevice.py-test_load_device_2": "9947760ad56ea110b6f3937883c37701c866dd57b6c342806bd8e8b3aa889887",
|
||||
"test_msg_loaddevice.py-test_load_device_slip39_advanced": "1c6db0d592b1d22b3c9fce3ddab8a9fd138f11d83e5d4e64431a02bf4ffed605",
|
||||
"test_msg_loaddevice.py-test_load_device_slip39_basic": "1c6db0d592b1d22b3c9fce3ddab8a9fd138f11d83e5d4e64431a02bf4ffed605",
|
||||
"test_msg_loaddevice.py-test_load_device_utf": "51fe82ea3081d06e69af1283bf9e32ea91ddb75ac87173a3d1f861c2b27a0635",
|
||||
"test_msg_loaddevice.py-test_load_device_utf": "65d570c16a561831e017c447736749d855f0bf6e0c3cfe7a9b186493c01f187b",
|
||||
"test_msg_monero_getaddress.py-test_monero_getaddress": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
|
||||
"test_msg_monero_getwatchkey.py-test_monero_getwatchkey": "d77fa4d4322e145c41f1ce07526ff59f8b58d8854aeffaa5266e14cd572350e7",
|
||||
"test_msg_nem_getaddress.py-test_nem_getaddress": "e726f99401a20eb74c33d755cecea2a3f69b7ae5b541302677ee05f80f5aef19",
|
||||
@ -200,7 +200,7 @@
|
||||
"test_msg_recoverydevice_bip39_dryrun.py::test_uninitialized": "14fcdd2ded299ca099a35966cc9f21204b31de8d6bab9ec91cb64537bd70440c",
|
||||
"test_msg_recoverydevice_bip39_t2.py-test_already_initialized": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
|
||||
"test_msg_recoverydevice_bip39_t2.py-test_nopin_nopassphrase": "9769cde3e3951a76364973ade753682e2acc67d1633a9f982f0604b5702aa895",
|
||||
"test_msg_recoverydevice_bip39_t2.py-test_pin_passphrase": "761b17d28d589eb4d4d6d73beb6061087828c469f60694893c94b301a37d34ad",
|
||||
"test_msg_recoverydevice_bip39_t2.py-test_pin_passphrase": "d746fda5234be75cded559817d0fdfb4397ac757e9847d4dfbef44ac031381d1",
|
||||
"test_msg_recoverydevice_slip39_advanced.py::test_abort": "793dde7fb47e9c4ad36369be396da20332560f29083d7f9a2b0582173371c9ed",
|
||||
"test_msg_recoverydevice_slip39_advanced.py::test_extra_share_entered": "00a94e20b786346c45f987860b2465f299075d7c6de4971f42a4749e1cc8bfc0",
|
||||
"test_msg_recoverydevice_slip39_advanced.py::test_group_threshold_reached": "3b075a276c4e0d53fbc51ce1f29594bbd474d25f47c0f6a32caac41ba0ba2138",
|
||||
@ -214,7 +214,7 @@
|
||||
"test_msg_recoverydevice_slip39_basic.py::test_abort": "793dde7fb47e9c4ad36369be396da20332560f29083d7f9a2b0582173371c9ed",
|
||||
"test_msg_recoverydevice_slip39_basic.py::test_ask_word_number": "8e9d9fd75e17f6b44829ae2d7b0eb9e60b48577f975abc6d75116f8365241082",
|
||||
"test_msg_recoverydevice_slip39_basic.py::test_noabort": "d374a9b85c03a0cc1bbb59130e454406513fc35f4f43b968db4920414de1bb72",
|
||||
"test_msg_recoverydevice_slip39_basic.py::test_recover_with_pin_passphrase": "3b49ab7cd70cf4e2576c842948029046bc6a686b3763983985e3d3965e8652d8",
|
||||
"test_msg_recoverydevice_slip39_basic.py::test_recover_with_pin_passphrase": "ee0edd912b913d31b8308b64ceed7fa3e58f5b63c8a4d4d85f3651d4b121a202",
|
||||
"test_msg_recoverydevice_slip39_basic.py::test_same_share": "e6a54429fdbedea9efca9cbed736aada07f95f3b20f895f9c1c5ec056a2be014",
|
||||
"test_msg_recoverydevice_slip39_basic.py::test_secret[shares0-491b795b80fc21ccdf466c0fbc98c8fc]": "54581a91b55ab531b215cba61052fd77c505232c510f170080760605eb9b8c46",
|
||||
"test_msg_recoverydevice_slip39_basic.py::test_secret[shares1-b770e0da1363247652de97a39bdbf2463be0878": "f73bef254762d761db27df46bff62641a9d2ac0602c34fc4e465262bf26ed08f",
|
||||
@ -224,11 +224,11 @@
|
||||
"test_msg_recoverydevice_slip39_basic_dryrun.py::test_2of3_dryrun": "d84427489f691ecc222b62f83af3e97fa09097404dcba07772a43b5eb0c689e8",
|
||||
"test_msg_recoverydevice_slip39_basic_dryrun.py::test_2of3_invalid_seed_dryrun": "55f2dd6b4958659f071c3f57e06286f872ac38af4828f446a0f4e91c657dfccc",
|
||||
"test_msg_resetdevice_bip39_t2.py-test_already_initialized": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
|
||||
"test_msg_resetdevice_bip39_t2.py-test_failed_pin": "a6ba803a7572dc8e29c821433a067c3a990185a8eb7b95d0106cd8f4b82e9da2",
|
||||
"test_msg_resetdevice_bip39_t2.py-test_reset_device": "cbe1e439c2e9810ff6ee1031268810188b3cd07e029a7aa1d4b227e33707c727",
|
||||
"test_msg_resetdevice_bip39_t2.py-test_reset_device_pin": "af378bff7e8fdf16324715f640e8b17bebd69f779758ab33cd7025c557a1d5c0",
|
||||
"test_msg_resetdevice_slip39_advanced.py-test_reset_device_slip39_advanced": "7daabeb21a17a8c8e8adb800a8b6b0504e5d2e7cf0bbfcb14770fb33df1426a2",
|
||||
"test_msg_resetdevice_slip39_basic.py-test_reset_device_slip39_basic": "1b56e3335876c01ec684839158bc0aa3a31aac03d2beef8d3ee63a0a441e3f1f",
|
||||
"test_msg_resetdevice_bip39_t2.py-test_failed_pin": "a52f3479f7d8e14c7f89af9b305a50f3bd244fee28f17ebc0abedba701c32811",
|
||||
"test_msg_resetdevice_bip39_t2.py-test_reset_device": "13d739a387a80aa96f533bb6f3b0f2ff12c7ba84608d0797ac603208d9aed796",
|
||||
"test_msg_resetdevice_bip39_t2.py-test_reset_device_pin": "ed64165674816dfb408ead89abb8d1a5743f50f63933aceba6701149522c3866",
|
||||
"test_msg_resetdevice_slip39_advanced.py-test_reset_device_slip39_advanced": "005622513c517610a4a1af529ef94760b4d0406b971f0b2f2557c93ce0dac8c9",
|
||||
"test_msg_resetdevice_slip39_basic.py-test_reset_device_slip39_basic": "ebc62d2c49136c6ce75c8e026e51157d96a6f2440881dca0cdd8bc357e2c9354",
|
||||
"test_msg_ripple_get_address.py-test_ripple_get_address": "2bb7d7bf48f1218530b4d7045d48480cad6411e110df537551b2f80b342007f2",
|
||||
"test_msg_ripple_get_address.py-test_ripple_get_address_other": "2bb7d7bf48f1218530b4d7045d48480cad6411e110df537551b2f80b342007f2",
|
||||
"test_msg_ripple_sign_tx.py-test_ripple_sign_invalid_fee": "1c0ca08b857da6121f43cfb1632c7f7e1d189ef1fdb665db7ba2cdfa7a59ea7c",
|
||||
@ -380,12 +380,12 @@
|
||||
"test_passphrase_slip39_advanced.py::test_256bit_passphrase": "69b6b8b22c819e1282d7d2c14b31bf8d015c81ac05fe034540dbb11c8a20dbdb",
|
||||
"test_passphrase_slip39_basic.py::test_2of5_passphrase": "1e00b1a7840bc144b98b7bce26f74fc913a0abf9d1c500571d7803b6b2e0943c",
|
||||
"test_passphrase_slip39_basic.py::test_3of6_passphrase": "1e00b1a7840bc144b98b7bce26f74fc913a0abf9d1c500571d7803b6b2e0943c",
|
||||
"test_reset_backup.py::test_skip_backup_manual[0-backup_flow_bip39]": "84ee487acfff37417e4d6862ab1b166be1165ae3eb4e19f828ae3363866edb39",
|
||||
"test_reset_backup.py::test_skip_backup_manual[1-backup_flow_slip39_basic]": "55224486083a1268c76ffe1d2b47ca7fba29ca6955e3601dae9e2abd44fb9d62",
|
||||
"test_reset_backup.py::test_skip_backup_manual[2-backup_flow_slip39_advanced]": "c40d7894875393bc093e23d02dab8ad668929767a542a27ff89c6dd5b7b963de",
|
||||
"test_reset_backup.py::test_skip_backup_msg[0-backup_flow_bip39]": "68eb02ad320eee49d3a6aac891e5dc4c35ee09a1e98a2480c2cb1b672cbb0696",
|
||||
"test_reset_backup.py::test_skip_backup_msg[1-backup_flow_slip39_basic]": "34674c8b9f4b6a836940fa347bacfd6d31dd681d41bf8e1ec129e9deef590588",
|
||||
"test_reset_backup.py::test_skip_backup_msg[2-backup_flow_slip39_advanced]": "24c641cde46665219de99978c3e27088fd4ade55f2665535a95e7dce1f2b3148",
|
||||
"test_reset_backup.py::test_skip_backup_manual[0-backup_flow_bip39]": "a73fcd9af54d3b55cc7c21e68b1637a6e29829bfd57d47b0503e67fa22e4106a",
|
||||
"test_reset_backup.py::test_skip_backup_manual[1-backup_flow_slip39_basic]": "4b117541a58e7c209786a728ebc1406720cbd7d5a234f20a550b1ad3970b4e76",
|
||||
"test_reset_backup.py::test_skip_backup_manual[2-backup_flow_slip39_advanced]": "2d1d55f25b21c2be2e7aeaa17eeb3a4b9ee4d7763c4a65ae2fd59da2d9dbbcc0",
|
||||
"test_reset_backup.py::test_skip_backup_msg[0-backup_flow_bip39]": "f448153452b154c0be153e18340f22c853430267316229548ad8f4f47cf946f7",
|
||||
"test_reset_backup.py::test_skip_backup_msg[1-backup_flow_slip39_basic]": "9fbcd0ab293de21ab3a24e0c29315d152c1bc5da4902ec054cc8800c0d855aa6",
|
||||
"test_reset_backup.py::test_skip_backup_msg[2-backup_flow_slip39_advanced]": "0bafb944169d0ab608513deadba15411d1756ef2658253740c5d8792f4527e9d",
|
||||
"test_u2f_counter.py::test_u2f_counter": "7d96a4d262b9d8a2c1158ac1e5f0f7b2c3ed5f2ba9d6235a014320313f9488fe",
|
||||
"test_zerosig.py-test_one_zero_signature": "401aeaf7b2f565e2064a3c1a57a8ee3afe1e9bf251fba0874390685e7e0f178f",
|
||||
"test_zerosig.py-test_two_zero_signature": "7a01a057fb5dd3e6e38e7986875c5d07f0700bd80b519660e0b42973a9afd664"
|
||||
|
Loading…
Reference in New Issue
Block a user