use anyhow::Result;
use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine as _};
use ironcrypto::kdf::argon2::{phc, Argon2Params, Variant, Version};
pub(crate) const PBKDF2_HASH_ROUNDS: u32 = 600_000;
const PBKDF2_HASH_LEN: usize = 32;
pub(crate) fn argon2id_hash(builtin: &str, password: &[u8], salt: &[u8]) -> Result<String> {
phc::hash_password_with(
Variant::Argon2id,
Version::V0x13,
&Argon2Params::DEFAULT,
password,
salt,
&[],
&[],
&[],
32,
)
.map_err(|e| crate::safety::ic_failed(builtin, e))
}
pub(crate) fn pbkdf2_sha256_hash(builtin: &str, password: &[u8], salt: &[u8]) -> Result<String> {
let hash = pbkdf2_sha256_derive(builtin, password, salt, PBKDF2_HASH_ROUNDS, PBKDF2_HASH_LEN)?;
Ok(format!(
"$pbkdf2-sha256$i={},l={}${}${}",
PBKDF2_HASH_ROUNDS,
PBKDF2_HASH_LEN,
STANDARD_NO_PAD.encode(salt),
STANDARD_NO_PAD.encode(&hash)
))
}
pub(crate) fn pbkdf2_sha256_derive(
builtin: &str,
password: &[u8],
salt: &[u8],
rounds: u32,
out_len: usize,
) -> Result<Vec<u8>> {
let mut out = vec![0u8; out_len];
ironcrypto::kdf::pbkdf2::<ironcrypto::mac::HmacSha256>(password, salt, rounds, &mut out)
.map_err(|e| crate::safety::ic_failed(builtin, e))?;
Ok(out)
}
pub(crate) fn pbkdf2_sha256_verify_derive(
builtin: &str,
password: &[u8],
salt: &[u8],
rounds: u32,
out_len: usize,
) -> Result<Vec<u8>> {
if let Ok(out) = pbkdf2_sha256_derive(builtin, password, salt, rounds, out_len) {
return Ok(out);
}
let mut out = vec![0u8; out_len];
pbkdf2_sha256_rfc8018(builtin, password, salt, rounds, &mut out)?;
Ok(out)
}
pub(crate) fn pbkdf2_sha256_rfc8018(
builtin: &str,
password: &[u8],
salt: &[u8],
iterations: u32,
out: &mut [u8],
) -> Result<()> {
use ironcrypto::core_types::traits::Mac as _;
let mac = |key: &[u8], data: &[u8]| -> Result<Vec<u8>> {
ironcrypto::mac::HmacSha256::mac(key, data)
.map(|tag| tag.as_ref().to_vec())
.map_err(|e| crate::safety::ic_failed(builtin, e))
};
for (index, block) in out.chunks_mut(32).enumerate() {
let mut first_input = salt.to_vec();
first_input.extend_from_slice(&((index as u32) + 1).to_be_bytes());
let mut u = mac(password, &first_input)?;
let mut t = u.clone();
for _ in 1..iterations {
u = mac(password, &u)?;
for (a, b) in t.iter_mut().zip(u.iter()) {
*a ^= b;
}
}
block.copy_from_slice(&t[..block.len()]);
}
Ok(())
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct Pbkdf2Phc {
pub rounds: Option<u32>,
pub output_length: Option<usize>,
pub salt: Vec<u8>,
pub hash: Vec<u8>,
}
pub(crate) fn parse_pbkdf2_sha256(stored: &str) -> Option<Pbkdf2Phc> {
let rest = stored.strip_prefix("$pbkdf2-sha256$")?;
let mut fields = rest.split('$');
let params = fields.next()?;
let salt = decode_canonical_b64(fields.next()?)?;
let hash = decode_canonical_b64(fields.next()?)?;
if fields.next().is_some() {
return None;
}
let (mut rounds, mut output_length) = (None, None);
for field in params.split(',') {
let (name, value) = field.split_once('=')?;
let value = canonical_decimal(value)?;
let slot = match name {
"i" => &mut rounds,
"l" => &mut output_length,
_ => return None,
};
if slot.is_some() {
return None;
}
*slot = Some(value);
}
let output_length = match output_length {
Some(l) => {
let l = usize::try_from(l).ok()?;
if hash.len() != l {
return None;
}
Some(l)
}
None => None,
};
Some(Pbkdf2Phc {
rounds,
output_length,
salt,
hash,
})
}
fn decode_canonical_b64(s: &str) -> Option<Vec<u8>> {
let bytes = STANDARD_NO_PAD.decode(s).ok()?;
(STANDARD_NO_PAD.encode(&bytes) == s).then_some(bytes)
}
fn canonical_decimal(s: &str) -> Option<u32> {
if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
if s.len() > 1 && s.starts_with('0') {
return None;
}
s.parse().ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn salt_from(base: u8) -> [u8; 16] {
let mut s = [0u8; 16];
for (i, b) in s.iter_mut().enumerate() {
*b = base.wrapping_add(i as u8);
}
s
}
const ARGON2_DEFAULT_1: &str = "$argon2id$v=19$m=19456,t=2,p=1$AAECAwQFBgcICQoLDA0ODw$gYJZtjEAJqjg26xdLmknq8/bB7MiWPrE9hsYuA+SkIU";
const ARGON2_DEFAULT_2: &str = "$argon2id$v=19$m=19456,t=2,p=1$gIGCg4SFhoeIiYqLjI2Ojw$3QMV47HYxBCzJUrQTS6njNOU/5f4GzvVs/qEm0Gojcs";
const ARGON2_CUSTOM: &str = "$argon2id$v=19$m=4096,t=3,p=2$ICEiIyQlJicoKSorLC0uLw$goVf4dWyyeylXc+CzM/YCU3/zX/17l5gSKlKGNhyRlo";
const PBKDF2_1000: &str = "$pbkdf2-sha256$i=1000,l=32$QEFCQ0RFRkdISUpLTE1OTw$wjfXzyJ4EYivw62TLSzmP1gcQ08iJ0Ii6aYSMUorh48";
const PBKDF2_DEFAULT: &str = "$pbkdf2-sha256$i=600000,l=32$YGFiY2RlZmdoaWprbG1ubw$+epjx17QTBTl+7hf30Ud2f90VcIV9iH6js5gQAGFLaY";
#[test]
fn argon2id_hash_reproduces_the_old_crate_output() {
let salt = salt_from(0x00);
let h = argon2id_hash("test", b"correct horse battery staple", &salt).unwrap();
assert_eq!(h, ARGON2_DEFAULT_1);
let salt = salt_from(0x80);
let h = argon2id_hash("test", "pässwörd ✓ 密码".as_bytes(), &salt).unwrap();
assert_eq!(h, ARGON2_DEFAULT_2);
}
#[test]
fn old_argon2_hashes_verify_under_ironcrypto() {
phc::verify_password(b"correct horse battery staple", ARGON2_DEFAULT_1).unwrap();
phc::verify_password("pässwörd ✓ 密码".as_bytes(), ARGON2_DEFAULT_2).unwrap();
phc::verify_password(b"custom-params", ARGON2_CUSTOM).unwrap();
assert!(phc::verify_password(b"custom-param", ARGON2_CUSTOM).is_err());
}
#[test]
fn pbkdf2_hash_reproduces_the_old_crate_output_at_default_cost() {
let salt = salt_from(0x60);
let h = pbkdf2_sha256_hash("test", b"fips default", &salt).unwrap();
assert_eq!(h, PBKDF2_DEFAULT);
}
#[test]
fn old_pbkdf2_hash_verifies_under_the_parser_and_derivation() {
let parsed = parse_pbkdf2_sha256(PBKDF2_1000).expect("canonical PHC");
assert_eq!(parsed.rounds, Some(1000));
assert_eq!(parsed.output_length, Some(32));
let derived =
pbkdf2_sha256_derive("test", b"pbkdf2 fixture", &parsed.salt, 1000, 32).unwrap();
assert_eq!(derived, parsed.hash);
assert_eq!(parsed.salt, salt_from(0x40).to_vec());
}
#[test]
fn pbkdf2_parser_refuses_what_the_writers_never_produce() {
let base = PBKDF2_1000;
for bad in [
base.replace(
"$wjfXzyJ4EYivw62TLSzmP1gcQ08iJ0Ii6aYSMUorh48",
"$wjfXzyJ4EYivw62TLSzmP1gcQ08iJ0Ii6aYSMUorh48=",
)
.as_str(),
base.replace("i=1000", "i=01000").as_str(),
base.replace("l=32", "l=32,l=32").as_str(),
base.replace("l=32", "x=32").as_str(),
base.replace("l=32", "l=16").as_str(),
format!("{base}$extra").as_str(),
] {
assert!(parse_pbkdf2_sha256(bad).is_none(), "accepted: {bad}");
}
}
#[test]
fn argon2_hash_is_refused_by_the_pbkdf2_parser_and_vice_versa() {
assert!(parse_pbkdf2_sha256(ARGON2_DEFAULT_1).is_none());
assert!(phc::PasswordHash::parse(PBKDF2_1000).is_err());
}
}