Massive cleanup

This commit is contained in:
2026-02-27 23:42:53 +01:00
parent 8e95149d8e
commit 74bf49c652
11 changed files with 76 additions and 3029 deletions

View File

@@ -17,12 +17,6 @@ static const char *TAG = "TangServer";
// Include core components // Include core components
#include "atecc608a.h" #include "atecc608a.h"
#include "crypto.h"
#include "encoding.h"
#include "provision.h"
#include "provision_handlers.h"
#include "tang_handlers.h"
#include "tang_storage.h"
#include "zk_auth.h" #include "zk_auth.h"
#include "zk_handlers.h" #include "zk_handlers.h"
@@ -33,7 +27,6 @@ const char *wifi_password = CONFIG_WIFI_PASSWORD;
// --- Global State --- // --- Global State ---
bool unlocked = false; // Start inactive until provisioned and authenticated bool unlocked = false; // Start inactive until provisioned and authenticated
httpd_handle_t server_http = NULL; httpd_handle_t server_http = NULL;
TangKeyStore keystore;
ZKAuth zk_auth; // Zero-Knowledge Authentication ZKAuth zk_auth; // Zero-Knowledge Authentication
// WiFi event group // WiFi event group
@@ -103,19 +96,6 @@ void setup_wifi() {
// --- Initial Setup --- // --- Initial Setup ---
bool perform_initial_setup() { bool perform_initial_setup() {
if (!P256::generate_keypair(keystore.exc_pub, keystore.exc_priv)) {
ESP_LOGE(TAG, "ERROR: Failed to generate exchange key");
return false;
}
// Save Tang keys directly (no encryption in prototype)
if (!keystore.save_tang_keys()) {
ESP_LOGE(TAG, "ERROR: Failed to save Tang keys");
return false;
}
ESP_LOGI(TAG, "Configuration saved to NVS");
ESP_LOGI(TAG, "======================================================="); ESP_LOGI(TAG, "=======================================================");
ESP_LOGI(TAG, "Setup complete! Device is ready to use"); ESP_LOGI(TAG, "Setup complete! Device is ready to use");
ESP_LOGI(TAG, "NOTE: Exchange key stored unencrypted for prototyping"); ESP_LOGI(TAG, "NOTE: Exchange key stored unencrypted for prototyping");
@@ -134,44 +114,7 @@ httpd_handle_t setup_http_server() {
httpd_handle_t server = NULL; httpd_handle_t server = NULL;
if (httpd_start(&server, &config) == ESP_OK) { if (httpd_start(&server, &config) == ESP_OK) {
register_provision_handlers(server);
register_zk_handlers(server); register_zk_handlers(server);
// Register Tang protocol handlers
httpd_uri_t adv_uri = {.uri = "/adv",
.method = HTTP_GET,
.handler = handle_adv,
.user_ctx = NULL};
httpd_register_uri_handler(server, &adv_uri);
httpd_uri_t adv_uri_slash = {.uri = "/adv/",
.method = HTTP_GET,
.handler = handle_adv,
.user_ctx = NULL};
httpd_register_uri_handler(server, &adv_uri_slash);
httpd_uri_t rec_uri = {.uri = "/rec",
.method = HTTP_POST,
.handler = handle_rec,
.user_ctx = NULL};
httpd_register_uri_handler(server, &rec_uri);
httpd_uri_t config_uri = {.uri = "/config",
.method = HTTP_GET,
.handler = handle_config,
.user_ctx = NULL};
httpd_register_uri_handler(server, &config_uri);
httpd_uri_t reboot_uri = {.uri = "/reboot",
.method = HTTP_GET,
.handler = handle_reboot,
.user_ctx = NULL};
httpd_register_uri_handler(server, &reboot_uri);
// Register custom error handler for 404
httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, handle_not_found);
ESP_LOGI(TAG, "HTTP server listening on port 80"); ESP_LOGI(TAG, "HTTP server listening on port 80");
} else { } else {
ESP_LOGE(TAG, "Failed to start HTTP server"); ESP_LOGE(TAG, "Failed to start HTTP server");
@@ -182,7 +125,6 @@ httpd_handle_t setup_http_server() {
// --- Main Setup --- // --- Main Setup ---
void setup() { void setup() {
ESP_LOGI(TAG, "\n\nESP32 Tang Server Starting...");
// Initialize NVS (required before any storage operations) // Initialize NVS (required before any storage operations)
esp_err_t ret = nvs_flash_init(); esp_err_t ret = nvs_flash_init();
@@ -194,26 +136,12 @@ void setup() {
ESP_ERROR_CHECK(ret); ESP_ERROR_CHECK(ret);
ESP_LOGI(TAG, "NVS initialized"); ESP_LOGI(TAG, "NVS initialized");
// Initialize ATECC608A
if (atecc608B_init()) { if (atecc608B_init()) {
atecc608B_print_config(); atecc608B_print_config();
} else { } else {
ESP_LOGW(TAG, "WARNING: ATECC608A initialization failed"); ESP_LOGW(TAG, "WARNING: ATECC608A initialization failed");
} }
// Load or initialize configuration
if (keystore.is_configured()) {
ESP_LOGI(TAG, "Found existing configuration");
// Auto-load Tang keys on startup (no activation needed in prototype)
if (keystore.load_tang_keys()) {
ESP_LOGI(TAG, "Loaded Tang keys - server ready");
} else {
ESP_LOGW(TAG, "Failed to load Tang keys");
}
} else {
perform_initial_setup();
}
// Initialize Zero-Knowledge Authentication // Initialize Zero-Knowledge Authentication
ESP_LOGI(TAG, "Initializing Zero-Knowledge Authentication..."); ESP_LOGI(TAG, "Initializing Zero-Knowledge Authentication...");
if (zk_auth.init()) { if (zk_auth.init()) {

View File

@@ -1,306 +0,0 @@
#ifndef CRYPTO_H
#define CRYPTO_H
#include <esp_log.h>
#include <esp_system.h>
#include <mbedtls/ctr_drbg.h>
#include <mbedtls/ecdsa.h>
#include <mbedtls/ecp.h>
#include <mbedtls/entropy.h>
#include <mbedtls/gcm.h>
#include <mbedtls/md.h>
#include <mbedtls/pkcs5.h>
#include <mbedtls/sha256.h>
#include <mbedtls/sha512.h>
static const char *TAG_CRYPTO = "crypto";
// --- Constants ---
// P-256 uses 256 bits = 32 bytes per coordinate
const int P256_PRIVATE_KEY_SIZE = 32; // Scalar value
const int P256_PUBLIC_KEY_SIZE = 64; // Uncompressed point (x + y)
const int P256_COORDINATE_SIZE = 32; // Single coordinate (x or y)
const int GCM_TAG_SIZE = 16;
const int SALT_SIZE = 16;
const int PBKDF2_ITERATIONS = 1000;
// --- RNG Management ---
class RNG {
private:
mbedtls_entropy_context entropy;
mbedtls_ctr_drbg_context ctr_drbg;
bool initialized;
public:
RNG() : initialized(false) {}
~RNG() { cleanup(); }
int init() {
if (initialized)
return 0;
mbedtls_entropy_init(&entropy);
mbedtls_ctr_drbg_init(&ctr_drbg);
const char *pers = "esp32_tang_server";
int ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy,
(const unsigned char *)pers, strlen(pers));
if (ret != 0)
return ret;
initialized = true;
return 0;
}
void cleanup() {
if (initialized) {
mbedtls_ctr_drbg_free(&ctr_drbg);
mbedtls_entropy_free(&entropy);
initialized = false;
}
}
mbedtls_ctr_drbg_context *context() { return &ctr_drbg; }
};
// Global RNG instance
static RNG global_rng;
// --- P-256 EC Operations ---
class P256 {
public:
static bool generate_keypair(uint8_t *pub_key, uint8_t *priv_key) {
int ret = global_rng.init();
if (ret != 0) {
ESP_LOGE(TAG_CRYPTO, "RNG init failed: -0x%04x", -ret);
return false;
}
mbedtls_ecp_group grp;
mbedtls_ecp_point Q;
mbedtls_mpi d;
mbedtls_ecp_group_init(&grp);
mbedtls_ecp_point_init(&Q);
mbedtls_mpi_init(&d);
ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1);
if (ret != 0) {
ESP_LOGE(TAG_CRYPTO, "ECP group load failed: -0x%04x", -ret);
} else {
ret = mbedtls_ecp_gen_keypair(&grp, &d, &Q, mbedtls_ctr_drbg_random,
global_rng.context());
if (ret != 0) {
ESP_LOGE(TAG_CRYPTO, "ECP keypair gen failed: -0x%04x", -ret);
}
}
if (ret == 0) {
ret = mbedtls_mpi_write_binary(&d, priv_key, P256_COORDINATE_SIZE);
if (ret != 0) {
ESP_LOGE(TAG_CRYPTO, "Write private key failed: -0x%04x", -ret);
}
}
if (ret == 0) {
ret = mbedtls_mpi_write_binary(&Q.MBEDTLS_PRIVATE(X), pub_key,
P256_COORDINATE_SIZE);
if (ret != 0) {
ESP_LOGE(TAG_CRYPTO, "Write pub key X failed: -0x%04x", -ret);
}
}
if (ret == 0) {
ret = mbedtls_mpi_write_binary(&Q.MBEDTLS_PRIVATE(Y),
pub_key + P256_COORDINATE_SIZE,
P256_COORDINATE_SIZE);
if (ret != 0) {
ESP_LOGE(TAG_CRYPTO, "Write pub key Y failed: -0x%04x", -ret);
}
}
mbedtls_ecp_group_free(&grp);
mbedtls_ecp_point_free(&Q);
mbedtls_mpi_free(&d);
return (ret == 0);
}
static bool compute_public_key(const uint8_t *priv_key, uint8_t *pub_key) {
if (global_rng.init() != 0)
return false;
mbedtls_ecp_group grp;
mbedtls_ecp_point Q;
mbedtls_mpi d;
mbedtls_ecp_group_init(&grp);
mbedtls_ecp_point_init(&Q);
mbedtls_mpi_init(&d);
int ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1);
if (ret == 0) {
ret = mbedtls_mpi_read_binary(&d, priv_key, P256_COORDINATE_SIZE);
}
if (ret == 0) {
ret = mbedtls_ecp_mul(&grp, &Q, &d, &grp.G, mbedtls_ctr_drbg_random,
global_rng.context());
}
if (ret == 0) {
ret = mbedtls_mpi_write_binary(&Q.MBEDTLS_PRIVATE(X), pub_key,
P256_COORDINATE_SIZE);
}
if (ret == 0) {
ret = mbedtls_mpi_write_binary(&Q.MBEDTLS_PRIVATE(Y),
pub_key + P256_COORDINATE_SIZE,
P256_COORDINATE_SIZE);
}
mbedtls_ecp_group_free(&grp);
mbedtls_ecp_point_free(&Q);
mbedtls_mpi_free(&d);
return (ret == 0);
}
static bool ecdh_compute_shared_point(const uint8_t *peer_pub_key,
const uint8_t *priv_key,
uint8_t *shared_point,
bool full_point = true) {
if (global_rng.init() != 0)
return false;
mbedtls_ecp_group grp;
mbedtls_ecp_point Q;
mbedtls_mpi d;
mbedtls_ecp_group_init(&grp);
mbedtls_ecp_point_init(&Q);
mbedtls_mpi_init(&d);
int ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1);
if (ret == 0) {
ret = mbedtls_mpi_read_binary(&d, priv_key, P256_COORDINATE_SIZE);
}
if (ret == 0) {
ret = mbedtls_mpi_read_binary(&Q.MBEDTLS_PRIVATE(X), peer_pub_key,
P256_COORDINATE_SIZE);
}
if (ret == 0) {
ret = mbedtls_mpi_read_binary(&Q.MBEDTLS_PRIVATE(Y),
peer_pub_key + P256_COORDINATE_SIZE,
P256_COORDINATE_SIZE);
}
if (ret == 0) {
ret = mbedtls_mpi_lset(&Q.MBEDTLS_PRIVATE(Z), 1);
}
if (ret == 0) {
ret = mbedtls_ecp_check_pubkey(&grp, &Q);
}
if (ret == 0) {
ret = mbedtls_ecp_mul(&grp, &Q, &d, &Q, mbedtls_ctr_drbg_random,
global_rng.context());
}
if (ret == 0) {
ret = mbedtls_mpi_write_binary(&Q.MBEDTLS_PRIVATE(X), shared_point,
P256_COORDINATE_SIZE);
}
if (ret == 0 && full_point) {
ret = mbedtls_mpi_write_binary(&Q.MBEDTLS_PRIVATE(Y),
shared_point + P256_COORDINATE_SIZE,
P256_COORDINATE_SIZE);
}
mbedtls_ecp_group_free(&grp);
mbedtls_ecp_point_free(&Q);
mbedtls_mpi_free(&d);
return (ret == 0);
}
static bool sign(const uint8_t *hash, size_t hash_len,
const uint8_t *priv_key, uint8_t *signature) {
if (global_rng.init() != 0)
return false;
mbedtls_ecp_group grp;
mbedtls_mpi d, r, s;
mbedtls_ecp_group_init(&grp);
mbedtls_mpi_init(&d);
mbedtls_mpi_init(&r);
mbedtls_mpi_init(&s);
int ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1);
if (ret == 0) {
ret = mbedtls_mpi_read_binary(&d, priv_key, P256_COORDINATE_SIZE);
}
if (ret == 0) {
ret = mbedtls_ecdsa_sign(&grp, &r, &s, &d, hash, hash_len,
mbedtls_ctr_drbg_random, global_rng.context());
}
if (ret == 0) {
ret = mbedtls_mpi_write_binary(&r, signature, P256_COORDINATE_SIZE);
}
if (ret == 0) {
ret = mbedtls_mpi_write_binary(&s, signature + P256_COORDINATE_SIZE,
P256_COORDINATE_SIZE);
}
mbedtls_ecp_group_free(&grp);
mbedtls_mpi_free(&d);
mbedtls_mpi_free(&r);
mbedtls_mpi_free(&s);
return (ret == 0);
}
};
// --- AES-GCM Operations ---
class AESGCM {
public:
static bool encrypt(uint8_t *plaintext, size_t len, const uint8_t *key,
size_t key_len, const uint8_t *iv, size_t iv_len,
const uint8_t *aad, size_t aad_len, uint8_t *tag) {
mbedtls_gcm_context ctx;
mbedtls_gcm_init(&ctx);
int ret = mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key, key_len * 8);
if (ret == 0) {
ret = mbedtls_gcm_crypt_and_tag(&ctx, MBEDTLS_GCM_ENCRYPT, len, iv,
iv_len, aad, aad_len, plaintext,
plaintext, GCM_TAG_SIZE, tag);
}
mbedtls_gcm_free(&ctx);
return (ret == 0);
}
static bool decrypt(uint8_t *ciphertext, size_t len, const uint8_t *key,
size_t key_len, const uint8_t *iv, size_t iv_len,
const uint8_t *aad, size_t aad_len, const uint8_t *tag) {
mbedtls_gcm_context ctx;
mbedtls_gcm_init(&ctx);
int ret = mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key, key_len * 8);
if (ret == 0) {
ret = mbedtls_gcm_auth_decrypt(&ctx, len, iv, iv_len, aad, aad_len, tag,
GCM_TAG_SIZE, ciphertext, ciphertext);
}
mbedtls_gcm_free(&ctx);
return (ret == 0);
}
};
// --- PBKDF2 ---
class PBKDF2 {
public:
static int derive_key(uint8_t *output, size_t key_len, const char *password,
const uint8_t *salt, size_t salt_len, int iterations) {
return mbedtls_pkcs5_pbkdf2_hmac_ext(
MBEDTLS_MD_SHA256, (const uint8_t *)password, strlen(password), salt,
salt_len, iterations, key_len, output);
}
};
#endif // CRYPTO_H

View File

@@ -1,30 +0,0 @@
#ifndef ENCODING_H
#define ENCODING_H
#include <atca_helpers.h>
#include <cstring>
#include <mbedtls/base64.h>
#include <string>
static bool b64url_encode_buf(const uint8_t *data, size_t data_len,
char *out_buf, size_t out_max_len) {
size_t b64_len = out_max_len;
ATCA_STATUS status = atcab_base64encode_(data, data_len, out_buf, &b64_len,
atcab_b64rules_urlsafe());
return (status == ATCA_SUCCESS && b64_len < out_max_len);
}
static bool b64url_decode_buf(const char *in_str, uint8_t *out_buf,
size_t expected_len) {
if (!in_str || !out_buf)
return false;
size_t out_len = expected_len;
ATCA_STATUS status = atcab_base64decode_(in_str, strlen(in_str), out_buf,
&out_len, atcab_b64rules_urlsafe());
return (status == ATCA_SUCCESS && out_len == expected_len);
}
#endif // ENCODING_H

View File

@@ -1,357 +0,0 @@
#ifndef PROVISION_H
#define PROVISION_H
#include "cryptoauthlib.h"
#include <esp_efuse.h>
#include <esp_efuse_table.h>
#include <esp_log.h>
static const char *TAG_PROVISION = "provision";
// Hardcoded HMAC key for prototyping (32 bytes)
// WARNING: This is for prototyping only! In production, this should be securely
// generated
static const uint8_t IO_KEY[32] = {
0x2c, 0x43, 0x34, 0x9c, 0x4c, 0xe6, 0x70, 0xf3, 0xcb, 0x10, 0xef,
0xcc, 0x56, 0xf0, 0xd0, 0xc4, 0x03, 0x2c, 0x45, 0x9f, 0xf3, 0xcb,
0x29, 0x27, 0x22, 0x8e, 0x93, 0x3c, 0xfe, 0x6e, 0x87, 0xed};
static const uint8_t HMAC_KEY[32] = {
0x24, 0x01, 0x2a, 0xf7, 0x3e, 0x62, 0x7a, 0x5e, 0x5e, 0xdc, 0xf0,
0xce, 0xd6, 0xe5, 0x32, 0x20, 0x56, 0xca, 0x29, 0xd1, 0x52, 0xf8,
0x17, 0x23, 0x06, 0x75, 0x4f, 0x1d, 0xb9, 0x85, 0x51, 0x5e};
// ATECC608B Configuration values
// SlotConfig (Bytes 20-51): 32 bytes for 16 slots (2 bytes each)
static const uint8_t ATECC_SLOT_CONFIG[32] = {
0x81, 0x00, 0x81, 0x20, 0x81, 0x20, 0x81, 0x20, 0x84, 0x20, 0x84,
0x20, 0xc6, 0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x9a, 0x4a,
0x84, 0x20, 0x84, 0x20, 0x9a, 0x4a, 0x00, 0x00, 0x00, 0x00};
// KeyConfig (Bytes 96-127): 32 bytes for 16 slots (2 bytes each)
static const uint8_t ATECC_KEY_CONFIG[32] = {
0x33, 0x00, 0x13, 0x00, 0xd3, 0x09, 0x93, 0x0a, 0x53, 0x00, 0x53,
0x10, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x5c, 0x00,
0x53, 0x00, 0x53, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00};
// Byte 69: Persistent Latch enabled with Keyslot 10
static const uint8_t ATECC_LAST_KEY_USE_10 = 0x8a;
// Bytes 90-91: IO Protection optional and Key is Slot 6
static const uint8_t ATECC_CHIP_OPTIONS[2] = {0x02, 0x60};
/**
* Check if ATECC608B config zone is locked
* Returns: true if locked, false if unlocked
*/
bool is_atecc608b_config_locked() {
extern uint8_t g_atecc_config_data[128];
extern bool g_atecc_config_valid;
// Read config if not already read
if (!g_atecc_config_valid) {
extern bool atecc608B_read_config();
if (!atecc608B_read_config()) {
ESP_LOGE(TAG_PROVISION, "Failed to read ATECC608B config");
return false; // Assume unlocked if we can't read
}
}
// Byte 87: LockConfig (Config Zone Lock)
// 0x00 = LOCKED, any other value = UNLOCKED
bool locked = (g_atecc_config_data[87] == 0x00);
ESP_LOGI(TAG_PROVISION, "ATECC608B Config Zone: %s (0x%02X)",
locked ? "LOCKED" : "UNLOCKED", g_atecc_config_data[87]);
return locked;
}
/**
* Check if ATECC608B data zone is locked
* Returns: true if locked, false if unlocked
*/
bool is_atecc608b_data_locked() {
extern uint8_t g_atecc_config_data[128];
extern bool g_atecc_config_valid;
// Read config if not already read
if (!g_atecc_config_valid) {
extern bool atecc608B_read_config();
if (!atecc608B_read_config()) {
ESP_LOGE(TAG_PROVISION, "Failed to read ATECC608B config");
return false; // Assume unlocked if we can't read
}
}
// Byte 86: LockValue (Data/OTP Zone Lock)
// 0x00 = LOCKED, any other value = UNLOCKED
bool locked = (g_atecc_config_data[86] == 0x00);
ESP_LOGI(TAG_PROVISION, "ATECC608B Data Zone: %s (0x%02X)",
locked ? "LOCKED" : "UNLOCKED", g_atecc_config_data[86]);
return locked;
}
/**
* Check if ESP32-C6 efuse BLOCK_KEY5 is already used
* Returns: true if used, false if not used
*/
bool is_efuse_key5_used() {
// Read the key purpose for BLOCK_KEY5
esp_efuse_purpose_t purpose;
esp_err_t err = esp_efuse_read_field_blob(ESP_EFUSE_KEY_PURPOSE_5, &purpose,
sizeof(purpose) * 8);
if (err != ESP_OK) {
ESP_LOGE(TAG_PROVISION, "Failed to read KEY_PURPOSE_5: %s",
esp_err_to_name(err));
return false;
}
ESP_LOGI(TAG_PROVISION, "EFUSE KEY_PURPOSE_5: %d (0=NONE/unused)", purpose);
// Purpose 0 (ESP_EFUSE_KEY_PURPOSE_USER) typically means unused/default
// Any non-zero value means it's configured for something
return (purpose != ESP_EFUSE_KEY_PURPOSE_USER);
}
/**
* Provision the ATECC608B configuration and lock the config zone
* Returns: true on success, false on failure
*/
bool provision_atecc608b_config() {
ESP_LOGI(TAG_PROVISION,
"=== Starting ATECC608B Configuration Provisioning ===");
// Check if config zone is already locked
if (is_atecc608b_config_locked()) {
ESP_LOGW(TAG_PROVISION, "Config zone is already locked!");
return false;
}
// Wake the device
ATCA_STATUS status = atcab_wakeup();
if (status != ATCA_SUCCESS) {
ESP_LOGE(TAG_PROVISION, "Failed to wake ATECC608B: 0x%02X", status);
return false;
}
// 1. Ensure we have the current 128-byte config read into memory
extern uint8_t g_atecc_config_data[128];
extern bool g_atecc_config_valid;
if (!g_atecc_config_valid) {
ESP_LOGI(TAG_PROVISION, "Reading full config zone into memory...");
status = atcab_read_config_zone(g_atecc_config_data);
if (status != ATCA_SUCCESS) {
ESP_LOGE(TAG_PROVISION, "Failed to read config zone: 0x%02X", status);
return false;
}
g_atecc_config_valid = true;
}
// 2. Modify the in-memory array with our desired settings
ESP_LOGI(TAG_PROVISION, "Applying configuration changes to memory...");
memcpy(&g_atecc_config_data[20], ATECC_SLOT_CONFIG, 32); // SlotConfig
g_atecc_config_data[69] = ATECC_LAST_KEY_USE_10; // Persistent Latch
memcpy(&g_atecc_config_data[90], ATECC_CHIP_OPTIONS, 2); // ChipOptions
memcpy(&g_atecc_config_data[96], ATECC_KEY_CONFIG, 32); // KeyConfig
// 3. Write the whole modified config zone back to the device
// atcab_write_config_zone safely skips the read-only bytes (0-15)
ESP_LOGI(TAG_PROVISION, "Writing modified config zone back to ATECC608B...");
status = atcab_write_config_zone(g_atecc_config_data);
if (status != ATCA_SUCCESS) {
ESP_LOGE(TAG_PROVISION, "Failed to write config zone: 0x%02X", status);
return false;
}
ESP_LOGI(TAG_PROVISION, "Configuration written successfully");
// 4. Lock the config zone
ESP_LOGI(TAG_PROVISION, "Locking config zone...");
status = atcab_lock_config_zone();
if (status != ATCA_SUCCESS) {
ESP_LOGE(TAG_PROVISION, "Failed to lock config zone: 0x%02X", status);
return false;
}
ESP_LOGI(TAG_PROVISION,
"ATECC608B configuration provisioned and locked successfully!");
// Invalidate cached config so it gets accurately re-read on next use
g_atecc_config_valid = false;
// Verify it was locked
if (is_atecc608b_config_locked()) {
ESP_LOGI(TAG_PROVISION, "Verification: Config zone is now locked");
return true;
} else {
ESP_LOGE(TAG_PROVISION,
"Verification failed: Config zone still appears unlocked");
return false;
}
}
/**
* Provision the ATECC608B Data Zone with required keys
* Returns: true on success, false on failure
*/
bool provision_atecc608b_data_zone() {
ESP_LOGI(TAG_PROVISION, "=== Starting ATECC608B Data Zone Provisioning ===");
// 1. Prerequisites check
if (is_atecc608b_data_locked()) {
ESP_LOGW(TAG_PROVISION,
"Data zone is already locked! Cannot provision keys.");
return false;
}
if (!is_atecc608b_config_locked()) {
ESP_LOGE(TAG_PROVISION,
"Config zone MUST be locked before generating keys (GenKey).");
return false;
}
ATCA_STATUS status;
// 2. Write Symmetric Keys (Slots 6, 10, 13)
ESP_LOGI(TAG_PROVISION, "Writing IO_KEY to Slot 6...");
status = atcab_write_bytes_zone(ATCA_ZONE_DATA, 6, 0, IO_KEY, 32);
if (status != ATCA_SUCCESS) {
ESP_LOGE(TAG_PROVISION, "Failed Slot 6: 0x%02X", status);
return false;
}
ESP_LOGI(TAG_PROVISION, "Writing HMAC_KEY to Slot 10...");
status = atcab_write_bytes_zone(ATCA_ZONE_DATA, 10, 0, HMAC_KEY, 32);
if (status != ATCA_SUCCESS) {
ESP_LOGE(TAG_PROVISION, "Failed Slot 10: 0x%02X", status);
return false;
}
ESP_LOGI(TAG_PROVISION, "Writing HMAC_KEY to Slot 13...");
status = atcab_write_bytes_zone(ATCA_ZONE_DATA, 13, 0, HMAC_KEY, 32);
if (status != ATCA_SUCCESS) {
ESP_LOGE(TAG_PROVISION, "Failed Slot 13: 0x%02X", status);
return false;
}
// 3. Write ECC Public Key to Slot 9
// Note: The 0x04 prefix byte is stripped. The ATECC608B expects exactly 64
// bytes.
static const uint8_t SLOT9_PUB_KEY[64] = {
0x76, 0xc1, 0xa2, 0xe9, 0x63, 0xda, 0x58, 0x41, 0x12, 0x4e, 0xe7,
0xc5, 0x3b, 0xeb, 0x2d, 0xad, 0x72, 0xf4, 0xc4, 0x61, 0xb8, 0x4a,
0x65, 0xb7, 0xc7, 0x91, 0xdd, 0x59, 0xf9, 0x0a, 0xad, 0xf0, 0x6f,
0x13, 0xf2, 0xb6, 0x29, 0x05, 0x4f, 0xab, 0x98, 0xdc, 0xfb, 0x93,
0xab, 0xbd, 0x90, 0xd1, 0xea, 0x92, 0x91, 0x0a, 0xfe, 0x95, 0x7c,
0xf6, 0xc7, 0x97, 0x41, 0x8e, 0x96, 0x6c, 0xaa, 0x15};
ESP_LOGI(TAG_PROVISION, "Writing ECC Public Key to Slot 9...");
status = atcab_write_pubkey(9, SLOT9_PUB_KEY);
if (status != ATCA_SUCCESS) {
ESP_LOGE(TAG_PROVISION, "Failed Slot 9: 0x%02X", status);
return false;
}
// 4. Generate Random ECC Private Keys (Slots 0-5, 11, 12)
uint8_t pubkey[64]; // Buffer to catch the generated public keys
const uint8_t genkey_slots[] = {0, 1, 2, 3, 4, 5, 11, 12};
for (int i = 0; i < sizeof(genkey_slots) / sizeof(genkey_slots[0]); i++) {
uint8_t slot = genkey_slots[i];
ESP_LOGI(TAG_PROVISION, "Generating ECC Private Key in Slot %d...", slot);
status = atcab_genkey(slot, pubkey);
if (status != ATCA_SUCCESS) {
ESP_LOGE(TAG_PROVISION, "Failed GenKey Slot %d: 0x%02X", slot, status);
return false;
}
ESP_LOGI(TAG_PROVISION, "Successfully generated Private Key in Slot %d.",
slot);
ESP_LOGI(TAG_PROVISION, "Corresponding Public Key (64 bytes):");
// Standard ESP-IDF hex dump (good for reading)
ESP_LOG_BUFFER_HEX(TAG_PROVISION, pubkey, sizeof(pubkey));
// Continuous hex string (good for copy-pasting to host/scripts)
printf("--> Copy this public key for Slot %d: 04",
slot); // Prepending the 0x04 uncompressed prefix
for (int j = 0; j < 64; j++) {
printf("%02x", pubkey[j]);
}
printf("\n\n");
}
// 5. Lock the Data Zone
ESP_LOGI(TAG_PROVISION, "Locking Data Zone...");
status = atcab_lock_data_zone();
if (status != ATCA_SUCCESS) {
ESP_LOGE(TAG_PROVISION, "Failed to lock Data Zone: 0x%02X", status);
return false;
}
ESP_LOGI(TAG_PROVISION,
"ATECC608B Data Zone provisioned and locked successfully!");
return true;
}
/**
* Check if provisioning is needed
* Returns: true if any condition requires provisioning
*/
bool needs_provisioning() {
bool config_unlocked = !is_atecc608b_config_locked();
bool data_unlocked = !is_atecc608b_data_locked();
bool key5_unused = !is_efuse_key5_used();
ESP_LOGI(TAG_PROVISION, "Provisioning check:");
ESP_LOGI(TAG_PROVISION, " - Config unlocked: %s",
config_unlocked ? "YES" : "NO");
ESP_LOGI(TAG_PROVISION, " - Data unlocked: %s",
data_unlocked ? "YES" : "NO");
ESP_LOGI(TAG_PROVISION, " - KEY5 unused: %s", key5_unused ? "YES" : "NO");
ESP_LOGI(TAG_PROVISION, " - Needs provisioning: %s",
(config_unlocked || data_unlocked || key5_unused) ? "YES" : "NO");
return (config_unlocked || data_unlocked || key5_unused);
}
/**
* Provision the ESP32-C6 efuse BLOCK_KEY5 with hardcoded HMAC key
* Returns: true on success, false on failure
*/
bool provision_efuse_key5() {
ESP_LOGI(TAG_PROVISION, "=== Starting EFUSE KEY5 Provisioning ===");
// Check if already used
if (is_efuse_key5_used()) {
ESP_LOGW(TAG_PROVISION, "EFUSE KEY5 is already used!");
return false;
}
// Write the HMAC key to BLOCK_KEY5
ESP_LOGI(TAG_PROVISION, "Writing HMAC key to BLOCK_KEY5...");
esp_err_t err =
esp_efuse_write_key(EFUSE_BLK_KEY5, ESP_EFUSE_KEY_PURPOSE_HMAC_UP,
HMAC_KEY, sizeof(HMAC_KEY));
if (err != ESP_OK) {
ESP_LOGE(TAG_PROVISION, "Failed to write EFUSE KEY5: %s",
esp_err_to_name(err));
return false;
}
ESP_LOGI(TAG_PROVISION, "EFUSE KEY5 provisioned successfully!");
ESP_LOGI(TAG_PROVISION, "Purpose set to: HMAC_UP");
// Verify it was written
if (is_efuse_key5_used()) {
ESP_LOGI(TAG_PROVISION, "Verification: KEY5 is now marked as used");
return true;
} else {
ESP_LOGE(TAG_PROVISION, "Verification failed: KEY5 still appears unused");
return false;
}
}
#endif // PROVISION_H

View File

@@ -1,187 +0,0 @@
#ifndef PROVISION_HANDLERS_H
#define PROVISION_HANDLERS_H
#include "provision.h"
#include "provision_web_page.h"
#include <cJSON.h>
#include <esp_http_server.h>
#include <esp_log.h>
static const char *TAG_PROVISION_HANDLERS = "provision_handlers";
// Forward declarations
extern httpd_handle_t server_http;
/**
* Serve the provisioning web page
*/
static esp_err_t handle_provision_page(httpd_req_t *req) {
httpd_resp_set_type(req, "text/html");
httpd_resp_sendstr(req, PROVISION_WEB_PAGE);
return ESP_OK;
}
/**
* API endpoint: Get provisioning status
*/
static esp_err_t handle_provision_status(httpd_req_t *req) {
cJSON *response = cJSON_CreateObject();
bool config_unlocked = !is_atecc608b_config_locked();
bool data_unlocked = !is_atecc608b_data_locked();
bool key5_unused = !is_efuse_key5_used();
bool needs_prov = needs_provisioning();
cJSON_AddBoolToObject(response, "needs_provisioning", needs_prov);
cJSON_AddBoolToObject(response, "config_unlocked", config_unlocked);
cJSON_AddBoolToObject(response, "data_unlocked", data_unlocked);
cJSON_AddBoolToObject(response, "key5_unused", key5_unused);
char *json_str = cJSON_PrintUnformatted(response);
cJSON_Delete(response);
httpd_resp_set_type(req, "application/json");
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
httpd_resp_sendstr(req, json_str);
free(json_str);
return ESP_OK;
}
/**
* API endpoint: Provision the device EFUSE
*/
static esp_err_t handle_provision_api(httpd_req_t *req) {
ESP_LOGI(TAG_PROVISION_HANDLERS, "Provisioning API called");
cJSON *response = cJSON_CreateObject();
bool success = false;
const char *message = "Unknown error";
// Check if provisioning is actually needed
if (!needs_provisioning()) {
message = "Device is already provisioned";
success = true;
ESP_LOGI(TAG_PROVISION_HANDLERS, "%s", message);
} else {
bool atecc_done = true;
bool efuse_done = true;
// Step 1: Provision ATECC608B config if needed
if (!is_atecc608b_config_locked()) {
ESP_LOGI(TAG_PROVISION_HANDLERS,
"Provisioning ATECC608B configuration...");
atecc_done = provision_atecc608b_config();
if (!atecc_done) {
message = "Failed to provision ATECC608B configuration";
success = false;
ESP_LOGE(TAG_PROVISION_HANDLERS, "%s", message);
}
}
// Step 2: Provision EFUSE KEY5 if needed and previous step succeeded
if (atecc_done && !is_efuse_key5_used()) {
ESP_LOGI(TAG_PROVISION_HANDLERS, "Provisioning EFUSE KEY5...");
efuse_done = provision_efuse_key5();
if (!efuse_done) {
message = "Failed to provision EFUSE KEY5";
success = false;
ESP_LOGE(TAG_PROVISION_HANDLERS, "%s", message);
}
}
if (!is_atecc608b_data_locked()) {
ESP_LOGI(TAG_PROVISION_HANDLERS, "Provisioning ATECC608B data zone...");
bool data_done = provision_atecc608b_data_zone();
if (!data_done) {
message = "Failed to provision ATECC608B data zone";
success = false;
ESP_LOGE(TAG_PROVISION_HANDLERS, "%s", message);
}
}
// Build success message
if (atecc_done && efuse_done) {
if (!is_atecc608b_config_locked() && !is_efuse_key5_used()) {
message =
"Provisioned ATECC608B configuration and EFUSE KEY5 successfully";
} else if (!is_atecc608b_config_locked()) {
message = "ATECC608B configuration provisioned and locked successfully";
} else {
message = "EFUSE KEY5 provisioned successfully with HMAC key";
}
success = true;
ESP_LOGI(TAG_PROVISION_HANDLERS, "%s", message);
}
}
// Build JSON response
cJSON_AddBoolToObject(response, "success", success);
cJSON_AddStringToObject(response, "message", message);
char *json_str = cJSON_PrintUnformatted(response);
cJSON_Delete(response);
// Send response
httpd_resp_set_type(req, "application/json");
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
httpd_resp_set_status(req, success ? "200 OK" : "500 Internal Server Error");
httpd_resp_sendstr(req, json_str);
free(json_str);
return ESP_OK;
}
/**
* Handle CORS preflight for provision API
*/
static esp_err_t handle_provision_options(httpd_req_t *req) {
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "POST, GET, OPTIONS");
httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type");
httpd_resp_set_status(req, "204 No Content");
httpd_resp_send(req, NULL, 0);
return ESP_OK;
}
/**
* Register provisioning handlers
*/
void register_provision_handlers(httpd_handle_t server) {
// Provision page
httpd_uri_t provision_page_uri = {.uri = "/provision",
.method = HTTP_GET,
.handler = handle_provision_page,
.user_ctx = NULL};
httpd_register_uri_handler(server, &provision_page_uri);
// API endpoint for getting status
httpd_uri_t provision_status_uri = {.uri = "/api/provision/status",
.method = HTTP_GET,
.handler = handle_provision_status,
.user_ctx = NULL};
httpd_register_uri_handler(server, &provision_status_uri);
// API endpoint for provisioning
httpd_uri_t provision_api_uri = {.uri = "/api/provision",
.method = HTTP_POST,
.handler = handle_provision_api,
.user_ctx = NULL};
httpd_register_uri_handler(server, &provision_api_uri);
// CORS preflight
httpd_uri_t provision_options_uri = {.uri = "/api/provision",
.method = HTTP_OPTIONS,
.handler = handle_provision_options,
.user_ctx = NULL};
httpd_register_uri_handler(server, &provision_options_uri);
ESP_LOGI(TAG_PROVISION_HANDLERS, "Provision routes registered:");
ESP_LOGI(TAG_PROVISION_HANDLERS,
" GET /provision - Provision page");
ESP_LOGI(TAG_PROVISION_HANDLERS,
" GET /api/provision/status - Provision status");
ESP_LOGI(TAG_PROVISION_HANDLERS, " POST /api/provision - Provision API");
}
#endif // PROVISION_HANDLERS_H

View File

@@ -1,323 +0,0 @@
#ifndef PROVISION_WEB_PAGE_H
#define PROVISION_WEB_PAGE_H
// Embedded web interface for device provisioning
const char PROVISION_WEB_PAGE[] = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 Provisioning</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 100%;
padding: 40px;
animation: slideIn 0.5s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #333;
font-size: 28px;
margin-bottom: 10px;
}
.header p {
color: #666;
font-size: 14px;
}
.warning-box {
background: #fff3cd;
border: 2px solid #ffc107;
border-radius: 10px;
padding: 20px;
margin-bottom: 30px;
}
.warning-box h3 {
color: #856404;
margin-bottom: 10px;
font-size: 18px;
}
.warning-box ul {
margin-left: 20px;
color: #856404;
}
.warning-box li {
margin-bottom: 5px;
}
.info-box {
background: #e7f3ff;
border: 2px solid #2196F3;
border-radius: 10px;
padding: 20px;
margin-bottom: 30px;
}
.info-box h3 {
color: #1976D2;
margin-bottom: 10px;
font-size: 18px;
}
.info-box p {
color: #1565C0;
line-height: 1.6;
}
.provision-btn {
width: 100%;
padding: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 15px;
}
.provision-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4);
}
.provision-btn:active:not(:disabled) {
transform: translateY(0);
}
.provision-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.status-message {
padding: 15px;
border-radius: 10px;
margin-bottom: 15px;
display: none;
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.status-message.success {
background: #d4edda;
border: 2px solid #28a745;
color: #155724;
display: block;
}
.status-message.error {
background: #f8d7da;
border: 2px solid #dc3545;
color: #721c24;
display: block;
}
.status-message.info {
background: #d1ecf1;
border: 2px solid #17a2b8;
color: #0c5460;
display: block;
}
.skip-link {
text-align: center;
margin-top: 20px;
}
.skip-link a {
color: #667eea;
text-decoration: none;
font-size: 14px;
}
.skip-link a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔧 Device Provisioning Required</h1>
<p>Your device needs to be provisioned before first use</p>
</div>
<div class="warning-box">
<h3> Provisioning Needed</h3>
<ul id="provisionReasons">
<li>Loading status...</li>
</ul>
</div>
<div class="info-box">
<h3> What This Does</h3>
<p>
This will provision your device security hardware:
</p>
<ul style="margin-top: 10px; margin-left: 20px; color: #1565C0;">
<li><strong>ATECC608B:</strong> Write secure configuration (SlotConfig, KeyConfig, ChipOptions) and lock the config zone</li>
<li><strong>ESP32-C6:</strong> Write hardcoded HMAC key to EFUSE BLOCK_KEY5 with purpose <code>EFUSE_KEY_PURPOSE_HMAC_UP</code></li>
</ul>
<p style="margin-top: 10px; font-weight: bold;">
Warning: These are one-time operations and cannot be undone!
</p>
</div>
<div id="statusMessage" class="status-message"></div>
Device
<button id="provisionBtn" class="provision-btn" onclick="provisionDevice()">
Provision EFUSE KEY5
</button>
<div class="skip-link">
<a href="/">Skip and continue anyway</a>
</div>
</div>
<script>
// Fetch and display current provisioning status on page load
async function loadProvisionStatus() {
const reasonsList = document.getElementById('provisionReasons');
try {
const response = await fetch('/api/provision/status');
const status = await response.json();
// Clear loading message
reasonsList.innerHTML = '';
// Build list of actual conditions
if (status.config_unlocked) {
const li = document.createElement('li');
li.textContent = 'ATECC608B Config Zone is unlocked';
reasonsList.appendChild(li);
}
if (status.data_unlocked) {
const li = document.createElement('li');
li.textContent = 'ATECC608B Data Zone is unlocked';
reasonsList.appendChild(li);
}
if (status.key5_unused) {
const li = document.createElement('li');
li.textContent = 'ESP32-C6 EFUSE BLOCK_KEY5 is not configured';
reasonsList.appendChild(li);
}
// If somehow no conditions are true, show message
if (!status.needs_provisioning) {
reasonsList.innerHTML = '<li>No provisioning needed (you may skip)</li>';
}
} catch (error) {
reasonsList.innerHTML = '<li>Error loading status: ' + error.message + '</li>';
}
}
async function provisionDevice() {
const btn = document.getElementById('provisionBtn');
const statusMsg = document.getElementById('statusMessage');
// Disable button
btn.disabled = true;
btn.textContent = 'Provisioning...';
// Show info messageProvisioning device security hardware
statusMsg.className = 'status-message info';
statusMsg.textContent = 'Writing HMAC key to EFUSE BLOCK_KEY5...';
try {
const response = await fetch('/api/provision', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (response.ok && result.success) {
statusMsg.className = 'status-message success';
statusMsg.textContent = ' ' + result.message;
btn.textContent = 'Provisioning Complete!';
// Redirect to home after 2 seconds
setTimeout(() => {
window.location.href = '/';
}, 2000);
} else {
statusMsg.className = 'status-message error';
statusMsg.textContent = ' Error: ' + (result.message || 'Unknown error');
btn.disabled = false;
btn.textContent = 'Retry Provisioning';
}
} catch (error) {
statusMsg.className = 'status-message error';
statusMsg.textContent = ' Connection error: ' + error.message;
btn.disabled = false;
btn.textContent = 'Retry Provisioning';
}
}
// Load status when page loads
window.addEventListener('DOMContentLoaded', loadProvisionStatus);
</script>
</body>
</html>
)rawliteral";
#endif // PROVISION_WEB_PAGE_H

View File

@@ -1,288 +0,0 @@
#ifndef TANG_HANDLERS_H
#define TANG_HANDLERS_H
#include "crypto.h"
#include "cryptoauthlib.h"
#include "encoding.h"
#include "tang_storage.h"
#include <cJSON.h>
#include <esp_http_server.h>
#include <esp_log.h>
#include <mbedtls/sha256.h>
#include <string.h>
static const char *TAG_HANDLERS = "tang_handlers";
// Forward declarations
extern httpd_handle_t server_http;
extern TangKeyStore keystore;
extern bool unlocked;
// GET /adv - Advertisement endpoint (signed JWK set)
static esp_err_t handle_adv(httpd_req_t *req) {
if (!unlocked) {
httpd_resp_set_status(req, "503 Service Unavailable");
httpd_resp_sendstr(req, "Server not active");
return ESP_FAIL;
}
uint8_t sig_pub[ATCA_PUB_KEY_SIZE];
if (atcab_get_pubkey(1, sig_pub) != ATCA_SUCCESS) {
ESP_LOGE(TAG_HANDLERS, "Failed to read public keys from secure element");
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Hardware error");
return ESP_FAIL;
}
char sig_x_b64[64] = {0}, sig_y_b64[64] = {0};
char rec_x_b64[64] = {0}, rec_y_b64[64] = {0};
b64url_encode_buf(&sig_pub[0], 32, sig_x_b64, sizeof(sig_x_b64));
b64url_encode_buf(&sig_pub[32], 32, sig_y_b64, sizeof(sig_y_b64));
b64url_encode_buf(&keystore.exc_pub[0], 32, rec_x_b64, sizeof(rec_x_b64));
b64url_encode_buf(&keystore.exc_pub[32], 32, rec_y_b64, sizeof(rec_y_b64));
// Build JWK set payload using cJSON
cJSON *payload_root = cJSON_CreateObject();
cJSON *keys = cJSON_CreateArray();
// Signing/verification key
cJSON *sig_key = cJSON_CreateObject();
cJSON_AddStringToObject(sig_key, "kty", "EC");
cJSON_AddStringToObject(sig_key, "alg", "ES256");
cJSON_AddStringToObject(sig_key, "crv", "P-256");
cJSON_AddStringToObject(sig_key, "x", sig_x_b64);
cJSON_AddStringToObject(sig_key, "y", sig_y_b64);
cJSON *sig_key_ops = cJSON_CreateArray();
cJSON_AddItemToArray(sig_key_ops, cJSON_CreateString("verify"));
cJSON_AddItemToObject(sig_key, "key_ops", sig_key_ops);
cJSON_AddItemToArray(keys, sig_key);
cJSON *rec_key = cJSON_CreateObject();
cJSON_AddStringToObject(rec_key, "alg", "ECMR");
cJSON_AddStringToObject(rec_key, "kty", "EC");
cJSON_AddStringToObject(rec_key, "crv", "P-256");
cJSON_AddStringToObject(rec_key, "x", rec_x_b64);
cJSON_AddStringToObject(rec_key, "y", rec_y_b64);
cJSON *rec_key_ops = cJSON_CreateArray();
cJSON_AddItemToArray(rec_key_ops, cJSON_CreateString("deriveKey"));
cJSON_AddItemToObject(rec_key, "key_ops", rec_key_ops);
cJSON_AddItemToArray(keys, rec_key);
cJSON_AddItemToObject(payload_root, "keys", keys);
// Print payload JSON and encode to B64 dynamically
char *payload_json = cJSON_PrintUnformatted(payload_root);
size_t payload_len = strlen(payload_json);
size_t payload_b64_size = ((payload_len + 2) / 3) * 4 + 1;
char *payload_b64 = (char *)malloc(payload_b64_size);
b64url_encode_buf((uint8_t *)payload_json, payload_len, payload_b64,
payload_b64_size);
free(payload_json);
cJSON_Delete(payload_root);
// Create protected header
cJSON *protected_root = cJSON_CreateObject();
cJSON_AddStringToObject(protected_root, "alg", "ES256");
cJSON_AddStringToObject(protected_root, "cty", "jwk-set+json");
char *protected_json = cJSON_PrintUnformatted(protected_root);
size_t protected_len = strlen(protected_json);
size_t protected_b64_size = ((protected_len + 2) / 3) * 4 + 1;
char *protected_b64 = (char *)malloc(protected_b64_size);
b64url_encode_buf((uint8_t *)protected_json, protected_len, protected_b64,
protected_b64_size);
free(protected_json);
cJSON_Delete(protected_root);
// Sign the payload
size_t signing_input_size =
strlen(protected_b64) + 1 + strlen(payload_b64) + 1;
char *signing_input = (char *)malloc(signing_input_size);
snprintf(signing_input, signing_input_size, "%s.%s", protected_b64,
payload_b64);
uint8_t hash[32];
mbedtls_sha256((const uint8_t *)signing_input, strlen(signing_input), hash,
0);
free(signing_input);
uint8_t signature[ATCA_ECCP256_SIG_SIZE];
if (atcab_sign(1, hash, signature) != ATCA_SUCCESS) {
ESP_LOGE(TAG_HANDLERS, "Hardware signing failed");
free(payload_b64);
free(protected_b64);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Signing failed");
return ESP_FAIL;
}
char sig_b64[128] = {0};
b64url_encode_buf(signature, sizeof(signature), sig_b64, sizeof(sig_b64));
cJSON *jws_root = cJSON_CreateObject();
cJSON_AddStringToObject(jws_root, "payload", payload_b64);
cJSON_AddStringToObject(jws_root, "protected", protected_b64);
cJSON_AddStringToObject(jws_root, "signature", sig_b64);
char *response = cJSON_PrintUnformatted(jws_root);
cJSON_Delete(jws_root);
free(payload_b64);
free(protected_b64);
httpd_resp_set_type(
req, "application/jose+json"); // Tang expects this specific type
httpd_resp_sendstr(req, response);
free(response);
ESP_LOGI(TAG_HANDLERS, "Served /adv");
return ESP_OK;
}
// POST /rec or /rec/{kid} - Recovery endpoint
static esp_err_t handle_rec(httpd_req_t *req) {
if (!unlocked) {
httpd_resp_set_status(req, "503 Service Unavailable");
httpd_resp_sendstr(req, "Server not active");
return ESP_FAIL;
}
// Read request body
char *buf = (char *)malloc(req->content_len + 1);
if (!buf) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Memory allocation failed");
return ESP_FAIL;
}
int ret = httpd_req_recv(req, buf, req->content_len);
if (ret <= 0) {
free(buf);
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
httpd_resp_send_408(req);
}
return ESP_FAIL;
}
buf[ret] = '\0';
// Parse JSON
cJSON *req_doc = cJSON_Parse(buf);
free(buf);
if (!req_doc) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
return ESP_FAIL;
}
// Extract client's ephemeral public key
cJSON *x_item = cJSON_GetObjectItem(req_doc, "x");
cJSON *y_item = cJSON_GetObjectItem(req_doc, "y");
if (!x_item || !y_item || !cJSON_IsString(x_item) ||
!cJSON_IsString(y_item)) {
cJSON_Delete(req_doc);
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
"Missing x or y coordinates");
return ESP_FAIL;
}
uint8_t client_pub_key[P256_PUBLIC_KEY_SIZE];
// Use the new helper to decode and strictly validate the length of both
// coordinates
if (!b64url_decode_buf(x_item->valuestring, &client_pub_key[0],
P256_COORDINATE_SIZE) ||
!b64url_decode_buf(y_item->valuestring,
&client_pub_key[P256_COORDINATE_SIZE],
P256_COORDINATE_SIZE)) {
cJSON_Delete(req_doc);
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid key coordinates");
return ESP_FAIL;
}
cJSON_Delete(req_doc);
// Perform ECDH to get shared point
uint8_t shared_point[P256_PUBLIC_KEY_SIZE];
if (!P256::ecdh_compute_shared_point(client_pub_key, keystore.exc_priv,
shared_point, true)) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"ECDH computation failed");
return ESP_FAIL;
}
char shared_x_b64[64] = {0};
char shared_y_b64[64] = {0};
b64url_encode_buf(&shared_point[0], P256_COORDINATE_SIZE, shared_x_b64,
sizeof(shared_x_b64));
b64url_encode_buf(&shared_point[P256_COORDINATE_SIZE], P256_COORDINATE_SIZE,
shared_y_b64, sizeof(shared_y_b64));
// Return shared point as JWK
cJSON *resp_root = cJSON_CreateObject();
cJSON_AddStringToObject(resp_root, "alg", "ECMR");
cJSON_AddStringToObject(resp_root, "kty", "EC");
cJSON_AddStringToObject(resp_root, "crv", "P-256");
cJSON_AddStringToObject(resp_root, "x", shared_x_b64);
cJSON_AddStringToObject(resp_root, "y", shared_y_b64);
cJSON *key_ops = cJSON_CreateArray();
cJSON_AddItemToArray(key_ops, cJSON_CreateString("deriveKey"));
cJSON_AddItemToObject(resp_root, "key_ops", key_ops);
char *response = cJSON_PrintUnformatted(resp_root);
cJSON_Delete(resp_root);
httpd_resp_set_type(req, "application/jose+json");
httpd_resp_sendstr(req, response);
free(response);
ESP_LOGI(TAG_HANDLERS, "Served %s", req->uri);
return ESP_OK;
}
// --- Administration Handlers ---
// GET /config - Get ATECC608B configuration
static esp_err_t handle_config(httpd_req_t *req) {
ESP_LOGI(TAG_HANDLERS, "Serving ATECC608B config");
char *json_str = atecc608B_get_config_json();
if (!json_str) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Config data not available");
return ESP_FAIL;
}
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, json_str);
free(json_str);
ESP_LOGI(TAG_HANDLERS, "Served /config");
return ESP_OK;
}
// GET /reboot - Reboot device
static esp_err_t handle_reboot(httpd_req_t *req) {
httpd_resp_sendstr(req, "Rebooting...");
vTaskDelay(pdMS_TO_TICKS(1000));
esp_restart();
return ESP_OK;
}
// 404 handler - Handle /rec/{kid} by routing to handle_rec
static esp_err_t handle_not_found(httpd_req_t *req, httpd_err_code_t err) {
// Check if it's a POST to /rec/{kid}
if (req->method == HTTP_POST && strncmp(req->uri, "/rec/", 5) == 0) {
return handle_rec(req);
}
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Not found");
return ESP_FAIL;
}
#endif // TANG_HANDLERS_H

View File

@@ -1,87 +0,0 @@
#ifndef TANG_STORAGE_H
#define TANG_STORAGE_H
#include "crypto.h"
#include "encoding.h"
#include <cstring>
#include <esp_log.h>
#include <esp_random.h>
#include <nvs.h>
#include <nvs_flash.h>
static const char *TAG_STORAGE = "tang_storage";
// --- Key Storage & Management ---
class TangKeyStore {
private:
// No longer need salt for password-based encryption
public:
// Tang server keys (encrypted at rest, decrypted in memory when active)
uint8_t exc_priv[P256_PRIVATE_KEY_SIZE];
uint8_t exc_pub[P256_PUBLIC_KEY_SIZE];
// Admin key (persistent)
uint8_t admin_priv[P256_PRIVATE_KEY_SIZE];
uint8_t admin_pub[P256_PUBLIC_KEY_SIZE];
bool is_configured() {
nvs_handle_t handle;
esp_err_t err = nvs_open("tang-server", NVS_READWRITE, &handle);
if (err != ESP_OK) {
return false;
}
size_t required_size = 0;
err = nvs_get_blob(handle, "admin_key", nullptr, &required_size);
bool configured = (err == ESP_OK && required_size == P256_PRIVATE_KEY_SIZE);
nvs_close(handle);
return configured;
}
// Save Tang keys directly to NVS (no encryption)
bool save_tang_keys() {
nvs_handle_t handle;
esp_err_t err = nvs_open("tang-server", NVS_READWRITE, &handle);
if (err != ESP_OK) {
ESP_LOGE(TAG_STORAGE, "Failed to open NVS: %s", esp_err_to_name(err));
return false;
}
// Save exchange key
err = nvs_set_blob(handle, "tang_exc_key", exc_priv, P256_PRIVATE_KEY_SIZE);
if (err != ESP_OK) {
nvs_close(handle);
return false;
}
err = nvs_commit(handle);
nvs_close(handle);
return (err == ESP_OK);
}
// Load Tang keys directly from NVS (no decryption)
bool load_tang_keys() {
nvs_handle_t handle;
esp_err_t err = nvs_open("tang-server", NVS_READONLY, &handle);
if (err != ESP_OK) {
ESP_LOGE(TAG_STORAGE, "Failed to open NVS: %s", esp_err_to_name(err));
return false;
}
// Load exchange key
size_t len = P256_PRIVATE_KEY_SIZE;
err = nvs_get_blob(handle, "tang_exc_key", exc_priv, &len);
if (err != ESP_OK || len != P256_PRIVATE_KEY_SIZE) {
nvs_close(handle);
return false;
}
P256::compute_public_key(exc_priv, exc_pub);
nvs_close(handle);
return true;
}
};
#endif // TANG_STORAGE_H

View File

@@ -8,6 +8,7 @@
#include <esp_mac.h> #include <esp_mac.h>
#include <esp_system.h> #include <esp_system.h>
#include <mbedtls/aes.h> #include <mbedtls/aes.h>
#include <mbedtls/base64.h>
#include <mbedtls/ctr_drbg.h> #include <mbedtls/ctr_drbg.h>
#include <mbedtls/ecdh.h> #include <mbedtls/ecdh.h>
#include <mbedtls/ecp.h> #include <mbedtls/ecp.h>
@@ -40,8 +41,6 @@ private:
uint8_t stored_password_hash[32]; // PBKDF2 hash of the correct password uint8_t stored_password_hash[32]; // PBKDF2 hash of the correct password
bool initialized; bool initialized;
bool password_set;
bool unlocked = false; // Set to true after successful authentication
// Convert binary to hex string // Convert binary to hex string
void bin_to_hex(const uint8_t *bin, size_t bin_len, char *hex) { void bin_to_hex(const uint8_t *bin, size_t bin_len, char *hex) {
@@ -65,7 +64,7 @@ private:
} }
public: public:
ZKAuth() : initialized(false), password_set(false), unlocked(false) { ZKAuth() : initialized(false) {
mbedtls_ecp_group_init(&grp); mbedtls_ecp_group_init(&grp);
mbedtls_mpi_init(&device_private_d); mbedtls_mpi_init(&device_private_d);
mbedtls_ecp_point_init(&device_public_Q); mbedtls_ecp_point_init(&device_public_Q);
@@ -83,60 +82,78 @@ public:
// Initialize the ZK authentication system // Initialize the ZK authentication system
bool init() { bool init() {
if (initialized) if (initialized) {
return true; return true;
} else {
// Seed the random number generator
const char *pers = "zk_auth_esp32";
int ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy,
(const unsigned char *)pers, strlen(pers));
if (ret != 0) {
printf("mbedtls_ctr_drbg_seed failed: -0x%04x\n", -ret);
return false;
}
// Setup ECC group for NIST P-256 (secp256r1)
ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1);
if (ret != 0) {
printf("mbedtls_ecp_group_load failed: -0x%04x\n", -ret);
return false;
}
// Generate device keypair
ret = mbedtls_ecdh_gen_public(&grp, &device_private_d, &device_public_Q,
mbedtls_ctr_drbg_random, &ctr_drbg);
if (ret != 0) {
printf("mbedtls_ecdh_gen_public failed: -0x%04x\n", -ret);
return false;
}
// Export public key to uncompressed format (0x04 + X + Y)
size_t olen;
ret = mbedtls_ecp_point_write_binary(
&grp, &device_public_Q, MBEDTLS_ECP_PF_UNCOMPRESSED, &olen,
device_public_key, sizeof(device_public_key));
if (ret != 0 || olen != 65) {
printf("mbedtls_ecp_point_write_binary failed: -0x%04x\n", -ret);
return false;
}
initialized = true; initialized = true;
printf("\n=== ZK Authentication Initialized ===\n"); printf("\n=== ZK Authentication Initialized ===\n");
printf("Public Key: ");
for (int i = 0; i < 65; i++) {
printf("%02x", device_public_key[i]);
} }
printf("\n");
printf("=====================================\n\n");
return true; return true;
} }
// Helper function to write a 32-bit integer in big-endian format
void write_uint32_be(uint8_t *buf, uint32_t val) {
buf[0] = (val >> 24) & 0xFF;
buf[1] = (val >> 16) & 0xFF;
buf[2] = (val >> 8) & 0xFF;
buf[3] = val & 0xFF;
}
// Function to generate the authorized_keys string
void generate_ssh_authorized_key(const uint8_t *atec_pubkey) {
uint8_t ssh_blob[104];
uint32_t offset = 0;
// 1. Key Type
const char *key_type = "ecdsa-sha2-nistp256";
write_uint32_be(&ssh_blob[offset], 19);
offset += 4;
memcpy(&ssh_blob[offset], key_type, 19);
offset += 19;
// 2. Curve Name
const char *curve_name = "nistp256";
write_uint32_be(&ssh_blob[offset], 8);
offset += 4;
memcpy(&ssh_blob[offset], curve_name, 8);
offset += 8;
// 3. Public Key (Uncompressed format: 0x04 + 64 bytes X/Y)
write_uint32_be(&ssh_blob[offset], 65);
offset += 4;
ssh_blob[offset++] = 0x04;
memcpy(&ssh_blob[offset], atec_pubkey, 64);
offset += 64;
// 4. Base64 Encode the blob
size_t b64_len = 0;
// Call once to get required length
mbedtls_base64_encode(NULL, 0, &b64_len, ssh_blob, 104);
unsigned char b64_out[b64_len];
// Call again to actually encode
mbedtls_base64_encode(b64_out, b64_len, &b64_len, ssh_blob, 104);
// 5. Print out the final authorized_keys line
printf("ecdsa-sha2-nistp256 %s esp32-atecc608b\n", b64_out);
}
// Get device identity (for /api/identity endpoint) // Get device identity (for /api/identity endpoint)
char *get_identity_json() { char *get_identity_json() {
char pubkey_hex[131]; // 65 bytes * 2 + null char pubkey_hex[131]; // 65 bytes * 2 + null
bin_to_hex(device_public_key, 65, pubkey_hex); uint8_t atec_pubkey[64]; // 65 bytes * 2 + null
uint8_t standard_pubkey[65];
standard_pubkey[0] = 0x04;
// Get public key from ATECC608B and convert to hex
ATCA_STATUS status = atcab_get_pubkey(0, atec_pubkey);
if (status != ATCA_SUCCESS) {
printf("Failed to read public key from ATECC608B: 0x%02X\n", status);
}
generate_ssh_authorized_key(atec_pubkey);
memcpy(&standard_pubkey[1], atec_pubkey, 64);
bin_to_hex(standard_pubkey, 65, pubkey_hex);
// Get MAC address to use as salt // Get MAC address to use as salt
uint8_t mac[6]; uint8_t mac[6];
@@ -152,384 +169,6 @@ public:
cJSON_Delete(root); cJSON_Delete(root);
return json_str; return json_str;
} }
void my_host_check_mac(const uint8_t *target_key, const uint8_t *tempkey,
const uint8_t *other_data, const uint8_t *sn,
uint8_t *out_mac) {
// See section 11.2 of the ATECC608B datasheet CheckMac message format
uint8_t msg[88] = {0};
memcpy(&msg[0], target_key, 32);
memcpy(&msg[32], tempkey, 32);
memcpy(&msg[64], &other_data[0], 4);
memset(&msg[68], 0, 8);
memcpy(&msg[76], &other_data[4], 3);
msg[79] = sn[8];
memcpy(&msg[80], &other_data[7], 4);
msg[84] = sn[0];
msg[85] = sn[1];
memcpy(&msg[86], &other_data[11], 2);
// Hash the exact 88 bytes using the ESP32's hardware accelerator
mbedtls_sha256(msg, sizeof(msg), out_mac, 0);
}
bool verify_key(const uint8_t *received_key, size_t key_len) {
(void)key_len;
if (received_key == NULL)
return false;
ATCA_STATUS status;
bool latch_status = false;
status = atcab_info_get_latch(&latch_status);
printf("Device Latch Status: %s (0x%02X)\n",
latch_status ? "LATCHED" : "UNLATCHED", latch_status);
uint8_t sn[ATCA_SERIAL_NUM_SIZE] = {0};
status = atcab_read_serial_number(sn);
if (status != ATCA_SUCCESS) {
printf("Failed to read Serial Number\n");
return false;
}
uint8_t nonce_num_in[20] = {0xAA, 0xBB, 0xCC, 0xDD};
uint8_t rand_out[32] = {0};
uint8_t slot10_mac[32] = {0};
uint8_t other_data_10[13] = "TangActivate";
struct atca_temp_key temp_key;
memset(&temp_key, 0, sizeof(temp_key));
status = atcab_nonce_rand(nonce_num_in, rand_out);
// Print the nonce output for debugging
if (status != ATCA_SUCCESS) {
printf("Slot 10 Nonce failed: 0x%02X\n", status);
} else {
// Compute the TempKey on the ESP32
struct atca_nonce_in_out nonce_params;
memset(&nonce_params, 0, sizeof(nonce_params));
nonce_params.mode = 0x00;
nonce_params.num_in = nonce_num_in;
nonce_params.rand_out = rand_out;
nonce_params.temp_key = &temp_key;
atcah_nonce(&nonce_params);
// Host calculates MAC using the calculated TempKey as the challenge
my_host_check_mac(received_key, temp_key.value, other_data_10, sn,
slot10_mac);
// See section 11.2 of the ATECC608B datasheet CheckMac message format
status = atcab_checkmac(0x01, 10, NULL, slot10_mac, other_data_10);
if (status == ATCA_SUCCESS) {
printf("Slot 10 CheckMac: PASSED\n");
} else if (status == (ATCA_STATUS)ATCA_CHECKMAC_VERIFY_FAILED) {
// 0xD1 (-47 / 0xFFFFFFD1) see atca_status.h
printf("Slot 10 CheckMac: ACCESS DENIED (Wrong Password)\n");
} else {
printf("Slot 10 CheckMac: HARDWARE ERROR (0x%02X)\n", status);
}
}
status = atcab_info_set_latch(true);
if (status != ATCA_SUCCESS) {
printf("Failed to latch device: 0x%02X\n", status);
}
atcab_info_get_latch(&latch_status);
printf("Device Latch Status after setting: %s (0x%02X)\n",
latch_status ? "LATCHED" : "UNLATCHED", latch_status);
return (status == ATCA_SUCCESS);
}
// Process unlock request with ECIES tunnel
char *process_unlock(const char *json_payload, bool *success_out) {
*success_out = false;
if (!initialized) {
return strdup("{\"error\":\"Not initialized\"}");
}
// Parse JSON
cJSON *doc = cJSON_Parse(json_payload);
if (doc == NULL) {
const char *error_ptr = cJSON_GetErrorPtr();
if (error_ptr != NULL) {
printf("JSON parse error before: %s\n", error_ptr);
}
return strdup("{\"error\":\"Invalid JSON\"}");
}
cJSON *client_pub_item = cJSON_GetObjectItem(doc, "clientPub");
cJSON *encrypted_blob_item = cJSON_GetObjectItem(doc, "blob");
const char *client_pub_hex = NULL;
const char *encrypted_blob_hex = NULL;
if (cJSON_IsString(client_pub_item))
client_pub_hex = client_pub_item->valuestring;
if (cJSON_IsString(encrypted_blob_item))
encrypted_blob_hex = encrypted_blob_item->valuestring;
if (!client_pub_hex || !encrypted_blob_hex) {
cJSON_Delete(doc);
return strdup("{\"error\":\"Missing required fields\"}");
}
printf("\n=== Processing Unlock Request ===\n");
printf("Client Public Key: %s\n", client_pub_hex);
printf("Encrypted Blob: %s\n", encrypted_blob_hex);
// Convert client public key from hex
size_t client_pub_len = strlen(client_pub_hex) / 2;
uint8_t *client_pub_bin = (uint8_t *)malloc(client_pub_len);
if (!hex_to_bin(client_pub_hex, client_pub_bin, client_pub_len)) {
free(client_pub_bin);
cJSON_Delete(doc);
return strdup("{\"error\":\"Invalid client public key format\"}");
}
// Parse client public key point
mbedtls_ecp_point client_point;
mbedtls_ecp_point_init(&client_point);
int ret = mbedtls_ecp_point_read_binary(&grp, &client_point, client_pub_bin,
client_pub_len);
free(client_pub_bin);
if (ret != 0) {
mbedtls_ecp_point_free(&client_point);
cJSON_Delete(doc);
printf("Failed to parse client key: -0x%04x\n", -ret);
return strdup("{\"error\":\"Invalid client public key\"}");
}
// Compute shared secret using ECDH
mbedtls_mpi shared_secret_mpi;
mbedtls_mpi_init(&shared_secret_mpi);
ret = mbedtls_ecdh_compute_shared(&grp, &shared_secret_mpi, &client_point,
&device_private_d,
mbedtls_ctr_drbg_random, &ctr_drbg);
mbedtls_ecp_point_free(&client_point);
if (ret != 0) {
mbedtls_mpi_free(&shared_secret_mpi);
cJSON_Delete(doc);
printf("ECDH computation failed: -0x%04x\n", -ret);
return strdup("{\"error\":\"ECDH failed\"}");
}
// Export shared secret to binary
uint8_t shared_secret_raw[32];
ret = mbedtls_mpi_write_binary(&shared_secret_mpi, shared_secret_raw, 32);
mbedtls_mpi_free(&shared_secret_mpi);
if (ret != 0) {
cJSON_Delete(doc);
printf("Shared secret export failed: -0x%04x\n", -ret);
return strdup("{\"error\":\"Shared secret export failed\"}");
}
printf("Shared Secret (raw): ");
for (int i = 0; i < 32; i++)
printf("%02x", shared_secret_raw[i]);
printf("\n");
// Derive separate keys for encryption and authentication
// This prevents key reuse vulnerabilities
const mbedtls_md_info_t *md_info =
mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
// Encryption key: SHA256("encryption" || shared_secret)
uint8_t enc_key[32];
mbedtls_md_context_t md_ctx;
mbedtls_md_init(&md_ctx);
mbedtls_md_setup(&md_ctx, md_info, 0);
mbedtls_md_starts(&md_ctx);
mbedtls_md_update(&md_ctx, (const uint8_t *)"encryption", 10);
mbedtls_md_update(&md_ctx, shared_secret_raw, 32);
mbedtls_md_finish(&md_ctx, enc_key);
// MAC key: SHA256("authentication" || shared_secret)
uint8_t mac_key[32];
mbedtls_md_starts(&md_ctx);
mbedtls_md_update(&md_ctx, (const uint8_t *)"authentication", 14);
mbedtls_md_update(&md_ctx, shared_secret_raw, 32);
mbedtls_md_finish(&md_ctx, mac_key);
mbedtls_md_free(&md_ctx);
printf("Encryption Key: ");
for (int i = 0; i < 32; i++)
printf("%02x", enc_key[i]);
printf("\n");
printf("MAC Key: ");
for (int i = 0; i < 32; i++)
printf("%02x", mac_key[i]);
printf("\n");
// Securely wipe the raw shared secret
memset(shared_secret_raw, 0, 32);
// Convert encrypted blob from hex
// Format: IV (16 bytes) + Ciphertext (32 bytes) + HMAC (32 bytes) = 80
// bytes
size_t encrypted_len = strlen(encrypted_blob_hex) / 2;
uint8_t *encrypted_blob = (uint8_t *)malloc(encrypted_len);
if (!hex_to_bin(encrypted_blob_hex, encrypted_blob, encrypted_len)) {
free(encrypted_blob);
memset(enc_key, 0, 32);
memset(mac_key, 0, 32);
cJSON_Delete(doc);
return strdup("{\"error\":\"Invalid blob format\"}");
}
// Verify blob length: IV(16) + Ciphertext(32) + HMAC(32) = 80 bytes
if (encrypted_len != 80) {
free(encrypted_blob);
memset(enc_key, 0, 32);
memset(mac_key, 0, 32);
cJSON_Delete(doc);
printf("Expected 80 bytes, got %d\n", encrypted_len);
return strdup("{\"error\":\"Invalid blob length\"}");
}
// Extract components from blob
uint8_t *iv = encrypted_blob; // First 16 bytes
uint8_t *ciphertext = encrypted_blob + 16; // Next 32 bytes
uint8_t *received_hmac = encrypted_blob + 48; // Last 32 bytes
printf("IV: ");
for (int i = 0; i < 16; i++)
printf("%02x", iv[i]);
printf("\n");
printf("Received HMAC: ");
for (int i = 0; i < 32; i++)
printf("%02x", received_hmac[i]);
printf("\n");
// ⚠️ CRITICAL: Verify HMAC BEFORE decrypting
// This prevents padding oracle attacks and ensures data authenticity
uint8_t computed_hmac[32];
ret = mbedtls_md_hmac(md_info, mac_key, 32, encrypted_blob, 48,
computed_hmac);
if (ret != 0) {
free(encrypted_blob);
memset(enc_key, 0, 32);
memset(mac_key, 0, 32);
cJSON_Delete(doc);
printf("HMAC computation failed: -0x%04x\n", -ret);
return strdup("{\"error\":\"HMAC computation failed\"}");
}
printf("Computed HMAC: ");
for (int i = 0; i < 32; i++)
printf("%02x", computed_hmac[i]);
printf("\n");
// Constant-time comparison to prevent timing attacks
int hmac_result = 0;
for (int i = 0; i < 32; i++) {
hmac_result |= received_hmac[i] ^ computed_hmac[i];
}
// Wipe MAC key after verification
memset(mac_key, 0, 32);
if (hmac_result != 0) {
free(encrypted_blob);
memset(enc_key, 0, 32);
cJSON_Delete(doc);
printf("❌ HMAC verification FAILED - ciphertext was modified or wrong "
"key!\n");
return strdup("{\"error\":\"Authentication failed - data tampered or "
"wrong password\"}");
}
printf("✅ HMAC verified - data is authentic\n");
// Now safe to decrypt (HMAC passed)
mbedtls_aes_context aes;
mbedtls_aes_init(&aes);
ret = mbedtls_aes_setkey_dec(&aes, enc_key, 256);
if (ret != 0) {
mbedtls_aes_free(&aes);
free(encrypted_blob);
memset(enc_key, 0, 32);
cJSON_Delete(doc);
printf("AES setkey failed: -0x%04x\n", -ret);
return strdup("{\"error\":\"AES setup failed\"}");
}
uint8_t *decrypted_data = (uint8_t *)malloc(32);
uint8_t iv_copy[16];
memcpy(iv_copy, iv, 16); // CBC mode modifies IV
ret = mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_DECRYPT,
32, // data length
iv_copy, // IV (will be modified)
ciphertext, // input
decrypted_data); // output
mbedtls_aes_free(&aes);
free(encrypted_blob);
memset(enc_key, 0, 32);
if (ret != 0) {
free(decrypted_data);
cJSON_Delete(doc);
printf("AES decrypt failed: -0x%04x\n", -ret);
return strdup("{\"error\":\"Decryption failed\"}");
}
// Extract the derived key (PBKDF2 hash, 32 bytes)
printf("\n=== DECRYPTED DERIVED KEY ===\n");
printf("Key (hex): ");
for (size_t i = 0; i < 32; i++) {
printf("%02x", decrypted_data[i]);
}
printf("\n");
printf("=============================\n\n");
// Verify the decrypted key against stored password hash
bool verification_result = verify_key(decrypted_data, 32);
cJSON *resp_doc = cJSON_CreateObject();
cJSON_AddBoolToObject(resp_doc, "success", verification_result);
if (verification_result) {
printf("✅ Password verification SUCCESSFUL\n");
unlocked = true;
cJSON_AddStringToObject(resp_doc, "message", "Unlock successful");
} else {
printf("❌ Password verification FAILED\n");
unlocked = false;
cJSON_AddStringToObject(resp_doc, "error", "Invalid password");
}
char *response_str = cJSON_PrintUnformatted(resp_doc);
cJSON_Delete(resp_doc);
cJSON_Delete(doc);
// Securely wipe decrypted data
memset(decrypted_data, 0, 32);
free(decrypted_data);
*success_out = verification_result;
return response_str;
}
// Check if device is unlocked
bool is_unlocked() const { return unlocked; }
// Lock the device
void lock() { unlocked = false; }
}; };
#endif // ZK_AUTH_H #endif // ZK_AUTH_H

View File

@@ -1,37 +1,25 @@
#ifndef ZK_HANDLERS_H #ifndef ZK_HANDLERS_H
#define ZK_HANDLERS_H #define ZK_HANDLERS_H
#include "provision.h"
#include "zk_auth.h" #include "zk_auth.h"
#include "zk_web_page.h"
#include <esp_http_server.h> #include <esp_http_server.h>
#include <esp_log.h> #include <esp_log.h>
#include <esp_timer.h> #include <esp_timer.h>
#include <mbedtls/ctr_drbg.h>
#include <mbedtls/ecdsa.h>
#include <mbedtls/ecp.h>
#include <mbedtls/entropy.h>
#include <mbedtls/gcm.h>
#include <mbedtls/md.h>
#include <mbedtls/pkcs5.h>
#include <mbedtls/sha256.h>
#include <mbedtls/sha512.h>
#include <string.h> #include <string.h>
static const char *TAG_ZK = "zk_handlers";
// Global ZK Auth instance (to be initialized in main) // Global ZK Auth instance (to be initialized in main)
extern ZKAuth zk_auth; extern ZKAuth zk_auth;
extern httpd_handle_t server_http; extern httpd_handle_t server_http;
// Serve the main web interface
static esp_err_t handle_zk_root(httpd_req_t *req) {
// Check if provisioning is needed - if so, redirect
if (needs_provisioning()) {
ESP_LOGI(TAG_ZK, "Provisioning needed - redirecting to /provision");
httpd_resp_set_status(req, "302 Found");
httpd_resp_set_hdr(req, "Location", "/provision");
httpd_resp_sendstr(req, "Redirecting to provisioning page...");
return ESP_OK;
}
// Normal landing page
httpd_resp_set_type(req, "text/html");
httpd_resp_sendstr(req, ZK_WEB_PAGE);
return ESP_OK;
}
// API endpoint: Get device identity // API endpoint: Get device identity
static esp_err_t handle_zk_identity(httpd_req_t *req) { static esp_err_t handle_zk_identity(httpd_req_t *req) {
char *json_response = zk_auth.get_identity_json(); char *json_response = zk_auth.get_identity_json();
@@ -49,111 +37,14 @@ static esp_err_t handle_zk_identity(httpd_req_t *req) {
} }
// API endpoint: Process unlock request // API endpoint: Process unlock request
static esp_err_t handle_zk_unlock(httpd_req_t *req) {
// Read POST body
char content[1024];
int ret = httpd_req_recv(req, content, sizeof(content) - 1);
if (ret <= 0) {
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
httpd_resp_send_408(req);
}
return ESP_FAIL;
}
content[ret] = '\0';
bool success = false;
char *response = zk_auth.process_unlock(content, &success);
if (response == NULL) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Internal error");
return ESP_FAIL;
}
httpd_resp_set_type(req, "application/json");
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
httpd_resp_set_status(req, success ? "200 OK" : "400 Bad Request");
httpd_resp_sendstr(req, response);
free(response);
return ESP_OK;
}
static esp_err_t handle_zk_status(httpd_req_t *req) {
unsigned long uptime_ms = esp_timer_get_time() / 1000;
char response[128];
snprintf(response, sizeof(response), "{\"unlocked\":%s,\"uptime\":%lu}",
zk_auth.is_unlocked() ? "true" : "false", uptime_ms);
httpd_resp_set_type(req, "application/json");
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
httpd_resp_sendstr(req, response);
return ESP_OK;
}
// API endpoint: Lock the device
static esp_err_t handle_zk_lock(httpd_req_t *req) {
zk_auth.lock();
httpd_resp_set_type(req, "application/json");
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
httpd_resp_sendstr(req, "{\"unlocked\":false}");
return ESP_OK;
}
// Handle CORS preflight // Handle CORS preflight
static esp_err_t handle_zk_options(httpd_req_t *req) {
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "POST, GET, OPTIONS");
httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type");
httpd_resp_set_status(req, "204 No Content");
httpd_resp_send(req, NULL, 0);
return ESP_OK;
}
// Register all ZK auth routes to the HTTP server // Register all ZK auth routes to the HTTP server
void register_zk_handlers(httpd_handle_t server) { void register_zk_handlers(httpd_handle_t server) {
// Root handler for ZK web interface
httpd_uri_t root_uri = {.uri = "/",
.method = HTTP_GET,
.handler = handle_zk_root,
.user_ctx = NULL};
httpd_register_uri_handler(server, &root_uri);
// API endpoints
httpd_uri_t identity_uri = {.uri = "/api/identity", httpd_uri_t identity_uri = {.uri = "/api/identity",
.method = HTTP_GET, .method = HTTP_GET,
.handler = handle_zk_identity, .handler = handle_zk_identity,
.user_ctx = NULL}; .user_ctx = NULL};
httpd_register_uri_handler(server, &identity_uri); httpd_register_uri_handler(server, &identity_uri);
httpd_uri_t status_uri = {.uri = "/api/status",
.method = HTTP_GET,
.handler = handle_zk_status,
.user_ctx = NULL};
httpd_register_uri_handler(server, &status_uri);
httpd_uri_t unlock_uri = {.uri = "/api/unlock",
.method = HTTP_POST,
.handler = handle_zk_unlock,
.user_ctx = NULL};
httpd_register_uri_handler(server, &unlock_uri);
httpd_uri_t unlock_options_uri = {.uri = "/api/unlock",
.method = HTTP_OPTIONS,
.handler = handle_zk_options,
.user_ctx = NULL};
httpd_register_uri_handler(server, &unlock_options_uri);
httpd_uri_t lock_uri = {.uri = "/api/lock",
.method = HTTP_POST,
.handler = handle_zk_lock,
.user_ctx = NULL};
httpd_register_uri_handler(server, &lock_uri);
ESP_LOGI(TAG_ZK, "ZK Auth routes registered:");
ESP_LOGI(TAG_ZK, " GET / - Web interface");
ESP_LOGI(TAG_ZK, " GET /api/identity - Device identity");
ESP_LOGI(TAG_ZK, " GET /api/status - Session status");
ESP_LOGI(TAG_ZK, " POST /api/unlock - Unlock request");
ESP_LOGI(TAG_ZK, " POST /api/lock - Lock device");
} }
#endif // ZK_HANDLERS_H #endif // ZK_HANDLERS_H

View File

@@ -1,833 +0,0 @@
#ifndef ZK_WEB_PAGE_H
#define ZK_WEB_PAGE_H
// Embedded web interface for Zero-Knowledge Authentication
// Includes: HTML, CSS, JavaScript, and crypto libraries (elliptic.js, CryptoJS)
const char ZK_WEB_PAGE[] = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 Zero-Knowledge Auth</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23333' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M13 2H6.5A2.5 2.5 0 0 0 4 4.5v15'/><path d='M17 2v6'/><path d='M17 4h2'/><path d='M20 15.2V21a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20'/><circle cx='17' cy='10' r='2'/></svg>">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #6b7c7c 0%, #5a7d5a 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 450px;
width: 100%;
padding: 40px;
animation: slideIn 0.5s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
h1 {
text-align: center;
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
text-align: center;
color: #666;
font-size: 14px;
margin-bottom: 30px;
}
.info-box {
background: #f0f5f3;
border-left: 4px solid #5a7d5a;
padding: 15px;
margin-bottom: 25px;
border-radius: 5px;
font-size: 13px;
color: #555;
}
.info-box strong {
color: #4a6d4a;
}
.form-group {
margin-bottom: 25px;
}
label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
font-size: 14px;
}
input[type="password"],
input[type="text"] {
width: 100%;
padding: 12px 15px;
border: 2px solid #e1e8ed;
border-radius: 10px;
font-size: 16px;
transition: all 0.3s ease;
outline: none;
}
input:focus {
border-color: #5a7d5a;
box-shadow: 0 0 0 3px rgba(90, 125, 90, 0.1);
}
.btn {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #6b7c7c 0%, #5a7d5a 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
margin-top: 10px;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(90, 125, 90, 0.4);
}
.btn:active {
transform: translateY(0);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
#status {
margin-top: 20px;
padding: 15px;
border-radius: 10px;
font-size: 14px;
text-align: center;
display: none;
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.status-info {
background: #e3f2fd;
color: #1976d2;
border: 1px solid #90caf9;
}
.status-success {
background: #e8f5e9;
color: #2e7d32;
border: 1px solid #81c784;
}
.status-error {
background: #ffebee;
color: #c62828;
border: 1px solid #ef5350;
}
.loader {
border: 3px solid #f3f3f3;
border-top: 3px solid #5a7d5a;
border-radius: 50%;
width: 20px;
height: 20px;
animation: spin 1s linear infinite;
display: inline-block;
margin-right: 10px;
vertical-align: middle;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.device-info {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
margin-top: 20px;
font-size: 12px;
color: #666;
}
.device-info .label {
font-weight: 600;
color: #333;
margin-bottom: 5px;
}
.device-info .value {
font-family: 'Courier New', monospace;
background: white;
padding: 8px;
border-radius: 4px;
word-break: break-all;
font-size: 11px;
}
.tech-badge {
display: inline-block;
background: #e8f5e9;
color: #2e7d32;
padding: 4px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
margin: 2px;
}
.success-animation {
text-align: center;
padding: 40px 20px;
}
.checkmark-circle {
width: 120px;
height: 120px;
margin: 0 auto 30px;
border-radius: 50%;
background: #e8f5e9;
position: relative;
animation: scaleIn 0.5s ease-out;
}
@keyframes scaleIn {
0% {
transform: scale(0);
opacity: 0;
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
opacity: 1;
}
}
.checkmark {
width: 60px;
height: 60px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.checkmark path {
stroke: #2e7d32;
stroke-width: 4;
stroke-linecap: round;
fill: none;
stroke-dasharray: 100;
stroke-dashoffset: 100;
animation: drawCheck 1s ease-out 1s forwards;
}
@keyframes drawCheck {
to {
stroke-dashoffset: 0;
}
}
.status-page {
display: none;
}
.status-page.active {
display: block;
animation: fadeIn 0.5s ease-in;
}
.unlock-page {
display: none;
}
.unlock-page.active {
display: block;
}
.status-card {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
margin: 20px 0;
}
.status-card h3 {
color: #333;
margin-bottom: 15px;
font-size: 18px;
}
.status-item {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #e1e8ed;
}
.status-item:last-child {
border-bottom: none;
}
.status-label {
color: #666;
font-size: 14px;
flex: 0 0 auto;
}
.status-value {
color: #333;
font-weight: 600;
font-size: 14px;
text-align: right;
flex: 1 1 auto;
}
.badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.badge-success {
background: #e8f5e9;
color: #2e7d32;
}
.btn-secondary {
background: #e1e8ed;
color: #333;
}
.btn-secondary:hover {
background: #d1d8dd;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<div class="container" id="mainContainer">
<div id="unlockPage" class="unlock-page">
<h1><svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 8px;"><path d="M13 2H6.5A2.5 2.5 0 0 0 4 4.5v15"/><path d="M17 2v6"/><path d="M17 4h2"/><path d="M20 15.2V21a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"/><circle cx="17" cy="10" r="2"/></svg>Zero-Knowledge Auth</h1>
<p class="subtitle">ESP32-C6 Secure Unlock</p>
<div class="info-box">
<strong><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 2px;"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/></svg> Privacy First:</strong> Your password is never transmitted.
The device only receives an encrypted, derived key over an ECIES tunnel.
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" placeholder="Enter your password" autocomplete="off">
</div>
<button class="btn" onclick="performSecureUnlock()" id="unlockBtn">
Unlock Device
</button>
<div id="status"></div>
</div>
<div id="statusPage" class="status-page">
<div class="success-animation">
<div class="checkmark-circle">
<svg class="checkmark" viewBox="0 0 52 52">
<path d="M14 27l7.5 7.5L38 18"/>
</svg>
</div>
<h2 style="color: #2e7d32; margin-bottom: 10px;">Device Unlocked!</h2>
<p style="color: #666;">Authentication successful</p>
</div>
<div class="status-card">
<h3>Session Information</h3>
<div class="status-item">
<span class="status-label">Status</span>
<span class="badge badge-success">Active</span>
</div>
<div class="status-item">
<span class="status-label">Authentication Method</span>
<span class="status-value">Password</span>
</div>
<div class="status-item">
<span class="status-label">Key Derivation</span>
<span class="status-value">PBKDF2-SHA256</span>
</div>
<div class="status-item">
<span class="status-label">Encryption</span>
<span class="status-value">ECIES (P-256 + AES-CBC + HMAC)</span>
</div>
<div class="status-item">
<span class="status-label">Authenticated At</span>
<span class="status-value" id="authTime">--</span>
</div>
<div class="status-item">
<span class="status-label">Device Uptime</span>
<span class="status-value" id="uptime">--</span>
</div>
</div>
<button class="btn btn-secondary" onclick="lockDevice()">
Lock Device
</button>
</div>
</div>
<!-- SECURITY WARNING: Password handling in browser - client-side only -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/elliptic/6.5.4/elliptic.min.js"></script>
<script>
// ===============================================================================
// Zero-Knowledge Authentication - Browser-Side Cryptography
// ===============================================================================
// SECURITY FEATURES IMPLEMENTED:
// 1. Password is NEVER transmitted - only PBKDF2-derived hash
// 2. Immediate password field clearing after read
// 3. Secure memory wiping of all sensitive variables (keys, secrets, hashes)
// 4. try-finally blocks ensure cleanup even on errors
// 5. Ephemeral ECDH keypair (generated per-session, discarded after)
// 6. All sensitive data cleared before function returns
//
// ENCRYPTION: AES-256-CBC + HMAC-SHA256 (Encrypt-then-MAC)
// - Format: IV (16 bytes) + Ciphertext (32 bytes) + HMAC (32 bytes) = 80 bytes
// - Provides confidentiality (CBC) + authenticity (HMAC)
// - Random IV for each session
// - HMAC prevents tampering and padding oracle attacks
//
// PRODUCTION CHECKLIST:
// - Remove console.log statements that expose secrets (marked with ⚠️)
// - Ensure input field has autocomplete="off" (already set)
// - Consider adding Content-Security-Policy headers
// - Use subresource integrity (SRI) for CDN libraries in production
// ===============================================================================
let deviceIdentity = null;
// ⚠️ SECURITY: Secure memory wiping functions
function secureWipeArray(arr) {
if (!arr) return;
for (let i = 0; i < arr.length; i++) {
arr[i] = 0;
}
}
function secureWipeWordArray(wordArray) {
if (!wordArray || !wordArray.words) return;
for (let i = 0; i < wordArray.words.length; i++) {
wordArray.words[i] = 0;
}
wordArray.sigBytes = 0;
}
function secureWipeString(str) {
// Note: JavaScript strings are immutable, but we can at least dereference
// Best practice: clear the input field immediately after reading
return null;
}
function hexToBytes(hex) {
const bytes = [];
for (let i = 0; i < hex.length; i += 2) {
bytes.push(parseInt(hex.substr(i, 2), 16));
}
return bytes;
}
function bytesToHex(bytes) {
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
}
function wordArrayToByteArray(wordArray) {
const words = wordArray.words;
const sigBytes = wordArray.sigBytes;
const bytes = [];
for (let i = 0; i < sigBytes; i++) {
bytes.push((words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff);
}
return bytes;
}
function byteArrayToWordArray(bytes) {
const words = [];
for (let i = 0; i < bytes.length; i++) {
words[i >>> 2] |= bytes[i] << (24 - (i % 4) * 8);
}
return CryptoJS.lib.WordArray.create(words, bytes.length);
}
async function loadDeviceIdentity() {
try {
const response = await fetch('/api/identity');
if (!response.ok) throw new Error('Failed to fetch device identity');
deviceIdentity = await response.json();
console.log('Device Public Key loaded');
return true;
} catch (error) {
console.error('Error loading device identity:', error);
showStatus('Failed to load device identity', 'error');
return false;
}
}
async function performSecureUnlock() {
const passwordInput = document.getElementById('password');
const password = passwordInput.value;
if (!password) {
showStatus('Please enter a password', 'error');
return;
}
const btn = document.getElementById('unlockBtn');
btn.disabled = true;
showStatus('Initializing secure connection...', 'info', true);
// ⚠️ SECURITY: Track all sensitive variables for cleanup
let sessionKeyHash = null;
let sessionKeyBytes = null;
let sharedSecretBytes = null;
let aesKeyHash = null;
let aesKeyBytes = null;
let clientKey = null;
try {
// Load device identity if not already loaded
if (!deviceIdentity) {
const loaded = await loadDeviceIdentity();
if (!loaded) {
btn.disabled = false;
return;
}
}
showStatus('Computing zero-knowledge proof...', 'info', true);
// Step 1: Derive session key using PBKDF2
// PBKDF2-HMAC-SHA256 with 10000 iterations
// Use MAC address as salt (received from device identity)
const macBytes = hexToBytes(deviceIdentity.macAddress);
const salt = byteArrayToWordArray(macBytes);
sessionKeyHash = CryptoJS.PBKDF2(password, salt, {
keySize: 256/32, // 256 bits = 8 words
iterations: 10000,
hasher: CryptoJS.algo.SHA256
});
// ⚠️ SECURITY: Clear password from input field immediately
passwordInput.value = '';
sessionKeyBytes = wordArrayToByteArray(sessionKeyHash);
const sessionKeyHex = bytesToHex(sessionKeyBytes);
// ⚠️ PRODUCTION WARNING: Remove console.log statements in production builds
// These logs expose sensitive cryptographic material
console.log('Salt (MAC Address):', deviceIdentity.macAddress);
console.log('Session Key (PBKDF2):', sessionKeyHex);
showStatus('Establishing ECIES tunnel...', 'info', true);
// Step 2: Generate ephemeral client keypair using elliptic
const ec = new elliptic.ec('p256');
clientKey = ec.genKeyPair();
// Export uncompressed public key (0x04 + X + Y)
const clientPubHex = clientKey.getPublic('hex');
console.log('Client Public Key:', clientPubHex); // Public key - safe to log
// Step 3: Import server public key and derive shared secret (ECDH)
const serverKey = ec.keyFromPublic(deviceIdentity.pubKey, 'hex');
const sharedPoint = clientKey.derive(serverKey.getPublic());
// Convert BN to 32-byte array
const sharedSecretHex = sharedPoint.toString(16).padStart(64, '0');
sharedSecretBytes = hexToBytes(sharedSecretHex);
// ⚠️ PRODUCTION WARNING: Remove in production - exposes shared secret
console.log('Shared Secret:', sharedSecretHex);
// Step 4: Derive separate keys for encryption and authentication
// This prevents key reuse vulnerabilities in Encrypt-then-MAC
const sharedSecretWA = byteArrayToWordArray(sharedSecretBytes);
// Encryption key: SHA256("encryption" || shared_secret)
const encKeyHash = CryptoJS.SHA256(
CryptoJS.enc.Utf8.parse('encryption').concat(sharedSecretWA)
);
const encKeyBytes = wordArrayToByteArray(encKeyHash);
// MAC key: SHA256("authentication" || shared_secret)
const macKeyHash = CryptoJS.SHA256(
CryptoJS.enc.Utf8.parse('authentication').concat(sharedSecretWA)
);
const macKeyBytes = wordArrayToByteArray(macKeyHash);
// ⚠️ PRODUCTION WARNING: Remove in production - exposes keys
console.log('Encryption Key:', bytesToHex(encKeyBytes));
console.log('MAC Key:', bytesToHex(macKeyBytes));
// ⚠️ SECURITY: Clear shared secret after deriving keys
secureWipeArray(sharedSecretBytes);
secureWipeWordArray(sharedSecretWA);
showStatus('Encrypting credentials...', 'info', true);
// Step 5: Encrypt the session key hash with AES-256-CBC
// Generate random IV (16 bytes for CBC)
const ivWords = CryptoJS.lib.WordArray.random(16);
// Convert encryption key bytes to WordArray
const encKey = byteArrayToWordArray(encKeyBytes);
// Encrypt with AES-256-CBC using NoPadding
// Session key hash is 32 bytes (exactly 2 blocks), so no padding needed
const encrypted = CryptoJS.AES.encrypt(
sessionKeyHash,
encKey,
{
iv: ivWords,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.NoPadding
}
);
// ⚠️ SECURITY: Clear session key after encryption
secureWipeArray(sessionKeyBytes);
secureWipeWordArray(sessionKeyHash);
// Extract IV and ciphertext
const ivBytes = wordArrayToByteArray(ivWords);
const ciphertextBytes = wordArrayToByteArray(encrypted.ciphertext);
showStatus('Computing authentication tag...', 'info', true);
// Build complete blob: IV (16) + Ciphertext (32) + HMAC (32) = 80 bytes
// Note: We only include first 32 bytes of ciphertext (should be exactly 32 after padding)
const completeBlob = new Uint8Array(80);
completeBlob.set(ivBytes, 0); // IV at offset 0
completeBlob.set(ciphertextBytes.slice(0, 32), 16); // First 32 bytes of ciphertext at offset 16
// Step 6: Compute HMAC-SHA256 over IV + Ciphertext (Encrypt-then-MAC)
// CRITICAL: Compute HMAC over the EXACT data in the blob (first 48 bytes: IV + CT)
const dataToAuthenticateBytes = new Uint8Array(48);
dataToAuthenticateBytes.set(completeBlob.slice(0, 48));
const dataToAuthenticate = byteArrayToWordArray(Array.from(dataToAuthenticateBytes));
const macKey = byteArrayToWordArray(macKeyBytes);
const hmac = CryptoJS.HmacSHA256(dataToAuthenticate, macKey);
const hmacBytes = wordArrayToByteArray(hmac);
console.log('IV:', bytesToHex(ivBytes));
console.log('HMAC:', bytesToHex(hmacBytes));
// Place HMAC in blob
completeBlob.set(hmacBytes, 48); // HMAC at offset 48
const encryptedBlobHex = bytesToHex(Array.from(completeBlob));
console.log('Encrypted Blob (IV+CT+HMAC):', encryptedBlobHex); // Encrypted data - safe to log
// ⚠️ SECURITY: Clear all keys after encryption
secureWipeArray(encKeyBytes);
secureWipeArray(macKeyBytes);
secureWipeWordArray(encKeyHash);
secureWipeWordArray(macKeyHash);
secureWipeWordArray(encKey);
secureWipeWordArray(macKey);
secureWipeWordArray(ivWords);
secureWipeWordArray(dataToAuthenticate);
secureWipeWordArray(hmac);
showStatus('Sending unlock request...', 'info', true);
// Step 7: Send to device
const response = await fetch('/api/unlock', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
clientPub: clientPubHex,
blob: encryptedBlobHex
})
});
if (!response.ok) {
throw new Error('Unlock request failed');
}
const result = await response.json();
if (result.success) {
showStatus(' Device unlocked successfully!', 'success');
// Reload page after 1.5s - server will show status page
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
showStatus(' Unlock failed: ' + (result.error || 'Unknown error'), 'error');
}
} catch (error) {
console.error('Error:', error);
showStatus(' Error: ' + error.message, 'error');
} finally {
// ⚠️ SECURITY: Always clear sensitive data, even on error
btn.disabled = false;
passwordInput.value = ''; // Ensure password field is cleared
// Wipe all sensitive variables
if (sessionKeyHash) secureWipeWordArray(sessionKeyHash);
if (sessionKeyBytes) secureWipeArray(sessionKeyBytes);
if (sharedSecretBytes) secureWipeArray(sharedSecretBytes);
// Clear client private key if possible
if (clientKey && clientKey.priv) {
// Elliptic.js uses BN.js for private keys - zero it out
if (clientKey.priv.words) {
for (let i = 0; i < clientKey.priv.words.length; i++) {
clientKey.priv.words[i] = 0;
}
}
}
}
}
function showStatus(message, type, showLoader = false) {
const status = document.getElementById('status');
status.className = 'status-' + type;
status.style.display = 'block';
if (showLoader) {
status.innerHTML = '<div class="loader"></div>' + message;
} else {
status.innerHTML = message;
}
}
function bytesToHex(bytes) {
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
}
async function lockDevice() {
await fetch('/api/lock', { method: 'POST' });
window.location.reload();
}
function formatUptime(milliseconds) {
const seconds = Math.floor(milliseconds / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) {
return `${days}d ${hours % 24}h ${minutes % 60}m`;
} else if (hours > 0) {
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
} else if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
} else {
return `${seconds}s`;
}
}
// On load: ask server which view to show
window.addEventListener('load', async () => {
try {
const res = await fetch('/api/status');
const data = await res.json();
if (data.unlocked) {
document.getElementById('authTime').textContent = new Date().toLocaleString();
if (data.uptime !== undefined) {
document.getElementById('uptime').textContent = formatUptime(data.uptime);
}
document.getElementById('statusPage').classList.add('active');
} else {
document.getElementById('unlockPage').classList.add('active');
loadDeviceIdentity();
}
} catch (e) {
document.getElementById('unlockPage').classList.add('active');
loadDeviceIdentity();
}
});
// Allow Enter key to submit
document.getElementById('password').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
performSecureUnlock();
}
});
// ⚠️ SECURITY: Clear password field on page unload/navigation
window.addEventListener('beforeunload', () => {
const passwordInput = document.getElementById('password');
if (passwordInput) {
passwordInput.value = '';
}
});
// ⚠️ SECURITY: Clear password field when page is restored from bfcache
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
// Page was restored from back/forward cache
const passwordInput = document.getElementById('password');
if (passwordInput) {
passwordInput.value = '';
}
}
});
</script>
</body>
</html>
)rawliteral";
#endif // ZK_WEB_PAGE_H