- 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.
254 lines
7.6 KiB
C++
254 lines
7.6 KiB
C++
// Минимальный JSON: writer с фиксированным буфером + парсер (jsmn).
|
|
// Никаких аллокаций; экранирование строк — достаточное для протокола.
|
|
#pragma once
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
|
|
#define JSMN_STATIC
|
|
#include "third_party/jsmn/jsmn.h"
|
|
|
|
namespace fgl::ayla::json {
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Writer. Стек глубин <= 8; состояние контейнеров (были ли элементы).
|
|
// ---------------------------------------------------------------------------
|
|
class Writer {
|
|
public:
|
|
Writer(char* buf, size_t cap) : buf_(buf), cap_(cap) {
|
|
buf_[0] = '\0';
|
|
}
|
|
|
|
void begin_object() { value_comma(); put('{'); push('{'); }
|
|
void end_object() { pop(); put('}'); }
|
|
void begin_array() { value_comma(); put('['); push('['); }
|
|
void end_array() { pop(); put(']'); }
|
|
|
|
// Ключ объекта; за ним должно следовать значение.
|
|
void key(const char* name) {
|
|
if (items_[depth_]) put(',');
|
|
items_[depth_] = true;
|
|
put('"');
|
|
str_escape(name);
|
|
put_raw("\":", 2);
|
|
await_value_ = true;
|
|
}
|
|
|
|
void string(const char* v) {
|
|
value_comma();
|
|
put('"');
|
|
str_escape(v);
|
|
put('"');
|
|
}
|
|
void integer(int64_t v) {
|
|
value_comma();
|
|
char tmp[24];
|
|
int n = snprintf(tmp, sizeof(tmp), "%lld", static_cast<long long>(v));
|
|
put_raw(tmp, static_cast<size_t>(n));
|
|
}
|
|
void boolean(bool v) { value_comma(); put_raw(v ? "true" : "false", v ? 4 : 5); }
|
|
void literal(const char* v) { value_comma(); put_raw(v, strlen(v)); }
|
|
void raw_json(const char* v) { value_comma(); put_raw(v, strlen(v)); }
|
|
|
|
const char* c_str() const { return buf_; }
|
|
size_t size() const { return pos_; }
|
|
bool ok() const { return ok_; }
|
|
|
|
private:
|
|
static constexpr int kMaxDepth = 8;
|
|
|
|
// Запятая/маркер перед ЗНАЧЕНИЕМ (не ключом, не закрытием).
|
|
void value_comma() {
|
|
if (await_value_) {
|
|
await_value_ = false;
|
|
return; // ключ уже поставил ':' — запятая не нужна
|
|
}
|
|
if (items_[depth_]) put(',');
|
|
items_[depth_] = true;
|
|
}
|
|
void push(char c) {
|
|
if (depth_ + 1 >= kMaxDepth) {
|
|
ok_ = false;
|
|
return;
|
|
}
|
|
depth_++;
|
|
items_[depth_] = false;
|
|
(void)c;
|
|
}
|
|
void pop() {
|
|
if (depth_ > 0) depth_--;
|
|
await_value_ = false;
|
|
}
|
|
void put(char c) {
|
|
if (!ok_ || pos_ + 1 >= cap_) {
|
|
ok_ = false;
|
|
return;
|
|
}
|
|
buf_[pos_++] = c;
|
|
buf_[pos_] = '\0';
|
|
}
|
|
void put_raw(const char* s, size_t n) {
|
|
if (!ok_ || pos_ + n + 1 > cap_) {
|
|
ok_ = false;
|
|
return;
|
|
}
|
|
memcpy(buf_ + pos_, s, n);
|
|
pos_ += n;
|
|
buf_[pos_] = '\0';
|
|
}
|
|
void str_escape(const char* s) {
|
|
for (const char* p = s; *p != '\0'; p++) {
|
|
char c = *p;
|
|
if (c == '"' || c == '\\') {
|
|
put('\\');
|
|
put(c);
|
|
} else if (c == '\n') {
|
|
put_raw("\\n", 2);
|
|
} else if (c == '\t') {
|
|
put_raw("\\t", 2);
|
|
} else if (c == '\r') {
|
|
put_raw("\\r", 2);
|
|
} else if (c == '\b') {
|
|
put_raw("\\b", 2);
|
|
} else if (c == '\f') {
|
|
put_raw("\\f", 2);
|
|
} else if (static_cast<unsigned char>(c) < 0x20) {
|
|
char tmp[8];
|
|
snprintf(tmp, sizeof(tmp), "\\u%04x", c);
|
|
put_raw(tmp, 6);
|
|
} else {
|
|
put(c);
|
|
}
|
|
}
|
|
}
|
|
|
|
char* buf_;
|
|
size_t cap_;
|
|
size_t pos_ = 0;
|
|
bool ok_ = true;
|
|
int depth_ = 0;
|
|
bool items_[kMaxDepth + 1] = {};
|
|
bool await_value_ = false;
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Parser (jsmn): токены поверх исходного буфера, без копирований.
|
|
// ---------------------------------------------------------------------------
|
|
constexpr int kMaxTokens = 64;
|
|
|
|
class Doc {
|
|
public:
|
|
// Разбирает buf (NUL-terminated, будет модифицироваться при unescape).
|
|
// После вызова buf должен жить, пока используется Doc.
|
|
bool parse(char* buf) {
|
|
buf_ = buf;
|
|
jsmn_init(&p_);
|
|
count_ = jsmn_parse(&p_, buf, strlen(buf), tokens_, kMaxTokens);
|
|
return count_ >= 1 && tokens_[0].type == JSMN_OBJECT;
|
|
}
|
|
|
|
// Значение ключа по имени (первое вхождение в порядке документа —
|
|
// глубина не различается; для форматов протокора ключи уникальны).
|
|
// Для строки возвращает указатель внутрь buf_ (БЕЗ кавычек,
|
|
// НЕ NUL-terminated) и длину.
|
|
bool find(const char* name, const char** out_start, size_t* out_len,
|
|
jsmntype_t* out_type = nullptr) const {
|
|
for (int i = 1; i + 1 < count_; i++) {
|
|
if (tokens_[i].type == JSMN_STRING && tokens_[i].size == 1) {
|
|
int len = tokens_[i].end - tokens_[i].start;
|
|
if (len == static_cast<int>(strlen(name)) &&
|
|
strncmp(buf_ + tokens_[i].start, name, len) == 0) {
|
|
const jsmntok_t& v = tokens_[i + 1];
|
|
*out_start = buf_ + v.start;
|
|
*out_len = static_cast<size_t>(v.end - v.start);
|
|
if (out_type != nullptr) *out_type = v.type;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Строка в out (снятие кавычек; поддержка \" \\ \/ \n \t \r \b \f).
|
|
bool get_string(const char* name, char* out, size_t out_cap) const {
|
|
const char* s;
|
|
size_t len;
|
|
jsmntype_t type;
|
|
if (!find(name, &s, &len, &type) || type != JSMN_STRING) return false;
|
|
size_t o = 0;
|
|
for (size_t i = 0; i < len; i++) {
|
|
if (o + 1 >= out_cap) return false;
|
|
char c = s[i];
|
|
if (c == '\\' && i + 1 < len) {
|
|
char e = s[++i];
|
|
switch (e) {
|
|
case '"': out[o++] = '"'; break;
|
|
case '\\': out[o++] = '\\'; break;
|
|
case '/': out[o++] = '/'; break;
|
|
case 'n': out[o++] = '\n'; break;
|
|
case 't': out[o++] = '\t'; break;
|
|
case 'r': out[o++] = '\r'; break;
|
|
case 'b': out[o++] = '\b'; break;
|
|
case 'f': out[o++] = '\f'; break;
|
|
default: return false; // \uXXXX не требуется протоколом
|
|
}
|
|
} else {
|
|
out[o++] = c;
|
|
}
|
|
}
|
|
out[o] = '\0';
|
|
return true;
|
|
}
|
|
|
|
bool get_int(const char* name, int64_t* out) const {
|
|
const char* s;
|
|
size_t len;
|
|
jsmntype_t type;
|
|
if (!find(name, &s, &len, &type) || type != JSMN_PRIMITIVE) return false;
|
|
int64_t v = 0;
|
|
bool neg = false;
|
|
size_t i = 0;
|
|
if (i < len && (s[i] == '-' || s[i] == '+')) {
|
|
neg = s[i] == '-';
|
|
i++;
|
|
}
|
|
if (i >= len) return false;
|
|
for (; i < len; i++) {
|
|
if (s[i] < '0' || s[i] > '9') return false;
|
|
int64_t digit = s[i] - '0';
|
|
if (v > (INT64_MAX - digit) / 10) return false; // переполнение
|
|
v = v * 10 + digit;
|
|
}
|
|
*out = neg ? -v : v;
|
|
return true;
|
|
}
|
|
|
|
bool get_bool(const char* name, bool* out) const {
|
|
const char* s;
|
|
size_t len;
|
|
jsmntype_t type;
|
|
if (!find(name, &s, &len, &type) || type != JSMN_PRIMITIVE) return false;
|
|
if (len == 4 && strncmp(s, "true", 4) == 0) {
|
|
*out = true;
|
|
return true;
|
|
}
|
|
if (len == 5 && strncmp(s, "false", 5) == 0) {
|
|
*out = false;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
int tokens_used() const { return count_; }
|
|
|
|
private:
|
|
char* buf_ = nullptr;
|
|
jsmn_parser p_;
|
|
jsmntok_t tokens_[kMaxTokens];
|
|
int count_ = 0;
|
|
};
|
|
|
|
} // namespace fgl::ayla::json
|