No server-side rate limiting or size enforcement
The relay server imposed no restrictions on connection rate, message
frequency, or payload size. The client-side pre-handshake buffer also
had no size cap. This created a straightforward denial-of-service path
against the relay independent of the cryptographic protocol.
Resolved in v0.7. Rate limiting, message frequency
caps, payload size limits, and a pre-handshake buffer cap added to
the relay.
No Content Security Policy
The page set no Content-Security-Policy header. In the absence of a
CSP, a successful XSS injection would have had unrestricted access to
the DOM, WebSocket connection, session key variables, and all decrypted
payloads.
Resolved in v0.7. CSP added, restricting script
sources and resource loading.
Session code could reach the app host via the URL
The app accepted a session code supplied in the page URL (a ?code=
parameter) and pre-filled it. A code in the URL travels with the page request to
the app host and can be recorded in its access logs, browser history, or the
Referer header — a path by which the code could reach the server, contradicting
the property that the raw code never touches the host.
Resolved in v0.8.0. The URL code parameter was removed entirely;
the code is entered manually only, so it never rides the request to the host.
Ephemeral private key was exportable
The per-session X25519 private key was generated as extractable. Nothing in the
app exported it, but extractability meant an injected script could in principle
read the live private key from memory, and forward secrecy relied on the key
reference being cleared rather than on the browser enforcing it.
Resolved in v0.8.0. The key is now generated non-extractable, so
the browser itself forbids export and forward secrecy is enforced by the platform.
Channel identifier could be inverted offline (S12)
Through v0.8.0 the channel identifier the relay sees was a plain SHA-256 of the
code, truncated. A relay operator could guess the 10-character code offline at hash
speed — about a day on one GPU for a generated code. Knowing the code does not by
itself decrypt traffic (the session key also needs the ephemeral key exchange), but
it defeats the property that the relay learns nothing about the code. Found during
the protocol v2 design.
Resolved in v0.9.0. The code is stretched once with
PBKDF2-HMAC-SHA512 (300,000 iterations) and the channel identifier, handshake HMAC
key, and session-key salt are all derived from that stretched key. Each guess
against the channel identifier now costs a full stretch.
Envelope format field was not authenticated (S3)
The GCM authenticated data bound only the counter. A relay could flip the
format field and the client would still decrypt and run the
relay-chosen branch. Demonstrated in a harness; low severity, contained.
Resolved in v0.9.0. The authenticated data is now the full header:
protocol tag, message type, channel, format, and counter. Altering any of them fails
decryption, verified by tools/v2-crypto-test.mjs.
Relayed decompression was not output-bounded (S4)
A relayed BBQr compressed payload was inflated with no output ceiling; a small
crafted frame could expand roughly a thousandfold and crash a counterpart's tab.
Member-gated and download-triggered, so low severity.
Resolved in v0.9.0. Inflation is streamed and aborts once output
exceeds 8 MB, far above any real PSBT.
One vendored library lacked verifiable provenance (S5)
Two of the three inlined libraries hashed to their published builds; the QR encoder
did not, so the "what was audited is what runs" guarantee could not be proven for it.
No evidence of tampering.
Resolved in v0.9.0. All three (jsQR 1.4.0, qrcode 1.5.1, pako 2.1.0)
are now byte-identical to their published releases. Sources and SHA-256 hashes are in
the repository's PROVENANCE.md and on the
verify page; node tools/verify-vendored.mjs
checks them and runs as a release preflight.