kochab/src/user_management/user.rs

505 lines
18 KiB
Rust

//! Several structs representing data about users
//!
//! This module contains any structs needed to store and retrieve data about users. The
//! different varieties have different purposes and come from different places.
//!
//! [`User`] is the most common for of user struct, and typically comes from calling
//! [`Request::user()`](crate::types::Request::user()). This is an enum with several
//! variants, and can be specialized into a [`NotSignedInUser`] or a [`RegisteredUser`] if
//! the user has presented a certificate. These two subtypes have more specific
//! information, like the user's username and active certificate.
//!
//! [`RegisteredUser`] is particularly signifigant in that this is the struct used to modify
//! the data stored for almost all users. This is accomplished through the
//! [`as_mut()`](RegisteredUser::as_mut) method. Changes made this way must be persisted
//! using [`save()`](RegisteredUser::save()) or by dropping the user.
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use sled::Transactional;
#[cfg(not(feature = "ring"))]
use std::time::{SystemTime, UNIX_EPOCH};
use crate::user_management::UserManager;
use crate::user_management::Result;
#[cfg(feature = "user_management_advanced")]
const ARGON2_CONFIG: argon2::Config = argon2::Config {
ad: &[],
hash_length: 32,
lanes: 1,
mem_cost: 4096,
secret: &[],
thread_mode: argon2::ThreadMode::Sequential,
time_cost: 3,
variant: argon2::Variant::Argon2id,
version: argon2::Version::Version13,
};
#[cfg(all(feature = "user_management_advanced", feature = "ring"))]
lazy_static::lazy_static! {
static ref RANDOM: ring::rand::SystemRandom = ring::rand::SystemRandom::new();
}
/// An struct corresponding to the data stored in the user tree
///
/// In order to generate a full user obj, you need to perform a lookup with a specific
/// certificate.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub (crate) struct PartialUser<UserData> {
pub data: UserData,
pub certificates: Vec<[u8; 32]>,
pub username: String,
#[cfg(feature = "user_management_advanced")]
pub pass_hash: Option<(Vec<u8>, [u8; 32])>,
}
impl<UserData> PartialUser<UserData> {
/// Write user data to the database
///
/// This MUST be called if the user data is modified using the AsMut trait, or else
/// changes will not be written to the database
fn store(&self, tree: &sled::Tree, uid: u64) -> Result<()>
where
UserData: Serialize
{
tree.insert(
uid.to_le_bytes(),
bincode::serialize(&self)?,
)?;
Ok(())
}
}
/// Any information about the connecting user
#[derive(Clone, Debug)]
pub enum User<UserData: Serialize + DeserializeOwned> {
/// A user who is connected without using a client certificate.
///
/// This could be a user who has an account but just isn't presenting a certificate at
/// the minute, a user whose client does not support client certificates, or a user
/// who has not yet created a certificate for the site
Unauthenticated,
/// A user who is connecting with a certificate that isn't connected to an account
///
/// This is typically a new user who hasn't set up an account yet, or a user
/// connecting with a new certificate that needs to be added to an existing account.
NotSignedIn(NotSignedInUser),
/// A user connecting with an identified account
SignedIn(RegisteredUser<UserData>),
}
#[derive(Clone, Debug)]
/// Data about a user with a certificate not associated with an account
///
/// For more information about the user lifecycle and sign-in stages, see [`User`]
pub struct NotSignedInUser {
pub (crate) certificate: [u8; 32],
pub (crate) manager: UserManager,
}
impl NotSignedInUser {
/// Register a new user with this certificate.
///
/// This creates a new user & user data entry in the database with the given username.
/// From now on, when this user logs in with this certificate, they will be
/// automatically authenticated, and their user data automatically retrieved.
///
/// # Errors
/// The provided username must be unique, or else an error will be raised.
///
/// Additional errors might occur if there is a problem writing to the database
pub fn register<UserData: Serialize + DeserializeOwned + Default>(
self,
username: String,
) -> Result<RegisteredUser<UserData>> {
if self.manager.users.contains_key(username.as_str())? {
Err(super::UserManagerError::UsernameNotUnique)
} else {
// Create the partial user that will go into the database. We can't create
// the full user yet, since the ID won't be generated until we perform the
// insert.
let partial = PartialUser {
username,
data: UserData::default(),
certificates: vec![self.certificate],
#[cfg(feature = "user_management_advanced")]
pass_hash: None,
};
let serialized = bincode::serialize(&partial)?;
// Insert the user into the three relevant tables, thus finalizing their
// creation. This also produces the user id.
let id = (&self.manager.users, &self.manager.certificates, &self.manager.usernames)
.transaction(|(tx_usr, tx_crt, tx_nam)| {
let id = tx_usr.generate_id()?;
let id_bytes = id.to_le_bytes();
tx_usr.insert(
&id_bytes,
serialized.as_slice(),
)?;
tx_crt.insert(
&partial.certificates[0],
&id_bytes,
)?;
tx_nam.insert(
partial.username.as_bytes(),
&id_bytes,
)?;
Ok(id)
})?;
info!("User {}#{:08x} registered!", partial.username, id);
Ok(RegisteredUser::new(
id,
Some(self.certificate),
self.manager,
partial,
))
}
}
#[cfg(feature = "user_management_advanced")]
/// Attach this certificate to an existing user
///
/// Try to add this certificate to another user using a username and password. If
/// successful, the user this certificate is attached to will be able to automatically
/// log in with either this certificate or any of the certificates they already had
/// registered.
///
/// This method can check the user's password to ensure that they match before
/// registering. If you want to skip this verification, perhaps because you've
/// already verified that this user owns this account, then you can pass [`None`] as
/// the password to skip the password check.
///
/// This method returns the new RegisteredUser instance representing the now-attached
/// user, or [`None`] if the username and password didn't match.
///
/// Because this method both performs a bcrypt verification and a database access, it
/// should be considered expensive.
///
/// If you already have a [`RegisteredUser`] that you would like to attach a
/// certificate to, consider using [`RegisteredUser::add_certificate()`]
///
/// # Errors
/// This will error if the user has yet to set a password.
///
/// Additional errors might occur if an error occurs during database lookup and
/// deserialization
pub fn attach<UserData: Serialize + DeserializeOwned>(
self,
username: &str,
password: Option<&[u8]>,
) -> Result<Option<RegisteredUser<UserData>>> {
if let Some(mut user) = self.manager.lookup_username(username)? {
// Perform password check, if caller wants
if let Some(password) = password {
if !user.check_password(password)? {
return Ok(None);
}
}
info!("User {} attached certificate with fingerprint {:x?}", username, &self.certificate[..]);
user.add_certificate(self.certificate)?;
user.active_certificate = Some(self.certificate);
Ok(Some(user))
} else {
Ok(None)
}
}
}
#[derive(Clone, Debug)]
/// Data about a logged in user
///
/// For more information about the user lifecycle and sign-in stages, see [`User`]
pub struct RegisteredUser<UserData: Serialize + DeserializeOwned> {
uid: u64,
active_certificate: Option<[u8; 32]>,
manager: UserManager,
inner: PartialUser<UserData>,
/// Indicates that [`RegisteredUser::as_mut()`] has been called, but [`RegisteredUser::save()`] has not
has_changed: bool,
}
impl<UserData: Serialize + DeserializeOwned> RegisteredUser<UserData> {
/// Create a new user from parts
pub (crate) fn new(
uid: u64,
active_certificate: Option<[u8; 32]>,
manager: UserManager,
inner: PartialUser<UserData>
) -> Self {
Self {
uid,
active_certificate,
manager,
inner,
has_changed: false,
}
}
/// Update the active certificate
///
/// This is not to be confused with [`RegisteredUser::add_certificate`], which
/// performs the database operations needed to register a new certificate to a user.
/// This literally just marks the active certificate.
pub (crate) fn with_cert(mut self, cert: [u8; 32]) -> Self {
self.active_certificate = Some(cert);
self
}
/// Get the fingerprint of the certificate that the user is currently using.
///
/// If this user was retrieved by a [`UserManager::lookup_user()`], this will be
/// [`None`]. In all other cases, this will be [`Some`].
pub fn active_certificate(&self) -> Option<&[u8; 32]> {
self.active_certificate.as_ref()
}
/// Produce a list of all certificate fingerprints registered to this account
pub fn all_certificates(&self) -> &Vec<[u8; 32]> {
&self.inner.certificates
}
/// Get the user's current username.
///
/// NOTE: This is not guaranteed not to change. If you need an immutable reference to
/// this user, prefer their [UID], which is guaranteed static.
///
/// [UID]: Self::uid()
pub fn username(&self) -> &String {
&self.inner.username
}
/// Get the user's id.
///
/// This is not guaranteed not to change.
pub fn uid(&self) -> u64 {
self.uid
}
#[cfg(feature = "user_management_advanced")]
/// Check a password against the user's password hash
///
/// # Errors
/// An error is raised if the user has yet to set a password, or if the user's
/// password hash is somehow malformed.
pub fn check_password(
&self,
try_password: impl AsRef<[u8]>
) -> Result<bool> {
if let Some((hash, salt)) = &self.inner.pass_hash {
Ok(argon2::verify_raw(
try_password.as_ref(),
salt,
hash.as_ref(),
&ARGON2_CONFIG,
)?)
} else {
Err(super::UserManagerError::PasswordNotSet)
}
}
#[cfg(feature = "user_management_advanced")]
/// Set's the password for this user
///
/// By default, users have no password, meaning the cannot add any certificates beyond
/// the one they created their account with. However, by setting their password,
/// users are able to add more devices to their account, and recover their account if
/// it's lost. Note that this will completely overwrite the users old password.
///
/// Use [`RegisteredUser::check_password()`] and [`NotSignedInUser::attach()`] to check
/// the password against another one, or to link a new certificate.
///
/// Because this method uses a key derivation algorithm, this should be considered a
/// very expensive operation.
pub fn set_password(
&mut self,
password: impl AsRef<[u8]>,
) -> Result<()> {
#[cfg_attr(feature = "ring", allow(unused_mut))]
let mut salt: [u8; 32];
// For a simple salt, system time nanos and a bit of PCG is plenty secure enough,
// but if we have ring anyway, may as well use it
#[cfg(feature = "ring")] {
salt = ring::rand::generate(&*RANDOM)
.expect("Error generating random salt")
.expose();
}
#[cfg(not(feature = "ring"))] {
let mut random = (SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos() | 0xffff) as u16;
random = random.wrapping_mul(0xd09d);
salt = [0; 32];
for byte in salt.as_mut() { *byte = pcg8(&mut random) }
}
self.inner.pass_hash = Some((
argon2::hash_raw(
password.as_ref(),
salt.as_ref(),
&ARGON2_CONFIG,
)?,
salt,
));
self.has_changed = true;
Ok(())
}
/// Write any updates to the user to the database.
///
/// Updates caused by calling methods directly on the user do not need to be saved.
/// This is only for changes made to the UserData.
pub fn save(&mut self) -> Result<()>
where
UserData: Serialize
{
self.inner.store(&self.manager.users, self.uid)?;
self.has_changed = false;
Ok(())
}
/// Register a new certificate to this user
///
/// This adds a new certificate to this user for use in logins. This requires a
/// couple database accesses, one in order to link the user to the certificate, and
/// one in order to link the certificate to the user.
///
/// If you have a [`NotSignedInUser`] and are looking for a way to link them to an
/// existing user, consider [`NotSignedInUser::attach()`], which contains facilities for
/// password checking and automatically performs the user lookup.
pub fn add_certificate(&mut self, certificate: [u8; 32]) -> Result<()> {
self.inner.certificates.push(certificate);
let inner_serialized = bincode::serialize(&self.inner)?;
let uid_bytes = self.uid.to_le_bytes();
(&self.manager.users, &self.manager.certificates)
.transaction(|(tx_usr, tx_crt)| {
tx_usr.insert(
&uid_bytes,
inner_serialized.clone(),
)?;
tx_crt.insert(
&certificate,
&uid_bytes,
)?;
Ok(())
})?;
Ok(())
}
/// Permanently delete this user and all their data
///
/// Permanently remove all traces of this user from the database, including:
/// * User data associated with their account
/// * Any certificates linked to their account
/// * Their username (which is freed for other users to take)
/// * Their password hash
/// * ~~Any happy memories you have with them~~
///
/// If you're not using [`UserManagementRoutes`], it's strongly recommended that you
/// expose some way for users to delete their accounts, in order to appropriately
/// respect their privacy and their right to their data.
///
/// If you *are* using [`UserManagementRoutes`], your users already have a way of
/// deleting their accounts! Just direct them to `/account`.
///
/// # Errors
/// Can error if the a database error occurs
pub fn delete(mut self) -> Result<()> {
// Prevent re-saving on drop
self.has_changed = false;
let certificates = self.all_certificates();
(&self.manager.users, &self.manager.certificates, &self.manager.usernames).transaction(|(tx_usr, tx_crt, tx_nam)| {
tx_nam.remove(
self.inner.username.as_str(),
)?;
tx_usr.remove(
&self.uid.to_le_bytes(),
)?;
for cert in certificates {
tx_crt.remove(
cert,
)?;
}
Ok(())
})?;
Ok(())
}
#[cfg(feature = "user_management_advanced")]
/// Check if the user has a password set
///
/// Since authentication is done using client certificates, users aren't required to
/// set a password up front. In some cases, it may be useful to know if a user has or
/// has not set a password yet.
///
/// This returns `true` if the user has a password set, or `false` otherwise
pub fn has_password(&self) -> bool {
self.inner.pass_hash.is_some()
}
/// Get an immutable reference to the data associated with this user
pub fn data(&self) -> &UserData {
&self.inner.data
}
/// Get a mutable reference to the data associated with this user
///
/// This automatically flags the user data as needing to be saved to the database,
/// which automatically performs the action when this user falls out of scope. If
/// need be, you can push these changes to the database sooner by calling [`save()`]
///
/// [`save()`]: Self::save()
pub fn mut_data(&mut self) -> &mut UserData {
self.has_changed = true;
&mut self.inner.data
}
}
impl<UserData: Serialize + DeserializeOwned> std::ops::Drop for RegisteredUser<UserData> {
fn drop(&mut self) {
if self.has_changed {
if let Err(e) = self.save() {
error!("Failed to save user data to database: {:?}", e);
}
}
}
}
impl<UserData: Serialize + DeserializeOwned> AsRef<UserData> for RegisteredUser<UserData> {
fn as_ref(&self) -> &UserData {
self.data()
}
}
impl<UserData: Serialize + DeserializeOwned> AsMut<UserData> for RegisteredUser<UserData> {
fn as_mut(&mut self) -> &mut UserData {
self.mut_data()
}
}
#[cfg(all(feature = "user_management_advanced", not(feature = "ring")))]
/// Inexpensive but low quality random
fn pcg8(state: &mut u16) -> u8 {
const MUL: u16 = 0xfb85;
const ADD: u16 = 0xfabb;
let mut x = *state;
*state = state.wrapping_mul(MUL).wrapping_add(ADD);
let count = x >> 13;
x ^= x >> 5;
((x >> 5) as u8).rotate_right(count as u32)
}