79 lines
2.7 KiB
Rust
79 lines
2.7 KiB
Rust
// 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 ) );
|
|
}
|
|
}
|