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::Rng;
23use serde::{Deserialize, Serialize};
24use thiserror::Error;
25#[cfg(feature = "wasm")]
26use wasm_bindgen::convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi};
27
28use crate::{
29    BitwardenLegacyKeyBytes, ContentFormat, CoseKeyBytes, CryptoError, EncodedSymmetricKey,
30    KEY_ID_SIZE, KeySlotIds, 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: KeySlotIds>(
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: KeySlotIds>(
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::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#[cfg(feature = "wasm")]
538impl OptionFromWasmAbi for PasswordProtectedKeyEnvelope {
539    fn is_none(abi: &Self::Abi) -> bool {
540        <String as OptionFromWasmAbi>::is_none(abi)
541    }
542}
543
544#[cfg(feature = "wasm")]
545impl IntoWasmAbi for PasswordProtectedKeyEnvelope {
546    type Abi = <String as IntoWasmAbi>::Abi;
547
548    fn into_abi(self) -> Self::Abi {
549        let string: String = self.into();
550        string.into_abi()
551    }
552}
553
554/// The content-layer separation namespace for password protected key envelopes.
555#[derive(Debug, Clone, Copy, PartialEq, Eq)]
556pub enum PasswordProtectedKeyEnvelopeNamespace {
557    /// The namespace for unlocking vaults with a PIN.
558    PinUnlock = 1,
559    /// This namespace is only used in tests
560    #[cfg(test)]
561    ExampleNamespace = -1,
562    /// This namespace is only used in tests
563    #[cfg(test)]
564    ExampleNamespace2 = -2,
565}
566
567impl PasswordProtectedKeyEnvelopeNamespace {
568    /// Returns the numeric value of the namespace.
569    fn as_i64(&self) -> i64 {
570        *self as i64
571    }
572}
573
574impl TryFrom<i128> for PasswordProtectedKeyEnvelopeNamespace {
575    type Error = PasswordProtectedKeyEnvelopeError;
576
577    fn try_from(value: i128) -> Result<Self, Self::Error> {
578        match value {
579            1 => Ok(PasswordProtectedKeyEnvelopeNamespace::PinUnlock),
580            #[cfg(test)]
581            -1 => Ok(PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace),
582            #[cfg(test)]
583            -2 => Ok(PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace2),
584            _ => Err(PasswordProtectedKeyEnvelopeError::InvalidNamespace),
585        }
586    }
587}
588
589impl TryFrom<i64> for PasswordProtectedKeyEnvelopeNamespace {
590    type Error = PasswordProtectedKeyEnvelopeError;
591
592    fn try_from(value: i64) -> Result<Self, Self::Error> {
593        Self::try_from(i128::from(value))
594    }
595}
596
597impl From<PasswordProtectedKeyEnvelopeNamespace> for i128 {
598    fn from(val: PasswordProtectedKeyEnvelopeNamespace) -> Self {
599        val.as_i64().into()
600    }
601}
602
603impl ContentNamespace for PasswordProtectedKeyEnvelopeNamespace {}
604
605#[cfg(test)]
606mod tests {
607    use super::*;
608    use crate::{KeyStore, SymmetricKeyAlgorithm, traits::tests::TestIds};
609
610    const TEST_UNSEALED_COSEKEY_ENCODED: &[u8] = &[
611        165, 1, 4, 2, 80, 63, 208, 189, 183, 204, 37, 72, 170, 179, 236, 190, 208, 22, 65, 227,
612        183, 3, 58, 0, 1, 17, 111, 4, 132, 3, 4, 5, 6, 32, 88, 32, 88, 25, 68, 85, 205, 28, 133,
613        28, 90, 147, 160, 145, 48, 3, 178, 184, 30, 11, 122, 132, 64, 59, 51, 233, 191, 117, 159,
614        117, 23, 168, 248, 36, 1,
615    ];
616    const TESTVECTOR_COSEKEY_ENVELOPE: &[u8] = &[
617        132, 68, 161, 3, 24, 101, 161, 5, 88, 24, 1, 31, 58, 230, 10, 92, 195, 233, 212, 7, 166,
618        252, 67, 115, 221, 58, 3, 191, 218, 188, 181, 192, 28, 11, 88, 84, 141, 183, 137, 167, 166,
619        161, 33, 82, 30, 255, 23, 10, 179, 149, 88, 24, 39, 60, 74, 232, 133, 44, 90, 98, 117, 31,
620        41, 69, 251, 76, 250, 141, 229, 83, 191, 6, 237, 107, 127, 93, 238, 110, 49, 125, 201, 37,
621        162, 120, 157, 32, 116, 195, 208, 143, 83, 254, 223, 93, 97, 158, 0, 24, 95, 197, 249, 35,
622        240, 3, 20, 71, 164, 97, 180, 29, 203, 69, 31, 151, 249, 244, 197, 91, 101, 174, 129, 131,
623        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,
624        90, 26, 0, 1, 0, 0, 58, 0, 1, 21, 91, 4, 58, 0, 1, 21, 88, 80, 165, 253, 56, 243, 255, 54,
625        246, 252, 231, 230, 33, 252, 49, 175, 1, 111, 246,
626    ];
627    const TEST_UNSEALED_LEGACYKEY_ENCODED: &[u8] = &[
628        135, 114, 97, 155, 115, 209, 215, 224, 175, 159, 231, 208, 15, 244, 40, 171, 239, 137, 57,
629        98, 207, 167, 231, 138, 145, 254, 28, 136, 236, 60, 23, 163, 4, 246, 219, 117, 104, 246,
630        86, 10, 152, 52, 90, 85, 58, 6, 70, 39, 111, 128, 93, 145, 143, 180, 77, 129, 178, 242, 82,
631        72, 57, 61, 192, 64,
632    ];
633    const TESTVECTOR_LEGACYKEY_ENVELOPE: &[u8] = &[
634        132, 88, 38, 161, 3, 120, 34, 97, 112, 112, 108, 105, 99, 97, 116, 105, 111, 110, 47, 120,
635        46, 98, 105, 116, 119, 97, 114, 100, 101, 110, 46, 108, 101, 103, 97, 99, 121, 45, 107,
636        101, 121, 161, 5, 88, 24, 218, 72, 22, 79, 149, 30, 12, 36, 180, 212, 44, 21, 167, 208,
637        214, 221, 7, 91, 178, 12, 104, 17, 45, 219, 88, 80, 114, 38, 14, 165, 85, 229, 103, 108,
638        17, 175, 41, 43, 203, 175, 119, 125, 227, 127, 163, 214, 213, 138, 12, 216, 163, 204, 38,
639        222, 47, 11, 44, 231, 239, 170, 63, 8, 249, 56, 102, 18, 134, 34, 232, 193, 44, 19, 228,
640        17, 187, 199, 238, 187, 2, 13, 30, 112, 103, 110, 5, 31, 238, 58, 4, 24, 19, 239, 135, 57,
641        206, 190, 144, 83, 128, 204, 59, 155, 21, 80, 180, 34, 129, 131, 71, 161, 1, 58, 0, 1, 21,
642        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,
643        1, 21, 91, 4, 58, 0, 1, 21, 88, 80, 212, 91, 185, 112, 92, 177, 108, 33, 182, 202, 26, 141,
644        11, 133, 95, 235, 246,
645    ];
646
647    const TESTVECTOR_PASSWORD: &str = "test_password";
648
649    #[test]
650    #[ignore = "Manual test to verify debug format"]
651    fn test_debug() {
652        let key = SymmetricCryptoKey::make_xchacha20_poly1305_key();
653        let envelope = PasswordProtectedKeyEnvelope::seal_ref(
654            &key,
655            "test_password",
656            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
657        )
658        .unwrap();
659        println!("{:?}", envelope);
660    }
661
662    #[test]
663    fn test_testvector_cosekey() {
664        let key_store = KeyStore::<TestIds>::default();
665        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
666        let envelope =
667            PasswordProtectedKeyEnvelope::try_from(&TESTVECTOR_COSEKEY_ENVELOPE.to_vec())
668                .expect("Key envelope should be valid");
669        let key = envelope
670            .unseal(
671                TESTVECTOR_PASSWORD,
672                PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
673                &mut ctx,
674            )
675            .expect("Unsealing should succeed");
676        #[allow(deprecated)]
677        let unsealed_key = ctx
678            .dangerous_get_symmetric_key(key)
679            .expect("Key should exist in the key store");
680        assert_eq!(
681            unsealed_key.to_encoded().to_vec(),
682            TEST_UNSEALED_COSEKEY_ENCODED
683        );
684    }
685
686    #[test]
687    fn test_testvector_legacykey() {
688        let key_store = KeyStore::<TestIds>::default();
689        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
690        let envelope =
691            PasswordProtectedKeyEnvelope::try_from(&TESTVECTOR_LEGACYKEY_ENVELOPE.to_vec())
692                .expect("Key envelope should be valid");
693        let key = envelope
694            .unseal(
695                TESTVECTOR_PASSWORD,
696                PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
697                &mut ctx,
698            )
699            .expect("Unsealing should succeed");
700        #[allow(deprecated)]
701        let unsealed_key = ctx
702            .dangerous_get_symmetric_key(key)
703            .expect("Key should exist in the key store");
704        assert_eq!(
705            unsealed_key.to_encoded().to_vec(),
706            TEST_UNSEALED_LEGACYKEY_ENCODED
707        );
708    }
709
710    #[test]
711    fn test_make_envelope() {
712        let key_store = KeyStore::<TestIds>::default();
713        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
714        let test_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
715
716        let password = "test_password";
717
718        // Seal the key with a password
719        let envelope = PasswordProtectedKeyEnvelope::seal(
720            test_key,
721            password,
722            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
723            &ctx,
724        )
725        .unwrap();
726        let serialized: Vec<u8> = (&envelope).into();
727
728        // Unseal the key from the envelope
729        let deserialized: PasswordProtectedKeyEnvelope =
730            PasswordProtectedKeyEnvelope::try_from(&serialized).unwrap();
731        let key = deserialized
732            .unseal(
733                password,
734                PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
735                &mut ctx,
736            )
737            .unwrap();
738
739        // Verify that the unsealed key matches the original key
740        #[allow(deprecated)]
741        let unsealed_key = ctx
742            .dangerous_get_symmetric_key(key)
743            .expect("Key should exist in the key store");
744
745        #[allow(deprecated)]
746        let key_before_sealing = ctx
747            .dangerous_get_symmetric_key(test_key)
748            .expect("Key should exist in the key store");
749
750        assert_eq!(unsealed_key, key_before_sealing);
751    }
752
753    #[test]
754    fn test_make_envelope_legacy_key() {
755        let key_store = KeyStore::<TestIds>::default();
756        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
757        let test_key = ctx.generate_symmetric_key();
758
759        let password = "test_password";
760
761        // Seal the key with a password
762        let envelope = PasswordProtectedKeyEnvelope::seal(
763            test_key,
764            password,
765            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
766            &ctx,
767        )
768        .unwrap();
769        let serialized: Vec<u8> = (&envelope).into();
770
771        // Unseal the key from the envelope
772        let deserialized: PasswordProtectedKeyEnvelope =
773            PasswordProtectedKeyEnvelope::try_from(&serialized).unwrap();
774        let key = deserialized
775            .unseal(
776                password,
777                PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
778                &mut ctx,
779            )
780            .unwrap();
781
782        // Verify that the unsealed key matches the original key
783        #[allow(deprecated)]
784        let unsealed_key = ctx
785            .dangerous_get_symmetric_key(key)
786            .expect("Key should exist in the key store");
787
788        #[allow(deprecated)]
789        let key_before_sealing = ctx
790            .dangerous_get_symmetric_key(test_key)
791            .expect("Key should exist in the key store");
792
793        assert_eq!(unsealed_key, key_before_sealing);
794    }
795
796    #[test]
797    fn test_reseal_envelope() {
798        let key = SymmetricCryptoKey::make_xchacha20_poly1305_key();
799        let password = "test_password";
800        let new_password = "new_test_password";
801
802        // Seal the key with a password
803        let envelope: PasswordProtectedKeyEnvelope = PasswordProtectedKeyEnvelope::seal_ref(
804            &key,
805            password,
806            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
807        )
808        .expect("Sealing should work");
809
810        // Reseal
811        let envelope = envelope
812            .reseal(
813                password,
814                new_password,
815                PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
816            )
817            .expect("Resealing should work");
818        let unsealed = envelope
819            .unseal_ref(
820                new_password,
821                PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
822            )
823            .expect("Unsealing should work");
824
825        // Verify that the unsealed key matches the original key
826        assert_eq!(unsealed, key);
827    }
828
829    #[test]
830    fn test_wrong_password() {
831        let key_store = KeyStore::<TestIds>::default();
832        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
833        let test_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
834
835        let password = "test_password";
836        let wrong_password = "wrong_password";
837
838        // Seal the key with a password
839        let envelope = PasswordProtectedKeyEnvelope::seal(
840            test_key,
841            password,
842            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
843            &ctx,
844        )
845        .unwrap();
846
847        // Attempt to unseal with the wrong password
848        let deserialized: PasswordProtectedKeyEnvelope =
849            PasswordProtectedKeyEnvelope::try_from(&(&envelope).into()).unwrap();
850        assert!(matches!(
851            deserialized.unseal(
852                wrong_password,
853                PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
854                &mut ctx
855            ),
856            Err(PasswordProtectedKeyEnvelopeError::WrongPassword)
857        ));
858    }
859
860    #[test]
861    fn test_wrong_safe_namespace() {
862        let key_store = KeyStore::<TestIds>::default();
863        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
864        let test_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
865        let password = "test_password";
866
867        let mut envelope = PasswordProtectedKeyEnvelope::seal(
868            test_key,
869            password,
870            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
871            &ctx,
872        )
873        .expect("Seal works");
874
875        if let Some((_, value)) = envelope
876            .cose_encrypt
877            .protected
878            .header
879            .rest
880            .iter_mut()
881            .find(|(label, _)| {
882                matches!(label, coset::Label::Int(label_value) if *label_value == crate::cose::SAFE_OBJECT_NAMESPACE)
883            })
884        {
885            *value = Value::Integer((SafeObjectNamespace::DataEnvelope as i64).into());
886        }
887
888        let deserialized: PasswordProtectedKeyEnvelope =
889            PasswordProtectedKeyEnvelope::try_from(&(&envelope).into())
890                .expect("Envelope should be valid");
891
892        let a = deserialized.unseal(
893            password,
894            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
895            &mut ctx,
896        );
897        println!("Error: {a:?}");
898        assert!(matches!(
899            a,
900            Err(PasswordProtectedKeyEnvelopeError::InvalidNamespace)
901        ));
902    }
903
904    #[test]
905    fn test_key_id() {
906        let key_store = KeyStore::<TestIds>::default();
907        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
908        let test_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
909        #[allow(deprecated)]
910        let key_id = ctx
911            .dangerous_get_symmetric_key(test_key)
912            .unwrap()
913            .key_id()
914            .unwrap();
915
916        let password = "test_password";
917
918        // Seal the key with a password
919        let envelope = PasswordProtectedKeyEnvelope::seal(
920            test_key,
921            password,
922            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
923            &ctx,
924        )
925        .unwrap();
926        let contained_key_id = envelope.contained_key_id().unwrap();
927        assert_eq!(Some(key_id), contained_key_id);
928    }
929
930    #[test]
931    fn test_no_key_id() {
932        let key_store = KeyStore::<TestIds>::default();
933        let mut ctx: KeyStoreContext<'_, TestIds> = key_store.context_mut();
934        let test_key = ctx.generate_symmetric_key();
935
936        let password = "test_password";
937
938        // Seal the key with a password
939        let envelope = PasswordProtectedKeyEnvelope::seal(
940            test_key,
941            password,
942            PasswordProtectedKeyEnvelopeNamespace::ExampleNamespace,
943            &ctx,
944        )
945        .unwrap();
946        let contained_key_id = envelope.contained_key_id().unwrap();
947        assert_eq!(None, contained_key_id);
948    }
949}