Skip to main content

shape_runtime/stdlib/
crypto.rs

1//! Native `crypto` module for hashing and encoding utilities.
2//!
3//! Exports: crypto.sha256, crypto.hmac_sha256, crypto.base64_encode,
4//!          crypto.base64_decode, crypto.hex_encode, crypto.hex_decode
5
6use crate::module_exports::{ModuleContext, ModuleExports, ModuleFunction, ModuleParam};
7use shape_value::ValueWord;
8use std::sync::Arc;
9
10/// Create the `crypto` module with hashing and encoding functions.
11pub fn create_crypto_module() -> ModuleExports {
12    let mut module = ModuleExports::new("crypto");
13    module.description = "Cryptographic hashing and encoding utilities".to_string();
14
15    // crypto.sha256(data: string) -> string
16    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    // crypto.hmac_sha256(data: string, key: string) -> string
46    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    // crypto.base64_encode(data: string) -> string
94    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    // crypto.base64_decode(encoded: string) -> Result<string>
121    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    // crypto.hex_encode(data: string) -> string
156    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    // crypto.hex_decode(hex: string) -> Result<string>
182    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        // Known SHA-256 digest for "hello"
258        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        // HMAC-SHA256("hello", "secret") is a known value
298        let digest = result.as_str().unwrap();
299        assert_eq!(digest.len(), 64); // 32 bytes = 64 hex chars
300    }
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}