mirror of
https://github.com/actix/examples
synced 2024-11-23 14:31:07 +01:00
add middleware-encrypted-payloads example
This commit is contained in:
parent
43d26f194d
commit
096521d526
78
Cargo.lock
generated
78
Cargo.lock
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
18
middleware/middleware-encrypted-payloads/Cargo.toml
Normal file
18
middleware/middleware-encrypted-payloads/Cargo.toml
Normal file
@ -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"
|
51
middleware/middleware-encrypted-payloads/README.md
Normal file
51
middleware/middleware-encrypted-payloads/README.md
Normal file
@ -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
|
||||
```
|
163
middleware/middleware-encrypted-payloads/src/main.rs
Normal file
163
middleware/middleware-encrypted-payloads/src/main.rs
Normal file
@ -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<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Res {
|
||||
data: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
nonce: Option<String>,
|
||||
}
|
||||
|
||||
async fn make_encrypted(
|
||||
cipher: Data<Aes256GcmSiv>,
|
||||
Json(Req { id, data, .. }): Json<Req>,
|
||||
) -> 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<Req>) -> 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<impl MessageBody>,
|
||||
) -> Result<dev::ServiceResponse<impl MessageBody>, Error> {
|
||||
// get cipher from app data
|
||||
let cipher = req.extract::<web::Data<Aes256GcmSiv>>().await.unwrap();
|
||||
|
||||
// extract JSON with encrypted+encoded data field
|
||||
let Json(Req { id, nonce, data }) = req.extract::<Json<Req>>().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)
|
||||
}
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user