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