Skip to main content

secrets_core/
token.rs

1use chrono::{DateTime, Utc};
2use rand::Rng;
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5
6use crate::storage::{StorageBackend, StorageEntry, StorageError};
7
8const TOKEN_PREFIX: &str = "sys/token/";
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct TokenEntry {
12    pub id_hash: String,
13    pub policies: Vec<String>,
14    pub created_at: DateTime<Utc>,
15    pub expires_at: Option<DateTime<Utc>>,
16}
17
18/// Generates a fresh opaque bearer token. Returns the raw token (given to the
19/// caller once) and the entry to persist, keyed by `sha256(token)`.
20pub fn generate_token(policies: Vec<String>, ttl_seconds: Option<i64>) -> (String, TokenEntry) {
21    let mut raw = [0u8; 24];
22    rand::rng().fill_bytes(&mut raw);
23    let token = format!("s.{}", hex::encode(raw));
24    let now = Utc::now();
25    let entry = TokenEntry {
26        id_hash: hash_token(&token),
27        policies,
28        created_at: now,
29        expires_at: ttl_seconds.map(|s| now + chrono::Duration::seconds(s)),
30    };
31    (token, entry)
32}
33
34pub fn hash_token(token: &str) -> String {
35    hex::encode(Sha256::digest(token.as_bytes()))
36}
37
38pub async fn store_token(
39    storage: &dyn StorageBackend,
40    token: &str,
41    entry: &TokenEntry,
42) -> Result<(), StorageError> {
43    let path = format!("{TOKEN_PREFIX}{}", hash_token(token));
44    let value = serde_json::to_vec(entry).map_err(|e| StorageError::Backend(e.to_string()))?;
45    storage
46        .put(
47            &path,
48            StorageEntry {
49                value,
50                expires_at: entry.expires_at,
51            },
52        )
53        .await
54}
55
56/// Looks up a token, treating an expired entry as absent.
57pub async fn lookup_token(
58    storage: &dyn StorageBackend,
59    token: &str,
60) -> Result<Option<TokenEntry>, StorageError> {
61    let path = format!("{TOKEN_PREFIX}{}", hash_token(token));
62    let Some(entry) = storage.get(&path).await? else {
63        return Ok(None);
64    };
65    if let Some(expires_at) = entry.expires_at
66        && expires_at < Utc::now()
67    {
68        return Ok(None);
69    }
70    let token_entry: TokenEntry =
71        serde_json::from_slice(&entry.value).map_err(|e| StorageError::Backend(e.to_string()))?;
72    Ok(Some(token_entry))
73}
74
75pub async fn revoke_token(storage: &dyn StorageBackend, token: &str) -> Result<(), StorageError> {
76    let path = format!("{TOKEN_PREFIX}{}", hash_token(token));
77    storage.delete(&path).await
78}
79
80pub async fn renew_token(
81    storage: &dyn StorageBackend,
82    token: &str,
83    ttl_seconds: i64,
84) -> Result<Option<TokenEntry>, StorageError> {
85    let Some(mut entry) = lookup_token(storage, token).await? else {
86        return Ok(None);
87    };
88    entry.expires_at = Some(Utc::now() + chrono::Duration::seconds(ttl_seconds));
89    store_token(storage, token, &entry).await?;
90    Ok(Some(entry))
91}