Skip to content

Commit

Permalink
extract hashing extractors into actix-hash crate
Browse files Browse the repository at this point in the history
  • Loading branch information
robjtede committed Mar 24, 2022
1 parent ea411ab commit 6588cfd
Show file tree
Hide file tree
Showing 16 changed files with 445 additions and 296 deletions.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[workspace]
members = [
"./actix-web-lab"
"./actix-hash",
"./actix-web-lab",
]
63 changes: 63 additions & 0 deletions actix-hash/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
[package]
name = "actix-hash"
version = "0.1.0"
authors = ["Rob Ede <robjtede@icloud.com>"]
description = "Hashing utilities for Actix Web"
keywords = ["actix", "http", "web", "framework", "async"]
categories = [
"web-programming::http-server",
"cryptography",
]
repository = "https://github.com/robjtede/actix-web-lab.git"
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.56"

[features]
default = [
"blake2",
"md5",
"md4",
"sha1",
"sha2",
"sha3",
]
blake2 = ["alg-blake2"]
md5 = ["alg-md5"]
md4 = ["alg-md4"]
sha1 = ["alg-sha1"]
sha2 = ["alg-sha2"]
sha3 = ["alg-sha3"]

[dependencies]
actix-http = "3"
actix-web = { version = "4", default-features = false }
futures-core = "0.3.7"
futures-util = { version = "0.3.7", default-features = false, features = ["std"] }
local-channel = "0.1"
tokio = { version = "1.13.1", features = ["sync"] }
tracing = { version = "0.1.30", features = ["log"] }

digest = "0.10"
subtle = "2"
alg-blake2 = { package = "blake2", version = "0.10", optional = true }
alg-md5 = { package = "md-5", version = "0.10", optional = true }
alg-md4 = { package = "md4", version = "0.10", optional = true }
alg-sha1 = { package = "sha1", version = "0.10", optional = true }
alg-sha2 = { package = "sha2", version = "0.10", optional = true }
alg-sha3 = { package = "sha3", version = "0.10", optional = true }

[dev-dependencies]
actix-web = "4"
actix-web-lab = "0.15"
base64 = "0.13"
env_logger = "0.9"
hex-literal = "0.3"

[[test]]
name = "sha2"
required-features = ["sha2"]

[[example]]
name = "sha2"
required-features = ["sha2"]
3 changes: 3 additions & 0 deletions actix-hash/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# actix-hash

> Hashing utilities for Actix Web.
31 changes: 31 additions & 0 deletions actix-hash/examples/sha2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//! Body + checksum hash extractor usage.
//!
//! For example, sending an empty body will return the hash starting with "E3":
//! ```sh
//! $ curl -XPOST localhost:8080
//! [E3, B0, C4, 42, 98, FC, 1C, ...
//! ```
use std::io;

use actix_hash::BodySha256;
use actix_web::{middleware::Logger, web, App, HttpServer};
use tracing::info;

#[actix_web::main]
async fn main() -> io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));

info!("staring server at http://localhost:8080");

HttpServer::new(|| {
App::new().wrap(Logger::default().log_target("@")).route(
"/",
web::post().to(|body: BodySha256<String>| async move { format!("{:X?}", body.hash()) }),
)
})
.workers(1)
.bind(("127.0.0.1", 8080))?
.run()
.await
}
8 changes: 8 additions & 0 deletions actix-hash/src/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Changelog

## Unreleased - 2022-xx-xx
- Body hashing extractors for many popular, general purpose hashing algorithms.


# 0.1.0
- Empty crate.
62 changes: 62 additions & 0 deletions actix-hash/src/body_extractor_fold.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use actix_http::BoxedPayloadStream;
use actix_web::{
dev,
web::{BufMut as _, Bytes, BytesMut},
FromRequest, HttpRequest,
};
use futures_core::future::LocalBoxFuture;
use futures_util::StreamExt as _;
use local_channel::mpsc;
use tokio::try_join;
use tracing::trace;

pub(crate) fn body_extractor_fold<T, Init, Out>(
req: &HttpRequest,
payload: &mut dev::Payload,
init: Init,
mut update_fn: impl FnMut(&mut Init, &HttpRequest, Bytes) + 'static,
mut finalize_fn: impl FnMut(T, Bytes, Init) -> Out + 'static,
) -> LocalBoxFuture<'static, Result<Out, T::Error>>
where
T: FromRequest,
Init: 'static,
{
let req = req.clone();
let payload = payload.take();

Box::pin(async move {
let (tx, mut rx) = mpsc::channel();

// wrap payload in stream that reads chunks and clones them (cheaply) back here
let proxy_stream: BoxedPayloadStream = Box::pin(payload.inspect(move |res| {
if let Ok(chunk) = res {
trace!("yielding {} byte chunk", chunk.len());
tx.send(chunk.clone()).unwrap();
}
}));

trace!("creating proxy payload");
let mut proxy_payload = dev::Payload::from(proxy_stream);
let body_fut = T::from_request(&req, &mut proxy_payload);

let mut body_buf = BytesMut::new();

// run update function as chunks are yielded from channel
let hash_fut = async {
let mut accumulator = init;
while let Some(chunk) = rx.recv().await {
trace!("updating hasher with {} byte chunk", chunk.len());
body_buf.put_slice(&chunk);
update_fn(&mut accumulator, &req, chunk)
}
Ok(accumulator)
};

trace!("driving both futures");
let (body, hash) = try_join!(body_fut, hash_fut)?;

let out = (finalize_fn)(body, body_buf.freeze(), hash);

Ok(out)
})
}
103 changes: 103 additions & 0 deletions actix-hash/src/body_hash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use actix_web::{dev, web::Bytes, FromRequest, HttpRequest};
use digest::{generic_array::GenericArray, Digest};
use futures_core::future::LocalBoxFuture;

use crate::body_extractor_fold::body_extractor_fold;

/// Parts of the resulting body hash extractor.
pub struct BodyHashParts<T> {
/// Extracted body item.
pub body: T,

/// Bytes of the body that were extracted.
pub body_bytes: Bytes,

/// Bytes of the calculated hash.
pub hash_bytes: Vec<u8>,
}

/// Wraps an extractor and calculates a body checksum hash alongside.
///
/// If your extractor would usually be `T` and you want to create a hash of type `D` then you need
/// to use `BodyHash<T, D>`. It is assumed that the `T` extractor will consume the payload.
///
/// Any hasher that implements [`Digest`] can be used. Type aliases for common hashing algorithms
/// are available at the crate root.
///
/// # Errors
/// This extractor produces no errors of its own and all errors from the underlying extractor are
/// propagated correctly; for example, if the payload limits are exceeded.
///
/// # Example
/// ```
/// use actix_web::{Responder, web};
/// use actix_web_lab::extract::BodyHash;
/// use sha2::Sha256;
///
/// # type T = u64;
/// async fn hash_payload(form: BodyHash<web::Json<T>, Sha256>) -> impl Responder {
/// if !form.verify_slice(b"correct-signature") {
/// // return unauthorized error
/// }
///
/// "Ok"
/// }
/// ```
#[derive(Debug, Clone)]
pub struct BodyHash<T, D: Digest> {
body: T,
bytes: Bytes,
hash: GenericArray<u8, D::OutputSize>,
}

impl<T, D: Digest> BodyHash<T, D> {
/// Returns hash slice.
pub fn hash(&self) -> &[u8] {
self.hash.as_slice()
}

/// Returns hash output size.
pub fn hash_size(&self) -> usize {
self.hash.len()
}

/// Verifies HMAC hash against provided `tag` using constant-time equality.
pub fn verify_slice(&self, tag: &[u8]) -> bool {
use subtle::ConstantTimeEq as _;
self.hash.ct_eq(tag).into()
}

/// Returns body type parts, including extracted body type, raw body bytes, and hash bytes.
pub fn into_parts(self) -> BodyHashParts<T> {
let hash = self.hash().to_vec();

BodyHashParts {
body: self.body,
body_bytes: self.bytes,
hash_bytes: hash,
}
}
}

impl<T, D> FromRequest for BodyHash<T, D>
where
T: FromRequest + 'static,
D: Digest + 'static,
{
type Error = T::Error;
type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;

fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future {
body_extractor_fold(
req,
payload,
D::new(),
|hasher, _req, chunk| hasher.update(&chunk),
|body, bytes, hasher| Self {
body,
bytes,
hash: hasher.finalize(),
},
)
}
}
83 changes: 83 additions & 0 deletions actix-hash/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//! Hashing utilities for Actix Web.
//!
//! # Crate Features
//! All features are enabled by default.
//! - `blake2`: Blake2 types
//! - `md5`: MD5 types
//! - `md4`: MD4 types
//! - `sha1`: SHA-1 types
//! - `sha2`: SHA-2 types
//! - `sha3`: SHA-3 types
#![forbid(unsafe_code)]
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible, missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]

#[cfg(feature = "blake2")]
extern crate alg_blake2 as blake2;
#[cfg(feature = "md4")]
extern crate alg_md4 as md4;
#[cfg(feature = "md5")]
extern crate alg_md5 as md5;
#[cfg(feature = "sha1")]
extern crate alg_sha1 as sha1;
#[cfg(feature = "sha2")]
extern crate alg_sha2 as sha2;
#[cfg(feature = "sha3")]
extern crate alg_sha3 as sha3;

mod body_extractor_fold;
mod body_hash;

pub use self::body_hash::{BodyHash, BodyHashParts};

macro_rules! body_hash_alias {
($name:ident, $digest:path, $feature:literal, $desc:literal, $out_size:literal) => {
#[doc = concat!("Wraps an extractor and calculates a `", $desc, "` body checksum hash alongside.")]
/// # Example
///
/// ```
#[doc = concat!("use actix_hash::", stringify!($name), ";")]
///
#[doc = concat!("async fn handler(body: ", stringify!($name), "<String>) -> String {")]
#[doc = concat!(" assert_eq!(body.hash().len(), ", $out_size, ");")]
/// body.into_parts().body
/// }
/// #
/// # // test that the documented hash size is correct
#[doc = concat!("# type Hasher = ", stringify!($digest), ";")]
#[doc = concat!("# const OutSize: usize = ", $out_size, ";")]
/// # assert_eq!(
/// # digest::generic_array::GenericArray::<u8,
/// # <Hasher as digest::OutputSizeUser>::OutputSize
/// # >::default().len(),
/// # OutSize
/// # );
/// ```
#[cfg(feature = $feature)]
#[cfg_attr(docsrs, doc(cfg(feature = $feature)))]
pub type $name<T> = BodyHash<T, $digest>;
};
}

// Obsolete
body_hash_alias!(BodyMd4, md4::Md4, "md4", "MD4", 16);
body_hash_alias!(BodyMd5, md5::Md5, "md5", "MD5", 16);
body_hash_alias!(BodySha1, sha1::Sha1, "sha1", "SHA-1", 20);

// SHA-2
body_hash_alias!(BodySha224, sha2::Sha224, "sha2", "SHA-224", 28);
body_hash_alias!(BodySha256, sha2::Sha256, "sha2", "SHA-256", 32);
body_hash_alias!(BodySha384, sha2::Sha384, "sha2", "SHA-384", 48);
body_hash_alias!(BodySha512, sha2::Sha512, "sha2", "SHA-512", 64);

// SHA-3
body_hash_alias!(BodySha3_224, sha3::Sha3_224, "sha3", "SHA-3-224", 28);
body_hash_alias!(BodySha3_256, sha3::Sha3_256, "sha3", "SHA-3-256", 32);
body_hash_alias!(BodySha3_384, sha3::Sha3_384, "sha3", "SHA-3-384", 48);
body_hash_alias!(BodySha3_512, sha3::Sha3_512, "sha3", "SHA-3-512", 64);

// Blake2
body_hash_alias!(BodyBlake2b, blake2::Blake2b512, "blake2", "Blake2b", 64);
body_hash_alias!(BodyBlake2s, blake2::Blake2s256, "blake2", "Blake2s", 32);
Loading

0 comments on commit 6588cfd

Please sign in to comment.