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