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
58 changes: 58 additions & 0 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions crates/kdk-zeroize/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ version = "0.0.1"
edition.workspace = true
license.workspace = true
description = "Sensitive-byte primitives for KDK"

[dev-dependencies]
zeroize = { version = "1", features = ["derive"] }
67 changes: 67 additions & 0 deletions crates/kdk-zeroize/examples/wipe_vs_zeroize.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//! Integration test with `zeroize` crate.
//!
//! Run with:
//! ```text
//! cargo run --example wipe_vs_zeroize -p kdk-zeroize --release
//! ```
//!
//! `--release` is intentional: that's where the optimiser is most
//! aggressive about dead-store elimination.

use kdk_zeroize::wipe_in_place_mut;
use zeroize::Zeroize;

fn case<const N: usize>(label: &str, mut a: [u8; N], mut b: [u8; N]) {
assert_eq!(a, b, "{label}: inputs differ pre-wipe");
unsafe { wipe_in_place_mut(&mut a) };
b.zeroize();
assert_eq!(a, [0u8; N], "{label}: KDK didn't zero");
assert_eq!(b, [0u8; N], "{label}: zeroize didn't zero");
assert_eq!(a, b, "{label}: KDK and zeroize disagree");
println!(" {label}: OK");
}

fn main() {
println!("kdk_zeroize::wipe_in_place_mut vs zeroize::Zeroize::zeroize");

case("[u8; 32] all 0xAA", [0xAAu8; 32], [0xAAu8; 32]);
case("[u8; 64] all 0xFF", [0xFFu8; 64], [0xFFu8; 64]);

let mut non_uniform = [0u8; 64];
for (i, x) in non_uniform.iter_mut().enumerate() {
*x = (i as u8).wrapping_add(0xA5);
}
case("[u8; 64] non-uniform", non_uniform, non_uniform);

// From here we will start a lot the `#[repr(C)]` attribute
#[repr(C)]
#[derive(Copy, Clone, PartialEq, Eq, Debug, Zeroize)]
struct Foo {
a: u32,
b: u64,
c: [u8; 8],
}
let init = Foo {
a: 0xDEADBEEF,
b: 0xCAFEBABE_F00DBABE,
c: [0xFFu8; 8],
};
let zero = Foo {
a: 0,
b: 0,
c: [0u8; 8],
};

let mut kdk_foo = init;
unsafe { wipe_in_place_mut(&mut kdk_foo) };
assert_eq!(kdk_foo, zero, "KDK struct wipe");

let mut zeroize_foo = init;
zeroize_foo.zeroize();
assert_eq!(zeroize_foo, zero, "zeroize struct wipe");

assert_eq!(kdk_foo, zeroize_foo, "struct: KDK and zeroize disagree");
println!(" Foo (struct): OK");

println!("PASS: KDK and zeroize produce byte-identical output across all cases.");
}
4 changes: 1 addition & 3 deletions crates/kdk-zeroize/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,5 @@
#![no_std]

mod sensitive;
mod wipe;

pub use sensitive::SensitiveBytes;
pub use wipe::wipe_in_place_mut;
pub use sensitive::{wipe_in_place_mut, SensitiveBytes};
25 changes: 23 additions & 2 deletions crates/kdk-zeroize/src/sensitive.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use core::fmt;
use core::marker::PhantomData;

use crate::wipe_in_place_mut;
use core::sync::atomic;

/// `N` is the byte length and `O` is a chosen zero-sized type that lets the
/// type system track where the bytes came from (BIP39 seed, dice entropy, AES
Expand All @@ -25,6 +24,28 @@ pub struct SensitiveBytes<const N: usize, O> {
_origin: PhantomData<O>,
}

/// Caller must ensure that after this function returns, **no code
/// reads `*value` as a `T`**.
///
/// # Safety
///
/// Zeroing the bytes may produce an invalid `T` (niches, validity invariants
/// on inner fields, etc.). Typically used inside `Drop`, where Rust's drop
/// semantics already guarantee no further reads.
///
/// WARN: authorship is from [@luisschwab](https://github.com/luisschwab)
/// while reading a go discord's topic. It's improvised and need auction.
pub unsafe fn wipe_in_place_mut<T>(value: &mut T) {
let ptr = (value as *mut T).cast::<u8>();
let len = core::mem::size_of::<T>();
for i in 0..len {
unsafe {
core::ptr::write_volatile(ptr.add(i), 0);
}
atomic::compiler_fence(atomic::Ordering::SeqCst);
}
}

impl<const N: usize, O> SensitiveBytes<N, O> {
/// Wrap a `[u8; N]` as origin-tagged sensitive bytes.
pub const fn new(bytes: [u8; N]) -> Self {
Expand Down
23 changes: 0 additions & 23 deletions crates/kdk-zeroize/src/wipe.rs

This file was deleted.

57 changes: 57 additions & 0 deletions crates/kdk-zeroize/tests/amnesia.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//! Amnesia verification — proves `wipe_in_place_mut` zeroes the bytes
//! of whatever `&mut T` you hand it.
//!
//! `SensitiveBytes::drop` is a one-line delegation
//! (`unsafe { wipe_in_place_mut(self) }`), so the Drop-time wipe is
//! guaranteed by *reading the source*, not by a test. These tests
//! prove the primitive itself.

use kdk_zeroize::{wipe_in_place_mut, SensitiveBytes};

enum AesKey {}

#[test]
fn wipe_in_place_mut_zeroes_u8_array_32() {
let mut buf = [0xAAu8; 32];
unsafe { wipe_in_place_mut(&mut buf) };
assert_eq!(buf, [0u8; 32]);
}

#[test]
fn wipe_in_place_mut_zeroes_u8_array_64_non_uniform() {
let mut buf = [0u8; 64];
for (i, b) in buf.iter_mut().enumerate() {
*b = (i as u8).wrapping_add(0xA5);
}
unsafe { wipe_in_place_mut(&mut buf) };
assert_eq!(buf, [0u8; 64]);
}

#[test]
fn wipe_in_place_mut_zeroes_arbitrary_struct() {
#[repr(C)]
struct Foo {
a: u32,
b: u64,
c: [u8; 8],
}
let mut foo = Foo {
a: 0xDEADBEEF,
b: 0xCAFEBABE_F00DBABE,
c: [0xFFu8; 8],
};
assert_ne!(foo.a, 0);
assert_ne!(foo.b, 0);
assert_ne!(foo.c, [0u8; 8]);
unsafe { wipe_in_place_mut(&mut foo) };
assert_eq!(foo.a, 0);
assert_eq!(foo.b, 0);
assert_eq!(foo.c, [0u8; 8]);
}

#[test]
fn wipe_in_place_mut_zeroes_sensitive_bytes() {
let mut value = SensitiveBytes::<32, AesKey>::new([0xDEu8; 32]);
unsafe { wipe_in_place_mut(&mut value) };
assert_eq!(value.expose_secret(), &[0u8; 32]);
}
12 changes: 3 additions & 9 deletions crates/kdk-zeroize/tests/sensitive.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
//! The kdk-zeroize guarantee can't be observed from outside the
//! process.
//! Behavioural tests: `expose_secret`, `Debug` redaction, origin markers, etc.
//!
//! Rust's memory model doesn't expose dropped storage so these tests are an
//! attempt to observe how probably a pin could occurs on through a
//! `expose_secret`, `Debug` redaction, `Drop` runs without panic,
//! `as_slice` agreement, etc., rather than the wipe itself.
//!
//! The wipe correctness is delegated to whoever wants to audit this crate and
//! we be welcomed through a [`SECURITY.md`] report.
//! Drop-time wipe proof lives in `tests/amnesia.rs` (`Box::into_raw` →
//! `drop_in_place` → volatile byte check → `dealloc`).
use kdk_zeroize::SensitiveBytes;

enum AesKey {}
Expand Down
Loading