Skip to main content

isb_apps/template/
generate.rs

1//! Values for generated template variables: passwords, keys, ids, ports,
2//! timestamps and signed JWTs. Randomness comes from the OS (ring).
3
4use base64::Engine;
5use ring::rand::{SecureRandom, SystemRandom};
6
7fn random_bytes(n: usize) -> Vec<u8> {
8    let mut b = vec![0u8; n];
9    SystemRandom::new()
10        .fill(&mut b)
11        .expect("the OS random number generator failed");
12    b
13}
14
15/// `n` characters drawn uniformly from `alphabet` (rejection sampling, so
16/// no character is likelier than another).
17fn from_alphabet(alphabet: &[u8], n: usize) -> String {
18    let limit = 256 - (256 % alphabet.len());
19    let mut out = String::with_capacity(n);
20    while out.len() < n {
21        for b in random_bytes(n * 2) {
22            if (b as usize) < limit && out.len() < n {
23                out.push(alphabet[b as usize % alphabet.len()] as char);
24            }
25        }
26    }
27    out
28}
29
30/// Letters and digits only, so the value is safe in URLs, shells and
31/// connection strings.
32pub fn password(len: u32) -> String {
33    from_alphabet(
34        b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
35        len as usize,
36    )
37}
38
39/// `bytes` random bytes, standard base64 with padding.
40pub fn base64(bytes: u32) -> String {
41    base64::engine::general_purpose::STANDARD.encode(random_bytes(bytes as usize))
42}
43
44/// `bytes` random bytes as lowercase hex.
45pub fn hex(bytes: u32) -> String {
46    random_bytes(bytes as usize)
47        .iter()
48        .map(|b| format!("{b:02x}"))
49        .collect()
50}
51
52/// A random (version 4) UUID.
53pub fn uuid() -> String {
54    let mut b = random_bytes(16);
55    b[6] = (b[6] & 0x0f) | 0x40;
56    b[8] = (b[8] & 0x3f) | 0x80;
57    let h: String = b.iter().map(|x| format!("{x:02x}")).collect();
58    format!(
59        "{}-{}-{}-{}-{}",
60        &h[0..8],
61        &h[8..12],
62        &h[12..16],
63        &h[16..20],
64        &h[20..32]
65    )
66}
67
68/// Lowercase letters, starting a name.
69pub fn username(len: u32) -> String {
70    from_alphabet(b"abcdefghijklmnopqrstuvwxyz", len.max(1) as usize)
71}
72
73/// A free TCP port on this host in 20000-39999 (bound and released, so it
74/// was free a moment ago).
75pub fn free_port() -> Option<u16> {
76    for _ in 0..200 {
77        let b = random_bytes(2);
78        let p = 20000 + (u16::from_le_bytes([b[0], b[1]]) % 20000);
79        if std::net::TcpListener::bind(("0.0.0.0", p)).is_ok() {
80            return Some(p);
81        }
82    }
83    None
84}
85
86pub fn now_secs() -> i64 {
87    std::time::SystemTime::now()
88        .duration_since(std::time::UNIX_EPOCH)
89        .map(|d| d.as_secs() as i64)
90        .unwrap_or(0)
91}
92
93/// `YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SS[Z]` (UTC) as unix seconds.
94pub fn parse_date(s: &str) -> Option<i64> {
95    let s = s.trim().trim_end_matches('Z');
96    let (date, time) = s.split_once(['T', ' ']).unwrap_or((s, "00:00:00"));
97    let d: Vec<i64> = date
98        .split('-')
99        .map(|p| p.parse().ok())
100        .collect::<Option<_>>()?;
101    let t: Vec<i64> = time
102        .split('.')
103        .next()?
104        .split(':')
105        .map(|p| p.parse().ok())
106        .collect::<Option<_>>()?;
107    if d.len() != 3 || t.is_empty() || t.len() > 3 {
108        return None;
109    }
110    let (y, m, day) = (d[0], d[1], d[2]);
111    if !(1..=12).contains(&m) || !(1..=31).contains(&day) {
112        return None;
113    }
114    // Days from the civil date (Howard Hinnant's algorithm).
115    let y2 = if m <= 2 { y - 1 } else { y };
116    let era = if y2 >= 0 { y2 } else { y2 - 399 } / 400;
117    let yoe = y2 - era * 400;
118    let mp = (m + 9) % 12;
119    let doy = (153 * mp + 2) / 5 + day - 1;
120    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
121    let days = era * 146097 + doe - 719468;
122    let secs = t[0] * 3600 + t.get(1).unwrap_or(&0) * 60 + t.get(2).unwrap_or(&0);
123    Some(days * 86400 + secs)
124}
125
126/// An HS256 JWT over `payload` (a JSON object), signed with `secret`.
127pub fn jwt(secret: &str, payload: &serde_json::Value) -> String {
128    let enc = |b: &[u8]| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b);
129    let header = enc(br#"{"alg":"HS256","typ":"JWT"}"#);
130    let body = enc(payload.to_string().as_bytes());
131    let input = format!("{header}.{body}");
132    let key = ring::hmac::Key::new(ring::hmac::HMAC_SHA256, secret.as_bytes());
133    let sig = ring::hmac::sign(&key, input.as_bytes());
134    format!("{input}.{}", enc(sig.as_ref()))
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn shapes() {
143        let p = password(32);
144        assert_eq!(p.len(), 32);
145        assert!(p.chars().all(|c| c.is_ascii_alphanumeric()));
146        assert_ne!(password(32), p);
147        let b = base64(48);
148        assert_eq!(
149            base64::engine::general_purpose::STANDARD
150                .decode(&b)
151                .unwrap()
152                .len(),
153            48
154        );
155        assert_eq!(hex(16).len(), 32);
156        let u = uuid();
157        assert_eq!(u.len(), 36);
158        assert_eq!(&u[14..15], "4");
159        assert!("89ab".contains(&u[19..20]));
160        assert!(username(8).chars().all(|c| c.is_ascii_lowercase()));
161        let port = free_port().unwrap();
162        assert!((20000..40000).contains(&port));
163    }
164
165    #[test]
166    fn dates() {
167        assert_eq!(parse_date("1970-01-01T00:00:00Z"), Some(0));
168        assert_eq!(parse_date("2030-01-01T00:00:00Z"), Some(1893456000));
169        assert_eq!(parse_date("2000-03-01"), Some(951868800));
170        assert_eq!(parse_date("tomorrow"), None);
171    }
172
173    #[test]
174    fn jwt_is_hs256() {
175        // The example from RFC 7515 appendix A.1's shape, checked against
176        // an independently computed signature.
177        let t = jwt("secret", &serde_json::json!({"sub": "1"}));
178        let parts: Vec<&str> = t.split('.').collect();
179        assert_eq!(parts.len(), 3);
180        assert_eq!(parts[0], "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9");
181        assert_eq!(parts[1], "eyJzdWIiOiIxIn0");
182        let key = ring::hmac::Key::new(ring::hmac::HMAC_SHA256, b"secret");
183        let sig = base64::engine::general_purpose::URL_SAFE_NO_PAD
184            .decode(parts[2])
185            .unwrap();
186        ring::hmac::verify(&key, format!("{}.{}", parts[0], parts[1]).as_bytes(), &sig).unwrap();
187    }
188}