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        } = 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    pub fn create_member_presentation(
848        &self,
849        server_public_params: &ServerPublicParams,
850        credential: &ExpiringProfileKeyCredential,
851    ) -> Vec<u8> {
852        let randomness: [u8; 32] = rand::random();
853        let presentation = server_public_params
854            .create_expiring_profile_key_credential_presentation(
855                randomness,
856                self.group_secret_params,
857                *credential,
858            );
859        zkgroup::serialize(&presentation)
860    }
861
862    /// Encrypt a group for creation, using credentials for member presentations.
863    ///
864    /// This method properly populates the `presentation` field for members with
865    /// credentials, which is required by the Signal server for group creation.
866    ///
867    /// Members with credentials get added with presentations (full members).
868    /// Members without credentials get added as pending invites.
869    ///
870    /// # Arguments
871    /// * `title` - The group title
872    /// * `description` - Optional group description
873    /// * `disappearing_messages_timer` - Optional disappearing messages timer
874    /// * `access_control` - Optional access control settings
875    /// * `self_credential` - The creator's own credential (required)
876    /// * `avatar_url` - The group avatar URL
877    /// * `member_candidates` - Other members to add, with optional credentials
878    /// * `server_public_params` - Server public params for creating presentations
879    /// * `rng` - Random number generator
880    #[allow(clippy::too_many_arguments)]
881    pub fn encrypt_group_with_credentials<R: rand::Rng + rand::CryptoRng>(
882        &self,
883        title: &str,
884        description: Option<&str>,
885        disappearing_messages_timer: Option<&Timer>,
886        access_control: Option<&AccessControl>,
887        self_credential: &ExpiringProfileKeyCredential,
888        member_candidates: &[GroupMemberCandidate],
889        server_public_params: &ServerPublicParams,
890        avatar_url: String,
891        rng: &mut R,
892    ) -> Result<proto::Group, GroupDecodingError> {
893        let mut members = Vec::new();
894        let mut members_pending_profile_key = Vec::new();
895
896        // Add self as administrator with presentation
897        let self_presentation = self
898            .create_member_presentation(server_public_params, self_credential);
899        members.push(proto::Member {
900            user_id: vec![],     // Server extracts from presentation
901            profile_key: vec![], // Server extracts from presentation
902            presentation: self_presentation,
903            role: proto::member::Role::Administrator.into(),
904            joined_at_version: 0,
905            label_emoji: vec![],
906            label_string: vec![],
907        });
908
909        // Add other members
910        for candidate in member_candidates {
911            if let Some(credential) = &candidate.credential {
912                // Has credential - add as full member with presentation
913                let presentation = self.create_member_presentation(
914                    server_public_params,
915                    credential,
916                );
917                members.push(proto::Member {
918                    user_id: vec![],
919                    profile_key: vec![],
920                    presentation,
921                    role: proto::member::Role::Default.into(),
922                    joined_at_version: 0,
923                    label_emoji: vec![],
924                    label_string: vec![],
925                });
926            } else {
927                // No credential - add as pending invite
928                let user_id_ciphertext =
929                    self.encrypt_service_id(candidate.service_id)?;
930                let self_aci = self_credential.aci();
931                members_pending_profile_key.push(
932                    proto::MemberPendingProfileKey {
933                        member: Some(proto::Member {
934                            user_id: user_id_ciphertext,
935                            profile_key: vec![],
936                            presentation: vec![],
937                            role: proto::member::Role::Default.into(),
938                            joined_at_version: 0,
939                            label_emoji: vec![],
940                            label_string: vec![],
941                        }),
942                        added_by_user_id: self.encrypt_aci(self_aci)?,
943                        timestamp: 0, // Server sets
944                    },
945                );
946            }
947        }
948
949        // Encrypt title, description, timer
950        let encrypted_title = self.encrypt_title(title, rng);
951        let encrypted_description = self.encrypt_description(description, rng);
952        let encrypted_timer = self.encrypt_disappearing_messages_timer(
953            disappearing_messages_timer,
954            rng,
955        );
956
957        // Convert access control
958        let proto_access_control =
959            access_control.map(|ac| proto::AccessControl {
960                attributes: ac.attributes.into(),
961                members: ac.members.into(),
962                add_from_invite_link: ac.add_from_invite_link.into(),
963                member_label: ac.member_label.into(),
964            });
965
966        Ok(proto::Group {
967            public_key: zkgroup::serialize(
968                &self.group_secret_params.get_public_params(),
969            ),
970            title: encrypted_title,
971            avatar_url,
972            disappearing_messages_timer: encrypted_timer,
973            access_control: proto_access_control,
974            version: 0,
975            members,
976            members_pending_profile_key,
977            members_pending_admin_approval: vec![],
978            invite_link_password: vec![],
979            description: encrypted_description,
980            announcements_only: false,
981            members_banned: vec![],
982        })
983    }
984
985    /// Build an AddMemberAction with a credential presentation for a GroupChange.
986    ///
987    /// This is used when adding members to an existing group with proper ZK proofs.
988    ///
989    /// # Role Parameter
990    ///
991    /// The `role` parameter is accepted for API consistency, but note that
992    /// Signal-Android only ever adds members with `Role::Default`. Adding a member
993    /// with `Role::Administrator` is an illegal operation that the server will reject.
994    /// Promotion to administrator requires a separate `ModifyMemberRoleAction` after
995    /// the member has been added.
996    pub fn build_add_member_action_with_credential(
997        &self,
998        credential: &ExpiringProfileKeyCredential,
999        role: super::model::Role,
1000        server_public_params: &ServerPublicParams,
1001    ) -> proto::group_change::actions::AddMemberAction {
1002        let presentation =
1003            self.create_member_presentation(server_public_params, credential);
1004        proto::group_change::actions::AddMemberAction {
1005            added: Some(proto::Member {
1006                user_id: vec![],     // Server extracts from presentation
1007                profile_key: vec![], // Server extracts from presentation
1008                presentation,
1009                role: role.into(),
1010                joined_at_version: 0, // Set by server
1011                label_emoji: vec![],
1012                label_string: vec![],
1013            }),
1014            join_from_invite_link: false,
1015        }
1016    }
1017
1018    /// Build an AddPendingMemberAction to invite a member without their profile key.
1019    ///
1020    /// This adds the member as a pending invite. They will receive a group invite
1021    /// notification and must accept to become a full member. No profile key is needed.
1022    ///
1023    /// The `invitee` may be either an ACI or a PNI. When only a PNI is known (e.g.
1024    /// the invitee has ACI disclosure disabled in CDSI), passing `ServiceId::Pni`
1025    /// allows the pending-invite path to proceed without an ACI. The Signal server
1026    /// stores whichever service ID is provided in the encrypted `user_id` field of
1027    /// the `PendingMember` proto. The `added_by_aci` must always be an ACI.
1028    ///
1029    /// # Role Parameter
1030    ///
1031    /// The `role` parameter is accepted for API consistency, but note that
1032    /// Signal-Android only ever adds pending members with `Role::Default`.
1033    pub fn build_add_pending_member_action(
1034        &self,
1035        invitee: ServiceId,
1036        added_by_aci: Aci,
1037        role: super::model::Role,
1038    ) -> Result<
1039        proto::group_change::actions::AddMemberPendingProfileKeyAction,
1040        GroupDecodingError,
1041    > {
1042        Ok(
1043            proto::group_change::actions::AddMemberPendingProfileKeyAction {
1044                added: Some(proto::MemberPendingProfileKey {
1045                    member: Some(proto::Member {
1046                        user_id: self.encrypt_service_id(invitee)?,
1047                        profile_key: vec![],
1048                        presentation: vec![],
1049                        role: role.into(),
1050                        joined_at_version: 0,
1051                        label_emoji: vec![],
1052                        label_string: vec![],
1053                    }),
1054                    added_by_user_id: self.encrypt_aci(added_by_aci)?,
1055                    timestamp: 0, // Server sets
1056                }),
1057            },
1058        )
1059    }
1060}
1061
1062#[cfg(test)]
1063mod tests {
1064    use super::*;
1065
1066    use rand::RngCore;
1067    use zkgroup::groups::GroupMasterKey;
1068
1069    fn create_group_operations() -> GroupOperations {
1070        // Create a test group master key (32 bytes)
1071        let master_key_bytes = [
1072            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
1073            0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
1074            0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
1075        ];
1076        let group_master_key = GroupMasterKey::new(master_key_bytes);
1077        let group_secret_params =
1078            GroupSecretParams::derive_from_master_key(group_master_key);
1079        GroupOperations::new(group_secret_params)
1080    }
1081
1082    #[test]
1083    fn roundtrip_title() {
1084        let ops = create_group_operations();
1085        let mut rng = rand::rng();
1086
1087        let title = "Test Group Title";
1088        let encrypted = ops.encrypt_title(title, &mut rng);
1089        let decrypted = ops.decrypt_title(&encrypted);
1090        assert_eq!(decrypted, title);
1091    }
1092
1093    #[test]
1094    fn roundtrip_description() {
1095        let ops = create_group_operations();
1096        let mut rng = rand::rng();
1097
1098        let description = "This is a test group description";
1099        let encrypted = ops.encrypt_description(Some(description), &mut rng);
1100        let decrypted = ops.decrypt_description_text(&encrypted);
1101        assert_eq!(decrypted, Some(description.to_string()));
1102    }
1103
1104    #[test]
1105    fn roundtrip_member_label() {
1106        let ops = create_group_operations();
1107        let mut rng = rand::rng();
1108
1109        let label = "Whisperfish / rubdos";
1110        let mut randomness = [0u8; 32];
1111        rng.fill_bytes(&mut randomness);
1112        let encrypted = ops.group_secret_params.encrypt_blob_with_padding(
1113            randomness,
1114            label.as_bytes(),
1115            0,
1116        );
1117
1118        assert_eq!(
1119            ops.decrypt_member_label_text(&encrypted),
1120            Some(label.to_string())
1121        );
1122    }
1123
1124    #[test]
1125    fn roundtrip_disappearing_message_timer() {
1126        let ops = create_group_operations();
1127        let mut rng = rand::rng();
1128
1129        let timer = Timer { duration: 3600 };
1130        let encrypted =
1131            ops.encrypt_disappearing_messages_timer(Some(&timer), &mut rng);
1132        let decrypted = ops.decrypt_disappearing_messages_timer(&encrypted);
1133        assert_eq!(decrypted, Some(timer));
1134    }
1135
1136    #[test]
1137    fn roundtrip_aci_encryption() {
1138        let ops = create_group_operations();
1139
1140        // Use a known ACI string (UUID format from existing test patterns)
1141        let aci = Aci::parse_from_service_id_string(
1142            "550e8400-e29b-41d4-a716-446655440000",
1143        )
1144        .expect("valid ACI");
1145        let encrypted =
1146            ops.encrypt_aci(aci).expect("encrypt_aci should succeed");
1147        let decrypted = ops
1148            .decrypt_aci(&encrypted)
1149            .expect("decrypt_aci should succeed");
1150        assert_eq!(decrypted, aci);
1151    }
1152
1153    #[test]
1154    fn roundtrip_service_id_encryption() {
1155        let ops = create_group_operations();
1156
1157        // Use a known UUID string for the service ID
1158        let service_id: ServiceId = ServiceId::parse_from_service_id_string(
1159            "550e8400-e29b-41d4-a716-446655440000",
1160        )
1161        .expect("valid service ID");
1162        let encrypted = ops
1163            .encrypt_service_id(service_id)
1164            .expect("encrypt_service_id should succeed");
1165        let decrypted = ops
1166            .decrypt_service_id(&encrypted)
1167            .expect("decrypt_service_id should succeed");
1168        assert_eq!(decrypted, service_id);
1169    }
1170
1171    #[test]
1172    fn roundtrip_service_id_pni_encryption() {
1173        let ops = create_group_operations();
1174
1175        // Use a known UUID string for the service ID
1176        let service_id: ServiceId = ServiceId::parse_from_service_id_string(
1177            "PNI:550e8400-e29b-41d4-a716-446655440000",
1178        )
1179        .expect("valid service ID");
1180        let encrypted = ops
1181            .encrypt_service_id(service_id)
1182            .expect("encrypt_service_id should succeed");
1183        let decrypted = ops
1184            .decrypt_service_id(&encrypted)
1185            .expect("decrypt_service_id should succeed");
1186        assert_eq!(decrypted, service_id);
1187    }
1188
1189    #[test]
1190    fn encrypt_title_different_each_time() {
1191        let ops = create_group_operations();
1192        let mut rng = rand::rng();
1193
1194        let title = "Test Title";
1195        let encrypted1 = ops.encrypt_title(title, &mut rng);
1196        let encrypted2 = ops.encrypt_title(title, &mut rng);
1197
1198        // Same plaintext should produce different ciphertext due to random padding
1199        // but both should decrypt to the same value
1200        assert_ne!(encrypted1, encrypted2);
1201        assert_eq!(ops.decrypt_title(&encrypted1), title);
1202        assert_eq!(ops.decrypt_title(&encrypted2), title);
1203    }
1204}