diff --git a/components/crypto_asm/include/crypto_asm.h b/components/crypto_asm/include/crypto_asm.h index 54f0b71..cc970b0 100644 --- a/components/crypto_asm/include/crypto_asm.h +++ b/components/crypto_asm/include/crypto_asm.h @@ -28,6 +28,10 @@ uint32_t ct_select32(uint32_t a, uint32_t b, uint32_t condition); void ct_select_bytes(void *out, const void *a, const void *b, size_t len, uint32_t condition); void ct_cswap32(uint32_t *a, uint32_t *b, uint32_t condition); +static inline int secure_memcmp(const void *a, const void *b, size_t len) { + return ct_compare(a, b, len); +} + #ifdef __cplusplus } #endif diff --git a/main/storage_crypto.c b/main/storage_crypto.c index 925cdc7..3842301 100644 --- a/main/storage_crypto.c +++ b/main/storage_crypto.c @@ -5,9 +5,11 @@ #include "error_codes.h" #include "random_utils.h" #include "crypto_asm.h" +#include "secure_element.h" #include #include -#include +#include +#include #include #include @@ -17,8 +19,8 @@ #include "esp_timer.h" #include "nvs_flash.h" #include "nvs.h" -static uint32_t get_time_ms(void) { - return (uint32_t)(esp_timer_get_time() / 1000); +static uint64_t get_time_ms(void) { + return (uint64_t)(esp_timer_get_time() / 1000); } #else #include @@ -31,58 +33,246 @@ static uint32_t get_time_ms(void) { fprintf(stderr, "E (%s): ", tag); \ fprintf(stderr, __VA_ARGS__); \ fprintf(stderr, "\n") -static uint32_t get_time_ms(void) { +static uint64_t get_time_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); - return (uint32_t)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000); + return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; } #endif #define TAG "storage_crypto" #define DEVICE_ID_SIZE 6 -#define PIN_RATE_LIMIT_WINDOW_MS 60000 -#define PIN_RATE_LIMIT_MAX 3 -#define PIN_LOCKOUT_MS 300000 -#define PIN_LOCKOUT_FAILURE_THRESH 5 +#define PIN_MAX_ATTEMPTS 21 +#define PIN_PBKDF2_ITERATIONS 100000 +#define PIN_PBKDF2_SALT_LEN 16 #define NVS_NAMESPACE "pin_rl" #define NVS_KEY_FAILURES "failures" #define NVS_KEY_LOCKOUT "lockout" +#define NVS_KEY_BRICKED "bricked" +#define NVS_KEY_SALT "salt" +#define NVS_KEY_HMAC "hmac" + +#define SE_SLOT_PIN_STATE 0 +#define SE_SLOT_HMAC_KEY 1 +#define HMAC_KEY_SIZE 32 + +typedef struct __attribute__((packed)) { + uint8_t magic[4]; + uint8_t failed_attempts; + uint64_t lockout_deadline; + uint8_t bricked; + uint8_t reserved[2]; +} pin_state_t; + +#define PIN_STATE_MAGIC "PIN\0" +#define PIN_STATE_SIZE sizeof(pin_state_t) static uint8_t storage_key[STORAGE_CRYPTO_KEY_SIZE]; static bool key_initialized = false; +static uint8_t pin_salt[PIN_PBKDF2_SALT_LEN]; +static bool salt_initialized = false; + +static pin_state_t pin_state; +static bool pin_state_loaded = false; +static bool se_available = false; + +static int get_device_id(uint8_t device_id[DEVICE_ID_SIZE]); + +static int get_hmac_secret_key(uint8_t key_out[HMAC_KEY_SIZE]) { + if (se_available) { + uint8_t se_data[SE_SLOT_SIZE]; + if (se_read_slot(SE_SLOT_HMAC_KEY, se_data, sizeof(se_data)) == SE_OK) { + bool key_set = false; + for (size_t i = 0; i < HMAC_KEY_SIZE; i++) { + if (se_data[i] != 0) { + key_set = true; + break; + } + } + if (key_set) { + memcpy(key_out, se_data, HMAC_KEY_SIZE); + secure_memzero(se_data, sizeof(se_data)); + return 0; + } + } + if (rng_fill_checked(key_out, HMAC_KEY_SIZE) != 0) { + return -1; + } + uint8_t se_write_data[SE_SLOT_SIZE]; + memset(se_write_data, 0, sizeof(se_write_data)); + memcpy(se_write_data, key_out, HMAC_KEY_SIZE); + se_write_slot(SE_SLOT_HMAC_KEY, se_write_data, sizeof(se_write_data)); + secure_memzero(se_write_data, sizeof(se_write_data)); + return 0; + } -static uint32_t pin_attempt_times[PIN_RATE_LIMIT_MAX]; -static uint8_t pin_attempt_count = 0; -static uint32_t pin_lockout_until = 0; -static uint8_t pin_consecutive_failures = 0; - -static void load_rate_limit_state(void) { #ifdef ESP_PLATFORM - nvs_handle_t handle; - if (nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle) == ESP_OK) { - uint8_t failures = 0; - uint32_t lockout = 0; - nvs_get_u8(handle, NVS_KEY_FAILURES, &failures); - nvs_get_u32(handle, NVS_KEY_LOCKOUT, &lockout); - nvs_close(handle); - pin_consecutive_failures = failures; - pin_lockout_until = lockout; + uint8_t serial[SE_SERIAL_SIZE]; + uint8_t device_id[DEVICE_ID_SIZE]; + if (get_device_id(device_id) != 0) { + return -1; + } + if (se_get_serial(serial) == SE_OK) { + mbedtls_sha256_context ctx; + mbedtls_sha256_init(&ctx); + mbedtls_sha256_starts(&ctx, 0); + mbedtls_sha256_update(&ctx, serial, SE_SERIAL_SIZE); + mbedtls_sha256_update(&ctx, device_id, DEVICE_ID_SIZE); + mbedtls_sha256_finish(&ctx, key_out); + mbedtls_sha256_free(&ctx); + secure_memzero(serial, sizeof(serial)); + secure_memzero(device_id, sizeof(device_id)); + return 0; + } + mbedtls_sha256_context ctx; + mbedtls_sha256_init(&ctx); + mbedtls_sha256_starts(&ctx, 0); + mbedtls_sha256_update(&ctx, device_id, DEVICE_ID_SIZE); + mbedtls_sha256_finish(&ctx, key_out); + mbedtls_sha256_free(&ctx); + secure_memzero(device_id, sizeof(device_id)); + return 0; +#else + uint8_t device_id[DEVICE_ID_SIZE]; + if (get_device_id(device_id) != 0) { + return -1; } + mbedtls_sha256_context ctx; + mbedtls_sha256_init(&ctx); + mbedtls_sha256_starts(&ctx, 0); + mbedtls_sha256_update(&ctx, device_id, DEVICE_ID_SIZE); + mbedtls_sha256_finish(&ctx, key_out); + mbedtls_sha256_free(&ctx); + secure_memzero(device_id, sizeof(device_id)); + return 0; #endif } -static void save_rate_limit_state(void) { +static int compute_state_hmac(const pin_state_t *state, uint8_t hmac_out[32]) { + uint8_t secret_key[HMAC_KEY_SIZE]; + if (get_hmac_secret_key(secret_key) != 0) { + secure_memzero(secret_key, sizeof(secret_key)); + return -1; + } + + uint8_t ipad[64], opad[64]; + uint8_t key_padded[64]; + memset(key_padded, 0, sizeof(key_padded)); + memcpy(key_padded, secret_key, HMAC_KEY_SIZE); + secure_memzero(secret_key, sizeof(secret_key)); + + for (int i = 0; i < 64; i++) { + ipad[i] = key_padded[i] ^ 0x36; + opad[i] = key_padded[i] ^ 0x5c; + } + + mbedtls_sha256_context ctx; + uint8_t inner_hash[32]; + + mbedtls_sha256_init(&ctx); + mbedtls_sha256_starts(&ctx, 0); + mbedtls_sha256_update(&ctx, ipad, 64); + mbedtls_sha256_update(&ctx, (const uint8_t *)state, PIN_STATE_SIZE); + mbedtls_sha256_finish(&ctx, inner_hash); + mbedtls_sha256_free(&ctx); + + mbedtls_sha256_init(&ctx); + mbedtls_sha256_starts(&ctx, 0); + mbedtls_sha256_update(&ctx, opad, 64); + mbedtls_sha256_update(&ctx, inner_hash, 32); + mbedtls_sha256_finish(&ctx, hmac_out); + mbedtls_sha256_free(&ctx); + + secure_memzero(ipad, sizeof(ipad)); + secure_memzero(opad, sizeof(opad)); + secure_memzero(key_padded, sizeof(key_padded)); + secure_memzero(inner_hash, sizeof(inner_hash)); + return 0; +} + +static void load_pin_state(void) { + if (pin_state_loaded) { + return; + } + + memcpy(pin_state.magic, PIN_STATE_MAGIC, 4); + pin_state.failed_attempts = 0; + pin_state.lockout_deadline = 0; + pin_state.bricked = 0; + memset(pin_state.reserved, 0, sizeof(pin_state.reserved)); + + if (se_init() == SE_OK && se_is_provisioned()) { + se_available = true; + uint8_t se_data[SE_SLOT_SIZE]; + if (se_read_slot(SE_SLOT_PIN_STATE, se_data, sizeof(se_data)) == SE_OK) { + if (memcmp(se_data, PIN_STATE_MAGIC, 4) == 0) { + memcpy(&pin_state, se_data, PIN_STATE_SIZE); + } + } + } else { #ifdef ESP_PLATFORM - nvs_handle_t handle; - if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle) == ESP_OK) { - nvs_set_u8(handle, NVS_KEY_FAILURES, pin_consecutive_failures); - nvs_set_u32(handle, NVS_KEY_LOCKOUT, pin_lockout_until); - nvs_commit(handle); - nvs_close(handle); + nvs_handle_t handle; + if (nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle) == ESP_OK) { + uint8_t failures = 0; + uint64_t lockout = 0; + uint8_t bricked = 0; + uint8_t stored_hmac[32]; + size_t hmac_len = sizeof(stored_hmac); + + nvs_get_u8(handle, NVS_KEY_FAILURES, &failures); + nvs_get_u64(handle, NVS_KEY_LOCKOUT, &lockout); + nvs_get_u8(handle, NVS_KEY_BRICKED, &bricked); + + pin_state_t temp_state; + memcpy(temp_state.magic, PIN_STATE_MAGIC, 4); + temp_state.failed_attempts = failures; + temp_state.lockout_deadline = lockout; + temp_state.bricked = bricked; + memset(temp_state.reserved, 0, sizeof(temp_state.reserved)); + + if (nvs_get_blob(handle, NVS_KEY_HMAC, stored_hmac, &hmac_len) == ESP_OK && + hmac_len == 32) { + uint8_t computed_hmac[32]; + if (compute_state_hmac(&temp_state, computed_hmac) == 0 && + secure_memcmp(stored_hmac, computed_hmac, 32) == 0) { + memcpy(&pin_state, &temp_state, sizeof(pin_state)); + } + } + nvs_close(handle); + } +#endif } + + pin_state_loaded = true; +} + +static void save_pin_state(void) { + if (se_available) { + uint8_t se_data[SE_SLOT_SIZE]; + memset(se_data, 0, sizeof(se_data)); + memcpy(se_data, &pin_state, PIN_STATE_SIZE); + se_write_slot(SE_SLOT_PIN_STATE, se_data, sizeof(se_data)); + } else { +#ifdef ESP_PLATFORM + nvs_handle_t handle; + if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle) == ESP_OK) { + uint8_t hmac[32]; + if (compute_state_hmac(&pin_state, hmac) != 0) { + nvs_close(handle); + return; + } + + nvs_set_u8(handle, NVS_KEY_FAILURES, pin_state.failed_attempts); + nvs_set_u64(handle, NVS_KEY_LOCKOUT, pin_state.lockout_deadline); + nvs_set_u8(handle, NVS_KEY_BRICKED, pin_state.bricked); + nvs_set_blob(handle, NVS_KEY_HMAC, hmac, sizeof(hmac)); + nvs_commit(handle); + nvs_close(handle); + } #endif + } } static int get_device_id(uint8_t device_id[DEVICE_ID_SIZE]) { @@ -116,94 +306,210 @@ static int get_device_id(uint8_t device_id[DEVICE_ID_SIZE]) { #endif } -// The "v1" in the salt is the HKDF key derivation version, not the storage format version. -// Do not change this value - it would invalidate all existing encrypted data. -static const uint8_t HKDF_SALT[] = "keep-esp32-share-storage-v1"; +static int init_or_load_salt(void) { + if (salt_initialized) { + return 0; + } + +#ifdef ESP_PLATFORM + nvs_handle_t handle; + if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle) == ESP_OK) { + size_t salt_len = PIN_PBKDF2_SALT_LEN; + if (nvs_get_blob(handle, NVS_KEY_SALT, pin_salt, &salt_len) == ESP_OK && + salt_len == PIN_PBKDF2_SALT_LEN) { + salt_initialized = true; + nvs_close(handle); + return 0; + } + if (rng_fill_checked(pin_salt, PIN_PBKDF2_SALT_LEN) != 0) { + nvs_close(handle); + return -1; + } + nvs_set_blob(handle, NVS_KEY_SALT, pin_salt, PIN_PBKDF2_SALT_LEN); + nvs_commit(handle); + nvs_close(handle); + salt_initialized = true; + return 0; + } + return -1; +#else + if (rng_fill_checked(pin_salt, PIN_PBKDF2_SALT_LEN) != 0) { + return -1; + } + salt_initialized = true; + return 0; +#endif +} + static const uint8_t HKDF_INFO[] = "share-encryption-key"; static int derive_key(const uint8_t *device_id, size_t device_id_len, const uint8_t *pin, size_t pin_len, uint8_t *key_out) { - if (device_id_len > DEVICE_ID_SIZE) { + if (device_id_len > DEVICE_ID_SIZE || !pin || pin_len == 0) { return -1; } - uint8_t ikm[DEVICE_ID_SIZE + STORAGE_CRYPTO_MAX_PIN_LEN]; - size_t ikm_len = device_id_len; - memcpy(ikm, device_id, device_id_len); + if (init_or_load_salt() != 0) { + return -1; + } + + uint8_t pbkdf2_salt[PIN_PBKDF2_SALT_LEN + DEVICE_ID_SIZE]; + memcpy(pbkdf2_salt, pin_salt, PIN_PBKDF2_SALT_LEN); + memcpy(pbkdf2_salt + PIN_PBKDF2_SALT_LEN, device_id, device_id_len); + + uint8_t stretched[32]; + int ret = mbedtls_pkcs5_pbkdf2_hmac_ext(MBEDTLS_MD_SHA256, pin, pin_len, pbkdf2_salt, + sizeof(pbkdf2_salt), PIN_PBKDF2_ITERATIONS, + sizeof(stretched), stretched); + secure_memzero(pbkdf2_salt, sizeof(pbkdf2_salt)); - if (pin && pin_len > 0) { - size_t copy_len = - pin_len < STORAGE_CRYPTO_MAX_PIN_LEN ? pin_len : STORAGE_CRYPTO_MAX_PIN_LEN; - memcpy(ikm + device_id_len, pin, copy_len); - ikm_len += copy_len; + if (ret != 0) { + secure_memzero(stretched, sizeof(stretched)); + return -1; } - int ret = mbedtls_hkdf(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), HKDF_SALT, - sizeof(HKDF_SALT) - 1, ikm, ikm_len, HKDF_INFO, sizeof(HKDF_INFO) - 1, - key_out, STORAGE_CRYPTO_KEY_SIZE); + ret = mbedtls_hkdf(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), NULL, 0, stretched, + sizeof(stretched), HKDF_INFO, sizeof(HKDF_INFO) - 1, key_out, + STORAGE_CRYPTO_KEY_SIZE); + secure_memzero(stretched, sizeof(stretched)); - secure_memzero(ikm, sizeof(ikm)); return (ret == 0) ? 0 : -1; } -static bool rate_limit_loaded = false; +static uint32_t get_delay_ms(uint8_t attempts) { + if (attempts <= 3) { + return 0; + } + if (attempts <= 6) { + return 15 * 1000; + } + if (attempts <= 9) { + return 60 * 1000; + } + if (attempts <= (PIN_MAX_ATTEMPTS - 1)) { + return 15 * 60 * 1000; + } + return UINT32_MAX; +} + +static void wipe_secrets(void) { + secure_memzero(storage_key, sizeof(storage_key)); + key_initialized = false; + +#ifdef ESP_PLATFORM + nvs_handle_t handle; + if (nvs_open("storage", NVS_READWRITE, &handle) == ESP_OK) { + nvs_erase_all(handle); + nvs_commit(handle); + nvs_close(handle); + } +#endif + + if (se_available) { + uint8_t zeros[SE_SLOT_SIZE]; + memset(zeros, 0, sizeof(zeros)); + for (int i = 1; i < SE_SLOT_COUNT; i++) { + se_write_slot(i, zeros, sizeof(zeros)); + } + } +} int storage_crypto_check_rate_limit(void) { - if (!rate_limit_loaded) { - load_rate_limit_state(); - rate_limit_loaded = true; + load_pin_state(); + + if (pin_state.bricked) { + return ERR_PIN_BRICKED; } - uint32_t now = get_time_ms(); + if (pin_state.failed_attempts >= PIN_MAX_ATTEMPTS) { + return ERR_PIN_BRICKED; + } - if (pin_lockout_until > 0) { - if ((int32_t)(now - pin_lockout_until) < 0) { - return ERR_PIN_LOCKED; - } - pin_lockout_until = 0; - pin_consecutive_failures = 0; - save_rate_limit_state(); + uint32_t delay_ms = get_delay_ms(pin_state.failed_attempts); + if (delay_ms == UINT32_MAX) { + return ERR_PIN_BRICKED; } - int recent = 0; - for (int i = 0; i < pin_attempt_count; i++) { - if ((now - pin_attempt_times[i]) < PIN_RATE_LIMIT_WINDOW_MS) { - recent++; + if (pin_state.lockout_deadline > 0) { + uint64_t now = get_time_ms(); + if (now < pin_state.lockout_deadline) { + return ERR_PIN_MUST_WAIT; } } - if (recent >= PIN_RATE_LIMIT_MAX) { - return ERR_PIN_MUST_WAIT; - } return 0; } void storage_crypto_record_attempt(bool success) { - uint32_t now = get_time_ms(); - - if (pin_attempt_count < PIN_RATE_LIMIT_MAX) { - pin_attempt_times[pin_attempt_count++] = now; - } else { - memmove(pin_attempt_times, pin_attempt_times + 1, - (PIN_RATE_LIMIT_MAX - 1) * sizeof(pin_attempt_times[0])); - pin_attempt_times[PIN_RATE_LIMIT_MAX - 1] = now; - } + load_pin_state(); if (success) { - pin_consecutive_failures = 0; + pin_state.failed_attempts = 0; + pin_state.lockout_deadline = 0; } else { - if (pin_consecutive_failures < UINT8_MAX) { - pin_consecutive_failures++; + if (pin_state.failed_attempts < UINT8_MAX) { + pin_state.failed_attempts++; + } + + if (pin_state.failed_attempts >= PIN_MAX_ATTEMPTS) { + ESP_LOGE(TAG, "Max PIN attempts exceeded - wiping device"); + pin_state.bricked = 1; + save_pin_state(); + wipe_secrets(); + return; } - if (pin_consecutive_failures >= PIN_LOCKOUT_FAILURE_THRESH) { - pin_lockout_until = now + PIN_LOCKOUT_MS; - ESP_LOGW(TAG, "PIN lockout activated for %d seconds", PIN_LOCKOUT_MS / 1000); + + uint32_t delay_ms = get_delay_ms(pin_state.failed_attempts); + if (delay_ms > 0 && delay_ms != UINT32_MAX) { + pin_state.lockout_deadline = get_time_ms() + delay_ms; + ESP_LOGW(TAG, "PIN attempt %d/%d - next attempt in %lu seconds", + pin_state.failed_attempts, PIN_MAX_ATTEMPTS, (unsigned long)(delay_ms / 1000)); + } else { + pin_state.lockout_deadline = 0; } } - save_rate_limit_state(); + save_pin_state(); +} + +uint8_t storage_crypto_get_attempts(void) { + load_pin_state(); + return pin_state.failed_attempts; +} + +uint8_t storage_crypto_get_max_attempts(void) { + return PIN_MAX_ATTEMPTS; +} + +uint32_t storage_crypto_get_delay_remaining(void) { + load_pin_state(); + + if (pin_state.bricked || pin_state.failed_attempts >= PIN_MAX_ATTEMPTS) { + return UINT32_MAX; + } + + if (pin_state.lockout_deadline == 0) { + return 0; + } + + uint64_t now = get_time_ms(); + if (now >= pin_state.lockout_deadline) { + return 0; + } + return (uint32_t)(pin_state.lockout_deadline - now); +} + +bool storage_crypto_is_bricked(void) { + load_pin_state(); + return pin_state.bricked != 0 || pin_state.failed_attempts >= PIN_MAX_ATTEMPTS; } int storage_crypto_init(const char *pin) { + load_pin_state(); + + if (pin_state.bricked || pin_state.failed_attempts >= PIN_MAX_ATTEMPTS) { + return ERR_PIN_BRICKED; + } + int rate_limit = storage_crypto_check_rate_limit(); if (rate_limit != 0) { return rate_limit; @@ -225,9 +531,6 @@ int storage_crypto_init(const char *pin) { if (ret == 0) { key_initialized = true; - storage_crypto_record_attempt(true); - } else { - storage_crypto_record_attempt(false); } return ret; } @@ -297,10 +600,29 @@ int storage_crypto_decrypt(const uint8_t *ciphertext, size_t ciphertext_len, con #ifdef UNIT_TEST void storage_crypto_reset_rate_limit(void) { - pin_attempt_count = 0; - pin_lockout_until = 0; - pin_consecutive_failures = 0; - memset(pin_attempt_times, 0, sizeof(pin_attempt_times)); - rate_limit_loaded = false; + memcpy(pin_state.magic, PIN_STATE_MAGIC, 4); + pin_state.failed_attempts = 0; + pin_state.lockout_deadline = 0; + pin_state.bricked = 0; + memset(pin_state.reserved, 0, sizeof(pin_state.reserved)); + pin_state_loaded = true; + salt_initialized = false; + se_available = false; +} + +void storage_crypto_set_attempts_for_test(uint8_t attempts) { + load_pin_state(); + pin_state.failed_attempts = attempts; + uint32_t delay_ms = get_delay_ms(attempts); + if (delay_ms > 0 && delay_ms != UINT32_MAX) { + pin_state.lockout_deadline = get_time_ms() + delay_ms; + } else { + pin_state.lockout_deadline = 0; + } +} + +void storage_crypto_set_bricked_for_test(bool bricked) { + load_pin_state(); + pin_state.bricked = bricked ? 1 : 0; } #endif diff --git a/main/storage_crypto.h b/main/storage_crypto.h index 5eeeb36..51e4e43 100644 --- a/main/storage_crypto.h +++ b/main/storage_crypto.h @@ -20,6 +20,11 @@ void storage_crypto_clear(void); int storage_crypto_check_rate_limit(void); void storage_crypto_record_attempt(bool success); +uint8_t storage_crypto_get_attempts(void); +uint8_t storage_crypto_get_max_attempts(void); +uint32_t storage_crypto_get_delay_remaining(void); +bool storage_crypto_is_bricked(void); + int storage_crypto_encrypt(const uint8_t *plaintext, size_t plaintext_len, const uint8_t *aad, size_t aad_len, uint8_t nonce[STORAGE_CRYPTO_NONCE_SIZE], uint8_t *ciphertext, uint8_t tag[STORAGE_CRYPTO_TAG_SIZE]); @@ -28,4 +33,10 @@ int storage_crypto_decrypt(const uint8_t *ciphertext, size_t ciphertext_len, con size_t aad_len, const uint8_t nonce[STORAGE_CRYPTO_NONCE_SIZE], const uint8_t tag[STORAGE_CRYPTO_TAG_SIZE], uint8_t *plaintext); +#ifdef UNIT_TEST +void storage_crypto_reset_rate_limit(void); +void storage_crypto_set_attempts_for_test(uint8_t attempts); +void storage_crypto_set_bricked_for_test(bool bricked); +#endif + #endif diff --git a/test/native/CMakeLists.txt b/test/native/CMakeLists.txt index 09672e2..0a82d31 100644 --- a/test/native/CMakeLists.txt +++ b/test/native/CMakeLists.txt @@ -165,3 +165,16 @@ target_include_directories(test_anti_glitch PRIVATE ${MAIN_DIR} ) target_compile_definitions(test_anti_glitch PRIVATE NATIVE_TEST=1) + +find_library(MBEDCRYPTO_LIB mbedcrypto) +if(MBEDCRYPTO_LIB) + add_executable(test_pin_attempt_limit test_pin_attempt_limit.c ${MAIN_DIR}/random_utils.c ${MAIN_DIR}/hw_entropy.c ${MAIN_DIR}/error_codes.c) + target_include_directories(test_pin_attempt_limit PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/mocks + ${MAIN_DIR} + ) + target_compile_definitions(test_pin_attempt_limit PRIVATE NATIVE_TEST=1 UNIT_TEST=1 MOCK_MBEDTLS=1) + target_link_libraries(test_pin_attempt_limit ${MBEDCRYPTO_LIB}) +else() + message(STATUS "Skipping test_pin_attempt_limit (mbedtls not found)") +endif() diff --git a/test/native/mocks/crypto_asm.h b/test/native/mocks/crypto_asm.h index ba478a1..d17bd67 100644 --- a/test/native/mocks/crypto_asm.h +++ b/test/native/mocks/crypto_asm.h @@ -23,6 +23,10 @@ static inline int ct_compare(const void *a, const void *b, size_t len) { return acc; } +static inline int secure_memcmp(const void *a, const void *b, size_t len) { + return ct_compare(a, b, len); +} + static inline int ct_is_zero(const void *ptr, size_t len) { const uint8_t *p = ptr; uint8_t acc = 0; diff --git a/test/native/mocks/storage_crypto.h b/test/native/mocks/storage_crypto.h index 6cc3dd7..b6a2044 100644 --- a/test/native/mocks/storage_crypto.h +++ b/test/native/mocks/storage_crypto.h @@ -16,6 +16,8 @@ static bool mock_crypto_initialized = true; static int mock_rate_limit_result = 0; +static uint8_t mock_pin_attempts = 0; +static bool mock_is_bricked = false; static uint8_t mock_last_encrypt_aad[128]; static size_t mock_last_encrypt_aad_len = 0; @@ -37,9 +39,27 @@ static inline void storage_crypto_record_attempt(bool success) { (void)success; } +static inline uint8_t storage_crypto_get_attempts(void) { + return mock_pin_attempts; +} + +static inline uint8_t storage_crypto_get_max_attempts(void) { + return 21; +} + +static inline uint32_t storage_crypto_get_delay_remaining(void) { + return 0; +} + +static inline bool storage_crypto_is_bricked(void) { + return mock_is_bricked; +} + #ifdef UNIT_TEST static inline void storage_crypto_reset_rate_limit(void) { mock_rate_limit_result = 0; + mock_pin_attempts = 0; + mock_is_bricked = false; } #endif diff --git a/test/native/test_pin_attempt_limit.c b/test/native/test_pin_attempt_limit.c new file mode 100644 index 0000000..082ca5f --- /dev/null +++ b/test/native/test_pin_attempt_limit.c @@ -0,0 +1,355 @@ +// SPDX-FileCopyrightText: © 2026 PrivKey LLC +// SPDX-License-Identifier: AGPL-3.0-or-later + +#include +#include +#include +#include +#include + +#define SECURE_ELEMENT_H +#define MOCK_SECURE_ELEMENT_H +#define SE_SLOT_COUNT 16 +#define SE_SLOT_SIZE 72 +#define SE_SERIAL_SIZE 9 + +typedef enum { + SE_OK = 0, + SE_ERR_INVALID_PARAM = -1, + SE_ERR_NOT_PROVISIONED = -2, + SE_ERR_COMM_FAIL = -3, + SE_ERR_LOCKED = -4, + SE_ERR_NOT_INITIALIZED = -5 +} se_status_t; + +static uint8_t mock_se_slots[SE_SLOT_COUNT][SE_SLOT_SIZE]; +static bool mock_se_available = false; + +se_status_t se_init(void) { + if (mock_se_available) { + return SE_OK; + } + return SE_ERR_NOT_PROVISIONED; +} + +bool se_is_provisioned(void) { + return mock_se_available; +} + +se_status_t se_read_slot(uint8_t slot, uint8_t *data, size_t len) { + if (!mock_se_available) { + return SE_ERR_NOT_PROVISIONED; + } + if (slot >= SE_SLOT_COUNT || !data || len > SE_SLOT_SIZE) { + return SE_ERR_INVALID_PARAM; + } + memcpy(data, mock_se_slots[slot], len); + return SE_OK; +} + +se_status_t se_write_slot(uint8_t slot, const uint8_t *data, size_t len) { + if (!mock_se_available) { + return SE_ERR_NOT_PROVISIONED; + } + if (slot >= SE_SLOT_COUNT || !data || len > SE_SLOT_SIZE) { + return SE_ERR_INVALID_PARAM; + } + memcpy(mock_se_slots[slot], data, len); + return SE_OK; +} + +se_status_t se_increment_counter(uint32_t *new_value) { + (void)new_value; + return SE_ERR_NOT_PROVISIONED; +} + +se_status_t se_get_counter(uint32_t *value) { + (void)value; + return SE_ERR_NOT_PROVISIONED; +} + +se_status_t se_get_serial(uint8_t serial[SE_SERIAL_SIZE]) { + (void)serial; + return SE_ERR_NOT_PROVISIONED; +} + +#define UNIT_TEST 1 + +#define STORAGE_CRYPTO_H +#define STORAGE_CRYPTO_KEY_SIZE 32 +#define STORAGE_CRYPTO_NONCE_SIZE 12 +#define STORAGE_CRYPTO_TAG_SIZE 16 +#define STORAGE_CRYPTO_MAX_PIN_LEN 64 + +#include "crypto_asm.h" +#include "random_utils.h" +#include "error_codes.h" + +#include "storage_crypto.c" + +#define TEST(name) printf(" TEST: %s\n", name) +#define PASS() printf(" PASS\n") +#define FAIL(msg) \ + do { \ + printf(" FAIL: %s\n", msg); \ + return 1; \ + } while (0) + +static void reset_test_state(void) { + storage_crypto_reset_rate_limit(); + memset(mock_se_slots, 0, sizeof(mock_se_slots)); + mock_se_available = false; +} + +static int test_initial_state(void) { + TEST("initial state allows PIN attempts"); + reset_test_state(); + + if (storage_crypto_check_rate_limit() != 0) + FAIL("initial state should allow attempts"); + if (storage_crypto_get_attempts() != 0) + FAIL("initial attempts should be 0"); + if (storage_crypto_is_bricked()) + FAIL("should not be bricked initially"); + + PASS(); + return 0; +} + +static int test_delay_schedule(void) { + TEST("exponential delay schedule"); + reset_test_state(); + + if (get_delay_ms(0) != 0) + FAIL("0 attempts should have no delay"); + if (get_delay_ms(3) != 0) + FAIL("3 attempts should have no delay"); + if (get_delay_ms(4) != 15000) + FAIL("4 attempts should have 15s delay"); + if (get_delay_ms(6) != 15000) + FAIL("6 attempts should have 15s delay"); + if (get_delay_ms(7) != 60000) + FAIL("7 attempts should have 1min delay"); + if (get_delay_ms(9) != 60000) + FAIL("9 attempts should have 1min delay"); + if (get_delay_ms(10) != 900000) + FAIL("10 attempts should have 15min delay"); + if (get_delay_ms(12) != 900000) + FAIL("12 attempts should have 15min delay"); + if (get_delay_ms(13) != 900000) + FAIL("13 attempts should have 15min delay"); + if (get_delay_ms(20) != 900000) + FAIL("20 attempts should have 15min delay"); + if (get_delay_ms(21) != UINT32_MAX) + FAIL("21 attempts should return max delay (bricked)"); + + PASS(); + return 0; +} + +static int test_attempt_tracking(void) { + TEST("failed attempt tracking"); + reset_test_state(); + + storage_crypto_record_attempt(false); + if (storage_crypto_get_attempts() != 1) + FAIL("should have 1 attempt"); + + storage_crypto_record_attempt(false); + storage_crypto_record_attempt(false); + if (storage_crypto_get_attempts() != 3) + FAIL("should have 3 attempts"); + + PASS(); + return 0; +} + +static int test_success_resets_counter(void) { + TEST("successful attempt resets counter"); + reset_test_state(); + + for (int i = 0; i < 5; i++) { + storage_crypto_record_attempt(false); + } + if (storage_crypto_get_attempts() != 5) + FAIL("should have 5 attempts"); + + storage_crypto_record_attempt(true); + if (storage_crypto_get_attempts() != 0) + FAIL("success should reset counter to 0"); + + PASS(); + return 0; +} + +static int test_bricking_after_max_attempts(void) { + TEST("device bricks after max attempts"); + reset_test_state(); + + for (int i = 0; i < 20; i++) { + storage_crypto_record_attempt(false); + } + if (storage_crypto_is_bricked()) + FAIL("should not be bricked at 20 attempts"); + + storage_crypto_record_attempt(false); + if (!storage_crypto_is_bricked()) + FAIL("should be bricked at 21 attempts"); + + PASS(); + return 0; +} + +static int test_bricked_device_rejects_attempts(void) { + TEST("bricked device rejects all PIN attempts"); + reset_test_state(); + + storage_crypto_set_bricked_for_test(true); + + if (storage_crypto_check_rate_limit() != ERR_PIN_BRICKED) + FAIL("bricked device should return ERR_PIN_BRICKED"); + if (storage_crypto_init("1234") != ERR_PIN_BRICKED) + FAIL("bricked device should reject PIN init"); + + PASS(); + return 0; +} + +static int test_max_attempts_getter(void) { + TEST("max attempts getter returns correct value"); + reset_test_state(); + + if (storage_crypto_get_max_attempts() != 21) + FAIL("max attempts should be 21"); + + PASS(); + return 0; +} + +static int test_delay_remaining(void) { + TEST("delay remaining calculation"); + reset_test_state(); + + storage_crypto_set_attempts_for_test(5); + + uint32_t delay = storage_crypto_get_delay_remaining(); + if (delay == 0) + FAIL("should have delay remaining after 5 failed attempts"); + if (delay > 15000) + FAIL("delay should not exceed 15s for 5 attempts"); + + PASS(); + return 0; +} + +static int test_se_persistence(void) { + TEST("SE-based state persistence"); + reset_test_state(); + mock_se_available = true; + pin_state_loaded = false; + + storage_crypto_record_attempt(false); + storage_crypto_record_attempt(false); + + if (memcmp(mock_se_slots[0], "PIN", 3) != 0) + FAIL("SE slot should contain PIN magic"); + + PASS(); + return 0; +} + +#ifndef MOCK_MBEDTLS +static int test_pbkdf2_key_derivation(void) { + TEST("PBKDF2 key derivation completes"); + reset_test_state(); + salt_initialized = false; + + uint8_t device_id[6] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}; + uint8_t key[32]; + + int ret = derive_key(device_id, sizeof(device_id), (const uint8_t *)"testpin", 7, key); + if (ret != 0) + FAIL("PBKDF2 key derivation should succeed"); + + PASS(); + return 0; +} + +static int test_hmac_tamper_detection(void) { + TEST("HMAC detects state tampering"); + reset_test_state(); + + pin_state_t state1, state2; + memcpy(state1.magic, "PIN\0", 4); + state1.failed_attempts = 5; + state1.lockout_deadline = 12345; + state1.bricked = 0; + memset(state1.reserved, 0, sizeof(state1.reserved)); + + memcpy(&state2, &state1, sizeof(state1)); + state2.failed_attempts = 0; + + uint8_t hmac1[32], hmac2[32]; + + if (compute_state_hmac(&state1, hmac1) != 0) + FAIL("compute_state_hmac should succeed for state1"); + if (compute_state_hmac(&state2, hmac2) != 0) + FAIL("compute_state_hmac should succeed for state2"); + + if (memcmp(hmac1, hmac2, 32) == 0) + FAIL("different states should produce different HMACs"); + + PASS(); + return 0; +} +#endif + +static int test_attempt_overflow_protection(void) { + TEST("attempt counter overflow protection"); + reset_test_state(); + + pin_state.failed_attempts = 254; + pin_state_loaded = true; + + storage_crypto_record_attempt(false); + if (pin_state.failed_attempts != 255) + FAIL("should increment to 255"); + + storage_crypto_record_attempt(false); + if (pin_state.failed_attempts != 255) + FAIL("should stay at 255 (no overflow)"); + + PASS(); + return 0; +} + +int main(void) { + printf("\n=== PIN Attempt Limiting Tests ===\n\n"); + + int failures = 0; + failures += test_initial_state(); + failures += test_delay_schedule(); + failures += test_attempt_tracking(); + failures += test_success_resets_counter(); + failures += test_bricking_after_max_attempts(); + failures += test_bricked_device_rejects_attempts(); + failures += test_max_attempts_getter(); + failures += test_delay_remaining(); + failures += test_se_persistence(); +#ifndef MOCK_MBEDTLS + failures += test_pbkdf2_key_derivation(); + failures += test_hmac_tamper_detection(); +#else + printf(" SKIPPED: PBKDF2 and HMAC tests (mocked mbedtls)\n"); +#endif + failures += test_attempt_overflow_protection(); + + printf("\n"); + if (failures == 0) { + printf("=== All tests passed ===\n\n"); + return 0; + } else { + printf("=== %d test(s) failed ===\n\n", failures); + return 1; + } +}