use crate::error::{Error, Result};
use crate::models::hash_algorithm::HashingAlgorithm;
use bcrypt::DEFAULT_COST;
use serde::{Deserialize, Serialize};
pub const BCRYPT_MAX_INPUT_BYTES: usize = 72;
#[derive(
Clone,
Copy,
Debug,
Default,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
Serialize,
Deserialize,
)]
pub enum PrehashAlgorithm {
#[default]
None,
Sha256,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct BcryptParams {
pub cost: u32,
pub prehash: PrehashAlgorithm,
}
impl Default for BcryptParams {
fn default() -> Self {
Self {
cost: DEFAULT_COST,
prehash: PrehashAlgorithm::None,
}
}
}
impl BcryptParams {
pub fn new(cost: u32) -> Self {
Self {
cost,
prehash: PrehashAlgorithm::None,
}
}
pub fn with_prehash(mut self, algo: PrehashAlgorithm) -> Self {
self.prehash = algo;
self
}
}
#[derive(
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
Serialize,
Deserialize,
)]
pub struct Bcrypt;
impl HashingAlgorithm for Bcrypt {
fn hash_password(password: &str, _salt: &str) -> Result<Vec<u8>> {
Self::hash_with(password, BcryptParams::default())
}
}
impl Bcrypt {
pub fn hash_with(
password: &str,
params: BcryptParams,
) -> Result<Vec<u8>> {
let payload =
prepare_payload(password.as_bytes(), params.prehash)?;
bcrypt::hash(&payload, params.cost)
.map(String::into_bytes)
.map_err(|e| {
Error::hashing(
crate::error::HashingErrorKind::Bcrypt,
e.to_string(),
)
})
}
pub fn verify(
password: &str,
stored: &str,
prehash: PrehashAlgorithm,
) -> Result<bool> {
let payload = prepare_payload(password.as_bytes(), prehash)?;
bcrypt::verify(&payload, stored).map_err(|_| {
Error::Verification("bcrypt verify failed".into())
})
}
}
fn prepare_payload(
password: &[u8],
prehash: PrehashAlgorithm,
) -> Result<Vec<u8>> {
match prehash {
PrehashAlgorithm::None => {
if password.len() > BCRYPT_MAX_INPUT_BYTES {
return Err(Error::InvalidPassword(
"bcrypt input exceeds 72 bytes; opt into a pre-hash via BcryptParams::with_prehash to handle longer inputs".into(),
));
}
Ok(password.to_vec())
}
PrehashAlgorithm::Sha256 => {
use sha2::{Digest, Sha256};
let digest = Sha256::digest(password);
use base64::{engine::general_purpose, Engine as _};
Ok(general_purpose::STANDARD_NO_PAD
.encode(digest)
.into_bytes())
}
}
}