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("std::core::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            crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Random)?;
312            use rand::RngCore;
313
314            let n = args
315                .first()
316                .and_then(|a| a.as_i64())
317                .ok_or_else(|| "crypto.random_bytes() requires an int argument".to_string())?;
318
319            if n < 0 || n > 65536 {
320                return Err("crypto.random_bytes() n must be between 0 and 65536".to_string());
321            }
322
323            let mut buf = vec![0u8; n as usize];
324            rand::thread_rng().fill_bytes(&mut buf);
325            Ok(ValueWord::from_string(Arc::new(hex::encode(buf))))
326        },
327        ModuleFunction {
328            description: "Generate n random bytes, returned as a hex-encoded string".to_string(),
329            params: vec![ModuleParam {
330                name: "n".to_string(),
331                type_name: "int".to_string(),
332                required: true,
333                description: "Number of random bytes to generate (0..65536)".to_string(),
334                ..Default::default()
335            }],
336            return_type: Some("string".to_string()),
337        },
338    );
339
340    // crypto.ed25519_generate_keypair() -> object
341    module.add_function_with_schema(
342        "ed25519_generate_keypair",
343        |_args: &[ValueWord], ctx: &ModuleContext| {
344            crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Random)?;
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.get(1).and_then(|a| a.as_str()).ok_or_else(|| {
381                "crypto.ed25519_sign() requires a secret_key hex string argument".to_string()
382            })?;
383
384            let secret_bytes = hex::decode(secret_hex)
385                .map_err(|e| format!("crypto.ed25519_sign() invalid secret_key hex: {}", e))?;
386
387            let secret_arr: [u8; 32] = secret_bytes.as_slice().try_into().map_err(|_| {
388                format!(
389                    "crypto.ed25519_sign() secret_key must be 32 bytes (got {})",
390                    secret_bytes.len()
391                )
392            })?;
393
394            let signing_key = ed25519_dalek::SigningKey::from_bytes(&secret_arr);
395            let signature = signing_key.sign(message.as_bytes());
396            Ok(ValueWord::from_string(Arc::new(hex::encode(
397                signature.to_bytes(),
398            ))))
399        },
400        ModuleFunction {
401            description:
402                "Sign a message with an Ed25519 secret key, returning a hex-encoded signature"
403                    .to_string(),
404            params: vec![
405                ModuleParam {
406                    name: "message".to_string(),
407                    type_name: "string".to_string(),
408                    required: true,
409                    description: "Message to sign".to_string(),
410                    ..Default::default()
411                },
412                ModuleParam {
413                    name: "secret_key".to_string(),
414                    type_name: "string".to_string(),
415                    required: true,
416                    description: "Hex-encoded 32-byte Ed25519 secret key".to_string(),
417                    ..Default::default()
418                },
419            ],
420            return_type: Some("string".to_string()),
421        },
422    );
423
424    // crypto.ed25519_verify(message: string, signature: string, public_key: string) -> bool
425    module.add_function_with_schema(
426        "ed25519_verify",
427        |args: &[ValueWord], _ctx: &ModuleContext| {
428            use ed25519_dalek::Verifier;
429
430            let message = args.first().and_then(|a| a.as_str()).ok_or_else(|| {
431                "crypto.ed25519_verify() requires a message string argument".to_string()
432            })?;
433
434            let sig_hex = args.get(1).and_then(|a| a.as_str()).ok_or_else(|| {
435                "crypto.ed25519_verify() requires a signature hex string argument".to_string()
436            })?;
437
438            let pub_hex = args.get(2).and_then(|a| a.as_str()).ok_or_else(|| {
439                "crypto.ed25519_verify() requires a public_key hex string argument".to_string()
440            })?;
441
442            let sig_bytes = hex::decode(sig_hex)
443                .map_err(|e| format!("crypto.ed25519_verify() invalid signature hex: {}", e))?;
444
445            let sig_arr: [u8; 64] = sig_bytes.as_slice().try_into().map_err(|_| {
446                format!(
447                    "crypto.ed25519_verify() signature must be 64 bytes (got {})",
448                    sig_bytes.len()
449                )
450            })?;
451
452            let pub_bytes = hex::decode(pub_hex)
453                .map_err(|e| format!("crypto.ed25519_verify() invalid public_key hex: {}", e))?;
454
455            let pub_arr: [u8; 32] = pub_bytes.as_slice().try_into().map_err(|_| {
456                format!(
457                    "crypto.ed25519_verify() public_key must be 32 bytes (got {})",
458                    pub_bytes.len()
459                )
460            })?;
461
462            let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&pub_arr)
463                .map_err(|e| format!("crypto.ed25519_verify() invalid public key: {}", e))?;
464
465            let signature = ed25519_dalek::Signature::from_bytes(&sig_arr);
466            let valid = verifying_key.verify(message.as_bytes(), &signature).is_ok();
467            Ok(ValueWord::from_bool(valid))
468        },
469        ModuleFunction {
470            description: "Verify an Ed25519 signature against a message and public key".to_string(),
471            params: vec![
472                ModuleParam {
473                    name: "message".to_string(),
474                    type_name: "string".to_string(),
475                    required: true,
476                    description: "Message that was signed".to_string(),
477                    ..Default::default()
478                },
479                ModuleParam {
480                    name: "signature".to_string(),
481                    type_name: "string".to_string(),
482                    required: true,
483                    description: "Hex-encoded 64-byte Ed25519 signature".to_string(),
484                    ..Default::default()
485                },
486                ModuleParam {
487                    name: "public_key".to_string(),
488                    type_name: "string".to_string(),
489                    required: true,
490                    description: "Hex-encoded 32-byte Ed25519 public key".to_string(),
491                    ..Default::default()
492                },
493            ],
494            return_type: Some("bool".to_string()),
495        },
496    );
497
498    module
499}
500
501#[cfg(test)]
502mod tests {
503    use super::*;
504
505    fn test_ctx() -> crate::module_exports::ModuleContext<'static> {
506        let registry = Box::leak(Box::new(crate::type_schema::TypeSchemaRegistry::new()));
507        crate::module_exports::ModuleContext {
508            schemas: registry,
509            invoke_callable: None,
510            raw_invoker: None,
511            function_hashes: None,
512            vm_state: None,
513            granted_permissions: None,
514            scope_constraints: None,
515            set_pending_resume: None,
516            set_pending_frame_resume: None,
517        }
518    }
519
520    #[test]
521    fn test_crypto_module_creation() {
522        let module = create_crypto_module();
523        assert_eq!(module.name, "std::core::crypto");
524        assert!(module.has_export("sha256"));
525        assert!(module.has_export("hmac_sha256"));
526        assert!(module.has_export("base64_encode"));
527        assert!(module.has_export("base64_decode"));
528        assert!(module.has_export("hex_encode"));
529        assert!(module.has_export("hex_decode"));
530    }
531
532    #[test]
533    fn test_sha256_known_digest() {
534        let module = create_crypto_module();
535        let ctx = test_ctx();
536        let sha_fn = module.get_export("sha256").unwrap();
537        let result = sha_fn(
538            &[ValueWord::from_string(Arc::new("hello".to_string()))],
539            &ctx,
540        )
541        .unwrap();
542        // Known SHA-256 digest for "hello"
543        assert_eq!(
544            result.as_str(),
545            Some("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
546        );
547    }
548
549    #[test]
550    fn test_sha256_empty_string() {
551        let module = create_crypto_module();
552        let ctx = test_ctx();
553        let sha_fn = module.get_export("sha256").unwrap();
554        let result = sha_fn(&[ValueWord::from_string(Arc::new(String::new()))], &ctx).unwrap();
555        assert_eq!(
556            result.as_str(),
557            Some("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
558        );
559    }
560
561    #[test]
562    fn test_sha256_requires_string() {
563        let module = create_crypto_module();
564        let ctx = test_ctx();
565        let sha_fn = module.get_export("sha256").unwrap();
566        assert!(sha_fn(&[ValueWord::from_f64(42.0)], &ctx).is_err());
567    }
568
569    #[test]
570    fn test_hmac_sha256() {
571        let module = create_crypto_module();
572        let ctx = test_ctx();
573        let hmac_fn = module.get_export("hmac_sha256").unwrap();
574        let result = hmac_fn(
575            &[
576                ValueWord::from_string(Arc::new("hello".to_string())),
577                ValueWord::from_string(Arc::new("secret".to_string())),
578            ],
579            &ctx,
580        )
581        .unwrap();
582        // HMAC-SHA256("hello", "secret") is a known value
583        let digest = result.as_str().unwrap();
584        assert_eq!(digest.len(), 64); // 32 bytes = 64 hex chars
585    }
586
587    #[test]
588    fn test_hmac_sha256_requires_both_args() {
589        let module = create_crypto_module();
590        let ctx = test_ctx();
591        let hmac_fn = module.get_export("hmac_sha256").unwrap();
592        assert!(
593            hmac_fn(
594                &[ValueWord::from_string(Arc::new("data".to_string()))],
595                &ctx
596            )
597            .is_err()
598        );
599        assert!(hmac_fn(&[], &ctx).is_err());
600    }
601
602    #[test]
603    fn test_base64_roundtrip() {
604        let module = create_crypto_module();
605        let ctx = test_ctx();
606        let encode_fn = module.get_export("base64_encode").unwrap();
607        let decode_fn = module.get_export("base64_decode").unwrap();
608
609        let original = "Hello, World!";
610        let encoded = encode_fn(
611            &[ValueWord::from_string(Arc::new(original.to_string()))],
612            &ctx,
613        )
614        .unwrap();
615        assert_eq!(encoded.as_str(), Some("SGVsbG8sIFdvcmxkIQ=="));
616
617        let decoded = decode_fn(&[encoded], &ctx).unwrap();
618        let inner = decoded.as_ok_inner().expect("should be Ok");
619        assert_eq!(inner.as_str(), Some(original));
620    }
621
622    #[test]
623    fn test_base64_decode_invalid() {
624        let module = create_crypto_module();
625        let ctx = test_ctx();
626        let decode_fn = module.get_export("base64_decode").unwrap();
627        let result = decode_fn(&[ValueWord::from_string(Arc::new("!!!".to_string()))], &ctx);
628        assert!(result.is_err());
629    }
630
631    #[test]
632    fn test_hex_roundtrip() {
633        let module = create_crypto_module();
634        let ctx = test_ctx();
635        let encode_fn = module.get_export("hex_encode").unwrap();
636        let decode_fn = module.get_export("hex_decode").unwrap();
637
638        let original = "hello";
639        let encoded = encode_fn(
640            &[ValueWord::from_string(Arc::new(original.to_string()))],
641            &ctx,
642        )
643        .unwrap();
644        assert_eq!(encoded.as_str(), Some("68656c6c6f"));
645
646        let decoded = decode_fn(&[encoded], &ctx).unwrap();
647        let inner = decoded.as_ok_inner().expect("should be Ok");
648        assert_eq!(inner.as_str(), Some(original));
649    }
650
651    #[test]
652    fn test_hex_decode_invalid() {
653        let module = create_crypto_module();
654        let ctx = test_ctx();
655        let decode_fn = module.get_export("hex_decode").unwrap();
656        let result = decode_fn(
657            &[ValueWord::from_string(Arc::new("zzzz".to_string()))],
658            &ctx,
659        );
660        assert!(result.is_err());
661    }
662
663    #[test]
664    fn test_crypto_schemas() {
665        let module = create_crypto_module();
666
667        let sha_schema = module.get_schema("sha256").unwrap();
668        assert_eq!(sha_schema.params.len(), 1);
669        assert_eq!(sha_schema.return_type.as_deref(), Some("string"));
670
671        let hmac_schema = module.get_schema("hmac_sha256").unwrap();
672        assert_eq!(hmac_schema.params.len(), 2);
673        assert!(hmac_schema.params[0].required);
674        assert!(hmac_schema.params[1].required);
675
676        let b64d_schema = module.get_schema("base64_decode").unwrap();
677        assert_eq!(b64d_schema.return_type.as_deref(), Some("Result<string>"));
678    }
679
680    #[test]
681    fn test_crypto_module_has_new_exports() {
682        let module = create_crypto_module();
683        assert!(module.has_export("sha512"));
684        assert!(module.has_export("sha1"));
685        assert!(module.has_export("md5"));
686        assert!(module.has_export("random_bytes"));
687        assert!(module.has_export("ed25519_generate_keypair"));
688        assert!(module.has_export("ed25519_sign"));
689        assert!(module.has_export("ed25519_verify"));
690    }
691
692    #[test]
693    fn test_sha512_known_digest() {
694        let module = create_crypto_module();
695        let ctx = test_ctx();
696        let sha_fn = module.get_export("sha512").unwrap();
697        let result = sha_fn(
698            &[ValueWord::from_string(Arc::new("hello".to_string()))],
699            &ctx,
700        )
701        .unwrap();
702        // Known SHA-512 digest for "hello"
703        assert_eq!(
704            result.as_str(),
705            Some(
706                "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043"
707            )
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(
721                "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
722            )
723        );
724    }
725
726    #[test]
727    fn test_sha512_requires_string() {
728        let module = create_crypto_module();
729        let ctx = test_ctx();
730        let sha_fn = module.get_export("sha512").unwrap();
731        assert!(sha_fn(&[ValueWord::from_f64(42.0)], &ctx).is_err());
732    }
733
734    #[test]
735    fn test_sha1_known_digest() {
736        let module = create_crypto_module();
737        let ctx = test_ctx();
738        let sha_fn = module.get_export("sha1").unwrap();
739        let result = sha_fn(
740            &[ValueWord::from_string(Arc::new("hello".to_string()))],
741            &ctx,
742        )
743        .unwrap();
744        // Known SHA-1 digest for "hello"
745        assert_eq!(
746            result.as_str(),
747            Some("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d")
748        );
749    }
750
751    #[test]
752    fn test_sha1_empty_string() {
753        let module = create_crypto_module();
754        let ctx = test_ctx();
755        let sha_fn = module.get_export("sha1").unwrap();
756        let result = sha_fn(&[ValueWord::from_string(Arc::new(String::new()))], &ctx).unwrap();
757        assert_eq!(
758            result.as_str(),
759            Some("da39a3ee5e6b4b0d3255bfef95601890afd80709")
760        );
761    }
762
763    #[test]
764    fn test_sha1_requires_string() {
765        let module = create_crypto_module();
766        let ctx = test_ctx();
767        let sha_fn = module.get_export("sha1").unwrap();
768        assert!(sha_fn(&[ValueWord::from_f64(42.0)], &ctx).is_err());
769    }
770
771    #[test]
772    fn test_md5_known_digest() {
773        let module = create_crypto_module();
774        let ctx = test_ctx();
775        let md5_fn = module.get_export("md5").unwrap();
776        let result = md5_fn(
777            &[ValueWord::from_string(Arc::new("hello".to_string()))],
778            &ctx,
779        )
780        .unwrap();
781        // Known MD5 digest for "hello"
782        assert_eq!(result.as_str(), Some("5d41402abc4b2a76b9719d911017c592"));
783    }
784
785    #[test]
786    fn test_md5_empty_string() {
787        let module = create_crypto_module();
788        let ctx = test_ctx();
789        let md5_fn = module.get_export("md5").unwrap();
790        let result = md5_fn(&[ValueWord::from_string(Arc::new(String::new()))], &ctx).unwrap();
791        assert_eq!(result.as_str(), Some("d41d8cd98f00b204e9800998ecf8427e"));
792    }
793
794    #[test]
795    fn test_md5_requires_string() {
796        let module = create_crypto_module();
797        let ctx = test_ctx();
798        let md5_fn = module.get_export("md5").unwrap();
799        assert!(md5_fn(&[ValueWord::from_f64(42.0)], &ctx).is_err());
800    }
801
802    #[test]
803    fn test_random_bytes_length() {
804        let module = create_crypto_module();
805        let ctx = test_ctx();
806        let rb_fn = module.get_export("random_bytes").unwrap();
807        let result = rb_fn(&[ValueWord::from_i64(16)], &ctx).unwrap();
808        let hex_str = result.as_str().unwrap();
809        // 16 bytes = 32 hex chars
810        assert_eq!(hex_str.len(), 32);
811    }
812
813    #[test]
814    fn test_random_bytes_zero() {
815        let module = create_crypto_module();
816        let ctx = test_ctx();
817        let rb_fn = module.get_export("random_bytes").unwrap();
818        let result = rb_fn(&[ValueWord::from_i64(0)], &ctx).unwrap();
819        assert_eq!(result.as_str(), Some(""));
820    }
821
822    #[test]
823    fn test_random_bytes_negative_rejected() {
824        let module = create_crypto_module();
825        let ctx = test_ctx();
826        let rb_fn = module.get_export("random_bytes").unwrap();
827        assert!(rb_fn(&[ValueWord::from_i64(-1)], &ctx).is_err());
828    }
829
830    #[test]
831    fn test_random_bytes_too_large_rejected() {
832        let module = create_crypto_module();
833        let ctx = test_ctx();
834        let rb_fn = module.get_export("random_bytes").unwrap();
835        assert!(rb_fn(&[ValueWord::from_i64(65537)], &ctx).is_err());
836    }
837
838    #[test]
839    fn test_random_bytes_requires_int() {
840        let module = create_crypto_module();
841        let ctx = test_ctx();
842        let rb_fn = module.get_export("random_bytes").unwrap();
843        assert!(rb_fn(&[ValueWord::from_string(Arc::new("10".to_string()))], &ctx).is_err());
844    }
845
846    #[test]
847    fn test_ed25519_generate_keypair() {
848        let module = create_crypto_module();
849        let ctx = test_ctx();
850        let gen_fn = module.get_export("ed25519_generate_keypair").unwrap();
851        let result = gen_fn(&[], &ctx).unwrap();
852
853        // Result should be a HashMap with public_key and secret_key
854        let hm = result.as_hashmap_data().expect("should be a HashMap");
855        let pub_key = hm.shape_get("public_key").expect("should have public_key");
856        let sec_key = hm.shape_get("secret_key").expect("should have secret_key");
857
858        // 32 bytes = 64 hex chars
859        assert_eq!(pub_key.as_str().unwrap().len(), 64);
860        assert_eq!(sec_key.as_str().unwrap().len(), 64);
861    }
862
863    #[test]
864    fn test_ed25519_sign_and_verify_roundtrip() {
865        let module = create_crypto_module();
866        let ctx = test_ctx();
867
868        // Generate a keypair
869        let gen_fn = module.get_export("ed25519_generate_keypair").unwrap();
870        let keypair = gen_fn(&[], &ctx).unwrap();
871        let hm = keypair.as_hashmap_data().unwrap();
872
873        let pub_key = hm.shape_get("public_key").unwrap().clone();
874        let sec_key = hm.shape_get("secret_key").unwrap().clone();
875
876        let message = ValueWord::from_string(Arc::new("test message".to_string()));
877
878        // Sign
879        let sign_fn = module.get_export("ed25519_sign").unwrap();
880        let signature = sign_fn(&[message.clone(), sec_key], &ctx).unwrap();
881        // 64 bytes = 128 hex chars
882        assert_eq!(signature.as_str().unwrap().len(), 128);
883
884        // Verify — should succeed
885        let verify_fn = module.get_export("ed25519_verify").unwrap();
886        let valid = verify_fn(&[message, signature, pub_key], &ctx).unwrap();
887        assert_eq!(valid.as_bool(), Some(true));
888    }
889
890    #[test]
891    fn test_ed25519_verify_wrong_message() {
892        let module = create_crypto_module();
893        let ctx = test_ctx();
894
895        let gen_fn = module.get_export("ed25519_generate_keypair").unwrap();
896        let keypair = gen_fn(&[], &ctx).unwrap();
897        let hm = keypair.as_hashmap_data().unwrap();
898
899        let pub_key = hm.shape_get("public_key").unwrap().clone();
900        let sec_key = hm.shape_get("secret_key").unwrap().clone();
901
902        let message = ValueWord::from_string(Arc::new("correct message".to_string()));
903        let wrong_message = ValueWord::from_string(Arc::new("wrong message".to_string()));
904
905        let sign_fn = module.get_export("ed25519_sign").unwrap();
906        let signature = sign_fn(&[message, sec_key], &ctx).unwrap();
907
908        let verify_fn = module.get_export("ed25519_verify").unwrap();
909        let valid = verify_fn(&[wrong_message, signature, pub_key], &ctx).unwrap();
910        assert_eq!(valid.as_bool(), Some(false));
911    }
912
913    #[test]
914    fn test_ed25519_sign_invalid_secret_key() {
915        let module = create_crypto_module();
916        let ctx = test_ctx();
917        let sign_fn = module.get_export("ed25519_sign").unwrap();
918
919        // Too short
920        let result = sign_fn(
921            &[
922                ValueWord::from_string(Arc::new("msg".to_string())),
923                ValueWord::from_string(Arc::new("abcd".to_string())),
924            ],
925            &ctx,
926        );
927        assert!(result.is_err());
928
929        // Invalid hex
930        let result = sign_fn(
931            &[
932                ValueWord::from_string(Arc::new("msg".to_string())),
933                ValueWord::from_string(Arc::new("zzzz".to_string())),
934            ],
935            &ctx,
936        );
937        assert!(result.is_err());
938    }
939
940    #[test]
941    fn test_ed25519_verify_invalid_inputs() {
942        let module = create_crypto_module();
943        let ctx = test_ctx();
944        let verify_fn = module.get_export("ed25519_verify").unwrap();
945
946        // Missing arguments
947        assert!(verify_fn(&[ValueWord::from_string(Arc::new("msg".to_string()))], &ctx).is_err());
948
949        // Invalid hex in signature
950        assert!(
951            verify_fn(
952                &[
953                    ValueWord::from_string(Arc::new("msg".to_string())),
954                    ValueWord::from_string(Arc::new("not_hex".to_string())),
955                    ValueWord::from_string(Arc::new("ab".repeat(32))),
956                ],
957                &ctx
958            )
959            .is_err()
960        );
961    }
962
963    #[test]
964    fn test_new_function_schemas() {
965        let module = create_crypto_module();
966
967        let sha512_schema = module.get_schema("sha512").unwrap();
968        assert_eq!(sha512_schema.params.len(), 1);
969        assert_eq!(sha512_schema.return_type.as_deref(), Some("string"));
970
971        let sha1_schema = module.get_schema("sha1").unwrap();
972        assert_eq!(sha1_schema.params.len(), 1);
973        assert_eq!(sha1_schema.return_type.as_deref(), Some("string"));
974
975        let md5_schema = module.get_schema("md5").unwrap();
976        assert_eq!(md5_schema.params.len(), 1);
977        assert_eq!(md5_schema.return_type.as_deref(), Some("string"));
978
979        let rb_schema = module.get_schema("random_bytes").unwrap();
980        assert_eq!(rb_schema.params.len(), 1);
981        assert_eq!(rb_schema.params[0].type_name, "int");
982        assert_eq!(rb_schema.return_type.as_deref(), Some("string"));
983
984        let gen_schema = module.get_schema("ed25519_generate_keypair").unwrap();
985        assert_eq!(gen_schema.params.len(), 0);
986        assert_eq!(gen_schema.return_type.as_deref(), Some("object"));
987
988        let sign_schema = module.get_schema("ed25519_sign").unwrap();
989        assert_eq!(sign_schema.params.len(), 2);
990        assert_eq!(sign_schema.return_type.as_deref(), Some("string"));
991
992        let verify_schema = module.get_schema("ed25519_verify").unwrap();
993        assert_eq!(verify_schema.params.len(), 3);
994        assert_eq!(verify_schema.return_type.as_deref(), Some("bool"));
995    }
996}