Skip to main content

bitwarden_importers/importers/onepassword/access/
rest.rs

1//! reqwest wrapper: identity headers, the MAC signing hook, encrypted GET/POST, and error parsing.
2
3use 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
25/// An HTTP client bound to a base URL, carrying the 1Password identity headers and an optional MAC
26/// signer. Requests are signed automatically once a signer is attached.
27pub(super) struct RestClient {
28    http: reqwest::Client,
29    base_url: String,
30    headers: HeaderMap,
31    signer: Option<MacSigner>,
32}
33
34impl RestClient {
35    /// Builds the base client with the identity headers derived from `ClientInfo`.
36    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    /// Derives a client for another API root. It keeps the transport and the headers, but does not
57    /// sign its requests, as the signer belongs to the receiver.
58    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    /// Derives a client that adds the session id header.
68    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    /// Derives a client that signs every request. The signer carries the session id already, so
81    /// this keeps whatever headers the receiver has.
82    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    /// POSTs a JSON body and parses the JSON response.
92    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    /// POSTs a JSON body and parses the JSON response, reporting an empty success body as `None`.
101    ///
102    /// Some endpoints answer 200 with an empty body while the result is not ready yet, which must
103    /// not be mistaken for a parse error.
104    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    /// POSTs a JSON body and parses the JSON response, or returns `None` when the server answers
117    /// with `status`, whatever the body.
118    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    /// PUTs a JSON body and parses the JSON response.
133    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    /// PUTs with no body and parses the JSON response.
142    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    /// DELETEs a JSON body. The response carries nothing to parse and errors surface for the
150    /// caller to ignore.
151    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    /// GETs an opdata envelope, decrypts it, and parses the JSON plaintext.
162    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    /// Encrypts `params`, POSTs the opdata envelope, then decrypts and parses the response.
172    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(&params)
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    /// Sends a request and parses the JSON response.
194    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    /// Sends a request and returns the status and the body, whatever the status.
216    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
249/// Turns an error status into the error its body describes.
250fn 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
263/// Decrypts an opdata envelope and parses its JSON plaintext.
264fn 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    // A decrypted payload can still be a server error: some errors come back HTTP 200 with the
271    // error object encrypted.
272    if let Some(error) = parse_server_error(&plaintext) {
273        return Err(error);
274    }
275    deserialize(&plaintext)
276}
277
278/// Maps a 1Password error body to an `OnePasswordError`, or `None` when the body is not an error.
279fn 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                    // The request body is an opdata envelope, not the plaintext params.
491                    .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}