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