// Тесты криптографии Ayla LAN: KDF по векторам (эталон = APK), CBC-цепочка, // подписи, оба варианта паддинга. #include "kdf_vectors.hpp" #include "doctest/doctest.h" #include #include #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(vec.plaintexts[m]), strlen(vec.plaintexts[m])); REQUIRE(ct_len > 0); CHECK(b64(ct, static_cast(ct_len)) == std::string(vec.enc_base64[m])); } // Подписи направлений. uint8_t mac[32]; sc.app.sign(mac, reinterpret_cast(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(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(d); bits += 6; if (bits >= 8) { bits -= 8; out[out_len++] = static_cast((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(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(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(ct_len)); REQUIRE(pt_len == static_cast(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("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("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("1234567890123456"), 16) == -1); }