//! A data type for holding sensitive data to avoid accidentally
//! leaking it.
//!
//! Once a `Sensitive` value has been created, it contains a string
//! value. It can be created by de-serializing using `serde`.
//!
//! The sensitive data can be accessed with [`Sensitive::as_str`]. The
//! caller needs to be careful to not leak that.
//!
//! `Sensitive` value itself can't be printed (via the `Display`
//! trait), even in debug mode (`Debug` trait), or serialized with
//! `serde`. Instead, the value is replaced with the string
//! `<REDACTED>`.
//!
//! Note that this does not prevent the value from ending up in a core
//! dump.
use std::fmt;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
const PLACEHOLDER: &str = "<REDACTED>";
/// Hold a sensitive string, such as a password or an API token. The
/// value can be accessed ([`Sensitive::as_str`]), but won't be
/// printed, even in debug output, and won't be serialized by `serde`.
#[derive()]
pub struct Sensitive {
#[allow(dead_code)]
//#[serde(serialize_with = "serialize")]
data: String,
}
impl fmt::Display for Sensitive {
/// Serialize for normal output.
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "{PLACEHOLDER}")
}
}
impl fmt::Debug for Sensitive {
/// Serialize for debug output.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "{PLACEHOLDER}")
}
}
impl Serialize for Sensitive {
/// Serialize for `serde`.
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(PLACEHOLDER)
}
}
impl<'de> Deserialize<'de> for Sensitive {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(SensitiveVisitor)
}
}
struct SensitiveVisitor;
impl<'de> de::Visitor<'de> for SensitiveVisitor {
type Value = Sensitive;
fn expecting(&self, _formatter: &mut fmt::Formatter) -> fmt::Result {
Ok(())
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Sensitive { data: s.into() })
}
}
impl Sensitive {
#[cfg(test)]
fn new(data: &str) -> Self {
Self { data: data.into() }
}
/// Return the contained string in cleartext. Do not leak this string.
pub fn as_str(&self) -> &str {
&self.data
}
}
#[cfg(test)]
mod test_sensitive {
use super::*;
#[test]
fn displayed() {
let s = Sensitive::new("foo");
let output = format!("{s}");
assert!(!output.contains("foo"));
}
#[test]
fn debugged() {
let s = Sensitive::new("foo");
let output = format!("{s:?}");
assert!(!output.contains("foo"));
}
#[test]
fn ser() -> Result<(), Box<dyn std::error::Error>> {
let s = Sensitive::new("foo");
let output = serde_norway::to_string(&s)?;
println!("{output:#?}");
assert!(!output.contains("foo"));
Ok(())
}
#[test]
fn deser() -> Result<(), Box<dyn std::error::Error>> {
#[derive(Deserialize)]
struct Foo {
secret: Sensitive,
}
let s: Foo = serde_norway::from_str("secret: foo")?;
assert_eq!(s.secret.data, "foo");
Ok(())
}
}