Core Architecture

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

Seed Bytes Agreed hex + Counter 0, 1, 2, ... SHA-256 Counter Mode keystream.pad 32B blocks buffered write

How it Works

  1. 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.
  2. XOR Encryption & Decryption: To encrypt, the client opens the pad file as a cached RandomAccessFile descriptor, 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.
  3. 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

🛑 SERIALIZED MUTEX LOCKING
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.
⚠️ BUFFER OVERFLOW
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.