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