use crate::secret_keyring::FileSecretKeyring;
use crate::token::{SecurityError, SecurityResult, TokenClaims, TokenProvider};
use hash_token_rust::{
AdvancedTokenManager, Algorithm, GenerateTokenOptions, ValidateTokenOptions,
};
use std::time::{SystemTime, UNIX_EPOCH};
use zeroize::Zeroize;
const KEYRING_TOKEN_PREFIX: &[u8] = b"appcore-keyring-v1.";
#[derive(Clone)]
enum SecretSource {
Static(Vec<u8>),
Keyring(FileSecretKeyring),
}
impl Drop for SecretSource {
fn drop(&mut self) {
if let Self::Static(secret) = self {
secret.zeroize();
}
}
}
#[derive(Clone)]
pub struct HashTokenProvider {
source: SecretSource,
salts: Vec<Vec<u8>>,
algorithm: Algorithm,
}
impl std::fmt::Debug for HashTokenProvider {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("HashTokenProvider")
.field(
"source",
&match &self.source {
SecretSource::Static(_) => "static(REDACTED)",
SecretSource::Keyring(_) => "file-keyring-v1",
},
)
.field("salts", &self.salts.len())
.field("algorithm", &self.algorithm)
.finish()
}
}
impl Drop for HashTokenProvider {
fn drop(&mut self) {
self.salts.zeroize();
}
}
impl HashTokenProvider {
pub fn from_secret(secret: Vec<u8>) -> SecurityResult<Self> {
if secret.len() < 16 {
return Err(SecurityError::InvalidToken);
}
Ok(Self {
source: SecretSource::Static(secret),
salts: vec![b"appcore-salt-1".to_vec(), b"appcore-salt-2".to_vec()],
algorithm: Algorithm::Sha256,
})
}
pub fn with_material(secret: Vec<u8>, salts: Vec<Vec<u8>>, algorithm: Algorithm) -> Self {
Self {
source: SecretSource::Static(secret),
salts,
algorithm,
}
}
pub fn with_secret(secret: Vec<u8>, salts: Vec<Vec<u8>>) -> Self {
Self {
source: SecretSource::Static(secret),
salts,
algorithm: Algorithm::Sha256,
}
}
pub fn from_keyring(keyring: FileSecretKeyring, salts: Vec<Vec<u8>>) -> SecurityResult<Self> {
keyring
.resolve_active(unix_time_ms())
.map_err(|_| SecurityError::SecretUnavailable)?;
Ok(Self {
source: SecretSource::Keyring(keyring),
salts,
algorithm: Algorithm::Sha256,
})
}
fn manager(&self, secret: &[u8]) -> SecurityResult<AdvancedTokenManager> {
let salt_slices: Vec<&[u8]> = self.salts.iter().map(Vec::as_slice).collect();
AdvancedTokenManager::new(secret, &salt_slices, self.algorithm)
.map_err(|_| SecurityError::InvalidToken)
}
fn active_manager(&self) -> SecurityResult<(AdvancedTokenManager, Option<String>)> {
match &self.source {
SecretSource::Static(secret) => self.manager(secret).map(|manager| (manager, None)),
SecretSource::Keyring(keyring) => {
let material = keyring
.resolve_active(unix_time_ms())
.map_err(|_| SecurityError::SecretUnavailable)?;
self.manager(&material.secret)
.map(|manager| (manager, Some(material.metadata.key_id.clone())))
}
}
}
fn validation_manager(&self, token: &[u8]) -> SecurityResult<(AdvancedTokenManager, Vec<u8>)> {
match &self.source {
SecretSource::Static(secret) => self
.manager(secret)
.map(|manager| (manager, token.to_vec())),
SecretSource::Keyring(keyring) => {
let (key_id, inner) = unwrap_keyring_token(token)?;
let material = keyring
.resolve_for_validation(key_id, unix_time_ms())
.map_err(|_| SecurityError::VerificationFailed)?;
self.manager(&material.secret)
.map(|manager| (manager, inner.to_vec()))
}
}
}
fn generate_options(claims: &TokenClaims) -> GenerateTokenOptions<'_> {
let expires_in = if claims.ttl_ms == 0 {
None
} else {
Some(claims.ttl_ms.div_ceil(1_000))
};
GenerateTokenOptions {
expires_in,
issuer: Some(claims.issuer.as_str()),
audience: Some(claims.audience.as_str()),
..Default::default()
}
}
fn validate_options(claims: &TokenClaims) -> ValidateTokenOptions<'_> {
ValidateTokenOptions {
issuer: Some(claims.issuer.as_str()),
audience: Some(claims.audience.as_str()),
..Default::default()
}
}
}
impl TokenProvider for HashTokenProvider {
fn seal(&self, payload: &[u8], claims: &TokenClaims) -> SecurityResult<Vec<u8>> {
let (mut manager, key_id) = self.active_manager()?;
let token = manager
.seal_token_bytes(payload, Self::generate_options(claims))
.map(|token| token.into_bytes())
.map_err(|_| SecurityError::InvalidToken)?;
Ok(wrap_keyring_token(key_id.as_deref(), token))
}
fn open(&self, token: &[u8], claims: &TokenClaims) -> SecurityResult<Vec<u8>> {
let (manager, token) = self.validation_manager(token)?;
let token = std::str::from_utf8(&token).map_err(|_| SecurityError::InvalidToken)?;
manager
.open_token_bytes(token, Self::validate_options(claims))
.map(|verified| verified.payload)
.map_err(|_| SecurityError::InvalidToken)
}
fn sign(&self, payload: &[u8], claims: &TokenClaims) -> SecurityResult<Vec<u8>> {
let (mut manager, key_id) = self.active_manager()?;
let token = manager
.generate_token_bytes(payload, Self::generate_options(claims))
.map(|token| token.into_bytes())
.map_err(|_| SecurityError::InvalidToken)?;
Ok(wrap_keyring_token(key_id.as_deref(), token))
}
fn verify(&self, payload: &[u8], signature: &[u8], claims: &TokenClaims) -> SecurityResult<()> {
let (manager, signature) = self.validation_manager(signature)?;
let signature = std::str::from_utf8(&signature).map_err(|_| SecurityError::InvalidToken)?;
let verified = manager
.validate_token_bytes(signature, Self::validate_options(claims))
.map_err(|_| SecurityError::VerificationFailed)?;
if verified.payload == payload {
return Ok(());
}
Err(SecurityError::VerificationFailed)
}
}
fn wrap_keyring_token(key_id: Option<&str>, token: Vec<u8>) -> Vec<u8> {
let Some(key_id) = key_id else {
return token;
};
let mut wrapped =
Vec::with_capacity(KEYRING_TOKEN_PREFIX.len() + key_id.len() + token.len() + 1);
wrapped.extend_from_slice(KEYRING_TOKEN_PREFIX);
wrapped.extend_from_slice(key_id.as_bytes());
wrapped.push(b'.');
wrapped.extend_from_slice(&token);
wrapped
}
fn unwrap_keyring_token(token: &[u8]) -> SecurityResult<(&str, &[u8])> {
let remainder = token
.strip_prefix(KEYRING_TOKEN_PREFIX)
.ok_or(SecurityError::InvalidToken)?;
let separator = remainder
.iter()
.position(|byte| *byte == b'.')
.ok_or(SecurityError::InvalidToken)?;
let key_id =
std::str::from_utf8(&remainder[..separator]).map_err(|_| SecurityError::InvalidToken)?;
if key_id.is_empty()
|| remainder
.get(separator + 1..)
.is_none_or(|inner| inner.is_empty())
{
return Err(SecurityError::InvalidToken);
}
Ok((key_id, &remainder[separator + 1..]))
}
fn unix_time_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis() as u64)
.unwrap_or(0)
}
#[cfg(test)]
#[path = "hashtoken_tests.rs"]
mod tests;