ncryptf/rocket/
json.rs

1use std::{error, fmt, io};
2
3use base64::{engine::general_purpose, Engine as _};
4use crate::shared::EncryptionKey;
5use super::{
6    CacheWrapper, RequestPublicKey, RequestSigningPublicKey
7};
8
9use crate::shared::NCRYPTF_CONTENT_TYPE;
10use anyhow::anyhow;
11use rocket::{
12    data::{FromData, Limits, Outcome},
13    http::{ContentType, Header, Status},
14    response::{self, Responder, Response},
15    Data, Request, State,
16};
17
18use serde::{Deserialize, Serialize};
19
20// Error returned by the [`Json`] guard when JSON deserialization fails.
21#[derive(Debug)]
22pub enum Error<'a> {
23    /// An I/O error occurred while reading the incoming request data.
24    Io(io::Error),
25    /// Parser failure
26    Parse(&'a str, serde_json::error::Error),
27}
28
29impl<'a> fmt::Display for Error<'a> {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        match self {
32            Self::Io(err) => write!(f, "i/o error: {}", err),
33            Self::Parse(_, err) => write!(f, "parse error: {}", err),
34        }
35    }
36}
37
38impl<'a> error::Error for Error<'a> {
39    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
40        match self {
41            Self::Io(err) => Some(err),
42            Self::Parse(_, err) => Some(err),
43        }
44    }
45}
46
47/// ncryptf::rocket::Json represents a application/vnd.ncryptf+json, JSON string
48/// The JSON struct supports both serialization and de-serialization to reduce implementation time within your application
49///
50/// ### Usage
51/// Encryption keys and identifiers are stored in Redis. Make sure you have a `rocket_db_pool::Config` setup
52/// for Redis, and added to your rocket figment()
53/// You do not have to setup a RequestGuard for Redis, this just uses the same configuration
54/// ```rust
55/// .merge(("databases.cache", rocket_db_pools::Config {
56///         url: format!("redis://127.0.0.1:6379/"),
57///         min_connections: None,
58///         max_connections: 1024,
59///         connect_timeout: 3,
60///         idle_timeout: None,
61///     }))
62/// ````
63///
64/// Next, create a struct to represent the request data. This struct MUST implement Serialize if you want to return a ncryptf encrypted response
65/// ```rust
66///  use rocket::serde::{Serialize, json::Json};
67///
68///  [derive(Serialize)]
69///  #[serde(crate = "rocket::serde")]
70///  struct TestStruct<'r> {
71///      pub hello: &'r str
72///  }
73/// ```
74///
75/// our request can now be parsed using data tags.
76/// Responses can be automatically converted into an JSON encrypted ncryptf response by returning `ncryptf::rocket::Json<T>`
77///
78///  If your header is application/vnd.ncryptf+json, returning a ncryptf::rocket::Json<T> will return an encrypted response
79///  If the header is an application/json (or anything else), ncryptf::rocket::Json<T> will return a rocket::serde::json::Json<T> equivalent JSON response in plain text
80///
81/// ```rust
82///     #[post("/echo", data="<data>")]
83///     fn echo(data: ncryptf::rocket::Json<TestStruct>) -> ncryptf::rocket::Json<TestStruct> {
84///         // data.0 is your TestStruct
85///         //
86///         return ncryptf::rocket::Json(data.0);
87///     }
88/// ```
89///
90///
91/// nryptf::rocket::Json<T>` supercedes rocket::serde::json::Json<T> for both encrypted messages, and regular JSON. If you intend to use
92///  the `Authorization` trait to authenticate users, you MUST receive and return this for your data type
93#[derive(Debug, Clone)]
94pub struct Json<T>(pub T);
95
96impl<T> Json<T> {
97    /// Returns the underlying serde_json::Value object
98    pub fn into_inner(self) -> T {
99        return self.0;
100    }
101
102    /// Returns a serde_json::Value wrapped as an ncryptf::Json representation
103    pub fn from_value(value: T) -> Self {
104        return Self(value);
105    }
106
107    /// Deserializes the request sting into a raw JSON string
108    pub fn deserialize_req_from_string<'r>(
109        req: &'r Request<'_>,
110        string: String,
111        cache: &CacheWrapper,
112    ) -> Result<String, Error<'r>> {
113        match req.headers().get_one("Content-Type") {
114            Some(h) => {
115                match h {
116                    NCRYPTF_CONTENT_TYPE => {
117                        // Convert the base64 payload into Vec<u8>
118                        let data = general_purpose::STANDARD.decode(string).unwrap();
119
120                        // Retrieve the HashID header
121                        let hash_id = match req.headers().get_one("X-HashId") {
122                            Some(h) => h,
123                            None => {
124                                return Err(Error::Io(io::Error::new(
125                                    io::ErrorKind::Other,
126                                    "Missing client provided hash identifier.",
127                                )));
128                            }
129                        };
130
131                        // Retrieve the encryption key from cache
132                        let ek = match cache.get(hash_id) {
133                            Some(ek) => ek,
134                            None => {
135                                return Err(Error::Io(io::Error::new(
136                                    io::ErrorKind::Other,
137                                    "Encryption key is either invalid, or may have expired.",
138                                )));
139                            }
140                        };
141
142                        // Retrieve the secret key
143                        let sk = ek.get_box_kp().get_secret_key();
144
145                        // Delete the key if it is ephemeral
146                        if ek.is_ephemeral() {
147                            cache.remove(hash_id);
148                        }
149
150                        // Decrypt the response, then deserialize the underlying JSON into the requested struct
151                        match crate::Response::from(sk) {
152                            Ok(response) => {
153                                // Pull the public key from the response then store it in the local cache so we can re-use it later
154                                // We need this in order to send an encrypted response back since Responder::respond_to
155                                // won't let us read the request
156                                match crate::Response::get_public_key_from_response(data.clone()) {
157                                    Ok(cpk) => {
158                                        req.local_cache(|| {
159                                            return RequestPublicKey(cpk.clone());
160                                        });
161                                    }
162                                    Err(error) => {
163                                        return Err(Error::Io(io::Error::new(
164                                            io::ErrorKind::Other,
165                                            error.to_string(),
166                                        )));
167                                    }
168                                };
169
170                                // Extract the signature key
171                                match crate::Response::get_signing_public_key_from_response(
172                                    data.clone(),
173                                ) {
174                                    Ok(cpk) => {
175                                        req.local_cache(|| {
176                                            return RequestSigningPublicKey(cpk.clone());
177                                        });
178                                    }
179                                    Err(error) => {
180                                        return Err(Error::Io(io::Error::new(
181                                            io::ErrorKind::Other,
182                                            error.to_string(),
183                                        )));
184                                    }
185                                };
186
187                                // Pull the public key and nonce from the headers if they are set.
188                                // If they are set they assume priority, and response.decrypt will likely decrypt this as a V1 response
189                                let public_key = match req.headers().get_one("X-PubKey") {
190                                    Some(h) => Some(h.as_bytes().to_vec()),
191                                    None => None,
192                                };
193
194                                let nonce = match req.headers().get_one("X-Nonce") {
195                                    Some(h) => Some(h.as_bytes().to_vec()),
196                                    None => None,
197                                };
198
199                                // Decrypt the request
200                                match response.decrypt(data.clone(), public_key, nonce) {
201                                    Ok(msg) => {
202                                        // Serialize this into a struct, and store the decrypted response in the cache
203                                        // We'll need this for the Authorization header
204                                        return Ok(req.local_cache(|| return msg).to_owned());
205                                    }
206                                    Err(error) => {
207                                        return Err(Error::Io(io::Error::new(
208                                            io::ErrorKind::Other,
209                                            error.to_string(),
210                                        )));
211                                    }
212                                };
213                            }
214                            Err(error) => {
215                                return Err(Error::Io(io::Error::new(
216                                    io::ErrorKind::Other,
217                                    error.to_string(),
218                                )));
219                            }
220                        };
221                    }
222                    // If this is a json request, return raw json
223                    "json" => {
224                        return Ok(req.local_cache(|| return string).to_owned());
225                    }
226                    // For now, return JSON even if another header was sent.
227                    _ => {
228                        return Ok(req.local_cache(|| return string).to_owned());
229                    }
230                }
231            }
232            // If there isn't an Accept header, also return JSON
233            None => {
234                return Ok(req.local_cache(|| return string).to_owned());
235            }
236        };
237    }
238}
239
240impl<'r, T: Deserialize<'r>> Json<T> {
241    pub fn from_str(s: &'r str) -> Result<Self, Error<'r>> {
242        return serde_json::from_str(s)
243            .map(Json)
244            .map_err(|e| Error::Parse(s, e));
245    }
246
247    pub async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Result<Self, Error<'r>> {
248        parse_body(req, data).await
249    }
250}
251
252#[rocket::async_trait]
253impl<'r, T: Deserialize<'r>> FromData<'r> for Json<T> {
254    type Error = Error<'r>;
255
256    async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> {
257        match Self::from_data(req, data).await {
258            Ok(value) => Outcome::Success(value),
259            Err(Error::Io(e)) if e.kind() == io::ErrorKind::UnexpectedEof => {
260                Outcome::Error((Status::PayloadTooLarge, Error::Io(e)))
261            }
262            Err(Error::Parse(s, e)) if e.classify() == serde_json::error::Category::Data => {
263                req.local_cache(|| return "".to_string());
264                Outcome::Error((Status::UnprocessableEntity, Error::Parse(s, e)))
265            }
266            Err(e) => Outcome::Error((Status::BadRequest, e)),
267        }
268    }
269}
270
271/// Allows for ncryptf::rocket::Json to be emitted as a responder to reduce code overhead
272/// This responder will handle encrypting the underlying request data correctly for all supported
273/// ncryptf versionn
274impl<'r, T: Serialize> Responder<'r, 'static> for Json<T> {
275    fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
276        match respond_to_with_ncryptf(self, Status::Ok, req) {
277            Ok(response) => response,
278            Err(_) => return Err(Status::InternalServerError),
279        }
280    }
281}
282
283/// ncryptf::rocket::JsonResponse is identical to ncryptf::rocket::Json<T> except that is also supports setting
284/// a status code on the response.
285///
286/// You may find this useful for:
287///     - Returning an common JSON structure for parsing in another clients / application
288///     - Returning an error struct if your request is not successful to inform the client on steps they can
289///        perform to resolve the error
290///
291/// The response with JsonResponse<T> will be identical to Json<T>. See Json<T> for more information
292#[derive(Debug, Clone)]
293pub struct JsonResponse<T> {
294    pub status: Status,
295    pub json: Json<T>,
296}
297
298impl<'r, T: Serialize> Responder<'r, 'static> for JsonResponse<T> {
299    fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
300        return match respond_to_with_ncryptf(self.json, self.status, req) {
301            Ok(response) => response,
302            Err(_) => return Err(Status::InternalServerError),
303        };
304    }
305}
306
307pub async fn parse_body<'r, T: Deserialize<'r>>(req: &'r Request<'_>, data: Data<'r>) -> Result<Json<T>, Error<'r>> {
308    let limit = req.limits().get("json").unwrap_or(Limits::JSON);
309    let string = match data.open(limit).into_string().await {
310        Ok(s) if s.is_complete() => s.into_inner(),
311        Ok(_) => {
312            let eof = io::ErrorKind::UnexpectedEof;
313            return Err(Error::Io(io::Error::new(eof, "data limit exceeded")));
314        }
315        Err(error) => return Err(Error::Io(error)),
316    };
317
318    // Get the cache from managed state
319    let cache = match req.rocket().state::<CacheWrapper>() {
320        Some(cache) => cache,
321        None => {
322            return Err(Error::Io(io::Error::new(
323                io::ErrorKind::Other,
324                "Cache not found in managed state",
325            )));
326        }
327    };
328
329    match Json::<T>::deserialize_req_from_string(req, string, cache) {
330        Ok(s) => {
331            return Json::<T>::from_str(req.local_cache(|| return s));
332        }
333        Err(error) => return Err(error),
334    };
335}
336
337pub fn respond_to_with_ncryptf<'r, 'a, T: serde::Serialize>(
338    m: Json<T>,
339    status: Status,
340    req: &'r Request<'_>,
341) -> Result<response::Result<'static>, anyhow::Error> {
342    // Handle serialization
343    let message = match serde_json::to_string(&m.0) {
344        Ok(json) => json,
345        Err(_error) => return Err(anyhow!("Could not deserialize message")),
346    };
347
348    match req.headers().get_one("Accept") {
349        Some(accept) => {
350            match accept {
351                NCRYPTF_CONTENT_TYPE => {
352                    // Retrieve the client public key
353                    let cpk = req.local_cache(|| {
354                        return RequestPublicKey(Vec::<u8>::new());
355                    });
356
357                    let pk: Vec<u8>;
358                    // If the cache data is empty, check the header as a fallback
359                    if cpk.0.is_empty() {
360                        pk = match req.headers().get_one("X-PubKey") {
361                            Some(h) => general_purpose::STANDARD.decode(h).unwrap(),
362                            // If the header isn't sent, then we have no way to encrypt a response the client can use
363                            None =>return Err(anyhow!("Public key is not available on request. Unable to re-encrypt message to client."))
364                        };
365                    } else {
366                        pk = cpk.0.clone();
367                    }
368
369                    let ek = EncryptionKey::new(false);
370
371                    // Create an encryption key then store it in cache
372                    // The client can choose to use the new key or ignore it, but we're always going to provide our own for each request
373                    let cache = match req.rocket().state::<CacheWrapper>() {
374                        Some(cache) => cache,
375                        None => return Err(anyhow!("Cache not found in managed state")),
376                    };
377
378                    cache.set(ek.get_hash_id(), ek.clone());
379
380                    let mut request = match crate::Request::from(
381                        ek.get_box_kp().get_secret_key(),
382                        // todo!() we should pull out the token signature, if it is set and use it
383                        ek.get_sign_kp().get_secret_key(),
384                    ) {
385                        Ok(request) => request,
386                        Err(_error) => return Err(anyhow!("Unable to encrypt message")),
387                    };
388
389                    let content = match request.encrypt(message, pk) {
390                        Ok(content) => content,
391                        Err(_error) => return Err(anyhow!("Unable to encrypt message")),
392                    };
393
394                    let d = general_purpose::STANDARD.encode(content);
395
396                    let respond_to = match d.respond_to(req) {
397                        Ok(s) => s,
398                        Err(_) => return Err(anyhow!("Could not send response")),
399                    };
400
401                    return Ok(Response::build_from(respond_to)
402                        .header(ContentType::new("application", "vnd.ncryptf+json"))
403                        .header(Header::new(
404                            "x-public-key",
405                            general_purpose::STANDARD.encode(ek.get_box_kp().get_public_key()),
406                        ))
407                        .header(Header::new(
408                            "x-signature-public-key",
409                            general_purpose::STANDARD.encode(ek.get_sign_kp().get_public_key()),
410                        ))
411                        .header(Header::new(
412                            "x-public-key-expiration",
413                            ek.expires_at.to_string(),
414                        ))
415                        .header(Header::new("x-hashid", ek.get_hash_id()))
416                        .status(status)
417                        .ok());
418                }
419                _ => {
420                    let respond_to = match message.respond_to(req) {
421                        Ok(s) => s,
422                        Err(_) => return Err(anyhow!("Could not send response")),
423                    };
424                    // Default to a JSON response if the content type is not an ncryptf content type
425                    // This is compatible with other implementations of ncryptf
426                    return Ok(Response::build_from(respond_to)
427                        .header(ContentType::new("application", "json"))
428                        .status(status)
429                        .ok());
430                }
431            }
432        }
433        None => {
434            let respond_to = match message.respond_to(req) {
435                Ok(s) => s,
436                Err(_) => return Err(anyhow!("Could not send response")),
437            };
438
439            // If an Accept is not defined on the request, return JSON when this struct is requested
440            return Ok(Response::build_from(respond_to)
441                .header(ContentType::new("application", "json"))
442                .status(status)
443                .ok());
444        }
445    }
446}