Skip to main content

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 XAES-256-GCM key.
10
11use std::sync::RwLock;
12
13use bitwarden_api_api::models::{
14    AccountKeysRequestModel, PrivateKeysResponseModel, SecurityStateModel,
15    WrappedAccountCryptographicStateRequestModel,
16};
17use bitwarden_crypto::{
18    CoseSerializable, CryptoError, EncString, KeyStore, KeyStoreContext,
19    PublicKeyEncryptionAlgorithm, SignatureAlgorithm, SignedPublicKey, SymmetricKeyAlgorithm,
20};
21use bitwarden_encoding::B64;
22use bitwarden_error::bitwarden_error;
23use serde::{Deserialize, Serialize};
24use thiserror::Error;
25use tracing::info;
26
27use crate::{
28    MissingFieldError,
29    key_management::{
30        KeySlotIds, PrivateKeySlotId, SecurityState, SignedSecurityState, SigningKeySlotId,
31        SymmetricKeySlotId,
32    },
33    require,
34};
35
36/// Errors that can occur during initialization of the account cryptographic state.
37#[derive(Debug, Error)]
38#[bitwarden_error(flat)]
39pub enum AccountCryptographyInitializationError {
40    /// The encryption algorithm from the user key does not match one of the encrypted items.
41    /// This would mean that the user's account is corrupt.
42    #[error("The encryption type of the user key does not match the account cryptographic state")]
43    WrongUserKeyType,
44    /// The provide user-key is incorrect or out-of-date. This may happen when a use-key changed
45    /// and a local unlock-method is not yet updated.
46    #[error("Wrong user key")]
47    WrongUserKey,
48    /// The decrypted data is corrupt.
49    #[error("Decryption succeeded but produced corrupt data")]
50    CorruptData,
51    /// The decrypted data is corrupt.
52    #[error("Signature or mac verification failed, the data may have been tampered with")]
53    TamperedData,
54    /// A generic cryptographic error occurred.
55    #[error("A generic cryptographic error occurred: {0}")]
56    GenericCrypto(CryptoError),
57}
58
59impl From<CryptoError> for AccountCryptographyInitializationError {
60    fn from(err: CryptoError) -> Self {
61        AccountCryptographyInitializationError::GenericCrypto(err)
62    }
63}
64
65/// Errors that can occur during rotation of the account cryptographic state.
66#[derive(Debug, Error)]
67#[bitwarden_error(flat)]
68pub enum RotateCryptographyStateError {
69    /// The key is missing from the key store
70    #[error("The provided key is missing from the key store")]
71    KeyMissing,
72    /// The provided data was invalid
73    #[error("The provided data was invalid")]
74    InvalidData,
75}
76
77/// Errors that can occur when parsing a `PrivateKeysResponseModel` into a
78/// `WrappedAccountCryptographicState`.
79#[derive(Debug, Error)]
80pub enum AccountKeysResponseParseError {
81    /// A required field was missing from the API response.
82    #[error(transparent)]
83    MissingField(#[from] MissingFieldError),
84    /// A field value could not be parsed into the expected type.
85    #[error("Malformed field value in API response")]
86    MalformedField,
87    /// The encryption type of the private key does not match the presence/absence of V2 fields.
88    #[error("Inconsistent account cryptographic state in API response")]
89    InconsistentState,
90}
91
92/// Any keys / cryptographic protection "downstream" from the account symmetric key (user key).
93/// Private keys are protected by the user key.
94#[derive(Clone, Serialize, Deserialize, PartialEq)]
95#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
96#[bitwarden_ffi::wasm_record]
97#[allow(clippy::large_enum_variant)]
98pub enum WrappedAccountCryptographicState {
99    /// A V1 user has only a private key.
100    V1 {
101        /// The user's encryption private key, wrapped by the user key.
102        private_key: EncString,
103    },
104    /// A V2 user has a private key, a signing key, a signed public key and a signed security state.
105    /// The SignedPublicKey ensures that others can verify the public key is claimed by an identity
106    /// they want to share data to. The signed security state protects against cryptographic
107    /// downgrades.
108    V2 {
109        /// The user's encryption private key, wrapped by the user key.
110        private_key: EncString,
111        /// The user's public-key for the private key, signed by the user's signing key.
112        signed_public_key: SignedPublicKey,
113        /// The user's signing key, wrapped by the user key.
114        signing_key: EncString,
115        /// The user's signed security state.
116        security_state: SignedSecurityState,
117    },
118}
119
120#[cfg(feature = "wasm")]
121impl TryFrom<wasm_bindgen::JsValue> for WrappedAccountCryptographicState {
122    type Error = serde_wasm_bindgen::Error;
123
124    fn try_from(value: wasm_bindgen::JsValue) -> Result<Self, Self::Error> {
125        serde_wasm_bindgen::from_value(value)
126    }
127}
128
129impl std::fmt::Debug for WrappedAccountCryptographicState {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        match self {
132            WrappedAccountCryptographicState::V1 { .. } => f
133                .debug_struct("WrappedAccountCryptographicState::V1")
134                .finish(),
135            WrappedAccountCryptographicState::V2 { security_state, .. } => f
136                .debug_struct("WrappedAccountCryptographicState::V2")
137                .field("security_state", security_state)
138                .finish(),
139        }
140    }
141}
142
143impl TryFrom<&PrivateKeysResponseModel> for WrappedAccountCryptographicState {
144    type Error = AccountKeysResponseParseError;
145
146    fn try_from(response: &PrivateKeysResponseModel) -> Result<Self, Self::Error> {
147        let private_key: EncString = EncString::parse_strict(require!(
148            &response.public_key_encryption_key_pair.wrapped_private_key
149        ))
150        .map_err(|_| AccountKeysResponseParseError::MalformedField)?;
151
152        let is_v2_encryption = matches!(private_key, EncString::Cose_Encrypt0_B64 { .. });
153
154        if is_v2_encryption {
155            let signature_key_pair = response
156                .signature_key_pair
157                .as_ref()
158                .ok_or(AccountKeysResponseParseError::InconsistentState)?;
159
160            let signing_key: EncString =
161                EncString::parse_strict(require!(&signature_key_pair.wrapped_signing_key))
162                    .map_err(|_| AccountKeysResponseParseError::MalformedField)?;
163
164            let signed_public_key: SignedPublicKey = response
165                .public_key_encryption_key_pair
166                .signed_public_key
167                .as_ref()
168                .ok_or(AccountKeysResponseParseError::InconsistentState)?
169                .parse()
170                .map_err(|_| AccountKeysResponseParseError::MalformedField)?;
171
172            let security_state_model = response
173                .security_state
174                .as_ref()
175                .ok_or(AccountKeysResponseParseError::InconsistentState)?;
176            let security_state: SignedSecurityState =
177                require!(&security_state_model.security_state)
178                    .parse()
179                    .map_err(|_| AccountKeysResponseParseError::MalformedField)?;
180
181            Ok(WrappedAccountCryptographicState::V2 {
182                private_key,
183                signed_public_key,
184                signing_key,
185                security_state,
186            })
187        } else {
188            if response.signature_key_pair.is_some() || response.security_state.is_some() {
189                return Err(AccountKeysResponseParseError::InconsistentState);
190            }
191
192            Ok(WrappedAccountCryptographicState::V1 { private_key })
193        }
194    }
195}
196
197impl WrappedAccountCryptographicState {
198    /// Converts to a WrappedAccountCryptographicStateRequestModel in order to make API requests.
199    /// Since the [WrappedAccountCryptographicState] is encrypted, the key store needs to
200    /// contain the user key required to unlock this state. This request model only supports v2
201    /// encryption.
202    pub fn to_wrapped_request_model(
203        &self,
204        user_key: &SymmetricKeySlotId,
205        ctx: &mut KeyStoreContext<KeySlotIds>,
206    ) -> Result<WrappedAccountCryptographicStateRequestModel, AccountCryptographyInitializationError>
207    {
208        match self {
209            WrappedAccountCryptographicState::V1 { .. } => {
210                Err(AccountCryptographyInitializationError::WrongUserKeyType)
211            }
212            WrappedAccountCryptographicState::V2 {
213                private_key,
214                signing_key,
215                security_state,
216                signed_public_key,
217                ..
218            } => {
219                let private_key = private_key.clone();
220                let private_key_tmp_id = ctx.unwrap_private_key(*user_key, &private_key)?;
221                let public_key = ctx.get_public_key(private_key_tmp_id)?;
222
223                let signing_key_tmp_id = ctx.unwrap_signing_key(*user_key, signing_key)?;
224                let verifying_key = ctx.get_verifying_key(signing_key_tmp_id)?;
225
226                Ok(WrappedAccountCryptographicStateRequestModel {
227                    signature_key_pair: Box::new(
228                        bitwarden_api_api::models::SignatureKeyPairRequestModel {
229                            wrapped_signing_key: Some(signing_key.to_string()),
230                            verifying_key: Some(B64::from(verifying_key.to_cose()).to_string()),
231                            signature_algorithm: Some(verifying_key.algorithm().to_string()),
232                        },
233                    ),
234                    public_key_encryption_key_pair: Box::new(
235                        bitwarden_api_api::models::PublicKeyEncryptionKeyPairRequestModel {
236                            wrapped_private_key: Some(private_key.to_string()),
237                            public_key: Some(B64::from(public_key.to_der()?).to_string()),
238                            signed_public_key: Some(signed_public_key.clone().into()),
239                        },
240                    ),
241                    // Convert the verified state's version to i32 for the API model
242                    security_state: Box::new(SecurityStateModel {
243                        security_state: Some(security_state.into()),
244                        security_version: security_state
245                            .to_owned()
246                            .verify_and_unwrap(&verifying_key)
247                            .map_err(|_| AccountCryptographyInitializationError::TamperedData)?
248                            .version() as i32,
249                    }),
250                })
251            }
252        }
253    }
254
255    /// Converts to a AccountKeysRequestModel in order to make API requests. Since the
256    /// [WrappedAccountCryptographicState] is encrypted, the key store needs to contain the
257    /// user key required to unlock this state.
258    #[bitwarden_logging::instrument(err)]
259    pub fn to_request_model(
260        &self,
261        user_key: &SymmetricKeySlotId,
262        ctx: &mut KeyStoreContext<KeySlotIds>,
263    ) -> Result<AccountKeysRequestModel, AccountCryptographyInitializationError> {
264        let private_key = match self {
265            WrappedAccountCryptographicState::V1 { private_key }
266            | WrappedAccountCryptographicState::V2 { private_key, .. } => private_key.clone(),
267        };
268        let private_key_tmp_id = ctx.unwrap_private_key(*user_key, &private_key)?;
269        let public_key = ctx.get_public_key(private_key_tmp_id)?;
270
271        let signature_keypair = match self {
272            WrappedAccountCryptographicState::V1 { .. } => None,
273            WrappedAccountCryptographicState::V2 { signing_key, .. } => {
274                let signing_key_tmp_id = ctx.unwrap_signing_key(*user_key, signing_key)?;
275                let verifying_key = ctx.get_verifying_key(signing_key_tmp_id)?;
276                Some((signing_key.clone(), verifying_key))
277            }
278        };
279
280        Ok(AccountKeysRequestModel {
281            // Note: This property is deprecated and should be removed after a transition period.
282            user_key_encrypted_account_private_key: Some(private_key.to_string()),
283            // Note: This property is deprecated and should be removed after a transition period.
284            account_public_key: Some(B64::from(public_key.to_der()?).to_string()),
285            signature_key_pair: signature_keypair
286                .as_ref()
287                .map(|(signing_key, verifying_key)| {
288                    Box::new(bitwarden_api_api::models::SignatureKeyPairRequestModel {
289                        wrapped_signing_key: Some(signing_key.to_string()),
290                        verifying_key: Some(B64::from(verifying_key.to_cose()).to_string()),
291                        signature_algorithm: Some(verifying_key.algorithm().to_string()),
292                    })
293                }),
294            public_key_encryption_key_pair: Some(Box::new(
295                bitwarden_api_api::models::PublicKeyEncryptionKeyPairRequestModel {
296                    wrapped_private_key: match self {
297                        WrappedAccountCryptographicState::V1 { private_key }
298                        | WrappedAccountCryptographicState::V2 { private_key, .. } => {
299                            Some(private_key.to_string())
300                        }
301                    },
302                    public_key: Some(B64::from(public_key.to_der()?).to_string()),
303                    signed_public_key: match self.signed_public_key() {
304                        Ok(Some(spk)) => Some(spk.clone().into()),
305                        _ => None,
306                    },
307                },
308            )),
309            security_state: match (self, signature_keypair.as_ref()) {
310                (_, None) | (WrappedAccountCryptographicState::V1 { .. }, Some(_)) => None,
311                (
312                    WrappedAccountCryptographicState::V2 { security_state, .. },
313                    Some((_, verifying_key)),
314                ) => {
315                    // Convert the verified state's version to i32 for the API model
316                    Some(Box::new(SecurityStateModel {
317                        security_state: Some(security_state.into()),
318                        security_version: security_state
319                            .to_owned()
320                            .verify_and_unwrap(verifying_key)
321                            .map_err(|_| AccountCryptographyInitializationError::TamperedData)?
322                            .version() as i32,
323                    }))
324                }
325            },
326        })
327    }
328
329    /// Creates a new V2 account cryptographic state with fresh keys. This does not change the user
330    /// state, but does set some keys to the local context.
331    pub fn make(
332        ctx: &mut KeyStoreContext<KeySlotIds>,
333    ) -> Result<(SymmetricKeySlotId, Self), AccountCryptographyInitializationError> {
334        let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
335        let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
336        let signing_key = ctx.make_signing_key(SignatureAlgorithm::MlDsa44);
337        let signed_public_key = ctx.make_signed_public_key(private_key, signing_key)?;
338
339        let security_state = SecurityState::new();
340        let signed_security_state = security_state.sign(signing_key, ctx)?;
341
342        Ok((
343            user_key,
344            WrappedAccountCryptographicState::V2 {
345                private_key: ctx.wrap_private_key(user_key, private_key)?,
346                signed_public_key,
347                signing_key: ctx.wrap_signing_key(user_key, signing_key)?,
348                security_state: signed_security_state,
349            },
350        ))
351    }
352
353    #[cfg(test)]
354    pub(crate) fn make_v1(
355        ctx: &mut KeyStoreContext<KeySlotIds>,
356    ) -> Result<(SymmetricKeySlotId, Self), AccountCryptographyInitializationError> {
357        let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
358        let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
359
360        Ok((
361            user_key,
362            WrappedAccountCryptographicState::V1 {
363                private_key: ctx.wrap_private_key(user_key, private_key)?,
364            },
365        ))
366    }
367
368    /// Reads the current account cryptographic state from the key store by wrapping the
369    /// user's private key with the user key.
370    ///
371    /// Currently only supports V1 accounts.
372    ///
373    /// This is useful for obtaining the wrapped state after an asymmetric key regeneration.
374    #[bitwarden_logging::instrument(err)]
375    pub fn get_from_key_store(
376        ctx: &KeyStoreContext<KeySlotIds>,
377    ) -> Result<Self, RotateCryptographyStateError> {
378        if !ctx
379            .is_v1_symmetric_key(SymmetricKeySlotId::User)
380            .map_err(|_| RotateCryptographyStateError::KeyMissing)?
381        {
382            return Err(RotateCryptographyStateError::InvalidData);
383        }
384
385        let private_key = ctx
386            .wrap_private_key(SymmetricKeySlotId::User, PrivateKeySlotId::UserPrivateKey)
387            .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
388
389        Ok(WrappedAccountCryptographicState::V1 { private_key })
390    }
391
392    /// Re-wraps the account cryptographic state with a new user key. If the cryptographic state is
393    /// a V1 state, it gets upgraded to a V2 state
394    #[bitwarden_logging::instrument(err, fields(current_user_key = ?current_user_key, new_user_key = ?new_user_key))]
395    pub fn rotate(
396        &self,
397        current_user_key: &SymmetricKeySlotId,
398        new_user_key: &SymmetricKeySlotId,
399        ctx: &mut KeyStoreContext<KeySlotIds>,
400    ) -> Result<Self, RotateCryptographyStateError> {
401        match self {
402            WrappedAccountCryptographicState::V1 { private_key } => {
403                // To upgrade a V1 state to a V2 state,
404                // 1. The private key is re-encrypted
405                // 2. The signing key is generated
406                // 3. The public key is signed and
407                // 4. The security state is initialized and signed.
408
409                // 1. Re-encrypt private key
410                let private_key_id = ctx
411                    .unwrap_private_key(*current_user_key, private_key)
412                    .map_err(|_| RotateCryptographyStateError::InvalidData)?;
413                let new_private_key = ctx
414                    .wrap_private_key(*new_user_key, private_key_id)
415                    .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
416
417                // 2. The signing key is generated
418                let signing_key_id = ctx.make_signing_key(SignatureAlgorithm::MlDsa44);
419                let new_signing_key = ctx
420                    .wrap_signing_key(*new_user_key, signing_key_id)
421                    .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
422
423                // 3. The public key is signed and
424                let signed_public_key = ctx
425                    .make_signed_public_key(private_key_id, signing_key_id)
426                    .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
427
428                // 4. The security state is initialized and signed.
429                let security_state = SecurityState::new();
430                let signed_security_state = security_state
431                    .sign(signing_key_id, ctx)
432                    .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
433
434                Ok(WrappedAccountCryptographicState::V2 {
435                    private_key: new_private_key,
436                    signed_public_key,
437                    signing_key: new_signing_key,
438                    security_state: signed_security_state,
439                })
440            }
441            WrappedAccountCryptographicState::V2 {
442                private_key,
443                signed_public_key,
444                signing_key,
445                security_state,
446            } => {
447                // To rotate a V2 state, the private and signing keys are re-encrypted with the new
448                // user key.
449                // 1. Re-encrypt private key
450                let private_key_id = ctx
451                    .unwrap_private_key(*current_user_key, private_key)
452                    .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
453                let new_private_key = ctx
454                    .wrap_private_key(*new_user_key, private_key_id)
455                    .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
456
457                // 2. Re-encrypt signing key
458                let signing_key_id = ctx
459                    .unwrap_signing_key(*current_user_key, signing_key)
460                    .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
461                let new_signing_key = ctx
462                    .wrap_signing_key(*new_user_key, signing_key_id)
463                    .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
464
465                Ok(WrappedAccountCryptographicState::V2 {
466                    private_key: new_private_key,
467                    signed_public_key: signed_public_key.clone(),
468                    signing_key: new_signing_key,
469                    security_state: security_state.clone(),
470                })
471            }
472        }
473    }
474
475    /// Set the decrypted account cryptographic state to the context's non-local storage.
476    /// This needs a mutable context passed in that already has a user_key set to a local key slot,
477    /// for which the id is passed in as `user_key`. Note, that this function drops the context
478    /// and clears the existing local state, after persisting it.
479    pub(crate) fn set_to_context(
480        &self,
481        security_state_rwlock: &RwLock<Option<SecurityState>>,
482        user_key: SymmetricKeySlotId,
483        store: &KeyStore<KeySlotIds>,
484        mut ctx: KeyStoreContext<KeySlotIds>,
485    ) -> Result<(), AccountCryptographyInitializationError> {
486        match self {
487            WrappedAccountCryptographicState::V1 { private_key } => {
488                info!(state = ?self, "Initializing V1 account cryptographic state");
489                if ctx.get_symmetric_key_algorithm(user_key)?
490                    != SymmetricKeyAlgorithm::Aes256CbcHmac
491                {
492                    return Err(AccountCryptographyInitializationError::WrongUserKeyType);
493                }
494
495                // Some users have unreadable V1 private keys. In this case, we set no keys to
496                // state.
497                if let Ok(private_key_id) = ctx.unwrap_private_key(user_key, private_key) {
498                    ctx.persist_private_key(private_key_id, PrivateKeySlotId::UserPrivateKey)?;
499                } else {
500                    tracing::warn!(
501                        "V1 private key could not be unwrapped, skipping setting private key"
502                    );
503                }
504
505                ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User)?;
506                #[cfg(feature = "dangerous-crypto-debug")]
507                #[allow(deprecated)]
508                {
509                    let user_key = ctx
510                        .dangerous_get_symmetric_key(SymmetricKeySlotId::User)
511                        .expect("User key should be set");
512                    let private_key = ctx
513                        .dangerous_get_private_key(PrivateKeySlotId::UserPrivateKey)
514                        .ok();
515                    let public_key = ctx.get_public_key(PrivateKeySlotId::UserPrivateKey).ok();
516                    info!(
517                        ?user_key,
518                        ?private_key,
519                        ?public_key,
520                        "V1 account cryptographic state set to context"
521                    );
522                }
523            }
524            WrappedAccountCryptographicState::V2 {
525                private_key,
526                signed_public_key,
527                signing_key,
528                security_state,
529            } => {
530                info!(state = ?self, "Initializing V2 account cryptographic state");
531                if !matches!(
532                    ctx.get_symmetric_key_algorithm(user_key)?,
533                    SymmetricKeyAlgorithm::XAes256Gcm
534                ) {
535                    return Err(AccountCryptographyInitializationError::WrongUserKeyType);
536                }
537
538                let private_key_id = ctx
539                    .unwrap_private_key(user_key, private_key)
540                    .map_err(|_| AccountCryptographyInitializationError::WrongUserKey)?;
541                let signing_key_id = ctx
542                    .unwrap_signing_key(user_key, signing_key)
543                    .map_err(|_| AccountCryptographyInitializationError::WrongUserKey)?;
544
545                signed_public_key
546                    .to_owned()
547                    .verify_and_unwrap(&ctx.get_verifying_key(signing_key_id)?)
548                    .map_err(|_| AccountCryptographyInitializationError::TamperedData)?;
549
550                let verifying_key = ctx.get_verifying_key(signing_key_id)?;
551                let security_state: SecurityState = security_state
552                    .to_owned()
553                    .verify_and_unwrap(&verifying_key)
554                    .map_err(|_| AccountCryptographyInitializationError::TamperedData)?;
555                info!(
556                    security_state_version = security_state.version(),
557                    verifying_key = ?verifying_key,
558                    "V2 account cryptographic state verified"
559                );
560                ctx.persist_private_key(private_key_id, PrivateKeySlotId::UserPrivateKey)?;
561                ctx.persist_signing_key(signing_key_id, SigningKeySlotId::UserSigningKey)?;
562                ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User)?;
563
564                #[cfg(feature = "dangerous-crypto-debug")]
565                #[allow(deprecated)]
566                {
567                    let user_key = ctx
568                        .dangerous_get_symmetric_key(SymmetricKeySlotId::User)
569                        .expect("User key should be set");
570                    let private_key = ctx
571                        .dangerous_get_private_key(PrivateKeySlotId::UserPrivateKey)
572                        .ok();
573                    let signing_key = ctx
574                        .dangerous_get_signing_key(SigningKeySlotId::UserSigningKey)
575                        .ok();
576                    let verifying_key =
577                        ctx.get_verifying_key(SigningKeySlotId::UserSigningKey).ok();
578                    let public_key = ctx.get_public_key(PrivateKeySlotId::UserPrivateKey).ok();
579                    info!(
580                        ?user_key,
581                        ?private_key,
582                        ?signing_key,
583                        ?verifying_key,
584                        ?public_key,
585                        ?signed_public_key,
586                        ?security_state,
587                        "V2 account cryptographic state set to context."
588                    );
589                }
590
591                // Not manually dropping ctx here would lead to a deadlock, since storing the state
592                // needs to acquire a lock on the inner key store
593                drop(ctx);
594                store.set_security_state_version(security_state.version());
595                *security_state_rwlock.write().expect("RwLock not poisoned") = Some(security_state);
596            }
597        }
598
599        Ok(())
600    }
601
602    /// Retrieve the signed public key from the wrapped state, if present.
603    fn signed_public_key(
604        &self,
605    ) -> Result<Option<&SignedPublicKey>, AccountCryptographyInitializationError> {
606        match self {
607            WrappedAccountCryptographicState::V1 { .. } => Ok(None),
608            WrappedAccountCryptographicState::V2 {
609                signed_public_key, ..
610            } => Ok(Some(signed_public_key)),
611        }
612    }
613}
614
615#[cfg(test)]
616mod tests {
617    use std::{str::FromStr, sync::RwLock};
618
619    use bitwarden_crypto::{KeyStore, PrimitiveEncryptable};
620
621    use super::*;
622    use crate::key_management::{PrivateKeySlotId, SigningKeySlotId, SymmetricKeySlotId};
623
624    #[test]
625    #[ignore = "Manual test to verify debug format"]
626    fn test_debug() {
627        let store: KeyStore<KeySlotIds> = KeyStore::default();
628        let mut ctx = store.context_mut();
629
630        let (_, v1) = WrappedAccountCryptographicState::make_v1(&mut ctx).unwrap();
631        println!("{:?}", v1);
632
633        let v1 = format!("{v1:?}");
634        assert!(!v1.contains("private_key"));
635
636        let (_, v2) = WrappedAccountCryptographicState::make(&mut ctx).unwrap();
637        println!("{:?}", v2);
638
639        let v2 = format!("{v2:?}");
640        assert!(!v2.contains("private_key"));
641        assert!(!v2.contains("signed_public_key"));
642        assert!(!v2.contains("signing_key"));
643    }
644
645    #[test]
646    fn test_set_to_context_v1() {
647        // Prepare a temporary store to create wrapped state using a known user key
648        let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
649        let mut temp_ctx = temp_store.context_mut();
650
651        // Create a V1-style user key (Aes256CbcHmac) and add to temp context
652        let user_key = temp_ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
653
654        // Make a private key and wrap it with the user key
655        let private_key_id = temp_ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
656        let wrapped_private = temp_ctx.wrap_private_key(user_key, private_key_id).unwrap();
657
658        // Construct the V1 wrapped state
659        let wrapped = WrappedAccountCryptographicState::V1 {
660            private_key: wrapped_private,
661        };
662        #[allow(deprecated)]
663        let user_key = temp_ctx
664            .dangerous_get_symmetric_key(user_key)
665            .unwrap()
666            .to_owned();
667        drop(temp_ctx);
668        drop(temp_store);
669
670        // Now attempt to set this wrapped state into a fresh store using the same user key
671        let store: KeyStore<KeySlotIds> = KeyStore::default();
672        let mut ctx = store.context_mut();
673        let user_key = ctx.add_local_symmetric_key(user_key);
674        let security_state = RwLock::new(None);
675
676        // This should succeed and move keys into the expected global slots
677        wrapped
678            .set_to_context(&security_state, user_key, &store, ctx)
679            .unwrap();
680        let ctx = store.context();
681
682        // Assert that the private key and user symmetric key were set in the store
683        assert!(ctx.has_private_key(PrivateKeySlotId::UserPrivateKey));
684        assert!(ctx.has_symmetric_key(SymmetricKeySlotId::User));
685    }
686
687    #[test]
688    fn test_set_to_context_v2() {
689        // Prepare a temporary store to create wrapped state using a known user key
690        let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
691        let mut temp_ctx = temp_store.context_mut();
692
693        // Create a V2-style XAES-256-GCM user key and add it to the temporary context
694        let user_key = temp_ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
695
696        // Make keys
697        let private_key_id = temp_ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
698        let signing_key_id = temp_ctx.make_signing_key(SignatureAlgorithm::Ed25519);
699        let signed_public_key = temp_ctx
700            .make_signed_public_key(private_key_id, signing_key_id)
701            .unwrap();
702
703        // Sign and wrap security state
704        let security_state = SecurityState::new();
705        let signed_security_state = security_state.sign(signing_key_id, &mut temp_ctx).unwrap();
706
707        // Wrap the private and signing keys with the user key
708        let wrapped_private = temp_ctx.wrap_private_key(user_key, private_key_id).unwrap();
709        let wrapped_signing = temp_ctx.wrap_signing_key(user_key, signing_key_id).unwrap();
710
711        let wrapped = WrappedAccountCryptographicState::V2 {
712            private_key: wrapped_private,
713            signed_public_key,
714            signing_key: wrapped_signing,
715            security_state: signed_security_state,
716        };
717        #[allow(deprecated)]
718        let user_key = temp_ctx
719            .dangerous_get_symmetric_key(user_key)
720            .unwrap()
721            .to_owned();
722        drop(temp_ctx);
723        drop(temp_store);
724
725        // Now attempt to set this wrapped state into a fresh store using the same user key
726        let store: KeyStore<KeySlotIds> = KeyStore::default();
727        let mut ctx = store.context_mut();
728        let user_key = ctx.add_local_symmetric_key(user_key);
729        let security_state = RwLock::new(None);
730
731        wrapped
732            .set_to_context(&security_state, user_key, &store, ctx)
733            .unwrap();
734
735        assert!(store.context().has_symmetric_key(SymmetricKeySlotId::User));
736        // Assert that the account keys and security state were set
737        assert!(
738            store
739                .context()
740                .has_private_key(PrivateKeySlotId::UserPrivateKey)
741        );
742        assert!(
743            store
744                .context()
745                .has_signing_key(SigningKeySlotId::UserSigningKey)
746        );
747        // Ensure security state was recorded
748        assert!(security_state.read().unwrap().is_some());
749    }
750
751    #[test]
752    fn test_to_private_keys_request_model_v2() {
753        let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
754        let mut temp_ctx = temp_store.context_mut();
755        let (user_key, wrapped_account_cryptography_state) =
756            WrappedAccountCryptographicState::make(&mut temp_ctx).unwrap();
757
758        wrapped_account_cryptography_state
759            .set_to_context(&RwLock::new(None), user_key, &temp_store, temp_ctx)
760            .unwrap();
761
762        let mut ctx = temp_store.context_mut();
763        let model = wrapped_account_cryptography_state
764            .to_request_model(&SymmetricKeySlotId::User, &mut ctx)
765            .expect("to_private_keys_request_model should succeed");
766        drop(ctx);
767
768        let ctx = temp_store.context();
769
770        let sig_pair = model
771            .signature_key_pair
772            .expect("signature_key_pair present");
773        assert_eq!(
774            sig_pair.verifying_key.unwrap(),
775            B64::from(
776                ctx.get_verifying_key(SigningKeySlotId::UserSigningKey)
777                    .unwrap()
778                    .to_cose()
779            )
780            .to_string()
781        );
782
783        let pk_pair = model.public_key_encryption_key_pair.unwrap();
784        assert_eq!(
785            pk_pair.public_key.unwrap(),
786            B64::from(
787                ctx.get_public_key(PrivateKeySlotId::UserPrivateKey)
788                    .unwrap()
789                    .to_der()
790                    .unwrap()
791            )
792            .to_string()
793        );
794
795        let signed_security_state = model
796            .security_state
797            .clone()
798            .expect("security_state present");
799        let security_state =
800            SignedSecurityState::from_str(signed_security_state.security_state.unwrap().as_str())
801                .unwrap()
802                .verify_and_unwrap(
803                    &ctx.get_verifying_key(SigningKeySlotId::UserSigningKey)
804                        .unwrap(),
805                )
806                .expect("security state should verify");
807        assert_eq!(
808            security_state.version(),
809            model.security_state.unwrap().security_version as u64
810        );
811    }
812
813    #[test]
814    fn test_set_to_context_v1_corrupt_private_key() {
815        // Test that a V1 account with a corrupt private key (valid EncString but invalid key data)
816        // can still initialize, but skips setting the private key
817        let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
818        let mut temp_ctx = temp_store.context_mut();
819
820        let user_key = temp_ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
821        let corrupt_private_key = "not a private key"
822            .encrypt(&mut temp_ctx, user_key)
823            .unwrap();
824
825        // Construct the V1 wrapped state with corrupt private key
826        let wrapped = WrappedAccountCryptographicState::V1 {
827            private_key: corrupt_private_key,
828        };
829
830        #[expect(deprecated)]
831        let user_key_material = temp_ctx
832            .dangerous_get_symmetric_key(user_key)
833            .unwrap()
834            .to_owned();
835        drop(temp_ctx);
836        drop(temp_store);
837
838        // Now attempt to set this wrapped state into a fresh store
839        let store: KeyStore<KeySlotIds> = KeyStore::default();
840        let mut ctx = store.context_mut();
841        let user_key = ctx.add_local_symmetric_key(user_key_material);
842        let security_state = RwLock::new(None);
843
844        wrapped
845            .set_to_context(&security_state, user_key, &store, ctx)
846            .unwrap();
847
848        let ctx = store.context();
849
850        // The user symmetric key should be set
851        assert!(ctx.has_symmetric_key(SymmetricKeySlotId::User));
852        // But the private key should NOT be set (due to corruption)
853        assert!(!ctx.has_private_key(PrivateKeySlotId::UserPrivateKey));
854    }
855
856    #[test]
857    fn test_try_from_response_v2_roundtrip() {
858        use bitwarden_api_api::models::{
859            PublicKeyEncryptionKeyPairResponseModel, SecurityStateModel,
860            SignatureKeyPairResponseModel,
861        };
862
863        let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
864        let mut temp_ctx = temp_store.context_mut();
865        let (user_key, wrapped_state) =
866            WrappedAccountCryptographicState::make(&mut temp_ctx).unwrap();
867
868        wrapped_state
869            .set_to_context(&RwLock::new(None), user_key, &temp_store, temp_ctx)
870            .unwrap();
871
872        let mut ctx = temp_store.context_mut();
873        let request_model = wrapped_state
874            .to_request_model(&SymmetricKeySlotId::User, &mut ctx)
875            .unwrap();
876        drop(ctx);
877
878        let pk_pair = request_model.public_key_encryption_key_pair.unwrap();
879        let sig_pair = request_model.signature_key_pair.unwrap();
880        let sec_state = request_model.security_state.unwrap();
881
882        let response = PrivateKeysResponseModel {
883            object: None,
884            public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
885                object: None,
886                wrapped_private_key: pk_pair.wrapped_private_key,
887                public_key: pk_pair.public_key,
888                signed_public_key: pk_pair.signed_public_key,
889            }),
890            signature_key_pair: Some(Box::new(SignatureKeyPairResponseModel {
891                object: None,
892                wrapped_signing_key: sig_pair.wrapped_signing_key,
893                verifying_key: sig_pair.verifying_key,
894            })),
895            security_state: Some(Box::new(SecurityStateModel {
896                security_state: sec_state.security_state,
897                security_version: sec_state.security_version,
898            })),
899        };
900
901        let parsed = WrappedAccountCryptographicState::try_from(&response)
902            .expect("V2 response should parse successfully");
903
904        assert_eq!(parsed, wrapped_state);
905    }
906
907    #[test]
908    fn test_try_from_response_v1() {
909        use bitwarden_api_api::models::PublicKeyEncryptionKeyPairResponseModel;
910
911        let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
912        let mut temp_ctx = temp_store.context_mut();
913        let (_user_key, wrapped_state) =
914            WrappedAccountCryptographicState::make_v1(&mut temp_ctx).unwrap();
915
916        let wrapped_private_key = match &wrapped_state {
917            WrappedAccountCryptographicState::V1 { private_key } => private_key.to_string(),
918            _ => panic!("Expected V1"),
919        };
920        drop(temp_ctx);
921
922        let response = PrivateKeysResponseModel {
923            object: None,
924            public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
925                object: None,
926                wrapped_private_key: Some(wrapped_private_key),
927                public_key: None,
928                signed_public_key: None,
929            }),
930            signature_key_pair: None,
931            security_state: None,
932        };
933
934        let parsed = WrappedAccountCryptographicState::try_from(&response)
935            .expect("V1 response should parse successfully");
936
937        assert_eq!(parsed, wrapped_state);
938    }
939
940    #[test]
941    fn test_try_from_response_missing_private_key() {
942        use bitwarden_api_api::models::PublicKeyEncryptionKeyPairResponseModel;
943
944        let response = PrivateKeysResponseModel {
945            object: None,
946            public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
947                object: None,
948                wrapped_private_key: None,
949                public_key: None,
950                signed_public_key: None,
951            }),
952            signature_key_pair: None,
953            security_state: None,
954        };
955
956        let result = WrappedAccountCryptographicState::try_from(&response);
957        assert!(result.is_err());
958        assert!(
959            matches!(
960                result.unwrap_err(),
961                AccountKeysResponseParseError::MissingField(_)
962            ),
963            "Should return MissingField error"
964        );
965    }
966
967    #[test]
968    fn test_try_from_response_v2_encryption_missing_signature_key_pair() {
969        use bitwarden_api_api::models::PublicKeyEncryptionKeyPairResponseModel;
970
971        // Create a V2 state to get a COSE-encrypted private key
972        let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
973        let mut temp_ctx = temp_store.context_mut();
974        let (user_key, wrapped_state) =
975            WrappedAccountCryptographicState::make(&mut temp_ctx).unwrap();
976
977        wrapped_state
978            .set_to_context(&RwLock::new(None), user_key, &temp_store, temp_ctx)
979            .unwrap();
980
981        let mut ctx = temp_store.context_mut();
982        let request_model = wrapped_state
983            .to_request_model(&SymmetricKeySlotId::User, &mut ctx)
984            .unwrap();
985        drop(ctx);
986
987        let pk_pair = request_model.public_key_encryption_key_pair.unwrap();
988
989        // V2-encrypted private key but no signature_key_pair or security_state
990        let response = PrivateKeysResponseModel {
991            object: None,
992            public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
993                object: None,
994                wrapped_private_key: pk_pair.wrapped_private_key,
995                public_key: pk_pair.public_key,
996                signed_public_key: None,
997            }),
998            signature_key_pair: None,
999            security_state: None,
1000        };
1001
1002        let result = WrappedAccountCryptographicState::try_from(&response);
1003        assert!(matches!(
1004            result.unwrap_err(),
1005            AccountKeysResponseParseError::InconsistentState
1006        ));
1007    }
1008
1009    #[test]
1010    fn test_try_from_response_v2_encryption_missing_signed_public_key() {
1011        use bitwarden_api_api::models::{
1012            PublicKeyEncryptionKeyPairResponseModel, SecurityStateModel,
1013            SignatureKeyPairResponseModel,
1014        };
1015
1016        let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
1017        let mut temp_ctx = temp_store.context_mut();
1018        let (user_key, wrapped_state) =
1019            WrappedAccountCryptographicState::make(&mut temp_ctx).unwrap();
1020
1021        wrapped_state
1022            .set_to_context(&RwLock::new(None), user_key, &temp_store, temp_ctx)
1023            .unwrap();
1024
1025        let mut ctx = temp_store.context_mut();
1026        let request_model = wrapped_state
1027            .to_request_model(&SymmetricKeySlotId::User, &mut ctx)
1028            .unwrap();
1029        drop(ctx);
1030
1031        let pk_pair = request_model.public_key_encryption_key_pair.unwrap();
1032        let sig_pair = request_model.signature_key_pair.unwrap();
1033        let sec_state = request_model.security_state.unwrap();
1034
1035        // Complete V2 response except for the signed public key
1036        let response = PrivateKeysResponseModel {
1037            object: None,
1038            public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
1039                object: None,
1040                wrapped_private_key: pk_pair.wrapped_private_key,
1041                public_key: pk_pair.public_key,
1042                signed_public_key: None,
1043            }),
1044            signature_key_pair: Some(Box::new(SignatureKeyPairResponseModel {
1045                object: None,
1046                wrapped_signing_key: sig_pair.wrapped_signing_key,
1047                verifying_key: sig_pair.verifying_key,
1048            })),
1049            security_state: Some(Box::new(SecurityStateModel {
1050                security_state: sec_state.security_state,
1051                security_version: sec_state.security_version,
1052            })),
1053        };
1054
1055        let result = WrappedAccountCryptographicState::try_from(&response);
1056        assert!(matches!(
1057            result.unwrap_err(),
1058            AccountKeysResponseParseError::InconsistentState
1059        ));
1060    }
1061
1062    #[test]
1063    fn test_try_from_response_v1_encryption_with_unexpected_v2_fields() {
1064        use bitwarden_api_api::models::{
1065            PublicKeyEncryptionKeyPairResponseModel, SignatureKeyPairResponseModel,
1066        };
1067
1068        // Create a V1 state to get an AES-encrypted private key
1069        let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
1070        let mut temp_ctx = temp_store.context_mut();
1071        let (_user_key, wrapped_state) =
1072            WrappedAccountCryptographicState::make_v1(&mut temp_ctx).unwrap();
1073
1074        let wrapped_private_key = match &wrapped_state {
1075            WrappedAccountCryptographicState::V1 { private_key } => private_key.to_string(),
1076            _ => panic!("Expected V1"),
1077        };
1078        drop(temp_ctx);
1079
1080        // V1-encrypted private key but with a signature_key_pair present
1081        let response = PrivateKeysResponseModel {
1082            object: None,
1083            public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
1084                object: None,
1085                wrapped_private_key: Some(wrapped_private_key),
1086                public_key: None,
1087                signed_public_key: None,
1088            }),
1089            signature_key_pair: Some(Box::new(SignatureKeyPairResponseModel {
1090                object: None,
1091                wrapped_signing_key: Some("bogus".to_string()),
1092                verifying_key: None,
1093            })),
1094            security_state: None,
1095        };
1096
1097        let result = WrappedAccountCryptographicState::try_from(&response);
1098        assert!(matches!(
1099            result.unwrap_err(),
1100            AccountKeysResponseParseError::InconsistentState
1101        ));
1102    }
1103
1104    #[test]
1105    fn test_rotate_v1_to_v2() {
1106        // Create a key store and context
1107        let store: KeyStore<KeySlotIds> = KeyStore::default();
1108        let mut ctx = store.context_mut();
1109
1110        // Create a V1-style user key and add to context
1111        let (old_user_key_id, wrapped_state) =
1112            WrappedAccountCryptographicState::make_v1(&mut ctx).unwrap();
1113        let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
1114        #[allow(deprecated)]
1115        let new_user_key_owned = ctx
1116            .dangerous_get_symmetric_key(new_user_key_id)
1117            .unwrap()
1118            .to_owned();
1119        wrapped_state
1120            .set_to_context(&RwLock::new(None), old_user_key_id, &store, ctx)
1121            .unwrap();
1122
1123        // The previous context got consumed, so we are creating a new one here. Setting the state
1124        // to context persisted the user-key and other keys
1125        let mut ctx = store.context_mut();
1126        let new_user_key_id = ctx.add_local_symmetric_key(new_user_key_owned.clone());
1127
1128        // Rotate the state
1129        let rotated_state = wrapped_state
1130            .rotate(&SymmetricKeySlotId::User, &new_user_key_id, &mut ctx)
1131            .unwrap();
1132
1133        // We need to ensure two things after a rotation from V1 to V2:
1134        // 1. The new state is valid and can be set to context
1135        // 2. The new state uses the same private and signing keys
1136
1137        // 1. The new state is valid and can be set to context
1138        match rotated_state {
1139            WrappedAccountCryptographicState::V2 { .. } => {}
1140            _ => panic!("Expected V2 after rotation from V1"),
1141        }
1142        let store_2 = KeyStore::<KeySlotIds>::default();
1143        let mut ctx_2 = store_2.context_mut();
1144        let user_key_id = ctx_2.add_local_symmetric_key(new_user_key_owned.clone());
1145        rotated_state
1146            .set_to_context(&RwLock::new(None), user_key_id, &store_2, ctx_2)
1147            .unwrap();
1148        // The context was consumed, so we create a new one to inspect the keys
1149        let ctx_2 = store_2.context();
1150
1151        // 2. The new state uses the same private and signing keys
1152        let public_key_before_rotation = ctx
1153            .get_public_key(PrivateKeySlotId::UserPrivateKey)
1154            .expect("Private key should be present in context before rotation");
1155        let public_key_after_rotation = ctx_2
1156            .get_public_key(PrivateKeySlotId::UserPrivateKey)
1157            .expect("Private key should be present in context after rotation");
1158        assert_eq!(
1159            public_key_before_rotation.to_der().unwrap(),
1160            public_key_after_rotation.to_der().unwrap(),
1161            "Private key should be preserved during rotation from V2 to V2"
1162        );
1163    }
1164
1165    #[test]
1166    fn test_rotate_v2() {
1167        // Create a key store and context
1168        let store: KeyStore<KeySlotIds> = KeyStore::default();
1169        let mut ctx = store.context_mut();
1170
1171        // Create a V2-style user key and add to context
1172        let (old_user_key_id, wrapped_state) =
1173            WrappedAccountCryptographicState::make(&mut ctx).unwrap();
1174        let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
1175        #[allow(deprecated)]
1176        let new_user_key_owned = ctx
1177            .dangerous_get_symmetric_key(new_user_key_id)
1178            .unwrap()
1179            .to_owned();
1180        wrapped_state
1181            .set_to_context(&RwLock::new(None), old_user_key_id, &store, ctx)
1182            .unwrap();
1183
1184        // The previous context got consumed, so we are creating a new one here. Setting the state
1185        // to context persisted the user-key and other keys
1186        let mut ctx = store.context_mut();
1187        let new_user_key_id = ctx.add_local_symmetric_key(new_user_key_owned.clone());
1188
1189        // Rotate the state
1190        let rotated_state = wrapped_state
1191            .rotate(&SymmetricKeySlotId::User, &new_user_key_id, &mut ctx)
1192            .unwrap();
1193
1194        // We need to ensure two things after a rotation from V1 to V2:
1195        // 1. The new state is valid and can be set to context
1196        // 2. The new state uses the same private and signing keys
1197
1198        // 1. The new state is valid and can be set to context
1199        match rotated_state {
1200            WrappedAccountCryptographicState::V2 { .. } => {}
1201            _ => panic!("Expected V2 after rotation from V2"),
1202        }
1203        let store_2 = KeyStore::<KeySlotIds>::default();
1204        let mut ctx_2 = store_2.context_mut();
1205        let user_key_id = ctx_2.add_local_symmetric_key(new_user_key_owned.clone());
1206        rotated_state
1207            .set_to_context(&RwLock::new(None), user_key_id, &store_2, ctx_2)
1208            .unwrap();
1209        // The context was consumed, so we create a new one to inspect the keys
1210        let ctx_2 = store_2.context();
1211
1212        // 2. The new state uses the same private and signing keys
1213        let verifying_key_before_rotation = ctx
1214            .get_verifying_key(SigningKeySlotId::UserSigningKey)
1215            .expect("Signing key should be present in context before rotation");
1216        let verifying_key_after_rotation = ctx_2
1217            .get_verifying_key(SigningKeySlotId::UserSigningKey)
1218            .expect("Signing key should be present in context after rotation");
1219        assert_eq!(
1220            verifying_key_before_rotation.to_cose(),
1221            verifying_key_after_rotation.to_cose(),
1222            "Signing key should be preserved during rotation from V2 to V2"
1223        );
1224
1225        let public_key_before_rotation = ctx
1226            .get_public_key(PrivateKeySlotId::UserPrivateKey)
1227            .expect("Private key should be present in context before rotation");
1228        let public_key_after_rotation = ctx_2
1229            .get_public_key(PrivateKeySlotId::UserPrivateKey)
1230            .expect("Private key should be present in context after rotation");
1231        assert_eq!(
1232            public_key_before_rotation.to_der().unwrap(),
1233            public_key_after_rotation.to_der().unwrap(),
1234            "Private key should be preserved during rotation from V2 to V2"
1235        );
1236    }
1237
1238    #[test]
1239    fn test_to_wrapped_request_model_v1_returns_wrong_user_key_type() {
1240        let store: KeyStore<KeySlotIds> = KeyStore::default();
1241        let mut ctx = store.context_mut();
1242        let (user_key_id, wrapped) = WrappedAccountCryptographicState::make_v1(&mut ctx).unwrap();
1243        let result = wrapped.to_wrapped_request_model(&user_key_id, &mut ctx);
1244        assert!(matches!(
1245            result.unwrap_err(),
1246            AccountCryptographyInitializationError::WrongUserKeyType
1247        ));
1248    }
1249
1250    #[test]
1251    fn test_to_wrapped_request_model_v2() {
1252        let store: KeyStore<KeySlotIds> = KeyStore::default();
1253        let mut ctx = store.context_mut();
1254        let (user_key_id, wrapped) = WrappedAccountCryptographicState::make(&mut ctx).unwrap();
1255        let result = wrapped
1256            .to_wrapped_request_model(&user_key_id, &mut ctx)
1257            .unwrap();
1258
1259        let wrapped_signing_key_str = result
1260            .signature_key_pair
1261            .wrapped_signing_key
1262            .as_ref()
1263            .unwrap();
1264        assert!(!wrapped_signing_key_str.is_empty());
1265
1266        let enc_signing_key: EncString = wrapped_signing_key_str.parse().unwrap();
1267        let signing_key_tmp = ctx
1268            .unwrap_signing_key(user_key_id, &enc_signing_key)
1269            .unwrap();
1270        let verifying_key = ctx.get_verifying_key(signing_key_tmp).unwrap();
1271        let expected = B64::from(verifying_key.to_cose()).to_string();
1272        assert!(
1273            result
1274                .signature_key_pair
1275                .verifying_key
1276                .as_ref()
1277                .is_some_and(|s| s == &expected),
1278            "verifying_key should match expected value"
1279        );
1280
1281        assert_eq!(
1282            result.signature_key_pair.signature_algorithm.as_deref(),
1283            Some("mldsa44")
1284        );
1285
1286        assert!(
1287            result
1288                .public_key_encryption_key_pair
1289                .wrapped_private_key
1290                .as_ref()
1291                .is_some_and(|s| !s.is_empty()),
1292            "wrapped_private_key should be non-empty"
1293        );
1294        let wrapped_private_key_str = result
1295            .public_key_encryption_key_pair
1296            .wrapped_private_key
1297            .as_ref()
1298            .unwrap();
1299        let enc_private_key: EncString = wrapped_private_key_str.parse().unwrap();
1300        let private_key_tmp = ctx
1301            .unwrap_private_key(user_key_id, &enc_private_key)
1302            .unwrap();
1303        let public_key = ctx.get_public_key(private_key_tmp).unwrap();
1304
1305        let expected = B64::from(public_key.to_der().unwrap()).to_string();
1306        assert!(
1307            result
1308                .public_key_encryption_key_pair
1309                .public_key
1310                .as_ref()
1311                .is_some_and(|s| s == &expected),
1312            "public_key should match expected value"
1313        );
1314        assert!(
1315            result
1316                .public_key_encryption_key_pair
1317                .signed_public_key
1318                .is_some(),
1319            "signed_public_key should be present"
1320        );
1321        assert!(
1322            result
1323                .security_state
1324                .security_state
1325                .as_ref()
1326                .is_some_and(|s| !s.is_empty()),
1327            "security_state string should be non-empty"
1328        );
1329        assert!(result.security_state.security_version == 2);
1330    }
1331
1332    #[test]
1333    fn test_to_wrapped_request_model_wrong_user_key_returns_error() {
1334        let store: KeyStore<KeySlotIds> = KeyStore::default();
1335        let mut ctx = store.context_mut();
1336        let (_user_key_id, wrapped) = WrappedAccountCryptographicState::make(&mut ctx).unwrap();
1337
1338        // Create a different XAES-256-GCM user key that wasn't used to wrap these keys
1339        let wrong_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
1340
1341        let result = wrapped.to_wrapped_request_model(&wrong_user_key_id, &mut ctx);
1342        assert!(result.is_err());
1343        // Decryption failure, not a key type mismatch
1344        assert!(!matches!(
1345            result.unwrap_err(),
1346            AccountCryptographyInitializationError::WrongUserKeyType
1347        ));
1348    }
1349
1350    #[test]
1351    fn test_get_from_key_store_v1() {
1352        let store: KeyStore<KeySlotIds> = KeyStore::default();
1353        let mut ctx = store.context_mut();
1354        let (user_key, state) = WrappedAccountCryptographicState::make_v1(&mut ctx).unwrap();
1355        state
1356            .set_to_context(&RwLock::new(None), user_key, &store, ctx)
1357            .unwrap();
1358
1359        let ctx = store.context();
1360        let result = WrappedAccountCryptographicState::get_from_key_store(&ctx);
1361        assert!(result.is_ok());
1362        assert!(matches!(
1363            result.unwrap(),
1364            WrappedAccountCryptographicState::V1 { .. }
1365        ));
1366    }
1367
1368    #[test]
1369    fn test_get_from_key_store_v2_returns_error() {
1370        let store: KeyStore<KeySlotIds> = KeyStore::default();
1371        let mut ctx = store.context_mut();
1372        let (user_key, state) = WrappedAccountCryptographicState::make(&mut ctx).unwrap();
1373        state
1374            .set_to_context(&RwLock::new(None), user_key, &store, ctx)
1375            .unwrap();
1376
1377        let ctx = store.context();
1378        let result = WrappedAccountCryptographicState::get_from_key_store(&ctx);
1379        assert!(matches!(
1380            result,
1381            Err(RotateCryptographyStateError::InvalidData)
1382        ));
1383    }
1384
1385    #[test]
1386    fn test_get_from_key_store_no_user_key() {
1387        let store: KeyStore<KeySlotIds> = KeyStore::default();
1388        let ctx = store.context();
1389        let result = WrappedAccountCryptographicState::get_from_key_store(&ctx);
1390        assert!(matches!(
1391            result,
1392            Err(RotateCryptographyStateError::KeyMissing)
1393        ));
1394    }
1395}