The problem
End-to-end encryption is close to solved. Signal's ratchet, MLS, and the libraries around them are mature, audited and free. If your threat model ends at "nobody should read my messages", that problem has been handled by people better at it than me.
What has not been handled, in anything an ordinary person can install, is the layer underneath. A server that relays your encrypted messages still learns the shape of your life: who you contact, when, how often, and how much you send. A network observer between you and that server learns a slightly blurrier version of the same thing. Neither of them needs to break any cryptography — the information is in the routing and the timing, not the payload.
Metadata is also the part that scales. Reading one person's messages takes a warrant and an analyst. Reading everyone's contact graph takes a database query.
What callwhite is
A messenger with no server in the message path, for one-to-one and group conversations, including media.
Messages are written to and read from addresses derived from key material shared only by the participants — dead drops. A relay stores fixed-size opaque blobs at addresses it cannot interpret and hands them back to whoever asks for the right address. It does not know who wrote a blob, who will read it, or whether the two are the same person. Writes are authorised by key blinding, so the relay can reject unauthorised writes without learning who is authorised.
On top of that: constant-rate cover traffic so that idle and active look the same on the wire; fixed-size blobs so that length reveals nothing; decoy responses on a miss so that occupancy reveals nothing; memory-hard proof of work on each write so that flooding costs attacker CPU rather than defender bandwidth. For groups, per-epoch rekeying, with member removal that takes effect immediately and a roster the relay never sees.
How it is built
- Core — Rust, seven crates: cryptography, protocol, transport, relay, Tor integration, FFI, node.
- Android — Kotlin with Jetpack Compose, bridged to the Rust core through UniFFI.
- Desktop — Tauri, with the same core compiled natively.
- Relay — a small static binary that speaks the wire protocol and stores nothing it can read. Cross-compiled to a fully static Linux target.
- Build — reproducible, with an SHA-256 manifest of the source tree, a CycloneDX SBOM covering 617 components, and dependency and licence gates in CI.
How I work
The interesting failures on this project were not crashes. They were things that looked correct and were not:
A test that passed for the wrong reason
A constant describing a storage layout had been copied into three test files. When the format gained a field, all three went wrong at once — in the same direction. The tests still passed. They were exercising a payload the code would never see, and the upgrade path they were supposed to protect had never actually been tested. Duplication had made a whole class of bug invisible.
A security gate that reported clean because it was broken
A build step scanned binaries for strings that must never ship. It reported clean for a long time. It turned out the tool underneath was aborting on that platform and printing nothing, and an empty result was being read as "found nothing" instead of "found nothing out". The gate now carries a canary: a string known to be present. If the scan cannot find the canary, the scan is broken, and the build stops rather than passing.
A refresh mechanism that produced nonce reuse
A rule intended to make group state more available had every participant write to the current and next window. Because the sealing key and nonce were both derived deterministically from the same inputs, the second write reused a keystream. The reason it never became an exploitable bug in practice is that an unrelated defence in the relay rejected the second write — that is, the thing that saved me was luck wearing the costume of design. The address derivation now includes the epoch, which makes the collision structurally impossible instead of accidentally prevented.
A finding dismissed for having no trigger
During a red-team round, two findings about a data path were rebutted on the grounds that no user interface could reach the code. That rebuttal was accepted at the time. It was the wrong conclusion: an entry point that does not exist is not a defence, it is a separate defect — functionality shipped that nobody could use. Both the original findings and the missing entry points are fixed now, and the lesson is written into the review process.
None of this is unusual. What is worth saying out loud is that the record of it exists in the repository, in the commit messages and in the review documents, including the parts where a previous claim of mine turned out to be wrong. Documentation that only records successes is a marketing artefact, and it is not much use to whoever maintains the thing next.
Contact
I am open to conversations about remote contract or employment work in Rust, applied cryptography, and privacy-preserving systems engineering. The callwhite source is also available for exclusive transfer; enquiries about either are welcome.
Reach me through this domain.
Back