From 209c81952a31ef968169020b9870f94e5fd12c72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Xu=C3=A2n=20D=C5=A9ng?= Date: Wed, 28 Oct 2026 17:01:44 +0700 Subject: [PATCH 1/7] feat(tools): add script to fetch the CPace draft and set up per-tool venvs Runs outside the sandbox (needs network): downloads the latest draft-irtf-cfrg-cpace text from the IETF datatracker into tools/vectors/.refs/ (gitignored), then creates an isolated .venv next to every requirements*.txt under tools/ and installs it. --- .gitignore | 4 ++ tools/setup-cpace-refs.sh | 102 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100755 tools/setup-cpace-refs.sh diff --git a/.gitignore b/.gitignore index 6be133c..f2896b0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,8 @@ # venv Python cục bộ của tools/ (tạo: python3 -m venv tools/.venv && tools/.venv/bin/pip install -r tools/vectors/requirements.txt -r tools/schemas/requirements.txt) tools/.venv/ +# venv riêng cho từng thư mục con của tools/ (tools/setup-cpace-refs.sh, một venv cạnh mỗi requirements*.txt) +tools/**/.venv/ +# Văn bản gốc (.txt) tải từ IETF datatracker để chép vector — không commit (tools/setup-cpace-refs.sh) +tools/vectors/.refs/ __pycache__/ diff --git a/tools/setup-cpace-refs.sh b/tools/setup-cpace-refs.sh new file mode 100755 index 0000000..91f3363 --- /dev/null +++ b/tools/setup-cpace-refs.sh @@ -0,0 +1,102 @@ +#!/bin/zsh +# Chay NGOAI sandbox (can mang ra ngoai): zsh handlive-shared/tools/setup-cpace-refs.sh +# +# Buoc 1: tai van ban chinh thuc (.txt) cua draft-irtf-cfrg-cpace, ban moi nhat, tu IETF datatracker, +# luu vao tools/vectors/.refs/draft-irtf-cfrg-cpace.txt (khong commit — xem .gitignore). +# Buoc 2: voi moi file requirements*.txt duoi tools/, dung mot .venv rieng ngay canh file do +# (tools/vectors/.venv, tools/schemas/.venv, tools/strings/.venv, tools/bench/.venv, tools/e2e/.venv, +# tools/e2e/relay_stack/.venv...) va pip install -r vao do. +# +# In ro tung buoc thanh cong/that bai; thoat 0 chi khi CA HAI buoc deu xong. + +SHARED_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +DRAFT_NAME="draft-irtf-cfrg-cpace" +REFS_DIR="$SHARED_ROOT/tools/vectors/.refs" +DRAFT_OUT="$REFS_DIR/${DRAFT_NAME}.txt" + +step1_ok=1 +step2_ok=1 + +echo "== Buoc 1: tai ${DRAFT_NAME} (.txt, ban moi nhat) tu IETF datatracker ==" +mkdir -p "$REFS_DIR" + +REV="" +API_JSON="$(curl -fsS "https://datatracker.ietf.org/api/v1/doc/document/${DRAFT_NAME}/" 2>/dev/null)" +if [ -n "$API_JSON" ]; then + REV="$(printf '%s' "$API_JSON" | python3 -c 'import json, sys +try: + print(json.load(sys.stdin).get("rev", "")) +except Exception: + pass' 2>/dev/null)" +fi + +if [ -z "$REV" ]; then + echo " API datatracker khong tra duoc 'rev', thu doc trang tai lieu (HTML) de do phien ban moi nhat..." + HTML="$(curl -fsS "https://datatracker.ietf.org/doc/${DRAFT_NAME}/" 2>/dev/null)" + REV="$(printf '%s' "$HTML" | grep -o "${DRAFT_NAME}-[0-9][0-9]" | head -1 | sed 's/.*-//')" +fi + +if [ -z "$REV" ]; then + echo " LOI buoc 1: khong xac dinh duoc phien ban moi nhat cua ${DRAFT_NAME} (API va trang HTML cua datatracker deu khong tra duoc 'rev'). Kiem lai mang ra hoac ten tai lieu." + step1_ok=0 +else + TXT_URL="https://www.ietf.org/archive/id/${DRAFT_NAME}-${REV}.txt" + echo " Phien ban moi nhat: -${REV}; tai tu ${TXT_URL}" + HTTP_CODE="$(curl -sS -w '%{http_code}' -o "${DRAFT_OUT}.tmp" "$TXT_URL")" + CURL_EXIT=$? + if [ "$CURL_EXIT" -ne 0 ] || [ "$HTTP_CODE" != "200" ]; then + echo " LOI buoc 1: tai ${TXT_URL} that bai (curl exit=${CURL_EXIT}, http=${HTTP_CODE})." + rm -f "${DRAFT_OUT}.tmp" + step1_ok=0 + else + mv "${DRAFT_OUT}.tmp" "$DRAFT_OUT" + echo " OK: da luu ${DRAFT_OUT} ($(wc -c < "$DRAFT_OUT" | tr -d ' ') byte)." + fi +fi + +echo +echo "== Buoc 2: dung .venv rieng cho tung thu muc tools/* co requirements*.txt, pip install -r ==" +REQ_FILES="$(find "$SHARED_ROOT/tools" -iname 'requirements*.txt' | sort)" +if [ -z "$REQ_FILES" ]; then + echo " LOI buoc 2: khong thay file requirements*.txt nao duoi $SHARED_ROOT/tools" + step2_ok=0 +else + while IFS= read -r REQ; do + [ -z "$REQ" ] && continue + DIR="$(dirname "$REQ")" + VENV="$DIR/.venv" + REL="${DIR#"$SHARED_ROOT"/}" + echo " -- $REL ($(basename "$REQ")) --" + if [ ! -x "$VENV/bin/python3" ]; then + echo " tao venv moi: $VENV/" + if ! python3 -m venv "$VENV"; then + echo " LOI: 'python3 -m venv' that bai cho $VENV" + step2_ok=0 + continue + fi + else + echo " venv da co san, dung lai: $VENV/" + fi + echo " pip install -r $REQ" + if ! "$VENV/bin/pip" install --quiet --upgrade-strategy only-if-needed -r "$REQ"; then + echo " LOI: pip install that bai cho $REQ" + step2_ok=0 + continue + fi + echo " OK" + done <<< "$REQ_FILES" +fi + +echo +if [ "$step1_ok" -eq 1 ] && [ "$step2_ok" -eq 1 ]; then + echo "XONG: ca 2 buoc thanh cong." + exit 0 +fi +if [ "$step1_ok" -eq 1 ]; then + echo "MOT PHAN XONG: buoc 1 (tai draft) OK, buoc 2 (venv) LOI — xem log phia tren." +elif [ "$step2_ok" -eq 1 ]; then + echo "MOT PHAN XONG: buoc 2 (venv) OK, buoc 1 (tai draft) LOI — xem log phia tren." +else + echo "LOI: ca 2 buoc deu khong xong — xem log phia tren." +fi +exit 1 From aea3827c0d021629c5601ddceb7c7b2b3ecec98c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Xu=C3=A2n=20D=C5=A9ng?= Date: Wed, 28 Oct 2026 17:04:08 +0700 Subject: [PATCH 2/7] feat(tools): add verification step to setup-cpace-refs.sh Adds step 3 to run verify_vectors.py, check_schemas.py, and check_strings.py using the newly created .venv for each tool. Reports clear success/failure for each step and exits with correct code (0 on success, 1 on any failure). --- tools/setup-cpace-refs.sh | 65 ++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/tools/setup-cpace-refs.sh b/tools/setup-cpace-refs.sh index 91f3363..57a9ada 100755 --- a/tools/setup-cpace-refs.sh +++ b/tools/setup-cpace-refs.sh @@ -87,16 +87,65 @@ else done <<< "$REQ_FILES" fi +step3_ok=1 + echo -if [ "$step1_ok" -eq 1 ] && [ "$step2_ok" -eq 1 ]; then - echo "XONG: ca 2 buoc thanh cong." - exit 0 +echo "== Buoc 3: chay thử verify_vectors.py, check_schemas.py, check_strings.py bang .venv vua dung ==" +VERIFY_SCRIPT="$SHARED_ROOT/tools/vectors/verify_vectors.py" +CHECK_SCHEMAS="$SHARED_ROOT/tools/schemas/check_schemas.py" +CHECK_STRINGS="$SHARED_ROOT/tools/strings/check_strings.py" + +if [ -f "$VERIFY_SCRIPT" ] && [ -x "$SHARED_ROOT/tools/vectors/.venv/bin/python3" ]; then + echo " Chay verify_vectors.py..." + if "$SHARED_ROOT/tools/vectors/.venv/bin/python3" "$VERIFY_SCRIPT"; then + echo " OK: verify_vectors.py xong." + else + echo " LOI: verify_vectors.py loi." + step3_ok=0 + fi +else + echo " SKIP: verify_vectors.py khong tim thay hoac venv khong san sang." + step3_ok=0 fi -if [ "$step1_ok" -eq 1 ]; then - echo "MOT PHAN XONG: buoc 1 (tai draft) OK, buoc 2 (venv) LOI — xem log phia tren." -elif [ "$step2_ok" -eq 1 ]; then - echo "MOT PHAN XONG: buoc 2 (venv) OK, buoc 1 (tai draft) LOI — xem log phia tren." + +if [ -f "$CHECK_SCHEMAS" ] && [ -x "$SHARED_ROOT/tools/schemas/.venv/bin/python3" ]; then + echo " Chay check_schemas.py..." + if "$SHARED_ROOT/tools/schemas/.venv/bin/python3" "$CHECK_SCHEMAS"; then + echo " OK: check_schemas.py xong." + else + echo " LOI: check_schemas.py loi." + step3_ok=0 + fi else - echo "LOI: ca 2 buoc deu khong xong — xem log phia tren." + echo " SKIP: check_schemas.py khong tim thay hoac venv khong san sang." + step3_ok=0 +fi + +if [ -f "$CHECK_STRINGS" ] && [ -x "$SHARED_ROOT/tools/strings/.venv/bin/python3" ]; then + echo " Chay check_strings.py..." + if "$SHARED_ROOT/tools/strings/.venv/bin/python3" "$CHECK_STRINGS"; then + echo " OK: check_strings.py xong." + else + echo " LOI: check_strings.py loi." + step3_ok=0 + fi +else + echo " SKIP: check_strings.py khong tim thay hoac venv khong san sang." + step3_ok=0 +fi + +echo +if [ "$step1_ok" -eq 1 ] && [ "$step2_ok" -eq 1 ] && [ "$step3_ok" -eq 1 ]; then + echo "XONG: ca 3 buoc thanh cong." + exit 0 +fi +if [ "$step1_ok" -eq 0 ]; then + echo "LOI: buoc 1 (tai draft) that bai." +fi +if [ "$step2_ok" -eq 0 ]; then + echo "LOI: buoc 2 (venv + pip install) that bai." +fi +if [ "$step3_ok" -eq 0 ]; then + echo "LOI: buoc 3 (thuc hien verify/check) that bai hoac khong tim thay script." fi exit 1 From 986af4ceed4e596412624c6a7241d81db50858ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Xu=C3=A2n=20D=C5=A9ng?= Date: Wed, 28 Oct 2026 17:31:56 +0700 Subject: [PATCH 3/7] feat(shared): add Bluetooth pairing and CPace-X25519-SHA512 test vectors bt-pairing.json: K_bt_pdisc/K_bt_plink/pr_bt derivation and the Noise _NNpsk0_25519_ChaChaPoly_SHA256 first-frame MAC for QR pairing links over Bluetooth (00-common-specs.md 0.4.5), with negative vectors for a wrong psk, a tampered tag/message and a replayed n_s. cpace.json: the draft-irtf-cfrg-cpace-21 CPACE-X25519-SHA512 vectors (Appendix B.1, calculate_generator/message/K/ISK), then HandLive's PIN pairing case (fixed n_s, PIN with a leading zero, K_bt_pinlink/K_bt_pin from ISK), plus negative vectors for a low-order Ya/Yb, a sid mismatch and a wrong PIN on one side. --- test-vectors/README.md | 44 ++++++ test-vectors/README.vi.md | 43 ++++++ test-vectors/bt-pairing.json | 100 +++++++++++++ test-vectors/cpace.json | 128 +++++++++++++++++ tools/vectors/bt_pairing_derivations.py | 22 +++ tools/vectors/build_bt_pairing_vectors.py | 104 ++++++++++++++ tools/vectors/build_cpace_vectors.py | 165 ++++++++++++++++++++++ tools/vectors/cpace_pin_derivations.py | 16 +++ tools/vectors/cpace_source_values.py | 87 ++++++++++++ tools/vectors/cpace_x25519_sha512.py | 113 +++++++++++++++ tools/vectors/generate_vectors.py | 4 + tools/vectors/noise_nnpsk0.py | 73 ++++++++++ tools/vectors/verify_bt_pairing_checks.py | 116 +++++++++++++++ tools/vectors/verify_cpace_checks.py | 142 +++++++++++++++++++ tools/vectors/verify_vectors.py | 4 +- 15 files changed, 1160 insertions(+), 1 deletion(-) create mode 100644 test-vectors/bt-pairing.json create mode 100644 test-vectors/cpace.json create mode 100644 tools/vectors/bt_pairing_derivations.py create mode 100644 tools/vectors/build_bt_pairing_vectors.py create mode 100644 tools/vectors/build_cpace_vectors.py create mode 100644 tools/vectors/cpace_pin_derivations.py create mode 100644 tools/vectors/cpace_source_values.py create mode 100644 tools/vectors/cpace_x25519_sha512.py create mode 100644 tools/vectors/noise_nnpsk0.py create mode 100644 tools/vectors/verify_bt_pairing_checks.py create mode 100644 tools/vectors/verify_cpace_checks.py diff --git a/test-vectors/README.md b/test-vectors/README.md index b963e09..db48f69 100644 --- a/test-vectors/README.md +++ b/test-vectors/README.md @@ -94,6 +94,50 @@ The whole PAIR-01 exchange (API 1–5 and the body of API 8) and the PAIR-02 Sec ### `discovery-hint.json` TXT `h` hints (0.4.1, CONN-01 step 3 and API 1–2) from the `prk` of `pair-prk.json`. `kind` = `key` (one per pair of `pair-prk.json`): `pair_id`, `prk_source`, `prk`, `info`, `length`, `k_disc`; `hours` = `[{hour, message, mac, hint}]` with `message` = `"HLDISC1"` ‖ hour int64 BE, for hours −1, 0, 1, 2 and 479762–479765; `clock` = `[{now_ms, hour, advertised, accepted}]`: the hour, the hint the phone advertises and the hints a client at that time accepts (`[previous, current, next]`), at 0, 3 599 999, 3 600 000, 1 727 150 000 123, 1 727 150 399 999 and 1 727 150 400 000. `kind` = `match`: a phone with the pairs `phone_pairs` advertises `txt_h` (in that order) at `phone_now_ms` (`phone_hour`); a client of `client_pair` at `client_now_ms` (`client_hour`) accepts `accepted` and must find `matched_hint`; `matched_as` = `previous`, `current` or `next` is the phone's hour relative to the client's. Scenarios: the same hour; the phone's clock behind and ahead by 1 ms across the hour boundary; behind and ahead by exactly one hour. `invalid_vectors`: `{name, pair, reason, client_now_ms, client_hour, txt_h}` plus evidence; the client must **not** treat `txt_h` as a match: `hint_outside_window` (`phone_now_ms`, `phone_hour`: the phone's hour is two away from the client's — ahead by one hour and 1 ms across two boundaries, or behind by exactly two hours; 0.4.1 accepts only the previous, current and next hour), `wrong_byte_order` (hour as int64 LE), `wrong_encoding` (hour as int32 BE), `wrong_label` (`"HLDISC1|"`), `wrong_key` (HMAC keyed with `PRK` instead of `K_disc`, `mac_key`); `message` = the bytes that were MACed. +### `bt-pairing.json` +Bluetooth pairing keys and the pairing-link "first frame" (00-common-specs.md 0.4.5 "Keys", "Pairing mode", +"Link-security handshake"), chained from the `pairing_secret` of `pair-prk.json`. `kind` = `key` (one per pair): +`pair_id`, `pairing_secret_source`, `pairing_secret`, `k_bt_pdisc_info`, `k_bt_pdisc` = HKDF-SHA256(ikm = +`pairing_secret`, empty salt, info `"handlive/v1/bt-pair-disc"`, L 32), `k_bt_plink_info`, `k_bt_plink` (same with +info `"handlive/v1/bt-pair-link"`), `pr_bt_label` (`"HLBTPAIR1"`), `pr_bt_mac` = HMAC-SHA256(`k_bt_pdisc`, +`pr_bt_label`), `pr_bt` = its first 4 bytes. `kind` = `first_frame`: message 1 (tokens `"psk, e"`) of +`Noise_NNpsk0_25519_ChaChaPoly_SHA256` (Noise Protocol Framework rev. 34), `psk` = `k_bt_plink` of pair 1, `mode` +`qr`, `prologue` = `"HLBT1"` ‖ `n_s`. Every intermediate value of the Noise symmetric state is recorded: `h0` +(`=InitializeSymmetric`), `h_after_prologue` (`MixHash`), `mix_key_and_hash` (`{temp_key, ck, temp_h, temp_k, h}` +of the `psk` token), `h_after_psk`, `ck_after_psk`, `k`, `e_priv`/`e_pub`, `h_after_e`, `nonce` (12 zero bytes, +`n = 0`), `ciphertext_tag` (`EncryptAndHash("")`, 16 bytes since the plaintext is empty), `h_final`, `message1` = +`e_pub` ‖ `ciphertext_tag`, and `record` = `rlen` (uint16 BE) ‖ `kind` (`0x02`) ‖ `message1` (0.4.5 "Link +records"). This is **not** a vector published by the Noise specification (it publishes none for HandLive's own +psk/prologue): it is HandLive's own use of that document's algorithm, so it needs its own review before A7.1a/M7.1b +rely on it. No PIN-mode vector yet (`psk` = `k_bt_pinlink`): `cpace.json` now has the `ISK`, this file just has not +been wired to it yet (left for a follow-up task). +`invalid_vectors`: `{name, reason, n_s, e_pub, ciphertext_tag}` — the responder's candidate psk must **not** open +the tag: `wrong_key` (tries `k_bt_plink` of the other pair, `wrong_psk_source`, `wrong_psk`), `tag_mismatch` (last +bit of `ciphertext_tag` flipped), `message_tampered` (a bit of `e_pub` flipped in transit), `aad_mismatch` (the +same `message1` replayed under a different `n_s`, so a different `prologue` — "the prologue binds `n_s`, so a +recorded `noise1` never verifies on a new channel"). + +### `cpace.json` +CPACE-X25519-SHA512 (draft-irtf-cfrg-cpace-21, suite G_X25519 + SHA-512), used for PIN pairing over Bluetooth with +no network (00-common-specs.md 0.4.5, PAIR-01 flow C). `kind` = `official_generator`: the draft's own +`calculate_generator` vector (Appendix B.1.1) — `H`, `PRS`, `CI`, `sid`, `generator_string`, +`hash_generator_string`, `decoded_field_element`, `g`. `kind` = `official_message`: the draft's own message +A/B, K and ISK vector (Appendix B.1.2–B.1.7) — `g`, `ya`/`ADa`/`Ya`, `yb`/`ADb`/`Yb`, `K`, +`ISK_initiator_responder` (the initiator/responder transcript HandLive uses, since C always initiates and S +always responds), plus `ISK_parallel_source` for reference (the draft's parallel/symmetric variant, not used by +HandLive). `kind` = `handlive_pin`: HandLive's own case — `pin` (UTF-8 bytes = PRS, one vector keeps a leading +zero), `ci` = `"HLBTPIN1"` fixed, `n_s` (32 bytes, doubles as `sid`), `ya`/`yb`, `g`, `Ya`, `Yb`, `K`, `ISK`, +`k_bt_pinlink_info`/`k_bt_pinlink` = HKDF-SHA256(ikm = `ISK`, salt = `n_s`, info `"handlive/v1/bt-pin-link"`, L +32), `k_bt_pin_info`/`k_bt_pin` (same, info `"handlive/v1/bt-pin-pair"`). `invalid_vectors`: +`low_order_ya`/`low_order_yb` — the other side receives a low-order point from the draft's Appendix B.1.10 (`u0`, +in the "MUST trigger abort" set regardless of scalar) as `Ya`/`Yb`; `K` must come back `None` (`G.I`) and the +channel closes without an answer (0.4.5: "An all-zero `K` → close without answer"). `sid_mismatch`: C and S use +different `n_s` (`n_s_c`/`n_s_s`) for the same PIN — `isk_c` and `isk_s` must differ. `wrong_pin`: one side +mistypes the PIN (`pin_c`/`pin_s`, same `n_s`) — the generators differ, so `isk_c` and `isk_s` must differ (or +either side aborts). Neither of the last two is an abort inside CPace itself; they only surface once `noise1` +(psk = `K_bt_pinlink`) fails to open in the next step — `bt-pairing.json` does not yet have a PIN-mode +`first_frame` vector wired to this `ISK`, left for a follow-up task. + ### `session-handshake.json` Chained from `pair-prk.json` by `pair_id`. `k_auth`; `client_eph_priv/pub`, `client_nonce` (32); `t1`, `hello_mac` = HMAC-SHA256(k_auth, t1); `server_eph_*`, `server_nonce`, `t2`, `welcome_mac`; `eph_shared` = X25519(eph C, eph S); `secret_salt` = SHA-256(t2); `secret` (64) = HKDF(eph_shared ‖ prk, secret_salt, `"handlive/v1/session"`, 64); `k_c2s` = secret[0:32], `k_s2c` = secret[32:64]. `hello_plaintext`/`welcome_plaintext`: JSON `{op, data}` with `eph`/`nonce`/`mac` as b64u; `hello_envelope`/`welcome_envelope`: envelope `type = session`, `payload` = b64 of the plaintext (not encrypted). `invalid_vectors`: `{key, message, mac}` — HMAC(key, message) compared in constant time with `mac` must **differ**. diff --git a/test-vectors/README.vi.md b/test-vectors/README.vi.md index 22d8234..a16f640 100644 --- a/test-vectors/README.vi.md +++ b/test-vectors/README.vi.md @@ -94,6 +94,49 @@ Toàn bộ trao đổi PAIR-01 (API 1–5 và thân request của API 8) và Mã ### `discovery-hint.json` Gợi ý TXT `h` (0.4.1, CONN-01 bước 3 và API 1–2) từ `prk` của `pair-prk.json`. `kind` = `key` (mỗi cặp của `pair-prk.json` một vector): `pair_id`, `prk_source`, `prk`, `info`, `length`, `k_disc`; `hours` = `[{hour, message, mac, hint}]` với `message` = `"HLDISC1"` ‖ giờ int64 BE, cho các giờ −1, 0, 1, 2 và 479762–479765; `clock` = `[{now_ms, hour, advertised, accepted}]`: giờ, gợi ý điện thoại quảng bá và các gợi ý client chấp nhận vào lúc đó (`[giờ trước, giờ này, giờ sau]`), tại 0, 3 599 999, 3 600 000, 1 727 150 000 123, 1 727 150 399 999 và 1 727 150 400 000. `kind` = `match`: điện thoại có các cặp `phone_pairs` quảng bá `txt_h` (theo đúng thứ tự đó) lúc `phone_now_ms` (`phone_hour`); client của `client_pair` lúc `client_now_ms` (`client_hour`) chấp nhận `accepted` và phải tìm thấy `matched_hint`; `matched_as` = `previous`, `current` hoặc `next` là giờ của điện thoại so với giờ của client. Kịch bản: cùng giờ; đồng hồ điện thoại chậm hơn và nhanh hơn 1 ms qua mốc giờ; chậm hơn và nhanh hơn đúng một giờ. `invalid_vectors`: `{name, pair, reason, client_now_ms, client_hour, txt_h}` kèm bằng chứng; client **không** được coi `txt_h` là khớp: `hint_outside_window` (`phone_now_ms`, `phone_hour`: giờ của điện thoại cách giờ của client hai giờ — nhanh hơn một giờ và 1 ms vượt hai mốc giờ, hoặc chậm hơn đúng hai giờ; 0.4.1 chỉ chấp nhận giờ trước, giờ hiện tại và giờ sau), `wrong_byte_order` (giờ int64 LE), `wrong_encoding` (giờ int32 BE), `wrong_label` (`"HLDISC1|"`), `wrong_key` (HMAC khóa bằng `PRK` thay vì `K_disc`, `mac_key`); `message` = các byte đã được MAC. +### `bt-pairing.json` +Khóa ghép đôi qua Bluetooth và "khung đầu" (first frame) của liên kết ghép đôi (00-common-specs.md 0.4.5 "Keys", +"Pairing mode", "Link-security handshake"), nối từ `pairing_secret` của `pair-prk.json`. `kind` = `key` (mỗi cặp +một vector): `pair_id`, `pairing_secret_source`, `pairing_secret`, `k_bt_pdisc_info`, `k_bt_pdisc` = +HKDF-SHA256(ikm = `pairing_secret`, salt rỗng, info `"handlive/v1/bt-pair-disc"`, L 32), `k_bt_plink_info`, +`k_bt_plink` (tương tự, info `"handlive/v1/bt-pair-link"`), `pr_bt_label` (`"HLBTPAIR1"`), `pr_bt_mac` = +HMAC-SHA256(`k_bt_pdisc`, `pr_bt_label`), `pr_bt` = 4 byte đầu của nó. `kind` = `first_frame`: message 1 (token +`"psk, e"`) của `Noise_NNpsk0_25519_ChaChaPoly_SHA256` (Noise Protocol Framework rev. 34), `psk` = `k_bt_plink` +của cặp 1, `mode` `qr`, `prologue` = `"HLBT1"` ‖ `n_s`. Mọi giá trị trung gian của trạng thái đối xứng Noise đều +được ghi lại: `h0` (`InitializeSymmetric`), `h_after_prologue` (`MixHash`), `mix_key_and_hash` +(`{temp_key, ck, temp_h, temp_k, h}` của token `psk`), `h_after_psk`, `ck_after_psk`, `k`, `e_priv`/`e_pub`, +`h_after_e`, `nonce` (12 byte 0, `n = 0`), `ciphertext_tag` (`EncryptAndHash("")`, 16 byte vì plaintext rỗng), +`h_final`, `message1` = `e_pub` ‖ `ciphertext_tag`, và `record` = `rlen` (uint16 BE) ‖ `kind` (`0x02`) ‖ +`message1` (0.4.5 "Link records"). Đây **không** phải vector do tài liệu Noise công bố (tài liệu đó không phát +hành vector cho psk/prologue riêng của HandLive): đây là cách HandLive dùng đúng thuật toán của tài liệu đó, nên +cần một vòng rà soát riêng trước khi A7.1a/M7.1b khoá theo. Chưa có vector mode PIN (`psk` = `k_bt_pinlink`): +`cpace.json` đã có `ISK`, chỉ là file này chưa nối vào (để lại cho một task sau). `invalid_vectors`: +`{name, reason, n_s, e_pub, ciphertext_tag}` — +psk ứng viên của S **không** được mở tag: `wrong_key` (thử `k_bt_plink` của cặp khác, `wrong_psk_source`, +`wrong_psk`), `tag_mismatch` (lật bit cuối của `ciphertext_tag`), `message_tampered` (lật 1 bit của `e_pub` trên +đường truyền), `aad_mismatch` (phát lại đúng `message1` dưới `n_s` khác, nên `prologue` khác — "prologue buộc +`n_s`, nên một `noise1` ghi lại không bao giờ xác thực trên kênh mới"). + +### `cpace.json` +CPACE-X25519-SHA512 (draft-irtf-cfrg-cpace-21, suite G_X25519 + SHA-512), dùng cho ghép đôi bằng PIN qua +Bluetooth không mạng (00-common-specs.md 0.4.5, PAIR-01 luồng C). `kind` = `official_generator`: vector +`calculate_generator` do draft công bố (Appendix B.1.1) — `H`, `PRS`, `CI`, `sid`, `generator_string`, +`hash_generator_string`, `decoded_field_element`, `g`. `kind` = `official_message`: vector message A/B, K và ISK +do draft công bố (Appendix B.1.2–B.1.7) — `g`, `ya`/`ADa`/`Ya`, `yb`/`ADb`/`Yb`, `K`, `ISK_initiator_responder` +(biến thể khởi tạo/trả lời mà HandLive dùng, vì C luôn khởi tạo và S luôn trả lời), thêm `ISK_parallel_source` +để tham khảo (biến thể song hành của draft, HandLive không dùng). `kind` = `handlive_pin`: case riêng của +HandLive — `pin` (UTF-8 bytes = PRS, một vector giữ số 0 đứng đầu), `ci` = `"HLBTPIN1"` cố định, `n_s` (32 byte, +cũng là `sid`), `ya`/`yb`, `g`, `Ya`, `Yb`, `K`, `ISK`, `k_bt_pinlink_info`/`k_bt_pinlink` = +HKDF-SHA256(ikm = `ISK`, salt = `n_s`, info `"handlive/v1/bt-pin-link"`, L 32), `k_bt_pin_info`/`k_bt_pin` +(tương tự, info `"handlive/v1/bt-pin-pair"`). `invalid_vectors`: `low_order_ya`/`low_order_yb` — phía đối diện +nhận một điểm bậc thấp từ Appendix B.1.10 của draft (`u0`, nằm trong tập "PHẢI kích hoạt abort" bất kể vô +hướng) làm `Ya`/`Yb`; `K` phải trả về `None` (`G.I`) và kênh đóng không trả lời (0.4.5: "Một `K` toàn 0 → đóng +kênh không trả lời"). `sid_mismatch`: C và S dùng `n_s` khác nhau (`n_s_c`/`n_s_s`) cho cùng một PIN — `isk_c` +và `isk_s` phải khác nhau. `wrong_pin`: một phía gõ sai PIN (`pin_c`/`pin_s`, cùng `n_s`) — generator hai phía +khác nhau nên `isk_c` và `isk_s` phải khác nhau (hoặc một phía abort). Cả hai trường hợp sau không phải abort +ngay trong CPace; chúng chỉ lộ ra khi `noise1` (psk = `K_bt_pinlink`) không mở được ở bước tiếp theo — +`bt-pairing.json` chưa nối vector `first_frame` mode PIN vào `ISK` này, để lại cho một task sau. + ### `session-handshake.json` Nối từ `pair-prk.json` theo `pair_id`. `k_auth`; `client_eph_priv/pub`, `client_nonce` (32); `t1`, `hello_mac` = HMAC-SHA256(k_auth, t1); `server_eph_*`, `server_nonce`, `t2`, `welcome_mac`; `eph_shared` = X25519(eph C, eph S); `secret_salt` = SHA-256(t2); `secret` (64) = HKDF(eph_shared ‖ prk, secret_salt, `"handlive/v1/session"`, 64); `k_c2s` = secret[0:32], `k_s2c` = secret[32:64]. `hello_plaintext`/`welcome_plaintext`: JSON `{op, data}` với `eph`/`nonce`/`mac` dạng b64u; `hello_envelope`/`welcome_envelope`: envelope `type = session`, `payload` = b64 của plaintext (chưa mã hóa). `invalid_vectors`: `{key, message, mac}` — HMAC(key, message) so hằng thời gian với `mac` phải **khác**. diff --git a/test-vectors/bt-pairing.json b/test-vectors/bt-pairing.json new file mode 100644 index 0000000..674e74d --- /dev/null +++ b/test-vectors/bt-pairing.json @@ -0,0 +1,100 @@ +{ + "description": "K_bt_pdisc = HKDF-SHA256(ikm = pairing_secret, salt rỗng, info \"handlive/v1/bt-pair-disc\", L 32); K_bt_plink = HKDF-SHA256(ikm = pairing_secret, salt rỗng, info \"handlive/v1/bt-pair-link\", L 32); pr_bt = 4 byte đầu của HMAC-SHA256(K_bt_pdisc, \"HLBTPAIR1\"), đổi theo mỗi QR, không suy ra được pk/ps (0.4.5 \"Keys\", \"Pairing mode\"). kind \"first_frame\": message 1 (\"psk, e\") của Noise_NNpsk0_25519_ChaChaPoly_SHA256 (Noise Protocol Framework rev. 34), psk = K_bt_plink, prologue = \"HLBT1\" ‖ n_s — xác thực \"khung đầu\" của một liên kết ghép đôi trước khi slot pre-auth được cấp (0.4.5 \"Pairing mode\", \"Link-security handshake\"). HKDF ở đây là HKDF nội bộ của Noise (3 ngõ ra qua HMAC), khác HKDF-SHA256 RFC 5869 dùng ở các file khác trong kho này. `record` = rlen (uint16 BE) ‖ kind (0x02) ‖ message1 — đúng khuôn \"Link records\" của 0.4.5. KHÔNG phải vector công bố của tài liệu Noise (tài liệu đó không phát hành vector cho psk/prologue riêng của HandLive): đây là cách HandLive dùng đúng thuật toán của tài liệu đó cho bộ psk/prologue của mình, cần Trang rà soát riêng. Chưa có vector mode PIN (psk thay bằng K_bt_pinlink): cpace.json đã có ISK, chỉ là chưa nối vào, để lại cho một task sau.", + "source": "docs/detailed-design/00-common-specs.md 0.4.5 \"Keys\", \"Pairing mode\", \"Link-security handshake\"; pairing_secret của pair-prk.json; Noise Protocol Framework rev. 34 (Noise_NNpsk0_25519_ChaChaPoly_SHA256)", + "vectors": [ + { + "name": "cặp 1", + "kind": "key", + "pair_id": "3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", + "pairing_secret_source": "pair-prk.json", + "pairing_secret": "ded3ceb0d81908fe595a576826471c0cdcbedb8807c46e2e78ca4f1ab1499581", + "k_bt_pdisc_info": "handlive/v1/bt-pair-disc", + "k_bt_pdisc": "a5c9a25c3908f41632126d88910c7c1591e194e2c3b528c7f9f6021ac5648d00", + "k_bt_plink_info": "handlive/v1/bt-pair-link", + "k_bt_plink": "8954577d9f55cc77bae94e5ae3b2ca77c1c34a5153c1582e677c8ed1f8a36426", + "pr_bt_label": "HLBTPAIR1", + "pr_bt_mac": "24be2584931e152c8835dc782aa324353d2f941f3295a73c2e95419749b5af95", + "pr_bt": "24be2584" + }, + { + "name": "cặp 2", + "kind": "key", + "pair_id": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f2a3b4c5d", + "pairing_secret_source": "pair-prk.json", + "pairing_secret": "d6d0ab47c7cd93b2fd17308fc91509a0f1f334c50f2703c22228a27fa5d230e9", + "k_bt_pdisc_info": "handlive/v1/bt-pair-disc", + "k_bt_pdisc": "74801aec60b12c0408517afd9895fa64edba413dd73588c274f7f9fda1ec485c", + "k_bt_plink_info": "handlive/v1/bt-pair-link", + "k_bt_plink": "a705d680b0be1037fc8ac0dd02c2803f7e55795564df81f55b5e9fc5c6364cec", + "pr_bt_label": "HLBTPAIR1", + "pr_bt_mac": "38dbf4483c7fa2fb62c87bab65e7ff36dfa6bc9bd0223d653dfda073808d4814", + "pr_bt": "38dbf448" + }, + { + "name": "cặp 1 / liên kết ghép đôi QR", + "kind": "first_frame", + "mode": "qr", + "pair_id": "3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", + "psk_source": "k_bt_plink của cặp 1", + "psk": "8954577d9f55cc77bae94e5ae3b2ca77c1c34a5153c1582e677c8ed1f8a36426", + "protocol_name": "Noise_NNpsk0_25519_ChaChaPoly_SHA256", + "n_s": "641cacb99fc22372cc2058145a54dda486a41d8c8aaf0d2e14a7153f0300420e", + "prologue_label": "HLBT1", + "prologue": "484c425431641cacb99fc22372cc2058145a54dda486a41d8c8aaf0d2e14a7153f0300420e", + "h0": "f0b890826fb8e6584af6948d227169c381c9ed6a4c1c90df4b9ff454fa947f7b", + "h_after_prologue": "88d9f0b6de0bce978824376d99b038449919fbb63a9b4cb84de8c826eac9ea57", + "mix_key_and_hash": { + "temp_key": "d1fe5993d0f49c69dd7535cee9ef6e4ef644e183f5f49f0c55666ac9e059eda4", + "ck": "3ba0261b99a6ab88fb8fee3f6204b310b6edbaf59aad56d8e676fe1835bca22e", + "temp_h": "b62999756f94b21d0cd7c9518479d3681fe7f8e80219c3e93212449bc7157502", + "temp_k": "de6117d2b12eead9c6b282d772b909920916d8fb1ef43bd1caa24ab037606311", + "h": "9ef7ca8694e03435f4cdc5de84c258bbb69179dc685f817c0139739589f08199" + }, + "h_after_psk": "9ef7ca8694e03435f4cdc5de84c258bbb69179dc685f817c0139739589f08199", + "ck_after_psk": "3ba0261b99a6ab88fb8fee3f6204b310b6edbaf59aad56d8e676fe1835bca22e", + "k": "de6117d2b12eead9c6b282d772b909920916d8fb1ef43bd1caa24ab037606311", + "e_priv": "9373bf265905f54866bd4566421bcf20bb7c6bf255e0afbdf6df27a6fd873d7b", + "e_pub": "676f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a550", + "h_after_e": "92ce9e8eb3761a90c9602e7b0b38dfff8855c89bd3bd6a7cacdbccf7ed669428", + "nonce": "000000000000000000000000", + "ciphertext_tag": "4d394b4515b386429b20d99902aafb7c", + "h_final": "6afe72063c65fdf2b9772f1bd4bd72ba13a69c7cf64f7c0508cc76d3d280330b", + "message1": "676f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a5504d394b4515b386429b20d99902aafb7c", + "record_kind": "0x02", + "rlen": 49, + "record": "003102676f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a5504d394b4515b386429b20d99902aafb7c" + } + ], + "invalid_vectors": [ + { + "name": "cặp 1 / S thử psk của cặp 2", + "reason": "wrong_key", + "n_s": "641cacb99fc22372cc2058145a54dda486a41d8c8aaf0d2e14a7153f0300420e", + "e_pub": "676f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a550", + "ciphertext_tag": "4d394b4515b386429b20d99902aafb7c", + "wrong_psk_source": "k_bt_plink của cặp 2", + "wrong_psk": "a705d680b0be1037fc8ac0dd02c2803f7e55795564df81f55b5e9fc5c6364cec" + }, + { + "name": "cặp 1 / tag sai", + "reason": "tag_mismatch", + "n_s": "641cacb99fc22372cc2058145a54dda486a41d8c8aaf0d2e14a7153f0300420e", + "e_pub": "676f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a550", + "ciphertext_tag": "4d394b4515b386429b20d99902aafb7d" + }, + { + "name": "cặp 1 / e_pub bị sửa trên đường truyền", + "reason": "message_tampered", + "n_s": "641cacb99fc22372cc2058145a54dda486a41d8c8aaf0d2e14a7153f0300420e", + "e_pub": "666f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a550", + "ciphertext_tag": "4d394b4515b386429b20d99902aafb7c" + }, + { + "name": "cặp 1 / phát lại message 1 trên kênh khác (n_s khác)", + "reason": "aad_mismatch", + "n_s": "5300745f6a02d27bb53c2c9852d204fcfbae14f9bfd39fc3165dfebacc2c0c6f", + "e_pub": "676f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a550", + "ciphertext_tag": "4d394b4515b386429b20d99902aafb7c" + } + ] +} diff --git a/test-vectors/cpace.json b/test-vectors/cpace.json new file mode 100644 index 0000000..052458f --- /dev/null +++ b/test-vectors/cpace.json @@ -0,0 +1,128 @@ +{ + "description": "CPACE-X25519-SHA512 (draft-irtf-cfrg-cpace-21, suite G_X25519 + SHA-512). kind \"official_generator\"/\"official_message\": vector công bố của draft (Appendix B.1), chép nguyên văn qua cpace_source_values.py, dùng để kiểm cài đặt calculate_generator/Ya/Yb/K/ISK đúng thuật toán trước khi áp case của HandLive. kind \"handlive_pin\": ghép đôi bằng PIN qua Bluetooth không mạng (0.4.5 luồng C) — PRS = UTF-8 của 6 chữ số PIN (giữ số 0 đứng đầu), CI = \"HLBTPIN1\" cố định, sid = n_s (32 byte, mới cho mỗi kênh), ADa = ADb rỗng; C khởi tạo (cpace1 = Ya), S trả lời (cpace2 = Yb); K_bt_pinlink/K_bt_pin = HKDF-SHA256(ikm = ISK, salt = n_s, info tương ứng, L 32). invalid_vectors: low_order_ya/low_order_yb (bên kia nhận một điểm bậc thấp của Appendix B.1.10 → K = G.I → phải đóng kênh không trả lời, đúng \"an all-zero K → close without answer\" của 0.4.5), sid_mismatch (hai phía dùng n_s khác nhau → ISK khác nhau), wrong_pin (PIN gõ sai một phía → generator khác nhau → ISK khác nhau hoặc K toàn 0) — cả hai trường hợp sau không bị CPace tự abort, chỉ lộ ra khi noise1 (psk = K_bt_pinlink) không mở được ở bước ghép đôi tiếp theo (bt-pairing.json, chưa nối case PIN vào — còn một việc để làm sau).", + "source": "draft-irtf-cfrg-cpace-21 Appendix B.1 (CPACE-X25519-SHA512); docs/detailed-design/00-common-specs.md 0.4.5 \"PIN (PAIR-01 flow C)\", \"Keys from ISK\"", + "vectors": [ + { + "name": "draft B.1.1 calculate_generator", + "kind": "official_generator", + "draft": "draft-irtf-cfrg-cpace-21", + "section": "Appendix B.1.1", + "H": "SHA-512", + "PRS": "50617373776f7264", + "CI": "0b415f696e69746961746f720b425f726573706f6e646572", + "sid": "7e4b4791d6a8ef019b936c79fb7f2c57", + "generator_string": "0843506163653235350850617373776f72646d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180b415f696e69746961746f720b425f726573706f6e646572107e4b4791d6a8ef019b936c79fb7f2c57", + "hash_generator_string": "03998087bdb1a2617bbe25ef5a7c18cd4f84f902328701790958755ee4aed1d3", + "decoded_field_element": "03998087bdb1a2617bbe25ef5a7c18cd4f84f902328701790958755ee4aed153", + "g": "d04bf6d41f6a289632a2e929fa29bebd51092512a7829fdde7d314b62f05a73f" + }, + { + "name": "draft B.1.2-B.1.7 message A/B, K, ISK", + "kind": "official_message", + "draft": "draft-irtf-cfrg-cpace-21", + "section": "Appendix B.1.2-B.1.7", + "PRS": "50617373776f7264", + "CI": "0b415f696e69746961746f720b425f726573706f6e646572", + "sid": "7e4b4791d6a8ef019b936c79fb7f2c57", + "g": "d04bf6d41f6a289632a2e929fa29bebd51092512a7829fdde7d314b62f05a73f", + "ya": "21b4f4bd9e64ed355c3eb676a28ebedaf6d8f17bdc365995b319097153044080", + "ADa": "414461", + "Ya": "1d13c89278cdadd826f6d8d7f887701430f8380ddc17611cdd6dc989ce0c9f32", + "yb": "848b0779ff415f0af4ea14df9dd1d3c29ac41d836c7808896c4eba19c51ac40a", + "ADb": "414462", + "Yb": "248cccf6d5cdc3646f0ad593f9e6cef4e69d4945f8372e623512ecea32185623", + "K": "5b067effbdc0b2a0e1d907b21ebb25cfedb96a852179a847c37e43ee71322c6b", + "ISK_initiator_responder": "6e19b875f7a561d6b3ca3dbb9ef42ac55de3e717881018204b8922b4d5e53bb2aa82c300bea7b65d2b671da71922ddf6472301b79bc270adfa8bf413285f2263", + "ISK_parallel_source": "eef745e2f6e7ae2b1a1e53da340e777167a07fe150436648c51fb199c11f3cbabfc683a2b48e1af5881940dc398d375c95e6b4ae9948a45b8770de0656382be4", + "note": "ISK_parallel_source là biến thể song hành (transcript_oc) công bố trong draft; HandLive chỉ dùng khởi tạo/trả lời (transcript_ir, ISK_initiator_responder) vì C luôn khởi tạo và S luôn trả lời (0.4.5 luồng C)." + }, + { + "name": "HandLive PIN 482917", + "kind": "handlive_pin", + "pin": "482917", + "ci": "HLBTPIN1", + "n_s": "d9ffef6cbff35962ba8a34204c9120e28c38ea8359e142a2078093e16174db9f", + "ya": "967a4f831f84d786d4b93add9c4df87970a7f755d988458e623d647e2b475756", + "yb": "7055ce569105f131eb4e136022b31813a2935be018132e34876a5d305a595e38", + "g": "dbd7b61972017c48ce9fdf0b27680bc473cd5c1b249c70da989f93921b194c4a", + "Ya": "4f55e241e6868d6248c26c3265ea833af2590ed8e000a069c1b15cf9f387b028", + "Yb": "65e7198a167a05e257689343d539b1135249c1ef184498e6f05345c749dc6876", + "K": "f5e92fa92192e3757d91871da7e9b3378db7f7863d480a218297bfd7f7dd8c4c", + "ISK": "a140d1555c39acfade8e5614f01f3f653e48bac557cb8696aa6e3cae697ddd4d1ac4bd465bb46d4b03e626eda18c22afc68027f2483639199033e5015d06559d", + "k_bt_pinlink_info": "handlive/v1/bt-pin-link", + "k_bt_pinlink": "c9c3410f6cef4b6f9f171728a89e9e5a957ae69565a479aacb33ae5d5333919d", + "k_bt_pin_info": "handlive/v1/bt-pin-pair", + "k_bt_pin": "697f1ab82b22fdb4fa9058e03ea4ef1ae0f83f04496c48586c06517faf8bc011" + }, + { + "name": "HandLive PIN với số 0 đứng đầu", + "kind": "handlive_pin", + "pin": "007042", + "ci": "HLBTPIN1", + "n_s": "649a041524d8e3283aecd1ae96bf77b4b8a891f0f64919dca1cb5b0299e793e7", + "ya": "59b192ebcd0c08df5eeb4c8d60e5dfc3a2a4a73eb8e947a1698f2f202a44728a", + "yb": "e75f7662275c3291e81e8b3a1a0de45762a4ddadec1bf8f6df0d99355572916a", + "g": "11d8f2ec1d5f75970b2e29e3450ff03adf35fbbc0052b4e0fb182fb2283fa762", + "Ya": "b73f7368964e21efa50a78985d9fe9a3b60811a33e286bb7b55cb5f418861732", + "Yb": "681dcca28dbc603a7d278fea8e0b5ddd8eafa19f364cae9c7a31d30a6b56c40e", + "K": "6e2372e1549d0903cd98a42cb60433fd749dcd6b336da102337a9a5f7c01bc20", + "ISK": "49e3b485cdb41c789d0517e73b1fe8f55c9e8c5b5438cfc91d744fb35066f9da9cee2af9e2a0d7172ca019f4e07718ec0e4f9e0d2e1c9b04be13f4bda62b40b2", + "k_bt_pinlink_info": "handlive/v1/bt-pin-link", + "k_bt_pinlink": "ff1ec28fb791604a5463b07b4a766e79a969957a5f261926c372e7759a30172f", + "k_bt_pin_info": "handlive/v1/bt-pin-pair", + "k_bt_pin": "68d6c9d67c60edb2e24829dbadcb3bb8a579850287d896cb4ed405d54f7840ef" + } + ], + "invalid_vectors": [ + { + "name": "Ya bậc thấp (C gửi điểm bậc thấp)", + "reason": "low_order_ya", + "pin": "482917", + "n_s": "39b2916f56d70c3a961b0c5c9a529554296e1444895a7b3f4d92bd7fe6b6c975", + "g": "ae90317d0ec527b03bfe2913bba8894a4867d9e9108c2211ceb9cebb67dd8c38", + "Ya_attacker": "0000000000000000000000000000000000000000000000000000000000000000", + "yb": "24a846ac94b2877f88b6fbc05d26bc34a6b39eca3f4111f86495bb545d7c072c", + "Yb": "70fc3700ef310a827f960e44a3789e658bf193af4de502ddaf903c17d45f1b50", + "expect": "S (trả lời) tính K = scalar_mult_vfy(yb, Ya) → None (G.I) → đóng kênh không trả lời" + }, + { + "name": "Yb bậc thấp (S trả lời điểm bậc thấp)", + "reason": "low_order_yb", + "pin": "482917", + "n_s": "f45b17823a5be592c77a25787d33d1e933468b16d6351e63ff694cbf21005ac8", + "g": "ac838292b96d3819195dd0e603e64db81e13bb201af332e55508510e11a9b962", + "ya": "87c62e1364379d2e7c38eca52153ff6fbd3ff23eac4d08995d3082df3f0b7221", + "Ya": "97443720baeff9822c713bc744c9b27b5d99addfc89b24dd31ef5ecf7e748c04", + "Yb_attacker": "0000000000000000000000000000000000000000000000000000000000000000", + "expect": "C (khởi tạo) tính K = scalar_mult_vfy(ya, Yb) → None (G.I) → đóng kênh không trả lời" + }, + { + "name": "sid khác nhau giữa C và S", + "reason": "sid_mismatch", + "pin": "482917", + "n_s_c": "e87431400d2326edce7bffa661f2410750593a7440da45318ebda492ee50e83b", + "n_s_s": "63013053536e4e93452b14122d1a20eb050b299b57614bc5a11036367955959f", + "ya": "a624ee31fddf2d27bed5f94805862a950cce7e8ff55b5c8a294c9739d8493d3b", + "yb": "7d9db7d0df243d74b0c2a11faa0b816c393a60995aedecefafec418f8be44ef1", + "Ya": "b2873620a0f94f86829caac85574aeb235cae323659be55ffe9de735353ea12c", + "Yb": "4172d1fa46dc332c8c67bb3d1ea7a4e36837bfc2ec4af634e09f1148059d1140", + "isk_c": "1abba784dcb2299e3a9268b7e1e63c1b7827e679b82a2c2556c9354f0486cb4bde38a7ff41dc3769eda23fd568d328719117e9170da98cec47458854fbe63694", + "isk_s": "b0c2c262912fd60f3034203566e7e4f8d12cc1fc2d5b5daada407181a8d319369a65437799f9b185b222d6d28b7c61728ce8bdaaf147b4a486f08ade01c87cf9", + "expect": "C dùng sid của mình, S dùng sid của mình (hai kênh khác nhau trộn vào) → ISK hai phía khác nhau (isk_c != isk_s), K_bt_pinlink suy từ đó cũng khác → noise1 của S không mở được" + }, + { + "name": "PIN sai ở một phía", + "reason": "wrong_pin", + "n_s": "a703f54bfb6373687d58b76bce23bcf8b703fd038e93a165ca19c06981e64338", + "pin_c": "482917", + "pin_s": "482918", + "ya": "3741bd1815d80f12d33a99f7a1af3472a902059b10737199d51e8ff9261eaea6", + "yb": "e31412bdc16c0c3387c341735477c6e25567e984287547e157b6ecf85f7415c4", + "Ya": "e01e299f6deb35b29bfc479c7c41efeef7c4285d6fee38f5b9f439c4c4ed9a75", + "Yb": "f7d79dec08cd392efea84b0042f3870a38a61d935c95c078609048491a729a08", + "isk_c": "945d17603e9ce161f8f9a99e56401fc60354c05e588442b9a6a49dbb26fc710d1d51f5cc82609827f14b15a4cb0210b791674826a7decef29d57e9aece3edc97", + "isk_s": "e7ec60648010bea911bb0a32a404af342628f932468f56ceca3aa5322da2ca970d43baff73e5df304a04e5b083ac35044042621178f6ffe4f7512102f27eeee2", + "expect": "PIN gõ sai ở một phía → generator khác nhau → ISK hai phía khác nhau (hoặc K toàn 0) → noise1 dùng K_bt_pinlink sai không mở được, một lần thử bị tính vào bộ đếm chung" + } + ] +} diff --git a/tools/vectors/bt_pairing_derivations.py b/tools/vectors/bt_pairing_derivations.py new file mode 100644 index 0000000..686a076 --- /dev/null +++ b/tools/vectors/bt_pairing_derivations.py @@ -0,0 +1,22 @@ +"""K_bt_pdisc/K_bt_plink và pr_bt (00-common-specs.md 0.4.5 "Keys", "Pairing mode"), dẫn từ `pairing_secret` của +pair-prk.json — dùng để SINH vector.""" +from handlive_protocol_derivations import hkdf, hmac256 + +PDISC_INFO = b"handlive/v1/bt-pair-disc" +PLINK_INFO = b"handlive/v1/bt-pair-link" +PR_BT_LABEL = b"HLBTPAIR1" + + +def k_bt_pdisc(pairing_secret: bytes) -> bytes: + """K_bt_pdisc = HKDF-SHA256(ikm = pairing_secret, salt rỗng, info "handlive/v1/bt-pair-disc", L = 32).""" + return hkdf(pairing_secret, PDISC_INFO, 32) + + +def k_bt_plink(pairing_secret: bytes) -> bytes: + """K_bt_plink = HKDF-SHA256(ikm = pairing_secret, salt rỗng, info "handlive/v1/bt-pair-link", L = 32).""" + return hkdf(pairing_secret, PLINK_INFO, 32) + + +def pr_bt(pdisc_key: bytes) -> bytes: + """pr_bt = 4 byte đầu của HMAC-SHA256(K_bt_pdisc, "HLBTPAIR1").""" + return hmac256(pdisc_key, PR_BT_LABEL)[:4] diff --git a/tools/vectors/build_bt_pairing_vectors.py b/tools/vectors/build_bt_pairing_vectors.py new file mode 100644 index 0000000..c167a70 --- /dev/null +++ b/tools/vectors/build_bt_pairing_vectors.py @@ -0,0 +1,104 @@ +"""Dựng bt-pairing.json: K_bt_pdisc/K_bt_plink, pr_bt (00-common-specs.md 0.4.5 "Keys", "Pairing mode") và một +vector "khung đầu" (first frame) — message 1 của Noise_NNpsk0_25519_ChaChaPoly_SHA256, psk = K_bt_plink — cho một +liên kết ghép đôi QR, cộng các mẫu âm của nó. + +Chưa có vector mode PIN (psk = K_bt_plink thay bằng K_bt_pinlink): cpace.json đã có ISK, file này chỉ là chưa +nối vào — xem ghi chú trong `description`. +""" +import bt_pairing_derivations as BT +import noise_nnpsk0 as N +from handlive_protocol_derivations import test_bytes + +H = bytes.fromhex +SPEC = "docs/detailed-design/00-common-specs.md" +PROLOGUE_LABEL = b"HLBT1" + + +def _key_vector(pv: dict) -> dict: + secret = H(pv["pairing_secret"]) + pdisc, plink = BT.k_bt_pdisc(secret), BT.k_bt_plink(secret) + mac = BT.hmac256(pdisc, BT.PR_BT_LABEL) + return {"name": pv["name"], "kind": "key", "pair_id": pv["pair_id"], "pairing_secret_source": "pair-prk.json", + "pairing_secret": secret.hex(), "k_bt_pdisc_info": BT.PDISC_INFO.decode(), "k_bt_pdisc": pdisc.hex(), + "k_bt_plink_info": BT.PLINK_INFO.decode(), "k_bt_plink": plink.hex(), + "pr_bt_label": BT.PR_BT_LABEL.decode(), "pr_bt_mac": mac.hex(), "pr_bt": mac[:4].hex()} + + +def _first_frame_vector(pv: dict, plink: bytes) -> dict: + n_s = test_bytes(f"bt first-frame n_s {pv['name']}", 32) + prologue = PROLOGUE_LABEL + n_s + e_priv = test_bytes(f"bt first-frame e_priv {pv['name']}", 32) + r = N.first_message(plink, prologue, e_priv) + opened = N.try_open_first_message(plink, prologue, r["e_pub"], r["ciphertext_tag"]) + assert opened == b"", f"{pv['name']}: khung đầu phải mở được với đúng psk" + kind, message1 = 0x02, r["message1"] + rlen = 1 + len(message1) + record = rlen.to_bytes(2, "big") + bytes([kind]) + message1 + return {"name": f"{pv['name']} / liên kết ghép đôi QR", "kind": "first_frame", "mode": "qr", + "pair_id": pv["pair_id"], "psk_source": f"k_bt_plink của {pv['name']}", "psk": plink.hex(), + "protocol_name": r["protocol_name"].decode(), "n_s": n_s.hex(), "prologue_label": PROLOGUE_LABEL.decode(), + "prologue": prologue.hex(), "h0": r["h0"].hex(), "h_after_prologue": r["h1"].hex(), + "mix_key_and_hash": {k: v.hex() for k, v in r["mix_key_and_hash"].items()}, + "h_after_psk": r["h2"].hex(), "ck_after_psk": r["ck1"].hex(), "k": r["k"].hex(), + "e_priv": r["e_priv"].hex(), "e_pub": r["e_pub"].hex(), "h_after_e": r["h3"].hex(), + "nonce": r["nonce"].hex(), "ciphertext_tag": r["ciphertext_tag"].hex(), "h_final": r["h4"].hex(), + "message1": message1.hex(), "record_kind": "0x02", "rlen": rlen, "record": record.hex()} + + +def _negatives(pv1: dict, pv2: dict, plink1: bytes, plink2: bytes, base: dict) -> list[dict]: + n_s, e_pub, ct = H(base["n_s"]), H(base["e_pub"]), H(base["ciphertext_tag"]) + prologue = H(base["prologue"]) + out = [] + + assert N.try_open_first_message(plink2, prologue, e_pub, ct) is None + out.append({"name": f"{pv1['name']} / S thử psk của {pv2['name']}", "reason": "wrong_key", + "n_s": base["n_s"], "e_pub": base["e_pub"], "ciphertext_tag": base["ciphertext_tag"], + "wrong_psk_source": f"k_bt_plink của {pv2['name']}", "wrong_psk": plink2.hex()}) + + bad_ct = bytearray(ct) + bad_ct[-1] ^= 0x01 + assert N.try_open_first_message(plink1, prologue, e_pub, bytes(bad_ct)) is None + out.append({"name": f"{pv1['name']} / tag sai", "reason": "tag_mismatch", "n_s": base["n_s"], + "e_pub": base["e_pub"], "ciphertext_tag": bytes(bad_ct).hex()}) + + bad_e = bytearray(e_pub) + bad_e[0] ^= 0x01 + assert N.try_open_first_message(plink1, prologue, bytes(bad_e), ct) is None + out.append({"name": f"{pv1['name']} / e_pub bị sửa trên đường truyền", "reason": "message_tampered", + "n_s": base["n_s"], "e_pub": bytes(bad_e).hex(), "ciphertext_tag": base["ciphertext_tag"]}) + + other_n_s = test_bytes(f"bt first-frame n_s khác kênh {pv1['name']}", 32) + assert N.try_open_first_message(plink1, PROLOGUE_LABEL + other_n_s, e_pub, ct) is None + out.append({"name": f"{pv1['name']} / phát lại message 1 trên kênh khác (n_s khác)", "reason": "aad_mismatch", + "n_s": other_n_s.hex(), "e_pub": base["e_pub"], "ciphertext_tag": base["ciphertext_tag"]}) + return out + + +DESCRIPTION = ( + "K_bt_pdisc = HKDF-SHA256(ikm = pairing_secret, salt rỗng, info \"handlive/v1/bt-pair-disc\", L 32); " + "K_bt_plink = HKDF-SHA256(ikm = pairing_secret, salt rỗng, info \"handlive/v1/bt-pair-link\", L 32); " + "pr_bt = 4 byte đầu của HMAC-SHA256(K_bt_pdisc, \"HLBTPAIR1\"), đổi theo mỗi QR, không suy ra được pk/ps " + "(0.4.5 \"Keys\", \"Pairing mode\"). kind \"first_frame\": message 1 (\"psk, e\") của " + "Noise_NNpsk0_25519_ChaChaPoly_SHA256 (Noise Protocol Framework rev. 34), psk = K_bt_plink, prologue = " + "\"HLBT1\" ‖ n_s — xác thực \"khung đầu\" của một liên kết ghép đôi trước khi slot pre-auth được cấp (0.4.5 " + "\"Pairing mode\", \"Link-security handshake\"). HKDF ở đây là HKDF nội bộ của Noise (3 ngõ ra qua HMAC), " + "khác HKDF-SHA256 RFC 5869 dùng ở các file khác trong kho này. `record` = rlen (uint16 BE) ‖ kind (0x02) ‖ " + "message1 — đúng khuôn \"Link records\" của 0.4.5. KHÔNG phải vector công bố của tài liệu Noise (tài liệu đó " + "không phát hành vector cho psk/prologue riêng của HandLive): đây là cách HandLive dùng đúng thuật toán của " + "tài liệu đó cho bộ psk/prologue của mình, cần Trang rà soát riêng. Chưa có vector mode PIN (psk thay bằng " + "K_bt_pinlink): cpace.json đã có ISK, chỉ là chưa nối vào, để lại cho một task sau." +) + + +def build(ctx: dict) -> dict: + pairs = {p["name"]: p for p in ctx["pairs"]} + pv1, pv2 = pairs["cặp 1"], pairs["cặp 2"] + key_vectors = [_key_vector(pv1), _key_vector(pv2)] + plink = {v["name"]: H(v["k_bt_plink"]) for v in key_vectors} + first_frame = _first_frame_vector(pv1, plink["cặp 1"]) + negatives = _negatives(pv1, pv2, plink["cặp 1"], plink["cặp 2"], first_frame) + doc = {"description": DESCRIPTION, + "source": f"{SPEC} 0.4.5 \"Keys\", \"Pairing mode\", \"Link-security handshake\"; pairing_secret của " + "pair-prk.json; Noise Protocol Framework rev. 34 (Noise_NNpsk0_25519_ChaChaPoly_SHA256)", + "vectors": key_vectors + [first_frame], "invalid_vectors": negatives} + return {"bt-pairing.json": doc} diff --git a/tools/vectors/build_cpace_vectors.py b/tools/vectors/build_cpace_vectors.py new file mode 100644 index 0000000..282bba8 --- /dev/null +++ b/tools/vectors/build_cpace_vectors.py @@ -0,0 +1,165 @@ +"""Dựng cpace.json: vector chính thức của draft-irtf-cfrg-cpace-21 (CPACE-X25519-SHA512, Appendix B.1), rồi các +case theo cách HandLive dùng (PIN qua Bluetooth không mạng, 00-common-specs.md 0.4.5 luồng C): `g`, `Ya`, `Yb`, +`K`, `ISK`, `K_bt_pinlink`, `K_bt_pin`; cộng mẫu âm (Ya/Yb bậc thấp, sid khác, PIN sai). +""" +import cpace_pin_derivations as PIN +import cpace_x25519_sha512 as C +from cpace_source_values import ( + CPACE_X25519_SHA512_GENERATOR as SRC_GEN, + CPACE_X25519_SHA512_LOW_ORDER_POINTS as SRC_LOW_POINTS, + CPACE_X25519_SHA512_MESSAGE as SRC_MSG, +) +from handlive_protocol_derivations import test_bytes + +H = bytes.fromhex +SPEC = "docs/detailed-design/00-common-specs.md" +DRAFT = "draft-irtf-cfrg-cpace-21" +HL_CI = b"HLBTPIN1" + + +def _official_generator_vector() -> dict: + prs, ci, sid = H(SRC_GEN["PRS"]), H(SRC_GEN["CI"]), H(SRC_GEN["sid"]) + g = C.calculate_generator(prs, ci, sid) + assert g == H(SRC_GEN["generator g"]), "calculate_generator phải khớp Appendix B.1.1 của draft" + return {"name": "draft B.1.1 calculate_generator", "kind": "official_generator", "draft": DRAFT, + "section": "Appendix B.1.1", "H": SRC_GEN["H"], "PRS": SRC_GEN["PRS"], "CI": SRC_GEN["CI"], + "sid": SRC_GEN["sid"], "generator_string": SRC_GEN["generator_string(G.DSI,PRS,CI,sid,H.s_in_bytes)"], + "hash_generator_string": SRC_GEN["hash generator string"], + "decoded_field_element": SRC_GEN["decoded field element of 255 bits"], "g": g.hex()} + + +def _official_message_vector() -> dict: + prs, ci, sid = H(SRC_MSG["PRS"]), H(SRC_MSG["CI"]), H(SRC_MSG["sid"]) + ya, ADa, yb, ADb = H(SRC_MSG["ya"]), H(SRC_MSG["ADa"]), H(SRC_MSG["yb"]), H(SRC_MSG["ADb"]) + r = C.run_both_sides(prs, ci, sid, ya, ADa, yb, ADb) + assert r["g"] == H(SRC_MSG["g"]) + assert r["Ya"] == H(SRC_MSG["Ya"]) and r["Yb"] == H(SRC_MSG["Yb"]) + assert r["k_a"] == H(SRC_MSG["K"]) == r["k_b"] + assert r["isk_a"] == H(SRC_MSG["ISK_IR"]) == r["isk_b"] + return {"name": "draft B.1.2-B.1.7 message A/B, K, ISK", "kind": "official_message", "draft": DRAFT, + "section": "Appendix B.1.2-B.1.7", "PRS": SRC_MSG["PRS"], "CI": SRC_MSG["CI"], "sid": SRC_MSG["sid"], + "g": r["g"].hex(), "ya": SRC_MSG["ya"], "ADa": SRC_MSG["ADa"], "Ya": r["Ya"].hex(), "yb": SRC_MSG["yb"], + "ADb": SRC_MSG["ADb"], "Yb": r["Yb"].hex(), "K": r["k_a"].hex(), "ISK_initiator_responder": r["isk_a"].hex(), + "ISK_parallel_source": SRC_MSG["ISK_SY"], + "note": "ISK_parallel_source là biến thể song hành (transcript_oc) công bố trong draft; HandLive chỉ " + "dùng khởi tạo/trả lời (transcript_ir, ISK_initiator_responder) vì C luôn khởi tạo và S luôn " + "trả lời (0.4.5 luồng C)."} + + +def _handlive_pin_vector(name: str, pin: str) -> dict: + prs = pin.encode("utf-8") + n_s = test_bytes(f"cpace pin n_s {name}", 32) + ya = test_bytes(f"cpace pin ya {name}", 32) + yb = test_bytes(f"cpace pin yb {name}", 32) + r = C.run_both_sides(prs, HL_CI, n_s, ya, b"", yb, b"") + assert r["k_a"] is not None and r["k_a"] == r["k_b"], f"{name}: K phải khớp cả hai phía" + assert r["isk_a"] == r["isk_b"] + isk = r["isk_a"] + pinlink, pinpair = PIN.k_bt_pinlink(isk, n_s), PIN.k_bt_pin(isk, n_s) + return {"name": name, "kind": "handlive_pin", "pin": pin, "ci": HL_CI.decode(), "n_s": n_s.hex(), + "ya": ya.hex(), "yb": yb.hex(), "g": r["g"].hex(), "Ya": r["Ya"].hex(), "Yb": r["Yb"].hex(), + "K": r["k_a"].hex(), "ISK": isk.hex(), "k_bt_pinlink_info": PIN.PINLINK_INFO.decode(), + "k_bt_pinlink": pinlink.hex(), "k_bt_pin_info": PIN.PIN_INFO.decode(), "k_bt_pin": pinpair.hex()} + + +def _negative_low_order(name: str, side: str) -> dict: + """side = "ya": C (khởi tạo) gửi Ya bậc thấp, S phải abort. side = "yb": S (trả lời) gửi Yb bậc thấp, C phải + abort. Dùng điểm u0 của draft (Appendix B.1.10, bất kể vô hướng — đã kiểm bằng vô hướng cố định của draft và + một vô hướng HandLive khác, cùng kết quả abort).""" + pin = "482917" + prs = pin.encode("utf-8") + n_s = test_bytes(f"cpace neg low_order {side} n_s", 32) + g = C.calculate_generator(prs, HL_CI, n_s) + low_u = H(SRC_LOW_POINTS[0]["u"]) # u0 = zero_bytes — luôn trong tập "MUST trigger abort" + assert SRC_LOW_POINTS[0]["must_abort"] + if side == "ya": + yb = test_bytes(f"cpace neg low_order {side} yb", 32) + Yb = C.scalar_mult_vfy(yb, g) + k_s = C.scalar_mult_vfy(yb, low_u) + assert k_s is None + return {"name": name, "reason": "low_order_ya", "pin": pin, "n_s": n_s.hex(), "g": g.hex(), + "Ya_attacker": low_u.hex(), "yb": yb.hex(), "Yb": Yb.hex(), + "expect": "S (trả lời) tính K = scalar_mult_vfy(yb, Ya) → None (G.I) → đóng kênh không trả lời"} + ya = test_bytes(f"cpace neg low_order {side} ya", 32) + Ya = C.scalar_mult_vfy(ya, g) + k_c = C.scalar_mult_vfy(ya, low_u) + assert k_c is None + return {"name": name, "reason": "low_order_yb", "pin": pin, "n_s": n_s.hex(), "g": g.hex(), "ya": ya.hex(), + "Ya": Ya.hex(), "Yb_attacker": low_u.hex(), + "expect": "C (khởi tạo) tính K = scalar_mult_vfy(ya, Yb) → None (G.I) → đóng kênh không trả lời"} + + +def _negative_sid_mismatch(name: str) -> dict: + pin = "482917" + prs = pin.encode("utf-8") + n_s_c = test_bytes("cpace neg sid_mismatch n_s_c", 32) + n_s_s = test_bytes("cpace neg sid_mismatch n_s_s", 32) + assert n_s_c != n_s_s + ya = test_bytes("cpace neg sid_mismatch ya", 32) + yb = test_bytes("cpace neg sid_mismatch yb", 32) + g_c, g_s = C.calculate_generator(prs, HL_CI, n_s_c), C.calculate_generator(prs, HL_CI, n_s_s) + Ya, Yb = C.scalar_mult_vfy(ya, g_c), C.scalar_mult_vfy(yb, g_s) + k_c, k_s = C.scalar_mult_vfy(ya, Yb), C.scalar_mult_vfy(yb, Ya) + isk_c = C.isk(n_s_c, k_c, Ya, b"", Yb, b"") if k_c else None + isk_s = C.isk(n_s_s, k_s, Ya, b"", Yb, b"") if k_s else None + assert isk_c is not None and isk_s is not None and isk_c != isk_s, "sid khác nhau phải cho ISK khác nhau" + return {"name": name, "reason": "sid_mismatch", "pin": pin, "n_s_c": n_s_c.hex(), "n_s_s": n_s_s.hex(), + "ya": ya.hex(), "yb": yb.hex(), "Ya": Ya.hex(), "Yb": Yb.hex(), "isk_c": isk_c.hex(), + "isk_s": isk_s.hex(), + "expect": "C dùng sid của mình, S dùng sid của mình (hai kênh khác nhau trộn vào) → ISK hai phía " + "khác nhau (isk_c != isk_s), K_bt_pinlink suy từ đó cũng khác → noise1 của S không mở được"} + + +def _negative_wrong_pin(name: str) -> dict: + n_s = test_bytes("cpace neg wrong_pin n_s", 32) + pin_c, pin_s = "482917", "482918" + ya = test_bytes("cpace neg wrong_pin ya", 32) + yb = test_bytes("cpace neg wrong_pin yb", 32) + g_c = C.calculate_generator(pin_c.encode(), HL_CI, n_s) + g_s = C.calculate_generator(pin_s.encode(), HL_CI, n_s) + assert g_c != g_s, "PIN khác phải cho generator khác" + Ya, Yb = C.scalar_mult_vfy(ya, g_c), C.scalar_mult_vfy(yb, g_s) + k_c, k_s = C.scalar_mult_vfy(ya, Yb), C.scalar_mult_vfy(yb, Ya) + isk_c = C.isk(n_s, k_c, Ya, b"", Yb, b"") if k_c else None + isk_s = C.isk(n_s, k_s, Ya, b"", Yb, b"") if k_s else None + assert isk_c is None or isk_s is None or isk_c != isk_s, "PIN khác phải cho ISK khác (hoặc abort)" + return {"name": name, "reason": "wrong_pin", "n_s": n_s.hex(), "pin_c": pin_c, "pin_s": pin_s, "ya": ya.hex(), + "yb": yb.hex(), "Ya": Ya.hex(), "Yb": Yb.hex(), "isk_c": isk_c.hex() if isk_c else None, + "isk_s": isk_s.hex() if isk_s else None, + "expect": "PIN gõ sai ở một phía → generator khác nhau → ISK hai phía khác nhau (hoặc K toàn 0) → " + "noise1 dùng K_bt_pinlink sai không mở được, một lần thử bị tính vào bộ đếm chung"} + + +DESCRIPTION = ( + "CPACE-X25519-SHA512 (draft-irtf-cfrg-cpace-21, suite G_X25519 + SHA-512). kind \"official_generator\"/" + "\"official_message\": vector công bố của draft (Appendix B.1), chép nguyên văn qua cpace_source_values.py, " + "dùng để kiểm cài đặt calculate_generator/Ya/Yb/K/ISK đúng thuật toán trước khi áp case của HandLive. kind " + "\"handlive_pin\": ghép đôi bằng PIN qua Bluetooth không mạng (0.4.5 luồng C) — PRS = UTF-8 của 6 chữ số PIN " + "(giữ số 0 đứng đầu), CI = \"HLBTPIN1\" cố định, sid = n_s (32 byte, mới cho mỗi kênh), ADa = ADb rỗng; C " + "khởi tạo (cpace1 = Ya), S trả lời (cpace2 = Yb); K_bt_pinlink/K_bt_pin = HKDF-SHA256(ikm = ISK, salt = n_s, " + "info tương ứng, L 32). invalid_vectors: low_order_ya/low_order_yb (bên kia nhận một điểm bậc thấp của " + "Appendix B.1.10 → K = G.I → phải đóng kênh không trả lời, đúng \"an all-zero K → close without answer\" của " + "0.4.5), sid_mismatch (hai phía dùng n_s khác nhau → ISK khác nhau), wrong_pin (PIN gõ sai một phía → " + "generator khác nhau → ISK khác nhau hoặc K toàn 0) — cả hai trường hợp sau không bị CPace tự abort, chỉ lộ " + "ra khi noise1 (psk = K_bt_pinlink) không mở được ở bước ghép đôi tiếp theo (bt-pairing.json, chưa nối case " + "PIN vào — còn một việc để làm sau)." +) + + +def build() -> dict: + official = [_official_generator_vector(), _official_message_vector()] + handlive = [ + _handlive_pin_vector("HandLive PIN 482917", "482917"), + _handlive_pin_vector("HandLive PIN với số 0 đứng đầu", "007042"), + ] + negatives = [ + _negative_low_order("Ya bậc thấp (C gửi điểm bậc thấp)", "ya"), + _negative_low_order("Yb bậc thấp (S trả lời điểm bậc thấp)", "yb"), + _negative_sid_mismatch("sid khác nhau giữa C và S"), + _negative_wrong_pin("PIN sai ở một phía"), + ] + doc = {"description": DESCRIPTION, + "source": f"{DRAFT} Appendix B.1 (CPACE-X25519-SHA512); {SPEC} 0.4.5 \"PIN (PAIR-01 flow C)\", " + "\"Keys from ISK\"", + "vectors": official + handlive, "invalid_vectors": negatives} + return {"cpace.json": doc} diff --git a/tools/vectors/cpace_pin_derivations.py b/tools/vectors/cpace_pin_derivations.py new file mode 100644 index 0000000..6b33672 --- /dev/null +++ b/tools/vectors/cpace_pin_derivations.py @@ -0,0 +1,16 @@ +"""K_bt_pinlink/K_bt_pin dẫn từ ISK của CPace (00-common-specs.md 0.4.5 "PIN", "Keys from ISK") — dùng để SINH +vector. `salt` = `n_s` (cũng là `sid` của CPace), không phải salt rỗng như K_bt_pdisc/K_bt_plink.""" +from handlive_protocol_derivations import hkdf + +PINLINK_INFO = b"handlive/v1/bt-pin-link" +PIN_INFO = b"handlive/v1/bt-pin-pair" + + +def k_bt_pinlink(isk: bytes, n_s: bytes) -> bytes: + """K_bt_pinlink = HKDF-SHA256(ikm = ISK, salt = n_s, info = "handlive/v1/bt-pin-link", L = 32).""" + return hkdf(isk, PINLINK_INFO, 32, salt=n_s) + + +def k_bt_pin(isk: bytes, n_s: bytes) -> bytes: + """K_bt_pin = HKDF-SHA256(ikm = ISK, salt = n_s, info = "handlive/v1/bt-pin-pair", L = 32).""" + return hkdf(isk, PIN_INFO, 32, salt=n_s) diff --git a/tools/vectors/cpace_source_values.py b/tools/vectors/cpace_source_values.py new file mode 100644 index 0000000..793be30 --- /dev/null +++ b/tools/vectors/cpace_source_values.py @@ -0,0 +1,87 @@ +"""Giá trị test vector CPace chép nguyên văn từ draft-irtf-cfrg-cpace-21 (tải bản .txt chính thức +từ IETF datatracker, 2026-10-28), phần Appendix B.1 "Test vector for CPace using group X25519 and +hash SHA-512". Trích tự động bằng script (giải base64 JSON nhúng sẵn trong draft ở B.1.1.1, B.1.9, +B.1.10.1 — không gõ tay) và đối chiếu khớp với bản hex-dump và bản C initializer cùng phần, cả ba +đều khớp byte-exact. Không sửa tay các giá trị này; verify_vectors.py kiểm lại bằng thư viện độc lập. +""" + +# B.1.1: calculate_generator(SHA-512, PRS, CI, sid) — generator_string, băm, phần tử trường, generator g. +CPACE_X25519_SHA512_GENERATOR = { + "H": "SHA-512", + "H.s_in_bytes": 128, + "PRS": "50617373776f7264", + "ZPAD length": 109, + "DSI": "4350616365323535", + "CI": "0b415f696e69746961746f720b425f726573706f6e646572", + "sid": "7e4b4791d6a8ef019b936c79fb7f2c57", + "generator_string(G.DSI,PRS,CI,sid,H.s_in_bytes)": ( + "0843506163653235350850617373776f72646d0000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000180b415f696e69746961746f720b425f" + "726573706f6e646572107e4b4791d6a8ef019b936c79fb7f2c57" + ), + "hash generator string": "03998087bdb1a2617bbe25ef5a7c18cd4f84f902328701790958755ee4aed1d3", + "decoded field element of 255 bits": "03998087bdb1a2617bbe25ef5a7c18cd4f84f902328701790958755ee4aed153", + "generator g": "d04bf6d41f6a289632a2e929fa29bebd51092512a7829fdde7d314b62f05a73f", +} + +# B.1.2-B.1.7: message A/B, K, ISK (initiator/responder và song hành), session id phụ. +CPACE_X25519_SHA512_MESSAGE = { + "PRS": "50617373776f7264", + "CI": "0b415f696e69746961746f720b425f726573706f6e646572", + "sid": "7e4b4791d6a8ef019b936c79fb7f2c57", + "g": "d04bf6d41f6a289632a2e929fa29bebd51092512a7829fdde7d314b62f05a73f", + "ya": "21b4f4bd9e64ed355c3eb676a28ebedaf6d8f17bdc365995b319097153044080", + "ADa": "414461", + "Ya": "1d13c89278cdadd826f6d8d7f887701430f8380ddc17611cdd6dc989ce0c9f32", + "yb": "848b0779ff415f0af4ea14df9dd1d3c29ac41d836c7808896c4eba19c51ac40a", + "ADb": "414462", + "Yb": "248cccf6d5cdc3646f0ad593f9e6cef4e69d4945f8372e623512ecea32185623", + "K": "5b067effbdc0b2a0e1d907b21ebb25cfedb96a852179a847c37e43ee71322c6b", + "ISK_IR": ( + "6e19b875f7a561d6b3ca3dbb9ef42ac55de3e717881018204b8922b4d5e53bb2aa82c300bea7b65d2b671da71922ddf6" + "472301b79bc270adfa8bf413285f2263" + ), + "ISK_SY": ( + "eef745e2f6e7ae2b1a1e53da340e777167a07fe150436648c51fb199c11f3cbabfc683a2b48e1af5881940dc398d375c" + "95e6b4ae9948a45b8770de0656382be4" + ), + "sid_output_ir": ( + "cbc73f62589bbc96ab6a95ec2363df621e93bc3b0cea83ba6b9571d005fa8f5d2d08f7165622777fa484c02a9e6b20a8" + "4ee2dbebae8c53be757dcfc0eebdeb5f" + ), + "sid_output_oc": ( + "3a504e9c7f1f7fa7314861e2c487d13f28566f3043f0ca760d22c4911aca0dd8b1f12a7ad0862eb92d08a76120140412" + "ae6b8322e99d75cf1d20d8cfde2b40fe" + ), +} + +# B.1.10: G_X25519.scalar_mult_vfy trên điểm bậc thấp (low-order) — u0..ub với vô hướng s cố định; +# q0..qb là kết quả mong đợi (toàn 0 = phải abort). u0,u1,u2,u3,u4,u5,u7 PHẢI kích hoạt abort. +CPACE_X25519_SHA512_LOW_ORDER_SCALAR = "af46e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449aff" +CPACE_X25519_SHA512_LOW_ORDER_POINTS = [ + {"label": "u0", "u": "0000000000000000000000000000000000000000000000000000000000000000", + "q": "0000000000000000000000000000000000000000000000000000000000000000", "must_abort": True}, + {"label": "u1", "u": "0100000000000000000000000000000000000000000000000000000000000000", + "q": "0000000000000000000000000000000000000000000000000000000000000000", "must_abort": True}, + {"label": "u2", "u": "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + "q": "0000000000000000000000000000000000000000000000000000000000000000", "must_abort": True}, + {"label": "u3", "u": "e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b800", + "q": "0000000000000000000000000000000000000000000000000000000000000000", "must_abort": True}, + {"label": "u4", "u": "5f9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f1157", + "q": "0000000000000000000000000000000000000000000000000000000000000000", "must_abort": True}, + {"label": "u5", "u": "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + "q": "0000000000000000000000000000000000000000000000000000000000000000", "must_abort": True}, + {"label": "u6", "u": "daffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "q": "d8e2c776bbacd510d09fd9278b7edcd25fc5ae9adfba3b6e040e8d3b71b21806", "must_abort": False}, + {"label": "u7", "u": "eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + "q": "0000000000000000000000000000000000000000000000000000000000000000", "must_abort": True}, + {"label": "u8", "u": "dbffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "q": "c85c655ebe8be44ba9c0ffde69f2fe10194458d137f09bbff725ce58803cdb38", "must_abort": False}, + {"label": "u9", "u": "d9ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "q": "db64dafa9b8fdd136914e61461935fe92aa372cb056314e1231bc4ec12417456", "must_abort": False}, + {"label": "ua", "u": "cdeb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b880", + "q": "e062dcd5376d58297be2618c7498f55baa07d7e03184e8aada20bca28888bf7a", "must_abort": False}, + {"label": "ub", "u": "4c9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f11d7", + "q": "993c6ad11c4c29da9a56f7691fd0ff8d732e49de6250b6c2e80003ff4629a175", "must_abort": False}, +] diff --git a/tools/vectors/cpace_x25519_sha512.py b/tools/vectors/cpace_x25519_sha512.py new file mode 100644 index 0000000..5893f8c --- /dev/null +++ b/tools/vectors/cpace_x25519_sha512.py @@ -0,0 +1,113 @@ +"""CPACE-X25519-SHA512 (draft-irtf-cfrg-cpace-21, suite G_X25519 + SHA-512): calculate_generator (Elligator2, +Appendix A.5), message A/B, K, ISK khởi tạo/trả lời (Appendix A.3.4, section 7.2). HandLive dùng suite này cho +ghép đôi bằng PIN qua Bluetooth không mạng (00-common-specs.md 0.4.5, luồng C): C khởi tạo (gửi `cpace1`), S trả +lời (gửi `cpace2`), nên chỉ cần `transcript_ir`/`isk` (khởi tạo/trả lời), không cần biến thể song hành (`_oc`). + +Kiểm end-to-end khớp byte-exact với vector B.1 của draft (`cpace_source_values.py`); `verify_vectors.py` kiểm lại +bằng pynacl (độc lập với `cryptography` dùng ở đây). +""" +import hashlib + +from cryptography.hazmat.primitives.asymmetric import x25519 + +DSI = b"CPace255" +DSI_ISK = DSI + b"_ISK" +H_S_IN_BYTES = 128 # kích thước khối nhập của SHA-512 +FIELD_SIZE_BYTES = 32 +FIELD_SIZE_BITS = 255 +Q_25519 = 2 ** 255 - 19 +A_25519 = 486662 +Z_25519 = 2 # find_z_ell2(GF(2^255 - 19)) cho Curve25519 (Appendix A.5) + + +def prepend_len(data: bytes) -> bytes: + """LEB128 độ dài ‖ data (Appendix A.1.1).""" + length, encoded = len(data), bytearray() + while True: + encoded.append(length if length < 128 else (length & 0x7F) + 0x80) + length >>= 7 + if length == 0: + break + return bytes(encoded) + data + + +def lv_cat(*parts: bytes) -> bytes: + """"length-value": nối các chuỗi, mỗi chuỗi có độ dài LEB128 đặt trước (Appendix A.1.3).""" + return b"".join(prepend_len(p) for p in parts) + + +def generator_string(prs: bytes, ci: bytes, sid: bytes, s_in_bytes: int = H_S_IN_BYTES, dsi: bytes = DSI) -> bytes: + """Appendix A.2: lv_cat(DSI, PRS, zero_bytes(len_zpad), CI, sid) — đệm 0 để DSI‖PRS lấp đầy khối băm đầu + tiên, làm độ dài băm độc lập với độ dài PRS thật (chống side-channel qua độ dài).""" + len_zpad = max(0, s_in_bytes - 1 - len(prepend_len(prs)) - len(prepend_len(dsi))) + return lv_cat(dsi, prs, bytes(len_zpad), ci, sid) + + +def decode_u_coordinate(u: bytes, bits: int = FIELD_SIZE_BITS) -> int: + """RFC 7748 decodeUCoordinate (Appendix A.4): xoá bit không dùng của byte cuối (mảng little-endian).""" + u_list = bytearray(u) + if bits % 8: + u_list[-1] &= (1 << (bits % 8)) - 1 + return int.from_bytes(bytes(u_list), "little") + + +def _encode_u_coordinate(u: int, bits: int = FIELD_SIZE_BITS) -> bytes: + return u.to_bytes((bits + 7) // 8, "little") + + +def elligator2_x25519(r: int) -> bytes: + """Appendix A.5 (map_to_curve_elligator2), A = 486662 và Z = 2 của Curve25519 — chuyển một phần tử trường r + (nguyên, đã decode_u_coordinate) thành toạ độ u hợp lệ của một điểm trên đường cong.""" + q, a = Q_25519, A_25519 % Q_25519 + z = Z_25519 % q + power = (q - 1) // 2 + denom = (1 + z * r * r) % q + v = (-a * pow(denom, q - 2, q)) % q + rhs = (pow(v, 3, q) + a * pow(v, 2, q) + v) % q + epsilon = pow(rhs, power, q) + if epsilon == q - 1: + epsilon = -1 + x = (epsilon * v - (1 - epsilon) * a * pow(2, q - 2, q)) % q + return _encode_u_coordinate(x) + + +def calculate_generator(prs: bytes, ci: bytes, sid: bytes) -> bytes: + """G.calculate_generator(SHA-512, PRS, CI, sid) của G_X25519 (0.4.5: `g` = calculate_generator(...)).""" + gen_str_hash = hashlib.sha512(generator_string(prs, ci, sid)).digest()[:FIELD_SIZE_BYTES] + return elligator2_x25519(decode_u_coordinate(gen_str_hash)) + + +def scalar_mult_vfy(scalar: bytes, point: bytes) -> bytes | None: + """G_X25519.scalar_mult_vfy = X25519(scalar, point); None (G.I — phải abort) khi `point` là điểm bậc thấp. + `cryptography` lên tiếng bằng ValueError đúng khi và chỉ khi kết quả là phần tử trung hoà (đã đối chiếu với + 12 điểm bậc thấp của Appendix B.1.10, bất kể vô hướng), nên bắt ValueError đúng với "một K toàn 0 → đóng kênh + không trả lời" (0.4.5, luồng C).""" + try: + priv = x25519.X25519PrivateKey.from_private_bytes(scalar) + pub = x25519.X25519PublicKey.from_public_bytes(point) + return priv.exchange(pub) + except ValueError: + return None + + +def transcript_ir(ya_pub: bytes, ada: bytes, yb_pub: bytes, adb: bytes) -> bytes: + """Appendix A.3.4 — thứ tự khởi tạo/trả lời cố định; HandLive luôn có vai rõ (C khởi tạo, S trả lời).""" + return lv_cat(ya_pub, ada) + lv_cat(yb_pub, adb) + + +def isk(sid: bytes, k: bytes, ya_pub: bytes, ada: bytes, yb_pub: bytes, adb: bytes) -> bytes: + """ISK = SHA-512(lv_cat(DSI ‖ "_ISK", sid, K) ‖ transcript_ir(Ya,ADa,Yb,ADb)) (section 7.2).""" + return hashlib.sha512(lv_cat(DSI_ISK, sid, k) + transcript_ir(ya_pub, ada, yb_pub, adb)).digest() + + +def run_both_sides(prs: bytes, ci: bytes, sid: bytes, ya: bytes, ada: bytes, yb: bytes, adb: bytes) -> dict: + """Chạy cả hai vai với đầu vào cho trước (dùng để sinh vector): trả g, Ya, Yb, k_a (phía C), k_b (phía S), + isk_a, isk_b. `k_a`/`k_b`/`isk_a`/`isk_b` là None khi vai đó phải abort (điểm nhận được bậc thấp).""" + g = calculate_generator(prs, ci, sid) + ya_pub = scalar_mult_vfy(ya, g) + yb_pub = scalar_mult_vfy(yb, g) + k_a = scalar_mult_vfy(ya, yb_pub) if ya_pub is not None and yb_pub is not None else None + k_b = scalar_mult_vfy(yb, ya_pub) if ya_pub is not None and yb_pub is not None else None + isk_a = isk(sid, k_a, ya_pub, ada, yb_pub, adb) if k_a is not None else None + isk_b = isk(sid, k_b, ya_pub, ada, yb_pub, adb) if k_b is not None else None + return {"g": g, "Ya": ya_pub, "Yb": yb_pub, "k_a": k_a, "k_b": k_b, "isk_a": isk_a, "isk_b": isk_b} diff --git a/tools/vectors/generate_vectors.py b/tools/vectors/generate_vectors.py index cea7d86..6f82a29 100644 --- a/tools/vectors/generate_vectors.py +++ b/tools/vectors/generate_vectors.py @@ -10,7 +10,9 @@ sys.dont_write_bytecode = True # không để lại __pycache__ trong kho sys.path.insert(0, str(Path(__file__).resolve().parent)) +import build_bt_pairing_vectors # noqa: E402 import build_clipboard_html_vectors # noqa: E402 +import build_cpace_vectors # noqa: E402 import build_discovery_hint_vectors # noqa: E402 import build_identity_session_vectors # noqa: E402 import build_message_vectors # noqa: E402 @@ -33,7 +35,9 @@ def render_all() -> dict[str, str]: files.update(build_revoke_vectors.build()) files.update(build_pair_handshake_vectors.build(ctx)) files.update(build_discovery_hint_vectors.build(ctx)) + files.update(build_bt_pairing_vectors.build(ctx)) files.update(build_clipboard_html_vectors.build()) + files.update(build_cpace_vectors.build()) return {name: json.dumps(doc, indent=2, ensure_ascii=False) + "\n" for name, doc in sorted(files.items())} diff --git a/tools/vectors/noise_nnpsk0.py b/tools/vectors/noise_nnpsk0.py new file mode 100644 index 0000000..0f52de1 --- /dev/null +++ b/tools/vectors/noise_nnpsk0.py @@ -0,0 +1,73 @@ +"""Noise_NNpsk0_25519_ChaChaPoly_SHA256 (Noise Protocol Framework rev. 34), chỉ message 1 (token "psk, e") — +"khung đầu" (first frame) của một liên kết ghép đôi qua Bluetooth (00-common-specs.md 0.4.5 "Link-security +handshake" bước 2–3, "Pairing mode"). HASHLEN = 32 (SHA-256) nên temp_k/temp_k1/temp_k2 không bị cắt (quy tắc cắt +chỉ áp dụng khi HASHLEN = 64). + +Không phải vector công bố của tài liệu Noise — draft/tài liệu Noise không phát hành vector cho psk/prologue riêng +của HandLive. Đây là cách HandLive dùng đúng thuật toán của tài liệu đó (InitializeSymmetric, MixHash, +MixKeyAndHash, EncryptAndHash) cho bộ psk/prologue của mình, nên cần một vòng rà soát riêng (không tự xác nhận được +bằng vector bên ngoài) trước khi A7.1a/M7.1b khoá theo. +""" +import hashlib +import hmac as _hmac + +from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 + +from handlive_protocol_derivations import x25519_pub + +PROTOCOL_NAME = b"Noise_NNpsk0_25519_ChaChaPoly_SHA256" + + +def _hmac_sha256(key: bytes, msg: bytes) -> bytes: + return _hmac.new(key, msg, hashlib.sha256).digest() + + +def _sha256(data: bytes) -> bytes: + return hashlib.sha256(data).digest() + + +def initialize_symmetric(protocol_name: bytes = PROTOCOL_NAME) -> tuple[bytes, bytes]: + """h = ck = protocol_name nếu ≤ 32 byte, ngược lại SHA-256(protocol_name) (ở đây 37 byte > 32). Trả (h, ck).""" + h = protocol_name if len(protocol_name) <= 32 else _sha256(protocol_name) + return h, h + + +def mix_key_and_hash(ck: bytes, h: bytes, ikm: bytes) -> dict: + """Token "psk": HKDF nội bộ của Noise (3 ngõ ra qua HMAC) rồi MixHash(temp_h); InitializeKey(temp_k).""" + temp_key = _hmac_sha256(ck, ikm) + out1 = _hmac_sha256(temp_key, b"\x01") + out2 = _hmac_sha256(temp_key, out1 + b"\x02") + out3 = _hmac_sha256(temp_key, out2 + b"\x03") + return {"temp_key": temp_key, "ck": out1, "temp_h": out2, "temp_k": out3, "h": _sha256(h + out2)} + + +def first_message(psk: bytes, prologue: bytes, e_priv: bytes) -> dict: + """Dựng message 1 ("psk, e") của C→S. `message1` = e.pub (32) ‖ EncryptAndHash("") (16, chỉ có tag vì + plaintext rỗng).""" + h0, ck0 = initialize_symmetric() + h1 = _sha256(h0 + prologue) # MixHash(prologue) + psk_step = mix_key_and_hash(ck0, h1, psk) + h2, ck1, k = psk_step["h"], psk_step["ck"], psk_step["temp_k"] + e_pub = x25519_pub(e_priv) + h3 = _sha256(h2 + e_pub) # MixHash(e.pub); khóa công khai tạm thời không bao giờ được mã hóa + nonce = bytes(4) + (0).to_bytes(8, "little") # n = 0 (lần ENCRYPT đầu tiên của hướng này) + ciphertext = ChaCha20Poly1305(k).encrypt(nonce, b"", h3) # EncryptAndHash(""), ad = h3 + h4 = _sha256(h3 + ciphertext) # MixHash(ciphertext) + return {"protocol_name": PROTOCOL_NAME, "h0": h0, "prologue": prologue, "h1": h1, "psk": psk, + "mix_key_and_hash": psk_step, "h2": h2, "ck1": ck1, "k": k, "e_priv": e_priv, "e_pub": e_pub, + "h3": h3, "nonce": nonce, "ciphertext_tag": ciphertext, "h4": h4, "message1": e_pub + ciphertext} + + +def try_open_first_message(psk: bytes, prologue: bytes, e_pub: bytes, ciphertext_tag: bytes) -> bytes | None: + """Phía S: thử một psk ứng viên. Trả b"" nếu tag khớp (xác thực được "khung đầu"), None nếu không — như + `reject`, không lộ lý do.""" + h0, ck0 = initialize_symmetric() + h1 = _sha256(h0 + prologue) + psk_step = mix_key_and_hash(ck0, h1, psk) + h2, k = psk_step["h"], psk_step["temp_k"] + h3 = _sha256(h2 + e_pub) + nonce = bytes(4) + (0).to_bytes(8, "little") + try: + return ChaCha20Poly1305(k).decrypt(nonce, ciphertext_tag, h3) + except Exception: + return None diff --git a/tools/vectors/verify_bt_pairing_checks.py b/tools/vectors/verify_bt_pairing_checks.py new file mode 100644 index 0000000..3b4b388 --- /dev/null +++ b/tools/vectors/verify_bt_pairing_checks.py @@ -0,0 +1,116 @@ +"""Kiểm bt-pairing.json bằng thư viện độc lập: HKDF/HMAC qua verify_common (hashlib/hmac), X25519 qua +`x25519_base` (pynacl), ChaCha20Poly1305 kiểm chéo `cryptography` + libsodium ietf — độc lập với +noise_nnpsk0.py (phía sinh). +""" +import hashlib +import hmac + +from cryptography.exceptions import InvalidTag +from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 +from nacl.bindings import crypto_aead_chacha20poly1305_ietf_decrypt +from nacl.exceptions import CryptoError + +from verify_common import H, hkdf, hmac256, x25519_base + +PDISC_INFO = b"handlive/v1/bt-pair-disc" +PLINK_INFO = b"handlive/v1/bt-pair-link" +PR_BT_LABEL = b"HLBTPAIR1" +PROTOCOL_NAME = b"Noise_NNpsk0_25519_ChaChaPoly_SHA256" +PROLOGUE_LABEL = b"HLBT1" +REASONS = {"wrong_key", "tag_mismatch", "message_tampered", "aad_mismatch"} +ZERO_NONCE = bytes(12) + + +def _sha256(b: bytes) -> bytes: + return hashlib.sha256(b).digest() + + +def _hmac(key: bytes, msg: bytes) -> bytes: + return hmac.new(key, msg, hashlib.sha256).digest() + + +def _ck_h_k_after_psk(prologue: bytes, psk: bytes) -> tuple[bytes, bytes]: + """InitializeSymmetric → MixHash(prologue) → token "psk" (MixKeyAndHash). Trả (h, k) sau bước này.""" + h0 = _sha256(PROTOCOL_NAME) # 37 byte > HASHLEN 32 + h1 = _sha256(h0 + prologue) + temp_key = _hmac(h0, psk) + out1 = _hmac(temp_key, b"\x01") + out2 = _hmac(temp_key, out1 + b"\x02") + out3 = _hmac(temp_key, out2 + b"\x03") + return _sha256(h1 + out2), out3 + + +def _open(psk: bytes, prologue: bytes, e_pub: bytes, ciphertext_tag: bytes): + """Trả (mở bằng cryptography, mở bằng libsodium, h sau token "e") — None khi tag không khớp.""" + h, k = _ck_h_k_after_psk(prologue, psk) + h3 = _sha256(h + e_pub) + try: + a = ChaCha20Poly1305(k).decrypt(ZERO_NONCE, ciphertext_tag, h3) + except InvalidTag: + a = None + try: + b = crypto_aead_chacha20poly1305_ietf_decrypt(ciphertext_tag, h3, ZERO_NONCE, k) + except CryptoError: + b = None + return a, b, h3 + + +def _check_key(c, n: str, v: dict, pair_prk: dict) -> bytes: + pv = pair_prk[v["name"]] + c.eq(f"{n} pairing_secret lấy từ pair-prk.json", (v["pairing_secret_source"], v["pairing_secret"]), + ("pair-prk.json", pv["pairing_secret"])) + secret = H(v["pairing_secret"]) + pdisc = hkdf(secret, PDISC_INFO, 32) + plink = hkdf(secret, PLINK_INFO, 32) + c.eq(f"{n} K_bt_pdisc", v["k_bt_pdisc"], pdisc.hex()) + c.eq(f"{n} K_bt_plink", v["k_bt_plink"], plink.hex()) + mac = hmac256(pdisc, PR_BT_LABEL) + c.eq(f"{n} pr_bt_mac", v["pr_bt_mac"], mac.hex()) + c.eq(f"{n} pr_bt = 4 byte đầu của pr_bt_mac", v["pr_bt"], mac[:4].hex()) + return plink + + +def _check_first_frame(c, n: str, v: dict) -> None: + psk, prologue = H(v["psk"]), H(v["prologue"]) + c.eq(f"{n} prologue = nhãn ‖ n_s", prologue, PROLOGUE_LABEL + H(v["n_s"])) + c.eq(f"{n} e_pub = X25519 base point của e_priv", v["e_pub"], x25519_base(H(v["e_priv"])).hex()) + a, b, h3 = _open(psk, prologue, H(v["e_pub"]), H(v["ciphertext_tag"])) + c.eq(f"{n} h_after_e", h3.hex(), v["h_after_e"]) + c.eq(f"{n} mở được bằng cryptography (đúng psk)", a, b"") + c.eq(f"{n} mở được bằng libsodium (đúng psk)", b, b"") + c.eq(f"{n} message1 = e_pub ‖ ciphertext_tag", v["message1"], v["e_pub"] + v["ciphertext_tag"]) + rlen = 1 + len(H(v["message1"])) + c.eq(f"{n} rlen = 1 (kind) + độ dài message1", v["rlen"], rlen) + c.eq(f"{n} record = rlen(2 BE) ‖ kind(0x02) ‖ message1", H(v["record"]), + rlen.to_bytes(2, "big") + bytes([0x02]) + H(v["message1"])) + + +def _check_negative(c, n: str, v: dict, plink: dict) -> None: + reason = v["reason"] + c.true(f"{n} reason nằm trong tập đã định", reason in REASONS) + prologue = PROLOGUE_LABEL + H(v["n_s"]) + psk = H(v["wrong_psk"]) if reason == "wrong_key" else plink["cặp 1"] + a, b, _ = _open(psk, prologue, H(v["e_pub"]), H(v["ciphertext_tag"])) + c.eq(f"{n} phải bị từ chối (cả hai đường)", (a, b), (None, None)) + + +def check_bt_pairing(c, doc: dict, all_docs: dict) -> None: + pair_prk = {v["name"]: v for v in all_docs["pair-prk.json"]["vectors"]} + plink, kinds = {}, set() + for v in doc["vectors"]: + n = f"bt-pairing/{v['name']}" + kinds.add(v["kind"]) + if v["kind"] == "key": + plink[v["name"]] = _check_key(c, n, v, pair_prk) + elif v["kind"] == "first_frame": + _check_first_frame(c, n, v) + else: + c.true(f"{n} kind {v['kind']!r} không nằm trong tập đã định", False) + c.eq("bt-pairing: có cả hai kind", kinds, {"key", "first_frame"}) + c.eq("bt-pairing: mỗi cặp của pair-prk.json có một vector key", sorted(plink), sorted(pair_prk)) + for v in doc["invalid_vectors"]: + _check_negative(c, f"bt-pairing/{v['name']}", v, plink) + c.eq("bt-pairing: đủ loại vector âm", {v["reason"] for v in doc["invalid_vectors"]}, REASONS) + + +CHECKS = {"bt-pairing.json": check_bt_pairing} diff --git a/tools/vectors/verify_cpace_checks.py b/tools/vectors/verify_cpace_checks.py new file mode 100644 index 0000000..9bc8f5b --- /dev/null +++ b/tools/vectors/verify_cpace_checks.py @@ -0,0 +1,142 @@ +"""Kiểm cpace.json bằng thư viện độc lập. `calculate_generator`/`generator_string` (Elligator2) dùng lại +`cpace_x25519_sha512.py` vì không có thư viện khác cho map_to_curve_elligator2 — giống cách `hchacha20_reference.py` +dùng chung cho HChaCha20 (verify_common.py); đã đối chiếu byte-exact với Appendix B.1.1 của draft trong +`build_cpace_vectors.py`. X25519/scalar_mult_vfy ở đây qua pynacl (phía sinh dùng `cryptography`); lv_cat, +transcript, ISK, HKDF cài lại bằng hashlib/hmac, độc lập với `cpace_x25519_sha512.py` và +`handlive_protocol_derivations.py`. +""" +import hashlib + +from nacl.bindings import crypto_scalarmult +from nacl.exceptions import CryptoError + +from cpace_x25519_sha512 import calculate_generator +from verify_common import H, hkdf + +DSI_ISK = b"CPace255_ISK" +REASONS = {"low_order_ya", "low_order_yb", "sid_mismatch", "wrong_pin"} + + +def _prepend_len(data: bytes) -> bytes: + length, out = len(data), bytearray() + while True: + out.append(length if length < 128 else (length & 0x7F) + 0x80) + length >>= 7 + if length == 0: + break + return bytes(out) + data + + +def _lv_cat(*parts: bytes) -> bytes: + return b"".join(_prepend_len(p) for p in parts) + + +def _transcript_ir(ya: bytes, ada: bytes, yb: bytes, adb: bytes) -> bytes: + return _lv_cat(ya, ada) + _lv_cat(yb, adb) + + +def _isk(sid: bytes, k: bytes, ya: bytes, ada: bytes, yb: bytes, adb: bytes) -> bytes: + return hashlib.sha512(_lv_cat(DSI_ISK, sid, k) + _transcript_ir(ya, ada, yb, adb)).digest() + + +def _scalar_mult_vfy(scalar: bytes, point: bytes) -> bytes | None: + try: + return crypto_scalarmult(scalar, point) + except CryptoError: + return None + + +def _check_official_generator(c, n: str, v: dict) -> None: + g = calculate_generator(H(v["PRS"]), H(v["CI"]), H(v["sid"])) + c.eq(f"{n} g (Elligator2)", v["g"], g.hex()) + + +def _check_official_message(c, n: str, v: dict) -> None: + g = calculate_generator(H(v["PRS"]), H(v["CI"]), H(v["sid"])) + c.eq(f"{n} g", v["g"], g.hex()) + Ya = _scalar_mult_vfy(H(v["ya"]), g) + Yb = _scalar_mult_vfy(H(v["yb"]), g) + c.eq(f"{n} Ya = scalar_mult_vfy(ya, g)", v["Ya"], Ya.hex() if Ya else None) + c.eq(f"{n} Yb = scalar_mult_vfy(yb, g)", v["Yb"], Yb.hex() if Yb else None) + k_a = _scalar_mult_vfy(H(v["ya"]), Yb) + k_b = _scalar_mult_vfy(H(v["yb"]), Ya) + c.eq(f"{n} K khớp cả hai phía", (v["K"], v["K"]), (k_a.hex() if k_a else None, k_b.hex() if k_b else None)) + isk = _isk(H(v["sid"]), H(v["K"]), Ya, H(v["ADa"]), Yb, H(v["ADb"])) + c.eq(f"{n} ISK (khởi tạo/trả lời)", v["ISK_initiator_responder"], isk.hex()) + + +def _check_handlive_pin(c, n: str, v: dict) -> None: + prs, ci, n_s = v["pin"].encode("utf-8"), v["ci"].encode("utf-8"), H(v["n_s"]) + c.eq(f"{n} ci = HLBTPIN1 (0.4.5 luồng C)", v["ci"], "HLBTPIN1") + g = calculate_generator(prs, ci, n_s) + c.eq(f"{n} g", v["g"], g.hex()) + Ya = _scalar_mult_vfy(H(v["ya"]), g) + Yb = _scalar_mult_vfy(H(v["yb"]), g) + c.eq(f"{n} Ya", v["Ya"], Ya.hex() if Ya else None) + c.eq(f"{n} Yb", v["Yb"], Yb.hex() if Yb else None) + k_a = _scalar_mult_vfy(H(v["ya"]), Yb) + c.eq(f"{n} K", v["K"], k_a.hex() if k_a else None) + isk = _isk(n_s, H(v["K"]), Ya, b"", Yb, b"") + c.eq(f"{n} ISK", v["ISK"], isk.hex()) + c.eq(f"{n} K_bt_pinlink", v["k_bt_pinlink"], hkdf(isk, v["k_bt_pinlink_info"].encode(), 32, salt=n_s).hex()) + c.eq(f"{n} K_bt_pin", v["k_bt_pin"], hkdf(isk, v["k_bt_pin_info"].encode(), 32, salt=n_s).hex()) + + +def _check_negative(c, n: str, v: dict) -> None: + reason = v["reason"] + c.true(f"{n} reason nằm trong tập đã định", reason in REASONS) + if reason in ("low_order_ya", "low_order_yb"): + g = calculate_generator(v["pin"].encode("utf-8"), b"HLBTPIN1", H(v["n_s"])) + c.eq(f"{n} g", v["g"], g.hex()) + if reason == "low_order_ya": + k_s = _scalar_mult_vfy(H(v["yb"]), H(v["Ya_attacker"])) + else: + k_s = _scalar_mult_vfy(H(v["ya"]), H(v["Yb_attacker"])) + c.eq(f"{n} K phải là None (G.I, điểm bậc thấp)", k_s, None) + elif reason == "sid_mismatch": + g_c = calculate_generator(v["pin"].encode("utf-8"), b"HLBTPIN1", H(v["n_s_c"])) + g_s = calculate_generator(v["pin"].encode("utf-8"), b"HLBTPIN1", H(v["n_s_s"])) + Ya, Yb = _scalar_mult_vfy(H(v["ya"]), g_c), _scalar_mult_vfy(H(v["yb"]), g_s) + c.eq(f"{n} Ya", v["Ya"], Ya.hex() if Ya else None) + c.eq(f"{n} Yb", v["Yb"], Yb.hex() if Yb else None) + k_c, k_s = _scalar_mult_vfy(H(v["ya"]), Yb), _scalar_mult_vfy(H(v["yb"]), Ya) + isk_c = _isk(H(v["n_s_c"]), k_c, Ya, b"", Yb, b"") if k_c else None + isk_s = _isk(H(v["n_s_s"]), k_s, Ya, b"", Yb, b"") if k_s else None + c.eq(f"{n} isk_c", v["isk_c"], isk_c.hex() if isk_c else None) + c.eq(f"{n} isk_s", v["isk_s"], isk_s.hex() if isk_s else None) + c.true(f"{n} isk_c phải khác isk_s", isk_c != isk_s) + else: # wrong_pin + g_c = calculate_generator(v["pin_c"].encode("utf-8"), b"HLBTPIN1", H(v["n_s"])) + g_s = calculate_generator(v["pin_s"].encode("utf-8"), b"HLBTPIN1", H(v["n_s"])) + c.true(f"{n} generator hai phía phải khác (PIN khác)", g_c != g_s) + Ya, Yb = _scalar_mult_vfy(H(v["ya"]), g_c), _scalar_mult_vfy(H(v["yb"]), g_s) + c.eq(f"{n} Ya", v["Ya"], Ya.hex() if Ya else None) + c.eq(f"{n} Yb", v["Yb"], Yb.hex() if Yb else None) + k_c, k_s = _scalar_mult_vfy(H(v["ya"]), Yb), _scalar_mult_vfy(H(v["yb"]), Ya) + isk_c = _isk(H(v["n_s"]), k_c, Ya, b"", Yb, b"") if k_c else None + isk_s = _isk(H(v["n_s"]), k_s, Ya, b"", Yb, b"") if k_s else None + c.eq(f"{n} isk_c", v["isk_c"], isk_c.hex() if isk_c else None) + c.eq(f"{n} isk_s", v["isk_s"], isk_s.hex() if isk_s else None) + c.true(f"{n} isk_c phải khác isk_s (hoặc một phía abort)", isk_c is None or isk_s is None or isk_c != isk_s) + + +def check_cpace(c, doc: dict) -> None: + kinds = set() + for v in doc["vectors"]: + n = f"cpace/{v['name']}" + kinds.add(v["kind"]) + if v["kind"] == "official_generator": + _check_official_generator(c, n, v) + elif v["kind"] == "official_message": + _check_official_message(c, n, v) + elif v["kind"] == "handlive_pin": + _check_handlive_pin(c, n, v) + else: + c.true(f"{n} kind {v['kind']!r} không nằm trong tập đã định", False) + c.eq("cpace: có đủ loại vector", kinds, {"official_generator", "official_message", "handlive_pin"}) + for v in doc["invalid_vectors"]: + _check_negative(c, f"cpace/{v['name']}", v) + c.eq("cpace: đủ loại vector âm", {v["reason"] for v in doc["invalid_vectors"]}, REASONS) + + +CHECKS = {"cpace.json": check_cpace} diff --git a/tools/vectors/verify_vectors.py b/tools/vectors/verify_vectors.py index bb7232a..1ed08e4 100644 --- a/tools/vectors/verify_vectors.py +++ b/tools/vectors/verify_vectors.py @@ -11,7 +11,9 @@ sys.dont_write_bytecode = True # không để lại __pycache__ trong kho sys.path.insert(0, str(Path(__file__).resolve().parent)) +import verify_bt_pairing_checks # noqa: E402 import verify_clipboard_html_checks # noqa: E402 +import verify_cpace_checks # noqa: E402 import verify_discovery_hint_checks # noqa: E402 import verify_message_checks # noqa: E402 import verify_pair_handshake_checks # noqa: E402 @@ -31,7 +33,7 @@ def main() -> int: checks = {**verify_primitive_checks.CHECKS, **verify_session_checks.CHECKS, **verify_message_checks.CHECKS, **verify_signature_checks.CHECKS, **verify_pair_handshake_checks.CHECKS, **verify_discovery_hint_checks.CHECKS, **verify_push_relay_checks.CHECKS, - **verify_revoke_checks.CHECKS} + **verify_revoke_checks.CHECKS, **verify_bt_pairing_checks.CHECKS, **verify_cpace_checks.CHECKS} c = Checker() for name in checks: c.true(f"{name} tồn tại", name in docs) From 79fef6da324984cc3848b25ed81a786a26807a03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Xu=C3=A2n=20D=C5=A9ng?= Date: Wed, 28 Oct 2026 18:10:25 +0700 Subject: [PATCH 4/7] feat(shared): add Bluetooth link/frame/advertisement vectors for S7.1a bt-link.json: K_bt_disc/K_bt_link from PRK, the rotating discovery hint, and the full link-security handshake (challenge, noise1, noise2, Split) for an already-paired Bluetooth link (00-common-specs.md 0.4.5), extending noise_nnpsk0.py with message 2 ("e, ee") and Split(). bt-frame.json: the pre-auth and transport record codec (rlen bounds, chan/flags/dlen/padding, encrypted with bt-link.json's c1), with negative vectors for every bound in 0.4.5/S7.1a: rlen out of bounds, an oversized pre-auth record, an unknown chan, more without long, a long-lane fragment over BT_BULK_FRAGMENT_MAX, a message over BT_MESSAGE_MAX. bt-advertisement.json: the app-controlled scan-response service data (ver/psm/hints) for 0/1/2 hints and the BT_PIN_HINT marker, with negative vectors for a wrong version and a hint outside the accepted epoch window. Also: sync error.schema.json/envelope.schema.json with the Phase 7 additions to 00-common-specs.md 0.8.1/0.7.1 (DIRECT_WIFI_UNAVAILABLE, bulk/direct); pin noiseprotocol==0.3.1 and cross-check the bt-link.json handshake against it (needs a real pip install to run); add bench events bt_seen/bt_lost/wifi_assoc and their reconnect_time.py trigger; add the Bluetooth permission/status UI strings (01-setup-settings.md SET-01/SET-03). --- schemas/envelope.schema.json | 6 +- schemas/error.schema.json | 5 +- strings/ui-strings.json | 65 +++++++ test-vectors/README.md | 41 ++++ test-vectors/README.vi.md | 41 ++++ test-vectors/bt-advertisement.json | 99 ++++++++++ test-vectors/bt-frame.json | 140 ++++++++++++++ test-vectors/bt-link.json | 139 ++++++++++++++ tools/bench/README.md | 8 + tools/bench/README.vi.md | 8 + tools/bench/bench_log.py | 5 + tools/bench/reconnect_time.py | 17 +- tools/bench/self_test.py | 23 ++- tools/vectors/bt_frame_codec.py | 46 +++++ tools/vectors/bt_link_derivations.py | 24 +++ .../vectors/build_bt_advertisement_vectors.py | 85 +++++++++ tools/vectors/build_bt_frame_vectors.py | 136 +++++++++++++ tools/vectors/build_bt_link_vectors.py | 137 ++++++++++++++ tools/vectors/generate_vectors.py | 6 + tools/vectors/noise_nnpsk0.py | 62 +++++- tools/vectors/requirements.txt | 1 + .../vectors/verify_bt_advertisement_checks.py | 79 ++++++++ tools/vectors/verify_bt_frame_checks.py | 107 +++++++++++ tools/vectors/verify_bt_link_checks.py | 178 ++++++++++++++++++ tools/vectors/verify_noise_library_checks.py | 48 +++++ tools/vectors/verify_vectors.py | 18 +- 26 files changed, 1503 insertions(+), 21 deletions(-) create mode 100644 test-vectors/bt-advertisement.json create mode 100644 test-vectors/bt-frame.json create mode 100644 test-vectors/bt-link.json create mode 100644 tools/vectors/bt_frame_codec.py create mode 100644 tools/vectors/bt_link_derivations.py create mode 100644 tools/vectors/build_bt_advertisement_vectors.py create mode 100644 tools/vectors/build_bt_frame_vectors.py create mode 100644 tools/vectors/build_bt_link_vectors.py create mode 100644 tools/vectors/verify_bt_advertisement_checks.py create mode 100644 tools/vectors/verify_bt_frame_checks.py create mode 100644 tools/vectors/verify_bt_link_checks.py create mode 100644 tools/vectors/verify_noise_library_checks.py diff --git a/schemas/envelope.schema.json b/schemas/envelope.schema.json index a981520..cf04a75 100644 --- a/schemas/envelope.schema.json +++ b/schemas/envelope.schema.json @@ -27,7 +27,7 @@ }, "$defs": { "type": { - "description": "Nhóm tin (0.7.1): tập đã chốt cộng session, camera, web.", + "description": "Nhóm tin (0.7.1): tập đã chốt cộng session, camera, web, direct, bulk (Phase 7, 0.4.6/0.4.7).", "enum": [ "clipboard", "sms", @@ -39,7 +39,9 @@ "capability", "session", "camera", - "web" + "web", + "direct", + "bulk" ] } } diff --git a/schemas/error.schema.json b/schemas/error.schema.json index a294faf..27567ac 100644 --- a/schemas/error.schema.json +++ b/schemas/error.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://handlive.app/schemas/v1/error.schema.json", "title": "HandLive — đối tượng lỗi trong ack", - "description": "00-common-specs 0.5.1 (ack lỗi) và 0.8.1 (mã lỗi ứng dụng). Mã lỗi relay HTTP (0.8.2) không thuộc schema này.", + "description": "00-common-specs 0.5.1 (ack lỗi) và 0.8.1 (mã lỗi ứng dụng, đủ cả Phase 7 DIRECT_WIFI_UNAVAILABLE). Mã lỗi relay HTTP (0.8.2) không thuộc schema này.", "type": "object", "required": ["code", "message"], "additionalProperties": false, @@ -70,7 +70,8 @@ "USB_ADB_UNAUTHORIZED", "USB_ADB_UNAVAILABLE", "MAC_EXTENSION_NOT_ACTIVE", - "MAC_MIC_DRIVER_MISSING" + "MAC_MIC_DRIVER_MISSING", + "DIRECT_WIFI_UNAVAILABLE" ] } } diff --git a/strings/ui-strings.json b/strings/ui-strings.json index 6165f89..38fac22 100644 --- a/strings/ui-strings.json +++ b/strings/ui-strings.json @@ -940,6 +940,14 @@ "platforms": ["android", "macos", "ios"], "specs": ["SET-02", "SET-01"] }, + { + "key": "common.open_bluetooth_settings", + "en": "Open Bluetooth Settings", + "vi": "Mở cài đặt Bluetooth", + "comment": "Button next to the Bluetooth-off status line, opens x-apple.systempreferences:com.apple.BluetoothSettings (SET-03 field 18)", + "platforms": ["macos"], + "specs": ["SET-03"] + }, { "key": "common.open_settings", "en": "Open Settings", @@ -1360,6 +1368,15 @@ "platforms": ["macos"], "specs": ["WEB-03"] }, + { + "key": "infoplist.bluetooth_always", + "plist_key": "NSBluetoothAlwaysUsageDescription", + "en": "HandLive connects to your Android phone over Bluetooth when they aren't on the same Wi-Fi network.", + "vi": "HandLive kết nối với điện thoại Android qua Bluetooth khi hai máy không cùng mạng Wi-Fi.", + "comment": "Purpose string of the Bluetooth permission, asked during setup (SET-03 field 17); shared with AUDIO-01, whose wording must cover call audio again when Phase 4 reopens", + "platforms": ["macos"], + "specs": ["SET-03"] + }, { "key": "infoplist.focus_status_usage", "plist_key": "NSFocusStatusUsageDescription", @@ -1921,6 +1938,30 @@ "platforms": ["android"], "specs": ["SET-01"] }, + { + "key": "permission.bluetooth_body", + "en": "When your phone and Mac aren't on the same Wi-Fi network, HandLive connects them over Bluetooth. Android calls this permission Nearby devices. HandLive doesn't use it to find your location.", + "vi": "Khi điện thoại và Mac không cùng mạng Wi-Fi, HandLive nối hai máy qua Bluetooth. Android gọi quyền này là Thiết bị ở gần. HandLive không dùng nó để biết vị trí của bạn.", + "comment": "Body of the Android PermissionPrimer before BLUETOOTH_ADVERTISE/BLUETOOTH_CONNECT (Nearby devices); title permission.bluetooth_title (SET-01 field 12)", + "platforms": ["android"], + "specs": ["SET-01"] + }, + { + "key": "permission.bluetooth_body_mac", + "en": "When your Mac and phone aren't on the same Wi-Fi network, HandLive connects them over Bluetooth. macOS asks for permission next.", + "vi": "Khi Mac và điện thoại không cùng mạng Wi-Fi, HandLive nối hai máy qua Bluetooth. Tiếp theo macOS sẽ hỏi quyền.", + "comment": "Body of the Mac primer before CBCentralManager triggers the system Bluetooth dialog; title permission.bluetooth_title (SET-03 field 17, step B1)", + "platforms": ["macos"], + "specs": ["SET-03"] + }, + { + "key": "permission.bluetooth_title", + "en": "Connect Without Wi-Fi", + "vi": "Kết nối không cần Wi-Fi", + "comment": "Title of the Bluetooth permission primer, shared by the Android PermissionPrimer (body permission.bluetooth_body, SET-01 field 12) and the Mac primer before the system dialog (body permission.bluetooth_body_mac, SET-03 field 17)", + "platforms": ["android", "macos"], + "specs": ["SET-01", "SET-03"] + }, { "key": "permission.calls_primer", "en": "To announce incoming calls and let you answer or decline them on your Mac, HandLive needs to read the phone state, the call log, and your contacts.", @@ -2210,6 +2251,14 @@ "platforms": ["android"], "specs": ["SET-02"] }, + { + "key": "settings.bluetooth_link", + "en": "Connect Without Wi-Fi", + "vi": "Kết nối không cần Wi-Fi", + "comment": "Feature card label for link.bluetooth in the feature list (SET-01 field 10): status ready/needs_permission/permanently_denied/off", + "platforms": ["android"], + "specs": ["SET-01"] + }, { "key": "settings.call_app_calls", "en": "Calls from Other Apps", @@ -3252,6 +3301,22 @@ "platforms": ["macos", "ios"], "specs": ["SMS-05"] }, + { + "key": "status.bluetooth_denied", + "en": "HandLive can't use Bluetooth, so it can't connect to the phone without Wi-Fi. Turn on HandLive in System Settings › Privacy & Security › Bluetooth.", + "vi": "HandLive không dùng được Bluetooth nên không kết nối được với điện thoại khi không có Wi-Fi. Bật HandLive trong Cài đặt hệ thống › Quyền riêng tư & Bảo mật › Bluetooth.", + "comment": "Mac Bluetooth-unavailable status line when access is denied or restricted (SET-03 E8); shown with common.open_system_settings", + "platforms": ["macos"], + "specs": ["SET-03", "CONN-05"] + }, + { + "key": "status.bluetooth_off", + "en": "Bluetooth is off. Turn it on to connect without Wi-Fi.", + "vi": "Bluetooth đang tắt. Bật Bluetooth để kết nối khi không có Wi-Fi.", + "comment": "Mac Bluetooth-unavailable status line, shown under the status line while there is no LAN session (SET-03 field 18); shown with common.open_bluetooth_settings", + "platforms": ["macos"], + "specs": ["SET-03", "CONN-05"] + }, { "key": "status.channel_internet", "en": "Internet", diff --git a/test-vectors/README.md b/test-vectors/README.md index db48f69..1aaf31a 100644 --- a/test-vectors/README.md +++ b/test-vectors/README.md @@ -117,6 +117,47 @@ bit of `ciphertext_tag` flipped), `message_tampered` (a bit of `e_pub` flipped i same `message1` replayed under a different `n_s`, so a different `prologue` — "the prologue binds `n_s`, so a recorded `noise1` never verifies on a new channel"). +### `bt-link.json` +`K_bt_disc`/`K_bt_link` (00-common-specs.md 0.4.5 "Keys"), chained from the `PRK` of `pair-prk.json` — unlike +`K_bt_pdisc`/`K_bt_plink` of `bt-pairing.json` (live only for one QR), these live for the life of the pair and +are used for an ordinary Bluetooth link to an already-paired phone. `kind` = `key`: `pair_id`, `prk_source`, +`prk`, `k_bt_disc_info`, `k_bt_disc`, `k_bt_link_info`, `k_bt_link`. `kind` = `hint`: the rotating advertising +hint (not the pairing one) = the first 4 bytes of HMAC-SHA256(`K_bt_disc`, `"HLBTDISC1"` ‖ epoch int64 BE), +`epoch` = `floor(now_ms / BT_HINT_PERIOD)`, shown at a few epochs. `kind` = `handshake`: the full link-security +handshake (0.4.5 "Link-security handshake" steps 1–4), `psk` = `k_bt_link`: `challenge_record` (kind `0x01`, +`n_s`), then message 1 (`"psk, e"`, `noise1_record`, kind `0x02`) and message 2 (`"e, ee"`, `noise2_record`, kind +`0x03`) of `Noise_NNpsk0_25519_ChaChaPoly_SHA256` (Noise Protocol Framework rev. 34), `prologue` = `"HLBT1"` ‖ +`n_s`, every intermediate symmetric-state value recorded for both messages, then `Split()` into `c1` (C encrypts, +S decrypts) and `c2` (the other way) — the two CipherStates of steps 4–5. Not a vector published by the Noise +specification (see the note under `bt-pairing.json`); message 1 was reviewed in #3328, message 2/`Split()` are +new and need their own review. `invalid_vectors`: `wrong_key`, `tag_mismatch_noise1`, `tag_mismatch_noise2` (0.4.5 +step 4: "C verifies its payload tag — failure → C closes the channel, CONN-05 E3"), `replayed_channel` (0.4.5 +step 3: "The prologue binds `n_s`, so a recorded `noise1` never verifies on a new channel"). + +### `bt-frame.json` +Transport record framing after link-security step 4 (00-common-specs.md 0.4.5 "Link records", "Transport records +and channels"), encrypted with `c1` of `bt-link.json` (Noise transport message, nonce = 4 zero bytes ‖ a 64-bit +little-endian counter, empty AAD). `kind` = `transport_frame`: plaintext `chan` (`0x01` control, `0x02` bulk) ‖ +`flags` (bit0 `more`, bit1 `long`) ‖ `dlen` (uint16 BE) ‖ `data` ‖ zero padding up to 256/1,024/4,096 bytes, then +`ciphertext_tag` and `record` = `rlen` (uint16 BE, one of 272/1,040/4,112) ‖ `ciphertext_tag`. Covers a short +control fragment, a 2-fragment long-control message, and a bulk fragment. `kind` = `preauth_frame`: the +`challenge`/`reject` wire bytes (`rlen` ‖ kind ‖ body) as a packaging example. `invalid_vectors` match the five +named in S7.1a plus one more from "Link records": `rlen_out_of_bounds` (not 272/1,040/4,112 → close before +allocating more), `preauth_record_over_bound` (over `BT_PREAUTH_RECORD_MAX` = 64 bytes), `unknown_chan` (neither +`0x01` nor `0x02`), `more_without_long` (`more` = 1 on the short lane, `long` = 0), `bulk_fragment_over_max` (a +long-lane or bulk fragment's `dlen` over `BT_BULK_FRAGMENT_MAX` = 1,020), `message_over_max` (a long-lane +message's running total over `BT_MESSAGE_MAX` = 256 KiB before its last fragment). + +### `bt-advertisement.json` +The service-data bytes the app itself controls inside the scan response (00-common-specs.md 0.4.5 +"Advertisement"): `ver` (always `0x01`) ‖ `psm` (uint16 LE) ‖ `hints` (4 bytes each, 0…`BT_ADV_MAX_HINTS` = 2); +the OS adds the other 18 bytes of the AD structure (flags, 128-bit UUID) to reach the 21/25/29-byte scan response +total of the 0.4.5 table — out of scope here, only recorded as `scan_response_total` for cross-checking. `kind` = +`scan_response`: 0, 1 and 2 hints (chained from `bt-link.json`'s `K_bt_disc`). `kind` = `pin_window`: `hints` = +only `BT_PIN_HINT` (`0x00000000`), the same for every phone while a PIN pairing window is open (AC9). +`invalid_vectors`: `wrong_version` (`ver` ≠ `0x01` → the client ignores the whole advertisement), +`hint_outside_window` (a hint computed at an epoch outside the client's accepted `[e-4, e+4]` window → no match). + ### `cpace.json` CPACE-X25519-SHA512 (draft-irtf-cfrg-cpace-21, suite G_X25519 + SHA-512), used for PIN pairing over Bluetooth with no network (00-common-specs.md 0.4.5, PAIR-01 flow C). `kind` = `official_generator`: the draft's own diff --git a/test-vectors/README.vi.md b/test-vectors/README.vi.md index a16f640..761fece 100644 --- a/test-vectors/README.vi.md +++ b/test-vectors/README.vi.md @@ -117,6 +117,47 @@ psk ứng viên của S **không** được mở tag: `wrong_key` (thử `k_bt_p đường truyền), `aad_mismatch` (phát lại đúng `message1` dưới `n_s` khác, nên `prologue` khác — "prologue buộc `n_s`, nên một `noise1` ghi lại không bao giờ xác thực trên kênh mới"). +### `bt-link.json` +`K_bt_disc`/`K_bt_link` (00-common-specs.md 0.4.5 "Keys"), nối từ `PRK` của `pair-prk.json` — khác +`K_bt_pdisc`/`K_bt_plink` của `bt-pairing.json` (chỉ sống theo một QR), hai khóa này sống suốt đời cặp và dùng +cho liên kết Bluetooth bình thường tới một điện thoại đã ghép đôi. `kind` = `key`: `pair_id`, `prk_source`, +`prk`, `k_bt_disc_info`, `k_bt_disc`, `k_bt_link_info`, `k_bt_link`. `kind` = `hint`: hint quảng bá xoay vòng +(khác hint lúc ghép đôi) = 4 byte đầu của HMAC-SHA256(`K_bt_disc`, `"HLBTDISC1"` ‖ epoch int64 BE), `epoch` = +`floor(now_ms / BT_HINT_PERIOD)`, cho vài kỷ nguyên ví dụ. `kind` = `handshake`: bắt tay bảo mật liên kết đầy đủ +(0.4.5 "Link-security handshake" bước 1–4), `psk` = `k_bt_link`: `challenge_record` (kind `0x01`, `n_s`), rồi +message 1 (`"psk, e"`, `noise1_record`, kind `0x02`) và message 2 (`"e, ee"`, `noise2_record`, kind `0x03`) của +`Noise_NNpsk0_25519_ChaChaPoly_SHA256` (Noise Protocol Framework rev. 34), `prologue` = `"HLBT1"` ‖ `n_s`, mọi +giá trị trung gian của trạng thái đối xứng được ghi lại cho cả hai message, rồi `Split()` ra `c1` (C mã hoá/S +giải mã) và `c2` (ngược lại) — hai CipherState của bước 4-5. Không phải vector do tài liệu Noise công bố (xem +ghi chú ở `bt-pairing.json`); message 1 đã được soát ở #3328, message 2/`Split()` là phần mới, cần soát riêng. +`invalid_vectors`: `wrong_key`, `tag_mismatch_noise1`, `tag_mismatch_noise2` (0.4.5 bước 4: "C verifies its +payload tag — failure → C closes the channel, CONN-05 E3"), `replayed_channel` (0.4.5 bước 3: "prologue buộc +`n_s`, nên một `noise1` ghi lại không bao giờ xác thực trên kênh mới"). + +### `bt-frame.json` +Đóng khung bản ghi transport sau bước 4 của bắt tay bảo mật liên kết (00-common-specs.md 0.4.5 "Link records", +"Transport records and channels"), mã hoá bằng `c1` của `bt-link.json` (Noise transport message, nonce = 4 byte +0 ‖ bộ đếm 64-bit little-endian, AAD rỗng). `kind` = `transport_frame`: bản rõ `chan` (`0x01` control, `0x02` +bulk) ‖ `flags` (bit0 `more`, bit1 `long`) ‖ `dlen` (uint16 BE) ‖ `data` ‖ đệm 0 tới 256/1.024/4.096 byte, rồi +`ciphertext_tag` và `record` = `rlen` (uint16 BE, một trong 272/1.040/4.112) ‖ `ciphertext_tag`. Có một mảnh làn +ngắn, một tin làn dài 2 mảnh, và một mảnh bulk. `kind` = `preauth_frame`: byte wire của `challenge`/`reject` +(`rlen` ‖ kind ‖ body) làm ví dụ đóng gói. `invalid_vectors` khớp đúng 5 luật nêu trong S7.1a cộng thêm 1 từ +"Link records": `rlen_out_of_bounds` (không thuộc 272/1.040/4.112 → đóng kênh trước khi cấp phát đọc thêm), +`preauth_record_over_bound` (vượt `BT_PREAUTH_RECORD_MAX` = 64 byte), `unknown_chan` (khác cả `0x01` và `0x02`), +`more_without_long` (`more` = 1 trên làn ngắn, `long` = 0), `bulk_fragment_over_max` (`dlen` một mảnh làn +dài/bulk vượt `BT_BULK_FRAGMENT_MAX` = 1.020), `message_over_max` (tổng byte một tin làn dài vượt +`BT_MESSAGE_MAX` = 256 KiB trước mảnh cuối). + +### `bt-advertisement.json` +Byte service data app tự kiểm soát trong scan response (00-common-specs.md 0.4.5 "Advertisement"): `ver` (luôn +`0x01`) ‖ `psm` (uint16 LE) ‖ `hints` (4 byte mỗi hint, 0…`BT_ADV_MAX_HINTS` = 2); hệ điều hành thêm 18 byte còn +lại của cấu trúc AD (cờ, UUID 128-bit) để ra tổng scan response 21/25/29 byte của bảng 0.4.5 — không thuộc +phạm vi ở đây, chỉ ghi `scan_response_total` để đối chiếu. `kind` = `scan_response`: 0, 1 và 2 hint (nối từ +`K_bt_disc` của `bt-link.json`). `kind` = `pin_window`: `hints` = chỉ `BT_PIN_HINT` (`0x00000000`), giống nhau +cho mọi điện thoại khi cửa sổ ghép đôi PIN đang mở (AC9). `invalid_vectors`: `wrong_version` (`ver` ≠ `0x01` → +client bỏ qua cả advertisement), `hint_outside_window` (hint tính ở kỷ nguyên ngoài cửa sổ chấp nhận `[e-4, e+4]` +của client → không khớp). + ### `cpace.json` CPACE-X25519-SHA512 (draft-irtf-cfrg-cpace-21, suite G_X25519 + SHA-512), dùng cho ghép đôi bằng PIN qua Bluetooth không mạng (00-common-specs.md 0.4.5, PAIR-01 luồng C). `kind` = `official_generator`: vector diff --git a/test-vectors/bt-advertisement.json b/test-vectors/bt-advertisement.json new file mode 100644 index 0000000..dedb90a --- /dev/null +++ b/test-vectors/bt-advertisement.json @@ -0,0 +1,99 @@ +{ + "description": "Byte service data app tự kiểm soát trong scan response (0.4.5 \"Advertisement\"): `ver` (1, luôn 0x01) ‖ `psm` (2, uint16 LE, PSM L2CAP hiện tại) ‖ `hints` (4 × n, n = 0…BT_ADV_MAX_HINTS = 2, mỗi hint = 4 byte đầu của HMAC-SHA256(K_bt_disc, \"HLBTDISC1\" ‖ epoch int64 BE)) — K_bt_disc/hint lấy từ bt-link.json. OS thêm 18 byte cấu trúc AD (cờ + UUID 128-bit) để ra tổng scan response (21/25/29 byte cho n=0/1/2) — không thuộc phạm vi vector này, chỉ ghi `scan_response_total` để đối chiếu bảng 0.4.5. `kind` = \"pin_window\": `hints` = chỉ `BT_PIN_HINT` (0x00000000), đánh dấu cửa sổ ghép đôi PIN đang mở, giống nhau cho mọi điện thoại (AC9). invalid_vectors: wrong_version (ver ≠ 0x01 → bỏ qua cả advertisement), hint_outside_window (hint tính ở kỷ nguyên ngoài [e-4, e+4] của client → không khớp).", + "source": "docs/detailed-design/00-common-specs.md 0.4.5 \"Advertisement\"; K_bt_disc của bt-link.json", + "vectors": [ + { + "name": "0 hint (không cặp nào có session nào, không có cửa sổ QR)", + "kind": "scan_response", + "ver": "0x01", + "psm": 23456, + "psm_hex": "a05b", + "hints": [], + "hint_pairs": [], + "service_data_bytes": "01a05b", + "service_data_len": 3, + "ad_uuid_struct_bytes": 18, + "scan_response_total": 21, + "note": "service_data_bytes = phần app tự kiểm soát; OS thêm 18 byte UUID/cờ nữa mới ra tổng 21 byte" + }, + { + "name": "1 hint (một cặp gần đây nhất)", + "kind": "scan_response", + "ver": "0x01", + "psm": 23456, + "psm_hex": "a05b", + "hints": [ + "be52c088" + ], + "hint_pairs": [ + "cặp 1" + ], + "service_data_bytes": "01a05bbe52c088", + "service_data_len": 7, + "ad_uuid_struct_bytes": 18, + "scan_response_total": 25, + "note": "service_data_bytes = phần app tự kiểm soát; OS thêm 18 byte UUID/cờ nữa mới ra tổng 25 byte" + }, + { + "name": "2 hint (BT_ADV_MAX_HINTS, hai cặp gần đây nhất)", + "kind": "scan_response", + "ver": "0x01", + "psm": 23456, + "psm_hex": "a05b", + "hints": [ + "be52c088", + "cadcf56f" + ], + "hint_pairs": [ + "cặp 1", + "cặp 2" + ], + "service_data_bytes": "01a05bbe52c088cadcf56f", + "service_data_len": 11, + "ad_uuid_struct_bytes": 18, + "scan_response_total": 29, + "note": "service_data_bytes = phần app tự kiểm soát; OS thêm 18 byte UUID/cờ nữa mới ra tổng 29 byte" + }, + { + "name": "cửa sổ PIN đang mở (BT_PIN_HINT)", + "kind": "pin_window", + "ver": "0x01", + "psm": 23456, + "psm_hex": "a05b", + "hints": [ + "00000000" + ], + "service_data_bytes": "01a05b00000000", + "service_data_len": 7, + "note": "BT_PIN_HINT giống nhau cho mọi điện thoại, không gắn với cặp hay thiết bị nào (AC9)" + } + ], + "invalid_vectors": [ + { + "name": "ver khác 0x01", + "reason": "wrong_version", + "service_data_bytes": "02a05bf7e5a436", + "expect": "ver = 0x02 ≠ 0x01 → client bỏ qua toàn bộ advertisement, không xét hint" + }, + { + "name": "hint ngoài cửa sổ e-4..e+4", + "reason": "hint_outside_window", + "client_epoch": 1000, + "accepted_epochs": [ + 996, + 997, + 998, + 999, + 1000, + 1001, + 1002, + 1003, + 1004 + ], + "advertised_epoch": 1005, + "advertised_hint": "b5e2508e", + "service_data_bytes": "01a05bb5e2508e", + "expect": "advertised_epoch = 1005 cách client_epoch 5 kỷ nguyên, ngoài [e-4, e+4] → client không thấy khớp hint nào" + } + ] +} diff --git a/test-vectors/bt-frame.json b/test-vectors/bt-frame.json new file mode 100644 index 0000000..9ea36f4 --- /dev/null +++ b/test-vectors/bt-frame.json @@ -0,0 +1,140 @@ +{ + "description": "Khung bản ghi pre-auth (`rlen` uint16 BE ‖ `rlen` byte, kind đầu tiên của thân — 0.4.5 \"Link records\") và khung control/bulk sau bước 4 (chan(1) ‖ flags(1, bit0 more/bit1 long) ‖ dlen(2 BE) ‖ data ‖ 0-padding tới 256/1 024/4 096 byte, mã hoá Noise transport = ChaCha20-Poly1305(key, nonce = 4 zero ‖ bộ đếm 64-bit LE, AAD rỗng) ‖ tag 16 byte — rlen hợp lệ duy nhất 272/1 040/4 112). `key_source` = `c1` của bt-link.json (hướng C→S của cùng bắt tay). invalid_vectors khớp đúng 6 luật cận của 0.4.5/S7.1a: rlen_out_of_bounds (rlen không thuộc {272,1040,4112} → đóng kênh trước khi cấp phát đọc thêm), preauth_record_over_bound (bản ghi pre-auth vượt BT_PREAUTH_RECORD_MAX = 64 byte), unknown_chan (chan khác 0x01/0x02), more_without_long (more=1 trên làn ngắn, long=0), bulk_fragment_over_max (dlen một mảnh làn dài/bulk vượt BT_BULK_FRAGMENT_MAX = 1 020), message_over_max (tổng byte một tin làn dài vượt BT_MESSAGE_MAX = 256 KiB trước mảnh cuối).", + "source": "docs/detailed-design/00-common-specs.md 0.4.5 \"Link records\", \"Transport records and channels\"; c1 của bt-link.json", + "vectors": [ + { + "name": "control / làn ngắn (long=0, 1 mảnh)", + "kind": "transport_frame", + "chan": "0x01", + "more": false, + "long": false, + "dlen": 40, + "data": "f1f7f408b0ce73fc47a3a5d91a793ffa5d4bd3a431c306e4a88f72189846bf015287781f95f97a29", + "plaintext": "01000028f1f7f408b0ce73fc47a3a5d91a793ffa5d4bd3a431c306e4a88f72189846bf015287781f95f97a290000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "padded_size": 256, + "key_source": "c1 của bt-link.json", + "counter": 10, + "nonce": "000000000a00000000000000", + "ciphertext_tag": "dfae9f81fb088d15ab042a4607905a15100ca1395a062722a0fb701b2018173a52d0d1f572a08d62272d20cd22d3dd16a763f79a5982e53af8933cfa4d18be6bfeea8d42deed30c5f952acfccfbf79991163d10fe3ea5a2513f759a32e22ff5f6c38f10f9e6a98bda304a5be4f4891c06e13de7cdbfd8b7aca424a45ac2be603cb158fe24261192950f24755782d9d873de4862a50ca3967d3e67126e1f8398dd415bd61465744d0f5eaf6abacf0900ab90add28b21ca6abd2eae1c69882ab5cf781ed1651bfb7ebec12f85ea9aed8eefd8dbf508e3a7ef3053cfb23d22d8807e723ccd9d0ec2511f2ea80354974facfeb25c537ae482f2e58f9a4d063978c6cbb66ed284c84c9d022c4d1e001f47484", + "rlen": 272, + "record": "0110dfae9f81fb088d15ab042a4607905a15100ca1395a062722a0fb701b2018173a52d0d1f572a08d62272d20cd22d3dd16a763f79a5982e53af8933cfa4d18be6bfeea8d42deed30c5f952acfccfbf79991163d10fe3ea5a2513f759a32e22ff5f6c38f10f9e6a98bda304a5be4f4891c06e13de7cdbfd8b7aca424a45ac2be603cb158fe24261192950f24755782d9d873de4862a50ca3967d3e67126e1f8398dd415bd61465744d0f5eaf6abacf0900ab90add28b21ca6abd2eae1c69882ab5cf781ed1651bfb7ebec12f85ea9aed8eefd8dbf508e3a7ef3053cfb23d22d8807e723ccd9d0ec2511f2ea80354974facfeb25c537ae482f2e58f9a4d063978c6cbb66ed284c84c9d022c4d1e001f47484" + }, + { + "name": "control / làn dài, mảnh 1/2 (more=1, long=1)", + "kind": "transport_frame", + "chan": "0x01", + "more": true, + "long": true, + "dlen": 1020, + "data": "552379c089d30cc8d6a995c5ab698a2becf18714ffa34589c764451f644b7711cbfd9b8d5c44ba79950fcb552682884b2859190e34918edc3495f0097ef90f91e34223b66e3279e64bcd6aba89c922d440bfc5159dd56d8f8b9f6de01f7370f84ad4eec8c367cbfe2b74891c0b4a0392a0bc466132c699eb877a00e3d649cd9a9c1ea8480ead2776a4bcdb7073d8128d4a3f7a29ae5a67dce2e9017ff5cea9291d376c58ba892a990286517ec730fba72361beaca168cc1a48864a7e954110c77c0fb1d3b7de690b8a5fa5db0520521f1aa29859b0e4f4adffd3479f2c744e0406f98d620797998a55c4e5940b501626515e6c7468a2d698d27a9794f0215d3cbfcd950cef4047b27526d2ab28fd36fa70bdf122bce895cb640e6879745c2fab7bc56539dd1addb6b8a74ff9fb808cff338d4eb83651b1a95f42e5387ff542126b55f6f53cba990c090f3d78eb6829e9b8da138419b39d4ba42bbd4d9ad846f126d987f7c64e25ec27915723b1e74caa9fd66e79b78bf84788987fd09b229a04397c085dae4ffdbbca02ec9430d5c3e77db5bcc9dff1bda2462aa12b1c3678fe443c99648b97d0189312d9e305e1b98119a23531525f05a91613139f631e8b14a54561bc209eb233f06ca35b41bf7ea3dc2a590ac6db75361501f2fbb92c2c63c20c7bf384e4b05b8d73bfc345ef1f5735e8e409d7882ca4fbdcd39c866284bd1fa8cf6ebb7ce98df2a346e987a173d0d560740a8d3fcab2db6ef0c49434c76ba0b81ac0233fcc9eaa9d8c104de597d895de2d05b5be1ffff217c9b12d2fbf2076644b72f7d3b49b3681c13197d163b8bde2cef93238d72d16005d26290c457369ea7ebbf11c90c63b4d19150d37a78a37ff31aeb4701294ebc8c580f2014d50fa359a5a284b4b88684d92707c088dd9702da585fcf5bcf42fc99e5fd5785a36502c25ab75fa9c7fee7227687c7ceb88390606a57a86867e9d4ce8b743a0065e02b0e999f28d66c8f05a5d8109f40d4624275626686d066f7ecbeca646e07764d74cd962e19d534ac30968a235c3bf3304d163cd1f2df1f124279573fda31b8a558233576458e02f73d822df8aad9900794c9715f90a93a2ab691cc6c10c13989e937022a56e3e9341033b565c2a6c721b0d178e046f5c5b68c04467ae5efa9398a8fbaac6b76322b2d802bc80f5cd7a6cdcd9781bd66488ab259b6afe13290ad6e5b279ada9cdc85695ca6c41ae88f6d30b943ec1e6a3c94a4f9f7330f5b85046bb698095f3e41b906eadcf44ce620edbf7f326650c7e6293cb544660cace47004bdb1d32e2c91e55e5f45e2d7fdda0e854531fdb4e9c4f16f3f9574e6547b275be235b68464c69695798566bc0307f6884248baa69ded861d48566b8fc07df062a89c9bef0c659fa7a49f27949afe005a98220bd1490439c6419c2", + "plaintext": "010303fc552379c089d30cc8d6a995c5ab698a2becf18714ffa34589c764451f644b7711cbfd9b8d5c44ba79950fcb552682884b2859190e34918edc3495f0097ef90f91e34223b66e3279e64bcd6aba89c922d440bfc5159dd56d8f8b9f6de01f7370f84ad4eec8c367cbfe2b74891c0b4a0392a0bc466132c699eb877a00e3d649cd9a9c1ea8480ead2776a4bcdb7073d8128d4a3f7a29ae5a67dce2e9017ff5cea9291d376c58ba892a990286517ec730fba72361beaca168cc1a48864a7e954110c77c0fb1d3b7de690b8a5fa5db0520521f1aa29859b0e4f4adffd3479f2c744e0406f98d620797998a55c4e5940b501626515e6c7468a2d698d27a9794f0215d3cbfcd950cef4047b27526d2ab28fd36fa70bdf122bce895cb640e6879745c2fab7bc56539dd1addb6b8a74ff9fb808cff338d4eb83651b1a95f42e5387ff542126b55f6f53cba990c090f3d78eb6829e9b8da138419b39d4ba42bbd4d9ad846f126d987f7c64e25ec27915723b1e74caa9fd66e79b78bf84788987fd09b229a04397c085dae4ffdbbca02ec9430d5c3e77db5bcc9dff1bda2462aa12b1c3678fe443c99648b97d0189312d9e305e1b98119a23531525f05a91613139f631e8b14a54561bc209eb233f06ca35b41bf7ea3dc2a590ac6db75361501f2fbb92c2c63c20c7bf384e4b05b8d73bfc345ef1f5735e8e409d7882ca4fbdcd39c866284bd1fa8cf6ebb7ce98df2a346e987a173d0d560740a8d3fcab2db6ef0c49434c76ba0b81ac0233fcc9eaa9d8c104de597d895de2d05b5be1ffff217c9b12d2fbf2076644b72f7d3b49b3681c13197d163b8bde2cef93238d72d16005d26290c457369ea7ebbf11c90c63b4d19150d37a78a37ff31aeb4701294ebc8c580f2014d50fa359a5a284b4b88684d92707c088dd9702da585fcf5bcf42fc99e5fd5785a36502c25ab75fa9c7fee7227687c7ceb88390606a57a86867e9d4ce8b743a0065e02b0e999f28d66c8f05a5d8109f40d4624275626686d066f7ecbeca646e07764d74cd962e19d534ac30968a235c3bf3304d163cd1f2df1f124279573fda31b8a558233576458e02f73d822df8aad9900794c9715f90a93a2ab691cc6c10c13989e937022a56e3e9341033b565c2a6c721b0d178e046f5c5b68c04467ae5efa9398a8fbaac6b76322b2d802bc80f5cd7a6cdcd9781bd66488ab259b6afe13290ad6e5b279ada9cdc85695ca6c41ae88f6d30b943ec1e6a3c94a4f9f7330f5b85046bb698095f3e41b906eadcf44ce620edbf7f326650c7e6293cb544660cace47004bdb1d32e2c91e55e5f45e2d7fdda0e854531fdb4e9c4f16f3f9574e6547b275be235b68464c69695798566bc0307f6884248baa69ded861d48566b8fc07df062a89c9bef0c659fa7a49f27949afe005a98220bd1490439c6419c2", + "padded_size": 1024, + "key_source": "c1 của bt-link.json", + "counter": 11, + "nonce": "000000000b00000000000000", + "ciphertext_tag": "037973e4c60b2c2f61b0ff531b5ec0b9c810ba3461ed1143f1cd8c2121cca21a2c49c6fc10ecdc57e19e9515c99df9abc5346321cffc1a14ab1eaba873260a7eae57eb280c9b929f079cdfd6b9a093e8ed9251902ef39a97d73f6b8ef2f73ebbd8d97249a7757aaf361027219f4cc88f42062d80571dc7e2576513624f309a6baa584d4f04382ddeea09a3607dfa220953f87da07c745540327176f474434b25c00b20360b53c826e4ffdae48a9e7b25129d04b0becbb24f9df04f07a8f55712e632feef9618399db440605da3b4fdca0322caa056514c25fd6bb958501fff92143d805af2117da061bf004eb1b1ecca134385c5544d08f3ac1112ba321623f69aff510b09d598791d875811ef82167fb771421cee0be9cbcbe783867989c77df4bf6ff3c36672a67b1da29e85f0414a0d1c8ae61bba0a1817163fe66532559c5a7bc7e372405e8ecf760af455734457392a606c197f854df3f9c1cbb2c5459a9c039404ecc821d4f9d6f150775a2494740adc68fbc9e98009fba62c3c34ea51c2ea5f9306c52dfa81dedaf5ce607787ccb7d568e1c37ca197f4ef2ed9a04e29d53ea4481000e00d83069e5b4685bc362bae10f8f844b9182699ff41e9e4a0ea8f27849f73947f76e1d834001025f7959902850e5f951e66f84874af47810559c80f8cd36f3ba427c1afd23af37fc7cb248f5e8fc28c836e9e1f6113fade18039e6f22f8c275baa92353fa6d5defeb19b5130e47e5eb965f5ed46f3a9e18fd918d3f2531ea4f5bfe36cdbb10c395dc7000a5ab8a6bc6b76081763450385d188bbf24d9959334e1613f85c1b93e5c69f5c309f91c9952ec9e33d7bb9f13c45aa826822bb344a052b3e0cf6318c3eeed64b60f4f7176f5f7a6817213fb460f9b686ba7d76420c8b46b43fce6c6aa24fea169dfda23351ecdfef9c1e61f6af74796f2bd44420fd28cee4445dbc8cc282b5422da42ca142b9ee43538b709a9a47f85f2d1df4a101b4ea208deae2786521a8bc2679615fe16907ff589f6fed39a331610a429855e462bb7ec78ed354ff9ee5dd9ad6ca77692cbbd8d7c1765d08945a6b3f1abc2cacc6c1ccf4dd79ca23f3b90946aab6b2abc8dd225b7482ee4a5409ccf4c38847b2ec9454ddeab4086a771d05fd9ae4d39ba2182d0a88869c50231cb12585b67263648ed3e22b3726b9384b3241692cd33e81a1eed1c87c9c4492d9802c4a17e4150608d73485cfa32b3ceec6513bfb6369be68e06e06f563bfa5448874c90467876387c4261c5935e318da439b45a11d16902189008d18a5def73aa15e419b32ea495db2729d95ac31790dca638decdfd1155157e417b71b3508e188135e8f2667a4da1d1e05077863b63f7b95efd431585b82722e2f315d2a298648d11c77648558aa6be31bd6013d457574bbba39e1d38ef98818adc0dde7d3ca6819d50fa70026642c35473f79d5252cc", + "rlen": 1040, + "record": "0410037973e4c60b2c2f61b0ff531b5ec0b9c810ba3461ed1143f1cd8c2121cca21a2c49c6fc10ecdc57e19e9515c99df9abc5346321cffc1a14ab1eaba873260a7eae57eb280c9b929f079cdfd6b9a093e8ed9251902ef39a97d73f6b8ef2f73ebbd8d97249a7757aaf361027219f4cc88f42062d80571dc7e2576513624f309a6baa584d4f04382ddeea09a3607dfa220953f87da07c745540327176f474434b25c00b20360b53c826e4ffdae48a9e7b25129d04b0becbb24f9df04f07a8f55712e632feef9618399db440605da3b4fdca0322caa056514c25fd6bb958501fff92143d805af2117da061bf004eb1b1ecca134385c5544d08f3ac1112ba321623f69aff510b09d598791d875811ef82167fb771421cee0be9cbcbe783867989c77df4bf6ff3c36672a67b1da29e85f0414a0d1c8ae61bba0a1817163fe66532559c5a7bc7e372405e8ecf760af455734457392a606c197f854df3f9c1cbb2c5459a9c039404ecc821d4f9d6f150775a2494740adc68fbc9e98009fba62c3c34ea51c2ea5f9306c52dfa81dedaf5ce607787ccb7d568e1c37ca197f4ef2ed9a04e29d53ea4481000e00d83069e5b4685bc362bae10f8f844b9182699ff41e9e4a0ea8f27849f73947f76e1d834001025f7959902850e5f951e66f84874af47810559c80f8cd36f3ba427c1afd23af37fc7cb248f5e8fc28c836e9e1f6113fade18039e6f22f8c275baa92353fa6d5defeb19b5130e47e5eb965f5ed46f3a9e18fd918d3f2531ea4f5bfe36cdbb10c395dc7000a5ab8a6bc6b76081763450385d188bbf24d9959334e1613f85c1b93e5c69f5c309f91c9952ec9e33d7bb9f13c45aa826822bb344a052b3e0cf6318c3eeed64b60f4f7176f5f7a6817213fb460f9b686ba7d76420c8b46b43fce6c6aa24fea169dfda23351ecdfef9c1e61f6af74796f2bd44420fd28cee4445dbc8cc282b5422da42ca142b9ee43538b709a9a47f85f2d1df4a101b4ea208deae2786521a8bc2679615fe16907ff589f6fed39a331610a429855e462bb7ec78ed354ff9ee5dd9ad6ca77692cbbd8d7c1765d08945a6b3f1abc2cacc6c1ccf4dd79ca23f3b90946aab6b2abc8dd225b7482ee4a5409ccf4c38847b2ec9454ddeab4086a771d05fd9ae4d39ba2182d0a88869c50231cb12585b67263648ed3e22b3726b9384b3241692cd33e81a1eed1c87c9c4492d9802c4a17e4150608d73485cfa32b3ceec6513bfb6369be68e06e06f563bfa5448874c90467876387c4261c5935e318da439b45a11d16902189008d18a5def73aa15e419b32ea495db2729d95ac31790dca638decdfd1155157e417b71b3508e188135e8f2667a4da1d1e05077863b63f7b95efd431585b82722e2f315d2a298648d11c77648558aa6be31bd6013d457574bbba39e1d38ef98818adc0dde7d3ca6819d50fa70026642c35473f79d5252cc" + }, + { + "name": "control / làn dài, mảnh 2/2 cuối (more=0, long=1)", + "kind": "transport_frame", + "chan": "0x01", + "more": false, + "long": true, + "dlen": 400, + "data": "c0caf6011520e0657f2538361174f12b934433e8a8a42cbd0292c9d6bb99abfe545506e13c351c8dafd2f2d9fe7aace1ef6b22fab7e04c2008c48b1a2faefa5d99d5207988187105be02557e7d304cc7484cb2d65d49d1fc6f4406003cd9faa7f0f59f096881ea621ec9eb1f428c599bab9634c335773018f476d1ea6d24c246eb1d6f07b83b3efdbabca652d1ab5d61d9b38ab2967b407aeeb26aa15c4d7751d446fe5e5d0e30c7dd0ef42793200a143c9cd5099f30f89cdd1d0c0ea73117e5a5e89a95218f242ea439c8e4646e19af0a7eb3be267dfd8b2cd4f1b5abf2a5415efee29e91a91149fcb3f1744735c5c22ca1a9b8cbf25280da15b00bb386a55fbfdd6c4d08e2d5a8227748c7ee63289d85e8d728f9e5db46bf339627162097b6d6902334ed329c5046ed433b4f80f286dcb7628bfdd1a9ed58db80edfe5734987bd73d08531f90d55e476ca6a8c0337d30b664d83ab49c4cf4ef84b6564594ecd19d3abdbb1b3126a457b99b322376f0273ddbfb6500b5f415a31d1eed75d517e2899ef3c94bea69e12c3fcfd1a04c42", + "plaintext": "01020190c0caf6011520e0657f2538361174f12b934433e8a8a42cbd0292c9d6bb99abfe545506e13c351c8dafd2f2d9fe7aace1ef6b22fab7e04c2008c48b1a2faefa5d99d5207988187105be02557e7d304cc7484cb2d65d49d1fc6f4406003cd9faa7f0f59f096881ea621ec9eb1f428c599bab9634c335773018f476d1ea6d24c246eb1d6f07b83b3efdbabca652d1ab5d61d9b38ab2967b407aeeb26aa15c4d7751d446fe5e5d0e30c7dd0ef42793200a143c9cd5099f30f89cdd1d0c0ea73117e5a5e89a95218f242ea439c8e4646e19af0a7eb3be267dfd8b2cd4f1b5abf2a5415efee29e91a91149fcb3f1744735c5c22ca1a9b8cbf25280da15b00bb386a55fbfdd6c4d08e2d5a8227748c7ee63289d85e8d728f9e5db46bf339627162097b6d6902334ed329c5046ed433b4f80f286dcb7628bfdd1a9ed58db80edfe5734987bd73d08531f90d55e476ca6a8c0337d30b664d83ab49c4cf4ef84b6564594ecd19d3abdbb1b3126a457b99b322376f0273ddbfb6500b5f415a31d1eed75d517e2899ef3c94bea69e12c3fcfd1a04c420000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "padded_size": 1024, + "key_source": "c1 của bt-link.json", + "counter": 12, + "nonce": "000000000c00000000000000", + "ciphertext_tag": "219ca00278a3b3ee53c2acde893326fd5bbdbf7fe8f2e94c11d8e333c8f503903c63b88ddbd95bf6ced278b3908a319ce13a6beeb31f033839beaa0a9b527078a63b12a5b95d15a8fcf28dde1082c54ff8da82c6b1b8ef88f96e4bbed2b9d378022bee1218a59002c332bc5031fb21b00eec918a6279f8dd1cf8a51ae44df931a055bd729d5f33d8710a6cf768e9ff8d9436aca74764ccffc60666bdd3db6b04a8b27cf764bd399b9ee9ed1596923f22581000257a9bf7d41c616b40f4b3f9bdb58543a6619b22d0229741865e40c7f57f5fda54d15018aa2915549c22a1163eae2f11df0c32e09c05aa52ea95787258cab0c53636b881011bc4c13d690d2794f7c46beffe80e4eba21732aeab9be7589b02cc04f115bc2963217bc77f3ec304481848ab02701da788728565e679c8478c8d2fba46b03d9fdd060b33f42b2f42e96143958576cd1f894eae3b9fc9d21ad94e077b64779b6b95d9a1ac51174cddd5cdd9f310be1efef5113e8cd5e826206bb105aa111e3890c075ef1d8002fdc87c1c6561951d3e69ea5d5eceb9fc90b46991c8b2967313e9dc3e2e7726cc6ad35464c4ec0bdaffd89723395e02a26399498465a28d6da54882477dd5ce83054f495a0cf1d1adb906f45a98bdb121955ed3f0e42452772098aa19ed78de9ea0df6c1da8cfc3c2ada970a75d070c284c517ebeecd75b6c6fdd94ca46b46cffdb343fbbf0eddf60a9ab1c7a8a45ce64b1cecdb003d42e747c928cf29a1085e79f87a61befa83ac78dd3a64035077c53e6be9fc0c57df3bdfe9880c5af4f79fa12edce0627f1fae4bef76bf7408431467e7aa2c2d9d15732779ca1e4c35c76a513b633feb5950133f9d8037705ace6d8e5adb71091aa6b0a1815bdfb9ebe3af50b8a9cf49ce2928e35fbdfeb0c3a3e847a003e7710294b2cf696855a478e51e8b8251dc19694c57bab3eb35de465844f8764f41efbea94dba3f73da9f01de1001aca41a3101f6c9e9d4a0438d56352cb64daf44f702204b70fbd2a989466ea9560ca836cb975b39e4e6417dfe402dd39fe0caf9352b64726c258fb05934b58639a0c1d7f29ad3742712b8f16d3b243a31c04f33d227b90093992f74f68421b596b6648b41b3e3843733aed65b6bc2e10dc2e245079d9471b3104a99bac559b53c587526fd8c4869102ac5ead0009629488969935d33f57d138f5b362f3bd9e746b1f1cf3b0d77985fa86c7ca28f2ada31ad9cfc27c96638b070af43936645c600a102647205fa3937f617caa000a372e56ccd5e6f03b2f94b786f49b3309d691c84a59caa323a2e4c04db113f76a27e4927d6215ca05dd74d87c64b019eef7490e5db00a6038e160214e4b1cc43e6aca7b5b0b28a5406fb4f45594a6e070a92c2d68dc40071e3a9887a58a30a39f3d3a510dca5fdf9fb8383afeedec7aeaab963a0d311829070570ab9d0a69dc05c1826013", + "rlen": 1040, + "record": "0410219ca00278a3b3ee53c2acde893326fd5bbdbf7fe8f2e94c11d8e333c8f503903c63b88ddbd95bf6ced278b3908a319ce13a6beeb31f033839beaa0a9b527078a63b12a5b95d15a8fcf28dde1082c54ff8da82c6b1b8ef88f96e4bbed2b9d378022bee1218a59002c332bc5031fb21b00eec918a6279f8dd1cf8a51ae44df931a055bd729d5f33d8710a6cf768e9ff8d9436aca74764ccffc60666bdd3db6b04a8b27cf764bd399b9ee9ed1596923f22581000257a9bf7d41c616b40f4b3f9bdb58543a6619b22d0229741865e40c7f57f5fda54d15018aa2915549c22a1163eae2f11df0c32e09c05aa52ea95787258cab0c53636b881011bc4c13d690d2794f7c46beffe80e4eba21732aeab9be7589b02cc04f115bc2963217bc77f3ec304481848ab02701da788728565e679c8478c8d2fba46b03d9fdd060b33f42b2f42e96143958576cd1f894eae3b9fc9d21ad94e077b64779b6b95d9a1ac51174cddd5cdd9f310be1efef5113e8cd5e826206bb105aa111e3890c075ef1d8002fdc87c1c6561951d3e69ea5d5eceb9fc90b46991c8b2967313e9dc3e2e7726cc6ad35464c4ec0bdaffd89723395e02a26399498465a28d6da54882477dd5ce83054f495a0cf1d1adb906f45a98bdb121955ed3f0e42452772098aa19ed78de9ea0df6c1da8cfc3c2ada970a75d070c284c517ebeecd75b6c6fdd94ca46b46cffdb343fbbf0eddf60a9ab1c7a8a45ce64b1cecdb003d42e747c928cf29a1085e79f87a61befa83ac78dd3a64035077c53e6be9fc0c57df3bdfe9880c5af4f79fa12edce0627f1fae4bef76bf7408431467e7aa2c2d9d15732779ca1e4c35c76a513b633feb5950133f9d8037705ace6d8e5adb71091aa6b0a1815bdfb9ebe3af50b8a9cf49ce2928e35fbdfeb0c3a3e847a003e7710294b2cf696855a478e51e8b8251dc19694c57bab3eb35de465844f8764f41efbea94dba3f73da9f01de1001aca41a3101f6c9e9d4a0438d56352cb64daf44f702204b70fbd2a989466ea9560ca836cb975b39e4e6417dfe402dd39fe0caf9352b64726c258fb05934b58639a0c1d7f29ad3742712b8f16d3b243a31c04f33d227b90093992f74f68421b596b6648b41b3e3843733aed65b6bc2e10dc2e245079d9471b3104a99bac559b53c587526fd8c4869102ac5ead0009629488969935d33f57d138f5b362f3bd9e746b1f1cf3b0d77985fa86c7ca28f2ada31ad9cfc27c96638b070af43936645c600a102647205fa3937f617caa000a372e56ccd5e6f03b2f94b786f49b3309d691c84a59caa323a2e4c04db113f76a27e4927d6215ca05dd74d87c64b019eef7490e5db00a6038e160214e4b1cc43e6aca7b5b0b28a5406fb4f45594a6e070a92c2d68dc40071e3a9887a58a30a39f3d3a510dca5fdf9fb8383afeedec7aeaab963a0d311829070570ab9d0a69dc05c1826013" + }, + { + "name": "bulk / một mảnh ảnh (more=1)", + "kind": "transport_frame", + "chan": "0x02", + "more": true, + "long": false, + "dlen": 1020, + "data": "f22ad204961b3d30a7b0c4657e279c9a91e252c4ab2eff7af7352de55021304584afa6e1456d309fdbfc3e635aa832d497de47608c0d0d833168c48e50ff6e98711cf30acb1770c9b43222a5fe5531df4b749e4ecf42097161347ee524b0869049fa419e07b28dac72197305451cc1bf4ef640fca34676cd8d8b37222a956b1b0c1b62c6a6d3ae18a71eb19091fdc68e10827e7b05433408897c69542b5e89b8b04880890b9539acf6df90640fb0a5ecb958a8b58afdd0057a92df36aa1802724d3c477a0ea37381c79f7a17f3c890ce0465beb72bbbba8140a3f7f43aca12f051020671297eebe457046a8cd25cab84114749619d83429c9e6c69804833023e9f7494ad927f3a6796a03d242e125e8b231a745c74d6e4a651fc6398d2c707b00294df69fb7543198023304b29b2a01f00ea25f7315d474e2db3b243aa0f9bec3db1021f649aafb598a5c28ae345564e101d659a34fa40fc799506de140990b8390cb5c041a8663dc7bcfe7d56fc513e58c3dde45875267ba769ad55c6183a2ab97317d625167b333cf9ed9d406ce50b5ec8cba84fde6ca86cdaa514694cf6167ebe3f4e1a5d4193ab97c451bbbe11bb144255568cd830945e3c6d45dde3ed4c6026ad9ace52087be7659d4a03d1d10fd86c85be8a4dee289f459167f8cadbbf9e277b41208435e10f5fabf4f44aea1aad05a35492e8d3657feeaa05d59cae35da2af9a92fba41967788a65bf83f62b0a270728c015e26642a4feaa04b9e7baa7cc436acf684fb0c481d5a6b0dd4c18d9b85cb9c8fae330a7398a7d897e66fae6d5aaab40a18b50e221811bba7dff4ea22a053031d939ad4d2e33ef0b588de735f0c21fd9ef9b71fdbf8fb03793e715a6f014d7f9a4df6cdafbe07c5f3fc1c8d9862c8f03ef0366293078a11a64cea35ddf29ebde98c510d38082d775fb41bcbaa445121ee1163d953225c60df049c5171e360908490c5c13f5cd789dc0b115a63cd8b2cd6701ace3722ab0588939d58b5b0c577195045c7c89f2ebd3600b73b22d733864385cd4a5e60bf0c73f06b5080ac348f8867df566a98e32a98805f4c8c64f86073cf3ffdfbaec4e6b4d36402bb92090ecec41b843a290d8711771b2925d7252bed0c0887341b37d87f8a7a1b42635bea3e4d5b6ccccec60d7b48330d94318a5fd0fc55d1f5f53838ac20180d20322a4f53d7b0ee9276eca7396247022556b7cb25f44bdb6254ee8428b7d608981c1da5896073cb4b0adfb65843c7a7cd5f77ab4d7a0bd3b38d681de7f496c46c27309ad7e4c4e004ee57a534ca689256fa79090c208c3b6e7a1f21dda9394b0be5b030b238f74fe74ddaf10e1d418fb5b08d8bf62b266a7ceff31c9d648b34957f9bfa13995d91032d115cdff2a4a03834bb5f07a75ae606541bfd5ad8f51868fe824da7b8d6f06288a82d", + "plaintext": "020103fcf22ad204961b3d30a7b0c4657e279c9a91e252c4ab2eff7af7352de55021304584afa6e1456d309fdbfc3e635aa832d497de47608c0d0d833168c48e50ff6e98711cf30acb1770c9b43222a5fe5531df4b749e4ecf42097161347ee524b0869049fa419e07b28dac72197305451cc1bf4ef640fca34676cd8d8b37222a956b1b0c1b62c6a6d3ae18a71eb19091fdc68e10827e7b05433408897c69542b5e89b8b04880890b9539acf6df90640fb0a5ecb958a8b58afdd0057a92df36aa1802724d3c477a0ea37381c79f7a17f3c890ce0465beb72bbbba8140a3f7f43aca12f051020671297eebe457046a8cd25cab84114749619d83429c9e6c69804833023e9f7494ad927f3a6796a03d242e125e8b231a745c74d6e4a651fc6398d2c707b00294df69fb7543198023304b29b2a01f00ea25f7315d474e2db3b243aa0f9bec3db1021f649aafb598a5c28ae345564e101d659a34fa40fc799506de140990b8390cb5c041a8663dc7bcfe7d56fc513e58c3dde45875267ba769ad55c6183a2ab97317d625167b333cf9ed9d406ce50b5ec8cba84fde6ca86cdaa514694cf6167ebe3f4e1a5d4193ab97c451bbbe11bb144255568cd830945e3c6d45dde3ed4c6026ad9ace52087be7659d4a03d1d10fd86c85be8a4dee289f459167f8cadbbf9e277b41208435e10f5fabf4f44aea1aad05a35492e8d3657feeaa05d59cae35da2af9a92fba41967788a65bf83f62b0a270728c015e26642a4feaa04b9e7baa7cc436acf684fb0c481d5a6b0dd4c18d9b85cb9c8fae330a7398a7d897e66fae6d5aaab40a18b50e221811bba7dff4ea22a053031d939ad4d2e33ef0b588de735f0c21fd9ef9b71fdbf8fb03793e715a6f014d7f9a4df6cdafbe07c5f3fc1c8d9862c8f03ef0366293078a11a64cea35ddf29ebde98c510d38082d775fb41bcbaa445121ee1163d953225c60df049c5171e360908490c5c13f5cd789dc0b115a63cd8b2cd6701ace3722ab0588939d58b5b0c577195045c7c89f2ebd3600b73b22d733864385cd4a5e60bf0c73f06b5080ac348f8867df566a98e32a98805f4c8c64f86073cf3ffdfbaec4e6b4d36402bb92090ecec41b843a290d8711771b2925d7252bed0c0887341b37d87f8a7a1b42635bea3e4d5b6ccccec60d7b48330d94318a5fd0fc55d1f5f53838ac20180d20322a4f53d7b0ee9276eca7396247022556b7cb25f44bdb6254ee8428b7d608981c1da5896073cb4b0adfb65843c7a7cd5f77ab4d7a0bd3b38d681de7f496c46c27309ad7e4c4e004ee57a534ca689256fa79090c208c3b6e7a1f21dda9394b0be5b030b238f74fe74ddaf10e1d418fb5b08d8bf62b266a7ceff31c9d648b34957f9bfa13995d91032d115cdff2a4a03834bb5f07a75ae606541bfd5ad8f51868fe824da7b8d6f06288a82d", + "padded_size": 1024, + "key_source": "c1 của bt-link.json", + "counter": 13, + "nonce": "000000000d00000000000000", + "ciphertext_tag": "3d0b904809ddb647a52897ee878f0e0ab583c5b885a6a2bea543a779ab4685c7fa150cab9f190db4dad63c155cb01fbadbd81ec658bea8214f6ef788475a943fe4f87c500e4f1c296be00e14e7cadb2ac61d2c8a9e05fbf5ceadb87c5d3c3e0825b65758995fccdd6d74ea0f49aeb55cea9e271f3ef722a8b5b546ebb5e56b9d4970513d69bbfcccb9fd59ef0f495a3a99660c3ed78cfc7ce6356e9462f5a0b10d72aa9a06518986aed7c9f9df5819a7a388741719fddefe8c06105ae954e2c179cb296cbfab7a3a1daa710b6f6c8c24948a09864bda1290b0f82e4c49c6b94ce0115c8ede464c45f9988da78647fa1f83fcd7201bc72a63a62e19e2bda518d6b50d5c3be7bd14d0480d5fd32fb82edfbfd8c11e898d323ec0edb73dce4b24656b524f73c8f63330e9234e5e13a25282fc5762f2783efdbd1a2f1dbbdf424ea3f56011a0ee3d74d2f3e81af4d68b4c80dc5941514845dc8e170d45a934d50c2c7e9cd103b067114d7155d4cb59816ba1a97454ababc5a28cc0f81f9b5df46ec5175c1469a466ba574ad4eedd505cd5d041a386ba465e798a673ded893e4b7f8b976665c413f83d3d386a263ece1542752a8b4565fe705cdba8f09b64bfc4e54e7a09736c024cc05785e18ff251f652cd8ca8626d1abedc29197f852f0f572f0bcd2e93f5f3d485ffb2d9ad22743ab66dddb3891eed5fbac9b4c134b6dcd695fb0ec8d0ad775961f664f2e35c3e717cc2c99e448621ca67a774ec521c76a528a0341a557de3e8a8328516c6fc3bc435d9f582190f9d5abf6dd5f63f0eb5b61decb0308b191a6463f3a13f498f28227b95d0f4537e54c93533f22ed0ecf8c5765efd798019ea45a15347a4d60cd562c10a0ce3a4839a0862a397ad0a8a38b2bbb88548ce996b3716ece65c4c3a1bf2281900212b9f87e2bbe8b08a890d3dfe24f1fa867ba5e67328d03c0322e908ae5877086d799d746ff9e38520e0770cb5992b6a6b4b44530f6f42161b71930ed09e8aac1a363a3c2cc97387065ef2d020a6106e65759b24ee20ca68a44181f462f560413392c8e4be142ce429b99bda0e58ad18b9465ceed60e35359fb8741661b1d82889619dad3825af6b2a2f5db10045a95fbd306c0d8c5540551cf0d983708311581904acb2906ed4fbb921b715db968136075f0d7b8bc43d646012e38a31f05e0729ca87e31bb3196be0e0539e26f6e5c17d4d479928d35eb62f5c916e9862260f609c7615e0f4f955bb3b442b1a35381f0f170e738bd24ae4b03aef939f43fe0ed1d567a35a291eba099ebb5e6bf247f03fe67886e624b6701887597945141db9bc8fbacc6a3bec6ec4d47e259b6dba912c99725b1bf6549679240cde06761950776cf5cdeb4d0af81d4fa04aaae061f4ddce6b7e59a0c11a659658398816de2b048ef415f4016ef816c0a041b96aba3c0599ffdcdea45c25c6ac2326eb5688", + "rlen": 1040, + "record": "04103d0b904809ddb647a52897ee878f0e0ab583c5b885a6a2bea543a779ab4685c7fa150cab9f190db4dad63c155cb01fbadbd81ec658bea8214f6ef788475a943fe4f87c500e4f1c296be00e14e7cadb2ac61d2c8a9e05fbf5ceadb87c5d3c3e0825b65758995fccdd6d74ea0f49aeb55cea9e271f3ef722a8b5b546ebb5e56b9d4970513d69bbfcccb9fd59ef0f495a3a99660c3ed78cfc7ce6356e9462f5a0b10d72aa9a06518986aed7c9f9df5819a7a388741719fddefe8c06105ae954e2c179cb296cbfab7a3a1daa710b6f6c8c24948a09864bda1290b0f82e4c49c6b94ce0115c8ede464c45f9988da78647fa1f83fcd7201bc72a63a62e19e2bda518d6b50d5c3be7bd14d0480d5fd32fb82edfbfd8c11e898d323ec0edb73dce4b24656b524f73c8f63330e9234e5e13a25282fc5762f2783efdbd1a2f1dbbdf424ea3f56011a0ee3d74d2f3e81af4d68b4c80dc5941514845dc8e170d45a934d50c2c7e9cd103b067114d7155d4cb59816ba1a97454ababc5a28cc0f81f9b5df46ec5175c1469a466ba574ad4eedd505cd5d041a386ba465e798a673ded893e4b7f8b976665c413f83d3d386a263ece1542752a8b4565fe705cdba8f09b64bfc4e54e7a09736c024cc05785e18ff251f652cd8ca8626d1abedc29197f852f0f572f0bcd2e93f5f3d485ffb2d9ad22743ab66dddb3891eed5fbac9b4c134b6dcd695fb0ec8d0ad775961f664f2e35c3e717cc2c99e448621ca67a774ec521c76a528a0341a557de3e8a8328516c6fc3bc435d9f582190f9d5abf6dd5f63f0eb5b61decb0308b191a6463f3a13f498f28227b95d0f4537e54c93533f22ed0ecf8c5765efd798019ea45a15347a4d60cd562c10a0ce3a4839a0862a397ad0a8a38b2bbb88548ce996b3716ece65c4c3a1bf2281900212b9f87e2bbe8b08a890d3dfe24f1fa867ba5e67328d03c0322e908ae5877086d799d746ff9e38520e0770cb5992b6a6b4b44530f6f42161b71930ed09e8aac1a363a3c2cc97387065ef2d020a6106e65759b24ee20ca68a44181f462f560413392c8e4be142ce429b99bda0e58ad18b9465ceed60e35359fb8741661b1d82889619dad3825af6b2a2f5db10045a95fbd306c0d8c5540551cf0d983708311581904acb2906ed4fbb921b715db968136075f0d7b8bc43d646012e38a31f05e0729ca87e31bb3196be0e0539e26f6e5c17d4d479928d35eb62f5c916e9862260f609c7615e0f4f955bb3b442b1a35381f0f170e738bd24ae4b03aef939f43fe0ed1d567a35a291eba099ebb5e6bf247f03fe67886e624b6701887597945141db9bc8fbacc6a3bec6ec4d47e259b6dba912c99725b1bf6549679240cde06761950776cf5cdeb4d0af81d4fa04aaae061f4ddce6b7e59a0c11a659658398816de2b048ef415f4016ef816c0a041b96aba3c0599ffdcdea45c25c6ac2326eb5688" + }, + { + "name": "challenge (ví dụ đóng gói)", + "kind": "preauth_frame", + "preauth_kind": "0x01", + "body": "01d4ae169c229c47b6721c338524c7b8d543ba75c4765944c4abfe0752503f1623", + "record": "002101d4ae169c229c47b6721c338524c7b8d543ba75c4765944c4abfe0752503f1623" + }, + { + "name": "reject (ví dụ đóng gói, thân rỗng)", + "kind": "preauth_frame", + "preauth_kind": "0x7F", + "body": "7f", + "record": "00017f" + } + ], + "invalid_vectors": [ + { + "name": "rlen ngoài {272,1040,4112}", + "reason": "rlen_out_of_bounds", + "rlen": 273, + "record": "0111d9ef0142422f2c2706dc8f71fcd1d9bf6314dd05a5dc5edb5c36ca78043e65fe89a6e5fa14a502cb132233f9b3412da6e3a3fc71ce2035eac2657ba82a7809e73beabb1f11752a5685eb34139635583a88a4f31fec312786b905956302a81eefbde9e4472f155c904ce3fbd199a0c9cd9f4bf75a911fd5ef2e4239dd5fff9e89572261f169f1fcd512b346e4e09ce34732283f84190925c7ed45246c93da7b7ec7bb7d3aa45018e372b410e3796e1b4b98bbf4cc950d641f47181e04ee7f75837df3eb20fc38371b0e573e4ce7c8807fbb1e137bdd46c458e5ede5d212486725fa154cc7e6c97c97fce99dab5caab9478286037aafb3af36543720de089a4a53b8df4dfd5e80c5623193cc896b8644e100", + "expect": "rlen không khớp 272/1040/4112 → đóng kênh ngay, không cấp phát đọc tiếp thân bản ghi" + }, + { + "name": "bản ghi pre-auth vượt BT_PREAUTH_RECORD_MAX", + "reason": "preauth_record_over_bound", + "rlen": 65, + "record": "00410156f0efdacb55fd54712cb72f749aa05a62c48ba25f68dcba444d70664b69b1501d6b728e3717bb6f8da0c599234a4bfe16f3f336264c07d1056c40e2425cd050", + "bt_preauth_record_max": 64, + "expect": "rlen = 65 > BT_PREAUTH_RECORD_MAX (64) → đóng kênh ngay, không cấp phát" + }, + { + "name": "chan lạ (không 0x01/0x02)", + "reason": "unknown_chan", + "chan": "0x03", + "plaintext": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "record": "0110790a900781091f03b09ec8b4541c794c7d8dccf7f76580664867fb6219271ef45215de48df37c48abf8f60ca3e557e4b87a14e9612e2d4a348d1c8522c458f2b6818429c1043ca6507ee2c20bc4b37f92bb8ad9d69d7376b5eff6a3c41c9398cba931620cde80acbb8b8f812f82e423e197a9df680b6127e84f205ff8dfa31b398e175b3e98ff230f0b13c9b1a2c11ba8295d639f44c9410aac82f6a1645adf716d85c505667297ab178e8d967bb65869c33de16e9c8bc1abf4d8bbd4731909e835f3b18dfc57a83aabe071459e327ad52b0cb2c94c635598eb6199c99a7e0217d354e190559283a171a8a9a248b2ff0ddf84f1e1094dfa428e4035f6b2c4de29aec982b72c15704c6635eaafdbe7cef", + "expect": "chan = 0x03 không phải 0x01 (control) hay 0x02 (bulk) → đóng liên kết" + }, + { + "name": "more=1 nhưng long=0 (làn ngắn)", + "reason": "more_without_long", + "plaintext": "0101000aa78dd42688b494ca7e8c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "record": "0110b6a8d63b052103bd49b40621f45a4423af6e9804384fc4f46d0bc1e0757685d0465b51c48639d887b04bb7fc98e95e4a1b909df1a25f39a9793b48417fd91b3acdc6629010763e284df23b8f7c50c6ab60630073c944c9194d32360ae57e705928b5586ca79e65631dfd67e1e554a88ebdc0ac335cb782645f4e2cce183572ee1d16a68a24cd1ac660e4477c0b55be13871a8dc9f501892a018c3a4b264ea70dfe9ed2eb2f528b560c01f1191255bacf8bd2180b80a2498a4b8d15fc1527e73fd56350553b3dfd809ad1bf7670f36b209a1aaefd85cfda1a8d92aa429d225de93f9e768e5ed1681049d949001db2149caab4e53f32dae58ccbfab93643982f95e0e1dcf00fbf34b49c2e35560711ecf9", + "expect": "làn ngắn (long=0) không bao giờ có more=1 (một làn ngắn luôn đúng một mảnh) → đóng liên kết" + }, + { + "name": "mảnh làn dài có dlen > BT_BULK_FRAGMENT_MAX (1020)", + "reason": "bulk_fragment_over_max", + "chan": "0x01", + "long": true, + "dlen": 1021, + "bt_bulk_fragment_max": 1020, + "expect": "dlen = 1021 > BT_BULK_FRAGMENT_MAX (1020) trên làn dài (long=1) hoặc làn bulk (chan=0x02) → đóng liên kết, không cần giải mã thân" + }, + { + "name": "tin làn dài vượt BT_MESSAGE_MAX trước mảnh cuối", + "reason": "message_over_max", + "chan": "0x01", + "long": true, + "bt_message_max": 262144, + "prior_bytes_received": 261644, + "last_fragment_dlen": 1020, + "total_after_fragment": 262664, + "expect": "261644 + 1020 = 262664 > BT_MESSAGE_MAX (262144) trước khi mảnh này là mảnh cuối (more=0) → đóng liên kết" + } + ] +} diff --git a/test-vectors/bt-link.json b/test-vectors/bt-link.json new file mode 100644 index 0000000..4756fa6 --- /dev/null +++ b/test-vectors/bt-link.json @@ -0,0 +1,139 @@ +{ + "description": "K_bt_disc = HKDF-SHA256(ikm = PRK, salt rỗng, info \"handlive/v1/bt-discovery\", L 32); K_bt_link (tương tự, info \"handlive/v1/bt-link\") — sống suốt đời cặp, khác K_bt_pdisc/K_bt_plink của bt-pairing.json (sống theo một QR). kind \"hint\": hint quảng bá bình thường (không phải lúc ghép đôi) = 4 byte đầu của HMAC-SHA256(K_bt_disc, \"HLBTDISC1\" ‖ epoch int64 BE), epoch = floor(now_ms / BT_HINT_PERIOD) — khác pr_bt (đổi theo mỗi QR) vì hint này đổi theo thời gian (0.4.5 \"Advertisement\"). kind \"handshake\": bắt tay bảo mật liên kết đầy đủ (0.4.5 \"Link-security handshake\" bước 1-4), psk = K_bt_link: challenge (kind 0x01, n_s 32 byte) rồi message 1 (\"psk, e\", kind 0x02 noise1) và message 2 (\"e, ee\", kind 0x03 noise2) của Noise_NNpsk0_25519_ChaChaPoly_SHA256 (Noise Protocol Framework rev. 34), prologue = \"HLBT1\" ‖ n_s, rồi Split() ra c1 (C mã hoá/S giải mã)/c2 (ngược lại) — hai CipherState của bước 4-5. KHÔNG phải vector công bố của tài liệu Noise (xem ghi chú ở bt-pairing.json); noise_nnpsk0.py đã được Trang rà soát ở #3328 cho message 1, cần rà soát thêm phần message 2/Split mới. invalid_vectors: wrong_key, tag_mismatch_noise1, tag_mismatch_noise2 (0.4.5 bước 4: 'C verifies its payload tag — failure → C closes the channel, CONN-05 E3'), replayed_channel (0.4.5 bước 3: 'The prologue binds n_s, so a recorded noise1 never verifies on a new channel').", + "source": "docs/detailed-design/00-common-specs.md 0.4.5 \"Keys\", \"Advertisement\", \"Link-security handshake\"; PRK của pair-prk.json; Noise Protocol Framework rev. 34 (Noise_NNpsk0_25519_ChaChaPoly_SHA256)", + "vectors": [ + { + "name": "cặp 1", + "kind": "key", + "pair_id": "3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", + "prk_source": "pair-prk.json", + "prk": "d9e9827d49f7db11a88ac107ed6fb22b761c723c35449fc798694867fe55c209", + "k_bt_disc_info": "handlive/v1/bt-discovery", + "k_bt_disc": "c02c87d9df21fd5cf735a27ccb04de7f3ce79a76d9e958c300a0416bb0edeb35", + "k_bt_link_info": "handlive/v1/bt-link", + "k_bt_link": "16948c82ea6b9f3f4df8fdc711be84fb2ed360bf82cb8a60cf2808bc63548988" + }, + { + "name": "cặp 2", + "kind": "key", + "pair_id": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f2a3b4c5d", + "prk_source": "pair-prk.json", + "prk": "8d3109350469af80c3db25ff2c1867ab56112623020d829301236ed7251411cc", + "k_bt_disc_info": "handlive/v1/bt-discovery", + "k_bt_disc": "25add16aef33f9c7ec8a251e0bdae7bb91e80cb6c2ae66a8c763f83c628cb384", + "k_bt_link_info": "handlive/v1/bt-link", + "k_bt_link": "fcdddc4e4638ed616aba885ef26218e63e14f091f1e0c9ec840366877fea3d7d" + }, + { + "name": "cặp 1 / hint quảng bá", + "kind": "hint", + "pair_id": "3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", + "k_bt_disc_source": "cặp 1", + "hint_label": "HLBTDISC1", + "bt_hint_period_ms": 900000, + "epochs": [ + { + "epoch": 0, + "hint": "72a1a227" + }, + { + "epoch": 1, + "hint": "b67a752b" + }, + { + "epoch": 2, + "hint": "4f40cd25" + }, + { + "epoch": 479762, + "hint": "1aeee44e" + } + ] + }, + { + "name": "cặp 1 / bắt tay liên kết thường", + "kind": "handshake", + "pair_id": "3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", + "psk_source": "k_bt_link của cặp 1", + "psk": "16948c82ea6b9f3f4df8fdc711be84fb2ed360bf82cb8a60cf2808bc63548988", + "protocol_name": "Noise_NNpsk0_25519_ChaChaPoly_SHA256", + "n_s": "2cb3744ccb10882cef6e96d024d80bff5601132eced64c9ab09f7605d7bb2651", + "prologue_label": "HLBT1", + "prologue": "484c4254312cb3744ccb10882cef6e96d024d80bff5601132eced64c9ab09f7605d7bb2651", + "challenge_kind": "0x01", + "challenge_record": "0021012cb3744ccb10882cef6e96d024d80bff5601132eced64c9ab09f7605d7bb2651", + "h0": "f0b890826fb8e6584af6948d227169c381c9ed6a4c1c90df4b9ff454fa947f7b", + "h_after_prologue": "e0220c0816e61954f8bf94de389c0786cdc9754de50446d37e2f0c9d3253da49", + "mix_key_and_hash_psk": { + "temp_key": "a726faeaaeb3c0d2e3d7cd0bc345318eae898b30c418654a64a9626623425f32", + "ck": "c6993109572a2d2e1142dc1bc4ce5e7d93c00ba623cca36b2a5f62308c1254c2", + "temp_h": "8b5a5ab1c9e9cded429858ed27b7c2213c80376f86cbdc98328acd0eac88b181", + "temp_k": "3493639e64b3e9889eab61e664b7b2db73e4c029e67411df3ef2b6f12769d663", + "h": "9b0e5a8a31cd403e0b2ef56bdee37911278e8000650ba8f6ec2dfb4efc122a92" + }, + "h_after_psk": "9b0e5a8a31cd403e0b2ef56bdee37911278e8000650ba8f6ec2dfb4efc122a92", + "ck_after_psk": "c6993109572a2d2e1142dc1bc4ce5e7d93c00ba623cca36b2a5f62308c1254c2", + "k_message1": "3493639e64b3e9889eab61e664b7b2db73e4c029e67411df3ef2b6f12769d663", + "e_priv_c": "6957d0f71dd22be6ea81d25694fd1abd2eed41d4fe5daa62af2bde2b3c8e957a", + "e_pub_c": "3ba31c49f8186b67851aee28dad3688d20e5b38995cd53193d332f03b145960f", + "h_after_e_c": "bd7216420d0896066bb98d44504e3c4945af95b92188f1531744db2ada017941", + "ciphertext_tag_1": "0ad750d42ff823ed2fa9111237c6af1f", + "h_after_message1": "1fdebd6ec8abf0b58b949c296a3a1d8aba2023087e81f96a37a2c319b5bb47ec", + "message1": "3ba31c49f8186b67851aee28dad3688d20e5b38995cd53193d332f03b145960f0ad750d42ff823ed2fa9111237c6af1f", + "noise1_kind": "0x02", + "noise1_record": "0031023ba31c49f8186b67851aee28dad3688d20e5b38995cd53193d332f03b145960f0ad750d42ff823ed2fa9111237c6af1f", + "e_priv_s": "e9948f4fd3e704551937595a00d3fe9b3fc018838941dc752e9cb6b9d63aba1b", + "e_pub_s": "27c5f172bf3ac8d482b86c452d5a6b1d02ba7aa31786a4e5ad04e805c272db3a", + "h_after_e_s": "4894fca1d7c42c42d73e730b00c8c51c77b3be058c8bb63467df4a1f69b8af26", + "dh_ee": "3f592867b0e2821dc88dc14923ddf403ad7447ab4868207a60b812de220d5b14", + "mix_key_ee": { + "temp_key": "805df8f644c22395a76b05d27bbb14755351c617b6f78938e003c546fad418d2", + "ck": "c304ad50ba19611d155399f774a1ee642cba48d491a9c34ea98d53e561203206", + "temp_k": "9ba3fecf8e71d6fb34777e828a18fe3aad59284a3b3122e0b7c1850dd5041185" + }, + "ck_after_ee": "c304ad50ba19611d155399f774a1ee642cba48d491a9c34ea98d53e561203206", + "k_message2": "9ba3fecf8e71d6fb34777e828a18fe3aad59284a3b3122e0b7c1850dd5041185", + "ciphertext_tag_2": "491f6def3a79e9abe9ccc8cc6651e16a", + "h_final": "1cb5548c90a38400772f804bbc734d5b72a4259b58aae26c19c2dae533113066", + "message2": "27c5f172bf3ac8d482b86c452d5a6b1d02ba7aa31786a4e5ad04e805c272db3a491f6def3a79e9abe9ccc8cc6651e16a", + "noise2_kind": "0x03", + "noise2_record": "00310327c5f172bf3ac8d482b86c452d5a6b1d02ba7aa31786a4e5ad04e805c272db3a491f6def3a79e9abe9ccc8cc6651e16a", + "c1": "6f28e2cbf21a0db47559d8a1b9632c1039d05c8de44738d92aac02c4db101637", + "c2": "8e9590365dcb92771ad591311f68e4246341cfc38daa405c41b624002c63ba3b", + "c1_note": "c1 mã hoá bởi C (khởi tạo), giải mã bởi S", + "c2_note": "c2 mã hoá bởi S, giải mã bởi C" + } + ], + "invalid_vectors": [ + { + "name": "cặp 1 / S thử psk của cặp 2", + "reason": "wrong_key", + "n_s": "2cb3744ccb10882cef6e96d024d80bff5601132eced64c9ab09f7605d7bb2651", + "e_pub_c": "3ba31c49f8186b67851aee28dad3688d20e5b38995cd53193d332f03b145960f", + "ciphertext_tag_1": "0ad750d42ff823ed2fa9111237c6af1f", + "wrong_psk_source": "k_bt_link của cặp 2", + "wrong_psk": "fcdddc4e4638ed616aba885ef26218e63e14f091f1e0c9ec840366877fea3d7d" + }, + { + "name": "cặp 1 / tag noise1 sai", + "reason": "tag_mismatch_noise1", + "n_s": "2cb3744ccb10882cef6e96d024d80bff5601132eced64c9ab09f7605d7bb2651", + "e_pub_c": "3ba31c49f8186b67851aee28dad3688d20e5b38995cd53193d332f03b145960f", + "ciphertext_tag_1": "0ad750d42ff823ed2fa9111237c6af1e" + }, + { + "name": "cặp 1 / tag noise2 sai", + "reason": "tag_mismatch_noise2", + "n_s": "2cb3744ccb10882cef6e96d024d80bff5601132eced64c9ab09f7605d7bb2651", + "e_pub_s": "27c5f172bf3ac8d482b86c452d5a6b1d02ba7aa31786a4e5ad04e805c272db3a", + "ciphertext_tag_2": "491f6def3a79e9abe9ccc8cc6651e16b" + }, + { + "name": "cặp 1 / phát lại noise1 trên kênh khác (n_s khác)", + "reason": "replayed_channel", + "n_s": "80982dfa4fb88f90dc856740bca0043664726aa1d2231f940819449553d808f1", + "e_pub_c": "3ba31c49f8186b67851aee28dad3688d20e5b38995cd53193d332f03b145960f", + "ciphertext_tag_1": "0ad750d42ff823ed2fa9111237c6af1f" + } + ] +} diff --git a/tools/bench/README.md b/tools/bench/README.md index 865e2b5..e687c6d 100644 --- a/tools/bench/README.md +++ b/tools/bench/README.md @@ -71,6 +71,14 @@ Values never contain spaces. Anything before the marker `HLBENCH/1 ` (logcat or | `net` | Any | OS network change: Android `ConnectivityManager.NetworkCallback`, Apple `NWPathMonitor` | `change`: `up` (a network is available), `down` (lost), `changed` (default network switched) | | `wake` | Mac | `NSWorkspace.didWakeNotification` | — | +### Bluetooth and Wi-Fi Direct events (Phase 7, 0.4.5/0.4.6) + +| `ev` | Who | When | Fields | +|------|-----|------|--------| +| `bt_seen` | Mac (M-APP, central) | A BLE scan result matches a candidate pair's rotating hint or `BT_PIN_HINT` (0.4.5 "Advertisement") — a reconnect trigger, like `wake`/`net up` | — | +| `bt_lost` | Either side | An L2CAP channel closes (idle timeout, `reject`, a key-confirmation failure, the OS tearing it down) | `reason` (`idle_timeout`, `reject`, `key_confirmation`, `os`) — informational only, never a `reconnect_time.py` trigger: the loss itself is already in `state` | +| `wifi_assoc` | Mac | The Mac associates with or leaves the phone's Wi-Fi Direct group (0.4.6) | `change`: `up` (joined), `down` (left) — a reconnect trigger like `net` | + ### SMS events (Phase 2) Privacy as above: only the provider's `message_key` (`sms:<_id>`), the random `local_id`, states and error codes — never the text, the number or a name. diff --git a/tools/bench/README.vi.md b/tools/bench/README.vi.md index 7800dd4..0dfca6b 100644 --- a/tools/bench/README.vi.md +++ b/tools/bench/README.vi.md @@ -71,6 +71,14 @@ Giá trị không chứa khoảng trắng. Mọi thứ trước dấu `HLBENCH/1 | `net` | Mọi thiết bị | Hệ điều hành báo mạng đổi: Android `ConnectivityManager.NetworkCallback`, Apple `NWPathMonitor` | `change`: `up` (có mạng), `down` (mất mạng), `changed` (đổi mạng mặc định) | | `wake` | Mac | `NSWorkspace.didWakeNotification` | — | +### Sự kiện Bluetooth và Wi-Fi Direct (Phase 7, 0.4.5/0.4.6) + +| `ev` | Ai | Khi nào | Trường | +|------|----|---------|--------| +| `bt_seen` | Mac (M-APP, vai trung tâm) | Kết quả quét BLE khớp hint xoay vòng của một cặp ứng viên hoặc `BT_PIN_HINT` (0.4.5 "Advertisement") — một trigger kết nối lại, như `wake`/`net up` | — | +| `bt_lost` | Bên nào cũng được | Một kênh L2CAP đóng (hết giờ idle, `reject`, xác thực khóa thất bại, hệ điều hành tự đóng) | `reason` (`idle_timeout`, `reject`, `key_confirmation`, `os`) — chỉ để biết, không bao giờ là trigger của `reconnect_time.py`: việc mất kết nối đã có trong `state` rồi | +| `wifi_assoc` | Mac | Mac vào hoặc ra khỏi group Wi-Fi Direct của điện thoại (0.4.6) | `change`: `up` (đã vào), `down` (đã ra) — một trigger kết nối lại như `net` | + ### Sự kiện SMS (Phase 2) Quyền riêng tư như trên: chỉ `message_key` của provider (`sms:<_id>`), `local_id` ngẫu nhiên, trạng thái và mã lỗi — không bao giờ có nội dung tin, số điện thoại hay tên. diff --git a/tools/bench/bench_log.py b/tools/bench/bench_log.py index b035fd4..89f6428 100644 --- a/tools/bench/bench_log.py +++ b/tools/bench/bench_log.py @@ -27,6 +27,11 @@ "state": {"to"}, "net": {"change"}, "wake": set(), + # Bluetooth link (P7, S7.1a): a scan match (reconnect trigger, like wake/net), a closed link (informational, + # not a trigger — the loss is already in `state`), and the Mac joining/leaving a Wi-Fi Direct group (trigger). + "bt_seen": set(), + "bt_lost": {"reason"}, + "wifi_assoc": {"change"}, # SMS (Phase 2): new SMS notification (SMS-02) and reply confirmed as Sent (SMS-04); see README.md. "sms_detected": {"msg", "box", "onchange"}, "sms_new_sent": {"msg", "peer", "via"}, diff --git a/tools/bench/reconnect_time.py b/tools/bench/reconnect_time.py index 63935b5..49c4ccc 100644 --- a/tools/bench/reconnect_time.py +++ b/tools/bench/reconnect_time.py @@ -3,10 +3,13 @@ python3 tools/bench/reconnect_time.py mac.log android.log [--offset A:B=MS] [--json] [--check] An episode runs on a client (Mac, iPhone, iPad) from a `state` event that leaves Connected to the next one that -enters Connected (00-common-specs 0.11). Its trigger is the last `net` event with change=up or changed, or the -last `wake` event, on any device inside the episode — times of other devices are put on the client's clock -(clock_sync.py). Reconnect time = Connected minus trigger; without a trigger (the phone's service restarted, -the pong timed out) only the time since the loss is shown. Target: under 3000 ms on the 95th percentile. +enters Connected (00-common-specs 0.11). Its trigger is the last `net` or `wifi_assoc` event with change=up or +changed, or the last `wake` or `bt_seen` event, on any device inside the episode — times of other devices are put +on the client's clock (clock_sync.py). `bt_seen` (a Bluetooth scan match, 0.4.5) and `wifi_assoc` (the Mac joining +a Wi-Fi Direct group, 0.4.6) are triggers like `wake`/`net up`; `bt_lost` is informational only (the loss itself +is already in `state`) and never a trigger. Reconnect time = Connected minus trigger; without a trigger (the +phone's service restarted, the pong timed out) only the time since the loss is shown. Target: under 3000 ms on +the 95th percentile. """ from __future__ import annotations @@ -41,7 +44,9 @@ class Episode: def episodes(log: Log, clocks: ClockModel) -> list[Episode]: out = [] clients = sorted({e.dev for e in log.events if e.ev == "state" and e.role in CLIENT_ROLES}) - triggers = [e for e in log.events if (e.ev == "net" and e.get("change") in ("up", "changed")) or e.ev == "wake"] + triggers = [e for e in log.events + if (e.ev in ("net", "wifi_assoc") and e.get("change") in ("up", "changed")) + or e.ev in ("wake", "bt_seen")] for client in clients: states = [e for e in log.events if e.dev == client and e.ev == "state"] lost: Event | None = None @@ -68,7 +73,7 @@ def _episode(client: str, up_since: Event, lost: Event, connected: Event, trigge if best is None: trigger, reconnect = "loss", None else: - what = "wake" if best.ev == "wake" else f"net {best.get('change')}" + what = best.ev if best.ev in ("wake", "bt_seen") else f"{best.ev} {best.get('change')}" trigger = f"{what} on {best.dev}" reconnect = (local_interval_ms(best, connected) if best.dev == client else connected.wall_ms - best_on_client) diff --git a/tools/bench/self_test.py b/tools/bench/self_test.py index 8854766..436cf60 100644 --- a/tools/bench/self_test.py +++ b/tools/bench/self_test.py @@ -75,7 +75,9 @@ def build_logs() -> tuple[dict, dict]: logs[IPAD].append(line(IPAD, 30_016.5, "ack_sent", clip=fwd, peer=PHONE, status="applied")) logs[PHONE].append(line(PHONE, 30_020.5, "ack_received", clip=fwd, peer=IPAD, status="applied")) truth["forward-ipad"] = 30_016 - 30_000 - # Reconnects on the Mac: its own Wi-Fi switch, then the phone's, then a pong timeout without trigger. + # Reconnects on the Mac: its own Wi-Fi switch, then the phone's, then a pong timeout without trigger, then a + # Bluetooth scan match (P7, 0.4.5), then the Mac joining a Wi-Fi Direct group (0.4.6); a bt_lost in between + # is informational only and must not be picked up as a trigger. for true_ms, ev, fields in [(0, "state", {"to": "Discovering"}), (400, "state", {"to": "Connected", "channel": "lan"}), (40_000, "net", {"change": "down"}), (40_100, "state", {"from": "Connected", "to": "Backoff"}), (41_900, "net", {"change": "up"}), (42_000, "state", {"to": "Discovering"}), @@ -83,10 +85,17 @@ def build_logs() -> tuple[dict, dict]: (60_000, "state", {"from": "Connected", "to": "Backoff"}), (64_700, "state", {"to": "Connected", "channel": "lan"}), (80_000, "state", {"from": "Connected", "to": "Backoff"}), - (80_900, "state", {"to": "Connected", "channel": "lan"})]: + (80_900, "state", {"to": "Connected", "channel": "lan"}), + (90_000, "state", {"from": "Connected", "to": "Backoff"}), + (91_500, "bt_seen", {}), + (92_200, "state", {"to": "Connected", "channel": "bluetooth"}), + (100_000, "state", {"from": "Connected", "to": "Backoff"}), + (101_000, "wifi_assoc", {"change": "up"}), + (101_800, "state", {"to": "Connected", "channel": "lan"}), + (101_900, "bt_lost", {"reason": "idle_timeout"})]: logs[MAC].append(line(MAC, true_ms, ev, **fields)) logs[PHONE].append(line(PHONE, 62_500, "net", change="up")) - truth["reconnect"] = [(43_150 - 41_900), (64_700 - 62_500), None] + truth["reconnect"] = [(43_150 - 41_900), (64_700 - 62_500), None, (92_200 - 91_500), (101_800 - 101_000)] # A clip the Mac refused (conflict, QC8): read and sent, never applied. lost = "0192f3e0-0000-7000-8000-00000000000f" logs[PHONE].append(line(PHONE, 50_000, "clip_read", clip=lost, kind="text", bytes=10, source="manual")) @@ -172,12 +181,18 @@ def main() -> int: eps = reconnect_time.episodes(log, clocks) got = [e.reconnect_ms for e in eps] - t.check("three reconnect episodes", len(eps) == 3, str(eps)) + t.check("five reconnect episodes", len(eps) == 5, str(eps)) t.check("Mac Wi-Fi switch", close(got[0], truth["reconnect"][0]) and eps[0].trigger == f"net up on {MAC}", str(eps[0])) t.check("phone Wi-Fi switch on the Mac's clock", close(got[1], truth["reconnect"][1]), str(eps[1])) t.check("episode without trigger keeps only the time since the loss", got[2] is None and close(eps[2].since_loss_ms, 900.0), str(eps[2])) + t.check("Bluetooth scan match (bt_seen) as trigger", + close(got[3], truth["reconnect"][3]) and eps[3].trigger == f"bt_seen on {MAC}", str(eps[3])) + t.check("Wi-Fi Direct group join (wifi_assoc) as trigger", + close(got[4], truth["reconnect"][4]) and eps[4].trigger == f"wifi_assoc up on {MAC}", str(eps[4])) + t.check("bt_lost event present and not counted as an extra episode", + len(log.of("bt_lost")) == 1 and log.of("bt_lost")[0].get("reason") == "idle_timeout", str(log.of("bt_lost"))) code, text = run([str(p) for p in paths] + ["--check"], reconnect_time.main) t.check("reconnect target met", code == 0 and "PASS" in text, text) diff --git a/tools/vectors/bt_frame_codec.py b/tools/vectors/bt_frame_codec.py new file mode 100644 index 0000000..579b3be --- /dev/null +++ b/tools/vectors/bt_frame_codec.py @@ -0,0 +1,46 @@ +"""Mã hoá/giải mã khung Bluetooth (00-common-specs.md 0.4.5 "Link records", "Transport records and channels") — +dùng cho cả phía sinh và phía kiểm (chỉ là phép toán đóng gói byte, không phải thuật toán mật mã, nên không cần +hai cài đặt độc lập — giống cách lv_cat/LEB128 của CPace được chia sẻ).""" + +PADDED_SIZES = (256, 1024, 4096) +TAG_LEN = 16 +RECORD_SIZES = tuple(p + TAG_LEN for p in PADDED_SIZES) # 272, 1040, 4112 +CHAN_CONTROL, CHAN_BULK = 0x01, 0x02 +FLAG_MORE, FLAG_LONG = 0x01, 0x02 +PREAUTH_RECORD_MAX = 64 + + +def padded_size(dlen: int) -> int: + """Cỡ nhỏ nhất trong {256, 1024, 4096} chứa đủ khung chan(1)+flags(1)+dlen(2)+data(dlen) (0.4.5).""" + frame_len = 4 + dlen + for size in PADDED_SIZES: + if frame_len <= size: + return size + raise ValueError(f"dlen={dlen} vượt cả 4096 (BT_FRAGMENT_MAX=4092 phải chặn trước khi gọi hàm này)") + + +def build_frame(chan: int, more: bool, long_lane: bool, data: bytes) -> bytes: + """chan(1) ‖ flags(1) ‖ dlen(2 BE) ‖ data ‖ padding 0, tới cỡ padded_size(len(data)).""" + flags = (FLAG_MORE if more else 0) | (FLAG_LONG if long_lane else 0) + header = bytes([chan, flags]) + len(data).to_bytes(2, "big") + plaintext = header + data + size = padded_size(len(data)) + return plaintext + bytes(size - len(plaintext)) + + +def parse_frame(plaintext: bytes) -> dict: + chan, flags = plaintext[0], plaintext[1] + dlen = int.from_bytes(plaintext[2:4], "big") + data = plaintext[4:4 + dlen] + padding = plaintext[4 + dlen:] + return {"chan": chan, "more": bool(flags & FLAG_MORE), "long": bool(flags & FLAG_LONG), "dlen": dlen, + "data": data, "padding": padding} + + +def build_record(rlen_body: bytes) -> bytes: + """rlen (uint16 BE) ‖ rlen bytes — khung chung cho mọi bản ghi (0.4.5 "Link records").""" + return len(rlen_body).to_bytes(2, "big") + rlen_body + + +def build_preauth_record(kind: int, body: bytes) -> bytes: + return build_record(bytes([kind]) + body) diff --git a/tools/vectors/bt_link_derivations.py b/tools/vectors/bt_link_derivations.py new file mode 100644 index 0000000..1121f9f --- /dev/null +++ b/tools/vectors/bt_link_derivations.py @@ -0,0 +1,24 @@ +"""K_bt_disc/K_bt_link dẫn từ `PRK` của pair-prk.json (00-common-specs.md 0.4.5 "Keys") — dùng để SINH vector. +Khác `K_bt_pdisc`/`K_bt_plink` (bt_pairing_derivations.py, dẫn từ `pairing_secret`, chỉ sống trong một QR): các +khoá này sống suốt đời cặp, dùng cho liên kết Bluetooth bình thường sau khi đã ghép đôi.""" +from handlive_protocol_derivations import hkdf, hmac256 + +DISC_INFO = b"handlive/v1/bt-discovery" +LINK_INFO = b"handlive/v1/bt-link" +HINT_LABEL = b"HLBTDISC1" + + +def k_bt_disc(prk: bytes) -> bytes: + """K_bt_disc = HKDF-SHA256(ikm = PRK, salt rỗng, info "handlive/v1/bt-discovery", L = 32).""" + return hkdf(prk, DISC_INFO, 32) + + +def k_bt_link(prk: bytes) -> bytes: + """K_bt_link = HKDF-SHA256(ikm = PRK, salt rỗng, info "handlive/v1/bt-link", L = 32).""" + return hkdf(prk, LINK_INFO, 32) + + +def disc_hint(k_bt_disc_key: bytes, epoch: int) -> bytes: + """hint quảng bá = 4 byte đầu của HMAC-SHA256(K_bt_disc, "HLBTDISC1" ‖ epoch int64 BE) (0.4.5 "Advertisement").""" + msg = HINT_LABEL + epoch.to_bytes(8, "big", signed=True) + return hmac256(k_bt_disc_key, msg)[:4] diff --git a/tools/vectors/build_bt_advertisement_vectors.py b/tools/vectors/build_bt_advertisement_vectors.py new file mode 100644 index 0000000..468ec4c --- /dev/null +++ b/tools/vectors/build_bt_advertisement_vectors.py @@ -0,0 +1,85 @@ +"""Dựng bt-advertisement.json: byte service data mà app tự kiểm soát trong scan response (0.4.5 "Advertisement") — +`ver` ‖ `psm` (uint16 LE) ‖ `hints` (4 × n, n ≤ BT_ADV_MAX_HINTS); hệ điều hành tự thêm phần còn lại của cấu trúc +AD (cờ, UUID 128-bit) nên không thuộc phạm vi vector này (xem #3337: service_data_bytes log 11, cộng 18 của OS +mới ra 29). Cộng `BT_PIN_HINT` khi đang mở cửa sổ ghép đôi PIN, và mẫu âm cho luật khớp hint ở client. +""" +import bt_link_derivations as BT + +H = bytes.fromhex +SPEC = "docs/detailed-design/00-common-specs.md" +BT_ADV_MAX_HINTS = 2 +BT_PIN_HINT = bytes(4) +AD_UUID_STRUCT_BYTES = 18 # AD length + AD type + UUID 128-bit, hệ điều hành tự thêm +VER = 0x01 + + +def _service_data(psm: int, hints: list) -> bytes: + return bytes([VER]) + psm.to_bytes(2, "little") + b"".join(hints) + + +def _scan_response_vector(name: str, psm: int, hints: list, pair_names: list) -> dict: + data = _service_data(psm, hints) + total = AD_UUID_STRUCT_BYTES + len(data) + return {"name": name, "kind": "scan_response", "ver": f"0x{VER:02x}", "psm": psm, "psm_hex": psm.to_bytes(2, "little").hex(), + "hints": [h.hex() for h in hints], "hint_pairs": pair_names, "service_data_bytes": data.hex(), + "service_data_len": len(data), "ad_uuid_struct_bytes": AD_UUID_STRUCT_BYTES, + "scan_response_total": total, "note": f"service_data_bytes = phần app tự kiểm soát; OS thêm " + f"{AD_UUID_STRUCT_BYTES} byte UUID/cờ nữa mới ra tổng {total} byte"} + + +def _negatives(disc: bytes) -> list[dict]: + out = [] + data = _service_data(23456, [BT.disc_hint(disc, 1000)]) + bad_ver = bytearray(data) + bad_ver[0] = 0x02 + out.append({"name": "ver khác 0x01", "reason": "wrong_version", "service_data_bytes": bytes(bad_ver).hex(), + "expect": "ver = 0x02 ≠ 0x01 → client bỏ qua toàn bộ advertisement, không xét hint"}) + + client_epoch = 1000 + accepted = list(range(client_epoch - 4, client_epoch + 5)) + outside_epoch = client_epoch + 5 + outside_hint = BT.disc_hint(disc, outside_epoch) + out.append({"name": "hint ngoài cửa sổ e-4..e+4", "reason": "hint_outside_window", + "client_epoch": client_epoch, "accepted_epochs": accepted, "advertised_epoch": outside_epoch, + "advertised_hint": outside_hint.hex(), "service_data_bytes": _service_data(23456, [outside_hint]).hex(), + "expect": f"advertised_epoch = {outside_epoch} cách client_epoch {outside_epoch - client_epoch} kỷ " + "nguyên, ngoài [e-4, e+4] → client không thấy khớp hint nào"}) + return out + + +DESCRIPTION = ( + "Byte service data app tự kiểm soát trong scan response (0.4.5 \"Advertisement\"): `ver` (1, luôn 0x01) ‖ " + "`psm` (2, uint16 LE, PSM L2CAP hiện tại) ‖ `hints` (4 × n, n = 0…BT_ADV_MAX_HINTS = 2, mỗi hint = 4 byte đầu " + "của HMAC-SHA256(K_bt_disc, \"HLBTDISC1\" ‖ epoch int64 BE)) — K_bt_disc/hint lấy từ bt-link.json. OS thêm 18 " + "byte cấu trúc AD (cờ + UUID 128-bit) để ra tổng scan response (21/25/29 byte cho n=0/1/2) — không thuộc " + "phạm vi vector này, chỉ ghi `scan_response_total` để đối chiếu bảng 0.4.5. `kind` = \"pin_window\": `hints` " + "= chỉ `BT_PIN_HINT` (0x00000000), đánh dấu cửa sổ ghép đôi PIN đang mở, giống nhau cho mọi điện thoại (AC9). " + "invalid_vectors: wrong_version (ver ≠ 0x01 → bỏ qua cả advertisement), hint_outside_window (hint tính ở kỷ " + "nguyên ngoài [e-4, e+4] của client → không khớp)." +) + + +def build(ctx: dict, files: dict) -> dict: + link_doc = files["bt-link.json"] + key_vectors = [v for v in link_doc["vectors"] if v["kind"] == "key"] + disc = H(next(v for v in key_vectors if v["name"] == "cặp 1")["k_bt_disc"]) + disc2 = H(next(v for v in key_vectors if v["name"] == "cặp 2")["k_bt_disc"]) + + psm = 23456 + hint1, hint2 = BT.disc_hint(disc, 2000), BT.disc_hint(disc2, 2000) + vectors = [ + _scan_response_vector("0 hint (không cặp nào có session nào, không có cửa sổ QR)", psm, [], []), + _scan_response_vector("1 hint (một cặp gần đây nhất)", psm, [hint1], ["cặp 1"]), + _scan_response_vector("2 hint (BT_ADV_MAX_HINTS, hai cặp gần đây nhất)", psm, [hint1, hint2], + ["cặp 1", "cặp 2"]), + {"name": "cửa sổ PIN đang mở (BT_PIN_HINT)", "kind": "pin_window", "ver": f"0x{VER:02x}", "psm": psm, + "psm_hex": psm.to_bytes(2, "little").hex(), "hints": [BT_PIN_HINT.hex()], + "service_data_bytes": _service_data(psm, [BT_PIN_HINT]).hex(), + "service_data_len": len(_service_data(psm, [BT_PIN_HINT])), + "note": "BT_PIN_HINT giống nhau cho mọi điện thoại, không gắn với cặp hay thiết bị nào (AC9)"}, + ] + negatives = _negatives(disc) + doc = {"description": DESCRIPTION, + "source": f"{SPEC} 0.4.5 \"Advertisement\"; K_bt_disc của bt-link.json", + "vectors": vectors, "invalid_vectors": negatives} + return {"bt-advertisement.json": doc} diff --git a/tools/vectors/build_bt_frame_vectors.py b/tools/vectors/build_bt_frame_vectors.py new file mode 100644 index 0000000..bd9fdec --- /dev/null +++ b/tools/vectors/build_bt_frame_vectors.py @@ -0,0 +1,136 @@ +"""Dựng bt-frame.json: khung bản ghi transport (chan/flags/dlen/data/padding, 0.4.5 "Transport records and +channels") mã hoá bằng `c1` của bt-link.json (Noise transport message, nonce = bộ đếm 64-bit LE), cùng mẫu âm cho +từng luật cận mà A7.1a/M7.1b phải chặn: `rlen` ngoài {272, 1 040, 4 112}, `chan` lạ, `more` không có `long`, một +mảnh làn dài vượt `BT_BULK_FRAGMENT_MAX`, một tin làn dài vượt `BT_MESSAGE_MAX` trước mảnh cuối, một bản ghi +pre-auth vượt `BT_PREAUTH_RECORD_MAX`. +""" +import bt_frame_codec as F +from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 +from handlive_protocol_derivations import test_bytes + +H = bytes.fromhex +SPEC = "docs/detailed-design/00-common-specs.md" +BT_FRAGMENT_MAX = 4092 +BT_BULK_FRAGMENT_MAX = 1020 +BT_MESSAGE_MAX = 256 * 1024 +BT_PREAUTH_RECORD_MAX = 64 + + +def _nonce(counter: int) -> bytes: + return bytes(4) + counter.to_bytes(8, "little") + + +def _encrypt(key: bytes, counter: int, plaintext: bytes) -> bytes: + return ChaCha20Poly1305(key).encrypt(_nonce(counter), plaintext, b"") + + +def _frame_vector(name: str, chan: int, more: bool, long_lane: bool, data: bytes, key: bytes, counter: int) -> dict: + plaintext = F.build_frame(chan, more, long_lane, data) + ciphertext_tag = _encrypt(key, counter, plaintext) + record = F.build_record(ciphertext_tag) + return {"name": name, "kind": "transport_frame", "chan": f"0x{chan:02x}", "more": more, "long": long_lane, + "dlen": len(data), "data": data.hex(), "plaintext": plaintext.hex(), "padded_size": len(plaintext), + "key_source": "c1 của bt-link.json", "counter": counter, "nonce": _nonce(counter).hex(), + "ciphertext_tag": ciphertext_tag.hex(), "rlen": len(ciphertext_tag), "record": record.hex()} + + +def _preauth_vectors() -> list[dict]: + challenge_body = bytes([0x01]) + test_bytes("bt frame preauth example n_s", 32) + reject_body = bytes([0x7F]) + return [ + {"name": "challenge (ví dụ đóng gói)", "kind": "preauth_frame", "preauth_kind": "0x01", + "body": challenge_body.hex(), "record": F.build_preauth_record(0x01, challenge_body[1:]).hex()}, + {"name": "reject (ví dụ đóng gói, thân rỗng)", "kind": "preauth_frame", "preauth_kind": "0x7F", + "body": reject_body.hex(), "record": F.build_preauth_record(0x7F, b"").hex()}, + ] + + +def _negatives(key: bytes) -> list[dict]: + out = [] + + good_plain = F.build_frame(F.CHAN_CONTROL, False, False, test_bytes("bt frame rlen_over_bound data", 100)) + good_ct = _encrypt(key, 900, good_plain) + assert len(good_ct) in F.RECORD_SIZES + bad_rlen_body = good_ct + b"\x00" # 273, không nằm trong {272,1040,4112} + out.append({"name": "rlen ngoài {272,1040,4112}", "reason": "rlen_out_of_bounds", + "rlen": len(bad_rlen_body), "record": F.build_record(bad_rlen_body).hex(), + "expect": "rlen không khớp 272/1040/4112 → đóng kênh ngay, không cấp phát đọc tiếp thân bản ghi"}) + + preauth_body = bytes([0x01]) + test_bytes("bt frame preauth_over_bound", BT_PREAUTH_RECORD_MAX) + assert len(preauth_body) == BT_PREAUTH_RECORD_MAX + 1 > BT_PREAUTH_RECORD_MAX + out.append({"name": "bản ghi pre-auth vượt BT_PREAUTH_RECORD_MAX", + "reason": "preauth_record_over_bound", "rlen": len(preauth_body), + "record": F.build_record(preauth_body).hex(), "bt_preauth_record_max": BT_PREAUTH_RECORD_MAX, + "expect": f"rlen = {len(preauth_body)} > BT_PREAUTH_RECORD_MAX ({BT_PREAUTH_RECORD_MAX}) → đóng " + "kênh ngay, không cấp phát"}) + + bad_chan_plain = bytes([0x03, 0x00]) + (0).to_bytes(2, "big") + bytes(252) + bad_chan_ct = _encrypt(key, 901, bad_chan_plain) + out.append({"name": "chan lạ (không 0x01/0x02)", "reason": "unknown_chan", "chan": "0x03", + "plaintext": bad_chan_plain.hex(), "record": F.build_record(bad_chan_ct).hex(), + "expect": "chan = 0x03 không phải 0x01 (control) hay 0x02 (bulk) → đóng liên kết"}) + + more_no_long_plain = bytes([F.CHAN_CONTROL, F.FLAG_MORE]) + (10).to_bytes(2, "big") + \ + test_bytes("bt frame more_without_long", 10) + bytes(256 - 14) + more_no_long_ct = _encrypt(key, 902, more_no_long_plain) + out.append({"name": "more=1 nhưng long=0 (làn ngắn)", "reason": "more_without_long", + "plaintext": more_no_long_plain.hex(), "record": F.build_record(more_no_long_ct).hex(), + "expect": "làn ngắn (long=0) không bao giờ có more=1 (một làn ngắn luôn đúng một mảnh) → đóng " + "liên kết"}) + + over_fragment_data = test_bytes("bt frame bulk_fragment_over_max data", BT_BULK_FRAGMENT_MAX + 1) + out.append({"name": f"mảnh làn dài có dlen > BT_BULK_FRAGMENT_MAX ({BT_BULK_FRAGMENT_MAX})", + "reason": "bulk_fragment_over_max", "chan": "0x01", "long": True, + "dlen": len(over_fragment_data), "bt_bulk_fragment_max": BT_BULK_FRAGMENT_MAX, + "expect": f"dlen = {len(over_fragment_data)} > BT_BULK_FRAGMENT_MAX ({BT_BULK_FRAGMENT_MAX}) trên " + "làn dài (long=1) hoặc làn bulk (chan=0x02) → đóng liên kết, không cần giải mã thân"}) + + prior = BT_MESSAGE_MAX - 500 + last_fragment_dlen = BT_BULK_FRAGMENT_MAX + out.append({"name": "tin làn dài vượt BT_MESSAGE_MAX trước mảnh cuối", "reason": "message_over_max", + "chan": "0x01", "long": True, "bt_message_max": BT_MESSAGE_MAX, + "prior_bytes_received": prior, "last_fragment_dlen": last_fragment_dlen, + "total_after_fragment": prior + last_fragment_dlen, + "expect": f"{prior} + {last_fragment_dlen} = {prior + last_fragment_dlen} > BT_MESSAGE_MAX " + f"({BT_MESSAGE_MAX}) trước khi mảnh này là mảnh cuối (more=0) → đóng liên kết"}) + return out + + +DESCRIPTION = ( + "Khung bản ghi pre-auth (`rlen` uint16 BE ‖ `rlen` byte, kind đầu tiên của thân — 0.4.5 \"Link records\") và " + "khung control/bulk sau bước 4 (chan(1) ‖ flags(1, bit0 more/bit1 long) ‖ dlen(2 BE) ‖ data ‖ 0-padding tới " + "256/1 024/4 096 byte, mã hoá Noise transport = ChaCha20-Poly1305(key, nonce = 4 zero ‖ bộ đếm 64-bit LE, " + "AAD rỗng) ‖ tag 16 byte — rlen hợp lệ duy nhất 272/1 040/4 112). `key_source` = `c1` của bt-link.json (hướng " + "C→S của cùng bắt tay). invalid_vectors khớp đúng 6 luật cận của 0.4.5/S7.1a: rlen_out_of_bounds (rlen không " + "thuộc {272,1040,4112} → đóng kênh trước khi cấp phát đọc thêm), preauth_record_over_bound (bản ghi pre-auth " + "vượt BT_PREAUTH_RECORD_MAX = 64 byte), unknown_chan (chan khác 0x01/0x02), more_without_long (more=1 trên " + "làn ngắn, long=0), bulk_fragment_over_max (dlen một mảnh làn dài/bulk vượt BT_BULK_FRAGMENT_MAX = 1 020), " + "message_over_max (tổng byte một tin làn dài vượt BT_MESSAGE_MAX = 256 KiB trước mảnh cuối)." +) + + +def build(ctx: dict, files: dict) -> dict: + link_doc = files["bt-link.json"] + handshake = next(v for v in link_doc["vectors"] if v["kind"] == "handshake") + c1 = H(handshake["c1"]) + + short_data = test_bytes("bt frame short control example", 40) + short_vec = _frame_vector("control / làn ngắn (long=0, 1 mảnh)", F.CHAN_CONTROL, False, False, short_data, + c1, 10) + + long_data_1 = test_bytes("bt frame long control example 1", BT_BULK_FRAGMENT_MAX) + long_vec_1 = _frame_vector("control / làn dài, mảnh 1/2 (more=1, long=1)", F.CHAN_CONTROL, True, True, + long_data_1, c1, 11) + long_data_2 = test_bytes("bt frame long control example 2", 400) + long_vec_2 = _frame_vector("control / làn dài, mảnh 2/2 cuối (more=0, long=1)", F.CHAN_CONTROL, False, True, + long_data_2, c1, 12) + + bulk_data = test_bytes("bt frame bulk example", BT_BULK_FRAGMENT_MAX) + bulk_vec = _frame_vector("bulk / một mảnh ảnh (more=1)", F.CHAN_BULK, True, False, bulk_data, c1, 13) + + vectors = [short_vec, long_vec_1, long_vec_2, bulk_vec] + _preauth_vectors() + negatives = _negatives(c1) + doc = {"description": DESCRIPTION, + "source": f"{SPEC} 0.4.5 \"Link records\", \"Transport records and channels\"; c1 của bt-link.json", + "vectors": vectors, "invalid_vectors": negatives} + return {"bt-frame.json": doc} diff --git a/tools/vectors/build_bt_link_vectors.py b/tools/vectors/build_bt_link_vectors.py new file mode 100644 index 0000000..3d1d6ff --- /dev/null +++ b/tools/vectors/build_bt_link_vectors.py @@ -0,0 +1,137 @@ +"""Dựng bt-link.json: K_bt_disc/K_bt_link (00-common-specs.md 0.4.5 "Keys"), hint quảng bá xoay vòng, và bắt tay +bảo mật liên kết đầy đủ (`challenge`/`noise1`/`noise2`, psk = `K_bt_link`) cho một liên kết Bluetooth bình thường +(sau khi đã ghép đôi) — khác bt-pairing.json (psk = `K_bt_plink`, chỉ cho liên kết ghép đôi QR). +""" +import bt_frame_codec as F +import bt_link_derivations as BT +import noise_nnpsk0 as N +from handlive_protocol_derivations import test_bytes + +H = bytes.fromhex +SPEC = "docs/detailed-design/00-common-specs.md" +PROLOGUE_LABEL = b"HLBT1" +BT_HINT_PERIOD_MS = 900_000 + + +def _key_vector(pv: dict) -> dict: + prk = H(pv["prk"]) + disc, link = BT.k_bt_disc(prk), BT.k_bt_link(prk) + return {"name": pv["name"], "kind": "key", "pair_id": pv["pair_id"], "prk_source": "pair-prk.json", + "prk": prk.hex(), "k_bt_disc_info": BT.DISC_INFO.decode(), "k_bt_disc": disc.hex(), + "k_bt_link_info": BT.LINK_INFO.decode(), "k_bt_link": link.hex()} + + +def _hint_vector(pv: dict, disc: bytes) -> dict: + """Hint ở 3 kỷ nguyên liên tiếp: client chấp nhận hint trong khoảng e-4..e+4 (0.4.5 "Advertisement").""" + epochs = [0, 1, 2, 479762] + hints = [{"epoch": e, "hint": BT.disc_hint(disc, e).hex()} for e in epochs] + return {"name": f"{pv['name']} / hint quảng bá", "kind": "hint", "pair_id": pv["pair_id"], + "k_bt_disc_source": pv["name"], "hint_label": BT.HINT_LABEL.decode(), + "bt_hint_period_ms": BT_HINT_PERIOD_MS, "epochs": hints} + + +def _handshake_vector(pv: dict, link: bytes) -> dict: + n_s = test_bytes(f"bt link n_s {pv['name']}", 32) + challenge_record = F.build_preauth_record(0x01, n_s) + prologue = PROLOGUE_LABEL + n_s + + e_priv_c = test_bytes(f"bt link e_priv_c {pv['name']}", 32) + m1 = N.first_message(link, prologue, e_priv_c) + opened = N.try_open_first_message(link, prologue, m1["e_pub"], m1["ciphertext_tag"]) + assert opened == b"", f"{pv['name']}: noise1 phải mở được với đúng psk" + noise1_record = F.build_preauth_record(0x02, m1["message1"]) + + e_priv_s = test_bytes(f"bt link e_priv_s {pv['name']}", 32) + m2 = N.second_message(m1["ck1"], m1["h4"], e_priv_s, m1["e_pub"]) + pt_c, ck2_c, h6_c = N.try_open_second_message(m1["ck1"], m1["h4"], e_priv_c, m2["e_pub"], m2["ciphertext_tag"]) + assert pt_c == b"" and ck2_c == m2["ck2"] and h6_c == m2["h6"], f"{pv['name']}: noise2 phải khớp cả hai phía" + noise2_record = F.build_preauth_record(0x03, m2["message2"]) + + split_s = N.split(m2["ck2"]) + split_c = N.split(ck2_c) + assert split_s == split_c, f"{pv['name']}: Split() phải khớp cả hai phía" + + return { + "name": f"{pv['name']} / bắt tay liên kết thường", "kind": "handshake", "pair_id": pv["pair_id"], + "psk_source": f"k_bt_link của {pv['name']}", "psk": link.hex(), "protocol_name": m1["protocol_name"].decode(), + "n_s": n_s.hex(), "prologue_label": PROLOGUE_LABEL.decode(), "prologue": prologue.hex(), + "challenge_kind": "0x01", "challenge_record": challenge_record.hex(), + "h0": m1["h0"].hex(), "h_after_prologue": m1["h1"].hex(), + "mix_key_and_hash_psk": {k: v.hex() for k, v in m1["mix_key_and_hash"].items()}, + "h_after_psk": m1["h2"].hex(), "ck_after_psk": m1["ck1"].hex(), "k_message1": m1["k"].hex(), + "e_priv_c": m1["e_priv"].hex(), "e_pub_c": m1["e_pub"].hex(), "h_after_e_c": m1["h3"].hex(), + "ciphertext_tag_1": m1["ciphertext_tag"].hex(), "h_after_message1": m1["h4"].hex(), + "message1": m1["message1"].hex(), "noise1_kind": "0x02", "noise1_record": noise1_record.hex(), + "e_priv_s": m2["e_priv"].hex(), "e_pub_s": m2["e_pub"].hex(), "h_after_e_s": m2["h5"].hex(), + "dh_ee": m2["dh"].hex(), "mix_key_ee": {k: v.hex() for k, v in m2["mix_key_ee"].items()}, + "ck_after_ee": m2["ck2"].hex(), "k_message2": m2["k"].hex(), "ciphertext_tag_2": m2["ciphertext_tag"].hex(), + "h_final": m2["h6"].hex(), "message2": m2["message2"].hex(), "noise2_kind": "0x03", + "noise2_record": noise2_record.hex(), "c1": split_s["c1"].hex(), "c2": split_s["c2"].hex(), + "c1_note": "c1 mã hoá bởi C (khởi tạo), giải mã bởi S", "c2_note": "c2 mã hoá bởi S, giải mã bởi C", + } + + +def _negatives(pv1: dict, pv2: dict, link1: bytes, link2: bytes, hs: dict) -> list[dict]: + n_s, prologue = H(hs["n_s"]), H(hs["prologue"]) + e_pub_c, ct1 = H(hs["e_pub_c"]), H(hs["ciphertext_tag_1"]) + e_pub_s, ct2 = H(hs["e_pub_s"]), H(hs["ciphertext_tag_2"]) + ck1, h4 = H(hs["ck_after_psk"]), H(hs["h_after_message1"]) + out = [] + + assert N.try_open_first_message(link2, prologue, e_pub_c, ct1) is None + out.append({"name": f"{pv1['name']} / S thử psk của {pv2['name']}", "reason": "wrong_key", "n_s": hs["n_s"], + "e_pub_c": hs["e_pub_c"], "ciphertext_tag_1": hs["ciphertext_tag_1"], + "wrong_psk_source": f"k_bt_link của {pv2['name']}", "wrong_psk": link2.hex()}) + + bad_ct1 = bytearray(ct1) + bad_ct1[-1] ^= 0x01 + assert N.try_open_first_message(link1, prologue, e_pub_c, bytes(bad_ct1)) is None + out.append({"name": f"{pv1['name']} / tag noise1 sai", "reason": "tag_mismatch_noise1", "n_s": hs["n_s"], + "e_pub_c": hs["e_pub_c"], "ciphertext_tag_1": bytes(bad_ct1).hex()}) + + bad_ct2 = bytearray(ct2) + bad_ct2[-1] ^= 0x01 + pt_bad, _, _ = N.try_open_second_message(ck1, h4, H(hs["e_priv_c"]), e_pub_s, bytes(bad_ct2)) + assert pt_bad is None + out.append({"name": f"{pv1['name']} / tag noise2 sai", "reason": "tag_mismatch_noise2", "n_s": hs["n_s"], + "e_pub_s": hs["e_pub_s"], "ciphertext_tag_2": bytes(bad_ct2).hex()}) + + other_n_s = test_bytes(f"bt link n_s kênh khác {pv1['name']}", 32) + assert N.try_open_first_message(link1, PROLOGUE_LABEL + other_n_s, e_pub_c, ct1) is None + out.append({"name": f"{pv1['name']} / phát lại noise1 trên kênh khác (n_s khác)", "reason": "replayed_channel", + "n_s": other_n_s.hex(), "e_pub_c": hs["e_pub_c"], "ciphertext_tag_1": hs["ciphertext_tag_1"]}) + return out + + +DESCRIPTION = ( + "K_bt_disc = HKDF-SHA256(ikm = PRK, salt rỗng, info \"handlive/v1/bt-discovery\", L 32); K_bt_link (tương tự, " + "info \"handlive/v1/bt-link\") — sống suốt đời cặp, khác K_bt_pdisc/K_bt_plink của bt-pairing.json (sống theo " + "một QR). kind \"hint\": hint quảng bá bình thường (không phải lúc ghép đôi) = 4 byte đầu của " + "HMAC-SHA256(K_bt_disc, \"HLBTDISC1\" ‖ epoch int64 BE), epoch = floor(now_ms / BT_HINT_PERIOD) — khác pr_bt " + "(đổi theo mỗi QR) vì hint này đổi theo thời gian (0.4.5 \"Advertisement\"). kind \"handshake\": bắt tay bảo " + "mật liên kết đầy đủ (0.4.5 \"Link-security handshake\" bước 1-4), psk = K_bt_link: challenge (kind 0x01, n_s " + "32 byte) rồi message 1 (\"psk, e\", kind 0x02 noise1) và message 2 (\"e, ee\", kind 0x03 noise2) của " + "Noise_NNpsk0_25519_ChaChaPoly_SHA256 (Noise Protocol Framework rev. 34), prologue = \"HLBT1\" ‖ n_s, rồi " + "Split() ra c1 (C mã hoá/S giải mã)/c2 (ngược lại) — hai CipherState của bước 4-5. KHÔNG phải vector công bố " + "của tài liệu Noise (xem ghi chú ở bt-pairing.json); noise_nnpsk0.py đã được Trang rà soát ở #3328 cho message " + "1, cần rà soát thêm phần message 2/Split mới. invalid_vectors: wrong_key, tag_mismatch_noise1, " + "tag_mismatch_noise2 (0.4.5 bước 4: 'C verifies its payload tag — failure → C closes the channel, CONN-05 E3'), " + "replayed_channel (0.4.5 bước 3: 'The prologue binds n_s, so a recorded noise1 never verifies on a new " + "channel')." +) + + +def build(ctx: dict) -> dict: + pairs = {p["name"]: p for p in ctx["pairs"]} + pv1, pv2 = pairs["cặp 1"], pairs["cặp 2"] + key_vectors = [_key_vector(pv1), _key_vector(pv2)] + disc = {v["name"]: H(v["k_bt_disc"]) for v in key_vectors} + link = {v["name"]: H(v["k_bt_link"]) for v in key_vectors} + hint_vectors = [_hint_vector(pv1, disc["cặp 1"])] + handshake = _handshake_vector(pv1, link["cặp 1"]) + negatives = _negatives(pv1, pv2, link["cặp 1"], link["cặp 2"], handshake) + doc = {"description": DESCRIPTION, + "source": f"{SPEC} 0.4.5 \"Keys\", \"Advertisement\", \"Link-security handshake\"; PRK của " + "pair-prk.json; Noise Protocol Framework rev. 34 (Noise_NNpsk0_25519_ChaChaPoly_SHA256)", + "vectors": key_vectors + hint_vectors + [handshake], "invalid_vectors": negatives} + return {"bt-link.json": doc} diff --git a/tools/vectors/generate_vectors.py b/tools/vectors/generate_vectors.py index 6f82a29..192fae1 100644 --- a/tools/vectors/generate_vectors.py +++ b/tools/vectors/generate_vectors.py @@ -10,6 +10,9 @@ sys.dont_write_bytecode = True # không để lại __pycache__ trong kho sys.path.insert(0, str(Path(__file__).resolve().parent)) +import build_bt_advertisement_vectors # noqa: E402 +import build_bt_frame_vectors # noqa: E402 +import build_bt_link_vectors # noqa: E402 import build_bt_pairing_vectors # noqa: E402 import build_clipboard_html_vectors # noqa: E402 import build_cpace_vectors # noqa: E402 @@ -36,6 +39,9 @@ def render_all() -> dict[str, str]: files.update(build_pair_handshake_vectors.build(ctx)) files.update(build_discovery_hint_vectors.build(ctx)) files.update(build_bt_pairing_vectors.build(ctx)) + files.update(build_bt_link_vectors.build(ctx)) + files.update(build_bt_frame_vectors.build(ctx, files)) + files.update(build_bt_advertisement_vectors.build(ctx, files)) files.update(build_clipboard_html_vectors.build()) files.update(build_cpace_vectors.build()) return {name: json.dumps(doc, indent=2, ensure_ascii=False) + "\n" for name, doc in sorted(files.items())} diff --git a/tools/vectors/noise_nnpsk0.py b/tools/vectors/noise_nnpsk0.py index 0f52de1..0c36ca6 100644 --- a/tools/vectors/noise_nnpsk0.py +++ b/tools/vectors/noise_nnpsk0.py @@ -1,18 +1,19 @@ -"""Noise_NNpsk0_25519_ChaChaPoly_SHA256 (Noise Protocol Framework rev. 34), chỉ message 1 (token "psk, e") — -"khung đầu" (first frame) của một liên kết ghép đôi qua Bluetooth (00-common-specs.md 0.4.5 "Link-security -handshake" bước 2–3, "Pairing mode"). HASHLEN = 32 (SHA-256) nên temp_k/temp_k1/temp_k2 không bị cắt (quy tắc cắt -chỉ áp dụng khi HASHLEN = 64). +"""Noise_NNpsk0_25519_ChaChaPoly_SHA256 (Noise Protocol Framework rev. 34): message 1 ("psk, e") và message 2 +("e, ee") đủ cho cả "khung đầu" ghép đôi (00-common-specs.md 0.4.5 "Link-security handshake" bước 2–3, "Pairing +mode") và bắt tay bảo mật liên kết đầy đủ (bước 1–5, psk = `K_bt_link`, dùng cho các cặp đã ghép). HASHLEN = 32 +(SHA-256) nên temp_k/temp_k1/temp_k2 không bị cắt (quy tắc cắt chỉ áp dụng khi HASHLEN = 64). Không phải vector công bố của tài liệu Noise — draft/tài liệu Noise không phát hành vector cho psk/prologue riêng của HandLive. Đây là cách HandLive dùng đúng thuật toán của tài liệu đó (InitializeSymmetric, MixHash, -MixKeyAndHash, EncryptAndHash) cho bộ psk/prologue của mình, nên cần một vòng rà soát riêng (không tự xác nhận được -bằng vector bên ngoài) trước khi A7.1a/M7.1b khoá theo. +MixKeyAndHash, MixKey, EncryptAndHash, Split) cho bộ psk/prologue của mình, nên cần một vòng rà soát riêng (không +tự xác nhận được bằng vector bên ngoài) trước khi A7.1a/M7.1b khoá theo. """ import hashlib import hmac as _hmac from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 +from handlive_protocol_derivations import x25519_dh as _x25519_dh from handlive_protocol_derivations import x25519_pub PROTOCOL_NAME = b"Noise_NNpsk0_25519_ChaChaPoly_SHA256" @@ -71,3 +72,52 @@ def try_open_first_message(psk: bytes, prologue: bytes, e_pub: bytes, ciphertext return ChaCha20Poly1305(k).decrypt(nonce, ciphertext_tag, h3) except Exception: return None + + +def mix_key(ck: bytes, ikm: bytes) -> dict: + """Token "ee"/"es"/"se"/"ss": MixKey (2 ngõ ra qua HMAC, KHÔNG MixHash — khác token "psk").""" + temp_key = _hmac_sha256(ck, ikm) + out1 = _hmac_sha256(temp_key, b"\x01") + out2 = _hmac_sha256(temp_key, out1 + b"\x02") + return {"temp_key": temp_key, "ck": out1, "temp_k": out2} + + +def second_message(ck1: bytes, h_after_message1: bytes, e_priv_s: bytes, e_pub_c: bytes) -> dict: + """Dựng message 2 ("e, ee") của S→C, tiếp theo sau `first_message`. `ck1`/`h_after_message1` = `ck1`/`h4` của + `first_message`. `message2` = e.pub của S (32) ‖ EncryptAndHash("") (16).""" + e_pub_s = x25519_pub(e_priv_s) + h5 = _sha256(h_after_message1 + e_pub_s) # MixHash(e.pub) của token "e" + dh = _x25519_dh(e_priv_s, e_pub_c) + ee_step = mix_key(ck1, dh) # token "ee" + ck2, k = ee_step["ck"], ee_step["temp_k"] + nonce = bytes(4) + (0).to_bytes(8, "little") # n = 0, k vừa được InitializeKey lại + ciphertext = ChaCha20Poly1305(k).encrypt(nonce, b"", h5) + h6 = _sha256(h5 + ciphertext) + return {"h5": h5, "dh": dh, "mix_key_ee": ee_step, "ck2": ck2, "k": k, "e_priv": e_priv_s, "e_pub": e_pub_s, + "nonce": nonce, "ciphertext_tag": ciphertext, "h6": h6, "message2": e_pub_s + ciphertext} + + +def try_open_second_message(ck1: bytes, h_after_message1: bytes, e_priv_c: bytes, e_pub_s: bytes, + ciphertext_tag: bytes) -> tuple[bytes | None, bytes | None, bytes | None]: + """Phía C: mở message 2. Trả (plaintext hoặc None, ck2, h6) — `ck2`/`h6` cần cho `split()` dù `plaintext` là + None (để caller tự quyết có đóng kênh hay không, không lộ gì qua số lượng giá trị trả về).""" + h5 = _sha256(h_after_message1 + e_pub_s) + dh = _x25519_dh(e_priv_c, e_pub_s) + ee_step = mix_key(ck1, dh) + ck2, k = ee_step["ck"], ee_step["temp_k"] + nonce = bytes(4) + (0).to_bytes(8, "little") + h6 = _sha256(h5 + ciphertext_tag) + try: + pt = ChaCha20Poly1305(k).decrypt(nonce, ciphertext_tag, h5) + except Exception: + pt = None + return pt, ck2, h6 + + +def split(ck2: bytes) -> dict: + """Split(): 2 CipherState, một cho mỗi hướng. `c1` dùng để C mã hoá/S giải mã; `c2` ngược lại (Noise rev. 34 + §5, "Split").""" + temp_key = _hmac_sha256(ck2, b"") + out1 = _hmac_sha256(temp_key, b"\x01") + out2 = _hmac_sha256(temp_key, out1 + b"\x02") + return {"temp_key": temp_key, "c1": out1, "c2": out2} diff --git a/tools/vectors/requirements.txt b/tools/vectors/requirements.txt index d0ac723..783ab2c 100644 --- a/tools/vectors/requirements.txt +++ b/tools/vectors/requirements.txt @@ -2,3 +2,4 @@ cryptography==50.0.1 PyNaCl==1.6.2 argon2-cffi==25.1.0 argon2-cffi-bindings==26.1.0 +noiseprotocol==0.3.1 diff --git a/tools/vectors/verify_bt_advertisement_checks.py b/tools/vectors/verify_bt_advertisement_checks.py new file mode 100644 index 0000000..69eb1c2 --- /dev/null +++ b/tools/vectors/verify_bt_advertisement_checks.py @@ -0,0 +1,79 @@ +"""Kiểm bt-advertisement.json: dẫn lại K_bt_disc/hint bằng verify_common (hashlib/hmac, độc lập với +bt_link_derivations.py dùng `cryptography`); so khớp byte service_data và luật cửa sổ epoch bằng số học thuần. +""" +from verify_common import H, hkdf, hmac256 + +DISC_INFO = b"handlive/v1/bt-discovery" +HINT_LABEL = b"HLBTDISC1" +VER = 0x01 +BT_ADV_MAX_HINTS = 2 +BT_PIN_HINT = bytes(4) +REASONS = {"wrong_version", "hint_outside_window"} + + +def _hint(disc: bytes, epoch: int) -> bytes: + return hmac256(disc, HINT_LABEL + epoch.to_bytes(8, "big", signed=True))[:4] + + +def _service_data(psm: int, hints: list) -> bytes: + return bytes([VER]) + psm.to_bytes(2, "little") + b"".join(hints) + + +def _check_scan_response(c, n: str, v: dict) -> None: + c.eq(f"{n} ver", v["ver"], f"0x{VER:02x}") + c.true(f"{n} số hint ≤ BT_ADV_MAX_HINTS", len(v["hints"]) <= BT_ADV_MAX_HINTS) + data = _service_data(v["psm"], [H(h) for h in v["hints"]]) + c.eq(f"{n} service_data_bytes", v["service_data_bytes"], data.hex()) + c.eq(f"{n} service_data_len", v["service_data_len"], len(data)) + c.eq(f"{n} scan_response_total = 18 + len(service_data)", v["scan_response_total"], + v["ad_uuid_struct_bytes"] + len(data)) + + +def _check_pin_window(c, n: str, v: dict) -> None: + c.eq(f"{n} chỉ có BT_PIN_HINT", v["hints"], [BT_PIN_HINT.hex()]) + data = _service_data(v["psm"], [BT_PIN_HINT]) + c.eq(f"{n} service_data_bytes", v["service_data_bytes"], data.hex()) + + +def _check_negative(c, n: str, v: dict, disc: bytes) -> None: + reason = v["reason"] + c.true(f"{n} reason nằm trong tập đã định", reason in REASONS) + if reason == "wrong_version": + data = H(v["service_data_bytes"]) + c.true(f"{n} byte đầu khác 0x01", data[0] != VER) + else: + advertised_hint = _hint(disc, v["advertised_epoch"]) + c.eq(f"{n} advertised_hint", v["advertised_hint"], advertised_hint.hex()) + diff = abs(v["advertised_epoch"] - v["client_epoch"]) + c.true(f"{n} kỷ nguyên cách client quá 4", diff > 4) + c.eq(f"{n} accepted_epochs = [client-4..client+4]", v["accepted_epochs"], + list(range(v["client_epoch"] - 4, v["client_epoch"] + 5))) + c.true(f"{n} advertised_epoch không nằm trong accepted_epochs", + v["advertised_epoch"] not in v["accepted_epochs"]) + + +def check_bt_advertisement(c, doc: dict, all_docs: dict) -> None: + link_doc = all_docs["bt-link.json"] + prk_doc = all_docs["pair-prk.json"] + pair1 = next(v for v in prk_doc["vectors"] if v["name"] == "cặp 1") + disc = hkdf(H(pair1["prk"]), DISC_INFO, 32) + key1 = next(v for v in link_doc["vectors"] if v["kind"] == "key" and v["name"] == "cặp 1") + c.eq("bt-advertisement: K_bt_disc khớp bt-link.json", disc.hex(), key1["k_bt_disc"]) + + kinds = set() + for v in doc["vectors"]: + n = f"bt-advertisement/{v['name']}" + kinds.add(v["kind"]) + if v["kind"] == "scan_response": + _check_scan_response(c, n, v) + elif v["kind"] == "pin_window": + _check_pin_window(c, n, v) + else: + c.true(f"{n} kind {v['kind']!r} không nằm trong tập đã định", False) + c.eq("bt-advertisement: có đủ loại vector", kinds, {"scan_response", "pin_window"}) + for v in doc["invalid_vectors"]: + _check_negative(c, f"bt-advertisement/{v['name']}", v, disc) + c.eq("bt-advertisement: đủ loại vector âm", {v["reason"] for v in doc["invalid_vectors"]}, REASONS) + + +CHECKS = {"bt-advertisement.json": check_bt_advertisement} diff --git a/tools/vectors/verify_bt_frame_checks.py b/tools/vectors/verify_bt_frame_checks.py new file mode 100644 index 0000000..d09da64 --- /dev/null +++ b/tools/vectors/verify_bt_frame_checks.py @@ -0,0 +1,107 @@ +"""Kiểm bt-frame.json bằng thư viện độc lập: ChaCha20-Poly1305 chéo `cryptography` + libsodium ietf (độc lập với +bt_frame_codec.py, vốn chỉ đóng gói byte không mã hoá); mọi luật cận (rlen, chan, more/long, BT_BULK_FRAGMENT_MAX, +BT_MESSAGE_MAX, BT_PREAUTH_RECORD_MAX) kiểm lại bằng số học thuần, không gọi lại bt_frame_codec.py. +""" +from cryptography.exceptions import InvalidTag +from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 +from nacl.bindings import crypto_aead_chacha20poly1305_ietf_decrypt +from nacl.exceptions import CryptoError + +from verify_common import H + +RECORD_SIZES = {272, 1040, 4112} +CHAN_VALID = {0x01, 0x02} +BT_BULK_FRAGMENT_MAX = 1020 +BT_MESSAGE_MAX = 256 * 1024 +BT_PREAUTH_RECORD_MAX = 64 +REASONS = {"rlen_out_of_bounds", "preauth_record_over_bound", "unknown_chan", "more_without_long", + "bulk_fragment_over_max", "message_over_max"} + + +def _decrypt_both(key: bytes, nonce: bytes, ciphertext_tag: bytes): + try: + a = ChaCha20Poly1305(key).decrypt(nonce, ciphertext_tag, b"") + except InvalidTag: + a = None + try: + b = crypto_aead_chacha20poly1305_ietf_decrypt(ciphertext_tag, b"", nonce, key) + except CryptoError: + b = None + return a, b + + +def _parse_plaintext(plaintext: bytes) -> dict: + chan, flags = plaintext[0], plaintext[1] + dlen = int.from_bytes(plaintext[2:4], "big") + return {"chan": chan, "more": bool(flags & 0x01), "long": bool(flags & 0x02), "dlen": dlen, + "data": plaintext[4:4 + dlen], "padding": plaintext[4 + dlen:]} + + +def _check_transport_frame(c, n: str, v: dict, c1: bytes) -> None: + nonce = bytes(4) + v["counter"].to_bytes(8, "little") + c.eq(f"{n} nonce = 4 byte 0 ‖ counter LE", v["nonce"], nonce.hex()) + a, b = _decrypt_both(c1, nonce, H(v["ciphertext_tag"])) + c.eq(f"{n} giải mã được (cryptography)", a, H(v["plaintext"])) + c.eq(f"{n} giải mã được (libsodium)", b, H(v["plaintext"])) + parsed = _parse_plaintext(H(v["plaintext"])) + c.eq(f"{n} chan", int(v["chan"], 16), parsed["chan"]) + c.eq(f"{n} more", v["more"], parsed["more"]) + c.eq(f"{n} long", v["long"], parsed["long"]) + c.eq(f"{n} dlen", v["dlen"], parsed["dlen"]) + c.eq(f"{n} data", v["data"], parsed["data"].hex()) + c.true(f"{n} padding toàn 0", parsed["padding"] == bytes(len(parsed["padding"]))) + c.eq(f"{n} rlen đúng tập {{272,1040,4112}}", v["rlen"] in RECORD_SIZES, True) + c.eq(f"{n} record = rlen(2BE) ‖ ciphertext_tag", H(v["record"]), + v["rlen"].to_bytes(2, "big") + H(v["ciphertext_tag"])) + + +def _check_preauth_frame(c, n: str, v: dict) -> None: + body = H(v["body"]) + kind = int(v["preauth_kind"], 16) + c.eq(f"{n} byte đầu = kind", body[0], kind) + c.eq(f"{n} record = rlen(2BE) ‖ body[1:] với kind riêng", H(v["record"]), + len(body).to_bytes(2, "big") + body) + + +def _check_negative(c, n: str, v: dict) -> None: + reason = v["reason"] + c.true(f"{n} reason nằm trong tập đã định", reason in REASONS) + if reason == "rlen_out_of_bounds": + c.true(f"{n} rlen không thuộc {{272,1040,4112}}", v["rlen"] not in RECORD_SIZES) + c.eq(f"{n} record đúng rlen ghi trong vector", H(v["record"])[:2], v["rlen"].to_bytes(2, "big")) + elif reason == "preauth_record_over_bound": + c.true(f"{n} rlen > BT_PREAUTH_RECORD_MAX", v["rlen"] > BT_PREAUTH_RECORD_MAX) + elif reason == "unknown_chan": + c.true(f"{n} chan không thuộc {{0x01,0x02}}", int(v["chan"], 16) not in CHAN_VALID) + elif reason == "more_without_long": + parsed = _parse_plaintext(H(v["plaintext"])) + c.true(f"{n} more=1 và long=0", parsed["more"] and not parsed["long"]) + elif reason == "bulk_fragment_over_max": + c.true(f"{n} dlen > BT_BULK_FRAGMENT_MAX", v["dlen"] > BT_BULK_FRAGMENT_MAX) + else: # message_over_max + c.eq(f"{n} tổng sau mảnh này", v["total_after_fragment"], v["prior_bytes_received"] + v["last_fragment_dlen"]) + c.true(f"{n} tổng vượt BT_MESSAGE_MAX", v["total_after_fragment"] > BT_MESSAGE_MAX) + + +def check_bt_frame(c, doc: dict, all_docs: dict) -> None: + link_doc = all_docs["bt-link.json"] + handshake = next(v for v in link_doc["vectors"] if v["kind"] == "handshake") + c1 = H(handshake["c1"]) + kinds = set() + for v in doc["vectors"]: + n = f"bt-frame/{v['name']}" + kinds.add(v["kind"]) + if v["kind"] == "transport_frame": + c.eq(f"{n} key_source là c1 của bt-link.json", v["key_source"], "c1 của bt-link.json") + _check_transport_frame(c, n, v, c1) + elif v["kind"] == "preauth_frame": + _check_preauth_frame(c, n, v) + else: + c.true(f"{n} kind {v['kind']!r} không nằm trong tập đã định", False) + c.eq("bt-frame: có đủ loại vector", kinds, {"transport_frame", "preauth_frame"}) + for v in doc["invalid_vectors"]: + _check_negative(c, f"bt-frame/{v['name']}", v) + c.eq("bt-frame: đủ loại vector âm", {v["reason"] for v in doc["invalid_vectors"]}, REASONS) + + +CHECKS = {"bt-frame.json": check_bt_frame} diff --git a/tools/vectors/verify_bt_link_checks.py b/tools/vectors/verify_bt_link_checks.py new file mode 100644 index 0000000..88a31eb --- /dev/null +++ b/tools/vectors/verify_bt_link_checks.py @@ -0,0 +1,178 @@ +"""Kiểm bt-link.json bằng thư viện độc lập với phía sinh: HKDF/HMAC qua verify_common (hashlib/hmac), X25519 qua +`x25519`/`x25519_base` (pynacl), ChaCha20Poly1305 kiểm chéo `cryptography` + libsodium ietf — độc lập với +noise_nnpsk0.py, giống verify_bt_pairing_checks.py nhưng thêm message 2 ("e, ee") và Split(). +""" +import hashlib +import hmac + +from cryptography.exceptions import InvalidTag +from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 +from nacl.bindings import crypto_aead_chacha20poly1305_ietf_decrypt +from nacl.exceptions import CryptoError + +from verify_common import H, hkdf, hmac256, x25519, x25519_base + +DISC_INFO = b"handlive/v1/bt-discovery" +LINK_INFO = b"handlive/v1/bt-link" +HINT_LABEL = b"HLBTDISC1" +PROTOCOL_NAME = b"Noise_NNpsk0_25519_ChaChaPoly_SHA256" +PROLOGUE_LABEL = b"HLBT1" +REASONS = {"wrong_key", "tag_mismatch_noise1", "tag_mismatch_noise2", "replayed_channel"} +ZERO_NONCE = bytes(12) + + +def _sha256(b: bytes) -> bytes: + return hashlib.sha256(b).digest() + + +def _hmac(key: bytes, msg: bytes) -> bytes: + return hmac.new(key, msg, hashlib.sha256).digest() + + +def _mix_key_and_hash(h: bytes, ck: bytes, ikm: bytes) -> tuple[bytes, bytes, bytes]: + """Token "psk": 3 ngõ ra qua HMAC (khác token "ee", chỉ 2 ngõ ra) — trả (h mới, ck mới, k mới).""" + temp_key = _hmac(ck, ikm) + out1 = _hmac(temp_key, b"\x01") + out2 = _hmac(temp_key, out1 + b"\x02") + out3 = _hmac(temp_key, out2 + b"\x03") + return _sha256(h + out2), out1, out3 + + +def _mix_key(ck: bytes, ikm: bytes) -> tuple[bytes, bytes]: + """Token "ee": trả (ck mới, k mới), KHÔNG đụng h.""" + temp_key = _hmac(ck, ikm) + out1 = _hmac(temp_key, b"\x01") + out2 = _hmac(temp_key, out1 + b"\x02") + return out1, out2 + + +def _open_chacha(k: bytes, aad: bytes, ciphertext_tag: bytes): + """Trả (mở bằng cryptography, mở bằng libsodium) — None khi tag không khớp.""" + try: + a = ChaCha20Poly1305(k).decrypt(ZERO_NONCE, ciphertext_tag, aad) + except InvalidTag: + a = None + try: + b = crypto_aead_chacha20poly1305_ietf_decrypt(ciphertext_tag, aad, ZERO_NONCE, k) + except CryptoError: + b = None + return a, b + + +def _open_message1(psk: bytes, prologue: bytes, e_pub_c: bytes, ciphertext_tag: bytes): + """Trả (mở cryptography, mở libsodium, h sau token "e" của C, ck sau token "psk").""" + h0 = _sha256(PROTOCOL_NAME) + h1 = _sha256(h0 + prologue) + h2, ck1, k1 = _mix_key_and_hash(h1, h0, psk) + h3 = _sha256(h2 + e_pub_c) + a, b = _open_chacha(k1, h3, ciphertext_tag) + return a, b, h3, ck1 + + +def _open_message2(ck1: bytes, h_after_m1: bytes, e_priv_c: bytes, e_pub_s: bytes, ciphertext_tag: bytes): + """Trả (mở cryptography, mở libsodium, h sau token "e" của S, ck sau token "ee").""" + h5 = _sha256(h_after_m1 + e_pub_s) + dh = x25519(e_priv_c, e_pub_s) + ck2, k2 = _mix_key(ck1, dh) + a, b = _open_chacha(k2, h5, ciphertext_tag) + return a, b, h5, ck2 + + +def _split(ck2: bytes) -> tuple[bytes, bytes]: + temp_key = _hmac(ck2, b"") + out1 = _hmac(temp_key, b"\x01") + out2 = _hmac(temp_key, out1 + b"\x02") + return out1, out2 + + +def _check_key(c, n: str, v: dict, pair_prk: dict) -> bytes: + pv = pair_prk[v["name"]] + c.eq(f"{n} PRK lấy từ pair-prk.json", (v["prk_source"], v["prk"]), ("pair-prk.json", pv["prk"])) + prk = H(v["prk"]) + disc, link = hkdf(prk, DISC_INFO, 32), hkdf(prk, LINK_INFO, 32) + c.eq(f"{n} K_bt_disc", v["k_bt_disc"], disc.hex()) + c.eq(f"{n} K_bt_link", v["k_bt_link"], link.hex()) + return link + + +def _check_hint(c, n: str, v: dict, disc: dict) -> None: + disc_key = disc[v["k_bt_disc_source"]] + for e in v["epochs"]: + msg = HINT_LABEL + e["epoch"].to_bytes(8, "big", signed=True) + c.eq(f"{n} hint epoch {e['epoch']}", e["hint"], hmac256(disc_key, msg)[:4].hex()) + + +def _check_handshake(c, n: str, v: dict) -> None: + psk, prologue = H(v["psk"]), H(v["prologue"]) + c.eq(f"{n} prologue = nhãn ‖ n_s", prologue, PROLOGUE_LABEL + H(v["n_s"])) + challenge_body = bytes([0x01]) + H(v["n_s"]) + c.eq(f"{n} challenge_record = rlen(2BE) ‖ kind(0x01) ‖ n_s", H(v["challenge_record"]), + len(challenge_body).to_bytes(2, "big") + challenge_body) + c.eq(f"{n} e_pub_c = X25519 base point của e_priv_c", v["e_pub_c"], x25519_base(H(v["e_priv_c"])).hex()) + + a1, b1, h3, ck1 = _open_message1(psk, prologue, H(v["e_pub_c"]), H(v["ciphertext_tag_1"])) + c.eq(f"{n} h_after_e_c", h3.hex(), v["h_after_e_c"]) + c.eq(f"{n} mở được noise1 (cryptography)", a1, b"") + c.eq(f"{n} mở được noise1 (libsodium)", b1, b"") + c.eq(f"{n} message1 = e_pub_c ‖ ciphertext_tag_1", v["message1"], v["e_pub_c"] + v["ciphertext_tag_1"]) + noise1_body = bytes([0x02]) + H(v["message1"]) + c.eq(f"{n} noise1_record = rlen(2BE) ‖ kind(0x02) ‖ message1", H(v["noise1_record"]), + len(noise1_body).to_bytes(2, "big") + noise1_body) + + c.eq(f"{n} e_pub_s = X25519 base point của e_priv_s", v["e_pub_s"], x25519_base(H(v["e_priv_s"])).hex()) + a2, b2, h5, ck2 = _open_message2(ck1, H(v["h_after_message1"]), H(v["e_priv_c"]), H(v["e_pub_s"]), + H(v["ciphertext_tag_2"])) + c.eq(f"{n} h_after_e_s", h5.hex(), v["h_after_e_s"]) + c.eq(f"{n} mở được noise2 (cryptography)", a2, b"") + c.eq(f"{n} mở được noise2 (libsodium)", b2, b"") + c.eq(f"{n} ck_after_ee", ck2.hex(), v["ck_after_ee"]) + c.eq(f"{n} message2 = e_pub_s ‖ ciphertext_tag_2", v["message2"], v["e_pub_s"] + v["ciphertext_tag_2"]) + noise2_body = bytes([0x03]) + H(v["message2"]) + c.eq(f"{n} noise2_record = rlen(2BE) ‖ kind(0x03) ‖ message2", H(v["noise2_record"]), + len(noise2_body).to_bytes(2, "big") + noise2_body) + + c1, c2 = _split(ck2) + c.eq(f"{n} Split() c1", v["c1"], c1.hex()) + c.eq(f"{n} Split() c2", v["c2"], c2.hex()) + c.true(f"{n} c1 != c2", c1 != c2) + + +def _check_negative(c, n: str, v: dict, hs: dict, link: dict) -> None: + reason = v["reason"] + c.true(f"{n} reason nằm trong tập đã định", reason in REASONS) + if reason == "tag_mismatch_noise2": + ck1 = H(hs["ck_after_psk"]) + a, b, _, _ = _open_message2(ck1, H(hs["h_after_message1"]), H(hs["e_priv_c"]), H(v["e_pub_s"]), + H(v["ciphertext_tag_2"])) + c.eq(f"{n} phải bị từ chối (cả hai đường)", (a, b), (None, None)) + return + prologue = PROLOGUE_LABEL + H(v["n_s"]) + psk = H(v["wrong_psk"]) if reason == "wrong_key" else link["cặp 1"] + a, b, _, _ = _open_message1(psk, prologue, H(v["e_pub_c"]), H(v["ciphertext_tag_1"])) + c.eq(f"{n} phải bị từ chối (cả hai đường)", (a, b), (None, None)) + + +def check_bt_link(c, doc: dict, all_docs: dict) -> None: + pair_prk = {v["name"]: v for v in all_docs["pair-prk.json"]["vectors"]} + disc, link, kinds = {}, {}, set() + handshake = None + for v in doc["vectors"]: + n = f"bt-link/{v['name']}" + kinds.add(v["kind"]) + if v["kind"] == "key": + disc[v["name"]] = H(v["k_bt_disc"]) + link[v["name"]] = _check_key(c, n, v, pair_prk) + elif v["kind"] == "hint": + _check_hint(c, n, v, disc) + elif v["kind"] == "handshake": + _check_handshake(c, n, v) + handshake = v + else: + c.true(f"{n} kind {v['kind']!r} không nằm trong tập đã định", False) + c.eq("bt-link: có đủ loại vector", kinds, {"key", "hint", "handshake"}) + for v in doc["invalid_vectors"]: + _check_negative(c, f"bt-link/{v['name']}", v, handshake, link) + c.eq("bt-link: đủ loại vector âm", {v["reason"] for v in doc["invalid_vectors"]}, REASONS) + + +CHECKS = {"bt-link.json": check_bt_link} diff --git a/tools/vectors/verify_noise_library_checks.py b/tools/vectors/verify_noise_library_checks.py new file mode 100644 index 0000000..f96ed74 --- /dev/null +++ b/tools/vectors/verify_noise_library_checks.py @@ -0,0 +1,48 @@ +"""Kiểm chéo bắt tay Noise_NNpsk0_25519_ChaChaPoly_SHA256 trên vector `handshake` của bt-link.json bằng +`noiseprotocol` (PyPI, pin ở tools/vectors/requirements.txt) — thư viện Noise ĐỘC LẬP HOÀN TOÀN với +noise_nnpsk0.py (không chỉ đổi primitive như verify_bt_link_checks.py, mà chạy lại chính thuật toán Noise bằng +mã người khác viết), theo ghi chú của Trang ở #3328 và yêu cầu của Lead ở #3376/#3380. + +THEO YÊU CẦU CỦA LEAD: thiếu `noiseprotocol` phải báo lỗi rõ, không âm thầm bỏ qua — môi trường chưa chạy +`pip install -r requirements.txt` (có `noiseprotocol==0.3.1`) sẽ thấy lỗi ImportError ở đây, không phải một dòng +"bỏ qua" lặng lẽ. + +Chưa tự chạy được trong sandbox của Tuan (không có mạng để pip install) — ghi lại trong task #189: cần chạy sau +khi `tools/setup-cpace-refs.sh` cài xong `noiseprotocol` để xác nhận API dưới đây (từ hiểu biết về thư viện, +chưa tự kiểm) khớp với phiên bản 0.3.1 thật. +""" +from verify_common import H + +PROTOCOL_NAME = "Noise_NNpsk0_25519_ChaChaPoly_SHA256" + + +def _side(role: str, psk: bytes, prologue: bytes, e_priv: bytes): + from noise.connection import Keypair, NoiseConnection # import trong hàm: báo lỗi rõ đúng tại bước này, + # không chặn mọi phép kiểm khác của verify_vectors.py khi thiếu thư viện (xem lời gọi trong verify_vectors.py). + conn = NoiseConnection.from_name(PROTOCOL_NAME.encode()) + conn.set_as_initiator() if role == "initiator" else conn.set_as_responder() + conn.set_psks(psk) + conn.set_prologue(prologue) + conn.set_keypair_from_private_bytes(Keypair.EPHEMERAL, e_priv) + conn.start_handshake() + return conn + + +def check_bt_link_with_noiseprotocol(c, doc: dict, all_docs: dict) -> None: + handshake = next(v for v in doc["vectors"] if v["kind"] == "handshake") + n = f"bt-link/noiseprotocol/{handshake['name']}" + psk, prologue = H(handshake["psk"]), H(handshake["prologue"]) + e_priv_c, e_priv_s = H(handshake["e_priv_c"]), H(handshake["e_priv_s"]) + + initiator = _side("initiator", psk, prologue, e_priv_c) + responder = _side("responder", psk, prologue, e_priv_s) + + message1 = initiator.write_message(b"") + c.eq(f"{n} message1 (noiseprotocol khởi tạo)", message1, H(handshake["message1"])) + responder.read_message(message1) + + message2 = responder.write_message(b"") + c.eq(f"{n} message2 (noiseprotocol trả lời)", message2, H(handshake["message2"])) + initiator.read_message(message2) + + c.true(f"{n} cả hai phía xong bắt tay", initiator.handshake_finished and responder.handshake_finished) diff --git a/tools/vectors/verify_vectors.py b/tools/vectors/verify_vectors.py index 1ed08e4..92ec032 100644 --- a/tools/vectors/verify_vectors.py +++ b/tools/vectors/verify_vectors.py @@ -11,11 +11,15 @@ sys.dont_write_bytecode = True # không để lại __pycache__ trong kho sys.path.insert(0, str(Path(__file__).resolve().parent)) +import verify_bt_advertisement_checks # noqa: E402 +import verify_bt_frame_checks # noqa: E402 +import verify_bt_link_checks # noqa: E402 import verify_bt_pairing_checks # noqa: E402 import verify_clipboard_html_checks # noqa: E402 import verify_cpace_checks # noqa: E402 import verify_discovery_hint_checks # noqa: E402 import verify_message_checks # noqa: E402 +import verify_noise_library_checks # noqa: E402 import verify_pair_handshake_checks # noqa: E402 import verify_primitive_checks # noqa: E402 import verify_push_relay_checks # noqa: E402 @@ -33,7 +37,9 @@ def main() -> int: checks = {**verify_primitive_checks.CHECKS, **verify_session_checks.CHECKS, **verify_message_checks.CHECKS, **verify_signature_checks.CHECKS, **verify_pair_handshake_checks.CHECKS, **verify_discovery_hint_checks.CHECKS, **verify_push_relay_checks.CHECKS, - **verify_revoke_checks.CHECKS, **verify_bt_pairing_checks.CHECKS, **verify_cpace_checks.CHECKS} + **verify_revoke_checks.CHECKS, **verify_bt_pairing_checks.CHECKS, **verify_cpace_checks.CHECKS, + **verify_bt_link_checks.CHECKS, **verify_bt_frame_checks.CHECKS, + **verify_bt_advertisement_checks.CHECKS} c = Checker() for name in checks: c.true(f"{name} tồn tại", name in docs) @@ -62,6 +68,16 @@ def main() -> int: c.failures.append(f"clipboard-html.json: ngoại lệ {type(exc).__name__}: {exc}") print(f"{'OK ' if len(c.failures) == before[1] else 'SAI'} {verify_clipboard_html_checks.NAME:26s} " f"{len(html_doc['cases']):2d} case, {c.count - before[0]:3d} phép kiểm") + link_doc, before = docs.get("bt-link.json"), (c.count, len(c.failures)) + if link_doc is not None: + try: + verify_noise_library_checks.check_bt_link_with_noiseprotocol(c, link_doc, docs) + status = "OK " if len(c.failures) == before[1] else "SAI" + except ImportError as exc: + c.failures.append(f"bt-link.json/noiseprotocol: thiếu thư viện độc lập (pip install -r " + f"tools/vectors/requirements.txt, có noiseprotocol==0.3.1) — {exc}") + status = "LỖI (thiếu thư viện)" + print(f"{status} bt-link.json/noiseprotocol {c.count - before[0]:3d} phép kiểm") for f in c.failures: print(f" LỖI: {f}") print(f"Tổng: {c.count} phép kiểm, {len(c.failures)} lỗi") From b53814100e526e73a8be550fc80749bb9310133f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Xu=C3=A2n=20D=C5=A9ng?= Date: Wed, 28 Oct 2026 18:20:29 +0700 Subject: [PATCH 5/7] fix(shared): add the missing MixKey(e.pub) of Noise's psk mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Noise rev. 34 §9.2: in a psk-modified pattern, every "e" token does MixHash(e.public_key) and then ALSO MixKey(e.public_key) — unlike a plain pattern, where "e" only does MixHash. noise_nnpsk0.py only did the MixHash half, so message 1 encrypted its payload with the "psk" token's key instead of the key that follows "e"'s own MixKey, and the chaining key carried into message 2's "ee" step (and from there into Split()) was wrong throughout, even though e/e.pub themselves never changed on the wire. Caught by cross-checking bt-link.json's handshake against the real `noiseprotocol` library (message1/message2 tag mismatch, e matched); message1, message2, handshake_hash and both Split() keys now agree byte-exact with that independent implementation. Fixes both message 1 ("psk, e", shared with bt-pairing.json's first frame) and message 2 ("e, ee", bt-link.json only); bt-frame.json's transport records are regenerated too since they are encrypted with bt-link.json's c1. --- test-vectors/bt-frame.json | 22 ++++---- test-vectors/bt-link.json | 53 ++++++++++++-------- test-vectors/bt-pairing.json | 20 ++++---- tools/vectors/build_bt_link_vectors.py | 9 ++-- tools/vectors/noise_nnpsk0.py | 44 ++++++++++------ tools/vectors/verify_bt_link_checks.py | 19 ++++--- tools/vectors/verify_bt_pairing_checks.py | 22 +++++--- tools/vectors/verify_noise_library_checks.py | 18 +++++-- 8 files changed, 130 insertions(+), 77 deletions(-) diff --git a/test-vectors/bt-frame.json b/test-vectors/bt-frame.json index 9ea36f4..9886d2b 100644 --- a/test-vectors/bt-frame.json +++ b/test-vectors/bt-frame.json @@ -15,9 +15,9 @@ "key_source": "c1 của bt-link.json", "counter": 10, "nonce": "000000000a00000000000000", - "ciphertext_tag": "dfae9f81fb088d15ab042a4607905a15100ca1395a062722a0fb701b2018173a52d0d1f572a08d62272d20cd22d3dd16a763f79a5982e53af8933cfa4d18be6bfeea8d42deed30c5f952acfccfbf79991163d10fe3ea5a2513f759a32e22ff5f6c38f10f9e6a98bda304a5be4f4891c06e13de7cdbfd8b7aca424a45ac2be603cb158fe24261192950f24755782d9d873de4862a50ca3967d3e67126e1f8398dd415bd61465744d0f5eaf6abacf0900ab90add28b21ca6abd2eae1c69882ab5cf781ed1651bfb7ebec12f85ea9aed8eefd8dbf508e3a7ef3053cfb23d22d8807e723ccd9d0ec2511f2ea80354974facfeb25c537ae482f2e58f9a4d063978c6cbb66ed284c84c9d022c4d1e001f47484", + "ciphertext_tag": "8d9c911e76158fd5af78661ba00616ba763fd7c85ef6271ff920cd8f6b6120a30f82d7dbb76a0f37d98c7ff90678390ddd3e7b3ef8a09fa202062b2c1e235ab3524916e8b015be38093861d2cbc0aa4e6b97a6624473a72b8decfa5f792df043d4c8d508d3488a8c4e5681ea641600ce4f313e4f3441cb5e5946f564959ddd067c9e183c7745b8449c0e95d5159119353f248e11aa91d2d73934300004f4541e7f8ad983ac5f6186a161b4449a20c85ebba9958c1dcce211ad796c0419b2e9e3939c944754bd10b964f57cbe0c30476ef22ad107f6930e9dcea44028b0f4880938509ae333cfdc9cc20e2078833a8bcfc9763e001a207f40aa99f72e49bafa7090bc148d05a16b2d53cf377753406514", "rlen": 272, - "record": "0110dfae9f81fb088d15ab042a4607905a15100ca1395a062722a0fb701b2018173a52d0d1f572a08d62272d20cd22d3dd16a763f79a5982e53af8933cfa4d18be6bfeea8d42deed30c5f952acfccfbf79991163d10fe3ea5a2513f759a32e22ff5f6c38f10f9e6a98bda304a5be4f4891c06e13de7cdbfd8b7aca424a45ac2be603cb158fe24261192950f24755782d9d873de4862a50ca3967d3e67126e1f8398dd415bd61465744d0f5eaf6abacf0900ab90add28b21ca6abd2eae1c69882ab5cf781ed1651bfb7ebec12f85ea9aed8eefd8dbf508e3a7ef3053cfb23d22d8807e723ccd9d0ec2511f2ea80354974facfeb25c537ae482f2e58f9a4d063978c6cbb66ed284c84c9d022c4d1e001f47484" + "record": "01108d9c911e76158fd5af78661ba00616ba763fd7c85ef6271ff920cd8f6b6120a30f82d7dbb76a0f37d98c7ff90678390ddd3e7b3ef8a09fa202062b2c1e235ab3524916e8b015be38093861d2cbc0aa4e6b97a6624473a72b8decfa5f792df043d4c8d508d3488a8c4e5681ea641600ce4f313e4f3441cb5e5946f564959ddd067c9e183c7745b8449c0e95d5159119353f248e11aa91d2d73934300004f4541e7f8ad983ac5f6186a161b4449a20c85ebba9958c1dcce211ad796c0419b2e9e3939c944754bd10b964f57cbe0c30476ef22ad107f6930e9dcea44028b0f4880938509ae333cfdc9cc20e2078833a8bcfc9763e001a207f40aa99f72e49bafa7090bc148d05a16b2d53cf377753406514" }, { "name": "control / làn dài, mảnh 1/2 (more=1, long=1)", @@ -32,9 +32,9 @@ "key_source": "c1 của bt-link.json", "counter": 11, "nonce": "000000000b00000000000000", - "ciphertext_tag": "037973e4c60b2c2f61b0ff531b5ec0b9c810ba3461ed1143f1cd8c2121cca21a2c49c6fc10ecdc57e19e9515c99df9abc5346321cffc1a14ab1eaba873260a7eae57eb280c9b929f079cdfd6b9a093e8ed9251902ef39a97d73f6b8ef2f73ebbd8d97249a7757aaf361027219f4cc88f42062d80571dc7e2576513624f309a6baa584d4f04382ddeea09a3607dfa220953f87da07c745540327176f474434b25c00b20360b53c826e4ffdae48a9e7b25129d04b0becbb24f9df04f07a8f55712e632feef9618399db440605da3b4fdca0322caa056514c25fd6bb958501fff92143d805af2117da061bf004eb1b1ecca134385c5544d08f3ac1112ba321623f69aff510b09d598791d875811ef82167fb771421cee0be9cbcbe783867989c77df4bf6ff3c36672a67b1da29e85f0414a0d1c8ae61bba0a1817163fe66532559c5a7bc7e372405e8ecf760af455734457392a606c197f854df3f9c1cbb2c5459a9c039404ecc821d4f9d6f150775a2494740adc68fbc9e98009fba62c3c34ea51c2ea5f9306c52dfa81dedaf5ce607787ccb7d568e1c37ca197f4ef2ed9a04e29d53ea4481000e00d83069e5b4685bc362bae10f8f844b9182699ff41e9e4a0ea8f27849f73947f76e1d834001025f7959902850e5f951e66f84874af47810559c80f8cd36f3ba427c1afd23af37fc7cb248f5e8fc28c836e9e1f6113fade18039e6f22f8c275baa92353fa6d5defeb19b5130e47e5eb965f5ed46f3a9e18fd918d3f2531ea4f5bfe36cdbb10c395dc7000a5ab8a6bc6b76081763450385d188bbf24d9959334e1613f85c1b93e5c69f5c309f91c9952ec9e33d7bb9f13c45aa826822bb344a052b3e0cf6318c3eeed64b60f4f7176f5f7a6817213fb460f9b686ba7d76420c8b46b43fce6c6aa24fea169dfda23351ecdfef9c1e61f6af74796f2bd44420fd28cee4445dbc8cc282b5422da42ca142b9ee43538b709a9a47f85f2d1df4a101b4ea208deae2786521a8bc2679615fe16907ff589f6fed39a331610a429855e462bb7ec78ed354ff9ee5dd9ad6ca77692cbbd8d7c1765d08945a6b3f1abc2cacc6c1ccf4dd79ca23f3b90946aab6b2abc8dd225b7482ee4a5409ccf4c38847b2ec9454ddeab4086a771d05fd9ae4d39ba2182d0a88869c50231cb12585b67263648ed3e22b3726b9384b3241692cd33e81a1eed1c87c9c4492d9802c4a17e4150608d73485cfa32b3ceec6513bfb6369be68e06e06f563bfa5448874c90467876387c4261c5935e318da439b45a11d16902189008d18a5def73aa15e419b32ea495db2729d95ac31790dca638decdfd1155157e417b71b3508e188135e8f2667a4da1d1e05077863b63f7b95efd431585b82722e2f315d2a298648d11c77648558aa6be31bd6013d457574bbba39e1d38ef98818adc0dde7d3ca6819d50fa70026642c35473f79d5252cc", + "ciphertext_tag": "f54ba3fd3319ccee592fee4cbb05a96826b0352a626a1801e49b527ee052ef868a0c2fd13cfc5eec9e82ae65e3ee4b3eabc02a5751ab6c970c41be6fe1cdc46333d2e79b32e8b393c9e79426d5d5e8407e69be3c15b6ef1f8c71de9bc32279032c05b10be57cace7ad22d646ead726d76c964afc11ef6ebb58542128ba7a5a867748846cddeb7e47a136c5d5fee5e24ad85fd74670513c3e4ed687b7860a0df2d90eb4b8e82c030d913202fb17897ae978e02e6ffa945b1429013396137406c0bc12b69856c098e45a872078a31982e93068d0c07d03c77ae890b019e54d672dab8f7f646da9a2827c73321abd6dfa752fc2f775c35d92e0b05d8966182e4fdd5755fa46b891b3eeb51e7025833ee2509e35f1f0e8fc9f14550e9af8ef0c137909654acb35414f74ebdb625771a17238dcb55b9059e58bfd5c1408764f8a6c39a76c685139a9e0923e2761ed5cf068cf78519bc772231427273d33f6a39118c4b36844349184b4e4f343a6ca2419b1af6808d3c594b018fc064f0bc3227220d88e29ba08fded497a966cbc1a647f109730f8b1ec38ccede567928981ef1aed463d1409c743741cf8d269cf9359a4cb10de43ecfb72fe37e81f10f761d6852b785d3f86b93d5457660c5bc5be3cd7c533be4a57ca175c32497f292422d17b7b805f9b4d687c0497d11c15c738ecb61e5db234c365974455c1fe489b95bebc123b1e096149f5d1e4e055ebc6325da0d78dc1d0d93f6b53954c60d80aa740e6c6915153efef99d6090feb8fe69f28c041c7b137822648c5d636f5e81fb931150aa5d611a0c3ed1cf6a02fed4f43c14f2ec71e4284c54940eb9ff5e7f70655b7a6e422977f7590995857fcfc64b5bc5e60532a148c29816d363831bacfa8560a02b647682402f3f553564751c09d1cae6ac45d13e4e67f1ba21e1fd1ce0319907e9786c9d449378d89b63b7cf8619e44fb06586598a5073e2eb6971a48e4f010b49321edd9654c3c6893f432b09e160f3d0a349410b273f4a00fbb194f60626a8e7baf9763fa1dd489731a4533a5405c58a2425515671331256843f8a7d4966d5e4470fc9a6fc164d6abcf22dd603582101a5c9850c2a70a8abfec5a2cd115b46d12bcef0fab26e15babd72b15b39240b7c03f4773242d113526b17019d3bfa46dc1eea82e6d6292e6a8732bff5414789507abbdc0e50f4f4e28ce094262e9ae6b8b9f3a3f776c831ded9f4c0616123ad8567b83f22c796bec86585c925332eee53a0df9406512996e0c9a2ce69abb85f819380239396e07685773bf08e5725a21416034f9dd71c02288bce287aa522f7ae0026f1f16d3a71546b9d55880d26eab14a335bc15ce16b895d945ba2edec528912835df7cad961a32e636467cc8e2554e5255c7717e146b827f0b47193d3f43bd0b735910ca3a7a509394a3527151de397afae8d2b669d4fbc228ecb7e3a67d47", "rlen": 1040, - "record": "0410037973e4c60b2c2f61b0ff531b5ec0b9c810ba3461ed1143f1cd8c2121cca21a2c49c6fc10ecdc57e19e9515c99df9abc5346321cffc1a14ab1eaba873260a7eae57eb280c9b929f079cdfd6b9a093e8ed9251902ef39a97d73f6b8ef2f73ebbd8d97249a7757aaf361027219f4cc88f42062d80571dc7e2576513624f309a6baa584d4f04382ddeea09a3607dfa220953f87da07c745540327176f474434b25c00b20360b53c826e4ffdae48a9e7b25129d04b0becbb24f9df04f07a8f55712e632feef9618399db440605da3b4fdca0322caa056514c25fd6bb958501fff92143d805af2117da061bf004eb1b1ecca134385c5544d08f3ac1112ba321623f69aff510b09d598791d875811ef82167fb771421cee0be9cbcbe783867989c77df4bf6ff3c36672a67b1da29e85f0414a0d1c8ae61bba0a1817163fe66532559c5a7bc7e372405e8ecf760af455734457392a606c197f854df3f9c1cbb2c5459a9c039404ecc821d4f9d6f150775a2494740adc68fbc9e98009fba62c3c34ea51c2ea5f9306c52dfa81dedaf5ce607787ccb7d568e1c37ca197f4ef2ed9a04e29d53ea4481000e00d83069e5b4685bc362bae10f8f844b9182699ff41e9e4a0ea8f27849f73947f76e1d834001025f7959902850e5f951e66f84874af47810559c80f8cd36f3ba427c1afd23af37fc7cb248f5e8fc28c836e9e1f6113fade18039e6f22f8c275baa92353fa6d5defeb19b5130e47e5eb965f5ed46f3a9e18fd918d3f2531ea4f5bfe36cdbb10c395dc7000a5ab8a6bc6b76081763450385d188bbf24d9959334e1613f85c1b93e5c69f5c309f91c9952ec9e33d7bb9f13c45aa826822bb344a052b3e0cf6318c3eeed64b60f4f7176f5f7a6817213fb460f9b686ba7d76420c8b46b43fce6c6aa24fea169dfda23351ecdfef9c1e61f6af74796f2bd44420fd28cee4445dbc8cc282b5422da42ca142b9ee43538b709a9a47f85f2d1df4a101b4ea208deae2786521a8bc2679615fe16907ff589f6fed39a331610a429855e462bb7ec78ed354ff9ee5dd9ad6ca77692cbbd8d7c1765d08945a6b3f1abc2cacc6c1ccf4dd79ca23f3b90946aab6b2abc8dd225b7482ee4a5409ccf4c38847b2ec9454ddeab4086a771d05fd9ae4d39ba2182d0a88869c50231cb12585b67263648ed3e22b3726b9384b3241692cd33e81a1eed1c87c9c4492d9802c4a17e4150608d73485cfa32b3ceec6513bfb6369be68e06e06f563bfa5448874c90467876387c4261c5935e318da439b45a11d16902189008d18a5def73aa15e419b32ea495db2729d95ac31790dca638decdfd1155157e417b71b3508e188135e8f2667a4da1d1e05077863b63f7b95efd431585b82722e2f315d2a298648d11c77648558aa6be31bd6013d457574bbba39e1d38ef98818adc0dde7d3ca6819d50fa70026642c35473f79d5252cc" + "record": "0410f54ba3fd3319ccee592fee4cbb05a96826b0352a626a1801e49b527ee052ef868a0c2fd13cfc5eec9e82ae65e3ee4b3eabc02a5751ab6c970c41be6fe1cdc46333d2e79b32e8b393c9e79426d5d5e8407e69be3c15b6ef1f8c71de9bc32279032c05b10be57cace7ad22d646ead726d76c964afc11ef6ebb58542128ba7a5a867748846cddeb7e47a136c5d5fee5e24ad85fd74670513c3e4ed687b7860a0df2d90eb4b8e82c030d913202fb17897ae978e02e6ffa945b1429013396137406c0bc12b69856c098e45a872078a31982e93068d0c07d03c77ae890b019e54d672dab8f7f646da9a2827c73321abd6dfa752fc2f775c35d92e0b05d8966182e4fdd5755fa46b891b3eeb51e7025833ee2509e35f1f0e8fc9f14550e9af8ef0c137909654acb35414f74ebdb625771a17238dcb55b9059e58bfd5c1408764f8a6c39a76c685139a9e0923e2761ed5cf068cf78519bc772231427273d33f6a39118c4b36844349184b4e4f343a6ca2419b1af6808d3c594b018fc064f0bc3227220d88e29ba08fded497a966cbc1a647f109730f8b1ec38ccede567928981ef1aed463d1409c743741cf8d269cf9359a4cb10de43ecfb72fe37e81f10f761d6852b785d3f86b93d5457660c5bc5be3cd7c533be4a57ca175c32497f292422d17b7b805f9b4d687c0497d11c15c738ecb61e5db234c365974455c1fe489b95bebc123b1e096149f5d1e4e055ebc6325da0d78dc1d0d93f6b53954c60d80aa740e6c6915153efef99d6090feb8fe69f28c041c7b137822648c5d636f5e81fb931150aa5d611a0c3ed1cf6a02fed4f43c14f2ec71e4284c54940eb9ff5e7f70655b7a6e422977f7590995857fcfc64b5bc5e60532a148c29816d363831bacfa8560a02b647682402f3f553564751c09d1cae6ac45d13e4e67f1ba21e1fd1ce0319907e9786c9d449378d89b63b7cf8619e44fb06586598a5073e2eb6971a48e4f010b49321edd9654c3c6893f432b09e160f3d0a349410b273f4a00fbb194f60626a8e7baf9763fa1dd489731a4533a5405c58a2425515671331256843f8a7d4966d5e4470fc9a6fc164d6abcf22dd603582101a5c9850c2a70a8abfec5a2cd115b46d12bcef0fab26e15babd72b15b39240b7c03f4773242d113526b17019d3bfa46dc1eea82e6d6292e6a8732bff5414789507abbdc0e50f4f4e28ce094262e9ae6b8b9f3a3f776c831ded9f4c0616123ad8567b83f22c796bec86585c925332eee53a0df9406512996e0c9a2ce69abb85f819380239396e07685773bf08e5725a21416034f9dd71c02288bce287aa522f7ae0026f1f16d3a71546b9d55880d26eab14a335bc15ce16b895d945ba2edec528912835df7cad961a32e636467cc8e2554e5255c7717e146b827f0b47193d3f43bd0b735910ca3a7a509394a3527151de397afae8d2b669d4fbc228ecb7e3a67d47" }, { "name": "control / làn dài, mảnh 2/2 cuối (more=0, long=1)", @@ -49,9 +49,9 @@ "key_source": "c1 của bt-link.json", "counter": 12, "nonce": "000000000c00000000000000", - "ciphertext_tag": "219ca00278a3b3ee53c2acde893326fd5bbdbf7fe8f2e94c11d8e333c8f503903c63b88ddbd95bf6ced278b3908a319ce13a6beeb31f033839beaa0a9b527078a63b12a5b95d15a8fcf28dde1082c54ff8da82c6b1b8ef88f96e4bbed2b9d378022bee1218a59002c332bc5031fb21b00eec918a6279f8dd1cf8a51ae44df931a055bd729d5f33d8710a6cf768e9ff8d9436aca74764ccffc60666bdd3db6b04a8b27cf764bd399b9ee9ed1596923f22581000257a9bf7d41c616b40f4b3f9bdb58543a6619b22d0229741865e40c7f57f5fda54d15018aa2915549c22a1163eae2f11df0c32e09c05aa52ea95787258cab0c53636b881011bc4c13d690d2794f7c46beffe80e4eba21732aeab9be7589b02cc04f115bc2963217bc77f3ec304481848ab02701da788728565e679c8478c8d2fba46b03d9fdd060b33f42b2f42e96143958576cd1f894eae3b9fc9d21ad94e077b64779b6b95d9a1ac51174cddd5cdd9f310be1efef5113e8cd5e826206bb105aa111e3890c075ef1d8002fdc87c1c6561951d3e69ea5d5eceb9fc90b46991c8b2967313e9dc3e2e7726cc6ad35464c4ec0bdaffd89723395e02a26399498465a28d6da54882477dd5ce83054f495a0cf1d1adb906f45a98bdb121955ed3f0e42452772098aa19ed78de9ea0df6c1da8cfc3c2ada970a75d070c284c517ebeecd75b6c6fdd94ca46b46cffdb343fbbf0eddf60a9ab1c7a8a45ce64b1cecdb003d42e747c928cf29a1085e79f87a61befa83ac78dd3a64035077c53e6be9fc0c57df3bdfe9880c5af4f79fa12edce0627f1fae4bef76bf7408431467e7aa2c2d9d15732779ca1e4c35c76a513b633feb5950133f9d8037705ace6d8e5adb71091aa6b0a1815bdfb9ebe3af50b8a9cf49ce2928e35fbdfeb0c3a3e847a003e7710294b2cf696855a478e51e8b8251dc19694c57bab3eb35de465844f8764f41efbea94dba3f73da9f01de1001aca41a3101f6c9e9d4a0438d56352cb64daf44f702204b70fbd2a989466ea9560ca836cb975b39e4e6417dfe402dd39fe0caf9352b64726c258fb05934b58639a0c1d7f29ad3742712b8f16d3b243a31c04f33d227b90093992f74f68421b596b6648b41b3e3843733aed65b6bc2e10dc2e245079d9471b3104a99bac559b53c587526fd8c4869102ac5ead0009629488969935d33f57d138f5b362f3bd9e746b1f1cf3b0d77985fa86c7ca28f2ada31ad9cfc27c96638b070af43936645c600a102647205fa3937f617caa000a372e56ccd5e6f03b2f94b786f49b3309d691c84a59caa323a2e4c04db113f76a27e4927d6215ca05dd74d87c64b019eef7490e5db00a6038e160214e4b1cc43e6aca7b5b0b28a5406fb4f45594a6e070a92c2d68dc40071e3a9887a58a30a39f3d3a510dca5fdf9fb8383afeedec7aeaab963a0d311829070570ab9d0a69dc05c1826013", + "ciphertext_tag": "c7bd07c88a7e4fd888310a16cb2bd15e91cdf19b5e04a59d3e36ea65ed83ef33c7267c695275bf21dfc66aa9478c4235708040291cb766486c3b3d180303cf2eb99f495545b770034aa43a69f6816bd5bc66c3fa70605af54c0c793308f3b4454fe288614a50faa4ce82620d507a595a39ac0e063de5958dfb0e9aa0d00f126aa618683f0c7bd5ad10a403a62644f9e90b4b9451d0dca48b70d4e05bc2ec85cacb14261d9ca4eed0052566f529a660555f14abe5724e39417c1da446ba91dd480111b069331b13a08b4e413ae81cc974f5c511c5534a4b9820cdf054e2997ba9292290667498a6bd7dc2fb2f343727bb189f7b4c479f36766dda9370e1af0bd908a18366f090924f4b8afed303e0998d63e06ffdda125a95a0cd1d03d2baffb12a685f27bb4c6826f830f9feb98cdc727cf4014e7b37fa8f1c475bc4c5e9e708364eca88e8ea3f8933e042887c0630fc0dc5355f5496f618b052325444a108b30cf242aac92488ecdf7613f7ae84ecb2a0849eabc4c86fa6290fa7ea5ce2bf74296ec9416ad38e906d717ee3f760d28c6824d3d42e26964597930d53a19b9ac632bade37fdd13acbfdb277ce44e0bc45ca347ad440b5b2fe01cfa812c251e59fd6229a79a19a27a74bce0494fbe567abaa78785815efc672df551a3ecd7ff68de6e65ab2c89c6d56ada62fc5e9b137ab7b2071b28fc5ba7aa46a4f1a42da573769dd3f717bdd619396ff9d9982d3b1265ef8c9c69e3ec9e7a97301b2a5edee231c404c860e93e782ed3a093459db8ba59aadd5a53f6726b5e1e0d16001166d8b51e1bba3885183e88ce68ee11ac4aca30d78395e0ac00d82f8bc5e9cb4752d288e0e88e187e3ae835ee6b4d8c283986f59baee118e05648247712d3bc7a6f40e671ded5c0c2d83a93018e80ff6c700a76171b31012539b7b04ffdc24787f1339833f614e86604cc67076e384787a69a23afa612fed4e30bda1787062b1a1aad5a8a3969dd0d5fb4b1f4309a98104cbb5440b2cc0fb537d12411785e2180e3b626f9304dcc9c34c0f1826d8568ade6153bcc87bb30844bc720e130f90c6e9110d789cbf583c019038d9bf6654b794b9c1bdc5a9b02a227572026de18e5dac18b61f958654f165d9ee71c06e0201d82d92d88e82b5958706040c852b0c3cd7a4632f78d7c07513942989950d0dfa189af1b060e5a06336b534fe397aaede2ab4fbffad2f6317e1ef977219a1e89a88064e670bedc6f7c08aa4d564d96397ce6cc89e15f42ea4835bb45259f56540c835f56565c34b8f3a7baeeb6df91a7ed10cf53c8d900cd83d119512bc5d76ad2e8a3911990dbc07e7833f0c57794b0e7ad76405f6ec78db1b2ec603af792940054b39469fa909b50f62c970924418634352462908364214b42a7029094189ba3258dbadceb5a8316ef194a7f0749c6fac07afa3bd0143d8beac33cdccc2f811214b51", "rlen": 1040, - "record": "0410219ca00278a3b3ee53c2acde893326fd5bbdbf7fe8f2e94c11d8e333c8f503903c63b88ddbd95bf6ced278b3908a319ce13a6beeb31f033839beaa0a9b527078a63b12a5b95d15a8fcf28dde1082c54ff8da82c6b1b8ef88f96e4bbed2b9d378022bee1218a59002c332bc5031fb21b00eec918a6279f8dd1cf8a51ae44df931a055bd729d5f33d8710a6cf768e9ff8d9436aca74764ccffc60666bdd3db6b04a8b27cf764bd399b9ee9ed1596923f22581000257a9bf7d41c616b40f4b3f9bdb58543a6619b22d0229741865e40c7f57f5fda54d15018aa2915549c22a1163eae2f11df0c32e09c05aa52ea95787258cab0c53636b881011bc4c13d690d2794f7c46beffe80e4eba21732aeab9be7589b02cc04f115bc2963217bc77f3ec304481848ab02701da788728565e679c8478c8d2fba46b03d9fdd060b33f42b2f42e96143958576cd1f894eae3b9fc9d21ad94e077b64779b6b95d9a1ac51174cddd5cdd9f310be1efef5113e8cd5e826206bb105aa111e3890c075ef1d8002fdc87c1c6561951d3e69ea5d5eceb9fc90b46991c8b2967313e9dc3e2e7726cc6ad35464c4ec0bdaffd89723395e02a26399498465a28d6da54882477dd5ce83054f495a0cf1d1adb906f45a98bdb121955ed3f0e42452772098aa19ed78de9ea0df6c1da8cfc3c2ada970a75d070c284c517ebeecd75b6c6fdd94ca46b46cffdb343fbbf0eddf60a9ab1c7a8a45ce64b1cecdb003d42e747c928cf29a1085e79f87a61befa83ac78dd3a64035077c53e6be9fc0c57df3bdfe9880c5af4f79fa12edce0627f1fae4bef76bf7408431467e7aa2c2d9d15732779ca1e4c35c76a513b633feb5950133f9d8037705ace6d8e5adb71091aa6b0a1815bdfb9ebe3af50b8a9cf49ce2928e35fbdfeb0c3a3e847a003e7710294b2cf696855a478e51e8b8251dc19694c57bab3eb35de465844f8764f41efbea94dba3f73da9f01de1001aca41a3101f6c9e9d4a0438d56352cb64daf44f702204b70fbd2a989466ea9560ca836cb975b39e4e6417dfe402dd39fe0caf9352b64726c258fb05934b58639a0c1d7f29ad3742712b8f16d3b243a31c04f33d227b90093992f74f68421b596b6648b41b3e3843733aed65b6bc2e10dc2e245079d9471b3104a99bac559b53c587526fd8c4869102ac5ead0009629488969935d33f57d138f5b362f3bd9e746b1f1cf3b0d77985fa86c7ca28f2ada31ad9cfc27c96638b070af43936645c600a102647205fa3937f617caa000a372e56ccd5e6f03b2f94b786f49b3309d691c84a59caa323a2e4c04db113f76a27e4927d6215ca05dd74d87c64b019eef7490e5db00a6038e160214e4b1cc43e6aca7b5b0b28a5406fb4f45594a6e070a92c2d68dc40071e3a9887a58a30a39f3d3a510dca5fdf9fb8383afeedec7aeaab963a0d311829070570ab9d0a69dc05c1826013" + "record": "0410c7bd07c88a7e4fd888310a16cb2bd15e91cdf19b5e04a59d3e36ea65ed83ef33c7267c695275bf21dfc66aa9478c4235708040291cb766486c3b3d180303cf2eb99f495545b770034aa43a69f6816bd5bc66c3fa70605af54c0c793308f3b4454fe288614a50faa4ce82620d507a595a39ac0e063de5958dfb0e9aa0d00f126aa618683f0c7bd5ad10a403a62644f9e90b4b9451d0dca48b70d4e05bc2ec85cacb14261d9ca4eed0052566f529a660555f14abe5724e39417c1da446ba91dd480111b069331b13a08b4e413ae81cc974f5c511c5534a4b9820cdf054e2997ba9292290667498a6bd7dc2fb2f343727bb189f7b4c479f36766dda9370e1af0bd908a18366f090924f4b8afed303e0998d63e06ffdda125a95a0cd1d03d2baffb12a685f27bb4c6826f830f9feb98cdc727cf4014e7b37fa8f1c475bc4c5e9e708364eca88e8ea3f8933e042887c0630fc0dc5355f5496f618b052325444a108b30cf242aac92488ecdf7613f7ae84ecb2a0849eabc4c86fa6290fa7ea5ce2bf74296ec9416ad38e906d717ee3f760d28c6824d3d42e26964597930d53a19b9ac632bade37fdd13acbfdb277ce44e0bc45ca347ad440b5b2fe01cfa812c251e59fd6229a79a19a27a74bce0494fbe567abaa78785815efc672df551a3ecd7ff68de6e65ab2c89c6d56ada62fc5e9b137ab7b2071b28fc5ba7aa46a4f1a42da573769dd3f717bdd619396ff9d9982d3b1265ef8c9c69e3ec9e7a97301b2a5edee231c404c860e93e782ed3a093459db8ba59aadd5a53f6726b5e1e0d16001166d8b51e1bba3885183e88ce68ee11ac4aca30d78395e0ac00d82f8bc5e9cb4752d288e0e88e187e3ae835ee6b4d8c283986f59baee118e05648247712d3bc7a6f40e671ded5c0c2d83a93018e80ff6c700a76171b31012539b7b04ffdc24787f1339833f614e86604cc67076e384787a69a23afa612fed4e30bda1787062b1a1aad5a8a3969dd0d5fb4b1f4309a98104cbb5440b2cc0fb537d12411785e2180e3b626f9304dcc9c34c0f1826d8568ade6153bcc87bb30844bc720e130f90c6e9110d789cbf583c019038d9bf6654b794b9c1bdc5a9b02a227572026de18e5dac18b61f958654f165d9ee71c06e0201d82d92d88e82b5958706040c852b0c3cd7a4632f78d7c07513942989950d0dfa189af1b060e5a06336b534fe397aaede2ab4fbffad2f6317e1ef977219a1e89a88064e670bedc6f7c08aa4d564d96397ce6cc89e15f42ea4835bb45259f56540c835f56565c34b8f3a7baeeb6df91a7ed10cf53c8d900cd83d119512bc5d76ad2e8a3911990dbc07e7833f0c57794b0e7ad76405f6ec78db1b2ec603af792940054b39469fa909b50f62c970924418634352462908364214b42a7029094189ba3258dbadceb5a8316ef194a7f0749c6fac07afa3bd0143d8beac33cdccc2f811214b51" }, { "name": "bulk / một mảnh ảnh (more=1)", @@ -66,9 +66,9 @@ "key_source": "c1 của bt-link.json", "counter": 13, "nonce": "000000000d00000000000000", - "ciphertext_tag": "3d0b904809ddb647a52897ee878f0e0ab583c5b885a6a2bea543a779ab4685c7fa150cab9f190db4dad63c155cb01fbadbd81ec658bea8214f6ef788475a943fe4f87c500e4f1c296be00e14e7cadb2ac61d2c8a9e05fbf5ceadb87c5d3c3e0825b65758995fccdd6d74ea0f49aeb55cea9e271f3ef722a8b5b546ebb5e56b9d4970513d69bbfcccb9fd59ef0f495a3a99660c3ed78cfc7ce6356e9462f5a0b10d72aa9a06518986aed7c9f9df5819a7a388741719fddefe8c06105ae954e2c179cb296cbfab7a3a1daa710b6f6c8c24948a09864bda1290b0f82e4c49c6b94ce0115c8ede464c45f9988da78647fa1f83fcd7201bc72a63a62e19e2bda518d6b50d5c3be7bd14d0480d5fd32fb82edfbfd8c11e898d323ec0edb73dce4b24656b524f73c8f63330e9234e5e13a25282fc5762f2783efdbd1a2f1dbbdf424ea3f56011a0ee3d74d2f3e81af4d68b4c80dc5941514845dc8e170d45a934d50c2c7e9cd103b067114d7155d4cb59816ba1a97454ababc5a28cc0f81f9b5df46ec5175c1469a466ba574ad4eedd505cd5d041a386ba465e798a673ded893e4b7f8b976665c413f83d3d386a263ece1542752a8b4565fe705cdba8f09b64bfc4e54e7a09736c024cc05785e18ff251f652cd8ca8626d1abedc29197f852f0f572f0bcd2e93f5f3d485ffb2d9ad22743ab66dddb3891eed5fbac9b4c134b6dcd695fb0ec8d0ad775961f664f2e35c3e717cc2c99e448621ca67a774ec521c76a528a0341a557de3e8a8328516c6fc3bc435d9f582190f9d5abf6dd5f63f0eb5b61decb0308b191a6463f3a13f498f28227b95d0f4537e54c93533f22ed0ecf8c5765efd798019ea45a15347a4d60cd562c10a0ce3a4839a0862a397ad0a8a38b2bbb88548ce996b3716ece65c4c3a1bf2281900212b9f87e2bbe8b08a890d3dfe24f1fa867ba5e67328d03c0322e908ae5877086d799d746ff9e38520e0770cb5992b6a6b4b44530f6f42161b71930ed09e8aac1a363a3c2cc97387065ef2d020a6106e65759b24ee20ca68a44181f462f560413392c8e4be142ce429b99bda0e58ad18b9465ceed60e35359fb8741661b1d82889619dad3825af6b2a2f5db10045a95fbd306c0d8c5540551cf0d983708311581904acb2906ed4fbb921b715db968136075f0d7b8bc43d646012e38a31f05e0729ca87e31bb3196be0e0539e26f6e5c17d4d479928d35eb62f5c916e9862260f609c7615e0f4f955bb3b442b1a35381f0f170e738bd24ae4b03aef939f43fe0ed1d567a35a291eba099ebb5e6bf247f03fe67886e624b6701887597945141db9bc8fbacc6a3bec6ec4d47e259b6dba912c99725b1bf6549679240cde06761950776cf5cdeb4d0af81d4fa04aaae061f4ddce6b7e59a0c11a659658398816de2b048ef415f4016ef816c0a041b96aba3c0599ffdcdea45c25c6ac2326eb5688", + "ciphertext_tag": "f8fcc21f579b9620e55c5f05526115ddcb1db03cab55a93da8e8ef770b63f74d2def8fdfe11406b23e41bd808cc852f70991c0b883bac61221011ebd974f3bf4cf07113cd5b1c76d85395dcf96f0780948c089a1455ba96dbb783f80c896a3fcf94887483aa4b426380485bf2ba1fa7c608e998043c64a70047c4e0b38821f10be89af5f6c1d6876963139c78d99a5037d2647b0cee0637d850abb16ce90593b0dccb376e1ff2ed7e0a047cfe5db29cd9e96d15ccd883f71ad1300702d4211be0154020c297b775a537d1e7eb0c3810fca23da9fba1ded617696ef843f8c922524360313086a76badcf33688083c9b2829e4d1c7f2ddddf5e2eccf0c5d6e2104b0c75e12892d33bef24e97c4be12de8e6c2e83bd8acfa64b304e7adcfbbf720b6b7adf4662567323e087a398cad3eef292ce5daee1038ca39fb68924cabe67c42273935b8aad96f38b35c2b1a6e7334920c192a65e01b10e4548318787675df9060e7b61281658ad520b54d4d360e5bbcd6835ac58eadb8d30de4a581724fbf561edddc8f388611d8950a3a31c18a408e152a6354f9a5be29ad0a9b129f2d6f243e03a075b47287b2637dc7ec853a2f4a914245e4e6f56f09680329b92898fa5c3a60a1ab18a745d9752bde353f856b8fde86d5ffd1214f06f418dfb9a1c3ec321be2c9a192fd910700ff1c5b28bac062fd593a166a2c952f0d3ff53bf03a54ecb8cc6e15670a289921b8c0291ddc5236273e0080477a5733bdaee12b0d47d31b27ca9db333671ee4aaac54b77c1accbf65d6a3f529ee882753194454fc004978ac6c6efe6776f054dec698318914578de4f77c31aba41100a6ceb22149d0eb9869ebeefcdbe3d733fe6238849a35ee2b34e05769535b52c57b0048a3b400e69ad263a48aa39877cc12ce624f32f5c0abd5dc84a49915414f9ec5508f9268d8008212624ef20c72dd23d05e26800fcedb9bca3955d8ba95667814b5481f40d94b3e02ad3307119e6008b4b6b15e75ee91ac545f6bb766471715042a67f12b0f476fd9c1f103d7fb4ff1a39045e2900d7c36d4bb2995230d2649cebab95fc423c08fda8943eb6e6335699bc3ba88ac8f265d6a134910a7dcdd4764c1840bff9b140a3048bfe42b6b1cbc0706ed23bddce02365495c690b29b49ecd9945965e14a47dd030e82fd47a7713355ab79e26dcad2bb7a6d3b4b43e4b55a228557ea2e267fe2e80d1cb206cb7555b0fcfac753f7669cf624901f2e153a1e073f9a825c78e259bedc17b802a2b405e79ab5e0e0e45f1554287dc3614509c35f98f429c9ce296867661ab5968524b1ee6d88d2bb2077f328d503ff5f380f3c26cdecd0c6d9ed883d6294642decec7ee87bca462db47b0bc7380886fdf90258c7c3434dae16e2bce19ebb74b1d2a661f82db7ea440a861071df470f54ad2aa59692a8cc58dfe4f496edab0ec2a4e854b6c43f931fd4", "rlen": 1040, - "record": "04103d0b904809ddb647a52897ee878f0e0ab583c5b885a6a2bea543a779ab4685c7fa150cab9f190db4dad63c155cb01fbadbd81ec658bea8214f6ef788475a943fe4f87c500e4f1c296be00e14e7cadb2ac61d2c8a9e05fbf5ceadb87c5d3c3e0825b65758995fccdd6d74ea0f49aeb55cea9e271f3ef722a8b5b546ebb5e56b9d4970513d69bbfcccb9fd59ef0f495a3a99660c3ed78cfc7ce6356e9462f5a0b10d72aa9a06518986aed7c9f9df5819a7a388741719fddefe8c06105ae954e2c179cb296cbfab7a3a1daa710b6f6c8c24948a09864bda1290b0f82e4c49c6b94ce0115c8ede464c45f9988da78647fa1f83fcd7201bc72a63a62e19e2bda518d6b50d5c3be7bd14d0480d5fd32fb82edfbfd8c11e898d323ec0edb73dce4b24656b524f73c8f63330e9234e5e13a25282fc5762f2783efdbd1a2f1dbbdf424ea3f56011a0ee3d74d2f3e81af4d68b4c80dc5941514845dc8e170d45a934d50c2c7e9cd103b067114d7155d4cb59816ba1a97454ababc5a28cc0f81f9b5df46ec5175c1469a466ba574ad4eedd505cd5d041a386ba465e798a673ded893e4b7f8b976665c413f83d3d386a263ece1542752a8b4565fe705cdba8f09b64bfc4e54e7a09736c024cc05785e18ff251f652cd8ca8626d1abedc29197f852f0f572f0bcd2e93f5f3d485ffb2d9ad22743ab66dddb3891eed5fbac9b4c134b6dcd695fb0ec8d0ad775961f664f2e35c3e717cc2c99e448621ca67a774ec521c76a528a0341a557de3e8a8328516c6fc3bc435d9f582190f9d5abf6dd5f63f0eb5b61decb0308b191a6463f3a13f498f28227b95d0f4537e54c93533f22ed0ecf8c5765efd798019ea45a15347a4d60cd562c10a0ce3a4839a0862a397ad0a8a38b2bbb88548ce996b3716ece65c4c3a1bf2281900212b9f87e2bbe8b08a890d3dfe24f1fa867ba5e67328d03c0322e908ae5877086d799d746ff9e38520e0770cb5992b6a6b4b44530f6f42161b71930ed09e8aac1a363a3c2cc97387065ef2d020a6106e65759b24ee20ca68a44181f462f560413392c8e4be142ce429b99bda0e58ad18b9465ceed60e35359fb8741661b1d82889619dad3825af6b2a2f5db10045a95fbd306c0d8c5540551cf0d983708311581904acb2906ed4fbb921b715db968136075f0d7b8bc43d646012e38a31f05e0729ca87e31bb3196be0e0539e26f6e5c17d4d479928d35eb62f5c916e9862260f609c7615e0f4f955bb3b442b1a35381f0f170e738bd24ae4b03aef939f43fe0ed1d567a35a291eba099ebb5e6bf247f03fe67886e624b6701887597945141db9bc8fbacc6a3bec6ec4d47e259b6dba912c99725b1bf6549679240cde06761950776cf5cdeb4d0af81d4fa04aaae061f4ddce6b7e59a0c11a659658398816de2b048ef415f4016ef816c0a041b96aba3c0599ffdcdea45c25c6ac2326eb5688" + "record": "0410f8fcc21f579b9620e55c5f05526115ddcb1db03cab55a93da8e8ef770b63f74d2def8fdfe11406b23e41bd808cc852f70991c0b883bac61221011ebd974f3bf4cf07113cd5b1c76d85395dcf96f0780948c089a1455ba96dbb783f80c896a3fcf94887483aa4b426380485bf2ba1fa7c608e998043c64a70047c4e0b38821f10be89af5f6c1d6876963139c78d99a5037d2647b0cee0637d850abb16ce90593b0dccb376e1ff2ed7e0a047cfe5db29cd9e96d15ccd883f71ad1300702d4211be0154020c297b775a537d1e7eb0c3810fca23da9fba1ded617696ef843f8c922524360313086a76badcf33688083c9b2829e4d1c7f2ddddf5e2eccf0c5d6e2104b0c75e12892d33bef24e97c4be12de8e6c2e83bd8acfa64b304e7adcfbbf720b6b7adf4662567323e087a398cad3eef292ce5daee1038ca39fb68924cabe67c42273935b8aad96f38b35c2b1a6e7334920c192a65e01b10e4548318787675df9060e7b61281658ad520b54d4d360e5bbcd6835ac58eadb8d30de4a581724fbf561edddc8f388611d8950a3a31c18a408e152a6354f9a5be29ad0a9b129f2d6f243e03a075b47287b2637dc7ec853a2f4a914245e4e6f56f09680329b92898fa5c3a60a1ab18a745d9752bde353f856b8fde86d5ffd1214f06f418dfb9a1c3ec321be2c9a192fd910700ff1c5b28bac062fd593a166a2c952f0d3ff53bf03a54ecb8cc6e15670a289921b8c0291ddc5236273e0080477a5733bdaee12b0d47d31b27ca9db333671ee4aaac54b77c1accbf65d6a3f529ee882753194454fc004978ac6c6efe6776f054dec698318914578de4f77c31aba41100a6ceb22149d0eb9869ebeefcdbe3d733fe6238849a35ee2b34e05769535b52c57b0048a3b400e69ad263a48aa39877cc12ce624f32f5c0abd5dc84a49915414f9ec5508f9268d8008212624ef20c72dd23d05e26800fcedb9bca3955d8ba95667814b5481f40d94b3e02ad3307119e6008b4b6b15e75ee91ac545f6bb766471715042a67f12b0f476fd9c1f103d7fb4ff1a39045e2900d7c36d4bb2995230d2649cebab95fc423c08fda8943eb6e6335699bc3ba88ac8f265d6a134910a7dcdd4764c1840bff9b140a3048bfe42b6b1cbc0706ed23bddce02365495c690b29b49ecd9945965e14a47dd030e82fd47a7713355ab79e26dcad2bb7a6d3b4b43e4b55a228557ea2e267fe2e80d1cb206cb7555b0fcfac753f7669cf624901f2e153a1e073f9a825c78e259bedc17b802a2b405e79ab5e0e0e45f1554287dc3614509c35f98f429c9ce296867661ab5968524b1ee6d88d2bb2077f328d503ff5f380f3c26cdecd0c6d9ed883d6294642decec7ee87bca462db47b0bc7380886fdf90258c7c3434dae16e2bce19ebb74b1d2a661f82db7ea440a861071df470f54ad2aa59692a8cc58dfe4f496edab0ec2a4e854b6c43f931fd4" }, { "name": "challenge (ví dụ đóng gói)", @@ -90,7 +90,7 @@ "name": "rlen ngoài {272,1040,4112}", "reason": "rlen_out_of_bounds", "rlen": 273, - "record": "0111d9ef0142422f2c2706dc8f71fcd1d9bf6314dd05a5dc5edb5c36ca78043e65fe89a6e5fa14a502cb132233f9b3412da6e3a3fc71ce2035eac2657ba82a7809e73beabb1f11752a5685eb34139635583a88a4f31fec312786b905956302a81eefbde9e4472f155c904ce3fbd199a0c9cd9f4bf75a911fd5ef2e4239dd5fff9e89572261f169f1fcd512b346e4e09ce34732283f84190925c7ed45246c93da7b7ec7bb7d3aa45018e372b410e3796e1b4b98bbf4cc950d641f47181e04ee7f75837df3eb20fc38371b0e573e4ce7c8807fbb1e137bdd46c458e5ede5d212486725fa154cc7e6c97c97fce99dab5caab9478286037aafb3af36543720de089a4a53b8df4dfd5e80c5623193cc896b8644e100", + "record": "0111fc87296ac3ccb514c06d82912135d426ca2e07a1fc4a707b2ef2c943e8811b13fc899736fae24a23ab3f299fdbc525733e409428190c76d618c501e9f61987c39e28f16b35b0bbbeba8b7fecdc4d1bf3e084f0e495391dc92364c4200cebd0ab034b7507fad7702e8fc81ed737dc221fc3bea2c4c8e1c622991bd07d470e54bfc8587c9ea94070f77245ed6c939b35084ba43b79a03ef2968d53b9ba98ea62786031b96cde16f1dc45e5913f6fd6f3a3004e03373efac7f6ce0ca982987a2ef41ab66f7bf929f3bba92894a3f46669f0007ad3a17328aa1a4e5465e5743954f4028f599dd05b2780d1dce30e8d1a6f87382b784159397271d9c9f66bc7c834f7f0a8bcede83a311a0778643d712f9da900", "expect": "rlen không khớp 272/1040/4112 → đóng kênh ngay, không cấp phát đọc tiếp thân bản ghi" }, { @@ -106,14 +106,14 @@ "reason": "unknown_chan", "chan": "0x03", "plaintext": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "record": "0110790a900781091f03b09ec8b4541c794c7d8dccf7f76580664867fb6219271ef45215de48df37c48abf8f60ca3e557e4b87a14e9612e2d4a348d1c8522c458f2b6818429c1043ca6507ee2c20bc4b37f92bb8ad9d69d7376b5eff6a3c41c9398cba931620cde80acbb8b8f812f82e423e197a9df680b6127e84f205ff8dfa31b398e175b3e98ff230f0b13c9b1a2c11ba8295d639f44c9410aac82f6a1645adf716d85c505667297ab178e8d967bb65869c33de16e9c8bc1abf4d8bbd4731909e835f3b18dfc57a83aabe071459e327ad52b0cb2c94c635598eb6199c99a7e0217d354e190559283a171a8a9a248b2ff0ddf84f1e1094dfa428e4035f6b2c4de29aec982b72c15704c6635eaafdbe7cef", + "record": "0110806908a31403380876b3b588eb1b26818968ffdb71e6de623d857f9b186abcf1fd3b00ce13c174ad9763530571c6adc9aa00da27c7412955d5465f214d120e7093b43bd59dd904058c99930a51b0a6cf9cfed57a2aa493d43a16dcbcab711acf3a5bb8f4c356b16fdcd1deed98fe4a52fe1c3e6a56e2641e61a69e40339e7af3d62a828266ce5b57209bbd50a484c0107c2707e1c0406a1d51abfd818c7aa3c87f9dc42f59b965dab4793b8e6dc708cb7d8b9986f460cd9aba016a66a3ae1f5fb0f859aad2e4ebf4130c4bb6474acf43919e38322e538876d6ee9b29971f86e96898e87f352fc9e674665ec4339843b300ff6f7c247d0f60d0ee9760f9a2118ed93268e941ed373347615a8126e0a252", "expect": "chan = 0x03 không phải 0x01 (control) hay 0x02 (bulk) → đóng liên kết" }, { "name": "more=1 nhưng long=0 (làn ngắn)", "reason": "more_without_long", "plaintext": "0101000aa78dd42688b494ca7e8c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "record": "0110b6a8d63b052103bd49b40621f45a4423af6e9804384fc4f46d0bc1e0757685d0465b51c48639d887b04bb7fc98e95e4a1b909df1a25f39a9793b48417fd91b3acdc6629010763e284df23b8f7c50c6ab60630073c944c9194d32360ae57e705928b5586ca79e65631dfd67e1e554a88ebdc0ac335cb782645f4e2cce183572ee1d16a68a24cd1ac660e4477c0b55be13871a8dc9f501892a018c3a4b264ea70dfe9ed2eb2f528b560c01f1191255bacf8bd2180b80a2498a4b8d15fc1527e73fd56350553b3dfd809ad1bf7670f36b209a1aaefd85cfda1a8d92aa429d225de93f9e768e5ed1681049d949001db2149caab4e53f32dae58ccbfab93643982f95e0e1dcf00fbf34b49c2e35560711ecf9", + "record": "01108e1f89a3cc0b6626c6d854c99ffb57faee1e58239e8fcd35a0056fa48c6ddb327f932f3e146594366bc05044adb4ab5fd8ecc93672cb75640a861743b78cddcffe58ebf23ee24e5ecec2baf6a21efadcb74552f9a6a9fd21a7da9024098f087c861a6a6fcbe98dbdd4c53fbd9f25cd3def99fae8e14d9b71543caa2dca6aaab984a2dc07d6c95f4991facdb1f7fb1aaddad3cb6e1ce753642f7783acb7ce35c91baf03f3c4f792728a95cab50f91cce59fbb87fe51cf04c98f569d6a717d3396080e17f2eb886fa051f4ca3aedaca72aefd40fa8c4774efd1605cc9ac8735d395a3fadf8eb0f9c41b6acd6a555039c5389a1e2058ed0926d68c191ffc007c6e6f4836b50d10073ec6c954f3dfa5fd9a2", "expect": "làn ngắn (long=0) không bao giờ có more=1 (một làn ngắn luôn đúng một mảnh) → đóng liên kết" }, { diff --git a/test-vectors/bt-link.json b/test-vectors/bt-link.json index 4756fa6..f72630f 100644 --- a/test-vectors/bt-link.json +++ b/test-vectors/bt-link.json @@ -73,33 +73,44 @@ }, "h_after_psk": "9b0e5a8a31cd403e0b2ef56bdee37911278e8000650ba8f6ec2dfb4efc122a92", "ck_after_psk": "c6993109572a2d2e1142dc1bc4ce5e7d93c00ba623cca36b2a5f62308c1254c2", - "k_message1": "3493639e64b3e9889eab61e664b7b2db73e4c029e67411df3ef2b6f12769d663", + "mix_key_e_c": { + "temp_key": "992aa59b6b45ff14691fbab352d1f1b6b6d558d971a1b9a3d6d8aeb6b9996d41", + "ck": "a496535f65bdea1ca2aedf11d16f1f8dc7128e23de83876fa11e7528a5d35192", + "temp_k": "79429352c7eaa1fb9f7534bf8e3a93c03a447e9c396ec2b2b57790f5129a6997" + }, + "ck_after_e_c": "a496535f65bdea1ca2aedf11d16f1f8dc7128e23de83876fa11e7528a5d35192", + "k_message1": "79429352c7eaa1fb9f7534bf8e3a93c03a447e9c396ec2b2b57790f5129a6997", "e_priv_c": "6957d0f71dd22be6ea81d25694fd1abd2eed41d4fe5daa62af2bde2b3c8e957a", "e_pub_c": "3ba31c49f8186b67851aee28dad3688d20e5b38995cd53193d332f03b145960f", "h_after_e_c": "bd7216420d0896066bb98d44504e3c4945af95b92188f1531744db2ada017941", - "ciphertext_tag_1": "0ad750d42ff823ed2fa9111237c6af1f", - "h_after_message1": "1fdebd6ec8abf0b58b949c296a3a1d8aba2023087e81f96a37a2c319b5bb47ec", - "message1": "3ba31c49f8186b67851aee28dad3688d20e5b38995cd53193d332f03b145960f0ad750d42ff823ed2fa9111237c6af1f", + "ciphertext_tag_1": "f759c40f6fcf7a7ece21ec8aa86264f8", + "h_after_message1": "b9235dc4090d80a0d97d3bcf513e4c8d9d9beffbfb26071c2c77bf451b000371", + "message1": "3ba31c49f8186b67851aee28dad3688d20e5b38995cd53193d332f03b145960ff759c40f6fcf7a7ece21ec8aa86264f8", "noise1_kind": "0x02", - "noise1_record": "0031023ba31c49f8186b67851aee28dad3688d20e5b38995cd53193d332f03b145960f0ad750d42ff823ed2fa9111237c6af1f", + "noise1_record": "0031023ba31c49f8186b67851aee28dad3688d20e5b38995cd53193d332f03b145960ff759c40f6fcf7a7ece21ec8aa86264f8", "e_priv_s": "e9948f4fd3e704551937595a00d3fe9b3fc018838941dc752e9cb6b9d63aba1b", "e_pub_s": "27c5f172bf3ac8d482b86c452d5a6b1d02ba7aa31786a4e5ad04e805c272db3a", - "h_after_e_s": "4894fca1d7c42c42d73e730b00c8c51c77b3be058c8bb63467df4a1f69b8af26", + "h_after_e_s": "1fcef945c1f79c4b788742d7076d890aeeae6b9005a82e270f3131d4b7db168a", + "mix_key_e_s": { + "temp_key": "6b365205ebf84636b230df2dc9f8d6dc1cb871c6a083b44b641b0fe456b11047", + "ck": "4068d72ab3eb25403fc6058a5fe90e6ff9614ab18bcb29d421eedd92892f5a20", + "temp_k": "05a65608bb2cbce4a8b056e093747e6d7cdfbcb83904cf2e1f162e3df582c61a" + }, "dh_ee": "3f592867b0e2821dc88dc14923ddf403ad7447ab4868207a60b812de220d5b14", "mix_key_ee": { - "temp_key": "805df8f644c22395a76b05d27bbb14755351c617b6f78938e003c546fad418d2", - "ck": "c304ad50ba19611d155399f774a1ee642cba48d491a9c34ea98d53e561203206", - "temp_k": "9ba3fecf8e71d6fb34777e828a18fe3aad59284a3b3122e0b7c1850dd5041185" + "temp_key": "65bff845c1d0e8bb7cf80673bcfed09e0059f0b3e69d2ff944e0d3e4c3b47c08", + "ck": "0eb39273fe05c0f078de05abb581f220838d876a1d466d6d2fd7766d1ef9db89", + "temp_k": "8a2f696cf4d0e5833d501cca4f23dca6b46dcac0096f8b5973b2c1855da8db9a" }, - "ck_after_ee": "c304ad50ba19611d155399f774a1ee642cba48d491a9c34ea98d53e561203206", - "k_message2": "9ba3fecf8e71d6fb34777e828a18fe3aad59284a3b3122e0b7c1850dd5041185", - "ciphertext_tag_2": "491f6def3a79e9abe9ccc8cc6651e16a", - "h_final": "1cb5548c90a38400772f804bbc734d5b72a4259b58aae26c19c2dae533113066", - "message2": "27c5f172bf3ac8d482b86c452d5a6b1d02ba7aa31786a4e5ad04e805c272db3a491f6def3a79e9abe9ccc8cc6651e16a", + "ck_after_ee": "0eb39273fe05c0f078de05abb581f220838d876a1d466d6d2fd7766d1ef9db89", + "k_message2": "8a2f696cf4d0e5833d501cca4f23dca6b46dcac0096f8b5973b2c1855da8db9a", + "ciphertext_tag_2": "ecc9c3f55d5df67b922f8f22177708ce", + "h_final": "df619d84c8e466cf942e4b4a561ccacd1a6cb0c18a5fbe71a1cc05eee6822bbb", + "message2": "27c5f172bf3ac8d482b86c452d5a6b1d02ba7aa31786a4e5ad04e805c272db3aecc9c3f55d5df67b922f8f22177708ce", "noise2_kind": "0x03", - "noise2_record": "00310327c5f172bf3ac8d482b86c452d5a6b1d02ba7aa31786a4e5ad04e805c272db3a491f6def3a79e9abe9ccc8cc6651e16a", - "c1": "6f28e2cbf21a0db47559d8a1b9632c1039d05c8de44738d92aac02c4db101637", - "c2": "8e9590365dcb92771ad591311f68e4246341cfc38daa405c41b624002c63ba3b", + "noise2_record": "00310327c5f172bf3ac8d482b86c452d5a6b1d02ba7aa31786a4e5ad04e805c272db3aecc9c3f55d5df67b922f8f22177708ce", + "c1": "a6d3dd0fccbbe0013127a75daa27ab07fd52c21cc7f50a3383fe60d3e5e10dbc", + "c2": "3288442eb16d11d2882265b638470fd1211854d19f7ff5f266606ed12ee8195e", "c1_note": "c1 mã hoá bởi C (khởi tạo), giải mã bởi S", "c2_note": "c2 mã hoá bởi S, giải mã bởi C" } @@ -110,7 +121,7 @@ "reason": "wrong_key", "n_s": "2cb3744ccb10882cef6e96d024d80bff5601132eced64c9ab09f7605d7bb2651", "e_pub_c": "3ba31c49f8186b67851aee28dad3688d20e5b38995cd53193d332f03b145960f", - "ciphertext_tag_1": "0ad750d42ff823ed2fa9111237c6af1f", + "ciphertext_tag_1": "f759c40f6fcf7a7ece21ec8aa86264f8", "wrong_psk_source": "k_bt_link của cặp 2", "wrong_psk": "fcdddc4e4638ed616aba885ef26218e63e14f091f1e0c9ec840366877fea3d7d" }, @@ -119,21 +130,21 @@ "reason": "tag_mismatch_noise1", "n_s": "2cb3744ccb10882cef6e96d024d80bff5601132eced64c9ab09f7605d7bb2651", "e_pub_c": "3ba31c49f8186b67851aee28dad3688d20e5b38995cd53193d332f03b145960f", - "ciphertext_tag_1": "0ad750d42ff823ed2fa9111237c6af1e" + "ciphertext_tag_1": "f759c40f6fcf7a7ece21ec8aa86264f9" }, { "name": "cặp 1 / tag noise2 sai", "reason": "tag_mismatch_noise2", "n_s": "2cb3744ccb10882cef6e96d024d80bff5601132eced64c9ab09f7605d7bb2651", "e_pub_s": "27c5f172bf3ac8d482b86c452d5a6b1d02ba7aa31786a4e5ad04e805c272db3a", - "ciphertext_tag_2": "491f6def3a79e9abe9ccc8cc6651e16b" + "ciphertext_tag_2": "ecc9c3f55d5df67b922f8f22177708cf" }, { "name": "cặp 1 / phát lại noise1 trên kênh khác (n_s khác)", "reason": "replayed_channel", "n_s": "80982dfa4fb88f90dc856740bca0043664726aa1d2231f940819449553d808f1", "e_pub_c": "3ba31c49f8186b67851aee28dad3688d20e5b38995cd53193d332f03b145960f", - "ciphertext_tag_1": "0ad750d42ff823ed2fa9111237c6af1f" + "ciphertext_tag_1": "f759c40f6fcf7a7ece21ec8aa86264f8" } ] } diff --git a/test-vectors/bt-pairing.json b/test-vectors/bt-pairing.json index 674e74d..7d3fe87 100644 --- a/test-vectors/bt-pairing.json +++ b/test-vectors/bt-pairing.json @@ -51,18 +51,18 @@ "h": "9ef7ca8694e03435f4cdc5de84c258bbb69179dc685f817c0139739589f08199" }, "h_after_psk": "9ef7ca8694e03435f4cdc5de84c258bbb69179dc685f817c0139739589f08199", - "ck_after_psk": "3ba0261b99a6ab88fb8fee3f6204b310b6edbaf59aad56d8e676fe1835bca22e", - "k": "de6117d2b12eead9c6b282d772b909920916d8fb1ef43bd1caa24ab037606311", + "ck_after_psk": "0fedd0a3f07e1abd54695065fdc83462fc2b2ef89a2532509c6090648aaca51e", + "k": "9491cbf574b16d2b036724c0f8566f335935fa899230540ec6beb83db64ae27b", "e_priv": "9373bf265905f54866bd4566421bcf20bb7c6bf255e0afbdf6df27a6fd873d7b", "e_pub": "676f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a550", "h_after_e": "92ce9e8eb3761a90c9602e7b0b38dfff8855c89bd3bd6a7cacdbccf7ed669428", "nonce": "000000000000000000000000", - "ciphertext_tag": "4d394b4515b386429b20d99902aafb7c", - "h_final": "6afe72063c65fdf2b9772f1bd4bd72ba13a69c7cf64f7c0508cc76d3d280330b", - "message1": "676f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a5504d394b4515b386429b20d99902aafb7c", + "ciphertext_tag": "01e0ca15311784bfa449d0d7b498db35", + "h_final": "ab1cf064715202bb93ab8cf45f43de39ab492e17ee8ccbe79954fe9e17075e25", + "message1": "676f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a55001e0ca15311784bfa449d0d7b498db35", "record_kind": "0x02", "rlen": 49, - "record": "003102676f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a5504d394b4515b386429b20d99902aafb7c" + "record": "003102676f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a55001e0ca15311784bfa449d0d7b498db35" } ], "invalid_vectors": [ @@ -71,7 +71,7 @@ "reason": "wrong_key", "n_s": "641cacb99fc22372cc2058145a54dda486a41d8c8aaf0d2e14a7153f0300420e", "e_pub": "676f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a550", - "ciphertext_tag": "4d394b4515b386429b20d99902aafb7c", + "ciphertext_tag": "01e0ca15311784bfa449d0d7b498db35", "wrong_psk_source": "k_bt_plink của cặp 2", "wrong_psk": "a705d680b0be1037fc8ac0dd02c2803f7e55795564df81f55b5e9fc5c6364cec" }, @@ -80,21 +80,21 @@ "reason": "tag_mismatch", "n_s": "641cacb99fc22372cc2058145a54dda486a41d8c8aaf0d2e14a7153f0300420e", "e_pub": "676f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a550", - "ciphertext_tag": "4d394b4515b386429b20d99902aafb7d" + "ciphertext_tag": "01e0ca15311784bfa449d0d7b498db34" }, { "name": "cặp 1 / e_pub bị sửa trên đường truyền", "reason": "message_tampered", "n_s": "641cacb99fc22372cc2058145a54dda486a41d8c8aaf0d2e14a7153f0300420e", "e_pub": "666f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a550", - "ciphertext_tag": "4d394b4515b386429b20d99902aafb7c" + "ciphertext_tag": "01e0ca15311784bfa449d0d7b498db35" }, { "name": "cặp 1 / phát lại message 1 trên kênh khác (n_s khác)", "reason": "aad_mismatch", "n_s": "5300745f6a02d27bb53c2c9852d204fcfbae14f9bfd39fc3165dfebacc2c0c6f", "e_pub": "676f48018c525ed5a76a0021178ba1465cb3b0bef455d5d857abfec88d18a550", - "ciphertext_tag": "4d394b4515b386429b20d99902aafb7c" + "ciphertext_tag": "01e0ca15311784bfa449d0d7b498db35" } ] } diff --git a/tools/vectors/build_bt_link_vectors.py b/tools/vectors/build_bt_link_vectors.py index 3d1d6ff..5313e4d 100644 --- a/tools/vectors/build_bt_link_vectors.py +++ b/tools/vectors/build_bt_link_vectors.py @@ -58,12 +58,15 @@ def _handshake_vector(pv: dict, link: bytes) -> dict: "challenge_kind": "0x01", "challenge_record": challenge_record.hex(), "h0": m1["h0"].hex(), "h_after_prologue": m1["h1"].hex(), "mix_key_and_hash_psk": {k: v.hex() for k, v in m1["mix_key_and_hash"].items()}, - "h_after_psk": m1["h2"].hex(), "ck_after_psk": m1["ck1"].hex(), "k_message1": m1["k"].hex(), + "h_after_psk": m1["h2"].hex(), "ck_after_psk": m1["ck_after_psk"].hex(), + "mix_key_e_c": {k: v.hex() for k, v in m1["mix_key_e"].items()}, + "ck_after_e_c": m1["ck1"].hex(), "k_message1": m1["k"].hex(), "e_priv_c": m1["e_priv"].hex(), "e_pub_c": m1["e_pub"].hex(), "h_after_e_c": m1["h3"].hex(), "ciphertext_tag_1": m1["ciphertext_tag"].hex(), "h_after_message1": m1["h4"].hex(), "message1": m1["message1"].hex(), "noise1_kind": "0x02", "noise1_record": noise1_record.hex(), "e_priv_s": m2["e_priv"].hex(), "e_pub_s": m2["e_pub"].hex(), "h_after_e_s": m2["h5"].hex(), - "dh_ee": m2["dh"].hex(), "mix_key_ee": {k: v.hex() for k, v in m2["mix_key_ee"].items()}, + "mix_key_e_s": {k: v.hex() for k, v in m2["mix_key_e"].items()}, "dh_ee": m2["dh"].hex(), + "mix_key_ee": {k: v.hex() for k, v in m2["mix_key_ee"].items()}, "ck_after_ee": m2["ck2"].hex(), "k_message2": m2["k"].hex(), "ciphertext_tag_2": m2["ciphertext_tag"].hex(), "h_final": m2["h6"].hex(), "message2": m2["message2"].hex(), "noise2_kind": "0x03", "noise2_record": noise2_record.hex(), "c1": split_s["c1"].hex(), "c2": split_s["c2"].hex(), @@ -75,7 +78,7 @@ def _negatives(pv1: dict, pv2: dict, link1: bytes, link2: bytes, hs: dict) -> li n_s, prologue = H(hs["n_s"]), H(hs["prologue"]) e_pub_c, ct1 = H(hs["e_pub_c"]), H(hs["ciphertext_tag_1"]) e_pub_s, ct2 = H(hs["e_pub_s"]), H(hs["ciphertext_tag_2"]) - ck1, h4 = H(hs["ck_after_psk"]), H(hs["h_after_message1"]) + ck1, h4 = H(hs["ck_after_e_c"]), H(hs["h_after_message1"]) out = [] assert N.try_open_first_message(link2, prologue, e_pub_c, ct1) is None diff --git a/tools/vectors/noise_nnpsk0.py b/tools/vectors/noise_nnpsk0.py index 0c36ca6..098e878 100644 --- a/tools/vectors/noise_nnpsk0.py +++ b/tools/vectors/noise_nnpsk0.py @@ -3,6 +3,11 @@ mode") và bắt tay bảo mật liên kết đầy đủ (bước 1–5, psk = `K_bt_link`, dùng cho các cặp đã ghép). HASHLEN = 32 (SHA-256) nên temp_k/temp_k1/temp_k2 không bị cắt (quy tắc cắt chỉ áp dụng khi HASHLEN = 64). +Rev. 34 §9.2 (chế độ psk): mỗi token "e" làm MixHash(e.pub) RỒI MixKey(e.pub) — không chỉ MixHash như pattern +không-psk. Thiếu bước MixKey(e.pub) này (lỗi #3418, bắt được qua kiểm chéo bằng thư viện `noiseprotocol` thật) +làm ck lan sai sang bước kế tiếp: message 1 mã nhầm bằng k của token "psk" thay vì k của token "e"; message 2 +tính "ee" trên ck sai nên ck2/Split() cũng sai theo, dù `e`/`e.pub` trên dây vẫn đúng (không đổi). + Không phải vector công bố của tài liệu Noise — draft/tài liệu Noise không phát hành vector cho psk/prologue riêng của HandLive. Đây là cách HandLive dùng đúng thuật toán của tài liệu đó (InitializeSymmetric, MixHash, MixKeyAndHash, MixKey, EncryptAndHash, Split) cho bộ psk/prologue của mình, nên cần một vòng rà soát riêng (không @@ -43,20 +48,25 @@ def mix_key_and_hash(ck: bytes, h: bytes, ikm: bytes) -> dict: def first_message(psk: bytes, prologue: bytes, e_priv: bytes) -> dict: - """Dựng message 1 ("psk, e") của C→S. `message1` = e.pub (32) ‖ EncryptAndHash("") (16, chỉ có tag vì - plaintext rỗng).""" + """Dựng message 1 ("psk, e") của C→S. Noise rev. 34 §9.2 (chế độ psk): mỗi token "e" làm MixHash(e.pub) RỒI + MixKey(e.pub) — khác pattern không-psk, nơi "e" chỉ MixHash. `k` dùng để mã hoá payload vì vậy là k SAU + MixKey(e.pub) của token "e", không phải k của token "psk" (bị đè ngay). `message1` = e.pub (32) ‖ + EncryptAndHash("") (16, chỉ có tag vì plaintext rỗng).""" h0, ck0 = initialize_symmetric() h1 = _sha256(h0 + prologue) # MixHash(prologue) - psk_step = mix_key_and_hash(ck0, h1, psk) - h2, ck1, k = psk_step["h"], psk_step["ck"], psk_step["temp_k"] + psk_step = mix_key_and_hash(ck0, h1, psk) # token "psk": MixKeyAndHash + h2, ck_psk = psk_step["h"], psk_step["ck"] e_pub = x25519_pub(e_priv) h3 = _sha256(h2 + e_pub) # MixHash(e.pub); khóa công khai tạm thời không bao giờ được mã hóa + e_step = mix_key(ck_psk, e_pub) # token "e", phần thêm của chế độ psk: MixKey(e.pub) + ck1, k = e_step["ck"], e_step["temp_k"] nonce = bytes(4) + (0).to_bytes(8, "little") # n = 0 (lần ENCRYPT đầu tiên của hướng này) - ciphertext = ChaCha20Poly1305(k).encrypt(nonce, b"", h3) # EncryptAndHash(""), ad = h3 + ciphertext = ChaCha20Poly1305(k).encrypt(nonce, b"", h3) # EncryptAndHash(""), ad = h3 (MixKey không đụng h) h4 = _sha256(h3 + ciphertext) # MixHash(ciphertext) return {"protocol_name": PROTOCOL_NAME, "h0": h0, "prologue": prologue, "h1": h1, "psk": psk, - "mix_key_and_hash": psk_step, "h2": h2, "ck1": ck1, "k": k, "e_priv": e_priv, "e_pub": e_pub, - "h3": h3, "nonce": nonce, "ciphertext_tag": ciphertext, "h4": h4, "message1": e_pub + ciphertext} + "mix_key_and_hash": psk_step, "h2": h2, "ck_after_psk": ck_psk, "mix_key_e": e_step, "ck1": ck1, "k": k, + "e_priv": e_priv, "e_pub": e_pub, "h3": h3, "nonce": nonce, "ciphertext_tag": ciphertext, "h4": h4, + "message1": e_pub + ciphertext} def try_open_first_message(psk: bytes, prologue: bytes, e_pub: bytes, ciphertext_tag: bytes) -> bytes | None: @@ -65,8 +75,10 @@ def try_open_first_message(psk: bytes, prologue: bytes, e_pub: bytes, ciphertext h0, ck0 = initialize_symmetric() h1 = _sha256(h0 + prologue) psk_step = mix_key_and_hash(ck0, h1, psk) - h2, k = psk_step["h"], psk_step["temp_k"] + h2, ck_psk = psk_step["h"], psk_step["ck"] h3 = _sha256(h2 + e_pub) + e_step = mix_key(ck_psk, e_pub) + k = e_step["temp_k"] nonce = bytes(4) + (0).to_bytes(8, "little") try: return ChaCha20Poly1305(k).decrypt(nonce, ciphertext_tag, h3) @@ -84,17 +96,20 @@ def mix_key(ck: bytes, ikm: bytes) -> dict: def second_message(ck1: bytes, h_after_message1: bytes, e_priv_s: bytes, e_pub_c: bytes) -> dict: """Dựng message 2 ("e, ee") của S→C, tiếp theo sau `first_message`. `ck1`/`h_after_message1` = `ck1`/`h4` của - `first_message`. `message2` = e.pub của S (32) ‖ EncryptAndHash("") (16).""" + `first_message`. Như message 1, token "e" ở chế độ psk làm MixHash RỒI MixKey(e.pub) trước khi token "ee" làm + MixKey(DH) — `k` dùng để mã hoá payload là k SAU "ee", vì "ee" đến sau và đè k của "e". `message2` = e.pub + của S (32) ‖ EncryptAndHash("") (16).""" e_pub_s = x25519_pub(e_priv_s) h5 = _sha256(h_after_message1 + e_pub_s) # MixHash(e.pub) của token "e" + e_step = mix_key(ck1, e_pub_s) # token "e", phần thêm của chế độ psk: MixKey(e.pub) dh = _x25519_dh(e_priv_s, e_pub_c) - ee_step = mix_key(ck1, dh) # token "ee" + ee_step = mix_key(e_step["ck"], dh) # token "ee": MixKey(DH(e,re)) ck2, k = ee_step["ck"], ee_step["temp_k"] nonce = bytes(4) + (0).to_bytes(8, "little") # n = 0, k vừa được InitializeKey lại - ciphertext = ChaCha20Poly1305(k).encrypt(nonce, b"", h5) + ciphertext = ChaCha20Poly1305(k).encrypt(nonce, b"", h5) # ad = h5 (MixKey không đụng h) h6 = _sha256(h5 + ciphertext) - return {"h5": h5, "dh": dh, "mix_key_ee": ee_step, "ck2": ck2, "k": k, "e_priv": e_priv_s, "e_pub": e_pub_s, - "nonce": nonce, "ciphertext_tag": ciphertext, "h6": h6, "message2": e_pub_s + ciphertext} + return {"h5": h5, "mix_key_e": e_step, "dh": dh, "mix_key_ee": ee_step, "ck2": ck2, "k": k, "e_priv": e_priv_s, + "e_pub": e_pub_s, "nonce": nonce, "ciphertext_tag": ciphertext, "h6": h6, "message2": e_pub_s + ciphertext} def try_open_second_message(ck1: bytes, h_after_message1: bytes, e_priv_c: bytes, e_pub_s: bytes, @@ -102,8 +117,9 @@ def try_open_second_message(ck1: bytes, h_after_message1: bytes, e_priv_c: bytes """Phía C: mở message 2. Trả (plaintext hoặc None, ck2, h6) — `ck2`/`h6` cần cho `split()` dù `plaintext` là None (để caller tự quyết có đóng kênh hay không, không lộ gì qua số lượng giá trị trả về).""" h5 = _sha256(h_after_message1 + e_pub_s) + e_step = mix_key(ck1, e_pub_s) dh = _x25519_dh(e_priv_c, e_pub_s) - ee_step = mix_key(ck1, dh) + ee_step = mix_key(e_step["ck"], dh) ck2, k = ee_step["ck"], ee_step["temp_k"] nonce = bytes(4) + (0).to_bytes(8, "little") h6 = _sha256(h5 + ciphertext_tag) diff --git a/tools/vectors/verify_bt_link_checks.py b/tools/vectors/verify_bt_link_checks.py index 88a31eb..48de9a6 100644 --- a/tools/vectors/verify_bt_link_checks.py +++ b/tools/vectors/verify_bt_link_checks.py @@ -30,7 +30,7 @@ def _hmac(key: bytes, msg: bytes) -> bytes: def _mix_key_and_hash(h: bytes, ck: bytes, ikm: bytes) -> tuple[bytes, bytes, bytes]: - """Token "psk": 3 ngõ ra qua HMAC (khác token "ee", chỉ 2 ngõ ra) — trả (h mới, ck mới, k mới).""" + """Token "psk": 3 ngõ ra qua HMAC (khác token "ee"/"e" ở chế độ psk, chỉ 2 ngõ ra) — trả (h mới, ck mới, k mới).""" temp_key = _hmac(ck, ikm) out1 = _hmac(temp_key, b"\x01") out2 = _hmac(temp_key, out1 + b"\x02") @@ -39,7 +39,7 @@ def _mix_key_and_hash(h: bytes, ck: bytes, ikm: bytes) -> tuple[bytes, bytes, by def _mix_key(ck: bytes, ikm: bytes) -> tuple[bytes, bytes]: - """Token "ee": trả (ck mới, k mới), KHÔNG đụng h.""" + """Token "ee", hoặc token "e" ở chế độ psk (Noise rev. 34 §9.2, #3418): trả (ck mới, k mới), KHÔNG đụng h.""" temp_key = _hmac(ck, ikm) out1 = _hmac(temp_key, b"\x01") out2 = _hmac(temp_key, out1 + b"\x02") @@ -60,20 +60,24 @@ def _open_chacha(k: bytes, aad: bytes, ciphertext_tag: bytes): def _open_message1(psk: bytes, prologue: bytes, e_pub_c: bytes, ciphertext_tag: bytes): - """Trả (mở cryptography, mở libsodium, h sau token "e" của C, ck sau token "psk").""" + """Trả (mở cryptography, mở libsodium, h sau token "e" của C, ck sau token "e" — #3418: chế độ psk làm + MixHash(e.pub) RỒI MixKey(e.pub), nên k để mở tag KHÔNG phải k của token "psk").""" h0 = _sha256(PROTOCOL_NAME) h1 = _sha256(h0 + prologue) - h2, ck1, k1 = _mix_key_and_hash(h1, h0, psk) + h2, ck_psk, _ = _mix_key_and_hash(h1, h0, psk) h3 = _sha256(h2 + e_pub_c) + ck1, k1 = _mix_key(ck_psk, e_pub_c) a, b = _open_chacha(k1, h3, ciphertext_tag) return a, b, h3, ck1 def _open_message2(ck1: bytes, h_after_m1: bytes, e_priv_c: bytes, e_pub_s: bytes, ciphertext_tag: bytes): - """Trả (mở cryptography, mở libsodium, h sau token "e" của S, ck sau token "ee").""" + """Trả (mở cryptography, mở libsodium, h sau token "e" của S, ck sau token "ee"). `ck1` phải là ck SAU token + "e" của message 1 (không phải ck của token "psk" một mình) — #3418.""" h5 = _sha256(h_after_m1 + e_pub_s) + ck_e, _ = _mix_key(ck1, e_pub_s) # token "e" của S, phần thêm của chế độ psk dh = x25519(e_priv_c, e_pub_s) - ck2, k2 = _mix_key(ck1, dh) + ck2, k2 = _mix_key(ck_e, dh) # token "ee" a, b = _open_chacha(k2, h5, ciphertext_tag) return a, b, h5, ck2 @@ -112,6 +116,7 @@ def _check_handshake(c, n: str, v: dict) -> None: a1, b1, h3, ck1 = _open_message1(psk, prologue, H(v["e_pub_c"]), H(v["ciphertext_tag_1"])) c.eq(f"{n} h_after_e_c", h3.hex(), v["h_after_e_c"]) + c.eq(f"{n} ck_after_e_c", ck1.hex(), v["ck_after_e_c"]) c.eq(f"{n} mở được noise1 (cryptography)", a1, b"") c.eq(f"{n} mở được noise1 (libsodium)", b1, b"") c.eq(f"{n} message1 = e_pub_c ‖ ciphertext_tag_1", v["message1"], v["e_pub_c"] + v["ciphertext_tag_1"]) @@ -141,7 +146,7 @@ def _check_negative(c, n: str, v: dict, hs: dict, link: dict) -> None: reason = v["reason"] c.true(f"{n} reason nằm trong tập đã định", reason in REASONS) if reason == "tag_mismatch_noise2": - ck1 = H(hs["ck_after_psk"]) + ck1 = H(hs["ck_after_e_c"]) a, b, _, _ = _open_message2(ck1, H(hs["h_after_message1"]), H(hs["e_priv_c"]), H(v["e_pub_s"]), H(v["ciphertext_tag_2"])) c.eq(f"{n} phải bị từ chối (cả hai đường)", (a, b), (None, None)) diff --git a/tools/vectors/verify_bt_pairing_checks.py b/tools/vectors/verify_bt_pairing_checks.py index 3b4b388..d538db9 100644 --- a/tools/vectors/verify_bt_pairing_checks.py +++ b/tools/vectors/verify_bt_pairing_checks.py @@ -29,21 +29,31 @@ def _hmac(key: bytes, msg: bytes) -> bytes: return hmac.new(key, msg, hashlib.sha256).digest() -def _ck_h_k_after_psk(prologue: bytes, psk: bytes) -> tuple[bytes, bytes]: - """InitializeSymmetric → MixHash(prologue) → token "psk" (MixKeyAndHash). Trả (h, k) sau bước này.""" +def _ck_h_after_psk(prologue: bytes, psk: bytes) -> tuple[bytes, bytes]: + """InitializeSymmetric → MixHash(prologue) → token "psk" (MixKeyAndHash). Trả (ck, h) sau bước này.""" h0 = _sha256(PROTOCOL_NAME) # 37 byte > HASHLEN 32 h1 = _sha256(h0 + prologue) temp_key = _hmac(h0, psk) out1 = _hmac(temp_key, b"\x01") out2 = _hmac(temp_key, out1 + b"\x02") - out3 = _hmac(temp_key, out2 + b"\x03") - return _sha256(h1 + out2), out3 + return out1, _sha256(h1 + out2) + + +def _mix_key(ck: bytes, ikm: bytes) -> tuple[bytes, bytes]: + """Token "ee"/"e" (chế độ psk, Noise rev. 34 §9.2): MixKey, 2 ngõ ra qua HMAC — KHÔNG MixHash.""" + temp_key = _hmac(ck, ikm) + out1 = _hmac(temp_key, b"\x01") + out2 = _hmac(temp_key, out1 + b"\x02") + return out1, out2 def _open(psk: bytes, prologue: bytes, e_pub: bytes, ciphertext_tag: bytes): - """Trả (mở bằng cryptography, mở bằng libsodium, h sau token "e") — None khi tag không khớp.""" - h, k = _ck_h_k_after_psk(prologue, psk) + """Trả (mở bằng cryptography, mở bằng libsodium, h sau token "e") — None khi tag không khớp. Chế độ psk: token + "e" làm MixHash(e.pub) RỒI MixKey(e.pub) (#3418) — k dùng để mở tag là k SAU MixKey(e.pub), không phải k của + token "psk".""" + ck, h = _ck_h_after_psk(prologue, psk) h3 = _sha256(h + e_pub) + _, k = _mix_key(ck, e_pub) try: a = ChaCha20Poly1305(k).decrypt(ZERO_NONCE, ciphertext_tag, h3) except InvalidTag: diff --git a/tools/vectors/verify_noise_library_checks.py b/tools/vectors/verify_noise_library_checks.py index f96ed74..8d955f3 100644 --- a/tools/vectors/verify_noise_library_checks.py +++ b/tools/vectors/verify_noise_library_checks.py @@ -1,15 +1,14 @@ """Kiểm chéo bắt tay Noise_NNpsk0_25519_ChaChaPoly_SHA256 trên vector `handshake` của bt-link.json bằng `noiseprotocol` (PyPI, pin ở tools/vectors/requirements.txt) — thư viện Noise ĐỘC LẬP HOÀN TOÀN với noise_nnpsk0.py (không chỉ đổi primitive như verify_bt_link_checks.py, mà chạy lại chính thuật toán Noise bằng -mã người khác viết), theo ghi chú của Trang ở #3328 và yêu cầu của Lead ở #3376/#3380. +mã người khác viết), theo ghi chú của Trang ở #3328 và yêu cầu của Lead ở #3376/#3380. Đã tự chạy thật (Lead cài +`noiseprotocol` qua `setup-cpace-refs.sh`, #3412): bắt đúng lỗi thiếu MixKey(e.pub) của chế độ psk (#3418), sau +khi vá noise_nnpsk0.py thì `message1`, `message2`, `handshake_hash` và cả hai khoá `Split()` (`cipher_state_encrypt.k`/ +`cipher_state_decrypt.k`) đều khớp byte-exact với `noiseprotocol`. THEO YÊU CẦU CỦA LEAD: thiếu `noiseprotocol` phải báo lỗi rõ, không âm thầm bỏ qua — môi trường chưa chạy `pip install -r requirements.txt` (có `noiseprotocol==0.3.1`) sẽ thấy lỗi ImportError ở đây, không phải một dòng "bỏ qua" lặng lẽ. - -Chưa tự chạy được trong sandbox của Tuan (không có mạng để pip install) — ghi lại trong task #189: cần chạy sau -khi `tools/setup-cpace-refs.sh` cài xong `noiseprotocol` để xác nhận API dưới đây (từ hiểu biết về thư viện, -chưa tự kiểm) khớp với phiên bản 0.3.1 thật. """ from verify_common import H @@ -46,3 +45,12 @@ def check_bt_link_with_noiseprotocol(c, doc: dict, all_docs: dict) -> None: initiator.read_message(message2) c.true(f"{n} cả hai phía xong bắt tay", initiator.handshake_finished and responder.handshake_finished) + c.eq(f"{n} handshake_hash (h6) cả hai phía", initiator.noise_protocol.handshake_hash, + responder.noise_protocol.handshake_hash) + c.eq(f"{n} handshake_hash (h6) khớp vector", initiator.noise_protocol.handshake_hash, H(handshake["h_final"])) + c.eq(f"{n} Split() c1 (C mã hoá/S giải mã)", initiator.noise_protocol.cipher_state_encrypt.k, + H(handshake["c1"])) + c.eq(f"{n} Split() c1 khớp phía S", responder.noise_protocol.cipher_state_decrypt.k, H(handshake["c1"])) + c.eq(f"{n} Split() c2 (S mã hoá/C giải mã)", responder.noise_protocol.cipher_state_encrypt.k, + H(handshake["c2"])) + c.eq(f"{n} Split() c2 khớp phía C", initiator.noise_protocol.cipher_state_decrypt.k, H(handshake["c2"])) From 259217d6e47c1cc9c0d44a266f71f6dffbc46390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Xu=C3=A2n=20D=C5=A9ng?= Date: Fri, 30 Oct 2026 07:55:33 +0700 Subject: [PATCH 6/7] feat(strings): add Bluetooth connection status strings Add two new catalog keys for Bluetooth connections: - status.connected_bluetooth: "Connected via Bluetooth" - status.connected_bluetooth_to: "Connected via Bluetooth to {device_name}" Spec 0.11 (route .bluetooth), new for Phase 7 (S7.1) --- strings/ui-strings.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/strings/ui-strings.json b/strings/ui-strings.json index 38fac22..637e055 100644 --- a/strings/ui-strings.json +++ b/strings/ui-strings.json @@ -3341,6 +3341,23 @@ "platforms": ["android", "macos", "ios"], "specs": ["PAIR-02"] }, + { + "key": "status.connected_bluetooth", + "en": "Connected via Bluetooth", + "vi": "Đã kết nối qua Bluetooth", + "comment": "State Connected while the session goes over Bluetooth; new catalog key (S7.1)", + "platforms": ["android", "macos", "ios"], + "specs": ["0.11"] + }, + { + "key": "status.connected_bluetooth_to", + "en": "Connected via Bluetooth to {device_name}", + "vi": "Đã kết nối qua Bluetooth với {device_name}", + "comment": "Status line with the device's name (Mac menu bar menu); also the VoiceOver/TalkBack text of the indicator; new catalog key (S7.1)", + "platforms": ["android", "macos", "ios"], + "args": [{"name": "device_name", "type": "string"}], + "specs": ["0.11"] + }, { "key": "status.connected_internet", "en": "Connected over the internet", From 5cbe4aae016052bf85d5d23e6e29a0ffc3617083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Xu=C3=A2n=20D=C5=A9ng?= Date: Fri, 30 Oct 2026 10:11:24 +0700 Subject: [PATCH 7/7] docs(changelog): correct Phase 7 0.2.0 entry to match shipped code Drop claims with no matching code or test vectors (Wi-Fi Direct SSID/ passphrase format, K_bulk bulk-lane stream keys, AUTH_FAILED/LINK_FAILED as new codes) into a Specs section; credit the strings and schema additions that exist but are not wired into any platform UI yet (permission primer, Bluetooth-off/denied status lines, settings label). --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c27feb..c411063 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,35 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +## [0.2.0] — 2026-10-30 + +### Added + +- **Phase 7 (offline connectivity):** test vectors for the Bluetooth L2CAP link-security handshake (Noise prologue, + challenge, `noise1`, `noise2`, `bt-link.json`), the pre-auth/transport frame codec (channel IDs, record length bounds, + `bt-frame.json`), advertisement service data (rotating hint, `bt-advertisement.json`) and CPace-X25519-SHA512 + password-authenticated key exchange (`cpace.json`, draft vectors plus HandLive-specific cases); Bluetooth pairing + envelopes (`bt-pairing.json`) — all consumed by `core/crypto`/`core/protocol`/`core/transport` on Android and + `HLCrypto`/`HLProtocol`/`HLTransport` on Apple; `tools/setup-cpace-refs.sh` fetches the CPace draft and verifies the + vectors in per-tool venvs. `envelope.schema.json`/`error.schema.json`: new envelope types `direct` and `bulk` (both + already in the Android/Apple `MessageType` enum). + +- UI strings for the device label "Connected via Bluetooth" (`status.connected_bluetooth(_to)`, CONN-05, shown by the + status indicator on Android/Mac/iOS) and the Mac Bluetooth permission prompt text (`infoplist.bluetooth_always`, wired + into `macOS/Info.plist`'s `NSBluetoothAlwaysUsageDescription`). + +### Specs + +- Catalog entries added for 01-setup-settings.md SET-01/SET-03 that no Android/Apple UI calls yet: the Bluetooth + permission-primer screen (`permission.bluetooth_title/body/body_mac`), the Mac "Bluetooth off"/"Bluetooth denied" + status lines and their settings button (`status.bluetooth_off`, `status.bluetooth_denied`, + `common.open_bluetooth_settings`), and the Settings feature-card label (`settings.bluetooth_link`). +- `error.schema.json`'s new `DIRECT_WIFI_UNAVAILABLE` code (0.4.6): schema only, no code returns or handles it yet — + Wi-Fi Direct itself (SSID/passphrase format, per-connection bulk stream keys `K_bulk`) has no code or test vectors on + either platform. +- `bt_seen`/`bt_lost`/`wifi_assoc` bench events (`tools/bench/reconnect_time.py`): the trigger logic is in place, but + only `tools/ble-spike` and `Tools/BLESpike` emit these events today — the shipped app does not yet. + ## [0.1.1] — 2026-10-22 ### Changed