diff --git a/SPECIFICATION.md b/SPECIFICATION.md index d79f0d1..8759514 100644 --- a/SPECIFICATION.md +++ b/SPECIFICATION.md @@ -741,9 +741,13 @@ Result: ## Constraints & Limits ### Message Constraints -- Maximum message length: 10,000 characters -- Maximum sender name: 200 characters -- Maximum sealed_box size: 50KB +- 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) @@ -775,17 +779,140 @@ Result: - 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 -- Implementation agnostic (can be RSA, ECC, X25519, etc.) +- 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 + +### Testing Considerations + +**Essential Test Coverage:** +1. State transitions (valid and invalid) +2. Input validation (empty, max, over-max) +3. Cryptographic randomness (same input → different output) +4. Memory cleanup (plaintext destroyed after sealing) +5. Unicode and special character handling +6. Multiple messages in sequence +7. Timestamp correctness +8. UUID uniqueness + +**Error Cases:** +- Invalid state transitions +- Missing public key +- Missing plaintext +- Missing sealed message +- Invalid sender name (empty, too long) +- Invalid message body (empty, too long) + +--- + +## Design Decisions (from Implementation) + +### Public Key Structure +**Decision:** PublicKey contains both key_id and public_key bytes +- **Rationale:** Sender needs key_id to include in MessageRecord +- **Alternative considered:** Fetch key_id separately from store +- **Chosen approach:** Bundle them together to reduce round trips + +### State Machine Strictness +**Decision:** Enforce state transitions with compile-time types would be ideal, but runtime checks are practical +- **Rationale:** Makes invalid states unrepresentable +- **Trade-off:** More complex type system vs runtime flexibility +- **Implementation:** Runtime checks with explicit error types + +### Plaintext Ownership +**Decision:** Move (consume) plaintext during seal operation +- **Rationale:** Prevents accidental reuse or logging +- **Alternative considered:** Clone and clear +- **Chosen approach:** Rust's move semantics provide memory safety guarantee + +### Reset from Any State +**Decision:** Allow reset from any state, not just SUBMITTED or ERROR +- **Rationale:** Provides escape hatch for error recovery +- **Trade-off:** Less strict state machine vs operational flexibility +- **Chosen approach:** Flexible reset for better UX + +### Timestamp Precision +**Decision:** Unix epoch seconds (not milliseconds) +- **Rationale:** Second precision sufficient for message ordering +- **Trade-off:** Less precision vs simpler arithmetic +- **Chosen approach:** Seconds for simplicity + +### Character vs Byte Limits +**Decision:** Enforce character limits, not byte limits +- **Rationale:** User-facing (WYSIWYG behavior) +- **Trade-off:** Unicode overhead vs user expectations +- **Chosen approach:** Character counting for better UX + +### Error Type Design +**Decision:** Use enum with contextual error messages +- **Rationale:** Structured errors enable better error handling +- **Alternative considered:** String errors +- **Chosen approach:** Typed errors with Display impl for messages + ## Open Questions 1. **Authentication mechanism** for Reader → Store operations (key rotation, deletion) @@ -793,6 +920,9 @@ Result: 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? +6. **Sealed box size** - current spec says max 50KB, but actual max message is 10KB chars + overhead (~48 bytes). Should store validate based on plaintext limit or total size? +7. **Key ID format** - should it be standardized (e.g., hash of public key) or implementation-defined? +8. **Concurrent key rotation** - if sender caches public key, how long is cache valid? ---