[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