#![deny(missing_docs)]
#![deny(unsafe_code)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::expect_used)]
use crate::errors::SecretError;
use crate::hashing::HashedValue;
use crate::hashing::errors::HashingOperation;
use crate::hashing::hashing_service::HashingService;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use webgates_core::verification_result::VerificationResult;
pub type Result<T, E = Box<dyn std::error::Error + Send + Sync>> = std::result::Result<T, E>;
pub mod errors;
pub mod hashing;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Secret {
pub account_id: Uuid,
pub secret: HashedValue,
}
impl Secret {
pub fn new<Hasher: HashingService>(
account_id: &Uuid,
plain_secret: &str,
hasher: Hasher,
) -> std::result::Result<Self, SecretError> {
let secret = hasher.hash_value(plain_secret).map_err(|e| {
SecretError::hashing_with_context(
HashingOperation::Hash,
e.to_string(),
Some("Argon2".to_string()),
Some("PHC".to_string()),
)
})?;
Ok(Self {
account_id: *account_id,
secret,
})
}
pub fn from_hashed(account_id: &Uuid, hashed_secret: &HashedValue) -> Self {
Self {
account_id: *account_id,
secret: hashed_secret.clone(),
}
}
pub fn verify<Hasher: HashingService>(
&self,
plain_secret: &str,
hasher: Hasher,
) -> std::result::Result<VerificationResult, SecretError> {
hasher
.verify_value(plain_secret, &self.secret)
.map_err(|error| {
SecretError::hashing_with_context(
HashingOperation::Verify,
error.to_string(),
Some("Argon2".to_string()),
Some("PHC".to_string()),
)
})
}
}
#[cfg(test)]
mod tests {
use super::Secret;
use crate::hashing::argon2::Argon2Hasher;
use uuid::Uuid;
use webgates_core::verification_result::VerificationResult;
#[test]
fn secret_verification_returns_unauthorized_for_wrong_secret() {
let id = Uuid::now_v7();
let correct_password = "admin_password";
let wrong_password = "admin_wrong_password";
let hasher = match Argon2Hasher::new_recommended() {
Ok(hasher) => hasher,
Err(error) => panic!(
"recommended Argon2 hasher should be constructible in tests: {}",
error
),
};
let secret = match Secret::new(&id, correct_password, hasher.clone()) {
Ok(secret) => secret,
Err(error) => panic!(
"secret construction should hash the provided test password: {}",
error
),
};
let verification = match secret.verify(wrong_password, hasher) {
Ok(verification) => verification,
Err(error) => panic!(
"verification should return an authorization result for a valid stored hash: {}",
error
),
};
assert_eq!(VerificationResult::Unauthorized, verification);
}
#[test]
fn secret_verification_returns_ok_for_matching_secret() {
let id = Uuid::now_v7();
let correct_password = "admin_password";
let hasher = match Argon2Hasher::new_recommended() {
Ok(hasher) => hasher,
Err(error) => panic!(
"recommended Argon2 hasher should be constructible in tests: {}",
error
),
};
let secret = match Secret::new(&id, correct_password, hasher.clone()) {
Ok(secret) => secret,
Err(error) => panic!(
"secret construction should hash the provided test password: {}",
error
),
};
let verification = match secret.verify(correct_password, hasher) {
Ok(verification) => verification,
Err(error) => panic!(
"verification should return success for the original plaintext secret: {}",
error
),
};
assert_eq!(VerificationResult::Ok, verification);
}
#[test]
fn from_hashed_preserves_account_id_and_hash() {
let id = Uuid::now_v7();
let hasher = match Argon2Hasher::new_recommended() {
Ok(hasher) => hasher,
Err(error) => panic!(
"recommended Argon2 hasher should be constructible in tests: {}",
error
),
};
let secret = match Secret::new(&id, "admin_password", hasher) {
Ok(secret) => secret,
Err(error) => panic!(
"secret construction should hash the provided test password: {}",
error
),
};
let reconstructed = Secret::from_hashed(&id, &secret.secret);
assert_eq!(reconstructed.account_id, id);
assert_eq!(reconstructed.secret, secret.secret);
}
}