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

4
.gitignore vendored
View File

@ -2,4 +2,6 @@
.fleet
# Rust stuff
target
target
start.sh

View File

@ -1,6 +1,6 @@
{
"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": {
"columns": [
{
@ -16,7 +16,7 @@
{
"ordinal": 2,
"name": "email",
"type_info": "Text"
"type_info": "Varchar"
},
{
"ordinal": 3,
@ -50,8 +50,8 @@
},
{
"ordinal": 9,
"name": "permissions",
"type_info": "Int8"
"name": "flags",
"type_info": "Int2"
}
],
"parameters": {
@ -72,5 +72,5 @@
false
]
},
"hash": "7009cdfba0b6c8d7c8ed9d49bf91905d159c3bbfe0c8e78876a8cd2b952e28b5"
"hash": "0d30b5067f0a9c1c9929882a7c8b7cb5511d4808b6ee0ffd3201ed3772f0b191"
}

View File

@ -1,6 +1,6 @@
{
"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": {
"columns": [
{
@ -16,7 +16,7 @@
{
"ordinal": 2,
"name": "email",
"type_info": "Text"
"type_info": "Varchar"
},
{
"ordinal": 3,
@ -50,8 +50,8 @@
},
{
"ordinal": 9,
"name": "permissions",
"type_info": "Int8"
"name": "flags",
"type_info": "Int2"
}
],
"parameters": {
@ -72,5 +72,5 @@
false
]
},
"hash": "71b834dfe4384a536257b888e293b046a70aefd4ac2dcbf3268e34c9576e903e"
"hash": "128da462eb065680f50c9c3fef93d49974ba1e0697d5a996736e3715e335e37d"
}

View File

@ -1,6 +1,6 @@
{
"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": {
"columns": [
{
@ -16,7 +16,7 @@
{
"ordinal": 2,
"name": "email",
"type_info": "Text"
"type_info": "Varchar"
},
{
"ordinal": 3,
@ -50,8 +50,8 @@
},
{
"ordinal": 9,
"name": "permissions",
"type_info": "Int8"
"name": "flags",
"type_info": "Int2"
}
],
"parameters": {
@ -72,5 +72,5 @@
false
]
},
"hash": "0e894c84befe397687d4820ea313aa937cf76286e98f7e66c342fb87a3896265"
"hash": "294103135f5b8550f1de72a04ee9ff7569e89b2e72c2e9628c4b8a493468f99d"
}

View File

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

View File

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

View File

@ -1,8 +1,10 @@
use anyhow::{Error, Result};
use pbkdf2::{
use argon2::{
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};
#[derive(Debug, Clone, Copy)]
@ -22,7 +24,7 @@ pub struct Account {
pub verified: bool,
pub follows: Option<Vec<i64>>,
pub followers: Option<Vec<i64>>,
pub permissions: i64,
pub flags: i16,
}
impl From<i64> for ID {
@ -64,19 +66,25 @@ impl Account {
password: &String,
) -> Result<Self> {
let salt = SaltString::generate(&mut OsRng);
let hash = Pbkdf2
let password_hash = Argon2::default()
.hash_password(password.as_bytes(), &salt)
.map_err(|_| anyhow::Error::msg("Failed to hash the password"))?
.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();
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,
email,
email_hash,
salt.to_string(),
hash,
password_hash,
joined,
)
.execute(pool)
@ -103,7 +111,7 @@ impl Account {
verified as "verified!: bool",
follows,
followers,
permissions
flags
FROM Accounts WHERE username = $1;"#,
username
)
@ -129,7 +137,7 @@ impl Account {
verified as "verified!: bool",
follows,
followers,
permissions
flags
FROM Accounts WHERE id = $1;"#,
id
)
@ -143,6 +151,12 @@ impl Account {
}
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!(
Self,
r#"SELECT
@ -155,9 +169,9 @@ impl Account {
verified as "verified!: bool",
follows,
followers,
permissions
flags
FROM Accounts WHERE email = $1;"#,
email
email_hash
)
.fetch_one(pool)
.await
@ -172,16 +186,19 @@ impl Account {
let hash = PasswordHash::new(self.password.as_str())
.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),
Err(_) => Ok(false),
}
}
pub async fn delete(&self, pool: &PgPool) -> Result<()> {
sqlx::query!(r#"DELETE FROM AuthTokens WHERE account = $1;"#, self.id.id())
.execute(pool)
.await?;
sqlx::query!(
r#"DELETE FROM AuthTokens WHERE account = $1;"#,
self.id.id()
)
.execute(pool)
.await?;
sqlx::query!(r#"DELETE FROM Accounts WHERE id = $1;"#, self.id.id())
.execute(pool)

View File

@ -25,7 +25,7 @@ pub async fn register(
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? {
// Check if the account that has taken the username is verified or has an open verification request.
if account.verified

View File

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