diff options
author | Alex Auvolat <alex@adnab.me> | 2024-02-29 10:57:07 +0100 |
---|---|---|
committer | Alex Auvolat <alex@adnab.me> | 2024-02-29 18:13:37 +0100 |
commit | c00a028cc8f11bb9f84b81a076ec5258998db276 (patch) | |
tree | f4cab7c16b905b85a024a24ee6a25281ab686a00 /src | |
parent | 9b44639844fe60add66286a161f69f817a2714cb (diff) | |
download | garage-c00a028cc8f11bb9f84b81a076ec5258998db276.tar.gz garage-c00a028cc8f11bb9f84b81a076ec5258998db276.zip |
[fix-auth-ct-eq] use argon2 hashing and verification for admin/metrics token checking
Diffstat (limited to 'src')
-rw-r--r-- | src/api/Cargo.toml | 1 | ||||
-rw-r--r-- | src/api/admin/api_server.rs | 59 |
2 files changed, 43 insertions, 17 deletions
diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml index 43167fdb..6a9cef8b 100644 --- a/src/api/Cargo.toml +++ b/src/api/Cargo.toml @@ -20,6 +20,7 @@ garage_block.workspace = true garage_util.workspace = true garage_rpc.workspace = true +argon2 = "0.5" async-trait = "0.1.7" base64 = "0.21" bytes = "1.0" diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs index 6f1e44e5..3e4b6acc 100644 --- a/src/api/admin/api_server.rs +++ b/src/api/admin/api_server.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::sync::Arc; +use argon2::password_hash::PasswordHash; use async_trait::async_trait; use futures::future::Future; @@ -42,14 +43,8 @@ impl AdminApiServer { #[cfg(feature = "metrics")] exporter: PrometheusExporter, ) -> Self { let cfg = &garage.config.admin; - let metrics_token = cfg - .metrics_token - .as_ref() - .map(|tok| format!("Bearer {}", tok)); - let admin_token = cfg - .admin_token - .as_ref() - .map(|tok| format!("Bearer {}", tok)); + let metrics_token = cfg.metrics_token.as_deref().map(hash_bearer_token); + let admin_token = cfg.admin_token.as_deref().map(hash_bearer_token); Self { garage, #[cfg(feature = "metrics")] @@ -237,11 +232,11 @@ impl ApiHandler for AdminApiServer { req: Request<Body>, endpoint: Endpoint, ) -> Result<Response<Body>, Error> { - let expected_auth_header = + let required_auth_hash = match endpoint.authorization_type() { Authorization::None => None, - Authorization::MetricsToken => self.metrics_token.as_ref(), - Authorization::AdminToken => match &self.admin_token { + Authorization::MetricsToken => self.metrics_token.as_deref(), + Authorization::AdminToken => match self.admin_token.as_deref() { None => return Err(Error::forbidden( "Admin token isn't configured, admin API access is disabled for security.", )), @@ -249,14 +244,11 @@ impl ApiHandler for AdminApiServer { }, }; - if let Some(h) = expected_auth_header { + if let Some(password_hash) = required_auth_hash { match req.headers().get("Authorization") { None => return Err(Error::forbidden("Authorization token must be provided")), - Some(v) => { - let authorized = v.to_str().map(|hv| hv.trim() == h).unwrap_or(false); - if !authorized { - return Err(Error::forbidden("Invalid authorization token provided")); - } + Some(authorization) => { + verify_bearer_token(&authorization, password_hash)?; } } } @@ -326,3 +318,36 @@ impl ApiEndpoint for Endpoint { fn add_span_attributes(&self, _span: SpanRef<'_>) {} } + +fn hash_bearer_token(token: &str) -> String { + use argon2::{ + password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, + Argon2, + }; + + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + argon2 + .hash_password(token.trim().as_bytes(), &salt) + .expect("could not hash API token") + .to_string() +} + +fn verify_bearer_token(token: &hyper::http::HeaderValue, password_hash: &str) -> Result<(), Error> { + use argon2::{password_hash::PasswordVerifier, Argon2}; + + let parsed_hash = PasswordHash::new(&password_hash).unwrap(); + + token + .to_str() + .ok() + .and_then(|header| header.strip_prefix("Bearer ")) + .and_then(|token| { + Argon2::default() + .verify_password(token.trim().as_bytes(), &parsed_hash) + .ok() + }) + .ok_or_else(|| Error::forbidden("Invalid authorization token"))?; + + Ok(()) +} |