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 (Argon2ID) combined with secret key encryption
5//! (XChaCha20-Poly1305). The KDF prevents brute-force by requiring work to be done to derive the
6//! key from the password.
7//!
8//! For the consumer, the output is an opaque blob that can be later unsealed with the same
9//! password. The KDF parameters and salt are contained in the envelope, and don't need to be
10//! provided for unsealing.
11//!
12//! Internally, the envelope is a CoseEncrypt object. The KDF parameters / salt are placed in the
13//! single recipient's unprotected headers. The output from the KDF - "envelope key", is used to
14//! wrap the symmetric key, that is sealed by the envelope.
15
16use std::{marker::PhantomData, num::TryFromIntError, str::FromStr};
17
18use argon2::Params;
19use bitwarden_encoding::{FromStrVisitor, B64};
20use ciborium::{value::Integer, Value};
21use coset::{CborSerializable, CoseError, Header, HeaderBuilder};
22use rand::RngCore;
23use serde::{Deserialize, Serialize};
24use thiserror::Error;
25
26use crate::{
27    cose::{
28        extract_bytes, extract_integer, CoseExtractError, ALG_ARGON2ID13, ARGON2_ITERATIONS,
29        ARGON2_MEMORY, ARGON2_PARALLELISM, ARGON2_SALT,
30    },
31    xchacha20, BitwardenLegacyKeyBytes, ContentFormat, CoseKeyBytes, EncodedSymmetricKey, KeyIds,
32    KeyStoreContext, SymmetricCryptoKey,
33};
34
35/// 16 is the RECOMMENDED salt size for all applications:
36/// <https://datatracker.ietf.org/doc/rfc9106/>
37const ENVELOPE_ARGON2_SALT_SIZE: usize = 16;
38/// 32 is chosen to match the size of an XChaCha20-Poly1305 key
39const ENVELOPE_ARGON2_OUTPUT_KEY_SIZE: usize = 32;
40
41/// A password-protected key envelope can seal a symmetric key, and protect it with a password. It
42/// does so by using a Key Derivation Function (KDF), to increase the difficulty of brute-forcing
43/// the password.
44///
45/// The KDF parameters such as iterations and salt are stored in the envelope and do not have to
46/// be provided.
47///
48/// Internally, Argon2 is used as the KDF and XChaCha20-Poly1305 is used to encrypt the key.
49pub struct PasswordProtectedKeyEnvelope<Ids: KeyIds> {
50    _phantom: PhantomData<Ids>,
51    cose_encrypt: coset::CoseEncrypt,
52}
53
54impl<Ids: KeyIds> PasswordProtectedKeyEnvelope<Ids> {
55    /// Seals a symmetric key with a password, using the current default KDF parameters and a random
56    /// salt.
57    ///
58    /// This should never fail, except for memory allocation error, when running the KDF.
59    pub fn seal(
60        key_to_seal: Ids::Symmetric,
61        password: &str,
62        ctx: &KeyStoreContext<Ids>,
63    ) -> Result<Self, PasswordProtectedKeyEnvelopeError> {
64        #[allow(deprecated)]
65        let key_ref = ctx
66            .dangerous_get_symmetric_key(key_to_seal)
67            .map_err(|_| PasswordProtectedKeyEnvelopeError::KeyMissingError)?;
68        Self::seal_ref(key_ref, password)
69    }
70
71    /// Seals a key reference with a password. This function is not public since callers are
72    /// expected to only work with key store references.
73    fn seal_ref(
74        key_to_seal: &SymmetricCryptoKey,
75        password: &str,
76    ) -> Result<Self, PasswordProtectedKeyEnvelopeError> {
77        Self::seal_ref_with_settings(
78            key_to_seal,
79            password,
80            &Argon2RawSettings::local_kdf_settings(),
81        )
82    }
83
84    /// Seals a key reference with a password and custom provided settings. This function is not
85    /// public since callers are expected to only work with key store references, and to not
86    /// control the KDF difficulty where possible.
87    fn seal_ref_with_settings(
88        key_to_seal: &SymmetricCryptoKey,
89        password: &str,
90        kdf_settings: &Argon2RawSettings,
91    ) -> Result<Self, PasswordProtectedKeyEnvelopeError> {
92        // Cose does not yet have a standardized way to protect a key using a password.
93        // This implements content encryption using direct encryption with a KDF derived key,
94        // similar to "Direct Key with KDF" mentioned in the COSE spec. The KDF settings are
95        // placed in a single recipient struct.
96
97        // The envelope key is directly derived from the KDF and used as the key to encrypt the key
98        // that should be sealed.
99        let envelope_key = derive_key(kdf_settings, password)
100            .map_err(|_| PasswordProtectedKeyEnvelopeError::KdfError)?;
101
102        let (content_format, key_to_seal_bytes) = match key_to_seal.to_encoded_raw() {
103            EncodedSymmetricKey::BitwardenLegacyKey(key_bytes) => {
104                (ContentFormat::BitwardenLegacyKey, key_bytes.to_vec())
105            }
106            EncodedSymmetricKey::CoseKey(key_bytes) => (ContentFormat::CoseKey, key_bytes.to_vec()),
107        };
108
109        let mut nonce = [0u8; crate::xchacha20::NONCE_SIZE];
110
111        // The message is constructed by placing the KDF settings in a recipient struct's
112        // unprotected headers. They do not need to live in the protected header, since to
113        // authenticate the protected header, the settings must be correct.
114        let mut cose_encrypt = coset::CoseEncryptBuilder::new()
115            .add_recipient({
116                let mut recipient = coset::CoseRecipientBuilder::new()
117                    .unprotected(kdf_settings.into())
118                    .build();
119                recipient.protected.header.alg = Some(coset::Algorithm::PrivateUse(ALG_ARGON2ID13));
120                recipient
121            })
122            .protected(HeaderBuilder::from(content_format).build())
123            .create_ciphertext(&key_to_seal_bytes, &[], |data, aad| {
124                let ciphertext = xchacha20::encrypt_xchacha20_poly1305(&envelope_key, data, aad);
125                nonce.copy_from_slice(&ciphertext.nonce());
126                ciphertext.encrypted_bytes().to_vec()
127            })
128            .build();
129        cose_encrypt.unprotected.iv = nonce.into();
130
131        Ok(PasswordProtectedKeyEnvelope {
132            _phantom: PhantomData,
133            cose_encrypt,
134        })
135    }
136
137    /// Unseals a symmetric key from the password-protected envelope, and stores it in the key store
138    /// context.
139    pub fn unseal(
140        &self,
141        target_keyslot: Ids::Symmetric,
142        password: &str,
143        ctx: &mut KeyStoreContext<Ids>,
144    ) -> Result<Ids::Symmetric, PasswordProtectedKeyEnvelopeError> {
145        let key = self.unseal_ref(password)?;
146        #[allow(deprecated)]
147        ctx.set_symmetric_key(target_keyslot, key)
148            .map_err(|_| PasswordProtectedKeyEnvelopeError::KeyStoreError)?;
149        Ok(target_keyslot)
150    }
151
152    fn unseal_ref(
153        &self,
154        password: &str,
155    ) -> Result<SymmetricCryptoKey, PasswordProtectedKeyEnvelopeError> {
156        // There must be exactly one recipient in the COSE Encrypt object, which contains the KDF
157        // parameters.
158        let recipient = self
159            .cose_encrypt
160            .recipients
161            .first()
162            .filter(|_| self.cose_encrypt.recipients.len() == 1)
163            .ok_or_else(|| {
164                PasswordProtectedKeyEnvelopeError::ParsingError(
165                    "Invalid number of recipients".to_string(),
166                )
167            })?;
168
169        if recipient.protected.header.alg != Some(coset::Algorithm::PrivateUse(ALG_ARGON2ID13)) {
170            return Err(PasswordProtectedKeyEnvelopeError::ParsingError(
171                "Unknown or unsupported KDF algorithm".to_string(),
172            ));
173        }
174
175        let kdf_settings: Argon2RawSettings =
176            (&recipient.unprotected).try_into().map_err(|_| {
177                PasswordProtectedKeyEnvelopeError::ParsingError(
178                    "Invalid or missing KDF parameters".to_string(),
179                )
180            })?;
181        let envelope_key = derive_key(&kdf_settings, password)
182            .map_err(|_| PasswordProtectedKeyEnvelopeError::KdfError)?;
183        let nonce: [u8; crate::xchacha20::NONCE_SIZE] = self
184            .cose_encrypt
185            .unprotected
186            .iv
187            .clone()
188            .try_into()
189            .map_err(|_| {
190                PasswordProtectedKeyEnvelopeError::ParsingError("Invalid IV".to_string())
191            })?;
192
193        let key_bytes = self
194            .cose_encrypt
195            .decrypt(&[], |data, aad| {
196                xchacha20::decrypt_xchacha20_poly1305(&nonce, &envelope_key, data, aad)
197            })
198            // If decryption fails, the envelope-key is incorrect and thus the password is incorrect
199            // since the KDF parameters & salt are guaranteed to be correct
200            .map_err(|_| PasswordProtectedKeyEnvelopeError::WrongPassword)?;
201
202        SymmetricCryptoKey::try_from(
203            match ContentFormat::try_from(&self.cose_encrypt.protected.header).map_err(|_| {
204                PasswordProtectedKeyEnvelopeError::ParsingError(
205                    "Invalid content format".to_string(),
206                )
207            })? {
208                ContentFormat::BitwardenLegacyKey => EncodedSymmetricKey::BitwardenLegacyKey(
209                    BitwardenLegacyKeyBytes::from(key_bytes),
210                ),
211                ContentFormat::CoseKey => {
212                    EncodedSymmetricKey::CoseKey(CoseKeyBytes::from(key_bytes))
213                }
214                _ => {
215                    return Err(PasswordProtectedKeyEnvelopeError::ParsingError(
216                        "Unknown or unsupported content format".to_string(),
217                    ));
218                }
219            },
220        )
221        .map_err(|_| {
222            PasswordProtectedKeyEnvelopeError::ParsingError("Failed to decode key".to_string())
223        })
224    }
225
226    /// Re-seals the key with new KDF parameters (updated settings, salt), and a new password
227    pub fn reseal(
228        &self,
229        password: &str,
230        new_password: &str,
231    ) -> Result<Self, PasswordProtectedKeyEnvelopeError> {
232        let unsealed = self.unseal_ref(password)?;
233        Self::seal_ref(&unsealed, new_password)
234    }
235}
236
237impl<Ids: KeyIds> From<&PasswordProtectedKeyEnvelope<Ids>> for Vec<u8> {
238    fn from(val: &PasswordProtectedKeyEnvelope<Ids>) -> Self {
239        val.cose_encrypt
240            .clone()
241            .to_vec()
242            .expect("Serialization to cose should not fail")
243    }
244}
245
246impl<Ids: KeyIds> TryFrom<&Vec<u8>> for PasswordProtectedKeyEnvelope<Ids> {
247    type Error = CoseError;
248
249    fn try_from(value: &Vec<u8>) -> Result<Self, Self::Error> {
250        let cose_encrypt = coset::CoseEncrypt::from_slice(value)?;
251        Ok(PasswordProtectedKeyEnvelope {
252            _phantom: PhantomData,
253            cose_encrypt,
254        })
255    }
256}
257
258impl<Ids: KeyIds> std::fmt::Debug for PasswordProtectedKeyEnvelope<Ids> {
259    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260        f.debug_struct("PasswordProtectedKeyEnvelope")
261            .field("cose_encrypt", &self.cose_encrypt)
262            .finish()
263    }
264}
265
266impl<Ids: KeyIds> FromStr for PasswordProtectedKeyEnvelope<Ids> {
267    type Err = PasswordProtectedKeyEnvelopeError;
268
269    fn from_str(s: &str) -> Result<Self, Self::Err> {
270        let data = B64::try_from(s).map_err(|_| {
271            PasswordProtectedKeyEnvelopeError::ParsingError(
272                "Invalid PasswordProtectedKeyEnvelope Base64 encoding".to_string(),
273            )
274        })?;
275        Self::try_from(&data.as_bytes().to_vec()).map_err(|_| {
276            PasswordProtectedKeyEnvelopeError::ParsingError(
277                "Failed to parse PasswordProtectedKeyEnvelope".to_string(),
278            )
279        })
280    }
281}
282
283impl<Ids: KeyIds> From<PasswordProtectedKeyEnvelope<Ids>> for String {
284    fn from(val: PasswordProtectedKeyEnvelope<Ids>) -> Self {
285        let serialized: Vec<u8> = (&val).into();
286        B64::from(serialized).to_string()
287    }
288}
289
290impl<'de, Ids: KeyIds> Deserialize<'de> for PasswordProtectedKeyEnvelope<Ids> {
291    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
292    where
293        D: serde::Deserializer<'de>,
294    {
295        deserializer.deserialize_str(FromStrVisitor::new())
296    }
297}
298
299impl<Ids: KeyIds> Serialize for PasswordProtectedKeyEnvelope<Ids> {
300    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
301    where
302        S: serde::Serializer,
303    {
304        let serialized: Vec<u8> = self.into();
305        serializer.serialize_str(&B64::from(serialized).to_string())
306    }
307}
308
309/// Raw argon2 settings differ from the [crate::keys::Kdf::Argon2id] struct defined for existing
310/// master-password unlock. The memory is represented in kibibytes (KiB) instead of mebibytes (MiB),
311/// and the salt is a fixed size of 32 bytes, and randomly generated, instead of being derived from
312/// the email.
313struct Argon2RawSettings {
314    iterations: u32,
315    /// Memory in KiB
316    memory: u32,
317    parallelism: u32,
318    salt: [u8; ENVELOPE_ARGON2_SALT_SIZE],
319}
320
321impl Argon2RawSettings {
322    /// Creates default Argon2 settings based on the device. This currently is a static preset
323    /// based on the target os
324    fn local_kdf_settings() -> Self {
325        // iOS has memory limitations in the auto-fill context. So, the memory is halved
326        // but the iterations are doubled
327        if cfg!(target_os = "ios") {
328            // The SECOND RECOMMENDED option from: https://datatracker.ietf.org/doc/rfc9106/, with halved memory and doubled iteration count
329            Self {
330                iterations: 6,
331                memory: 32 * 1024, // 32 MiB
332                parallelism: 4,
333                salt: make_salt(),
334            }
335        } else {
336            // The SECOND RECOMMENDED option from: https://datatracker.ietf.org/doc/rfc9106/
337            // The FIRST RECOMMENDED option currently still has too much memory consumption for most
338            // clients except desktop.
339            Self {
340                iterations: 3,
341                memory: 64 * 1024, // 64 MiB
342                parallelism: 4,
343                salt: make_salt(),
344            }
345        }
346    }
347}
348
349impl From<&Argon2RawSettings> for Header {
350    fn from(settings: &Argon2RawSettings) -> Header {
351        let builder = HeaderBuilder::new()
352            .value(ARGON2_ITERATIONS, Integer::from(settings.iterations).into())
353            .value(ARGON2_MEMORY, Integer::from(settings.memory).into())
354            .value(
355                ARGON2_PARALLELISM,
356                Integer::from(settings.parallelism).into(),
357            )
358            .value(ARGON2_SALT, Value::from(settings.salt.to_vec()));
359
360        let mut header = builder.build();
361        header.alg = Some(coset::Algorithm::PrivateUse(ALG_ARGON2ID13));
362        header
363    }
364}
365
366impl TryInto<Params> for &Argon2RawSettings {
367    type Error = PasswordProtectedKeyEnvelopeError;
368
369    fn try_into(self) -> Result<Params, PasswordProtectedKeyEnvelopeError> {
370        Params::new(
371            self.memory,
372            self.iterations,
373            self.parallelism,
374            Some(ENVELOPE_ARGON2_OUTPUT_KEY_SIZE),
375        )
376        .map_err(|_| PasswordProtectedKeyEnvelopeError::KdfError)
377    }
378}
379
380impl TryInto<Argon2RawSettings> for &Header {
381    type Error = PasswordProtectedKeyEnvelopeError;
382
383    fn try_into(self) -> Result<Argon2RawSettings, PasswordProtectedKeyEnvelopeError> {
384        Ok(Argon2RawSettings {
385            iterations: extract_integer(self, ARGON2_ITERATIONS, "iterations")?.try_into()?,
386            memory: extract_integer(self, ARGON2_MEMORY, "memory")?.try_into()?,
387            parallelism: extract_integer(self, ARGON2_PARALLELISM, "parallelism")?.try_into()?,
388            salt: extract_bytes(self, ARGON2_SALT, "salt")?
389                .try_into()
390                .map_err(|_| {
391                    PasswordProtectedKeyEnvelopeError::ParsingError(
392                        "Invalid Argon2 salt".to_string(),
393                    )
394                })?,
395        })
396    }
397}
398
399fn make_salt() -> [u8; ENVELOPE_ARGON2_SALT_SIZE] {
400    let mut salt = [0u8; ENVELOPE_ARGON2_SALT_SIZE];
401    rand::thread_rng().fill_bytes(&mut salt);
402    salt
403}
404
405fn derive_key(
406    argon2_settings: &Argon2RawSettings,
407    password: &str,
408) -> Result<[u8; ENVELOPE_ARGON2_OUTPUT_KEY_SIZE], PasswordProtectedKeyEnvelopeError> {
409    use argon2::*;
410
411    let mut hash = [0u8; ENVELOPE_ARGON2_OUTPUT_KEY_SIZE];
412    Argon2::new(
413        Algorithm::Argon2id,
414        Version::V0x13,
415        argon2_settings.try_into()?,
416    )
417    .hash_password_into(password.as_bytes(), &argon2_settings.salt, &mut hash)
418    .map_err(|_| PasswordProtectedKeyEnvelopeError::KdfError)?;
419
420    Ok(hash)
421}
422
423/// Errors that can occur when sealing or unsealing a key with the `PasswordProtectedKeyEnvelope`.
424#[derive(Debug, Error)]
425pub enum PasswordProtectedKeyEnvelopeError {
426    /// The password provided is incorrect or the envelope was tampered with
427    #[error("Wrong password")]
428    WrongPassword,
429    /// The envelope could not be parsed correctly, or the KDF parameters are invalid
430    #[error("Parsing error {0}")]
431    ParsingError(String),
432    /// The KDF failed to derive a key, possibly due to invalid parameters or memory allocation
433    /// issues
434    #[error("Kdf error")]
435    KdfError,
436    /// There is no key for the provided key id in the key store
437    #[error("Key missing error")]
438    KeyMissingError,
439    /// The key store could not be written to, for example due to being read-only
440    #[error("Could not write to key store")]
441    KeyStoreError,
442}
443
444impl From<CoseExtractError> for PasswordProtectedKeyEnvelopeError {
445    fn from(err: CoseExtractError) -> Self {
446        let CoseExtractError::MissingValue(label) = err;
447        PasswordProtectedKeyEnvelopeError::ParsingError(format!("Missing value for {}", label))
448    }
449}
450
451impl From<TryFromIntError> for PasswordProtectedKeyEnvelopeError {
452    fn from(err: TryFromIntError) -> Self {
453        PasswordProtectedKeyEnvelopeError::ParsingError(format!("Invalid integer: {}", err))
454    }
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460    use crate::{
461        traits::tests::{TestIds, TestSymmKey},
462        KeyStore,
463    };
464
465    const TEST_UNSEALED_COSEKEY_ENCODED: &[u8] = &[
466        165, 1, 4, 2, 80, 63, 208, 189, 183, 204, 37, 72, 170, 179, 236, 190, 208, 22, 65, 227,
467        183, 3, 58, 0, 1, 17, 111, 4, 132, 3, 4, 5, 6, 32, 88, 32, 88, 25, 68, 85, 205, 28, 133,
468        28, 90, 147, 160, 145, 48, 3, 178, 184, 30, 11, 122, 132, 64, 59, 51, 233, 191, 117, 159,
469        117, 23, 168, 248, 36, 1,
470    ];
471    const TESTVECTOR_COSEKEY_ENVELOPE: &[u8] = &[
472        132, 68, 161, 3, 24, 101, 161, 5, 88, 24, 1, 31, 58, 230, 10, 92, 195, 233, 212, 7, 166,
473        252, 67, 115, 221, 58, 3, 191, 218, 188, 181, 192, 28, 11, 88, 84, 141, 183, 137, 167, 166,
474        161, 33, 82, 30, 255, 23, 10, 179, 149, 88, 24, 39, 60, 74, 232, 133, 44, 90, 98, 117, 31,
475        41, 69, 251, 76, 250, 141, 229, 83, 191, 6, 237, 107, 127, 93, 238, 110, 49, 125, 201, 37,
476        162, 120, 157, 32, 116, 195, 208, 143, 83, 254, 223, 93, 97, 158, 0, 24, 95, 197, 249, 35,
477        240, 3, 20, 71, 164, 97, 180, 29, 203, 69, 31, 151, 249, 244, 197, 91, 101, 174, 129, 131,
478        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,
479        90, 26, 0, 1, 0, 0, 58, 0, 1, 21, 91, 4, 58, 0, 1, 21, 88, 80, 165, 253, 56, 243, 255, 54,
480        246, 252, 231, 230, 33, 252, 49, 175, 1, 111, 246,
481    ];
482    const TEST_UNSEALED_LEGACYKEY_ENCODED: &[u8] = &[
483        135, 114, 97, 155, 115, 209, 215, 224, 175, 159, 231, 208, 15, 244, 40, 171, 239, 137, 57,
484        98, 207, 167, 231, 138, 145, 254, 28, 136, 236, 60, 23, 163, 4, 246, 219, 117, 104, 246,
485        86, 10, 152, 52, 90, 85, 58, 6, 70, 39, 111, 128, 93, 145, 143, 180, 77, 129, 178, 242, 82,
486        72, 57, 61, 192, 64,
487    ];
488    const TESTVECTOR_LEGACYKEY_ENVELOPE: &[u8] = &[
489        132, 88, 38, 161, 3, 120, 34, 97, 112, 112, 108, 105, 99, 97, 116, 105, 111, 110, 47, 120,
490        46, 98, 105, 116, 119, 97, 114, 100, 101, 110, 46, 108, 101, 103, 97, 99, 121, 45, 107,
491        101, 121, 161, 5, 88, 24, 218, 72, 22, 79, 149, 30, 12, 36, 180, 212, 44, 21, 167, 208,
492        214, 221, 7, 91, 178, 12, 104, 17, 45, 219, 88, 80, 114, 38, 14, 165, 85, 229, 103, 108,
493        17, 175, 41, 43, 203, 175, 119, 125, 227, 127, 163, 214, 213, 138, 12, 216, 163, 204, 38,
494        222, 47, 11, 44, 231, 239, 170, 63, 8, 249, 56, 102, 18, 134, 34, 232, 193, 44, 19, 228,
495        17, 187, 199, 238, 187, 2, 13, 30, 112, 103, 110, 5, 31, 238, 58, 4, 24, 19, 239, 135, 57,
496        206, 190, 144, 83, 128, 204, 59, 155, 21, 80, 180, 34, 129, 131, 71, 161, 1, 58, 0, 1, 21,
497        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,
498        1, 21, 91, 4, 58, 0, 1, 21, 88, 80, 212, 91, 185, 112, 92, 177, 108, 33, 182, 202, 26, 141,
499        11, 133, 95, 235, 246,
500    ];
501
502    const TESTVECTOR_PASSWORD: &str = "test_password";
503
504    #[test]
505    fn test_testvector_cosekey() {
506        let key_store = KeyStore::<TestIds>::default();
507        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
508        let envelope =
509            PasswordProtectedKeyEnvelope::try_from(&TESTVECTOR_COSEKEY_ENVELOPE.to_vec())
510                .expect("Key envelope should be valid");
511        envelope
512            .unseal(TestSymmKey::A(0), TESTVECTOR_PASSWORD, &mut ctx)
513            .expect("Unsealing should succeed");
514        #[allow(deprecated)]
515        let unsealed_key = ctx
516            .dangerous_get_symmetric_key(TestSymmKey::A(0))
517            .expect("Key should exist in the key store");
518        assert_eq!(
519            unsealed_key.to_encoded().to_vec(),
520            TEST_UNSEALED_COSEKEY_ENCODED
521        );
522    }
523
524    #[test]
525    fn test_testvector_legacykey() {
526        let key_store = KeyStore::<TestIds>::default();
527        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
528        let envelope =
529            PasswordProtectedKeyEnvelope::try_from(&TESTVECTOR_LEGACYKEY_ENVELOPE.to_vec())
530                .expect("Key envelope should be valid");
531        envelope
532            .unseal(TestSymmKey::A(0), TESTVECTOR_PASSWORD, &mut ctx)
533            .expect("Unsealing should succeed");
534        #[allow(deprecated)]
535        let unsealed_key = ctx
536            .dangerous_get_symmetric_key(TestSymmKey::A(0))
537            .expect("Key should exist in the key store");
538        assert_eq!(
539            unsealed_key.to_encoded().to_vec(),
540            TEST_UNSEALED_LEGACYKEY_ENCODED
541        );
542    }
543
544    #[test]
545    fn test_make_envelope() {
546        let key_store = KeyStore::<TestIds>::default();
547        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
548        let test_key = ctx.make_cose_symmetric_key(TestSymmKey::A(0)).unwrap();
549
550        let password = "test_password";
551
552        // Seal the key with a password
553        let envelope = PasswordProtectedKeyEnvelope::seal(test_key, password, &ctx).unwrap();
554        let serialized: Vec<u8> = (&envelope).into();
555
556        // Unseal the key from the envelope
557        let deserialized: PasswordProtectedKeyEnvelope<TestIds> =
558            PasswordProtectedKeyEnvelope::try_from(&serialized).unwrap();
559        deserialized
560            .unseal(TestSymmKey::A(1), password, &mut ctx)
561            .unwrap();
562
563        // Verify that the unsealed key matches the original key
564        #[allow(deprecated)]
565        let unsealed_key = ctx
566            .dangerous_get_symmetric_key(TestSymmKey::A(1))
567            .expect("Key should exist in the key store");
568
569        #[allow(deprecated)]
570        let key_before_sealing = ctx
571            .dangerous_get_symmetric_key(test_key)
572            .expect("Key should exist in the key store");
573
574        assert_eq!(unsealed_key, key_before_sealing);
575    }
576
577    #[test]
578    fn test_make_envelope_legacy_key() {
579        let key_store = KeyStore::<TestIds>::default();
580        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
581        let test_key = ctx.generate_symmetric_key(TestSymmKey::A(0)).unwrap();
582
583        let password = "test_password";
584
585        // Seal the key with a password
586        let envelope = PasswordProtectedKeyEnvelope::seal(test_key, password, &ctx).unwrap();
587        let serialized: Vec<u8> = (&envelope).into();
588
589        // Unseal the key from the envelope
590        let deserialized: PasswordProtectedKeyEnvelope<TestIds> =
591            PasswordProtectedKeyEnvelope::try_from(&serialized).unwrap();
592        deserialized
593            .unseal(TestSymmKey::A(1), password, &mut ctx)
594            .unwrap();
595
596        // Verify that the unsealed key matches the original key
597        #[allow(deprecated)]
598        let unsealed_key = ctx
599            .dangerous_get_symmetric_key(TestSymmKey::A(1))
600            .expect("Key should exist in the key store");
601
602        #[allow(deprecated)]
603        let key_before_sealing = ctx
604            .dangerous_get_symmetric_key(test_key)
605            .expect("Key should exist in the key store");
606
607        assert_eq!(unsealed_key, key_before_sealing);
608    }
609
610    #[test]
611    fn test_reseal_envelope() {
612        let key = SymmetricCryptoKey::make_xchacha20_poly1305_key();
613        let password = "test_password";
614        let new_password = "new_test_password";
615
616        // Seal the key with a password
617        let envelope: PasswordProtectedKeyEnvelope<TestIds> =
618            PasswordProtectedKeyEnvelope::seal_ref(&key, password).expect("Sealing should work");
619
620        // Reseal
621        let envelope = envelope
622            .reseal(password, new_password)
623            .expect("Resealing should work");
624        let unsealed = envelope
625            .unseal_ref(new_password)
626            .expect("Unsealing should work");
627
628        // Verify that the unsealed key matches the original key
629        assert_eq!(unsealed, key);
630    }
631
632    #[test]
633    fn test_wrong_password() {
634        let key_store = KeyStore::<TestIds>::default();
635        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
636        let test_key = ctx.make_cose_symmetric_key(TestSymmKey::A(0)).unwrap();
637
638        let password = "test_password";
639        let wrong_password = "wrong_password";
640
641        // Seal the key with a password
642        let envelope = PasswordProtectedKeyEnvelope::seal(test_key, password, &ctx).unwrap();
643
644        // Attempt to unseal with the wrong password
645        let deserialized: PasswordProtectedKeyEnvelope<TestIds> =
646            PasswordProtectedKeyEnvelope::try_from(&(&envelope).into()).unwrap();
647        assert!(matches!(
648            deserialized.unseal(TestSymmKey::A(1), wrong_password, &mut ctx),
649            Err(PasswordProtectedKeyEnvelopeError::WrongPassword)
650        ));
651    }
652}