use std::fmt;
use secrecy::ExposeSecret;
use secrecy::SecretSlice;
use secrecy::zeroize::Zeroizing;
use crate::MonosecretError;
use crate::Result;
pub struct SecretBytes(SecretSlice<u8>);
impl SecretBytes {
pub fn from_vec(value: Vec<u8>) -> Self {
if value.len() == value.capacity() {
return Self(value.into());
}
let value = Zeroizing::new(value);
Self(value.as_slice().to_vec().into())
}
pub fn from_slice(value: &[u8]) -> Self {
Self::from_vec(value.to_vec())
}
pub fn from_utf8(value: impl Into<String>) -> Self {
Self::from_vec(value.into().into_bytes())
}
pub fn expose_secret(&self) -> &[u8] {
self.0.expose_secret()
}
pub fn try_as_utf8(&self) -> Result<&str> {
std::str::from_utf8(self.expose_secret()).map_err(|_| {
MonosecretError::ProviderOperationFailed(
"secret value contains bytes that are not valid UTF-8".to_string(),
)
})
}
pub fn try_as_utf8_for(&self, name: &str) -> Result<&str> {
std::str::from_utf8(self.expose_secret()).map_err(|error| {
MonosecretError::SecretNotText {
name: name.to_string(),
reason: format!(
"it contains bytes that are not valid UTF-8 (first invalid byte at offset {}); \
declare `as_path = true` to receive a file path or resolve it as bytes",
error.valid_up_to()
),
}
})
}
pub fn try_as_env_value_for(&self, name: &str) -> Result<&std::ffi::OsStr> {
if self.expose_secret().contains(&0) {
return Err(MonosecretError::SecretNotText {
name: name.to_string(),
reason: "it contains a NUL byte, which cannot be passed in a process environment"
.to_string(),
});
}
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
Ok(std::ffi::OsStr::from_bytes(self.expose_secret()))
}
#[cfg(not(unix))]
{
self.try_as_utf8_for(name).map(std::ffi::OsStr::new)
}
}
pub fn try_as_env_value(&self) -> Result<&std::ffi::OsStr> {
if self.expose_secret().contains(&0) {
return Err(MonosecretError::ProviderOperationFailed(
"secret value contains a NUL byte and cannot be passed in a process environment"
.to_string(),
));
}
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
Ok(std::ffi::OsStr::from_bytes(self.expose_secret()))
}
#[cfg(not(unix))]
{
self.try_as_utf8().map(std::ffi::OsStr::new)
}
}
pub fn to_vec(&self) -> Vec<u8> {
self.expose_secret().to_vec()
}
}
impl Clone for SecretBytes {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl PartialEq for SecretBytes {
fn eq(&self, other: &Self) -> bool {
self.expose_secret() == other.expose_secret()
}
}
impl Eq for SecretBytes {}
impl fmt::Debug for SecretBytes {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("SecretBytes([REDACTED])")
}
}
impl From<Vec<u8>> for SecretBytes {
fn from(value: Vec<u8>) -> Self {
Self::from_vec(value)
}
}
impl From<&[u8]> for SecretBytes {
fn from(value: &[u8]) -> Self {
Self::from_slice(value)
}
}
impl From<String> for SecretBytes {
fn from(value: String) -> Self {
Self::from_vec(value.into_bytes())
}
}
impl From<&str> for SecretBytes {
fn from(value: &str) -> Self {
Self::from_utf8(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn preserves_arbitrary_bytes_and_redacts_debug() {
let value = SecretBytes::from_slice(&[0x00, 0xff, 0x80, 0x0a]);
assert_eq!(value.expose_secret(), &[0x00, 0xff, 0x80, 0x0a]);
assert_eq!(value.clone(), value);
assert_eq!(format!("{value:?}"), "SecretBytes([REDACTED])");
assert!(value.try_as_utf8().is_err());
}
#[test]
fn from_vec_keeps_bytes_of_a_buffer_with_spare_capacity() {
let mut spare = Vec::with_capacity(64);
spare.extend_from_slice(b"-----BEGIN KEY-----\n");
assert!(spare.capacity() > spare.len());
let value = SecretBytes::from_vec(spare);
assert_eq!(value.expose_secret(), b"-----BEGIN KEY-----\n");
assert_eq!(
SecretBytes::from_vec(Vec::with_capacity(8)).expose_secret(),
b""
);
}
#[test]
fn environment_values_reject_nul_without_exposing_bytes() {
let value = SecretBytes::from_slice(b"do-not-leak\0");
let error = value.try_as_env_value().unwrap_err().to_string();
assert!(error.contains("NUL"));
assert!(!error.contains("do-not-leak"));
}
#[cfg(unix)]
#[test]
fn unix_environment_values_preserve_non_utf8() {
use std::os::unix::ffi::OsStrExt;
let value = SecretBytes::from_slice(b"raw\xff\x80\n");
assert_eq!(
value.try_as_env_value().unwrap().as_bytes(),
value.expose_secret()
);
}
#[cfg(not(unix))]
#[test]
fn non_unix_environment_values_require_utf8() {
let value = SecretBytes::from_slice(b"raw\xff\x80");
assert!(value.try_as_env_value().is_err());
}
}