Skip to main content

krabka_security/
delegation_token.rs

1//! KIP-48 delegation token primitives: the HMAC and a secret-key wrapper that
2//! keeps the bytes out of Debug.
3
4use std::fmt;
5
6use bytes::Bytes;
7use ring::hmac;
8
9#[derive(Clone, PartialEq, Eq)]
10pub struct SecretBytes(Bytes);
11
12impl SecretBytes {
13    #[must_use]
14    pub fn new(bytes: impl Into<Bytes>) -> Self {
15        Self(bytes.into())
16    }
17    #[must_use]
18    pub fn as_bytes(&self) -> &[u8] {
19        &self.0
20    }
21    #[must_use]
22    pub fn len(&self) -> usize {
23        self.0.len()
24    }
25    #[must_use]
26    pub fn is_empty(&self) -> bool {
27        self.0.is_empty()
28    }
29}
30
31impl fmt::Debug for SecretBytes {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        write!(f, "SecretBytes(<{} bytes redacted>)", self.0.len())
34    }
35}
36
37/// A delegation token's HMAC: `HmacSHA512` over the token id's UTF-8 bytes.
38///
39/// The HMAC is keyed with the secret key's bytes, as Kafka's
40/// `DelegationTokenManager.createHmac` computes it. The result is 64 bytes,
41/// and its base64 is the SCRAM password a client authenticates with. HMAC
42/// accepts a key of any length, so this cannot fail.
43#[must_use]
44pub fn compute_token_hmac(secret_key: &[u8], token_id: &str) -> Vec<u8> {
45    let key = hmac::Key::new(hmac::HMAC_SHA512, secret_key);
46    hmac::sign(&key, token_id.as_bytes()).as_ref().to_vec()
47}
48
49#[cfg(test)]
50mod tests {
51    use assert2::check;
52
53    use super::*;
54
55    #[test]
56    fn secret_bytes_accessors() {
57        let s = SecretBytes::new(Bytes::from_static(b"abc"));
58        check!(
59            (
60                s.as_bytes(),
61                s.len(),
62                s.is_empty(),
63                SecretBytes::new(Bytes::new()).is_empty(),
64            ) == (b"abc".as_slice(), 3, false, true)
65        );
66    }
67
68    /// The expected bytes are `printf tok-1 | openssl dgst -sha512 -hmac k`,
69    /// which is what Kafka's `Mac.getInstance("HmacSHA512")` produces.
70    #[test]
71    fn hmac_is_hmac_sha512_of_the_token_id() {
72        let expected = hex::decode(
73            "0e527131aafda6c68f4161e8593b8e6eb591403471ef360854b63c883f5a3247\
74             fde5b7720739ea603cb7226cd209ab14718a27b4d3c1b384da9beb197ee7cb3c",
75        )
76        .expect("valid hex");
77        assert2::assert!(compute_token_hmac(b"k", "tok-1") == expected);
78    }
79
80    #[test]
81    fn hmac_diverges_on_key_change() {
82        let h1 = compute_token_hmac(b"k1", "tok-1");
83        let h2 = compute_token_hmac(b"k2", "tok-1");
84        assert2::assert!(h1 != h2);
85    }
86
87    #[test]
88    fn secret_bytes_debug_does_not_leak_bytes() {
89        let s = SecretBytes::new(b"super-secret-master-key".to_vec());
90        let d = format!("{s:?}");
91        assert2::assert!(d.contains("redacted"));
92        assert2::assert!(!d.contains("super-secret"));
93    }
94}