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