# Anonymous Messaging System Specification ## System Overview A secure anonymous messaging system where: - Visitors leave messages via web form (name + message) - Messages are cryptographically sealed in browser before transmission - Encrypted messages stored on semi-untrusted VPS - Only recipient can decrypt messages locally on their laptop - Message content is opaque to all intermediaries --- ## System Architecture ### Three Components 1. **Sender Client** - Browser-based message composition and encryption 2. **Message Store** - VPS-hosted storage for encrypted messages 3. **Reader Client** - Laptop-based decryption and archive ### Trust Model ``` Sender Browser ─(sealed_box)─→ Message Store ─(sealed_box)─→ Reader Laptop ↓ Sees metadata only Cannot read content ``` **Security Property:** Only holder of private key can decrypt message content. --- ## Core Data Structures ### Message Record ``` { message_id: UUID, sender_name: string, // PLAINTEXT created_at: timestamp, // PLAINTEXT key_id: string, // Which public key was used sealed_box: bytes // ENCRYPTED message content } ``` ### Public Key Record ``` { key_id: string, public_key: bytes, created_at: timestamp, status: ACTIVE | INACTIVE } ``` ### Key Pool Entry (Reader only) ``` { key_id: string, public_key: bytes, private_key: bytes, created_at: timestamp, status: ACTIVE | ARCHIVED } ``` ### Decrypted Message (Reader local storage) ``` { message_id: UUID, sender_name: string, sent_at: timestamp, message_body: string, retrieved_at: timestamp, decrypted_with: string // key_id used for decryption } ``` --- ## Component 1: Sender Client ### State Machine ``` States: IDLE READY COMPOSING ENCRYPTING SUBMITTED ERROR Transitions: IDLE → READY (on load_public_key) READY → COMPOSING (on user_input) COMPOSING → ENCRYPTING (on submit) ENCRYPTING → SUBMITTED (on encryption_complete) SUBMITTED → IDLE (on confirmation) * → ERROR (on any failure) ERROR → IDLE (on reset) ``` ### State Context The sender state machine maintains: - **current_state**: One of {IDLE, READY, COMPOSING, ENCRYPTING, SUBMITTED, ERROR} - **public_key**: The active public key loaded from the message store - **plaintext**: Temporary storage for sender_name and message_body - **sealed_message**: Encrypted message ready for transmission ### State Transitions #### IDLE → READY **Trigger:** `load_public_key(public_key)` **Actions:** 1. Store public_key in context 2. Transition to READY state **Postconditions:** - public_key is available for encryption - System ready to accept message composition --- #### READY → COMPOSING **Trigger:** `compose_message(sender_name, message_body)` **Actions:** 1. Validate sender_name (non-empty, ≤200 chars) 2. Validate message_body (non-empty, ≤10,000 chars) 3. Store plaintext in context 4. Transition to COMPOSING state **Postconditions:** - plaintext message is in memory - Ready for encryption --- #### COMPOSING → ENCRYPTING **Trigger:** `seal_message()` **Actions:** 1. Retrieve public_key from context 2. Apply cryptographic seal: `sealed_box = seal(message_body, public_key)` 3. Create sealed_message with: - sender_name (plaintext) - key_id (from public_key) - sealed_box (ciphertext) 4. Destroy plaintext from memory 5. Transition to ENCRYPTING state **Postconditions:** - plaintext no longer in memory - sealed_message ready for transmission - Message content is cryptographically sealed --- #### ENCRYPTING → SUBMITTED **Trigger:** `submit_to_store()` **Actions:** 1. Generate unique message_id (UUID) 2. Capture current timestamp 3. Construct MessageRecord: - message_id - sender_name - created_at - key_id - sealed_box 4. Transmit MessageRecord to message store 5. Wait for confirmation 6. Transition to SUBMITTED state **Postconditions:** - Message stored remotely - Confirmation received --- #### SUBMITTED → IDLE **Trigger:** `reset()` **Actions:** 1. Clear sealed_message from memory 2. Clear any remaining context 3. Transition to IDLE state **Postconditions:** - No message data retained - Ready for next message --- #### Any State → ERROR **Trigger:** Any operation failure **Actions:** 1. Capture error details 2. Transition to ERROR state 3. Preserve context for debugging **Recovery:** Manual reset to IDLE ### Data Flow ``` User Input (name, message_body) ↓ Store in memory as plaintext ↓ Load active public_key from store ↓ Encrypt: message_body + public_key → sealed_box ↓ Destroy plaintext from memory ↓ Create MessageRecord with sealed_box + metadata ↓ Transmit to Message Store ↓ Receive confirmation ↓ Reset state (sender retains nothing) ``` ### Constraints - **Message length:** Max 10,000 characters (reasonable essay length) - **Sender name:** Max 200 characters - **Memory safety:** Plaintext destroyed immediately after encryption - **No persistence:** Sender client stores nothing after submission --- ## Component 2: Message Store ### State Machine (per message) ``` States: RECEIVED VALIDATED STORED REJECTED Transitions: RECEIVED → VALIDATED (on validate_structure) VALIDATED → STORED (on persist) RECEIVED → REJECTED (on validation_failure) VALIDATED → REJECTED (on storage_failure) ``` ### State Context The message store maintains: - **messages**: Collection of MessageRecord entries - **keys**: Collection of PublicKeyRecord entries ### Operations #### receive_message(incoming_message) **State Flow:** RECEIVED → VALIDATED → STORED (or REJECTED) **Validation Phase (RECEIVED → VALIDATED):** 1. Check sender_name: non-empty and ≤200 characters 2. Check sealed_box: non-empty and ≤50KB 3. Verify key_id exists in keys collection 4. If any check fails: transition to REJECTED, return error **Storage Phase (VALIDATED → STORED):** 1. Generate unique message_id (UUID) 2. Capture current timestamp 3. Construct MessageRecord with all fields 4. Add to messages collection 5. Return message_id as confirmation --- #### get_all_messages() **Action:** Return all MessageRecord entries from messages collection **Use Case:** Reader client batch retrieval --- #### get_messages_since(timestamp) **Action:** Return MessageRecord entries where created_at > timestamp **Use Case:** Incremental message retrieval --- #### delete_message(message_id) **Actions:** 1. Locate MessageRecord by message_id 2. If not found: return error 3. Remove from messages collection 4. Return success **Use Case:** Reader cleanup after successful decryption --- #### add_public_key(public_key) **Actions:** 1. Find all keys with status=ACTIVE 2. Update them to status=INACTIVE 3. Generate new key_id 4. Create PublicKeyRecord: - key_id - public_key - created_at (current timestamp) - status = ACTIVE 5. Add to keys collection 6. Return key_id **Side Effect:** Only one key is ACTIVE at any time --- #### get_active_key() **Action:** Return PublicKeyRecord where status=ACTIVE **Use Case:** Sender client retrieving current encryption key --- #### get_all_keys() **Action:** Return all PublicKeyRecord entries **Use Case:** Reader client key synchronization ### Storage Schema ``` messages: [MessageRecord] keys: [PublicKeyRecord] ``` ### Validation Rules **On message submission:** - `sender_name`: non-empty, max 200 chars - `sealed_box`: non-empty, max 50KB - `key_id`: must exist in keys table **On key addition:** - Authenticated request (recipient only) - Valid public key format - Automatically deactivates previous ACTIVE keys ### Data Visibility **Store can see:** - Sender name (plaintext) - Timestamp (plaintext) - Which public key was used (key_id) - Number and size of messages **Store cannot see:** - Message content (encrypted in sealed_box) - Decryption success/failure - Reader's retrieval patterns (stateless) --- ## Component 3: Reader Client ### State Machine (Batch Operation) ``` States: IDLE FETCHING_BATCH DECRYPTING_BATCH SAVING_BATCH ERROR Transitions: IDLE → FETCHING_BATCH (on fetch_messages) FETCHING_BATCH → DECRYPTING_BATCH (on messages_received) DECRYPTING_BATCH → SAVING_BATCH (on batch_decrypted) SAVING_BATCH → IDLE (on save_complete) FETCHING_BATCH → ERROR (on network_failure) DECRYPTING_BATCH → IDLE (on partial_success, logs failures) SAVING_BATCH → ERROR (on storage_failure) ERROR → IDLE (on reset) ``` ### State Context The reader client maintains: - **current_state**: One of {IDLE, FETCHING_BATCH, DECRYPTING_BATCH, SAVING_BATCH, ERROR} - **key_pool**: Collection of KeyPoolEntry (all historical private keys) - **local_archive**: Collection of DecryptedMessage entries - **undecryptable**: List of message_id values that failed decryption - **last_fetch**: Timestamp of most recent successful fetch ### State Transitions #### IDLE → FETCHING_BATCH **Trigger:** `fetch_messages()` **Actions:** 1. Transition to FETCHING_BATCH state 2. Request all MessageRecord entries from message store 3. Receive batch of encrypted messages **Error Handling:** Network failure → ERROR state --- #### FETCHING_BATCH → DECRYPTING_BATCH **Trigger:** `decrypt_batch(messages)` **Actions:** 1. Transition to DECRYPTING_BATCH state 2. For each MessageRecord in batch: - Call decrypt_single_message() - On success: add to decrypted list - On failure: add message_id to failed list 3. Return BatchDecryptResult containing both lists **Decryption Strategy (per message):** 1. **Primary attempt:** Find key in pool matching message.key_id 2. **Try unseal:** `unseal(sealed_box, private_key)` 3. **If fails:** Iterate through all keys in pool 4. **If any succeeds:** Return plaintext + key_id 5. **If all fail:** Return decryption error **Partial Success:** Successfully decrypted messages are saved; failures are logged --- #### DECRYPTING_BATCH → SAVING_BATCH **Trigger:** `save_batch(result)` **Actions:** 1. Transition to SAVING_BATCH state 2. Append all decrypted messages to local_archive 3. Append all failed message_ids to undecryptable list 4. Update last_fetch to current timestamp 5. Persist to local storage **Error Handling:** Storage failure → ERROR state --- #### SAVING_BATCH → IDLE **Trigger:** `complete()` **Actions:** 1. Transition to IDLE state 2. Ready for next fetch cycle --- ### Key Pool Operations #### rotate_key() **Actions:** 1. Generate new cryptographic keypair (public_key, private_key) 2. Find all keys in pool with status=ACTIVE 3. Update them to status=ARCHIVED 4. Generate new key_id 5. Create KeyPoolEntry: - key_id - public_key - private_key - created_at (current timestamp) - status = ACTIVE 6. Add to key_pool 7. Publish public_key to message store (external call to add_public_key) **Key Retention:** Archived keys are NEVER deleted (required for decrypting old messages) --- #### sync_keys() **Actions:** 1. Fetch all PublicKeyRecord entries from message store 2. Extract key_ids from local key_pool 3. Identify remote keys not in local pool 4. Return KeySyncReport listing missing private keys **Use Case:** Detecting key pool desynchronization (e.g., backup restore scenario) ### Local Storage ``` key_pool.json: [KeyPoolEntry, ...] messages.json: [DecryptedMessage, ...] state.json: { last_fetch: timestamp, undecryptable: [UUID, ...] } ``` ### Data Flow ``` Request all messages from Message Store ↓ Receive Vec (batch) ↓ For each message in batch: ↓ Find matching private_key by key_id ↓ Attempt decrypt with matched key ↓ If fail: try all keys in pool ↓ If success: add to decrypted list If fail: add to undecryptable list ↓ Save all decrypted messages to local archive ↓ Update state (last_fetch timestamp) ↓ Return to IDLE ``` ### Key Pool Management **Key pool properties:** - Maintains ALL historical private keys (never deletes) - One key marked ACTIVE (for rotation operations) - Old keys marked ARCHIVED (still used for decryption) - Keys never removed (would make old messages undecryptable) **Rotation process:** ``` Generate new keypair ↓ Add to local pool as ACTIVE ↓ Mark previous ACTIVE → ARCHIVED ↓ Publish new public_key to Message Store ↓ Store marks new key ACTIVE, old key INACTIVE ↓ Future messages encrypted with new key ↓ Old messages still decryptable with archived keys ``` --- ## System-Wide Flows ### End-to-End Message Flow ``` 1. SENDER SIDE User enters name + message → Sender loads active public key from store → Sender seals message with public key → Sender transmits sealed_box + metadata → Sender destroys plaintext → Sender receives confirmation 2. STORAGE Store receives MessageRecord → Validates structure → Persists to storage → Returns message_id 3. READER SIDE Reader fetches all messages (batch) → For each message: Try decrypt with key_id match Fallback to all keys in pool → Save successful decryptions → Log failed decryptions → Update local state ``` ### Key Rotation Flow ``` 1. READER INITIATES ROTATION Generate new keypair → Add to local key_pool (ACTIVE) → Archive old keys (ARCHIVED) → Publish new public_key to store 2. STORE UPDATES Receive new public_key → Deactivate old keys (INACTIVE) → Activate new key (ACTIVE) 3. CONCURRENT SENDERS Sender A: fetched old key before rotation → Encrypts with old key → Reader still has old private key (ARCHIVED) → Decryption succeeds Sender B: fetches new key after rotation → Encrypts with new key → Reader has new private key (ACTIVE) → Decryption succeeds ``` --- ## Edge Cases & Failure Modes ### Undecryptable Messages **Causes:** - Message encrypted with unknown key_id - Corrupted sealed_box during transmission - Key rotation timing edge case - Malicious tampering **Handling:** - Add message_id to undecryptable list - Preserve raw sealed_box for manual inspection - Periodic retry (in case missing key added later) - Log for debugging ### Key Sync Mismatch **Scenario 1: Store has key X, Reader doesn't** ``` Reader fetches messages encrypted to X ↓ Decryption fails (no matching private key) ↓ Reader calls sync_keys() ↓ Discovers missing private key for X ↓ Flags for manual intervention ``` **Scenario 2: Reader has key Y, Store doesn't** ``` No impact ↓ Key Y is historical/archived ↓ No new messages encrypted to Y ↓ Reader keeps Y for old messages ``` ### Store Compromise **Attacker gains access to VPS:** **Can:** - Read all metadata (sender names, timestamps) - See all sealed_box ciphertexts (useless without private keys) - Delete messages (availability attack) - Serve malicious public key (MITM future messages) **Cannot:** - Decrypt existing messages (no private keys) - Forge messages that decrypt properly - Retroactively decrypt past messages **Mitigation:** - Regular backups of message store - Monitor for unexpected key rotations - Out-of-band public key verification (future enhancement) ### Concurrent Key Rotation **Scenario:** ``` T0: Sender fetches public_key A T1: Reader rotates to key B T2: Store updates active key → B T3: Sender submits message encrypted with A Result: Message encrypted with old key A → Reader still has private_key A in pool (ARCHIVED) → Decryption succeeds → No data loss ``` ### Message Store Full **Not currently specified** - future consideration: - Max storage quota - Auto-deletion after N days - Reader notification when approaching limit --- ## Security Properties ### Confidentiality - **Message content:** Only reader with private key can decrypt - **Sender name:** Visible to store (plaintext) - **Timing:** Message timestamps visible to store ### Integrity - Sealed box cryptography provides authentication - Tampering detection built into crypto scheme - Failed authentication → decryption failure ### Availability - Stateless retrieval (no read locks) - Store compromise → messages still readable from backups - Key loss → messages permanently lost (by design) ### Anonymity - No sender authentication required - IP addresses, user agents: implementation detail - Sender name is self-asserted (no verification) --- ## Constraints & Limits ### Message Constraints - Maximum message length: 10,000 characters - Maximum sender name: 200 characters - Maximum sealed_box size: 50KB ### Storage Constraints - Messages persist indefinitely (no auto-deletion) - No maximum message count (unbounded growth) ### Performance Constraints - Batch operations preferred (reader fetches all at once) - Stateless protocol (no session management) - Crypto operations: single-threaded acceptable for low volume --- ## Future Enhancements (Out of Scope) - Public key fingerprint verification - Sender reply channel (optional contact info) - Message categories/tags - Read receipts - Storage quotas and auto-cleanup - Multi-device reader support - Message threading - Rate limiting and spam prevention --- ## Cryptographic Primitives **Sealed Box:** NaCl/libsodium compatible - Combines public key encryption + authentication - No separate nonce management required - All-in-one ciphertext blob **Key Generation:** - Public/private keypair generation - Implementation agnostic (can be RSA, ECC, X25519, etc.) **Operations:** - `seal(plaintext, public_key) → sealed_box` - `unseal(sealed_box, private_key) → plaintext | error` --- ## Open Questions 1. **Authentication mechanism** for Reader → Store operations (key rotation, deletion) 2. **Public key verification** - how does sender know public_key is authentic? 3. **Rate limiting** - should store impose submission limits? 4. **Message expiry** - auto-delete after N days? 5. **Multi-device reader** - how to distribute private keys securely? --- ## Version History - v0.1 - Initial specification (2026-01-19)