Reach is an end-to-end encrypted communication platform that enables collaborative groups to safely receive information and requests from anonymous individuals and securely respond to them. The system is specifically engineered to address the security challenges faced by organizations and collectives that need to protect both themselves and their anonymous contacts.
The project expands on established whistleblowing system principles to serve a broader purpose. Reach evolved from SecureDrop's end-to-end encryption research (to which it contributed valuable insights). It targets a wider audience than GlobaLeaks while implementing stronger security measures than Hush Line to handle more sophisticated threat models.
Warning
Reach is currently in pre-alpha stage. The code in this repository is not ready for deployment, and neither the underlying protocol nor the codebase has undergone independent security review. Current documentation focuses primarily on information for developers. To follow the project's progress, you can follow @reach@floss.social to find out about its progress.
What's what? Repository structure
Reach's architecture is designed to anticipate diverse usage scenarios, resulting in a multi-component structure even in its early development stage:
End-User Applications
-
reachable-app/: The Reachable Application is a Tauri, Gleam and Lustre based desktop app enabling reachable peers to manage and respond to messages. -
reaching-app/: The Reaching Application is a browser-based single-page application built with Gleam and Lustre, allows anonymous individuals to contact reachable peers. -
attestant/: The Reach Attestant command-line tool that handles administrative tasks including deployment preparation and peer onboarding/offboarding.
Service Components
-
node/: Implements Reachable Nodes - the central communication hubs that include Server Nodes (exposed via Tor Onion Services) and Peer Nodes (accessible only to onboarded reachable peers through authenticated Onion Services). -
secrets/: The Reachable Secrets service manages key generation, encryption key signing, and encryption/decryption of message vaults for reachable peers. -
reaching-app/src-wasm/: The Reaching Link is a Wasm library used by the Reaching Application to handle passphrase generation, key derivation, encryption/decryption and encoding/decoding.
Common Libraries
-
common/aliases/: Type aliases (and wrapper types) used across all Rust components. -
common/core/gleam/: Shared types and functions for Gleam-based frontends. -
common/core/rust/: Shared data structures and functionality used (primarily) by end-user applications and service components. -
common/ecdh-omr/: ECDH-based Oblivious Message Retrieval implementation - a core protocol component (reusable in other projects). -
common/encryption/: Encryption and decryption functionality used by service components and some end-user applications. -
common/passphrase/: Generates passphrases from word lists, and provides deterministic key generation infrastructure. -
common/rotating-bloom-filter/: Bloom filter variant with automatic rotation for recent membership testing. -
common/signatures/: Generic trait implementations for signing and verification. -
common/ui/: Shared Lustre UI components used by the Reachable and Reaching applications. -
common/visual-key-identity/: Emoji mapping system that replaces traditional alpha-numeric hashes with visual elements for more user-friendly key verification. -
common/websocket/: Generic implementations for strongly typed automatically decoding/encoding WebSocket clients and servers.
Development Support
-
common/harness/: Rust test harness for component testing. -
common/proc-macros/: Procedural macros that generate code for fundamental data structures. -
tests/: Integration tests for the Rust portion of the project. -
test-bin/: Command-line applications to aid testing and developing service components.
Development
This project uses a Nix flakes based development shell to help developers share a predictable environment with all the prerequisite tooling installed to get right to work. To use it, we recommend to install the (delicious) Lix implementation of the Nix package manager. Lix provides install and upgrade instructions for your respective configuration.
To enable flakes, either enable it via the Lix installer, or the following line to your nix.conf
(found in /etc/nix/ or ~/.config/nix/):
experimental-features = nix-command flakes
The Nix flake will set up all the tools Reach relies on (in no particular order): Rust, Gleam,
just, wasm-pack, REUSE, protoc, etc.
Note
For Nix sceptics and the impatient, we also provide a container based setup (compatible with Podman and Docker) that can be used by installing
justand running$ just containerized help.
Getting Started
Once you installed Nix, you can enter the development shell by running the following command in the root directory of the repository:
$ nix develop .
It's strongly encouraged for you to run just repo-config as a next step to set up and configure
Git hooks that are also managed by the repository itself.
If you already installed just through your regular package manager, you can also run just develop from anywhere in the repository, which will trigger the repo-config recipe before
dropping you into the development shell.
Running just help will give you an overview of which recipes exist and what they do:
$ just help
Welcome to Reach's development justfile š
Donāt forget to read CONTRIBUTING.md before you start āØ
Available recipes:
Available recipes:
audit # š§ Check if any of this project's dependencies have advisories on rustsec.org
build # š Build everything
build-attestant # šļø Build reach-attestant and reach-core with the attestant feature enabled
[ā¦]
Contributing
If you would like to contribute to Reach (ā¤ļø) please refer to the CONTRIBUTING.md document to
get familiar with the conventions that are expected by the project. Thank you for taking the time!
Credits
Reach was originally derived from and contributed to research into the Next Generation End-to-End Encrypted Design of SecureDrop by Giulio B. of Freedom of the Press Foundation, @smaury and Davide @TheZero of Shielder as well as the SecureDrop Team. Additional credits go to Olivia M. for pointing out and insisting on the inevitability of Asymmetric Forward/Backward Secrecy, and @redshiftzero for prior art, as well as countless of people who provided us with feedback. This list will expand.
Early contributors (ā¤ļø) to Reach include:
Reach was initiated by @eaon
Component Communication
Which component communicates with what?
flowchart TD
subgraph A[Attestant]
B[/<a href="https://codeberg.org/reachable-systems/reach/src/branch/main/attestant">Reach Attestant</a>/]
end
subgraph C[Local Peer]
E{{<a href="https://codeberg.org/reachable-systems/reach/src/branch/main/node#peer-node">Reachable Peer Node</a>}}
D[<a href="https://codeberg.org/reachable-systems/reach/src/branch/main/reachable-app">Reachable Application</a>]
F([<a href="https://codeberg.org/reachable-systems/reach/src/branch/main/secrets">Reachable Secrets</a>])
F -.->|encrypts/decrypts & signs/verifies messages for| D
E -.->|provides long-term storage for| D
end
subgraph G[Remote Peer]
H{{<a href="https://codeberg.org/reachable-systems/reach/src/branch/main/node#peer-node">Reachable Peer Node</a>}}
end
subgraph Reaching Party
I[<a href="https://codeberg.org/reachable-systems/reach/src/branch/main/reaching-app">Reaching Application</a>]
J([<a href="https://codeberg.org/reachable-systems/reach/src/branch/main/reaching-app/src-wasm">Reaching Link</a>])
J -.->|encrypts/decrypts & signs/verifies messages for| I
end
K{{<a href="https://codeberg.org/reachable-systems/reach/src/branch/main/node#server-node">Reachable Server Node</a>}}
K -->|hosts| I
A -->|signs verifying keys of| C
A -->|signs verifying keys of| G
A -->|provides prerequisites to| K
I ==>|collects messages from| K
E ==>|collects messages from| K
H ==>|collects messages from| K
B ==>|uploads signed verifying keys to| K
E <==>|opportunistically syncs with| H
All local connections use WebSocket based binary protocols via Unix domain sockets.
How does the Reachable Application communicate with its Secrets and Node services?
sequenceDiagram
participant rs as Reachable Secrets
participant rc as Reachable Application
participant rn as Reachable Node
participant rrn as Remote Reachable Node
rc->>rn: Init
note over rn,rrn: only valid for Server Nodes
rn->>rrn: Init
rrn->>rn: Reach
note over rrn,rn: from Server Node
rn->>rc: Reach
note over rc: caches ReachablePublicKeys for eventual replies
rc->>rs: (asks to decrypt) EnvelopeIdHints, (passes our own) ReachablePublicKeys
rs-->>rs: attempts to decrypt EnvelopeIdHints
note over rs: ReachablePublicKeys are ordered, so this is sped up<br>by only attempting with the delta of unused keys since<br>the most recent successful decryption and the<br>ReachablePublicKeys we received
rs->>rc: (successfully decrypted) HintedEnvelopeIds
note over rs: for every HintedEnvelopeId, saves a the relationship<br>between the asymmetric secret key that and the EnvelopeId
rc->>rn: EnvelopeId
note over rn,rrn: Peers are queried before Server
rn->>rrn: EnvelopeId
rrn->>rn: Envelope
note over rn: stores Envelope along with EnvelopeId
rn->>rc: Envelope
rc->>rs: Envelope
note over rs: queries saved asymmetric secret key that was able to<br>recover the HintedEnvelopeId for this Envelope<br>as it is the same key that decrypts it
rs-->>rs: decrypts MessageVaultLink::MessageVaultId
alt Reaching user Envelope contains MessageVaultLink::SealedLink
rs-->>rs: decrypts SealedMessageVaultId (with SharedSecretKeys)
end
note over rs: saves the relationship between MessageVaultId and the<br>recovered symmetric secret key
rs->>rc: MessageVaultId
rc->>rn: MessageVaultId
note over rn,rrn: Peers are queried before Server
rn->>rrn: MessageVaultId
rrn->>rn: MessageVault
note over rn: stores MessageVault along with MessageVaultId
rn->>rc: MessageVault
rc->>rs: MessageVault
note over rs: queries saved symmetric secret key that was able to<br>recover the MessageVaultLink linking to this MessageVault<br>as it is the same key that decrypts it
rs->>rc: Message
note over rc: displays contents
How does the Reachable Application reply to messages?
Additional context:
- Reachable Secrets generates, signs, and saves all secret keys as well as their relationship to EnvelopeIds and MessageVaultIds, also encrypts
- Reachable Application does not save messages by itself. It displays decrypted contents but does not save it at any point. It is merely the UI for interaction with other components.
- Reachable Node locally saves encrypted Envelopes (queryable by EnvelopeIds) and MessageVaults (queryable by MessageVaultId) to decrypt on demand and sharing with other remote peers.
sequenceDiagram
participant rs as Reachable Secrets
participant rc as Reachable Application
participant rn as Reachable Node
participant rrn as Remote Reachable Node
rc-->>rc: starts Message reply in existing thread
rc->>rn: StartReply with MessageVaultId (of initial contact)
rn->>rrn: Init
rrn->>rn: Reach
rn->>rc: ReplyInfo with MessageVault (of initial contact) + ReachablePublicKeys
rc->>rs: Message Content + MessageVault + ReachablePublicKeys
rs->>rc: MessageVaultSeed
rc->>rn: MessageVaultSeed
rn->>rrn: MessageVaultSeed
rrn->>rn: SealedMessageVaultId
rn->>rc: SealedMessageVaultId
rc->>rs: SealedMessageVaultId
rs->>rc: MessageVaultId + EnvelopeSeed
rc->>rn: MessageVaultId + EnvelopeSeed
note over rn: Uses MessageVaultSeed and MessageVaultId to save<br>MessageVault (to share with peers)
rn->>rrn: EnvelopeSeed
rrn->>rn: SealedEnvelopeId
rn->>rc: SealedEnvelopeId
rc->>rs: SealedEnvelopeId
rs->>rc: EnvelopeId
rc->>rn: EnvelopeId
note over rn: uses EnvelopeSeed and EnvelopeId to save<br>Envelope (to share with peers)
State and Threat-Adaptive Asymmetric Forward Secrecy
For contemporary end-to-end encrypted protocols, [forward secrecy] is essentially required to protect users and their communications over time. It limits the impact of key compromises through ephemeral keys. In asynchronous messaging systems, these ephemeral keys must be shared with conversation partners and remain available until decryption is needed1.
Implementing forward secrecy typically requires participants to track state. The widely-reviewed [Double Ratchet Algorithm] elegantly aligns multiple conversation partners' states, but necessarily relies on all of them maintaining this state information throughout their interaction.
But what if state itself was something one would want to avoid?
Threat Model Specialisation
Reach's design acknowledges a fundamental asymmetry in its users' threat profiles. Users initiating contact ("Reaching Users" or "Reaching Parties") typically face different security challenges than those receiving messages ("Reachable Peers").
For reaching users, localised threats predominate - abusive partners, employers, or local law enforcement gaining physical device access. Most of the time, these immediate physical risks outweigh the sophisticated state-level attacks that might target receiving organisations or collectives.
Communication patterns further exacerbates this asymmetry. Reaching users send and receive far fewer messages than reachable peers, making key reuse for messages sent to reachable peers a more significant security risk than the reverse scenario.
Many end-to-end encrypted systems that offer Forward Secrecy without requiring users to maintain device state typically store key material and state information on their servers. However, this approach allows servers to enumerate, identify, and distinguish between their users. Reach recognises that servers frequently operate in cloud environments where third-party access cannot be avoided, therefore, Reach intentionally avoids offloading state to servers.
An Approach with Trade-Offs
Reach's unusual threat model demands unconventional security trade-offs. The twin requirements of avoiding state tracking whilst preventing adversaries with server access from distinguishing between users and user types necessitates a private-information-retrieval based polling mechanism for message delivery. This contrasts with the push notification approach common in popular encrypted messaging platforms.
While this design choice limits Reach's deployment scale, it aligns perfectly with the intended use case: asynchronous communication by smaller groups that more closely resembles email rathern than instant messaging.
For Reachable Peers, the system delivers strong security guarantees. Both Forward and Backward Secrecy are provided through the distribution of new asymmetric encryption keys during each message polling cycle. This ensures that even if an adversary compromises the keys for an individual message, both previously obtained and future messages remain secure.
By contrast, the Reaching Party uses the same asymmetric encryption keys throughout a defined temporal window to receive replies, after which messages automatically expire. This approach represents a significant departure from systems employing protocols like the Double Ratchet Algorithm, but constitutes a carefully calculated compromise based on Reach's unique security priorities.
Asymmetric Security Properties by Design
In conclusion, Reach deliberately implements different security guarantees for its two user types, reflecting their distinct threat models, resulting in asymmetric Forward (and Backward) Secrecy properties:
Reachable Peers
- Benefit from full Forward and Backward Secrecy.
- Have unique encryption keys distributed for each polling cycle.
- Maintain strong protection against key compromise, as each compromise affects only a single message.
Reaching Users
-
Prioritise minimising device state and server-side identifiers.
-
Benefit from partial Forward and Backward Secrecy by using the same keys for limited (overlapping) time windows.
-
Accept the calculated risk of using consistent keys during these windows.
-
This may happen with significant delay. See also: Fallacies of distributed computing, point 2 [forward secrecy]: https://en.wikipedia.org/wiki/Forward_secrecy [Double Ratchet Algorithm]: https://en.wikipedia.org/wiki/Double_Ratchet_Algorithm ā©
Reachable Server Node Protocol Flow
The following sequence diagram does not encompass all the details of message composing but gives an accurate overview of what happens on the wire, and which participant has access to what information:
sequenceDiagram
participant ppnt as Participant
participant node as Server Node
alt Reaching Participant
ppnt-->>ppnt: generate/derive ReachingSecretKeys/ReachingPublicKeys
end
ppnt->>node: requests AttestantVerifyingKeys
ppnt->>node: requests Initialisation
node->>ppnt: responds with Attestant Verifying Keys
alt Reaching Participant
ppnt-->>ppnt: verifies Visual Key Identity for the AttestantVerifyingKeys
end
node->>ppnt: responds with<br>ReachableVerifyingKeys,<br>ephemeral ReachablePublicKeys,<br>and EnvelopeIdHints
note over node: There always have to be the same number of EnvelopeIdHints across all requests,<br>irrespective of how many Envelopes are actually stored on the server node
node-->>node: drops disclosed ephemeral ReachablePublicKeys from its database
ppnt-->>ppnt: verifies ReachablePublicKeys<br>attempts to decrypt EnvelopeIdHints
alt successfully decrypted a EnvelopeIdHint
ppnt-->>ppnt: recovers EnvelopeId and EnvelopeIdHint shared secret
ppnt->>node: requests Envelope using recovered EnvelopeId
node->>ppnt: responds with Envelope
ppnt-->>ppnt: decrypts Envelope<br>recovers MessageVault Credentials
alt Reaching Participant
ppnt-->>ppnt: recovers MessageVaultId
else Reachable Participant
ppnt-->>ppnt: recovers the MessageVaultId from SealedMessageVaultId<br>using the SharedSecretKeys
end
ppnt->>node: requests MessageVault with MessageVaultId
node->>ppnt: responds with MessageVault
ppnt-->>ppnt: decrypts MessageVault,<br>recovers Message
alt removes their EnvelopeIdHint to prevent continued access to this resource
ppnt->>node: requests removal of EnvelopeIdHint by EnvelopeId and a EnvelopeIdHint specific token
node-->>node: drops respective EnvelopeIdHint record
end
end
ppnt-->>ppnt: composes new Message<br>generates per-Envelope+MessageVault shared secret<br>uses it to encrypt the MessageVaultLink and MessageVault
ppnt->>node: uploads MessageVault
node-->>node: generates a MessageVaultId<br>encrypts it with ephemeral secret keys and SharedPublicKeys<br>creating a SealedMessageVaultId
node->>ppnt: responds with SealedMessageVaultId
alt Reaching Participant
ppnt-->>ppnt: embeds SealedMessageVaultId in the Envelope
else Reachable Party
ppnt-->>ppnt: recovers the MessageVaultId from SealedMessageVaultId<br>using the SharedSecretKeys<br>and embeds the MessageVaultId in the Envelope
end
ppnt->>node: uploads Envelope
node->>ppnt: responds with SealedEnvelopeId
alt Message is large enough to require multiple chunks
note over ppnt, node: Participant follows the flow described above to upload a MessageVault,<br>embeds its SealedEnvelopeId or EnvelopeId in the new Message
alt Reaching Participant
ppnt-->>ppnt: embeds the last SealedEnvelopeId in new Message
else Reachable Participant
ppnt-->>ppnt: recovers the EnvelopeId from the last SealedEnvelopeId<br>using the SharedSecretKeys, and embeds it in the new Message
end
ppnt->>node: follows the same MessageVault/Envelope upload flow from above
end
Reach WebSocket + ProtoBuf based protocol, version 0
- Initialisation messages
InitandInitAuthenticatedSessionboth communicate the desired protocol version for the respective sessionInitis a request for all the information necessary to receive and send messagesInitAuthenticatedSessionindicates desire to establish a session allowing for operations specific to reachable peers and attestants.
- All responses, including generic Ok/Error ones are represented by the
wire::ResponseenumOkcan be assumed to be the default response type when no other response type is specified belowUnsupportedis the response if the node does not support the requested protocol version during initialisationDecode Erroris the response if any supplied data could not be successfully decoded, including Protocol Buffer Message conversion errorsVerification Errormay be a response to requests with signatures of which the verification failedNot Foundmay be a reply to requests involving IDs that do not identify any information currently stored on the node. All IDed requests are marked as such in the table below
| request | user role | response | IDed | description |
|---|---|---|---|---|
wire::Init | reach::Reach | EnvelopeIdHint hint polls, pre-MessageVault upload | ||
wire::InitAuthenticatedSession | reach::AuthenticationChallenge | request an authenticated session | ||
envelope::EnvelopeId | envelope::Envelope | Yes | download Envelope | |
message_vault::MessageVaultId | message_vault::MessageVault | Yes | download MessageVault | |
message_vault::AddMessageVault | message_vault::SealedMessageVaultId | add/upload MessageVault | ||
envelope::EnvelopeSeed | envelope::SealedEnvelopeId | add/upload Envelope | ||
envelope::RemoveEnvelopeIdHint | Yes | remove EnvelopeIdHint by Envelope.id and removal token | ||
reach::AuthenticationAssurance | Reachable, Attestant | proof of signing ability for authenticated session | ||
wire::ListEnvelopeIds | Reachable | envelope::EnvelopeIds | lists all EnvelopeIds currently stored on this node | |
reach::ReachablePublicKeyRing | Reachable | add ReachablePublicKeys for a reachable peer | ||
reach::RemoveReachablePublicKeys | Reachable | Yes | remove ReachablePublicKeys for a reachable peer | |
hint::AddSharedPublicKeys | Attestant | TODO | ||
reach::AddReachableVerifyingKeys | Attestant | add/onboard new ReachableVerifyingKeys | ||
reach::RemoveReachableVerifyingKeys | Attestant | Yes | remove/offboard ReachableVerifyingKeys |
Protocol processing state change
State changes indicated by "ā". If no state changes are indicated, as well as in case of failures
(Unsupported, Verification Error etc.) state remains unchanged after processing the request.
- Init:
reach::Initā Readyreach::InitAuthenticatedSessionā Pending Authentication
- Ready:
envelope::EnvelopeIdmessage_vault::MessageVaultā Pending Envelope Uploadenvelope::RemoveEnvelopeIdHint
- Pending Envelope Upload
message::AddEnvelope
- Pending Authentication:
reach::AuthenticationAssuranceā Authenticated (Attestant) | Authenticated (Reachable) | Ready (Error)
- Authenticated (Attestant):
hint::AddSharedPublicKeysreach::AddVerifyingKeysreach::RemoveVerifyingKeys
- Authenticated (Reachable):
wire::Request::ListEnvelopeIdsreach::AddReachablePublicKeysreach::RemoveReachablePublicKeys
Threat Model Overview
Reach's threat model aims to be flexible. Because not every deploying group has the same needs, the goal is to provide them with information about the available options, rather than a one-size-fits-all solution. While Reach's aims are lofty, journalists reporting on matters of national security should look into other already established turn-key solutions engineered for their usecase.
TODO: actually go into detail
Expand description
§ECDH-OMR: ECDH based Oblivious Message Retrieval
ECDH-OMR aims to solve the following problem:
- Alice wants to leave a message for Bob on a server.
- The server is not supposed to know that the message is for Bob.
- Eve should neither be able to count the amount of real messages available for pick-up on the server, nor if new messages came in or old ones were removed.
- Bob should be able to pick their message up from the server without the server knowing whether this has happened or not.
ECDH-OMR solves this by using the commutative property of Diffie-Hellman key exchanges to implement a form of public key blinding, allowing third parties to send messages to a recipient this third party cannot identify at rest or when relaying it. It enables a form of Private Information Retrieval.
The protocol is designed as a minimal cryptographic primitive focused on a single task: oblivious message retrieval. By keeping the scope narrow and the mechanics simple, ECDH-OMR becomes both formally verifiable and practical to integrate into larger systems. Authentication, rate limiting, and additional encryption layers are intentionally left to other layers, allowing protocol designers to choose implementations that match their specific security requirements.
This library implements this scheme with x25519-dalek and (generically) with RustCryptoās elliptic-curves, as well as their AEADs (again, generically).
Warning This work has not yet been independently audited!
While the scheme at its core received preliminary reviews with positive results, more rigorous proofs should be published before considering its use. The implementation is a first stab at what a reasonable generic API for this could look like and has not received any reviews so far.
For experimental use and research only.
§Basics
Although not widespread in practice, ECDH supports multi-party shared secrets due to its commutative nature. This allows keys to be combined in different orders while still producing the same shared secret:
ECDH ( ServerSK, ECDH ( AliceSK, BobPK ) )ECDH ( BobSK, ECDH ( ServerSK, AlicePK ) )
Because both produce the same shared secret, the server can encrypt messages for Bob without ever seeing Bobās public key, while Bob can decrypt them without revealing his identity to the server.
§Single hint scheme flow
For the purposes of this breakdown, we show Alice sending an unencrypted message to Bob through a server. In practice, message encryption would be handled by higher protocol layers.
-
Alice blinds Bobās public key
- Obtains Bobās public key (out of band):
BobPK - Generates ephemeral secret key:
EphemeralSK - Derives blinding factor from ephemeral secret:
BlindedBobBF = G^EphemeralSK - Blinds Bobās public key, hiding Bobās identity from the server:
BlindedBobBK = ECDH ( EphemeralSK, BobPK )
Alice ā Server:
HintSeed { BlindedBobBK, BlindedBobBF, Message } - Obtains Bobās public key (out of band):
-
Server creates hint
- Generates ephemeral (per-request) secret key and nonce:
RequestSK,Nonce - Blinds Aliceās blinding factor:
BlindedBlindingFactorBK = ECDH ( RequestSK, BlindedBobBF ) - Derives shared secret with Bob from Aliceās blinded materials:
ZZ = ECDH ( RequestSK, BlindedBobBK ) - Encrypts Aliceās message:
MessageCiphertext = AEAD Enc ( ZZ, Nonce, Message )
Server ā Anyone:
Hint { BlindedBlindingFactorBK, Nonce, MessageCiphertext } - Generates ephemeral (per-request) secret key and nonce:
-
Bob decrypts the serverās hint, revealing Aliceās message
- Recovers shared secret with server:
ZZā = ECDH ( BobSK, BlindedBlindingFactorBK ) - Decrypts serverās message ciphertext, recovering Aliceās message:
Messageā = AEAD Dec ( ZZā, Nonce, MessageCiphertext )
- Recovers shared secret with server:
At no point does the server need to know Bobās public key to encrypt a message to Bob.
§Multi-hint scheme flow
In practice, servers donāt process individual hints but rather batches of hints to obscure communication patterns and protect against traffic analysis.
On the server:
- Fixed batch sizes: The server always creates the same number of hints (e.g., 5000), regardless of how many real messages it stores
- Decoy padding: If there arenāt enough real messages, the server generates fake hints that look identical to real ones
- Batch shuffling: All hints are randomly mixed together so observers canāt tell which ones might be important or worth brute forcing
- Ephemeral keying: Each batch generates new secret keys, ensuring identical messages create distinct ciphertexts across batches which remain decryptable by their intended recipients
This creates indistinguishable hint batches where observers cannot determine how many real messages exist, how many recipients there are, or when messages were added and removed.
On the client:
- Download everything: The recipient downloads the entire batch without revealing what theyāre looking for or who they are
- Trial decryption: The recipient attempts to decrypt every hint. Only hints encrypted with the recipientās blinded public key will decrypt successfully, while others fail silently
§What ECDH-OMR Doesnāt Provide
By design, ECDH-OMR focuses solely on oblivious retrieval and deliberately excludes:
- Authentication: Messages are not authenticated, use authenticated encryption or signatures for this
- Forward Secrecy: Not inherent to the protocol, but achieved through key management, ideally use one-time or short-lived keys for recipients
- Rate Limiting: No built-in DoS protection, implement at a higher layer
- Bidirectional Communication: One-way only, compose with other primitives for replies
§API Flow
For an annotated version of this that involves the use of ādecoy hintsā, please see
examples/decoyed.rs.
use ecdh_omr::{curves::X25519, Blind, Hinting, TakeTheHint};
use rand_core::{OsRng, RngCore};
use x25519_dalek::*;
type Hint = ecdh_omr::Hint<X25519, ocb3::Ocb3<aes::Aes128>, 32>;
fn main() {
// Bob
let bob_secret = StaticSecret::random_from_rng(&mut OsRng);
let bob_public = PublicKey::from(&bob_secret); // -> Alice
// Alice
</span><span class="kw">let </span>bob_blinded = bob_public.blind(<span class="kw-2">&mut </span>OsRng); <span class="comment">// -> Server
</span><span class="kw">let </span>alice_message = [<span class="number">42u8</span>; <span class="number">32</span>]; <span class="comment">// -> Server
// Server
</span><span class="kw">let </span><span class="kw-2">mut </span>salt = [<span class="number">0u8</span>; <span class="number">32</span>];
OsRng.fill_bytes(<span class="kw-2">&mut </span>salt); <span class="comment">// -> Bob
</span><span class="kw">let </span>hint = Hint::new(<span class="kw-2">&</span>bob_blinded, <span class="kw-2">&</span>alice_message, <span class="kw-2">&</span>salt, <span class="kw-2">&mut </span>OsRng).unwrap(); <span class="comment">// -> Bob
// Bob
</span><span class="kw">let </span>bob_recovered_message = bob_secret.take_the(<span class="kw-2">&</span>hint, <span class="kw-2">&</span>salt).unwrap(); <span class="comment">// ā
</span><span class="macro">assert_eq!</span>(alice_message, bob_recovered_message);
}
§Notes
§Terminology
ECDH-OMRās namesake is the 2022 paper Oblivious Message Retrieval (eprint) by Zeyu Liu and Eran Tromer, which describes a protocol using Fully Homomorphic Encryption to achieve similar properties where a server stays oblivious as to which messages a recipient decrypts.
A key difference in terminology between the two approaches though is that ECDH-OMRās uses the term hint where OMR would use clue. However, the authorās use of hint precedes their awareness of OMR, and they decided to stick with it: Clues are usually more concrete pieces of evidence that directly point towards a solution or answer, whereas hints are commonly more veiled indications that can guide someone towards the intended meaning without giving away the game. Clues you can collect and combine, while hints you have to āgetā by using pre-existing knowledge, or in a more adversarial setting you may also be asked to ātakeā them.
§Scale
Fundamentally, because it is a polling based scheme, rather than Fuzzy Message Detection (eprint) for example where the server does matching work, its scale is limited by bandwidth in addition to compute. The batch download approach provides stronger privacy guarantees than selective retrieval schemes, but at the cost of bandwidth efficiency. Consequently ECDH-OMRās target audience wants to build high-security, moderate-scale systems rather than mass-market applications.
§Post-Quantum Considerations
-
CSIDH/CTIDH technically supports this scheme, but arenāt researched well enough to be viable at this point. Due to the scale limitations of ECDH-OMR though, it may be conceivable that CTIDH-OMR or ECDH-CTIDH-OMR could be usable in certain circumstances, provided the implementation is fast enough to handle a reasonable amount of hints. An experimental non-constant-time proof-of-concept is available.
-
ML-KEM is not commutative, so this would require a way for a third party to change the ciphertext without changing the secret embedded within. Weāre not aware of whether this is possible or not.
§Acknowledgements
ECDH based Oblivious Message Retrieval was developed by @eaon as part of Reach, an end-to-end encrypted communication platform designed for collaborative groups who wish to let anonymous individuals contact them with information and/or requests. Reach and the research that led to this crate has been self-funded so far, please get in touch if you would like to change that š
The author also contributed the scheme to SecureDropās E2EE protocol research.
-
The author would like to thank Davide @TheZero for his early contribution to the aforementioned research, whereby an unusual use of multi-party Diffie-Hellman key exchanges were used to ephemerally āproveā secret key possession in a challenge/response protocol.
-
The author would also like to express their gratitude to Jacob Young for highlighting how the challenge/response protocol would allow servers to quickly correlate messages and infer identity properties of recipients, leading the author to take up this problem once again. And then also for taking even more time at Recurse Center to review the ECDH based Oblivious Message Retrieval scheme implemented in this crate. š
Modules§
- curves
- Shorthands for compatible elliptic curve implementations.
Structs§
- Blinded
Public Key - An ECDH public key that has been blinded, enabling a third party to send a message without knowing the cryptographic identity of the recipient.
- Hint
- Message encrypted by a third party, decryptable by an anonymous recipient that doesnāt know whether it is addressed to them or not.
- Hint
Seed - Pairing of message contents and the
BlindedPublicKeyof its recipient. - Hints
- Batch of
Hints that enforces inner vector size as well as shuffling, and also mitigates potential timing leaks at creation time.
Enums§
- Error
- Error type.
Traits§
- Blind
- Blind a public key.
- Blinded
- (De)serialization for
BlindedPublicKey. - Decoy
- Generate legitimate looking instances of data structures without user input.
- Hinting
- Semi generic implementations to create new
Hints - Take
TheHint - Decrypt
HintandHints. Also a silly pun.
Structs§
Constants§
Traits§
- Prost
Message - A Protocol Buffers message.
Type Aliases§
- Blinded
Public Key - Ed25519
Signature - Ed25519
Signing - Ed25519
Verifying - Envelope
IdHint - Envelope
IdHints - FnDsa
Domain Context - FnDsa
Signature - FnDsa
Signing Decoded - FnDsa
Signing Inner - FnDsa
Verifying - FnDsa
Verifying Decoded - MacShared
Secret - MlKem
- MlKem
Ciphertext - MlKem
Public - MlKem
Secret - OneSix
- Reach
Rng - SixFour
- Three
Two - TwoFour
- X25519
Public - X25519
Secret - XCha
ChaKey - Key type (256-bits/32-bytes).
Derive Macros§
Expand description
§Reach Encryption
This crate provides symmetric encryption using generic encrypt/decrypt traits for various data types
with XChaCha20-Poly1305 authenticated encryption (via the RustCrypto implementation).
For public key encryption, it supports multiple recipients using X-Wing Hybrid KEM
(eprint) construction that combines X25519 (via dalek) and ML-KEM (via
RustCrypto), implements SIGMA-I style authenticated encryption using
reach-signatures, and includes HMAC verification for verifying key authenticity.
§Core Traits
Encryptable: Types that can be encrypted with symmetric or public key encryptionDecryptable: Types that can be decrypted from ciphertext with nonce or key materialCiphertext: Types that contain encrypted dataNonce: Types that provide nonces for encryption/decryption operationsPublicKeyEncrypted: Types encrypted with the X-Wingy hybrid KEMParticipantSecretKeys: Provides access to EC and PQ secret keys for decryption
The crate is designed to work across different Reach components including Reaching Link, Reachable Secrets, and the Reach Attestant.
Traits§
- Authenticate
- Authenticates encrypted data using SIGMA-I-style verification.
- Authenticating
Wrapper From Parts - Construct type from a wrapped value and authentication components.
- Ciphertext
- Contains encrypted data.
- Decryptable
- Decryptable from data structures that include nonces.
- Decryptable
With Nonce - Decryptable when provided with an explicit nonce.
- Encryptable
- Encryptable using symmetric encryption.
- Hint
Taker - Secret keys that ātakeā (decrypt) hints using ECDH-OMR.
- Nonce
- Provides nonces for encryption/decryption operations.
- Participant
Secret Keys - Provides access to both elliptic curve and post-quantum secret keys.
- Public
KeyDecrypter - Decrypts public key encrypted data.
- Public
KeyEncryptable - Encryptable using SIGMA-I-style authenticated public key encryption.
- Public
KeyEncrypted - Encrypted with hybrid public key cryptography.
- Public
KeyEncrypted From Parts - Constructed type from public key encryption components.
- Verifiable
Mac - Generates and verifies HMAC tags for key authentication.
Functions§
- authenticated_
encrypt_ key - Encrypt a symmetric encryption key for multiple recipients using SIGMA-I-style authenticated encryption.
- binding_
digest - Cryptographic bind ephemeral material and participant keys.
- blind_
public_ key - Blind a participantās public key.
- build_
envelope_ seed - Build an [
EnvelopeSeed] containing all components needed for secure message delivery. - cipher_
and_ material_ for - Generate cipher and cryptographic material for public key encryption.
- cipher_
with_ secrets - Create a cipher instance using secret keys and encrypted material.
- open_
envelope - Open and authenticate an envelope.
Expand description
§Reach Signatures
This crate provides a dual-signature scheme combining classical Ed25519 elliptic curve signatures with post-quantum FN-DSA lattice-based signatures. Both signatures must be valid for authentication to succeed, providing security against both classical and quantum computer attacks.
It uses SHA3-512 for digest generation and provides context-aware signing using type names for domain separation.
§Core Traits
Digestible: Convert data structures to SHA3-512 digests for signingSignable: Types that can be signed and converted to their signed variantsSign: Signing key operations with both EC and PQ algorithmsVerifier: Provides verifying keys for operations with verifiable data structuresVerifiable: Types that carry signatures and can be verified
The crate is designed to work across different Reach components including Reaching Link, Reachable Secrets, and the Reach Attestant.
Constants§
- FN_
DSA_ SIGNATURE_ EMPTY - Zero filled array with the size of an FN-DSA signature.
- FN_
DSA_ SIGNING_ EMPTY - Zero filled array with the size of an FN-DSA signing key.
Traits§
- Digestible
- Convert data structures to SHA3-512 digests.
- Sign
- Signing key operations.
- Signable
- Sign and convert to the respective signed variant.
- Signatures
- Carries signatures.
- Verifiable
- Verifiable if
Digestibleand carryingSignatures. - Verifier
- Provides verifying keys for signature verification operations.
- Verifies
- Indicating which types can be verified by a given verifier.
- Verifying
Keys - Links signing keys to their corresponding verifying keys.
Functions§
- verify_
digest - Verify both Ed25519 and FN-DSA signatures with a digest.
Modules§
Structs§
- Encoding
Responder - Remote
Server Context Extensions - Responder
- Server
Context - Server
Options - WebSocket
Channel - WebSocket
Client - WebSocket
Client Options - WebSocket
Item - WebSocket
Item Receiver - WebSocket
Server - WebSocket
Sink Item - WebSocket
Upgrade
Enums§
- Handle
Request Error - Handle
Response Error - Next
- Respond
ToItem Error - WebSocket
Client Error - WebSocket
Error - WebSocket
Stream Error
Traits§
- Contains
Responders - Into
WebSocket Context - Request
Delegator - Request
Handler - Responders
- WebSocket
Client Extension - WebSocket
Context - WebSocket
Error Responder
Functions§
Type Aliases§
Expand description
§Rotating Bloom Filter
A probabilistic data structure that maintains recent items using a dual-buffer rotation mechanism.
RotatingBloomFilter provides membership testing for recently inserted items, with older items
automatically rotating out. Items are guaranteed to be retained for at least a minimum retention
period, typically remaining for up to twice that duration.
§Features
- Time-limited tracking: Only maintains membership information for recently inserted items
- Type flexible: Works with any hashable type
§How it works
The filter maintains two internal Bloom filters:
- Current buffer: Holds items from previous rotations plus ongoing insertions
- Next buffer: Accumulates new insertions until rotation
When the minimum retention threshold is reached, the next buffer becomes the new current buffer, and a new next buffer is created.
§Example
use rotating_bloom_filter::RotatingBloomFilter;
use rand_core::OsRng;
let mut filter = RotatingBloomFilter::new(0.01, 1000, &mut OsRng);
filter.insert("hello");
assert!(filter.contains("hello"));
// "hello" will be retained for at least 1000 insertions, but sticks around to 2000 insertions if
// the filter continues to be used.
Structs§
- Rotating
Bloom Filter - A probabilistic data structure that rotates out old items to maintain recent membership.