Skip to main content

vcon_server/
ids.rs

1//! Identifiers, written in lowercase hex: a tenant or a version is 128
2//! random bits, a key 64 bits.
3
4use std::fmt;
5
6/// Names a key without revealing it: an encryption key by the start of
7/// its SHA-256 (`Key::id`), an access key by 64 random bits.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub struct KeyId(pub [u8; 8]);
10
11impl KeyId {
12    /// A new random key id.
13    #[must_use]
14    pub fn generate() -> KeyId {
15        KeyId(random())
16    }
17
18    /// Reads a key id written by `Display`: exactly 16 lowercase hex
19    /// digits.
20    pub fn parse(text: &str) -> Option<KeyId> {
21        unhex(text).map(KeyId)
22    }
23
24    /// The id's 8 bytes, as catalogs store them.
25    pub(crate) fn bytes(self) -> [u8; 8] {
26        self.0
27    }
28}
29
30/// A tenant.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub struct TenantId(pub [u8; 16]);
33
34impl TenantId {
35    /// A new random tenant id.
36    #[must_use]
37    pub fn generate() -> TenantId {
38        TenantId(random())
39    }
40
41    /// Reads a tenant id written by `Display`: exactly 32 lowercase hex
42    /// digits.
43    pub fn parse(text: &str) -> Option<TenantId> {
44        unhex(text).map(TenantId)
45    }
46}
47
48/// One stored version of a vCon.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub struct VersionId(pub [u8; 16]);
51
52impl VersionId {
53    /// A new random version id.
54    #[must_use]
55    pub fn generate() -> VersionId {
56        VersionId(random())
57    }
58}
59
60impl VersionId {
61    /// Reads a version id written by `Display`: exactly 32 lowercase hex
62    /// digits.
63    pub fn parse(text: &str) -> Option<VersionId> {
64        unhex(text).map(VersionId)
65    }
66}
67
68fn random<const N: usize>() -> [u8; N] {
69    let mut bytes = [0; N];
70    // AWS-LC's RAND_bytes aborts the process when it cannot get enough
71    // randomness, so `fill` never returns its error.
72    aws_lc_rs::rand::fill(&mut bytes).expect("AWS-LC aborts instead of failing");
73    bytes
74}
75
76/// Reads exactly 2N lowercase hex digits.
77fn unhex<const N: usize>(text: &str) -> Option<[u8; N]> {
78    let digits = text.as_bytes();
79    if digits.len() != 2 * N {
80        return None;
81    }
82    let digit = |d: u8| match d {
83        b'0'..=b'9' => Some(d - b'0'),
84        b'a'..=b'f' => Some(d - b'a' + 10),
85        _ => None,
86    };
87    let mut bytes = [0; N];
88    let (pairs, _) = digits.as_chunks::<2>();
89    for (byte, [high, low]) in bytes.iter_mut().zip(pairs) {
90        *byte = digit(*high)? << 4 | digit(*low)?;
91    }
92    Some(bytes)
93}
94
95fn hex(bytes: &[u8], f: &mut fmt::Formatter<'_>) -> fmt::Result {
96    bytes.iter().try_for_each(|b| write!(f, "{b:02x}"))
97}
98
99impl fmt::Display for KeyId {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        hex(&self.0, f)
102    }
103}
104
105impl fmt::Display for TenantId {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        hex(&self.0, f)
108    }
109}
110
111impl fmt::Display for VersionId {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        hex(&self.0, f)
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn ids_are_written_as_32_lowercase_hex_digits() {
123        let mut bytes = [0; 16];
124        bytes[0] = 0x0a;
125        bytes[15] = 0xff;
126        let expected = format!("0a{}ff", "00".repeat(14));
127        assert_eq!(
128            TenantId(bytes).to_string(),
129            expected,
130            "two lower-case hex digits per byte, in order, zeros kept"
131        );
132        assert_eq!(
133            VersionId(bytes).to_string(),
134            expected,
135            "two lower-case hex digits per byte, in order, zeros kept"
136        );
137    }
138
139    /// Every byte of `ids` varies across them.
140    fn varies(ids: &[[u8; 16]]) {
141        for i in 0..16 {
142            assert!(
143                ids.iter().any(|id| id[i] != ids[0][i]),
144                "byte {i} never varies"
145            );
146        }
147    }
148
149    #[test]
150    fn a_key_id_is_64_random_bits_written_as_16_hex_digits() {
151        let ids: Vec<[u8; 8]> = (0..8).map(|_| KeyId::generate().0).collect();
152        for i in 0..8 {
153            assert!(
154                ids.iter().any(|id| id[i] != ids[0][i]),
155                "byte {i} never varies"
156            );
157        }
158        let id = KeyId([0x0a, 0, 0, 0, 0, 0, 0, 0xff]);
159        assert_eq!(
160            id.to_string(),
161            "0a000000000000ff",
162            "two lower-case hex digits per byte, in order, zeros kept"
163        );
164        assert_eq!(
165            KeyId::parse("0a000000000000ff"),
166            Some(id),
167            "parse reads the spelling Display writes"
168        );
169        for bad in [
170            "0A000000000000FF",
171            "0a000000000000f",
172            "0a000000000000ff0",
173            "",
174        ] {
175            assert_eq!(KeyId::parse(bad), None, "{bad:?}");
176        }
177    }
178
179    #[test]
180    fn ids_are_random() {
181        varies(&(0..8).map(|_| VersionId::generate().0).collect::<Vec<_>>());
182        varies(&(0..8).map(|_| TenantId::generate().0).collect::<Vec<_>>());
183    }
184
185    /// The spellings parse refuses, given one id's own spelling `text`.
186    fn misspelled(text: &str) -> [String; 7] {
187        [
188            String::new(),
189            text[..31].to_owned(),
190            format!("{text}0"),
191            text.to_uppercase(),
192            format!("g{}", &text[1..]),
193            format!("{text}.tmp"),
194            format!("+{}", &text[1..]),
195        ]
196    }
197
198    #[test]
199    fn tenant_and_version_ids_parse_only_from_their_own_spelling() {
200        let tenant = TenantId::generate();
201        let version = VersionId::generate();
202        assert_eq!(
203            TenantId::parse(&tenant.to_string()),
204            Some(tenant),
205            "parse reads the spelling Display writes"
206        );
207        assert_eq!(
208            VersionId::parse(&version.to_string()),
209            Some(version),
210            "parse reads the spelling Display writes"
211        );
212        for (text, parsed) in [
213            (
214                tenant.to_string(),
215                (|t: &str| TenantId::parse(t).is_some()) as fn(&str) -> bool,
216            ),
217            (version.to_string(), |t: &str| VersionId::parse(t).is_some()),
218        ] {
219            for bad in misspelled(&text) {
220                // An id with no letter is its own upper-case spelling.
221                if bad != text {
222                    assert!(!parsed(&bad), "not an id's own spelling: {bad:?}");
223                }
224            }
225        }
226    }
227}