switched to blake3

This commit is contained in:
Hlars 2025-05-31 17:57:05 +02:00
parent 7d933e647b
commit ef9f0fd31c
7 changed files with 516 additions and 472 deletions

854
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -18,18 +18,16 @@ error-stack = "0.5.0"
# CLI # CLI
# ======================================== # ========================================
dotenv = "0.15" dotenv = "0.15"
clap = { version = "4.5.37", features = ["derive"] } clap = { version = "4.5.39", features = ["derive"] }
config = "0.15.11" config = "0.15.11"
colored = "3.0.0" colored = "3.0.0"
# User Authentication # User Authentication
# ======================================== # ========================================
uuid = { version = "1.16.0", features = ["v4"] } uuid = { version = "1.17.0", features = ["v4"] }
sha2 = "0.10.9" blake3 = "1.1.18"
hmac = "0.12.1"
minisign = "0.7.9" minisign = "0.7.9"
# axum-jwt-login = { path = "../axum-login-jwt" } axum-jwt-login = { version = "0.1.1", registry = "kellnr" }
axum-jwt-login = { version = "0.1.0", registry = "kellnr" }
rust-argon2 = "2.1.0" rust-argon2 = "2.1.0"
rand = "0.9.1" rand = "0.9.1"
ldap3 = "0.11.5" ldap3 = "0.11.5"
@ -43,7 +41,7 @@ validator = { version = "0.20.0", features = ["derive"] }
strum = { version = "0.27", features = ["derive"] } strum = { version = "0.27", features = ["derive"] }
utoipa = { version = "5.3.1", features = ["axum_extras"] } utoipa = { version = "5.3.1", features = ["axum_extras"] }
utoipa-axum = "0.2.0" utoipa-axum = "0.2.0"
utoipa-swagger-ui = { version = "9.0.1", features = ["axum"] } utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
utoipa-redoc = { version = "6.0.0", features = ["axum"] } utoipa-redoc = { version = "6.0.0", features = ["axum"] }
utoipa-scalar = { version = "0.3.0", features = ["axum"] } utoipa-scalar = { version = "0.3.0", features = ["axum"] }
ts-rs = { version = "10.1.0", features = ["chrono-impl"] } ts-rs = { version = "10.1.0", features = ["chrono-impl"] }
@ -52,11 +50,11 @@ ts-rs = { version = "10.1.0", features = ["chrono-impl"] }
# ======================================== # ========================================
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140" serde_json = "1.0.140"
tokio = { version = "1.44.2", features = ["full"] } tokio = { version = "1.45.1", features = ["full"] }
tokio-util = { version = "0.7.15", features = ["rt"] } tokio-util = { version = "0.7.15", features = ["rt"] }
tokio-stream = { version = "0.1.17", features = ["sync"] } tokio-stream = { version = "0.1.17", features = ["sync"] }
futures = "0.3" futures = "0.3"
once_cell = "1.21.3" once_cell = "1.21.3"
sqlx = { version = "0.8.5", features = ["runtime-tokio", "postgres", "chrono"] } sqlx = { version = "0.8.6", features = ["runtime-tokio", "postgres", "chrono"] }
chrono = { version = "0.4.41", features = ["serde"] } chrono = { version = "0.4.41", features = ["serde"] }
zip = "2.6.1" zip = "4.0.0"

View File

@ -1,62 +1,61 @@
use hmac::{Hmac, Mac}; use blake3::{Hash, Hasher};
use sha2::{Digest, Sha512};
use crate::errors::ApiError; use crate::errors::ApiError;
type HashType = Sha512;
pub trait CustomHash { pub trait CustomHash {
fn updater(&self, hasher: &mut Sha512); fn updater(&self, hasher: &mut Hasher);
fn hash(&self) -> Vec<u8> { fn hash(&self) -> Hash {
let mut hasher = Sha512::new(); let mut hasher = Hasher::new();
Self::updater(&self, &mut hasher); Self::updater(&self, &mut hasher);
hasher.finalize().to_vec() hasher.finalize()
} }
} }
pub struct IntegrityVerifier { pub struct IntegrityVerifier {
mac: Hmac<HashType>, mac: Hasher,
} }
impl IntegrityVerifier { impl IntegrityVerifier {
pub fn new() -> Result<Self, ApiError> { pub fn new() -> Result<Self, ApiError> {
Ok(Self { Ok(Self {
mac: Hmac::<HashType>::new_from_slice( mac: Hasher::new_derive_key(include_str!("./verification_secret")),
include_str!("./verification_secret").as_bytes(),
)?,
}) })
} }
pub fn sign<T>(&self, data: &T) -> Result<Vec<u8>, ApiError> pub fn sign<T>(&self, data: &T) -> Result<Hash, ApiError>
where where
T: CustomHash, T: CustomHash,
{ {
let mut mac = self.mac.clone(); let mut mac = self.mac.clone();
mac.update(&data.hash()); mac.update(data.hash().as_bytes());
let signature = mac.finalize_reset().into_bytes(); let signature = mac.finalize();
mac.reset();
Ok(signature.to_vec()) Ok(signature)
} }
pub fn verify<T>(&self, data: &T, signature: &[u8]) -> Result<(), ApiError> pub fn verify<T>(&self, data: &T, signature: Hash) -> Result<(), ApiError>
where where
T: CustomHash, T: CustomHash,
{ {
let mut mac = self.mac.clone(); let mut mac = self.mac.clone();
mac.update(&data.hash()); mac.update(data.hash().as_bytes());
let verify = mac.finalize();
mac.reset();
match mac.verify_slice_reset(signature) { match verify == signature {
Ok(_) => Ok(()), true => Ok(()),
Err(_e) => Err(ApiError::AccessDenied), false => Err(ApiError::AccessDenied),
} }
} }
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use hmac::digest::Update;
use blake3::Hasher;
use crate::api::backend::integrity_verification::{CustomHash, IntegrityVerifier}; use crate::api::backend::integrity_verification::{CustomHash, IntegrityVerifier};
@ -68,7 +67,7 @@ mod test {
} }
impl CustomHash for TestData { impl CustomHash for TestData {
fn updater(&self, hasher: &mut sha2::Sha512) { fn updater(&self, hasher: &mut Hasher) {
hasher.update(&self.one.to_be_bytes()); hasher.update(&self.one.to_be_bytes());
hasher.update(&self.two.as_bytes()); hasher.update(&self.two.as_bytes());
} }
@ -82,9 +81,9 @@ mod test {
}; };
let signature = verifier.sign(&item).unwrap(); let signature = verifier.sign(&item).unwrap();
assert!(verifier.verify(&item, &signature).is_ok()); assert!(verifier.verify(&item, signature).is_ok());
item.two = "Wrong".to_string(); item.two = "Wrong".to_string();
assert!(verifier.verify(&item, &signature).is_err()); assert!(verifier.verify(&item, signature).is_err());
} }
} }

View File

@ -1,10 +1,9 @@
use std::collections::HashSet; use std::collections::HashSet;
use axum_jwt_login::ApiKey as ApiKeyTrait; use axum_jwt_login::ApiKey as ApiKeyTrait;
use blake3::{Hash, Hasher};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::Sha512;
use sqlx::PgPool; use sqlx::PgPool;
use ts_rs::TS; use ts_rs::TS;
use utoipa::ToSchema; use utoipa::ToSchema;
@ -27,7 +26,7 @@ const KEY_LENGTH: usize = 40;
pub struct ApiKey { pub struct ApiKey {
pub id: String, pub id: String,
#[serde(skip)] // Don't leak Hash #[serde(skip)] // Don't leak Hash
pub hash: Vec<u8>, pub hash: String,
pub name: String, pub name: String,
pub auth_required: bool, pub auth_required: bool,
#[schema(inline)] #[schema(inline)]
@ -68,9 +67,9 @@ impl ApiKey {
// create API Key secret part // create API Key secret part
let key: String = create_random(KEY_LENGTH); let key: String = create_random(KEY_LENGTH);
// calculate API Key signature // calculate API Key signature
let mut mac = Hmac::<Sha512>::new_from_slice(config.token_secret.as_bytes())?; let mut hasher = Hasher::new_derive_key(&config.token_secret);
mac.update(key.as_bytes()); hasher.update(key.as_bytes());
let signature = mac.finalize(); let signature = hasher.finalize();
// Return Api Key Secret and Api Key // Return Api Key Secret and Api Key
Ok(( Ok((
@ -79,7 +78,7 @@ impl ApiKey {
name: name.to_string(), name: name.to_string(),
auth_required: requires_auth, auth_required: requires_auth,
id: uuid.to_string(), id: uuid.to_string(),
hash: signature.into_bytes().as_slice().to_vec(), hash: signature.to_hex().to_string(),
permissions: PermissionContainer(permissions), permissions: PermissionContainer(permissions),
api_config_secret: Some(config.token_secret.clone()), api_config_secret: Some(config.token_secret.clone()),
creation_date: None, creation_date: None,
@ -91,20 +90,29 @@ impl ApiKey {
pub fn validate(&self, key_secret_part: &str) -> Result<(), ApiError> { pub fn validate(&self, key_secret_part: &str) -> Result<(), ApiError> {
// calculate API Key signature // calculate API Key signature
let mut mac = Hmac::<Sha512>::new_from_slice( // let mut mac = Hmac::<Sha512>::new_from_slice(
self.api_config_secret // self.api_config_secret
.clone() // .clone()
.ok_or(ApiError::InternalError( // .ok_or(ApiError::InternalError(
"Missing API Config Secret".to_string(), // "Missing API Config Secret".to_string(),
))? // ))?
.as_bytes(), // .as_bytes(),
)?; // )?;
mac.update(key_secret_part.as_bytes()); // mac.update(key_secret_part.as_bytes());
let mut hasher = Hasher::new_derive_key(&self.api_config_secret.clone().ok_or(
ApiError::InternalError("Missing API Config Secret".to_string()),
)?);
hasher.update(key_secret_part.as_bytes());
let signature = hasher.finalize();
match mac.verify_slice(&self.hash) { match signature == Hash::from_hex(self.hash.clone())? {
Ok(_) => Ok(()), true => Ok(()),
Err(_e) => Err(ApiError::AccessDenied), false => Err(ApiError::AccessDenied),
} }
// match mac.verify_slice(&self.hash) {
// Ok(_) => Ok(()),
// Err(_e) => Err(ApiError::AccessDenied),
// }
} }
} }

View File

@ -1,5 +1,5 @@
use blake3::Hasher;
use serde::Serialize; use serde::Serialize;
use sha2::{Digest, Sha256};
use tokio_util::bytes::Bytes; use tokio_util::bytes::Bytes;
use ts_rs::TS; use ts_rs::TS;
use utoipa::ToSchema; use utoipa::ToSchema;
@ -23,10 +23,10 @@ pub struct File {
impl File { impl File {
pub fn hash(&self) -> String { pub fn hash(&self) -> String {
let mut hasher = Sha256::new(); let mut hasher = Hasher::new();
hasher.update(&self.data); hasher.update(&self.data);
let hash = hasher.finalize(); let hash = hasher.finalize();
format!("{hash:X}") hash.to_hex().to_string()
} }
} }

View File

@ -9,7 +9,6 @@ use axum::{
use error_stack::Context; use error_stack::Context;
use minisign::PError; use minisign::PError;
use serde::Serialize; use serde::Serialize;
use sha2::digest::InvalidLength;
use tracing::error; use tracing::error;
use validator::ValidationErrors; use validator::ValidationErrors;
use zip::result::ZipError; use zip::result::ZipError;
@ -111,15 +110,15 @@ impl From<axum_jwt_login::Error<User, ApiBackend>> for ApiError {
} }
} }
impl From<argon2::Error> for ApiError { impl From<blake3::HexError> for ApiError {
fn from(_: argon2::Error) -> Self { fn from(e: blake3::HexError) -> Self {
Self::InvalidCredentials Self::InternalError(format!("Could not create Hash from Hex: {e}"))
} }
} }
impl From<InvalidLength> for ApiError { impl From<argon2::Error> for ApiError {
fn from(value: InvalidLength) -> Self { fn from(_: argon2::Error) -> Self {
Self::InternalError(format!("Invalid HMac Key length: {value}")) Self::InvalidCredentials
} }
} }

View File

@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS public.apikeys
"UserAuthRequired" boolean NOT NULL DEFAULT true, "UserAuthRequired" boolean NOT NULL DEFAULT true,
"CreationDate" timestamp without time zone NOT NULL DEFAULT now(), "CreationDate" timestamp without time zone NOT NULL DEFAULT now(),
"LastChanged" timestamp without time zone NOT NULL DEFAULT now(), "LastChanged" timestamp without time zone NOT NULL DEFAULT now(),
"Hash" bytea NOT NULL, "Hash" character varying COLLATE pg_catalog."default" NOT NULL,
"Status" smallint NOT NULL DEFAULT 1, "Status" smallint NOT NULL DEFAULT 1,
CONSTRAINT apikeys_pkey PRIMARY KEY ("KeyID") CONSTRAINT apikeys_pkey PRIMARY KEY ("KeyID")
) )