core(M1): ayla-криптография, конверт, JSON (jsmn), HTTP-клиент

- crypto: KDF Ayla (двойной HMAC, suffix 0x30/31/32; app/dev направления),
  AES-256-CBC с непрерывной цепочкой (iv обновляется mbedtls на месте),
  Java-паддинг >=1 NUL; mode-latch против misuse (encrypt|decrypt);
  zeroize ключей при повторном init; векторы из APK (4 сессии × 4 сообщения,
  включая legacy-приём без NUL) — scripts/gen_kdf_vectors.py.
- envelope: pack/unpack {"enc","sign"}; расшифровка (движение цепочки)
  ДО проверки подписи; сравнение подписи в константном времени;
  extract_seq_no — depth-1 сканер без лимита токенов (OOB после escape
  исправлен, регресс-тесты по ASan-репро ревьюера).
- json: Writer (фикс. буфер, стек глубин, escape, ok()=false при
  переполнении) + Doc на jsmn (64 токена, unescape, overflow-guard).
- httpc: блокирующий POST/PUT для local_reg (статус 200-599, дренаж,
  shutdown перед close).
- third_party/jsmn (MIT, JSMN_STATIC).
- CMake: mbedtls системный (/usr/include/mbedtls3) или FetchContent;
  IDF: PRIV_REQUIRES mbedtls.
- CI: 3 конфигурации — gcc-Release, gcc-Debug+ASan/UBSan, clang-Release;
  6/6 тестов стабильно; ESP-IDF esp32 build complete.
Ревью под-агентом: 3 круга (OOB-блокер + тестовые флаки закрыты), APPROVED.
This commit is contained in:
2026-09-22 15:45:20 +03:00
parent b44004627b
commit e91d4605b1
19 changed files with 2120 additions and 3 deletions

70
src/ayla/crypto.hpp Normal file
View File

@@ -0,0 +1,70 @@
// Сессионная криптография Ayla LAN (docs/PROTOCOL.md §3).
// KDF: key = HMAC-SHA256_k(HMAC-SHA256_k(msg) || msg), msg = rnd_a|rnd_b|t_a|t_b|X,
// X: 0x30 sign / 0x31 crypto / 0x32 iv(первые 16 байт).
// Направления: app (rnd1,rnd2,t1,t2) — исходящие; dev (rnd2,rnd1,t2,t1) — входящие.
// ВАЖНО: AES-CBC-цепочка непрерывна в рамках сессии (каждое сообщение
// продолжает цепочку с последнего шифроблока предыдущего).
#pragma once
#include <cstddef>
#include <cstdint>
#include "mbedtls/aes.h"
#include "mbedtls/md.h"
namespace fgl::ayla {
constexpr size_t kSha256Len = 32;
constexpr size_t kAesKeyLen = 32; // AES-256
constexpr size_t kAesBlockLen = 16;
constexpr size_t kHmacKeyLen = 32;
class DirectionCrypto {
public:
// Выводит ключи направления. rnd_a/rnd_b — 16-символьные токены,
// t_a/t_b — 64-битные «времена» (материал KDF, значение произвольно).
// app: (random_1, random_2, time_1, time_2); dev: (random_2, random_1, time_2, time_1).
bool init(const char* lanip_key, const char* rnd_a, const char* rnd_b,
int64_t t_a, int64_t t_b);
// Шифрует in (любая длина) с zero-паддингом (>=1 NUL, кратно 16).
// out_cap должен быть >= padded_len = ((in_len+1)/16+1)*16.
// Возвращает длину шифротекста или -1. Продолжает CBC-цепочку.
// ВАЖНО: у направления ОДНА цепочка; первый вызов фиксирует режим
// (шифрование либо расшифровка) — смешение режимов возвращает -1.
long encrypt(uint8_t* out, size_t out_cap, const uint8_t* in, size_t in_len);
// Расшифровывает in (длина кратна 16), снимает завершающие NUL.
// Возвращает длину открытого текста или -1. Продолжает CBC-цепочку
// (даже если далее подпись не сойдётся — состояние обязано двигаться).
long decrypt(uint8_t* out, size_t out_cap, const uint8_t* in, size_t in_len);
// HMAC-SHA256(sign_key, data) — подпись/проверка сообщений направления.
void sign(uint8_t out[kSha256Len], const uint8_t* data, size_t len) const;
bool initialized() const { return initialized_; }
private:
static bool derive(const uint8_t* key, size_t key_len, const uint8_t* msg,
size_t msg_len, uint8_t out[kSha256Len]);
bool initialized_ = false;
uint8_t mode_ = 0; // 0 не зафиксирован, 1 шифрование, 2 расшифровка
uint8_t sign_key_[kHmacKeyLen];
uint8_t aes_key_[kAesKeyLen];
uint8_t chain_iv_[kAesBlockLen]; // текущее состояние CBC-цепочки
mbedtls_aes_context aes_enc_;
mbedtls_aes_context aes_dec_;
};
// Пара направлений одной сессии.
struct SessionCrypto {
DirectionCrypto app; // приложение -> модуль
DirectionCrypto dev; // модуль -> приложение
// lanip_key — строка base64 КАК ЕСТЬ (utf-8 байты, не декодировать).
bool init(const char* lanip_key, const char* random_1, const char* random_2,
int64_t time_1, int64_t time_2);
};
} // namespace fgl::ayla