libsignal_service/
cipher.rs

1use std::{convert::TryFrom, fmt, time::SystemTime};
2
3use aes::cipher::block_padding::{Iso7816, RawPadding};
4use base64::prelude::*;
5use libsignal_core::ServiceIdKind;
6use libsignal_protocol::{
7    group_decrypt, message_decrypt_prekey, message_decrypt_signal,
8    message_encrypt, process_sender_key_distribution_message,
9    sealed_sender_decrypt_to_usmc, sealed_sender_encrypt,
10    CiphertextMessageType, DeviceId, IdentityKeyStore, KyberPreKeyStore,
11    PreKeySignalMessage, PreKeyStore, ProtocolAddress, ProtocolStore,
12    PublicKey, SealedSenderDecryptionResult, SenderCertificate,
13    SenderKeyDistributionMessage, SenderKeyStore, ServiceId, SessionStore,
14    SignalMessage, SignalProtocolError, SignedPreKeyStore, Timestamp,
15};
16use prost::Message;
17use rand::{rng, CryptoRng, Rng};
18use uuid::Uuid;
19
20use crate::{
21    content::{Content, Metadata},
22    envelope::Envelope,
23    push_service::ServiceError,
24    sender::OutgoingPushMessage,
25    session_store::SessionStoreExt,
26    utils::BASE64_RELAXED,
27    ServiceIdExt,
28};
29
30/// Decrypts incoming messages and encrypts outgoing messages.
31///
32/// Equivalent of SignalServiceCipher in Java.
33#[derive(Clone)]
34pub struct ServiceCipher<S> {
35    protocol_store: S,
36    trust_roots: Vec<PublicKey>,
37    local_uuid: Uuid,
38    local_device_id: DeviceId,
39}
40
41impl<S> fmt::Debug for ServiceCipher<S> {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        f.debug_struct("ServiceCipher")
44            .field("protocol_store", &"...")
45            .field("trust_root", &"...")
46            .field("local_uuid", &self.local_uuid)
47            .field("local_device_id", &self.local_device_id)
48            .finish()
49    }
50}
51
52fn debug_envelope(envelope: &Envelope) -> String {
53    if envelope.content.is_none() {
54        "Envelope { empty }".to_string()
55    } else {
56        format!(
57            "Envelope {{ \
58                 source_address: {:?}, \
59                 source_device: {:?}, \
60                 server_guid: {:?}, \
61                 timestamp: {:?}, \
62                 content: {} bytes, \
63             }}",
64            envelope.source_service_id,
65            envelope.source_device(),
66            envelope.server_guid(),
67            envelope.timestamp(),
68            envelope.content().len(),
69        )
70    }
71}
72
73impl<S> ServiceCipher<S>
74where
75    S: ProtocolStore + SenderKeyStore + SessionStoreExt + Clone,
76{
77    pub fn new(
78        protocol_store: S,
79        trust_roots: Vec<PublicKey>,
80        local_uuid: Uuid,
81        local_device_id: DeviceId,
82    ) -> Self {
83        Self {
84            protocol_store,
85            trust_roots,
86            local_uuid,
87            local_device_id,
88        }
89    }
90
91    /// Opens ("decrypts") an envelope.
92    ///
93    /// Envelopes may be empty, in which case this method returns `Ok(None)`
94    #[tracing::instrument(skip(envelope, csprng), fields(envelope = debug_envelope(&envelope)))]
95    pub async fn open_envelope<R: Rng + CryptoRng>(
96        &mut self,
97        envelope: Envelope,
98        csprng: &mut R,
99    ) -> Result<Option<Content>, ServiceError> {
100        if envelope.content.is_some() {
101            let plaintext = self.decrypt(&envelope, csprng).await?;
102            let was_plaintext = plaintext.metadata.was_plaintext;
103            let message =
104                crate::proto::Content::decode(plaintext.data.as_slice())?;
105
106            tracing::Span::current()
107                .record("envelope_metadata", plaintext.metadata.to_string());
108
109            // Sanity test: if the envelope was plaintext, the message should *only* be a
110            // decryption failure error
111            if was_plaintext {
112                if let crate::proto::Content {
113                    data_message: None,
114                    sync_message: None,
115                    call_message: None,
116                    null_message: None,
117                    receipt_message: None,
118                    typing_message: None,
119                    sender_key_distribution_message: None,
120                    decryption_error_message: Some(decryption_error_message),
121                    story_message: None,
122                    pni_signature_message: None,
123                    edit_message: None,
124                } = &message
125                {
126                    tracing::warn!(
127                        ?envelope,
128                        "Received a decryption error message: {}.",
129                        String::from_utf8_lossy(decryption_error_message)
130                    );
131                } else {
132                    tracing::error!(
133                        ?envelope,
134                        "Received a plaintext envelope with a non-decryption error message."
135                    );
136                    return Ok(None);
137                }
138            }
139
140            if message.sync_message.is_some()
141                && plaintext.metadata.sender.aci().map(Into::into)
142                    != Some(self.local_uuid)
143            {
144                tracing::warn!("Source is not ourself.");
145                return Ok(None);
146            }
147
148            if let Some(bytes) = &message.sender_key_distribution_message {
149                let skdm = SenderKeyDistributionMessage::try_from(&bytes[..])?;
150                process_sender_key_distribution_message(
151                    &plaintext.metadata.protocol_address()?,
152                    &skdm,
153                    &mut self.protocol_store,
154                )
155                .await?;
156
157                match Content::from_proto(message, plaintext.metadata) {
158                    Err(ServiceError::UnsupportedContent) => {
159                        tracing::trace!("Sender key distribution message without additional content");
160                        return Ok(None);
161                    },
162                    content => return Ok(Some(content?)),
163                }
164            }
165            let content = Content::from_proto(message, plaintext.metadata);
166            Ok(Some(content?))
167        } else {
168            Ok(None)
169        }
170    }
171
172    /// Equivalent of decrypt(Envelope, ciphertext)
173    ///
174    /// Triage of legacy messages happens inside this method, as opposed to the
175    /// Java implementation, because it makes the borrow checker and the
176    /// author happier.
177    #[tracing::instrument(skip(envelope, csprng), fields(envelope = debug_envelope(envelope)))]
178    async fn decrypt<R: Rng + CryptoRng>(
179        &mut self,
180        envelope: &Envelope,
181        csprng: &mut R,
182    ) -> Result<Plaintext, ServiceError> {
183        let ciphertext = if let Some(msg) = envelope.content.as_ref() {
184            msg
185        } else {
186            return Err(ServiceError::InvalidFrame {
187                reason:
188                    "envelope should have either a legacy message or content.",
189            });
190        };
191
192        let server_guid = envelope.parse_server_guid();
193
194        let Some(destination_service_id) =
195            envelope.parse_destination_service_id()
196        else {
197            tracing::warn!(
198                "missing destination service id; ignoring invalid message."
199            );
200            return Err(ServiceError::InvalidFrame {
201                reason: "missing destination service id",
202            });
203        };
204
205        if destination_service_id.raw_uuid() != self.local_uuid {
206            tracing::warn!(
207                "mismatching destination service id; ignoring invalid message."
208            );
209            return Err(ServiceError::InvalidFrame {
210                reason: "mismatch destination service id",
211            });
212        }
213
214        if destination_service_id.kind() == ServiceIdKind::Pni
215            && envelope.source_service_id.is_none()
216        {
217            tracing::warn!("received sealed sender message to our PNI; ignoring invalid message");
218            return Err(ServiceError::InvalidFrame {
219                reason: "sealed sender received on our PNI",
220            });
221        }
222
223        // TODO: let chain in edition 2024
224        if let Some(source_service_id) = envelope.parse_source_service_id() {
225            if source_service_id.kind() == ServiceIdKind::Pni
226                && envelope.r#type() != Type::ServerDeliveryReceipt
227            {
228                tracing::warn!("got a message from a PNI that was not a ServerDeliveryReceipt; ignoring invalid message");
229                return Err(ServiceError::InvalidFrame {
230                    reason: "PNI received a non-ServerDeliveryReceipt",
231                });
232            }
233        }
234
235        use crate::proto::envelope::Type;
236        let plaintext = match envelope.r#type() {
237            Type::PrekeyBundle => {
238                let sender = get_preferred_protocol_address(
239                    &self.protocol_store,
240                    &envelope
241                        .parse_source_service_id()
242                        .expect("prekey bundle format"),
243                    envelope.source_device().try_into()?,
244                )
245                .await?;
246                let metadata = Metadata {
247                    destination: envelope
248                        .parse_destination_service_id()
249                        .expect("prekey bundle format"),
250                    sender: envelope
251                        .parse_source_service_id()
252                        .expect("prekey bundle format"),
253                    sender_device: envelope.source_device().try_into()?,
254                    timestamp: envelope.server_timestamp(),
255                    needs_receipt: false,
256                    unidentified_sender: false,
257                    was_plaintext: false,
258
259                    server_guid,
260                };
261
262                let mut data = message_decrypt_prekey(
263                    &PreKeySignalMessage::try_from(&ciphertext[..])?,
264                    &sender,
265                    &mut self.protocol_store.clone(),
266                    &mut self.protocol_store.clone(),
267                    &mut self.protocol_store.clone(),
268                    &self.protocol_store.clone(),
269                    &mut self.protocol_store.clone(),
270                    csprng,
271                )
272                .await?
273                .as_slice()
274                .to_vec();
275
276                let session_record = self
277                    .protocol_store
278                    .load_session(&sender)
279                    .await?
280                    .ok_or(SignalProtocolError::SessionNotFound(sender))?;
281
282                strip_padding_version(
283                    session_record.session_version()?,
284                    &mut data,
285                )?;
286                Plaintext { metadata, data }
287            },
288            Type::PlaintextContent => {
289                tracing::warn!(?envelope, "Envelope with plaintext content.  This usually indicates a decryption retry.");
290                let metadata = Metadata {
291                    destination: envelope
292                        .parse_destination_service_id()
293                        .expect("plaintext content format"),
294                    sender: envelope
295                        .parse_source_service_id()
296                        .expect("plaintext content format"),
297                    sender_device: envelope.source_device().try_into()?,
298                    timestamp: envelope.server_timestamp(),
299                    needs_receipt: false,
300                    unidentified_sender: false,
301                    was_plaintext: true,
302
303                    server_guid,
304                };
305                Plaintext {
306                    metadata,
307                    data: ciphertext.clone(),
308                }
309            },
310            Type::Ciphertext => {
311                let sender = get_preferred_protocol_address(
312                    &self.protocol_store,
313                    &envelope
314                        .parse_source_service_id()
315                        .expect("ciphertext envelope format"),
316                    envelope.source_device().try_into()?,
317                )
318                .await?;
319                let metadata = Metadata {
320                    destination: envelope
321                        .parse_destination_service_id()
322                        .expect("ciphertext envelope format"),
323                    sender: envelope
324                        .parse_source_service_id()
325                        .expect("ciphertext envelope format"),
326                    sender_device: envelope.source_device().try_into()?,
327                    timestamp: envelope.timestamp(),
328                    needs_receipt: false,
329                    unidentified_sender: false,
330                    was_plaintext: false,
331
332                    server_guid,
333                };
334
335                let mut data = message_decrypt_signal(
336                    &SignalMessage::try_from(&ciphertext[..])?,
337                    &sender,
338                    &mut self.protocol_store.clone(),
339                    &mut self.protocol_store.clone(),
340                    csprng,
341                )
342                .await?
343                .as_slice()
344                .to_vec();
345
346                let session_record = self
347                    .protocol_store
348                    .load_session(&sender)
349                    .await?
350                    .ok_or(SignalProtocolError::SessionNotFound(sender))?;
351
352                strip_padding_version(
353                    session_record.session_version()?,
354                    &mut data,
355                )?;
356                Plaintext { metadata, data }
357            },
358            Type::UnidentifiedSender => {
359                let SealedSenderDecryptionResult {
360                    sender_uuid,
361                    sender_e164: _,
362                    device_id,
363                    mut message,
364                } = sealed_sender_decrypt(
365                    ciphertext,
366                    &self.trust_roots,
367                    Timestamp::from_epoch_millis(envelope.timestamp()),
368                    None,
369                    self.local_uuid.to_string(),
370                    self.local_device_id,
371                    &mut self.protocol_store.clone(),
372                    &mut self.protocol_store.clone(),
373                    &mut self.protocol_store.clone(),
374                    &mut self.protocol_store.clone(),
375                    &mut self.protocol_store.clone(),
376                    &mut self.protocol_store,
377                )
378                .await?;
379
380                let Some(sender) =
381                    ServiceId::parse_from_service_id_string(&sender_uuid)
382                else {
383                    return Err(
384                        SignalProtocolError::InvalidSealedSenderMessage(
385                            "invalid sender UUID".to_string(),
386                        )
387                        .into(),
388                    );
389                };
390
391                let needs_receipt = if envelope.source_service_id.is_some() {
392                    tracing::warn!(?envelope, "Received an unidentified delivery over an identified channel.  Marking needs_receipt=false");
393                    false
394                } else {
395                    true
396                };
397
398                if sender.kind() == ServiceIdKind::Pni {
399                    tracing::warn!(
400                        "sealed sender used for PNI; ignoring invalid message"
401                    );
402                    return Err(ServiceError::InvalidFrame {
403                        reason: "sealed sender used for PNI",
404                    });
405                }
406
407                let metadata = Metadata {
408                    destination: envelope
409                        .parse_destination_service_id()
410                        .expect("unidentified sender envelope format"),
411                    sender,
412                    sender_device: device_id,
413                    timestamp: envelope.timestamp(),
414                    unidentified_sender: true,
415                    needs_receipt,
416                    was_plaintext: false,
417
418                    server_guid,
419                };
420
421                strip_padding(&mut message)?;
422
423                Plaintext {
424                    metadata,
425                    data: message,
426                }
427            },
428            _ => {
429                // else
430                return Err(ServiceError::InvalidFrame {
431                    reason: "envelope has unknown type",
432                });
433            },
434        };
435        Ok(plaintext)
436    }
437
438    #[tracing::instrument(
439        skip(address, unidentified_access, content, csprng),
440        fields(
441            address = %address,
442            with_unidentified_access = unidentified_access.is_some(),
443            content_length = content.len(),
444        )
445    )]
446    pub(crate) async fn encrypt<R: Rng + CryptoRng>(
447        &mut self,
448        address: &ProtocolAddress,
449        unidentified_access: Option<&SenderCertificate>,
450        content: &[u8],
451        csprng: &mut R,
452    ) -> Result<OutgoingPushMessage, ServiceError> {
453        let mut rng = rng();
454
455        let session_record = self
456            .protocol_store
457            .load_session(address)
458            .await?
459            .ok_or_else(|| {
460            SignalProtocolError::SessionNotFound(address.clone())
461        })?;
462
463        let padded_content =
464            add_padding(session_record.session_version()?, content)?;
465
466        if let Some(unindentified_access) = unidentified_access {
467            let destination_registration_id =
468                session_record.remote_registration_id()?;
469
470            let message = sealed_sender_encrypt(
471                address,
472                unindentified_access,
473                &padded_content,
474                &mut self.protocol_store.clone(),
475                &mut self.protocol_store,
476                SystemTime::now(),
477                csprng,
478            )
479            .await?;
480
481            use crate::proto::envelope::Type;
482            Ok(OutgoingPushMessage {
483                r#type: Type::UnidentifiedSender as u32,
484                destination_device_id: address.device_id(),
485                destination_registration_id,
486                content: BASE64_RELAXED.encode(message),
487            })
488        } else {
489            let message = message_encrypt(
490                &padded_content,
491                address,
492                &mut self.protocol_store.clone(),
493                &mut self.protocol_store.clone(),
494                SystemTime::now(),
495                &mut rng,
496            )
497            .await?;
498
499            let destination_registration_id =
500                session_record.remote_registration_id()?;
501
502            let body = BASE64_RELAXED.encode(message.serialize());
503
504            use crate::proto::envelope::Type;
505            let message_type = match message.message_type() {
506                CiphertextMessageType::PreKey => Type::PrekeyBundle,
507                CiphertextMessageType::Whisper => Type::Ciphertext,
508                t => panic!("Bad type: {:?}", t),
509            } as u32;
510            Ok(OutgoingPushMessage {
511                r#type: message_type,
512                destination_device_id: address.device_id(),
513                destination_registration_id,
514                content: body,
515            })
516        }
517    }
518}
519
520struct Plaintext {
521    metadata: Metadata,
522    data: Vec<u8>,
523}
524
525#[expect(clippy::comparison_chain)]
526fn add_padding(version: u32, contents: &[u8]) -> Result<Vec<u8>, ServiceError> {
527    if version < 2 {
528        Err(ServiceError::PaddingVersion(version))
529    } else if version == 2 {
530        Ok(contents.to_vec())
531    } else {
532        let message_length = contents.len();
533        let message_length_with_terminator = contents.len() + 1;
534        let mut message_part_count = message_length_with_terminator / 160;
535        if !message_length_with_terminator.is_multiple_of(160) {
536            message_part_count += 1;
537        }
538
539        let message_length_with_padding = message_part_count * 160;
540
541        let mut buffer = vec![0u8; message_length_with_padding];
542        buffer[..message_length].copy_from_slice(contents);
543        Iso7816::raw_pad(&mut buffer, message_length);
544        Ok(buffer)
545    }
546}
547
548#[expect(clippy::comparison_chain)]
549fn strip_padding_version(
550    version: u32,
551    contents: &mut Vec<u8>,
552) -> Result<(), ServiceError> {
553    if version < 2 {
554        Err(ServiceError::InvalidFrame {
555            reason: "unknown version",
556        })
557    } else if version == 2 {
558        Ok(())
559    } else {
560        strip_padding(contents)?;
561        Ok(())
562    }
563}
564
565fn strip_padding(contents: &mut Vec<u8>) -> Result<(), ServiceError> {
566    let new_length = Iso7816::raw_unpad(contents)?.len();
567    contents.resize(new_length, 0);
568    Ok(())
569}
570
571/// Equivalent of `SignalServiceCipher::getPreferredProtocolAddress`
572pub async fn get_preferred_protocol_address<S: SessionStore>(
573    session_store: &S,
574    address: &ServiceId,
575    device_id: DeviceId,
576) -> Result<ProtocolAddress, libsignal_protocol::error::SignalProtocolError> {
577    let address = address.to_protocol_address(device_id);
578    if session_store.load_session(&address).await?.is_some() {
579        return Ok(address);
580    }
581
582    Ok(address)
583}
584
585/// Decrypt a Sealed Sender message `ciphertext` in either the v1 or v2 format, validate its sender
586/// certificate, and then decrypt the inner message payload.
587///
588/// This method calls [`sealed_sender_decrypt_to_usmc`] to extract the sender information, including
589/// the embedded [`SenderCertificate`]. The sender certificate (signed by the [`ServerCertificate`])
590/// is then validated against the `trust_root` baked into the client to ensure that the sender's
591/// identity was not forged.
592#[allow(clippy::too_many_arguments)]
593#[tracing::instrument(
594    skip(
595        ciphertext,
596        trust_roots,
597        identity_store,
598        session_store,
599        pre_key_store,
600        signed_pre_key_store,
601        sender_key_store,
602        kyber_pre_key_store
603    ),
604    fields(
605        ciphertext = ciphertext.len(),
606    )
607)]
608async fn sealed_sender_decrypt(
609    ciphertext: &[u8],
610    trust_roots: &[PublicKey],
611    timestamp: Timestamp,
612    local_e164: Option<String>,
613    local_uuid: String,
614    local_device_id: DeviceId,
615    identity_store: &mut dyn IdentityKeyStore,
616    session_store: &mut dyn SessionStore,
617    pre_key_store: &mut dyn PreKeyStore,
618    signed_pre_key_store: &mut dyn SignedPreKeyStore,
619    sender_key_store: &mut dyn SenderKeyStore,
620    kyber_pre_key_store: &mut dyn KyberPreKeyStore,
621) -> Result<SealedSenderDecryptionResult, SignalProtocolError> {
622    let usmc =
623        sealed_sender_decrypt_to_usmc(ciphertext, identity_store).await?;
624
625    if !usmc
626        .sender()?
627        .validate_with_trust_roots(trust_roots, timestamp)?
628    {
629        return Err(SignalProtocolError::InvalidSealedSenderMessage(
630            "trust root validation failed".to_string(),
631        ));
632    }
633
634    let is_local_uuid = local_uuid == usmc.sender()?.sender_uuid()?;
635
636    let is_local_e164 = match (local_e164, usmc.sender()?.sender_e164()?) {
637        (Some(l), Some(s)) => l == s,
638        (_, _) => false,
639    };
640
641    if (is_local_e164 || is_local_uuid)
642        && usmc.sender()?.sender_device_id()? == local_device_id
643    {
644        return Err(SignalProtocolError::SealedSenderSelfSend);
645    }
646
647    let mut rng = rng();
648
649    let remote_address = ProtocolAddress::new(
650        usmc.sender()?.sender_uuid()?.to_string(),
651        usmc.sender()?.sender_device_id()?,
652    );
653
654    let message = match usmc.msg_type()? {
655        CiphertextMessageType::Whisper => {
656            let ctext = SignalMessage::try_from(usmc.contents()?)?;
657            message_decrypt_signal(
658                &ctext,
659                &remote_address,
660                session_store,
661                identity_store,
662                &mut rng,
663            )
664            .await?
665        },
666        CiphertextMessageType::PreKey => {
667            let ctext = PreKeySignalMessage::try_from(usmc.contents()?)?;
668            message_decrypt_prekey(
669                &ctext,
670                &remote_address,
671                session_store,
672                identity_store,
673                pre_key_store,
674                signed_pre_key_store,
675                kyber_pre_key_store,
676                &mut rng,
677            )
678            .await?
679        },
680        CiphertextMessageType::SenderKey => {
681            group_decrypt(usmc.contents()?, sender_key_store, &remote_address)
682                .await?
683        },
684        msg_type => {
685            return Err(SignalProtocolError::InvalidMessage(
686                msg_type,
687                "unexpected message type for sealed_sender_decrypt",
688            ));
689        },
690    };
691
692    Ok(SealedSenderDecryptionResult {
693        sender_uuid: usmc.sender()?.sender_uuid()?.to_string(),
694        sender_e164: usmc.sender()?.sender_e164()?.map(|s| s.to_string()),
695        device_id: usmc.sender()?.sender_device_id()?,
696        message,
697    })
698}