1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-19 12:58:13 +00:00

Merge branch 'andrewkozlik/sd-protect'

This commit is contained in:
Pavol Rusnak 2019-09-18 19:12:41 +02:00
commit 5b11d9a65d
No known key found for this signature in database
GPG Key ID: 91F3B339B9A02A3D
39 changed files with 906 additions and 154 deletions

View File

@ -131,6 +131,24 @@ message ChangePin {
optional bool remove = 1; // is PIN removal requested? 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 * Request: Test if the device is alive, device sends back the message in Success response
* @start * @start

View File

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

View File

@ -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, STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorconfig_init_obj, 0, 1,
mod_trezorconfig_init); 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 /// Attempts to unlock the storage with the given PIN and external salt.
/// success, False on failure. /// 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); 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_false;
} }
return mp_const_true; 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); 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) { STATIC mp_obj_t mod_trezorconfig_check_pin(mp_obj_t pin, mp_obj_t ext_salt) {
return mod_trezorconfig_unlock(pin); 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); mod_trezorconfig_check_pin);
/// def lock() -> None: /// 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, STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorconfig_get_pin_rem_obj,
mod_trezorconfig_get_pin_rem); 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) { STATIC mp_obj_t mod_trezorconfig_change_pin(size_t n_args,
uint32_t pin_i = trezor_obj_get_uint(pin); const mp_obj_t *args) {
uint32_t newpin_i = trezor_obj_get_uint(newpin); uint32_t oldpin = trezor_obj_get_uint(args[0]);
if (sectrue != storage_change_pin(pin_i, newpin_i)) { 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_false;
} }
return mp_const_true; return mp_const_true;
} }
STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorconfig_change_pin_obj, STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorconfig_change_pin_obj, 4,
mod_trezorconfig_change_pin); 4, mod_trezorconfig_change_pin);
/// def get(app: int, key: int, public: bool = False) -> Optional[bytes]: /// def get(app: int, key: int, public: bool = False) -> Optional[bytes]:
/// """ /// """

View File

@ -12,17 +12,18 @@ def init(
# extmod/modtrezorconfig/modtrezorconfig.c # 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 Attempts to unlock the storage with the given PIN and external salt.
success, False on failure. Returns True on success, False on failure.
""" """
# extmod/modtrezorconfig/modtrezorconfig.c # 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 # 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.
""" """

View File

@ -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.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__: if __debug__:
from apps.debug import input_signal from apps.debug import input_signal
@ -9,6 +21,10 @@ class PinCancelled(Exception):
pass pass
class PinInvalid(Exception):
pass
async def request_pin( async def request_pin(
prompt: str = "Enter your PIN", prompt: str = "Enter your PIN",
attempts_remaining: int = None, attempts_remaining: int = None,
@ -31,3 +47,68 @@ async def request_pin(
if result is CANCELLED: if result is CANCELLED:
raise PinCancelled raise PinCancelled
return result 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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ from ubinascii import hexlify
from trezor.crypto import random from trezor.crypto import random
from apps.common.sd_salt import SD_SALT_AUTH_KEY_LEN_BYTES
from apps.common.storage import common from apps.common.storage import common
if False: if False:
@ -31,6 +32,7 @@ _MNEMONIC_TYPE = const(0x0E) # int
_ROTATION = const(0x0F) # int _ROTATION = const(0x0F) # int
_SLIP39_IDENTIFIER = const(0x10) # bool _SLIP39_IDENTIFIER = const(0x10) # bool
_SLIP39_ITERATION_EXPONENT = const(0x11) # int _SLIP39_ITERATION_EXPONENT = const(0x11) # int
_SD_SALT_AUTH_KEY = const(0x12) # bytes
# fmt: on # fmt: on
HOMESCREEN_MAXSIZE = 16384 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. The device's actual SLIP-39 iteration exponent used in passphrase derivation.
""" """
return common.get_uint8(_NAMESPACE, _SLIP39_ITERATION_EXPONENT) 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)

View File

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

View File

@ -1,34 +1,26 @@
from trezor import config, ui, wire 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.messages.Success import Success
from trezor.pin import pin_to_int from trezor.pin import pin_to_int
from trezor.ui.popup import Popup
from trezor.ui.text import Text from trezor.ui.text import Text
from apps.common.confirm import require_confirm 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: if False:
from typing import Any
from trezor.messages.ChangePin import ChangePin from trezor.messages.ChangePin import ChangePin
async def change_pin(ctx: wire.Context, msg: ChangePin) -> Success: async def change_pin(ctx: wire.Context, msg: ChangePin) -> Success:
# confirm that user wants to change the pin # confirm that user wants to change the pin
await require_confirm_change_pin(ctx, msg) await require_confirm_change_pin(ctx, msg)
# get current pin, return failure if invalid # get old pin
if config.has_pin(): curpin, salt = await request_pin_and_sd_salt(ctx, "Enter old PIN")
curpin = await request_pin_ack(ctx, "Enter old PIN", config.get_pin_rem())
# if removing, defer check to change_pin() # if changing pin, pre-check the entered pin before getting new pin
if not msg.remove: if curpin and not msg.remove:
if not config.check_pin(pin_to_int(curpin)): if not config.check_pin(pin_to_int(curpin), salt):
raise wire.PinInvalid("PIN invalid") raise wire.PinInvalid("PIN invalid")
else:
curpin = ""
# get new pin # get new pin
if not msg.remove: if not msg.remove:
@ -37,7 +29,7 @@ async def change_pin(ctx: wire.Context, msg: ChangePin) -> Success:
newpin = "" newpin = ""
# write into storage # 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") raise wire.PinInvalid("PIN invalid")
if newpin: 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.normal("Do you really want to")
text.bold("enable PIN protection?") text.bold("enable PIN protection?")
return require_confirm(ctx, text) 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

View File

@ -55,6 +55,6 @@ async def load_device(ctx, msg):
use_passphrase=msg.passphrase_protection, label=msg.label use_passphrase=msg.passphrase_protection, label=msg.label
) )
if msg.pin: 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") return Success(message="Device loaded")

View File

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

View File

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

View File

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

View File

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

View File

@ -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 trezor.pin import pin_to_int, show_pin_timeout
from apps.common import storage from apps.common import storage
from apps.common.request_pin import request_pin 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: async def bootscreen() -> None:
ui.display.orientation(storage.device.get_rotation()) ui.display.orientation(storage.device.get_rotation())
salt_auth_key = device.get_sd_salt_auth_key()
while True: while True:
try: 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(): if not config.has_pin():
config.unlock(pin_to_int("")) config.unlock(pin_to_int(""), salt)
storage.init_unlocked() storage.init_unlocked()
return return
await lockscreen()
label = "Enter your PIN" label = "Enter your PIN"
while True: while True:
pin = await request_pin(label, config.get_pin_rem()) 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() storage.init_unlocked()
return return
else: else:
@ -55,6 +72,19 @@ async def lockscreen() -> None:
await ui.click() 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.display.backlight(ui.BACKLIGHT_NONE)
ui.backlight_fade(ui.BACKLIGHT_NORMAL) ui.backlight_fade(ui.BACKLIGHT_NORMAL)
config.init(show_pin_timeout) config.init(show_pin_timeout)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,7 +27,7 @@ class TestConfig(unittest.TestCase):
def test_wipe(self): def test_wipe(self):
config.init() config.init()
config.wipe() 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, 1, b'hello')
config.set(1, 2, b'world') config.set(1, 2, b'world')
v0 = config.get(1, 1) v0 = config.get(1, 1)
@ -44,7 +44,7 @@ class TestConfig(unittest.TestCase):
for _ in range(128): for _ in range(128):
config.init() config.init()
config.wipe() config.wipe()
self.assertEqual(config.unlock(pin_to_int('')), True) self.assertEqual(config.unlock(pin_to_int(''), None), True)
appid, key = random_entry() appid, key = random_entry()
value = random.bytes(16) value = random.bytes(16)
config.set(appid, key, value) config.set(appid, key, value)
@ -58,7 +58,7 @@ class TestConfig(unittest.TestCase):
def test_public(self): def test_public(self):
config.init() config.init()
config.wipe() config.wipe()
self.assertEqual(config.unlock(pin_to_int('')), True) self.assertEqual(config.unlock(pin_to_int(''), None), True)
appid, key = random_entry() appid, key = random_entry()
@ -84,25 +84,59 @@ class TestConfig(unittest.TestCase):
def test_change_pin(self): def test_change_pin(self):
config.init() config.init()
config.wipe() config.wipe()
self.assertEqual(config.unlock(pin_to_int('')), True) self.assertEqual(config.unlock(pin_to_int(''), None), True)
with self.assertRaises(RuntimeError): with self.assertRaises(RuntimeError):
config.set(PINAPP, PINKEY, b'value') 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('000'), pin_to_int('666'), None, None), False)
self.assertEqual(config.change_pin(pin_to_int(''), pin_to_int('000')), True) self.assertEqual(config.change_pin(pin_to_int(''), pin_to_int('000'), None, None), True)
self.assertEqual(config.get(PINAPP, PINKEY), None) self.assertEqual(config.get(PINAPP, PINKEY), None)
config.set(1, 1, b'value') config.set(1, 1, b'value')
config.init() config.init()
self.assertEqual(config.unlock(pin_to_int('000')), True) self.assertEqual(config.unlock(pin_to_int('000'), None), True)
config.change_pin(pin_to_int('000'), pin_to_int('')) config.change_pin(pin_to_int('000'), pin_to_int(''), None, None)
config.init() config.init()
self.assertEqual(config.unlock(pin_to_int('000')), False) self.assertEqual(config.unlock(pin_to_int('000'), None), False)
self.assertEqual(config.unlock(pin_to_int('')), True) 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') self.assertEqual(config.get(1, 1), b'value')
def test_set_get(self): def test_set_get(self):
config.init() config.init()
config.wipe() config.wipe()
self.assertEqual(config.unlock(pin_to_int('')), True) self.assertEqual(config.unlock(pin_to_int(''), None), True)
for _ in range(32): for _ in range(32):
appid, key = random_entry() appid, key = random_entry()
value = random.bytes(128) value = random.bytes(128)
@ -113,7 +147,7 @@ class TestConfig(unittest.TestCase):
def test_compact(self): def test_compact(self):
config.init() config.init()
config.wipe() config.wipe()
self.assertEqual(config.unlock(pin_to_int('')), True) self.assertEqual(config.unlock(pin_to_int(''), None), True)
appid, key = 1, 1 appid, key = 1, 1
for _ in range(259): for _ in range(259):
value = random.bytes(259) value = random.bytes(259)
@ -124,7 +158,7 @@ class TestConfig(unittest.TestCase):
def test_get_default(self): def test_get_default(self):
config.init() config.init()
config.wipe() config.wipe()
self.assertEqual(config.unlock(pin_to_int('')), True) self.assertEqual(config.unlock(pin_to_int(''), None), True)
for _ in range(128): for _ in range(128):
appid, key = random_entry() appid, key = random_entry()
value = config.get(appid, key) value = config.get(appid, key)

View File

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

View File

@ -2,7 +2,7 @@ ifneq ($(V),1)
Q := @ Q := @
endif 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) ifeq ($(BITCOIN_ONLY), 1)
SKIPPED_MESSAGES += Ethereum Lisk NEM Stellar SKIPPED_MESSAGES += Ethereum Lisk NEM Stellar

View File

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

View File

@ -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): class UnderscoreAgnosticGroup(click.Group):
"""Command group that normalizes dashes and underscores. """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.option("-r", "--remove", is_flag=True)
@click.pass_obj @click.pass_obj
def change_pin(connect, remove): def change_pin(connect, remove):
return device.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.") @cli.command(help="Enable passphrase.")
@click.pass_obj @click.pass_obj
def enable_passphrase(connect): def enable_passphrase(connect):

View File

@ -90,6 +90,12 @@ def change_pin(client, remove=False):
return ret 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") @expect(proto.Success, field="message")
def set_u2f_counter(client, u2f_counter): def set_u2f_counter(client, u2f_counter):
ret = client.call(proto.SetU2FCounter(u2f_counter=u2f_counter)) ret = client.call(proto.SetU2FCounter(u2f_counter=u2f_counter))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,6 +24,9 @@
#include <stdint.h> #include <stdint.h>
#include "secbool.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, typedef secbool (*PIN_UI_WAIT_CALLBACK)(uint32_t wait, uint32_t progress,
const char *message); const char *message);
@ -32,11 +35,13 @@ void storage_init(PIN_UI_WAIT_CALLBACK callback, const uint8_t *salt,
void storage_wipe(void); void storage_wipe(void);
secbool storage_is_unlocked(void); secbool storage_is_unlocked(void);
void storage_lock(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_has_pin(void);
secbool storage_pin_fails_increase(void); secbool storage_pin_fails_increase(void);
uint32_t storage_get_pin_rem(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, secbool storage_get(const uint16_t key, void *val, const uint16_t max_len,
uint16_t *len); uint16_t *len);
secbool storage_set(const uint16_t key, const void *val, uint16_t len); secbool storage_set(const uint16_t key, const void *val, uint16_t len);

Binary file not shown.

View File

@ -1,6 +1,7 @@
import ctypes as c import ctypes as c
import os import os
EXTERNAL_SALT_LEN = 32
sectrue = -1431655766 # 0xAAAAAAAAA sectrue = -1431655766 # 0xAAAAAAAAA
fname = os.path.join(os.path.dirname(__file__), "libtrezor-storage.so") fname = os.path.join(os.path.dirname(__file__), "libtrezor-storage.so")
@ -20,8 +21,10 @@ class Storage:
def wipe(self) -> None: def wipe(self) -> None:
self.lib.storage_wipe() self.lib.storage_wipe()
def unlock(self, pin: int) -> bool: def unlock(self, pin: int, ext_salt: bytes = None) -> bool:
return sectrue == self.lib.storage_unlock(c.c_uint32(pin)) 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: def lock(self) -> None:
self.lib.storage_lock() self.lib.storage_lock()
@ -32,9 +35,19 @@ class Storage:
def get_pin_rem(self) -> int: def get_pin_rem(self) -> int:
return self.lib.storage_get_pin_rem() 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( 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: def get(self, key: int) -> bytes:

View File

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