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::Internal(format!(
172 "unexpected response from '{endpoint}' (HTTP {})",
173 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 signs_requests_once_a_signer_is_attached() {
344 let server = MockServer::start().await;
345 server
346 .register(
347 Mock::given(matchers::path("/api/v1/auth/verify"))
348 .and(matchers::header("x-agilebits-session-id", "SESSION"))
349 .and(matchers::header_regex("x-agilebits-mac", r"^v1\|\d+\|.+$"))
350 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"hello": "you"})))
351 .expect(1),
352 )
353 .await;
354
355 let key = session_key();
356 let rest = client(&server)
357 .with_session_id(&key.id)
358 .expect("valid session id")
359 .with_signer(MacSigner::new(&key));
360 let _: Greeting = rest
361 .post_json("v1/auth/verify", json!({}))
362 .await
363 .expect("request succeeds");
364
365 server.verify().await;
366 }
367
368 #[tokio::test]
369 async fn round_trips_an_encrypted_request() {
370 let key = session_key();
371 let response_body = {
372 let mut iv = [0u8; IV_SIZE];
373 bitwarden_random::rng().fill_bytes(&mut iv);
374 let envelope = key
375 .encrypt(br#"{"hello":"encrypted"}"#, &iv)
376 .expect("encrypts");
377 serde_json::to_value(&envelope).expect("serializes")
378 };
379
380 let server = MockServer::start().await;
381 server
382 .register(
383 Mock::given(matchers::path("/api/v1/auth/mfa"))
384 .and(matchers::method("POST"))
385 .and(matchers::body_partial_json(
387 json!({"kid": "SESSION", "enc": "A256GCM"}),
388 ))
389 .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
390 .expect(1),
391 )
392 .await;
393
394 let response: Greeting = client(&server)
395 .post_encrypted_json("v1/auth/mfa", json!({"totp": {"code": "123456"}}), &key)
396 .await
397 .expect("request succeeds");
398
399 assert_eq!(response.hello, "encrypted");
400 server.verify().await;
401 }
402
403 #[tokio::test]
404 async fn surfaces_errors_hidden_inside_an_encrypted_response() {
405 let key = session_key();
406 let response_body = {
407 let mut iv = [0u8; IV_SIZE];
408 bitwarden_random::rng().fill_bytes(&mut iv);
409 let envelope = key
410 .encrypt(br#"{"errorCode":102,"errorMessage":"nope"}"#, &iv)
411 .expect("encrypts");
412 serde_json::to_value(&envelope).expect("serializes")
413 };
414
415 let server = MockServer::start().await;
416 server
417 .register(
418 Mock::given(matchers::path("/api/v1/auth/mfa"))
419 .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
420 .expect(1),
421 )
422 .await;
423
424 let error = client(&server)
425 .post_encrypted_json::<Greeting>("v1/auth/mfa", json!({}), &key)
426 .await
427 .expect_err("encrypted error is surfaced");
428
429 assert!(matches!(error, OnePasswordError::BadCredentials));
430 server.verify().await;
431 }
432}