1use base64::engine::general_purpose::STANDARD as BASE64;
9use base64::Engine as _;
10use rainier_support::{Error, Result};
11use sha2::{Digest, Sha256};
12
13pub const KEY_LEN: usize = 32;
16
17#[derive(Clone)]
20pub struct Key {
21 id: String,
22 bytes: [u8; KEY_LEN],
23}
24
25impl Key {
26 pub fn from_bytes(bytes: [u8; KEY_LEN]) -> Self {
28 Self { id: fingerprint(&bytes), bytes }
29 }
30
31 pub fn from_base64(encoded: &str) -> Result<Self> {
37 let trimmed = encoded.trim();
38 let payload = trimmed.strip_prefix("base64:").unwrap_or(trimmed);
39
40 let decoded = BASE64
41 .decode(payload)
42 .map_err(|_| Error::internal("the application key is not valid base64"))?;
43
44 let bytes: [u8; KEY_LEN] = decoded.as_slice().try_into().map_err(|_| {
45 Error::internal(format!(
46 "the application key must be {KEY_LEN} bytes, but decoded to {}",
47 decoded.len()
48 ))
49 })?;
50
51 Ok(Self::from_bytes(bytes))
52 }
53
54 pub fn generate() -> Self {
56 use rand::RngCore;
57
58 let mut bytes = [0u8; KEY_LEN];
59 rand::rngs::OsRng.fill_bytes(&mut bytes);
60 Self::from_bytes(bytes)
61 }
62
63 pub fn id(&self) -> &str {
69 &self.id
70 }
71
72 pub fn bytes(&self) -> &[u8; KEY_LEN] {
74 &self.bytes
75 }
76
77 pub fn to_base64(&self) -> String {
79 format!("base64:{}", BASE64.encode(self.bytes))
80 }
81}
82
83impl std::fmt::Debug for Key {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 f.debug_struct("Key").field("id", &self.id).field("bytes", &"<redacted>").finish()
88 }
89}
90
91fn fingerprint(bytes: &[u8; KEY_LEN]) -> String {
94 let mut hasher = Sha256::new();
95 hasher.update(b"rainier-key-id\0");
96 hasher.update(bytes);
97 let digest = hasher.finalize();
98 digest.iter().take(4).map(|byte| format!("{byte:02x}")).collect()
99}
100
101#[derive(Clone, Debug)]
107pub struct KeyRing {
108 keys: Vec<Key>,
109}
110
111impl KeyRing {
112 pub fn new(current: Key) -> Self {
114 Self { keys: vec![current] }
115 }
116
117 pub fn with_previous(mut self, key: Key) -> Self {
121 self.keys.push(key);
122 self
123 }
124
125 pub fn from_base64(current: &str, previous: &[String]) -> Result<Self> {
127 let mut ring = Self::new(Key::from_base64(current)?);
128 for encoded in previous {
129 if encoded.trim().is_empty() {
130 continue;
131 }
132 ring = ring.with_previous(Key::from_base64(encoded)?);
133 }
134 Ok(ring)
135 }
136
137 pub fn current(&self) -> &Key {
139 &self.keys[0]
141 }
142
143 pub fn find(&self, id: &str) -> Option<&Key> {
145 self.keys.iter().find(|key| key.id() == id)
146 }
147
148 pub fn len(&self) -> usize {
150 self.keys.len()
151 }
152
153 pub fn is_empty(&self) -> bool {
155 false
156 }
157
158 pub fn all(&self) -> impl Iterator<Item = &Key> {
164 self.keys.iter()
165 }
166
167 pub fn ids(&self) -> Vec<&str> {
169 self.keys.iter().map(Key::id).collect()
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn a_generated_key_round_trips_through_base64() {
179 let key = Key::generate();
180 let restored = Key::from_base64(&key.to_base64()).unwrap();
181
182 assert_eq!(restored.bytes(), key.bytes());
183 assert_eq!(restored.id(), key.id());
184 }
185
186 #[test]
187 fn the_base64_prefix_is_accepted_and_optional() {
188 let key = Key::generate();
189 let raw = BASE64.encode(key.bytes());
190
191 assert_eq!(Key::from_base64(&raw).unwrap().bytes(), key.bytes());
192 assert_eq!(Key::from_base64(&format!("base64:{raw}")).unwrap().bytes(), key.bytes());
193 }
194
195 #[test]
196 fn a_key_of_the_wrong_length_is_rejected_with_its_length() {
197 let short = BASE64.encode([0u8; 16]);
198 let err = Key::from_base64(&short).unwrap_err();
199
200 assert!(err.message().contains("32"), "{}", err.message());
201 assert!(err.message().contains("16"), "{}", err.message());
202 }
203
204 #[test]
205 fn rubbish_is_rejected_as_base64() {
206 assert!(Key::from_base64("not base64 at all!!").is_err());
207 }
208
209 #[test]
210 fn the_id_is_derived_from_the_key_and_is_stable() {
211 let key = Key::from_bytes([7u8; KEY_LEN]);
212 let same = Key::from_bytes([7u8; KEY_LEN]);
213 let other = Key::from_bytes([8u8; KEY_LEN]);
214
215 assert_eq!(key.id(), same.id());
216 assert_ne!(key.id(), other.id());
217 assert_eq!(key.id().len(), 8);
218 }
219
220 #[test]
221 fn debug_does_not_disclose_the_key() {
222 let key = Key::generate();
223 let rendered = format!("{key:?}");
224
225 assert!(rendered.contains("redacted"));
226 assert!(!rendered.contains(&BASE64.encode(key.bytes())), "{rendered}");
227 assert!(rendered.contains(key.id()), "{rendered}");
229 }
230
231 #[test]
232 fn a_ring_finds_current_and_retired_keys_by_id() {
233 let current = Key::generate();
234 let old = Key::generate();
235 let ring = KeyRing::new(current.clone()).with_previous(old.clone());
236
237 assert_eq!(ring.current().id(), current.id());
238 assert_eq!(ring.find(old.id()).unwrap().bytes(), old.bytes());
239 assert!(ring.find("00000000").is_none());
240 assert_eq!(ring.len(), 2);
241 }
242
243 #[test]
244 fn empty_previous_entries_are_skipped() {
245 let key = Key::generate().to_base64();
246 let ring = KeyRing::from_base64(&key, &[String::new(), " ".to_string()]).unwrap();
247
248 assert_eq!(ring.len(), 1, "blank entries in a comma-separated list are not keys");
249 }
250}