Skip to main content

moq_token/
key_id.rs

1use std::fmt;
2use std::ops::Deref;
3
4use serde::{Deserialize, Serialize};
5
6/// A validated key identifier (kid) that is safe for use in file paths and URLs.
7///
8/// Only allows ASCII alphanumeric characters, hyphens, and underscores.
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(try_from = "String", into = "String")]
11pub struct KeyId(String);
12
13impl KeyId {
14	const MAX_LENGTH: usize = 128;
15
16	/// Validate and create a KeyId from a string.
17	pub fn decode(s: &str) -> Result<Self, KeyIdError> {
18		if s.is_empty() {
19			return Err(KeyIdError::Empty);
20		}
21		if s.len() > Self::MAX_LENGTH {
22			return Err(KeyIdError::TooLong);
23		}
24		if !s.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') {
25			return Err(KeyIdError::Invalid);
26		}
27		Ok(Self(s.to_owned()))
28	}
29
30	/// Returns the key ID as a string slice.
31	pub fn encode(&self) -> &str {
32		&self.0
33	}
34
35	/// Generate a random key ID using cryptographically secure randomness.
36	pub fn random() -> Self {
37		let mut bytes = [0u8; 8];
38		aws_lc_rs::rand::fill(&mut bytes).expect("failed to generate random bytes");
39		let hex: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
40		Self(hex)
41	}
42}
43
44impl fmt::Display for KeyId {
45	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46		f.write_str(&self.0)
47	}
48}
49
50impl AsRef<str> for KeyId {
51	fn as_ref(&self) -> &str {
52		&self.0
53	}
54}
55
56impl Deref for KeyId {
57	type Target = str;
58
59	fn deref(&self) -> &str {
60		&self.0
61	}
62}
63
64impl From<KeyId> for String {
65	fn from(kid: KeyId) -> Self {
66		kid.0
67	}
68}
69
70impl TryFrom<String> for KeyId {
71	type Error = KeyIdError;
72
73	fn try_from(s: String) -> Result<Self, Self::Error> {
74		KeyId::decode(&s)
75	}
76}
77
78#[derive(Debug, Clone, thiserror::Error)]
79#[non_exhaustive]
80pub enum KeyIdError {
81	#[error("key ID must not be empty")]
82	Empty,
83
84	#[error("key ID exceeds maximum length of 128 characters")]
85	TooLong,
86
87	#[error("key ID contains invalid characters (only alphanumeric, hyphens, underscores allowed)")]
88	Invalid,
89}
90
91#[cfg(test)]
92mod tests {
93	use super::*;
94
95	#[test]
96	fn valid_key_ids() {
97		assert!(KeyId::decode("abc-123_DEF").is_ok());
98		assert!(KeyId::decode("simple").is_ok());
99		assert!(KeyId::decode("key-1").is_ok());
100		assert!(KeyId::decode("a").is_ok());
101	}
102
103	#[test]
104	fn invalid_key_ids() {
105		assert!(KeyId::decode("").is_err());
106		assert!(KeyId::decode("../etc/passwd").is_err());
107		assert!(KeyId::decode("key with spaces").is_err());
108		assert!(KeyId::decode("key/slash").is_err());
109		assert!(KeyId::decode("key.dot").is_err());
110	}
111
112	#[test]
113	fn random_key_id_is_valid() {
114		let kid = KeyId::random();
115		assert!(KeyId::decode(kid.encode()).is_ok());
116		assert!(!kid.encode().is_empty());
117	}
118}