refactor(secrets): back SecretBytes32/SecretVec with the secrets crate

Replace the homegrown `region::lock` + `Zeroizing` wrappers with thin
adapters over `secrets::SecretBox` and `secrets::SecretVec`. The
upstream crate already provides everything the local types were doing
by hand (mlock, zero-on-drop) and adds protections we didn't have:
guard pages around the allocation and `mprotect`-based access control
(`PROT_NONE` at rest, `PROT_READ` during a borrow, `PROT_READ|WRITE`
during a mut borrow). Net result is a security upgrade, not just a
dependency swap.

Why a local adapter still exists
--------------------------------
`secrets::SecretVec` is fixed-length — no `push`. The tty passphrase
reader needs to append bytes one at a time without ever reallocating
(a panicking partial read must not leave stale plaintext on the heap),
so `SecretVec` keeps a separate logical `len` over a fixed protected
allocation of `MAX_PASSPHRASE_LEN` bytes. Bytes past `len` stay
zero-padding and are never exposed through `with_slice`.

API shape: closure-scoped borrows
---------------------------------
The previous `as_slice` / `as_array` returned long-lived `&[u8]`
references, which would have kept the upstream pages in `PROT_READ`
for the full lifetime of the borrow. The new API uses
`with_array(|s| ...)`, `with_mut_array(|s| ...)`, `with_slice(|s| ...)`
so the unprotected window is exactly the closure body. This is uglier
at call sites (notably the nested closures in `derive_key`) but it's
the right tradeoff — minimizing the unprotected window is the whole
point of using the crate.

AEAD key copy footnote
----------------------
`XChaCha20Poly1305::new` copies the key into its own (unprotected)
state, which then lives in the `aead` binding for the entire
encrypt/decrypt loop. This is unchanged from before — the cipher
state was never protected — but it's now called out explicitly with
a comment at both call sites noting that `chacha20poly1305` zeroizes
that internal copy on drop. Future readers shouldn't have to
rediscover this by reading upstream source.

`from_vec` zeroing
------------------
`SecretVec::from_vec(v: Vec<u8>)` is used on the env-var path. It
calls `secrets::SecretVec::from(&mut [u8])`, which (verified against
secrets-1.3.0: `Box::from` -> `transfer` -> `memtransfer`) copies the
bytes into protected storage and zeroes the source slice. The
original Vec's allocation is then released through the normal
allocator — the bytes inside it are zero, but the heap block itself
isn't specially handled. The doc comment on `from_vec` reflects this
precisely. As before, the env-var path also leaves a copy in the
process `environ` table, which is a known accepted leak.

Cargo.toml
----------
Use `protected-secrets = { package = "secrets", version = "1.3" }`
with default features. The `secrets` crate has no pure-Rust backend
at v1.3 — disabling default features only switches *how* libsodium
is linked (bundled `libsodium-sys` vs. the crate's own bindings to a
system libsodium), and can break builds where the chosen path isn't
set up. Defaults are correct here. The `region` dependency is
dropped.

Test plan
---------
- `cargo build` clean.
- `cargo test` — 2 unit + 18 integration tests pass, including
  `roundtrip_passphrase_argon2id` which exercises the full
  passphrase -> argon2id -> AEAD key path through the new wrappers.
- `cargo clippy` (and `--tests`, `--benches`) clean.
- `cargo +nightly fmt` applied.
This commit is contained in:
2026-05-02 19:14:42 +02:00
parent fe65e1f899
commit 898697016a
5 changed files with 142 additions and 133 deletions
Generated
+58 -38
View File
@@ -95,12 +95,6 @@ version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.11.1" version = "2.11.1"
@@ -286,8 +280,8 @@ dependencies = [
"clap", "clap",
"getrandom 0.3.4", "getrandom 0.3.4",
"libc", "libc",
"region",
"rlimit", "rlimit",
"secrets",
"tempfile", "tempfile",
"windows-sys 0.59.0", "windows-sys 0.59.0",
"zeroize", "zeroize",
@@ -359,15 +353,6 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "mach2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.0" version = "2.8.0"
@@ -392,6 +377,16 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "page_size"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
dependencies = [
"libc",
"winapi",
]
[[package]] [[package]]
name = "password-hash" name = "password-hash"
version = "0.5.0" version = "0.5.0"
@@ -403,6 +398,12 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "pkg-config"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]] [[package]]
name = "poly1305" name = "poly1305"
version = "0.8.0" version = "0.8.0"
@@ -480,18 +481,6 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
[[package]]
name = "region"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7"
dependencies = [
"bitflags 1.3.2",
"libc",
"mach2",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "rlimit" name = "rlimit"
version = "0.10.2" version = "0.10.2"
@@ -507,13 +496,25 @@ version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "secrets"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71f5325144404085953b8078fa4a0b4d224d13f17ee3854534260ad7ff3dfb5c"
dependencies = [
"libc",
"page_size",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.228" version = "1.0.228"
@@ -613,6 +614,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.5" version = "0.9.5"
@@ -643,21 +650,34 @@ dependencies = [
"wit-bindgen", "wit-bindgen",
] ]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.2.1" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.59.0" version = "0.59.0"
+1 -1
View File
@@ -9,7 +9,7 @@ argon2 = "0.5"
chacha20poly1305 = "0.10" chacha20poly1305 = "0.10"
clap = {version = "4", features = ["derive"]} clap = {version = "4", features = ["derive"]}
getrandom = {version = "0.3"} getrandom = {version = "0.3"}
region = "3" protected-secrets = {package = "secrets", version = "1.3"}
zeroize = {version = "1", features = ["derive"]} zeroize = {version = "1", features = ["derive"]}
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
+13 -9
View File
@@ -6,7 +6,7 @@ use std::io::Write;
use crate::error::*; use crate::error::*;
use crate::header::{AlgId, Header, KdfParams, NONCE_PREFIX_LEN, TAG_LEN}; use crate::header::{AlgId, Header, KdfParams, NONCE_PREFIX_LEN, TAG_LEN};
use crate::reader::{AheadReader, ReadInfoChunk}; use crate::reader::{AheadReader, ReadInfoChunk};
use crate::secrets::SecretBytes32; use crate::secrets::{SecretBytes32, SecretVec};
use crate::utils::*; use crate::utils::*;
/// XChaCha20Poly1305 nonce: 24 bytes total. STREAM splits the trailing 5 bytes /// XChaCha20Poly1305 nonce: 24 bytes total. STREAM splits the trailing 5 bytes
@@ -28,15 +28,15 @@ fn make_nonce(prefix: &[u8; NONCE_PREFIX_LEN], counter: u32, last: bool) -> XNon
/// For `KdfParams::Argon2id`, `passphrase` must be supplied. /// For `KdfParams::Argon2id`, `passphrase` must be supplied.
pub fn derive_key( pub fn derive_key(
kdf: &KdfParams, kdf: &KdfParams,
raw_key: Option<&[u8; 32]>, raw_key: Option<&SecretBytes32>,
passphrase: Option<&[u8]>, passphrase: Option<&SecretVec>,
) -> Result<SecretBytes32, FcryError> { ) -> Result<SecretBytes32, FcryError> {
let mut out = SecretBytes32::zeroed(); let mut out = SecretBytes32::zeroed();
match kdf { match kdf {
KdfParams::Raw => { KdfParams::Raw => {
let raw = let raw =
raw_key.ok_or_else(|| FcryError::Format("raw kdf requires --raw-key".into()))?; raw_key.ok_or_else(|| FcryError::Format("raw kdf requires --raw-key".into()))?;
out.as_mut_array().copy_from_slice(raw); raw.with_array(|raw| out.with_mut_array(|out| out.copy_from_slice(raw)));
} }
KdfParams::Argon2id { KdfParams::Argon2id {
salt, salt,
@@ -49,7 +49,7 @@ pub fn derive_key(
let params = argon2::Params::new(*m_cost, *t_cost, *p_cost, Some(32))?; let params = argon2::Params::new(*m_cost, *t_cost, *p_cost, Some(32))?;
let argon = let argon =
argon2::Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params); argon2::Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
argon.hash_password_into(pw, salt, out.as_mut_array())?; pw.with_slice(|pw| out.with_mut_array(|out| argon.hash_password_into(pw, salt, out)))?;
} }
} }
Ok(out) Ok(out)
@@ -79,7 +79,9 @@ pub fn encrypt<S: AsRef<str>>(
let aad = header.encode(); let aad = header.encode();
f_encrypted.write_all(&aad)?; f_encrypted.write_all(&aad)?;
let aead = XChaCha20Poly1305::new(key.as_array().into()); // The AEAD keeps its own unprotected key copy while the loop runs.
// chacha20poly1305 zeroizes that copy on drop.
let aead = key.with_array(|key| XChaCha20Poly1305::new(key.into()));
let mut buf = vec![0u8; chunk_sz]; let mut buf = vec![0u8; chunk_sz];
let mut counter: u32 = 0; let mut counter: u32 = 0;
@@ -121,8 +123,8 @@ pub fn encrypt<S: AsRef<str>>(
pub fn decrypt<S: AsRef<str>>( pub fn decrypt<S: AsRef<str>>(
input_file: Option<S>, input_file: Option<S>,
output_file: Option<S>, output_file: Option<S>,
raw_key: Option<&[u8; 32]>, raw_key: Option<&SecretBytes32>,
passphrase: Option<&[u8]>, passphrase: Option<&SecretVec>,
) -> Result<(), FcryError> { ) -> Result<(), FcryError> {
let mut reader = open_input(input_file)?; let mut reader = open_input(input_file)?;
let header = Header::read(&mut reader)?; let header = Header::read(&mut reader)?;
@@ -136,7 +138,9 @@ pub fn decrypt<S: AsRef<str>>(
let mut f_encrypted = AheadReader::from(reader, cipher_chunk); let mut f_encrypted = AheadReader::from(reader, cipher_chunk);
let mut f_plain = OutSink::open(output_file)?; let mut f_plain = OutSink::open(output_file)?;
let aead = XChaCha20Poly1305::new(key.as_array().into()); // The AEAD keeps its own unprotected key copy while the loop runs.
// chacha20poly1305 zeroizes that copy on drop.
let aead = key.with_array(|key| XChaCha20Poly1305::new(key.into()));
let mut buf = vec![0u8; cipher_chunk]; let mut buf = vec![0u8; cipher_chunk];
let mut counter: u32 = 0; let mut counter: u32 = 0;
+6 -12
View File
@@ -73,7 +73,7 @@ fn parse_raw_key(s: &str) -> Result<SecretBytes32, FcryError> {
))); )));
} }
let mut key = SecretBytes32::zeroed(); let mut key = SecretBytes32::zeroed();
key.as_mut_array().copy_from_slice(raw); key.with_mut_array(|key| key.copy_from_slice(raw));
Ok(key) Ok(key)
} }
@@ -86,9 +86,8 @@ enum PassphraseSource {
fn read_passphrase(src: &PassphraseSource, confirm: bool) -> Result<SecretVec, FcryError> { fn read_passphrase(src: &PassphraseSource, confirm: bool) -> Result<SecretVec, FcryError> {
match src { match src {
PassphraseSource::EnvVar(var) => { PassphraseSource::EnvVar(var) => {
// Take the env value, then immediately convert to a Zeroize+mlock'd // Take the env value, then immediately copy it into upstream
// buffer. The original `String` from `env::var` is consumed by // protected storage. The source Vec is zeroed after the copy.
// `into_bytes()`, so its allocation moves into our SecretVec.
// Note: a copy still exists in the process `environ` table; that is // Note: a copy still exists in the process `environ` table; that is
// a known and accepted leak for the env-var path. // a known and accepted leak for the env-var path.
let v = std::env::var(var).map_err(|_| { let v = std::env::var(var).map_err(|_| {
@@ -105,7 +104,7 @@ fn read_passphrase(src: &PassphraseSource, confirm: bool) -> Result<SecretVec, F
if pw != pw2 { if pw != pw2 {
return Err(FcryError::Passphrase("passphrases do not match".into())); return Err(FcryError::Passphrase("passphrases do not match".into()));
} }
// pw2 dropped here -> zeroized + munlocked. // pw2 dropped here -> zeroized + unlocked by the upstream crate.
} }
Ok(pw) Ok(pw)
} }
@@ -160,12 +159,7 @@ fn run(mut cli: Cli) -> Result<(), FcryError> {
Some(src) => Some(read_passphrase(src, false)?), Some(src) => Some(read_passphrase(src, false)?),
None => None, None => None,
}; };
decrypt( decrypt(input, output, raw_key.as_ref(), pw.as_ref())?;
input,
output,
raw_key.as_ref().map(|k| k.as_array()),
pw.as_ref().map(|p| p.as_slice()),
)?;
} else { } else {
let (key, kdf) = if let Some(src) = &pw_src { let (key, kdf) = if let Some(src) = &pw_src {
let mut salt = [0u8; ARGON2_SALT_LEN]; let mut salt = [0u8; ARGON2_SALT_LEN];
@@ -180,7 +174,7 @@ fn run(mut cli: Cli) -> Result<(), FcryError> {
p_cost: argon_parallelism, p_cost: argon_parallelism,
}; };
let pw = read_passphrase(src, true)?; let pw = read_passphrase(src, true)?;
let key = derive_key(&kdf, None, Some(pw.as_slice()))?; let key = derive_key(&kdf, None, Some(&pw))?;
(key, kdf) (key, kdf)
} else { } else {
let key = parse_raw_key(raw_key_str.as_deref().unwrap())?; let key = parse_raw_key(raw_key_str.as_deref().unwrap())?;
+56 -65
View File
@@ -2,120 +2,111 @@
//! Secret-handling primitives. //! Secret-handling primitives.
//! //!
//! Two wrappers and a cross-platform passphrase reader: //! Thin local adapters around the upstream `secrets` crate plus a
//! cross-platform passphrase reader:
//! //!
//! * [`SecretBytes32`] — heap-allocated 32-byte buffer, mlock'd, zero on drop. //! * [`SecretBytes32`] — heap-allocated 32-byte buffer protected by
//! * [`SecretVec`] — heap-allocated `Vec<u8>` with stable capacity, mlock'd, //! `secrets::SecretBox`.
//! zero on drop. //! * [`SecretVec`] — fixed-allocation byte buffer protected by
//! `secrets::SecretVec`, with a separate logical length so tty input can be
//! appended without reallocations.
//! * [`read_passphrase_tty`] — direct tty reader (Linux/macOS termios, //! * [`read_passphrase_tty`] — direct tty reader (Linux/macOS termios,
//! Windows Console API). Reads into a pre-reserved `SecretVec` so no //! Windows Console API). Reads into a pre-reserved `SecretVec` so no
//! reallocation can leave stale unzeroed copies on the heap. //! reallocation can leave stale unzeroed copies on the heap.
//!
//! mlock is provided via the `region` crate (portable across Linux/macOS/Windows).
//! The lock is dropped *before* the underlying buffer is freed (field order
//! matters in Rust drop semantics).
use std::io; use std::io;
use zeroize::Zeroizing;
use protected_secrets::{SecretBox as ProtectedSecretBox, SecretVec as ProtectedSecretVec};
/// Maximum passphrase length we accept on the tty. /// Maximum passphrase length we accept on the tty.
/// Pre-reserved so the underlying Vec never reallocates while reading. /// Pre-reserved so the underlying Vec never reallocates while reading.
pub const MAX_PASSPHRASE_LEN: usize = 1024; pub const MAX_PASSPHRASE_LEN: usize = 1024;
/// Heap-allocated 32-byte secret. mlock'd; zeroed on drop. /// Heap-allocated 32-byte secret protected by the upstream `secrets` crate.
pub struct SecretBytes32 { pub struct SecretBytes32 {
// Field order matters: `_lock` is dropped first (munlock the page), then inner: ProtectedSecretBox<[u8; 32]>,
// `inner` is dropped (zeroize the bytes, then free).
_lock: Option<region::LockGuard>,
inner: Box<Zeroizing<[u8; 32]>>,
} }
impl SecretBytes32 { impl SecretBytes32 {
pub fn zeroed() -> Self { pub fn zeroed() -> Self {
let inner = Box::new(Zeroizing::new([0u8; 32])); Self {
let lock = region::lock(inner.as_ptr(), inner.len()).ok(); inner: ProtectedSecretBox::zero(),
Self { _lock: lock, inner }
}
pub fn as_array(&self) -> &[u8; 32] {
&self.inner
}
pub fn as_mut_array(&mut self) -> &mut [u8; 32] {
&mut self.inner
} }
} }
/// Heap-allocated byte buffer with **fixed capacity** that is mlock'd and pub fn with_array<R>(&self, f: impl FnOnce(&[u8; 32]) -> R) -> R {
/// zeroed on drop. Pushing beyond the reserved capacity is rejected so the let inner = self.inner.borrow();
/// underlying allocation never moves (which would invalidate the lock and f(&inner)
/// leave a stale unzeroed copy behind). }
pub fn with_mut_array<R>(&mut self, f: impl FnOnce(&mut [u8; 32]) -> R) -> R {
let mut inner = self.inner.borrow_mut();
f(&mut inner)
}
}
/// Heap-allocated byte buffer with **fixed capacity** protected by upstream
/// `secrets::SecretVec`.
///
/// Upstream `SecretVec` is fixed-length, so this adapter stores a separate
/// logical length. Bytes after `len` remain zero-filled padding and are never
/// exposed through [`SecretVec::with_slice`].
pub struct SecretVec { pub struct SecretVec {
_lock: Option<region::LockGuard>, inner: ProtectedSecretVec<u8>,
inner: Zeroizing<Vec<u8>>, len: usize,
capacity: usize,
} }
impl SecretVec { impl SecretVec {
/// Allocate a buffer with fixed `capacity` and mlock it. /// Allocate a protected buffer with fixed `capacity`.
pub fn with_capacity(capacity: usize) -> Self { pub fn with_capacity(capacity: usize) -> Self {
let inner = Zeroizing::new(Vec::with_capacity(capacity));
let lock = if capacity > 0 {
region::lock(inner.as_ptr(), capacity).ok()
} else {
None
};
Self { Self {
_lock: lock, inner: ProtectedSecretVec::zero(capacity),
inner, len: 0,
capacity,
} }
} }
/// Wrap an already-allocated `Vec<u8>` (e.g. one we got from /// Copy bytes from an already-allocated `Vec<u8>` into protected storage.
/// `String::into_bytes()` for the env-var path). The Vec's `capacity` /// The upstream conversion zeroes the source bytes after copying; the
/// is mlock'd as-is. Pushing afterwards is forbidden. /// allocation itself is then released normally when the Vec is dropped.
pub fn from_vec(v: Vec<u8>) -> Self { pub fn from_vec(mut v: Vec<u8>) -> Self {
let cap = v.capacity(); let len = v.len();
let inner = Zeroizing::new(v);
let lock = if cap > 0 {
region::lock(inner.as_ptr(), cap).ok()
} else {
None
};
Self { Self {
_lock: lock, inner: ProtectedSecretVec::from(&mut v[..]),
inner, len,
capacity: cap,
} }
} }
pub fn push(&mut self, b: u8) -> io::Result<()> { pub fn push(&mut self, b: u8) -> io::Result<()> {
if self.inner.len() >= self.capacity { if self.len >= self.inner.len() {
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::InvalidInput, io::ErrorKind::InvalidInput,
"secret buffer full", "secret buffer full",
)); ));
} }
self.inner.push(b); {
let mut inner = self.inner.borrow_mut();
inner[self.len] = b;
}
self.len += 1;
Ok(()) Ok(())
} }
pub fn as_slice(&self) -> &[u8] { pub fn with_slice<R>(&self, f: impl FnOnce(&[u8]) -> R) -> R {
&self.inner let inner = self.inner.borrow();
f(&inner[..self.len])
} }
} }
impl PartialEq for SecretVec { impl PartialEq for SecretVec {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
// constant-time-ish: compare full slices, no early return on mismatch. // Constant-time-ish: length still leaks, but contents do not early-out.
let a = self.as_slice(); if self.len != other.len {
let b = other.as_slice();
if a.len() != b.len() {
return false; return false;
} }
let a = self.inner.borrow();
let b = other.inner.borrow();
let mut diff: u8 = 0; let mut diff: u8 = 0;
for (x, y) in a.iter().zip(b.iter()) { for (x, y) in a[..self.len].iter().zip(b[..other.len].iter()) {
diff |= x ^ y; diff |= x ^ y;
} }
diff == 0 diff == 0