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,
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            terminated,
463        } = group;
464
465        let title = self.decrypt_title(&title);
466
467        let description_text = self.decrypt_description_text(&description);
468
469        let disappearing_messages_timer = self
470            .decrypt_disappearing_messages_timer(&disappearing_messages_timer);
471
472        let members = members
473            .into_iter()
474            .map(|m| self.decrypt_member(m))
475            .collect::<Result<_, _>>()?;
476
477        let members_pending_profile_key = members_pending_profile_key
478            .into_iter()
479            .map(|m| self.decrypt_pending_member(m))
480            .collect::<Result<_, _>>()?;
481
482        let members_pending_admin_approval = members_pending_admin_approval
483            .into_iter()
484            .map(|m| self.decrypt_requesting_member(m))
485            .collect::<Result<_, _>>()?;
486
487        let members_banned = members_banned
488            .into_iter()
489            .map(|m| self.decrypt_banned_member(m))
490            .collect::<Result<_, _>>()?;
491
492        let access_control =
493            access_control.map(TryInto::try_into).transpose()?;
494
495        Ok(Group {
496            title,
497            avatar: avatar_url,
498            disappearing_messages_timer,
499            access_control,
500            version,
501            members,
502            members_pending_profile_key,
503            members_pending_admin_approval,
504            invite_link_password,
505            description_text,
506            announcements_only,
507            members_banned,
508            terminated,
509        })
510    }
511
512    pub fn decrypt_group_change(
513        &self,
514        group_change: proto::GroupChange,
515    ) -> Result<GroupChanges, GroupDecodingError> {
516        // Destructuring to catch any future changes
517        let proto::GroupChange {
518            actions,
519            server_signature: _,
520            change_epoch,
521        } = group_change;
522
523        let proto::group_change::Actions {
524            group_id,
525            source_user_id,
526            version,
527            add_members,
528            delete_members,
529            modify_member_roles,
530            modify_member_profile_keys,
531            add_members_pending_profile_key,
532            delete_members_pending_profile_key,
533            promote_members_pending_profile_key,
534            modify_title,
535            modify_avatar,
536            modify_disappearing_message_timer,
537            modify_attributes_access,
538            modify_member_access,
539            modify_add_from_invite_link_access,
540            add_members_pending_admin_approval,
541            delete_members_pending_admin_approval,
542            promote_members_pending_admin_approval,
543            modify_invite_link_password,
544            modify_description,
545            modify_announcements_only,
546            add_members_banned,
547            delete_members_banned,
548            promote_members_pending_pni_aci_profile_key,
549            modify_member_labels,
550            modify_member_label_access,
551            terminate_group,
552        } = Message::decode(Bytes::from(actions))?;
553
554        let source_user_id = self.decrypt_aci(&source_user_id)?;
555
556        let new_members =
557            add_members
558                .into_iter()
559                .filter_map(|m| m.added)
560                .map(|added| {
561                    Ok(GroupChange::NewMember(self.decrypt_member(added)?))
562                });
563
564        let delete_members = delete_members.into_iter().map(|c| {
565            Ok(GroupChange::DeleteMember(
566                self.decrypt_aci(&c.deleted_user_id)?,
567            ))
568        });
569
570        let modify_member_roles = modify_member_roles.into_iter().map(|m| {
571            Ok(GroupChange::ModifyMemberRole {
572                aci: self.decrypt_aci(&m.user_id)?,
573                role: m.role.try_into()?,
574            })
575        });
576
577        let modify_member_profile_keys =
578            modify_member_profile_keys.into_iter().map(|m| {
579                let (aci, profile_key) = self
580                    .decrypt_profile_key_presentation(
581                        &m.user_id,
582                        &m.profile_key,
583                        &m.presentation,
584                    )?;
585                Ok(GroupChange::ModifyMemberProfileKey { aci, profile_key })
586            });
587
588        let add_members_pending_profile_key = add_members_pending_profile_key
589            .into_iter()
590            .filter_map(|m| m.added)
591            .map(|added| {
592                Ok(GroupChange::NewPendingMember(
593                    self.decrypt_pending_member(added)?,
594                ))
595            });
596
597        let delete_members_pending_profile_key =
598            delete_members_pending_profile_key.into_iter().map(|m| {
599                Ok(GroupChange::DeletePendingMember(
600                    self.decrypt_service_id(&m.deleted_user_id)?,
601                ))
602            });
603
604        let promote_members_pending_profile_key =
605            promote_members_pending_profile_key.into_iter().map(|m| {
606                let (aci, profile_key) = self
607                    .decrypt_profile_key_presentation(
608                        &m.user_id,
609                        &m.profile_key,
610                        &m.presentation,
611                    )?;
612                Ok(GroupChange::PromotePendingMember {
613                    address: aci.into(),
614                    profile_key,
615                })
616            });
617
618        let modify_title = modify_title
619            .into_iter()
620            .map(|m| Ok(GroupChange::Title(self.decrypt_title(&m.title))));
621
622        let modify_avatar = modify_avatar
623            .into_iter()
624            .map(|m| Ok(GroupChange::Avatar(m.avatar)));
625
626        let modify_description = modify_description.into_iter().map(|m| {
627            Ok(GroupChange::Description(
628                self.decrypt_description_text(&m.description),
629            ))
630        });
631
632        let modify_disappearing_message_timer =
633            modify_disappearing_message_timer.into_iter().map(|m| {
634                Ok(GroupChange::Timer(
635                    self.decrypt_disappearing_messages_timer(&m.timer),
636                ))
637            });
638
639        let modify_attributes_access =
640            modify_attributes_access.into_iter().map(|m| {
641                Ok(GroupChange::AttributeAccess(
642                    m.attributes_access.try_into()?,
643                ))
644            });
645
646        let modify_member_access = modify_member_access.into_iter().map(|m| {
647            Ok(GroupChange::MemberAccess(m.members_access.try_into()?))
648        });
649
650        let add_members_banned = add_members_banned
651            .into_iter()
652            .filter_map(|m| m.added)
653            .map(|m| {
654                Ok(GroupChange::AddBannedMember(self.decrypt_banned_member(m)?))
655            });
656
657        let delete_members_banned =
658            delete_members_banned.into_iter().map(|m| {
659                Ok(GroupChange::DeleteBannedMember(
660                    self.decrypt_service_id(&m.deleted_user_id)?,
661                ))
662            });
663
664        let promote_members_pending_pni_aci_profile_key =
665            promote_members_pending_pni_aci_profile_key
666                .into_iter()
667                .map(|m| {
668                    let promoted =
669                        self.decrypt_pni_aci_promotion_presentation(&m)?;
670                    Ok(GroupChange::PromotePendingPniAciMemberProfileKey(
671                        promoted,
672                    ))
673                });
674
675        let modify_add_from_invite_link_access =
676            modify_add_from_invite_link_access.into_iter().map(|m| {
677                Ok(GroupChange::InviteLinkAccess(
678                    m.add_from_invite_link_access.try_into()?,
679                ))
680            });
681
682        let add_members_pending_admin_approval =
683            add_members_pending_admin_approval
684                .into_iter()
685                .filter_map(|m| m.added)
686                .map(|added| {
687                    Ok(GroupChange::NewRequestingMember(
688                        self.decrypt_requesting_member(added)?,
689                    ))
690                });
691
692        let delete_members_pending_admin_approval =
693            delete_members_pending_admin_approval.into_iter().map(|m| {
694                Ok(GroupChange::DeleteRequestingMember(
695                    self.decrypt_aci(&m.deleted_user_id)?,
696                ))
697            });
698
699        let promote_members_pending_admin_approval =
700            promote_members_pending_admin_approval.into_iter().map(|m| {
701                Ok(GroupChange::PromoteRequestingMember {
702                    aci: self.decrypt_aci(&m.user_id)?,
703                    role: m.role.try_into()?,
704                })
705            });
706
707        let modify_invite_link_password =
708            modify_invite_link_password.into_iter().map(|m| {
709                Ok(GroupChange::InviteLinkPassword(
710                    BASE64_RELAXED.encode(m.invite_link_password),
711                ))
712            });
713
714        let modify_announcements_only = modify_announcements_only
715            .into_iter()
716            .map(|m| Ok(GroupChange::AnnouncementOnly(m.announcements_only)));
717
718        let modify_member_labels = modify_member_labels.into_iter().map(|m| {
719            Ok(GroupChange::MemberLabel {
720                user_id: self.decrypt_service_id(&m.user_id)?,
721                label_emoji: self.decrypt_member_label_emoji(&m.label_emoji),
722                label_string: self.decrypt_member_label_text(&m.label_string),
723            })
724        });
725
726        let modify_member_label_access =
727            modify_member_label_access.into_iter().map(|m| {
728                Ok(GroupChange::MemberLabelAccess(
729                    m.member_label_access.try_into()?,
730                ))
731            });
732
733        let terminate_group = terminate_group.into_iter().map(|_m| {
734            // The *presence* of the field signifies termination.
735            // TerminateGroupAction is a unit struct.
736            Ok(GroupChange::TerminateGroup)
737        });
738
739        let changes: Result<Vec<GroupChange>, GroupDecodingError> = new_members
740            .chain(delete_members)
741            .chain(modify_member_roles)
742            .chain(modify_member_profile_keys)
743            .chain(add_members_pending_profile_key)
744            .chain(delete_members_pending_profile_key)
745            .chain(promote_members_pending_profile_key)
746            .chain(modify_title)
747            .chain(modify_avatar)
748            .chain(modify_disappearing_message_timer)
749            .chain(modify_attributes_access)
750            .chain(modify_description)
751            .chain(modify_member_access)
752            .chain(add_members_banned)
753            .chain(delete_members_banned)
754            .chain(promote_members_pending_pni_aci_profile_key)
755            .chain(modify_add_from_invite_link_access)
756            .chain(add_members_pending_admin_approval)
757            .chain(delete_members_pending_admin_approval)
758            .chain(promote_members_pending_admin_approval)
759            .chain(modify_invite_link_password)
760            .chain(modify_announcements_only)
761            .chain(modify_member_labels)
762            .chain(modify_member_label_access)
763            .chain(terminate_group)
764            .collect();
765
766        Ok(GroupChanges {
767            group_id: group_id
768                .try_into()
769                .map_err(|_| GroupDecodingError::WrongBlob)?,
770            editor: source_user_id,
771            version,
772            changes: changes?,
773            change_epoch,
774        })
775    }
776
777    pub fn decrypt_avatar(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
778        use group_attribute_blob::Content;
779        match self.decrypt_blob(ciphertext).content {
780            Some(Content::Avatar(d)) => Some(d).filter(|d| !d.is_empty()),
781            _ => None,
782        }
783    }
784
785    /// Build an AddMemberAction for a GroupChange.
786    ///
787    /// # Role Parameter
788    ///
789    /// The `role` parameter is accepted for API consistency, but note that
790    /// Signal-Android only ever adds members with `Role::Default`. Adding a member
791    /// with `Role::Administrator` is an illegal operation that the server will reject.
792    /// Promotion to administrator requires a separate `ModifyMemberRoleAction` after
793    /// the member has been added.
794    ///
795    /// See Signal-Android's `GroupsV2Operations.GroupOperations.createModifyGroupMembershipChange()`
796    /// which hardcodes `Member.Role newMemberRole = Member.Role.DEFAULT`.
797    pub fn build_add_member_action(
798        &self,
799        aci: Aci,
800        profile_key: ProfileKey,
801        role: super::model::Role,
802    ) -> Result<proto::group_change::actions::AddMemberAction, GroupDecodingError>
803    {
804        Ok(proto::group_change::actions::AddMemberAction {
805            added: Some(proto::Member {
806                user_id: self.encrypt_aci(aci)?,
807                profile_key: self.encrypt_profile_key(profile_key, aci)?,
808                presentation: vec![],
809                role: role.into(),
810                joined_at_version: 0, // Set by server
811                // XXX: should these be exposed?
812                label_emoji: vec![],
813                label_string: vec![],
814            }),
815            join_from_invite_link: false,
816        })
817    }
818
819    /// Build a DeleteMemberAction for a GroupChange
820    pub fn build_remove_member_action(
821        &self,
822        aci: Aci,
823    ) -> Result<
824        proto::group_change::actions::DeleteMemberAction,
825        GroupDecodingError,
826    > {
827        Ok(proto::group_change::actions::DeleteMemberAction {
828            deleted_user_id: self.encrypt_aci(aci)?,
829        })
830    }
831
832    /// Build a DeletePendingMemberAction to retract an outstanding invitation.
833    ///
834    /// Used when a pending member (invite not yet accepted) is to be removed.
835    /// The `invitee` may be ACI or PNI — whichever service ID was used when
836    /// the invite was originally created.  The Signal server stores and matches
837    /// on the encrypted `user_id` field of `PendingMember`, which may be
838    /// either kind of `ServiceId`.
839    pub fn build_remove_pending_member_action(
840        &self,
841        invitee: ServiceId,
842    ) -> Result<
843        proto::group_change::actions::DeleteMemberPendingProfileKeyAction,
844        GroupDecodingError,
845    > {
846        Ok(
847            proto::group_change::actions::DeleteMemberPendingProfileKeyAction {
848                deleted_user_id: self.encrypt_service_id(invitee)?,
849            },
850        )
851    }
852
853    /// Create a presentation from a credential for adding a member to a group.
854    ///
855    /// This creates a ZK proof (ExpiringProfileKeyCredentialPresentation) that the
856    /// Signal server can verify to validate the member's identity and profile key.
857    pub fn create_member_presentation(
858        &self,
859        server_public_params: &ServerPublicParams,
860        credential: &ExpiringProfileKeyCredential,
861    ) -> Vec<u8> {
862        let randomness: [u8; 32] = rand::random();
863        let presentation = server_public_params
864            .create_expiring_profile_key_credential_presentation(
865                randomness,
866                self.group_secret_params,
867                *credential,
868            );
869        zkgroup::serialize(&presentation)
870    }
871
872    /// Encrypt a group for creation, using credentials for member presentations.
873    ///
874    /// This method properly populates the `presentation` field for members with
875    /// credentials, which is required by the Signal server for group creation.
876    ///
877    /// Members with credentials get added with presentations (full members).
878    /// Members without credentials get added as pending invites.
879    ///
880    /// # Arguments
881    /// * `title` - The group title
882    /// * `description` - Optional group description
883    /// * `disappearing_messages_timer` - Optional disappearing messages timer
884    /// * `access_control` - Optional access control settings
885    /// * `self_credential` - The creator's own credential (required)
886    /// * `avatar_url` - The group avatar URL
887    /// * `member_candidates` - Other members to add, with optional credentials
888    /// * `server_public_params` - Server public params for creating presentations
889    /// * `rng` - Random number generator
890    #[allow(clippy::too_many_arguments)]
891    pub fn encrypt_group_with_credentials<R: rand::Rng + rand::CryptoRng>(
892        &self,
893        title: &str,
894        description: Option<&str>,
895        disappearing_messages_timer: Option<&Timer>,
896        access_control: Option<&AccessControl>,
897        self_credential: &ExpiringProfileKeyCredential,
898        member_candidates: &[GroupMemberCandidate],
899        server_public_params: &ServerPublicParams,
900        avatar_url: String,
901        rng: &mut R,
902    ) -> Result<proto::Group, GroupDecodingError> {
903        let mut members = Vec::new();
904        let mut members_pending_profile_key = Vec::new();
905
906        // Add self as administrator with presentation
907        let self_presentation = self
908            .create_member_presentation(server_public_params, self_credential);
909        members.push(proto::Member {
910            user_id: vec![],     // Server extracts from presentation
911            profile_key: vec![], // Server extracts from presentation
912            presentation: self_presentation,
913            role: proto::member::Role::Administrator.into(),
914            joined_at_version: 0,
915            label_emoji: vec![],
916            label_string: vec![],
917        });
918
919        // Add other members
920        for candidate in member_candidates {
921            if let Some(credential) = &candidate.credential {
922                // Has credential - add as full member with presentation
923                let presentation = self.create_member_presentation(
924                    server_public_params,
925                    credential,
926                );
927                members.push(proto::Member {
928                    user_id: vec![],
929                    profile_key: vec![],
930                    presentation,
931                    role: proto::member::Role::Default.into(),
932                    joined_at_version: 0,
933                    label_emoji: vec![],
934                    label_string: vec![],
935                });
936            } else {
937                // No credential - add as pending invite
938                let user_id_ciphertext =
939                    self.encrypt_service_id(candidate.service_id)?;
940                let self_aci = self_credential.aci();
941                members_pending_profile_key.push(
942                    proto::MemberPendingProfileKey {
943                        member: Some(proto::Member {
944                            user_id: user_id_ciphertext,
945                            profile_key: vec![],
946                            presentation: vec![],
947                            role: proto::member::Role::Default.into(),
948                            joined_at_version: 0,
949                            label_emoji: vec![],
950                            label_string: vec![],
951                        }),
952                        added_by_user_id: self.encrypt_aci(self_aci)?,
953                        timestamp: 0, // Server sets
954                    },
955                );
956            }
957        }
958
959        // Encrypt title, description, timer
960        let encrypted_title = self.encrypt_title(title, rng);
961        let encrypted_description = self.encrypt_description(description, rng);
962        let encrypted_timer = self.encrypt_disappearing_messages_timer(
963            disappearing_messages_timer,
964            rng,
965        );
966
967        // Convert access control
968        let proto_access_control =
969            access_control.map(|ac| proto::AccessControl {
970                attributes: ac.attributes.into(),
971                members: ac.members.into(),
972                add_from_invite_link: ac.add_from_invite_link.into(),
973                member_label: ac.member_label.into(),
974            });
975
976        Ok(proto::Group {
977            public_key: zkgroup::serialize(
978                &self.group_secret_params.get_public_params(),
979            ),
980            title: encrypted_title,
981            avatar_url,
982            disappearing_messages_timer: encrypted_timer,
983            access_control: proto_access_control,
984            version: 0,
985            members,
986            members_pending_profile_key,
987            members_pending_admin_approval: vec![],
988            invite_link_password: vec![],
989            description: encrypted_description,
990            announcements_only: false,
991            members_banned: vec![],
992            terminated: false,
993        })
994    }
995
996    /// Build an AddMemberAction with a credential presentation for a GroupChange.
997    ///
998    /// This is used when adding members to an existing group with proper ZK proofs.
999    ///
1000    /// # Role Parameter
1001    ///
1002    /// The `role` parameter is accepted for API consistency, but note that
1003    /// Signal-Android only ever adds members with `Role::Default`. Adding a member
1004    /// with `Role::Administrator` is an illegal operation that the server will reject.
1005    /// Promotion to administrator requires a separate `ModifyMemberRoleAction` after
1006    /// the member has been added.
1007    pub fn build_add_member_action_with_credential(
1008        &self,
1009        credential: &ExpiringProfileKeyCredential,
1010        role: super::model::Role,
1011        server_public_params: &ServerPublicParams,
1012    ) -> proto::group_change::actions::AddMemberAction {
1013        let presentation =
1014            self.create_member_presentation(server_public_params, credential);
1015        proto::group_change::actions::AddMemberAction {
1016            added: Some(proto::Member {
1017                user_id: vec![],     // Server extracts from presentation
1018                profile_key: vec![], // Server extracts from presentation
1019                presentation,
1020                role: role.into(),
1021                joined_at_version: 0, // Set by server
1022                label_emoji: vec![],
1023                label_string: vec![],
1024            }),
1025            join_from_invite_link: false,
1026        }
1027    }
1028
1029    /// Build an AddPendingMemberAction to invite a member without their profile key.
1030    ///
1031    /// This adds the member as a pending invite. They will receive a group invite
1032    /// notification and must accept to become a full member. No profile key is needed.
1033    ///
1034    /// The `invitee` may be either an ACI or a PNI. When only a PNI is known (e.g.
1035    /// the invitee has ACI disclosure disabled in CDSI), passing `ServiceId::Pni`
1036    /// allows the pending-invite path to proceed without an ACI. The Signal server
1037    /// stores whichever service ID is provided in the encrypted `user_id` field of
1038    /// the `PendingMember` proto. The `added_by_aci` must always be an ACI.
1039    ///
1040    /// # Role Parameter
1041    ///
1042    /// The `role` parameter is accepted for API consistency, but note that
1043    /// Signal-Android only ever adds pending members with `Role::Default`.
1044    pub fn build_add_pending_member_action(
1045        &self,
1046        invitee: ServiceId,
1047        added_by_aci: Aci,
1048        role: super::model::Role,
1049    ) -> Result<
1050        proto::group_change::actions::AddMemberPendingProfileKeyAction,
1051        GroupDecodingError,
1052    > {
1053        Ok(
1054            proto::group_change::actions::AddMemberPendingProfileKeyAction {
1055                added: Some(proto::MemberPendingProfileKey {
1056                    member: Some(proto::Member {
1057                        user_id: self.encrypt_service_id(invitee)?,
1058                        profile_key: vec![],
1059                        presentation: vec![],
1060                        role: role.into(),
1061                        joined_at_version: 0,
1062                        label_emoji: vec![],
1063                        label_string: vec![],
1064                    }),
1065                    added_by_user_id: self.encrypt_aci(added_by_aci)?,
1066                    timestamp: 0, // Server sets
1067                }),
1068            },
1069        )
1070    }
1071}
1072
1073#[cfg(test)]
1074mod tests {
1075    use super::*;
1076
1077    use rand::RngCore;
1078    use zkgroup::groups::GroupMasterKey;
1079
1080    fn create_group_operations() -> GroupOperations {
1081        // Create a test group master key (32 bytes)
1082        let master_key_bytes = [
1083            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
1084            0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
1085            0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
1086        ];
1087        let group_master_key = GroupMasterKey::new(master_key_bytes);
1088        let group_secret_params =
1089            GroupSecretParams::derive_from_master_key(group_master_key);
1090        GroupOperations::new(group_secret_params)
1091    }
1092
1093    #[test]
1094    fn roundtrip_title() {
1095        let ops = create_group_operations();
1096        let mut rng = rand::rng();
1097
1098        let title = "Test Group Title";
1099        let encrypted = ops.encrypt_title(title, &mut rng);
1100        let decrypted = ops.decrypt_title(&encrypted);
1101        assert_eq!(decrypted, title);
1102    }
1103
1104    #[test]
1105    fn roundtrip_description() {
1106        let ops = create_group_operations();
1107        let mut rng = rand::rng();
1108
1109        let description = "This is a test group description";
1110        let encrypted = ops.encrypt_description(Some(description), &mut rng);
1111        let decrypted = ops.decrypt_description_text(&encrypted);
1112        assert_eq!(decrypted, Some(description.to_string()));
1113    }
1114
1115    #[test]
1116    fn roundtrip_member_label() {
1117        let ops = create_group_operations();
1118        let mut rng = rand::rng();
1119
1120        let label = "Whisperfish / rubdos";
1121        let mut randomness = [0u8; 32];
1122        rng.fill_bytes(&mut randomness);
1123        let encrypted = ops.group_secret_params.encrypt_blob_with_padding(
1124            randomness,
1125            label.as_bytes(),
1126            0,
1127        );
1128
1129        assert_eq!(
1130            ops.decrypt_member_label_text(&encrypted),
1131            Some(label.to_string())
1132        );
1133    }
1134
1135    #[test]
1136    fn roundtrip_disappearing_message_timer() {
1137        let ops = create_group_operations();
1138        let mut rng = rand::rng();
1139
1140        let timer = Timer { duration: 3600 };
1141        let encrypted =
1142            ops.encrypt_disappearing_messages_timer(Some(&timer), &mut rng);
1143        let decrypted = ops.decrypt_disappearing_messages_timer(&encrypted);
1144        assert_eq!(decrypted, Some(timer));
1145    }
1146
1147    #[test]
1148    fn roundtrip_aci_encryption() {
1149        let ops = create_group_operations();
1150
1151        // Use a known ACI string (UUID format from existing test patterns)
1152        let aci = Aci::parse_from_service_id_string(
1153            "550e8400-e29b-41d4-a716-446655440000",
1154        )
1155        .expect("valid ACI");
1156        let encrypted =
1157            ops.encrypt_aci(aci).expect("encrypt_aci should succeed");
1158        let decrypted = ops
1159            .decrypt_aci(&encrypted)
1160            .expect("decrypt_aci should succeed");
1161        assert_eq!(decrypted, aci);
1162    }
1163
1164    #[test]
1165    fn roundtrip_service_id_encryption() {
1166        let ops = create_group_operations();
1167
1168        // Use a known UUID string for the service ID
1169        let service_id: ServiceId = ServiceId::parse_from_service_id_string(
1170            "550e8400-e29b-41d4-a716-446655440000",
1171        )
1172        .expect("valid service ID");
1173        let encrypted = ops
1174            .encrypt_service_id(service_id)
1175            .expect("encrypt_service_id should succeed");
1176        let decrypted = ops
1177            .decrypt_service_id(&encrypted)
1178            .expect("decrypt_service_id should succeed");
1179        assert_eq!(decrypted, service_id);
1180    }
1181
1182    #[test]
1183    fn roundtrip_service_id_pni_encryption() {
1184        let ops = create_group_operations();
1185
1186        // Use a known UUID string for the service ID
1187        let service_id: ServiceId = ServiceId::parse_from_service_id_string(
1188            "PNI:550e8400-e29b-41d4-a716-446655440000",
1189        )
1190        .expect("valid service ID");
1191        let encrypted = ops
1192            .encrypt_service_id(service_id)
1193            .expect("encrypt_service_id should succeed");
1194        let decrypted = ops
1195            .decrypt_service_id(&encrypted)
1196            .expect("decrypt_service_id should succeed");
1197        assert_eq!(decrypted, service_id);
1198    }
1199
1200    #[test]
1201    fn encrypt_title_different_each_time() {
1202        let ops = create_group_operations();
1203        let mut rng = rand::rng();
1204
1205        let title = "Test Title";
1206        let encrypted1 = ops.encrypt_title(title, &mut rng);
1207        let encrypted2 = ops.encrypt_title(title, &mut rng);
1208
1209        // Same plaintext should produce different ciphertext due to random padding
1210        // but both should decrypt to the same value
1211        assert_ne!(encrypted1, encrypted2);
1212        assert_eq!(ops.decrypt_title(&encrypted1), title);
1213        assert_eq!(ops.decrypt_title(&encrypted2), title);
1214    }
1215}