forked from lavina/lavina
1
0
Fork 0

Compare commits

..

No commits in common. "784711f99b3800725d2c3577a88c6dcff072a9a3" and "6c08d69f416d3e2feba4328bd2804291b4f98061" have entirely different histories.

13 changed files with 69 additions and 191 deletions

34
Cargo.lock generated
View File

@ -114,18 +114,6 @@ version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
[[package]]
name = "argon2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2",
"cpufeatures",
"password-hash",
]
[[package]] [[package]]
name = "assert_matches" name = "assert_matches"
version = "1.5.0" version = "1.5.0"
@ -204,15 +192,6 @@ 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"
@ -903,10 +882,8 @@ name = "lavina-core"
version = "0.0.2-dev" version = "0.0.2-dev"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2",
"chrono", "chrono",
"prometheus", "prometheus",
"rand_core",
"serde", "serde",
"sqlx", "sqlx",
"tokio", "tokio",
@ -1149,17 +1126,6 @@ dependencies = [
"windows-targets 0.48.5", "windows-targets 0.48.5",
] ]
[[package]]
name = "password-hash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core",
"subtle",
]
[[package]] [[package]]
name = "paste" name = "paste"
version = "1.0.14" version = "1.0.14"

View File

@ -11,5 +11,3 @@ tokio.workspace = true
tracing.workspace = true tracing.workspace = true
prometheus.workspace = true prometheus.workspace = true
chrono.workspace = true chrono.workspace = true
argon2 = { version = "0.5.3" }
rand_core = { version = "0.6.4", features = ["getrandom"] }

View File

@ -1,4 +0,0 @@
create table challenges_argon2_password(
user_id integer primary key not null,
hash string not null
);

View File

@ -1,64 +0,0 @@
use anyhow::{anyhow, Result};
use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
use argon2::Argon2;
use rand_core::OsRng;
use crate::prelude::log;
use crate::repo::Storage;
pub enum Verdict {
Authenticated,
UserNotFound,
InvalidPassword,
}
pub enum UpdatePasswordResult {
PasswordUpdated,
UserNotFound,
}
pub struct Authenticator<'a> {
storage: &'a Storage,
}
impl<'a> Authenticator<'a> {
pub fn new(storage: &'a Storage) -> Self {
Self { storage }
}
pub async fn authenticate(&self, login: &str, provided_password: &str) -> Result<Verdict> {
let Some(stored_user) = self.storage.retrieve_user_by_name(login).await? else {
return Ok(Verdict::UserNotFound);
};
if let Some(argon2_hash) = stored_user.argon2_hash {
let argon2 = Argon2::default();
let password_hash =
PasswordHash::new(&argon2_hash).map_err(|e| anyhow!("Failed to parse password hash: {e:?}"))?;
let password_verifier = argon2.verify_password(provided_password.as_bytes(), &password_hash);
if password_verifier.is_ok() {
return Ok(Verdict::Authenticated);
}
}
if let Some(expected_password) = stored_user.password {
if expected_password == provided_password {
return Ok(Verdict::Authenticated);
}
}
Ok(Verdict::InvalidPassword)
}
pub async fn set_password(&self, login: &str, provided_password: &str) -> Result<UpdatePasswordResult> {
let Some(u) = self.storage.retrieve_user_by_name(login).await? else {
return Ok(UpdatePasswordResult::UserNotFound);
};
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_hash = argon2
.hash_password(provided_password.as_bytes(), &salt)
.map_err(|e| anyhow!("Failed to hash password: {e:?}"))?;
self.storage.set_argon2_challenge(u.id, password_hash.to_string().as_str()).await?;
log::info!("Password changed for player {login}");
Ok(UpdatePasswordResult::PasswordUpdated)
}
}

View File

@ -6,7 +6,6 @@ use crate::player::PlayerRegistry;
use crate::repo::Storage; use crate::repo::Storage;
use crate::room::RoomRegistry; use crate::room::RoomRegistry;
pub mod auth;
pub mod player; pub mod player;
pub mod prelude; pub mod prelude;
pub mod repo; pub mod repo;

View File

@ -233,21 +233,14 @@ impl PlayerRegistry {
} }
pub async fn get_or_launch_player(&mut self, id: &PlayerId) -> PlayerHandle { pub async fn get_or_launch_player(&mut self, id: &PlayerId) -> PlayerHandle {
let inner = self.0.read().await; let mut inner = self.0.write().await;
if let Some((handle, _)) = inner.players.get(id) { if let Some((handle, _)) = inner.players.get(id) {
handle.clone() handle.clone()
} else { } else {
drop(inner); let (handle, fiber) = Player::launch(id.clone(), inner.room_registry.clone(), inner.storage.clone()).await;
let mut inner = self.0.write().await; inner.players.insert(id.clone(), (handle.clone(), fiber));
if let Some((handle, _)) = inner.players.get(id) { inner.metric_active_players.inc();
handle.clone() handle
} else {
let (handle, fiber) =
Player::launch(id.clone(), inner.room_registry.clone(), inner.storage.clone()).await;
inner.players.insert(id.clone(), (handle.clone(), fiber));
inner.metric_active_players.inc();
handle
}
} }
} }

View File

@ -1,19 +0,0 @@
use anyhow::Result;
use crate::repo::Storage;
impl Storage {
pub async fn set_argon2_challenge(&self, user_id: u32, hash: &str) -> Result<()> {
let mut executor = self.conn.lock().await;
sqlx::query(
"insert into challenges_argon2_password(user_id, hash)
values (?, ?)
on conflict(user_id) do update set hash = excluded.hash;",
)
.bind(user_id)
.bind(hash)
.execute(&mut *executor)
.await?;
Ok(())
}
}

View File

@ -12,7 +12,6 @@ use tokio::sync::Mutex;
use crate::prelude::*; use crate::prelude::*;
mod auth;
mod room; mod room;
mod user; mod user;
@ -39,12 +38,11 @@ impl Storage {
Ok(Storage { conn }) Ok(Storage { conn })
} }
pub async fn retrieve_user_by_name(&self, name: &str) -> Result<Option<StoredUser>> { pub async fn retrieve_user_by_name(&mut self, name: &str) -> Result<Option<StoredUser>> {
let mut executor = self.conn.lock().await; let mut executor = self.conn.lock().await;
let res = sqlx::query_as( let res = sqlx::query_as(
"select u.id, u.name, c.password, a.hash as argon2_hash "select u.id, u.name, c.password
from users u left join challenges_plain_password c on u.id = c.user_id from users u left join challenges_plain_password c on u.id = c.user_id
left join challenges_argon2_password a on u.id = a.user_id
where u.name = ?;", where u.name = ?;",
) )
.bind(name) .bind(name)
@ -138,7 +136,7 @@ impl Storage {
Ok(()) Ok(())
} }
pub async fn set_password<'a>(&'a self, name: &'a str, pwd: &'a str) -> Result<Option<()>> { pub async fn set_password<'a>(&'a mut self, name: &'a str, pwd: &'a str) -> Result<Option<()>> {
async fn inner(txn: &mut Transaction<'_, Sqlite>, name: &str, pwd: &str) -> Result<Option<()>> { async fn inner(txn: &mut Transaction<'_, Sqlite>, name: &str, pwd: &str) -> Result<Option<()>> {
let id: Option<(u32,)> = sqlx::query_as("select * from users where name = ? limit 1;") let id: Option<(u32,)> = sqlx::query_as("select * from users where name = ? limit 1;")
.bind(name) .bind(name)
@ -176,7 +174,6 @@ pub struct StoredUser {
pub id: u32, pub id: u32,
pub name: String, pub name: String,
pub password: Option<String>, pub password: Option<String>,
pub argon2_hash: Option<Box<str>>,
} }
#[derive(FromRow)] #[derive(FromRow)]

View File

@ -14,7 +14,6 @@ use tokio::net::tcp::{ReadHalf, WriteHalf};
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
use tokio::sync::mpsc::channel; use tokio::sync::mpsc::channel;
use lavina_core::auth::{Authenticator, Verdict};
use lavina_core::player::*; use lavina_core::player::*;
use lavina_core::prelude::*; use lavina_core::prelude::*;
use lavina_core::repo::Storage; use lavina_core::repo::Storage;
@ -406,13 +405,24 @@ fn sasl_fail_message(sender: Str, nick: Str, text: Str) -> ServerMessage {
} }
async fn auth_user(storage: &mut Storage, login: &str, plain_password: &str) -> Result<()> { async fn auth_user(storage: &mut Storage, login: &str, plain_password: &str) -> Result<()> {
let verdict = Authenticator::new(storage).authenticate(login, plain_password).await?; let stored_user = storage.retrieve_user_by_name(login).await?;
// TODO properly map these onto protocol messages
match verdict { let stored_user = match stored_user {
Verdict::Authenticated => Ok(()), Some(u) => u,
Verdict::UserNotFound => Err(anyhow!("no user found")), None => {
Verdict::InvalidPassword => Err(anyhow!("incorrect credentials")), log::info!("User '{}' not found", login);
return Err(anyhow!("no user found"));
}
};
let Some(expected_password) = stored_user.password else {
log::info!("Password not defined for user '{}'", login);
return Err(anyhow!("password is not defined"));
};
if expected_password != plain_password {
log::info!("Incorrect password supplied for user '{}'", login);
return Err(anyhow!("passwords do not match"));
} }
Ok(())
} }
async fn handle_registered_socket<'a>( async fn handle_registered_socket<'a>(

View File

@ -8,7 +8,6 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::tcp::{ReadHalf, WriteHalf}; use tokio::net::tcp::{ReadHalf, WriteHalf};
use tokio::net::TcpStream; use tokio::net::TcpStream;
use lavina_core::auth::Authenticator;
use lavina_core::player::{JoinResult, PlayerId, SendMessageResult}; use lavina_core::player::{JoinResult, PlayerId, SendMessageResult};
use lavina_core::repo::{Storage, StorageConfig}; use lavina_core::repo::{Storage, StorageConfig};
use lavina_core::room::RoomId; use lavina_core::room::RoomId;
@ -28,7 +27,7 @@ impl<'a> TestScope<'a> {
let (reader, writer) = stream.split(); let (reader, writer) = stream.split();
let reader = BufReader::new(reader); let reader = BufReader::new(reader);
let buffer = vec![]; let buffer = vec![];
let timeout = Duration::from_millis(1000); let timeout = Duration::from_millis(100);
TestScope { TestScope {
reader, reader,
writer, writer,
@ -160,7 +159,7 @@ async fn scenario_basic() -> Result<()> {
// test scenario // test scenario
server.storage.create_user("tester").await?; server.storage.create_user("tester").await?;
Authenticator::new(&server.storage).set_password("tester", "password").await?; server.storage.set_password("tester", "password").await?;
let mut stream = TcpStream::connect(server.server.addr).await?; let mut stream = TcpStream::connect(server.server.addr).await?;
let mut s = TestScope::new(&mut stream); let mut s = TestScope::new(&mut stream);
@ -189,7 +188,7 @@ async fn scenario_join_and_reboot() -> Result<()> {
// test scenario // test scenario
server.storage.create_user("tester").await?; server.storage.create_user("tester").await?;
Authenticator::new(&server.storage).set_password("tester", "password").await?; server.storage.set_password("tester", "password").await?;
let mut stream = TcpStream::connect(server.server.addr).await?; let mut stream = TcpStream::connect(server.server.addr).await?;
let mut s = TestScope::new(&mut stream); let mut s = TestScope::new(&mut stream);
@ -259,7 +258,7 @@ async fn scenario_force_join_msg() -> Result<()> {
// test scenario // test scenario
server.storage.create_user("tester").await?; server.storage.create_user("tester").await?;
Authenticator::new(&server.storage).set_password("tester", "password").await?; server.storage.set_password("tester", "password").await?;
let mut stream1 = TcpStream::connect(server.server.addr).await?; let mut stream1 = TcpStream::connect(server.server.addr).await?;
let mut s1 = TestScope::new(&mut stream1); let mut s1 = TestScope::new(&mut stream1);
@ -325,9 +324,9 @@ async fn scenario_two_users() -> Result<()> {
// test scenario // test scenario
server.storage.create_user("tester1").await?; server.storage.create_user("tester1").await?;
Authenticator::new(&server.storage).set_password("tester1", "password").await?; server.storage.set_password("tester1", "password").await?;
server.storage.create_user("tester2").await?; server.storage.create_user("tester2").await?;
Authenticator::new(&server.storage).set_password("tester2", "password").await?; server.storage.set_password("tester2", "password").await?;
let mut stream1 = TcpStream::connect(server.server.addr).await?; let mut stream1 = TcpStream::connect(server.server.addr).await?;
let mut s1 = TestScope::new(&mut stream1); let mut s1 = TestScope::new(&mut stream1);
@ -389,7 +388,7 @@ async fn scenario_cap_full_negotiation() -> Result<()> {
// test scenario // test scenario
server.storage.create_user("tester").await?; server.storage.create_user("tester").await?;
Authenticator::new(&server.storage).set_password("tester", "password").await?; server.storage.set_password("tester", "password").await?;
let mut stream = TcpStream::connect(server.server.addr).await?; let mut stream = TcpStream::connect(server.server.addr).await?;
let mut s = TestScope::new(&mut stream); let mut s = TestScope::new(&mut stream);
@ -429,7 +428,7 @@ async fn scenario_cap_full_negotiation_nick_last() -> Result<()> {
// test scenario // test scenario
server.storage.create_user("tester").await?; server.storage.create_user("tester").await?;
Authenticator::new(&server.storage).set_password("tester", "password").await?; server.storage.set_password("tester", "password").await?;
let mut stream = TcpStream::connect(server.server.addr).await?; let mut stream = TcpStream::connect(server.server.addr).await?;
let mut s = TestScope::new(&mut stream); let mut s = TestScope::new(&mut stream);
@ -468,7 +467,7 @@ async fn scenario_cap_short_negotiation() -> Result<()> {
// test scenario // test scenario
server.storage.create_user("tester").await?; server.storage.create_user("tester").await?;
Authenticator::new(&server.storage).set_password("tester", "password").await?; server.storage.set_password("tester", "password").await?;
let mut stream = TcpStream::connect(server.server.addr).await?; let mut stream = TcpStream::connect(server.server.addr).await?;
let mut s = TestScope::new(&mut stream); let mut s = TestScope::new(&mut stream);
@ -506,7 +505,7 @@ async fn scenario_cap_sasl_fail() -> Result<()> {
// test scenario // test scenario
server.storage.create_user("tester").await?; server.storage.create_user("tester").await?;
Authenticator::new(&server.storage).set_password("tester", "password").await?; server.storage.set_password("tester", "password").await?;
let mut stream = TcpStream::connect(server.server.addr).await?; let mut stream = TcpStream::connect(server.server.addr).await?;
let mut s = TestScope::new(&mut stream); let mut s = TestScope::new(&mut stream);
@ -550,7 +549,7 @@ async fn terminate_socket_scenario() -> Result<()> {
// test scenario // test scenario
server.storage.create_user("tester").await?; server.storage.create_user("tester").await?;
Authenticator::new(&server.storage).set_password("tester", "password").await?; server.storage.set_password("tester", "password").await?;
let mut stream = TcpStream::connect(server.server.addr).await?; let mut stream = TcpStream::connect(server.server.addr).await?;
let mut s = TestScope::new(&mut stream); let mut s = TestScope::new(&mut stream);
@ -575,7 +574,7 @@ async fn server_time_capability() -> Result<()> {
// test scenario // test scenario
server.storage.create_user("tester").await?; server.storage.create_user("tester").await?;
Authenticator::new(&server.storage).set_password("tester", "password").await?; server.storage.set_password("tester", "password").await?;
let mut stream = TcpStream::connect(server.server.addr).await?; let mut stream = TcpStream::connect(server.server.addr).await?;
let mut s = TestScope::new(&mut stream); let mut s = TestScope::new(&mut stream);

View File

@ -9,7 +9,6 @@ use std::net::SocketAddr;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use anyhow::anyhow;
use futures_util::future::join_all; use futures_util::future::join_all;
use prometheus::Registry as MetricsRegistry; use prometheus::Registry as MetricsRegistry;
use quick_xml::events::{BytesDecl, Event}; use quick_xml::events::{BytesDecl, Event};
@ -22,7 +21,6 @@ use tokio::sync::mpsc::channel;
use tokio_rustls::rustls::{Certificate, PrivateKey}; use tokio_rustls::rustls::{Certificate, PrivateKey};
use tokio_rustls::TlsAcceptor; use tokio_rustls::TlsAcceptor;
use lavina_core::auth::{Authenticator, Verdict};
use lavina_core::player::{PlayerConnection, PlayerId, PlayerRegistry}; use lavina_core::player::{PlayerConnection, PlayerId, PlayerRegistry};
use lavina_core::prelude::*; use lavina_core::prelude::*;
use lavina_core::repo::Storage; use lavina_core::repo::Storage;
@ -302,18 +300,28 @@ async fn socket_auth(
match AuthBody::from_str(&auth.body) { match AuthBody::from_str(&auth.body) {
Ok(logopass) => { Ok(logopass) => {
let name = &logopass.login; let name = &logopass.login;
let verdict = Authenticator::new(storage).authenticate(name, &logopass.password).await?; let stored_user = storage.retrieve_user_by_name(name).await?;
let stored_user = match stored_user {
Some(u) => u,
None => {
log::info!("User '{}' not found", name);
return Err(fail("no user found"));
}
};
// TODO return proper XML errors to the client // TODO return proper XML errors to the client
match verdict {
Verdict::Authenticated => {} if stored_user.password.is_none() {
Verdict::UserNotFound => { log::info!("Password not defined for user '{}'", name);
return Err(anyhow!("no user found")); return Err(fail("password is not defined"));
}
Verdict::InvalidPassword => {
return Err(anyhow!("incorrect credentials"));
}
} }
if stored_user.password.as_deref() != Some(&logopass.password) {
log::info!("Incorrect password supplied for user '{}'", name);
return Err(fail("passwords do not match"));
}
let name: Str = name.as_str().into(); let name: Str = name.as_str().into();
Ok(Authenticated { Ok(Authenticated {
player_id: PlayerId::from(name.clone())?, player_id: PlayerId::from(name.clone())?,
xmpp_name: Name(name.clone()), xmpp_name: Name(name.clone()),

View File

@ -16,7 +16,6 @@ use tokio_rustls::rustls::client::ServerCertVerifier;
use tokio_rustls::rustls::{ClientConfig, ServerName}; use tokio_rustls::rustls::{ClientConfig, ServerName};
use tokio_rustls::TlsConnector; use tokio_rustls::TlsConnector;
use lavina_core::auth::Authenticator;
use lavina_core::repo::{Storage, StorageConfig}; use lavina_core::repo::{Storage, StorageConfig};
use lavina_core::LavinaCore; use lavina_core::LavinaCore;
use projection_xmpp::{launch, RunningServer, ServerConfig}; use projection_xmpp::{launch, RunningServer, ServerConfig};
@ -159,7 +158,7 @@ async fn scenario_basic() -> Result<()> {
// test scenario // test scenario
server.storage.create_user("tester").await?; server.storage.create_user("tester").await?;
Authenticator::new(&server.storage).set_password("tester", "password").await?; server.storage.set_password("tester", "password").await?;
let mut stream = TcpStream::connect(server.server.addr).await?; let mut stream = TcpStream::connect(server.server.addr).await?;
let mut s = TestScope::new(&mut stream); let mut s = TestScope::new(&mut stream);
@ -211,7 +210,7 @@ async fn scenario_basic_without_headers() -> Result<()> {
// test scenario // test scenario
server.storage.create_user("tester").await?; server.storage.create_user("tester").await?;
Authenticator::new(&server.storage).set_password("tester", "password").await?; server.storage.set_password("tester", "password").await?;
let mut stream = TcpStream::connect(server.server.addr).await?; let mut stream = TcpStream::connect(server.server.addr).await?;
let mut s = TestScope::new(&mut stream); let mut s = TestScope::new(&mut stream);
@ -261,7 +260,7 @@ async fn terminate_socket() -> Result<()> {
// test scenario // test scenario
server.storage.create_user("tester").await?; server.storage.create_user("tester").await?;
Authenticator::new(&server.storage).set_password("tester", "password").await?; server.storage.set_password("tester", "password").await?;
let mut stream = TcpStream::connect(server.server.addr).await?; let mut stream = TcpStream::connect(server.server.addr).await?;
let mut s = TestScope::new(&mut stream); let mut s = TestScope::new(&mut stream);

View File

@ -12,7 +12,6 @@ use prometheus::{Encoder, Registry as MetricsRegistry, TextEncoder};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::net::TcpListener; use tokio::net::TcpListener;
use lavina_core::auth::{Authenticator, UpdatePasswordResult};
use lavina_core::prelude::*; use lavina_core::prelude::*;
use lavina_core::repo::Storage; use lavina_core::repo::Storage;
use lavina_core::room::RoomRegistry; use lavina_core::room::RoomRegistry;
@ -142,20 +141,17 @@ async fn endpoint_set_password(
*response.status_mut() = StatusCode::BAD_REQUEST; *response.status_mut() = StatusCode::BAD_REQUEST;
return Ok(response); return Ok(response);
}; };
let verdict = Authenticator::new(&storage).set_password(&res.player_name, &res.password).await?; let Some(_) = storage.set_password(&res.player_name, &res.password).await? else {
match verdict { let payload = ErrorResponse {
UpdatePasswordResult::PasswordUpdated => {} code: errors::PLAYER_NOT_FOUND,
UpdatePasswordResult::UserNotFound => { message: "No such player exists",
let payload = ErrorResponse {
code: errors::PLAYER_NOT_FOUND,
message: "No such player exists",
}
.to_body();
let mut response = Response::new(payload);
*response.status_mut() = StatusCode::UNPROCESSABLE_ENTITY;
return Ok(response);
} }
} .to_body();
let mut response = Response::new(payload);
*response.status_mut() = StatusCode::UNPROCESSABLE_ENTITY;
return Ok(response);
};
log::info!("Password changed for player {}", res.player_name);
let mut response = Response::new(Full::<Bytes>::default()); let mut response = Response::new(Full::<Bytes>::default());
*response.status_mut() = StatusCode::NO_CONTENT; *response.status_mut() = StatusCode::NO_CONTENT;
Ok(response) Ok(response)