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