Skip to main content

Crate efema

Crate efema 

Source
Expand description

The efema client: sync a local-first app’s changes between devices through a relay that cannot read them.

An app pushes its changes into a stream as opaque items; every other device pulls what came after its cursor, in the same order for everyone. What an item means and how two changes are merged stay with the app: this crate moves bytes, sealed.

Every entry is sealed on the device. A Client cannot be made without a Secret - the stream’s passphrase or its key - and has no method that sends an item as it is. The relay stores ciphertext, and so does every backup of it.

§The pieces

§Keys

The first device to open a stream creates its key - 32 random bytes - and writes it to the stream locked under the passphrase (Argon2id, then XChaCha20-Poly1305: lacodda_seal). Every other device finds that lock, unlocks the key with the same passphrase and keeps the lock in its state, so it opens without the network after that. The key itself is never written anywhere unless the app asks for it with Client::key.

Changing the passphrase locks the same key again and writes the new lock to the stream; nothing is sealed again, and every device moves to the new lock as it reads it.

§Compression

Items are compressed with zstd before they are sealed, each one only if it gets shorter - text usually to a third. Compression::Off turns it off for an app that mixes secrets with text someone else chose in one item.

§Delivery

At least once. Client::pull reads from the device’s acknowledged cursor and Client::ack moves it, once the app has applied what it pulled - so a crash in between hands the same entries over again, and an app applies an entry twice without harm.

§Epochs

A client writes in its app’s epoch and reads up to it. When a newer version of the app moves a stream to a newer epoch, an older one stops in front of the first newer entry (Error::NewerEpoch) and can no longer write (Error::EpochBehind) - instead of misreading the newer format or mixing the older one in.

Modules§

chain
The wire format and the hash chain, for writing a Transport. The hash chain that links every entry of a stream to the one before it.
wire
The wire format and the hash chain, for writing a Transport. The bodies of requests and responses, and their CBOR encoding.

Structs§

Client
One device’s view of one stream: it seals what it pushes, opens what it pulls, and remembers how far it has read.
Cursor
A reader’s place in a stream: which stream, how far, and what the history up to there was.
DeviceId
Which device this is: sixteen random bytes drawn the first time its state is opened.
Epoch
The version of the format an app writes into a stream.
InvalidUrl
Why a text is not the address of a relay.
Key
A symmetric key: 32 bytes, wiped from memory when dropped.
KeyId
The identity of a key: eight bytes of a hash over it.
KeyReport
The stream’s key, as far as the device can tell without the passphrase.
Limits
What one request may carry and one page may hold.
LockedKey
A key locked under a passphrase.
Pulled
What one Client::pull read.
Pushed
Where a push landed.
Received
An entry as the app receives it.
Relay
An efema relay, reached over HTTP or HTTPS.
RelayReport
The stream as the relay has it.
Report
A device’s sync, as Client::doctor finds it.
State
A device’s sync state: one file, opened by one process at a time.
StreamId
Which incarnation of a stream name this is.
StreamName
The name of a stream: what goes into the URL, and what a person reads in the relay’s listing.

Enums§

Compression
Whether a client compresses items before it seals them.
Error
Why a client operation failed.
Secret
What a client opens a stream with. Required: there is no client without one, and so no way to send an entry unsealed.
TransportError
Why a transport could not do what it was asked.

Constants§

ENTRY_OVERHEAD
How many bytes sealing adds to an item it does not compress: the sealed blob’s own and the entry’s header inside it.

Traits§

Transport
A way to reach streams.