Skip to main content

bitwarden_crypto/safe/
secret_protected_key_envelope.rs

1//! Secret protected key envelope is a cryptographic building block that allows sealing a symmetric
2//! key with a high-entropy secret (a random URL-fragment secret, a derived key, random bytes, etc.)
3//! of arbitrary length.
4//!
5//! It is implemented by using a cheap KDF (HKDF-SHA256) combined with symmetric key encryption
6//! (AES-256-GCM). Unlike the [crate::safe::PasswordProtectedKeyEnvelope], which protects a
7//! low-entropy secret (password, PIN) and therefore uses a hard KDF (Argon2ID, PBKDF2) to slow down
8//! brute-forcing, this envelope assumes the secret is high-entropy and not brute-forceable, so a
9//! cheap KDF is sufficient. The cheap KDF also natively accepts input material of arbitrary length.
10//!
11//! For the consumer, the output is an opaque blob that can be later unsealed with the same secret.
12//! The KDF salt is contained in the envelope, and does not need to be provided for unsealing.
13//!
14//! Internally, the envelope is a CoseEncrypt object that uses the standardized COSE "Direct Key
15//! with KDF" construction with the `direct+HKDF-SHA-256` recipient algorithm, as described in
16//! [RFC 9053 §6.1.2](https://datatracker.ietf.org/doc/html/rfc9053#name-direct-key-with-kdf). The
17//! random HKDF salt is placed in the single recipient's unprotected headers (the standardized
18//! `salt` header parameter), and the secret is used as the input keying material. The output from
19//! the KDF - "envelope key", is used directly as the content-encryption key that wraps the
20//! symmetric key sealed by the envelope.
21//!
22//! Note: AES-GCM is used here since the CEK is locally derived, so there is no nonce re-use
23//! problem.
24
25use std::{num::TryFromIntError, str::FromStr};
26
27use bitwarden_encoding::{B64, FromStrVisitor};
28use bitwarden_sensitive_value::ExposeSensitive;
29use ciborium::Value;
30use coset::{CborSerializable, CoseError, Header, HeaderBuilder, iana};
31use rand::Rng;
32use serde::{Deserialize, Serialize};
33use thiserror::Error;
34#[cfg(feature = "wasm")]
35use wasm_bindgen::convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi};
36
37use crate::{
38    ContentFormat, EncodedSymmetricKey, KeySlotIds, KeyStoreContext, SymmetricCryptoKey,
39    cose::{
40        ContentNamespace, CoseExtractError, SafeObjectNamespace, extract_bytes,
41        symmetric::{
42            CoseAlgorithmPolicy, CoseContentEncryptionAlgorithm, decrypt_cose, encrypt_cose,
43        },
44    },
45    keys::KeyId,
46    safe::{
47        DecodeSealedKeyError, HighEntropySecret, decode_sealed_symmetric_key, extract_key_id,
48        extract_single_recipient,
49        helpers::{debug_fmt, set_safe_namespaces, validate_safe_namespaces},
50        set_contained_key_id,
51    },
52};
53
54/// The recipient algorithm used by the envelope: `direct+HKDF-SHA-256`
55/// (<https://datatracker.ietf.org/doc/html/rfc9053#name-direct-key-with-kdf>).
56const HKDF_ALGORITHM: coset::iana::Algorithm = iana::Algorithm::Direct_HKDF_SHA_256;
57/// The standardized COSE `salt` header algorithm parameter label (-20).
58const HKDF_SALT_LABEL: i64 = iana::HeaderAlgorithmParameter::Salt as i64;
59/// 32 matches the SHA-256 output size (HashLen), which is the RECOMMENDED salt size for HKDF:
60/// <https://datatracker.ietf.org/doc/html/rfc5869>
61const ENVELOPE_HKDF_SALT_SIZE: usize = 32;
62/// 32 is chosen to match the size of an AES-256-GCM key
63const ENVELOPE_HKDF_OUTPUT_KEY_SIZE: usize = 32;
64
65/// A secret-protected key envelope can seal a symmetric key, and protect it with a high-entropy
66/// secret of arbitrary length.
67///
68/// Unlike the [crate::safe::PasswordProtectedKeyEnvelope], which is meant for low-entropy secrets
69/// such as PINs and uses a compute-hard or memory-hard KDF, this envelope assumes the secret is
70/// high-entropy and thus uses a cheap KDF (HKDF). The KDF salt is stored in the envelope and does
71/// not have to be provided.
72///
73/// Internally, HKDF-SHA256 is used as the KDF and AES-256-GCM is used to encrypt the key.
74#[derive(Clone)]
75pub struct SecretProtectedKeyEnvelope {
76    cose_encrypt: coset::CoseEncrypt,
77}
78
79impl SecretProtectedKeyEnvelope {
80    /// Seals a symmetric key with a [`HighEntropySecret`], using a random salt.
81    ///
82    /// The secret is guaranteed to be high-entropy by the [`HighEntropySecret`] type, which the
83    /// cheap KDF used here relies on, since it cannot defend a low-entropy secret against
84    /// brute-forcing.
85    ///
86    /// This should never fail, except for memory allocation error, when running the KDF.
87    pub fn seal<Ids: KeySlotIds>(
88        key_to_seal: Ids::Symmetric,
89        secret: &HighEntropySecret,
90        namespace: SecretProtectedKeyEnvelopeNamespace,
91        ctx: &KeyStoreContext<Ids>,
92    ) -> Result<Self, SecretProtectedKeyEnvelopeError> {
93        let key_ref = ctx
94            .get_symmetric_key(key_to_seal)
95            .map_err(|_| SecretProtectedKeyEnvelopeError::KeyMissing)?;
96        Self::seal_ref(key_ref, secret, namespace)
97    }
98
99    /// Seals a key reference with a secret. This function is not public since callers are expected
100    /// to only work with key store references.
101    fn seal_ref(
102        key_to_seal: &SymmetricCryptoKey,
103        secret: &HighEntropySecret,
104        namespace: SecretProtectedKeyEnvelopeNamespace,
105    ) -> Result<Self, SecretProtectedKeyEnvelopeError> {
106        Self::seal_ref_with_settings(key_to_seal, secret, &HkdfSettings::new(), namespace)
107    }
108
109    /// Seals a key reference with a secret and custom provided settings. This function is not
110    /// public since callers are expected to only work with key store references.
111    fn seal_ref_with_settings(
112        key_to_seal: &SymmetricCryptoKey,
113        secret: &HighEntropySecret,
114        hkdf_settings: &HkdfSettings,
115        namespace: SecretProtectedKeyEnvelopeNamespace,
116    ) -> Result<Self, SecretProtectedKeyEnvelopeError> {
117        // The envelope key is directly derived from the KDF and used as the key to encrypt the key
118        // that should be sealed. The secret is guaranteed to be high-entropy by its type, which the
119        // cheap KDF relies on for brute-force resistance. The KDF context binds the derived key to
120        // the content-encryption algorithm, which is also declared in the protected header below.
121        // EXPOSE: derive_cek needs the raw secret bytes to feed the KDF; the bytes never leave this
122        // crate and are not logged.
123        // This envelope only uses AES-256-GCM; the KDF context is bound to its IANA algorithm id.
124        let cek = derive_cek(
125            hkdf_settings,
126            secret.as_bytes().expose_owned(),
127            iana::Algorithm::A256GCM,
128        )?;
129
130        let (content_format, key_to_seal_bytes) = match key_to_seal.to_encoded_raw() {
131            EncodedSymmetricKey::BitwardenLegacyKey(key_bytes) => {
132                (ContentFormat::BitwardenLegacyKey, key_bytes.to_vec())
133            }
134            EncodedSymmetricKey::CoseKey(key_bytes) => (ContentFormat::CoseKey, key_bytes.to_vec()),
135        };
136
137        let protected_header = {
138            let mut header = HeaderBuilder::from(content_format).build();
139            set_contained_key_id(&mut header, key_to_seal.key_id());
140            set_safe_namespaces(
141                &mut header,
142                SafeObjectNamespace::SecretProtectedKeyEnvelope,
143                namespace,
144            );
145            header
146        };
147
148        // The message is constructed by placing the KDF settings in a recipient struct's
149        // unprotected headers. The envelope key derived above is the content-encryption key, and
150        // the content-encryption algorithm is declared in the protected header by `encrypt_cose`.
151        let builder = coset::CoseEncryptBuilder::new().add_recipient(
152            coset::CoseRecipientBuilder::new()
153                .unprotected(hkdf_settings.into())
154                .build(),
155        );
156        // `cek` is the fixed-size key produced by `derive_cek`, so the length check inside
157        // `encrypt_cose` never trips here.
158        let cose_encrypt = encrypt_cose(
159            CoseContentEncryptionAlgorithm::Aes256Gcm,
160            builder,
161            protected_header,
162            &key_to_seal_bytes,
163            &cek,
164        )
165        .map_err(|_| SecretProtectedKeyEnvelopeError::Kdf)?;
166
167        Ok(SecretProtectedKeyEnvelope { cose_encrypt })
168    }
169
170    /// Unseals a symmetric key from the secret-protected envelope, and stores it in the key store
171    /// context.
172    pub fn unseal<Ids: KeySlotIds>(
173        &self,
174        secret: &HighEntropySecret,
175        namespace: SecretProtectedKeyEnvelopeNamespace,
176        ctx: &mut KeyStoreContext<Ids>,
177    ) -> Result<Ids::Symmetric, SecretProtectedKeyEnvelopeError> {
178        let key = self.unseal_ref(secret, namespace)?;
179        Ok(ctx.add_local_symmetric_key(key))
180    }
181
182    fn unseal_ref(
183        &self,
184        secret: &HighEntropySecret,
185        content_namespace: SecretProtectedKeyEnvelopeNamespace,
186    ) -> Result<SymmetricCryptoKey, SecretProtectedKeyEnvelopeError> {
187        // There must be exactly one recipient in the COSE Encrypt object, which contains the KDF
188        // parameters.
189        let recipient = extract_single_recipient(&self.cose_encrypt).map_err(|_| {
190            SecretProtectedKeyEnvelopeError::Parsing("Invalid number of recipients".to_string())
191        })?;
192
193        if recipient.unprotected.alg
194            != Some(coset::RegisteredLabelWithPrivate::Assigned(HKDF_ALGORITHM))
195        {
196            return Err(SecretProtectedKeyEnvelopeError::Parsing(
197                "Unknown or unsupported KDF algorithm".to_string(),
198            ));
199        }
200
201        validate_safe_namespaces(
202            &self.cose_encrypt.protected.header,
203            SafeObjectNamespace::SecretProtectedKeyEnvelope,
204            content_namespace,
205        )
206        .map_err(|_| SecretProtectedKeyEnvelopeError::InvalidNamespace)?;
207
208        let kdf_settings: HkdfSettings = (&recipient.unprotected).try_into().map_err(|_| {
209            SecretProtectedKeyEnvelopeError::Parsing(
210                "Invalid or missing KDF parameters".to_string(),
211            )
212        })?;
213        // The KDF context binds the derived key to the content-encryption algorithm declared in the
214        // protected header (RFC 9053 §6.1.2). `decrypt_cose` separately validates that this matches
215        // the cipher, and the header is authenticated as AEAD associated data.
216        let content_alg = content_encryption_algorithm(&self.cose_encrypt.protected.header)?;
217        // EXPOSE: derive_cek needs the raw secret bytes to feed the KDF; the bytes never leave this
218        // crate and are not logged.
219        let cek = derive_cek(&kdf_settings, secret.as_bytes().expose_owned(), content_alg)?;
220
221        // If decryption fails, the envelope-key is incorrect and thus the secret is incorrect
222        // since the KDF salt is guaranteed to be correct. The envelope always declares its
223        // content-encryption algorithm in the protected header, so no decryption fallback is
224        // needed.
225        let key_bytes = decrypt_cose(
226            &self.cose_encrypt,
227            CoseAlgorithmPolicy::RequireProtectedHeaderAlgorithm,
228            &cek,
229        )
230        .map_err(|_| SecretProtectedKeyEnvelopeError::WrongSecret)?;
231
232        decode_sealed_symmetric_key(&self.cose_encrypt.protected.header, key_bytes).map_err(|e| {
233            match e {
234                DecodeSealedKeyError::InvalidContentFormat => {
235                    SecretProtectedKeyEnvelopeError::Parsing("Invalid content format".to_string())
236                }
237                DecodeSealedKeyError::UnsupportedContentFormat => {
238                    SecretProtectedKeyEnvelopeError::Parsing(
239                        "Unknown or unsupported content format".to_string(),
240                    )
241                }
242                DecodeSealedKeyError::InvalidKey => {
243                    SecretProtectedKeyEnvelopeError::Parsing("Failed to decode key".to_string())
244                }
245            }
246        })
247    }
248
249    /// Re-seals the key with a new salt, and a new secret
250    pub fn reseal(
251        &self,
252        secret: &HighEntropySecret,
253        new_secret: &HighEntropySecret,
254        namespace: SecretProtectedKeyEnvelopeNamespace,
255    ) -> Result<Self, SecretProtectedKeyEnvelopeError> {
256        let unsealed = self.unseal_ref(secret, namespace)?;
257        Self::seal_ref(&unsealed, new_secret, namespace)
258    }
259
260    /// Get the key ID of the contained key, if the key ID is stored on the envelope headers.
261    /// Only COSE keys have a key ID, legacy keys do not.
262    pub fn contained_key_id(&self) -> Result<Option<KeyId>, SecretProtectedKeyEnvelopeError> {
263        extract_key_id(&self.cose_encrypt.protected.header)
264            .map_err(|_| SecretProtectedKeyEnvelopeError::Parsing("Invalid key id".to_string()))
265    }
266}
267
268impl From<&SecretProtectedKeyEnvelope> for Vec<u8> {
269    fn from(val: &SecretProtectedKeyEnvelope) -> Self {
270        val.cose_encrypt
271            .clone()
272            .to_vec()
273            .expect("Serialization to cose should not fail")
274    }
275}
276
277impl TryFrom<&Vec<u8>> for SecretProtectedKeyEnvelope {
278    type Error = CoseError;
279
280    fn try_from(value: &Vec<u8>) -> Result<Self, Self::Error> {
281        let cose_encrypt = coset::CoseEncrypt::from_slice(value)?;
282        Ok(SecretProtectedKeyEnvelope { cose_encrypt })
283    }
284}
285
286impl std::fmt::Debug for SecretProtectedKeyEnvelope {
287    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288        let mut s = f.debug_struct("SecretProtectedKeyEnvelope");
289
290        debug_fmt::<SecretProtectedKeyEnvelopeNamespace>(
291            &mut s,
292            &self.cose_encrypt.protected.header,
293        );
294
295        if let Ok(Some(key_id)) = self.contained_key_id() {
296            s.field("contained_key_id", &key_id);
297        }
298
299        s.finish()
300    }
301}
302
303impl FromStr for SecretProtectedKeyEnvelope {
304    type Err = SecretProtectedKeyEnvelopeError;
305
306    fn from_str(s: &str) -> Result<Self, Self::Err> {
307        let data = B64::try_from(s).map_err(|_| {
308            SecretProtectedKeyEnvelopeError::Parsing(
309                "Invalid SecretProtectedKeyEnvelope Base64 encoding".to_string(),
310            )
311        })?;
312        Self::try_from(&data.into_bytes()).map_err(|_| {
313            SecretProtectedKeyEnvelopeError::Parsing(
314                "Failed to parse SecretProtectedKeyEnvelope".to_string(),
315            )
316        })
317    }
318}
319
320impl From<SecretProtectedKeyEnvelope> for String {
321    fn from(val: SecretProtectedKeyEnvelope) -> Self {
322        let serialized: Vec<u8> = (&val).into();
323        B64::from(serialized).to_string()
324    }
325}
326
327impl<'de> Deserialize<'de> for SecretProtectedKeyEnvelope {
328    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
329    where
330        D: serde::Deserializer<'de>,
331    {
332        deserializer.deserialize_str(FromStrVisitor::new())
333    }
334}
335
336impl Serialize for SecretProtectedKeyEnvelope {
337    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
338    where
339        S: serde::Serializer,
340    {
341        let serialized: Vec<u8> = self.into();
342        serializer.serialize_str(&B64::from(serialized).to_string())
343    }
344}
345
346/// Raw HKDF settings. The salt is a fixed size, randomly generated value. Unlike a memory-hard KDF,
347/// HKDF has no difficulty parameters to tune, since the input secret is assumed to be high-entropy.
348struct HkdfSettings {
349    alg: iana::Algorithm,
350    salt: [u8; ENVELOPE_HKDF_SALT_SIZE],
351}
352
353impl HkdfSettings {
354    /// Creates HKDF settings with a freshly generated random salt.
355    fn new() -> Self {
356        Self {
357            alg: HKDF_ALGORITHM,
358            salt: make_salt(),
359        }
360    }
361}
362
363impl From<&HkdfSettings> for Header {
364    fn from(settings: &HkdfSettings) -> Header {
365        HeaderBuilder::new()
366            .value(HKDF_SALT_LABEL, Value::from(settings.salt.to_vec()))
367            .algorithm(settings.alg)
368            .build()
369    }
370}
371
372impl TryInto<HkdfSettings> for &Header {
373    type Error = SecretProtectedKeyEnvelopeError;
374
375    fn try_into(self) -> Result<HkdfSettings, SecretProtectedKeyEnvelopeError> {
376        Ok(HkdfSettings {
377            alg: match self.alg {
378                Some(coset::RegisteredLabelWithPrivate::Assigned(alg)) => alg,
379                _ => {
380                    return Err(SecretProtectedKeyEnvelopeError::Parsing(
381                        "Missing KDF algorithm".to_string(),
382                    ));
383                }
384            },
385            salt: extract_bytes(self, HKDF_SALT_LABEL, "salt")?
386                .try_into()
387                .map_err(|_| {
388                    SecretProtectedKeyEnvelopeError::Parsing("Invalid HKDF salt".to_string())
389                })?,
390        })
391    }
392}
393
394fn make_salt() -> [u8; ENVELOPE_HKDF_SALT_SIZE] {
395    let mut salt = [0u8; ENVELOPE_HKDF_SALT_SIZE];
396    bitwarden_random::rng().fill_bytes(&mut salt);
397    salt
398}
399
400/// Builds the serialized `COSE_KDF_Context` used as the HKDF `info` parameter, as required by the
401/// COSE "Direct Key with KDF" construction
402/// (<https://datatracker.ietf.org/doc/html/rfc9053#name-context-information-structu>):
403///
404/// ```cddl
405/// COSE_KDF_Context = [
406///     AlgorithmID : int / tstr,
407///     PartyUInfo  : [ identity / nil, nonce / nil, other / nil ],
408///     PartyVInfo  : [ identity / nil, nonce / nil, other / nil ],
409///     SuppPubInfo : [ keyDataLength : uint, protected : empty_or_serialized_map ],
410/// ]
411/// ```
412///
413/// The context binds the derived key to the content-encryption algorithm (`alg`) and the requested
414/// key length, providing domain separation. `PartyUInfo`/`PartyVInfo` are empty and `protected` is
415/// the empty (zero-length) map, since no additional negotiated parameters apply.
416fn kdf_context_info(alg: iana::Algorithm) -> Result<Vec<u8>, SecretProtectedKeyEnvelopeError> {
417    let empty_party_info = || Value::Array(vec![Value::Null, Value::Null, Value::Null]);
418    let context = Value::Array(vec![
419        // AlgorithmID: the content-encryption algorithm the derived key is used for. This is the
420        // algorithm declared in the protected header of the message (e.g. AES-256-GCM).
421        Value::Integer((alg as i64).into()),
422        empty_party_info(),
423        empty_party_info(),
424        Value::Array(vec![
425            // keyDataLength is expressed in bits.
426            Value::Integer((ENVELOPE_HKDF_OUTPUT_KEY_SIZE as u64 * 8).into()),
427            // An empty protected header is the zero-length byte string.
428            Value::Bytes(vec![]),
429        ]),
430    ]);
431
432    let mut info = Vec::new();
433    ciborium::into_writer(&context, &mut info).map_err(|_| SecretProtectedKeyEnvelopeError::Kdf)?;
434    Ok(info)
435}
436
437/// Reads the content-encryption algorithm declared in the protected header. This value is used as
438/// the `AlgorithmID` of the `COSE_KDF_Context`, binding the derived key to the message's declared
439/// algorithm (RFC 9053 §6.1.2).
440fn content_encryption_algorithm(
441    header: &Header,
442) -> Result<iana::Algorithm, SecretProtectedKeyEnvelopeError> {
443    match header.alg {
444        Some(coset::RegisteredLabelWithPrivate::Assigned(alg)) => Ok(alg),
445        _ => Err(SecretProtectedKeyEnvelopeError::Parsing(
446            "Missing or unsupported content encryption algorithm".to_string(),
447        )),
448    }
449}
450
451fn derive_cek(
452    hkdf_settings: &HkdfSettings,
453    secret: &[u8],
454    alg: iana::Algorithm,
455) -> Result<[u8; ENVELOPE_HKDF_OUTPUT_KEY_SIZE], SecretProtectedKeyEnvelopeError> {
456    // COSE "Direct Key with KDF" (RFC 9053 §6.1.2) using `direct+HKDF-SHA-256`: the secret is the
457    // input keying material, the random salt is the HKDF-Extract salt, and the serialized
458    // COSE_KDF_Context is the HKDF-Expand info. Full HKDF (extract + expand) is used since the
459    // secret can be of arbitrary length and is not required to be a uniformly random key.
460    let info = kdf_context_info(alg)?;
461    let hkdf = hkdf::Hkdf::<sha2::Sha256>::new(Some(&hkdf_settings.salt), secret);
462    let mut key = [0u8; ENVELOPE_HKDF_OUTPUT_KEY_SIZE];
463    hkdf.expand(&info, &mut key)
464        .map_err(|_| SecretProtectedKeyEnvelopeError::Kdf)?;
465    Ok(key)
466}
467
468/// Errors that can occur when sealing or unsealing a key with the `SecretProtectedKeyEnvelope`.
469#[derive(Debug, Error)]
470pub enum SecretProtectedKeyEnvelopeError {
471    /// The secret provided is incorrect or the envelope was tampered with
472    #[error("Wrong secret")]
473    WrongSecret,
474    /// The envelope could not be parsed correctly, or the KDF parameters are invalid
475    #[error("Parsing error {0}")]
476    Parsing(String),
477    /// The KDF failed to derive a key, possibly due to invalid parameters
478    #[error("Kdf error")]
479    Kdf,
480    /// There is no key for the provided key id in the key store
481    #[error("Key missing error")]
482    KeyMissing,
483    /// The key store could not be written to, for example due to being read-only
484    #[error("Could not write to key store")]
485    KeyStore,
486    /// The namespace provided in the envelope does not match any known namespaces, or is invalid
487    #[error("Invalid namespace")]
488    InvalidNamespace,
489}
490
491impl From<CoseExtractError> for SecretProtectedKeyEnvelopeError {
492    fn from(err: CoseExtractError) -> Self {
493        let CoseExtractError::MissingValue(label) = err;
494        SecretProtectedKeyEnvelopeError::Parsing(format!("Missing value for {}", label))
495    }
496}
497
498impl From<TryFromIntError> for SecretProtectedKeyEnvelopeError {
499    fn from(err: TryFromIntError) -> Self {
500        SecretProtectedKeyEnvelopeError::Parsing(format!("Invalid integer: {}", err))
501    }
502}
503
504#[cfg(feature = "wasm")]
505#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
506const TS_CUSTOM_TYPES: &'static str = r#"
507export type SecretProtectedKeyEnvelope = Tagged<string, "SecretProtectedKeyEnvelope">;
508"#;
509
510#[cfg(feature = "wasm")]
511impl wasm_bindgen::describe::WasmDescribe for SecretProtectedKeyEnvelope {
512    fn describe() {
513        <String as wasm_bindgen::describe::WasmDescribe>::describe();
514    }
515}
516
517#[cfg(feature = "wasm")]
518impl FromWasmAbi for SecretProtectedKeyEnvelope {
519    type Abi = <String as FromWasmAbi>::Abi;
520
521    unsafe fn from_abi(abi: Self::Abi) -> Self {
522        use wasm_bindgen::UnwrapThrowExt;
523        let string = unsafe { String::from_abi(abi) };
524        SecretProtectedKeyEnvelope::from_str(&string).unwrap_throw()
525    }
526}
527
528#[cfg(feature = "wasm")]
529impl OptionFromWasmAbi for SecretProtectedKeyEnvelope {
530    fn is_none(abi: &Self::Abi) -> bool {
531        <String as OptionFromWasmAbi>::is_none(abi)
532    }
533}
534
535#[cfg(feature = "wasm")]
536impl IntoWasmAbi for SecretProtectedKeyEnvelope {
537    type Abi = <String as IntoWasmAbi>::Abi;
538
539    fn into_abi(self) -> Self::Abi {
540        let string: String = self.into();
541        string.into_abi()
542    }
543}
544
545#[cfg(feature = "wasm")]
546impl TryFrom<wasm_bindgen::JsValue> for SecretProtectedKeyEnvelope {
547    type Error = SecretProtectedKeyEnvelopeError;
548
549    fn try_from(value: wasm_bindgen::JsValue) -> Result<Self, Self::Error> {
550        let string = value.as_string().ok_or_else(|| {
551            SecretProtectedKeyEnvelopeError::Parsing(
552                "SecretProtectedKeyEnvelope JsValue is not a string".to_string(),
553            )
554        })?;
555        SecretProtectedKeyEnvelope::from_str(&string)
556    }
557}
558
559/// The content-layer separation namespace for secret protected key envelopes.
560#[derive(Debug, Clone, Copy, PartialEq, Eq)]
561pub enum SecretProtectedKeyEnvelopeNamespace {
562    /// Organization member invite links. The high-entropy secret is the random invite secret
563    /// carried in the invite link, and the sealed key is the invite key.
564    OrganizationInvite = 1,
565    /// Bitwarden Desktop biometric (Windows Hello) unlock. The high-entropy secret is a PRF derived
566    /// from the Windows Hello signing credential, and the sealed key is the user key.
567    DesktopBiometricUnlock = 2,
568    /// Used for threading the open-organization-invite data through the registration with email
569    /// verification process. Is generated per registration email.  
570    RegistrationOpenOrgInvite = 3,
571    /// This namespace is only used in tests
572    #[cfg(test)]
573    ExampleNamespace = -1,
574    /// This namespace is only used in tests
575    #[cfg(test)]
576    ExampleNamespace2 = -2,
577}
578
579impl SecretProtectedKeyEnvelopeNamespace {
580    /// Returns the numeric value of the namespace.
581    fn as_i64(&self) -> i64 {
582        *self as i64
583    }
584}
585
586impl TryFrom<i128> for SecretProtectedKeyEnvelopeNamespace {
587    type Error = SecretProtectedKeyEnvelopeError;
588
589    fn try_from(value: i128) -> Result<Self, Self::Error> {
590        match value {
591            1 => Ok(SecretProtectedKeyEnvelopeNamespace::OrganizationInvite),
592            2 => Ok(SecretProtectedKeyEnvelopeNamespace::DesktopBiometricUnlock),
593            3 => Ok(SecretProtectedKeyEnvelopeNamespace::RegistrationOpenOrgInvite),
594            #[cfg(test)]
595            -1 => Ok(SecretProtectedKeyEnvelopeNamespace::ExampleNamespace),
596            #[cfg(test)]
597            -2 => Ok(SecretProtectedKeyEnvelopeNamespace::ExampleNamespace2),
598            _ => Err(SecretProtectedKeyEnvelopeError::InvalidNamespace),
599        }
600    }
601}
602
603impl TryFrom<i64> for SecretProtectedKeyEnvelopeNamespace {
604    type Error = SecretProtectedKeyEnvelopeError;
605
606    fn try_from(value: i64) -> Result<Self, Self::Error> {
607        Self::try_from(i128::from(value))
608    }
609}
610
611impl From<SecretProtectedKeyEnvelopeNamespace> for i128 {
612    fn from(val: SecretProtectedKeyEnvelopeNamespace) -> Self {
613        val.as_i64().into()
614    }
615}
616
617impl ContentNamespace for SecretProtectedKeyEnvelopeNamespace {}
618
619#[cfg(test)]
620mod tests {
621    use super::*;
622    use crate::{KeyStore, SymmetricKeyAlgorithm, traits::tests::TestIds};
623
624    // A fixed, high-entropy (random) 32-byte secret. The envelope only accepts high-entropy
625    // secrets, so the test vectors must be sealed with random bytes rather than ASCII text.
626    const TESTVECTOR_SECRET_BYTES: &[u8] = &[
627        174, 83, 45, 9, 235, 3, 186, 62, 199, 125, 198, 108, 129, 205, 24, 21, 174, 148, 88, 80,
628        10, 238, 169, 66, 75, 202, 41, 201, 186, 244, 169, 67,
629    ];
630
631    fn testvector_secret() -> HighEntropySecret {
632        HighEntropySecret::from_internal(TESTVECTOR_SECRET_BYTES)
633    }
634
635    // Test vectors below are generated by sealing a key with `TESTVECTOR_SECRET_BYTES` and
636    // capturing the encoded key + the serialized envelope bytes (see the password-protected
637    // envelope for the same approach).
638    const TEST_UNSEALED_COSEKEY_ENCODED: &[u8] = &[
639        165, 1, 4, 2, 80, 214, 124, 137, 200, 1, 180, 227, 27, 77, 48, 119, 198, 210, 9, 149, 144,
640        3, 58, 0, 1, 17, 111, 4, 132, 3, 4, 5, 6, 32, 88, 32, 111, 105, 200, 46, 142, 185, 114,
641        127, 136, 152, 153, 40, 8, 62, 120, 184, 252, 175, 210, 2, 245, 237, 175, 195, 73, 211,
642        136, 23, 217, 203, 35, 10, 1,
643    ];
644    const TESTVECTOR_COSEKEY_ENVELOPE: &[u8] = &[
645        132, 88, 40, 165, 1, 3, 3, 24, 101, 58, 0, 1, 21, 92, 80, 214, 124, 137, 200, 1, 180, 227,
646        27, 77, 48, 119, 198, 210, 9, 149, 144, 58, 0, 1, 56, 129, 6, 58, 0, 1, 56, 128, 32, 161,
647        5, 76, 155, 157, 246, 33, 115, 165, 158, 222, 125, 222, 199, 188, 88, 84, 132, 235, 37,
648        236, 53, 75, 63, 253, 184, 134, 147, 83, 103, 87, 56, 81, 69, 202, 114, 23, 82, 25, 163,
649        68, 36, 13, 104, 187, 54, 143, 167, 113, 63, 62, 88, 146, 50, 214, 209, 170, 6, 235, 122,
650        44, 129, 149, 67, 213, 112, 112, 55, 51, 183, 165, 61, 168, 174, 215, 147, 110, 133, 164,
651        198, 29, 177, 84, 20, 203, 8, 0, 211, 218, 226, 62, 121, 51, 129, 230, 248, 66, 170, 83,
652        106, 109, 129, 131, 64, 162, 1, 41, 51, 88, 32, 123, 254, 226, 185, 81, 106, 88, 73, 109,
653        191, 241, 1, 143, 230, 179, 47, 36, 100, 235, 131, 4, 180, 12, 96, 125, 91, 184, 5, 175,
654        125, 188, 16, 246,
655    ];
656    const TEST_UNSEALED_LEGACYKEY_ENCODED: &[u8] = &[
657        23, 37, 64, 225, 53, 59, 143, 179, 18, 121, 128, 120, 86, 134, 93, 166, 214, 151, 210, 46,
658        240, 216, 69, 249, 247, 222, 110, 100, 185, 38, 173, 84, 202, 107, 132, 251, 144, 245, 105,
659        244, 220, 93, 212, 227, 98, 208, 173, 122, 245, 78, 244, 106, 174, 124, 109, 91, 53, 119,
660        96, 182, 45, 174, 206, 131,
661    ];
662    const TESTVECTOR_LEGACYKEY_ENVELOPE: &[u8] = &[
663        132, 88, 52, 164, 1, 3, 3, 120, 34, 97, 112, 112, 108, 105, 99, 97, 116, 105, 111, 110, 47,
664        120, 46, 98, 105, 116, 119, 97, 114, 100, 101, 110, 46, 108, 101, 103, 97, 99, 121, 45,
665        107, 101, 121, 58, 0, 1, 56, 129, 6, 58, 0, 1, 56, 128, 32, 161, 5, 76, 20, 11, 52, 107,
666        155, 203, 125, 143, 165, 38, 59, 135, 88, 80, 84, 46, 227, 50, 142, 191, 103, 207, 31, 192,
667        201, 215, 163, 102, 18, 93, 181, 247, 229, 12, 166, 221, 143, 98, 86, 74, 138, 12, 165, 1,
668        206, 101, 240, 222, 51, 239, 216, 4, 85, 61, 212, 62, 44, 29, 1, 184, 4, 191, 189, 248,
669        174, 159, 11, 133, 205, 19, 22, 28, 148, 138, 238, 136, 253, 173, 250, 69, 186, 232, 91,
670        222, 238, 9, 175, 178, 214, 27, 120, 254, 212, 110, 129, 131, 64, 162, 1, 41, 51, 88, 32,
671        222, 10, 249, 242, 57, 196, 223, 240, 234, 177, 19, 72, 201, 32, 1, 129, 46, 6, 76, 38,
672        149, 151, 217, 94, 84, 67, 50, 107, 103, 74, 88, 72, 246,
673    ];
674
675    #[test]
676    fn test_registration_open_org_invite_namespace_maps_to_expected_discriminant() {
677        // The wire discriminant is load-bearing once shipped; a regression here would silently
678        // invalidate every envelope sealed by production callers.
679        assert_eq!(
680            SecretProtectedKeyEnvelopeNamespace::try_from(3i128).unwrap(),
681            SecretProtectedKeyEnvelopeNamespace::RegistrationOpenOrgInvite
682        );
683        assert_eq!(
684            i128::from(SecretProtectedKeyEnvelopeNamespace::RegistrationOpenOrgInvite),
685            3
686        );
687    }
688
689    #[test]
690    fn test_registration_open_org_invite_rejects_cross_namespace_unseal() {
691        // An envelope sealed under the new production namespace must not unseal under any
692        // other production namespace (here, the sibling production
693        // `DesktopBiometricUnlock`). Mirrors the sibling test in `data_envelope.rs` for the
694        // new variant.
695        let key_store = KeyStore::<TestIds>::default();
696        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
697        let test_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
698        let secret = testvector_secret();
699
700        let envelope = SecretProtectedKeyEnvelope::seal(
701            test_key,
702            &secret,
703            SecretProtectedKeyEnvelopeNamespace::RegistrationOpenOrgInvite,
704            &ctx,
705        )
706        .expect("seal");
707
708        assert!(matches!(
709            envelope.unseal(
710                &secret,
711                SecretProtectedKeyEnvelopeNamespace::DesktopBiometricUnlock,
712                &mut ctx,
713            ),
714            Err(SecretProtectedKeyEnvelopeError::InvalidNamespace)
715        ));
716
717        // Correct namespace still succeeds.
718        let _ = envelope
719            .unseal(
720                &secret,
721                SecretProtectedKeyEnvelopeNamespace::RegistrationOpenOrgInvite,
722                &mut ctx,
723            )
724            .expect("unseal under correct namespace succeeds");
725    }
726
727    #[test]
728    #[ignore = "Manual test to verify debug format"]
729    fn test_debug() {
730        let key = SymmetricCryptoKey::make_xchacha20_poly1305_key();
731        let envelope = SecretProtectedKeyEnvelope::seal_ref(
732            &key,
733            &testvector_secret(),
734            SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
735        )
736        .unwrap();
737        println!("{:?}", envelope);
738    }
739
740    #[test]
741    fn test_testvector_cosekey() {
742        let key_store = KeyStore::<TestIds>::default();
743        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
744        let envelope = SecretProtectedKeyEnvelope::try_from(&TESTVECTOR_COSEKEY_ENVELOPE.to_vec())
745            .expect("Key envelope should be valid");
746        let key = envelope
747            .unseal(
748                &testvector_secret(),
749                SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
750                &mut ctx,
751            )
752            .expect("Unsealing should succeed");
753        let unsealed_key = ctx
754            .get_symmetric_key(key)
755            .expect("Key should exist in the key store");
756        assert_eq!(
757            unsealed_key.to_encoded().to_vec(),
758            TEST_UNSEALED_COSEKEY_ENCODED
759        );
760    }
761
762    #[test]
763    fn test_testvector_legacykey() {
764        let key_store = KeyStore::<TestIds>::default();
765        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
766        let envelope =
767            SecretProtectedKeyEnvelope::try_from(&TESTVECTOR_LEGACYKEY_ENVELOPE.to_vec())
768                .expect("Key envelope should be valid");
769        let key = envelope
770            .unseal(
771                &testvector_secret(),
772                SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
773                &mut ctx,
774            )
775            .expect("Unsealing should succeed");
776        let unsealed_key = ctx
777            .get_symmetric_key(key)
778            .expect("Key should exist in the key store");
779        assert_eq!(
780            unsealed_key.to_encoded().to_vec(),
781            TEST_UNSEALED_LEGACYKEY_ENCODED
782        );
783    }
784
785    #[test]
786    fn test_make_envelope() {
787        let key_store = KeyStore::<TestIds>::default();
788        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
789        let test_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
790
791        let secret = testvector_secret();
792
793        // Seal the key with a secret
794        let envelope = SecretProtectedKeyEnvelope::seal(
795            test_key,
796            &secret,
797            SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
798            &ctx,
799        )
800        .unwrap();
801        let serialized: Vec<u8> = (&envelope).into();
802
803        // Unseal the key from the envelope
804        let deserialized: SecretProtectedKeyEnvelope =
805            SecretProtectedKeyEnvelope::try_from(&serialized).unwrap();
806        let key = deserialized
807            .unseal(
808                &secret,
809                SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
810                &mut ctx,
811            )
812            .unwrap();
813
814        // Verify that the unsealed key matches the original key
815        let unsealed_key = ctx
816            .get_symmetric_key(key)
817            .expect("Key should exist in the key store");
818
819        let key_before_sealing = ctx
820            .get_symmetric_key(test_key)
821            .expect("Key should exist in the key store");
822
823        assert_eq!(unsealed_key, key_before_sealing);
824    }
825
826    #[test]
827    fn test_make_envelope_legacy_key() {
828        let key_store = KeyStore::<TestIds>::default();
829        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
830        let test_key = ctx.generate_symmetric_key();
831
832        let secret = testvector_secret();
833
834        // Seal the key with a secret
835        let envelope = SecretProtectedKeyEnvelope::seal(
836            test_key,
837            &secret,
838            SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
839            &ctx,
840        )
841        .unwrap();
842        let serialized: Vec<u8> = (&envelope).into();
843
844        // Unseal the key from the envelope
845        let deserialized: SecretProtectedKeyEnvelope =
846            SecretProtectedKeyEnvelope::try_from(&serialized).unwrap();
847        let key = deserialized
848            .unseal(
849                &secret,
850                SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
851                &mut ctx,
852            )
853            .unwrap();
854
855        // Verify that the unsealed key matches the original key
856        let unsealed_key = ctx
857            .get_symmetric_key(key)
858            .expect("Key should exist in the key store");
859
860        let key_before_sealing = ctx
861            .get_symmetric_key(test_key)
862            .expect("Key should exist in the key store");
863
864        assert_eq!(unsealed_key, key_before_sealing);
865    }
866
867    #[test]
868    fn test_reseal_envelope() {
869        let key = SymmetricCryptoKey::make_xchacha20_poly1305_key();
870        let secret = testvector_secret();
871        let new_secret = HighEntropySecret::make(32).unwrap();
872
873        // Seal the key with a secret
874        let envelope: SecretProtectedKeyEnvelope = SecretProtectedKeyEnvelope::seal_ref(
875            &key,
876            &secret,
877            SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
878        )
879        .expect("Sealing should work");
880
881        // Reseal
882        let envelope = envelope
883            .reseal(
884                &secret,
885                &new_secret,
886                SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
887            )
888            .expect("Resealing should work");
889        let unsealed = envelope
890            .unseal_ref(
891                &new_secret,
892                SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
893            )
894            .expect("Unsealing should work");
895
896        // Verify that the unsealed key matches the original key
897        assert_eq!(unsealed, key);
898    }
899
900    #[test]
901    fn test_wrong_secret() {
902        let key_store = KeyStore::<TestIds>::default();
903        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
904        let test_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
905
906        let secret = testvector_secret();
907        let wrong_secret = HighEntropySecret::make(32).unwrap();
908
909        // Seal the key with a secret
910        let envelope = SecretProtectedKeyEnvelope::seal(
911            test_key,
912            &secret,
913            SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
914            &ctx,
915        )
916        .unwrap();
917
918        // Attempt to unseal with the wrong secret
919        let deserialized: SecretProtectedKeyEnvelope =
920            SecretProtectedKeyEnvelope::try_from(&(&envelope).into()).unwrap();
921        assert!(matches!(
922            deserialized.unseal(
923                &wrong_secret,
924                SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
925                &mut ctx
926            ),
927            Err(SecretProtectedKeyEnvelopeError::WrongSecret)
928        ));
929    }
930
931    #[test]
932    fn test_wrong_safe_namespace() {
933        let key_store = KeyStore::<TestIds>::default();
934        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
935        let test_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
936        let secret = testvector_secret();
937
938        let mut envelope = SecretProtectedKeyEnvelope::seal(
939            test_key,
940            &secret,
941            SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
942            &ctx,
943        )
944        .expect("Seal works");
945
946        if let Some((_, value)) = envelope
947            .cose_encrypt
948            .protected
949            .header
950            .rest
951            .iter_mut()
952            .find(|(label, _)| {
953                matches!(label, coset::Label::Int(label_value) if *label_value == crate::cose::SAFE_OBJECT_NAMESPACE)
954            })
955        {
956            *value = Value::Integer((SafeObjectNamespace::DataEnvelope as i64).into());
957        }
958
959        let deserialized: SecretProtectedKeyEnvelope =
960            SecretProtectedKeyEnvelope::try_from(&(&envelope).into())
961                .expect("Envelope should be valid");
962
963        assert!(matches!(
964            deserialized.unseal(
965                &secret,
966                SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
967                &mut ctx,
968            ),
969            Err(SecretProtectedKeyEnvelopeError::InvalidNamespace)
970        ));
971    }
972
973    #[test]
974    fn test_key_id() {
975        let key_store = KeyStore::<TestIds>::default();
976        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
977        let test_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
978        let key_id = ctx.get_symmetric_key(test_key).unwrap().key_id().unwrap();
979
980        let secret = testvector_secret();
981
982        // Seal the key with a secret
983        let envelope = SecretProtectedKeyEnvelope::seal(
984            test_key,
985            &secret,
986            SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
987            &ctx,
988        )
989        .unwrap();
990        let contained_key_id = envelope.contained_key_id().unwrap();
991        assert_eq!(Some(key_id), contained_key_id);
992    }
993
994    #[test]
995    fn test_no_key_id() {
996        let key_store = KeyStore::<TestIds>::default();
997        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
998        let test_key = ctx.generate_symmetric_key();
999
1000        let secret = testvector_secret();
1001
1002        // Seal the key with a secret
1003        let envelope = SecretProtectedKeyEnvelope::seal(
1004            test_key,
1005            &secret,
1006            SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
1007            &ctx,
1008        )
1009        .unwrap();
1010        let contained_key_id = envelope.contained_key_id().unwrap();
1011        assert_eq!(None, contained_key_id);
1012    }
1013}