feat: implement Phase 1 TCP client

This commit is contained in:
randogoth 2026-09-28 16:03:53 +03:00
parent 1690aed3dc
commit 37df976fde
19 changed files with 5631 additions and 377 deletions

435
README.md
View file

@ -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