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 AEAD key, currently XChaCha20-Poly1305.
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::XChaCha20Poly1305);
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 ctx.get_symmetric_key_algorithm(user_key)?
536                    != SymmetricKeyAlgorithm::XChaCha20Poly1305
537                {
538                    return Err(AccountCryptographyInitializationError::WrongUserKeyType);
539                }
540
541                let private_key_id = ctx
542                    .unwrap_private_key(user_key, private_key)
543                    .map_err(|_| AccountCryptographyInitializationError::WrongUserKey)?;
544                let signing_key_id = ctx
545                    .unwrap_signing_key(user_key, signing_key)
546                    .map_err(|_| AccountCryptographyInitializationError::WrongUserKey)?;
547
548                if let Some(signed_public_key) = signed_public_key {
549                    signed_public_key
550                        .to_owned()
551                        .verify_and_unwrap(&ctx.get_verifying_key(signing_key_id)?)
552                        .map_err(|_| AccountCryptographyInitializationError::TamperedData)?;
553                }
554
555                let verifying_key = ctx.get_verifying_key(signing_key_id)?;
556                let security_state: SecurityState = security_state
557                    .to_owned()
558                    .verify_and_unwrap(&verifying_key)
559                    .map_err(|_| AccountCryptographyInitializationError::TamperedData)?;
560                info!(
561                    security_state_version = security_state.version(),
562                    verifying_key = ?verifying_key,
563                    "V2 account cryptographic state verified"
564                );
565                ctx.persist_private_key(private_key_id, PrivateKeySlotId::UserPrivateKey)?;
566                ctx.persist_signing_key(signing_key_id, SigningKeySlotId::UserSigningKey)?;
567                ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User)?;
568
569                #[cfg(feature = "dangerous-crypto-debug")]
570                #[allow(deprecated)]
571                {
572                    let user_key = ctx
573                        .dangerous_get_symmetric_key(SymmetricKeySlotId::User)
574                        .expect("User key should be set");
575                    let private_key = ctx
576                        .dangerous_get_private_key(PrivateKeySlotId::UserPrivateKey)
577                        .ok();
578                    let signing_key = ctx
579                        .dangerous_get_signing_key(SigningKeySlotId::UserSigningKey)
580                        .ok();
581                    let verifying_key =
582                        ctx.get_verifying_key(SigningKeySlotId::UserSigningKey).ok();
583                    let public_key = ctx.get_public_key(PrivateKeySlotId::UserPrivateKey).ok();
584                    info!(
585                        ?user_key,
586                        ?private_key,
587                        ?signing_key,
588                        ?verifying_key,
589                        ?public_key,
590                        ?signed_public_key,
591                        ?security_state,
592                        "V2 account cryptographic state set to context."
593                    );
594                }
595
596                // Not manually dropping ctx here would lead to a deadlock, since storing the state
597                // needs to acquire a lock on the inner key store
598                drop(ctx);
599                store.set_security_state_version(security_state.version());
600                *security_state_rwlock.write().expect("RwLock not poisoned") = Some(security_state);
601            }
602        }
603
604        Ok(())
605    }
606
607    /// Retrieve the signed public key from the wrapped state, if present.
608    fn signed_public_key(
609        &self,
610    ) -> Result<Option<&SignedPublicKey>, AccountCryptographyInitializationError> {
611        match self {
612            WrappedAccountCryptographicState::V1 { .. } => Ok(None),
613            WrappedAccountCryptographicState::V2 {
614                signed_public_key, ..
615            } => Ok(signed_public_key.as_ref()),
616        }
617    }
618}
619
620#[cfg(test)]
621mod tests {
622    use std::{str::FromStr, sync::RwLock};
623
624    use bitwarden_crypto::{KeyStore, PrimitiveEncryptable};
625
626    use super::*;
627    use crate::key_management::{PrivateKeySlotId, SigningKeySlotId, SymmetricKeySlotId};
628
629    #[test]
630    #[ignore = "Manual test to verify debug format"]
631    fn test_debug() {
632        let store: KeyStore<KeySlotIds> = KeyStore::default();
633        let mut ctx = store.context_mut();
634
635        let (_, v1) = WrappedAccountCryptographicState::make_v1(&mut ctx).unwrap();
636        println!("{:?}", v1);
637
638        let v1 = format!("{v1:?}");
639        assert!(!v1.contains("private_key"));
640
641        let (_, v2) = WrappedAccountCryptographicState::make(&mut ctx).unwrap();
642        println!("{:?}", v2);
643
644        let v2 = format!("{v2:?}");
645        assert!(!v2.contains("private_key"));
646        assert!(!v2.contains("signed_public_key"));
647        assert!(!v2.contains("signing_key"));
648    }
649
650    #[test]
651    fn test_set_to_context_v1() {
652        // Prepare a temporary store to create wrapped state using a known user key
653        let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
654        let mut temp_ctx = temp_store.context_mut();
655
656        // Create a V1-style user key (Aes256CbcHmac) and add to temp context
657        let user_key = temp_ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
658
659        // Make a private key and wrap it with the user key
660        let private_key_id = temp_ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
661        let wrapped_private = temp_ctx.wrap_private_key(user_key, private_key_id).unwrap();
662
663        // Construct the V1 wrapped state
664        let wrapped = WrappedAccountCryptographicState::V1 {
665            private_key: wrapped_private,
666        };
667        #[allow(deprecated)]
668        let user_key = temp_ctx
669            .dangerous_get_symmetric_key(user_key)
670            .unwrap()
671            .to_owned();
672        drop(temp_ctx);
673        drop(temp_store);
674
675        // Now attempt to set this wrapped state into a fresh store using the same user key
676        let store: KeyStore<KeySlotIds> = KeyStore::default();
677        let mut ctx = store.context_mut();
678        let user_key = ctx.add_local_symmetric_key(user_key);
679        let security_state = RwLock::new(None);
680
681        // This should succeed and move keys into the expected global slots
682        wrapped
683            .set_to_context(&security_state, user_key, &store, ctx)
684            .unwrap();
685        let ctx = store.context();
686
687        // Assert that the private key and user symmetric key were set in the store
688        assert!(ctx.has_private_key(PrivateKeySlotId::UserPrivateKey));
689        assert!(ctx.has_symmetric_key(SymmetricKeySlotId::User));
690    }
691
692    #[test]
693    fn test_set_to_context_v2() {
694        // Prepare a temporary store to create wrapped state using a known user key
695        let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
696        let mut temp_ctx = temp_store.context_mut();
697
698        // Create a V2-style user key (XChaCha20Poly1305) and add to temp context
699        let user_key = temp_ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
700
701        // Make keys
702        let private_key_id = temp_ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
703        let signing_key_id = temp_ctx.make_signing_key(SignatureAlgorithm::Ed25519);
704        let signed_public_key = temp_ctx
705            .make_signed_public_key(private_key_id, signing_key_id)
706            .unwrap();
707
708        // Sign and wrap security state
709        let security_state = SecurityState::new();
710        let signed_security_state = security_state.sign(signing_key_id, &mut temp_ctx).unwrap();
711
712        // Wrap the private and signing keys with the user key
713        let wrapped_private = temp_ctx.wrap_private_key(user_key, private_key_id).unwrap();
714        let wrapped_signing = temp_ctx.wrap_signing_key(user_key, signing_key_id).unwrap();
715
716        let wrapped = WrappedAccountCryptographicState::V2 {
717            private_key: wrapped_private,
718            signed_public_key: Some(signed_public_key),
719            signing_key: wrapped_signing,
720            security_state: signed_security_state,
721        };
722        #[allow(deprecated)]
723        let user_key = temp_ctx
724            .dangerous_get_symmetric_key(user_key)
725            .unwrap()
726            .to_owned();
727        drop(temp_ctx);
728        drop(temp_store);
729
730        // Now attempt to set this wrapped state into a fresh store using the same user key
731        let store: KeyStore<KeySlotIds> = KeyStore::default();
732        let mut ctx = store.context_mut();
733        let user_key = ctx.add_local_symmetric_key(user_key);
734        let security_state = RwLock::new(None);
735
736        wrapped
737            .set_to_context(&security_state, user_key, &store, ctx)
738            .unwrap();
739
740        assert!(store.context().has_symmetric_key(SymmetricKeySlotId::User));
741        // Assert that the account keys and security state were set
742        assert!(
743            store
744                .context()
745                .has_private_key(PrivateKeySlotId::UserPrivateKey)
746        );
747        assert!(
748            store
749                .context()
750                .has_signing_key(SigningKeySlotId::UserSigningKey)
751        );
752        // Ensure security state was recorded
753        assert!(security_state.read().unwrap().is_some());
754    }
755
756    #[test]
757    fn test_to_private_keys_request_model_v2() {
758        let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
759        let mut temp_ctx = temp_store.context_mut();
760        let (user_key, wrapped_account_cryptography_state) =
761            WrappedAccountCryptographicState::make(&mut temp_ctx).unwrap();
762
763        wrapped_account_cryptography_state
764            .set_to_context(&RwLock::new(None), user_key, &temp_store, temp_ctx)
765            .unwrap();
766
767        let mut ctx = temp_store.context_mut();
768        let model = wrapped_account_cryptography_state
769            .to_request_model(&SymmetricKeySlotId::User, &mut ctx)
770            .expect("to_private_keys_request_model should succeed");
771        drop(ctx);
772
773        let ctx = temp_store.context();
774
775        let sig_pair = model
776            .signature_key_pair
777            .expect("signature_key_pair present");
778        assert_eq!(
779            sig_pair.verifying_key.unwrap(),
780            B64::from(
781                ctx.get_verifying_key(SigningKeySlotId::UserSigningKey)
782                    .unwrap()
783                    .to_cose()
784            )
785            .to_string()
786        );
787
788        let pk_pair = model.public_key_encryption_key_pair.unwrap();
789        assert_eq!(
790            pk_pair.public_key.unwrap(),
791            B64::from(
792                ctx.get_public_key(PrivateKeySlotId::UserPrivateKey)
793                    .unwrap()
794                    .to_der()
795                    .unwrap()
796            )
797            .to_string()
798        );
799
800        let signed_security_state = model
801            .security_state
802            .clone()
803            .expect("security_state present");
804        let security_state =
805            SignedSecurityState::from_str(signed_security_state.security_state.unwrap().as_str())
806                .unwrap()
807                .verify_and_unwrap(
808                    &ctx.get_verifying_key(SigningKeySlotId::UserSigningKey)
809                        .unwrap(),
810                )
811                .expect("security state should verify");
812        assert_eq!(
813            security_state.version(),
814            model.security_state.unwrap().security_version as u64
815        );
816    }
817
818    #[test]
819    fn test_set_to_context_v1_corrupt_private_key() {
820        // Test that a V1 account with a corrupt private key (valid EncString but invalid key data)
821        // can still initialize, but skips setting the private key
822        let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
823        let mut temp_ctx = temp_store.context_mut();
824
825        let user_key = temp_ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
826        let corrupt_private_key = "not a private key"
827            .encrypt(&mut temp_ctx, user_key)
828            .unwrap();
829
830        // Construct the V1 wrapped state with corrupt private key
831        let wrapped = WrappedAccountCryptographicState::V1 {
832            private_key: corrupt_private_key,
833        };
834
835        #[expect(deprecated)]
836        let user_key_material = temp_ctx
837            .dangerous_get_symmetric_key(user_key)
838            .unwrap()
839            .to_owned();
840        drop(temp_ctx);
841        drop(temp_store);
842
843        // Now attempt to set this wrapped state into a fresh store
844        let store: KeyStore<KeySlotIds> = KeyStore::default();
845        let mut ctx = store.context_mut();
846        let user_key = ctx.add_local_symmetric_key(user_key_material);
847        let security_state = RwLock::new(None);
848
849        wrapped
850            .set_to_context(&security_state, user_key, &store, ctx)
851            .unwrap();
852
853        let ctx = store.context();
854
855        // The user symmetric key should be set
856        assert!(ctx.has_symmetric_key(SymmetricKeySlotId::User));
857        // But the private key should NOT be set (due to corruption)
858        assert!(!ctx.has_private_key(PrivateKeySlotId::UserPrivateKey));
859    }
860
861    #[test]
862    fn test_try_from_response_v2_roundtrip() {
863        use bitwarden_api_api::models::{
864            PublicKeyEncryptionKeyPairResponseModel, SecurityStateModel,
865            SignatureKeyPairResponseModel,
866        };
867
868        let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
869        let mut temp_ctx = temp_store.context_mut();
870        let (user_key, wrapped_state) =
871            WrappedAccountCryptographicState::make(&mut temp_ctx).unwrap();
872
873        wrapped_state
874            .set_to_context(&RwLock::new(None), user_key, &temp_store, temp_ctx)
875            .unwrap();
876
877        let mut ctx = temp_store.context_mut();
878        let request_model = wrapped_state
879            .to_request_model(&SymmetricKeySlotId::User, &mut ctx)
880            .unwrap();
881        drop(ctx);
882
883        let pk_pair = request_model.public_key_encryption_key_pair.unwrap();
884        let sig_pair = request_model.signature_key_pair.unwrap();
885        let sec_state = request_model.security_state.unwrap();
886
887        let response = PrivateKeysResponseModel {
888            object: None,
889            public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
890                object: None,
891                wrapped_private_key: pk_pair.wrapped_private_key,
892                public_key: pk_pair.public_key,
893                signed_public_key: pk_pair.signed_public_key,
894            }),
895            signature_key_pair: Some(Box::new(SignatureKeyPairResponseModel {
896                object: None,
897                wrapped_signing_key: sig_pair.wrapped_signing_key,
898                verifying_key: sig_pair.verifying_key,
899            })),
900            security_state: Some(Box::new(SecurityStateModel {
901                security_state: sec_state.security_state,
902                security_version: sec_state.security_version,
903            })),
904        };
905
906        let parsed = WrappedAccountCryptographicState::try_from(&response)
907            .expect("V2 response should parse successfully");
908
909        assert_eq!(parsed, wrapped_state);
910    }
911
912    #[test]
913    fn test_try_from_response_v1() {
914        use bitwarden_api_api::models::PublicKeyEncryptionKeyPairResponseModel;
915
916        let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
917        let mut temp_ctx = temp_store.context_mut();
918        let (_user_key, wrapped_state) =
919            WrappedAccountCryptographicState::make_v1(&mut temp_ctx).unwrap();
920
921        let wrapped_private_key = match &wrapped_state {
922            WrappedAccountCryptographicState::V1 { private_key } => private_key.to_string(),
923            _ => panic!("Expected V1"),
924        };
925        drop(temp_ctx);
926
927        let response = PrivateKeysResponseModel {
928            object: None,
929            public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
930                object: None,
931                wrapped_private_key: Some(wrapped_private_key),
932                public_key: None,
933                signed_public_key: None,
934            }),
935            signature_key_pair: None,
936            security_state: None,
937        };
938
939        let parsed = WrappedAccountCryptographicState::try_from(&response)
940            .expect("V1 response should parse successfully");
941
942        assert_eq!(parsed, wrapped_state);
943    }
944
945    #[test]
946    fn test_try_from_response_missing_private_key() {
947        use bitwarden_api_api::models::PublicKeyEncryptionKeyPairResponseModel;
948
949        let response = PrivateKeysResponseModel {
950            object: None,
951            public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
952                object: None,
953                wrapped_private_key: None,
954                public_key: None,
955                signed_public_key: None,
956            }),
957            signature_key_pair: None,
958            security_state: None,
959        };
960
961        let result = WrappedAccountCryptographicState::try_from(&response);
962        assert!(result.is_err());
963        assert!(
964            matches!(
965                result.unwrap_err(),
966                AccountKeysResponseParseError::MissingField(_)
967            ),
968            "Should return MissingField error"
969        );
970    }
971
972    #[test]
973    fn test_try_from_response_v2_encryption_missing_signature_key_pair() {
974        use bitwarden_api_api::models::PublicKeyEncryptionKeyPairResponseModel;
975
976        // Create a V2 state to get a COSE-encrypted private key
977        let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
978        let mut temp_ctx = temp_store.context_mut();
979        let (user_key, wrapped_state) =
980            WrappedAccountCryptographicState::make(&mut temp_ctx).unwrap();
981
982        wrapped_state
983            .set_to_context(&RwLock::new(None), user_key, &temp_store, temp_ctx)
984            .unwrap();
985
986        let mut ctx = temp_store.context_mut();
987        let request_model = wrapped_state
988            .to_request_model(&SymmetricKeySlotId::User, &mut ctx)
989            .unwrap();
990        drop(ctx);
991
992        let pk_pair = request_model.public_key_encryption_key_pair.unwrap();
993
994        // V2-encrypted private key but no signature_key_pair or security_state
995        let response = PrivateKeysResponseModel {
996            object: None,
997            public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
998                object: None,
999                wrapped_private_key: pk_pair.wrapped_private_key,
1000                public_key: pk_pair.public_key,
1001                signed_public_key: None,
1002            }),
1003            signature_key_pair: None,
1004            security_state: None,
1005        };
1006
1007        let result = WrappedAccountCryptographicState::try_from(&response);
1008        assert!(matches!(
1009            result.unwrap_err(),
1010            AccountKeysResponseParseError::InconsistentState
1011        ));
1012    }
1013
1014    #[test]
1015    fn test_try_from_response_v1_encryption_with_unexpected_v2_fields() {
1016        use bitwarden_api_api::models::{
1017            PublicKeyEncryptionKeyPairResponseModel, SignatureKeyPairResponseModel,
1018        };
1019
1020        // Create a V1 state to get an AES-encrypted private key
1021        let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
1022        let mut temp_ctx = temp_store.context_mut();
1023        let (_user_key, wrapped_state) =
1024            WrappedAccountCryptographicState::make_v1(&mut temp_ctx).unwrap();
1025
1026        let wrapped_private_key = match &wrapped_state {
1027            WrappedAccountCryptographicState::V1 { private_key } => private_key.to_string(),
1028            _ => panic!("Expected V1"),
1029        };
1030        drop(temp_ctx);
1031
1032        // V1-encrypted private key but with a signature_key_pair present
1033        let response = PrivateKeysResponseModel {
1034            object: None,
1035            public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
1036                object: None,
1037                wrapped_private_key: Some(wrapped_private_key),
1038                public_key: None,
1039                signed_public_key: None,
1040            }),
1041            signature_key_pair: Some(Box::new(SignatureKeyPairResponseModel {
1042                object: None,
1043                wrapped_signing_key: Some("bogus".to_string()),
1044                verifying_key: None,
1045            })),
1046            security_state: None,
1047        };
1048
1049        let result = WrappedAccountCryptographicState::try_from(&response);
1050        assert!(matches!(
1051            result.unwrap_err(),
1052            AccountKeysResponseParseError::InconsistentState
1053        ));
1054    }
1055
1056    #[test]
1057    fn test_rotate_v1_to_v2() {
1058        // Create a key store and context
1059        let store: KeyStore<KeySlotIds> = KeyStore::default();
1060        let mut ctx = store.context_mut();
1061
1062        // Create a V1-style user key and add to context
1063        let (old_user_key_id, wrapped_state) =
1064            WrappedAccountCryptographicState::make_v1(&mut ctx).unwrap();
1065        let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
1066        #[allow(deprecated)]
1067        let new_user_key_owned = ctx
1068            .dangerous_get_symmetric_key(new_user_key_id)
1069            .unwrap()
1070            .to_owned();
1071        wrapped_state
1072            .set_to_context(&RwLock::new(None), old_user_key_id, &store, ctx)
1073            .unwrap();
1074
1075        // The previous context got consumed, so we are creating a new one here. Setting the state
1076        // to context persisted the user-key and other keys
1077        let mut ctx = store.context_mut();
1078        let new_user_key_id = ctx.add_local_symmetric_key(new_user_key_owned.clone());
1079
1080        // Rotate the state
1081        let rotated_state = wrapped_state
1082            .rotate(&SymmetricKeySlotId::User, &new_user_key_id, &mut ctx)
1083            .unwrap();
1084
1085        // We need to ensure two things after a rotation from V1 to V2:
1086        // 1. The new state is valid and can be set to context
1087        // 2. The new state uses the same private and signing keys
1088
1089        // 1. The new state is valid and can be set to context
1090        match rotated_state {
1091            WrappedAccountCryptographicState::V2 { .. } => {}
1092            _ => panic!("Expected V2 after rotation from V1"),
1093        }
1094        let store_2 = KeyStore::<KeySlotIds>::default();
1095        let mut ctx_2 = store_2.context_mut();
1096        let user_key_id = ctx_2.add_local_symmetric_key(new_user_key_owned.clone());
1097        rotated_state
1098            .set_to_context(&RwLock::new(None), user_key_id, &store_2, ctx_2)
1099            .unwrap();
1100        // The context was consumed, so we create a new one to inspect the keys
1101        let ctx_2 = store_2.context();
1102
1103        // 2. The new state uses the same private and signing keys
1104        let public_key_before_rotation = ctx
1105            .get_public_key(PrivateKeySlotId::UserPrivateKey)
1106            .expect("Private key should be present in context before rotation");
1107        let public_key_after_rotation = ctx_2
1108            .get_public_key(PrivateKeySlotId::UserPrivateKey)
1109            .expect("Private key should be present in context after rotation");
1110        assert_eq!(
1111            public_key_before_rotation.to_der().unwrap(),
1112            public_key_after_rotation.to_der().unwrap(),
1113            "Private key should be preserved during rotation from V2 to V2"
1114        );
1115    }
1116
1117    #[test]
1118    fn test_rotate_v2() {
1119        // Create a key store and context
1120        let store: KeyStore<KeySlotIds> = KeyStore::default();
1121        let mut ctx = store.context_mut();
1122
1123        // Create a V2-style user key and add to context
1124        let (old_user_key_id, wrapped_state) =
1125            WrappedAccountCryptographicState::make(&mut ctx).unwrap();
1126        let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
1127        #[allow(deprecated)]
1128        let new_user_key_owned = ctx
1129            .dangerous_get_symmetric_key(new_user_key_id)
1130            .unwrap()
1131            .to_owned();
1132        wrapped_state
1133            .set_to_context(&RwLock::new(None), old_user_key_id, &store, ctx)
1134            .unwrap();
1135
1136        // The previous context got consumed, so we are creating a new one here. Setting the state
1137        // to context persisted the user-key and other keys
1138        let mut ctx = store.context_mut();
1139        let new_user_key_id = ctx.add_local_symmetric_key(new_user_key_owned.clone());
1140
1141        // Rotate the state
1142        let rotated_state = wrapped_state
1143            .rotate(&SymmetricKeySlotId::User, &new_user_key_id, &mut ctx)
1144            .unwrap();
1145
1146        // We need to ensure two things after a rotation from V1 to V2:
1147        // 1. The new state is valid and can be set to context
1148        // 2. The new state uses the same private and signing keys
1149
1150        // 1. The new state is valid and can be set to context
1151        match rotated_state {
1152            WrappedAccountCryptographicState::V2 { .. } => {}
1153            _ => panic!("Expected V2 after rotation from V2"),
1154        }
1155        let store_2 = KeyStore::<KeySlotIds>::default();
1156        let mut ctx_2 = store_2.context_mut();
1157        let user_key_id = ctx_2.add_local_symmetric_key(new_user_key_owned.clone());
1158        rotated_state
1159            .set_to_context(&RwLock::new(None), user_key_id, &store_2, ctx_2)
1160            .unwrap();
1161        // The context was consumed, so we create a new one to inspect the keys
1162        let ctx_2 = store_2.context();
1163
1164        // 2. The new state uses the same private and signing keys
1165        let verifying_key_before_rotation = ctx
1166            .get_verifying_key(SigningKeySlotId::UserSigningKey)
1167            .expect("Signing key should be present in context before rotation");
1168        let verifying_key_after_rotation = ctx_2
1169            .get_verifying_key(SigningKeySlotId::UserSigningKey)
1170            .expect("Signing key should be present in context after rotation");
1171        assert_eq!(
1172            verifying_key_before_rotation.to_cose(),
1173            verifying_key_after_rotation.to_cose(),
1174            "Signing key should be preserved during rotation from V2 to V2"
1175        );
1176
1177        let public_key_before_rotation = ctx
1178            .get_public_key(PrivateKeySlotId::UserPrivateKey)
1179            .expect("Private key should be present in context before rotation");
1180        let public_key_after_rotation = ctx_2
1181            .get_public_key(PrivateKeySlotId::UserPrivateKey)
1182            .expect("Private key should be present in context after rotation");
1183        assert_eq!(
1184            public_key_before_rotation.to_der().unwrap(),
1185            public_key_after_rotation.to_der().unwrap(),
1186            "Private key should be preserved during rotation from V2 to V2"
1187        );
1188    }
1189
1190    #[test]
1191    fn test_to_wrapped_request_model_v1_returns_wrong_user_key_type() {
1192        let store: KeyStore<KeySlotIds> = KeyStore::default();
1193        let mut ctx = store.context_mut();
1194        let (user_key_id, wrapped) = WrappedAccountCryptographicState::make_v1(&mut ctx).unwrap();
1195        let result = wrapped.to_wrapped_request_model(&user_key_id, &mut ctx);
1196        assert!(matches!(
1197            result.unwrap_err(),
1198            AccountCryptographyInitializationError::WrongUserKeyType
1199        ));
1200    }
1201
1202    #[test]
1203    fn test_to_wrapped_request_model_v2() {
1204        let store: KeyStore<KeySlotIds> = KeyStore::default();
1205        let mut ctx = store.context_mut();
1206        let (user_key_id, wrapped) = WrappedAccountCryptographicState::make(&mut ctx).unwrap();
1207        let result = wrapped
1208            .to_wrapped_request_model(&user_key_id, &mut ctx)
1209            .unwrap();
1210
1211        let wrapped_signing_key_str = result
1212            .signature_key_pair
1213            .wrapped_signing_key
1214            .as_ref()
1215            .unwrap();
1216        assert!(!wrapped_signing_key_str.is_empty());
1217
1218        let enc_signing_key: EncString = wrapped_signing_key_str.parse().unwrap();
1219        let signing_key_tmp = ctx
1220            .unwrap_signing_key(user_key_id, &enc_signing_key)
1221            .unwrap();
1222        let verifying_key = ctx.get_verifying_key(signing_key_tmp).unwrap();
1223        let expected = B64::from(verifying_key.to_cose()).to_string();
1224        assert!(
1225            result
1226                .signature_key_pair
1227                .verifying_key
1228                .as_ref()
1229                .is_some_and(|s| s == &expected),
1230            "verifying_key should match expected value"
1231        );
1232
1233        assert_eq!(
1234            result.signature_key_pair.signature_algorithm.as_deref(),
1235            Some("mldsa44")
1236        );
1237
1238        assert!(
1239            result
1240                .public_key_encryption_key_pair
1241                .wrapped_private_key
1242                .as_ref()
1243                .is_some_and(|s| !s.is_empty()),
1244            "wrapped_private_key should be non-empty"
1245        );
1246        let wrapped_private_key_str = result
1247            .public_key_encryption_key_pair
1248            .wrapped_private_key
1249            .as_ref()
1250            .unwrap();
1251        let enc_private_key: EncString = wrapped_private_key_str.parse().unwrap();
1252        let private_key_tmp = ctx
1253            .unwrap_private_key(user_key_id, &enc_private_key)
1254            .unwrap();
1255        let public_key = ctx.get_public_key(private_key_tmp).unwrap();
1256
1257        let expected = B64::from(public_key.to_der().unwrap()).to_string();
1258        assert!(
1259            result
1260                .public_key_encryption_key_pair
1261                .public_key
1262                .as_ref()
1263                .is_some_and(|s| s == &expected),
1264            "public_key should match expected value"
1265        );
1266        assert!(
1267            result
1268                .public_key_encryption_key_pair
1269                .signed_public_key
1270                .is_some(),
1271            "signed_public_key should be present"
1272        );
1273        assert!(
1274            result
1275                .security_state
1276                .security_state
1277                .as_ref()
1278                .is_some_and(|s| !s.is_empty()),
1279            "security_state string should be non-empty"
1280        );
1281        assert!(result.security_state.security_version == 2);
1282    }
1283
1284    #[test]
1285    fn test_to_wrapped_request_model_wrong_user_key_returns_error() {
1286        let store: KeyStore<KeySlotIds> = KeyStore::default();
1287        let mut ctx = store.context_mut();
1288        let (_user_key_id, wrapped) = WrappedAccountCryptographicState::make(&mut ctx).unwrap();
1289
1290        // Create a different XChaCha20Poly1305 user key that wasn't used to wrap these keys
1291        let wrong_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
1292
1293        let result = wrapped.to_wrapped_request_model(&wrong_user_key_id, &mut ctx);
1294        assert!(result.is_err());
1295        // Decryption failure, not a key type mismatch
1296        assert!(!matches!(
1297            result.unwrap_err(),
1298            AccountCryptographyInitializationError::WrongUserKeyType
1299        ));
1300    }
1301
1302    #[test]
1303    fn test_get_from_key_store_v1() {
1304        let store: KeyStore<KeySlotIds> = KeyStore::default();
1305        let mut ctx = store.context_mut();
1306        let (user_key, state) = WrappedAccountCryptographicState::make_v1(&mut ctx).unwrap();
1307        state
1308            .set_to_context(&RwLock::new(None), user_key, &store, ctx)
1309            .unwrap();
1310
1311        let ctx = store.context();
1312        let result = WrappedAccountCryptographicState::get_from_key_store(&ctx);
1313        assert!(result.is_ok());
1314        assert!(matches!(
1315            result.unwrap(),
1316            WrappedAccountCryptographicState::V1 { .. }
1317        ));
1318    }
1319
1320    #[test]
1321    fn test_get_from_key_store_v2_returns_error() {
1322        let store: KeyStore<KeySlotIds> = KeyStore::default();
1323        let mut ctx = store.context_mut();
1324        let (user_key, state) = WrappedAccountCryptographicState::make(&mut ctx).unwrap();
1325        state
1326            .set_to_context(&RwLock::new(None), user_key, &store, ctx)
1327            .unwrap();
1328
1329        let ctx = store.context();
1330        let result = WrappedAccountCryptographicState::get_from_key_store(&ctx);
1331        assert!(matches!(
1332            result,
1333            Err(RotateCryptographyStateError::InvalidData)
1334        ));
1335    }
1336
1337    #[test]
1338    fn test_get_from_key_store_no_user_key() {
1339        let store: KeyStore<KeySlotIds> = KeyStore::default();
1340        let ctx = store.context();
1341        let result = WrappedAccountCryptographicState::get_from_key_store(&ctx);
1342        assert!(matches!(
1343            result,
1344            Err(RotateCryptographyStateError::KeyMissing)
1345        ));
1346    }
1347}