1use rand::Rng;
4use reqwest::{
5 Method, StatusCode,
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_base_url(&self, base_url: impl Into<String>) -> RestClient {
59 RestClient {
60 http: self.http.clone(),
61 base_url: base_url.into(),
62 headers: self.headers.clone(),
63 signer: None,
64 }
65 }
66
67 pub(super) fn with_session_id(&self, session_id: &str) -> Result<RestClient, OnePasswordError> {
69 let mut headers = self.headers.clone();
70 insert_header(&mut headers, SESSION_ID_HEADER, session_id)?;
71
72 Ok(RestClient {
73 http: self.http.clone(),
74 base_url: self.base_url.clone(),
75 headers,
76 signer: None,
77 })
78 }
79
80 pub(super) fn with_signer(&self, signer: MacSigner) -> RestClient {
83 RestClient {
84 http: self.http.clone(),
85 base_url: self.base_url.clone(),
86 headers: self.headers.clone(),
87 signer: Some(signer),
88 }
89 }
90
91 pub(super) async fn post_json<T: DeserializeOwned>(
93 &self,
94 endpoint: &str,
95 body: Value,
96 ) -> Result<T, OnePasswordError> {
97 self.request_json(Method::POST, endpoint, Some(&body)).await
98 }
99
100 pub(super) async fn post_json_or_empty<T: DeserializeOwned>(
105 &self,
106 endpoint: &str,
107 body: Value,
108 ) -> Result<Option<T>, OnePasswordError> {
109 let text = self.request(Method::POST, endpoint, Some(&body)).await?;
110 if text.trim().is_empty() {
111 return Ok(None);
112 }
113 deserialize(text.as_bytes()).map(Some)
114 }
115
116 pub(super) async fn post_json_unless_status<T: DeserializeOwned>(
119 &self,
120 endpoint: &str,
121 body: Value,
122 status: StatusCode,
123 ) -> Result<Option<T>, OnePasswordError> {
124 let (response_status, text) = self.send(Method::POST, endpoint, Some(&body)).await?;
125 if response_status == status {
126 return Ok(None);
127 }
128 check_status(endpoint, response_status, &text)?;
129 deserialize(text.as_bytes()).map(Some)
130 }
131
132 pub(super) async fn put_json<T: DeserializeOwned>(
134 &self,
135 endpoint: &str,
136 body: Value,
137 ) -> Result<T, OnePasswordError> {
138 self.request_json(Method::PUT, endpoint, Some(&body)).await
139 }
140
141 pub(super) async fn put<T: DeserializeOwned>(
143 &self,
144 endpoint: &str,
145 ) -> Result<T, OnePasswordError> {
146 self.request_json(Method::PUT, endpoint, None).await
147 }
148
149 pub(super) async fn delete_json(
152 &self,
153 endpoint: &str,
154 body: Value,
155 ) -> Result<(), OnePasswordError> {
156 self.request(Method::DELETE, endpoint, Some(&body))
157 .await
158 .map(|_| ())
159 }
160
161 pub(super) async fn get_encrypted_json<T: DeserializeOwned>(
163 &self,
164 endpoint: &str,
165 session_key: &AesKey,
166 ) -> Result<T, OnePasswordError> {
167 let envelope = self.request_json(Method::GET, endpoint, None).await?;
168 decrypt_response(envelope, session_key)
169 }
170
171 pub(super) async fn post_encrypted_json<T: DeserializeOwned>(
173 &self,
174 endpoint: &str,
175 params: Value,
176 session_key: &AesKey,
177 ) -> Result<T, OnePasswordError> {
178 let payload = serde_json::to_vec(¶ms)
179 .map_err(|_| OnePasswordError::Internal("failed to serialize request".into()))?;
180
181 let mut iv = [0u8; IV_SIZE];
182 bitwarden_random::rng().fill_bytes(&mut iv);
183 let envelope = session_key.encrypt(&payload, &iv)?;
184 let body = serde_json::to_value(&envelope)
185 .map_err(|_| OnePasswordError::Internal("failed to serialize envelope".into()))?;
186
187 let response = self
188 .request_json(Method::POST, endpoint, Some(&body))
189 .await?;
190 decrypt_response(response, session_key)
191 }
192
193 async fn request_json<T: DeserializeOwned>(
195 &self,
196 method: Method,
197 endpoint: &str,
198 body: Option<&Value>,
199 ) -> Result<T, OnePasswordError> {
200 let text = self.request(method, endpoint, body).await?;
201 deserialize(text.as_bytes())
202 }
203
204 async fn request(
205 &self,
206 method: Method,
207 endpoint: &str,
208 body: Option<&Value>,
209 ) -> Result<String, OnePasswordError> {
210 let (status, text) = self.send(method, endpoint, body).await?;
211 check_status(endpoint, status, &text)?;
212 Ok(text)
213 }
214
215 async fn send(
217 &self,
218 method: Method,
219 endpoint: &str,
220 body: Option<&Value>,
221 ) -> Result<(StatusCode, String), OnePasswordError> {
222 let url = format!("{}/{}", self.base_url, endpoint);
223
224 let mut builder = self
225 .http
226 .request(method.clone(), &url)
227 .headers(self.headers.clone());
228 if let Some(body) = body {
229 builder = builder.json(body);
230 }
231 if let Some(signer) = &self.signer {
232 builder = builder.header(MAC_HEADER, signer.sign(&url, method.as_str())?);
233 }
234
235 let response = builder
236 .send()
237 .await
238 .map_err(|e| OnePasswordError::Network(e.to_string()))?;
239 let status = response.status();
240 let text = response
241 .text()
242 .await
243 .map_err(|e| OnePasswordError::Network(e.to_string()))?;
244
245 Ok((status, text))
246 }
247}
248
249fn check_status(endpoint: &str, status: StatusCode, body: &str) -> Result<(), OnePasswordError> {
251 if status.is_success() {
252 return Ok(());
253 }
254
255 Err(
256 parse_server_error(body.as_bytes()).unwrap_or_else(|| OnePasswordError::UnexpectedStatus {
257 endpoint: endpoint.to_string(),
258 status: status.as_u16(),
259 }),
260 )
261}
262
263fn decrypt_response<T: DeserializeOwned>(
265 envelope: EncryptedEnvelope,
266 session_key: &AesKey,
267) -> Result<T, OnePasswordError> {
268 let plaintext = session_key.decrypt(&Encrypted::parse(&envelope)?)?;
269
270 if let Some(error) = parse_server_error(&plaintext) {
273 return Err(error);
274 }
275 deserialize(&plaintext)
276}
277
278fn parse_server_error(body: &[u8]) -> Option<OnePasswordError> {
280 if let Ok(error) = serde_json::from_slice::<ErrorResponse>(body) {
281 return Some(match error.code {
282 102 => OnePasswordError::BadCredentials,
283 117 => OnePasswordError::NotFound,
284 code => OnePasswordError::Internal(format!(
285 "the server responded with error code {code}: '{}'",
286 error.message
287 )),
288 });
289 }
290
291 if let Ok(failure) = serde_json::from_slice::<FailureReason>(body)
292 && !failure.reason.is_empty()
293 {
294 return Some(OnePasswordError::Internal(format!(
295 "the server responded with failure reason: '{}'",
296 failure.reason
297 )));
298 }
299
300 None
301}
302
303fn deserialize<T: DeserializeOwned>(body: &[u8]) -> Result<T, OnePasswordError> {
304 serde_json::from_slice(body).map_err(|_| OnePasswordError::Parse)
305}
306
307fn insert_header(
308 headers: &mut HeaderMap,
309 name: &'static str,
310 value: &str,
311) -> Result<(), OnePasswordError> {
312 let value = HeaderValue::from_str(value)
313 .map_err(|_| OnePasswordError::Internal(format!("invalid header value for '{name}'")))?;
314 headers.insert(HeaderName::from_static(name), value);
315 Ok(())
316}
317
318#[cfg(test)]
319mod tests {
320 use bitwarden_api_base::new_http_client;
321 use serde::Deserialize;
322 use serde_json::json;
323 use wiremock::{Mock, MockServer, ResponseTemplate, matchers};
324
325 use super::{super::opdata::decode64_loose, *};
326
327 #[derive(Debug, Deserialize)]
328 struct Greeting {
329 hello: String,
330 }
331
332 fn client(server: &MockServer) -> RestClient {
333 RestClient::new(
334 new_http_client(),
335 format!("http://{}/api", server.address()),
336 "1Password for Mac/81210036",
337 "1Password for Mac/81210036",
338 "op-user-agent",
339 )
340 .expect("valid headers")
341 }
342
343 fn session_key() -> AesKey {
344 AesKey::new(
345 "SESSION",
346 decode64_loose("WyICHHlP5lPigZUGZYoivbJMqgHjSti86UKwdjCryYM").expect("valid key"),
347 )
348 }
349
350 #[test]
351 fn parses_error_bodies() {
352 let error = parse_server_error(br#"{"errorCode":102,"errorMessage":"nope"}"#)
353 .expect("recognized error");
354 assert!(matches!(error, OnePasswordError::BadCredentials));
355
356 let error = parse_server_error(br#"{"errorCode":117,"errorMessage":"gone"}"#)
357 .expect("recognized error");
358 assert!(matches!(error, OnePasswordError::NotFound));
359
360 let error = parse_server_error(br#"{"errorCode":401,"errorMessage":"no auth"}"#)
361 .expect("recognized error");
362 assert!(matches!(error, OnePasswordError::Internal(_)));
363 assert!(error.to_string().contains("401"));
364
365 let error = parse_server_error(br#"{"reason":"rate limited"}"#).expect("recognized error");
366 assert!(error.to_string().contains("rate limited"));
367 }
368
369 #[test]
370 fn ignores_non_error_bodies() {
371 assert!(parse_server_error(br#"{"status":"ok","sessionID":"S"}"#).is_none());
372 assert!(parse_server_error(br#"{"mfa":null}"#).is_none());
373 }
374
375 #[tokio::test]
376 async fn post_json_sends_identity_headers() {
377 let server = MockServer::start().await;
378 server
379 .register(
380 Mock::given(matchers::path("/api/v2/auth/methods"))
381 .and(matchers::method("POST"))
382 .and(matchers::header(
383 "x-agilebits-client",
384 "1Password for Mac/81210036",
385 ))
386 .and(matchers::body_json(json!({"email": "[email protected]"})))
387 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"hello": "you"})))
388 .expect(1),
389 )
390 .await;
391
392 let response: Greeting = client(&server)
393 .post_json("v2/auth/methods", json!({"email": "[email protected]"}))
394 .await
395 .expect("request succeeds");
396
397 assert_eq!(response.hello, "you");
398 server.verify().await;
399 }
400
401 #[tokio::test]
402 async fn maps_error_responses_to_errors() {
403 let server = MockServer::start().await;
404 server
405 .register(
406 Mock::given(matchers::path("/api/v2/auth"))
407 .respond_with(ResponseTemplate::new(401).set_body_json(
408 json!({"errorCode": 102, "errorMessage": "bad credentials"}),
409 ))
410 .expect(1),
411 )
412 .await;
413
414 let error = client(&server)
415 .post_json::<Greeting>("v2/auth", json!({}))
416 .await
417 .expect_err("server rejects");
418
419 assert!(matches!(error, OnePasswordError::BadCredentials));
420 server.verify().await;
421 }
422
423 #[tokio::test]
424 async fn keeps_the_status_of_an_error_without_a_known_body() {
425 let server = MockServer::start().await;
426 server
427 .register(
428 Mock::given(matchers::path("/api/v2/auth/confirm-key"))
429 .respond_with(ResponseTemplate::new(401))
430 .expect(1),
431 )
432 .await;
433
434 let error = client(&server)
435 .post_json::<Greeting>("v2/auth/confirm-key", json!({}))
436 .await
437 .expect_err("server rejects");
438
439 assert!(matches!(
440 error,
441 OnePasswordError::UnexpectedStatus { ref endpoint, status: 401 }
442 if endpoint == "v2/auth/confirm-key"
443 ));
444 server.verify().await;
445 }
446
447 #[tokio::test]
448 async fn signs_requests_once_a_signer_is_attached() {
449 let server = MockServer::start().await;
450 server
451 .register(
452 Mock::given(matchers::path("/api/v1/auth/verify"))
453 .and(matchers::header("x-agilebits-session-id", "SESSION"))
454 .and(matchers::header_regex("x-agilebits-mac", r"^v1\|\d+\|.+$"))
455 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"hello": "you"})))
456 .expect(1),
457 )
458 .await;
459
460 let key = session_key();
461 let rest = client(&server)
462 .with_session_id(&key.id)
463 .expect("valid session id")
464 .with_signer(MacSigner::new(&key));
465 let _: Greeting = rest
466 .post_json("v1/auth/verify", json!({}))
467 .await
468 .expect("request succeeds");
469
470 server.verify().await;
471 }
472
473 #[tokio::test]
474 async fn round_trips_an_encrypted_request() {
475 let key = session_key();
476 let response_body = {
477 let mut iv = [0u8; IV_SIZE];
478 bitwarden_random::rng().fill_bytes(&mut iv);
479 let envelope = key
480 .encrypt(br#"{"hello":"encrypted"}"#, &iv)
481 .expect("encrypts");
482 serde_json::to_value(&envelope).expect("serializes")
483 };
484
485 let server = MockServer::start().await;
486 server
487 .register(
488 Mock::given(matchers::path("/api/v1/auth/mfa"))
489 .and(matchers::method("POST"))
490 .and(matchers::body_partial_json(
492 json!({"kid": "SESSION", "enc": "A256GCM"}),
493 ))
494 .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
495 .expect(1),
496 )
497 .await;
498
499 let response: Greeting = client(&server)
500 .post_encrypted_json("v1/auth/mfa", json!({"totp": {"code": "123456"}}), &key)
501 .await
502 .expect("request succeeds");
503
504 assert_eq!(response.hello, "encrypted");
505 server.verify().await;
506 }
507
508 #[tokio::test]
509 async fn surfaces_errors_hidden_inside_an_encrypted_response() {
510 let key = session_key();
511 let response_body = {
512 let mut iv = [0u8; IV_SIZE];
513 bitwarden_random::rng().fill_bytes(&mut iv);
514 let envelope = key
515 .encrypt(br#"{"errorCode":102,"errorMessage":"nope"}"#, &iv)
516 .expect("encrypts");
517 serde_json::to_value(&envelope).expect("serializes")
518 };
519
520 let server = MockServer::start().await;
521 server
522 .register(
523 Mock::given(matchers::path("/api/v1/auth/mfa"))
524 .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
525 .expect(1),
526 )
527 .await;
528
529 let error = client(&server)
530 .post_encrypted_json::<Greeting>("v1/auth/mfa", json!({}), &key)
531 .await
532 .expect_err("encrypted error is surfaced");
533
534 assert!(matches!(error, OnePasswordError::BadCredentials));
535 server.verify().await;
536 }
537
538 #[tokio::test]
539 async fn post_json_or_empty_reads_a_blank_body_as_none_and_parses_json() {
540 let server = MockServer::start().await;
541 for (path, body) in [
542 ("empty", ""),
543 ("blank", " "),
544 ("json", r#"{"hello":"you"}"#),
545 ] {
546 server
547 .register(
548 Mock::given(matchers::path(format!("/api/{path}")))
549 .respond_with(ResponseTemplate::new(200).set_body_string(body)),
550 )
551 .await;
552 }
553 let rest = client(&server);
554
555 for path in ["empty", "blank"] {
556 let response: Option<Greeting> = rest
557 .post_json_or_empty(path, json!({}))
558 .await
559 .expect("a blank body is not an error");
560 assert!(response.is_none());
561 }
562 let response: Option<Greeting> = rest
563 .post_json_or_empty("json", json!({}))
564 .await
565 .expect("request succeeds");
566 assert_eq!(response.expect("the body is not blank").hello, "you");
567 }
568}