Skip to main content

alterion_encrypt/tools/
serializer.rs

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