boo-rs 0.1.3

Encrypt primitives types at compile time
Documentation
//! Runtime XOR mangling against a per-process cookie: `boo_mangle_init!()` embeds this verbatim
//! into the caller's `__boo_mangle` module. Mirrors glibc's `PTR_MANGLE` and Windows'
//! `EncodePointer`/`DecodePointer`. `std`-only: needs a real clock for per-run entropy.

use std::sync::OnceLock;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Instant, SystemTime, UNIX_EPOCH};

static COOKIE: OnceLock<[u8; 32]> = OnceLock::new();
static NONCE: AtomicU64 = AtomicU64::new(0);

/// SplitMix64's finalizer: dependency-free integer mixer.
const fn mix(mut x: u64, i: u64) -> u64 {
    x = x.wrapping_add(i).wrapping_add(0x9E37_79B9_7F4A_7C15);
    x = (x ^ (x >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
    x = (x ^ (x >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
    x ^ (x >> 31)
}

/// This process's mangling cookie: a stack address, wall-clock time, and a monotonic-clock delta
/// folded together, computed once and cached. Never a fixed constant - differs across every run,
/// including two back-to-back runs of the same binary.
fn cookie() -> &'static [u8; 32] {
    COOKIE.get_or_init(|| {
        let stack_marker = 0u8;
        let addr = (&raw const stack_marker) as u64;
        let wall = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|d| d.as_nanos() as u64)
            .unwrap_or(0);
        let a = Instant::now();
        let b = Instant::now();
        let mono = u64::from(b.duration_since(a).subsec_nanos());

        let mut seed = addr ^ wall.rotate_left(17) ^ mono.rotate_left(31);
        let mut out = [0u8; 32];
        for (i, chunk) in out.chunks_mut(8).enumerate() {
            seed = mix(seed, i as u64);
            chunk.copy_from_slice(&seed.to_le_bytes());
        }
        out
    })
}

/// A fresh per-value nonce: a process-local counter, never repeated for the life of the process.
/// Not secret, only unique - the same role an IV plays for a stream cipher.
fn next_nonce() -> u64 {
    NONCE.fetch_add(1, Ordering::Relaxed)
}

/// Folds the whole cookie and this value's nonce into one seed - once per mangle or reveal, not
/// once per 8-byte block.
fn value_seed(nonce: u64) -> u64 {
    let mut seed = nonce;
    for (i, chunk) in cookie().chunks(8).enumerate() {
        let word = u64::from_le_bytes(chunk.try_into().unwrap());
        seed = mix(seed ^ word, i as u64);
    }
    seed
}

/// `mix(seed, block)` is the same expansion [`cookie`] uses to turn one seed into successive
/// chunks, keyed here by `block` so every 8-byte chunk of every value's stream is distinct - both
/// within one value longer than 32 bytes, and across values, since a different nonce folds into a
/// different seed.
fn xor_with_keystream<const N: usize>(mut value: [u8; N], nonce: u64) -> [u8; N] {
    let seed = value_seed(nonce);
    for (block, chunk) in value.chunks_mut(8).enumerate() {
        let key = mix(seed, block as u64).to_le_bytes();
        for (byte, k) in chunk.iter_mut().zip(key.iter()) {
            *byte ^= k;
        }
    }
    value
}

/// A value mangled against this process's cookie and a per-value nonce.
///
/// The plaintext only ever exists inside the closure passed to [`Mangled::reveal_with`], scrubbed
/// the moment it returns. A value an external tool saves during one run decodes to garbage against
/// the next run's different cookie.
#[derive(Clone, Copy)]
pub struct Mangled<const N: usize> {
    pub(crate) nonce: u64,
    pub(crate) bytes: [u8; N],
}

impl<const N: usize> Mangled<N> {
    /// Mangles `value` for storage.
    pub fn new(value: [u8; N]) -> Self {
        let nonce = next_nonce();
        Self {
            nonce,
            bytes: xor_with_keystream(value, nonce),
        }
    }

    /// Decodes the value and hands it to `f`, zeroing the plaintext the moment `f` returns or
    /// unwinds - the scrub runs in `Drop`.
    pub fn reveal_with<R>(&self, f: impl FnOnce(&[u8; N]) -> R) -> R {
        struct Scrub<'a, const N: usize>(&'a mut [u8; N]);

        impl<const N: usize> Drop for Scrub<'_, N> {
            fn drop(&mut self) {
                for byte in self.0.iter_mut() {
                    unsafe { std::ptr::write_volatile(byte, 0) };
                }
            }
        }

        let mut plain = xor_with_keystream(self.bytes, self.nonce);
        let guard = Scrub(&mut plain);
        f(guard.0)
    }
}