use zeroize::Zeroizing;
pub const KDF_TYPE_PBKDF2: i32 = 0;
pub const KDF_TYPE_ARGON2ID: i32 = 1;
pub const PBKDF2_SERVER_ITERATIONS: u32 = 100_000;
#[cfg(not(target_family = "wasm"))]
pub fn pbkdf2_derive(password: &[u8], salt: &[u8], iterations: u32, key_len: u32) -> Pbkdf2Key {
use pbkdf2::pbkdf2_hmac;
use sha2::Sha256;
let mut output = vec![0u8; key_len as usize];
pbkdf2_hmac::<Sha256>(password, salt, iterations, &mut output);
Pbkdf2Key(Zeroizing::new(output))
}
#[cfg(not(target_family = "wasm"))]
pub fn pbkdf2_verify(password: &[u8], salt: &[u8], expected: &[u8], iterations: u32, key_len: u32) -> bool {
let computed = pbkdf2_derive(password, salt, iterations, key_len);
constant_time_eq(&computed.0, expected)
}
#[cfg(target_family = "wasm")]
pub async fn pbkdf2_derive(password: &[u8], salt: &[u8], iterations: u32, key_len: u32) -> Result<Pbkdf2Key, CryptoError> {
use js_sys::{Object, Uint8Array};
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::JsFuture;
use web_sys::Crypto;
fn subtle() -> Result<web_sys::SubtleCrypto, CryptoError> {
let global = js_sys::global();
let crypto: Crypto = js_sys::Reflect::get(&global, &"crypto".into())
.map_err(|_| CryptoError::CryptoUnavailable)?
.into();
Ok(crypto.subtle())
}
fn js_set(obj: &Object, key: &str, val: &JsValue) -> Result<(), CryptoError> {
js_sys::Reflect::set(obj, &JsValue::from_str(key), val)
.map(|_| ())
.map_err(|e| CryptoError::JsError(format!("set {key}: {e:?}")))
}
let subtle = subtle()?;
let password_arr = Uint8Array::from(password);
let import_promise = subtle.import_key_with_str(
"raw", &password_arr, "PBKDF2", false,
&js_sys::Array::from_iter([JsValue::from_str("deriveBits")]),
).map_err(|e| CryptoError::JsError(format!("import_key: {e:?}")))?;
let key = JsFuture::from(import_promise).await
.map_err(|e| CryptoError::JsError(format!("import_key await: {e:?}")))?;
let algo = Object::new();
js_set(&algo, "name", &"PBKDF2".into())?;
js_set(&algo, "hash", &"SHA-256".into())?;
js_set(&algo, "salt", &Uint8Array::from(salt))?;
js_set(&algo, "iterations", &JsValue::from(iterations))?;
let derive_promise = subtle.derive_bits_with_object(&algo, &key.into(), key_len * 8)
.map_err(|e| CryptoError::JsError(format!("derive_bits: {e:?}")))?;
let result = JsFuture::from(derive_promise).await
.map_err(|e| CryptoError::JsError(format!("derive_bits await: {e:?}")))?;
let buf = Uint8Array::new(&result);
Ok(Pbkdf2Key(Zeroizing::new(buf.to_vec())))
}
#[cfg(target_family = "wasm")]
pub async fn pbkdf2_verify(password: &[u8], salt: &[u8], expected: &[u8], iterations: u32, key_len: u32) -> Result<bool, CryptoError> {
let computed = pbkdf2_derive(password, salt, iterations, key_len).await?;
Ok(constant_time_eq(&computed.0, expected))
}
pub const ARGON2ID_MEMORY_KB: u32 = 65536; pub const ARGON2ID_PARALLELISM: u32 = 4;
pub const ARGON2ID_ITERATIONS: u32 = 3;
#[cfg(not(target_family = "wasm"))]
pub struct Argon2idKey(Zeroizing<Vec<u8>>);
#[cfg(not(target_family = "wasm"))]
impl Argon2idKey {
pub fn as_bytes(&self) -> &[u8] { &self.0 }
pub fn into_bytes(self) -> Vec<u8> { (*self.0).clone() }
}
#[cfg(not(target_family = "wasm"))]
pub fn argon2id_derive(
password: &[u8],
salt: &[u8],
iterations: u32,
memory_kb: u32,
parallelism: u32,
key_len: u32,
) -> Argon2idKey {
use argon2::{Argon2, Params, Version};
let params = Params::new(memory_kb, iterations, parallelism, Some(key_len as usize))
.expect("argon2 params");
let argon2 = Argon2::new(argon2::Algorithm::Argon2id, Version::V0x13, params);
let mut output = vec![0u8; key_len as usize];
argon2.hash_password_into(password, salt, &mut output)
.expect("argon2 derive");
Argon2idKey(Zeroizing::new(output))
}
#[cfg(not(target_family = "wasm"))]
pub fn argon2id_verify(
password: &[u8],
salt: &[u8],
expected: &[u8],
iterations: u32,
memory_kb: u32,
parallelism: u32,
key_len: u32,
) -> bool {
let computed = argon2id_derive(password, salt, iterations, memory_kb, parallelism, key_len);
constant_time_eq(&computed.0, expected)
}
pub struct PasswordKey(Zeroizing<Vec<u8>>);
impl PasswordKey {
pub fn as_bytes(&self) -> &[u8] { &self.0 }
pub fn into_bytes(self) -> Vec<u8> { (*self.0).clone() }
}
pub struct Pbkdf2Key(Zeroizing<Vec<u8>>);
impl Pbkdf2Key {
pub fn as_bytes(&self) -> &[u8] { &self.0 }
pub fn into_bytes(self) -> Vec<u8> { (*self.0).clone() }
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() { return false; }
let mut diff = 0u8;
for (x, y) in a.iter().zip(b.iter()) { diff |= x ^ y; }
diff == 0
}
#[cfg(target_family = "wasm")]
#[derive(Debug)]
pub enum CryptoError {
JsError(String),
CryptoUnavailable,
}
#[cfg(target_family = "wasm")]
impl core::fmt::Display for CryptoError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
CryptoError::JsError(s) => write!(f, "Crypto JS error: {s}"),
CryptoError::CryptoUnavailable => write!(f, "Web Crypto unavailable"),
}
}
}
#[cfg(target_family = "wasm")]
impl core::error::Error for CryptoError {}
#[cfg(not(target_family = "wasm"))]
pub fn verify_password(
kdf_type: i32,
password_hash: &[u8],
salt: &[u8],
stored_hash: &[u8],
iterations: u32,
memory_kb: Option<u32>,
parallelism: Option<u32>,
) -> bool {
match kdf_type {
KDF_TYPE_PBKDF2 => {
pbkdf2_verify(password_hash, salt, stored_hash, iterations, 32)
}
KDF_TYPE_ARGON2ID => {
argon2id_verify(
password_hash, salt, stored_hash,
iterations,
memory_kb.unwrap_or(ARGON2ID_MEMORY_KB),
parallelism.unwrap_or(ARGON2ID_PARALLELISM),
32,
)
}
_ => false,
}
}
#[cfg(target_family = "wasm")]
pub async fn verify_password(
kdf_type: i32,
password_hash: &[u8],
salt: &[u8],
stored_hash: &[u8],
iterations: u32,
) -> Result<bool, CryptoError> {
match kdf_type {
KDF_TYPE_PBKDF2 => {
pbkdf2_verify(password_hash, salt, stored_hash, iterations, 32).await
}
KDF_TYPE_ARGON2ID => {
Err(CryptoError::CryptoUnavailable)
}
_ => Ok(false),
}
}