coven/sync/
membership_ops.rs1use tracing::{info, warn};
7
8use crate::encryption::EncryptionService;
9use crate::keys::{KeyService, UserKeypair};
10
11use super::hlc::Hlc;
12use super::membership::{
13 sign_membership_entry, MemberRole, MembershipAction, MembershipChain, MembershipEntry,
14};
15use super::storage::SyncStorage;
16
17pub struct MemberInfo {
19 pub pubkey: String,
20 pub role: MemberRole,
21 pub is_self: bool,
22}
23
24pub async fn get_members(
26 storage: &dyn SyncStorage,
27 user_pubkey: Option<&[u8]>,
28) -> Result<Vec<MemberInfo>, MembershipOpsError> {
29 let entry_keys = storage
30 .list_membership_entries()
31 .await
32 .map_err(|e| MembershipOpsError(format!("Failed to list membership entries: {e}")))?;
33
34 if entry_keys.is_empty() {
35 return Ok(Vec::new());
36 }
37
38 let chain = download_chain(storage, &entry_keys).await?;
39 let user_pubkey_hex = user_pubkey.map(hex::encode);
40
41 let current = chain.current_members();
42 let members = current
43 .into_iter()
44 .map(|(pubkey, role)| {
45 let is_self = user_pubkey_hex.as_deref() == Some(&pubkey);
46 MemberInfo {
47 pubkey,
48 role,
49 is_self,
50 }
51 })
52 .collect();
53
54 Ok(members)
55}
56
57pub async fn invite_member(
65 storage: &dyn SyncStorage,
66 cloud_home: &dyn crate::storage::cloud::CloudHome,
67 user_keypair: &UserKeypair,
68 hlc: &Hlc,
69 public_key_hex: &str,
70 role: MemberRole,
71 encryption_key: &[u8; 32],
72 library_id: &str,
73 library_name: &str,
74) -> Result<crate::join_code::InviteCode, MembershipOpsError> {
75 let user_pubkey_hex = hex::encode(user_keypair.public_key);
76
77 if public_key_hex == user_pubkey_hex {
78 return Err(MembershipOpsError("Cannot invite yourself".to_string()));
79 }
80
81 let entry_keys = storage
83 .list_membership_entries()
84 .await
85 .map_err(|e| MembershipOpsError(format!("Failed to list membership entries: {e}")))?;
86
87 let mut chain = if entry_keys.is_empty() {
88 let mut founder = MembershipEntry {
90 action: MembershipAction::Add,
91 user_pubkey: user_pubkey_hex.clone(),
92 role: MemberRole::Owner,
93 timestamp: hlc.now().to_string(),
94 author_pubkey: String::new(),
95 signature: String::new(),
96 };
97
98 sign_membership_entry(&mut founder, user_keypair);
99
100 let mut chain = MembershipChain::new();
101 chain
102 .add_entry(founder.clone())
103 .map_err(|e| MembershipOpsError(format!("Failed to create founder entry: {e}")))?;
104
105 let founder_bytes = serde_json::to_vec(&founder)
107 .map_err(|e| MembershipOpsError(format!("Failed to serialize founder entry: {e}")))?;
108 storage
109 .put_membership_entry(&user_pubkey_hex, 1, founder_bytes)
110 .await
111 .map_err(|e| MembershipOpsError(format!("Failed to upload founder entry: {e}")))?;
112
113 info!("Bootstrapped membership chain with founder entry");
114
115 chain
116 } else {
117 download_chain(storage, &entry_keys).await?
118 };
119
120 let invite_ts = hlc.now().to_string();
122 let join_info = super::invite::create_invitation(
123 storage,
124 cloud_home,
125 &mut chain,
126 user_keypair,
127 public_key_hex,
128 role,
129 encryption_key,
130 &invite_ts,
131 )
132 .await
133 .map_err(|e| MembershipOpsError(format!("Failed to create invitation: {e}")))?;
134
135 info!(
136 "Invited member {}...",
137 &public_key_hex[..public_key_hex.len().min(16)]
138 );
139
140 if let Err(e) = sync_authorized_keys(cloud_home, &chain).await {
142 warn!("Failed to sync authorized keys: {e}");
143 }
144
145 Ok(crate::join_code::InviteCode {
147 library_id: library_id.to_string(),
148 library_name: library_name.to_string(),
149 join_info,
150 owner_pubkey: user_pubkey_hex,
151 })
152}
153
154pub async fn remove_member(
161 storage: &dyn SyncStorage,
162 cloud_home: &dyn crate::storage::cloud::CloudHome,
163 user_keypair: &UserKeypair,
164 hlc: &Hlc,
165 public_key_hex: &str,
166) -> Result<[u8; 32], MembershipOpsError> {
167 let entry_keys = storage
169 .list_membership_entries()
170 .await
171 .map_err(|e| MembershipOpsError(format!("Failed to list membership entries: {e}")))?;
172
173 if entry_keys.is_empty() {
174 return Err(MembershipOpsError("No membership chain exists".to_string()));
175 }
176
177 let mut chain = download_chain(storage, &entry_keys).await?;
178
179 let revoke_ts = hlc.now().to_string();
181 let new_key = super::invite::revoke_member(
182 storage,
183 cloud_home,
184 &mut chain,
185 user_keypair,
186 public_key_hex,
187 &revoke_ts,
188 )
189 .await
190 .map_err(|e| MembershipOpsError(format!("Failed to revoke member: {e}")))?;
191
192 info!(
193 "Revoked member {}... and rotated encryption key",
194 &public_key_hex[..public_key_hex.len().min(16)]
195 );
196
197 if let Err(e) = sync_authorized_keys(cloud_home, &chain).await {
199 warn!("Failed to sync authorized keys: {e}");
200 }
201
202 Ok(new_key)
203}
204
205pub fn apply_key_rotation(
210 new_key: [u8; 32],
211 key_service: &KeyService,
212 encryption_lock: &std::sync::RwLock<EncryptionService>,
213) -> Result<String, MembershipOpsError> {
214 let new_key_hex = hex::encode(new_key);
215 key_service
216 .set_encryption_key(&new_key_hex)
217 .map_err(|e| MembershipOpsError(format!("Failed to persist new encryption key: {e}")))?;
218
219 {
220 let mut enc = encryption_lock.write().unwrap();
221 *enc = EncryptionService::from_key(new_key);
222 }
223
224 let new_fingerprint = encryption_lock.read().unwrap().fingerprint();
225 Ok(new_fingerprint)
226}
227
228pub(crate) async fn download_chain(
230 storage: &dyn SyncStorage,
231 entry_keys: &[(String, u64)],
232) -> Result<MembershipChain, MembershipOpsError> {
233 let mut raw_entries = Vec::new();
234 for (author, seq) in entry_keys {
235 let data = storage
236 .get_membership_entry(author, *seq)
237 .await
238 .map_err(|e| {
239 MembershipOpsError(format!(
240 "Failed to get membership entry {author}/{seq}: {e}"
241 ))
242 })?;
243
244 let entry: MembershipEntry = serde_json::from_slice(&data).map_err(|e| {
245 MembershipOpsError(format!(
246 "Failed to parse membership entry {author}/{seq}: {e}"
247 ))
248 })?;
249 raw_entries.push(entry);
250 }
251
252 MembershipChain::from_entries(raw_entries)
253 .map_err(|e| MembershipOpsError(format!("Invalid membership chain: {e}")))
254}
255
256pub async fn sync_authorized_keys(
262 cloud_home: &dyn crate::storage::cloud::CloudHome,
263 chain: &MembershipChain,
264) -> Result<(), MembershipOpsError> {
265 use std::collections::HashSet;
266
267 let current: HashSet<String> = chain
268 .current_members()
269 .into_iter()
270 .map(|(pk, _)| pk)
271 .collect();
272
273 let existing = cloud_home
274 .list("auth/keys/")
275 .await
276 .map_err(|e| MembershipOpsError(format!("list auth keys: {e}")))?;
277 let existing_keys: HashSet<String> = existing
278 .iter()
279 .filter_map(|k| k.strip_prefix("auth/keys/"))
280 .map(|s| s.to_string())
281 .collect();
282
283 for pk in ¤t {
284 if !existing_keys.contains(pk) {
285 cloud_home
286 .write(&format!("auth/keys/{pk}"), vec![])
287 .await
288 .map_err(|e| MembershipOpsError(format!("write auth key: {e}")))?;
289 }
290 }
291
292 for pk in &existing_keys {
293 if !current.contains(pk) {
294 cloud_home
295 .delete(&format!("auth/keys/{pk}"))
296 .await
297 .map_err(|e| MembershipOpsError(format!("delete auth key: {e}")))?;
298 }
299 }
300
301 Ok(())
302}
303
304#[derive(Debug, thiserror::Error)]
306#[error("{0}")]
307pub struct MembershipOpsError(pub String);