First commit. Version 0.1.0
This commit is contained in:
78
src/secure_mem.rs
Normal file
78
src/secure_mem.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-only
|
||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||
|
||||
//! Defensive primitives for credential handling.
|
||||
//!
|
||||
//! [`secure_zero`] overwrites a byte slice with zeros using volatile stores
|
||||
//! so the optimiser cannot elide the wipe even when it can prove the buffer
|
||||
//! is no longer read. It is the building block the `Drop` impls of secure
|
||||
//! widgets (currently [`crate::widget::text_edit::TextEdit`] when
|
||||
//! `secure( true )` is set) use to scrub credential text before the heap is
|
||||
//! returned to the allocator.
|
||||
//!
|
||||
//! This is the minimal stand-in for the well-known `zeroize` crate: ltk is
|
||||
//! a UI toolkit, the only call site is text-input wiping, and the cost of
|
||||
//! pulling another dependency is not justified.
|
||||
|
||||
use core::ptr;
|
||||
use core::sync::atomic::{ compiler_fence, Ordering };
|
||||
|
||||
/// Overwrite `buf` with zeros. The writes go through `write_volatile` so
|
||||
/// the compiler treats them as observable side effects — the elision pass
|
||||
/// cannot drop them even when the buffer is about to be freed.
|
||||
///
|
||||
/// A `compiler_fence(SeqCst)` after the loop pins the wipe to "before any
|
||||
/// later memory operation", so a subsequent `Drop` that hands the
|
||||
/// underlying allocation back to the allocator cannot be reordered above
|
||||
/// the zero stores.
|
||||
pub( crate ) fn secure_zero( buf: &mut [u8] )
|
||||
{
|
||||
for b in buf.iter_mut()
|
||||
{
|
||||
// SAFETY: writing a primitive byte through a unique mutable
|
||||
// reference; volatile reflects the intent that the store has an
|
||||
// observer beyond ordinary Rust semantics (the ex-credential).
|
||||
unsafe { ptr::write_volatile( b, 0u8 ); }
|
||||
}
|
||||
compiler_fence( Ordering::SeqCst );
|
||||
}
|
||||
|
||||
#[ cfg( test ) ]
|
||||
mod tests
|
||||
{
|
||||
use super::*;
|
||||
|
||||
#[ test ]
|
||||
fn empty_slice_is_a_noop()
|
||||
{
|
||||
let mut buf: [u8; 0] = [];
|
||||
secure_zero( &mut buf );
|
||||
// Nothing to assert beyond "did not panic" — exercised so the
|
||||
// fence + zero-iter loop compiles for the empty case.
|
||||
}
|
||||
|
||||
#[ test ]
|
||||
fn fills_every_byte_with_zero()
|
||||
{
|
||||
let mut buf = [ 0xAAu8; 64 ];
|
||||
secure_zero( &mut buf );
|
||||
assert!( buf.iter().all( |&b| b == 0 ) );
|
||||
}
|
||||
|
||||
#[ test ]
|
||||
fn wipes_a_credential_string_in_place()
|
||||
{
|
||||
let mut password = String::from( "hunter2" );
|
||||
// SAFETY: as_mut_vec lets us reach the underlying byte buffer.
|
||||
// We only write zeros, leaving an empty / NUL-filled UTF-8 byte
|
||||
// sequence which is still valid UTF-8 (NUL is U+0000).
|
||||
let bytes = unsafe { password.as_mut_vec() };
|
||||
secure_zero( bytes );
|
||||
assert!( bytes.iter().all( |&b| b == 0 ) );
|
||||
// After the wipe the String is technically all NULs, not empty;
|
||||
// the consumer drops it immediately so the heap allocation is
|
||||
// returned to the allocator already overwritten.
|
||||
assert_eq!( password.len(), 7 );
|
||||
assert!( password.bytes().all( |b| b == 0 ) );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user