Skip to main content

bitwarden_crypto/safe/
high_entropy_secret.rs

1//! A high-entropy secret is a wrapper around secret bytes that are guaranteed to be high-entropy,
2//! and therefore safe to use as input keying material for a cheap KDF (such as the one used by the
3//! [crate::safe::SecretProtectedKeyEnvelope]).
4//!
5//! Examples of high-entropy secrets are a random URL-fragment secret, a derived key, or random
6//! bytes. They are unlike low-entropy secrets such as PINs or passwords, which can be brute-forced
7//! and therefore require a memory- or compute-hard KDF.
8
9#[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
18/// Minimum accepted secret length in bytes. 16 bytes = 128 bits of headroom for a uniformly
19/// random secret, matching the security level a cheap KDF assumes.
20const MIN_SECRET_LENGTH: usize = 16;
21
22/// A high entropy secret, generated from a CSPRNG or derived from a high-entropy key. This MUST NOT
23/// be:
24/// - An UTF-8 or ASCII string, even if Base64-encoded or hex-encoded high-entropy bytes
25/// - A low-entropy secret
26/// - A static key
27///
28/// Approved sources are:
29/// - A PRF derived from windows-hello biometrics in Bitwarden-Desktop
30/// - A PRF output from a passkey with the hmac-secret extension
31/// - Key-connector-stored bytes
32/// - A high entropy secret generated with `HighEntropySecret::make()`
33pub trait HighEntropySecretSource {
34    /// Returns the secret bytes as a redacted [`SensitiveSlice`].
35    fn provide_high_entropy_bytes(&self) -> SensitiveSlice<'_>;
36}
37
38/// A secret that is guaranteed to be high-entropy, and therefore safe to use as input keying
39/// material for a cheap KDF.
40#[derive(Clone)]
41pub struct HighEntropySecret {
42    secret: Zeroizing<Vec<u8>>,
43}
44
45impl HighEntropySecret {
46    /// Constructs a `HighEntropySecret` from any [`HighEntropySecretSource`], whose implementation
47    /// guarantees the provided bytes are high-entropy.
48    pub fn from<T: HighEntropySecretSource>(secret: T) -> Self {
49        // EXPOSE: This conversion is safe because the `HighEntropySecret` overrides the Debug
50        // implementation to never print the secret bytes.
51        Self::from_internal(secret.provide_high_entropy_bytes().expose_owned())
52    }
53
54    // Creates a new `HighEntropySecret` from the provided bytes, without validation. The caller is
55    // responsible for ensuring that the bytes are high-entropy and not derived from a low-entropy
56    // source!
57    pub(crate) fn from_internal(secret: &[u8]) -> Self {
58        Self {
59            secret: Zeroizing::new(secret.to_vec()),
60        }
61    }
62
63    /// Generates a new high-entropy secret of the desired size, using cryptographically secure
64    /// random bytes. The generated bytes are high-entropy by construction.
65    ///
66    /// `desired_size` should be at least 16 bytes to provide adequate security headroom.
67    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    /// Returns the secret bytes as a redacted [`SensitiveSlice`]. Not public since callers should
78    /// not handle the raw secret material directly.
79    // Consumed by in-crate KDF/envelope code that is not yet wired up on this branch.
80    #[allow(dead_code)]
81    pub(crate) fn as_bytes(&self) -> SensitiveSlice<'_> {
82        Sensitive::from(self.secret.as_slice())
83    }
84}
85
86// Manually implemented so the secret material is never printed.
87impl 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/// Errors that can occur when constructing a [`HighEntropySecret`].
94#[derive(Debug, Error)]
95pub enum HighEntropySecretError {
96    /// The provided secret is too short to be used as a high-entropy secret.
97    #[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        // Two independently generated secrets are overwhelmingly unlikely to collide.
188        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}