// Тесты конверта {"enc","sign"}: roundtrip, цепочка, порча подписи. #include "doctest/doctest.h" #include #include #include "ayla/crypto.hpp" #include "ayla/envelope.hpp" TEST_CASE("envelope: roundtrip и CBC-цепочка (3 сообщения)") { const char* key = "UnitTestKey01234567890=="; fgl::ayla::SessionCrypto tx, rx; REQUIRE(tx.init(key, "rnd1111rnd1111aa", "rnd2222rnd2222bb", 42, 43)); REQUIRE(rx.init(key, "rnd1111rnd1111aa", "rnd2222rnd2222bb", 42, 43)); const char* payloads[] = { "{}", "{\"cmds\":[{\"cmd\":{\"cmd_id\":7,\"method\":\"GET\"," "\"resource\":\"property.json?name=x\",\"data\":\"\"," "\"uri\":\"/local_lan/property/datapoint.json\"}}]}", "{\"properties\":[{\"property\":{\"base_type\":\"integer\"," "\"name\":\"n\",\"value\":9,\"id\":\"zz\"}}]}", }; char envelopes[3][fgl::ayla::kEnvelopeMaxB64]; for (int i = 0; i < 3; i++) { REQUIRE(fgl::ayla::envelope_pack(tx.app, i, payloads[i], envelopes[i], sizeof(envelopes[i]))); } // Разные шифротексты (цепочка движется). CHECK(std::string(envelopes[0]) != std::string(envelopes[1])); // Приёмник с той же цепочкой расшифровывает по порядку. for (int i = 0; i < 3; i++) { // Разбор конверта на enc/sign (в тесте — вручную, простым поиском). std::string env(envelopes[i]); size_t e1 = env.find("\"enc\":\"") + 7; size_t e2 = env.find('"', e1); size_t s1 = env.find("\"sign\":\"") + 8; size_t s2 = env.find('"', s1); REQUIRE(e2 != std::string::npos); REQUIRE(s2 != std::string::npos); std::string enc = env.substr(e1, e2 - e1); std::string sign = env.substr(s1, s2 - s1); char json[fgl::ayla::kEnvelopeMaxPlain]; int64_t seq = -1; REQUIRE(fgl::ayla::envelope_unpack(rx.app, enc.c_str(), sign.c_str(), json, sizeof(json), &seq)); CHECK(seq == i); std::string expected = "{\"seq_no\":" + std::to_string(i) + ",\"data\":" + payloads[i] + "}"; CHECK(std::string(json) == expected); } } TEST_CASE("envelope: порча подписи отклоняется, цепочка продолжает движение") { const char* key = "UnitTestKey01234567890=="; fgl::ayla::SessionCrypto tx, rx; REQUIRE(tx.init(key, "aaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbb", 1, 2)); REQUIRE(rx.init(key, "aaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbb", 1, 2)); char e0[fgl::ayla::kEnvelopeMaxB64], e1[fgl::ayla::kEnvelopeMaxB64]; REQUIRE(fgl::ayla::envelope_pack(tx.app, 0, "{}", e0, sizeof(e0))); REQUIRE(fgl::ayla::envelope_pack(tx.app, 1, "{\"a\":1}", e1, sizeof(e1))); auto split = [](const char* env, std::string* enc, std::string* sign) { std::string s(env); size_t e1 = s.find("\"enc\":\"") + 7, e2 = s.find('"', e1); size_t g1 = s.find("\"sign\":\"") + 8, g2 = s.find('"', g1); *enc = s.substr(e1, e2 - e1); *sign = s.substr(g1, g2 - g1); }; std::string enc0, sign0, enc1, sign1; split(e0, &enc0, &sign0); split(e1, &enc1, &sign1); // Портим подпись первого сообщения: распаковка обязана провалиться, // но расшифровка (и движение цепочки) уже произошла. std::string bad = sign0; bad[bad.size() - 2] = (bad[bad.size() - 2] == 'A') ? 'B' : 'A'; char json[fgl::ayla::kEnvelopeMaxPlain]; int64_t seq = -1; CHECK_FALSE(fgl::ayla::envelope_unpack(rx.app, enc0.c_str(), bad.c_str(), json, sizeof(json), &seq)); // Второе сообщение — валидное, цепочка корректна. REQUIRE(fgl::ayla::envelope_unpack(rx.app, enc1.c_str(), sign1.c_str(), json, sizeof(json), &seq)); CHECK(seq == 1); } TEST_CASE("envelope: некорректный base64 и повреждённый шифротекст") { const char* key = "UnitTestKey01234567890=="; fgl::ayla::SessionCrypto tx, rx; REQUIRE(tx.init(key, "cccccccccccccccc", "dddddddddddddddd", 5, 6)); REQUIRE(rx.init(key, "cccccccccccccccc", "dddddddddddddddd", 5, 6)); char env[fgl::ayla::kEnvelopeMaxB64]; REQUIRE(fgl::ayla::envelope_pack(tx.app, 0, "{}", env, sizeof(env))); std::string s(env); size_t e1 = s.find("\"enc\":\"") + 7, e2 = s.find('"', e1); size_t g1 = s.find("\"sign\":\"") + 8, g2 = s.find('"', g1); std::string enc = s.substr(e1, e2 - e1); std::string sign = s.substr(g1, g2 - g1); char json[fgl::ayla::kEnvelopeMaxPlain]; int64_t seq = -1; // Мусорный base64. CHECK_FALSE(fgl::ayla::envelope_unpack(rx.app, "!!!not-base64!!!", sign.c_str(), json, sizeof(json), &seq)); // Порча шифротекста: подпись не сойдётся (подводного камня нет — просто // проверяем, что это отклоняется). std::string broken = enc; if (broken[10] == 'A') { broken[10] = 'B'; } else { broken[10] = 'A'; } CHECK_FALSE(fgl::ayla::envelope_unpack(rx.app, broken.c_str(), sign.c_str(), json, sizeof(json), &seq)); // Оригинал после двух провалов уже не расшифруется (цепочка ушла) — // таково требование протокола; восстановление только re-key. CHECK_FALSE(fgl::ayla::envelope_unpack(rx.app, enc.c_str(), sign.c_str(), json, sizeof(json), &seq)); } namespace { // base64 для ручной сборки конвертов в тестах. std::string tb64(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("envelope: extract_seq_no устойчив к обрыву после escape (OOB-регрессия)") { // Аутентифицированный plaintext с незакрытой строкой, заканчивающейся '\' // (раньше — чтение за границей буфера, поймано ASan в ревью круга 1). // Конверты собираем вручную: sign + encrypt по битому plaintext. const char* key = "UnitTestKey01234567890=="; fgl::ayla::SessionCrypto tx, rx; REQUIRE(tx.init(key, "eeeeeeeeeeeeeeee", "ffffffffffffffff", 7, 8)); REQUIRE(rx.init(key, "eeeeeeeeeeeeeeee", "ffffffffffffffff", 7, 8)); // seq_no отсутствует/после битой части — сканер обязан ПРОПУСКАТЬ // повреждённое значение (именно на пропуске был OOB). const char* cases[] = { "{\"a\":\"x\\", // обрыв в строке-значении верхнего уровня "{\"a\":{\"b\":\"x\\", // обрыв в строке внутри вложенного объекта "{\"a\":[1,2,\"x\\", // обрыв в строке внутри массива }; for (const char* plain : cases) { CAPTURE(plain); uint8_t mac[32]; tx.app.sign(mac, reinterpret_cast(plain), strlen(plain)); uint8_t ct[fgl::ayla::kEnvelopeMaxPlain + 16]; long ct_len = tx.app.encrypt(ct, sizeof(ct), reinterpret_cast(plain), strlen(plain)); REQUIRE(ct_len > 0); std::string enc = tb64(ct, static_cast(ct_len)); std::string sign = tb64(mac, sizeof(mac)); char json[fgl::ayla::kEnvelopeMaxPlain]; int64_t seq = -1; // unpack обязан вернуть false (битый JSON) и не выйти за границы. CHECK_FALSE(fgl::ayla::envelope_unpack(rx.app, enc.c_str(), sign.c_str(), json, sizeof(json), &seq)); } }