1use super::{SeaError, WorkOptions};
28use base64::prelude::*;
29use pbkdf2::pbkdf2_hmac;
30use rand::RngCore;
31use sha2::{Digest, Sha256};
32use std::sync::Arc;
33
34pub async fn work(data: &[u8], salt: Option<&[u8]>, opts: WorkOptions) -> Result<String, SeaError> {
60 let opts = Arc::new(opts);
61 let data = data.to_vec();
62
63 let name_lower = opts
65 .name
66 .as_ref()
67 .map(|n| n.to_lowercase())
68 .unwrap_or_else(|| "pbkdf2".to_string());
69
70 if name_lower.starts_with("sha") {
71 return tokio::task::spawn_blocking(move || {
73 let mut hasher = Sha256::new();
74 hasher.update(&data);
75 let hash = hasher.finalize();
76 let encoded = BASE64_URL_SAFE_NO_PAD.encode(&hash[..]);
77 Ok(encoded)
78 })
79 .await
80 .map_err(|e| SeaError::Crypto(format!("task join error: {}", e)))?;
81 }
82
83 let salt = if let Some(s) = salt {
85 s.to_vec()
86 } else if let Some(ref opt_salt) = opts.salt {
87 opt_salt.clone()
88 } else {
89 let mut salt_bytes = vec![0u8; 9];
91 rand::rng().fill_bytes(&mut salt_bytes);
92 salt_bytes
93 };
94
95 let iterations = opts.iterations.unwrap_or(100_000);
96 let length_bits = opts.length.unwrap_or(512);
97 let length_bytes = length_bits / 8;
98
99 let result = tokio::task::spawn_blocking(move || {
101 let mut output = vec![0u8; length_bytes];
102 pbkdf2_hmac::<Sha256>(&data, &salt, iterations, &mut output);
103 output
104 })
105 .await
106 .map_err(|e| SeaError::Crypto(format!("task join error: {}", e)))?;
107
108 let encoded = BASE64_URL_SAFE_NO_PAD.encode(&result);
110 Ok(encoded)
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[tokio::test]
118 async fn test_work_pbkdf2_default() {
119 let result = work(b"password", None, WorkOptions::default())
120 .await
121 .unwrap();
122 assert!(!result.is_empty());
123 assert_eq!(result.len(), 86);
125 }
126
127 #[tokio::test]
128 async fn test_work_sha256_mode() {
129 let opts = WorkOptions {
130 name: Some("SHA-256".to_string()),
131 ..Default::default()
132 };
133 let result = work(b"data", None, opts).await.unwrap();
134 assert_eq!(result.len(), 43);
136 }
137
138 #[tokio::test]
139 async fn test_work_sha256_deterministic() {
140 let opts = WorkOptions {
141 name: Some("sha".to_string()),
142 ..Default::default()
143 };
144 let a = work(b"same input", None, opts.clone()).await.unwrap();
145 let b = work(b"same input", None, opts).await.unwrap();
146 assert_eq!(a, b, "SHA-256 should be deterministic for same input");
147 }
148
149 #[tokio::test]
150 async fn test_work_pbkdf2_different_salt_different_output() {
151 let opts = WorkOptions::default();
152 let a = work(b"password", Some(b"salt_a"), opts.clone())
153 .await
154 .unwrap();
155 let b = work(b"password", Some(b"salt_b"), opts).await.unwrap();
156 assert_ne!(a, b, "different salts should produce different outputs");
157 }
158
159 #[tokio::test]
160 async fn test_work_pbkdf2_same_salt_same_output() {
161 let opts = WorkOptions::default();
162 let a = work(b"password", Some(b"same_salt"), opts.clone())
163 .await
164 .unwrap();
165 let b = work(b"password", Some(b"same_salt"), opts).await.unwrap();
166 assert_eq!(a, b, "same salt should produce same output");
167 }
168
169 #[tokio::test]
170 async fn test_work_custom_iterations() {
171 let opts = WorkOptions {
172 iterations: Some(100),
173 ..Default::default()
174 };
175 let result = work(b"password", Some(b"salt"), opts).await.unwrap();
176 assert!(!result.is_empty());
177 }
178
179 #[tokio::test]
180 async fn test_work_empty_data() {
181 let result = work(b"", Some(b"salt"), WorkOptions::default())
182 .await
183 .unwrap();
184 assert!(!result.is_empty());
185 }
186
187 #[tokio::test]
188 async fn test_work_sha256_known_value() {
189 let opts = WorkOptions {
191 name: Some("sha".to_string()),
192 ..Default::default()
193 };
194 let result = work(b"", None, opts).await.unwrap();
195 let expected = "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU";
197 assert_eq!(result, expected);
198 }
199}