use crate::{fingerprint_str, UniversalError};
use base64::{
engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD},
Engine,
};
use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use std::time::{SystemTime, UNIX_EPOCH};
#[cfg(feature = "compression")]
use crate::compress::{compress, decompress};
type HmacSha256 = Hmac<Sha256>;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum SignedEnvelopeMode {
Standard,
UrlSafe,
Compressed,
Ttl,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedEnvelope {
pub d: String,
pub f: String,
pub sig: String,
pub m: SignedEnvelopeMode,
#[serde(skip_serializing_if = "Option::is_none")]
pub e: Option<u64>,
pub v: u8,
}
fn hmac_sign(fingerprint: &str, key: &str) -> String {
let mut mac = HmacSha256::new_from_slice(key.as_bytes()).expect("HMAC accepts any key length");
mac.update(fingerprint.as_bytes());
hex::encode(mac.finalize().into_bytes())
}
fn hmac_verify(fingerprint: &str, key: &str, sig: &str) -> bool {
let mut mac = HmacSha256::new_from_slice(key.as_bytes()).expect("HMAC accepts any key length");
mac.update(fingerprint.as_bytes());
let expected = hex::decode(sig).unwrap_or_default();
mac.verify_slice(&expected).is_ok()
}
impl SignedEnvelope {
#[must_use]
pub fn wrap(input: &str, key: &str) -> Self {
let fp = fingerprint_str(input);
Self {
d: STANDARD.encode(input.as_bytes()),
sig: hmac_sign(&fp, key),
f: fp,
m: SignedEnvelopeMode::Standard,
e: None,
v: 3,
}
}
#[must_use]
pub fn wrap_url_safe(input: &str, key: &str) -> Self {
let fp = fingerprint_str(input);
Self {
d: URL_SAFE_NO_PAD.encode(input.as_bytes()),
sig: hmac_sign(&fp, key),
f: fp,
m: SignedEnvelopeMode::UrlSafe,
e: None,
v: 3,
}
}
#[cfg(feature = "compression")]
pub fn wrap_compressed(input: &str, key: &str) -> Result<Self, UniversalError> {
let compressed = compress(input.as_bytes())?;
let fp = fingerprint_str(input);
Ok(Self {
d: STANDARD.encode(&compressed),
sig: hmac_sign(&fp, key),
f: fp,
m: SignedEnvelopeMode::Compressed,
e: None,
v: 3,
})
}
#[must_use]
pub fn wrap_with_ttl(input: &str, key: &str, ttl_secs: u64) -> Self {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let fp = fingerprint_str(input);
Self {
d: STANDARD.encode(input.as_bytes()),
sig: hmac_sign(&fp, key),
f: fp,
m: SignedEnvelopeMode::Ttl,
e: Some(now + ttl_secs),
v: 3,
}
}
pub fn unwrap_verified(&self, key: &str) -> Result<String, UniversalError> {
if !hmac_verify(&self.f, key, &self.sig) {
return Err(UniversalError::MalformedEnvelope(
"HMAC signature invalid -- wrong key or tampered envelope".into(),
));
}
if let Some(expiry) = self.e {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
if now >= expiry {
return Err(UniversalError::Expired {
expired_at: expiry,
now,
});
}
}
let bytes = match self.m {
SignedEnvelopeMode::Standard | SignedEnvelopeMode::Ttl => STANDARD
.decode(&self.d)
.map_err(|e| UniversalError::DecodeError(e.to_string()))?,
SignedEnvelopeMode::UrlSafe => URL_SAFE_NO_PAD
.decode(&self.d)
.map_err(|e| UniversalError::DecodeError(e.to_string()))?,
#[cfg(feature = "compression")]
SignedEnvelopeMode::Compressed => {
let compressed = STANDARD
.decode(&self.d)
.map_err(|e| UniversalError::DecodeError(e.to_string()))?;
decompress(&compressed)?
}
#[cfg(not(feature = "compression"))]
SignedEnvelopeMode::Compressed => {
return Err(UniversalError::DecodeError(
"compression feature not enabled".to_string(),
))
}
};
let decoded =
String::from_utf8(bytes).map_err(|e| UniversalError::DecodeError(e.to_string()))?;
let actual_fp = fingerprint_str(&decoded);
if actual_fp != self.f {
return Err(UniversalError::IntegrityViolation {
expected: self.f.clone(),
actual: actual_fp,
});
}
Ok(decoded)
}
pub fn to_json(&self) -> Result<String, UniversalError> {
serde_json::to_string(self).map_err(|e| UniversalError::SerializationError(e.to_string()))
}
pub fn from_json(s: &str) -> Result<Self, UniversalError> {
serde_json::from_str(s).map_err(|e| UniversalError::SerializationError(e.to_string()))
}
}