feat: implement Phase 1 TCP client
This commit is contained in:
parent
1690aed3dc
commit
37df976fde
19 changed files with 5631 additions and 377 deletions
435
README.md
435
README.md
|
|
@ -1,399 +1,80 @@
|
|||
# Fumi - Smol Mail Client (Rust)
|
||||
# fumi
|
||||
|
||||
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.
|
||||
A Rust client for [Smol Mail](https://code.randogoth.com/randogoth/smolmail): a minimalist, end-to-end encrypted mail protocol over a Noise-secured TCP connection. `fumi` derives an identity from one 32-byte master secret, seals and opens mail, and talks to a mailbox; it is the counterpart to [bunshin](https://code.randogoth.com/randogoth/bunshin), the server.
|
||||
|
||||
## Overview
|
||||
## Status
|
||||
|
||||
Smol Mail is a minimalist, end-to-end encrypted mail protocol. This client implementation supports:
|
||||
Phase 1, the TCP carrier (SPEC.md 1.1), is implemented and interoperates in both directions with the reference client `smolmail.py` and the reference server `smolmaild.py`, and with `bunshin`. The Reticulum carrier of RNS.md (1.2) is not built yet: `smol+rns://` addresses parse and fail with a message naming the `rns` feature, and the flake does not yet vendor microReticulum.
|
||||
|
||||
- **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
|
||||
## Addressing
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 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 │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
alice@example.org[:1961] short form, resolved via the server
|
||||
smol://alice@example.org/mfrggzdfzt... self-certifying, 52-char base32 identity
|
||||
smol+rns://alice@8f2c1d0a7b6e5f4c3d2a1b0e9f8c7d6a short form over Reticulum (rns feature)
|
||||
smol+rns://alice@8f2c1d0a7b6e5f4c3d2a1b0e9f8c7d6a/mfrggzdfzt... self-certifying over Reticulum
|
||||
```
|
||||
|
||||
---
|
||||
## Usage
|
||||
|
||||
## 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
|
||||
One master per store, selected by the global `--key` and `--db` flags; a second identity is a second pair of files.
|
||||
|
||||
```
|
||||
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
|
||||
fumi [--key identity.key] [--db fumi.db] [--timeout 30] <command>
|
||||
|
||||
# identity
|
||||
fumi keygen [--force] create a master secret
|
||||
fumi whoami address, public key, fingerprint, rotation index
|
||||
fumi restore <address> recover local state from the master alone
|
||||
fumi rotate advance the rotation index, sign and push the certificate
|
||||
|
||||
# servers and contacts
|
||||
fumi trust <host> <key-b32> [--force] pin a server's Noise static key
|
||||
fumi register <address> [--invite TOKEN] bind this identity to a username
|
||||
fumi resolve <address> look up a contact's key, walk the chain, pin it
|
||||
fumi import <smol-uri> add a contact from a self-certifying address
|
||||
fumi contacts known keys and how each was learned
|
||||
|
||||
# accept tokens
|
||||
fumi accept <address> issue a token, admit to the main tier, sync the set
|
||||
fumi block <address> withdraw it, sync the set
|
||||
|
||||
# mail
|
||||
fumi send <address> [--subject S] [--body TEXT | --file F | -] [--header K:V] [--reply-to ID] [--anonymous] [--no-pad]
|
||||
fumi fetch [--keep] [--reset] retrieve, verify, store, acknowledge
|
||||
fumi delete <id>... remove from the server explicitly
|
||||
fumi list [--sent] [--requests]
|
||||
fumi read <id> [--sent]
|
||||
```
|
||||
|
||||
---
|
||||
Mail is stored sealed and opened on demand; there is no plaintext at rest. A first contact is learned by trust on first use and marked unverified when the server's key was not pinned; a key change is accepted only when a signed rotation chain leads from the key held to the one offered, and is surfaced rather than applied silently.
|
||||
|
||||
## Implementation Priority
|
||||
## Build
|
||||
|
||||
### Phase 1: TCP Client (High Priority)
|
||||
```
|
||||
nix build # or: nix develop -c cargo build
|
||||
nix develop -c cargo test
|
||||
```
|
||||
|
||||
| # | 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 |
|
||||
`bunshin` and the reference clients make a complete test rig: register against a local server, exchange mail in both directions, then rotate and fetch the mail that the superseded key still receives.
|
||||
|
||||
### Phase 2: RNS Support (Medium Priority)
|
||||
## Modules
|
||||
|
||||
| # | 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 |
|
||||
| File | Contents |
|
||||
|---|---|
|
||||
| `src/crypto.rs` | base32, HKDF, HMAC, SHA-256, the Ed25519→X25519 map with SPEC.md §2's checks |
|
||||
| `src/address.rs` | parsing for all four address forms, username validation, fingerprints |
|
||||
| `src/account.rs` | master, `seed_n` derivation, accept-key and tokens, rotation certificates |
|
||||
| `src/message.rs` | envelope seal/open (§5.1–§5.3), message id (§5.4), frontmatter (§5.5) |
|
||||
| `src/transport.rs` | `Transport` trait, bind values, operation and status constants |
|
||||
| `src/tcp.rs` | Noise_NX initiator, `len u32 \|\| op u8 \|\| body` framing, static-key pinning |
|
||||
| `src/client.rs` | the six operations, AUTH, chain walking, token sync, fetch pipeline |
|
||||
| `src/store.rs` | SQLite: state, contacts, accepted, tokens, seen ids, inbox, sent |
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
---
|
||||
Unit tests pin every derivation against vectors generated from `smolmail.py`, including a full envelope reproduced byte for byte.
|
||||
|
||||
## 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)
|
||||
- [SPEC.md](https://code.randogoth.com/randogoth/smolmail/src/branch/main/SPEC.md) — protocol 1.1
|
||||
- [RNS.md](https://code.randogoth.com/randogoth/smolmail/src/branch/main/RNS.md) — the 1.2 Reticulum addendum
|
||||
- [bunshin](https://code.randogoth.com/randogoth/bunshin) — the server
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue