11//! In-memory credential encryption using ChaCha20-Poly1305.
22//!
33//! This module provides encryption-at-rest for passwords stored in the credential store.
4- //! A randomly generated 256-bit master key is protected using libsodium's memory locking
5- //! facilities (mlock/mprotect), and passwords are encrypted using ChaCha20-Poly1305 AEAD.
4+ //! A randomly generated 256-bit master key is stored in a zeroize-on-drop wrapper.
5+ //! When the `mlock` feature is enabled, libsodium's memory locking facilities
6+ //! (mlock/mprotect) are additionally used to prevent the key from being swapped to
7+ //! disk or appearing in core dumps.
68//!
79//! ## Security Properties
810//!
9- //! - Master key stored in mlock'd memory (excluded from core dumps)
1011//! - Passwords encrypted at rest in regular heap memory
1112//! - Decryption on-demand into short-lived zeroized buffers
1213//! - ChaCha20-Poly1305 provides authenticated encryption
1314//! - Random 96-bit nonces prevent nonce reuse
15+ //! - Master key zeroized on drop
16+ //! - With `mlock` feature: Master key stored in mlock'd memory (excluded from core dumps)
1417
1518use core:: fmt;
1619use std:: sync:: LazyLock ;
@@ -20,28 +23,33 @@ use chacha20poly1305::aead::{Aead, AeadCore, KeyInit, OsRng};
2023use chacha20poly1305:: { ChaCha20Poly1305 , Nonce } ;
2124use parking_lot:: Mutex ;
2225use rand:: RngCore as _;
26+ #[ cfg( feature = "mlock" ) ]
2327use secrets:: SecretBox ;
28+ #[ cfg( not( feature = "mlock" ) ) ]
29+ use zeroize:: Zeroizing ;
2430use zeroize:: { Zeroize , ZeroizeOnDrop } ;
2531
2632/// Global master key for credential encryption.
2733///
28- /// Initialized lazily on first access. The key material is stored in memory
29- /// protected by mlock/mprotect via libsodium's SecretBox, wrapped in a Mutex
30- /// for thread-safe access .
34+ /// Initialized lazily on first access. The key material is wrapped in a Mutex
35+ /// for thread-safe access. With the `mlock` feature, key memory is additionally
36+ /// protected by mlock/mprotect via libsodium's SecretBox .
3137pub ( super ) static MASTER_KEY : LazyLock < Mutex < MasterKeyManager > > = LazyLock :: new ( || {
3238 Mutex :: new ( MasterKeyManager :: new ( ) . expect ( "failed to initialize credential encryption master key" ) )
3339} ) ;
3440
35- /// Manages the master encryption key using libsodium's secure memory facilities .
41+ /// Manages the master encryption key.
3642///
37- /// The key is stored in memory that is:
43+ /// The key is zeroized on drop. When the `mlock` feature is enabled, the key
44+ /// memory is additionally:
3845/// - Locked (mlock) to prevent swapping to disk
3946/// - Protected (mprotect) with appropriate access controls
4047/// - Excluded from core dumps
41- /// - Zeroized on drop
4248pub ( super ) struct MasterKeyManager {
43- // SecretBox provides mlock/mprotect for the key material.
49+ # [ cfg ( feature = " mlock" ) ]
4450 key_material : SecretBox < [ u8 ; 32 ] > ,
51+ #[ cfg( not( feature = "mlock" ) ) ]
52+ key_material : Zeroizing < [ u8 ; 32 ] > ,
4553}
4654
4755impl MasterKeyManager {
@@ -51,22 +59,36 @@ impl MasterKeyManager {
5159 ///
5260 /// Returns error if secure memory allocation fails or RNG fails.
5361 fn new ( ) -> anyhow:: Result < Self > {
54- // SecretBox allocates memory with mlock and mprotect.
62+ # [ cfg ( feature = " mlock" ) ]
5563 let key_material = SecretBox :: try_new ( |key_bytes : & mut [ u8 ; 32 ] | {
5664 OsRng . fill_bytes ( key_bytes) ;
5765 Ok :: < _ , anyhow:: Error > ( ( ) )
5866 } )
5967 . context ( "failed to allocate secure memory for master key" ) ?;
6068
69+ #[ cfg( not( feature = "mlock" ) ) ]
70+ let key_material = {
71+ let mut key = Zeroizing :: new ( [ 0u8 ; 32 ] ) ;
72+ OsRng . fill_bytes ( key. as_mut ( ) ) ;
73+ key
74+ } ;
75+
6176 Ok ( Self { key_material } )
6277 }
6378
6479 /// Encrypt a password using ChaCha20-Poly1305.
6580 ///
6681 /// Returns the nonce and ciphertext (which includes the Poly1305 auth tag).
6782 pub ( super ) fn encrypt ( & self , plaintext : & str ) -> anyhow:: Result < EncryptedPassword > {
83+ #[ cfg( feature = "mlock" ) ]
6884 let key_ref = self . key_material . borrow ( ) ;
69- let cipher = ChaCha20Poly1305 :: new_from_slice ( key_ref. as_ref ( ) ) . expect ( "key is exactly 32 bytes" ) ;
85+ #[ cfg( feature = "mlock" ) ]
86+ let key_bytes: & [ u8 ] = key_ref. as_ref ( ) ;
87+
88+ #[ cfg( not( feature = "mlock" ) ) ]
89+ let key_bytes: & [ u8 ] = self . key_material . as_ref ( ) ;
90+
91+ let cipher = ChaCha20Poly1305 :: new_from_slice ( key_bytes) . expect ( "key is exactly 32 bytes" ) ;
7092
7193 // Generate random 96-bit nonce (12 bytes for ChaCha20-Poly1305).
7294 let nonce = ChaCha20Poly1305 :: generate_nonce ( OsRng ) ;
@@ -84,8 +106,15 @@ impl MasterKeyManager {
84106 /// The returned `DecryptedPassword` should have a short lifetime.
85107 /// Use it immediately and let it drop to zeroize the plaintext.
86108 pub ( super ) fn decrypt ( & self , encrypted : & EncryptedPassword ) -> anyhow:: Result < DecryptedPassword > {
109+ #[ cfg( feature = "mlock" ) ]
87110 let key_ref = self . key_material . borrow ( ) ;
88- let cipher = ChaCha20Poly1305 :: new_from_slice ( key_ref. as_ref ( ) ) . expect ( "key is exactly 32 bytes" ) ;
111+ #[ cfg( feature = "mlock" ) ]
112+ let key_bytes: & [ u8 ] = key_ref. as_ref ( ) ;
113+
114+ #[ cfg( not( feature = "mlock" ) ) ]
115+ let key_bytes: & [ u8 ] = self . key_material . as_ref ( ) ;
116+
117+ let cipher = ChaCha20Poly1305 :: new_from_slice ( key_bytes) . expect ( "key is exactly 32 bytes" ) ;
89118
90119 let plaintext_bytes = cipher
91120 . decrypt ( & encrypted. nonce , encrypted. ciphertext . as_ref ( ) )
@@ -98,7 +127,8 @@ impl MasterKeyManager {
98127 }
99128}
100129
101- // Note: SecretBox handles secure zeroization and munlock automatically on drop.
130+ // Note: With `mlock` feature, SecretBox handles secure zeroization and munlock automatically on drop.
131+ // Without `mlock` feature, Zeroizing handles secure zeroization on drop (no mlock).
102132
103133/// Encrypted password stored in heap memory.
104134///
0 commit comments