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_base_urls() -> BaseUrls {
252 BaseUrls {
253 identity_url: "https://identity.example.com".to_string(),
254 api_url: "https://api.example.com".to_string(),
255 }
256 }
257
258 fn is_unlocked(client: &Client) -> bool {
259 client
260 .internal
261 .get_key_store()
262 .context()
263 .has_symmetric_key(SymmetricKeySlotId::User)
264 }
265
266 fn user_key_base64(client: &Client) -> String {
267 let key_store = client.internal.get_key_store();
268 let ctx = key_store.context();
269 #[allow(deprecated)]
270 ctx.dangerous_get_symmetric_key(SymmetricKeySlotId::User)
271 .unwrap()
272 .to_base64()
273 .to_string()
274 }
275
276 fn make_test_user_crypto() -> (SymmetricCryptoKey, WrappedAccountCryptographicState) {
279 let store: KeyStore<KeySlotIds> = KeyStore::default();
280 let mut ctx = store.context_mut();
281 let user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
282 let private_key_id = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
283 let signing_key_id = ctx.make_signing_key(SignatureAlgorithm::Ed25519);
284 let signed_public_key = ctx
285 .make_signed_public_key(private_key_id, signing_key_id)
286 .unwrap();
287 let security_state = SecurityState::new();
288 let signed_security_state = security_state.sign(signing_key_id, &mut ctx).unwrap();
289 let wrapped_private = ctx.wrap_private_key(user_key_id, private_key_id).unwrap();
290 let wrapped_signing = ctx.wrap_signing_key(user_key_id, signing_key_id).unwrap();
291 #[allow(deprecated)]
292 let user_key = ctx
293 .dangerous_get_symmetric_key(user_key_id)
294 .unwrap()
295 .clone();
296 (
297 user_key,
298 WrappedAccountCryptographicState::V2 {
299 private_key: wrapped_private,
300 signed_public_key: Some(signed_public_key),
301 signing_key: wrapped_signing,
302 security_state: signed_security_state,
303 },
304 )
305 }
306
307 fn seal_with_new_session_key(
310 user_key: &SymmetricCryptoKey,
311 ) -> (SymmetricKeyEnvelope, SessionKey) {
312 let store: KeyStore<KeySlotIds> = KeyStore::default();
313 let mut ctx = store.context_mut();
314 let user_key_id = ctx.add_local_symmetric_key(user_key.clone());
315 let session_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
316 let envelope = SymmetricKeyEnvelope::seal(
317 user_key_id,
318 session_key_id,
319 SymmetricKeyEnvelopeNamespace::SessionKey,
320 &ctx,
321 )
322 .unwrap();
323 #[allow(deprecated)]
324 let session_key = ctx
325 .dangerous_get_symmetric_key(session_key_id)
326 .unwrap()
327 .clone();
328 (envelope, SessionKey(session_key))
329 }
330
331 async fn populate_registry_for_unlock(
332 envelope: SymmetricKeyEnvelope,
333 crypto_state: WrappedAccountCryptographicState,
334 ) -> StateRegistry {
335 let reg = StateRegistry::new_with_memory_db();
336 reg.setting(SESSION_PROTECTED_USER_KEY)
337 .unwrap()
338 .update(envelope)
339 .await
340 .unwrap();
341 Client::save_to_state(
342 SaveStateData {
343 user_id: test_user_id(),
344 urls: test_base_urls(),
345 crypto_state,
346 },
347 ®,
348 )
349 .await
350 .unwrap();
351 reg
352 }
353
354 #[tokio::test]
355 async fn generate_session_key_persists_envelope() {
356 ensure_platform_info();
357 let (user_key, crypto_state) = make_test_user_crypto();
358
359 let reg = StateRegistry::new_with_memory_db();
360 Client::save_to_state(
361 SaveStateData {
362 user_id: test_user_id(),
363 urls: test_base_urls(),
364 crypto_state: crypto_state.clone(),
365 },
366 ®,
367 )
368 .await
369 .unwrap();
370
371 let token_handler: Arc<dyn TokenHandler> = Arc::new(NoopTokenHandler);
372 let client = Client::load_from_state(token_handler, reg).await.unwrap();
373 client
374 .internal
375 .initialize_user_crypto_decrypted_key(user_key, crypto_state, &None)
376 .unwrap();
377
378 let _session_key = client.unlock().generate_session_key().await.unwrap();
379
380 let envelope: Option<SymmetricKeyEnvelope> = client
381 .platform()
382 .state()
383 .setting(SESSION_PROTECTED_USER_KEY)
384 .unwrap()
385 .get()
386 .await
387 .unwrap();
388 assert!(
389 envelope.is_some(),
390 "SESSION_PROTECTED_USER_KEY should be persisted after generate_session_key"
391 );
392 }
393
394 #[tokio::test]
395 async fn unlock_with_session_key_restores_user_key() {
396 ensure_platform_info();
397 let (user_key, crypto_state) = make_test_user_crypto();
398 let expected_user_key = user_key.to_base64().to_string();
399 let (envelope, session_key) = seal_with_new_session_key(&user_key);
400
401 let reg = populate_registry_for_unlock(envelope, crypto_state).await;
402 let token_handler: Arc<dyn TokenHandler> = Arc::new(NoopTokenHandler);
403 let client = Client::load_from_state(token_handler, reg).await.unwrap();
404 assert!(
405 !is_unlocked(&client),
406 "Rehydrated client should start locked"
407 );
408
409 client
410 .unlock()
411 .unlock(UnlockMethod::SessionKey(session_key))
412 .await
413 .unwrap();
414
415 assert!(is_unlocked(&client));
416 assert_eq!(
417 user_key_base64(&client),
418 expected_user_key,
419 "Unlocked user key should match the original"
420 );
421 }
422
423 #[tokio::test]
424 async fn unlock_missing_session_protected_user_key_returns_unknown() {
425 ensure_platform_info();
426 let (user_key, crypto_state) = make_test_user_crypto();
427 let (_, session_key) = seal_with_new_session_key(&user_key);
428
429 let reg = StateRegistry::new_with_memory_db();
430 Client::save_to_state(
431 SaveStateData {
432 user_id: test_user_id(),
433 urls: test_base_urls(),
434 crypto_state,
435 },
436 ®,
437 )
438 .await
439 .unwrap();
440 let token_handler: Arc<dyn TokenHandler> = Arc::new(NoopTokenHandler);
441 let client = Client::load_from_state(token_handler, reg).await.unwrap();
442
443 let result = client
444 .unlock()
445 .unlock(UnlockMethod::SessionKey(session_key))
446 .await;
447 assert!(matches!(result, Err(UnlockError::Unknown)));
448 }
449
450 #[tokio::test]
451 async fn unlock_missing_account_crypto_state_returns_unknown() {
452 ensure_platform_info();
453 let (user_key, _crypto_state) = make_test_user_crypto();
454 let (envelope, session_key) = seal_with_new_session_key(&user_key);
455
456 let reg = StateRegistry::new_with_memory_db();
457 reg.setting(BASE_URLS)
458 .unwrap()
459 .update(test_base_urls())
460 .await
461 .unwrap();
462 reg.setting(USER_ID)
463 .unwrap()
464 .update(test_user_id())
465 .await
466 .unwrap();
467 reg.setting(SESSION_PROTECTED_USER_KEY)
468 .unwrap()
469 .update(envelope)
470 .await
471 .unwrap();
472 let token_handler: Arc<dyn TokenHandler> = Arc::new(NoopTokenHandler);
473 let client = Client::load_from_state(token_handler, reg).await.unwrap();
474
475 let result = client
476 .unlock()
477 .unlock(UnlockMethod::SessionKey(session_key))
478 .await;
479 assert!(matches!(result, Err(UnlockError::Unknown)));
480 }
481
482 #[tokio::test]
483 async fn unlock_with_wrong_session_key_returns_unknown() {
484 ensure_platform_info();
485 let (user_key, crypto_state) = make_test_user_crypto();
486 let (envelope, _real_session_key) = seal_with_new_session_key(&user_key);
487
488 let reg = populate_registry_for_unlock(envelope, crypto_state).await;
489 let token_handler: Arc<dyn TokenHandler> = Arc::new(NoopTokenHandler);
490 let client = Client::load_from_state(token_handler, reg).await.unwrap();
491
492 let wrong_key = SymmetricCryptoKey::make_xchacha20_poly1305_key();
493 let result = client
494 .unlock()
495 .unlock(UnlockMethod::SessionKey(SessionKey(wrong_key)))
496 .await;
497 assert!(matches!(result, Err(UnlockError::Unknown)));
498 }
499}