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 };
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}