Skip to content

Commit

Permalink
Ch10: Use Argon2 with PHC String Format instead of storing salt or us…
Browse files Browse the repository at this point in the history
…ing SHA3
  • Loading branch information
tahaafzal5 committed Aug 12, 2024
1 parent 8f66aa6 commit 5bddc1e
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 98 deletions.

This file was deleted.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

91 changes: 35 additions & 56 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ rand = { version = "0.8", features = ["std_rng"] }
thiserror = "1"
anyhow = "1"
base64 = "0.21"
sha3 = "0.9"
argon2 = { version = "0.4", features = ["std"] }

# We need the optional `derive` feature to use `serde`'s procedural macros:
# `#[derive(Serialize)]` and `#[derive(Deserialize)]`.
Expand Down
28 changes: 28 additions & 0 deletions Notes/Ch6-10.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@
- [Preimage Attack](#preimage-attack)
- [Naive Dictionary Attack](#naive-dictionary-attack)
- [Dictionary Attack](#dictionary-attack)
- [Argon2](#argon2)
- [Salting](#salting)
- [PHC String Format](#phc-string-format)

# Ch 6 - Reject Invalid Subscribers #1
* Our input validation for `/POST` is limited: we just ensure that both the `name` and the `email` fields are provided, even if they are empty.
Expand Down Expand Up @@ -1023,3 +1026,28 @@ curl "https://api.postmarkapp.com/email" \
#### Dictionary Attack
* In a couple of minutes an attacker can pre-compute the SHA3-256 hash of the most commonly used 10 million passwords.
* Then they start scanning our database looking for a match - aka do a dictionary attack - and it’s extremely effective.

#### Argon2
* The Open Web Application Security Project (OWASP) provides useful guidance on safe password storage including how to choose the correct hashing algorithm.
* We will replace SHA-3 with Argon2id as recommended.

#### Salting
* For each user, we generate a unique random string - the **salt**.
* The salt is prepended to the user password before generating the hash. `PasswordHasher::hash_password` takes care of the prepending business for us.
* The salt is stored next to the password hash, in our database.
* If an attacker gets their hands on a database backup, they will have access to all salts6. But they have to compute `dictionary_size * n_users` hashes instead of `dictionary_size`.
* Also, pre-computing the hashes is no longer an option - this buys us time to detect the breach and take action (e.g. force a password reset for all users).
* We can do better than base64-encoding.

#### PHC String Format
* To authenticate a user, we need **reproducibility**: we must run the very same hashing routine every single time.
* If we store a base64-encoded representation of the hash, we are making a strong implicit assumption: all values stored in the `password_hash` column have been computed using the same load parameters.
* We have 2 options:
1. In the users table, we add the parameters (`t_cost`, `m_cost`, and `p_cost`) and the algorithm.
2. Or add the PHC string format that provides a standard representation for a password hash: the salt, the algorithm, and all the parameters.
It looks like:
```
# ${algorithm}${algorithm version}${,-separated algorithm parameters}${hash}${salt}
$argon2id$v=19$m=65536,t=2,p=1$
gZiV/M1gPc22ElAH/Jh1Hw$CWOrkoo7oJBQ/iyh7uJ0LO2aLEfrHwTWllSAxT0zRno
```
2 changes: 2 additions & 0 deletions migrations/20240812164132_add_salt_to_users.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Add salt column to users table
ALTER TABLE users ADD COLUMN salt TEXT NOT NULL;
2 changes: 2 additions & 0 deletions migrations/20240812175535_remove_salt_from_users.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Remove salt column to users table
ALTER TABLE users DROP COLUMN salt;
40 changes: 25 additions & 15 deletions src/routes/newsletter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ use actix_web::{
HttpRequest, HttpResponse, ResponseError,
};
use anyhow::Context;
use argon2::{Argon2, PasswordHash, PasswordVerifier};
use base64::Engine;
use reqwest::{
header::{self, HeaderValue},
StatusCode,
};
use secrecy::{ExposeSecret, Secret};
use sha3::Digest;
use sqlx::PgPool;
use std::fmt::Debug;

Expand Down Expand Up @@ -113,26 +113,36 @@ async fn validate_credentials(
credentials: &Credentials,
connection_pool: &PgPool,
) -> Result<uuid::Uuid, PublishError> {
let password_hash = sha3::Sha3_256::digest(credentials.password.expose_secret().as_bytes());
// Lowercase hexadecimal encoding
let password_hash = format!("{:x}", password_hash);

let user_id: Option<_> = sqlx::query!(
"SELECT user_id
let row: Option<_> = sqlx::query!(
"
SELECT user_id, password_hash
FROM users
WHERE username = $1 AND password_hash = $2",
credentials.username,
password_hash
WHERE username = $1
",
credentials.username
)
.fetch_optional(connection_pool)
.await
.context("Failed to perform query to validate auth credentials.")
.context("Failed to perform a query to retrieve stored credentials.")
.map_err(PublishError::UnexpectedError)?;

user_id
.map(|row| row.user_id)
.ok_or_else(|| anyhow::anyhow!("Invalid username or password."))
.map_err(PublishError::AuthError)
let (user_id, expected_password_hash) = match row {
Some(row) => (row.user_id, row.password_hash),
None => 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.expose_secret().as_bytes(),
&expected_password_hash,
)
.context("Invalid password.")
.map_err(PublishError::AuthError)?;

Ok(user_id)
}

#[tracing::instrument(
Expand Down
Loading

0 comments on commit 5bddc1e

Please sign in to comment.