- 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.
172 lines
6.7 KiB
C++
172 lines
6.7 KiB
C++
// Тесты криптографии 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);
|
||
}
|