89 lines
5.6 KiB
Markdown
89 lines
5.6 KiB
Markdown
# 文 fumi
|
||
|
||
[](LICENSE) [](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
|
||
```
|
||
|
||
`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.
|
||
|
||
## Modules
|
||
|
||
| 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/rns/` | the Reticulum carrier behind the `rns` feature: FFI over `shim/`, path discovery, one link per session |
|
||
| `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 |
|
||
|
||
Unit tests pin every derivation against vectors 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
|