Skip to main content

entrouter_universal/
signed_envelope.rs

1// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2//  Entrouter Universal - HMAC-Signed Envelope v3
3//
4//  Like Envelope, but with HMAC-SHA256 authentication.
5//  Proves both integrity (SHA-256 fingerprint) AND origin
6//  (only someone with the key could produce the signature).
7//
8//  Modes: Standard, UrlSafe, Compressed, Ttl
9// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10
11use crate::{fingerprint_str, UniversalError};
12use base64::{
13    engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD},
14    Engine,
15};
16use hmac::{Hmac, Mac};
17use serde::{Deserialize, Serialize};
18use sha2::Sha256;
19use std::time::{SystemTime, UNIX_EPOCH};
20
21#[cfg(feature = "compression")]
22use crate::compress::{compress, decompress};
23
24type HmacSha256 = Hmac<Sha256>;
25
26/// The encoding mode used to create a [`SignedEnvelope`].
27#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
28pub enum SignedEnvelopeMode {
29    Standard,
30    UrlSafe,
31    Compressed,
32    Ttl,
33}
34
35/// A sealed, HMAC-authenticated envelope.
36///
37/// Carries data, its SHA-256 fingerprint, and an HMAC-SHA256 signature
38/// over the fingerprint. Unwrapping requires the same key used to sign.
39///
40/// # Example
41///
42/// ```
43/// use entrouter_universal::SignedEnvelope;
44///
45/// let env = SignedEnvelope::wrap("secret", "my-key");
46/// assert_eq!(env.unwrap_verified("my-key").unwrap(), "secret");
47/// assert!(env.unwrap_verified("wrong-key").is_err());
48/// ```
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct SignedEnvelope {
51    /// Encoded data
52    pub d: String,
53    /// SHA-256 fingerprint of the original raw input
54    pub f: String,
55    /// HMAC-SHA256 signature (hex) over the fingerprint
56    pub sig: String,
57    /// Encoding mode
58    pub m: SignedEnvelopeMode,
59    /// Optional expiry as Unix timestamp (seconds)
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub e: Option<u64>,
62    /// Version
63    pub v: u8,
64}
65
66fn hmac_sign(fingerprint: &str, key: &str) -> String {
67    let mut mac = HmacSha256::new_from_slice(key.as_bytes()).expect("HMAC accepts any key length");
68    mac.update(fingerprint.as_bytes());
69    hex::encode(mac.finalize().into_bytes())
70}
71
72fn hmac_verify(fingerprint: &str, key: &str, sig: &str) -> bool {
73    let mut mac = HmacSha256::new_from_slice(key.as_bytes()).expect("HMAC accepts any key length");
74    mac.update(fingerprint.as_bytes());
75    let expected = hex::decode(sig).unwrap_or_default();
76    mac.verify_slice(&expected).is_ok()
77}
78
79impl SignedEnvelope {
80    // ── Constructors ──────────────────────────────────────
81
82    /// Standard Base64 wrap with HMAC signature.
83    #[must_use]
84    pub fn wrap(input: &str, key: &str) -> Self {
85        let fp = fingerprint_str(input);
86        Self {
87            d: STANDARD.encode(input.as_bytes()),
88            sig: hmac_sign(&fp, key),
89            f: fp,
90            m: SignedEnvelopeMode::Standard,
91            e: None,
92            v: 3,
93        }
94    }
95
96    /// URL-safe Base64 wrap with HMAC signature.
97    #[must_use]
98    pub fn wrap_url_safe(input: &str, key: &str) -> Self {
99        let fp = fingerprint_str(input);
100        Self {
101            d: URL_SAFE_NO_PAD.encode(input.as_bytes()),
102            sig: hmac_sign(&fp, key),
103            f: fp,
104            m: SignedEnvelopeMode::UrlSafe,
105            e: None,
106            v: 3,
107        }
108    }
109
110    /// Compressed wrap with HMAC signature.
111    #[cfg(feature = "compression")]
112    pub fn wrap_compressed(input: &str, key: &str) -> Result<Self, UniversalError> {
113        let compressed = compress(input.as_bytes())?;
114        let fp = fingerprint_str(input);
115        Ok(Self {
116            d: STANDARD.encode(&compressed),
117            sig: hmac_sign(&fp, key),
118            f: fp,
119            m: SignedEnvelopeMode::Compressed,
120            e: None,
121            v: 3,
122        })
123    }
124
125    /// TTL wrap with HMAC signature.
126    #[must_use]
127    pub fn wrap_with_ttl(input: &str, key: &str, ttl_secs: u64) -> Self {
128        let now = SystemTime::now()
129            .duration_since(UNIX_EPOCH)
130            .unwrap_or_default()
131            .as_secs();
132        let fp = fingerprint_str(input);
133        Self {
134            d: STANDARD.encode(input.as_bytes()),
135            sig: hmac_sign(&fp, key),
136            f: fp,
137            m: SignedEnvelopeMode::Ttl,
138            e: Some(now + ttl_secs),
139            v: 3,
140        }
141    }
142
143    // ── Unwrap ────────────────────────────────────────────
144
145    /// Verify HMAC signature, then decode and verify integrity.
146    pub fn unwrap_verified(&self, key: &str) -> Result<String, UniversalError> {
147        // HMAC check first -- reject before decoding
148        if !hmac_verify(&self.f, key, &self.sig) {
149            return Err(UniversalError::MalformedEnvelope(
150                "HMAC signature invalid -- wrong key or tampered envelope".into(),
151            ));
152        }
153
154        // TTL check
155        if let Some(expiry) = self.e {
156            let now = SystemTime::now()
157                .duration_since(UNIX_EPOCH)
158                .unwrap_or_default()
159                .as_secs();
160            if now >= expiry {
161                return Err(UniversalError::Expired {
162                    expired_at: expiry,
163                    now,
164                });
165            }
166        }
167
168        // Decode
169        let bytes = match self.m {
170            SignedEnvelopeMode::Standard | SignedEnvelopeMode::Ttl => STANDARD
171                .decode(&self.d)
172                .map_err(|e| UniversalError::DecodeError(e.to_string()))?,
173            SignedEnvelopeMode::UrlSafe => URL_SAFE_NO_PAD
174                .decode(&self.d)
175                .map_err(|e| UniversalError::DecodeError(e.to_string()))?,
176            #[cfg(feature = "compression")]
177            SignedEnvelopeMode::Compressed => {
178                let compressed = STANDARD
179                    .decode(&self.d)
180                    .map_err(|e| UniversalError::DecodeError(e.to_string()))?;
181                decompress(&compressed)?
182            }
183            #[cfg(not(feature = "compression"))]
184            SignedEnvelopeMode::Compressed => {
185                return Err(UniversalError::DecodeError(
186                    "compression feature not enabled".to_string(),
187                ))
188            }
189        };
190
191        let decoded =
192            String::from_utf8(bytes).map_err(|e| UniversalError::DecodeError(e.to_string()))?;
193
194        // Verify fingerprint
195        let actual_fp = fingerprint_str(&decoded);
196        if actual_fp != self.f {
197            return Err(UniversalError::IntegrityViolation {
198                expected: self.f.clone(),
199                actual: actual_fp,
200            });
201        }
202
203        Ok(decoded)
204    }
205
206    /// Serialize to JSON.
207    pub fn to_json(&self) -> Result<String, UniversalError> {
208        serde_json::to_string(self).map_err(|e| UniversalError::SerializationError(e.to_string()))
209    }
210
211    /// Deserialize from JSON.
212    pub fn from_json(s: &str) -> Result<Self, UniversalError> {
213        serde_json::from_str(s).map_err(|e| UniversalError::SerializationError(e.to_string()))
214    }
215}