Files
fgl-aircon/tests/ayla/test_crypto.cpp
Petr Polezhaev e91d4605b1 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.
2026-09-22 15:46:18 +03:00

172 lines
6.7 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Тесты криптографии Ayla LAN: KDF по векторам (эталон = APK), CBC-цепочка,
// подписи, оба варианта паддинга.
#include "kdf_vectors.hpp"
#include "doctest/doctest.h"
#include <cstring>
#include <string>
#include "ayla/crypto.hpp"
using fgl::ayla::test::kKdfVectorCount;
using fgl::ayla::test::kKdfVectors;
namespace {
// base64 для сравнения в тестах.
std::string b64(const uint8_t* data, size_t len) {
static const char kTable[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string out;
for (size_t i = 0; i < len; i += 3) {
uint32_t v = data[i] << 16;
if (i + 1 < len) v |= data[i + 1] << 8;
if (i + 2 < len) v |= data[i + 2];
out += kTable[(v >> 18) & 63];
out += kTable[(v >> 12) & 63];
out += (i + 1 < len) ? kTable[(v >> 6) & 63] : '=';
out += (i + 2 < len) ? kTable[v & 63] : '=';
}
return out;
}
} // namespace
TEST_CASE("KDF+envelope: векторы APK (шифрование app-направления, цепочка)") {
for (size_t v = 0; v < kKdfVectorCount; v++) {
CAPTURE(v);
const auto& vec = kKdfVectors[v];
fgl::ayla::SessionCrypto sc;
REQUIRE(sc.init(vec.lanip_key, vec.random_1, vec.random_2, vec.time_1,
vec.time_2));
// Отправитель всегда использует Java-паддинг (>=1 NUL) — сверяем 0..2.
// (Вектор 3 — legacy-приём без NUL; проверяется в decrypt-тесте.)
for (int m = 0; m < 3; m++) {
CAPTURE(m);
uint8_t ct[1024];
long ct_len = sc.app.encrypt(ct, sizeof(ct),
reinterpret_cast<const uint8_t*>(vec.plaintexts[m]),
strlen(vec.plaintexts[m]));
REQUIRE(ct_len > 0);
CHECK(b64(ct, static_cast<size_t>(ct_len)) == std::string(vec.enc_base64[m]));
}
// Подписи направлений.
uint8_t mac[32];
sc.app.sign(mac, reinterpret_cast<const uint8_t*>(vec.plaintexts[0]),
strlen(vec.plaintexts[0]));
CHECK(b64(mac, sizeof(mac)) == std::string(vec.app_sign_msg0_b64));
sc.dev.sign(mac, reinterpret_cast<const uint8_t*>(vec.plaintexts[1]),
strlen(vec.plaintexts[1]));
CHECK(b64(mac, sizeof(mac)) == std::string(vec.dev_sign_msg1_b64));
}
}
TEST_CASE("Расшифровка векторов: независимый приёмник, цепочка, оба паддинга") {
for (size_t v = 0; v < kKdfVectorCount; v++) {
const auto& vec = kKdfVectors[v];
// «Приёмник» — свежая сессия: цепочка стартует с iv_seed.
fgl::ayla::SessionCrypto rx;
REQUIRE(rx.init(vec.lanip_key, vec.random_1, vec.random_2, vec.time_1,
vec.time_2));
// base64-декодирование вектора (таблица для теста).
auto unb64 = [](const char* in, uint8_t* out) -> size_t {
auto val = [](char c) -> int {
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
if (c >= '0' && c <= '9') return c - '0' + 52;
if (c == '+') return 62;
if (c == '/') return 63;
return -1;
};
size_t len = strlen(in), out_len = 0;
uint32_t acc = 0;
int bits = 0;
for (size_t i = 0; i < len; i++) {
if (in[i] == '=') break;
int d = val(in[i]);
if (d < 0) return 0;
acc = (acc << 6) | static_cast<uint32_t>(d);
bits += 6;
if (bits >= 8) {
bits -= 8;
out[out_len++] = static_cast<uint8_t>((acc >> bits) & 0xff);
}
}
return out_len;
};
for (int m = 0; m < 4; m++) {
uint8_t ct[1024];
size_t ct_len = unb64(vec.enc_base64[m], ct);
REQUIRE(ct_len > 0);
REQUIRE(ct_len % 16 == 0);
uint8_t pt[1024] = {};
long pt_len = rx.app.decrypt(pt, sizeof(pt), ct, ct_len);
REQUIRE(pt_len > 0);
// Открытый текст совпадает (NUL-паддинг снят; legacy-вариант — точно).
CHECK(pt_len == static_cast<long>(strlen(vec.plaintexts[m])));
CHECK(memcmp(pt, vec.plaintexts[m], pt_len) == 0);
}
}
}
TEST_CASE("dev-направление: roundtrip цепочки encrypt→decrypt") {
const char* key = "UnitTestKey01234567890==";
const char* msgs[] = {
"{\"seq_no\":0,\"data\":{\"name\":\"a\",\"value\":1}}",
"{\"seq_no\":1,\"data\":{\"name\":\"b\",\"value\":2}}",
"{\"seq_no\":2,\"data\":{}}",
};
fgl::ayla::SessionCrypto tx, rx;
REQUIRE(tx.init(key, "aaaa1111aaaa1111", "bbbb2222bbbb2222", 111, 222));
REQUIRE(rx.init(key, "aaaa1111aaaa1111", "bbbb2222bbbb2222", 111, 222));
for (int i = 0; i < 3; i++) {
uint8_t ct[512];
long ct_len = tx.dev.encrypt(ct, sizeof(ct),
reinterpret_cast<const uint8_t*>(msgs[i]),
strlen(msgs[i]));
REQUIRE(ct_len > 0);
uint8_t pt[512];
long pt_len = rx.dev.decrypt(pt, sizeof(pt), ct, static_cast<size_t>(ct_len));
REQUIRE(pt_len == static_cast<long>(strlen(msgs[i])));
CHECK(memcmp(pt, msgs[i], pt_len) == 0);
}
}
TEST_CASE("encrypt/decrypt: граничные случаи") {
fgl::ayla::SessionCrypto sc;
REQUIRE(sc.init("k", "a", "b", 1, 2));
uint8_t out[2048];
// out_cap меньше padded — ошибка.
CHECK(sc.app.encrypt(out, 1, reinterpret_cast<const uint8_t*>("xx"), 2) == -1);
// Некратная длина шифротекста — ошибка расшифровки.
uint8_t junk[17] = {};
CHECK(sc.app.decrypt(out, sizeof(out), junk, 17) == -1);
// Нулевая длина — ошибка.
CHECK(sc.app.decrypt(out, sizeof(out), junk, 0) == -1);
}
TEST_CASE("DirectionCrypto: смешение encrypt/decrypt одного направления запрещено") {
fgl::ayla::SessionCrypto sc;
REQUIRE(sc.init("k", "a", "b", 1, 2));
uint8_t out[64];
// Фиксируем режим шифрованием.
REQUIRE(sc.app.encrypt(out, sizeof(out),
reinterpret_cast<const uint8_t*>("1234567890123456"), 16) == 32);
// Расшифровка тем же направлением — отказ (misuse-защита одной цепочки).
uint8_t pt[64];
CHECK(sc.app.decrypt(pt, sizeof(pt), out, 16) == -1);
// Обратный порядок для dev-направления.
fgl::ayla::SessionCrypto sc2;
REQUIRE(sc2.init("k", "a", "b", 1, 2));
uint8_t junk[16] = {};
REQUIRE(sc2.dev.decrypt(pt, sizeof(pt), junk, 16) >= 0);
CHECK(sc2.dev.encrypt(out, sizeof(out),
reinterpret_cast<const uint8_t*>("1234567890123456"), 16) == -1);
}