Systems engineering · Applied cryptography

Yilmaz MertTRgenci

I build systems that try not to reveal who is talking to whom. Encryption is the easy half.

Encrypting a message is close to solved. What survives encryption is everything else: who contacted whom, at what time, how often, and how much. A network observer answers all of that without breaking a single ciphertext — the information lives 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.

So I built callwhite: a messenger with no server in the message path.

The mechanism

CONVENTIONAL Alice Server holds the graph Bob ciphertext ciphertext learns: Alice ↔ Bob, when, how often CALLWHITE Alice relay Bob PUT addr GET addr sees: 4096-byte blobs, nothing else addr = f(shared key) · never transmitted
The pairing has to live somewhere. In a conventional messenger it lives on the server, which is why the server can be subpoenaed, breached or sold. Here it lives only in key material the two participants already share: both sides compute the same address, and the relay stores a fixed-size blob at an address it cannot derive, link to a sender, or link to a reader.

What an observer sees

CONVENTIONAL shape reveals: a conversation is happening, now CALLWHITE reveals: nothing. idle and active are the same picture
Same two users, same week. Above, message size and timing draw the outline of a conversation even though every byte is encrypted. Below, the client emits fixed 4096-byte blobs at a constant rate whether you are typing or asleep, so there is no outline to draw.

Measured, not claimed

  • 4096bytes, always Every blob on the wire is the same size. A lookup miss returns 4096 bytes of decoy, so occupancy is not an oracle either.
  • 3.9MB / hour, constant Cover traffic runs at a fixed rate whether you are chatting or silent. Idle and active look identical from outside.
  • 98ms proof of work Memory-hard Argon2id at 64 MiB, paid on every write, so flooding a relay costs attacker CPU rather than defender bandwidth.
  • 412automated tests Across the Rust core, the desktop client and the Android client. Clippy clean at -D warnings.
  • 43red-team findings From one structured internal round. Every one closed, and every one holds a named regression test so it cannot quietly return.
  • 0servers that know No component in the message path holds routing state. There is no conversation graph to seize, subpoena, leak or sell.

Why it is written down this way

Most of what I have learned on this project came from being wrong in a way that looked right. A test that passed for the wrong reason. A security gate that reported "clean" because the tool underneath it had crashed and printed nothing. A refresh mechanism that quietly produced nonce reuse, where the only thing that saved me was an unrelated defence in another component.

So the code carries the reasoning, not the mechanics — what the failure was, why the fix is shaped the way it is, and what it costs. Anyone can read what a function does. What is expensive to reconstruct is why it does it that way, and what was tried first.