1use crate::token::SecurityResult;
14use crate::SecurityError;
15use std::collections::HashMap;
16use std::fmt;
17use std::time::{SystemTime, UNIX_EPOCH};
18use zeroize::Zeroize;
19
20pub use crate::secret_file::FileSecretResolver;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct SecuritySecretRef(
25 pub String,
27);
28
29pub trait SecretStore {
31 fn put(&mut self, data: Vec<u8>) -> SecurityResult<SecuritySecretRef>;
33 fn get(&self, reference: &SecuritySecretRef) -> SecurityResult<Vec<u8>>;
35}
36
37#[derive(Clone, PartialEq, Eq)]
39pub struct SecretBytes(Vec<u8>);
40
41impl SecretBytes {
42 pub fn new(value: Vec<u8>) -> Self {
44 Self(value)
45 }
46
47 pub fn as_bytes(&self) -> &[u8] {
49 &self.0
50 }
51}
52
53impl fmt::Debug for SecretBytes {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 f.write_str("SecretBytes(REDACTED)")
56 }
57}
58
59impl Drop for SecretBytes {
60 fn drop(&mut self) {
61 self.0.zeroize();
62 }
63}
64
65pub trait SecretResolver: Send + Sync {
67 fn resolve(&self, reference: &SecuritySecretRef) -> SecurityResult<SecretBytes>;
69}
70
71#[derive(Debug, Clone, Copy, Default)]
73pub struct EnvSecretResolver;
74
75impl SecretResolver for EnvSecretResolver {
76 fn resolve(&self, reference: &SecuritySecretRef) -> SecurityResult<SecretBytes> {
77 let value = std::env::var(&reference.0).map_err(|_| SecurityError::SecretUnavailable)?;
78 Ok(SecretBytes::new(value.into_bytes()))
79 }
80}
81
82#[derive(Debug, Clone)]
84pub struct StaticSecretResolver {
85 secrets: HashMap<String, SecretBytes>,
86}
87
88impl StaticSecretResolver {
89 pub fn new(secrets: HashMap<String, SecretBytes>) -> Self {
91 Self { secrets }
92 }
93}
94
95impl SecretResolver for StaticSecretResolver {
96 fn resolve(&self, reference: &SecuritySecretRef) -> SecurityResult<SecretBytes> {
97 self.secrets
98 .get(&reference.0)
99 .cloned()
100 .ok_or(SecurityError::SecretUnavailable)
101 }
102}
103
104#[derive(Clone, PartialEq, Eq)]
106pub struct PeerCredential {
107 pub key_id: String,
109 pub secret: SecretBytes,
111}
112
113impl fmt::Debug for PeerCredential {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 f.debug_struct("PeerCredential")
116 .field("key_id", &self.key_id)
117 .field("secret", &"REDACTED")
118 .finish()
119 }
120}
121
122pub trait PeerCredentialProvider: Send + Sync {
124 fn credential_for_peer(&self, peer_core_id: &str) -> SecurityResult<PeerCredential>;
126}
127
128#[derive(Debug, Clone)]
130pub struct StaticPeerCredentialProvider {
131 credentials: HashMap<String, PeerCredential>,
132}
133
134impl StaticPeerCredentialProvider {
135 pub fn new(credentials: HashMap<String, PeerCredential>) -> Self {
137 Self { credentials }
138 }
139}
140
141impl PeerCredentialProvider for StaticPeerCredentialProvider {
142 fn credential_for_peer(&self, peer_core_id: &str) -> SecurityResult<PeerCredential> {
143 self.credentials
144 .get(peer_core_id)
145 .cloned()
146 .ok_or(SecurityError::SecretUnavailable)
147 }
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
152pub enum SecuritySecretStatus {
153 Active,
155 Deprecated,
157 Revoked,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
163pub struct SecuritySecretMetadata {
164 pub key_id: String,
166 pub created_at_ms: u64,
168 pub expires_at_ms: Option<u64>,
170 pub status: SecuritySecretStatus,
172}
173
174#[derive(Clone, PartialEq, Eq)]
176pub struct SecuritySecretMaterial {
177 pub secret: Vec<u8>,
179 pub metadata: SecuritySecretMetadata,
181}
182
183impl fmt::Debug for SecuritySecretMaterial {
184 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
185 formatter
186 .debug_struct("SecuritySecretMaterial")
187 .field("secret", &"REDACTED")
188 .field("metadata", &self.metadata)
189 .finish()
190 }
191}
192
193impl Drop for SecuritySecretMaterial {
194 fn drop(&mut self) {
195 self.secret.zeroize();
196 }
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
201pub enum SecretFormatError {
202 InvalidFormat(&'static str),
204 InvalidSecret,
206 RandomUnavailable,
208}
209
210impl fmt::Display for SecretFormatError {
211 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212 match self {
213 SecretFormatError::InvalidFormat(msg) => write!(f, "{msg}"),
214 SecretFormatError::InvalidSecret => write!(f, "invalid secret"),
215 SecretFormatError::RandomUnavailable => write!(f, "OS random source unavailable"),
216 }
217 }
218}
219
220impl SecuritySecretStatus {
221 pub fn as_str(&self) -> &'static str {
223 match self {
224 SecuritySecretStatus::Active => "active",
225 SecuritySecretStatus::Deprecated => "deprecated",
226 SecuritySecretStatus::Revoked => "revoked",
227 }
228 }
229}
230
231impl SecuritySecretMaterial {
232 pub fn is_expired(&self, now_ms: u64) -> bool {
234 self.metadata
235 .expires_at_ms
236 .map(|exp| exp <= now_ms)
237 .unwrap_or(false)
238 }
239}
240
241pub fn parse_secret_material(input: &[u8]) -> Result<SecuritySecretMaterial, SecretFormatError> {
243 let text = std::str::from_utf8(input)
244 .map_err(|_| SecretFormatError::InvalidFormat("NO MORE SUPPORTED PLEASE UPDATE"))?;
245 if !text.contains("secret=") {
246 return Err(SecretFormatError::InvalidFormat(
247 "NO MORE SUPPORTED PLEASE UPDATE",
248 ));
249 }
250 parse_structured_secret(text)
251}
252
253pub fn format_secret_material(material: &SecuritySecretMaterial) -> String {
255 let expires = material
256 .metadata
257 .expires_at_ms
258 .map(|value| value.to_string())
259 .unwrap_or_else(|| "none".to_string());
260 format!(
261 "key_id={}\ncreated_at_ms={}\nexpires_at_ms={}\nstatus={}\nsecret={}\n",
262 material.metadata.key_id,
263 material.metadata.created_at_ms,
264 expires,
265 material.metadata.status.as_str(),
266 encode_hex(&material.secret)
267 )
268}
269
270pub fn new_rotated_secret(
272 expires_at_ms: Option<u64>,
273) -> Result<SecuritySecretMaterial, SecretFormatError> {
274 let now_ms = SystemTime::now()
275 .duration_since(UNIX_EPOCH)
276 .map(|d| d.as_millis() as u64)
277 .unwrap_or(0);
278 let mut secret = vec![0u8; 32];
279 getrandom::getrandom(&mut secret).map_err(|_| SecretFormatError::RandomUnavailable)?;
280 let key_id = format!(
281 "k-{now_ms}-{:02x}{:02x}{:02x}{:02x}",
282 secret[0], secret[1], secret[2], secret[3]
283 );
284 Ok(SecuritySecretMaterial {
285 secret,
286 metadata: SecuritySecretMetadata {
287 key_id,
288 created_at_ms: now_ms,
289 expires_at_ms,
290 status: SecuritySecretStatus::Active,
291 },
292 })
293}
294
295fn parse_structured_secret(text: &str) -> Result<SecuritySecretMaterial, SecretFormatError> {
296 let mut key_id = None::<String>;
297 let mut created_at_ms = None::<u64>;
298 let mut expires_at_ms = None::<Option<u64>>;
299 let mut status = None::<SecuritySecretStatus>;
300 let mut secret = None::<Vec<u8>>;
301 for line in text.lines() {
302 let line = line.trim();
303 if line.is_empty() || line.starts_with('#') {
304 continue;
305 }
306 let Some((k, v)) = line.split_once('=') else {
307 continue;
308 };
309 let value = v.trim();
310 match k.trim() {
311 "key_id" => key_id = Some(value.to_string()),
312 "created_at_ms" => {
313 created_at_ms = Some(
314 value
315 .parse::<u64>()
316 .map_err(|_| SecretFormatError::InvalidFormat("invalid created_at_ms"))?,
317 )
318 }
319 "expires_at_ms" => {
320 if value == "none" {
321 expires_at_ms = Some(None);
322 } else {
323 expires_at_ms =
324 Some(Some(value.parse::<u64>().map_err(|_| {
325 SecretFormatError::InvalidFormat("invalid expires_at_ms")
326 })?));
327 }
328 }
329 "status" => {
330 status = Some(match value {
331 "active" => SecuritySecretStatus::Active,
332 "deprecated" => SecuritySecretStatus::Deprecated,
333 "revoked" => SecuritySecretStatus::Revoked,
334 _ => return Err(SecretFormatError::InvalidFormat("invalid status")),
335 })
336 }
337 "secret" => {
338 let bytes = if let Some(value) = value.strip_prefix("hex:") {
339 decode_hex(value).ok_or(SecretFormatError::InvalidSecret)?
340 } else if looks_like_hex(value) {
341 decode_hex(value).unwrap_or_else(|| value.as_bytes().to_vec())
342 } else {
343 value.as_bytes().to_vec()
344 };
345 secret = Some(bytes);
346 }
347 _ => {}
348 }
349 }
350 let key_id = key_id.ok_or(SecretFormatError::InvalidFormat("missing key_id"))?;
351 let created_at_ms =
352 created_at_ms.ok_or(SecretFormatError::InvalidFormat("missing created_at_ms"))?;
353 let expires_at_ms = expires_at_ms.unwrap_or(None);
354 let status = status.ok_or(SecretFormatError::InvalidFormat("missing status"))?;
355 let secret = secret.ok_or(SecretFormatError::InvalidFormat("missing secret"))?;
356 if secret.len() < 16 {
357 return Err(SecretFormatError::InvalidSecret);
358 }
359 Ok(SecuritySecretMaterial {
360 secret,
361 metadata: SecuritySecretMetadata {
362 key_id,
363 created_at_ms,
364 expires_at_ms,
365 status,
366 },
367 })
368}
369
370fn looks_like_hex(value: &str) -> bool {
371 !value.is_empty()
372 && value.len().is_multiple_of(2)
373 && value.bytes().all(|b| b.is_ascii_hexdigit())
374}
375
376fn encode_hex(bytes: &[u8]) -> String {
377 const HEX: &[u8; 16] = b"0123456789abcdef";
378 let mut out = String::with_capacity(bytes.len() * 2);
379 for byte in bytes {
380 out.push(HEX[(byte >> 4) as usize] as char);
381 out.push(HEX[(byte & 0x0f) as usize] as char);
382 }
383 out
384}
385
386fn decode_hex(input: &str) -> Option<Vec<u8>> {
387 if input.is_empty() || !input.len().is_multiple_of(2) {
388 return None;
389 }
390 let mut output = Vec::with_capacity(input.len() / 2);
391 let bytes = input.as_bytes();
392 let mut index = 0usize;
393 while index < bytes.len() {
394 let hi = hex_value(bytes[index])?;
395 let lo = hex_value(bytes[index + 1])?;
396 output.push((hi << 4) | lo);
397 index += 2;
398 }
399 Some(output)
400}
401
402fn hex_value(byte: u8) -> Option<u8> {
403 match byte {
404 b'0'..=b'9' => Some(byte - b'0'),
405 b'a'..=b'f' => Some(10 + byte - b'a'),
406 b'A'..=b'F' => Some(10 + byte - b'A'),
407 _ => None,
408 }
409}
410
411#[cfg(test)]
412#[path = "secret_tests.rs"]
413mod tests;