Skip to main content

bitwarden_sensitive_value/
sensitive_string.rs

1use core::fmt;
2
3use crate::{Sensitive, SensitiveSlice, sensitive::ExposeSensitive};
4
5/// FFI-friendly concrete wrapper around a sensitive `String`. This is the type exposed across the
6/// UniFFI / WASM boundary; bindings see it as an opaque tagged string. `Debug` and `Display`
7/// delegate to the inner [`Sensitive`], so output is redacted unless `dangerous-crypto-debug`
8/// is enabled.
9pub struct SensitiveString(Sensitive<String>);
10
11impl From<&str> for Sensitive<String> {
12    fn from(value: &str) -> Self {
13        Self(value.to_string())
14    }
15}
16
17impl SensitiveString {
18    /// Borrow the secret as a zero-copy [`SensitiveSlice`] over its UTF-8 bytes. The returned
19    /// slice borrows from this value and stays wrapped, so the bytes are never exposed and the
20    /// borrow cannot outlive `self`.
21    pub fn as_bytes(&self) -> SensitiveSlice<'_> {
22        // EXPOSE: We borrow the inner string only to immediately re-wrap its bytes in another
23        // `Sensitive`, so the secret is never actually exposed to logging.
24        Sensitive::from(self.0.expose().as_bytes())
25    }
26}
27
28impl<T: fmt::Debug> fmt::Debug for Sensitive<T> {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        #[cfg(feature = "dangerous-crypto-debug")]
31        {
32            self.0.fmt(f)
33        }
34        #[cfg(not(feature = "dangerous-crypto-debug"))]
35        {
36            f.write_str("[REDACTED]")
37        }
38    }
39}
40
41impl<T: fmt::Display> fmt::Display for Sensitive<T> {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        #[cfg(feature = "dangerous-crypto-debug")]
44        {
45            self.0.fmt(f)
46        }
47        #[cfg(not(feature = "dangerous-crypto-debug"))]
48        {
49            f.write_str("[REDACTED]")
50        }
51    }
52}
53
54impl ExposeSensitive for SensitiveString {
55    type Exposed = String;
56
57    fn expose(&self) -> &Self::Exposed {
58        self.0.expose()
59    }
60
61    fn expose_owned(self) -> Self::Exposed {
62        self.0.expose_owned()
63    }
64}
65
66impl From<&str> for SensitiveString {
67    fn from(value: &str) -> Self {
68        Self(Sensitive::from(value.to_string()))
69    }
70}
71
72impl From<String> for SensitiveString {
73    fn from(value: String) -> Self {
74        Self(Sensitive::from(value))
75    }
76}
77
78impl PartialEq for SensitiveString {
79    fn eq(&self, other: &Self) -> bool {
80        self.0.eq(&other.0)
81    }
82}
83
84impl serde::Serialize for SensitiveString {
85    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
86        self.0.serialize(serializer)
87    }
88}
89
90impl<'de> serde::Deserialize<'de> for SensitiveString {
91    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
92        Ok(Self(Sensitive::<String>::deserialize(deserializer)?))
93    }
94}
95
96impl fmt::Debug for SensitiveString {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        self.0.fmt(f)
99    }
100}
101
102impl fmt::Display for SensitiveString {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        self.0.fmt(f)
105    }
106}
107
108#[cfg(feature = "uniffi")]
109uniffi::custom_type!(SensitiveString, String, {
110    try_lift: |val| Ok(SensitiveString::from(val)),
111    lower: |obj| obj.expose().to_string(),
112});
113
114bitwarden_ffi::impl_wire_string!(
115    SensitiveString,
116    parse = |wire: String| Ok::<_, std::convert::Infallible>(SensitiveString::from(wire)),
117    format = |value: SensitiveString| value.expose_owned(),
118);
119
120#[cfg(feature = "wasm")]
121const _: () = {
122    use wasm_bindgen::{JsValue, prelude::wasm_bindgen};
123
124    #[wasm_bindgen(typescript_custom_section)]
125    const TS_CUSTOM_TYPES: &'static str = r#"
126export type SensitiveString = Tagged<string, "SensitiveString">;
127"#;
128
129    impl From<SensitiveString> for JsValue {
130        fn from(value: SensitiveString) -> Self {
131            JsValue::from(value.expose_owned())
132        }
133    }
134
135    impl TryFrom<JsValue> for SensitiveString {
136        type Error = &'static str;
137
138        fn try_from(value: JsValue) -> Result<Self, Self::Error> {
139            value
140                .as_string()
141                .map(SensitiveString::from)
142                .ok_or("SensitiveString JsValue is not a string")
143        }
144    }
145};
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn from_str_and_from_string_are_equivalent() {
153        assert_eq!(
154            SensitiveString::from("secret"),
155            SensitiveString::from("secret".to_string())
156        );
157    }
158
159    #[test]
160    fn partial_eq_compares_inner_values() {
161        assert_eq!(
162            SensitiveString::from("secret"),
163            SensitiveString::from("secret")
164        );
165        assert_ne!(
166            SensitiveString::from("secret"),
167            SensitiveString::from("other")
168        );
169    }
170
171    #[test]
172    fn expose_returns_inner_string() {
173        let sensitive = SensitiveString::from("secret");
174        assert_eq!(sensitive.expose(), "secret");
175        assert_eq!(sensitive.expose_owned(), "secret");
176    }
177
178    #[test]
179    fn as_bytes_borrows_utf8_bytes() {
180        let sensitive = SensitiveString::from("secret");
181        let bytes = sensitive.as_bytes();
182        assert_eq!(bytes.expose_owned(), b"secret");
183    }
184
185    #[cfg(not(feature = "dangerous-crypto-debug"))]
186    #[test]
187    fn debug_is_redacted() {
188        let sensitive = SensitiveString::from("secret");
189        assert_eq!(format!("{sensitive:?}"), "[REDACTED]");
190    }
191
192    #[cfg(not(feature = "dangerous-crypto-debug"))]
193    #[test]
194    fn display_is_redacted() {
195        let sensitive = SensitiveString::from("secret");
196        assert_eq!(format!("{sensitive}"), "[REDACTED]");
197    }
198
199    #[test]
200    fn serde_json_round_trips_transparently() {
201        let sensitive = SensitiveString::from("secret");
202
203        let serialized = serde_json::to_string(&sensitive).unwrap();
204        assert_eq!(serialized, "\"secret\"");
205
206        let deserialized: SensitiveString = serde_json::from_str(&serialized).unwrap();
207        assert_eq!(deserialized, sensitive);
208    }
209}