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