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