use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
use base64::Engine;
use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
use ed25519_dalek::{Signer, SigningKey};
use sha2::{Digest, Sha256};
use crate::errors::Error;
const TOKEN_TTL: i64 = 3600;
const REFRESH_MARGIN: i64 = 300;
const CLOCK_SKEW_LEEWAY: i64 = 5;
pub(crate) struct TokenCache {
signing_key: SigningKey,
kfp: String,
cached: Mutex<Cached>,
}
#[derive(Default)]
struct Cached {
token: String,
expiry: i64,
}
impl TokenCache {
pub(crate) fn new(private_key_base64: &str) -> Result<Self, Error> {
let seed = ed25519_seed(private_key_base64)?;
let signing_key = SigningKey::from_bytes(&seed);
let mut hasher = Sha256::new();
hasher.update(signing_key.verifying_key().as_bytes());
let digest = hasher.finalize();
let kfp = hex::encode(&digest[..16]);
Ok(Self {
signing_key,
kfp,
cached: Mutex::new(Cached::default()),
})
}
pub(crate) fn token(&self) -> Result<String, Error> {
let now = unix_now();
let mut cached = self.cached.lock().unwrap_or_else(|e| e.into_inner());
if !cached.token.is_empty() && now < cached.expiry - REFRESH_MARGIN {
return Ok(cached.token.clone());
}
let exp = now + TOKEN_TTL;
let token = self.sign(now, exp);
cached.token = token.clone();
cached.expiry = exp;
Ok(token)
}
fn sign(&self, now: i64, exp: i64) -> String {
let header = format!(r#"{{"alg":"EdDSA","typ":"JWT","kid":"{}"}}"#, self.kfp);
let payload = format!(
r#"{{"sub":"mountos:provider","aud":"mountos/appserv","iat":{now},"nbf":{},"exp":{exp},"jti":"{}","scope":"service","kfp":"{}"}}"#,
now - CLOCK_SKEW_LEEWAY,
uuid::Uuid::new_v4(),
self.kfp,
);
let signing_input = format!(
"{}.{}",
URL_SAFE_NO_PAD.encode(header.as_bytes()),
URL_SAFE_NO_PAD.encode(payload.as_bytes()),
);
let sig = self.signing_key.sign(signing_input.as_bytes());
format!("{signing_input}.{}", URL_SAFE_NO_PAD.encode(sig.to_bytes()))
}
}
fn ed25519_seed(private_key_base64: &str) -> Result<[u8; 32], Error> {
let raw = STANDARD
.decode(private_key_base64.trim())
.map_err(|e| Error::Key(e.to_string()))?;
let seed = match raw.len() {
32 | 64 => &raw[..32],
n => return Err(Error::Key(format!("expected 32 or 64 bytes, got {n}"))),
};
seed.try_into()
.map_err(|_| Error::Key("invalid ED25519 seed length".into()))
}
fn unix_now() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}