Skip to main content

alterion_encrypt/tools/
serializer.rs

1// SPDX-License-Identifier: GPL-3.0
2//! Wire-format serialisation and the client/server encryption pipeline.
3//!
4//! ## Request pipeline (client → server)
5//!
6//! ```text
7//! T (Serialize)
8//!   → serde_json::to_vec
9//!   → deflate compress
10//!   → msgpack encode (ByteBuf)
11//!   → AES-256-GCM encrypt  (random enc_key)
12//!   → ECDH wrap enc_key    (ephemeral X25519 + HKDF-SHA256 wrap key)
13//!   → Request { data, wrapped_key, client_pk, key_id, ts }
14//!   → msgpack encode
15//!   → send over the wire
16//! ```
17//!
18//! On the server side [`Interceptor`](crate::interceptor::Interceptor) calls
19//! [`deserialize_packet`] → ECDH → [`derive_wrap_key`] → unwrap `enc_key` → AES-GCM decrypt →
20//! injects [`DecryptedBody`](crate::interceptor::DecryptedBody). Handlers then call
21//! [`decode_request_payload`] to finish the deserialisation.
22//!
23//! ## Response pipeline (server → client)
24//!
25//! ```text
26//! raw JSON bytes
27//!   → deflate compress
28//!   → msgpack encode
29//!   → AES-256-GCM encrypt  (same enc_key the client generated)
30//!   → HMAC-SHA256          (mac key = HKDF-SHA256(enc_key, "alterion-response-mac"))
31//!   → Response { payload, hmac }
32//!   → msgpack encode
33//! ```
34//!
35//! Clients call [`decode_response_packet`] which verifies the HMAC before decrypting.
36//!
37//! ## Replay protection
38//! Every [`Request`] carries a Unix timestamp (`ts`). [`deserialize_packet`] rejects packets whose
39//! `ts` deviates more than [`REPLAY_WINDOW_SECS`] (30 s) from the server clock. Combined with the
40//! optional Redis `replay_store` in the interceptor, this prevents both delayed-replay and
41//! duplicate-submission attacks.
42use serde::{Deserialize, Serialize};
43use serde::de::DeserializeOwned;
44use serde_bytes::ByteBuf;
45use flate2::write::DeflateEncoder;
46use flate2::read::DeflateDecoder;
47use flate2::Compression;
48use std::io::{Write, Read};
49use hkdf::Hkdf;
50use sha2::Sha256;
51use crate::tools::helper::hmac;
52use crate::tools::crypt::{aes_encrypt, aes_decrypt};
53use x25519_dalek::{EphemeralSecret, PublicKey as X25519PublicKey};
54use rand_core::{RngCore, OsRng};
55
56/// Maximum acceptable timestamp skew (seconds) between client and server for replay protection.
57pub const REPLAY_WINDOW_SECS: i64 = 30;
58
59/// Default cap on decompressed payload size used by [`decode_request_payload`] and [`decode_response_packet`].
60/// The [`crate::interceptor::Interceptor`] exposes `max_decompressed_bytes` so operators can set
61/// a higher (or lower) limit per deployment without recompiling.
62pub const MAX_DECOMPRESSED_SIZE: usize = 10 * 1024 * 1024; // 10 MiB
63
64/// Derives a 32-byte AES wrapping key from the ECDH shared secret via HKDF-SHA256,
65/// binding both parties' public keys into the derivation via the salt.
66///
67/// Used server-side to unwrap the client's randomly-generated AES key from the `Request`.
68pub fn derive_wrap_key(
69    shared_secret: &[u8; 32],
70    client_pk:     &[u8; 32],
71    server_pk:     &[u8; 32],
72) -> [u8; 32] {
73    let mut salt = [0u8; 64];
74    salt[..32].copy_from_slice(client_pk);
75    salt[32..].copy_from_slice(server_pk);
76    let hk = Hkdf::<Sha256>::new(Some(&salt), shared_secret);
77    let mut key = [0u8; 32];
78    hk.expand(b"alterion-wrap", &mut key).expect("HKDF expand failed");
79    key
80}
81
82/// Derives a 32-byte HMAC key from the session AES key via HKDF-SHA256.
83///
84/// Keeps the HMAC key domain-separated from the AES encryption key so neither leaks information
85/// about the other. Used internally by [`build_signed_response_raw`] and [`decode_response_packet`].
86fn derive_response_mac_key(enc_key: &[u8; 32]) -> [u8; 32] {
87    let hk = Hkdf::<Sha256>::new(None, enc_key);
88    let mut mac_key = [0u8; 32];
89    hk.expand(b"alterion-response-mac", &mut mac_key).expect("HKDF expand failed");
90    mac_key
91}
92
93/// Outgoing encrypted request packet produced by [`build_request_packet`].
94///
95/// `data` holds the AES-256-GCM ciphertext. `kx` is the session key material encrypted under the
96/// ECDH-derived wrap key; the server recovers it via ECDH to decrypt `data`. `client_pk` is the
97/// ephemeral X25519 public key. Integrity is guaranteed by the AES-GCM tags on both fields.
98#[derive(Debug, Serialize, Deserialize)]
99pub struct Request {
100    pub data:      ByteBuf,
101    pub kx:        ByteBuf,
102    pub client_pk: ByteBuf,
103    pub key_id:    String,
104    pub ts:        i64,
105}
106
107/// Encrypted response packet produced by [`build_signed_response_raw`].
108///
109/// `payload` is the AES-256-GCM-encrypted response body. `hmac` is HMAC-SHA256 over the
110/// ciphertext, keyed with a mac key derived from `enc_key` — verified by the client before
111/// decrypting via [`decode_response_packet`].
112#[derive(Debug, Serialize, Deserialize)]
113pub struct Response {
114    pub payload: ByteBuf,
115    pub hmac:    ByteBuf,
116}
117
118#[derive(Debug, thiserror::Error)]
119pub enum SerializerError {
120    #[error("serialize error: {0}")]
121    Serialize(String),
122    #[error("deserialize error: {0}")]
123    Deserialize(String),
124    #[error("compress error: {0}")]
125    Compress(String),
126    #[error("decompress error: {0}")]
127    Decompress(String),
128}
129
130impl From<SerializerError> for actix_web::Error {
131    fn from(e: SerializerError) -> Self {
132        actix_web::error::ErrorInternalServerError(e.to_string())
133    }
134}
135
136/// Encodes a value to MessagePack bytes using named fields.
137pub fn serialize<T: Serialize>(value: &T) -> Result<Vec<u8>, SerializerError> {
138    rmp_serde::to_vec_named(value)
139        .map_err(|e| SerializerError::Serialize(e.to_string()))
140}
141
142/// Decodes MessagePack bytes into the target type.
143pub fn deserialize<T: DeserializeOwned>(data: &[u8]) -> Result<T, SerializerError> {
144    rmp_serde::from_slice(data)
145        .map_err(|e| SerializerError::Deserialize(e.to_string()))
146}
147
148/// Deflate-compresses `data` and returns the compressed bytes.
149pub fn compress(data: &[u8]) -> Result<Vec<u8>, SerializerError> {
150    let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
151    encoder.write_all(data)
152        .map_err(|e: std::io::Error| SerializerError::Compress(e.to_string()))?;
153    encoder.finish()
154        .map_err(|e: std::io::Error| SerializerError::Compress(e.to_string()))
155}
156
157/// Deflate-decompresses `data`, rejecting output that exceeds `max_size` bytes.
158///
159/// Pass [`MAX_DECOMPRESSED_SIZE`] for the standard limit, or a deployment-specific value
160/// when called from the [`crate::interceptor::Interceptor`].
161pub fn decompress(data: &[u8], max_size: usize) -> Result<Vec<u8>, SerializerError> {
162    let mut decoder = DeflateDecoder::new(data);
163    let mut out     = Vec::new();
164    decoder
165        .by_ref()
166        .take(max_size as u64 + 1)
167        .read_to_end(&mut out)
168        .map_err(|e: std::io::Error| SerializerError::Decompress(e.to_string()))?;
169    if out.len() > max_size {
170        return Err(SerializerError::Decompress(
171            "decompressed payload exceeds size limit".into(),
172        ));
173    }
174    Ok(out)
175}
176
177/// Deserialises and timestamp-validates a [`Request`].
178///
179/// Returns an error if `ts` deviates more than ±30 seconds from the server clock.
180/// After this succeeds, call [`derive_wrap_key`] via ECDH to unwrap the AES key and decrypt.
181pub fn deserialize_packet(data: &[u8]) -> Result<Request, SerializerError> {
182    let packet = deserialize::<Request>(data)?;
183    let now = std::time::SystemTime::now()
184        .duration_since(std::time::UNIX_EPOCH)
185        .map_err(|e| SerializerError::Deserialize(format!("system clock error: {e}")))?
186        .as_secs() as i64;
187    if (packet.ts - now).abs() > REPLAY_WINDOW_SECS {
188        return Err(SerializerError::Deserialize(
189            format!("timestamp out of window: skew={}s", packet.ts - now)
190        ));
191    }
192    Ok(packet)
193}
194
195
196/// Decodes a request payload from AES-decrypted bytes:
197/// msgpack decode → deflate decompress → JSON deserialise.
198pub fn decode_request_payload<T: DeserializeOwned>(
199    decrypted_data: &[u8],
200) -> Result<T, SerializerError> {
201    let compressed: ByteBuf = deserialize(decrypted_data)?;
202    let json_bytes          = decompress(&compressed, MAX_DECOMPRESSED_SIZE)?;
203    serde_json::from_slice(&json_bytes)
204        .map_err(|e| SerializerError::Deserialize(e.to_string()))
205}
206
207/// Serialises `value` to JSON then passes it through `build_signed_response_raw`.
208pub fn build_signed_response<T: Serialize>(
209    value:   &T,
210    enc_key: &[u8; 32],
211) -> Result<Vec<u8>, SerializerError> {
212    let json_bytes = serde_json::to_vec(value)
213        .map_err(|e| SerializerError::Serialize(e.to_string()))?;
214    build_signed_response_raw(&json_bytes, enc_key)
215}
216
217/// Builds a signed response from raw JSON bytes:
218/// deflate compress → msgpack → AES-256-GCM (enc_key) → HMAC-SHA256 (mac_key derived from enc_key) → `Response` → msgpack.
219pub fn build_signed_response_raw(
220    json_bytes: &[u8],
221    enc_key:    &[u8; 32],
222) -> Result<Vec<u8>, SerializerError> {
223    let compressed = compress(json_bytes)?;
224    let msgpacked  = serialize(&ByteBuf::from(compressed))?;
225    let encrypted  = aes_encrypt(&msgpacked, enc_key)
226        .map_err(|e| SerializerError::Serialize(e.to_string()))?;
227    let mac_key    = derive_response_mac_key(enc_key);
228    let sig        = hmac::sign(&encrypted, &mac_key);
229    let response   = Response {
230        payload: ByteBuf::from(encrypted),
231        hmac:    ByteBuf::from(sig),
232    };
233    serialize(&response)
234}
235
236/// Builds an encrypted request packet ready to send to the server.
237///
238/// ## Pipeline
239/// `T` → JSON → deflate compress → msgpack (`ByteBuf`) → AES-256-GCM (random `enc_key`) →
240/// ECDH-wrap `enc_key` → [`Request`] → msgpack
241///
242/// A fresh random AES-256 key is generated per call and used to encrypt the payload. An ephemeral
243/// X25519 keypair is generated, ECDH is performed against `server_pk`, and the AES key is wrapped
244/// with the HKDF-derived wrap key so only the server can recover it. Integrity of both the payload
245/// and the wrapped key is guaranteed by the AES-GCM authentication tags.
246///
247/// # Arguments
248/// * `value`     – Any `serde::Serialize` payload.
249/// * `server_pk` – Server's 32-byte X25519 public key (from the server's key endpoint).
250/// * `key_id`    – Key identifier returned alongside the server's public key.
251///
252/// # Returns
253/// `(wire_bytes, enc_key)` — store `enc_key` client-side indexed by request ID and pass it to
254/// [`decode_response_packet`] when the server's reply arrives.
255pub fn build_request_packet<T: Serialize>(
256    value:     &T,
257    server_pk: &[u8; 32],
258    key_id:    String,
259) -> Result<(Vec<u8>, [u8; 32]), SerializerError> {
260    let json_bytes = serde_json::to_vec(value)
261        .map_err(|e| SerializerError::Serialize(e.to_string()))?;
262    let compressed = compress(&json_bytes)?;
263    let msgpacked  = serialize(&ByteBuf::from(compressed))?;
264
265    let mut enc_key = [0u8; 32];
266    OsRng.fill_bytes(&mut enc_key);
267
268    let encrypted = aes_encrypt(&msgpacked, &enc_key)
269        .map_err(|e| SerializerError::Serialize(e.to_string()))?;
270
271    let client_sk       = EphemeralSecret::random_from_rng(OsRng);
272    let client_pk       = X25519PublicKey::from(&client_sk);
273    let server_pub      = X25519PublicKey::from(*server_pk);
274    let shared          = client_sk.diffie_hellman(&server_pub);
275    let client_pk_bytes = client_pk.to_bytes();
276
277    let wrap_key    = derive_wrap_key(shared.as_bytes(), &client_pk_bytes, server_pk);
278    let kx = aes_encrypt(&enc_key, &wrap_key)
279        .map_err(|e| SerializerError::Serialize(e.to_string()))?;
280
281    let ts = std::time::SystemTime::now()
282        .duration_since(std::time::UNIX_EPOCH)
283        .map_err(|e| SerializerError::Serialize(format!("system clock error: {e}")))?
284        .as_secs() as i64;
285
286    let packet = Request {
287        data:      ByteBuf::from(encrypted),
288        kx:        ByteBuf::from(kx),
289        client_pk: ByteBuf::from(client_pk_bytes.to_vec()),
290        key_id,
291        ts,
292    };
293    let wire_bytes = serialize(&packet)?;
294
295    Ok((wire_bytes, enc_key))
296}
297
298/// Decodes and verifies a server [`Response`] using the AES key returned by [`build_request_packet`].
299///
300/// ## Pipeline
301/// msgpack → [`Response`] → HMAC-SHA256 verify (enc_key) → AES-256-GCM decrypt → msgpack →
302/// deflate decompress → JSON → `T`
303///
304/// Returns `Err` if the HMAC is invalid, decryption fails, or deserialization fails.
305pub fn decode_response_packet<T: DeserializeOwned>(
306    data:    &[u8],
307    enc_key: &[u8; 32],
308) -> Result<T, SerializerError> {
309    let signed:  Response = deserialize(data)?;
310    let mac_key = derive_response_mac_key(enc_key);
311
312    if !hmac::verify(signed.payload.as_ref(), &mac_key, signed.hmac.as_ref()) {
313        return Err(SerializerError::Deserialize("response HMAC invalid".into()));
314    }
315
316    let decrypted        = aes_decrypt(signed.payload.as_ref(), enc_key)
317        .map_err(|e| SerializerError::Deserialize(e.to_string()))?;
318    let compressed: ByteBuf = deserialize(&decrypted)?;
319    let json_bytes       = decompress(&compressed, MAX_DECOMPRESSED_SIZE)?;
320
321    serde_json::from_slice(&json_bytes)
322        .map_err(|e| SerializerError::Deserialize(e.to_string()))
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328    use serde::{Deserialize, Serialize};
329    use crate::tools::crypt::aes_decrypt;
330
331    #[derive(Debug, PartialEq, Serialize, Deserialize)]
332    struct TestPayload { id: u32, name: String, flag: bool }
333
334    fn sample() -> TestPayload { TestPayload { id: 42, name: "alterion".into(), flag: true } }
335
336    fn test_enc_key() -> [u8; 32] { [0x42u8; 32] }
337
338    #[test]
339    fn compress_decompress_roundtrip() {
340        let data = b"hello alterion enc pipeline payload";
341        assert_eq!(decompress(&compress(data).unwrap(), MAX_DECOMPRESSED_SIZE).unwrap(), data);
342    }
343
344    #[test]
345    fn decode_request_payload_roundtrip() {
346        let original   = sample();
347        let json_bytes = serde_json::to_vec(&original).unwrap();
348        let compressed = compress(&json_bytes).unwrap();
349        let msgpacked  = serialize(&ByteBuf::from(compressed)).unwrap();
350        let decoded: TestPayload = decode_request_payload(&msgpacked).unwrap();
351        assert_eq!(original, decoded);
352    }
353
354    #[test]
355    fn derive_wrap_key_bound_to_public_keys() {
356        let shared    = [0x42u8; 32];
357        let client_pk = [0x01u8; 32];
358        let server_pk = [0x02u8; 32];
359        let k1 = derive_wrap_key(&shared, &client_pk, &server_pk);
360        let k2 = derive_wrap_key(&shared, &server_pk, &client_pk);
361        assert_ne!(k1, k2);
362    }
363
364    #[test]
365    fn build_signed_response_roundtrip() {
366        let enc_key = test_enc_key();
367        let payload = sample();
368        let bytes   = build_signed_response(&payload, &enc_key).unwrap();
369        let signed: Response = deserialize(&bytes).unwrap();
370
371        let mac_key = derive_response_mac_key(&enc_key);
372        assert_eq!(signed.hmac.as_ref(), hmac::sign(&signed.payload, &mac_key).as_slice());
373
374        let decrypted: Vec<u8>   = aes_decrypt(&signed.payload, &enc_key).unwrap();
375        let compressed: ByteBuf  = deserialize(&decrypted).unwrap();
376        let json_bytes           = decompress(&compressed, MAX_DECOMPRESSED_SIZE).unwrap();
377        let decoded: TestPayload = serde_json::from_slice(&json_bytes).unwrap();
378        assert_eq!(payload, decoded);
379    }
380
381    #[test]
382    fn decompress_garbage_returns_error() {
383        assert!(decompress(b"not compressed", MAX_DECOMPRESSED_SIZE).is_err());
384    }
385
386    /// Full client→server→client round trip with actual ephemeral ECDH and AES key wrapping.
387    /// Mirrors the steps the interceptor performs on the server side.
388    #[test]
389    fn request_response_full_roundtrip() {
390        let server_sk       = EphemeralSecret::random_from_rng(OsRng);
391        let server_pk       = X25519PublicKey::from(&server_sk);
392        let server_pk_bytes: [u8; 32] = server_pk.to_bytes();
393
394        let (wire, client_enc_key) =
395            build_request_packet(&sample(), &server_pk_bytes, "test-key".to_string()).unwrap();
396
397        let packet: Request           = deserialize(&wire).unwrap();
398        let client_pk_bytes: [u8; 32] = packet.client_pk.as_ref().try_into().unwrap();
399        let client_pub                = X25519PublicKey::from(client_pk_bytes);
400        let shared                    = server_sk.diffie_hellman(&client_pub);
401        let wrap_key                  = derive_wrap_key(shared.as_bytes(), &client_pk_bytes, &server_pk_bytes);
402
403        let enc_key_bytes             = aes_decrypt(packet.kx.as_ref(), &wrap_key).unwrap();
404        let srv_enc_key: [u8; 32]     = enc_key_bytes.as_slice().try_into().unwrap();
405        assert_eq!(client_enc_key, srv_enc_key);
406
407        let decrypted: TestPayload = decode_request_payload(
408            &aes_decrypt(packet.data.as_ref(), &srv_enc_key).unwrap()
409        ).unwrap();
410        assert_eq!(decrypted, sample());
411
412        let response_bytes = build_signed_response(&sample(), &srv_enc_key).unwrap();
413        let decoded: TestPayload =
414            decode_response_packet(&response_bytes, &client_enc_key).unwrap();
415        assert_eq!(decoded, sample());
416    }
417
418    #[test]
419    fn decode_response_packet_rejects_tampered_hmac() {
420        let enc_key   = test_enc_key();
421        let mut bytes = build_signed_response(&sample(), &enc_key).unwrap();
422        let last      = bytes.len() - 1;
423        bytes[last] ^= 0xFF;
424        assert!(decode_response_packet::<TestPayload>(&bytes, &enc_key).is_err());
425    }
426
427    #[test]
428    fn decode_response_packet_rejects_wrong_key() {
429        let enc_key   = test_enc_key();
430        let bytes     = build_signed_response(&sample(), &enc_key).unwrap();
431        let wrong_key = [0x00u8; 32];
432        assert!(decode_response_packet::<TestPayload>(&bytes, &wrong_key).is_err());
433    }
434}