bitwarden_importers/importers/onepassword/access/
rest.rs1use rand::Rng;
4use reqwest::{
5 Method,
6 header::{HeaderMap, HeaderName, HeaderValue},
7};
8use serde::de::DeserializeOwned;
9use serde_json::Value;
10
11use super::{
12 error::OnePasswordError,
13 mac::MacSigner,
14 opdata::{AesKey, Encrypted},
15 wire::{EncryptedEnvelope, ErrorResponse, FailureReason},
16};
17
18const CLIENT_HEADER: &str = "x-agilebits-client";
19const USER_AGENT_HEADER: &str = "user-agent";
20const OP_USER_AGENT_HEADER: &str = "op-user-agent";
21const SESSION_ID_HEADER: &str = "x-agilebits-session-id";
22const MAC_HEADER: &str = "x-agilebits-mac";
23const IV_SIZE: usize = 12;
24
25pub(super) struct RestClient {
28 http: reqwest::Client,
29 base_url: String,
30 headers: HeaderMap,
31 signer: Option<MacSigner>,
32}
33
34impl RestClient {
35 pub(super) fn new(
37 http: reqwest::Client,
38 base_url: impl Into<String>,
39 client_id: &str,
40 user_agent: &str,
41 op_user_agent: &str,
42 ) -> Result<RestClient, OnePasswordError> {
43 let mut headers = HeaderMap::new();
44 insert_header(&mut headers, CLIENT_HEADER, client_id)?;
45 insert_header(&mut headers, USER_AGENT_HEADER, user_agent)?;
46 insert_header(&mut headers, OP_USER_AGENT_HEADER, op_user_agent)?;
47
48 Ok(RestClient {
49 http,
50 base_url: base_url.into(),
51 headers,
52 signer: None,
53 })
54 }
55
56 pub(super) fn with_session_id(&self, session_id: &str) -> Result<RestClient, OnePasswordError> {
58 let mut headers = self.headers.clone();
59 insert_header(&mut headers, SESSION_ID_HEADER, session_id)?;
60
61 Ok(RestClient {
62 http: self.http.clone(),
63 base_url: self.base_url.clone(),
64 headers,
65 signer: None,
66 })
67 }
68
69 pub(super) fn with_signer(&self, signer: MacSigner) -> RestClient {
72 RestClient {
73 http: self.http.clone(),
74 base_url: self.base_url.clone(),
75 headers: self.headers.clone(),
76 signer: Some(signer),
77 }
78 }
79
80 pub(super) async fn post_json<T: DeserializeOwned>(
82 &self,
83 endpoint: &str,
84 body: Value,
85 ) -> Result<T, OnePasswordError> {
86 self.request_json(Method::POST, endpoint, Some(&body)).await
87 }
88
89 pub(super) async fn put<T: DeserializeOwned>(
91 &self,
92 endpoint: &str,
93 ) -> Result<T, OnePasswordError> {
94 self.request_json(Method::PUT, endpoint, None).await
95 }
96
97 pub(super) async fn get_encrypted_json<T: DeserializeOwned>(
99 &self,
100 endpoint: &str,
101 session_key: &AesKey,
102 ) -> Result<T, OnePasswordError> {
103 let envelope = self.request_json(Method::GET, endpoint, None).await?;
104 decrypt_response(envelope, session_key)
105 }
106
107 pub(super) async fn post_encrypted_json<T: DeserializeOwned>(
109 &self,
110 endpoint: &str,
111 params: Value,
112 session_key: &AesKey,
113 ) -> Result<T, OnePasswordError> {
114 let payload = serde_json::to_vec(¶ms)
115 .map_err(|_| OnePasswordError::Internal("failed to serialize request".into()))?;
116
117 let mut iv = [0u8; IV_SIZE];
118 bitwarden_random::rng().fill_bytes(&mut iv);
119 let envelope = session_key.encrypt(&payload, &iv)?;
120 let body = serde_json::to_value(&envelope)
121 .map_err(|_| OnePasswordError::Internal("failed to serialize envelope".into()))?;
122
123 let response = self
124 .request_json(Method::POST, endpoint, Some(&body))
125 .await?;
126 decrypt_response(response, session_key)
127 }
128
129 async fn request_json<T: DeserializeOwned>(
131 &self,
132 method: Method,
133 endpoint: &str,
134 body: Option<&Value>,
135 ) -> Result<T, OnePasswordError> {
136 let text = self.request(method, endpoint, body).await?;
137 deserialize(text.as_bytes())
138 }
139
140 async fn request(
141 &self,
142 method: Method,
143 endpoint: &str,
144 body: Option<&Value>,
145 ) -> Result<String, OnePasswordError> {
146 let url = format!("{}/{}", self.base_url, endpoint);
147
148 let mut builder = self
149 .http
150 .request(method.clone(), &url)
151 .headers(self.headers.clone());
152 if let Some(body) = body {
153 builder = builder.json(body);
154 }
155 if let Some(signer) = &self.signer {
156 builder = builder.header(MAC_HEADER, signer.sign(&url, method.as_str())?);
157 }
158
159 let response = builder
160 .send()
161 .await
162 .map_err(|e| OnePasswordError::Network(e.to_string()))?;
163 let status = response.status();
164 let text = response
165 .text()
166 .await
167 .map_err(|e| OnePasswordError::Network(e.to_string()))?;
168
169 if !status.is_success() {
170 return Err(parse_server_error(text.as_bytes()).unwrap_or_else(|| {
171 OnePasswordError::UnexpectedStatus {
172 endpoint: endpoint.to_string(),
173 status: status.as_u16(),
174 }
175 }));
176 }
177
178 Ok(text)
179 }
180}
181
182fn decrypt_response<T: DeserializeOwned>(
184 envelope: EncryptedEnvelope,
185 session_key: &AesKey,
186) -> Result<T, OnePasswordError> {
187 let plaintext = session_key.decrypt(&Encrypted::parse(&envelope)?)?;
188
189 if let Some(error) = parse_server_error(&plaintext) {
192 return Err(error);
193 }
194 deserialize(&plaintext)
195}
196
197fn parse_server_error(body: &[u8]) -> Option<OnePasswordError> {
199 if let Ok(error) = serde_json::from_slice::<ErrorResponse>(body) {
200 return Some(match error.code {
201 102 => OnePasswordError::BadCredentials,
202 117 => OnePasswordError::NotFound,
203 code => OnePasswordError::Internal(format!(
204 "the server responded with error code {code}: '{}'",
205 error.message
206 )),
207 });
208 }
209
210 if let Ok(failure) = serde_json::from_slice::<FailureReason>(body)
211 && !failure.reason.is_empty()
212 {
213 return Some(OnePasswordError::Internal(format!(
214 "the server responded with failure reason: '{}'",
215 failure.reason
216 )));
217 }
218
219 None
220}
221
222fn deserialize<T: DeserializeOwned>(body: &[u8]) -> Result<T, OnePasswordError> {
223 serde_json::from_slice(body).map_err(|_| OnePasswordError::Parse)
224}
225
226fn insert_header(
227 headers: &mut HeaderMap,
228 name: &'static str,
229 value: &str,
230) -> Result<(), OnePasswordError> {
231 let value = HeaderValue::from_str(value)
232 .map_err(|_| OnePasswordError::Internal(format!("invalid header value for '{name}'")))?;
233 headers.insert(HeaderName::from_static(name), value);
234 Ok(())
235}
236
237#[cfg(test)]
238mod tests {
239 use bitwarden_api_base::new_http_client;
240 use serde::Deserialize;
241 use serde_json::json;
242 use wiremock::{Mock, MockServer, ResponseTemplate, matchers};
243
244 use super::{super::opdata::decode64_loose, *};
245
246 #[derive(Debug, Deserialize)]
247 struct Greeting {
248 hello: String,
249 }
250
251 fn client(server: &MockServer) -> RestClient {
252 RestClient::new(
253 new_http_client(),
254 format!("http://{}/api", server.address()),
255 "1Password for Mac/81210036",
256 "1Password for Mac/81210036",
257 "op-user-agent",
258 )
259 .expect("valid headers")
260 }
261
262 fn session_key() -> AesKey {
263 AesKey::new(
264 "SESSION",
265 decode64_loose("WyICHHlP5lPigZUGZYoivbJMqgHjSti86UKwdjCryYM").expect("valid key"),
266 )
267 }
268
269 #[test]
270 fn parses_error_bodies() {
271 let error = parse_server_error(br#"{"errorCode":102,"errorMessage":"nope"}"#)
272 .expect("recognized error");
273 assert!(matches!(error, OnePasswordError::BadCredentials));
274
275 let error = parse_server_error(br#"{"errorCode":117,"errorMessage":"gone"}"#)
276 .expect("recognized error");
277 assert!(matches!(error, OnePasswordError::NotFound));
278
279 let error = parse_server_error(br#"{"errorCode":401,"errorMessage":"no auth"}"#)
280 .expect("recognized error");
281 assert!(matches!(error, OnePasswordError::Internal(_)));
282 assert!(error.to_string().contains("401"));
283
284 let error = parse_server_error(br#"{"reason":"rate limited"}"#).expect("recognized error");
285 assert!(error.to_string().contains("rate limited"));
286 }
287
288 #[test]
289 fn ignores_non_error_bodies() {
290 assert!(parse_server_error(br#"{"status":"ok","sessionID":"S"}"#).is_none());
291 assert!(parse_server_error(br#"{"mfa":null}"#).is_none());
292 }
293
294 #[tokio::test]
295 async fn post_json_sends_identity_headers() {
296 let server = MockServer::start().await;
297 server
298 .register(
299 Mock::given(matchers::path("/api/v2/auth/methods"))
300 .and(matchers::method("POST"))
301 .and(matchers::header(
302 "x-agilebits-client",
303 "1Password for Mac/81210036",
304 ))
305 .and(matchers::body_json(json!({"email": "[email protected]"})))
306 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"hello": "you"})))
307 .expect(1),
308 )
309 .await;
310
311 let response: Greeting = client(&server)
312 .post_json("v2/auth/methods", json!({"email": "[email protected]"}))
313 .await
314 .expect("request succeeds");
315
316 assert_eq!(response.hello, "you");
317 server.verify().await;
318 }
319
320 #[tokio::test]
321 async fn maps_error_responses_to_errors() {
322 let server = MockServer::start().await;
323 server
324 .register(
325 Mock::given(matchers::path("/api/v2/auth"))
326 .respond_with(ResponseTemplate::new(401).set_body_json(
327 json!({"errorCode": 102, "errorMessage": "bad credentials"}),
328 ))
329 .expect(1),
330 )
331 .await;
332
333 let error = client(&server)
334 .post_json::<Greeting>("v2/auth", json!({}))
335 .await
336 .expect_err("server rejects");
337
338 assert!(matches!(error, OnePasswordError::BadCredentials));
339 server.verify().await;
340 }
341
342 #[tokio::test]
343 async fn keeps_the_status_of_an_error_without_a_known_body() {
344 let server = MockServer::start().await;
345 server
346 .register(
347 Mock::given(matchers::path("/api/v2/auth/confirm-key"))
348 .respond_with(ResponseTemplate::new(401))
349 .expect(1),
350 )
351 .await;
352
353 let error = client(&server)
354 .post_json::<Greeting>("v2/auth/confirm-key", json!({}))
355 .await
356 .expect_err("server rejects");
357
358 assert!(matches!(
359 error,
360 OnePasswordError::UnexpectedStatus { ref endpoint, status: 401 }
361 if endpoint == "v2/auth/confirm-key"
362 ));
363 server.verify().await;
364 }
365
366 #[tokio::test]
367 async fn signs_requests_once_a_signer_is_attached() {
368 let server = MockServer::start().await;
369 server
370 .register(
371 Mock::given(matchers::path("/api/v1/auth/verify"))
372 .and(matchers::header("x-agilebits-session-id", "SESSION"))
373 .and(matchers::header_regex("x-agilebits-mac", r"^v1\|\d+\|.+$"))
374 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"hello": "you"})))
375 .expect(1),
376 )
377 .await;
378
379 let key = session_key();
380 let rest = client(&server)
381 .with_session_id(&key.id)
382 .expect("valid session id")
383 .with_signer(MacSigner::new(&key));
384 let _: Greeting = rest
385 .post_json("v1/auth/verify", json!({}))
386 .await
387 .expect("request succeeds");
388
389 server.verify().await;
390 }
391
392 #[tokio::test]
393 async fn round_trips_an_encrypted_request() {
394 let key = session_key();
395 let response_body = {
396 let mut iv = [0u8; IV_SIZE];
397 bitwarden_random::rng().fill_bytes(&mut iv);
398 let envelope = key
399 .encrypt(br#"{"hello":"encrypted"}"#, &iv)
400 .expect("encrypts");
401 serde_json::to_value(&envelope).expect("serializes")
402 };
403
404 let server = MockServer::start().await;
405 server
406 .register(
407 Mock::given(matchers::path("/api/v1/auth/mfa"))
408 .and(matchers::method("POST"))
409 .and(matchers::body_partial_json(
411 json!({"kid": "SESSION", "enc": "A256GCM"}),
412 ))
413 .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
414 .expect(1),
415 )
416 .await;
417
418 let response: Greeting = client(&server)
419 .post_encrypted_json("v1/auth/mfa", json!({"totp": {"code": "123456"}}), &key)
420 .await
421 .expect("request succeeds");
422
423 assert_eq!(response.hello, "encrypted");
424 server.verify().await;
425 }
426
427 #[tokio::test]
428 async fn surfaces_errors_hidden_inside_an_encrypted_response() {
429 let key = session_key();
430 let response_body = {
431 let mut iv = [0u8; IV_SIZE];
432 bitwarden_random::rng().fill_bytes(&mut iv);
433 let envelope = key
434 .encrypt(br#"{"errorCode":102,"errorMessage":"nope"}"#, &iv)
435 .expect("encrypts");
436 serde_json::to_value(&envelope).expect("serializes")
437 };
438
439 let server = MockServer::start().await;
440 server
441 .register(
442 Mock::given(matchers::path("/api/v1/auth/mfa"))
443 .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
444 .expect(1),
445 )
446 .await;
447
448 let error = client(&server)
449 .post_encrypted_json::<Greeting>("v1/auth/mfa", json!({}), &key)
450 .await
451 .expect_err("encrypted error is surfaced");
452
453 assert!(matches!(error, OnePasswordError::BadCredentials));
454 server.verify().await;
455 }
456}