Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,22 @@ 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() {}
```

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:

Expand Down
5 changes: 5 additions & 0 deletions examples/basic_hash_and_aes/basic_hash_and_aes.ino
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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<const char *>(decrypted.value.data()), decrypted.value.size()).c_str());

// Explicit teardown for static runtime resources/caches.
ESPCrypto::deinit();
}

void loop() {
Expand Down
80 changes: 67 additions & 13 deletions src/esp_crypto/esp_crypto.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,39 @@ struct NonceRecord {
bool used = false;
};

struct GlobalRuntimeState {
std::atomic<bool> initialized{false};
std::map<std::string, bool> nvsInitMap;
#if ESPCRYPTO_ENABLE_NONCE_GUARD
std::array<NonceRecord, ESPCRYPTO_NONCE_GUARD_CACHE> nonceCache = {};
size_t nonceCursor = 0;
#endif
std::atomic<uint64_t> 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<uint8_t> &key) {
uint32_t hash = 2166136261u;
for (uint8_t b : key) {
Expand All @@ -162,13 +195,13 @@ uint32_t fingerprintKey(const std::vector<uint8_t> &key) {

bool nonceReused(const std::vector<uint8_t> &key, const std::vector<uint8_t> &iv) {
#if ESPCRYPTO_ENABLE_NONCE_GUARD
static std::array<NonceRecord, ESPCRYPTO_NONCE_GUARD_CACHE> 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;
}
Expand All @@ -179,12 +212,12 @@ bool nonceReused(const std::vector<uint8_t> &key, const std::vector<uint8_t> &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;
Expand Down Expand Up @@ -816,6 +849,7 @@ CryptoStatusDetail AesGcmCtx::beginCommon(const std::vector<uint8_t> &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");
Expand Down Expand Up @@ -902,9 +936,10 @@ std::string handleKeyString(const KeyHandle &handle) {

bool ensureNvsReady(const String &partition) {
#if defined(ESP_PLATFORM)
static std::map<std::string, bool> 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());
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -1928,6 +1967,7 @@ CryptoStatusDetail aesGcmEncryptSpan(const std::vector<uint8_t> &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");
Expand Down Expand Up @@ -1967,6 +2007,7 @@ CryptoStatusDetail aesGcmDecryptSpan(const std::vector<uint8_t> &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");
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -2369,6 +2419,7 @@ CryptoResult<GcmMessage> ESPCrypto::aesGcmEncryptAuto(const std::vector<uint8_t>
size_t ivLength,
const GcmNonceOptions &nonceOptions) {
CryptoResult<GcmMessage> result;
markRuntimeInitialized();
const CryptoPolicy &policy = mutablePolicy();
if (ivLength == 0) {
ivLength = policy.minAesGcmIvBytes;
Expand All @@ -2382,9 +2433,9 @@ CryptoResult<GcmMessage> ESPCrypto::aesGcmEncryptAuto(const std::vector<uint8_t>
return result;
}
result.value.iv.assign(ivLength, 0);
GlobalRuntimeState &state = runtimeState();
uint32_t keyHash = fingerprintKey(key);
static std::atomic<uint64_t> 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:
Expand Down Expand Up @@ -2420,7 +2471,7 @@ CryptoResult<GcmMessage> ESPCrypto::aesGcmEncryptAuto(const std::vector<uint8_t>
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<int>(ivLength); ++i) {
result.value.iv[i] = static_cast<uint8_t>((counter >> (56 - 8 * i)) & 0xFF);
}
Expand Down Expand Up @@ -3005,6 +3056,7 @@ CryptoResult<String> ESPCrypto::hashStringResult(const String &input, const Pass
fillRandom(salt.data(), salt.size());
uint8_t cost = std::min<uint8_t>(options.cost, 31);
uint32_t iterations = 1u << cost;
markRuntimeInitialized();
const CryptoPolicy &policy = mutablePolicy();
if (!policy.allowLegacy && iterations < policy.minPbkdf2Iterations) {
uint8_t adjustedCost = cost;
Expand Down Expand Up @@ -3051,6 +3103,7 @@ CryptoResult<void> 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");
Expand Down Expand Up @@ -3167,6 +3220,7 @@ CryptoResult<std::vector<uint8_t>> 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");
Expand Down
3 changes: 3 additions & 0 deletions src/esp_crypto/esp_crypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
34 changes: 34 additions & 0 deletions test/test_esp_crypto/test_esp_crypto.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,38 @@
#include <cstring>
#include <vector>

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<uint8_t> key(16, 0x5A);
std::vector<uint8_t> 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<const uint8_t *>(data), strlen(data));
Expand Down Expand Up @@ -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);
Expand Down