#![cfg_attr(test, feature(test))]
#[cfg(feature = "base64")] mod base64;
#[cfg(feature = "serde")] pub mod serde;
#[cfg(feature = "postgres")] mod sqlx;
use std::num::{NonZero, NonZeroU32};
use argon2::Algorithm::Argon2id;
use argon2::password_hash::rand_core::{OsRng, RngCore};
use argon2::{Argon2, Block, Version};
use bytemuck::{Pod, Zeroable};
const DEFAULT_PARAMS: argon2::Params = argon2::Params::DEFAULT;
const DEFAULT_VERSION: Version = Version::V0x13;
const SALT_LEN: usize = 16;
const OUTPUT_LEN: usize = 32;
pub const OUTPUT_SIZE: usize = size_of::<SerializedHash>();
#[cfg(feature = "base64")]
pub const BASE64_OUTPUT_SIZE: usize = (OUTPUT_SIZE * 4).div_ceil(3);
#[derive(Debug, thiserror::Error)]
pub enum ParseError {
#[error("unrecognized version {0:#02x?}")]
InvalidVersion(u8),
#[error("memory too low, expected at least {expected}, got {got}")]
MemoryTooLow { expected: u32, got: u32 },
#[error("parameters out of range")]
InvalidParameters,
#[error("input is the wrong length, expected exactly {OUTPUT_SIZE} bytes")]
SliceLength,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Params {
iterations: NonZeroU32,
parallelism: NonZeroU32,
memory: NonZeroU32,
}
impl Params {
pub fn iterations(&self) -> u32 { self.iterations.get() }
pub fn parallelism(&self) -> u32 { self.parallelism.get() }
pub fn memory(&self) -> u32 { self.memory.get() }
pub fn block_count(&self) -> usize { argon2::Params::from(*self).block_count() }
#[inline]
fn from_argon2(params: &argon2::Params) -> Self {
Self {
iterations: NonZeroU32::new(params.t_cost()).unwrap(),
parallelism: NonZeroU32::new(params.p_cost()).unwrap(),
memory: NonZeroU32::new(params.m_cost()).unwrap(),
}
}
}
impl Default for Params {
fn default() -> Self { Self::from_argon2(&DEFAULT_PARAMS) }
}
impl From<Params> for argon2::Params {
fn from(value: Params) -> Self {
Self::new(
value.memory(),
value.iterations(),
value.parallelism(),
None,
)
.unwrap()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Hash {
params: Params,
version: Version,
salt: [u8; SALT_LEN],
out: [u8; OUTPUT_LEN],
}
#[derive(Pod, Zeroable, Clone, Copy)]
#[repr(C, packed)]
struct SerializedHash {
iterations: [u8; 3],
parallelism: [u8; 3],
memory: [u8; 4],
version: u8,
salt: [u8; 16],
out: [u8; 32],
}
fn get_u24be(v: [u8; 3]) -> u32 { u32::from_be_bytes([0, v[0], v[1], v[2]]) }
impl Hash {
#[inline]
fn serialized(&self) -> SerializedHash {
SerializedHash {
iterations: self.params.iterations().to_be_bytes()[1..]
.try_into()
.unwrap(),
parallelism: self.params.parallelism().to_be_bytes()[1..]
.try_into()
.unwrap(),
memory: self.params.memory().to_be_bytes(),
version: self.version as u8,
salt: self.salt,
out: self.out,
}
}
pub fn params(&self) -> &Params { &self.params }
pub fn version(&self) -> Version { self.version }
pub fn verify(&self, password: &[u8]) -> Result<bool, argon2::Error> { verify(password, self) }
pub fn write_to_slice(&self, slice: &mut [u8]) {
let serialized = self.serialized();
slice.copy_from_slice(bytemuck::bytes_of(&serialized));
}
pub fn to_bytes(&self) -> [u8; OUTPUT_SIZE] {
let mut slice = [0u8; _];
self.write_to_slice(&mut slice);
slice
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ParseError> {
if bytes.len() != OUTPUT_SIZE {
return Err(ParseError::SliceLength);
}
let serialized: &SerializedHash = bytemuck::from_bytes(bytes);
let iterations =
NonZero::new(get_u24be(serialized.iterations)).ok_or(ParseError::InvalidParameters)?;
let parallelism =
NonZero::new(get_u24be(serialized.parallelism)).ok_or(ParseError::InvalidParameters)?;
let memory = NonZero::new(u32::from_be_bytes(serialized.memory))
.ok_or(ParseError::InvalidParameters)?;
let required_memory = parallelism.get() * 8;
if memory.get() < required_memory {
return Err(ParseError::MemoryTooLow {
expected: required_memory,
got: memory.get(),
});
}
let version = Version::try_from(serialized.version as u32)
.map_err(|_| ParseError::InvalidVersion(serialized.version))?;
Ok(Self {
params: Params {
iterations,
parallelism,
memory,
},
version,
salt: serialized.salt,
out: serialized.out,
})
}
}
impl TryFrom<&[u8; 59]> for Hash {
type Error = ParseError;
fn try_from(value: &[u8; 59]) -> Result<Self, Self::Error> { Self::from_bytes(&*value) }
}
impl TryFrom<&[u8]> for Hash {
type Error = ParseError;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> { Self::from_bytes(value) }
}
impl From<Hash> for [u8; OUTPUT_SIZE] {
fn from(value: Hash) -> Self { value.to_bytes() }
}
impl std::hash::Hash for Hash {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.params.hash(state);
(self.version as u32).hash(state);
self.salt.hash(state);
self.out.hash(state);
}
}
pub fn hash(password: &[u8]) -> Result<Hash, argon2::Error> {
let mut blocks = vec![Block::default(); DEFAULT_PARAMS.block_count()];
hash_with_memory(password, blocks.as_mut_slice())
}
pub fn hash_with_memory(
password: &[u8],
memory: impl AsMut<[Block]>,
) -> Result<Hash, argon2::Error> {
let version = DEFAULT_VERSION;
let hasher = Argon2::new(Argon2id, version, DEFAULT_PARAMS);
let params = Params::from_argon2(hasher.params());
let mut hash = Hash {
params,
version,
salt: [0; _],
out: [0; _],
};
OsRng.fill_bytes(&mut hash.salt);
hasher.hash_password_into_with_memory(password, &hash.salt, &mut hash.out, memory)?;
Ok(hash)
}
pub fn verify(password: &[u8], hash: &Hash) -> Result<bool, argon2::Error> {
let mut blocks = vec![Block::default(); DEFAULT_PARAMS.block_count()];
verify_with_memory(password, hash, blocks.as_mut_slice())
}
pub fn verify_with_memory(
password: &[u8],
hash: &Hash,
memory: impl AsMut<[Block]>,
) -> Result<bool, argon2::Error> {
let mut out = [0; _];
let hasher = Argon2::new(Argon2id, hash.version, hash.params.into());
hasher.hash_password_into_with_memory(password, &hash.salt, &mut out, memory)?;
Ok(out.eq(&hash.out))
}
#[cfg(test)]
mod tests {
extern crate test;
use std::hint::black_box;
use std::sync::LazyLock;
use argon2::password_hash::{PasswordHashString, SaltString};
use argon2::{PasswordHash, PasswordHasher};
use test::Bencher;
use super::*;
pub(crate) static HASH: LazyLock<Hash> = LazyLock::new(|| hash(b"hunter2").unwrap());
static PHC_HASH: LazyLock<PasswordHash> = LazyLock::new(|| {
static SALT: LazyLock<SaltString> = LazyLock::new(|| SaltString::generate(&mut OsRng));
let argon2 = argon2::Argon2::default();
argon2.hash_password(b"hunter2", &*SALT).unwrap()
});
#[test]
fn it_works() {
let password = b"hunter2";
let hash = hash(password).unwrap();
assert!(hash.verify(password).unwrap());
}
#[test]
fn round_trip() {
let bytes = HASH.to_bytes();
let reconstructed = Hash::from_bytes(&bytes).unwrap();
assert_eq!(*HASH, reconstructed);
}
#[bench]
fn to_bytes(bencher: &mut Bencher) {
let hash = *HASH;
bencher.iter(|| black_box(hash).to_bytes());
}
#[bench]
fn from_bytes(bencher: &mut Bencher) {
let bytes = HASH.to_bytes();
bencher.iter(|| Hash::from_bytes(black_box(&bytes)));
}
#[bench]
fn from_phc_string(bencher: &mut Bencher) {
let phc_string = PasswordHashString::from(&*PHC_HASH);
bencher.iter(|| black_box(&phc_string).password_hash());
}
#[bench]
fn to_phc_string(bencher: &mut Bencher) {
let phc_hash = &*PHC_HASH;
bencher.iter(|| PasswordHashString::from(black_box(phc_hash)));
}
}