Closing the §13 cross-engine gap (paulhitt/flamenet-e2e!2) turned up two problems in the
ratchet I merged this afternoon. One of them would have broken every existing
Android conversation.
The bug: a v2 peer was sent a v3 ratchet step
E2ESession.initiate turned v3 on whenever the peer offered a PQ prekey:
Every v2 device offers one. Offering a prekey is a claim to §12, not §13. So
this build would attach pqdh/pqct to messages for 0.6 — the released
build — whose ratchet has no post-quantum anything.
It fails twice over, which is why it would have read as a network problem:
root key
theirs derives without ss under _v1; ours used _v3
AAD
their canonical header omits both fields entirely
The first message never opens and never would.
Now gated on a proven claim, as iOS already does: chosen != null && bundle.proto >= 3. bundle.proto alone is the relay-s word for it; chosen is non-null only when
the capability signature over that same proto verified (§12.7).
The test asserts on the absence of a header field. Nothing else catches
this — every message still decrypts in a self-test, because both sides of one
engine agree about the mistake.
The ratchet was inert anyway
PROTOCOL_VERSION was 2. iOS advertises 3 and gates on the peer doing the same,
so an iOS↔Android session negotiated v2 and no message ever carried a ratchet
step, whatever either engine implemented.
Now 3 — moved in the same change as the evidence rather than ahead of it. A
version is a claim other devices act on: peers pin the highest proven version and
the pin is monotonic (§13.4), so advertising 3 without honouring it strands every
contact that believed it.
Tests that had to move
a capability signature does not transfer between versions edited a bundle to proto = 3 to prove the signature would not follow — a hardcoded neighbour
that stopped testing anything once the engine published 3. Uses PROTOCOL_VERSION + 1.
The v2-gating test needs a peer that has genuinely proven 2, which this engine
no longer produces. copy(proto = 2) is not enough — the capability
signature commits to the version — so publishedV2 re-signs for it.
New: the published bundle claims 3 and proves it. Bumping the constant
without the signature following produces a bundle every peer silently reads as
v1, with nothing reporting an error.
Also fixed
the sealed blob reveals nothing was the same coin flip as its iOS twin —
sealing ids 41/42 and searching the base64 for "41"/"42", ~14% chance of
a meaningless failure. It fired during this run. The iOS one was fixed this
morning; this was missed because only one engine was looked at.
Closing the §13 cross-engine gap (paulhitt/flamenet-e2e!2) turned up two problems in the
ratchet I merged this afternoon. One of them would have broken every existing
Android conversation.
### The bug: a v2 peer was sent a v3 ratchet step
`E2ESession.initiate` turned v3 on whenever the peer offered a PQ prekey:
```kotlin
DoubleRatchet.initiator(sharedSecret, bundle.signedPreKey, chosen?.publicKey)
```
**Every v2 device offers one.** Offering a prekey is a claim to §12, not §13. So
this build would attach `pqdh`/`pqct` to messages for **0.6 — the released
build** — whose ratchet has no post-quantum anything.
It fails twice over, which is why it would have read as a network problem:
| | |
|---|---|
| root key | theirs derives without `ss` under `_v1`; ours used `_v3` |
| AAD | their canonical header omits both fields entirely |
The first message never opens and never would.
Now gated on a **proven** claim, as iOS already does: `chosen != null && bundle.proto >= 3`.
`bundle.proto` alone is the relay-s word for it; `chosen` is non-null only when
the capability signature over that same proto verified (§12.7).
The test asserts on the **absence** of a header field. Nothing else catches
this — every message still decrypts in a self-test, because both sides of one
engine agree about the mistake.
### The ratchet was inert anyway
`PROTOCOL_VERSION` was 2. iOS advertises 3 and gates on the peer doing the same,
so an iOS↔Android session negotiated v2 and **no message ever carried a ratchet
step**, whatever either engine implemented.
Now 3 — moved in the same change as the evidence rather than ahead of it. A
version is a claim other devices act on: peers pin the highest proven version and
the pin is monotonic (§13.4), so advertising 3 without honouring it strands every
contact that believed it.
### Tests that had to move
- `a capability signature does not transfer between versions` edited a bundle to
`proto = 3` to prove the signature would not follow — a hardcoded neighbour
that stopped testing anything once the engine published 3. Uses
`PROTOCOL_VERSION + 1`.
- The v2-gating test needs a peer that has genuinely proven 2, which this engine
no longer produces. `copy(proto = 2)` is **not** enough — the capability
signature commits to the version — so `publishedV2` re-signs for it.
- New: the published bundle claims 3 **and proves it**. Bumping the constant
without the signature following produces a bundle every peer silently reads as
v1, with nothing reporting an error.
### Also fixed
`the sealed blob reveals nothing` was the same coin flip as its iOS twin —
sealing ids `41`/`42` and searching the base64 for `"41"`/`"42"`, ~14% chance of
a meaningless failure. **It fired during this run.** The iOS one was fixed this
morning; this was missed because only one engine was looked at.
### Verification
```
606 tests, 0 failures (engine 275, client 277, app 54)
:app:assembleDebug BUILD SUCCESSFUL
cross-engine 56 passed, 0 failed
```
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closing the §13 cross-engine gap (paulhitt/flamenet-e2e!2) turned up two problems in the
ratchet I merged this afternoon. One of them would have broken every existing
Android conversation.
The bug: a v2 peer was sent a v3 ratchet step
E2ESession.initiateturned v3 on whenever the peer offered a PQ prekey:Every v2 device offers one. Offering a prekey is a claim to §12, not §13. So
this build would attach
pqdh/pqctto messages for 0.6 — the releasedbuild — whose ratchet has no post-quantum anything.
It fails twice over, which is why it would have read as a network problem:
ssunder_v1; ours used_v3The first message never opens and never would.
Now gated on a proven claim, as iOS already does:
chosen != null && bundle.proto >= 3.bundle.protoalone is the relay-s word for it;chosenis non-null only whenthe capability signature over that same proto verified (§12.7).
The test asserts on the absence of a header field. Nothing else catches
this — every message still decrypts in a self-test, because both sides of one
engine agree about the mistake.
The ratchet was inert anyway
PROTOCOL_VERSIONwas 2. iOS advertises 3 and gates on the peer doing the same,so an iOS↔Android session negotiated v2 and no message ever carried a ratchet
step, whatever either engine implemented.
Now 3 — moved in the same change as the evidence rather than ahead of it. A
version is a claim other devices act on: peers pin the highest proven version and
the pin is monotonic (§13.4), so advertising 3 without honouring it strands every
contact that believed it.
Tests that had to move
a capability signature does not transfer between versionsedited a bundle toproto = 3to prove the signature would not follow — a hardcoded neighbourthat stopped testing anything once the engine published 3. Uses
PROTOCOL_VERSION + 1.no longer produces.
copy(proto = 2)is not enough — the capabilitysignature commits to the version — so
publishedV2re-signs for it.without the signature following produces a bundle every peer silently reads as
v1, with nothing reporting an error.
Also fixed
the sealed blob reveals nothingwas the same coin flip as its iOS twin —sealing ids
41/42and searching the base64 for"41"/"42", ~14% chance ofa meaningless failure. It fired during this run. The iOS one was fixed this
morning; this was missed because only one engine was looked at.
Verification
mentioned in merge request flamenet-e2e!2
mentioned in commit
cc51854fddchanged the description