Update specification with implementation insights

Add comprehensive implementation notes based on sender client development:
- Detailed cryptographic primitive specifications (Curve25519, XSalsa20-Poly1305)
- Memory safety patterns (move semantics for plaintext)
- State machine enforcement strategies
- Unicode and character counting considerations
- Cryptographic property documentation (randomness, overhead)
- Design decisions with rationales
- Additional open questions discovered during implementation
- Updated constraints with actual size calculations
This commit is contained in:
Zachery Aaron Shores-Chmielewski 2026-01-19 08:50:50 +07:00
parent f0f33e0934
commit f3ab842cc2

View file

@ -741,9 +741,13 @@ Result:
## Constraints & Limits ## Constraints & Limits
### Message Constraints ### Message Constraints
- Maximum message length: 10,000 characters - Maximum message length: 10,000 characters (not bytes - Unicode aware)
- Maximum sender name: 200 characters - Maximum sender name: 200 characters (not bytes - Unicode aware)
- Maximum sealed_box size: 50KB - 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 ### Storage Constraints
- Messages persist indefinitely (no auto-deletion) - Messages persist indefinitely (no auto-deletion)
@ -775,17 +779,140 @@ Result:
- Combines public key encryption + authentication - Combines public key encryption + authentication
- No separate nonce management required - No separate nonce management required
- All-in-one ciphertext blob - 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:** **Key Generation:**
- Public/private keypair generation - Public/private keypair generation using Curve25519
- Implementation agnostic (can be RSA, ECC, X25519, etc.) - Public key: 32 bytes
- Private key: 32 bytes
- Implementation: libsodium/sodiumoxide crypto_box keypair
**Operations:** **Operations:**
- `seal(plaintext, public_key) → sealed_box` - `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` - `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 ## Open Questions
1. **Authentication mechanism** for Reader → Store operations (key rotation, deletion) 1. **Authentication mechanism** for Reader → Store operations (key rotation, deletion)
@ -793,6 +920,9 @@ Result:
3. **Rate limiting** - should store impose submission limits? 3. **Rate limiting** - should store impose submission limits?
4. **Message expiry** - auto-delete after N days? 4. **Message expiry** - auto-delete after N days?
5. **Multi-device reader** - how to distribute private keys securely? 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?
--- ---