1use actix_web::{
2 body::{self, BoxBody, MessageBody},
3 dev::{Service, ServiceRequest, ServiceResponse, Transform},
4 http::{header, Method, StatusCode, Uri},
5 web::{Bytes, BytesMut},
6 Error, HttpMessage, HttpResponse,
7};
8use base64::{engine::general_purpose::STANDARD, Engine};
9use futures::{future::LocalBoxFuture, future::Ready, Stream, StreamExt};
10use ring::{
11 aead::{self, Aad, LessSafeKey, Nonce, UnboundKey},
12 rand::{SecureRandom, SystemRandom},
13};
14use std::{
15 collections::HashMap,
16 fmt,
17 pin::Pin,
18 rc::Rc,
19 sync::Arc,
20 task::{Context, Poll},
21};
22
23pub const CONTENT_ENCRYPTION_HEADER: &str = "x-content-encryption";
24pub const CONTENT_KEY_ID_HEADER: &str = "x-content-key-id";
25const ALGORITHM: &str = "aes-256-gcm-v1";
26const MAGIC: &[u8; 4] = b"RZC1";
27const NONCE_LEN: usize = 12;
28const TAG_LEN: usize = 16;
29
30#[derive(Clone, PartialEq, Eq)]
32pub struct ContentEncryptionKey {
33 id: String,
34 bytes: [u8; 32],
35}
36
37impl ContentEncryptionKey {
38 pub fn new(id: impl Into<String>, bytes: [u8; 32]) -> Result<Self, ContentEncryptionError> {
39 let id = id.into();
40 if id.trim().is_empty() || id.len() > 128 || !id.bytes().all(is_key_id_byte) {
41 return Err(ContentEncryptionError::InvalidKeyId);
42 }
43 Ok(Self { id, bytes })
44 }
45
46 pub fn id(&self) -> &str {
47 &self.id
48 }
49
50 pub fn encrypt_request(
52 &self,
53 method: &Method,
54 uri: &Uri,
55 plaintext: &[u8],
56 ) -> Result<String, ContentEncryptionError> {
57 self.seal(&request_aad(method, uri), plaintext)
58 }
59
60 pub fn decrypt_request(
61 &self,
62 method: &Method,
63 uri: &Uri,
64 encoded: &[u8],
65 ) -> Result<Bytes, ContentEncryptionError> {
66 self.open(&request_aad(method, uri), encoded)
67 }
68
69 pub fn encrypt_response(
70 &self,
71 method: &Method,
72 uri: &Uri,
73 status: StatusCode,
74 plaintext: &[u8],
75 ) -> Result<String, ContentEncryptionError> {
76 self.seal(&response_aad(method, uri, status), plaintext)
77 }
78
79 pub fn decrypt_response(
80 &self,
81 method: &Method,
82 uri: &Uri,
83 status: StatusCode,
84 encoded: &[u8],
85 ) -> Result<Bytes, ContentEncryptionError> {
86 self.open(&response_aad(method, uri, status), encoded)
87 }
88
89 fn seal(&self, aad: &[u8], plaintext: &[u8]) -> Result<String, ContentEncryptionError> {
90 let key = less_safe_key(&self.bytes)?;
91 let mut nonce = [0_u8; NONCE_LEN];
92 SystemRandom::new()
93 .fill(&mut nonce)
94 .map_err(|_| ContentEncryptionError::RandomnessUnavailable)?;
95 let mut ciphertext = plaintext.to_vec();
96 key.seal_in_place_append_tag(
97 Nonce::assume_unique_for_key(nonce),
98 Aad::from(aad),
99 &mut ciphertext,
100 )
101 .map_err(|_| ContentEncryptionError::EncryptionFailed)?;
102
103 let mut envelope = Vec::with_capacity(MAGIC.len() + nonce.len() + ciphertext.len());
104 envelope.extend_from_slice(MAGIC);
105 envelope.extend_from_slice(&nonce);
106 envelope.extend_from_slice(&ciphertext);
107 Ok(STANDARD.encode(envelope))
108 }
109
110 fn open(&self, aad: &[u8], encoded: &[u8]) -> Result<Bytes, ContentEncryptionError> {
111 let mut envelope = STANDARD
112 .decode(encoded)
113 .map_err(|_| ContentEncryptionError::InvalidEnvelope)?;
114 if envelope.len() < MAGIC.len() + NONCE_LEN + TAG_LEN || &envelope[..MAGIC.len()] != MAGIC {
115 return Err(ContentEncryptionError::InvalidEnvelope);
116 }
117 let nonce_start = MAGIC.len();
118 let ciphertext_start = nonce_start + NONCE_LEN;
119 let nonce: [u8; NONCE_LEN] = envelope[nonce_start..ciphertext_start]
120 .try_into()
121 .map_err(|_| ContentEncryptionError::InvalidEnvelope)?;
122 let key = less_safe_key(&self.bytes)?;
123 let plaintext = key
124 .open_in_place(
125 Nonce::assume_unique_for_key(nonce),
126 Aad::from(aad),
127 &mut envelope[ciphertext_start..],
128 )
129 .map_err(|_| ContentEncryptionError::AuthenticationFailed)?;
130 Ok(Bytes::copy_from_slice(plaintext))
131 }
132}
133
134impl fmt::Debug for ContentEncryptionKey {
135 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
136 formatter
137 .debug_struct("ContentEncryptionKey")
138 .field("id", &self.id)
139 .field("bytes", &"[REDACTED]")
140 .finish()
141 }
142}
143
144pub trait ContentKeyProvider: Send + Sync + 'static {
150 fn current_key(&self) -> Option<ContentEncryptionKey>;
151 fn key(&self, id: &str) -> Option<ContentEncryptionKey>;
152}
153
154#[derive(Debug, Clone)]
156pub struct StaticContentKeyProvider {
157 current_id: String,
158 keys: HashMap<String, ContentEncryptionKey>,
159}
160
161impl StaticContentKeyProvider {
162 pub fn new(current: ContentEncryptionKey) -> Self {
163 let current_id = current.id.clone();
164 Self {
165 current_id,
166 keys: HashMap::from([(current.id.clone(), current)]),
167 }
168 }
169
170 pub fn with_decryption_key(
171 mut self,
172 key: ContentEncryptionKey,
173 ) -> Result<Self, ContentEncryptionError> {
174 if self.keys.insert(key.id.clone(), key).is_some() {
175 return Err(ContentEncryptionError::DuplicateKeyId);
176 }
177 Ok(self)
178 }
179}
180
181impl ContentKeyProvider for StaticContentKeyProvider {
182 fn current_key(&self) -> Option<ContentEncryptionKey> {
183 self.keys.get(&self.current_id).cloned()
184 }
185
186 fn key(&self, id: &str) -> Option<ContentEncryptionKey> {
187 self.keys.get(id).cloned()
188 }
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192pub enum ContentEncryptionError {
193 InvalidKeyId,
194 DuplicateKeyId,
195 InvalidEnvelope,
196 AuthenticationFailed,
197 EncryptionFailed,
198 RandomnessUnavailable,
199}
200
201impl fmt::Display for ContentEncryptionError {
202 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
203 formatter.write_str(match self {
204 Self::InvalidKeyId => "invalid content-encryption key ID",
205 Self::DuplicateKeyId => "duplicate content-encryption key ID",
206 Self::InvalidEnvelope => "invalid encrypted-content envelope",
207 Self::AuthenticationFailed => "encrypted-content authentication failed",
208 Self::EncryptionFailed => "content encryption failed",
209 Self::RandomnessUnavailable => "secure randomness is unavailable",
210 })
211 }
212}
213
214impl std::error::Error for ContentEncryptionError {}
215
216#[derive(Clone)]
222pub struct ContentEncryption {
223 provider: Option<Arc<dyn ContentKeyProvider>>,
224 max_request_bytes: usize,
225 max_response_bytes: usize,
226}
227
228impl ContentEncryption {
229 pub fn new<P>(provider: P, max_body_bytes: usize) -> Self
230 where
231 P: ContentKeyProvider,
232 {
233 assert!(
234 max_body_bytes > 0,
235 "encrypted body limit must be greater than zero"
236 );
237 Self {
238 provider: Some(Arc::new(provider)),
239 max_request_bytes: max_body_bytes,
240 max_response_bytes: max_body_bytes,
241 }
242 }
243
244 pub fn with_response_limit(mut self, max_response_bytes: usize) -> Self {
245 assert!(
246 max_response_bytes > 0,
247 "encrypted response limit must be greater than zero"
248 );
249 self.max_response_bytes = max_response_bytes;
250 self
251 }
252
253 pub(crate) fn disabled(max_body_bytes: usize) -> Self {
254 Self {
255 provider: None,
256 max_request_bytes: max_body_bytes,
257 max_response_bytes: max_body_bytes,
258 }
259 }
260}
261
262impl<S, B> Transform<S, ServiceRequest> for ContentEncryption
263where
264 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
265 S::Future: 'static,
266 B: MessageBody + 'static,
267{
268 type Response = ServiceResponse<BoxBody>;
269 type Error = Error;
270 type Transform = ContentEncryptionMiddleware<S>;
271 type InitError = ();
272 type Future = Ready<Result<Self::Transform, Self::InitError>>;
273
274 fn new_transform(&self, service: S) -> Self::Future {
275 futures::future::ready(Ok(ContentEncryptionMiddleware {
276 service: Rc::new(service),
277 provider: self.provider.clone(),
278 max_request_bytes: self.max_request_bytes,
279 max_response_bytes: self.max_response_bytes,
280 }))
281 }
282}
283
284pub struct ContentEncryptionMiddleware<S> {
285 service: Rc<S>,
286 provider: Option<Arc<dyn ContentKeyProvider>>,
287 max_request_bytes: usize,
288 max_response_bytes: usize,
289}
290
291impl<S, B> Service<ServiceRequest> for ContentEncryptionMiddleware<S>
292where
293 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
294 S::Future: 'static,
295 B: MessageBody + 'static,
296{
297 type Response = ServiceResponse<BoxBody>;
298 type Error = Error;
299 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
300
301 fn poll_ready(&self, context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
302 self.service.poll_ready(context)
303 }
304
305 fn call(&self, mut request: ServiceRequest) -> Self::Future {
306 let service = Rc::clone(&self.service);
307 let provider = self.provider.clone();
308 let max_request_bytes = self.max_request_bytes;
309 let max_response_bytes = self.max_response_bytes;
310
311 Box::pin(async move {
312 let Some(provider) = provider else {
313 return Ok(service.call(request).await?.map_into_boxed_body());
314 };
315 let method = request.method().clone();
316 let uri = request.uri().clone();
317 let mut payload = request.take_payload();
318 let mut encoded = BytesMut::new();
319 while let Some(chunk) = payload.next().await {
320 let chunk = chunk.map_err(actix_web::error::ErrorBadRequest)?;
321 if encoded.len().saturating_add(chunk.len()) > max_request_bytes {
322 return Ok(error_response(request, StatusCode::PAYLOAD_TOO_LARGE));
323 }
324 encoded.extend_from_slice(&chunk);
325 }
326
327 if !encoded.is_empty() {
328 let algorithm = request
329 .headers()
330 .get(CONTENT_ENCRYPTION_HEADER)
331 .and_then(|value| value.to_str().ok());
332 if algorithm != Some(ALGORITHM) {
333 return Ok(error_response(request, StatusCode::UNSUPPORTED_MEDIA_TYPE));
334 }
335 let Some(key_id) = request
336 .headers()
337 .get(CONTENT_KEY_ID_HEADER)
338 .and_then(|value| value.to_str().ok())
339 else {
340 return Ok(error_response(request, StatusCode::BAD_REQUEST));
341 };
342 let Some(key) = provider.key(key_id) else {
343 return Ok(error_response(request, StatusCode::BAD_REQUEST));
344 };
345 let plaintext = match key.decrypt_request(&method, &uri, &encoded) {
346 Ok(plaintext) => plaintext,
347 Err(_) => return Ok(error_response(request, StatusCode::BAD_REQUEST)),
348 };
349 request.headers_mut().remove(CONTENT_ENCRYPTION_HEADER);
350 request.headers_mut().remove(CONTENT_KEY_ID_HEADER);
351 request.headers_mut().remove(header::CONTENT_LENGTH);
352 set_payload(&mut request, plaintext);
353 } else {
354 set_payload(&mut request, Bytes::new());
355 }
356
357 let response = service.call(request).await?.map_into_boxed_body();
358 let status = response.status();
359 if response
360 .headers()
361 .get(header::CONTENT_TYPE)
362 .and_then(|value| value.to_str().ok())
363 .is_some_and(|value| value.starts_with("text/event-stream"))
364 || response
365 .headers()
366 .get(header::CONTENT_ENCODING)
367 .and_then(|value| value.to_str().ok())
368 .is_some_and(|value| !value.eq_ignore_ascii_case("identity"))
369 {
370 let (request, _) = response.into_parts();
371 return Ok(error_from_request(
372 request,
373 StatusCode::INTERNAL_SERVER_ERROR,
374 ));
375 }
376
377 let (request, response) = response.into_parts();
378 let response_headers = response.headers().clone();
379 let plaintext =
380 match body::to_bytes_limited(response.into_body(), max_response_bytes).await {
381 Ok(Ok(body)) => body,
382 _ => {
383 return Ok(error_from_request(
384 request,
385 StatusCode::INTERNAL_SERVER_ERROR,
386 ))
387 }
388 };
389 let Some(key) = provider.current_key() else {
390 return Ok(error_from_request(
391 request,
392 StatusCode::INTERNAL_SERVER_ERROR,
393 ));
394 };
395 let encoded = match key.encrypt_response(&method, &uri, status, &plaintext) {
396 Ok(encoded) => encoded,
397 Err(_) => {
398 return Ok(error_from_request(
399 request,
400 StatusCode::INTERNAL_SERVER_ERROR,
401 ))
402 }
403 };
404 let mut builder = HttpResponse::build(status);
405 for (name, value) in response_headers.iter() {
406 if name != header::CONTENT_LENGTH && name != header::TRANSFER_ENCODING {
407 builder.append_header((name.clone(), value.clone()));
408 }
409 }
410 builder.insert_header((CONTENT_ENCRYPTION_HEADER, ALGORITHM));
411 builder.insert_header((CONTENT_KEY_ID_HEADER, key.id()));
412 Ok(ServiceResponse::new(request, builder.body(encoded)))
413 })
414 }
415}
416
417fn set_payload(request: &mut ServiceRequest, body: Bytes) {
418 let payload =
419 futures::stream::once(async move { Ok::<_, actix_web::error::PayloadError>(body) });
420 let payload: Pin<
421 Box<dyn Stream<Item = Result<actix_web::web::Bytes, actix_web::error::PayloadError>>>,
422 > = Box::pin(payload);
423 request.set_payload(payload.into());
424}
425
426fn error_response(request: ServiceRequest, status: StatusCode) -> ServiceResponse<BoxBody> {
427 request.into_response(HttpResponse::build(status).finish().map_into_boxed_body())
428}
429
430fn error_from_request(
431 request: actix_web::HttpRequest,
432 status: StatusCode,
433) -> ServiceResponse<BoxBody> {
434 ServiceResponse::new(request, HttpResponse::build(status).finish())
435}
436
437fn less_safe_key(bytes: &[u8; 32]) -> Result<LessSafeKey, ContentEncryptionError> {
438 UnboundKey::new(&aead::AES_256_GCM, bytes)
439 .map(LessSafeKey::new)
440 .map_err(|_| ContentEncryptionError::EncryptionFailed)
441}
442
443fn request_aad(method: &Method, uri: &Uri) -> Vec<u8> {
444 format!("rust-zero-content-v1\0request\0{method}\0{uri}").into_bytes()
445}
446
447fn response_aad(method: &Method, uri: &Uri, status: StatusCode) -> Vec<u8> {
448 format!(
449 "rust-zero-content-v1\0response\0{method}\0{uri}\0{}",
450 status.as_u16()
451 )
452 .into_bytes()
453}
454
455fn is_key_id_byte(byte: u8) -> bool {
456 byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462 use actix_web::{test as actix_test, web, App};
463
464 fn key(id: &str, byte: u8) -> ContentEncryptionKey {
465 ContentEncryptionKey::new(id, [byte; 32]).unwrap()
466 }
467
468 #[test]
469 fn envelope_authenticates_context_and_ciphertext() {
470 let key = key("primary", 7);
471 let method = Method::POST;
472 let uri: Uri = "/records?tenant=one".parse().unwrap();
473 let encoded = key.encrypt_request(&method, &uri, b"secret").unwrap();
474 assert_eq!(
475 key.decrypt_request(&method, &uri, encoded.as_bytes())
476 .unwrap(),
477 Bytes::from_static(b"secret")
478 );
479 assert_eq!(
480 key.decrypt_request(&Method::PUT, &uri, encoded.as_bytes()),
481 Err(ContentEncryptionError::AuthenticationFailed)
482 );
483
484 let mut tampered = STANDARD.decode(encoded).unwrap();
485 *tampered.last_mut().unwrap() ^= 1;
486 assert_eq!(
487 key.decrypt_request(&method, &uri, STANDARD.encode(tampered).as_bytes()),
488 Err(ContentEncryptionError::AuthenticationFailed)
489 );
490 }
491
492 #[actix_rt::test]
493 async fn middleware_decrypts_request_and_encrypts_response_with_rotated_key() {
494 let previous = key("previous", 3);
495 let current = key("current", 9);
496 let provider = StaticContentKeyProvider::new(current.clone())
497 .with_decryption_key(previous.clone())
498 .unwrap();
499 let app = actix_test::init_service(
500 App::new()
501 .wrap(ContentEncryption::new(provider, 4096))
502 .route("/echo", web::post().to(|body: Bytes| async move { body })),
503 )
504 .await;
505 let method = Method::POST;
506 let uri: Uri = "/echo".parse().unwrap();
507 let encrypted = previous.encrypt_request(&method, &uri, b"hello").unwrap();
508 let request = actix_test::TestRequest::post()
509 .uri("/echo")
510 .insert_header((CONTENT_ENCRYPTION_HEADER, ALGORITHM))
511 .insert_header((CONTENT_KEY_ID_HEADER, previous.id()))
512 .set_payload(encrypted)
513 .to_request();
514 let response = actix_test::call_service(&app, request).await;
515 assert_eq!(response.status(), StatusCode::OK);
516 assert_eq!(
517 response.headers().get(CONTENT_KEY_ID_HEADER).unwrap(),
518 current.id()
519 );
520 let body = actix_test::read_body(response).await;
521 assert_eq!(
522 current
523 .decrypt_response(&method, &uri, StatusCode::OK, &body)
524 .unwrap(),
525 Bytes::from_static(b"hello")
526 );
527 }
528
529 #[actix_rt::test]
530 async fn malformed_or_unknown_ciphertext_fails_before_handler() {
531 let provider = StaticContentKeyProvider::new(key("current", 9));
532 let app = actix_test::init_service(
533 App::new()
534 .wrap(ContentEncryption::new(provider, 4096))
535 .route("/", web::post().to(HttpResponse::Ok)),
536 )
537 .await;
538 let response = actix_test::call_service(
539 &app,
540 actix_test::TestRequest::post()
541 .uri("/")
542 .insert_header((CONTENT_ENCRYPTION_HEADER, ALGORITHM))
543 .insert_header((CONTENT_KEY_ID_HEADER, "missing"))
544 .set_payload("not ciphertext")
545 .to_request(),
546 )
547 .await;
548 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
549 }
550
551 #[actix_rt::test]
552 async fn middleware_rejects_streaming_and_oversized_responses() {
553 let provider = StaticContentKeyProvider::new(key("current", 9));
554 let app = actix_test::init_service(
555 App::new()
556 .wrap(ContentEncryption::new(provider, 4096).with_response_limit(4))
557 .route(
558 "/events",
559 web::get().to(|| async {
560 HttpResponse::Ok()
561 .content_type("text/event-stream")
562 .streaming(futures::stream::iter([Ok::<_, Error>(Bytes::from_static(
563 b"data: ready\n\n",
564 ))]))
565 }),
566 )
567 .route(
568 "/large",
569 web::get().to(|| async { HttpResponse::Ok().body("12345") }),
570 ),
571 )
572 .await;
573
574 for uri in ["/events", "/large"] {
575 let response = actix_test::call_service(
576 &app,
577 actix_test::TestRequest::get().uri(uri).to_request(),
578 )
579 .await;
580 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
581 assert!(!response.headers().contains_key(CONTENT_ENCRYPTION_HEADER));
582 }
583 }
584}