win_util: add DPAPI wrappers

Adds wrappers for CryptProtectData and CryptUnprotectData.

BUG=b:324301901
TEST=unit tests

Change-Id: I89373a6e546c0364471d05b3f665de0e96b3cd81
Reviewed-on: https://chromium-review.googlesource.com/c/crosvm/crosvm/+/5277999
Reviewed-by: Judson Powers <judsonp@google.com>
Reviewed-by: Vikram Auradkar <auradkar@google.com>
Commit-Queue: Noah Gold <nkgold@google.com>
This commit is contained in:
Noah Gold 2024-02-07 16:00:43 -08:00 committed by crosvm LUCI
parent 9a180d024b
commit 75e21814ac
2 changed files with 183 additions and 0 deletions

181
win_util/src/dpapi.rs Normal file
View file

@ -0,0 +1,181 @@
// Copyright 2024 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#![deny(unsafe_op_in_unsafe_fn)]
//! Safe, Rusty wrappers around DPAPI.
use std::ffi::c_void;
use std::ptr;
use std::slice;
use anyhow::Context;
use anyhow::Result;
use winapi::um::dpapi::CryptProtectData;
use winapi::um::dpapi::CryptUnprotectData;
use winapi::um::winbase::LocalFree;
use winapi::um::wincrypt::DATA_BLOB;
use crate::syscall_bail;
/// Wrapper around buffers allocated by DPAPI that can be freed with LocalFree.
pub struct LocalAllocBuffer {
ptr: *mut u8,
len: usize,
}
impl LocalAllocBuffer {
/// # Safety
/// 0. ptr is a valid buffer of length len and is safe to free with LocalFree.
/// 1. The caller transfers ownership of the buffer to this object on construction.
unsafe fn new(ptr: *mut u8, len: usize) -> Self {
Self { ptr, len }
}
pub fn as_mut_slice(&mut self) -> &mut [u8] {
// SAFETY: ptr is a pointer to a buffer of length len.
unsafe { slice::from_raw_parts_mut(self.ptr, self.len) }
}
pub fn as_slice(&self) -> &[u8] {
// SAFETY: ptr is a pointer to a buffer of length len.
unsafe { slice::from_raw_parts(self.ptr, self.len) }
}
}
impl Drop for LocalAllocBuffer {
fn drop(&mut self) {
// SAFETY: when this struct is created, the caller guarantees
// ptr is a valid pointer to a buffer that can be freed with LocalFree.
unsafe {
LocalFree(self.ptr as *mut c_void);
}
}
}
/// # Summary
/// Wrapper around CryptProtectData that displays no UI.
pub fn crypt_protect_data(plaintext: &mut [u8]) -> Result<LocalAllocBuffer> {
let mut plaintext_blob = DATA_BLOB {
cbData: plaintext
.len()
.try_into()
.context("plaintext size won't fit in DWORD")?,
pbData: plaintext.as_mut_ptr(),
};
let mut ciphertext_blob = DATA_BLOB {
cbData: 0,
pbData: ptr::null_mut(),
};
// SAFETY: the FFI call is safe because
// 1. plaintext_blob lives longer than the call.
// 2. ciphertext_blob lives longer than the call, and we later give
// ownership of the memory the kernel allocates to LocalAllocBuffer
// which guarantees it is freed.
let res = unsafe {
CryptProtectData(
&mut plaintext_blob as *mut _,
/* szDataDescr= */ ptr::null_mut(),
/* pOptionalEntropy= */ ptr::null_mut(),
/* pvReserved= */ ptr::null_mut(),
/* pPromptStruct */ ptr::null_mut(),
/* dwFlags */ 0,
&mut ciphertext_blob as *mut _,
)
};
if res == 0 {
syscall_bail!("CryptProtectData failed");
}
let ciphertext_len: usize = ciphertext_blob
.cbData
.try_into()
.context("resulting ciphertext had an invalid size")?;
// SAFETY: safe because ciphertext_blob refers to a valid buffer of the specified length. This
// is guaranteed because CryptProtectData returned success.
Ok(unsafe { LocalAllocBuffer::new(ciphertext_blob.pbData, ciphertext_len) })
}
/// # Summary
/// Wrapper around CryptProtectData that displays no UI.
pub fn crypt_unprotect_data(ciphertext: &mut [u8]) -> Result<LocalAllocBuffer> {
let mut ciphertext_blob = DATA_BLOB {
cbData: ciphertext
.len()
.try_into()
.context("plaintext size won't fit in DWORD")?,
pbData: ciphertext.as_mut_ptr(),
};
let mut plaintext_blob = DATA_BLOB {
cbData: 0,
pbData: ptr::null_mut(),
};
// SAFETY: the FFI call is safe because
// 1. ciphertext_blob lives longer than the call.
// 2. plaintext_blob lives longer than the call, and we later give
// ownership of the memory the kernel allocates to LocalAllocBuffer
// which guarantees it is freed.
let res = unsafe {
CryptUnprotectData(
&mut ciphertext_blob as *mut _,
/* szDataDescr= */ ptr::null_mut(),
/* pOptionalEntropy= */ ptr::null_mut(),
/* pvReserved= */ ptr::null_mut(),
/* pPromptStruct */ ptr::null_mut(),
/* dwFlags */ 0,
&mut plaintext_blob as *mut _,
)
};
if res == 0 {
syscall_bail!("CryptUnprotectData failed");
}
let plaintext_len: usize = plaintext_blob
.cbData
.try_into()
.context("resulting plaintext had an invalid size")?;
// SAFETY: safe because plaintext_blob refers to a valid buffer of the specified length. This
// is guaranteed because CryptUnprotectData returned success.
Ok(unsafe { LocalAllocBuffer::new(plaintext_blob.pbData, plaintext_len) })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encrypt_empty_string_is_valid() {
let plaintext_str = "";
let mut plaintext_buffer = Vec::from(plaintext_str.as_bytes());
let mut ciphertext_buffer = crypt_protect_data(plaintext_buffer.as_mut_slice()).unwrap();
let decrypted_plaintext_buffer =
crypt_unprotect_data(ciphertext_buffer.as_mut_slice()).unwrap();
let decrypted_plaintext_str =
std::str::from_utf8(decrypted_plaintext_buffer.as_slice()).unwrap();
assert_eq!(plaintext_str, decrypted_plaintext_str);
}
#[test]
fn encrypt_decrypt_plaintext_matches() {
let plaintext_str = "test plaintext";
let mut plaintext_buffer = Vec::from(plaintext_str.as_bytes());
let mut ciphertext_buffer = crypt_protect_data(plaintext_buffer.as_mut_slice()).unwrap();
// If our plaintext & ciphertext are the same, something is very wrong.
assert_ne!(plaintext_str.as_bytes(), ciphertext_buffer.as_slice());
// Decrypt the ciphertext and make sure it's our original plaintext.
let decrypted_plaintext_buffer =
crypt_unprotect_data(ciphertext_buffer.as_mut_slice()).unwrap();
let decrypted_plaintext_str =
std::str::from_utf8(decrypted_plaintext_buffer.as_slice()).unwrap();
assert_eq!(plaintext_str, decrypted_plaintext_str);
}
}

View file

@ -55,6 +55,8 @@ use winapi::um::winnt::WCHAR;
pub use crate::dll_notification::*;
pub mod dpapi;
#[macro_export]
macro_rules! syscall_bail {
($details:expr) => {