coldstar_signer/
secure_buffer.rs1use std::ops::{Deref, DerefMut};
14use std::ptr;
15use zeroize::Zeroize;
16
17use crate::error::SignerError;
18
19pub struct SecureBuffer {
38 data: Vec<u8>,
40 is_locked: bool,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq)]
46pub enum LockingMode {
47 Strict,
49 Permissive,
51}
52
53impl SecureBuffer {
54 pub fn new(capacity: usize) -> Result<Self, SignerError> {
69 Self::with_mode(capacity, LockingMode::Permissive)
70 }
71
72 pub fn with_mode(capacity: usize, mode: LockingMode) -> Result<Self, SignerError> {
82 let data = vec![0u8; capacity];
83
84 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 pub fn new_permissive(capacity: usize) -> Result<Self, SignerError> {
113 Self::with_mode(capacity, LockingMode::Permissive)
114 }
115
116 pub fn from_slice(source: &[u8]) -> Result<Self, SignerError> {
124 Self::from_slice_with_mode(source, LockingMode::Permissive)
125 }
126
127 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 pub fn from_slice_permissive(source: &[u8]) -> Result<Self, SignerError> {
136 Self::from_slice_with_mode(source, LockingMode::Permissive)
137 }
138
139 pub fn from_bytes(bytes: &[u8]) -> Result<Self, SignerError> {
141 Self::from_slice(bytes)
142 }
143
144 pub fn len(&self) -> usize {
146 self.data.len()
147 }
148
149 pub fn is_empty(&self) -> bool {
151 self.data.is_empty()
152 }
153
154 pub fn is_locked(&self) -> bool {
156 self.is_locked
157 }
158
159 pub fn as_slice(&self) -> &[u8] {
165 &self.data
166 }
167
168 pub fn as_mut_slice(&mut self) -> &mut [u8] {
174 &mut self.data
175 }
176
177 pub fn as_bytes(&self) -> &[u8] {
179 &self.data
180 }
181
182 pub fn as_mut_bytes(&mut self) -> &mut [u8] {
184 &mut self.data
185 }
186
187 pub fn zeroize(&mut self) {
191 self.data.zeroize();
192 }
193
194 pub fn resize(&mut self, new_len: usize) -> Result<(), SignerError> {
200 self.resize_with_mode(new_len, LockingMode::Strict)
201 }
202
203 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 let mut new_data = vec![0u8; new_len];
212
213 let new_locked = lock_memory(&new_data);
215
216 if mode == LockingMode::Strict && !new_locked {
217 return Err(SignerError::MemoryLockFailed(
219 "mlock failed on resized buffer".to_string(),
220 ));
221 }
222
223 if self.is_locked {
225 unlock_memory(&self.data);
226 }
227
228 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 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 self.data.zeroize();
251
252 if self.is_locked {
254 unlock_memory(&self.data);
255 }
256
257 }
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
275impl 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#[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 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 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 }
363
364pub struct SecureGuard<'a> {
368 data: &'a mut [u8],
369}
370
371impl<'a> SecureGuard<'a> {
372 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 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 }
460 Err(e) => panic!("Unexpected error: {}", e),
461 }
462 }
463}