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 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
56pub(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 contained_key_id: None,
272 };
273 let decrypted_user_key = master_password_unlock_data
274 .unwrap_to_context(&mock_password, &mut ctx)
275 .expect("unwrap should succeed");
276 assert_symmetric_keys_equal(user_key_id, decrypted_user_key, &mut ctx);
277 }
278
279 #[test]
280 fn test_reencrypt_unlock_method_data_password_argon2id() {
281 let mock_password = "test_password".to_string();
282 let store: KeyStore<KeySlotIds> = KeyStore::default();
283 let mut ctx = store.context_mut();
284 let user_key_id = ctx.generate_symmetric_key();
285
286 let input = PrimaryUnlockMethod::Password {
287 password: mock_password.clone(),
288 kdf: create_test_kdf_argon2id(),
289 salt: "[email protected]".to_string(),
290 };
291
292 let result = reencrypt_unlock_method_data(input, user_key_id, &mut ctx);
293 assert!(result.is_ok());
294
295 let model = result.expect("should be ok");
296 assert_eq!(model.unlock_method, UnlockMethod::MasterPassword);
297 assert!(model.master_password_unlock_data.is_some());
298 assert!(model.key_connector_key_wrapped_user_key.is_none());
299
300 let master_password_unlock_data_model = model
301 .master_password_unlock_data
302 .expect("should be present");
303 let master_password_unlock_data = MasterPasswordUnlockData {
304 master_key_wrapped_user_key: master_password_unlock_data_model
305 .master_key_wrapped_user_key
306 .parse()
307 .expect("should parse"),
308 kdf: create_test_kdf_argon2id(),
309 salt: "[email protected]".to_string(),
310 contained_key_id: None,
311 };
312 let decrypted_user_key = master_password_unlock_data
313 .unwrap_to_context(&mock_password, &mut ctx)
314 .expect("unwrap should succeed");
315 assert_symmetric_keys_equal(user_key_id, decrypted_user_key, &mut ctx);
316 }
317
318 #[test]
319 fn test_reencrypt_unlock_method_data_key_connector() {
320 let key_connector_key = KeyConnectorKey::make();
321 let store: KeyStore<KeySlotIds> = KeyStore::default();
322 let mut ctx = store.context_mut();
323 let user_key_id = ctx.generate_symmetric_key();
324
325 let input = PrimaryUnlockMethod::KeyConnector {
326 key_connector_key: key_connector_key.clone(),
327 };
328
329 let result = reencrypt_unlock_method_data(input, user_key_id, &mut ctx);
330
331 let model = result.expect("should be ok");
332 assert_eq!(model.unlock_method, UnlockMethod::KeyConnector);
333 assert!(model.master_password_unlock_data.is_none());
334 let wrapped_user_key_str = model
335 .key_connector_key_wrapped_user_key
336 .expect("should be present");
337 let wrapped_user_key: bitwarden_crypto::EncString =
338 wrapped_user_key_str.parse().expect("should parse");
339
340 let unwrapped_user_key_id = key_connector_key
341 .unwrap_user_key(wrapped_user_key, &mut ctx)
342 .expect("unwrap should succeed");
343 assert_symmetric_keys_equal(user_key_id, unwrapped_user_key_id, &mut ctx);
344 }
345
346 #[test]
347 fn test_reencrypt_unlock_method_data_tde() {
348 let store: KeyStore<KeySlotIds> = KeyStore::default();
349 let mut ctx = store.context_mut();
350 let user_key_id = ctx.generate_symmetric_key();
351
352 let result = reencrypt_unlock_method_data(PrimaryUnlockMethod::Tde, user_key_id, &mut ctx);
353
354 let model = result.expect("should be ok");
355 assert_eq!(model.unlock_method, UnlockMethod::Tde);
356 assert!(model.master_password_unlock_data.is_none());
357 assert!(model.key_connector_key_wrapped_user_key.is_none());
358 }
359}