Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 0 additions & 4 deletions packages/permutation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,3 @@ rand = { workspace = true }
serde = { workspace = true }
subtle = "2.6.1"
zeroize = { workspace = true }
paste = "1.0.15"

[dev-dependencies]
paste = "1.0.15"
4 changes: 2 additions & 2 deletions packages/permutation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use vitaminc_protected::{Controlled, Protected};
let mut rng = SafeRand::from_seed([0; 32]);
let key = PermutationKey::random(&mut rng).expect("Random error");
let input: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8];
assert_eq!(key.permute(input), [7, 8, 6, 3, 4, 1, 2, 5]);
assert_eq!(key.permute(input), [4, 1, 5, 6, 8, 7, 2, 3]);
```

## Bitwise Permutations
Expand All @@ -41,7 +41,7 @@ use vitaminc_random::{Generatable, SafeRand, SeedableRng};
let mut rng = SafeRand::from_seed([0; 32]);
let key = PermutationKey::random(&mut rng).expect("Random error");
let input: u32 = 1000;
assert_eq!(key.bitwise_permute(input), 2250248200);
assert_eq!(key.bitwise_permute(input), 606208516);
```

## Permutations and Security
Expand Down
74 changes: 33 additions & 41 deletions packages/permutation/src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ impl<const N: usize> PermutationKey<N> {
}

/// Creates a new permutation key from a seed.
///
/// Derivation is deterministic: a given seed always yields the same key.
/// If it returns [`RandomError::SeedRejected`] (probability ≈ N²/2⁵⁷,
/// at most ≈ 2⁻⁴³ for N = 128), the seed can *never* derive a key —
/// discard it and provision a fresh seed. Only retain seeds whose first
/// derivation succeeds.
///
/// TODO: Perhaps seed should be protected?
pub fn from_seed(seed: [u8; 32]) -> Result<Self, RandomError>
where
Expand Down Expand Up @@ -93,23 +100,18 @@ where
[u8; N]: IsPermutable,
{
fn random(rng: &mut SafeRand) -> Result<Self, RandomError> {
let key = KeyInner::<N>::generate(identity).map(|mut key| {
// Fisher–Yates: step `i` needs `j` uniform in `0..=i`, so the
// half-open bound is `i + 1`. `j == i` (no swap) must be as likely
// as any other choice or the permutation is not uniform. The loop
// stops at `i == 1`: the `i == 0` step could only draw `j == 0`
// and swap an element with itself, so it would spend a draw for
// no entropy.
for i in (1..N).rev() {
let mut j = rng.next_below(i as u32 + 1) as usize;
key.swap(i, j);
// `j` is derived from the (possibly secret-seeded) key stream;
// wipe it as this crate does for every secret intermediate.
j.zeroize();
}
key
});

// Oblivious sort-by-random-key shuffle: unlike Fisher–Yates, whose
// `swap(i, j)` addresses memory with the secret draw `j`, timing and
// access patterns here are functions of `N` only. See `crate::shuffle`.
//
// Exactly one batch is attempted: `Err(SeedRejected)` means the seed
// behind `rng` is unusable and must be replaced, not retried.
//
// The permutation is written straight into the key's own wiped-on-
// drop slot, so no plain `[u8; N]` copy of it exists at any point;
// on failure the zeroed slot is dropped and wiped like any key.
let mut key = KeyInner::<N>::generate(|| [0; N]);
crate::shuffle::random_permutation(rng, key.inner_mut())?;
Ok(Self(key))
}
}
Expand Down Expand Up @@ -178,31 +180,14 @@ mod tests {
test_key_invert::<16>()?;
test_key_invert::<32>()?;
test_key_invert::<64>()?;
test_key_invert::<128>()?;
Ok(())
}

/// The generator is Fisher-Yates over the identity, drawing
/// `next_below(i + 1)` for `i` from `N - 1` down to `1` (the `i == 0`
/// step is a no-op and draws nothing). Replaying that with a second
/// generator on the same seed must reproduce the key exactly, which pins
/// the draw order, the bound, and that no extra draw is spent.
#[test]
fn key_is_fisher_yates_over_next_below() {
let key = PermutationKey::<16>::from_seed([7u8; 32]).expect("random");
let mut rng = SafeRand::from_seed([7u8; 32]);
let mut expected: [u8; 16] = core::array::from_fn(|i| i as u8);
for i in (1..16).rev() {
let j = rng.next_below(i as u32 + 1) as usize;
expected.swap(i, j);
}
let got: Vec<u8> = key.iter().map(|b| b.risky_unwrap()).collect();
assert_eq!(got, expected);
}

/// A generated key is a valid permutation: every value in `0..N` present
/// exactly once. The invert / complement round-trips imply this only
/// transitively; checking it directly fails loudly if the Fisher–Yates
/// loop bounds regress.
/// transitively; checking it directly at the `PermutationKey` level fails
/// loudly if the generator regresses, whichever shuffle it uses.
fn test_key_is_a_permutation<const N: usize>() -> Result<(), Box<dyn std::error::Error>>
where
[u8; N]: IsPermutable,
Expand Down Expand Up @@ -251,17 +236,23 @@ mod tests {
}
}
let expected = (SAMPLES / N) as f64;
let chi2: f64 = counts
let raw: f64 = counts
.iter()
.flatten()
.map(|&c| {
let d = f64::from(c) - expected;
d * d / expected
})
.sum();
// The position matrix is doubly stochastic, so (N - 1)² = 49 degrees
// of freedom; p = 0.001 critical value is 85.35. The seed is fixed, so
// this is deterministic — no flakiness.
// Each sample is a permutation matrix, not N independent draws, so
// the raw Pearson sum over the N² cells is not χ² on (N − 1)² = 49
// degrees of freedom: its mean is N/(N − 1) times that. Scaling by
// (N − 1)/N recovers a χ²(49) statistic (verified by simulation:
// mean 49.0, 0.1% above the threshold). p = 0.001 critical value for
// χ²(49) is 85.35. The seed is fixed, so the value is reproducible;
// an honest generator would exceed the threshold for about one seed
// in a thousand.
let chi2 = raw * (N - 1) as f64 / N as f64;
assert!(chi2 < 85.35, "chi-squared too high: {chi2}");
Ok(())
}
Expand All @@ -272,6 +263,7 @@ mod tests {
test_key_complement::<16>()?;
test_key_complement::<32>()?;
test_key_complement::<64>()?;
test_key_complement::<128>()?;
Ok(())
}
}
46 changes: 30 additions & 16 deletions packages/permutation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
mod bitwise;
mod elementwise;
mod key;
mod shuffle;

// TODO: Add tests and docs for use with Controlled types

Expand All @@ -10,24 +11,37 @@ pub use elementwise::{Depermute, Permute};
pub use key::PermutationKey;

mod private {
use crate::shuffle::{batcher_gate_count, batcher_schedule};
use vitaminc_protected::Zeroed;

pub trait IsPermutable: Zeroed {}
impl IsPermutable for [u8; 8] {}
impl IsPermutable for [u8; 16] {}
impl IsPermutable for [u8; 32] {}
impl IsPermutable for [u8; 64] {}
impl IsPermutable for [u8; 128] {}
impl IsPermutable for [u16; 8] {}
impl IsPermutable for [u16; 16] {}
impl IsPermutable for [u16; 32] {}
impl IsPermutable for [u16; 64] {}
impl IsPermutable for [u16; 128] {}
impl IsPermutable for [u32; 8] {}
impl IsPermutable for [u32; 16] {}
impl IsPermutable for [u32; 32] {}
impl IsPermutable for [u32; 64] {}
impl IsPermutable for [u32; 128] {}
/// The array shapes a `PermutationKey<N>` can act on. Each impl carries
/// the compare-exchange schedule of the sorting network for its length,
/// so the set of supported lengths lives in exactly one place: adding a
/// length without a network is a missing associated const, caught when
/// this crate compiles, never a downstream const-eval failure.
pub trait IsPermutable: Zeroed {
/// The Batcher network for this length, as `(a, b)` gate pairs with
/// `a < b`. Only the `[u8; N]` impl builds one; the wider element
/// types alias it, since the network depends on the length alone.
const SCHEDULE: &'static [(u8, u8)];
}

macro_rules! permutable {
($($n:literal),* $(,)?) => {$(
impl IsPermutable for [u8; $n] {
const SCHEDULE: &'static [(u8, u8)] =
&batcher_schedule::<{ batcher_gate_count($n) }>($n);
}
impl IsPermutable for [u16; $n] {
const SCHEDULE: &'static [(u8, u8)] = <[u8; $n] as IsPermutable>::SCHEDULE;
}
impl IsPermutable for [u32; $n] {
const SCHEDULE: &'static [(u8, u8)] = <[u8; $n] as IsPermutable>::SCHEDULE;
}
)*};
}

permutable!(8, 16, 32, 64, 128);

pub(crate) const fn identity<const N: usize>() -> [u8; N]
where
Expand Down
Loading