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        Ok(Member {
211            aci,
212            profile_key,
213            role: member.role.try_into()?,
214            joined_at_version: member.joined_at_version,
215        })
216    }
217
218    fn decrypt_pending_member(
219        &self,
220        member: proto::MemberPendingProfileKey,
221    ) -> Result<PendingMember, GroupDecodingError> {
222        let inner_member =
223            member.member.ok_or(GroupDecodingError::WrongBlob)?;
224        let service_id = self.decrypt_service_id(&inner_member.user_id)?;
225        let added_by_aci = self.decrypt_aci(&member.added_by_user_id)?;
226
227        Ok(PendingMember {
228            address: service_id,
229            role: inner_member.role.try_into()?,
230            added_by_aci,
231            timestamp: member.timestamp,
232        })
233    }
234
235    fn decrypt_requesting_member(
236        &self,
237        member: proto::MemberPendingAdminApproval,
238    ) -> Result<RequestingMember, GroupDecodingError> {
239        let (aci, profile_key) = self.decrypt_profile_key_presentation(
240            &member.user_id,
241            &member.profile_key,
242            &member.presentation,
243        )?;
244        Ok(RequestingMember {
245            profile_key,
246            aci,
247            timestamp: member.timestamp,
248        })
249    }
250
251    fn decrypt_banned_member(
252        &self,
253        member: proto::MemberBanned,
254    ) -> Result<BannedMember, GroupDecodingError> {
255        Ok(BannedMember {
256            user_id: self.decrypt_service_id(&member.user_id)?,
257            timestamp: member.timestamp,
258        })
259    }
260
261    fn decrypt_string(
262        &self,
263        bytes: &[u8],
264    ) -> Result<String, GroupDecodingError> {
265        let bytes = self.group_secret_params.decrypt_blob(bytes)?;
266        String::from_utf8(bytes).map_err(|_| GroupDecodingError::WrongBlob)
267    }
268
269    fn decrypt_blob(&self, bytes: &[u8]) -> GroupAttributeBlob {
270        if bytes.is_empty() {
271            GroupAttributeBlob::default()
272        } else if bytes.len() < 29 {
273            tracing::warn!("bad encrypted blob length");
274            GroupAttributeBlob::default()
275        } else {
276            self.group_secret_params
277                .decrypt_blob_with_padding(bytes)
278                .map_err(GroupDecodingError::from)
279                .and_then(|plaintext| {
280                    GroupAttributeBlob::decode(Bytes::from(plaintext))
281                        .map_err(GroupDecodingError::ProtobufDecodeError)
282                })
283                .unwrap_or_else(|e| {
284                    tracing::warn!("bad encrypted blob: {}", e);
285                    GroupAttributeBlob::default()
286                })
287        }
288    }
289
290    /// Helper method to encrypt a `group_attribute_blob::Content`.
291    ///
292    /// # Padding Format
293    ///
294    /// Uses `encrypt_blob_with_padding` format from Signal's zkgroup's
295    /// `GroupSecretParams`, which prepends a 4-byte big-endian padding length value
296    /// to the plaintext before encryption. For group attribute blobs, padding is
297    /// always 0, so the format is:
298    /// - First 4 bytes: `0u32.to_be_bytes()` (padding length = 0)
299    /// - Remaining bytes: protobuf-encoded `GroupAttributeBlob`
300    ///
301    /// # References
302    ///
303    /// - Signal libsignal repository: <https://github.com/signalapp/libsignal>
304    /// - GroupSecretParams implementation:
305    ///   `rust/zkgroup/src/api/groups/group_params.rs`
306    /// - Java ClientZkGroupCipher usage:
307    ///   `java/shared/java/org/signal/libsignal/zkgroup/groups/ClientZkGroupCipher.java`
308    fn encrypt_blob_content<R: rand::Rng + rand::CryptoRng>(
309        &self,
310        content: group_attribute_blob::Content,
311        rng: &mut R,
312    ) -> Vec<u8> {
313        let blob = GroupAttributeBlob {
314            content: Some(content),
315        };
316        let buf = blob.encode_to_vec();
317
318        let mut randomness = [0u8; 32];
319        rng.fill_bytes(&mut randomness);
320        self.group_secret_params
321            .encrypt_blob_with_padding(randomness, &buf, 0)
322    }
323
324    pub fn encrypt_title<R: rand::Rng + rand::CryptoRng>(
325        &self,
326        title: &str,
327        rng: &mut R,
328    ) -> Vec<u8> {
329        self.encrypt_blob_content(
330            group_attribute_blob::Content::Title(title.to_string()),
331            rng,
332        )
333    }
334
335    pub fn encrypt_description<R: rand::Rng + rand::CryptoRng>(
336        &self,
337        description: Option<&str>,
338        rng: &mut R,
339    ) -> Vec<u8> {
340        self.encrypt_blob_content(
341            group_attribute_blob::Content::DescriptionText(
342                description.unwrap_or_default().to_string(),
343            ),
344            rng,
345        )
346    }
347
348    pub fn encrypt_disappearing_messages_timer<
349        R: rand::Rng + rand::CryptoRng,
350    >(
351        &self,
352        timer: Option<&Timer>,
353        rng: &mut R,
354    ) -> Vec<u8> {
355        self.encrypt_blob_content(
356            group_attribute_blob::Content::DisappearingMessagesDuration(
357                timer.map(|t| t.duration).unwrap_or(0),
358            ),
359            rng,
360        )
361    }
362
363    fn decrypt_title(&self, ciphertext: &[u8]) -> String {
364        use group_attribute_blob::Content;
365        match self.decrypt_blob(ciphertext).content {
366            Some(Content::Title(title)) => title,
367            _ => "".into(),
368        }
369    }
370
371    fn decrypt_description_text(&self, ciphertext: &[u8]) -> Option<String> {
372        use group_attribute_blob::Content;
373        match self.decrypt_blob(ciphertext).content {
374            Some(Content::DescriptionText(d)) => {
375                Some(d).filter(|d| !d.is_empty())
376            },
377            _ => None,
378        }
379    }
380
381    fn decrypt_disappearing_messages_timer(
382        &self,
383        ciphertext: &[u8],
384    ) -> Option<Timer> {
385        use group_attribute_blob::Content;
386        match self.decrypt_blob(ciphertext).content {
387            Some(Content::DisappearingMessagesDuration(duration)) => {
388                Some(Timer { duration })
389            },
390            _ => None,
391        }
392    }
393
394    pub fn new(group_secret_params: GroupSecretParams) -> Self {
395        Self {
396            group_secret_params,
397        }
398    }
399
400    pub fn decrypt_group(
401        &self,
402        group: proto::Group,
403    ) -> Result<Group, GroupDecodingError> {
404        // Destructuring to catch any future changes
405        let proto::Group {
406            public_key: _,
407            title,
408            avatar_url,
409            disappearing_messages_timer,
410            access_control,
411            version,
412            members,
413            members_pending_profile_key,
414            members_pending_admin_approval,
415            invite_link_password,
416            description,
417            announcements_only,
418            members_banned,
419        } = group;
420
421        let title = self.decrypt_title(&title);
422
423        let description_text = self.decrypt_description_text(&description);
424
425        let disappearing_messages_timer = self
426            .decrypt_disappearing_messages_timer(&disappearing_messages_timer);
427
428        let members = members
429            .into_iter()
430            .map(|m| self.decrypt_member(m))
431            .collect::<Result<_, _>>()?;
432
433        let members_pending_profile_key = members_pending_profile_key
434            .into_iter()
435            .map(|m| self.decrypt_pending_member(m))
436            .collect::<Result<_, _>>()?;
437
438        let members_pending_admin_approval = members_pending_admin_approval
439            .into_iter()
440            .map(|m| self.decrypt_requesting_member(m))
441            .collect::<Result<_, _>>()?;
442
443        let members_banned = members_banned
444            .into_iter()
445            .map(|m| self.decrypt_banned_member(m))
446            .collect::<Result<_, _>>()?;
447
448        let access_control =
449            access_control.map(TryInto::try_into).transpose()?;
450
451        Ok(Group {
452            title,
453            avatar: avatar_url,
454            disappearing_messages_timer,
455            access_control,
456            version,
457            members,
458            members_pending_profile_key,
459            members_pending_admin_approval,
460            invite_link_password,
461            description_text,
462            announcements_only,
463            members_banned,
464        })
465    }
466
467    pub fn decrypt_group_change(
468        &self,
469        group_change: proto::GroupChange,
470    ) -> Result<GroupChanges, GroupDecodingError> {
471        // Destructuring to catch any future changes
472        let proto::GroupChange {
473            actions,
474            server_signature: _,
475            change_epoch,
476        } = group_change;
477
478        let proto::group_change::Actions {
479            group_id,
480            source_user_id,
481            version,
482            add_members,
483            delete_members,
484            modify_member_roles,
485            modify_member_profile_keys,
486            add_members_pending_profile_key,
487            delete_members_pending_profile_key,
488            promote_members_pending_profile_key,
489            modify_title,
490            modify_avatar,
491            modify_disappearing_message_timer,
492            modify_attributes_access,
493            modify_member_access,
494            modify_add_from_invite_link_access,
495            add_members_pending_admin_approval,
496            delete_members_pending_admin_approval,
497            promote_members_pending_admin_approval,
498            modify_invite_link_password,
499            modify_description,
500            modify_announcements_only,
501            add_members_banned,
502            delete_members_banned,
503            promote_members_pending_pni_aci_profile_key,
504            modify_member_labels,
505            modify_member_label_access,
506        } = Message::decode(Bytes::from(actions))?;
507
508        let source_user_id = self.decrypt_aci(&source_user_id)?;
509
510        let new_members =
511            add_members
512                .into_iter()
513                .filter_map(|m| m.added)
514                .map(|added| {
515                    Ok(GroupChange::NewMember(self.decrypt_member(added)?))
516                });
517
518        let delete_members = delete_members.into_iter().map(|c| {
519            Ok(GroupChange::DeleteMember(
520                self.decrypt_aci(&c.deleted_user_id)?,
521            ))
522        });
523
524        let modify_member_roles = modify_member_roles.into_iter().map(|m| {
525            Ok(GroupChange::ModifyMemberRole {
526                aci: self.decrypt_aci(&m.user_id)?,
527                role: m.role.try_into()?,
528            })
529        });
530
531        let modify_member_profile_keys =
532            modify_member_profile_keys.into_iter().map(|m| {
533                let (aci, profile_key) = self
534                    .decrypt_profile_key_presentation(
535                        &m.user_id,
536                        &m.profile_key,
537                        &m.presentation,
538                    )?;
539                Ok(GroupChange::ModifyMemberProfileKey { aci, profile_key })
540            });
541
542        let add_members_pending_profile_key = add_members_pending_profile_key
543            .into_iter()
544            .filter_map(|m| m.added)
545            .map(|added| {
546                Ok(GroupChange::NewPendingMember(
547                    self.decrypt_pending_member(added)?,
548                ))
549            });
550
551        let delete_members_pending_profile_key =
552            delete_members_pending_profile_key.into_iter().map(|m| {
553                Ok(GroupChange::DeletePendingMember(
554                    self.decrypt_service_id(&m.deleted_user_id)?,
555                ))
556            });
557
558        let promote_members_pending_profile_key =
559            promote_members_pending_profile_key.into_iter().map(|m| {
560                let (aci, profile_key) = self
561                    .decrypt_profile_key_presentation(
562                        &m.user_id,
563                        &m.profile_key,
564                        &m.presentation,
565                    )?;
566                Ok(GroupChange::PromotePendingMember {
567                    address: aci.into(),
568                    profile_key,
569                })
570            });
571
572        let modify_title = modify_title
573            .into_iter()
574            .map(|m| Ok(GroupChange::Title(self.decrypt_title(&m.title))));
575
576        let modify_avatar = modify_avatar
577            .into_iter()
578            .map(|m| Ok(GroupChange::Avatar(m.avatar)));
579
580        let modify_description = modify_description.into_iter().map(|m| {
581            Ok(GroupChange::Description(
582                self.decrypt_description_text(&m.description),
583            ))
584        });
585
586        let modify_disappearing_message_timer =
587            modify_disappearing_message_timer.into_iter().map(|m| {
588                Ok(GroupChange::Timer(
589                    self.decrypt_disappearing_messages_timer(&m.timer),
590                ))
591            });
592
593        let modify_attributes_access =
594            modify_attributes_access.into_iter().map(|m| {
595                Ok(GroupChange::AttributeAccess(
596                    m.attributes_access.try_into()?,
597                ))
598            });
599
600        let modify_member_access = modify_member_access.into_iter().map(|m| {
601            Ok(GroupChange::MemberAccess(m.members_access.try_into()?))
602        });
603
604        let add_members_banned = add_members_banned
605            .into_iter()
606            .filter_map(|m| m.added)
607            .map(|m| {
608                Ok(GroupChange::AddBannedMember(self.decrypt_banned_member(m)?))
609            });
610
611        let delete_members_banned =
612            delete_members_banned.into_iter().map(|m| {
613                Ok(GroupChange::DeleteBannedMember(
614                    self.decrypt_service_id(&m.deleted_user_id)?,
615                ))
616            });
617
618        let promote_members_pending_pni_aci_profile_key =
619            promote_members_pending_pni_aci_profile_key
620                .into_iter()
621                .map(|m| {
622                    let promoted =
623                        self.decrypt_pni_aci_promotion_presentation(&m)?;
624                    Ok(GroupChange::PromotePendingPniAciMemberProfileKey(
625                        promoted,
626                    ))
627                });
628
629        let modify_add_from_invite_link_access =
630            modify_add_from_invite_link_access.into_iter().map(|m| {
631                Ok(GroupChange::InviteLinkAccess(
632                    m.add_from_invite_link_access.try_into()?,
633                ))
634            });
635
636        let add_members_pending_admin_approval =
637            add_members_pending_admin_approval
638                .into_iter()
639                .filter_map(|m| m.added)
640                .map(|added| {
641                    Ok(GroupChange::NewRequestingMember(
642                        self.decrypt_requesting_member(added)?,
643                    ))
644                });
645
646        let delete_members_pending_admin_approval =
647            delete_members_pending_admin_approval.into_iter().map(|m| {
648                Ok(GroupChange::DeleteRequestingMember(
649                    self.decrypt_aci(&m.deleted_user_id)?,
650                ))
651            });
652
653        let promote_members_pending_admin_approval =
654            promote_members_pending_admin_approval.into_iter().map(|m| {
655                Ok(GroupChange::PromoteRequestingMember {
656                    aci: self.decrypt_aci(&m.user_id)?,
657                    role: m.role.try_into()?,
658                })
659            });
660
661        let modify_invite_link_password =
662            modify_invite_link_password.into_iter().map(|m| {
663                Ok(GroupChange::InviteLinkPassword(
664                    BASE64_RELAXED.encode(m.invite_link_password),
665                ))
666            });
667
668        let modify_announcements_only = modify_announcements_only
669            .into_iter()
670            .map(|m| Ok(GroupChange::AnnouncementOnly(m.announcements_only)));
671
672        let modify_member_labels = modify_member_labels.into_iter().map(|m| {
673            Ok(GroupChange::MemberLabel {
674                user_id: self.decrypt_service_id(&m.user_id)?,
675                label_emoji: self.decrypt_string(&m.label_emoji)?,
676                label_string: self.decrypt_string(&m.label_string)?,
677            })
678        });
679
680        let modify_member_label_access =
681            modify_member_label_access.into_iter().map(|m| {
682                Ok(GroupChange::MemberLabelAccess(
683                    m.member_label_access.try_into()?,
684                ))
685            });
686
687        let changes: Result<Vec<GroupChange>, GroupDecodingError> = new_members
688            .chain(delete_members)
689            .chain(modify_member_roles)
690            .chain(modify_member_profile_keys)
691            .chain(add_members_pending_profile_key)
692            .chain(delete_members_pending_profile_key)
693            .chain(promote_members_pending_profile_key)
694            .chain(modify_title)
695            .chain(modify_avatar)
696            .chain(modify_disappearing_message_timer)
697            .chain(modify_attributes_access)
698            .chain(modify_description)
699            .chain(modify_member_access)
700            .chain(add_members_banned)
701            .chain(delete_members_banned)
702            .chain(promote_members_pending_pni_aci_profile_key)
703            .chain(modify_add_from_invite_link_access)
704            .chain(add_members_pending_admin_approval)
705            .chain(delete_members_pending_admin_approval)
706            .chain(promote_members_pending_admin_approval)
707            .chain(modify_invite_link_password)
708            .chain(modify_announcements_only)
709            .chain(modify_member_labels)
710            .chain(modify_member_label_access)
711            .collect();
712
713        Ok(GroupChanges {
714            group_id: group_id
715                .try_into()
716                .map_err(|_| GroupDecodingError::WrongBlob)?,
717            editor: source_user_id,
718            version,
719            changes: changes?,
720            change_epoch,
721        })
722    }
723
724    pub fn decrypt_avatar(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
725        use group_attribute_blob::Content;
726        match self.decrypt_blob(ciphertext).content {
727            Some(Content::Avatar(d)) => Some(d).filter(|d| !d.is_empty()),
728            _ => None,
729        }
730    }
731
732    /// Build an AddMemberAction for a GroupChange.
733    ///
734    /// # Role Parameter
735    ///
736    /// The `role` parameter is accepted for API consistency, but note that
737    /// Signal-Android only ever adds members with `Role::Default`. Adding a member
738    /// with `Role::Administrator` is an illegal operation that the server will reject.
739    /// Promotion to administrator requires a separate `ModifyMemberRoleAction` after
740    /// the member has been added.
741    ///
742    /// See Signal-Android's `GroupsV2Operations.GroupOperations.createModifyGroupMembershipChange()`
743    /// which hardcodes `Member.Role newMemberRole = Member.Role.DEFAULT`.
744    pub fn build_add_member_action(
745        &self,
746        aci: Aci,
747        profile_key: ProfileKey,
748        role: super::model::Role,
749    ) -> Result<proto::group_change::actions::AddMemberAction, GroupDecodingError>
750    {
751        Ok(proto::group_change::actions::AddMemberAction {
752            added: Some(proto::Member {
753                user_id: self.encrypt_aci(aci)?,
754                profile_key: self.encrypt_profile_key(profile_key, aci)?,
755                presentation: vec![],
756                role: role.into(),
757                joined_at_version: 0, // Set by server
758                // XXX: should these be exposed?
759                label_emoji: vec![],
760                label_string: vec![],
761            }),
762            join_from_invite_link: false,
763        })
764    }
765
766    /// Build a DeleteMemberAction for a GroupChange
767    pub fn build_remove_member_action(
768        &self,
769        aci: Aci,
770    ) -> Result<
771        proto::group_change::actions::DeleteMemberAction,
772        GroupDecodingError,
773    > {
774        Ok(proto::group_change::actions::DeleteMemberAction {
775            deleted_user_id: self.encrypt_aci(aci)?,
776        })
777    }
778
779    /// Build a DeletePendingMemberAction to retract an outstanding invitation.
780    ///
781    /// Used when a pending member (invite not yet accepted) is to be removed.
782    /// The `invitee` may be ACI or PNI — whichever service ID was used when
783    /// the invite was originally created.  The Signal server stores and matches
784    /// on the encrypted `user_id` field of `PendingMember`, which may be
785    /// either kind of `ServiceId`.
786    pub fn build_remove_pending_member_action(
787        &self,
788        invitee: ServiceId,
789    ) -> Result<
790        proto::group_change::actions::DeleteMemberPendingProfileKeyAction,
791        GroupDecodingError,
792    > {
793        Ok(
794            proto::group_change::actions::DeleteMemberPendingProfileKeyAction {
795                deleted_user_id: self.encrypt_service_id(invitee)?,
796            },
797        )
798    }
799
800    /// Create a presentation from a credential for adding a member to a group.
801    ///
802    /// This creates a ZK proof (ExpiringProfileKeyCredentialPresentation) that the
803    /// Signal server can verify to validate the member's identity and profile key.
804    pub fn create_member_presentation(
805        &self,
806        server_public_params: &ServerPublicParams,
807        credential: &ExpiringProfileKeyCredential,
808    ) -> Vec<u8> {
809        let randomness: [u8; 32] = rand::random();
810        let presentation = server_public_params
811            .create_expiring_profile_key_credential_presentation(
812                randomness,
813                self.group_secret_params,
814                *credential,
815            );
816        zkgroup::serialize(&presentation)
817    }
818
819    /// Encrypt a group for creation, using credentials for member presentations.
820    ///
821    /// This method properly populates the `presentation` field for members with
822    /// credentials, which is required by the Signal server for group creation.
823    ///
824    /// Members with credentials get added with presentations (full members).
825    /// Members without credentials get added as pending invites.
826    ///
827    /// # Arguments
828    /// * `title` - The group title
829    /// * `description` - Optional group description
830    /// * `disappearing_messages_timer` - Optional disappearing messages timer
831    /// * `access_control` - Optional access control settings
832    /// * `self_credential` - The creator's own credential (required)
833    /// * `avatar_url` - The group avatar URL
834    /// * `member_candidates` - Other members to add, with optional credentials
835    /// * `server_public_params` - Server public params for creating presentations
836    /// * `rng` - Random number generator
837    #[allow(clippy::too_many_arguments)]
838    pub fn encrypt_group_with_credentials<R: rand::Rng + rand::CryptoRng>(
839        &self,
840        title: &str,
841        description: Option<&str>,
842        disappearing_messages_timer: Option<&Timer>,
843        access_control: Option<&AccessControl>,
844        self_credential: &ExpiringProfileKeyCredential,
845        member_candidates: &[GroupMemberCandidate],
846        server_public_params: &ServerPublicParams,
847        avatar_url: String,
848        rng: &mut R,
849    ) -> Result<proto::Group, GroupDecodingError> {
850        let mut members = Vec::new();
851        let mut members_pending_profile_key = Vec::new();
852
853        // Add self as administrator with presentation
854        let self_presentation = self
855            .create_member_presentation(server_public_params, self_credential);
856        members.push(proto::Member {
857            user_id: vec![],     // Server extracts from presentation
858            profile_key: vec![], // Server extracts from presentation
859            presentation: self_presentation,
860            role: proto::member::Role::Administrator.into(),
861            joined_at_version: 0,
862            label_emoji: vec![],
863            label_string: vec![],
864        });
865
866        // Add other members
867        for candidate in member_candidates {
868            if let Some(credential) = &candidate.credential {
869                // Has credential - add as full member with presentation
870                let presentation = self.create_member_presentation(
871                    server_public_params,
872                    credential,
873                );
874                members.push(proto::Member {
875                    user_id: vec![],
876                    profile_key: vec![],
877                    presentation,
878                    role: proto::member::Role::Default.into(),
879                    joined_at_version: 0,
880                    label_emoji: vec![],
881                    label_string: vec![],
882                });
883            } else {
884                // No credential - add as pending invite
885                let user_id_ciphertext =
886                    self.encrypt_service_id(candidate.service_id)?;
887                let self_aci = self_credential.aci();
888                members_pending_profile_key.push(
889                    proto::MemberPendingProfileKey {
890                        member: Some(proto::Member {
891                            user_id: user_id_ciphertext,
892                            profile_key: vec![],
893                            presentation: vec![],
894                            role: proto::member::Role::Default.into(),
895                            joined_at_version: 0,
896                            label_emoji: vec![],
897                            label_string: vec![],
898                        }),
899                        added_by_user_id: self.encrypt_aci(self_aci)?,
900                        timestamp: 0, // Server sets
901                    },
902                );
903            }
904        }
905
906        // Encrypt title, description, timer
907        let encrypted_title = self.encrypt_title(title, rng);
908        let encrypted_description = self.encrypt_description(description, rng);
909        let encrypted_timer = self.encrypt_disappearing_messages_timer(
910            disappearing_messages_timer,
911            rng,
912        );
913
914        // Convert access control
915        let proto_access_control =
916            access_control.map(|ac| proto::AccessControl {
917                attributes: ac.attributes.into(),
918                members: ac.members.into(),
919                add_from_invite_link: ac.add_from_invite_link.into(),
920                member_label: ac.member_label.into(),
921            });
922
923        Ok(proto::Group {
924            public_key: zkgroup::serialize(
925                &self.group_secret_params.get_public_params(),
926            ),
927            title: encrypted_title,
928            avatar_url,
929            disappearing_messages_timer: encrypted_timer,
930            access_control: proto_access_control,
931            version: 0,
932            members,
933            members_pending_profile_key,
934            members_pending_admin_approval: vec![],
935            invite_link_password: vec![],
936            description: encrypted_description,
937            announcements_only: false,
938            members_banned: vec![],
939        })
940    }
941
942    /// Build an AddMemberAction with a credential presentation for a GroupChange.
943    ///
944    /// This is used when adding members to an existing group with proper ZK proofs.
945    ///
946    /// # Role Parameter
947    ///
948    /// The `role` parameter is accepted for API consistency, but note that
949    /// Signal-Android only ever adds members with `Role::Default`. Adding a member
950    /// with `Role::Administrator` is an illegal operation that the server will reject.
951    /// Promotion to administrator requires a separate `ModifyMemberRoleAction` after
952    /// the member has been added.
953    pub fn build_add_member_action_with_credential(
954        &self,
955        credential: &ExpiringProfileKeyCredential,
956        role: super::model::Role,
957        server_public_params: &ServerPublicParams,
958    ) -> proto::group_change::actions::AddMemberAction {
959        let presentation =
960            self.create_member_presentation(server_public_params, credential);
961        proto::group_change::actions::AddMemberAction {
962            added: Some(proto::Member {
963                user_id: vec![],     // Server extracts from presentation
964                profile_key: vec![], // Server extracts from presentation
965                presentation,
966                role: role.into(),
967                joined_at_version: 0, // Set by server
968                label_emoji: vec![],
969                label_string: vec![],
970            }),
971            join_from_invite_link: false,
972        }
973    }
974
975    /// Build an AddPendingMemberAction to invite a member without their profile key.
976    ///
977    /// This adds the member as a pending invite. They will receive a group invite
978    /// notification and must accept to become a full member. No profile key is needed.
979    ///
980    /// The `invitee` may be either an ACI or a PNI. When only a PNI is known (e.g.
981    /// the invitee has ACI disclosure disabled in CDSI), passing `ServiceId::Pni`
982    /// allows the pending-invite path to proceed without an ACI. The Signal server
983    /// stores whichever service ID is provided in the encrypted `user_id` field of
984    /// the `PendingMember` proto. The `added_by_aci` must always be an ACI.
985    ///
986    /// # Role Parameter
987    ///
988    /// The `role` parameter is accepted for API consistency, but note that
989    /// Signal-Android only ever adds pending members with `Role::Default`.
990    pub fn build_add_pending_member_action(
991        &self,
992        invitee: ServiceId,
993        added_by_aci: Aci,
994        role: super::model::Role,
995    ) -> Result<
996        proto::group_change::actions::AddMemberPendingProfileKeyAction,
997        GroupDecodingError,
998    > {
999        Ok(
1000            proto::group_change::actions::AddMemberPendingProfileKeyAction {
1001                added: Some(proto::MemberPendingProfileKey {
1002                    member: Some(proto::Member {
1003                        user_id: self.encrypt_service_id(invitee)?,
1004                        profile_key: vec![],
1005                        presentation: vec![],
1006                        role: role.into(),
1007                        joined_at_version: 0,
1008                        label_emoji: vec![],
1009                        label_string: vec![],
1010                    }),
1011                    added_by_user_id: self.encrypt_aci(added_by_aci)?,
1012                    timestamp: 0, // Server sets
1013                }),
1014            },
1015        )
1016    }
1017}
1018
1019#[cfg(test)]
1020mod tests {
1021    use super::*;
1022
1023    use zkgroup::groups::GroupMasterKey;
1024
1025    fn create_group_operations() -> GroupOperations {
1026        // Create a test group master key (32 bytes)
1027        let master_key_bytes = [
1028            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
1029            0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
1030            0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
1031        ];
1032        let group_master_key = GroupMasterKey::new(master_key_bytes);
1033        let group_secret_params =
1034            GroupSecretParams::derive_from_master_key(group_master_key);
1035        GroupOperations::new(group_secret_params)
1036    }
1037
1038    #[test]
1039    fn roundtrip_title() {
1040        let ops = create_group_operations();
1041        let mut rng = rand::rng();
1042
1043        let title = "Test Group Title";
1044        let encrypted = ops.encrypt_title(title, &mut rng);
1045        let decrypted = ops.decrypt_title(&encrypted);
1046        assert_eq!(decrypted, title);
1047    }
1048
1049    #[test]
1050    fn roundtrip_description() {
1051        let ops = create_group_operations();
1052        let mut rng = rand::rng();
1053
1054        let description = "This is a test group description";
1055        let encrypted = ops.encrypt_description(Some(description), &mut rng);
1056        let decrypted = ops.decrypt_description_text(&encrypted);
1057        assert_eq!(decrypted, Some(description.to_string()));
1058    }
1059
1060    #[test]
1061    fn roundtrip_disappearing_message_timer() {
1062        let ops = create_group_operations();
1063        let mut rng = rand::rng();
1064
1065        let timer = Timer { duration: 3600 };
1066        let encrypted =
1067            ops.encrypt_disappearing_messages_timer(Some(&timer), &mut rng);
1068        let decrypted = ops.decrypt_disappearing_messages_timer(&encrypted);
1069        assert_eq!(decrypted, Some(timer));
1070    }
1071
1072    #[test]
1073    fn roundtrip_aci_encryption() {
1074        let ops = create_group_operations();
1075
1076        // Use a known ACI string (UUID format from existing test patterns)
1077        let aci = Aci::parse_from_service_id_string(
1078            "550e8400-e29b-41d4-a716-446655440000",
1079        )
1080        .expect("valid ACI");
1081        let encrypted =
1082            ops.encrypt_aci(aci).expect("encrypt_aci should succeed");
1083        let decrypted = ops
1084            .decrypt_aci(&encrypted)
1085            .expect("decrypt_aci should succeed");
1086        assert_eq!(decrypted, aci);
1087    }
1088
1089    #[test]
1090    fn roundtrip_service_id_encryption() {
1091        let ops = create_group_operations();
1092
1093        // Use a known UUID string for the service ID
1094        let service_id: ServiceId = ServiceId::parse_from_service_id_string(
1095            "550e8400-e29b-41d4-a716-446655440000",
1096        )
1097        .expect("valid service ID");
1098        let encrypted = ops
1099            .encrypt_service_id(service_id)
1100            .expect("encrypt_service_id should succeed");
1101        let decrypted = ops
1102            .decrypt_service_id(&encrypted)
1103            .expect("decrypt_service_id should succeed");
1104        assert_eq!(decrypted, service_id);
1105    }
1106
1107    #[test]
1108    fn roundtrip_service_id_pni_encryption() {
1109        let ops = create_group_operations();
1110
1111        // Use a known UUID string for the service ID
1112        let service_id: ServiceId = ServiceId::parse_from_service_id_string(
1113            "PNI:550e8400-e29b-41d4-a716-446655440000",
1114        )
1115        .expect("valid service ID");
1116        let encrypted = ops
1117            .encrypt_service_id(service_id)
1118            .expect("encrypt_service_id should succeed");
1119        let decrypted = ops
1120            .decrypt_service_id(&encrypted)
1121            .expect("decrypt_service_id should succeed");
1122        assert_eq!(decrypted, service_id);
1123    }
1124
1125    #[test]
1126    fn encrypt_title_different_each_time() {
1127        let ops = create_group_operations();
1128        let mut rng = rand::rng();
1129
1130        let title = "Test Title";
1131        let encrypted1 = ops.encrypt_title(title, &mut rng);
1132        let encrypted2 = ops.encrypt_title(title, &mut rng);
1133
1134        // Same plaintext should produce different ciphertext due to random padding
1135        // but both should decrypt to the same value
1136        assert_ne!(encrypted1, encrypted2);
1137        assert_eq!(ops.decrypt_title(&encrypted1), title);
1138        assert_eq!(ops.decrypt_title(&encrypted2), title);
1139    }
1140}