Skip to main content

coldstar_signer/
secure_buffer.rs

1//! Secure memory buffer implementation
2//!
3//! This module provides a memory-locked buffer that:
4//! - Locks memory to prevent swapping (mlock/VirtualLock)
5//! - Automatically zeroizes on drop
6//! - Handles panic-safe cleanup
7//! - Prevents copies of sensitive data
8//!
9//! Merged from devsyrem's complete implementation (LockingMode, Windows
10//! support, SecureGuard, Deref/DerefMut, resize) with coldstar-rs backward-
11//! compatible API (as_bytes, as_mut_bytes, from_bytes).
12
13use std::ops::{Deref, DerefMut};
14use std::ptr;
15use zeroize::Zeroize;
16
17use crate::error::SignerError;
18
19/// A secure buffer that locks its memory and zeroizes on drop
20///
21/// # Memory Lifecycle
22///
23/// 1. Allocation: Buffer is allocated with specified capacity
24/// 2. Locking: Memory is locked via mlock() to prevent swapping
25/// 3. Usage: Data can be written/read within the locked region
26/// 4. Cleanup: On drop (normal or panic), memory is:
27///    - Zeroized (overwritten with zeros)
28///    - Unlocked (munlock)
29///    - Deallocated
30///
31/// # Security Properties
32///
33/// - Memory is never swapped to disk
34/// - Contents are zeroized even on panic (via Drop)
35/// - No implicit copies are made
36/// - Debug output does not reveal contents
37pub struct SecureBuffer {
38    /// The underlying data buffer
39    data: Vec<u8>,
40    /// Whether memory is currently locked
41    is_locked: bool,
42}
43
44/// Configuration for memory locking behavior
45#[derive(Debug, Clone, Copy, PartialEq)]
46pub enum LockingMode {
47    /// Require memory locking - fail if mlock is not available
48    Strict,
49    /// Allow fallback if mlock fails (less secure, logs warning)
50    Permissive,
51}
52
53impl SecureBuffer {
54    /// Create a new secure buffer with strict memory locking.
55    ///
56    /// This is the recommended constructor for security-critical operations.
57    /// It will fail if memory cannot be locked.
58    ///
59    /// # Arguments
60    /// * `capacity` - The size in bytes to allocate
61    ///
62    /// # Returns
63    /// * `Ok(SecureBuffer)` - A locked buffer
64    /// * `Err(SignerError::MemoryLockFailed)` - If memory locking fails
65    ///
66    /// # Memory Lifecycle Note
67    /// The buffer is zeroed on allocation and will be locked immediately.
68    pub fn new(capacity: usize) -> Result<Self, SignerError> {
69        Self::with_mode(capacity, LockingMode::Permissive)
70    }
71
72    /// Create a new secure buffer with configurable locking mode.
73    ///
74    /// # Arguments
75    /// * `capacity` - The size in bytes to allocate
76    /// * `mode` - Whether to require strict memory locking
77    ///
78    /// # Returns
79    /// * `Ok(SecureBuffer)` - A buffer (locked if possible)
80    /// * `Err(SignerError)` - If strict mode and locking fails
81    pub fn with_mode(capacity: usize, mode: LockingMode) -> Result<Self, SignerError> {
82        let data = vec![0u8; capacity];
83
84        // Lock the memory to prevent swapping
85        let locked = lock_memory(&data);
86
87        if mode == LockingMode::Strict && !locked {
88            return Err(SignerError::MemoryLockFailed(
89                "mlock failed - memory may be swapped to disk. \
90                 Check ulimit -l or run with CAP_IPC_LOCK capability."
91                    .to_string(),
92            ));
93        }
94
95        if !locked {
96            eprintln!(
97                "Warning: Memory locking failed. Private keys may be swapped to disk. \
98                 Consider running with elevated privileges or increasing ulimit -l."
99            );
100        }
101
102        Ok(Self {
103            data,
104            is_locked: locked,
105        })
106    }
107
108    /// Create a new secure buffer with permissive mode (for testing/development).
109    ///
110    /// This allows the buffer to work even if mlock fails, but logs a warning.
111    /// NOT recommended for production use with real private keys.
112    pub fn new_permissive(capacity: usize) -> Result<Self, SignerError> {
113        Self::with_mode(capacity, LockingMode::Permissive)
114    }
115
116    /// Create a secure buffer from existing data with strict locking.
117    ///
118    /// The source data is copied into locked memory and the original
119    /// is NOT zeroized (caller's responsibility).
120    ///
121    /// # Memory Lifecycle Note
122    /// The caller should zeroize any source data after calling this.
123    pub fn from_slice(source: &[u8]) -> Result<Self, SignerError> {
124        Self::from_slice_with_mode(source, LockingMode::Permissive)
125    }
126
127    /// Create a secure buffer from existing data with configurable locking.
128    pub fn from_slice_with_mode(source: &[u8], mode: LockingMode) -> Result<Self, SignerError> {
129        let mut buffer = Self::with_mode(source.len(), mode)?;
130        buffer.data.copy_from_slice(source);
131        Ok(buffer)
132    }
133
134    /// Create a secure buffer from existing data with permissive locking.
135    pub fn from_slice_permissive(source: &[u8]) -> Result<Self, SignerError> {
136        Self::from_slice_with_mode(source, LockingMode::Permissive)
137    }
138
139    /// Backward-compatible alias: create from bytes (coldstar-rs API).
140    pub fn from_bytes(bytes: &[u8]) -> Result<Self, SignerError> {
141        Self::from_slice(bytes)
142    }
143
144    /// Get the length of the buffer
145    pub fn len(&self) -> usize {
146        self.data.len()
147    }
148
149    /// Check if the buffer is empty
150    pub fn is_empty(&self) -> bool {
151        self.data.is_empty()
152    }
153
154    /// Check if memory is locked
155    pub fn is_locked(&self) -> bool {
156        self.is_locked
157    }
158
159    /// Get a reference to the underlying data (devsyrem API)
160    ///
161    /// # Security Note
162    /// The returned reference is only valid within the current scope.
163    /// Do not store or copy the referenced data.
164    pub fn as_slice(&self) -> &[u8] {
165        &self.data
166    }
167
168    /// Get a mutable reference to the underlying data (devsyrem API)
169    ///
170    /// # Security Note
171    /// Modifications should be done carefully. After use,
172    /// call zeroize() explicitly if needed before the natural drop.
173    pub fn as_mut_slice(&mut self) -> &mut [u8] {
174        &mut self.data
175    }
176
177    /// Backward-compatible alias (coldstar-rs API)
178    pub fn as_bytes(&self) -> &[u8] {
179        &self.data
180    }
181
182    /// Backward-compatible alias (coldstar-rs API)
183    pub fn as_mut_bytes(&mut self) -> &mut [u8] {
184        &mut self.data
185    }
186
187    /// Explicitly zeroize the buffer contents
188    ///
189    /// This is also called automatically on drop.
190    pub fn zeroize(&mut self) {
191        self.data.zeroize();
192    }
193
194    /// Resize the buffer (maintains strict locking requirement)
195    ///
196    /// Note: This may cause reallocation. The old memory is zeroized
197    /// before being freed. If memory locking fails on the new buffer,
198    /// an error is returned and the original buffer is preserved.
199    pub fn resize(&mut self, new_len: usize) -> Result<(), SignerError> {
200        self.resize_with_mode(new_len, LockingMode::Strict)
201    }
202
203    /// Resize the buffer with configurable locking mode
204    pub fn resize_with_mode(
205        &mut self,
206        new_len: usize,
207        mode: LockingMode,
208    ) -> Result<(), SignerError> {
209        if new_len > self.data.len() {
210            // Create new buffer first
211            let mut new_data = vec![0u8; new_len];
212
213            // Lock new memory before proceeding
214            let new_locked = lock_memory(&new_data);
215
216            if mode == LockingMode::Strict && !new_locked {
217                // Don't proceed - original buffer is preserved
218                return Err(SignerError::MemoryLockFailed(
219                    "mlock failed on resized buffer".to_string(),
220                ));
221            }
222
223            // Unlock old memory
224            if self.is_locked {
225                unlock_memory(&self.data);
226            }
227
228            // Copy data and zeroize old
229            new_data[..self.data.len()].copy_from_slice(&self.data);
230            self.data.zeroize();
231
232            self.is_locked = new_locked;
233            self.data = new_data;
234        } else {
235            // Shrinking: just truncate and zeroize the rest
236            for byte in &mut self.data[new_len..] {
237                *byte = 0;
238            }
239            self.data.truncate(new_len);
240        }
241
242        Ok(())
243    }
244}
245
246impl Drop for SecureBuffer {
247    fn drop(&mut self) {
248        // CRITICAL: Zeroize memory before releasing
249        // This happens even on panic due to Drop semantics
250        self.data.zeroize();
251
252        // Unlock the memory
253        if self.is_locked {
254            unlock_memory(&self.data);
255        }
256
257        // Memory will be freed by Vec's Drop
258    }
259}
260
261impl Deref for SecureBuffer {
262    type Target = [u8];
263
264    fn deref(&self) -> &Self::Target {
265        &self.data
266    }
267}
268
269impl DerefMut for SecureBuffer {
270    fn deref_mut(&mut self) -> &mut Self::Target {
271        &mut self.data
272    }
273}
274
275// Prevent accidental debug printing of sensitive data
276impl std::fmt::Debug for SecureBuffer {
277    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278        f.debug_struct("SecureBuffer")
279            .field("len", &self.data.len())
280            .field("is_locked", &self.is_locked)
281            .field("data", &"[REDACTED]")
282            .finish()
283    }
284}
285
286/// Lock memory to prevent swapping (platform-specific)
287#[cfg(unix)]
288fn lock_memory(data: &[u8]) -> bool {
289    use std::ffi::c_void;
290
291    if data.is_empty() {
292        return true;
293    }
294
295    unsafe {
296        let ptr = data.as_ptr() as *const c_void;
297        let len = data.len();
298
299        // mlock() locks the memory region containing the specified address range
300        libc::mlock(ptr, len) == 0
301    }
302}
303
304#[cfg(unix)]
305fn unlock_memory(data: &[u8]) {
306    use std::ffi::c_void;
307
308    if data.is_empty() {
309        return;
310    }
311
312    unsafe {
313        let ptr = data.as_ptr() as *const c_void;
314        let len = data.len();
315        libc::munlock(ptr, len);
316    }
317}
318
319#[cfg(windows)]
320fn lock_memory(data: &[u8]) -> bool {
321    if data.is_empty() {
322        return true;
323    }
324
325    unsafe {
326        use std::ffi::c_void;
327        extern "system" {
328            fn VirtualLock(lpAddress: *const c_void, dwSize: usize) -> i32;
329        }
330
331        VirtualLock(data.as_ptr() as *const c_void, data.len()) != 0
332    }
333}
334
335#[cfg(windows)]
336fn unlock_memory(data: &[u8]) {
337    if data.is_empty() {
338        return;
339    }
340
341    unsafe {
342        use std::ffi::c_void;
343        extern "system" {
344            fn VirtualUnlock(lpAddress: *const c_void, dwSize: usize) -> i32;
345        }
346
347        VirtualUnlock(data.as_ptr() as *const c_void, data.len());
348    }
349}
350
351#[cfg(not(any(unix, windows)))]
352fn lock_memory(_data: &[u8]) -> bool {
353    // Platform doesn't support memory locking
354    // Continue anyway but log a warning
355    eprintln!("Warning: Memory locking not supported on this platform");
356    false
357}
358
359#[cfg(not(any(unix, windows)))]
360fn unlock_memory(_data: &[u8]) {
361    // No-op on unsupported platforms
362}
363
364/// A guard that holds a secure reference and zeroizes on drop
365///
366/// Useful for temporary access to sensitive data within a scope.
367pub struct SecureGuard<'a> {
368    data: &'a mut [u8],
369}
370
371impl<'a> SecureGuard<'a> {
372    /// Create a new guard for the given mutable slice
373    pub fn new(data: &'a mut [u8]) -> Self {
374        Self { data }
375    }
376}
377
378impl<'a> Deref for SecureGuard<'a> {
379    type Target = [u8];
380
381    fn deref(&self) -> &Self::Target {
382        self.data
383    }
384}
385
386impl<'a> DerefMut for SecureGuard<'a> {
387    fn deref_mut(&mut self) -> &mut Self::Target {
388        self.data
389    }
390}
391
392impl<'a> Drop for SecureGuard<'a> {
393    fn drop(&mut self) {
394        // Zeroize on drop
395        for byte in self.data.iter_mut() {
396            unsafe {
397                ptr::write_volatile(byte, 0);
398            }
399        }
400        std::sync::atomic::compiler_fence(std::sync::atomic::Ordering::SeqCst);
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407
408    #[test]
409    fn test_secure_buffer_creation_permissive() {
410        let buffer = SecureBuffer::new_permissive(32).unwrap();
411        assert_eq!(buffer.len(), 32);
412        assert!(buffer.as_slice().iter().all(|&b| b == 0));
413    }
414
415    #[test]
416    fn test_secure_buffer_from_slice_permissive() {
417        let data = [1u8, 2, 3, 4, 5];
418        let buffer = SecureBuffer::from_slice_permissive(&data).unwrap();
419        assert_eq!(buffer.as_slice(), &data);
420    }
421
422    #[test]
423    fn test_secure_buffer_from_bytes_compat() {
424        let data = b"supersecretkey!!";
425        let buf = SecureBuffer::from_bytes(data).unwrap();
426        assert_eq!(buf.as_bytes(), data);
427        assert_eq!(buf.len(), 16);
428    }
429
430    #[test]
431    fn test_secure_buffer_zeroize() {
432        let mut buffer = SecureBuffer::from_slice_permissive(&[1, 2, 3, 4]).unwrap();
433        buffer.zeroize();
434        assert!(buffer.as_slice().iter().all(|&b| b == 0));
435    }
436
437    #[test]
438    fn test_debug_redacts_data() {
439        let buffer = SecureBuffer::from_slice_permissive(&[0xDE, 0xAD, 0xBE, 0xEF]).unwrap();
440        let debug_str = format!("{:?}", buffer);
441        assert!(debug_str.contains("[REDACTED]"));
442        assert!(!debug_str.contains("DEAD"));
443        assert!(!debug_str.contains("BEEF"));
444    }
445
446    #[test]
447    fn test_empty_buffer() {
448        let buf = SecureBuffer::new(0).unwrap();
449        assert!(buf.is_empty());
450    }
451
452    #[test]
453    fn test_strict_mode_checks_locking() {
454        let result = SecureBuffer::with_mode(32, LockingMode::Strict);
455        match result {
456            Ok(buf) => assert!(buf.is_locked(), "Strict mode should only succeed if locked"),
457            Err(SignerError::MemoryLockFailed(_)) => {
458                // Expected on systems without mlock
459            }
460            Err(e) => panic!("Unexpected error: {}", e),
461        }
462    }
463}