diff --git a/crates/cli/src/commands/server.rs b/crates/cli/src/commands/server.rs index 827f0a3d..fff9756d 100644 --- a/crates/cli/src/commands/server.rs +++ b/crates/cli/src/commands/server.rs @@ -20,7 +20,7 @@ use futures_util::stream::{StreamExt, TryStreamExt}; use itertools::Itertools; use mas_config::RootConfig; use mas_email::Mailer; -use mas_handlers::{AppState, HttpClientFactory, MatrixHomeserver}; +use mas_handlers::{passwords::PasswordManager, AppState, HttpClientFactory, MatrixHomeserver}; use mas_listener::{server::Server, shutdown::ShutdownStream}; use mas_policy::PolicyFactory; use mas_router::UrlBuilder; @@ -168,6 +168,24 @@ impl Options { let listeners_config = config.http.listeners.clone(); + let password_manager = config + .passwords + .load() + .await + .context("failed to load the password schemes")? + .into_iter() + .map(|(version, algorithm, secret)| { + use mas_handlers::passwords::Hasher; + let hasher = match algorithm { + mas_config::PasswordAlgorithm::Pbkdf2 => Hasher::pbkdf2(secret), + mas_config::PasswordAlgorithm::Bcrypt { cost } => Hasher::bcrypt(cost, secret), + mas_config::PasswordAlgorithm::Argon2id => Hasher::argon2id(secret), + }; + + (version, hasher) + }); + let password_manager = PasswordManager::new(password_manager)?; + // Explicitely the config to properly zeroize secret keys drop(config); @@ -199,6 +217,7 @@ impl Options { policy_factory, graphql_schema, http_client_factory, + password_manager, }; let mut fd_manager = listenfd::ListenFd::from_env(); diff --git a/crates/config/src/sections/mod.rs b/crates/config/src/sections/mod.rs index df4482b7..d2c6280f 100644 --- a/crates/config/src/sections/mod.rs +++ b/crates/config/src/sections/mod.rs @@ -23,6 +23,7 @@ mod database; mod email; mod http; mod matrix; +mod passwords; mod policy; mod secrets; mod telemetry; @@ -38,6 +39,7 @@ pub use self::{ Resource as HttpResource, TlsConfig as HttpTlsConfig, UnixOrTcp, }, matrix::MatrixConfig, + passwords::{Algorithm as PasswordAlgorithm, PasswordsConfig}, policy::PolicyConfig, secrets::SecretsConfig, telemetry::{ @@ -82,6 +84,10 @@ pub struct RootConfig { /// Application secrets pub secrets: SecretsConfig, + /// Configuration related to user passwords + #[serde(default)] + pub passwords: PasswordsConfig, + /// Configuration related to the homeserver #[serde(default)] pub matrix: MatrixConfig, @@ -109,6 +115,7 @@ impl ConfigurationSection<'_> for RootConfig { templates: TemplatesConfig::generate(&mut rng).await?, csrf: CsrfConfig::generate(&mut rng).await?, email: EmailConfig::generate(&mut rng).await?, + passwords: PasswordsConfig::generate(&mut rng).await?, secrets: SecretsConfig::generate(&mut rng).await?, matrix: MatrixConfig::generate(&mut rng).await?, policy: PolicyConfig::generate(&mut rng).await?, @@ -122,6 +129,7 @@ impl ConfigurationSection<'_> for RootConfig { database: DatabaseConfig::test(), telemetry: TelemetryConfig::test(), templates: TemplatesConfig::test(), + passwords: PasswordsConfig::test(), csrf: CsrfConfig::test(), email: EmailConfig::test(), secrets: SecretsConfig::test(), diff --git a/crates/handlers/src/app_state.rs b/crates/handlers/src/app_state.rs index daea4ae0..c9c65090 100644 --- a/crates/handlers/src/app_state.rs +++ b/crates/handlers/src/app_state.rs @@ -23,7 +23,7 @@ use mas_router::UrlBuilder; use mas_templates::Templates; use sqlx::PgPool; -use crate::MatrixHomeserver; +use crate::{passwords::PasswordManager, MatrixHomeserver}; #[derive(Clone)] pub struct AppState { @@ -37,6 +37,7 @@ pub struct AppState { pub policy_factory: Arc, pub graphql_schema: mas_graphql::Schema, pub http_client_factory: HttpClientFactory, + pub password_manager: PasswordManager, } impl FromRef for PgPool { @@ -92,8 +93,15 @@ impl FromRef for Arc { input.policy_factory.clone() } } + impl FromRef for HttpClientFactory { fn from_ref(input: &AppState) -> Self { input.http_client_factory.clone() } } + +impl FromRef for PasswordManager { + fn from_ref(input: &AppState) -> Self { + input.password_manager.clone() + } +} diff --git a/crates/handlers/src/passwords.rs b/crates/handlers/src/passwords.rs index 6bfc80cf..eb9e7b6d 100644 --- a/crates/handlers/src/passwords.rs +++ b/crates/handlers/src/passwords.rs @@ -21,7 +21,7 @@ use pbkdf2::Pbkdf2; use rand::{CryptoRng, Rng, RngCore, SeedableRng}; use zeroize::Zeroizing; -pub type SchemeVersion = u32; +pub type SchemeVersion = u16; #[derive(Clone)] pub struct PasswordManager { diff --git a/docs/config.schema.json b/docs/config.schema.json index 5fe836da..d075dbd3 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -128,6 +128,22 @@ } ] }, + "passwords": { + "description": "Configuration related to user passwords", + "default": { + "schemes": [ + { + "algorithm": "argon2id", + "version": 1 + } + ] + }, + "allOf": [ + { + "$ref": "#/definitions/PasswordsConfig" + } + ] + }, "policy": { "description": "Configuration related to the OPA policies", "default": { @@ -647,6 +663,74 @@ } ] }, + "HashingScheme": { + "description": "A hashing algorithm", + "type": "object", + "oneOf": [ + { + "description": "bcrypt", + "type": "object", + "required": [ + "algorithm" + ], + "properties": { + "algorithm": { + "type": "string", + "enum": [ + "bcrypt" + ] + }, + "cost": { + "description": "Hashing cost", + "default": 12, + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + } + }, + { + "description": "argon2id", + "type": "object", + "required": [ + "algorithm" + ], + "properties": { + "algorithm": { + "type": "string", + "enum": [ + "argon2id" + ] + } + } + }, + { + "description": "PBKDF2", + "type": "object", + "required": [ + "algorithm" + ], + "properties": { + "algorithm": { + "type": "string", + "enum": [ + "pbkdf2" + ] + } + } + } + ], + "required": [ + "version" + ], + "properties": { + "version": { + "type": "integer", + "format": "uint16", + "minimum": 0.0 + } + } + }, "HttpConfig": { "description": "Configuration related to the web server", "type": "object", @@ -1209,6 +1293,24 @@ } ] }, + "PasswordsConfig": { + "description": "User password hashing config", + "type": "object", + "properties": { + "schemes": { + "default": [ + { + "algorithm": "argon2id", + "version": 1 + } + ], + "type": "array", + "items": { + "$ref": "#/definitions/HashingScheme" + } + } + } + }, "PolicyConfig": { "description": "Application secrets", "type": "object",