diff --git a/win_util/src/dpapi.rs b/win_util/src/dpapi.rs new file mode 100644 index 0000000000..d40054b374 --- /dev/null +++ b/win_util/src/dpapi.rs @@ -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 { + 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 { + 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); + } +} diff --git a/win_util/src/lib.rs b/win_util/src/lib.rs index 1e90052486..8619610635 100644 --- a/win_util/src/lib.rs +++ b/win_util/src/lib.rs @@ -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) => {