Skip to main content

tailscale_rest/
secret.rs

1//! A string that does not print itself.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7/// A credential.
8///
9/// The only reason this type exists is its [`fmt::Debug`] implementation:
10/// `Config`, `Credentials` and everything holding them derive `Debug`, and a
11/// derived `Debug` on a `String` field is how tokens end up in logs.
12///
13/// It is transparent to serde, so a model field holding one reads and writes
14/// the plain string the control plane sent (Q62). That is deliberate and is
15/// the narrow path: serialising a model is the tool result a caller asked for,
16/// and is the one place a minted secret is meant to travel. Printing it is the
17/// accident, and printing is what stays redacted.
18#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(transparent)]
20pub struct Secret(String);
21
22impl Secret {
23    pub fn new(value: impl Into<String>) -> Self {
24        Self(value.into())
25    }
26
27    /// The value itself. Every call site is a place to check.
28    pub fn expose(&self) -> &str {
29        &self.0
30    }
31
32    pub fn is_empty(&self) -> bool {
33        self.0.is_empty()
34    }
35}
36
37impl fmt::Debug for Secret {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        f.write_str("Secret([redacted])")
40    }
41}
42
43impl fmt::Display for Secret {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        f.write_str("[redacted]")
46    }
47}
48
49impl From<String> for Secret {
50    fn from(value: String) -> Self {
51        Self(value)
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn a_secret_never_prints_itself() {
61        let secret = Secret::new("tskey-api-example1CNTRL-secretpart");
62        assert!(!format!("{secret:?}").contains("secretpart"));
63        assert!(!format!("{secret}").contains("secretpart"));
64        assert!(!format!("{:?}", Some(secret.clone())).contains("secretpart"));
65        assert_eq!(secret.expose(), "tskey-api-example1CNTRL-secretpart");
66    }
67
68    #[test]
69    fn a_secret_is_the_plain_string_to_serde() {
70        let json = r#""tskey-api-example1CNTRL-secretpart""#;
71        let secret: Secret = serde_json::from_str(json).expect("a string is a secret");
72        assert_eq!(secret.expose(), "tskey-api-example1CNTRL-secretpart");
73        assert_eq!(serde_json::to_string(&secret).expect("it serialises"), json);
74    }
75}