fumi/README.md

108 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 文 fumi
[![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) [![AI-DECLARATION: auto](https://img.shields.io/badge/%E4%B7%BC%20AI--DECLARATION-auto-ede9fe?labelColor=ede9fe)](https://ai-declaration.md)
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.
## Status
Both carriers are implemented: the TCP carrier (SPEC.md 1.1) and the Reticulum carrier of RNS.md (1.2), the latter behind the `rns` cargo feature (`nix build .#rns`). `fumi` interoperates in both directions with the reference stack — `smolmail.py`/`smolmaild.py` over TCP and `smolmail_rns.py`/`smolmaild_rns.py` over Reticulum — and with `bunshin` over both carriers, including the cross-transport case upstream section 15 promises: send over TCP, fetch over the mesh, one mailbox.
## Addressing
```
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
One master per store, selected by the global `--key` and `--db` flags; a second identity is a second pair of files.
```
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]
# RNS carrier (built with --features rns / nix build .#rns); addresses select
# it by scheme, so these flags apply only to smol+rns:// dials:
fumi --rns-udp <host:port> --rns-udp-forward <host:port> [--rns-storage DIR] <command>
```
On the mesh the destination hash is the pin (RNS.md 13.4): there is no `trust`, no static key, and a client creates no Reticulum identity (13.8). The `--rns-udp-forward` target is effectively required for a client, which speaks first; it should point at an `rnsd` or at the server's UDP interface. Requests carry an explicit 30-second timeout (13.5), links are torn down when a session ends (13.10), and a resend is safe because ids are derived.
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.
## Build
```
nix build # or: nix develop -c cargo build
nix develop -c cargo test --workspace
```
The crate is a workspace: `core/` is the `fumi-core` library — everything but the command line — and `cli/` is the `fumi` binary. GUI hosts depend on `fumi-core` alone; it pulls no argument parsing, no logging globals, and no keyfile conventions. `bundled-sqlite` (default) vendors libsqlite3; embedders that link their own SQLite, as Android's NDK does, build with `default-features = false`.
`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.
## Embedding
`fumi-core` is the protocol core of a GUI client, not just this CLI's engine. The contract an embedder can rely on:
- **Secrets stay bytes in the API, files stay in the CLI.** `Account::new(master, index)` takes the master as bytes and `Store::open(path)` takes only mail and state; `identity.key` next to `fumi.db` is a CLI convention. A GUI keeps the master wherever its platform wants it (Android Keystore, OS keychain) and hands it over per operation.
- **One shareable store handle.** `Store` is `Send + Sync`: every operation locks an inner mutex, and the database runs in WAL mode, so a host holds one `Store` behind an executor and reads it while a fetch writes. Each envelope is committed as it is verified — a concurrent reader listing the inbox sees fetch progress without any callback.
- **Decisions, not prose.** `error::Error` is an enum: `NotPinned`, `PinMismatch`, `KeyChanged { address, known, offered }`, `NotRegistered`, `AuthFailed`, `RateLimited`, `QuotaExceeded`, `SchemaVersion`, ... — the distinctions a UI routes on. `Display` still produces the message the CLI prints.
- **Long operations can stop.** `fetch_with` takes a `FetchOptions` with a cancellation token checked between pages; the cursor is saved per page, so a cancelled fetch resumes rather than repeats.
- **Composable flows.** `restore` remains the one-call convenience, but its pieces are public: `connect` + `resolve`, `account::find_rotation_index`, then the `Store` setters — a wizard can hold the master and the address before it has connectivity, and show each step. Trust outcomes arrive as values: `TrustChange` from `trust_key` and `send`, `Session::unpinned_static` for the sec 4 warning.
- **A versioned store.** The schema carries a `user_version`; a store written by a newer build is refused (`Error::SchemaVersion`) rather than misread, and within a version the schema is stable. The host, not the library, decides when to upgrade the binary.
- **Binding from other languages.** The API is `Send`-friendly and free of CLI globals, so plain Rust bindings (flutter_rust_bridge, uniffi) work against it. `core/vectors.json` holds the reference vectors the unit tests pin — derivations, seals, a full envelope — so a port or FFI binding can verify itself end to end without the reference stack.
The intended call graph: `Store::open` once, `Account::new(master, rotations)` per session, then `client::*` operations taking `&Store` and `&Account`. The store outlives accounts; nothing in the library caches between calls.
## Modules
| File | Contents |
|---|---|
| `core/src/crypto.rs` | base32, HKDF, HMAC, SHA-256, the Ed25519→X25519 map with SPEC.md §2's checks |
| `core/src/address.rs` | parsing for all four address forms, username validation, fingerprints |
| `core/src/account.rs` | master, `seed_n` derivation, accept-key and tokens, rotation certificates, `find_rotation_index` |
| `core/src/message.rs` | envelope seal/open (§5.1–§5.3), message id (§5.4), frontmatter (§5.5) |
| `core/src/transport.rs` | `Transport` trait, bind values, operation and status constants |
| `core/src/tcp.rs` | Noise_NX initiator, `len u32 \|\| op u8 \|\| body` framing, static-key pinning |
| `core/src/rns/` | the Reticulum carrier behind the `rns` feature: FFI over `core/shim/`, path discovery, one link per session |
| `core/src/client.rs` | the six operations, AUTH, chain walking, token sync, fetch pipeline with cancellation |
| `core/src/store.rs` | SQLite: state, contacts, accepted, tokens, seen ids, inbox, sent; `Send + Sync`, WAL, versioned schema |
| `core/src/error.rs` | the `Error` enum embedders match on, the status-code mapping |
| `core/vectors.json` | the committed reference vectors, the tests' source of truth |
| `cli/src/main.rs` | the command line: keyfile and `--db` conventions, hints, output |
Unit tests pin every derivation against `vectors.json`, whose values were generated from `smolmail.py`, including a full envelope reproduced byte for byte.
## References
- [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