zoe_state_machine/
state.rs

1use serde::{Deserialize, Serialize};
2
3use zoe_app_primitives::GroupActivityEvent;
4use zoe_wire_protocol::{ChaCha20Poly1305Content, EncryptionKey, MessageId};
5
6// GroupState and GroupMember are now unified in app-primitives
7// Re-export them here for backwards compatibility
8pub use zoe_app_primitives::{GroupMember, GroupState};
9
10#[cfg(feature = "frb-api")]
11use flutter_rust_bridge::frb;
12
13#[derive(Debug, thiserror::Error)]
14pub enum GroupSessionError {
15    /// Crypto error
16    #[error("Crypto error: {0}")]
17    Crypto(String),
18    /// Serialization error
19    #[error("Serialization error: {0}")]
20    Serialization(String),
21    /// Deserialization error
22    #[error("Deserialization error: {0}")]
23    Deserialization(String),
24}
25
26/// Complete group session state including both group state and encryption keys
27/// Since both are stored in the same encrypted database and always used together,
28/// combining them reduces complexity and eliminates synchronization issues.
29#[cfg_attr(feature = "frb-api", frb(opaque))]
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct GroupSession {
32    /// The group's business logic state (members, roles, metadata, etc.)
33    pub state: GroupState,
34    /// Current encryption key for this group
35    pub current_key: EncryptionKey,
36    /// Previous keys (for decrypting old messages during key rotation)
37    pub previous_keys: Vec<EncryptionKey>,
38}
39
40pub fn encrypt_group_event_content<T>(
41    encryption_key: &EncryptionKey,
42    event: &GroupActivityEvent<T>,
43) -> Result<ChaCha20Poly1305Content, GroupSessionError>
44where
45    T: Serialize,
46{
47    // Serialize the event
48    let plaintext = postcard::to_stdvec(event)
49        .map_err(|e| GroupSessionError::Crypto(format!("Group event serialization failed: {e}")))?;
50
51    // Encrypt using ChaCha20-Poly1305
52    encryption_key
53        .encrypt_content(&plaintext)
54        .map_err(|e| GroupSessionError::Crypto(format!("Group event encryption failed: {e}")))
55}
56
57#[cfg_attr(feature = "frb-api", frb(ignore))]
58impl GroupSession {
59    /// Create a new group session with initial state and encryption key
60    pub fn new(state: GroupState, encryption_key: EncryptionKey) -> Self {
61        Self {
62            state,
63            current_key: encryption_key,
64            previous_keys: Vec::new(),
65        }
66    }
67
68    /// Rotate the encryption key, moving the current key to previous keys
69    pub fn rotate_key(&mut self, new_key: EncryptionKey) {
70        let old_key = std::mem::replace(&mut self.current_key, new_key);
71        self.previous_keys.push(old_key);
72    }
73
74    /// Get all keys (current + previous) for decryption attempts
75    pub fn all_keys(&self) -> impl Iterator<Item = &EncryptionKey> {
76        std::iter::once(&self.current_key).chain(self.previous_keys.iter())
77    }
78
79    /// Encrypt a group event using ChaCha20-Poly1305
80    pub fn encrypt_group_event_content<T>(
81        &self,
82        event: &GroupActivityEvent<T>,
83    ) -> Result<ChaCha20Poly1305Content, GroupSessionError>
84    where
85        T: Serialize,
86    {
87        encrypt_group_event_content(&self.current_key, event)
88    }
89
90    /// Decrypt a group event using ChaCha20-Poly1305
91    pub fn decrypt_group_event<T>(
92        &self,
93        payload: &ChaCha20Poly1305Content,
94    ) -> Result<GroupActivityEvent<T>, GroupSessionError>
95    where
96        T: for<'de> Deserialize<'de>,
97    {
98        // Note: No key ID verification needed since key is determined by channel context
99
100        // Decrypt using ChaCha20-Poly1305
101        let plaintext = self.current_key.decrypt_content(payload).map_err(|e| {
102            GroupSessionError::Crypto(format!("Group event decryption failed: {e}"))
103        })?;
104        // FIXME: cycle through older keys trying to decrypt until one succeeds
105
106        // Deserialize the event
107        let event: GroupActivityEvent<T> = postcard::from_bytes(&plaintext).map_err(|e| {
108            GroupSessionError::Crypto(format!("Group event deserialization failed: {e}"))
109        })?;
110        Ok(event)
111    }
112}
113
114/// A snapshot of a group's state at a specific point in time
115#[cfg_attr(feature = "frb-api", frb(ignore))]
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct GroupStateSnapshot {
118    pub state: GroupState,
119    pub snapshot_at: u64,
120    pub snapshot_event_id: MessageId,
121}
122
123// All GroupState implementation methods are now in app-primitives