From 65fc5e513ae9904e2306a185d8767b90b9fa014c Mon Sep 17 00:00:00 2001 From: zekageri Date: Tue, 24 Feb 2026 12:51:46 +0100 Subject: [PATCH] Add esp-crypto teardown contract and lifecycle tests --- README.md | 9 +++ .../basic_hash_and_aes/basic_hash_and_aes.ino | 5 ++ src/esp_crypto/esp_crypto.cpp | 80 ++++++++++++++++--- src/esp_crypto/esp_crypto.h | 3 + test/test_esp_crypto/test_esp_crypto.cpp | 34 ++++++++ 5 files changed, 118 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 774df04..239f3f4 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,9 @@ void setup() { auto decrypted = ESPCrypto::aesGcmDecrypt(key, gcm.value.iv, gcm.value.ciphertext, gcm.value.tag); (void)decrypted; } + + // Release ESPCrypto runtime caches/state before deep sleep or shutdown paths. + ESPCrypto::deinit(); } void loop() {} @@ -59,6 +62,12 @@ void loop() {} Run `examples/basic_hash_and_aes` via PlatformIO/Arduino to see the full output. +## Lifecycle and Teardown +- `ESPCrypto` is static-style, so there is no instance destructor to release global runtime state. +- Call `ESPCrypto::deinit()` when your app no longer needs crypto helpers (for example before deep sleep, app shutdown, or full subsystem restart). +- `deinit()` is safe before any crypto call and safe to call repeatedly. +- Use `ESPCrypto::isInitialized()` to check whether runtime state (policy/caches/counters) is currently active. + ### Key management and device-bound helpers Cache parsed keys, rotate aliases, and derive symmetric keys without shipping long-lived secrets in firmware: diff --git a/examples/basic_hash_and_aes/basic_hash_and_aes.ino b/examples/basic_hash_and_aes/basic_hash_and_aes.ino index c4c096c..f10bc32 100644 --- a/examples/basic_hash_and_aes/basic_hash_and_aes.ino +++ b/examples/basic_hash_and_aes/basic_hash_and_aes.ino @@ -28,6 +28,7 @@ void setup() { auto encrypted = ESPCrypto::aesGcmEncryptAuto(key, plaintext); if (!encrypted.ok()) { Serial.printf("GCM encrypt failed: %s\n", toString(encrypted.status.code)); + ESPCrypto::deinit(); return; } Serial.printf("GCM IV: %s\n", bytesToHex(encrypted.value.iv).c_str()); @@ -37,10 +38,14 @@ void setup() { auto decrypted = ESPCrypto::aesGcmDecrypt(key, encrypted.value.iv, encrypted.value.ciphertext, encrypted.value.tag); if (!decrypted.ok()) { Serial.printf("GCM decrypt failed: %s\n", toString(decrypted.status.code)); + ESPCrypto::deinit(); return; } Serial.printf("GCM plaintext recovered: %s\n", String(reinterpret_cast(decrypted.value.data()), decrypted.value.size()).c_str()); + + // Explicit teardown for static runtime resources/caches. + ESPCrypto::deinit(); } void loop() { diff --git a/src/esp_crypto/esp_crypto.cpp b/src/esp_crypto/esp_crypto.cpp index 13acdfb..67160d0 100644 --- a/src/esp_crypto/esp_crypto.cpp +++ b/src/esp_crypto/esp_crypto.cpp @@ -151,6 +151,39 @@ struct NonceRecord { bool used = false; }; +struct GlobalRuntimeState { + std::atomic initialized{false}; + std::map nvsInitMap; +#if ESPCRYPTO_ENABLE_NONCE_GUARD + std::array nonceCache = {}; + size_t nonceCursor = 0; +#endif + std::atomic bootCounter{0}; +}; + +GlobalRuntimeState &runtimeState() { + static GlobalRuntimeState state; + return state; +} + +void markRuntimeInitialized() { + runtimeState().initialized.store(true, std::memory_order_release); +} + +void resetRuntimeState() { + GlobalRuntimeState &state = runtimeState(); + state.nvsInitMap.clear(); +#if ESPCRYPTO_ENABLE_NONCE_GUARD + for (auto &record : state.nonceCache) { + record = NonceRecord{}; + } + state.nonceCursor = 0; +#endif + state.bootCounter.store(0, std::memory_order_release); + mutablePolicy() = CryptoPolicy{}; + state.initialized.store(false, std::memory_order_release); +} + uint32_t fingerprintKey(const std::vector &key) { uint32_t hash = 2166136261u; for (uint8_t b : key) { @@ -162,13 +195,13 @@ uint32_t fingerprintKey(const std::vector &key) { bool nonceReused(const std::vector &key, const std::vector &iv) { #if ESPCRYPTO_ENABLE_NONCE_GUARD - static std::array cache; - static size_t cursor = 0; - if (iv.empty() || iv.size() > cache[0].iv.size()) { + GlobalRuntimeState &state = runtimeState(); + if (iv.empty() || iv.size() > state.nonceCache[0].iv.size()) { return false; } + markRuntimeInitialized(); uint32_t keyHash = fingerprintKey(key); - for (const auto &record : cache) { + for (const auto &record : state.nonceCache) { if (!record.used || record.ivLen != iv.size()) { continue; } @@ -179,12 +212,12 @@ bool nonceReused(const std::vector &key, const std::vector &iv return true; } } - NonceRecord &slot = cache[cursor % cache.size()]; + NonceRecord &slot = state.nonceCache[state.nonceCursor % state.nonceCache.size()]; slot.used = true; slot.keyHash = keyHash; slot.ivLen = iv.size(); memcpy(slot.iv.data(), iv.data(), iv.size()); - cursor++; + state.nonceCursor++; #else (void)key; (void)iv; @@ -816,6 +849,7 @@ CryptoStatusDetail AesGcmCtx::beginCommon(const std::vector &key, if (!aesKeyValid(key) || iv.empty()) { return makeStatus(CryptoStatus::InvalidInput, "invalid key or iv"); } + markRuntimeInitialized(); const CryptoPolicy &policy = mutablePolicy(); if (!policy.allowLegacy && iv.size() < policy.minAesGcmIvBytes) { return makeStatus(CryptoStatus::PolicyViolation, "iv too short"); @@ -902,9 +936,10 @@ std::string handleKeyString(const KeyHandle &handle) { bool ensureNvsReady(const String &partition) { #if defined(ESP_PLATFORM) - static std::map initMap; - auto it = initMap.find(partition.c_str()); - if (it != initMap.end() && it->second) { + GlobalRuntimeState &state = runtimeState(); + auto it = state.nvsInitMap.find(partition.c_str()); + if (it != state.nvsInitMap.end() && it->second) { + markRuntimeInitialized(); return true; } esp_err_t err = nvs_flash_init_partition(partition.c_str()); @@ -913,7 +948,10 @@ bool ensureNvsReady(const String &partition) { err = nvs_flash_init_partition(partition.c_str()); } bool ok = (err == ESP_OK); - initMap[partition.c_str()] = ok; + state.nvsInitMap[partition.c_str()] = ok; + if (ok) { + markRuntimeInitialized(); + } return ok; #else (void)partition; @@ -1591,6 +1629,7 @@ bool pkPolicyAllows(mbedtls_pk_context &pk, mbedtls_pk_type_t expected) { if (!mbedtls_pk_can_do(&pk, expected)) { return false; } + markRuntimeInitialized(); const CryptoPolicy &policy = mutablePolicy(); size_t bitlen = mbedtls_pk_get_bitlen(&pk); if (!policy.allowLegacy) { @@ -1928,6 +1967,7 @@ CryptoStatusDetail aesGcmEncryptSpan(const std::vector &key, if (!aesKeyValid(key) || iv.empty()) { return makeStatus(CryptoStatus::InvalidInput, "invalid key or iv"); } + markRuntimeInitialized(); const CryptoPolicy &policy = mutablePolicy(); if (!policy.allowLegacy && iv.size() < policy.minAesGcmIvBytes) { return makeStatus(CryptoStatus::PolicyViolation, "iv too short"); @@ -1967,6 +2007,7 @@ CryptoStatusDetail aesGcmDecryptSpan(const std::vector &key, if (!aesKeyValid(key) || iv.empty() || tag.size() != AES_GCM_TAG_BYTES) { return makeStatus(CryptoStatus::InvalidInput, "invalid key/iv/tag"); } + markRuntimeInitialized(); const CryptoPolicy &policy = mutablePolicy(); if (!policy.allowLegacy && iv.size() < policy.minAesGcmIvBytes) { return makeStatus(CryptoStatus::PolicyViolation, "iv too short"); @@ -2145,12 +2186,21 @@ void SecureString::wipe() { void ESPCrypto::setPolicy(const CryptoPolicy &policy) { mutablePolicy() = policy; + markRuntimeInitialized(); } CryptoPolicy ESPCrypto::policy() { return mutablePolicy(); } +void ESPCrypto::deinit() { + resetRuntimeState(); +} + +bool ESPCrypto::isInitialized() { + return runtimeState().initialized.load(std::memory_order_acquire); +} + CryptoCaps ESPCrypto::caps() { CryptoCaps c; c.shaAccel = ESPCRYPTO_SHA_ACCEL; @@ -2369,6 +2419,7 @@ CryptoResult ESPCrypto::aesGcmEncryptAuto(const std::vector size_t ivLength, const GcmNonceOptions &nonceOptions) { CryptoResult result; + markRuntimeInitialized(); const CryptoPolicy &policy = mutablePolicy(); if (ivLength == 0) { ivLength = policy.minAesGcmIvBytes; @@ -2382,9 +2433,9 @@ CryptoResult ESPCrypto::aesGcmEncryptAuto(const std::vector return result; } result.value.iv.assign(ivLength, 0); + GlobalRuntimeState &state = runtimeState(); uint32_t keyHash = fingerprintKey(key); - static std::atomic bootCounter{0}; - bootCounter.fetch_add(1, std::memory_order_relaxed); + state.bootCounter.fetch_add(1, std::memory_order_relaxed); switch (nonceOptions.strategy) { case GcmNonceStrategy::Random96: default: @@ -2420,7 +2471,7 @@ CryptoResult ESPCrypto::aesGcmEncryptAuto(const std::vector result.status = makeStatus(CryptoStatus::PolicyViolation, "counter strategy needs >=12 iv bytes"); return result; } - uint64_t counter = bootCounter.load(std::memory_order_relaxed); + uint64_t counter = state.bootCounter.load(std::memory_order_relaxed); for (int i = 0; i < 8 && i < static_cast(ivLength); ++i) { result.value.iv[i] = static_cast((counter >> (56 - 8 * i)) & 0xFF); } @@ -3005,6 +3056,7 @@ CryptoResult ESPCrypto::hashStringResult(const String &input, const Pass fillRandom(salt.data(), salt.size()); uint8_t cost = std::min(options.cost, 31); uint32_t iterations = 1u << cost; + markRuntimeInitialized(); const CryptoPolicy &policy = mutablePolicy(); if (!policy.allowLegacy && iterations < policy.minPbkdf2Iterations) { uint8_t adjustedCost = cost; @@ -3051,6 +3103,7 @@ CryptoResult ESPCrypto::verifyStringResult(const String &input, const Stri return result; } uint32_t iterations = 1u << cost; + markRuntimeInitialized(); const CryptoPolicy &policy = mutablePolicy(); if (!policy.allowLegacy && iterations < policy.minPbkdf2Iterations) { result.status = makeStatus(CryptoStatus::PolicyViolation, "pbkdf2 iterations below policy"); @@ -3167,6 +3220,7 @@ CryptoResult> ESPCrypto::pbkdf2(const String &password, result.status = makeStatus(CryptoStatus::InvalidInput, "missing password/salt/len"); return result; } + markRuntimeInitialized(); const CryptoPolicy &policy = mutablePolicy(); if (!policy.allowLegacy && iterations < policy.minPbkdf2Iterations) { result.status = makeStatus(CryptoStatus::PolicyViolation, "iterations below policy"); diff --git a/src/esp_crypto/esp_crypto.h b/src/esp_crypto/esp_crypto.h index d117b90..4710a51 100644 --- a/src/esp_crypto/esp_crypto.h +++ b/src/esp_crypto/esp_crypto.h @@ -407,6 +407,9 @@ struct GcmMessage { class ESPCrypto { public: + static void deinit(); + static bool isInitialized(); + static void setPolicy(const CryptoPolicy &policy); static CryptoPolicy policy(); static CryptoCaps caps(); diff --git a/test/test_esp_crypto/test_esp_crypto.cpp b/test/test_esp_crypto/test_esp_crypto.cpp index 462175c..d34cc2a 100644 --- a/test/test_esp_crypto/test_esp_crypto.cpp +++ b/test/test_esp_crypto/test_esp_crypto.cpp @@ -4,6 +4,38 @@ #include #include +void test_teardown_preinit_and_idempotent() { + ESPCrypto::deinit(); + TEST_ASSERT_FALSE(ESPCrypto::isInitialized()); + + ESPCrypto::deinit(); + TEST_ASSERT_FALSE(ESPCrypto::isInitialized()); +} + +void test_teardown_reinit_lifecycle() { + ESPCrypto::deinit(); + TEST_ASSERT_FALSE(ESPCrypto::isInitialized()); + + CryptoPolicy customPolicy = ESPCrypto::policy(); + customPolicy.minPbkdf2Iterations = 4096; + ESPCrypto::setPolicy(customPolicy); + TEST_ASSERT_TRUE(ESPCrypto::isInitialized()); + TEST_ASSERT_EQUAL_UINT32(4096, ESPCrypto::policy().minPbkdf2Iterations); + + ESPCrypto::deinit(); + TEST_ASSERT_FALSE(ESPCrypto::isInitialized()); + TEST_ASSERT_EQUAL_UINT32(1024, ESPCrypto::policy().minPbkdf2Iterations); + + std::vector key(16, 0x5A); + std::vector plaintext = {0x01, 0x02, 0x03}; + auto enc = ESPCrypto::aesGcmEncryptAuto(key, plaintext); + TEST_ASSERT_TRUE_MESSAGE(enc.ok(), enc.status.message.c_str()); + TEST_ASSERT_TRUE(ESPCrypto::isInitialized()); + + ESPCrypto::deinit(); + TEST_ASSERT_FALSE(ESPCrypto::isInitialized()); +} + void test_sha_hex_matches_known_value() { const char *data = "hello world"; String digest = ESPCrypto::shaHex(reinterpret_cast(data), strlen(data)); @@ -271,6 +303,8 @@ void tearDown() {} void setup() { delay(2000); UNITY_BEGIN(); + RUN_TEST(test_teardown_preinit_and_idempotent); + RUN_TEST(test_teardown_reinit_lifecycle); RUN_TEST(test_sha_hex_matches_known_value); RUN_TEST(test_sha_known_vectors); RUN_TEST(test_sha_ctx_streaming);