#![no_std]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc = include_str!("../README.md")]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg",
html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg"
)]
#![forbid(unsafe_code)]
#![warn(missing_docs, rust_2018_idioms, unused_lifetimes)]
#[cfg(feature = "alloc")]
extern crate alloc;
#[cfg(feature = "std")]
extern crate std;
#[cfg(feature = "rand_core")]
#[cfg_attr(docsrs, doc(cfg(feature = "rand_core")))]
pub use rand_core;
pub mod errors;
mod encoding;
mod ident;
mod output;
mod params;
mod salt;
mod traits;
mod value;
pub use crate::{
encoding::Encoding,
errors::{Error, Result},
ident::Ident,
output::Output,
params::ParamsString,
salt::{Salt, SaltString},
traits::{McfHasher, PasswordHasher, PasswordVerifier},
value::{Decimal, Value},
};
use core::fmt::{self, Debug};
#[cfg(feature = "alloc")]
use alloc::{
str::FromStr,
string::{String, ToString},
};
const PASSWORD_HASH_SEPARATOR: char = '$';
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PasswordHash<'a> {
pub algorithm: Ident<'a>,
pub version: Option<Decimal>,
pub params: ParamsString,
pub salt: Option<Salt<'a>>,
pub hash: Option<Output>,
}
impl<'a> PasswordHash<'a> {
pub fn new(s: &'a str) -> Result<Self> {
Self::parse(s, Encoding::default())
}
pub fn parse(s: &'a str, encoding: Encoding) -> Result<Self> {
if s.is_empty() {
return Err(Error::PhcStringTooShort);
}
let mut fields = s.split(PASSWORD_HASH_SEPARATOR);
let beginning = fields.next().expect("no first field");
if beginning.chars().next().is_some() {
return Err(Error::PhcStringInvalid);
}
let algorithm = fields
.next()
.ok_or(Error::PhcStringTooShort)
.and_then(Ident::try_from)?;
let mut version = None;
let mut params = ParamsString::new();
let mut salt = None;
let mut hash = None;
let mut next_field = fields.next();
if let Some(field) = next_field {
if field.starts_with("v=") && !field.contains(params::PARAMS_DELIMITER) {
version = Some(Value::new(&field[2..]).and_then(|value| value.decimal())?);
next_field = None;
}
}
if next_field.is_none() {
next_field = fields.next();
}
if let Some(field) = next_field {
if field.contains(params::PAIR_DELIMITER) {
params = field.parse()?;
next_field = None;
}
}
if next_field.is_none() {
next_field = fields.next();
}
if let Some(s) = next_field {
salt = Some(s.try_into()?);
}
if let Some(field) = fields.next() {
hash = Some(Output::decode(field, encoding)?);
}
if fields.next().is_some() {
return Err(Error::PhcStringTooLong);
}
Ok(Self {
algorithm,
version,
params,
salt,
hash,
})
}
pub fn generate(
phf: impl PasswordHasher,
password: impl AsRef<[u8]>,
salt: impl Into<Salt<'a>>,
) -> Result<Self> {
phf.hash_password(password.as_ref(), salt)
}
pub fn verify_password(
&self,
phfs: &[&dyn PasswordVerifier],
password: impl AsRef<[u8]>,
) -> Result<()> {
for &phf in phfs {
if phf.verify_password(password.as_ref(), self).is_ok() {
return Ok(());
}
}
Err(Error::Password)
}
pub fn encoding(&self) -> Encoding {
self.hash.map(|h| h.encoding()).unwrap_or_default()
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn serialize(&self) -> PasswordHashString {
self.into()
}
}
impl<'a> TryFrom<&'a str> for PasswordHash<'a> {
type Error = Error;
fn try_from(s: &'a str) -> Result<Self> {
Self::new(s)
}
}
impl<'a> fmt::Display for PasswordHash<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}", PASSWORD_HASH_SEPARATOR, self.algorithm)?;
if let Some(version) = self.version {
write!(f, "{}v={}", PASSWORD_HASH_SEPARATOR, version)?;
}
if !self.params.is_empty() {
write!(f, "{}{}", PASSWORD_HASH_SEPARATOR, self.params)?;
}
if let Some(salt) = &self.salt {
write!(f, "{}{}", PASSWORD_HASH_SEPARATOR, salt)?;
if let Some(hash) = &self.hash {
write!(f, "{}{}", PASSWORD_HASH_SEPARATOR, hash)?;
}
}
Ok(())
}
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PasswordHashString {
string: String,
encoding: Encoding,
}
#[cfg(feature = "alloc")]
#[allow(clippy::len_without_is_empty)]
impl PasswordHashString {
pub fn new(s: &str) -> Result<Self> {
Self::parse(s, Encoding::default())
}
pub fn parse(s: &str, encoding: Encoding) -> Result<Self> {
Ok(PasswordHash::parse(s, encoding)?.into())
}
pub fn password_hash(&self) -> PasswordHash<'_> {
PasswordHash::parse(&self.string, self.encoding).expect("malformed password hash")
}
pub fn encoding(&self) -> Encoding {
self.encoding
}
pub fn as_str(&self) -> &str {
self.string.as_str()
}
pub fn as_bytes(&self) -> &[u8] {
self.as_str().as_bytes()
}
pub fn len(&self) -> usize {
self.as_str().len()
}
pub fn algorithm(&self) -> Ident<'_> {
self.password_hash().algorithm
}
pub fn version(&self) -> Option<Decimal> {
self.password_hash().version
}
pub fn params(&self) -> ParamsString {
self.password_hash().params
}
pub fn salt(&self) -> Option<Salt<'_>> {
self.password_hash().salt
}
pub fn hash(&self) -> Option<Output> {
self.password_hash().hash
}
}
#[cfg(feature = "alloc")]
impl AsRef<str> for PasswordHashString {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[cfg(feature = "alloc")]
impl From<PasswordHash<'_>> for PasswordHashString {
fn from(hash: PasswordHash<'_>) -> PasswordHashString {
PasswordHashString::from(&hash)
}
}
#[cfg(feature = "alloc")]
impl From<&PasswordHash<'_>> for PasswordHashString {
fn from(hash: &PasswordHash<'_>) -> PasswordHashString {
PasswordHashString {
string: hash.to_string(),
encoding: hash.encoding(),
}
}
}
#[cfg(feature = "alloc")]
impl FromStr for PasswordHashString {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Self::new(s)
}
}
#[cfg(feature = "alloc")]
impl fmt::Display for PasswordHashString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}