bitwarden_crypto/safe/
high_entropy_secret.rs1use std::str::FromStr;
10
11use bitwarden_encoding::{B64, FromStrVisitor};
12use bitwarden_sensitive_value::{ExposeSensitive, Sensitive, SensitiveSlice};
13use rand::Rng;
14use serde::{Deserialize, Serialize};
15use thiserror::Error;
16#[cfg(feature = "wasm")]
17use wasm_bindgen::convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi};
18use zeroize::Zeroizing;
19
20const MIN_SECRET_LENGTH: usize = 16;
23
24pub trait HighEntropySecretSource {
36 fn provide_high_entropy_bytes(&self) -> SensitiveSlice<'_>;
38}
39
40#[derive(Clone)]
43pub struct HighEntropySecret {
44 secret: Zeroizing<Vec<u8>>,
45}
46
47impl HighEntropySecret {
48 pub fn from<T: HighEntropySecretSource>(secret: T) -> Self {
51 Self::from_internal(secret.provide_high_entropy_bytes().expose_owned())
54 }
55
56 pub(crate) fn from_internal(secret: &[u8]) -> Self {
60 Self {
61 secret: Zeroizing::new(secret.to_vec()),
62 }
63 }
64
65 pub fn make(desired_size: usize) -> Result<Self, HighEntropySecretError> {
70 if desired_size < MIN_SECRET_LENGTH {
71 return Err(HighEntropySecretError::TooShort);
72 }
73
74 let mut secret = Zeroizing::new(vec![0u8; desired_size]);
75 bitwarden_random::rng().fill_bytes(secret.as_mut_slice());
76 Ok(Self { secret })
77 }
78
79 #[allow(dead_code)]
83 pub(crate) fn as_bytes(&self) -> SensitiveSlice<'_> {
84 Sensitive::from(self.secret.as_slice())
85 }
86}
87
88impl FromStr for HighEntropySecret {
89 type Err = HighEntropySecretError;
90
91 fn from_str(s: &str) -> Result<Self, Self::Err> {
92 let bytes = B64::try_from(s).map_err(|_| HighEntropySecretError::Malformed)?;
93 if bytes.as_bytes().len() < MIN_SECRET_LENGTH {
94 return Err(HighEntropySecretError::TooShort);
95 }
96 Ok(Self::from_internal(bytes.as_bytes()))
97 }
98}
99
100impl From<HighEntropySecret> for String {
101 fn from(val: HighEntropySecret) -> Self {
102 B64::from(val.secret.as_slice()).to_string()
103 }
104}
105
106impl<'de> Deserialize<'de> for HighEntropySecret {
107 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
108 where
109 D: serde::Deserializer<'de>,
110 {
111 deserializer.deserialize_str(FromStrVisitor::new())
112 }
113}
114
115impl Serialize for HighEntropySecret {
116 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
117 where
118 S: serde::Serializer,
119 {
120 serializer.serialize_str(&B64::from(self.secret.as_slice()).to_string())
121 }
122}
123
124impl std::fmt::Debug for HighEntropySecret {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 f.debug_struct("HighEntropySecret").finish()
128 }
129}
130
131#[derive(Debug, Error)]
133pub enum HighEntropySecretError {
134 #[error("Secret is too short")]
136 TooShort,
137 #[error("Secret is not valid base64")]
139 Malformed,
140}
141
142#[cfg(feature = "wasm")]
143#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
144const TS_CUSTOM_TYPES: &'static str = r#"
145export type HighEntropySecret = Tagged<string, "HighEntropySecret">;
146"#;
147
148#[cfg(feature = "wasm")]
149impl wasm_bindgen::describe::WasmDescribe for HighEntropySecret {
150 fn describe() {
151 <String as wasm_bindgen::describe::WasmDescribe>::describe();
152 }
153}
154
155#[cfg(feature = "wasm")]
156impl FromWasmAbi for HighEntropySecret {
157 type Abi = <String as FromWasmAbi>::Abi;
158
159 unsafe fn from_abi(abi: Self::Abi) -> Self {
160 use wasm_bindgen::UnwrapThrowExt;
161 let string = unsafe { String::from_abi(abi) };
162 HighEntropySecret::from_str(&string).unwrap_throw()
163 }
164}
165
166#[cfg(feature = "wasm")]
167impl OptionFromWasmAbi for HighEntropySecret {
168 fn is_none(abi: &Self::Abi) -> bool {
169 <String as OptionFromWasmAbi>::is_none(abi)
170 }
171}
172
173#[cfg(feature = "wasm")]
174impl IntoWasmAbi for HighEntropySecret {
175 type Abi = <String as IntoWasmAbi>::Abi;
176
177 fn into_abi(self) -> Self::Abi {
178 let string: String = B64::from(self.secret.as_slice()).to_string();
179 string.into_abi()
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use bitwarden_sensitive_value::ExposeSensitive;
186
187 use super::*;
188
189 #[test]
190 fn test_debug_does_not_leak_secret() {
191 let secret = HighEntropySecret::make(32).unwrap();
192 let debug = format!("{secret:?}");
193 assert_eq!(debug, "HighEntropySecret");
194 }
195
196 #[test]
197 fn test_make_rejects_secret_below_minimum_length() {
198 for size in 0..MIN_SECRET_LENGTH {
199 assert!(
200 matches!(
201 HighEntropySecret::make(size),
202 Err(HighEntropySecretError::TooShort)
203 ),
204 "expected size {size} to be rejected as too short"
205 );
206 }
207 }
208
209 #[test]
210 fn test_make_accepts_secret_at_minimum_length() {
211 let secret = HighEntropySecret::make(MIN_SECRET_LENGTH).unwrap();
212 assert_eq!(secret.as_bytes().expose_owned().len(), MIN_SECRET_LENGTH);
213 }
214
215 #[test]
216 fn test_make_produces_secret_of_requested_length() {
217 for size in [16, 32, 64, 128] {
218 let secret = HighEntropySecret::make(size).unwrap();
219 assert_eq!(secret.as_bytes().expose_owned().len(), size);
220 }
221 }
222
223 #[test]
224 fn test_make_produces_distinct_secrets() {
225 let first = HighEntropySecret::make(32).unwrap();
226 let second = HighEntropySecret::make(32).unwrap();
227 assert_ne!(
229 first.as_bytes().expose_owned(),
230 second.as_bytes().expose_owned()
231 );
232 }
233
234 #[test]
235 fn test_from_bytes_preserves_secret() {
236 let bytes = vec![7u8; 32];
237 let secret = HighEntropySecret::from_internal(bytes.as_slice());
238 assert_eq!(secret.as_bytes().expose_owned(), bytes.as_slice());
239 }
240
241 #[test]
242 fn test_from_bytes_accepts_secret_at_minimum_length() {
243 let secret = HighEntropySecret::from_internal(vec![1u8; MIN_SECRET_LENGTH].as_slice());
244 assert_eq!(secret.as_bytes().expose_owned().len(), MIN_SECRET_LENGTH);
245 }
246
247 #[test]
248 fn test_clone_preserves_secret_bytes() {
249 let secret = HighEntropySecret::make(32).unwrap();
250 let cloned = secret.clone();
251 assert_eq!(
252 secret.as_bytes().expose_owned(),
253 cloned.as_bytes().expose_owned()
254 );
255 }
256
257 #[test]
258 fn test_base64_round_trip_preserves_bytes() {
259 let secret = HighEntropySecret::make(32).unwrap();
260 let bytes = secret.as_bytes().expose_owned().to_vec();
261 let encoded = String::from(secret);
262 let decoded: HighEntropySecret = encoded.parse().unwrap();
263 assert_eq!(decoded.as_bytes().expose_owned(), bytes.as_slice());
264 }
265
266 #[test]
267 fn test_from_str_rejects_malformed_input() {
268 assert!(matches!(
269 "!!!not-base64!!!".parse::<HighEntropySecret>(),
270 Err(HighEntropySecretError::Malformed)
271 ));
272 }
273
274 #[test]
275 fn test_from_str_rejects_below_minimum_length() {
276 for byte_len in 0..MIN_SECRET_LENGTH {
279 let bytes = vec![0u8; byte_len];
280 let encoded = B64::from(bytes.as_slice()).to_string();
281 assert!(
282 matches!(
283 encoded.parse::<HighEntropySecret>(),
284 Err(HighEntropySecretError::TooShort)
285 ),
286 "expected decoded length {byte_len} to be rejected as too short"
287 );
288 }
289 }
290
291 #[test]
292 fn test_from_str_accepts_input_at_minimum_length() {
293 let bytes = vec![7u8; MIN_SECRET_LENGTH];
294 let encoded = B64::from(bytes.as_slice()).to_string();
295 let secret: HighEntropySecret = encoded
296 .parse()
297 .expect("input at minimum length is accepted");
298 assert_eq!(secret.as_bytes().expose_owned().len(), MIN_SECRET_LENGTH);
299 }
300
301 #[test]
302 fn test_serde_json_round_trip_preserves_bytes() {
303 let secret = HighEntropySecret::make(32).unwrap();
306 let bytes = secret.as_bytes().expose_owned().to_vec();
307 let json = serde_json::to_string(&secret).expect("serialize");
308 let round_tripped: HighEntropySecret = serde_json::from_str(&json).expect("deserialize");
309 assert_eq!(round_tripped.as_bytes().expose_owned(), bytes.as_slice());
310 }
311}