Add comprehensive plan for Rust client implementation including: - TCP transport (v1.1) support - RNS transport (v1.2) support via FFI with microReticulum - Architecture with transport abstraction - Module structure and dependencies - CLI command design - Testing strategy - Implementation priority Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
16 KiB
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, 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
[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
-
crypto.rs - Cryptographic primitives
- Base32 encoding/decoding
- HKDF-SHA256
- HMAC-SHA256
- Identity derivation
- Accept token generation
- Message ID computation
-
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
- Parse TCP addresses:
-
account.rs - Account management
- Master secret storage
- Identity key derivation with rotation
- Current identity tracking
- Token generation for correspondents
-
transport.rs - Transport trait
- Define
Transporttrait with connect/request/close - Define
TransportBindValuesfor auth bindings - Define operation and status constants
- Define
-
tcp_transport.rs - TCP transport
- Noise_NX handshake
- Frame reading/writing
- Server key pinning (optional)
-
client.rs - Core client logic
- Session management
- REGISTER operation
- AUTH operation
- RESOLVE operation
- SEND operation (envelope creation)
- FETCH operation
- DELETE operation
- Message sealing/unsealing
-
store.rs - Local SQLite store
- Account storage
- Trusted server keys
- Accept tokens
- Outbound message queue
- Inbound message storage
-
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:
-
FFI with microReticulum (recommended for production)
- Use C++ microReticulum via FFI bindings
- Official implementation, full compliance
- Complex but most reliable
-
Python IPC (quick validation)
- Call
smolmail_rns.pyas subprocess - Share data via files or pipes
- Fast to implement
- Call
-
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
# 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.pyreference client - Against
smolmaild.pyreference server - Against
smolmail_rns.pyreference client - Against
smolmaild_rns.pyreference 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)
- Generate ephemeral X25519 keypair
- Convert recipient Ed25519 public key to X25519 (per SPEC.md S2)
- Perform X25519 key agreement
- 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]
- Build payload with frontmatter (unencrypted): version, sender, timestamp
- Build body with: version, timestamp, body_length, body (padded to 1024-byte boundary)
- Encrypt body with ChaCha20-Poly1305 using k1
- 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
-
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
-
Implement Phase 2 (RNS Support)
- Research microReticulum C API
- Create FFI bindings
- Implement RNS transport
- Add tests with reference implementations
-
Polish and Package
- Documentation
- Nix flake integration (optional)
- Performance optimization
- Error handling improvements