use std::fmt;
pub struct Secret {
bytes: Vec<u8>,
label: &'static str,
}
impl Secret {
pub fn new(label: &'static str, value: impl Into<String>) -> Self {
Self {
bytes: value.into().into_bytes(),
label,
}
}
pub fn from_env(label: &'static str, variable: &str) -> crate::Result<Self> {
match std::env::var(variable) {
Ok(value) if !value.trim().is_empty() => Ok(Self::new(label, value)),
_ => Err(crate::Error::Auth(format!("{variable} is not set"))),
}
}
pub fn expose(&self) -> &[u8] {
&self.bytes
}
pub fn expose_str(&self) -> std::result::Result<&str, std::str::Utf8Error> {
std::str::from_utf8(&self.bytes)
}
pub fn label(&self) -> &'static str {
self.label
}
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
}
impl fmt::Debug for Secret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Secret({})", self.label)
}
}
impl fmt::Display for Secret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{} redacted]", self.label)
}
}
impl Drop for Secret {
fn drop(&mut self) {
self.bytes.fill(0);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn debug_and_display_never_carry_the_value() {
let key = Secret::new("api-key", "sk-super-secret");
assert!(!format!("{key:?}").contains("secret"));
assert!(!format!("{key}").contains("secret"));
}
#[test]
fn an_unset_variable_reads_as_an_authentication_problem() {
let missing = Secret::from_env("api-key", "LLMR_TEST_DEFINITELY_UNSET");
assert!(matches!(missing, Err(crate::Error::Auth(_))));
}
}