zoe_wire_protocol/
invitation.rs

1//! Group invitation message types for PQXDH-based secure invitations
2//!
3//! This module defines the message types used in the group invitation flow,
4//! providing type-safe structures for the multi-step verification process.
5
6use crate::{Tag, VerifyingKey};
7use serde::{Deserialize, Serialize};
8
9/// Protocol version for invitation messages
10#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
11pub enum InboxHandshakeProtocolVersion {
12    V1,
13}
14
15/// Purpose of the PQXDH handshake session
16#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
17pub enum HandshakePurpose {
18    GroupInvitation,
19    DirectMessage,
20    FileTransfer,
21    // Future purposes can be added here
22}
23
24/// Initial handshake request sent in PqxdhInitialMessage payload
25///
26/// This message establishes the PQXDH session and requests verification.
27/// It contains NO sensitive group information for security.
28#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
29pub struct VerificationHandshakeRequest {
30    pub protocol_version: InboxHandshakeProtocolVersion,
31    pub purpose: HandshakePurpose,
32    pub timestamp: u64,
33}
34
35/// Response sent after emoji verification by the invitee
36///
37/// This message indicates whether the user accepted or rejected the invitation
38/// after verifying the emoji sequence derived from the shared PQXDH secret.
39#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
40pub struct HandshakeResponse {
41    pub accepted: bool,
42    pub timestamp: u64,
43}
44
45/// User profile information
46#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
47pub struct UserProfile {
48    pub display_name: String,
49    pub avatar: Option<Vec<u8>>, // Optional avatar data
50    pub public_key: VerifyingKey,
51}
52
53/// Group metadata shared during invitation
54#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
55pub struct GroupMetadata {
56    pub name: String,
57    pub description: Option<String>,
58    pub member_count: u32,
59    pub created_at: u64,
60}
61
62/// Sensitive group data sent only after successful verification
63///
64/// This message contains all the information needed for the invitee to join
65/// the group. It is only sent after the handshake has been confirmed.
66#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
67pub struct GroupInvitationData {
68    pub group_tag: Tag,
69    pub shared_aes_key: [u8; 32],
70    pub inviter_profile: UserProfile,
71    pub group_metadata: GroupMetadata,
72    pub timestamp: u64,
73}
74
75/// Event sent to the group to announce a new member
76///
77/// This is sent outside the PQXDH session to the group's regular channel
78/// to notify existing members of the new joiner.
79#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
80pub struct ProfileSetEvent {
81    pub event_type: String, // "member_joined"
82    pub user_profile: UserProfile,
83    pub timestamp: u64,
84}
85
86/// Generate a random ephemeral group invite protocol ID
87///
88/// Returns a random value in the range 0-999 for use with
89/// PqxdhInboxProtocol::EphemeralGroupInvite(id).
90/// This provides unlinkability between different invitation sessions.
91pub fn generate_ephemeral_group_invite_id() -> u32 {
92    use rand::Rng;
93    let mut rng = rand::thread_rng();
94    rng.gen_range(0..1000)
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn test_ephemeral_id_range() {
103        for _ in 0..100 {
104            let id = generate_ephemeral_group_invite_id();
105            assert!(id < 1000, "Generated ID {id} should be less than 1000");
106        }
107    }
108
109    #[test]
110    fn test_message_serialization() {
111        let request = VerificationHandshakeRequest {
112            protocol_version: InboxHandshakeProtocolVersion::V1,
113            purpose: HandshakePurpose::GroupInvitation,
114            timestamp: 1234567890,
115        };
116
117        let serialized = postcard::to_allocvec(&request).unwrap();
118        let deserialized: VerificationHandshakeRequest = postcard::from_bytes(&serialized).unwrap();
119        assert_eq!(request, deserialized);
120    }
121
122    #[test]
123    fn test_handshake_response() {
124        let response = HandshakeResponse {
125            accepted: true,
126            timestamp: 1234567890,
127        };
128
129        let serialized = postcard::to_allocvec(&response).unwrap();
130        let deserialized: HandshakeResponse = postcard::from_bytes(&serialized).unwrap();
131        assert_eq!(response, deserialized);
132    }
133}