mod keys;
#[cfg(test)]
mod tests;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::error::CoolError;
pub use keys::{InMemoryNonceStore, KeyProvider, NonceStore, StaticKeyProvider};
const ENVELOPE_DEFAULT_CLOCK_SKEW_SECS: i64 = 300;
#[derive(Clone)]
pub struct HmacEnvelope<K: KeyProvider> {
keys: Arc<K>,
signing_kid: String,
clock_skew_secs: i64,
nonces: Option<Arc<dyn NonceStore>>,
}
impl<K: KeyProvider> HmacEnvelope<K> {
pub fn new(keys: Arc<K>, signing_kid: impl Into<String>) -> Self {
Self {
keys,
signing_kid: signing_kid.into(),
clock_skew_secs: ENVELOPE_DEFAULT_CLOCK_SKEW_SECS,
nonces: None,
}
}
pub fn with_clock_skew_secs(mut self, secs: i64) -> Self {
self.clock_skew_secs = secs;
self
}
pub fn with_nonce_store(mut self, store: Arc<dyn NonceStore>) -> Self {
self.nonces = Some(store);
self
}
async fn compute_mac(&self, key: &[u8], input: &[u8]) -> Result<Vec<u8>, CoolError> {
use hmac::{Hmac, Mac};
let mut mac = <Hmac<sha2::Sha256> as Mac>::new_from_slice(key)
.map_err(|_| CoolError::Internal("HMAC key length error".to_owned()))?;
mac.update(input);
Ok(mac.finalize().into_bytes().to_vec())
}
pub async fn seal(&self, payload: serde_json::Value) -> Result<SealedEnvelope, CoolError> {
let key = self.keys.resolve_signing_key(&self.signing_kid).await?;
let ts = chrono::Utc::now().timestamp();
let nonce = uuid::Uuid::new_v4().to_string();
let mut envelope = SealedEnvelope {
kid: self.signing_kid.clone(),
alg: "HS256".to_owned(),
ts,
nonce,
body: payload,
mac_b64: String::new(),
};
let input = envelope.signing_input()?;
let mac = self.compute_mac(&key, &input).await?;
use base64::Engine;
envelope.mac_b64 = base64::engine::general_purpose::STANDARD.encode(mac);
Ok(envelope)
}
pub async fn open(&self, envelope: &SealedEnvelope) -> Result<serde_json::Value, CoolError> {
if envelope.alg != "HS256" {
return Err(CoolError::Unauthorized(format!(
"unsupported envelope algorithm '{}'",
envelope.alg,
)));
}
let now = chrono::Utc::now().timestamp();
let drift = (now - envelope.ts).abs();
if drift > self.clock_skew_secs {
return Err(CoolError::Unauthorized(
"envelope timestamp outside accepted skew window".to_owned(),
));
}
let key = self.keys.resolve_signing_key(&envelope.kid).await?;
let input = envelope.signing_input()?;
let expected = self.compute_mac(&key, &input).await?;
use base64::Engine;
let actual = base64::engine::general_purpose::STANDARD
.decode(&envelope.mac_b64)
.map_err(|_| CoolError::Unauthorized("envelope MAC is not base64".to_owned()))?;
if actual.len() != expected.len() {
return Err(CoolError::Unauthorized(
"envelope MAC has wrong length".to_owned(),
));
}
use subtle::ConstantTimeEq;
if !bool::from(actual.as_slice().ct_eq(expected.as_slice())) {
return Err(CoolError::Unauthorized(
"envelope MAC verification failed".to_owned(),
));
}
if let Some(nonces) = &self.nonces {
let expires_at = chrono::DateTime::<chrono::Utc>::from_timestamp(
envelope.ts + self.clock_skew_secs,
0,
)
.ok_or_else(|| CoolError::Unauthorized("envelope timestamp out of range".to_owned()))?;
let recorded = nonces.record_if_unseen(&envelope.nonce, expires_at).await?;
if !recorded {
return Err(CoolError::Unauthorized(
"envelope nonce replay detected".to_owned(),
));
}
}
Ok(envelope.body.clone())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SealedEnvelope {
pub kid: String,
pub alg: String,
pub ts: i64,
pub nonce: String,
pub body: serde_json::Value,
pub mac_b64: String,
}
impl SealedEnvelope {
pub(crate) fn signing_input(&self) -> Result<Vec<u8>, CoolError> {
let mut buf = Vec::with_capacity(256);
buf.extend_from_slice(self.kid.as_bytes());
buf.push(0);
buf.extend_from_slice(self.alg.as_bytes());
buf.push(0);
buf.extend_from_slice(&self.ts.to_be_bytes());
buf.push(0);
buf.extend_from_slice(self.nonce.as_bytes());
buf.push(0);
let body_bytes = serde_json::to_vec(&self.body)
.map_err(|error| CoolError::Codec(format!("encode envelope body: {error}")))?;
buf.extend_from_slice(&body_bytes);
Ok(buf)
}
}