Skip to content

Commit c1e0e03

Browse files
authored
Merge pull request #165 from cipherstash/harden/permutation-bitwise
fix(permutation): make bitwise_permute constant-time
2 parents e766584 + 1d247ad commit c1e0e03

1 file changed

Lines changed: 101 additions & 15 deletions

File tree

packages/permutation/src/bitwise.rs

Lines changed: 101 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
use crate::PermutationKey;
2-
use bitvec::{array::BitArray, order::Msb0};
1+
use crate::{elementwise::permute_array, PermutationKey};
32
use std::num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8};
4-
use zeroize::Zeroize;
3+
use zeroize::{Zeroize, Zeroizing};
54

65
// TODO: Make this a private trait
76
// FIXME: This trait is backwards - self should be T and the argument should be a key
@@ -13,19 +12,25 @@ macro_rules! impl_bitwise_permutable {
1312
($N:literal, $int_type:ty, $array_size:expr) => {
1413
impl BitwisePermute<$N, $int_type> for PermutationKey<$N> {
1514
fn bitwise_permute(&self, mut input: $int_type) -> $int_type {
16-
let bytes = input.to_be_bytes();
17-
let arr: BitArray<[u8; $array_size], Msb0> = BitArray::new(bytes);
18-
let out: BitArray<[u8; $array_size], Msb0> = self.iter().enumerate().fold(
19-
BitArray::new([0; $array_size]),
20-
|mut out, (i, k)| {
21-
out.set(i, *unsafe { arr.get_unchecked(k) });
22-
out
23-
},
24-
);
25-
15+
// Unpack the input into one byte per bit (MSB-first), permute
16+
// the bit-vector through the constant-time `permute_array`
17+
// primitive, then re-pack. The previous implementation used
18+
// `bitvec::get_unchecked(secret_index)` which performs a
19+
// secret-dependent bit load; even though the bit array fits in
20+
// a single cache line, this defends against intra-cache-line
21+
// microarchitectural leaks (port pressure, etc.).
22+
let bytes = Zeroizing::new(input.to_be_bytes());
23+
let mut bits: Zeroizing<[u8; $N]> = Zeroizing::new([0u8; $N]);
24+
for j in 0..$N {
25+
bits[j] = (bytes[j / 8] >> (7 - (j % 8))) & 1;
26+
}
27+
let permuted = Zeroizing::new(permute_array(self, *bits));
28+
let mut out = [0u8; $array_size];
29+
for j in 0..$N {
30+
out[j / 8] |= (permuted[j] & 1) << (7 - (j % 8));
31+
}
2632
input.zeroize();
27-
28-
<$int_type>::from_be_bytes(out.into_inner())
33+
<$int_type>::from_be_bytes(out)
2934
}
3035
}
3136
};
@@ -92,4 +97,85 @@ mod tests {
9297
test_permute::<64, _>(NonZeroU64::new(7178231783183).unwrap());
9398
test_permute::<128, _>(NonZeroU128::new(29472929298731313).unwrap());
9499
}
100+
101+
// Zero in -> zero out, for every supported width.
102+
// Catches sign/endian bugs in the unpack/repack glue: any path that
103+
// accidentally introduces a stray bit would fail here.
104+
#[test]
105+
fn bitwise_permute_zero_in_zero_out() {
106+
assert_eq!(tests::gen_key::<8>([0; 32]).bitwise_permute(0u8), 0);
107+
assert_eq!(tests::gen_key::<16>([0; 32]).bitwise_permute(0u16), 0);
108+
assert_eq!(tests::gen_key::<32>([0; 32]).bitwise_permute(0u32), 0);
109+
assert_eq!(tests::gen_key::<64>([0; 32]).bitwise_permute(0u64), 0);
110+
assert_eq!(tests::gen_key::<128>([0; 32]).bitwise_permute(0u128), 0);
111+
}
112+
113+
// All-ones is a fixed point of every bit permutation (every bit position
114+
// already holds a 1, so permuting positions can't change anything).
115+
// Catches missing-bit bugs at the high or low boundary of each width.
116+
#[test]
117+
fn bitwise_permute_all_ones_invariant() {
118+
assert_eq!(
119+
tests::gen_key::<8>([0; 32]).bitwise_permute(u8::MAX),
120+
u8::MAX
121+
);
122+
assert_eq!(
123+
tests::gen_key::<16>([0; 32]).bitwise_permute(u16::MAX),
124+
u16::MAX
125+
);
126+
assert_eq!(
127+
tests::gen_key::<32>([0; 32]).bitwise_permute(u32::MAX),
128+
u32::MAX
129+
);
130+
assert_eq!(
131+
tests::gen_key::<64>([0; 32]).bitwise_permute(u64::MAX),
132+
u64::MAX
133+
);
134+
assert_eq!(
135+
tests::gen_key::<128>([0; 32]).bitwise_permute(u128::MAX),
136+
u128::MAX
137+
);
138+
}
139+
140+
// Hamming weight (popcount) must be preserved — a permutation moves bits
141+
// but neither creates nor destroys them. This is the strongest single
142+
// property check available for the bit-shuffle glue: any bug that drops,
143+
// duplicates, or mis-positions a bit will change the count. As a
144+
// corollary, this is also what justifies the `unsafe new_unchecked` in
145+
// the NonZero impls: popcount > 0 in -> popcount > 0 out.
146+
#[test]
147+
fn bitwise_permute_preserves_hamming_weight() {
148+
let key8 = tests::gen_key::<8>([0; 32]);
149+
for n in [0u8, 1, 0x80, 0x55, 0xaa, 0xff] {
150+
assert_eq!(key8.bitwise_permute(n).count_ones(), n.count_ones());
151+
}
152+
153+
let key32 = tests::gen_key::<32>([0; 32]);
154+
for n in [0u32, 1, 0x8000_0000, 0xcafe_babe, 0x1234_5678, u32::MAX] {
155+
assert_eq!(key32.bitwise_permute(n).count_ones(), n.count_ones());
156+
}
157+
158+
let key128 = tests::gen_key::<128>([0; 32]);
159+
for n in [
160+
0u128,
161+
1,
162+
1 << 127,
163+
0x0123_4567_89ab_cdef_fedc_ba98_7654_3210,
164+
u128::MAX,
165+
] {
166+
assert_eq!(key128.bitwise_permute(n).count_ones(), n.count_ones());
167+
}
168+
}
169+
170+
// Same key + same input must produce the same output. Cheap guard against
171+
// any future change that accidentally introduces nondeterminism (e.g. a
172+
// hash-seed in the permute path, or branching on uninitialised memory
173+
// that happens to read consistent values in test but not in production).
174+
#[test]
175+
fn bitwise_permute_is_deterministic() {
176+
let key_a = tests::gen_key::<64>([7; 32]);
177+
let key_b = tests::gen_key::<64>([7; 32]);
178+
let input = 0xcafe_babe_dead_beefu64;
179+
assert_eq!(key_a.bitwise_permute(input), key_b.bitwise_permute(input));
180+
}
95181
}

0 commit comments

Comments
 (0)