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"
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"
"rand 0.8.3",
"rand_chacha 0.3.0",
"redis",
+ "scrypt",
"serde",
"serde_derive",
"serde_json",
- "sha2",
"signal-hook",
"signal-hook-async-std",
"tide",
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"
"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"
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"] }
-use crate::auth::Auth;
+use crate::auth::CreateAuth;
use crate::game::{Action, GameSettings, GameSummary, UserAction};
use crate::username::Username;
#[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 },
-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 {
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"));
+ }
+}
}
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)]
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;
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
}