detz: baseline 1.46.1 pre-transpilation

This commit is contained in:
2026-10-28 01:58:28 +01:00
parent 69542ab933
commit 12ee647d78
32 changed files with 1824 additions and 68 deletions
+69 -6
View File
@@ -581,18 +581,23 @@ it `wa_contact` stays empty and names cannot be resolved.
| Piece | Files | What it does |
|---|---|---|
| Key receipt | `wa_receiver.*`, `wa_proto.*` (`parse_protocol_message`), `WaClient::handle_message` | Decrypts `<enc>` from our own account (pkmsg via X3DH responder, msg via session), unpads, stores each APP_STATE_SYNC_KEY_SHARE key in `wa_appstate_key`. Only the phone's own messages are decrypted; other senders are acked and ignored as before. |
| Key request | `WaClient::request_app_state_keys`, `send_peer_message`, `encode_app_state_key_request` | Peer message to the primary phone (category `peer`), at most `kWaAppStateKeyIdsMax` ids, one request per key id per `kWaAppStateKeyRequestInterval` (24 h), stored in `wa_appstate_keyreq`. |
| Key receipt | `wa_receiver.*`, `wa_proto.*` (`parse_protocol_message`), `WaClient::handle_message` | Decrypts `<enc>` from our own account (pkmsg via X3DH responder, msg via session), unpads, stores each APP_STATE_SYNC_KEY_SHARE key in `wa_appstate_key`. Own means our phone number or our LID (whatsmeow `IsFromMe`); other senders are acked and ignored as before. Every arrival is logged before the filter, so a trace shows dropped mail too. An empty share (keys without key data, openclaw/wacli PR 443) is reported and ends the wait without storing; a consumed peer message gets a `peer_msg` protocol receipt. |
| Key request | `WaClient::request_app_state_keys`, `send_peer_message`, `encode_app_state_key_request` | Peer message to the primary phone (category `peer`, meta + enc, device-identity only on a prekey message), at most `kWaAppStateKeyIdsMax` ids, one request per key id per `kWaAppStateKeyRequestInterval` (24 h), stored in `wa_appstate_keyreq`. The send is verified (Signal session must exist, the server ack must carry no error); when it fails the 24 h marks are rolled back so the next sync asks again (whatsmeow PR 1268). |
| Notifications, receipts | `WaClient::handle_notification`, `handle_receipt` | `server_sync` re-syncs the contact collection (other collections are named and skipped); `encrypt` with fewer than `kWaMinPrekeys` left refills prekeys; a retry receipt (the phone could not decrypt our message) is logged loudly. Notification syncs never nest inside a sync (guard + one more pass). |
| Key expansion, MACs, values | `wa_appstate.*` (`WaAppStateKeys`, `WaAppStateCrypto`) | HKDF-SHA256 `WhatsApp Mutation Keys` to 160 bytes; value MAC HMAC-SHA512 truncated to 32; index, snapshot and patch MACs HMAC-SHA256; AES-256-CBC values. `WaCrypto::hmac_sha512` added. |
| LTHash | `wa_appstate.*` (`WaLtHash`) | 128-byte state, HKDF-SHA256 info `WhatsApp Patch Integrity` (whatsmeow, not BLAKE2b; see research conclusion `appstate-lthash-hkdf-not-blake2b`). |
| Patch fetch and apply | `WaAppStateSync::sync` | iq `w:sync:app:state`, `return_snapshot` at version 0, `version=N` otherwise; external blobs via `WaMedia::download` (`md-app-state`); snapshot MAC, patch MAC, index and value MACs checked before anything is stored. |
| Contacts | `WaAppStateSync::apply_contact` | SET stores jid = index[1], name = fullName else firstName; empty name is counted and skipped; REMOVE deletes the row. |
| Push-name learning | `WaClient::handle_message`, `push_name_candidate`, `WaStore::put_contact_if_absent` | Every incoming message carries the sender's self-reported name in `notify` (whatsmeow `message.go`); group senders come from `participant`, the PN alternate is preferred when present. Stored only when the jid has no name yet, so a saved, imported or aliased name always wins; truncated to `kWaPushNameMax` (128). No server query returns names for regular users (`GetUserInfo` usync has status, picture, devices, LID and business verified names only), so this plus VCF/alias/app-state is the complete name-resolution stack. |
| Login hook | `WaClient::login` calls `sync_app_state` after prekeys | Never fails the login (errors are logged). If a key is missing, requests it, waits up to `kWaAppStateKeyWait` s for a key share, then syncs once more. |
| Long sync | `detz --whatsapp-sync` (`WaWizard::sync`) | Logs in, re-syncs with a `kWaSyncOnlineWait` (300 s) key wait for a sleeping phone, reports the contact count. Exit 0 with contacts, 1 without, 2 when not linked or login failed. |
| VCF fallback | `detz --whatsapp-import FILE` (`WaVcf`) | Imports a .vcf exported on the phone (FN else N, TEL lines, international digits) into the same contact search. Loud skips, capped at `kWaContactsMax`, file capped at `kWaVcfFileMax`. |
Caps (`wa_consts.h`): `kWaAppStatePatchMax` 50 patches per sync,
`kWaAppStateMutationMax` 10000 mutations, `kWaAppStateTimeout` 30 s per iq,
`kWaAppStateBlobMax` 16 MiB per external blob, `kWaAppStateKeyIdsMax` 16 key
ids per request, `kWaAppStateKeyWait` 20 s. A cap stops the sync loudly
ids per request, `kWaAppStateKeyWait` 20 s, `kWaSyncOnlineWait` 300 s,
`kWaVcfFileMax` 8 MiB. A cap stops the sync loudly
(`capped` in the report, a log line) and the rest waits for the next login.
Failure rule: a patch or snapshot that fails a MAC or hash check is not stored,
@@ -616,7 +621,17 @@ Verification (offline, `build/whatsapp_test`, section `appstate`):
- a missing key stores nothing and reports the key id;
- the key share and key request bytes match whatsmeow's Message encoding;
- a pkmsg built with `WaSession` decrypts through `WaReceiver` to the key share
Message, and a corrupted pkmsg is refused.
Message, and a corrupted pkmsg is refused;
- the 24 h request mark can be cleared for an immediate retry;
- own senders accept PN and LID, the peer node carries device-identity only on
a prekey message, server_sync collections and retry receipts parse;
- an empty key share parses with empty key data;
- a 5-card .vcf imports 4 numbers (FN, N fallback, unfolding, 00 form) and the
typo-tolerant search finds them;
- push-name candidates parse for 1:1 (PN, LID, PN alternate), groups
(participant, PN alternate), and reject own senders, `-`, missing notify,
non-user senders and over-long names; fill-if-absent keeps the first name and
never overwrites a saved one.
The oracle is `tests/whatsapp_data/appstate_vectors.json`, generated by
`tests/whatsapp_data/appstate_oracle.go` (a Go program using whatsmeow's own
@@ -624,8 +639,10 @@ The oracle is `tests/whatsapp_data/appstate_vectors.json`, generated by
Not verified: the live server's answers to the iq, key share timing, the media
host for external blobs, and whether a REMOVE of an unknown index (whatsmeow
only warns) causes trouble. Known gap: a SET with an empty name keeps any older
name the row had; the spec says skip, not delete.
only warns) causes trouble. Known gaps: a SET with an empty name keeps any
older name the row had (the spec says skip, not delete); a retry receipt is
logged, not answered with a resend (whatsmeow `retry.go`); history sync
notifications are not downloaded.
@@ -646,3 +663,49 @@ Everything here was measured on 2026-10-27/28 against the live server and the us
Gaps: the contact sync and the address-book key share are not verified against a phone yet; a newly linked device
must be created with this build for the phone to send history. Set `DETZ_WA_TRACE=1` to log every node's tag and
attributes when debugging a protocol exchange.
## 16. Incident WA-2026-10-28: the phone never sends the app-state sync key
Full record: `problem.md`. A newly linked companion (`31630045827:54`) logged
in, uploaded prekeys and sent an image to its own number (visible in "Message
yourself", participant hash matching the server's), but the phone never sent
the app-state sync key: 100 to 120 s of `DETZ_WA_TRACE=1` showed only
`<success>`, `<ib edge_routing>` and `<ib dirty>`, no `<message>` at all.
The request path was checked line by line against whatsmeow `send.go`,
`message.go`, `appstate.go`, `notification.go`, `client.go`, `pair.go` and
`store/clientpayload.go` (main, fetched 2026-10-27). Registration, login,
DeviceProps (including `historySyncConfig`), the peer node shape, the key
request bytes and the padding all match. whatsmeow answers the open questions
this way:
- A peer message needs a Signal session with the phone first:
`encryptMessageForDevice` with no bundle and no session returns
`ErrNoSession` and the request never leaves the process (hence whatsmeow PR
1268, which rolls back the 24 h mark on send failure, and Volt-Chat PR 5,
which adds an explicit session setup).
- Own senders are the PN user or the LID user (`parseMessageSource`); no
`recipient` attribute is needed. The `<ib dirty type=account_sync>` node
needs no answer (whatsmeow `handleIB` leaves it alone).
- The `jurisdiction` and `encryption-metadata` nodes in `pair-success` are
server-side account metadata; whatsmeow `handlePairSuccess` ignores every
child it does not know, as we do. Not related to key sharing.
Fixes in this build (all local; the phone side is still unseen):
| Gap | Fix | Evidence |
|---|---|---|
| a failed key request still stamped the 24 h mark, blocking the retry | `clear_app_key_request_time`, rollback on send failure (whatsmeow PR 1268) | test `key request rollback` |
| a peer send was trusted without checking the session or the ack | session verified after the prekey fetch, ack error attribute checked and logged (Volt-Chat PR 5) | code, log lines |
| device-identity rode on every peer message, whatsmeow only on pkmsg | `build_peer_message_node` matches whatsmeow | test `peer message helpers` |
| key shares from our LID were silently dropped (PN-only filter) | `is_own_sender` accepts PN or LID, every arrival is logged before the filter | test `peer message helpers` |
| an empty key share looked like no answer | stored/empty counted separately, the wait ends, `peer_msg` receipt sent (openclaw/wacli PR 443) | test `empty key share` |
| notifications and receipts were ignored | `server_sync` re-syncs, `encrypt` refills prekeys, retry receipts logged loudly | test `peer message helpers` |
| 20 s online is short for a sleeping phone | `detz --whatsapp-sync` stays online 300 s (`kWaSyncOnlineWait`) | wizard source |
| no fallback when the phone never answers | `detz --whatsapp-import FILE` imports a .vcf into the contact search | test `vcf import` |
What to run now: link fresh if needed, then `detz --whatsapp-sync` with
WhatsApp open on the phone (no battery restriction). If no `<message>` arrives
within the 300 s, the phone is gating on its side; import the .vcf and retry
the sync another day. Research: `research/whatsapp-automation/`
(conclusion `key-share-never-arrives.md`).