radicle_ci_broker/
sensitive.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
//! 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::{de, Deserialize, Deserializer, Serialize, Serializer};

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_yml::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_yml::from_str("secret: foo")?;
        assert_eq!(s.secret.data, "foo");
        Ok(())
    }
}