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