Skip to main content

bitwarden_core/key_management/
local_user_data_key.rs

1use bitwarden_crypto::{EncString, KeyStoreContext};
2use key_management::LocalUserDataKeyState;
3use thiserror::Error;
4
5use crate::{
6    key_management,
7    key_management::{KeySlotIds, SymmetricKeySlotId},
8};
9
10/// An indirect symmetric key for encrypting local user data (e.g. password generator history).
11/// Enables offline decryption of local data after a key rotation: only the wrapped key is
12/// re-encrypted; the local user data key itself stays intact.
13#[derive(Debug, Clone)]
14pub(crate) struct WrappedLocalUserDataKey(pub(crate) EncString);
15
16impl WrappedLocalUserDataKey {
17    /// Create a user key, wrapped by the user key.
18    #[bitwarden_logging::instrument(err)]
19    pub(crate) fn from_context_user_key(
20        ctx: &mut KeyStoreContext<KeySlotIds>,
21    ) -> Result<Self, LocalUserDataKeyError> {
22        let wrapped_local_user_data_key = ctx
23            .wrap_symmetric_key(SymmetricKeySlotId::User, SymmetricKeySlotId::User)
24            .map_err(|_| LocalUserDataKeyError::EncryptionFailed)?;
25        Ok(WrappedLocalUserDataKey(wrapped_local_user_data_key))
26    }
27
28    /// Re-wrap an existing wrapped local user data key, preserving the inner key plaintext but
29    /// changing the wrapping key from `old_wrapping_key_id` to the current
30    /// [`SymmetricKeySlotId::User`].
31    ///
32    /// Used during V1→V2 user-key upgrades: the wrapped key was previously sealed with the V1
33    /// user key and must be re-sealed with the V2 user key so that local data encrypted under
34    /// the local user data key remains decryptable after rotation.
35    #[bitwarden_logging::instrument(err, fields(old_wrapping_key_id = ?old_wrapping_key_id))]
36    pub(crate) fn rewrap_with_user_key(
37        &self,
38        old_wrapping_key_id: SymmetricKeySlotId,
39        ctx: &mut KeyStoreContext<KeySlotIds>,
40    ) -> Result<Self, LocalUserDataKeyError> {
41        let local_id = ctx
42            .unwrap_symmetric_key(old_wrapping_key_id, &self.0)
43            .map_err(|_| LocalUserDataKeyError::DecryptionFailed)?;
44        let new_wrapped = ctx
45            .wrap_symmetric_key(SymmetricKeySlotId::User, local_id)
46            .map_err(|_| LocalUserDataKeyError::EncryptionFailed)?;
47        Ok(WrappedLocalUserDataKey(new_wrapped))
48    }
49
50    /// Unwrap the local user data key and set it in the context under the
51    /// [`SymmetricKeySlotId::LocalUserData`] key id.
52    #[bitwarden_logging::instrument(err)]
53    pub(crate) fn unwrap_to_context(
54        &self,
55        ctx: &mut KeyStoreContext<KeySlotIds>,
56    ) -> Result<(), LocalUserDataKeyError> {
57        let local_id = ctx
58            .unwrap_symmetric_key(SymmetricKeySlotId::User, &self.0)
59            .map_err(|_| LocalUserDataKeyError::DecryptionFailed)?;
60        ctx.persist_symmetric_key(local_id, SymmetricKeySlotId::LocalUserData)
61            .map_err(|_| LocalUserDataKeyError::DecryptionFailed)?;
62        Ok(())
63    }
64}
65
66/// Errors that can occur when working with [`WrappedLocalUserDataKey`].
67#[derive(Debug, Error)]
68pub enum LocalUserDataKeyError {
69    /// Decryption of a wrapped key failed
70    #[error("Decryption failed")]
71    DecryptionFailed,
72    /// Failed to encrypt a key
73    #[error("Encryption failed")]
74    EncryptionFailed,
75}
76
77impl From<WrappedLocalUserDataKey> for LocalUserDataKeyState {
78    fn from(wrapped_key: WrappedLocalUserDataKey) -> Self {
79        Self {
80            wrapped_key: wrapped_key.0,
81        }
82    }
83}
84
85impl From<&LocalUserDataKeyState> for WrappedLocalUserDataKey {
86    fn from(state: &LocalUserDataKeyState) -> Self {
87        Self(state.wrapped_key.clone())
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use bitwarden_crypto::{Decryptable, KeyStore, PrimitiveEncryptable};
94
95    use super::*;
96    use crate::key_management::{KeySlotIds, SymmetricKeySlotId};
97
98    fn make_key_store_with_user_key() -> KeyStore<KeySlotIds> {
99        let key_store = KeyStore::<KeySlotIds>::default();
100        let mut ctx = key_store.context_mut();
101        let user_key = ctx.generate_symmetric_key();
102        ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User)
103            .expect("persisting user key should succeed");
104        drop(ctx);
105        key_store
106    }
107
108    #[test]
109    fn test_from_context_user_key_wraps_user_key() {
110        let key_store = make_key_store_with_user_key();
111        let mut ctx = key_store.context_mut();
112
113        let plaintext = "test data";
114        let ciphertext = plaintext
115            .encrypt(&mut ctx, SymmetricKeySlotId::User)
116            .expect("encryption with user key should succeed");
117
118        let wrapped = WrappedLocalUserDataKey::from_context_user_key(&mut ctx)
119            .expect("wrapping should succeed");
120        wrapped
121            .unwrap_to_context(&mut ctx)
122            .expect("unwrapping should succeed");
123
124        // Verify LocalUserData key is the same as User key: data encrypted with User
125        // must be decryptable with LocalUserData.
126        let decrypted: String = ciphertext
127            .decrypt(&mut ctx, SymmetricKeySlotId::LocalUserData)
128            .expect("decryption with local user data key should succeed");
129        assert_eq!(decrypted, plaintext);
130    }
131
132    #[test]
133    fn test_rewrap_with_user_key_preserves_inner_plaintext() {
134        use bitwarden_crypto::SymmetricKeyAlgorithm;
135
136        let key_store = KeyStore::<KeySlotIds>::default();
137        let mut ctx = key_store.context_mut();
138
139        // Create an initial wrapped local user data key using a V1 user key.
140        let v1_local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
141        ctx.persist_symmetric_key(v1_local_key_id, SymmetricKeySlotId::User)
142            .expect("persisting old user key should succeed");
143
144        let wrapped_v1_local_key = WrappedLocalUserDataKey::from_context_user_key(&mut ctx)
145            .expect("initial wrap should succeed");
146
147        wrapped_v1_local_key
148            .unwrap_to_context(&mut ctx)
149            .expect("unwrap with old user key should succeed");
150        let plaintext = "rewrap round-trip data";
151        let ciphertext = plaintext
152            .encrypt(&mut ctx, SymmetricKeySlotId::LocalUserData)
153            .expect("encryption with LocalUserData slot should succeed");
154
155        // Save v1 to a fresh local id before overwriting the User slot with v2.
156        // Mirrors the production path where the V1→V2 upgrade token re-materializes
157        // v1 into a local slot at rewrap time.
158        let v1_old_wrapping_id = ctx
159            .unwrap_symmetric_key(SymmetricKeySlotId::User, &wrapped_v1_local_key.0)
160            .expect("unwrap with v1 user key should succeed");
161
162        // Rewrap
163        let new_local = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
164        ctx.persist_symmetric_key(new_local, SymmetricKeySlotId::User)
165            .expect("persisting new user key should succeed");
166        let wrapped_new = wrapped_v1_local_key
167            .rewrap_with_user_key(v1_old_wrapping_id, &mut ctx)
168            .expect("rewrap should succeed");
169
170        // Validate that the new wrapped version can still decrypt the data
171        wrapped_new
172            .unwrap_to_context(&mut ctx)
173            .expect("unwrap with new user key should succeed");
174
175        let decrypted: String = ciphertext
176            .decrypt(&mut ctx, SymmetricKeySlotId::LocalUserData)
177            .expect("decryption after rewrap should succeed");
178        assert_eq!(decrypted, plaintext);
179    }
180
181    #[test]
182    fn test_unwrap_to_context_fails_with_wrong_key() {
183        let key_store_a = make_key_store_with_user_key();
184        let wrapped = {
185            let mut ctx = key_store_a.context_mut();
186            WrappedLocalUserDataKey::from_context_user_key(&mut ctx)
187                .expect("wrapping should succeed")
188        };
189
190        let key_store_b = make_key_store_with_user_key();
191        let mut ctx_b = key_store_b.context_mut();
192        assert!(
193            wrapped.unwrap_to_context(&mut ctx_b).is_err(),
194            "unwrapping with a different key should fail"
195        );
196    }
197}