pub async fn sealed_sender_multi_recipient_encrypt<R: Rng + CryptoRng, X: IntoIterator<Item = ServiceId>>(
    destinations: &[&ProtocolAddress],
    destination_sessions: &[&SessionRecord],
    excluded_recipients: X,
    usmc: &UnidentifiedSenderMessageContent,
    identity_store: &dyn IdentityKeyStore,
    rng: &mut R,
) -> Result<Vec<u8>>
Expand description

This method implements a single-key multi-recipient KEM as defined in Manuel Barbosa’s “Randomness Reuse: Extensions and Improvements”, a.k.a. Sealed Sender v2.

§Contrast with Sealed Sender v1

The KEM scheme implemented by this method uses the “Generic Construction” in 4.1 of Barbosa’s paper, instantiated with ElGamal encryption. This technique enables reusing a single sequence of random bytes across multiple messages with the same content, which reduces computation time for clients sending the same message to multiple recipients (without compromising the message security).

There are a few additional design tradeoffs this method makes vs Sealed Sender v1 which may make it comparatively unwieldy for certain scenarios:

  1. it requires a SessionRecord to exist already for the recipient, i.e. that a Double Ratchet message chain has previously been established in the SessionStore via process_prekey_bundle after an initial PreKeySignalMessage is received.
  2. it ferries a lot of additional information in its encoding which makes the resulting message bulkier than the message produced by Sealed Sender v1. For sending, this will generally still be more compact than sending the same message N times, but on the receiver side the message is slightly larger.
  3. unlike other message types sent over the wire, the encoded message returned by this method does not use protobuf, in order to avoid inefficiencies produced by protobuf’s packing (see Wire Format).

§High-level algorithmic overview

The high-level steps of this process are summarized below:

  1. Generate a series of random bytes.
  2. Derive an ephemeral key pair from (1).
  3. Once per recipient: Encrypt (1) using a shared secret derived from the private ephemeral key (2) and the recipient’s public identity key.
  4. Once per recipient: Add an authentication tag for (3) using a secret derived from the sender’s private identity key and the recipient’s public identity key.
  5. Generate a symmetric key from (1) and use it to symmetrically encrypt the underlying UnidentifiedSenderMessageContent via AEAD encryption. This step is only performed once per message, regardless of the number of recipients.
  6. Send the public ephemeral key (2) to the server, along with the sequence of encrypted random bytes (3) and authentication tags (4), and the single encrypted message (5).

§Pseudocode

 ENCRYPT(message, R_i):
     M = Random(32)
     r = KDF(label_r, M, len=32)
     K = KDF(label_K, M, len=32)
     E = DeriveKeyPair(r)
     for i in num_recipients:
         C_i = KDF(label_DH, DH(E, R_i) || E.public || R_i.public, len=32) XOR M
         AT_i = KDF(label_DH_s, DH(S, R_i) || E.public || C_i || S.public || R_i.public, len=16)
     ciphertext = AEAD_Encrypt(K, message)
     return E.public, C_i, AT_i, ciphertext

 DECRYPT(E.public, C, AT, ciphertext):
     M = KDF(label_DH, DH(E, R) || E.public || R.public, len=32) xor C
     r = KDF(label_r, M, len=32)
     K = KDF(label_K, M, len=32)
     E' = DeriveKeyPair(r)
     if E.public != E'.public:
         return DecryptionError
     message = AEAD_Decrypt(K, ciphertext) // includes S.public
     AT' = KDF(label_DH_s, DH(S, R) || E.public || C || S.public || R.public, len=16)
     if AT != AT':
         return DecryptionError
     return message

§Routing messages to recipients

The server will split up the set of messages and securely route each individual received message to its intended recipient. SealedSenderV2SentMessage can perform this fan-out operation.

§Wire Format

Multi-recipient sealed-sender does not use protobufs for its payload format. Instead, it uses a flat format marked with a version byte. The format is different for sending and receiving. The decrypted content is a protobuf-encoded UnidentifiedSenderMessage.Message from sealed_sender.proto.

The public key used in Sealed Sender v2 is always a Curve25519 DJB key.

§The version byte

Sealed sender messages (v1 and v2) in serialized form begin with a version byte. This byte has the form:

(requiredVersion << 4) | currentVersion

v1 messages thus have a version byte of 0x11. v2 messages have a version byte of 0x22 or 0x23. A hypothetical version byte 0x34 would indicate a message encoded as Sealed Sender v4, but decodable by any client that supports Sealed Sender v3.

§Received messages

ReceivedMessage {
    version_byte: u8,
    c: [u8; 32],
    at: [u8; 16],
    e_pub: [u8; 32],
    message: [u8] // remaining bytes
}

Each individual Sealed Sender message received from the server is decoded in the Signal client by calling sealed_sender_decrypt.

§Sent messages

SentMessage {
    version_byte: u8,
    count: varint,
    recipients: [PerRecipientData | ExcludedRecipient; count],
    e_pub: [u8; 32],
    message: [u8] // remaining bytes
}

PerRecipientData {
    recipient: Recipient,
    devices: [DeviceList], // last element's has_more = 0
    c: [u8; 32],
    at: [u8; 16],
}

ExcludedRecipient {
    recipient: Recipient,
    no_devices_marker: u8 = 0, // never a valid device ID
}

DeviceList {
    device_id: u8,
    has_more: u1, // high bit of following field
    unused: u1,   // high bit of following field
    registration_id: u14,
}

Recipient {
    service_id_fixed_width_binary: [u8; 17],
}

The varint encoding used is the same as protobuf’s. Values are unsigned. Fixed-width-binary encoding is used for the ServiceId values. Fixed-width integers are unaligned and in network byte order (big-endian).