|
| 1 | +//! Check of the secret-leak boundary between `bip39`'s plain types and |
| 2 | +//! `kdk-mnemonic`'s wrappers. |
| 3 | +//! |
| 4 | +//! `kdk-mnemonic` does not eliminate copies; it try to guarantees the sensitive |
| 5 | +//! copy is wiped on drop and removes the legal path to read the source |
| 6 | +//! copy after move. |
| 7 | +//! |
| 8 | +//! Run: `cargo run --example secret_leak -p kdk-mnemonic` |
| 9 | +//! |
| 10 | +//! # Security notice |
| 11 | +//! |
| 12 | +//! `bip39::Mnemonic::from_entropy(&entropy)` borrows the entropy: the |
| 13 | +//! caller keeps their copy alongside bip39's internal one. |
| 14 | +//! `SensitiveBytes::new(entropy_bytes)` *moves* the entropy in: the |
| 15 | +//! compiler bans further reads of the source. This does not eliminate the |
| 16 | +//! physical bytes in the source frame until LLVM reuses that slot. |
| 17 | +
|
| 18 | +use bip39::Mnemonic; |
| 19 | +use kdk_mnemonic::{entropy_to_mnemonic, GameMnemonic}; |
| 20 | +use kdk_zeroize::{prelude::*, SensitiveBytes}; |
| 21 | + |
| 22 | +enum Vector {} |
| 23 | + |
| 24 | +fn main() { |
| 25 | + let entropy = [0u8; 16]; // canonical "abandon × 11 + about" 12-word vector |
| 26 | + |
| 27 | + println!("== bip39 (reference: Debug redacts, Display leaks) =="); |
| 28 | + let bip_mnemonic = Mnemonic::from_entropy(&entropy).expect("valid entropy"); |
| 29 | + let dbg = format!("{:?}", bip_mnemonic); |
| 30 | + let disp = format!("{}", bip_mnemonic); |
| 31 | + println!(" Debug: {dbg}"); |
| 32 | + println!(" Display: {disp}"); |
| 33 | + assert!( |
| 34 | + !dbg.contains("abandon"), |
| 35 | + "bip39 Debug unexpectedly leaked the word list" |
| 36 | + ); |
| 37 | + assert!( |
| 38 | + disp.contains("about"), |
| 39 | + "expected bip39 Display to leak the word list" |
| 40 | + ); |
| 41 | + |
| 42 | + println!(); |
| 43 | + println!("== kdk-mnemonic (Debug AND Display both redact) =="); |
| 44 | + let kdk_entropy = SensitiveBytes::<16, Vector>::new(entropy); |
| 45 | + let kdk_mnemonic: GameMnemonic<Vector, 12> = |
| 46 | + entropy_to_mnemonic(&kdk_entropy).expect("entropy length matches W=12"); |
| 47 | + let dbg = format!("{:?}", kdk_mnemonic); |
| 48 | + let disp = format!("{}", kdk_mnemonic); |
| 49 | + println!(" Debug: {dbg}"); |
| 50 | + println!(" Display: {disp}"); |
| 51 | + // Real evidence: no word from the canonical 12-word vector leaks |
| 52 | + // on either channel. The literal "GameMnemonic(REDACTED)" string |
| 53 | + // is our own impl's output — asserting it equals itself adds no |
| 54 | + // information. |
| 55 | + assert!(!dbg.contains("abandon") && !dbg.contains("about")); |
| 56 | + assert!(!disp.contains("abandon") && !disp.contains("about")); |
| 57 | + |
| 58 | + println!(); |
| 59 | + println!("== bip39-style raw [u8; 16] =="); |
| 60 | + let raw = [0xAAu8; 16]; |
| 61 | + let raw_dbg = format!("{:?}", raw); |
| 62 | + println!(" Debug: {raw_dbg}"); |
| 63 | + assert!( |
| 64 | + raw_dbg.contains("170"), // 0xAA = 170 decimal |
| 65 | + "raw array Debug should print byte values" |
| 66 | + ); |
| 67 | + // Display is not implemented for `[u8; N]`; the line below would |
| 68 | + // fail with E0277 ("the trait `Display` is not implemented for `[u8; 16]`"): |
| 69 | + // let _ = format!("{}", raw); |
| 70 | + println!(" Display: <not implemented on [u8; N] — would not compile>"); |
| 71 | + |
| 72 | + println!(); |
| 73 | + println!("== kdk SensitiveBytes<16> =="); |
| 74 | + let wrapped = SensitiveBytes::<16, Vector>::new(raw); |
| 75 | + let wrapped_dbg = format!("{:?}", wrapped); |
| 76 | + let wrapped_disp = format!("{}", wrapped); |
| 77 | + println!(" Debug: {wrapped_dbg}"); |
| 78 | + println!(" Display: {wrapped_disp}"); |
| 79 | + // Real evidence: the original byte value (170 decimal = 0xAA) |
| 80 | + // does not appear in either channel. |
| 81 | + assert!(!wrapped_dbg.contains("170") && !wrapped_disp.contains("170")); |
| 82 | + |
| 83 | + println!(); |
| 84 | + println!("== bip39::Mnemonic Drop =="); |
| 85 | + let bip_ptr: *mut Mnemonic; |
| 86 | + { |
| 87 | + // <internal code> |
| 88 | + let boxed = Box::new(Mnemonic::from_entropy(&[0xAAu8; 16]).expect("valid entropy")); |
| 89 | + bip_ptr = Box::into_raw(boxed); |
| 90 | + } |
| 91 | + |
| 92 | + unsafe { |
| 93 | + let size = core::mem::size_of::<Mnemonic>(); |
| 94 | + let pre: Vec<u8> = (0..size) |
| 95 | + .map(|i| core::ptr::read_volatile((bip_ptr as *const u8).add(i))) |
| 96 | + .collect(); |
| 97 | + core::ptr::drop_in_place(bip_ptr); |
| 98 | + let post: Vec<u8> = (0..size) |
| 99 | + .map(|i| core::ptr::read_volatile((bip_ptr as *const u8).add(i))) |
| 100 | + .collect(); |
| 101 | + assert!( |
| 102 | + !post.iter().all(|&b| b == 0), |
| 103 | + "bip39 Mnemonic unexpectedly zeroed on Drop" |
| 104 | + ); |
| 105 | + println!( |
| 106 | + " bytes pre-drop (first 16): {:02x?}", |
| 107 | + &pre[..16.min(size)] |
| 108 | + ); |
| 109 | + println!( |
| 110 | + " bytes post-drop (first 16): {:02x?}", |
| 111 | + &post[..16.min(size)] |
| 112 | + ); |
| 113 | + std::alloc::dealloc(bip_ptr as *mut u8, std::alloc::Layout::new::<Mnemonic>()); |
| 114 | + } |
| 115 | + |
| 116 | + println!(); |
| 117 | + println!("== SensitiveBytes Drop =="); |
| 118 | + let raw_ptr: *mut SensitiveBytes<16, Vector>; |
| 119 | + { |
| 120 | + // <internal code> |
| 121 | + // Heap-alloc so the address survives the inner scope and stays |
| 122 | + // valid for post-drop inspection. `Box::into_raw` consumes the |
| 123 | + // Box without running its destructor — the SensitiveBytes is |
| 124 | + // still alive in memory, ownership transferred to `raw_ptr`. |
| 125 | + let boxed = Box::new(SensitiveBytes::<16, Vector>::new([0xAAu8; 16])); |
| 126 | + raw_ptr = Box::into_raw(boxed); |
| 127 | + } |
| 128 | + |
| 129 | + unsafe { |
| 130 | + let mut pre = [0u8; 16]; |
| 131 | + for (i, slot) in pre.iter_mut().enumerate() { |
| 132 | + *slot = core::ptr::read_volatile((raw_ptr as *const u8).add(i)); |
| 133 | + } |
| 134 | + core::ptr::drop_in_place(raw_ptr); |
| 135 | + let mut post = [0u8; 16]; |
| 136 | + for (i, slot) in post.iter_mut().enumerate() { |
| 137 | + *slot = core::ptr::read_volatile((raw_ptr as *const u8).add(i)); |
| 138 | + } |
| 139 | + assert_eq!(post, [0u8; 16], "Drop did NOT wipe the bytes"); |
| 140 | + println!(" bytes pre-drop: {:02x?}", pre); |
| 141 | + println!(" bytes post-drop: {:02x?}", post); |
| 142 | + std::alloc::dealloc( |
| 143 | + raw_ptr as *mut u8, |
| 144 | + std::alloc::Layout::new::<SensitiveBytes<16, Vector>>(), |
| 145 | + ); |
| 146 | + } |
| 147 | + let leaked = format!("{}", kdk_mnemonic.expose_secret()); |
| 148 | + let seed = kdk_mnemonic.expose_secret().to_seed(""); |
| 149 | + let seed_dbg = format!("{:?}", seed); |
| 150 | + assert!(leaked.contains("abandon")); |
| 151 | + assert!(seed_dbg.starts_with("["), "bare seed prints its bytes"); |
| 152 | + println!(); |
| 153 | + println!(" > You can deliberately expose secret `.expose_secret()` and/or"); |
| 154 | + println!(" > use `Display` to leak it:"); |
| 155 | + println!(" >"); |
| 156 | + println!(" >"); |
| 157 | + println!(" > {leaked}"); |
| 158 | + println!(" >"); |
| 159 | + println!(" >"); |
| 160 | + println!(" > Or use `to_seed()` and slice it:"); |
| 161 | + println!(" >"); |
| 162 | + println!(" >"); |
| 163 | + println!(" > {:?}", &seed[..8]); |
| 164 | +} |
0 commit comments