Add initial protocol specification
Documents the messagebox protocol with implementation-agnostic state machines for sender, store, and reader components.
This commit is contained in:
commit
184702057b
1 changed files with 801 additions and 0 deletions
801
SPECIFICATION.md
Normal file
801
SPECIFICATION.md
Normal file
|
|
@ -0,0 +1,801 @@
|
||||||
|
# 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
|
||||||
|
- 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)
|
||||||
Loading…
Reference in a new issue