Reach

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

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 just and 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:

  1. Reachable Secrets generates, signs, and saves all secret keys as well as their relationship to EnvelopeIds and MessageVaultIds, also encrypts
  2. 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.
  3. 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.


  1. 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 Init and InitAuthenticatedSession both communicate the desired protocol version for the respective session
    • Init is a request for all the information necessary to receive and send messages
    • InitAuthenticatedSession indicates 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::Response enum
    • Ok can be assumed to be the default response type when no other response type is specified below
    • Unsupported is the response if the node does not support the requested protocol version during initialisation
    • Decode Error is the response if any supplied data could not be successfully decoded, including Protocol Buffer Message conversion errors
    • Verification Error may be a response to requests with signatures of which the verification failed
    • Not Found may 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
requestuser roleresponseIDeddescription
wire::Initreach::ReachEnvelopeIdHint hint polls, pre-MessageVault upload
wire::InitAuthenticatedSessionreach::AuthenticationChallengerequest an authenticated session
envelope::EnvelopeIdenvelope::EnvelopeYesdownload Envelope
message_vault::MessageVaultIdmessage_vault::MessageVaultYesdownload MessageVault
message_vault::AddMessageVaultmessage_vault::SealedMessageVaultIdadd/upload MessageVault
envelope::EnvelopeSeedenvelope::SealedEnvelopeIdadd/upload Envelope
envelope::RemoveEnvelopeIdHintYesremove EnvelopeIdHint by Envelope.id and removal token
reach::AuthenticationAssuranceReachable, Attestantproof of signing ability for authenticated session
wire::ListEnvelopeIdsReachableenvelope::EnvelopeIdslists all EnvelopeIds currently stored on this node
reach::ReachablePublicKeyRingReachableadd ReachablePublicKeys for a reachable peer
reach::RemoveReachablePublicKeysReachableYesremove ReachablePublicKeys for a reachable peer
hint::AddSharedPublicKeysAttestantTODO
reach::AddReachableVerifyingKeysAttestantadd/onboard new ReachableVerifyingKeys
reach::RemoveReachableVerifyingKeysAttestantYesremove/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 → Ready
    • reach::InitAuthenticatedSession → Pending Authentication
  • Ready:
    • envelope::EnvelopeId
    • message_vault::MessageVault → Pending Envelope Upload
    • envelope::RemoveEnvelopeIdHint
  • Pending Envelope Upload
    • message::AddEnvelope
  • Pending Authentication:
    • reach::AuthenticationAssurance → Authenticated (Attestant) | Authenticated (Reachable) | Ready (Error)
  • Authenticated (Attestant):
    • hint::AddSharedPublicKeys
    • reach::AddVerifyingKeys
    • reach::RemoveVerifyingKeys
  • Authenticated (Reachable):
    • wire::Request::ListEnvelopeIds
    • reach::AddReachablePublicKeys
    • reach::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
ecdh_omr - Rust
Expand description

§ECDH-OMR: ECDH based Oblivious Message Retrieval

Latest release Docs License

ECDH-OMR aims to solve the following problem:

  1. Alice wants to leave a message for Bob on a server.
  2. The server is not supposed to know that the message is for Bob.
  3. 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.
  4. 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.

  1. 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 }

  2. 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 }

  3. 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 )

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:

  1. Fixed batch sizes: The server always creates the same number of hints (e.g., 5000), regardless of how many real messages it stores
  2. Decoy padding: If there aren’t enough real messages, the server generates fake hints that look identical to real ones
  3. Batch shuffling: All hints are randomly mixed together so observers can’t tell which ones might be important or worth brute forcing
  4. 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:

  1. Download everything: The recipient downloads the entire batch without revealing what they’re looking for or who they are
  2. 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">&amp;mut </span>OsRng); <span class="comment">// -&gt; Server
</span><span class="kw">let </span>alice_message = [<span class="number">42u8</span>; <span class="number">32</span>]; <span class="comment">// -&gt; 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">&amp;mut </span>salt); <span class="comment">// -&gt; Bob
</span><span class="kw">let </span>hint = Hint::new(<span class="kw-2">&amp;</span>bob_blinded, <span class="kw-2">&amp;</span>alice_message, <span class="kw-2">&amp;</span>salt, <span class="kw-2">&amp;mut </span>OsRng).unwrap(); <span class="comment">// -&gt; Bob

// Bob
</span><span class="kw">let </span>bob_recovered_message = bob_secret.take_the(<span class="kw-2">&amp;</span>hint, <span class="kw-2">&amp;</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§

BlindedPublicKey
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.
HintSeed
Pairing of message contents and the BlindedPublicKey of 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
TakeTheHint
Decrypt Hint and Hints. Also a silly pun.
reach_aliases - Rust

Structs§

FnDsaSigning

Constants§

FN_DSA_DEGREES
FN_DSA_HASH_ID
FN_DSA_VERIFYING_EMPTY

Traits§

ProstMessage
A Protocol Buffers message.

Type Aliases§

BlindedPublicKey
Ed25519Signature
Ed25519Signing
Ed25519Verifying
EnvelopeIdHint
EnvelopeIdHints
FnDsaDomainContext
FnDsaSignature
FnDsaSigningDecoded
FnDsaSigningInner
FnDsaVerifying
FnDsaVerifyingDecoded
MacSharedSecret
MlKem
MlKemCiphertext
MlKemPublic
MlKemSecret
OneSix
ReachRng
SixFour
ThreeTwo
TwoFour
X25519Public
X25519Secret
XChaChaKey
Key type (256-bits/32-bytes).

Derive Macros§

ProstMessage
reach_attestant - Rust
reach_core - Rust
reach_encryption - Rust
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 encryption
  • Decryptable: Types that can be decrypted from ciphertext with nonce or key material
  • Ciphertext: Types that contain encrypted data
  • Nonce: Types that provide nonces for encryption/decryption operations
  • PublicKeyEncrypted: Types encrypted with the X-Wingy hybrid KEM
  • ParticipantSecretKeys: 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.
AuthenticatingWrapperFromParts
Construct type from a wrapped value and authentication components.
Ciphertext
Contains encrypted data.
Decryptable
Decryptable from data structures that include nonces.
DecryptableWithNonce
Decryptable when provided with an explicit nonce.
Encryptable
Encryptable using symmetric encryption.
HintTaker
Secret keys that ā€œtakeā€ (decrypt) hints using ECDH-OMR.
Nonce
Provides nonces for encryption/decryption operations.
ParticipantSecretKeys
Provides access to both elliptic curve and post-quantum secret keys.
PublicKeyDecrypter
Decrypts public key encrypted data.
PublicKeyEncryptable
Encryptable using SIGMA-I-style authenticated public key encryption.
PublicKeyEncrypted
Encrypted with hybrid public key cryptography.
PublicKeyEncryptedFromParts
Constructed type from public key encryption components.
VerifiableMac
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.
reach_harness - Rust
reach_passphrase - Rust
reach_proc_macros - Rust
reach_signatures - Rust
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 signing
  • Signable: Types that can be signed and converted to their signed variants
  • Sign: Signing key operations with both EC and PQ algorithms
  • Verifier: Provides verifying keys for operations with verifiable data structures
  • Verifiable: 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 Digestible and carrying Signatures.
Verifier
Provides verifying keys for signature verification operations.
Verifies
Indicating which types can be verified by a given verifier.
VerifyingKeys
Links signing keys to their corresponding verifying keys.

Functions§

verify_digest
Verify both Ed25519 and FN-DSA signatures with a digest.
reach_visual_key_identity - Rust
reach_websocket - Rust

Modules§

macros

Structs§

EncodingResponder
RemoteServerContextExtensions
Responder
ServerContext
ServerOptions
WebSocketChannel
WebSocketClient
WebSocketClientOptions
WebSocketItem
WebSocketItemReceiver
WebSocketServer
WebSocketSinkItem
WebSocketUpgrade

Enums§

HandleRequestError
HandleResponseError
Next
RespondToItemError
WebSocketClientError
WebSocketError
WebSocketStreamError

Traits§

ContainsResponders
IntoWebSocketContext
RequestDelegator
RequestHandler
Responders
WebSocketClientExtension
WebSocketContext
WebSocketErrorResponder

Functions§

on_upgrade
respond_to_item
start
websocketstream_from_raw_socket
websocketstream_from_request

Type Aliases§

LocalServerContext
LocalServerOptions
LocalWebSocketChannel
NextOutput
RawWebSocketItem
RemoteServerContext
RemoteServerOptions
RemoteWebSocketChannel
TaggedWebSocketItemReceiver
WebSocketLocalClientOptions
WebSocketRemoteClientOptions
WebSocketStreamResult
reachable_node - Rust
reachable_secrets - Rust
reaching_link - Rust
rotating_bloom_filter - Rust
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:

  1. Current buffer: Holds items from previous rotations plus ongoing insertions
  2. 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§

RotatingBloomFilter
A probabilistic data structure that rotates out old items to maintain recent membership.
reach_core Ā· v0.0.1