Skip to main content

bitwarden_user_crypto_management/key_rotation/
unlock_method.rs

1//! Functionality for re-encrypting unlock method data during user key rotation.
2
3use bitwarden_api_api::models::{self, UnlockMethodRequestModel};
4use bitwarden_core::key_management::{KeySlotIds, MasterPasswordUnlockData, SymmetricKeySlotId};
5use bitwarden_crypto::{Kdf, KeyConnectorKey, KeyStoreContext};
6
7use crate::key_rotation::{
8    RotateUserKeysError, rotate_user_keys::KeyRotationMethod, sync::SyncedAccountData,
9    unlock::ReencryptError,
10};
11
12/// The primary unlock method for the account and the data needed to re-encrypt it under the new
13/// user key.
14pub(super) enum PrimaryUnlockMethod {
15    /// The master password based unlock method.
16    Password {
17        password: String,
18        kdf: Kdf,
19        salt: String,
20    },
21    /// The Key Connector based unlock method.
22    KeyConnector { key_connector_key: KeyConnectorKey },
23    /// TDE based unlock method. TDE keys themselves are rotated as part of the
24    /// common_unlock_data.
25    Tde,
26}
27
28impl PrimaryUnlockMethod {
29    pub(super) fn from_key_rotation_method(
30        method: KeyRotationMethod,
31        synced_account_data: &SyncedAccountData,
32        key_connector_key: Option<KeyConnectorKey>,
33    ) -> Result<Self, RotateUserKeysError> {
34        match method {
35            KeyRotationMethod::Password { password } => {
36                let (kdf, salt) = synced_account_data
37                    .kdf_and_salt
38                    .clone()
39                    .ok_or(RotateUserKeysError::Api)?;
40                Ok(PrimaryUnlockMethod::Password {
41                    password,
42                    kdf,
43                    salt,
44                })
45            }
46            KeyRotationMethod::KeyConnector { .. } => {
47                let key_connector_key =
48                    key_connector_key.ok_or(RotateUserKeysError::KeyConnectorApi)?;
49                Ok(PrimaryUnlockMethod::KeyConnector { key_connector_key })
50            }
51            KeyRotationMethod::Tde => Ok(PrimaryUnlockMethod::Tde),
52        }
53    }
54}
55
56/// Re-encrypt the unlock method data for the given input and new user key id.
57pub(super) fn reencrypt_unlock_method_data(
58    input: PrimaryUnlockMethod,
59    new_user_key_id: SymmetricKeySlotId,
60    ctx: &mut KeyStoreContext<KeySlotIds>,
61) -> Result<UnlockMethodRequestModel, ReencryptError> {
62    match input {
63        PrimaryUnlockMethod::Password {
64            password,
65            kdf,
66            salt,
67        } => {
68            let master_password_unlock_data =
69                MasterPasswordUnlockData::derive(&password, &kdf, &salt, new_user_key_id, ctx)
70                    .map_err(|_| ReencryptError::MasterPasswordDerivation)?;
71
72            Ok(UnlockMethodRequestModel {
73                unlock_method: models::UnlockMethod::MasterPassword,
74                master_password_unlock_data: Some(Box::new((&master_password_unlock_data).into())),
75                key_connector_key_wrapped_user_key: None,
76            })
77        }
78        PrimaryUnlockMethod::KeyConnector { key_connector_key } => {
79            let wrapped_user_key = key_connector_key
80                .wrap_user_key(new_user_key_id, ctx)
81                .map_err(|_| ReencryptError::KeyConnectorWrapping)?;
82
83            Ok(UnlockMethodRequestModel {
84                unlock_method: models::UnlockMethod::KeyConnector,
85                master_password_unlock_data: None,
86                key_connector_key_wrapped_user_key: Some(wrapped_user_key.to_string()),
87            })
88        }
89        PrimaryUnlockMethod::Tde => Ok(UnlockMethodRequestModel {
90            unlock_method: models::UnlockMethod::Tde,
91            master_password_unlock_data: None,
92            key_connector_key_wrapped_user_key: None,
93        }),
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use std::num::NonZeroU32;
100
101    use bitwarden_api_api::models::UnlockMethod;
102    use bitwarden_core::key_management::{
103        KeySlotIds, MasterPasswordUnlockData,
104        account_cryptographic_state::WrappedAccountCryptographicState,
105    };
106    use bitwarden_crypto::{Kdf, KeyConnectorKey, KeyStore, KeyStoreContext};
107
108    use super::*;
109    use crate::key_rotation::{rotate_user_keys::KeyRotationMethod, sync::SyncedAccountData};
110
111    fn make_synced_account_data(kdf_and_salt: Option<(Kdf, String)>) -> SyncedAccountData {
112        let store: KeyStore<KeySlotIds> = KeyStore::default();
113        let mut ctx = store.context_mut();
114        let (_, wrapped_account_cryptographic_state) =
115            WrappedAccountCryptographicState::make(&mut ctx)
116                .expect("make wrapped account cryptographic state should succeed");
117        SyncedAccountData {
118            wrapped_account_cryptographic_state,
119            folders: vec![],
120            ciphers: vec![],
121            sends: vec![],
122            emergency_access_memberships: vec![],
123            organization_memberships: vec![],
124            trusted_devices: vec![],
125            passkeys: vec![],
126            kdf_and_salt,
127        }
128    }
129
130    fn create_test_kdf_pbkdf2() -> Kdf {
131        Kdf::PBKDF2 {
132            iterations: NonZeroU32::new(600000).expect("valid iterations"),
133        }
134    }
135
136    fn create_test_kdf_argon2id() -> Kdf {
137        Kdf::Argon2id {
138            iterations: NonZeroU32::new(3).expect("valid iterations"),
139            memory: NonZeroU32::new(64).expect("valid memory"),
140            parallelism: NonZeroU32::new(4).expect("valid parallelism"),
141        }
142    }
143
144    fn assert_symmetric_keys_equal(
145        key_id_1: SymmetricKeySlotId,
146        key_id_2: SymmetricKeySlotId,
147        ctx: &mut KeyStoreContext<KeySlotIds>,
148    ) {
149        #[allow(deprecated)]
150        let key_1 = ctx
151            .dangerous_get_symmetric_key(key_id_1)
152            .expect("key 1 should exist");
153        #[allow(deprecated)]
154        let key_2 = ctx
155            .dangerous_get_symmetric_key(key_id_2)
156            .expect("key 2 should exist");
157        assert_eq!(key_1, key_2, "symmetric keys should be equal");
158    }
159
160    #[test]
161    fn test_from_key_rotation_method_password_returns_input() {
162        let kdf = create_test_kdf_pbkdf2();
163        let salt = "[email protected]".to_string();
164        let synced_data = make_synced_account_data(Some((kdf.clone(), salt.clone())));
165
166        let result = PrimaryUnlockMethod::from_key_rotation_method(
167            KeyRotationMethod::Password {
168                password: "pass".to_string(),
169            },
170            &synced_data,
171            None,
172        );
173
174        let input = result.expect("should succeed");
175        let PrimaryUnlockMethod::Password {
176            password,
177            kdf: result_kdf,
178            salt: result_salt,
179        } = input
180        else {
181            panic!("expected Password variant");
182        };
183        assert_eq!(password, "pass");
184        assert_eq!(result_kdf, kdf);
185        assert_eq!(result_salt, salt);
186    }
187
188    #[test]
189    fn test_from_key_rotation_method_password_no_kdf_returns_error() {
190        let synced_data = make_synced_account_data(None);
191
192        let result = PrimaryUnlockMethod::from_key_rotation_method(
193            KeyRotationMethod::Password {
194                password: "pass".to_string(),
195            },
196            &synced_data,
197            None,
198        );
199
200        assert!(matches!(result, Err(RotateUserKeysError::Api)));
201    }
202
203    #[test]
204    fn test_from_key_rotation_method_key_connector_returns_input() {
205        let synced_data = make_synced_account_data(None);
206        let key = KeyConnectorKey::make();
207        let expected_b64: bitwarden_encoding::B64 = key.clone().into();
208
209        let result = PrimaryUnlockMethod::from_key_rotation_method(
210            KeyRotationMethod::KeyConnector {
211                key_connector_url: "https://kc.example.com".to_string(),
212            },
213            &synced_data,
214            Some(key),
215        );
216
217        let PrimaryUnlockMethod::KeyConnector { key_connector_key } =
218            result.expect("should succeed")
219        else {
220            panic!("expected KeyConnector variant");
221        };
222        let actual_b64: bitwarden_encoding::B64 = key_connector_key.into();
223        assert_eq!(actual_b64.to_string(), expected_b64.to_string());
224    }
225
226    #[test]
227    fn test_from_key_rotation_method_key_connector_no_key_returns_error() {
228        let synced_data = make_synced_account_data(None);
229
230        let result = PrimaryUnlockMethod::from_key_rotation_method(
231            KeyRotationMethod::KeyConnector {
232                key_connector_url: "https://kc.example.com".to_string(),
233            },
234            &synced_data,
235            None,
236        );
237
238        assert!(matches!(result, Err(RotateUserKeysError::KeyConnectorApi)));
239    }
240
241    #[test]
242    fn test_reencrypt_unlock_method_data_password_pbkdf2() {
243        let mock_password = "test_password".to_string();
244        let store: KeyStore<KeySlotIds> = KeyStore::default();
245        let mut ctx = store.context_mut();
246        let user_key_id = ctx.generate_symmetric_key();
247
248        let input = PrimaryUnlockMethod::Password {
249            password: mock_password.clone(),
250            kdf: create_test_kdf_pbkdf2(),
251            salt: "[email protected]".to_string(),
252        };
253
254        let result = reencrypt_unlock_method_data(input, user_key_id, &mut ctx);
255
256        let model = result.expect("should be ok");
257        assert_eq!(model.unlock_method, UnlockMethod::MasterPassword);
258        assert!(model.master_password_unlock_data.is_some());
259        assert!(model.key_connector_key_wrapped_user_key.is_none());
260
261        let master_password_unlock_data_model = model
262            .master_password_unlock_data
263            .expect("should be present");
264        let master_password_unlock_data = MasterPasswordUnlockData {
265            master_key_wrapped_user_key: master_password_unlock_data_model
266                .master_key_wrapped_user_key
267                .parse()
268                .expect("should parse"),
269            kdf: create_test_kdf_pbkdf2(),
270            salt: "[email protected]".to_string(),
271        };
272        let decrypted_user_key = master_password_unlock_data
273            .unwrap_to_context(&mock_password, &mut ctx)
274            .expect("unwrap should succeed");
275        assert_symmetric_keys_equal(user_key_id, decrypted_user_key, &mut ctx);
276    }
277
278    #[test]
279    fn test_reencrypt_unlock_method_data_password_argon2id() {
280        let mock_password = "test_password".to_string();
281        let store: KeyStore<KeySlotIds> = KeyStore::default();
282        let mut ctx = store.context_mut();
283        let user_key_id = ctx.generate_symmetric_key();
284
285        let input = PrimaryUnlockMethod::Password {
286            password: mock_password.clone(),
287            kdf: create_test_kdf_argon2id(),
288            salt: "[email protected]".to_string(),
289        };
290
291        let result = reencrypt_unlock_method_data(input, user_key_id, &mut ctx);
292        assert!(result.is_ok());
293
294        let model = result.expect("should be ok");
295        assert_eq!(model.unlock_method, UnlockMethod::MasterPassword);
296        assert!(model.master_password_unlock_data.is_some());
297        assert!(model.key_connector_key_wrapped_user_key.is_none());
298
299        let master_password_unlock_data_model = model
300            .master_password_unlock_data
301            .expect("should be present");
302        let master_password_unlock_data = MasterPasswordUnlockData {
303            master_key_wrapped_user_key: master_password_unlock_data_model
304                .master_key_wrapped_user_key
305                .parse()
306                .expect("should parse"),
307            kdf: create_test_kdf_argon2id(),
308            salt: "[email protected]".to_string(),
309        };
310        let decrypted_user_key = master_password_unlock_data
311            .unwrap_to_context(&mock_password, &mut ctx)
312            .expect("unwrap should succeed");
313        assert_symmetric_keys_equal(user_key_id, decrypted_user_key, &mut ctx);
314    }
315
316    #[test]
317    fn test_reencrypt_unlock_method_data_key_connector() {
318        let key_connector_key = KeyConnectorKey::make();
319        let store: KeyStore<KeySlotIds> = KeyStore::default();
320        let mut ctx = store.context_mut();
321        let user_key_id = ctx.generate_symmetric_key();
322
323        let input = PrimaryUnlockMethod::KeyConnector {
324            key_connector_key: key_connector_key.clone(),
325        };
326
327        let result = reencrypt_unlock_method_data(input, user_key_id, &mut ctx);
328
329        let model = result.expect("should be ok");
330        assert_eq!(model.unlock_method, UnlockMethod::KeyConnector);
331        assert!(model.master_password_unlock_data.is_none());
332        let wrapped_user_key_str = model
333            .key_connector_key_wrapped_user_key
334            .expect("should be present");
335        let wrapped_user_key: bitwarden_crypto::EncString =
336            wrapped_user_key_str.parse().expect("should parse");
337
338        let unwrapped_user_key_id = key_connector_key
339            .unwrap_user_key(wrapped_user_key, &mut ctx)
340            .expect("unwrap should succeed");
341        assert_symmetric_keys_equal(user_key_id, unwrapped_user_key_id, &mut ctx);
342    }
343
344    #[test]
345    fn test_reencrypt_unlock_method_data_tde() {
346        let store: KeyStore<KeySlotIds> = KeyStore::default();
347        let mut ctx = store.context_mut();
348        let user_key_id = ctx.generate_symmetric_key();
349
350        let result = reencrypt_unlock_method_data(PrimaryUnlockMethod::Tde, user_key_id, &mut ctx);
351
352        let model = result.expect("should be ok");
353        assert_eq!(model.unlock_method, UnlockMethod::Tde);
354        assert!(model.master_password_unlock_data.is_none());
355        assert!(model.key_connector_key_wrapped_user_key.is_none());
356    }
357}