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