#![no_std]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc = include_str!("../README.md")]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/RustCrypto/media/6ee8e381/logo.svg",
html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/media/6ee8e381/logo.svg"
)]
#![forbid(unsafe_code)]
#![warn(
clippy::mod_module_files,
clippy::unwrap_used,
missing_docs,
unused_qualifications
)]
#[cfg(feature = "alloc")]
extern crate alloc;
mod error;
mod ident;
mod output;
mod params;
mod salt;
mod string_buf;
mod value;
pub use error::{Error, Result};
pub use ident::Ident;
pub use output::Output;
pub use params::ParamsString;
pub use salt::{Salt, SaltString};
pub use value::{Decimal, Value};
use base64ct::Base64Unpadded as B64;
use core::{fmt, str::FromStr};
use string_buf::StringBuf;
#[cfg(feature = "alloc")]
use alloc::string::{String, ToString};
const PASSWORD_HASH_SEPARATOR: char = '$';
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PasswordHash {
pub algorithm: Ident,
pub version: Option<Decimal>,
pub params: ParamsString,
pub salt: Option<Salt>,
pub hash: Option<Output>,
}
impl PasswordHash {
pub fn new(s: &str) -> Result<Self> {
if s.is_empty() {
return Err(Error::MissingField);
}
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::MissingField);
}
let algorithm = fields
.next()
.ok_or(Error::MissingField)
.and_then(Ident::from_str)?;
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.parse()?);
}
if let Some(field) = fields.next() {
hash = Some(Output::decode(field)?);
}
if fields.next().is_some() {
return Err(Error::TrailingData);
}
Ok(Self {
algorithm,
version,
params,
salt,
hash,
})
}
#[allow(deprecated)]
#[cfg(feature = "alloc")]
#[deprecated(since = "0.3.0", note = "Use `PasswordHash` or `String` instead")]
pub fn serialize(&self) -> PasswordHashString {
self.into()
}
}
impl FromStr for PasswordHash {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Self::new(s)
}
}
impl TryFrom<&str> for PasswordHash {
type Error = Error;
fn try_from(s: &str) -> Result<Self> {
Self::new(s)
}
}
impl fmt::Display for PasswordHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}", PASSWORD_HASH_SEPARATOR, self.algorithm)?;
if let Some(version) = self.version {
write!(f, "{PASSWORD_HASH_SEPARATOR}v={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(())
}
}
#[deprecated(since = "0.3.0", note = "Use `PasswordHash` or `String` instead")]
#[cfg(feature = "alloc")]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PasswordHashString {
string: String,
}
#[cfg(feature = "alloc")]
#[allow(clippy::len_without_is_empty, deprecated)]
impl PasswordHashString {
pub fn new(s: &str) -> Result<Self> {
PasswordHash::new(s).map(Into::into)
}
pub fn password_hash(&self) -> PasswordHash {
PasswordHash::new(&self.string).expect("malformed password hash")
}
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
}
}
#[allow(deprecated)]
#[cfg(feature = "alloc")]
impl AsRef<str> for PasswordHashString {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[allow(deprecated)]
#[cfg(feature = "alloc")]
impl From<PasswordHash> for PasswordHashString {
fn from(hash: PasswordHash) -> PasswordHashString {
PasswordHashString::from(&hash)
}
}
#[allow(deprecated)]
#[cfg(feature = "alloc")]
impl From<&PasswordHash> for PasswordHashString {
fn from(hash: &PasswordHash) -> PasswordHashString {
PasswordHashString {
string: hash.to_string(),
}
}
}
#[allow(deprecated)]
#[cfg(feature = "alloc")]
impl FromStr for PasswordHashString {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Self::new(s)
}
}
#[allow(deprecated)]
#[cfg(feature = "alloc")]
impl fmt::Display for PasswordHashString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}