seq_runtime/
crypto.rs

1//! Cryptographic operations for Seq
2//!
3//! These functions are exported with C ABI for LLVM codegen to call.
4//!
5//! # API
6//!
7//! ```seq
8//! # SHA-256 hashing
9//! "hello" crypto.sha256                    # ( String -- String ) hex digest
10//!
11//! # HMAC-SHA256 for webhook verification
12//! "message" "secret" crypto.hmac-sha256    # ( String String -- String ) hex signature
13//!
14//! # Timing-safe comparison
15//! sig1 sig2 crypto.constant-time-eq        # ( String String -- Bool )
16//!
17//! # Secure random bytes
18//! 32 crypto.random-bytes                   # ( Int -- String ) hex-encoded random bytes
19//!
20//! # UUID v4
21//! crypto.uuid4                             # ( -- String ) "550e8400-e29b-41d4-a716-446655440000"
22//! ```
23
24use crate::seqstring::global_string;
25use crate::stack::{Stack, pop, push};
26use crate::value::Value;
27
28use hmac::{Hmac, Mac};
29use rand::RngCore;
30use sha2::{Digest, Sha256};
31use subtle::ConstantTimeEq;
32use uuid::Uuid;
33
34type HmacSha256 = Hmac<Sha256>;
35
36/// Compute SHA-256 hash of a string
37///
38/// Stack effect: ( String -- String )
39///
40/// Returns the hash as a lowercase hex string (64 characters).
41///
42/// # Safety
43/// Stack must have a String value on top
44#[unsafe(no_mangle)]
45pub unsafe extern "C" fn patch_seq_sha256(stack: Stack) -> Stack {
46    assert!(!stack.is_null(), "sha256: stack is empty");
47
48    let (stack, value) = unsafe { pop(stack) };
49
50    match value {
51        Value::String(s) => {
52            let mut hasher = Sha256::new();
53            hasher.update(s.as_str().as_bytes());
54            let result = hasher.finalize();
55            let hex_digest = hex::encode(result);
56            unsafe { push(stack, Value::String(global_string(hex_digest))) }
57        }
58        _ => panic!("sha256: expected String on stack, got {:?}", value),
59    }
60}
61
62/// Compute HMAC-SHA256 of a message with a key
63///
64/// Stack effect: ( message key -- String )
65///
66/// Returns the signature as a lowercase hex string (64 characters).
67/// Used for webhook verification, JWT signing, API authentication.
68///
69/// # Safety
70/// Stack must have two String values on top (message, then key)
71#[unsafe(no_mangle)]
72pub unsafe extern "C" fn patch_seq_hmac_sha256(stack: Stack) -> Stack {
73    assert!(!stack.is_null(), "hmac-sha256: stack is empty");
74
75    let (stack, key_value) = unsafe { pop(stack) };
76    let (stack, msg_value) = unsafe { pop(stack) };
77
78    match (msg_value, key_value) {
79        (Value::String(msg), Value::String(key)) => {
80            let mut mac =
81                HmacSha256::new_from_slice(key.as_str().as_bytes()).expect("HMAC can take any key");
82            mac.update(msg.as_str().as_bytes());
83            let result = mac.finalize();
84            let hex_sig = hex::encode(result.into_bytes());
85            unsafe { push(stack, Value::String(global_string(hex_sig))) }
86        }
87        (msg, key) => panic!(
88            "hmac-sha256: expected (String, String) on stack, got ({:?}, {:?})",
89            msg, key
90        ),
91    }
92}
93
94/// Timing-safe string comparison
95///
96/// Stack effect: ( String String -- Bool )
97///
98/// Compares two strings in constant time to prevent timing attacks.
99/// Essential for comparing signatures, hashes, tokens, etc.
100///
101/// Uses the `subtle` crate for cryptographically secure constant-time comparison.
102/// This prevents timing side-channel attacks where an attacker could deduce
103/// secret values by measuring comparison duration.
104///
105/// # Safety
106/// Stack must have two String values on top
107#[unsafe(no_mangle)]
108pub unsafe extern "C" fn patch_seq_constant_time_eq(stack: Stack) -> Stack {
109    assert!(!stack.is_null(), "constant-time-eq: stack is empty");
110
111    let (stack, b_value) = unsafe { pop(stack) };
112    let (stack, a_value) = unsafe { pop(stack) };
113
114    match (a_value, b_value) {
115        (Value::String(a), Value::String(b)) => {
116            let a_bytes = a.as_str().as_bytes();
117            let b_bytes = b.as_str().as_bytes();
118
119            // Use subtle crate for truly constant-time comparison
120            // This handles different-length strings correctly without timing leaks
121            let eq = a_bytes.ct_eq(b_bytes);
122
123            unsafe { push(stack, Value::Bool(bool::from(eq))) }
124        }
125        (a, b) => panic!(
126            "constant-time-eq: expected (String, String) on stack, got ({:?}, {:?})",
127            a, b
128        ),
129    }
130}
131
132/// Generate cryptographically secure random bytes
133///
134/// Stack effect: ( Int -- String )
135///
136/// Returns the random bytes as a lowercase hex string (2 chars per byte).
137/// Uses the operating system's secure random number generator.
138///
139/// # Limits
140/// - Maximum: 1024 bytes (to prevent memory exhaustion)
141/// - Common use cases: 16-32 bytes for tokens/nonces, 32-64 bytes for keys
142///
143/// # Safety
144/// Stack must have an Int value on top (number of bytes to generate)
145#[unsafe(no_mangle)]
146pub unsafe extern "C" fn patch_seq_random_bytes(stack: Stack) -> Stack {
147    assert!(!stack.is_null(), "random-bytes: stack is empty");
148
149    let (stack, value) = unsafe { pop(stack) };
150
151    match value {
152        Value::Int(n) => {
153            if n < 0 {
154                panic!("random-bytes: byte count must be non-negative, got {}", n);
155            }
156            if n > 1024 {
157                panic!("random-bytes: byte count too large (max 1024), got {}", n);
158            }
159
160            let mut bytes = vec![0u8; n as usize];
161            rand::thread_rng().fill_bytes(&mut bytes);
162            let hex_str = hex::encode(&bytes);
163            unsafe { push(stack, Value::String(global_string(hex_str))) }
164        }
165        _ => panic!("random-bytes: expected Int on stack, got {:?}", value),
166    }
167}
168
169/// Generate a UUID v4 (random)
170///
171/// Stack effect: ( -- String )
172///
173/// Returns a UUID in standard format: "550e8400-e29b-41d4-a716-446655440000"
174///
175/// # Safety
176/// Stack pointer must be valid
177#[unsafe(no_mangle)]
178pub unsafe extern "C" fn patch_seq_uuid4(stack: Stack) -> Stack {
179    assert!(!stack.is_null(), "uuid4: stack is empty");
180
181    let uuid = Uuid::new_v4();
182    unsafe { push(stack, Value::String(global_string(uuid.to_string()))) }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use crate::stack::pop;
189
190    #[test]
191    fn test_sha256() {
192        unsafe {
193            let stack = crate::stack::alloc_test_stack();
194            let stack = push(stack, Value::String(global_string("hello".to_string())));
195            let stack = patch_seq_sha256(stack);
196            let (_, value) = pop(stack);
197
198            match value {
199                Value::String(s) => {
200                    // SHA-256 of "hello"
201                    assert_eq!(
202                        s.as_str(),
203                        "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
204                    );
205                }
206                _ => panic!("Expected String"),
207            }
208        }
209    }
210
211    #[test]
212    fn test_sha256_empty() {
213        unsafe {
214            let stack = crate::stack::alloc_test_stack();
215            let stack = push(stack, Value::String(global_string(String::new())));
216            let stack = patch_seq_sha256(stack);
217            let (_, value) = pop(stack);
218
219            match value {
220                Value::String(s) => {
221                    // SHA-256 of empty string
222                    assert_eq!(
223                        s.as_str(),
224                        "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
225                    );
226                }
227                _ => panic!("Expected String"),
228            }
229        }
230    }
231
232    #[test]
233    fn test_hmac_sha256() {
234        unsafe {
235            let stack = crate::stack::alloc_test_stack();
236            let stack = push(stack, Value::String(global_string("message".to_string())));
237            let stack = push(stack, Value::String(global_string("secret".to_string())));
238            let stack = patch_seq_hmac_sha256(stack);
239            let (_, value) = pop(stack);
240
241            match value {
242                Value::String(s) => {
243                    // HMAC-SHA256("message", "secret")
244                    assert_eq!(
245                        s.as_str(),
246                        "8b5f48702995c1598c573db1e21866a9b825d4a794d169d7060a03605796360b"
247                    );
248                }
249                _ => panic!("Expected String"),
250            }
251        }
252    }
253
254    #[test]
255    fn test_constant_time_eq_equal() {
256        unsafe {
257            let stack = crate::stack::alloc_test_stack();
258            let stack = push(stack, Value::String(global_string("hello".to_string())));
259            let stack = push(stack, Value::String(global_string("hello".to_string())));
260            let stack = patch_seq_constant_time_eq(stack);
261            let (_, value) = pop(stack);
262
263            match value {
264                Value::Bool(b) => assert!(b),
265                _ => panic!("Expected Bool"),
266            }
267        }
268    }
269
270    #[test]
271    fn test_constant_time_eq_different() {
272        unsafe {
273            let stack = crate::stack::alloc_test_stack();
274            let stack = push(stack, Value::String(global_string("hello".to_string())));
275            let stack = push(stack, Value::String(global_string("world".to_string())));
276            let stack = patch_seq_constant_time_eq(stack);
277            let (_, value) = pop(stack);
278
279            match value {
280                Value::Bool(b) => assert!(!b),
281                _ => panic!("Expected Bool"),
282            }
283        }
284    }
285
286    #[test]
287    fn test_constant_time_eq_different_lengths() {
288        unsafe {
289            let stack = crate::stack::alloc_test_stack();
290            let stack = push(stack, Value::String(global_string("hello".to_string())));
291            let stack = push(stack, Value::String(global_string("hi".to_string())));
292            let stack = patch_seq_constant_time_eq(stack);
293            let (_, value) = pop(stack);
294
295            match value {
296                Value::Bool(b) => assert!(!b),
297                _ => panic!("Expected Bool"),
298            }
299        }
300    }
301
302    #[test]
303    fn test_random_bytes() {
304        unsafe {
305            let stack = crate::stack::alloc_test_stack();
306            let stack = push(stack, Value::Int(16));
307            let stack = patch_seq_random_bytes(stack);
308            let (_, value) = pop(stack);
309
310            match value {
311                Value::String(s) => {
312                    // 16 bytes = 32 hex chars
313                    assert_eq!(s.as_str().len(), 32);
314                    // Should be valid hex
315                    assert!(hex::decode(s.as_str()).is_ok());
316                }
317                _ => panic!("Expected String"),
318            }
319        }
320    }
321
322    #[test]
323    fn test_random_bytes_zero() {
324        unsafe {
325            let stack = crate::stack::alloc_test_stack();
326            let stack = push(stack, Value::Int(0));
327            let stack = patch_seq_random_bytes(stack);
328            let (_, value) = pop(stack);
329
330            match value {
331                Value::String(s) => {
332                    assert_eq!(s.as_str(), "");
333                }
334                _ => panic!("Expected String"),
335            }
336        }
337    }
338
339    #[test]
340    fn test_uuid4() {
341        unsafe {
342            let stack = crate::stack::alloc_test_stack();
343            let stack = patch_seq_uuid4(stack);
344            let (_, value) = pop(stack);
345
346            match value {
347                Value::String(s) => {
348                    // UUID format: 8-4-4-4-12
349                    assert_eq!(s.as_str().len(), 36);
350                    assert_eq!(s.as_str().chars().filter(|c| *c == '-').count(), 4);
351                    // Version 4 indicator at position 14
352                    assert_eq!(s.as_str().chars().nth(14), Some('4'));
353                }
354                _ => panic!("Expected String"),
355            }
356        }
357    }
358
359    #[test]
360    fn test_uuid4_unique() {
361        unsafe {
362            let stack = crate::stack::alloc_test_stack();
363            let stack = patch_seq_uuid4(stack);
364            let (stack, value1) = pop(stack);
365            let stack = patch_seq_uuid4(stack);
366            let (_, value2) = pop(stack);
367
368            match (value1, value2) {
369                (Value::String(s1), Value::String(s2)) => {
370                    assert_ne!(s1.as_str(), s2.as_str());
371                }
372                _ => panic!("Expected Strings"),
373            }
374        }
375    }
376
377    #[test]
378    fn test_random_bytes_max_limit() {
379        unsafe {
380            let stack = crate::stack::alloc_test_stack();
381            let stack = push(stack, Value::Int(1024)); // Max allowed
382            let stack = patch_seq_random_bytes(stack);
383            let (_, value) = pop(stack);
384
385            match value {
386                Value::String(s) => {
387                    // 1024 bytes = 2048 hex chars
388                    assert_eq!(s.as_str().len(), 2048);
389                }
390                _ => panic!("Expected String"),
391            }
392        }
393    }
394    // Note: Exceeding the 1024 byte limit causes a panic, which aborts in FFI context.
395    // This is intentional - the limit prevents memory exhaustion attacks.
396}