ncryptf/client/
request.rs

1use std::fmt;
2
3use base64::{engine::general_purpose, Engine as _};
4use chrono::Utc;
5use reqwest::{
6    header::{HeaderMap, HeaderValue},
7    RequestBuilder,
8};
9use thiserror::Error;
10
11#[derive(Error, Debug)]
12pub enum RequestError {
13    #[error("reqwest failed")]
14    ReqwestError(#[from] reqwest::Error),
15    #[error("unable to create authorization")]
16    AuthConstructionError,
17    #[error("bootstrapping encrypted requst failed.")]
18    ReKeyError,
19    #[error("handling the response failed")]
20    HandlingResponse(#[from] crate::client::ResponseError),
21    #[error("the argument provided was not one that can be handled")]
22    InvalidArgument,
23    #[error("the request could not be encrypted")]
24    EncryptionError,
25    #[error("the token provided has expired, and could not be renewed")]
26    TokenExpired,
27}
28
29/// The client request simplifies creating, sending, and handling an ncryptf request and response by providing a
30/// simplified API that utilizes reqwest underneath.
31///
32/// Requests can be constructed by calling:
33///
34/// ```rust
35/// let mut request = ncryptf::client::Request::<T>::new(client, "https://www.ncryptf.com", Some(ncryptf::Token), Some(T));
36/// ```
37/// Where `T` is an implementation of `UpdateTokenTrait`, which provides an essential function for handling refresh tokens.
38/// When the Token object is updated, `UpdateTokenTrait::token_update` will be called with the new token for you to handle.
39/// If you wish to handle this separatedly, you can use the `UpdateTokenImpl` dummy trait.
40///
41/// and then use the helper http verb methods to make an request, which will automatically handle setting up an encrypted request
42/// for you which includes bootstraping a new encryption key from a compliant server, and encrypting the request with a one-time encryption key
43/// that is thrown away at the end of the request
44///
45/// ```rust
46/// let response: ncryptf::Client::Response = request.get("/user/1").await.unwrap();
47/// let response: ncryptf::Client::Response = request.delete("/user/1").await.unwrap();
48/// let response: ncryptf::Client::Response = request.post("/user", "{ ... json ...}").await.unwrap();
49/// let response: ncryptf::Client::Response = request.put("/user/1", "{ .. json ..}").await.unwrap();
50/// ```
51///
52/// > NOTE: Only GET, DELETE, POST, PATHCH, and PUT verbs are supported for this client library -- you likely do not need to have an encrypted HEAD, or OPTIONS for an API.
53///
54/// An `ncryptf::Client::Response` is emitted on success. The response automatically handles decrypting the response for your application.
55#[derive(Debug, Clone)]
56pub struct Request<UT, RT>
57where
58    UT: UpdateTokenTrait,
59    RT: RequestTrait,
60{
61    pub client: reqwest::Client,
62    pub endpoint: String,
63    pub token: Option<crate::Token>,
64    pub ut: Option<UT>,
65    pub rt: Option<RT>,
66    ek: Option<crate::rocket::ExportableEncryptionKeyData>,
67}
68
69#[derive(Debug, Clone)]
70pub enum Method {
71    Get,
72    Post,
73    Put,
74    Patch,
75    Delete,
76}
77
78impl fmt::Display for Method {
79    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
80        write!(f, "{:?}", self)
81    }
82}
83
84pub trait UpdateTokenTrait: Send + Sync {
85    /// Provides a post-callback // token update mechansim that can be controlled by the caller
86    /// Necessary for Token refresh implementation
87    fn token_update(&self, _token: crate::Token) -> bool {
88        return true;
89    }
90}
91
92pub trait RequestTrait: Send + Sync {
93    /// Modify the request before it is sent
94    fn before(&self, builder: RequestBuilder) -> RequestBuilder {
95        return builder;
96    }
97
98    /// Run a task after the request is sent
99    fn after(&self, _response: crate::client::Response) {
100        return;
101    }
102}
103
104impl<UT: UpdateTokenTrait, RT: RequestTrait> Request<UT, RT> {
105    /// Constructs a new request
106    pub fn new_simple(
107        client: reqwest::Client,
108        endpoint: &str,
109        token: Option<crate::Token>,
110    ) -> Self {
111        return Self::new(client, endpoint, token, None, None);
112    }
113
114    /// Constructs a new request
115    pub fn new(
116        client: reqwest::Client,
117        endpoint: &str,
118        token: Option<crate::Token>,
119        ut: Option<UT>,
120        rt: Option<RT>,
121    ) -> Self {
122        Self {
123            client,
124            endpoint: endpoint.to_string(),
125            token,
126            ut,
127            rt,
128            ek: None,
129        }
130    }
131
132    /// Updates the token in both the current instance and via the callback
133    pub fn update_token(&mut self, token: Option<crate::Token>) {
134        self.token = token.clone();
135
136        match &self.ut {
137            Some(callback) => match token {
138                Some(token) => {
139                    callback.token_update(token);
140                }
141                None => {}
142            },
143            None => {}
144        };
145    }
146
147    /// This will bootstrap our request and get the necessary encryption keys to encrypt the request
148    /// and decrypt the response
149    /// This function is recursive, and will call itself until it ensures the underlying data is encrypted and non-readable
150    #[async_recursion::async_recursion]
151    pub async fn rekey(&mut self, hashid: Option<String>) -> Result<bool, RequestError> {
152        let kp = crate::Keypair::new();
153        let mut headers = HeaderMap::new();
154        headers.insert(
155            "Content-Type",
156            HeaderValue::from_str(&"application/json").unwrap(),
157        );
158
159        match hashid.clone() {
160            Some(hashid) => {
161                headers.insert(
162                    "Accept",
163                    HeaderValue::from_str(&"application/vnd.ncryptf+json").unwrap(),
164                );
165                headers.insert("X-HashId", HeaderValue::from_str(&hashid).unwrap());
166                let pk = general_purpose::STANDARD.encode(kp.get_public_key());
167                headers.insert("X-PubKey", HeaderValue::from_str(&pk).unwrap());
168            }
169            _ => {
170                headers.insert(
171                    "Accept",
172                    HeaderValue::from_str(&"application/json").unwrap(),
173                );
174            }
175        };
176
177        let furi = format!("{}{}", self.endpoint, "/ncryptf/ek");
178        let builder = self.client.clone().get(furi).headers(headers);
179
180        match self.do_request(builder, kp).await {
181            Ok(response) => match response.status {
182                reqwest::StatusCode::OK => match serde_json::from_str::<
183                    crate::rocket::ExportableEncryptionKeyData,
184                >(&response.body.unwrap())
185                {
186                    Ok(ek) => {
187                        self.ek = Some(ek.clone());
188                        match hashid.clone() {
189                            Some(_) => return Ok(true),
190                            _ => return self.rekey(Some(ek.hash_id)).await,
191                        }
192                    }
193                    Err(_error) => return Err(RequestError::ReKeyError),
194                },
195                _ => return Err(RequestError::ReKeyError),
196            },
197            Err(_error) => return Err(RequestError::ReKeyError),
198        };
199    }
200
201    /// Performs an HTTP GET request
202    pub async fn get(&mut self, url: &str) -> Result<crate::client::Response, RequestError> {
203        return self.execute(Method::Get, url, None).await;
204    }
205
206    /// Performs an HTTP DELETE request
207    pub async fn delete(
208        &mut self,
209        url: &str,
210        payload: Option<&str>,
211    ) -> Result<crate::client::Response, RequestError> {
212        return self.execute(Method::Delete, url, payload).await;
213    }
214
215    /// Performs an HTTP PATCH request
216    pub async fn patch(
217        &mut self,
218        url: &str,
219        payload: Option<&str>,
220    ) -> Result<crate::client::Response, RequestError> {
221        return self.execute(Method::Patch, url, payload).await;
222    }
223
224    /// Performs an HTTP POST request
225    pub async fn post(
226        &mut self,
227        url: &str,
228        payload: Option<&str>,
229    ) -> Result<crate::client::Response, RequestError> {
230        return self.execute(Method::Post, url, payload).await;
231    }
232
233    /// Performs an HTTP PUT request
234    pub async fn put(
235        &mut self,
236        url: &str,
237        payload: Option<&str>,
238    ) -> Result<crate::client::Response, RequestError> {
239        return self.execute(Method::Put, url, payload).await;
240    }
241
242    ///  Executes a request
243    ///
244    /// If a token is provided, the request is assumed to require authentication and the appropriate auth header is added
245    /// GET requets are assumed to expect an encrypted response
246    /// This will bootstrap the encryption process if necessary for an ncryptf encrypted response
247    ///
248    /// AsyncRecursion is to prevent Rust Compiler from detecting a loop - this method is not recursive.
249    #[async_recursion::async_recursion]
250    async fn execute(
251        &mut self,
252        method: Method,
253        url: &str,
254        payload: Option<&'async_recursion str>,
255    ) -> Result<crate::client::Response, RequestError> {
256        let payload_actual = match payload {
257            Some(payload) => payload,
258            None => "",
259        };
260
261        match &self.ek {
262            Some(ek) => {
263                if ek.is_expired() {
264                    match self.rekey(None).await {
265                        Ok(_) => {}
266                        Err(error) => return Err(error),
267                    };
268                }
269            }
270            _ => match self.rekey(None).await {
271                Ok(_) => {}
272                Err(error) => return Err(error),
273            },
274        };
275
276        let auth: Option<crate::Authorization> = match self.token.clone() {
277            Some(mut token) => {
278                // If the token has, or is nearing expiry, attempt to refresh it
279                let expiration_limit = chrono::Utc::now().timestamp() + 120;
280                if token.expires_at <= expiration_limit {
281                    let refresh_token = token.refresh_token;
282                    // Throw away this token
283                    self.token = None;
284
285                    match self
286                        .post(
287                            format!("/ncryptf/token/refresh?refresh_token={}", refresh_token)
288                                .as_str(),
289                            None,
290                        )
291                        .await
292                    {
293                        Ok(response) => match response.status {
294                            reqwest::StatusCode::OK => match response.into::<crate::Token>() {
295                                Ok(tt) => {
296                                    self.update_token(Some(tt.clone()));
297                                    token = self.token.clone().unwrap();
298                                }
299                                Err(_error) => return Err(RequestError::TokenExpired),
300                            },
301                            _ => return Err(RequestError::TokenExpired),
302                        },
303                        Err(_error) => return Err(RequestError::TokenExpired),
304                    };
305                }
306
307                // For requests with tokens, attempt to generate an Authorization struct
308                match crate::Authorization::from(
309                    method.to_string().to_uppercase(),
310                    url.to_string().clone(),
311                    token.clone(),
312                    Utc::now(),
313                    payload_actual.to_string(),
314                    None,
315                    None,
316                ) {
317                    Ok(auth) => Some(auth),
318                    Err(_error) => return Err(RequestError::AuthConstructionError),
319                }
320            }
321            None => None,
322        };
323
324        let kp = crate::Keypair::new();
325
326        let mut headers = HeaderMap::new();
327        headers.insert(
328            "Accept",
329            HeaderValue::from_str(&"application/vnd.ncryptf+json").unwrap(),
330        );
331        // We always send the headers incase the request don't have a body
332        headers.insert(
333            "X-PubKey",
334            HeaderValue::from_str(&general_purpose::STANDARD.encode(kp.get_public_key())).unwrap(),
335        );
336        headers.insert(
337            "X-HashId",
338            HeaderValue::from_str(&self.ek.clone().unwrap().hash_id).unwrap(),
339        );
340
341        match auth {
342            Some(auth) => {
343                headers.insert(
344                    "Authorization",
345                    HeaderValue::from_str(auth.get_header().as_str()).unwrap(),
346                );
347            }
348            _ => {}
349        }
350
351        let furi = format!("{}{}", self.endpoint, url);
352        let mut builder: reqwest::RequestBuilder = match method {
353            Method::Get => self.client.clone().get(furi),
354            Method::Post => self.client.clone().post(furi),
355            Method::Put => self.client.clone().put(furi),
356            Method::Delete => self.client.clone().delete(furi),
357            Method::Patch => self.client.clone().patch(furi),
358        };
359
360        match payload_actual {
361            "" => {
362                headers.insert(
363                    "Content-Type",
364                    HeaderValue::from_str(&"application/json").unwrap(),
365                );
366            }
367            _ => {
368                headers.insert(
369                    "Content-Type",
370                    HeaderValue::from_str(&"application/vnd.ncryptf+json").unwrap(),
371                );
372                let sk = match self.token.clone() {
373                    Some(token) => token.signature,
374                    None => {
375                        let sk = crate::Signature::new();
376                        sk.get_secret_key()
377                    }
378                };
379
380                let mut request = crate::Request::from(kp.get_secret_key(), sk).unwrap();
381                match request.encrypt(
382                    payload_actual.to_string(),
383                    self.ek.as_ref().unwrap().clone().get_public_key().unwrap(),
384                ) {
385                    Ok(body) => {
386                        builder = builder.body(general_purpose::STANDARD.encode(body));
387                    }
388                    Err(_error) => return Err(RequestError::EncryptionError),
389                }
390            }
391        }
392
393        // Execute any before request implementation
394        builder = match &self.rt {
395            Some(rt) => rt.before(builder),
396            None => builder,
397        };
398        builder = builder.headers(headers);
399
400        match self.do_request(builder, kp).await {
401            Ok(response) => match &self.rt {
402                Some(rt) => {
403                    rt.after(response.clone());
404                    return Ok(response);
405                }
406                None => return Ok(response),
407            },
408            Err(error) => return Err(error),
409        };
410    }
411
412    /// Internal method to perform the http request
413    async fn do_request(
414        &mut self,
415        builder: reqwest::RequestBuilder,
416        kp: crate::Keypair,
417    ) -> Result<crate::client::Response, RequestError> {
418        match builder.send().await {
419            Ok(response) => {
420                // If the key is ephemeral or expired, we need to purge it so future requests don't use it
421                // We can handle re-keying on the next request
422                if self.ek.is_some() {
423                    if self.ek.clone().unwrap().ephemeral || self.ek.clone().unwrap().is_expired() {
424                        self.ek = None;
425                    }
426                }
427
428                let result = match crate::client::Response::new(response, kp.get_secret_key()).await
429                {
430                    Ok(response) => response,
431                    Err(error) => return Err(RequestError::HandlingResponse(error)),
432                };
433
434                // Opportunistically update the encryption key headers
435                let hash_id = self.get_header_by_name(result.headers.get("x-hashid"));
436                let expires_at =
437                    self.get_header_by_name(result.headers.get("x-public-key-expiration"));
438                let public_key = self.get_key_string_by_result_or_header(
439                    result.pk.clone(),
440                    result.headers.get("x-public-key"),
441                );
442                let signature_key = self.get_key_string_by_result_or_header(
443                    result.sk.clone(),
444                    result.headers.get("x-signature-key"),
445                );
446                if hash_id.is_some()
447                    && expires_at.is_some()
448                    && public_key.is_some()
449                    && signature_key.is_some()
450                {
451                    let xp = expires_at.unwrap().parse::<i64>();
452                    if xp.is_ok() {
453                        self.ek = Some(crate::rocket::ExportableEncryptionKeyData {
454                            public: public_key.unwrap(),
455                            signature: signature_key.unwrap(),
456                            hash_id: hash_id.unwrap(),
457                            ephemeral: false,
458                            expires_at: xp.unwrap(),
459                        });
460                    }
461                }
462
463                return Ok(result);
464            }
465            Err(error) => Err(RequestError::ReqwestError(error)),
466        }
467    }
468
469    /// Helper method to get the key material from either the response body or the headers
470    fn get_key_string_by_result_or_header(
471        &self,
472        key: Option<Vec<u8>>,
473        header: Option<&HeaderValue>,
474    ) -> Option<String> {
475        match key {
476            // If we have a key from the response, base64 encode and return it
477            Some(key) => Some(general_purpose::STANDARD.encode(key)),
478            // If we don't have a key check the header
479            None => match header {
480                Some(header) => match header.to_str() {
481                    // The header will already be base64 encoded, return it directly.
482                    Ok(s) => Some(s.to_string()),
483                    Err(_) => None,
484                },
485                None => None,
486            },
487        }
488    }
489
490    /// Helper method to grab a given header by its name
491    fn get_header_by_name(&self, header: Option<&HeaderValue>) -> Option<String> {
492        match header {
493            Some(h) => match h.to_str() {
494                Ok(s) => Some(s.to_string()),
495                Err(_) => None,
496            },
497            None => None,
498        }
499    }
500}