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::{KeyStore, SymmetricKeyAlgorithm, traits::tests::TestIds};
469
470    const TEST_UNSEALED_COSEKEY_ENCODED: &[u8] = &[
471        165, 1, 4, 2, 80, 63, 208, 189, 183, 204, 37, 72, 170, 179, 236, 190, 208, 22, 65, 227,
472        183, 3, 58, 0, 1, 17, 111, 4, 132, 3, 4, 5, 6, 32, 88, 32, 88, 25, 68, 85, 205, 28, 133,
473        28, 90, 147, 160, 145, 48, 3, 178, 184, 30, 11, 122, 132, 64, 59, 51, 233, 191, 117, 159,
474        117, 23, 168, 248, 36, 1,
475    ];
476    const TESTVECTOR_COSEKEY_ENVELOPE: &[u8] = &[
477        132, 68, 161, 3, 24, 101, 161, 5, 88, 24, 1, 31, 58, 230, 10, 92, 195, 233, 212, 7, 166,
478        252, 67, 115, 221, 58, 3, 191, 218, 188, 181, 192, 28, 11, 88, 84, 141, 183, 137, 167, 166,
479        161, 33, 82, 30, 255, 23, 10, 179, 149, 88, 24, 39, 60, 74, 232, 133, 44, 90, 98, 117, 31,
480        41, 69, 251, 76, 250, 141, 229, 83, 191, 6, 237, 107, 127, 93, 238, 110, 49, 125, 201, 37,
481        162, 120, 157, 32, 116, 195, 208, 143, 83, 254, 223, 93, 97, 158, 0, 24, 95, 197, 249, 35,
482        240, 3, 20, 71, 164, 97, 180, 29, 203, 69, 31, 151, 249, 244, 197, 91, 101, 174, 129, 131,
483        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,
484        90, 26, 0, 1, 0, 0, 58, 0, 1, 21, 91, 4, 58, 0, 1, 21, 88, 80, 165, 253, 56, 243, 255, 54,
485        246, 252, 231, 230, 33, 252, 49, 175, 1, 111, 246,
486    ];
487    const TEST_UNSEALED_LEGACYKEY_ENCODED: &[u8] = &[
488        135, 114, 97, 155, 115, 209, 215, 224, 175, 159, 231, 208, 15, 244, 40, 171, 239, 137, 57,
489        98, 207, 167, 231, 138, 145, 254, 28, 136, 236, 60, 23, 163, 4, 246, 219, 117, 104, 246,
490        86, 10, 152, 52, 90, 85, 58, 6, 70, 39, 111, 128, 93, 145, 143, 180, 77, 129, 178, 242, 82,
491        72, 57, 61, 192, 64,
492    ];
493    const TESTVECTOR_LEGACYKEY_ENVELOPE: &[u8] = &[
494        132, 88, 38, 161, 3, 120, 34, 97, 112, 112, 108, 105, 99, 97, 116, 105, 111, 110, 47, 120,
495        46, 98, 105, 116, 119, 97, 114, 100, 101, 110, 46, 108, 101, 103, 97, 99, 121, 45, 107,
496        101, 121, 161, 5, 88, 24, 218, 72, 22, 79, 149, 30, 12, 36, 180, 212, 44, 21, 167, 208,
497        214, 221, 7, 91, 178, 12, 104, 17, 45, 219, 88, 80, 114, 38, 14, 165, 85, 229, 103, 108,
498        17, 175, 41, 43, 203, 175, 119, 125, 227, 127, 163, 214, 213, 138, 12, 216, 163, 204, 38,
499        222, 47, 11, 44, 231, 239, 170, 63, 8, 249, 56, 102, 18, 134, 34, 232, 193, 44, 19, 228,
500        17, 187, 199, 238, 187, 2, 13, 30, 112, 103, 110, 5, 31, 238, 58, 4, 24, 19, 239, 135, 57,
501        206, 190, 144, 83, 128, 204, 59, 155, 21, 80, 180, 34, 129, 131, 71, 161, 1, 58, 0, 1, 21,
502        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,
503        1, 21, 91, 4, 58, 0, 1, 21, 88, 80, 212, 91, 185, 112, 92, 177, 108, 33, 182, 202, 26, 141,
504        11, 133, 95, 235, 246,
505    ];
506
507    const TESTVECTOR_PASSWORD: &str = "test_password";
508
509    #[test]
510    fn test_testvector_cosekey() {
511        let key_store = KeyStore::<TestIds>::default();
512        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
513        let envelope =
514            PasswordProtectedKeyEnvelope::try_from(&TESTVECTOR_COSEKEY_ENVELOPE.to_vec())
515                .expect("Key envelope should be valid");
516        let key = envelope
517            .unseal(TESTVECTOR_PASSWORD, &mut ctx)
518            .expect("Unsealing should succeed");
519        #[allow(deprecated)]
520        let unsealed_key = ctx
521            .dangerous_get_symmetric_key(key)
522            .expect("Key should exist in the key store");
523        assert_eq!(
524            unsealed_key.to_encoded().to_vec(),
525            TEST_UNSEALED_COSEKEY_ENCODED
526        );
527    }
528
529    #[test]
530    fn test_testvector_legacykey() {
531        let key_store = KeyStore::<TestIds>::default();
532        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
533        let envelope =
534            PasswordProtectedKeyEnvelope::try_from(&TESTVECTOR_LEGACYKEY_ENVELOPE.to_vec())
535                .expect("Key envelope should be valid");
536        let key = envelope
537            .unseal(TESTVECTOR_PASSWORD, &mut ctx)
538            .expect("Unsealing should succeed");
539        #[allow(deprecated)]
540        let unsealed_key = ctx
541            .dangerous_get_symmetric_key(key)
542            .expect("Key should exist in the key store");
543        assert_eq!(
544            unsealed_key.to_encoded().to_vec(),
545            TEST_UNSEALED_LEGACYKEY_ENCODED
546        );
547    }
548
549    #[test]
550    fn test_make_envelope() {
551        let key_store = KeyStore::<TestIds>::default();
552        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
553        let test_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
554
555        let password = "test_password";
556
557        // Seal the key with a password
558        let envelope = PasswordProtectedKeyEnvelope::seal(test_key, password, &ctx).unwrap();
559        let serialized: Vec<u8> = (&envelope).into();
560
561        // Unseal the key from the envelope
562        let deserialized: PasswordProtectedKeyEnvelope =
563            PasswordProtectedKeyEnvelope::try_from(&serialized).unwrap();
564        let key = deserialized.unseal(password, &mut ctx).unwrap();
565
566        // Verify that the unsealed key matches the original key
567        #[allow(deprecated)]
568        let unsealed_key = ctx
569            .dangerous_get_symmetric_key(key)
570            .expect("Key should exist in the key store");
571
572        #[allow(deprecated)]
573        let key_before_sealing = ctx
574            .dangerous_get_symmetric_key(test_key)
575            .expect("Key should exist in the key store");
576
577        assert_eq!(unsealed_key, key_before_sealing);
578    }
579
580    #[test]
581    fn test_make_envelope_legacy_key() {
582        let key_store = KeyStore::<TestIds>::default();
583        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
584        let test_key = ctx.generate_symmetric_key();
585
586        let password = "test_password";
587
588        // Seal the key with a password
589        let envelope = PasswordProtectedKeyEnvelope::seal(test_key, password, &ctx).unwrap();
590        let serialized: Vec<u8> = (&envelope).into();
591
592        // Unseal the key from the envelope
593        let deserialized: PasswordProtectedKeyEnvelope =
594            PasswordProtectedKeyEnvelope::try_from(&serialized).unwrap();
595        let key = deserialized.unseal(password, &mut ctx).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(key)
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 =
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_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
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 =
647            PasswordProtectedKeyEnvelope::try_from(&(&envelope).into()).unwrap();
648        assert!(matches!(
649            deserialized.unseal(wrong_password, &mut ctx),
650            Err(PasswordProtectedKeyEnvelopeError::WrongPassword)
651        ));
652    }
653}