Skip to main content

cdx_core/security/
encryption.rs

1//! Encryption support using AES-256-GCM and ChaCha20-Poly1305.
2//!
3//! This module provides encryption and decryption capabilities for Codex documents
4//! using authenticated encryption algorithms (AEAD).
5
6use serde::{Deserialize, Serialize};
7
8use crate::error::{encryption_error, invalid_manifest};
9use crate::Result;
10
11/// Encryption algorithm enumeration.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, strum::Display)]
13pub enum EncryptionAlgorithm {
14    /// AES-256-GCM (required).
15    #[serde(rename = "AES-256-GCM")]
16    #[strum(serialize = "AES-256-GCM")]
17    Aes256Gcm,
18    /// ChaCha20-Poly1305 (optional).
19    #[serde(rename = "ChaCha20-Poly1305")]
20    #[strum(serialize = "ChaCha20-Poly1305")]
21    ChaCha20Poly1305,
22}
23
24impl EncryptionAlgorithm {
25    /// Get the algorithm identifier string.
26    #[must_use]
27    pub const fn as_str(&self) -> &'static str {
28        match self {
29            Self::Aes256Gcm => "AES-256-GCM",
30            Self::ChaCha20Poly1305 => "ChaCha20-Poly1305",
31        }
32    }
33
34    /// Get the key size in bytes.
35    #[must_use]
36    pub const fn key_size(&self) -> usize {
37        match self {
38            // Both algorithms use 256-bit keys
39            Self::Aes256Gcm | Self::ChaCha20Poly1305 => 32,
40        }
41    }
42
43    /// Get the nonce size in bytes.
44    #[must_use]
45    pub const fn nonce_size(&self) -> usize {
46        match self {
47            // Both algorithms use 96-bit nonces
48            Self::Aes256Gcm | Self::ChaCha20Poly1305 => 12,
49        }
50    }
51}
52
53/// Encryption metadata stored in the document.
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "camelCase")]
56pub struct EncryptionMetadata {
57    /// Encryption algorithm used.
58    pub algorithm: EncryptionAlgorithm,
59
60    /// Key derivation function (if password-based).
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub kdf: Option<KeyDerivation>,
63
64    /// Encrypted content key (if key wrapping is used).
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub wrapped_key: Option<String>,
67
68    /// Key management algorithm (for key wrapping).
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub key_management: Option<KeyManagementAlgorithm>,
71
72    /// Recipients who can decrypt (for multi-recipient encryption).
73    #[serde(default, skip_serializing_if = "Vec::is_empty")]
74    pub recipients: Vec<Recipient>,
75}
76
77/// Key management algorithm for key wrapping.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
79pub enum KeyManagementAlgorithm {
80    /// ECDH-ES with AES-256 Key Wrap.
81    #[serde(rename = "ECDH-ES+A256KW")]
82    EcdhEsA256kw,
83    /// RSA-OAEP with SHA-256.
84    #[serde(rename = "RSA-OAEP-256")]
85    RsaOaep256,
86    /// PBES2-HS256 with AES-256 Key Wrap (password-based).
87    #[serde(rename = "PBES2-HS256+A256KW")]
88    Pbes2HsA256kw,
89}
90
91/// Key derivation function parameters.
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase")]
94pub struct KeyDerivation {
95    /// KDF algorithm.
96    pub algorithm: KdfAlgorithm,
97
98    /// Salt (base64 encoded).
99    pub salt: String,
100
101    /// Iteration count (for PBKDF2).
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub iterations: Option<u32>,
104
105    /// Memory parameter (for Argon2).
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub memory: Option<u32>,
108
109    /// Parallelism parameter (for Argon2).
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    pub parallelism: Option<u32>,
112}
113
114/// Key derivation algorithm.
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
116pub enum KdfAlgorithm {
117    /// PBKDF2 with HMAC-SHA256.
118    #[serde(rename = "PBKDF2-SHA256")]
119    Pbkdf2Sha256,
120    /// Argon2id (recommended).
121    Argon2id,
122}
123
124/// Recipient information for multi-recipient encryption.
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
126#[serde(rename_all = "camelCase")]
127pub struct Recipient {
128    /// Recipient identifier (e.g., key ID, email).
129    pub id: String,
130
131    /// Encrypted content key for this recipient (base64 encoded).
132    pub encrypted_key: String,
133
134    /// Key encryption algorithm.
135    #[serde(default, skip_serializing_if = "Option::is_none")]
136    pub algorithm: Option<String>,
137
138    /// Ephemeral public key (base64-encoded, for ECDH-ES key agreement).
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub ephemeral_public_key: Option<String>,
141}
142
143/// Result of encryption operation.
144#[derive(Debug, Clone)]
145pub struct EncryptedData {
146    /// The encrypted ciphertext.
147    pub ciphertext: Vec<u8>,
148
149    /// The nonce used for encryption.
150    pub nonce: Vec<u8>,
151
152    /// Authentication tag (included in ciphertext for GCM).
153    pub tag: Vec<u8>,
154}
155
156/// AES-256-GCM encryptor.
157#[cfg(feature = "encryption")]
158pub struct Aes256GcmEncryptor {
159    key: [u8; 32],
160}
161
162#[cfg(feature = "encryption")]
163#[allow(clippy::missing_panics_doc)] // getrandom::fill only fails on misconfigured systems
164impl Aes256GcmEncryptor {
165    /// Create a new encryptor with the given key.
166    ///
167    /// # Errors
168    ///
169    /// Returns an error if the key is not 32 bytes.
170    pub fn new(key: &[u8]) -> Result<Self> {
171        let key: [u8; 32] = key.try_into().map_err(|_| {
172            invalid_manifest(format!(
173                "Invalid key length: expected 32 bytes, got {}",
174                key.len()
175            ))
176        })?;
177        Ok(Self { key })
178    }
179
180    /// Generate a new random encryption key.
181    #[must_use]
182    pub fn generate_key() -> [u8; 32] {
183        let mut key = [0u8; 32];
184        getrandom::fill(&mut key).expect("system RNG failed");
185        key
186    }
187
188    /// Generate a random nonce.
189    #[must_use]
190    pub fn generate_nonce() -> [u8; 12] {
191        let mut nonce = [0u8; 12];
192        getrandom::fill(&mut nonce).expect("system RNG failed");
193        nonce
194    }
195
196    /// Encrypt data with a random nonce.
197    ///
198    /// # Errors
199    ///
200    /// Returns an error if encryption fails.
201    pub fn encrypt(&self, plaintext: &[u8]) -> Result<EncryptedData> {
202        self.encrypt_with_nonce(plaintext, &Self::generate_nonce())
203    }
204
205    /// Encrypt data with a specific nonce.
206    ///
207    /// # Errors
208    ///
209    /// Returns an error if encryption fails.
210    pub fn encrypt_with_nonce(&self, plaintext: &[u8], nonce: &[u8; 12]) -> Result<EncryptedData> {
211        use aes_gcm::{
212            aead::{Aead, KeyInit},
213            Aes256Gcm, Nonce,
214        };
215
216        let cipher = Aes256Gcm::new_from_slice(&self.key)
217            .map_err(|e| invalid_manifest(format!("Failed to create cipher: {e}")))?;
218
219        let nonce_obj = Nonce::from(*nonce);
220        let ciphertext = cipher
221            .encrypt(&nonce_obj, plaintext)
222            .map_err(|e| invalid_manifest(format!("Encryption failed: {e}")))?;
223
224        // GCM appends the tag to the ciphertext
225        let tag_start = ciphertext.len().saturating_sub(16);
226        let tag = ciphertext[tag_start..].to_vec();
227
228        Ok(EncryptedData {
229            ciphertext,
230            nonce: nonce.to_vec(),
231            tag,
232        })
233    }
234
235    /// Decrypt data.
236    ///
237    /// # Errors
238    ///
239    /// Returns an error if decryption fails (wrong key or tampered data).
240    pub fn decrypt(&self, ciphertext: &[u8], nonce: &[u8]) -> Result<Vec<u8>> {
241        use aes_gcm::{
242            aead::{Aead, KeyInit},
243            Aes256Gcm, Nonce,
244        };
245
246        let nonce: [u8; 12] = nonce.try_into().map_err(|_| {
247            invalid_manifest(format!(
248                "Invalid nonce length: expected 12 bytes, got {}",
249                nonce.len()
250            ))
251        })?;
252
253        let cipher = Aes256Gcm::new_from_slice(&self.key)
254            .map_err(|e| invalid_manifest(format!("Failed to create cipher: {e}")))?;
255
256        let nonce_obj = Nonce::from(nonce);
257        cipher
258            .decrypt(&nonce_obj, ciphertext)
259            .map_err(|e| invalid_manifest(format!("Decryption failed: {e}")))
260    }
261}
262
263/// ChaCha20-Poly1305 encryptor.
264#[cfg(feature = "encryption-chacha")]
265pub struct ChaCha20Poly1305Encryptor {
266    key: [u8; 32],
267}
268
269#[cfg(feature = "encryption-chacha")]
270#[allow(clippy::missing_panics_doc)] // getrandom::fill only fails on misconfigured systems
271impl ChaCha20Poly1305Encryptor {
272    /// Create a new encryptor with the given key.
273    ///
274    /// # Errors
275    ///
276    /// Returns an error if the key is not 32 bytes.
277    pub fn new(key: &[u8]) -> Result<Self> {
278        let key: [u8; 32] = key.try_into().map_err(|_| {
279            invalid_manifest(format!(
280                "Invalid key length: expected 32 bytes, got {}",
281                key.len()
282            ))
283        })?;
284        Ok(Self { key })
285    }
286
287    /// Generate a new random encryption key.
288    #[must_use]
289    pub fn generate_key() -> [u8; 32] {
290        let mut key = [0u8; 32];
291        getrandom::fill(&mut key).expect("system RNG failed");
292        key
293    }
294
295    /// Generate a random nonce.
296    #[must_use]
297    pub fn generate_nonce() -> [u8; 12] {
298        let mut nonce = [0u8; 12];
299        getrandom::fill(&mut nonce).expect("system RNG failed");
300        nonce
301    }
302
303    /// Encrypt data with a random nonce.
304    ///
305    /// # Errors
306    ///
307    /// Returns an error if encryption fails.
308    pub fn encrypt(&self, plaintext: &[u8]) -> Result<EncryptedData> {
309        self.encrypt_with_nonce(plaintext, &Self::generate_nonce())
310    }
311
312    /// Encrypt data with a specific nonce.
313    ///
314    /// # Errors
315    ///
316    /// Returns an error if encryption fails.
317    pub fn encrypt_with_nonce(&self, plaintext: &[u8], nonce: &[u8; 12]) -> Result<EncryptedData> {
318        use chacha20poly1305::{
319            aead::{Aead, KeyInit},
320            ChaCha20Poly1305, Nonce,
321        };
322
323        let cipher = ChaCha20Poly1305::new_from_slice(&self.key)
324            .map_err(|e| invalid_manifest(format!("Failed to create cipher: {e}")))?;
325
326        let nonce_obj = Nonce::from(*nonce);
327        let ciphertext = cipher
328            .encrypt(&nonce_obj, plaintext)
329            .map_err(|e| invalid_manifest(format!("Encryption failed: {e}")))?;
330
331        // Poly1305 appends the tag to the ciphertext (16 bytes)
332        let tag_start = ciphertext.len().saturating_sub(16);
333        let tag = ciphertext[tag_start..].to_vec();
334
335        Ok(EncryptedData {
336            ciphertext,
337            nonce: nonce.to_vec(),
338            tag,
339        })
340    }
341
342    /// Decrypt data.
343    ///
344    /// # Errors
345    ///
346    /// Returns an error if decryption fails (wrong key or tampered data).
347    pub fn decrypt(&self, ciphertext: &[u8], nonce: &[u8]) -> Result<Vec<u8>> {
348        use chacha20poly1305::{
349            aead::{Aead, KeyInit},
350            ChaCha20Poly1305, Nonce,
351        };
352
353        let nonce: [u8; 12] = nonce.try_into().map_err(|_| {
354            invalid_manifest(format!(
355                "Invalid nonce length: expected 12 bytes, got {}",
356                nonce.len()
357            ))
358        })?;
359
360        let cipher = ChaCha20Poly1305::new_from_slice(&self.key)
361            .map_err(|e| invalid_manifest(format!("Failed to create cipher: {e}")))?;
362
363        let nonce_obj = Nonce::from(nonce);
364        cipher
365            .decrypt(&nonce_obj, ciphertext)
366            .map_err(|e| invalid_manifest(format!("Decryption failed: {e}")))
367    }
368}
369
370// ---------------------------------------------------------------------------
371// ECDH-ES+A256KW key wrapping (RFC 3394 / RFC 7518)
372// ---------------------------------------------------------------------------
373
374/// Result of wrapping a content encryption key.
375#[cfg(feature = "key-wrapping")]
376#[derive(Debug, Clone)]
377pub struct WrappedKeyData {
378    /// The wrapped (encrypted) content encryption key.
379    pub wrapped_key: Vec<u8>,
380    /// The ephemeral public key (SEC1 uncompressed point), for transmission to the recipient.
381    pub ephemeral_public_key: Vec<u8>,
382}
383
384/// ECDH-ES+A256KW key wrapper (sender side).
385///
386/// Generates an ephemeral P-256 keypair, performs ECDH to derive
387/// a shared secret, runs HKDF-SHA256 to derive a 256-bit KEK,
388/// then wraps the content encryption key with AES Key Wrap (RFC 3394).
389#[cfg(feature = "key-wrapping")]
390pub struct EcdhEsKeyWrapper {
391    /// Recipient's P-256 public key.
392    recipient_public_key: p256::PublicKey,
393}
394
395#[cfg(feature = "key-wrapping")]
396impl EcdhEsKeyWrapper {
397    /// Create a key wrapper for the given recipient public key.
398    ///
399    /// The public key should be the recipient's P-256 (secp256r1) public key.
400    #[must_use]
401    pub fn new(recipient_public_key: p256::PublicKey) -> Self {
402        Self {
403            recipient_public_key,
404        }
405    }
406
407    /// Wrap a content encryption key for the recipient.
408    ///
409    /// Performs ECDH-ES+A256KW:
410    /// 1. Generate ephemeral P-256 keypair
411    /// 2. ECDH key agreement with recipient's public key
412    /// 3. HKDF-SHA256 to derive a 256-bit KEK
413    /// 4. AES Key Wrap (RFC 3394) the content encryption key
414    ///
415    /// # Errors
416    ///
417    /// Returns an error if the content key length is not a multiple of 8 bytes
418    /// (as required by AES Key Wrap), or if any cryptographic operation fails.
419    pub fn wrap(&self, content_key: &[u8]) -> Result<WrappedKeyData> {
420        use aes_kw::{cipher::KeyInit, KwAes256};
421        use hkdf::Hkdf;
422        use p256::ecdh::EphemeralSecret;
423        use p256::elliptic_curve::Generate;
424        use sha2::Sha256;
425
426        // 1. Generate ephemeral keypair
427        let ephemeral_secret = EphemeralSecret::generate();
428        let ephemeral_public = p256::PublicKey::from(&ephemeral_secret);
429
430        // 2. ECDH key agreement
431        let shared_secret = ephemeral_secret.diffie_hellman(&self.recipient_public_key);
432
433        // 3. HKDF-SHA256 to derive KEK
434        //    info string per RFC 7518 §4.6.2 "ECDH-ES+A256KW"
435        let hkdf = Hkdf::<Sha256>::new(None, shared_secret.raw_secret_bytes());
436        let mut kek_bytes = [0u8; 32];
437        hkdf.expand(b"ECDH-ES+A256KW", &mut kek_bytes)
438            .map_err(|e| encryption_error(format!("HKDF expansion failed: {e}")))?;
439
440        // 4. AES Key Wrap (RFC 3394)
441        let kek = KwAes256::new(&kek_bytes.into());
442        let mut wrapped = vec![0u8; content_key.len() + 8]; // AES-KW adds 8-byte IV
443        kek.wrap_key(content_key, &mut wrapped)
444            .map_err(|e| encryption_error(format!("AES key wrap failed: {e}")))?;
445
446        // Encode ephemeral public key as SEC1 uncompressed point
447        let ephemeral_public_bytes = ephemeral_public.to_sec1_bytes().to_vec();
448
449        Ok(WrappedKeyData {
450            wrapped_key: wrapped,
451            ephemeral_public_key: ephemeral_public_bytes,
452        })
453    }
454}
455
456/// ECDH-ES+A256KW key unwrapper (recipient side).
457///
458/// Uses the recipient's private key and the sender's ephemeral public key
459/// to reverse the ECDH-ES+A256KW key wrapping and recover the content
460/// encryption key.
461#[cfg(feature = "key-wrapping")]
462pub struct EcdhEsKeyUnwrapper {
463    /// Recipient's P-256 secret key.
464    recipient_secret: p256::SecretKey,
465}
466
467#[cfg(feature = "key-wrapping")]
468impl EcdhEsKeyUnwrapper {
469    /// Create a key unwrapper with the recipient's secret key.
470    #[must_use]
471    pub fn new(recipient_secret: p256::SecretKey) -> Self {
472        Self { recipient_secret }
473    }
474
475    /// Unwrap a content encryption key.
476    ///
477    /// # Errors
478    ///
479    /// Returns an error if the ephemeral public key is invalid, the wrapped
480    /// key is corrupted, or any cryptographic operation fails.
481    pub fn unwrap(&self, data: &WrappedKeyData) -> Result<Vec<u8>> {
482        use aes_kw::{cipher::KeyInit, KwAes256};
483        use hkdf::Hkdf;
484        use sha2::Sha256;
485
486        // 1. Decode the ephemeral public key from SEC1 bytes
487        let ephemeral_public = p256::PublicKey::from_sec1_bytes(&data.ephemeral_public_key)
488            .map_err(|e| encryption_error(format!("Invalid ephemeral public key: {e}")))?;
489
490        // 2. ECDH key agreement using recipient's secret key
491        let shared_secret = p256::ecdh::diffie_hellman(
492            self.recipient_secret.to_nonzero_scalar(),
493            ephemeral_public.as_affine(),
494        );
495
496        // 3. HKDF-SHA256 to derive KEK (same parameters as wrap)
497        let hkdf = Hkdf::<Sha256>::new(None, shared_secret.raw_secret_bytes());
498        let mut kek_bytes = [0u8; 32];
499        hkdf.expand(b"ECDH-ES+A256KW", &mut kek_bytes)
500            .map_err(|e| encryption_error(format!("HKDF expansion failed: {e}")))?;
501
502        // 4. AES Key Unwrap (wrapped key is original_len + 8 bytes)
503        let kek = KwAes256::new(&kek_bytes.into());
504        let unwrapped_len = data
505            .wrapped_key
506            .len()
507            .checked_sub(8)
508            .ok_or_else(|| encryption_error("Wrapped key too short"))?;
509        let mut unwrapped = vec![0u8; unwrapped_len];
510        kek.unwrap_key(&data.wrapped_key, &mut unwrapped)
511            .map_err(|e| encryption_error(format!("AES key unwrap failed: {e}")))?;
512        Ok(unwrapped)
513    }
514}
515
516// ---------------------------------------------------------------------------
517// RSA-OAEP-256 key wrapping
518// ---------------------------------------------------------------------------
519
520/// Result of wrapping a content encryption key with RSA-OAEP.
521#[cfg(feature = "key-wrapping-rsa")]
522#[derive(Debug, Clone)]
523pub struct RsaWrappedKeyData {
524    /// The wrapped (encrypted) content encryption key.
525    pub wrapped_key: Vec<u8>,
526}
527
528/// RSA-OAEP-256 key wrapper (sender side).
529///
530/// Encrypts a content encryption key using the recipient's RSA public key
531/// with OAEP padding and SHA-256 as the hash function.
532#[cfg(feature = "key-wrapping-rsa")]
533pub struct RsaOaepKeyWrapper {
534    recipient_public_key: rsa::RsaPublicKey,
535}
536
537#[cfg(feature = "key-wrapping-rsa")]
538impl RsaOaepKeyWrapper {
539    /// Create a key wrapper for the given recipient RSA public key.
540    #[must_use]
541    pub fn new(recipient_public_key: rsa::RsaPublicKey) -> Self {
542        Self {
543            recipient_public_key,
544        }
545    }
546
547    /// Wrap a content encryption key for the recipient.
548    ///
549    /// # Errors
550    ///
551    /// Returns an error if RSA-OAEP encryption fails (e.g., key too small for payload).
552    pub fn wrap(&self, content_key: &[u8]) -> Result<RsaWrappedKeyData> {
553        use rsa::oaep::EncryptingKey;
554        use rsa::sha2::Sha256;
555        use rsa::traits::RandomizedEncryptor;
556
557        let encrypting_key = EncryptingKey::<Sha256>::new(self.recipient_public_key.clone());
558        let wrapped_key = encrypting_key
559            .encrypt_with_rng(&mut rand_core::UnwrapErr(getrandom::SysRng), content_key)
560            .map_err(|e| encryption_error(format!("RSA-OAEP wrap failed: {e}")))?;
561
562        Ok(RsaWrappedKeyData { wrapped_key })
563    }
564}
565
566/// RSA-OAEP-256 key unwrapper (recipient side).
567///
568/// Decrypts a content encryption key using the recipient's RSA private key.
569#[cfg(feature = "key-wrapping-rsa")]
570pub struct RsaOaepKeyUnwrapper {
571    recipient_private_key: rsa::RsaPrivateKey,
572}
573
574#[cfg(feature = "key-wrapping-rsa")]
575impl RsaOaepKeyUnwrapper {
576    /// Create a key unwrapper with the recipient's RSA private key.
577    #[must_use]
578    pub fn new(recipient_private_key: rsa::RsaPrivateKey) -> Self {
579        Self {
580            recipient_private_key,
581        }
582    }
583
584    /// Unwrap a content encryption key.
585    ///
586    /// # Errors
587    ///
588    /// Returns an error if RSA-OAEP decryption fails (wrong key or tampered data).
589    pub fn unwrap(&self, data: &RsaWrappedKeyData) -> Result<Vec<u8>> {
590        use rsa::oaep::DecryptingKey;
591        use rsa::sha2::Sha256;
592        use rsa::traits::Decryptor;
593
594        let decrypting_key = DecryptingKey::<Sha256>::new(self.recipient_private_key.clone());
595        decrypting_key
596            .decrypt(&data.wrapped_key)
597            .map_err(|e| encryption_error(format!("RSA-OAEP unwrap failed: {e}")))
598    }
599}
600
601// ---------------------------------------------------------------------------
602// PBES2-HS256+A256KW password-based key wrapping
603// ---------------------------------------------------------------------------
604
605/// Result of wrapping a content encryption key with PBES2.
606#[cfg(feature = "key-wrapping-pbes2")]
607#[derive(Debug, Clone)]
608pub struct Pbes2WrappedKeyData {
609    /// The wrapped (encrypted) content encryption key.
610    pub wrapped_key: Vec<u8>,
611    /// The salt used for key derivation.
612    pub salt: Vec<u8>,
613    /// The PBKDF2 iteration count.
614    pub iterations: u32,
615}
616
617/// PBES2-HS256+A256KW key wrapper (password-based).
618///
619/// Derives a 256-bit KEK from a password using PBKDF2-HMAC-SHA256,
620/// then wraps the content encryption key with AES Key Wrap (RFC 3394).
621#[cfg(feature = "key-wrapping-pbes2")]
622pub struct Pbes2KeyWrapper {
623    password: Vec<u8>,
624    iterations: u32,
625}
626
627#[cfg(feature = "key-wrapping-pbes2")]
628impl Pbes2KeyWrapper {
629    /// Default PBKDF2 iteration count (600,000).
630    pub const DEFAULT_ITERATIONS: u32 = 600_000;
631
632    /// Create a key wrapper with the given password and iteration count.
633    #[must_use]
634    pub fn new(password: impl AsRef<[u8]>, iterations: u32) -> Self {
635        Self {
636            password: password.as_ref().to_vec(),
637            iterations,
638        }
639    }
640
641    /// Wrap a content encryption key.
642    ///
643    /// Generates a random 16-byte salt, derives a KEK via PBKDF2-HMAC-SHA256,
644    /// then wraps the content key with AES-256 Key Wrap.
645    ///
646    /// # Errors
647    ///
648    /// Returns an error if AES key wrapping fails.
649    pub fn wrap(&self, content_key: &[u8]) -> Result<Pbes2WrappedKeyData> {
650        use aes_kw::{cipher::KeyInit, KwAes256};
651
652        // Generate random 16-byte salt
653        let mut salt = [0u8; 16];
654        getrandom::fill(&mut salt)
655            .map_err(|e| encryption_error(format!("System RNG failed: {e}")))?;
656
657        // Derive KEK via PBKDF2-HMAC-SHA256
658        let mut kek_bytes = [0u8; 32];
659        pbkdf2::pbkdf2_hmac::<sha2::Sha256>(&self.password, &salt, self.iterations, &mut kek_bytes);
660
661        // AES Key Wrap
662        let kek = KwAes256::new(&kek_bytes.into());
663        let mut wrapped = vec![0u8; content_key.len() + 8];
664        kek.wrap_key(content_key, &mut wrapped)
665            .map_err(|e| encryption_error(format!("PBES2 AES key wrap failed: {e}")))?;
666
667        Ok(Pbes2WrappedKeyData {
668            wrapped_key: wrapped,
669            salt: salt.to_vec(),
670            iterations: self.iterations,
671        })
672    }
673}
674
675/// PBES2-HS256+A256KW key unwrapper (password-based).
676///
677/// Derives the same KEK from the password and salt/iterations stored
678/// in the wrapped data, then unwraps the content encryption key.
679#[cfg(feature = "key-wrapping-pbes2")]
680pub struct Pbes2KeyUnwrapper {
681    password: Vec<u8>,
682}
683
684#[cfg(feature = "key-wrapping-pbes2")]
685impl Pbes2KeyUnwrapper {
686    /// Create a key unwrapper with the given password.
687    #[must_use]
688    pub fn new(password: impl AsRef<[u8]>) -> Self {
689        Self {
690            password: password.as_ref().to_vec(),
691        }
692    }
693
694    /// Unwrap a content encryption key.
695    ///
696    /// Uses the salt and iteration count from the wrapped data to derive the KEK,
697    /// then unwraps the content key.
698    ///
699    /// # Errors
700    ///
701    /// Returns an error if the password is wrong or the wrapped data is tampered.
702    pub fn unwrap(&self, data: &Pbes2WrappedKeyData) -> Result<Vec<u8>> {
703        use aes_kw::{cipher::KeyInit, KwAes256};
704
705        // Derive KEK via PBKDF2-HMAC-SHA256 (same params as wrap)
706        let mut kek_bytes = [0u8; 32];
707        pbkdf2::pbkdf2_hmac::<sha2::Sha256>(
708            &self.password,
709            &data.salt,
710            data.iterations,
711            &mut kek_bytes,
712        );
713
714        // AES Key Unwrap
715        let kek = KwAes256::new(&kek_bytes.into());
716        let unwrapped_len = data
717            .wrapped_key
718            .len()
719            .checked_sub(8)
720            .ok_or_else(|| encryption_error("Wrapped key too short"))?;
721        let mut unwrapped = vec![0u8; unwrapped_len];
722        kek.unwrap_key(&data.wrapped_key, &mut unwrapped)
723            .map_err(|e| encryption_error(format!("PBES2 AES key unwrap failed: {e}")))?;
724        Ok(unwrapped)
725    }
726}
727
728#[cfg(all(test, feature = "encryption"))]
729mod tests {
730    use super::*;
731
732    #[test]
733    fn test_encrypt_decrypt() {
734        let key = Aes256GcmEncryptor::generate_key();
735        let encryptor = Aes256GcmEncryptor::new(&key).unwrap();
736
737        let plaintext = b"Hello, World! This is a test message.";
738        let encrypted = encryptor.encrypt(plaintext).unwrap();
739
740        assert_ne!(&encrypted.ciphertext[..plaintext.len()], plaintext);
741
742        let decrypted = encryptor
743            .decrypt(&encrypted.ciphertext, &encrypted.nonce)
744            .unwrap();
745        assert_eq!(decrypted, plaintext);
746    }
747
748    #[test]
749    fn test_wrong_key_fails() {
750        let key1 = Aes256GcmEncryptor::generate_key();
751        let key2 = Aes256GcmEncryptor::generate_key();
752
753        let encryptor1 = Aes256GcmEncryptor::new(&key1).unwrap();
754        let encryptor2 = Aes256GcmEncryptor::new(&key2).unwrap();
755
756        let plaintext = b"Secret message";
757        let encrypted = encryptor1.encrypt(plaintext).unwrap();
758
759        let result = encryptor2.decrypt(&encrypted.ciphertext, &encrypted.nonce);
760        assert!(result.is_err());
761    }
762
763    #[test]
764    fn test_tampered_data_fails() {
765        let key = Aes256GcmEncryptor::generate_key();
766        let encryptor = Aes256GcmEncryptor::new(&key).unwrap();
767
768        let plaintext = b"Original message";
769        let mut encrypted = encryptor.encrypt(plaintext).unwrap();
770
771        // Tamper with the ciphertext
772        if !encrypted.ciphertext.is_empty() {
773            encrypted.ciphertext[0] ^= 0xFF;
774        }
775
776        let result = encryptor.decrypt(&encrypted.ciphertext, &encrypted.nonce);
777        assert!(result.is_err());
778    }
779
780    #[test]
781    fn test_empty_plaintext() {
782        let key = Aes256GcmEncryptor::generate_key();
783        let encryptor = Aes256GcmEncryptor::new(&key).unwrap();
784
785        let plaintext = b"";
786        let encrypted = encryptor.encrypt(plaintext).unwrap();
787        let decrypted = encryptor
788            .decrypt(&encrypted.ciphertext, &encrypted.nonce)
789            .unwrap();
790
791        assert_eq!(decrypted, plaintext);
792    }
793
794    #[test]
795    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
796    fn test_large_plaintext() {
797        let key = Aes256GcmEncryptor::generate_key();
798        let encryptor = Aes256GcmEncryptor::new(&key).unwrap();
799
800        // 1 MB of data
801        let plaintext: Vec<u8> = (0..1024 * 1024).map(|i| (i % 256) as u8).collect();
802        let encrypted = encryptor.encrypt(&plaintext).unwrap();
803        let decrypted = encryptor
804            .decrypt(&encrypted.ciphertext, &encrypted.nonce)
805            .unwrap();
806
807        assert_eq!(decrypted, plaintext);
808    }
809
810    #[test]
811    fn test_encryption_metadata_serialization() {
812        let metadata = EncryptionMetadata {
813            algorithm: EncryptionAlgorithm::Aes256Gcm,
814            kdf: Some(KeyDerivation {
815                algorithm: KdfAlgorithm::Argon2id,
816                salt: "base64salt".to_string(),
817                iterations: None,
818                memory: Some(65536),
819                parallelism: Some(4),
820            }),
821            wrapped_key: None,
822            key_management: None,
823            recipients: vec![],
824        };
825
826        let json = serde_json::to_string_pretty(&metadata).unwrap();
827        assert!(json.contains("AES-256-GCM"));
828        assert!(json.contains("Argon2id"));
829
830        let deserialized: EncryptionMetadata = serde_json::from_str(&json).unwrap();
831        assert_eq!(deserialized.algorithm, metadata.algorithm);
832    }
833
834    #[test]
835    fn test_key_management_algorithm_roundtrip() {
836        let variants = [
837            (KeyManagementAlgorithm::EcdhEsA256kw, "\"ECDH-ES+A256KW\""),
838            (KeyManagementAlgorithm::RsaOaep256, "\"RSA-OAEP-256\""),
839            (
840                KeyManagementAlgorithm::Pbes2HsA256kw,
841                "\"PBES2-HS256+A256KW\"",
842            ),
843        ];
844        for (alg, expected_json) in &variants {
845            let json = serde_json::to_string(alg).unwrap();
846            assert_eq!(&json, expected_json);
847            let parsed: KeyManagementAlgorithm = serde_json::from_str(&json).unwrap();
848            assert_eq!(&parsed, alg);
849        }
850    }
851
852    #[test]
853    fn test_encryption_metadata_with_key_management() {
854        let metadata = EncryptionMetadata {
855            algorithm: EncryptionAlgorithm::Aes256Gcm,
856            kdf: None,
857            wrapped_key: Some("wrapped-key-base64".to_string()),
858            key_management: Some(KeyManagementAlgorithm::EcdhEsA256kw),
859            recipients: vec![Recipient {
860                id: "recipient-1".to_string(),
861                encrypted_key: "enc-key-base64".to_string(),
862                algorithm: Some("ECDH-ES+A256KW".to_string()),
863                ephemeral_public_key: Some("ephemeral-pk-base64".to_string()),
864            }],
865        };
866
867        let json = serde_json::to_string_pretty(&metadata).unwrap();
868        assert!(json.contains("ECDH-ES+A256KW"));
869        assert!(json.contains("ephemeralPublicKey"));
870
871        let parsed: EncryptionMetadata = serde_json::from_str(&json).unwrap();
872        assert_eq!(parsed.key_management, metadata.key_management);
873        assert_eq!(
874            parsed.recipients[0].ephemeral_public_key,
875            Some("ephemeral-pk-base64".to_string())
876        );
877    }
878
879    #[test]
880    fn test_encryption_metadata_backward_compat() {
881        // JSON without key_management or ephemeral_public_key should deserialize fine
882        let json = r#"{
883            "algorithm": "AES-256-GCM",
884            "recipients": [{
885                "id": "r1",
886                "encryptedKey": "key-data"
887            }]
888        }"#;
889        let metadata: EncryptionMetadata = serde_json::from_str(json).unwrap();
890        assert!(metadata.key_management.is_none());
891        assert!(metadata.recipients[0].ephemeral_public_key.is_none());
892    }
893}
894
895#[cfg(all(test, feature = "encryption-chacha"))]
896mod chacha_tests {
897    use super::*;
898
899    #[test]
900    fn test_chacha_encrypt_decrypt() {
901        let key = ChaCha20Poly1305Encryptor::generate_key();
902        let encryptor = ChaCha20Poly1305Encryptor::new(&key).unwrap();
903
904        let plaintext = b"Hello, World! This is a test message.";
905        let encrypted = encryptor.encrypt(plaintext).unwrap();
906
907        assert_ne!(&encrypted.ciphertext[..plaintext.len()], plaintext);
908
909        let decrypted = encryptor
910            .decrypt(&encrypted.ciphertext, &encrypted.nonce)
911            .unwrap();
912        assert_eq!(decrypted, plaintext);
913    }
914
915    #[test]
916    fn test_chacha_wrong_key_fails() {
917        let key1 = ChaCha20Poly1305Encryptor::generate_key();
918        let key2 = ChaCha20Poly1305Encryptor::generate_key();
919
920        let encryptor1 = ChaCha20Poly1305Encryptor::new(&key1).unwrap();
921        let encryptor2 = ChaCha20Poly1305Encryptor::new(&key2).unwrap();
922
923        let plaintext = b"Secret message";
924        let encrypted = encryptor1.encrypt(plaintext).unwrap();
925
926        let result = encryptor2.decrypt(&encrypted.ciphertext, &encrypted.nonce);
927        assert!(result.is_err());
928    }
929
930    #[test]
931    fn test_chacha_tampered_data_fails() {
932        let key = ChaCha20Poly1305Encryptor::generate_key();
933        let encryptor = ChaCha20Poly1305Encryptor::new(&key).unwrap();
934
935        let plaintext = b"Original message";
936        let mut encrypted = encryptor.encrypt(plaintext).unwrap();
937
938        // Tamper with the ciphertext
939        if !encrypted.ciphertext.is_empty() {
940            encrypted.ciphertext[0] ^= 0xFF;
941        }
942
943        let result = encryptor.decrypt(&encrypted.ciphertext, &encrypted.nonce);
944        assert!(result.is_err());
945    }
946
947    #[test]
948    fn test_chacha_empty_plaintext() {
949        let key = ChaCha20Poly1305Encryptor::generate_key();
950        let encryptor = ChaCha20Poly1305Encryptor::new(&key).unwrap();
951
952        let plaintext = b"";
953        let encrypted = encryptor.encrypt(plaintext).unwrap();
954        let decrypted = encryptor
955            .decrypt(&encrypted.ciphertext, &encrypted.nonce)
956            .unwrap();
957
958        assert_eq!(decrypted, plaintext);
959    }
960
961    #[test]
962    fn test_chacha_encryption_algorithm_enum() {
963        let algo = EncryptionAlgorithm::ChaCha20Poly1305;
964        assert_eq!(algo.as_str(), "ChaCha20-Poly1305");
965        assert_eq!(algo.key_size(), 32);
966        assert_eq!(algo.nonce_size(), 12);
967
968        let json = serde_json::to_string(&algo).unwrap();
969        assert_eq!(json, "\"ChaCha20-Poly1305\"");
970
971        let deserialized: EncryptionAlgorithm = serde_json::from_str(&json).unwrap();
972        assert_eq!(deserialized, algo);
973    }
974}
975
976#[cfg(all(test, feature = "key-wrapping"))]
977mod key_wrapping_tests {
978    use super::*;
979
980    /// Generate a P-256 keypair for testing.
981    fn generate_keypair() -> (p256::SecretKey, p256::PublicKey) {
982        use p256::elliptic_curve::Generate;
983        let secret = p256::SecretKey::generate();
984        let public = secret.public_key();
985        (secret, public)
986    }
987
988    #[test]
989    fn test_wrap_unwrap_roundtrip() {
990        let (secret, public) = generate_keypair();
991
992        // Generate a 32-byte content encryption key
993        let content_key = Aes256GcmEncryptor::generate_key();
994
995        // Wrap
996        let wrapper = EcdhEsKeyWrapper::new(public);
997        let wrapped = wrapper.wrap(&content_key).unwrap();
998
999        // Wrapped key should be 8 bytes longer than original (AES-KW IV)
1000        assert_eq!(wrapped.wrapped_key.len(), content_key.len() + 8);
1001        // Ephemeral public key should be 65 bytes (uncompressed SEC1 point)
1002        assert_eq!(wrapped.ephemeral_public_key.len(), 65);
1003
1004        // Unwrap
1005        let unwrapper = EcdhEsKeyUnwrapper::new(secret);
1006        let recovered = unwrapper.unwrap(&wrapped).unwrap();
1007
1008        assert_eq!(recovered, content_key);
1009    }
1010
1011    #[test]
1012    fn test_multi_recipient_wrap() {
1013        let (secret_a, public_a) = generate_keypair();
1014        let (secret_b, public_b) = generate_keypair();
1015
1016        let content_key = Aes256GcmEncryptor::generate_key();
1017
1018        // Wrap for two recipients
1019        let wrapper_a = EcdhEsKeyWrapper::new(public_a);
1020        let wrapped_a = wrapper_a.wrap(&content_key).unwrap();
1021
1022        let wrapper_b = EcdhEsKeyWrapper::new(public_b);
1023        let wrapped_b = wrapper_b.wrap(&content_key).unwrap();
1024
1025        // Each should unwrap independently
1026        let unwrapper_a = EcdhEsKeyUnwrapper::new(secret_a);
1027        let recovered_a = unwrapper_a.unwrap(&wrapped_a).unwrap();
1028        assert_eq!(recovered_a, content_key);
1029
1030        let unwrapper_b = EcdhEsKeyUnwrapper::new(secret_b);
1031        let recovered_b = unwrapper_b.unwrap(&wrapped_b).unwrap();
1032        assert_eq!(recovered_b, content_key);
1033
1034        // Cross-unwrap should fail (wrong key)
1035        assert!(unwrapper_a.unwrap(&wrapped_b).is_err());
1036        assert!(unwrapper_b.unwrap(&wrapped_a).is_err());
1037    }
1038
1039    #[test]
1040    fn test_wrong_private_key_fails() {
1041        let (_secret, public) = generate_keypair();
1042        let (wrong_secret, _wrong_public) = generate_keypair();
1043
1044        let content_key = Aes256GcmEncryptor::generate_key();
1045
1046        let wrapper = EcdhEsKeyWrapper::new(public);
1047        let wrapped = wrapper.wrap(&content_key).unwrap();
1048
1049        // Unwrapping with wrong key should fail
1050        let unwrapper = EcdhEsKeyUnwrapper::new(wrong_secret);
1051        let result = unwrapper.unwrap(&wrapped);
1052        assert!(result.is_err());
1053    }
1054
1055    #[test]
1056    fn test_tampered_wrapped_key_fails() {
1057        let (secret, public) = generate_keypair();
1058
1059        let content_key = Aes256GcmEncryptor::generate_key();
1060
1061        let wrapper = EcdhEsKeyWrapper::new(public);
1062        let mut wrapped = wrapper.wrap(&content_key).unwrap();
1063
1064        // Tamper with the wrapped key
1065        if !wrapped.wrapped_key.is_empty() {
1066            wrapped.wrapped_key[0] ^= 0xFF;
1067        }
1068
1069        let unwrapper = EcdhEsKeyUnwrapper::new(secret);
1070        let result = unwrapper.unwrap(&wrapped);
1071        assert!(result.is_err());
1072    }
1073
1074    #[test]
1075    fn test_tampered_ephemeral_key_fails() {
1076        let (secret, public) = generate_keypair();
1077
1078        let content_key = Aes256GcmEncryptor::generate_key();
1079
1080        let wrapper = EcdhEsKeyWrapper::new(public);
1081        let mut wrapped = wrapper.wrap(&content_key).unwrap();
1082
1083        // Tamper with the ephemeral public key (change a coordinate byte)
1084        // Byte 0 is the tag (0x04 for uncompressed), skip it
1085        if wrapped.ephemeral_public_key.len() > 1 {
1086            wrapped.ephemeral_public_key[1] ^= 0xFF;
1087        }
1088
1089        let unwrapper = EcdhEsKeyUnwrapper::new(secret);
1090        let result = unwrapper.unwrap(&wrapped);
1091        // Should either fail to decode the point or produce wrong shared secret
1092        assert!(result.is_err());
1093    }
1094
1095    #[test]
1096    fn test_integration_encrypt_wrap_unwrap_decrypt() {
1097        let (secret, public) = generate_keypair();
1098
1099        // 1. Generate content encryption key and encrypt content
1100        let content_key = Aes256GcmEncryptor::generate_key();
1101        let encryptor = Aes256GcmEncryptor::new(&content_key).unwrap();
1102        let plaintext = b"Codex document content for encryption";
1103        let encrypted = encryptor.encrypt(plaintext).unwrap();
1104
1105        // 2. Wrap the content key for the recipient
1106        let wrapper = EcdhEsKeyWrapper::new(public);
1107        let wrapped = wrapper.wrap(&content_key).unwrap();
1108
1109        // 3. Build metadata (as would be serialized in the archive)
1110        let metadata = EncryptionMetadata {
1111            algorithm: EncryptionAlgorithm::Aes256Gcm,
1112            kdf: None,
1113            wrapped_key: None,
1114            key_management: Some(KeyManagementAlgorithm::EcdhEsA256kw),
1115            recipients: vec![Recipient {
1116                id: "recipient-1".to_string(),
1117                encrypted_key: base64::Engine::encode(
1118                    &base64::engine::general_purpose::STANDARD,
1119                    &wrapped.wrapped_key,
1120                ),
1121                algorithm: Some("ECDH-ES+A256KW".to_string()),
1122                ephemeral_public_key: Some(base64::Engine::encode(
1123                    &base64::engine::general_purpose::STANDARD,
1124                    &wrapped.ephemeral_public_key,
1125                )),
1126            }],
1127        };
1128
1129        // 4. Serialize/deserialize metadata (roundtrip)
1130        let json = serde_json::to_string(&metadata).unwrap();
1131        let parsed: EncryptionMetadata = serde_json::from_str(&json).unwrap();
1132
1133        // 5. Recipient unwraps the key
1134        let recipient = &parsed.recipients[0];
1135        let wrapped_data = WrappedKeyData {
1136            wrapped_key: base64::Engine::decode(
1137                &base64::engine::general_purpose::STANDARD,
1138                &recipient.encrypted_key,
1139            )
1140            .unwrap(),
1141            ephemeral_public_key: base64::Engine::decode(
1142                &base64::engine::general_purpose::STANDARD,
1143                recipient.ephemeral_public_key.as_ref().unwrap(),
1144            )
1145            .unwrap(),
1146        };
1147
1148        let unwrapper = EcdhEsKeyUnwrapper::new(secret);
1149        let recovered_key = unwrapper.unwrap(&wrapped_data).unwrap();
1150
1151        // 6. Decrypt the content
1152        let decryptor = Aes256GcmEncryptor::new(&recovered_key).unwrap();
1153        let decrypted = decryptor
1154            .decrypt(&encrypted.ciphertext, &encrypted.nonce)
1155            .unwrap();
1156
1157        assert_eq!(decrypted, plaintext);
1158    }
1159
1160    #[test]
1161    fn test_wrap_16_byte_key() {
1162        // AES Key Wrap works with any key that's a multiple of 8 bytes
1163        let (secret, public) = generate_keypair();
1164        let content_key = [0x42u8; 16]; // 128-bit key
1165
1166        let wrapper = EcdhEsKeyWrapper::new(public);
1167        let wrapped = wrapper.wrap(&content_key).unwrap();
1168        assert_eq!(wrapped.wrapped_key.len(), 24); // 16 + 8
1169
1170        let unwrapper = EcdhEsKeyUnwrapper::new(secret);
1171        let recovered = unwrapper.unwrap(&wrapped).unwrap();
1172        assert_eq!(recovered, content_key);
1173    }
1174}
1175
1176#[cfg(all(test, feature = "key-wrapping-rsa"))]
1177mod rsa_oaep_tests {
1178    use super::*;
1179
1180    fn generate_rsa_keypair(bits: usize) -> (rsa::RsaPrivateKey, rsa::RsaPublicKey) {
1181        let private_key =
1182            rsa::RsaPrivateKey::new(&mut rand_core::UnwrapErr(getrandom::SysRng), bits).unwrap();
1183        let public_key = rsa::RsaPublicKey::from(&private_key);
1184        (private_key, public_key)
1185    }
1186
1187    #[test]
1188    fn test_rsa_oaep_roundtrip_2048() {
1189        let (private_key, public_key) = generate_rsa_keypair(2048);
1190        let content_key = Aes256GcmEncryptor::generate_key();
1191
1192        let wrapper = RsaOaepKeyWrapper::new(public_key);
1193        let wrapped = wrapper.wrap(&content_key).unwrap();
1194
1195        let unwrapper = RsaOaepKeyUnwrapper::new(private_key);
1196        let recovered = unwrapper.unwrap(&wrapped).unwrap();
1197
1198        assert_eq!(recovered, content_key);
1199    }
1200
1201    #[test]
1202    fn test_rsa_oaep_roundtrip_4096() {
1203        let (private_key, public_key) = generate_rsa_keypair(4096);
1204        let content_key = Aes256GcmEncryptor::generate_key();
1205
1206        let wrapper = RsaOaepKeyWrapper::new(public_key);
1207        let wrapped = wrapper.wrap(&content_key).unwrap();
1208
1209        let unwrapper = RsaOaepKeyUnwrapper::new(private_key);
1210        let recovered = unwrapper.unwrap(&wrapped).unwrap();
1211
1212        assert_eq!(recovered, content_key);
1213    }
1214
1215    #[test]
1216    fn test_rsa_oaep_wrong_key_fails() {
1217        let (_private_key, public_key) = generate_rsa_keypair(2048);
1218        let (wrong_private_key, _wrong_public_key) = generate_rsa_keypair(2048);
1219        let content_key = Aes256GcmEncryptor::generate_key();
1220
1221        let wrapper = RsaOaepKeyWrapper::new(public_key);
1222        let wrapped = wrapper.wrap(&content_key).unwrap();
1223
1224        let unwrapper = RsaOaepKeyUnwrapper::new(wrong_private_key);
1225        let result = unwrapper.unwrap(&wrapped);
1226        assert!(result.is_err());
1227    }
1228
1229    #[test]
1230    fn test_rsa_oaep_tampered_data_fails() {
1231        let (private_key, public_key) = generate_rsa_keypair(2048);
1232        let content_key = Aes256GcmEncryptor::generate_key();
1233
1234        let wrapper = RsaOaepKeyWrapper::new(public_key);
1235        let mut wrapped = wrapper.wrap(&content_key).unwrap();
1236
1237        if !wrapped.wrapped_key.is_empty() {
1238            wrapped.wrapped_key[0] ^= 0xFF;
1239        }
1240
1241        let unwrapper = RsaOaepKeyUnwrapper::new(private_key);
1242        let result = unwrapper.unwrap(&wrapped);
1243        assert!(result.is_err());
1244    }
1245
1246    #[test]
1247    fn test_rsa_oaep_integration_encrypt_wrap_unwrap_decrypt() {
1248        let (private_key, public_key) = generate_rsa_keypair(2048);
1249
1250        // Encrypt content
1251        let content_key = Aes256GcmEncryptor::generate_key();
1252        let encryptor = Aes256GcmEncryptor::new(&content_key).unwrap();
1253        let plaintext = b"Codex document encrypted with RSA-OAEP key wrapping";
1254        let encrypted = encryptor.encrypt(plaintext).unwrap();
1255
1256        // Wrap the content key
1257        let wrapper = RsaOaepKeyWrapper::new(public_key);
1258        let wrapped = wrapper.wrap(&content_key).unwrap();
1259
1260        // Unwrap and decrypt
1261        let unwrapper = RsaOaepKeyUnwrapper::new(private_key);
1262        let recovered_key = unwrapper.unwrap(&wrapped).unwrap();
1263
1264        let decryptor = Aes256GcmEncryptor::new(&recovered_key).unwrap();
1265        let decrypted = decryptor
1266            .decrypt(&encrypted.ciphertext, &encrypted.nonce)
1267            .unwrap();
1268
1269        assert_eq!(decrypted, plaintext);
1270    }
1271
1272    #[test]
1273    fn test_rsa_oaep_metadata_serialization() {
1274        let metadata = EncryptionMetadata {
1275            algorithm: EncryptionAlgorithm::Aes256Gcm,
1276            kdf: None,
1277            wrapped_key: None,
1278            key_management: Some(KeyManagementAlgorithm::RsaOaep256),
1279            recipients: vec![Recipient {
1280                id: "rsa-recipient".to_string(),
1281                encrypted_key: "wrapped-key-base64".to_string(),
1282                algorithm: Some("RSA-OAEP-256".to_string()),
1283                ephemeral_public_key: None,
1284            }],
1285        };
1286
1287        let json = serde_json::to_string_pretty(&metadata).unwrap();
1288        assert!(json.contains("RSA-OAEP-256"));
1289        // RSA-OAEP doesn't use ephemeral keys
1290        assert!(!json.contains("ephemeralPublicKey"));
1291
1292        let parsed: EncryptionMetadata = serde_json::from_str(&json).unwrap();
1293        assert_eq!(
1294            parsed.key_management,
1295            Some(KeyManagementAlgorithm::RsaOaep256)
1296        );
1297    }
1298}
1299
1300#[cfg(all(test, feature = "key-wrapping-pbes2"))]
1301mod pbes2_tests {
1302    use super::*;
1303
1304    #[test]
1305    fn test_pbes2_roundtrip() {
1306        let password = b"correct horse battery staple";
1307        let content_key = Aes256GcmEncryptor::generate_key();
1308
1309        let wrapper = Pbes2KeyWrapper::new(password, Pbes2KeyWrapper::DEFAULT_ITERATIONS);
1310        let wrapped = wrapper.wrap(&content_key).unwrap();
1311
1312        assert_eq!(wrapped.wrapped_key.len(), content_key.len() + 8);
1313        assert_eq!(wrapped.salt.len(), 16);
1314        assert_eq!(wrapped.iterations, Pbes2KeyWrapper::DEFAULT_ITERATIONS);
1315
1316        let unwrapper = Pbes2KeyUnwrapper::new(password);
1317        let recovered = unwrapper.unwrap(&wrapped).unwrap();
1318
1319        assert_eq!(recovered, content_key);
1320    }
1321
1322    #[test]
1323    fn test_pbes2_wrong_password_fails() {
1324        let content_key = Aes256GcmEncryptor::generate_key();
1325
1326        let wrapper = Pbes2KeyWrapper::new(b"correct password", 1000);
1327        let wrapped = wrapper.wrap(&content_key).unwrap();
1328
1329        let unwrapper = Pbes2KeyUnwrapper::new(b"wrong password");
1330        let result = unwrapper.unwrap(&wrapped);
1331        assert!(result.is_err());
1332    }
1333
1334    #[test]
1335    fn test_pbes2_tampered_salt_fails() {
1336        let password = b"my password";
1337        let content_key = Aes256GcmEncryptor::generate_key();
1338
1339        let wrapper = Pbes2KeyWrapper::new(password, 1000);
1340        let mut wrapped = wrapper.wrap(&content_key).unwrap();
1341
1342        // Tamper with the salt
1343        if !wrapped.salt.is_empty() {
1344            wrapped.salt[0] ^= 0xFF;
1345        }
1346
1347        let unwrapper = Pbes2KeyUnwrapper::new(password);
1348        let result = unwrapper.unwrap(&wrapped);
1349        assert!(result.is_err());
1350    }
1351
1352    #[test]
1353    fn test_pbes2_different_iteration_counts() {
1354        let password = b"shared password";
1355        let content_key = Aes256GcmEncryptor::generate_key();
1356
1357        // Wrap with different iteration counts
1358        for &iterations in &[1000u32, 10_000, 100_000] {
1359            let wrapper = Pbes2KeyWrapper::new(password, iterations);
1360            let wrapped = wrapper.wrap(&content_key).unwrap();
1361            assert_eq!(wrapped.iterations, iterations);
1362
1363            let unwrapper = Pbes2KeyUnwrapper::new(password);
1364            let recovered = unwrapper.unwrap(&wrapped).unwrap();
1365            assert_eq!(recovered, content_key);
1366        }
1367    }
1368
1369    #[test]
1370    fn test_pbes2_integration_encrypt_wrap_unwrap_decrypt() {
1371        let password = b"document encryption password";
1372
1373        // Encrypt content
1374        let content_key = Aes256GcmEncryptor::generate_key();
1375        let encryptor = Aes256GcmEncryptor::new(&content_key).unwrap();
1376        let plaintext = b"Codex document with password-based key wrapping";
1377        let encrypted = encryptor.encrypt(plaintext).unwrap();
1378
1379        // Wrap the content key with password
1380        let wrapper = Pbes2KeyWrapper::new(password, 1000);
1381        let wrapped = wrapper.wrap(&content_key).unwrap();
1382
1383        // Unwrap and decrypt
1384        let unwrapper = Pbes2KeyUnwrapper::new(password);
1385        let recovered_key = unwrapper.unwrap(&wrapped).unwrap();
1386
1387        let decryptor = Aes256GcmEncryptor::new(&recovered_key).unwrap();
1388        let decrypted = decryptor
1389            .decrypt(&encrypted.ciphertext, &encrypted.nonce)
1390            .unwrap();
1391
1392        assert_eq!(decrypted, plaintext);
1393    }
1394
1395    #[test]
1396    fn test_pbes2_metadata_serialization() {
1397        let metadata = EncryptionMetadata {
1398            algorithm: EncryptionAlgorithm::Aes256Gcm,
1399            kdf: None,
1400            wrapped_key: None,
1401            key_management: Some(KeyManagementAlgorithm::Pbes2HsA256kw),
1402            recipients: vec![],
1403        };
1404
1405        let json = serde_json::to_string(&metadata).unwrap();
1406        assert!(json.contains("PBES2-HS256+A256KW"));
1407
1408        let parsed: EncryptionMetadata = serde_json::from_str(&json).unwrap();
1409        assert_eq!(
1410            parsed.key_management,
1411            Some(KeyManagementAlgorithm::Pbes2HsA256kw)
1412        );
1413    }
1414}