From: Geoffrey Allott Date: Sat, 13 Mar 2021 23:44:44 +0000 (+0000) Subject: add scrypt password hashing X-Git-Url: https://git.pointlesshacks.com/?a=commitdiff_plain;h=e1569f4346e9841b409652686c33d02427c92c03;p=pokerwave.git add scrypt password hashing --- diff --git a/Cargo.lock b/Cargo.lock index 6a99a0e..eb77bd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -356,6 +356,12 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "base64ct" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b8a45dc8036c7e52889226a96edacd45831c0dbdb8b803a58b8e0e12613b1a6" + [[package]] name = "bitflags" version = "1.2.1" @@ -1203,6 +1209,25 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" +[[package]] +name = "password-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721a49e14f1803441886c688ba8b653b52e1dcc926969081d22384e300ea4106" +dependencies = [ + "base64ct", + "rand_core 0.6.2", +] + +[[package]] +name = "pbkdf2" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "309c95c5f738c85920eb7062a2de29f3840d4f96974453fc9ac1ba078da9c627" +dependencies = [ + "crypto-mac", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -1270,10 +1295,10 @@ dependencies = [ "rand 0.8.3", "rand_chacha 0.3.0", "redis", + "scrypt", "serde", "serde_derive", "serde_json", - "sha2", "signal-hook", "signal-hook-async-std", "tide", @@ -1537,6 +1562,15 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "salsa20" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "399f290ffc409596022fce5ea5d4138184be4784f2b28c62c59f0d8389059a15" +dependencies = [ + "cipher", +] + [[package]] name = "schannel" version = "0.1.19" @@ -1547,6 +1581,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "scrypt" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b14b541af4bb9dfd8bb3ca9b487e430103303421953402bc5b44d9c06e620829" +dependencies = [ + "base64ct", + "hmac", + "password-hash", + "pbkdf2", + "salsa20", + "sha2", +] + [[package]] name = "sct" version = "0.6.0" diff --git a/Cargo.toml b/Cargo.toml index 05c09b6..fce125d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,10 +18,10 @@ pin-project = "1.0" rand = "0.8" rand_chacha = "0.3" redis = { version = "0.20", features = ["async-std-tls-comp"] } +scrypt = "0.6" serde = "1" serde_derive = "1" serde_json = "1" -sha2 = "0.9" signal-hook = "0.3" signal-hook-async-std = "0.2" tide = { version = "0.16.0", default-features = false, features = ["h1-server"] } diff --git a/src/api.rs b/src/api.rs index 2817214..9a3b682 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,4 +1,4 @@ -use crate::auth::Auth; +use crate::auth::CreateAuth; use crate::game::{Action, GameSettings, GameSummary, UserAction}; use crate::username::Username; @@ -13,10 +13,10 @@ pub enum Scope { #[derive(Debug, Clone, Deserialize)] #[serde(tag = "type")] pub enum ClientMessage { - CreateUser { username: Username, auth: Auth, nickname: String }, + CreateUser { username: Username, auth: CreateAuth, nickname: String }, Login { username: Username }, LoginAuthResponse { signature: String }, - ChangeAuth { auth: Auth }, + ChangeAuth { auth: CreateAuth }, ChangeNickname { nickname: String }, Logout, CreateGame { settings: GameSettings }, diff --git a/src/auth.rs b/src/auth.rs index 89a9f48..3f2457e 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,15 +1,24 @@ -use sha2::{Digest, Sha256}; +use core::convert::TryFrom; + +use rand::rngs::OsRng; +use scrypt::{ + password_hash::{HasherError, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, + Scrypt, +}; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "method")] pub enum Auth { NoLogin, Plain { password: String }, - Sha256 { - salt: String, - #[serde(with = "hex")] - hash: [u8; 32], - }, + Scrypt { phc: String }, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(tag = "method")] +pub enum CreateAuth { + Plain { password: String }, + Scrypt { password: String }, } impl Auth { @@ -17,12 +26,48 @@ impl Auth { match self { Auth::NoLogin => false, Auth::Plain { password } => signature == password, - Auth::Sha256 { salt, hash } => { - let mut hasher = Sha256::new(); - hasher.update(salt.as_bytes()); - hasher.update(signature.as_bytes()); - hasher.finalize()[..] == hash[..] + Auth::Scrypt { phc } => match PasswordHash::new(&phc) { + Ok(hash) => Scrypt.verify_password(signature.as_bytes(), &hash).is_ok(), + Err(err) => { + error!("Failed to parse {:?} as PHC string format specification: {}", phc, err); + false + } + }, + } + } +} + +impl TryFrom for Auth { + type Error = HasherError; + fn try_from(auth: CreateAuth) -> Result { + match auth { + CreateAuth::Plain { password } => Ok(Auth::Plain { password }), + CreateAuth::Scrypt { password } => { + let salt = SaltString::generate(&mut OsRng); + let hash = Scrypt.hash_password_simple(password.as_bytes(), salt.as_ref())?; + Ok(Auth::Scrypt { phc: hash.to_string() }) } } } } + +#[cfg(test)] +mod tests { + use super::*; + + use std::convert::TryInto; + + #[test] + fn verify_plain_authentication() { + let auth = CreateAuth::Plain { password: String::from("hunter2") }; + let auth: Auth = auth.try_into().unwrap(); + assert!(auth.verify("", "hunter2")); + } + + #[test] + fn verify_scrypt_authentication() { + let auth = CreateAuth::Scrypt { password: String::from("hunter2") }; + let auth: Auth = auth.try_into().unwrap(); + assert!(auth.verify("", "hunter2")); + } +} diff --git a/src/game/filter.rs b/src/game/filter.rs index 74658d5..699f51f 100644 --- a/src/game/filter.rs +++ b/src/game/filter.rs @@ -109,9 +109,7 @@ impl Filter for ParsedFilter { } pub fn parse_filter(input: &str) -> Result { - parse_or(input) - .map(|(_, filter)| ParsedFilter(filter)) - .map_err(|err| err.to_string()) + parse_or(input).map(|(_, filter)| ParsedFilter(filter)).map_err(|err| err.to_string()) } #[cfg(test)] diff --git a/src/server.rs b/src/server.rs index b51f196..6e567cc 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,13 +1,15 @@ use std::collections::HashSet; +use std::convert::TryInto; use futures::{ channel::mpsc::{Receiver, Sender}, SinkExt, }; use redis::{aio::MultiplexedConnection, cmd, AsyncCommands, ErrorKind, FromRedisValue, RedisError, RedisResult, RedisWrite, Script, ToRedisArgs, Value}; +use scrypt::password_hash::HasherError; use serde::{Deserialize, Serialize}; -use crate::auth::Auth; +use crate::auth::{Auth, CreateAuth}; use crate::game::{GameSettings, GameSummary, ValidatedUserAction}; use crate::pubsub::{client_interest_channel, ClientInterest, ClientInterestReceiver, ClientInterestSender}; use crate::rng::Seed; @@ -74,17 +76,24 @@ fn timeout_key(id: i64) -> String { format!("game:{}:timeout", id) } +fn convert_auth_error(err: HasherError) -> RedisError { + error!("Creating password authentication failed: {}", err); + RedisError::from((ErrorKind::AuthenticationFailed, "Creating password authentication failed", err.to_string())) +} + impl ServerState { - pub async fn create_user(&mut self, username: Username, auth: Auth, nickname: &str) -> RedisResult<()> { + pub async fn create_user(&mut self, username: Username, auth: CreateAuth, nickname: &str) -> RedisResult<()> { let key = user_key(username); + let auth: Auth = auth.try_into().map_err(convert_auth_error)?; if self.redis.hset_nx::<_, _, _, i32>(&key, "auth", AsJson(auth)).await? == 0 { return Err(RedisError::from((ErrorKind::ResponseError, "User already exists"))); } self.redis.hset(&key, "nickname", nickname).await } - pub async fn set_user_auth(&mut self, username: Username, auth: Auth) -> RedisResult<()> { + pub async fn set_user_auth(&mut self, username: Username, auth: CreateAuth) -> RedisResult<()> { let key = user_key(username); + let auth: Auth = auth.try_into().map_err(convert_auth_error)?; self.redis.hset(key, "auth", AsJson(auth)).await }