Skip to main content

zoi_lua/api/
crypto.rs

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