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