1use zeroize::Zeroizing;
4
5use super::{
6 account_key::AccountKey,
7 credentials::{Credentials, SsoCredentials},
8 device::ClientInfo,
9 error::OnePasswordError,
10 keychain::{Keychain, MASTER_KEY_ID},
11 login::{self, LoginOutcome, SignInMethods},
12 model::{
13 DownloadedAccount, Item, ItemCategory, SkippedItem, SkippedReason, SkippedVault, Vault,
14 },
15 opdata::{AesKey, Encrypted},
16 rest::RestClient,
17 session::Session,
18 sign_in::SignInAddress,
19 sso::{Timing, sso_login, ui::SsoUi},
20 two_factor::TwoFactorUi,
21 wire::{
22 AccountInfo, EncryptedEnvelope, KeysetsInfo, VaultAccess, VaultAttributes, VaultItem,
23 VaultItemOverview, VaultItemsBatch,
24 },
25};
26
27const MAX_OTP_ATTEMPTS: u32 = 3;
28const ACCOUNT_INFO_ENDPOINT: &str =
29 "v1/account?attrs=billing,counts,groups,invite,me,settings,tier,user-flags,users,vaults";
30const KEYSETS_ENDPOINT: &str = "v1/account/keysets";
31const VAULT_ENDPOINT: &str = "v1/vault";
32
33pub struct Client {
35 http: reqwest::Client,
36}
37
38impl Client {
39 pub fn new(http: reqwest::Client) -> Client {
42 Client { http }
43 }
44
45 pub async fn sign_in_methods(
48 &self,
49 username: &str,
50 mut sign_in_address: SignInAddress,
51 ) -> Result<SignInMethods, OnePasswordError> {
52 sign_in_address.normalize()?;
53 let device_uuid = super::device::generate_device_uuid();
54 let rest = self.rest_client(&sign_in_address, &ClientInfo::for_desktop(&device_uuid))?;
55
56 let login_info = login::fetch_auth_methods(username, &rest).await?;
57 Ok(SignInMethods::of(&login_info))
58 }
59
60 pub async fn open_account(
63 &self,
64 credentials: Credentials,
65 ui: &dyn TwoFactorUi,
66 ) -> Result<DownloadedAccount, OnePasswordError> {
67 let mut credentials = Zeroizing::new(credentials);
68 credentials.sign_in_address.normalize()?;
69 let account_key = AccountKey::parse(&credentials.account_key)?;
70 let session = self.login(&credentials, &account_key, ui).await?;
71 download_vaults(
72 MasterKey::Derived {
73 credentials: &credentials,
74 account_key: &account_key,
75 },
76 &session,
77 )
78 .await
79 }
80
81 pub async fn open_account_sso(
86 &self,
87 mut credentials: SsoCredentials,
88 ui: &dyn SsoUi,
89 ) -> Result<DownloadedAccount, OnePasswordError> {
90 credentials.validate()?;
91 let device_uuid = super::device::generate_device_uuid();
92 let client_info = ClientInfo::for_desktop(&device_uuid);
93 let rest = self.rest_client(&credentials.sign_in_address, &client_info)?;
94
95 let (session, master_key) =
96 sso_login(&credentials, &client_info, ui, &rest, &Timing::default()).await?;
97 download_vaults(MasterKey::Known(master_key), &session).await
98 }
99
100 async fn login(
105 &self,
106 credentials: &Credentials,
107 account_key: &AccountKey,
108 ui: &dyn TwoFactorUi,
109 ) -> Result<Session, OnePasswordError> {
110 let device_uuid = super::device::generate_device_uuid();
111 let client_info = ClientInfo::for_desktop(&device_uuid);
112 let rest = self.rest_client(&credentials.sign_in_address, &client_info)?;
113
114 let login_info = login::fetch_auth_methods(&credentials.username, &rest).await?;
116 if !SignInMethods::of(&login_info).password {
117 return Err(OnePasswordError::Unsupported(format!(
118 "no password login method found for account {}",
119 credentials.username
120 )));
121 }
122
123 for attempt in 0..MAX_OTP_ATTEMPTS {
124 match login::login_attempt(credentials, account_key, &client_info, attempt, ui, &rest)
125 .await?
126 {
127 LoginOutcome::Success(session) => return Ok(*session),
128 LoginOutcome::BadOtp => continue,
129 }
130 }
131
132 Err(OnePasswordError::TwoFactorFailed)
133 }
134
135 fn rest_client(
136 &self,
137 sign_in_address: &SignInAddress,
138 client_info: &ClientInfo,
139 ) -> Result<RestClient, OnePasswordError> {
140 RestClient::new(
141 self.http.clone(),
142 format!("https://{sign_in_address}/api"),
143 &client_info.client_id(),
144 &client_info.user_agent,
145 &client_info.op_user_agent,
146 )
147 }
148}
149
150pub(super) enum MasterKey<'a> {
152 Derived {
154 credentials: &'a Credentials,
155 account_key: &'a AccountKey,
156 },
157
158 Known(Zeroizing<Vec<u8>>),
160}
161
162pub(super) async fn download_vaults(
164 master_key: MasterKey<'_>,
165 session: &Session,
166) -> Result<DownloadedAccount, OnePasswordError> {
167 let (keychain, vaults, mut skipped_vaults) = unlock_account(master_key, session).await?;
168
169 let mut downloaded = Vec::with_capacity(vaults.len());
170 for info in vaults {
171 match download_vault(info, &keychain, session).await? {
172 VaultDownload::Downloaded(vault) => downloaded.push(vault),
173 VaultDownload::Skipped(vault) => skipped_vaults.push(vault),
174 }
175 }
176
177 Ok(DownloadedAccount {
178 vaults: downloaded,
179 skipped_vaults,
180 })
181}
182
183struct VaultInfo {
185 id: String,
186 name: String,
187 item_count: Option<u32>,
188}
189
190enum VaultDownload {
191 Downloaded(Vault),
192 Skipped(SkippedVault),
193}
194
195async fn download_vault(
197 info: VaultInfo,
198 keychain: &Keychain,
199 session: &Session,
200) -> Result<VaultDownload, OnePasswordError> {
201 let download = match download_vault_items(&info.id, keychain, session).await {
202 Ok(download) => download,
203 Err(OnePasswordError::NotFound) => {
206 return Ok(VaultDownload::Skipped(SkippedVault {
207 id: info.id,
208 item_count: info.item_count,
209 reason: SkippedReason::NoAccess,
210 }));
211 }
212 Err(error) => return Err(error),
213 };
214
215 Ok(VaultDownload::Downloaded(Vault {
216 id: info.id,
217 name: info.name,
218 items: download.items,
219 skipped_items: download.skipped_items,
220 }))
221}
222
223async fn unlock_account(
225 master_key: MasterKey<'_>,
226 session: &Session,
227) -> Result<(Keychain, Vec<VaultInfo>, Vec<SkippedVault>), OnePasswordError> {
228 let account_info: AccountInfo = session
230 .rest
231 .get_encrypted_json(ACCOUNT_INFO_ENDPOINT, &session.key)
232 .await?;
233 let keysets: KeysetsInfo = session
234 .rest
235 .get_encrypted_json(KEYSETS_ENDPOINT, &session.key)
236 .await?;
237
238 let mut keychain = Keychain::new();
240 match master_key {
241 MasterKey::Derived {
242 credentials,
243 account_key,
244 } => {
245 keychain.decrypt_keysets(
246 &keysets.keysets,
247 &credentials.username,
248 &credentials.password,
249 account_key,
250 )?;
251 }
252 MasterKey::Known(key) => {
253 keychain
254 .decrypt_reachable(&keysets.keysets, AesKey::new(MASTER_KEY_ID, key.to_vec()))?;
255 }
256 }
257
258 let mut vaults = Vec::new();
259 let mut skipped_vaults = Vec::new();
260 for vault in &account_info.vaults {
261 match unlock_vault(vault, &mut keychain)? {
262 VaultUnlock::Unlocked(vault) => vaults.push(vault),
263 VaultUnlock::Skipped(vault) => skipped_vaults.push(vault),
264 }
265 }
266
267 Ok((keychain, vaults, skipped_vaults))
268}
269
270enum VaultUnlock {
271 Unlocked(VaultInfo),
272 Skipped(SkippedVault),
273}
274
275fn unlock_vault(
278 vault: &super::wire::VaultInfo,
279 keychain: &mut Keychain,
280) -> Result<VaultUnlock, OnePasswordError> {
281 let skipped = |reason| {
282 VaultUnlock::Skipped(SkippedVault {
283 id: vault.uuid.clone(),
284 item_count: vault.active_item_count,
285 reason,
286 })
287 };
288
289 let enc_key = match find_working_key(&vault.access, keychain)? {
290 VaultKey::Found(enc_key) => enc_key,
291 VaultKey::Missing(reason) => return Ok(skipped(reason)),
292 };
293 keychain.decrypt_aes_key(enc_key)?;
294
295 if let Some(reason) = skip_reason(&vault.enc_attrs, keychain)? {
296 return Ok(skipped(reason));
297 }
298 let attributes: VaultAttributes = keychain.decrypt_json(&vault.enc_attrs)?;
299 Ok(VaultUnlock::Unlocked(VaultInfo {
300 id: vault.uuid.clone(),
301 name: attributes.name.unwrap_or_default(),
302 item_count: vault.active_item_count,
303 }))
304}
305
306struct VaultItemsDownload {
307 items: Vec<Item>,
308 skipped_items: Vec<SkippedItem>,
309}
310
311async fn download_vault_items(
313 vault_id: &str,
314 keychain: &Keychain,
315 session: &Session,
316) -> Result<VaultItemsDownload, OnePasswordError> {
317 let mut items = Vec::new();
318 let mut skipped_items = Vec::new();
319 let mut batch_id: i64 = 0;
320 loop {
321 let batch: VaultItemsBatch = session
322 .rest
323 .get_encrypted_json(
324 &format!("{VAULT_ENDPOINT}/{vault_id}/{batch_id}/items"),
325 &session.key,
326 )
327 .await?;
328
329 for item in batch.items.into_iter().flatten() {
330 if item.trashed == "Y" {
331 continue;
332 }
333 match parse_item(&item, keychain)? {
334 ItemDownload::Downloaded(item) => items.push(item),
335 ItemDownload::Skipped(item) => skipped_items.push(item),
336 }
337 }
338
339 if batch.complete {
340 return Ok(VaultItemsDownload {
341 items,
342 skipped_items,
343 });
344 }
345
346 if batch.version <= batch_id {
349 return Err(OnePasswordError::Internal(format!(
350 "vault {vault_id} pagination stalled at content version {batch_id}"
351 )));
352 }
353 batch_id = batch.version;
354 }
355}
356
357enum ItemDownload {
358 Downloaded(Item),
359 Skipped(SkippedItem),
360}
361
362fn parse_item(item: &VaultItem, keychain: &Keychain) -> Result<ItemDownload, OnePasswordError> {
365 let category = ItemCategory::from_template_id(&item.template_uuid);
366 if let Some(reason) = skip_reason(&item.enc_overview, keychain)? {
367 return Ok(ItemDownload::Skipped(SkippedItem {
368 id: item.uuid.clone(),
369 name: None,
370 category,
371 reason,
372 }));
373 }
374 let overview: VaultItemOverview = keychain.decrypt_json(&item.enc_overview)?;
375
376 if let Some(reason) = skip_reason(&item.enc_details, keychain)? {
377 return Ok(ItemDownload::Skipped(SkippedItem {
378 id: item.uuid.clone(),
379 name: overview.title.clone(),
380 category,
381 reason,
382 }));
383 }
384 let details = keychain.decrypt_json(&item.enc_details)?;
385
386 Ok(ItemDownload::Downloaded(Item {
387 id: item.uuid.clone(),
388 category,
389 overview,
390 details,
391 }))
392}
393
394fn skip_reason(
397 envelope: &EncryptedEnvelope,
398 keychain: &Keychain,
399) -> Result<Option<SkippedReason>, OnePasswordError> {
400 let encrypted = Encrypted::parse(envelope)?;
401 match keychain.can_decrypt(&encrypted) {
402 Ok(true) => Ok(None),
403 Ok(false) => Ok(Some(SkippedReason::NoAccess)),
404 Err(OnePasswordError::Unsupported(_)) => Ok(Some(SkippedReason::Unsupported)),
405 Err(error) => Err(error),
406 }
407}
408
409enum VaultKey<'a> {
410 Found(&'a EncryptedEnvelope),
411 Missing(SkippedReason),
412}
413
414fn find_working_key<'a>(
420 access: &'a [VaultAccess],
421 keychain: &Keychain,
422) -> Result<VaultKey<'a>, OnePasswordError> {
423 let mut reason = SkippedReason::NoAccess;
424 for entry in access.iter().filter(|entry| is_read_accessible(entry.acl)) {
425 match skip_reason(&entry.enc_vault_key, keychain)? {
426 None => return Ok(VaultKey::Found(&entry.enc_vault_key)),
427 Some(SkippedReason::Unsupported) => reason = SkippedReason::Unsupported,
428 Some(SkippedReason::NoAccess) => {}
429 }
430 }
431
432 Ok(VaultKey::Missing(reason))
433}
434
435fn is_read_accessible(acl: i32) -> bool {
437 const HAVE_READ_ACCESS: i32 = 32;
438 acl & HAVE_READ_ACCESS != 0
439}
440
441#[cfg(test)]
442mod tests {
443 use bitwarden_api_base::new_http_client;
444 use serde_json::json;
445 use wiremock::{Mock, MockServer, ResponseTemplate, matchers};
446
447 use super::{
448 super::opdata::{AesKey, decode64_loose},
449 *,
450 };
451
452 const VAULT_ID: &str = "vault-id";
453
454 fn session(server: &MockServer) -> Session {
455 let rest = RestClient::new(
456 new_http_client(),
457 format!("http://{}/api", server.address()),
458 "client-id",
459 "user-agent",
460 "op-user-agent",
461 )
462 .expect("valid headers");
463
464 Session::new(session_key(), rest)
465 }
466
467 fn session_key() -> AesKey {
468 AesKey::new(
469 "SESSION",
470 decode64_loose("WyICHHlP5lPigZUGZYoivbJMqgHjSti86UKwdjCryYM").expect("valid key"),
471 )
472 }
473
474 async fn mock_batch(server: &MockServer, batch_id: i64, body: serde_json::Value) {
476 let envelope = session_key()
477 .encrypt(body.to_string().as_bytes(), &[0u8; 12])
478 .expect("encrypts");
479 server
480 .register(
481 Mock::given(matchers::path(format!(
482 "/api/v1/vault/{VAULT_ID}/{batch_id}/items"
483 )))
484 .respond_with(
485 ResponseTemplate::new(200)
486 .set_body_json(serde_json::to_value(&envelope).expect("serializes")),
487 )
488 .expect(1),
489 )
490 .await;
491 }
492
493 fn batch(version: i64, complete: bool) -> serde_json::Value {
494 json!({"contentVersion": version, "batchComplete": complete, "items": []})
495 }
496
497 #[tokio::test]
498 async fn download_pages_until_the_batch_is_complete() {
499 let server = MockServer::start().await;
500 mock_batch(&server, 0, batch(7, false)).await;
501 mock_batch(&server, 7, batch(9, true)).await;
502
503 let download = download_vault_items(VAULT_ID, &Keychain::new(), &session(&server))
504 .await
505 .expect("pagination advances to the final batch");
506
507 assert!(download.items.is_empty());
508 assert!(download.skipped_items.is_empty());
509 server.verify().await;
510 }
511
512 #[tokio::test]
513 async fn items_without_keys_are_recorded_and_other_items_continue() {
514 let server = MockServer::start().await;
515 let readable_key = AesKey::new("VAULT", vec![7u8; 32]);
516 let missing_key = AesKey::new("MISSING", vec![8u8; 32]);
517 let overview = readable_key
518 .encrypt(br#"{"title":"partly readable"}"#, &[1u8; 12])
519 .expect("encrypts");
520 let missing_overview = missing_key
521 .encrypt(br#"{"title":"not readable"}"#, &[2u8; 12])
522 .expect("encrypts");
523 let missing_details = missing_key.encrypt(br#"{}"#, &[5u8; 12]).expect("encrypts");
524 let good_overview = readable_key
525 .encrypt(br#"{"title":"readable"}"#, &[3u8; 12])
526 .expect("encrypts");
527 let good_details = readable_key
528 .encrypt(br#"{}"#, &[4u8; 12])
529 .expect("encrypts");
530 mock_batch(
531 &server,
532 0,
533 json!({
534 "contentVersion": 1,
535 "batchComplete": true,
536 "items": [
537 {
538 "uuid": "bad-details",
539 "templateUuid": "001",
540 "trashed": "N",
541 "encOverview": overview,
542 "encDetails": missing_details
543 },
544 {
545 "uuid": "bad-overview",
546 "templateUuid": "002",
547 "trashed": "N",
548 "encOverview": missing_overview,
549 "encDetails": {
550 "kid": "VAULT", "enc": "A256GCM", "cty": "b5+jwk+json",
551 "data": "!"
552 }
553 },
554 {
555 "uuid": "good-item",
556 "templateUuid": "003",
557 "trashed": "N",
558 "encOverview": good_overview,
559 "encDetails": good_details
560 },
561 {
562 "uuid": "unsupported-encryption",
563 "templateUuid": "004",
564 "trashed": "N",
565 "encOverview": {
566 "kid": "VAULT", "enc": "FUTURE", "cty": "b5+jwk+json", "data": ""
567 },
568 "encDetails": {
569 "kid": "VAULT", "enc": "A256GCM", "cty": "b5+jwk+json", "data": ""
570 }
571 }
572 ]
573 }),
574 )
575 .await;
576
577 let mut keychain = Keychain::new();
578 keychain.add_aes(readable_key);
579 let download = download_vault_items(VAULT_ID, &keychain, &session(&server))
580 .await
581 .expect("one bad item does not fail its vault");
582
583 assert_eq!(download.items.len(), 1);
584 assert_eq!(download.items[0].id, "good-item");
585 assert_eq!(download.skipped_items.len(), 3);
586 assert_eq!(download.skipped_items[0].id, "bad-details");
587 assert_eq!(
588 download.skipped_items[0].name.as_deref(),
589 Some("partly readable")
590 );
591 assert_eq!(download.skipped_items[0].category, ItemCategory::Login);
592 assert_eq!(download.skipped_items[0].reason, SkippedReason::NoAccess);
593 assert_eq!(download.skipped_items[1].id, "bad-overview");
594 assert_eq!(download.skipped_items[1].name, None);
595 assert_eq!(download.skipped_items[1].category, ItemCategory::CreditCard);
596 assert_eq!(download.skipped_items[1].reason, SkippedReason::NoAccess);
597 assert_eq!(download.skipped_items[2].id, "unsupported-encryption");
598 assert_eq!(download.skipped_items[2].reason, SkippedReason::Unsupported);
599 server.verify().await;
600 }
601
602 #[test]
603 fn invalid_json_or_undecryptable_items_are_errors() {
604 let readable_key = AesKey::new("VAULT", vec![7u8; 32]);
605 let wrong_key = AesKey::new("VAULT", vec![8u8; 32]);
606 let details = readable_key
607 .encrypt(br#"{}"#, &[1u8; 12])
608 .expect("encrypts");
609 let invalid_json: VaultItem = serde_json::from_value(json!({
610 "uuid": "invalid-json",
611 "templateUuid": "001",
612 "trashed": "N",
613 "encOverview": readable_key
614 .encrypt(b"not json", &[4u8; 12])
615 .expect("encrypts"),
616 "encDetails": details,
617 }))
618 .expect("valid item envelope");
619 let undecryptable: VaultItem = serde_json::from_value(json!({
620 "uuid": "undecryptable",
621 "templateUuid": "001",
622 "trashed": "N",
623 "encOverview": wrong_key
624 .encrypt(br#"{}"#, &[2u8; 12])
625 .expect("encrypts"),
626 "encDetails": readable_key
627 .encrypt(br#"{}"#, &[3u8; 12])
628 .expect("encrypts"),
629 }))
630 .expect("valid item envelope");
631
632 let mut keychain = Keychain::new();
633 keychain.add_aes(readable_key);
634 assert!(matches!(
635 parse_item(&invalid_json, &keychain),
636 Err(OnePasswordError::Parse)
637 ));
638 assert!(matches!(
639 parse_item(&undecryptable, &keychain),
640 Err(OnePasswordError::Internal(_))
641 ));
642 }
643
644 #[tokio::test]
645 async fn download_stops_when_pagination_does_not_advance() {
646 let server = MockServer::start().await;
647 mock_batch(&server, 0, batch(7, false)).await;
648 mock_batch(&server, 7, batch(7, false)).await;
649
650 let error = download_vault_items(VAULT_ID, &Keychain::new(), &session(&server))
651 .await
652 .map(|_| ())
653 .expect_err("refetching the same page is an error, not a loop");
654
655 assert!(
656 error.to_string().contains("pagination stalled"),
657 "unexpected error: {error}"
658 );
659 server.verify().await;
660 }
661
662 #[tokio::test]
663 async fn a_missing_vault_is_reported() {
664 let server = MockServer::start().await;
665 server
666 .register(
667 Mock::given(matchers::path(format!("/api/v1/vault/{VAULT_ID}/0/items")))
668 .respond_with(ResponseTemplate::new(404).set_body_json(json!({
669 "errorCode": 117,
670 "errorMessage": "vault not found",
671 })))
672 .expect(1),
673 )
674 .await;
675
676 let result = download_vault(
677 VaultInfo {
678 id: VAULT_ID.into(),
679 name: "Known vault".into(),
680 item_count: Some(17),
681 },
682 &Keychain::new(),
683 &session(&server),
684 )
685 .await
686 .expect("a missing vault only skips that vault");
687
688 let VaultDownload::Skipped(vault) = result else {
689 panic!("the failed vault should be reported as skipped");
690 };
691 assert_eq!(vault.id, VAULT_ID);
692 assert_eq!(vault.item_count, Some(17));
693 assert_eq!(vault.reason, SkippedReason::NoAccess);
694 server.verify().await;
695 }
696
697 #[tokio::test]
698 async fn an_auth_or_transient_vault_failure_aborts_the_import() {
699 for status in [401, 503] {
700 let server = MockServer::start().await;
701 server
702 .register(
703 Mock::given(matchers::path(format!("/api/v1/vault/{VAULT_ID}/0/items")))
704 .respond_with(ResponseTemplate::new(status))
705 .expect(1),
706 )
707 .await;
708
709 let result = download_vault(
710 VaultInfo {
711 id: VAULT_ID.into(),
712 name: "Known vault".into(),
713 item_count: Some(17),
714 },
715 &Keychain::new(),
716 &session(&server),
717 )
718 .await;
719
720 assert!(result.is_err());
721 server.verify().await;
722 }
723 }
724
725 fn access(acl: i32, kid: &str) -> VaultAccess {
726 access_with_scheme(acl, kid, "A256GCM")
727 }
728
729 fn access_with_scheme(acl: i32, kid: &str, scheme: &str) -> VaultAccess {
730 serde_json::from_value(json!({
731 "acl": acl,
732 "encVaultKey": {"kid": kid, "enc": scheme, "cty": "b5+jwk+json", "data": ""},
733 }))
734 .expect("valid access entry")
735 }
736
737 #[test]
738 fn a_vault_with_unreadable_attributes_is_skipped() {
739 let account_key = AesKey::new("ACCOUNT", vec![1u8; 32]);
740 let vault_key = json!({"kid": "VAULT", "k": "WyICHHlP5lPigZUGZYoivbJMqgHjSti86UKwdjCryYM"});
741 let enc_vault_key = account_key
742 .encrypt(vault_key.to_string().as_bytes(), &[0u8; 12])
743 .expect("encrypts");
744 let mut keychain = Keychain::new();
745 keychain.add_aes(account_key);
746
747 for (kid, scheme, expected) in [
748 ("VAULT", "FUTURE", SkippedReason::Unsupported),
749 ("MISSING", "A256GCM", SkippedReason::NoAccess),
750 ] {
751 let vault: super::super::wire::VaultInfo = serde_json::from_value(json!({
752 "uuid": VAULT_ID,
753 "encAttrs": {"kid": kid, "enc": scheme, "cty": "b5+jwk+json", "data": ""},
754 "access": [{"acl": 32, "encVaultKey": enc_vault_key}],
755 }))
756 .expect("valid vault");
757
758 let unlocked =
759 unlock_vault(&vault, &mut keychain).expect("the envelopes are well formed");
760 assert!(matches!(
761 unlocked,
762 VaultUnlock::Skipped(SkippedVault { reason, .. }) if reason == expected
763 ));
764 }
765 }
766
767 #[test]
768 fn read_access_requires_the_read_bit() {
769 assert!(is_read_accessible(32));
770 assert!(is_read_accessible(0xFFFF));
771 assert!(!is_read_accessible(0));
772 assert!(!is_read_accessible(31));
773 }
774
775 #[test]
776 fn find_working_key_skips_entries_we_cannot_use() {
777 let mut keychain = Keychain::new();
778 keychain.add_aes(AesKey::new("usable", vec![0u8; 32]));
779
780 let entries = vec![
781 access(32, "missing"),
783 access_with_scheme(32, "usable", "FUTURE"),
785 access(1, "usable"),
787 access(32, "usable"),
789 ];
790
791 let found = find_working_key(&entries, &keychain).expect("the envelopes are well formed");
792 assert!(matches!(
793 found,
794 VaultKey::Found(entry) if entry.kid == "usable" && entry.enc == "A256GCM"
795 ));
796 }
797
798 #[test]
799 fn find_working_key_reports_no_access_without_a_usable_entry() {
800 let keychain = Keychain::new();
801 let entries = [
802 access(32, "missing"),
803 access_with_scheme(1, "missing", "FUTURE"),
805 ];
806
807 let found = find_working_key(&entries, &keychain).expect("the envelopes are well formed");
808 assert!(matches!(found, VaultKey::Missing(SkippedReason::NoAccess)));
809 }
810
811 #[test]
812 fn find_working_key_reports_an_unsupported_scheme() {
813 let keychain = Keychain::new();
814 let entries = [
815 access_with_scheme(32, "missing", "FUTURE"),
816 access(32, "missing"),
817 ];
818
819 let found = find_working_key(&entries, &keychain).expect("the envelopes are well formed");
820 assert!(matches!(
821 found,
822 VaultKey::Missing(SkippedReason::Unsupported)
823 ));
824 }
825}