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