From 4381511930e47916831a55b5f9f39afef67ab1e9 Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Fri, 18 Oct 2019 15:28:53 +0200 Subject: [PATCH 01/14] common: Add ChangeWipeCode message. --- common/protob/messages-common.proto | 3 +++ common/protob/messages-management.proto | 10 +++++++ common/protob/messages.proto | 1 + core/src/trezor/messages/ChangeWipeCode.py | 26 +++++++++++++++++++ core/src/trezor/messages/Failure.py | 4 +-- core/src/trezor/messages/FailureType.py | 1 + core/src/trezor/messages/MessageType.py | 1 + core/src/trezor/wire/errors.py | 5 ++++ .../src/trezorlib/messages/ChangeWipeCode.py | 26 +++++++++++++++++++ python/src/trezorlib/messages/Failure.py | 4 +-- python/src/trezorlib/messages/FailureType.py | 1 + python/src/trezorlib/messages/MessageType.py | 1 + .../trezorlib/messages/PinMatrixRequest.py | 4 +-- .../messages/PinMatrixRequestType.py | 2 ++ python/src/trezorlib/messages/__init__.py | 1 + 15 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 core/src/trezor/messages/ChangeWipeCode.py create mode 100644 python/src/trezorlib/messages/ChangeWipeCode.py diff --git a/common/protob/messages-common.proto b/common/protob/messages-common.proto index 1c121ae24..cb89d8a9a 100644 --- a/common/protob/messages-common.proto +++ b/common/protob/messages-common.proto @@ -33,6 +33,7 @@ message Failure { Failure_NotEnoughFunds = 10; Failure_NotInitialized = 11; Failure_PinMismatch = 12; + Failure_WipeCodeMismatch = 13; Failure_FirmwareError = 99; } } @@ -90,6 +91,8 @@ message PinMatrixRequest { PinMatrixRequestType_Current = 1; PinMatrixRequestType_NewFirst = 2; PinMatrixRequestType_NewSecond = 3; + PinMatrixRequestType_WipeCodeFirst = 4; + PinMatrixRequestType_WipeCodeSecond = 5; } } diff --git a/common/protob/messages-management.proto b/common/protob/messages-management.proto index 129493828..2e4caa981 100644 --- a/common/protob/messages-management.proto +++ b/common/protob/messages-management.proto @@ -141,6 +141,16 @@ message ChangePin { optional bool remove = 1; // is PIN removal requested? } +/** + * Request: Starts workflow for setting/removing the wipe code + * @start + * @next Success + * @next Failure + */ +message ChangeWipeCode { + optional bool remove = 1; // is wipe code removal requested? +} + /** * Request: Starts workflow for enabling/regenerating/disabling SD card protection * @start diff --git a/common/protob/messages.proto b/common/protob/messages.proto index 26117c364..9da9a70c6 100644 --- a/common/protob/messages.proto +++ b/common/protob/messages.proto @@ -64,6 +64,7 @@ enum MessageType { MessageType_SdProtect = 79 [(wire_in) = true]; MessageType_GetNextU2FCounter = 80 [(wire_in) = true]; MessageType_NextU2FCounter = 81 [(wire_out) = true]; + MessageType_ChangeWipeCode = 82 [(wire_in) = true]; // Bootloader MessageType_FirmwareErase = 6 [(wire_in) = true, (wire_bootloader) = true]; diff --git a/core/src/trezor/messages/ChangeWipeCode.py b/core/src/trezor/messages/ChangeWipeCode.py new file mode 100644 index 000000000..0d1285fc4 --- /dev/null +++ b/core/src/trezor/messages/ChangeWipeCode.py @@ -0,0 +1,26 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class ChangeWipeCode(p.MessageType): + MESSAGE_WIRE_TYPE = 82 + + def __init__( + self, + remove: bool = None, + ) -> None: + self.remove = remove + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('remove', p.BoolType, 0), + } diff --git a/core/src/trezor/messages/Failure.py b/core/src/trezor/messages/Failure.py index eeec1d7c7..7c1d37dde 100644 --- a/core/src/trezor/messages/Failure.py +++ b/core/src/trezor/messages/Failure.py @@ -6,7 +6,7 @@ if __debug__: try: from typing import Dict, List # noqa: F401 from typing_extensions import Literal # noqa: F401 - EnumTypeFailureType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 99] + EnumTypeFailureType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 99] except ImportError: pass @@ -25,6 +25,6 @@ class Failure(p.MessageType): @classmethod def get_fields(cls) -> Dict: return { - 1: ('code', p.EnumType("FailureType", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 99)), 0), + 1: ('code', p.EnumType("FailureType", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 99)), 0), 2: ('message', p.UnicodeType, 0), } diff --git a/core/src/trezor/messages/FailureType.py b/core/src/trezor/messages/FailureType.py index 1e5c8b6e7..f94652b9e 100644 --- a/core/src/trezor/messages/FailureType.py +++ b/core/src/trezor/messages/FailureType.py @@ -15,4 +15,5 @@ ProcessError = 9 # type: Literal[9] NotEnoughFunds = 10 # type: Literal[10] NotInitialized = 11 # type: Literal[11] PinMismatch = 12 # type: Literal[12] +WipeCodeMismatch = 13 # type: Literal[13] FirmwareError = 99 # type: Literal[99] diff --git a/core/src/trezor/messages/MessageType.py b/core/src/trezor/messages/MessageType.py index 6bc5f8533..f9994d83f 100644 --- a/core/src/trezor/messages/MessageType.py +++ b/core/src/trezor/messages/MessageType.py @@ -39,6 +39,7 @@ SetU2FCounter = 63 # type: Literal[63] SdProtect = 79 # type: Literal[79] GetNextU2FCounter = 80 # type: Literal[80] NextU2FCounter = 81 # type: Literal[81] +ChangeWipeCode = 82 # type: Literal[82] FirmwareErase = 6 # type: Literal[6] FirmwareUpload = 7 # type: Literal[7] FirmwareRequest = 8 # type: Literal[8] diff --git a/core/src/trezor/wire/errors.py b/core/src/trezor/wire/errors.py index cc4f490b0..1211536e8 100644 --- a/core/src/trezor/wire/errors.py +++ b/core/src/trezor/wire/errors.py @@ -71,6 +71,11 @@ class PinMismatch(Error): super().__init__(FailureType.PinMismatch, message) +class WipeCodeMismatch(Error): + def __init__(self, message: str) -> None: + super().__init__(FailureType.WipeCodeMismatch, message) + + class FirmwareError(Error): def __init__(self, message: str) -> None: super().__init__(FailureType.FirmwareError, message) diff --git a/python/src/trezorlib/messages/ChangeWipeCode.py b/python/src/trezorlib/messages/ChangeWipeCode.py new file mode 100644 index 000000000..733217c83 --- /dev/null +++ b/python/src/trezorlib/messages/ChangeWipeCode.py @@ -0,0 +1,26 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class ChangeWipeCode(p.MessageType): + MESSAGE_WIRE_TYPE = 82 + + def __init__( + self, + remove: bool = None, + ) -> None: + self.remove = remove + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('remove', p.BoolType, 0), + } diff --git a/python/src/trezorlib/messages/Failure.py b/python/src/trezorlib/messages/Failure.py index 6157cffa0..c6c2dd066 100644 --- a/python/src/trezorlib/messages/Failure.py +++ b/python/src/trezorlib/messages/Failure.py @@ -6,7 +6,7 @@ if __debug__: try: from typing import Dict, List # noqa: F401 from typing_extensions import Literal # noqa: F401 - EnumTypeFailureType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 99] + EnumTypeFailureType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 99] except ImportError: pass @@ -25,6 +25,6 @@ class Failure(p.MessageType): @classmethod def get_fields(cls) -> Dict: return { - 1: ('code', p.EnumType("FailureType", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 99)), 0), + 1: ('code', p.EnumType("FailureType", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 99)), 0), 2: ('message', p.UnicodeType, 0), } diff --git a/python/src/trezorlib/messages/FailureType.py b/python/src/trezorlib/messages/FailureType.py index 1e5c8b6e7..f94652b9e 100644 --- a/python/src/trezorlib/messages/FailureType.py +++ b/python/src/trezorlib/messages/FailureType.py @@ -15,4 +15,5 @@ ProcessError = 9 # type: Literal[9] NotEnoughFunds = 10 # type: Literal[10] NotInitialized = 11 # type: Literal[11] PinMismatch = 12 # type: Literal[12] +WipeCodeMismatch = 13 # type: Literal[13] FirmwareError = 99 # type: Literal[99] diff --git a/python/src/trezorlib/messages/MessageType.py b/python/src/trezorlib/messages/MessageType.py index a01db19b5..0d8a6baf9 100644 --- a/python/src/trezorlib/messages/MessageType.py +++ b/python/src/trezorlib/messages/MessageType.py @@ -37,6 +37,7 @@ SetU2FCounter = 63 # type: Literal[63] SdProtect = 79 # type: Literal[79] GetNextU2FCounter = 80 # type: Literal[80] NextU2FCounter = 81 # type: Literal[81] +ChangeWipeCode = 82 # type: Literal[82] FirmwareErase = 6 # type: Literal[6] FirmwareUpload = 7 # type: Literal[7] FirmwareRequest = 8 # type: Literal[8] diff --git a/python/src/trezorlib/messages/PinMatrixRequest.py b/python/src/trezorlib/messages/PinMatrixRequest.py index a37241e67..12882e128 100644 --- a/python/src/trezorlib/messages/PinMatrixRequest.py +++ b/python/src/trezorlib/messages/PinMatrixRequest.py @@ -6,7 +6,7 @@ if __debug__: try: from typing import Dict, List # noqa: F401 from typing_extensions import Literal # noqa: F401 - EnumTypePinMatrixRequestType = Literal[1, 2, 3] + EnumTypePinMatrixRequestType = Literal[1, 2, 3, 4, 5] except ImportError: pass @@ -23,5 +23,5 @@ class PinMatrixRequest(p.MessageType): @classmethod def get_fields(cls) -> Dict: return { - 1: ('type', p.EnumType("PinMatrixRequestType", (1, 2, 3)), 0), + 1: ('type', p.EnumType("PinMatrixRequestType", (1, 2, 3, 4, 5)), 0), } diff --git a/python/src/trezorlib/messages/PinMatrixRequestType.py b/python/src/trezorlib/messages/PinMatrixRequestType.py index ca6535e78..ec3c6a38d 100644 --- a/python/src/trezorlib/messages/PinMatrixRequestType.py +++ b/python/src/trezorlib/messages/PinMatrixRequestType.py @@ -6,3 +6,5 @@ if False: Current = 1 # type: Literal[1] NewFirst = 2 # type: Literal[2] NewSecond = 3 # type: Literal[3] +WipeCodeFirst = 4 # type: Literal[4] +WipeCodeSecond = 5 # type: Literal[5] diff --git a/python/src/trezorlib/messages/__init__.py b/python/src/trezorlib/messages/__init__.py index 2da185bb0..e217ee0ad 100644 --- a/python/src/trezorlib/messages/__init__.py +++ b/python/src/trezorlib/messages/__init__.py @@ -31,6 +31,7 @@ from .CardanoTxInputType import CardanoTxInputType from .CardanoTxOutputType import CardanoTxOutputType from .CardanoTxRequest import CardanoTxRequest from .ChangePin import ChangePin +from .ChangeWipeCode import ChangeWipeCode from .CipherKeyValue import CipherKeyValue from .CipheredKeyValue import CipheredKeyValue from .ClearSession import ClearSession From 83fab3c220ced8eb64155e0c64bd62a4d69bf054 Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Fri, 18 Oct 2019 15:59:12 +0200 Subject: [PATCH 02/14] trezorctl: Add set wipe-code command. --- python/docs/OPTIONS.rst | 1 + python/src/trezorlib/cli/settings.py | 13 +++++++++++++ python/src/trezorlib/device.py | 7 +++++++ python/src/trezorlib/ui.py | 8 +++++++- 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/python/docs/OPTIONS.rst b/python/docs/OPTIONS.rst index 67e44f44c..85f3a7dd8 100644 --- a/python/docs/OPTIONS.rst +++ b/python/docs/OPTIONS.rst @@ -362,6 +362,7 @@ Device settings. label Set new device label. passphrase Enable, disable or configure passphrase protection. pin Set, change or remove PIN. + wipe-code Set or remove the wipe code. Stellar commands. ~~~~~~~~~~~~~~~~~ diff --git a/python/src/trezorlib/cli/settings.py b/python/src/trezorlib/cli/settings.py index bdc5d598b..2bf72cc38 100644 --- a/python/src/trezorlib/cli/settings.py +++ b/python/src/trezorlib/cli/settings.py @@ -41,6 +41,19 @@ def pin(connect, remove): return device.change_pin(connect(), remove) +@cli.command() +@click.option("-r", "--remove", is_flag=True) +@click.pass_obj +def wipe_code(connect, remove): + """Set or remove the wipe code. + + The wipe code functions as a "self-destruct PIN". If the wipe code is ever + entered into any PIN entry dialog, then all private data will be immediately + removed and the device will be reset to factory defaults. + """ + return device.change_wipe_code(connect(), remove) + + @cli.command() # keep the deprecated -l/--label option, make it do nothing @click.option("-l", "--label", "_ignore", is_flag=True, hidden=True, expose_value=False) diff --git a/python/src/trezorlib/device.py b/python/src/trezorlib/device.py index 51decd4e3..f95836137 100644 --- a/python/src/trezorlib/device.py +++ b/python/src/trezorlib/device.py @@ -90,6 +90,13 @@ def change_pin(client, remove=False): return ret +@expect(messages.Success, field="message") +def change_wipe_code(client, remove=False): + ret = client.call(messages.ChangeWipeCode(remove=remove)) + client.init_device() # Re-read features + return ret + + @expect(messages.Success, field="message") def sd_protect(client, operation): ret = client.call(messages.SdProtect(operation=operation)) diff --git a/python/src/trezorlib/ui.py b/python/src/trezorlib/ui.py index e95eb50e3..fc0f6e20f 100644 --- a/python/src/trezorlib/ui.py +++ b/python/src/trezorlib/ui.py @@ -45,6 +45,8 @@ PIN_GENERIC = None PIN_CURRENT = PinMatrixRequestType.Current PIN_NEW = PinMatrixRequestType.NewFirst PIN_CONFIRM = PinMatrixRequestType.NewSecond +WIPE_CODE_NEW = PinMatrixRequestType.WipeCodeFirst +WIPE_CODE_CONFIRM = PinMatrixRequestType.WipeCodeSecond def echo(*args, **kwargs): @@ -74,6 +76,10 @@ class ClickUI: desc = "new PIN" elif code == PIN_CONFIRM: desc = "new PIN again" + elif code == WIPE_CODE_NEW: + desc = "new wipe code" + elif code == WIPE_CODE_CONFIRM: + desc = "new wipe code again" else: desc = "PIN" @@ -88,7 +94,7 @@ class ClickUI: except click.Abort: raise Cancelled from None if not pin.isdigit(): - echo("Non-numerical PIN provided, please try again") + echo("Non-numerical value provided, please try again") else: return pin From a9b98ab966ea5d98c65ee6f26fa16556b1ef62df Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Thu, 31 Oct 2019 14:44:39 +0100 Subject: [PATCH 03/14] storage: Implement storage_change_wipe_code(). --- storage/storage.c | 191 ++++++++++++++++++++-- storage/storage.h | 8 +- storage/tests/c/norcow_config.h | 2 +- storage/tests/python/src/consts.py | 16 +- storage/tests/python/src/storage.py | 11 +- storage/tests/python/tests/test_norcow.py | 16 +- 6 files changed, 215 insertions(+), 29 deletions(-) diff --git a/storage/storage.c b/storage/storage.c index 5526262b4..4feeba9af 100644 --- a/storage/storage.c +++ b/storage/storage.c @@ -50,6 +50,9 @@ // Norcow storage key of the storage authentication tag. #define STORAGE_TAG_KEY ((APP_STORAGE << 8) | 0x05) +// Norcow storage key of the wipe code data. +#define WIPE_CODE_DATA_KEY ((APP_STORAGE << 8) | 0x06) + // The PIN value corresponding to an empty PIN. #define PIN_EMPTY 1 @@ -104,6 +107,24 @@ // The length of the ChaCha20 block in bytes. #define CHACHA20_BLOCK_SIZE 64 +// The length of the wipe code in bytes. +#define WIPE_CODE_SIZE (sizeof(uint32_t)) + +// The byte length of the salt used in checking the wipe code. +#define WIPE_CODE_SALT_SIZE 8 + +// The byte length of the tag used in checking the wipe code. +#define WIPE_CODE_TAG_SIZE 8 + +// The total length of the WIPE_CODE_DATA_KEY entry. +#define WIPE_CODE_DATA_SIZE \ + (WIPE_CODE_SIZE + WIPE_CODE_SALT_SIZE + WIPE_CODE_TAG_SIZE) + +// The value corresponding to an unconfigured wipe code. +// NOTE: This is intentionally different from PIN_EMPTY so that we don't need +// special handling when both the PIN and wipe code are not set. +#define WIPE_CODE_EMPTY 0 + // The length of the counter tail in words. #define COUNTER_TAIL_WORDS 2 @@ -323,6 +344,83 @@ static secbool auth_get(uint16_t key, const void **val, uint16_t *len) { return sectrue; } +static secbool set_wipe_code(uint32_t wipe_code) { + if (wipe_code == PIN_EMPTY) { + // This is to avoid having to check pin != PIN_EMPTY when checking the wipe + // code. + wipe_code = WIPE_CODE_EMPTY; + } + + // The format of the WIPE_CODE_DATA_KEY entry is: + // wipe code (4 bytes), random salt (16 bytes), authentication tag (16 bytes) + // NOTE: We allocate extra space for the HMAC computation. + uint8_t wipe_code_data[WIPE_CODE_SIZE + WIPE_CODE_SALT_SIZE + + SHA256_DIGEST_LENGTH] = {0}; + uint8_t *salt = wipe_code_data + WIPE_CODE_SIZE; + uint8_t *tag = wipe_code_data + WIPE_CODE_SIZE + WIPE_CODE_SALT_SIZE; + memcpy(wipe_code_data, &wipe_code, sizeof(wipe_code)); + memzero(&wipe_code, sizeof(wipe_code)); + random_buffer(salt, WIPE_CODE_SALT_SIZE); + hmac_sha256(salt, WIPE_CODE_SALT_SIZE, wipe_code_data, WIPE_CODE_SIZE, tag); + return norcow_set(WIPE_CODE_DATA_KEY, wipe_code_data, WIPE_CODE_DATA_SIZE); +} + +static secbool is_not_wipe_code(uint32_t pin) { + uint8_t wipe_code[WIPE_CODE_SIZE] = {0}; + uint8_t salt[WIPE_CODE_SALT_SIZE] = {0}; + uint8_t stored_tag[WIPE_CODE_TAG_SIZE] = {0}; + uint8_t computed_tag1[SHA256_DIGEST_LENGTH] = {0}; + uint8_t computed_tag2[SHA256_DIGEST_LENGTH] = {0}; + + // Read the wipe code data from the storage. + const void *wipe_code_data = NULL; + uint16_t len = 0; + if (sectrue != norcow_get(WIPE_CODE_DATA_KEY, &wipe_code_data, &len) || + len != WIPE_CODE_DATA_SIZE) { + handle_fault("no wipe code"); + return secfalse; + } + memcpy(wipe_code, wipe_code_data, sizeof(wipe_code)); + memcpy(salt, (uint8_t *)wipe_code_data + WIPE_CODE_SIZE, sizeof(salt)); + memcpy(stored_tag, + (uint8_t *)wipe_code_data + WIPE_CODE_SIZE + WIPE_CODE_SALT_SIZE, + sizeof(stored_tag)); + + // Check integrity in case of flash read manipulation attack. + hmac_sha256(salt, WIPE_CODE_SALT_SIZE, wipe_code, sizeof(wipe_code), + computed_tag1); + memzero(wipe_code, sizeof(wipe_code)); + if (sectrue != secequal(stored_tag, computed_tag1, sizeof(stored_tag))) { + handle_fault("wipe code tag"); + return secfalse; + } + + // Prepare the authentication tag of the entered PIN. + wait_random(); + hmac_sha256(salt, WIPE_CODE_SALT_SIZE, (const uint8_t *)&pin, WIPE_CODE_SIZE, + computed_tag1); + + // Recompute to check for fault injection attack. + wait_random(); + hmac_sha256(salt, WIPE_CODE_SALT_SIZE, (const uint8_t *)&pin, WIPE_CODE_SIZE, + computed_tag2); + memzero(salt, sizeof(salt)); + if (sectrue != + secequal(computed_tag1, computed_tag2, sizeof(computed_tag1))) { + handle_fault("wipe code fault"); + return secfalse; + } + memzero(&pin, sizeof(pin)); + + // Compare wipe code with the entered PIN via the authentication tag. + wait_random(); + if (secfalse != secequal(stored_tag, computed_tag1, sizeof(stored_tag))) { + return secfalse; + } + memzero(stored_tag, sizeof(stored_tag)); + return sectrue; +} + static void derive_kek(uint32_t pin, const uint8_t *random_salt, const uint8_t *ext_salt, uint8_t kek[SHA256_DIGEST_LENGTH], @@ -383,6 +481,13 @@ static void derive_kek(uint32_t pin, const uint8_t *random_salt, } static secbool set_pin(uint32_t pin, const uint8_t *ext_salt) { + // Fail if the PIN is the same as the wipe code. Ignore during upgrade. + if (norcow_active_version != 0 && sectrue != is_not_wipe_code(pin)) { + memzero(&pin, sizeof(pin)); + return secfalse; + } + + // Encrypt the cached keys using the new PIN and set the new PVC. uint8_t buffer[RANDOM_SALT_SIZE + KEYS_SIZE + POLY1305_TAG_SIZE] = {0}; uint8_t *rand_salt = buffer; uint8_t *ekeys = buffer + RANDOM_SALT_SIZE; @@ -517,6 +622,8 @@ static void init_wiped_storage(void) { ensure(storage_set_encrypted(VERSION_KEY, &version, sizeof(version)), "set_storage_version failed"); ensure(pin_logs_init(0), "init_pin_logs failed"); + ensure(set_wipe_code(WIPE_CODE_EMPTY), "set_wipe_code failed"); + ui_total = DERIVE_SECS; ui_rem = ui_total; ui_message = PROCESSING_MSG; @@ -738,6 +845,25 @@ void storage_lock(void) { memzero(authentication_sum, sizeof(authentication_sum)); } +secbool check_storage_version(void) { + uint32_t version = 0; + uint16_t len = 0; + if (sectrue != + storage_get_encrypted(VERSION_KEY, &version, sizeof(version), &len) || + len != sizeof(version)) { + handle_fault("storage version check"); + return secfalse; + } + + if (version > norcow_active_version) { + storage_wipe(); + } else if (version < norcow_active_version) { + storage_set_encrypted(VERSION_KEY, &norcow_active_version, + sizeof(norcow_active_version)); + } + return sectrue; +} + static secbool decrypt_dek(const uint8_t *kek, const uint8_t *keiv) { const void *buffer = NULL; uint16_t len = 0; @@ -776,17 +902,9 @@ static secbool decrypt_dek(const uint8_t *kek, const uint8_t *keiv) { memzero(tag, sizeof(tag)); // Check that the authenticated version number matches the norcow version. - // NOTE: storage_get_encrypted() calls auth_get(), which initializes the - // authentication_sum. - uint32_t version = 0; - if (sectrue != - storage_get_encrypted(VERSION_KEY, &version, sizeof(version), &len) || - len != sizeof(version) || version != norcow_active_version) { - handle_fault("storage version check"); - return secfalse; - } - - return sectrue; + // NOTE: This also initializes the authentication_sum by calling + // storage_get_encrypted() which calls auth_get(). + return check_storage_version(); } static secbool unlock(uint32_t pin, const uint8_t *ext_salt) { @@ -794,10 +912,16 @@ static secbool unlock(uint32_t pin, const uint8_t *ext_salt) { return secfalse; } + // Check whether the user entered the wipe code. + if (sectrue != is_not_wipe_code(pin)) { + storage_wipe(); + error_shutdown("You have entered the", "wipe code. All private", + "data has been erased.", NULL); + } + // Get the pin failure counter uint32_t ctr = 0; if (sectrue != pin_get_fails(&ctr)) { - memzero(&pin, sizeof(pin)); return secfalse; } @@ -837,7 +961,6 @@ static secbool unlock(uint32_t pin, const uint8_t *ext_salt) { if (sectrue != initialized || 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"); return secfalse; } @@ -860,7 +983,7 @@ static secbool unlock(uint32_t pin, const uint8_t *ext_salt) { return secfalse; } - // Check that the PIN was correct. + // Check whether the entered PIN is correct. if (sectrue != decrypt_dek(kek, keiv)) { // Wipe storage if too many failures wait_random(); @@ -1175,6 +1298,28 @@ secbool storage_change_pin(uint32_t oldpin, uint32_t newpin, return ret; } +secbool storage_change_wipe_code(uint32_t pin, const uint8_t *ext_salt, + uint32_t wipe_code) { + if (sectrue != initialized || (pin != PIN_EMPTY && pin == wipe_code)) { + memzero(&pin, sizeof(pin)); + memzero(&wipe_code, sizeof(wipe_code)); + return secfalse; + } + + ui_total = DERIVE_SECS; + ui_rem = ui_total; + ui_message = (pin != PIN_EMPTY && wipe_code == PIN_EMPTY) ? VERIFYING_PIN_MSG + : PROCESSING_MSG; + + secbool ret = secfalse; + if (sectrue == unlock(pin, ext_salt)) { + ret = set_wipe_code(wipe_code); + } + memzero(&pin, sizeof(pin)); + memzero(&wipe_code, sizeof(wipe_code)); + return ret; +} + void storage_wipe(void) { norcow_wipe(); norcow_active_version = NORCOW_VERSION; @@ -1253,6 +1398,7 @@ static secbool storage_upgrade(void) { uint16_t key = 0; uint16_t len = 0; const void *val = NULL; + secbool ret = secfalse; if (norcow_active_version == 0) { random_buffer(cached_keys, sizeof(cached_keys)); @@ -1261,7 +1407,7 @@ static secbool storage_upgrade(void) { auth_init(); // Set the new storage version number. - uint32_t version = NORCOW_VERSION; + uint32_t version = 1; if (sectrue != storage_set_encrypted(VERSION_KEY, &version, sizeof(version))) { return secfalse; @@ -1289,7 +1435,6 @@ static secbool storage_upgrade(void) { continue; } - secbool ret = secfalse; if (((key >> 8) & FLAG_PUBLIC) != 0) { ret = norcow_set(key, val, len); } else { @@ -1304,7 +1449,19 @@ static secbool storage_upgrade(void) { unlocked = secfalse; memzero(cached_keys, sizeof(cached_keys)); } else { - return secfalse; + // Copy all entries. + uint32_t offset = 0; + while (sectrue == norcow_get_next(&offset, &key, &val, &len)) { + if (sectrue != norcow_set(key, val, len)) { + return secfalse; + } + } + } + + if (norcow_active_version <= 1) { + if (sectrue != set_wipe_code(WIPE_CODE_EMPTY)) { + return secfalse; + } } norcow_active_version = NORCOW_VERSION; diff --git a/storage/storage.h b/storage/storage.h index 05138fcbe..4c548abbe 100644 --- a/storage/storage.h +++ b/storage/storage.h @@ -45,16 +45,18 @@ 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, const uint8_t *ext_salt); +secbool storage_unlock(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(uint32_t oldpin, uint32_t newpin, const uint8_t *old_ext_salt, const uint8_t *new_ext_salt); +secbool storage_change_wipe_code(uint32_t pin, const uint8_t *ext_salt, + uint32_t wipe_code); 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); +secbool storage_set(const uint16_t key, const void *val, const uint16_t len); secbool storage_delete(const uint16_t key); secbool storage_set_counter(const uint16_t key, const uint32_t count); secbool storage_next_counter(const uint16_t key, uint32_t *count); diff --git a/storage/tests/c/norcow_config.h b/storage/tests/c/norcow_config.h index 83213c301..7f0efb637 100644 --- a/storage/tests/c/norcow_config.h +++ b/storage/tests/c/norcow_config.h @@ -40,6 +40,6 @@ /* * Current storage version. */ -#define NORCOW_VERSION ((uint32_t)0x00000001) +#define NORCOW_VERSION ((uint32_t)0x00000002) #endif diff --git a/storage/tests/python/src/consts.py b/storage/tests/python/src/consts.py index b0512bd06..ee54b3c27 100644 --- a/storage/tests/python/src/consts.py +++ b/storage/tests/python/src/consts.py @@ -15,6 +15,9 @@ VERSION_KEY = (PIN_APP_ID << 8) | 0x04 # Norcow storage key of the storage authentication tag. SAT_KEY = (PIN_APP_ID << 8) | 0x05 +# Norcow storage key of the wipe code data. +WIPE_CODE_DATA_KEY = (PIN_APP_ID << 8) | 0x06 + # The PIN value corresponding to an empty PIN. PIN_EMPTY = 1 @@ -46,6 +49,17 @@ KEK_SIZE = 32 # The length of KEIV in bytes. KEIV_SIZE = 12 +# The byte length of the salt used in checking the wipe code. +WIPE_CODE_SALT_SIZE = 8 + +# The byte length of the tag used in checking the wipe code. +WIPE_CODE_TAG_SIZE = 8 + +# The value corresponding to an unconfigured wipe code. +# NOTE: This is intentionally different from PIN_EMPTY so that we don't need +# special handling when both the PIN and wipe code are not set. +WIPE_CODE_EMPTY = 0 + # Size of counter. 4B integer and 8B tail. COUNTER_TAIL = 12 COUNTER_TAIL_SIZE = 8 @@ -106,7 +120,7 @@ NORCOW_SECTOR_SIZE = 64 * 1024 NORCOW_MAGIC = b"NRC2" # Norcow version, set in the storage header, but also as an encrypted item. -NORCOW_VERSION = b"\x01\x00\x00\x00" +NORCOW_VERSION = b"\x02\x00\x00\x00" # Norcow magic combined with the version, which is stored as its negation. NORCOW_MAGIC_AND_VERSION = NORCOW_MAGIC + bytes( diff --git a/storage/tests/python/src/storage.py b/storage/tests/python/src/storage.py index e223d287f..bd5dc43d4 100644 --- a/storage/tests/python/src/storage.py +++ b/storage/tests/python/src/storage.py @@ -38,8 +38,9 @@ class Storage: self.sak = prng.random_buffer(consts.SAK_SIZE) self.nc.set(consts.SAT_KEY, crypto.init_hmacs(self.sak)) - self._set_encrypt(consts.VERSION_KEY, b"\x01\x00\x00\x00") + self._set_encrypt(consts.VERSION_KEY, b"\x02\x00\x00\x00") self.pin_log.init() + self._set_wipe_code(consts.WIPE_CODE_EMPTY) self._set_pin(consts.PIN_EMPTY) self.unlocked = False @@ -59,6 +60,14 @@ class Storage: else: self._set_bool(consts.PIN_NOT_SET_KEY, False) + def _set_wipe_code(self, wipe_code: int): + if wipe_code == consts.PIN_EMPTY: + wipe_code = consts.WIPE_CODE_EMPTY + wipe_code_bytes = wipe_code.to_bytes(4, "little") + salt = prng.random_buffer(consts.WIPE_CODE_SALT_SIZE) + tag = crypto._hmac(salt, wipe_code_bytes)[: consts.WIPE_CODE_TAG_SIZE] + self.nc.set(consts.WIPE_CODE_DATA_KEY, wipe_code_bytes + salt + tag) + def wipe(self): self.nc.wipe() self._init_pin() diff --git a/storage/tests/python/tests/test_norcow.py b/storage/tests/python/tests/test_norcow.py index 30c5f797a..a142ef51b 100644 --- a/storage/tests/python/tests/test_norcow.py +++ b/storage/tests/python/tests/test_norcow.py @@ -9,7 +9,7 @@ def test_norcow_set(): n.init() n.set(0x0001, b"123") data = n._dump()[0][:256] - assert data[:8] == b"NRC2\xFE\xFF\xFF\xFF" + assert data[:8] == consts.NORCOW_MAGIC_AND_VERSION assert data[8:10] == b"\x01\x00" # app + key assert data[10:12] == b"\x03\x00" # length assert data[12:15] == b"123" # data @@ -18,7 +18,7 @@ def test_norcow_set(): n.wipe() n.set(0x0901, b"hello") data = n._dump()[0][:256] - assert data[:8] == b"NRC2\xFE\xFF\xFF\xFF" + assert data[:8] == consts.NORCOW_MAGIC_AND_VERSION assert data[8:10] == b"\x01\x09" # app + key assert data[10:12] == b"\x05\x00" # length assert data[12:17] == b"hello" # data @@ -63,7 +63,8 @@ def test_norcow_get_item(): assert value == b"123" assert ( n._dump()[0][:40].hex() - == "4e524332feffffff010003003132330002000300343536000101030037383900ffffffffffffffff" + == consts.NORCOW_MAGIC_AND_VERSION.hex() + + "010003003132330002000300343536000101030037383900ffffffffffffffff" ) # replacing item with the same value (update) @@ -72,7 +73,8 @@ def test_norcow_get_item(): assert value == b"789" assert ( n._dump()[0][:40].hex() - == "4e524332feffffff010003003132330002000300343536000101030037383900ffffffffffffffff" + == consts.NORCOW_MAGIC_AND_VERSION.hex() + + "010003003132330002000300343536000101030037383900ffffffffffffffff" ) # replacing item with value with less 1 bits than before (update) @@ -81,7 +83,8 @@ def test_norcow_get_item(): assert value == b"788" assert ( n._dump()[0][:40].hex() - == "4e524332feffffff010003003132330002000300343536000101030037383800ffffffffffffffff" + == consts.NORCOW_MAGIC_AND_VERSION.hex() + + "010003003132330002000300343536000101030037383800ffffffffffffffff" ) # replacing item with value with more 1 bits than before (wipe and new entry) @@ -90,7 +93,8 @@ def test_norcow_get_item(): assert value == b"787" assert ( n._dump()[0][:44].hex() - == "4e524332feffffff0100030031323300020003003435360000000300000000000101030037383700ffffffff" + == consts.NORCOW_MAGIC_AND_VERSION.hex() + + "0100030031323300020003003435360000000300000000000101030037383700ffffffff" ) n.set(0x0002, b"world") From a168d661cf75e45de5bc7d1f7b37637690e3004f Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Fri, 18 Oct 2019 20:12:17 +0200 Subject: [PATCH 04/14] core: Add support for ChangeWipeCode message. --- .../extmod/modtrezorconfig/modtrezorconfig.c | 32 ++++++ .../extmod/modtrezorconfig/norcow_config.h | 2 +- core/mocks/generated/trezorconfig.pyi | 11 +++ core/src/apps/common/request_pin.py | 8 ++ core/src/apps/management/__init__.py | 1 + core/src/apps/management/change_pin.py | 6 +- core/src/apps/management/change_wipe_code.py | 99 +++++++++++++++++++ 7 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 core/src/apps/management/change_wipe_code.py diff --git a/core/embed/extmod/modtrezorconfig/modtrezorconfig.c b/core/embed/extmod/modtrezorconfig/modtrezorconfig.c index c72cc0d1c..e4a6900c1 100644 --- a/core/embed/extmod/modtrezorconfig/modtrezorconfig.c +++ b/core/embed/extmod/modtrezorconfig/modtrezorconfig.c @@ -173,6 +173,36 @@ STATIC mp_obj_t mod_trezorconfig_change_pin(size_t n_args, STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorconfig_change_pin_obj, 4, 4, mod_trezorconfig_change_pin); +/// def change_wipe_code( +/// pin: int, +/// ext_salt: Optional[bytes], +/// wipe_code: int, +/// ) -> bool: +/// """ +/// Change wipe code. Returns True on success, False on failure. +/// """ +STATIC mp_obj_t mod_trezorconfig_change_wipe_code(size_t n_args, + const mp_obj_t *args) { + uint32_t pin = trezor_obj_get_uint(args[0]); + uint32_t wipe_code = trezor_obj_get_uint(args[2]); + mp_buffer_info_t ext_salt_b; + const uint8_t *ext_salt = NULL; + if (args[1] != mp_const_none) { + mp_get_buffer_raise(args[1], &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."); + ext_salt = ext_salt_b.buf; + } + + if (sectrue != storage_change_wipe_code(pin, ext_salt, wipe_code)) { + return mp_const_false; + } + return mp_const_true; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN( + mod_trezorconfig_change_wipe_code_obj, 3, 3, + mod_trezorconfig_change_wipe_code); + /// def get(app: int, key: int, public: bool = False) -> Optional[bytes]: /// """ /// Gets the value of the given key for the given app (or None if not set). @@ -324,6 +354,8 @@ STATIC const mp_rom_map_elem_t mp_module_trezorconfig_globals_table[] = { MP_ROM_PTR(&mod_trezorconfig_get_pin_rem_obj)}, {MP_ROM_QSTR(MP_QSTR_change_pin), MP_ROM_PTR(&mod_trezorconfig_change_pin_obj)}, + {MP_ROM_QSTR(MP_QSTR_change_wipe_code), + MP_ROM_PTR(&mod_trezorconfig_change_wipe_code_obj)}, {MP_ROM_QSTR(MP_QSTR_get), MP_ROM_PTR(&mod_trezorconfig_get_obj)}, {MP_ROM_QSTR(MP_QSTR_set), MP_ROM_PTR(&mod_trezorconfig_set_obj)}, {MP_ROM_QSTR(MP_QSTR_delete), MP_ROM_PTR(&mod_trezorconfig_delete_obj)}, diff --git a/core/embed/extmod/modtrezorconfig/norcow_config.h b/core/embed/extmod/modtrezorconfig/norcow_config.h index b27ce58c6..405d1441a 100644 --- a/core/embed/extmod/modtrezorconfig/norcow_config.h +++ b/core/embed/extmod/modtrezorconfig/norcow_config.h @@ -46,6 +46,6 @@ /* * Current storage version. */ -#define NORCOW_VERSION ((uint32_t)0x00000001) +#define NORCOW_VERSION ((uint32_t)0x00000002) #endif diff --git a/core/mocks/generated/trezorconfig.pyi b/core/mocks/generated/trezorconfig.pyi index 5b8645b9b..146835997 100644 --- a/core/mocks/generated/trezorconfig.pyi +++ b/core/mocks/generated/trezorconfig.pyi @@ -60,6 +60,17 @@ def change_pin( """ +# extmod/modtrezorconfig/modtrezorconfig.c +def change_wipe_code( + pin: int, + ext_salt: Optional[bytes], + wipe_code: int, +) -> bool: + """ + Change wipe code. Returns True on success, False on failure. + """ + + # extmod/modtrezorconfig/modtrezorconfig.c def get(app: int, key: int, public: bool = False) -> Optional[bytes]: """ diff --git a/core/src/apps/common/request_pin.py b/core/src/apps/common/request_pin.py index 3c0690b1c..36cc7eaa4 100644 --- a/core/src/apps/common/request_pin.py +++ b/core/src/apps/common/request_pin.py @@ -117,3 +117,11 @@ async def show_pin_invalid(ctx: wire.Context) -> None: text = Text("Wrong PIN", ui.ICON_WRONG, ui.RED) text.normal("The PIN you entered is", "invalid.") await confirm(ctx, text, confirm=None, cancel="Close") + + +async def show_pin_matches_wipe_code(ctx: wire.Context) -> None: + from apps.common.confirm import confirm + + text = Text("Invalid PIN", ui.ICON_WRONG, ui.RED) + text.normal("The new PIN must be", "different from your", "wipe code.") + await confirm(ctx, text, confirm=None, cancel="Close") diff --git a/core/src/apps/management/__init__.py b/core/src/apps/management/__init__.py index d6781fb59..255bea63a 100644 --- a/core/src/apps/management/__init__.py +++ b/core/src/apps/management/__init__.py @@ -13,3 +13,4 @@ def boot() -> None: wire.add(MessageType.SetU2FCounter, __name__, "set_u2f_counter") wire.add(MessageType.GetNextU2FCounter, __name__, "get_next_u2f_counter") wire.add(MessageType.SdProtect, __name__, "sd_protect") + wire.add(MessageType.ChangeWipeCode, __name__, "change_wipe_code") diff --git a/core/src/apps/management/change_pin.py b/core/src/apps/management/change_pin.py index 991f901c5..ce99e2b29 100644 --- a/core/src/apps/management/change_pin.py +++ b/core/src/apps/management/change_pin.py @@ -10,6 +10,7 @@ from apps.common.request_pin import ( request_pin_and_sd_salt, request_pin_confirm, show_pin_invalid, + show_pin_matches_wipe_code, ) if False: @@ -40,7 +41,10 @@ async def change_pin(ctx: wire.Context, msg: ChangePin) -> Success: # write into storage if not config.change_pin(pin_to_int(curpin), pin_to_int(newpin), salt, salt): - await show_pin_invalid(ctx) + if newpin: + await show_pin_matches_wipe_code(ctx) + else: + await show_pin_invalid(ctx) raise wire.PinInvalid("PIN invalid") if newpin: diff --git a/core/src/apps/management/change_wipe_code.py b/core/src/apps/management/change_wipe_code.py new file mode 100644 index 000000000..12e927baa --- /dev/null +++ b/core/src/apps/management/change_wipe_code.py @@ -0,0 +1,99 @@ +from storage import is_initialized +from trezor import config, ui, wire +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.layout import show_success +from apps.common.request_pin import ( + request_pin_ack, + request_pin_and_sd_salt, + show_pin_invalid, +) + +if False: + from trezor.messages.ChangeWipeCode import ChangeWipeCode + + +async def change_wipe_code(ctx: wire.Context, msg: ChangeWipeCode) -> Success: + if not is_initialized(): + raise wire.NotInitialized("Device is not initialized") + + # Confirm that user wants to set or remove the wipe code. + await _require_confirm_action(ctx, msg) + + # Get the unlocking PIN. + pin, salt = await request_pin_and_sd_salt(ctx) + + if not msg.remove: + # Pre-check the entered PIN. + if config.has_pin() and not config.check_pin(pin_to_int(pin), salt): + await show_pin_invalid(ctx) + raise wire.PinInvalid("PIN invalid") + + # Get new wipe code. + wipe_code = await _request_wipe_code_confirm(ctx, pin) + else: + wipe_code = "" + + # Write into storage. + if not config.change_wipe_code(pin_to_int(pin), salt, pin_to_int(wipe_code)): + await show_pin_invalid(ctx) + raise wire.PinInvalid("PIN invalid") + + if wipe_code: + msg_screen = "set the wipe code." + msg_wire = "Wipe code set" + else: + msg_screen = "disabled the wipe code." + msg_wire = "Wipe code removed" + + await show_success(ctx, ("You have successfully", msg_screen)) + return Success(message=msg_wire) + + +def _require_confirm_action(ctx: wire.Context, msg: ChangeWipeCode) -> None: + if msg.remove: + text = Text("Disable wipe code", ui.ICON_CONFIG) + text.normal("Do you really want to") + text.bold("disable wipe code") + text.bold("protection?") + return require_confirm(ctx, text) + else: + text = Text("Set wipe code", ui.ICON_CONFIG) + text.normal("Do you really want to") + text.bold("set the wipe code?") + return require_confirm(ctx, text) + + +async def _request_wipe_code_confirm(ctx: wire.Context, pin: str) -> str: + while True: + code1 = await request_pin_ack(ctx, "Enter new wipe code") + if code1 == pin: + await _wipe_code_invalid() + continue + + code2 = await request_pin_ack(ctx, "Re-enter new wipe code") + if code1 == code2: + return code1 + await _wipe_code_mismatch() + + +async def _wipe_code_invalid() -> None: + text = Text("Invalid wipe code", ui.ICON_WRONG, ui.RED) + text.normal("The wipe code must be", "different from your PIN.") + text.normal("") + text.normal("Please try again.") + popup = Popup(text, 3000) # show for 3 seconds + await popup + + +async def _wipe_code_mismatch() -> None: + text = Text("Code mismatch", ui.ICON_WRONG, ui.RED) + text.normal("The wipe codes you", "entered do not match.") + text.normal("") + text.normal("Please try again.") + popup = Popup(text, 3000) # show for 3 seconds + await popup From 1deebf1065d344b0b524d7d15697ef07c653abdf Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Wed, 23 Oct 2019 17:50:46 +0200 Subject: [PATCH 05/14] storage: Add STORAGE_UPGRADED_KEY to protect against fake storage upgrade attacks. --- storage/storage.c | 39 +++++++++++++++++++++++++++-- storage/tests/python/src/consts.py | 5 ++++ storage/tests/python/src/storage.py | 1 + 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/storage/storage.c b/storage/storage.c index 4feeba9af..e23b3fafa 100644 --- a/storage/storage.c +++ b/storage/storage.c @@ -53,6 +53,9 @@ // Norcow storage key of the wipe code data. #define WIPE_CODE_DATA_KEY ((APP_STORAGE << 8) | 0x06) +// Norcow storage key of the storage upgrade flag. +#define STORAGE_UPGRADED_KEY ((APP_STORAGE << 8) | 0x07) + // The PIN value corresponding to an empty PIN. #define PIN_EMPTY 1 @@ -150,6 +153,8 @@ static uint8_t hardware_salt[HARDWARE_SALT_SIZE] = {0}; static uint32_t norcow_active_version = 0; static const uint8_t TRUE_BYTE = 0x01; static const uint8_t FALSE_BYTE = 0x00; +static const uint32_t TRUE_WORD = 0xC35A69A5; +static const uint32_t FALSE_WORD = 0x3CA5965A; static void __handle_fault(const char *msg, const char *file, int line, const char *func); @@ -621,6 +626,8 @@ static void init_wiped_storage(void) { ensure(auth_init(), "set_storage_auth_tag failed"); ensure(storage_set_encrypted(VERSION_KEY, &version, sizeof(version)), "set_storage_version failed"); + ensure(norcow_set(STORAGE_UPGRADED_KEY, &FALSE_WORD, sizeof(FALSE_WORD)), + "set_storage_not_upgraded failed"); ensure(pin_logs_init(0), "init_pin_logs failed"); ensure(set_wipe_code(WIPE_CODE_EMPTY), "set_wipe_code failed"); @@ -854,12 +861,36 @@ secbool check_storage_version(void) { handle_fault("storage version check"); return secfalse; } - + const void *storage_upgraded = NULL; + if (sectrue != norcow_get(STORAGE_UPGRADED_KEY, &storage_upgraded, &len) || + len != sizeof(TRUE_WORD)) { + handle_fault("storage version check"); + return secfalse; + } if (version > norcow_active_version) { + // Attack: Storage was downgraded. storage_wipe(); + handle_fault("storage version check"); + return secfalse; } else if (version < norcow_active_version) { + // Storage was upgraded. + if (*(const uint32_t *)storage_upgraded != TRUE_WORD) { + // Attack: The upgrade process was bypassed. + storage_wipe(); + handle_fault("storage version check"); + return secfalse; + } + norcow_set(STORAGE_UPGRADED_KEY, &FALSE_WORD, sizeof(FALSE_WORD)); storage_set_encrypted(VERSION_KEY, &norcow_active_version, sizeof(norcow_active_version)); + } else { + // Standard operation. The storage was neither upgraded nor downgraded. + if (*(const uint32_t *)storage_upgraded != FALSE_WORD) { + // Attack: The upgrade process was launched when it shouldn't have been. + storage_wipe(); + handle_fault("storage version check"); + return secfalse; + } } return sectrue; } @@ -900,7 +931,6 @@ static secbool decrypt_dek(const uint8_t *kek, const uint8_t *keiv) { memcpy(cached_keys, keys, sizeof(keys)); memzero(keys, sizeof(keys)); memzero(tag, sizeof(tag)); - // Check that the authenticated version number matches the norcow version. // NOTE: This also initializes the authentication_sum by calling // storage_get_encrypted() which calls auth_get(). @@ -1464,6 +1494,11 @@ static secbool storage_upgrade(void) { } } + if (sectrue != + norcow_set(STORAGE_UPGRADED_KEY, &TRUE_WORD, sizeof(TRUE_WORD))) { + return secfalse; + } + norcow_active_version = NORCOW_VERSION; return norcow_upgrade_finish(); } diff --git a/storage/tests/python/src/consts.py b/storage/tests/python/src/consts.py index ee54b3c27..2fb2e1146 100644 --- a/storage/tests/python/src/consts.py +++ b/storage/tests/python/src/consts.py @@ -18,6 +18,9 @@ SAT_KEY = (PIN_APP_ID << 8) | 0x05 # Norcow storage key of the wipe code data. WIPE_CODE_DATA_KEY = (PIN_APP_ID << 8) | 0x06 +# Norcow storage key of the storage upgrade flag. +STORAGE_UPGRADED_KEY = (PIN_APP_ID << 8) | 0x07 + # The PIN value corresponding to an empty PIN. PIN_EMPTY = 1 @@ -102,6 +105,8 @@ WORD_SIZE = 4 # Boolean values are stored as a simple 0/1 int. TRUE_BYTE = b"\x01" FALSE_BYTE = b"\x00" +TRUE_WORD = b"\xA5\x69\x5A\xC3" +FALSE_WORD = b"\x5A\x96\xA5\x3C" # ----- Crypto ----- # diff --git a/storage/tests/python/src/storage.py b/storage/tests/python/src/storage.py index bd5dc43d4..cb4238247 100644 --- a/storage/tests/python/src/storage.py +++ b/storage/tests/python/src/storage.py @@ -39,6 +39,7 @@ class Storage: self.nc.set(consts.SAT_KEY, crypto.init_hmacs(self.sak)) self._set_encrypt(consts.VERSION_KEY, b"\x02\x00\x00\x00") + self.nc.set(consts.STORAGE_UPGRADED_KEY, consts.FALSE_WORD) self.pin_log.init() self._set_wipe_code(consts.WIPE_CODE_EMPTY) self._set_pin(consts.PIN_EMPTY) From b874539e2c2c9eef530d93be2742244e1c29091d Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Tue, 29 Oct 2019 15:55:58 +0100 Subject: [PATCH 06/14] storage: Change secequal32() to use length in bytes instead of length in words. --- storage/storage.c | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/storage/storage.c b/storage/storage.c index e23b3fafa..ccad58d94 100644 --- a/storage/storage.c +++ b/storage/storage.c @@ -17,6 +17,7 @@ * along with this program. If not, see . */ +#include #include #include "chacha20poly1305/rfc7539.h" @@ -185,19 +186,22 @@ static secbool secequal(const void *ptr1, const void *ptr2, size_t n) { return diff ? secfalse : sectrue; } -static secbool secequal32(const uint32_t *ptr1, const uint32_t *ptr2, - size_t n) { +static secbool secequal32(const void *ptr1, const void *ptr2, size_t n) { + assert(n % sizeof(uint32_t) == 0); + size_t wn = n / sizeof(uint32_t); + const uint32_t *p1 = (const uint32_t *)ptr1; + const uint32_t *p2 = (const uint32_t *)ptr2; uint32_t diff = 0; size_t i = 0; - for (i = 0; i < n; ++i) { + for (i = 0; i < wn; ++i) { uint32_t mask = random32(); - diff |= (*ptr1 + mask - *ptr2) ^ mask; - ++ptr1; - ++ptr2; + diff |= (*p1 + mask - *p2) ^ mask; + ++p1; + ++p2; } // Check loop completion in case of a fault injection attack. - if (i != n) { + if (i != wn) { handle_fault("loop completion check"); } @@ -922,8 +926,7 @@ static secbool decrypt_dek(const uint8_t *kek, const uint8_t *keiv) { rfc7539_finish(&ctx, 0, KEYS_SIZE, tag); memzero(&ctx, sizeof(ctx)); wait_random(); - if (secequal32((const uint32_t *)tag, pvc, PVC_SIZE / sizeof(uint32_t)) != - sectrue) { + if (secequal32(tag, pvc, PVC_SIZE) != sectrue) { memzero(keys, sizeof(keys)); memzero(tag, sizeof(tag)); return secfalse; From a3b608d8dd5ed6260e157bbcb5cfc5b499650ab0 Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Tue, 29 Oct 2019 15:58:53 +0100 Subject: [PATCH 07/14] storage, core: Reveal the wipe code status after the device is unlocked. --- .../extmod/modtrezorconfig/modtrezorconfig.c | 15 ++++++++++ core/mocks/generated/trezorconfig.pyi | 7 +++++ core/src/apps/management/change_wipe_code.py | 29 +++++++++++++++---- storage/storage.c | 8 +++++ storage/storage.h | 1 + 5 files changed, 54 insertions(+), 6 deletions(-) diff --git a/core/embed/extmod/modtrezorconfig/modtrezorconfig.c b/core/embed/extmod/modtrezorconfig/modtrezorconfig.c index e4a6900c1..1f574f511 100644 --- a/core/embed/extmod/modtrezorconfig/modtrezorconfig.c +++ b/core/embed/extmod/modtrezorconfig/modtrezorconfig.c @@ -173,6 +173,19 @@ STATIC mp_obj_t mod_trezorconfig_change_pin(size_t n_args, STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorconfig_change_pin_obj, 4, 4, mod_trezorconfig_change_pin); +/// def has_wipe_code() -> bool: +/// """ +/// Returns True if storage has a configured wipe code, False otherwise. +/// """ +STATIC mp_obj_t mod_trezorconfig_has_wipe_code(void) { + if (sectrue != storage_has_wipe_code()) { + return mp_const_false; + } + return mp_const_true; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorconfig_has_wipe_code_obj, + mod_trezorconfig_has_wipe_code); + /// def change_wipe_code( /// pin: int, /// ext_salt: Optional[bytes], @@ -354,6 +367,8 @@ STATIC const mp_rom_map_elem_t mp_module_trezorconfig_globals_table[] = { MP_ROM_PTR(&mod_trezorconfig_get_pin_rem_obj)}, {MP_ROM_QSTR(MP_QSTR_change_pin), MP_ROM_PTR(&mod_trezorconfig_change_pin_obj)}, + {MP_ROM_QSTR(MP_QSTR_has_wipe_code), + MP_ROM_PTR(&mod_trezorconfig_has_wipe_code_obj)}, {MP_ROM_QSTR(MP_QSTR_change_wipe_code), MP_ROM_PTR(&mod_trezorconfig_change_wipe_code_obj)}, {MP_ROM_QSTR(MP_QSTR_get), MP_ROM_PTR(&mod_trezorconfig_get_obj)}, diff --git a/core/mocks/generated/trezorconfig.pyi b/core/mocks/generated/trezorconfig.pyi index 146835997..6430ee9ba 100644 --- a/core/mocks/generated/trezorconfig.pyi +++ b/core/mocks/generated/trezorconfig.pyi @@ -60,6 +60,13 @@ def change_pin( """ +# extmod/modtrezorconfig/modtrezorconfig.c +def has_wipe_code() -> bool: + """ + Returns True if storage has a configured wipe code, False otherwise. + """ + + # extmod/modtrezorconfig/modtrezorconfig.c def change_wipe_code( pin: int, diff --git a/core/src/apps/management/change_wipe_code.py b/core/src/apps/management/change_wipe_code.py index 12e927baa..9fc269270 100644 --- a/core/src/apps/management/change_wipe_code.py +++ b/core/src/apps/management/change_wipe_code.py @@ -22,7 +22,8 @@ async def change_wipe_code(ctx: wire.Context, msg: ChangeWipeCode) -> Success: raise wire.NotInitialized("Device is not initialized") # Confirm that user wants to set or remove the wipe code. - await _require_confirm_action(ctx, msg) + has_wipe_code = config.has_wipe_code() + await _require_confirm_action(ctx, msg, has_wipe_code) # Get the unlocking PIN. pin, salt = await request_pin_and_sd_salt(ctx) @@ -44,8 +45,12 @@ async def change_wipe_code(ctx: wire.Context, msg: ChangeWipeCode) -> Success: raise wire.PinInvalid("PIN invalid") if wipe_code: - msg_screen = "set the wipe code." - msg_wire = "Wipe code set" + if has_wipe_code: + msg_screen = "changed the wipe code." + msg_wire = "Wipe code changed" + else: + msg_screen = "set the wipe code." + msg_wire = "Wipe code set" else: msg_screen = "disabled the wipe code." msg_wire = "Wipe code removed" @@ -54,19 +59,31 @@ async def change_wipe_code(ctx: wire.Context, msg: ChangeWipeCode) -> Success: return Success(message=msg_wire) -def _require_confirm_action(ctx: wire.Context, msg: ChangeWipeCode) -> None: - if msg.remove: +def _require_confirm_action( + ctx: wire.Context, msg: ChangeWipeCode, has_wipe_code: bool +) -> None: + if msg.remove and has_wipe_code: text = Text("Disable wipe code", ui.ICON_CONFIG) text.normal("Do you really want to") text.bold("disable wipe code") text.bold("protection?") return require_confirm(ctx, text) - else: + + if not msg.remove and has_wipe_code: + text = Text("Change wipe code", ui.ICON_CONFIG) + text.normal("Do you really want to") + text.bold("change the wipe code?") + return require_confirm(ctx, text) + + if not msg.remove and not has_wipe_code: text = Text("Set wipe code", ui.ICON_CONFIG) text.normal("Do you really want to") text.bold("set the wipe code?") return require_confirm(ctx, text) + # Removing non-existing wipe code. + raise wire.ProcessError("Wipe code protection is already disabled") + async def _request_wipe_code_confirm(ctx: wire.Context, pin: str) -> str: while True: diff --git a/storage/storage.c b/storage/storage.c index ccad58d94..6a8c7fd99 100644 --- a/storage/storage.c +++ b/storage/storage.c @@ -1331,6 +1331,14 @@ secbool storage_change_pin(uint32_t oldpin, uint32_t newpin, return ret; } +secbool storage_has_wipe_code(void) { + if (sectrue != initialized || sectrue != unlocked) { + return secfalse; + } + + return is_not_wipe_code(WIPE_CODE_EMPTY); +} + secbool storage_change_wipe_code(uint32_t pin, const uint8_t *ext_salt, uint32_t wipe_code) { if (sectrue != initialized || (pin != PIN_EMPTY && pin == wipe_code)) { diff --git a/storage/storage.h b/storage/storage.h index 4c548abbe..6abcf6f25 100644 --- a/storage/storage.h +++ b/storage/storage.h @@ -52,6 +52,7 @@ uint32_t storage_get_pin_rem(void); secbool storage_change_pin(uint32_t oldpin, uint32_t newpin, const uint8_t *old_ext_salt, const uint8_t *new_ext_salt); +secbool storage_has_wipe_code(void); secbool storage_change_wipe_code(uint32_t pin, const uint8_t *ext_salt, uint32_t wipe_code); secbool storage_get(const uint16_t key, void *val, const uint16_t max_len, From 1bc8226a50d3acf31b843befe109a2c9271662e6 Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Tue, 29 Oct 2019 16:38:12 +0100 Subject: [PATCH 08/14] common, core: Add wipe_code_protection to the Features message. --- common/protob/messages-management.proto | 1 + core/src/apps/homescreen/__init__.py | 1 + core/src/trezor/messages/Features.py | 3 +++ python/src/trezorlib/messages/Features.py | 3 +++ 4 files changed, 8 insertions(+) diff --git a/common/protob/messages-management.proto b/common/protob/messages-management.proto index 2e4caa981..bf19c94f4 100644 --- a/common/protob/messages-management.proto +++ b/common/protob/messages-management.proto @@ -87,6 +87,7 @@ message Features { optional BackupType backup_type = 31; // type of device backup (BIP-39 / SLIP-39 basic / SLIP-39 advanced) optional bool sd_card_present = 32; // is SD card present optional bool sd_protection = 33; // is SD Protect enabled + optional bool wipe_code_protection = 34; // is wipe code protection enabled } /** diff --git a/core/src/apps/homescreen/__init__.py b/core/src/apps/homescreen/__init__.py index d86862067..59669706e 100644 --- a/core/src/apps/homescreen/__init__.py +++ b/core/src/apps/homescreen/__init__.py @@ -70,6 +70,7 @@ def get_features() -> Features: ] f.sd_card_present = io.SDCard().present() f.sd_protection = storage.sd_salt.is_enabled() + f.wipe_code_protection = config.has_wipe_code() return f diff --git a/core/src/trezor/messages/Features.py b/core/src/trezor/messages/Features.py index ed38304d2..17586cc24 100644 --- a/core/src/trezor/messages/Features.py +++ b/core/src/trezor/messages/Features.py @@ -49,6 +49,7 @@ class Features(p.MessageType): backup_type: EnumTypeBackupType = None, sd_card_present: bool = None, sd_protection: bool = None, + wipe_code_protection: bool = None, ) -> None: self.vendor = vendor self.major_version = major_version @@ -82,6 +83,7 @@ class Features(p.MessageType): self.backup_type = backup_type self.sd_card_present = sd_card_present self.sd_protection = sd_protection + self.wipe_code_protection = wipe_code_protection @classmethod def get_fields(cls) -> Dict: @@ -118,4 +120,5 @@ class Features(p.MessageType): 31: ('backup_type', p.EnumType("BackupType", (0, 1, 2)), 0), 32: ('sd_card_present', p.BoolType, 0), 33: ('sd_protection', p.BoolType, 0), + 34: ('wipe_code_protection', p.BoolType, 0), } diff --git a/python/src/trezorlib/messages/Features.py b/python/src/trezorlib/messages/Features.py index 4e355e50e..23720aa22 100644 --- a/python/src/trezorlib/messages/Features.py +++ b/python/src/trezorlib/messages/Features.py @@ -49,6 +49,7 @@ class Features(p.MessageType): backup_type: EnumTypeBackupType = None, sd_card_present: bool = None, sd_protection: bool = None, + wipe_code_protection: bool = None, ) -> None: self.vendor = vendor self.major_version = major_version @@ -82,6 +83,7 @@ class Features(p.MessageType): self.backup_type = backup_type self.sd_card_present = sd_card_present self.sd_protection = sd_protection + self.wipe_code_protection = wipe_code_protection @classmethod def get_fields(cls) -> Dict: @@ -118,4 +120,5 @@ class Features(p.MessageType): 31: ('backup_type', p.EnumType("BackupType", (0, 1, 2)), 0), 32: ('sd_card_present', p.BoolType, 0), 33: ('sd_protection', p.BoolType, 0), + 34: ('wipe_code_protection', p.BoolType, 0), } From 829da5fe6cd82234259f09a048e5ff495d733f9f Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Wed, 23 Oct 2019 19:20:08 +0200 Subject: [PATCH 09/14] legacy: Implement ChangeWipeCode message for Trezor One. --- legacy/firmware/config.c | 16 +++++++ legacy/firmware/config.h | 3 ++ legacy/firmware/fsm.c | 3 ++ legacy/firmware/fsm.h | 1 + legacy/firmware/fsm_msg_common.h | 50 +++++++++++++++++++++ legacy/firmware/protect.c | 74 ++++++++++++++++++++++++++++++++ legacy/firmware/protect.h | 1 + legacy/norcow_config.h | 2 +- 8 files changed, 149 insertions(+), 1 deletion(-) diff --git a/legacy/firmware/config.c b/legacy/firmware/config.c index 918571c1e..98a1e907b 100644 --- a/legacy/firmware/config.c +++ b/legacy/firmware/config.c @@ -785,6 +785,22 @@ bool config_getPin(char *dest, uint16_t dest_size) { } #endif +bool config_hasWipeCode(void) { return sectrue == storage_has_wipe_code(); } + +bool config_changeWipeCode(const char *pin, const char *wipe_code) { + uint32_t wipe_code_int = pin_to_int(wipe_code); + if (wipe_code_int == 0) { + return false; + } + + char oldTiny = usbTiny(1); + secbool ret = storage_change_wipe_code(pin_to_int(pin), NULL, wipe_code_int); + usbTiny(oldTiny); + + memzero(&wipe_code_int, sizeof(wipe_code_int)); + return sectrue == ret; +} + void session_cachePassphrase(const char *passphrase) { strlcpy(sessionPassphrase, passphrase, sizeof(sessionPassphrase)); sessionPassphraseCached = sectrue; diff --git a/legacy/firmware/config.h b/legacy/firmware/config.h index 31c7f3c8b..d061dea71 100644 --- a/legacy/firmware/config.h +++ b/legacy/firmware/config.h @@ -129,6 +129,9 @@ bool config_hasPin(void); bool config_changePin(const char *old_pin, const char *new_pin); bool session_isUnlocked(void); +bool config_hasWipeCode(void); +bool config_changeWipeCode(const char *pin, const char *wipe_code); + uint32_t config_nextU2FCounter(void); void config_setU2FCounter(uint32_t u2fcounter); diff --git a/legacy/firmware/fsm.c b/legacy/firmware/fsm.c index 7b15fe6e0..4c82442b3 100644 --- a/legacy/firmware/fsm.c +++ b/legacy/firmware/fsm.c @@ -167,6 +167,9 @@ void fsm_sendFailure(FailureType code, const char *text) case FailureType_Failure_PinMismatch: text = _("PIN mismatch"); break; + case FailureType_Failure_WipeCodeMismatch: + text = _("Wipe code mismatch"); + break; case FailureType_Failure_FirmwareError: text = _("Firmware error"); break; diff --git a/legacy/firmware/fsm.h b/legacy/firmware/fsm.h index d66e1699e..23c4ffe47 100644 --- a/legacy/firmware/fsm.h +++ b/legacy/firmware/fsm.h @@ -52,6 +52,7 @@ void fsm_msgInitialize(const Initialize *msg); void fsm_msgGetFeatures(const GetFeatures *msg); void fsm_msgPing(const Ping *msg); void fsm_msgChangePin(const ChangePin *msg); +void fsm_msgChangeWipeCode(const ChangeWipeCode *msg); void fsm_msgWipeDevice(const WipeDevice *msg); void fsm_msgGetEntropy(const GetEntropy *msg); #if DEBUG_LINK diff --git a/legacy/firmware/fsm_msg_common.h b/legacy/firmware/fsm_msg_common.h index d84b47abb..9de5b2ec4 100644 --- a/legacy/firmware/fsm_msg_common.h +++ b/legacy/firmware/fsm_msg_common.h @@ -82,6 +82,10 @@ void fsm_msgGetFeatures(const GetFeatures *msg) { resp->has_flags = config_getFlags(&(resp->flags)); resp->has_model = true; strlcpy(resp->model, "1", sizeof(resp->model)); + if (session_isUnlocked()) { + resp->has_wipe_code_protection = true; + resp->wipe_code_protection = config_hasWipeCode(); + } #if BITCOIN_ONLY resp->capabilities_count = 2; @@ -176,6 +180,52 @@ void fsm_msgChangePin(const ChangePin *msg) { layoutHome(); } +void fsm_msgChangeWipeCode(const ChangeWipeCode *msg) { + CHECK_INITIALIZED + + bool removal = msg->has_remove && msg->remove; + bool has_wipe_code = config_hasWipeCode(); + + if (removal) { + // Note that if storage is locked, then config_hasWipeCode() returns false. + if (has_wipe_code || !session_isUnlocked()) { + layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL, + _("Do you really want to"), _("disable wipe code"), + _("protection?"), NULL, NULL, NULL); + } else { + fsm_sendSuccess(_("Wipe code removed")); + return; + } + } else { + if (has_wipe_code) { + layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL, + _("Do you really want to"), _("change the current"), + _("wipe code?"), NULL, NULL, NULL); + } else { + layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL, + _("Do you really want to"), _("set a new wipe code?"), + NULL, NULL, NULL, NULL); + } + } + if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + layoutHome(); + return; + } + + if (protectChangeWipeCode(removal)) { + if (removal) { + fsm_sendSuccess(_("Wipe code removed")); + } else if (has_wipe_code) { + fsm_sendSuccess(_("Wipe code changed")); + } else { + fsm_sendSuccess(_("Wipe code set")); + } + } + + layoutHome(); +} + void fsm_msgWipeDevice(const WipeDevice *msg) { (void)msg; layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL, diff --git a/legacy/firmware/protect.c b/legacy/firmware/protect.c index 687d35aee..2b77b291d 100644 --- a/legacy/firmware/protect.c +++ b/legacy/firmware/protect.c @@ -270,6 +270,80 @@ bool protectChangePin(bool removal) { bool ret = config_changePin(old_pin, new_pin); memzero(old_pin, sizeof(old_pin)); memzero(new_pin, sizeof(new_pin)); + if (ret == false) { + if (removal) { + fsm_sendFailure(FailureType_Failure_PinInvalid, NULL); + } else { + fsm_sendFailure(FailureType_Failure_ProcessError, + _("The new PIN must be different from your wipe code.")); + } + } + return ret; +} + +bool protectChangeWipeCode(bool removal) { + static CONFIDENTIAL char pin[MAX_PIN_LEN + 1] = ""; + static CONFIDENTIAL char wipe_code[MAX_PIN_LEN + 1] = ""; + const char *input = NULL; + + if (config_hasPin()) { + input = requestPin(PinMatrixRequestType_PinMatrixRequestType_Current, + _("Please enter your PIN:")); + if (input == NULL) { + fsm_sendFailure(FailureType_Failure_PinCancelled, NULL); + return false; + } + + // If removing, defer the check to config_changeWipeCode(). + if (!removal) { + usbTiny(1); + bool ret = config_unlock(input); + usbTiny(0); + if (ret == false) { + fsm_sendFailure(FailureType_Failure_PinInvalid, NULL); + return false; + } + } + + strlcpy(pin, input, sizeof(pin)); + } + + if (!removal) { + input = requestPin(PinMatrixRequestType_PinMatrixRequestType_WipeCodeFirst, + _("Enter new wipe code:")); + if (input == NULL) { + memzero(pin, sizeof(pin)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + return false; + } + if (strncmp(pin, input, sizeof(pin)) == 0) { + memzero(pin, sizeof(pin)); + fsm_sendFailure(FailureType_Failure_ProcessError, + _("The wipe code must be different from your PIN.")); + return false; + } + strlcpy(wipe_code, input, sizeof(wipe_code)); + + input = requestPin(PinMatrixRequestType_PinMatrixRequestType_WipeCodeSecond, + _("Re-enter new wipe code:")); + if (input == NULL) { + memzero(pin, sizeof(pin)); + memzero(wipe_code, sizeof(wipe_code)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + return false; + } + + if (strncmp(wipe_code, input, sizeof(wipe_code)) != 0) { + memzero(pin, sizeof(pin)); + memzero(wipe_code, sizeof(wipe_code)); + fsm_sendFailure(FailureType_Failure_WipeCodeMismatch, NULL); + return false; + } + } + + bool ret = config_changeWipeCode(pin, wipe_code); + memzero(pin, sizeof(pin)); + memzero(wipe_code, sizeof(wipe_code)); if (ret == false) { fsm_sendFailure(FailureType_Failure_PinInvalid, NULL); } diff --git a/legacy/firmware/protect.h b/legacy/firmware/protect.h index dfae16934..4d9af7c6e 100644 --- a/legacy/firmware/protect.h +++ b/legacy/firmware/protect.h @@ -29,6 +29,7 @@ secbool protectPinUiCallback(uint32_t wait, uint32_t progress, const char* message); bool protectPin(bool use_cached); bool protectChangePin(bool removal); +bool protectChangeWipeCode(bool removal); bool protectPassphrase(void); extern bool protectAbortedByCancel; diff --git a/legacy/norcow_config.h b/legacy/norcow_config.h index 9fb4b20a4..59a2d79f3 100644 --- a/legacy/norcow_config.h +++ b/legacy/norcow_config.h @@ -36,6 +36,6 @@ /* * Current storage version. */ -#define NORCOW_VERSION ((uint32_t)0x00000001) +#define NORCOW_VERSION ((uint32_t)0x00000002) #endif From a08b66ee6c9b419b1364a3f91ec564bebed37d61 Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Thu, 31 Oct 2019 14:56:48 +0100 Subject: [PATCH 10/14] storage: gitignore hypothesis and libtrezor-storage.so. --- storage/.gitignore | 2 ++ storage/tests/c/libtrezor-storage.so | Bin 78632 -> 0 bytes 2 files changed, 2 insertions(+) delete mode 100755 storage/tests/c/libtrezor-storage.so diff --git a/storage/.gitignore b/storage/.gitignore index 6142305dc..0662be654 100644 --- a/storage/.gitignore +++ b/storage/.gitignore @@ -1,2 +1,4 @@ *.o *.d +*.so +.hypothesis diff --git a/storage/tests/c/libtrezor-storage.so b/storage/tests/c/libtrezor-storage.so deleted file mode 100755 index 64898cc473e16081cab5d711043404b7f7d3b08e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 78632 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 X4;8=SsCn1tf3mBzRO7d@5B2|Fg#yV` From 89a3ec16965d80e4cc3b946a6d364f3a05ba51fc Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Mon, 25 Nov 2019 17:08:19 +0100 Subject: [PATCH 11/14] tests: Add device tests for wipe code protection. --- .../test_msg_change_wipe_code_t1.py | 229 +++++++++++++++ .../test_msg_change_wipe_code_t2.py | 260 ++++++++++++++++++ 2 files changed, 489 insertions(+) create mode 100644 tests/device_tests/test_msg_change_wipe_code_t1.py create mode 100644 tests/device_tests/test_msg_change_wipe_code_t2.py diff --git a/tests/device_tests/test_msg_change_wipe_code_t1.py b/tests/device_tests/test_msg_change_wipe_code_t1.py new file mode 100644 index 000000000..d4152bba0 --- /dev/null +++ b/tests/device_tests/test_msg_change_wipe_code_t1.py @@ -0,0 +1,229 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + +import pytest + +from trezorlib import messages + +PIN4 = "1234" +WIPE_CODE4 = "4321" +WIPE_CODE6 = "456789" + +pytestmark = pytest.mark.skip_t2 + + +def _set_wipe_code(client, wipe_code): + # Set/change wipe code. + ret = client.call_raw(messages.ChangeWipeCode()) + assert isinstance(ret, messages.ButtonRequest) + + # Confirm intent to set/change wipe code. + client.debug.press_yes() + ret = client.call_raw(messages.ButtonAck()) + + if client.features.pin_protection: + # Send current PIN. + assert isinstance(ret, messages.PinMatrixRequest) + pin_encoded = client.debug.read_pin_encoded() + ret = client.call_raw(messages.PinMatrixAck(pin=pin_encoded)) + + # Send the new wipe code for the first time. + assert isinstance(ret, messages.PinMatrixRequest) + wipe_code_encoded = client.debug.encode_pin(wipe_code) + ret = client.call_raw(messages.PinMatrixAck(pin=wipe_code_encoded)) + + # Send the new wipe code for the second time. + assert isinstance(ret, messages.PinMatrixRequest) + wipe_code_encoded = client.debug.encode_pin(wipe_code) + ret = client.call_raw(messages.PinMatrixAck(pin=wipe_code_encoded)) + + # Now we're done. + assert isinstance(ret, messages.Success) + + +def _remove_wipe_code(client): + # Remove wipe code + ret = client.call_raw(messages.ChangeWipeCode(remove=True)) + assert isinstance(ret, messages.ButtonRequest) + + # Confirm intent to remove wipe code. + client.debug.press_yes() + ret = client.call_raw(messages.ButtonAck()) + + # Send current PIN. + assert isinstance(ret, messages.PinMatrixRequest) + pin_encoded = client.debug.read_pin_encoded() + ret = client.call_raw(messages.PinMatrixAck(pin=pin_encoded)) + + # Now we're done. + assert isinstance(ret, messages.Success) + + +def _check_wipe_code(client, wipe_code): + # Try to change the PIN to the current wipe code value. The operation should fail. + ret = client.call_raw(messages.ChangePin()) + assert isinstance(ret, messages.ButtonRequest) + + # Confirm intent to change PIN. + client.debug.press_yes() + ret = client.call_raw(messages.ButtonAck()) + + # Send current PIN. + assert isinstance(ret, messages.PinMatrixRequest) + pin_encoded = client.debug.read_pin_encoded() + ret = client.call_raw(messages.PinMatrixAck(pin=pin_encoded)) + + # Send the new wipe code for the first time. + assert isinstance(ret, messages.PinMatrixRequest) + wipe_code_encoded = client.debug.encode_pin(wipe_code) + ret = client.call_raw(messages.PinMatrixAck(pin=wipe_code_encoded)) + + # Send the new wipe code for the second time. + assert isinstance(ret, messages.PinMatrixRequest) + wipe_code_encoded = client.debug.encode_pin(wipe_code) + ret = client.call_raw(messages.PinMatrixAck(pin=wipe_code_encoded)) + + # Expect failure. + assert isinstance(ret, messages.Failure) + + +@pytest.mark.setup_client(pin=PIN4) +def test_set_remove_wipe_code(client): + # Check that wipe code protection status is not revealed in locked state. + assert client.features.wipe_code_protection is None + + # Test set wipe code. + _set_wipe_code(client, WIPE_CODE4) + + # Check that there's wipe code protection now. + client.init_device() + assert client.features.wipe_code_protection is True + + # Check that the wipe code is correct. + _check_wipe_code(client, WIPE_CODE4) + + # Test change wipe code. + _set_wipe_code(client, WIPE_CODE6) + + # Check that there's still wipe code protection now. + client.init_device() + assert client.features.wipe_code_protection is True + + # Check that the PIN is correct. + _check_wipe_code(client, WIPE_CODE6) + + # Test remove wipe code. + _remove_wipe_code(client) + + # Check that there's no wipe code protection now. + client.init_device() + assert client.features.wipe_code_protection is False + + +def test_set_wipe_code_mismatch(client): + # Check that there is no wipe code protection. + assert client.features.wipe_code_protection is False + + # Let's set a new wipe code. + ret = client.call_raw(messages.ChangeWipeCode()) + assert isinstance(ret, messages.ButtonRequest) + + # Confirm intent to set wipe code. + client.debug.press_yes() + ret = client.call_raw(messages.ButtonAck()) + + # Send the new wipe code for the first time. + assert isinstance(ret, messages.PinMatrixRequest) + wipe_code_encoded = client.debug.encode_pin(WIPE_CODE4) + ret = client.call_raw(messages.PinMatrixAck(pin=wipe_code_encoded)) + + # Send the new wipe code for the second time, but different. + assert isinstance(ret, messages.PinMatrixRequest) + wipe_code_encoded = client.debug.encode_pin(WIPE_CODE6) + ret = client.call_raw(messages.PinMatrixAck(pin=wipe_code_encoded)) + + # The operation should fail, because the wipe codes are different. + assert isinstance(ret, messages.Failure) + assert ret.code == messages.FailureType.WipeCodeMismatch + + # Check that there is no wipe code protection. + client.init_device() + assert client.features.wipe_code_protection is False + + +@pytest.mark.setup_client(pin=PIN4) +def test_set_wipe_code_to_pin(client): + # Check that wipe code protection status is not revealed in locked state. + assert client.features.wipe_code_protection is None + + # Let's try setting the wipe code to the curent PIN value. + ret = client.call_raw(messages.ChangeWipeCode()) + assert isinstance(ret, messages.ButtonRequest) + + # Confirm intent to set wipe code. + client.debug.press_yes() + ret = client.call_raw(messages.ButtonAck()) + + # Send current PIN. + assert isinstance(ret, messages.PinMatrixRequest) + pin_encoded = client.debug.read_pin_encoded() + ret = client.call_raw(messages.PinMatrixAck(pin=pin_encoded)) + + # Send the new wipe code. + assert isinstance(ret, messages.PinMatrixRequest) + pin_encoded = client.debug.read_pin_encoded() + ret = client.call_raw(messages.PinMatrixAck(pin=pin_encoded)) + + # The operation should fail, because the wipe code must be different from the PIN. + assert isinstance(ret, messages.Failure) + assert ret.code == messages.FailureType.ProcessError + + # Check that there is no wipe code protection. + client.init_device() + assert client.features.wipe_code_protection is False + + +def test_set_pin_to_wipe_code(client): + # Set wipe code. + _set_wipe_code(client, WIPE_CODE4) + + # Try to set the PIN to the current wipe code value. + ret = client.call_raw(messages.ChangePin()) + assert isinstance(ret, messages.ButtonRequest) + + # Confirm intent to set PIN. + client.debug.press_yes() + ret = client.call_raw(messages.ButtonAck()) + + # Send the new PIN for the first time. + assert isinstance(ret, messages.PinMatrixRequest) + pin_encoded = client.debug.encode_pin(WIPE_CODE4) + ret = client.call_raw(messages.PinMatrixAck(pin=pin_encoded)) + + # Send the new PIN for the second time. + assert isinstance(ret, messages.PinMatrixRequest) + pin_encoded = client.debug.encode_pin(WIPE_CODE4) + ret = client.call_raw(messages.PinMatrixAck(pin=pin_encoded)) + + # The operation should fail, because the PIN must be different from the wipe code. + assert isinstance(ret, messages.Failure) + assert ret.code == messages.FailureType.ProcessError + + # Check that there is no PIN protection. + client.init_device() + assert client.features.pin_protection is False + ret = client.call_raw(messages.Ping(pin_protection=True)) + assert isinstance(ret, messages.Success) diff --git a/tests/device_tests/test_msg_change_wipe_code_t2.py b/tests/device_tests/test_msg_change_wipe_code_t2.py new file mode 100644 index 000000000..d6ffd4df3 --- /dev/null +++ b/tests/device_tests/test_msg_change_wipe_code_t2.py @@ -0,0 +1,260 @@ +# 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 device, messages +from trezorlib.exceptions import Cancelled, TrezorFailure + +PIN4 = "1234" +WIPE_CODE4 = "4321" +WIPE_CODE6 = "456789" + +pytestmark = pytest.mark.skip_t1 + + +def _input_flow_set_pin(debug, pin): + yield # do you want to set a new pin? + print("set pin?") + debug.press_yes() + yield # enter new pin + print(f"enter pin {pin}") + debug.input(pin) + yield # enter new pin again + print(f"reenter pin {pin}") + debug.input(pin) + yield # success + print("success") + debug.press_yes() + + +def _input_flow_change_pin(debug, old_pin, new_pin): + yield # do you want to change pin? + debug.press_yes() + yield # enter current pin + debug.input(old_pin) + yield # enter new pin + debug.input(new_pin) + yield # enter new pin again + debug.input(new_pin) + yield # success + debug.press_yes() + + +def _input_flow_clear_pin(debug, old_pin): + yield # do you want to remove pin? + debug.press_yes() + yield # enter current pin + debug.input(old_pin) + yield # success + debug.press_yes() + + +def _input_flow_set_wipe_code(debug, pin, wipe_code): + yield # do you want to set/change the wipe_code? + debug.press_yes() + if pin is not None: + yield # enter current pin + debug.input(pin) + yield # enter new wipe code + debug.input(wipe_code) + yield # enter new wipe code again + debug.input(wipe_code) + yield # success + debug.press_yes() + + +def _input_flow_remove_wipe_code(debug, pin): + yield # do you want to remove wipe code? + debug.press_yes() + yield # enter current pin + debug.input(pin) + yield # success + debug.press_yes() + + +def _check_wipe_code(client, pin, wipe_code): + client.clear_session() + assert client.features.wipe_code_protection is True + + # Try to change the PIN to the current wipe code value. The operation should fail. + with client, pytest.raises(TrezorFailure): + client.set_expected_responses( + [messages.ButtonRequest()] * 5 + + [messages.Failure(code=messages.FailureType.PinInvalid)] + ) + client.set_input_flow(_input_flow_change_pin(client.debug, pin, wipe_code)) + device.change_pin(client) + + +@pytest.mark.setup_client(pin=PIN4) +def test_set_remove_wipe_code(client): + # Test set wipe code. + assert client.features.wipe_code_protection is False + + with client: + client.set_expected_responses( + [messages.ButtonRequest()] * 5 + [messages.Success(), messages.Features()] + ) + client.set_input_flow(_input_flow_set_wipe_code(client.debug, PIN4, WIPE_CODE4)) + + device.change_wipe_code(client) + + client.init_device() + assert client.features.wipe_code_protection is True + _check_wipe_code(client, PIN4, WIPE_CODE4) + + # Test change wipe code. + with client: + client.set_expected_responses( + [messages.ButtonRequest()] * 5 + [messages.Success(), messages.Features()] + ) + client.set_input_flow(_input_flow_set_wipe_code(client.debug, PIN4, WIPE_CODE6)) + + device.change_wipe_code(client) + + client.init_device() + assert client.features.wipe_code_protection is True + _check_wipe_code(client, PIN4, WIPE_CODE6) + + # Test remove wipe code. + with client: + client.set_expected_responses( + [messages.ButtonRequest()] * 3 + [messages.Success(), messages.Features()] + ) + client.set_input_flow(_input_flow_clear_pin(client.debug, PIN4)) + + device.change_wipe_code(client, remove=True) + + client.init_device() + assert client.features.wipe_code_protection is False + + +def test_set_wipe_code_mismatch(client): + # Let's set a wipe code. + def input_flow(): + yield # do you want to set the wipe code? + client.debug.press_yes() + yield # enter new wipe code + client.debug.input(WIPE_CODE4) + yield # enter new wipe code again (but different) + client.debug.input(WIPE_CODE6) + + # failed retry + yield # enter new wipe code + client.cancel() + + with client, pytest.raises(Cancelled): + client.set_expected_responses( + [messages.ButtonRequest()] * 4 + [messages.Failure()] + ) + client.set_input_flow(input_flow) + + device.change_wipe_code(client) + + # Check that there's still no wipe code protection now + client.init_device() + assert client.features.wipe_code_protection is False + + +@pytest.mark.setup_client(pin=PIN4) +def test_set_wipe_code_to_pin(client): + def input_flow(): + yield # do you want to set the wipe code? + client.debug.press_yes() + yield # enter current pin + client.debug.input(PIN4) + yield # enter new wipe code (same as PIN) + client.debug.input(PIN4) + + # failed retry + yield # enter new wipe code + client.debug.input(WIPE_CODE4) + yield # enter new wipe code again + client.debug.input(WIPE_CODE4) + yield # success + client.debug.press_yes() + + with client: + client.set_expected_responses( + [messages.ButtonRequest()] * 6 + [messages.Success(), messages.Features()] + ) + client.set_input_flow(input_flow) + + device.change_wipe_code(client) + + client.init_device() + assert client.features.wipe_code_protection is True + _check_wipe_code(client, PIN4, WIPE_CODE4) + + +def test_set_pin_to_wipe_code(client): + # Set wipe code. + with client: + client.set_expected_responses( + [messages.ButtonRequest()] * 4 + [messages.Success(), messages.Features()] + ) + client.set_input_flow(_input_flow_set_wipe_code(client.debug, None, WIPE_CODE4)) + + device.change_wipe_code(client) + + # Try to set the PIN to the current wipe code value. + with client, pytest.raises(TrezorFailure): + client.set_expected_responses( + [messages.ButtonRequest()] * 4 + + [messages.Failure(code=messages.FailureType.PinInvalid)] + ) + client.set_input_flow(_input_flow_set_pin(client.debug, WIPE_CODE4)) + device.change_pin(client) + + +@pytest.mark.setup_client(pin=PIN4) +def test_wipe_code_activate(client): + import time + + device_id = client.features.device_id + + # Set wipe code. + with client: + client.set_expected_responses( + [messages.ButtonRequest()] * 5 + [messages.Success(), messages.Features()] + ) + client.set_input_flow(_input_flow_set_wipe_code(client.debug, PIN4, WIPE_CODE4)) + + device.change_wipe_code(client) + + # Try to change the PIN. + ret = client.call_raw(messages.ChangePin(remove=False)) + + # Confirm change PIN. + assert isinstance(ret, messages.ButtonRequest) + client.debug.press_yes() + ret = client.call_raw(messages.ButtonAck()) + + # Enter the wipe code instead of the current PIN + assert ret == messages.ButtonRequest(code=messages.ButtonRequestType.Other) + client.debug.input(WIPE_CODE4) + client._raw_write(messages.ButtonAck()) + + # Allow the device to display wipe code popup and restart. + time.sleep(7) + + # Check that the device has been wiped. + client.init_device() + assert client.features.initialized is False + assert client.features.pin_protection is False + assert client.features.wipe_code_protection is False + assert client.features.device_id != device_id From d0d3ad2912061c6a7bce20bc8242879e94fc6cea Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Mon, 2 Dec 2019 14:29:04 +0100 Subject: [PATCH 12/14] fixup! storage: Change secequal32() to use length in bytes instead of length in words. --- storage/storage.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/storage/storage.c b/storage/storage.c index 6a8c7fd99..a65c2372e 100644 --- a/storage/storage.c +++ b/storage/storage.c @@ -188,6 +188,9 @@ static secbool secequal(const void *ptr1, const void *ptr2, size_t n) { static secbool secequal32(const void *ptr1, const void *ptr2, size_t n) { assert(n % sizeof(uint32_t) == 0); + assert((uintptr_t)ptr1 % sizeof(uint32_t) == 0); + assert((uintptr_t)ptr2 % sizeof(uint32_t) == 0); + size_t wn = n / sizeof(uint32_t); const uint32_t *p1 = (const uint32_t *)ptr1; const uint32_t *p2 = (const uint32_t *)ptr2; From 579244b068367735374740db12bc9bed4f9502b8 Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Mon, 2 Dec 2019 15:52:51 +0100 Subject: [PATCH 13/14] storage: When wiping, erase the active sector first. --- storage/norcow.c | 14 +++++++++----- storage/tests/python/src/norcow.py | 6 +++++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/storage/norcow.c b/storage/norcow.c index d13383ff9..2ead2d1ba 100644 --- a/storage/norcow.c +++ b/storage/norcow.c @@ -302,6 +302,7 @@ void norcow_init(uint32_t *norcow_version) { flash_init(); secbool found = secfalse; *norcow_version = 0; + norcow_active_sector = 0; // detect active sector - starts with magic and has highest version for (uint8_t i = 0; i < NORCOW_SECTOR_COUNT; i++) { uint32_t offset = 0; @@ -332,13 +333,16 @@ void norcow_init(uint32_t *norcow_version) { * Wipe the storage */ void norcow_wipe(void) { - erase_sector(0, sectrue); - for (uint8_t i = 1; i < NORCOW_SECTOR_COUNT; i++) { - erase_sector(i, secfalse); + // Erase the active sector first, because it contains sensitive data. + erase_sector(norcow_active_sector, sectrue); + + for (uint8_t i = 0; i < NORCOW_SECTOR_COUNT; i++) { + if (i != norcow_active_sector) { + erase_sector(i, secfalse); + } } - norcow_active_sector = 0; norcow_active_version = NORCOW_VERSION; - norcow_write_sector = 0; + norcow_write_sector = norcow_active_sector; norcow_free_offset = NORCOW_STORAGE_START; } diff --git a/storage/tests/python/src/norcow.py b/storage/tests/python/src/norcow.py index 3a8baca33..f3c9510d5 100644 --- a/storage/tests/python/src/norcow.py +++ b/storage/tests/python/src/norcow.py @@ -15,6 +15,7 @@ def align4_data(data): class Norcow: def __init__(self): self.sectors = None + self.active_sector = 0 def init(self): if self.sectors: @@ -26,7 +27,10 @@ class Norcow: else: self.wipe() - def wipe(self, sector: int = 0): + def wipe(self, sector: int = None): + if sector is None: + sector = self.active_sector + self.sectors = [ bytearray([0xFF] * consts.NORCOW_SECTOR_SIZE) for _ in range(consts.NORCOW_SECTOR_COUNT) From de96e675e5ab063e1492e9ee609ff97892c95c4d Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Mon, 2 Dec 2019 16:06:36 +0100 Subject: [PATCH 14/14] fixup! storage: Implement storage_change_wipe_code(). --- storage/storage.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/storage/storage.c b/storage/storage.c index a65c2372e..2e9845835 100644 --- a/storage/storage.c +++ b/storage/storage.c @@ -958,6 +958,7 @@ static secbool unlock(uint32_t pin, const uint8_t *ext_salt) { // Get the pin failure counter uint32_t ctr = 0; if (sectrue != pin_get_fails(&ctr)) { + memzero(&pin, sizeof(pin)); return secfalse; } @@ -997,6 +998,7 @@ static secbool unlock(uint32_t pin, const uint8_t *ext_salt) { if (sectrue != initialized || 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"); return secfalse; }