|
| 1 | +//! Secret and identifier generators: `uuidgen`, `openssl rand`. |
| 2 | +//! |
| 3 | +//! Decisions: |
| 4 | +//! - Bytes come from the OS CSPRNG via `getrandom` (same source as |
| 5 | +//! `/dev/urandom`), never from the `$RANDOM` LCG: these are used for |
| 6 | +//! tokens, keys and salts. |
| 7 | +//! - `openssl` implements only the `rand` subcommand; anything else fails |
| 8 | +//! with openssl's own "Invalid command" message so scripts can fall back. |
| 9 | +//! - THREAT[TM-DOS-123]: `openssl rand N` is capped at |
| 10 | +//! `OPENSSL_RAND_MAX_BYTES` so one call cannot allocate unbounded memory. |
| 11 | +//! - `uuidgen` emits random (v4) UUIDs; time-based `-t` and name-based |
| 12 | +//! `-m`/`-s` are not supported (no host clock/MAC leakage, L-RAND-001). |
| 13 | +
|
| 14 | +use async_trait::async_trait; |
| 15 | +use base64::Engine; |
| 16 | + |
| 17 | +use super::{Builtin, Context, resolve_path}; |
| 18 | +use crate::error::Result; |
| 19 | +use crate::interpreter::ExecResult; |
| 20 | + |
| 21 | +/// Largest `openssl rand` request (1 MiB). |
| 22 | +pub(crate) const OPENSSL_RAND_MAX_BYTES: usize = 1024 * 1024; |
| 23 | + |
| 24 | +fn random_bytes(n: usize) -> std::result::Result<Vec<u8>, String> { |
| 25 | + let mut buf = vec![0u8; n]; |
| 26 | + getrandom::fill(&mut buf).map_err(|_| "random source unavailable".to_string())?; |
| 27 | + Ok(buf) |
| 28 | +} |
| 29 | + |
| 30 | +/// A random version 4 UUID in lowercase hyphenated form. |
| 31 | +pub(crate) fn uuid_v4() -> std::result::Result<String, String> { |
| 32 | + let mut b = random_bytes(16)?; |
| 33 | + b[6] = (b[6] & 0x0f) | 0x40; |
| 34 | + b[8] = (b[8] & 0x3f) | 0x80; |
| 35 | + let h: String = b.iter().map(|x| format!("{x:02x}")).collect(); |
| 36 | + Ok(format!( |
| 37 | + "{}-{}-{}-{}-{}", |
| 38 | + &h[0..8], |
| 39 | + &h[8..12], |
| 40 | + &h[12..16], |
| 41 | + &h[16..20], |
| 42 | + &h[20..32] |
| 43 | + )) |
| 44 | +} |
| 45 | + |
| 46 | +/// The `uuidgen` builtin: `uuidgen [-r|--random]`. |
| 47 | +pub struct Uuidgen; |
| 48 | + |
| 49 | +#[async_trait] |
| 50 | +impl Builtin for Uuidgen { |
| 51 | + async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> { |
| 52 | + for arg in ctx.args { |
| 53 | + match arg.as_str() { |
| 54 | + "-r" | "--random" => {} |
| 55 | + "-h" | "--help" => { |
| 56 | + return Ok(ExecResult::ok( |
| 57 | + "Usage: uuidgen [-r|--random]\nCreate a new random (version 4) UUID.\n", |
| 58 | + )); |
| 59 | + } |
| 60 | + "-t" | "--time" | "-m" | "--md5" | "-s" | "--sha1" => { |
| 61 | + return Ok(ExecResult::err( |
| 62 | + format!("uuidgen: {arg}: not supported in bashkit (random UUIDs only)\n"), |
| 63 | + 1, |
| 64 | + )); |
| 65 | + } |
| 66 | + other => { |
| 67 | + return Ok(ExecResult::err( |
| 68 | + format!( |
| 69 | + "uuidgen: invalid option -- '{}'\n", |
| 70 | + other.trim_start_matches('-') |
| 71 | + ), |
| 72 | + 1, |
| 73 | + )); |
| 74 | + } |
| 75 | + } |
| 76 | + } |
| 77 | + match uuid_v4() { |
| 78 | + Ok(u) => Ok(ExecResult::ok(format!("{u}\n"))), |
| 79 | + Err(e) => Ok(ExecResult::err(format!("uuidgen: {e}\n"), 1)), |
| 80 | + } |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +/// The `openssl` builtin: only `openssl rand [-hex|-base64] [-out FILE] NUM`. |
| 85 | +pub struct Openssl; |
| 86 | + |
| 87 | +const RAND_USAGE: &str = "rand: Use -help for summary.\n"; |
| 88 | + |
| 89 | +#[async_trait] |
| 90 | +impl Builtin for Openssl { |
| 91 | + async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> { |
| 92 | + let Some((cmd, rest)) = ctx.args.split_first() else { |
| 93 | + return Ok(ExecResult::err( |
| 94 | + "openssl: interactive mode is not supported in bashkit\n", |
| 95 | + 1, |
| 96 | + )); |
| 97 | + }; |
| 98 | + if cmd != "rand" { |
| 99 | + return Ok(ExecResult::err( |
| 100 | + format!("Invalid command '{cmd}'; type \"help\" for a list.\n"), |
| 101 | + 1, |
| 102 | + )); |
| 103 | + } |
| 104 | + let mut hex = false; |
| 105 | + let mut b64 = false; |
| 106 | + let mut out: Option<&str> = None; |
| 107 | + let mut num: Option<&str> = None; |
| 108 | + let mut i = 0; |
| 109 | + while i < rest.len() { |
| 110 | + match rest[i].as_str() { |
| 111 | + "-hex" => hex = true, |
| 112 | + "-base64" => b64 = true, |
| 113 | + "-out" => { |
| 114 | + i += 1; |
| 115 | + match rest.get(i) { |
| 116 | + Some(f) => out = Some(f), |
| 117 | + None => return Ok(ExecResult::err(RAND_USAGE, 1)), |
| 118 | + } |
| 119 | + } |
| 120 | + "-help" => { |
| 121 | + return Ok(ExecResult::ok( |
| 122 | + "Usage: rand [options] num\n -out outfile Output file\n -base64 Base64 encode output\n -hex Hex encode output\n", |
| 123 | + )); |
| 124 | + } |
| 125 | + a if num.is_none() && !a.starts_with('-') => num = Some(a), |
| 126 | + a => { |
| 127 | + return Ok(ExecResult::err( |
| 128 | + format!("rand: Unknown option or number: {a}\n{RAND_USAGE}"), |
| 129 | + 1, |
| 130 | + )); |
| 131 | + } |
| 132 | + } |
| 133 | + i += 1; |
| 134 | + } |
| 135 | + let Some(num) = num else { |
| 136 | + return Ok(ExecResult::err(RAND_USAGE, 1)); |
| 137 | + }; |
| 138 | + let n: usize = match num.parse() { |
| 139 | + Ok(n) => n, |
| 140 | + Err(_) => { |
| 141 | + return Ok(ExecResult::err( |
| 142 | + format!("rand: Can't parse \"{num}\" as a number\n{RAND_USAGE}"), |
| 143 | + 1, |
| 144 | + )); |
| 145 | + } |
| 146 | + }; |
| 147 | + if n > OPENSSL_RAND_MAX_BYTES { |
| 148 | + return Ok(ExecResult::err( |
| 149 | + format!("rand: {n} bytes exceeds the {OPENSSL_RAND_MAX_BYTES} byte limit\n"), |
| 150 | + 1, |
| 151 | + )); |
| 152 | + } |
| 153 | + let bytes = match random_bytes(n) { |
| 154 | + Ok(b) => b, |
| 155 | + Err(e) => return Ok(ExecResult::err(format!("rand: {e}\n"), 1)), |
| 156 | + }; |
| 157 | + let data: Vec<u8> = if n == 0 { |
| 158 | + Vec::new() |
| 159 | + } else if hex { |
| 160 | + let mut s: String = bytes.iter().map(|x| format!("{x:02x}")).collect(); |
| 161 | + s.push('\n'); |
| 162 | + s.into_bytes() |
| 163 | + } else if b64 { |
| 164 | + let enc = base64::engine::general_purpose::STANDARD.encode(&bytes); |
| 165 | + // openssl wraps base64 at 64 columns. |
| 166 | + let mut s = String::with_capacity(enc.len() + enc.len() / 64 + 1); |
| 167 | + for chunk in enc.as_bytes().chunks(64) { |
| 168 | + s.push_str(&String::from_utf8_lossy(chunk)); |
| 169 | + s.push('\n'); |
| 170 | + } |
| 171 | + s.into_bytes() |
| 172 | + } else { |
| 173 | + bytes |
| 174 | + }; |
| 175 | + match out { |
| 176 | + Some(f) => { |
| 177 | + let path = resolve_path(ctx.cwd, f); |
| 178 | + if let Err(e) = ctx.fs.write_file(&path, &data).await { |
| 179 | + return Ok(ExecResult::err(format!("rand: {f}: {e}\n"), 1)); |
| 180 | + } |
| 181 | + Ok(ExecResult::ok(String::new())) |
| 182 | + } |
| 183 | + None => Ok(ExecResult::ok(crate::StreamData::from(data))), |
| 184 | + } |
| 185 | + } |
| 186 | +} |
| 187 | + |
| 188 | +#[cfg(test)] |
| 189 | +mod tests { |
| 190 | + use super::*; |
| 191 | + |
| 192 | + #[test] |
| 193 | + fn uuid_v4_shape() { |
| 194 | + let u = uuid_v4().unwrap(); |
| 195 | + assert_eq!(u.len(), 36); |
| 196 | + assert_eq!(&u[14..15], "4"); |
| 197 | + assert!(matches!(&u[19..20], "8" | "9" | "a" | "b")); |
| 198 | + assert_ne!(u, uuid_v4().unwrap()); |
| 199 | + } |
| 200 | + |
| 201 | + #[tokio::test] |
| 202 | + async fn openssl_rand_limits() { |
| 203 | + let mut bash = crate::Bash::new(); |
| 204 | + let r = bash.exec("openssl rand -hex 2000000").await.unwrap(); |
| 205 | + assert_eq!(r.exit_code, 1); |
| 206 | + assert!(r.stderr.contains("limit")); |
| 207 | + let r = bash.exec("uuidgen -t").await.unwrap(); |
| 208 | + assert_eq!(r.exit_code, 1); |
| 209 | + } |
| 210 | +} |
0 commit comments