#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as test;
use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
use gunnar_crypt::envelope::{self, MAX_CHUNK, TAG_LEN, V2_HEADER_LEN};
use gunnar_crypt::keyring::Keyring;
struct Counting;
thread_local! {
static TRACK: Cell<bool> = const { Cell::new(false) };
static LARGEST: Cell<usize> = const { Cell::new(0) };
}
fn note(size: usize) {
if TRACK.with(Cell::get) {
LARGEST.with(|l| l.set(l.get().max(size)));
}
}
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
note(layout.size());
unsafe { System.alloc(layout) }
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
note(layout.size());
unsafe { System.alloc_zeroed(layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
note(new_size);
unsafe { System.realloc(ptr, layout, new_size) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
}
#[global_allocator]
static ALLOC: Counting = Counting;
fn largest_alloc<T>(f: impl FnOnce() -> T) -> (usize, T) {
LARGEST.with(|l| l.set(0));
TRACK.with(|t| t.set(true));
let out = f();
TRACK.with(|t| t.set(false));
(LARGEST.with(Cell::get), out)
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 ^= self.0 >> 12;
self.0 ^= self.0 << 25;
self.0 ^= self.0 >> 27;
self.0.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
fn below(&mut self, n: usize) -> usize {
if n == 0 {
0
} else {
(self.next() % n as u64) as usize
}
}
}
fn ring() -> Keyring {
Keyring::parse(include_str!("vectors/keyring.txt")).unwrap()
}
fn must_refuse(ring: &Keyring, e: &[u8], what: &str) {
let (big, mem) = largest_alloc(|| envelope::open(ring, e));
assert!(mem.is_err(), "{what}: opened in memory");
assert!(
big <= e.len() + 4096,
"{what}: the in-memory open of {} bytes allocated {big} at once",
e.len()
);
let mut sink = Vec::new();
let (big, streamed) = largest_alloc(|| envelope::open_stream(ring, &mut &e[..], &mut sink));
assert!(streamed.is_err(), "{what}: opened streamed");
assert!(
big <= MAX_CHUNK as usize + TAG_LEN + 4096,
"{what}: the streamed open allocated {big} at once"
);
if let Some(n) = envelope::plaintext_len(e, e.len() as u64) {
assert!(n <= e.len() as u64, "{what}");
}
let _ = envelope::is_envelope(e);
let _ = envelope::epoch_of(e);
let _ = envelope::version_of(e);
}
#[test]
fn random_mutations_of_v2_envelopes_are_refused_without_a_panic() {
let ring = ring();
let mut rng = Rng(0x6775_6e6e_6172_2d76);
let mut refused = 0;
for round in 0..3000 {
let len = rng.below(200);
let chunk = [1u32, 7, 16, 64, 1, 7, 16, envelope::CHUNK][rng.below(8)];
let plain: Vec<u8> = (0..len).map(|_| rng.next() as u8).collect();
let env = envelope::seal_with_chunk(ring.newest(), chunk, &plain);
let mut m = env.clone();
let kind = rng.below(7);
match kind {
0 => m.truncate(rng.below(env.len())),
1 => {
for _ in 0..1 + rng.below(4) {
let i = rng.below(m.len());
m[i] ^= 1 << rng.below(8);
}
}
2 => {
let i = rng.below(m.len() + 1);
m.insert(i, rng.next() as u8);
}
3 => {
for _ in 0..1 + rng.below(40) {
m.push(rng.next() as u8);
}
}
4 => {
let declared =
[0, 1, MAX_CHUNK, MAX_CHUNK + 1, u32::MAX, rng.next() as u32][rng.below(6)];
m[13..17].copy_from_slice(&declared.to_be_bytes());
}
5 => m[8] = rng.next() as u8,
_ => {
let a = rng.below(m.len());
let b = (a + rng.below(48)).min(m.len());
let piece = m[a..b].to_vec();
let at = rng.below(m.len() + 1);
m.splice(at..at, piece);
}
}
if m == env {
continue;
}
must_refuse(&ring, &m, &format!("round {round}, mutation {kind}"));
refused += 1;
}
assert!(refused > 2500, "the mutations did something ({refused})");
}
#[test]
fn a_huge_declared_chunk_size_is_refused_and_never_allocated() {
let ring = ring();
let env = envelope::seal(ring.newest(), b"small");
for declared in [MAX_CHUNK, MAX_CHUNK + 1, u32::MAX / 2, u32::MAX] {
let mut e = env.clone();
e[13..17].copy_from_slice(&declared.to_be_bytes());
must_refuse(&ring, &e, &format!("chunk {declared}"));
let mut sink = Vec::new();
let (big, r) = largest_alloc(|| envelope::open_stream(&ring, &mut &e[..], &mut sink));
assert!(r.is_err());
let bound = if declared > MAX_CHUNK {
4096
} else {
(64 << 10) + 4096
};
assert!(big <= bound, "chunk {declared}: {big} bytes allocated");
}
let (big, plain) = largest_alloc(|| envelope::open(&ring, &env));
assert_eq!(plain.unwrap(), b"small");
assert!(big < 4096, "{big}");
}
#[test]
fn lengths_that_would_overflow_a_block_count_are_answered_without_a_panic() {
let ring = ring();
let env = envelope::seal_with_chunk(ring.newest(), 1, b"abc");
for total in [
0,
1,
V2_HEADER_LEN as u64,
V2_HEADER_LEN as u64 + TAG_LEN as u64,
u64::MAX - 1,
u64::MAX,
(u64::MAX - 100) / 17 * 17 + V2_HEADER_LEN as u64,
] {
if let Some(n) = envelope::plaintext_len(&env, total) {
assert!(n <= total, "total {total}: {n}");
}
}
for declared in [1u32, MAX_CHUNK] {
let mut head = env[..V2_HEADER_LEN].to_vec();
head[13..17].copy_from_slice(&declared.to_be_bytes());
for total in [u64::MAX, u64::MAX - 7, 1 << 63] {
if let Some(n) = envelope::plaintext_len(&head, total) {
assert!(n < total);
}
}
}
must_refuse(&ring, &env[..V2_HEADER_LEN], "no block");
must_refuse(&ring, &env[..V2_HEADER_LEN + 1], "one byte of a tag");
}