Encryption & One-Time Pad
Seed-derived keystream expansion, one-time key discipline, and local disk-locking mechanics.
Overview
Wiltkey is built on an offline-first cipher inspired by the One-Time Pad (OTP). Rather than carrying physical pad cassettes, client devices swap a high-entropy seed face-to-face and expand it deterministically into a keystream with SHA-256 in counter mode. Plaintext bytes are XORed with that keystream. Because the keystream is derived from a seed (not truly-random key material as long as every message), this is precisely a computationally-secure stream cipher with one-time key discipline — not the information-theoretic “perfect secrecy” of a textbook OTP. In practice it keeps message content opaque to the blind relay and to any passive network observer; the honest claim is strong modern confidentiality, not mathematical unbreakability.
The primary security invariant is that a keystream byte is never used twice. Offsets are tracked so each byte is consumed once and never reissued; once the key material is exhausted it is gone forever, and the chat naturally wilts unless recharged in person.
Two storage models share this one cipher. A 1-on-1 chat pre-expands the keystream into a consumed pad on disk (the OTP-style model — each byte read once from a stored file, then retired). Group chats don't store a pad at all: every member shares one group seed and recomputes the group keystream on demand from it — a pure stream cipher, with fixed per-member lane offsets so any member can derive any lane. See Group Shared Pad.
Keystream Expansion Flow
How it Works
-
Deterministic Expansion:
During pairing, both clients exchange a high-entropy seed. A background task expands this seed into a pad file. The client hashes the seed concatenated with a 32-bit counter using SHA-256:
block_bytes = SHA-256(seed_bytes || counter_bytes)This continues until the requested pad buffer size (e.g. 10MB) is fully populated. Writing is done in 128KB chunks, yielding to the Flutter event loop to prevent main thread stutters.
-
XOR Encryption & Decryption:
To encrypt, the client opens the pad file as a cached
RandomAccessFiledescriptor, jumps to the outgoing offset, reads the required number of keystream bytes, and XORs them with the plaintext bytes:ciphertext = plaintext XOR keystream[offset ... offset + length]Decryption operates identically. Because the operation is symmetrical, the receiver reads from the same offset to restore the plaintext. - Envelope Absolute Offset: Every message is sent wrapped in an envelope containing the sender's identifier, the message ID, the ciphertext, and the absolute offset used. Decryption key bytes are derived solely from the envelope's offset, ensuring independence of local state.
Key Files & Symbols
| File Path | Symbol Name | Description |
|---|---|---|
lib/core/crypto/otp_service.dart |
WiltkeyOtpService |
Core utility class for keystream files and XOR crypt operations. |
lib/core/crypto/otp_service.dart |
generateKeystreamFile() |
Generates the keystream_<contactId>.pad file via chunked counter-mode hashing. |
lib/core/crypto/otp_service.dart |
xorWithKeystream() |
Reads pad bytes at a specific offset and performs the XOR crypt operation. Runs under a mutex lock. |
lib/core/crypto/otp_service.dart |
reconcilePads() |
Deletes orphan pad files whose identifiers are no longer active contacts. |
Gotchas & Edge Cases
Parallel reads from the same pad file will corrupt the file descriptor seek pointer. To prevent this,
WiltkeyOtpService serializes all XOR reads on a per-contact lock: _locks[contactId]. A failed read will block subsequent message processing for that contact.
If the message size pushes the offset past the file's end,
xorWithKeystream throws a pad overflow exception. The message fails to encrypt, and the pointer resets.