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