Abstract password hashing into util.
Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
parent
282c2feca8
commit
732e8b82aa
13 changed files with 92 additions and 64 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -624,7 +624,6 @@ dependencies = [
|
||||||
name = "conduit_api"
|
name = "conduit_api"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
|
||||||
"axum 0.7.5",
|
"axum 0.7.5",
|
||||||
"axum-extra",
|
"axum-extra",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
@ -657,6 +656,7 @@ dependencies = [
|
||||||
name = "conduit_core"
|
name = "conduit_core"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"argon2",
|
||||||
"axum 0.7.5",
|
"axum 0.7.5",
|
||||||
"axum-server",
|
"axum-server",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -748,7 +748,6 @@ dependencies = [
|
||||||
name = "conduit_service"
|
name = "conduit_service"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
|
@ -815,6 +815,10 @@ verbose_file_reads = "warn"
|
||||||
###################
|
###################
|
||||||
style = "warn"
|
style = "warn"
|
||||||
|
|
||||||
|
## some sadness
|
||||||
|
# trivial assertions are quite alright
|
||||||
|
assertions_on_constants = "allow"
|
||||||
|
|
||||||
###################
|
###################
|
||||||
suspicious = "warn"
|
suspicious = "warn"
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,6 @@ brotli_compression = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
argon2.workspace = true
|
|
||||||
axum-extra.workspace = true
|
axum-extra.workspace = true
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use argon2::{PasswordHash, PasswordVerifier};
|
|
||||||
use ruma::{
|
use ruma::{
|
||||||
api::client::{
|
api::client::{
|
||||||
error::ErrorKind,
|
error::ErrorKind,
|
||||||
|
@ -21,7 +20,7 @@ use serde::Deserialize;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
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)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct Claims {
|
struct Claims {
|
||||||
|
@ -87,15 +86,7 @@ pub(crate) async fn login_route(body: Ruma<login::v3::Request>) -> Result<login:
|
||||||
return Err(Error::BadRequest(ErrorKind::UserDeactivated, "The user has been deactivated"));
|
return Err(Error::BadRequest(ErrorKind::UserDeactivated, "The user has been deactivated"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsed_hash = PasswordHash::new(&hash)
|
if hash::verify_password(password, &hash).is_err() {
|
||||||
.map_err(|_| Error::BadServerResponse("Unknown error occurred hashing password."))?;
|
|
||||||
|
|
||||||
if services()
|
|
||||||
.globals
|
|
||||||
.argon
|
|
||||||
.verify_password(password.as_bytes(), &parsed_hash)
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
return Err(Error::BadRequest(ErrorKind::forbidden(), "Wrong username or password."));
|
return Err(Error::BadRequest(ErrorKind::forbidden(), "Wrong username or password."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,7 @@ perf_measurements = []
|
||||||
sentry_telemetry = []
|
sentry_telemetry = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
argon2.workspace = true
|
||||||
axum-server.workspace = true
|
axum-server.workspace = true
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
bytes.workspace = true
|
bytes.workspace = true
|
||||||
|
|
72
src/core/utils/hash.rs
Normal file
72
src/core/utils/hash.rs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use argon2::{
|
||||||
|
password_hash, password_hash::SaltString, Algorithm, Argon2, Params, PasswordHash, PasswordHasher,
|
||||||
|
PasswordVerifier, Version,
|
||||||
|
};
|
||||||
|
|
||||||
|
const M_COST: u32 = Params::DEFAULT_M_COST; // memory size in 1 KiB blocks
|
||||||
|
const T_COST: u32 = Params::DEFAULT_T_COST; // nr of iterations
|
||||||
|
const P_COST: u32 = Params::DEFAULT_P_COST; // parallelism
|
||||||
|
|
||||||
|
static STATE: Mutex<Option<Argon2<'static>>> = Mutex::new(None);
|
||||||
|
|
||||||
|
#[allow(clippy::let_underscore_must_use)]
|
||||||
|
pub fn init() {
|
||||||
|
// 19456 Kib blocks, iterations = 2, parallelism = 1
|
||||||
|
// * <https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id>
|
||||||
|
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<usize> = 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<String, password_hash::Error> {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
pub mod content_disposition;
|
pub mod content_disposition;
|
||||||
pub mod debug;
|
pub mod debug;
|
||||||
pub mod defer;
|
pub mod defer;
|
||||||
|
pub mod hash;
|
||||||
pub mod html;
|
pub mod html;
|
||||||
pub mod json;
|
pub mod json;
|
||||||
pub mod sys;
|
pub mod sys;
|
||||||
|
|
|
@ -5,7 +5,7 @@ use conduit::{
|
||||||
config::Config,
|
config::Config,
|
||||||
info,
|
info,
|
||||||
log::{LogLevelReloadHandles, ReloadHandle},
|
log::{LogLevelReloadHandles, ReloadHandle},
|
||||||
utils::sys::maximize_fd_limit,
|
utils::{hash, sys},
|
||||||
Error, Result,
|
Error, Result,
|
||||||
};
|
};
|
||||||
use tokio::runtime;
|
use tokio::runtime;
|
||||||
|
@ -31,13 +31,16 @@ pub(crate) struct Server {
|
||||||
impl Server {
|
impl Server {
|
||||||
pub(crate) fn build(args: Args, runtime: Option<&runtime::Handle>) -> Result<Arc<Server>, Error> {
|
pub(crate) fn build(args: Args, runtime: Option<&runtime::Handle>) -> Result<Arc<Server>, Error> {
|
||||||
let config = Config::new(args.config)?;
|
let config = Config::new(args.config)?;
|
||||||
|
|
||||||
#[cfg(feature = "sentry_telemetry")]
|
#[cfg(feature = "sentry_telemetry")]
|
||||||
let sentry_guard = init_sentry(&config);
|
let sentry_guard = init_sentry(&config);
|
||||||
let (tracing_reload_handle, tracing_flame_guard) = init_tracing(&config);
|
let (tracing_reload_handle, tracing_flame_guard) = init_tracing(&config);
|
||||||
|
|
||||||
config.check()?;
|
config.check()?;
|
||||||
#[cfg(unix)]
|
#[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!(
|
info!(
|
||||||
server_name = %config.server_name,
|
server_name = %config.server_name,
|
||||||
database_path = ?config.database_path,
|
database_path = ?config.database_path,
|
||||||
|
|
|
@ -35,7 +35,6 @@ sha256_media = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
argon2.workspace = true
|
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
bytes.workspace = true
|
bytes.workspace = true
|
||||||
|
|
|
@ -6,10 +6,8 @@ use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use argon2::{password_hash::SaltString, PasswordHasher, PasswordVerifier};
|
|
||||||
use database::KeyValueDatabase;
|
use database::KeyValueDatabase;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use rand::thread_rng;
|
|
||||||
use ruma::{
|
use ruma::{
|
||||||
events::{push_rules::PushRulesEvent, GlobalAccountDataEventType},
|
events::{push_rules::PushRulesEvent, GlobalAccountDataEventType},
|
||||||
push::Ruleset,
|
push::Ruleset,
|
||||||
|
@ -67,18 +65,9 @@ pub(crate) async fn migrations(db: &KeyValueDatabase, config: &Config) -> Result
|
||||||
if services().globals.database_version()? < 2 {
|
if services().globals.database_version()? < 2 {
|
||||||
// We accidentally inserted hashed versions of "" into the db instead of just ""
|
// We accidentally inserted hashed versions of "" into the db instead of just ""
|
||||||
for (userid, password) in db.userid_password.iter() {
|
for (userid, password) in db.userid_password.iter() {
|
||||||
let salt = SaltString::generate(thread_rng());
|
let empty_pass = utils::hash::password("").expect("our own password to be properly hashed");
|
||||||
let empty_pass = services()
|
let password = std::str::from_utf8(&password).expect("password is valid utf-8");
|
||||||
.globals
|
let empty_hashed_password = utils::hash::verify_password(password, &empty_pass).is_ok();
|
||||||
.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();
|
|
||||||
|
|
||||||
if empty_hashed_password {
|
if empty_hashed_password {
|
||||||
db.userid_password.insert(&userid, b"")?;
|
db.userid_password.insert(&userid, b"")?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ use std::{
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
|
|
||||||
use argon2::Argon2;
|
|
||||||
use base64::{engine::general_purpose, Engine as _};
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
use data::Data;
|
use data::Data;
|
||||||
use hickory_resolver::TokioAsyncResolver;
|
use hickory_resolver::TokioAsyncResolver;
|
||||||
|
@ -61,7 +60,6 @@ pub struct Service {
|
||||||
pub updates_handle: Mutex<Option<JoinHandle<()>>>,
|
pub updates_handle: Mutex<Option<JoinHandle<()>>>,
|
||||||
pub stateres_mutex: Arc<Mutex<()>>,
|
pub stateres_mutex: Arc<Mutex<()>>,
|
||||||
pub rotate: RotationHandler,
|
pub rotate: RotationHandler,
|
||||||
pub argon: Argon2<'static>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles "rotation" of long-polling requests. "Rotation" in this context is
|
/// Handles "rotation" of long-polling requests. "Rotation" in this context is
|
||||||
|
@ -125,13 +123,6 @@ impl Service {
|
||||||
// Experimental, partially supported room versions
|
// Experimental, partially supported room versions
|
||||||
let unstable_room_versions = vec![RoomVersionId::V2, RoomVersionId::V3, RoomVersionId::V4, RoomVersionId::V5];
|
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();
|
let mut cidr_range_denylist = Vec::new();
|
||||||
for cidr in config.ip_range_denylist.clone() {
|
for cidr in config.ip_range_denylist.clone() {
|
||||||
let cidr = IPAddress::parse(cidr).expect("valid cidr range");
|
let cidr = IPAddress::parse(cidr).expect("valid cidr range");
|
||||||
|
@ -159,7 +150,6 @@ impl Service {
|
||||||
updates_handle: Mutex::new(None),
|
updates_handle: Mutex::new(None),
|
||||||
stateres_mutex: Arc::new(Mutex::new(())),
|
stateres_mutex: Arc::new(Mutex::new(())),
|
||||||
rotate: RotationHandler::new(),
|
rotate: RotationHandler::new(),
|
||||||
argon,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fs::create_dir_all(s.get_media_folder())?;
|
fs::create_dir_all(s.get_media_folder())?;
|
||||||
|
|
|
@ -2,8 +2,7 @@ mod data;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use argon2::{PasswordHash, PasswordVerifier};
|
use conduit::{utils, utils::hash, Error, Result};
|
||||||
use conduit::{utils, Error, Result};
|
|
||||||
use data::Data;
|
use data::Data;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
api::client::{
|
api::client::{
|
||||||
|
@ -70,15 +69,7 @@ impl Service {
|
||||||
|
|
||||||
// Check if password is correct
|
// Check if password is correct
|
||||||
if let Some(hash) = services().users.password_hash(&user_id)? {
|
if let Some(hash) = services().users.password_hash(&user_id)? {
|
||||||
let hash_matches = services()
|
let hash_matches = hash::verify_password(password, &hash).is_ok();
|
||||||
.globals
|
|
||||||
.argon
|
|
||||||
.verify_password(
|
|
||||||
password.as_bytes(),
|
|
||||||
&PasswordHash::new(&hash).expect("valid hash in database"),
|
|
||||||
)
|
|
||||||
.is_ok();
|
|
||||||
|
|
||||||
if !hash_matches {
|
if !hash_matches {
|
||||||
uiaainfo.auth_error = Some(ruma::api::client::error::StandardErrorBody {
|
uiaainfo.auth_error = Some(ruma::api::client::error::StandardErrorBody {
|
||||||
kind: ErrorKind::forbidden(),
|
kind: ErrorKind::forbidden(),
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use std::{collections::BTreeMap, mem::size_of};
|
use std::{collections::BTreeMap, mem::size_of};
|
||||||
|
|
||||||
use argon2::{password_hash::SaltString, PasswordHasher};
|
|
||||||
use ruma::{
|
use ruma::{
|
||||||
api::client::{device::Device, error::ErrorKind, filter::FilterDefinition},
|
api::client::{device::Device, error::ErrorKind, filter::FilterDefinition},
|
||||||
encryption::{CrossSigningKey, DeviceKeys, OneTimeKey},
|
encryption::{CrossSigningKey, DeviceKeys, OneTimeKey},
|
||||||
|
@ -227,7 +226,7 @@ impl Data for KeyValueDatabase {
|
||||||
/// Hash and set the user's password to the Argon2 hash
|
/// Hash and set the user's password to the Argon2 hash
|
||||||
fn set_password(&self, user_id: &UserId, password: Option<&str>) -> Result<()> {
|
fn set_password(&self, user_id: &UserId, password: Option<&str>) -> Result<()> {
|
||||||
if let Some(password) = password {
|
if let Some(password) = password {
|
||||||
if let Ok(hash) = calculate_password_hash(password) {
|
if let Ok(hash) = utils::hash::password(password) {
|
||||||
self.userid_password
|
self.userid_password
|
||||||
.insert(user_id.as_bytes(), hash.as_bytes())?;
|
.insert(user_id.as_bytes(), hash.as_bytes())?;
|
||||||
Ok(())
|
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<String, argon2::password_hash::Error> {
|
|
||||||
let salt = SaltString::generate(rand::thread_rng());
|
|
||||||
services()
|
|
||||||
.globals
|
|
||||||
.argon
|
|
||||||
.hash_password(password.as_bytes(), &salt)
|
|
||||||
.map(|it| it.to_string())
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue