use crate::{schema, UIDCError}; use microrm::prelude::*; use microrm::schema::Stored; #[derive(Debug)] pub enum UserError { NoSuchUser, NoSuchChallenge, ChallengeInvalid, InvalidInput, } static PBKDF2_ROUNDS: std::num::NonZeroU32 = unsafe { std::num::NonZeroU32::new_unchecked(20000) }; fn generate_totp_digits(secret: &[u8], time_offset: isize) -> Result { use hmac::Mac; let mut mac = hmac::Hmac::::new_from_slice(secret) .map_err(|_| UserError::ChallengeInvalid)?; let timestamp = std::time::SystemTime::now() .duration_since(std::time::SystemTime::UNIX_EPOCH) .unwrap() .as_secs(); let interval = 30; let time_index = (timestamp / interval) as isize + time_offset; mac.update(u64::to_be_bytes(time_index as u64).as_slice()); let hmac = mac.finalize().into_bytes(); // this is from RFC4226 let offset = (hmac[19] & 0xf) as usize; let truncation = u32::from_be_bytes(hmac[offset..offset + 4].try_into().unwrap()) & 0x7fff_ffff; Ok(truncation % 1_000_000) } pub trait UserExt { fn stored_user(&self) -> &Stored; /*fn change_username(&mut self, new_name: &String) -> Result<(), UIDCError> { let realm = self.stored_user().realm; // check to ensure the new username isn't already in use if self.realm.users.with(schema::User::Username, new_name).first().get()?.is_some() { Err(UIDCError::Abort("username already in use")) } else { self.user.username = new_name.clone(); self.user.sync()?; Ok(()) } }*/ /// returns Ok(true) if challenge passed, Ok(false) if challenge failed, and /// UserError::NoSuchChallenge if challenge not found fn verify_challenge_by_type( &self, challenge_type: schema::AuthChallengeType, response: &[u8], ) -> Result { let ct = challenge_type.into(); let challenge = self .stored_user() .auth .with(schema::AuthChallenge::ChallengeType, &ct) .first() .get()? .ok_or(UserError::NoSuchChallenge)?; match challenge_type { schema::AuthChallengeType::Password => challenge.verify_password_challenge(response), schema::AuthChallengeType::TOTP => challenge.verify_totp_challenge(response), _ => todo!(), } } fn set_new_password(&self, password: &[u8]) -> Result<(), UIDCError> { self.stored_user() .auth .with( schema::AuthChallenge::ChallengeType, &schema::AuthChallengeType::Password.into(), ) .delete()?; let rng = ring::rand::SystemRandom::new(); let salt: [u8; 16] = ring::rand::generate(&rng) .expect("Couldn't generate random salt?") .expose(); let mut generated = [0u8; ring::digest::SHA256_OUTPUT_LEN]; ring::pbkdf2::derive( ring::pbkdf2::PBKDF2_HMAC_SHA256, PBKDF2_ROUNDS, &salt, password, &mut generated, ); self.stored_user().auth.insert(schema::AuthChallenge { user_id: self.stored_user().id(), challenge_type: schema::AuthChallengeType::Password.into(), public: salt.into(), secret: generated.into(), enabled: true, })?; Ok(()) } fn generate_totp_with_uri(&self) -> Result<(Vec, String), UIDCError> { let rng = ring::rand::SystemRandom::new(); let secret: [u8; 16] = ring::rand::generate(&rng) .expect("Couldn't generate random secret?") .expose(); let uri_secret = base32::encode( base32::Alphabet::RFC4648 { padding: false }, secret.as_slice(), ); let uri = format!( "otpauth://totp/uidc:{username}@uidc?secret={uri_secret}&issuer=uidc", username = self.stored_user().username ); Ok((secret.into(), uri)) } fn set_new_totp(&self, secret: &[u8]) -> Result<(), UIDCError> { self.clear_totp()?; self.stored_user().auth.insert(schema::AuthChallenge { user_id: self.stored_user().id(), challenge_type: schema::AuthChallengeType::TOTP.into(), public: vec![], secret: secret.into(), enabled: true, })?; Ok(()) } fn clear_totp(&self) -> Result<(), UIDCError> { self.stored_user() .auth .with( schema::AuthChallenge::ChallengeType, &schema::AuthChallengeType::TOTP.into(), ) .delete()?; Ok(()) } } impl UserExt for Stored { fn stored_user(&self) -> &Stored { self } } impl schema::AuthChallenge { pub fn verify_password_challenge(&self, response: &[u8]) -> Result { use ring::pbkdf2; if *self.challenge_type.as_ref() != schema::AuthChallengeType::Password { return Err(UIDCError::Abort( "verifying password challenge on non-password challenge", )); } Ok(pbkdf2::verify( pbkdf2::PBKDF2_HMAC_SHA256, PBKDF2_ROUNDS, self.public.as_slice(), response, self.secret.as_slice(), ) .is_ok()) } pub fn verify_totp_challenge(&self, response: &[u8]) -> Result { let response_digits = std::str::from_utf8(response) .map_err(|_| UserError::InvalidInput)? .parse::() .map_err(|_| UserError::InvalidInput)?; if *self.challenge_type.as_ref() != schema::AuthChallengeType::TOTP { return Err(UIDCError::Abort( "verifying TOTP challenge on non-TOTP challenge", )); } // allow for some clock skew for time_offset in -2..3 { if generate_totp_digits(self.secret.as_slice(), time_offset)? == response_digits { return Ok(true); } } Ok(false) } }