diff --git a/Cargo.lock b/Cargo.lock index c941056d..78badeb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -624,7 +624,6 @@ dependencies = [ name = "conduit_api" version = "0.4.1" dependencies = [ - "argon2", "axum 0.7.5", "axum-extra", "base64 0.22.1", @@ -657,6 +656,7 @@ dependencies = [ name = "conduit_core" version = "0.4.1" dependencies = [ + "argon2", "axum 0.7.5", "axum-server", "bytes", @@ -748,7 +748,6 @@ dependencies = [ name = "conduit_service" version = "0.4.1" dependencies = [ - "argon2", "async-trait", "base64 0.22.1", "bytes", diff --git a/Cargo.toml b/Cargo.toml index ee9c0100..22df188d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -815,6 +815,10 @@ verbose_file_reads = "warn" ################### style = "warn" +## some sadness +# trivial assertions are quite alright +assertions_on_constants = "allow" + ################### suspicious = "warn" diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml index a80a254e..5ac15dd5 100644 --- a/src/api/Cargo.toml +++ b/src/api/Cargo.toml @@ -33,7 +33,6 @@ brotli_compression = [ ] [dependencies] -argon2.workspace = true axum-extra.workspace = true axum.workspace = true base64.workspace = true diff --git a/src/api/client_server/session.rs b/src/api/client_server/session.rs index 28df625f..6df46549 100644 --- a/src/api/client_server/session.rs +++ b/src/api/client_server/session.rs @@ -1,4 +1,3 @@ -use argon2::{PasswordHash, PasswordVerifier}; use ruma::{ api::client::{ error::ErrorKind, @@ -21,7 +20,7 @@ use serde::Deserialize; use tracing::{debug, info, warn}; use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH}; -use crate::{services, utils, Error, Result, Ruma}; +use crate::{services, utils, utils::hash, Error, Result, Ruma}; #[derive(Debug, Deserialize)] struct Claims { @@ -87,15 +86,7 @@ pub(crate) async fn login_route(body: Ruma) -> Result>> = Mutex::new(None); + +#[allow(clippy::let_underscore_must_use)] +pub fn init() { + // 19456 Kib blocks, iterations = 2, parallelism = 1 + // * + debug_assert!(M_COST == 19_456, "M_COST default changed"); + debug_assert!(T_COST == 2, "T_COST default changed"); + debug_assert!(P_COST == 1, "P_COST default changed"); + + let algorithm = Algorithm::Argon2id; + let version = Version::default(); + let out_len: Option = None; + let params = Params::new(M_COST, T_COST, P_COST, out_len).expect("valid parameters"); + let state = Argon2::new(algorithm, version, params); + _ = STATE.lock().expect("hashing state locked").insert(state); +} + +pub fn password(password: &str) -> Result { + let salt = SaltString::generate(rand::thread_rng()); + STATE + .lock() + .expect("hashing state locked") + .as_ref() + .expect("hashing state initialized") + .hash_password(password.as_bytes(), &salt) + .map(|it| it.to_string()) +} + +pub fn verify_password(password: &str, password_hash: &str) -> Result<(), password_hash::Error> { + let password_hash = PasswordHash::new(password_hash)?; + STATE + .lock() + .expect("hashing state locked") + .as_ref() + .expect("hashing state initialized") + .verify_password(password.as_bytes(), &password_hash) +} + +#[cfg(test)] +mod tests { + #[test] + fn password_hash_and_verify() { + use crate::utils::hash; + hash::init(); + let preimage = "temp123"; + let digest = hash::password(preimage).expect("digest"); + hash::verify_password(preimage, &digest).expect("verified"); + } + + #[test] + #[should_panic(expected = "unverified")] + fn password_hash_and_verify_fail() { + use crate::utils::hash; + hash::init(); + let preimage = "temp123"; + let fakeimage = "temp321"; + let digest = hash::password(preimage).expect("digest"); + hash::verify_password(fakeimage, &digest).expect("unverified"); + } +} diff --git a/src/core/utils/mod.rs b/src/core/utils/mod.rs index e85080f1..f3a45e47 100644 --- a/src/core/utils/mod.rs +++ b/src/core/utils/mod.rs @@ -1,6 +1,7 @@ pub mod content_disposition; pub mod debug; pub mod defer; +pub mod hash; pub mod html; pub mod json; pub mod sys; diff --git a/src/main/server.rs b/src/main/server.rs index 8e61d502..cd5b1cdb 100644 --- a/src/main/server.rs +++ b/src/main/server.rs @@ -5,7 +5,7 @@ use conduit::{ config::Config, info, log::{LogLevelReloadHandles, ReloadHandle}, - utils::sys::maximize_fd_limit, + utils::{hash, sys}, Error, Result, }; use tokio::runtime; @@ -31,13 +31,16 @@ pub(crate) struct Server { impl Server { pub(crate) fn build(args: Args, runtime: Option<&runtime::Handle>) -> Result, Error> { let config = Config::new(args.config)?; + #[cfg(feature = "sentry_telemetry")] let sentry_guard = init_sentry(&config); let (tracing_reload_handle, tracing_flame_guard) = init_tracing(&config); config.check()?; #[cfg(unix)] - maximize_fd_limit().expect("Unable to increase maximum soft and hard file descriptor limit"); + sys::maximize_fd_limit().expect("Unable to increase maximum soft and hard file descriptor limit"); + hash::init(); + info!( server_name = %config.server_name, database_path = ?config.database_path, diff --git a/src/service/Cargo.toml b/src/service/Cargo.toml index 2cdf6240..3bfd0e51 100644 --- a/src/service/Cargo.toml +++ b/src/service/Cargo.toml @@ -35,7 +35,6 @@ sha256_media = [ ] [dependencies] -argon2.workspace = true async-trait.workspace = true base64.workspace = true bytes.workspace = true diff --git a/src/service/globals/migrations.rs b/src/service/globals/migrations.rs index 0e04caeb..75990df0 100644 --- a/src/service/globals/migrations.rs +++ b/src/service/globals/migrations.rs @@ -6,10 +6,8 @@ use std::{ sync::Arc, }; -use argon2::{password_hash::SaltString, PasswordHasher, PasswordVerifier}; use database::KeyValueDatabase; use itertools::Itertools; -use rand::thread_rng; use ruma::{ events::{push_rules::PushRulesEvent, GlobalAccountDataEventType}, push::Ruleset, @@ -67,18 +65,9 @@ pub(crate) async fn migrations(db: &KeyValueDatabase, config: &Config) -> Result if services().globals.database_version()? < 2 { // We accidentally inserted hashed versions of "" into the db instead of just "" for (userid, password) in db.userid_password.iter() { - let salt = SaltString::generate(thread_rng()); - let empty_pass = services() - .globals - .argon - .hash_password(b"", &salt) - .expect("our own password to be properly hashed"); - let empty_hashed_password = services() - .globals - .argon - .verify_password(&password, &empty_pass) - .is_ok(); - + let empty_pass = utils::hash::password("").expect("our own password to be properly hashed"); + let password = std::str::from_utf8(&password).expect("password is valid utf-8"); + let empty_hashed_password = utils::hash::verify_password(password, &empty_pass).is_ok(); if empty_hashed_password { db.userid_password.insert(&userid, b"")?; } diff --git a/src/service/globals/mod.rs b/src/service/globals/mod.rs index b30d1566..7543741f 100644 --- a/src/service/globals/mod.rs +++ b/src/service/globals/mod.rs @@ -14,7 +14,6 @@ use std::{ time::Instant, }; -use argon2::Argon2; use base64::{engine::general_purpose, Engine as _}; use data::Data; use hickory_resolver::TokioAsyncResolver; @@ -61,7 +60,6 @@ pub struct Service { pub updates_handle: Mutex>>, pub stateres_mutex: Arc>, pub rotate: RotationHandler, - pub argon: Argon2<'static>, } /// Handles "rotation" of long-polling requests. "Rotation" in this context is @@ -125,13 +123,6 @@ impl Service { // Experimental, partially supported room versions let unstable_room_versions = vec![RoomVersionId::V2, RoomVersionId::V3, RoomVersionId::V4, RoomVersionId::V5]; - // 19456 Kib blocks, iterations = 2, parallelism = 1 for more info https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id - let argon = Argon2::new( - argon2::Algorithm::Argon2id, - argon2::Version::default(), - argon2::Params::new(19456, 2, 1, None).expect("valid parameters"), - ); - let mut cidr_range_denylist = Vec::new(); for cidr in config.ip_range_denylist.clone() { let cidr = IPAddress::parse(cidr).expect("valid cidr range"); @@ -159,7 +150,6 @@ impl Service { updates_handle: Mutex::new(None), stateres_mutex: Arc::new(Mutex::new(())), rotate: RotationHandler::new(), - argon, }; fs::create_dir_all(s.get_media_folder())?; diff --git a/src/service/uiaa/mod.rs b/src/service/uiaa/mod.rs index 63293867..11f848ad 100644 --- a/src/service/uiaa/mod.rs +++ b/src/service/uiaa/mod.rs @@ -2,8 +2,7 @@ mod data; use std::sync::Arc; -use argon2::{PasswordHash, PasswordVerifier}; -use conduit::{utils, Error, Result}; +use conduit::{utils, utils::hash, Error, Result}; use data::Data; use ruma::{ api::client::{ @@ -70,15 +69,7 @@ impl Service { // Check if password is correct if let Some(hash) = services().users.password_hash(&user_id)? { - let hash_matches = services() - .globals - .argon - .verify_password( - password.as_bytes(), - &PasswordHash::new(&hash).expect("valid hash in database"), - ) - .is_ok(); - + let hash_matches = hash::verify_password(password, &hash).is_ok(); if !hash_matches { uiaainfo.auth_error = Some(ruma::api::client::error::StandardErrorBody { kind: ErrorKind::forbidden(), diff --git a/src/service/users/data.rs b/src/service/users/data.rs index 1254a988..f2355755 100644 --- a/src/service/users/data.rs +++ b/src/service/users/data.rs @@ -1,6 +1,5 @@ use std::{collections::BTreeMap, mem::size_of}; -use argon2::{password_hash::SaltString, PasswordHasher}; use ruma::{ api::client::{device::Device, error::ErrorKind, filter::FilterDefinition}, encryption::{CrossSigningKey, DeviceKeys, OneTimeKey}, @@ -227,7 +226,7 @@ impl Data for KeyValueDatabase { /// Hash and set the user's password to the Argon2 hash fn set_password(&self, user_id: &UserId, password: Option<&str>) -> Result<()> { if let Some(password) = password { - if let Ok(hash) = calculate_password_hash(password) { + if let Ok(hash) = utils::hash::password(password) { self.userid_password .insert(user_id.as_bytes(), hash.as_bytes())?; Ok(()) @@ -1021,13 +1020,3 @@ fn get_username_with_valid_password(username: &[u8], password: &[u8]) -> Option< } } } - -/// Calculate a new hash for the given password -fn calculate_password_hash(password: &str) -> Result { - let salt = SaltString::generate(rand::thread_rng()); - services() - .globals - .argon - .hash_password(password.as_bytes(), &salt) - .map(|it| it.to_string()) -}