Skip to main content

doge_runtime/stdlib/
crypto.rs

1//! `crypto` — the security-primitives stdlib module. A deliberately tiny,
2//! hard-to-misuse surface: `sha256` and `hmac_sha256` hash a `Str` or `Bytes` to a
3//! 32-byte `Bytes` digest, `token` draws cryptographically secure random `Bytes`
4//! from the OS CSPRNG (independent of the clock-seeded `roll` PRNG), and `same`
5//! compares two `Str`/`Bytes` in constant time so secrets never leak via timing.
6//! Wrong types are a catchable TypeError and a non-positive `token` length a
7//! catchable ValueError — never a panic.
8
9use hmac::{Hmac, Mac};
10use sha2::{Digest, Sha256};
11use subtle::ConstantTimeEq;
12
13use crate::error::{DogeError, DogeResult};
14use crate::stdlib::int_arg;
15use crate::value::Value;
16
17/// A `Str` (its UTF-8 bytes) or `Bytes` argument as `&[u8]`, or a catchable type
18/// error naming the member. Hashing treats text and raw bytes uniformly.
19fn str_or_bytes<'a>(fname: &str, v: &'a Value) -> DogeResult<&'a [u8]> {
20    match v {
21        Value::Str(s) => Ok(s.as_bytes()),
22        Value::Bytes(b) => Ok(b),
23        _ => Err(DogeError::type_error(format!(
24            "crypto.{fname} needs a Str or Bytes, got {}",
25            v.describe()
26        ))),
27    }
28}
29
30pub fn crypto_sha256(data: &Value) -> DogeResult {
31    let digest = Sha256::digest(str_or_bytes("sha256", data)?);
32    Ok(Value::bytes(digest))
33}
34
35pub fn crypto_hmac_sha256(key: &Value, data: &Value) -> DogeResult {
36    let key = str_or_bytes("hmac_sha256", key)?;
37    let data = str_or_bytes("hmac_sha256", data)?;
38    let mut mac = Hmac::<Sha256>::new_from_slice(key)
39        .map_err(|_| DogeError::value_error("crypto.hmac_sha256 could not build the HMAC key"))?;
40    mac.update(data);
41    Ok(Value::bytes(mac.finalize().into_bytes()))
42}
43
44pub fn crypto_token(n: &Value) -> DogeResult {
45    let n = int_arg("crypto", "token", n)?;
46    if n <= 0 {
47        return Err(DogeError::value_error(format!(
48            "crypto.token needs a positive length, got {n}"
49        )));
50    }
51    let mut buf = vec![0u8; n as usize];
52    getrandom::getrandom(&mut buf)
53        .map_err(|_| DogeError::io_error("crypto.token could not read the OS random source"))?;
54    Ok(Value::bytes(buf))
55}
56
57pub fn crypto_same(a: &Value, b: &Value) -> DogeResult {
58    let a = str_or_bytes("same", a)?;
59    let b = str_or_bytes("same", b)?;
60    let equal = a.len() == b.len() && bool::from(a.ct_eq(b));
61    Ok(Value::Bool(equal))
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use crate::error::ErrorKind;
68
69    fn hex(v: &Value) -> String {
70        match v {
71            Value::Bytes(b) => b.iter().map(|byte| format!("{byte:02x}")).collect(),
72            _ => panic!("expected Bytes"),
73        }
74    }
75
76    fn as_bool(v: &Value) -> bool {
77        match v {
78            Value::Bool(b) => *b,
79            _ => panic!("expected Bool"),
80        }
81    }
82
83    #[test]
84    fn sha256_matches_known_vector() {
85        let empty = crypto_sha256(&Value::str("")).unwrap();
86        assert_eq!(
87            hex(&empty),
88            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
89        );
90        let abc = crypto_sha256(&Value::str("abc")).unwrap();
91        assert_eq!(
92            hex(&abc),
93            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
94        );
95    }
96
97    #[test]
98    fn sha256_str_and_bytes_agree() {
99        let from_str = crypto_sha256(&Value::str("doge")).unwrap();
100        let from_bytes = crypto_sha256(&Value::bytes(b"doge")).unwrap();
101        assert_eq!(hex(&from_str), hex(&from_bytes));
102    }
103
104    #[test]
105    fn hmac_sha256_matches_rfc4231_vector() {
106        // RFC 4231 test case 2: key = "Jefe", data = "what do ya want for nothing?"
107        let mac = crypto_hmac_sha256(
108            &Value::str("Jefe"),
109            &Value::str("what do ya want for nothing?"),
110        )
111        .unwrap();
112        assert_eq!(
113            hex(&mac),
114            "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"
115        );
116    }
117
118    #[test]
119    fn token_has_requested_length_and_varies() {
120        let a = crypto_token(&Value::int(16)).unwrap();
121        let b = crypto_token(&Value::int(16)).unwrap();
122        match (&a, &b) {
123            (Value::Bytes(x), Value::Bytes(y)) => {
124                assert_eq!(x.len(), 16);
125                assert_eq!(y.len(), 16);
126                assert_ne!(x, y, "two secure draws must differ");
127            }
128            _ => panic!("expected Bytes"),
129        }
130    }
131
132    #[test]
133    fn token_rejects_non_positive_length() {
134        assert_eq!(
135            crypto_token(&Value::int(0)).unwrap_err().kind,
136            ErrorKind::ValueError
137        );
138        assert_eq!(
139            crypto_token(&Value::int(-1)).unwrap_err().kind,
140            ErrorKind::ValueError
141        );
142    }
143
144    #[test]
145    fn same_compares_content_and_accepts_str_or_bytes() {
146        assert!(as_bool(
147            &crypto_same(&Value::str("secret"), &Value::str("secret")).unwrap()
148        ));
149        assert!(!as_bool(
150            &crypto_same(&Value::str("secret"), &Value::str("secreT")).unwrap()
151        ));
152        assert!(!as_bool(
153            &crypto_same(&Value::str("ab"), &Value::str("abc")).unwrap()
154        ));
155        assert!(as_bool(
156            &crypto_same(&Value::str("hi"), &Value::bytes(b"hi")).unwrap()
157        ));
158    }
159
160    #[test]
161    fn wrong_types_are_type_errors() {
162        assert_eq!(
163            crypto_sha256(&Value::int(123)).unwrap_err().kind,
164            ErrorKind::TypeError
165        );
166        assert_eq!(
167            crypto_hmac_sha256(&Value::int(1), &Value::str("x"))
168                .unwrap_err()
169                .kind,
170            ErrorKind::TypeError
171        );
172        assert_eq!(
173            crypto_same(&Value::int(1), &Value::str("x"))
174                .unwrap_err()
175                .kind,
176            ErrorKind::TypeError
177        );
178        assert_eq!(
179            crypto_token(&Value::str("16")).unwrap_err().kind,
180            ErrorKind::TypeError
181        );
182    }
183}