Skip to main content

river_core/room_state/
secret.rs

1use crate::room_state::member::MemberId;
2use crate::room_state::privacy::{RoomCipherSpec, SecretVersion};
3use crate::room_state::ChatRoomParametersV1;
4use crate::util::{sign_struct, verify_struct};
5use crate::ChatRoomStateV1;
6use ed25519_dalek::{Signature, SigningKey, VerifyingKey};
7use freenet_scaffold::ComposableState;
8use serde::{Deserialize, Serialize};
9use std::collections::{HashMap, HashSet};
10use std::time::SystemTime;
11
12/// Room secrets state managing encrypted secret distribution
13#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
14pub struct RoomSecretsV1 {
15    pub current_version: SecretVersion,
16    pub versions: Vec<AuthorizedSecretVersionRecord>,
17    pub encrypted_secrets: Vec<AuthorizedEncryptedSecretForMember>,
18}
19
20impl ComposableState for RoomSecretsV1 {
21    type ParentState = ChatRoomStateV1;
22    type Summary = SecretsSummary;
23    type Delta = SecretsDelta;
24    type Parameters = ChatRoomParametersV1;
25
26    fn verify(
27        &self,
28        _parent_state: &Self::ParentState,
29        parameters: &Self::Parameters,
30    ) -> Result<(), String> {
31        // Verify all secret version records are signed by owner
32        for version_record in &self.versions {
33            version_record
34                .verify_signature(&parameters.owner)
35                .map_err(|e| format!("Invalid version record signature: {}", e))?;
36        }
37
38        // Verify all encrypted secrets are signed by owner
39        for encrypted_secret in &self.encrypted_secrets {
40            encrypted_secret
41                .verify_signature(&parameters.owner)
42                .map_err(|e| format!("Invalid encrypted secret signature: {}", e))?;
43        }
44
45        // Verify current_version matches the maximum version in versions
46        if let Some(max_version) = self.versions.iter().map(|v| v.record.version).max() {
47            if self.current_version != max_version {
48                return Err(format!(
49                    "Current version {} does not match maximum version {}",
50                    self.current_version, max_version
51                ));
52            }
53        } else if self.current_version != 0 {
54            return Err("Current version is non-zero but no version records exist".to_string());
55        }
56
57        Ok(())
58    }
59
60    fn summarize(
61        &self,
62        _parent_state: &Self::ParentState,
63        _parameters: &Self::Parameters,
64    ) -> Self::Summary {
65        let version_ids: HashSet<SecretVersion> =
66            self.versions.iter().map(|v| v.record.version).collect();
67
68        let member_secrets: HashSet<(SecretVersion, MemberId)> = self
69            .encrypted_secrets
70            .iter()
71            .map(|s| (s.secret.secret_version, s.secret.member_id))
72            .collect();
73
74        SecretsSummary {
75            current_version: self.current_version,
76            version_ids,
77            member_secrets,
78        }
79    }
80
81    fn delta(
82        &self,
83        _parent_state: &Self::ParentState,
84        _parameters: &Self::Parameters,
85        old_state_summary: &Self::Summary,
86    ) -> Option<Self::Delta> {
87        let new_versions: Vec<AuthorizedSecretVersionRecord> = self
88            .versions
89            .iter()
90            .filter(|v| !old_state_summary.version_ids.contains(&v.record.version))
91            .cloned()
92            .collect();
93
94        let new_encrypted_secrets: Vec<AuthorizedEncryptedSecretForMember> = self
95            .encrypted_secrets
96            .iter()
97            .filter(|s| {
98                !old_state_summary
99                    .member_secrets
100                    .contains(&(s.secret.secret_version, s.secret.member_id))
101            })
102            .cloned()
103            .collect();
104
105        if new_versions.is_empty()
106            && new_encrypted_secrets.is_empty()
107            && self.current_version == old_state_summary.current_version
108        {
109            None
110        } else {
111            Some(SecretsDelta {
112                current_version: if self.current_version > old_state_summary.current_version {
113                    Some(self.current_version)
114                } else {
115                    None
116                },
117                new_versions,
118                new_encrypted_secrets,
119            })
120        }
121    }
122
123    fn apply_delta(
124        &mut self,
125        parent_state: &Self::ParentState,
126        parameters: &Self::Parameters,
127        delta: &Option<Self::Delta>,
128    ) -> Result<(), String> {
129        // Transactional: validate and stage all changes against a working
130        // copy of `self`. Only commit (`*self = working`) if every check
131        // passes. Bug #3 PR A — previously, a failing sub-check after
132        // `versions.push(...)` left `self` half-mutated: `versions` had
133        // gained a record, but `current_version` / `encrypted_secrets` /
134        // post-prune cleanup never ran, and `recent_messages` (later in the
135        // composable `apply_delta`) was skipped entirely by the `?`
136        // short-circuit. That partial state then survived as the new
137        // baseline, silently corrupting the room and breaking CRDT
138        // convergence. Building a working copy and only committing on
139        // success makes apply_delta all-or-nothing.
140        let mut working = self.clone();
141
142        if let Some(delta) = delta {
143            // Verify and stage new version records
144            for version_record in &delta.new_versions {
145                version_record
146                    .verify_signature(&parameters.owner)
147                    .map_err(|e| format!("Invalid version record signature in delta: {}", e))?;
148
149                // Check for duplicate version
150                if working
151                    .versions
152                    .iter()
153                    .any(|v| v.record.version == version_record.record.version)
154                {
155                    return Err(format!(
156                        "Duplicate secret version: {}",
157                        version_record.record.version
158                    ));
159                }
160
161                working.versions.push(version_record.clone());
162            }
163
164            // Verify and stage new encrypted secrets
165            let members_by_id = parent_state.members.members_by_member_id();
166            for encrypted_secret in &delta.new_encrypted_secrets {
167                encrypted_secret
168                    .verify_signature(&parameters.owner)
169                    .map_err(|e| format!("Invalid encrypted secret signature in delta: {}", e))?;
170
171                let member_id = encrypted_secret.secret.member_id;
172
173                // Skip secrets for removed members — they'll be pruned below.
174                if member_id != parameters.owner_id() && !members_by_id.contains_key(&member_id) {
175                    continue;
176                }
177
178                // Verify secret version exists (in the staged working copy,
179                // so a same-delta new_versions + new_encrypted_secrets pair
180                // resolves correctly).
181                if !working
182                    .versions
183                    .iter()
184                    .any(|v| v.record.version == encrypted_secret.secret.secret_version)
185                {
186                    return Err(format!(
187                        "Encrypted secret references non-existent version: {}",
188                        encrypted_secret.secret.secret_version
189                    ));
190                }
191
192                // Check for duplicate (version, member_id) pair
193                if working.encrypted_secrets.iter().any(|s| {
194                    s.secret.secret_version == encrypted_secret.secret.secret_version
195                        && s.secret.member_id == member_id
196                }) {
197                    return Err(format!(
198                        "Duplicate encrypted secret for member {:?} version {}",
199                        member_id, encrypted_secret.secret.secret_version
200                    ));
201                }
202
203                working.encrypted_secrets.push(encrypted_secret.clone());
204            }
205
206            // Update current version if provided
207            if let Some(new_version) = delta.current_version {
208                if new_version <= working.current_version {
209                    return Err(format!(
210                        "New current version {} must be greater than existing version {}",
211                        new_version, working.current_version
212                    ));
213                }
214
215                // Verify the new version exists in versions
216                if !working
217                    .versions
218                    .iter()
219                    .any(|v| v.record.version == new_version)
220                {
221                    return Err(format!(
222                        "Cannot set current version to non-existent version: {}",
223                        new_version
224                    ));
225                }
226
227                working.current_version = new_version;
228            }
229
230            // Prune encrypted secrets for removed members
231            let owner_id = parameters.owner_id();
232            working.encrypted_secrets.retain(|s| {
233                s.secret.member_id == owner_id || members_by_id.contains_key(&s.secret.member_id)
234            });
235        }
236
237        // Sort for deterministic ordering (CRDT convergence requirement)
238        working.versions.sort_by_key(|v| v.record.version);
239        working
240            .encrypted_secrets
241            .sort_by_key(|s| (s.secret.secret_version, s.secret.member_id));
242
243        // Commit: every check passed, so move the working copy into self.
244        *self = working;
245        Ok(())
246    }
247}
248
249/// Summary of room secrets state for delta calculation
250#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
251pub struct SecretsSummary {
252    pub current_version: SecretVersion,
253    pub version_ids: HashSet<SecretVersion>,
254    pub member_secrets: HashSet<(SecretVersion, MemberId)>,
255}
256
257/// Delta for room secrets state
258#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
259pub struct SecretsDelta {
260    pub current_version: Option<SecretVersion>,
261    pub new_versions: Vec<AuthorizedSecretVersionRecord>,
262    pub new_encrypted_secrets: Vec<AuthorizedEncryptedSecretForMember>,
263}
264
265/// Metadata about a secret version
266#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
267pub struct SecretVersionRecordV1 {
268    pub version: SecretVersion,
269    pub cipher_spec: RoomCipherSpec,
270    pub created_at: SystemTime,
271}
272
273/// Authorized secret version record signed by room owner
274#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
275pub struct AuthorizedSecretVersionRecord {
276    pub record: SecretVersionRecordV1,
277    pub owner_signature: Signature,
278}
279
280impl AuthorizedSecretVersionRecord {
281    pub fn new(record: SecretVersionRecordV1, owner_signing_key: &SigningKey) -> Self {
282        let signature = sign_struct(&record, owner_signing_key);
283        Self {
284            record,
285            owner_signature: signature,
286        }
287    }
288
289    /// Create an AuthorizedSecretVersionRecord with a pre-computed signature.
290    /// Use this when signing is done externally (e.g., via delegate).
291    pub fn with_signature(record: SecretVersionRecordV1, owner_signature: Signature) -> Self {
292        Self {
293            record,
294            owner_signature,
295        }
296    }
297
298    pub fn verify_signature(&self, owner_verifying_key: &VerifyingKey) -> Result<(), String> {
299        verify_struct(&self.record, &self.owner_signature, owner_verifying_key)
300            .map_err(|e| format!("Invalid signature: {}", e))
301    }
302}
303
304/// Encrypted secret blob for a specific member
305#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
306pub struct EncryptedSecretForMemberV1 {
307    pub member_id: MemberId,
308    pub secret_version: SecretVersion,
309    pub ciphertext: Vec<u8>,
310    pub nonce: [u8; 12],
311    pub sender_ephemeral_public_key: [u8; 32],
312    pub provider: MemberId,
313}
314
315/// Authorized encrypted secret signed by room owner
316#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
317pub struct AuthorizedEncryptedSecretForMember {
318    pub secret: EncryptedSecretForMemberV1,
319    pub owner_signature: Signature,
320}
321
322impl AuthorizedEncryptedSecretForMember {
323    pub fn new(secret: EncryptedSecretForMemberV1, owner_signing_key: &SigningKey) -> Self {
324        let signature = sign_struct(&secret, owner_signing_key);
325        Self {
326            secret,
327            owner_signature: signature,
328        }
329    }
330
331    /// Create an AuthorizedEncryptedSecretForMember with a pre-computed signature.
332    /// Use this when signing is done externally (e.g., via delegate).
333    pub fn with_signature(secret: EncryptedSecretForMemberV1, owner_signature: Signature) -> Self {
334        Self {
335            secret,
336            owner_signature,
337        }
338    }
339
340    pub fn verify_signature(&self, owner_verifying_key: &VerifyingKey) -> Result<(), String> {
341        verify_struct(&self.secret, &self.owner_signature, owner_verifying_key)
342            .map_err(|e| format!("Invalid signature: {}", e))
343    }
344}
345
346/// Build the list of `AuthorizedEncryptedSecretForMember` records to emit
347/// in a rotation update.
348///
349/// For each current member (owner + each in `current_members_with_vks`),
350/// for each version `v` in `[0..=new_version]`:
351/// * If `(member, v)` is already in `existing_encrypted_secrets`, skip —
352///   the room state already has that pair and emitting it again would be
353///   rejected by `RoomSecretsV1::apply_delta`'s duplicate guard, wedging
354///   rotation permanently.
355/// * Otherwise, emit a fresh `AuthorizedEncryptedSecretForMember` that
356///   encrypts the per-version secret for the member's VK.
357///
358/// Per-version secrets are sourced as follows:
359/// * `new_version` → `new_secret` (the value the caller just derived).
360/// * Any prior `v < new_version` → RECOVERED by ECIES-decrypting the
361///   owner's existing `encrypted_secret`-at-v using the owner's signing
362///   key. The owner has the signing key, so they can decrypt the blob
363///   they originally produced for themselves and recover the actual
364///   secret bytes the room is really using. We do NOT re-derive via
365///   `derive_room_secret`: River's UI generates v0 randomly at room
366///   creation (`ui/src/room_data.rs:create_new_room_with_name`), so a
367///   derived v0 would not match what was sealed under the actual v0.
368///
369/// If a prior version's secret can't be recovered (no owner blob at
370/// that version, or decrypt fails), entries at that version are
371/// skipped. The newly-joined member won't decrypt content sealed at
372/// that version, but nobody else can either — the data is irrecoverable.
373///
374/// Determining continuing-vs-newly-joined per `(member, version)`
375/// directly from `existing_encrypted_secrets` (rather than from a
376/// caller-local cache) is deliberate: the local cache can be missing
377/// (fresh delegate, restart, webapp reinstall), and using it as the
378/// dedup source would produce duplicate `(member, version)` pairs that
379/// the contract rejects.
380///
381/// Pure function, no I/O — extracted so the UI's synchronous
382/// `rotate_secret` fast-path and the chat-delegate's asynchronous
383/// rotation pipeline produce byte-identical blob sets for the same
384/// inputs. See issue #271 and Bug #3 PR B (Ivvor 2026-05-17).
385#[cfg(feature = "ecies")]
386#[allow(clippy::too_many_arguments)]
387pub fn build_rotation_encrypted_secrets(
388    signing_key: &SigningKey,
389    owner_vk: &VerifyingKey,
390    owner_id: MemberId,
391    new_version: SecretVersion,
392    new_secret: &[u8; 32],
393    current_members_with_vks: &[(MemberId, VerifyingKey)],
394    existing_encrypted_secrets: &[AuthorizedEncryptedSecretForMember],
395) -> Result<Vec<AuthorizedEncryptedSecretForMember>, String> {
396    use crate::ecies::{decrypt_secret_from_member_blob_raw, encrypt_secret_for_member};
397    use std::collections::{BTreeMap, BTreeSet};
398
399    // What's already on the wire — never re-emit any of these.
400    let existing: BTreeSet<(MemberId, SecretVersion)> = existing_encrypted_secrets
401        .iter()
402        .map(|s| (s.secret.member_id, s.secret.secret_version))
403        .collect();
404
405    // Recover prior-version secrets by decrypting the owner's existing
406    // blobs. If decrypt fails (malformed blob, unexpected sender) we just
407    // skip — defensive, shouldn't happen on well-formed state.
408    let mut prior_secrets: BTreeMap<SecretVersion, [u8; 32]> = BTreeMap::new();
409    for blob in existing_encrypted_secrets {
410        if blob.secret.member_id != owner_id {
411            continue;
412        }
413        if blob.secret.secret_version >= new_version {
414            continue;
415        }
416        if prior_secrets.contains_key(&blob.secret.secret_version) {
417            // First-wins. Should not happen — contract dedups
418            // (member, version) — but be defensive.
419            //
420            // Surface a warning when this actually fires in practice
421            // so we can investigate. Using `eprintln!` because
422            // river-core has no logging dependency (intentionally,
423            // to keep the room-contract WASM small); this is a
424            // defensive log so a no-op in WASM is acceptable, and
425            // native tests / native delegate builds will still show
426            // it. See IMPORTANT #7 on PR #272 review round 2.
427            eprintln!(
428                "warn(build_rotation_encrypted_secrets): duplicate owner blob \
429                 at version {} (first-wins applied); contract should have \
430                 dedup'd (member, version) — investigate",
431                blob.secret.secret_version
432            );
433            continue;
434        }
435        if let Ok(s) = decrypt_secret_from_member_blob_raw(
436            &blob.secret.ciphertext,
437            &blob.secret.nonce,
438            &blob.secret.sender_ephemeral_public_key,
439            signing_key,
440        ) {
441            prior_secrets.insert(blob.secret.secret_version, s);
442        }
443    }
444    // The new version's secret is whatever the caller just derived.
445    prior_secrets.insert(new_version, *new_secret);
446
447    let mut out: Vec<AuthorizedEncryptedSecretForMember> = Vec::new();
448
449    // Owner + every current member.
450    let all_members =
451        std::iter::once((owner_id, *owner_vk)).chain(current_members_with_vks.iter().copied());
452
453    // Iterate the versions we actually have secrets for (not the full
454    // numeric range `0..=new_version`). Secret versions are NOT required
455    // to be contiguous — `RoomSecretsV1::apply_delta` only enforces
456    // monotonicity of `current_version`, so a valid owner-signed state
457    // could legitimately jump from v0 to v1_000_000_000, and the next
458    // rotation would otherwise loop a billion times per member checking
459    // versions that have no recoverable secret. See Codex review of
460    // PR #272 (third pass).
461    for (member_id, member_vk) in all_members {
462        for (&v, secret_for_version) in &prior_secrets {
463            if existing.contains(&(member_id, v)) {
464                continue;
465            }
466            let (ciphertext, nonce, ephemeral_key) =
467                encrypt_secret_for_member(secret_for_version, &member_vk);
468            let secret_struct = EncryptedSecretForMemberV1 {
469                member_id,
470                secret_version: v,
471                ciphertext,
472                nonce,
473                sender_ephemeral_public_key: ephemeral_key.to_bytes(),
474                provider: owner_id,
475            };
476            out.push(AuthorizedEncryptedSecretForMember::new(
477                secret_struct,
478                signing_key,
479            ));
480        }
481    }
482
483    Ok(out)
484}
485
486impl RoomSecretsV1 {
487    /// Check if all current members have encrypted blobs for the current version
488    pub fn has_complete_distribution(
489        &self,
490        members: &HashMap<MemberId, &crate::room_state::member::AuthorizedMember>,
491    ) -> bool {
492        if self.current_version == 0 {
493            return true; // No secrets yet
494        }
495
496        let member_ids_with_current: HashSet<MemberId> = self
497            .encrypted_secrets
498            .iter()
499            .filter(|s| s.secret.secret_version == self.current_version)
500            .map(|s| s.secret.member_id)
501            .collect();
502
503        members
504            .keys()
505            .all(|id| member_ids_with_current.contains(id))
506    }
507}
508
509#[cfg(test)]
510mod tests {
511    use super::*;
512    use crate::room_state::member::{AuthorizedMember, Member};
513    use ed25519_dalek::SigningKey;
514    use rand::rngs::OsRng;
515
516    fn create_test_state_and_params() -> (ChatRoomStateV1, ChatRoomParametersV1, SigningKey) {
517        let owner_signing_key = SigningKey::generate(&mut OsRng);
518        let owner_verifying_key = owner_signing_key.verifying_key();
519
520        let state = ChatRoomStateV1::default();
521        let params = ChatRoomParametersV1 {
522            owner: owner_verifying_key,
523        };
524
525        (state, params, owner_signing_key)
526    }
527
528    fn create_version_record(
529        version: SecretVersion,
530        owner_sk: &SigningKey,
531    ) -> AuthorizedSecretVersionRecord {
532        let record = SecretVersionRecordV1 {
533            version,
534            cipher_spec: RoomCipherSpec::Aes256Gcm,
535            created_at: SystemTime::now(),
536        };
537        AuthorizedSecretVersionRecord::new(record, owner_sk)
538    }
539
540    fn create_encrypted_secret(
541        member_id: MemberId,
542        version: SecretVersion,
543        owner_sk: &SigningKey,
544    ) -> AuthorizedEncryptedSecretForMember {
545        let secret = EncryptedSecretForMemberV1 {
546            member_id,
547            secret_version: version,
548            ciphertext: vec![1, 2, 3, 4],
549            nonce: [0u8; 12],
550            sender_ephemeral_public_key: [0u8; 32],
551            provider: member_id,
552        };
553        AuthorizedEncryptedSecretForMember::new(secret, owner_sk)
554    }
555
556    #[test]
557    fn test_room_secrets_v1_default() {
558        let secrets = RoomSecretsV1::default();
559        assert_eq!(secrets.current_version, 0);
560        assert!(secrets.versions.is_empty());
561        assert!(secrets.encrypted_secrets.is_empty());
562    }
563
564    #[test]
565    fn test_authorized_secret_version_record() {
566        let owner_signing_key = SigningKey::generate(&mut OsRng);
567        let owner_verifying_key = owner_signing_key.verifying_key();
568
569        let record = SecretVersionRecordV1 {
570            version: 1,
571            cipher_spec: RoomCipherSpec::Aes256Gcm,
572            created_at: SystemTime::now(),
573        };
574
575        let authorized_record =
576            AuthorizedSecretVersionRecord::new(record.clone(), &owner_signing_key);
577
578        assert_eq!(authorized_record.record, record);
579        assert!(authorized_record
580            .verify_signature(&owner_verifying_key)
581            .is_ok());
582
583        // Test with wrong key
584        let wrong_key = SigningKey::generate(&mut OsRng).verifying_key();
585        assert!(authorized_record.verify_signature(&wrong_key).is_err());
586    }
587
588    #[test]
589    fn test_authorized_encrypted_secret_for_member() {
590        let owner_signing_key = SigningKey::generate(&mut OsRng);
591        let owner_verifying_key = owner_signing_key.verifying_key();
592        let member_id = MemberId::from(&owner_verifying_key);
593
594        let secret = EncryptedSecretForMemberV1 {
595            member_id,
596            secret_version: 1,
597            ciphertext: vec![1, 2, 3, 4],
598            nonce: [0u8; 12],
599            sender_ephemeral_public_key: [0u8; 32],
600            provider: member_id,
601        };
602
603        let authorized_secret =
604            AuthorizedEncryptedSecretForMember::new(secret.clone(), &owner_signing_key);
605
606        assert_eq!(authorized_secret.secret, secret);
607        assert!(authorized_secret
608            .verify_signature(&owner_verifying_key)
609            .is_ok());
610
611        // Test with wrong key
612        let wrong_key = SigningKey::generate(&mut OsRng).verifying_key();
613        assert!(authorized_secret.verify_signature(&wrong_key).is_err());
614    }
615
616    // ============================================================================
617    // COMPREHENSIVE COMPOSABLESTATE TESTS
618    // ============================================================================
619
620    #[test]
621    fn test_verify_empty_state() {
622        let (state, params, _) = create_test_state_and_params();
623        let secrets = RoomSecretsV1::default();
624
625        assert!(secrets.verify(&state, &params).is_ok());
626    }
627
628    #[test]
629    fn test_verify_valid_state_with_version() {
630        let (state, params, owner_sk) = create_test_state_and_params();
631        let owner_id = params.owner_id();
632
633        let mut secrets = RoomSecretsV1 {
634            current_version: 1,
635            ..Default::default()
636        };
637        secrets.versions.push(create_version_record(1, &owner_sk));
638        secrets
639            .encrypted_secrets
640            .push(create_encrypted_secret(owner_id, 1, &owner_sk));
641
642        assert!(secrets.verify(&state, &params).is_ok());
643    }
644
645    #[test]
646    fn test_verify_fails_with_invalid_version_signature() {
647        let (state, params, _owner_sk) = create_test_state_and_params();
648        let wrong_sk = SigningKey::generate(&mut OsRng);
649
650        let mut secrets = RoomSecretsV1 {
651            current_version: 1,
652            ..Default::default()
653        };
654        secrets.versions.push(create_version_record(1, &wrong_sk)); // Wrong signature!
655
656        let result = secrets.verify(&state, &params);
657        assert!(result.is_err());
658        assert!(result
659            .unwrap_err()
660            .contains("Invalid version record signature"));
661    }
662
663    #[test]
664    fn test_verify_fails_with_invalid_secret_signature() {
665        let (state, params, owner_sk) = create_test_state_and_params();
666        let owner_id = params.owner_id();
667        let wrong_sk = SigningKey::generate(&mut OsRng);
668
669        let mut secrets = RoomSecretsV1 {
670            current_version: 1,
671            ..Default::default()
672        };
673        secrets.versions.push(create_version_record(1, &owner_sk));
674        secrets
675            .encrypted_secrets
676            .push(create_encrypted_secret(owner_id, 1, &wrong_sk)); // Wrong signature!
677
678        let result = secrets.verify(&state, &params);
679        assert!(result.is_err());
680        assert!(result
681            .unwrap_err()
682            .contains("Invalid encrypted secret signature"));
683    }
684
685    #[test]
686    fn test_verify_fails_with_mismatched_current_version() {
687        let (state, params, owner_sk) = create_test_state_and_params();
688
689        let mut secrets = RoomSecretsV1 {
690            current_version: 2,
691            ..Default::default()
692        }; // Mismatch!
693        secrets.versions.push(create_version_record(1, &owner_sk));
694
695        let result = secrets.verify(&state, &params);
696        assert!(result.is_err());
697        assert!(result
698            .unwrap_err()
699            .contains("does not match maximum version"));
700    }
701
702    #[test]
703    fn test_verify_fails_with_nonzero_current_but_no_versions() {
704        let (state, params, _) = create_test_state_and_params();
705
706        let secrets = RoomSecretsV1 {
707            current_version: 1,
708            ..Default::default()
709        };
710        // No versions!
711
712        let result = secrets.verify(&state, &params);
713        assert!(result.is_err());
714        assert!(result.unwrap_err().contains("no version records exist"));
715    }
716
717    #[test]
718    fn test_summarize_empty_state() {
719        let (state, params, _) = create_test_state_and_params();
720        let secrets = RoomSecretsV1::default();
721
722        let summary = secrets.summarize(&state, &params);
723        assert_eq!(summary.current_version, 0);
724        assert!(summary.version_ids.is_empty());
725        assert!(summary.member_secrets.is_empty());
726    }
727
728    #[test]
729    fn test_summarize_with_data() {
730        let (state, params, owner_sk) = create_test_state_and_params();
731        let owner_id = params.owner_id();
732
733        let mut secrets = RoomSecretsV1 {
734            current_version: 2,
735            ..Default::default()
736        };
737        secrets.versions.push(create_version_record(1, &owner_sk));
738        secrets.versions.push(create_version_record(2, &owner_sk));
739        secrets
740            .encrypted_secrets
741            .push(create_encrypted_secret(owner_id, 1, &owner_sk));
742        secrets
743            .encrypted_secrets
744            .push(create_encrypted_secret(owner_id, 2, &owner_sk));
745
746        let summary = secrets.summarize(&state, &params);
747        assert_eq!(summary.current_version, 2);
748        assert_eq!(summary.version_ids.len(), 2);
749        assert!(summary.version_ids.contains(&1));
750        assert!(summary.version_ids.contains(&2));
751        assert_eq!(summary.member_secrets.len(), 2);
752        assert!(summary.member_secrets.contains(&(1, owner_id)));
753        assert!(summary.member_secrets.contains(&(2, owner_id)));
754    }
755
756    #[test]
757    fn test_delta_no_changes() {
758        let (state, params, _) = create_test_state_and_params();
759        let secrets = RoomSecretsV1::default();
760        let summary = secrets.summarize(&state, &params);
761
762        let delta = secrets.delta(&state, &params, &summary);
763        assert!(delta.is_none());
764    }
765
766    #[test]
767    fn test_delta_new_version() {
768        let (state, params, owner_sk) = create_test_state_and_params();
769        let owner_id = params.owner_id();
770
771        let mut secrets = RoomSecretsV1 {
772            current_version: 1,
773            ..Default::default()
774        };
775        secrets.versions.push(create_version_record(1, &owner_sk));
776        secrets
777            .encrypted_secrets
778            .push(create_encrypted_secret(owner_id, 1, &owner_sk));
779
780        let old_summary = SecretsSummary {
781            current_version: 0,
782            version_ids: HashSet::new(),
783            member_secrets: HashSet::new(),
784        };
785
786        let delta = secrets.delta(&state, &params, &old_summary).unwrap();
787        assert_eq!(delta.current_version, Some(1));
788        assert_eq!(delta.new_versions.len(), 1);
789        assert_eq!(delta.new_encrypted_secrets.len(), 1);
790    }
791
792    #[test]
793    fn test_delta_partial_update() {
794        let (state, params, owner_sk) = create_test_state_and_params();
795        let owner_id = params.owner_id();
796
797        let mut secrets = RoomSecretsV1 {
798            current_version: 2,
799            ..Default::default()
800        };
801        secrets.versions.push(create_version_record(1, &owner_sk));
802        secrets.versions.push(create_version_record(2, &owner_sk));
803        secrets
804            .encrypted_secrets
805            .push(create_encrypted_secret(owner_id, 1, &owner_sk));
806        secrets
807            .encrypted_secrets
808            .push(create_encrypted_secret(owner_id, 2, &owner_sk));
809
810        let mut old_summary = SecretsSummary {
811            current_version: 1,
812            version_ids: HashSet::new(),
813            member_secrets: HashSet::new(),
814        };
815        old_summary.version_ids.insert(1);
816        old_summary.member_secrets.insert((1, owner_id));
817
818        let delta = secrets.delta(&state, &params, &old_summary).unwrap();
819        assert_eq!(delta.current_version, Some(2));
820        assert_eq!(delta.new_versions.len(), 1);
821        assert_eq!(delta.new_versions[0].record.version, 2);
822        assert_eq!(delta.new_encrypted_secrets.len(), 1);
823        assert_eq!(delta.new_encrypted_secrets[0].secret.secret_version, 2);
824    }
825
826    #[test]
827    fn test_apply_delta_add_first_version() {
828        let (state, params, owner_sk) = create_test_state_and_params();
829        let owner_id = params.owner_id();
830
831        let mut secrets = RoomSecretsV1::default();
832
833        let delta = SecretsDelta {
834            current_version: Some(1),
835            new_versions: vec![create_version_record(1, &owner_sk)],
836            new_encrypted_secrets: vec![create_encrypted_secret(owner_id, 1, &owner_sk)],
837        };
838
839        let result = secrets.apply_delta(&state, &params, &Some(delta));
840        assert!(result.is_ok(), "Failed: {:?}", result.err());
841        assert_eq!(secrets.current_version, 1);
842        assert_eq!(secrets.versions.len(), 1);
843        assert_eq!(secrets.encrypted_secrets.len(), 1);
844    }
845
846    #[test]
847    fn test_apply_delta_rejects_duplicate_version() {
848        let (state, params, owner_sk) = create_test_state_and_params();
849
850        let mut secrets = RoomSecretsV1 {
851            current_version: 1,
852            ..Default::default()
853        };
854        secrets.versions.push(create_version_record(1, &owner_sk));
855
856        let delta = SecretsDelta {
857            current_version: None,
858            new_versions: vec![create_version_record(1, &owner_sk)], // Duplicate!
859            new_encrypted_secrets: vec![],
860        };
861
862        let result = secrets.apply_delta(&state, &params, &Some(delta));
863        assert!(result.is_err());
864        assert!(result.unwrap_err().contains("Duplicate secret version"));
865    }
866
867    #[test]
868    fn test_apply_delta_skips_secret_for_nonexistent_member() {
869        let (state, params, owner_sk) = create_test_state_and_params();
870        let fake_member_id = MemberId::from(&SigningKey::generate(&mut OsRng).verifying_key());
871
872        let mut secrets = RoomSecretsV1 {
873            current_version: 1,
874            ..Default::default()
875        };
876        secrets.versions.push(create_version_record(1, &owner_sk));
877
878        let delta = SecretsDelta {
879            current_version: None,
880            new_versions: vec![],
881            new_encrypted_secrets: vec![create_encrypted_secret(fake_member_id, 1, &owner_sk)],
882        };
883
884        // Should succeed — secret for removed member is silently skipped
885        let result = secrets.apply_delta(&state, &params, &Some(delta));
886        assert!(
887            result.is_ok(),
888            "Should skip non-existent member, got: {:?}",
889            result.err()
890        );
891        // The secret should not have been added
892        assert!(
893            !secrets
894                .encrypted_secrets
895                .iter()
896                .any(|s| s.secret.member_id == fake_member_id),
897            "Secret for non-existent member should not be added"
898        );
899    }
900
901    #[test]
902    fn test_apply_delta_rejects_secret_for_nonexistent_version() {
903        let (state, params, owner_sk) = create_test_state_and_params();
904        let owner_id = params.owner_id();
905
906        let mut secrets = RoomSecretsV1::default();
907
908        let delta = SecretsDelta {
909            current_version: None,
910            new_versions: vec![],
911            new_encrypted_secrets: vec![create_encrypted_secret(owner_id, 99, &owner_sk)], // Version 99 doesn't exist!
912        };
913
914        let result = secrets.apply_delta(&state, &params, &Some(delta));
915        assert!(result.is_err());
916        assert!(result.unwrap_err().contains("non-existent version"));
917    }
918
919    #[test]
920    fn test_apply_delta_rejects_duplicate_member_secret() {
921        let (state, params, owner_sk) = create_test_state_and_params();
922        let owner_id = params.owner_id();
923
924        let mut secrets = RoomSecretsV1 {
925            current_version: 1,
926            ..Default::default()
927        };
928        secrets.versions.push(create_version_record(1, &owner_sk));
929        secrets
930            .encrypted_secrets
931            .push(create_encrypted_secret(owner_id, 1, &owner_sk));
932
933        let delta = SecretsDelta {
934            current_version: None,
935            new_versions: vec![],
936            new_encrypted_secrets: vec![create_encrypted_secret(owner_id, 1, &owner_sk)], // Duplicate!
937        };
938
939        let result = secrets.apply_delta(&state, &params, &Some(delta));
940        assert!(result.is_err());
941        assert!(result.unwrap_err().contains("Duplicate encrypted secret"));
942    }
943
944    #[test]
945    fn test_apply_delta_rejects_invalid_version_transition() {
946        let (state, params, owner_sk) = create_test_state_and_params();
947
948        let mut secrets = RoomSecretsV1 {
949            current_version: 2,
950            ..Default::default()
951        };
952        secrets.versions.push(create_version_record(1, &owner_sk));
953        secrets.versions.push(create_version_record(2, &owner_sk));
954
955        let delta = SecretsDelta {
956            current_version: Some(1), // Can't go backward!
957            new_versions: vec![],
958            new_encrypted_secrets: vec![],
959        };
960
961        let result = secrets.apply_delta(&state, &params, &Some(delta));
962        assert!(result.is_err());
963        assert!(result
964            .unwrap_err()
965            .contains("must be greater than existing version"));
966    }
967
968    #[test]
969    fn test_apply_delta_rejects_nonexistent_current_version() {
970        let (state, params, _owner_sk) = create_test_state_and_params();
971
972        let mut secrets = RoomSecretsV1::default();
973
974        let delta = SecretsDelta {
975            current_version: Some(99), // Version 99 doesn't exist!
976            new_versions: vec![],
977            new_encrypted_secrets: vec![],
978        };
979
980        let result = secrets.apply_delta(&state, &params, &Some(delta));
981        assert!(result.is_err());
982        assert!(result.unwrap_err().contains("non-existent version"));
983    }
984
985    #[test]
986    fn test_apply_delta_prunes_removed_member_secrets() {
987        let (mut state, params, owner_sk) = create_test_state_and_params();
988        let owner_id = params.owner_id();
989
990        // Add a member
991        let member_sk = SigningKey::generate(&mut OsRng);
992        let member_vk = member_sk.verifying_key();
993        let member_id = MemberId::from(&member_vk);
994
995        let member = Member {
996            owner_member_id: owner_id,
997            invited_by: owner_id,
998            member_vk,
999        };
1000        let auth_member = AuthorizedMember::new(member, &owner_sk);
1001        state.members.members.push(auth_member);
1002
1003        // Set up secrets with both owner and member
1004        let mut secrets = RoomSecretsV1 {
1005            current_version: 1,
1006            ..Default::default()
1007        };
1008        secrets.versions.push(create_version_record(1, &owner_sk));
1009        secrets
1010            .encrypted_secrets
1011            .push(create_encrypted_secret(owner_id, 1, &owner_sk));
1012        secrets
1013            .encrypted_secrets
1014            .push(create_encrypted_secret(member_id, 1, &owner_sk));
1015
1016        assert_eq!(secrets.encrypted_secrets.len(), 2);
1017
1018        // Remove the member
1019        state.members.members.clear();
1020
1021        // Apply empty delta (triggers pruning)
1022        let delta = SecretsDelta {
1023            current_version: None,
1024            new_versions: vec![],
1025            new_encrypted_secrets: vec![],
1026        };
1027
1028        let result = secrets.apply_delta(&state, &params, &Some(delta));
1029        assert!(result.is_ok());
1030
1031        // Member's secret should be pruned, owner's should remain
1032        assert_eq!(secrets.encrypted_secrets.len(), 1);
1033        assert_eq!(secrets.encrypted_secrets[0].secret.member_id, owner_id);
1034    }
1035
1036    #[test]
1037    fn test_has_complete_distribution_empty() {
1038        let secrets = RoomSecretsV1::default();
1039        let members = HashMap::new();
1040
1041        assert!(secrets.has_complete_distribution(&members));
1042    }
1043
1044    #[test]
1045    fn test_has_complete_distribution_complete() {
1046        let (_state, params, owner_sk) = create_test_state_and_params();
1047        let owner_id = params.owner_id();
1048
1049        let mut secrets = RoomSecretsV1 {
1050            current_version: 1,
1051            ..Default::default()
1052        };
1053        secrets.versions.push(create_version_record(1, &owner_sk));
1054        secrets
1055            .encrypted_secrets
1056            .push(create_encrypted_secret(owner_id, 1, &owner_sk));
1057
1058        let member = Member {
1059            owner_member_id: owner_id,
1060            invited_by: owner_id,
1061            member_vk: params.owner,
1062        };
1063        let auth_member = AuthorizedMember::new(member, &owner_sk);
1064
1065        let mut members = HashMap::new();
1066        members.insert(owner_id, &auth_member);
1067
1068        assert!(secrets.has_complete_distribution(&members));
1069    }
1070
1071    #[test]
1072    fn test_has_complete_distribution_incomplete() {
1073        let (_state, params, owner_sk) = create_test_state_and_params();
1074        let owner_id = params.owner_id();
1075
1076        let member_sk = SigningKey::generate(&mut OsRng);
1077        let member_vk = member_sk.verifying_key();
1078        let member_id = MemberId::from(&member_vk);
1079
1080        let mut secrets = RoomSecretsV1 {
1081            current_version: 1,
1082            ..Default::default()
1083        };
1084        secrets.versions.push(create_version_record(1, &owner_sk));
1085        secrets
1086            .encrypted_secrets
1087            .push(create_encrypted_secret(owner_id, 1, &owner_sk));
1088        // Missing secret for member_id!
1089
1090        let member = Member {
1091            owner_member_id: owner_id,
1092            invited_by: owner_id,
1093            member_vk,
1094        };
1095        let auth_member = AuthorizedMember::new(member, &owner_sk);
1096
1097        let mut members = HashMap::new();
1098        members.insert(member_id, &auth_member);
1099
1100        assert!(!secrets.has_complete_distribution(&members));
1101    }
1102
1103    /// Regression test: apply_delta should succeed when the delta contains
1104    /// encrypted secrets for a member that was simultaneously removed from
1105    /// parent_state.members (e.g. ban or max_members eviction).
1106    #[test]
1107    fn test_apply_delta_with_removed_member_secret() {
1108        let (mut state, params, owner_sk) = create_test_state_and_params();
1109        let owner_id = params.owner_id();
1110
1111        // Add a member
1112        let member_sk = SigningKey::generate(&mut OsRng);
1113        let member_vk = member_sk.verifying_key();
1114        let member_id = MemberId::from(&member_vk);
1115
1116        let member = Member {
1117            owner_member_id: owner_id,
1118            invited_by: owner_id,
1119            member_vk,
1120        };
1121        let auth_member = AuthorizedMember::new(member, &owner_sk);
1122        state.members.members.push(auth_member);
1123
1124        // Set up initial secrets with version 1
1125        let mut secrets = RoomSecretsV1 {
1126            current_version: 1,
1127            ..Default::default()
1128        };
1129        secrets.versions.push(create_version_record(1, &owner_sk));
1130        secrets
1131            .encrypted_secrets
1132            .push(create_encrypted_secret(owner_id, 1, &owner_sk));
1133        secrets
1134            .encrypted_secrets
1135            .push(create_encrypted_secret(member_id, 1, &owner_sk));
1136
1137        // Now remove the member (simulates ban)
1138        state.members.members.clear();
1139
1140        // Delta includes a new secret version with encrypted secret for removed member
1141        let delta = SecretsDelta {
1142            current_version: Some(2),
1143            new_versions: vec![create_version_record(2, &owner_sk)],
1144            new_encrypted_secrets: vec![
1145                create_encrypted_secret(owner_id, 2, &owner_sk),
1146                create_encrypted_secret(member_id, 2, &owner_sk), // member was removed
1147            ],
1148        };
1149
1150        // Previously this would error; now it should succeed
1151        let result = secrets.apply_delta(&state, &params, &Some(delta));
1152        assert!(
1153            result.is_ok(),
1154            "apply_delta should skip removed member's secret, got: {:?}",
1155            result.err()
1156        );
1157
1158        // Removed member's secrets should be pruned
1159        assert!(
1160            !secrets
1161                .encrypted_secrets
1162                .iter()
1163                .any(|s| s.secret.member_id == member_id),
1164            "Removed member's secrets should be pruned"
1165        );
1166
1167        // Owner's secrets should remain
1168        assert!(
1169            secrets
1170                .encrypted_secrets
1171                .iter()
1172                .any(|s| s.secret.member_id == owner_id && s.secret.secret_version == 2),
1173            "Owner's new secret should be present"
1174        );
1175    }
1176}