actr_protocol/turn/
claims.rs

1//! TURN username claims.
2//!
3//! Claims are serialized into the TURN username and carry a base122-encoded
4//! binary payload containing realm_id, key_id, and encrypted token.
5
6use anyhow::Result;
7use bytes::{Buf as _, Bytes};
8
9/// TURN authentication claims.
10#[derive(Debug, Clone)]
11pub struct Claims {
12    /// Realm identifier.
13    pub realm_id: u32,
14
15    /// Key identifier used by the TURN auth cache.
16    pub key_id: u32,
17    /// Encrypted token.
18    pub token: Bytes,
19}
20
21impl Claims {
22    pub fn new(realm_id: u32, key_id: u32, token: Bytes) -> Self {
23        Self {
24            realm_id,
25            key_id,
26            token,
27        }
28    }
29
30    /// Encodes Claims into base122 string for TURN server compatibility
31    ///
32    /// TURN servers only accept String usernames, so we encode the binary data:
33    ///
34    /// ```text
35    ///     Byte Layout (ASCII Table):
36    ///     +----------+----------+-------------------+
37    ///     | Byte 0-3 | Byte 4-7 |     Byte 8+       |
38    ///     +----------+----------+-------------------+
39    ///     | realm_id |  key_id  |      token        |
40    ///     | (u32 BE) | (u32 BE) | (variable len)    |
41    ///     +----------+----------+-------------------+
42    /// ```
43    ///
44    /// The bytes are then encoded with base122 to create a valid String
45    /// for TURN protocol compatibility.
46    pub fn encode(&self) -> String {
47        let mut buffer = Vec::new();
48
49        // realm_id: 4 bytes (big endian)
50        buffer.extend_from_slice(&self.realm_id.to_be_bytes());
51
52        // key_id: 4 bytes (big endian)
53        buffer.extend_from_slice(&self.key_id.to_be_bytes());
54
55        // token: variable length bytes
56        buffer.extend_from_slice(&self.token);
57
58        // Encode to base122 string for TURN server compatibility
59        base122::encode(&buffer)
60    }
61
62    /// Decodes base122 string back to Claims using bytes::Buf trait
63    ///
64    /// ```text
65    ///     Byte Layout (ASCII Table):
66    ///     +----------+----------+-------------------+
67    ///     | Byte 0-3 | Byte 4-7 |     Byte 8+       |
68    ///     +----------+----------+-------------------+
69    ///     | realm_id |  key_id  |      token        |
70    ///     | (u32 BE) | (u32 BE) | (variable len)    |
71    ///     +----------+----------+-------------------+
72    /// ```
73    pub fn decode(s: &str) -> Result<Self> {
74        // Decode from base122 string
75        let bytes =
76            base122::decode(s).map_err(|e| anyhow::anyhow!("Failed to decode base122: {}", e))?;
77
78        // Minimum length check: 4 + 4 = 8 bytes
79        if bytes.len() < 8 {
80            return Err(anyhow::anyhow!("Invalid username length: too short"));
81        }
82
83        let mut buf = bytes.as_slice();
84
85        // Read realm_id: 4 bytes (big endian)
86        if buf.remaining() < 4 {
87            return Err(anyhow::anyhow!(
88                "Invalid format: insufficient bytes for realm_id"
89            ));
90        }
91        let realm_id = buf.get_u32();
92
93        // Read key_id: 4 bytes (big endian)
94        if buf.remaining() < 4 {
95            return Err(anyhow::anyhow!(
96                "Invalid format: insufficient bytes for key_id"
97            ));
98        }
99        let key_id = buf.get_u32();
100
101        // Read token: remaining bytes
102        let token = Bytes::from(buf.to_vec());
103
104        Ok(Claims {
105            realm_id,
106            key_id,
107            token,
108        })
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_claims_new() {
118        let token_bytes = Bytes::from(b"test_token".as_slice());
119        let claims = Claims::new(123, 456, token_bytes.clone());
120
121        assert_eq!(claims.realm_id, 123);
122        assert_eq!(claims.key_id, 456);
123        assert_eq!(claims.token, token_bytes);
124    }
125
126    #[test]
127    fn test_claims_encode_decode() {
128        let token_bytes = Bytes::from(b"test_token_data".as_slice());
129        let claims = Claims::new(12345, 67890, token_bytes.clone());
130
131        // Encode
132        let encoded = claims.encode();
133        assert!(!encoded.is_empty());
134
135        // Decode
136        let decoded = Claims::decode(&encoded).expect("Failed to decode");
137        assert_eq!(decoded.realm_id, 12345);
138        assert_eq!(decoded.key_id, 67890);
139        assert_eq!(decoded.token, token_bytes);
140    }
141
142    #[test]
143    fn test_claims_encode_decode_empty_token() {
144        let token_bytes = Bytes::from(b"".as_slice());
145        let claims = Claims::new(1, 2, token_bytes.clone());
146
147        let encoded = claims.encode();
148        let decoded = Claims::decode(&encoded).expect("Failed to decode");
149
150        assert_eq!(decoded.realm_id, 1);
151        assert_eq!(decoded.key_id, 2);
152        assert_eq!(decoded.token, token_bytes);
153    }
154
155    #[test]
156    fn test_claims_encode_decode_large_values() {
157        let token_bytes = Bytes::from(vec![0x01, 0x02, 0x03, 0x04, 0x05]);
158        let claims = Claims::new(u32::MAX, u32::MAX, token_bytes.clone());
159
160        let encoded = claims.encode();
161        let decoded = Claims::decode(&encoded).expect("Failed to decode");
162
163        assert_eq!(decoded.realm_id, u32::MAX);
164        assert_eq!(decoded.key_id, u32::MAX);
165        assert_eq!(decoded.token, token_bytes);
166    }
167
168    #[test]
169    fn test_claims_decode_invalid_short_string() {
170        // Create a base122 string that's too short (less than 8 bytes when decoded)
171        let short_encoded = base122::encode(&[1, 2, 3, 4, 5, 6, 7]); // Only 7 bytes
172
173        let result = Claims::decode(&short_encoded);
174        assert!(result.is_err());
175        assert!(
176            result
177                .unwrap_err()
178                .to_string()
179                .contains("Invalid username length")
180        );
181    }
182
183    #[test]
184    fn test_claims_decode_invalid_base122() {
185        // Test with a string that might decode but produces invalid data
186        // base122-rs might decode some invalid strings, so we test with a string
187        // that decodes to less than 8 bytes
188        let result = Claims::decode("a"); // Very short string that might decode to < 8 bytes
189        match result {
190            Ok(decoded) => {
191                // If it decodes successfully, verify it fails our validation
192                // This should not happen if our validation works, but if base122 decodes it,
193                // our length check should catch it
194                assert!(decoded.token.len() < 1000); // Just verify it doesn't panic
195            }
196            Err(e) => {
197                // Expected: should fail validation
198                let err_msg = e.to_string();
199                assert!(
200                    err_msg.contains("Invalid username length")
201                        || err_msg.contains("Failed to decode base122")
202                        || err_msg.contains("insufficient bytes"),
203                    "Unexpected error message: {}",
204                    err_msg
205                );
206            }
207        }
208    }
209
210    #[test]
211    fn test_claims_encode_decode_roundtrip_multiple() {
212        let test_cases = vec![
213            (0u32, 0u32, b"".as_slice()),
214            (1u32, 1u32, b"a".as_slice()),
215            (100u32, 200u32, b"hello".as_slice()),
216            (999u32, 888u32, b"world".as_slice()),
217            (12345u32, 67890u32, b"test_token_data_12345".as_slice()),
218        ];
219
220        for (realm_id, key_id, token_data) in test_cases {
221            let token_bytes = Bytes::from(token_data);
222            let claims = Claims::new(realm_id, key_id, token_bytes.clone());
223
224            let encoded = claims.encode();
225            let decoded = Claims::decode(&encoded).expect("Failed to decode");
226
227            assert_eq!(
228                decoded.realm_id, realm_id,
229                "realm_id mismatch for test case: realm_id={}, key_id={}",
230                realm_id, key_id
231            );
232            assert_eq!(
233                decoded.key_id, key_id,
234                "key_id mismatch for test case: realm_id={}, key_id={}",
235                realm_id, key_id
236            );
237            assert_eq!(
238                decoded.token, token_bytes,
239                "token mismatch for test case: realm_id={}, key_id={}",
240                realm_id, key_id
241            );
242        }
243    }
244
245    #[test]
246    fn test_claims_byte_layout_verification() {
247        // Verify the byte layout: realm_id (4 bytes) + key_id (4 bytes) + token
248        let token_bytes = Bytes::from(b"token".as_slice());
249        let claims = Claims::new(0x12345678, 0xABCDEF00, token_bytes);
250
251        let encoded = claims.encode();
252        let decoded_bytes = base122::decode(&encoded).expect("Failed to decode base122");
253
254        // Should have at least 8 bytes (4 + 4) + token length
255        assert!(decoded_bytes.len() >= 8);
256
257        // Verify realm_id (first 4 bytes, big endian)
258        let realm_id_bytes = &decoded_bytes[0..4];
259        let realm_id = u32::from_be_bytes([
260            realm_id_bytes[0],
261            realm_id_bytes[1],
262            realm_id_bytes[2],
263            realm_id_bytes[3],
264        ]);
265        assert_eq!(realm_id, 0x12345678);
266
267        // Verify key_id (next 4 bytes, big endian)
268        let key_id_bytes = &decoded_bytes[4..8];
269        let key_id = u32::from_be_bytes([
270            key_id_bytes[0],
271            key_id_bytes[1],
272            key_id_bytes[2],
273            key_id_bytes[3],
274        ]);
275        assert_eq!(key_id, 0xABCDEF00);
276    }
277}