use super::{SeaError, WorkOptions};
use base64::prelude::*;
use pbkdf2::pbkdf2_hmac;
use rand::RngCore;
use sha2::{Digest, Sha256};
use std::sync::Arc;
pub async fn work(data: &[u8], salt: Option<&[u8]>, opts: WorkOptions) -> Result<String, SeaError> {
let opts = Arc::new(opts);
let data = data.to_vec();
let name_lower = opts
.name
.as_ref()
.map(|n| n.to_lowercase())
.unwrap_or_else(|| "pbkdf2".to_string());
if name_lower.starts_with("sha") {
return tokio::task::spawn_blocking(move || {
let mut hasher = Sha256::new();
hasher.update(&data);
let hash = hasher.finalize();
let encoded = BASE64_URL_SAFE_NO_PAD.encode(&hash[..]);
Ok(encoded)
})
.await
.map_err(|e| SeaError::Crypto(format!("task join error: {}", e)))?;
}
let salt = if let Some(s) = salt {
s.to_vec()
} else if let Some(ref opt_salt) = opts.salt {
opt_salt.clone()
} else {
let mut salt_bytes = vec![0u8; 9];
rand::rng().fill_bytes(&mut salt_bytes);
salt_bytes
};
let iterations = opts.iterations.unwrap_or(100_000);
let length_bits = opts.length.unwrap_or(512);
let length_bytes = length_bits / 8;
let result = tokio::task::spawn_blocking(move || {
let mut output = vec![0u8; length_bytes];
pbkdf2_hmac::<Sha256>(&data, &salt, iterations, &mut output);
output
})
.await
.map_err(|e| SeaError::Crypto(format!("task join error: {}", e)))?;
let encoded = BASE64_URL_SAFE_NO_PAD.encode(&result);
Ok(encoded)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_work_pbkdf2_default() {
let result = work(b"password", None, WorkOptions::default())
.await
.unwrap();
assert!(!result.is_empty());
assert_eq!(result.len(), 86);
}
#[tokio::test]
async fn test_work_sha256_mode() {
let opts = WorkOptions {
name: Some("SHA-256".to_string()),
..Default::default()
};
let result = work(b"data", None, opts).await.unwrap();
assert_eq!(result.len(), 43);
}
#[tokio::test]
async fn test_work_sha256_deterministic() {
let opts = WorkOptions {
name: Some("sha".to_string()),
..Default::default()
};
let a = work(b"same input", None, opts.clone()).await.unwrap();
let b = work(b"same input", None, opts).await.unwrap();
assert_eq!(a, b, "SHA-256 should be deterministic for same input");
}
#[tokio::test]
async fn test_work_pbkdf2_different_salt_different_output() {
let opts = WorkOptions::default();
let a = work(b"password", Some(b"salt_a"), opts.clone())
.await
.unwrap();
let b = work(b"password", Some(b"salt_b"), opts).await.unwrap();
assert_ne!(a, b, "different salts should produce different outputs");
}
#[tokio::test]
async fn test_work_pbkdf2_same_salt_same_output() {
let opts = WorkOptions::default();
let a = work(b"password", Some(b"same_salt"), opts.clone())
.await
.unwrap();
let b = work(b"password", Some(b"same_salt"), opts).await.unwrap();
assert_eq!(a, b, "same salt should produce same output");
}
#[tokio::test]
async fn test_work_custom_iterations() {
let opts = WorkOptions {
iterations: Some(100),
..Default::default()
};
let result = work(b"password", Some(b"salt"), opts).await.unwrap();
assert!(!result.is_empty());
}
#[tokio::test]
async fn test_work_empty_data() {
let result = work(b"", Some(b"salt"), WorkOptions::default())
.await
.unwrap();
assert!(!result.is_empty());
}
#[tokio::test]
async fn test_work_sha256_known_value() {
let opts = WorkOptions {
name: Some("sha".to_string()),
..Default::default()
};
let result = work(b"", None, opts).await.unwrap();
let expected = "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU";
assert_eq!(result, expected);
}
}