Files
bitchat-android/docs/PRIVATE_MEDIA_V1.md

8.5 KiB

Private media v1 interoperability and migration

Private media reuses the canonical BitchatFilePacket TLV. A capable sender wraps the complete encoded file TLV as Noise payload type 0x20, encrypts it for the recipient, and only then fragments the final outer packet.

Encrypted wire contract

  • Outer packet type: MessageType.NOISE_ENCRYPTED (0x11).
  • Decrypted Noise payload type: NoisePayloadType.FILE_TRANSFER (0x20).
  • Noise payload data: one complete encoded BitchatFilePacket.
  • Outer recipient: the target peer ID; never broadcast for private media.

Prerelease iOS builds of #1434 briefly emitted the inner file type as 0x09. Android accepts that value on decode and immediately canonicalizes it to NoisePayloadType.FILE_TRANSFER; every Android encode remains 0x20. Do not allocate or emit a second Noise payload type for this format.

The decode-only 0x09 alias may be removed only after every TestFlight/internal build that emitted it has expired and the project's minimum-supported-client policy excludes those builds. Track that release criterion explicitly; do not remove the alias on an arbitrary calendar date.

Discovery hint

Identity announcement TLV 0x05 is a minimal little-endian bitfield. Bit 8 means the peer implements private-media v1, so its exact encoding is:

05 02 00 01
|  |  |----- capability bytes: 0x0100 little-endian
|  |-------- value length
|----------- capabilities TLV

Current Android builds include this TLV in broadcast and peer-directed announcements over both BLE and Wi-Fi Aware. Older clients safely skip the unknown TLV. Its absence, or a present TLV with bit 8 clear, does not invalidate the announcement.

Decoders retain unknown low-64-bit capability bits and unknown announcement TLVs so a decode/re-encode cycle does not erase newer extensions.

The announcement bit is discovery metadata only. A self-signed announcement does not prove possession of its public Noise key, so it never authorizes encrypted media, creates a capability pin, or satisfies a pending send.

Authenticated peer state (0x21)

Every newly authenticated Noise generation, including a rekey with the same static key, exchanges a fresh peer-state proof. Its decrypted Noise payload type is 0x21, followed by this canonical byte sequence:

01                         version
01 <len 1...8> <value>     capabilities, minimal little-endian
02 20 <32 bytes>           Ed25519 signing public key

Both known TLVs are required exactly once. Decoders reject an unknown version, truncated TLV, duplicate known TLV, a capability length outside 1...8, a non-minimal capability value, or an Ed25519 key whose length is not 32. Unknown TLVs are skipped for forward compatibility, and the two known TLVs may arrive in either order.

Each endpoint sends 0x21 when the generation authenticates and echoes its local state at most once after accepting the peer's first valid proof for that generation. Repeated identical proofs are idempotent; a different second proof cannot replace the first within that generation. A five-second generation watchdog distinguishes an older client that ignores 0x21 from a new client that supplied a proof. Persisted state from a previous connection never satisfies the fresh-generation watchdog.

Locally, the Noise handshake hash is the generation token. Decryption returns that token with the plaintext, coordinator mutations hold a lease on that exact session, and private-media encryption accepts the policy decision only while the same token remains active. A same-static rekey therefore cannot reuse an older proof or race policy into encrypting on an unproved generation.

The proof is bound by the Noise channel to the exact authenticated 32-byte remote static key and canonical peer ID. Store its capabilities and 32-byte Ed25519 key under the SHA-256 fingerprint of that static key in encrypted identity state. A proof with bit 8 creates an HSTS-style private-media pin; a later no-bit proof does not erase that history. Panic wipe clears both records and prevents an in-flight pre-wipe controller from restoring them.

The persisted Ed25519 key is consulted before accepting later announcements. This lets a valid proof recover from a copied-static, wrong-Ed preannouncement, while preventing that preannouncement from winning again after restart. A fresh proof in a later Noise generation may intentionally rotate the Ed25519 key; an announcement by itself cannot.

Mixed-client send policy

  • No live authenticated Noise remote-static key: retain the first send intent, emit no media or local echo, and initiate one Noise handshake.
  • Live generation waiting for 0x21: retain that same intent while the five-second peer-state watchdog runs.
  • Fresh proof with bit 8: automatically retry the retained intent and send encrypted 0x11 / 0x20 after final-packet admission.
  • Fresh proof without bit 8, or a five-second no-proof timeout for an unpinned old client: retry the retained intent by showing the existing explicit one-shot legacy consent prompt.
  • A previously pinned identity that proves no bit, or fails to prove state in the current generation, is visibly blocked. It cannot silently fall back.

The exact automatic intent is reserved before its first policy evaluation, so a proof or timeout callback racing that evaluation cannot be lost. It is bounded and singular, and expires after 15 seconds with a visible system message. Retries are serialized and always re-run current policy and exact packet admission. No waiting, rejected, expired, or cancelled attempt creates a local file echo or transmits raw media; terminal rejection is shown as a system message rather than failing silently.

The legacy consent path sends a recipient-directed raw MessageType.FILE_TRANSFER (0x22) packet. Its contents are visible to relays, so the UI must say that it is not end-to-end encrypted. The final routed packet must carry a valid Ed25519 signature over the canonical packet bytes; signing failure aborts the send. Receivers reject unsigned or invalid signed directed raw files.

Consent is consumed at most once. On approval the sender re-runs the policy and packet admission checks. If a bit-8 proof arrived while the dialog was open, the send upgrades to encrypted mode. Cancellation, duplicate approval, panic wipe, and changed security state cannot cause a later send.

Final-packet admission

Before creating a local echo or progress mapping, the sender builds the exact encrypted-or-legacy packet, attaches its final source route, signs it, and creates the exact transport fragment plan. Commit sends that prepared plan without rebuilding it.

One packet may use at most 256 fragments. This limit is checked after route, signature, encryption, and envelope overhead are known; therefore there is no single safe file-byte estimate for every route. Fragment totals and indices must also fit their unsigned 16-bit wire fields without truncation. A rejected plan creates no local echo and sends no fragments.

The 256-fragment limit is a transport/reassembly safety bound, not a capability negotiated through bit 8. A future larger transfer protocol needs a separate capability and bounded streaming design.

Compatibility summary

  • New Android to new iOS/Android: generation-scoped authenticated 0x21 exchange, then encrypted Noise 0x20.
  • Prerelease iOS to new Android: encrypted Noise 0x09 is decoded, canonicalized to 0x20, and delivered during the migration window.
  • New Android to an older client: the unknown 0x21 is ignored; after the five-second watchdog, an unpinned identity may use one explicitly consented, relay-visible signed raw 0x22 transfer.
  • Old clients receiving a new announcement: ignore TLV 0x05 and continue operating normally. Old clients receiving 0x21 drop the unknown inner type without affecting their existing Noise session or private messages.
  • A previously capable authenticated identity cannot force a silent downgrade by omitting 0x21, clearing the bit, or changing only its announcement.

Do not remove the 0x21 watchdog/legacy-consent migration path until the minimum-supported Android and iOS versions both emit an authenticated bit-8 0x21 proof on every Noise generation, and the released legacy population has aged out under the project's explicit support policy. Merely observing an announcement bit or waiting for an arbitrary date is not sufficient. The HSTS pin remains necessary after that point; removing old-client consent must not re-enable a raw automatic fallback.

Public media remains signed broadcast MessageType.FILE_TRANSFER (0x22) and is outside this private-media capability.