Skip to main content

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