use crate::error::Argon2Error;
use crate::lexer::TokenizedHash;
use base64::engine::general_purpose::STANDARD_NO_PAD as b64_stdnopad;
use base64::Engine;
use rand::rngs::OsRng;
use rand::TryRngCore;
use std::ffi::CStr;
use std::mem::MaybeUninit;
use std::str::FromStr;
use std::{borrow::Cow, default::Default};
use crate::bindings::{
argon2_error_message, argon2d_ctx, argon2i_ctx, argon2id_ctx, Argon2_Context,
Argon2_ErrorCodes_ARGON2_OK, Argon2_version_ARGON2_VERSION_13,
};
#[derive(Clone, Copy, Debug)]
pub enum Algorithm {
Argon2d,
Argon2i,
Argon2id,
}
#[derive(Clone, Copy, Debug)]
pub struct Secret<'a>(&'a [u8]);
impl<'a> Secret<'a> {
pub fn using(secret: &'a [u8]) -> Self {
Self(secret)
}
}
impl<'a> From<&'a [u8]> for Secret<'a> {
fn from(secret: &'a [u8]) -> Self {
Self(secret)
}
}
impl<'a> From<&'a Vec<u8>> for Secret<'a> {
fn from(secret: &'a Vec<u8>) -> Self {
Self(secret)
}
}
impl<'a, const SIZE: usize> From<&'a [u8; SIZE]> for Secret<'a> {
fn from(secret: &'a [u8; SIZE]) -> Self {
Self(secret)
}
}
impl<'a> From<&'a str> for Secret<'a> {
fn from(secret: &'a str) -> Self {
Self(secret.as_bytes())
}
}
impl<'a> From<&'a String> for Secret<'a> {
fn from(secret: &'a String) -> Self {
Self(secret.as_bytes())
}
}
#[derive(Clone, Debug)]
pub struct Hasher<'a> {
alg: Algorithm,
custom_salt: Option<&'a [u8]>,
salt_len: u32,
hash_len: u32,
iterations: u32,
mem_cost_kib: u32,
threads: u32,
secret: Option<Secret<'a>>,
}
impl Default for Hasher<'_> {
fn default() -> Self {
Self {
alg: Algorithm::Argon2id,
custom_salt: None,
salt_len: 16,
hash_len: 32,
iterations: 18,
mem_cost_kib: 62500,
threads: 1,
secret: None,
}
}
}
impl<'a> Hasher<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn algorithm(mut self, alg: Algorithm) -> Self {
self.alg = alg;
self
}
pub fn custom_salt(mut self, salt: &'a [u8]) -> Self {
self.custom_salt = Some(salt);
self
}
pub fn salt_length(mut self, salt_len: u32) -> Self {
self.salt_len = salt_len;
self
}
pub fn hash_length(mut self, hash_len: u32) -> Self {
self.hash_len = hash_len;
self
}
pub fn iterations(mut self, iterations: u32) -> Self {
self.iterations = iterations;
self
}
pub fn memory_cost_kib(mut self, cost: u32) -> Self {
self.mem_cost_kib = cost;
self
}
pub fn threads(mut self, threads: u32) -> Self {
self.threads = threads;
self
}
pub fn secret(mut self, secret: Secret<'a>) -> Self {
self.secret = Some(secret);
self
}
pub fn hash(self, password: &[u8]) -> Result<Hash<'_>, Argon2Error> {
let hash_len_usize = match usize::try_from(self.hash_len) {
Ok(l) => l,
Err(_) => return Err(Argon2Error::InvalidParameter("Hash length is too big")),
};
let mut hash_buffer = MaybeUninit::new(Vec::with_capacity(hash_len_usize));
let mut hash_buffer = unsafe {
(*hash_buffer.as_mut_ptr()).set_len(hash_len_usize);
OsRng
.try_fill_bytes(&mut *hash_buffer.as_mut_ptr())
.expect("Failed to fill buffer with random bytes");
hash_buffer.assume_init()
};
let (salt_len_u32, salt_len_usize) = if let Some(s) = self.custom_salt {
let salt_len_u32 = match u32::try_from(s.len()) {
Ok(l) => l,
Err(_) => return Err(Argon2Error::InvalidParameter("Salt length is too big")),
};
(salt_len_u32, s.len())
} else {
let salt_len_usize = match usize::try_from(self.salt_len) {
Ok(l) => l,
Err(_) => return Err(Argon2Error::InvalidParameter("Salt length is too big")),
};
(self.salt_len, salt_len_usize)
};
let mut salt = if let Some(s) = self.custom_salt {
Vec::from(s)
} else {
let mut rand_salt = MaybeUninit::new(Vec::with_capacity(salt_len_usize));
unsafe {
(*rand_salt.as_mut_ptr()).set_len(salt_len_usize);
OsRng
.try_fill_bytes(&mut *rand_salt.as_mut_ptr())
.expect("Failed to fill buffer with random bytes");
rand_salt.assume_init()
}
};
let (secret_ptr, secret_len) = {
if let Some(s) = self.secret {
let length = match s.0.len().try_into() {
Ok(l) => l,
Err(_) => return Err(Argon2Error::InvalidParameter("Secret is too long")),
};
(s.0.as_ptr() as *mut _, length)
} else {
(std::ptr::null_mut(), 0)
}
};
let mut ctx = Argon2_Context {
out: hash_buffer.as_mut_ptr(),
outlen: self.hash_len,
pwd: password as *const _ as *mut _,
pwdlen: match password.len().try_into() {
Ok(l) => l,
Err(_) => return Err(Argon2Error::InvalidParameter("Password is too long")),
},
salt: salt.as_mut_ptr(),
saltlen: salt_len_u32,
secret: secret_ptr,
secretlen: secret_len,
ad: std::ptr::null_mut(),
adlen: 0,
t_cost: self.iterations,
m_cost: self.mem_cost_kib,
lanes: self.threads,
threads: self.threads,
version: Argon2_version_ARGON2_VERSION_13 as _,
allocate_cbk: None,
free_cbk: None,
flags: 0,
};
let result = unsafe {
match self.alg {
Algorithm::Argon2d => argon2d_ctx(&mut ctx as *mut _),
Algorithm::Argon2i => argon2i_ctx(&mut ctx as *mut _),
Algorithm::Argon2id => argon2id_ctx(&mut ctx as *mut _),
}
};
if result != Argon2_ErrorCodes_ARGON2_OK {
let err_msg = String::from_utf8_lossy(unsafe {
CStr::from_ptr(argon2_error_message(result)).to_bytes()
});
return Err(Argon2Error::CLibError(err_msg.into_owned()));
}
Ok(Hash {
alg: self.alg,
mem_cost_kib: self.mem_cost_kib,
iterations: self.iterations,
threads: self.threads,
salt: Cow::Owned(salt),
hash: Cow::Owned(hash_buffer),
})
}
}
#[derive(Clone, Debug)]
pub struct Hash<'a> {
alg: Algorithm,
mem_cost_kib: u32,
iterations: u32,
threads: u32,
salt: Cow<'a, [u8]>,
hash: Cow<'a, [u8]>,
}
#[allow(clippy::to_string_trait_impl)]
impl ToString for Hash<'_> {
fn to_string(&self) -> String {
let b64_salt = b64_stdnopad.encode(self.salt.as_ref());
let b64_hash = b64_stdnopad.encode(self.hash.as_ref());
let alg = match self.alg {
Algorithm::Argon2d => "d",
Algorithm::Argon2i => "i",
Algorithm::Argon2id => "id",
};
format!(
"$argon2{}$v={}$m={},t={},p={}${}${}",
alg,
Argon2_version_ARGON2_VERSION_13,
self.mem_cost_kib,
self.iterations,
self.threads,
b64_salt,
b64_hash,
)
}
}
impl FromStr for Hash<'_> {
type Err = Argon2Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let tokenized_hash = TokenizedHash::from_str(s)?;
if tokenized_hash.v != Argon2_version_ARGON2_VERSION_13 as _ {
return Err(Argon2Error::InvalidHash("Hash version is unsupported"));
}
let decoded_salt = match b64_stdnopad.decode(tokenized_hash.b64_salt) {
Ok(s) => s,
Err(_) => {
return Err(Argon2Error::InvalidHash(
"Invalid character in base64-encoded salt",
))
}
};
let decoded_hash = match b64_stdnopad.decode(tokenized_hash.b64_hash) {
Ok(h) => h,
Err(_) => {
return Err(Argon2Error::InvalidHash(
"Invalid character in base64-encoded hash",
))
}
};
Ok(Self {
alg: tokenized_hash.alg,
mem_cost_kib: tokenized_hash.mem_cost_kib,
iterations: tokenized_hash.iterations,
threads: tokenized_hash.threads,
salt: Cow::Owned(decoded_salt),
hash: Cow::Owned(decoded_hash),
})
}
}
impl<'a> Hash<'a> {
pub fn from_parts(
hash: &'a [u8],
salt: &'a [u8],
alg: Algorithm,
mem_cost_kib: u32,
iterations: u32,
threads: u32,
) -> Self {
Self {
alg,
mem_cost_kib,
iterations,
threads,
salt: Cow::Borrowed(salt),
hash: Cow::Borrowed(hash),
}
}
pub fn from_parts_owned(
hash: Vec<u8>,
salt: Vec<u8>,
alg: Algorithm,
mem_cost_kib: u32,
iterations: u32,
threads: u32,
) -> Self {
Self {
alg,
mem_cost_kib,
iterations,
threads,
salt: Cow::Owned(salt),
hash: Cow::Owned(hash),
}
}
pub fn as_bytes(&self) -> &[u8] {
self.hash.as_ref()
}
pub fn salt_bytes(&self) -> &[u8] {
self.salt.as_ref()
}
pub fn algorithm(&self) -> Algorithm {
self.alg
}
pub fn memory_cost_kib(&self) -> u32 {
self.mem_cost_kib
}
pub fn iterations(&self) -> u32 {
self.iterations
}
pub fn threads(&self) -> u32 {
self.threads
}
pub fn verify(&self, password: &[u8]) -> bool {
self.verify_with_or_without_secret(password, None)
}
pub fn verify_with_secret(&self, password: &[u8], secret: Secret) -> bool {
self.verify_with_or_without_secret(password, Some(secret))
}
#[inline]
fn verify_with_or_without_secret(&self, password: &[u8], secret: Option<Secret>) -> bool {
let hash_length: u32 = match self.hash.len().try_into() {
Ok(l) => l,
Err(_) => return false,
};
let mut hash_builder = Hasher::default()
.algorithm(self.alg)
.custom_salt(&self.salt)
.hash_length(hash_length)
.iterations(self.iterations)
.memory_cost_kib(self.mem_cost_kib)
.threads(self.threads);
if let Some(s) = secret {
hash_builder = hash_builder.secret(s);
}
let hashed_password = match hash_builder.hash(password) {
Ok(h) => h,
Err(_) => return false,
};
let mut hashes_dont_match = 0u8;
if self.hash.len() != hashed_password.hash.len() || self.hash.is_empty() {
return false;
}
for (i, hash_byte) in hashed_password.hash.iter().enumerate() {
unsafe {
hashes_dont_match |= hash_byte ^ self.hash.get_unchecked(i);
}
}
hashes_dont_match == 0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_byte_hash_into_hash_string() {
let hash = Hash {
alg: Algorithm::Argon2id,
mem_cost_kib: 128,
iterations: 3,
threads: 2,
salt: Cow::Borrowed(&[1, 2, 3, 4, 5, 6, 7, 8]),
hash: Cow::Owned(
b64_stdnopad
.decode("ypJ3pKxN4aWGkwMv0TOb08OIzwrfK1SZWy64vyTLKo8")
.unwrap()
.to_vec(),
),
};
assert_eq!(
hash.to_string(),
String::from(
"$argon2id$v=19$m=128,t=3,p=2$AQIDBAUGBwg$ypJ3pKxN4aWGkwMv0TOb08OIzwrfK1SZWy64vyTLKo8"
)
);
}
#[test]
fn test_hash_from_str() {
let hash = Hash::from_str(
"$argon2id$v=19$m=128,t=3,p=2$AQIDBAUGBwg$7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc",
)
.unwrap();
assert_eq!(hash.mem_cost_kib, 128);
assert_eq!(hash.iterations, 3);
assert_eq!(hash.threads, 2);
assert_eq!(hash.salt, b64_stdnopad.decode("AQIDBAUGBwg").unwrap());
assert_eq!(
hash.hash,
b64_stdnopad
.decode("7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc",)
.unwrap()
);
let hash = Hash::from_str(
"$argon2id$v=19$t=3,m=128,p=2$AQIDBAUGBwg$7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc",
)
.unwrap();
assert_eq!(hash.mem_cost_kib, 128);
assert_eq!(hash.iterations, 3);
assert_eq!(hash.threads, 2);
assert_eq!(hash.salt, b64_stdnopad.decode("AQIDBAUGBwg").unwrap());
assert_eq!(
hash.hash,
b64_stdnopad
.decode("7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc",)
.unwrap()
);
let hash = Hash::from_str(
"$argon2id$v=19$p=2,m=128,t=3$AQIDBAUGBwg$7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc",
)
.unwrap();
assert_eq!(hash.mem_cost_kib, 128);
assert_eq!(hash.iterations, 3);
assert_eq!(hash.threads, 2);
assert_eq!(hash.salt, b64_stdnopad.decode("AQIDBAUGBwg").unwrap());
assert_eq!(
hash.hash,
b64_stdnopad
.decode("7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc",)
.unwrap()
);
let hash = Hash::from_str(
"$argon2id$v=19$t=3,p=2,m=128$AQIDBAUGBwg$7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc",
)
.unwrap();
assert_eq!(hash.mem_cost_kib, 128);
assert_eq!(hash.iterations, 3);
assert_eq!(hash.threads, 2);
assert_eq!(hash.salt, b64_stdnopad.decode("AQIDBAUGBwg").unwrap());
assert_eq!(
hash.hash,
b64_stdnopad
.decode("7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc",)
.unwrap()
);
}
#[test]
fn test_invalid_hash_from_str() {
let hash = Hash::from_str(
"$argon2id$v=19$m=128,t=3,p=2,$AQIDBAUGBwg$7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc",
);
assert!(hash.is_err());
let hash = Hash::from_str(
"$argon2id$v=19$t=3,m=128,p=2,m=128$AQIDBAUGBwg$7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc"
);
assert!(hash.is_err());
let hash = Hash::from_str(
"$argon2i$v=19$p=2m=128,t=3$AQIDBAUGBwg$7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc",
);
assert!(hash.is_err());
let hash = Hash::from_str(
"$argon2id$v=19$p=2m=128,t=3$AQIDBAUGBwg$7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc",
);
assert!(hash.is_err());
let hash = Hash::from_str(
"$argon2id$t=3,p=2,m=128$AQIDBAUGBwg$7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc",
);
assert!(hash.is_err());
let hash = Hash::from_str(
"$argon2$v=19$m=128,t=3,p=2$AQIDBAUGBwg$7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc",
);
assert!(hash.is_err());
let hash = Hash::from_str(
"$argon2id$v=19$m=128,t=3,p=2AQIDBAUGBwg$7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc",
);
assert!(hash.is_err());
let hash = Hash::from_str(
"$argon2id$v=18$m=128,t=3,p=2$AQIDBAUGBwg$7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc",
);
assert!(hash.is_err());
let hash = Hash::from_str(
"argon2id$v=19$m=128,t=3,p=2$AQIDBAUGBwg$7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc",
);
assert!(hash.is_err());
let hash = Hash::from_str(
"$argon2id$v=19$m=128,t3,p=2$AQIDBAUGBwg$7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc",
);
assert!(hash.is_err());
let hash = Hash::from_str(
"$argon2id$v=19$m=128,t=3,p=2$AQIDBAUGBwg7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc",
);
assert!(hash.is_err());
let hash = Hash::from_str(
"$argon2id$v=19$m=128,t=3,p=2$AQIDBAUGBwg$7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc$",
);
assert!(hash.is_err());
let hash = Hash::from_str("$argon2id$v=19$m=128,t=3,p=2$AQIDBAUGBwg$$");
assert!(hash.is_err());
let hash = Hash::from_str(
"$argon2id$v=19$m=128,p=2$AQIDBAUGBwg$7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc",
);
assert!(hash.is_err());
let hash = Hash::from_str(
"$argon2id$v=19$t=2,p=2$AQIDBAUGBwg$7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc",
);
assert!(hash.is_err());
let hash = Hash::from_str(
"$argon2id$v=19$t=2,m=128$AQIDBAUGBwg$7OU7S/azjYpnXXySR52cFWeisxk1VVjNeXqtQ8ZM/Oc",
);
assert!(hash.is_err());
}
#[test]
fn test_hash_auth_string_argon2d() {
let auth_string = b"@Pa$$20rd-Test";
let key = [1u8; 32];
let hash_builder = Hasher::default()
.algorithm(Algorithm::Argon2d)
.salt_length(16)
.hash_length(32)
.iterations(1)
.memory_cost_kib(16)
.threads(1)
.secret((&key).into());
let hash = hash_builder.hash(auth_string).unwrap().to_string();
assert!(!Hash::from_str(&hash).unwrap().verify(auth_string));
assert!(Hash::from_str(&hash)
.unwrap()
.verify_with_secret(auth_string, (&key).into()));
}
#[test]
fn test_hash_auth_string_no_secret() {
let auth_string = b"@Pa$$20rd-Test";
let hash = Hasher::default()
.salt_length(16)
.hash_length(32)
.iterations(1)
.memory_cost_kib(16)
.threads(1)
.hash(auth_string)
.unwrap()
.to_string();
assert!(!Hash::from_str(&hash)
.unwrap()
.verify_with_secret(auth_string, (&[0, 1, 2, 3]).into()));
assert!(Hash::from_str(&hash).unwrap().verify(auth_string));
}
#[test]
fn test_hash_auth_string_argon2i() {
let auth_string = b"@Pa$$20rd-Test";
let key = [1u8; 32];
let hash_builder = Hasher::default()
.algorithm(Algorithm::Argon2i)
.salt_length(16)
.hash_length(32)
.iterations(1)
.memory_cost_kib(16)
.threads(1)
.secret((&key).into());
let hash = hash_builder.hash(auth_string).unwrap().to_string();
assert!(!Hash::from_str(&hash).unwrap().verify(auth_string));
assert!(Hash::from_str(&hash)
.unwrap()
.verify_with_secret(auth_string, (&key).into()));
}
#[test]
fn test_hash_auth_string_argon2id() {
let auth_string = b"@Pa$$20rd-Test";
let key = [1u8; 32];
let hash_builder = Hasher::new()
.algorithm(Algorithm::Argon2id)
.salt_length(16)
.hash_length(32)
.iterations(1)
.memory_cost_kib(16)
.threads(1)
.secret((&key).into());
let hash = hash_builder.hash(auth_string).unwrap().to_string();
assert!(!Hash::from_str(&hash).unwrap().verify(auth_string));
assert!(Hash::from_str(&hash)
.unwrap()
.verify_with_secret(auth_string, (&key).into()));
}
#[test]
fn test_get_fields() {
let auth_string = b"@Pa$$20rd-Test";
let salt = b"seasalts";
let hash_builder = Hasher::new()
.algorithm(Algorithm::Argon2d)
.custom_salt(salt)
.hash_length(32)
.iterations(1)
.memory_cost_kib(16)
.threads(1);
let hash = hash_builder.hash(auth_string).unwrap().to_string();
let hash = Hash::from_str(&hash).unwrap();
assert!(hash.verify(auth_string));
assert!(matches!(hash.algorithm(), Algorithm::Argon2d));
assert_eq!(hash.salt_bytes(), salt);
assert_eq!(hash.as_bytes().len(), 32);
assert_eq!(hash.iterations(), 1);
assert_eq!(hash.memory_cost_kib(), 16);
assert_eq!(hash.threads(), 1);
}
#[test]
fn test_custom_salt() {
let auth_string = b"@Pa$$20rd-Test";
let salt = b"seasalts";
let hash = Hasher::default()
.custom_salt(salt)
.hash(auth_string)
.unwrap();
assert_eq!(hash.salt.as_ref(), salt);
let hash_string = hash.to_string();
assert!(!Hash::from_str(&hash_string)
.unwrap()
.verify_with_secret(auth_string, (&[0, 1, 2, 3]).into()));
assert!(Hash::from_str(&hash_string).unwrap().verify(auth_string));
}
#[test]
fn test_verify_hash() {
let auth_string = b"@Pa$$20rd-Test";
let key = [0u8; 32];
let hash_builder = Hasher::default()
.salt_length(16)
.hash_length(32)
.iterations(1)
.memory_cost_kib(16)
.threads(1)
.secret((&key).into());
let hash = hash_builder.hash(auth_string).unwrap().to_string();
assert!(!Hash::from_str(&hash).unwrap().verify(auth_string));
assert!(Hash::from_str(&hash)
.unwrap()
.verify_with_secret(auth_string, (&key).into()));
}
#[test]
fn test_verify_incorrect_auth_string() {
let auth_string = b"@Pa$$20rd-Test";
let key = [0u8; 32];
let hash_builder = Hasher::default()
.salt_length(16)
.hash_length(32)
.iterations(1)
.memory_cost_kib(16)
.threads(1)
.secret((&key).into());
let hash = hash_builder.hash(auth_string).unwrap().to_string();
assert!(!Hash::from_str(&hash)
.unwrap()
.verify_with_secret(b"@Pa$$20rd-Tests", (&key).into()));
}
#[test]
fn test_verify_incorrect_key() {
let auth_string = b"@Pa$$20rd-Test";
let key = [0u8; 32];
let hash_builder = Hasher::default()
.salt_length(16)
.hash_length(32)
.iterations(1)
.memory_cost_kib(16)
.threads(1)
.secret((&key).into());
let hash = hash_builder.hash(auth_string).unwrap().to_string();
assert!(!Hash::from_str(&hash)
.unwrap()
.verify_with_secret(auth_string, (&[0u8; 33]).into()));
}
#[test]
fn test_from_parts() {
let hash = Hash::from_parts(
&[155, 147, 76, 205, 220, 49, 114, 102],
b"testsalt",
Algorithm::Argon2id,
16,
1,
1,
);
assert!(matches!(hash.algorithm(), Algorithm::Argon2id));
assert_eq!(hash.mem_cost_kib, 16);
assert_eq!(hash.iterations, 1);
assert_eq!(hash.threads, 1);
assert_eq!(hash.salt.as_ref(), b"testsalt");
assert_eq!(hash.hash.as_ref(), &[155, 147, 76, 205, 220, 49, 114, 102]);
assert!(hash.verify(b"password"));
}
#[test]
fn test_from_parts_owned() {
let hash = Hash::from_parts_owned(
vec![155, 147, 76, 205, 220, 49, 114, 102],
Vec::from(b"testsalt"),
Algorithm::Argon2id,
16,
1,
1,
);
assert!(matches!(hash.algorithm(), Algorithm::Argon2id));
assert_eq!(hash.mem_cost_kib, 16);
assert_eq!(hash.iterations, 1);
assert_eq!(hash.threads, 1);
assert_eq!(hash.salt.as_ref(), b"testsalt");
assert_eq!(hash.hash.as_ref(), &[155, 147, 76, 205, 220, 49, 114, 102]);
assert!(hash.verify(b"password"));
}
}