qsfs_core/
lib.rs

1//! # qsfs-core: Quantum-Shield File System Core Library
2//!
3//! [![Crates.io](https://img.shields.io/crates/v/qsfs-core)](https://crates.io/crates/qsfs-core)
4//! [![Documentation](https://docs.rs/qsfs-core/badge.svg)](https://docs.rs/qsfs-core)
5//! [![License](https://img.shields.io/badge/license-MIT%2FApache--2.0-green)](https://github.com/AnubisQuantumCipher/quantum-shield/blob/main/LICENSE-MIT)
6//!
7//! **The world's most comprehensive post-quantum file encryption library with complete cryptographic suite**
8//!
9//! This crate provides the core cryptographic primitives and file format implementation for QSFS,
10//! a quantum-resistant file encryption system that implements NIST-standardized post-quantum
11//! cryptography with ML-DSA-87 digital signatures and a complete suite of encryption technologies.
12//!
13//! ## 🛡️ Complete Cryptographic Arsenal
14//!
15//! ### Post-Quantum Cryptography (CNSA 2.0 Compliant)
16//! - **ML-KEM-1024** (FIPS 203) - Quantum-resistant key encapsulation, NIST Level 5 security
17//! - **ML-DSA-87** (FIPS 204) - Post-quantum digital signatures, NIST Level 5 security
18//! - **CNSA 2.0 Compliant** - Meets NSA Commercial National Security Algorithm Suite requirements
19//!
20//! ### Hybrid Classical Cryptography
21//! - **X25519** - Elliptic curve Diffie-Hellman key exchange for additional security layers
22//! - **Ed25519** - EdDSA signatures for classical authentication backup
23//!
24//! ### Authenticated Encryption (AEAD)
25//! - **AES-256-GCM-SIV** - Nonce misuse-resistant authenticated encryption (default)
26//! - **AES-256-GCM** - Standard authenticated encryption with additional data
27//! - **ChaCha20-Poly1305** - Stream cipher with Poly1305 MAC for cascade encryption
28//!
29//! ### Key Derivation & Hashing
30//! - **HKDF-SHA3-384** - Key derivation with enhanced domain separation
31//! - **BLAKE3** - High-performance cryptographic hashing for file integrity
32//! - **Argon2** - Memory-hard key derivation for password-based encryption
33//!
34//! ### Hardware Security Module (HSM) Support
35//! - **PKCS#11** - Hardware security module integration via cryptoki
36//! - **Hardware acceleration** - Optimized primitives using available CPU instructions
37//!
38//! ### Memory Safety & Security
39//! - **Automatic zeroization** - Secure memory clearing using zeroize crate
40//! - **Constant-time operations** - Side-channel attack resistance
41//! - **Memory locking** - Prevents sensitive data from being swapped to disk
42//! - **Secure permissions** - Atomic file operations with restricted access
43//!
44//! ## 🚀 Quick Start
45//!
46//! Add this to your `Cargo.toml`:
47//!
48//! ```toml
49//! [dependencies]
50//! qsfs-core = { version = "0.1.2", features = ["pq", "hybrid-x25519", "gcm-siv", "cascade", "hsm"] }
51//! ```
52//!
53//! ### Basic Encryption with All Technologies
54//!
55//! ```rust,no_run
56//! use qsfs_core::{seal, unseal, SealRequest, UnsealContext, Signer};
57//! use tokio::fs::File;
58//!
59//! #[tokio::main]
60//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
61//!     // Generate or load cryptographic keys
62//!     let ml_kem_pk = /* load ML-KEM-1024 public key */;
63//!     let x25519_pk = /* load X25519 public key */;
64//!     let signer = Signer::generate()?; // ML-DSA-87 signer
65//!
66//!     // Encrypt with complete quantum-resistant suite
67//!     let request = SealRequest {
68//!         input_path: "sensitive-document.pdf",
69//!         recipients: vec![(
70//!             "alice".to_string(),
71//!             ml_kem_pk,
72//!             x25519_pk,
73//!         )],
74//!         header_sign_mldsa_sk: None,
75//!         chunk_size: 131072, // 128KB chunks for streaming
76//!         signer: Some(&signer), // ML-DSA-87 signatures
77//!     };
78//!
79//!     // Seal with all encryption technologies
80//!     seal(request, "document.qsfs").await?;
81//!
82//!     // Decrypt with signature verification
83//!     let input = File::open("document.qsfs").await?;
84//!     let context = UnsealContext {
85//!         mlkem_sk: &ml_kem_sk,
86//!         x25519_sk: Some(x25519_sk),
87//!         allow_unsigned: false, // Require signatures
88//!         trust_any_signer: false, // Use trust store
89//!     };
90//!
91//!     unseal(input, "decrypted.pdf", context).await?;
92//!     println!("✅ File decrypted and signature verified!");
93//!
94//!     Ok(())
95//! }
96//! ```
97//!
98//! ## 🔧 Feature Flags
99//!
100//! Enable specific cryptographic technologies:
101//!
102//! ```toml
103//! [dependencies]
104//! qsfs-core = { version = "0.1.2", features = [
105//!     "pq",           # Post-quantum: ML-KEM-1024 + ML-DSA-87
106//!     "hybrid-x25519", # Hybrid: X25519 + Ed25519
107//!     "gcm-siv",      # AES-256-GCM-SIV (nonce misuse resistant)
108//!     "gcm",          # AES-256-GCM (standard AEAD)
109//!     "cascade",      # ChaCha20-Poly1305 cascade encryption
110//!     "hsm",          # Hardware Security Module support
111//!     "wasm",         # WebAssembly compatibility
112//! ] }
113//! ```
114//!
115//! ## 📊 Security Specifications
116//!
117//! | Component | Algorithm | Key Size | Security Level | Quantum Safe |
118//! |-----------|-----------|----------|----------------|--------------|
119//! | **Key Encapsulation** | ML-KEM-1024 | 1568 bytes | NIST Level 5 | ✅ Yes |
120//! | **Digital Signatures** | ML-DSA-87 | 4864 bytes | NIST Level 5 | ✅ Yes |
121//! | **Hybrid Key Exchange** | X25519 | 32 bytes | ~128-bit | ❌ No |
122//! | **Symmetric Encryption** | AES-256-GCM-SIV | 256 bits | 128-bit | ✅ Yes |
123//! | **Stream Cipher** | ChaCha20-Poly1305 | 256 bits | 128-bit | ✅ Yes |
124//! | **Key Derivation** | HKDF-SHA3-384 | Variable | 192-bit | ✅ Yes |
125//! | **File Integrity** | BLAKE3 | 256 bits | 128-bit | ✅ Yes |
126//!
127//! ## 🏗️ Architecture
128//!
129//! The library is organized into specialized modules:
130//!
131//! - **[`derivation`]** - Key derivation functions and cryptographic key management
132//! - **[`header`]** - File format header parsing, serialization, and metadata
133//! - **[`streaming`]** - Streaming AEAD for efficient large file processing
134//! - **[`security`]** - Memory safety, system security, and side-channel protection
135//! - **[`signer`]** - ML-DSA-87 digital signature creation and verification
136
137
138
139
140//! ## 🔒 Security Properties
141//!
142//! ### Quantum Resistance
143//! - **Post-quantum algorithms** protect against Shor's algorithm (breaks RSA/ECC)
144//! - **Large key sizes** provide security against Grover's algorithm
145//! - **NIST standardized** algorithms with extensive cryptanalysis
146//!
147//! ### Forward Secrecy
148//! - **Ephemeral key exchange** ensures past sessions remain secure
149//! - **Perfect forward secrecy** through ephemeral X25519 keys
150//! - **Session isolation** prevents compromise propagation
151//!
152//! ### Non-Repudiation
153//! - **ML-DSA-87 signatures** provide cryptographic proof of origin
154//! - **Trust store management** for signer verification
155//! - **Canonical serialization** ensures deterministic signatures
156//!
157//! ### Integrity Protection
158//! - **AEAD authentication** detects any tampering
159//! - **BLAKE3 hashing** for file-level integrity
160//! - **Digital signatures** for authenticity verification
161//!
162//! ## 🌐 Platform Compatibility
163//!
164//! | Platform | Status | Notes |
165//! |----------|--------|-------|
166//! | Linux x86_64 | ✅ Full Support | Primary development platform |
167//! | Linux ARM64 | ✅ Full Support | Raspberry Pi 4+ compatible |
168//! | macOS Intel | ✅ Full Support | Hardware acceleration available |
169//! | macOS Apple Silicon | ✅ Full Support | Native ARM64 optimizations |
170//! | Windows x64 | ✅ Full Support | MSVC and GNU toolchains |
171//! | FreeBSD | 🟡 Experimental | Community maintained |
172//! | WebAssembly | 🟡 Limited | Basic functionality with `wasm` feature |
173//!
174//! ## 📚 Examples
175//!
176//! ### Multi-Recipient Encryption
177//!
178//! ```rust,no_run
179//! use qsfs_core::{seal, SealRequest, Signer};
180//!
181//! #[tokio::main]
182//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
183//!     let signer = Signer::generate()?;
184//!     
185//!     let request = SealRequest {
186//!         input_path: "company-secrets.tar.gz",
187//!         recipients: vec![
188//!             ("alice".to_string(), alice_mlkem_pk, alice_x25519_pk),
189//!             ("bob".to_string(), bob_mlkem_pk, bob_x25519_pk),
190//!             ("charlie".to_string(), charlie_mlkem_pk, charlie_x25519_pk),
191//!         ],
192//!         header_sign_mldsa_sk: None,
193//!         chunk_size: 262144, // 256KB chunks
194//!         signer: Some(&signer),
195//!     };
196//!
197//!     seal(request, "secrets.qsfs").await?;
198//!     println!("✅ Encrypted for {} recipients", request.recipients.len());
199//!     Ok(())
200//! }
201//! ```
202//!
203//! ### HSM Integration
204//!
205//! ```rust,no_run
206//! #[cfg(feature = "hsm")]
207//! use qsfs_core::security::hsm;
208//!
209//! #[cfg(feature = "hsm")]
210//! #[tokio::main]
211//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
212//!     // Initialize HSM connection
213//!     let hsm = hsm::initialize_pkcs11("/usr/lib/libpkcs11.so")?;
214//!     
215//!     // Use HSM for key operations
216//!     let key_handle = hsm.generate_key("AES", 256)?;
217//!     
218//!     // Encrypt using HSM-stored keys
219//!     // ... encryption logic with HSM integration
220//!     
221//!     Ok(())
222//! }
223//! ```
224//!
225//! ## 🔧 Advanced Configuration
226//!
227//! ### Custom Chunk Sizes
228//!
229//! ```rust,no_run
230//! // Small files: 64KB chunks for lower latency
231//! let small_file_request = SealRequest {
232//!     chunk_size: 65536,
233//!     // ... other fields
234//! };
235//!
236//! // Large files: 1MB chunks for better throughput
237//! let large_file_request = SealRequest {
238//!     chunk_size: 1048576,
239//!     // ... other fields
240//! };
241//! ```
242//!
243//! ### Algorithm Selection
244//!
245//! ```rust,no_run
246//! use qsfs_core::suite::SuiteId;
247//!
248//! // Force specific AEAD algorithm
249//! let suite = SuiteId::Aes256GcmSiv; // Nonce misuse resistant
250//! let suite = SuiteId::Aes256Gcm;    // Standard AEAD
251//! ```
252//!
253//! ## 🛠️ Integration Guide
254//!
255//! ### Error Handling
256//!
257//! ```rust,no_run
258//! use qsfs_core::{seal, SealRequest};
259//! use anyhow::Result;
260//!
261//! async fn secure_encrypt(input: &str, output: &str) -> Result<()> {
262//!     let request = SealRequest {
263//!         // ... configuration
264//!     };
265//!
266//!     match seal(request, output).await {
267//!         Ok(()) => {
268//!             println!("✅ Encryption successful");
269//!             Ok(())
270//!         }
271//!         Err(e) => {
272//!             eprintln!("❌ Encryption failed: {}", e);
273//!             Err(e)
274//!         }
275//!     }
276//! }
277//! ```
278//!
279//! ### Memory Management
280//!
281//! ```rust,no_run
282//! use qsfs_core::security::{disable_core_dumps, lock_memory};
283//!
284//! fn secure_initialization() -> Result<(), Box<dyn std::error::Error>> {
285//!     // Disable core dumps to prevent key leakage
286//!     disable_core_dumps()?;
287//!     
288//!     // Lock sensitive memory pages
289//!     let sensitive_data = vec![0u8; 1024];
290//!     lock_memory(&sensitive_data)?;
291//!     
292//!     // ... cryptographic operations
293//!     
294//!     // Memory is automatically zeroized on drop
295//!     Ok(())
296//! }
297//! ```
298//!
299//! ## 📖 Further Reading
300//!
301//! - [NIST Post-Quantum Cryptography Standards](https://csrc.nist.gov/projects/post-quantum-cryptography)
302//! - [NSA CNSA 2.0 Guidelines](https://media.defense.gov/2022/Sep/07/2003071834/-1/-1/0/CSA_CNSA_2.0_ALGORITHMS_.PDF)
303//! - [Quantum-Shield Security Analysis](https://github.com/AnubisQuantumCipher/quantum-shield/blob/main/docs/SECURITY_ANALYSIS.md)
304//! - [Complete Command Reference](https://github.com/AnubisQuantumCipher/quantum-shield/blob/main/docs/COMMAND_REFERENCE.md)
305
306pub mod derivation;
307mod header;
308mod pq;
309mod streaming;
310mod security;
311pub mod suite;
312pub mod pae;
313pub mod signer;
314pub mod canonical;
315
316use anyhow::Result;
317use derivation::{derive_file_nonce_seed, hkdf_expand_keys, ContentEncryptionKey, derive_kek, wrap_dek, unwrap_dek};
318pub use header::{Header, RecipientEntry, SignatureMetadata};
319use crate::suite::SuiteId;
320use pq::mlkem;
321use tokio::{fs::File, io::AsyncReadExt};
322use base64::{engine::general_purpose, Engine as _};
323use rand::RngCore;
324use std::io::Write;
325use secrecy::ExposeSecret;
326use tempfile::NamedTempFile;
327use std::path::Path;
328use security::{disable_core_dumps, set_secure_permissions};
329pub use signer::{Signer, TrustStore, verify_signature, default_trustdb_path, auto_provision_signer};
330pub use canonical::{CanonicalHeader, SignatureMetadata as CanonicalSignatureMetadata};
331
332#[cfg(feature="pq")]
333use pqcrypto_traits::kem::{SharedSecret as SharedSecretTrait, Ciphertext as CiphertextTrait};
334#[cfg(feature="pq")]
335use pqcrypto_traits::sign::PublicKey as PublicKeyTrait;
336
337pub struct SealRequest<'a> {
338    pub input_path: &'a str,
339    pub recipients: Vec<(String, pqcrypto_mlkem::mlkem1024::PublicKey, [u8;32])>,
340    pub header_sign_mldsa_sk: Option<pqcrypto_mldsa::mldsa87::SecretKey>,
341    pub chunk_size: usize,
342    pub signer: Option<&'a Signer>,
343}
344
345pub struct UnsealContext<'a> {
346    pub mlkem_sk: &'a pqcrypto_mlkem::mlkem1024::SecretKey,
347    pub x25519_sk: Option<[u8;32]>,
348    pub allow_unsigned: bool,
349    pub trust_any_signer: bool,
350}
351
352pub async fn seal(req: SealRequest<'_>, output_path: &str) -> Result<()> {
353    // Disable core dumps for security
354    disable_core_dumps().ok();
355    
356    // 1) Prepare header (no plaintext fingerprint in clear)
357    
358    // 2) Generate CEK and wrap for each recipient
359    let cek = ContentEncryptionKey::generate()?;
360    let mut recipients = Vec::new();
361    
362    // Ephemeral X25519 key for this file
363    #[cfg(feature="hybrid-x25519")]
364    let eph_x_sk = {
365        let rng = rand::rngs::OsRng;
366        x25519_dalek::StaticSecret::random_from_rng(rng)
367    };
368    #[cfg(feature="hybrid-x25519")]
369    let eph_x_pk = x25519_dalek::PublicKey::from(&eph_x_sk);
370
371    // Generate per-file kdf_salt (v2.1)
372    let mut kdf_salt = [0u8; 32];
373    rand::rngs::OsRng.fill_bytes(&mut kdf_salt);
374
375    for (label, mlkem_pk, recip_x25519_pk_bytes) in req.recipients {
376        let (ss, ct) = mlkem::encapsulate(&mlkem_pk);
377
378        #[cfg(feature="hybrid-x25519")]
379        let kek = {
380            let recip_x_pk = x25519_dalek::PublicKey::from(recip_x25519_pk_bytes);
381            let x_ss = eph_x_sk.diffie_hellman(&recip_x_pk);
382            derive_kek(ss.as_bytes(), x_ss.as_bytes(), Some(&kdf_salt))
383        };
384
385    #[cfg(not(feature="hybrid-x25519"))]
386    let kek = {
387            // Always KDF the shared secret (even when not hybrid)
388            derive_kek(ss.as_bytes(), &[], Some(&kdf_salt))
389        };
390
391        // Wrap DEK under KEK
392        let mut wrap_nonce = [0u8;12];
393        rand::rngs::OsRng.fill_bytes(&mut wrap_nonce);
394        let wrapped_dek = wrap_dek(&kek, &wrap_nonce, cek.expose_secret())?;
395
396        // Recipient fingerprint
397        let x25519_pk_fpr = {
398            let h = blake3::hash(&recip_x25519_pk_bytes);
399            let mut f = [0u8;8]; f.copy_from_slice(&h.as_bytes()[..8]); f
400        };
401
402        recipients.push(RecipientEntry {
403            label,
404            mlkem_ct: ct.as_bytes().to_vec(),
405            wrap: wrapped_dek.clone(), // legacy mirror
406            wrapped_dek,
407            wrap_nonce,
408            x25519_pk_fpr,
409            x25519_pub: recip_x25519_pk_bytes.to_vec(),
410        });
411    }
412
413    // 4) Derive keys and file-id from CEK with enhanced domain separation
414    let confirm = b"qsfs_confirm_v2";
415    let keys = hkdf_expand_keys(cek.expose_secret(), Some(confirm));
416    let file_id = derive_file_nonce_seed(cek.expose_secret());
417    
418    let mut hdr = header::Header {
419        magic: *b"QSFS2\0",
420        chunk_size: req.chunk_size as u32,
421        file_id,
422        blake3_of_plain: [0u8;32],
423        suite: SuiteId::current(),
424        kdf_salt: Some(kdf_salt),
425        recipients,
426        #[cfg(feature="hybrid-x25519")]
427        eph_x25519_pk: *eph_x_pk.as_bytes(),
428        #[cfg(not(feature="hybrid-x25519"))]
429        eph_x25519_pk: [0u8;32],
430        mldsa_sig: vec![],
431        ed25519_sig: vec![],
432        signature_metadata: None,
433        fin: 1,
434    };
435    
436    // Sign header with ML-DSA-87 if signer is provided
437    if let Some(signer) = req.signer {
438        let canonical_bytes = CanonicalHeader::serialize(&hdr)?;
439        let signature = signer.sign(&canonical_bytes)?;
440        
441        let sig_metadata = CanonicalSignatureMetadata::new(
442            signer.id_hex(),
443            signer.pk.as_bytes().to_vec(),
444            signature.clone(),
445        );
446        
447        hdr.mldsa_sig = signature;
448        hdr.signature_metadata = Some(SignatureMetadata {
449            signer_id: sig_metadata.signer_id,
450            algorithm: sig_metadata.algorithm,
451            public_key: sig_metadata.public_key,
452        });
453    }
454    
455    // 5) Atomic write: use temporary file with secure permissions
456    let output_dir = Path::new(output_path).parent().unwrap_or(Path::new("."));
457    let mut temp_file = NamedTempFile::new_in(output_dir)?;
458    
459    // Set secure permissions on temporary file
460    set_secure_permissions(temp_file.path()).ok();
461    
462    let hdr_bytes = postcard::to_allocvec(&hdr)?;
463
464    // Write header length + header + encrypted stream
465    temp_file.write_all(&(hdr_bytes.len() as u32).to_be_bytes())?;
466    temp_file.write_all(&hdr_bytes)?;
467
468    let aad = hdr.aead_aad();
469    streaming::encrypt_stream(
470        req.input_path,
471        temp_file.as_file_mut(),
472        req.chunk_size,
473        file_id,
474        &aad,
475        keys.aes_k1.expose_secret(),
476        None,
477    ).await?;
478
479    // Ensure data is written to disk before atomic rename
480    temp_file.as_file_mut().sync_all()?;
481    
482    // Atomic rename
483    temp_file.persist(output_path)?;
484    
485    Ok(())
486}
487
488pub async fn unseal(mut input: File, output_path: &str, ctx: UnsealContext<'_>) -> Result<()> {
489    // Disable core dumps for security
490    disable_core_dumps().ok();
491    
492    // 1) Read header length and header
493    let mut len_buf = [0u8; 4];
494    input.read_exact(&mut len_buf).await?;
495    let hdr_len = u32::from_be_bytes(len_buf) as usize;
496    
497    if hdr_len > 1024 * 1024 {
498        return Err(anyhow::anyhow!("Header too large: {}", hdr_len));
499    }
500    
501    let mut hdr_buf = vec![0u8; hdr_len];
502    input.read_exact(&mut hdr_buf).await?;
503    let hdr: Header = postcard::from_bytes(&hdr_buf)?;
504    // Enforce magic/version
505    if hdr.magic != *b"QSFS2\0" {
506        return Err(anyhow::anyhow!("Unrecognized file format (bad magic)"));
507    }
508    
509    // 2) Verify signature if present (default behavior)
510    if !hdr.mldsa_sig.is_empty() {
511        // Signature is present - verify it
512        let canonical_bytes = CanonicalHeader::serialize(&hdr)?;
513        
514        if let Some(sig_metadata) = &hdr.signature_metadata {
515            let public_key_bytes = general_purpose::STANDARD
516                .decode(&sig_metadata.public_key)
517                .map_err(|e| anyhow::anyhow!("Invalid public key base64: {}", e))?;
518            
519            // Verify signature
520            let signature_valid = verify_signature(&canonical_bytes, &hdr.mldsa_sig, &public_key_bytes)?;
521            if !signature_valid {
522                return Err(anyhow::anyhow!("❌ ML-DSA-87 signature verification failed"));
523            }
524            
525            // Check trust store unless --trust-any-signer is specified
526            if !ctx.trust_any_signer {
527                let trust_store = TrustStore::load_from_file(default_trustdb_path()?)?;
528                if !trust_store.is_trusted(&sig_metadata.signer_id) {
529                    return Err(anyhow::anyhow!(
530                        "❌ Signer not trusted: {} (use 'qsfs trust add' or --trust-any-signer)", 
531                        sig_metadata.signer_id
532                    ));
533                }
534            }
535            
536            eprintln!("✅ ML-DSA-87 signature verified: {}", sig_metadata.signer_id);
537        } else {
538            return Err(anyhow::anyhow!("❌ Signature present but metadata missing"));
539        }
540    } else {
541        // No signature present
542        if !ctx.allow_unsigned {
543            return Err(anyhow::anyhow!(
544                "❌ File is not signed. Use --allow-unsigned to decrypt unsigned files (security risk)"
545            ));
546        }
547        eprintln!("⚠️  Processing unsigned file (--allow-unsigned specified)");
548    }
549
550    // 3) Try to decrypt CEK with our key (verifiable decapsulation)
551    let mut cek_bytes = None;
552    for rec in &hdr.recipients {
553        if let Ok(ct) = pqcrypto_mlkem::mlkem1024::Ciphertext::from_bytes(&rec.mlkem_ct) {
554            let ss = mlkem::decapsulate(&ct, ctx.mlkem_sk);
555            #[cfg(feature="hybrid-x25519")]
556            {
557                if let Some(xsk) = ctx.x25519_sk {
558                    let recip_x_sk = x25519_dalek::StaticSecret::from(xsk);
559                    let eph_x_pk = x25519_dalek::PublicKey::from(hdr.eph_x25519_pk);
560                    let x_ss = recip_x_sk.diffie_hellman(&eph_x_pk);
561                    let kek = derive_kek(ss.as_bytes(), x_ss.as_bytes(), hdr.kdf_salt.as_ref().map(|s| s.as_slice()));
562                    if rec.wrapped_dek.len() == 48 {
563                        if let Ok(cek) = unwrap_dek(&kek, &rec.wrap_nonce, &rec.wrapped_dek) {
564                            cek_bytes = Some((cek, b"qsfs_confirm_v2".to_vec()));
565                            break;
566                        }
567                    }
568                } else {
569                    return Err(anyhow::anyhow!("X25519 secret required for hybrid decrypt"));
570                }
571            }
572            #[cfg(not(feature="hybrid-x25519"))]
573            {
574                // Non-hybrid: use KEK derived from ML-KEM SS and unwrap via AES-GCM
575                let kek = derive_kek(ss.as_bytes(), &[], hdr.kdf_salt.as_ref().map(|s| s.as_slice()));
576                if rec.wrapped_dek.len() == 48 {
577                    if let Ok(cek) = unwrap_dek(&kek, &rec.wrap_nonce, &rec.wrapped_dek) {
578                        cek_bytes = Some((cek, b"qsfs_confirm_v2".to_vec()));
579                        break;
580                    }
581                }
582            }
583        }
584    }
585    
586    let (cek, confirm) = cek_bytes.ok_or_else(|| anyhow::anyhow!("No matching recipient key"))?;
587    
588    // 4) Derive keys from CEK
589    let keys = hkdf_expand_keys(&cek, Some(&confirm));
590    
591    // 5) Atomic write: use temporary file with secure permissions
592    let output_dir = Path::new(output_path).parent().unwrap_or(Path::new("."));
593    let mut temp_file = NamedTempFile::new_in(output_dir)?;
594    
595    // Set secure permissions on temporary file
596    set_secure_permissions(temp_file.path()).ok();
597    
598    let aad = hdr.aead_aad();
599    let mut rest = input;
600    streaming::decrypt_stream(&mut rest, temp_file.as_file_mut(), hdr.file_id, &aad,
601        keys.aes_k1.expose_secret(),
602        None,
603    ).await?;
604
605    // Ensure data is written to disk before atomic rename
606    temp_file.as_file_mut().sync_all()?;
607    
608    // Atomic rename
609    temp_file.persist(output_path)?;
610    
611    Ok(())
612}