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//!
8//! Phase 2c: migrated to the typed marshal layer
9//! (`crate::marshal::register_typed_fn_N`).
10
11use crate::marshal::{register_typed_fn_0, register_typed_fn_1, register_typed_fn_2, register_typed_fn_3};
12use crate::module_exports::ModuleExports;
13use crate::typed_module_exports::{ConcreteReturn, ConcreteType, TypedReturn};
14use std::sync::Arc;
15
16/// Create the `crypto` module with hashing and encoding functions.
17pub fn create_crypto_module() -> ModuleExports {
18    let mut module = ModuleExports::new("std::core::crypto");
19    module.description = "Cryptographic hashing and encoding utilities".to_string();
20
21    // crypto.sha256(data: string) -> string
22    register_typed_fn_1::<_, Arc<String>>(
23        &mut module,
24        "sha256",
25        "Compute the SHA-256 hash of a string, returning a hex-encoded digest",
26        "data",
27        "string",
28        ConcreteType::String,
29        |data, _ctx| {
30            use sha2::{Digest, Sha256};
31            let mut hasher = Sha256::new();
32            hasher.update(data.as_bytes());
33            let result = hasher.finalize();
34            Ok(TypedReturn::Concrete(ConcreteReturn::String(hex::encode(result))))
35        },
36    );
37
38    // crypto.hmac_sha256(data: string, key: string) -> string
39    register_typed_fn_2::<_, Arc<String>, Arc<String>>(
40        &mut module,
41        "hmac_sha256",
42        "Compute HMAC-SHA256 of data with the given key, returning hex digest",
43        [("data", "string"), ("key", "string")],
44        ConcreteType::String,
45        |data, key, _ctx| {
46            use hmac::{Hmac, Mac};
47            use sha2::Sha256;
48            type HmacSha256 = Hmac<Sha256>;
49            let mut mac = HmacSha256::new_from_slice(key.as_bytes())
50                .map_err(|e| format!("crypto.hmac_sha256() key error: {}", e))?;
51            mac.update(data.as_bytes());
52            let result = mac.finalize();
53            Ok(TypedReturn::Concrete(ConcreteReturn::String(hex::encode(result.into_bytes()))))
54        },
55    );
56
57    // crypto.base64_encode(data: string) -> string
58    register_typed_fn_1::<_, Arc<String>>(
59        &mut module,
60        "base64_encode",
61        "Encode a string to Base64",
62        "data",
63        "string",
64        ConcreteType::String,
65        |data, _ctx| {
66            use base64::Engine;
67            let encoded = base64::engine::general_purpose::STANDARD.encode(data.as_bytes());
68            Ok(TypedReturn::Concrete(ConcreteReturn::String(encoded)))
69        },
70    );
71
72    // crypto.base64_decode(encoded: string) -> Result<string>
73    register_typed_fn_1::<_, Arc<String>>(
74        &mut module,
75        "base64_decode",
76        "Decode a Base64 string",
77        "encoded",
78        "string",
79        ConcreteType::Result(Box::new(ConcreteType::String)),
80        |encoded, _ctx| {
81            use base64::Engine;
82            let bytes = base64::engine::general_purpose::STANDARD
83                .decode(encoded.as_str())
84                .map_err(|e| format!("crypto.base64_decode() failed: {}", e))?;
85            let decoded = String::from_utf8(bytes)
86                .map_err(|e| format!("crypto.base64_decode() invalid UTF-8: {}", e))?;
87            Ok(TypedReturn::Ok(ConcreteReturn::String(decoded)))
88        },
89    );
90
91    // crypto.hex_encode(data: string) -> string
92    register_typed_fn_1::<_, Arc<String>>(
93        &mut module,
94        "hex_encode",
95        "Encode a string as hexadecimal",
96        "data",
97        "string",
98        ConcreteType::String,
99        |data, _ctx| {
100            Ok(TypedReturn::Concrete(ConcreteReturn::String(hex::encode(data.as_bytes()))))
101        },
102    );
103
104    // crypto.hex_decode(hex: string) -> Result<string>
105    register_typed_fn_1::<_, Arc<String>>(
106        &mut module,
107        "hex_decode",
108        "Decode a hexadecimal string",
109        "hex",
110        "string",
111        ConcreteType::Result(Box::new(ConcreteType::String)),
112        |hex_str, _ctx| {
113            let bytes = hex::decode(hex_str.as_str())
114                .map_err(|e| format!("crypto.hex_decode() failed: {}", e))?;
115            let decoded = String::from_utf8(bytes)
116                .map_err(|e| format!("crypto.hex_decode() invalid UTF-8: {}", e))?;
117            Ok(TypedReturn::Ok(ConcreteReturn::String(decoded)))
118        },
119    );
120
121    // crypto.sha512(data: string) -> string
122    register_typed_fn_1::<_, Arc<String>>(
123        &mut module,
124        "sha512",
125        "Compute the SHA-512 hash of a string, returning a hex-encoded digest",
126        "data",
127        "string",
128        ConcreteType::String,
129        |data, _ctx| {
130            use sha2::{Digest, Sha512};
131            let mut hasher = Sha512::new();
132            hasher.update(data.as_bytes());
133            let result = hasher.finalize();
134            Ok(TypedReturn::Concrete(ConcreteReturn::String(hex::encode(result))))
135        },
136    );
137
138    // crypto.sha1(data: string) -> string
139    register_typed_fn_1::<_, Arc<String>>(
140        &mut module,
141        "sha1",
142        "Compute the SHA-1 hash of a string, returning a hex-encoded digest (legacy)",
143        "data",
144        "string",
145        ConcreteType::String,
146        |data, _ctx| {
147            use sha1::Digest;
148            let mut hasher = sha1::Sha1::new();
149            hasher.update(data.as_bytes());
150            let result = hasher.finalize();
151            Ok(TypedReturn::Concrete(ConcreteReturn::String(hex::encode(result))))
152        },
153    );
154
155    // crypto.md5(data: string) -> string
156    register_typed_fn_1::<_, Arc<String>>(
157        &mut module,
158        "md5",
159        "Compute the MD5 hash of a string, returning a hex-encoded digest (legacy)",
160        "data",
161        "string",
162        ConcreteType::String,
163        |data, _ctx| {
164            use md5::Digest;
165            let mut hasher = md5::Md5::new();
166            hasher.update(data.as_bytes());
167            let result = hasher.finalize();
168            Ok(TypedReturn::Concrete(ConcreteReturn::String(hex::encode(result))))
169        },
170    );
171
172    // crypto.random_bytes(n: int) -> string
173    register_typed_fn_1::<_, i64>(
174        &mut module,
175        "random_bytes",
176        "Generate n random bytes, returned as a hex-encoded string",
177        "n",
178        "int",
179        ConcreteType::String,
180        |n, ctx| {
181            crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Random)?;
182            use rand::RngCore;
183            if n < 0 || n > 65536 {
184                return Err("crypto.random_bytes() n must be between 0 and 65536".to_string());
185            }
186            let mut buf = vec![0u8; n as usize];
187            rand::thread_rng().fill_bytes(&mut buf);
188            Ok(TypedReturn::Concrete(ConcreteReturn::String(hex::encode(buf))))
189        },
190    );
191
192    // crypto.ed25519_generate_keypair() -> object
193    register_typed_fn_0(
194        &mut module,
195        "ed25519_generate_keypair",
196        "Generate an Ed25519 keypair, returning an object with hex-encoded public_key and secret_key",
197        ConcreteType::Object,
198        |ctx| {
199            crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Random)?;
200            use rand::RngCore;
201            let mut secret = [0u8; 32];
202            rand::thread_rng().fill_bytes(&mut secret);
203            let signing_key = ed25519_dalek::SigningKey::from_bytes(&secret);
204            let verifying_key = signing_key.verifying_key();
205            Ok(TypedReturn::ObjectPairs(vec![
206                (
207                    "public_key".to_string(),
208                    ConcreteReturn::String(hex::encode(verifying_key.to_bytes())),
209                ),
210                (
211                    "secret_key".to_string(),
212                    ConcreteReturn::String(hex::encode(signing_key.to_bytes())),
213                ),
214            ]))
215        },
216    );
217
218    // crypto.ed25519_sign(message: string, secret_key: string) -> string
219    register_typed_fn_2::<_, Arc<String>, Arc<String>>(
220        &mut module,
221        "ed25519_sign",
222        "Sign a message with an Ed25519 secret key, returning a hex-encoded signature",
223        [("message", "string"), ("secret_key", "string")],
224        ConcreteType::String,
225        |message, secret_hex, _ctx| {
226            use ed25519_dalek::Signer;
227            let secret_bytes = hex::decode(secret_hex.as_str())
228                .map_err(|e| format!("crypto.ed25519_sign() invalid secret_key hex: {}", e))?;
229            let secret_arr: [u8; 32] = secret_bytes.as_slice().try_into().map_err(|_| {
230                format!(
231                    "crypto.ed25519_sign() secret_key must be 32 bytes (got {})",
232                    secret_bytes.len()
233                )
234            })?;
235            let signing_key = ed25519_dalek::SigningKey::from_bytes(&secret_arr);
236            let signature = signing_key.sign(message.as_bytes());
237            Ok(TypedReturn::Concrete(ConcreteReturn::String(hex::encode(signature.to_bytes()))))
238        },
239    );
240
241    // crypto.ed25519_verify(message: string, signature: string, public_key: string) -> bool
242    register_typed_fn_3::<_, Arc<String>, Arc<String>, Arc<String>>(
243        &mut module,
244        "ed25519_verify",
245        "Verify an Ed25519 signature against a message and public key",
246        [("message", "string"), ("signature", "string"), ("public_key", "string")],
247        ConcreteType::Bool,
248        |message, sig_hex, pub_hex, _ctx| {
249            use ed25519_dalek::Verifier;
250            let sig_bytes = hex::decode(sig_hex.as_str())
251                .map_err(|e| format!("crypto.ed25519_verify() invalid signature hex: {}", e))?;
252            let sig_arr: [u8; 64] = sig_bytes.as_slice().try_into().map_err(|_| {
253                format!(
254                    "crypto.ed25519_verify() signature must be 64 bytes (got {})",
255                    sig_bytes.len()
256                )
257            })?;
258            let pub_bytes = hex::decode(pub_hex.as_str())
259                .map_err(|e| format!("crypto.ed25519_verify() invalid public_key hex: {}", e))?;
260            let pub_arr: [u8; 32] = pub_bytes.as_slice().try_into().map_err(|_| {
261                format!(
262                    "crypto.ed25519_verify() public_key must be 32 bytes (got {})",
263                    pub_bytes.len()
264                )
265            })?;
266            let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&pub_arr)
267                .map_err(|e| format!("crypto.ed25519_verify() invalid public key: {}", e))?;
268            let signature = ed25519_dalek::Signature::from_bytes(&sig_arr);
269            let valid = verifying_key.verify(message.as_bytes(), &signature).is_ok();
270            Ok(TypedReturn::Concrete(ConcreteReturn::Bool(valid)))
271        },
272    );
273
274    module
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn test_crypto_module_creation() {
283        let module = create_crypto_module();
284        assert_eq!(module.name, "std::core::crypto");
285        assert!(module.has_export("sha256"));
286        assert!(module.has_export("hmac_sha256"));
287        assert!(module.has_export("base64_encode"));
288        assert!(module.has_export("base64_decode"));
289        assert!(module.has_export("hex_encode"));
290        assert!(module.has_export("hex_decode"));
291        assert!(module.has_export("sha512"));
292        assert!(module.has_export("sha1"));
293        assert!(module.has_export("md5"));
294        assert!(module.has_export("random_bytes"));
295        assert!(module.has_export("ed25519_generate_keypair"));
296        assert!(module.has_export("ed25519_sign"));
297        assert!(module.has_export("ed25519_verify"));
298    }
299
300    #[test]
301    fn test_crypto_schemas() {
302        let module = create_crypto_module();
303
304        let sha_schema = module.get_schema("sha256").unwrap();
305        assert_eq!(sha_schema.params.len(), 1);
306        assert_eq!(sha_schema.return_type.as_deref(), Some("string"));
307
308        let hmac_schema = module.get_schema("hmac_sha256").unwrap();
309        assert_eq!(hmac_schema.params.len(), 2);
310
311        let b64d_schema = module.get_schema("base64_decode").unwrap();
312        assert_eq!(b64d_schema.return_type.as_deref(), Some("Result<string>"));
313
314        let rb_schema = module.get_schema("random_bytes").unwrap();
315        assert_eq!(rb_schema.params[0].type_name, "int");
316
317        let gen_schema = module.get_schema("ed25519_generate_keypair").unwrap();
318        assert_eq!(gen_schema.params.len(), 0);
319
320        let verify_schema = module.get_schema("ed25519_verify").unwrap();
321        assert_eq!(verify_schema.params.len(), 3);
322    }
323
324    #[test]
325    fn test_crypto_typed_registry() {
326        let module = create_crypto_module();
327        let typed = module.typed_exports();
328        assert_eq!(typed.functions.len(), 13);
329
330        let sha = typed.get("sha256").unwrap();
331        assert_eq!(sha.arg_kinds.len(), 1);
332        assert_eq!(sha.arg_kinds[0], shape_value::NativeKind::String);
333
334        let rb = typed.get("random_bytes").unwrap();
335        assert_eq!(rb.arg_kinds[0], shape_value::NativeKind::Int64);
336
337        let keygen = typed.get("ed25519_generate_keypair").unwrap();
338        assert!(keygen.arg_kinds.is_empty());
339    }
340
341    // Behavioural invocation tests removed — they used `module.invoke_export`
342    // with `ValueWord` arrays, which is the deleted dynamic-dispatch entry
343    // point. End-to-end behaviour is now covered through typed-slot dispatch
344    // via the marshal layer; integration tests in `shape-test` will exercise
345    // the full path once the strict-typed cascade reaches shape-vm.
346}