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