End-to-End Message Encryption in River

All messages in the context of the communication modalities that River supports are encrypted. Nodes are only privy to ciphertext and clients manage their device encryption keys.

Two related cryptographic ratcheting algorithms are used by River to encrypt messages. River employs an implementation of the Double Ratchet cryptographic ratchet described by Signal to encrypt peer to peer messages. River also employs an AES-based cryptographic ratched optimized for group messaging.

Devices

To facilitate end to end encryption in River, each (user, client instance) tuple is associated with a deviceId, which identifies that device and is used to establish peer-to-peer encrypted sessions for the purpose of sharing group message encryption keys.

Devices are objects storing key material created on the client and stored in the River Node on the user’s UserDeviceKey stream containing the following pair of keys:

  1. Curve25519 peer-to-peer encryption key - A long-lived Curve25519 asymmetric key pair is created with a new device of a user. The private portion of this key never leaves the device, while the public portion is stored in the user’s UserDeviceKey stream. This key along with the following key are used by other user’s to establish secure and ephemeral p2p sessions.
  2. Curve25519 fallback key - A second Curve25519 key pair is created using the encryption key and published to a user’s UserDeviceKey stream. For Alice to establish a new secure p2p encrypted session with Bob, Alice would use her encryption key along with Bob’s public key portion of his encryption key and fallback key.

Device lifecycle is outside of the purview of the River protocol and managed entirely by client implementations. However, given it is expected under the protocol that there exists a 1-1 relation between (user, client instance) tuples and devices, the River Node performs periodic compaction criteria to stem the uncontrolled growth of user’s device key stream in storage.

Encryption Data Schemas

Encrypted data originating from messages or metadata, such as usernames, is described in the protocol with a protobuf message as follows.

message EncryptedData {
    /**
    * Ciphertext of the encryption envelope.
    */
    string ciphertext = 1;
    /**
    * Encryption algorithm  used to encrypt this event.
    */
    string algorithm = 2;
    /**
    * Sender device public key identifying the sender's device.
    */
    string sender_key = 3;
    /**
    * The ID of the session used to encrypt the message.
    */
    string session_id = 4;

    /**
    * Optional checksum of the cleartext data.
    */
    optional string checksum = 5;
}

The session_id is used to identify the keys associated with the ciphertext, which can be used to decrypt the same message multiple times. This is particularly useful in a group messaging application as it avoids the need to re-establish peer-to-peer encrypted sessions for each message.

Peer to peer encryption sessions are only used to transmit session keys corresponding to message events and are described by the following protobuf in the protocol.

    message GroupEncryptionSessions {
        string stream_id = 1;
        string sender_key = 2;
        repeated string session_ids = 3;
        // deviceKey: per device ciphertext of encrypted session keys that match session_ids
        map<string, string> ciphertexts = 4;
    }

A map is used to index ciphertext by the intended user’s deviceKey since peer-to-peer encrypted payloads are only able to be decrypted by the deviceKey that the outbound sender’s session was created for. In general, peer-to-peer encrypted messages are encrypted in a per device basis.

Encryption Lifecycle

Below is an example of the encryption lifecycle between Alice and Bob who are co-members of a channel within a space.

  1. Alice logs in to her client, creates a new device, and joins a Space.
  2. Bob who is already logged in and a member of the Space sees a KeySolicitation message with a device_key corresponding to Alice’s device.
  3. Bob validates that Alice isEntitled to decryption keys for the channel stream that the solicitation event appeared in and creates a new p2p encrypted outbound session using Alice’s device key and fallback key to transmit the keys requested from his local cache.
  4. Bob sends an ack to the stream to notify other co-members of the channel that Alice’s request is being worked on.
  5. Alice sees Bob’s message on her UserToDevice key stream and created a new inbound session with Bob’s device key and fallback key obtained from his UserDeviceKey stream.
  6. Alice decrypts Bob’s message and extracts the key material storing it in her local cache.
  7. Alice attempts re-decrypting the channel messages that share the session_id of the keys she now has in her possession.

Peer-to-peer encryption in River requires distinct sessions outbound, inbound for encryption and decryption, respectively. Moreover, each message can only be decrypted once per established session.

Key Sharing

Active Sharing

Session keys to encrypt message events and metadata in River are created on an as-needed basis by clients. If a user is joining a channel and decides to subsequently send a message for the first time, they will create a new outbound session key to encrypt the message and send it to the member roster of the channel along with the message. The same key can be used as an inbound session key by other members to decrypt the message encrypted with that session.

Passive Sharing

The above contrasts with passive key sharing, which is used to transmit keys that users are entitled to but do not have locally. When joining a channel for the first time, a user will sync stream events, but will need session keys to decrypt message events. The protocol supports an efficient key sharing scheme that has users place an KeySolicitation event on the stream, which any online member of the stream will see and conditionally service if the solicitator is an entitled member of the stream.

Data

The protocol allows for any stream to support key sharing by way of KeySolicitation, and KeyFulfillment messages. Since fulfilling a solicitation requires creating a peer-to-peer encrypted session with the solicitator, the device_key and fallback_key are added to the payload to save a lookup against the UserDeviceKey stream. Fulfillments are synced by members of the same stream to avoid the worst case behavior of every member fulfilling every request in a duplicative manner.


    message KeySolicitation {
        string device_key = 1; // requesters device_key
        string fallback_key = 2; // requesters fallback_key
        bool is_new_device = 3; // true if this is a new device, session_ids will be empty
        repeated string session_ids = 4;
    }

    message KeyFulfillment {
        string user_id = 1;
        string device_key = 2;
        repeated string session_ids = 3;
    }