1use crate::module_exports::{ModuleContext, ModuleExports, ModuleFunction, ModuleParam};
7use shape_value::ValueWord;
8use std::sync::Arc;
9
10pub fn create_crypto_module() -> ModuleExports {
12 let mut module = ModuleExports::new("crypto");
13 module.description = "Cryptographic hashing and encoding utilities".to_string();
14
15 module.add_function_with_schema(
17 "sha256",
18 |args: &[ValueWord], _ctx: &ModuleContext| {
19 use sha2::{Digest, Sha256};
20
21 let data = args
22 .first()
23 .and_then(|a| a.as_str())
24 .ok_or_else(|| "crypto.sha256() requires a string argument".to_string())?;
25
26 let mut hasher = Sha256::new();
27 hasher.update(data.as_bytes());
28 let result = hasher.finalize();
29 Ok(ValueWord::from_string(Arc::new(hex::encode(result))))
30 },
31 ModuleFunction {
32 description: "Compute the SHA-256 hash of a string, returning a hex-encoded digest"
33 .to_string(),
34 params: vec![ModuleParam {
35 name: "data".to_string(),
36 type_name: "string".to_string(),
37 required: true,
38 description: "Data to hash".to_string(),
39 ..Default::default()
40 }],
41 return_type: Some("string".to_string()),
42 },
43 );
44
45 module.add_function_with_schema(
47 "hmac_sha256",
48 |args: &[ValueWord], _ctx: &ModuleContext| {
49 use hmac::{Hmac, Mac};
50 use sha2::Sha256;
51
52 let data = args.first().and_then(|a| a.as_str()).ok_or_else(|| {
53 "crypto.hmac_sha256() requires a data string argument".to_string()
54 })?;
55
56 let key = args
57 .get(1)
58 .and_then(|a| a.as_str())
59 .ok_or_else(|| "crypto.hmac_sha256() requires a key string argument".to_string())?;
60
61 type HmacSha256 = Hmac<Sha256>;
62 let mut mac = HmacSha256::new_from_slice(key.as_bytes())
63 .map_err(|e| format!("crypto.hmac_sha256() key error: {}", e))?;
64 mac.update(data.as_bytes());
65 let result = mac.finalize();
66 Ok(ValueWord::from_string(Arc::new(hex::encode(
67 result.into_bytes(),
68 ))))
69 },
70 ModuleFunction {
71 description: "Compute HMAC-SHA256 of data with the given key, returning hex digest"
72 .to_string(),
73 params: vec![
74 ModuleParam {
75 name: "data".to_string(),
76 type_name: "string".to_string(),
77 required: true,
78 description: "Data to authenticate".to_string(),
79 ..Default::default()
80 },
81 ModuleParam {
82 name: "key".to_string(),
83 type_name: "string".to_string(),
84 required: true,
85 description: "HMAC key".to_string(),
86 ..Default::default()
87 },
88 ],
89 return_type: Some("string".to_string()),
90 },
91 );
92
93 module.add_function_with_schema(
95 "base64_encode",
96 |args: &[ValueWord], _ctx: &ModuleContext| {
97 use base64::Engine;
98
99 let data = args
100 .first()
101 .and_then(|a| a.as_str())
102 .ok_or_else(|| "crypto.base64_encode() requires a string argument".to_string())?;
103
104 let encoded = base64::engine::general_purpose::STANDARD.encode(data.as_bytes());
105 Ok(ValueWord::from_string(Arc::new(encoded)))
106 },
107 ModuleFunction {
108 description: "Encode a string to Base64".to_string(),
109 params: vec![ModuleParam {
110 name: "data".to_string(),
111 type_name: "string".to_string(),
112 required: true,
113 description: "Data to encode".to_string(),
114 ..Default::default()
115 }],
116 return_type: Some("string".to_string()),
117 },
118 );
119
120 module.add_function_with_schema(
122 "base64_decode",
123 |args: &[ValueWord], _ctx: &ModuleContext| {
124 use base64::Engine;
125
126 let encoded = args
127 .first()
128 .and_then(|a| a.as_str())
129 .ok_or_else(|| "crypto.base64_decode() requires a string argument".to_string())?;
130
131 let bytes = base64::engine::general_purpose::STANDARD
132 .decode(encoded)
133 .map_err(|e| format!("crypto.base64_decode() failed: {}", e))?;
134
135 let decoded = String::from_utf8(bytes)
136 .map_err(|e| format!("crypto.base64_decode() invalid UTF-8: {}", e))?;
137
138 Ok(ValueWord::from_ok(ValueWord::from_string(Arc::new(
139 decoded,
140 ))))
141 },
142 ModuleFunction {
143 description: "Decode a Base64 string".to_string(),
144 params: vec![ModuleParam {
145 name: "encoded".to_string(),
146 type_name: "string".to_string(),
147 required: true,
148 description: "Base64-encoded string to decode".to_string(),
149 ..Default::default()
150 }],
151 return_type: Some("Result<string>".to_string()),
152 },
153 );
154
155 module.add_function_with_schema(
157 "hex_encode",
158 |args: &[ValueWord], _ctx: &ModuleContext| {
159 let data = args
160 .first()
161 .and_then(|a| a.as_str())
162 .ok_or_else(|| "crypto.hex_encode() requires a string argument".to_string())?;
163
164 Ok(ValueWord::from_string(Arc::new(hex::encode(
165 data.as_bytes(),
166 ))))
167 },
168 ModuleFunction {
169 description: "Encode a string as hexadecimal".to_string(),
170 params: vec![ModuleParam {
171 name: "data".to_string(),
172 type_name: "string".to_string(),
173 required: true,
174 description: "Data to hex-encode".to_string(),
175 ..Default::default()
176 }],
177 return_type: Some("string".to_string()),
178 },
179 );
180
181 module.add_function_with_schema(
183 "hex_decode",
184 |args: &[ValueWord], _ctx: &ModuleContext| {
185 let hex_str = args
186 .first()
187 .and_then(|a| a.as_str())
188 .ok_or_else(|| "crypto.hex_decode() requires a string argument".to_string())?;
189
190 let bytes =
191 hex::decode(hex_str).map_err(|e| format!("crypto.hex_decode() failed: {}", e))?;
192
193 let decoded = String::from_utf8(bytes)
194 .map_err(|e| format!("crypto.hex_decode() invalid UTF-8: {}", e))?;
195
196 Ok(ValueWord::from_ok(ValueWord::from_string(Arc::new(
197 decoded,
198 ))))
199 },
200 ModuleFunction {
201 description: "Decode a hexadecimal string".to_string(),
202 params: vec![ModuleParam {
203 name: "hex".to_string(),
204 type_name: "string".to_string(),
205 required: true,
206 description: "Hex-encoded string to decode".to_string(),
207 ..Default::default()
208 }],
209 return_type: Some("Result<string>".to_string()),
210 },
211 );
212
213 module
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 fn test_ctx() -> crate::module_exports::ModuleContext<'static> {
221 let registry = Box::leak(Box::new(crate::type_schema::TypeSchemaRegistry::new()));
222 crate::module_exports::ModuleContext {
223 schemas: registry,
224 invoke_callable: None,
225 raw_invoker: None,
226 function_hashes: None,
227 vm_state: None,
228 granted_permissions: None,
229 scope_constraints: None,
230 set_pending_resume: None,
231 set_pending_frame_resume: None,
232 }
233 }
234
235 #[test]
236 fn test_crypto_module_creation() {
237 let module = create_crypto_module();
238 assert_eq!(module.name, "crypto");
239 assert!(module.has_export("sha256"));
240 assert!(module.has_export("hmac_sha256"));
241 assert!(module.has_export("base64_encode"));
242 assert!(module.has_export("base64_decode"));
243 assert!(module.has_export("hex_encode"));
244 assert!(module.has_export("hex_decode"));
245 }
246
247 #[test]
248 fn test_sha256_known_digest() {
249 let module = create_crypto_module();
250 let ctx = test_ctx();
251 let sha_fn = module.get_export("sha256").unwrap();
252 let result = sha_fn(
253 &[ValueWord::from_string(Arc::new("hello".to_string()))],
254 &ctx,
255 )
256 .unwrap();
257 assert_eq!(
259 result.as_str(),
260 Some("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
261 );
262 }
263
264 #[test]
265 fn test_sha256_empty_string() {
266 let module = create_crypto_module();
267 let ctx = test_ctx();
268 let sha_fn = module.get_export("sha256").unwrap();
269 let result = sha_fn(&[ValueWord::from_string(Arc::new(String::new()))], &ctx).unwrap();
270 assert_eq!(
271 result.as_str(),
272 Some("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
273 );
274 }
275
276 #[test]
277 fn test_sha256_requires_string() {
278 let module = create_crypto_module();
279 let ctx = test_ctx();
280 let sha_fn = module.get_export("sha256").unwrap();
281 assert!(sha_fn(&[ValueWord::from_f64(42.0)], &ctx).is_err());
282 }
283
284 #[test]
285 fn test_hmac_sha256() {
286 let module = create_crypto_module();
287 let ctx = test_ctx();
288 let hmac_fn = module.get_export("hmac_sha256").unwrap();
289 let result = hmac_fn(
290 &[
291 ValueWord::from_string(Arc::new("hello".to_string())),
292 ValueWord::from_string(Arc::new("secret".to_string())),
293 ],
294 &ctx,
295 )
296 .unwrap();
297 let digest = result.as_str().unwrap();
299 assert_eq!(digest.len(), 64); }
301
302 #[test]
303 fn test_hmac_sha256_requires_both_args() {
304 let module = create_crypto_module();
305 let ctx = test_ctx();
306 let hmac_fn = module.get_export("hmac_sha256").unwrap();
307 assert!(
308 hmac_fn(
309 &[ValueWord::from_string(Arc::new("data".to_string()))],
310 &ctx
311 )
312 .is_err()
313 );
314 assert!(hmac_fn(&[], &ctx).is_err());
315 }
316
317 #[test]
318 fn test_base64_roundtrip() {
319 let module = create_crypto_module();
320 let ctx = test_ctx();
321 let encode_fn = module.get_export("base64_encode").unwrap();
322 let decode_fn = module.get_export("base64_decode").unwrap();
323
324 let original = "Hello, World!";
325 let encoded = encode_fn(
326 &[ValueWord::from_string(Arc::new(original.to_string()))],
327 &ctx,
328 )
329 .unwrap();
330 assert_eq!(encoded.as_str(), Some("SGVsbG8sIFdvcmxkIQ=="));
331
332 let decoded = decode_fn(&[encoded], &ctx).unwrap();
333 let inner = decoded.as_ok_inner().expect("should be Ok");
334 assert_eq!(inner.as_str(), Some(original));
335 }
336
337 #[test]
338 fn test_base64_decode_invalid() {
339 let module = create_crypto_module();
340 let ctx = test_ctx();
341 let decode_fn = module.get_export("base64_decode").unwrap();
342 let result = decode_fn(&[ValueWord::from_string(Arc::new("!!!".to_string()))], &ctx);
343 assert!(result.is_err());
344 }
345
346 #[test]
347 fn test_hex_roundtrip() {
348 let module = create_crypto_module();
349 let ctx = test_ctx();
350 let encode_fn = module.get_export("hex_encode").unwrap();
351 let decode_fn = module.get_export("hex_decode").unwrap();
352
353 let original = "hello";
354 let encoded = encode_fn(
355 &[ValueWord::from_string(Arc::new(original.to_string()))],
356 &ctx,
357 )
358 .unwrap();
359 assert_eq!(encoded.as_str(), Some("68656c6c6f"));
360
361 let decoded = decode_fn(&[encoded], &ctx).unwrap();
362 let inner = decoded.as_ok_inner().expect("should be Ok");
363 assert_eq!(inner.as_str(), Some(original));
364 }
365
366 #[test]
367 fn test_hex_decode_invalid() {
368 let module = create_crypto_module();
369 let ctx = test_ctx();
370 let decode_fn = module.get_export("hex_decode").unwrap();
371 let result = decode_fn(
372 &[ValueWord::from_string(Arc::new("zzzz".to_string()))],
373 &ctx,
374 );
375 assert!(result.is_err());
376 }
377
378 #[test]
379 fn test_crypto_schemas() {
380 let module = create_crypto_module();
381
382 let sha_schema = module.get_schema("sha256").unwrap();
383 assert_eq!(sha_schema.params.len(), 1);
384 assert_eq!(sha_schema.return_type.as_deref(), Some("string"));
385
386 let hmac_schema = module.get_schema("hmac_sha256").unwrap();
387 assert_eq!(hmac_schema.params.len(), 2);
388 assert!(hmac_schema.params[0].required);
389 assert!(hmac_schema.params[1].required);
390
391 let b64d_schema = module.get_schema("base64_decode").unwrap();
392 assert_eq!(b64d_schema.return_type.as_deref(), Some("Result<string>"));
393 }
394}