Ratchet: a one-time pad for people who type

In 1882, a California banker named Frank Miller published a book called Telegraphic Code to Insure Privacy and Secrecy in the Transmission of Telegrams. It described a system in which the sender and receiver each held identical lists of random numbers. To encrypt a message, you added each letter to the next unused number on the list, modulo the alphabet. To decrypt, you subtracted. When the list ran out, the conversation was over.

Miller had invented the one-time pad. It would take another thirty-five years before Gilbert Vernam patented a version, and another thirty after that before Claude Shannon proved what Miller had intuited: a one-time pad, used correctly, is unbreakable. Not computationally hard. Not exponentially expensive. Mathematically impossible to crack. The ciphertext contains literally no information about the plaintext, because every possible plaintext is equally likely.

The proof is elegant. If the key is truly random, at least as long as the message, and never reused, then for any ciphertext there exists a key that decrypts it to any plaintext of the same length. An attacker who intercepts the ciphertext and tries every possible key will recover every possible message — “ATTACK AT DAWN” and “IGNORE THIS NOTE” and “WEATHER IS NICE” — with equal probability. There is no way to distinguish the real plaintext from the noise. Information-theoretic security. The gold standard. The only provably unbreakable encryption scheme known to mathematics.

The catch is in the three conditions. The key must be truly random. The key must be at least as long as the message. The key must never be reused. Violate any one of these and the proof collapses. The Venona project broke Soviet one-time pads not because one-time pads are weak, but because the Soviets, under wartime pressure, reused pages. Two messages encrypted with the same key can be superimposed, and the key cancels out, leaving a relationship between the two plaintexts that a patient cryptanalyst can exploit.

This is the fundamental tension: the most secure encryption scheme ever devised is also the least practical. You need to generate and distribute vast quantities of truly random key material, in advance, through a secure channel, and then destroy it after a single use. For most of human history, this meant filling notebooks with random numbers and physically handing them to your correspondent.

Ratchet is an attempt to build something that feels like a one-time pad — forward-secret, destroy-after-use, immune to retrospective compromise — using modern symmetric cryptography and a key that fits in a 32-byte file.


What ratchet does

You have two terminals. One encrypts, one decrypts. You type a line, press enter, and the encrypted frame appears on the other end. Each line gets its own key. After encryption, the key is destroyed and a new one is derived. The old key cannot be recovered. If someone captures the ciphertext and later compromises your passphrase, your key file, and even the current session key, they cannot decrypt any message that was encrypted before the compromise.

This is forward secrecy. Signal has it. TLS 1.3 has it. What ratchet does differently is strip the concept to its essential form: a single Rust binary with two modes of operation. Pipe mode reads from stdin and writes to stdout — offline encryption for files, tapes, and carrier pigeons. Interactive mode opens a QUIC tunnel between two peers, encrypted end-to-end, with the ratchet running on top. Both modes use the same cryptographic core.

# Generate a shared key file (exchange out-of-band)
ratchet keygen -o shared.key

# Pipe mode: encrypt and decrypt streams
ratchet enc -k shared.key < plaintext > ciphertext
ratchet dec -k shared.key < ciphertext > plaintext

# Interactive mode: encrypted chat over QUIC
ratchet listen -k shared.key --sign
ratchet connect -k shared.key <ticket> --sign --peer-key alice.pub

How to use it

Two people — call them Alice and Bob. They need to meet once, or have access to any channel they trust long enough to exchange two small files.

The meeting. Alice generates the shared key file. Both generate Ed25519 identities. They exchange the key file and their public keys — on a USB stick, via QR code, over an existing encrypted channel, whatever is available. After this, Alice has shared.key and bob.pub. Bob has the same shared.key and alice.pub. The meeting is over. They do not need to meet again.

# Alice generates the shared key (32 bytes of randomness)
ratchet keygen -o shared.key

# Both generate their identities (once, ever)
ratchet identity init

# They exchange shared.key and each other's ~/.ratchet/identity.pub

Interactive chat. Alice listens. Ratchet prints a connection ticket — a JSON blob containing her endpoint’s network address and relay information. She sends this ticket to Bob through any channel. The ticket is not secret; it is an address, not a key. Bob connects. Both are prompted for the passphrase they agreed on at the meeting.

# Alice
ratchet listen -k shared.key --sign --peer-key bob.pub

# Alice sees:
# listening. give this ticket to your peer:
# {"id":"...","addrs":[...]}

# Bob (pastes Alice's ticket)
ratchet connect -k shared.key <ticket> --sign --peer-key alice.pub

Both type lines. Each line is encrypted with a unique ratcheted key, signed with the sender’s Ed25519 identity, sent over QUIC, verified against the expected public key, decrypted, and printed. Every 50 messages, both sides see [rekey complete at seq 50] — the DH ratchet has fired, fresh randomness has entered the key chain, and any prior compromise of the symmetric state has been healed.

Pipe mode. When there is no live connection — when the message must travel by file, by email, by dead drop — pipe mode encrypts from stdin and writes framed ciphertext to stdout. The receiver decrypts from stdin and writes plaintext to stdout. The ratchet advances identically on both sides as long as they process messages in the same order.

# Alice encrypts a message
echo "meet at the usual place" | ratchet enc -k shared.key --sign > msg.bin

# She sends msg.bin however she likes — scp, email attachment, USB stick

# Bob decrypts and verifies Alice's signature
ratchet dec -k shared.key --peer-key alice.pub < msg.bin

Pipe mode without --sign and --peer-key works for pure symmetric encryption with no identity verification — useful when the channel itself provides authentication, or when the overhead of exchanging public keys is not justified.

Burning the notebook. When the conversation is over — when the relationship ends, the operation concludes, or the key has been in use long enough — both sides delete shared.key. Every message encrypted under that key file is now permanently undecryptable. Not by Alice. Not by Bob. Not by anyone who later obtains the passphrase. The key file was the second factor in the derivation, and it no longer exists. The ratchet has no reverse gear, and the seed from which it grew has been burned.

# Both sides
shred -u shared.key    # or: rm shared.key

A new conversation requires a new key file and a new exchange. This is the digital equivalent of tearing the last page out of the notebook and striking a match.


The key file as a one-time pad

The key file is 32 bytes of randomness generated by the operating system’s cryptographic random number generator. It is the closest thing in this system to the notebook of random numbers that Frank Miller described in 1882.

The analogy is imperfect but instructive. In a classical one-time pad, the notebook is the key — you consume it character by character. In ratchet, the key file is not consumed directly. Instead, it is combined with a passphrase through two layers of key derivation to produce a session key, and the session key is then ratcheted forward after every message. The key file provides entropy that the passphrase alone cannot — 256 bits of randomness versus whatever entropy a human-chosen passphrase contains. The passphrase provides something the key file alone cannot — a secret that exists only in the user’s memory and is never written to any storage medium.

Both are required. Compromise the key file without the passphrase and you have 32 bytes of random noise. Compromise the passphrase without the key file and you have a password to nothing. This is defense in depth at the key derivation layer.

The critical property the key file shares with a one-time pad is its disposability. If you and your correspondent agree to destroy the key file after a conversation, then even an adversary who later compels you to reveal your passphrase — through legal process, coercion, or rubber-hose cryptanalysis — cannot reconstruct the session key. The key file was the second factor, and it no longer exists.


How key derivation works

When you run ratchet enc or ratchet listen, three things happen before any plaintext is processed.

First, a 32-byte random salt is generated for this session. The salt is written to the output stream as part of a five-byte header — four bytes of magic (RTCH) and one byte of version — followed by the salt itself. The salt ensures that even if two sessions use the same passphrase and the same key file, they produce different session keys. This is a property that classical one-time pads get for free — each page is different — but that digital systems must engineer deliberately.

Second, the passphrase is fed through Argon2id, the current state of the art in password hashing. Argon2id is a memory-hard function — it requires 128 megabytes of RAM and three iterations to compute, which means an attacker trying to brute-force the passphrase must allocate 128 megabytes for every guess. A GPU with 24 gigabytes of VRAM can try fewer than 200 passphrases per second. At that rate, even a modest passphrase — six randomly chosen dictionary words — would take longer than the projected lifespan of the sun.

The salt for Argon2id is the random 32-byte salt generated for this session. This is a deliberate departure from the common pattern of using a fixed application salt. A per-session random salt means that two encryptions of the same plaintext with the same passphrase and the same key file produce completely different ciphertext, because the Argon2 output — and therefore the entire key schedule — is different.

Third, the Argon2 output and the key file are combined through HKDF — HMAC-based Key Derivation Function, specified in RFC 5869. The key file is the salt (in HKDF terminology), and the Argon2 output is the input keying material. The info string is ratchet-v2-session. HKDF produces the 32-byte session key.

salt        = random 32 bytes (written to stream header)
argon2_out  = Argon2id(passphrase, salt, 128 MiB, 3 iterations)
session_key = HKDF-SHA256(ikm=argon2_out, salt=key_file, info="ratchet-v2-session")

The Argon2 intermediate output is zeroed from memory immediately. The passphrase is zeroed. The key file contents are zeroed. Only the session key survives, and it is locked into physical RAM via mlock() to prevent the operating system from swapping it to disk.


The ratchet

Here is where ratchet diverges most sharply from a one-time pad — and where it does something that, in a certain light, is more interesting.

A one-time pad consumes key material. Each character of plaintext absorbs one character of key. When the notebook is empty, the system is dead. You need more key material, which means another physical meeting, another exchange of notebooks.

Ratchet generates its own key material from the session key, indefinitely, without any external input. After encrypting message n with session key K_n, the system derives K_{n+1}:

K_{n+1} = HKDF-SHA256(ikm=K_n, salt=none, info="ratchet-v2" || n)

This is a one-way function. Given K_{n+1}, you cannot compute K_n. The operation is a hash — it compresses the key through a function that is easy to compute forward and computationally infeasible to reverse. After derivation, K_n is overwritten in memory with K_{n+1}. The old key does not exist in any register, any cache line, any swap partition. It is gone.

This is the ratchet. Like a mechanical ratchet, it turns in only one direction. Each message advances the state by one click. An adversary who compromises the system at message 100 obtains K_{100} and can decrypt messages 100, 101, 102, and so on — but cannot walk the ratchet backward to recover K_0 through K_{99}. Those keys were destroyed the moment they were used.

A one-time pad achieves perfect secrecy for each message because the key for that message is perfectly random and unrelated to any other key. Ratchet achieves forward secrecy because the key for each message is computationally unrelated to the keys for previous messages — related only through a one-way function that cannot be inverted. This is a weaker guarantee than information-theoretic security. It depends on the assumption that SHA-256 is a good one-way function, which is a computational assumption, not a mathematical proof. But it is an assumption shared by essentially every secure system in existence, and one that has withstood decades of intensive study.

The sequence number is included in the HKDF info string as a domain separator. This means K_5 derived from the same parent key through two different paths — say, because of a bug or a replay attack — would only be valid if the sequence numbers match. The counter is also bound as Additional Authenticated Data in the ChaCha20-Poly1305 encryption. If an attacker reorders, duplicates, or drops frames, the decryption will fail, because the receiver’s expected sequence number will not match the AAD embedded in the ciphertext.


The sliding window

The v0.1 ratchet had a strict requirement: both sides must process messages in exactly the same order, or the session dies. Drop a single frame in transit and every subsequent decryption fails, because the receiver’s ratchet state has fallen behind the sender’s.

This is the cost of strict synchronization. It is also the cost of sending data over any real network, where packets can be delayed, reordered, or lost.

Ratchet v0.2 solves this with a sliding window. Instead of deriving one key at a time, the receiver pre-derives the next 32 keys from the current ratchet state and stores them in a hash map indexed by sequence number. When a frame arrives, the receiver looks up the key for that frame’s sequence number. If the frame is message 15 but the receiver was expecting message 12, the receiver skips ahead — it pulls K_15 from the window, decrypts the message, advances the ratchet state to 16, discards all keys below 15, and refills the window from the new position.

Messages 12, 13, and 14 are gone. Their keys were in the window but they were never claimed. If those messages arrive later, they can still be decrypted as long as their keys haven’t been evicted. If they arrive after the window has moved past them, they are silently lost — exactly as they would be on any lossy channel.

The window size is 32 by default. This means the receiver can tolerate up to 31 consecutive dropped messages before losing synchronization. In practice, for hand-entered text over a QUIC connection, this is more than sufficient. The window also provides natural replay protection: once a key is consumed, it is zeroed and removed from the map. A replayed frame will find no key and be rejected.


ChaCha20-Poly1305

Each message is encrypted with XChaCha20-Poly1305, the extended-nonce variant of the AEAD cipher specified in RFC 8439.

ChaCha20 is a stream cipher designed by Daniel Bernstein. It generates a pseudorandom keystream from a 256-bit key and a nonce, and XORs the keystream with the plaintext. The “X” variant uses a 192-bit nonce instead of the standard 96-bit nonce, which means nonces can be generated randomly without meaningful collision risk — the birthday bound for 192-bit nonces is approximately 2^96, a number so large that you could encrypt a message every nanosecond for the lifetime of the universe and never repeat.

Poly1305 is an authenticator. It computes a 128-bit tag over the ciphertext and any additional authenticated data (in ratchet’s case, the sequence number). If even one bit of the ciphertext or the AAD is modified, the tag will not verify and decryption will fail. There is no silent corruption. There is no partial decryption. Either the message is authentic and intact, or it is rejected.

The combination of ChaCha20 and Poly1305 provides authenticated encryption with associated data — AEAD. It guarantees confidentiality (no one can read the message), integrity (no one can modify the message), and authenticity (the message was produced by someone who knew the key). This is the same cipher used in TLS 1.3, WireGuard, and Signal’s Double Ratchet protocol.


Identity

A one-time pad has no concept of identity. If you hold the notebook, you are the correspondent. There is no way to prove that a particular message was written by a particular person — only that it was written by someone who had the key.

Ratchet v0.2 adds an optional identity layer built on Ed25519, the same elliptic curve signature scheme used in SSH, Tor, and Signal. The identity init command generates a keypair — a 32-byte private key stored in ~/.ratchet/identity.key with mode 0600, and a 32-byte public key at ~/.ratchet/identity.pub that can be freely shared.

When you encrypt with --sign, each frame gets a 64-byte Ed25519 signature appended after the Poly1305 tag. The signature covers the entire ciphertext — nonce, encrypted payload, and authentication tag. This means the signature attests not just to the plaintext content but to the specific encryption of that content at that position in the ratchet sequence.

When you decrypt with --peer-key alice.pub, each frame’s signature is verified before decryption is even attempted. If the signature does not match, the frame is rejected immediately. This provides a guarantee that the symmetric ratchet alone cannot: even if an attacker obtains the key file and the passphrase and the current ratchet state, they cannot forge messages that the receiver will accept as genuine unless they also possess the sender’s Ed25519 private key. The symmetric state authenticates the content. The signature authenticates the author.

The identity is optional. Pipe mode without --sign works exactly as it did before — pure symmetric encryption with no public key involvement. The identity layer is additive. It does not change the ratchet, the key derivation, or the AEAD. It adds a second, independent layer of authentication on top.


The DH ratchet: healing from compromise

Forward secrecy protects the past. If an adversary compromises the current key, they cannot decrypt previous messages because those keys were destroyed. But what about the future?

In ratchet v0.1, compromising the symmetric state was fatal. The adversary obtains K_n and can derive K_{n+1}, K_{n+2}, and every subsequent key, because the ratchet is deterministic — each key is derived from the one before it, and no external entropy enters the chain. The only defense was to abandon the session and start over with a new key file.

Ratchet v0.2 introduces a Diffie-Hellman ratchet step that runs automatically every 50 messages in interactive mode. Both peers generate fresh, ephemeral X25519 keypairs, exchange public keys over the QUIC stream, and perform a Diffie-Hellman key agreement. The resulting 32-byte shared secret is mixed into the ratchet state through HKDF:

dh_secret  = X25519(our_ephemeral_secret, peer_ephemeral_public)
K_new      = HKDF-SHA256(ikm=K_current, salt=dh_secret, info="ratchet-v2-dh-rekey")

The ephemeral private keys are discarded immediately after the computation. The DH shared secret is zeroed. Only the new ratchet key survives.

This is the same principle that Signal’s Double Ratchet uses, adapted for a simpler protocol. The effect is future secrecy: if an adversary compromises the ratchet state at message 40, they can read messages 40 through 49 — but at message 50, the DH ratchet fires, fresh randomness enters the chain, and the adversary is locked out again. They would need to compromise the ephemeral DH exchange itself, which requires an active man-in-the-middle position at the exact moment of the rekey, not merely passive access to the ratchet state.

In pipe mode, the DH ratchet does not fire — there is no bidirectional channel to exchange public keys. Pipe mode provides forward secrecy only. Interactive mode provides both forward secrecy and future secrecy. This is a meaningful distinction, and the right response is to use interactive mode when you can and pipe mode when you must.

If identity signing is enabled, the ephemeral DH public keys are signed with Ed25519 before transmission. This binds the rekey to the identity — an active attacker who tries to inject their own DH public key would need to forge an Ed25519 signature, which is computationally infeasible without the private key.


The QUIC transport

The v0.1 ratchet had no transport. It read from stdin and wrote to stdout, and getting the ciphertext from one machine to another was your problem. This was a principled choice — separation of concerns, Unix philosophy, don’t reinvent the wheel. It was also a practical limitation that made the tool difficult to use for its intended purpose: interactive, real-time encrypted communication between two people.

Ratchet v0.2 includes a built-in transport layer based on Iroh, a QUIC networking library that handles NAT traversal, hole punching, and relay fallback. The listen command starts an Iroh endpoint and prints a connection ticket — a JSON blob containing the endpoint’s public identity and network addresses. The connect command takes that ticket and establishes a QUIC connection.

The QUIC connection is itself encrypted with TLS 1.3 using X25519 key exchange. This is the transport layer — it protects data in transit against passive eavesdroppers. But ratchet does not trust the transport layer alone. Every message is encrypted again with ChaCha20-Poly1305 under the ratcheted session key, and optionally signed with Ed25519. A quantum computer that breaks the QUIC transport’s X25519 would find only ciphertext, encrypted with a key derived from Argon2id and HKDF, ratcheted forward through HKDF, and periodically refreshed via ephemeral X25519 DH exchanges.

Two layers of encryption. Two independent key schedules. The transport layer handles connectivity. The application layer handles secrecy.

The interactive mode uses a tokio::select! loop: one branch reads lines from stdin and encrypts them, the other reads frames from the QUIC stream and decrypts them. Both directions use the same ratchet state, protected by a mutex. The DH rekey is synchronous — when one side initiates a rekey, it sends its ephemeral public key and waits for the peer’s response before continuing. This simplifies the state machine at the cost of a brief pause every 50 messages, which for hand-entered text is imperceptible.


The display: TEMPEST countermeasures

Everything described so far protects the message on the wire and in memory. The cryptography is sound. The key management is disciplined. The ratchet is one-way. And then the plaintext appears on a screen, and a different class of threat emerges.

TEMPEST is the NSA’s code name for the study of unintentional electromagnetic emanations from electronic equipment. Every monitor, every cable, every keystroke produces electromagnetic radiation that, with the right antenna and signal processing, can be reconstructed from a distance. In 1985, Wim van Eck demonstrated that he could reconstruct the contents of a CRT display from its electromagnetic emissions at a range of several hundred meters using equipment costing a few hundred dollars. The technique works on LCD panels too — the emanations are different but recoverable.

Screen scraping is the software equivalent: a compromised terminal multiplexer, a malicious accessibility service, a memory-mapped framebuffer reader, or simply a recording of the terminal scrollback. The cryptography protects the message in transit. Nothing in the cryptographic protocol protects the message once it is rendered as human-readable text on a display device.

Ratchet v0.3 adds six software-level countermeasures. They are not a substitute for physical TEMPEST shielding — a Faraday cage will always outperform any software trick. But they raise the cost of both electromagnetic and software-based surveillance significantly, and they cost nothing in binary size, no additional dependencies, and no perceptible latency.

Alternate screen buffer. When ratchet enters interactive mode, it switches the terminal to the alternate screen buffer — the same mechanism that vim, less, and tmux use. The alternate buffer is a separate memory region from the normal terminal scrollback. When ratchet exits, it clears this buffer and switches back to the normal screen. The entire conversation vanishes. There is no scrollback to search, no buffer to dump, no terminal history to forensically recover. Screen capture tools that read the scrollback buffer get nothing, because the scrollback buffer never contained the conversation.

Auto-erasing messages. Each message — sent or received — is displayed for thirty seconds and then overwritten with blanks. The erase is handled by a background thread that sleeps for the configured duration and then writes ANSI clear-line sequences to the message’s screen position. An asynchronous screen scraper that samples the display at intervals has a thirty-second window to capture each message. After that, the screen position contains nothing. A shoulder surfer who glances at the screen sees only the most recent messages. A camera recording the display captures a rolling window that constantly erases itself.

Temporal dithering. Before each real character is rendered, two decoy characters are briefly flashed at the same cursor position. Each decoy is a random printable ASCII character displayed for a randomized interval between 400 and 1600 microseconds. The cursor moves back one position after each decoy, so the next character — decoy or real — overwrites the previous one at the same screen coordinates.

The purpose is electromagnetic. A TEMPEST receiver correlates the timing of screen updates with the electromagnetic emanations of the display hardware. When a character changes on screen, the display emits a characteristic pulse whose shape depends on the character. By recording many such pulses and aligning them to the known screen refresh timing, an attacker can reconstruct the displayed text. Temporal dithering breaks this correlation. The receiver sees three characters per position — two random decoys and one real character — rendered at randomized intervals that do not align with any predictable timing pattern. The signal-to-noise ratio for the real character drops by roughly a factor of three, and the randomized timing makes alignment across multiple frames unreliable.

This is not perfect. A sufficiently patient attacker with enough recorded frames can use statistical methods to distinguish the real character from the decoys — the real character persists while the decoys are transient. But the cost of the attack increases by orders of magnitude, and against a real-time conversation where messages auto-erase after thirty seconds, the recording window is severely constrained.

Screen clear on suspend. Unix terminals support job control — Ctrl-Z sends SIGTSTP to the foreground process, suspending it. The terminal then displays whatever was on screen at the moment of suspension. For a chat application, this means the plaintext of the conversation is frozen on the display, visible to anyone who walks past.

Ratchet installs a SIGTSTP handler that clears the entire screen before the process suspends. The handler writes ANSI clear-screen sequences, flushes the output, then re-raises SIGTSTP with the default handler to actually suspend the process. When the process resumes, it re-registers the handler. The effect is that Ctrl-Z produces a blank screen, not a frozen conversation.

Clipboard isolation. Bracketed paste mode is a terminal feature that wraps pasted text in escape sequences, allowing applications to distinguish typed input from pasted input. It can also be exploited by malicious terminal content to inject commands. Ratchet disables bracketed paste mode on initialization. It also disables all three levels of mouse event reporting (X10, button-event, any-event), which prevents selection-based text extraction by compromised terminal multiplexers or accessibility tools that monitor mouse-selected regions.

Full-screen clear on exit. Before leaving the alternate screen buffer, ratchet writes a full-screen clear sequence. This is belt-and-suspenders — the alternate screen buffer is already discarded on exit, but the explicit clear ensures that no terminal emulator implementation leaves residual content in video memory or in an intermediate rendering buffer.


Comparison: ratchet v0.3 vs. paper one-time pad

PropertyPaper OTPRatchet v0.3
Security modelInformation-theoreticComputational
Key materialMust be as long as all messages combined32-byte file + passphrase
Key distributionPhysical exchange of notebooksPhysical exchange of a file
Forward secrecyInherent (each page is independent)Engineered (HKDF ratchet)
Future secrecyN/A (no online protocol)X25519 DH rekey every 50 messages
Key reuse riskCatastrophic (Venona)Impossible (key is destroyed after each message)
IntegrityNone (OTP provides no authentication)Poly1305 128-bit MAC
IdentityNone (anyone with the notebook can send)Ed25519 signatures (optional)
Replay protectionNone (no sequence numbers)Sequence counter in AAD + sliding window
Message loss toleranceNone32-message sliding window
TransportPhysical courierQUIC with TLS 1.3 + NAT traversal
Brute-force resistanceUnconditional128 MiB Argon2id + 256-bit key file
Quantum resistanceUnconditionalChaCha20 has 128-bit post-quantum security
Display securityInk on paper (no emanations)Alternate screen, auto-erase, temporal dithering, SIGTSTP clear
PracticalityLow (bulky, fragile, consumed)High (single binary, indefinite use)

The paper one-time pad still wins on theoretical security. Nothing can beat it. Nothing ever will. Shannon’s proof is airtight. And paper, it must be noted, does not emit electromagnetic radiation when you read it.

But paper has its own display vulnerabilities. A discarded page can be recovered from a wastebasket. A notebook left open on a desk can be photographed. Ink can be read through the back of a thin page. The Soviets famously used one-time pad pages that dissolved in water — their version of auto-erase. Ratchet’s thirty-second display window and alternate screen buffer are the digital equivalent: the message exists long enough to be read and then ceases to exist on the display, in the scrollback, and in the terminal’s memory.

The one-time pad is a perfect lock with no door. Ratchet is a very good lock installed in a door, with hinges, a frame, a deadbolt, a chain, and curtains on the windows.


The binary

Ratchet v0.3 compiles to a 7.5-megabyte stripped ELF binary — unchanged from v0.2. The TEMPEST countermeasures are implemented entirely with ANSI escape sequences, POSIX signals, and standard thread primitives. No new dependencies. The release profile uses link-time optimization, single codegen unit, size optimization, and symbol stripping.

The key file is generated with ratchet keygen, which reads 32 bytes from /dev/urandom and writes them with mode 0600. The passphrase is read from /dev/tty by default, or from a file via --passphrase-file for scripted use. The passphrase never appears in the process table, the command line, or /proc.

All key material — the passphrase, the key file contents, the Argon2 intermediate, the session key, the DH ephemeral secrets, the pre-derived window keys — is zeroed after use with the zeroize crate, which compiles to volatile writes that the optimizer cannot remove. The session key is additionally locked into physical RAM with mlock() to prevent it from being paged to swap. Displayed plaintext is overwritten on screen after thirty seconds and never enters the terminal scrollback.


The notebook in digital form

There is something pleasingly circular about this. Frank Miller’s one-time pad was a physical object — a notebook, a stack of cards, a roll of tape. You held the key in your hands. You tore off each page as you used it and burned it. The security of the system was tangible: as long as the notebook was in your possession and the ashes were cold, your messages were safe.

Ratchet is not a one-time pad. It trades information-theoretic security for practicality, replacing an infinite supply of random key material with a 32-byte seed and a chain of one-way derivations. But it preserves the essential property that makes one-time pads compelling: after you encrypt a message, the key that encrypted it ceases to exist. Not “is stored securely.” Not “is protected by access controls.” Ceases to exist. The bits are overwritten. The memory is zeroed. The ratchet clicks forward and the past is unreachable.

There is a version of this tool that runs on paper. Two people share a notebook. On each page is a 32-character random string. To send a message, you XOR your plaintext with the string on the current page, tear out the page, and burn it. To receive, you XOR the ciphertext with your copy of the same page and burn yours.

Ratchet does exactly this, except the notebook has infinite pages and the pages are generated by hashing the previous page. The first page comes from Argon2id and HKDF. Every subsequent page comes from HKDF applied to the page before it. Every 50 pages, a fresh sheet of randomness is shuffled in from a Diffie-Hellman exchange that no one else can reproduce. Every page is signed in ink that only one person can produce. And the room you read it in has its lights flickering at random intervals, its windows covered, and a shredder that eats each page thirty seconds after you set it down.

You never run out. You never need to meet your correspondent to exchange a fresh notebook. You need to meet them once, to exchange 32 bytes and a public key.

That one meeting. That one USB stick. That is the notebook.

AEAD cipher: XChaCha20-Poly1305 (24-byte nonce, 128-bit tag)
Key derivation: Argon2id (128 MiB, 3 iterations) + HKDF-SHA256
Ratchet: HKDF-SHA256 with per-message sequence counter
DH ratchet: X25519 ephemeral rekey every 50 messages (interactive mode)
Identity: Ed25519 signatures (optional, 64-byte per frame)
Resync: 32-message sliding key window
Transport: QUIC via Iroh 0.96 (ALPN ratchet/2), TLS 1.3
Display: alternate screen buffer, 30s auto-erase, temporal dithering,
         decoy character rendering, SIGTSTP screen clear, clipboard isolation
Memory hygiene: zeroize crate + mlock()
Key file: 32 bytes, OS CSPRNG, mode 0600
Wire format: RTCH v2 header + 32-byte salt + typed/sequenced AEAD frames
Binary: 7.5 MB, Rust, no additional dependencies for display security