Files
tai/taicpp/problem.md
T

115 lines
7.0 KiB
Markdown

# Open problem: the phone never sends the app-state sync key to a newly linked companion
Date: 2026-10-28. Client: detz 1.45.0 (`src/whatsapp/`, namespace `detz::wa`), a C++ WhatsApp companion
written from scratch after whatsmeow (Go). Device in the last test: `31630045827:54`.
## In one sentence
A newly linked companion device logs in and sends fine, but the phone never sends it the app-state sync key.
Without that key the address book (collection `critical_unblock_low`) cannot be decrypted, so no contact
names arrive.
## What works (confirmed by the user on the phone)
- QR pairing, automatic restart (`stream:error 515`), login, upload of 50 prekeys.
- Sending an image to the user's own number; it shows in "Message yourself".
- Our participant hash equals the server's (`2:TKwG4B1n`), so the device list we send to is correct.
## What does not work
Every login runs the contact sync. The log shows:
```
app state critical_unblock_low: missing app state key for the snapshot
app state critical_unblock_low: version 0, 0 patches, 0 contacts saved, 0 removed, 0 unnamed
app state keys: requesting 1 from the phone
app state: no key share arrived; contacts will sync on a later login
```
I then stayed connected for 100 to 120 seconds with a trace of every incoming node
(`DETZ_WA_TRACE=1`), after clearing our own 24-hour limit on key requests. The only nodes that arrived were
`<success>`, `<ib edge_routing>` and `<ib dirty type=account_sync>`. No `<message>` node arrived at all.
The database holds 0 app-state keys and one Signal session, with the phone's LID address
`173735822786802.0`, created by our own sends.
## What the code does
1. Login succeeds. We request the collection `critical_unblock_low`; the server's reply shows a key is needed.
2. We send a peer message (`category="peer"`, `push_priority="high"`) to the user's own account. It contains a
protocol message of type `APP_STATE_SYNC_KEY_REQUEST` and is encrypted to the phone's LID identity.
3. We wait 20 seconds for the phone's `APP_STATE_SYNC_KEY_SHARE`. Nothing arrives.
Registration now carries `DeviceProps.historySyncConfig` with whatsmeow's values (added after a first failed
attempt; the device was re-linked afterwards).
## Checked against the reference
Pairing reply, media upload, key derivation and the key-request bytes match whatsmeow. The app-state crypto
(HKDF key expansion, LTHash, MACs) passes against an oracle built from whatsmeow's own packages. The data path
is not the problem: the key never arrives, so nothing has been decrypted from real data yet.
## Hypotheses, none confirmed
1. The phone cannot decrypt or accept our key request. The server acknowledges it, but an ack only means the
server took it. Related reports: whatsmeow PR 1268 and Volt-Chat PR 5 describe key requests that never leave
the process without a Signal session to the phone; WhatsApp Web ensures that session before sending.
2. The phone pushes keys only once, right after pairing, and that happened inside the link session, which I
never traced. If messages arrived then and our handler dropped or failed them silently, the keys are lost.
Checkable by logging everything during a fresh link.
3. The phone is not answering for another reason: in the background or battery-restricted, WhatsApp not open.
Reports also exist of the phone answering with an empty key (openclaw/wacli PR 443).
4. A classification issue like the one in Rich627/whatsapp-claude-plugin PR 45: peer messages from the phone
have no `recipient` attribute and were dropped as spoofed. That explains missing keys there, but here no
`<message>` arrives at all, so it is not our case yet.
## Questions for an expert
- On a fresh link, does the primary phone always push the key share and history to the new companion without
being asked? What does it key that on (device props, `platformType`, client version)?
- Is a peer key request encrypted to the LID identity of device 0 enough, or must a Signal session with the
phone already exist from an earlier exchange?
- Does the phone answer key requests from a device that has not yet received the initial history sync?
- Is anything in the 2026 pairing flow, such as the `jurisdiction` and `encryption-metadata` nodes seen in
`pair-success` for a Dutch number, related to when keys are shared?
## Sources
- https://github.com/tulir/whatsmeow/pull/1268
- https://github.com/Volt-Chat/whatsmeow/pull/5
- https://github.com/openclaw/wacli/pull/443
- https://github.com/Rich627/whatsapp-claude-plugin/pull/45
- Research archive: `research/whatsapp-automation/` (whatsmeow-appstate-sync.md, whatsmeow-key-share-and-peer-messages.md,
whatsmeow-device-props-history-sync.md)
## Fallback
Import the contacts from a `.vcf` file exported on the phone, into the existing contact search. Built: `detz --whatsapp-import contacts.vcf` (see `WaVcf`, docs section 14).
## Resolution (2026-10-27, detz 1.46.0)
The request path was verified line by line against whatsmeow main (`send.go`, `message.go`, `appstate.go`,
`notification.go`, `client.go`, `pair.go`, `store/clientpayload.go`, fetched 2026-10-27). Registration, login,
DeviceProps, the peer node, the key-request bytes and the padding all match the reference. Eight local gaps were
fixed anyway (docs section 16 has the table): 24 h rollback on send failure (whatsmeow PR 1268), verified peer
send (Volt-Chat PR 5), pkmsg-only device-identity, LID senders accepted plus an arrival log before the filter,
empty shares distinguished from silence (openclaw/wacli PR 443), notification/receipt handling, `--whatsapp-sync`
(300 s online for a sleeping phone), and the `--whatsapp-import` fallback. `build/whatsapp_test` covers the new
behavior; the live answer still needs the phone.
Answers to the questions above, from the reference source (not from the phone, which is still unseen):
- Fresh link, unsolicited push: yes, the phone pushes the initial history sync and the key shares on its own once
it notices the new companion (that is the initial sync `historySyncConfig` declares support for). Re-requests are
answered too in a healthy setup (whatsmeow `requestMissingAppStateKeys` sends them on every fetch with missing
keys); reports of "just once" come from clients that dropped the first push.
- Session before the peer request: yes, required. whatsmeow `encryptMessageForDevice` returns `ErrNoSession` when
no session exists and the message never leaves the process. We now verify the session exists after the prekey
fetch and say so loudly when it does not.
- Requests before the initial history sync: answered in principle (wacli sees even empty shares), but a phone that
is asleep or battery-restricted answers nothing until it wakes; that is what `--whatsapp-sync` (300 s) is for.
- `jurisdiction` / `encryption-metadata` in `pair-success`: server-side account metadata, ignored by whatsmeow
`handlePairSuccess` like any unknown child. Not related to key sharing.
Next step if keys still do not arrive: run `detz --whatsapp-sync` with WhatsApp open on the phone and no battery
restriction. A 300 s trace with no `<message>` proves the gate is on the phone's side; import the .vcf meanwhile.