bitwarden_crypto/safe/
high_entropy_secret.rs1#[cfg(feature = "wasm")]
10use bitwarden_encoding::B64;
11use bitwarden_sensitive_value::{ExposeSensitive, Sensitive, SensitiveSlice};
12use rand::Rng;
13use thiserror::Error;
14#[cfg(feature = "wasm")]
15use wasm_bindgen::convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi};
16use zeroize::Zeroizing;
17
18const MIN_SECRET_LENGTH: usize = 16;
21
22pub trait HighEntropySecretSource {
34 fn provide_high_entropy_bytes(&self) -> SensitiveSlice<'_>;
36}
37
38#[derive(Clone)]
41pub struct HighEntropySecret {
42 secret: Zeroizing<Vec<u8>>,
43}
44
45impl HighEntropySecret {
46 pub fn from<T: HighEntropySecretSource>(secret: T) -> Self {
49 Self::from_internal(secret.provide_high_entropy_bytes().expose_owned())
52 }
53
54 pub(crate) fn from_internal(secret: &[u8]) -> Self {
58 Self {
59 secret: Zeroizing::new(secret.to_vec()),
60 }
61 }
62
63 pub fn make(desired_size: usize) -> Result<Self, HighEntropySecretError> {
68 if desired_size < MIN_SECRET_LENGTH {
69 return Err(HighEntropySecretError::TooShort);
70 }
71
72 let mut secret = Zeroizing::new(vec![0u8; desired_size]);
73 bitwarden_random::rng().fill_bytes(secret.as_mut_slice());
74 Ok(Self { secret })
75 }
76
77 #[allow(dead_code)]
81 pub(crate) fn as_bytes(&self) -> SensitiveSlice<'_> {
82 Sensitive::from(self.secret.as_slice())
83 }
84}
85
86impl std::fmt::Debug for HighEntropySecret {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 f.debug_struct("HighEntropySecret").finish()
90 }
91}
92
93#[derive(Debug, Error)]
95pub enum HighEntropySecretError {
96 #[error("Secret is too short")]
98 TooShort,
99}
100
101#[cfg(feature = "wasm")]
102#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
103const TS_CUSTOM_TYPES: &'static str = r#"
104export type HighEntropySecret = Tagged<String, "HighEntropySecret">;
105"#;
106
107#[cfg(feature = "wasm")]
108impl wasm_bindgen::describe::WasmDescribe for HighEntropySecret {
109 fn describe() {
110 <String as wasm_bindgen::describe::WasmDescribe>::describe();
111 }
112}
113
114#[cfg(feature = "wasm")]
115impl FromWasmAbi for HighEntropySecret {
116 type Abi = <String as FromWasmAbi>::Abi;
117
118 unsafe fn from_abi(abi: Self::Abi) -> Self {
119 use wasm_bindgen::UnwrapThrowExt;
120 let string = unsafe { String::from_abi(abi) };
121 let b64 = B64::try_from(string).unwrap_throw();
122 HighEntropySecret::from_internal(b64.as_bytes())
123 }
124}
125
126#[cfg(feature = "wasm")]
127impl OptionFromWasmAbi for HighEntropySecret {
128 fn is_none(abi: &Self::Abi) -> bool {
129 <String as OptionFromWasmAbi>::is_none(abi)
130 }
131}
132
133#[cfg(feature = "wasm")]
134impl IntoWasmAbi for HighEntropySecret {
135 type Abi = <String as IntoWasmAbi>::Abi;
136
137 fn into_abi(self) -> Self::Abi {
138 let string: String = B64::from(self.secret.as_slice()).to_string();
139 string.into_abi()
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use bitwarden_sensitive_value::ExposeSensitive;
146
147 use super::*;
148
149 #[test]
150 fn test_debug_does_not_leak_secret() {
151 let secret = HighEntropySecret::make(32).unwrap();
152 let debug = format!("{secret:?}");
153 assert_eq!(debug, "HighEntropySecret");
154 }
155
156 #[test]
157 fn test_make_rejects_secret_below_minimum_length() {
158 for size in 0..MIN_SECRET_LENGTH {
159 assert!(
160 matches!(
161 HighEntropySecret::make(size),
162 Err(HighEntropySecretError::TooShort)
163 ),
164 "expected size {size} to be rejected as too short"
165 );
166 }
167 }
168
169 #[test]
170 fn test_make_accepts_secret_at_minimum_length() {
171 let secret = HighEntropySecret::make(MIN_SECRET_LENGTH).unwrap();
172 assert_eq!(secret.as_bytes().expose_owned().len(), MIN_SECRET_LENGTH);
173 }
174
175 #[test]
176 fn test_make_produces_secret_of_requested_length() {
177 for size in [16, 32, 64, 128] {
178 let secret = HighEntropySecret::make(size).unwrap();
179 assert_eq!(secret.as_bytes().expose_owned().len(), size);
180 }
181 }
182
183 #[test]
184 fn test_make_produces_distinct_secrets() {
185 let first = HighEntropySecret::make(32).unwrap();
186 let second = HighEntropySecret::make(32).unwrap();
187 assert_ne!(
189 first.as_bytes().expose_owned(),
190 second.as_bytes().expose_owned()
191 );
192 }
193
194 #[test]
195 fn test_from_bytes_preserves_secret() {
196 let bytes = vec![7u8; 32];
197 let secret = HighEntropySecret::from_internal(bytes.as_slice());
198 assert_eq!(secret.as_bytes().expose_owned(), bytes.as_slice());
199 }
200
201 #[test]
202 fn test_from_bytes_accepts_secret_at_minimum_length() {
203 let secret = HighEntropySecret::from_internal(vec![1u8; MIN_SECRET_LENGTH].as_slice());
204 assert_eq!(secret.as_bytes().expose_owned().len(), MIN_SECRET_LENGTH);
205 }
206
207 #[test]
208 fn test_clone_preserves_secret_bytes() {
209 let secret = HighEntropySecret::make(32).unwrap();
210 let cloned = secret.clone();
211 assert_eq!(
212 secret.as_bytes().expose_owned(),
213 cloned.as_bytes().expose_owned()
214 );
215 }
216}