Skip to main content

bitwarden_crypto/safe/
password_protected_key_envelope.rs

1//! Password protected key envelope is a cryptographic building block that allows sealing a
2//! symmetric key with a low entropy secret (password, PIN, etc.).
3//!
4//! It is implemented by using a KDF combined with secret key encryption. The KDF prevents
5//! brute-force by requiring work to be done to derive the key from the password. The algorithms
6//! depend on the key store's [`CipherSuite`]: the standard suite uses Argon2id + AES-256-GCM,
7//! while the FIPS suite uses the FIPS-approved PBKDF2 + AES-256-GCM.
8//!
9//! For the consumer, the output is an opaque blob that can be later unsealed with the same
10//! password. The KDF parameters and salt are contained in the envelope, and don't need to be
11//! provided for unsealing.
12//!
13//! Internally, the envelope is a CoseEncrypt object. The KDF parameters / salt are placed in the
14//! single recipient's unprotected headers. The output from the KDF - "envelope key", is used to
15//! wrap the symmetric key, that is sealed by the envelope.
16
17use std::{num::TryFromIntError, str::FromStr};
18
19use argon2::Params;
20use bitwarden_encoding::{B64, FromStrVisitor};
21use ciborium::{Value, value::Integer};
22use coset::{CborSerializable, CoseError, Header, HeaderBuilder};
23use rand::Rng;
24use serde::{Deserialize, Serialize};
25use thiserror::Error;
26#[cfg(feature = "wasm")]
27use wasm_bindgen::convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi};
28
29use crate::{
30    CipherSuite, ContentFormat, EncodedSymmetricKey, KeySlotIds, KeyStoreContext,
31    SymmetricCryptoKey,
32    cose::{
33        ALG_ARGON2ID13, ALG_PBKDF2_SHA256, ARGON2_ITERATIONS, ARGON2_MEMORY, ARGON2_PARALLELISM,
34        ARGON2_SALT, ContentNamespace, CoseExtractError, PBKDF2_ITERATIONS, PBKDF2_SALT,
35        SafeObjectNamespace, extract_bytes, extract_integer,
36        symmetric::{
37            CoseAlgorithmPolicy, CoseContentEncryptionAlgorithm, decrypt_cose, encrypt_cose,
38        },
39    },
40    keys::KeyId,
41    safe::{
42        DecodeSealedKeyError, decode_sealed_symmetric_key, extract_key_id,
43        helpers::{debug_fmt, set_safe_namespaces, validate_safe_namespaces},
44        set_contained_key_id,
45    },
46};
47
48/// 16 is the RECOMMENDED salt size for all applications:
49/// <https://datatracker.ietf.org/doc/rfc9106/>
50const ENVELOPE_ARGON2_SALT_SIZE: usize = 16;
51/// 32 is chosen to match the size of an XChaCha20-Poly1305 key
52const ENVELOPE_ARGON2_OUTPUT_KEY_SIZE: usize = 32;
53const ENVELOPE_PBKDF2_SALT_SIZE: usize = 16;
54
55/// A password-protected key envelope can seal a symmetric key, and protect it with a password. It
56/// does so by using a Key Derivation Function (KDF), to increase the difficulty of brute-forcing
57/// the password.
58///
59/// The KDF parameters such as iterations and salt are stored in the envelope and do not have to
60/// be provided.
61///
62/// The algorithms used depend on the key store's [`CipherSuite`]: the standard suite uses Argon2id
63/// as the KDF, while the FIPS suite uses PBKDF2. Both suites encrypt the key with AES-256-GCM; the
64/// KDF and content-encryption algorithms are recorded in the envelope, so unsealing does not need
65/// to know the suite up front.
66#[derive(Clone)]
67pub struct PasswordProtectedKeyEnvelope {
68    cose_encrypt: coset::CoseEncrypt,
69}
70
71impl PasswordProtectedKeyEnvelope {
72    /// Seals a symmetric key with a password, using the current default KDF parameters and a random
73    /// salt.
74    ///
75    /// This should never fail, except for memory allocation error, when running the KDF.
76    pub fn seal<Ids: KeySlotIds>(
77        key_to_seal: Ids::Symmetric,
78        password: &str,
79        namespace: PasswordProtectedKeyEnvelopeNamespace,
80        ctx: &KeyStoreContext<Ids>,
81    ) -> Result<Self, PasswordProtectedKeyEnvelopeError> {
82        let key_ref = ctx
83            .get_symmetric_key(key_to_seal)
84            .map_err(|_| PasswordProtectedKeyEnvelopeError::KeyMissing)?;
85        let kdf = suite_kdf(ctx.cipher_suite());
86        Self::seal_ref_with_settings(key_ref, password, &kdf, namespace)
87    }
88
89    /// Seals a key reference with a password, using the standard cipher suite (Argon2id +
90    /// AES-256-GCM). This function is not public since callers are expected to only work
91    /// with key store references.
92    #[cfg(test)]
93    fn seal_ref(
94        key_to_seal: &SymmetricCryptoKey,
95        password: &str,
96        namespace: PasswordProtectedKeyEnvelopeNamespace,
97    ) -> Result<Self, PasswordProtectedKeyEnvelopeError> {
98        let kdf = suite_kdf(CipherSuite::Standard);
99        Self::seal_ref_with_settings(key_to_seal, password, &kdf, namespace)
100    }
101
102    /// Seals a key reference with a password, KDF settings, and content-encryption algorithm. This
103    /// function is not public since callers are expected to only work with key store references,
104    /// and to not control the KDF difficulty where possible.
105    fn seal_ref_with_settings(
106        key_to_seal: &SymmetricCryptoKey,
107        password: &str,
108        kdf: &EnvelopeKdf,
109        namespace: PasswordProtectedKeyEnvelopeNamespace,
110    ) -> Result<Self, PasswordProtectedKeyEnvelopeError> {
111        // Cose does not yet have a standardized way to protect a key using a password.
112        // This implements content encryption using direct encryption with a KDF derived key,
113        // similar to "Direct Key with KDF" mentioned in the COSE spec. The KDF settings are
114        // placed in a single recipient struct.
115
116        // The envelope key is directly derived from the KDF and used as the key to encrypt the key
117        // that should be sealed.
118        let envelope_key =
119            derive_key(kdf, password).map_err(|_| PasswordProtectedKeyEnvelopeError::Kdf)?;
120
121        let (content_format, key_to_seal_bytes) = match key_to_seal.to_encoded_raw() {
122            EncodedSymmetricKey::BitwardenLegacyKey(key_bytes) => {
123                (ContentFormat::BitwardenLegacyKey, key_bytes.to_vec())
124            }
125            EncodedSymmetricKey::CoseKey(key_bytes) => (ContentFormat::CoseKey, key_bytes.to_vec()),
126        };
127
128        let protected_header = {
129            let mut header = HeaderBuilder::from(content_format).build();
130            set_contained_key_id(&mut header, key_to_seal.key_id());
131            set_safe_namespaces(
132                &mut header,
133                SafeObjectNamespace::PasswordProtectedKeyEnvelope,
134                namespace,
135            );
136            header
137        };
138
139        // The message is constructed by placing the KDF settings in a single recipient struct's
140        // unprotected headers. They do not need to live in the protected header, since to
141        // authenticate the protected header, the settings must be correct.
142        let builder = coset::CoseEncryptBuilder::new().add_recipient({
143            let mut recipient = coset::CoseRecipientBuilder::new()
144                .unprotected(kdf.into())
145                .build();
146            recipient.protected.header.alg = Some(kdf.cose_alg());
147            recipient
148        });
149
150        let cose_encrypt = encrypt_cose(
151            CoseContentEncryptionAlgorithm::Aes256Gcm,
152            builder,
153            protected_header,
154            &key_to_seal_bytes,
155            &envelope_key,
156        )
157        .map_err(|_| PasswordProtectedKeyEnvelopeError::Kdf)?;
158
159        Ok(PasswordProtectedKeyEnvelope { cose_encrypt })
160    }
161
162    /// Unseals a symmetric key from the password-protected envelope, and stores it in the key store
163    /// context.
164    pub fn unseal<Ids: KeySlotIds>(
165        &self,
166        password: &str,
167        namespace: PasswordProtectedKeyEnvelopeNamespace,
168        ctx: &mut KeyStoreContext<Ids>,
169    ) -> Result<Ids::Symmetric, PasswordProtectedKeyEnvelopeError> {
170        let key = self.unseal_ref(password, namespace)?;
171        Ok(ctx.add_local_symmetric_key(key))
172    }
173
174    fn unseal_ref(
175        &self,
176        password: &str,
177        content_namespace: PasswordProtectedKeyEnvelopeNamespace,
178    ) -> Result<SymmetricCryptoKey, PasswordProtectedKeyEnvelopeError> {
179        // There must be exactly one recipient in the COSE Encrypt object, which contains the KDF
180        // parameters.
181        let recipient = self
182            .cose_encrypt
183            .recipients
184            .first()
185            .filter(|_| self.cose_encrypt.recipients.len() == 1)
186            .ok_or_else(|| {
187                PasswordProtectedKeyEnvelopeError::Parsing(
188                    "Invalid number of recipients".to_string(),
189                )
190            })?;
191
192        validate_safe_namespaces(
193            &self.cose_encrypt.protected.header,
194            SafeObjectNamespace::PasswordProtectedKeyEnvelope,
195            content_namespace,
196        )
197        .map_err(|_| PasswordProtectedKeyEnvelopeError::InvalidNamespace)?;
198
199        // The KDF algorithm and its parameters are read from the recipient. This dispatches to
200        // the correct KDF (Argon2id or PBKDF2), and errors on an unknown algorithm.
201        let kdf = EnvelopeKdf::try_from(recipient)?;
202        let envelope_key =
203            derive_key(&kdf, password).map_err(|_| PasswordProtectedKeyEnvelopeError::Kdf)?;
204
205        // If decryption fails, the envelope-key is incorrect and thus the password is incorrect
206        // since the KDF parameters & salt are guaranteed to be correct. Envelopes sealed before the
207        // content-encryption algorithm was written to the protected header omit it, so
208        // XChaCha20-Poly1305 (the only algorithm such legacy envelopes ever used) is supplied as
209        // the decryption fallback. Envelopes that declare their algorithm (including all
210        // AES-256-GCM envelopes) dispatch on the protected header regardless of this fallback.
211        let key_bytes = decrypt_cose(
212            &self.cose_encrypt,
213            CoseAlgorithmPolicy::ProtectedHeaderAlgorithmOrLegacyDefault(
214                CoseContentEncryptionAlgorithm::XChaCha20Poly1305,
215            ),
216            &envelope_key,
217        )
218        .map_err(|_| PasswordProtectedKeyEnvelopeError::WrongPassword)?;
219
220        decode_sealed_symmetric_key(&self.cose_encrypt.protected.header, key_bytes).map_err(|e| {
221            match e {
222                DecodeSealedKeyError::InvalidContentFormat => {
223                    PasswordProtectedKeyEnvelopeError::Parsing("Invalid content format".to_string())
224                }
225                DecodeSealedKeyError::UnsupportedContentFormat => {
226                    PasswordProtectedKeyEnvelopeError::Parsing(
227                        "Unknown or unsupported content format".to_string(),
228                    )
229                }
230                DecodeSealedKeyError::InvalidKey => {
231                    PasswordProtectedKeyEnvelopeError::Parsing("Failed to decode key".to_string())
232                }
233            }
234        })
235    }
236
237    /// Re-seals the envelope for a new password, but the same KDF settings.
238    ///
239    /// Note:
240    /// Resealing a legacy Argon2id + XChaCha20-Poly1305 envelope upgrades it to Argon2id +
241    /// AES-256-GCM and the encrypt path never uses XChaCha20-Poly1305.
242    pub fn reseal(
243        &self,
244        password: &str,
245        new_password: &str,
246        namespace: PasswordProtectedKeyEnvelopeNamespace,
247    ) -> Result<Self, PasswordProtectedKeyEnvelopeError> {
248        // Determine the existing envelope's KDF family from its single recipient, so the resealed
249        // envelope keeps the same KDF family. The content-encryption algorithm is always
250        // AES-256-GCM, so resealing never uses XChaCha20-Poly1305.
251        let recipient = self
252            .cose_encrypt
253            .recipients
254            .first()
255            .filter(|_| self.cose_encrypt.recipients.len() == 1)
256            .ok_or_else(|| {
257                PasswordProtectedKeyEnvelopeError::Parsing(
258                    "Invalid number of recipients".to_string(),
259                )
260            })?;
261
262        let unsealed = self.unseal_ref(password, namespace)?;
263        Self::seal_ref_with_settings(
264            &unsealed,
265            new_password,
266            &EnvelopeKdf::try_from(recipient)?,
267            namespace,
268        )
269    }
270
271    /// Get the key ID of the contained key, if the key ID is stored on the envelope headers.
272    /// Only COSE keys have a key ID, legacy keys do not.
273    pub fn contained_key_id(&self) -> Result<Option<KeyId>, PasswordProtectedKeyEnvelopeError> {
274        extract_key_id(&self.cose_encrypt.protected.header)
275            .map_err(|_| PasswordProtectedKeyEnvelopeError::Parsing("Invalid key id".to_string()))
276    }
277}
278
279impl From<&PasswordProtectedKeyEnvelope> for Vec<u8> {
280    fn from(val: &PasswordProtectedKeyEnvelope) -> Self {
281        val.cose_encrypt
282            .clone()
283            .to_vec()
284            .expect("Serialization to cose should not fail")
285    }
286}
287
288impl TryFrom<&Vec<u8>> for PasswordProtectedKeyEnvelope {
289    type Error = CoseError;
290
291    fn try_from(value: &Vec<u8>) -> Result<Self, Self::Error> {
292        let cose_encrypt = coset::CoseEncrypt::from_slice(value)?;
293        Ok(PasswordProtectedKeyEnvelope { cose_encrypt })
294    }
295}
296
297impl std::fmt::Debug for PasswordProtectedKeyEnvelope {
298    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299        let mut s = f.debug_struct("PasswordProtectedKeyEnvelope");
300
301        if let Some(recipient) = self.cose_encrypt.recipients.first() {
302            match EnvelopeKdf::try_from(recipient) {
303                Ok(EnvelopeKdf::Argon2id(settings)) => {
304                    s.field("kdf", &"Argon2id");
305                    s.field("argon2_iterations", &settings.iterations);
306                    s.field("argon2_memory_kib", &settings.memory);
307                    s.field("argon2_parallelism", &settings.parallelism);
308                }
309                Ok(EnvelopeKdf::Pbkdf2(settings)) => {
310                    s.field("kdf", &"PBKDF2-HMAC-SHA256");
311                    s.field("pbkdf2_iterations", &settings.iterations);
312                }
313                Err(_) => {}
314            }
315        }
316
317        debug_fmt::<PasswordProtectedKeyEnvelopeNamespace>(
318            &mut s,
319            &self.cose_encrypt.protected.header,
320        );
321
322        if let Ok(Some(key_id)) = self.contained_key_id() {
323            s.field("contained_key_id", &key_id);
324        }
325
326        s.finish()
327    }
328}
329
330impl FromStr for PasswordProtectedKeyEnvelope {
331    type Err = PasswordProtectedKeyEnvelopeError;
332
333    fn from_str(s: &str) -> Result<Self, Self::Err> {
334        let data = B64::try_from(s).map_err(|_| {
335            PasswordProtectedKeyEnvelopeError::Parsing(
336                "Invalid PasswordProtectedKeyEnvelope Base64 encoding".to_string(),
337            )
338        })?;
339        Self::try_from(&data.into_bytes()).map_err(|_| {
340            PasswordProtectedKeyEnvelopeError::Parsing(
341                "Failed to parse PasswordProtectedKeyEnvelope".to_string(),
342            )
343        })
344    }
345}
346
347impl From<PasswordProtectedKeyEnvelope> for String {
348    fn from(val: PasswordProtectedKeyEnvelope) -> Self {
349        let serialized: Vec<u8> = (&val).into();
350        B64::from(serialized).to_string()
351    }
352}
353
354impl<'de> Deserialize<'de> for PasswordProtectedKeyEnvelope {
355    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
356    where
357        D: serde::Deserializer<'de>,
358    {
359        deserializer.deserialize_str(FromStrVisitor::new())
360    }
361}
362
363impl Serialize for PasswordProtectedKeyEnvelope {
364    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
365    where
366        S: serde::Serializer,
367    {
368        let serialized: Vec<u8> = self.into();
369        serializer.serialize_str(&B64::from(serialized).to_string())
370    }
371}
372
373/// Default PBKDF2-HMAC-SHA256 iteration count for FIPS envelopes
374const ENVELOPE_PBKDF2_ITERATIONS: u32 = 600_000;
375
376/// The KDF used to derive the envelope key from the password. The variant, and thus the algorithm,
377/// is selected from the key store's [`CipherSuite`] at seal time and recorded in the envelope's
378/// recipient so unsealing can dispatch on it.
379enum EnvelopeKdf {
380    Argon2id(Argon2RawSettings),
381    Pbkdf2(Pbkdf2RawSettings),
382}
383
384impl EnvelopeKdf {
385    /// The COSE algorithm discriminant written to the recipient's protected header.
386    fn cose_alg(&self) -> coset::Algorithm {
387        match self {
388            EnvelopeKdf::Argon2id(_) => coset::Algorithm::PrivateUse(ALG_ARGON2ID13),
389            EnvelopeKdf::Pbkdf2(_) => coset::Algorithm::PrivateUse(ALG_PBKDF2_SHA256),
390        }
391    }
392}
393
394impl From<&EnvelopeKdf> for Header {
395    fn from(kdf: &EnvelopeKdf) -> Header {
396        match kdf {
397            EnvelopeKdf::Argon2id(settings) => settings.into(),
398            EnvelopeKdf::Pbkdf2(settings) => settings.into(),
399        }
400    }
401}
402
403impl TryFrom<&coset::CoseRecipient> for EnvelopeKdf {
404    type Error = PasswordProtectedKeyEnvelopeError;
405
406    fn try_from(recipient: &coset::CoseRecipient) -> Result<Self, Self::Error> {
407        let alg = recipient.protected.header.alg.as_ref();
408        if alg == Some(&coset::Algorithm::PrivateUse(ALG_ARGON2ID13)) {
409            Ok(EnvelopeKdf::Argon2id((&recipient.unprotected).try_into()?))
410        } else if alg == Some(&coset::Algorithm::PrivateUse(ALG_PBKDF2_SHA256)) {
411            Ok(EnvelopeKdf::Pbkdf2((&recipient.unprotected).try_into()?))
412        } else {
413            Err(PasswordProtectedKeyEnvelopeError::Parsing(
414                "Unknown or unsupported KDF algorithm".to_string(),
415            ))
416        }
417    }
418}
419
420/// Returns the KDF settings for the given cipher suite.
421fn suite_kdf(suite: CipherSuite) -> EnvelopeKdf {
422    match suite {
423        CipherSuite::Standard => EnvelopeKdf::Argon2id(Argon2RawSettings::local_kdf_settings()),
424        CipherSuite::Fips => EnvelopeKdf::Pbkdf2(Pbkdf2RawSettings::local_kdf_settings()),
425    }
426}
427
428/// Raw argon2 settings differ from the [crate::keys::Kdf::Argon2id] struct defined for existing
429/// master-password unlock. The memory is represented in kibibytes (KiB) instead of mebibytes (MiB),
430/// and the salt is a fixed size of 32 bytes, and randomly generated, instead of being derived from
431/// the email.
432struct Argon2RawSettings {
433    iterations: u32,
434    /// Memory in KiB
435    memory: u32,
436    parallelism: u32,
437    salt: [u8; ENVELOPE_ARGON2_SALT_SIZE],
438}
439
440impl Argon2RawSettings {
441    /// Creates default Argon2 settings based on the device. This currently is a static preset
442    /// based on the target os
443    fn local_kdf_settings() -> Self {
444        let mut salt = [0u8; ENVELOPE_ARGON2_SALT_SIZE];
445        bitwarden_random::rng().fill_bytes(&mut salt);
446
447        // iOS has memory limitations in the auto-fill context. So, the memory is halved
448        // but the iterations are doubled
449        if cfg!(target_os = "ios") {
450            // The SECOND RECOMMENDED option from: https://datatracker.ietf.org/doc/rfc9106/, with halved memory and doubled iteration count
451            Self {
452                iterations: 6,
453                memory: 32 * 1024, // 32 MiB
454                parallelism: 4,
455                salt,
456            }
457        } else {
458            // The SECOND RECOMMENDED option from: https://datatracker.ietf.org/doc/rfc9106/
459            // The FIRST RECOMMENDED option currently still has too much memory consumption for most
460            // clients except desktop.
461            Self {
462                iterations: 3,
463                memory: 64 * 1024, // 64 MiB
464                parallelism: 4,
465                salt,
466            }
467        }
468    }
469}
470
471impl From<&Argon2RawSettings> for Header {
472    fn from(settings: &Argon2RawSettings) -> Header {
473        let builder = HeaderBuilder::new()
474            .value(ARGON2_ITERATIONS, Integer::from(settings.iterations).into())
475            .value(ARGON2_MEMORY, Integer::from(settings.memory).into())
476            .value(
477                ARGON2_PARALLELISM,
478                Integer::from(settings.parallelism).into(),
479            )
480            .value(ARGON2_SALT, Value::from(settings.salt.to_vec()));
481
482        let mut header = builder.build();
483        header.alg = Some(coset::Algorithm::PrivateUse(ALG_ARGON2ID13));
484        header
485    }
486}
487
488impl TryInto<Params> for &Argon2RawSettings {
489    type Error = PasswordProtectedKeyEnvelopeError;
490
491    fn try_into(self) -> Result<Params, PasswordProtectedKeyEnvelopeError> {
492        Params::new(
493            self.memory,
494            self.iterations,
495            self.parallelism,
496            Some(ENVELOPE_ARGON2_OUTPUT_KEY_SIZE),
497        )
498        .map_err(|_| PasswordProtectedKeyEnvelopeError::Kdf)
499    }
500}
501
502impl TryInto<Argon2RawSettings> for &Header {
503    type Error = PasswordProtectedKeyEnvelopeError;
504
505    fn try_into(self) -> Result<Argon2RawSettings, PasswordProtectedKeyEnvelopeError> {
506        Ok(Argon2RawSettings {
507            iterations: extract_integer(self, ARGON2_ITERATIONS, "iterations")?.try_into()?,
508            memory: extract_integer(self, ARGON2_MEMORY, "memory")?.try_into()?,
509            parallelism: extract_integer(self, ARGON2_PARALLELISM, "parallelism")?.try_into()?,
510            salt: extract_bytes(self, ARGON2_SALT, "salt")?
511                .try_into()
512                .map_err(|_| {
513                    PasswordProtectedKeyEnvelopeError::Parsing("Invalid Argon2 salt".to_string())
514                })?,
515        })
516    }
517}
518
519/// Raw PBKDF2-HMAC-SHA256 settings for FIPS envelopes. Like [`Argon2RawSettings`], the salt is a
520/// fixed size and randomly generated, and lives in the envelope alongside the iteration count.
521struct Pbkdf2RawSettings {
522    iterations: u32,
523    salt: [u8; ENVELOPE_PBKDF2_SALT_SIZE],
524}
525
526impl Pbkdf2RawSettings {
527    /// Creates default PBKDF2 settings with a random salt.
528    fn local_kdf_settings() -> Self {
529        let mut salt = [0u8; ENVELOPE_PBKDF2_SALT_SIZE];
530        bitwarden_random::rng().fill_bytes(&mut salt);
531
532        Self {
533            iterations: ENVELOPE_PBKDF2_ITERATIONS,
534            salt,
535        }
536    }
537}
538
539impl From<&Pbkdf2RawSettings> for Header {
540    fn from(settings: &Pbkdf2RawSettings) -> Header {
541        let builder = HeaderBuilder::new()
542            .value(PBKDF2_ITERATIONS, Integer::from(settings.iterations).into())
543            .value(PBKDF2_SALT, Value::from(settings.salt.to_vec()));
544
545        let mut header = builder.build();
546        header.alg = Some(coset::Algorithm::PrivateUse(ALG_PBKDF2_SHA256));
547        header
548    }
549}
550
551impl TryInto<Pbkdf2RawSettings> for &Header {
552    type Error = PasswordProtectedKeyEnvelopeError;
553
554    fn try_into(self) -> Result<Pbkdf2RawSettings, PasswordProtectedKeyEnvelopeError> {
555        Ok(Pbkdf2RawSettings {
556            iterations: extract_integer(self, PBKDF2_ITERATIONS, "iterations")?.try_into()?,
557            salt: extract_bytes(self, PBKDF2_SALT, "salt")?
558                .try_into()
559                .map_err(|_| {
560                    PasswordProtectedKeyEnvelopeError::Parsing("Invalid PBKDF2 salt".to_string())
561                })?,
562        })
563    }
564}
565
566/// Derives the envelope key from the password using the configured KDF.
567fn derive_key(
568    kdf: &EnvelopeKdf,
569    password: &str,
570) -> Result<[u8; ENVELOPE_ARGON2_OUTPUT_KEY_SIZE], PasswordProtectedKeyEnvelopeError> {
571    match kdf {
572        EnvelopeKdf::Argon2id(settings) => derive_argon2_key(settings, password),
573        EnvelopeKdf::Pbkdf2(settings) => derive_pbkdf2_key(settings, password),
574    }
575}
576
577fn derive_argon2_key(
578    argon2_settings: &Argon2RawSettings,
579    password: &str,
580) -> Result<[u8; ENVELOPE_ARGON2_OUTPUT_KEY_SIZE], PasswordProtectedKeyEnvelopeError> {
581    use argon2::*;
582
583    let mut hash = [0u8; ENVELOPE_ARGON2_OUTPUT_KEY_SIZE];
584    Argon2::new(
585        Algorithm::Argon2id,
586        Version::V0x13,
587        argon2_settings.try_into()?,
588    )
589    .hash_password_into(password.as_bytes(), &argon2_settings.salt, &mut hash)
590    .map_err(|_| PasswordProtectedKeyEnvelopeError::Kdf)?;
591
592    Ok(hash)
593}
594
595fn derive_pbkdf2_key(
596    pbkdf2_settings: &Pbkdf2RawSettings,
597    password: &str,
598) -> Result<[u8; ENVELOPE_ARGON2_OUTPUT_KEY_SIZE], PasswordProtectedKeyEnvelopeError> {
599    Ok(crate::util::pbkdf2(
600        password.as_bytes(),
601        &pbkdf2_settings.salt,
602        pbkdf2_settings.iterations,
603    ))
604}
605
606/// Errors that can occur when sealing or unsealing a key with the `PasswordProtectedKeyEnvelope`.
607#[derive(Debug, Error)]
608pub enum PasswordProtectedKeyEnvelopeError {
609    /// The password provided is incorrect or the envelope was tampered with
610    #[error("Wrong password")]
611    WrongPassword,
612    /// The envelope could not be parsed correctly, or the KDF parameters are invalid
613    #[error("Parsing error {0}")]
614    Parsing(String),
615    /// The KDF failed to derive a key, possibly due to invalid parameters or memory allocation
616    /// issues
617    #[error("Kdf error")]
618    Kdf,
619    /// There is no key for the provided key id in the key store
620    #[error("Key missing error")]
621    KeyMissing,
622    /// The key store could not be written to, for example due to being read-only
623    #[error("Could not write to key store")]
624    KeyStore,
625    /// The namespace provided in the envelope does not match any known namespaces, or is invalid
626    #[error("Invalid namespace")]
627    InvalidNamespace,
628}
629
630impl From<CoseExtractError> for PasswordProtectedKeyEnvelopeError {
631    fn from(err: CoseExtractError) -> Self {
632        let CoseExtractError::MissingValue(label) = err;
633        PasswordProtectedKeyEnvelopeError::Parsing(format!("Missing value for {}", label))
634    }
635}
636
637impl From<TryFromIntError> for PasswordProtectedKeyEnvelopeError {
638    fn from(err: TryFromIntError) -> Self {
639        PasswordProtectedKeyEnvelopeError::Parsing(format!("Invalid integer: {}", err))
640    }
641}
642
643#[cfg(feature = "wasm")]
644#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
645const TS_CUSTOM_TYPES: &'static str = r#"
646export type PasswordProtectedKeyEnvelope = Tagged<string, "PasswordProtectedKeyEnvelope">;
647"#;
648
649#[cfg(feature = "wasm")]
650impl wasm_bindgen::describe::WasmDescribe for PasswordProtectedKeyEnvelope {
651    fn describe() {
652        <String as wasm_bindgen::describe::WasmDescribe>::describe();
653    }
654}
655
656#[cfg(feature = "wasm")]
657impl FromWasmAbi for PasswordProtectedKeyEnvelope {
658    type Abi = <String as FromWasmAbi>::Abi;
659
660    unsafe fn from_abi(abi: Self::Abi) -> Self {
661        use wasm_bindgen::UnwrapThrowExt;
662        let string = unsafe { String::from_abi(abi) };
663        PasswordProtectedKeyEnvelope::from_str(&string).unwrap_throw()
664    }
665}
666
667#[cfg(feature = "wasm")]
668impl OptionFromWasmAbi for PasswordProtectedKeyEnvelope {
669    fn is_none(abi: &Self::Abi) -> bool {
670        <String as OptionFromWasmAbi>::is_none(abi)
671    }
672}
673
674#[cfg(feature = "wasm")]
675impl IntoWasmAbi for PasswordProtectedKeyEnvelope {
676    type Abi = <String as IntoWasmAbi>::Abi;
677
678    fn into_abi(self) -> Self::Abi {
679        let string: String = self.into();
680        string.into_abi()
681    }
682}
683
684#[cfg(feature = "wasm")]
685impl TryFrom<wasm_bindgen::JsValue> for PasswordProtectedKeyEnvelope {
686    type Error = PasswordProtectedKeyEnvelopeError;
687
688    fn try_from(value: wasm_bindgen::JsValue) -> Result<Self, Self::Error> {
689        let string = value.as_string().ok_or_else(|| {
690            PasswordProtectedKeyEnvelopeError::Parsing(
691                "PasswordProtectedKeyEnvelope JsValue is not a string".to_string(),
692            )
693        })?;
694        PasswordProtectedKeyEnvelope::from_str(&string)
695    }
696}
697
698/// The content-layer separation namespace for password protected key envelopes.
699#[derive(Debug, Clone, Copy, PartialEq, Eq)]
700pub enum PasswordProtectedKeyEnvelopeNamespace {
701    /// The namespace for unlocking vaults with a PIN.
702    PinUnlock = 1,
703    /// This namespace is only used in tests
704    #[cfg(test)]
705    ExampleNamespace = -1,
706    /// This namespace is only used in tests
707    #[cfg(test)]
708    ExampleNamespace2 = -2,
709}
710
711impl PasswordProtectedKeyEnvelopeNamespace {
712    /// Returns the numeric value of the namespace.
713    fn as_i64(&self) -> i64 {
714        *self as i64
715    }
716}
717
718impl TryFrom<i128> for PasswordProtectedKeyEnvelopeNamespace {
719    type Error = PasswordProtectedKeyEnvelopeError;
720
721    fn try_from(value: i128) -> Result<Self, Self::Error> {
722        match value {
723            1 => Ok(PasswordProtectedKeyEnvelopeNamespace::PinUnlock),
724            #[cfg(test)]
725            -1 => Ok(PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace),
726            #[cfg(test)]
727            -2 => Ok(PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace2),
728            _ => Err(PasswordProtectedKeyEnvelopeError::InvalidNamespace),
729        }
730    }
731}
732
733impl TryFrom<i64> for PasswordProtectedKeyEnvelopeNamespace {
734    type Error = PasswordProtectedKeyEnvelopeError;
735
736    fn try_from(value: i64) -> Result<Self, Self::Error> {
737        Self::try_from(i128::from(value))
738    }
739}
740
741impl From<PasswordProtectedKeyEnvelopeNamespace> for i128 {
742    fn from(val: PasswordProtectedKeyEnvelopeNamespace) -> Self {
743        val.as_i64().into()
744    }
745}
746
747impl ContentNamespace for PasswordProtectedKeyEnvelopeNamespace {}
748
749#[cfg(test)]
750mod tests {
751    use super::*;
752    use crate::{KeyStore, SymmetricKeyAlgorithm, traits::tests::TestIds};
753
754    const TEST_UNSEALED_COSEKEY_ENCODED: &[u8] = &[
755        165, 1, 4, 2, 80, 63, 208, 189, 183, 204, 37, 72, 170, 179, 236, 190, 208, 22, 65, 227,
756        183, 3, 58, 0, 1, 17, 111, 4, 132, 3, 4, 5, 6, 32, 88, 32, 88, 25, 68, 85, 205, 28, 133,
757        28, 90, 147, 160, 145, 48, 3, 178, 184, 30, 11, 122, 132, 64, 59, 51, 233, 191, 117, 159,
758        117, 23, 168, 248, 36, 1,
759    ];
760    const TESTVECTOR_COSEKEY_ENVELOPE: &[u8] = &[
761        132, 68, 161, 3, 24, 101, 161, 5, 88, 24, 1, 31, 58, 230, 10, 92, 195, 233, 212, 7, 166,
762        252, 67, 115, 221, 58, 3, 191, 218, 188, 181, 192, 28, 11, 88, 84, 141, 183, 137, 167, 166,
763        161, 33, 82, 30, 255, 23, 10, 179, 149, 88, 24, 39, 60, 74, 232, 133, 44, 90, 98, 117, 31,
764        41, 69, 251, 76, 250, 141, 229, 83, 191, 6, 237, 107, 127, 93, 238, 110, 49, 125, 201, 37,
765        162, 120, 157, 32, 116, 195, 208, 143, 83, 254, 223, 93, 97, 158, 0, 24, 95, 197, 249, 35,
766        240, 3, 20, 71, 164, 97, 180, 29, 203, 69, 31, 151, 249, 244, 197, 91, 101, 174, 129, 131,
767        71, 161, 1, 58, 0, 1, 21, 87, 165, 1, 58, 0, 1, 21, 87, 58, 0, 1, 21, 89, 3, 58, 0, 1, 21,
768        90, 26, 0, 1, 0, 0, 58, 0, 1, 21, 91, 4, 58, 0, 1, 21, 88, 80, 165, 253, 56, 243, 255, 54,
769        246, 252, 231, 230, 33, 252, 49, 175, 1, 111, 246,
770    ];
771    const TEST_UNSEALED_LEGACYKEY_ENCODED: &[u8] = &[
772        135, 114, 97, 155, 115, 209, 215, 224, 175, 159, 231, 208, 15, 244, 40, 171, 239, 137, 57,
773        98, 207, 167, 231, 138, 145, 254, 28, 136, 236, 60, 23, 163, 4, 246, 219, 117, 104, 246,
774        86, 10, 152, 52, 90, 85, 58, 6, 70, 39, 111, 128, 93, 145, 143, 180, 77, 129, 178, 242, 82,
775        72, 57, 61, 192, 64,
776    ];
777    const TESTVECTOR_LEGACYKEY_ENVELOPE: &[u8] = &[
778        132, 88, 38, 161, 3, 120, 34, 97, 112, 112, 108, 105, 99, 97, 116, 105, 111, 110, 47, 120,
779        46, 98, 105, 116, 119, 97, 114, 100, 101, 110, 46, 108, 101, 103, 97, 99, 121, 45, 107,
780        101, 121, 161, 5, 88, 24, 218, 72, 22, 79, 149, 30, 12, 36, 180, 212, 44, 21, 167, 208,
781        214, 221, 7, 91, 178, 12, 104, 17, 45, 219, 88, 80, 114, 38, 14, 165, 85, 229, 103, 108,
782        17, 175, 41, 43, 203, 175, 119, 125, 227, 127, 163, 214, 213, 138, 12, 216, 163, 204, 38,
783        222, 47, 11, 44, 231, 239, 170, 63, 8, 249, 56, 102, 18, 134, 34, 232, 193, 44, 19, 228,
784        17, 187, 199, 238, 187, 2, 13, 30, 112, 103, 110, 5, 31, 238, 58, 4, 24, 19, 239, 135, 57,
785        206, 190, 144, 83, 128, 204, 59, 155, 21, 80, 180, 34, 129, 131, 71, 161, 1, 58, 0, 1, 21,
786        87, 165, 1, 58, 0, 1, 21, 87, 58, 0, 1, 21, 89, 3, 58, 0, 1, 21, 90, 26, 0, 1, 0, 0, 58, 0,
787        1, 21, 91, 4, 58, 0, 1, 21, 88, 80, 212, 91, 185, 112, 92, 177, 108, 33, 182, 202, 26, 141,
788        11, 133, 95, 235, 246,
789    ];
790
791    const TESTVECTOR_PASSWORD: &str = "test_password";
792
793    // FIPS (PBKDF2-HMAC-SHA256 + AES-256-GCM) test vector, generated by `generate_fips_test_vector`
794    // with a low iteration count. Locks the FIPS envelope wire format for backward compatibility.
795    const TEST_UNSEALED_FIPS_ENCODED: &[u8] = &[
796        165, 1, 4, 2, 80, 40, 136, 73, 38, 113, 191, 48, 29, 141, 167, 113, 250, 16, 155, 74, 5, 3,
797        58, 0, 1, 17, 111, 4, 132, 3, 4, 5, 6, 32, 88, 32, 3, 78, 100, 122, 143, 20, 156, 26, 63,
798        89, 134, 100, 93, 32, 137, 121, 214, 98, 147, 183, 198, 61, 176, 86, 176, 65, 220, 38, 211,
799        233, 121, 100, 1,
800    ];
801    const TESTVECTOR_FIPS_ENVELOPE: &[u8] = &[
802        132, 88, 40, 165, 1, 3, 3, 24, 101, 58, 0, 1, 21, 92, 80, 40, 136, 73, 38, 113, 191, 48,
803        29, 141, 167, 113, 250, 16, 155, 74, 5, 58, 0, 1, 56, 129, 1, 58, 0, 1, 56, 128, 32, 161,
804        5, 76, 26, 141, 71, 238, 227, 209, 118, 32, 47, 159, 178, 11, 88, 84, 3, 184, 23, 73, 5,
805        232, 28, 144, 109, 242, 86, 211, 155, 157, 221, 76, 72, 122, 18, 64, 20, 190, 25, 34, 166,
806        3, 34, 125, 59, 172, 105, 238, 123, 17, 123, 55, 203, 107, 157, 37, 186, 240, 79, 103, 95,
807        160, 196, 38, 71, 202, 212, 241, 77, 159, 165, 31, 43, 163, 126, 182, 127, 90, 119, 35, 29,
808        155, 119, 121, 163, 98, 158, 18, 120, 199, 91, 197, 46, 224, 63, 215, 119, 191, 60, 21,
809        129, 131, 71, 161, 1, 58, 0, 1, 21, 97, 163, 1, 58, 0, 1, 21, 97, 58, 0, 1, 21, 98, 25, 19,
810        136, 58, 0, 1, 21, 99, 80, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 246,
811    ];
812
813    #[test]
814    #[ignore = "Manual test to verify debug format"]
815    fn test_debug() {
816        let key = SymmetricCryptoKey::make_xchacha20_poly1305_key();
817        let envelope = PasswordProtectedKeyEnvelope::seal_ref(
818            &key,
819            "test_password",
820            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
821        )
822        .unwrap();
823        println!("{:?}", envelope);
824    }
825
826    #[test]
827    fn test_testvector_cosekey() {
828        let key_store = KeyStore::<TestIds>::default();
829        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
830        let envelope =
831            PasswordProtectedKeyEnvelope::try_from(&TESTVECTOR_COSEKEY_ENVELOPE.to_vec())
832                .expect("Key envelope should be valid");
833        let key = envelope
834            .unseal(
835                TESTVECTOR_PASSWORD,
836                PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
837                &mut ctx,
838            )
839            .expect("Unsealing should succeed");
840        let unsealed_key = ctx
841            .get_symmetric_key(key)
842            .expect("Key should exist in the key store");
843        assert_eq!(
844            unsealed_key.to_encoded().to_vec(),
845            TEST_UNSEALED_COSEKEY_ENCODED
846        );
847    }
848
849    #[test]
850    fn test_testvector_legacykey() {
851        let key_store = KeyStore::<TestIds>::default();
852        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
853        let envelope =
854            PasswordProtectedKeyEnvelope::try_from(&TESTVECTOR_LEGACYKEY_ENVELOPE.to_vec())
855                .expect("Key envelope should be valid");
856        let key = envelope
857            .unseal(
858                TESTVECTOR_PASSWORD,
859                PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
860                &mut ctx,
861            )
862            .expect("Unsealing should succeed");
863        let unsealed_key = ctx
864            .get_symmetric_key(key)
865            .expect("Key should exist in the key store");
866        assert_eq!(
867            unsealed_key.to_encoded().to_vec(),
868            TEST_UNSEALED_LEGACYKEY_ENCODED
869        );
870    }
871
872    #[test]
873    #[ignore = "Manual test to generate a FIPS (PBKDF2 + AES-GCM) test vector"]
874    fn generate_fips_test_vector() {
875        let key = SymmetricCryptoKey::make_xchacha20_poly1305_key();
876        let envelope = PasswordProtectedKeyEnvelope::seal_ref_with_settings(
877            &key,
878            TESTVECTOR_PASSWORD,
879            &EnvelopeKdf::Pbkdf2(Pbkdf2RawSettings {
880                // Low iteration count keeps the test vector cheap to verify.
881                iterations: 5000,
882                salt: [7u8; ENVELOPE_PBKDF2_SALT_SIZE],
883            }),
884            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
885        )
886        .unwrap();
887        println!(
888            "const TEST_UNSEALED_FIPS_ENCODED: &[u8] = &{:?};",
889            key.to_encoded().to_vec()
890        );
891        println!(
892            "const TESTVECTOR_FIPS_ENVELOPE: &[u8] = &{:?};",
893            Vec::<u8>::from(&envelope)
894        );
895    }
896
897    #[test]
898    fn test_testvector_fips() {
899        let envelope = PasswordProtectedKeyEnvelope::try_from(&TESTVECTOR_FIPS_ENVELOPE.to_vec())
900            .expect("Key envelope should be valid");
901        let key = envelope
902            .unseal_ref(
903                TESTVECTOR_PASSWORD,
904                PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
905            )
906            .expect("Unsealing should succeed");
907        assert_eq!(key.to_encoded().to_vec(), TEST_UNSEALED_FIPS_ENCODED);
908    }
909
910    #[test]
911    fn test_make_envelope() {
912        let key_store = KeyStore::<TestIds>::default();
913        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
914        let test_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
915
916        let password = "test_password";
917
918        // Seal the key with a password
919        let envelope = PasswordProtectedKeyEnvelope::seal(
920            test_key,
921            password,
922            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
923            &ctx,
924        )
925        .unwrap();
926        let serialized: Vec<u8> = (&envelope).into();
927
928        // Unseal the key from the envelope
929        let deserialized: PasswordProtectedKeyEnvelope =
930            PasswordProtectedKeyEnvelope::try_from(&serialized).unwrap();
931        let key = deserialized
932            .unseal(
933                password,
934                PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
935                &mut ctx,
936            )
937            .unwrap();
938
939        // Verify that the unsealed key matches the original key
940        let unsealed_key = ctx
941            .get_symmetric_key(key)
942            .expect("Key should exist in the key store");
943
944        let key_before_sealing = ctx
945            .get_symmetric_key(test_key)
946            .expect("Key should exist in the key store");
947
948        assert_eq!(unsealed_key, key_before_sealing);
949    }
950
951    #[test]
952    fn test_make_envelope_legacy_key() {
953        let key_store = KeyStore::<TestIds>::default();
954        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
955        let test_key = ctx.generate_symmetric_key();
956
957        let password = "test_password";
958
959        // Seal the key with a password
960        let envelope = PasswordProtectedKeyEnvelope::seal(
961            test_key,
962            password,
963            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
964            &ctx,
965        )
966        .unwrap();
967        let serialized: Vec<u8> = (&envelope).into();
968
969        // Unseal the key from the envelope
970        let deserialized: PasswordProtectedKeyEnvelope =
971            PasswordProtectedKeyEnvelope::try_from(&serialized).unwrap();
972        let key = deserialized
973            .unseal(
974                password,
975                PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
976                &mut ctx,
977            )
978            .unwrap();
979
980        // Verify that the unsealed key matches the original key
981        let unsealed_key = ctx
982            .get_symmetric_key(key)
983            .expect("Key should exist in the key store");
984
985        let key_before_sealing = ctx
986            .get_symmetric_key(test_key)
987            .expect("Key should exist in the key store");
988
989        assert_eq!(unsealed_key, key_before_sealing);
990    }
991
992    #[test]
993    fn test_reseal_envelope() {
994        let key = SymmetricCryptoKey::make_xchacha20_poly1305_key();
995        let password = "test_password";
996        let new_password = "new_test_password";
997
998        // Seal the key with a password
999        let envelope: PasswordProtectedKeyEnvelope = PasswordProtectedKeyEnvelope::seal_ref(
1000            &key,
1001            password,
1002            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
1003        )
1004        .expect("Sealing should work");
1005
1006        // Reseal
1007        let envelope = envelope
1008            .reseal(
1009                password,
1010                new_password,
1011                PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
1012            )
1013            .expect("Resealing should work");
1014
1015        // Resealing keeps the Argon2id KDF but always writes AES-256-GCM content, never
1016        // XChaCha20-Poly1305.
1017        assert_eq!(
1018            envelope.cose_encrypt.recipients[0].protected.header.alg,
1019            Some(coset::Algorithm::PrivateUse(ALG_ARGON2ID13))
1020        );
1021        assert_eq!(
1022            envelope.cose_encrypt.protected.header.alg,
1023            Some(coset::Algorithm::Assigned(coset::iana::Algorithm::A256GCM))
1024        );
1025
1026        let unsealed = envelope
1027            .unseal_ref(
1028                new_password,
1029                PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
1030            )
1031            .expect("Unsealing should work");
1032
1033        // Verify that the unsealed key matches the original key
1034        assert_eq!(unsealed, key);
1035    }
1036
1037    #[test]
1038    fn test_wrong_password() {
1039        let key_store = KeyStore::<TestIds>::default();
1040        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
1041        let test_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
1042
1043        let password = "test_password";
1044        let wrong_password = "wrong_password";
1045
1046        // Seal the key with a password
1047        let envelope = PasswordProtectedKeyEnvelope::seal(
1048            test_key,
1049            password,
1050            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
1051            &ctx,
1052        )
1053        .unwrap();
1054
1055        // Attempt to unseal with the wrong password
1056        let deserialized: PasswordProtectedKeyEnvelope =
1057            PasswordProtectedKeyEnvelope::try_from(&(&envelope).into()).unwrap();
1058        assert!(matches!(
1059            deserialized.unseal(
1060                wrong_password,
1061                PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
1062                &mut ctx
1063            ),
1064            Err(PasswordProtectedKeyEnvelopeError::WrongPassword)
1065        ));
1066    }
1067
1068    #[test]
1069    fn test_wrong_safe_namespace() {
1070        let key_store = KeyStore::<TestIds>::default();
1071        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
1072        let test_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
1073        let password = "test_password";
1074
1075        let mut envelope = PasswordProtectedKeyEnvelope::seal(
1076            test_key,
1077            password,
1078            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
1079            &ctx,
1080        )
1081        .expect("Seal works");
1082
1083        if let Some((_, value)) = envelope
1084            .cose_encrypt
1085            .protected
1086            .header
1087            .rest
1088            .iter_mut()
1089            .find(|(label, _)| {
1090                matches!(label, coset::Label::Int(label_value) if *label_value == crate::cose::SAFE_OBJECT_NAMESPACE)
1091            })
1092        {
1093            *value = Value::Integer((SafeObjectNamespace::DataEnvelope as i64).into());
1094        }
1095
1096        let deserialized: PasswordProtectedKeyEnvelope =
1097            PasswordProtectedKeyEnvelope::try_from(&(&envelope).into())
1098                .expect("Envelope should be valid");
1099
1100        let a = deserialized.unseal(
1101            password,
1102            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
1103            &mut ctx,
1104        );
1105        println!("Error: {a:?}");
1106        assert!(matches!(
1107            a,
1108            Err(PasswordProtectedKeyEnvelopeError::InvalidNamespace)
1109        ));
1110    }
1111
1112    #[test]
1113    fn test_key_id() {
1114        let key_store = KeyStore::<TestIds>::default();
1115        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
1116        let test_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
1117        let key_id = ctx.get_symmetric_key(test_key).unwrap().key_id().unwrap();
1118
1119        let password = "test_password";
1120
1121        // Seal the key with a password
1122        let envelope = PasswordProtectedKeyEnvelope::seal(
1123            test_key,
1124            password,
1125            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
1126            &ctx,
1127        )
1128        .unwrap();
1129        let contained_key_id = envelope.contained_key_id().unwrap();
1130        assert_eq!(Some(key_id), contained_key_id);
1131    }
1132
1133    #[test]
1134    fn test_no_key_id() {
1135        let key_store = KeyStore::<TestIds>::default();
1136        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
1137        let test_key = ctx.generate_symmetric_key();
1138
1139        let password = "test_password";
1140
1141        // Seal the key with a password
1142        let envelope = PasswordProtectedKeyEnvelope::seal(
1143            test_key,
1144            password,
1145            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
1146            &ctx,
1147        )
1148        .unwrap();
1149        let contained_key_id = envelope.contained_key_id().unwrap();
1150        assert_eq!(None, contained_key_id);
1151    }
1152
1153    /// Builds a key store configured with the given cipher suite.
1154    fn key_store_with_suite(suite: CipherSuite) -> KeyStore<TestIds> {
1155        let key_store = KeyStore::<TestIds>::default();
1156        key_store.set_cipher_suite(suite);
1157        key_store
1158    }
1159
1160    #[test]
1161    fn test_standard_suite_uses_argon2_and_aes_gcm() {
1162        let key_store = key_store_with_suite(CipherSuite::Standard);
1163        let mut ctx = key_store.context_mut();
1164        let test_key = SymmetricCryptoKey::make_xchacha20_poly1305_key();
1165        let key_id = ctx.add_local_symmetric_key(test_key);
1166
1167        let envelope = PasswordProtectedKeyEnvelope::seal(
1168            key_id,
1169            "test_password",
1170            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
1171            &ctx,
1172        )
1173        .unwrap();
1174
1175        // Recipient declares Argon2id, content is AES-256-GCM (never XChaCha20-Poly1305).
1176        assert_eq!(
1177            envelope.cose_encrypt.recipients[0].protected.header.alg,
1178            Some(coset::Algorithm::PrivateUse(ALG_ARGON2ID13))
1179        );
1180        assert_eq!(
1181            envelope.cose_encrypt.protected.header.alg,
1182            Some(coset::Algorithm::Assigned(coset::iana::Algorithm::A256GCM))
1183        );
1184    }
1185
1186    #[test]
1187    fn test_fips_suite_uses_pbkdf2_and_aes_gcm() {
1188        let key_store = key_store_with_suite(CipherSuite::Fips);
1189        let mut ctx = key_store.context_mut();
1190        let test_key = SymmetricCryptoKey::make_xchacha20_poly1305_key();
1191        let key_id = ctx.add_local_symmetric_key(test_key);
1192
1193        let password = "test_password";
1194        let envelope = PasswordProtectedKeyEnvelope::seal(
1195            key_id,
1196            password,
1197            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
1198            &ctx,
1199        )
1200        .unwrap();
1201
1202        // Recipient declares PBKDF2, content is AES-256-GCM.
1203        assert_eq!(
1204            envelope.cose_encrypt.recipients[0].protected.header.alg,
1205            Some(coset::Algorithm::PrivateUse(ALG_PBKDF2_SHA256))
1206        );
1207        assert_eq!(
1208            envelope.cose_encrypt.protected.header.alg,
1209            Some(coset::Algorithm::Assigned(coset::iana::Algorithm::A256GCM))
1210        );
1211
1212        // Round-trips.
1213        let serialized: Vec<u8> = (&envelope).into();
1214        let deserialized = PasswordProtectedKeyEnvelope::try_from(&serialized).unwrap();
1215        let unsealed = deserialized
1216            .unseal(
1217                password,
1218                PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
1219                &mut ctx,
1220            )
1221            .unwrap();
1222        let unsealed_key = ctx.get_symmetric_key(unsealed).unwrap();
1223        let original = ctx.get_symmetric_key(key_id).unwrap();
1224        assert_eq!(unsealed_key, original);
1225    }
1226
1227    #[test]
1228    fn test_fips_wrong_password() {
1229        let key_store = key_store_with_suite(CipherSuite::Fips);
1230        let mut ctx = key_store.context_mut();
1231        let key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
1232
1233        let envelope = PasswordProtectedKeyEnvelope::seal(
1234            key_id,
1235            "test_password",
1236            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
1237            &ctx,
1238        )
1239        .unwrap();
1240
1241        assert!(matches!(
1242            envelope.unseal(
1243                "wrong_password",
1244                PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
1245                &mut ctx,
1246            ),
1247            Err(PasswordProtectedKeyEnvelopeError::WrongPassword)
1248        ));
1249    }
1250
1251    #[test]
1252    fn test_reseal_preserves_fips_suite() {
1253        let key_store = key_store_with_suite(CipherSuite::Fips);
1254        let mut ctx = key_store.context_mut();
1255        let key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
1256
1257        let envelope = PasswordProtectedKeyEnvelope::seal(
1258            key_id,
1259            "test_password",
1260            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
1261            &ctx,
1262        )
1263        .unwrap();
1264
1265        let resealed = envelope
1266            .reseal(
1267                "test_password",
1268                "new_password",
1269                PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
1270            )
1271            .unwrap();
1272
1273        // The resealed envelope keeps the FIPS algorithm suite.
1274        assert_eq!(
1275            resealed.cose_encrypt.recipients[0].protected.header.alg,
1276            Some(coset::Algorithm::PrivateUse(ALG_PBKDF2_SHA256))
1277        );
1278        assert_eq!(
1279            resealed.cose_encrypt.protected.header.alg,
1280            Some(coset::Algorithm::Assigned(coset::iana::Algorithm::A256GCM))
1281        );
1282
1283        let unsealed = resealed
1284            .unseal_ref(
1285                "new_password",
1286                PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
1287            )
1288            .unwrap();
1289        assert!(matches!(
1290            unsealed,
1291            SymmetricCryptoKey::XChaCha20Poly1305Key(_)
1292        ));
1293    }
1294}