What the system protects against, what it does not, and how to add real protection. The short version: every check in the app detects accidental damage (noise, blur, echo). None of them resist a deliberate attacker. Treat every transfer as public.
Back to the documentation index.
| Property | Provided? | How / why not |
|---|---|---|
| Integrity against noise | Yes | CRC-32 per Light frame, CRC-16 + Reed-Solomon per Sound frame, CRC-32 per packet, QR’s own Reed-Solomon |
| Integrity against an attacker | No | CRCs are public, unkeyed functions; anyone can compute a valid one |
| Confidentiality | No | Payloads are sent in the clear |
| Authenticity (who sent it) | No | No keys, signatures or identities |
| Replay protection | No | A recorded Sound or filmed Light transfer can be played again |
| Availability | Partial | Jamming is easy (noise, a bright light, another QR stream), but it’s local and obvious |
| No remote attack surface | Yes | No network permission in release builds, no server, no open ports |
| Actor | Capability | Realistic? |
|---|---|---|
| Bystander | Sees the screen or hears the tones | Very: that’s how the channels work |
| Eavesdropper with equipment | Films the screen with a zoom lens; records audio from across the room | Yes |
| Active attacker | Shows their own QR stream or plays their own tones near the receiver | Yes, with the app or a modified build |
| Remote attacker | Over the Internet | No: there is no network path |
| Malicious file | Crafted image or video bytes delivered through a valid transfer | Possible; handled by platform decoders (§5) |
| Threat | Channel | Current state | Mitigation |
|---|---|---|---|
| Eavesdropping | Light, Sound | Anyone in range can decode | Shield the screen; lower the volume; move closer; encrypt |
| Unnoticed transfer | Sound (Silent) | Silent tones are inaudible to most adults, so people nearby may not notice a transfer, but any phone running the app within about half a metre can still decode it | Treat Silent as quiet, not private; it offers no confidentiality |
| Eavesdropping | Vibration | Requires touching the phones | Inherently private, but very slow |
| Spoofed message | Light, Sound | A fake stream with valid CRCs is accepted | Authenticated encryption with a shared passphrase |
| Session hijack / pollution | Light | Frames with the victim’s session ID but wrong data would corrupt the decode (caught later only if the envelope magic breaks) | Keyed MAC per frame, or authenticated encryption of the envelope |
| Replay | Light, Sound | A recording decodes again | Timestamps or nonces inside an authenticated envelope |
| Jamming | Sound | Loud noise in the 1.2–7.2 kHz band stops decoding; ordinary noise barely reaches 18–20 kHz, but a deliberate high-frequency tone would jam Silent | Rugged profile, or Silent in a noisy room; move away; switch to Light |
| Jamming | Light | Another screen in view, glare | Per-session decoders (up to 3) keep going; aim carefully |
| Denial of service by memory | Light | Frames can announce large K or file lengths | The receiver keeps at most 3 sessions; K and block sizes come from a CRC-checked header. A hostile frame could still claim a large file, so a hard cap on fileLen would be a sensible addition |
| Malicious media | All | Received bytes go to Flutter’s image decoder and the platform video player | Platform decoders are sandboxed and regularly patched; keep the OS updated |
A CRC is a fixed, public polynomial function. Given any payload, anyone can compute the CRC that makes it “valid”. CRCs catch random bit flips with overwhelming probability:
| Check | Undetected random error probability |
|---|---|
| CRC-16 (Sound frame, after RS repair) | about 1 in 65 536 |
| CRC-32 (Light frame, packet) | about 1 in 4.3 billion |
So they’re excellent against noise and useless against intent. Security needs a secret key, via a MAC or authenticated encryption.
Likewise, the session ID (CRC32(content) XOR blockLen·0x9E3779B1) is an identifier, not a secret. It reveals nothing useful, but anyone who sees one frame knows it.
The receiver treats every frame as untrusted input:
APCF, version 3, and pass CRC-32 before any field is used. Sound frames must pass Reed-Solomon and CRC-16.ChatPayloadCodec.decodeIncoming) checks every length field against the remaining bytes before slicing, so a truncated or lying envelope returns null instead of reading out of bounds.A design that fits the current architecture without changing any modem:
key = Argon2id(passphrase, salt = random 16 B, memory ≥ 64 MiB)
nonce = random 12 B (AES-GCM) or 24 B (XChaCha20-Poly1305)
sealed = AEAD_Encrypt(key, nonce, plaintext = APCM envelope, aad = "APCE1")
envelope' = "APCE" | version 1 | salt | nonce | sealed (ciphertext + 16-byte tag)
envelope' exactly like any other bytes.APCE magic, asks for the passphrase, derives the key and decrypts. A wrong passphrase or any tampering fails the tag check.cryptography (AES-GCM, ChaCha20-Poly1305, Argon2id).This is on the Roadmap.
For ordinary bugs, open an issue on the project’s GitHub repository with the steps to reproduce.
For anything that could hurt users (for example a crash triggered by a crafted frame), don’t open a public issue. Report it privately through the repository’s Security tab, as described in the security policy. That policy also lists what is in scope, the by-design limitations above that don’t need reporting, and the response times to expect.