Skip to main content

zoi_lua/api/
crypto.rs

1use mlua::{self, Lua, Value};
2use std::path::{Path, PathBuf};
3use zoi_core::utils;
4
5use colored::Colorize;
6use sequoia_openpgp::{Cert, parse::Parse};
7use std::fs;
8/// Exposes cryptographic utilities to the Lua environment.
9///
10/// These functions allow package scripts to perform security validations:
11/// - `verifyHash`: Checks a file's SHA-256 or SHA-512 integrity.
12/// - `verifySignature`: Validates a detached PGP signature using a local or remote key.
13/// - `addPgpKey`: Dynamically imports a trusted PGP key into the Zoi keyring.
14///
15/// This provides the "Chain of Trust" within the package build process, ensuring
16/// that downloaded assets have not been tampered with.
17pub fn add_verify_hash(lua: &Lua, quiet: bool) -> Result<(), mlua::Error> {
18    let verify_hash_fn = lua.create_function(move |lua, args: mlua::MultiValue| {
19        let mut args_iter = args.into_iter();
20        let file_path = match args_iter.next().unwrap_or(Value::Nil) {
21            Value::String(s) => s.to_str()?.to_string(),
22            v => {
23                return Err(mlua::Error::RuntimeError(format!(
24                    "verifyHash: first argument must be a string, got {}",
25                    v.type_name()
26                )));
27            }
28        };
29        let hash_str = match args_iter.next().unwrap_or(Value::Nil) {
30            Value::String(s) => s.to_str()?.to_string(),
31            v => {
32                return Err(mlua::Error::RuntimeError(format!(
33                    "verifyHash: second argument must be a string, got {}",
34                    v.type_name()
35                )));
36            }
37        };
38
39        let parts: Vec<&str> = hash_str.splitn(2, '-').collect();
40        if parts.len() != 2 {
41            return Err(mlua::Error::RuntimeError(
42                "Invalid hash format. Expected 'algo-hash'".to_string(),
43            ));
44        }
45        let algo = parts[0];
46        let expected_hash = parts[1];
47
48        let p = Path::new(&file_path);
49        let actual_path = if p.exists() {
50            p.to_path_buf()
51        } else if let Ok(build_dir) = lua.globals().get::<String>("BUILD_DIR") {
52            Path::new(&build_dir).join(p)
53        } else {
54            p.to_path_buf()
55        };
56
57        let hash_algo = match zoi_core::hash::HashAlgorithm::from_name(algo) {
58            Some(a) => a,
59            None => {
60                return Err(mlua::Error::RuntimeError(format!(
61                    "Unsupported hash algorithm: {}",
62                    algo
63                )));
64            }
65        };
66
67        let actual_hash = match zoi_core::hash::calculate_file_hash(&actual_path, hash_algo) {
68            Ok(h) => h,
69            Err(e) => {
70                return Err(mlua::Error::RuntimeError(format!(
71                    "Failed to calculate hash: {}",
72                    e
73                )));
74            }
75        };
76
77        if actual_hash.eq_ignore_ascii_case(expected_hash) {
78            Ok(true)
79        } else {
80            if !quiet {
81                println!(
82                    "\n{}: Hash mismatch for {}",
83                    "Error".red().bold(),
84                    file_path.cyan()
85                );
86                println!("  specified: {}-{}", algo, expected_hash.yellow());
87                println!("       got:    {}-{}", algo, actual_hash.green());
88            }
89            Ok(false)
90        }
91    })?;
92    lua.globals().set("verifyHash", verify_hash_fn)?;
93    Ok(())
94}
95
96pub fn add_verify_signature(lua: &Lua, quiet: bool) -> Result<(), mlua::Error> {
97    let verify_sig_fn = lua.create_function(move |lua, args: mlua::MultiValue| {
98        let mut args_iter = args.into_iter();
99        let file_path = match args_iter.next().unwrap_or(Value::Nil) {
100            Value::String(s) => s.to_str()?.to_string(),
101            v => {
102                return Err(mlua::Error::RuntimeError(format!(
103                    "verifySignature: first argument must be a string, got {}",
104                    v.type_name()
105                )));
106            }
107        };
108        let sig_path = match args_iter.next().unwrap_or(Value::Nil) {
109            Value::String(s) => s.to_str()?.to_string(),
110            v => {
111                return Err(mlua::Error::RuntimeError(format!(
112                    "verifySignature: second argument must be a string, got {}",
113                    v.type_name()
114                )));
115            }
116        };
117        let key_source = match args_iter.next().unwrap_or(Value::Nil) {
118            Value::String(s) => s.to_str()?.to_string(),
119            v => {
120                return Err(mlua::Error::RuntimeError(format!(
121                    "verifySignature: third argument must be a string, got {}",
122                    v.type_name()
123                )));
124            }
125        };
126
127        let resolve_path = |p_str: &str| -> PathBuf {
128            let p = Path::new(p_str);
129            if p.exists() {
130                p.to_path_buf()
131            } else if let Ok(build_dir) = lua.globals().get::<String>("BUILD_DIR") {
132                Path::new(&build_dir).join(p)
133            } else {
134                p.to_path_buf()
135            }
136        };
137
138        let key_bytes: Vec<u8> = if key_source.starts_with("http") {
139            let client =
140                utils::get_http_client().map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
141            match client.get(&key_source).send().and_then(|r| r.bytes()) {
142                Ok(b) => b.to_vec(),
143                Err(e) => {
144                    return Err(mlua::Error::RuntimeError(format!(
145                        "Failed to download key: {}",
146                        e
147                    )));
148                }
149            }
150        } else {
151            let resolved_key_path = resolve_path(&key_source);
152            if resolved_key_path.exists() {
153                match fs::read(&resolved_key_path) {
154                    Ok(b) => b,
155                    Err(e) => {
156                        return Err(mlua::Error::RuntimeError(format!(
157                            "Failed to read key file {:?}: {}",
158                            resolved_key_path, e
159                        )));
160                    }
161                }
162            } else {
163                let pgp_dir = match zoi_core::pgp::get_pgp_dir() {
164                    Ok(dir) => dir,
165                    Err(e) => {
166                        return Err(mlua::Error::RuntimeError(format!(
167                            "Failed to get PGP dir: {}",
168                            e
169                        )));
170                    }
171                };
172                let key_path = pgp_dir.join(format!("{}.asc", key_source));
173                if !key_path.exists() {
174                    return Err(mlua::Error::RuntimeError(format!(
175                        "Key with name '{}' not found (checked locally and at {:?}).",
176                        key_source, resolved_key_path
177                    )));
178                }
179                match fs::read(&key_path) {
180                    Ok(b) => b,
181                    Err(e) => {
182                        return Err(mlua::Error::RuntimeError(format!(
183                            "Failed to read key file {:?}: {}",
184                            key_path, e
185                        )));
186                    }
187                }
188            }
189        };
190
191        let cert = match Cert::from_bytes(&key_bytes) {
192            Ok(c) => c,
193            Err(e) => return Err(mlua::Error::RuntimeError(format!("Invalid PGP key: {}", e))),
194        };
195
196        let final_file_path = resolve_path(&file_path);
197        let final_sig_path = resolve_path(&sig_path);
198
199        let result =
200            zoi_core::pgp::verify_detached_signature(&final_file_path, &final_sig_path, &cert);
201
202        match result {
203            Ok(_) => Ok(true),
204            Err(e) => {
205                if !quiet {
206                    eprintln!("Signature verification failed: {}", e);
207                }
208                Ok(false)
209            }
210        }
211    })?;
212    lua.globals().set("verifySignature", verify_sig_fn)?;
213    Ok(())
214}
215
216pub fn add_add_pgp_key(lua: &Lua, quiet: bool) -> Result<(), mlua::Error> {
217    let add_pgp_key_fn = lua.create_function(move |lua, args: mlua::MultiValue| {
218        let mut args_iter = args.into_iter();
219        let source = match args_iter.next().unwrap_or(Value::Nil) {
220            Value::String(s) => s.to_str()?.to_string(),
221            v => {
222                return Err(mlua::Error::RuntimeError(format!(
223                    "addPgpKey: first argument must be a string, got {}",
224                    v.type_name()
225                )));
226            }
227        };
228        let name = match args_iter.next().unwrap_or(Value::Nil) {
229            Value::String(s) => s.to_str()?.to_string(),
230            v => {
231                return Err(mlua::Error::RuntimeError(format!(
232                    "addPgpKey: second argument must be a string, got {}",
233                    v.type_name()
234                )));
235            }
236        };
237
238        let result = if source.starts_with("http") {
239            zoi_core::pgp::add_key_from_url(&source, &name, quiet)
240        } else {
241            let p = Path::new(&source);
242            let actual_path = if p.exists() {
243                p.to_path_buf()
244            } else if let Ok(build_dir) = lua.globals().get::<String>("BUILD_DIR") {
245                Path::new(&build_dir).join(p)
246            } else {
247                p.to_path_buf()
248            };
249            zoi_core::pgp::add_key_from_path(
250                actual_path.to_str().unwrap_or(&source),
251                Some(&name),
252                quiet,
253            )
254        };
255
256        if let Err(e) = result {
257            if !quiet {
258                eprintln!("Failed to add PGP key '{}': {}", name, e);
259            }
260            return Ok(false);
261        }
262        Ok(true)
263    })?;
264    lua.globals().set("addPgpKey", add_pgp_key_fn)?;
265    Ok(())
266}