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