// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! A string wrapper that never reveals itself when logged or serialized. use std::fmt; use serde::{Deserialize, Deserializer, Serialize, Serializer}; /// The placeholder rendered in every non-authorised view of a secret. pub const REDACTED: &str = ""; /// A secret string (signing key, SMTP password, …). /// /// The inner value is only reachable through [`Secret::expose`]. Both the /// [`fmt::Debug`] and [`Serialize`] implementations emit [`REDACTED`] instead of /// the real value, so a `Secret` can never leak through `tracing` output, a /// `Debug`-formatted [`crate::Config`], or `fabrica config show`. #[derive(Clone, PartialEq, Eq)] pub struct Secret(String); impl Secret { /// Wrap a value as a secret. #[must_use] pub fn new(value: String) -> Self { Self(value) } /// Borrow the underlying secret value. /// /// This is the single deliberate escape hatch; call it only where the raw /// value is genuinely needed (e.g. signing a token or authenticating to an /// SMTP server), never for display or logging. #[must_use] pub fn expose(&self) -> &str { &self.0 } } impl fmt::Debug for Secret { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(REDACTED) } } impl Serialize for Secret { fn serialize(&self, serializer: S) -> Result { serializer.serialize_str(REDACTED) } } impl<'de> Deserialize<'de> for Secret { fn deserialize>(deserializer: D) -> Result { String::deserialize(deserializer).map(Self) } } impl From for Secret { fn from(value: String) -> Self { Self(value) } } #[cfg(test)] mod tests { #![allow(clippy::unwrap_used, clippy::expect_used)] use serde::{Deserialize, Serialize}; use super::*; #[test] fn debug_is_redacted() { let s = Secret::new("hunter2".to_string()); assert_eq!(format!("{s:?}"), REDACTED); assert!(!format!("{s:?}").contains("hunter2")); } #[derive(Serialize, Deserialize)] struct Holder { s: Secret, } #[test] fn serialize_is_redacted_but_expose_is_not() { let holder = Holder { s: Secret::new("hunter2".to_string()), }; let rendered = toml::to_string(&holder).unwrap(); assert!(rendered.contains(REDACTED)); assert!(!rendered.contains("hunter2")); assert_eq!(holder.s.expose(), "hunter2"); } #[test] fn deserialize_reads_the_real_value() { let holder: Holder = toml::from_str("s = \"real\"").unwrap(); assert_eq!(holder.s.expose(), "real"); } }