diff --git a/common/protob/messages-management.proto b/common/protob/messages-management.proto index 58c5dbd3d..83d99b057 100644 --- a/common/protob/messages-management.proto +++ b/common/protob/messages-management.proto @@ -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 diff --git a/common/protob/messages.proto b/common/protob/messages.proto index 0ad3c5dae..4bfa3e941 100644 --- a/common/protob/messages.proto +++ b/common/protob/messages.proto @@ -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]; diff --git a/core/embed/extmod/modtrezorconfig/modtrezorconfig.c b/core/embed/extmod/modtrezorconfig/modtrezorconfig.c index 43572e78b..b8755a409 100644 --- a/core/embed/extmod/modtrezorconfig/modtrezorconfig.c +++ b/core/embed/extmod/modtrezorconfig/modtrezorconfig.c @@ -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]: /// """ diff --git a/core/mocks/generated/trezorconfig.pyi b/core/mocks/generated/trezorconfig.pyi index 399ea4d95..5b8645b9b 100644 --- a/core/mocks/generated/trezorconfig.pyi +++ b/core/mocks/generated/trezorconfig.pyi @@ -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. """ diff --git a/core/src/apps/common/request_pin.py b/core/src/apps/common/request_pin.py index 7c6ae4cb6..999d1dddc 100644 --- a/core/src/apps/common/request_pin.py +++ b/core/src/apps/common/request_pin.py @@ -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 diff --git a/core/src/apps/common/sd_salt.py b/core/src/apps/common/sd_salt.py new file mode 100644 index 000000000..94b81fa81 --- /dev/null +++ b/core/src/apps/common/sd_salt.py @@ -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) diff --git a/core/src/apps/common/storage/common.py b/core/src/apps/common/storage/common.py index af188f118..f00c3ea36 100644 --- a/core/src/apps/common/storage/common.py +++ b/core/src/apps/common/storage/common.py @@ -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: diff --git a/core/src/apps/common/storage/device.py b/core/src/apps/common/storage/device.py index 693e82579..a9104db86 100644 --- a/core/src/apps/common/storage/device.py +++ b/core/src/apps/common/storage/device.py @@ -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) diff --git a/core/src/apps/management/__init__.py b/core/src/apps/management/__init__.py index e1bbbc386..51eb80ced 100644 --- a/core/src/apps/management/__init__.py +++ b/core/src/apps/management/__init__.py @@ -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") diff --git a/core/src/apps/management/change_pin.py b/core/src/apps/management/change_pin.py index 793eef62a..90d0a533e 100644 --- a/core/src/apps/management/change_pin.py +++ b/core/src/apps/management/change_pin.py @@ -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 diff --git a/core/src/apps/management/load_device.py b/core/src/apps/management/load_device.py index 7fabba906..b7b8aeeb7 100644 --- a/core/src/apps/management/load_device.py +++ b/core/src/apps/management/load_device.py @@ -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") diff --git a/core/src/apps/management/recovery_device/__init__.py b/core/src/apps/management/recovery_device/__init__.py index fa13558ed..09b3e1817 100644 --- a/core/src/apps/management/recovery_device/__init__.py +++ b/core/src/apps/management/recovery_device/__init__.py @@ -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) diff --git a/core/src/apps/management/reset_device.py b/core/src/apps/management/reset_device.py index 7770b7234..5c5d05930 100644 --- a/core/src/apps/management/reset_device.py +++ b/core/src/apps/management/reset_device.py @@ -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 diff --git a/core/src/apps/management/sd_protect.py b/core/src/apps/management/sd_protect.py new file mode 100644 index 000000000..4d980ed9b --- /dev/null +++ b/core/src/apps/management/sd_protect.py @@ -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) diff --git a/core/src/apps/webauthn/__init__.py b/core/src/apps/webauthn/__init__.py index 71a7efff0..ce66aa61b 100644 --- a/core/src/apps/webauthn/__init__.py +++ b/core/src/apps/webauthn/__init__.py @@ -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: diff --git a/core/src/boot.py b/core/src/boot.py index f6c414482..a9f48d6ad 100644 --- a/core/src/boot.py +++ b/core/src/boot.py @@ -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) diff --git a/core/src/trezor/messages/MessageType.py b/core/src/trezor/messages/MessageType.py index a4fb1eb4a..9c7223d7c 100644 --- a/core/src/trezor/messages/MessageType.py +++ b/core/src/trezor/messages/MessageType.py @@ -33,6 +33,7 @@ WordRequest = 46 WordAck = 47 GetFeatures = 55 SetU2FCounter = 63 +SdProtect = 79 FirmwareErase = 6 FirmwareUpload = 7 FirmwareRequest = 8 diff --git a/core/src/trezor/messages/SdProtect.py b/core/src/trezor/messages/SdProtect.py new file mode 100644 index 000000000..38fc0c62c --- /dev/null +++ b/core/src/trezor/messages/SdProtect.py @@ -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), + } diff --git a/core/src/trezor/messages/SdProtectOperationType.py b/core/src/trezor/messages/SdProtectOperationType.py new file mode 100644 index 000000000..c3960723b --- /dev/null +++ b/core/src/trezor/messages/SdProtectOperationType.py @@ -0,0 +1,5 @@ +# Automatically generated by pb2py +# fmt: off +DISABLE = 0 +ENABLE = 1 +REFRESH = 2 diff --git a/core/src/trezor/ui/pin.py b/core/src/trezor/ui/pin.py index a69cc0526..d25b1136b 100644 --- a/core/src/trezor/ui/pin.py +++ b/core/src/trezor/ui/pin.py @@ -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, "") diff --git a/core/tests/run_tests_device_emu.sh b/core/tests/run_tests_device_emu.sh index f214a1b63..e810d73fd 100755 --- a/core/tests/run_tests_device_emu.sh +++ b/core/tests/run_tests_device_emu.sh @@ -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 diff --git a/core/tests/run_tests_device_emu_monero.sh b/core/tests/run_tests_device_emu_monero.sh index feb8d67eb..714dd3bb6 100755 --- a/core/tests/run_tests_device_emu_monero.sh +++ b/core/tests/run_tests_device_emu_monero.sh @@ -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="" diff --git a/core/tests/run_tests_device_emu_u2f.sh b/core/tests/run_tests_device_emu_u2f.sh index 20cf25bd7..d7f7f7296 100755 --- a/core/tests/run_tests_device_emu_u2f.sh +++ b/core/tests/run_tests_device_emu_u2f.sh @@ -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 diff --git a/core/tests/test_trezor.config.py b/core/tests/test_trezor.config.py index 416ec3059..a6879996d 100644 --- a/core/tests/test_trezor.config.py +++ b/core/tests/test_trezor.config.py @@ -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) diff --git a/legacy/firmware/config.c b/legacy/firmware/config.c index 16a206d46..6272cff0c 100644 --- a/legacy/firmware/config.c +++ b/legacy/firmware/config.c @@ -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)); diff --git a/legacy/firmware/protob/Makefile b/legacy/firmware/protob/Makefile index 1608b327a..52f91a085 100644 --- a/legacy/firmware/protob/Makefile +++ b/legacy/firmware/protob/Makefile @@ -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 diff --git a/python/docs/OPTIONS.rst b/python/docs/OPTIONS.rst index 54f2a388a..cb3a4dfd1 100644 --- a/python/docs/OPTIONS.rst +++ b/python/docs/OPTIONS.rst @@ -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. diff --git a/python/src/trezorlib/cli/trezorctl.py b/python/src/trezorlib/cli/trezorctl.py index a78893d3e..c23cd1582 100755 --- a/python/src/trezorlib/cli/trezorctl.py +++ b/python/src/trezorlib/cli/trezorctl.py @@ -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): diff --git a/python/src/trezorlib/device.py b/python/src/trezorlib/device.py index 0e4355bb0..68cddfada 100644 --- a/python/src/trezorlib/device.py +++ b/python/src/trezorlib/device.py @@ -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)) diff --git a/python/src/trezorlib/messages/MessageType.py b/python/src/trezorlib/messages/MessageType.py index fab7070f0..75f0ccfe6 100644 --- a/python/src/trezorlib/messages/MessageType.py +++ b/python/src/trezorlib/messages/MessageType.py @@ -31,6 +31,7 @@ WordRequest = 46 WordAck = 47 GetFeatures = 55 SetU2FCounter = 63 +SdProtect = 79 FirmwareErase = 6 FirmwareUpload = 7 FirmwareRequest = 8 diff --git a/python/src/trezorlib/messages/SdProtect.py b/python/src/trezorlib/messages/SdProtect.py new file mode 100644 index 000000000..48493f952 --- /dev/null +++ b/python/src/trezorlib/messages/SdProtect.py @@ -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), + } diff --git a/python/src/trezorlib/messages/SdProtectOperationType.py b/python/src/trezorlib/messages/SdProtectOperationType.py new file mode 100644 index 000000000..c3960723b --- /dev/null +++ b/python/src/trezorlib/messages/SdProtectOperationType.py @@ -0,0 +1,5 @@ +# Automatically generated by pb2py +# fmt: off +DISABLE = 0 +ENABLE = 1 +REFRESH = 2 diff --git a/python/src/trezorlib/messages/__init__.py b/python/src/trezorlib/messages/__init__.py index 1310b657b..5019c2bf5 100644 --- a/python/src/trezorlib/messages/__init__.py +++ b/python/src/trezorlib/messages/__init__.py @@ -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 diff --git a/storage/storage.c b/storage/storage.c index 31c10898b..50ef07e01 100644 --- a/storage/storage.c +++ b/storage/storage.c @@ -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. diff --git a/storage/storage.h b/storage/storage.h index 2abb409cc..2a8b22375 100644 --- a/storage/storage.h +++ b/storage/storage.h @@ -24,6 +24,9 @@ #include #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); diff --git a/storage/tests/c/libtrezor-storage.so b/storage/tests/c/libtrezor-storage.so index 83aa07284..64898cc47 100755 Binary files a/storage/tests/c/libtrezor-storage.so and b/storage/tests/c/libtrezor-storage.so differ diff --git a/storage/tests/c/storage.py b/storage/tests/c/storage.py index d446b0dcf..51ba59498 100644 --- a/storage/tests/c/storage.py +++ b/storage/tests/c/storage.py @@ -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: diff --git a/storage/tests/c0/libtrezor-storage0.so b/storage/tests/c0/libtrezor-storage0.so index e03da87a7..3504b9d60 100755 Binary files a/storage/tests/c0/libtrezor-storage0.so and b/storage/tests/c0/libtrezor-storage0.so differ diff --git a/tests/device_tests/test_msg_sd_protect.py b/tests/device_tests/test_msg_sd_protect.py new file mode 100644 index 000000000..943d968fc --- /dev/null +++ b/tests/device_tests/test_msg_sd_protect.py @@ -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 . + +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)