From 106e36b42fab41f5679e58a133e51a0d3dc0843e Mon Sep 17 00:00:00 2001 From: zekageri Date: Tue, 10 Mar 2026 13:11:36 +0100 Subject: [PATCH] chore: align formatter baseline with esptoolkit-template --- .clang-format | 11 + .editorconfig | 11 + .gitignore | 1 - .vscode/bin/clang-format | 19 + .vscode/extensions.json | 9 + .vscode/settings.json | 30 + .vscode/tasks.json | 12 + README.md | 7 + .../advanced_primitives.ino | 226 +- .../basic_hash_and_aes/basic_hash_and_aes.ino | 89 +- examples/bench_crypto/bench_crypto.ino | 57 +- examples/jwks_rotation/jwks_rotation.ino | 55 +- .../jwt_and_password/jwt_and_password.ino | 77 +- .../keys_and_streaming/keys_and_streaming.ino | 126 +- scripts/format_cpp.sh | 24 + src/esp_crypto/ed25519.h | 15 +- src/esp_crypto/esp_crypto.cpp | 6263 +++++++++-------- src/esp_crypto/esp_crypto.h | 1197 ++-- test/test_esp_crypto/test_esp_crypto.cpp | 734 +- 19 files changed, 4951 insertions(+), 4012 deletions(-) create mode 100644 .clang-format create mode 100644 .editorconfig create mode 100755 .vscode/bin/clang-format create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100755 scripts/format_cpp.sh diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..8450693 --- /dev/null +++ b/.clang-format @@ -0,0 +1,11 @@ +BasedOnStyle: LLVM +ColumnLimit: 100 +BinPackArguments: false +BinPackParameters: false +AllowAllArgumentsOnNextLine: false +AlignAfterOpenBracket: BlockIndent +UseTab: ForIndentation +IndentWidth: 4 +TabWidth: 4 +ContinuationIndentWidth: 4 +AllowShortFunctionsOnASingleLine: None diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d89c76d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +[*.{c,cc,cpp,h,hpp,ino}] +indent_style = tab +indent_size = tab +tab_width = 4 diff --git a/.gitignore b/.gitignore index cbc3f0e..6346d5c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ .venv build/ build_prev_runner/ -.vscode diff --git a/.vscode/bin/clang-format b/.vscode/bin/clang-format new file mode 100755 index 0000000..0df371f --- /dev/null +++ b/.vscode/bin/clang-format @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if command -v clang-format >/dev/null 2>&1; then + exec clang-format "$@" +fi + +_home_dir="${HOME:-}" +if [ -n "$_home_dir" ]; then + _candidate="$(ls -1d "$_home_dir"/.vscode/extensions/ms-vscode.cpptools-*-linux-x64/LLVM/bin/clang-format 2>/dev/null | tail -n 1 || true)" + if [ -n "$_candidate" ] && [ -x "$_candidate" ]; then + exec "$_candidate" "$@" + fi +fi + +echo "clang-format executable not found." >&2 +echo "Install clang-format system-wide or install/update ms-vscode.cpptools." >&2 +exit 127 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..f814711 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "pioarduino.pioarduino-ide", + "xaver.clang-format" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..24368c8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,30 @@ +{ + "files.associations": { + "*.ino": "cpp" + }, + "editor.defaultFormatter": "xaver.clang-format", + "C_Cpp.formatting": "Disabled", + "clang-format.style": "file", + "clang-format.executable": "${workspaceRoot}/.vscode/bin/clang-format", + "[cpp]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + }, + "[c]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + }, + "[arduino]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..20e66d5 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,12 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Format Firmware Sources", + "type": "shell", + "command": "bash ${workspaceFolder}/scripts/format_cpp.sh", + "group": "build", + "problemMatcher": [] + } + ] +} diff --git a/README.md b/README.md index 5999739..2de801e 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,13 @@ void rotate_keys() { ## Tests Hardware exercises run via PlatformIO Unity tests under `test/test_esp_crypto`, including KATs for SHA-2, AES-GCM (with tag checks), HKDF, PBKDF2, JWT HS256 round-trips, and password hashing. Host-side CMake just stubs out tests (ESP-IDF primitives are unavailable when cross-compiling for CI). +## Formatting Baseline + +This repository follows the firmware formatting baseline from `esptoolkit-template`: +- `.clang-format` is the source of truth for C/C++/INO layout. +- `.editorconfig` enforces tabs (`tab_width = 4`), LF endings, and final newline. +- Format all tracked firmware sources with `bash scripts/format_cpp.sh`. + ## License MIT — see [LICENSE.md](LICENSE.md). diff --git a/examples/advanced_primitives/advanced_primitives.ino b/examples/advanced_primitives/advanced_primitives.ino index e3e55b9..ed02278 100644 --- a/examples/advanced_primitives/advanced_primitives.ino +++ b/examples/advanced_primitives/advanced_primitives.ino @@ -1,8 +1,8 @@ #include #include -#include #include +#include const char *RSA_PRIVATE_PEM = R"(-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCwudwslbzHhgGu @@ -55,93 +55,163 @@ IuWpNdrP2f4GvtZYKkeYrhXDidn1+qYo+jGWUwmCdbo0yKmpDYwmy3/BnQ== -----END PUBLIC KEY-----)"; String bytesToHex(const std::vector &bytes) { - static const char *HEX_DIGITS = "0123456789ABCDEF"; - String out; - for (uint8_t b : bytes) { - out += HEX_DIGITS[(b >> 4) & 0x0F]; - out += HEX_DIGITS[b & 0x0F]; - } - return out; + static const char *HEX_DIGITS = "0123456789ABCDEF"; + String out; + for (uint8_t b : bytes) { + out += HEX_DIGITS[(b >> 4) & 0x0F]; + out += HEX_DIGITS[b & 0x0F]; + } + return out; } String statusText(const CryptoStatusDetail &status) { - if (status.ok()) { - return "ok"; - } - if (status.message.length() > 0) { - return status.message; - } - return String(toString(status.code)); + if (status.ok()) { + return "ok"; + } + if (status.message.length() > 0) { + return status.message; + } + return String(toString(status.code)); } void logCaps() { - CryptoCaps caps = ESPCrypto::caps(); - Serial.printf("HW accel → SHA:%s AES:%s GCM:%s\n", - caps.shaAccel ? "yes" : "no", - caps.aesAccel ? "yes" : "no", - caps.aesGcmAccel ? "yes" : "no"); + CryptoCaps caps = ESPCrypto::caps(); + Serial.printf( + "HW accel → SHA:%s AES:%s GCM:%s\n", + caps.shaAccel ? "yes" : "no", + caps.aesAccel ? "yes" : "no", + caps.aesGcmAccel ? "yes" : "no" + ); } void setup() { - Serial.begin(115200); - delay(200); - - // Tighten policy (PBKDF2 iterations >= 2048 by default here) - CryptoPolicy pol = ESPCrypto::policy(); - pol.minPbkdf2Iterations = 2048; - ESPCrypto::setPolicy(pol); - - logCaps(); - - // Secure key material that zeroizes on scope exit - SecureBuffer key(32); - for (size_t i = 0; i < key.size(); ++i) { - key.raw()[i] = static_cast(0xA0 + i); - } - - // HMAC-SHA256 - std::vector msg = {'a', 'p', 'i'}; - auto hmac = ESPCrypto::hmac(ShaVariant::SHA256, CryptoSpan(key.raw()), CryptoSpan(msg)); - Serial.printf("HMAC-SHA256: %s (status=%s)\n", bytesToHex(hmac.value).c_str(), statusText(hmac.status).c_str()); - - // HKDF derive two subkeys - std::vector salt = {0x01, 0x02, 0x03, 0x04}; - std::vector info = {'h', 'a', 'n', 'd', 's', 'h', 'a', 'k', 'e'}; - auto hkdf = ESPCrypto::hkdf(ShaVariant::SHA256, CryptoSpan(salt), CryptoSpan(key.raw()), CryptoSpan(info), 32); - Serial.printf("HKDF key: %s (status=%s)\n", bytesToHex(hkdf.value).c_str(), statusText(hkdf.status).c_str()); - - // PBKDF2 (policy-enforced iterations) - std::vector passwordSalt = {0x10, 0x20, 0x30, 0x40, 0x50}; - auto pbkdf2 = ESPCrypto::pbkdf2("wifi-password", CryptoSpan(passwordSalt), pol.minPbkdf2Iterations, 32); - Serial.printf("PBKDF2: %s (status=%s)\n", bytesToHex(pbkdf2.value).c_str(), statusText(pbkdf2.status).c_str()); - - // AES-CTR streaming demo - std::vector ctrNonce(16, 0x00); - for (size_t i = 0; i < ctrNonce.size(); ++i) { - ctrNonce[i] = static_cast(i); - } - std::vector streamInput = {'s', 't', 'r', 'e', 'a', 'm', '-', 'c', 't', 'r'}; - auto ctrOut = ESPCrypto::aesCtrCrypt(key.raw(), ctrNonce, streamInput); - Serial.printf("AES-CTR cipher: %s (status=%s)\n", bytesToHex(ctrOut.value).c_str(), statusText(ctrOut.status).c_str()); - auto ctrPlain = ESPCrypto::aesCtrCrypt(key.raw(), ctrNonce, ctrOut.value); - Serial.printf("AES-CTR plain: %s (status=%s)\n", - String(reinterpret_cast(ctrPlain.value.data()), ctrPlain.value.size()).c_str(), - statusText(ctrPlain.status).c_str()); - - // RSA sign/verify - std::vector firmware = {'f', 'w', '-', '1', '.', '0'}; - auto rsaSig = ESPCrypto::rsaSign(std::string(RSA_PRIVATE_PEM), CryptoSpan(firmware), ShaVariant::SHA256); - Serial.printf("RSA sig bytes: %u (status=%s)\n", static_cast(rsaSig.value.size()), statusText(rsaSig.status).c_str()); - auto rsaVerify = ESPCrypto::rsaVerify(std::string(RSA_PUBLIC_PEM), CryptoSpan(firmware), CryptoSpan(rsaSig.value), ShaVariant::SHA256); - Serial.printf("RSA verify: %s (status=%s)\n", rsaVerify.ok() ? "ok" : "fail", statusText(rsaVerify.status).c_str()); - - // ECDSA sign/verify - auto eccSig = ESPCrypto::eccSign(std::string(ECC_PRIVATE_PEM), CryptoSpan(firmware), ShaVariant::SHA256); - Serial.printf("ECC sig bytes: %u (status=%s)\n", static_cast(eccSig.value.size()), statusText(eccSig.status).c_str()); - auto eccVerify = ESPCrypto::eccVerify(std::string(ECC_PUBLIC_PEM), CryptoSpan(firmware), CryptoSpan(eccSig.value), ShaVariant::SHA256); - Serial.printf("ECC verify: %s (status=%s)\n", eccVerify.ok() ? "ok" : "fail", statusText(eccVerify.status).c_str()); + Serial.begin(115200); + delay(200); + + // Tighten policy (PBKDF2 iterations >= 2048 by default here) + CryptoPolicy pol = ESPCrypto::policy(); + pol.minPbkdf2Iterations = 2048; + ESPCrypto::setPolicy(pol); + + logCaps(); + + // Secure key material that zeroizes on scope exit + SecureBuffer key(32); + for (size_t i = 0; i < key.size(); ++i) { + key.raw()[i] = static_cast(0xA0 + i); + } + + // HMAC-SHA256 + std::vector msg = {'a', 'p', 'i'}; + auto hmac = ESPCrypto::hmac( + ShaVariant::SHA256, + CryptoSpan(key.raw()), + CryptoSpan(msg) + ); + Serial.printf( + "HMAC-SHA256: %s (status=%s)\n", + bytesToHex(hmac.value).c_str(), + statusText(hmac.status).c_str() + ); + + // HKDF derive two subkeys + std::vector salt = {0x01, 0x02, 0x03, 0x04}; + std::vector info = {'h', 'a', 'n', 'd', 's', 'h', 'a', 'k', 'e'}; + auto hkdf = ESPCrypto::hkdf( + ShaVariant::SHA256, + CryptoSpan(salt), + CryptoSpan(key.raw()), + CryptoSpan(info), + 32 + ); + Serial.printf( + "HKDF key: %s (status=%s)\n", + bytesToHex(hkdf.value).c_str(), + statusText(hkdf.status).c_str() + ); + + // PBKDF2 (policy-enforced iterations) + std::vector passwordSalt = {0x10, 0x20, 0x30, 0x40, 0x50}; + auto pbkdf2 = ESPCrypto::pbkdf2( + "wifi-password", + CryptoSpan(passwordSalt), + pol.minPbkdf2Iterations, + 32 + ); + Serial.printf( + "PBKDF2: %s (status=%s)\n", + bytesToHex(pbkdf2.value).c_str(), + statusText(pbkdf2.status).c_str() + ); + + // AES-CTR streaming demo + std::vector ctrNonce(16, 0x00); + for (size_t i = 0; i < ctrNonce.size(); ++i) { + ctrNonce[i] = static_cast(i); + } + std::vector streamInput = {'s', 't', 'r', 'e', 'a', 'm', '-', 'c', 't', 'r'}; + auto ctrOut = ESPCrypto::aesCtrCrypt(key.raw(), ctrNonce, streamInput); + Serial.printf( + "AES-CTR cipher: %s (status=%s)\n", + bytesToHex(ctrOut.value).c_str(), + statusText(ctrOut.status).c_str() + ); + auto ctrPlain = ESPCrypto::aesCtrCrypt(key.raw(), ctrNonce, ctrOut.value); + Serial.printf( + "AES-CTR plain: %s (status=%s)\n", + String(reinterpret_cast(ctrPlain.value.data()), ctrPlain.value.size()) + .c_str(), + statusText(ctrPlain.status).c_str() + ); + + // RSA sign/verify + std::vector firmware = {'f', 'w', '-', '1', '.', '0'}; + auto rsaSig = ESPCrypto::rsaSign( + std::string(RSA_PRIVATE_PEM), + CryptoSpan(firmware), + ShaVariant::SHA256 + ); + Serial.printf( + "RSA sig bytes: %u (status=%s)\n", + static_cast(rsaSig.value.size()), + statusText(rsaSig.status).c_str() + ); + auto rsaVerify = ESPCrypto::rsaVerify( + std::string(RSA_PUBLIC_PEM), + CryptoSpan(firmware), + CryptoSpan(rsaSig.value), + ShaVariant::SHA256 + ); + Serial.printf( + "RSA verify: %s (status=%s)\n", + rsaVerify.ok() ? "ok" : "fail", + statusText(rsaVerify.status).c_str() + ); + + // ECDSA sign/verify + auto eccSig = ESPCrypto::eccSign( + std::string(ECC_PRIVATE_PEM), + CryptoSpan(firmware), + ShaVariant::SHA256 + ); + Serial.printf( + "ECC sig bytes: %u (status=%s)\n", + static_cast(eccSig.value.size()), + statusText(eccSig.status).c_str() + ); + auto eccVerify = ESPCrypto::eccVerify( + std::string(ECC_PUBLIC_PEM), + CryptoSpan(firmware), + CryptoSpan(eccSig.value), + ShaVariant::SHA256 + ); + Serial.printf( + "ECC verify: %s (status=%s)\n", + eccVerify.ok() ? "ok" : "fail", + statusText(eccVerify.status).c_str() + ); } void loop() { - vTaskDelay(pdMS_TO_TICKS(1000)); + vTaskDelay(pdMS_TO_TICKS(1000)); } 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 f10bc32..c94c884 100644 --- a/examples/basic_hash_and_aes/basic_hash_and_aes.ino +++ b/examples/basic_hash_and_aes/basic_hash_and_aes.ino @@ -4,50 +4,59 @@ #include String bytesToHex(const std::vector &bytes) { - static const char *HEX_DIGITS = "0123456789ABCDEF"; - String out; - for (uint8_t b : bytes) { - out += HEX_DIGITS[(b >> 4) & 0x0F]; - out += HEX_DIGITS[b & 0x0F]; - } - return out; + static const char *HEX_DIGITS = "0123456789ABCDEF"; + String out; + for (uint8_t b : bytes) { + out += HEX_DIGITS[(b >> 4) & 0x0F]; + out += HEX_DIGITS[b & 0x0F]; + } + return out; } void setup() { - Serial.begin(115200); - delay(200); - - // Basic SHA helper - String message = "ESPCrypto"; - String digest = ESPCrypto::shaHex(reinterpret_cast(message.c_str()), message.length()); - Serial.printf("SHA256('%s') = %s\n", message.c_str(), digest.c_str()); - - // Basic AES-GCM with auto IV/tag handling - std::vector key(32, 0x01); // 256-bit key - std::vector plaintext = {'h', 'e', 'l', 'l', 'o'}; - 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()); - Serial.printf("GCM ciphertext: %s\n", bytesToHex(encrypted.value.ciphertext).c_str()); - Serial.printf("GCM tag: %s\n", bytesToHex(encrypted.value.tag).c_str()); - - 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(); + Serial.begin(115200); + delay(200); + + // Basic SHA helper + String message = "ESPCrypto"; + String digest = + ESPCrypto::shaHex(reinterpret_cast(message.c_str()), message.length()); + Serial.printf("SHA256('%s') = %s\n", message.c_str(), digest.c_str()); + + // Basic AES-GCM with auto IV/tag handling + std::vector key(32, 0x01); // 256-bit key + std::vector plaintext = {'h', 'e', 'l', 'l', 'o'}; + 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()); + Serial.printf("GCM ciphertext: %s\n", bytesToHex(encrypted.value.ciphertext).c_str()); + Serial.printf("GCM tag: %s\n", bytesToHex(encrypted.value.tag).c_str()); + + 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() { - vTaskDelay(pdMS_TO_TICKS(1000)); + vTaskDelay(pdMS_TO_TICKS(1000)); } diff --git a/examples/bench_crypto/bench_crypto.ino b/examples/bench_crypto/bench_crypto.ino index 1760bf2..cf25e40 100644 --- a/examples/bench_crypto/bench_crypto.ino +++ b/examples/bench_crypto/bench_crypto.ino @@ -2,36 +2,43 @@ #include void benchSha() { - std::vector data(1024, 0xAB); - uint8_t out[32] = {0}; - uint32_t start = millis(); - for (int i = 0; i < 200; ++i) { - ESPCrypto::sha(CryptoSpan(data), CryptoSpan(out)); - } - uint32_t elapsed = millis() - start; - Serial.printf("SHA256 x200 of 1KB: %ums\n", elapsed); + std::vector data(1024, 0xAB); + uint8_t out[32] = {0}; + uint32_t start = millis(); + for (int i = 0; i < 200; ++i) { + ESPCrypto::sha(CryptoSpan(data), CryptoSpan(out)); + } + uint32_t elapsed = millis() - start; + Serial.printf("SHA256 x200 of 1KB: %ums\n", elapsed); } void benchGcm() { - std::vector key(16, 0x01); - std::vector iv(12, 0x02); - std::vector plaintext(512, 0x11); - std::vector ciphertext(plaintext.size(), 0); - std::vector tag(16, 0); - uint32_t start = millis(); - for (int i = 0; i < 50; ++i) { - ESPCrypto::aesGcmEncrypt(key, CryptoSpan(iv), CryptoSpan(plaintext), CryptoSpan(ciphertext), CryptoSpan(tag)); - } - uint32_t elapsed = millis() - start; - Serial.printf("AES-GCM x50 of 512B: %ums\n", elapsed); + std::vector key(16, 0x01); + std::vector iv(12, 0x02); + std::vector plaintext(512, 0x11); + std::vector ciphertext(plaintext.size(), 0); + std::vector tag(16, 0); + uint32_t start = millis(); + for (int i = 0; i < 50; ++i) { + ESPCrypto::aesGcmEncrypt( + key, + CryptoSpan(iv), + CryptoSpan(plaintext), + CryptoSpan(ciphertext), + CryptoSpan(tag) + ); + } + uint32_t elapsed = millis() - start; + Serial.printf("AES-GCM x50 of 512B: %ums\n", elapsed); } void setup() { - Serial.begin(115200); - delay(1000); - Serial.println("ESPCrypto micro-bench"); - benchSha(); - benchGcm(); + Serial.begin(115200); + delay(1000); + Serial.println("ESPCrypto micro-bench"); + benchSha(); + benchGcm(); } -void loop() {} +void loop() { +} diff --git a/examples/jwks_rotation/jwks_rotation.ino b/examples/jwks_rotation/jwks_rotation.ino index 2136726..74bfb8b 100644 --- a/examples/jwks_rotation/jwks_rotation.ino +++ b/examples/jwks_rotation/jwks_rotation.ino @@ -3,36 +3,37 @@ #include void setup() { - Serial.begin(115200); - delay(1000); - Serial.println("JWKS rotation demo"); + Serial.begin(115200); + delay(1000); + Serial.println("JWKS rotation demo"); - // Build a JWKS with two keys; rotate by switching kid - JsonDocument jwks; - JsonArray keys = jwks["keys"].to(); - JsonObject k1 = keys.add(); - k1["kty"] = "oct"; - k1["kid"] = "k1"; - k1["alg"] = "HS256"; - k1["k"] = "c3VwZXJzZWNyZXQ"; // "supersecret" + // Build a JWKS with two keys; rotate by switching kid + JsonDocument jwks; + JsonArray keys = jwks["keys"].to(); + JsonObject k1 = keys.add(); + k1["kty"] = "oct"; + k1["kid"] = "k1"; + k1["alg"] = "HS256"; + k1["k"] = "c3VwZXJzZWNyZXQ"; // "supersecret" - JsonObject k2 = keys.add(); - k2["kty"] = "oct"; - k2["kid"] = "k2"; - k2["alg"] = "HS256"; - k2["k"] = "bW9yZXNlY3JldA"; // "moresecret" + JsonObject k2 = keys.add(); + k2["kty"] = "oct"; + k2["kid"] = "k2"; + k2["alg"] = "HS256"; + k2["k"] = "bW9yZXNlY3JldA"; // "moresecret" - // Issue token with kid=k2 - JsonDocument claims; - claims["iss"] = "jwks-demo"; - JwtSignOptions sign; - sign.algorithm = JwtAlgorithm::HS256; - sign.keyId = "k2"; - String token = ESPCrypto::createJwt(claims, "moresecret", sign); + // Issue token with kid=k2 + JsonDocument claims; + claims["iss"] = "jwks-demo"; + JwtSignOptions sign; + sign.algorithm = JwtAlgorithm::HS256; + sign.keyId = "k2"; + String token = ESPCrypto::createJwt(claims, "moresecret", sign); - JsonDocument decoded; - auto res = ESPCrypto::verifyJwtWithJwks(token, jwks, decoded); - Serial.printf("JWKS verify with rotation (kid=k2) ok? %s\n", res.ok() ? "yes" : "no"); + JsonDocument decoded; + auto res = ESPCrypto::verifyJwtWithJwks(token, jwks, decoded); + Serial.printf("JWKS verify with rotation (kid=k2) ok? %s\n", res.ok() ? "yes" : "no"); } -void loop() {} +void loop() { +} diff --git a/examples/jwt_and_password/jwt_and_password.ino b/examples/jwt_and_password/jwt_and_password.ino index 4dae6b8..33bde6f 100644 --- a/examples/jwt_and_password/jwt_and_password.ino +++ b/examples/jwt_and_password/jwt_and_password.ino @@ -2,52 +2,53 @@ #include String toFriendly(const CryptoStatusDetail &status) { - if (status.ok()) { - return "ok"; - } - if (status.message.length() > 0) { - return status.message; - } - return String(toString(status.code)); + if (status.ok()) { + return "ok"; + } + if (status.message.length() > 0) { + return status.message; + } + return String(toString(status.code)); } void setup() { - Serial.begin(115200); - delay(200); + Serial.begin(115200); + delay(200); - // JWT creation/verification (HS256) - JsonDocument claims; - claims["role"] = "admin"; - JwtSignOptions sign; - sign.algorithm = JwtAlgorithm::HS256; - sign.issuer = "esp32"; - sign.expiresInSeconds = 60; + // JWT creation/verification (HS256) + JsonDocument claims; + claims["role"] = "admin"; + JwtSignOptions sign; + sign.algorithm = JwtAlgorithm::HS256; + sign.issuer = "esp32"; + sign.expiresInSeconds = 60; - auto tokenResult = ESPCrypto::createJwtResult(claims, "super-secret", sign); - if (!tokenResult.ok()) { - Serial.printf("JWT create failed: %s\n", toFriendly(tokenResult.status).c_str()); - return; - } - Serial.printf("JWT: %s\n", tokenResult.value.c_str()); + auto tokenResult = ESPCrypto::createJwtResult(claims, "super-secret", sign); + if (!tokenResult.ok()) { + Serial.printf("JWT create failed: %s\n", toFriendly(tokenResult.status).c_str()); + return; + } + Serial.printf("JWT: %s\n", tokenResult.value.c_str()); - JsonDocument decoded; - JwtVerifyOptions verify; - verify.algorithm = JwtAlgorithm::HS256; - verify.issuer = "esp32"; - auto verifyResult = ESPCrypto::verifyJwtResult(tokenResult.value, "super-secret", decoded, verify); - if (!verifyResult.ok()) { - Serial.printf("JWT verify failed: %s\n", toFriendly(verifyResult.status).c_str()); - } else { - Serial.printf("JWT role claim: %s\n", decoded["role"].as()); - } + JsonDocument decoded; + JwtVerifyOptions verify; + verify.algorithm = JwtAlgorithm::HS256; + verify.issuer = "esp32"; + auto verifyResult = + ESPCrypto::verifyJwtResult(tokenResult.value, "super-secret", decoded, verify); + if (!verifyResult.ok()) { + Serial.printf("JWT verify failed: %s\n", toFriendly(verifyResult.status).c_str()); + } else { + Serial.printf("JWT role claim: %s\n", decoded["role"].as()); + } - // Password hashing + verification - String hashed = ESPCrypto::hashString("hunter2"); - Serial.printf("Hashed password: %s\n", hashed.c_str()); - bool ok = ESPCrypto::verifyString("hunter2", hashed); - Serial.printf("Password matches: %s\n", ok ? "true" : "false"); + // Password hashing + verification + String hashed = ESPCrypto::hashString("hunter2"); + Serial.printf("Hashed password: %s\n", hashed.c_str()); + bool ok = ESPCrypto::verifyString("hunter2", hashed); + Serial.printf("Password matches: %s\n", ok ? "true" : "false"); } void loop() { - vTaskDelay(pdMS_TO_TICKS(1000)); + vTaskDelay(pdMS_TO_TICKS(1000)); } diff --git a/examples/keys_and_streaming/keys_and_streaming.ino b/examples/keys_and_streaming/keys_and_streaming.ino index 6cd5a85..881ed18 100644 --- a/examples/keys_and_streaming/keys_and_streaming.ino +++ b/examples/keys_and_streaming/keys_and_streaming.ino @@ -4,72 +4,88 @@ MemoryKeyStore memoryStore; void demoKeystore() { - KeyHandle handle{String("demo-key"), 1}; - const char *pem = "-----BEGIN PRIVATE KEY-----\n...replace-with-real-key...\n-----END PRIVATE KEY-----"; - ESPCrypto::storeKey(memoryStore, handle, CryptoSpan(reinterpret_cast(pem), strlen(pem))); - auto loaded = ESPCrypto::loadKey(memoryStore, handle, KeyFormat::Pem, KeyKind::Private); - if (loaded.ok()) { - auto sig = ESPCrypto::rsaSign(loaded.value, - CryptoSpan(reinterpret_cast("payload"), 7), - ShaVariant::SHA256); - Serial.printf("Loaded key and produced signature? %s\n", sig.ok() ? "yes" : "no"); - } else { - Serial.printf("Key load failed: %s\n", loaded.status.message.c_str()); - } + KeyHandle handle{String("demo-key"), 1}; + const char *pem = + "-----BEGIN PRIVATE KEY-----\n...replace-with-real-key...\n-----END PRIVATE KEY-----"; + ESPCrypto::storeKey( + memoryStore, + handle, + CryptoSpan(reinterpret_cast(pem), strlen(pem)) + ); + auto loaded = ESPCrypto::loadKey(memoryStore, handle, KeyFormat::Pem, KeyKind::Private); + if (loaded.ok()) { + auto sig = ESPCrypto::rsaSign( + loaded.value, + CryptoSpan(reinterpret_cast("payload"), 7), + ShaVariant::SHA256 + ); + Serial.printf("Loaded key and produced signature? %s\n", sig.ok() ? "yes" : "no"); + } else { + Serial.printf("Key load failed: %s\n", loaded.status.message.c_str()); + } } void demoStreaming() { - // Streaming SHA256 - ShaCtx shaCtx; - shaCtx.begin(ShaVariant::SHA256); - shaCtx.update(CryptoSpan(reinterpret_cast("hello "), 6)); - shaCtx.update(CryptoSpan(reinterpret_cast("world"), 5)); - uint8_t digest[32] = {0}; - shaCtx.finish(CryptoSpan(digest)); - Serial.print("SHA256(stream) digest[0..3]: "); - for (int i = 0; i < 4; ++i) { - Serial.printf("%02x", digest[i]); - } - Serial.println(); + // Streaming SHA256 + ShaCtx shaCtx; + shaCtx.begin(ShaVariant::SHA256); + shaCtx.update(CryptoSpan(reinterpret_cast("hello "), 6)); + shaCtx.update(CryptoSpan(reinterpret_cast("world"), 5)); + uint8_t digest[32] = {0}; + shaCtx.finish(CryptoSpan(digest)); + Serial.print("SHA256(stream) digest[0..3]: "); + for (int i = 0; i < 4; ++i) { + Serial.printf("%02x", digest[i]); + } + Serial.println(); - // AES-GCM streaming with caller buffers - std::vector key(16, 0x01); - std::vector iv(12, 0x02); - std::vector plaintext = {'E', 'S', 'P', 'C', 'r', 'y', 'p', 't', 'o'}; - std::vector ciphertext(plaintext.size(), 0); - std::vector tag(16, 0); + // AES-GCM streaming with caller buffers + std::vector key(16, 0x01); + std::vector iv(12, 0x02); + std::vector plaintext = {'E', 'S', 'P', 'C', 'r', 'y', 'p', 't', 'o'}; + std::vector ciphertext(plaintext.size(), 0); + std::vector tag(16, 0); - AesGcmCtx enc; - enc.beginEncrypt(key, CryptoSpan(iv), CryptoSpan()); - enc.update(CryptoSpan(plaintext), CryptoSpan(ciphertext)); - enc.finish(CryptoSpan(tag)); + AesGcmCtx enc; + enc.beginEncrypt(key, CryptoSpan(iv), CryptoSpan()); + enc.update(CryptoSpan(plaintext), CryptoSpan(ciphertext)); + enc.finish(CryptoSpan(tag)); - std::vector decrypted(plaintext.size(), 0); - AesGcmCtx dec; - dec.beginDecrypt(key, CryptoSpan(iv), CryptoSpan(), CryptoSpan(tag)); - dec.update(CryptoSpan(ciphertext), CryptoSpan(decrypted)); - auto decStatus = dec.finish(CryptoSpan(tag)); - Serial.printf("AES-GCM streaming decrypt ok? %s\n", decStatus.ok() && ESPCrypto::constantTimeEq(plaintext, decrypted) ? "yes" : "no"); + std::vector decrypted(plaintext.size(), 0); + AesGcmCtx dec; + dec.beginDecrypt( + key, + CryptoSpan(iv), + CryptoSpan(), + CryptoSpan(tag) + ); + dec.update(CryptoSpan(ciphertext), CryptoSpan(decrypted)); + auto decStatus = dec.finish(CryptoSpan(tag)); + Serial.printf( + "AES-GCM streaming decrypt ok? %s\n", + decStatus.ok() && ESPCrypto::constantTimeEq(plaintext, decrypted) ? "yes" : "no" + ); } void demoNonceStrategies() { - std::vector key(16, 0x03); - std::vector plaintext = {0x01, 0x02}; - GcmNonceOptions opts; - opts.strategy = GcmNonceStrategy::Counter64_Random32; - auto msg = ESPCrypto::aesGcmEncryptAuto(key, plaintext, {}, 12, opts); - Serial.printf("GCM iv (counter strategy) size: %u\n", msg.value.iv.size()); + std::vector key(16, 0x03); + std::vector plaintext = {0x01, 0x02}; + GcmNonceOptions opts; + opts.strategy = GcmNonceStrategy::Counter64_Random32; + auto msg = ESPCrypto::aesGcmEncryptAuto(key, plaintext, {}, 12, opts); + Serial.printf("GCM iv (counter strategy) size: %u\n", msg.value.iv.size()); } void setup() { - Serial.begin(115200); - delay(1000); - Serial.println("ESPCrypto keystore + streaming demo"); - demoKeystore(); - demoStreaming(); - demoNonceStrategies(); - auto deviceKey = ESPCrypto::deriveDeviceKey("example", CryptoSpan(), 32); - Serial.printf("Device-bound key derived? %s\n", deviceKey.ok() ? "yes" : "no"); + Serial.begin(115200); + delay(1000); + Serial.println("ESPCrypto keystore + streaming demo"); + demoKeystore(); + demoStreaming(); + demoNonceStrategies(); + auto deviceKey = ESPCrypto::deriveDeviceKey("example", CryptoSpan(), 32); + Serial.printf("Device-bound key derived? %s\n", deviceKey.ok() ? "yes" : "no"); } -void loop() {} +void loop() { +} diff --git a/scripts/format_cpp.sh b/scripts/format_cpp.sh new file mode 100755 index 0000000..7d17b04 --- /dev/null +++ b/scripts/format_cpp.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -euo pipefail + +_repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +_clang_format="${_repo_root}/.vscode/bin/clang-format" + +if [ ! -x "${_clang_format}" ]; then + echo "clang-format wrapper not found: ${_clang_format}" >&2 + exit 1 +fi + +mapfile -d '' _format_files < <( + git -C "${_repo_root}" ls-files -z -- '*.c' '*.cc' '*.cpp' '*.h' '*.hpp' '*.ino' +) + +if [ "${#_format_files[@]}" -eq 0 ]; then + echo "No tracked C/C++/INO files found to format." + exit 0 +fi + +"${_clang_format}" -i --style=file "${_format_files[@]}" + +echo "Formatted ${#_format_files[@]} files." diff --git a/src/esp_crypto/ed25519.h b/src/esp_crypto/ed25519.h index 2aee12e..a6efa34 100644 --- a/src/esp_crypto/ed25519.h +++ b/src/esp_crypto/ed25519.h @@ -3,9 +3,14 @@ #include #include -// Placeholder Ed25519 API. Current toolchain lacks Ed25519 primitives; these functions return failure. +// Placeholder Ed25519 API. Current toolchain lacks Ed25519 primitives; these functions return +// failure. namespace ed25519 { -inline void keypair(uint8_t *, uint8_t *, const uint8_t *) {} -inline void sign(uint8_t *, const uint8_t *, size_t, const uint8_t *) {} -inline int verify(const uint8_t *, const uint8_t *, size_t, const uint8_t *) { return -1; } -} // namespace ed25519 +inline void keypair(uint8_t *, uint8_t *, const uint8_t *) { +} +inline void sign(uint8_t *, const uint8_t *, size_t, const uint8_t *) { +} +inline int verify(const uint8_t *, const uint8_t *, size_t, const uint8_t *) { + return -1; +} +} // namespace ed25519 diff --git a/src/esp_crypto/esp_crypto.cpp b/src/esp_crypto/esp_crypto.cpp index bc434db..8bf8af8 100644 --- a/src/esp_crypto/esp_crypto.cpp +++ b/src/esp_crypto/esp_crypto.cpp @@ -2,31 +2,31 @@ #include #include -#include -#include +#include #include -#include +#include +#include #include +#include +#include +#include #include -#include #include -#include -#include -#include +#include #include "mbedtls/aes.h" +#include "mbedtls/asn1write.h" #include "mbedtls/base64.h" +#include "mbedtls/chachapoly.h" #include "mbedtls/ctr_drbg.h" +#include "mbedtls/ecdh.h" #include "mbedtls/entropy.h" #include "mbedtls/gcm.h" #include "mbedtls/md.h" #include "mbedtls/pk.h" #include "mbedtls/pkcs5.h" -#include "mbedtls/version.h" #include "mbedtls/platform_util.h" -#include "mbedtls/asn1write.h" -#include "mbedtls/chachapoly.h" -#include "mbedtls/ecdh.h" +#include "mbedtls/version.h" #if defined(__has_include) #if __has_include("mbedtls/private_access.h") #include "mbedtls/private_access.h" @@ -48,8 +48,8 @@ extern "C" { #include "esp_system.h" #include "esp_timer.h" -#include "nvs_flash.h" #include "nvs.h" +#include "nvs_flash.h" #if defined(__has_include) #if __has_include("esp_mac.h") #include "esp_mac.h" @@ -118,3339 +118,3776 @@ constexpr size_t AES_GCM_TAG_BYTES = 16; #endif void secureZero(void *data, size_t length) { - if (!data || length == 0) { - return; - } - volatile uint8_t *p = static_cast(data); - while (length--) { - *p++ = 0; - } + if (!data || length == 0) { + return; + } + volatile uint8_t *p = static_cast(data); + while (length--) { + *p++ = 0; + } #if defined(__GNUC__) - __asm__ __volatile__("" : : : "memory"); + __asm__ __volatile__("" : : : "memory"); #endif } CryptoPolicy &mutablePolicy() { - static CryptoPolicy policy; - return policy; + static CryptoPolicy policy; + return policy; } CryptoStatusDetail makeStatus(CryptoStatus code, const char *message = nullptr) { - CryptoStatusDetail status; - status.code = code; - if (message) { - status.message = message; - } - return status; + CryptoStatusDetail status; + status.code = code; + if (message) { + status.message = message; + } + return status; } struct NonceRecord { - uint32_t keyHash = 0; - std::array iv = {}; - size_t ivLen = 0; - bool used = false; + uint32_t keyHash = 0; + std::array iv = {}; + size_t ivLen = 0; + bool used = false; }; struct GlobalRuntimeState { - std::atomic initialized{false}; - std::map nvsInitMap; + std::atomic initialized{false}; + std::map nvsInitMap; #if ESPCRYPTO_ENABLE_NONCE_GUARD - std::array nonceCache = {}; - size_t nonceCursor = 0; + std::array nonceCache = {}; + size_t nonceCursor = 0; #endif - std::atomic bootCounter{0}; + std::atomic bootCounter{0}; }; GlobalRuntimeState &runtimeState() { - static GlobalRuntimeState state; - return state; + static GlobalRuntimeState state; + return state; } void markRuntimeInitialized() { - runtimeState().initialized.store(true, std::memory_order_release); + runtimeState().initialized.store(true, std::memory_order_release); } void resetRuntimeState() { - GlobalRuntimeState &state = runtimeState(); - state.nvsInitMap.clear(); + GlobalRuntimeState &state = runtimeState(); + state.nvsInitMap.clear(); #if ESPCRYPTO_ENABLE_NONCE_GUARD - for (auto &record : state.nonceCache) { - record = NonceRecord{}; - } - state.nonceCursor = 0; + 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); + 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) { - hash ^= b; - hash *= 16777619u; - } - return hash; + uint32_t hash = 2166136261u; + for (uint8_t b : key) { + hash ^= b; + hash *= 16777619u; + } + return hash; } bool nonceReused(const std::vector &key, const std::vector &iv) { #if ESPCRYPTO_ENABLE_NONCE_GUARD - 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 : state.nonceCache) { - if (!record.used || record.ivLen != iv.size()) { - continue; - } - if (record.keyHash != keyHash) { - continue; - } - if (memcmp(record.iv.data(), iv.data(), iv.size()) == 0) { - return true; - } - } - 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()); - state.nonceCursor++; + 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 : state.nonceCache) { + if (!record.used || record.ivLen != iv.size()) { + continue; + } + if (record.keyHash != keyHash) { + continue; + } + if (memcmp(record.iv.data(), iv.data(), iv.size()) == 0) { + return true; + } + } + 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()); + state.nonceCursor++; #else - (void)key; - (void)iv; + (void)key; + (void)iv; #endif - return false; + return false; } enum class Base64Alphabet { Standard, Url }; size_t digestLength(ShaVariant variant) { - switch (variant) { - case ShaVariant::SHA256: - return 32; - case ShaVariant::SHA384: - return 48; - case ShaVariant::SHA512: - return 64; - } - return 0; + switch (variant) { + case ShaVariant::SHA256: + return 32; + case ShaVariant::SHA384: + return 48; + case ShaVariant::SHA512: + return 64; + } + return 0; } const mbedtls_md_info_t *mdInfoForVariant(ShaVariant variant) { - switch (variant) { - case ShaVariant::SHA256: - return mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); - case ShaVariant::SHA384: - return mbedtls_md_info_from_type(MBEDTLS_MD_SHA384); - case ShaVariant::SHA512: - return mbedtls_md_info_from_type(MBEDTLS_MD_SHA512); - } - return nullptr; + switch (variant) { + case ShaVariant::SHA256: + return mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); + case ShaVariant::SHA384: + return mbedtls_md_info_from_type(MBEDTLS_MD_SHA384); + case ShaVariant::SHA512: + return mbedtls_md_info_from_type(MBEDTLS_MD_SHA512); + } + return nullptr; } bool softwareSha(ShaVariant variant, const uint8_t *data, size_t length, uint8_t *out) { - const mbedtls_md_info_t *info = mdInfoForVariant(variant); - if (!info) { - return false; - } - return mbedtls_md(info, data, length, out) == 0; + const mbedtls_md_info_t *info = mdInfoForVariant(variant); + if (!info) { + return false; + } + return mbedtls_md(info, data, length, out) == 0; } bool tryHardwareSha(ShaVariant variant, const uint8_t *data, size_t length, uint8_t *out) { #if ESPCRYPTO_SHA_ACCEL - esp_sha_type type = SHA1; - switch (variant) { - case ShaVariant::SHA256: - type = SHA2_256; - break; - case ShaVariant::SHA384: + esp_sha_type type = SHA1; + switch (variant) { + case ShaVariant::SHA256: + type = SHA2_256; + break; + case ShaVariant::SHA384: #if defined(SHA2_384) - type = SHA2_384; - break; + type = SHA2_384; + break; #else - return false; + return false; #endif - case ShaVariant::SHA512: + case ShaVariant::SHA512: #if defined(SHA2_512) - type = SHA2_512; - break; + type = SHA2_512; + break; #else - return false; + return false; #endif - } - esp_sha(type, data, length, out); - return true; + } + esp_sha(type, data, length, out); + return true; #else - (void)variant; - (void)data; - (void)length; - (void)out; - return false; + (void)variant; + (void)data; + (void)length; + (void)out; + return false; #endif } std::string base64Encode(const uint8_t *data, size_t length, Base64Alphabet alphabet) { - if (length == 0) { - return std::string(); - } - size_t encodedLen = 4 * ((length + 2) / 3); - std::string buffer(encodedLen, '\0'); - size_t actualLen = 0; - if (mbedtls_base64_encode(reinterpret_cast(&buffer[0]), buffer.size(), &actualLen, data, length) != 0) { - return std::string(); - } - buffer.resize(actualLen); - if (alphabet == Base64Alphabet::Url) { - for (char &c : buffer) { - if (c == '+') { - c = '-'; - } else if (c == '/') { - c = '_'; - } - } - while (!buffer.empty() && buffer.back() == '=') { - buffer.pop_back(); - } - } - return buffer; + if (length == 0) { + return std::string(); + } + size_t encodedLen = 4 * ((length + 2) / 3); + std::string buffer(encodedLen, '\0'); + size_t actualLen = 0; + if (mbedtls_base64_encode( + reinterpret_cast(&buffer[0]), + buffer.size(), + &actualLen, + data, + length + ) != 0) { + return std::string(); + } + buffer.resize(actualLen); + if (alphabet == Base64Alphabet::Url) { + for (char &c : buffer) { + if (c == '+') { + c = '-'; + } else if (c == '/') { + c = '_'; + } + } + while (!buffer.empty() && buffer.back() == '=') { + buffer.pop_back(); + } + } + return buffer; } bool base64Decode(const std::string &input, Base64Alphabet alphabet, std::vector &output) { - std::string transformed = input; - if (alphabet == Base64Alphabet::Url) { - for (char &c : transformed) { - if (c == '-') { - c = '+'; - } else if (c == '_') { - c = '/'; - } - } - while (transformed.size() % 4 != 0) { - transformed.push_back('='); - } - } - size_t required = 0; - int probe = mbedtls_base64_decode(nullptr, 0, &required, - reinterpret_cast(transformed.c_str()), - transformed.size()); - if (probe != MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL && probe != 0) { - return false; - } - output.assign(required, 0); - size_t actual = 0; - int ret = mbedtls_base64_decode(output.data(), output.size(), &actual, - reinterpret_cast(transformed.c_str()), - transformed.size()); - if (ret != 0) { - output.clear(); - return false; - } - output.resize(actual); - return true; + std::string transformed = input; + if (alphabet == Base64Alphabet::Url) { + for (char &c : transformed) { + if (c == '-') { + c = '+'; + } else if (c == '_') { + c = '/'; + } + } + while (transformed.size() % 4 != 0) { + transformed.push_back('='); + } + } + size_t required = 0; + int probe = mbedtls_base64_decode( + nullptr, + 0, + &required, + reinterpret_cast(transformed.c_str()), + transformed.size() + ); + if (probe != MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL && probe != 0) { + return false; + } + output.assign(required, 0); + size_t actual = 0; + int ret = mbedtls_base64_decode( + output.data(), + output.size(), + &actual, + reinterpret_cast(transformed.c_str()), + transformed.size() + ); + if (ret != 0) { + output.clear(); + return false; + } + output.resize(actual); + return true; } CryptoResult> ecdsaDerToRawInternal(CryptoSpan der) { - CryptoResult> result; - unsigned char *cursor = const_cast(der.data()); - const unsigned char *end = der.data() + der.size(); - size_t len = 0; - mbedtls_mpi r, s; - mbedtls_mpi_init(&r); - mbedtls_mpi_init(&s); - do { - if (mbedtls_asn1_get_tag(&cursor, end, &len, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) { - result.status = makeStatus(CryptoStatus::DecodeError, "asn1 seq"); - break; - } - if (mbedtls_asn1_get_mpi(&cursor, end, &r) != 0 || mbedtls_asn1_get_mpi(&cursor, end, &s) != 0) { - result.status = makeStatus(CryptoStatus::DecodeError, "asn1 mpi"); - break; - } - size_t rlen = mbedtls_mpi_size(&r); - size_t slen = mbedtls_mpi_size(&s); - size_t part = std::max(rlen, slen); - result.value.assign(part * 2, 0); - mbedtls_mpi_write_binary(&r, result.value.data() + (part - rlen), rlen); - mbedtls_mpi_write_binary(&s, result.value.data() + part + (part - slen), slen); - result.status = makeStatus(CryptoStatus::Ok); - } while (false); - mbedtls_mpi_free(&r); - mbedtls_mpi_free(&s); - return result; + CryptoResult> result; + unsigned char *cursor = const_cast(der.data()); + const unsigned char *end = der.data() + der.size(); + size_t len = 0; + mbedtls_mpi r, s; + mbedtls_mpi_init(&r); + mbedtls_mpi_init(&s); + do { + if (mbedtls_asn1_get_tag( + &cursor, + end, + &len, + MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE + ) != 0) { + result.status = makeStatus(CryptoStatus::DecodeError, "asn1 seq"); + break; + } + if (mbedtls_asn1_get_mpi(&cursor, end, &r) != 0 || + mbedtls_asn1_get_mpi(&cursor, end, &s) != 0) { + result.status = makeStatus(CryptoStatus::DecodeError, "asn1 mpi"); + break; + } + size_t rlen = mbedtls_mpi_size(&r); + size_t slen = mbedtls_mpi_size(&s); + size_t part = std::max(rlen, slen); + result.value.assign(part * 2, 0); + mbedtls_mpi_write_binary(&r, result.value.data() + (part - rlen), rlen); + mbedtls_mpi_write_binary(&s, result.value.data() + part + (part - slen), slen); + result.status = makeStatus(CryptoStatus::Ok); + } while (false); + mbedtls_mpi_free(&r); + mbedtls_mpi_free(&s); + return result; } CryptoResult> ecdsaRawToDerInternal(CryptoSpan raw) { - CryptoResult> result; - if (raw.size() % 2 != 0 || raw.empty()) { - result.status = makeStatus(CryptoStatus::InvalidInput, "raw len invalid"); - return result; - } - size_t part = raw.size() / 2; - mbedtls_mpi r, s; - mbedtls_mpi_init(&r); - mbedtls_mpi_init(&s); - do { - if (mbedtls_mpi_read_binary(&r, raw.data(), part) != 0 || mbedtls_mpi_read_binary(&s, raw.data() + part, part) != 0) { - result.status = makeStatus(CryptoStatus::DecodeError, "raw mpi"); - break; - } - unsigned char buffer[200]; - unsigned char *p = buffer + sizeof(buffer); - size_t len = 0; - if (mbedtls_asn1_write_mpi(&p, buffer, &s) < 0 || mbedtls_asn1_write_mpi(&p, buffer, &r) < 0) { - result.status = makeStatus(CryptoStatus::InternalError, "asn1 mpi write"); - break; - } - len = static_cast(buffer + sizeof(buffer) - p); - if (mbedtls_asn1_write_len(&p, buffer, len) < 0 || - mbedtls_asn1_write_tag(&p, buffer, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) < 0) { - result.status = makeStatus(CryptoStatus::InternalError, "asn1 len"); - break; - } - size_t total = static_cast(buffer + sizeof(buffer) - p); - result.value.assign(p, p + total); - result.status = makeStatus(CryptoStatus::Ok); - } while (false); - do { - if (mbedtls_mpi_read_binary(&r, raw.data(), part) != 0 || mbedtls_mpi_read_binary(&s, raw.data() + part, part) != 0) { - result.status = makeStatus(CryptoStatus::DecodeError, "raw mpi"); - break; - } - unsigned char buffer[200]; - unsigned char *p = buffer + sizeof(buffer); - size_t len = 0; - if (mbedtls_asn1_write_mpi(&p, buffer, &s) < 0 || mbedtls_asn1_write_mpi(&p, buffer, &r) < 0) { - result.status = makeStatus(CryptoStatus::InternalError, "asn1 mpi write"); - break; - } - len = static_cast(buffer + sizeof(buffer) - p); - if (mbedtls_asn1_write_len(&p, buffer, len) < 0 || - mbedtls_asn1_write_tag(&p, buffer, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) < 0) { - result.status = makeStatus(CryptoStatus::InternalError, "asn1 len"); - break; - } - size_t total = static_cast(buffer + sizeof(buffer) - p); - result.value.assign(p, p + total); - result.status = makeStatus(CryptoStatus::Ok); - } while (false); - mbedtls_mpi_free(&r); - mbedtls_mpi_free(&s); - return result; + CryptoResult> result; + if (raw.size() % 2 != 0 || raw.empty()) { + result.status = makeStatus(CryptoStatus::InvalidInput, "raw len invalid"); + return result; + } + size_t part = raw.size() / 2; + mbedtls_mpi r, s; + mbedtls_mpi_init(&r); + mbedtls_mpi_init(&s); + do { + if (mbedtls_mpi_read_binary(&r, raw.data(), part) != 0 || + mbedtls_mpi_read_binary(&s, raw.data() + part, part) != 0) { + result.status = makeStatus(CryptoStatus::DecodeError, "raw mpi"); + break; + } + unsigned char buffer[200]; + unsigned char *p = buffer + sizeof(buffer); + size_t len = 0; + if (mbedtls_asn1_write_mpi(&p, buffer, &s) < 0 || + mbedtls_asn1_write_mpi(&p, buffer, &r) < 0) { + result.status = makeStatus(CryptoStatus::InternalError, "asn1 mpi write"); + break; + } + len = static_cast(buffer + sizeof(buffer) - p); + if (mbedtls_asn1_write_len(&p, buffer, len) < 0 || + mbedtls_asn1_write_tag(&p, buffer, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) < + 0) { + result.status = makeStatus(CryptoStatus::InternalError, "asn1 len"); + break; + } + size_t total = static_cast(buffer + sizeof(buffer) - p); + result.value.assign(p, p + total); + result.status = makeStatus(CryptoStatus::Ok); + } while (false); + do { + if (mbedtls_mpi_read_binary(&r, raw.data(), part) != 0 || + mbedtls_mpi_read_binary(&s, raw.data() + part, part) != 0) { + result.status = makeStatus(CryptoStatus::DecodeError, "raw mpi"); + break; + } + unsigned char buffer[200]; + unsigned char *p = buffer + sizeof(buffer); + size_t len = 0; + if (mbedtls_asn1_write_mpi(&p, buffer, &s) < 0 || + mbedtls_asn1_write_mpi(&p, buffer, &r) < 0) { + result.status = makeStatus(CryptoStatus::InternalError, "asn1 mpi write"); + break; + } + len = static_cast(buffer + sizeof(buffer) - p); + if (mbedtls_asn1_write_len(&p, buffer, len) < 0 || + mbedtls_asn1_write_tag(&p, buffer, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) < + 0) { + result.status = makeStatus(CryptoStatus::InternalError, "asn1 len"); + break; + } + size_t total = static_cast(buffer + sizeof(buffer) - p); + result.value.assign(p, p + total); + result.status = makeStatus(CryptoStatus::Ok); + } while (false); + mbedtls_mpi_free(&r); + mbedtls_mpi_free(&s); + return result; } uint32_t currentTimeSeconds(uint32_t overrideValue) { - if (overrideValue != 0) { - return overrideValue; - } + if (overrideValue != 0) { + return overrideValue; + } #if defined(ESP_PLATFORM) - struct timeval tv; - if (gettimeofday(&tv, nullptr) == 0 && tv.tv_sec > 0) { - return static_cast(tv.tv_sec); - } - return static_cast(esp_timer_get_time() / 1000000ULL); + struct timeval tv; + if (gettimeofday(&tv, nullptr) == 0 && tv.tv_sec > 0) { + return static_cast(tv.tv_sec); + } + return static_cast(esp_timer_get_time() / 1000000ULL); #else - return static_cast(time(nullptr)); + return static_cast(time(nullptr)); #endif } void fillRandom(uint8_t *data, size_t length) { #if defined(ESP_PLATFORM) - esp_fill_random(data, length); + esp_fill_random(data, length); #else - std::random_device rd; - for (size_t i = 0; i < length; ++i) { - data[i] = static_cast(rd()); - } + std::random_device rd; + for (size_t i = 0; i < length; ++i) { + data[i] = static_cast(rd()); + } #endif } bool constantTimeEquals(CryptoSpan a, CryptoSpan b) { - if (a.size() != b.size()) { - return false; - } - uint8_t diff = 0; - for (size_t i = 0; i < a.size(); ++i) { - diff |= static_cast(a.data()[i] ^ b.data()[i]); - } - return diff == 0; -} - -CryptoStatusDetail buildRsaPemFromJwk(const std::vector &n, - const std::vector &e, - std::string &outPem) { - mbedtls_pk_context pk; - mbedtls_pk_init(&pk); - CryptoStatusDetail status = makeStatus(CryptoStatus::InternalError, "rsa setup failed"); - if (mbedtls_pk_setup(&pk, mbedtls_pk_info_from_type(MBEDTLS_PK_RSA)) != 0) { - mbedtls_pk_free(&pk); - return status; - } - mbedtls_rsa_context *rsa = mbedtls_pk_rsa(pk); - if (mbedtls_rsa_import_raw(rsa, n.data(), n.size(), nullptr, 0, nullptr, 0, nullptr, 0, e.data(), e.size()) != 0 || - mbedtls_rsa_complete(rsa) != 0 || - mbedtls_rsa_check_pubkey(rsa) != 0) { - mbedtls_pk_free(&pk); - return makeStatus(CryptoStatus::DecodeError, "rsa jwk invalid"); - } - std::vector buffer(1600, 0); - if (mbedtls_pk_write_pubkey_pem(&pk, buffer.data(), buffer.size()) != 0) { - mbedtls_pk_free(&pk); - return status; - } - outPem.assign(reinterpret_cast(buffer.data())); - mbedtls_pk_free(&pk); - return makeStatus(CryptoStatus::Ok); -} - -CryptoStatusDetail buildEcPemFromJwk(const std::vector &x, - const std::vector &y, - const std::string &crv, - std::string &outPem) { - mbedtls_pk_context pk; - mbedtls_pk_init(&pk); - mbedtls_ecp_keypair *ec = nullptr; - CryptoStatusDetail status = makeStatus(CryptoStatus::Unsupported, "curve unsupported"); - mbedtls_ecp_group_id gid = MBEDTLS_ECP_DP_NONE; - if (crv == "P-256") { - gid = MBEDTLS_ECP_DP_SECP256R1; - } - if (gid == MBEDTLS_ECP_DP_NONE) { - return status; - } - if (mbedtls_pk_setup(&pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)) != 0) { - mbedtls_pk_free(&pk); - return makeStatus(CryptoStatus::InternalError, "ec setup failed"); - } - ec = mbedtls_pk_ec(pk); - if (!ec || mbedtls_ecp_group_load(&ec->MBEDTLS_PRIVATE(grp), gid) != 0) { - mbedtls_pk_free(&pk); - return status; - } - if (mbedtls_mpi_read_binary(&ec->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(X), x.data(), x.size()) != 0 || - mbedtls_mpi_read_binary(&ec->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(Y), y.data(), y.size()) != 0) { - mbedtls_pk_free(&pk); - return makeStatus(CryptoStatus::DecodeError, "ec coord read"); - } - if (mbedtls_mpi_lset(&ec->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(Z), 1) != 0) { - mbedtls_pk_free(&pk); - return makeStatus(CryptoStatus::DecodeError, "ec coord set"); - } - if (mbedtls_ecp_check_pubkey(&ec->MBEDTLS_PRIVATE(grp), &ec->MBEDTLS_PRIVATE(Q)) != 0) { - mbedtls_pk_free(&pk); - return makeStatus(CryptoStatus::DecodeError, "ec jwk invalid"); - } - { - std::vector buffer(800, 0); - if (mbedtls_pk_write_pubkey_pem(&pk, buffer.data(), buffer.size()) != 0) { - mbedtls_pk_free(&pk); - return makeStatus(CryptoStatus::InternalError, "ec pem write failed"); - } - outPem.assign(reinterpret_cast(buffer.data())); - } - mbedtls_pk_free(&pk); - return makeStatus(CryptoStatus::Ok); + if (a.size() != b.size()) { + return false; + } + uint8_t diff = 0; + for (size_t i = 0; i < a.size(); ++i) { + diff |= static_cast(a.data()[i] ^ b.data()[i]); + } + return diff == 0; +} + +CryptoStatusDetail buildRsaPemFromJwk( + const std::vector &n, const std::vector &e, std::string &outPem +) { + mbedtls_pk_context pk; + mbedtls_pk_init(&pk); + CryptoStatusDetail status = makeStatus(CryptoStatus::InternalError, "rsa setup failed"); + if (mbedtls_pk_setup(&pk, mbedtls_pk_info_from_type(MBEDTLS_PK_RSA)) != 0) { + mbedtls_pk_free(&pk); + return status; + } + mbedtls_rsa_context *rsa = mbedtls_pk_rsa(pk); + if (mbedtls_rsa_import_raw( + rsa, + n.data(), + n.size(), + nullptr, + 0, + nullptr, + 0, + nullptr, + 0, + e.data(), + e.size() + ) != 0 || + mbedtls_rsa_complete(rsa) != 0 || mbedtls_rsa_check_pubkey(rsa) != 0) { + mbedtls_pk_free(&pk); + return makeStatus(CryptoStatus::DecodeError, "rsa jwk invalid"); + } + std::vector buffer(1600, 0); + if (mbedtls_pk_write_pubkey_pem(&pk, buffer.data(), buffer.size()) != 0) { + mbedtls_pk_free(&pk); + return status; + } + outPem.assign(reinterpret_cast(buffer.data())); + mbedtls_pk_free(&pk); + return makeStatus(CryptoStatus::Ok); +} + +CryptoStatusDetail buildEcPemFromJwk( + const std::vector &x, + const std::vector &y, + const std::string &crv, + std::string &outPem +) { + mbedtls_pk_context pk; + mbedtls_pk_init(&pk); + mbedtls_ecp_keypair *ec = nullptr; + CryptoStatusDetail status = makeStatus(CryptoStatus::Unsupported, "curve unsupported"); + mbedtls_ecp_group_id gid = MBEDTLS_ECP_DP_NONE; + if (crv == "P-256") { + gid = MBEDTLS_ECP_DP_SECP256R1; + } + if (gid == MBEDTLS_ECP_DP_NONE) { + return status; + } + if (mbedtls_pk_setup(&pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)) != 0) { + mbedtls_pk_free(&pk); + return makeStatus(CryptoStatus::InternalError, "ec setup failed"); + } + ec = mbedtls_pk_ec(pk); + if (!ec || mbedtls_ecp_group_load(&ec->MBEDTLS_PRIVATE(grp), gid) != 0) { + mbedtls_pk_free(&pk); + return status; + } + if (mbedtls_mpi_read_binary(&ec->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(X), x.data(), x.size()) != + 0 || + mbedtls_mpi_read_binary(&ec->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(Y), y.data(), y.size()) != + 0) { + mbedtls_pk_free(&pk); + return makeStatus(CryptoStatus::DecodeError, "ec coord read"); + } + if (mbedtls_mpi_lset(&ec->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(Z), 1) != 0) { + mbedtls_pk_free(&pk); + return makeStatus(CryptoStatus::DecodeError, "ec coord set"); + } + if (mbedtls_ecp_check_pubkey(&ec->MBEDTLS_PRIVATE(grp), &ec->MBEDTLS_PRIVATE(Q)) != 0) { + mbedtls_pk_free(&pk); + return makeStatus(CryptoStatus::DecodeError, "ec jwk invalid"); + } + { + std::vector buffer(800, 0); + if (mbedtls_pk_write_pubkey_pem(&pk, buffer.data(), buffer.size()) != 0) { + mbedtls_pk_free(&pk); + return makeStatus(CryptoStatus::InternalError, "ec pem write failed"); + } + outPem.assign(reinterpret_cast(buffer.data())); + } + mbedtls_pk_free(&pk); + return makeStatus(CryptoStatus::Ok); } CryptoResult jwkToKey(const JsonObjectConst &jwk) { - CryptoResult result; - const char *kty = jwk["kty"].as(); - if (!kty) { - result.status = makeStatus(CryptoStatus::InvalidInput, "missing kty"); - return result; - } - if (strcmp(kty, "oct") == 0) { - std::vector k; - if (!base64Decode(jwk["k"].as(), Base64Alphabet::Url, k)) { - result.status = makeStatus(CryptoStatus::DecodeError, "oct decode failed"); - return result; - } - result.value = CryptoKey::fromRaw(k, KeyKind::Symmetric); - result.status = makeStatus(CryptoStatus::Ok); - return result; - } - if (strcmp(kty, "RSA") == 0) { - std::vector n, e; - if (!base64Decode(jwk["n"].as(), Base64Alphabet::Url, n) || - !base64Decode(jwk["e"].as(), Base64Alphabet::Url, e)) { - result.status = makeStatus(CryptoStatus::DecodeError, "rsa decode failed"); - return result; - } - std::string pem; - auto status = buildRsaPemFromJwk(n, e, pem); - if (!status.ok()) { - result.status = status; - return result; - } - result.value = CryptoKey::fromPem(pem, KeyKind::Public); - result.status = makeStatus(CryptoStatus::Ok); - return result; - } - if (strcmp(kty, "EC") == 0) { - std::vector x, y; - if (!base64Decode(jwk["x"].as(), Base64Alphabet::Url, x) || - !base64Decode(jwk["y"].as(), Base64Alphabet::Url, y)) { - result.status = makeStatus(CryptoStatus::DecodeError, "ec decode failed"); - return result; - } - std::string pem; - auto status = buildEcPemFromJwk(x, y, std::string(jwk["crv"].as() ? jwk["crv"].as() : ""), pem); - if (!status.ok()) { - result.status = status; - return result; - } - result.value = CryptoKey::fromPem(pem, KeyKind::Public); - result.status = makeStatus(CryptoStatus::Ok); - return result; - } - result.status = makeStatus(CryptoStatus::Unsupported, "kty unsupported"); - return result; -} - -CryptoResult selectJwkFromSet(const JsonDocument &jwks, const String &kid, JwtAlgorithm algHint) { - CryptoResult result; - JsonArrayConst keys = jwks["keys"].as(); - if (keys.isNull()) { - result.status = makeStatus(CryptoStatus::InvalidInput, "jwks missing keys"); - return result; - } - for (JsonVariantConst v : keys) { - JsonObjectConst jwk = v.as(); - const char *jwkKid = jwk["kid"].as(); - if (kid.length() > 0 && (!jwkKid || kid != jwkKid)) { - continue; - } - const char *algStr = jwk["alg"].as(); - if (algStr && algHint != JwtAlgorithm::Auto && algorithmFromName(algStr) != JwtAlgorithm::Auto && algorithmFromName(algStr) != algHint) { - continue; - } - auto parsed = jwkToKey(jwk); - if (parsed.ok()) { - return parsed; - } - result.status = parsed.status; - } - if (kid.length() > 0) { - result.status = makeStatus(CryptoStatus::DecodeError, "kid not found"); - } else if (!result.status.ok()) { - // Keep last parse error - } else { - result.status = makeStatus(CryptoStatus::DecodeError, "no jwk matched"); - } - return result; -} - -} // namespace + CryptoResult result; + const char *kty = jwk["kty"].as(); + if (!kty) { + result.status = makeStatus(CryptoStatus::InvalidInput, "missing kty"); + return result; + } + if (strcmp(kty, "oct") == 0) { + std::vector k; + if (!base64Decode(jwk["k"].as(), Base64Alphabet::Url, k)) { + result.status = makeStatus(CryptoStatus::DecodeError, "oct decode failed"); + return result; + } + result.value = CryptoKey::fromRaw(k, KeyKind::Symmetric); + result.status = makeStatus(CryptoStatus::Ok); + return result; + } + if (strcmp(kty, "RSA") == 0) { + std::vector n, e; + if (!base64Decode(jwk["n"].as(), Base64Alphabet::Url, n) || + !base64Decode(jwk["e"].as(), Base64Alphabet::Url, e)) { + result.status = makeStatus(CryptoStatus::DecodeError, "rsa decode failed"); + return result; + } + std::string pem; + auto status = buildRsaPemFromJwk(n, e, pem); + if (!status.ok()) { + result.status = status; + return result; + } + result.value = CryptoKey::fromPem(pem, KeyKind::Public); + result.status = makeStatus(CryptoStatus::Ok); + return result; + } + if (strcmp(kty, "EC") == 0) { + std::vector x, y; + if (!base64Decode(jwk["x"].as(), Base64Alphabet::Url, x) || + !base64Decode(jwk["y"].as(), Base64Alphabet::Url, y)) { + result.status = makeStatus(CryptoStatus::DecodeError, "ec decode failed"); + return result; + } + std::string pem; + auto status = buildEcPemFromJwk( + x, + y, + std::string(jwk["crv"].as() ? jwk["crv"].as() : ""), + pem + ); + if (!status.ok()) { + result.status = status; + return result; + } + result.value = CryptoKey::fromPem(pem, KeyKind::Public); + result.status = makeStatus(CryptoStatus::Ok); + return result; + } + result.status = makeStatus(CryptoStatus::Unsupported, "kty unsupported"); + return result; +} + +CryptoResult +selectJwkFromSet(const JsonDocument &jwks, const String &kid, JwtAlgorithm algHint) { + CryptoResult result; + JsonArrayConst keys = jwks["keys"].as(); + if (keys.isNull()) { + result.status = makeStatus(CryptoStatus::InvalidInput, "jwks missing keys"); + return result; + } + for (JsonVariantConst v : keys) { + JsonObjectConst jwk = v.as(); + const char *jwkKid = jwk["kid"].as(); + if (kid.length() > 0 && (!jwkKid || kid != jwkKid)) { + continue; + } + const char *algStr = jwk["alg"].as(); + if (algStr && algHint != JwtAlgorithm::Auto && + algorithmFromName(algStr) != JwtAlgorithm::Auto && + algorithmFromName(algStr) != algHint) { + continue; + } + auto parsed = jwkToKey(jwk); + if (parsed.ok()) { + return parsed; + } + result.status = parsed.status; + } + if (kid.length() > 0) { + result.status = makeStatus(CryptoStatus::DecodeError, "kid not found"); + } else if (!result.status.ok()) { + // Keep last parse error + } else { + result.status = makeStatus(CryptoStatus::DecodeError, "no jwk matched"); + } + return result; +} + +} // namespace bool initDrbg(mbedtls_ctr_drbg_context &ctr, mbedtls_entropy_context &entropy); -bool softwareGcmCrypt(int mode, - const std::vector &key, - CryptoSpan iv, - CryptoSpan aad, - CryptoSpan input, - CryptoSpan output, - CryptoSpan tag); +bool softwareGcmCrypt( + int mode, + const std::vector &key, + CryptoSpan iv, + CryptoSpan aad, + CryptoSpan input, + CryptoSpan output, + CryptoSpan tag +); bool aesKeyValid(const std::vector &key); ShaCtx::ShaCtx() { - mbedtls_md_init(&ctx); + mbedtls_md_init(&ctx); } ShaCtx::~ShaCtx() { - mbedtls_md_free(&ctx); + mbedtls_md_free(&ctx); } CryptoStatusDetail ShaCtx::begin(ShaVariant variant, bool /*preferHardware*/) { - // Reset any prior digest allocation so repeated begin() calls do not leak. - mbedtls_md_free(&ctx); - mbedtls_md_init(&ctx); - started = false; - info = nullptr; - - info = mdInfoForVariant(variant); - if (!info) { - return makeStatus(CryptoStatus::InvalidInput, "invalid sha variant"); - } - if (mbedtls_md_setup(&ctx, info, 0) != 0) { - return makeStatus(CryptoStatus::InternalError, "md setup failed"); - } - if (mbedtls_md_starts(&ctx) != 0) { - return makeStatus(CryptoStatus::InternalError, "md start failed"); - } - started = true; - return makeStatus(CryptoStatus::Ok); + // Reset any prior digest allocation so repeated begin() calls do not leak. + mbedtls_md_free(&ctx); + mbedtls_md_init(&ctx); + started = false; + info = nullptr; + + info = mdInfoForVariant(variant); + if (!info) { + return makeStatus(CryptoStatus::InvalidInput, "invalid sha variant"); + } + if (mbedtls_md_setup(&ctx, info, 0) != 0) { + return makeStatus(CryptoStatus::InternalError, "md setup failed"); + } + if (mbedtls_md_starts(&ctx) != 0) { + return makeStatus(CryptoStatus::InternalError, "md start failed"); + } + started = true; + return makeStatus(CryptoStatus::Ok); } CryptoStatusDetail ShaCtx::update(CryptoSpan data) { - if (!started) { - return makeStatus(CryptoStatus::InvalidInput, "sha not started"); - } - if (data.empty()) { - return makeStatus(CryptoStatus::Ok); - } - if (mbedtls_md_update(&ctx, data.data(), data.size()) != 0) { - return makeStatus(CryptoStatus::InternalError, "md update failed"); - } - return makeStatus(CryptoStatus::Ok); + if (!started) { + return makeStatus(CryptoStatus::InvalidInput, "sha not started"); + } + if (data.empty()) { + return makeStatus(CryptoStatus::Ok); + } + if (mbedtls_md_update(&ctx, data.data(), data.size()) != 0) { + return makeStatus(CryptoStatus::InternalError, "md update failed"); + } + return makeStatus(CryptoStatus::Ok); } CryptoStatusDetail ShaCtx::finish(CryptoSpan out) { - if (!started || !info) { - return makeStatus(CryptoStatus::InvalidInput, "sha not started"); - } - size_t need = mbedtls_md_get_size(info); - if (out.size() < need) { - return makeStatus(CryptoStatus::BufferTooSmall, "digest buffer too small"); - } - if (mbedtls_md_finish(&ctx, out.data()) != 0) { - return makeStatus(CryptoStatus::InternalError, "md finish failed"); - } - started = false; - return makeStatus(CryptoStatus::Ok); + if (!started || !info) { + return makeStatus(CryptoStatus::InvalidInput, "sha not started"); + } + size_t need = mbedtls_md_get_size(info); + if (out.size() < need) { + return makeStatus(CryptoStatus::BufferTooSmall, "digest buffer too small"); + } + if (mbedtls_md_finish(&ctx, out.data()) != 0) { + return makeStatus(CryptoStatus::InternalError, "md finish failed"); + } + started = false; + return makeStatus(CryptoStatus::Ok); } HmacCtx::HmacCtx() { - mbedtls_md_init(&ctx); + mbedtls_md_init(&ctx); } HmacCtx::~HmacCtx() { - mbedtls_md_free(&ctx); + mbedtls_md_free(&ctx); } CryptoStatusDetail HmacCtx::begin(ShaVariant variant, CryptoSpan key) { - // Reset any prior digest/HMAC allocation so repeated begin() calls do not leak. - mbedtls_md_free(&ctx); - mbedtls_md_init(&ctx); - started = false; - info = nullptr; - - info = mdInfoForVariant(variant); - if (!info || key.empty()) { - return makeStatus(CryptoStatus::InvalidInput, "invalid hmac params"); - } - if (mbedtls_md_setup(&ctx, info, 1) != 0) { - return makeStatus(CryptoStatus::InternalError, "md setup failed"); - } - if (mbedtls_md_hmac_starts(&ctx, key.data(), key.size()) != 0) { - return makeStatus(CryptoStatus::InternalError, "hmac start failed"); - } - started = true; - return makeStatus(CryptoStatus::Ok); + // Reset any prior digest/HMAC allocation so repeated begin() calls do not leak. + mbedtls_md_free(&ctx); + mbedtls_md_init(&ctx); + started = false; + info = nullptr; + + info = mdInfoForVariant(variant); + if (!info || key.empty()) { + return makeStatus(CryptoStatus::InvalidInput, "invalid hmac params"); + } + if (mbedtls_md_setup(&ctx, info, 1) != 0) { + return makeStatus(CryptoStatus::InternalError, "md setup failed"); + } + if (mbedtls_md_hmac_starts(&ctx, key.data(), key.size()) != 0) { + return makeStatus(CryptoStatus::InternalError, "hmac start failed"); + } + started = true; + return makeStatus(CryptoStatus::Ok); } CryptoStatusDetail HmacCtx::update(CryptoSpan data) { - if (!started) { - return makeStatus(CryptoStatus::InvalidInput, "hmac not started"); - } - if (data.empty()) { - return makeStatus(CryptoStatus::Ok); - } - if (mbedtls_md_hmac_update(&ctx, data.data(), data.size()) != 0) { - return makeStatus(CryptoStatus::InternalError, "hmac update failed"); - } - return makeStatus(CryptoStatus::Ok); + if (!started) { + return makeStatus(CryptoStatus::InvalidInput, "hmac not started"); + } + if (data.empty()) { + return makeStatus(CryptoStatus::Ok); + } + if (mbedtls_md_hmac_update(&ctx, data.data(), data.size()) != 0) { + return makeStatus(CryptoStatus::InternalError, "hmac update failed"); + } + return makeStatus(CryptoStatus::Ok); } CryptoStatusDetail HmacCtx::finish(CryptoSpan out) { - if (!started || !info) { - return makeStatus(CryptoStatus::InvalidInput, "hmac not started"); - } - size_t need = mbedtls_md_get_size(info); - if (out.size() < need) { - return makeStatus(CryptoStatus::BufferTooSmall, "digest buffer too small"); - } - if (mbedtls_md_hmac_finish(&ctx, out.data()) != 0) { - return makeStatus(CryptoStatus::InternalError, "hmac finish failed"); - } - started = false; - return makeStatus(CryptoStatus::Ok); + if (!started || !info) { + return makeStatus(CryptoStatus::InvalidInput, "hmac not started"); + } + size_t need = mbedtls_md_get_size(info); + if (out.size() < need) { + return makeStatus(CryptoStatus::BufferTooSmall, "digest buffer too small"); + } + if (mbedtls_md_hmac_finish(&ctx, out.data()) != 0) { + return makeStatus(CryptoStatus::InternalError, "hmac finish failed"); + } + started = false; + return makeStatus(CryptoStatus::Ok); } AesCtrStream::AesCtrStream() { - mbedtls_aes_init(&ctx); - memset(counter, 0, sizeof(counter)); - memset(streamBlock, 0, sizeof(streamBlock)); + mbedtls_aes_init(&ctx); + memset(counter, 0, sizeof(counter)); + memset(streamBlock, 0, sizeof(streamBlock)); } AesCtrStream::~AesCtrStream() { - mbedtls_aes_free(&ctx); - mbedtls_platform_zeroize(counter, sizeof(counter)); - mbedtls_platform_zeroize(streamBlock, sizeof(streamBlock)); -} - -CryptoStatusDetail AesCtrStream::begin(const std::vector &key, CryptoSpan nonceCounter) { - if (!aesKeyValid(key) || nonceCounter.size() != 16) { - return makeStatus(CryptoStatus::InvalidInput, "invalid key or nonce"); - } - if (mbedtls_aes_setkey_enc(&ctx, key.data(), key.size() * 8) != 0) { - return makeStatus(CryptoStatus::InternalError, "aes setkey failed"); - } - memcpy(counter, nonceCounter.data(), 16); - offset = 0; - started = true; - return makeStatus(CryptoStatus::Ok); -} - -CryptoStatusDetail AesCtrStream::update(CryptoSpan input, CryptoSpan output) { - if (!started) { - return makeStatus(CryptoStatus::InvalidInput, "ctr not started"); - } - if (output.size() < input.size()) { - return makeStatus(CryptoStatus::BufferTooSmall, "output too small"); - } - if (input.empty()) { - return makeStatus(CryptoStatus::Ok); - } - size_t offCopy = offset; - int ret = mbedtls_aes_crypt_ctr(&ctx, input.size(), &offCopy, counter, streamBlock, input.data(), output.data()); - offset = offCopy; - return ret == 0 ? makeStatus(CryptoStatus::Ok) : makeStatus(CryptoStatus::InternalError, "ctr update failed"); -} - -static int gcmStartsCompat(mbedtls_gcm_context &ctx, int mode, CryptoSpan iv, CryptoSpan aad) { + mbedtls_aes_free(&ctx); + mbedtls_platform_zeroize(counter, sizeof(counter)); + mbedtls_platform_zeroize(streamBlock, sizeof(streamBlock)); +} + +CryptoStatusDetail +AesCtrStream::begin(const std::vector &key, CryptoSpan nonceCounter) { + if (!aesKeyValid(key) || nonceCounter.size() != 16) { + return makeStatus(CryptoStatus::InvalidInput, "invalid key or nonce"); + } + if (mbedtls_aes_setkey_enc(&ctx, key.data(), key.size() * 8) != 0) { + return makeStatus(CryptoStatus::InternalError, "aes setkey failed"); + } + memcpy(counter, nonceCounter.data(), 16); + offset = 0; + started = true; + return makeStatus(CryptoStatus::Ok); +} + +CryptoStatusDetail +AesCtrStream::update(CryptoSpan input, CryptoSpan output) { + if (!started) { + return makeStatus(CryptoStatus::InvalidInput, "ctr not started"); + } + if (output.size() < input.size()) { + return makeStatus(CryptoStatus::BufferTooSmall, "output too small"); + } + if (input.empty()) { + return makeStatus(CryptoStatus::Ok); + } + size_t offCopy = offset; + int ret = mbedtls_aes_crypt_ctr( + &ctx, + input.size(), + &offCopy, + counter, + streamBlock, + input.data(), + output.data() + ); + offset = offCopy; + return ret == 0 ? makeStatus(CryptoStatus::Ok) + : makeStatus(CryptoStatus::InternalError, "ctr update failed"); +} + +static int gcmStartsCompat( + mbedtls_gcm_context &ctx, int mode, CryptoSpan iv, CryptoSpan aad +) { #if ESPCRYPTO_MBEDTLS_V3 - int ret = mbedtls_gcm_starts(&ctx, mode, iv.data(), iv.size()); - if (ret != 0 || aad.empty()) { - return ret; - } - return mbedtls_gcm_update_ad(&ctx, aad.data(), aad.size()); + int ret = mbedtls_gcm_starts(&ctx, mode, iv.data(), iv.size()); + if (ret != 0 || aad.empty()) { + return ret; + } + return mbedtls_gcm_update_ad(&ctx, aad.data(), aad.size()); #else - return mbedtls_gcm_starts(&ctx, - mode, - iv.data(), - iv.size(), - aad.empty() ? nullptr : aad.data(), - aad.size()); + return mbedtls_gcm_starts( + &ctx, + mode, + iv.data(), + iv.size(), + aad.empty() ? nullptr : aad.data(), + aad.size() + ); #endif } -static int gcmUpdateCompat(mbedtls_gcm_context &ctx, CryptoSpan input, CryptoSpan output) { +static int gcmUpdateCompat( + mbedtls_gcm_context &ctx, CryptoSpan input, CryptoSpan output +) { #if ESPCRYPTO_MBEDTLS_V3 - size_t outLen = 0; - return mbedtls_gcm_update(&ctx, input.data(), input.size(), output.data(), output.size(), &outLen); + size_t outLen = 0; + return mbedtls_gcm_update( + &ctx, + input.data(), + input.size(), + output.data(), + output.size(), + &outLen + ); #else - return mbedtls_gcm_update(&ctx, input.size(), input.data(), output.data()); + return mbedtls_gcm_update(&ctx, input.size(), input.data(), output.data()); #endif } static int gcmFinishCompat(mbedtls_gcm_context &ctx, CryptoSpan tagOut) { #if ESPCRYPTO_MBEDTLS_V3 - size_t outLen = 0; - return mbedtls_gcm_finish(&ctx, nullptr, 0, &outLen, tagOut.data(), tagOut.size()); + size_t outLen = 0; + return mbedtls_gcm_finish(&ctx, nullptr, 0, &outLen, tagOut.data(), tagOut.size()); #else - return mbedtls_gcm_finish(&ctx, tagOut.data(), tagOut.size()); + return mbedtls_gcm_finish(&ctx, tagOut.data(), tagOut.size()); #endif } AesGcmCtx::AesGcmCtx() { - mbedtls_gcm_init(&ctx); + mbedtls_gcm_init(&ctx); } AesGcmCtx::~AesGcmCtx() { - mbedtls_gcm_free(&ctx); - mbedtls_platform_zeroize(tagVerify.data(), tagVerify.size()); -} - -CryptoStatusDetail AesGcmCtx::beginCommon(const std::vector &key, - CryptoSpan iv, - CryptoSpan aad, - bool decryptMode, - CryptoSpan tag) { - 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"); - } - decrypt = decryptMode; - if (decrypt) { - tagVerify.assign(tag.data(), tag.data() + tag.size()); - } else { - tagVerify.clear(); - } - if (mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key.data(), key.size() * 8) != 0) { - return makeStatus(CryptoStatus::InternalError, "gcm setkey failed"); - } - int mode = decrypt ? MBEDTLS_GCM_DECRYPT : MBEDTLS_GCM_ENCRYPT; - if (gcmStartsCompat(ctx, mode, iv, aad) != 0) { - return makeStatus(CryptoStatus::InternalError, "gcm start failed"); - } - started = true; - return makeStatus(CryptoStatus::Ok); -} - -CryptoStatusDetail AesGcmCtx::beginEncrypt(const std::vector &key, - CryptoSpan iv, - CryptoSpan aad) { - return beginCommon(key, iv, aad, false, CryptoSpan()); -} - -CryptoStatusDetail AesGcmCtx::beginDecrypt(const std::vector &key, - CryptoSpan iv, - CryptoSpan aad, - CryptoSpan tag) { - if (tag.size() != AES_GCM_TAG_BYTES) { - return makeStatus(CryptoStatus::InvalidInput, "tag size invalid"); - } - return beginCommon(key, iv, aad, true, tag); + mbedtls_gcm_free(&ctx); + mbedtls_platform_zeroize(tagVerify.data(), tagVerify.size()); +} + +CryptoStatusDetail AesGcmCtx::beginCommon( + const std::vector &key, + CryptoSpan iv, + CryptoSpan aad, + bool decryptMode, + CryptoSpan tag +) { + 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"); + } + decrypt = decryptMode; + if (decrypt) { + tagVerify.assign(tag.data(), tag.data() + tag.size()); + } else { + tagVerify.clear(); + } + if (mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key.data(), key.size() * 8) != 0) { + return makeStatus(CryptoStatus::InternalError, "gcm setkey failed"); + } + int mode = decrypt ? MBEDTLS_GCM_DECRYPT : MBEDTLS_GCM_ENCRYPT; + if (gcmStartsCompat(ctx, mode, iv, aad) != 0) { + return makeStatus(CryptoStatus::InternalError, "gcm start failed"); + } + started = true; + return makeStatus(CryptoStatus::Ok); +} + +CryptoStatusDetail AesGcmCtx::beginEncrypt( + const std::vector &key, CryptoSpan iv, CryptoSpan aad +) { + return beginCommon(key, iv, aad, false, CryptoSpan()); +} + +CryptoStatusDetail AesGcmCtx::beginDecrypt( + const std::vector &key, + CryptoSpan iv, + CryptoSpan aad, + CryptoSpan tag +) { + if (tag.size() != AES_GCM_TAG_BYTES) { + return makeStatus(CryptoStatus::InvalidInput, "tag size invalid"); + } + return beginCommon(key, iv, aad, true, tag); } CryptoStatusDetail AesGcmCtx::update(CryptoSpan input, CryptoSpan output) { - if (!started) { - return makeStatus(CryptoStatus::InvalidInput, "gcm not started"); - } - if (output.size() < input.size()) { - return makeStatus(CryptoStatus::BufferTooSmall, "output too small"); - } - if (input.empty()) { - return makeStatus(CryptoStatus::Ok); - } - if (gcmUpdateCompat(ctx, input, output) != 0) { - return makeStatus(CryptoStatus::InternalError, "gcm update failed"); - } - return makeStatus(CryptoStatus::Ok); + if (!started) { + return makeStatus(CryptoStatus::InvalidInput, "gcm not started"); + } + if (output.size() < input.size()) { + return makeStatus(CryptoStatus::BufferTooSmall, "output too small"); + } + if (input.empty()) { + return makeStatus(CryptoStatus::Ok); + } + if (gcmUpdateCompat(ctx, input, output) != 0) { + return makeStatus(CryptoStatus::InternalError, "gcm update failed"); + } + return makeStatus(CryptoStatus::Ok); } CryptoStatusDetail AesGcmCtx::finish(CryptoSpan tagOut) { - if (!started) { - return makeStatus(CryptoStatus::InvalidInput, "gcm not started"); - } - started = false; - if (!decrypt) { - if (tagOut.size() < AES_GCM_TAG_BYTES) { - return makeStatus(CryptoStatus::BufferTooSmall, "tag too small"); - } - if (gcmFinishCompat(ctx, CryptoSpan(tagOut.data(), AES_GCM_TAG_BYTES)) != 0) { - return makeStatus(CryptoStatus::InternalError, "gcm finish failed"); - } - return makeStatus(CryptoStatus::Ok); - } - std::vector computed(AES_GCM_TAG_BYTES, 0); - if (gcmFinishCompat(ctx, CryptoSpan(computed)) != 0) { - return makeStatus(CryptoStatus::InternalError, "gcm finish failed"); - } - bool ok = constantTimeEquals(CryptoSpan(tagVerify), CryptoSpan(computed)); - mbedtls_platform_zeroize(computed.data(), computed.size()); - return ok ? makeStatus(CryptoStatus::Ok) : makeStatus(CryptoStatus::VerifyFailed, "gcm tag mismatch"); + if (!started) { + return makeStatus(CryptoStatus::InvalidInput, "gcm not started"); + } + started = false; + if (!decrypt) { + if (tagOut.size() < AES_GCM_TAG_BYTES) { + return makeStatus(CryptoStatus::BufferTooSmall, "tag too small"); + } + if (gcmFinishCompat(ctx, CryptoSpan(tagOut.data(), AES_GCM_TAG_BYTES)) != 0) { + return makeStatus(CryptoStatus::InternalError, "gcm finish failed"); + } + return makeStatus(CryptoStatus::Ok); + } + std::vector computed(AES_GCM_TAG_BYTES, 0); + if (gcmFinishCompat(ctx, CryptoSpan(computed)) != 0) { + return makeStatus(CryptoStatus::InternalError, "gcm finish failed"); + } + bool ok = constantTimeEquals( + CryptoSpan(tagVerify), + CryptoSpan(computed) + ); + mbedtls_platform_zeroize(computed.data(), computed.size()); + return ok ? makeStatus(CryptoStatus::Ok) + : makeStatus(CryptoStatus::VerifyFailed, "gcm tag mismatch"); } std::string handleKeyString(const KeyHandle &handle) { - std::string alias(handle.alias.c_str(), handle.alias.length()); - if (alias.empty()) { - return std::string(); - } - return alias + ":" + std::to_string(handle.version); + std::string alias(handle.alias.c_str(), handle.alias.length()); + if (alias.empty()) { + return std::string(); + } + return alias + ":" + std::to_string(handle.version); } bool ensureNvsReady(const String &partition) { #if defined(ESP_PLATFORM) - 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()); - if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { - nvs_flash_erase_partition(partition.c_str()); - err = nvs_flash_init_partition(partition.c_str()); - } - bool ok = (err == ESP_OK); - state.nvsInitMap[partition.c_str()] = ok; - if (ok) { - markRuntimeInitialized(); - } - return ok; + 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()); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { + nvs_flash_erase_partition(partition.c_str()); + err = nvs_flash_init_partition(partition.c_str()); + } + bool ok = (err == ESP_OK); + state.nvsInitMap[partition.c_str()] = ok; + if (ok) { + markRuntimeInitialized(); + } + return ok; #else - (void)partition; - return false; + (void)partition; + return false; #endif } -uint64_t loadCounterFromNvs(const String &ns, const String &partition, const std::string &key, bool &found) { - found = false; - uint64_t value = 0; +uint64_t +loadCounterFromNvs(const String &ns, const String &partition, const std::string &key, bool &found) { + found = false; + uint64_t value = 0; #if defined(ESP_PLATFORM) - if (!ensureNvsReady(partition)) { - return value; - } - nvs_handle_t nvs; - if (nvs_open_from_partition(partition.c_str(), ns.c_str(), NVS_READONLY, &nvs) != ESP_OK) { - return value; - } - size_t size = sizeof(uint64_t); - if (nvs_get_blob(nvs, key.c_str(), &value, &size) == ESP_OK && size == sizeof(uint64_t)) { - found = true; - } - nvs_close(nvs); + if (!ensureNvsReady(partition)) { + return value; + } + nvs_handle_t nvs; + if (nvs_open_from_partition(partition.c_str(), ns.c_str(), NVS_READONLY, &nvs) != ESP_OK) { + return value; + } + size_t size = sizeof(uint64_t); + if (nvs_get_blob(nvs, key.c_str(), &value, &size) == ESP_OK && size == sizeof(uint64_t)) { + found = true; + } + nvs_close(nvs); #else - (void)ns; - (void)partition; - (void)key; + (void)ns; + (void)partition; + (void)key; #endif - return value; + return value; } -void storeCounterToNvs(const String &ns, const String &partition, const std::string &key, uint64_t value) { +void storeCounterToNvs( + const String &ns, const String &partition, const std::string &key, uint64_t value +) { #if defined(ESP_PLATFORM) - if (!ensureNvsReady(partition)) { - return; - } - nvs_handle_t nvs; - if (nvs_open_from_partition(partition.c_str(), ns.c_str(), NVS_READWRITE, &nvs) != ESP_OK) { - return; - } - nvs_set_blob(nvs, key.c_str(), &value, sizeof(value)); - nvs_commit(nvs); - nvs_close(nvs); + if (!ensureNvsReady(partition)) { + return; + } + nvs_handle_t nvs; + if (nvs_open_from_partition(partition.c_str(), ns.c_str(), NVS_READWRITE, &nvs) != ESP_OK) { + return; + } + nvs_set_blob(nvs, key.c_str(), &value, sizeof(value)); + nvs_commit(nvs); + nvs_close(nvs); #else - (void)ns; - (void)partition; - (void)key; - (void)value; + (void)ns; + (void)partition; + (void)key; + (void)value; #endif } CryptoResult> MemoryKeyStore::load(const KeyHandle &handle) { - CryptoResult> result; - std::string key = handleKeyString(handle); - if (key.empty()) { - result.status = makeStatus(CryptoStatus::InvalidInput, "alias missing"); - return result; - } - auto it = storage.find(key); - if (it == storage.end()) { - result.status = makeStatus(CryptoStatus::DecodeError, "key not found"); - return result; - } - result.value = it->second; - result.status = makeStatus(CryptoStatus::Ok); - return result; + CryptoResult> result; + std::string key = handleKeyString(handle); + if (key.empty()) { + result.status = makeStatus(CryptoStatus::InvalidInput, "alias missing"); + return result; + } + auto it = storage.find(key); + if (it == storage.end()) { + result.status = makeStatus(CryptoStatus::DecodeError, "key not found"); + return result; + } + result.value = it->second; + result.status = makeStatus(CryptoStatus::Ok); + return result; } CryptoStatusDetail MemoryKeyStore::store(const KeyHandle &handle, CryptoSpan key) { - std::string k = handleKeyString(handle); - if (k.empty() || key.empty()) { - return makeStatus(CryptoStatus::InvalidInput, "alias/key missing"); - } - storage[k] = std::vector(key.data(), key.data() + key.size()); - return makeStatus(CryptoStatus::Ok); + std::string k = handleKeyString(handle); + if (k.empty() || key.empty()) { + return makeStatus(CryptoStatus::InvalidInput, "alias/key missing"); + } + storage[k] = std::vector(key.data(), key.data() + key.size()); + return makeStatus(CryptoStatus::Ok); } CryptoStatusDetail MemoryKeyStore::remove(const KeyHandle &handle) { - std::string k = handleKeyString(handle); - if (k.empty()) { - return makeStatus(CryptoStatus::InvalidInput, "alias missing"); - } - storage.erase(k); - return makeStatus(CryptoStatus::Ok); + std::string k = handleKeyString(handle); + if (k.empty()) { + return makeStatus(CryptoStatus::InvalidInput, "alias missing"); + } + storage.erase(k); + return makeStatus(CryptoStatus::Ok); } -NvsKeyStore::NvsKeyStore(String ns, String partition) : ns(std::move(ns)), partition(std::move(partition)) {} +NvsKeyStore::NvsKeyStore(String ns, String partition) + : ns(std::move(ns)), partition(std::move(partition)) { +} CryptoStatusDetail NvsKeyStore::ensureInit() const { #if defined(ESP_PLATFORM) - if (!ensureNvsReady(partition)) { - return makeStatus(CryptoStatus::InternalError, "nvs init failed"); - } - return makeStatus(CryptoStatus::Ok); + if (!ensureNvsReady(partition)) { + return makeStatus(CryptoStatus::InternalError, "nvs init failed"); + } + return makeStatus(CryptoStatus::Ok); #else - (void)partition; - return makeStatus(CryptoStatus::Unsupported, "nvs unavailable"); + (void)partition; + return makeStatus(CryptoStatus::Unsupported, "nvs unavailable"); #endif } String NvsKeyStore::makeKeyName(const KeyHandle &handle) const { - return String(handleKeyString(handle).c_str()); + return String(handleKeyString(handle).c_str()); } CryptoResult> NvsKeyStore::load(const KeyHandle &handle) { - CryptoResult> result; + CryptoResult> result; #if defined(ESP_PLATFORM) - auto initStatus = ensureInit(); - if (!initStatus.ok()) { - result.status = initStatus; - return result; - } - std::string key = handleKeyString(handle); - if (key.empty()) { - result.status = makeStatus(CryptoStatus::InvalidInput, "alias missing"); - return result; - } - nvs_handle_t nvs; - if (nvs_open_from_partition(partition.c_str(), ns.c_str(), NVS_READONLY, &nvs) != ESP_OK) { - result.status = makeStatus(CryptoStatus::DecodeError, "nvs open failed"); - return result; - } - size_t size = 0; - esp_err_t err = nvs_get_blob(nvs, key.c_str(), nullptr, &size); - if (err != ESP_OK) { - nvs_close(nvs); - result.status = makeStatus(CryptoStatus::DecodeError, "key missing"); - return result; - } - result.value.assign(size, 0); - err = nvs_get_blob(nvs, key.c_str(), result.value.data(), &size); - nvs_close(nvs); - if (err != ESP_OK) { - result.value.clear(); - result.status = makeStatus(CryptoStatus::InternalError, "read failed"); - return result; - } - result.status = makeStatus(CryptoStatus::Ok); - return result; + auto initStatus = ensureInit(); + if (!initStatus.ok()) { + result.status = initStatus; + return result; + } + std::string key = handleKeyString(handle); + if (key.empty()) { + result.status = makeStatus(CryptoStatus::InvalidInput, "alias missing"); + return result; + } + nvs_handle_t nvs; + if (nvs_open_from_partition(partition.c_str(), ns.c_str(), NVS_READONLY, &nvs) != ESP_OK) { + result.status = makeStatus(CryptoStatus::DecodeError, "nvs open failed"); + return result; + } + size_t size = 0; + esp_err_t err = nvs_get_blob(nvs, key.c_str(), nullptr, &size); + if (err != ESP_OK) { + nvs_close(nvs); + result.status = makeStatus(CryptoStatus::DecodeError, "key missing"); + return result; + } + result.value.assign(size, 0); + err = nvs_get_blob(nvs, key.c_str(), result.value.data(), &size); + nvs_close(nvs); + if (err != ESP_OK) { + result.value.clear(); + result.status = makeStatus(CryptoStatus::InternalError, "read failed"); + return result; + } + result.status = makeStatus(CryptoStatus::Ok); + return result; #else - (void)handle; - result.status = makeStatus(CryptoStatus::Unsupported, "nvs unavailable"); - return result; + (void)handle; + result.status = makeStatus(CryptoStatus::Unsupported, "nvs unavailable"); + return result; #endif } CryptoStatusDetail NvsKeyStore::store(const KeyHandle &handle, CryptoSpan key) { #if defined(ESP_PLATFORM) - auto initStatus = ensureInit(); - if (!initStatus.ok()) { - return initStatus; - } - std::string name = handleKeyString(handle); - if (name.empty() || key.empty()) { - return makeStatus(CryptoStatus::InvalidInput, "alias/key missing"); - } - nvs_handle_t nvs; - if (nvs_open_from_partition(partition.c_str(), ns.c_str(), NVS_READWRITE, &nvs) != ESP_OK) { - return makeStatus(CryptoStatus::InternalError, "nvs open failed"); - } - esp_err_t err = nvs_set_blob(nvs, name.c_str(), key.data(), key.size()); - if (err == ESP_OK) { - err = nvs_commit(nvs); - } - nvs_close(nvs); - if (err != ESP_OK) { - return makeStatus(CryptoStatus::InternalError, "nvs write failed"); - } - return makeStatus(CryptoStatus::Ok); + auto initStatus = ensureInit(); + if (!initStatus.ok()) { + return initStatus; + } + std::string name = handleKeyString(handle); + if (name.empty() || key.empty()) { + return makeStatus(CryptoStatus::InvalidInput, "alias/key missing"); + } + nvs_handle_t nvs; + if (nvs_open_from_partition(partition.c_str(), ns.c_str(), NVS_READWRITE, &nvs) != ESP_OK) { + return makeStatus(CryptoStatus::InternalError, "nvs open failed"); + } + esp_err_t err = nvs_set_blob(nvs, name.c_str(), key.data(), key.size()); + if (err == ESP_OK) { + err = nvs_commit(nvs); + } + nvs_close(nvs); + if (err != ESP_OK) { + return makeStatus(CryptoStatus::InternalError, "nvs write failed"); + } + return makeStatus(CryptoStatus::Ok); #else - (void)handle; - (void)key; - return makeStatus(CryptoStatus::Unsupported, "nvs unavailable"); + (void)handle; + (void)key; + return makeStatus(CryptoStatus::Unsupported, "nvs unavailable"); #endif } CryptoStatusDetail NvsKeyStore::remove(const KeyHandle &handle) { #if defined(ESP_PLATFORM) - auto initStatus = ensureInit(); - if (!initStatus.ok()) { - return initStatus; - } - std::string name = handleKeyString(handle); - if (name.empty()) { - return makeStatus(CryptoStatus::InvalidInput, "alias missing"); - } - nvs_handle_t nvs; - if (nvs_open_from_partition(partition.c_str(), ns.c_str(), NVS_READWRITE, &nvs) != ESP_OK) { - return makeStatus(CryptoStatus::InternalError, "nvs open failed"); - } - esp_err_t err = nvs_erase_key(nvs, name.c_str()); - if (err == ESP_OK || err == ESP_ERR_NVS_NOT_FOUND) { - nvs_commit(nvs); - } - nvs_close(nvs); - return makeStatus(CryptoStatus::Ok); + auto initStatus = ensureInit(); + if (!initStatus.ok()) { + return initStatus; + } + std::string name = handleKeyString(handle); + if (name.empty()) { + return makeStatus(CryptoStatus::InvalidInput, "alias missing"); + } + nvs_handle_t nvs; + if (nvs_open_from_partition(partition.c_str(), ns.c_str(), NVS_READWRITE, &nvs) != ESP_OK) { + return makeStatus(CryptoStatus::InternalError, "nvs open failed"); + } + esp_err_t err = nvs_erase_key(nvs, name.c_str()); + if (err == ESP_OK || err == ESP_ERR_NVS_NOT_FOUND) { + nvs_commit(nvs); + } + nvs_close(nvs); + return makeStatus(CryptoStatus::Ok); #else - (void)handle; - return makeStatus(CryptoStatus::Unsupported, "nvs unavailable"); + (void)handle; + return makeStatus(CryptoStatus::Unsupported, "nvs unavailable"); #endif } -LittleFsKeyStore::LittleFsKeyStore(String basePath) : basePath(std::move(basePath)) {} +LittleFsKeyStore::LittleFsKeyStore(String basePath) : basePath(std::move(basePath)) { +} String LittleFsKeyStore::makePath(const KeyHandle &handle) const { - std::string name = handleKeyString(handle); - if (name.empty()) { - return String(); - } - if (basePath.endsWith("/")) { - return basePath + name.c_str(); - } - return basePath + "/" + name.c_str(); + std::string name = handleKeyString(handle); + if (name.empty()) { + return String(); + } + if (basePath.endsWith("/")) { + return basePath + name.c_str(); + } + return basePath + "/" + name.c_str(); } CryptoResult> LittleFsKeyStore::load(const KeyHandle &handle) { - CryptoResult> result; + CryptoResult> result; #if ESPCRYPTO_HAS_LITTLEFS - String path = makePath(handle); - if (path.length() == 0) { - result.status = makeStatus(CryptoStatus::InvalidInput, "alias missing"); - return result; - } - if (!LittleFS.begin()) { - result.status = makeStatus(CryptoStatus::InternalError, "littlefs mount failed"); - return result; - } - File f = LittleFS.open(path, "r"); - if (!f) { - result.status = makeStatus(CryptoStatus::DecodeError, "key missing"); - return result; - } - result.value.assign(f.size(), 0); - size_t read = f.read(result.value.data(), result.value.size()); - f.close(); - if (read != result.value.size()) { - result.value.clear(); - result.status = makeStatus(CryptoStatus::InternalError, "short read"); - return result; - } - result.status = makeStatus(CryptoStatus::Ok); - return result; + String path = makePath(handle); + if (path.length() == 0) { + result.status = makeStatus(CryptoStatus::InvalidInput, "alias missing"); + return result; + } + if (!LittleFS.begin()) { + result.status = makeStatus(CryptoStatus::InternalError, "littlefs mount failed"); + return result; + } + File f = LittleFS.open(path, "r"); + if (!f) { + result.status = makeStatus(CryptoStatus::DecodeError, "key missing"); + return result; + } + result.value.assign(f.size(), 0); + size_t read = f.read(result.value.data(), result.value.size()); + f.close(); + if (read != result.value.size()) { + result.value.clear(); + result.status = makeStatus(CryptoStatus::InternalError, "short read"); + return result; + } + result.status = makeStatus(CryptoStatus::Ok); + return result; #else - (void)handle; - result.status = makeStatus(CryptoStatus::Unsupported, "littlefs unavailable"); - return result; + (void)handle; + result.status = makeStatus(CryptoStatus::Unsupported, "littlefs unavailable"); + return result; #endif } CryptoStatusDetail LittleFsKeyStore::store(const KeyHandle &handle, CryptoSpan key) { #if ESPCRYPTO_HAS_LITTLEFS - String path = makePath(handle); - if (path.length() == 0 || key.empty()) { - return makeStatus(CryptoStatus::InvalidInput, "alias/key missing"); - } - if (!LittleFS.begin()) { - return makeStatus(CryptoStatus::InternalError, "littlefs mount failed"); - } - if (!LittleFS.exists(basePath)) { - LittleFS.mkdir(basePath); - } - File f = LittleFS.open(path, "w"); - if (!f) { - return makeStatus(CryptoStatus::InternalError, "open failed"); - } - size_t written = f.write(key.data(), key.size()); - f.close(); - if (written != key.size()) { - return makeStatus(CryptoStatus::InternalError, "write failed"); - } - return makeStatus(CryptoStatus::Ok); + String path = makePath(handle); + if (path.length() == 0 || key.empty()) { + return makeStatus(CryptoStatus::InvalidInput, "alias/key missing"); + } + if (!LittleFS.begin()) { + return makeStatus(CryptoStatus::InternalError, "littlefs mount failed"); + } + if (!LittleFS.exists(basePath)) { + LittleFS.mkdir(basePath); + } + File f = LittleFS.open(path, "w"); + if (!f) { + return makeStatus(CryptoStatus::InternalError, "open failed"); + } + size_t written = f.write(key.data(), key.size()); + f.close(); + if (written != key.size()) { + return makeStatus(CryptoStatus::InternalError, "write failed"); + } + return makeStatus(CryptoStatus::Ok); #else - (void)handle; - (void)key; - return makeStatus(CryptoStatus::Unsupported, "littlefs unavailable"); + (void)handle; + (void)key; + return makeStatus(CryptoStatus::Unsupported, "littlefs unavailable"); #endif } CryptoStatusDetail LittleFsKeyStore::remove(const KeyHandle &handle) { #if ESPCRYPTO_HAS_LITTLEFS - String path = makePath(handle); - if (path.length() == 0) { - return makeStatus(CryptoStatus::InvalidInput, "alias missing"); - } - if (!LittleFS.begin()) { - return makeStatus(CryptoStatus::InternalError, "littlefs mount failed"); - } - LittleFS.remove(path); - return makeStatus(CryptoStatus::Ok); + String path = makePath(handle); + if (path.length() == 0) { + return makeStatus(CryptoStatus::InvalidInput, "alias missing"); + } + if (!LittleFS.begin()) { + return makeStatus(CryptoStatus::InternalError, "littlefs mount failed"); + } + LittleFS.remove(path); + return makeStatus(CryptoStatus::Ok); #else - (void)handle; - return makeStatus(CryptoStatus::Unsupported, "littlefs unavailable"); + (void)handle; + return makeStatus(CryptoStatus::Unsupported, "littlefs unavailable"); #endif } std::vector deviceFingerprint() { - std::vector fingerprint; + std::vector fingerprint; #if defined(ESP_PLATFORM) - uint8_t mac[6] = {0}; - bool haveMac = false; + uint8_t mac[6] = {0}; + bool haveMac = false; #if ESPCRYPTO_HAS_ESP_MAC && defined(ESP_MAC_WIFI_STA) - if (esp_read_mac(mac, ESP_MAC_WIFI_STA) == ESP_OK) { - haveMac = true; - } + if (esp_read_mac(mac, ESP_MAC_WIFI_STA) == ESP_OK) { + haveMac = true; + } #endif #if ESPCRYPTO_HAS_ESP_EFUSE_MAC - if (!haveMac && esp_efuse_mac_get_default(mac) == ESP_OK) { - haveMac = true; - } + if (!haveMac && esp_efuse_mac_get_default(mac) == ESP_OK) { + haveMac = true; + } #endif - if (haveMac) { - fingerprint.insert(fingerprint.end(), mac, mac + sizeof(mac)); - } + if (haveMac) { + fingerprint.insert(fingerprint.end(), mac, mac + sizeof(mac)); + } #else - std::random_device rd; - for (size_t i = 0; i < 8; ++i) { - fingerprint.push_back(static_cast(rd())); - } + std::random_device rd; + for (size_t i = 0; i < 8; ++i) { + fingerprint.push_back(static_cast(rd())); + } #endif - if (fingerprint.empty()) { - fingerprint.resize(8, 0xAA); - } - return fingerprint; + if (fingerprint.empty()) { + fingerprint.resize(8, 0xAA); + } + return fingerprint; } CryptoStatusDetail loadOrCreateSeed(std::vector &seed, const DeviceKeyOptions &options) { - if (options.seedBytes == 0) { - return makeStatus(CryptoStatus::InvalidInput, "seed size missing"); - } - seed.assign(options.seedBytes, 0); + if (options.seedBytes == 0) { + return makeStatus(CryptoStatus::InvalidInput, "seed size missing"); + } + seed.assign(options.seedBytes, 0); #if defined(ESP_PLATFORM) - if (options.persistSeed) { - NvsKeyStore store(options.nvsNamespace, options.nvsPartition); - KeyHandle handle; - handle.alias = "device_seed"; - auto loaded = store.load(handle); - if (loaded.ok() && loaded.value.size() == options.seedBytes) { - seed = loaded.value; - return makeStatus(CryptoStatus::Ok); - } - fillRandom(seed.data(), seed.size()); - auto writeStatus = store.store(handle, CryptoSpan(seed)); - if (!writeStatus.ok()) { - return writeStatus; - } - return makeStatus(CryptoStatus::Ok); - } + if (options.persistSeed) { + NvsKeyStore store(options.nvsNamespace, options.nvsPartition); + KeyHandle handle; + handle.alias = "device_seed"; + auto loaded = store.load(handle); + if (loaded.ok() && loaded.value.size() == options.seedBytes) { + seed = loaded.value; + return makeStatus(CryptoStatus::Ok); + } + fillRandom(seed.data(), seed.size()); + auto writeStatus = store.store(handle, CryptoSpan(seed)); + if (!writeStatus.ok()) { + return writeStatus; + } + return makeStatus(CryptoStatus::Ok); + } #endif - fillRandom(seed.data(), seed.size()); - return makeStatus(CryptoStatus::Ok); + fillRandom(seed.data(), seed.size()); + return makeStatus(CryptoStatus::Ok); } CryptoKey::CryptoKey() = default; CryptoKey::CryptoKey(const CryptoKey &other) { - data = other.data; - format = other.format; - keyKind = other.keyKind; - pk = nullptr; + data = other.data; + format = other.format; + keyKind = other.keyKind; + pk = nullptr; } CryptoKey &CryptoKey::operator=(const CryptoKey &other) { - if (this != &other) { - clear(); - data = other.data; - format = other.format; - keyKind = other.keyKind; - } - return *this; + if (this != &other) { + clear(); + data = other.data; + format = other.format; + keyKind = other.keyKind; + } + return *this; } CryptoKey::CryptoKey(CryptoKey &&other) noexcept { - data = std::move(other.data); - format = other.format; - keyKind = other.keyKind; - pk = other.pk; - other.pk = nullptr; + data = std::move(other.data); + format = other.format; + keyKind = other.keyKind; + pk = other.pk; + other.pk = nullptr; } CryptoKey &CryptoKey::operator=(CryptoKey &&other) noexcept { - if (this != &other) { - clear(); - data = std::move(other.data); - format = other.format; - keyKind = other.keyKind; - pk = other.pk; - other.pk = nullptr; - } - return *this; + if (this != &other) { + clear(); + data = std::move(other.data); + format = other.format; + keyKind = other.keyKind; + pk = other.pk; + other.pk = nullptr; + } + return *this; } CryptoKey::~CryptoKey() { - clear(); + clear(); } CryptoKey CryptoKey::fromPem(const std::string &pem, KeyKind kind) { - CryptoKey key; - key.data.assign(pem.begin(), pem.end()); - key.data.push_back('\0'); - key.format = KeyFormat::Pem; - key.keyKind = kind; - return key; + CryptoKey key; + key.data.assign(pem.begin(), pem.end()); + key.data.push_back('\0'); + key.format = KeyFormat::Pem; + key.keyKind = kind; + return key; } CryptoKey CryptoKey::fromDer(const std::vector &der, KeyKind kind) { - CryptoKey key; - key.data = der; - key.format = KeyFormat::Der; - key.keyKind = kind; - return key; + CryptoKey key; + key.data = der; + key.format = KeyFormat::Der; + key.keyKind = kind; + return key; } CryptoKey CryptoKey::fromRaw(const std::vector &raw, KeyKind kind) { - CryptoKey key; - key.data = raw; - key.format = KeyFormat::Raw; - key.keyKind = kind; - return key; + CryptoKey key; + key.data = raw; + key.format = KeyFormat::Raw; + key.keyKind = kind; + return key; } bool CryptoKey::valid() const { - return !data.empty(); + return !data.empty(); } KeyKind CryptoKey::kind() const { - return keyKind; + return keyKind; } CryptoSpan CryptoKey::bytes() const { - return CryptoSpan(data); + return CryptoSpan(data); } bool CryptoKey::parsed() const { - return pk && pk->hasKey; + return pk && pk->hasKey; } void CryptoKey::clear() { - if (pk) { - mbedtls_pk_free(&pk->ctx); - delete pk; - pk = nullptr; - } - if (!data.empty()) { - secureZero(data.data(), data.size()); - data.clear(); - } - keyKind = KeyKind::Auto; - format = KeyFormat::Raw; + if (pk) { + mbedtls_pk_free(&pk->ctx); + delete pk; + pk = nullptr; + } + if (!data.empty()) { + secureZero(data.data(), data.size()); + data.clear(); + } + keyKind = KeyKind::Auto; + format = KeyFormat::Raw; } CryptoStatusDetail CryptoKey::ensureParsedPk(bool requirePrivate) const { - if (format != KeyFormat::Pem && format != KeyFormat::Der) { - return makeStatus(CryptoStatus::Unsupported, "pk parse requires pem/der"); - } - if (pk && pk->hasKey) { - if (requirePrivate && !pk->isPrivate) { - return makeStatus(CryptoStatus::PolicyViolation, "private key required"); - } - return makeStatus(CryptoStatus::Ok); - } - pk = new PkCache(); - mbedtls_pk_init(&pk->ctx); - int ret = 0; - if (format == KeyFormat::Pem) { - ret = mbedtls_pk_parse_public_key(&pk->ctx, - reinterpret_cast(data.data()), - data.size()); - if (ret == 0) { - pk->hasKey = true; - pk->isPrivate = false; - } - } - mbedtls_ctr_drbg_context ctr; - mbedtls_entropy_context entropy; - bool seeded = initDrbg(ctr, entropy); - if (!pk->hasKey && seeded) { + if (format != KeyFormat::Pem && format != KeyFormat::Der) { + return makeStatus(CryptoStatus::Unsupported, "pk parse requires pem/der"); + } + if (pk && pk->hasKey) { + if (requirePrivate && !pk->isPrivate) { + return makeStatus(CryptoStatus::PolicyViolation, "private key required"); + } + return makeStatus(CryptoStatus::Ok); + } + pk = new PkCache(); + mbedtls_pk_init(&pk->ctx); + int ret = 0; + if (format == KeyFormat::Pem) { + ret = mbedtls_pk_parse_public_key( + &pk->ctx, + reinterpret_cast(data.data()), + data.size() + ); + if (ret == 0) { + pk->hasKey = true; + pk->isPrivate = false; + } + } + mbedtls_ctr_drbg_context ctr; + mbedtls_entropy_context entropy; + bool seeded = initDrbg(ctr, entropy); + if (!pk->hasKey && seeded) { #if ESPCRYPTO_MBEDTLS_V3 - ret = mbedtls_pk_parse_key(&pk->ctx, - reinterpret_cast(data.data()), - format == KeyFormat::Pem ? data.size() : data.size(), - nullptr, - 0, - mbedtls_ctr_drbg_random, - &ctr); + ret = mbedtls_pk_parse_key( + &pk->ctx, + reinterpret_cast(data.data()), + format == KeyFormat::Pem ? data.size() : data.size(), + nullptr, + 0, + mbedtls_ctr_drbg_random, + &ctr + ); #else - ret = mbedtls_pk_parse_key(&pk->ctx, - reinterpret_cast(data.data()), - format == KeyFormat::Pem ? data.size() : data.size(), - nullptr, - 0); + ret = mbedtls_pk_parse_key( + &pk->ctx, + reinterpret_cast(data.data()), + format == KeyFormat::Pem ? data.size() : data.size(), + nullptr, + 0 + ); #endif - if (ret == 0) { - pk->hasKey = true; - pk->isPrivate = true; - } - } - if (seeded) { - mbedtls_ctr_drbg_free(&ctr); - mbedtls_entropy_free(&entropy); - } - if (!pk->hasKey) { - mbedtls_pk_free(&pk->ctx); - delete pk; - pk = nullptr; - return makeStatus(CryptoStatus::DecodeError, "pk parse failed"); - } - if (requirePrivate && !pk->isPrivate) { - return makeStatus(CryptoStatus::PolicyViolation, "private key required"); - } - return makeStatus(CryptoStatus::Ok); + if (ret == 0) { + pk->hasKey = true; + pk->isPrivate = true; + } + } + if (seeded) { + mbedtls_ctr_drbg_free(&ctr); + mbedtls_entropy_free(&entropy); + } + if (!pk->hasKey) { + mbedtls_pk_free(&pk->ctx); + delete pk; + pk = nullptr; + return makeStatus(CryptoStatus::DecodeError, "pk parse failed"); + } + if (requirePrivate && !pk->isPrivate) { + return makeStatus(CryptoStatus::PolicyViolation, "private key required"); + } + return makeStatus(CryptoStatus::Ok); } std::string algorithmName(JwtAlgorithm alg) { - switch (alg) { - case JwtAlgorithm::HS256: - return "HS256"; - case JwtAlgorithm::RS256: - return "RS256"; - case JwtAlgorithm::ES256: - return "ES256"; - case JwtAlgorithm::Auto: - default: - return ""; - } + switch (alg) { + case JwtAlgorithm::HS256: + return "HS256"; + case JwtAlgorithm::RS256: + return "RS256"; + case JwtAlgorithm::ES256: + return "ES256"; + case JwtAlgorithm::Auto: + default: + return ""; + } } JwtAlgorithm algorithmFromName(const std::string &name) { - if (name == "HS256") { - return JwtAlgorithm::HS256; - } - if (name == "RS256") { - return JwtAlgorithm::RS256; - } - if (name == "ES256") { - return JwtAlgorithm::ES256; - } - return JwtAlgorithm::Auto; -} - -bool hmacSha256(const std::string &key, const uint8_t *data, size_t length, std::vector &out) { - const mbedtls_md_info_t *info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); - if (!info) { - return false; - } - out.assign(mbedtls_md_get_size(info), 0); - mbedtls_md_context_t ctx; - mbedtls_md_init(&ctx); - int ret = mbedtls_md_setup(&ctx, info, 1); - if (ret == 0) { - ret = mbedtls_md_hmac_starts(&ctx, reinterpret_cast(key.data()), key.size()); - } - if (ret == 0) { - ret = mbedtls_md_hmac_update(&ctx, data, length); - } - if (ret == 0) { - ret = mbedtls_md_hmac_finish(&ctx, out.data()); - } - mbedtls_md_free(&ctx); - if (ret != 0) { - out.clear(); - return false; - } - return true; + if (name == "HS256") { + return JwtAlgorithm::HS256; + } + if (name == "RS256") { + return JwtAlgorithm::RS256; + } + if (name == "ES256") { + return JwtAlgorithm::ES256; + } + return JwtAlgorithm::Auto; +} + +bool hmacSha256( + const std::string &key, const uint8_t *data, size_t length, std::vector &out +) { + const mbedtls_md_info_t *info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); + if (!info) { + return false; + } + out.assign(mbedtls_md_get_size(info), 0); + mbedtls_md_context_t ctx; + mbedtls_md_init(&ctx); + int ret = mbedtls_md_setup(&ctx, info, 1); + if (ret == 0) { + ret = mbedtls_md_hmac_starts( + &ctx, + reinterpret_cast(key.data()), + key.size() + ); + } + if (ret == 0) { + ret = mbedtls_md_hmac_update(&ctx, data, length); + } + if (ret == 0) { + ret = mbedtls_md_hmac_finish(&ctx, out.data()); + } + mbedtls_md_free(&ctx); + if (ret != 0) { + out.clear(); + return false; + } + return true; } bool initDrbg(mbedtls_ctr_drbg_context &ctr, mbedtls_entropy_context &entropy) { - mbedtls_entropy_init(&entropy); - mbedtls_ctr_drbg_init(&ctr); - static const char *pers = "espcrypto"; - int ret = mbedtls_ctr_drbg_seed(&ctr, mbedtls_entropy_func, &entropy, - reinterpret_cast(pers), strlen(pers)); - if (ret != 0) { - mbedtls_ctr_drbg_free(&ctr); - mbedtls_entropy_free(&entropy); - return false; - } - return true; -} - -bool computeHash(ShaVariant variant, const uint8_t *data, size_t length, std::vector &hash) { - hash.assign(digestLength(variant), 0); - if (hash.empty()) { - return false; - } - static const uint8_t ZERO_BYTE = 0; - const uint8_t *buffer = (!data && length == 0) ? &ZERO_BYTE : data; - if (softwareSha(variant, buffer, length, hash.data())) { - return true; - } - return false; -} - -int pbkdf2Sha256(const unsigned char *password, - size_t passwordLength, - const uint8_t *salt, - size_t saltLength, - uint32_t iterations, - uint8_t *output, - size_t outputLength) { + mbedtls_entropy_init(&entropy); + mbedtls_ctr_drbg_init(&ctr); + static const char *pers = "espcrypto"; + int ret = mbedtls_ctr_drbg_seed( + &ctr, + mbedtls_entropy_func, + &entropy, + reinterpret_cast(pers), + strlen(pers) + ); + if (ret != 0) { + mbedtls_ctr_drbg_free(&ctr); + mbedtls_entropy_free(&entropy); + return false; + } + return true; +} + +bool computeHash( + ShaVariant variant, const uint8_t *data, size_t length, std::vector &hash +) { + hash.assign(digestLength(variant), 0); + if (hash.empty()) { + return false; + } + static const uint8_t ZERO_BYTE = 0; + const uint8_t *buffer = (!data && length == 0) ? &ZERO_BYTE : data; + if (softwareSha(variant, buffer, length, hash.data())) { + return true; + } + return false; +} + +int pbkdf2Sha256( + const unsigned char *password, + size_t passwordLength, + const uint8_t *salt, + size_t saltLength, + uint32_t iterations, + uint8_t *output, + size_t outputLength +) { #if ESPCRYPTO_MBEDTLS_V3 - return mbedtls_pkcs5_pbkdf2_hmac_ext(MBEDTLS_MD_SHA256, - password, - passwordLength, - salt, - saltLength, - iterations, - outputLength, - output); + return mbedtls_pkcs5_pbkdf2_hmac_ext( + MBEDTLS_MD_SHA256, + password, + passwordLength, + salt, + saltLength, + iterations, + outputLength, + output + ); #else - mbedtls_md_context_t ctx; - mbedtls_md_init(&ctx); - const mbedtls_md_info_t *info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); - if (!info) { - mbedtls_md_free(&ctx); - return MBEDTLS_ERR_MD_BAD_INPUT_DATA; - } - int ret = mbedtls_md_setup(&ctx, info, 1); - if (ret == 0) { - ret = mbedtls_pkcs5_pbkdf2_hmac(&ctx, - password, - passwordLength, - salt, - saltLength, - iterations, - outputLength, - output); - } - mbedtls_md_free(&ctx); - return ret; + mbedtls_md_context_t ctx; + mbedtls_md_init(&ctx); + const mbedtls_md_info_t *info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); + if (!info) { + mbedtls_md_free(&ctx); + return MBEDTLS_ERR_MD_BAD_INPUT_DATA; + } + int ret = mbedtls_md_setup(&ctx, info, 1); + if (ret == 0) { + ret = mbedtls_pkcs5_pbkdf2_hmac( + &ctx, + password, + passwordLength, + salt, + saltLength, + iterations, + outputLength, + output + ); + } + mbedtls_md_free(&ctx); + return ret; #endif } -bool pkParsePublicOrPrivate(mbedtls_pk_context &pk, - const std::string &pem, - mbedtls_ctr_drbg_context *ctr, - mbedtls_entropy_context *entropy) { - int ret = mbedtls_pk_parse_public_key(&pk, - reinterpret_cast(pem.c_str()), - pem.size() + 1); - if (ret == 0) { - return true; - } - mbedtls_ctr_drbg_context localCtr; - mbedtls_entropy_context localEntropy; - if (!ctr || !entropy) { - ctr = &localCtr; - entropy = &localEntropy; - if (!initDrbg(localCtr, localEntropy)) { - return false; - } - } +bool pkParsePublicOrPrivate( + mbedtls_pk_context &pk, + const std::string &pem, + mbedtls_ctr_drbg_context *ctr, + mbedtls_entropy_context *entropy +) { + int ret = mbedtls_pk_parse_public_key( + &pk, + reinterpret_cast(pem.c_str()), + pem.size() + 1 + ); + if (ret == 0) { + return true; + } + mbedtls_ctr_drbg_context localCtr; + mbedtls_entropy_context localEntropy; + if (!ctr || !entropy) { + ctr = &localCtr; + entropy = &localEntropy; + if (!initDrbg(localCtr, localEntropy)) { + return false; + } + } #if ESPCRYPTO_MBEDTLS_V3 - ret = mbedtls_pk_parse_key(&pk, - reinterpret_cast(pem.c_str()), - pem.size() + 1, - nullptr, - 0, - mbedtls_ctr_drbg_random, - ctr); + ret = mbedtls_pk_parse_key( + &pk, + reinterpret_cast(pem.c_str()), + pem.size() + 1, + nullptr, + 0, + mbedtls_ctr_drbg_random, + ctr + ); #else - ret = mbedtls_pk_parse_key(&pk, - reinterpret_cast(pem.c_str()), - pem.size() + 1, - nullptr, - 0); + ret = mbedtls_pk_parse_key( + &pk, + reinterpret_cast(pem.c_str()), + pem.size() + 1, + nullptr, + 0 + ); #endif - if (ctr == &localCtr) { - mbedtls_ctr_drbg_free(&localCtr); - mbedtls_entropy_free(&localEntropy); - } - return ret == 0; + if (ctr == &localCtr) { + mbedtls_ctr_drbg_free(&localCtr); + mbedtls_entropy_free(&localEntropy); + } + return ret == 0; } 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) { - if (expected == MBEDTLS_PK_RSA && bitlen < policy.minRsaBits) { - return false; - } - if (expected == MBEDTLS_PK_ECKEY && !policy.allowWeakCurves && bitlen < 256) { - return false; - } - } - return true; -} - -bool pkSignContext(mbedtls_pk_context &pk, - mbedtls_pk_type_t expected, - ShaVariant variant, - const uint8_t *data, - size_t length, - std::vector &signature) { - if (!pkPolicyAllows(pk, expected)) { - return false; - } - std::vector hash; - if (!computeHash(variant, data, length, hash)) { - return false; - } - const mbedtls_md_info_t *info = mdInfoForVariant(variant); - if (!info) { - return false; - } - size_t sigLen = mbedtls_pk_get_len(&pk); - signature.assign(sigLen, 0); - mbedtls_ctr_drbg_context ctr; - mbedtls_entropy_context entropy; - if (!initDrbg(ctr, entropy)) { - signature.clear(); - return false; - } + 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) { + if (expected == MBEDTLS_PK_RSA && bitlen < policy.minRsaBits) { + return false; + } + if (expected == MBEDTLS_PK_ECKEY && !policy.allowWeakCurves && bitlen < 256) { + return false; + } + } + return true; +} + +bool pkSignContext( + mbedtls_pk_context &pk, + mbedtls_pk_type_t expected, + ShaVariant variant, + const uint8_t *data, + size_t length, + std::vector &signature +) { + if (!pkPolicyAllows(pk, expected)) { + return false; + } + std::vector hash; + if (!computeHash(variant, data, length, hash)) { + return false; + } + const mbedtls_md_info_t *info = mdInfoForVariant(variant); + if (!info) { + return false; + } + size_t sigLen = mbedtls_pk_get_len(&pk); + signature.assign(sigLen, 0); + mbedtls_ctr_drbg_context ctr; + mbedtls_entropy_context entropy; + if (!initDrbg(ctr, entropy)) { + signature.clear(); + return false; + } #if ESPCRYPTO_MBEDTLS_V3 - int ret = mbedtls_pk_sign(&pk, - mbedtls_md_get_type(info), - hash.data(), hash.size(), - signature.data(), signature.size(), &sigLen, - mbedtls_ctr_drbg_random, - &ctr); + int ret = mbedtls_pk_sign( + &pk, + mbedtls_md_get_type(info), + hash.data(), + hash.size(), + signature.data(), + signature.size(), + &sigLen, + mbedtls_ctr_drbg_random, + &ctr + ); #else - int ret = mbedtls_pk_sign(&pk, - mbedtls_md_get_type(info), - hash.data(), hash.size(), - signature.data(), &sigLen, - mbedtls_ctr_drbg_random, - &ctr); + int ret = mbedtls_pk_sign( + &pk, + mbedtls_md_get_type(info), + hash.data(), + hash.size(), + signature.data(), + &sigLen, + mbedtls_ctr_drbg_random, + &ctr + ); #endif - mbedtls_ctr_drbg_free(&ctr); - mbedtls_entropy_free(&entropy); - if (ret != 0) { - signature.clear(); - return false; - } - signature.resize(sigLen); - return true; -} - -bool pkVerifyContext(mbedtls_pk_context &pk, - mbedtls_pk_type_t expected, - ShaVariant variant, - const uint8_t *data, - size_t length, - const std::vector &signature) { - if (!pkPolicyAllows(pk, expected)) { - return false; - } - std::vector hash; - if (!computeHash(variant, data, length, hash)) { - return false; - } - const mbedtls_md_info_t *info = mdInfoForVariant(variant); - if (!info) { - return false; - } - int ret = mbedtls_pk_verify(&pk, - mbedtls_md_get_type(info), - hash.data(), hash.size(), - signature.data(), signature.size()); - return ret == 0; -} - -bool pkSignInternal(const std::string &pem, - mbedtls_pk_type_t expected, - ShaVariant variant, - const uint8_t *data, - size_t length, - std::vector &signature) { - mbedtls_pk_context pk; - mbedtls_pk_init(&pk); - mbedtls_ctr_drbg_context ctr; - mbedtls_entropy_context entropy; - if (!initDrbg(ctr, entropy)) { - mbedtls_pk_free(&pk); - return false; - } + mbedtls_ctr_drbg_free(&ctr); + mbedtls_entropy_free(&entropy); + if (ret != 0) { + signature.clear(); + return false; + } + signature.resize(sigLen); + return true; +} + +bool pkVerifyContext( + mbedtls_pk_context &pk, + mbedtls_pk_type_t expected, + ShaVariant variant, + const uint8_t *data, + size_t length, + const std::vector &signature +) { + if (!pkPolicyAllows(pk, expected)) { + return false; + } + std::vector hash; + if (!computeHash(variant, data, length, hash)) { + return false; + } + const mbedtls_md_info_t *info = mdInfoForVariant(variant); + if (!info) { + return false; + } + int ret = mbedtls_pk_verify( + &pk, + mbedtls_md_get_type(info), + hash.data(), + hash.size(), + signature.data(), + signature.size() + ); + return ret == 0; +} + +bool pkSignInternal( + const std::string &pem, + mbedtls_pk_type_t expected, + ShaVariant variant, + const uint8_t *data, + size_t length, + std::vector &signature +) { + mbedtls_pk_context pk; + mbedtls_pk_init(&pk); + mbedtls_ctr_drbg_context ctr; + mbedtls_entropy_context entropy; + if (!initDrbg(ctr, entropy)) { + mbedtls_pk_free(&pk); + return false; + } #if ESPCRYPTO_MBEDTLS_V3 - int ret = mbedtls_pk_parse_key(&pk, - reinterpret_cast(pem.c_str()), - pem.size() + 1, - nullptr, - 0, - mbedtls_ctr_drbg_random, - &ctr); + int ret = mbedtls_pk_parse_key( + &pk, + reinterpret_cast(pem.c_str()), + pem.size() + 1, + nullptr, + 0, + mbedtls_ctr_drbg_random, + &ctr + ); #else - int ret = mbedtls_pk_parse_key(&pk, - reinterpret_cast(pem.c_str()), - pem.size() + 1, - nullptr, - 0); + int ret = mbedtls_pk_parse_key( + &pk, + reinterpret_cast(pem.c_str()), + pem.size() + 1, + nullptr, + 0 + ); #endif - mbedtls_ctr_drbg_free(&ctr); - mbedtls_entropy_free(&entropy); - if (ret != 0) { - mbedtls_pk_free(&pk); - return false; - } - bool ok = pkSignContext(pk, expected, variant, data, length, signature); - mbedtls_pk_free(&pk); - return ok; -} - -bool pkVerifyInternal(const std::string &pem, - mbedtls_pk_type_t expected, - ShaVariant variant, - const uint8_t *data, - size_t length, - const std::vector &signature) { - mbedtls_pk_context pk; - mbedtls_pk_init(&pk); - if (!pkParsePublicOrPrivate(pk, pem, nullptr, nullptr)) { - mbedtls_pk_free(&pk); - return false; - } - bool ok = pkVerifyContext(pk, expected, variant, data, length, signature); - mbedtls_pk_free(&pk); - return ok; -} - -bool signJwt(JwtAlgorithm alg, - const std::string &key, - const uint8_t *data, - size_t length, - std::vector &signature) { - switch (alg) { - case JwtAlgorithm::HS256: - return hmacSha256(key, data, length, signature); - case JwtAlgorithm::RS256: - return pkSignInternal(key, MBEDTLS_PK_RSA, ShaVariant::SHA256, data, length, signature); - case JwtAlgorithm::ES256: - return pkSignInternal(key, MBEDTLS_PK_ECKEY, ShaVariant::SHA256, data, length, signature); - case JwtAlgorithm::Auto: - default: - return false; - } -} - -bool verifySignature(JwtAlgorithm alg, - const std::string &key, - const uint8_t *data, - size_t length, - const std::vector &signature) { - switch (alg) { - case JwtAlgorithm::HS256: { - std::vector expected; - if (!hmacSha256(key, data, length, expected)) { - return false; - } - return constantTimeEquals(expected, signature); - } - case JwtAlgorithm::RS256: - return pkVerifyInternal(key, MBEDTLS_PK_RSA, ShaVariant::SHA256, data, length, signature); - case JwtAlgorithm::ES256: - return pkVerifyInternal(key, MBEDTLS_PK_ECKEY, ShaVariant::SHA256, data, length, signature); - case JwtAlgorithm::Auto: - default: - return false; - } + mbedtls_ctr_drbg_free(&ctr); + mbedtls_entropy_free(&entropy); + if (ret != 0) { + mbedtls_pk_free(&pk); + return false; + } + bool ok = pkSignContext(pk, expected, variant, data, length, signature); + mbedtls_pk_free(&pk); + return ok; +} + +bool pkVerifyInternal( + const std::string &pem, + mbedtls_pk_type_t expected, + ShaVariant variant, + const uint8_t *data, + size_t length, + const std::vector &signature +) { + mbedtls_pk_context pk; + mbedtls_pk_init(&pk); + if (!pkParsePublicOrPrivate(pk, pem, nullptr, nullptr)) { + mbedtls_pk_free(&pk); + return false; + } + bool ok = pkVerifyContext(pk, expected, variant, data, length, signature); + mbedtls_pk_free(&pk); + return ok; +} + +bool signJwt( + JwtAlgorithm alg, + const std::string &key, + const uint8_t *data, + size_t length, + std::vector &signature +) { + switch (alg) { + case JwtAlgorithm::HS256: + return hmacSha256(key, data, length, signature); + case JwtAlgorithm::RS256: + return pkSignInternal(key, MBEDTLS_PK_RSA, ShaVariant::SHA256, data, length, signature); + case JwtAlgorithm::ES256: + return pkSignInternal(key, MBEDTLS_PK_ECKEY, ShaVariant::SHA256, data, length, signature); + case JwtAlgorithm::Auto: + default: + return false; + } +} + +bool verifySignature( + JwtAlgorithm alg, + const std::string &key, + const uint8_t *data, + size_t length, + const std::vector &signature +) { + switch (alg) { + case JwtAlgorithm::HS256: { + std::vector expected; + if (!hmacSha256(key, data, length, expected)) { + return false; + } + return constantTimeEquals(expected, signature); + } + case JwtAlgorithm::RS256: + return pkVerifyInternal(key, MBEDTLS_PK_RSA, ShaVariant::SHA256, data, length, signature); + case JwtAlgorithm::ES256: + return pkVerifyInternal(key, MBEDTLS_PK_ECKEY, ShaVariant::SHA256, data, length, signature); + case JwtAlgorithm::Auto: + default: + return false; + } } bool aesKeyValid(const std::vector &key) { - return key.size() == 16 || key.size() == 24 || key.size() == 32; + return key.size() == 16 || key.size() == 24 || key.size() == 32; } -bool hardwareAesCtr(const std::vector &key, - const std::vector &nonceCounter, - const std::vector &input, - std::vector &output) { +bool hardwareAesCtr( + const std::vector &key, + const std::vector &nonceCounter, + const std::vector &input, + std::vector &output +) { #if ESPCRYPTO_AES_ACCEL - esp_aes_context ctx; - esp_aes_init(&ctx); - bool ok = esp_aes_setkey(&ctx, key.data(), key.size() * 8) == 0; - unsigned char counter[16] = {0}; - unsigned char stream[16] = {0}; - memcpy(counter, nonceCounter.data(), 16); - size_t off = 0; - if (ok) { - ok = esp_aes_crypt_ctr(&ctx, input.size(), &off, counter, stream, input.data(), output.data()) == 0; - } - esp_aes_free(&ctx); - return ok; + esp_aes_context ctx; + esp_aes_init(&ctx); + bool ok = esp_aes_setkey(&ctx, key.data(), key.size() * 8) == 0; + unsigned char counter[16] = {0}; + unsigned char stream[16] = {0}; + memcpy(counter, nonceCounter.data(), 16); + size_t off = 0; + if (ok) { + ok = esp_aes_crypt_ctr( + &ctx, + input.size(), + &off, + counter, + stream, + input.data(), + output.data() + ) == 0; + } + esp_aes_free(&ctx); + return ok; #else - (void)key; - (void)nonceCounter; - (void)input; - (void)output; - return false; + (void)key; + (void)nonceCounter; + (void)input; + (void)output; + return false; #endif } -bool softwareAesCtr(const std::vector &key, - const std::vector &nonceCounter, - const std::vector &input, - std::vector &output) { - mbedtls_aes_context ctx; - mbedtls_aes_init(&ctx); - bool ok = mbedtls_aes_setkey_enc(&ctx, key.data(), key.size() * 8) == 0; - unsigned char counter[16] = {0}; - unsigned char stream[16] = {0}; - memcpy(counter, nonceCounter.data(), 16); - size_t off = 0; - if (ok) { - ok = mbedtls_aes_crypt_ctr(&ctx, input.size(), &off, counter, stream, input.data(), output.data()) == 0; - } - mbedtls_aes_free(&ctx); - return ok; -} - -bool hardwareGcmCryptSpan(int mode, - const std::vector &key, - CryptoSpan iv, - CryptoSpan aad, - CryptoSpan input, - CryptoSpan output, - CryptoSpan tag) { +bool softwareAesCtr( + const std::vector &key, + const std::vector &nonceCounter, + const std::vector &input, + std::vector &output +) { + mbedtls_aes_context ctx; + mbedtls_aes_init(&ctx); + bool ok = mbedtls_aes_setkey_enc(&ctx, key.data(), key.size() * 8) == 0; + unsigned char counter[16] = {0}; + unsigned char stream[16] = {0}; + memcpy(counter, nonceCounter.data(), 16); + size_t off = 0; + if (ok) { + ok = mbedtls_aes_crypt_ctr( + &ctx, + input.size(), + &off, + counter, + stream, + input.data(), + output.data() + ) == 0; + } + mbedtls_aes_free(&ctx); + return ok; +} + +bool hardwareGcmCryptSpan( + int mode, + const std::vector &key, + CryptoSpan iv, + CryptoSpan aad, + CryptoSpan input, + CryptoSpan output, + CryptoSpan tag +) { #if ESPCRYPTO_AES_GCM_ACCEL - esp_gcm_context ctx; - esp_aes_gcm_init(&ctx); - bool ok = esp_aes_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key.data(), key.size() * 8) == 0; - if (ok && mode == MBEDTLS_GCM_ENCRYPT) { - ok = esp_aes_gcm_crypt_and_tag(&ctx, mode, input.size(), - iv.data(), iv.size(), - aad.empty() ? nullptr : aad.data(), aad.size(), - input.data(), output.data(), - tag.size(), tag.data()) == 0; - } else if (ok && mode == MBEDTLS_GCM_DECRYPT) { - ok = esp_aes_gcm_auth_decrypt(&ctx, input.size(), - iv.data(), iv.size(), - aad.empty() ? nullptr : aad.data(), aad.size(), - tag.data(), tag.size(), - input.data(), output.data()) == 0; - } - esp_aes_gcm_free(&ctx); - return ok; + esp_gcm_context ctx; + esp_aes_gcm_init(&ctx); + bool ok = esp_aes_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key.data(), key.size() * 8) == 0; + if (ok && mode == MBEDTLS_GCM_ENCRYPT) { + ok = esp_aes_gcm_crypt_and_tag( + &ctx, + mode, + input.size(), + iv.data(), + iv.size(), + aad.empty() ? nullptr : aad.data(), + aad.size(), + input.data(), + output.data(), + tag.size(), + tag.data() + ) == 0; + } else if (ok && mode == MBEDTLS_GCM_DECRYPT) { + ok = esp_aes_gcm_auth_decrypt( + &ctx, + input.size(), + iv.data(), + iv.size(), + aad.empty() ? nullptr : aad.data(), + aad.size(), + tag.data(), + tag.size(), + input.data(), + output.data() + ) == 0; + } + esp_aes_gcm_free(&ctx); + return ok; #else - (void)mode; - (void)key; - (void)iv; - (void)aad; - (void)input; - (void)output; - (void)tag; - return false; + (void)mode; + (void)key; + (void)iv; + (void)aad; + (void)input; + (void)output; + (void)tag; + return false; #endif } -bool hardwareGcmCrypt(int mode, - const std::vector &key, - const std::vector &iv, - const std::vector &aad, - const std::vector &input, - std::vector &output, - std::vector &tag) { - return hardwareGcmCryptSpan(mode, key, - CryptoSpan(iv), - CryptoSpan(aad), - CryptoSpan(input), - CryptoSpan(output), - CryptoSpan(tag)); -} - -bool softwareGcmCrypt(int mode, - const std::vector &key, - const std::vector &iv, - const std::vector &aad, - const std::vector &input, - std::vector &output, - std::vector &tag) { - return softwareGcmCrypt(mode, key, - CryptoSpan(iv), - CryptoSpan(aad), - CryptoSpan(input), - CryptoSpan(output), - CryptoSpan(tag)); -} - -bool softwareGcmCrypt(int mode, - const std::vector &key, - CryptoSpan iv, - CryptoSpan aad, - CryptoSpan input, - CryptoSpan output, - CryptoSpan tag) { - mbedtls_gcm_context ctx; - mbedtls_gcm_init(&ctx); - bool ok = mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key.data(), key.size() * 8) == 0; - if (ok && mode == MBEDTLS_GCM_ENCRYPT) { - ok = mbedtls_gcm_crypt_and_tag(&ctx, MBEDTLS_GCM_ENCRYPT, input.size(), - iv.data(), iv.size(), - aad.empty() ? nullptr : aad.data(), aad.size(), - input.data(), output.data(), - tag.size(), tag.data()) == 0; - } else if (ok && mode == MBEDTLS_GCM_DECRYPT) { - ok = mbedtls_gcm_auth_decrypt(&ctx, input.size(), - iv.data(), iv.size(), - aad.empty() ? nullptr : aad.data(), aad.size(), - tag.data(), tag.size(), - input.data(), output.data()) == 0; - } - mbedtls_gcm_free(&ctx); - return ok; -} - -CryptoStatusDetail aesGcmEncryptSpan(const std::vector &key, - CryptoSpan iv, - CryptoSpan aad, - CryptoSpan plaintext, - CryptoSpan ciphertext, - CryptoSpan tag) { - 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"); - } - if (ciphertext.size() < plaintext.size()) { - return makeStatus(CryptoStatus::BufferTooSmall, "ciphertext buffer too small"); - } - if (tag.size() < AES_GCM_TAG_BYTES) { - return makeStatus(CryptoStatus::BufferTooSmall, "tag buffer too small"); - } - std::vector ivCopy(iv.data(), iv.data() + iv.size()); - if (nonceReused(key, ivCopy)) { - secureZero(ivCopy.data(), ivCopy.size()); - return makeStatus(CryptoStatus::NonceReuse, "iv reuse"); - } - secureZero(ivCopy.data(), ivCopy.size()); - CryptoSpan ctSlice(ciphertext.data(), plaintext.size()); - CryptoSpan tagSlice(tag.data(), AES_GCM_TAG_BYTES); - bool ok = hardwareGcmCryptSpan(MBEDTLS_GCM_ENCRYPT, key, iv, aad, plaintext, ctSlice, tagSlice); - if (!ok) { - ok = softwareGcmCrypt(MBEDTLS_GCM_ENCRYPT, key, iv, aad, plaintext, ctSlice, tagSlice); - } - if (!ok) { - secureZero(ctSlice.data(), ctSlice.size()); - secureZero(tagSlice.data(), tagSlice.size()); - return makeStatus(CryptoStatus::InternalError, "aes gcm encrypt failed"); - } - return makeStatus(CryptoStatus::Ok); -} - -CryptoStatusDetail aesGcmDecryptSpan(const std::vector &key, - CryptoSpan iv, - CryptoSpan aad, - CryptoSpan ciphertext, - CryptoSpan tag, - CryptoSpan plaintext) { - 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"); - } - if (plaintext.size() < ciphertext.size()) { - return makeStatus(CryptoStatus::BufferTooSmall, "plaintext buffer too small"); - } - CryptoSpan ptSlice(plaintext.data(), ciphertext.size()); - std::vector tagCopy(tag.data(), tag.data() + tag.size()); - bool ok = hardwareGcmCryptSpan(MBEDTLS_GCM_DECRYPT, key, iv, aad, ciphertext, ptSlice, CryptoSpan(tagCopy)); - if (!ok) { - tagCopy.assign(tag.data(), tag.data() + tag.size()); - ok = softwareGcmCrypt(MBEDTLS_GCM_DECRYPT, key, iv, aad, ciphertext, ptSlice, CryptoSpan(tagCopy)); - } - secureZero(tagCopy.data(), tagCopy.size()); - if (!ok) { - secureZero(ptSlice.data(), ptSlice.size()); - return makeStatus(CryptoStatus::VerifyFailed, "gcm auth failed"); - } - return makeStatus(CryptoStatus::Ok); -} - -bool parsePasswordHash(const std::string &encoded, - uint8_t &cost, - std::vector &salt, - std::vector &hash) { - std::vector parts; - size_t start = 0; - while (start <= encoded.size()) { - size_t pos = encoded.find('$', start); - if (pos == std::string::npos) { - parts.push_back(encoded.substr(start)); - break; - } - parts.push_back(encoded.substr(start, pos - start)); - start = pos + 1; - } - if (parts.size() < 6 || parts[1] != "esphash" || parts[2] != "v1") { - return false; - } - cost = static_cast(atoi(parts[3].c_str())); - if (!base64Decode(parts[4], Base64Alphabet::Standard, salt)) { - return false; - } - if (!base64Decode(parts[5], Base64Alphabet::Standard, hash)) { - return false; - } - return true; -} - -CryptoStatusDetail aesGcmEncryptInternal(const std::vector &key, - const std::vector &iv, - const std::vector &aad, - const std::vector &plaintext, - std::vector &ciphertext, - std::vector &tag) { - ciphertext.assign(plaintext.size(), 0); - tag.assign(AES_GCM_TAG_BYTES, 0); - return aesGcmEncryptSpan(key, - CryptoSpan(iv), - CryptoSpan(aad), - CryptoSpan(plaintext), - CryptoSpan(ciphertext), - CryptoSpan(tag)); -} - -CryptoStatusDetail aesGcmDecryptInternal(const std::vector &key, - const std::vector &iv, - const std::vector &aad, - const std::vector &ciphertext, - const std::vector &tag, - std::vector &plaintext) { - plaintext.assign(ciphertext.size(), 0); - return aesGcmDecryptSpan(key, - CryptoSpan(iv), - CryptoSpan(aad), - CryptoSpan(ciphertext), - CryptoSpan(tag), - CryptoSpan(plaintext)); +bool hardwareGcmCrypt( + int mode, + const std::vector &key, + const std::vector &iv, + const std::vector &aad, + const std::vector &input, + std::vector &output, + std::vector &tag +) { + return hardwareGcmCryptSpan( + mode, + key, + CryptoSpan(iv), + CryptoSpan(aad), + CryptoSpan(input), + CryptoSpan(output), + CryptoSpan(tag) + ); +} + +bool softwareGcmCrypt( + int mode, + const std::vector &key, + const std::vector &iv, + const std::vector &aad, + const std::vector &input, + std::vector &output, + std::vector &tag +) { + return softwareGcmCrypt( + mode, + key, + CryptoSpan(iv), + CryptoSpan(aad), + CryptoSpan(input), + CryptoSpan(output), + CryptoSpan(tag) + ); +} + +bool softwareGcmCrypt( + int mode, + const std::vector &key, + CryptoSpan iv, + CryptoSpan aad, + CryptoSpan input, + CryptoSpan output, + CryptoSpan tag +) { + mbedtls_gcm_context ctx; + mbedtls_gcm_init(&ctx); + bool ok = mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key.data(), key.size() * 8) == 0; + if (ok && mode == MBEDTLS_GCM_ENCRYPT) { + ok = mbedtls_gcm_crypt_and_tag( + &ctx, + MBEDTLS_GCM_ENCRYPT, + input.size(), + iv.data(), + iv.size(), + aad.empty() ? nullptr : aad.data(), + aad.size(), + input.data(), + output.data(), + tag.size(), + tag.data() + ) == 0; + } else if (ok && mode == MBEDTLS_GCM_DECRYPT) { + ok = mbedtls_gcm_auth_decrypt( + &ctx, + input.size(), + iv.data(), + iv.size(), + aad.empty() ? nullptr : aad.data(), + aad.size(), + tag.data(), + tag.size(), + input.data(), + output.data() + ) == 0; + } + mbedtls_gcm_free(&ctx); + return ok; +} + +CryptoStatusDetail aesGcmEncryptSpan( + const std::vector &key, + CryptoSpan iv, + CryptoSpan aad, + CryptoSpan plaintext, + CryptoSpan ciphertext, + CryptoSpan tag +) { + 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"); + } + if (ciphertext.size() < plaintext.size()) { + return makeStatus(CryptoStatus::BufferTooSmall, "ciphertext buffer too small"); + } + if (tag.size() < AES_GCM_TAG_BYTES) { + return makeStatus(CryptoStatus::BufferTooSmall, "tag buffer too small"); + } + std::vector ivCopy(iv.data(), iv.data() + iv.size()); + if (nonceReused(key, ivCopy)) { + secureZero(ivCopy.data(), ivCopy.size()); + return makeStatus(CryptoStatus::NonceReuse, "iv reuse"); + } + secureZero(ivCopy.data(), ivCopy.size()); + CryptoSpan ctSlice(ciphertext.data(), plaintext.size()); + CryptoSpan tagSlice(tag.data(), AES_GCM_TAG_BYTES); + bool ok = hardwareGcmCryptSpan(MBEDTLS_GCM_ENCRYPT, key, iv, aad, plaintext, ctSlice, tagSlice); + if (!ok) { + ok = softwareGcmCrypt(MBEDTLS_GCM_ENCRYPT, key, iv, aad, plaintext, ctSlice, tagSlice); + } + if (!ok) { + secureZero(ctSlice.data(), ctSlice.size()); + secureZero(tagSlice.data(), tagSlice.size()); + return makeStatus(CryptoStatus::InternalError, "aes gcm encrypt failed"); + } + return makeStatus(CryptoStatus::Ok); +} + +CryptoStatusDetail aesGcmDecryptSpan( + const std::vector &key, + CryptoSpan iv, + CryptoSpan aad, + CryptoSpan ciphertext, + CryptoSpan tag, + CryptoSpan plaintext +) { + 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"); + } + if (plaintext.size() < ciphertext.size()) { + return makeStatus(CryptoStatus::BufferTooSmall, "plaintext buffer too small"); + } + CryptoSpan ptSlice(plaintext.data(), ciphertext.size()); + std::vector tagCopy(tag.data(), tag.data() + tag.size()); + bool ok = hardwareGcmCryptSpan( + MBEDTLS_GCM_DECRYPT, + key, + iv, + aad, + ciphertext, + ptSlice, + CryptoSpan(tagCopy) + ); + if (!ok) { + tagCopy.assign(tag.data(), tag.data() + tag.size()); + ok = softwareGcmCrypt( + MBEDTLS_GCM_DECRYPT, + key, + iv, + aad, + ciphertext, + ptSlice, + CryptoSpan(tagCopy) + ); + } + secureZero(tagCopy.data(), tagCopy.size()); + if (!ok) { + secureZero(ptSlice.data(), ptSlice.size()); + return makeStatus(CryptoStatus::VerifyFailed, "gcm auth failed"); + } + return makeStatus(CryptoStatus::Ok); +} + +bool parsePasswordHash( + const std::string &encoded, + uint8_t &cost, + std::vector &salt, + std::vector &hash +) { + std::vector parts; + size_t start = 0; + while (start <= encoded.size()) { + size_t pos = encoded.find('$', start); + if (pos == std::string::npos) { + parts.push_back(encoded.substr(start)); + break; + } + parts.push_back(encoded.substr(start, pos - start)); + start = pos + 1; + } + if (parts.size() < 6 || parts[1] != "esphash" || parts[2] != "v1") { + return false; + } + cost = static_cast(atoi(parts[3].c_str())); + if (!base64Decode(parts[4], Base64Alphabet::Standard, salt)) { + return false; + } + if (!base64Decode(parts[5], Base64Alphabet::Standard, hash)) { + return false; + } + return true; +} + +CryptoStatusDetail aesGcmEncryptInternal( + const std::vector &key, + const std::vector &iv, + const std::vector &aad, + const std::vector &plaintext, + std::vector &ciphertext, + std::vector &tag +) { + ciphertext.assign(plaintext.size(), 0); + tag.assign(AES_GCM_TAG_BYTES, 0); + return aesGcmEncryptSpan( + key, + CryptoSpan(iv), + CryptoSpan(aad), + CryptoSpan(plaintext), + CryptoSpan(ciphertext), + CryptoSpan(tag) + ); +} + +CryptoStatusDetail aesGcmDecryptInternal( + const std::vector &key, + const std::vector &iv, + const std::vector &aad, + const std::vector &ciphertext, + const std::vector &tag, + std::vector &plaintext +) { + plaintext.assign(ciphertext.size(), 0); + return aesGcmDecryptSpan( + key, + CryptoSpan(iv), + CryptoSpan(aad), + CryptoSpan(ciphertext), + CryptoSpan(tag), + CryptoSpan(plaintext) + ); } const char *toString(CryptoStatus status) { - switch (status) { - case CryptoStatus::Ok: - return "ok"; - case CryptoStatus::InvalidInput: - return "invalid input"; - case CryptoStatus::RandomFailure: - return "random source failed"; - case CryptoStatus::Unsupported: - return "unsupported"; - case CryptoStatus::PolicyViolation: - return "policy violation"; - case CryptoStatus::BufferTooSmall: - return "buffer too small"; - case CryptoStatus::VerifyFailed: - return "verification failed"; - case CryptoStatus::DecodeError: - return "decode error"; - case CryptoStatus::JsonError: - return "json error"; - case CryptoStatus::Expired: - return "token expired"; - case CryptoStatus::NotYetValid: - return "token not active"; - case CryptoStatus::AudienceMismatch: - return "audience mismatch"; - case CryptoStatus::IssuerMismatch: - return "issuer mismatch"; - case CryptoStatus::NonceReuse: - return "nonce reuse detected"; - case CryptoStatus::InternalError: - default: - return "internal error"; - } + switch (status) { + case CryptoStatus::Ok: + return "ok"; + case CryptoStatus::InvalidInput: + return "invalid input"; + case CryptoStatus::RandomFailure: + return "random source failed"; + case CryptoStatus::Unsupported: + return "unsupported"; + case CryptoStatus::PolicyViolation: + return "policy violation"; + case CryptoStatus::BufferTooSmall: + return "buffer too small"; + case CryptoStatus::VerifyFailed: + return "verification failed"; + case CryptoStatus::DecodeError: + return "decode error"; + case CryptoStatus::JsonError: + return "json error"; + case CryptoStatus::Expired: + return "token expired"; + case CryptoStatus::NotYetValid: + return "token not active"; + case CryptoStatus::AudienceMismatch: + return "audience mismatch"; + case CryptoStatus::IssuerMismatch: + return "issuer mismatch"; + case CryptoStatus::NonceReuse: + return "nonce reuse detected"; + case CryptoStatus::InternalError: + default: + return "internal error"; + } } SecureBuffer::SecureBuffer(size_t bytes) { - buffer.assign(bytes, 0); + buffer.assign(bytes, 0); } SecureBuffer::SecureBuffer(SecureBuffer &&other) noexcept : buffer(std::move(other.buffer)) { - other.wipe(); + other.wipe(); } SecureBuffer &SecureBuffer::operator=(SecureBuffer &&other) noexcept { - if (this != &other) { - wipe(); - buffer = std::move(other.buffer); - other.wipe(); - } - return *this; + if (this != &other) { + wipe(); + buffer = std::move(other.buffer); + other.wipe(); + } + return *this; } SecureBuffer::~SecureBuffer() { - wipe(); + wipe(); } void SecureBuffer::wipe() { - if (!buffer.empty()) { - secureZero(buffer.data(), buffer.size()); - buffer.clear(); - } + if (!buffer.empty()) { + secureZero(buffer.data(), buffer.size()); + buffer.clear(); + } } void SecureBuffer::resize(size_t bytes) { - wipe(); - buffer.assign(bytes, 0); + wipe(); + buffer.assign(bytes, 0); } -SecureString::SecureString(std::string value) : value(std::move(value)) {} +SecureString::SecureString(std::string value) : value(std::move(value)) { +} SecureString::SecureString(SecureString &&other) noexcept : value(std::move(other.value)) { - other.wipe(); + other.wipe(); } SecureString &SecureString::operator=(SecureString &&other) noexcept { - if (this != &other) { - wipe(); - value = std::move(other.value); - other.wipe(); - } - return *this; + if (this != &other) { + wipe(); + value = std::move(other.value); + other.wipe(); + } + return *this; } SecureString::~SecureString() { - wipe(); + wipe(); } void SecureString::wipe() { - if (!value.empty()) { - secureZero(&value[0], value.size()); - value.clear(); - } + if (!value.empty()) { + secureZero(&value[0], value.size()); + value.clear(); + } } void ESPCrypto::setPolicy(const CryptoPolicy &policy) { - mutablePolicy() = policy; - markRuntimeInitialized(); + mutablePolicy() = policy; + markRuntimeInitialized(); } CryptoPolicy ESPCrypto::policy() { - return mutablePolicy(); + return mutablePolicy(); } void ESPCrypto::deinit() { - resetRuntimeState(); + resetRuntimeState(); } bool ESPCrypto::isInitialized() { - return runtimeState().initialized.load(std::memory_order_acquire); + return runtimeState().initialized.load(std::memory_order_acquire); } CryptoCaps ESPCrypto::caps() { - CryptoCaps c; - c.shaAccel = ESPCRYPTO_SHA_ACCEL; - c.aesAccel = ESPCRYPTO_AES_ACCEL; - c.aesGcmAccel = ESPCRYPTO_AES_GCM_ACCEL; - return c; + CryptoCaps c; + c.shaAccel = ESPCRYPTO_SHA_ACCEL; + c.aesAccel = ESPCRYPTO_AES_ACCEL; + c.aesGcmAccel = ESPCRYPTO_AES_GCM_ACCEL; + return c; } bool ESPCrypto::constantTimeEq(const std::vector &a, const std::vector &b) { - return constantTimeEquals(CryptoSpan(a), CryptoSpan(b)); + return constantTimeEquals(CryptoSpan(a), CryptoSpan(b)); } bool ESPCrypto::constantTimeEq(CryptoSpan a, CryptoSpan b) { - return constantTimeEquals(a, b); -} - -CryptoResult> ESPCrypto::shaResult(CryptoSpan data, const ShaOptions &options) { - CryptoResult> result; - if (!data.data() && data.size() > 0) { - result.status = makeStatus(CryptoStatus::InvalidInput, "null data"); - return result; - } - result.value.assign(digestLength(options.variant), 0); - if (result.value.empty()) { - result.status = makeStatus(CryptoStatus::InvalidInput, "unknown sha variant"); - return result; - } - static const uint8_t ZERO_BYTE = 0; - const uint8_t *buffer = data.size() == 0 ? &ZERO_BYTE : data.data(); - size_t length = data.size(); - bool hashed = false; - if (options.preferHardware) { - hashed = tryHardwareSha(options.variant, buffer, length, result.value.data()); - } - if (!hashed) { - hashed = softwareSha(options.variant, buffer, length, result.value.data()); - } - if (!hashed) { - secureZero(result.value.data(), result.value.size()); - result.value.clear(); - result.status = makeStatus(CryptoStatus::InternalError, "sha failed"); - return result; - } - result.status = makeStatus(CryptoStatus::Ok); - return result; -} - -CryptoResult ESPCrypto::sha(CryptoSpan data, CryptoSpan out, const ShaOptions &options) { - CryptoResult result; - size_t needed = digestLength(options.variant); - if (needed == 0) { - result.status = makeStatus(CryptoStatus::InvalidInput, "unknown sha variant"); - return result; - } - if (out.size() < needed) { - result.status = makeStatus(CryptoStatus::BufferTooSmall, "digest buffer too small"); - return result; - } - auto hashed = shaResult(data, options); - if (!hashed.ok()) { - result.status = hashed.status; - return result; - } - memcpy(out.data(), hashed.value.data(), needed); - result.status = makeStatus(CryptoStatus::Ok); - return result; -} - -CryptoResult> ESPCrypto::deriveDeviceKey(const String &purpose, - CryptoSpan contextInfo, - size_t length, - const DeviceKeyOptions &options) { - CryptoResult> result; - if (purpose.length() == 0 || length == 0) { - result.status = makeStatus(CryptoStatus::InvalidInput, "purpose/length missing"); - return result; - } - auto deviceSalt = deviceFingerprint(); - std::vector seed; - auto seedStatus = loadOrCreateSeed(seed, options); - if (!seedStatus.ok()) { - result.status = seedStatus; - return result; - } - std::vector info; - info.insert(info.end(), purpose.begin(), purpose.end()); - if (!contextInfo.empty()) { - info.insert(info.end(), contextInfo.data(), contextInfo.data() + contextInfo.size()); - } - auto derived = hkdf(ShaVariant::SHA256, - CryptoSpan(deviceSalt), - CryptoSpan(seed), - CryptoSpan(info), - length); - secureZero(seed.data(), seed.size()); - secureZero(info.data(), info.size()); - if (!derived.ok()) { - result.status = derived.status; - return result; - } - result.value = std::move(derived.value); - result.status = makeStatus(CryptoStatus::Ok); - return result; -} - -CryptoResult ESPCrypto::storeKey(KeyStore &store, const KeyHandle &handle, CryptoSpan keyMaterial) { - CryptoResult result; - auto status = store.store(handle, keyMaterial); - result.status = status; - return result; -} - -CryptoResult ESPCrypto::loadKey(KeyStore &store, const KeyHandle &handle, KeyFormat format, KeyKind kind) { - CryptoResult result; - auto loaded = store.load(handle); - if (!loaded.ok()) { - result.status = loaded.status; - return result; - } - switch (format) { - case KeyFormat::Pem: - result.value = CryptoKey::fromPem(std::string(reinterpret_cast(loaded.value.data()), loaded.value.size()), kind); - break; - case KeyFormat::Der: - result.value = CryptoKey::fromDer(loaded.value, kind); - break; - case KeyFormat::Raw: - result.value = CryptoKey::fromRaw(loaded.value, kind); - break; - case KeyFormat::Jwk: - result.status = makeStatus(CryptoStatus::Unsupported, "jwk decode not implemented"); - return result; - } - result.status = makeStatus(CryptoStatus::Ok); - return result; + return constantTimeEquals(a, b); +} + +CryptoResult> +ESPCrypto::shaResult(CryptoSpan data, const ShaOptions &options) { + CryptoResult> result; + if (!data.data() && data.size() > 0) { + result.status = makeStatus(CryptoStatus::InvalidInput, "null data"); + return result; + } + result.value.assign(digestLength(options.variant), 0); + if (result.value.empty()) { + result.status = makeStatus(CryptoStatus::InvalidInput, "unknown sha variant"); + return result; + } + static const uint8_t ZERO_BYTE = 0; + const uint8_t *buffer = data.size() == 0 ? &ZERO_BYTE : data.data(); + size_t length = data.size(); + bool hashed = false; + if (options.preferHardware) { + hashed = tryHardwareSha(options.variant, buffer, length, result.value.data()); + } + if (!hashed) { + hashed = softwareSha(options.variant, buffer, length, result.value.data()); + } + if (!hashed) { + secureZero(result.value.data(), result.value.size()); + result.value.clear(); + result.status = makeStatus(CryptoStatus::InternalError, "sha failed"); + return result; + } + result.status = makeStatus(CryptoStatus::Ok); + return result; +} + +CryptoResult +ESPCrypto::sha(CryptoSpan data, CryptoSpan out, const ShaOptions &options) { + CryptoResult result; + size_t needed = digestLength(options.variant); + if (needed == 0) { + result.status = makeStatus(CryptoStatus::InvalidInput, "unknown sha variant"); + return result; + } + if (out.size() < needed) { + result.status = makeStatus(CryptoStatus::BufferTooSmall, "digest buffer too small"); + return result; + } + auto hashed = shaResult(data, options); + if (!hashed.ok()) { + result.status = hashed.status; + return result; + } + memcpy(out.data(), hashed.value.data(), needed); + result.status = makeStatus(CryptoStatus::Ok); + return result; +} + +CryptoResult> ESPCrypto::deriveDeviceKey( + const String &purpose, + CryptoSpan contextInfo, + size_t length, + const DeviceKeyOptions &options +) { + CryptoResult> result; + if (purpose.length() == 0 || length == 0) { + result.status = makeStatus(CryptoStatus::InvalidInput, "purpose/length missing"); + return result; + } + auto deviceSalt = deviceFingerprint(); + std::vector seed; + auto seedStatus = loadOrCreateSeed(seed, options); + if (!seedStatus.ok()) { + result.status = seedStatus; + return result; + } + std::vector info; + info.insert(info.end(), purpose.begin(), purpose.end()); + if (!contextInfo.empty()) { + info.insert(info.end(), contextInfo.data(), contextInfo.data() + contextInfo.size()); + } + auto derived = hkdf( + ShaVariant::SHA256, + CryptoSpan(deviceSalt), + CryptoSpan(seed), + CryptoSpan(info), + length + ); + secureZero(seed.data(), seed.size()); + secureZero(info.data(), info.size()); + if (!derived.ok()) { + result.status = derived.status; + return result; + } + result.value = std::move(derived.value); + result.status = makeStatus(CryptoStatus::Ok); + return result; +} + +CryptoResult ESPCrypto::storeKey( + KeyStore &store, const KeyHandle &handle, CryptoSpan keyMaterial +) { + CryptoResult result; + auto status = store.store(handle, keyMaterial); + result.status = status; + return result; +} + +CryptoResult +ESPCrypto::loadKey(KeyStore &store, const KeyHandle &handle, KeyFormat format, KeyKind kind) { + CryptoResult result; + auto loaded = store.load(handle); + if (!loaded.ok()) { + result.status = loaded.status; + return result; + } + switch (format) { + case KeyFormat::Pem: + result.value = CryptoKey::fromPem( + std::string(reinterpret_cast(loaded.value.data()), loaded.value.size()), + kind + ); + break; + case KeyFormat::Der: + result.value = CryptoKey::fromDer(loaded.value, kind); + break; + case KeyFormat::Raw: + result.value = CryptoKey::fromRaw(loaded.value, kind); + break; + case KeyFormat::Jwk: + result.status = makeStatus(CryptoStatus::Unsupported, "jwk decode not implemented"); + return result; + } + result.status = makeStatus(CryptoStatus::Ok); + return result; } CryptoResult ESPCrypto::removeKey(KeyStore &store, const KeyHandle &handle) { - CryptoResult result; - result.status = store.remove(handle); - return result; + CryptoResult result; + result.status = store.remove(handle); + return result; } std::vector ESPCrypto::sha(const uint8_t *data, size_t length, const ShaOptions &options) { - auto result = shaResult(CryptoSpan(data, length), options); - return result.ok() ? result.value : std::vector(); + auto result = shaResult(CryptoSpan(data, length), options); + return result.ok() ? result.value : std::vector(); } std::vector ESPCrypto::sha(const std::vector &data, const ShaOptions &options) { - return sha(data.data(), data.size(), options); + return sha(data.data(), data.size(), options); } String ESPCrypto::shaHex(const uint8_t *data, size_t length, const ShaOptions &options) { - auto digest = sha(data, length, options); - if (digest.empty()) { - return String(); - } - static const char *HEX_DIGITS = "0123456789abcdef"; - std::string hex; - hex.reserve(digest.size() * 2); - for (uint8_t b : digest) { - hex.push_back(HEX_DIGITS[(b >> 4) & 0x0F]); - hex.push_back(HEX_DIGITS[b & 0x0F]); - } - return String(hex.c_str()); + auto digest = sha(data, length, options); + if (digest.empty()) { + return String(); + } + static const char *HEX_DIGITS = "0123456789abcdef"; + std::string hex; + hex.reserve(digest.size() * 2); + for (uint8_t b : digest) { + hex.push_back(HEX_DIGITS[(b >> 4) & 0x0F]); + hex.push_back(HEX_DIGITS[b & 0x0F]); + } + return String(hex.c_str()); } String ESPCrypto::shaHex(const String &text, const ShaOptions &options) { - return shaHex(reinterpret_cast(text.c_str()), text.length(), options); -} - -bool ESPCrypto::aesGcmEncrypt(const std::vector &key, - const std::vector &iv, - const std::vector &plaintext, - std::vector &ciphertext, - std::vector &tag, - const std::vector &aad) { - CryptoStatusDetail status = aesGcmEncryptInternal(key, iv, aad, plaintext, ciphertext, tag); - if (!status.ok()) { - secureZero(ciphertext.data(), ciphertext.size()); - secureZero(tag.data(), tag.size()); - } - return status.ok(); -} - -bool ESPCrypto::aesGcmDecrypt(const std::vector &key, - const std::vector &iv, - const std::vector &ciphertext, - const std::vector &tag, - std::vector &plaintext, - const std::vector &aad) { - CryptoStatusDetail status = aesGcmDecryptInternal(key, iv, aad, ciphertext, tag, plaintext); - if (!status.ok()) { - secureZero(plaintext.data(), plaintext.size()); - plaintext.clear(); - } - return status.ok(); -} - -bool ESPCrypto::aesCtrCrypt(const std::vector &key, - const std::vector &nonceCounter, - const std::vector &input, - std::vector &output) { - auto result = aesCtrCrypt(key, nonceCounter, input); - if (!result.ok()) { - output.clear(); - return false; - } - output = std::move(result.value); - return true; -} - -CryptoResult ESPCrypto::aesGcmEncryptAuto(const std::vector &key, - const std::vector &plaintext, - const std::vector &aad, - size_t ivLength, - const GcmNonceOptions &nonceOptions) { - CryptoResult result; - markRuntimeInitialized(); - const CryptoPolicy &policy = mutablePolicy(); - if (ivLength == 0) { - ivLength = policy.minAesGcmIvBytes; - } - if (!policy.allowLegacy && ivLength < policy.minAesGcmIvBytes) { - result.status = makeStatus(CryptoStatus::PolicyViolation, "iv too short"); - return result; - } - if (!aesKeyValid(key)) { - result.status = makeStatus(CryptoStatus::InvalidInput, "invalid key"); - return result; - } - result.value.iv.assign(ivLength, 0); - GlobalRuntimeState &state = runtimeState(); - uint32_t keyHash = fingerprintKey(key); - state.bootCounter.fetch_add(1, std::memory_order_relaxed); - switch (nonceOptions.strategy) { - case GcmNonceStrategy::Random96: - default: - fillRandom(result.value.iv.data(), result.value.iv.size()); - break; - case GcmNonceStrategy::Counter64_Random32: { - if (ivLength < 12) { - result.status = makeStatus(CryptoStatus::PolicyViolation, "counter strategy needs >=12 iv bytes"); - return result; - } - bool found = false; - uint64_t counter = loadCounterFromNvs(nonceOptions.nvsNamespace, nonceOptions.nvsPartition, "gcmctr_" + std::to_string(keyHash), found); - if (!found) { - counter = 1; - } else { - counter += 1; - } - if (nonceOptions.persistCounter) { - storeCounterToNvs(nonceOptions.nvsNamespace, nonceOptions.nvsPartition, "gcmctr_" + std::to_string(keyHash), counter); - } - for (int i = 0; i < 8 && i < static_cast(ivLength); ++i) { - result.value.iv[i] = static_cast((counter >> (56 - 8 * i)) & 0xFF); - } - std::vector tail(ivLength > 8 ? ivLength - 8 : 0, 0); - if (!tail.empty()) { - fillRandom(tail.data(), tail.size()); - memcpy(result.value.iv.data() + 8, tail.data(), tail.size()); - } - break; - } - case GcmNonceStrategy::BootCounter_Random32: { - if (ivLength < 12) { - result.status = makeStatus(CryptoStatus::PolicyViolation, "counter strategy needs >=12 iv bytes"); - return result; - } - 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); - } - std::vector tail(ivLength > 8 ? ivLength - 8 : 0, 0); - if (!tail.empty()) { - fillRandom(tail.data(), tail.size()); - memcpy(result.value.iv.data() + 8, tail.data(), tail.size()); - } - break; - } - } - result.status = aesGcmEncryptInternal(key, result.value.iv, aad, plaintext, result.value.ciphertext, result.value.tag); - if (!result.ok()) { - result.value = {}; - } - return result; -} - -CryptoResult> ESPCrypto::aesGcmDecrypt(const std::vector &key, - const std::vector &iv, - const std::vector &ciphertext, - const std::vector &tag, - const std::vector &aad) { - CryptoResult> result; - result.status = aesGcmDecryptInternal(key, iv, aad, ciphertext, tag, result.value); - if (!result.ok()) { - result.value.clear(); - } - return result; -} - -CryptoResult ESPCrypto::aesGcmEncrypt(const std::vector &key, - CryptoSpan iv, - CryptoSpan plaintext, - CryptoSpan ciphertextOut, - CryptoSpan tagOut, - CryptoSpan aad) { - CryptoResult result; - result.status = aesGcmEncryptSpan(key, iv, aad, plaintext, ciphertextOut, tagOut); - if (!result.ok()) { - if (!ciphertextOut.empty()) { - secureZero(ciphertextOut.data(), std::min(ciphertextOut.size(), plaintext.size())); - } - if (!tagOut.empty()) { - secureZero(tagOut.data(), std::min(tagOut.size(), static_cast(AES_GCM_TAG_BYTES))); - } - } - return result; -} - -CryptoResult ESPCrypto::aesGcmDecrypt(const std::vector &key, - CryptoSpan iv, - CryptoSpan ciphertext, - CryptoSpan tag, - CryptoSpan plaintextOut, - CryptoSpan aad) { - CryptoResult result; - result.status = aesGcmDecryptSpan(key, iv, aad, ciphertext, tag, plaintextOut); - if (!result.ok()) { - if (!plaintextOut.empty()) { - secureZero(plaintextOut.data(), std::min(plaintextOut.size(), ciphertext.size())); - } - } - return result; -} - -CryptoResult> ESPCrypto::aesCtrCrypt(const std::vector &key, - const std::vector &nonceCounter, - const std::vector &input) { - CryptoResult> result; - if (!aesKeyValid(key) || nonceCounter.size() != 16) { - result.status = makeStatus(CryptoStatus::InvalidInput, "invalid key or nonce"); - return result; - } - result.value.assign(input.size(), 0); - bool ok = hardwareAesCtr(key, nonceCounter, input, result.value); - if (!ok) { - ok = softwareAesCtr(key, nonceCounter, input, result.value); - } - if (!ok) { - secureZero(result.value.data(), result.value.size()); - result.value.clear(); - result.status = makeStatus(CryptoStatus::InternalError, "aes ctr failed"); - return result; - } - result.status = makeStatus(CryptoStatus::Ok); - return result; -} - -bool ESPCrypto::rsaSign(const std::string &privateKeyPem, - const uint8_t *data, - size_t length, - ShaVariant variant, - std::vector &signature) { - if (privateKeyPem.empty() || (!data && length > 0)) { - return false; - } - return pkSignInternal(privateKeyPem, MBEDTLS_PK_RSA, variant, data, length, signature); -} - -bool ESPCrypto::rsaVerify(const std::string &publicKeyPem, - const uint8_t *data, - size_t length, - const std::vector &signature, - ShaVariant variant) { - if (publicKeyPem.empty() || (!data && length > 0) || signature.empty()) { - return false; - } - return pkVerifyInternal(publicKeyPem, MBEDTLS_PK_RSA, variant, data, length, signature); -} - -CryptoResult> ESPCrypto::rsaSign(const std::string &privateKeyPem, - CryptoSpan data, - ShaVariant variant) { - CryptoResult> result; - if (privateKeyPem.empty() || (!data.data() && data.size() > 0)) { - result.status = makeStatus(CryptoStatus::InvalidInput, "missing key or data"); - return result; - } - if (!pkSignInternal(privateKeyPem, MBEDTLS_PK_RSA, variant, data.data(), data.size(), result.value)) { - result.status = makeStatus(CryptoStatus::VerifyFailed, "rsa sign failed"); - result.value.clear(); - return result; - } - result.status = makeStatus(CryptoStatus::Ok); - return result; -} - -CryptoResult ESPCrypto::rsaVerify(const std::string &publicKeyPem, - CryptoSpan data, - CryptoSpan signature, - ShaVariant variant) { - CryptoResult result; - if (publicKeyPem.empty() || (!data.data() && data.size() > 0) || signature.empty()) { - result.status = makeStatus(CryptoStatus::InvalidInput, "missing key/data/signature"); - return result; - } - if (!pkVerifyInternal(publicKeyPem, MBEDTLS_PK_RSA, variant, data.data(), data.size(), std::vector(signature.data(), signature.data() + signature.size()))) { - result.status = makeStatus(CryptoStatus::VerifyFailed, "rsa verify failed"); - return result; - } - result.status = makeStatus(CryptoStatus::Ok); - return result; -} - -CryptoResult> ESPCrypto::rsaSign(const CryptoKey &privateKey, - CryptoSpan data, - ShaVariant variant) { - CryptoResult> result; - if (!privateKey.valid() || (!data.data() && data.size() > 0)) { - result.status = makeStatus(CryptoStatus::InvalidInput, "missing key or data"); - return result; - } - auto parsed = privateKey.ensureParsedPk(true); - if (!parsed.ok()) { - result.status = parsed; - return result; - } - if (!pkSignContext(privateKey.pk->ctx, MBEDTLS_PK_RSA, variant, data.data(), data.size(), result.value)) { - result.status = makeStatus(CryptoStatus::VerifyFailed, "rsa sign failed"); - result.value.clear(); - return result; - } - result.status = makeStatus(CryptoStatus::Ok); - return result; -} - -CryptoResult ESPCrypto::rsaVerify(const CryptoKey &publicKey, - CryptoSpan data, - CryptoSpan signature, - ShaVariant variant) { - CryptoResult result; - if (!publicKey.valid() || (!data.data() && data.size() > 0) || signature.empty()) { - result.status = makeStatus(CryptoStatus::InvalidInput, "missing key/data/signature"); - return result; - } - auto parsed = publicKey.ensureParsedPk(false); - if (!parsed.ok()) { - result.status = parsed; - return result; - } - std::vector sigVec(signature.data(), signature.data() + signature.size()); - if (!pkVerifyContext(publicKey.pk->ctx, MBEDTLS_PK_RSA, variant, data.data(), data.size(), sigVec)) { - result.status = makeStatus(CryptoStatus::VerifyFailed, "rsa verify failed"); - return result; - } - result.status = makeStatus(CryptoStatus::Ok); - return result; -} - -bool ESPCrypto::eccSign(const std::string &privateKeyPem, - const uint8_t *data, - size_t length, - ShaVariant variant, - std::vector &signature) { - if (privateKeyPem.empty() || (!data && length > 0)) { - return false; - } - return pkSignInternal(privateKeyPem, MBEDTLS_PK_ECKEY, variant, data, length, signature); -} - -bool ESPCrypto::eccVerify(const std::string &publicKeyPem, - const uint8_t *data, - size_t length, - const std::vector &signature, - ShaVariant variant) { - if (publicKeyPem.empty() || (!data && length > 0) || signature.empty()) { - return false; - } - return pkVerifyInternal(publicKeyPem, MBEDTLS_PK_ECKEY, variant, data, length, signature); -} - -CryptoResult> ESPCrypto::eccSign(const std::string &privateKeyPem, - CryptoSpan data, - ShaVariant variant) { - CryptoResult> result; - if (privateKeyPem.empty() || (!data.data() && data.size() > 0)) { - result.status = makeStatus(CryptoStatus::InvalidInput, "missing key or data"); - return result; - } - if (!pkSignInternal(privateKeyPem, MBEDTLS_PK_ECKEY, variant, data.data(), data.size(), result.value)) { - result.status = makeStatus(CryptoStatus::VerifyFailed, "ecc sign failed"); - result.value.clear(); - return result; - } - result.status = makeStatus(CryptoStatus::Ok); - return result; -} - -CryptoResult ESPCrypto::eccVerify(const std::string &publicKeyPem, - CryptoSpan data, - CryptoSpan signature, - ShaVariant variant) { - CryptoResult result; - if (publicKeyPem.empty() || (!data.data() && data.size() > 0) || signature.empty()) { - result.status = makeStatus(CryptoStatus::InvalidInput, "missing key/data/signature"); - return result; - } - if (!pkVerifyInternal(publicKeyPem, MBEDTLS_PK_ECKEY, variant, data.data(), data.size(), std::vector(signature.data(), signature.data() + signature.size()))) { - result.status = makeStatus(CryptoStatus::VerifyFailed, "ecc verify failed"); - return result; - } - result.status = makeStatus(CryptoStatus::Ok); - return result; -} - -CryptoResult> ESPCrypto::eccSign(const CryptoKey &privateKey, - CryptoSpan data, - ShaVariant variant) { - CryptoResult> result; - if (!privateKey.valid() || (!data.data() && data.size() > 0)) { - result.status = makeStatus(CryptoStatus::InvalidInput, "missing key or data"); - return result; - } - auto parsed = privateKey.ensureParsedPk(true); - if (!parsed.ok()) { - result.status = parsed; - return result; - } - if (!pkSignContext(privateKey.pk->ctx, MBEDTLS_PK_ECKEY, variant, data.data(), data.size(), result.value)) { - result.status = makeStatus(CryptoStatus::VerifyFailed, "ecc sign failed"); - result.value.clear(); - return result; - } - result.status = makeStatus(CryptoStatus::Ok); - return result; -} - -CryptoResult ESPCrypto::eccVerify(const CryptoKey &publicKey, - CryptoSpan data, - CryptoSpan signature, - ShaVariant variant) { - CryptoResult result; - if (!publicKey.valid() || (!data.data() && data.size() > 0) || signature.empty()) { - result.status = makeStatus(CryptoStatus::InvalidInput, "missing key/data/signature"); - return result; - } - auto parsed = publicKey.ensureParsedPk(false); - if (!parsed.ok()) { - result.status = parsed; - return result; - } - std::vector sigVec(signature.data(), signature.data() + signature.size()); - if (!pkVerifyContext(publicKey.pk->ctx, MBEDTLS_PK_ECKEY, variant, data.data(), data.size(), sigVec)) { - result.status = makeStatus(CryptoStatus::VerifyFailed, "ecc verify failed"); - return result; - } - result.status = makeStatus(CryptoStatus::Ok); - return result; -} - -String ESPCrypto::createJwt(const JsonDocument &claims, - const std::string &key, - const JwtSignOptions &options) { - auto result = createJwtResult(claims, key, options); - return result.ok() ? result.value : String(); -} - -bool ESPCrypto::verifyJwt(const String &token, - const std::string &key, - JsonDocument &outClaims, - String &error, - const JwtVerifyOptions &options) { - auto result = verifyJwtResult(token, key, outClaims, options); - if (!result.ok()) { - error = result.status.message.length() > 0 ? result.status.message : String(toString(result.status.code)); - return false; - } - error = ""; - return true; -} - -CryptoResult ESPCrypto::createJwtResult(const JsonDocument &claims, - const std::string &key, - const JwtSignOptions &options) { - CryptoResult result; - if (key.empty()) { - result.status = makeStatus(CryptoStatus::InvalidInput, "key missing"); - return result; - } - JsonDocument header; - std::string algName = algorithmName(options.algorithm); - if (algName.empty()) { - result.status = makeStatus(CryptoStatus::Unsupported, "unsupported alg"); - return result; - } - header["alg"] = algName.c_str(); - header["typ"] = "JWT"; - if (options.keyId.length() > 0) { - header["kid"] = options.keyId.c_str(); - } - JsonDocument payload; - payload.set(claims); - if (options.issuer.length() > 0 && payload["iss"].isNull()) { - payload["iss"] = options.issuer.c_str(); - } - if (options.subject.length() > 0 && payload["sub"].isNull()) { - payload["sub"] = options.subject.c_str(); - } - if (options.audience.length() > 0 && payload["aud"].isNull()) { - payload["aud"] = options.audience.c_str(); - } - uint32_t now = currentTimeSeconds(options.currentTimestamp != 0 ? options.currentTimestamp : options.issuedAt); - if (options.issuedAt != 0) { - payload["iat"] = options.issuedAt; - } else { - payload["iat"] = now; - } - if (options.expiresInSeconds > 0) { - payload["exp"] = static_cast(payload["iat"].as() + options.expiresInSeconds); - } - if (options.notBefore > 0) { - payload["nbf"] = options.notBefore; - } - - std::string headerJson; - if (serializeJson(header, headerJson) == 0) { - result.status = makeStatus(CryptoStatus::JsonError, "header serialization failed"); - return result; - } - std::string payloadJson; - if (serializeJson(payload, payloadJson) == 0) { - result.status = makeStatus(CryptoStatus::JsonError, "payload serialization failed"); - return result; - } - - std::string encodedHeader = base64Encode(reinterpret_cast(headerJson.data()), headerJson.size(), Base64Alphabet::Url); - std::string encodedPayload = base64Encode(reinterpret_cast(payloadJson.data()), payloadJson.size(), Base64Alphabet::Url); - if (encodedHeader.empty() || encodedPayload.empty()) { - result.status = makeStatus(CryptoStatus::DecodeError, "base64 encode failed"); - return result; - } - std::string signingInput = encodedHeader + "." + encodedPayload; - std::vector signature; - if (!signJwt(options.algorithm, key, - reinterpret_cast(signingInput.data()), signingInput.size(), signature)) { - result.status = makeStatus(CryptoStatus::InternalError, "sign failed"); - return result; - } - std::string encodedSignature = base64Encode(signature.data(), signature.size(), Base64Alphabet::Url); - std::string token = signingInput + "." + encodedSignature; - result.value = String(token.c_str()); - result.status = makeStatus(CryptoStatus::Ok); - return result; -} - -CryptoResult ESPCrypto::verifyJwtResult(const String &token, - const std::string &key, - JsonDocument &outClaims, - const JwtVerifyOptions &options) { - CryptoResult result; - if (token.length() == 0 || key.empty()) { - result.status = makeStatus(CryptoStatus::InvalidInput, "token or key missing"); - return result; - } - std::string tokenStd(token.c_str(), token.length()); - size_t first = tokenStd.find('.'); - size_t second = tokenStd.find('.', first == std::string::npos ? 0 : first + 1); - if (first == std::string::npos || second == std::string::npos) { - result.status = makeStatus(CryptoStatus::DecodeError, "invalid token structure"); - return result; - } - std::string headerPart = tokenStd.substr(0, first); - std::string payloadPart = tokenStd.substr(first + 1, second - first - 1); - std::string signaturePart = tokenStd.substr(second + 1); - std::vector headerBytes; - std::vector payloadBytes; - std::vector signatureBytes; - if (!base64Decode(headerPart, Base64Alphabet::Url, headerBytes) || - !base64Decode(payloadPart, Base64Alphabet::Url, payloadBytes) || - !base64Decode(signaturePart, Base64Alphabet::Url, signatureBytes)) { - result.status = makeStatus(CryptoStatus::DecodeError, "base64 decode failed"); - return result; - } - JsonDocument headerDoc; - if (deserializeJson(headerDoc, headerBytes.data(), headerBytes.size()) != DeserializationError::Ok) { - result.status = makeStatus(CryptoStatus::JsonError, "invalid header json"); - return result; - } - JsonDocument payloadDoc; - if (deserializeJson(payloadDoc, payloadBytes.data(), payloadBytes.size()) != DeserializationError::Ok) { - result.status = makeStatus(CryptoStatus::JsonError, "invalid payload json"); - return result; - } - const char *algStr = headerDoc["alg"].as(); - JwtAlgorithm alg = algorithmFromName(algStr ? algStr : ""); - if (alg == JwtAlgorithm::Auto) { - result.status = makeStatus(CryptoStatus::Unsupported, "unsupported alg"); - return result; - } - if (options.algorithm != JwtAlgorithm::Auto && options.algorithm != alg) { - result.status = makeStatus(CryptoStatus::PolicyViolation, "alg mismatch"); - return result; - } - const char *typHdr = headerDoc["typ"].as(); - if (options.expectedTyp.length() > 0) { - if (!typHdr || options.expectedTyp != typHdr) { - result.status = makeStatus(CryptoStatus::PolicyViolation, "typ mismatch"); - return result; - } - } - JsonArray crit = headerDoc["crit"].as(); - if (!crit.isNull() && !options.criticalHeadersAllowed.empty()) { - for (JsonVariant v : crit) { - const char *name = v.as(); - bool allowed = false; - for (const auto &allowedName : options.criticalHeadersAllowed) { - if (name && allowedName == name) { - allowed = true; - break; - } - } - if (!allowed) { - result.status = makeStatus(CryptoStatus::PolicyViolation, "crit header not allowed"); - return result; - } - } - } else if (!crit.isNull() && options.criticalHeadersAllowed.empty()) { - result.status = makeStatus(CryptoStatus::PolicyViolation, "crit header not allowed"); - return result; - } - std::string signingInput = headerPart + "." + payloadPart; - if (!verifySignature(alg, key, - reinterpret_cast(signingInput.data()), signingInput.size(), - signatureBytes)) { - result.status = makeStatus(CryptoStatus::VerifyFailed, "signature mismatch"); - return result; - } - uint32_t now = currentTimeSeconds(options.currentTimestamp); - uint32_t leeway = options.leewaySeconds; - uint32_t exp = payloadDoc["exp"].as(); - uint32_t nbf = payloadDoc["nbf"].as(); - if (options.requireExpiration && exp == 0) { - result.status = makeStatus(CryptoStatus::PolicyViolation, "missing exp"); - return result; - } - if (exp != 0 && now > exp + leeway) { - result.status = makeStatus(CryptoStatus::Expired, "token expired"); - return result; - } - if (nbf != 0 && now + leeway < nbf) { - result.status = makeStatus(CryptoStatus::NotYetValid, "token not active"); - return result; - } - auto audMatch = [&](const char *aud) -> bool { - if (!aud) { - return false; - } - if (options.audience.length() > 0 && options.audience == aud) { - return true; - } - for (const auto &a : options.audiences) { - if (a == aud) { - return true; - } - } - return options.audience.length() == 0 && options.audiences.empty(); - }; - if (options.audience.length() > 0 || !options.audiences.empty()) { - bool ok = false; - if (payloadDoc["aud"].is()) { - JsonArray arr = payloadDoc["aud"].as(); - for (JsonVariant v : arr) { - ok = audMatch(v.as()); - if (ok) break; - } - } else { - ok = audMatch(payloadDoc["aud"].as()); - } - if (!ok) { - result.status = makeStatus(CryptoStatus::AudienceMismatch, "aud mismatch"); - return result; - } - } - if (options.issuer.length() > 0) { - const char *iss = payloadDoc["iss"].as(); - if (!iss || options.issuer != iss) { - result.status = makeStatus(CryptoStatus::IssuerMismatch, "iss mismatch"); - return result; - } - } - outClaims.set(payloadDoc); - result.status = makeStatus(CryptoStatus::Ok); - return result; -} - -CryptoResult ESPCrypto::verifyJwtWithJwks(const String &token, - const JsonDocument &jwks, - JsonDocument &outClaims, - const JwtVerifyOptions &options) { - CryptoResult result; - if (token.length() == 0) { - result.status = makeStatus(CryptoStatus::InvalidInput, "token missing"); - return result; - } - std::string tokenStd(token.c_str(), token.length()); - size_t first = tokenStd.find('.'); - size_t second = tokenStd.find('.', first == std::string::npos ? 0 : first + 1); - if (first == std::string::npos || second == std::string::npos) { - result.status = makeStatus(CryptoStatus::DecodeError, "invalid token structure"); - return result; - } - std::string headerPart = tokenStd.substr(0, first); - std::vector headerBytes; - if (!base64Decode(headerPart, Base64Alphabet::Url, headerBytes)) { - result.status = makeStatus(CryptoStatus::DecodeError, "base64 decode failed"); - return result; - } - JsonDocument headerDoc; - if (deserializeJson(headerDoc, headerBytes.data(), headerBytes.size()) != DeserializationError::Ok) { - result.status = makeStatus(CryptoStatus::JsonError, "invalid header json"); - return result; - } - const char *kid = headerDoc["kid"].as(); - JwtAlgorithm alg = algorithmFromName(headerDoc["alg"].as() ? headerDoc["alg"].as() : ""); - auto keyRes = selectJwkFromSet(jwks, kid ? String(kid) : String(), alg); - if (!keyRes.ok()) { - result.status = keyRes.status; - return result; - } - auto bytes = keyRes.value.bytes(); - std::string keyStr(reinterpret_cast(bytes.data()), bytes.size()); - return verifyJwtResult(token, keyStr, outClaims, options); + return shaHex(reinterpret_cast(text.c_str()), text.length(), options); +} + +bool ESPCrypto::aesGcmEncrypt( + const std::vector &key, + const std::vector &iv, + const std::vector &plaintext, + std::vector &ciphertext, + std::vector &tag, + const std::vector &aad +) { + CryptoStatusDetail status = aesGcmEncryptInternal(key, iv, aad, plaintext, ciphertext, tag); + if (!status.ok()) { + secureZero(ciphertext.data(), ciphertext.size()); + secureZero(tag.data(), tag.size()); + } + return status.ok(); +} + +bool ESPCrypto::aesGcmDecrypt( + const std::vector &key, + const std::vector &iv, + const std::vector &ciphertext, + const std::vector &tag, + std::vector &plaintext, + const std::vector &aad +) { + CryptoStatusDetail status = aesGcmDecryptInternal(key, iv, aad, ciphertext, tag, plaintext); + if (!status.ok()) { + secureZero(plaintext.data(), plaintext.size()); + plaintext.clear(); + } + return status.ok(); +} + +bool ESPCrypto::aesCtrCrypt( + const std::vector &key, + const std::vector &nonceCounter, + const std::vector &input, + std::vector &output +) { + auto result = aesCtrCrypt(key, nonceCounter, input); + if (!result.ok()) { + output.clear(); + return false; + } + output = std::move(result.value); + return true; +} + +CryptoResult ESPCrypto::aesGcmEncryptAuto( + const std::vector &key, + const std::vector &plaintext, + const std::vector &aad, + size_t ivLength, + const GcmNonceOptions &nonceOptions +) { + CryptoResult result; + markRuntimeInitialized(); + const CryptoPolicy &policy = mutablePolicy(); + if (ivLength == 0) { + ivLength = policy.minAesGcmIvBytes; + } + if (!policy.allowLegacy && ivLength < policy.minAesGcmIvBytes) { + result.status = makeStatus(CryptoStatus::PolicyViolation, "iv too short"); + return result; + } + if (!aesKeyValid(key)) { + result.status = makeStatus(CryptoStatus::InvalidInput, "invalid key"); + return result; + } + result.value.iv.assign(ivLength, 0); + GlobalRuntimeState &state = runtimeState(); + uint32_t keyHash = fingerprintKey(key); + state.bootCounter.fetch_add(1, std::memory_order_relaxed); + switch (nonceOptions.strategy) { + case GcmNonceStrategy::Random96: + default: + fillRandom(result.value.iv.data(), result.value.iv.size()); + break; + case GcmNonceStrategy::Counter64_Random32: { + if (ivLength < 12) { + result.status = + makeStatus(CryptoStatus::PolicyViolation, "counter strategy needs >=12 iv bytes"); + return result; + } + bool found = false; + uint64_t counter = loadCounterFromNvs( + nonceOptions.nvsNamespace, + nonceOptions.nvsPartition, + "gcmctr_" + std::to_string(keyHash), + found + ); + if (!found) { + counter = 1; + } else { + counter += 1; + } + if (nonceOptions.persistCounter) { + storeCounterToNvs( + nonceOptions.nvsNamespace, + nonceOptions.nvsPartition, + "gcmctr_" + std::to_string(keyHash), + counter + ); + } + for (int i = 0; i < 8 && i < static_cast(ivLength); ++i) { + result.value.iv[i] = static_cast((counter >> (56 - 8 * i)) & 0xFF); + } + std::vector tail(ivLength > 8 ? ivLength - 8 : 0, 0); + if (!tail.empty()) { + fillRandom(tail.data(), tail.size()); + memcpy(result.value.iv.data() + 8, tail.data(), tail.size()); + } + break; + } + case GcmNonceStrategy::BootCounter_Random32: { + if (ivLength < 12) { + result.status = + makeStatus(CryptoStatus::PolicyViolation, "counter strategy needs >=12 iv bytes"); + return result; + } + 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); + } + std::vector tail(ivLength > 8 ? ivLength - 8 : 0, 0); + if (!tail.empty()) { + fillRandom(tail.data(), tail.size()); + memcpy(result.value.iv.data() + 8, tail.data(), tail.size()); + } + break; + } + } + result.status = aesGcmEncryptInternal( + key, + result.value.iv, + aad, + plaintext, + result.value.ciphertext, + result.value.tag + ); + if (!result.ok()) { + result.value = {}; + } + return result; +} + +CryptoResult> ESPCrypto::aesGcmDecrypt( + const std::vector &key, + const std::vector &iv, + const std::vector &ciphertext, + const std::vector &tag, + const std::vector &aad +) { + CryptoResult> result; + result.status = aesGcmDecryptInternal(key, iv, aad, ciphertext, tag, result.value); + if (!result.ok()) { + result.value.clear(); + } + return result; +} + +CryptoResult ESPCrypto::aesGcmEncrypt( + const std::vector &key, + CryptoSpan iv, + CryptoSpan plaintext, + CryptoSpan ciphertextOut, + CryptoSpan tagOut, + CryptoSpan aad +) { + CryptoResult result; + result.status = aesGcmEncryptSpan(key, iv, aad, plaintext, ciphertextOut, tagOut); + if (!result.ok()) { + if (!ciphertextOut.empty()) { + secureZero(ciphertextOut.data(), std::min(ciphertextOut.size(), plaintext.size())); + } + if (!tagOut.empty()) { + secureZero( + tagOut.data(), + std::min(tagOut.size(), static_cast(AES_GCM_TAG_BYTES)) + ); + } + } + return result; +} + +CryptoResult ESPCrypto::aesGcmDecrypt( + const std::vector &key, + CryptoSpan iv, + CryptoSpan ciphertext, + CryptoSpan tag, + CryptoSpan plaintextOut, + CryptoSpan aad +) { + CryptoResult result; + result.status = aesGcmDecryptSpan(key, iv, aad, ciphertext, tag, plaintextOut); + if (!result.ok()) { + if (!plaintextOut.empty()) { + secureZero(plaintextOut.data(), std::min(plaintextOut.size(), ciphertext.size())); + } + } + return result; +} + +CryptoResult> ESPCrypto::aesCtrCrypt( + const std::vector &key, + const std::vector &nonceCounter, + const std::vector &input +) { + CryptoResult> result; + if (!aesKeyValid(key) || nonceCounter.size() != 16) { + result.status = makeStatus(CryptoStatus::InvalidInput, "invalid key or nonce"); + return result; + } + result.value.assign(input.size(), 0); + bool ok = hardwareAesCtr(key, nonceCounter, input, result.value); + if (!ok) { + ok = softwareAesCtr(key, nonceCounter, input, result.value); + } + if (!ok) { + secureZero(result.value.data(), result.value.size()); + result.value.clear(); + result.status = makeStatus(CryptoStatus::InternalError, "aes ctr failed"); + return result; + } + result.status = makeStatus(CryptoStatus::Ok); + return result; +} + +bool ESPCrypto::rsaSign( + const std::string &privateKeyPem, + const uint8_t *data, + size_t length, + ShaVariant variant, + std::vector &signature +) { + if (privateKeyPem.empty() || (!data && length > 0)) { + return false; + } + return pkSignInternal(privateKeyPem, MBEDTLS_PK_RSA, variant, data, length, signature); +} + +bool ESPCrypto::rsaVerify( + const std::string &publicKeyPem, + const uint8_t *data, + size_t length, + const std::vector &signature, + ShaVariant variant +) { + if (publicKeyPem.empty() || (!data && length > 0) || signature.empty()) { + return false; + } + return pkVerifyInternal(publicKeyPem, MBEDTLS_PK_RSA, variant, data, length, signature); +} + +CryptoResult> ESPCrypto::rsaSign( + const std::string &privateKeyPem, CryptoSpan data, ShaVariant variant +) { + CryptoResult> result; + if (privateKeyPem.empty() || (!data.data() && data.size() > 0)) { + result.status = makeStatus(CryptoStatus::InvalidInput, "missing key or data"); + return result; + } + if (!pkSignInternal( + privateKeyPem, + MBEDTLS_PK_RSA, + variant, + data.data(), + data.size(), + result.value + )) { + result.status = makeStatus(CryptoStatus::VerifyFailed, "rsa sign failed"); + result.value.clear(); + return result; + } + result.status = makeStatus(CryptoStatus::Ok); + return result; +} + +CryptoResult ESPCrypto::rsaVerify( + const std::string &publicKeyPem, + CryptoSpan data, + CryptoSpan signature, + ShaVariant variant +) { + CryptoResult result; + if (publicKeyPem.empty() || (!data.data() && data.size() > 0) || signature.empty()) { + result.status = makeStatus(CryptoStatus::InvalidInput, "missing key/data/signature"); + return result; + } + if (!pkVerifyInternal( + publicKeyPem, + MBEDTLS_PK_RSA, + variant, + data.data(), + data.size(), + std::vector(signature.data(), signature.data() + signature.size()) + )) { + result.status = makeStatus(CryptoStatus::VerifyFailed, "rsa verify failed"); + return result; + } + result.status = makeStatus(CryptoStatus::Ok); + return result; +} + +CryptoResult> ESPCrypto::rsaSign( + const CryptoKey &privateKey, CryptoSpan data, ShaVariant variant +) { + CryptoResult> result; + if (!privateKey.valid() || (!data.data() && data.size() > 0)) { + result.status = makeStatus(CryptoStatus::InvalidInput, "missing key or data"); + return result; + } + auto parsed = privateKey.ensureParsedPk(true); + if (!parsed.ok()) { + result.status = parsed; + return result; + } + if (!pkSignContext( + privateKey.pk->ctx, + MBEDTLS_PK_RSA, + variant, + data.data(), + data.size(), + result.value + )) { + result.status = makeStatus(CryptoStatus::VerifyFailed, "rsa sign failed"); + result.value.clear(); + return result; + } + result.status = makeStatus(CryptoStatus::Ok); + return result; +} + +CryptoResult ESPCrypto::rsaVerify( + const CryptoKey &publicKey, + CryptoSpan data, + CryptoSpan signature, + ShaVariant variant +) { + CryptoResult result; + if (!publicKey.valid() || (!data.data() && data.size() > 0) || signature.empty()) { + result.status = makeStatus(CryptoStatus::InvalidInput, "missing key/data/signature"); + return result; + } + auto parsed = publicKey.ensureParsedPk(false); + if (!parsed.ok()) { + result.status = parsed; + return result; + } + std::vector sigVec(signature.data(), signature.data() + signature.size()); + if (!pkVerifyContext( + publicKey.pk->ctx, + MBEDTLS_PK_RSA, + variant, + data.data(), + data.size(), + sigVec + )) { + result.status = makeStatus(CryptoStatus::VerifyFailed, "rsa verify failed"); + return result; + } + result.status = makeStatus(CryptoStatus::Ok); + return result; +} + +bool ESPCrypto::eccSign( + const std::string &privateKeyPem, + const uint8_t *data, + size_t length, + ShaVariant variant, + std::vector &signature +) { + if (privateKeyPem.empty() || (!data && length > 0)) { + return false; + } + return pkSignInternal(privateKeyPem, MBEDTLS_PK_ECKEY, variant, data, length, signature); +} + +bool ESPCrypto::eccVerify( + const std::string &publicKeyPem, + const uint8_t *data, + size_t length, + const std::vector &signature, + ShaVariant variant +) { + if (publicKeyPem.empty() || (!data && length > 0) || signature.empty()) { + return false; + } + return pkVerifyInternal(publicKeyPem, MBEDTLS_PK_ECKEY, variant, data, length, signature); +} + +CryptoResult> ESPCrypto::eccSign( + const std::string &privateKeyPem, CryptoSpan data, ShaVariant variant +) { + CryptoResult> result; + if (privateKeyPem.empty() || (!data.data() && data.size() > 0)) { + result.status = makeStatus(CryptoStatus::InvalidInput, "missing key or data"); + return result; + } + if (!pkSignInternal( + privateKeyPem, + MBEDTLS_PK_ECKEY, + variant, + data.data(), + data.size(), + result.value + )) { + result.status = makeStatus(CryptoStatus::VerifyFailed, "ecc sign failed"); + result.value.clear(); + return result; + } + result.status = makeStatus(CryptoStatus::Ok); + return result; +} + +CryptoResult ESPCrypto::eccVerify( + const std::string &publicKeyPem, + CryptoSpan data, + CryptoSpan signature, + ShaVariant variant +) { + CryptoResult result; + if (publicKeyPem.empty() || (!data.data() && data.size() > 0) || signature.empty()) { + result.status = makeStatus(CryptoStatus::InvalidInput, "missing key/data/signature"); + return result; + } + if (!pkVerifyInternal( + publicKeyPem, + MBEDTLS_PK_ECKEY, + variant, + data.data(), + data.size(), + std::vector(signature.data(), signature.data() + signature.size()) + )) { + result.status = makeStatus(CryptoStatus::VerifyFailed, "ecc verify failed"); + return result; + } + result.status = makeStatus(CryptoStatus::Ok); + return result; +} + +CryptoResult> ESPCrypto::eccSign( + const CryptoKey &privateKey, CryptoSpan data, ShaVariant variant +) { + CryptoResult> result; + if (!privateKey.valid() || (!data.data() && data.size() > 0)) { + result.status = makeStatus(CryptoStatus::InvalidInput, "missing key or data"); + return result; + } + auto parsed = privateKey.ensureParsedPk(true); + if (!parsed.ok()) { + result.status = parsed; + return result; + } + if (!pkSignContext( + privateKey.pk->ctx, + MBEDTLS_PK_ECKEY, + variant, + data.data(), + data.size(), + result.value + )) { + result.status = makeStatus(CryptoStatus::VerifyFailed, "ecc sign failed"); + result.value.clear(); + return result; + } + result.status = makeStatus(CryptoStatus::Ok); + return result; +} + +CryptoResult ESPCrypto::eccVerify( + const CryptoKey &publicKey, + CryptoSpan data, + CryptoSpan signature, + ShaVariant variant +) { + CryptoResult result; + if (!publicKey.valid() || (!data.data() && data.size() > 0) || signature.empty()) { + result.status = makeStatus(CryptoStatus::InvalidInput, "missing key/data/signature"); + return result; + } + auto parsed = publicKey.ensureParsedPk(false); + if (!parsed.ok()) { + result.status = parsed; + return result; + } + std::vector sigVec(signature.data(), signature.data() + signature.size()); + if (!pkVerifyContext( + publicKey.pk->ctx, + MBEDTLS_PK_ECKEY, + variant, + data.data(), + data.size(), + sigVec + )) { + result.status = makeStatus(CryptoStatus::VerifyFailed, "ecc verify failed"); + return result; + } + result.status = makeStatus(CryptoStatus::Ok); + return result; +} + +String ESPCrypto::createJwt( + const JsonDocument &claims, const std::string &key, const JwtSignOptions &options +) { + auto result = createJwtResult(claims, key, options); + return result.ok() ? result.value : String(); +} + +bool ESPCrypto::verifyJwt( + const String &token, + const std::string &key, + JsonDocument &outClaims, + String &error, + const JwtVerifyOptions &options +) { + auto result = verifyJwtResult(token, key, outClaims, options); + if (!result.ok()) { + error = result.status.message.length() > 0 ? result.status.message + : String(toString(result.status.code)); + return false; + } + error = ""; + return true; +} + +CryptoResult ESPCrypto::createJwtResult( + const JsonDocument &claims, const std::string &key, const JwtSignOptions &options +) { + CryptoResult result; + if (key.empty()) { + result.status = makeStatus(CryptoStatus::InvalidInput, "key missing"); + return result; + } + JsonDocument header; + std::string algName = algorithmName(options.algorithm); + if (algName.empty()) { + result.status = makeStatus(CryptoStatus::Unsupported, "unsupported alg"); + return result; + } + header["alg"] = algName.c_str(); + header["typ"] = "JWT"; + if (options.keyId.length() > 0) { + header["kid"] = options.keyId.c_str(); + } + JsonDocument payload; + payload.set(claims); + if (options.issuer.length() > 0 && payload["iss"].isNull()) { + payload["iss"] = options.issuer.c_str(); + } + if (options.subject.length() > 0 && payload["sub"].isNull()) { + payload["sub"] = options.subject.c_str(); + } + if (options.audience.length() > 0 && payload["aud"].isNull()) { + payload["aud"] = options.audience.c_str(); + } + uint32_t now = currentTimeSeconds( + options.currentTimestamp != 0 ? options.currentTimestamp : options.issuedAt + ); + if (options.issuedAt != 0) { + payload["iat"] = options.issuedAt; + } else { + payload["iat"] = now; + } + if (options.expiresInSeconds > 0) { + payload["exp"] = + static_cast(payload["iat"].as() + options.expiresInSeconds); + } + if (options.notBefore > 0) { + payload["nbf"] = options.notBefore; + } + + std::string headerJson; + if (serializeJson(header, headerJson) == 0) { + result.status = makeStatus(CryptoStatus::JsonError, "header serialization failed"); + return result; + } + std::string payloadJson; + if (serializeJson(payload, payloadJson) == 0) { + result.status = makeStatus(CryptoStatus::JsonError, "payload serialization failed"); + return result; + } + + std::string encodedHeader = base64Encode( + reinterpret_cast(headerJson.data()), + headerJson.size(), + Base64Alphabet::Url + ); + std::string encodedPayload = base64Encode( + reinterpret_cast(payloadJson.data()), + payloadJson.size(), + Base64Alphabet::Url + ); + if (encodedHeader.empty() || encodedPayload.empty()) { + result.status = makeStatus(CryptoStatus::DecodeError, "base64 encode failed"); + return result; + } + std::string signingInput = encodedHeader + "." + encodedPayload; + std::vector signature; + if (!signJwt( + options.algorithm, + key, + reinterpret_cast(signingInput.data()), + signingInput.size(), + signature + )) { + result.status = makeStatus(CryptoStatus::InternalError, "sign failed"); + return result; + } + std::string encodedSignature = + base64Encode(signature.data(), signature.size(), Base64Alphabet::Url); + std::string token = signingInput + "." + encodedSignature; + result.value = String(token.c_str()); + result.status = makeStatus(CryptoStatus::Ok); + return result; +} + +CryptoResult ESPCrypto::verifyJwtResult( + const String &token, + const std::string &key, + JsonDocument &outClaims, + const JwtVerifyOptions &options +) { + CryptoResult result; + if (token.length() == 0 || key.empty()) { + result.status = makeStatus(CryptoStatus::InvalidInput, "token or key missing"); + return result; + } + std::string tokenStd(token.c_str(), token.length()); + size_t first = tokenStd.find('.'); + size_t second = tokenStd.find('.', first == std::string::npos ? 0 : first + 1); + if (first == std::string::npos || second == std::string::npos) { + result.status = makeStatus(CryptoStatus::DecodeError, "invalid token structure"); + return result; + } + std::string headerPart = tokenStd.substr(0, first); + std::string payloadPart = tokenStd.substr(first + 1, second - first - 1); + std::string signaturePart = tokenStd.substr(second + 1); + std::vector headerBytes; + std::vector payloadBytes; + std::vector signatureBytes; + if (!base64Decode(headerPart, Base64Alphabet::Url, headerBytes) || + !base64Decode(payloadPart, Base64Alphabet::Url, payloadBytes) || + !base64Decode(signaturePart, Base64Alphabet::Url, signatureBytes)) { + result.status = makeStatus(CryptoStatus::DecodeError, "base64 decode failed"); + return result; + } + JsonDocument headerDoc; + if (deserializeJson(headerDoc, headerBytes.data(), headerBytes.size()) != + DeserializationError::Ok) { + result.status = makeStatus(CryptoStatus::JsonError, "invalid header json"); + return result; + } + JsonDocument payloadDoc; + if (deserializeJson(payloadDoc, payloadBytes.data(), payloadBytes.size()) != + DeserializationError::Ok) { + result.status = makeStatus(CryptoStatus::JsonError, "invalid payload json"); + return result; + } + const char *algStr = headerDoc["alg"].as(); + JwtAlgorithm alg = algorithmFromName(algStr ? algStr : ""); + if (alg == JwtAlgorithm::Auto) { + result.status = makeStatus(CryptoStatus::Unsupported, "unsupported alg"); + return result; + } + if (options.algorithm != JwtAlgorithm::Auto && options.algorithm != alg) { + result.status = makeStatus(CryptoStatus::PolicyViolation, "alg mismatch"); + return result; + } + const char *typHdr = headerDoc["typ"].as(); + if (options.expectedTyp.length() > 0) { + if (!typHdr || options.expectedTyp != typHdr) { + result.status = makeStatus(CryptoStatus::PolicyViolation, "typ mismatch"); + return result; + } + } + JsonArray crit = headerDoc["crit"].as(); + if (!crit.isNull() && !options.criticalHeadersAllowed.empty()) { + for (JsonVariant v : crit) { + const char *name = v.as(); + bool allowed = false; + for (const auto &allowedName : options.criticalHeadersAllowed) { + if (name && allowedName == name) { + allowed = true; + break; + } + } + if (!allowed) { + result.status = + makeStatus(CryptoStatus::PolicyViolation, "crit header not allowed"); + return result; + } + } + } else if (!crit.isNull() && options.criticalHeadersAllowed.empty()) { + result.status = makeStatus(CryptoStatus::PolicyViolation, "crit header not allowed"); + return result; + } + std::string signingInput = headerPart + "." + payloadPart; + if (!verifySignature( + alg, + key, + reinterpret_cast(signingInput.data()), + signingInput.size(), + signatureBytes + )) { + result.status = makeStatus(CryptoStatus::VerifyFailed, "signature mismatch"); + return result; + } + uint32_t now = currentTimeSeconds(options.currentTimestamp); + uint32_t leeway = options.leewaySeconds; + uint32_t exp = payloadDoc["exp"].as(); + uint32_t nbf = payloadDoc["nbf"].as(); + if (options.requireExpiration && exp == 0) { + result.status = makeStatus(CryptoStatus::PolicyViolation, "missing exp"); + return result; + } + if (exp != 0 && now > exp + leeway) { + result.status = makeStatus(CryptoStatus::Expired, "token expired"); + return result; + } + if (nbf != 0 && now + leeway < nbf) { + result.status = makeStatus(CryptoStatus::NotYetValid, "token not active"); + return result; + } + auto audMatch = [&](const char *aud) -> bool { + if (!aud) { + return false; + } + if (options.audience.length() > 0 && options.audience == aud) { + return true; + } + for (const auto &a : options.audiences) { + if (a == aud) { + return true; + } + } + return options.audience.length() == 0 && options.audiences.empty(); + }; + if (options.audience.length() > 0 || !options.audiences.empty()) { + bool ok = false; + if (payloadDoc["aud"].is()) { + JsonArray arr = payloadDoc["aud"].as(); + for (JsonVariant v : arr) { + ok = audMatch(v.as()); + if (ok) + break; + } + } else { + ok = audMatch(payloadDoc["aud"].as()); + } + if (!ok) { + result.status = makeStatus(CryptoStatus::AudienceMismatch, "aud mismatch"); + return result; + } + } + if (options.issuer.length() > 0) { + const char *iss = payloadDoc["iss"].as(); + if (!iss || options.issuer != iss) { + result.status = makeStatus(CryptoStatus::IssuerMismatch, "iss mismatch"); + return result; + } + } + outClaims.set(payloadDoc); + result.status = makeStatus(CryptoStatus::Ok); + return result; +} + +CryptoResult ESPCrypto::verifyJwtWithJwks( + const String &token, + const JsonDocument &jwks, + JsonDocument &outClaims, + const JwtVerifyOptions &options +) { + CryptoResult result; + if (token.length() == 0) { + result.status = makeStatus(CryptoStatus::InvalidInput, "token missing"); + return result; + } + std::string tokenStd(token.c_str(), token.length()); + size_t first = tokenStd.find('.'); + size_t second = tokenStd.find('.', first == std::string::npos ? 0 : first + 1); + if (first == std::string::npos || second == std::string::npos) { + result.status = makeStatus(CryptoStatus::DecodeError, "invalid token structure"); + return result; + } + std::string headerPart = tokenStd.substr(0, first); + std::vector headerBytes; + if (!base64Decode(headerPart, Base64Alphabet::Url, headerBytes)) { + result.status = makeStatus(CryptoStatus::DecodeError, "base64 decode failed"); + return result; + } + JsonDocument headerDoc; + if (deserializeJson(headerDoc, headerBytes.data(), headerBytes.size()) != + DeserializationError::Ok) { + result.status = makeStatus(CryptoStatus::JsonError, "invalid header json"); + return result; + } + const char *kid = headerDoc["kid"].as(); + JwtAlgorithm alg = algorithmFromName( + headerDoc["alg"].as() ? headerDoc["alg"].as() : "" + ); + auto keyRes = selectJwkFromSet(jwks, kid ? String(kid) : String(), alg); + if (!keyRes.ok()) { + result.status = keyRes.status; + return result; + } + auto bytes = keyRes.value.bytes(); + std::string keyStr(reinterpret_cast(bytes.data()), bytes.size()); + return verifyJwtResult(token, keyStr, outClaims, options); } String ESPCrypto::hashString(const String &input, const PasswordHashOptions &options) { - auto result = hashStringResult(input, options); - return result.ok() ? result.value : String(); + auto result = hashStringResult(input, options); + return result.ok() ? result.value : String(); } bool ESPCrypto::verifyString(const String &input, const String &encoded) { - auto result = verifyStringResult(input, encoded); - return result.ok(); -} - -CryptoResult ESPCrypto::hashStringResult(const String &input, const PasswordHashOptions &options) { - CryptoResult result; - if (input.length() == 0 || options.saltBytes == 0 || options.outputBytes == 0) { - result.status = makeStatus(CryptoStatus::InvalidInput, "missing password or params"); - return result; - } - std::vector salt(options.saltBytes, 0); - 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; - while ((1u << adjustedCost) < policy.minPbkdf2Iterations && adjustedCost < 31) { - adjustedCost++; - } - cost = adjustedCost; - iterations = 1u << cost; - } - auto derived = pbkdf2(input, CryptoSpan(salt), iterations, options.outputBytes); - if (!derived.ok()) { - result.status = derived.status; - return result; - } - std::string saltB64 = base64Encode(salt.data(), salt.size(), Base64Alphabet::Standard); - std::string hashB64 = base64Encode(derived.value.data(), derived.value.size(), Base64Alphabet::Standard); - secureZero(derived.value.data(), derived.value.size()); - if (saltB64.empty() || hashB64.empty()) { - result.status = makeStatus(CryptoStatus::InternalError, "base64 encode failed"); - return result; - } - std::string encoded = "$esphash$v1$" + std::to_string(cost) + "$" + saltB64 + "$" + hashB64; - result.value = String(encoded.c_str()); - result.status = makeStatus(CryptoStatus::Ok); - return result; + auto result = verifyStringResult(input, encoded); + return result.ok(); +} + +CryptoResult +ESPCrypto::hashStringResult(const String &input, const PasswordHashOptions &options) { + CryptoResult result; + if (input.length() == 0 || options.saltBytes == 0 || options.outputBytes == 0) { + result.status = makeStatus(CryptoStatus::InvalidInput, "missing password or params"); + return result; + } + std::vector salt(options.saltBytes, 0); + 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; + while ((1u << adjustedCost) < policy.minPbkdf2Iterations && adjustedCost < 31) { + adjustedCost++; + } + cost = adjustedCost; + iterations = 1u << cost; + } + auto derived = pbkdf2(input, CryptoSpan(salt), iterations, options.outputBytes); + if (!derived.ok()) { + result.status = derived.status; + return result; + } + std::string saltB64 = base64Encode(salt.data(), salt.size(), Base64Alphabet::Standard); + std::string hashB64 = + base64Encode(derived.value.data(), derived.value.size(), Base64Alphabet::Standard); + secureZero(derived.value.data(), derived.value.size()); + if (saltB64.empty() || hashB64.empty()) { + result.status = makeStatus(CryptoStatus::InternalError, "base64 encode failed"); + return result; + } + std::string encoded = "$esphash$v1$" + std::to_string(cost) + "$" + saltB64 + "$" + hashB64; + result.value = String(encoded.c_str()); + result.status = makeStatus(CryptoStatus::Ok); + return result; } CryptoResult ESPCrypto::verifyStringResult(const String &input, const String &encoded) { - CryptoResult result; - if (input.length() == 0 || encoded.length() == 0) { - result.status = makeStatus(CryptoStatus::InvalidInput, "missing password or encoded hash"); - return result; - } - uint8_t cost = 0; - std::vector salt; - std::vector hash; - std::string encodedStd(encoded.c_str(), encoded.length()); - if (!parsePasswordHash(encodedStd, cost, salt, hash)) { - result.status = makeStatus(CryptoStatus::DecodeError, "invalid esphash envelope"); - return result; - } - if (salt.empty() || hash.empty()) { - result.status = makeStatus(CryptoStatus::DecodeError, "invalid esphash parts"); - 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"); - return result; - } - auto derived = pbkdf2(input, CryptoSpan(salt), iterations, hash.size()); - if (!derived.ok()) { - result.status = derived.status; - return result; - } - bool match = constantTimeEquals(CryptoSpan(hash), CryptoSpan(derived.value)); - secureZero(derived.value.data(), derived.value.size()); - result.status = match ? makeStatus(CryptoStatus::Ok) : makeStatus(CryptoStatus::VerifyFailed, "hash mismatch"); - return result; -} - -CryptoResult> ESPCrypto::hmac(ShaVariant variant, - CryptoSpan key, - CryptoSpan data) { - CryptoResult> result; - const mbedtls_md_info_t *info = mdInfoForVariant(variant); - if (!info) { - result.status = makeStatus(CryptoStatus::InvalidInput, "invalid sha variant"); - return result; - } - result.value.assign(mbedtls_md_get_size(info), 0); - mbedtls_md_context_t ctx; - mbedtls_md_init(&ctx); - int ret = mbedtls_md_setup(&ctx, info, 1); - if (ret == 0) { - ret = mbedtls_md_hmac_starts(&ctx, reinterpret_cast(key.data()), key.size()); - } - if (ret == 0) { - ret = mbedtls_md_hmac_update(&ctx, data.data(), data.size()); - } - if (ret == 0) { - ret = mbedtls_md_hmac_finish(&ctx, result.value.data()); - } - mbedtls_md_free(&ctx); - if (ret != 0) { - secureZero(result.value.data(), result.value.size()); - result.value.clear(); - result.status = makeStatus(CryptoStatus::InternalError, "hmac failed"); - return result; - } - result.status = makeStatus(CryptoStatus::Ok); - return result; -} - -CryptoResult> ESPCrypto::hkdf(ShaVariant variant, - CryptoSpan salt, - CryptoSpan ikm, - CryptoSpan info, - size_t length) { - CryptoResult> result; - if (length == 0) { - result.status = makeStatus(CryptoStatus::InvalidInput, "length missing"); - return result; - } - const size_t hashLen = digestLength(variant); - if (hashLen == 0) { - result.status = makeStatus(CryptoStatus::InvalidInput, "invalid sha variant"); - return result; - } - size_t blocks = (length + hashLen - 1) / hashLen; - if (blocks > 255) { - result.status = makeStatus(CryptoStatus::BufferTooSmall, "length too large"); - return result; - } - std::vector actualSalt; - if (salt.empty()) { - actualSalt.assign(hashLen, 0); - } else { - actualSalt.assign(salt.data(), salt.data() + salt.size()); - } - auto prk = hmac(variant, CryptoSpan(actualSalt), ikm); - secureZero(actualSalt.data(), actualSalt.size()); - if (!prk.ok()) { - result.status = prk.status; - return result; - } - result.value.reserve(length); - std::vector previous; - for (size_t i = 0; i < blocks; ++i) { - std::vector blockInput; - blockInput.insert(blockInput.end(), previous.begin(), previous.end()); - if (!info.empty()) { - blockInput.insert(blockInput.end(), info.data(), info.data() + info.size()); - } - blockInput.push_back(static_cast(i + 1)); - auto block = hmac(variant, CryptoSpan(prk.value), CryptoSpan(blockInput)); - secureZero(blockInput.data(), blockInput.size()); - if (!block.ok()) { - secureZero(prk.value.data(), prk.value.size()); - result.status = block.status; - return result; - } - size_t take = std::min(hashLen, length - result.value.size()); - result.value.insert(result.value.end(), block.value.begin(), block.value.begin() + take); - previous = std::move(block.value); - } - secureZero(prk.value.data(), prk.value.size()); - secureZero(previous.data(), previous.size()); - result.status = makeStatus(CryptoStatus::Ok); - return result; -} - -CryptoResult> ESPCrypto::pbkdf2(const String &password, - CryptoSpan salt, - uint32_t iterations, - size_t outputLength) { - CryptoResult> result; - if (password.length() == 0 || salt.empty() || outputLength == 0) { - 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"); - return result; - } - result.value.assign(outputLength, 0); - int ret = pbkdf2Sha256(reinterpret_cast(password.c_str()), - password.length(), - salt.data(), - salt.size(), - iterations, - result.value.data(), - result.value.size()); - if (ret != 0) { - secureZero(result.value.data(), result.value.size()); - result.value.clear(); - result.status = makeStatus(CryptoStatus::InternalError, "pbkdf2 failed"); - return result; - } - result.status = makeStatus(CryptoStatus::Ok); - return result; + CryptoResult result; + if (input.length() == 0 || encoded.length() == 0) { + result.status = makeStatus(CryptoStatus::InvalidInput, "missing password or encoded hash"); + return result; + } + uint8_t cost = 0; + std::vector salt; + std::vector hash; + std::string encodedStd(encoded.c_str(), encoded.length()); + if (!parsePasswordHash(encodedStd, cost, salt, hash)) { + result.status = makeStatus(CryptoStatus::DecodeError, "invalid esphash envelope"); + return result; + } + if (salt.empty() || hash.empty()) { + result.status = makeStatus(CryptoStatus::DecodeError, "invalid esphash parts"); + 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"); + return result; + } + auto derived = pbkdf2(input, CryptoSpan(salt), iterations, hash.size()); + if (!derived.ok()) { + result.status = derived.status; + return result; + } + bool match = constantTimeEquals( + CryptoSpan(hash), + CryptoSpan(derived.value) + ); + secureZero(derived.value.data(), derived.value.size()); + result.status = match ? makeStatus(CryptoStatus::Ok) + : makeStatus(CryptoStatus::VerifyFailed, "hash mismatch"); + return result; +} + +CryptoResult> +ESPCrypto::hmac(ShaVariant variant, CryptoSpan key, CryptoSpan data) { + CryptoResult> result; + const mbedtls_md_info_t *info = mdInfoForVariant(variant); + if (!info) { + result.status = makeStatus(CryptoStatus::InvalidInput, "invalid sha variant"); + return result; + } + result.value.assign(mbedtls_md_get_size(info), 0); + mbedtls_md_context_t ctx; + mbedtls_md_init(&ctx); + int ret = mbedtls_md_setup(&ctx, info, 1); + if (ret == 0) { + ret = mbedtls_md_hmac_starts( + &ctx, + reinterpret_cast(key.data()), + key.size() + ); + } + if (ret == 0) { + ret = mbedtls_md_hmac_update(&ctx, data.data(), data.size()); + } + if (ret == 0) { + ret = mbedtls_md_hmac_finish(&ctx, result.value.data()); + } + mbedtls_md_free(&ctx); + if (ret != 0) { + secureZero(result.value.data(), result.value.size()); + result.value.clear(); + result.status = makeStatus(CryptoStatus::InternalError, "hmac failed"); + return result; + } + result.status = makeStatus(CryptoStatus::Ok); + return result; +} + +CryptoResult> ESPCrypto::hkdf( + ShaVariant variant, + CryptoSpan salt, + CryptoSpan ikm, + CryptoSpan info, + size_t length +) { + CryptoResult> result; + if (length == 0) { + result.status = makeStatus(CryptoStatus::InvalidInput, "length missing"); + return result; + } + const size_t hashLen = digestLength(variant); + if (hashLen == 0) { + result.status = makeStatus(CryptoStatus::InvalidInput, "invalid sha variant"); + return result; + } + size_t blocks = (length + hashLen - 1) / hashLen; + if (blocks > 255) { + result.status = makeStatus(CryptoStatus::BufferTooSmall, "length too large"); + return result; + } + std::vector actualSalt; + if (salt.empty()) { + actualSalt.assign(hashLen, 0); + } else { + actualSalt.assign(salt.data(), salt.data() + salt.size()); + } + auto prk = hmac(variant, CryptoSpan(actualSalt), ikm); + secureZero(actualSalt.data(), actualSalt.size()); + if (!prk.ok()) { + result.status = prk.status; + return result; + } + result.value.reserve(length); + std::vector previous; + for (size_t i = 0; i < blocks; ++i) { + std::vector blockInput; + blockInput.insert(blockInput.end(), previous.begin(), previous.end()); + if (!info.empty()) { + blockInput.insert(blockInput.end(), info.data(), info.data() + info.size()); + } + blockInput.push_back(static_cast(i + 1)); + auto block = hmac( + variant, + CryptoSpan(prk.value), + CryptoSpan(blockInput) + ); + secureZero(blockInput.data(), blockInput.size()); + if (!block.ok()) { + secureZero(prk.value.data(), prk.value.size()); + result.status = block.status; + return result; + } + size_t take = std::min(hashLen, length - result.value.size()); + result.value.insert(result.value.end(), block.value.begin(), block.value.begin() + take); + previous = std::move(block.value); + } + secureZero(prk.value.data(), prk.value.size()); + secureZero(previous.data(), previous.size()); + result.status = makeStatus(CryptoStatus::Ok); + return result; +} + +CryptoResult> ESPCrypto::pbkdf2( + const String &password, CryptoSpan salt, uint32_t iterations, size_t outputLength +) { + CryptoResult> result; + if (password.length() == 0 || salt.empty() || outputLength == 0) { + 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"); + return result; + } + result.value.assign(outputLength, 0); + int ret = pbkdf2Sha256( + reinterpret_cast(password.c_str()), + password.length(), + salt.data(), + salt.size(), + iterations, + result.value.data(), + result.value.size() + ); + if (ret != 0) { + secureZero(result.value.data(), result.value.size()); + result.value.clear(); + result.status = makeStatus(CryptoStatus::InternalError, "pbkdf2 failed"); + return result; + } + result.status = makeStatus(CryptoStatus::Ok); + return result; } CryptoResult> ESPCrypto::ecdsaDerToRaw(CryptoSpan der) { - return ecdsaDerToRawInternal(der); + return ecdsaDerToRawInternal(der); } CryptoResult> ESPCrypto::ecdsaRawToDer(CryptoSpan raw) { - return ecdsaRawToDerInternal(raw); + return ecdsaRawToDerInternal(raw); } -CryptoResult> ESPCrypto::chacha20Poly1305Encrypt(CryptoSpan key, - CryptoSpan nonce, - CryptoSpan aad, - CryptoSpan plaintext) { - CryptoResult> result; +CryptoResult> ESPCrypto::chacha20Poly1305Encrypt( + CryptoSpan key, + CryptoSpan nonce, + CryptoSpan aad, + CryptoSpan plaintext +) { + CryptoResult> result; #if defined(MBEDTLS_CHACHAPOLY_C) - if (key.size() != 32 || nonce.size() < 12) { - result.status = makeStatus(CryptoStatus::InvalidInput, "key/nonce invalid"); - return result; - } - mbedtls_chachapoly_context ctx; - mbedtls_chachapoly_init(&ctx); - if (mbedtls_chachapoly_setkey(&ctx, key.data()) != 0) { - mbedtls_chachapoly_free(&ctx); - result.status = makeStatus(CryptoStatus::InternalError, "setkey failed"); - return result; - } - result.value.assign(plaintext.size() + 16, 0); - if (mbedtls_chachapoly_encrypt_and_tag(&ctx, plaintext.size(), - nonce.data(), - aad.data(), aad.size(), - plaintext.data(), - result.value.data(), - result.value.data() + plaintext.size()) != 0) { - result.value.clear(); - result.status = makeStatus(CryptoStatus::InternalError, "chacha20poly1305 encrypt failed"); - mbedtls_chachapoly_free(&ctx); - return result; - } - mbedtls_chachapoly_free(&ctx); - result.status = makeStatus(CryptoStatus::Ok); + if (key.size() != 32 || nonce.size() < 12) { + result.status = makeStatus(CryptoStatus::InvalidInput, "key/nonce invalid"); + return result; + } + mbedtls_chachapoly_context ctx; + mbedtls_chachapoly_init(&ctx); + if (mbedtls_chachapoly_setkey(&ctx, key.data()) != 0) { + mbedtls_chachapoly_free(&ctx); + result.status = makeStatus(CryptoStatus::InternalError, "setkey failed"); + return result; + } + result.value.assign(plaintext.size() + 16, 0); + if (mbedtls_chachapoly_encrypt_and_tag( + &ctx, + plaintext.size(), + nonce.data(), + aad.data(), + aad.size(), + plaintext.data(), + result.value.data(), + result.value.data() + plaintext.size() + ) != 0) { + result.value.clear(); + result.status = makeStatus(CryptoStatus::InternalError, "chacha20poly1305 encrypt failed"); + mbedtls_chachapoly_free(&ctx); + return result; + } + mbedtls_chachapoly_free(&ctx); + result.status = makeStatus(CryptoStatus::Ok); #else - (void)key; - (void)nonce; - (void)aad; - (void)plaintext; - result.status = makeStatus(CryptoStatus::Unsupported, "chachapoly unavailable"); + (void)key; + (void)nonce; + (void)aad; + (void)plaintext; + result.status = makeStatus(CryptoStatus::Unsupported, "chachapoly unavailable"); #endif - return result; + return result; } -CryptoResult> ESPCrypto::chacha20Poly1305Decrypt(CryptoSpan key, - CryptoSpan nonce, - CryptoSpan aad, - CryptoSpan ciphertextAndTag) { - CryptoResult> result; +CryptoResult> ESPCrypto::chacha20Poly1305Decrypt( + CryptoSpan key, + CryptoSpan nonce, + CryptoSpan aad, + CryptoSpan ciphertextAndTag +) { + CryptoResult> result; #if defined(MBEDTLS_CHACHAPOLY_C) - if (key.size() != 32 || nonce.size() < 12 || ciphertextAndTag.size() < 17) { - result.status = makeStatus(CryptoStatus::InvalidInput, "input invalid"); - return result; - } - size_t cipherLen = ciphertextAndTag.size() - 16; - const uint8_t *tag = ciphertextAndTag.data() + cipherLen; - result.value.assign(cipherLen, 0); - mbedtls_chachapoly_context ctx; - mbedtls_chachapoly_init(&ctx); - if (mbedtls_chachapoly_setkey(&ctx, key.data()) != 0) { - mbedtls_chachapoly_free(&ctx); - result.status = makeStatus(CryptoStatus::InternalError, "setkey failed"); - return result; - } - if (mbedtls_chachapoly_auth_decrypt(&ctx, cipherLen, - nonce.data(), - aad.data(), aad.size(), - tag, - ciphertextAndTag.data(), - result.value.data()) != 0) { - mbedtls_chachapoly_free(&ctx); - secureZero(result.value.data(), result.value.size()); - result.value.clear(); - result.status = makeStatus(CryptoStatus::VerifyFailed, "auth failed"); - return result; - } - mbedtls_chachapoly_free(&ctx); - result.status = makeStatus(CryptoStatus::Ok); + if (key.size() != 32 || nonce.size() < 12 || ciphertextAndTag.size() < 17) { + result.status = makeStatus(CryptoStatus::InvalidInput, "input invalid"); + return result; + } + size_t cipherLen = ciphertextAndTag.size() - 16; + const uint8_t *tag = ciphertextAndTag.data() + cipherLen; + result.value.assign(cipherLen, 0); + mbedtls_chachapoly_context ctx; + mbedtls_chachapoly_init(&ctx); + if (mbedtls_chachapoly_setkey(&ctx, key.data()) != 0) { + mbedtls_chachapoly_free(&ctx); + result.status = makeStatus(CryptoStatus::InternalError, "setkey failed"); + return result; + } + if (mbedtls_chachapoly_auth_decrypt( + &ctx, + cipherLen, + nonce.data(), + aad.data(), + aad.size(), + tag, + ciphertextAndTag.data(), + result.value.data() + ) != 0) { + mbedtls_chachapoly_free(&ctx); + secureZero(result.value.data(), result.value.size()); + result.value.clear(); + result.status = makeStatus(CryptoStatus::VerifyFailed, "auth failed"); + return result; + } + mbedtls_chachapoly_free(&ctx); + result.status = makeStatus(CryptoStatus::Ok); #else - (void)key; - (void)nonce; - (void)aad; - (void)ciphertextAndTag; - result.status = makeStatus(CryptoStatus::Unsupported, "chachapoly unavailable"); + (void)key; + (void)nonce; + (void)aad; + (void)ciphertextAndTag; + result.status = makeStatus(CryptoStatus::Unsupported, "chachapoly unavailable"); #endif - return result; + return result; } -CryptoResult> ESPCrypto::x25519(CryptoSpan privateKey, - CryptoSpan peerPublic) { - CryptoResult> result; +CryptoResult> +ESPCrypto::x25519(CryptoSpan privateKey, CryptoSpan peerPublic) { + CryptoResult> result; #if defined(MBEDTLS_ECP_DP_CURVE25519_ENABLED) - if (privateKey.size() != 32 || peerPublic.size() != 32) { - result.status = makeStatus(CryptoStatus::InvalidInput, "keys must be 32 bytes"); - return result; - } - mbedtls_ecp_group grp; - mbedtls_ecp_point Qp; - mbedtls_mpi d; - mbedtls_mpi z; - mbedtls_ecp_group_init(&grp); - mbedtls_ecp_point_init(&Qp); - mbedtls_mpi_init(&d); - mbedtls_mpi_init(&z); - - int ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_CURVE25519); - if (ret != 0) { - result.status = makeStatus(CryptoStatus::Unsupported, "curve not available"); - goto cleanup; - } - ret = mbedtls_mpi_read_binary(&d, privateKey.data(), privateKey.size()); - if (ret != 0) { - result.status = makeStatus(CryptoStatus::DecodeError, "private key load failed"); - goto cleanup; - } - ret = mbedtls_mpi_read_binary(&Qp.MBEDTLS_PRIVATE(X), peerPublic.data(), peerPublic.size()); - if (ret != 0 || mbedtls_mpi_lset(&Qp.MBEDTLS_PRIVATE(Z), 1) != 0) { - result.status = makeStatus(CryptoStatus::DecodeError, "key load failed"); - goto cleanup; - } - result.value.assign(32, 0); - ret = mbedtls_ecdh_compute_shared(&grp, &z, &Qp, &d, nullptr, nullptr); - if (ret != 0) { - result.value.clear(); - result.status = makeStatus(CryptoStatus::InternalError, "x25519 failed"); - goto cleanup; - } - ret = mbedtls_mpi_write_binary(&z, result.value.data(), result.value.size()); - if (ret != 0) { - secureZero(result.value.data(), result.value.size()); - result.value.clear(); - result.status = makeStatus(CryptoStatus::InternalError, "x25519 write failed"); - goto cleanup; - } - result.status = makeStatus(CryptoStatus::Ok); + if (privateKey.size() != 32 || peerPublic.size() != 32) { + result.status = makeStatus(CryptoStatus::InvalidInput, "keys must be 32 bytes"); + return result; + } + mbedtls_ecp_group grp; + mbedtls_ecp_point Qp; + mbedtls_mpi d; + mbedtls_mpi z; + mbedtls_ecp_group_init(&grp); + mbedtls_ecp_point_init(&Qp); + mbedtls_mpi_init(&d); + mbedtls_mpi_init(&z); + + int ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_CURVE25519); + if (ret != 0) { + result.status = makeStatus(CryptoStatus::Unsupported, "curve not available"); + goto cleanup; + } + ret = mbedtls_mpi_read_binary(&d, privateKey.data(), privateKey.size()); + if (ret != 0) { + result.status = makeStatus(CryptoStatus::DecodeError, "private key load failed"); + goto cleanup; + } + ret = mbedtls_mpi_read_binary(&Qp.MBEDTLS_PRIVATE(X), peerPublic.data(), peerPublic.size()); + if (ret != 0 || mbedtls_mpi_lset(&Qp.MBEDTLS_PRIVATE(Z), 1) != 0) { + result.status = makeStatus(CryptoStatus::DecodeError, "key load failed"); + goto cleanup; + } + result.value.assign(32, 0); + ret = mbedtls_ecdh_compute_shared(&grp, &z, &Qp, &d, nullptr, nullptr); + if (ret != 0) { + result.value.clear(); + result.status = makeStatus(CryptoStatus::InternalError, "x25519 failed"); + goto cleanup; + } + ret = mbedtls_mpi_write_binary(&z, result.value.data(), result.value.size()); + if (ret != 0) { + secureZero(result.value.data(), result.value.size()); + result.value.clear(); + result.status = makeStatus(CryptoStatus::InternalError, "x25519 write failed"); + goto cleanup; + } + result.status = makeStatus(CryptoStatus::Ok); cleanup: - mbedtls_mpi_free(&z); - mbedtls_mpi_free(&d); - mbedtls_ecp_point_free(&Qp); - mbedtls_ecp_group_free(&grp); + mbedtls_mpi_free(&z); + mbedtls_mpi_free(&d); + mbedtls_ecp_point_free(&Qp); + mbedtls_ecp_group_free(&grp); #else - (void)privateKey; - (void)peerPublic; - result.status = makeStatus(CryptoStatus::Unsupported, "curve25519 unavailable"); + (void)privateKey; + (void)peerPublic; + result.status = makeStatus(CryptoStatus::Unsupported, "curve25519 unavailable"); #endif - return result; -} - -CryptoResult> ESPCrypto::xchacha20Poly1305Encrypt(CryptoSpan key, - CryptoSpan nonce, - CryptoSpan aad, - CryptoSpan plaintext) { - CryptoResult> result; - (void)key; - (void)nonce; - (void)aad; - (void)plaintext; - result.status = makeStatus(CryptoStatus::Unsupported, "xchacha20poly1305 unavailable"); - return result; -} - -CryptoResult> ESPCrypto::xchacha20Poly1305Decrypt(CryptoSpan key, - CryptoSpan nonce, - CryptoSpan aad, - CryptoSpan ciphertextAndTag) { - CryptoResult> result; - (void)key; - (void)nonce; - (void)aad; - (void)ciphertextAndTag; - result.status = makeStatus(CryptoStatus::Unsupported, "xchacha20poly1305 unavailable"); - return result; -} - -CryptoResult> ESPCrypto::ed25519Sign(CryptoSpan privateKey, - CryptoSpan message) { - CryptoResult> result; - (void)privateKey; - (void)message; - result.status = makeStatus(CryptoStatus::Unsupported, "ed25519 unavailable"); - return result; -} - -CryptoResult ESPCrypto::ed25519Verify(CryptoSpan publicKey, - CryptoSpan message, - CryptoSpan signature) { - CryptoResult result; - (void)publicKey; - (void)message; - (void)signature; - result.status = makeStatus(CryptoStatus::Unsupported, "ed25519 unavailable"); - return result; + return result; +} + +CryptoResult> ESPCrypto::xchacha20Poly1305Encrypt( + CryptoSpan key, + CryptoSpan nonce, + CryptoSpan aad, + CryptoSpan plaintext +) { + CryptoResult> result; + (void)key; + (void)nonce; + (void)aad; + (void)plaintext; + result.status = makeStatus(CryptoStatus::Unsupported, "xchacha20poly1305 unavailable"); + return result; +} + +CryptoResult> ESPCrypto::xchacha20Poly1305Decrypt( + CryptoSpan key, + CryptoSpan nonce, + CryptoSpan aad, + CryptoSpan ciphertextAndTag +) { + CryptoResult> result; + (void)key; + (void)nonce; + (void)aad; + (void)ciphertextAndTag; + result.status = makeStatus(CryptoStatus::Unsupported, "xchacha20poly1305 unavailable"); + return result; +} + +CryptoResult> +ESPCrypto::ed25519Sign(CryptoSpan privateKey, CryptoSpan message) { + CryptoResult> result; + (void)privateKey; + (void)message; + result.status = makeStatus(CryptoStatus::Unsupported, "ed25519 unavailable"); + return result; +} + +CryptoResult ESPCrypto::ed25519Verify( + CryptoSpan publicKey, + CryptoSpan message, + CryptoSpan signature +) { + CryptoResult result; + (void)publicKey; + (void)message; + (void)signature; + result.status = makeStatus(CryptoStatus::Unsupported, "ed25519 unavailable"); + return result; } diff --git a/src/esp_crypto/esp_crypto.h b/src/esp_crypto/esp_crypto.h index 4710a51..2d3df17 100644 --- a/src/esp_crypto/esp_crypto.h +++ b/src/esp_crypto/esp_crypto.h @@ -3,18 +3,18 @@ #include #include -#include -#include #include -#include -#include +#include #include #include -#include +#include +#include +#include +#include -#include "mbedtls/md.h" #include "mbedtls/aes.h" #include "mbedtls/gcm.h" +#include "mbedtls/md.h" #include "mbedtls/pk.h" #if __has_include() @@ -32,620 +32,761 @@ #endif enum class CryptoStatus { - Ok, - InvalidInput, - RandomFailure, - Unsupported, - PolicyViolation, - BufferTooSmall, - VerifyFailed, - DecodeError, - JsonError, - Expired, - NotYetValid, - AudienceMismatch, - IssuerMismatch, - NonceReuse, - InternalError + Ok, + InvalidInput, + RandomFailure, + Unsupported, + PolicyViolation, + BufferTooSmall, + VerifyFailed, + DecodeError, + JsonError, + Expired, + NotYetValid, + AudienceMismatch, + IssuerMismatch, + NonceReuse, + InternalError }; const char *toString(CryptoStatus status); struct CryptoStatusDetail { - CryptoStatus code = CryptoStatus::Ok; - String message; + CryptoStatus code = CryptoStatus::Ok; + String message; - bool ok() const { return code == CryptoStatus::Ok; } + bool ok() const { + return code == CryptoStatus::Ok; + } }; -template -struct CryptoResult { - CryptoStatusDetail status; - T value; +template struct CryptoResult { + CryptoStatusDetail status; + T value; - bool ok() const { return status.ok(); } + bool ok() const { + return status.ok(); + } }; -template <> -struct CryptoResult { - CryptoStatusDetail status; - bool ok() const { return status.ok(); } +template <> struct CryptoResult { + CryptoStatusDetail status; + bool ok() const { + return status.ok(); + } }; -template -struct CryptoSpan { - using element_type = T; - using pointer = T *; - using const_pointer = const T *; +template struct CryptoSpan { + using element_type = T; + using pointer = T *; + using const_pointer = const T *; - CryptoSpan() : ptr(nullptr), len(0) {} - CryptoSpan(pointer data, size_t size) : ptr(data), len(size) {} - CryptoSpan(std::vector::type> &vec) : ptr(vec.data()), len(vec.size()) {} - CryptoSpan(const std::vector::type> &vec) : ptr(vec.data()), len(vec.size()) {} + CryptoSpan() : ptr(nullptr), len(0) { + } + CryptoSpan(pointer data, size_t size) : ptr(data), len(size) { + } + CryptoSpan(std::vector::type> &vec) + : ptr(vec.data()), len(vec.size()) { + } + CryptoSpan(const std::vector::type> &vec) + : ptr(vec.data()), len(vec.size()) { + } #if defined(__cpp_lib_array_constexpr) || __cpp_lib_array_constexpr >= 201803L - template ::value, int>::type = 0> - constexpr CryptoSpan(U (&arr)[N]) : ptr(arr), len(N) {} - template ::value, int>::type = 0> - constexpr CryptoSpan(const typename std::remove_const::type (&arr)[N]) : ptr(arr), len(N) {} + template < + size_t N, + typename U = T, + typename std::enable_if::value, int>::type = 0> + constexpr CryptoSpan(U (&arr)[N]) : ptr(arr), len(N) { + } + template < + size_t N, + typename U = T, + typename std::enable_if::value, int>::type = 0> + constexpr CryptoSpan(const typename std::remove_const::type (&arr)[N]) : ptr(arr), len(N) { + } #else - template ::value, int>::type = 0> - CryptoSpan(U (&arr)[N]) : ptr(arr), len(N) {} - template ::value, int>::type = 0> - CryptoSpan(const typename std::remove_const::type (&arr)[N]) : ptr(arr), len(N) {} + template < + size_t N, + typename U = T, + typename std::enable_if::value, int>::type = 0> + CryptoSpan(U (&arr)[N]) : ptr(arr), len(N) { + } + template < + size_t N, + typename U = T, + typename std::enable_if::value, int>::type = 0> + CryptoSpan(const typename std::remove_const::type (&arr)[N]) : ptr(arr), len(N) { + } #endif #if ESPCRYPTO_HAS_STD_SPAN #if defined(ESPCRYPTO_USE_EXPERIMENTAL_SPAN) - CryptoSpan(std::experimental::span span) : ptr(span.data()), len(span.size()) {} + CryptoSpan(std::experimental::span span) : ptr(span.data()), len(span.size()) { + } #else - CryptoSpan(std::span span) : ptr(span.data()), len(span.size()) {} + CryptoSpan(std::span span) : ptr(span.data()), len(span.size()) { + } #endif #endif - pointer data() const { return ptr; } - size_t size() const { return len; } - bool empty() const { return len == 0; } + pointer data() const { + return ptr; + } + size_t size() const { + return len; + } + bool empty() const { + return len == 0; + } - private: - pointer ptr; - size_t len; + private: + pointer ptr; + size_t len; }; -enum class KeyFormat { - Raw, - Pem, - Der, - Jwk -}; +enum class KeyFormat { Raw, Pem, Der, Jwk }; -enum class KeyKind { - Auto, - Public, - Private, - Symmetric -}; +enum class KeyKind { Auto, Public, Private, Symmetric }; struct KeyHandle { - String alias; - uint32_t version = 0; + String alias; + uint32_t version = 0; }; class CryptoKey { - public: - CryptoKey(); - CryptoKey(const CryptoKey &other); - CryptoKey &operator=(const CryptoKey &other); - CryptoKey(CryptoKey &&other) noexcept; - CryptoKey &operator=(CryptoKey &&other) noexcept; - ~CryptoKey(); - static CryptoKey fromPem(const std::string &pem, KeyKind kind = KeyKind::Auto); - static CryptoKey fromDer(const std::vector &der, KeyKind kind = KeyKind::Auto); - static CryptoKey fromRaw(const std::vector &raw, KeyKind kind = KeyKind::Symmetric); - - bool valid() const; - KeyKind kind() const; - CryptoSpan bytes() const; - void clear(); - bool parsed() const; - - private: - friend class ESPCrypto; - struct PkCache { - mbedtls_pk_context ctx; - bool hasKey = false; - bool isPrivate = false; - }; - CryptoStatusDetail ensureParsedPk(bool requirePrivate) const; - std::vector data; - KeyFormat format = KeyFormat::Raw; - KeyKind keyKind = KeyKind::Auto; - mutable PkCache *pk = nullptr; + public: + CryptoKey(); + CryptoKey(const CryptoKey &other); + CryptoKey &operator=(const CryptoKey &other); + CryptoKey(CryptoKey &&other) noexcept; + CryptoKey &operator=(CryptoKey &&other) noexcept; + ~CryptoKey(); + static CryptoKey fromPem(const std::string &pem, KeyKind kind = KeyKind::Auto); + static CryptoKey fromDer(const std::vector &der, KeyKind kind = KeyKind::Auto); + static CryptoKey fromRaw(const std::vector &raw, KeyKind kind = KeyKind::Symmetric); + + bool valid() const; + KeyKind kind() const; + CryptoSpan bytes() const; + void clear(); + bool parsed() const; + + private: + friend class ESPCrypto; + struct PkCache { + mbedtls_pk_context ctx; + bool hasKey = false; + bool isPrivate = false; + }; + CryptoStatusDetail ensureParsedPk(bool requirePrivate) const; + std::vector data; + KeyFormat format = KeyFormat::Raw; + KeyKind keyKind = KeyKind::Auto; + mutable PkCache *pk = nullptr; }; class KeyStore { - public: - virtual ~KeyStore() = default; - virtual CryptoResult> load(const KeyHandle &handle) = 0; - virtual CryptoStatusDetail store(const KeyHandle &handle, CryptoSpan key) = 0; - virtual CryptoStatusDetail remove(const KeyHandle &handle) = 0; + public: + virtual ~KeyStore() = default; + virtual CryptoResult> load(const KeyHandle &handle) = 0; + virtual CryptoStatusDetail store(const KeyHandle &handle, CryptoSpan key) = 0; + virtual CryptoStatusDetail remove(const KeyHandle &handle) = 0; }; class MemoryKeyStore : public KeyStore { - public: - CryptoResult> load(const KeyHandle &handle) override; - CryptoStatusDetail store(const KeyHandle &handle, CryptoSpan key) override; - CryptoStatusDetail remove(const KeyHandle &handle) override; + public: + CryptoResult> load(const KeyHandle &handle) override; + CryptoStatusDetail store(const KeyHandle &handle, CryptoSpan key) override; + CryptoStatusDetail remove(const KeyHandle &handle) override; - private: - std::map> storage; + private: + std::map> storage; }; class NvsKeyStore : public KeyStore { - public: - NvsKeyStore(String ns = "espcrypto", String partition = "nvs"); - CryptoResult> load(const KeyHandle &handle) override; - CryptoStatusDetail store(const KeyHandle &handle, CryptoSpan key) override; - CryptoStatusDetail remove(const KeyHandle &handle) override; - - private: - String ns; - String partition; - CryptoStatusDetail ensureInit() const; - String makeKeyName(const KeyHandle &handle) const; + public: + NvsKeyStore(String ns = "espcrypto", String partition = "nvs"); + CryptoResult> load(const KeyHandle &handle) override; + CryptoStatusDetail store(const KeyHandle &handle, CryptoSpan key) override; + CryptoStatusDetail remove(const KeyHandle &handle) override; + + private: + String ns; + String partition; + CryptoStatusDetail ensureInit() const; + String makeKeyName(const KeyHandle &handle) const; }; class LittleFsKeyStore : public KeyStore { - public: - LittleFsKeyStore(String basePath = "/keys"); - CryptoResult> load(const KeyHandle &handle) override; - CryptoStatusDetail store(const KeyHandle &handle, CryptoSpan key) override; - CryptoStatusDetail remove(const KeyHandle &handle) override; - - private: - String basePath; - String makePath(const KeyHandle &handle) const; + public: + LittleFsKeyStore(String basePath = "/keys"); + CryptoResult> load(const KeyHandle &handle) override; + CryptoStatusDetail store(const KeyHandle &handle, CryptoSpan key) override; + CryptoStatusDetail remove(const KeyHandle &handle) override; + + private: + String basePath; + String makePath(const KeyHandle &handle) const; }; struct DeviceKeyOptions { - bool persistSeed = true; - String nvsNamespace = "espcrypto"; - String nvsPartition = "nvs"; - size_t seedBytes = 32; + bool persistSeed = true; + String nvsNamespace = "espcrypto"; + String nvsPartition = "nvs"; + size_t seedBytes = 32; }; -enum class ShaVariant { - SHA256, - SHA384, - SHA512 -}; +enum class ShaVariant { SHA256, SHA384, SHA512 }; struct ShaOptions { - ShaVariant variant = ShaVariant::SHA256; - bool preferHardware = true; + ShaVariant variant = ShaVariant::SHA256; + bool preferHardware = true; }; -enum class GcmNonceStrategy { - Random96, - Counter64_Random32, - BootCounter_Random32 -}; +enum class GcmNonceStrategy { Random96, Counter64_Random32, BootCounter_Random32 }; struct GcmNonceOptions { - GcmNonceStrategy strategy = GcmNonceStrategy::Random96; - bool persistCounter = false; - String nvsNamespace = "espcrypto"; - String nvsPartition = "nvs"; + GcmNonceStrategy strategy = GcmNonceStrategy::Random96; + bool persistCounter = false; + String nvsNamespace = "espcrypto"; + String nvsPartition = "nvs"; }; class ShaCtx { - public: - ShaCtx(); - ~ShaCtx(); - CryptoStatusDetail begin(ShaVariant variant, bool preferHardware = true); - CryptoStatusDetail update(CryptoSpan data); - CryptoStatusDetail finish(CryptoSpan out); - - private: - const mbedtls_md_info_t *info = nullptr; - mbedtls_md_context_t ctx; - bool started = false; + public: + ShaCtx(); + ~ShaCtx(); + CryptoStatusDetail begin(ShaVariant variant, bool preferHardware = true); + CryptoStatusDetail update(CryptoSpan data); + CryptoStatusDetail finish(CryptoSpan out); + + private: + const mbedtls_md_info_t *info = nullptr; + mbedtls_md_context_t ctx; + bool started = false; }; class HmacCtx { - public: - HmacCtx(); - ~HmacCtx(); - CryptoStatusDetail begin(ShaVariant variant, CryptoSpan key); - CryptoStatusDetail update(CryptoSpan data); - CryptoStatusDetail finish(CryptoSpan out); - - private: - const mbedtls_md_info_t *info = nullptr; - mbedtls_md_context_t ctx; - bool started = false; + public: + HmacCtx(); + ~HmacCtx(); + CryptoStatusDetail begin(ShaVariant variant, CryptoSpan key); + CryptoStatusDetail update(CryptoSpan data); + CryptoStatusDetail finish(CryptoSpan out); + + private: + const mbedtls_md_info_t *info = nullptr; + mbedtls_md_context_t ctx; + bool started = false; }; class AesCtrStream { - public: - AesCtrStream(); - ~AesCtrStream(); - CryptoStatusDetail begin(const std::vector &key, CryptoSpan nonceCounter); - CryptoStatusDetail update(CryptoSpan input, CryptoSpan output); - - private: - mbedtls_aes_context ctx; - unsigned char counter[16]; - unsigned char streamBlock[16]; - size_t offset = 0; - bool started = false; + public: + AesCtrStream(); + ~AesCtrStream(); + CryptoStatusDetail + begin(const std::vector &key, CryptoSpan nonceCounter); + CryptoStatusDetail update(CryptoSpan input, CryptoSpan output); + + private: + mbedtls_aes_context ctx; + unsigned char counter[16]; + unsigned char streamBlock[16]; + size_t offset = 0; + bool started = false; }; class AesGcmCtx { - public: - AesGcmCtx(); - ~AesGcmCtx(); - CryptoStatusDetail beginEncrypt(const std::vector &key, - CryptoSpan iv, - CryptoSpan aad); - CryptoStatusDetail beginDecrypt(const std::vector &key, - CryptoSpan iv, - CryptoSpan aad, - CryptoSpan tag); - CryptoStatusDetail update(CryptoSpan input, CryptoSpan output); - CryptoStatusDetail finish(CryptoSpan tagOut); - - private: - bool decrypt = false; - bool started = false; - CryptoStatusDetail beginCommon(const std::vector &key, - CryptoSpan iv, - CryptoSpan aad, - bool decryptMode, - CryptoSpan tag); - mbedtls_gcm_context ctx; - std::vector tagVerify; + public: + AesGcmCtx(); + ~AesGcmCtx(); + CryptoStatusDetail beginEncrypt( + const std::vector &key, CryptoSpan iv, CryptoSpan aad + ); + CryptoStatusDetail beginDecrypt( + const std::vector &key, + CryptoSpan iv, + CryptoSpan aad, + CryptoSpan tag + ); + CryptoStatusDetail update(CryptoSpan input, CryptoSpan output); + CryptoStatusDetail finish(CryptoSpan tagOut); + + private: + bool decrypt = false; + bool started = false; + CryptoStatusDetail beginCommon( + const std::vector &key, + CryptoSpan iv, + CryptoSpan aad, + bool decryptMode, + CryptoSpan tag + ); + mbedtls_gcm_context ctx; + std::vector tagVerify; }; class SecureBuffer { - public: - SecureBuffer() = default; - explicit SecureBuffer(size_t bytes); - SecureBuffer(SecureBuffer &&other) noexcept; - SecureBuffer &operator=(SecureBuffer &&other) noexcept; - SecureBuffer(const SecureBuffer &) = delete; - SecureBuffer &operator=(const SecureBuffer &) = delete; - ~SecureBuffer(); - - uint8_t *data() { return buffer.data(); } - const uint8_t *data() const { return buffer.data(); } - size_t size() const { return buffer.size(); } - void resize(size_t bytes); - std::vector &raw() { return buffer; } - const std::vector &raw() const { return buffer; } - - private: - void wipe(); - std::vector buffer; + public: + SecureBuffer() = default; + explicit SecureBuffer(size_t bytes); + SecureBuffer(SecureBuffer &&other) noexcept; + SecureBuffer &operator=(SecureBuffer &&other) noexcept; + SecureBuffer(const SecureBuffer &) = delete; + SecureBuffer &operator=(const SecureBuffer &) = delete; + ~SecureBuffer(); + + uint8_t *data() { + return buffer.data(); + } + const uint8_t *data() const { + return buffer.data(); + } + size_t size() const { + return buffer.size(); + } + void resize(size_t bytes); + std::vector &raw() { + return buffer; + } + const std::vector &raw() const { + return buffer; + } + + private: + void wipe(); + std::vector buffer; }; class SecureString { - public: - SecureString() = default; - explicit SecureString(std::string value); - SecureString(SecureString &&other) noexcept; - SecureString &operator=(SecureString &&other) noexcept; - SecureString(const SecureString &) = delete; - SecureString &operator=(const SecureString &) = delete; - ~SecureString(); - - const std::string &get() const { return value; } - std::string &get() { return value; } - const char *c_str() const { return value.c_str(); } - size_t size() const { return value.size(); } - bool empty() const { return value.empty(); } - - private: - void wipe(); - std::string value; -}; - -enum class JwtAlgorithm { - Auto, - HS256, - RS256, - ES256 -}; + public: + SecureString() = default; + explicit SecureString(std::string value); + SecureString(SecureString &&other) noexcept; + SecureString &operator=(SecureString &&other) noexcept; + SecureString(const SecureString &) = delete; + SecureString &operator=(const SecureString &) = delete; + ~SecureString(); + + const std::string &get() const { + return value; + } + std::string &get() { + return value; + } + const char *c_str() const { + return value.c_str(); + } + size_t size() const { + return value.size(); + } + bool empty() const { + return value.empty(); + } + + private: + void wipe(); + std::string value; +}; + +enum class JwtAlgorithm { Auto, HS256, RS256, ES256 }; struct JwtSignOptions { - JwtAlgorithm algorithm = JwtAlgorithm::HS256; - String keyId; - String issuer; - String subject; - String audience; - uint32_t expiresInSeconds = 3600; - uint32_t notBefore = 0; - uint32_t issuedAt = 0; - uint32_t currentTimestamp = 0; + JwtAlgorithm algorithm = JwtAlgorithm::HS256; + String keyId; + String issuer; + String subject; + String audience; + uint32_t expiresInSeconds = 3600; + uint32_t notBefore = 0; + uint32_t issuedAt = 0; + uint32_t currentTimestamp = 0; }; struct JwtVerifyOptions { - JwtAlgorithm algorithm = JwtAlgorithm::Auto; - String audience; - String issuer; - uint32_t currentTimestamp = 0; - bool requireExpiration = true; - uint32_t leewaySeconds = 0; - String expectedTyp; - std::vector audiences; - std::vector criticalHeadersAllowed; + JwtAlgorithm algorithm = JwtAlgorithm::Auto; + String audience; + String issuer; + uint32_t currentTimestamp = 0; + bool requireExpiration = true; + uint32_t leewaySeconds = 0; + String expectedTyp; + std::vector audiences; + std::vector criticalHeadersAllowed; }; struct PasswordHashOptions { - uint8_t cost = 10; // Similar to bcrypt cost factor - size_t saltBytes = 16; - size_t outputBytes = 32; + uint8_t cost = 10; // Similar to bcrypt cost factor + size_t saltBytes = 16; + size_t outputBytes = 32; }; struct CryptoPolicy { - size_t minRsaBits = 2048; - uint32_t minPbkdf2Iterations = 1024; - bool allowLegacy = false; - bool allowWeakCurves = false; - uint8_t minAesGcmIvBytes = 12; + size_t minRsaBits = 2048; + uint32_t minPbkdf2Iterations = 1024; + bool allowLegacy = false; + bool allowWeakCurves = false; + uint8_t minAesGcmIvBytes = 12; }; struct CryptoCaps { - bool shaAccel = false; - bool aesAccel = false; - bool aesGcmAccel = false; + bool shaAccel = false; + bool aesAccel = false; + bool aesGcmAccel = false; }; struct GcmMessage { - std::vector iv; - std::vector ciphertext; - std::vector tag; + std::vector iv; + std::vector ciphertext; + std::vector tag; }; class ESPCrypto { - public: - static void deinit(); - static bool isInitialized(); - - static void setPolicy(const CryptoPolicy &policy); - static CryptoPolicy policy(); - static CryptoCaps caps(); - static bool constantTimeEq(const std::vector &a, const std::vector &b); - static bool constantTimeEq(CryptoSpan a, CryptoSpan b); - - static std::vector sha(const uint8_t *data, size_t length, const ShaOptions &options = ShaOptions{}); - static std::vector sha(const std::vector &data, const ShaOptions &options = ShaOptions{}); - static CryptoResult> shaResult(CryptoSpan data, const ShaOptions &options = ShaOptions{}); - static CryptoResult sha(CryptoSpan data, CryptoSpan out, const ShaOptions &options = ShaOptions{}); - - static String shaHex(const uint8_t *data, size_t length, const ShaOptions &options = ShaOptions{}); - static String shaHex(const String &text, const ShaOptions &options = ShaOptions{}); - - static bool aesGcmEncrypt(const std::vector &key, - const std::vector &iv, - const std::vector &plaintext, - std::vector &ciphertext, - std::vector &tag, - const std::vector &aad = {}); - static bool aesGcmDecrypt(const std::vector &key, - const std::vector &iv, - const std::vector &ciphertext, - const std::vector &tag, - std::vector &plaintext, - const std::vector &aad = {}); - static bool aesCtrCrypt(const std::vector &key, - const std::vector &nonceCounter, - const std::vector &input, - std::vector &output); - - static CryptoResult aesGcmEncryptAuto(const std::vector &key, - const std::vector &plaintext, - const std::vector &aad = {}, - size_t ivLength = 12, - const GcmNonceOptions &nonceOptions = GcmNonceOptions{}); - static CryptoResult> aesGcmDecrypt(const std::vector &key, - const std::vector &iv, - const std::vector &ciphertext, - const std::vector &tag, - const std::vector &aad = {}); - static CryptoResult aesGcmEncrypt(const std::vector &key, - CryptoSpan iv, - CryptoSpan plaintext, - CryptoSpan ciphertextOut, - CryptoSpan tagOut, - CryptoSpan aad = {}); - static CryptoResult aesGcmDecrypt(const std::vector &key, - CryptoSpan iv, - CryptoSpan ciphertext, - CryptoSpan tag, - CryptoSpan plaintextOut, - CryptoSpan aad = {}); - static CryptoResult> aesCtrCrypt(const std::vector &key, - const std::vector &nonceCounter, - const std::vector &input); - - static bool rsaSign(const std::string &privateKeyPem, - const uint8_t *data, - size_t length, - ShaVariant variant, - std::vector &signature); - static bool rsaSign(const String &privateKeyPem, - const uint8_t *data, - size_t length, - ShaVariant variant, - std::vector &signature) { - return rsaSign(std::string(privateKeyPem.c_str(), privateKeyPem.length()), data, length, variant, signature); - } - static bool rsaVerify(const std::string &publicKeyPem, - const uint8_t *data, - size_t length, - const std::vector &signature, - ShaVariant variant); - static bool rsaVerify(const String &publicKeyPem, - const uint8_t *data, - size_t length, - const std::vector &signature, - ShaVariant variant) { - return rsaVerify(std::string(publicKeyPem.c_str(), publicKeyPem.length()), data, length, signature, variant); - } - - static CryptoResult> rsaSign(const std::string &privateKeyPem, - CryptoSpan data, - ShaVariant variant); - static CryptoResult rsaVerify(const std::string &publicKeyPem, - CryptoSpan data, - CryptoSpan signature, - ShaVariant variant); - static CryptoResult> rsaSign(const CryptoKey &privateKey, - CryptoSpan data, - ShaVariant variant); - static CryptoResult rsaVerify(const CryptoKey &publicKey, - CryptoSpan data, - CryptoSpan signature, - ShaVariant variant); - - static bool eccSign(const std::string &privateKeyPem, - const uint8_t *data, - size_t length, - ShaVariant variant, - std::vector &signature); - static bool eccSign(const String &privateKeyPem, - const uint8_t *data, - size_t length, - ShaVariant variant, - std::vector &signature) { - return eccSign(std::string(privateKeyPem.c_str(), privateKeyPem.length()), data, length, variant, signature); - } - static bool eccVerify(const std::string &publicKeyPem, - const uint8_t *data, - size_t length, - const std::vector &signature, - ShaVariant variant); - static bool eccVerify(const String &publicKeyPem, - const uint8_t *data, - size_t length, - const std::vector &signature, - ShaVariant variant) { - return eccVerify(std::string(publicKeyPem.c_str(), publicKeyPem.length()), data, length, signature, variant); - } - - static CryptoResult> eccSign(const std::string &privateKeyPem, - CryptoSpan data, - ShaVariant variant); - static CryptoResult eccVerify(const std::string &publicKeyPem, - CryptoSpan data, - CryptoSpan signature, - ShaVariant variant); - static CryptoResult> eccSign(const CryptoKey &privateKey, - CryptoSpan data, - ShaVariant variant); - static CryptoResult eccVerify(const CryptoKey &publicKey, - CryptoSpan data, - CryptoSpan signature, - ShaVariant variant); - - static String createJwt(const JsonDocument &claims, - const std::string &key, - const JwtSignOptions &options = JwtSignOptions{}); - static String createJwt(const JsonDocument &claims, - const String &key, - const JwtSignOptions &options = JwtSignOptions{}) { - return createJwt(claims, std::string(key.c_str(), key.length()), options); - } - static String createJwt(const JsonDocument &claims, - const char *key, - const JwtSignOptions &options = JwtSignOptions{}) { - return createJwt(claims, key ? std::string(key) : std::string(), options); - } - - static bool verifyJwt(const String &token, - const std::string &key, - JsonDocument &outClaims, - String &error, - const JwtVerifyOptions &options = JwtVerifyOptions{}); - static bool verifyJwt(const String &token, - const String &key, - JsonDocument &outClaims, - String &error, - const JwtVerifyOptions &options = JwtVerifyOptions{}) { - return verifyJwt(token, std::string(key.c_str(), key.length()), outClaims, error, options); - } - static bool verifyJwt(const String &token, - const char *key, - JsonDocument &outClaims, - String &error, - const JwtVerifyOptions &options = JwtVerifyOptions{}) { - return verifyJwt(token, key ? std::string(key) : std::string(), outClaims, error, options); - } - static CryptoResult createJwtResult(const JsonDocument &claims, - const std::string &key, - const JwtSignOptions &options = JwtSignOptions{}); - static CryptoResult verifyJwtResult(const String &token, - const std::string &key, - JsonDocument &outClaims, - const JwtVerifyOptions &options = JwtVerifyOptions{}); - static CryptoResult verifyJwtWithJwks(const String &token, - const JsonDocument &jwks, - JsonDocument &outClaims, - const JwtVerifyOptions &options = JwtVerifyOptions{}); - - static String hashString(const String &input, const PasswordHashOptions &options = PasswordHashOptions{}); - static bool verifyString(const String &input, const String &encoded); - - static CryptoResult hashStringResult(const String &input, const PasswordHashOptions &options = PasswordHashOptions{}); - static CryptoResult verifyStringResult(const String &input, const String &encoded); - - static CryptoResult> hmac(ShaVariant variant, - CryptoSpan key, - CryptoSpan data); - static CryptoResult> hkdf(ShaVariant variant, - CryptoSpan salt, - CryptoSpan ikm, - CryptoSpan info, - size_t length); - static CryptoResult> pbkdf2(const String &password, - CryptoSpan salt, - uint32_t iterations, - size_t outputLength); - - static CryptoResult> deriveDeviceKey(const String &purpose, - CryptoSpan contextInfo = {}, - size_t length = 32, - const DeviceKeyOptions &options = DeviceKeyOptions{}); - - static CryptoResult storeKey(KeyStore &store, const KeyHandle &handle, CryptoSpan keyMaterial); - static CryptoResult loadKey(KeyStore &store, const KeyHandle &handle, KeyFormat format, KeyKind kind = KeyKind::Auto); - static CryptoResult removeKey(KeyStore &store, const KeyHandle &handle); - - static CryptoResult> ecdsaDerToRaw(CryptoSpan der); - static CryptoResult> ecdsaRawToDer(CryptoSpan raw); - - static CryptoResult> chacha20Poly1305Encrypt(CryptoSpan key, - CryptoSpan nonce, - CryptoSpan aad, - CryptoSpan plaintext); - static CryptoResult> chacha20Poly1305Decrypt(CryptoSpan key, - CryptoSpan nonce, - CryptoSpan aad, - CryptoSpan ciphertextAndTag); - static CryptoResult> xchacha20Poly1305Encrypt(CryptoSpan key, - CryptoSpan nonce, - CryptoSpan aad, - CryptoSpan plaintext); - static CryptoResult> xchacha20Poly1305Decrypt(CryptoSpan key, - CryptoSpan nonce, - CryptoSpan aad, - CryptoSpan ciphertextAndTag); - - static CryptoResult> x25519(CryptoSpan privateKey, - CryptoSpan peerPublic); - - static CryptoResult> ed25519Sign(CryptoSpan privateKey, - CryptoSpan message); - static CryptoResult ed25519Verify(CryptoSpan publicKey, - CryptoSpan message, - CryptoSpan signature); + public: + static void deinit(); + static bool isInitialized(); + + static void setPolicy(const CryptoPolicy &policy); + static CryptoPolicy policy(); + static CryptoCaps caps(); + static bool constantTimeEq(const std::vector &a, const std::vector &b); + static bool constantTimeEq(CryptoSpan a, CryptoSpan b); + + static std::vector + sha(const uint8_t *data, size_t length, const ShaOptions &options = ShaOptions{}); + static std::vector + sha(const std::vector &data, const ShaOptions &options = ShaOptions{}); + static CryptoResult> + shaResult(CryptoSpan data, const ShaOptions &options = ShaOptions{}); + static CryptoResult + sha(CryptoSpan data, + CryptoSpan out, + const ShaOptions &options = ShaOptions{}); + + static String + shaHex(const uint8_t *data, size_t length, const ShaOptions &options = ShaOptions{}); + static String shaHex(const String &text, const ShaOptions &options = ShaOptions{}); + + static bool aesGcmEncrypt( + const std::vector &key, + const std::vector &iv, + const std::vector &plaintext, + std::vector &ciphertext, + std::vector &tag, + const std::vector &aad = {} + ); + static bool aesGcmDecrypt( + const std::vector &key, + const std::vector &iv, + const std::vector &ciphertext, + const std::vector &tag, + std::vector &plaintext, + const std::vector &aad = {} + ); + static bool aesCtrCrypt( + const std::vector &key, + const std::vector &nonceCounter, + const std::vector &input, + std::vector &output + ); + + static CryptoResult aesGcmEncryptAuto( + const std::vector &key, + const std::vector &plaintext, + const std::vector &aad = {}, + size_t ivLength = 12, + const GcmNonceOptions &nonceOptions = GcmNonceOptions{} + ); + static CryptoResult> aesGcmDecrypt( + const std::vector &key, + const std::vector &iv, + const std::vector &ciphertext, + const std::vector &tag, + const std::vector &aad = {} + ); + static CryptoResult aesGcmEncrypt( + const std::vector &key, + CryptoSpan iv, + CryptoSpan plaintext, + CryptoSpan ciphertextOut, + CryptoSpan tagOut, + CryptoSpan aad = {} + ); + static CryptoResult aesGcmDecrypt( + const std::vector &key, + CryptoSpan iv, + CryptoSpan ciphertext, + CryptoSpan tag, + CryptoSpan plaintextOut, + CryptoSpan aad = {} + ); + static CryptoResult> aesCtrCrypt( + const std::vector &key, + const std::vector &nonceCounter, + const std::vector &input + ); + + static bool rsaSign( + const std::string &privateKeyPem, + const uint8_t *data, + size_t length, + ShaVariant variant, + std::vector &signature + ); + static bool rsaSign( + const String &privateKeyPem, + const uint8_t *data, + size_t length, + ShaVariant variant, + std::vector &signature + ) { + return rsaSign( + std::string(privateKeyPem.c_str(), privateKeyPem.length()), + data, + length, + variant, + signature + ); + } + static bool rsaVerify( + const std::string &publicKeyPem, + const uint8_t *data, + size_t length, + const std::vector &signature, + ShaVariant variant + ); + static bool rsaVerify( + const String &publicKeyPem, + const uint8_t *data, + size_t length, + const std::vector &signature, + ShaVariant variant + ) { + return rsaVerify( + std::string(publicKeyPem.c_str(), publicKeyPem.length()), + data, + length, + signature, + variant + ); + } + + static CryptoResult> + rsaSign(const std::string &privateKeyPem, CryptoSpan data, ShaVariant variant); + static CryptoResult rsaVerify( + const std::string &publicKeyPem, + CryptoSpan data, + CryptoSpan signature, + ShaVariant variant + ); + static CryptoResult> + rsaSign(const CryptoKey &privateKey, CryptoSpan data, ShaVariant variant); + static CryptoResult rsaVerify( + const CryptoKey &publicKey, + CryptoSpan data, + CryptoSpan signature, + ShaVariant variant + ); + + static bool eccSign( + const std::string &privateKeyPem, + const uint8_t *data, + size_t length, + ShaVariant variant, + std::vector &signature + ); + static bool eccSign( + const String &privateKeyPem, + const uint8_t *data, + size_t length, + ShaVariant variant, + std::vector &signature + ) { + return eccSign( + std::string(privateKeyPem.c_str(), privateKeyPem.length()), + data, + length, + variant, + signature + ); + } + static bool eccVerify( + const std::string &publicKeyPem, + const uint8_t *data, + size_t length, + const std::vector &signature, + ShaVariant variant + ); + static bool eccVerify( + const String &publicKeyPem, + const uint8_t *data, + size_t length, + const std::vector &signature, + ShaVariant variant + ) { + return eccVerify( + std::string(publicKeyPem.c_str(), publicKeyPem.length()), + data, + length, + signature, + variant + ); + } + + static CryptoResult> + eccSign(const std::string &privateKeyPem, CryptoSpan data, ShaVariant variant); + static CryptoResult eccVerify( + const std::string &publicKeyPem, + CryptoSpan data, + CryptoSpan signature, + ShaVariant variant + ); + static CryptoResult> + eccSign(const CryptoKey &privateKey, CryptoSpan data, ShaVariant variant); + static CryptoResult eccVerify( + const CryptoKey &publicKey, + CryptoSpan data, + CryptoSpan signature, + ShaVariant variant + ); + + static String createJwt( + const JsonDocument &claims, + const std::string &key, + const JwtSignOptions &options = JwtSignOptions{} + ); + static String createJwt( + const JsonDocument &claims, + const String &key, + const JwtSignOptions &options = JwtSignOptions{} + ) { + return createJwt(claims, std::string(key.c_str(), key.length()), options); + } + static String createJwt( + const JsonDocument &claims, + const char *key, + const JwtSignOptions &options = JwtSignOptions{} + ) { + return createJwt(claims, key ? std::string(key) : std::string(), options); + } + + static bool verifyJwt( + const String &token, + const std::string &key, + JsonDocument &outClaims, + String &error, + const JwtVerifyOptions &options = JwtVerifyOptions{} + ); + static bool verifyJwt( + const String &token, + const String &key, + JsonDocument &outClaims, + String &error, + const JwtVerifyOptions &options = JwtVerifyOptions{} + ) { + return verifyJwt(token, std::string(key.c_str(), key.length()), outClaims, error, options); + } + static bool verifyJwt( + const String &token, + const char *key, + JsonDocument &outClaims, + String &error, + const JwtVerifyOptions &options = JwtVerifyOptions{} + ) { + return verifyJwt(token, key ? std::string(key) : std::string(), outClaims, error, options); + } + static CryptoResult createJwtResult( + const JsonDocument &claims, + const std::string &key, + const JwtSignOptions &options = JwtSignOptions{} + ); + static CryptoResult verifyJwtResult( + const String &token, + const std::string &key, + JsonDocument &outClaims, + const JwtVerifyOptions &options = JwtVerifyOptions{} + ); + static CryptoResult verifyJwtWithJwks( + const String &token, + const JsonDocument &jwks, + JsonDocument &outClaims, + const JwtVerifyOptions &options = JwtVerifyOptions{} + ); + + static String + hashString(const String &input, const PasswordHashOptions &options = PasswordHashOptions{}); + static bool verifyString(const String &input, const String &encoded); + + static CryptoResult hashStringResult( + const String &input, const PasswordHashOptions &options = PasswordHashOptions{} + ); + static CryptoResult verifyStringResult(const String &input, const String &encoded); + + static CryptoResult> + hmac(ShaVariant variant, CryptoSpan key, CryptoSpan data); + static CryptoResult> hkdf( + ShaVariant variant, + CryptoSpan salt, + CryptoSpan ikm, + CryptoSpan info, + size_t length + ); + static CryptoResult> pbkdf2( + const String &password, + CryptoSpan salt, + uint32_t iterations, + size_t outputLength + ); + + static CryptoResult> deriveDeviceKey( + const String &purpose, + CryptoSpan contextInfo = {}, + size_t length = 32, + const DeviceKeyOptions &options = DeviceKeyOptions{} + ); + + static CryptoResult + storeKey(KeyStore &store, const KeyHandle &handle, CryptoSpan keyMaterial); + static CryptoResult loadKey( + KeyStore &store, const KeyHandle &handle, KeyFormat format, KeyKind kind = KeyKind::Auto + ); + static CryptoResult removeKey(KeyStore &store, const KeyHandle &handle); + + static CryptoResult> ecdsaDerToRaw(CryptoSpan der); + static CryptoResult> ecdsaRawToDer(CryptoSpan raw); + + static CryptoResult> chacha20Poly1305Encrypt( + CryptoSpan key, + CryptoSpan nonce, + CryptoSpan aad, + CryptoSpan plaintext + ); + static CryptoResult> chacha20Poly1305Decrypt( + CryptoSpan key, + CryptoSpan nonce, + CryptoSpan aad, + CryptoSpan ciphertextAndTag + ); + static CryptoResult> xchacha20Poly1305Encrypt( + CryptoSpan key, + CryptoSpan nonce, + CryptoSpan aad, + CryptoSpan plaintext + ); + static CryptoResult> xchacha20Poly1305Decrypt( + CryptoSpan key, + CryptoSpan nonce, + CryptoSpan aad, + CryptoSpan ciphertextAndTag + ); + + static CryptoResult> + x25519(CryptoSpan privateKey, CryptoSpan peerPublic); + + static CryptoResult> + ed25519Sign(CryptoSpan privateKey, CryptoSpan message); + static CryptoResult ed25519Verify( + CryptoSpan publicKey, + CryptoSpan message, + CryptoSpan signature + ); }; diff --git a/test/test_esp_crypto/test_esp_crypto.cpp b/test/test_esp_crypto/test_esp_crypto.cpp index f56e9b1..a0608fa 100644 --- a/test/test_esp_crypto/test_esp_crypto.cpp +++ b/test/test_esp_crypto/test_esp_crypto.cpp @@ -1,379 +1,509 @@ #include #include -#include #include +#include #include void test_teardown_preinit_and_idempotent() { - ESPCrypto::deinit(); - TEST_ASSERT_FALSE(ESPCrypto::isInitialized()); + ESPCrypto::deinit(); + TEST_ASSERT_FALSE(ESPCrypto::isInitialized()); - 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()); + 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)); - TEST_ASSERT_EQUAL_STRING("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", - digest.c_str()); + const char *data = "hello world"; + String digest = ESPCrypto::shaHex(reinterpret_cast(data), strlen(data)); + TEST_ASSERT_EQUAL_STRING( + "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", + digest.c_str() + ); } void test_password_hash_roundtrip() { - String hashed = ESPCrypto::hashString("hunter2"); - TEST_ASSERT_TRUE(hashed.length() > 0); - TEST_ASSERT_TRUE(ESPCrypto::verifyString("hunter2", hashed)); - TEST_ASSERT_FALSE(ESPCrypto::verifyString("badpass", hashed)); + String hashed = ESPCrypto::hashString("hunter2"); + TEST_ASSERT_TRUE(hashed.length() > 0); + TEST_ASSERT_TRUE(ESPCrypto::verifyString("hunter2", hashed)); + TEST_ASSERT_FALSE(ESPCrypto::verifyString("badpass", hashed)); } void test_sha_known_vectors() { - ShaOptions opts; - opts.variant = ShaVariant::SHA256; - String sha256 = ESPCrypto::shaHex(reinterpret_cast("abc"), 3, opts); - TEST_ASSERT_EQUAL_STRING("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", - sha256.c_str()); - opts.variant = ShaVariant::SHA384; - String sha384 = ESPCrypto::shaHex(reinterpret_cast("abc"), 3, opts); - TEST_ASSERT_EQUAL_STRING("cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7", - sha384.c_str()); - opts.variant = ShaVariant::SHA512; - String sha512 = ESPCrypto::shaHex(reinterpret_cast("abc"), 3, opts); - TEST_ASSERT_EQUAL_STRING("ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f", - sha512.c_str()); + ShaOptions opts; + opts.variant = ShaVariant::SHA256; + String sha256 = ESPCrypto::shaHex(reinterpret_cast("abc"), 3, opts); + TEST_ASSERT_EQUAL_STRING( + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", + sha256.c_str() + ); + opts.variant = ShaVariant::SHA384; + String sha384 = ESPCrypto::shaHex(reinterpret_cast("abc"), 3, opts); + TEST_ASSERT_EQUAL_STRING( + "cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134" + "c825a7", + sha384.c_str() + ); + opts.variant = ShaVariant::SHA512; + String sha512 = ESPCrypto::shaHex(reinterpret_cast("abc"), 3, opts); + TEST_ASSERT_EQUAL_STRING( + "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3" + "feebbd454d4423643ce80e2a9ac94fa54ca49f", + sha512.c_str() + ); } void test_aes_gcm_known_vector() { - std::vector key(16, 0x00); - std::vector iv = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - std::vector plaintext(16, 0x00); - std::vector ciphertext; - std::vector tag; - TEST_ASSERT_TRUE(ESPCrypto::aesGcmEncrypt(key, iv, plaintext, ciphertext, tag)); - const uint8_t expectedCipher[] = {0x03, 0x88, 0xda, 0xce, 0x60, 0xb6, 0xa3, 0x92, - 0xf3, 0x28, 0xc2, 0xb9, 0x71, 0xb2, 0xfe, 0x78}; - const uint8_t expectedTag[] = {0xab, 0x6e, 0x47, 0xd4, 0x2c, 0xec, 0x13, 0xbd, - 0xf5, 0x3a, 0x67, 0xb2, 0x12, 0x57, 0xbd, 0xdf}; - TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(ciphertext, std::vector(expectedCipher, expectedCipher + sizeof(expectedCipher)))); - TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(tag, std::vector(expectedTag, expectedTag + sizeof(expectedTag)))); - - auto decrypted = ESPCrypto::aesGcmDecrypt(key, iv, ciphertext, tag); - TEST_ASSERT_TRUE_MESSAGE(decrypted.ok(), decrypted.status.message.c_str()); - TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(plaintext, decrypted.value)); + std::vector key(16, 0x00); + std::vector iv = + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + std::vector plaintext(16, 0x00); + std::vector ciphertext; + std::vector tag; + TEST_ASSERT_TRUE(ESPCrypto::aesGcmEncrypt(key, iv, plaintext, ciphertext, tag)); + const uint8_t expectedCipher[] = { + 0x03, + 0x88, + 0xda, + 0xce, + 0x60, + 0xb6, + 0xa3, + 0x92, + 0xf3, + 0x28, + 0xc2, + 0xb9, + 0x71, + 0xb2, + 0xfe, + 0x78 + }; + const uint8_t expectedTag[] = { + 0xab, + 0x6e, + 0x47, + 0xd4, + 0x2c, + 0xec, + 0x13, + 0xbd, + 0xf5, + 0x3a, + 0x67, + 0xb2, + 0x12, + 0x57, + 0xbd, + 0xdf + }; + TEST_ASSERT_TRUE( + ESPCrypto::constantTimeEq( + ciphertext, + std::vector(expectedCipher, expectedCipher + sizeof(expectedCipher)) + ) + ); + TEST_ASSERT_TRUE( + ESPCrypto::constantTimeEq( + tag, + std::vector(expectedTag, expectedTag + sizeof(expectedTag)) + ) + ); + + auto decrypted = ESPCrypto::aesGcmDecrypt(key, iv, ciphertext, tag); + TEST_ASSERT_TRUE_MESSAGE(decrypted.ok(), decrypted.status.message.c_str()); + TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(plaintext, decrypted.value)); } void test_aes_gcm_auto_iv_roundtrip() { - std::vector key(16, 0x01); - std::vector plaintext = {0x01, 0x02, 0x03, 0x04, 0x05}; - auto enc = ESPCrypto::aesGcmEncryptAuto(key, plaintext); - TEST_ASSERT_TRUE_MESSAGE(enc.ok(), enc.status.message.c_str()); - TEST_ASSERT_EQUAL_UINT32(12, enc.value.iv.size()); - auto dec = ESPCrypto::aesGcmDecrypt(key, enc.value.iv, enc.value.ciphertext, enc.value.tag); - TEST_ASSERT_TRUE_MESSAGE(dec.ok(), dec.status.message.c_str()); - TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(plaintext, dec.value)); + std::vector key(16, 0x01); + std::vector plaintext = {0x01, 0x02, 0x03, 0x04, 0x05}; + auto enc = ESPCrypto::aesGcmEncryptAuto(key, plaintext); + TEST_ASSERT_TRUE_MESSAGE(enc.ok(), enc.status.message.c_str()); + TEST_ASSERT_EQUAL_UINT32(12, enc.value.iv.size()); + auto dec = ESPCrypto::aesGcmDecrypt(key, enc.value.iv, enc.value.ciphertext, enc.value.tag); + TEST_ASSERT_TRUE_MESSAGE(dec.ok(), dec.status.message.c_str()); + TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(plaintext, dec.value)); } void test_hkdf_rfc5869_case1() { - std::vector ikm(22, 0x0b); - std::vector salt = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c}; - std::vector info = {0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9}; - auto okm = ESPCrypto::hkdf(ShaVariant::SHA256, CryptoSpan(salt), CryptoSpan(ikm), CryptoSpan(info), 42); - TEST_ASSERT_TRUE_MESSAGE(okm.ok(), okm.status.message.c_str()); - const uint8_t expected[] = { - 0x3c, 0xb2, 0x5f, 0x25, 0xfa, 0xac, 0xd5, 0x7a, 0x90, 0x43, 0x4f, 0x64, 0xd0, 0x36, 0x2f, 0x2a, - 0x2d, 0x2d, 0x0a, 0x90, 0xcf, 0x1a, 0x5a, 0x4c, 0x5d, 0xb0, 0x2d, 0x56, 0xec, 0xc4, 0xc5, 0xbf, - 0x34, 0x00, 0x72, 0x08, 0xd5, 0xb8, 0x87, 0x18, 0x58, 0x65}; - TEST_ASSERT_EQUAL(sizeof(expected), okm.value.size()); - TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(okm.value, std::vector(expected, expected + sizeof(expected)))); + std::vector ikm(22, 0x0b); + std::vector salt = + {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c}; + std::vector info = {0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9}; + auto okm = ESPCrypto::hkdf( + ShaVariant::SHA256, + CryptoSpan(salt), + CryptoSpan(ikm), + CryptoSpan(info), + 42 + ); + TEST_ASSERT_TRUE_MESSAGE(okm.ok(), okm.status.message.c_str()); + const uint8_t expected[] = {0x3c, 0xb2, 0x5f, 0x25, 0xfa, 0xac, 0xd5, 0x7a, 0x90, 0x43, 0x4f, + 0x64, 0xd0, 0x36, 0x2f, 0x2a, 0x2d, 0x2d, 0x0a, 0x90, 0xcf, 0x1a, + 0x5a, 0x4c, 0x5d, 0xb0, 0x2d, 0x56, 0xec, 0xc4, 0xc5, 0xbf, 0x34, + 0x00, 0x72, 0x08, 0xd5, 0xb8, 0x87, 0x18, 0x58, 0x65}; + TEST_ASSERT_EQUAL(sizeof(expected), okm.value.size()); + TEST_ASSERT_TRUE( + ESPCrypto::constantTimeEq( + okm.value, + std::vector(expected, expected + sizeof(expected)) + ) + ); } void test_pbkdf2_vector() { - std::vector salt = {'s', 'a', 'l', 't'}; - auto derived = ESPCrypto::pbkdf2("password", CryptoSpan(salt), 1024, 32); - TEST_ASSERT_TRUE_MESSAGE(derived.ok(), derived.status.message.c_str()); - const uint8_t expected[] = {0x23, 0x1a, 0xfb, 0x7d, 0xcd, 0x2e, 0x86, 0x0c, 0xfd, 0x58, 0xab, 0x13, 0x37, 0x2b, 0xd1, 0x2c, - 0x92, 0x30, 0x76, 0xc3, 0x59, 0x8a, 0x12, 0x19, 0x60, 0x32, 0x0f, 0x6f, 0xec, 0x8a, 0x56, 0x98}; - TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(derived.value, std::vector(expected, expected + sizeof(expected)))); + std::vector salt = {'s', 'a', 'l', 't'}; + auto derived = ESPCrypto::pbkdf2("password", CryptoSpan(salt), 1024, 32); + TEST_ASSERT_TRUE_MESSAGE(derived.ok(), derived.status.message.c_str()); + const uint8_t expected[] = {0x23, 0x1a, 0xfb, 0x7d, 0xcd, 0x2e, 0x86, 0x0c, 0xfd, 0x58, 0xab, + 0x13, 0x37, 0x2b, 0xd1, 0x2c, 0x92, 0x30, 0x76, 0xc3, 0x59, 0x8a, + 0x12, 0x19, 0x60, 0x32, 0x0f, 0x6f, 0xec, 0x8a, 0x56, 0x98}; + TEST_ASSERT_TRUE( + ESPCrypto::constantTimeEq( + derived.value, + std::vector(expected, expected + sizeof(expected)) + ) + ); } void test_jwt_roundtrip_hs256() { - JsonDocument claims; - claims["scope"] = "demo"; - JwtSignOptions signOptions; - signOptions.algorithm = JwtAlgorithm::HS256; - signOptions.issuer = "unity"; - signOptions.expiresInSeconds = 15; - String token = ESPCrypto::createJwt(claims, "secret", signOptions); - TEST_ASSERT_TRUE(token.length() > 0); - - JsonDocument decoded; - String error; - JwtVerifyOptions verifyOptions; - verifyOptions.algorithm = JwtAlgorithm::HS256; - verifyOptions.issuer = "unity"; - TEST_ASSERT_TRUE_MESSAGE(ESPCrypto::verifyJwt(token, "secret", decoded, error, verifyOptions), error.c_str()); - TEST_ASSERT_EQUAL_STRING("demo", decoded["scope"].as()); + JsonDocument claims; + claims["scope"] = "demo"; + JwtSignOptions signOptions; + signOptions.algorithm = JwtAlgorithm::HS256; + signOptions.issuer = "unity"; + signOptions.expiresInSeconds = 15; + String token = ESPCrypto::createJwt(claims, "secret", signOptions); + TEST_ASSERT_TRUE(token.length() > 0); + + JsonDocument decoded; + String error; + JwtVerifyOptions verifyOptions; + verifyOptions.algorithm = JwtAlgorithm::HS256; + verifyOptions.issuer = "unity"; + TEST_ASSERT_TRUE_MESSAGE( + ESPCrypto::verifyJwt(token, "secret", decoded, error, verifyOptions), + error.c_str() + ); + TEST_ASSERT_EQUAL_STRING("demo", decoded["scope"].as()); } void test_sha_ctx_streaming() { - ShaCtx ctx; - TEST_ASSERT_TRUE(ctx.begin(ShaVariant::SHA256).ok()); - const char *chunk1 = "ab"; - const char *chunk2 = "c"; - TEST_ASSERT_TRUE(ctx.update(CryptoSpan(reinterpret_cast(chunk1), 2)).ok()); - TEST_ASSERT_TRUE(ctx.update(CryptoSpan(reinterpret_cast(chunk2), 1)).ok()); - std::vector out(32, 0); - TEST_ASSERT_TRUE(ctx.finish(CryptoSpan(out)).ok()); - const uint8_t expected[] = {0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23, - 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad}; - TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(out, std::vector(expected, expected + sizeof(expected)))); + ShaCtx ctx; + TEST_ASSERT_TRUE(ctx.begin(ShaVariant::SHA256).ok()); + const char *chunk1 = "ab"; + const char *chunk2 = "c"; + TEST_ASSERT_TRUE( + ctx.update(CryptoSpan(reinterpret_cast(chunk1), 2)).ok() + ); + TEST_ASSERT_TRUE( + ctx.update(CryptoSpan(reinterpret_cast(chunk2), 1)).ok() + ); + std::vector out(32, 0); + TEST_ASSERT_TRUE(ctx.finish(CryptoSpan(out)).ok()); + const uint8_t expected[] = {0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, + 0xde, 0x5d, 0xae, 0x22, 0x23, 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, + 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad}; + TEST_ASSERT_TRUE( + ESPCrypto::constantTimeEq(out, std::vector(expected, expected + sizeof(expected))) + ); } void test_sha_ctx_rebegin_reuses_context() { - ShaCtx ctx; - const char *input = "abc"; - - std::vector sha256(32, 0); - TEST_ASSERT_TRUE(ctx.begin(ShaVariant::SHA256).ok()); - TEST_ASSERT_TRUE(ctx.update(CryptoSpan(reinterpret_cast(input), 3)).ok()); - TEST_ASSERT_TRUE(ctx.finish(CryptoSpan(sha256)).ok()); - - std::vector sha512(64, 0); - TEST_ASSERT_TRUE(ctx.begin(ShaVariant::SHA512).ok()); - TEST_ASSERT_TRUE(ctx.update(CryptoSpan(reinterpret_cast(input), 3)).ok()); - TEST_ASSERT_TRUE(ctx.finish(CryptoSpan(sha512)).ok()); - - const uint8_t expectedSha256[] = { - 0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23, - 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad}; - const uint8_t expectedSha512[] = { - 0xdd, 0xaf, 0x35, 0xa1, 0x93, 0x61, 0x7a, 0xba, 0xcc, 0x41, 0x73, 0x49, 0xae, 0x20, 0x41, 0x31, - 0x12, 0xe6, 0xfa, 0x4e, 0x89, 0xa9, 0x7e, 0xa2, 0x0a, 0x9e, 0xee, 0xe6, 0x4b, 0x55, 0xd3, 0x9a, - 0x21, 0x92, 0x99, 0x2a, 0x27, 0x4f, 0xc1, 0xa8, 0x36, 0xba, 0x3c, 0x23, 0xa3, 0xfe, 0xeb, 0xbd, - 0x45, 0x4d, 0x44, 0x23, 0x64, 0x3c, 0xe8, 0x0e, 0x2a, 0x9a, 0xc9, 0x4f, 0xa5, 0x4c, 0xa4, 0x9f}; - TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(sha256, std::vector(expectedSha256, expectedSha256 + sizeof(expectedSha256)))); - TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(sha512, std::vector(expectedSha512, expectedSha512 + sizeof(expectedSha512)))); + ShaCtx ctx; + const char *input = "abc"; + + std::vector sha256(32, 0); + TEST_ASSERT_TRUE(ctx.begin(ShaVariant::SHA256).ok()); + TEST_ASSERT_TRUE( + ctx.update(CryptoSpan(reinterpret_cast(input), 3)).ok() + ); + TEST_ASSERT_TRUE(ctx.finish(CryptoSpan(sha256)).ok()); + + std::vector sha512(64, 0); + TEST_ASSERT_TRUE(ctx.begin(ShaVariant::SHA512).ok()); + TEST_ASSERT_TRUE( + ctx.update(CryptoSpan(reinterpret_cast(input), 3)).ok() + ); + TEST_ASSERT_TRUE(ctx.finish(CryptoSpan(sha512)).ok()); + + const uint8_t expectedSha256[] = {0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, + 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23, + 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, + 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad}; + const uint8_t expectedSha512[] = {0xdd, 0xaf, 0x35, 0xa1, 0x93, 0x61, 0x7a, 0xba, 0xcc, 0x41, + 0x73, 0x49, 0xae, 0x20, 0x41, 0x31, 0x12, 0xe6, 0xfa, 0x4e, + 0x89, 0xa9, 0x7e, 0xa2, 0x0a, 0x9e, 0xee, 0xe6, 0x4b, 0x55, + 0xd3, 0x9a, 0x21, 0x92, 0x99, 0x2a, 0x27, 0x4f, 0xc1, 0xa8, + 0x36, 0xba, 0x3c, 0x23, 0xa3, 0xfe, 0xeb, 0xbd, 0x45, 0x4d, + 0x44, 0x23, 0x64, 0x3c, 0xe8, 0x0e, 0x2a, 0x9a, 0xc9, 0x4f, + 0xa5, 0x4c, 0xa4, 0x9f}; + TEST_ASSERT_TRUE( + ESPCrypto::constantTimeEq( + sha256, + std::vector(expectedSha256, expectedSha256 + sizeof(expectedSha256)) + ) + ); + TEST_ASSERT_TRUE( + ESPCrypto::constantTimeEq( + sha512, + std::vector(expectedSha512, expectedSha512 + sizeof(expectedSha512)) + ) + ); } void test_hmac_ctx_rebegin_reuses_context() { - HmacCtx ctx; - std::vector key = {'k', 'e', 'y'}; - std::vector msg = {'a', 'b', 'c'}; - - std::vector out1(32, 0); - TEST_ASSERT_TRUE(ctx.begin(ShaVariant::SHA256, CryptoSpan(key)).ok()); - TEST_ASSERT_TRUE(ctx.update(CryptoSpan(msg)).ok()); - TEST_ASSERT_TRUE(ctx.finish(CryptoSpan(out1)).ok()); - - std::vector out2(32, 0); - TEST_ASSERT_TRUE(ctx.begin(ShaVariant::SHA256, CryptoSpan(key)).ok()); - TEST_ASSERT_TRUE(ctx.update(CryptoSpan(msg)).ok()); - TEST_ASSERT_TRUE(ctx.finish(CryptoSpan(out2)).ok()); - - auto oneShot = ESPCrypto::hmac(ShaVariant::SHA256, CryptoSpan(key), CryptoSpan(msg)); - TEST_ASSERT_TRUE(oneShot.ok()); - TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(out1, oneShot.value)); - TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(out2, oneShot.value)); + HmacCtx ctx; + std::vector key = {'k', 'e', 'y'}; + std::vector msg = {'a', 'b', 'c'}; + + std::vector out1(32, 0); + TEST_ASSERT_TRUE(ctx.begin(ShaVariant::SHA256, CryptoSpan(key)).ok()); + TEST_ASSERT_TRUE(ctx.update(CryptoSpan(msg)).ok()); + TEST_ASSERT_TRUE(ctx.finish(CryptoSpan(out1)).ok()); + + std::vector out2(32, 0); + TEST_ASSERT_TRUE(ctx.begin(ShaVariant::SHA256, CryptoSpan(key)).ok()); + TEST_ASSERT_TRUE(ctx.update(CryptoSpan(msg)).ok()); + TEST_ASSERT_TRUE(ctx.finish(CryptoSpan(out2)).ok()); + + auto oneShot = ESPCrypto::hmac( + ShaVariant::SHA256, + CryptoSpan(key), + CryptoSpan(msg) + ); + TEST_ASSERT_TRUE(oneShot.ok()); + TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(out1, oneShot.value)); + TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(out2, oneShot.value)); } void test_aes_ctr_stream_roundtrip() { - std::vector key(16, 0x00); - std::vector nonce(16, 0x01); - std::vector plaintext = {0x10, 0x20, 0x30, 0x40}; - std::vector ciphertext(plaintext.size(), 0); - std::vector decrypted(plaintext.size(), 0); - - AesCtrStream enc; - TEST_ASSERT_TRUE(enc.begin(key, CryptoSpan(nonce)).ok()); - TEST_ASSERT_TRUE(enc.update(CryptoSpan(plaintext), CryptoSpan(ciphertext)).ok()); - - AesCtrStream dec; - TEST_ASSERT_TRUE(dec.begin(key, CryptoSpan(nonce)).ok()); - TEST_ASSERT_TRUE(dec.update(CryptoSpan(ciphertext), CryptoSpan(decrypted)).ok()); - TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(plaintext, decrypted)); + std::vector key(16, 0x00); + std::vector nonce(16, 0x01); + std::vector plaintext = {0x10, 0x20, 0x30, 0x40}; + std::vector ciphertext(plaintext.size(), 0); + std::vector decrypted(plaintext.size(), 0); + + AesCtrStream enc; + TEST_ASSERT_TRUE(enc.begin(key, CryptoSpan(nonce)).ok()); + TEST_ASSERT_TRUE( + enc.update(CryptoSpan(plaintext), CryptoSpan(ciphertext)).ok() + ); + + AesCtrStream dec; + TEST_ASSERT_TRUE(dec.begin(key, CryptoSpan(nonce)).ok()); + TEST_ASSERT_TRUE( + dec.update(CryptoSpan(ciphertext), CryptoSpan(decrypted)).ok() + ); + TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(plaintext, decrypted)); } void test_aes_gcm_ctx_roundtrip() { - std::vector key(16, 0x33); - std::vector iv(12, 0x44); - std::vector aad = {0x01, 0x02}; - std::vector plaintext = {0x0A, 0x0B, 0x0C, 0x0D}; - std::vector ciphertext(plaintext.size(), 0); - std::vector tag(16, 0); - - AesGcmCtx enc; - TEST_ASSERT_TRUE(enc.beginEncrypt(key, CryptoSpan(iv), CryptoSpan(aad)).ok()); - TEST_ASSERT_TRUE(enc.update(CryptoSpan(plaintext), CryptoSpan(ciphertext)).ok()); - TEST_ASSERT_TRUE(enc.finish(CryptoSpan(tag)).ok()); - - std::vector decrypted(plaintext.size(), 0); - AesGcmCtx dec; - TEST_ASSERT_TRUE(dec.beginDecrypt(key, CryptoSpan(iv), CryptoSpan(aad), CryptoSpan(tag)).ok()); - TEST_ASSERT_TRUE(dec.update(CryptoSpan(ciphertext), CryptoSpan(decrypted)).ok()); - TEST_ASSERT_TRUE(dec.finish(CryptoSpan(tag)).ok()); - TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(plaintext, decrypted)); + std::vector key(16, 0x33); + std::vector iv(12, 0x44); + std::vector aad = {0x01, 0x02}; + std::vector plaintext = {0x0A, 0x0B, 0x0C, 0x0D}; + std::vector ciphertext(plaintext.size(), 0); + std::vector tag(16, 0); + + AesGcmCtx enc; + TEST_ASSERT_TRUE( + enc.beginEncrypt(key, CryptoSpan(iv), CryptoSpan(aad)).ok() + ); + TEST_ASSERT_TRUE( + enc.update(CryptoSpan(plaintext), CryptoSpan(ciphertext)).ok() + ); + TEST_ASSERT_TRUE(enc.finish(CryptoSpan(tag)).ok()); + + std::vector decrypted(plaintext.size(), 0); + AesGcmCtx dec; + TEST_ASSERT_TRUE(dec.beginDecrypt( + key, + CryptoSpan(iv), + CryptoSpan(aad), + CryptoSpan(tag) + ) + .ok()); + TEST_ASSERT_TRUE( + dec.update(CryptoSpan(ciphertext), CryptoSpan(decrypted)).ok() + ); + TEST_ASSERT_TRUE(dec.finish(CryptoSpan(tag)).ok()); + TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(plaintext, decrypted)); } void test_gcm_nonce_strategy_counter() { - std::vector key(16, 0x55); - std::vector plaintext = {0xAA, 0xBB}; - GcmNonceOptions opts; - opts.strategy = GcmNonceStrategy::Counter64_Random32; - auto first = ESPCrypto::aesGcmEncryptAuto(key, plaintext, {}, 12, opts); - auto second = ESPCrypto::aesGcmEncryptAuto(key, plaintext, {}, 12, opts); - TEST_ASSERT_TRUE(first.ok()); - TEST_ASSERT_TRUE(second.ok()); - TEST_ASSERT_EQUAL_UINT32(12, first.value.iv.size()); - TEST_ASSERT_EQUAL_UINT32(12, second.value.iv.size()); - TEST_ASSERT_FALSE(ESPCrypto::constantTimeEq(first.value.iv, second.value.iv)); + std::vector key(16, 0x55); + std::vector plaintext = {0xAA, 0xBB}; + GcmNonceOptions opts; + opts.strategy = GcmNonceStrategy::Counter64_Random32; + auto first = ESPCrypto::aesGcmEncryptAuto(key, plaintext, {}, 12, opts); + auto second = ESPCrypto::aesGcmEncryptAuto(key, plaintext, {}, 12, opts); + TEST_ASSERT_TRUE(first.ok()); + TEST_ASSERT_TRUE(second.ok()); + TEST_ASSERT_EQUAL_UINT32(12, first.value.iv.size()); + TEST_ASSERT_EQUAL_UINT32(12, second.value.iv.size()); + TEST_ASSERT_FALSE(ESPCrypto::constantTimeEq(first.value.iv, second.value.iv)); } void test_chacha20poly1305_roundtrip() { - std::vector key(32, 0x01); - std::vector nonce(12, 0x02); - std::vector aad = {0x03, 0x04}; - std::vector plaintext = {0x10, 0x20, 0x30}; - auto enc = ESPCrypto::chacha20Poly1305Encrypt(CryptoSpan(key), - CryptoSpan(nonce), - CryptoSpan(aad), - CryptoSpan(plaintext)); - TEST_ASSERT_TRUE_MESSAGE(enc.ok(), enc.status.message.c_str()); - auto dec = ESPCrypto::chacha20Poly1305Decrypt(CryptoSpan(key), - CryptoSpan(nonce), - CryptoSpan(aad), - CryptoSpan(enc.value)); - TEST_ASSERT_TRUE_MESSAGE(dec.ok(), dec.status.message.c_str()); - TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(plaintext, dec.value)); + std::vector key(32, 0x01); + std::vector nonce(12, 0x02); + std::vector aad = {0x03, 0x04}; + std::vector plaintext = {0x10, 0x20, 0x30}; + auto enc = ESPCrypto::chacha20Poly1305Encrypt( + CryptoSpan(key), + CryptoSpan(nonce), + CryptoSpan(aad), + CryptoSpan(plaintext) + ); + TEST_ASSERT_TRUE_MESSAGE(enc.ok(), enc.status.message.c_str()); + auto dec = ESPCrypto::chacha20Poly1305Decrypt( + CryptoSpan(key), + CryptoSpan(nonce), + CryptoSpan(aad), + CryptoSpan(enc.value) + ); + TEST_ASSERT_TRUE_MESSAGE(dec.ok(), dec.status.message.c_str()); + TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(plaintext, dec.value)); } void test_ecdsa_raw_der_roundtrip() { - std::vector raw(64, 0); - for (size_t i = 0; i < raw.size(); ++i) raw[i] = static_cast(i + 1); - auto der = ESPCrypto::ecdsaRawToDer(CryptoSpan(raw)); - TEST_ASSERT_TRUE_MESSAGE(der.ok(), der.status.message.c_str()); - auto rawBack = ESPCrypto::ecdsaDerToRaw(CryptoSpan(der.value)); - TEST_ASSERT_TRUE_MESSAGE(rawBack.ok(), rawBack.status.message.c_str()); - TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(raw, rawBack.value)); + std::vector raw(64, 0); + for (size_t i = 0; i < raw.size(); ++i) + raw[i] = static_cast(i + 1); + auto der = ESPCrypto::ecdsaRawToDer(CryptoSpan(raw)); + TEST_ASSERT_TRUE_MESSAGE(der.ok(), der.status.message.c_str()); + auto rawBack = ESPCrypto::ecdsaDerToRaw(CryptoSpan(der.value)); + TEST_ASSERT_TRUE_MESSAGE(rawBack.ok(), rawBack.status.message.c_str()); + TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(raw, rawBack.value)); } void test_aes_gcm_span_roundtrip() { - std::vector key(16, 0x11); - std::vector iv(12, 0x22); - std::vector plaintext = {0xAA, 0xBB, 0xCC, 0xDD}; - std::vector ciphertext(plaintext.size(), 0); - std::vector tag(16, 0); - - auto enc = ESPCrypto::aesGcmEncrypt(key, - CryptoSpan(iv), - CryptoSpan(plaintext), - CryptoSpan(ciphertext), - CryptoSpan(tag)); - TEST_ASSERT_TRUE_MESSAGE(enc.ok(), enc.status.message.c_str()); - - std::vector decrypted(plaintext.size(), 0); - auto dec = ESPCrypto::aesGcmDecrypt(key, - CryptoSpan(iv), - CryptoSpan(ciphertext), - CryptoSpan(tag), - CryptoSpan(decrypted)); - TEST_ASSERT_TRUE_MESSAGE(dec.ok(), dec.status.message.c_str()); - TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(plaintext, decrypted)); + std::vector key(16, 0x11); + std::vector iv(12, 0x22); + std::vector plaintext = {0xAA, 0xBB, 0xCC, 0xDD}; + std::vector ciphertext(plaintext.size(), 0); + std::vector tag(16, 0); + + auto enc = ESPCrypto::aesGcmEncrypt( + key, + CryptoSpan(iv), + CryptoSpan(plaintext), + CryptoSpan(ciphertext), + CryptoSpan(tag) + ); + TEST_ASSERT_TRUE_MESSAGE(enc.ok(), enc.status.message.c_str()); + + std::vector decrypted(plaintext.size(), 0); + auto dec = ESPCrypto::aesGcmDecrypt( + key, + CryptoSpan(iv), + CryptoSpan(ciphertext), + CryptoSpan(tag), + CryptoSpan(decrypted) + ); + TEST_ASSERT_TRUE_MESSAGE(dec.ok(), dec.status.message.c_str()); + TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(plaintext, decrypted)); } void test_device_key_is_stable() { - auto first = ESPCrypto::deriveDeviceKey("unity-device-key", CryptoSpan(), 32); - auto second = ESPCrypto::deriveDeviceKey("unity-device-key", CryptoSpan(), 32); - TEST_ASSERT_TRUE_MESSAGE(first.ok(), first.status.message.c_str()); - TEST_ASSERT_TRUE_MESSAGE(second.ok(), second.status.message.c_str()); - TEST_ASSERT_EQUAL_UINT32(32, first.value.size()); - TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(first.value, second.value)); + auto first = ESPCrypto::deriveDeviceKey("unity-device-key", CryptoSpan(), 32); + auto second = ESPCrypto::deriveDeviceKey("unity-device-key", CryptoSpan(), 32); + TEST_ASSERT_TRUE_MESSAGE(first.ok(), first.status.message.c_str()); + TEST_ASSERT_TRUE_MESSAGE(second.ok(), second.status.message.c_str()); + TEST_ASSERT_EQUAL_UINT32(32, first.value.size()); + TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(first.value, second.value)); } void test_jwt_and_envelope_fuzz() { - const char *badTokens[] = {"", "abc", "a.b", "a.b.c", "e30=.e30=.@@@@", "eyJhbGciOiJIUzI1NiJ9.e30.bad"}; - JsonDocument out; - String err; - JwtVerifyOptions opts; - opts.algorithm = JwtAlgorithm::HS256; - for (auto t : badTokens) { - TEST_ASSERT_FALSE(ESPCrypto::verifyJwt(String(t), "secret", out, err, opts)); - } - TEST_ASSERT_FALSE(ESPCrypto::verifyString("pw", "$esphash$v1$bad$bad$bad")); + const char *badTokens[] = + {"", "abc", "a.b", "a.b.c", "e30=.e30=.@@@@", "eyJhbGciOiJIUzI1NiJ9.e30.bad"}; + JsonDocument out; + String err; + JwtVerifyOptions opts; + opts.algorithm = JwtAlgorithm::HS256; + for (auto t : badTokens) { + TEST_ASSERT_FALSE(ESPCrypto::verifyJwt(String(t), "secret", out, err, opts)); + } + TEST_ASSERT_FALSE(ESPCrypto::verifyString("pw", "$esphash$v1$bad$bad$bad")); } void test_jwks_verification() { - // Build HS256 token and JWKS with oct key - JsonDocument claims; - claims["iss"] = "jwks"; - JwtSignOptions signOpts; - signOpts.algorithm = JwtAlgorithm::HS256; - signOpts.expiresInSeconds = 60; - signOpts.keyId = "k1"; - String token = ESPCrypto::createJwt(claims, "supersecret", signOpts); - JsonDocument jwks; - JsonArray keys = jwks["keys"].to(); - JsonObject k = keys.add(); - k["kty"] = "oct"; - k["kid"] = "k1"; - k["k"] = "c3VwZXJzZWNyZXQ"; // base64url("supersecret") - JsonDocument decoded; - auto res = ESPCrypto::verifyJwtWithJwks(token, jwks, decoded); - TEST_ASSERT_TRUE_MESSAGE(res.ok(), res.status.message.c_str()); - TEST_ASSERT_EQUAL_STRING("jwks", decoded["iss"].as()); + // Build HS256 token and JWKS with oct key + JsonDocument claims; + claims["iss"] = "jwks"; + JwtSignOptions signOpts; + signOpts.algorithm = JwtAlgorithm::HS256; + signOpts.expiresInSeconds = 60; + signOpts.keyId = "k1"; + String token = ESPCrypto::createJwt(claims, "supersecret", signOpts); + JsonDocument jwks; + JsonArray keys = jwks["keys"].to(); + JsonObject k = keys.add(); + k["kty"] = "oct"; + k["kid"] = "k1"; + k["k"] = "c3VwZXJzZWNyZXQ"; // base64url("supersecret") + JsonDocument decoded; + auto res = ESPCrypto::verifyJwtWithJwks(token, jwks, decoded); + TEST_ASSERT_TRUE_MESSAGE(res.ok(), res.status.message.c_str()); + TEST_ASSERT_EQUAL_STRING("jwks", decoded["iss"].as()); +} +void setUp() { +} +void tearDown() { } -void setUp() {} -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); - RUN_TEST(test_sha_ctx_rebegin_reuses_context); - RUN_TEST(test_hmac_ctx_rebegin_reuses_context); - RUN_TEST(test_password_hash_roundtrip); - RUN_TEST(test_aes_gcm_known_vector); - RUN_TEST(test_aes_gcm_auto_iv_roundtrip); - RUN_TEST(test_aes_gcm_span_roundtrip); - RUN_TEST(test_aes_ctr_stream_roundtrip); - RUN_TEST(test_aes_gcm_ctx_roundtrip); - RUN_TEST(test_gcm_nonce_strategy_counter); - RUN_TEST(test_chacha20poly1305_roundtrip); - RUN_TEST(test_ecdsa_raw_der_roundtrip); - RUN_TEST(test_hkdf_rfc5869_case1); - RUN_TEST(test_pbkdf2_vector); - RUN_TEST(test_jwt_roundtrip_hs256); - RUN_TEST(test_jwt_and_envelope_fuzz); - RUN_TEST(test_device_key_is_stable); - UNITY_END(); + 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); + RUN_TEST(test_sha_ctx_rebegin_reuses_context); + RUN_TEST(test_hmac_ctx_rebegin_reuses_context); + RUN_TEST(test_password_hash_roundtrip); + RUN_TEST(test_aes_gcm_known_vector); + RUN_TEST(test_aes_gcm_auto_iv_roundtrip); + RUN_TEST(test_aes_gcm_span_roundtrip); + RUN_TEST(test_aes_ctr_stream_roundtrip); + RUN_TEST(test_aes_gcm_ctx_roundtrip); + RUN_TEST(test_gcm_nonce_strategy_counter); + RUN_TEST(test_chacha20poly1305_roundtrip); + RUN_TEST(test_ecdsa_raw_der_roundtrip); + RUN_TEST(test_hkdf_rfc5869_case1); + RUN_TEST(test_pbkdf2_vector); + RUN_TEST(test_jwt_roundtrip_hs256); + RUN_TEST(test_jwt_and_envelope_fuzz); + RUN_TEST(test_device_key_is_stable); + UNITY_END(); } void loop() { - vTaskDelay(pdMS_TO_TICKS(1000)); + vTaskDelay(pdMS_TO_TICKS(1000)); }