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:
@@ -0,0 +1,180 @@
|
||||
#include "ayla/envelope.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include "mbedtls/base64.h"
|
||||
|
||||
|
||||
namespace fgl::ayla {
|
||||
|
||||
namespace {
|
||||
|
||||
bool b64_encode(const uint8_t* in, size_t in_len, char* out, size_t out_cap) {
|
||||
size_t olen = 0;
|
||||
if (mbedtls_base64_encode(reinterpret_cast<unsigned char*>(out), out_cap,
|
||||
&olen, in, in_len) != 0) {
|
||||
return false;
|
||||
}
|
||||
return true; // mbedtls пишет NUL-терминатор
|
||||
}
|
||||
|
||||
bool b64_decode(const char* in, uint8_t* out, size_t out_cap, size_t* out_len) {
|
||||
size_t olen = 0;
|
||||
if (mbedtls_base64_decode(out, out_cap, &olen,
|
||||
reinterpret_cast<const unsigned char*>(in),
|
||||
strlen(in)) != 0) {
|
||||
return false;
|
||||
}
|
||||
*out_len = olen;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Извлекает seq_no верхнего уровня из '{"seq_no":N,"data":...}'.
|
||||
// Ручной depth-1 сканер (без ограничений на размер data; вход уже
|
||||
// проверен подписью). Понимает пробелы между лексемами.
|
||||
bool extract_seq_no(const char* json, int64_t* seq_no) {
|
||||
const char* p = json;
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
if (*p != '{') return false;
|
||||
p++;
|
||||
for (;;) {
|
||||
while (*p == ' ' || *p == ',' || *p == '\t') p++;
|
||||
if (*p == '}' || *p == '\0') return false;
|
||||
// ключ
|
||||
if (*p != '"') return false;
|
||||
const char* k = ++p;
|
||||
while (*p != '"' && *p != '\0') p++;
|
||||
if (*p != '"') return false;
|
||||
size_t klen = static_cast<size_t>(p - k);
|
||||
p++;
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
if (*p != ':') return false;
|
||||
p++;
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
if (klen == 6 && strncmp(k, "seq_no", 6) == 0) {
|
||||
if (*p == '-' || (*p >= '0' && *p <= '9')) {
|
||||
*seq_no = strtoll(p, nullptr, 10);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// пропустить значение другого ключа
|
||||
if (*p == '{' || *p == '[') {
|
||||
int depth = 0;
|
||||
bool in_str = false;
|
||||
for (; *p != '\0'; p++) {
|
||||
if (in_str) {
|
||||
if (*p == '\\') {
|
||||
p++;
|
||||
if (*p == '\0') return false; // обрыв после escape
|
||||
} else if (*p == '"') {
|
||||
in_str = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (*p == '"') {
|
||||
in_str = true;
|
||||
} else if (*p == '{' || *p == '[') {
|
||||
depth++;
|
||||
} else if (*p == '}' || *p == ']') {
|
||||
depth--;
|
||||
if (depth == 0) {
|
||||
p++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (*p == '\0') return false;
|
||||
} else if (*p == '"') {
|
||||
p++;
|
||||
while (*p != '"' && *p != '\0') {
|
||||
if (*p == '\\') {
|
||||
p++;
|
||||
if (*p == '\0') return false; // обрыв после escape
|
||||
}
|
||||
p++;
|
||||
}
|
||||
if (*p != '"') return false;
|
||||
p++;
|
||||
} else {
|
||||
while (*p != ',' && *p != '}' && *p != '\0') p++;
|
||||
if (*p == '\0') return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Сравнение за константное время (аналог mbedtls_ct_memcmp, без зависимости
|
||||
// от версии библиотеки).
|
||||
int ct_memcmp(const uint8_t* a, const uint8_t* b, size_t n) {
|
||||
uint8_t diff = 0;
|
||||
for (size_t i = 0; i < n; i++) diff |= static_cast<uint8_t>(a[i] ^ b[i]);
|
||||
return diff != 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool envelope_pack(DirectionCrypto& dir, int64_t seq_no,
|
||||
const char* data_json, char* out, size_t out_cap) {
|
||||
if (data_json == nullptr || out == nullptr) return false;
|
||||
|
||||
// Открытый текст: {"seq_no":N,"data":<data_json>}
|
||||
char plain[kEnvelopeMaxPlain];
|
||||
int n = snprintf(plain, sizeof(plain), "{\"seq_no\":%lld,\"data\":%s}",
|
||||
static_cast<long long>(seq_no), data_json);
|
||||
if (n <= 0 || static_cast<size_t>(n) >= sizeof(plain)) return false;
|
||||
size_t plain_len = static_cast<size_t>(n);
|
||||
|
||||
// Подпись — по байтам открытого текста без паддинга.
|
||||
uint8_t mac[kSha256Len];
|
||||
dir.sign(mac, reinterpret_cast<const uint8_t*>(plain), plain_len);
|
||||
char sign_b64[64];
|
||||
if (!b64_encode(mac, sizeof(mac), sign_b64, sizeof(sign_b64))) return false;
|
||||
|
||||
// Шифрование с zero-паддингом (>=1 NUL, кратно 16).
|
||||
uint8_t ct[kEnvelopeMaxPlain + kAesBlockLen];
|
||||
long ct_len = dir.encrypt(ct, sizeof(ct),
|
||||
reinterpret_cast<const uint8_t*>(plain), plain_len);
|
||||
if (ct_len < 0) return false;
|
||||
char enc_b64[kEnvelopeMaxB64];
|
||||
if (!b64_encode(ct, static_cast<size_t>(ct_len), enc_b64, sizeof(enc_b64))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int m = snprintf(out, out_cap, "{\"enc\":\"%s\",\"sign\":\"%s\"}", enc_b64,
|
||||
sign_b64);
|
||||
return m > 0 && static_cast<size_t>(m) < out_cap;
|
||||
}
|
||||
|
||||
bool envelope_unpack(DirectionCrypto& dir, const char* enc_b64,
|
||||
const char* sign_b64, char* json_out, size_t out_cap,
|
||||
int64_t* seq_no) {
|
||||
if (enc_b64 == nullptr || sign_b64 == nullptr || json_out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
uint8_t ct[kEnvelopeMaxPlain + kAesBlockLen];
|
||||
size_t ct_len = 0;
|
||||
if (!b64_decode(enc_b64, ct, sizeof(ct), &ct_len)) return false;
|
||||
|
||||
// Расшифровка: CBC-цепочка двигается независимо от исхода проверки подписи.
|
||||
uint8_t plain[kEnvelopeMaxPlain + kAesBlockLen];
|
||||
long plain_len = dir.decrypt(plain, sizeof(plain), ct, ct_len);
|
||||
if (plain_len < 0 || static_cast<size_t>(plain_len) >= out_cap) return false;
|
||||
|
||||
// Проверка подписи по расшифрованным байтам (без паддинга);
|
||||
// сравнение декодированных байт — константное время.
|
||||
uint8_t mac[kSha256Len];
|
||||
dir.sign(mac, plain, static_cast<size_t>(plain_len));
|
||||
uint8_t given[kSha256Len];
|
||||
size_t given_len = 0;
|
||||
if (!b64_decode(sign_b64, given, sizeof(given), &given_len)) return false;
|
||||
if (given_len != kSha256Len) return false;
|
||||
if (ct_memcmp(mac, given, kSha256Len) != 0) return false;
|
||||
|
||||
memcpy(json_out, plain, static_cast<size_t>(plain_len));
|
||||
json_out[plain_len] = '\0';
|
||||
return extract_seq_no(json_out, seq_no);
|
||||
}
|
||||
|
||||
} // namespace fgl::ayla
|
||||
Reference in New Issue
Block a user