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