Skip to main content

tatara_lisp_script/stdlib/
crypto_extra.rs

1//! Additional hashes + HMAC + random/uuid.
2//!
3//!   (sha1 STR)                → hex digest
4//!   (sha512 STR)              → hex digest
5//!   (hmac-sha256 KEY MESSAGE) → hex digest
6//!   (uuid-v4)                 → RFC 4122 v4 UUID string
7//!   (random-hex N)            → N bytes of crypto-random hex (2*N chars)
8//!   (random-int LO HI)        → uniform random in [LO, HI)
9
10use std::sync::Arc;
11
12use hmac::{Hmac, Mac};
13use sha1::Sha1;
14use sha2::{Digest, Sha256, Sha512};
15use tatara_lisp_eval::{Arity, EvalError, Interpreter, Value};
16
17use crate::script_ctx::ScriptCtx;
18use crate::stdlib::env::{int_arg, str_arg};
19
20type HmacSha256 = Hmac<Sha256>;
21
22pub fn install(interp: &mut Interpreter<ScriptCtx>) {
23    interp.register_fn(
24        "sha1",
25        Arity::Exact(1),
26        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
27            let s = str_arg(&args[0], "sha1", sp)?;
28            Ok(Value::Str(Arc::from(hex::encode(Sha1::digest(
29                s.as_bytes(),
30            )))))
31        },
32    );
33
34    interp.register_fn(
35        "sha512",
36        Arity::Exact(1),
37        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
38            let s = str_arg(&args[0], "sha512", sp)?;
39            Ok(Value::Str(Arc::from(hex::encode(Sha512::digest(
40                s.as_bytes(),
41            )))))
42        },
43    );
44
45    interp.register_fn(
46        "hmac-sha256",
47        Arity::Exact(2),
48        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
49            let key = str_arg(&args[0], "hmac-sha256", sp)?;
50            let msg = str_arg(&args[1], "hmac-sha256", sp)?;
51            let mut mac = HmacSha256::new_from_slice(key.as_bytes())
52                .map_err(|e| EvalError::native_fn("hmac-sha256", e.to_string(), sp))?;
53            mac.update(msg.as_bytes());
54            Ok(Value::Str(Arc::from(hex::encode(
55                mac.finalize().into_bytes(),
56            ))))
57        },
58    );
59
60    interp.register_fn(
61        "uuid-v4",
62        Arity::Exact(0),
63        |_args: &[Value], _ctx: &mut ScriptCtx, _sp| Ok(Value::Str(Arc::from(uuid_v4()))),
64    );
65
66    interp.register_fn(
67        "random-hex",
68        Arity::Exact(1),
69        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
70            let n = int_arg(&args[0], "random-hex", sp)?;
71            if n < 0 {
72                return Err(EvalError::native_fn(
73                    "random-hex",
74                    format!("need >= 0 bytes, got {n}"),
75                    sp,
76                ));
77            }
78            let bytes = random_bytes(n as usize);
79            Ok(Value::Str(Arc::from(hex::encode(bytes))))
80        },
81    );
82
83    interp.register_fn(
84        "random-int",
85        Arity::Exact(2),
86        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
87            let lo = int_arg(&args[0], "random-int", sp)?;
88            let hi = int_arg(&args[1], "random-int", sp)?;
89            if hi <= lo {
90                return Err(EvalError::native_fn(
91                    "random-int",
92                    format!("hi ({hi}) must be > lo ({lo})"),
93                    sp,
94                ));
95            }
96            let range = (hi - lo) as u64;
97            let r = {
98                let bytes = random_bytes(8);
99                let mut u = 0u64;
100                for b in bytes {
101                    u = (u << 8) | b as u64;
102                }
103                lo + (u % range) as i64
104            };
105            Ok(Value::Int(r))
106        },
107    );
108}
109
110fn random_bytes(n: usize) -> Vec<u8> {
111    // Mix /dev/urandom (when available) with a process-unique fallback.
112    let mut out = vec![0u8; n];
113    if let Ok(mut f) = std::fs::File::open("/dev/urandom") {
114        use std::io::Read;
115        if f.read_exact(&mut out).is_ok() {
116            return out;
117        }
118    }
119    // Fallback: weak (timestamp + pid + counter) — scripts should not rely
120    // on this, but it's better than zeros if /dev/urandom is absent.
121    let ts = std::time::SystemTime::now()
122        .duration_since(std::time::UNIX_EPOCH)
123        .map(|d| d.as_nanos() as u64)
124        .unwrap_or(0);
125    let pid = std::process::id() as u64;
126    for (i, b) in out.iter_mut().enumerate() {
127        let seed = ts
128            .wrapping_add(pid)
129            .wrapping_add(i as u64)
130            .wrapping_mul(2_862_933_555_777_941_757);
131        *b = (seed >> 32) as u8;
132    }
133    out
134}
135
136fn uuid_v4() -> String {
137    let mut bytes = random_bytes(16);
138    // v4 variant bits
139    bytes[6] = (bytes[6] & 0x0f) | 0x40;
140    bytes[8] = (bytes[8] & 0x3f) | 0x80;
141    format!(
142        "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
143        bytes[0], bytes[1], bytes[2], bytes[3],
144        bytes[4], bytes[5],
145        bytes[6], bytes[7],
146        bytes[8], bytes[9],
147        bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]
148    )
149}