From 38c6a5c0a4df938a94452ac632da7969b629f14f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Sun, 1 Mar 2026 18:38:01 +0100 Subject: [PATCH] chore(crypto): use blake3 instead of sha2 This uses blake3 for static file content hashing as well as calculating the session auth hash (using blake3's native keyed mode instead of HMAC). This is done to improve the performance without sacrificing security and to reduce the number of deps when we merge #443. --- Cargo.lock | 36 +++++++++++++++++++++++++++++++++--- Cargo.toml | 4 +--- cot/Cargo.toml | 4 +--- cot/src/auth.rs | 28 ++++++++++------------------ cot/src/auth/db.rs | 14 +++++--------- cot/src/lib.rs | 3 ++- cot/src/static_files.rs | 3 +-- deny.toml | 1 + 8 files changed, 54 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96d504f2..7050f248 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -166,6 +166,18 @@ dependencies = [ "password-hash", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "askama" version = "0.15.4" @@ -523,6 +535,20 @@ dependencies = [ "digest", ] +[[package]] +name = "blake3" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.2.17", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -807,6 +833,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "convert_case" version = "0.10.0" @@ -871,6 +903,7 @@ dependencies = [ "askama", "async-trait", "axum", + "blake3", "bytes", "chrono", "chrono-tz", @@ -882,7 +915,6 @@ dependencies = [ "deadpool-redis", "derive_builder", "derive_more", - "digest", "email_address", "fake", "fantoccini", @@ -891,7 +923,6 @@ dependencies = [ "futures-util", "grass", "hex", - "hmac", "http 1.4.0", "http-body-util", "humantime", @@ -913,7 +944,6 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sha2", "sqlx", "subtle", "swagger-ui-redist", diff --git a/Cargo.toml b/Cargo.toml index b445f0bb..7d3193af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ async-stream = "0.3" async-trait = "0.1" axum = { version = "0.8", default-features = false } backtrace = "0.3.76" +blake3 = "1.8.3" bytes = "1.11" cargo_toml = "0.22" chrono = { version = "0.4.44", default-features = false } @@ -85,7 +86,6 @@ darling = "0.23" deadpool-redis = { version = "0.23", default-features = false } derive_builder = "0.20" derive_more = "2" -digest = "0.10" email_address = "0.2.9" fake = "4" fantoccini = "0.22" @@ -97,7 +97,6 @@ glob = "0.3" grass = { version = "0.13.4", default-features = false } heck = "0.5" hex = "0.4" -hmac = "0.12" http = "1.4" http-body = "1" http-body-util = "0.1.3" @@ -130,7 +129,6 @@ serde_html_form = { version = "0.4", default-features = false } serde_json = "1" serde_path_to_error = "0.1.20" serde_urlencoded = "0.7" -sha2 = "0.10" sqlx = { version = "0.8", default-features = false } subtle = { version = "2", default-features = false } swagger-ui-redist = { version = "0.1" } diff --git a/cot/Cargo.toml b/cot/Cargo.toml index 9e83834b..4900c27b 100644 --- a/cot/Cargo.toml +++ b/cot/Cargo.toml @@ -20,6 +20,7 @@ aide = { workspace = true, optional = true } askama = { workspace = true, features = ["std"] } async-trait.workspace = true axum = { workspace = true, features = ["http1", "tokio"] } +blake3.workspace = true bytes.workspace = true chrono = { workspace = true, features = ["alloc", "serde", "clock"] } chrono-tz.workspace = true @@ -30,14 +31,12 @@ cot_macros.workspace = true deadpool-redis = { workspace = true, features = ["tokio-comp", "rt_tokio_1"], optional = true } derive_builder.workspace = true derive_more = { workspace = true, features = ["debug", "deref", "display", "from"] } -digest.workspace = true email_address.workspace = true fake = { workspace = true, optional = true, features = ["derive", "chrono"] } form_urlencoded.workspace = true futures-core.workspace = true futures-util.workspace = true hex.workspace = true -hmac.workspace = true http-body-util.workspace = true http.workspace = true humantime.workspace = true @@ -55,7 +54,6 @@ sea-query = { workspace = true, optional = true } sea-query-binder = { workspace = true, features = ["with-chrono", "runtime-tokio"], optional = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, optional = true } -sha2.workspace = true sqlx = { workspace = true, features = ["runtime-tokio", "chrono"], optional = true } subtle = { workspace = true, features = ["std"] } swagger-ui-redist = { workspace = true, optional = true } diff --git a/cot/src/auth.rs b/cot/src/auth.rs index e6bc9acc..0ee95c4f 100644 --- a/cot/src/auth.rs +++ b/cot/src/auth.rs @@ -180,16 +180,12 @@ pub trait User { /// use cot::auth::{SessionAuthHash, User, UserId}; /// use cot::common_types::Password; /// use cot::config::SecretKey; - /// use hmac::{Hmac, Mac}; - /// use sha2::Sha512; /// /// struct MyUser { /// id: i64, /// password: Password, /// } /// - /// type SessionAuthHmac = Hmac; - /// /// impl User for MyUser { /// fn id(&self) -> Option { /// Some(UserId::Int(self.id)) @@ -211,12 +207,12 @@ pub trait User { /// // thanks to this, the session hash is invalidated when the user changes their password /// // and the user is automatically logged out /// - /// let mut mac = SessionAuthHmac::new_from_slice(secret_key.as_bytes()) - /// .expect("HMAC can take key of any size"); - /// mac.update(self.password.as_str().as_bytes()); - /// let hmac_data = mac.finalize().into_bytes(); + /// const SESSION_AUTH_HASH_CONTEXT: &'static str = "cot.rs session auth hash v1"; + /// + /// let key = blake3::derive_key(SESSION_AUTH_HASH_CONTEXT, secret_key.as_bytes()); + /// let hash = blake3::keyed_hash(&key, self.password.as_str().as_bytes()); /// - /// Some(SessionAuthHash::new(&hmac_data)) + /// Some(SessionAuthHash::new(hash.as_slice())) /// } /// } /// ``` @@ -350,16 +346,12 @@ impl User for AnonymousUser {} /// use cot::auth::{SessionAuthHash, User, UserId}; /// use cot::common_types::Password; /// use cot::config::SecretKey; -/// use hmac::{Hmac, Mac}; -/// use sha2::Sha512; /// /// struct MyUser { /// id: i64, /// password: Password, /// } /// -/// type SessionAuthHmac = Hmac; -/// /// impl User for MyUser { /// fn id(&self) -> Option { /// Some(UserId::Int(self.id)) @@ -381,12 +373,12 @@ impl User for AnonymousUser {} /// // thanks to this, the session hash is invalidated when the user changes their password /// // and the user is automatically logged out /// -/// let mut mac = SessionAuthHmac::new_from_slice(secret_key.as_bytes()) -/// .expect("HMAC can take key of any size"); -/// mac.update(self.password.as_str().as_bytes()); -/// let hmac_data = mac.finalize().into_bytes(); +/// const SESSION_AUTH_HASH_CONTEXT: &'static str = "cot.rs session auth hash v1"; +/// +/// let key = blake3::derive_key(SESSION_AUTH_HASH_CONTEXT, secret_key.as_bytes()); +/// let hash = blake3::keyed_hash(&key, self.password.as_str().as_bytes()); /// -/// Some(SessionAuthHash::new(&hmac_data)) +/// Some(SessionAuthHash::new(hash.as_slice())) /// } /// } /// ``` diff --git a/cot/src/auth/db.rs b/cot/src/auth/db.rs index b7cf90b2..8eb6190f 100644 --- a/cot/src/auth/db.rs +++ b/cot/src/auth/db.rs @@ -12,8 +12,6 @@ use async_trait::async_trait; // can figure out it's an autogenerated field use cot::db::Auto; use cot_macros::AdminModel; -use hmac::{Hmac, Mac}; -use sha2::Sha512; use thiserror::Error; use crate::App; @@ -345,8 +343,6 @@ impl DatabaseUser { } } -type SessionAuthHmac = Hmac; - impl User for DatabaseUser { fn id(&self) -> Option { Some(UserId::Int(self.id())) @@ -365,12 +361,12 @@ impl User for DatabaseUser { } fn session_auth_hash(&self, secret_key: &SecretKey) -> Option { - let mut mac = SessionAuthHmac::new_from_slice(secret_key.as_bytes()) - .expect("HMAC can take key of any size"); - mac.update(self.password.as_str().as_bytes()); - let hmac_data = mac.finalize().into_bytes(); + const SESSION_AUTH_HASH_CONTEXT: &str = "cot.rs session auth hash v1"; + + let key = blake3::derive_key(SESSION_AUTH_HASH_CONTEXT, secret_key.as_bytes()); + let hash = blake3::keyed_hash(&key, self.password.as_str().as_bytes()); - Some(SessionAuthHash::new(&hmac_data)) + Some(SessionAuthHash::new(hash.as_slice())) } } diff --git a/cot/src/lib.rs b/cot/src/lib.rs index cc11ba1a..22ea64d5 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -84,6 +84,7 @@ pub(crate) mod utils; #[cfg(feature = "openapi")] pub use aide; +pub use bytes; /// A wrapper around a handler that's used in [`Bootstrapper`]. /// /// It is returned by [`Bootstrapper::finish`]. Typically, you don't need to @@ -181,9 +182,9 @@ pub use cot_macros::e2e_test; /// ``` pub use cot_macros::main; pub use cot_macros::test; +pub use http; #[cfg(feature = "openapi")] pub use schemars; -pub use {bytes, http}; pub use crate::__private::askama::{Template, filter_fn}; pub use crate::project::{ diff --git a/cot/src/static_files.rs b/cot/src/static_files.rs index f1504106..720bc4cd 100644 --- a/cot/src/static_files.rs +++ b/cot/src/static_files.rs @@ -13,7 +13,6 @@ use std::time::Duration; use bytes::Bytes; use cot_core::error::impl_into_cot_error; -use digest::Digest; use futures_core::ready; use http::{Request, header}; use pin_project_lite::pin_project; @@ -128,7 +127,7 @@ impl StaticFiles { #[must_use] fn file_hash(file: &StaticFile) -> String { - hex::encode(&sha2::Sha256::digest(&file.content).as_slice()[0..6]) + hex::encode(&blake3::hash(file.content.as_ref()).as_slice()[0..6]) } #[must_use] diff --git a/deny.toml b/deny.toml index 9ebe4dfe..44e84bbb 100644 --- a/deny.toml +++ b/deny.toml @@ -17,6 +17,7 @@ allow = [ "0BSD", "Apache-2.0 WITH LLVM-exception", "Apache-2.0", + "BSD-2-Clause", "BSD-3-Clause", "BSL-1.0", "CDLA-Permissive-2.0",