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