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>>where
X::IntoIter: ExactSizeIterator,
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:
- it requires a
SessionRecord
to exist already for the recipient, i.e. that a Double Ratchet message chain has previously been established in theSessionStore
viaprocess_prekey_bundle
after an initialPreKeySignalMessage
is received. - 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.
- 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:
- Generate a series of random bytes.
- Derive an ephemeral key pair from (1).
- Once per recipient: Encrypt (1) using a shared secret derived from the private ephemeral key (2) and the recipient’s public identity key.
- 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.
- 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. - 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).