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
114#[cfg(feature = "wasm")]
115const _: () = {
116    use wasm_bindgen::{
117        JsValue,
118        convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi, OptionIntoWasmAbi},
119        describe::WasmDescribe,
120        prelude::wasm_bindgen,
121    };
122
123    #[wasm_bindgen(typescript_custom_section)]
124    const TS_CUSTOM_TYPES: &'static str = r#"
125export type SensitiveString = Tagged<string, "SensitiveString">;
126"#;
127
128    impl WasmDescribe for SensitiveString {
129        fn describe() {
130            <String as WasmDescribe>::describe();
131        }
132    }
133
134    impl FromWasmAbi for SensitiveString {
135        type Abi = <String as FromWasmAbi>::Abi;
136
137        unsafe fn from_abi(abi: Self::Abi) -> Self {
138            let string = unsafe { String::from_abi(abi) };
139            SensitiveString::from(string)
140        }
141    }
142
143    impl OptionFromWasmAbi for SensitiveString {
144        fn is_none(abi: &Self::Abi) -> bool {
145            <String as OptionFromWasmAbi>::is_none(abi)
146        }
147    }
148
149    impl IntoWasmAbi for SensitiveString {
150        type Abi = <String as IntoWasmAbi>::Abi;
151
152        fn into_abi(self) -> Self::Abi {
153            self.expose_owned().into_abi()
154        }
155    }
156
157    impl OptionIntoWasmAbi for SensitiveString {
158        fn none() -> Self::Abi {
159            <String as OptionIntoWasmAbi>::none()
160        }
161    }
162
163    impl From<SensitiveString> for JsValue {
164        fn from(value: SensitiveString) -> Self {
165            JsValue::from(value.expose_owned())
166        }
167    }
168
169    impl TryFrom<JsValue> for SensitiveString {
170        type Error = &'static str;
171
172        fn try_from(value: JsValue) -> Result<Self, Self::Error> {
173            value
174                .as_string()
175                .map(SensitiveString::from)
176                .ok_or("SensitiveString JsValue is not a string")
177        }
178    }
179};
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn from_str_and_from_string_are_equivalent() {
187        assert_eq!(
188            SensitiveString::from("secret"),
189            SensitiveString::from("secret".to_string())
190        );
191    }
192
193    #[test]
194    fn partial_eq_compares_inner_values() {
195        assert_eq!(
196            SensitiveString::from("secret"),
197            SensitiveString::from("secret")
198        );
199        assert_ne!(
200            SensitiveString::from("secret"),
201            SensitiveString::from("other")
202        );
203    }
204
205    #[test]
206    fn expose_returns_inner_string() {
207        let sensitive = SensitiveString::from("secret");
208        assert_eq!(sensitive.expose(), "secret");
209        assert_eq!(sensitive.expose_owned(), "secret");
210    }
211
212    #[test]
213    fn as_bytes_borrows_utf8_bytes() {
214        let sensitive = SensitiveString::from("secret");
215        let bytes = sensitive.as_bytes();
216        assert_eq!(bytes.expose_owned(), b"secret");
217    }
218
219    #[cfg(not(feature = "dangerous-crypto-debug"))]
220    #[test]
221    fn debug_is_redacted() {
222        let sensitive = SensitiveString::from("secret");
223        assert_eq!(format!("{sensitive:?}"), "[REDACTED]");
224    }
225
226    #[cfg(not(feature = "dangerous-crypto-debug"))]
227    #[test]
228    fn display_is_redacted() {
229        let sensitive = SensitiveString::from("secret");
230        assert_eq!(format!("{sensitive}"), "[REDACTED]");
231    }
232
233    #[test]
234    fn serde_json_round_trips_transparently() {
235        let sensitive = SensitiveString::from("secret");
236
237        let serialized = serde_json::to_string(&sensitive).unwrap();
238        assert_eq!(serialized, "\"secret\"");
239
240        let deserialized: SensitiveString = serde_json::from_str(&serialized).unwrap();
241        assert_eq!(deserialized, sensitive);
242    }
243}