add scrypt password hashing
authorGeoffrey Allott <geoffrey@allott.email>
Sat, 13 Mar 2021 23:44:44 +0000 (23:44 +0000)
committerGeoffrey Allott <geoffrey@allott.email>
Sat, 13 Mar 2021 23:44:44 +0000 (23:44 +0000)
Cargo.lock
Cargo.toml
src/api.rs
src/auth.rs
src/game/filter.rs
src/server.rs

index 6a99a0e280a1152633094882652f275422dd7054..eb77bd3c865173f8d2828897869b6457e1d46c2c 100644 (file)
@@ -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"
index 05c09b6cb4a1537f537d3c28c89bb67a67e9ae47..fce125d9b994c675afdb557a6cd39eef0684f7ea 100644 (file)
@@ -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"] }
index 2817214409d1f569e1bb9bc9f53de7916ea76640..9a3b682352c4665db6a8258d9221087f522a206f 100644 (file)
@@ -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 },
index 89a9f48afdb764ede2f41094edfc79876841142f..3f2457e66096d2ada8a475001f9923cea028afe1 100644 (file)
@@ -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<CreateAuth> for Auth {
+    type Error = HasherError;
+    fn try_from(auth: CreateAuth) -> Result<Auth, HasherError> {
+        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"));
+    }
+}
index 74658d5a03f393025505d75b90749d011482cb49..699f51f030a27dd977b2f9c90b197cd05ff823e2 100644 (file)
@@ -109,9 +109,7 @@ impl Filter for ParsedFilter {
 }
 
 pub fn parse_filter(input: &str) -> Result<ParsedFilter, String> {
-    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)]
index b51f19638e7407fae7f949dd25591d7420662c7c..6e567cc3d55f3c024f1dce854c3eb9b85706df0f 100644 (file)
@@ -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
     }