Skip to main content

shape_runtime/stdlib/
crypto.rs

1//! Native `crypto` module for hashing, encoding, and signing utilities.
2//!
3//! Exports: crypto.sha256, crypto.sha512, crypto.sha1, crypto.md5,
4//!          crypto.hmac_sha256, crypto.base64_encode, crypto.base64_decode,
5//!          crypto.hex_encode, crypto.hex_decode, crypto.random_bytes,
6//!          crypto.ed25519_generate_keypair, crypto.ed25519_sign, crypto.ed25519_verify
7
8use crate::module_exports::{ModuleContext, ModuleExports, ModuleFunction, ModuleParam};
9use shape_value::ValueWord;
10use std::sync::Arc;
11
12/// Create the `crypto` module with hashing and encoding functions.
13pub fn create_crypto_module() -> ModuleExports {
14    let mut module = ModuleExports::new("crypto");
15    module.description = "Cryptographic hashing and encoding utilities".to_string();
16
17    // crypto.sha256(data: string) -> string
18    module.add_function_with_schema(
19        "sha256",
20        |args: &[ValueWord], _ctx: &ModuleContext| {
21            use sha2::{Digest, Sha256};
22
23            let data = args
24                .first()
25                .and_then(|a| a.as_str())
26                .ok_or_else(|| "crypto.sha256() requires a string argument".to_string())?;
27
28            let mut hasher = Sha256::new();
29            hasher.update(data.as_bytes());
30            let result = hasher.finalize();
31            Ok(ValueWord::from_string(Arc::new(hex::encode(result))))
32        },
33        ModuleFunction {
34            description: "Compute the SHA-256 hash of a string, returning a hex-encoded digest"
35                .to_string(),
36            params: vec![ModuleParam {
37                name: "data".to_string(),
38                type_name: "string".to_string(),
39                required: true,
40                description: "Data to hash".to_string(),
41                ..Default::default()
42            }],
43            return_type: Some("string".to_string()),
44        },
45    );
46
47    // crypto.hmac_sha256(data: string, key: string) -> string
48    module.add_function_with_schema(
49        "hmac_sha256",
50        |args: &[ValueWord], _ctx: &ModuleContext| {
51            use hmac::{Hmac, Mac};
52            use sha2::Sha256;
53
54            let data = args.first().and_then(|a| a.as_str()).ok_or_else(|| {
55                "crypto.hmac_sha256() requires a data string argument".to_string()
56            })?;
57
58            let key = args
59                .get(1)
60                .and_then(|a| a.as_str())
61                .ok_or_else(|| "crypto.hmac_sha256() requires a key string argument".to_string())?;
62
63            type HmacSha256 = Hmac<Sha256>;
64            let mut mac = HmacSha256::new_from_slice(key.as_bytes())
65                .map_err(|e| format!("crypto.hmac_sha256() key error: {}", e))?;
66            mac.update(data.as_bytes());
67            let result = mac.finalize();
68            Ok(ValueWord::from_string(Arc::new(hex::encode(
69                result.into_bytes(),
70            ))))
71        },
72        ModuleFunction {
73            description: "Compute HMAC-SHA256 of data with the given key, returning hex digest"
74                .to_string(),
75            params: vec![
76                ModuleParam {
77                    name: "data".to_string(),
78                    type_name: "string".to_string(),
79                    required: true,
80                    description: "Data to authenticate".to_string(),
81                    ..Default::default()
82                },
83                ModuleParam {
84                    name: "key".to_string(),
85                    type_name: "string".to_string(),
86                    required: true,
87                    description: "HMAC key".to_string(),
88                    ..Default::default()
89                },
90            ],
91            return_type: Some("string".to_string()),
92        },
93    );
94
95    // crypto.base64_encode(data: string) -> string
96    module.add_function_with_schema(
97        "base64_encode",
98        |args: &[ValueWord], _ctx: &ModuleContext| {
99            use base64::Engine;
100
101            let data = args
102                .first()
103                .and_then(|a| a.as_str())
104                .ok_or_else(|| "crypto.base64_encode() requires a string argument".to_string())?;
105
106            let encoded = base64::engine::general_purpose::STANDARD.encode(data.as_bytes());
107            Ok(ValueWord::from_string(Arc::new(encoded)))
108        },
109        ModuleFunction {
110            description: "Encode a string to Base64".to_string(),
111            params: vec![ModuleParam {
112                name: "data".to_string(),
113                type_name: "string".to_string(),
114                required: true,
115                description: "Data to encode".to_string(),
116                ..Default::default()
117            }],
118            return_type: Some("string".to_string()),
119        },
120    );
121
122    // crypto.base64_decode(encoded: string) -> Result<string>
123    module.add_function_with_schema(
124        "base64_decode",
125        |args: &[ValueWord], _ctx: &ModuleContext| {
126            use base64::Engine;
127
128            let encoded = args
129                .first()
130                .and_then(|a| a.as_str())
131                .ok_or_else(|| "crypto.base64_decode() requires a string argument".to_string())?;
132
133            let bytes = base64::engine::general_purpose::STANDARD
134                .decode(encoded)
135                .map_err(|e| format!("crypto.base64_decode() failed: {}", e))?;
136
137            let decoded = String::from_utf8(bytes)
138                .map_err(|e| format!("crypto.base64_decode() invalid UTF-8: {}", e))?;
139
140            Ok(ValueWord::from_ok(ValueWord::from_string(Arc::new(
141                decoded,
142            ))))
143        },
144        ModuleFunction {
145            description: "Decode a Base64 string".to_string(),
146            params: vec![ModuleParam {
147                name: "encoded".to_string(),
148                type_name: "string".to_string(),
149                required: true,
150                description: "Base64-encoded string to decode".to_string(),
151                ..Default::default()
152            }],
153            return_type: Some("Result<string>".to_string()),
154        },
155    );
156
157    // crypto.hex_encode(data: string) -> string
158    module.add_function_with_schema(
159        "hex_encode",
160        |args: &[ValueWord], _ctx: &ModuleContext| {
161            let data = args
162                .first()
163                .and_then(|a| a.as_str())
164                .ok_or_else(|| "crypto.hex_encode() requires a string argument".to_string())?;
165
166            Ok(ValueWord::from_string(Arc::new(hex::encode(
167                data.as_bytes(),
168            ))))
169        },
170        ModuleFunction {
171            description: "Encode a string as hexadecimal".to_string(),
172            params: vec![ModuleParam {
173                name: "data".to_string(),
174                type_name: "string".to_string(),
175                required: true,
176                description: "Data to hex-encode".to_string(),
177                ..Default::default()
178            }],
179            return_type: Some("string".to_string()),
180        },
181    );
182
183    // crypto.hex_decode(hex: string) -> Result<string>
184    module.add_function_with_schema(
185        "hex_decode",
186        |args: &[ValueWord], _ctx: &ModuleContext| {
187            let hex_str = args
188                .first()
189                .and_then(|a| a.as_str())
190                .ok_or_else(|| "crypto.hex_decode() requires a string argument".to_string())?;
191
192            let bytes =
193                hex::decode(hex_str).map_err(|e| format!("crypto.hex_decode() failed: {}", e))?;
194
195            let decoded = String::from_utf8(bytes)
196                .map_err(|e| format!("crypto.hex_decode() invalid UTF-8: {}", e))?;
197
198            Ok(ValueWord::from_ok(ValueWord::from_string(Arc::new(
199                decoded,
200            ))))
201        },
202        ModuleFunction {
203            description: "Decode a hexadecimal string".to_string(),
204            params: vec![ModuleParam {
205                name: "hex".to_string(),
206                type_name: "string".to_string(),
207                required: true,
208                description: "Hex-encoded string to decode".to_string(),
209                ..Default::default()
210            }],
211            return_type: Some("Result<string>".to_string()),
212        },
213    );
214
215    // crypto.sha512(data: string) -> string
216    module.add_function_with_schema(
217        "sha512",
218        |args: &[ValueWord], _ctx: &ModuleContext| {
219            use sha2::{Digest, Sha512};
220
221            let data = args
222                .first()
223                .and_then(|a| a.as_str())
224                .ok_or_else(|| "crypto.sha512() requires a string argument".to_string())?;
225
226            let mut hasher = Sha512::new();
227            hasher.update(data.as_bytes());
228            let result = hasher.finalize();
229            Ok(ValueWord::from_string(Arc::new(hex::encode(result))))
230        },
231        ModuleFunction {
232            description: "Compute the SHA-512 hash of a string, returning a hex-encoded digest"
233                .to_string(),
234            params: vec![ModuleParam {
235                name: "data".to_string(),
236                type_name: "string".to_string(),
237                required: true,
238                description: "Data to hash".to_string(),
239                ..Default::default()
240            }],
241            return_type: Some("string".to_string()),
242        },
243    );
244
245    // crypto.sha1(data: string) -> string
246    module.add_function_with_schema(
247        "sha1",
248        |args: &[ValueWord], _ctx: &ModuleContext| {
249            use sha1::Digest;
250
251            let data = args
252                .first()
253                .and_then(|a| a.as_str())
254                .ok_or_else(|| "crypto.sha1() requires a string argument".to_string())?;
255
256            let mut hasher = sha1::Sha1::new();
257            hasher.update(data.as_bytes());
258            let result = hasher.finalize();
259            Ok(ValueWord::from_string(Arc::new(hex::encode(result))))
260        },
261        ModuleFunction {
262            description:
263                "Compute the SHA-1 hash of a string, returning a hex-encoded digest (legacy)"
264                    .to_string(),
265            params: vec![ModuleParam {
266                name: "data".to_string(),
267                type_name: "string".to_string(),
268                required: true,
269                description: "Data to hash".to_string(),
270                ..Default::default()
271            }],
272            return_type: Some("string".to_string()),
273        },
274    );
275
276    // crypto.md5(data: string) -> string
277    module.add_function_with_schema(
278        "md5",
279        |args: &[ValueWord], _ctx: &ModuleContext| {
280            use md5::Digest;
281
282            let data = args
283                .first()
284                .and_then(|a| a.as_str())
285                .ok_or_else(|| "crypto.md5() requires a string argument".to_string())?;
286
287            let mut hasher = md5::Md5::new();
288            hasher.update(data.as_bytes());
289            let result = hasher.finalize();
290            Ok(ValueWord::from_string(Arc::new(hex::encode(result))))
291        },
292        ModuleFunction {
293            description:
294                "Compute the MD5 hash of a string, returning a hex-encoded digest (legacy)"
295                    .to_string(),
296            params: vec![ModuleParam {
297                name: "data".to_string(),
298                type_name: "string".to_string(),
299                required: true,
300                description: "Data to hash".to_string(),
301                ..Default::default()
302            }],
303            return_type: Some("string".to_string()),
304        },
305    );
306
307    // crypto.random_bytes(n: int) -> string
308    module.add_function_with_schema(
309        "random_bytes",
310        |args: &[ValueWord], _ctx: &ModuleContext| {
311            use rand::RngCore;
312
313            let n = args
314                .first()
315                .and_then(|a| a.as_i64())
316                .ok_or_else(|| "crypto.random_bytes() requires an int argument".to_string())?;
317
318            if n < 0 || n > 65536 {
319                return Err(
320                    "crypto.random_bytes() n must be between 0 and 65536".to_string()
321                );
322            }
323
324            let mut buf = vec![0u8; n as usize];
325            rand::thread_rng().fill_bytes(&mut buf);
326            Ok(ValueWord::from_string(Arc::new(hex::encode(buf))))
327        },
328        ModuleFunction {
329            description: "Generate n random bytes, returned as a hex-encoded string".to_string(),
330            params: vec![ModuleParam {
331                name: "n".to_string(),
332                type_name: "int".to_string(),
333                required: true,
334                description: "Number of random bytes to generate (0..65536)".to_string(),
335                ..Default::default()
336            }],
337            return_type: Some("string".to_string()),
338        },
339    );
340
341    // crypto.ed25519_generate_keypair() -> object
342    module.add_function_with_schema(
343        "ed25519_generate_keypair",
344        |_args: &[ValueWord], _ctx: &ModuleContext| {
345            use rand::RngCore;
346
347            let mut secret = [0u8; 32];
348            rand::thread_rng().fill_bytes(&mut secret);
349            let signing_key = ed25519_dalek::SigningKey::from_bytes(&secret);
350            let verifying_key = signing_key.verifying_key();
351
352            let keys = vec![
353                ValueWord::from_string(Arc::new("public_key".to_string())),
354                ValueWord::from_string(Arc::new("secret_key".to_string())),
355            ];
356            let values = vec![
357                ValueWord::from_string(Arc::new(hex::encode(verifying_key.to_bytes()))),
358                ValueWord::from_string(Arc::new(hex::encode(signing_key.to_bytes()))),
359            ];
360            Ok(ValueWord::from_hashmap_pairs(keys, values))
361        },
362        ModuleFunction {
363            description: "Generate an Ed25519 keypair, returning an object with hex-encoded public_key and secret_key"
364                .to_string(),
365            params: vec![],
366            return_type: Some("object".to_string()),
367        },
368    );
369
370    // crypto.ed25519_sign(message: string, secret_key: string) -> string
371    module.add_function_with_schema(
372        "ed25519_sign",
373        |args: &[ValueWord], _ctx: &ModuleContext| {
374            use ed25519_dalek::Signer;
375
376            let message = args.first().and_then(|a| a.as_str()).ok_or_else(|| {
377                "crypto.ed25519_sign() requires a message string argument".to_string()
378            })?;
379
380            let secret_hex = args
381                .get(1)
382                .and_then(|a| a.as_str())
383                .ok_or_else(|| {
384                    "crypto.ed25519_sign() requires a secret_key hex string argument".to_string()
385                })?;
386
387            let secret_bytes = hex::decode(secret_hex)
388                .map_err(|e| format!("crypto.ed25519_sign() invalid secret_key hex: {}", e))?;
389
390            let secret_arr: [u8; 32] = secret_bytes.as_slice().try_into().map_err(|_| {
391                format!(
392                    "crypto.ed25519_sign() secret_key must be 32 bytes (got {})",
393                    secret_bytes.len()
394                )
395            })?;
396
397            let signing_key = ed25519_dalek::SigningKey::from_bytes(&secret_arr);
398            let signature = signing_key.sign(message.as_bytes());
399            Ok(ValueWord::from_string(Arc::new(hex::encode(
400                signature.to_bytes(),
401            ))))
402        },
403        ModuleFunction {
404            description: "Sign a message with an Ed25519 secret key, returning a hex-encoded signature"
405                .to_string(),
406            params: vec![
407                ModuleParam {
408                    name: "message".to_string(),
409                    type_name: "string".to_string(),
410                    required: true,
411                    description: "Message to sign".to_string(),
412                    ..Default::default()
413                },
414                ModuleParam {
415                    name: "secret_key".to_string(),
416                    type_name: "string".to_string(),
417                    required: true,
418                    description: "Hex-encoded 32-byte Ed25519 secret key".to_string(),
419                    ..Default::default()
420                },
421            ],
422            return_type: Some("string".to_string()),
423        },
424    );
425
426    // crypto.ed25519_verify(message: string, signature: string, public_key: string) -> bool
427    module.add_function_with_schema(
428        "ed25519_verify",
429        |args: &[ValueWord], _ctx: &ModuleContext| {
430            use ed25519_dalek::Verifier;
431
432            let message = args.first().and_then(|a| a.as_str()).ok_or_else(|| {
433                "crypto.ed25519_verify() requires a message string argument".to_string()
434            })?;
435
436            let sig_hex = args.get(1).and_then(|a| a.as_str()).ok_or_else(|| {
437                "crypto.ed25519_verify() requires a signature hex string argument".to_string()
438            })?;
439
440            let pub_hex = args.get(2).and_then(|a| a.as_str()).ok_or_else(|| {
441                "crypto.ed25519_verify() requires a public_key hex string argument".to_string()
442            })?;
443
444            let sig_bytes = hex::decode(sig_hex)
445                .map_err(|e| format!("crypto.ed25519_verify() invalid signature hex: {}", e))?;
446
447            let sig_arr: [u8; 64] = sig_bytes.as_slice().try_into().map_err(|_| {
448                format!(
449                    "crypto.ed25519_verify() signature must be 64 bytes (got {})",
450                    sig_bytes.len()
451                )
452            })?;
453
454            let pub_bytes = hex::decode(pub_hex)
455                .map_err(|e| format!("crypto.ed25519_verify() invalid public_key hex: {}", e))?;
456
457            let pub_arr: [u8; 32] = pub_bytes.as_slice().try_into().map_err(|_| {
458                format!(
459                    "crypto.ed25519_verify() public_key must be 32 bytes (got {})",
460                    pub_bytes.len()
461                )
462            })?;
463
464            let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&pub_arr)
465                .map_err(|e| format!("crypto.ed25519_verify() invalid public key: {}", e))?;
466
467            let signature = ed25519_dalek::Signature::from_bytes(&sig_arr);
468            let valid = verifying_key.verify(message.as_bytes(), &signature).is_ok();
469            Ok(ValueWord::from_bool(valid))
470        },
471        ModuleFunction {
472            description: "Verify an Ed25519 signature against a message and public key".to_string(),
473            params: vec![
474                ModuleParam {
475                    name: "message".to_string(),
476                    type_name: "string".to_string(),
477                    required: true,
478                    description: "Message that was signed".to_string(),
479                    ..Default::default()
480                },
481                ModuleParam {
482                    name: "signature".to_string(),
483                    type_name: "string".to_string(),
484                    required: true,
485                    description: "Hex-encoded 64-byte Ed25519 signature".to_string(),
486                    ..Default::default()
487                },
488                ModuleParam {
489                    name: "public_key".to_string(),
490                    type_name: "string".to_string(),
491                    required: true,
492                    description: "Hex-encoded 32-byte Ed25519 public key".to_string(),
493                    ..Default::default()
494                },
495            ],
496            return_type: Some("bool".to_string()),
497        },
498    );
499
500    module
501}
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506
507    fn test_ctx() -> crate::module_exports::ModuleContext<'static> {
508        let registry = Box::leak(Box::new(crate::type_schema::TypeSchemaRegistry::new()));
509        crate::module_exports::ModuleContext {
510            schemas: registry,
511            invoke_callable: None,
512            raw_invoker: None,
513            function_hashes: None,
514            vm_state: None,
515            granted_permissions: None,
516            scope_constraints: None,
517            set_pending_resume: None,
518            set_pending_frame_resume: None,
519        }
520    }
521
522    #[test]
523    fn test_crypto_module_creation() {
524        let module = create_crypto_module();
525        assert_eq!(module.name, "crypto");
526        assert!(module.has_export("sha256"));
527        assert!(module.has_export("hmac_sha256"));
528        assert!(module.has_export("base64_encode"));
529        assert!(module.has_export("base64_decode"));
530        assert!(module.has_export("hex_encode"));
531        assert!(module.has_export("hex_decode"));
532    }
533
534    #[test]
535    fn test_sha256_known_digest() {
536        let module = create_crypto_module();
537        let ctx = test_ctx();
538        let sha_fn = module.get_export("sha256").unwrap();
539        let result = sha_fn(
540            &[ValueWord::from_string(Arc::new("hello".to_string()))],
541            &ctx,
542        )
543        .unwrap();
544        // Known SHA-256 digest for "hello"
545        assert_eq!(
546            result.as_str(),
547            Some("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
548        );
549    }
550
551    #[test]
552    fn test_sha256_empty_string() {
553        let module = create_crypto_module();
554        let ctx = test_ctx();
555        let sha_fn = module.get_export("sha256").unwrap();
556        let result = sha_fn(&[ValueWord::from_string(Arc::new(String::new()))], &ctx).unwrap();
557        assert_eq!(
558            result.as_str(),
559            Some("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
560        );
561    }
562
563    #[test]
564    fn test_sha256_requires_string() {
565        let module = create_crypto_module();
566        let ctx = test_ctx();
567        let sha_fn = module.get_export("sha256").unwrap();
568        assert!(sha_fn(&[ValueWord::from_f64(42.0)], &ctx).is_err());
569    }
570
571    #[test]
572    fn test_hmac_sha256() {
573        let module = create_crypto_module();
574        let ctx = test_ctx();
575        let hmac_fn = module.get_export("hmac_sha256").unwrap();
576        let result = hmac_fn(
577            &[
578                ValueWord::from_string(Arc::new("hello".to_string())),
579                ValueWord::from_string(Arc::new("secret".to_string())),
580            ],
581            &ctx,
582        )
583        .unwrap();
584        // HMAC-SHA256("hello", "secret") is a known value
585        let digest = result.as_str().unwrap();
586        assert_eq!(digest.len(), 64); // 32 bytes = 64 hex chars
587    }
588
589    #[test]
590    fn test_hmac_sha256_requires_both_args() {
591        let module = create_crypto_module();
592        let ctx = test_ctx();
593        let hmac_fn = module.get_export("hmac_sha256").unwrap();
594        assert!(
595            hmac_fn(
596                &[ValueWord::from_string(Arc::new("data".to_string()))],
597                &ctx
598            )
599            .is_err()
600        );
601        assert!(hmac_fn(&[], &ctx).is_err());
602    }
603
604    #[test]
605    fn test_base64_roundtrip() {
606        let module = create_crypto_module();
607        let ctx = test_ctx();
608        let encode_fn = module.get_export("base64_encode").unwrap();
609        let decode_fn = module.get_export("base64_decode").unwrap();
610
611        let original = "Hello, World!";
612        let encoded = encode_fn(
613            &[ValueWord::from_string(Arc::new(original.to_string()))],
614            &ctx,
615        )
616        .unwrap();
617        assert_eq!(encoded.as_str(), Some("SGVsbG8sIFdvcmxkIQ=="));
618
619        let decoded = decode_fn(&[encoded], &ctx).unwrap();
620        let inner = decoded.as_ok_inner().expect("should be Ok");
621        assert_eq!(inner.as_str(), Some(original));
622    }
623
624    #[test]
625    fn test_base64_decode_invalid() {
626        let module = create_crypto_module();
627        let ctx = test_ctx();
628        let decode_fn = module.get_export("base64_decode").unwrap();
629        let result = decode_fn(&[ValueWord::from_string(Arc::new("!!!".to_string()))], &ctx);
630        assert!(result.is_err());
631    }
632
633    #[test]
634    fn test_hex_roundtrip() {
635        let module = create_crypto_module();
636        let ctx = test_ctx();
637        let encode_fn = module.get_export("hex_encode").unwrap();
638        let decode_fn = module.get_export("hex_decode").unwrap();
639
640        let original = "hello";
641        let encoded = encode_fn(
642            &[ValueWord::from_string(Arc::new(original.to_string()))],
643            &ctx,
644        )
645        .unwrap();
646        assert_eq!(encoded.as_str(), Some("68656c6c6f"));
647
648        let decoded = decode_fn(&[encoded], &ctx).unwrap();
649        let inner = decoded.as_ok_inner().expect("should be Ok");
650        assert_eq!(inner.as_str(), Some(original));
651    }
652
653    #[test]
654    fn test_hex_decode_invalid() {
655        let module = create_crypto_module();
656        let ctx = test_ctx();
657        let decode_fn = module.get_export("hex_decode").unwrap();
658        let result = decode_fn(
659            &[ValueWord::from_string(Arc::new("zzzz".to_string()))],
660            &ctx,
661        );
662        assert!(result.is_err());
663    }
664
665    #[test]
666    fn test_crypto_schemas() {
667        let module = create_crypto_module();
668
669        let sha_schema = module.get_schema("sha256").unwrap();
670        assert_eq!(sha_schema.params.len(), 1);
671        assert_eq!(sha_schema.return_type.as_deref(), Some("string"));
672
673        let hmac_schema = module.get_schema("hmac_sha256").unwrap();
674        assert_eq!(hmac_schema.params.len(), 2);
675        assert!(hmac_schema.params[0].required);
676        assert!(hmac_schema.params[1].required);
677
678        let b64d_schema = module.get_schema("base64_decode").unwrap();
679        assert_eq!(b64d_schema.return_type.as_deref(), Some("Result<string>"));
680    }
681
682    #[test]
683    fn test_crypto_module_has_new_exports() {
684        let module = create_crypto_module();
685        assert!(module.has_export("sha512"));
686        assert!(module.has_export("sha1"));
687        assert!(module.has_export("md5"));
688        assert!(module.has_export("random_bytes"));
689        assert!(module.has_export("ed25519_generate_keypair"));
690        assert!(module.has_export("ed25519_sign"));
691        assert!(module.has_export("ed25519_verify"));
692    }
693
694    #[test]
695    fn test_sha512_known_digest() {
696        let module = create_crypto_module();
697        let ctx = test_ctx();
698        let sha_fn = module.get_export("sha512").unwrap();
699        let result = sha_fn(
700            &[ValueWord::from_string(Arc::new("hello".to_string()))],
701            &ctx,
702        )
703        .unwrap();
704        // Known SHA-512 digest for "hello"
705        assert_eq!(
706            result.as_str(),
707            Some("9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043")
708        );
709    }
710
711    #[test]
712    fn test_sha512_empty_string() {
713        let module = create_crypto_module();
714        let ctx = test_ctx();
715        let sha_fn = module.get_export("sha512").unwrap();
716        let result = sha_fn(&[ValueWord::from_string(Arc::new(String::new()))], &ctx).unwrap();
717        // SHA-512 of empty string
718        assert_eq!(
719            result.as_str(),
720            Some("cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e")
721        );
722    }
723
724    #[test]
725    fn test_sha512_requires_string() {
726        let module = create_crypto_module();
727        let ctx = test_ctx();
728        let sha_fn = module.get_export("sha512").unwrap();
729        assert!(sha_fn(&[ValueWord::from_f64(42.0)], &ctx).is_err());
730    }
731
732    #[test]
733    fn test_sha1_known_digest() {
734        let module = create_crypto_module();
735        let ctx = test_ctx();
736        let sha_fn = module.get_export("sha1").unwrap();
737        let result = sha_fn(
738            &[ValueWord::from_string(Arc::new("hello".to_string()))],
739            &ctx,
740        )
741        .unwrap();
742        // Known SHA-1 digest for "hello"
743        assert_eq!(
744            result.as_str(),
745            Some("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d")
746        );
747    }
748
749    #[test]
750    fn test_sha1_empty_string() {
751        let module = create_crypto_module();
752        let ctx = test_ctx();
753        let sha_fn = module.get_export("sha1").unwrap();
754        let result = sha_fn(&[ValueWord::from_string(Arc::new(String::new()))], &ctx).unwrap();
755        assert_eq!(
756            result.as_str(),
757            Some("da39a3ee5e6b4b0d3255bfef95601890afd80709")
758        );
759    }
760
761    #[test]
762    fn test_sha1_requires_string() {
763        let module = create_crypto_module();
764        let ctx = test_ctx();
765        let sha_fn = module.get_export("sha1").unwrap();
766        assert!(sha_fn(&[ValueWord::from_f64(42.0)], &ctx).is_err());
767    }
768
769    #[test]
770    fn test_md5_known_digest() {
771        let module = create_crypto_module();
772        let ctx = test_ctx();
773        let md5_fn = module.get_export("md5").unwrap();
774        let result = md5_fn(
775            &[ValueWord::from_string(Arc::new("hello".to_string()))],
776            &ctx,
777        )
778        .unwrap();
779        // Known MD5 digest for "hello"
780        assert_eq!(
781            result.as_str(),
782            Some("5d41402abc4b2a76b9719d911017c592")
783        );
784    }
785
786    #[test]
787    fn test_md5_empty_string() {
788        let module = create_crypto_module();
789        let ctx = test_ctx();
790        let md5_fn = module.get_export("md5").unwrap();
791        let result = md5_fn(&[ValueWord::from_string(Arc::new(String::new()))], &ctx).unwrap();
792        assert_eq!(
793            result.as_str(),
794            Some("d41d8cd98f00b204e9800998ecf8427e")
795        );
796    }
797
798    #[test]
799    fn test_md5_requires_string() {
800        let module = create_crypto_module();
801        let ctx = test_ctx();
802        let md5_fn = module.get_export("md5").unwrap();
803        assert!(md5_fn(&[ValueWord::from_f64(42.0)], &ctx).is_err());
804    }
805
806    #[test]
807    fn test_random_bytes_length() {
808        let module = create_crypto_module();
809        let ctx = test_ctx();
810        let rb_fn = module.get_export("random_bytes").unwrap();
811        let result = rb_fn(&[ValueWord::from_i64(16)], &ctx).unwrap();
812        let hex_str = result.as_str().unwrap();
813        // 16 bytes = 32 hex chars
814        assert_eq!(hex_str.len(), 32);
815    }
816
817    #[test]
818    fn test_random_bytes_zero() {
819        let module = create_crypto_module();
820        let ctx = test_ctx();
821        let rb_fn = module.get_export("random_bytes").unwrap();
822        let result = rb_fn(&[ValueWord::from_i64(0)], &ctx).unwrap();
823        assert_eq!(result.as_str(), Some(""));
824    }
825
826    #[test]
827    fn test_random_bytes_negative_rejected() {
828        let module = create_crypto_module();
829        let ctx = test_ctx();
830        let rb_fn = module.get_export("random_bytes").unwrap();
831        assert!(rb_fn(&[ValueWord::from_i64(-1)], &ctx).is_err());
832    }
833
834    #[test]
835    fn test_random_bytes_too_large_rejected() {
836        let module = create_crypto_module();
837        let ctx = test_ctx();
838        let rb_fn = module.get_export("random_bytes").unwrap();
839        assert!(rb_fn(&[ValueWord::from_i64(65537)], &ctx).is_err());
840    }
841
842    #[test]
843    fn test_random_bytes_requires_int() {
844        let module = create_crypto_module();
845        let ctx = test_ctx();
846        let rb_fn = module.get_export("random_bytes").unwrap();
847        assert!(
848            rb_fn(
849                &[ValueWord::from_string(Arc::new("10".to_string()))],
850                &ctx
851            )
852            .is_err()
853        );
854    }
855
856    #[test]
857    fn test_ed25519_generate_keypair() {
858        let module = create_crypto_module();
859        let ctx = test_ctx();
860        let gen_fn = module.get_export("ed25519_generate_keypair").unwrap();
861        let result = gen_fn(&[], &ctx).unwrap();
862
863        // Result should be a HashMap with public_key and secret_key
864        let hm = result.as_hashmap_data().expect("should be a HashMap");
865        let pub_key = hm.shape_get("public_key").expect("should have public_key");
866        let sec_key = hm.shape_get("secret_key").expect("should have secret_key");
867
868        // 32 bytes = 64 hex chars
869        assert_eq!(pub_key.as_str().unwrap().len(), 64);
870        assert_eq!(sec_key.as_str().unwrap().len(), 64);
871    }
872
873    #[test]
874    fn test_ed25519_sign_and_verify_roundtrip() {
875        let module = create_crypto_module();
876        let ctx = test_ctx();
877
878        // Generate a keypair
879        let gen_fn = module.get_export("ed25519_generate_keypair").unwrap();
880        let keypair = gen_fn(&[], &ctx).unwrap();
881        let hm = keypair.as_hashmap_data().unwrap();
882
883        let pub_key = hm.shape_get("public_key").unwrap().clone();
884        let sec_key = hm.shape_get("secret_key").unwrap().clone();
885
886        let message = ValueWord::from_string(Arc::new("test message".to_string()));
887
888        // Sign
889        let sign_fn = module.get_export("ed25519_sign").unwrap();
890        let signature = sign_fn(&[message.clone(), sec_key], &ctx).unwrap();
891        // 64 bytes = 128 hex chars
892        assert_eq!(signature.as_str().unwrap().len(), 128);
893
894        // Verify — should succeed
895        let verify_fn = module.get_export("ed25519_verify").unwrap();
896        let valid = verify_fn(&[message, signature, pub_key], &ctx).unwrap();
897        assert_eq!(valid.as_bool(), Some(true));
898    }
899
900    #[test]
901    fn test_ed25519_verify_wrong_message() {
902        let module = create_crypto_module();
903        let ctx = test_ctx();
904
905        let gen_fn = module.get_export("ed25519_generate_keypair").unwrap();
906        let keypair = gen_fn(&[], &ctx).unwrap();
907        let hm = keypair.as_hashmap_data().unwrap();
908
909        let pub_key = hm.shape_get("public_key").unwrap().clone();
910        let sec_key = hm.shape_get("secret_key").unwrap().clone();
911
912        let message = ValueWord::from_string(Arc::new("correct message".to_string()));
913        let wrong_message = ValueWord::from_string(Arc::new("wrong message".to_string()));
914
915        let sign_fn = module.get_export("ed25519_sign").unwrap();
916        let signature = sign_fn(&[message, sec_key], &ctx).unwrap();
917
918        let verify_fn = module.get_export("ed25519_verify").unwrap();
919        let valid = verify_fn(&[wrong_message, signature, pub_key], &ctx).unwrap();
920        assert_eq!(valid.as_bool(), Some(false));
921    }
922
923    #[test]
924    fn test_ed25519_sign_invalid_secret_key() {
925        let module = create_crypto_module();
926        let ctx = test_ctx();
927        let sign_fn = module.get_export("ed25519_sign").unwrap();
928
929        // Too short
930        let result = sign_fn(
931            &[
932                ValueWord::from_string(Arc::new("msg".to_string())),
933                ValueWord::from_string(Arc::new("abcd".to_string())),
934            ],
935            &ctx,
936        );
937        assert!(result.is_err());
938
939        // Invalid hex
940        let result = sign_fn(
941            &[
942                ValueWord::from_string(Arc::new("msg".to_string())),
943                ValueWord::from_string(Arc::new("zzzz".to_string())),
944            ],
945            &ctx,
946        );
947        assert!(result.is_err());
948    }
949
950    #[test]
951    fn test_ed25519_verify_invalid_inputs() {
952        let module = create_crypto_module();
953        let ctx = test_ctx();
954        let verify_fn = module.get_export("ed25519_verify").unwrap();
955
956        // Missing arguments
957        assert!(
958            verify_fn(
959                &[ValueWord::from_string(Arc::new("msg".to_string()))],
960                &ctx
961            )
962            .is_err()
963        );
964
965        // Invalid hex in signature
966        assert!(
967            verify_fn(
968                &[
969                    ValueWord::from_string(Arc::new("msg".to_string())),
970                    ValueWord::from_string(Arc::new("not_hex".to_string())),
971                    ValueWord::from_string(Arc::new("ab".repeat(32))),
972                ],
973                &ctx
974            )
975            .is_err()
976        );
977    }
978
979    #[test]
980    fn test_new_function_schemas() {
981        let module = create_crypto_module();
982
983        let sha512_schema = module.get_schema("sha512").unwrap();
984        assert_eq!(sha512_schema.params.len(), 1);
985        assert_eq!(sha512_schema.return_type.as_deref(), Some("string"));
986
987        let sha1_schema = module.get_schema("sha1").unwrap();
988        assert_eq!(sha1_schema.params.len(), 1);
989        assert_eq!(sha1_schema.return_type.as_deref(), Some("string"));
990
991        let md5_schema = module.get_schema("md5").unwrap();
992        assert_eq!(md5_schema.params.len(), 1);
993        assert_eq!(md5_schema.return_type.as_deref(), Some("string"));
994
995        let rb_schema = module.get_schema("random_bytes").unwrap();
996        assert_eq!(rb_schema.params.len(), 1);
997        assert_eq!(rb_schema.params[0].type_name, "int");
998        assert_eq!(rb_schema.return_type.as_deref(), Some("string"));
999
1000        let gen_schema = module.get_schema("ed25519_generate_keypair").unwrap();
1001        assert_eq!(gen_schema.params.len(), 0);
1002        assert_eq!(gen_schema.return_type.as_deref(), Some("object"));
1003
1004        let sign_schema = module.get_schema("ed25519_sign").unwrap();
1005        assert_eq!(sign_schema.params.len(), 2);
1006        assert_eq!(sign_schema.return_type.as_deref(), Some("string"));
1007
1008        let verify_schema = module.get_schema("ed25519_verify").unwrap();
1009        assert_eq!(verify_schema.params.len(), 3);
1010        assert_eq!(verify_schema.return_type.as_deref(), Some("bool"));
1011    }
1012}