Skip to main content

toolkit_zero/serialization/
veil.rs

1// VEIL — Variable-Expansion Interleaved Lattice cipher
2//
3// This file implements the full seal/open pipeline.  Nothing in this file
4// imports from `serde` or any external crypto crate.  The only external
5// crate used is `bincode` (with its own `Encode`/`Decode` traits) for the
6// outermost byte-level framing and the initial struct → bytes step.
7
8use bincode::{
9    config::standard,
10    encode_to_vec, decode_from_slice,
11    Encode, Decode,
12    error::{EncodeError, DecodeError},
13};
14
15// ─── public error type ────────────────────────────────────────────────────────
16
17/// Errors returned by [`seal`] and [`open`].
18#[derive(Debug)]
19pub enum SerializationError {
20    /// The struct could not be encoded to bytes by `bincode`.
21    Encode(EncodeError),
22    /// The byte blob could not be decoded / the key is wrong.
23    Decode(DecodeError),
24}
25
26impl std::fmt::Display for SerializationError {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        match self {
29            Self::Encode(e) => write!(f, "seal encode error: {e}"),
30            Self::Decode(e) => write!(f, "open decode error: {e}"),
31        }
32    }
33}
34
35impl std::error::Error for SerializationError {}
36
37impl From<EncodeError> for SerializationError {
38    fn from(e: EncodeError) -> Self { Self::Encode(e) }
39}
40
41impl From<DecodeError> for SerializationError {
42    fn from(e: DecodeError) -> Self { Self::Decode(e) }
43}
44
45// ─── default key ─────────────────────────────────────────────────────────────
46
47const DEFAULT_KEY: &str = "serialization/deserialization";
48const BLOCK: usize = 16;
49
50// ─── public API ──────────────────────────────────────────────────────────────
51
52/// Encode `value` to an opaque byte blob sealed with `key`.
53///
54/// If `key` is `None` the default key `"serialization/deserialization"` is
55/// used.  The resulting blob can only be decoded by [`open`] with the same
56/// key.
57///
58/// # Errors
59///
60/// Returns [`SerializationError::Encode`] if `bincode` cannot serialize the
61/// value.
62pub fn seal<T: Encode>(value: &T, key: Option<&str>) -> Result<Vec<u8>, SerializationError> {
63    let key = key.unwrap_or(DEFAULT_KEY);
64
65    // Step 0: struct → raw bytes via bincode
66    let plain = encode_to_vec(value, standard())?;
67
68    // Steps 1—5: VEIL forward transform
69    let cipher = veil_encrypt(&plain, key);
70
71    // Outer bincode framing: Vec<u8> → length-prefixed blob
72    let blob = encode_to_vec(&cipher, standard())?;
73    Ok(blob)
74}
75
76/// Decode a byte blob produced by [`seal`] back into `T`.
77///
78/// If `key` is `None` the default key is used.
79///
80/// # Errors
81///
82/// Returns [`SerializationError::Decode`] if the blob is malformed or the
83/// key is incorrect.
84pub fn open<T: Decode<()>>(blob: &[u8], key: Option<&str>) -> Result<T, SerializationError> {
85    let key = key.unwrap_or(DEFAULT_KEY);
86
87    // Outer bincode unframing
88    let (cipher, _): (Vec<u8>, _) = decode_from_slice(blob, standard())?;
89
90    // Steps 5—1 reversed
91    let plain = veil_decrypt(&cipher, key)?;
92
93    // raw bytes → struct via bincode
94    let (value, _): (T, _) = decode_from_slice(&plain, standard())?;
95    Ok(value)
96}
97
98// ─── VEIL forward (encrypt) ──────────────────────────────────────────────────
99
100fn veil_encrypt(plain: &[u8], key: &str) -> Vec<u8> {
101    let ks = KeySchedule::new(key);
102
103    // 1. S-box substitution (key-dependent permutation table)
104    let mut buf: Vec<u8> = plain.iter().map(|&b| ks.sbox[b as usize]).collect();
105
106    // 2. Key-stream XOR
107    for (i, b) in buf.iter_mut().enumerate() {
108        *b ^= ks.stream_byte(i);
109    }
110
111    // 3. Position mixing:  b[i] ^= mix(i, b[i-1], b[i+1])
112    position_mix_forward(&mut buf);
113
114    // 4. Block diffusion (sequential accumulator across 16-byte blocks)
115    block_diffuse_forward(&mut buf);
116
117    // 5. Shuffle bytes within each 16-byte block
118    block_shuffle_forward(&mut buf, &ks);
119
120    buf
121}
122
123// ─── VEIL reverse (decrypt) ──────────────────────────────────────────────────
124
125fn veil_decrypt(cipher: &[u8], key: &str) -> Result<Vec<u8>, SerializationError> {
126    let ks = KeySchedule::new(key);
127
128    let mut buf = cipher.to_vec();
129
130    // 5 reverse: un-shuffle bytes within each 16-byte block
131    block_shuffle_reverse(&mut buf, &ks);
132
133    // 4 reverse: undo sequential accumulator
134    block_diffuse_reverse(&mut buf);
135
136    // 3 reverse: undo position mixing
137    position_mix_reverse(&mut buf);
138
139    // 2 reverse: XOR again with same key-stream (XOR is its own inverse)
140    for (i, b) in buf.iter_mut().enumerate() {
141        *b ^= ks.stream_byte(i);
142    }
143
144    // 1 reverse: inverse S-box
145    for b in buf.iter_mut() {
146        *b = ks.sbox_inv[*b as usize];
147    }
148
149    Ok(buf)
150}
151
152// ─── Key schedule ─────────────────────────────────────────────────────────────
153//
154// All keying material is derived from a FNV-1a hash of the key string fed into
155// a splitmix64 PRNG.  No standard crypto hash or cipher is used.
156
157struct KeySchedule {
158    /// Forward S-box: substitution table indexed by plaintext byte.
159    sbox:     [u8; 256],
160    /// Inverse S-box: indexed by ciphertext byte.
161    sbox_inv: [u8; 256],
162    /// PRNG seed for per-position stream bytes.
163    stream_seed: u64,
164    /// PRNG seed for block shuffles.
165    shuffle_seed: u64,
166}
167
168impl KeySchedule {
169    fn new(key: &str) -> Self {
170        // ── keyed FNV-1a hash → two independent 64-bit seeds ─────────────
171        let h0 = fnv1a_64(key.as_bytes());
172        let h1 = splitmix64(h0 ^ 0xdeadbeef_cafebabe);
173
174        // ── build S-box with Fisher-Yates using splitmix64 PRNG ───────────
175        let mut sbox: [u8; 256] = std::array::from_fn(|i| i as u8);
176        let mut rng = Rng::new(h0);
177        for i in (1..256usize).rev() {
178            let j = (rng.next() as usize) % (i + 1);
179            sbox.swap(i, j);
180        }
181
182        // ── inverse S-box ─────────────────────────────────────────────────
183        let mut sbox_inv = [0u8; 256];
184        for (i, &v) in sbox.iter().enumerate() {
185            sbox_inv[v as usize] = i as u8;
186        }
187
188        Self {
189            sbox,
190            sbox_inv,
191            stream_seed: h1,
192            shuffle_seed: splitmix64(h1 ^ 0x1234567890abcdef),
193        }
194    }
195
196    /// One key-stream byte at position `pos`.
197    ///
198    /// Uses two splitmix64 steps from an index-salted version of the stream
199    /// seed so that each position produces a distinct, unpredictable byte.
200    #[inline]
201    fn stream_byte(&self, pos: usize) -> u8 {
202        let salted = self.stream_seed
203            .wrapping_add(pos as u64)
204            .wrapping_mul(0x9e3779b97f4a7c15);
205        let v = splitmix64(salted);
206        // XOR the high and low halves for extra mixing
207        ((v >> 32) ^ (v & 0xffff_ffff)) as u8
208    }
209
210    /// Generate the shuffle permutation for block `block_index` with a given length.
211    ///
212    /// When `len` == BLOCK this produces the standard 16-element permutation.
213    /// When `len` < BLOCK (tail block) it produces a bijective permutation of
214    /// exactly `len` elements.
215    fn block_perm_len(&self, block_index: usize, len: usize) -> Vec<usize> {
216        let seed = self.shuffle_seed
217            .wrapping_add(block_index as u64)
218            .wrapping_mul(0x6c62272e07bb0142);
219        let mut perm: Vec<usize> = (0..len).collect();
220        let mut rng = Rng::new(splitmix64(seed));
221        for i in (1..len).rev() {
222            let j = (rng.next() as usize) % (i + 1);
223            perm.swap(i, j);
224        }
225        perm
226    }
227
228    /// Generate the full-block (BLOCK-length) shuffle permutation.
229    fn block_perm(&self, block_index: usize) -> [usize; BLOCK] {
230        let v = self.block_perm_len(block_index, BLOCK);
231        std::array::from_fn(|i| v[i])
232    }
233}
234
235// ─── PRNG ─────────────────────────────────────────────────────────────────────
236
237struct Rng(u64);
238
239impl Rng {
240    fn new(seed: u64) -> Self { Self(seed) }
241    #[inline]
242    fn next(&mut self) -> u64 {
243        self.0 = splitmix64(self.0);
244        self.0
245    }
246}
247
248/// splitmix64 — single step.
249#[inline]
250fn splitmix64(mut x: u64) -> u64 {
251    x = x.wrapping_add(0x9e3779b97f4a7c15);
252    x = (x ^ (x >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
253    x = (x ^ (x >> 27)).wrapping_mul(0x94d049bb133111eb);
254    x ^ (x >> 31)
255}
256
257/// FNV-1a 64-bit hash.
258#[inline]
259fn fnv1a_64(data: &[u8]) -> u64 {
260    const OFFSET: u64 = 0xcbf29ce484222325;
261    const PRIME:  u64 = 0x00000100000001b3;
262    let mut h = OFFSET;
263    for &b in data {
264        h ^= b as u64;
265        h = h.wrapping_mul(PRIME);
266    }
267    h
268}
269
270// ─── Step 3: position mixing ──────────────────────────────────────────────────
271//
272// Forward:  b[i] ^= (i as u8).wrapping_add(prev).wrapping_mul(0x6b ^ next_preview)
273// The "preview" of b[i+1] uses the *original* (pre-mix) value, so the reverse
274// can be computed left-to-right without looking ahead at a transformed value.
275
276fn position_mix_forward(buf: &mut [u8]) {
277    // Sequential left-to-right: mix at position i depends only on the
278    // *original* (pre-mix) value of the left neighbour, so the inverse is
279    // trivially computable left-to-right without lookahead.
280    let mut prev_orig: u8 = 0xA7;
281    for (i, b) in buf.iter_mut().enumerate() {
282        let original = *b;
283        let mix = (i as u8).wrapping_add(prev_orig).wrapping_mul(0x6b);
284        *b ^= mix;
285        prev_orig = original;
286    }
287}
288
289fn position_mix_reverse(buf: &mut [u8]) {
290    // Mirror of forward: recover original left-to-right, feeding each
291    // recovered byte as the left-neighbour input for the next position.
292    let mut prev_orig: u8 = 0xA7;
293    for (i, b) in buf.iter_mut().enumerate() {
294        let mix = (i as u8).wrapping_add(prev_orig).wrapping_mul(0x6b);
295        let original = *b ^ mix;
296        *b = original;
297        prev_orig = original;
298    }
299}
300
301// ─── Step 4: block diffusion ──────────────────────────────────────────────────
302//
303// Forward:  process blocks left-to-right; accumulator starts at 0xB3.
304//           For each byte in the block: out = in ^ acc; acc = acc.rotate_left(3) ^ out
305// The accumulator carries information from all prior bytes into each new byte.
306
307fn block_diffuse_forward(buf: &mut [u8]) {
308    let mut acc: u8 = 0xB3;
309    for b in buf.iter_mut() {
310        let out = *b ^ acc;
311        acc = acc.rotate_left(3) ^ out;
312        *b = out;
313    }
314}
315
316fn block_diffuse_reverse(buf: &mut [u8]) {
317    // Reverse: given out = in ^ acc, recover in = out ^ acc.
318    // acc update: acc_new = acc_old.rotate_left(3) ^ out
319    // So: acc_old can be recovered if we run left-to-right maintaining the same acc.
320    let mut acc: u8 = 0xB3;
321    for b in buf.iter_mut() {
322        let out = *b;
323        let original = out ^ acc;
324        acc = acc.rotate_left(3) ^ out;
325        *b = original;
326    }
327}
328
329// ─── Step 5: block shuffle ────────────────────────────────────────────────────
330//
331// Each 16-byte block has its bytes permuted according to a key+block-index-
332// derived permutation.  The last (possibly short) block is permuted with
333// indices clamped to its actual length.
334
335fn block_shuffle_forward(buf: &mut [u8], ks: &KeySchedule) {
336    let n = buf.len();
337    let full_blocks = n / BLOCK;
338    for bi in 0..full_blocks {
339        let perm = ks.block_perm(bi);
340        let base = bi * BLOCK;
341        let block: [u8; BLOCK] = std::array::from_fn(|i| buf[base + i]);
342        for i in 0..BLOCK {
343            buf[base + i] = block[perm[i]];
344        }
345    }
346    // tail block (< 16 bytes) — use a properly bounded bijection
347    let tail_start = full_blocks * BLOCK;
348    let tail_len = n - tail_start;
349    if tail_len > 1 {
350        let perm = ks.block_perm_len(full_blocks, tail_len);
351        let tail: Vec<u8> = buf[tail_start..].to_vec();
352        for i in 0..tail_len {
353            buf[tail_start + i] = tail[perm[i]];
354        }
355    }
356}
357
358fn block_shuffle_reverse(buf: &mut [u8], ks: &KeySchedule) {
359    let n = buf.len();
360    let full_blocks = n / BLOCK;
361    for bi in 0..full_blocks {
362        let perm = ks.block_perm(bi);
363        let base = bi * BLOCK;
364        let block: [u8; BLOCK] = std::array::from_fn(|i| buf[base + i]);
365        let mut orig = [0u8; BLOCK];
366        for i in 0..BLOCK {
367            orig[perm[i]] = block[i];
368        }
369        buf[base..base + BLOCK].copy_from_slice(&orig);
370    }
371    // tail block
372    let tail_start = full_blocks * BLOCK;
373    let tail_len = n - tail_start;
374    if tail_len > 1 {
375        let perm = ks.block_perm_len(full_blocks, tail_len);
376        let tail: Vec<u8> = buf[tail_start..].to_vec();
377        let mut orig = vec![0u8; tail_len];
378        for i in 0..tail_len {
379            orig[perm[i]] = tail[i];
380        }
381        buf[tail_start..].copy_from_slice(&orig);
382    }
383}
384
385// ─── tests ───────────────────────────────────────────────────────────────────
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390    use bincode::{Encode, Decode};
391
392    #[derive(Encode, Decode, Debug, PartialEq)]
393    struct Point { x: f64, y: f64, label: String }
394
395    #[derive(Encode, Decode, Debug, PartialEq)]
396    struct Nested { id: u64, inner: Point, tags: Vec<String> }
397
398    #[test]
399    fn round_trip_default_key() {
400        let p = Point { x: 1.5, y: -3.0, label: "origin".into() };
401        let blob = seal(&p, None).unwrap();
402        let back: Point = open(&blob, None).unwrap();
403        assert_eq!(p, back);
404    }
405
406
407    #[test]
408    fn round_trip_custom_key() {
409        let p = Point { x: 42.0, y: 0.001, label: "custom".into() };
410        let blob = seal(&p, Some("hunter2")).unwrap();
411        let back: Point = open(&blob, Some("hunter2")).unwrap();
412        assert_eq!(p, back);
413    }
414
415    #[test]
416    fn round_trip_nested() {
417        let n = Nested {
418            id: 9999,
419            inner: Point { x: -1.0, y: 2.5, label: "nested".into() },
420            tags: vec!["a".into(), "bb".into(), "ccc".into()],
421        };
422        let blob = seal(&n, Some("nested-key")).unwrap();
423        let back: Nested = open(&blob, Some("nested-key")).unwrap();
424        assert_eq!(n, back);
425    }
426
427    #[test]
428    fn wrong_key_fails() {
429        let p = Point { x: 1.0, y: 2.0, label: "x".into() };
430        let blob = seal(&p, Some("correct")).unwrap();
431        // Opening with wrong key should either error or produce garbage that
432        // fails to decode as Point.
433        let result: Result<Point, _> = open(&blob, Some("wrong"));
434        assert!(result.is_err());
435    }
436
437    #[test]
438    fn ciphertext_differs_from_plaintext() {
439        let p = Point { x: 0.0, y: 0.0, label: "zero".into() };
440        let plain = bincode::encode_to_vec(&p, bincode::config::standard()).unwrap();
441        let blob = seal(&p, None).unwrap();
442        // The sealed blob must not equal the raw bincode bytes.
443        assert_ne!(blob, plain);
444    }
445
446    #[test]
447    fn same_plaintext_same_key_produces_same_ciphertext() {
448        let p = Point { x: 1.0, y: 2.0, label: "det".into() };
449        let b1 = seal(&p, Some("k")).unwrap();
450        let b2 = seal(&p, Some("k")).unwrap();
451        assert_eq!(b1, b2); // cipher is deterministic
452    }
453}