use rand::RngCore;
use windows_sys::Win32::Foundation::LocalFree;
use windows_sys::Win32::Security::Cryptography::{
CryptProtectData, CryptUnprotectData, CRYPTPROTECT_UI_FORBIDDEN, CRYPT_INTEGER_BLOB,
};
use crate::error::Error;
const ENTROPY_LEN: usize = 16;
#[derive(Default)]
pub struct DpapiKeystore;
impl DpapiKeystore {
#[must_use]
pub fn new() -> Self {
Self
}
pub fn seal(&self, plaintext: &[u8]) -> Result<Vec<u8>, Error> {
let mut entropy = [0u8; ENTROPY_LEN];
rand::rngs::OsRng.fill_bytes(&mut entropy);
let cipher = dpapi_protect(plaintext, &entropy)?;
let mut out = Vec::with_capacity(ENTROPY_LEN + cipher.len());
out.extend_from_slice(&entropy);
out.extend_from_slice(&cipher);
Ok(out)
}
pub fn unseal(&self, sealed: &[u8]) -> Result<Vec<u8>, Error> {
if sealed.len() < ENTROPY_LEN {
return Err(Error::CryptoFailure(
"DPAPI envelope shorter than entropy header".to_string(),
));
}
let (entropy, cipher) = sealed.split_at(ENTROPY_LEN);
dpapi_unprotect(cipher, entropy)
}
}
fn dpapi_protect(plaintext: &[u8], entropy: &[u8]) -> Result<Vec<u8>, Error> {
let input = make_blob(plaintext);
let entropy_blob = make_blob(entropy);
let mut output = CRYPT_INTEGER_BLOB {
cbData: 0,
pbData: std::ptr::null_mut(),
};
let ok = unsafe {
CryptProtectData(
&input,
std::ptr::null(), &entropy_blob,
std::ptr::null_mut(), std::ptr::null_mut(), CRYPTPROTECT_UI_FORBIDDEN,
&mut output,
)
};
if ok == 0 {
return Err(Error::CryptoFailure(format!(
"CryptProtectData failed: GetLastError={}",
unsafe { windows_sys::Win32::Foundation::GetLastError() }
)));
}
let result =
unsafe { std::slice::from_raw_parts(output.pbData, output.cbData as usize).to_vec() };
unsafe {
LocalFree(output.pbData.cast());
}
Ok(result)
}
fn dpapi_unprotect(cipher: &[u8], entropy: &[u8]) -> Result<Vec<u8>, Error> {
let input = make_blob(cipher);
let entropy_blob = make_blob(entropy);
let mut output = CRYPT_INTEGER_BLOB {
cbData: 0,
pbData: std::ptr::null_mut(),
};
let ok = unsafe {
CryptUnprotectData(
&input,
std::ptr::null_mut(), &entropy_blob,
std::ptr::null_mut(), std::ptr::null_mut(), CRYPTPROTECT_UI_FORBIDDEN,
&mut output,
)
};
if ok == 0 {
return Err(Error::CryptoFailure(format!(
"CryptUnprotectData failed (different user/machine, or corrupted blob): \
GetLastError={}",
unsafe { windows_sys::Win32::Foundation::GetLastError() }
)));
}
let result =
unsafe { std::slice::from_raw_parts(output.pbData, output.cbData as usize).to_vec() };
unsafe {
LocalFree(output.pbData.cast());
}
Ok(result)
}
fn make_blob(data: &[u8]) -> CRYPT_INTEGER_BLOB {
#[allow(clippy::cast_possible_truncation)]
let cb = data.len() as u32;
CRYPT_INTEGER_BLOB {
cbData: cb,
pbData: data.as_ptr().cast_mut(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn seal_then_unseal_roundtrips() {
let ks = DpapiKeystore::new();
let plaintext = b"the master key wrapped envelope goes here";
let sealed = ks
.seal(plaintext)
.expect("DPAPI seal must succeed in user session");
assert_ne!(
sealed.as_slice(),
plaintext,
"sealed must not equal plaintext"
);
let recovered = ks
.unseal(&sealed)
.expect("DPAPI unseal must succeed in same session");
assert_eq!(recovered, plaintext);
}
#[test]
fn unseal_rejects_corrupted_entropy() {
let ks = DpapiKeystore::new();
let plaintext = b"some payload";
let mut sealed = ks.seal(plaintext).unwrap();
sealed[0] ^= 0x01;
assert!(ks.unseal(&sealed).is_err());
}
#[test]
fn unseal_rejects_corrupted_ciphertext() {
let ks = DpapiKeystore::new();
let plaintext = b"some payload";
let mut sealed = ks.seal(plaintext).unwrap();
let last = sealed.len() - 1;
sealed[last] ^= 0x01;
assert!(ks.unseal(&sealed).is_err());
}
#[test]
fn seal_unseal_handles_large_blob() {
let ks = DpapiKeystore::new();
let plaintext = vec![0xABu8; 4096];
let sealed = ks.seal(&plaintext).unwrap();
assert_eq!(ks.unseal(&sealed).unwrap(), plaintext);
}
#[test]
fn unseal_rejects_truncated_envelope() {
let ks = DpapiKeystore::new();
assert!(ks.unseal(&[0u8; 8]).is_err());
}
}