gardesk/garlock / bb76690

Browse files

Add secure password buffer with zeroize

- Automatic memory zeroing on drop
- UTF-8 character support for international passwords
- Proper backspace handling for multi-byte characters
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
bb7669059c4c3bc15479b1281245324707cf05e2
Parents
5367521
Tree
f253e0b

1 changed file

StatusFile+-
A garlock/src/password.rs 190 0
garlock/src/password.rsadded
@@ -0,0 +1,190 @@
1
+//! Secure password buffer for garlock
2
+//!
3
+//! Implements a password buffer that securely clears memory on drop
4
+//! using the zeroize crate to prevent password remnants in memory.
5
+
6
+use zeroize::{Zeroize, ZeroizeOnDrop};
7
+
8
+/// Default buffer capacity (should be enough for most passwords)
9
+const DEFAULT_CAPACITY: usize = 256;
10
+
11
+/// Secure password buffer
12
+///
13
+/// This buffer automatically zeroes its contents when dropped,
14
+/// preventing password data from lingering in memory.
15
+#[derive(ZeroizeOnDrop)]
16
+pub struct Password {
17
+    /// Raw UTF-8 bytes of the password
18
+    buffer: Vec<u8>,
19
+    /// Current length in bytes (not characters)
20
+    len: usize,
21
+}
22
+
23
+impl Password {
24
+    /// Create a new empty password buffer
25
+    pub fn new() -> Self {
26
+        Self {
27
+            buffer: vec![0u8; DEFAULT_CAPACITY],
28
+            len: 0,
29
+        }
30
+    }
31
+
32
+    /// Create a password buffer with custom capacity
33
+    pub fn with_capacity(capacity: usize) -> Self {
34
+        Self {
35
+            buffer: vec![0u8; capacity],
36
+            len: 0,
37
+        }
38
+    }
39
+
40
+    /// Push a character onto the password buffer
41
+    ///
42
+    /// Returns `true` if the character was added, `false` if buffer is full.
43
+    pub fn push(&mut self, c: char) -> bool {
44
+        let mut buf = [0u8; 4];
45
+        let encoded = c.encode_utf8(&mut buf);
46
+        let bytes = encoded.as_bytes();
47
+
48
+        // Check if we have room
49
+        if self.len + bytes.len() > self.buffer.len() {
50
+            tracing::warn!("Password buffer full, cannot add character");
51
+            return false;
52
+        }
53
+
54
+        // Copy bytes into buffer
55
+        self.buffer[self.len..self.len + bytes.len()].copy_from_slice(bytes);
56
+        self.len += bytes.len();
57
+
58
+        tracing::trace!(len = self.len, "Character added to password buffer");
59
+        true
60
+    }
61
+
62
+    /// Remove the last character from the password buffer
63
+    ///
64
+    /// Returns `true` if a character was removed, `false` if buffer was empty.
65
+    pub fn pop(&mut self) -> bool {
66
+        if self.len == 0 {
67
+            return false;
68
+        }
69
+
70
+        // Find the start of the last UTF-8 character
71
+        // UTF-8 continuation bytes start with 10xxxxxx (0x80-0xBF)
72
+        let mut char_start = self.len - 1;
73
+        while char_start > 0 && (self.buffer[char_start] & 0xC0) == 0x80 {
74
+            char_start -= 1;
75
+        }
76
+
77
+        // Zero out the removed bytes
78
+        for i in char_start..self.len {
79
+            self.buffer[i] = 0;
80
+        }
81
+        self.len = char_start;
82
+
83
+        tracing::trace!(len = self.len, "Character removed from password buffer");
84
+        true
85
+    }
86
+
87
+    /// Clear the entire password buffer
88
+    ///
89
+    /// Securely zeros all data in the buffer.
90
+    pub fn clear(&mut self) {
91
+        self.buffer[..self.len].zeroize();
92
+        self.len = 0;
93
+        tracing::trace!("Password buffer cleared");
94
+    }
95
+
96
+    /// Check if the password buffer is empty
97
+    pub fn is_empty(&self) -> bool {
98
+        self.len == 0
99
+    }
100
+
101
+    /// Get the current length in bytes
102
+    pub fn len(&self) -> usize {
103
+        self.len
104
+    }
105
+
106
+    /// Get the number of characters in the password
107
+    pub fn char_count(&self) -> usize {
108
+        self.as_str().chars().count()
109
+    }
110
+
111
+    /// Get the password as a string slice
112
+    ///
113
+    /// This is used when passing the password to PAM for authentication.
114
+    pub fn as_str(&self) -> &str {
115
+        // Safety: We only ever add valid UTF-8 characters via push()
116
+        std::str::from_utf8(&self.buffer[..self.len]).unwrap_or("")
117
+    }
118
+}
119
+
120
+impl Default for Password {
121
+    fn default() -> Self {
122
+        Self::new()
123
+    }
124
+}
125
+
126
+// Manual Zeroize implementation for extra safety
127
+impl Zeroize for Password {
128
+    fn zeroize(&mut self) {
129
+        self.buffer.zeroize();
130
+        self.len = 0;
131
+    }
132
+}
133
+
134
+#[cfg(test)]
135
+mod tests {
136
+    use super::*;
137
+
138
+    #[test]
139
+    fn test_push_pop() {
140
+        let mut pw = Password::new();
141
+        assert!(pw.is_empty());
142
+
143
+        pw.push('h');
144
+        pw.push('e');
145
+        pw.push('l');
146
+        pw.push('l');
147
+        pw.push('o');
148
+
149
+        assert_eq!(pw.as_str(), "hello");
150
+        assert_eq!(pw.char_count(), 5);
151
+
152
+        pw.pop();
153
+        assert_eq!(pw.as_str(), "hell");
154
+
155
+        pw.clear();
156
+        assert!(pw.is_empty());
157
+    }
158
+
159
+    #[test]
160
+    fn test_unicode() {
161
+        let mut pw = Password::new();
162
+
163
+        pw.push('日');
164
+        pw.push('本');
165
+        pw.push('語');
166
+
167
+        assert_eq!(pw.as_str(), "日本語");
168
+        assert_eq!(pw.char_count(), 3);
169
+        assert_eq!(pw.len(), 9); // 3 bytes per character
170
+
171
+        pw.pop();
172
+        assert_eq!(pw.as_str(), "日本");
173
+    }
174
+
175
+    #[test]
176
+    fn test_mixed_characters() {
177
+        let mut pw = Password::new();
178
+
179
+        pw.push('a');
180
+        pw.push('é');
181
+        pw.push('日');
182
+        pw.push('!');
183
+
184
+        assert_eq!(pw.char_count(), 4);
185
+
186
+        pw.pop(); // !
187
+        pw.pop(); // 日
188
+        assert_eq!(pw.as_str(), "aé");
189
+    }
190
+}