#[cfg(feature = "encryption")]
use crate::crypto::{CipherSuite, EncryptedSecret};
use crate::{fmt::impl_redacted_fmt, random, range::LengthRange};
use argon2::{PasswordHasher, PasswordVerifier, password_hash::SaltString};
use std::num::NonZeroU32;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[cfg(feature = "encryption")]
#[error("Crypto error: {0}")]
Crypto(#[from] crate::crypto::Error),
#[error("Random error: {0}")]
Random(#[from] crate::random::Error),
#[error("Argon2 hash error: {0}")]
Argon2HashError(argon2::password_hash::Error),
#[error("Hash algorithm is missing")]
HashAlgorithmMissing,
#[error("Hash algorithm is unknown or invalid: {0}")]
HashAlgorithmInvalid(u8),
#[error("Invalid UTF-8: {0}")]
Utf8(#[from] std::str::Utf8Error),
#[error("Password alphabet is empty. At least one alphabet should be enabled")]
EmptyAlphabet,
#[error("Specified length range is invalid: {0}")]
InvalidRange(LengthRange),
#[error("Min length specified in policy does not allow all alphabets to be present")]
ImpossiblePolicy,
#[error("Length mismatch: expected = {expected}; provided = {provided}")]
LengthMismatch {
expected: LengthRange,
provided: usize,
},
#[error("Digits are required but were not found")]
DigitsNotFound,
#[error("Lowercase symbols are required but were not found")]
LowercaseNotFound,
#[error("Uppercase symbols are required but were not found")]
UppercaseNotFound,
#[error("Special symbols are required but were not found")]
SpecialNotFound,
}
impl From<argon2::password_hash::Error> for Error {
fn from(value: argon2::password_hash::Error) -> Self {
Self::Argon2HashError(value)
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(zeroize::ZeroizeOnDrop, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct Password(String);
impl Password {
pub fn random(policy: &PasswordPolicy) -> Result<Self> {
let alph = policy.alphabet();
let mut passw = String::with_capacity(policy.length().min());
let alph_len = NonZeroU32::new(alph.len() as u32).ok_or(Error::EmptyAlphabet)?;
loop {
while passw.len() < policy.length().min() {
let idx = random::secure_idx(alph_len)?;
passw.push(alph[idx] as char);
}
if policy.validate(&passw).is_ok() {
return Ok(Self(passw));
} else {
passw.clear();
}
}
}
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
#[must_use]
pub fn from_string(data: String) -> Self {
Self(data)
}
#[cfg(feature = "encryption")]
pub fn encrypt(&self, key: &[u8], algo: CipherSuite) -> Result<EncryptedPassword> {
let enc = EncryptedSecret::encrypt(self.as_bytes(), key, algo)?;
Ok(EncryptedPassword::from_secret(enc))
}
pub fn to_default_hash(&self) -> Result<PasswordHash> {
self.to_default_argon2()
}
pub fn to_default_argon2(&self) -> Result<PasswordHash> {
let salt = SaltString::generate(&mut argon2::password_hash::rand_core::OsRng);
let h = argon2::Argon2::default()
.hash_password(self.as_bytes(), &salt)?
.to_string();
Ok(PasswordHash::Argon2id(h))
}
}
impl_redacted_fmt!(Password);
const ALG_ARGON2ID_V1: u8 = 0;
#[derive(zeroize::ZeroizeOnDrop, Clone)]
pub enum PasswordHash {
Argon2id(String),
}
impl PasswordHash {
pub fn to_bytes(&self) -> Vec<u8> {
match &self {
Self::Argon2id(h) => {
let mut res = Vec::with_capacity(1 + 64);
res.push(ALG_ARGON2ID_V1);
res.extend_from_slice(h.as_bytes());
res
}
}
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.is_empty() {
return Err(Error::HashAlgorithmMissing);
}
match data[0] {
ALG_ARGON2ID_V1 => Ok(Self::Argon2id(str::from_utf8(&data[1..])?.into())),
algo => Err(Error::HashAlgorithmInvalid(algo)),
}
}
pub fn verify(&self, passw: &Password) -> Result<()> {
match self {
PasswordHash::Argon2id(h) => {
let h = argon2::PasswordHash::new(h)?;
argon2::Argon2::default().verify_password(passw.as_bytes(), &h)?;
Ok(())
}
}
}
}
impl_redacted_fmt!(PasswordHash);
#[cfg(feature = "serde")]
mod serde_impl {
use super::*;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
impl Serialize for PasswordHash {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
let bytes = self.to_bytes();
serializer.serialize_bytes(&bytes)
}
}
impl<'de> Deserialize<'de> for PasswordHash {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let bytes = <Vec<u8>>::deserialize(deserializer)?;
PasswordHash::from_bytes(&bytes).map_err(serde::de::Error::custom)
}
}
}
#[cfg(feature = "sqlx")]
mod sqlx_impl {
use super::*;
use sqlx::{Database, Decode, Encode, Type, encode::IsNull, error::BoxDynError};
#[cfg(feature = "sqlx_postgres")]
impl sqlx::postgres::PgHasArrayType for PasswordHash {
fn array_type_info() -> sqlx::postgres::PgTypeInfo {
<Vec<u8> as sqlx::postgres::PgHasArrayType>::array_type_info()
}
}
impl<'a, DB: Database> Type<DB> for PasswordHash
where
&'a [u8]: Type<DB>,
{
fn type_info() -> <DB as Database>::TypeInfo {
<&[u8] as Type<DB>>::type_info()
}
fn compatible(ty: &<DB as Database>::TypeInfo) -> bool {
<&[u8] as Type<DB>>::compatible(ty)
}
}
impl<'a, DB: Database> Encode<'a, DB> for PasswordHash
where
Vec<u8>: Encode<'a, DB>,
{
fn encode_by_ref(
&self,
buf: &mut <DB as Database>::ArgumentBuffer<'a>,
) -> std::result::Result<IsNull, BoxDynError> {
<Vec<u8> as Encode<'a, DB>>::encode_by_ref(&self.to_bytes(), buf)
}
fn produces(&self) -> Option<<DB as Database>::TypeInfo> {
<Vec<u8> as Encode<'a, DB>>::produces(&self.to_bytes())
}
fn size_hint(&self) -> usize {
<Vec<u8> as Encode<'a, DB>>::size_hint(&self.to_bytes())
}
}
impl<'a, DB: Database> Decode<'a, DB> for PasswordHash
where
Vec<u8>: Decode<'a, DB>,
{
fn decode(value: <DB as Database>::ValueRef<'a>) -> std::result::Result<Self, BoxDynError> {
Ok(Self::from_bytes(&<Vec<u8> as Decode<'a, DB>>::decode(
value,
)?)?)
}
}
}
#[cfg(feature = "encryption")]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[cfg_attr(feature = "sqlx", sqlx(transparent))]
#[derive(Clone, Debug)]
pub struct EncryptedPassword(EncryptedSecret);
#[cfg(feature = "encryption")]
impl EncryptedPassword {
pub fn from_encrypted_bytes(input: &[u8]) -> Result<Self> {
let secret = EncryptedSecret::from_encrypted_bytes(input)?;
Ok(Self(secret))
}
#[must_use]
pub fn from_secret(secret: EncryptedSecret) -> Self {
Self(secret)
}
#[must_use]
pub fn to_encrypted_bytes(&self) -> Vec<u8> {
self.0.to_encrypted_bytes()
}
pub fn decrypt(&self, key: &[u8]) -> Result<Password> {
let plaintext = self.0.decrypt(key)?;
let s = String::from_utf8(plaintext).map_err(|e| e.utf8_error())?;
Ok(Password::from_string(s))
}
pub fn algorithm(&self) -> CipherSuite {
self.0.algorithm()
}
}
#[cfg(feature = "encryption")]
impl std::fmt::Display for EncryptedPassword {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone)]
pub struct PasswordPolicyBuilder {
range: LengthRange,
lowercase: bool,
uppercase: bool,
digits: bool,
special: bool,
}
impl PasswordPolicyBuilder {
pub fn length<R>(mut self, range: R) -> Self
where
R: std::ops::RangeBounds<usize>,
{
self.range = LengthRange::new(range);
self
}
pub fn lowercase(mut self, enable: bool) -> Self {
self.lowercase = enable;
self
}
pub fn uppercase(mut self, enable: bool) -> Self {
self.uppercase = enable;
self
}
pub fn digits(mut self, enable: bool) -> Self {
self.digits = enable;
self
}
pub fn special(mut self, enable: bool) -> Self {
self.special = enable;
self
}
pub fn build(self) -> Result<PasswordPolicy> {
let alph_total = self.lowercase as usize
+ self.uppercase as usize
+ self.digits as usize
+ self.special as usize;
if alph_total == 0 {
return Err(Error::EmptyAlphabet);
}
if self
.range
.max()
.map(|max| max < self.range.min())
.unwrap_or(false)
{
return Err(Error::InvalidRange(self.range));
}
if self.range.min() < alph_total {
return Err(Error::ImpossiblePolicy);
}
Ok(PasswordPolicy {
range: self.range,
lowercase: self.lowercase,
uppercase: self.uppercase,
digits: self.digits,
special: self.special,
})
}
}
#[derive(Debug, Clone)]
pub struct PasswordPolicy {
range: LengthRange,
lowercase: bool,
uppercase: bool,
digits: bool,
special: bool,
}
impl PasswordPolicy {
pub fn builder() -> PasswordPolicyBuilder {
let defaults = Self::with_sane_defaults();
PasswordPolicyBuilder {
range: defaults.range,
lowercase: defaults.lowercase,
uppercase: defaults.uppercase,
digits: defaults.digits,
special: defaults.special,
}
}
pub fn with_sane_defaults() -> Self {
Self {
range: LengthRange::new(12..),
lowercase: true,
uppercase: true,
digits: true,
special: false,
}
}
pub fn length(&self) -> LengthRange {
self.range
}
pub fn lowercase(&self) -> bool {
self.lowercase
}
pub fn uppercase(&self) -> bool {
self.uppercase
}
pub fn digits(&self) -> bool {
self.digits
}
pub fn special(&self) -> bool {
self.special
}
pub fn alphabet(&self) -> Vec<u8> {
let mut alph = Vec::new();
if self.lowercase {
alph.extend_from_slice(b"abcdefghijklmnopqrstuvwxyz");
}
if self.uppercase {
alph.extend_from_slice(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ");
}
if self.digits {
alph.extend_from_slice(b"0123456789");
}
if self.special {
alph.extend_from_slice(b"!@#$%^&*()-_=+[]{};:,.<>/?");
}
alph
}
pub fn validate(&self, passw: &str) -> Result<()> {
let mut uppercase = 0;
let mut lowercase = 0;
let mut digits = 0;
let mut special = 0;
for c in passw.chars() {
match c {
'a'..='z' => lowercase += 1,
'A'..='Z' => uppercase += 1,
'0'..='9' => digits += 1,
_ => special += 1,
}
}
let len = uppercase + lowercase + digits + special;
if !self.range.within(len) {
return Err(Error::LengthMismatch {
expected: self.range,
provided: len,
});
}
if self.uppercase && uppercase == 0 {
return Err(Error::UppercaseNotFound);
}
if self.lowercase && lowercase == 0 {
return Err(Error::LowercaseNotFound);
}
if self.digits && digits == 0 {
return Err(Error::DigitsNotFound);
}
if self.special && special == 0 {
return Err(Error::SpecialNotFound);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn length() {
let verifier = PasswordPolicy::builder().length(5..10).build().unwrap();
assert!(matches!(
verifier.validate("Ab1!"),
Err(Error::LengthMismatch { .. })
));
assert!(matches!(
verifier.validate("Abcdef123456!@#$"),
Err(Error::LengthMismatch { .. })
));
assert!(verifier.validate("Abc123!").is_ok());
}
#[test]
fn uppercase() {
let verifier = PasswordPolicy::builder()
.length(2..)
.uppercase(true)
.lowercase(false)
.digits(false)
.special(false)
.build()
.unwrap();
assert!(matches!(
verifier.validate("aaa"),
Err(Error::UppercaseNotFound)
));
assert!(verifier.validate("ABC").is_ok());
}
#[test]
fn lowercase() {
let verifier = PasswordPolicy::builder()
.length(2..)
.uppercase(false)
.lowercase(true)
.digits(false)
.special(false)
.build()
.unwrap();
assert!(matches!(
verifier.validate("AAA"),
Err(Error::LowercaseNotFound)
));
assert!(verifier.validate("abc").is_ok());
}
#[test]
fn digits() {
let verifier = PasswordPolicy::builder()
.length(2..)
.uppercase(false)
.lowercase(false)
.digits(true)
.special(false)
.build()
.unwrap();
assert!(matches!(
verifier.validate("AAA"),
Err(Error::DigitsNotFound)
));
assert!(verifier.validate("123").is_ok());
}
#[test]
fn special() {
let verifier = PasswordPolicy::builder()
.length(2..)
.uppercase(false)
.lowercase(false)
.digits(false)
.special(true)
.build()
.unwrap();
assert!(matches!(
verifier.validate("AAA"),
Err(Error::SpecialNotFound)
));
assert!(verifier.validate("!@#$").is_ok());
}
#[test]
fn defaults() {
let verifier = PasswordPolicy::with_sane_defaults();
assert!(verifier.validate("AytW416p75yV").is_ok());
assert!(matches!(
verifier.validate("Ab1!"),
Err(Error::LengthMismatch { .. })
));
assert!(matches!(
verifier.validate("byr35dht8sbe6"),
Err(Error::UppercaseNotFound)
));
}
#[test]
fn impossible_policy() {
let policy = PasswordPolicy::builder()
.length(3..)
.digits(true)
.lowercase(true)
.uppercase(true)
.special(true)
.build();
assert!(matches!(policy, Err(Error::ImpossiblePolicy)));
}
#[test]
fn invalid_range() {
let policy = PasswordPolicy::builder().length(8..4).build();
assert!(matches!(policy, Err(Error::InvalidRange(..))));
}
#[test]
fn empty_alphabet() {
let policy = PasswordPolicy::builder()
.digits(false)
.uppercase(false)
.lowercase(false)
.special(false)
.build();
assert!(matches!(policy, Err(Error::EmptyAlphabet)));
}
}