fabrica

hanna/fabrica

2915 bytes
Raw
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! A string wrapper that never reveals itself when logged or serialized.
6
7use std::fmt;
8
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10
11/// The placeholder rendered in every non-authorised view of a secret.
12pub const REDACTED: &str = "<redacted>";
13
14/// A secret string (signing key, SMTP password, …).
15///
16/// The inner value is only reachable through [`Secret::expose`]. Both the
17/// [`fmt::Debug`] and [`Serialize`] implementations emit [`REDACTED`] instead of
18/// the real value, so a `Secret` can never leak through `tracing` output, a
19/// `Debug`-formatted [`crate::Config`], or `fabrica config show`.
20#[derive(Clone, PartialEq, Eq)]
21pub struct Secret(String);
22
23impl Secret {
24 /// Wrap a value as a secret.
25 #[must_use]
26 pub fn new(value: String) -> Self {
27 Self(value)
28 }
29
30 /// Borrow the underlying secret value.
31 ///
32 /// This is the single deliberate escape hatch; call it only where the raw
33 /// value is genuinely needed (e.g. signing a token or authenticating to an
34 /// SMTP server), never for display or logging.
35 #[must_use]
36 pub fn expose(&self) -> &str {
37 &self.0
38 }
39}
40
41impl fmt::Debug for Secret {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 f.write_str(REDACTED)
44 }
45}
46
47impl Serialize for Secret {
48 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
49 serializer.serialize_str(REDACTED)
50 }
51}
52
53impl<'de> Deserialize<'de> for Secret {
54 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
55 String::deserialize(deserializer).map(Self)
56 }
57}
58
59impl From<String> for Secret {
60 fn from(value: String) -> Self {
61 Self(value)
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 #![allow(clippy::unwrap_used, clippy::expect_used)]
68
69 use serde::{Deserialize, Serialize};
70
71 use super::*;
72
73 #[test]
74 fn debug_is_redacted() {
75 let s = Secret::new("hunter2".to_string());
76 assert_eq!(format!("{s:?}"), REDACTED);
77 assert!(!format!("{s:?}").contains("hunter2"));
78 }
79
80 #[derive(Serialize, Deserialize)]
81 struct Holder {
82 s: Secret,
83 }
84
85 #[test]
86 fn serialize_is_redacted_but_expose_is_not() {
87 let holder = Holder {
88 s: Secret::new("hunter2".to_string()),
89 };
90 let rendered = toml::to_string(&holder).unwrap();
91 assert!(rendered.contains(REDACTED));
92 assert!(!rendered.contains("hunter2"));
93 assert_eq!(holder.s.expose(), "hunter2");
94 }
95
96 #[test]
97 fn deserialize_reads_the_real_value() {
98 let holder: Holder = toml::from_str("s = \"real\"").unwrap();
99 assert_eq!(holder.s.expose(), "real");
100 }
101}