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