1use bitwarden_core::Client;
4#[cfg(feature = "cli")]
5use bitwarden_core::client::persisted_state::{
6 ACCOUNT_CRYPTO_STATE, OrganizationSharedKey, SESSION_PROTECTED_USER_KEY,
7};
8
9use crate::SessionKey;
10
11pub enum UnlockMethod {
16 SessionKey(SessionKey),
19}
20
21#[derive(Debug, thiserror::Error)]
28pub enum UnlockError {
29 #[error("An unknown error occurred while unlocking the client")]
31 Unknown,
32}
33
34#[derive(Clone)]
36pub struct UnlockClient {
37 #[cfg_attr(not(feature = "cli"), allow(dead_code))]
38 pub(crate) client: Client,
39}
40
41impl UnlockClient {
42 pub(crate) fn new(client: Client) -> Self {
43 Self { client }
44 }
45
46 #[cfg(feature = "cli")]
53 pub async fn generate_session_key(&self) -> Result<SessionKey, UnlockError> {
54 use bitwarden_core::key_management::SymmetricKeySlotId;
55
56 let (envelope, session_key) = {
57 let key_store = self.client.internal.get_key_store();
58 let mut ctx = key_store.context_mut();
59 SessionKey::from_context(SymmetricKeySlotId::User, &mut ctx).map_err(|e| {
60 tracing::error!("Failed to encrypt user key with session key: {e}");
61 UnlockError::Unknown
62 })?
63 };
64
65 self.client
66 .platform()
67 .state()
68 .setting(SESSION_PROTECTED_USER_KEY)
69 .map_err(|e| {
70 tracing::error!("Failed to read session_protected_user_key setting handle: {e}");
71 UnlockError::Unknown
72 })?
73 .update(envelope)
74 .await
75 .map_err(|e| {
76 tracing::error!("Failed to save session_protected_user_key: {e}");
77 UnlockError::Unknown
78 })?;
79
80 Ok(session_key)
81 }
82
83 #[cfg(feature = "cli")]
89 pub async fn unlock(&self, unlock: UnlockMethod) -> Result<(), UnlockError> {
90 let UnlockMethod::SessionKey(session_key) = unlock;
91
92 let state = self.client.platform().state();
93
94 let session_protected_user_key = state
95 .setting(SESSION_PROTECTED_USER_KEY)
96 .map_err(|e| {
97 tracing::error!("Failed to read session_protected_user_key setting handle: {e}");
98 UnlockError::Unknown
99 })?
100 .get()
101 .await
102 .map_err(|e| {
103 tracing::error!("Failed to read session_protected_user_key: {e}");
104 UnlockError::Unknown
105 })?
106 .ok_or_else(|| {
107 tracing::error!("Missing session_protected_user_key in database");
108 UnlockError::Unknown
109 })?;
110
111 let account_crypto_state = state
112 .setting(ACCOUNT_CRYPTO_STATE)
113 .map_err(|e| {
114 tracing::error!("Failed to read account_crypto_state setting handle: {e}");
115 UnlockError::Unknown
116 })?
117 .get()
118 .await
119 .map_err(|e| {
120 tracing::error!("Failed to read account_crypto_state: {e}");
121 UnlockError::Unknown
122 })?
123 .ok_or_else(|| {
124 tracing::error!("Missing account_crypto_state in database");
125 UnlockError::Unknown
126 })?;
127
128 let decrypted_key = {
129 let key_store = self.client.internal.get_key_store();
130 let mut ctx = key_store.context_mut();
131 let decrypted_key_id = session_key
132 .unwrap_to_context(&session_protected_user_key, &mut ctx)
133 .map_err(|e| {
134 tracing::error!("Failed to unseal user key with session key: {e}");
135 UnlockError::Unknown
136 })?;
137 #[allow(deprecated)]
138 ctx.dangerous_get_symmetric_key(decrypted_key_id)
139 .map_err(|e| {
140 tracing::error!("Failed to read decrypted user key from key store: {e}");
141 UnlockError::Unknown
142 })?
143 .clone()
144 };
145
146 self.client
147 .internal
148 .initialize_user_crypto_decrypted_key(decrypted_key, account_crypto_state, &None)
149 .map_err(|e| {
150 tracing::error!("Failed to initialize user crypto with decrypted key: {e}");
151 UnlockError::Unknown
152 })?;
153
154 let org_keys = state
155 .get::<OrganizationSharedKey>()
156 .map_err(|e| {
157 tracing::error!("Failed to read organization keys repository: {e}");
158 UnlockError::Unknown
159 })?
160 .list()
161 .await
162 .map_err(|e| {
163 tracing::error!("Failed to list organization keys: {e}");
164 UnlockError::Unknown
165 })?;
166
167 self.client
168 .internal
169 .initialize_org_crypto(org_keys.into_iter().map(|k| (k.org_id, k.key)).collect())
170 .map_err(|e| {
171 tracing::error!("Failed to decrypt organization keys: {e}");
172 UnlockError::Unknown
173 })?;
174
175 Ok(())
176 }
177
178 #[cfg(feature = "cli")]
185 pub async fn invalidate_session_key(&self) -> Result<(), bitwarden_state::SettingsError> {
186 self.client
187 .platform()
188 .state()
189 .setting(SESSION_PROTECTED_USER_KEY)?
190 .delete()
191 .await
192 }
193}
194
195pub trait UnlockClientExt {
197 fn unlock(&self) -> UnlockClient;
199}
200
201impl UnlockClientExt for Client {
202 fn unlock(&self) -> UnlockClient {
203 UnlockClient::new(self.clone())
204 }
205}
206
207#[cfg(all(test, feature = "cli"))]
208mod tests {
209 #![allow(clippy::unwrap_used)]
212
213 use std::sync::{Arc, Once};
214
215 use bitwarden_core::{
216 Client, DeviceType, HostPlatformInfo, SaveStateData, UserId,
217 auth::auth_tokens::{NoopTokenHandler, TokenHandler},
218 client::persisted_state::{BASE_URLS, BaseUrls, SESSION_PROTECTED_USER_KEY, USER_ID},
219 key_management::{
220 KeySlotIds, SecurityState, SymmetricKeySlotId,
221 account_cryptographic_state::WrappedAccountCryptographicState,
222 },
223 };
224 use bitwarden_crypto::{
225 KeyStore, PublicKeyEncryptionAlgorithm, SignatureAlgorithm, SymmetricCryptoKey,
226 SymmetricKeyAlgorithm,
227 safe::{SymmetricKeyEnvelope, SymmetricKeyEnvelopeNamespace},
228 };
229 use bitwarden_state::registry::StateRegistry;
230
231 use super::*;
232
233 static INIT: Once = Once::new();
234
235 fn ensure_platform_info() {
236 INIT.call_once(|| {
237 bitwarden_core::init_host_platform_info(HostPlatformInfo {
238 user_agent: "unlock-tests".to_string(),
239 device_type: DeviceType::SDK,
240 device_identifier: None,
241 bitwarden_client_version: None,
242 bitwarden_package_type: None,
243 });
244 });
245 }
246
247 fn test_user_id() -> UserId {
248 "d5b1fde2-a1e3-4c5b-9e0f-1a2b3c4d5e6f".parse().unwrap()
249 }
250
251 fn test_email() -> String {
252 "[email protected]".to_string()
253 }
254
255 fn test_base_urls() -> BaseUrls {
256 BaseUrls {
257 identity_url: "https://identity.example.com".to_string(),
258 api_url: "https://api.example.com".to_string(),
259 }
260 }
261
262 fn is_unlocked(client: &Client) -> bool {
263 client
264 .internal
265 .get_key_store()
266 .context()
267 .has_symmetric_key(SymmetricKeySlotId::User)
268 }
269
270 fn user_key_base64(client: &Client) -> String {
271 let key_store = client.internal.get_key_store();
272 let ctx = key_store.context();
273 #[allow(deprecated)]
274 ctx.dangerous_get_symmetric_key(SymmetricKeySlotId::User)
275 .unwrap()
276 .to_base64()
277 .to_string()
278 }
279
280 fn make_test_user_crypto() -> (SymmetricCryptoKey, WrappedAccountCryptographicState) {
283 let store: KeyStore<KeySlotIds> = KeyStore::default();
284 let mut ctx = store.context_mut();
285 let user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
286 let private_key_id = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
287 let signing_key_id = ctx.make_signing_key(SignatureAlgorithm::Ed25519);
288 let signed_public_key = ctx
289 .make_signed_public_key(private_key_id, signing_key_id)
290 .unwrap();
291 let security_state = SecurityState::new();
292 let signed_security_state = security_state.sign(signing_key_id, &mut ctx).unwrap();
293 let wrapped_private = ctx.wrap_private_key(user_key_id, private_key_id).unwrap();
294 let wrapped_signing = ctx.wrap_signing_key(user_key_id, signing_key_id).unwrap();
295 #[allow(deprecated)]
296 let user_key = ctx
297 .dangerous_get_symmetric_key(user_key_id)
298 .unwrap()
299 .clone();
300 (
301 user_key,
302 WrappedAccountCryptographicState::V2 {
303 private_key: wrapped_private,
304 signed_public_key: Some(signed_public_key),
305 signing_key: wrapped_signing,
306 security_state: signed_security_state,
307 },
308 )
309 }
310
311 fn seal_with_new_session_key(
314 user_key: &SymmetricCryptoKey,
315 ) -> (SymmetricKeyEnvelope, SessionKey) {
316 let store: KeyStore<KeySlotIds> = KeyStore::default();
317 let mut ctx = store.context_mut();
318 let user_key_id = ctx.add_local_symmetric_key(user_key.clone());
319 let session_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
320 let envelope = SymmetricKeyEnvelope::seal(
321 user_key_id,
322 session_key_id,
323 SymmetricKeyEnvelopeNamespace::SessionKey,
324 &ctx,
325 )
326 .unwrap();
327 #[allow(deprecated)]
328 let session_key = ctx
329 .dangerous_get_symmetric_key(session_key_id)
330 .unwrap()
331 .clone();
332 (envelope, SessionKey(session_key))
333 }
334
335 async fn populate_registry_for_unlock(
336 envelope: SymmetricKeyEnvelope,
337 crypto_state: WrappedAccountCryptographicState,
338 ) -> StateRegistry {
339 let reg = StateRegistry::new_with_memory_db();
340 reg.setting(SESSION_PROTECTED_USER_KEY)
341 .unwrap()
342 .update(envelope)
343 .await
344 .unwrap();
345 Client::save_to_state(
346 SaveStateData {
347 user_id: test_user_id(),
348 email: test_email(),
349 urls: test_base_urls(),
350 crypto_state,
351 },
352 ®,
353 )
354 .await
355 .unwrap();
356 reg
357 }
358
359 #[tokio::test]
360 async fn generate_session_key_persists_envelope() {
361 ensure_platform_info();
362 let (user_key, crypto_state) = make_test_user_crypto();
363
364 let reg = StateRegistry::new_with_memory_db();
365 Client::save_to_state(
366 SaveStateData {
367 user_id: test_user_id(),
368 email: test_email(),
369 urls: test_base_urls(),
370 crypto_state: crypto_state.clone(),
371 },
372 ®,
373 )
374 .await
375 .unwrap();
376
377 let token_handler: Arc<dyn TokenHandler> = Arc::new(NoopTokenHandler);
378 let client = Client::load_from_state(token_handler, reg).await.unwrap();
379 client
380 .internal
381 .initialize_user_crypto_decrypted_key(user_key, crypto_state, &None)
382 .unwrap();
383
384 let _session_key = client.unlock().generate_session_key().await.unwrap();
385
386 let envelope: Option<SymmetricKeyEnvelope> = client
387 .platform()
388 .state()
389 .setting(SESSION_PROTECTED_USER_KEY)
390 .unwrap()
391 .get()
392 .await
393 .unwrap();
394 assert!(
395 envelope.is_some(),
396 "SESSION_PROTECTED_USER_KEY should be persisted after generate_session_key"
397 );
398 }
399
400 #[tokio::test]
401 async fn unlock_with_session_key_restores_user_key() {
402 ensure_platform_info();
403 let (user_key, crypto_state) = make_test_user_crypto();
404 let expected_user_key = user_key.to_base64().to_string();
405 let (envelope, session_key) = seal_with_new_session_key(&user_key);
406
407 let reg = populate_registry_for_unlock(envelope, crypto_state).await;
408 let token_handler: Arc<dyn TokenHandler> = Arc::new(NoopTokenHandler);
409 let client = Client::load_from_state(token_handler, reg).await.unwrap();
410 assert!(
411 !is_unlocked(&client),
412 "Rehydrated client should start locked"
413 );
414
415 client
416 .unlock()
417 .unlock(UnlockMethod::SessionKey(session_key))
418 .await
419 .unwrap();
420
421 assert!(is_unlocked(&client));
422 assert_eq!(
423 user_key_base64(&client),
424 expected_user_key,
425 "Unlocked user key should match the original"
426 );
427 }
428
429 #[tokio::test]
430 async fn unlock_missing_session_protected_user_key_returns_unknown() {
431 ensure_platform_info();
432 let (user_key, crypto_state) = make_test_user_crypto();
433 let (_, session_key) = seal_with_new_session_key(&user_key);
434
435 let reg = StateRegistry::new_with_memory_db();
436 Client::save_to_state(
437 SaveStateData {
438 user_id: test_user_id(),
439 email: test_email(),
440 urls: test_base_urls(),
441 crypto_state,
442 },
443 ®,
444 )
445 .await
446 .unwrap();
447 let token_handler: Arc<dyn TokenHandler> = Arc::new(NoopTokenHandler);
448 let client = Client::load_from_state(token_handler, reg).await.unwrap();
449
450 let result = client
451 .unlock()
452 .unlock(UnlockMethod::SessionKey(session_key))
453 .await;
454 assert!(matches!(result, Err(UnlockError::Unknown)));
455 }
456
457 #[tokio::test]
458 async fn unlock_missing_account_crypto_state_returns_unknown() {
459 ensure_platform_info();
460 let (user_key, _crypto_state) = make_test_user_crypto();
461 let (envelope, session_key) = seal_with_new_session_key(&user_key);
462
463 let reg = StateRegistry::new_with_memory_db();
464 reg.setting(BASE_URLS)
465 .unwrap()
466 .update(test_base_urls())
467 .await
468 .unwrap();
469 reg.setting(USER_ID)
470 .unwrap()
471 .update(test_user_id())
472 .await
473 .unwrap();
474 reg.setting(SESSION_PROTECTED_USER_KEY)
475 .unwrap()
476 .update(envelope)
477 .await
478 .unwrap();
479 let token_handler: Arc<dyn TokenHandler> = Arc::new(NoopTokenHandler);
480 let client = Client::load_from_state(token_handler, reg).await.unwrap();
481
482 let result = client
483 .unlock()
484 .unlock(UnlockMethod::SessionKey(session_key))
485 .await;
486 assert!(matches!(result, Err(UnlockError::Unknown)));
487 }
488
489 #[tokio::test]
490 async fn unlock_with_wrong_session_key_returns_unknown() {
491 ensure_platform_info();
492 let (user_key, crypto_state) = make_test_user_crypto();
493 let (envelope, _real_session_key) = seal_with_new_session_key(&user_key);
494
495 let reg = populate_registry_for_unlock(envelope, crypto_state).await;
496 let token_handler: Arc<dyn TokenHandler> = Arc::new(NoopTokenHandler);
497 let client = Client::load_from_state(token_handler, reg).await.unwrap();
498
499 let wrong_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::XChaCha20Poly1305);
500 let result = client
501 .unlock()
502 .unlock(UnlockMethod::SessionKey(SessionKey(wrong_key)))
503 .await;
504 assert!(matches!(result, Err(UnlockError::Unknown)));
505 }
506}