qsfs_core/lib.rs
1//! # qsfs-core: Quantum-Shield File System Core Library
2//!
3//! [](https://crates.io/crates/qsfs-core)
4//! [](https://docs.rs/qsfs-core)
5//! [](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}