use base64::{
engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD},
Engine,
};
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::{fingerprint_str, UniversalError};
#[cfg(feature = "compression")]
use crate::compress::{compress, decompress};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum EnvelopeMode {
Standard,
UrlSafe,
Compressed,
Ttl,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Envelope {
pub d: String,
pub f: String,
pub m: EnvelopeMode,
#[serde(skip_serializing_if = "Option::is_none")]
pub e: Option<u64>,
pub v: u8,
}
impl Envelope {
#[must_use]
pub fn wrap(input: &str) -> Self {
Self {
d: STANDARD.encode(input.as_bytes()),
f: fingerprint_str(input),
m: EnvelopeMode::Standard,
e: None,
v: 3,
}
}
#[must_use]
pub fn wrap_url_safe(input: &str) -> Self {
Self {
d: URL_SAFE_NO_PAD.encode(input.as_bytes()),
f: fingerprint_str(input),
m: EnvelopeMode::UrlSafe,
e: None,
v: 3,
}
}
#[cfg(feature = "compression")]
pub fn wrap_compressed(input: &str) -> Result<Self, UniversalError> {
let compressed = compress(input.as_bytes())?;
Ok(Self {
d: STANDARD.encode(&compressed),
f: fingerprint_str(input),
m: EnvelopeMode::Compressed,
e: None,
v: 3,
})
}
#[must_use]
pub fn wrap_with_ttl(input: &str, ttl_secs: u64) -> Self {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Self {
d: STANDARD.encode(input.as_bytes()),
f: fingerprint_str(input),
m: EnvelopeMode::Ttl,
e: Some(now + ttl_secs),
v: 3,
}
}
pub fn unwrap_verified(&self) -> Result<String, UniversalError> {
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 {
EnvelopeMode::Standard | EnvelopeMode::Ttl => {
STANDARD.decode(&self.d)
.map_err(|e| UniversalError::DecodeError(e.to_string()))?
}
EnvelopeMode::UrlSafe => {
URL_SAFE_NO_PAD.decode(&self.d)
.map_err(|e| UniversalError::DecodeError(e.to_string()))?
}
#[cfg(feature = "compression")]
EnvelopeMode::Compressed => {
let compressed = STANDARD.decode(&self.d)
.map_err(|e| UniversalError::DecodeError(e.to_string()))?;
decompress(&compressed)?
}
#[cfg(not(feature = "compression"))]
EnvelopeMode::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 unwrap_raw(&self) -> Result<String, UniversalError> {
let bytes = match self.m {
EnvelopeMode::Standard | EnvelopeMode::Ttl => {
STANDARD.decode(&self.d)
.map_err(|e| UniversalError::DecodeError(e.to_string()))?
}
EnvelopeMode::UrlSafe => {
URL_SAFE_NO_PAD.decode(&self.d)
.map_err(|e| UniversalError::DecodeError(e.to_string()))?
}
#[cfg(feature = "compression")]
EnvelopeMode::Compressed => {
let compressed = STANDARD.decode(&self.d)
.map_err(|e| UniversalError::DecodeError(e.to_string()))?;
decompress(&compressed)?
}
#[cfg(not(feature = "compression"))]
EnvelopeMode::Compressed => {
return Err(UniversalError::DecodeError(
"compression feature not enabled".to_string()
))
}
};
String::from_utf8(bytes)
.map_err(|e| UniversalError::DecodeError(e.to_string()))
}
pub fn is_expired(&self) -> bool {
if let Some(expiry) = self.e {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
return now >= expiry;
}
false
}
pub fn ttl_remaining(&self) -> Option<u64> {
let expiry = self.e?;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Some(expiry.saturating_sub(now))
}
pub fn is_intact(&self) -> bool {
self.unwrap_verified().is_ok()
}
pub fn fingerprint(&self) -> &str {
&self.f
}
pub fn mode(&self) -> EnvelopeMode {
self.m
}
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()))
}
}