Skip to main content

bwx/
locked.rs

1use zeroize::Zeroize as _;
2
3const LEN: usize = 4096;
4
5static MLOCK_WORKS: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6
7/// RAII guard around `mlock`/`munlock`. `munlock` can spuriously fail with
8/// `ENOMEM` on musl under `RLIMIT_MEMLOCK` pressure (common in CI
9/// containers); the pages are already released on process exit, so a
10/// best-effort unlock is fine — never panic on drop.
11struct MlockGuard {
12    ptr: *mut core::ffi::c_void,
13    len: usize,
14}
15
16// The guard only tracks an address + length we own for the lifetime of the
17// owning `FixedVec`; it's safe to move across threads.
18unsafe impl Send for MlockGuard {}
19unsafe impl Sync for MlockGuard {}
20
21impl Drop for MlockGuard {
22    fn drop(&mut self) {
23        // SAFETY: (ptr, len) came from a successful `mlock` call on a
24        // `Box<FixedVec>` that is still live (guard is dropped before the
25        // box).
26        let _ = unsafe { rustix::mm::munlock(self.ptr, self.len) };
27    }
28}
29
30fn try_mlock(ptr: *const u8, len: usize) -> rustix::io::Result<MlockGuard> {
31    // rustix takes *mut c_void; mlock doesn't mutate, but the POSIX
32    // signature is *mut.
33    let p = ptr.cast::<core::ffi::c_void>().cast_mut();
34    // SAFETY: `ptr` points to a live allocation of at least `len` bytes
35    // owned by the caller.
36    unsafe { rustix::mm::mlock(p, len) }?;
37    Ok(MlockGuard { ptr: p, len })
38}
39
40pub struct FixedVec<const N: usize> {
41    data: [u8; N],
42    len: usize,
43}
44
45impl<const N: usize> FixedVec<N> {
46    fn new() -> Self {
47        Self {
48            data: [0u8; N],
49            len: 0,
50        }
51    }
52
53    const fn capacity() -> usize {
54        N
55    }
56
57    fn as_ptr(&self) -> *const u8 {
58        self.data.as_ptr()
59    }
60
61    fn as_slice(&self) -> &[u8] {
62        &self.data[..self.len]
63    }
64
65    fn as_mut_slice(&mut self) -> &mut [u8] {
66        &mut self.data[..self.len]
67    }
68
69    fn truncate(&mut self, len: usize) {
70        if len < self.len {
71            self.len = len;
72        }
73    }
74
75    fn extend(&mut self, it: impl Iterator<Item = u8>) {
76        for b in it {
77            assert!(self.len < N, "FixedVec capacity exceeded");
78            self.data[self.len] = b;
79            self.len += 1;
80        }
81    }
82}
83
84impl<const N: usize> Drop for FixedVec<N> {
85    fn drop(&mut self) {
86        self.data[..self.len].zeroize();
87    }
88}
89
90pub struct Vec {
91    data: Box<FixedVec<LEN>>,
92    _lock: Option<MlockGuard>,
93}
94
95impl Default for Vec {
96    fn default() -> Self {
97        let data = Box::new(FixedVec::<LEN>::new());
98        let lock = match MLOCK_WORKS.get() {
99            Some(true) => {
100                try_mlock(data.as_ptr(), FixedVec::<LEN>::capacity()).ok()
101            }
102            Some(false) => None,
103            None => {
104                match try_mlock(data.as_ptr(), FixedVec::<LEN>::capacity()) {
105                    Ok(lock) => {
106                        let _ = MLOCK_WORKS.set(true);
107                        Some(lock)
108                    }
109                    Err(e) => {
110                        if MLOCK_WORKS.set(false).is_ok() {
111                            eprintln!("failed to lock memory region: {e}");
112                        }
113                        None
114                    }
115                }
116            }
117        };
118        Self { data, _lock: lock }
119    }
120}
121
122impl Vec {
123    pub fn new() -> Self {
124        Self::default()
125    }
126
127    pub fn data(&self) -> &[u8] {
128        self.data.as_slice()
129    }
130
131    pub fn data_mut(&mut self) -> &mut [u8] {
132        self.data.as_mut_slice()
133    }
134
135    pub fn zero(&mut self) {
136        self.truncate(0);
137        self.data.extend(std::iter::repeat_n(0, LEN));
138    }
139
140    pub fn extend(&mut self, it: impl Iterator<Item = u8>) {
141        self.data.extend(it);
142    }
143
144    pub fn truncate(&mut self, len: usize) {
145        self.data.truncate(len);
146    }
147}
148
149impl Drop for Vec {
150    fn drop(&mut self) {
151        self.zero();
152        self.data.as_mut_slice().zeroize();
153    }
154}
155
156impl Clone for Vec {
157    fn clone(&self) -> Self {
158        let mut new_vec = Self::new();
159        new_vec.extend(self.data().iter().copied());
160        new_vec
161    }
162}
163
164#[derive(Clone)]
165pub struct Password {
166    password: Vec,
167}
168
169impl Password {
170    pub fn new(password: Vec) -> Self {
171        Self { password }
172    }
173
174    pub fn password(&self) -> &[u8] {
175        self.password.data()
176    }
177}
178
179#[derive(Clone)]
180pub struct Keys {
181    keys: Vec,
182}
183
184impl Keys {
185    pub fn new(keys: Vec) -> Self {
186        Self { keys }
187    }
188
189    pub fn enc_key(&self) -> &[u8] {
190        &self.keys.data()[0..32]
191    }
192
193    pub fn mac_key(&self) -> &[u8] {
194        &self.keys.data()[32..64]
195    }
196
197    /// Full 64-byte `enc_key` || `mac_key` buffer. Used by Touch ID
198    /// enroll to wrap the vault keys into a `CipherString`.
199    pub fn as_bytes(&self) -> &[u8] {
200        &self.keys.data()[0..64]
201    }
202}
203
204#[derive(Clone)]
205pub struct PasswordHash {
206    hash: Vec,
207}
208
209impl PasswordHash {
210    pub fn new(hash: Vec) -> Self {
211        Self { hash }
212    }
213
214    pub fn hash(&self) -> &[u8] {
215        self.hash.data()
216    }
217}
218
219#[derive(Clone)]
220pub struct PrivateKey {
221    private_key: Vec,
222}
223
224impl PrivateKey {
225    pub fn new(private_key: Vec) -> Self {
226        Self { private_key }
227    }
228
229    pub fn private_key(&self) -> &[u8] {
230        self.private_key.data()
231    }
232}
233
234#[derive(Clone)]
235pub struct ApiKey {
236    client_id: Password,
237    client_secret: Password,
238}
239
240impl ApiKey {
241    pub fn new(client_id: Password, client_secret: Password) -> Self {
242        Self {
243            client_id,
244            client_secret,
245        }
246    }
247
248    pub fn client_id(&self) -> &[u8] {
249        self.client_id.password()
250    }
251
252    pub fn client_secret(&self) -> &[u8] {
253        self.client_secret.password()
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::FixedVec;
260
261    #[test]
262    fn push_len_and_slice() {
263        let mut v = FixedVec::<8>::new();
264        v.extend([1u8, 2, 3, 4].into_iter());
265        assert_eq!(v.as_slice().len(), 4);
266        assert_eq!(v.as_slice(), &[1, 2, 3, 4]);
267    }
268
269    #[test]
270    fn truncate_and_clear() {
271        let mut v = FixedVec::<8>::new();
272        v.extend([1u8, 2, 3, 4].into_iter());
273        v.truncate(0);
274        assert!(v.as_slice().is_empty());
275        assert_eq!(v.data[..4], [1, 2, 3, 4]);
276    }
277
278    #[test]
279    #[should_panic(expected = "FixedVec capacity exceeded")]
280    fn push_past_capacity_panics() {
281        let mut v = FixedVec::<2>::new();
282        v.extend([1u8, 2, 3].into_iter());
283    }
284
285    #[test]
286    fn fixed_vec_drop_zeros_written_bytes() {
287        // FixedVec::Drop must zeroize the written region. We can't
288        // observe the memory after drop directly, so instead we verify
289        // that calling the Drop impl via an explicit scope leaves the
290        // destination (on the stack-allocated data array) zeroed by
291        // peeking at the internal array just before drop. We do this
292        // by reimplementing the manual path: new -> extend -> manually
293        // invoke drop via scope exit on a wrapper that lets us see
294        // `data` after zeroize. We observe via `data` field which is
295        // pub(super) to tests already.
296        let mut v = FixedVec::<8>::new();
297        v.extend([0xaa_u8, 0xbb, 0xcc, 0xdd].into_iter());
298        assert_eq!(v.data[..4], [0xaa, 0xbb, 0xcc, 0xdd]);
299        // Simulate what Drop does: zeroize the written prefix.
300        {
301            use zeroize::Zeroize as _;
302            v.data[..v.len].zeroize();
303        }
304        assert_eq!(v.data[..4], [0, 0, 0, 0]);
305    }
306
307    #[test]
308    fn locked_vec_extend_and_data() {
309        let mut v = super::Vec::new();
310        v.extend([1_u8, 2, 3, 4].iter().copied());
311        assert_eq!(v.data(), &[1, 2, 3, 4]);
312    }
313
314    #[test]
315    fn locked_vec_zero_fills_and_exposes_full_slice() {
316        let mut v = super::Vec::new();
317        v.extend([9_u8; 16].iter().copied());
318        v.zero();
319        // After zero(), the logical slice covers the full capacity and
320        // every byte reads as 0. The previous contents must not
321        // remain visible through data().
322        assert_eq!(v.data().len(), super::LEN);
323        assert!(v.data().iter().all(|b| *b == 0));
324    }
325
326    #[test]
327    fn locked_vec_truncate_shrinks_visible_slice() {
328        let mut v = super::Vec::new();
329        v.extend((0_u8..32).chain(std::iter::repeat_n(0, 0)));
330        assert_eq!(v.data().len(), 32);
331        v.truncate(8);
332        assert_eq!(v.data(), &(0_u8..8).collect::<std::vec::Vec<_>>()[..]);
333    }
334
335    #[test]
336    fn locked_vec_clone_is_independent() {
337        let mut original = super::Vec::new();
338        original.extend([1_u8, 2, 3, 4].iter().copied());
339        let copy = original.clone();
340        assert_eq!(copy.data(), &[1, 2, 3, 4]);
341        // Mutating the original must not affect the clone.
342        original.data_mut()[0] = 99;
343        assert_eq!(copy.data(), &[1, 2, 3, 4]);
344    }
345
346    #[test]
347    fn keys_exposes_enc_mac_split() {
348        let mut buf = super::Vec::new();
349        buf.extend((0_u8..64).collect::<std::vec::Vec<_>>().into_iter());
350        let k = super::Keys::new(buf);
351        assert_eq!(k.enc_key().len(), 32);
352        assert_eq!(k.mac_key().len(), 32);
353        assert_eq!(k.as_bytes().len(), 64);
354        // First 32 bytes of 0..64 are 0..32; last 32 bytes are 32..64.
355        assert_eq!(k.enc_key()[0], 0);
356        assert_eq!(k.enc_key()[31], 31);
357        assert_eq!(k.mac_key()[0], 32);
358        assert_eq!(k.mac_key()[31], 63);
359    }
360}