bitwarden_importers/importers/onepassword/access/
client.rs1use super::{
4 account_key::AccountKey,
5 credentials::Credentials,
6 device::ClientInfo,
7 error::OnePasswordError,
8 keychain::Keychain,
9 login::{self, LoginOutcome},
10 model::{Item, ItemCategory, Vault},
11 opdata::Encrypted,
12 rest::RestClient,
13 session::Session,
14 two_factor::TwoFactorUi,
15 wire::{
16 AccountInfo, EncryptedEnvelope, KeysetsInfo, VaultAccess, VaultAttributes, VaultItem,
17 VaultItemsBatch,
18 },
19};
20
21const PASSWORD_SK_METHOD: &str = "PASSWORD+SK";
22const MAX_OTP_ATTEMPTS: u32 = 3;
23const ACCOUNT_INFO_ENDPOINT: &str =
24 "v1/account?attrs=billing,counts,groups,invite,me,settings,tier,user-flags,users,vaults";
25const KEYSETS_ENDPOINT: &str = "v1/account/keysets";
26const VAULT_ENDPOINT: &str = "v1/vault";
27
28pub struct Client {
30 http: reqwest::Client,
31}
32
33impl Client {
34 pub fn new(http: reqwest::Client) -> Client {
37 Client { http }
38 }
39
40 pub async fn download_all_vaults(
45 &self,
46 credentials: &Credentials,
47 ui: &dyn TwoFactorUi,
48 ) -> Result<Vec<Vault>, OnePasswordError> {
49 let account_key = AccountKey::parse(&credentials.account_key)?;
50 let session = self.login(credentials, &account_key, ui).await?;
51 let (keychain, vaults) = unlock(credentials, &account_key, &session).await?;
52
53 let mut downloaded = Vec::with_capacity(vaults.len());
54 for info in &vaults {
55 downloaded.push(Vault {
56 id: info.id.clone(),
57 name: info.name.clone(),
58 description: info.description.clone(),
59 items: download_vault_items(&info.id, &keychain, &session).await?,
60 });
61 }
62
63 Ok(downloaded)
64 }
65
66 async fn login(
71 &self,
72 credentials: &Credentials,
73 account_key: &AccountKey,
74 ui: &dyn TwoFactorUi,
75 ) -> Result<Session, OnePasswordError> {
76 let client_info = ClientInfo::for_desktop(&credentials.device_uuid);
77 let rest = RestClient::new(
78 self.http.clone(),
79 format!("https://{}/api", credentials.sign_in_address),
80 &client_info.client_id(),
81 &client_info.user_agent,
82 &client_info.op_user_agent,
83 )?;
84
85 let login_info = login::fetch_auth_methods(&credentials.username, &rest).await?;
87 if !login_info
88 .auth_methods
89 .iter()
90 .any(|m| m.kind == PASSWORD_SK_METHOD)
91 {
92 return Err(OnePasswordError::Unsupported(format!(
93 "no password login method found for account {}",
94 credentials.username
95 )));
96 }
97
98 for attempt in 0..MAX_OTP_ATTEMPTS {
99 match login::login_attempt(credentials, account_key, &client_info, attempt, ui, &rest)
100 .await?
101 {
102 LoginOutcome::Success(session) => return Ok(*session),
103 LoginOutcome::BadOtp => continue,
104 }
105 }
106
107 Err(OnePasswordError::TwoFactorFailed)
108 }
109}
110
111struct VaultInfo {
113 id: String,
114 name: String,
115 description: String,
116}
117
118async fn unlock(
122 credentials: &Credentials,
123 account_key: &AccountKey,
124 session: &Session,
125) -> Result<(Keychain, Vec<VaultInfo>), OnePasswordError> {
126 let account_info: AccountInfo = session
128 .rest
129 .get_encrypted_json(ACCOUNT_INFO_ENDPOINT, &session.key)
130 .await?;
131 let keysets: KeysetsInfo = session
132 .rest
133 .get_encrypted_json(KEYSETS_ENDPOINT, &session.key)
134 .await?;
135
136 let mut keychain = Keychain::new();
138 keychain.decrypt_keysets(
139 &keysets.keysets,
140 &credentials.username,
141 &credentials.password,
142 account_key,
143 )?;
144
145 let mut vaults = Vec::new();
149 for vault in &account_info.vaults {
150 let Some(enc_key) = find_working_key(&vault.access, &keychain)? else {
151 continue;
152 };
153 keychain.decrypt_aes_key(enc_key)?;
154
155 let attributes: VaultAttributes = keychain.decrypt_json(&vault.enc_attrs)?;
156 vaults.push(VaultInfo {
157 id: vault.uuid.clone(),
158 name: attributes.name.unwrap_or_default(),
159 description: attributes.desc.unwrap_or_default(),
160 });
161 }
162
163 Ok((keychain, vaults))
164}
165
166async fn download_vault_items(
168 vault_id: &str,
169 keychain: &Keychain,
170 session: &Session,
171) -> Result<Vec<Item>, OnePasswordError> {
172 let mut items = Vec::new();
173 let mut batch_id: i64 = 0;
174 loop {
175 let batch: VaultItemsBatch = session
176 .rest
177 .get_encrypted_json(
178 &format!("{VAULT_ENDPOINT}/{vault_id}/{batch_id}/items"),
179 &session.key,
180 )
181 .await?;
182
183 for item in batch.items.into_iter().flatten() {
184 if item.trashed == "Y" {
185 continue;
186 }
187 items.push(parse_item(&item, keychain)?);
188 }
189
190 if batch.complete {
191 return Ok(items);
192 }
193
194 if batch.version <= batch_id {
197 return Err(OnePasswordError::Internal(format!(
198 "vault {vault_id} pagination stalled at content version {batch_id}"
199 )));
200 }
201 batch_id = batch.version;
202 }
203}
204
205fn parse_item(item: &VaultItem, keychain: &Keychain) -> Result<Item, OnePasswordError> {
207 Ok(Item {
208 id: item.uuid.clone(),
209 category: ItemCategory::from_template_id(&item.template_uuid),
210 overview: keychain.decrypt_json(&item.enc_overview)?,
211 details: keychain.decrypt_json(&item.enc_details)?,
212 })
213}
214
215fn find_working_key<'a>(
221 access: &'a [VaultAccess],
222 keychain: &Keychain,
223) -> Result<Option<&'a EncryptedEnvelope>, OnePasswordError> {
224 for entry in access {
225 if is_read_accessible(entry.acl) {
226 let encrypted = Encrypted::parse(&entry.enc_vault_key)?;
227 if keychain.can_decrypt(&encrypted)? {
228 return Ok(Some(&entry.enc_vault_key));
229 }
230 }
231 }
232
233 Ok(None)
234}
235
236fn is_read_accessible(acl: i32) -> bool {
238 const HAVE_READ_ACCESS: i32 = 32;
239 acl & HAVE_READ_ACCESS != 0
240}
241
242#[cfg(test)]
243mod tests {
244 use bitwarden_api_base::new_http_client;
245 use serde_json::json;
246 use wiremock::{Mock, MockServer, ResponseTemplate, matchers};
247
248 use super::{
249 super::opdata::{AesKey, decode64_loose},
250 *,
251 };
252
253 const VAULT_ID: &str = "vault-id";
254
255 fn session(server: &MockServer) -> Session {
256 let rest = RestClient::new(
257 new_http_client(),
258 format!("http://{}/api", server.address()),
259 "client-id",
260 "user-agent",
261 "op-user-agent",
262 )
263 .expect("valid headers");
264
265 Session::new(session_key(), rest)
266 }
267
268 fn session_key() -> AesKey {
269 AesKey::new(
270 "SESSION",
271 decode64_loose("WyICHHlP5lPigZUGZYoivbJMqgHjSti86UKwdjCryYM").expect("valid key"),
272 )
273 }
274
275 async fn mock_batch(server: &MockServer, batch_id: i64, body: serde_json::Value) {
277 let envelope = session_key()
278 .encrypt(body.to_string().as_bytes(), &[0u8; 12])
279 .expect("encrypts");
280 server
281 .register(
282 Mock::given(matchers::path(format!(
283 "/api/v1/vault/{VAULT_ID}/{batch_id}/items"
284 )))
285 .respond_with(
286 ResponseTemplate::new(200)
287 .set_body_json(serde_json::to_value(&envelope).expect("serializes")),
288 )
289 .expect(1),
290 )
291 .await;
292 }
293
294 fn batch(version: i64, complete: bool) -> serde_json::Value {
295 json!({"contentVersion": version, "batchComplete": complete, "items": []})
296 }
297
298 #[tokio::test]
299 async fn download_pages_until_the_batch_is_complete() {
300 let server = MockServer::start().await;
301 mock_batch(&server, 0, batch(7, false)).await;
302 mock_batch(&server, 7, batch(9, true)).await;
303
304 let items = download_vault_items(VAULT_ID, &Keychain::new(), &session(&server))
305 .await
306 .expect("pagination advances to the final batch");
307
308 assert!(items.is_empty());
309 server.verify().await;
310 }
311
312 #[tokio::test]
313 async fn download_stops_when_pagination_does_not_advance() {
314 let server = MockServer::start().await;
315 mock_batch(&server, 0, batch(7, false)).await;
316 mock_batch(&server, 7, batch(7, false)).await;
317
318 let error = download_vault_items(VAULT_ID, &Keychain::new(), &session(&server))
319 .await
320 .map(|_| ())
321 .expect_err("refetching the same page is an error, not a loop");
322
323 assert!(
324 error.to_string().contains("pagination stalled"),
325 "unexpected error: {error}"
326 );
327 server.verify().await;
328 }
329
330 fn access(acl: i32, kid: &str) -> VaultAccess {
331 serde_json::from_value(json!({
332 "acl": acl,
333 "encVaultKey": {"kid": kid, "enc": "A256GCM", "cty": "b5+jwk+json", "data": ""},
334 }))
335 .expect("valid access entry")
336 }
337
338 #[test]
339 fn read_access_requires_the_read_bit() {
340 assert!(is_read_accessible(32));
341 assert!(is_read_accessible(0xFFFF));
342 assert!(!is_read_accessible(0));
343 assert!(!is_read_accessible(31));
344 }
345
346 #[test]
347 fn find_working_key_skips_entries_we_cannot_use() {
348 let mut keychain = Keychain::new();
349 keychain.add_aes(AesKey::new("usable", vec![0u8; 32]));
350
351 let entries = vec![
352 access(32, "missing"),
354 access(1, "usable"),
356 access(32, "usable"),
358 ];
359
360 let found = find_working_key(&entries, &keychain)
361 .expect("the schemes are all supported")
362 .expect("a usable entry");
363 assert_eq!(found.kid, "usable");
364 }
365
366 #[test]
367 fn find_working_key_returns_nothing_without_a_usable_entry() {
368 let keychain = Keychain::new();
369 let entries = [access(32, "missing")];
370 let found = find_working_key(&entries, &keychain).expect("the scheme is supported");
371 assert!(found.is_none());
372 }
373}