diff --git a/Cargo.lock b/Cargo.lock index 40a2ea2..efe3f96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4929,6 +4929,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "pem" version = "3.0.6" @@ -6132,6 +6142,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "schannel" version = "0.1.29" @@ -6177,6 +6196,18 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "password-hash", + "pbkdf2", + "salsa20", + "sha2", +] + [[package]] name = "sct" version = "0.7.1" @@ -7698,6 +7729,7 @@ dependencies = [ "totp-rs", "tranquil-config", "tranquil-crypto", + "tranquil-types", "urlencoding", "uuid", ] @@ -7825,6 +7857,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tracing", + "tranquil-types", "unicode-segmentation", "urlencoding", "wiremock", @@ -7933,6 +7966,7 @@ dependencies = [ "redis", "regex", "reqwest", + "scrypt", "serde", "serde_bytes", "serde_ipld_dagcbor", @@ -8034,6 +8068,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tracing", + "tranquil-types", "urlencoding", ] @@ -8189,13 +8224,16 @@ dependencies = [ name = "tranquil-types" version = "0.6.5" dependencies = [ + "base64 0.22.1", "chrono", "cid", "jacquard-common", + "rand 0.8.5", "serde", "serde_json", "sqlx", "thiserror 2.0.18", + "uuid", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6fa2a0c..198b2e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,6 +124,7 @@ serde_ipld_dagcbor = "0.6" serde_json = "1.0" serde_urlencoded = "0.7" sha2 = "0.10" +scrypt = "0.11" sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "json"] } subtle = "2.5" thiserror = "2.0" diff --git a/crates/tranquil-api/src/common.rs b/crates/tranquil-api/src/common.rs index c5df7c4..bc10553 100644 --- a/crates/tranquil-api/src/common.rs +++ b/crates/tranquil-api/src/common.rs @@ -219,7 +219,7 @@ pub async fn verify_credential( password_hash: Option<&PasswordHash>, ) -> Option { let main_valid = password_hash - .map(|h| bcrypt::verify(password, h.as_str()).unwrap_or(false)) + .map(|h| tranquil_pds::auth::verify_password(password, h.as_str())) .unwrap_or(false); if main_valid { return Some(CredentialMatch::MainPassword); @@ -230,7 +230,7 @@ pub async fn verify_credential( .unwrap_or_default(); app_passwords .into_iter() - .find(|app| bcrypt::verify(password, app.password_hash.as_str()).unwrap_or(false)) + .find(|app| tranquil_pds::auth::verify_password(password, app.password_hash.as_str())) .map(|app| { let scopes = app.scopes.unwrap_or_else(|| { if app.privilege.is_privileged() { diff --git a/crates/tranquil-api/src/server/reauth.rs b/crates/tranquil-api/src/server/reauth.rs index 154f958..c78adf4 100644 --- a/crates/tranquil-api/src/server/reauth.rs +++ b/crates/tranquil-api/src/server/reauth.rs @@ -74,7 +74,7 @@ pub async fn reauth_password( .log_db_err("fetching password hash")? .ok_or(ApiError::AccountNotFound)?; - let password_valid = bcrypt::verify(&input.password, password_hash.as_str()).unwrap_or(false); + let password_valid = tranquil_pds::auth::verify_password(&input.password, password_hash.as_str()); if !password_valid { let app_password_hashes = state @@ -85,7 +85,7 @@ pub async fn reauth_password( .unwrap_or_default(); let app_password_valid = app_password_hashes.iter().fold(false, |acc, h| { - acc | bcrypt::verify(&input.password, h.as_str()).unwrap_or(false) + acc | tranquil_pds::auth::verify_password(&input.password, h.as_str()) }); if !app_password_valid { diff --git a/crates/tranquil-oauth-server/src/endpoints/authorize/login.rs b/crates/tranquil-oauth-server/src/endpoints/authorize/login.rs index 0e3bd8d..e8dfd72 100644 --- a/crates/tranquil-oauth-server/src/endpoints/authorize/login.rs +++ b/crates/tranquil-oauth-server/src/endpoints/authorize/login.rs @@ -478,12 +478,7 @@ pub async fn authorize_post( } let password_valid = match &user.password_hash { - Some(hash) => match bcrypt::verify(&form.password, hash.as_str()) { - Ok(valid) => valid, - Err(_) => { - return show_login_error("An error occurred. Please try again.", json_response); - } - }, + Some(hash) => tranquil_pds::auth::verify_password(&form.password, hash.as_str()), None => false, }; if !password_valid { diff --git a/crates/tranquil-oauth-server/src/endpoints/authorize/registration.rs b/crates/tranquil-oauth-server/src/endpoints/authorize/registration.rs index 9e8cea1..80eb089 100644 --- a/crates/tranquil-oauth-server/src/endpoints/authorize/registration.rs +++ b/crates/tranquil-oauth-server/src/endpoints/authorize/registration.rs @@ -140,13 +140,13 @@ pub async fn register_complete( }; let mut password_valid = password_hashes.iter().fold(false, |acc, hash| { - acc | bcrypt::verify(&form.app_password, hash.as_str()).unwrap_or(false) + acc | tranquil_pds::auth::verify_password(&form.app_password, hash.as_str()) }); if !password_valid && let Ok(Some(account_hash)) = state.repos.user.get_password_hash_by_did(&did).await { - password_valid = bcrypt::verify(&form.app_password, account_hash.as_str()).unwrap_or(false); + password_valid = tranquil_pds::auth::verify_password(&form.app_password, account_hash.as_str()); } if !password_valid { diff --git a/crates/tranquil-oauth-server/src/endpoints/delegation.rs b/crates/tranquil-oauth-server/src/endpoints/delegation.rs index 748f90a..fd0ef5c 100644 --- a/crates/tranquil-oauth-server/src/endpoints/delegation.rs +++ b/crates/tranquil-oauth-server/src/endpoints/delegation.rs @@ -294,7 +294,7 @@ pub async fn delegation_auth( let password_valid = controller .password_hash .as_ref() - .map(|hash| bcrypt::verify(password, hash.as_str()).unwrap_or_default()) + .map(|hash| tranquil_pds::auth::verify_password(password, hash.as_str())) .unwrap_or_default(); if !password_valid { diff --git a/crates/tranquil-pds/Cargo.toml b/crates/tranquil-pds/Cargo.toml index 12a7550..8b9c30a 100644 --- a/crates/tranquil-pds/Cargo.toml +++ b/crates/tranquil-pds/Cargo.toml @@ -60,6 +60,7 @@ rand = { workspace = true } redis = { workspace = true, optional = true } regex = { workspace = true } reqwest = { workspace = true } +scrypt = { workspace = true } serde = { workspace = true } serde_bytes = { workspace = true } serde_ipld_dagcbor = { workspace = true } diff --git a/crates/tranquil-pds/src/auth/mfa_verified.rs b/crates/tranquil-pds/src/auth/mfa_verified.rs index dde64b1..3fd22be 100644 --- a/crates/tranquil-pds/src/auth/mfa_verified.rs +++ b/crates/tranquil-pds/src/auth/mfa_verified.rs @@ -201,7 +201,7 @@ pub async fn verify_password_mfa<'a>( match hash { Some(h) => { - if bcrypt::verify(password, h.as_str()).unwrap_or(false) { + if crate::auth::verify_password(password, h.as_str()) { Ok(MfaVerified::from_password(user)) } else { Err(crate::api::error::ApiError::InvalidPassword( diff --git a/crates/tranquil-pds/src/auth/mod.rs b/crates/tranquil-pds/src/auth/mod.rs index 5a6e64b..b4465e8 100644 --- a/crates/tranquil-pds/src/auth/mod.rs +++ b/crates/tranquil-pds/src/auth/mod.rs @@ -61,6 +61,29 @@ pub fn lxm_permits(lxm: &str, expected: &crate::types::Nsid) -> bool { lxm == "*" || lxm == expected.as_str() } +pub fn verify_password(password: &str, stored_hash: &str) -> bool { + if stored_hash.contains(':') { + verify_scrypt_password(password, stored_hash) + } else { + bcrypt::verify(password, stored_hash).unwrap_or(false) + } +} + +fn verify_scrypt_password(password: &str, stored_hash: &str) -> bool { + let Some((salt, hash)) = stored_hash.split_once(':') else { + return false; + }; + let Ok(params) = scrypt::Params::new(14, 8, 1, 64) else { + return false; + }; + let mut derived = [0u8; 64]; + if scrypt::scrypt(password.as_bytes(), salt.as_bytes(), ¶ms, &mut derived).is_err() { + return false; + } + let derived_hex = hex::encode(derived); + subtle::ConstantTimeEq::ct_eq(derived_hex.as_bytes(), hash.as_bytes()).into() +} + pub fn try_decrypt_user_key( key_bytes: Option<&[u8]>, encryption_version: Option,