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}