bitwarden_core/key_management/
account_cryptographic_state.rs

1//! User account cryptographic state
2//!
3//! This module contains initialization and unwrapping of the user account cryptographic state.
4//! The user account cryptographic state contains keys and cryptographic objects unlocked by
5//! the user-key, or protected by keys unlocked by the user-key.
6//!
7//! V1 users have only a private key protected by an AES256-CBC-HMAC user key.
8//! V2 users have a private key, a signing key, a signed public key and a signed security state,
9//! all protected by a Cose serialized AEAD key, currently XChaCha20-Poly1305.
10
11use std::sync::RwLock;
12
13use bitwarden_api_api::models::{AccountKeysRequestModel, SecurityStateModel};
14use bitwarden_crypto::{
15    CoseSerializable, CryptoError, EncString, KeyStore, KeyStoreContext,
16    PublicKeyEncryptionAlgorithm, SignatureAlgorithm, SignedPublicKey, SymmetricKeyAlgorithm,
17};
18use bitwarden_encoding::B64;
19use bitwarden_error::bitwarden_error;
20use serde::{Deserialize, Serialize};
21use thiserror::Error;
22use tracing::{info, instrument};
23#[cfg(feature = "wasm")]
24use tsify::Tsify;
25
26use crate::{
27    UserId,
28    key_management::{
29        KeyIds, PrivateKeyId, SecurityState, SignedSecurityState, SigningKeyId, SymmetricKeyId,
30    },
31};
32
33/// Errors that can occur during initialization of the account cryptographic state.
34#[derive(Debug, Error)]
35#[bitwarden_error(flat)]
36pub enum AccountCryptographyInitializationError {
37    /// The encryption algorithm from the user key does not match one of the encrypted items.
38    /// This would mean that the user's account is corrupt.
39    #[error("The encryption type of the user key does not match the account cryptographic state")]
40    WrongUserKeyType,
41    /// The provide user-key is incorrect or out-of-date. This may happen when a use-key changed
42    /// and a local unlock-method is not yet updated.
43    #[error("Wrong user key")]
44    WrongUserKey,
45    /// The decrypted data is corrupt.
46    #[error("Decryption succeeded but produced corrupt data")]
47    CorruptData,
48    /// The decrypted data is corrupt.
49    #[error("Signature or mac verification failed, the data may have been tampered with")]
50    TamperedData,
51    /// The key store is already initialized with account keys. Currently, updating keys is not a
52    /// supported operation
53    #[error("Key store is already initialized")]
54    KeyStoreAlreadyInitialized,
55    /// A generic cryptographic error occurred.
56    #[error("A generic cryptographic error occurred: {0}")]
57    GenericCrypto(CryptoError),
58}
59
60impl From<CryptoError> for AccountCryptographyInitializationError {
61    fn from(err: CryptoError) -> Self {
62        AccountCryptographyInitializationError::GenericCrypto(err)
63    }
64}
65
66/// Errors that can occur during rotation of the account cryptographic state.
67#[derive(Debug, Error)]
68#[bitwarden_error(flat)]
69pub enum RotateCryptographyStateError {
70    /// The key is missing from the key store
71    #[error("The provided key is missing from the key store")]
72    KeyMissing,
73    /// The provided data was invalid
74    #[error("The provided data was invalid")]
75    InvalidData,
76}
77
78/// Any keys / cryptographic protection "downstream" from the account symmetric key (user key).
79/// Private keys are protected by the user key.
80#[derive(Clone, Serialize, Deserialize)]
81#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
82#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
83#[allow(clippy::large_enum_variant)]
84pub enum WrappedAccountCryptographicState {
85    /// A V1 user has only a private key.
86    V1 {
87        /// The user's encryption private key, wrapped by the user key.
88        private_key: EncString,
89    },
90    /// A V2 user has a private key, a signing key, a signed public key and a signed security state.
91    /// The SignedPublicKey ensures that others can verify the public key is claimed by an identity
92    /// they want to share data to. The signed security state protects against cryptographic
93    /// downgrades.
94    V2 {
95        /// The user's encryption private key, wrapped by the user key.
96        private_key: EncString,
97        /// The user's public-key for the private key, signed by the user's signing key.
98        /// Note: This is optional for backwards compatibility. After a few releases, this will be
99        /// made non-optional once all clients store the response on sync.
100        signed_public_key: Option<SignedPublicKey>,
101        /// The user's signing key, wrapped by the user key.
102        signing_key: EncString,
103        /// The user's signed security state.
104        security_state: SignedSecurityState,
105    },
106}
107
108impl std::fmt::Debug for WrappedAccountCryptographicState {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        match self {
111            WrappedAccountCryptographicState::V1 { .. } => f
112                .debug_struct("WrappedAccountCryptographicState::V1")
113                .finish(),
114            WrappedAccountCryptographicState::V2 { .. } => f
115                .debug_struct("WrappedAccountCryptographicState::V2")
116                .finish(),
117        }
118    }
119}
120
121impl WrappedAccountCryptographicState {
122    /// Converts to a AccountKeysRequestModel in order to make API requests. Since the
123    /// [WrappedAccountCryptographicState] is encrypted, the key store needs to contain the
124    /// user key required to unlock this state.
125    pub fn to_request_model(
126        &self,
127        user_key: &SymmetricKeyId,
128        ctx: &mut KeyStoreContext<KeyIds>,
129    ) -> Result<AccountKeysRequestModel, AccountCryptographyInitializationError> {
130        let private_key = match self {
131            WrappedAccountCryptographicState::V1 { private_key }
132            | WrappedAccountCryptographicState::V2 { private_key, .. } => private_key.clone(),
133        };
134        let private_key_tmp_id = ctx.unwrap_private_key(*user_key, &private_key)?;
135        let public_key = ctx.get_public_key(private_key_tmp_id)?;
136
137        let signature_keypair = match self {
138            WrappedAccountCryptographicState::V1 { .. } => None,
139            WrappedAccountCryptographicState::V2 { signing_key, .. } => {
140                let signing_key_tmp_id = ctx.unwrap_signing_key(*user_key, signing_key)?;
141                let verifying_key = ctx.get_verifying_key(signing_key_tmp_id)?;
142                Some((signing_key.clone(), verifying_key))
143            }
144        };
145
146        Ok(AccountKeysRequestModel {
147            // Note: This property is deprecated and should be removed after a transition period.
148            user_key_encrypted_account_private_key: Some(private_key.to_string()),
149            // Note: This property is deprecated and should be removed after a transition period.
150            account_public_key: Some(B64::from(public_key.to_der()?).to_string()),
151            signature_key_pair: signature_keypair
152                .as_ref()
153                .map(|(signing_key, verifying_key)| {
154                    Box::new(bitwarden_api_api::models::SignatureKeyPairRequestModel {
155                        wrapped_signing_key: Some(signing_key.to_string()),
156                        verifying_key: Some(B64::from(verifying_key.to_cose()).to_string()),
157                        signature_algorithm: Some(match verifying_key.algorithm() {
158                            SignatureAlgorithm::Ed25519 => "ed25519".to_string(),
159                        }),
160                    })
161                }),
162            public_key_encryption_key_pair: Some(Box::new(
163                bitwarden_api_api::models::PublicKeyEncryptionKeyPairRequestModel {
164                    wrapped_private_key: match self {
165                        WrappedAccountCryptographicState::V1 { private_key }
166                        | WrappedAccountCryptographicState::V2 { private_key, .. } => {
167                            Some(private_key.to_string())
168                        }
169                    },
170                    public_key: Some(B64::from(public_key.to_der()?).to_string()),
171                    signed_public_key: match self.signed_public_key() {
172                        Ok(Some(spk)) => Some(spk.clone().into()),
173                        _ => None,
174                    },
175                },
176            )),
177            security_state: match (self, signature_keypair.as_ref()) {
178                (_, None) | (WrappedAccountCryptographicState::V1 { .. }, Some(_)) => None,
179                (
180                    WrappedAccountCryptographicState::V2 { security_state, .. },
181                    Some((_, verifying_key)),
182                ) => {
183                    // Convert the verified state's version to i32 for the API model
184                    Some(Box::new(SecurityStateModel {
185                        security_state: Some(security_state.into()),
186                        security_version: security_state
187                            .to_owned()
188                            .verify_and_unwrap(verifying_key)
189                            .map_err(|_| AccountCryptographyInitializationError::TamperedData)?
190                            .version() as i32,
191                    }))
192                }
193            },
194        })
195    }
196
197    /// Creates a new V2 account cryptographic state with fresh keys. This does not change the user
198    /// state, but does set some keys to the local context.
199    pub fn make(
200        ctx: &mut KeyStoreContext<KeyIds>,
201        user_id: UserId,
202    ) -> Result<(SymmetricKeyId, Self), AccountCryptographyInitializationError> {
203        let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
204        let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
205        let signing_key = ctx.make_signing_key(SignatureAlgorithm::Ed25519);
206        let signed_public_key = ctx.make_signed_public_key(private_key, signing_key)?;
207
208        let security_state = SecurityState::initialize_for_user(user_id);
209        let signed_security_state = security_state.sign(signing_key, ctx)?;
210
211        Ok((
212            user_key,
213            WrappedAccountCryptographicState::V2 {
214                private_key: ctx.wrap_private_key(user_key, private_key)?,
215                signed_public_key: Some(signed_public_key),
216                signing_key: ctx.wrap_signing_key(user_key, signing_key)?,
217                security_state: signed_security_state,
218            },
219        ))
220    }
221
222    #[cfg(test)]
223    fn make_v1(
224        ctx: &mut KeyStoreContext<KeyIds>,
225    ) -> Result<(SymmetricKeyId, Self), AccountCryptographyInitializationError> {
226        let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
227        let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
228
229        Ok((
230            user_key,
231            WrappedAccountCryptographicState::V1 {
232                private_key: ctx.wrap_private_key(user_key, private_key)?,
233            },
234        ))
235    }
236
237    /// Re-wraps the account cryptographic state with a new user key. If the cryptographic state is
238    /// a V1 state, it gets upgraded to a V2 state
239    #[instrument(skip(self, ctx), err)]
240    pub fn rotate(
241        &self,
242        current_user_key: &SymmetricKeyId,
243        new_user_key: &SymmetricKeyId,
244        user_id: UserId,
245        ctx: &mut KeyStoreContext<KeyIds>,
246    ) -> Result<Self, RotateCryptographyStateError> {
247        match self {
248            WrappedAccountCryptographicState::V1 { private_key } => {
249                // To upgrade a V1 state to a V2 state,
250                // 1. The private key is re-encrypted
251                // 2. The signing key is generated
252                // 3. The public key is signed and
253                // 4. The security state is initialized and signed.
254
255                // 1. Re-encrypt private key
256                let private_key_id = ctx
257                    .unwrap_private_key(*current_user_key, private_key)
258                    .map_err(|_| RotateCryptographyStateError::InvalidData)?;
259                let new_private_key = ctx
260                    .wrap_private_key(*new_user_key, private_key_id)
261                    .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
262
263                // 2. The signing key is generated
264                let signing_key_id = ctx.make_signing_key(SignatureAlgorithm::Ed25519);
265                let new_signing_key = ctx
266                    .wrap_signing_key(*new_user_key, signing_key_id)
267                    .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
268
269                // 3. The public key is signed and
270                let signed_public_key = ctx
271                    .make_signed_public_key(private_key_id, signing_key_id)
272                    .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
273
274                // 4. The security state is initialized and signed.
275                let security_state = SecurityState::initialize_for_user(user_id);
276                let signed_security_state = security_state
277                    .sign(signing_key_id, ctx)
278                    .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
279
280                Ok(WrappedAccountCryptographicState::V2 {
281                    private_key: new_private_key,
282                    signed_public_key: Some(signed_public_key),
283                    signing_key: new_signing_key,
284                    security_state: signed_security_state,
285                })
286            }
287            WrappedAccountCryptographicState::V2 {
288                private_key,
289                signed_public_key,
290                signing_key,
291                security_state,
292            } => {
293                // To rotate a V2 state, the private and signing keys are re-encrypted with the new
294                // user key.
295                // 1. Re-encrypt private key
296                let private_key_id = ctx
297                    .unwrap_private_key(*current_user_key, private_key)
298                    .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
299                let new_private_key = ctx
300                    .wrap_private_key(*new_user_key, private_key_id)
301                    .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
302
303                // 2. Re-encrypt signing key
304                let signing_key_id = ctx
305                    .unwrap_signing_key(*current_user_key, signing_key)
306                    .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
307                let new_signing_key = ctx
308                    .wrap_signing_key(*new_user_key, signing_key_id)
309                    .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
310
311                Ok(WrappedAccountCryptographicState::V2 {
312                    private_key: new_private_key,
313                    signed_public_key: signed_public_key.clone(),
314                    signing_key: new_signing_key,
315                    security_state: security_state.clone(),
316                })
317            }
318        }
319    }
320
321    /// Set the decrypted account cryptographic state to the context's non-local storage.
322    /// This needs a mutable context passed in that already has a user_key set to a local key slot,
323    /// for which the id is passed in as `user_key`. Note, that this function drops the context
324    /// and clears the existing local state, after persisting it.
325    pub(crate) fn set_to_context(
326        &self,
327        security_state_rwlock: &RwLock<Option<SecurityState>>,
328        user_key: SymmetricKeyId,
329        store: &KeyStore<KeyIds>,
330        mut ctx: KeyStoreContext<KeyIds>,
331    ) -> Result<(), AccountCryptographyInitializationError> {
332        if ctx.has_symmetric_key(SymmetricKeyId::User)
333            || ctx.has_private_key(PrivateKeyId::UserPrivateKey)
334            || ctx.has_signing_key(SigningKeyId::UserSigningKey)
335        {
336            return Err(AccountCryptographyInitializationError::KeyStoreAlreadyInitialized);
337        }
338
339        match self {
340            WrappedAccountCryptographicState::V1 { private_key } => {
341                info!("Initializing V1 account cryptographic state");
342                if ctx.get_symmetric_key_algorithm(user_key)?
343                    != SymmetricKeyAlgorithm::Aes256CbcHmac
344                {
345                    return Err(AccountCryptographyInitializationError::WrongUserKeyType);
346                }
347
348                // Some users have unreadable V1 private keys. In this case, we set no keys to
349                // state.
350                if let Ok(private_key_id) = ctx.unwrap_private_key(user_key, private_key) {
351                    ctx.persist_private_key(private_key_id, PrivateKeyId::UserPrivateKey)?;
352                } else {
353                    tracing::warn!(
354                        "V1 private key could not be unwrapped, skipping setting private key"
355                    );
356                }
357
358                ctx.persist_symmetric_key(user_key, SymmetricKeyId::User)?;
359            }
360            WrappedAccountCryptographicState::V2 {
361                private_key,
362                signed_public_key,
363                signing_key,
364                security_state,
365            } => {
366                info!("Initializing V2 account cryptographic state");
367                if ctx.get_symmetric_key_algorithm(user_key)?
368                    != SymmetricKeyAlgorithm::XChaCha20Poly1305
369                {
370                    return Err(AccountCryptographyInitializationError::WrongUserKeyType);
371                }
372
373                let private_key_id = ctx
374                    .unwrap_private_key(user_key, private_key)
375                    .map_err(|_| AccountCryptographyInitializationError::WrongUserKey)?;
376                let signing_key_id = ctx
377                    .unwrap_signing_key(user_key, signing_key)
378                    .map_err(|_| AccountCryptographyInitializationError::WrongUserKey)?;
379
380                if let Some(signed_public_key) = signed_public_key {
381                    signed_public_key
382                        .to_owned()
383                        .verify_and_unwrap(&ctx.get_verifying_key(signing_key_id)?)
384                        .map_err(|_| AccountCryptographyInitializationError::TamperedData)?;
385                }
386
387                let security_state: SecurityState = security_state
388                    .to_owned()
389                    .verify_and_unwrap(&ctx.get_verifying_key(signing_key_id)?)
390                    .map_err(|_| AccountCryptographyInitializationError::TamperedData)?;
391                ctx.persist_private_key(private_key_id, PrivateKeyId::UserPrivateKey)?;
392                ctx.persist_signing_key(signing_key_id, SigningKeyId::UserSigningKey)?;
393                ctx.persist_symmetric_key(user_key, SymmetricKeyId::User)?;
394                // Not manually dropping ctx here would lead to a deadlock, since storing the state
395                // needs to acquire a lock on the inner key store
396                drop(ctx);
397                store.set_security_state_version(security_state.version());
398                *security_state_rwlock.write().expect("RwLock not poisoned") = Some(security_state);
399            }
400        }
401
402        Ok(())
403    }
404
405    /// Retrieve the signed public key from the wrapped state, if present.
406    fn signed_public_key(
407        &self,
408    ) -> Result<Option<&SignedPublicKey>, AccountCryptographyInitializationError> {
409        match self {
410            WrappedAccountCryptographicState::V1 { .. } => Ok(None),
411            WrappedAccountCryptographicState::V2 {
412                signed_public_key, ..
413            } => Ok(signed_public_key.as_ref()),
414        }
415    }
416}
417
418#[cfg(test)]
419mod tests {
420    use std::{str::FromStr, sync::RwLock};
421
422    use bitwarden_crypto::{KeyStore, PrimitiveEncryptable};
423
424    use super::*;
425    use crate::key_management::{PrivateKeyId, SigningKeyId, SymmetricKeyId};
426
427    #[test]
428    fn test_set_to_context_v1() {
429        // Prepare a temporary store to create wrapped state using a known user key
430        let temp_store: KeyStore<KeyIds> = KeyStore::default();
431        let mut temp_ctx = temp_store.context_mut();
432
433        // Create a V1-style user key (Aes256CbcHmac) and add to temp context
434        let user_key = temp_ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
435
436        // Make a private key and wrap it with the user key
437        let private_key_id = temp_ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
438        let wrapped_private = temp_ctx.wrap_private_key(user_key, private_key_id).unwrap();
439
440        // Construct the V1 wrapped state
441        let wrapped = WrappedAccountCryptographicState::V1 {
442            private_key: wrapped_private,
443        };
444        #[allow(deprecated)]
445        let user_key = temp_ctx
446            .dangerous_get_symmetric_key(user_key)
447            .unwrap()
448            .to_owned();
449        drop(temp_ctx);
450        drop(temp_store);
451
452        // Now attempt to set this wrapped state into a fresh store using the same user key
453        let store: KeyStore<KeyIds> = KeyStore::default();
454        let mut ctx = store.context_mut();
455        let user_key = ctx.add_local_symmetric_key(user_key);
456        let security_state = RwLock::new(None);
457
458        // This should succeed and move keys into the expected global slots
459        wrapped
460            .set_to_context(&security_state, user_key, &store, ctx)
461            .unwrap();
462        let ctx = store.context();
463
464        // Assert that the private key and user symmetric key were set in the store
465        assert!(ctx.has_private_key(PrivateKeyId::UserPrivateKey));
466        assert!(ctx.has_symmetric_key(SymmetricKeyId::User));
467    }
468
469    #[test]
470    fn test_set_to_context_v2() {
471        // Prepare a temporary store to create wrapped state using a known user key
472        let temp_store: KeyStore<KeyIds> = KeyStore::default();
473        let mut temp_ctx = temp_store.context_mut();
474
475        // Create a V2-style user key (XChaCha20Poly1305) and add to temp context
476        let user_key = temp_ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
477
478        // Make keys
479        let private_key_id = temp_ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
480        let signing_key_id = temp_ctx.make_signing_key(SignatureAlgorithm::Ed25519);
481        let signed_public_key = temp_ctx
482            .make_signed_public_key(private_key_id, signing_key_id)
483            .unwrap();
484
485        // Sign and wrap security state
486        let user_id = UserId::new_v4();
487        let security_state = SecurityState::initialize_for_user(user_id);
488        let signed_security_state = security_state.sign(signing_key_id, &mut temp_ctx).unwrap();
489
490        // Wrap the private and signing keys with the user key
491        let wrapped_private = temp_ctx.wrap_private_key(user_key, private_key_id).unwrap();
492        let wrapped_signing = temp_ctx.wrap_signing_key(user_key, signing_key_id).unwrap();
493
494        let wrapped = WrappedAccountCryptographicState::V2 {
495            private_key: wrapped_private,
496            signed_public_key: Some(signed_public_key),
497            signing_key: wrapped_signing,
498            security_state: signed_security_state,
499        };
500        #[allow(deprecated)]
501        let user_key = temp_ctx
502            .dangerous_get_symmetric_key(user_key)
503            .unwrap()
504            .to_owned();
505        drop(temp_ctx);
506        drop(temp_store);
507
508        // Now attempt to set this wrapped state into a fresh store using the same user key
509        let store: KeyStore<KeyIds> = KeyStore::default();
510        let mut ctx = store.context_mut();
511        let user_key = ctx.add_local_symmetric_key(user_key);
512        let security_state = RwLock::new(None);
513
514        wrapped
515            .set_to_context(&security_state, user_key, &store, ctx)
516            .unwrap();
517
518        assert!(store.context().has_symmetric_key(SymmetricKeyId::User));
519        // Assert that the account keys and security state were set
520        assert!(
521            store
522                .context()
523                .has_private_key(PrivateKeyId::UserPrivateKey)
524        );
525        assert!(
526            store
527                .context()
528                .has_signing_key(SigningKeyId::UserSigningKey)
529        );
530        // Ensure security state was recorded
531        assert!(security_state.read().unwrap().is_some());
532    }
533
534    #[test]
535    fn test_to_private_keys_request_model_v2() {
536        let temp_store: KeyStore<KeyIds> = KeyStore::default();
537        let mut temp_ctx = temp_store.context_mut();
538        let user_id = UserId::new_v4();
539        let (user_key, wrapped_account_cryptography_state) =
540            WrappedAccountCryptographicState::make(&mut temp_ctx, user_id).unwrap();
541
542        wrapped_account_cryptography_state
543            .set_to_context(&RwLock::new(None), user_key, &temp_store, temp_ctx)
544            .unwrap();
545
546        let mut ctx = temp_store.context_mut();
547        let model = wrapped_account_cryptography_state
548            .to_request_model(&SymmetricKeyId::User, &mut ctx)
549            .expect("to_private_keys_request_model should succeed");
550        drop(ctx);
551
552        let ctx = temp_store.context();
553
554        let sig_pair = model
555            .signature_key_pair
556            .expect("signature_key_pair present");
557        assert_eq!(
558            sig_pair.verifying_key.unwrap(),
559            B64::from(
560                ctx.get_verifying_key(SigningKeyId::UserSigningKey)
561                    .unwrap()
562                    .to_cose()
563            )
564            .to_string()
565        );
566
567        let pk_pair = model.public_key_encryption_key_pair.unwrap();
568        assert_eq!(
569            pk_pair.public_key.unwrap(),
570            B64::from(
571                ctx.get_public_key(PrivateKeyId::UserPrivateKey)
572                    .unwrap()
573                    .to_der()
574                    .unwrap()
575            )
576            .to_string()
577        );
578
579        let signed_security_state = model
580            .security_state
581            .clone()
582            .expect("security_state present");
583        let security_state =
584            SignedSecurityState::from_str(signed_security_state.security_state.unwrap().as_str())
585                .unwrap()
586                .verify_and_unwrap(&ctx.get_verifying_key(SigningKeyId::UserSigningKey).unwrap())
587                .expect("security state should verify");
588        assert_eq!(
589            security_state.version(),
590            model.security_state.unwrap().security_version as u64
591        );
592    }
593
594    #[test]
595    fn test_set_to_context_v1_corrupt_private_key() {
596        // Test that a V1 account with a corrupt private key (valid EncString but invalid key data)
597        // can still initialize, but skips setting the private key
598        let temp_store: KeyStore<KeyIds> = KeyStore::default();
599        let mut temp_ctx = temp_store.context_mut();
600
601        let user_key = temp_ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
602        let corrupt_private_key = "not a private key"
603            .encrypt(&mut temp_ctx, user_key)
604            .unwrap();
605
606        // Construct the V1 wrapped state with corrupt private key
607        let wrapped = WrappedAccountCryptographicState::V1 {
608            private_key: corrupt_private_key,
609        };
610
611        #[expect(deprecated)]
612        let user_key_material = temp_ctx
613            .dangerous_get_symmetric_key(user_key)
614            .unwrap()
615            .to_owned();
616        drop(temp_ctx);
617        drop(temp_store);
618
619        // Now attempt to set this wrapped state into a fresh store
620        let store: KeyStore<KeyIds> = KeyStore::default();
621        let mut ctx = store.context_mut();
622        let user_key = ctx.add_local_symmetric_key(user_key_material);
623        let security_state = RwLock::new(None);
624
625        wrapped
626            .set_to_context(&security_state, user_key, &store, ctx)
627            .unwrap();
628
629        let ctx = store.context();
630
631        // The user symmetric key should be set
632        assert!(ctx.has_symmetric_key(SymmetricKeyId::User));
633        // But the private key should NOT be set (due to corruption)
634        assert!(!ctx.has_private_key(PrivateKeyId::UserPrivateKey));
635    }
636
637    #[test]
638    fn test_rotate_v1_to_v2() {
639        // Create a key store and context
640        let store: KeyStore<KeyIds> = KeyStore::default();
641        let mut ctx = store.context_mut();
642
643        // Create a V1-style user key and add to context
644        let user_id = UserId::new_v4();
645        let (old_user_key_id, wrapped_state) =
646            WrappedAccountCryptographicState::make_v1(&mut ctx).unwrap();
647        let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
648        #[allow(deprecated)]
649        let new_user_key_owned = ctx
650            .dangerous_get_symmetric_key(new_user_key_id)
651            .unwrap()
652            .to_owned();
653        wrapped_state
654            .set_to_context(&RwLock::new(None), old_user_key_id, &store, ctx)
655            .unwrap();
656
657        // The previous context got consumed, so we are creating a new one here. Setting the state
658        // to context persisted the user-key and other keys
659        let mut ctx = store.context_mut();
660        let new_user_key_id = ctx.add_local_symmetric_key(new_user_key_owned.clone());
661
662        // Rotate the state
663        let rotated_state = wrapped_state
664            .rotate(&SymmetricKeyId::User, &new_user_key_id, user_id, &mut ctx)
665            .unwrap();
666
667        // We need to ensure two things after a rotation from V1 to V2:
668        // 1. The new state is valid and can be set to context
669        // 2. The new state uses the same private and signing keys
670
671        // 1. The new state is valid and can be set to context
672        match rotated_state {
673            WrappedAccountCryptographicState::V2 { .. } => {}
674            _ => panic!("Expected V2 after rotation from V1"),
675        }
676        let store_2 = KeyStore::<KeyIds>::default();
677        let mut ctx_2 = store_2.context_mut();
678        let user_key_id = ctx_2.add_local_symmetric_key(new_user_key_owned.clone());
679        rotated_state
680            .set_to_context(&RwLock::new(None), user_key_id, &store_2, ctx_2)
681            .unwrap();
682        // The context was consumed, so we create a new one to inspect the keys
683        let ctx_2 = store_2.context();
684
685        // 2. The new state uses the same private and signing keys
686        let public_key_before_rotation = ctx
687            .get_public_key(PrivateKeyId::UserPrivateKey)
688            .expect("Private key should be present in context before rotation");
689        let public_key_after_rotation = ctx_2
690            .get_public_key(PrivateKeyId::UserPrivateKey)
691            .expect("Private key should be present in context after rotation");
692        assert_eq!(
693            public_key_before_rotation.to_der().unwrap(),
694            public_key_after_rotation.to_der().unwrap(),
695            "Private key should be preserved during rotation from V2 to V2"
696        );
697    }
698
699    #[test]
700    fn test_rotate_v2() {
701        // Create a key store and context
702        let store: KeyStore<KeyIds> = KeyStore::default();
703        let mut ctx = store.context_mut();
704
705        // Create a V2-style user key and add to context
706        let user_id = UserId::new_v4();
707        let (old_user_key_id, wrapped_state) =
708            WrappedAccountCryptographicState::make(&mut ctx, user_id).unwrap();
709        let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
710        #[allow(deprecated)]
711        let new_user_key_owned = ctx
712            .dangerous_get_symmetric_key(new_user_key_id)
713            .unwrap()
714            .to_owned();
715        wrapped_state
716            .set_to_context(&RwLock::new(None), old_user_key_id, &store, ctx)
717            .unwrap();
718
719        // The previous context got consumed, so we are creating a new one here. Setting the state
720        // to context persisted the user-key and other keys
721        let mut ctx = store.context_mut();
722        let new_user_key_id = ctx.add_local_symmetric_key(new_user_key_owned.clone());
723
724        // Rotate the state
725        let rotated_state = wrapped_state
726            .rotate(&SymmetricKeyId::User, &new_user_key_id, user_id, &mut ctx)
727            .unwrap();
728
729        // We need to ensure two things after a rotation from V1 to V2:
730        // 1. The new state is valid and can be set to context
731        // 2. The new state uses the same private and signing keys
732
733        // 1. The new state is valid and can be set to context
734        match rotated_state {
735            WrappedAccountCryptographicState::V2 { .. } => {}
736            _ => panic!("Expected V2 after rotation from V2"),
737        }
738        let store_2 = KeyStore::<KeyIds>::default();
739        let mut ctx_2 = store_2.context_mut();
740        let user_key_id = ctx_2.add_local_symmetric_key(new_user_key_owned.clone());
741        rotated_state
742            .set_to_context(&RwLock::new(None), user_key_id, &store_2, ctx_2)
743            .unwrap();
744        // The context was consumed, so we create a new one to inspect the keys
745        let ctx_2 = store_2.context();
746
747        // 2. The new state uses the same private and signing keys
748        let verifying_key_before_rotation = ctx
749            .get_verifying_key(SigningKeyId::UserSigningKey)
750            .expect("Signing key should be present in context before rotation");
751        let verifying_key_after_rotation = ctx_2
752            .get_verifying_key(SigningKeyId::UserSigningKey)
753            .expect("Signing key should be present in context after rotation");
754        assert_eq!(
755            verifying_key_before_rotation.to_cose(),
756            verifying_key_after_rotation.to_cose(),
757            "Signing key should be preserved during rotation from V2 to V2"
758        );
759
760        let public_key_before_rotation = ctx
761            .get_public_key(PrivateKeyId::UserPrivateKey)
762            .expect("Private key should be present in context before rotation");
763        let public_key_after_rotation = ctx_2
764            .get_public_key(PrivateKeyId::UserPrivateKey)
765            .expect("Private key should be present in context after rotation");
766        assert_eq!(
767            public_key_before_rotation.to_der().unwrap(),
768            public_key_after_rotation.to_der().unwrap(),
769            "Private key should be preserved during rotation from V2 to V2"
770        );
771    }
772}