Skip to main content

oauth_toolkit/
crypto.rs

1//! Cryptographic helpers for OAuth2 tokens and secrets.
2
3use hmac::{Hmac, Mac};
4use rand::Rng;
5use sha2::{Digest, Sha256};
6
7type HmacSha256 = Hmac<Sha256>;
8
9/// SHA-256 hex digest of a raw value (unkeyed).
10///
11/// Used for hashing bearer tokens and authorization codes at rest.
12pub fn sha256_hex(value: &str) -> String {
13    let hash = Sha256::digest(value.as_bytes());
14    hex::encode(hash)
15}
16
17/// HMAC-SHA256 keyed hash of a value.
18///
19/// Used for hashing client secrets at registration time.
20pub fn hmac_sha256_hex(key: &str, value: &str) -> String {
21    let mut mac = HmacSha256::new_from_slice(key.as_bytes())
22        .expect("HMAC accepts any key length");
23    mac.update(value.as_bytes());
24    hex::encode(mac.finalize().into_bytes())
25}
26
27/// Verify an HMAC-SHA256 signature (constant-time comparison).
28pub fn hmac_sha256_verify(key: &str, value: &str, signature_hex: &str) -> Result<(), CryptoError> {
29    let mut mac = HmacSha256::new_from_slice(key.as_bytes())
30        .map_err(|e| CryptoError::InvalidKey(e.to_string()))?;
31    mac.update(value.as_bytes());
32    let expected = hex::decode(signature_hex)
33        .map_err(|e| CryptoError::InvalidHex(e.to_string()))?;
34    mac.verify_slice(&expected)
35        .map_err(|_| CryptoError::SignatureMismatch)
36}
37
38/// Generate a cryptographically secure random token with a configurable prefix.
39///
40/// Returns "{prefix}{random_hex}" where random_hex has `2 * byte_length` hex characters.
41pub fn generate_opaque_token(prefix: &str, byte_length: usize) -> String {
42    let mut bytes = vec![0u8; byte_length];
43    rand::rng().fill(&mut bytes[..]);
44    format!("{}{}", prefix, hex::encode(bytes))
45}
46
47/// Generate a hex-encoded random state nonce for CSRF / PKCE state.
48pub fn generate_state_nonce(byte_length: usize) -> String {
49    generate_opaque_token("", byte_length)
50}
51
52/// Errors for cryptographic operations.
53#[derive(Debug, thiserror::Error)]
54pub enum CryptoError {
55    /// Invalid key for HMAC operation.
56    #[error("invalid key: {0}")]
57    InvalidKey(String),
58
59    /// Invalid hex encoding.
60    #[error("invalid hex: {0}")]
61    InvalidHex(String),
62
63    /// Signature does not match (constant-time comparison).
64    #[error("signature mismatch")]
65    SignatureMismatch,
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn sha256_hex_deterministic() {
74        let h1 = sha256_hex("hello");
75        let h2 = sha256_hex("hello");
76        assert_eq!(h1, h2);
77        assert_eq!(h1.len(), 64);
78    }
79
80    #[test]
81    fn sha256_hex_different_inputs() {
82        assert_ne!(sha256_hex("hello"), sha256_hex("world"));
83    }
84
85    #[test]
86    fn hmac_roundtrip() {
87        let sig = hmac_sha256_hex("secret-key", "test-message");
88        assert!(hmac_sha256_verify("secret-key", "test-message", &sig).is_ok());
89    }
90
91    #[test]
92    fn hmac_wrong_key_fails() {
93        let sig = hmac_sha256_hex("key1", "message");
94        assert!(hmac_sha256_verify("key2", "message", &sig).is_err());
95    }
96
97    #[test]
98    fn hmac_wrong_message_fails() {
99        let sig = hmac_sha256_hex("key", "msg1");
100        assert!(hmac_sha256_verify("key", "msg2", &sig).is_err());
101    }
102
103    #[test]
104    fn generate_opaque_token_prefix() {
105        let token = generate_opaque_token("tok_", 16);
106        assert!(token.starts_with("tok_"));
107        assert_eq!(token.len(), 4 + 32);
108    }
109
110    #[test]
111    fn generate_state_nonce_length() {
112        let nonce = generate_state_nonce(16);
113        assert_eq!(nonce.len(), 32);
114    }
115
116    #[test]
117    fn hmac_invalid_hex_fails() {
118        assert!(hmac_sha256_verify("key", "msg", "not-hex").is_err());
119    }
120}