Skip to content

Commit e733fb3

Browse files
committed
feat(kdk-zeroize): add Sensitive + SensitiveMut traits (#7)
* feat(kdk-zeroize): add Sensitive + SensitiveMut traits Enforce `expose_secret` and `expose_secret_mut` to sensitive types (e.g. SensitiveMnemonic`). Fix #6. * test(kdk-zeroize): added tests for `Sensitive` trait * chore(kdk-entropy): applied `prelude` call for zeroize on crates
1 parent 2105ab0 commit e733fb3

12 files changed

Lines changed: 243 additions & 171 deletions

File tree

‎crates/kdk-entropy/src/coin.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use kdk_zeroize::prelude::*;
12
use sha2::{Digest, Sha256};
23

34
use crate::error::EntropyError;
@@ -32,6 +33,7 @@ pub const fn max_flips<const N: usize>() -> Result<usize, EntropyError> {
3233
///
3334
/// ```
3435
/// use kdk_entropy::{coin_to_entropy, CoinEntropy};
36+
/// use kdk_zeroize::prelude::*;
3537
///
3638
/// // SHA-256("0" * 128)[:16].
3739
/// let flips = [0u8; 128];

‎crates/kdk-entropy/src/deck.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use kdk_zeroize::prelude::*;
12
use sha2::{Digest, Sha256};
23

34
use crate::error::EntropyError;
@@ -46,6 +47,7 @@ pub const fn max_draws<const CARDS: u8, const N: usize>() -> Result<usize, Entro
4647
///
4748
/// ```
4849
/// use kdk_entropy::{deck_to_entropy, DeckEntropy};
50+
/// use kdk_zeroize::prelude::*;
4951
///
5052
/// // SHA-256("0-1-2-3-...-24")[:16].
5153
/// let cards: [u8; 25] = [

‎crates/kdk-entropy/src/dice.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use kdk_zeroize::prelude::*;
12
use sha2::{Digest, Sha256};
23

34
use crate::error::EntropyError;
@@ -38,6 +39,7 @@ pub const fn max_rolls<const FACES: u8, const N: usize>() -> Result<usize, Entro
3839
///
3940
/// ```
4041
/// use kdk_entropy::{dice_to_entropy, DiceEntropy};
42+
/// use kdk_zeroize::prelude::*;
4143
///
4244
/// // 50 d6 rolls (all "1") → SHA-256("1"*50) truncated to 16 bytes.
4345
/// // Same bytes Krux produces for the same input.

‎crates/kdk-entropy/tests/coin.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use kdk_entropy::{coin_to_entropy, max_flips, min_flips, CoinEntropy, EntropyError};
2+
use kdk_zeroize::prelude::*;
23

34
macro_rules! min_flips_ok {
45
($name:ident, $n:literal => $expected:literal) => {

‎crates/kdk-entropy/tests/deck.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use kdk_entropy::{deck_to_entropy, max_draws, min_draws, DeckEntropy, EntropyError};
2+
use kdk_zeroize::prelude::*;
23

34
macro_rules! min_draws_ok {
45
($name:ident, $cards:literal, $n:literal => $expected:literal) => {

‎crates/kdk-entropy/tests/dice.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use kdk_entropy::{dice_to_entropy, max_rolls, min_rolls, DiceEntropy, EntropyError};
2+
use kdk_zeroize::prelude::*;
23

34
macro_rules! min_rolls_ok {
45
($name:ident, $faces:literal, $n:literal => $expected:literal) => {

‎crates/kdk-zeroize/examples/sensitive.rs‎

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,73 @@
66
//! cargo run --example sensitive_lifecycle -p kdk-zeroize
77
//! ```
88
9-
use kdk_zeroize::SensitiveBytes;
9+
use kdk_zeroize::prelude::*;
1010

1111
pub enum AesKey {}
1212
pub enum SeedBytes {}
1313

1414
type AesKey256 = SensitiveBytes<32, AesKey>;
1515
type Bip39Seed = SensitiveBytes<64, SeedBytes>;
1616

17-
fn main() {
17+
use kdk_zeroize::wipe_in_place_mut;
18+
use zeroize::Zeroize;
19+
20+
fn case<const N: usize>(label: &str, mut a: [u8; N], mut b: [u8; N]) {
21+
assert_eq!(a, b, "{label}: inputs differ pre-wipe");
22+
unsafe { wipe_in_place_mut(&mut a) };
23+
b.zeroize();
24+
assert_eq!(a, [0u8; N], "{label}: KDK didn't zero");
25+
assert_eq!(b, [0u8; N], "{label}: zeroize didn't zero");
26+
assert_eq!(a, b, "{label}: KDK and zeroize disagree");
27+
println!(" {label}: OK");
28+
}
29+
30+
fn zeroize_wipe() {
31+
println!("kdk_zeroize::wipe_in_place_mut vs zeroize::Zeroize::zeroize");
32+
33+
case("[u8; 32] all 0xAA", [0xAAu8; 32], [0xAAu8; 32]);
34+
case("[u8; 64] all 0xFF", [0xFFu8; 64], [0xFFu8; 64]);
35+
36+
let mut non_uniform = [0u8; 64];
37+
for (i, x) in non_uniform.iter_mut().enumerate() {
38+
*x = (i as u8).wrapping_add(0xA5);
39+
}
40+
case("[u8; 64] non-uniform", non_uniform, non_uniform);
41+
42+
// From here we will start a lot the `#[repr(C)]` attribute
43+
#[repr(C)]
44+
#[derive(Copy, Clone, PartialEq, Eq, Debug, Zeroize)]
45+
struct Foo {
46+
a: u32,
47+
b: u64,
48+
c: [u8; 8],
49+
}
50+
let init = Foo {
51+
a: 0xDEADBEEF,
52+
b: 0xCAFEBABE_F00DBABE,
53+
c: [0xFFu8; 8],
54+
};
55+
let zero = Foo {
56+
a: 0,
57+
b: 0,
58+
c: [0u8; 8],
59+
};
60+
61+
let mut kdk_foo = init;
62+
unsafe { wipe_in_place_mut(&mut kdk_foo) };
63+
assert_eq!(kdk_foo, zero, "KDK struct wipe");
64+
65+
let mut zeroize_foo = init;
66+
zeroize_foo.zeroize();
67+
assert_eq!(zeroize_foo, zero, "zeroize struct wipe");
68+
69+
assert_eq!(kdk_foo, zeroize_foo, "struct: KDK and zeroize disagree");
70+
println!(" Foo (struct): OK");
71+
72+
println!("PASS: KDK and zeroize produce byte-identical output across all cases.");
73+
}
74+
75+
fn detach() {
1876
let key = AesKey256::new([0xAA; 32]);
1977
let seed = Bip39Seed::new([0x42; 64]);
2078

@@ -41,3 +99,8 @@ fn main() {
4199
mixed.expose_secret()[2]
42100
);
43101
}
102+
103+
fn main() {
104+
zeroize_wipe();
105+
detach();
106+
}

‎crates/kdk-zeroize/examples/wipe_vs_zeroize.rs‎

Lines changed: 0 additions & 67 deletions
This file was deleted.

‎crates/kdk-zeroize/src/lib.rs‎

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
11
//! SPDX-License-Identifier: MIT
22
//!
3-
//! # Security
4-
//!
5-
//! > Every secret byte that touches memory in a KDK-built signer flows through
6-
//! > types defined here. A bug in this crate — a missed `Drop`, a leaky `Debug`,
7-
//! > a copy that escapes the wipe — silently breaks the amnesia guarantee across
8-
//! > EVERY downstream crate.
9-
//!
10-
//! # About
11-
//!
123
//! Every KDK crate that touches secret bytes pulls **this** crate
134
//! rather than `zeroize` directly. We aim to be an auditable-friendly project
145
//! on level at purge secret bytes correctly.
156
//!
167
//! # Security notice
178
//!
9+
//! > A bug in this crate — a missed `Drop`, a leaky `Debug`, a copy that
10+
//! > escapes the wipe — silently breaks the amnesia guarantee across
11+
//! > EVERY downstream crate.
12+
//!
1813
//! Treat this crate's API surface as a **published security contract**.
1914
//! Breaking changes here propagate amnesia regressions
2015
//! through every downstream crate.
@@ -24,4 +19,8 @@
2419

2520
mod sensitive;
2621

27-
pub use sensitive::{wipe_in_place_mut, SensitiveBytes};
22+
pub use sensitive::{wipe_in_place_mut, Sensitive, SensitiveBytes, SensitiveMut};
23+
24+
pub mod prelude {
25+
pub use crate::{Sensitive, SensitiveBytes, SensitiveMut};
26+
}

‎crates/kdk-zeroize/src/sensitive.rs‎

Lines changed: 59 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,26 @@ use core::fmt;
22
use core::marker::PhantomData;
33
use core::sync::atomic;
44

5-
/// `N` is the byte length and `O` is a chosen zero-sized type that lets the
6-
/// type system track where the bytes came from (BIP39 seed, dice entropy, AES
7-
/// key, decrypted KEF payload, etc...).
8-
///
9-
/// # Example
10-
///
11-
/// ```
12-
/// use kdk_zeroize::SensitiveBytes;
13-
///
14-
/// // A caller-chosen marker type.
15-
/// pub enum AesKey {}
16-
///
17-
/// type AesKey256 = SensitiveBytes<32, AesKey>;
18-
///
19-
///
20-
/// let key = AesKey256::new([0u8; 32]);
21-
/// ```
22-
pub struct SensitiveBytes<const N: usize, O> {
23-
bytes: [u8; N],
24-
_origin: PhantomData<O>,
5+
/// Disciplines yourself. This trait does NOT enforce:
6+
/// - `Drop` wipes the storage on out-of-scope.
7+
/// - Redacting `fmt::Debug` impl (never derive).
8+
/// - No `Clone` / no `Copy`.
9+
pub trait Sensitive {
10+
/// The type whose bytes are the secret material.
11+
type Inner: ?Sized;
12+
13+
/// Explicit-secret-access getter. Mirrors the inherent
14+
/// `expose_secret` every sensitive type provides.
15+
fn expose_secret(&self) -> &Self::Inner;
16+
}
17+
18+
/// Read-only sensitive types (computed/derived values like a wrapped
19+
/// `bip39::Mnemonic`) impl only [`Sensitive`], not this trait —
20+
/// generic code that requires mutation takes `<S: SensitiveMut>` and
21+
/// will refuse those at compile time.
22+
pub trait SensitiveMut: Sensitive {
23+
/// Mutable explicit-secret-access getter.
24+
fn expose_secret_mut(&mut self) -> &mut Self::Inner;
2525
}
2626

2727
/// Caller must ensure that after this function returns, **no code
@@ -46,6 +46,28 @@ pub unsafe fn wipe_in_place_mut<T>(value: &mut T) {
4646
}
4747
}
4848

49+
/// `N` is the byte length and `O` is a chosen zero-sized type that lets the
50+
/// type system track where the bytes came from (BIP39 seed, dice entropy, AES
51+
/// key, decrypted KEF payload, etc...).
52+
///
53+
/// # Example
54+
///
55+
/// ```
56+
/// use kdk_zeroize::SensitiveBytes;
57+
///
58+
/// // A caller-chosen marker type.
59+
/// pub enum AesKey {}
60+
///
61+
/// type AesKey256 = SensitiveBytes<32, AesKey>;
62+
///
63+
///
64+
/// let key = AesKey256::new([0u8; 32]);
65+
/// ```
66+
pub struct SensitiveBytes<const N: usize, O> {
67+
bytes: [u8; N],
68+
_origin: PhantomData<O>,
69+
}
70+
4971
impl<const N: usize, O> SensitiveBytes<N, O> {
5072
/// Wrap a `[u8; N]` as origin-tagged sensitive bytes.
5173
pub const fn new(bytes: [u8; N]) -> Self {
@@ -55,20 +77,9 @@ impl<const N: usize, O> SensitiveBytes<N, O> {
5577
}
5678
}
5779

58-
/// Explicit-secret-access getter. Prefer this over `as_slice`
59-
/// when the bytes are about to be fed into a cryptographic
60-
/// primitive — the name makes the secrecy visible in diffs.
61-
pub fn expose_secret(&self) -> &[u8; N] {
62-
&self.bytes
63-
}
64-
65-
/// Mutable explicit-secret-access getter
66-
pub fn expose_secret_mut(&mut self) -> &mut [u8; N] {
67-
&mut self.bytes
68-
}
69-
70-
/// Same bytes as `expose_secret`; the looser name is fine for
71-
/// non-cryptographic reads (e.g. equality checks in tests).
80+
/// Same bytes as `Sensitive::expose_secret`; the looser `&[u8]`
81+
/// return is fine for non-cryptographic reads (e.g. equality
82+
/// checks in tests).
7283
pub fn as_slice(&self) -> &[u8] {
7384
&self.bytes
7485
}
@@ -85,3 +96,17 @@ impl<const N: usize, O> fmt::Debug for SensitiveBytes<N, O> {
8596
f.write_str("SensitiveBytes(REDACTED)")
8697
}
8798
}
99+
100+
impl<const N: usize, O> Sensitive for SensitiveBytes<N, O> {
101+
type Inner = [u8; N];
102+
103+
fn expose_secret(&self) -> &[u8; N] {
104+
&self.bytes
105+
}
106+
}
107+
108+
impl<const N: usize, O> SensitiveMut for SensitiveBytes<N, O> {
109+
fn expose_secret_mut(&mut self) -> &mut [u8; N] {
110+
&mut self.bytes
111+
}
112+
}

0 commit comments

Comments
 (0)