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