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    /// Neutral placeholder so the public API and example are usable. Replace with a
563    /// product-specific variant when the first real consumer lands.
564    ExampleUse = 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    /// This namespace is only used in tests
569    #[cfg(test)]
570    ExampleNamespace = -1,
571    /// This namespace is only used in tests
572    #[cfg(test)]
573    ExampleNamespace2 = -2,
574}
575
576impl SecretProtectedKeyEnvelopeNamespace {
577    /// Returns the numeric value of the namespace.
578    fn as_i64(&self) -> i64 {
579        *self as i64
580    }
581}
582
583impl TryFrom<i128> for SecretProtectedKeyEnvelopeNamespace {
584    type Error = SecretProtectedKeyEnvelopeError;
585
586    fn try_from(value: i128) -> Result<Self, Self::Error> {
587        match value {
588            1 => Ok(SecretProtectedKeyEnvelopeNamespace::ExampleUse),
589            2 => Ok(SecretProtectedKeyEnvelopeNamespace::DesktopBiometricUnlock),
590            #[cfg(test)]
591            -1 => Ok(SecretProtectedKeyEnvelopeNamespace::ExampleNamespace),
592            #[cfg(test)]
593            -2 => Ok(SecretProtectedKeyEnvelopeNamespace::ExampleNamespace2),
594            _ => Err(SecretProtectedKeyEnvelopeError::InvalidNamespace),
595        }
596    }
597}
598
599impl TryFrom<i64> for SecretProtectedKeyEnvelopeNamespace {
600    type Error = SecretProtectedKeyEnvelopeError;
601
602    fn try_from(value: i64) -> Result<Self, Self::Error> {
603        Self::try_from(i128::from(value))
604    }
605}
606
607impl From<SecretProtectedKeyEnvelopeNamespace> for i128 {
608    fn from(val: SecretProtectedKeyEnvelopeNamespace) -> Self {
609        val.as_i64().into()
610    }
611}
612
613impl ContentNamespace for SecretProtectedKeyEnvelopeNamespace {}
614
615#[cfg(test)]
616mod tests {
617    use super::*;
618    use crate::{KeyStore, SymmetricKeyAlgorithm, traits::tests::TestIds};
619
620    // A fixed, high-entropy (random) 32-byte secret. The envelope only accepts high-entropy
621    // secrets, so the test vectors must be sealed with random bytes rather than ASCII text.
622    const TESTVECTOR_SECRET_BYTES: &[u8] = &[
623        174, 83, 45, 9, 235, 3, 186, 62, 199, 125, 198, 108, 129, 205, 24, 21, 174, 148, 88, 80,
624        10, 238, 169, 66, 75, 202, 41, 201, 186, 244, 169, 67,
625    ];
626
627    fn testvector_secret() -> HighEntropySecret {
628        HighEntropySecret::from_internal(TESTVECTOR_SECRET_BYTES)
629    }
630
631    // Test vectors below are generated by sealing a key with `TESTVECTOR_SECRET_BYTES` and
632    // capturing the encoded key + the serialized envelope bytes (see the password-protected
633    // envelope for the same approach).
634    const TEST_UNSEALED_COSEKEY_ENCODED: &[u8] = &[
635        165, 1, 4, 2, 80, 214, 124, 137, 200, 1, 180, 227, 27, 77, 48, 119, 198, 210, 9, 149, 144,
636        3, 58, 0, 1, 17, 111, 4, 132, 3, 4, 5, 6, 32, 88, 32, 111, 105, 200, 46, 142, 185, 114,
637        127, 136, 152, 153, 40, 8, 62, 120, 184, 252, 175, 210, 2, 245, 237, 175, 195, 73, 211,
638        136, 23, 217, 203, 35, 10, 1,
639    ];
640    const TESTVECTOR_COSEKEY_ENVELOPE: &[u8] = &[
641        132, 88, 40, 165, 1, 3, 3, 24, 101, 58, 0, 1, 21, 92, 80, 214, 124, 137, 200, 1, 180, 227,
642        27, 77, 48, 119, 198, 210, 9, 149, 144, 58, 0, 1, 56, 129, 6, 58, 0, 1, 56, 128, 32, 161,
643        5, 76, 155, 157, 246, 33, 115, 165, 158, 222, 125, 222, 199, 188, 88, 84, 132, 235, 37,
644        236, 53, 75, 63, 253, 184, 134, 147, 83, 103, 87, 56, 81, 69, 202, 114, 23, 82, 25, 163,
645        68, 36, 13, 104, 187, 54, 143, 167, 113, 63, 62, 88, 146, 50, 214, 209, 170, 6, 235, 122,
646        44, 129, 149, 67, 213, 112, 112, 55, 51, 183, 165, 61, 168, 174, 215, 147, 110, 133, 164,
647        198, 29, 177, 84, 20, 203, 8, 0, 211, 218, 226, 62, 121, 51, 129, 230, 248, 66, 170, 83,
648        106, 109, 129, 131, 64, 162, 1, 41, 51, 88, 32, 123, 254, 226, 185, 81, 106, 88, 73, 109,
649        191, 241, 1, 143, 230, 179, 47, 36, 100, 235, 131, 4, 180, 12, 96, 125, 91, 184, 5, 175,
650        125, 188, 16, 246,
651    ];
652    const TEST_UNSEALED_LEGACYKEY_ENCODED: &[u8] = &[
653        23, 37, 64, 225, 53, 59, 143, 179, 18, 121, 128, 120, 86, 134, 93, 166, 214, 151, 210, 46,
654        240, 216, 69, 249, 247, 222, 110, 100, 185, 38, 173, 84, 202, 107, 132, 251, 144, 245, 105,
655        244, 220, 93, 212, 227, 98, 208, 173, 122, 245, 78, 244, 106, 174, 124, 109, 91, 53, 119,
656        96, 182, 45, 174, 206, 131,
657    ];
658    const TESTVECTOR_LEGACYKEY_ENVELOPE: &[u8] = &[
659        132, 88, 52, 164, 1, 3, 3, 120, 34, 97, 112, 112, 108, 105, 99, 97, 116, 105, 111, 110, 47,
660        120, 46, 98, 105, 116, 119, 97, 114, 100, 101, 110, 46, 108, 101, 103, 97, 99, 121, 45,
661        107, 101, 121, 58, 0, 1, 56, 129, 6, 58, 0, 1, 56, 128, 32, 161, 5, 76, 20, 11, 52, 107,
662        155, 203, 125, 143, 165, 38, 59, 135, 88, 80, 84, 46, 227, 50, 142, 191, 103, 207, 31, 192,
663        201, 215, 163, 102, 18, 93, 181, 247, 229, 12, 166, 221, 143, 98, 86, 74, 138, 12, 165, 1,
664        206, 101, 240, 222, 51, 239, 216, 4, 85, 61, 212, 62, 44, 29, 1, 184, 4, 191, 189, 248,
665        174, 159, 11, 133, 205, 19, 22, 28, 148, 138, 238, 136, 253, 173, 250, 69, 186, 232, 91,
666        222, 238, 9, 175, 178, 214, 27, 120, 254, 212, 110, 129, 131, 64, 162, 1, 41, 51, 88, 32,
667        222, 10, 249, 242, 57, 196, 223, 240, 234, 177, 19, 72, 201, 32, 1, 129, 46, 6, 76, 38,
668        149, 151, 217, 94, 84, 67, 50, 107, 103, 74, 88, 72, 246,
669    ];
670
671    #[test]
672    #[ignore = "Manual test to verify debug format"]
673    fn test_debug() {
674        let key = SymmetricCryptoKey::make_xchacha20_poly1305_key();
675        let envelope = SecretProtectedKeyEnvelope::seal_ref(
676            &key,
677            &testvector_secret(),
678            SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
679        )
680        .unwrap();
681        println!("{:?}", envelope);
682    }
683
684    #[test]
685    fn test_testvector_cosekey() {
686        let key_store = KeyStore::<TestIds>::default();
687        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
688        let envelope = SecretProtectedKeyEnvelope::try_from(&TESTVECTOR_COSEKEY_ENVELOPE.to_vec())
689            .expect("Key envelope should be valid");
690        let key = envelope
691            .unseal(
692                &testvector_secret(),
693                SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
694                &mut ctx,
695            )
696            .expect("Unsealing should succeed");
697        let unsealed_key = ctx
698            .get_symmetric_key(key)
699            .expect("Key should exist in the key store");
700        assert_eq!(
701            unsealed_key.to_encoded().to_vec(),
702            TEST_UNSEALED_COSEKEY_ENCODED
703        );
704    }
705
706    #[test]
707    fn test_testvector_legacykey() {
708        let key_store = KeyStore::<TestIds>::default();
709        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
710        let envelope =
711            SecretProtectedKeyEnvelope::try_from(&TESTVECTOR_LEGACYKEY_ENVELOPE.to_vec())
712                .expect("Key envelope should be valid");
713        let key = envelope
714            .unseal(
715                &testvector_secret(),
716                SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
717                &mut ctx,
718            )
719            .expect("Unsealing should succeed");
720        let unsealed_key = ctx
721            .get_symmetric_key(key)
722            .expect("Key should exist in the key store");
723        assert_eq!(
724            unsealed_key.to_encoded().to_vec(),
725            TEST_UNSEALED_LEGACYKEY_ENCODED
726        );
727    }
728
729    #[test]
730    fn test_make_envelope() {
731        let key_store = KeyStore::<TestIds>::default();
732        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
733        let test_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
734
735        let secret = testvector_secret();
736
737        // Seal the key with a secret
738        let envelope = SecretProtectedKeyEnvelope::seal(
739            test_key,
740            &secret,
741            SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
742            &ctx,
743        )
744        .unwrap();
745        let serialized: Vec<u8> = (&envelope).into();
746
747        // Unseal the key from the envelope
748        let deserialized: SecretProtectedKeyEnvelope =
749            SecretProtectedKeyEnvelope::try_from(&serialized).unwrap();
750        let key = deserialized
751            .unseal(
752                &secret,
753                SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
754                &mut ctx,
755            )
756            .unwrap();
757
758        // Verify that the unsealed key matches the original key
759        let unsealed_key = ctx
760            .get_symmetric_key(key)
761            .expect("Key should exist in the key store");
762
763        let key_before_sealing = ctx
764            .get_symmetric_key(test_key)
765            .expect("Key should exist in the key store");
766
767        assert_eq!(unsealed_key, key_before_sealing);
768    }
769
770    #[test]
771    fn test_make_envelope_legacy_key() {
772        let key_store = KeyStore::<TestIds>::default();
773        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
774        let test_key = ctx.generate_symmetric_key();
775
776        let secret = testvector_secret();
777
778        // Seal the key with a secret
779        let envelope = SecretProtectedKeyEnvelope::seal(
780            test_key,
781            &secret,
782            SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
783            &ctx,
784        )
785        .unwrap();
786        let serialized: Vec<u8> = (&envelope).into();
787
788        // Unseal the key from the envelope
789        let deserialized: SecretProtectedKeyEnvelope =
790            SecretProtectedKeyEnvelope::try_from(&serialized).unwrap();
791        let key = deserialized
792            .unseal(
793                &secret,
794                SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
795                &mut ctx,
796            )
797            .unwrap();
798
799        // Verify that the unsealed key matches the original key
800        let unsealed_key = ctx
801            .get_symmetric_key(key)
802            .expect("Key should exist in the key store");
803
804        let key_before_sealing = ctx
805            .get_symmetric_key(test_key)
806            .expect("Key should exist in the key store");
807
808        assert_eq!(unsealed_key, key_before_sealing);
809    }
810
811    #[test]
812    fn test_reseal_envelope() {
813        let key = SymmetricCryptoKey::make_xchacha20_poly1305_key();
814        let secret = testvector_secret();
815        let new_secret = HighEntropySecret::make(32).unwrap();
816
817        // Seal the key with a secret
818        let envelope: SecretProtectedKeyEnvelope = SecretProtectedKeyEnvelope::seal_ref(
819            &key,
820            &secret,
821            SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
822        )
823        .expect("Sealing should work");
824
825        // Reseal
826        let envelope = envelope
827            .reseal(
828                &secret,
829                &new_secret,
830                SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
831            )
832            .expect("Resealing should work");
833        let unsealed = envelope
834            .unseal_ref(
835                &new_secret,
836                SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
837            )
838            .expect("Unsealing should work");
839
840        // Verify that the unsealed key matches the original key
841        assert_eq!(unsealed, key);
842    }
843
844    #[test]
845    fn test_wrong_secret() {
846        let key_store = KeyStore::<TestIds>::default();
847        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
848        let test_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
849
850        let secret = testvector_secret();
851        let wrong_secret = HighEntropySecret::make(32).unwrap();
852
853        // Seal the key with a secret
854        let envelope = SecretProtectedKeyEnvelope::seal(
855            test_key,
856            &secret,
857            SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
858            &ctx,
859        )
860        .unwrap();
861
862        // Attempt to unseal with the wrong secret
863        let deserialized: SecretProtectedKeyEnvelope =
864            SecretProtectedKeyEnvelope::try_from(&(&envelope).into()).unwrap();
865        assert!(matches!(
866            deserialized.unseal(
867                &wrong_secret,
868                SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
869                &mut ctx
870            ),
871            Err(SecretProtectedKeyEnvelopeError::WrongSecret)
872        ));
873    }
874
875    #[test]
876    fn test_wrong_safe_namespace() {
877        let key_store = KeyStore::<TestIds>::default();
878        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
879        let test_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
880        let secret = testvector_secret();
881
882        let mut envelope = SecretProtectedKeyEnvelope::seal(
883            test_key,
884            &secret,
885            SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
886            &ctx,
887        )
888        .expect("Seal works");
889
890        if let Some((_, value)) = envelope
891            .cose_encrypt
892            .protected
893            .header
894            .rest
895            .iter_mut()
896            .find(|(label, _)| {
897                matches!(label, coset::Label::Int(label_value) if *label_value == crate::cose::SAFE_OBJECT_NAMESPACE)
898            })
899        {
900            *value = Value::Integer((SafeObjectNamespace::DataEnvelope as i64).into());
901        }
902
903        let deserialized: SecretProtectedKeyEnvelope =
904            SecretProtectedKeyEnvelope::try_from(&(&envelope).into())
905                .expect("Envelope should be valid");
906
907        assert!(matches!(
908            deserialized.unseal(
909                &secret,
910                SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
911                &mut ctx,
912            ),
913            Err(SecretProtectedKeyEnvelopeError::InvalidNamespace)
914        ));
915    }
916
917    #[test]
918    fn test_key_id() {
919        let key_store = KeyStore::<TestIds>::default();
920        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
921        let test_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
922        let key_id = ctx.get_symmetric_key(test_key).unwrap().key_id().unwrap();
923
924        let secret = testvector_secret();
925
926        // Seal the key with a secret
927        let envelope = SecretProtectedKeyEnvelope::seal(
928            test_key,
929            &secret,
930            SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
931            &ctx,
932        )
933        .unwrap();
934        let contained_key_id = envelope.contained_key_id().unwrap();
935        assert_eq!(Some(key_id), contained_key_id);
936    }
937
938    #[test]
939    fn test_no_key_id() {
940        let key_store = KeyStore::<TestIds>::default();
941        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
942        let test_key = ctx.generate_symmetric_key();
943
944        let secret = testvector_secret();
945
946        // Seal the key with a secret
947        let envelope = SecretProtectedKeyEnvelope::seal(
948            test_key,
949            &secret,
950            SecretProtectedKeyEnvelopeNamespace::ExampleNamespace,
951            &ctx,
952        )
953        .unwrap();
954        let contained_key_id = envelope.contained_key_id().unwrap();
955        assert_eq!(None, contained_key_id);
956    }
957}