ncryptf/rocket/
json.rs

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