400 lines
16 KiB
Markdown
400 lines
16 KiB
Markdown
|
|
# Fumi - Smol Mail Client (Rust)
|
||
|
|
|
||
|
|
A Rust implementation of the Smol Mail client protocol, compatible with version 1.2 (including RNS transport). Fumi is the client counterpart to [bunshin](../bunshin), the Smol Mail server implementation.
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
Smol Mail is a minimalist, end-to-end encrypted mail protocol. This client implementation supports:
|
||
|
|
|
||
|
|
- **Full protocol v1.1**: All operations over TCP with Noise_NX_25519_ChaChaPoly_SHA256
|
||
|
|
- **Full protocol v1.2**: Reticulum Network Stack (RNS) transport for mesh networking
|
||
|
|
- **Shared codebase**: Single binary supports both transports (via feature flags)
|
||
|
|
- **Interoperability**: Works with reference Python implementations and bunshin server
|
||
|
|
|
||
|
|
## Protocol Compatibility
|
||
|
|
|
||
|
|
| Feature | v1.1 (TCP) | v1.2 (RNS) |
|
||
|
|
|---------|------------|------------|
|
||
|
|
| AUTH | ✅ | ✅ |
|
||
|
|
| RESOLVE | ✅ | ✅ |
|
||
|
|
| SEND | ✅ | ✅ |
|
||
|
|
| FETCH | ✅ | ✅ |
|
||
|
|
| DELETE | ✅ | ✅ |
|
||
|
|
| REGISTER | ✅ | ✅ |
|
||
|
|
| Accept Tokens | ✅ | ✅ |
|
||
|
|
| Key Rotation | ✅ | ✅ |
|
||
|
|
| Message Encryption | ✅ | ✅ |
|
||
|
|
|
||
|
|
## Address Formats
|
||
|
|
|
||
|
|
### TCP Transport (v1.1)
|
||
|
|
```
|
||
|
|
smol://alice@example.com[:1961]
|
||
|
|
smol://alice@example.com/abcdefghijklmnopqrstuvwxyz234567 # self-certifying
|
||
|
|
```
|
||
|
|
|
||
|
|
### RNS Transport (v1.2)
|
||
|
|
```
|
||
|
|
smol+rns://alice@8f2c1d0a7b6e5f4c3d2a1b0e9f8c7d6a
|
||
|
|
smol+rns://alice@8f2c1d0a7b6e5f4c3d2a1b0e9f8c7d6a/abcdefghijklmnopqrstuvwxyz234567
|
||
|
|
```
|
||
|
|
|
||
|
|
The RNS address uses the server's 32-character hex destination hash as the host.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Architecture
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────────────────────┐
|
||
|
|
│ fumi client │
|
||
|
|
├─────────────────────────────────────────────────────────────┤
|
||
|
|
│ │
|
||
|
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ CLI (main.rs) │ │
|
||
|
|
│ │ keygen, import, trust, register, send, │ │
|
||
|
|
│ │ fetch, delete, resolve, show │ │
|
||
|
|
│ └──────────────────────────────┬──────────────────────────┘ │
|
||
|
|
│ │ │
|
||
|
|
│ ┌──────────────────────────────▼──────────────────────────┐ │
|
||
|
|
│ │ Client Core (client.rs) │ │
|
||
|
|
│ │ - Session management with servers │ │
|
||
|
|
│ │ - Message composition and parsing │ │
|
||
|
|
│ │ - Envelope sealing/unsealing │ │
|
||
|
|
│ └──────────────────────────────┬──────────────────────────┘ │
|
||
|
|
│ │ │
|
||
|
|
│ ┌──────────────────────────────▼──────────────────────────┐ │
|
||
|
|
│ │ Account (account.rs) │ │
|
||
|
|
│ │ - Master secret management │ │
|
||
|
|
│ │ - Identity derivation (rotation chains) │ │
|
||
|
|
│ │ - Token generation │ │
|
||
|
|
│ └──────────────────────────────┬──────────────────────────┘ │
|
||
|
|
│ │ │
|
||
|
|
│ ┌─────────────────┐ ┌─────────────────────────────┐ │ │
|
||
|
|
│ │ TCP Transport │ │ RNS Transport │ │ │
|
||
|
|
│ │ (tcp_transport) │ │ (rns_transport + FFI) │ │ │
|
||
|
|
│ └────────┬────────┘ └──────────┬───────────────────┘ │ │
|
||
|
|
│ │ │ │
|
||
|
|
│ ▼ ▼ │
|
||
|
|
│ ┌────────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ Transport Trait (transport.rs) │ │
|
||
|
|
│ │ - connect(address) : Establish session │ │
|
||
|
|
│ │ - request(op, body) : Send frame, get response │ │
|
||
|
|
│ │ - bind_values() : Get auth binding values │ │
|
||
|
|
│ │ - close() : Clean up connection │ │
|
||
|
|
│ └────────────────────────────────────────────────────────┘ │
|
||
|
|
│ │
|
||
|
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ Store (store.rs) │ │
|
||
|
|
│ │ - SQLite local mailbox │ │
|
||
|
|
│ │ - Accounts, identities, tokens, messages │ │
|
||
|
|
│ └─────────────────────────────────────────────────────────┘ │
|
||
|
|
└─────────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Implementation Plan
|
||
|
|
|
||
|
|
### Phase 1: Core Client (TCP Only)
|
||
|
|
|
||
|
|
#### Dependencies
|
||
|
|
|
||
|
|
```toml
|
||
|
|
[package]
|
||
|
|
name = "fumi"
|
||
|
|
version = "0.1.0"
|
||
|
|
edition = "2021"
|
||
|
|
description = "Smol Mail client"
|
||
|
|
|
||
|
|
[dependencies]
|
||
|
|
snow = "0.9" # Noise protocol
|
||
|
|
ed25519-dalek = "2" # Ed25519 signatures
|
||
|
|
x25519-dalek = "2" # X25519 key agreement
|
||
|
|
sha2 = "0.10" # SHA-256
|
||
|
|
rand_core = "0.6" # Randomness
|
||
|
|
hmac = "0.12" # HMAC-SHA256
|
||
|
|
clap = "4" # CLI parsing
|
||
|
|
log = "0.4" # Logging
|
||
|
|
env_logger = "0.11" # Logger setup
|
||
|
|
anyhow = "1" # Error handling
|
||
|
|
rusqlite = "0.32" # SQLite local store
|
||
|
|
hex = "0.4" # Hex encoding
|
||
|
|
data-encoding = "2" # Base32 encoding
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Modules
|
||
|
|
|
||
|
|
1. **crypto.rs** - Cryptographic primitives
|
||
|
|
- Base32 encoding/decoding
|
||
|
|
- HKDF-SHA256
|
||
|
|
- HMAC-SHA256
|
||
|
|
- Identity derivation
|
||
|
|
- Accept token generation
|
||
|
|
- Message ID computation
|
||
|
|
|
||
|
|
2. **address.rs** - Address parsing
|
||
|
|
- Parse TCP addresses: `smol://user@host[:port][/identity]`
|
||
|
|
- Parse RNS addresses: `smol+rns://user@desthash[/identity]`
|
||
|
|
- Validate usernames
|
||
|
|
- Generate self-certifying URIs
|
||
|
|
|
||
|
|
3. **account.rs** - Account management
|
||
|
|
- Master secret storage
|
||
|
|
- Identity key derivation with rotation
|
||
|
|
- Current identity tracking
|
||
|
|
- Token generation for correspondents
|
||
|
|
|
||
|
|
4. **transport.rs** - Transport trait
|
||
|
|
- Define `Transport` trait with connect/request/close
|
||
|
|
- Define `TransportBindValues` for auth bindings
|
||
|
|
- Define operation and status constants
|
||
|
|
|
||
|
|
5. **tcp_transport.rs** - TCP transport
|
||
|
|
- Noise_NX handshake
|
||
|
|
- Frame reading/writing
|
||
|
|
- Server key pinning (optional)
|
||
|
|
|
||
|
|
6. **client.rs** - Core client logic
|
||
|
|
- Session management
|
||
|
|
- REGISTER operation
|
||
|
|
- AUTH operation
|
||
|
|
- RESOLVE operation
|
||
|
|
- SEND operation (envelope creation)
|
||
|
|
- FETCH operation
|
||
|
|
- DELETE operation
|
||
|
|
- Message sealing/unsealing
|
||
|
|
|
||
|
|
7. **store.rs** - Local SQLite store
|
||
|
|
- Account storage
|
||
|
|
- Trusted server keys
|
||
|
|
- Accept tokens
|
||
|
|
- Outbound message queue
|
||
|
|
- Inbound message storage
|
||
|
|
|
||
|
|
8. **main.rs** - CLI
|
||
|
|
- keygen: Generate new identity
|
||
|
|
- import: Import existing identity
|
||
|
|
- accounts: List accounts
|
||
|
|
- trust: Trust server key (TCP)
|
||
|
|
- register: Register username
|
||
|
|
- send: Send message
|
||
|
|
- fetch: Fetch messages
|
||
|
|
- delete: Delete messages
|
||
|
|
- resolve: Resolve username
|
||
|
|
- show: Display message info
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Phase 2: RNS Transport
|
||
|
|
|
||
|
|
Same approach as bunshin's RNS integration:
|
||
|
|
|
||
|
|
1. **FFI with microReticulum** (recommended for production)
|
||
|
|
- Use C++ microReticulum via FFI bindings
|
||
|
|
- Official implementation, full compliance
|
||
|
|
- Complex but most reliable
|
||
|
|
|
||
|
|
2. **Python IPC** (quick validation)
|
||
|
|
- Call `smolmail_rns.py` as subprocess
|
||
|
|
- Share data via files or pipes
|
||
|
|
- Fast to implement
|
||
|
|
|
||
|
|
3. **Native Rust** (future)
|
||
|
|
- Wait for mature Rust RNS crate
|
||
|
|
- Or implement minimal subset
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Key Differences from Server (bunshin)
|
||
|
|
|
||
|
|
| Aspect | bunshin (server) | fumi (client) |
|
||
|
|
|--------|------------------|---------------|
|
||
|
|
| Noise role | Responder (has static key) | Initiator (no static key) |
|
||
|
|
| Session | Accepts connections | Initiates connections |
|
||
|
|
| Trust | Server key pinned by client | Client pins server key |
|
||
|
|
| Auth | Verifies client signatures | Signs requests |
|
||
|
|
| Rate limiting | Enforces limits | Respects limits |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## CLI Commands
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Identity management
|
||
|
|
fumi keygen # Generate new identity
|
||
|
|
fumi keygen --name alice # Generate with name
|
||
|
|
fumi import --name alice -m ABCD... # Import master secret
|
||
|
|
fumi accounts # List accounts
|
||
|
|
|
||
|
|
# Server trust (TCP only)
|
||
|
|
fumi trust smol://alice@example.com ABCD... # Trust server key
|
||
|
|
|
||
|
|
# Account registration
|
||
|
|
fumi register smol://alice@example.com --username alice
|
||
|
|
fumi register smol://alice@example.com --username alice --invite TOK...
|
||
|
|
|
||
|
|
# Messaging
|
||
|
|
fumi send smol://bob@example.com --subject "Hello" --body "World"
|
||
|
|
fumi send smol://bob@example.com --file message.txt
|
||
|
|
fumi fetch smol://alice@example.com
|
||
|
|
fumi delete smol://alice@example.com --ids ABCD...
|
||
|
|
fumi resolve smol://alice@example.com --username bob
|
||
|
|
fumi show ABCD... # Show message details
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Project Structure
|
||
|
|
|
||
|
|
```
|
||
|
|
fumi/
|
||
|
|
├── Cargo.toml # Project configuration
|
||
|
|
├── Cargo.lock
|
||
|
|
├── src/
|
||
|
|
│ ├── main.rs # CLI entry point
|
||
|
|
│ ├── lib.rs # Library exports
|
||
|
|
│ ├── client.rs # Core client logic
|
||
|
|
│ ├── account.rs # Account/identity management
|
||
|
|
│ ├── address.rs # Address parsing
|
||
|
|
│ ├── store.rs # Local SQLite store
|
||
|
|
│ ├── crypto.rs # Cryptographic primitives
|
||
|
|
│ ├── transport.rs # Transport trait
|
||
|
|
│ ├── tcp_transport.rs # TCP transport implementation
|
||
|
|
│ └── error.rs # Error types
|
||
|
|
├── tests/
|
||
|
|
│ └── integration/ # Integration tests
|
||
|
|
├── flake.nix # Nix flake (optional)
|
||
|
|
└── README.md # This file
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Implementation Priority
|
||
|
|
|
||
|
|
### Phase 1: TCP Client (High Priority)
|
||
|
|
|
||
|
|
| # | Module | Description | Dependencies |
|
||
|
|
|---|--------|-------------|--------------|
|
||
|
|
| 1 | crypto.rs | Base32, HKDF, HMAC, identity derivation | sha2, hmac, data-encoding |
|
||
|
|
| 2 | address.rs | Address parsing and validation | regex, anyhow |
|
||
|
|
| 3 | account.rs | Account and identity management | ed25519-dalek, rand_core |
|
||
|
|
| 4 | transport.rs | Transport trait and types | None |
|
||
|
|
| 5 | tcp_transport.rs | TCP/Noise transport | snow, std::net |
|
||
|
|
| 6 | client.rs | Core operations (REGISTER, AUTH, etc.) | All above |
|
||
|
|
| 7 | store.rs | SQLite local storage | rusqlite |
|
||
|
|
| 8 | main.rs | CLI | clap, all above |
|
||
|
|
| 9 | Tests | Unit and integration tests | All above |
|
||
|
|
|
||
|
|
### Phase 2: RNS Support (Medium Priority)
|
||
|
|
|
||
|
|
| # | Module | Description | Dependencies |
|
||
|
|
|---|--------|-------------|--------------|
|
||
|
|
| 1 | rns/ffi.rs | FFI bindings to microReticulum | libffi |
|
||
|
|
| 2 | rns/transport.rs | RNS transport implementation | ffi.rs |
|
||
|
|
| 3 | rns/mod.rs | RNS module exports | transport.rs |
|
||
|
|
| 4 | Update transport.rs | Add RNS feature flag | None |
|
||
|
|
| 5 | Update client.rs | Handle RNS-specific logic | rns module |
|
||
|
|
| 6 | Update main.rs | Add RNS CLI commands | rns module |
|
||
|
|
| 7 | Tests | RNS integration tests | All above |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Testing Strategy
|
||
|
|
|
||
|
|
### Unit Tests
|
||
|
|
- Address parsing (TCP and RNS)
|
||
|
|
- Identity generation and rotation
|
||
|
|
- Message sealing and unsealing
|
||
|
|
- Envelope format validation
|
||
|
|
- Token generation and verification
|
||
|
|
|
||
|
|
### Integration Tests
|
||
|
|
- Register with bunshin server (TCP)
|
||
|
|
- Send and receive messages (TCP)
|
||
|
|
- Key rotation interoperability
|
||
|
|
- Accept token usage
|
||
|
|
- Error response handling
|
||
|
|
|
||
|
|
### Interoperability Tests
|
||
|
|
- Against `smolmail.py` reference client
|
||
|
|
- Against `smolmaild.py` reference server
|
||
|
|
- Against `smolmail_rns.py` reference client
|
||
|
|
- Against `smolmaild_rns.py` reference server
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Compatibility Matrix
|
||
|
|
|
||
|
|
| Client \ Server | bunshin (TCP) | bunshin (RNS) | smolmaild.py | smolmaild_rns.py |
|
||
|
|
|------------------|---------------|---------------|---------------|------------------|
|
||
|
|
| fumi (TCP) | ✅ Full | ❌ No | ✅ Full | ❌ No |
|
||
|
|
| fumi (RNS) | ❌ No | ✅ Full | ❌ No | ✅ Full |
|
||
|
|
|
||
|
|
Both transports share the same account and store, so a user can use both transports with the same identity.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Key Implementation Notes
|
||
|
|
|
||
|
|
### Message Sealing (SPEC.md S5)
|
||
|
|
|
||
|
|
1. Generate ephemeral X25519 keypair
|
||
|
|
2. Convert recipient Ed25519 public key to X25519 (per SPEC.md S2)
|
||
|
|
3. Perform X25519 key agreement
|
||
|
|
4. Derive two 32-byte keys using HKDF-SHA256:
|
||
|
|
- `k1 = HKDF(shared, "", "smolmail/1 msg", 64)[0:32]`
|
||
|
|
- `k2 = HKDF(shared, "", "smolmail/1 msg", 64)[32:64]`
|
||
|
|
5. Build payload with frontmatter (unencrypted): version, sender, timestamp
|
||
|
|
6. Build body with: version, timestamp, body_length, body (padded to 1024-byte boundary)
|
||
|
|
7. Encrypt body with ChaCha20-Poly1305 using k1
|
||
|
|
8. Build envelope: magic ("SMOL"), version, recipient, ephemeral_pub, payload
|
||
|
|
|
||
|
|
### Transport Differences
|
||
|
|
|
||
|
|
#### TCP (v1.1)
|
||
|
|
- Noise_NX handshake (client is initiator, server has static key)
|
||
|
|
- Frames: u32 length || u8 op || body (split across Noise messages)
|
||
|
|
- Binding values: Noise handshake hash and server static public key
|
||
|
|
- Server pinning: Client verifies server's Noise static key
|
||
|
|
|
||
|
|
#### RNS (v1.2)
|
||
|
|
- Reticulum link establishment
|
||
|
|
- Frames: u8 op || body (RNS delimits messages)
|
||
|
|
- Binding values: SHA-256("smolmail/1 bind" || dest_hash || link_id) and SHA-256("smolmail/1 bind" || dest_hash)
|
||
|
|
- Server pinning: Destination hash IS the server's identity (no separate pinning)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Next Steps
|
||
|
|
|
||
|
|
1. **Implement Phase 1 (TCP Client)**
|
||
|
|
- Start with crypto, address, and account modules
|
||
|
|
- Then transport and tcp_transport
|
||
|
|
- Then client core operations
|
||
|
|
- Finally store and CLI
|
||
|
|
- Test against bunshin server
|
||
|
|
|
||
|
|
2. **Implement Phase 2 (RNS Support)**
|
||
|
|
- Research microReticulum C API
|
||
|
|
- Create FFI bindings
|
||
|
|
- Implement RNS transport
|
||
|
|
- Add tests with reference implementations
|
||
|
|
|
||
|
|
3. **Polish and Package**
|
||
|
|
- Documentation
|
||
|
|
- Nix flake integration (optional)
|
||
|
|
- Performance optimization
|
||
|
|
- Error handling improvements
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## References
|
||
|
|
|
||
|
|
- [Smol Mail SPEC.md](https://code.randogoth.com/randogoth/smolmail/src/branch/main/SPEC.md)
|
||
|
|
- [Smol Mail RNS.md (v1.2)](https://code.randogoth.com/randogoth/smolmail/src/branch/main/RNS.md)
|
||
|
|
- [smolmail.py - Reference TCP client](https://code.randogoth.com/randogoth/smolmail/src/branch/main/smolmail.py)
|
||
|
|
- [smolmail_rns.py - Reference RNS client](https://code.randogoth.com/randogoth/smolmail/src/branch/main/smolmail_rns.py)
|
||
|
|
- [bunshin - Rust server implementation](../bunshin)
|
||
|
|
- [microReticulum - C++ RNS implementation](https://github.com/attermann/microReticulum)
|