From 096521d5264eacf19c779d2e094350be16191a95 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sun, 18 Dec 2022 21:10:03 +0000 Subject: [PATCH] add middleware-encrypted-payloads example --- Cargo.lock | 78 ++++++--- Cargo.toml | 1 + .../middleware-encrypted-payloads/Cargo.toml | 18 ++ .../middleware-encrypted-payloads/README.md | 51 ++++++ .../middleware-encrypted-payloads/src/main.rs | 163 ++++++++++++++++++ middleware/middleware-http-to-https/README.md | 2 +- 6 files changed, 292 insertions(+), 21 deletions(-) create mode 100644 middleware/middleware-encrypted-payloads/Cargo.toml create mode 100644 middleware/middleware-encrypted-payloads/README.md create mode 100644 middleware/middleware-encrypted-payloads/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index a85007f..f6bd57d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a18fb402a32ddc56ef7015df8d575c326ea911887ba9b7661ce18786282e89a3" dependencies = [ "anyhow", - "base64", + "base64 0.13.1", "lazy_static", "log", "openssl", @@ -141,7 +141,7 @@ dependencies = [ "actix-tls", "actix-utils", "ahash 0.7.6", - "base64", + "base64 0.13.1", "bitflags", "brotli", "bytes 1.3.0", @@ -180,7 +180,7 @@ dependencies = [ "actix-tls", "actix-utils", "awc", - "base64", + "base64 0.13.1", "bytes 1.3.0", "futures-core", "http", @@ -594,6 +594,21 @@ dependencies = [ "subtle", ] +[[package]] +name = "aes-gcm-siv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "polyval", + "subtle", + "zeroize", +] + [[package]] name = "ahash" version = "0.7.6" @@ -776,7 +791,7 @@ dependencies = [ "async-graphql-value", "async-stream", "async-trait", - "base64", + "base64 0.13.1", "bytes 1.3.0", "fast_chemail", "fnv", @@ -971,7 +986,7 @@ dependencies = [ "actix-tls", "actix-utils", "ahash 0.7.6", - "base64", + "base64 0.13.1", "bytes 1.3.0", "cfg-if 1.0.0", "cookie 0.16.1", @@ -1357,6 +1372,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" + [[package]] name = "basics" version = "1.0.0" @@ -1509,7 +1530,7 @@ version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de0aa578035b938855a710ba58d43cfb4d435f3619f99236fb35922a574d6cb1" dependencies = [ - "base64", + "base64 0.13.1", "chrono", "hex", "lazy_static", @@ -1527,7 +1548,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d76085681585d39016f4d3841eb019201fc54d2dd0d92ad1e4fab3bfb32754" dependencies = [ "ahash 0.7.6", - "base64", + "base64 0.13.1", "hex", "indexmap", "lazy_static", @@ -1922,7 +1943,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "344adc371239ef32293cb1c4fe519592fcf21206c79c02854320afcdf3ab4917" dependencies = [ "aes-gcm", - "base64", + "base64 0.13.1", "hkdf", "hmac", "percent-encoding", @@ -4099,6 +4120,23 @@ dependencies = [ "autocfg", ] +[[package]] +name = "middleware-encrypted-payloads" +version = "1.0.0" +dependencies = [ + "actix-http", + "actix-web", + "actix-web-lab", + "aes-gcm-siv", + "base64 0.20.0", + "env_logger", + "futures-util", + "log", + "pin-project", + "serde", + "serde_json", +] + [[package]] name = "middleware-example" version = "1.0.0" @@ -4245,7 +4283,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5a1df476ac9541b0e4fdc8e2cc48884e66c92c933cd17a1fd75e68caf75752e" dependencies = [ "async-trait", - "base64", + "base64 0.13.1", "bitflags", "bson 2.4.0", "chrono", @@ -4364,7 +4402,7 @@ version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9006c95034ccf7b903d955f210469119f6c3477fc9c9e7a7845ce38a3e665c2a" dependencies = [ - "base64", + "base64 0.13.1", "bigdecimal", "bindgen", "bitflags", @@ -4712,7 +4750,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4" dependencies = [ - "base64", + "base64 0.13.1", ] [[package]] @@ -4904,7 +4942,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "878c6cbf956e03af9aa8204b407b9cbf47c072164800aa918c516cd4b056c50c" dependencies = [ - "base64", + "base64 0.13.1", "byteorder", "bytes 1.3.0", "fallible-iterator", @@ -5320,7 +5358,7 @@ version = "0.10.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0718f81a8e14c4dbb3b34cf23dc6aaf9ab8a0dfec160c534b3dbca1aaa21f47c" dependencies = [ - "base64", + "base64 0.13.1", "bytes 0.5.6", "encoding_rs", "futures-core", @@ -5356,7 +5394,7 @@ version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" dependencies = [ - "base64", + "base64 0.13.1", "bytes 1.3.0", "encoding_rs", "futures-core", @@ -5486,7 +5524,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" dependencies = [ - "base64", + "base64 0.13.1", "bitflags", "serde", ] @@ -5521,7 +5559,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50162d19404029c1ceca6f6980fe40d45c8b369f6f44446fa14bb39573b5bb9" dependencies = [ - "base64", + "base64 0.13.1", "blake2b_simd", "constant_time_eq", "crossbeam-utils", @@ -5609,7 +5647,7 @@ version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" dependencies = [ - "base64", + "base64 0.13.1", "log", "ring", "sct 0.6.1", @@ -5671,7 +5709,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ee86d63972a7c661d1536fefe8c3c8407321c3df668891286de28abcd087360" dependencies = [ - "base64", + "base64 0.13.1", ] [[package]] @@ -5680,7 +5718,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" dependencies = [ - "base64", + "base64 0.13.1", ] [[package]] @@ -7313,7 +7351,7 @@ version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b8b063c2d59218ae09f22b53c42eaad0d53516457905f5235ca4bc9e99daa71" dependencies = [ - "base64", + "base64 0.13.1", "chunked_transfer", "cookie 0.14.4", "cookie_store", diff --git a/Cargo.toml b/Cargo.toml index 42339b2..5cdfbb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ members = [ "json/json-validation", "json/json", "json/jsonrpc", + "middleware/middleware-encrypted-payloads", "middleware/middleware-ext-mut", "middleware/middleware-http-to-https", "middleware/middleware", diff --git a/middleware/middleware-encrypted-payloads/Cargo.toml b/middleware/middleware-encrypted-payloads/Cargo.toml new file mode 100644 index 0000000..7572479 --- /dev/null +++ b/middleware/middleware-encrypted-payloads/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "middleware-encrypted-payloads" +version = "1.0.0" +edition = "2021" + +[dependencies] +actix-http = "3" +actix-web = "4" +actix-web-lab = "0.18" + +aes-gcm-siv = "0.11" +base64 = "0.20" +env_logger = "0.10" +futures-util = { version = "0.3.17", default-features = false, features = ["std"] } +log = "0.4" +pin-project = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/middleware/middleware-encrypted-payloads/README.md b/middleware/middleware-encrypted-payloads/README.md new file mode 100644 index 0000000..8a27273 --- /dev/null +++ b/middleware/middleware-encrypted-payloads/README.md @@ -0,0 +1,51 @@ +# Encrypted Request + Response Payloads Middleware + +Shows an example of a `from_fn` middleware that: + +1. extracts a JSON request payload; +1. decrypts a data field; +1. re-encodes as JSON and re-packs the request; +1. calls the wrapped service; +1. unpacks the response and parses a similarly structured JSON object; +1. encrypts the data field; +1. re-packs the response. + +All this to say, it provides a (fairly brittle) way to have handlers not need to care about the encryption and decryption steps. + +## Usage + +```console +$ http POST :8080/encrypt id:=1234 data=abcde > tmp.json +POST /reverse HTTP/1.1 +Content-Length: 76 +Content-Type: application/json + +{ + "id": 1234, + "data": "kq6MKdP+I0hoI7YC7CN39yamx67T", + "nonce": "dW5pcXVlIG5vbmNl" +} + +$ cat tmp.json | http -v POST :8080/reverse +HTTP/1.1 200 OK +Content-Length: 66 +Content-Type: application/json + +{ + "data": "UL4PeOr9Di8xpFEJZgylJ5K8R7vW", + "nonce": "dW5pcXVlIG5vbmNl" +} +``` + +The server logs would look something like + +```plain +[INFO middleware_encrypted_payloads] creating encrypted sample request for ID = 1234 +[INFO actix_web::middleware::logger] 127.0.0.1 "POST /encrypt HTTP/1.1" 200 76 "-" "-" 0.000393 +... +[INFO middleware_encrypted_payloads] decrypting request 1234 +[INFO middleware_encrypted_payloads] request 1234 with data: abcde +[INFO middleware_encrypted_payloads] response 1234 with new data: edcba +[INFO middleware_encrypted_payloads] encrypting response 1234 +[INFO actix_web::middleware::logger] 127.0.0.1 "POST /reverse HTTP/1.1" 200 66 "-" "-" 0.000425 +``` diff --git a/middleware/middleware-encrypted-payloads/src/main.rs b/middleware/middleware-encrypted-payloads/src/main.rs new file mode 100644 index 0000000..b1438be --- /dev/null +++ b/middleware/middleware-encrypted-payloads/src/main.rs @@ -0,0 +1,163 @@ +use actix_web::{ + body::{self, MessageBody}, + dev::{self, ServiceResponse}, + middleware::Logger, + web::{self, Data, Json}, + App, Error, HttpServer, Responder, +}; +use actix_web_lab::middleware::{from_fn, Next}; +use aes_gcm_siv::{aead::Aead as _, Aes256GcmSiv, KeyInit as _, Nonce}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +struct Req { + id: u64, + data: String, + + #[serde(skip_serializing_if = "Option::is_none")] + nonce: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct Res { + data: String, + + #[serde(skip_serializing_if = "Option::is_none")] + nonce: Option, +} + +async fn make_encrypted( + cipher: Data, + Json(Req { id, data, .. }): Json, +) -> impl Responder { + log::info!("creating encrypted sample request for ID = {id:?}"); + + // this nonce should actually be unique per message in a production environment + let nonce = Nonce::from_slice(b"unique nonce"); + let nonce_b64 = Some(base64::encode(nonce)); + + let data_enc = cipher.encrypt(nonce, data.as_bytes()).unwrap(); + let data_enc = base64::encode(data_enc); + + web::Json(Req { + id, + nonce: nonce_b64, + data: data_enc, + }) +} + +async fn reverse_data(Json(Req { id, data, .. }): Json) -> impl Responder { + log::info!("request {id:?} with data: {data}"); + + let data_rev = data.chars().rev().collect(); + + log::info!("response {id:?} with new data: {data_rev}"); + + Json(Res { + data: data_rev, + nonce: None, + }) +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + log::info!("starting HTTP server at http://localhost:8080"); + + // initialize cipher outside HttpServer closure + let cipher = Aes256GcmSiv::new_from_slice(&[0; 32]).unwrap(); + + HttpServer::new(move || { + App::new() + .app_data(Data::new(cipher.clone())) + .service(web::resource("/encrypt").route(web::post().to(make_encrypted))) + .service( + web::resource("/reverse") + .route(web::post().to(reverse_data)) + .wrap(from_fn(encrypt_payloads)), + ) + .wrap(Logger::default()) + }) + .bind(("127.0.0.1", 8080))? + .workers(1) + .run() + .await +} + +async fn encrypt_payloads( + mut req: dev::ServiceRequest, + next: Next, +) -> Result, Error> { + // get cipher from app data + let cipher = req.extract::>().await.unwrap(); + + // extract JSON with encrypted+encoded data field + let Json(Req { id, nonce, data }) = req.extract::>().await?; + + log::info!("decrypting request {id:?}"); + + // decode nonce from payload + let nonce = base64::decode(nonce.unwrap()).unwrap(); + let nonce = Nonce::from_slice(&nonce); + + // decode and decrypt data field + let data_enc = base64::decode(&data).unwrap(); + let data = cipher.decrypt(nonce, data_enc.as_slice()).unwrap(); + + // construct request body format with plaintext data + let req_body = Req { + id, + nonce: None, + data: String::from_utf8(data).unwrap(), + }; + + // encode request body as JSON + let req_body = serde_json::to_vec(&req_body).unwrap(); + + // re-insert request body + req.set_payload(bytes_to_payload(web::Bytes::from(req_body))); + + // call next service + let res = next.call(req).await?; + + log::info!("encrypting response {id:?}"); + + // deconstruct response into parts + let (req, res) = res.into_parts(); + let (res, body) = res.into_parts(); + + // Read all bytes out of response stream. Only use `to_bytes` if you can guarantee all handlers + // wrapped by this middleware return complete responses or bounded streams. + let body = body::to_bytes(body).await.ok().unwrap(); + + // parse JSON from response body + let Res { data, .. } = serde_json::from_slice(&body).unwrap(); + + // generate and encode nonce for later + let nonce = Nonce::from_slice(b"unique nonce"); + let nonce_b64 = Some(base64::encode(nonce)); + + // encrypt and encode data field + let data_enc = cipher.encrypt(nonce, data.as_bytes()).unwrap(); + let data_enc = base64::encode(data_enc); + + // re-pack response into JSON format + let res_body = Res { + data: data_enc, + nonce: nonce_b64, + }; + let res_body_enc = serde_json::to_string(&res_body).unwrap(); + + // set response body as new JSON payload and re-combine response object + let res = res.set_body(res_body_enc); + let res = ServiceResponse::new(req, res); + + Ok(res) +} + +fn bytes_to_payload(buf: web::Bytes) -> dev::Payload { + let (_, mut pl) = actix_http::h1::Payload::create(true); + pl.unread_data(buf); + dev::Payload::from(pl) +} diff --git a/middleware/middleware-http-to-https/README.md b/middleware/middleware-http-to-https/README.md index f65a19e..b943f9d 100644 --- a/middleware/middleware-http-to-https/README.md +++ b/middleware/middleware-http-to-https/README.md @@ -2,7 +2,7 @@ ## Alternatives -A pre-built solution is soon to be built-in. For now, see [`RedirectHttps`](https://docs.rs/actix-web-lab/0.16/actix_web_lab/middleware/struct.RedirectHttps.html) from [`actix-web-lab`](https://crates.io/crates/actix-web-lab). +A pre-built solution is soon to be built-in. For now, see [`RedirectHttps`](https://docs.rs/actix-web-lab/0.18/actix_web_lab/middleware/struct.RedirectHttps.html) from [`actix-web-lab`](https://crates.io/crates/actix-web-lab). ## This Example