use std::{
io::{
Error as IoError,
ErrorKind::{AlreadyExists, InvalidData, WouldBlock},
Result as IoResult,
},
path::{Path, PathBuf},
};
use argon2::{Argon2, PasswordVerifier};
use otpauth::TOTP;
use rand::Rng;
use thiserror::Error as ThisError;
use tokio::fs::{DirBuilder, read, read_to_string, remove_file, write};
use uuid::Uuid;
use webauthn_rs::{
Webauthn,
prelude::{
CreationChallengeResponse, CredentialID, Passkey, PublicKeyCredential,
RegisterPublicKeyCredential, WebauthnError,
},
};
use crate::{AuthData, Challenge, SaneName, data::auth_data::LoginChallenge};
const BY_NAME_DIR: &str = "user_name";
const BY_ID_DIR: &str = "user_id";
const NAME_FILE: &str = "name";
const CHALLENGES_FILE: &str = "challenges";
const AUTH_DATA_FILE: &str = "auth";
async fn mkdir(dir: &Path) -> IoResult<()> {
DirBuilder::new().recursive(true).create(dir).await
}
pub struct UserDirectory {
prefix: PathBuf,
}
impl UserDirectory {
pub fn new(path_prefix: PathBuf) -> Self {
Self {
prefix: path_prefix,
}
}
fn id_path(&self, id: &Uuid) -> PathBuf {
let mut path = self.prefix.clone();
path.push(BY_ID_DIR);
path.push(id.to_string());
path
}
fn name_path(&self, namespace: &SaneName, name: &SaneName) -> PathBuf {
let mut path = self.prefix.clone();
path.push(BY_NAME_DIR);
path.push(namespace.as_str());
path.push(name.as_str());
path
}
pub async fn create_user(&self, namespace: &SaneName, name: &SaneName) -> IoResult<Uuid> {
let id = Uuid::new_v4();
let path = self.name_path(namespace, name);
if path.exists() {
Err(AlreadyExists)? } else {
mkdir(path.parent().expect("several components")).await?; write(path, id.as_bytes()).await?;
let id_path = self.id_path(&id);
if id_path.exists() {
Err(AlreadyExists)? }
mkdir(&id_path).await?;
let mut name_file = id_path.clone();
name_file.push(NAME_FILE);
write(name_file, format!("{namespace}/{name}")).await?;
let auth_data = AuthData::default();
self.save_auth_data(&id, &auth_data).await?;
let challenges: Vec<crate::Challenge> = Vec::with_capacity(0);
self.save_challenges_to_disk(&id, &challenges).await?;
Ok(id)
}
}
pub async fn user_id_by_name(&self, namespace: &SaneName, name: &SaneName) -> IoResult<Uuid> {
let path = self.name_path(namespace, name);
let id_data = read(path).await?;
Uuid::from_slice(&id_data).map_err(IoError::other)
}
pub async fn user_name_by_id(&self, id: &Uuid) -> IoResult<(SaneName, SaneName)> {
let mut name_file = self.id_path(id);
name_file.push("name");
let name_parts = read_to_string(name_file).await?;
if let Some((namespace, name)) = name_parts.split_once('/') {
let namespace = SaneName::new(namespace.to_string()).map_err(IoError::other)?;
let name = SaneName::new(name.to_string()).map_err(IoError::other)?;
Ok((namespace, name))
} else {
Err(InvalidData)? }
}
pub async fn load_auth_data(&self, id: &Uuid) -> IoResult<AuthData> {
let mut path = self.id_path(id);
path.push(AUTH_DATA_FILE);
let data = read_to_string(path).await?;
let result: AuthData = serde_json::from_str(&data).map_err(IoError::other)?;
Ok(result)
}
pub async fn save_auth_data(&self, id: &Uuid, auth_data: &AuthData) -> IoResult<()> {
let mut path = self.id_path(id);
path.push(AUTH_DATA_FILE);
write(
path,
serde_json::to_string(auth_data).expect("regular data"),
)
.await
}
pub async fn take(&self, id: &Uuid) -> IoResult<Vec<Challenge>> {
let mut path = self.id_path(id);
path.push(CHALLENGES_FILE);
let data = read_to_string(&path).await?;
remove_file(path).await?;
let result: Vec<Challenge> = serde_json::from_str(&data).map_err(IoError::other)?;
Ok(result)
}
pub async fn save_challenges_to_disk(
&self,
id: &Uuid,
challenges: &[Challenge],
) -> IoResult<()> {
let mut path = self.id_path(id);
path.push(CHALLENGES_FILE);
write(
path,
serde_json::to_string(challenges).expect("regular types"),
)
.await
}
pub async fn setup_login_challenge(
&self,
webauthn: &Webauthn,
user_id: &Uuid,
) -> IoResult<LoginChallenge> {
let auth_data = self.load_auth_data(user_id).await?;
let keys: Vec<Passkey> = auth_data.webauthn.values().cloned().collect();
let webauthn = if keys.is_empty() {
None
} else if let Ok((challenge, state)) = webauthn.start_passkey_authentication(&keys) {
let state = vec![Challenge::WebauthnLogin(state)];
self.save_challenges_to_disk(user_id, &state).await?;
Some(challenge)
} else {
todo!("Handle start_passkey_authentication() failure");
};
Ok(LoginChallenge {
password: auth_data.phc.is_some(),
totp: auth_data.totp_setup.is_some(),
webauthn,
})
}
pub async fn verify_login_challenge(
&self,
webauthn: &Webauthn,
user_id: &Uuid,
password: Option<&str>,
totp: Option<u32>,
pubkey_credential: Option<PublicKeyCredential>,
) -> IoResult<bool> {
let now = jiff::Timestamp::now().as_second() as u64;
let mut auth_data = self.load_auth_data(user_id).await?;
let mut methods_count: u8 = 0;
if let Some(hash) = auth_data.password_hash() {
methods_count += 1;
if let Some(password) = password {
if Argon2::default()
.verify_password(password.as_bytes(), &hash)
.is_err()
{
return Ok(false);
}
} else {
return Ok(false); }
}
if let Some(validator) = auth_data.totp() {
methods_count += 1;
const DURATION: u64 = 30;
if let Some(totp) = totp {
let mut success = false;
for time in &[now - DURATION, now, now + DURATION] {
success = validator.verify(totp, DURATION, *time);
if success {
break;
}
}
if !success {
return Ok(false);
}
} else {
return Ok(false); }
}
if !auth_data.webauthn.is_empty() {
methods_count += 1;
let Ok(challenges) = self.take(user_id).await else {
return Ok(false); };
if challenges.len() != 1 {
return Ok(false);
}
let Challenge::WebauthnLogin(state) = &challenges[0] else {
return Ok(false);
};
if let Some(pubkey_credential) = pubkey_credential {
match webauthn.finish_passkey_authentication(&pubkey_credential, state) {
Ok(auth_result) => {
for key in auth_data.webauthn.values_mut() {
key.update_credential(&auth_result);
}
self.save_auth_data(user_id, &auth_data).await?;
}
Err(_) => {
return Ok(false);
}
}
} else {
return Ok(false); }
}
Ok(methods_count > 0) }
pub async fn new_totp(&self, user_id: &Uuid) -> IoResult<TOTP> {
let mut seed = [0u8; 20];
{
let mut random = rand::thread_rng();
if random.try_fill(&mut seed).is_err() {
return Err(WouldBlock.into()); };
}
let totp = TOTP::from_bytes(&seed);
let seed_base32 = totp.base32_secret();
let challenges = vec![Challenge::ConfirmTotp { seed_base32 }];
self.save_challenges_to_disk(user_id, &challenges).await?;
Ok(totp)
}
pub async fn confirm_new_totp_challenge(
&self,
user_id: &Uuid,
proposed_totp: u32,
) -> IoResult<bool> {
let now = jiff::Timestamp::now().as_second() as u64;
let mut challenges = self.take(user_id).await?;
if challenges.len() != 1 {
return Ok(false);
}
let Some(Challenge::ConfirmTotp { seed_base32 }) = challenges.pop() else {
return Err(InvalidData.into()); };
let Some(totp) = TOTP::from_base32(&seed_base32) else {
return Err(InvalidData.into()); };
if totp.verify(proposed_totp, 30, now) {
let mut auth_data = self.load_auth_data(user_id).await?;
auth_data.totp_setup = Some(seed_base32);
self.save_auth_data(user_id, &auth_data).await?;
Ok(true)
} else {
Ok(false)
}
}
pub async fn new_webauthn_credential(
&self,
user_id: &Uuid,
webauthn: &Webauthn,
) -> Result<CreationChallengeResponse, SetupWebauthnCredentialError> {
let auth_data = self.load_auth_data(user_id).await?;
let (namespace, username) = self.user_name_by_id(user_id).await?;
let namespaced_name = format!("{namespace}/{username}");
let exclude_credentials: Vec<CredentialID> = auth_data
.webauthn
.values()
.map(|k| k.cred_id().clone())
.collect();
let (fido2_config, state) = webauthn.start_passkey_registration(
*user_id,
namespaced_name.as_str(),
username.as_str(),
Some(exclude_credentials),
)?;
let challenges = vec![Challenge::WebauthnRegister(state)];
self.save_challenges_to_disk(user_id, &challenges).await?;
Ok(fido2_config)
}
pub async fn confirm_new_webauthn_credential(
&self,
user_id: &Uuid,
webauthn: &Webauthn,
key_name: SaneName,
fido2data: RegisterPublicKeyCredential,
) -> Result<(), SetupWebauthnCredentialError> {
let challenges = self.take(user_id).await?;
if challenges.len() != 1 {
return Err(SetupWebauthnCredentialError::BadChallenge);
}
let Challenge::WebauthnRegister(state) = &challenges[0] else {
return Err(SetupWebauthnCredentialError::BadChallenge);
};
let new_key = webauthn.finish_passkey_registration(&fido2data, state)?;
let mut auth_data = self.load_auth_data(user_id).await?;
auth_data.webauthn.insert(key_name, new_key);
self.save_auth_data(user_id, &auth_data).await?;
Ok(())
}
pub async fn delete_webauthn_credential(
&self,
user_id: &Uuid,
key_name: SaneName,
) -> IoResult<()> {
let mut auth_data = self.load_auth_data(user_id).await?;
auth_data.webauthn.remove(&key_name); self.save_auth_data(user_id, &auth_data).await?;
Ok(())
}
}
#[derive(Debug, ThisError)]
#[non_exhaustive]
pub enum SetupWebauthnCredentialError {
#[error("Reading/writing data failed")]
IO(#[from] IoError),
#[error("Webauthn setup ceremony failed")]
WebAuthn(#[from] WebauthnError),
#[error("Bad challenge data on disk")]
BadChallenge,
}
pub async fn username_from_id(auth_data: PathBuf, user_id: &Uuid) -> IoResult<SaneName> {
Ok(UserDirectory::new(auth_data)
.user_name_by_id(user_id)
.await?
.1)
}