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