diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8cc76b..48364b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,13 +2,13 @@ name: CI on: push: - branches: [ main, master ] + branches: [ main, master, v2 ] tags: ['v*'] pull_request: workflow_dispatch: jobs: - unit-tests: + host-portable-smoke: runs-on: ubuntu-latest steps: - name: Checkout @@ -17,15 +17,33 @@ jobs: - name: Configure CMake run: cmake -S . -B build - - name: Build tests + - name: Build host smoke target run: cmake --build build - - name: Run tests + - name: Run host smoke target run: ctest --test-dir build --output-on-failure - build-examples: + esp-idf-core-smoke: runs-on: ubuntu-latest - needs: unit-tests + needs: host-portable-smoke + strategy: + fail-fast: false + matrix: + target: [esp32, esp32s3, esp32c3, esp32p4] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build ESP-IDF component smoke app + uses: espressif/esp-idf-ci-action@v1 + with: + esp_idf_version: v5.4 + target: ${{ matrix.target }} + path: examples/esp_idf_core_smoke + + arduino-platformio: + runs-on: ubuntu-latest + needs: host-portable-smoke strategy: fail-fast: false matrix: @@ -53,13 +71,12 @@ jobs: - name: Install PIOArduino ESP32 Platform run: pio platform install https://github.com/pioarduino/platform-espressif32.git - - name: Build library examples (ESP32 Arduino) + - name: Build Arduino examples with PlatformIO run: | set -e for d in examples/*; do - if [ -d "$d" ]; then + if [ -d "$d" ] && [ ! -f "$d/CMakeLists.txt" ]; then echo "Building $d on ${{ matrix.board }} via PlatformIO CI" - # Force C++17 for this project to satisfy library requirements pio ci "$d" \ --board ${{ matrix.board }} \ --lib="." \ @@ -72,7 +89,7 @@ jobs: arduino-cli: runs-on: ubuntu-latest - needs: unit-tests + needs: host-portable-smoke env: ESP32_CORE_VERSION: 3.3.3 ESP32_ADDITIONAL_URL: https://espressif.github.io/arduino-esp32/package_esp32_index.json @@ -112,7 +129,7 @@ jobs: - name: Install libraries run: | arduino-cli lib update-index - arduino-cli lib install "ArduinoJson" "StreamUtils" + arduino-cli lib install "ArduinoJson" - name: Add local library to sketchbook run: | @@ -121,7 +138,7 @@ jobs: mkdir -p "$SKETCHBOOK_DIR/libraries/ESPCrypto" rsync -a --delete --exclude ".git" ./ "$SKETCHBOOK_DIR/libraries/ESPCrypto/" - - name: Build examples + - name: Build Arduino examples env: BOARDS: ${{ env.ARDUINO_BOARDS }} run: | @@ -132,7 +149,7 @@ jobs: fi echo "::group::Compiling examples for ${board_name} (${fqbn})" for d in examples/*; do - if [ -d "$d" ]; then + if [ -d "$d" ] && [ ! -f "$d/CMakeLists.txt" ]; then echo "Compiling $d" arduino-cli compile --fqbn "$fqbn" "$d" fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 3081796..c5a53ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ _No changes yet._ - ChaCha20-Poly1305 encrypt/decrypt and X25519 shared-secret helper (capability-gated); XChaCha20-Poly1305 and Ed25519/EdDSA APIs are present but return `Unsupported` until a backend is available. - New examples: keystore/streaming demo, JWKS rotation, and micro-benchmarks for SHA/AES-GCM. - Planned curve25519 helpers once ESP-IDF exposes hardware accel hooks. -- `SecureBuffer`/`SecureString` RAII containers that zeroize sensitive material, plus `CryptoStatus`/`CryptoResult` and span-based overloads for SHA, AES, JWT, signing, and password helpers. +- `SecureBuffer`/secure text RAII containers that zeroize sensitive material, plus `CryptoStatus`/`CryptoResult` and span-based overloads for SHA, AES, JWT, signing, and password helpers. - AES-GCM safe helpers that auto-generate nonces, optional nonce-reuse debug guardrails, and capability reporting via `ESPCrypto::caps()`. - HMAC/HKDF/PBKDF2 APIs (SHA-256/384/512) with policy enforcement for PBKDF2 iteration counts and RSA/ECC key sizes. - Known-answer tests for SHA-2 variants, AES-GCM (NIST vectors), HKDF, PBKDF2, and AES-GCM auto-IV round-trips to keep regressions visible. diff --git a/CMakeLists.txt b/CMakeLists.txt index 860f0c6..f950f6d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,25 @@ # MIT License cmake_minimum_required(VERSION 3.12) + +set(ESPCRYPTO_SOURCES + src/esp_crypto/crypto_core.cpp + src/esp_crypto/crypto_hash_kdf.cpp + src/esp_crypto/crypto_symmetric.cpp + src/esp_crypto/crypto_asymmetric.cpp + src/esp_crypto/crypto_storage.cpp + src/esp_crypto/crypto_jwt.cpp +) + +if(ESP_PLATFORM) + idf_component_register( + SRCS ${ESPCRYPTO_SOURCES} + INCLUDE_DIRS src + REQUIRES mbedtls nvs_flash esp_timer + ) + return() +endif() + project(ESPCrypto) include(CTest) diff --git a/README.md b/README.md index c747baa..005770d 100644 --- a/README.md +++ b/README.md @@ -1,154 +1,98 @@ -# ESPCrypto +# ESPCrypto v2 -ESPCrypto wraps the ESP32 hardware crypto blocks (SHA, AES-GCM/CTR, RSA/ECC) with guardrails, automatic fallbacks, and high-level helpers (JWTs, salted hashes) that work in both ESP-IDF and Arduino builds. +ESPCrypto v2 is an ESP32-focused crypto library with a standard C++ public surface. The core API is ESP-IDF-friendly, avoids `Arduino.h` and Arduino string types, and keeps `JsonDocument` only in the optional JWT/JWKS module. ## CI / Release / License [![CI](https://github.com/ESPToolKit/esp-crypto/actions/workflows/ci.yml/badge.svg)](https://github.com/ESPToolKit/esp-crypto/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/v/release/ESPToolKit/esp-crypto?sort=semver)](https://github.com/ESPToolKit/esp-crypto/releases) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.md) -## Toolchain Compatibility -- GitHub Actions builds against the ESP32 Arduino core `3.3.3` via Espressif's board manager URL (IDF 5.x generation) and caches the toolchains to keep PlatformIO/Arduino builds in sync for `esp32`, `esp32-s3`, `esp32-c3`, and `esp32-p4` boards. -- Runtime code gates the mbedTLS 2.x (ESP-IDF 4.x) and 3.x (ESP-IDF 5.x) API differences—including the ESP AES-GCM alt streaming signatures—so PlatformIO/Arduino builds succeed regardless of which ESP-IDF revision a board package ships. -- Device fingerprinting prefers `esp_read_mac` from `esp_mac.h` when present and falls back to `esp_efuse_mac_get_default`, so Arduino/PlatformIO board packages that dropped `esp_efuse_mac.h` still build. - -## Features -- SHA256/384/512 helpers that try the ESP parallel SHA engine first and fall back to mbedTLS when the accelerator (or platform) is unavailable. -- AES-GCM and AES-CTR utilities with a safe `aesGcmEncryptAuto` that generates a random 12-byte IV, optional nonce-reuse debug guard, and capability introspection via `ESPCrypto::caps()`. -- RSA/ECC signing + verification helpers (PKCS#1 v1.5 + ECDSA) that power HS256/RS256/ES256 JWT flows or stand-alone signatures. -- `CryptoKey` + `KeyHandle` abstractions with `MemoryKeyStore`, `NvsKeyStore`, and `LittleFsKeyStore` for alias/versioned key rotation plus cached mbedTLS contexts to avoid repeated parsing. -- Device-bound HKDF helper `deriveDeviceKey(...)` that derives stable per-device keys from an optional persistent NVS secret plus the chip fingerprint, mainly to avoid hard-coded symmetric keys rather than to provide hardware-backed key protection. -- Buffer-friendly span overloads for SHA digests and AES-GCM encrypt/decrypt that write into caller-provided buffers to reduce heap churn on large payloads, plus streaming contexts (`ShaCtx`, `HmacCtx`, `AesCtrStream`, `AesGcmCtx`) for chunked workloads. -- Nonce strategies for AES-GCM auto IVs: random 96-bit (default), counter+random hybrid, or boot-counter based with optional NVS persistence to avoid reuse under long-lived keys. -- Modern lanes: ChaCha20-Poly1305 for CPUs without AES accel, X25519 ECDH helper, and ECDSA DER↔raw helpers to interop with JOSE stacks. (Ed25519/EdDSA stay capability-gated to platform support.) -- HMAC/HKDF/PBKDF2 (SHA-256/384/512) building blocks with policy enforcement for PBKDF2 iteration counts; password hashing uses these primitives and constant-time verification. -- Structured `CryptoStatus` + `CryptoResult` with span-friendly overloads to reduce heap churn and keep error handling uniform; `SecureBuffer`/`SecureString` zeroize sensitive data on scope exit. -- Full JWT builder/validator that uses ArduinoJson v7 `JsonDocument`s, fills `iat`/`exp`/`nbf` fields, enforces issuer/audience, and exposes both friendly errors and structured status codes. -- Ready-to-flash example plus Unity tests under `test/test_esp_crypto` with NIST/RFC vectors for SHA, AES-GCM, HKDF, PBKDF2, JWT, and password hashing regressions. +## Module Layout +- Core headers: `esp_crypto/types.h`, `esp_crypto/runtime.h`, `esp_crypto/policy.h`, `esp_crypto/hash.h`, `esp_crypto/kdf.h`, `esp_crypto/symmetric.h`, `esp_crypto/asymmetric.h`, `esp_crypto/stream.h` +- Optional headers: `esp_crypto/password.h`, `esp_crypto/jwt.h`, `esp_crypto/keystore.h`, `esp_crypto/device_key.h` +- Umbrella include: `ESPCrypto.h` -## Examples -- `examples/basic_hash_and_aes` – SHA plus AES-GCM with auto IV/tag handling and structured status. -- `examples/jwt_and_password` – HS256 JWT creation/verification and password hashing/verification. -- `examples/advanced_primitives` – Capability/policy introspection, SecureBuffer/String, HMAC/HKDF/PBKDF2, AES-CTR streaming, and RSA/ECDSA signing flows. -- `examples/keys_and_streaming` – Keystore usage, streaming SHA/AES-GCM, nonce strategies, and device-bound key derivation. -- `examples/bench_crypto` – Tiny on-device timing loops for SHA and AES-GCM to gauge perf per board. -- `examples/jwks_rotation` – JWKS verification with rotating `kid` values for HS256. +## Support Matrix +- Core crypto modules: ESP-IDF and Arduino +- Password module: ESP-IDF and Arduino +- JWT/JWKS module: ESP-IDF and Arduino, with `JsonDocument` from ArduinoJson v7 +- NVS/device-key helpers: ESP-IDF and Arduino on ESP32 targets +- LittleFS keystore: Arduino-compatible targets where `LittleFS.h` is available + +## Design Notes +- Core APIs use `CryptoResult`, `CryptoSpan`, `std::string`, and `std::string_view`. +- The password module now uses explicit PBKDF2 iterations, emits `$esphash$v2$$$`, and calibrates to a 250 ms target by default with a 100,000-iteration floor. +- Legacy `$esphash$v1$...` envelopes are rejected by default and only accepted through `PasswordVerifyOptions{.allowLegacy = true}`. +- Ed25519 placeholder headers were removed. Unsupported algorithms are not exposed as public no-op APIs. -The basic AES example shows SHA and AES-GCM in one go: +## Quick Start ```cpp -#include -#include +#include +#include +#include +#include #include -void setup() { - Serial.begin(115200); - std::vector key(32, 0x01); +void use_crypto() { + std::string digest = espcrypto::hash::shaHex("esptoolkit"); + + std::vector key(32, 0x11); std::vector plaintext = {'h', 'e', 'l', 'l', 'o'}; - String digest = ESPCrypto::shaHex("esptoolkit"); - auto gcm = ESPCrypto::aesGcmEncryptAuto(key, plaintext); - if (gcm.ok()) { - auto decrypted = ESPCrypto::aesGcmDecrypt(key, gcm.value.iv, gcm.value.ciphertext, gcm.value.tag); - (void)decrypted; + auto encrypted = espcrypto::symmetric::aesGcmEncryptAuto(key, plaintext); + if (!encrypted.ok()) { + return; } - // Release ESPCrypto runtime caches/state before deep sleep or shutdown paths. - ESPCrypto::deinit(); -} - -void loop() {} -``` - -Run `examples/basic_hash_and_aes` via PlatformIO/Arduino to see the full output. - -## Lifecycle and Teardown -- `ESPCrypto` is static-style, so there is no instance destructor to release global runtime state. -- Call `ESPCrypto::deinit()` when your app no longer needs crypto helpers (for example before deep sleep, app shutdown, or full subsystem restart). -- `deinit()` is safe before any crypto call and safe to call repeatedly. -- Use `ESPCrypto::isInitialized()` to check whether runtime state (policy/caches/counters) is currently active. - -### Key management and device-bound helpers -Cache parsed keys, rotate aliases, and derive symmetric keys without shipping long-lived secrets in firmware: + auto decrypted = espcrypto::symmetric::aesGcmDecrypt( + key, + encrypted.value.iv, + encrypted.value.ciphertext, + encrypted.value.tag + ); + (void)digest; + (void)decrypted; -```cpp -#include - -void rotate_keys() { - MemoryKeyStore memory; - KeyHandle current{String("jwt_auth"), 2}; - - // Store a PEM private key and reload it as a cached CryptoKey - const char *pem = "-----BEGIN PRIVATE KEY-----..."; - ESPCrypto::storeKey(memory, current, CryptoSpan(reinterpret_cast(pem), strlen(pem))); - auto loaded = ESPCrypto::loadKey(memory, current, KeyFormat::Pem, KeyKind::Private); - if (loaded.ok()) { - auto sig = ESPCrypto::rsaSign( - loaded.value, - CryptoSpan(reinterpret_cast("payload"), 7), - ShaVariant::SHA256 - ); - (void)sig; - } - - // Derive a device-bound symmetric key using HKDF + NVS-backed seed - auto derived = ESPCrypto::deriveDeviceKey("provisioning", CryptoSpan(), 32); - if (derived.ok()) { - // use derived.value as an AES or HMAC key without embedding long-term secrets - } + espcrypto::runtime::deinit(); } ``` -`MemoryKeyStore` keeps key material only in RAM for tests or ephemeral rotations. `LittleFsKeyStore` stores blobs on LittleFS when mounted, and `NvsKeyStore` persists blobs in NVS with whatever flash/NVS protection the device is configured for. None of these backends should be treated as a secure-element substitute. - ## API Highlights -- `CryptoResult> shaResult(...)` / `shaHex(...)` – SHA256/384/512 with optional hardware preference (default on) and structured status codes. -- `CryptoResult aesGcmEncryptAuto(...)` + `aesGcmDecrypt(...)` – 128/192/256-bit AES-GCM with random IVs, optional AAD, 16-byte tags, and policy-enforced IV length; `aesCtrCrypt(...)` covers stream-like CTR use cases. -- `CryptoResult> rsaSign/eccSign` and `rsaVerify/eccVerify` – Wrap mbedTLS PK contexts while enforcing minimum key sizes unless `allowLegacy` is enabled. -- `CryptoKey` helpers for RSA/ECC reuse parsed PK contexts; pair them with `KeyHandle` aliases in a `KeyStore` to rotate versions without reparsing PEM/DER. -- `CryptoResult sha(...)` and `aesGcmEncrypt/Decrypt(...)` span overloads – write digests/ciphertext/tag into caller-owned buffers to avoid heap allocations on large payloads. -- Streaming helpers: `ShaCtx`/`HmacCtx` for incremental hashing/HMAC, `AesCtrStream` for chunked CTR flows, and `AesGcmCtx` for AAD + payload streaming with tag verification. -- `GcmNonceOptions` lets you pick random, counter+random, or boot-counter IV strategies (with optional NVS persistence) when using `aesGcmEncryptAuto(...)`. -- JWT additions: JWK/JWKS verification helper, leeway support, multi-audience/typ enforcement, and DER↔raw ECDSA helpers to match JOSE encodings. -- ChaCha20-Poly1305 encrypt/decrypt helpers and X25519 shared-secret derivation for devices where AES accel varies. XChaCha20-Poly1305 and Ed25519/EdDSA APIs currently return `Unsupported` until the toolchain provides those primitives. -- `CryptoResult createJwtResult(...)` / `verifyJwtResult(...)` – Build HS256/RS256/ES256 JWTs with auto `iat`/`exp` fields and get back structured status plus the friendly error string versions. -- `CryptoResult> hmac/hkdf/pbkdf2` and `hashString`/`verifyString` – HMAC/HKDF/PBKDF2 building blocks; password hashes stay in the `$esphash$v1$cost$salt$hash` envelope and compare in constant time. -- `CryptoCaps caps()` and `SecureBuffer`/`SecureString` – Introspect hardware acceleration availability and zeroize sensitive buffers on scope exit. - -## JWT Helpers -`JwtSignOptions` lets you set `issuer`, `subject`, `audience`, `expiresInSeconds`, `notBefore`, `issuedAt`, and `keyId`. `JwtVerifyOptions` can enforce issuer/audience matches, require expiration, and accept externally supplied clocks (e.g., SNTP time). Header/payload data stays as ArduinoJson v7 `JsonDocument`s, so you can merge them with `doc.set(...)` or stream them over serial for debugging. Use `createJwt`/`verifyJwt` for friendly strings or `createJwtResult`/`verifyJwtResult` for structured status codes. - -`verifyJwtWithJwks` consumes an in-memory JWKS (`JsonDocument`) and picks keys by `kid`, with support for leeway, multi-audience payloads, `typ` enforcement, and `crit` header allowlists. ECDSA raw/DER conversion helpers are available when interoping with JOSE stacks that send compact raw signatures. - -## Policy & Guardrails -- `CryptoPolicy` (default: RSA ≥ 2048 bits, PBKDF2 iterations ≥ 1024, GCM IV ≥ 12 bytes) is readable via `ESPCrypto::policy()` and adjustable with `setPolicy(...)`; set `allowLegacy = true` to opt into weaker parameters. -- AES-GCM can enable debug nonce-reuse detection via `ESPCRYPTO_ENABLE_NONCE_GUARD` (tiny LRU cache keyed by IV + key fingerprint). -- `constantTimeEq` performs content-constant-time comparison only when both inputs already have the same length; a length mismatch returns `false` immediately. `SecureBuffer` and `SecureString` zeroize owned memory on cleanup. - -## Security Posture -- Constant-time coverage: `constantTimeEq` underpins password verification and HS256 JWT checks when compared buffers are the same length; it does not hide input length. Other primitives lean on ESP-IDF/mbedTLS implementations and should be treated as best-effort constant-time rather than hardened side-channel countermeasures. -- Hardware acceleration: SHA, AES-CTR, and AES-GCM try the ESP hardware blocks first and fall back to mbedTLS software paths; `ESPCrypto::caps()` reports what is active at runtime. Random bytes come from `esp_fill_random` on-device and from `std::random_device` only for host builds/tests. -- Best-effort hardening: password hashes stay in a structured envelope with policy-enforced PBKDF2 costs, AES-GCM enforces IV length and offers an optional nonce-reuse guard, and sensitive buffers zeroize on scope exit or failure paths. -- Threat model: aimed at network-connected ESP32-class devices where attackers can send arbitrary inputs. It does not attempt to defend against physical capture, power/EM/fault-injection side channels, or secure element/key storage requirements; review your board’s secure boot/flash encryption story separately. - -## Password Hashing -`hashString` emits `$esphash$v1$$$` so you can persist passwords without storing secrets. Costs map to `2^cost` PBKDF2 iterations (default 10 ⇒ 1024) and will auto-bump to the policy minimum iteration count unless `allowLegacy` is enabled. `verifyString` accepts any string in that envelope, decodes the salt/hash, replays PBKDF2, and compares in constant time. - -## 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`. +- `espcrypto::hash::sha(...)` and `espcrypto::hash::shaHex(...)` +- `espcrypto::kdf::hmac(...)`, `espcrypto::kdf::hkdf(...)`, `espcrypto::kdf::pbkdf2(...)` +- `espcrypto::symmetric::aesGcmEncryptAuto(...)`, `espcrypto::symmetric::aesGcmDecrypt(...)`, `espcrypto::symmetric::aesCtrCrypt(...)` +- `espcrypto::asymmetric::rsaSign(...)`, `espcrypto::asymmetric::rsaVerify(...)`, `espcrypto::asymmetric::eccSign(...)`, `espcrypto::asymmetric::eccVerify(...)` +- `ShaCtx`, `HmacCtx`, `AesCtrStream`, and `AesGcmCtx` for streaming workloads +- `espcrypto::password::hash(...)`, `espcrypto::password::verify(...)`, and `espcrypto::password::calibrateIterations(...)` +- `espcrypto::jwt::create(...)`, `espcrypto::jwt::verify(...)`, and `espcrypto::jwt::verifyWithJwks(...)` +- `espcrypto::keystore::store(...)`, `espcrypto::keystore::load(...)`, and `espcrypto::device::deriveKey(...)` + +## Migration From v1 +- Replace every Arduino string input/output with `std::string`. +- Replace `ESPCrypto::shaHex(...)` with `espcrypto::hash::shaHex(...)`. +- Replace `ESPCrypto::createJwtResult(...)` / `verifyJwtResult(...)` with `espcrypto::jwt::create(...)` / `espcrypto::jwt::verify(...)`. +- Replace `ESPCrypto::hashString(...)` / `verifyString(...)` with `espcrypto::password::hash(...)` / `espcrypto::password::verify(...)`. +- Replace `ESPCrypto::storeKey(...)`, `loadKey(...)`, and `deriveDeviceKey(...)` with `espcrypto::keystore::store(...)`, `espcrypto::keystore::load(...)`, and `espcrypto::device::deriveKey(...)`. +- Replace `SecureString` with `SecureText`. + +## Testing +- Device-side Unity coverage lives in `test/test_esp_crypto`. +- Host-side CMake coverage is limited to portable header/API smoke checks and no longer claims to run functional crypto tests. +- CI also builds the example sketches across multiple ESP32 boards. -## License -MIT — see [LICENSE.md](LICENSE.md). +## Examples +- `examples/basic_hash_and_aes` +- `examples/jwt_and_password` +- `examples/advanced_primitives` +- `examples/keys_and_streaming` +- `examples/bench_crypto` +- `examples/jwks_rotation` + +## Formatting +- `.clang-format` is the C/C++/INO formatting source of truth. +- `.editorconfig` enforces tabs, LF endings, and final newlines. +- Run `bash scripts/format_cpp.sh` for tracked firmware sources. -## ESPToolKit -- Discover other libraries: -- Website: -- Support the project: -- Visit the website: +## License +MIT. See [LICENSE.md](LICENSE.md). diff --git a/examples/advanced_primitives/advanced_primitives.ino b/examples/advanced_primitives/advanced_primitives.ino index ed02278..6d0ca37 100644 --- a/examples/advanced_primitives/advanced_primitives.ino +++ b/examples/advanced_primitives/advanced_primitives.ino @@ -4,214 +4,77 @@ #include #include -const char *RSA_PRIVATE_PEM = R"(-----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCwudwslbzHhgGu -dFb3e6tz3E71iiqpYE5kpdAipKyMlbjIQgCiEFRfZnDNGZJSMEEkFYQDjGx/Q0wu -mZxgI52rIDW3ZQcp2LqMg16JesUUMv7WW/EEhOX7yQjth2uZVHio3sKanteLjG0s -NPbyMfG3oGp1m6m75kovsm/KF6/stftPmqhbJSD9vfeDF9NQDw1nsnxY1X8an4cl -clidYssFPoD+mImoKxX+y+GLTcsbY+RLRV1DK5PFKWSVZ3UHwdOp9OEZhNgXgCQ2 -o8IE9d7Be2PbzCmgYQGPekRehZwf/q5bhnS00dT+/qwcwlA8sra2Po7H34XtjDOK -IOCjkMuhAgMBAAECggEAAgG3mnlVdu3dHVuB1KE+svvt7kN+34R8mg+jRjluIicz -EscOax4Erz6iX5nU5leuQwLMMx7IPpuyL5dGm3WGvUzff0ZyPIs9obR+LCZ3kBan -e8yjIc+BLbOR2oyemqjxuSJ/vYdkitech74kOF97z1TO0Ki6ASxeFvOPlGZiH4Me -pxvHMJ3LVW3UBLOJRnpM5/sIyVhyj3ANHKkEU2yIe6qvzKo5sRWI/NtID87wi36Y -LbWA7zCHqXxenyawhHWs1a9757UQKh8Gzd+qX6M5jeIWNgdVMeTPtW8gMN9+raRH -2nAYOBKMTgl5rFB+KYxA47VCEHPq5RmlOFf5GY3HSQKBgQDgumgxejxK3P2bFDee -znpBrqvTPRAbAa02Y+rcBb0tMt3QeFuuvvF9+kkJxVZk6HKID933Vo88tr6p4qDx -pRE2lK+wgk90mBcsGH5FOas9VlJspXeFMwcPsaznmX1gP8otYo+HH037sT9KqP1N -N/l+cv8TE9KqLq6uhzlgTYhZZQKBgQDJUXN+e6liSseLyBHsJRL466xS3GSVu0fI -22Z3c9x158Mz0JeqW0zywjJfdDTa9JJpXzuaNFlPMi7VsicB0JeDrKOlfOcdBvDv -jUWRNzgaTPQSEAQROmDnhOAgDXCCHw7k1Pnpr2VU42jnoXc3aJKKUnLC9I7jiXkE -IF2EDbfjjQKBgA1DczrYWA6jFGS+wLmivhx6TrHc/MJbSvnW09nAjPXJ9sWDFQYv -RtmEmCL3fq3d+kSFizg556JRttcYBR+9+lIaXHQyfLYI8/UqTOmRCcZI/fxjl7ZI -2LXYargQmxG/MhOTqZz0AApG39FsP+b60sLfzqY1mU1qC+1JFd3VNaLxAoGBAI9h -Z3Rp9pV+1OgFMl6ReRW4JB9PwIOzwsiXGj9xUU7YJfq9UYePRxqOnPnG9e4Lyksp -/HUzW3hAMYMZQxbTzVWGm3a9oozV6Lt0TlvCjD6PGDXVGlB615GM3WN2ru692Am6 -ddOti+oNnSV7pkDcRaImXn3jV/FOc9YwhuoKKzHxAoGAMsH96Qa4X2fKpd7qCG+V -MXMST6MY2rWR+UqNHfd8FCJc8zMERm8yeibG6CxdZQkN4VBoq5kj4ZCiPkSmhWbQ -Gtz/xpPPMUrpRSy8QXQHEGbSBelyK3bgpDukTt90qgqj+2Emvna5tmZrPS3bxx1l -ZpIBKrSVJRXd+g0+Ykq17jc= ------END PRIVATE KEY-----)"; - -const char *RSA_PUBLIC_PEM = R"(-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsLncLJW8x4YBrnRW93ur -c9xO9YoqqWBOZKXQIqSsjJW4yEIAohBUX2ZwzRmSUjBBJBWEA4xsf0NMLpmcYCOd -qyA1t2UHKdi6jINeiXrFFDL+1lvxBITl+8kI7YdrmVR4qN7Cmp7Xi4xtLDT28jHx -t6BqdZupu+ZKL7Jvyhev7LX7T5qoWyUg/b33gxfTUA8NZ7J8WNV/Gp+HJXJYnWLL -BT6A/piJqCsV/svhi03LG2PkS0VdQyuTxSlklWd1B8HTqfThGYTYF4AkNqPCBPXe -wXtj28wpoGEBj3pEXoWcH/6uW4Z0tNHU/v6sHMJQPLK2tj6Ox9+F7YwziiDgo5DL -oQIDAQAB ------END PUBLIC KEY-----)"; - -const char *ECC_PRIVATE_PEM = R"(-----BEGIN EC PRIVATE KEY----- -MHcCAQEEIP0pKFycEBq/Ni+ZHDktdahCYFm8UnFnBXLEvaGpRCAxoAoGCCqGSM49 -AwEHoUQDQgAEIN0ZqE/X7JvEH6W+Z6VcVpZYiT/GIuWpNdrP2f4GvtZYKkeYrhXD -idn1+qYo+jGWUwmCdbo0yKmpDYwmy3/BnQ== ------END EC PRIVATE KEY-----)"; - -const char *ECC_PUBLIC_PEM = R"(-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIN0ZqE/X7JvEH6W+Z6VcVpZYiT/G -IuWpNdrP2f4GvtZYKkeYrhXDidn1+qYo+jGWUwmCdbo0yKmpDYwmy3/BnQ== ------END PUBLIC KEY-----)"; - -String bytesToHex(const std::vector &bytes) { - static const char *HEX_DIGITS = "0123456789ABCDEF"; - String out; +namespace { +std::string bytesToHex(const std::vector &bytes) { + static const char *hexDigits = "0123456789abcdef"; + std::string out; + out.reserve(bytes.size() * 2); for (uint8_t b : bytes) { - out += HEX_DIGITS[(b >> 4) & 0x0F]; - out += HEX_DIGITS[b & 0x0F]; + out.push_back(hexDigits[(b >> 4) & 0x0F]); + out.push_back(hexDigits[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)); -} - -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" - ); +const char *statusText(const CryptoStatusDetail &status) { + return status.message.empty() ? toString(status.code) : status.message.c_str(); } +} // namespace void setup() { Serial.begin(115200); - delay(200); + delay(1000); - // Tighten policy (PBKDF2 iterations >= 2048 by default here) - CryptoPolicy pol = ESPCrypto::policy(); - pol.minPbkdf2Iterations = 2048; - ESPCrypto::setPolicy(pol); + CryptoCaps caps = espcrypto::runtime::caps(); + Serial.printf("caps: sha=%d aes=%d gcm=%d\n", caps.shaAccel, caps.aesAccel, caps.aesGcmAccel); - logCaps(); + CryptoPolicy policy = espcrypto::policy::get(); + policy.minPbkdf2Iterations = 100000; + espcrypto::policy::set(policy); - // 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( + std::vector key = {0x6b, 0x65, 0x79}; + std::vector msg = {'d', 'a', 't', 'a'}; + auto hmac = espcrypto::kdf::hmac( ShaVariant::SHA256, - CryptoSpan(key.raw()), + CryptoSpan(key), CryptoSpan(msg) ); - Serial.printf( - "HMAC-SHA256: %s (status=%s)\n", - bytesToHex(hmac.value).c_str(), - statusText(hmac.status).c_str() - ); + Serial.printf("hmac: %s\n", hmac.ok() ? bytesToHex(hmac.value).c_str() : statusText(hmac.status)); - // 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( + auto hkdf = espcrypto::kdf::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, + CryptoSpan(key), + CryptoSpan(msg), + CryptoSpan(), 32 ); - Serial.printf( - "PBKDF2: %s (status=%s)\n", - bytesToHex(pbkdf2.value).c_str(), - statusText(pbkdf2.status).c_str() - ); + Serial.printf("hkdf: %s\n", hkdf.ok() ? bytesToHex(hkdf.value).c_str() : statusText(hkdf.status)); - // 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); + auto calibrated = espcrypto::password::calibrateIterations(); 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() + "calibrated iterations: %lu\n", + calibrated.ok() ? static_cast(calibrated.value) : 0UL ); - // 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() - ); + SecureText secret("top-secret"); + Serial.printf("secure text size: %u\n", static_cast(secret.size())); - // 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() - ); + std::vector ctrKey(16, 0x22); + std::vector counter(16, 0x00); + std::vector streamInput = {'s', 't', 'r', 'e', 'a', 'm'}; + auto ctrOut = espcrypto::symmetric::aesCtrCrypt(ctrKey, counter, streamInput); + if (!ctrOut.ok()) { + Serial.printf("ctr encrypt failed: %s\n", statusText(ctrOut.status)); + return; + } + auto ctrPlain = espcrypto::symmetric::aesCtrCrypt(ctrKey, counter, ctrOut.value); + if (!ctrPlain.ok()) { + Serial.printf("ctr decrypt failed: %s\n", statusText(ctrPlain.status)); + return; + } + std::string recovered(reinterpret_cast(ctrPlain.value.data()), ctrPlain.value.size()); + Serial.printf("ctr recovered: %s\n", recovered.c_str()); } void loop() { - 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 c94c884..db02f77 100644 --- a/examples/basic_hash_and_aes/basic_hash_and_aes.ino +++ b/examples/basic_hash_and_aes/basic_hash_and_aes.ino @@ -1,62 +1,58 @@ #include #include +#include #include -String bytesToHex(const std::vector &bytes) { - static const char *HEX_DIGITS = "0123456789ABCDEF"; - String out; +namespace { +std::string bytesToHex(const std::vector &bytes) { + static const char *hexDigits = "0123456789abcdef"; + std::string out; + out.reserve(bytes.size() * 2); for (uint8_t b : bytes) { - out += HEX_DIGITS[(b >> 4) & 0x0F]; - out += HEX_DIGITS[b & 0x0F]; + out.push_back(hexDigits[(b >> 4) & 0x0F]); + out.push_back(hexDigits[b & 0x0F]); } return out; } +} // namespace void setup() { Serial.begin(115200); - delay(200); + delay(1000); - // 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()); + std::string message = "ESPCrypto"; + std::string digest = espcrypto::hash::shaHex(message); + Serial.printf("SHA-256(%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 key(32, 0x11); std::vector plaintext = {'h', 'e', 'l', 'l', 'o'}; - auto encrypted = ESPCrypto::aesGcmEncryptAuto(key, plaintext); + auto encrypted = espcrypto::symmetric::aesGcmEncryptAuto(key, plaintext); if (!encrypted.ok()) { - Serial.printf("GCM encrypt failed: %s\n", toString(encrypted.status.code)); - ESPCrypto::deinit(); + Serial.printf("encrypt failed: %s\n", encrypted.status.message.c_str()); + espcrypto::runtime::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( + Serial.printf("IV : %s\n", bytesToHex(encrypted.value.iv).c_str()); + Serial.printf("TAG : %s\n", bytesToHex(encrypted.value.tag).c_str()); + + auto decrypted = espcrypto::symmetric::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(); + Serial.printf("decrypt failed: %s\n", decrypted.status.message.c_str()); + espcrypto::runtime::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(); + std::string clear(reinterpret_cast(decrypted.value.data()), decrypted.value.size()); + Serial.printf("Plaintext recovered: %s\n", clear.c_str()); + espcrypto::runtime::deinit(); } void loop() { - vTaskDelay(pdMS_TO_TICKS(1000)); } diff --git a/examples/bench_crypto/bench_crypto.ino b/examples/bench_crypto/bench_crypto.ino index cf25e40..f888ddf 100644 --- a/examples/bench_crypto/bench_crypto.ino +++ b/examples/bench_crypto/bench_crypto.ino @@ -1,43 +1,35 @@ #include #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)); +#include + +void setup() { + Serial.begin(115200); + delay(1000); + + std::vector data(256, 0x5A); + uint8_t digest[32] = {0}; + uint32_t started = millis(); + for (int i = 0; i < 100; ++i) { + espcrypto::hash::sha(CryptoSpan(data), CryptoSpan(digest, sizeof(digest))); } - uint32_t elapsed = millis() - start; - Serial.printf("SHA256 x200 of 1KB: %ums\n", elapsed); -} + Serial.printf("100 sha rounds: %lu ms\n", static_cast(millis() - started)); -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 key(16, 0x11); + std::vector iv(12, 0x22); + std::vector ciphertext(data.size(), 0); std::vector tag(16, 0); - uint32_t start = millis(); + started = millis(); for (int i = 0; i < 50; ++i) { - ESPCrypto::aesGcmEncrypt( + espcrypto::symmetric::aesGcmEncrypt( key, CryptoSpan(iv), - CryptoSpan(plaintext), + CryptoSpan(data), 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.printf("50 aes-gcm rounds: %lu ms\n", static_cast(millis() - started)); } void loop() { diff --git a/examples/esp_idf_core_smoke/CMakeLists.txt b/examples/esp_idf_core_smoke/CMakeLists.txt new file mode 100644 index 0000000..c7415ac --- /dev/null +++ b/examples/esp_idf_core_smoke/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.16) + +set(EXTRA_COMPONENT_DIRS "${CMAKE_CURRENT_LIST_DIR}/../..") + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(espcrypto_core_smoke) diff --git a/examples/esp_idf_core_smoke/main/CMakeLists.txt b/examples/esp_idf_core_smoke/main/CMakeLists.txt new file mode 100644 index 0000000..eec9b53 --- /dev/null +++ b/examples/esp_idf_core_smoke/main/CMakeLists.txt @@ -0,0 +1 @@ +idf_component_register(SRCS "main.cpp" INCLUDE_DIRS ".") diff --git a/examples/esp_idf_core_smoke/main/main.cpp b/examples/esp_idf_core_smoke/main/main.cpp new file mode 100644 index 0000000..75f85a0 --- /dev/null +++ b/examples/esp_idf_core_smoke/main/main.cpp @@ -0,0 +1,10 @@ +#include + +#include + +#include + +extern "C" void app_main(void) { + const std::string digest = espcrypto::hash::shaHex("esp-idf"); + ESP_LOGI("espcrypto", "sha256=%s", digest.c_str()); +} diff --git a/examples/jwks_rotation/jwks_rotation.ino b/examples/jwks_rotation/jwks_rotation.ino index 74bfb8b..a4e7757 100644 --- a/examples/jwks_rotation/jwks_rotation.ino +++ b/examples/jwks_rotation/jwks_rotation.ino @@ -1,38 +1,49 @@ #include #include #include +#include + +#include + +namespace { +const char *statusText(const CryptoStatusDetail &status) { + return status.message.empty() ? toString(status.code) : status.message.c_str(); +} +} // namespace void setup() { 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" - - 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"; + claims["scope"] = "rotation"; + JwtSignOptions sign; sign.algorithm = JwtAlgorithm::HS256; - sign.keyId = "k2"; - String token = ESPCrypto::createJwt(claims, "moresecret", sign); + sign.keyId = "current"; + sign.issuer = "jwks"; + + auto token = espcrypto::jwt::create(claims, "moresecret", sign); + if (!token.ok()) { + Serial.printf("create failed: %s\n", statusText(token.status)); + return; + } + + JsonDocument jwks; + JsonArray keys = jwks["keys"].to(); + JsonObject current = keys.add(); + current["kid"] = "current"; + current["kty"] = "oct"; + current["alg"] = "HS256"; + current["k"] = "bW9yZXNlY3JldA"; JsonDocument decoded; - auto res = ESPCrypto::verifyJwtWithJwks(token, jwks, decoded); - Serial.printf("JWKS verify with rotation (kid=k2) ok? %s\n", res.ok() ? "yes" : "no"); + JwtVerifyOptions verify; + verify.algorithm = JwtAlgorithm::HS256; + verify.issuer = "jwks"; + + auto res = espcrypto::jwt::verifyWithJwks(token.value, jwks, decoded, verify); + Serial.printf("jwks verify: %s\n", res.ok() ? "ok" : statusText(res.status)); } void loop() { diff --git a/examples/jwt_and_password/jwt_and_password.ino b/examples/jwt_and_password/jwt_and_password.ino index 33bde6f..82ce7f6 100644 --- a/examples/jwt_and_password/jwt_and_password.ino +++ b/examples/jwt_and_password/jwt_and_password.ino @@ -1,54 +1,57 @@ #include +#include #include +#include -String toFriendly(const CryptoStatusDetail &status) { - if (status.ok()) { - return "ok"; - } - if (status.message.length() > 0) { - return status.message; - } - return String(toString(status.code)); +#include + +namespace { +const char *statusText(const CryptoStatusDetail &status) { + return status.message.empty() ? toString(status.code) : status.message.c_str(); } +} // namespace void setup() { Serial.begin(115200); - delay(200); + delay(1000); - // JWT creation/verification (HS256) JsonDocument claims; - claims["role"] = "admin"; + claims["device"] = "esp32"; + claims["scope"] = "demo"; + JwtSignOptions sign; sign.algorithm = JwtAlgorithm::HS256; - sign.issuer = "esp32"; + sign.issuer = "example"; 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()); + auto token = espcrypto::jwt::create(claims, "super-secret", sign); + if (!token.ok()) { + Serial.printf("token create failed: %s\n", statusText(token.status)); return; } - Serial.printf("JWT: %s\n", tokenResult.value.c_str()); + Serial.printf("token: %s\n", token.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()); + verify.issuer = "example"; + + auto verified = espcrypto::jwt::verify(token.value, "super-secret", decoded, verify); + Serial.printf( + "jwt verify: %s\n", + verified.ok() ? "ok" : statusText(verified.status) + ); + + auto hashed = espcrypto::password::hash("hunter2"); + if (!hashed.ok()) { + Serial.printf("hash failed: %s\n", statusText(hashed.status)); + return; } + Serial.printf("password hash: %s\n", hashed.value.c_str()); - // 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"); + auto ok = espcrypto::password::verify("hunter2", hashed.value); + Serial.printf("password verify: %s\n", ok.ok() ? "ok" : statusText(ok.status)); } void loop() { - 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 881ed18..917b072 100644 --- a/examples/keys_and_streaming/keys_and_streaming.ino +++ b/examples/keys_and_streaming/keys_and_streaming.ino @@ -1,90 +1,78 @@ #include #include -MemoryKeyStore memoryStore; +#include +#include -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( +namespace { +const char *statusText(const CryptoStatusDetail &status) { + return status.message.empty() ? toString(status.code) : status.message.c_str(); +} +} // namespace + +void setup() { + Serial.begin(115200); + delay(1000); + + MemoryKeyStore memoryStore; + KeyHandle handle{"demo-key", 1}; + std::vector rawKey(32, 0x44); + + auto stored = espcrypto::keystore::store( memoryStore, handle, - CryptoSpan(reinterpret_cast(pem), strlen(pem)) + CryptoSpan(rawKey) ); - 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()); - } -} + Serial.printf("store key: %s\n", stored.ok() ? "ok" : statusText(stored.status)); + + auto loaded = espcrypto::keystore::load(memoryStore, handle, KeyFormat::Raw, KeyKind::Symmetric); + Serial.printf("load key: %s\n", loaded.ok() ? "ok" : statusText(loaded.status)); -void demoStreaming() { - // Streaming SHA256 - ShaCtx shaCtx; - shaCtx.begin(ShaVariant::SHA256); - shaCtx.update(CryptoSpan(reinterpret_cast("hello "), 6)); - shaCtx.update(CryptoSpan(reinterpret_cast("world"), 5)); + ShaCtx sha; 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(); + sha.begin(ShaVariant::SHA256); + sha.update(CryptoSpan(reinterpret_cast("hello"), 5)); + sha.update(CryptoSpan(reinterpret_cast(" world"), 6)); + sha.finish(CryptoSpan(digest, sizeof(digest))); + Serial.println("streaming sha complete"); - // 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 aesKey(16, 0x33); + std::vector iv(12, 0x11); + std::vector aad = {'a', 'a', 'd'}; + std::vector plaintext = {'c', 'h', 'u', 'n', 'k'}; std::vector ciphertext(plaintext.size(), 0); std::vector tag(16, 0); + std::vector decrypted(plaintext.size(), 0); 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, + auto encStart = enc.beginEncrypt(aesKey, CryptoSpan(iv), CryptoSpan(aad)); + auto encUpdate = enc.update(CryptoSpan(plaintext), CryptoSpan(ciphertext)); + auto encFinish = enc.finish(CryptoSpan(tag)); + auto decStart = dec.beginDecrypt( + aesKey, CryptoSpan(iv), - CryptoSpan(), + CryptoSpan(aad), CryptoSpan(tag) ); - dec.update(CryptoSpan(ciphertext), CryptoSpan(decrypted)); - auto decStatus = dec.finish(CryptoSpan(tag)); + auto decUpdate = dec.update(CryptoSpan(ciphertext), CryptoSpan(decrypted)); + auto decFinish = dec.finish(CryptoSpan(tag)); + Serial.printf( - "AES-GCM streaming decrypt ok? %s\n", - decStatus.ok() && ESPCrypto::constantTimeEq(plaintext, decrypted) ? "yes" : "no" + "streaming gcm: %s\n", + (encStart.ok() && encUpdate.ok() && encFinish.ok() && decStart.ok() && decUpdate.ok() && + decFinish.ok() && espcrypto::runtime::constantTimeEq(plaintext, decrypted)) + ? "ok" + : "failed" ); -} -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()); -} + GcmNonceOptions nonceOptions; + nonceOptions.strategy = GcmNonceStrategy::Counter64_Random32; + auto message = espcrypto::symmetric::aesGcmEncryptAuto(aesKey, plaintext, {}, 12, nonceOptions); + Serial.printf("auto nonce encrypt: %s\n", message.ok() ? "ok" : statusText(message.status)); -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"); + auto deviceKey = espcrypto::device::deriveKey("example", CryptoSpan(), 32); + Serial.printf("device key: %s\n", deviceKey.ok() ? "ok" : statusText(deviceKey.status)); } void loop() { diff --git a/idf_component.yml b/idf_component.yml new file mode 100644 index 0000000..a5c9dab --- /dev/null +++ b/idf_component.yml @@ -0,0 +1,2 @@ +dependencies: + bblanchon/arduinojson: "^7.4.2" diff --git a/library.json b/library.json index e1f9993..b8426d8 100644 --- a/library.json +++ b/library.json @@ -1,7 +1,7 @@ { "name": "ESPCrypto", - "version": "1.0.2", - "description": "Hardware-accelerated crypto helpers (SHA, AES, RSA/ECC, JWT, salted hashing) for ESP32", + "version": "2.0.0", + "description": "ESP32 crypto modules with std::string-based APIs for ESP-IDF and Arduino", "keywords": [ "esp32", "crypto", @@ -21,7 +21,8 @@ } ], "frameworks": [ - "arduino" + "arduino", + "espidf" ], "platforms": [ "espressif32" diff --git a/library.properties b/library.properties index 2f7fde6..9e612d9 100644 --- a/library.properties +++ b/library.properties @@ -1,9 +1,9 @@ name=ESPCrypto -version=1.0.2 +version=2.0.0 author=zekageri maintainer=zekageri -sentence=Hardware-accelerated crypto helpers (SHA, AES-GCM/CTR, RSA/ECC, JWT) for ESP32. -paragraph=Provides ergonomic wrappers that prefer ESP-IDF accelerators with software fallbacks, JWT helpers built on ArduinoJson v7, and salted password hashing similar to bcrypt. +sentence=ESP32 crypto modules with std::string-based APIs for Arduino and ESP-IDF. +paragraph=Provides hardware-aware SHA, AES, PK, JWT, password, and keystore helpers with a standard C++ API surface and ArduinoJson v7 for optional JWT support. category=Data Processing url=https://github.com/ESPToolKit/esp-crypto repository=https://github.com/ESPToolKit/esp-crypto.git diff --git a/src/esp_crypto/asymmetric.h b/src/esp_crypto/asymmetric.h new file mode 100644 index 0000000..16eadfb --- /dev/null +++ b/src/esp_crypto/asymmetric.h @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include "hash.h" +#include "types.h" + +namespace espcrypto::asymmetric { +CryptoResult> +rsaSign(std::string_view privateKeyPem, CryptoSpan data, ShaVariant variant); +CryptoResult rsaVerify( + std::string_view publicKeyPem, + CryptoSpan data, + CryptoSpan signature, + ShaVariant variant +); +CryptoResult> +rsaSign(const CryptoKey &privateKey, CryptoSpan data, ShaVariant variant); +CryptoResult rsaVerify( + const CryptoKey &publicKey, + CryptoSpan data, + CryptoSpan signature, + ShaVariant variant +); +CryptoResult> +eccSign(std::string_view privateKeyPem, CryptoSpan data, ShaVariant variant); +CryptoResult eccVerify( + std::string_view publicKeyPem, + CryptoSpan data, + CryptoSpan signature, + ShaVariant variant +); +CryptoResult> +eccSign(const CryptoKey &privateKey, CryptoSpan data, ShaVariant variant); +CryptoResult eccVerify( + const CryptoKey &publicKey, + CryptoSpan data, + CryptoSpan signature, + ShaVariant variant +); +CryptoResult> ecdsaDerToRaw(CryptoSpan der); +CryptoResult> ecdsaRawToDer(CryptoSpan raw); +CryptoResult> +x25519(CryptoSpan privateKey, CryptoSpan peerPublic); +} // namespace espcrypto::asymmetric diff --git a/src/esp_crypto/crypto_asymmetric.cpp b/src/esp_crypto/crypto_asymmetric.cpp index d57553b..b55dc0e 100644 --- a/src/esp_crypto/crypto_asymmetric.cpp +++ b/src/esp_crypto/crypto_asymmetric.cpp @@ -167,6 +167,7 @@ CryptoStatusDetail buildEcPemFromJwk( return makeStatus(CryptoStatus::Ok); } +#if ESPCRYPTO_HAS_ARDUINOJSON CryptoResult jwkToKey(const JsonObjectConst &jwk) { CryptoResult result; const char *kty = jwk["kty"].as(); @@ -226,6 +227,7 @@ CryptoResult jwkToKey(const JsonObjectConst &jwk) { result.status = makeStatus(CryptoStatus::Unsupported, "kty unsupported"); return result; } +#endif bool pkParsePublicOrPrivate( mbedtls_pk_context &pk, @@ -451,34 +453,11 @@ bool pkVerifyInternal( return ok; } -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, +namespace espcrypto::asymmetric { +CryptoResult> rsaSign( + std::string_view privateKeyPem, + CryptoSpan data, 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)) { @@ -486,7 +465,7 @@ CryptoResult> ESPCrypto::rsaSign( return result; } if (!pkSignInternal( - privateKeyPem, + std::string(privateKeyPem), MBEDTLS_PK_RSA, variant, data.data(), @@ -501,8 +480,8 @@ CryptoResult> ESPCrypto::rsaSign( return result; } -CryptoResult ESPCrypto::rsaVerify( - const std::string &publicKeyPem, +CryptoResult rsaVerify( + std::string_view publicKeyPem, CryptoSpan data, CryptoSpan signature, ShaVariant variant @@ -513,7 +492,7 @@ CryptoResult ESPCrypto::rsaVerify( return result; } if (!pkVerifyInternal( - publicKeyPem, + std::string(publicKeyPem), MBEDTLS_PK_RSA, variant, data.data(), @@ -527,7 +506,7 @@ CryptoResult ESPCrypto::rsaVerify( return result; } -CryptoResult> ESPCrypto::rsaSign( +CryptoResult> rsaSign( const CryptoKey &privateKey, CryptoSpan data, ShaVariant variant ) { CryptoResult> result; @@ -541,7 +520,7 @@ CryptoResult> ESPCrypto::rsaSign( return result; } if (!pkSignContext( - privateKey.pk->ctx, + pkContext(privateKey), MBEDTLS_PK_RSA, variant, data.data(), @@ -556,7 +535,7 @@ CryptoResult> ESPCrypto::rsaSign( return result; } -CryptoResult ESPCrypto::rsaVerify( +CryptoResult rsaVerify( const CryptoKey &publicKey, CryptoSpan data, CryptoSpan signature, @@ -574,7 +553,7 @@ CryptoResult ESPCrypto::rsaVerify( } std::vector sigVec(signature.data(), signature.data() + signature.size()); if (!pkVerifyContext( - publicKey.pk->ctx, + pkContext(publicKey), MBEDTLS_PK_RSA, variant, data.data(), @@ -588,34 +567,10 @@ CryptoResult ESPCrypto::rsaVerify( 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, +CryptoResult> eccSign( + std::string_view privateKeyPem, + CryptoSpan data, 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)) { @@ -623,7 +578,7 @@ CryptoResult> ESPCrypto::eccSign( return result; } if (!pkSignInternal( - privateKeyPem, + std::string(privateKeyPem), MBEDTLS_PK_ECKEY, variant, data.data(), @@ -638,8 +593,8 @@ CryptoResult> ESPCrypto::eccSign( return result; } -CryptoResult ESPCrypto::eccVerify( - const std::string &publicKeyPem, +CryptoResult eccVerify( + std::string_view publicKeyPem, CryptoSpan data, CryptoSpan signature, ShaVariant variant @@ -650,7 +605,7 @@ CryptoResult ESPCrypto::eccVerify( return result; } if (!pkVerifyInternal( - publicKeyPem, + std::string(publicKeyPem), MBEDTLS_PK_ECKEY, variant, data.data(), @@ -664,7 +619,7 @@ CryptoResult ESPCrypto::eccVerify( return result; } -CryptoResult> ESPCrypto::eccSign( +CryptoResult> eccSign( const CryptoKey &privateKey, CryptoSpan data, ShaVariant variant ) { CryptoResult> result; @@ -678,7 +633,7 @@ CryptoResult> ESPCrypto::eccSign( return result; } if (!pkSignContext( - privateKey.pk->ctx, + pkContext(privateKey), MBEDTLS_PK_ECKEY, variant, data.data(), @@ -693,7 +648,7 @@ CryptoResult> ESPCrypto::eccSign( return result; } -CryptoResult ESPCrypto::eccVerify( +CryptoResult eccVerify( const CryptoKey &publicKey, CryptoSpan data, CryptoSpan signature, @@ -711,7 +666,7 @@ CryptoResult ESPCrypto::eccVerify( } std::vector sigVec(signature.data(), signature.data() + signature.size()); if (!pkVerifyContext( - publicKey.pk->ctx, + pkContext(publicKey), MBEDTLS_PK_ECKEY, variant, data.data(), @@ -725,16 +680,16 @@ CryptoResult ESPCrypto::eccVerify( return result; } -CryptoResult> ESPCrypto::ecdsaDerToRaw(CryptoSpan der) { +CryptoResult> ecdsaDerToRaw(CryptoSpan der) { return ecdsaDerToRawInternal(der); } -CryptoResult> ESPCrypto::ecdsaRawToDer(CryptoSpan raw) { +CryptoResult> ecdsaRawToDer(CryptoSpan raw) { return ecdsaRawToDerInternal(raw); } CryptoResult> -ESPCrypto::x25519(CryptoSpan privateKey, CryptoSpan peerPublic) { +x25519(CryptoSpan privateKey, CryptoSpan peerPublic) { CryptoResult> result; #if defined(MBEDTLS_ECP_DP_CURVE25519_ENABLED) if (privateKey.size() != 32 || peerPublic.size() != 32) { @@ -792,25 +747,4 @@ ESPCrypto::x25519(CryptoSpan privateKey, CryptoSpan> -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; -} +} // namespace espcrypto::asymmetric diff --git a/src/esp_crypto/crypto_core.cpp b/src/esp_crypto/crypto_core.cpp index 5713a2e..936ea47 100644 --- a/src/esp_crypto/crypto_core.cpp +++ b/src/esp_crypto/crypto_core.cpp @@ -104,6 +104,19 @@ uint32_t currentTimeSeconds(uint32_t overrideValue) { #endif } +uint64_t monotonicMillis() { +#if defined(ESP_PLATFORM) + return static_cast(esp_timer_get_time() / 1000ULL); +#else + return static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch() + ) + .count() + ); +#endif +} + std::string base64Encode(const uint8_t *data, size_t length, Base64Alphabet alphabet) { if (length == 0) { return std::string(); @@ -180,7 +193,7 @@ bool base64Decode(const std::string &input, Base64Alphabet alphabet, std::vector } void fillRandom(uint8_t *data, size_t length) { -#if defined(ESP_PLATFORM) +#if ESPCRYPTO_HAS_ESP_RANDOM esp_fill_random(data, length); #else std::random_device rd; @@ -201,6 +214,16 @@ bool constantTimeEquals(CryptoSpan a, CryptoSpan b return diff == 0; } +struct CryptoKey::PkCache { + mbedtls_pk_context ctx; + bool hasKey = false; + bool isPrivate = false; +}; + +mbedtls_pk_context &pkContext(const CryptoKey &key) { + return key.pk->ctx; +} + CryptoKey::CryptoKey() = default; CryptoKey::CryptoKey(const CryptoKey &other) @@ -449,14 +472,14 @@ void SecureBuffer::resize(size_t bytes) { buffer.assign(bytes, 0); } -SecureString::SecureString(std::string value) : value(std::move(value)) { +SecureText::SecureText(std::string value) : value(std::move(value)) { } -SecureString::SecureString(SecureString &&other) noexcept : value(std::move(other.value)) { +SecureText::SecureText(SecureText &&other) noexcept : value(std::move(other.value)) { other.wipe(); } -SecureString &SecureString::operator=(SecureString &&other) noexcept { +SecureText &SecureText::operator=(SecureText &&other) noexcept { if (this != &other) { wipe(); value = std::move(other.value); @@ -465,35 +488,38 @@ SecureString &SecureString::operator=(SecureString &&other) noexcept { return *this; } -SecureString::~SecureString() { +SecureText::~SecureText() { wipe(); } -void SecureString::wipe() { +void SecureText::wipe() { if (!value.empty()) { secureZero(&value[0], value.size()); value.clear(); } } -void ESPCrypto::setPolicy(const CryptoPolicy &policy) { +namespace espcrypto::policy { +void set(const CryptoPolicy &policy) { mutablePolicy() = policy; markRuntimeInitialized(); } -CryptoPolicy ESPCrypto::policy() { +CryptoPolicy get() { return mutablePolicy(); } +} // namespace espcrypto::policy -void ESPCrypto::deinit() { +namespace espcrypto::runtime { +void deinit() { resetRuntimeState(); } -bool ESPCrypto::isInitialized() { +bool isInitialized() { return runtimeState().initialized.load(std::memory_order_acquire); } -CryptoCaps ESPCrypto::caps() { +CryptoCaps caps() { CryptoCaps c; c.shaAccel = ESPCRYPTO_SHA_ACCEL; c.aesAccel = ESPCRYPTO_AES_ACCEL; @@ -501,10 +527,11 @@ CryptoCaps ESPCrypto::caps() { return c; } -bool ESPCrypto::constantTimeEq(const std::vector &a, const std::vector &b) { +bool constantTimeEq(const std::vector &a, const std::vector &b) { return constantTimeEquals(CryptoSpan(a), CryptoSpan(b)); } -bool ESPCrypto::constantTimeEq(CryptoSpan a, CryptoSpan b) { +bool constantTimeEq(CryptoSpan a, CryptoSpan b) { return constantTimeEquals(a, b); } +} // namespace espcrypto::runtime diff --git a/src/esp_crypto/crypto_hash_kdf.cpp b/src/esp_crypto/crypto_hash_kdf.cpp index 66da85d..5cff027 100644 --- a/src/esp_crypto/crypto_hash_kdf.cpp +++ b/src/esp_crypto/crypto_hash_kdf.cpp @@ -65,117 +65,133 @@ bool tryHardwareSha(ShaVariant variant, const uint8_t *data, size_t length, uint #endif } +struct ShaCtx::Impl { + const mbedtls_md_info_t *info = nullptr; + mbedtls_md_context_t ctx; + bool started = false; +}; + ShaCtx::ShaCtx() { - mbedtls_md_init(&ctx); + impl = new Impl(); + mbedtls_md_init(&impl->ctx); } ShaCtx::~ShaCtx() { - mbedtls_md_free(&ctx); + mbedtls_md_free(&impl->ctx); + delete impl; } 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; + mbedtls_md_free(&impl->ctx); + mbedtls_md_init(&impl->ctx); + impl->started = false; + impl->info = nullptr; - info = mdInfoForVariant(variant); - if (!info) { + impl->info = mdInfoForVariant(variant); + if (!impl->info) { return makeStatus(CryptoStatus::InvalidInput, "invalid sha variant"); } - if (mbedtls_md_setup(&ctx, info, 0) != 0) { + if (mbedtls_md_setup(&impl->ctx, impl->info, 0) != 0) { return makeStatus(CryptoStatus::InternalError, "md setup failed"); } - if (mbedtls_md_starts(&ctx) != 0) { + if (mbedtls_md_starts(&impl->ctx) != 0) { return makeStatus(CryptoStatus::InternalError, "md start failed"); } - started = true; + impl->started = true; return makeStatus(CryptoStatus::Ok); } CryptoStatusDetail ShaCtx::update(CryptoSpan data) { - if (!started) { + if (!impl->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) { + if (mbedtls_md_update(&impl->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) { + if (!impl->started || !impl->info) { return makeStatus(CryptoStatus::InvalidInput, "sha not started"); } - size_t need = mbedtls_md_get_size(info); + size_t need = mbedtls_md_get_size(impl->info); if (out.size() < need) { return makeStatus(CryptoStatus::BufferTooSmall, "digest buffer too small"); } - if (mbedtls_md_finish(&ctx, out.data()) != 0) { + if (mbedtls_md_finish(&impl->ctx, out.data()) != 0) { return makeStatus(CryptoStatus::InternalError, "md finish failed"); } - started = false; + impl->started = false; return makeStatus(CryptoStatus::Ok); } +struct HmacCtx::Impl { + const mbedtls_md_info_t *info = nullptr; + mbedtls_md_context_t ctx; + bool started = false; +}; + HmacCtx::HmacCtx() { - mbedtls_md_init(&ctx); + impl = new Impl(); + mbedtls_md_init(&impl->ctx); } HmacCtx::~HmacCtx() { - mbedtls_md_free(&ctx); + mbedtls_md_free(&impl->ctx); + delete impl; } 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; + mbedtls_md_free(&impl->ctx); + mbedtls_md_init(&impl->ctx); + impl->started = false; + impl->info = nullptr; - info = mdInfoForVariant(variant); - if (!info || key.empty()) { + impl->info = mdInfoForVariant(variant); + if (!impl->info || key.empty()) { return makeStatus(CryptoStatus::InvalidInput, "invalid hmac params"); } - if (mbedtls_md_setup(&ctx, info, 1) != 0) { + if (mbedtls_md_setup(&impl->ctx, impl->info, 1) != 0) { return makeStatus(CryptoStatus::InternalError, "md setup failed"); } - if (mbedtls_md_hmac_starts(&ctx, key.data(), key.size()) != 0) { + if (mbedtls_md_hmac_starts(&impl->ctx, key.data(), key.size()) != 0) { return makeStatus(CryptoStatus::InternalError, "hmac start failed"); } - started = true; + impl->started = true; return makeStatus(CryptoStatus::Ok); } CryptoStatusDetail HmacCtx::update(CryptoSpan data) { - if (!started) { + if (!impl->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) { + if (mbedtls_md_hmac_update(&impl->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) { + if (!impl->started || !impl->info) { return makeStatus(CryptoStatus::InvalidInput, "hmac not started"); } - size_t need = mbedtls_md_get_size(info); + size_t need = mbedtls_md_get_size(impl->info); if (out.size() < need) { return makeStatus(CryptoStatus::BufferTooSmall, "digest buffer too small"); } - if (mbedtls_md_hmac_finish(&ctx, out.data()) != 0) { + if (mbedtls_md_hmac_finish(&impl->ctx, out.data()) != 0) { return makeStatus(CryptoStatus::InternalError, "hmac finish failed"); } - started = false; + impl->started = false; return makeStatus(CryptoStatus::Ok); } @@ -272,12 +288,15 @@ int pbkdf2Sha256( #endif } -bool parsePasswordHash( - const std::string &encoded, - uint8_t &cost, - std::vector &salt, - std::vector &hash -) { +namespace { +struct ParsedPasswordHash { + uint32_t version = 0; + uint32_t iterations = 0; + std::vector salt; + std::vector hash; +}; + +bool parsePasswordHash(const std::string &encoded, ParsedPasswordHash &parsed) { std::vector parts; size_t start = 0; while (start <= encoded.size()) { @@ -289,35 +308,96 @@ bool parsePasswordHash( parts.push_back(encoded.substr(start, pos - start)); start = pos + 1; } - if (parts.size() != 6 || parts[1] != "esphash" || parts[2] != "v1") { + if (parts.size() != 6 || parts[1] != "esphash") { return false; } - const std::string &costPart = parts[3]; - if (costPart.empty()) { + const std::string &versionPart = parts[2]; + const std::string &iterationPart = parts[3]; + if (iterationPart.empty()) { return false; } - uint32_t parsedCost = 0; - for (char ch : costPart) { + uint32_t parsedNumber = 0; + for (char ch : iterationPart) { if (ch < '0' || ch > '9') { return false; } - parsedCost = parsedCost * 10u + static_cast(ch - '0'); - if (parsedCost > 31u) { + parsedNumber = parsedNumber * 10u + static_cast(ch - '0'); + if (parsedNumber > 1000000000u) { return false; } } - cost = static_cast(parsedCost); - if (!base64Decode(parts[4], Base64Alphabet::Standard, salt)) { + if (versionPart == "v1") { + if (parsedNumber > 31u) { + return false; + } + parsed.version = 1; + parsed.iterations = 1u << parsedNumber; + } else if (versionPart == "v2") { + if (parsedNumber == 0) { + return false; + } + parsed.version = 2; + parsed.iterations = parsedNumber; + } else { return false; } - if (!base64Decode(parts[5], Base64Alphabet::Standard, hash)) { + if (!base64Decode(parts[4], Base64Alphabet::Standard, parsed.salt)) { + return false; + } + if (!base64Decode(parts[5], Base64Alphabet::Standard, parsed.hash)) { return false; } return true; } -CryptoResult> -ESPCrypto::shaResult(CryptoSpan data, const ShaOptions &options) { +uint32_t passwordIterationFloor(const PasswordHashOptions &options) { + markRuntimeInitialized(); + const CryptoPolicy &cryptoPolicy = mutablePolicy(); + return std::max(options.minIterations, cryptoPolicy.minPbkdf2Iterations); +} + +CryptoResult> derivePbkdf2( + std::string_view password, + CryptoSpan salt, + uint32_t iterations, + size_t outputLength, + bool enforcePolicy +) { + CryptoResult> result; + if (password.empty() || salt.empty() || outputLength == 0) { + result.status = makeStatus(CryptoStatus::InvalidInput, "missing password/salt/len"); + return result; + } + markRuntimeInitialized(); + const CryptoPolicy &cryptoPolicy = mutablePolicy(); + if (enforcePolicy && !cryptoPolicy.allowLegacy && iterations < cryptoPolicy.minPbkdf2Iterations) { + result.status = makeStatus(CryptoStatus::PolicyViolation, "iterations below policy"); + return result; + } + result.value.assign(outputLength, 0); + int ret = pbkdf2Sha256( + reinterpret_cast(password.data()), + password.size(), + 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; +} + +} // namespace + +namespace espcrypto::hash { +CryptoResult> sha(CryptoSpan data, const ShaOptions &options) { CryptoResult> result; if (!data.data() && data.size() > 0) { result.status = makeStatus(CryptoStatus::InvalidInput, "null data"); @@ -352,8 +432,11 @@ ESPCrypto::shaResult(CryptoSpan data, const ShaOptions &options) return result; } -CryptoResult -ESPCrypto::sha(CryptoSpan data, CryptoSpan out, const ShaOptions &options) { +CryptoResult sha( + CryptoSpan data, + CryptoSpan out, + const ShaOptions &options +) { CryptoResult result; size_t needed = digestLength(options.variant); if (needed == 0) { @@ -364,7 +447,7 @@ ESPCrypto::sha(CryptoSpan data, CryptoSpan out, const Sh result.status = makeStatus(CryptoStatus::BufferTooSmall, "digest buffer too small"); return result; } - auto hashed = shaResult(data, options); + auto hashed = sha(data, options); if (!hashed.ok()) { result.status = hashed.status; return result; @@ -374,66 +457,96 @@ ESPCrypto::sha(CryptoSpan data, CryptoSpan out, const Sh 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(); -} - -std::vector ESPCrypto::sha(const std::vector &data, const ShaOptions &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(); +std::string shaHex(CryptoSpan data, const ShaOptions &options) { + auto digest = sha(data, options); + if (!digest.ok()) { + return std::string(); } static const char *HEX_DIGITS = "0123456789abcdef"; std::string hex; - hex.reserve(digest.size() * 2); - for (uint8_t b : digest) { + hex.reserve(digest.value.size() * 2); + for (uint8_t b : digest.value) { 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); + return hex; } -String ESPCrypto::hashString(const String &input, const PasswordHashOptions &options) { - auto result = hashStringResult(input, options); - return result.ok() ? result.value : String(); +std::string shaHex(std::string_view text, const ShaOptions &options) { + return shaHex( + CryptoSpan( + reinterpret_cast(text.data()), + text.size() + ), + options + ); } +} // namespace espcrypto::hash -bool ESPCrypto::verifyString(const String &input, const String &encoded) { - auto result = verifyStringResult(input, encoded); - return result.ok(); +namespace espcrypto::password { +CryptoResult calibrateIterations(const PasswordHashOptions &options) { + CryptoResult result; + if (options.saltBytes == 0 || options.outputBytes == 0 || options.targetMillis == 0) { + result.status = makeStatus(CryptoStatus::InvalidInput, "invalid calibration options"); + return result; + } + std::vector salt(options.saltBytes, 0xA5); + std::vector derived(options.outputBytes, 0); + const std::string probePassword = "espcrypto-calibration"; + uint32_t probeIterations = 4096; + uint64_t elapsedMs = 0; + for (;;) { + uint64_t startedMs = monotonicMillis(); + int ret = pbkdf2Sha256( + reinterpret_cast(probePassword.data()), + probePassword.size(), + salt.data(), + salt.size(), + probeIterations, + derived.data(), + derived.size() + ); + elapsedMs = std::max(1, monotonicMillis() - startedMs); + secureZero(derived.data(), derived.size()); + if (ret != 0) { + result.status = makeStatus(CryptoStatus::InternalError, "pbkdf2 calibration failed"); + return result; + } + if (elapsedMs >= 20 || probeIterations >= (1u << 22)) { + break; + } + probeIterations *= 4; + } + uint64_t projected = + (static_cast(probeIterations) * static_cast(options.targetMillis)) / + elapsedMs; + uint32_t floorIterations = passwordIterationFloor(options); + result.value = static_cast(std::min(projected, UINT32_MAX)); + result.value = std::max(result.value, floorIterations); + result.status = makeStatus(CryptoStatus::Ok); + return result; } -CryptoResult -ESPCrypto::hashStringResult(const String &input, const PasswordHashOptions &options) { - CryptoResult result; - if (input.length() == 0 || options.saltBytes == 0 || options.outputBytes == 0) { +CryptoResult hash(std::string_view input, const PasswordHashOptions &options) { + CryptoResult result; + if (input.empty() || 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 &cryptoPolicy = mutablePolicy(); - if (!cryptoPolicy.allowLegacy && iterations < cryptoPolicy.minPbkdf2Iterations) { - uint8_t adjustedCost = cost; - while ((1u << adjustedCost) < cryptoPolicy.minPbkdf2Iterations && adjustedCost < 31) { - adjustedCost++; + uint32_t iterations = options.iterations; + if (iterations == 0) { + auto calibrated = calibrateIterations(options); + if (!calibrated.ok()) { + result.status = calibrated.status; + return result; } - cost = adjustedCost; - iterations = 1u << cost; + iterations = calibrated.value; } - auto derived = pbkdf2(input, CryptoSpan(salt), iterations, options.outputBytes); + iterations = std::max(iterations, passwordIterationFloor(options)); + auto derived = + derivePbkdf2(input, CryptoSpan(salt), iterations, options.outputBytes, false); if (!derived.ok()) { result.status = derived.status; return result; @@ -446,48 +559,49 @@ ESPCrypto::hashStringResult(const String &input, const PasswordHashOptions &opti 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.value = + "$esphash$v2$" + std::to_string(iterations) + "$" + saltB64 + "$" + hashB64; result.status = makeStatus(CryptoStatus::Ok); return result; } -CryptoResult ESPCrypto::verifyStringResult(const String &input, const String &encoded) { +CryptoResult +verify(std::string_view input, std::string_view encoded, const PasswordVerifyOptions &options) { CryptoResult result; - if (input.length() == 0 || encoded.length() == 0) { + if (input.empty() || encoded.empty()) { 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)) { + ParsedPasswordHash parsed; + if (!parsePasswordHash(std::string(encoded), parsed)) { result.status = makeStatus(CryptoStatus::DecodeError, "invalid esphash envelope"); return result; } - if (salt.empty() || hash.empty()) { + if (parsed.salt.empty() || parsed.hash.empty()) { result.status = makeStatus(CryptoStatus::DecodeError, "invalid esphash parts"); return result; } - if (cost > 31) { - result.status = makeStatus(CryptoStatus::DecodeError, "invalid esphash envelope"); - return result; - } - uint32_t iterations = 1u << cost; - markRuntimeInitialized(); - const CryptoPolicy &cryptoPolicy = mutablePolicy(); - if (!cryptoPolicy.allowLegacy && iterations < cryptoPolicy.minPbkdf2Iterations) { - result.status = makeStatus(CryptoStatus::PolicyViolation, "pbkdf2 iterations below policy"); + uint32_t floorIterations = passwordIterationFloor(PasswordHashOptions{}); + if ((parsed.version == 1 || parsed.iterations < floorIterations) && !options.allowLegacy) { + result.status = makeStatus( + CryptoStatus::PolicyViolation, + "password hash requires explicit legacy compatibility" + ); return result; } - auto derived = pbkdf2(input, CryptoSpan(salt), iterations, hash.size()); + auto derived = derivePbkdf2( + input, + CryptoSpan(parsed.salt), + parsed.iterations, + parsed.hash.size(), + false + ); if (!derived.ok()) { result.status = derived.status; return result; } bool match = constantTimeEquals( - CryptoSpan(hash), + CryptoSpan(parsed.hash), CryptoSpan(derived.value) ); secureZero(derived.value.data(), derived.value.size()); @@ -495,9 +609,11 @@ CryptoResult ESPCrypto::verifyStringResult(const String &input, const Stri : makeStatus(CryptoStatus::VerifyFailed, "hash mismatch"); return result; } +} // namespace espcrypto::password +namespace espcrypto::kdf { CryptoResult> -ESPCrypto::hmac(ShaVariant variant, CryptoSpan key, CryptoSpan data) { +hmac(ShaVariant variant, CryptoSpan key, CryptoSpan data) { CryptoResult> result; const mbedtls_md_info_t *info = mdInfoForVariant(variant); if (!info) { @@ -532,7 +648,7 @@ ESPCrypto::hmac(ShaVariant variant, CryptoSpan key, CryptoSpan> ESPCrypto::hkdf( +CryptoResult> hkdf( ShaVariant variant, CryptoSpan salt, CryptoSpan ikm, @@ -575,11 +691,8 @@ CryptoResult> ESPCrypto::hkdf( 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) - ); + auto block = + hmac(variant, CryptoSpan(prk.value), CryptoSpan(blockInput)); secureZero(blockInput.data(), blockInput.size()); if (!block.ok()) { secureZero(prk.value.data(), prk.value.size()); @@ -596,36 +709,12 @@ CryptoResult> ESPCrypto::hkdf( return result; } -CryptoResult> ESPCrypto::pbkdf2( - const String &password, CryptoSpan salt, uint32_t iterations, size_t outputLength +CryptoResult> pbkdf2( + std::string_view 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 &cryptoPolicy = mutablePolicy(); - if (!cryptoPolicy.allowLegacy && iterations < cryptoPolicy.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; + return derivePbkdf2(password, salt, iterations, outputLength, true); } +} // namespace espcrypto::kdf diff --git a/src/esp_crypto/crypto_jwt.cpp b/src/esp_crypto/crypto_jwt.cpp index e72863f..6f3d0aa 100644 --- a/src/esp_crypto/crypto_jwt.cpp +++ b/src/esp_crypto/crypto_jwt.cpp @@ -1,7 +1,9 @@ #include "internal/crypto_internal.h" +#if ESPCRYPTO_HAS_ARDUINOJSON + CryptoResult -selectJwkFromSet(const JsonDocument &jwks, const String &kid, JwtAlgorithm algHint) { +selectJwkFromSet(const JsonDocument &jwks, const std::string &kid, JwtAlgorithm algHint) { CryptoResult result; JsonArrayConst keys = jwks["keys"].as(); if (keys.isNull()) { @@ -11,7 +13,7 @@ selectJwkFromSet(const JsonDocument &jwks, const String &kid, JwtAlgorithm algHi for (JsonVariantConst v : keys) { JsonObjectConst jwk = v.as(); const char *jwkKid = jwk["kid"].as(); - if (kid.length() > 0 && (!jwkKid || kid != jwkKid)) { + if (!kid.empty() && (!jwkKid || kid != jwkKid)) { continue; } const char *algStr = jwk["alg"].as(); @@ -26,10 +28,10 @@ selectJwkFromSet(const JsonDocument &jwks, const String &kid, JwtAlgorithm algHi } result.status = parsed.status; } - if (kid.length() > 0) { + if (!kid.empty()) { result.status = makeStatus(CryptoStatus::DecodeError, "kid not found"); } else if (!result.status.ok()) { - // Keep last parse error + // Keep last parse error. } else { result.status = makeStatus(CryptoStatus::DecodeError, "no jwk matched"); } @@ -96,7 +98,10 @@ bool verifySignature( if (!hmacSha256(key, data, length, expected)) { return false; } - return constantTimeEquals(expected, signature); + return constantTimeEquals( + CryptoSpan(expected), + CryptoSpan(signature) + ); } case JwtAlgorithm::RS256: return pkVerifyInternal(key, MBEDTLS_PK_RSA, ShaVariant::SHA256, data, length, signature); @@ -108,34 +113,13 @@ bool verifySignature( } } -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 +namespace espcrypto::jwt { +CryptoResult create( + const JsonDocument &claims, + std::string_view key, + const JwtSignOptions &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; + CryptoResult result; if (key.empty()) { result.status = makeStatus(CryptoStatus::InvalidInput, "key missing"); return result; @@ -148,20 +132,22 @@ CryptoResult ESPCrypto::createJwtResult( } header["alg"] = algName.c_str(); header["typ"] = "JWT"; - if (options.keyId.length() > 0) { + if (!options.keyId.empty()) { header["kid"] = options.keyId.c_str(); } + JsonDocument payload; payload.set(claims); - if (options.issuer.length() > 0 && payload["iss"].isNull()) { + if (!options.issuer.empty() && payload["iss"].isNull()) { payload["iss"] = options.issuer.c_str(); } - if (options.subject.length() > 0 && payload["sub"].isNull()) { + if (!options.subject.empty() && payload["sub"].isNull()) { payload["sub"] = options.subject.c_str(); } - if (options.audience.length() > 0 && payload["aud"].isNull()) { + if (!options.audience.empty() && payload["aud"].isNull()) { payload["aud"] = options.audience.c_str(); } + uint32_t now = currentTimeSeconds( options.currentTimestamp != 0 ? options.currentTimestamp : options.issuedAt ); @@ -203,11 +189,12 @@ CryptoResult ESPCrypto::createJwtResult( result.status = makeStatus(CryptoStatus::DecodeError, "base64 encode failed"); return result; } + std::string signingInput = encodedHeader + "." + encodedPayload; std::vector signature; if (!signJwt( options.algorithm, - key, + std::string(key), reinterpret_cast(signingInput.data()), signingInput.size(), signature @@ -215,32 +202,34 @@ CryptoResult ESPCrypto::createJwtResult( 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.value = signingInput + "." + encodedSignature; result.status = makeStatus(CryptoStatus::Ok); return result; } -CryptoResult ESPCrypto::verifyJwtResult( - const String &token, - const std::string &key, +CryptoResult verify( + std::string_view token, + std::string_view key, JsonDocument &outClaims, const JwtVerifyOptions &options ) { CryptoResult result; - if (token.length() == 0 || key.empty()) { + if (token.empty() || key.empty()) { result.status = makeStatus(CryptoStatus::InvalidInput, "token or key missing"); return result; } - std::string tokenStd(token.c_str(), token.length()); + + std::string tokenStd(token); 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); @@ -253,6 +242,7 @@ CryptoResult ESPCrypto::verifyJwtResult( result.status = makeStatus(CryptoStatus::DecodeError, "base64 decode failed"); return result; } + JsonDocument headerDoc; if (deserializeJson(headerDoc, headerBytes.data(), headerBytes.size()) != DeserializationError::Ok) { @@ -265,6 +255,7 @@ CryptoResult ESPCrypto::verifyJwtResult( 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) { @@ -275,13 +266,13 @@ CryptoResult ESPCrypto::verifyJwtResult( 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; - } + if (!options.expectedTyp.empty() && (!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) { @@ -289,7 +280,9 @@ CryptoResult ESPCrypto::verifyJwtResult( bool allowed = name && std::any_of( options.criticalHeadersAllowed.begin(), options.criticalHeadersAllowed.end(), - [&](const String &allowedName) { return allowedName == name; } + [&](const std::string &allowedName) { + return allowedName == name; + } ); if (!allowed) { result.status = @@ -301,10 +294,11 @@ CryptoResult ESPCrypto::verifyJwtResult( result.status = makeStatus(CryptoStatus::PolicyViolation, "crit header not allowed"); return result; } + std::string signingInput = headerPart + "." + payloadPart; if (!verifySignature( alg, - key, + std::string(key), reinterpret_cast(signingInput.data()), signingInput.size(), signatureBytes @@ -312,6 +306,7 @@ CryptoResult ESPCrypto::verifyJwtResult( 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(); @@ -328,30 +323,33 @@ CryptoResult ESPCrypto::verifyJwtResult( 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) { + if (!options.audience.empty() && options.audience == aud) { return true; } if (std::any_of( options.audiences.begin(), options.audiences.end(), - [&](const String &allowedAudience) { return allowedAudience == aud; } + [&](const std::string &allowedAudience) { return allowedAudience == aud; } )) { return true; } - return options.audience.length() == 0 && options.audiences.empty(); + return options.audience.empty() && options.audiences.empty(); }; - if (options.audience.length() > 0 || !options.audiences.empty()) { + + if (!options.audience.empty() || !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) + if (ok) { break; + } } } else { ok = audMatch(payloadDoc["aud"].as()); @@ -361,58 +359,68 @@ CryptoResult ESPCrypto::verifyJwtResult( return result; } } - if (options.issuer.length() > 0) { + + if (!options.issuer.empty()) { 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, +CryptoResult verifyWithJwks( + std::string_view token, const JsonDocument &jwks, JsonDocument &outClaims, const JwtVerifyOptions &options ) { CryptoResult result; - if (token.length() == 0) { + if (token.empty()) { result.status = makeStatus(CryptoStatus::InvalidInput, "token missing"); return result; } - std::string tokenStd(token.c_str(), token.length()); + + std::string tokenStd(token); 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); + auto keyRes = selectJwkFromSet(jwks, kid ? std::string(kid) : std::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 verify(token, keyStr, outClaims, options); } +} // namespace espcrypto::jwt + +#endif diff --git a/src/esp_crypto/crypto_storage.cpp b/src/esp_crypto/crypto_storage.cpp index 5b5b43a..94224d8 100644 --- a/src/esp_crypto/crypto_storage.cpp +++ b/src/esp_crypto/crypto_storage.cpp @@ -1,17 +1,16 @@ #include "internal/crypto_internal.h" std::string handleKeyString(const KeyHandle &handle) { - std::string alias(handle.alias.c_str(), handle.alias.length()); - if (alias.empty()) { + if (handle.alias.empty()) { return std::string(); } - return alias + ":" + std::to_string(handle.version); + return handle.alias + ":" + std::to_string(handle.version); } -bool ensureNvsReady(const String &partition) { +bool ensureNvsReady(const std::string &partition) { #if defined(ESP_PLATFORM) GlobalRuntimeState &state = runtimeState(); - auto it = state.nvsInitMap.find(partition.c_str()); + auto it = state.nvsInitMap.find(partition); if (it != state.nvsInitMap.end() && it->second) { markRuntimeInitialized(); return true; @@ -22,7 +21,7 @@ bool ensureNvsReady(const String &partition) { err = nvs_flash_init_partition(partition.c_str()); } bool ok = (err == ESP_OK); - state.nvsInitMap[partition.c_str()] = ok; + state.nvsInitMap[partition] = ok; if (ok) { markRuntimeInitialized(); } @@ -34,7 +33,12 @@ bool ensureNvsReady(const String &partition) { } uint64_t -loadCounterFromNvs(const String &ns, const String &partition, const std::string &key, bool &found) { +loadCounterFromNvs( + const std::string &ns, + const std::string &partition, + const std::string &key, + bool &found +) { found = false; uint64_t value = 0; #if defined(ESP_PLATFORM) @@ -59,7 +63,10 @@ loadCounterFromNvs(const String &ns, const String &partition, const std::string } void storeCounterToNvs( - const String &ns, const String &partition, const std::string &key, uint64_t value + const std::string &ns, + const std::string &partition, + const std::string &key, + uint64_t value ) { #if defined(ESP_PLATFORM) if (!ensureNvsReady(partition)) { @@ -115,7 +122,7 @@ CryptoStatusDetail MemoryKeyStore::remove(const KeyHandle &handle) { return makeStatus(CryptoStatus::Ok); } -NvsKeyStore::NvsKeyStore(String ns, String partition) +NvsKeyStore::NvsKeyStore(std::string ns, std::string partition) : ns(std::move(ns)), partition(std::move(partition)) { } @@ -131,8 +138,8 @@ CryptoStatusDetail NvsKeyStore::ensureInit() const { #endif } -String NvsKeyStore::makeKeyName(const KeyHandle &handle) const { - return String(handleKeyString(handle).c_str()); +std::string NvsKeyStore::makeKeyName(const KeyHandle &handle) const { + return handleKeyString(handle); } CryptoResult> NvsKeyStore::load(const KeyHandle &handle) { @@ -233,25 +240,25 @@ CryptoStatusDetail NvsKeyStore::remove(const KeyHandle &handle) { #endif } -LittleFsKeyStore::LittleFsKeyStore(String basePath) : basePath(std::move(basePath)) { +LittleFsKeyStore::LittleFsKeyStore(std::string basePath) : basePath(std::move(basePath)) { } -String LittleFsKeyStore::makePath(const KeyHandle &handle) const { +std::string LittleFsKeyStore::makePath(const KeyHandle &handle) const { std::string name = handleKeyString(handle); if (name.empty()) { - return String(); + return std::string(); } - if (basePath.endsWith("/")) { - return basePath + name.c_str(); + if (!basePath.empty() && basePath.back() == '/') { + return basePath + name; } - return basePath + "/" + name.c_str(); + return basePath + "/" + name; } CryptoResult> LittleFsKeyStore::load(const KeyHandle &handle) { CryptoResult> result; #if ESPCRYPTO_HAS_LITTLEFS - String path = makePath(handle); - if (path.length() == 0) { + std::string path = makePath(handle); + if (path.empty()) { result.status = makeStatus(CryptoStatus::InvalidInput, "alias missing"); return result; } @@ -259,7 +266,7 @@ CryptoResult> LittleFsKeyStore::load(const KeyHandle &handl result.status = makeStatus(CryptoStatus::InternalError, "littlefs mount failed"); return result; } - File f = LittleFS.open(path, "r"); + File f = LittleFS.open(path.c_str(), "r"); if (!f) { result.status = makeStatus(CryptoStatus::DecodeError, "key missing"); return result; @@ -283,17 +290,17 @@ CryptoResult> LittleFsKeyStore::load(const KeyHandle &handl CryptoStatusDetail LittleFsKeyStore::store(const KeyHandle &handle, CryptoSpan key) { #if ESPCRYPTO_HAS_LITTLEFS - String path = makePath(handle); - if (path.length() == 0 || key.empty()) { + std::string path = makePath(handle); + if (path.empty() || 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); + if (!LittleFS.exists(basePath.c_str())) { + LittleFS.mkdir(basePath.c_str()); } - File f = LittleFS.open(path, "w"); + File f = LittleFS.open(path.c_str(), "w"); if (!f) { return makeStatus(CryptoStatus::InternalError, "open failed"); } @@ -312,14 +319,14 @@ CryptoStatusDetail LittleFsKeyStore::store(const KeyHandle &handle, CryptoSpan &seed, const DeviceKeyO return makeStatus(CryptoStatus::Ok); } -CryptoResult> ESPCrypto::deriveDeviceKey( - const String &purpose, +namespace espcrypto::device { +CryptoResult> deriveKey( + std::string_view purpose, CryptoSpan contextInfo, size_t length, const DeviceKeyOptions &options ) { CryptoResult> result; - if (purpose.length() == 0 || length == 0) { + if (purpose.empty() || length == 0) { result.status = makeStatus(CryptoStatus::InvalidInput, "purpose/length missing"); return result; } @@ -406,7 +414,7 @@ CryptoResult> ESPCrypto::deriveDeviceKey( if (!contextInfo.empty()) { info.insert(info.end(), contextInfo.data(), contextInfo.data() + contextInfo.size()); } - auto derived = hkdf( + auto derived = espcrypto::kdf::hkdf( ShaVariant::SHA256, CryptoSpan(deviceSalt), CryptoSpan(seed), @@ -423,8 +431,10 @@ CryptoResult> ESPCrypto::deriveDeviceKey( result.status = makeStatus(CryptoStatus::Ok); return result; } +} // namespace espcrypto::device -CryptoResult ESPCrypto::storeKey( +namespace espcrypto::keystore { +CryptoResult store( KeyStore &store, const KeyHandle &handle, CryptoSpan keyMaterial ) { CryptoResult result; @@ -434,7 +444,7 @@ CryptoResult ESPCrypto::storeKey( } CryptoResult -ESPCrypto::loadKey(KeyStore &store, const KeyHandle &handle, KeyFormat format, KeyKind kind) { +load(KeyStore &store, const KeyHandle &handle, KeyFormat format, KeyKind kind) { CryptoResult result; auto loaded = store.load(handle); if (!loaded.ok()) { @@ -462,8 +472,9 @@ ESPCrypto::loadKey(KeyStore &store, const KeyHandle &handle, KeyFormat format, K return result; } -CryptoResult ESPCrypto::removeKey(KeyStore &store, const KeyHandle &handle) { +CryptoResult remove(KeyStore &store, const KeyHandle &handle) { CryptoResult result; result.status = store.remove(handle); return result; } +} // namespace espcrypto::keystore diff --git a/src/esp_crypto/crypto_symmetric.cpp b/src/esp_crypto/crypto_symmetric.cpp index af1b46f..ebd25cb 100644 --- a/src/esp_crypto/crypto_symmetric.cpp +++ b/src/esp_crypto/crypto_symmetric.cpp @@ -1,15 +1,23 @@ #include "internal/crypto_internal.h" +struct AesCtrStream::Impl { + mbedtls_aes_context ctx; + unsigned char counter[16] = {0}; + unsigned char streamBlock[16] = {0}; + size_t offset = 0; + bool started = false; +}; + AesCtrStream::AesCtrStream() { - mbedtls_aes_init(&ctx); - memset(counter, 0, sizeof(counter)); - memset(streamBlock, 0, sizeof(streamBlock)); + impl = new Impl(); + mbedtls_aes_init(&impl->ctx); } AesCtrStream::~AesCtrStream() { - mbedtls_aes_free(&ctx); - mbedtls_platform_zeroize(counter, sizeof(counter)); - mbedtls_platform_zeroize(streamBlock, sizeof(streamBlock)); + mbedtls_aes_free(&impl->ctx); + mbedtls_platform_zeroize(impl->counter, sizeof(impl->counter)); + mbedtls_platform_zeroize(impl->streamBlock, sizeof(impl->streamBlock)); + delete impl; } CryptoStatusDetail @@ -17,18 +25,18 @@ AesCtrStream::begin(const std::vector &key, CryptoSpan n 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) { + if (mbedtls_aes_setkey_enc(&impl->ctx, key.data(), key.size() * 8) != 0) { return makeStatus(CryptoStatus::InternalError, "aes setkey failed"); } - memcpy(counter, nonceCounter.data(), 16); - offset = 0; - started = true; + memcpy(impl->counter, nonceCounter.data(), 16); + impl->offset = 0; + impl->started = true; return makeStatus(CryptoStatus::Ok); } CryptoStatusDetail AesCtrStream::update(CryptoSpan input, CryptoSpan output) { - if (!started) { + if (!impl->started) { return makeStatus(CryptoStatus::InvalidInput, "ctr not started"); } if (output.size() < input.size()) { @@ -37,17 +45,17 @@ AesCtrStream::update(CryptoSpan input, CryptoSpan output if (input.empty()) { return makeStatus(CryptoStatus::Ok); } - size_t offCopy = offset; + size_t offCopy = impl->offset; int ret = mbedtls_aes_crypt_ctr( - &ctx, + &impl->ctx, input.size(), &offCopy, - counter, - streamBlock, + impl->counter, + impl->streamBlock, input.data(), output.data() ); - offset = offCopy; + impl->offset = offCopy; return ret == 0 ? makeStatus(CryptoStatus::Ok) : makeStatus(CryptoStatus::InternalError, "ctr update failed"); } @@ -100,13 +108,22 @@ static int gcmFinishCompat(mbedtls_gcm_context &ctx, CryptoSpan tagOut) #endif } +struct AesGcmCtx::Impl { + bool decrypt = false; + bool started = false; + mbedtls_gcm_context ctx; + std::vector tagVerify; +}; + AesGcmCtx::AesGcmCtx() { - mbedtls_gcm_init(&ctx); + impl = new Impl(); + mbedtls_gcm_init(&impl->ctx); } AesGcmCtx::~AesGcmCtx() { - mbedtls_gcm_free(&ctx); - mbedtls_platform_zeroize(tagVerify.data(), tagVerify.size()); + mbedtls_gcm_free(&impl->ctx); + mbedtls_platform_zeroize(impl->tagVerify.data(), impl->tagVerify.size()); + delete impl; } CryptoStatusDetail AesGcmCtx::beginCommon( @@ -124,20 +141,20 @@ CryptoStatusDetail AesGcmCtx::beginCommon( 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()); + impl->decrypt = decryptMode; + if (impl->decrypt) { + impl->tagVerify.assign(tag.data(), tag.data() + tag.size()); } else { - tagVerify.clear(); + impl->tagVerify.clear(); } - if (mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key.data(), key.size() * 8) != 0) { + if (mbedtls_gcm_setkey(&impl->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) { + int mode = impl->decrypt ? MBEDTLS_GCM_DECRYPT : MBEDTLS_GCM_ENCRYPT; + if (gcmStartsCompat(impl->ctx, mode, iv, aad) != 0) { return makeStatus(CryptoStatus::InternalError, "gcm start failed"); } - started = true; + impl->started = true; return makeStatus(CryptoStatus::Ok); } @@ -160,7 +177,7 @@ CryptoStatusDetail AesGcmCtx::beginDecrypt( } CryptoStatusDetail AesGcmCtx::update(CryptoSpan input, CryptoSpan output) { - if (!started) { + if (!impl->started) { return makeStatus(CryptoStatus::InvalidInput, "gcm not started"); } if (output.size() < input.size()) { @@ -169,32 +186,33 @@ CryptoStatusDetail AesGcmCtx::update(CryptoSpan input, CryptoSpan if (input.empty()) { return makeStatus(CryptoStatus::Ok); } - if (gcmUpdateCompat(ctx, input, output) != 0) { + if (gcmUpdateCompat(impl->ctx, input, output) != 0) { return makeStatus(CryptoStatus::InternalError, "gcm update failed"); } return makeStatus(CryptoStatus::Ok); } CryptoStatusDetail AesGcmCtx::finish(CryptoSpan tagOut) { - if (!started) { + if (!impl->started) { return makeStatus(CryptoStatus::InvalidInput, "gcm not started"); } - started = false; - if (!decrypt) { + impl->started = false; + if (!impl->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) { + if (gcmFinishCompat(impl->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) { + if (gcmFinishCompat(impl->ctx, CryptoSpan(computed)) != 0) { return makeStatus(CryptoStatus::InternalError, "gcm finish failed"); } bool ok = constantTimeEquals( - CryptoSpan(tagVerify), + CryptoSpan(impl->tagVerify), CryptoSpan(computed) ); mbedtls_platform_zeroize(computed.data(), computed.size()); @@ -563,54 +581,8 @@ CryptoStatusDetail aesGcmDecryptInternal( ); } -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( +namespace espcrypto::symmetric { +CryptoResult aesGcmEncryptAuto( const std::vector &key, const std::vector &plaintext, const std::vector &aad, @@ -708,7 +680,7 @@ CryptoResult ESPCrypto::aesGcmEncryptAuto( return result; } -CryptoResult> ESPCrypto::aesGcmDecrypt( +CryptoResult> aesGcmDecrypt( const std::vector &key, const std::vector &iv, const std::vector &ciphertext, @@ -723,7 +695,7 @@ CryptoResult> ESPCrypto::aesGcmDecrypt( return result; } -CryptoResult ESPCrypto::aesGcmEncrypt( +CryptoResult aesGcmEncrypt( const std::vector &key, CryptoSpan iv, CryptoSpan plaintext, @@ -747,7 +719,7 @@ CryptoResult ESPCrypto::aesGcmEncrypt( return result; } -CryptoResult ESPCrypto::aesGcmDecrypt( +CryptoResult aesGcmDecrypt( const std::vector &key, CryptoSpan iv, CryptoSpan ciphertext, @@ -765,7 +737,7 @@ CryptoResult ESPCrypto::aesGcmDecrypt( return result; } -CryptoResult> ESPCrypto::aesCtrCrypt( +CryptoResult> aesCtrCrypt( const std::vector &key, const std::vector &nonceCounter, const std::vector &input @@ -794,7 +766,7 @@ CryptoResult> ESPCrypto::aesCtrCrypt( return result; } -CryptoResult> ESPCrypto::chacha20Poly1305Encrypt( +CryptoResult> chacha20Poly1305Encrypt( CryptoSpan key, CryptoSpan nonce, CryptoSpan aad, @@ -841,7 +813,7 @@ CryptoResult> ESPCrypto::chacha20Poly1305Encrypt( return result; } -CryptoResult> ESPCrypto::chacha20Poly1305Decrypt( +CryptoResult> chacha20Poly1305Decrypt( CryptoSpan key, CryptoSpan nonce, CryptoSpan aad, @@ -891,7 +863,7 @@ CryptoResult> ESPCrypto::chacha20Poly1305Decrypt( return result; } -CryptoResult> ESPCrypto::xchacha20Poly1305Encrypt( +CryptoResult> xchacha20Poly1305Encrypt( CryptoSpan key, CryptoSpan nonce, CryptoSpan aad, @@ -906,7 +878,7 @@ CryptoResult> ESPCrypto::xchacha20Poly1305Encrypt( return result; } -CryptoResult> ESPCrypto::xchacha20Poly1305Decrypt( +CryptoResult> xchacha20Poly1305Decrypt( CryptoSpan key, CryptoSpan nonce, CryptoSpan aad, @@ -920,3 +892,4 @@ CryptoResult> ESPCrypto::xchacha20Poly1305Decrypt( result.status = makeStatus(CryptoStatus::Unsupported, "xchacha20poly1305 unavailable"); return result; } +} // namespace espcrypto::symmetric diff --git a/src/esp_crypto/device_key.h b/src/esp_crypto/device_key.h new file mode 100644 index 0000000..1c29fb7 --- /dev/null +++ b/src/esp_crypto/device_key.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include "kdf.h" +#include "keystore.h" + +struct DeviceKeyOptions { + bool persistSeed = true; + std::string nvsNamespace = "espcrypto"; + std::string nvsPartition = "nvs"; + size_t seedBytes = 32; +}; + +namespace espcrypto::device { +CryptoResult> deriveKey( + std::string_view purpose, + CryptoSpan contextInfo = {}, + size_t length = 32, + const DeviceKeyOptions &options = DeviceKeyOptions{} +); +} // namespace espcrypto::device diff --git a/src/esp_crypto/ed25519.h b/src/esp_crypto/ed25519.h deleted file mode 100644 index a6efa34..0000000 --- a/src/esp_crypto/ed25519.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include -#include - -// 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 diff --git a/src/esp_crypto/esp_crypto.h b/src/esp_crypto/esp_crypto.h index 4ad027f..c4e73fc 100644 --- a/src/esp_crypto/esp_crypto.h +++ b/src/esp_crypto/esp_crypto.h @@ -1,794 +1,17 @@ #pragma once -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "mbedtls/aes.h" -#include "mbedtls/gcm.h" -#include "mbedtls/md.h" -#include "mbedtls/pk.h" - -#if __has_include() -#include -#endif - -#if defined(__cpp_lib_span) -#define ESPCRYPTO_HAS_STD_SPAN 1 -#elif __has_include() -#include -#define ESPCRYPTO_HAS_STD_SPAN 1 -#define ESPCRYPTO_USE_EXPERIMENTAL_SPAN 1 -#else -#define ESPCRYPTO_HAS_STD_SPAN 0 -#endif - -enum class CryptoStatus { - 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; - - bool ok() const { - return code == CryptoStatus::Ok; - } -}; - -template struct CryptoResult { - CryptoStatusDetail status; - T value; - - 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 *; - - 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 < - 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 < - 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) { - } +#include "asymmetric.h" +#include "device_key.h" +#include "hash.h" +#include "kdf.h" +#include "keystore.h" +#include "password.h" +#include "policy.h" +#include "runtime.h" +#include "stream.h" +#include "symmetric.h" +#include "types.h" + +#if __has_include() +#include "jwt.h" #endif -#if ESPCRYPTO_HAS_STD_SPAN -#if defined(ESPCRYPTO_USE_EXPERIMENTAL_SPAN) - CryptoSpan(std::experimental::span span) : ptr(span.data()), len(span.size()) { - } -#else - 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; - } - - private: - pointer ptr; - size_t len; -}; - -enum class KeyFormat { Raw, Pem, Der, Jwk }; - -enum class KeyKind { Auto, Public, Private, Symmetric }; - -struct KeyHandle { - 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; -}; - -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; -}; - -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; - - 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; -}; - -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; -}; - -struct DeviceKeyOptions { - bool persistSeed = true; - String nvsNamespace = "espcrypto"; - String nvsPartition = "nvs"; - size_t seedBytes = 32; -}; - -enum class ShaVariant { SHA256, SHA384, SHA512 }; - -struct ShaOptions { - ShaVariant variant = ShaVariant::SHA256; - bool preferHardware = true; -}; - -enum class GcmNonceStrategy { Random96, Counter64_Random32, BootCounter_Random32 }; - -struct GcmNonceOptions { - 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; -}; - -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; -}; - -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; -}; - -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; -}; - -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; -}; - -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 }; - -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; -}; - -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; -}; - -struct PasswordHashOptions { - 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; -}; - -struct CryptoCaps { - bool shaAccel = false; - bool aesAccel = false; - bool aesGcmAccel = false; -}; - -struct GcmMessage { - 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(); - // Content comparison is constant-time only when lengths already match. - // A length mismatch returns false immediately and should be treated as public metadata. - 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/src/esp_crypto/hash.h b/src/esp_crypto/hash.h new file mode 100644 index 0000000..868a4df --- /dev/null +++ b/src/esp_crypto/hash.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +#include "types.h" + +enum class ShaVariant { SHA256, SHA384, SHA512 }; + +struct ShaOptions { + ShaVariant variant = ShaVariant::SHA256; + bool preferHardware = true; +}; + +namespace espcrypto::hash { +CryptoResult> sha(CryptoSpan data, const ShaOptions &options = ShaOptions{}); +CryptoResult sha( + CryptoSpan data, + CryptoSpan out, + const ShaOptions &options = ShaOptions{} +); +std::string shaHex(CryptoSpan data, const ShaOptions &options = ShaOptions{}); +std::string shaHex(std::string_view text, const ShaOptions &options = ShaOptions{}); +} // namespace espcrypto::hash diff --git a/src/esp_crypto/internal/crypto_internal.h b/src/esp_crypto/internal/crypto_internal.h index 263e37a..b9333bb 100644 --- a/src/esp_crypto/internal/crypto_internal.h +++ b/src/esp_crypto/internal/crypto_internal.h @@ -2,9 +2,17 @@ #include "../esp_crypto.h" +#if __has_include() +#include "../jwt.h" +#define ESPCRYPTO_HAS_ARDUINOJSON 1 +#else +#define ESPCRYPTO_HAS_ARDUINOJSON 0 +#endif + #include #include #include +#include #include #include #include @@ -46,10 +54,40 @@ #define ESPCRYPTO_HAS_LITTLEFS 0 #endif -#if defined(ESP_PLATFORM) +#if (defined(ESP_PLATFORM) || defined(ARDUINO_ARCH_ESP32)) && __has_include("esp_system.h") +#define ESPCRYPTO_HAS_ESP_SYSTEM 1 +#else +#define ESPCRYPTO_HAS_ESP_SYSTEM 0 +#endif + +#if (defined(ESP_PLATFORM) || defined(ARDUINO_ARCH_ESP32)) && __has_include("esp_timer.h") +#define ESPCRYPTO_HAS_ESP_TIMER 1 +#else +#define ESPCRYPTO_HAS_ESP_TIMER 0 +#endif + +#if (defined(ESP_PLATFORM) || defined(ARDUINO_ARCH_ESP32)) && __has_include("esp_random.h") +#define ESPCRYPTO_HAS_ESP_RANDOM 1 +#else +#define ESPCRYPTO_HAS_ESP_RANDOM 0 +#endif + +#if ESPCRYPTO_HAS_ESP_SYSTEM || ESPCRYPTO_HAS_ESP_TIMER || ESPCRYPTO_HAS_ESP_RANDOM extern "C" { +#if ESPCRYPTO_HAS_ESP_SYSTEM #include "esp_system.h" +#endif +#if ESPCRYPTO_HAS_ESP_TIMER #include "esp_timer.h" +#endif +#if ESPCRYPTO_HAS_ESP_RANDOM +#include "esp_random.h" +#endif +} +#endif + +#if defined(ESP_PLATFORM) +extern "C" { #include "nvs.h" #include "nvs_flash.h" #if defined(__has_include) @@ -135,8 +173,10 @@ struct GlobalRuntimeState { std::atomic bootCounter{0}; }; +#if ESPCRYPTO_HAS_ARDUINOJSON std::string algorithmName(JwtAlgorithm alg); JwtAlgorithm algorithmFromName(const std::string &name); +#endif void secureZero(void *data, size_t length); CryptoPolicy &mutablePolicy(); @@ -153,6 +193,7 @@ bool tryHardwareSha(ShaVariant variant, const uint8_t *data, size_t length, uint std::string base64Encode(const uint8_t *data, size_t length, Base64Alphabet alphabet); bool base64Decode(const std::string &input, Base64Alphabet alphabet, std::vector &output); uint32_t currentTimeSeconds(uint32_t overrideValue); +uint64_t monotonicMillis(); void fillRandom(uint8_t *data, size_t length); bool constantTimeEquals(CryptoSpan a, CryptoSpan b); CryptoStatusDetail buildRsaPemFromJwk( @@ -164,15 +205,24 @@ CryptoStatusDetail buildEcPemFromJwk( const std::string &crv, std::string &outPem ); +#if ESPCRYPTO_HAS_ARDUINOJSON CryptoResult jwkToKey(const JsonObjectConst &jwk); -CryptoResult selectJwkFromSet(const JsonDocument &jwks, const String &kid, JwtAlgorithm algHint); +CryptoResult +selectJwkFromSet(const JsonDocument &jwks, const std::string &kid, JwtAlgorithm algHint); +#endif std::string handleKeyString(const KeyHandle &handle); -bool ensureNvsReady(const String &partition); +bool ensureNvsReady(const std::string &partition); uint64_t loadCounterFromNvs( - const String &ns, const String &partition, const std::string &key, bool &found + const std::string &ns, + const std::string &partition, + const std::string &key, + bool &found ); void storeCounterToNvs( - const String &ns, const String &partition, const std::string &key, uint64_t value + const std::string &ns, + const std::string &partition, + const std::string &key, + uint64_t value ); bool hmacSha256( const std::string &key, const uint8_t *data, size_t length, std::vector &out @@ -197,6 +247,7 @@ bool pkParsePublicOrPrivate( const mbedtls_entropy_context *entropy ); bool pkPolicyAllows(mbedtls_pk_context &pk, mbedtls_pk_type_t expected); +mbedtls_pk_context &pkContext(const CryptoKey &key); bool pkSignContext( mbedtls_pk_context &pk, mbedtls_pk_type_t expected, @@ -229,6 +280,7 @@ bool pkVerifyInternal( size_t length, const std::vector &signature ); +#if ESPCRYPTO_HAS_ARDUINOJSON bool signJwt( JwtAlgorithm alg, const std::string &key, @@ -243,6 +295,7 @@ bool verifySignature( size_t length, const std::vector &signature ); +#endif bool aesKeyValid(const std::vector &key); bool hardwareAesCtr( const std::vector &key, diff --git a/src/esp_crypto/jwt.h b/src/esp_crypto/jwt.h new file mode 100644 index 0000000..0528a16 --- /dev/null +++ b/src/esp_crypto/jwt.h @@ -0,0 +1,54 @@ +#pragma once + +#include + +#include +#include + +#include "asymmetric.h" + +enum class JwtAlgorithm { Auto, HS256, RS256, ES256 }; + +struct JwtSignOptions { + JwtAlgorithm algorithm = JwtAlgorithm::HS256; + std::string keyId; + std::string issuer; + std::string subject; + std::string audience; + uint32_t expiresInSeconds = 3600; + uint32_t notBefore = 0; + uint32_t issuedAt = 0; + uint32_t currentTimestamp = 0; +}; + +struct JwtVerifyOptions { + JwtAlgorithm algorithm = JwtAlgorithm::Auto; + std::string audience; + std::string issuer; + uint32_t currentTimestamp = 0; + bool requireExpiration = true; + uint32_t leewaySeconds = 0; + std::string expectedTyp; + std::vector audiences; + std::vector criticalHeadersAllowed; +}; + +namespace espcrypto::jwt { +CryptoResult create( + const JsonDocument &claims, + std::string_view key, + const JwtSignOptions &options = JwtSignOptions{} +); +CryptoResult verify( + std::string_view token, + std::string_view key, + JsonDocument &outClaims, + const JwtVerifyOptions &options = JwtVerifyOptions{} +); +CryptoResult verifyWithJwks( + std::string_view token, + const JsonDocument &jwks, + JsonDocument &outClaims, + const JwtVerifyOptions &options = JwtVerifyOptions{} +); +} // namespace espcrypto::jwt diff --git a/src/esp_crypto/kdf.h b/src/esp_crypto/kdf.h new file mode 100644 index 0000000..62d99f0 --- /dev/null +++ b/src/esp_crypto/kdf.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "hash.h" + +namespace espcrypto::kdf { +CryptoResult> +hmac(ShaVariant variant, CryptoSpan key, CryptoSpan data); +CryptoResult> hkdf( + ShaVariant variant, + CryptoSpan salt, + CryptoSpan ikm, + CryptoSpan info, + size_t length +); +CryptoResult> pbkdf2( + std::string_view password, + CryptoSpan salt, + uint32_t iterations, + size_t outputLength +); +} // namespace espcrypto::kdf diff --git a/src/esp_crypto/keystore.h b/src/esp_crypto/keystore.h new file mode 100644 index 0000000..b733e66 --- /dev/null +++ b/src/esp_crypto/keystore.h @@ -0,0 +1,68 @@ +#pragma once + +#include +#include + +#include "asymmetric.h" + +struct KeyHandle { + std::string alias; + uint32_t version = 0; +}; + +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; +}; + +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; + + private: + std::map> storage; +}; + +class NvsKeyStore : public KeyStore { + public: + NvsKeyStore(std::string ns = "espcrypto", std::string partition = "nvs"); + CryptoResult> load(const KeyHandle &handle) override; + CryptoStatusDetail store(const KeyHandle &handle, CryptoSpan key) override; + CryptoStatusDetail remove(const KeyHandle &handle) override; + + private: + CryptoStatusDetail ensureInit() const; + std::string makeKeyName(const KeyHandle &handle) const; + + std::string ns; + std::string partition; +}; + +class LittleFsKeyStore : public KeyStore { + public: + explicit LittleFsKeyStore(std::string basePath = "/keys"); + CryptoResult> load(const KeyHandle &handle) override; + CryptoStatusDetail store(const KeyHandle &handle, CryptoSpan key) override; + CryptoStatusDetail remove(const KeyHandle &handle) override; + + private: + std::string makePath(const KeyHandle &handle) const; + + std::string basePath; +}; + +namespace espcrypto::keystore { +CryptoResult store(KeyStore &store, const KeyHandle &handle, CryptoSpan keyMaterial); +CryptoResult load( + KeyStore &store, + const KeyHandle &handle, + KeyFormat format, + KeyKind kind = KeyKind::Auto +); +CryptoResult remove(KeyStore &store, const KeyHandle &handle); +} // namespace espcrypto::keystore diff --git a/src/esp_crypto/password.h b/src/esp_crypto/password.h new file mode 100644 index 0000000..1ce6bc1 --- /dev/null +++ b/src/esp_crypto/password.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include "kdf.h" + +struct PasswordHashOptions { + uint32_t iterations = 0; + size_t saltBytes = 16; + size_t outputBytes = 32; + uint32_t targetMillis = 250; + uint32_t minIterations = 100000; +}; + +struct PasswordVerifyOptions { + bool allowLegacy = false; +}; + +namespace espcrypto::password { +CryptoResult calibrateIterations(const PasswordHashOptions &options = PasswordHashOptions{}); +CryptoResult hash( + std::string_view input, + const PasswordHashOptions &options = PasswordHashOptions{} +); +CryptoResult verify( + std::string_view input, + std::string_view encoded, + const PasswordVerifyOptions &options = PasswordVerifyOptions{} +); +} // namespace espcrypto::password diff --git a/src/esp_crypto/policy.h b/src/esp_crypto/policy.h new file mode 100644 index 0000000..d7a8a6f --- /dev/null +++ b/src/esp_crypto/policy.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +struct CryptoPolicy { + size_t minRsaBits = 2048; + uint32_t minPbkdf2Iterations = 100000; + bool allowLegacy = false; + bool allowWeakCurves = false; + uint8_t minAesGcmIvBytes = 12; +}; + +struct CryptoCaps { + bool shaAccel = false; + bool aesAccel = false; + bool aesGcmAccel = false; +}; + +namespace espcrypto::policy { +void set(const CryptoPolicy &policy); +CryptoPolicy get(); +} // namespace espcrypto::policy diff --git a/src/esp_crypto/runtime.h b/src/esp_crypto/runtime.h new file mode 100644 index 0000000..e883786 --- /dev/null +++ b/src/esp_crypto/runtime.h @@ -0,0 +1,12 @@ +#pragma once + +#include "policy.h" +#include "types.h" + +namespace espcrypto::runtime { +void deinit(); +bool isInitialized(); +CryptoCaps caps(); +bool constantTimeEq(const std::vector &a, const std::vector &b); +bool constantTimeEq(CryptoSpan a, CryptoSpan b); +} // namespace espcrypto::runtime diff --git a/src/esp_crypto/stream.h b/src/esp_crypto/stream.h new file mode 100644 index 0000000..5caa452 --- /dev/null +++ b/src/esp_crypto/stream.h @@ -0,0 +1,85 @@ +#pragma once + +#include "hash.h" +#include "types.h" + +class ShaCtx { + public: + ShaCtx(); + ~ShaCtx(); + ShaCtx(const ShaCtx &) = delete; + ShaCtx &operator=(const ShaCtx &) = delete; + + CryptoStatusDetail begin(ShaVariant variant, bool preferHardware = true); + CryptoStatusDetail update(CryptoSpan data); + CryptoStatusDetail finish(CryptoSpan out); + + private: + struct Impl; + Impl *impl = nullptr; +}; + +class HmacCtx { + public: + HmacCtx(); + ~HmacCtx(); + HmacCtx(const HmacCtx &) = delete; + HmacCtx &operator=(const HmacCtx &) = delete; + + CryptoStatusDetail begin(ShaVariant variant, CryptoSpan key); + CryptoStatusDetail update(CryptoSpan data); + CryptoStatusDetail finish(CryptoSpan out); + + private: + struct Impl; + Impl *impl = nullptr; +}; + +class AesCtrStream { + public: + AesCtrStream(); + ~AesCtrStream(); + AesCtrStream(const AesCtrStream &) = delete; + AesCtrStream &operator=(const AesCtrStream &) = delete; + + CryptoStatusDetail begin(const std::vector &key, CryptoSpan nonceCounter); + CryptoStatusDetail update(CryptoSpan input, CryptoSpan output); + + private: + struct Impl; + Impl *impl = nullptr; +}; + +class AesGcmCtx { + public: + AesGcmCtx(); + ~AesGcmCtx(); + AesGcmCtx(const AesGcmCtx &) = delete; + AesGcmCtx &operator=(const AesGcmCtx &) = delete; + + 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: + CryptoStatusDetail beginCommon( + const std::vector &key, + CryptoSpan iv, + CryptoSpan aad, + bool decryptMode, + CryptoSpan tag + ); + + struct Impl; + Impl *impl = nullptr; +}; diff --git a/src/esp_crypto/symmetric.h b/src/esp_crypto/symmetric.h new file mode 100644 index 0000000..002987c --- /dev/null +++ b/src/esp_crypto/symmetric.h @@ -0,0 +1,81 @@ +#pragma once + +#include "policy.h" +#include "types.h" + +enum class GcmNonceStrategy { Random96, Counter64_Random32, BootCounter_Random32 }; + +struct GcmNonceOptions { + GcmNonceStrategy strategy = GcmNonceStrategy::Random96; + bool persistCounter = false; + std::string nvsNamespace = "espcrypto"; + std::string nvsPartition = "nvs"; +}; + +struct GcmMessage { + std::vector iv; + std::vector ciphertext; + std::vector tag; +}; + +namespace espcrypto::symmetric { +CryptoResult aesGcmEncryptAuto( + const std::vector &key, + const std::vector &plaintext, + const std::vector &aad = {}, + size_t ivLength = 12, + const GcmNonceOptions &nonceOptions = GcmNonceOptions{} +); +CryptoResult> aesGcmDecrypt( + const std::vector &key, + const std::vector &iv, + const std::vector &ciphertext, + const std::vector &tag, + const std::vector &aad = {} +); +CryptoResult aesGcmEncrypt( + const std::vector &key, + CryptoSpan iv, + CryptoSpan plaintext, + CryptoSpan ciphertextOut, + CryptoSpan tagOut, + CryptoSpan aad = {} +); +CryptoResult aesGcmDecrypt( + const std::vector &key, + CryptoSpan iv, + CryptoSpan ciphertext, + CryptoSpan tag, + CryptoSpan plaintextOut, + CryptoSpan aad = {} +); +CryptoResult> aesCtrCrypt( + const std::vector &key, + const std::vector &nonceCounter, + const std::vector &input +); +CryptoResult> chacha20Poly1305Encrypt( + CryptoSpan key, + CryptoSpan nonce, + CryptoSpan aad, + CryptoSpan plaintext +); +CryptoResult> chacha20Poly1305Decrypt( + CryptoSpan key, + CryptoSpan nonce, + CryptoSpan aad, + CryptoSpan ciphertextAndTag +); +CryptoResult> xchacha20Poly1305Encrypt( + CryptoSpan key, + CryptoSpan nonce, + CryptoSpan aad, + CryptoSpan plaintext +); +CryptoResult> xchacha20Poly1305Decrypt( + CryptoSpan key, + CryptoSpan nonce, + CryptoSpan aad, + CryptoSpan ciphertextAndTag +); +} // namespace espcrypto::symmetric diff --git a/src/esp_crypto/types.h b/src/esp_crypto/types.h new file mode 100644 index 0000000..091ca5b --- /dev/null +++ b/src/esp_crypto/types.h @@ -0,0 +1,248 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#if __has_include() +#include +#endif + +#if defined(__cpp_lib_span) +#define ESPCRYPTO_HAS_STD_SPAN 1 +#elif __has_include() +#include +#define ESPCRYPTO_HAS_STD_SPAN 1 +#define ESPCRYPTO_USE_EXPERIMENTAL_SPAN 1 +#else +#define ESPCRYPTO_HAS_STD_SPAN 0 +#endif + +enum class CryptoStatus { + 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; + std::string message; + + bool ok() const { + return code == CryptoStatus::Ok; + } +}; + +template struct CryptoResult { + CryptoStatusDetail status; + T value; + + 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 *; + + 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 < + 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 < + 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()) { + } +#else + 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; + } + + private: + pointer ptr; + size_t len; +}; + +enum class KeyFormat { Raw, Pem, Der, Jwk }; + +enum class KeyKind { Auto, Public, Private, Symmetric }; + +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; + bool parsed() const; + void clear(); + struct PkCache; + CryptoStatusDetail ensureParsedPk(bool requirePrivate) const; + + std::vector data; + KeyFormat format = KeyFormat::Raw; + KeyKind keyKind = KeyKind::Auto; + mutable PkCache *pk = nullptr; +}; + +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; +}; + +class SecureText { + public: + SecureText() = default; + explicit SecureText(std::string value); + SecureText(SecureText &&other) noexcept; + SecureText &operator=(SecureText &&other) noexcept; + SecureText(const SecureText &) = delete; + SecureText &operator=(const SecureText &) = delete; + ~SecureText(); + + 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; +}; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 3e67f96..8083efb 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,4 +1,4 @@ -# ESPCrypto exercises require ESP-IDF/Arduino runtime to access hardware -# accelerators for AES/SHA. Host builds only emit this notice so CI -# completes quickly; see test/test_esp_crypto for Unity-based device tests. -message(STATUS "ESPCrypto: tests are disabled for the host build.") +add_executable(host_api_smoke host_api_smoke.cpp) +target_include_directories(host_api_smoke PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../src) +target_compile_features(host_api_smoke PRIVATE cxx_std_17) +add_test(NAME host_api_smoke COMMAND host_api_smoke) diff --git a/test/host_api_smoke.cpp b/test/host_api_smoke.cpp new file mode 100644 index 0000000..9dc771b --- /dev/null +++ b/test/host_api_smoke.cpp @@ -0,0 +1,32 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +int main() { + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + + CryptoResult result; + result.status = CryptoStatusDetail{}; + result.value = 7; + + CryptoSpan empty; + CryptoPolicy policy; + policy.minPbkdf2Iterations = 100000; + (void)empty; + (void)policy; + return result.value == 7 ? 0 : 1; +} diff --git a/test/test_esp_crypto/test_esp_crypto.cpp b/test/test_esp_crypto/test_esp_crypto.cpp index fca7068..7641b93 100644 --- a/test/test_esp_crypto/test_esp_crypto.cpp +++ b/test/test_esp_crypto/test_esp_crypto.cpp @@ -1,570 +1,265 @@ #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()); +namespace { +const char *statusMessage(const CryptoStatusDetail &status) { + return status.message.empty() ? toString(status.code) : status.message.c_str(); } -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); +void test_runtime_lifecycle_and_policy_reset() { + espcrypto::runtime::deinit(); + TEST_ASSERT_FALSE(espcrypto::runtime::isInitialized()); - ESPCrypto::deinit(); - TEST_ASSERT_FALSE(ESPCrypto::isInitialized()); - TEST_ASSERT_EQUAL_UINT32(1024, ESPCrypto::policy().minPbkdf2Iterations); + CryptoPolicy policy = espcrypto::policy::get(); + policy.minPbkdf2Iterations = 1024; + espcrypto::policy::set(policy); + TEST_ASSERT_TRUE(espcrypto::runtime::isInitialized()); + TEST_ASSERT_EQUAL_UINT32(1024, espcrypto::policy::get().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::runtime::deinit(); + TEST_ASSERT_FALSE(espcrypto::runtime::isInitialized()); + TEST_ASSERT_EQUAL_UINT32(100000, espcrypto::policy::get().minPbkdf2Iterations); } void test_sha_hex_matches_known_value() { - const char *data = "hello world"; - String digest = ESPCrypto::shaHex(reinterpret_cast(data), strlen(data)); + auto digest = espcrypto::hash::shaHex("hello world"); 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)); -} - -void test_password_verify_rejects_invalid_cost_envelope() { - const char *invalidCosts[] = {"bad", "-1", "10x", "32", "999999999999999999999999999999"}; - for (auto cost : invalidCosts) { - String encoded = String("$esphash$v1$") + cost + "$AQ==$AQ=="; - auto res = ESPCrypto::verifyStringResult("pw", encoded); - TEST_ASSERT_FALSE(res.ok()); - TEST_ASSERT_EQUAL_INT(static_cast(CryptoStatus::DecodeError), static_cast(res.status.code)); - TEST_ASSERT_FALSE(ESPCrypto::verifyString("pw", encoded)); - } - - auto trailing = - ESPCrypto::verifyStringResult("pw", "$esphash$v1$10$AQ==$AQ==$unexpected"); - TEST_ASSERT_FALSE(trailing.ok()); +void test_password_hash_roundtrip_v2() { + auto hashed = espcrypto::password::hash("hunter2"); + TEST_ASSERT_TRUE_MESSAGE(hashed.ok(), statusMessage(hashed.status)); + TEST_ASSERT_TRUE_MESSAGE( + espcrypto::password::verify("hunter2", hashed.value).ok(), + "expected password verify success" + ); + auto rejected = espcrypto::password::verify("badpass", hashed.value); + TEST_ASSERT_FALSE(rejected.ok()); TEST_ASSERT_EQUAL_INT( - static_cast(CryptoStatus::DecodeError), - static_cast(trailing.status.code) + static_cast(CryptoStatus::VerifyFailed), + static_cast(rejected.status.code) ); - TEST_ASSERT_FALSE(ESPCrypto::verifyString("pw", "$esphash$v1$10$AQ==$AQ==$unexpected")); } -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( - "cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134" - "c825a7", - sha384.c_str() +void test_password_legacy_envelopes_require_explicit_compat() { + std::string legacy = "$esphash$v1$10$AQ==$AQ=="; + auto rejected = espcrypto::password::verify("pw", legacy); + TEST_ASSERT_FALSE(rejected.ok()); + TEST_ASSERT_EQUAL_INT( + static_cast(CryptoStatus::PolicyViolation), + static_cast(rejected.status.code) ); - opts.variant = ShaVariant::SHA512; - String sha512 = ESPCrypto::shaHex(reinterpret_cast("abc"), 3, opts); - TEST_ASSERT_EQUAL_STRING( - "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3" - "feebbd454d4423643ce80e2a9ac94fa54ca49f", - sha512.c_str() + + PasswordVerifyOptions compat; + compat.allowLegacy = true; + auto compatRes = espcrypto::password::verify("pw", legacy, compat); + TEST_ASSERT_FALSE(compatRes.ok()); + TEST_ASSERT_NOT_EQUAL( + static_cast(CryptoStatus::PolicyViolation), + static_cast(compatRes.status.code) ); } 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 iv(12, 0x00); std::vector plaintext(16, 0x00); - std::vector ciphertext; - std::vector tag; - TEST_ASSERT_TRUE(ESPCrypto::aesGcmEncrypt(key, iv, plaintext, ciphertext, tag)); + std::vector ciphertext(plaintext.size(), 0); + std::vector tag(16, 0); + + auto enc = espcrypto::symmetric::aesGcmEncrypt( + key, + CryptoSpan(iv), + CryptoSpan(plaintext), + CryptoSpan(ciphertext), + CryptoSpan(tag) + ); + TEST_ASSERT_TRUE_MESSAGE(enc.ok(), statusMessage(enc.status)); + const uint8_t expectedCipher[] = { - 0x03, - 0x88, - 0xda, - 0xce, - 0x60, - 0xb6, - 0xa3, - 0x92, - 0xf3, - 0x28, - 0xc2, - 0xb9, - 0x71, - 0xb2, - 0xfe, - 0x78 + 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 + 0xab, 0x6e, 0x47, 0xd4, 0x2c, 0xec, 0x13, 0xbd, + 0xf5, 0x3a, 0x67, 0xb2, 0x12, 0x57, 0xbd, 0xdf }; TEST_ASSERT_TRUE( - ESPCrypto::constantTimeEq( + espcrypto::runtime::constantTimeEq( ciphertext, std::vector(expectedCipher, expectedCipher + sizeof(expectedCipher)) ) ); TEST_ASSERT_TRUE( - ESPCrypto::constantTimeEq( + espcrypto::runtime::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)); + auto dec = espcrypto::symmetric::aesGcmDecrypt(key, iv, ciphertext, tag); + TEST_ASSERT_TRUE_MESSAGE(dec.ok(), statusMessage(dec.status)); + TEST_ASSERT_TRUE(espcrypto::runtime::constantTimeEq(plaintext, dec.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)); -} +void test_hkdf_and_pbkdf2() { + CryptoPolicy policy = espcrypto::policy::get(); + policy.minPbkdf2Iterations = 1024; + espcrypto::policy::set(policy); -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( + auto hkdf = espcrypto::kdf::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)) - ) - ); + TEST_ASSERT_TRUE_MESSAGE(hkdf.ok(), statusMessage(hkdf.status)); + TEST_ASSERT_EQUAL_UINT32(42, hkdf.value.size()); + + std::vector pbkdfSalt = {'s', 'a', 'l', 't'}; + auto pbkdf2 = + espcrypto::kdf::pbkdf2("password", CryptoSpan(pbkdfSalt), 1024, 32); + TEST_ASSERT_TRUE_MESSAGE(pbkdf2.ok(), statusMessage(pbkdf2.status)); + TEST_ASSERT_EQUAL_UINT32(32, pbkdf2.value.size()); } 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); + + auto token = espcrypto::jwt::create(claims, "secret", signOptions); + TEST_ASSERT_TRUE_MESSAGE(token.ok(), statusMessage(token.status)); 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() - ); + + auto verified = espcrypto::jwt::verify(token.value, "secret", decoded, verifyOptions); + TEST_ASSERT_TRUE_MESSAGE(verified.ok(), statusMessage(verified.status)); TEST_ASSERT_EQUAL_STRING("demo", decoded["scope"].as()); } -void test_base64_encode_contract_via_jwt_result_api() { +void test_jwks_verification() { JsonDocument claims; - claims["x"] = "y"; + claims["scope"] = "rotation"; + JwtSignOptions signOptions; signOptions.algorithm = JwtAlgorithm::HS256; - signOptions.issuer = "base64-contract"; - signOptions.expiresInSeconds = 30; + signOptions.keyId = "primary"; + signOptions.issuer = "jwks"; - auto token = ESPCrypto::createJwtResult(claims, std::string("secret"), signOptions); - TEST_ASSERT_TRUE_MESSAGE(token.ok(), token.status.message.c_str()); - TEST_ASSERT_TRUE(token.value.length() > 0); + auto token = espcrypto::jwt::create(claims, "supersecret", signOptions); + TEST_ASSERT_TRUE_MESSAGE(token.ok(), statusMessage(token.status)); + + JsonDocument jwks; + JsonArray keys = jwks["keys"].to(); + JsonObject key = keys.add(); + key["kid"] = "primary"; + key["kty"] = "oct"; + key["alg"] = "HS256"; + key["k"] = "c3VwZXJzZWNyZXQ"; JsonDocument decoded; JwtVerifyOptions verifyOptions; verifyOptions.algorithm = JwtAlgorithm::HS256; - verifyOptions.issuer = "base64-contract"; - auto verified = ESPCrypto::verifyJwtResult(token.value, std::string("secret"), decoded, verifyOptions); - TEST_ASSERT_TRUE_MESSAGE(verified.ok(), verified.status.message.c_str()); - TEST_ASSERT_EQUAL_STRING("y", decoded["x"].as()); + verifyOptions.issuer = "jwks"; + + auto verified = espcrypto::jwt::verifyWithJwks(token.value, jwks, decoded, verifyOptions); + TEST_ASSERT_TRUE_MESSAGE(verified.ok(), statusMessage(verified.status)); + TEST_ASSERT_EQUAL_STRING("rotation", decoded["scope"].as()); } -void test_sha_ctx_streaming() { - ShaCtx ctx; - TEST_ASSERT_TRUE(ctx.begin(ShaVariant::SHA256).ok()); +void test_streaming_and_device_key_helpers() { const char *chunk1 = "ab"; const char *chunk2 = "c"; + uint8_t digest[32] = {0}; + ShaCtx sha; + TEST_ASSERT_TRUE(sha.begin(ShaVariant::SHA256).ok()); TEST_ASSERT_TRUE( - ctx.update(CryptoSpan(reinterpret_cast(chunk1), 2)).ok() + sha.update( + CryptoSpan( + reinterpret_cast(chunk1), + strlen(chunk1) + ) + ) + .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)) - ) - ); -} - -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) + sha.update( + CryptoSpan( + reinterpret_cast(chunk2), + strlen(chunk2) + ) + ) + .ok() ); - 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); + TEST_ASSERT_TRUE(sha.finish(CryptoSpan(digest, sizeof(digest))).ok()); + std::vector key(16, 0x33); + std::vector counter(16, 0x00); + std::vector input = {'s', 't', 'r', 'e', 'a', 'm'}; + std::vector encrypted(input.size(), 0); + std::vector decrypted(input.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.begin(key, CryptoSpan(counter)).ok()); + TEST_ASSERT_TRUE(dec.begin(key, CryptoSpan(counter)).ok()); TEST_ASSERT_TRUE( - enc.update(CryptoSpan(plaintext), CryptoSpan(ciphertext)).ok() + enc.update( + CryptoSpan(input), + CryptoSpan(encrypted) + ) + .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() + dec.update( + CryptoSpan(encrypted), + 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)); -} - -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)); -} - -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()); - TEST_ASSERT_TRUE(der.value.size() > 0); - 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)); + TEST_ASSERT_TRUE(espcrypto::runtime::constantTimeEq(input, decrypted)); - auto derAgain = ESPCrypto::ecdsaRawToDer(CryptoSpan(rawBack.value)); - TEST_ASSERT_TRUE_MESSAGE(derAgain.ok(), derAgain.status.message.c_str()); - TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(der.value, derAgain.value)); -} - -void test_constant_time_eq_semantics() { - const std::vector a = {0x01, 0x02, 0x03}; - const std::vector same = {0x01, 0x02, 0x03}; - const std::vector diffSameLength = {0x01, 0x02, 0x04}; - const std::vector diffLength = {0x01, 0x02}; - - TEST_ASSERT_TRUE(ESPCrypto::constantTimeEq(a, same)); - TEST_ASSERT_FALSE(ESPCrypto::constantTimeEq(a, diffSameLength)); - TEST_ASSERT_FALSE(ESPCrypto::constantTimeEq(a, diffLength)); -} - -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)); -} - -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)); -} - -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")); -} - -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()); -} -void setUp() { -} -void tearDown() { + auto first = espcrypto::device::deriveKey("unity-device-key", CryptoSpan(), 32); + auto second = espcrypto::device::deriveKey("unity-device-key", CryptoSpan(), 32); + TEST_ASSERT_TRUE_MESSAGE(first.ok(), statusMessage(first.status)); + TEST_ASSERT_TRUE_MESSAGE(second.ok(), statusMessage(second.status)); + TEST_ASSERT_TRUE(espcrypto::runtime::constantTimeEq(first.value, second.value)); } +} // namespace void setup() { - delay(2000); + delay(1000); UNITY_BEGIN(); - RUN_TEST(test_teardown_preinit_and_idempotent); - RUN_TEST(test_teardown_reinit_lifecycle); + RUN_TEST(test_runtime_lifecycle_and_policy_reset); 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_password_verify_rejects_invalid_cost_envelope); + RUN_TEST(test_password_hash_roundtrip_v2); + RUN_TEST(test_password_legacy_envelopes_require_explicit_compat); 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_constant_time_eq_semantics); - RUN_TEST(test_hkdf_rfc5869_case1); - RUN_TEST(test_pbkdf2_vector); + RUN_TEST(test_hkdf_and_pbkdf2); RUN_TEST(test_jwt_roundtrip_hs256); - RUN_TEST(test_base64_encode_contract_via_jwt_result_api); - RUN_TEST(test_jwt_and_envelope_fuzz); RUN_TEST(test_jwks_verification); - RUN_TEST(test_device_key_is_stable); + RUN_TEST(test_streaming_and_device_key_helpers); UNITY_END(); } void loop() { - vTaskDelay(pdMS_TO_TICKS(1000)); }