Skip to main content

libsignal_service/groups_v2/
operations.rs

1use std::convert::TryInto;
2
3use base64::prelude::*;
4use bytes::Bytes;
5use libsignal_protocol::{Aci, Pni, ServiceId};
6use prost::Message;
7use zkgroup::{
8    groups::GroupSecretParams,
9    profiles::{
10        AnyProfileKeyCredentialPresentation, ExpiringProfileKeyCredential,
11        ProfileKey,
12    },
13    ServerPublicParams, PRESENTATION_VERSION_3,
14};
15
16use crate::{
17    groups_v2::model::Timer,
18    proto::{
19        self, group_attribute_blob, GroupAttributeBlob,
20        Member as EncryptedMember,
21    },
22    utils::BASE64_RELAXED,
23};
24
25use super::{
26    model::{
27        AccessControl, BannedMember, GroupMemberCandidate, Member,
28        PendingMember, PromotedMember, RequestingMember,
29    },
30    Group, GroupChange, GroupChanges,
31};
32
33pub struct GroupOperations {
34    pub group_secret_params: GroupSecretParams,
35}
36
37#[derive(Debug, thiserror::Error)]
38pub enum GroupDecodingError {
39    #[error("zero-knowledge group deserialization failure")]
40    ZkGroupDeserializationFailure,
41    #[error("zero-knowledge group verification failure")]
42    ZkGroupVerificationFailure,
43    #[error(transparent)]
44    BincodeError(#[from] bincode::Error),
45    #[error("protobuf message decoding error: {0}")]
46    ProtobufDecodeError(#[from] prost::DecodeError),
47    #[error("wrong group attribute blob")]
48    WrongBlob,
49    #[error("wrong enum value")]
50    WrongEnumValue,
51    #[error("wrong service ID type: should be ACI")]
52    NotAci,
53    #[error("wrong service ID type: should be PNI")]
54    NotPni,
55}
56
57impl From<zkgroup::ZkGroupDeserializationFailure> for GroupDecodingError {
58    fn from(_: zkgroup::ZkGroupDeserializationFailure) -> Self {
59        GroupDecodingError::ZkGroupDeserializationFailure
60    }
61}
62
63impl From<zkgroup::ZkGroupVerificationFailure> for GroupDecodingError {
64    fn from(_: zkgroup::ZkGroupVerificationFailure) -> Self {
65        GroupDecodingError::ZkGroupVerificationFailure
66    }
67}
68
69impl GroupOperations {
70    fn encrypt_service_id(
71        &self,
72        service_id: ServiceId,
73    ) -> Result<Vec<u8>, GroupDecodingError> {
74        let ciphertext =
75            self.group_secret_params.encrypt_service_id(service_id);
76        Ok(zkgroup::serialize(&ciphertext))
77    }
78
79    fn decrypt_service_id(
80        &self,
81        ciphertext: &[u8],
82    ) -> Result<ServiceId, GroupDecodingError> {
83        match self
84            .group_secret_params
85            .decrypt_service_id(zkgroup::deserialize(ciphertext)?)?
86        {
87            ServiceId::Aci(aci) => Ok(ServiceId::from(aci)),
88            ServiceId::Pni(pni) => Ok(ServiceId::from(pni)),
89        }
90    }
91
92    fn encrypt_aci(&self, aci: Aci) -> Result<Vec<u8>, GroupDecodingError> {
93        self.encrypt_service_id(aci.into())
94    }
95
96    fn decrypt_aci(
97        &self,
98        ciphertext: &[u8],
99    ) -> Result<Aci, GroupDecodingError> {
100        match self
101            .group_secret_params
102            .decrypt_service_id(zkgroup::deserialize(ciphertext)?)?
103        {
104            ServiceId::Aci(aci) => Ok(aci),
105            ServiceId::Pni(pni) => {
106                tracing::error!(
107                    "Expected Aci, got Pni: {}",
108                    pni.service_id_string()
109                );
110                Err(GroupDecodingError::NotAci)
111            },
112        }
113    }
114
115    fn decrypt_pni(
116        &self,
117        ciphertext: &[u8],
118    ) -> Result<Pni, GroupDecodingError> {
119        match self
120            .group_secret_params
121            .decrypt_service_id(zkgroup::deserialize(ciphertext)?)?
122        {
123            ServiceId::Pni(pni) => Ok(pni),
124            ServiceId::Aci(aci) => {
125                tracing::error!(
126                    "Expected Pni, got Aci: {}",
127                    aci.service_id_string()
128                );
129                Err(GroupDecodingError::NotPni)
130            },
131        }
132    }
133
134    fn encrypt_profile_key(
135        &self,
136        profile_key: ProfileKey,
137        aci: Aci,
138    ) -> Result<Vec<u8>, GroupDecodingError> {
139        let ciphertext = self
140            .group_secret_params
141            .encrypt_profile_key(profile_key, aci);
142        Ok(zkgroup::serialize(&ciphertext))
143    }
144
145    fn decrypt_profile_key(
146        &self,
147        encrypted_profile_key: &[u8],
148        decrypted_aci: libsignal_protocol::Aci,
149    ) -> Result<ProfileKey, GroupDecodingError> {
150        Ok(self.group_secret_params.decrypt_profile_key(
151            zkgroup::deserialize(encrypted_profile_key)?,
152            decrypted_aci,
153        )?)
154    }
155
156    fn decrypt_profile_key_presentation(
157        &self,
158        aci: &[u8],
159        profile_key: &[u8],
160        presentation: &[u8],
161    ) -> Result<(Aci, ProfileKey), GroupDecodingError> {
162        if presentation.is_empty() {
163            let aci = self.decrypt_aci(aci)?;
164            let profile_key = self.decrypt_profile_key(profile_key, aci)?;
165            return Ok((aci, profile_key));
166        }
167
168        let profile_key_credential_presentation =
169            AnyProfileKeyCredentialPresentation::new(presentation)?;
170
171        match self.group_secret_params.decrypt_service_id(
172            profile_key_credential_presentation.get_uuid_ciphertext(),
173        )? {
174            ServiceId::Aci(aci) => {
175                let profile_key =
176                    self.group_secret_params.decrypt_profile_key(
177                        profile_key_credential_presentation
178                            .get_profile_key_ciphertext(),
179                        aci,
180                    )?;
181                Ok((aci, profile_key))
182            },
183            _ => Err(GroupDecodingError::NotAci),
184        }
185    }
186
187    fn decrypt_pni_aci_promotion_presentation(
188        &self,
189        member: &proto::group_change::actions::PromoteMemberPendingPniAciProfileKeyAction,
190    ) -> Result<PromotedMember, GroupDecodingError> {
191        let aci = self.decrypt_aci(&member.user_id)?;
192        let pni = self.decrypt_pni(&member.pni)?;
193        let profile_key = self.decrypt_profile_key(&member.profile_key, aci)?;
194        Ok(PromotedMember {
195            aci,
196            pni,
197            profile_key,
198        })
199    }
200
201    fn decrypt_member(
202        &self,
203        member: EncryptedMember,
204    ) -> Result<Member, GroupDecodingError> {
205        let (aci, profile_key) = self.decrypt_profile_key_presentation(
206            &member.user_id,
207            &member.profile_key,
208            &member.presentation,
209        )?;
210
211        let label = self.decrypt_member_label_text(&member.label_string);
212        let label_emoji = self.decrypt_member_label_emoji(&member.label_emoji);
213
214        Ok(Member {
215            aci,
216            profile_key,
217            role: member.role.try_into()?,
218            joined_at_version: member.joined_at_version,
219            label,
220            label_emoji,
221        })
222    }
223
224    fn decrypt_pending_member(
225        &self,
226        member: proto::MemberPendingProfileKey,
227    ) -> Result<PendingMember, GroupDecodingError> {
228        let inner_member =
229            member.member.ok_or(GroupDecodingError::WrongBlob)?;
230        let service_id = self.decrypt_service_id(&inner_member.user_id)?;
231        let added_by_aci = self.decrypt_aci(&member.added_by_user_id)?;
232
233        Ok(PendingMember {
234            address: service_id,
235            role: inner_member.role.try_into()?,
236            added_by_aci,
237            timestamp: member.timestamp,
238        })
239    }
240
241    fn decrypt_requesting_member(
242        &self,
243        member: proto::MemberPendingAdminApproval,
244    ) -> Result<RequestingMember, GroupDecodingError> {
245        let (aci, profile_key) = self.decrypt_profile_key_presentation(
246            &member.user_id,
247            &member.profile_key,
248            &member.presentation,
249        )?;
250        Ok(RequestingMember {
251            profile_key,
252            aci,
253            timestamp: member.timestamp,
254        })
255    }
256
257    fn decrypt_banned_member(
258        &self,
259        member: proto::MemberBanned,
260    ) -> Result<BannedMember, GroupDecodingError> {
261        Ok(BannedMember {
262            user_id: self.decrypt_service_id(&member.user_id)?,
263            timestamp: member.timestamp,
264        })
265    }
266
267    fn decrypt_string(
268        &self,
269        bytes: &[u8],
270    ) -> Result<String, GroupDecodingError> {
271        let bytes =
272            self.group_secret_params.decrypt_blob_with_padding(bytes)?;
273        String::from_utf8(bytes).map_err(|_| GroupDecodingError::WrongBlob)
274    }
275
276    /// Decrypts an optional string field, returning `None` when `bytes` is empty
277    /// so absent labels don't fail the whole decode.
278    fn maybe_decrypt_string(
279        &self,
280        bytes: &[u8],
281    ) -> Result<Option<String>, GroupDecodingError> {
282        if bytes.is_empty() {
283            return Ok(None);
284        }
285        self.decrypt_string(bytes).map(Some)
286    }
287
288    /// Decrypts a member label, treating both empty input and decryption failure
289    /// as unset, matching Signal-Android's `decryptMemberLabelText`.
290    fn decrypt_member_label_text(&self, bytes: &[u8]) -> Option<String> {
291        match self.maybe_decrypt_string(bytes) {
292            Ok(s) => s,
293            Err(e) => {
294                tracing::warn!("failed to decrypt member label string: {e}");
295                None
296            },
297        }
298    }
299
300    /// Decrypts a member label emoji, treating both empty input and decryption
301    /// failure as unset, matching Signal-Android's `decryptMemberLabelEmoji`.
302    fn decrypt_member_label_emoji(&self, bytes: &[u8]) -> Option<String> {
303        match self.maybe_decrypt_string(bytes) {
304            Ok(s) => s,
305            Err(e) => {
306                tracing::warn!("failed to decrypt member label emoji: {e}");
307                None
308            },
309        }
310    }
311
312    fn decrypt_blob(&self, bytes: &[u8]) -> GroupAttributeBlob {
313        if bytes.is_empty() {
314            GroupAttributeBlob::default()
315        } else if bytes.len() < 29 {
316            tracing::warn!("bad encrypted blob length");
317            GroupAttributeBlob::default()
318        } else {
319            self.group_secret_params
320                .decrypt_blob_with_padding(bytes)
321                .map_err(GroupDecodingError::from)
322                .and_then(|plaintext| {
323                    GroupAttributeBlob::decode(Bytes::from(plaintext))
324                        .map_err(GroupDecodingError::ProtobufDecodeError)
325                })
326                .unwrap_or_else(|e| {
327                    tracing::warn!("bad encrypted blob: {}", e);
328                    GroupAttributeBlob::default()
329                })
330        }
331    }
332
333    /// Helper method to encrypt a `group_attribute_blob::Content`.
334    ///
335    /// # Padding Format
336    ///
337    /// Uses `encrypt_blob_with_padding` format from Signal's zkgroup's
338    /// `GroupSecretParams`, which prepends a 4-byte big-endian padding length value
339    /// to the plaintext before encryption. For group attribute blobs, padding is
340    /// always 0, so the format is:
341    /// - First 4 bytes: `0u32.to_be_bytes()` (padding length = 0)
342    /// - Remaining bytes: protobuf-encoded `GroupAttributeBlob`
343    ///
344    /// # References
345    ///
346    /// - Signal libsignal repository: <https://github.com/signalapp/libsignal>
347    /// - GroupSecretParams implementation:
348    ///   `rust/zkgroup/src/api/groups/group_params.rs`
349    /// - Java ClientZkGroupCipher usage:
350    ///   `java/shared/java/org/signal/libsignal/zkgroup/groups/ClientZkGroupCipher.java`
351    fn encrypt_blob_content<R: rand::Rng + rand::CryptoRng>(
352        &self,
353        content: group_attribute_blob::Content,
354        rng: &mut R,
355    ) -> Vec<u8> {
356        let blob = GroupAttributeBlob {
357            content: Some(content),
358        };
359        let buf = blob.encode_to_vec();
360
361        let mut randomness = [0u8; 32];
362        rng.fill_bytes(&mut randomness);
363        self.group_secret_params
364            .encrypt_blob_with_padding(randomness, &buf, 0)
365    }
366
367    pub fn encrypt_title<R: rand::Rng + rand::CryptoRng>(
368        &self,
369        title: &str,
370        rng: &mut R,
371    ) -> Vec<u8> {
372        self.encrypt_blob_content(
373            group_attribute_blob::Content::Title(title.to_string()),
374            rng,
375        )
376    }
377
378    pub fn encrypt_description<R: rand::Rng + rand::CryptoRng>(
379        &self,
380        description: Option<&str>,
381        rng: &mut R,
382    ) -> Vec<u8> {
383        self.encrypt_blob_content(
384            group_attribute_blob::Content::DescriptionText(
385                description.unwrap_or_default().to_string(),
386            ),
387            rng,
388        )
389    }
390
391    pub fn encrypt_disappearing_messages_timer<
392        R: rand::Rng + rand::CryptoRng,
393    >(
394        &self,
395        timer: Option<&Timer>,
396        rng: &mut R,
397    ) -> Vec<u8> {
398        self.encrypt_blob_content(
399            group_attribute_blob::Content::DisappearingMessagesDuration(
400                timer.map(|t| t.duration).unwrap_or(0),
401            ),
402            rng,
403        )
404    }
405
406    fn decrypt_title(&self, ciphertext: &[u8]) -> String {
407        use group_attribute_blob::Content;
408        match self.decrypt_blob(ciphertext).content {
409            Some(Content::Title(title)) => title,
410            _ => "".into(),
411        }
412    }
413
414    fn decrypt_description_text(&self, ciphertext: &[u8]) -> Option<String> {
415        use group_attribute_blob::Content;
416        match self.decrypt_blob(ciphertext).content {
417            Some(Content::DescriptionText(d)) => {
418                Some(d).filter(|d| !d.is_empty())
419            },
420            _ => None,
421        }
422    }
423
424    fn decrypt_disappearing_messages_timer(
425        &self,
426        ciphertext: &[u8],
427    ) -> Option<Timer> {
428        use group_attribute_blob::Content;
429        match self.decrypt_blob(ciphertext).content {
430            Some(Content::DisappearingMessagesDuration(duration)) => {
431                Some(Timer { duration })
432            },
433            _ => None,
434        }
435    }
436
437    pub fn new(group_secret_params: GroupSecretParams) -> Self {
438        Self {
439            group_secret_params,
440        }
441    }
442
443    pub fn decrypt_group(
444        &self,
445        group: proto::Group,
446    ) -> Result<Group, GroupDecodingError> {
447        // Destructuring to catch any future changes
448        let proto::Group {
449            public_key: _,
450            title,
451            avatar_url,
452            disappearing_messages_timer,
453            access_control,
454            version,
455            members,
456            members_pending_profile_key,
457            members_pending_admin_approval,
458            invite_link_password,
459            description,
460            announcements_only,
461            members_banned,
462        } = group;
463
464        let title = self.decrypt_title(&title);
465
466        let description_text = self.decrypt_description_text(&description);
467
468        let disappearing_messages_timer = self
469            .decrypt_disappearing_messages_timer(&disappearing_messages_timer);
470
471        let members = members
472            .into_iter()
473            .map(|m| self.decrypt_member(m))
474            .collect::<Result<_, _>>()?;
475
476        let members_pending_profile_key = members_pending_profile_key
477            .into_iter()
478            .map(|m| self.decrypt_pending_member(m))
479            .collect::<Result<_, _>>()?;
480
481        let members_pending_admin_approval = members_pending_admin_approval
482            .into_iter()
483            .map(|m| self.decrypt_requesting_member(m))
484            .collect::<Result<_, _>>()?;
485
486        let members_banned = members_banned
487            .into_iter()
488            .map(|m| self.decrypt_banned_member(m))
489            .collect::<Result<_, _>>()?;
490
491        let access_control =
492            access_control.map(TryInto::try_into).transpose()?;
493
494        Ok(Group {
495            title,
496            avatar: avatar_url,
497            disappearing_messages_timer,
498            access_control,
499            version,
500            members,
501            members_pending_profile_key,
502            members_pending_admin_approval,
503            invite_link_password,
504            description_text,
505            announcements_only,
506            members_banned,
507        })
508    }
509
510    pub fn decrypt_group_change(
511        &self,
512        group_change: proto::GroupChange,
513    ) -> Result<GroupChanges, GroupDecodingError> {
514        // Destructuring to catch any future changes
515        let proto::GroupChange {
516            actions,
517            server_signature: _,
518            change_epoch,
519        } = group_change;
520
521        let proto::group_change::Actions {
522            group_id,
523            source_user_id,
524            version,
525            add_members,
526            delete_members,
527            modify_member_roles,
528            modify_member_profile_keys,
529            add_members_pending_profile_key,
530            delete_members_pending_profile_key,
531            promote_members_pending_profile_key,
532            modify_title,
533            modify_avatar,
534            modify_disappearing_message_timer,
535            modify_attributes_access,
536            modify_member_access,
537            modify_add_from_invite_link_access,
538            add_members_pending_admin_approval,
539            delete_members_pending_admin_approval,
540            promote_members_pending_admin_approval,
541            modify_invite_link_password,
542            modify_description,
543            modify_announcements_only,
544            add_members_banned,
545            delete_members_banned,
546            promote_members_pending_pni_aci_profile_key,
547            modify_member_labels,
548            modify_member_label_access,
549        } = Message::decode(Bytes::from(actions))?;
550
551        let source_user_id = self.decrypt_aci(&source_user_id)?;
552
553        let new_members =
554            add_members
555                .into_iter()
556                .filter_map(|m| m.added)
557                .map(|added| {
558                    Ok(GroupChange::NewMember(self.decrypt_member(added)?))
559                });
560
561        let delete_members = delete_members.into_iter().map(|c| {
562            Ok(GroupChange::DeleteMember(
563                self.decrypt_aci(&c.deleted_user_id)?,
564            ))
565        });
566
567        let modify_member_roles = modify_member_roles.into_iter().map(|m| {
568            Ok(GroupChange::ModifyMemberRole {
569                aci: self.decrypt_aci(&m.user_id)?,
570                role: m.role.try_into()?,
571            })
572        });
573
574        let modify_member_profile_keys =
575            modify_member_profile_keys.into_iter().map(|m| {
576                let (aci, profile_key) = self
577                    .decrypt_profile_key_presentation(
578                        &m.user_id,
579                        &m.profile_key,
580                        &m.presentation,
581                    )?;
582                Ok(GroupChange::ModifyMemberProfileKey { aci, profile_key })
583            });
584
585        let add_members_pending_profile_key = add_members_pending_profile_key
586            .into_iter()
587            .filter_map(|m| m.added)
588            .map(|added| {
589                Ok(GroupChange::NewPendingMember(
590                    self.decrypt_pending_member(added)?,
591                ))
592            });
593
594        let delete_members_pending_profile_key =
595            delete_members_pending_profile_key.into_iter().map(|m| {
596                Ok(GroupChange::DeletePendingMember(
597                    self.decrypt_service_id(&m.deleted_user_id)?,
598                ))
599            });
600
601        let promote_members_pending_profile_key =
602            promote_members_pending_profile_key.into_iter().map(|m| {
603                let (aci, profile_key) = self
604                    .decrypt_profile_key_presentation(
605                        &m.user_id,
606                        &m.profile_key,
607                        &m.presentation,
608                    )?;
609                Ok(GroupChange::PromotePendingMember {
610                    address: aci.into(),
611                    profile_key,
612                })
613            });
614
615        let modify_title = modify_title
616            .into_iter()
617            .map(|m| Ok(GroupChange::Title(self.decrypt_title(&m.title))));
618
619        let modify_avatar = modify_avatar
620            .into_iter()
621            .map(|m| Ok(GroupChange::Avatar(m.avatar)));
622
623        let modify_description = modify_description.into_iter().map(|m| {
624            Ok(GroupChange::Description(
625                self.decrypt_description_text(&m.description),
626            ))
627        });
628
629        let modify_disappearing_message_timer =
630            modify_disappearing_message_timer.into_iter().map(|m| {
631                Ok(GroupChange::Timer(
632                    self.decrypt_disappearing_messages_timer(&m.timer),
633                ))
634            });
635
636        let modify_attributes_access =
637            modify_attributes_access.into_iter().map(|m| {
638                Ok(GroupChange::AttributeAccess(
639                    m.attributes_access.try_into()?,
640                ))
641            });
642
643        let modify_member_access = modify_member_access.into_iter().map(|m| {
644            Ok(GroupChange::MemberAccess(m.members_access.try_into()?))
645        });
646
647        let add_members_banned = add_members_banned
648            .into_iter()
649            .filter_map(|m| m.added)
650            .map(|m| {
651                Ok(GroupChange::AddBannedMember(self.decrypt_banned_member(m)?))
652            });
653
654        let delete_members_banned =
655            delete_members_banned.into_iter().map(|m| {
656                Ok(GroupChange::DeleteBannedMember(
657                    self.decrypt_service_id(&m.deleted_user_id)?,
658                ))
659            });
660
661        let promote_members_pending_pni_aci_profile_key =
662            promote_members_pending_pni_aci_profile_key
663                .into_iter()
664                .map(|m| {
665                    let promoted =
666                        self.decrypt_pni_aci_promotion_presentation(&m)?;
667                    Ok(GroupChange::PromotePendingPniAciMemberProfileKey(
668                        promoted,
669                    ))
670                });
671
672        let modify_add_from_invite_link_access =
673            modify_add_from_invite_link_access.into_iter().map(|m| {
674                Ok(GroupChange::InviteLinkAccess(
675                    m.add_from_invite_link_access.try_into()?,
676                ))
677            });
678
679        let add_members_pending_admin_approval =
680            add_members_pending_admin_approval
681                .into_iter()
682                .filter_map(|m| m.added)
683                .map(|added| {
684                    Ok(GroupChange::NewRequestingMember(
685                        self.decrypt_requesting_member(added)?,
686                    ))
687                });
688
689        let delete_members_pending_admin_approval =
690            delete_members_pending_admin_approval.into_iter().map(|m| {
691                Ok(GroupChange::DeleteRequestingMember(
692                    self.decrypt_aci(&m.deleted_user_id)?,
693                ))
694            });
695
696        let promote_members_pending_admin_approval =
697            promote_members_pending_admin_approval.into_iter().map(|m| {
698                Ok(GroupChange::PromoteRequestingMember {
699                    aci: self.decrypt_aci(&m.user_id)?,
700                    role: m.role.try_into()?,
701                })
702            });
703
704        let modify_invite_link_password =
705            modify_invite_link_password.into_iter().map(|m| {
706                Ok(GroupChange::InviteLinkPassword(
707                    BASE64_RELAXED.encode(m.invite_link_password),
708                ))
709            });
710
711        let modify_announcements_only = modify_announcements_only
712            .into_iter()
713            .map(|m| Ok(GroupChange::AnnouncementOnly(m.announcements_only)));
714
715        let modify_member_labels = modify_member_labels.into_iter().map(|m| {
716            Ok(GroupChange::MemberLabel {
717                user_id: self.decrypt_service_id(&m.user_id)?,
718                label_emoji: self.decrypt_member_label_emoji(&m.label_emoji),
719                label_string: self.decrypt_member_label_text(&m.label_string),
720            })
721        });
722
723        let modify_member_label_access =
724            modify_member_label_access.into_iter().map(|m| {
725                Ok(GroupChange::MemberLabelAccess(
726                    m.member_label_access.try_into()?,
727                ))
728            });
729
730        let changes: Result<Vec<GroupChange>, GroupDecodingError> = new_members
731            .chain(delete_members)
732            .chain(modify_member_roles)
733            .chain(modify_member_profile_keys)
734            .chain(add_members_pending_profile_key)
735            .chain(delete_members_pending_profile_key)
736            .chain(promote_members_pending_profile_key)
737            .chain(modify_title)
738            .chain(modify_avatar)
739            .chain(modify_disappearing_message_timer)
740            .chain(modify_attributes_access)
741            .chain(modify_description)
742            .chain(modify_member_access)
743            .chain(add_members_banned)
744            .chain(delete_members_banned)
745            .chain(promote_members_pending_pni_aci_profile_key)
746            .chain(modify_add_from_invite_link_access)
747            .chain(add_members_pending_admin_approval)
748            .chain(delete_members_pending_admin_approval)
749            .chain(promote_members_pending_admin_approval)
750            .chain(modify_invite_link_password)
751            .chain(modify_announcements_only)
752            .chain(modify_member_labels)
753            .chain(modify_member_label_access)
754            .collect();
755
756        Ok(GroupChanges {
757            group_id: group_id
758                .try_into()
759                .map_err(|_| GroupDecodingError::WrongBlob)?,
760            editor: source_user_id,
761            version,
762            changes: changes?,
763            change_epoch,
764        })
765    }
766
767    pub fn decrypt_avatar(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
768        use group_attribute_blob::Content;
769        match self.decrypt_blob(ciphertext).content {
770            Some(Content::Avatar(d)) => Some(d).filter(|d| !d.is_empty()),
771            _ => None,
772        }
773    }
774
775    /// Build an AddMemberAction for a GroupChange.
776    ///
777    /// # Role Parameter
778    ///
779    /// The `role` parameter is accepted for API consistency, but note that
780    /// Signal-Android only ever adds members with `Role::Default`. Adding a member
781    /// with `Role::Administrator` is an illegal operation that the server will reject.
782    /// Promotion to administrator requires a separate `ModifyMemberRoleAction` after
783    /// the member has been added.
784    ///
785    /// See Signal-Android's `GroupsV2Operations.GroupOperations.createModifyGroupMembershipChange()`
786    /// which hardcodes `Member.Role newMemberRole = Member.Role.DEFAULT`.
787    pub fn build_add_member_action(
788        &self,
789        aci: Aci,
790        profile_key: ProfileKey,
791        role: super::model::Role,
792    ) -> Result<proto::group_change::actions::AddMemberAction, GroupDecodingError>
793    {
794        Ok(proto::group_change::actions::AddMemberAction {
795            added: Some(proto::Member {
796                user_id: self.encrypt_aci(aci)?,
797                profile_key: self.encrypt_profile_key(profile_key, aci)?,
798                presentation: vec![],
799                role: role.into(),
800                joined_at_version: 0, // Set by server
801                // XXX: should these be exposed?
802                label_emoji: vec![],
803                label_string: vec![],
804            }),
805            join_from_invite_link: false,
806        })
807    }
808
809    /// Build a DeleteMemberAction for a GroupChange
810    pub fn build_remove_member_action(
811        &self,
812        aci: Aci,
813    ) -> Result<
814        proto::group_change::actions::DeleteMemberAction,
815        GroupDecodingError,
816    > {
817        Ok(proto::group_change::actions::DeleteMemberAction {
818            deleted_user_id: self.encrypt_aci(aci)?,
819        })
820    }
821
822    /// Build a DeletePendingMemberAction to retract an outstanding invitation.
823    ///
824    /// Used when a pending member (invite not yet accepted) is to be removed.
825    /// The `invitee` may be ACI or PNI — whichever service ID was used when
826    /// the invite was originally created.  The Signal server stores and matches
827    /// on the encrypted `user_id` field of `PendingMember`, which may be
828    /// either kind of `ServiceId`.
829    pub fn build_remove_pending_member_action(
830        &self,
831        invitee: ServiceId,
832    ) -> Result<
833        proto::group_change::actions::DeleteMemberPendingProfileKeyAction,
834        GroupDecodingError,
835    > {
836        Ok(
837            proto::group_change::actions::DeleteMemberPendingProfileKeyAction {
838                deleted_user_id: self.encrypt_service_id(invitee)?,
839            },
840        )
841    }
842
843    /// Create a presentation from a credential for adding a member to a group.
844    ///
845    /// This creates a ZK proof (ExpiringProfileKeyCredentialPresentation) that the
846    /// Signal server can verify to validate the member's identity and profile key.
847    ///
848    /// # Presentation protocol version for `ExpiringProfileKeyCredentialPresentation` ZK proofs.
849    ///
850    /// This is the version number sent as a const generic parameter to
851    /// `create_expiring_profile_key_credential_presentation`. It must match the
852    /// version expected by the Signal server's zkgroup verification logic.
853    ///
854    /// - Current default value: `PRESENTATION_VERSION_3` (raw value `2`), which is also
855    ///   the default type parameter for `ExpiringProfileKeyCredentialPresentation`
856    ///   in libsignal's zkgroup API.
857    /// - To check the default, look at libsignal's zkgroup source:
858    ///   `rust/zkgroup/src/api/profiles/profile_key_credential_presentation.rs` —
859    ///   `ExpiringProfileKeyCredentialPresentation<const V: u8 = PRESENTATION_VERSION_3>`.
860    // NOTE: Do NOT automatically bump this to the latest version (e.g.
861    // `PRESENTATION_VERSION_4`) without verifying that the Signal server accepts
862    // it. A mismatched version will cause ZK proof verification to fail and
863    // members will be rejected when joining groups.
864    pub fn create_member_presentation<const V: u8>(
865        &self,
866        server_public_params: &ServerPublicParams,
867        credential: &ExpiringProfileKeyCredential,
868    ) -> Vec<u8> {
869        let randomness: [u8; 32] = rand::random();
870        let presentation = server_public_params
871            .create_expiring_profile_key_credential_presentation::<V>(
872                randomness,
873                self.group_secret_params,
874                *credential,
875            );
876        zkgroup::serialize(&presentation)
877    }
878
879    /// Encrypt a group for creation, using credentials for member presentations.
880    ///
881    /// This method properly populates the `presentation` field for members with
882    /// credentials, which is required by the Signal server for group creation.
883    ///
884    /// Members with credentials get added with presentations (full members).
885    /// Members without credentials get added as pending invites.
886    ///
887    /// # Arguments
888    /// * `title` - The group title
889    /// * `description` - Optional group description
890    /// * `disappearing_messages_timer` - Optional disappearing messages timer
891    /// * `access_control` - Optional access control settings
892    /// * `self_credential` - The creator's own credential (required)
893    /// * `avatar_url` - The group avatar URL
894    /// * `member_candidates` - Other members to add, with optional credentials
895    /// * `server_public_params` - Server public params for creating presentations
896    /// * `rng` - Random number generator
897    #[allow(clippy::too_many_arguments)]
898    pub fn encrypt_group_with_credentials<R: rand::Rng + rand::CryptoRng>(
899        &self,
900        title: &str,
901        description: Option<&str>,
902        disappearing_messages_timer: Option<&Timer>,
903        access_control: Option<&AccessControl>,
904        self_credential: &ExpiringProfileKeyCredential,
905        member_candidates: &[GroupMemberCandidate],
906        server_public_params: &ServerPublicParams,
907        avatar_url: String,
908        rng: &mut R,
909    ) -> Result<proto::Group, GroupDecodingError> {
910        let mut members = Vec::new();
911        let mut members_pending_profile_key = Vec::new();
912
913        // Add self as administrator with presentation
914        let self_presentation = self
915            .create_member_presentation::<PRESENTATION_VERSION_3>(
916                server_public_params,
917                self_credential,
918            );
919        members.push(proto::Member {
920            user_id: vec![],     // Server extracts from presentation
921            profile_key: vec![], // Server extracts from presentation
922            presentation: self_presentation,
923            role: proto::member::Role::Administrator.into(),
924            joined_at_version: 0,
925            label_emoji: vec![],
926            label_string: vec![],
927        });
928
929        // Add other members
930        for candidate in member_candidates {
931            if let Some(credential) = &candidate.credential {
932                // Has credential - add as full member with presentation
933                let presentation = self
934                    .create_member_presentation::<PRESENTATION_VERSION_3>(
935                        server_public_params,
936                        credential,
937                    );
938                members.push(proto::Member {
939                    user_id: vec![],
940                    profile_key: vec![],
941                    presentation,
942                    role: proto::member::Role::Default.into(),
943                    joined_at_version: 0,
944                    label_emoji: vec![],
945                    label_string: vec![],
946                });
947            } else {
948                // No credential - add as pending invite
949                let user_id_ciphertext =
950                    self.encrypt_service_id(candidate.service_id)?;
951                let self_aci = self_credential.aci();
952                members_pending_profile_key.push(
953                    proto::MemberPendingProfileKey {
954                        member: Some(proto::Member {
955                            user_id: user_id_ciphertext,
956                            profile_key: vec![],
957                            presentation: vec![],
958                            role: proto::member::Role::Default.into(),
959                            joined_at_version: 0,
960                            label_emoji: vec![],
961                            label_string: vec![],
962                        }),
963                        added_by_user_id: self.encrypt_aci(self_aci)?,
964                        timestamp: 0, // Server sets
965                    },
966                );
967            }
968        }
969
970        // Encrypt title, description, timer
971        let encrypted_title = self.encrypt_title(title, rng);
972        let encrypted_description = self.encrypt_description(description, rng);
973        let encrypted_timer = self.encrypt_disappearing_messages_timer(
974            disappearing_messages_timer,
975            rng,
976        );
977
978        // Convert access control
979        let proto_access_control =
980            access_control.map(|ac| proto::AccessControl {
981                attributes: ac.attributes.into(),
982                members: ac.members.into(),
983                add_from_invite_link: ac.add_from_invite_link.into(),
984                member_label: ac.member_label.into(),
985            });
986
987        Ok(proto::Group {
988            public_key: zkgroup::serialize(
989                &self.group_secret_params.get_public_params(),
990            ),
991            title: encrypted_title,
992            avatar_url,
993            disappearing_messages_timer: encrypted_timer,
994            access_control: proto_access_control,
995            version: 0,
996            members,
997            members_pending_profile_key,
998            members_pending_admin_approval: vec![],
999            invite_link_password: vec![],
1000            description: encrypted_description,
1001            announcements_only: false,
1002            members_banned: vec![],
1003        })
1004    }
1005
1006    /// Build an AddMemberAction with a credential presentation for a GroupChange.
1007    ///
1008    /// This is used when adding members to an existing group with proper ZK proofs.
1009    ///
1010    /// # Role Parameter
1011    ///
1012    /// The `role` parameter is accepted for API consistency, but note that
1013    /// Signal-Android only ever adds members with `Role::Default`. Adding a member
1014    /// with `Role::Administrator` is an illegal operation that the server will reject.
1015    /// Promotion to administrator requires a separate `ModifyMemberRoleAction` after
1016    /// the member has been added.
1017    pub fn build_add_member_action_with_credential(
1018        &self,
1019        credential: &ExpiringProfileKeyCredential,
1020        role: super::model::Role,
1021        server_public_params: &ServerPublicParams,
1022    ) -> proto::group_change::actions::AddMemberAction {
1023        let presentation = self
1024            .create_member_presentation::<PRESENTATION_VERSION_3>(
1025                server_public_params,
1026                credential,
1027            );
1028        proto::group_change::actions::AddMemberAction {
1029            added: Some(proto::Member {
1030                user_id: vec![],     // Server extracts from presentation
1031                profile_key: vec![], // Server extracts from presentation
1032                presentation,
1033                role: role.into(),
1034                joined_at_version: 0, // Set by server
1035                label_emoji: vec![],
1036                label_string: vec![],
1037            }),
1038            join_from_invite_link: false,
1039        }
1040    }
1041
1042    /// Build an AddPendingMemberAction to invite a member without their profile key.
1043    ///
1044    /// This adds the member as a pending invite. They will receive a group invite
1045    /// notification and must accept to become a full member. No profile key is needed.
1046    ///
1047    /// The `invitee` may be either an ACI or a PNI. When only a PNI is known (e.g.
1048    /// the invitee has ACI disclosure disabled in CDSI), passing `ServiceId::Pni`
1049    /// allows the pending-invite path to proceed without an ACI. The Signal server
1050    /// stores whichever service ID is provided in the encrypted `user_id` field of
1051    /// the `PendingMember` proto. The `added_by_aci` must always be an ACI.
1052    ///
1053    /// # Role Parameter
1054    ///
1055    /// The `role` parameter is accepted for API consistency, but note that
1056    /// Signal-Android only ever adds pending members with `Role::Default`.
1057    pub fn build_add_pending_member_action(
1058        &self,
1059        invitee: ServiceId,
1060        added_by_aci: Aci,
1061        role: super::model::Role,
1062    ) -> Result<
1063        proto::group_change::actions::AddMemberPendingProfileKeyAction,
1064        GroupDecodingError,
1065    > {
1066        Ok(
1067            proto::group_change::actions::AddMemberPendingProfileKeyAction {
1068                added: Some(proto::MemberPendingProfileKey {
1069                    member: Some(proto::Member {
1070                        user_id: self.encrypt_service_id(invitee)?,
1071                        profile_key: vec![],
1072                        presentation: vec![],
1073                        role: role.into(),
1074                        joined_at_version: 0,
1075                        label_emoji: vec![],
1076                        label_string: vec![],
1077                    }),
1078                    added_by_user_id: self.encrypt_aci(added_by_aci)?,
1079                    timestamp: 0, // Server sets
1080                }),
1081            },
1082        )
1083    }
1084}
1085
1086#[cfg(test)]
1087mod tests {
1088    use super::*;
1089
1090    use rand::RngCore;
1091    use zkgroup::groups::GroupMasterKey;
1092
1093    fn create_group_operations() -> GroupOperations {
1094        // Create a test group master key (32 bytes)
1095        let master_key_bytes = [
1096            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
1097            0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
1098            0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
1099        ];
1100        let group_master_key = GroupMasterKey::new(master_key_bytes);
1101        let group_secret_params =
1102            GroupSecretParams::derive_from_master_key(group_master_key);
1103        GroupOperations::new(group_secret_params)
1104    }
1105
1106    #[test]
1107    fn roundtrip_title() {
1108        let ops = create_group_operations();
1109        let mut rng = rand::rng();
1110
1111        let title = "Test Group Title";
1112        let encrypted = ops.encrypt_title(title, &mut rng);
1113        let decrypted = ops.decrypt_title(&encrypted);
1114        assert_eq!(decrypted, title);
1115    }
1116
1117    #[test]
1118    fn roundtrip_description() {
1119        let ops = create_group_operations();
1120        let mut rng = rand::rng();
1121
1122        let description = "This is a test group description";
1123        let encrypted = ops.encrypt_description(Some(description), &mut rng);
1124        let decrypted = ops.decrypt_description_text(&encrypted);
1125        assert_eq!(decrypted, Some(description.to_string()));
1126    }
1127
1128    #[test]
1129    fn roundtrip_member_label() {
1130        let ops = create_group_operations();
1131        let mut rng = rand::rng();
1132
1133        let label = "Whisperfish / rubdos";
1134        let mut randomness = [0u8; 32];
1135        rng.fill_bytes(&mut randomness);
1136        let encrypted = ops.group_secret_params.encrypt_blob_with_padding(
1137            randomness,
1138            label.as_bytes(),
1139            0,
1140        );
1141
1142        assert_eq!(
1143            ops.decrypt_member_label_text(&encrypted),
1144            Some(label.to_string())
1145        );
1146    }
1147
1148    #[test]
1149    fn roundtrip_disappearing_message_timer() {
1150        let ops = create_group_operations();
1151        let mut rng = rand::rng();
1152
1153        let timer = Timer { duration: 3600 };
1154        let encrypted =
1155            ops.encrypt_disappearing_messages_timer(Some(&timer), &mut rng);
1156        let decrypted = ops.decrypt_disappearing_messages_timer(&encrypted);
1157        assert_eq!(decrypted, Some(timer));
1158    }
1159
1160    #[test]
1161    fn roundtrip_aci_encryption() {
1162        let ops = create_group_operations();
1163
1164        // Use a known ACI string (UUID format from existing test patterns)
1165        let aci = Aci::parse_from_service_id_string(
1166            "550e8400-e29b-41d4-a716-446655440000",
1167        )
1168        .expect("valid ACI");
1169        let encrypted =
1170            ops.encrypt_aci(aci).expect("encrypt_aci should succeed");
1171        let decrypted = ops
1172            .decrypt_aci(&encrypted)
1173            .expect("decrypt_aci should succeed");
1174        assert_eq!(decrypted, aci);
1175    }
1176
1177    #[test]
1178    fn roundtrip_service_id_encryption() {
1179        let ops = create_group_operations();
1180
1181        // Use a known UUID string for the service ID
1182        let service_id: ServiceId = ServiceId::parse_from_service_id_string(
1183            "550e8400-e29b-41d4-a716-446655440000",
1184        )
1185        .expect("valid service ID");
1186        let encrypted = ops
1187            .encrypt_service_id(service_id)
1188            .expect("encrypt_service_id should succeed");
1189        let decrypted = ops
1190            .decrypt_service_id(&encrypted)
1191            .expect("decrypt_service_id should succeed");
1192        assert_eq!(decrypted, service_id);
1193    }
1194
1195    #[test]
1196    fn roundtrip_service_id_pni_encryption() {
1197        let ops = create_group_operations();
1198
1199        // Use a known UUID string for the service ID
1200        let service_id: ServiceId = ServiceId::parse_from_service_id_string(
1201            "PNI:550e8400-e29b-41d4-a716-446655440000",
1202        )
1203        .expect("valid service ID");
1204        let encrypted = ops
1205            .encrypt_service_id(service_id)
1206            .expect("encrypt_service_id should succeed");
1207        let decrypted = ops
1208            .decrypt_service_id(&encrypted)
1209            .expect("decrypt_service_id should succeed");
1210        assert_eq!(decrypted, service_id);
1211    }
1212
1213    #[test]
1214    fn encrypt_title_different_each_time() {
1215        let ops = create_group_operations();
1216        let mut rng = rand::rng();
1217
1218        let title = "Test Title";
1219        let encrypted1 = ops.encrypt_title(title, &mut rng);
1220        let encrypted2 = ops.encrypt_title(title, &mut rng);
1221
1222        // Same plaintext should produce different ciphertext due to random padding
1223        // but both should decrypt to the same value
1224        assert_ne!(encrypted1, encrypted2);
1225        assert_eq!(ops.decrypt_title(&encrypted1), title);
1226        assert_eq!(ops.decrypt_title(&encrypted2), title);
1227    }
1228}