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