- 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.
87 lines
3.9 KiB
Python
87 lines
3.9 KiB
Python
#!/usr/bin/env python3
|
||
# Регенерация tests/ayla/kdf_vectors.hpp. Эталон — APK AylaEncryption
|
||
# (совпадает с tools/probe_reference.py). Зависимость: pycryptodome.
|
||
# Запуск: python scripts/gen_kdf_vectors.py > tests/ayla/kdf_vectors.hpp
|
||
import base64, hmac, json, random, string
|
||
from Crypto.Cipher import AES
|
||
|
||
random.seed(20260922)
|
||
|
||
def rand_token(n=16, alpha=string.ascii_letters + string.digits):
|
||
return "".join(random.choice(alpha) for _ in range(n))
|
||
|
||
def cstr(s):
|
||
return json.dumps(s)
|
||
|
||
vectors = []
|
||
for i in range(4):
|
||
lanip_key = base64.b64encode(random.randbytes(15 + i)).decode()
|
||
rnd1 = rand_token(16, string.ascii_letters + string.digits + "+/")
|
||
rnd2 = rand_token(16)
|
||
t1 = random.randrange(10**12, 10**15)
|
||
t2 = random.randrange(10**12, 10**15)
|
||
k = lanip_key.encode(); b1, b2 = rnd1.encode(), rnd2.encode()
|
||
s1, s2 = str(t1).encode(), str(t2).encode()
|
||
|
||
def m(msg, suf):
|
||
msg = msg + bytes([suf])
|
||
return hmac.digest(k, hmac.digest(k, msg, "sha256") + msg, "sha256")
|
||
|
||
A, D = b1 + b2 + s1 + s2, b2 + b1 + s2 + s1
|
||
app_sign, dev_sign = m(A, 0x30), m(D, 0x30)
|
||
e = AES.new(m(A, 0x31), AES.MODE_CBC, m(A, 0x32)[:16])
|
||
msgs = [
|
||
'{"seq_no":0,"data":{}}',
|
||
'{"seq_no":1,"data":{"cmds":[{"cmd":{"method":"GET","resource":"property.json?name=fan_speed","uri":"/local_lan/property/datapoint.json","data":"","cmd_id":3}}]}}',
|
||
'{"seq_no":2,"data":{"properties":[{"property":{"base_type":"integer","name":"fan_speed","value":3,"id":"ab12cd34"}}]}}',
|
||
]
|
||
encs = []
|
||
for mtext in msgs:
|
||
raw = mtext.encode()
|
||
n = ((len(raw) + 1 + 15) // 16) * 16 # Java-паддинг (>=1 NUL)
|
||
encs.append(base64.b64encode(e.encrypt(raw.ljust(n, b"\x00"))).decode())
|
||
legacy_msg = '{"seq_no":3,"data":{"pad":"XX"}}' # 32 байта, без NUL
|
||
assert len(legacy_msg) % 16 == 0 and legacy_msg[-1] != "\x00"
|
||
encs.append(base64.b64encode(e.encrypt(legacy_msg.encode())).decode())
|
||
msgs.append(legacy_msg)
|
||
|
||
vectors.append(dict(
|
||
lanip_key=lanip_key, random_1=rnd1, random_2=rnd2,
|
||
time_1=t1, time_2=t2, plaintexts=msgs, enc_base64=encs,
|
||
app_sign_msg0=base64.b64encode(hmac.digest(app_sign, msgs[0].encode(), "sha256")).decode(),
|
||
dev_sign_msg1=base64.b64encode(hmac.digest(dev_sign, msgs[1].encode(), "sha256")).decode()))
|
||
|
||
lines = []
|
||
lines.append("// СГЕНЕРИРОВАНО scripts/gen_kdf_vectors.py (эталон = APK AylaEncryption).")
|
||
lines.append("// Не редактировать руками; регенерация:")
|
||
lines.append("// python scripts/gen_kdf_vectors.py > tests/ayla/kdf_vectors.hpp")
|
||
lines.append("#pragma once")
|
||
lines.append("#include <cstddef>")
|
||
lines.append("")
|
||
lines.append("namespace fgl::ayla::test {")
|
||
lines.append("")
|
||
lines.append("struct KdfVector {")
|
||
lines.append(" const char* lanip_key;")
|
||
lines.append(" const char* random_1;")
|
||
lines.append(" const char* random_2;")
|
||
lines.append(" long long time_1;")
|
||
lines.append(" long long time_2;")
|
||
lines.append(" const char* plaintexts[4];")
|
||
lines.append(" const char* enc_base64[4]; // 0..2: Java-паддинг; 3: legacy без NUL")
|
||
lines.append(" const char* app_sign_msg0_b64;")
|
||
lines.append(" const char* dev_sign_msg1_b64;")
|
||
lines.append("};")
|
||
lines.append("")
|
||
lines.append("inline constexpr KdfVector kKdfVectors[] = {")
|
||
for v in vectors:
|
||
pt = ", ".join(cstr(m) for m in v["plaintexts"])
|
||
en = ", ".join(cstr(e) for e in v["enc_base64"])
|
||
lines.append(f" {{{cstr(v['lanip_key'])}, {cstr(v['random_1'])}, {cstr(v['random_2'])}, "
|
||
f"{v['time_1']}LL, {v['time_2']}LL, {{{pt}}}, {{{en}}}, "
|
||
f"{cstr(v['app_sign_msg0'])}, {cstr(v['dev_sign_msg1'])}}},")
|
||
lines.append("};")
|
||
lines.append("inline constexpr size_t kKdfVectorCount = sizeof(kKdfVectors) / sizeof(kKdfVectors[0]);")
|
||
lines.append("")
|
||
lines.append("} // namespace fgl::ayla::test")
|
||
print("\n".join(lines))
|