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::{AccountKeysRequestModel, SecurityStateModel};
14use bitwarden_crypto::{
15    CoseSerializable, CryptoError, EncString, KeyStore, KeyStoreContext,
16    PublicKeyEncryptionAlgorithm, SignatureAlgorithm, SignedPublicKey, SymmetricKeyAlgorithm,
17};
18use bitwarden_encoding::B64;
19use bitwarden_error::bitwarden_error;
20use serde::{Deserialize, Serialize};
21use thiserror::Error;
22use tracing::info;
23#[cfg(feature = "wasm")]
24use tsify::Tsify;
25
26use crate::{
27    UserId,
28    key_management::{
29        AsymmetricKeyId, KeyIds, SecurityState, SignedSecurityState, SigningKeyId, SymmetricKeyId,
30    },
31};
32
33/// Errors that can occur during initialization of the account cryptographic state.
34#[derive(Debug, Error)]
35#[bitwarden_error(flat)]
36pub enum AccountCryptographyInitializationError {
37    /// The encryption algorithm from the user key does not match one of the encrypted items.
38    /// This would mean that the user's account is corrupt.
39    #[error("The encryption type of the user key does not match the account cryptographic state")]
40    WrongUserKeyType,
41    /// The provide user-key is incorrect or out-of-date. This may happen when a use-key changed
42    /// and a local unlock-method is not yet updated.
43    #[error("Wrong user key")]
44    WrongUserKey,
45    /// The decrypted data is corrupt.
46    #[error("Decryption succeeded but produced corrupt data")]
47    CorruptData,
48    /// The decrypted data is corrupt.
49    #[error("Signature or mac verification failed, the data may have been tampered with")]
50    TamperedData,
51    /// The key store is already initialized with account keys. Currently, updating keys is not a
52    /// supported operation
53    #[error("Key store is already initialized")]
54    KeyStoreAlreadyInitialized,
55    /// A generic cryptographic error occurred.
56    #[error("A generic cryptographic error occurred: {0}")]
57    GenericCrypto(CryptoError),
58}
59
60impl From<CryptoError> for AccountCryptographyInitializationError {
61    fn from(err: CryptoError) -> Self {
62        AccountCryptographyInitializationError::GenericCrypto(err)
63    }
64}
65
66/// Any keys / cryptographic protection "downstream" from the account symmetric key (user key).
67/// Private keys are protected by the user key.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
70#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
71#[allow(clippy::large_enum_variant)]
72pub enum WrappedAccountCryptographicState {
73    /// A V1 user has only a private key.
74    V1 {
75        /// The user's encryption private key, wrapped by the user key.
76        private_key: EncString,
77    },
78    /// A V2 user has a private key, a signing key, a signed public key and a signed security state.
79    /// The SignedPublicKey ensures that others can verify the public key is claimed by an identity
80    /// they want to share data to. The signed security state protects against cryptographic
81    /// downgrades.
82    V2 {
83        /// The user's encryption private key, wrapped by the user key.
84        private_key: EncString,
85        /// The user's public-key for the private key, signed by the user's signing key.
86        /// Note: This is optional for backwards compatibility. After a few releases, this will be
87        /// made non-optional once all clients store the response on sync.
88        signed_public_key: Option<SignedPublicKey>,
89        /// The user's signing key, wrapped by the user key.
90        signing_key: EncString,
91        /// The user's signed security state.
92        security_state: SignedSecurityState,
93    },
94}
95
96impl WrappedAccountCryptographicState {
97    /// Converts to a AccountKeysRequestModel in order to make API requests. Since the
98    /// [WrappedAccountCryptographicState] is encrypted, the key store needs to contain the
99    /// user key required to unlock this state.
100    pub fn to_request_model(
101        &self,
102        user_key: &SymmetricKeyId,
103        ctx: &mut KeyStoreContext<KeyIds>,
104    ) -> Result<AccountKeysRequestModel, AccountCryptographyInitializationError> {
105        let private_key = match self {
106            WrappedAccountCryptographicState::V1 { private_key }
107            | WrappedAccountCryptographicState::V2 { private_key, .. } => private_key.clone(),
108        };
109        let private_key_tmp_id = ctx.unwrap_private_key(*user_key, &private_key)?;
110        let public_key = ctx.get_public_key(private_key_tmp_id)?;
111
112        let signature_keypair = match self {
113            WrappedAccountCryptographicState::V1 { .. } => None,
114            WrappedAccountCryptographicState::V2 { signing_key, .. } => {
115                let signing_key_tmp_id = ctx.unwrap_signing_key(*user_key, signing_key)?;
116                let verifying_key = ctx.get_verifying_key(signing_key_tmp_id)?;
117                Some((signing_key.clone(), verifying_key))
118            }
119        };
120
121        Ok(AccountKeysRequestModel {
122            // Note: This property is deprecated and should be removed after a transition period.
123            user_key_encrypted_account_private_key: Some(private_key.to_string()),
124            // Note: This property is deprecated and should be removed after a transition period.
125            account_public_key: Some(B64::from(public_key.to_der()?).to_string()),
126            signature_key_pair: signature_keypair
127                .as_ref()
128                .map(|(signing_key, verifying_key)| {
129                    Box::new(bitwarden_api_api::models::SignatureKeyPairRequestModel {
130                        wrapped_signing_key: Some(signing_key.to_string()),
131                        verifying_key: Some(B64::from(verifying_key.to_cose()).to_string()),
132                        signature_algorithm: Some(match verifying_key.algorithm() {
133                            SignatureAlgorithm::Ed25519 => "ed25519".to_string(),
134                        }),
135                    })
136                }),
137            public_key_encryption_key_pair: Some(Box::new(
138                bitwarden_api_api::models::PublicKeyEncryptionKeyPairRequestModel {
139                    wrapped_private_key: match self {
140                        WrappedAccountCryptographicState::V1 { private_key }
141                        | WrappedAccountCryptographicState::V2 { private_key, .. } => {
142                            Some(private_key.to_string())
143                        }
144                    },
145                    public_key: Some(B64::from(public_key.to_der()?).to_string()),
146                    signed_public_key: match self.signed_public_key() {
147                        Ok(Some(spk)) => Some(spk.clone().into()),
148                        _ => None,
149                    },
150                },
151            )),
152            security_state: match (self, signature_keypair.as_ref()) {
153                (_, None) | (WrappedAccountCryptographicState::V1 { .. }, Some(_)) => None,
154                (
155                    WrappedAccountCryptographicState::V2 { security_state, .. },
156                    Some((_, verifying_key)),
157                ) => {
158                    // Convert the verified state's version to i32 for the API model
159                    Some(Box::new(SecurityStateModel {
160                        security_state: Some(security_state.into()),
161                        security_version: security_state
162                            .to_owned()
163                            .verify_and_unwrap(verifying_key)
164                            .map_err(|_| AccountCryptographyInitializationError::TamperedData)?
165                            .version() as i32,
166                    }))
167                }
168            },
169        })
170    }
171
172    /// Creates a new V2 account cryptographic state with fresh keys. This does not change the user
173    /// state, but does set some keys to the local context.
174    pub fn make(
175        ctx: &mut KeyStoreContext<KeyIds>,
176        user_id: UserId,
177    ) -> Result<(SymmetricKeyId, Self), AccountCryptographyInitializationError> {
178        let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
179        let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1)?;
180        let signing_key = ctx.make_signing_key(SignatureAlgorithm::Ed25519)?;
181        let signed_public_key = ctx.make_signed_public_key(private_key, signing_key)?;
182
183        let security_state = SecurityState::initialize_for_user(user_id);
184        let signed_security_state = security_state.sign(signing_key, ctx)?;
185
186        Ok((
187            user_key,
188            WrappedAccountCryptographicState::V2 {
189                private_key: ctx.wrap_private_key(user_key, private_key)?,
190                signed_public_key: Some(signed_public_key),
191                signing_key: ctx.wrap_signing_key(user_key, signing_key)?,
192                security_state: signed_security_state,
193            },
194        ))
195    }
196
197    /// Set the decrypted account cryptographic state to the context's non-local storage.
198    /// This needs a mutable context passed in that already has a user_key set to a local key slot,
199    /// for which the id is passed in as `user_key`. Note, that this function drops the context
200    /// and clears the existing local state, after persisting it.
201    pub(crate) fn set_to_context(
202        &self,
203        security_state_rwlock: &RwLock<Option<SecurityState>>,
204        user_key: SymmetricKeyId,
205        store: &KeyStore<KeyIds>,
206        mut ctx: KeyStoreContext<KeyIds>,
207    ) -> Result<(), AccountCryptographyInitializationError> {
208        if ctx.has_symmetric_key(SymmetricKeyId::User)
209            || ctx.has_asymmetric_key(AsymmetricKeyId::UserPrivateKey)
210            || ctx.has_signing_key(SigningKeyId::UserSigningKey)
211        {
212            return Err(AccountCryptographyInitializationError::KeyStoreAlreadyInitialized);
213        }
214
215        match self {
216            WrappedAccountCryptographicState::V1 { private_key } => {
217                info!("Initializing V1 account cryptographic state");
218                if ctx.get_symmetric_key_algorithm(user_key)?
219                    != SymmetricKeyAlgorithm::Aes256CbcHmac
220                {
221                    return Err(AccountCryptographyInitializationError::WrongUserKeyType);
222                }
223
224                // Some users have unreadable V1 private keys. In this case, we set no keys to
225                // state.
226                if let Ok(private_key_id) = ctx.unwrap_private_key(user_key, private_key) {
227                    ctx.persist_asymmetric_key(private_key_id, AsymmetricKeyId::UserPrivateKey)?;
228                } else {
229                    tracing::warn!(
230                        "V1 private key could not be unwrapped, skipping setting private key"
231                    );
232                }
233
234                ctx.persist_symmetric_key(user_key, SymmetricKeyId::User)?;
235            }
236            WrappedAccountCryptographicState::V2 {
237                private_key,
238                signed_public_key,
239                signing_key,
240                security_state,
241            } => {
242                info!("Initializing V2 account cryptographic state");
243                if ctx.get_symmetric_key_algorithm(user_key)?
244                    != SymmetricKeyAlgorithm::XChaCha20Poly1305
245                {
246                    return Err(AccountCryptographyInitializationError::WrongUserKeyType);
247                }
248
249                let private_key_id = ctx
250                    .unwrap_private_key(user_key, private_key)
251                    .map_err(|_| AccountCryptographyInitializationError::WrongUserKey)?;
252                let signing_key_id = ctx
253                    .unwrap_signing_key(user_key, signing_key)
254                    .map_err(|_| AccountCryptographyInitializationError::WrongUserKey)?;
255
256                if let Some(signed_public_key) = signed_public_key {
257                    signed_public_key
258                        .to_owned()
259                        .verify_and_unwrap(&ctx.get_verifying_key(signing_key_id)?)
260                        .map_err(|_| AccountCryptographyInitializationError::TamperedData)?;
261                }
262
263                let security_state: SecurityState = security_state
264                    .to_owned()
265                    .verify_and_unwrap(&ctx.get_verifying_key(signing_key_id)?)
266                    .map_err(|_| AccountCryptographyInitializationError::TamperedData)?;
267                ctx.persist_asymmetric_key(private_key_id, AsymmetricKeyId::UserPrivateKey)?;
268                ctx.persist_signing_key(signing_key_id, SigningKeyId::UserSigningKey)?;
269                ctx.persist_symmetric_key(user_key, SymmetricKeyId::User)?;
270                // Not manually dropping ctx here would lead to a deadlock, since storing the state
271                // needs to acquire a lock on the inner key store
272                drop(ctx);
273                store.set_security_state_version(security_state.version());
274                *security_state_rwlock.write().expect("RwLock not poisoned") = Some(security_state);
275            }
276        }
277
278        Ok(())
279    }
280
281    /// Retrieve the signed public key from the wrapped state, if present.
282    fn signed_public_key(
283        &self,
284    ) -> Result<Option<&SignedPublicKey>, AccountCryptographyInitializationError> {
285        match self {
286            WrappedAccountCryptographicState::V1 { .. } => Ok(None),
287            WrappedAccountCryptographicState::V2 {
288                signed_public_key, ..
289            } => Ok(signed_public_key.as_ref()),
290        }
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use std::{str::FromStr, sync::RwLock};
297
298    use bitwarden_crypto::{KeyStore, PrimitiveEncryptable};
299
300    use super::*;
301    use crate::key_management::{AsymmetricKeyId, SigningKeyId, SymmetricKeyId};
302
303    #[test]
304    fn test_set_to_context_v1() {
305        // Prepare a temporary store to create wrapped state using a known user key
306        let temp_store: KeyStore<KeyIds> = KeyStore::default();
307        let mut temp_ctx = temp_store.context_mut();
308
309        // Create a V1-style user key (Aes256CbcHmac) and add to temp context
310        let user_key = temp_ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
311
312        // Make a private key and wrap it with the user key
313        let private_key_id = temp_ctx
314            .make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1)
315            .unwrap();
316        let wrapped_private = temp_ctx.wrap_private_key(user_key, private_key_id).unwrap();
317
318        // Construct the V1 wrapped state
319        let wrapped = WrappedAccountCryptographicState::V1 {
320            private_key: wrapped_private,
321        };
322        #[allow(deprecated)]
323        let user_key = temp_ctx
324            .dangerous_get_symmetric_key(user_key)
325            .unwrap()
326            .to_owned();
327        drop(temp_ctx);
328        drop(temp_store);
329
330        // Now attempt to set this wrapped state into a fresh store using the same user key
331        let store: KeyStore<KeyIds> = KeyStore::default();
332        let mut ctx = store.context_mut();
333        let user_key = ctx.add_local_symmetric_key(user_key);
334        let security_state = RwLock::new(None);
335
336        // This should succeed and move keys into the expected global slots
337        wrapped
338            .set_to_context(&security_state, user_key, &store, ctx)
339            .unwrap();
340        let ctx = store.context();
341
342        // Assert that the private key and user symmetric key were set in the store
343        assert!(ctx.has_asymmetric_key(AsymmetricKeyId::UserPrivateKey));
344        assert!(ctx.has_symmetric_key(SymmetricKeyId::User));
345    }
346
347    #[test]
348    fn test_set_to_context_v2() {
349        // Prepare a temporary store to create wrapped state using a known user key
350        let temp_store: KeyStore<KeyIds> = KeyStore::default();
351        let mut temp_ctx = temp_store.context_mut();
352
353        // Create a V2-style user key (XChaCha20Poly1305) and add to temp context
354        let user_key = temp_ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
355
356        // Make keys
357        let private_key_id = temp_ctx
358            .make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1)
359            .unwrap();
360        let signing_key_id = temp_ctx
361            .make_signing_key(SignatureAlgorithm::Ed25519)
362            .unwrap();
363        let signed_public_key = temp_ctx
364            .make_signed_public_key(private_key_id, signing_key_id)
365            .unwrap();
366
367        // Sign and wrap security state
368        let user_id = UserId::new_v4();
369        let security_state = SecurityState::initialize_for_user(user_id);
370        let signed_security_state = security_state.sign(signing_key_id, &mut temp_ctx).unwrap();
371
372        // Wrap the private and signing keys with the user key
373        let wrapped_private = temp_ctx.wrap_private_key(user_key, private_key_id).unwrap();
374        let wrapped_signing = temp_ctx.wrap_signing_key(user_key, signing_key_id).unwrap();
375
376        let wrapped = WrappedAccountCryptographicState::V2 {
377            private_key: wrapped_private,
378            signed_public_key: Some(signed_public_key),
379            signing_key: wrapped_signing,
380            security_state: signed_security_state,
381        };
382        #[allow(deprecated)]
383        let user_key = temp_ctx
384            .dangerous_get_symmetric_key(user_key)
385            .unwrap()
386            .to_owned();
387        drop(temp_ctx);
388        drop(temp_store);
389
390        // Now attempt to set this wrapped state into a fresh store using the same user key
391        let store: KeyStore<KeyIds> = KeyStore::default();
392        let mut ctx = store.context_mut();
393        let user_key = ctx.add_local_symmetric_key(user_key);
394        let security_state = RwLock::new(None);
395
396        wrapped
397            .set_to_context(&security_state, user_key, &store, ctx)
398            .unwrap();
399
400        assert!(store.context().has_symmetric_key(SymmetricKeyId::User));
401        // Assert that the account keys and security state were set
402        assert!(
403            store
404                .context()
405                .has_asymmetric_key(AsymmetricKeyId::UserPrivateKey)
406        );
407        assert!(
408            store
409                .context()
410                .has_signing_key(SigningKeyId::UserSigningKey)
411        );
412        // Ensure security state was recorded
413        assert!(security_state.read().unwrap().is_some());
414    }
415
416    #[test]
417    fn test_to_private_keys_request_model_v2() {
418        let temp_store: KeyStore<KeyIds> = KeyStore::default();
419        let mut temp_ctx = temp_store.context_mut();
420        let user_id = UserId::new_v4();
421        let (user_key, wrapped_account_cryptography_state) =
422            WrappedAccountCryptographicState::make(&mut temp_ctx, user_id).unwrap();
423
424        wrapped_account_cryptography_state
425            .set_to_context(&RwLock::new(None), user_key, &temp_store, temp_ctx)
426            .unwrap();
427
428        let mut ctx = temp_store.context_mut();
429        let model = wrapped_account_cryptography_state
430            .to_request_model(&SymmetricKeyId::User, &mut ctx)
431            .expect("to_private_keys_request_model should succeed");
432        drop(ctx);
433
434        let ctx = temp_store.context();
435
436        let sig_pair = model
437            .signature_key_pair
438            .expect("signature_key_pair present");
439        assert_eq!(
440            sig_pair.verifying_key.unwrap(),
441            B64::from(
442                ctx.get_verifying_key(SigningKeyId::UserSigningKey)
443                    .unwrap()
444                    .to_cose()
445            )
446            .to_string()
447        );
448
449        let pk_pair = model.public_key_encryption_key_pair.unwrap();
450        assert_eq!(
451            pk_pair.public_key.unwrap(),
452            B64::from(
453                ctx.get_public_key(AsymmetricKeyId::UserPrivateKey)
454                    .unwrap()
455                    .to_der()
456                    .unwrap()
457            )
458            .to_string()
459        );
460
461        let signed_security_state = model
462            .security_state
463            .clone()
464            .expect("security_state present");
465        let security_state =
466            SignedSecurityState::from_str(signed_security_state.security_state.unwrap().as_str())
467                .unwrap()
468                .verify_and_unwrap(&ctx.get_verifying_key(SigningKeyId::UserSigningKey).unwrap())
469                .expect("security state should verify");
470        assert_eq!(
471            security_state.version(),
472            model.security_state.unwrap().security_version as u64
473        );
474    }
475
476    #[test]
477    fn test_set_to_context_v1_corrupt_private_key() {
478        // Test that a V1 account with a corrupt private key (valid EncString but invalid key data)
479        // can still initialize, but skips setting the private key
480        let temp_store: KeyStore<KeyIds> = KeyStore::default();
481        let mut temp_ctx = temp_store.context_mut();
482
483        let user_key = temp_ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
484        let corrupt_private_key = "not a private key"
485            .encrypt(&mut temp_ctx, user_key)
486            .unwrap();
487
488        // Construct the V1 wrapped state with corrupt private key
489        let wrapped = WrappedAccountCryptographicState::V1 {
490            private_key: corrupt_private_key,
491        };
492
493        #[expect(deprecated)]
494        let user_key_material = temp_ctx
495            .dangerous_get_symmetric_key(user_key)
496            .unwrap()
497            .to_owned();
498        drop(temp_ctx);
499        drop(temp_store);
500
501        // Now attempt to set this wrapped state into a fresh store
502        let store: KeyStore<KeyIds> = KeyStore::default();
503        let mut ctx = store.context_mut();
504        let user_key = ctx.add_local_symmetric_key(user_key_material);
505        let security_state = RwLock::new(None);
506
507        wrapped
508            .set_to_context(&security_state, user_key, &store, ctx)
509            .unwrap();
510
511        let ctx = store.context();
512
513        // The user symmetric key should be set
514        assert!(ctx.has_symmetric_key(SymmetricKeyId::User));
515        // But the private key should NOT be set (due to corruption)
516        assert!(!ctx.has_asymmetric_key(AsymmetricKeyId::UserPrivateKey));
517    }
518}