refactor(api): hashing and database changes

1. changed password hashing algorithm from pbkdf2-sha256 to argon2id.
2. storing emails as base64 encoded sha256 hash instead of plain text
This commit is contained in:
antifallobst 2023-09-09 13:29:00 +02:00
parent 4524f601ab
commit 452d2d2015
Signed by: antifallobst
GPG Key ID: 2B4F402172791BAF
12 changed files with 106 additions and 76 deletions

2
.gitignore vendored
View File

@ -3,3 +3,5 @@
# Rust stuff # Rust stuff
target target
start.sh

View File

@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "SELECT\n id,\n username,\n email,\n salt,\n password,\n joined,\n verified as \"verified!: bool\",\n follows,\n followers,\n permissions\n FROM Accounts WHERE id = $1;", "query": "SELECT\n id,\n username,\n email,\n salt,\n password,\n joined,\n verified as \"verified!: bool\",\n follows,\n followers,\n flags\n FROM Accounts WHERE id = $1;",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@ -16,7 +16,7 @@
{ {
"ordinal": 2, "ordinal": 2,
"name": "email", "name": "email",
"type_info": "Text" "type_info": "Varchar"
}, },
{ {
"ordinal": 3, "ordinal": 3,
@ -50,8 +50,8 @@
}, },
{ {
"ordinal": 9, "ordinal": 9,
"name": "permissions", "name": "flags",
"type_info": "Int8" "type_info": "Int2"
} }
], ],
"parameters": { "parameters": {
@ -72,5 +72,5 @@
false false
] ]
}, },
"hash": "7009cdfba0b6c8d7c8ed9d49bf91905d159c3bbfe0c8e78876a8cd2b952e28b5" "hash": "0d30b5067f0a9c1c9929882a7c8b7cb5511d4808b6ee0ffd3201ed3772f0b191"
} }

View File

@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "SELECT\n id,\n username,\n email,\n salt,\n password,\n joined,\n verified as \"verified!: bool\",\n follows,\n followers,\n permissions\n FROM Accounts WHERE email = $1;", "query": "SELECT\n id,\n username,\n email,\n salt,\n password,\n joined,\n verified as \"verified!: bool\",\n follows,\n followers,\n flags\n FROM Accounts WHERE email = $1;",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@ -16,7 +16,7 @@
{ {
"ordinal": 2, "ordinal": 2,
"name": "email", "name": "email",
"type_info": "Text" "type_info": "Varchar"
}, },
{ {
"ordinal": 3, "ordinal": 3,
@ -50,8 +50,8 @@
}, },
{ {
"ordinal": 9, "ordinal": 9,
"name": "permissions", "name": "flags",
"type_info": "Int8" "type_info": "Int2"
} }
], ],
"parameters": { "parameters": {
@ -72,5 +72,5 @@
false false
] ]
}, },
"hash": "71b834dfe4384a536257b888e293b046a70aefd4ac2dcbf3268e34c9576e903e" "hash": "128da462eb065680f50c9c3fef93d49974ba1e0697d5a996736e3715e335e37d"
} }

View File

@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "SELECT\n id,\n username,\n email,\n salt,\n password,\n joined,\n verified as \"verified!: bool\",\n follows,\n followers,\n permissions\n FROM Accounts WHERE username = $1;", "query": "SELECT\n id,\n username,\n email,\n salt,\n password,\n joined,\n verified as \"verified!: bool\",\n follows,\n followers,\n flags\n FROM Accounts WHERE username = $1;",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@ -16,7 +16,7 @@
{ {
"ordinal": 2, "ordinal": 2,
"name": "email", "name": "email",
"type_info": "Text" "type_info": "Varchar"
}, },
{ {
"ordinal": 3, "ordinal": 3,
@ -50,8 +50,8 @@
}, },
{ {
"ordinal": 9, "ordinal": 9,
"name": "permissions", "name": "flags",
"type_info": "Int8" "type_info": "Int2"
} }
], ],
"parameters": { "parameters": {
@ -72,5 +72,5 @@
false false
] ]
}, },
"hash": "0e894c84befe397687d4820ea313aa937cf76286e98f7e66c342fb87a3896265" "hash": "294103135f5b8550f1de72a04ee9ff7569e89b2e72c2e9628c4b8a493468f99d"
} }

View File

@ -1,12 +1,12 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "INSERT INTO Accounts (username, email, salt, password, joined, verified, permissions) VALUES ($1, $2, $3, $4, $5, false, 0);", "query": "INSERT INTO Accounts (username, email, salt, password, joined, verified, flags) VALUES ($1, $2, $3, $4, $5, false, 0);",
"describe": { "describe": {
"columns": [], "columns": [],
"parameters": { "parameters": {
"Left": [ "Left": [
"Varchar", "Varchar",
"Text", "Varchar",
"Varchar", "Varchar",
"Varchar", "Varchar",
"Timestamp" "Timestamp"
@ -14,5 +14,5 @@
}, },
"nullable": [] "nullable": []
}, },
"hash": "8b37c356c1df9961b47359a56f69b658adafa9c58b79e6dd867f6e4f62fbd144" "hash": "4919f945175894aac7c9a49c948876996b35dc3ffe772856de59443c2a1b9b64"
} }

View File

@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n CREATE TABLE IF NOT EXISTS Accounts (\n id SERIAL8 NOT NULL,\n\t username VARCHAR(32) NOT NULL,\n email TEXT NOT NULL,\n\t salt VARCHAR(22) NOT NULL,\n\t password VARCHAR(96) NOT NULL,\n joined TIMESTAMP NOT NULL,\n verified BOOLEAN NOT NULL,\n follows BIGINT[],\n followers BIGINT[],\n flags INT2 NOT NULL,\n\t PRIMARY KEY(id)\n );\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "50c529b57bcd8d46a5160f07fe11ce40e1db91fab254f3604e5b08587fe7caf2"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "\n CREATE TABLE IF NOT EXISTS Accounts (\n id SERIAL8 NOT NULL,\n\t username VARCHAR(32) NOT NULL,\n email VARCHAR(44) NOT NULL,\n\t salt VARCHAR(22) NOT NULL,\n\t password VARCHAR(128) NOT NULL,\n joined TIMESTAMP NOT NULL,\n verified BOOLEAN NOT NULL,\n follows BIGINT[],\n followers BIGINT[],\n flags INT2 NOT NULL,\n\t PRIMARY KEY(id)\n );\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "56757ca46f6c90c12658dcaf2a64aca3d15ae6540bfafac0db23848c1aa32ba4"
}

50
Cargo.lock generated
View File

@ -30,7 +30,7 @@ dependencies = [
"actix-service", "actix-service",
"actix-utils", "actix-utils",
"ahash 0.8.3", "ahash 0.8.3",
"base64 0.21.2", "base64 0.21.3",
"bitflags 1.3.2", "bitflags 1.3.2",
"brotli", "brotli",
"bytes", "bytes",
@ -347,6 +347,18 @@ version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
[[package]]
name = "argon2"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17ba4cac0a46bc1d2912652a751c47f2a9f3a7fe89bcae2275d418f5270402f9"
dependencies = [
"base64ct",
"blake2",
"cpufeatures",
"password-hash 0.5.0",
]
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.73" version = "0.1.73"
@ -402,9 +414,9 @@ checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.21.2" version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53"
[[package]] [[package]]
name = "base64ct" name = "base64ct"
@ -449,6 +461,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest",
]
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.10.4"
@ -1603,13 +1624,14 @@ dependencies = [
"actix-web", "actix-web",
"actix-web-httpauth", "actix-web-httpauth",
"anyhow", "anyhow",
"argon2",
"base64 0.21.3",
"chrono", "chrono",
"clap", "clap",
"env_logger", "env_logger",
"libinjection", "libinjection",
"log", "log",
"mail-send", "mail-send",
"pbkdf2 0.12.2",
"regex", "regex",
"serde", "serde",
"sha2", "sha2",
@ -1782,18 +1804,6 @@ dependencies = [
"sha2", "sha2",
] ]
[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest",
"hmac",
"password-hash 0.5.0",
"sha2",
]
[[package]] [[package]]
name = "peeking_take_while" name = "peeking_take_while"
version = "0.1.2" version = "0.1.2"
@ -2092,7 +2102,7 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
dependencies = [ dependencies = [
"base64 0.21.2", "base64 0.21.3",
] ]
[[package]] [[package]]
@ -2410,7 +2420,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ca69bf415b93b60b80dc8fda3cb4ef52b2336614d8da2de5456cc942a110482" checksum = "8ca69bf415b93b60b80dc8fda3cb4ef52b2336614d8da2de5456cc942a110482"
dependencies = [ dependencies = [
"atoi", "atoi",
"base64 0.21.2", "base64 0.21.3",
"bitflags 2.4.0", "bitflags 2.4.0",
"byteorder", "byteorder",
"bytes", "bytes",
@ -2453,7 +2463,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0db2df1b8731c3651e204629dd55e52adbae0462fa1bdcbed56a2302c18181e" checksum = "a0db2df1b8731c3651e204629dd55e52adbae0462fa1bdcbed56a2302c18181e"
dependencies = [ dependencies = [
"atoi", "atoi",
"base64 0.21.2", "base64 0.21.3",
"bitflags 2.4.0", "bitflags 2.4.0",
"byteorder", "byteorder",
"chrono", "chrono",
@ -3157,7 +3167,7 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
"flate2", "flate2",
"hmac", "hmac",
"pbkdf2 0.11.0", "pbkdf2",
"sha1", "sha1",
"time 0.3.25", "time 0.3.25",
"zstd 0.11.2+zstd.1.5.2", "zstd 0.11.2+zstd.1.5.2",

View File

@ -12,7 +12,6 @@ actix-web = "4"
serde = { version = "1.0.183", features = ["derive"] } serde = { version = "1.0.183", features = ["derive"] }
libinjection = "0.3.2" libinjection = "0.3.2"
sha2 = "0.10.2" sha2 = "0.10.2"
pbkdf2 = { version = "0.12", features = ["simple"] }
env_logger = "0.10" env_logger = "0.10"
log = "0.4" log = "0.4"
clap = { version = "4.3.21", features = ["derive"] } clap = { version = "4.3.21", features = ["derive"] }
@ -22,3 +21,5 @@ uuid = { version = "1.4.1", features = ["v4"] }
chrono = "0.4" chrono = "0.4"
mail-send = "0.4.0" mail-send = "0.4.0"
regex = "1.9.3" regex = "1.9.3"
argon2 = "0.5.2"
base64 = "0.21.3"

View File

@ -1,8 +1,10 @@
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use pbkdf2::{ use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Pbkdf2, Argon2,
}; };
use base64;
use sha2::{Digest, Sha256};
use sqlx::{postgres::PgPool, types::chrono as sqlx_chrono}; use sqlx::{postgres::PgPool, types::chrono as sqlx_chrono};
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -22,7 +24,7 @@ pub struct Account {
pub verified: bool, pub verified: bool,
pub follows: Option<Vec<i64>>, pub follows: Option<Vec<i64>>,
pub followers: Option<Vec<i64>>, pub followers: Option<Vec<i64>>,
pub permissions: i64, pub flags: i16,
} }
impl From<i64> for ID { impl From<i64> for ID {
@ -64,19 +66,25 @@ impl Account {
password: &String, password: &String,
) -> Result<Self> { ) -> Result<Self> {
let salt = SaltString::generate(&mut OsRng); let salt = SaltString::generate(&mut OsRng);
let hash = Pbkdf2 let password_hash = Argon2::default()
.hash_password(password.as_bytes(), &salt) .hash_password(password.as_bytes(), &salt)
.map_err(|_| anyhow::Error::msg("Failed to hash the password"))? .map_err(|_| anyhow::Error::msg("Failed to hash the password"))?
.to_string(); .to_string();
let email_hash = {
let mut hasher = Sha256::new();
hasher.update(email.as_bytes());
base64::encode(hasher.finalize())
};
let joined = sqlx_chrono::Utc::now().naive_utc(); let joined = sqlx_chrono::Utc::now().naive_utc();
sqlx::query!( sqlx::query!(
r#"INSERT INTO Accounts (username, email, salt, password, joined, verified, permissions) VALUES ($1, $2, $3, $4, $5, false, 0);"#, r#"INSERT INTO Accounts (username, email, salt, password, joined, verified, flags) VALUES ($1, $2, $3, $4, $5, false, 0);"#,
username, username,
email, email_hash,
salt.to_string(), salt.to_string(),
hash, password_hash,
joined, joined,
) )
.execute(pool) .execute(pool)
@ -103,7 +111,7 @@ impl Account {
verified as "verified!: bool", verified as "verified!: bool",
follows, follows,
followers, followers,
permissions flags
FROM Accounts WHERE username = $1;"#, FROM Accounts WHERE username = $1;"#,
username username
) )
@ -129,7 +137,7 @@ impl Account {
verified as "verified!: bool", verified as "verified!: bool",
follows, follows,
followers, followers,
permissions flags
FROM Accounts WHERE id = $1;"#, FROM Accounts WHERE id = $1;"#,
id id
) )
@ -143,6 +151,12 @@ impl Account {
} }
pub async fn from_email(pool: &PgPool, email: &String) -> Result<Option<Self>> { pub async fn from_email(pool: &PgPool, email: &String) -> Result<Option<Self>> {
let email_hash = {
let mut hasher = Sha256::new();
hasher.update(email.as_bytes());
base64::encode(hasher.finalize())
};
match sqlx::query_as!( match sqlx::query_as!(
Self, Self,
r#"SELECT r#"SELECT
@ -155,9 +169,9 @@ impl Account {
verified as "verified!: bool", verified as "verified!: bool",
follows, follows,
followers, followers,
permissions flags
FROM Accounts WHERE email = $1;"#, FROM Accounts WHERE email = $1;"#,
email email_hash
) )
.fetch_one(pool) .fetch_one(pool)
.await .await
@ -172,16 +186,19 @@ impl Account {
let hash = PasswordHash::new(self.password.as_str()) let hash = PasswordHash::new(self.password.as_str())
.map_err(|_| anyhow::Error::msg("Failed to parse the password hash"))?; .map_err(|_| anyhow::Error::msg("Failed to parse the password hash"))?;
match Pbkdf2.verify_password(password.as_bytes(), &hash) { match Argon2::default().verify_password(password.as_bytes(), &hash) {
Ok(_) => Ok(true), Ok(_) => Ok(true),
Err(_) => Ok(false), Err(_) => Ok(false),
} }
} }
pub async fn delete(&self, pool: &PgPool) -> Result<()> { pub async fn delete(&self, pool: &PgPool) -> Result<()> {
sqlx::query!(r#"DELETE FROM AuthTokens WHERE account = $1;"#, self.id.id()) sqlx::query!(
.execute(pool) r#"DELETE FROM AuthTokens WHERE account = $1;"#,
.await?; self.id.id()
)
.execute(pool)
.await?;
sqlx::query!(r#"DELETE FROM Accounts WHERE id = $1;"#, self.id.id()) sqlx::query!(r#"DELETE FROM Accounts WHERE id = $1;"#, self.id.id())
.execute(pool) .execute(pool)

View File

@ -25,7 +25,7 @@ pub async fn register(
return Ok(data::RegisterResponse::MalformedEmail); return Ok(data::RegisterResponse::MalformedEmail);
} }
// Check if the usernam is already taken // Check if the username is already taken
if let Some(account) = Account::from_username(pool, &request.username).await? { if let Some(account) = Account::from_username(pool, &request.username).await? {
// Check if the account that has taken the username is verified or has an open verification request. // Check if the account that has taken the username is verified or has an open verification request.
if account.verified if account.verified

View File

@ -16,16 +16,16 @@ pub async fn start(port: u16, pool: PgPool) -> Result<()> {
sqlx::query!( sqlx::query!(
r#" r#"
CREATE TABLE IF NOT EXISTS Accounts ( CREATE TABLE IF NOT EXISTS Accounts (
id SERIAL8 NOT NULL, id SERIAL8 NOT NULL,
username VARCHAR(32) NOT NULL, username VARCHAR(32) NOT NULL,
email TEXT NOT NULL, email VARCHAR(44) NOT NULL,
salt VARCHAR(22) NOT NULL, salt VARCHAR(22) NOT NULL,
password VARCHAR(96) NOT NULL, password VARCHAR(128) NOT NULL,
joined TIMESTAMP NOT NULL, joined TIMESTAMP NOT NULL,
verified BOOLEAN NOT NULL, verified BOOLEAN NOT NULL,
follows BIGINT[], follows BIGINT[],
followers BIGINT[], followers BIGINT[],
flags INT2 NOT NULL, flags INT2 NOT NULL,
PRIMARY KEY(id) PRIMARY KEY(id)
); );
"# "#