123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201 |
- 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<u32, UIDCError> {
- use hmac::Mac;
- let mut mac = hmac::Hmac::<sha1::Sha1>::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<schema::User>;
- /*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<bool, UIDCError> {
- 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<u8>, 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<schema::User> {
- fn stored_user(&self) -> &Stored<schema::User> {
- self
- }
- }
- impl schema::AuthChallenge {
- pub fn verify_password_challenge(&self, response: &[u8]) -> Result<bool, UIDCError> {
- 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<bool, UIDCError> {
- let response_digits = std::str::from_utf8(response)
- .map_err(|_| UserError::InvalidInput)?
- .parse::<u32>()
- .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)
- }
- }
|