Merge branch 'andrewkozlik/sd-protect'

pull/539/head
Pavol Rusnak 5 years ago
commit 5b11d9a65d
No known key found for this signature in database
GPG Key ID: 91F3B339B9A02A3D

@ -131,6 +131,24 @@ message ChangePin {
optional bool remove = 1; // is PIN removal requested?
}
/**
* Request: Starts workflow for enabling/regenerating/disabling SD card protection
* @start
* @next Success
* @next Failure
*/
message SdProtect {
optional SdProtectOperationType operation = 1;
/**
* Structure representing SD card protection operation
*/
enum SdProtectOperationType {
DISABLE = 0;
ENABLE = 1;
REFRESH = 2;
}
}
/**
* Request: Test if the device is alive, device sends back the message in Success response
* @start

@ -61,6 +61,7 @@ enum MessageType {
MessageType_WordAck = 47 [(wire_in) = true];
MessageType_GetFeatures = 55 [(wire_in) = true];
MessageType_SetU2FCounter = 63 [(wire_in) = true];
MessageType_SdProtect = 79 [(wire_in) = true];
// Bootloader
MessageType_FirmwareErase = 6 [(wire_in) = true, (wire_bootloader) = true];

@ -67,29 +67,38 @@ STATIC mp_obj_t mod_trezorconfig_init(size_t n_args, const mp_obj_t *args) {
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorconfig_init_obj, 0, 1,
mod_trezorconfig_init);
/// def unlock(pin: int) -> bool:
/// def unlock(pin: int, ext_salt: Optional[bytes]) -> bool:
/// """
/// Attempts to unlock the storage with given PIN. Returns True on
/// success, False on failure.
/// Attempts to unlock the storage with the given PIN and external salt.
/// Returns True on success, False on failure.
/// """
STATIC mp_obj_t mod_trezorconfig_unlock(mp_obj_t pin) {
STATIC mp_obj_t mod_trezorconfig_unlock(mp_obj_t pin, mp_obj_t ext_salt) {
uint32_t pin_i = trezor_obj_get_uint(pin);
if (sectrue != storage_unlock(pin_i)) {
mp_buffer_info_t ext_salt_b;
ext_salt_b.buf = NULL;
if (ext_salt != mp_const_none) {
mp_get_buffer_raise(ext_salt, &ext_salt_b, MP_BUFFER_READ);
if (ext_salt_b.len != EXTERNAL_SALT_SIZE)
mp_raise_msg(&mp_type_ValueError, "Invalid length of external salt.");
}
if (sectrue != storage_unlock(pin_i, ext_salt_b.buf)) {
return mp_const_false;
}
return mp_const_true;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorconfig_unlock_obj,
STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorconfig_unlock_obj,
mod_trezorconfig_unlock);
/// def check_pin(pin: int) -> bool:
/// def check_pin(pin: int, ext_salt: Optional[bytes]) -> bool:
/// """
/// Check the given PIN. Returns True on success, False on failure.
/// Check the given PIN with the given external salt.
/// Returns True on success, False on failure.
/// """
STATIC mp_obj_t mod_trezorconfig_check_pin(mp_obj_t pin) {
return mod_trezorconfig_unlock(pin);
STATIC mp_obj_t mod_trezorconfig_check_pin(mp_obj_t pin, mp_obj_t ext_salt) {
return mod_trezorconfig_unlock(pin, ext_salt);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorconfig_check_pin_obj,
STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorconfig_check_pin_obj,
mod_trezorconfig_check_pin);
/// def lock() -> None:
@ -126,20 +135,43 @@ STATIC mp_obj_t mod_trezorconfig_get_pin_rem(void) {
STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorconfig_get_pin_rem_obj,
mod_trezorconfig_get_pin_rem);
/// def change_pin(pin: int, newpin: int) -> bool:
/// def change_pin(
/// oldpin: int,
/// newpin: int,
/// old_ext_salt: Optional[bytes],
/// new_ext_salt: Optional[bytes],
/// ) -> bool:
/// """
/// Change PIN. Returns True on success, False on failure.
/// Change PIN and external salt. Returns True on success, False on failure.
/// """
STATIC mp_obj_t mod_trezorconfig_change_pin(mp_obj_t pin, mp_obj_t newpin) {
uint32_t pin_i = trezor_obj_get_uint(pin);
uint32_t newpin_i = trezor_obj_get_uint(newpin);
if (sectrue != storage_change_pin(pin_i, newpin_i)) {
STATIC mp_obj_t mod_trezorconfig_change_pin(size_t n_args,
const mp_obj_t *args) {
uint32_t oldpin = trezor_obj_get_uint(args[0]);
uint32_t newpin = trezor_obj_get_uint(args[1]);
mp_buffer_info_t ext_salt_b;
const uint8_t *old_ext_salt = NULL;
if (args[2] != mp_const_none) {
mp_get_buffer_raise(args[2], &ext_salt_b, MP_BUFFER_READ);
if (ext_salt_b.len != EXTERNAL_SALT_SIZE)
mp_raise_msg(&mp_type_ValueError, "Invalid length of external salt.");
old_ext_salt = ext_salt_b.buf;
}
const uint8_t *new_ext_salt = NULL;
if (args[3] != mp_const_none) {
mp_get_buffer_raise(args[3], &ext_salt_b, MP_BUFFER_READ);
if (ext_salt_b.len != EXTERNAL_SALT_SIZE)
mp_raise_msg(&mp_type_ValueError, "Invalid length of external salt.");
new_ext_salt = ext_salt_b.buf;
}
if (sectrue !=
storage_change_pin(oldpin, newpin, old_ext_salt, new_ext_salt)) {
return mp_const_false;
}
return mp_const_true;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorconfig_change_pin_obj,
mod_trezorconfig_change_pin);
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorconfig_change_pin_obj, 4,
4, mod_trezorconfig_change_pin);
/// def get(app: int, key: int, public: bool = False) -> Optional[bytes]:
/// """

@ -12,17 +12,18 @@ def init(
# extmod/modtrezorconfig/modtrezorconfig.c
def unlock(pin: int) -> bool:
def unlock(pin: int, ext_salt: Optional[bytes]) -> bool:
"""
Attempts to unlock the storage with given PIN. Returns True on
success, False on failure.
Attempts to unlock the storage with the given PIN and external salt.
Returns True on success, False on failure.
"""
# extmod/modtrezorconfig/modtrezorconfig.c
def check_pin(pin: int) -> bool:
def check_pin(pin: int, ext_salt: Optional[bytes]) -> bool:
"""
Check the given PIN. Returns True on success, False on failure.
Check the given PIN with the given external salt.
Returns True on success, False on failure.
"""
@ -48,9 +49,14 @@ def get_pin_rem() -> int:
# extmod/modtrezorconfig/modtrezorconfig.c
def change_pin(pin: int, newpin: int) -> bool:
def change_pin(
oldpin: int,
newpin: int,
old_ext_salt: Optional[bytes],
new_ext_salt: Optional[bytes],
) -> bool:
"""
Change PIN. Returns True on success, False on failure.
Change PIN and external salt. Returns True on success, False on failure.
"""

@ -1,5 +1,17 @@
from trezor import loop
from trezor import config, loop, ui, wire
from trezor.messages import ButtonRequestType
from trezor.messages.ButtonAck import ButtonAck
from trezor.messages.ButtonRequest import ButtonRequest
from trezor.pin import pin_to_int
from trezor.ui.pin import CANCELLED, PinDialog
from trezor.ui.popup import Popup
from trezor.ui.text import Text
from apps.common.sd_salt import request_sd_salt
from apps.common.storage import device
if False:
from typing import Any, Optional, Tuple
if __debug__:
from apps.debug import input_signal
@ -9,6 +21,10 @@ class PinCancelled(Exception):
pass
class PinInvalid(Exception):
pass
async def request_pin(
prompt: str = "Enter your PIN",
attempts_remaining: int = None,
@ -31,3 +47,68 @@ async def request_pin(
if result is CANCELLED:
raise PinCancelled
return result
async def request_pin_ack(ctx: wire.Context, *args: Any, **kwargs: Any) -> str:
try:
await ctx.call(ButtonRequest(code=ButtonRequestType.Other), ButtonAck)
return await ctx.wait(request_pin(*args, **kwargs))
except PinCancelled:
raise wire.ActionCancelled("Cancelled")
async def request_pin_confirm(ctx: wire.Context, *args: Any, **kwargs: Any) -> str:
while True:
pin1 = await request_pin_ack(ctx, "Enter new PIN", *args, **kwargs)
pin2 = await request_pin_ack(ctx, "Re-enter new PIN", *args, **kwargs)
if pin1 == pin2:
return pin1
await pin_mismatch()
async def pin_mismatch() -> None:
text = Text("PIN mismatch", ui.ICON_WRONG, ui.RED)
text.normal("The PINs you entered", "do not match.")
text.normal("")
text.normal("Please try again.")
popup = Popup(text, 3000) # show for 3 seconds
await popup
async def request_pin_and_sd_salt(
ctx: wire.Context, prompt: str = "Enter your PIN", allow_cancel: bool = True
) -> Tuple[str, Optional[bytearray]]:
salt_auth_key = device.get_sd_salt_auth_key()
if salt_auth_key is not None:
salt = await request_sd_salt(ctx, salt_auth_key) # type: Optional[bytearray]
else:
salt = None
if config.has_pin():
pin = await request_pin_ack(ctx, prompt, config.get_pin_rem(), allow_cancel)
else:
pin = ""
return pin, salt
async def verify_user_pin(
prompt: str = "Enter your PIN", allow_cancel: bool = True, retry: bool = True
) -> None:
salt_auth_key = device.get_sd_salt_auth_key()
if salt_auth_key is not None:
salt = await request_sd_salt(None, salt_auth_key) # type: Optional[bytearray]
else:
salt = None
if not config.has_pin() and not config.check_pin(pin_to_int(""), salt):
raise RuntimeError
while retry:
pin = await request_pin(prompt, config.get_pin_rem(), allow_cancel)
if config.check_pin(pin_to_int(pin), salt):
return
else:
prompt = "Wrong PIN, enter again"
raise PinInvalid

@ -0,0 +1,193 @@
from micropython import const
from trezor import io, ui, wire
from trezor.crypto import hmac
from trezor.crypto.hashlib import sha256
from trezor.ui.confirm import Confirm
from trezor.ui.text import Text
from trezor.utils import consteq
from apps.common import storage
from apps.common.confirm import require_confirm
if False:
from typing import Optional
class SdProtectCancelled(Exception):
pass
SD_SALT_LEN_BYTES = const(32)
SD_SALT_AUTH_TAG_LEN_BYTES = const(16)
SD_SALT_AUTH_KEY_LEN_BYTES = const(16)
async def wrong_card_dialog(ctx: Optional[wire.Context]) -> None:
text = Text("SD card protection", ui.ICON_WRONG)
text.bold("Wrong SD card.")
text.br_half()
text.normal("Please unplug the", "device and insert a", "different card.")
if ctx is None:
await Confirm(text, confirm=None)
else:
await require_confirm(ctx, text, confirm=None)
async def insert_card_dialog(ctx: Optional[wire.Context]) -> None:
text = Text("SD card protection")
text.bold("SD card required.")
text.br_half()
text.normal("Please unplug the", "device and insert your", "SD card.")
if ctx is None:
await Confirm(text, confirm=None)
else:
await require_confirm(ctx, text, confirm=None)
async def request_sd_salt(
ctx: Optional[wire.Context], salt_auth_key: bytes
) -> bytearray:
device_dir = "/trezor/device_%s" % storage.device.get_device_id()
salt_path = "%s/salt" % device_dir
new_salt_path = "%s/salt.new" % device_dir
sd = io.SDCard()
fs = io.FatFS()
if not sd.power(True):
await insert_card_dialog(ctx)
raise SdProtectCancelled
try:
fs.mount()
# Load salt if it exists.
try:
with fs.open(salt_path, "r") as f:
salt = bytearray(SD_SALT_LEN_BYTES) # type: Optional[bytearray]
salt_tag = bytearray(SD_SALT_AUTH_TAG_LEN_BYTES)
f.read(salt)
f.read(salt_tag)
except OSError:
salt = None
if salt is not None and consteq(
hmac.new(salt_auth_key, salt, sha256).digest()[:SD_SALT_AUTH_TAG_LEN_BYTES],
salt_tag,
):
return salt
# Load salt.new if it exists.
try:
with fs.open(new_salt_path, "r") as f:
new_salt = bytearray(SD_SALT_LEN_BYTES) # type: Optional[bytearray]
new_salt_tag = bytearray(SD_SALT_AUTH_TAG_LEN_BYTES)
f.read(new_salt)
f.read(new_salt_tag)
except OSError:
new_salt = None
if new_salt is not None and consteq(
hmac.new(salt_auth_key, new_salt, sha256).digest()[
:SD_SALT_AUTH_TAG_LEN_BYTES
],
new_salt_tag,
):
# SD salt regeneration was interrupted earlier. Bring into consistent state.
# TODO Possibly overwrite salt file with random data.
try:
fs.unlink(salt_path)
except OSError:
pass
fs.rename(new_salt_path, salt_path)
return new_salt
finally:
fs.unmount()
sd.power(False)
await wrong_card_dialog(ctx)
raise SdProtectCancelled
async def set_sd_salt(
ctx: Optional[wire.Context], salt: bytes, salt_tag: bytes, filename: str = "salt"
) -> None:
device_dir = "/trezor/device_%s" % storage.device.get_device_id()
salt_path = "%s/%s" % (device_dir, filename)
sd = io.SDCard()
fs = io.FatFS()
if not sd.power(True):
await insert_card_dialog(ctx)
raise SdProtectCancelled
try:
fs.mount()
try:
fs.mkdir("/trezor")
except OSError:
# Directory already exists.
pass
try:
fs.mkdir(device_dir)
except OSError:
# Directory already exists.
pass
with fs.open(salt_path, "w") as f:
f.write(salt)
f.write(salt_tag)
finally:
fs.unmount()
sd.power(False)
async def stage_sd_salt(
ctx: Optional[wire.Context], salt: bytes, salt_tag: bytes
) -> None:
await set_sd_salt(ctx, salt, salt_tag, "salt.new")
async def commit_sd_salt(ctx: Optional[wire.Context]) -> None:
device_dir = "/trezor/device_%s" % storage.device.get_device_id()
salt_path = "%s/salt" % device_dir
new_salt_path = "%s/salt.new" % device_dir
sd = io.SDCard()
fs = io.FatFS()
if not sd.power(True):
await insert_card_dialog(ctx)
raise SdProtectCancelled
try:
fs.mount()
# TODO Possibly overwrite salt file with random data.
try:
fs.unlink(salt_path)
except OSError:
pass
fs.rename(new_salt_path, salt_path)
finally:
fs.unmount()
sd.power(False)
async def remove_sd_salt(ctx: Optional[wire.Context]) -> None:
device_dir = "/trezor/device_%s" % storage.device.get_device_id()
salt_path = "%s/salt" % device_dir
sd = io.SDCard()
fs = io.FatFS()
if not sd.power(True):
await insert_card_dialog(ctx)
raise SdProtectCancelled
try:
fs.mount()
# TODO Possibly overwrite salt file with random data.
fs.unlink(salt_path)
finally:
fs.unmount()
sd.power(False)

@ -28,8 +28,8 @@ def get(app: int, key: int, public: bool = False) -> Optional[bytes]:
return config.get(app, key, public)
def delete(app: int, key: int) -> None:
config.delete(app, key)
def delete(app: int, key: int, public: bool = False) -> None:
config.delete(app, key, public)
def set_true_or_delete(app: int, key: int, value: bool) -> None:

@ -3,6 +3,7 @@ from ubinascii import hexlify
from trezor.crypto import random
from apps.common.sd_salt import SD_SALT_AUTH_KEY_LEN_BYTES
from apps.common.storage import common
if False:
@ -31,6 +32,7 @@ _MNEMONIC_TYPE = const(0x0E) # int
_ROTATION = const(0x0F) # int
_SLIP39_IDENTIFIER = const(0x10) # bool
_SLIP39_ITERATION_EXPONENT = const(0x11) # int
_SD_SALT_AUTH_KEY = const(0x12) # bytes
# fmt: on
HOMESCREEN_MAXSIZE = 16384
@ -234,3 +236,25 @@ def get_slip39_iteration_exponent() -> Optional[int]:
The device's actual SLIP-39 iteration exponent used in passphrase derivation.
"""
return common.get_uint8(_NAMESPACE, _SLIP39_ITERATION_EXPONENT)
def get_sd_salt_auth_key() -> Optional[bytes]:
"""
The key used to check the authenticity of the SD card salt.
"""
auth_key = common.get(_NAMESPACE, _SD_SALT_AUTH_KEY, public=True)
if auth_key is not None and len(auth_key) != SD_SALT_AUTH_KEY_LEN_BYTES:
raise ValueError
return auth_key
def set_sd_salt_auth_key(auth_key: Optional[bytes]) -> None:
"""
The key used to check the authenticity of the SD card salt.
"""
if auth_key is not None:
if len(auth_key) != SD_SALT_AUTH_KEY_LEN_BYTES:
raise ValueError
return common.set(_NAMESPACE, _SD_SALT_AUTH_KEY, auth_key, public=True)
else:
return common.delete(_NAMESPACE, _SD_SALT_AUTH_KEY, public=True)

@ -14,3 +14,4 @@ def boot() -> None:
wire.add(MessageType.ApplyFlags, __name__, "apply_flags")
wire.add(MessageType.ChangePin, __name__, "change_pin")
wire.add(MessageType.SetU2FCounter, __name__, "set_u2f_counter")
wire.add(MessageType.SdProtect, __name__, "sd_protect")

@ -1,34 +1,26 @@
from trezor import config, ui, wire
from trezor.messages import ButtonRequestType
from trezor.messages.ButtonAck import ButtonAck
from trezor.messages.ButtonRequest import ButtonRequest
from trezor.messages.Success import Success
from trezor.pin import pin_to_int
from trezor.ui.popup import Popup
from trezor.ui.text import Text
from apps.common.confirm import require_confirm
from apps.common.request_pin import PinCancelled, request_pin
from apps.common.request_pin import request_pin_and_sd_salt, request_pin_confirm
if False:
from typing import Any
from trezor.messages.ChangePin import ChangePin
async def change_pin(ctx: wire.Context, msg: ChangePin) -> Success:
# confirm that user wants to change the pin
await require_confirm_change_pin(ctx, msg)
# get current pin, return failure if invalid
if config.has_pin():
curpin = await request_pin_ack(ctx, "Enter old PIN", config.get_pin_rem())
# if removing, defer check to change_pin()
if not msg.remove:
if not config.check_pin(pin_to_int(curpin)):
raise wire.PinInvalid("PIN invalid")
else:
curpin = ""
# get old pin
curpin, salt = await request_pin_and_sd_salt(ctx, "Enter old PIN")
# if changing pin, pre-check the entered pin before getting new pin
if curpin and not msg.remove:
if not config.check_pin(pin_to_int(curpin), salt):
raise wire.PinInvalid("PIN invalid")
# get new pin
if not msg.remove:
@ -37,7 +29,7 @@ async def change_pin(ctx: wire.Context, msg: ChangePin) -> Success:
newpin = ""
# write into storage
if not config.change_pin(pin_to_int(curpin), pin_to_int(newpin)):
if not config.change_pin(pin_to_int(curpin), pin_to_int(newpin), salt, salt):
raise wire.PinInvalid("PIN invalid")
if newpin:
@ -66,29 +58,3 @@ def require_confirm_change_pin(ctx: wire.Context, msg: ChangePin) -> None:
text.normal("Do you really want to")
text.bold("enable PIN protection?")
return require_confirm(ctx, text)
async def request_pin_confirm(ctx: wire.Context, *args: Any, **kwargs: Any) -> str:
while True:
pin1 = await request_pin_ack(ctx, "Enter new PIN", *args, **kwargs)
pin2 = await request_pin_ack(ctx, "Re-enter new PIN", *args, **kwargs)
if pin1 == pin2:
return pin1
await pin_mismatch()
async def request_pin_ack(ctx: wire.Context, *args: Any, **kwargs: Any) -> str:
try:
await ctx.call(ButtonRequest(code=ButtonRequestType.Other), ButtonAck)
return await ctx.wait(request_pin(*args, **kwargs))
except PinCancelled:
raise wire.ActionCancelled("Cancelled")
async def pin_mismatch() -> None:
text = Text("PIN mismatch", ui.ICON_WRONG, ui.RED)
text.normal("The PINs you entered", "do not match.")
text.normal("")
text.normal("Please try again.")
popup = Popup(text, 3000) # show for 3 seconds
await popup

@ -55,6 +55,6 @@ async def load_device(ctx, msg):
use_passphrase=msg.passphrase_protection, label=msg.label
)
if msg.pin:
config.change_pin(pin_to_int(""), pin_to_int(msg.pin))
config.change_pin(pin_to_int(""), pin_to_int(msg.pin), None, None)
return Success(message="Device loaded")

@ -6,7 +6,7 @@ from trezor.ui.text import Text
from apps.common import storage
from apps.common.confirm import require_confirm
from apps.management.change_pin import request_pin_ack, request_pin_confirm
from apps.common.request_pin import request_pin_and_sd_salt, request_pin_confirm
from apps.management.recovery_device.homescreen import recovery_process
if False:
@ -24,13 +24,10 @@ async def recovery_device(ctx: wire.Context, msg: RecoveryDevice) -> Success:
await _continue_dialog(ctx, msg)
# for dry run pin needs to entered
# for dry run pin needs to be entered
if msg.dry_run:
if config.has_pin():
curpin = await request_pin_ack(ctx, "Enter PIN", config.get_pin_rem())
else:
curpin = ""
if not config.check_pin(pin_to_int(curpin)):
curpin, salt = await request_pin_and_sd_salt(ctx, "Enter PIN")
if not config.check_pin(pin_to_int(curpin), salt):
raise wire.PinInvalid("PIN invalid")
# set up pin if requested
@ -38,7 +35,7 @@ async def recovery_device(ctx: wire.Context, msg: RecoveryDevice) -> Success:
if msg.dry_run:
raise wire.ProcessError("Can't setup PIN during dry_run recovery.")
newpin = await request_pin_confirm(ctx, allow_cancel=False)
config.change_pin(pin_to_int(""), pin_to_int(newpin))
config.change_pin(pin_to_int(""), pin_to_int(newpin), None, None)
if msg.u2f_counter:
storage.device.set_u2f_counter(msg.u2f_counter)

@ -10,7 +10,7 @@ from trezor.ui.text import Text
from apps.common import mnemonic, storage
from apps.common.confirm import require_confirm
from apps.management.change_pin import request_pin_confirm
from apps.common.request_pin import request_pin_confirm
from apps.management.common import layout
if __debug__:
@ -71,7 +71,7 @@ async def reset_device(ctx: wire.Context, msg: ResetDevice) -> Success:
await backup_bip39_wallet(ctx, secret)
# write PIN into storage
if not config.change_pin(pin_to_int(""), pin_to_int(newpin)):
if not config.change_pin(pin_to_int(""), pin_to_int(newpin), None, None):
raise wire.ProcessError("Could not change PIN")
# write settings and master secret into storage

@ -0,0 +1,169 @@
from trezor import config, ui, wire
from trezor.crypto import hmac, random
from trezor.crypto.hashlib import sha256
from trezor.messages import SdProtectOperationType
from trezor.messages.Success import Success
from trezor.pin import pin_to_int
from trezor.ui.text import Text
from apps.common.confirm import require_confirm
from apps.common.request_pin import request_pin_ack, request_pin_and_sd_salt
from apps.common.sd_salt import (
SD_SALT_AUTH_KEY_LEN_BYTES,
SD_SALT_AUTH_TAG_LEN_BYTES,
SD_SALT_LEN_BYTES,
commit_sd_salt,
remove_sd_salt,
set_sd_salt,
stage_sd_salt,
)
from apps.common.storage import device, is_initialized
if False:
from trezor.messages.SdProtect import SdProtect
async def sd_protect(ctx: wire.Context, msg: SdProtect) -> Success:
if not is_initialized():
raise wire.ProcessError("Device is not initialized")
if msg.operation == SdProtectOperationType.ENABLE:
return await sd_protect_enable(ctx, msg)
elif msg.operation == SdProtectOperationType.DISABLE:
return await sd_protect_disable(ctx, msg)
elif msg.operation == SdProtectOperationType.REFRESH:
return await sd_protect_refresh(ctx, msg)
else:
raise wire.ProcessError("Unknown operation")
async def sd_protect_enable(ctx: wire.Context, msg: SdProtect) -> Success:
salt_auth_key = device.get_sd_salt_auth_key()
if salt_auth_key is not None:
raise wire.ProcessError("SD card protection already enabled")
# Confirm that user wants to proceed with the operation.
await require_confirm_sd_protect(ctx, msg)
# Get the current PIN.
if config.has_pin():
pin = pin_to_int(await request_pin_ack(ctx, "Enter PIN", config.get_pin_rem()))
else:
pin = pin_to_int("")
# Check PIN and prepare salt file.
salt = random.bytes(SD_SALT_LEN_BYTES)
salt_auth_key = random.bytes(SD_SALT_AUTH_KEY_LEN_BYTES)
salt_tag = hmac.new(salt_auth_key, salt, sha256).digest()[
:SD_SALT_AUTH_TAG_LEN_BYTES
]
try:
await set_sd_salt(ctx, salt, salt_tag)
except Exception:
raise wire.ProcessError("Failed to write to SD card")
if not config.change_pin(pin, pin, None, salt):
# Wrong PIN. Clean up the prepared salt file.
try:
await remove_sd_salt(ctx)
except Exception:
# The cleanup is not necessary for the correct functioning of
# SD-protection. If it fails for any reason, we suppress the
# exception, because primarily we need to raise wire.PinInvalid.
pass
raise wire.PinInvalid("PIN invalid")
device.set_sd_salt_auth_key(salt_auth_key)
return Success(message="SD card protection enabled")
async def sd_protect_disable(ctx: wire.Context, msg: SdProtect) -> Success:
if device.get_sd_salt_auth_key() is None:
raise wire.ProcessError("SD card protection not enabled")
# Confirm that user wants to proceed with the operation.
await require_confirm_sd_protect(ctx, msg)
# Get the current PIN and salt from the SD card.
pin, salt = await request_pin_and_sd_salt(ctx, "Enter PIN")
# Check PIN and remove salt.
if not config.change_pin(pin_to_int(pin), pin_to_int(pin), salt, None):
raise wire.PinInvalid("PIN invalid")
device.set_sd_salt_auth_key(None)
try:
# Clean up.
await remove_sd_salt(ctx)
except Exception:
# The cleanup is not necessary for the correct functioning of
# SD-protection. If it fails for any reason, we suppress the exception,
# because overall SD-protection was successfully disabled.
pass
return Success(message="SD card protection disabled")
async def sd_protect_refresh(ctx: wire.Context, msg: SdProtect) -> Success:
if device.get_sd_salt_auth_key() is None:
raise wire.ProcessError("SD card protection not enabled")
# Confirm that user wants to proceed with the operation.
await require_confirm_sd_protect(ctx, msg)
# Get the current PIN and salt from the SD card.
pin, old_salt = await request_pin_and_sd_salt(ctx, "Enter PIN")
# Check PIN and change salt.
new_salt = random.bytes(SD_SALT_LEN_BYTES)
new_salt_auth_key = random.bytes(SD_SALT_AUTH_KEY_LEN_BYTES)
new_salt_tag = hmac.new(new_salt_auth_key, new_salt, sha256).digest()[
:SD_SALT_AUTH_TAG_LEN_BYTES
]
try:
await stage_sd_salt(ctx, new_salt, new_salt_tag)
except Exception:
raise wire.ProcessError("Failed to write to SD card")
if not config.change_pin(pin_to_int(pin), pin_to_int(pin), old_salt, new_salt):
raise wire.PinInvalid("PIN invalid")
device.set_sd_salt_auth_key(new_salt_auth_key)
try:
# Clean up.
await commit_sd_salt(ctx)
except Exception:
# If the cleanup fails, then request_sd_salt() will bring the SD card
# into a consistent state. We suppress the exception, because overall
# SD-protection was successfully refreshed.
pass
return Success(message="SD card protection refreshed")
def require_confirm_sd_protect(ctx: wire.Context, msg: SdProtect) -> None:
if msg.operation == SdProtectOperationType.ENABLE:
text = Text("SD card protection", ui.ICON_CONFIG)
text.normal(
"Do you really want to", "secure your device with", "SD card protection?"
)
elif msg.operation == SdProtectOperationType.DISABLE:
text = Text("SD card protection", ui.ICON_CONFIG)
text.normal(
"Do you really want to", "remove SD card", "protection from your", "device?"
)
elif msg.operation == SdProtectOperationType.REFRESH:
text = Text("SD card protection", ui.ICON_CONFIG)
text.normal(
"Do you really want to",
"replace the current",
"SD card secret with a",
"newly generated one?",
)
else:
raise wire.ProcessError("Unknown operation")
return require_confirm(ctx, text)

@ -512,20 +512,15 @@ class KeepaliveCallback:
send_cmd_sync(cmd_keepalive(self.cid, _KEEPALIVE_STATUS_PROCESSING), self.iface)
async def check_pin(keepalive_callback: KeepaliveCallback) -> bool:
from apps.common.request_pin import PinCancelled, request_pin
async def verify_user(keepalive_callback: KeepaliveCallback) -> bool:
from apps.common.request_pin import verify_user_pin, PinCancelled, PinInvalid
import trezor.pin
try:
trezor.pin.keepalive_callback = keepalive_callback
if config.has_pin():
pin = await request_pin("Enter your PIN", config.get_pin_rem())
while config.unlock(trezor.pin.pin_to_int(pin)) is not True:
pin = await request_pin("Wrong PIN, enter again", config.get_pin_rem())
ret = True
else:
ret = config.unlock(trezor.pin.pin_to_int(""))
except PinCancelled:
await verify_user_pin()
ret = True
except (PinCancelled, PinInvalid):
ret = False
finally:
trezor.pin.keepalive_callback = None
@ -695,7 +690,7 @@ class Fido2ConfirmMakeCredential(Fido2State, ConfirmInfo):
if not await confirm(content):
return False
if self._user_verification:
return await check_pin(KeepaliveCallback(self.cid, self.iface))
return await verify_user(KeepaliveCallback(self.cid, self.iface))
return True
async def on_confirm(self) -> None:
@ -764,7 +759,7 @@ class Fido2ConfirmGetAssertion(Fido2State, ConfirmInfo, Pageable):
if await ConfirmPageable(self, content) is not CONFIRMED:
return False
if self._user_verification:
return await check_pin(KeepaliveCallback(self.cid, self.iface))
return await verify_user(KeepaliveCallback(self.cid, self.iface))
return True
async def on_confirm(self) -> None:

@ -1,23 +1,40 @@
from trezor import config, log, loop, res, ui
from trezor import config, io, log, loop, res, ui, utils
from trezor.pin import pin_to_int, show_pin_timeout
from apps.common import storage
from apps.common.request_pin import request_pin
from apps.common.sd_salt import request_sd_salt
from apps.common.storage import device
if False:
from typing import Optional
async def bootscreen() -> None:
ui.display.orientation(storage.device.get_rotation())
salt_auth_key = device.get_sd_salt_auth_key()
while True:
try:
if salt_auth_key is not None or config.has_pin():
await lockscreen()
if salt_auth_key is not None:
salt = await request_sd_salt(
None, salt_auth_key
) # type: Optional[bytearray]
else:
salt = None
if not config.has_pin():
config.unlock(pin_to_int(""))
config.unlock(pin_to_int(""), salt)
storage.init_unlocked()
return
await lockscreen()
label = "Enter your PIN"
while True:
pin = await request_pin(label, config.get_pin_rem())
if config.unlock(pin_to_int(pin)):
if config.unlock(pin_to_int(pin), salt):
storage.init_unlocked()
return
else:
@ -55,6 +72,19 @@ async def lockscreen() -> None:
await ui.click()
if utils.EMULATOR:
# Ensure the emulated SD card is FAT32 formatted.
sd = io.SDCard()
fs = io.FatFS()
sd.power(True)
try:
fs.mount()
except OSError:
fs.mkfs()
else:
fs.unmount()
sd.power(False)
ui.display.backlight(ui.BACKLIGHT_NONE)
ui.backlight_fade(ui.BACKLIGHT_NORMAL)
config.init(show_pin_timeout)

@ -33,6 +33,7 @@ WordRequest = 46
WordAck = 47
GetFeatures = 55
SetU2FCounter = 63
SdProtect = 79
FirmwareErase = 6
FirmwareUpload = 7
FirmwareRequest = 8

@ -0,0 +1,28 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
if __debug__:
try:
from typing import Dict, List, Optional
from typing_extensions import Literal # noqa: F401
EnumTypeSdProtectOperationType = Literal[0, 1, 2]
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
EnumTypeSdProtectOperationType = None # type: ignore
class SdProtect(p.MessageType):
MESSAGE_WIRE_TYPE = 79
def __init__(
self,
operation: EnumTypeSdProtectOperationType = None,
) -> None:
self.operation = operation
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('operation', p.EnumType("SdProtectOperationType", (0, 1, 2)), 0),
}

@ -0,0 +1,5 @@
# Automatically generated by pb2py
# fmt: off
DISABLE = 0
ENABLE = 1
REFRESH = 2

@ -12,7 +12,7 @@ from trezor.ui.button import (
)
if False:
from typing import Iterable
from typing import Iterable, Optional
def digit_area(i: int) -> ui.Area:
@ -30,7 +30,7 @@ def generate_digits() -> Iterable[int]:
class PinInput(ui.Component):
def __init__(self, prompt: str, subprompt: str, pin: str) -> None:
def __init__(self, prompt: str, subprompt: Optional[str], pin: str) -> None:
self.prompt = prompt
self.subprompt = subprompt
self.pin = pin
@ -82,7 +82,11 @@ CANCELLED = object()
class PinDialog(ui.Layout):
def __init__(
self, prompt: str, subprompt: str, allow_cancel: bool = True, maxlength: int = 9
self,
prompt: str,
subprompt: Optional[str],
allow_cancel: bool = True,
maxlength: int = 9,
) -> None:
self.maxlength = maxlength
self.input = PinInput(prompt, subprompt, "")

@ -25,7 +25,7 @@ if [[ $RUN_TEST_EMU > 0 ]]; then
$MICROPYTHON $ARGS "${MAIN}" &> "${TREZOR_LOGFILE}" &
upy_pid=$!
cd -
sleep 1
sleep 30
fi
# run tests

@ -27,7 +27,7 @@ if [[ $RUN_TEST_EMU > 0 ]]; then
$MICROPYTHON $ARGS "${MAIN}" &> "${TREZOR_LOGFILE}" &
upy_pid=$!
cd -
sleep 1
sleep 30
fi
DOCKER_ID=""

@ -25,7 +25,7 @@ if [[ $RUN_TEST_EMU > 0 ]]; then
$MICROPYTHON $ARGS "${MAIN}" &> "${TREZOR_LOGFILE}" &
upy_pid=$!
cd -
sleep 1
sleep 30
fi
# run tests

@ -27,7 +27,7 @@ class TestConfig(unittest.TestCase):
def test_wipe(self):
config.init()
config.wipe()
self.assertEqual(config.unlock(pin_to_int('')), True)
self.assertEqual(config.unlock(pin_to_int(''), None), True)
config.set(1, 1, b'hello')
config.set(1, 2, b'world')
v0 = config.get(1, 1)
@ -44,7 +44,7 @@ class TestConfig(unittest.TestCase):
for _ in range(128):
config.init()
config.wipe()
self.assertEqual(config.unlock(pin_to_int('')), True)
self.assertEqual(config.unlock(pin_to_int(''), None), True)
appid, key = random_entry()
value = random.bytes(16)
config.set(appid, key, value)
@ -58,7 +58,7 @@ class TestConfig(unittest.TestCase):
def test_public(self):
config.init()
config.wipe()
self.assertEqual(config.unlock(pin_to_int('')), True)
self.assertEqual(config.unlock(pin_to_int(''), None), True)
appid, key = random_entry()
@ -84,25 +84,59 @@ class TestConfig(unittest.TestCase):
def test_change_pin(self):
config.init()
config.wipe()
self.assertEqual(config.unlock(pin_to_int('')), True)
self.assertEqual(config.unlock(pin_to_int(''), None), True)
with self.assertRaises(RuntimeError):
config.set(PINAPP, PINKEY, b'value')
self.assertEqual(config.change_pin(pin_to_int('000'), pin_to_int('666')), False)
self.assertEqual(config.change_pin(pin_to_int(''), pin_to_int('000')), True)
self.assertEqual(config.change_pin(pin_to_int('000'), pin_to_int('666'), None, None), False)
self.assertEqual(config.change_pin(pin_to_int(''), pin_to_int('000'), None, None), True)
self.assertEqual(config.get(PINAPP, PINKEY), None)
config.set(1, 1, b'value')
config.init()
self.assertEqual(config.unlock(pin_to_int('000')), True)
config.change_pin(pin_to_int('000'), pin_to_int(''))
self.assertEqual(config.unlock(pin_to_int('000'), None), True)
config.change_pin(pin_to_int('000'), pin_to_int(''), None, None)
config.init()
self.assertEqual(config.unlock(pin_to_int('000')), False)
self.assertEqual(config.unlock(pin_to_int('')), True)
self.assertEqual(config.unlock(pin_to_int('000'), None), False)
self.assertEqual(config.unlock(pin_to_int(''), None), True)
self.assertEqual(config.get(1, 1), b'value')
def test_change_sd_salt(self):
salt1 = b"0123456789abcdef0123456789abcdef"
salt2 = b"0123456789ABCDEF0123456789ABCDEF"
# Enable PIN and SD salt.
config.init()
config.wipe()
self.assertTrue(config.unlock(pin_to_int(''), None))
config.set(1, 1, b'value')
self.assertFalse(config.change_pin(pin_to_int(''), pin_to_int(''), salt1, None))
self.assertTrue(config.change_pin(pin_to_int(''), pin_to_int('000'), None, salt1))
self.assertEqual(config.get(1, 1), b'value')
# Disable PIN and change SD salt.
config.init()
self.assertFalse(config.unlock(pin_to_int('000'), None))
self.assertIsNone(config.get(1, 1))
self.assertTrue(config.unlock(pin_to_int('000'), salt1))
self.assertTrue(config.change_pin(pin_to_int('000'), pin_to_int(''), salt1, salt2))
self.assertEqual(config.get(1, 1), b'value')
# Disable SD salt.
config.init()
self.assertFalse(config.unlock(pin_to_int('000'), salt2))
self.assertIsNone(config.get(1, 1))
self.assertTrue(config.unlock(pin_to_int(''), salt2))
self.assertTrue(config.change_pin(pin_to_int(''), pin_to_int(''), salt2, None))
self.assertEqual(config.get(1, 1), b'value')
# Check that PIN and SD salt are disabled.
config.init()
self.assertTrue(config.unlock(pin_to_int(''), None))
self.assertEqual(config.get(1, 1), b'value')
def test_set_get(self):
config.init()
config.wipe()
self.assertEqual(config.unlock(pin_to_int('')), True)
self.assertEqual(config.unlock(pin_to_int(''), None), True)
for _ in range(32):
appid, key = random_entry()
value = random.bytes(128)
@ -113,7 +147,7 @@ class TestConfig(unittest.TestCase):
def test_compact(self):
config.init()
config.wipe()
self.assertEqual(config.unlock(pin_to_int('')), True)
self.assertEqual(config.unlock(pin_to_int(''), None), True)
appid, key = 1, 1
for _ in range(259):
value = random.bytes(259)
@ -124,7 +158,7 @@ class TestConfig(unittest.TestCase):
def test_get_default(self):
config.init()
config.wipe()
self.assertEqual(config.unlock(pin_to_int('')), True)
self.assertEqual(config.unlock(pin_to_int(''), None), True)
for _ in range(128):
appid, key = random_entry()
value = config.get(appid, key)

@ -316,9 +316,9 @@ static secbool config_upgrade_v10(void) {
}
storage_init(NULL, HW_ENTROPY_DATA, HW_ENTROPY_LEN);
storage_unlock(PIN_EMPTY);
storage_unlock(PIN_EMPTY, NULL);
if (config.has_pin) {
storage_change_pin(PIN_EMPTY, pin_to_int(config.pin));
storage_change_pin(PIN_EMPTY, pin_to_int(config.pin), NULL, NULL);
}
while (pin_wait != 0) {
@ -386,7 +386,7 @@ void config_init(void) {
// Auto-unlock storage if no PIN is set.
if (storage_is_unlocked() == secfalse && storage_has_pin() == secfalse) {
storage_unlock(PIN_EMPTY);
storage_unlock(PIN_EMPTY, NULL);
}
uint16_t len = 0;
@ -759,7 +759,7 @@ bool config_containsMnemonic(const char *mnemonic) {
*/
bool config_unlock(const char *pin) {
char oldTiny = usbTiny(1);
secbool ret = storage_unlock(pin_to_int(pin));
secbool ret = storage_unlock(pin_to_int(pin), NULL);
usbTiny(oldTiny);
return sectrue == ret;
}
@ -773,7 +773,8 @@ bool config_changePin(const char *old_pin, const char *new_pin) {
}
char oldTiny = usbTiny(1);
secbool ret = storage_change_pin(pin_to_int(old_pin), new_pin_int);
secbool ret =
storage_change_pin(pin_to_int(old_pin), new_pin_int, NULL, NULL);
usbTiny(oldTiny);
#if DEBUG_LINK
@ -925,7 +926,7 @@ void config_wipe(void) {
char oldTiny = usbTiny(1);
storage_wipe();
if (storage_is_unlocked() != sectrue) {
storage_unlock(PIN_EMPTY);
storage_unlock(PIN_EMPTY, NULL);
}
usbTiny(oldTiny);
random_buffer((uint8_t *)config_uuid, sizeof(config_uuid));

@ -2,7 +2,7 @@ ifneq ($(V),1)
Q := @
endif
SKIPPED_MESSAGES := Binance Cardano DebugMonero Eos Monero Ontology Ripple Tezos WebAuthn
SKIPPED_MESSAGES := Binance Cardano DebugMonero Eos Monero Ontology Ripple SdProtect Tezos WebAuthn
ifeq ($(BITCOIN_ONLY), 1)
SKIPPED_MESSAGES += Ethereum Lisk NEM Stellar

@ -24,7 +24,7 @@ Use the following command to see all options:
cardano-get-address Get Cardano address.
cardano-get-public-key Get Cardano public key.
cardano-sign-tx Sign Cardano transaction.
change-pin Change new PIN or remove existing.
change-pin Set, change or remove PIN.
clear-session Clear session (remove cached PIN, passphrase, etc.).
cosi-commit Ask device to commit to CoSi signing.
cosi-sign Ask device to sign using CoSi.
@ -66,6 +66,7 @@ Use the following command to see all options:
reset-device Perform device setup and generate new seed.
ripple-get-address Get Ripple address
ripple-sign-tx Sign Ripple transaction
sd-protect Secure the device with SD card protection.
self-test Perform a self-test.
set-auto-lock-delay Set auto-lock delay (in seconds).
set-flags Set device flags.

@ -123,6 +123,14 @@ CHOICE_RESET_DEVICE_TYPE = ChoiceType(
}
)
CHOICE_SD_PROTECT_OPERATION_TYPE = ChoiceType(
{
"enable": proto.SdProtectOperationType.ENABLE,
"disable": proto.SdProtectOperationType.DISABLE,
"refresh": proto.SdProtectOperationType.REFRESH,
}
)
class UnderscoreAgnosticGroup(click.Group):
"""Command group that normalizes dashes and underscores.
@ -261,13 +269,35 @@ def get_features(connect):
#
@cli.command(help="Change new PIN or remove existing.")
@cli.command(help="Set, change or remove PIN.")
@click.option("-r", "--remove", is_flag=True)
@click.pass_obj
def change_pin(connect, remove):
return device.change_pin(connect(), remove)
@cli.command()
@click.argument("operation", type=CHOICE_SD_PROTECT_OPERATION_TYPE)
@click.pass_obj
def sd_protect(connect, operation):
"""Secure the device with SD card protection.
When SD card protection is enabled, a randomly generated secret is stored
on the SD card. During every PIN checking and unlocking operation this
secret is combined with the entered PIN value to decrypt data stored on
the device. The SD card will thus be needed every time you unlock the
device. The options are:
\b
enable - Generate SD card secret and use it to protect the PIN and storage.
disable - Remove SD card secret protection.
refresh - Replace the current SD card secret with a new one.
"""
if connect().features.model == "1":
raise click.BadUsage("Trezor One does not support SD card protection.")
return device.sd_protect(connect(), operation)
@cli.command(help="Enable passphrase.")
@click.pass_obj
def enable_passphrase(connect):

@ -90,6 +90,12 @@ def change_pin(client, remove=False):
return ret
@expect(proto.Success, field="message")
def sd_protect(client, operation):
ret = client.call(proto.SdProtect(operation=operation))
return ret
@expect(proto.Success, field="message")
def set_u2f_counter(client, u2f_counter):
ret = client.call(proto.SetU2FCounter(u2f_counter=u2f_counter))

@ -31,6 +31,7 @@ WordRequest = 46
WordAck = 47
GetFeatures = 55
SetU2FCounter = 63
SdProtect = 79
FirmwareErase = 6
FirmwareUpload = 7
FirmwareRequest = 8

@ -0,0 +1,28 @@
# Automatically generated by pb2py
# fmt: off
from .. import protobuf as p
if __debug__:
try:
from typing import Dict, List, Optional
from typing_extensions import Literal # noqa: F401
EnumTypeSdProtectOperationType = Literal[0, 1, 2]
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
EnumTypeSdProtectOperationType = None # type: ignore
class SdProtect(p.MessageType):
MESSAGE_WIRE_TYPE = 79
def __init__(
self,
operation: EnumTypeSdProtectOperationType = None,
) -> None:
self.operation = operation
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('operation', p.EnumType("SdProtectOperationType", (0, 1, 2)), 0),
}

@ -0,0 +1,5 @@
# Automatically generated by pb2py
# fmt: off
DISABLE = 0
ENABLE = 1
REFRESH = 2

@ -201,6 +201,7 @@ from .RippleGetAddress import RippleGetAddress
from .RipplePayment import RipplePayment
from .RippleSignTx import RippleSignTx
from .RippleSignedTx import RippleSignedTx
from .SdProtect import SdProtect
from .SelfTest import SelfTest
from .SetU2FCounter import SetU2FCounter
from .SignIdentity import SignIdentity
@ -274,6 +275,7 @@ from . import PinMatrixRequestType
from . import RecoveryDeviceType
from . import RequestType
from . import ResetDeviceBackupType
from . import SdProtectOperationType
from . import TezosBallotType
from . import TezosContractType
from . import WordRequestType

@ -331,15 +331,26 @@ static secbool auth_get(uint16_t key, const void **val, uint16_t *len) {
}
static void derive_kek(uint32_t pin, const uint8_t *random_salt,
const uint8_t *ext_salt,
uint8_t kek[SHA256_DIGEST_LENGTH],
uint8_t keiv[SHA256_DIGEST_LENGTH]) {
#if BYTE_ORDER == BIG_ENDIAN
REVERSE32(pin, pin);
#endif
uint8_t salt[HARDWARE_SALT_SIZE + RANDOM_SALT_SIZE];
memcpy(salt, hardware_salt, HARDWARE_SALT_SIZE);
memcpy(salt + HARDWARE_SALT_SIZE, random_salt, RANDOM_SALT_SIZE);
uint8_t salt[HARDWARE_SALT_SIZE + RANDOM_SALT_SIZE + EXTERNAL_SALT_SIZE];
size_t salt_len = 0;
memcpy(salt + salt_len, hardware_salt, HARDWARE_SALT_SIZE);
salt_len += HARDWARE_SALT_SIZE;
memcpy(salt + salt_len, random_salt, RANDOM_SALT_SIZE);
salt_len += RANDOM_SALT_SIZE;
if (ext_salt != NULL) {
memcpy(salt + salt_len, ext_salt, EXTERNAL_SALT_SIZE);
salt_len += EXTERNAL_SALT_SIZE;
}
uint32_t progress = (ui_total - ui_rem) * 1000 / ui_total;
if (ui_callback && ui_message) {
@ -348,7 +359,7 @@ static void derive_kek(uint32_t pin, const uint8_t *random_salt,
PBKDF2_HMAC_SHA256_CTX ctx;
pbkdf2_hmac_sha256_Init(&ctx, (const uint8_t *)&pin, sizeof(pin), salt,
sizeof(salt), 1);
salt_len, 1);
for (int i = 1; i <= 5; i++) {
pbkdf2_hmac_sha256_Update(&ctx, PIN_ITER_COUNT / 10);
if (ui_callback && ui_message) {
@ -360,7 +371,7 @@ static void derive_kek(uint32_t pin, const uint8_t *random_salt,
pbkdf2_hmac_sha256_Final(&ctx, kek);
pbkdf2_hmac_sha256_Init(&ctx, (const uint8_t *)&pin, sizeof(pin), salt,
sizeof(salt), 2);
salt_len, 2);
for (int i = 6; i <= 10; i++) {
pbkdf2_hmac_sha256_Update(&ctx, PIN_ITER_COUNT / 10);
if (ui_callback && ui_message) {
@ -377,17 +388,17 @@ static void derive_kek(uint32_t pin, const uint8_t *random_salt,
memzero(&salt, sizeof(salt));
}
static secbool set_pin(uint32_t pin) {
static secbool set_pin(uint32_t pin, const uint8_t *ext_salt) {
uint8_t buffer[RANDOM_SALT_SIZE + KEYS_SIZE + POLY1305_TAG_SIZE];
uint8_t *salt = buffer;
uint8_t *rand_salt = buffer;
uint8_t *ekeys = buffer + RANDOM_SALT_SIZE;
uint8_t *pvc = buffer + RANDOM_SALT_SIZE + KEYS_SIZE;
uint8_t kek[SHA256_DIGEST_LENGTH];
uint8_t keiv[SHA256_DIGEST_LENGTH];
chacha20poly1305_ctx ctx;
random_buffer(salt, RANDOM_SALT_SIZE);
derive_kek(pin, salt, kek, keiv);
random_buffer(rand_salt, RANDOM_SALT_SIZE);
derive_kek(pin, rand_salt, ext_salt, kek, keiv);
rfc7539_init(&ctx, kek, keiv);
memzero(kek, sizeof(kek));
memzero(keiv, sizeof(keiv));
@ -515,7 +526,7 @@ static void init_wiped_storage(void) {
ui_total = DERIVE_SECS;
ui_rem = ui_total;
ui_message = PROCESSING_MSG;
ensure(set_pin(PIN_EMPTY), "init_pin failed");
ensure(set_pin(PIN_EMPTY, NULL), "init_pin failed");
if (unlocked != sectrue) {
memzero(cached_keys, sizeof(cached_keys));
}
@ -784,7 +795,7 @@ static secbool decrypt_dek(const uint8_t *kek, const uint8_t *keiv) {
return sectrue;
}
static secbool unlock(uint32_t pin) {
static secbool unlock(uint32_t pin, const uint8_t *ext_salt) {
if (sectrue != initialized) {
return secfalse;
}
@ -827,10 +838,10 @@ static secbool unlock(uint32_t pin) {
// Read the random salt from EDEK_PVC_KEY and use it to derive the KEK and
// KEIV from the PIN.
const void *salt = NULL;
const void *rand_salt = NULL;
uint16_t len = 0;
if (sectrue != initialized ||
sectrue != norcow_get(EDEK_PVC_KEY, &salt, &len) ||
sectrue != norcow_get(EDEK_PVC_KEY, &rand_salt, &len) ||
len != RANDOM_SALT_SIZE + KEYS_SIZE + PVC_SIZE) {
memzero(&pin, sizeof(pin));
handle_fault("no EDEK");
@ -838,7 +849,7 @@ static secbool unlock(uint32_t pin) {
}
uint8_t kek[SHA256_DIGEST_LENGTH];
uint8_t keiv[SHA256_DIGEST_LENGTH];
derive_kek(pin, (const uint8_t *)salt, kek, keiv);
derive_kek(pin, (const uint8_t *)rand_salt, ext_salt, kek, keiv);
memzero(&pin, sizeof(pin));
// First, we increase PIN fail counter in storage, even before checking the
@ -875,7 +886,7 @@ static secbool unlock(uint32_t pin) {
return pin_fails_reset();
}
secbool storage_unlock(uint32_t pin) {
secbool storage_unlock(uint32_t pin, const uint8_t *ext_salt) {
ui_total = DERIVE_SECS;
ui_rem = ui_total;
if (pin == PIN_EMPTY) {
@ -887,7 +898,7 @@ secbool storage_unlock(uint32_t pin) {
} else {
ui_message = VERIFYING_PIN_MSG;
}
return unlock(pin);
return unlock(pin, ext_salt);
}
/*
@ -1152,7 +1163,9 @@ uint32_t storage_get_pin_rem(void) {
return PIN_MAX_TRIES - ctr;
}
secbool storage_change_pin(uint32_t oldpin, uint32_t newpin) {
secbool storage_change_pin(uint32_t oldpin, uint32_t newpin,
const uint8_t *old_ext_salt,
const uint8_t *new_ext_salt) {
if (sectrue != initialized) {
return secfalse;
}
@ -1162,10 +1175,10 @@ secbool storage_change_pin(uint32_t oldpin, uint32_t newpin) {
ui_message = (oldpin != PIN_EMPTY && newpin == PIN_EMPTY) ? VERIFYING_PIN_MSG
: PROCESSING_MSG;
if (sectrue != unlock(oldpin)) {
if (sectrue != unlock(oldpin, old_ext_salt)) {
return secfalse;
}
secbool ret = set_pin(newpin);
secbool ret = set_pin(newpin, new_ext_salt);
memzero(&oldpin, sizeof(oldpin));
memzero(&newpin, sizeof(newpin));
return ret;
@ -1268,9 +1281,9 @@ static secbool storage_upgrade(void) {
ui_rem = ui_total;
ui_message = PROCESSING_MSG;
if (sectrue == norcow_get(V0_PIN_KEY, &val, &len)) {
set_pin(*(const uint32_t *)val);
set_pin(*(const uint32_t *)val, NULL);
} else {
set_pin(PIN_EMPTY);
set_pin(PIN_EMPTY, NULL);
}
// Convert PIN failure counter.

@ -24,6 +24,9 @@
#include <stdint.h>
#include "secbool.h"
// The length of the external salt in bytes.
#define EXTERNAL_SALT_SIZE 32
typedef secbool (*PIN_UI_WAIT_CALLBACK)(uint32_t wait, uint32_t progress,
const char *message);
@ -32,11 +35,13 @@ void storage_init(PIN_UI_WAIT_CALLBACK callback, const uint8_t *salt,
void storage_wipe(void);
secbool storage_is_unlocked(void);
void storage_lock(void);
secbool storage_unlock(const uint32_t pin);
secbool storage_unlock(const uint32_t pin, const uint8_t *ext_salt);
secbool storage_has_pin(void);
secbool storage_pin_fails_increase(void);
uint32_t storage_get_pin_rem(void);
secbool storage_change_pin(const uint32_t oldpin, const uint32_t newpin);
secbool storage_change_pin(const uint32_t oldpin, const uint32_t newpin,
const uint8_t *old_ext_salt,
const uint8_t *new_ext_salt);
secbool storage_get(const uint16_t key, void *val, const uint16_t max_len,
uint16_t *len);
secbool storage_set(const uint16_t key, const void *val, uint16_t len);

@ -1,6 +1,7 @@
import ctypes as c
import os
EXTERNAL_SALT_LEN = 32
sectrue = -1431655766 # 0xAAAAAAAAA
fname = os.path.join(os.path.dirname(__file__), "libtrezor-storage.so")
@ -20,8 +21,10 @@ class Storage:
def wipe(self) -> None:
self.lib.storage_wipe()
def unlock(self, pin: int) -> bool:
return sectrue == self.lib.storage_unlock(c.c_uint32(pin))
def unlock(self, pin: int, ext_salt: bytes = None) -> bool:
if ext_salt is not None and len(ext_salt) != EXTERNAL_SALT_LEN:
raise ValueError
return sectrue == self.lib.storage_unlock(c.c_uint32(pin), ext_salt)
def lock(self) -> None:
self.lib.storage_lock()
@ -32,9 +35,19 @@ class Storage:
def get_pin_rem(self) -> int:
return self.lib.storage_get_pin_rem()
def change_pin(self, oldpin: int, newpin: int) -> bool:
def change_pin(
self,
oldpin: int,
newpin: int,
old_ext_salt: bytes = None,
new_ext_salt: bytes = None,
) -> bool:
if old_ext_salt is not None and len(old_ext_salt) != EXTERNAL_SALT_LEN:
raise ValueError
if new_ext_salt is not None and len(new_ext_salt) != EXTERNAL_SALT_LEN:
raise ValueError
return sectrue == self.lib.storage_change_pin(
c.c_uint32(oldpin), c.c_uint32(newpin)
c.c_uint32(oldpin), c.c_uint32(newpin), old_ext_salt, new_ext_salt
)
def get(self, key: int) -> bytes:

@ -0,0 +1,62 @@
# 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 debuglink, device, messages as proto
from trezorlib.exceptions import TrezorFailure
from ..common import MNEMONIC12
@pytest.mark.skip_t1
class TestMsgSdProtect:
@pytest.mark.setup_client(mnemonic=MNEMONIC12)
def test_sd_protect(self, client):
# Disabling SD protection should fail
with pytest.raises(TrezorFailure):
device.sd_protect(client, proto.SdProtectOperationType.DISABLE)
# Enable SD protection
device.sd_protect(client, proto.SdProtectOperationType.ENABLE)
# Enabling SD protection should fail
with pytest.raises(TrezorFailure):
device.sd_protect(client, proto.SdProtectOperationType.ENABLE)
# Wipe
device.wipe(client)
debuglink.load_device_by_mnemonic(
client,
mnemonic=MNEMONIC12,
pin="",
passphrase_protection=False,
label="test",
)
# Enable SD protection
device.sd_protect(client, proto.SdProtectOperationType.ENABLE)
# Refresh SD protection
device.sd_protect(client, proto.SdProtectOperationType.REFRESH)
# Disable SD protection
device.sd_protect(client, proto.SdProtectOperationType.DISABLE)
# Refreshing SD protection should fail
with pytest.raises(TrezorFailure):
device.sd_protect(client, proto.SdProtectOperationType.REFRESH)
Loading…
Cancel
Save