messagebox-protocol/SPECIFICATION.md

841 lines
20 KiB
Markdown
Raw Permalink Normal View History

# 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<MessageRecord> (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 (not bytes - Unicode aware)
- Maximum sender name: 200 characters (not bytes - Unicode aware)
- Maximum sealed_box size: ~40KB (10,000 chars × 4 bytes/char UTF-8 max + ~48 bytes overhead)
- Note: Actual sealed_box size depends on plaintext encoding
- Conservative estimate: 50KB covers worst case
- Minimum message length: 1 character (empty rejected)
- Minimum sender name: 1 character (empty rejected)
### 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
---
## Cryptographic Primitives
**Sealed Box:** NaCl/libsodium compatible
- Combines public key encryption + authentication
- No separate nonce management required
- All-in-one ciphertext blob
- Uses Curve25519 for public key cryptography (X25519)
- Encrypts to a public key without requiring the sender's private key
- Provides IND-CCA2 security with authenticated encryption
**Key Generation:**
- Public/private keypair generation using Curve25519
- Public key: 32 bytes
- Private key: 32 bytes
- Implementation: libsodium/sodiumoxide crypto_box keypair
**Operations:**
- `seal(plaintext, public_key) → sealed_box`
- Internally generates ephemeral keypair
- Encrypts using XSalsa20-Poly1305
- Returns ciphertext || ephemeral_public_key
- `unseal(sealed_box, private_key) → plaintext | error`
- Extracts ephemeral public key from sealed_box
- Derives shared secret
- Decrypts and authenticates
- Returns plaintext on success, error on tampering/wrong key
---
## Implementation Insights
### Sender Client Implementation Notes
**State Enforcement:**
- Strict state machine prevents invalid transitions
- Each operation checks current state before proceeding
- Invalid transitions return explicit error messages
- Error state allows for graceful recovery via reset
**Memory Safety:**
- Plaintext is consumed (moved) during seal operation
- After sealing, plaintext is automatically dropped
- Sealed message is consumed during submit operation
- No plaintext remains in memory after encryption
**Validation Edge Cases:**
- Empty strings are rejected (both sender_name and message_body)
- Unicode characters are supported and count correctly
- Character limits are enforced, not byte limits
- Newlines and special characters are preserved
**Cryptographic Properties:**
- Same plaintext produces different ciphertext each time (due to ephemeral keys)
- Sealed box is larger than plaintext by ~48 bytes (ephemeral PK + auth tag + nonce)
- Encryption cannot fail given valid public key
- No key management required on sender side (stateless)
**UUIDs and Timestamps:**
- UUIDs are v4 (random), not v1 (time-based)
- Timestamps are Unix epoch seconds (i64)
- Message IDs are globally unique with high probability
**Reset Behavior:**
- Reset can be called from any state (not just SUBMITTED)
- Allows error recovery by forcing return to IDLE
- Clears all intermediate state (plaintext, sealed_message)
- Public key is retained after reset (stays in IDLE, not destroyed)
**Key ID Management:**
- Key ID is opaque string (implementation-defined format)
- Sender doesn't validate key_id format
- Key ID is preserved from PublicKey through MessageRecord
- Enables key rotation without sender awareness
---
## Version History
- v0.1 - Initial specification (2026-01-19)