feat(handshake): in-band key exchange; SBQ2 live in the connection path; release v5.9.0

The descriptor no longer carries key material. It carries what brings up DTLS
plus a 16-byte commitment; the ECDH and ECDSA public keys travel as the first
frame on the DataChannel and are checked against that commitment BEFORE they are
parsed or imported. Measured on the live site, the invitation went from 2274
characters across 4 animated QR frames to 151 characters in one frame.

What the split buys is that nothing is trusted on the strength of having
completed a handshake. The fingerprint arrives over the channel the user
authenticated by looking at it, so DTLS completes only with whoever showed the
code; the commitment makes substituted key material fail closed automatically
instead of relying on the human comparison; and the SAS is computed over a
transcript covering both descriptors verbatim and both key blobs, so anything an
attacker can change anywhere in the handshake changes the digits.

Consequences for the rest of the crypto:

- the HKDF salt is derived from the transcript rather than transmitted, binding
  every session key to both DTLS fingerprints and every candidate;
- authProof is replaced by one ECDSA signature over the transcript, proving the
  same possession without echoing a nonce back across seven fields;
- Double Ratchet support is implied by the format rather than advertised in it.
  SBQ2 postdates the ratchet, so the "peer is old, fall back to static keys"
  branch is unreachable here -- it used to fire silently, since SBQ2 has no `dr`
  field for _initializeRatchet to find.

Fail closed throughout. _handshakeMode is latched per connection, so a session
cannot be pushed back onto SB1 halfway through; commitment mismatch, wrong role,
duplicate blob, bad proof and timeout all tear the connection down with a
specific message rather than degrade. SBQ2 handshake frames are refused outright
on a non-SBQ2 session, where there would be no commitment to check them against.

Reception of both formats is unconditional and split by first byte (0x02 vs
ASCII 'S') and text prefix. SBQ2_SEND_ENABLED is the single value that governs
what we emit; flipping it to false and redeploying reverts new invitations to
SB1, and the animated multi-frame QR path stays for them.

Verified end to end in real browsers -- 12/12 across {Chrome, Firefox} squared
and three network profiles -- through to a confirmed SAS, an active ratchet, a
64-byte transcript-derived salt and a decrypted message on the far side.
This commit is contained in:
lockbitchat
2026-08-06 18:20:48 -04:00
parent fb959d7ca7
commit 7b703e4e65
15 changed files with 2845 additions and 65 deletions
+50
View File
@@ -1,5 +1,55 @@
# Changelog
## v5.9.0 — The invitation is now one small QR code
The connection descriptor moves to SBQ2 and the key material moves onto the
DataChannel. Measured on the live site: the invitation payload went from **2274
characters across 4 animated QR frames to 151 characters in a single frame**.
### Changed
- **Invitations use the SBQ2 format.** The out-of-band code now carries only what
brings up DTLS — ICE credentials, the certificate fingerprint, candidates, an
expiry — plus a 16-byte commitment to the key material.
- **Key material is exchanged in band.** The ECDH and ECDSA public keys travel as
the first frame on the DataChannel, and are checked against the commitment from
the invitation *before* they are parsed or imported. A mismatch closes the
connection; it is not a warning.
- **The SAS is computed over a transcript** covering both descriptors verbatim and
both key blobs, with length prefixes. Anything an attacker can alter anywhere in
the handshake, in either direction, changes the digits the two people compare.
- **The HKDF salt is derived from that transcript instead of transmitted**, so
every session key is bound to both DTLS fingerprints and every candidate, and
neither side can steer it.
- **`authProof` is replaced by one signature over the transcript.** The old
challenge/response echoed a nonce back across seven fields; the signature proves
the same possession and binds the whole handshake at once.
- The Double Ratchet starts from the transcript-derived material. SBQ2 postdates
the ratchet entirely, so support is implied by the format rather than advertised
in it — which also removes the silent "peer is old, use static keys" fallback
from this path.
### Rollback
`EnhancedSecureWebRTCManager.SBQ2_SEND_ENABLED = false` and redeploy puts every
new invitation back on SB1. Reception of both formats is unconditional and is not
governed by the flag, so a client built with it off still reads SBQ2 invitations.
### Compatibility
SB1 invitations are still read, and the animated multi-frame QR path stays for
them. A client older than 5.9.0 cannot read an SBQ2 invitation: from 5.9.0 on, an
unrecognised `SB<n>:` family reports "This invitation was created by a newer
version of SecureBit. Please update the app to connect." Versions 5.8.1 and
earlier predate that check and show a JSON parse error instead — the two ends must
both be on 5.9.0+.
## v5.8.1 — SBQ2 rebuilds SDP that Firefox accepts
Still nothing in the application calls the SBQ2 descriptor; this fixes defects in