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