Implement password hashing using Argon2, with PHC string format as storage encoding.

This commit is contained in:
Luca Palmieri 2021-08-29 22:09:43 +02:00
parent 312ee4aa89
commit 7d515138d5
5 changed files with 78 additions and 13 deletions

52
Cargo.lock generated
View file

@ -250,6 +250,17 @@ version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b"
[[package]]
name = "argon2"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d805bb12b532be9ce066df7913311f43716b41d8d780e9322113e8a6ae7c41ab"
dependencies = [
"base64ct",
"blake2",
"password-hash",
]
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.5.2" version = "0.5.2"
@ -305,6 +316,12 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "base64ct"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.2.1" version = "1.2.1"
@ -323,6 +340,17 @@ dependencies = [
"wyz", "wyz",
] ]
[[package]]
name = "blake2"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a4e37d16930f5459780f5621038b6382b9bb37c19016f39fb6b5808d831f174"
dependencies = [
"crypto-mac 0.8.0",
"digest",
"opaque-debug",
]
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.9.0" version = "0.9.0"
@ -537,6 +565,16 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "crypto-mac"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab"
dependencies = [
"generic-array",
"subtle",
]
[[package]] [[package]]
name = "crypto-mac" name = "crypto-mac"
version = "0.10.0" version = "0.10.0"
@ -930,7 +968,7 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
dependencies = [ dependencies = [
"crypto-mac", "crypto-mac 0.10.0",
"digest", "digest",
] ]
@ -1380,6 +1418,17 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "password-hash"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ad7268ef9bc463fddde8361d358fbfae1aeeb1fb62eca111cd8c763bf1c5891"
dependencies = [
"base64ct",
"rand_core 0.6.2",
"subtle",
]
[[package]] [[package]]
name = "paste" name = "paste"
version = "1.0.5" version = "1.0.5"
@ -2701,6 +2750,7 @@ dependencies = [
"actix-rt", "actix-rt",
"actix-web", "actix-web",
"anyhow", "anyhow",
"argon2",
"base64", "base64",
"chrono", "chrono",
"claim", "claim",

View file

@ -36,6 +36,7 @@ tracing-actix-web = "0.4.0-beta.8"
anyhow = "1.0.40" anyhow = "1.0.40"
base64 = "0.13.0" base64 = "0.13.0"
sha3 = "0.9" sha3 = "0.9"
argon2 = { version = "0.3", features = ["std"] }
[dev-dependencies] [dev-dependencies]
once_cell = "1.7.2" once_cell = "1.7.2"

View file

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN salt TEXT NOT NULL;

View file

@ -0,0 +1 @@
ALTER TABLE users DROP COLUMN salt;

View file

@ -4,7 +4,7 @@ use crate::routes::error_chain_fmt;
use actix_web::http::{HeaderMap, HeaderValue, StatusCode}; use actix_web::http::{HeaderMap, HeaderValue, StatusCode};
use actix_web::{web, HttpResponse, ResponseError}; use actix_web::{web, HttpResponse, ResponseError};
use anyhow::Context; use anyhow::Context;
use sha3::Digest; use argon2::{Argon2, PasswordHash, PasswordVerifier};
use sqlx::PgPool; use sqlx::PgPool;
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
@ -88,26 +88,38 @@ async fn validate_credentials(
credentials: Credentials, credentials: Credentials,
pool: &PgPool, pool: &PgPool,
) -> Result<uuid::Uuid, PublishError> { ) -> Result<uuid::Uuid, PublishError> {
let password_hash = sha3::Sha3_256::digest(credentials.password.as_bytes()); let row: Option<_> = sqlx::query!(
let password_hash = format!("{:x}", password_hash);
let user_id: Option<_> = sqlx::query!(
r#" r#"
SELECT user_id SELECT user_id, password_hash
FROM users FROM users
WHERE username = $1 AND password_hash = $2 WHERE username = $1
"#, "#,
credentials.username, credentials.username,
password_hash
) )
.fetch_optional(pool) .fetch_optional(pool)
.await .await
.context("Failed to performed a query to validate auth credentials.") .context("Failed to performed a query to retrieve stored credentials.")
.map_err(PublishError::UnexpectedError)?; .map_err(PublishError::UnexpectedError)?;
user_id let (expected_password_hash, user_id) = match row {
.map(|row| row.user_id) Some(row) => (row.password_hash, row.user_id),
.ok_or_else(|| anyhow::anyhow!("Invalid username or password.")) None => {
.map_err(PublishError::AuthError) return Err(PublishError::AuthError(anyhow::anyhow!(
"Unknown username."
)))
}
};
let expected_password_hash = PasswordHash::new(&expected_password_hash)
.context("Failed to parse hash in PHC string format.")
.map_err(PublishError::UnexpectedError)?;
Argon2::default()
.verify_password(credentials.password.as_bytes(), &expected_password_hash)
.context("Invalid password.")
.map_err(PublishError::AuthError)?;
Ok(user_id)
} }
#[tracing::instrument( #[tracing::instrument(