bitwarden_user_crypto_management/key_rotation/
unlock_method.rs1use 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
12pub(super) enum PrimaryUnlockMethod {
15 Password {
17 password: String,
18 kdf: Kdf,
19 salt: String,
20 },
21 KeyConnector { key_connector_key: KeyConnectorKey },
23 }
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
54pub(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}