I implemented TPM-based key wrapping on Windows using the windows-sys crate.
(Linux version: How I Used TPM for Key Encryption in Rust on Linux (Hardware TPM & vTPM))
At first, I tried key wrapping with tss-esapi, but since it's designed for Linux, using it on Windows would have required a more complex setup.
So I chose windows-sys for this implementation.
In this article, I’ll walk you through the key-wrapping implementation and explain how it works!
Table of Contents
Cargo.toml
[dependencies]
windows-sys = { version = "0.61", features = [
"Win32_Security_Cryptography",
"Win32_Foundation",
] }
rand = "0.9"
zeroize = "1.8"
Implementation
This crate uses FFI, so you'll need to call the Windows APIs inside an unsafe block.
use std::{ffi::OsStr, os::windows::ffi::OsStrExt, ptr};
use windows_sys::{
core::HRESULT,
Win32::{
Foundation::NTE_BAD_KEYSET,
Security::Cryptography::*,
},
};
use zeroize::Zeroizing;
type TpmResult<T> = Result<T, HRESULT>;
const KEY_NAME: &str = "RSA_KEY";
fn to_utf16(s: &str) -> Vec<u16> {
let mut utf16: Vec<u16> = OsStr::new(s).encode_wide().collect();
utf16.push(0);
utf16
}
struct KeyWrapper {
hprov: NCRYPT_PROV_HANDLE,
hkey: NCRYPT_KEY_HANDLE,
}
impl Drop for KeyWrapper {
fn drop(&mut self) {
// Free retrieved provider and key handles
unsafe {
if self.hkey != 0 {
NCryptFreeObject(self.hkey);
}
if self.hprov != 0 {
NCryptFreeObject(self.hprov);
}
}
}
}
// The target key is 32 bytes (AES-256), which is generally recommended
// The status is an i32 (HRESULT) value,
// and 0 means the function was successful
impl KeyWrapper {
fn open(key_name: &str) -> TpmResult<Self> {
let hprov: NCRYPT_PROV_HANDLE = Self::open_provider()?;
let key_name_utf16 = to_utf16(key_name);
let hkey = match Self::retrieve_key(hprov, key_name_utf16) {
Ok(h) => h,
Err(e) => {
unsafe {
NCryptFreeObject(hprov);
}
return Err(e);
}
};
Ok(Self { hprov, hkey })
}
fn open_provider() -> TpmResult<NCRYPT_PROV_HANDLE> {
let mut hprov: NCRYPT_PROV_HANDLE = 0;
let status =
unsafe { NCryptOpenStorageProvider(&mut hprov, MS_PLATFORM_CRYPTO_PROVIDER, 0) };
if status != 0 {
eprintln!("Failed to open the storage provider");
return Err(status);
}
Ok(hprov)
}
fn retrieve_key(hprov: NCRYPT_PROV_HANDLE, key_name: Vec<u16>) -> TpmResult<NCRYPT_KEY_HANDLE> {
let mut hkey: NCRYPT_KEY_HANDLE = 0;
let status = unsafe { NCryptOpenKey(hprov, &mut hkey, key_name.as_ptr(), 0, 0) };
match status {
0 => Ok(hkey),
NTE_BAD_KEYSET => {
// If no key with that name exists
println!("");
Self::create_rsa_key(hprov, key_name)
}
_ => {
eprintln!("Failed to retrieve the key");
Err(status)
}
}
}
fn create_padding_info() -> BCRYPT_OAEP_PADDING_INFO {
BCRYPT_OAEP_PADDING_INFO {
pszAlgId: BCRYPT_SHA256_ALGORITHM,
pbLabel: ptr::null_mut(),
cbLabel: 0,
}
}
fn create_rsa_key(
hprov: NCRYPT_PROV_HANDLE,
key_name: Vec<u16>,
) -> TpmResult<NCRYPT_KEY_HANDLE> {
let mut hkey: NCRYPT_KEY_HANDLE = 0;
let status = unsafe {
// Create a persistent RSA key with the given name
NCryptCreatePersistedKey(
hprov,
&mut hkey,
BCRYPT_RSA_ALGORITHM,
key_name.as_ptr(),
0,
0,
)
};
if status != 0 {
eprintln!("Failed to create the RSA key");
return Err(status);
}
// Finalize the key for use
let status = unsafe { NCryptFinalizeKey(hkey, 0) };
if status != 0 {
eprintln!("Failed to finalize the key");
unsafe {
NCryptFreeObject(hkey);
}
return Err(status);
}
Ok(hkey)
}
fn wrap_key(&self, target_key: &[u8]) -> TpmResult<Vec<u8>> {
let padding_info = Self::create_padding_info();
let mut size: u32 = 0;
// Zeroize the local copy on drop
let target_key = Zeroizing::new(target_key.to_vec());
// Get the required output size if needed
let status = unsafe {
NCryptEncrypt(
self.hkey,
target_key.as_ptr(),
target_key.len() as u32,
(&padding_info as *const BCRYPT_OAEP_PADDING_INFO).cast(),
ptr::null_mut(),
0,
&mut size,
NCRYPT_PAD_OAEP_FLAG,
)
};
if status != 0 {
eprintln!("Failed to get the required output size");
return Err(status);
}
let mut wrapped_key = vec![0u8; size as usize];
// Wrap the target key
let status = unsafe {
NCryptEncrypt(
self.hkey,
target_key.as_ptr(),
target_key.len() as u32,
(&padding_info as *const BCRYPT_OAEP_PADDING_INFO).cast(),
wrapped_key.as_mut_ptr(),
size,
&mut size,
NCRYPT_PAD_OAEP_FLAG,
)
};
if status != 0 {
eprintln!("Failed to wrap the key");
return Err(status);
}
// Truncate to the actual size
wrapped_key.truncate(size as usize);
Ok(wrapped_key)
}
fn unwrap_key(&self, target_key: &[u8]) -> TpmResult<Vec<u8>> {
let padding_info = Self::create_padding_info();
let mut size: u32 = 0;
// Get the required output size if needed
let status = unsafe {
NCryptDecrypt(
self.hkey,
target_key.as_ptr(),
target_key.len() as u32,
(&padding_info as *const BCRYPT_OAEP_PADDING_INFO).cast(),
ptr::null_mut(),
0,
&mut size,
NCRYPT_PAD_OAEP_FLAG,
)
};
if status != 0 {
eprintln!("Failed to get the required output size");
return Err(status);
}
let mut unwrapped_key = vec![0u8; size as usize];
// Unwrap the target key
let status = unsafe {
NCryptDecrypt(
self.hkey,
target_key.as_ptr(),
target_key.len() as u32,
(&padding_info as *const BCRYPT_OAEP_PADDING_INFO).cast(),
unwrapped_key.as_mut_ptr(),
size,
&mut size,
NCRYPT_PAD_OAEP_FLAG,
)
};
if status != 0 {
eprintln!("Failed to unwrap the key");
return Err(status);
}
// Truncate to the actual size
unwrapped_key.truncate(size as usize);
Ok(unwrapped_key)
}
}
The retrieved provider and key handles need to be freed at the end.
In this implementation, the cleanup is handled in Drop.
I also checked the RSA key length used by the TPM provider with NCryptGetProperty().
In my environment, the default was 2048 bits, and the supported range was 1024 to 2048 bits.
This may vary depending on the environment, so if you want to check it on your own system, see the Checking and Setting Key Properties section.
Try running it in the main function:
fn main() {
let key: [u8; 32] = rand::random();
println!("Original key: {:?}", key);
let key_wrapper = match KeyWrapper::open(KEY_NAME) {
Ok(s) => s,
Err(e) => {
eprintln!("Error code: {e}");
return;
}
};
let wrapped_key = match key_wrapper.wrap_key(&key) {
Ok(k) => {
println!("Wrapped key: {:?}", k);
k
}
Err(e) => {
eprintln!("Error code: {e}");
return;
}
};
match key_wrapper.unwrap_key(&wrapped_key) {
Ok(k) => println!("Unwrapped key: {:?}", k),
Err(e) => eprintln!("Error code: {e}"),
}
}
If you want to delete the key, you can add delete_key() to KeyWrapper and call it:
fn delete_key(&mut self) -> TpmResult<()> {
let status = unsafe {
// Delete the key
NCryptDeleteKey(self.hkey, 0)
};
if status != 0 {
eprintln!("Failed to delete the key");
return Err(status);
}
// NCryptDeleteKey() automatically releases the key handle
self.hkey = 0;
Ok(())
}
Error Codes
This is a list of the main error codes, their corresponding constants, and descriptions.
| Code (i32) | Code (hex) | Name | Description |
|---|---|---|---|
| 0 | 0x00000000 | ERROR_SUCCESS | Operation succeeded |
| -2146893802 | 0x80090006 | NTE_BAD_KEYSET | The key does not exist or is not registered with the TPM |
| -2146893809 | 0x8009000F | NTE_EXISTS | A key with the same name already exists |
| -2146893786 | 0x80090026 | NTE_INVALID_HANDLE | The handle is invalid (provider or key) |
| -2146893785 | 0x80090027 | NTE_INVALID_PARAMETER | One or more parameters are invalid |
| -2146893810 | 0x8009000E | NTE_NO_MEMORY | Not enough memory to complete the operation |
| -2146893808 | 0x80090010 | NTE_PERM | Access denied or operation not permitted |
| -2146893792 | 0x80090020 | NTE_FAIL | An internal error occurred |
| -2146893783 | 0x80090029 | NTE_NOT_SUPPORTED | The algorithm or option is not supported |
| -2146893815 | 0x80090009 | NTE_BAD_FLAGS | Invalid flags specified in dwFlags |
| -2146893784 | 0x80090028 | NTE_BUFFER_TOO_SMALL | Output buffer is too small |
| -2147479534 | 0x80090032 | NTE_INVALID_STATE | Object is in an invalid state (e.g. not finalized) |
| -2146893816 | 0x80090008 | NTE_BAD_ALGID | Invalid algorithm identifier |
| -2146893814 | 0x8009000A | NTE_BAD_TYPE | The key or object type is invalid |
Explanation
I referred to the official Microsoft documentation to choose the right functions and understand each parameter, and I used the windows-sys docs to check the parameter types.
References:
- Microsoft documentation for NCrypt → ncrypt.h header
- windows-sys documentation → Module Cryptography
In this section, I'll explain each function and parameter used in the implementation, based on these official documents.
1. Open the TPM Provider
First, you need to open the provider using NCryptOpenStorageProvider().
Microsoft Documentation:
windows-sys Documentation:
For phprovider, pass a variable (&mut usize) to receive the provider handle.
For pszprovidername, pass the alias of a key storage provider to load.
If you pass std::ptr::null(), the default key storage provider is loaded.
This implementation uses MS_PLATFORM_CRYPTO_PROVIDER, but if a TPM isn’t available, you can use MS_KEY_STORAGE_PROVIDER instead.
The dwflags parameter is reserved, so it should always be set to 0 for now.
let mut hprov: NCRYPT_PROV_HANDLE = 0;
let status =
unsafe { NCryptOpenStorageProvider(&mut hprov, MS_PLATFORM_CRYPTO_PROVIDER, 0) };
Some of the return codes:
If this function fails, do not use the provider handle in other functions.
It’s described in the Microsoft documentation:
2. Create a Persistent Key (If It Doesn't Exist)
NCryptCreatePersistedKey() can create a persistent key with a given name.
Microsoft Documentation:
windows-sys Documentation:
For phkey, pass a variable (&mut usize) to receive the key handle.
For pszalgid, pass a null-terminated UTF-16 string that specifies the cryptographic algorithm.
In most cases, you specify one of the CNG Algorithm Identifiers, but you can also specify algorithm identifiers that are registered independently by the provider.
This implementation uses BCRYPT_RSA_ALGORITHM.
For pszkeyname, pass a pointer to a null-terminated UTF-16 string containing the key name.
If the parameter is set to std::ptr::null(), this function will create an ephemeral key that won't be stored.
fn to_utf16(s: &str) -> Vec<u16> {
let mut utf16: Vec<u16> = OsStr::new(s).encode_wide().collect();
utf16.push(0);
utf16
}
encode_wide() converts an &OsStr to UTF-16.
Since pszkeyname needs a null-terminated string, a trailing 0 is appended.
For dwlegacykeyspec, pass a legacy identifier that specifies the key type.
In typical CNG key creation, pass 0, since the other values are for CryptoAPI(CAPI)/CSP compatibility.
If you set it to anything other than 0, it may fail with an error (NTE_NOT_SUPPORTED).
For dwflags, pass flags that modify the behavior of this function.
If no flags are needed, pass 0.
let mut hkey: NCRYPT_KEY_HANDLE = 0;
let status = unsafe {
// Create a persistent RSA key with the given name
NCryptCreatePersistedKey(
hprov,
&mut hkey,
BCRYPT_RSA_ALGORITHM,
key_name.as_ptr(),
0,
0,
)
};
Some of the return codes:
After creating the key, you can use NCryptSetProperty() to configure its properties.
Details about setting the key length are covered in the Checking and Setting Key Properties section.
After creating the key, you need to call NCryptFinalizeKey() to use it.
Microsoft Documentation:
windows-sys Documentation:
For dwflags, pass flags that modify the behavior of this function.
If no flags are needed, pass 0.
NCRYPT_SILENT_FLAG prevents the user interface from being displayed, but it is usually not needed for the TPM-based processing in this example.
let status = unsafe { NCryptFinalizeKey(hkey, 0) };
Some of the return codes:
3. Get a Key from the Provider
If the key already exists, you can retrieve it from the provider using NCryptOpenKey().
Microsoft Documentation:
windows-sys Documentation:
These parameters are mostly the same as those of NCryptCreatePersistedKey().
For dwflags, pass flags that modify the behavior of this function.
If no flags are needed, pass 0.
fn retrieve_key(hprov: NCRYPT_PROV_HANDLE, key_name: Vec<u16>) -> TpmResult<NCRYPT_KEY_HANDLE> {
let mut hkey: NCRYPT_KEY_HANDLE = 0;
let status = unsafe { NCryptOpenKey(hprov, &mut hkey, key_name.as_ptr(), 0, 0) };
match status {
0 => Ok(hkey),
NTE_BAD_KEYSET => {
// If no key with that name exists
println!("");
Self::create_rsa_key(hprov, key_name)
}
_ => {
eprintln!("Failed to retrieve the key");
Err(status)
}
}
}
If no key with the specified name exists, NCryptOpenKey() returns NTE_BAD_KEYSET.
Some of the return codes:
4. Get the Encrypted Data Size
You need to get the size of the encrypted data before encryption.
If you already know it, you can skip this step.
Microsoft Documentation:
windows-sys Documentation:
For pbinput, pass a pointer to the buffer to encrypt.
For cdinput, pass the size (in bytes) of the data (pbinput).
For ppaddinginfo, pass a pointer to a BCRYPT_OAEP_PADDING_INFO struct that contains padding information.
If you don’t pass NCRYPT_PAD_OAEP_FLAG in dwflags, or if you’re using a symmetric key, pass std::ptr::null().
For pboutput, pass a pointer to the output buffer for the encrypted data.
If you only want to get the encrypted data size, pass std::ptr::null_mut().
For cboutput, pass the buffer size (pboutput).
If pboutput is std::ptr::null_mut(), this parameter is ignored.
However, in my test, passing a non-zero value caused an NTE_INVALID_PARAMETER error.
For pcbresult, pass a variable (&mut u32) that receives the output size for pboutput.
If pboutput is std::ptr::null_mut(), this parameter receives the required output size.
For dwflags, pass a flag that modifies the behavior of this function.
The allowed flags depend on the key type specified by hkey.
NCRYPT_PAD_OAEP_FLAG is recommended for modern security.
When using this flag, you need to pass a pointer to a BCRYPT_OAEP_PADDING_INFO struct in ppaddinginfo.
For symmetric keys, pass 0.
let padding_info = Self::create_padding_info();
let mut size: u32 = 0;
// Zeroize the local copy on drop
let target_key = Zeroizing::new(target_key.to_vec());
// Get the required output size if needed
let status = unsafe {
NCryptEncrypt(
self.hkey,
target_key.as_ptr(),
target_key.len() as u32,
(&padding_info as *const BCRYPT_OAEP_PADDING_INFO).cast(),
ptr::null_mut(),
0,
&mut size,
NCRYPT_PAD_OAEP_FLAG,
)
};
Some of the return codes:
BCRYPT_OAEP_PADDING_INFO
Microsoft Documentation:
windows-sys Documentation:
For pszAlgId, pass the hashing algorithm used to create the padding.
You can find the available hash algorithms in the CNG Algorithm Identifiers.
For pbLabel, pass a pointer to a buffer that contains label data used for OAEP padding.
If you don't need it, pass std::ptr::null_mut().
For cbLabel, pass the size of the pbLabel buffer.
If pbLabel is std::ptr::null_mut(), pass 0.
fn create_padding_info() -> BCRYPT_OAEP_PADDING_INFO {
BCRYPT_OAEP_PADDING_INFO {
pszAlgId: BCRYPT_SHA256_ALGORITHM,
pbLabel: ptr::null_mut(),
cbLabel: 0,
}
}
This code uses BCRYPT_SHA256_ALGORITHM as the hash algorithm.
5. Wrap the Key
Finally, wrap the key using NCryptEncrypt().
let mut wrapped_key = vec![0u8; size as usize];
// Wrap the target key
let status = unsafe {
NCryptEncrypt(
self.hkey,
target_key.as_ptr(),
target_key.len() as u32,
(&padding_info as *const BCRYPT_OAEP_PADDING_INFO).cast(),
wrapped_key.as_mut_ptr(),
size,
&mut size,
NCRYPT_PAD_OAEP_FLAG,
)
};
To get the encrypted data, prepare a buffer with the required size, and pass a pointer to that buffer in pboutput.
If you queried the size with this function, allocate the buffer using the size stored in the variable you passed to pcbresult.
See the previous section for an explanation of NCryptEncrypt().
Lastly, truncate the output buffer to the actual size written, as returned in pcbresult.
wrapped_key.truncate(size as usize);
Unwrap the Key
Decrypting the wrapped key is almost the same as encryption — just replace NCryptEncrypt() with NCryptDecrypt().
fn unwrap_key(&self, target_key: &[u8]) -> TpmResult<Vec<u8>> {
let padding_info = Self::create_padding_info();
let mut size: u32 = 0;
// Get the required output size if needed
let status = unsafe {
NCryptDecrypt(
self.hkey,
target_key.as_ptr(),
target_key.len() as u32,
(&padding_info as *const BCRYPT_OAEP_PADDING_INFO).cast(),
ptr::null_mut(),
0,
&mut size,
NCRYPT_PAD_OAEP_FLAG,
)
};
if status != 0 {
eprintln!("Failed to get the required output size");
return Err(status);
}
let mut unwrapped_key = vec![0u8; size as usize];
// Unwrap the target key
let status = unsafe {
NCryptDecrypt(
self.hkey,
target_key.as_ptr(),
target_key.len() as u32,
(&padding_info as *const BCRYPT_OAEP_PADDING_INFO).cast(),
unwrapped_key.as_mut_ptr(),
size,
&mut size,
NCRYPT_PAD_OAEP_FLAG,
)
};
if status != 0 {
eprintln!("Failed to unwrap the key");
return Err(status);
}
// Truncate to the actual size
unwrapped_key.truncate(size as usize);
Ok(unwrapped_key)
}
Some of the return codes:
Checking and Setting Key Properties
To check an object's properties, use NCryptGetProperty().
Microsoft documentation:
windows-sys documentation:
For hobject, pass the handle of the target object.
To retrieve key properties, pass a key handle.
For pszproperty, pass the property identifier you want to retrieve.
You can find a list of identifiers in Key Storage Property Identifiers.
If you want to check supported key lengths, the default length, and related values, pass NCRYPT_LENGTHS_PROPERTY.
For pboutput, pass a pointer to the buffer that receives the property value.
If you only want to get the required buffer size, pass std::ptr::null_mut().
For cboutput, pass the size of the output buffer (pboutput) in bytes.
If pboutput is std::ptr::null_mut(), pass 0.
For pcbresult, pass a pointer to a variable (&mut u32) that receives either the number of bytes actually written or the required buffer size.
For dwflags, pass flags that modify the behavior of this function.
If no flags are needed, pass 0.
fn check_key_lengths(&self) -> TpmResult<()> {
let mut supported_lengths = NCRYPT_SUPPORTED_LENGTHS {
dwMinLength: 0,
dwMaxLength: 0,
dwIncrement: 0,
dwDefaultLength: 0,
};
let mut size: u32 = 0;
let status = unsafe {
NCryptGetProperty(
self.hkey,
NCRYPT_LENGTHS_PROPERTY,
(&mut supported_lengths as *mut NCRYPT_SUPPORTED_LENGTHS).cast(),
size_of::<NCRYPT_SUPPORTED_LENGTHS>() as u32,
&mut size,
0,
)
};
if status != 0 {
eprintln!("Failed to retrieve the key property");
return Err(status);
}
println!(
"Default length: {} bits, Supported range: {} to {} bits",
supported_lengths.dwDefaultLength,
supported_lengths.dwMinLength,
supported_lengths.dwMaxLength,
);
Ok(())
}
To retrieve the current key length, specify NCRYPT_LENGTH_PROPERTY as the property identifier.
fn check_current_key_length(&self) -> TpmResult<()> {
let mut cur_key_length: u32 = 0;
let mut size: u32 = 0;
let status = unsafe {
NCryptGetProperty(
self.hkey,
NCRYPT_LENGTH_PROPERTY,
(&mut cur_key_length as *mut u32).cast(),
size_of::<u32>() as u32,
&mut size,
0,
)
};
if status != 0 {
eprintln!("Failed to retrieve the key property");
return Err(status);
}
println!("Current key length: {cur_key_length} bits");
Ok(())
}
Some of the return codes:
To set an object's properties, use NCryptSetProperty().
For a newly created key, the property must be set before calling NCryptFinalizeKey().
Microsoft documentation:
windows-sys documentation:
For pszproperty, pass the property identifier you want to set.
If you want to set the key length, pass NCRYPT_LENGTH_PROPERTY.
For pbinput, pass a pointer to a buffer that contains the new property value.
For cbinput, pass the size of the buffer passed to pbinput in bytes.
For dwflags, pass flags that modify the behavior of this function.
If no flags are needed, pass 0.
fn set_key_length(hkey: NCRYPT_KEY_HANDLE, length: u32) -> TpmResult<()> {
let status = unsafe {
NCryptSetProperty(
hkey,
NCRYPT_LENGTH_PROPERTY,
(&length as *const u32).cast(),
size_of::<u32>() as u32,
NCRYPT_PERSIST_FLAG,
)
};
if status != 0 {
eprintln!("Failed to set the key property");
return Err(status);
}
Ok(())
}
Some of the return codes:
Delete Registered Key
You can delete the registered key using NCryptDeleteKey().
Microsoft Documentation:
windows-sys Documentation:
For dwflags, pass a flag that modifies the behavior of this function.
The only supported value is NCRYPT_SILENT_FLAG.
If no flags are needed, pass 0.
let status = unsafe {
// Delete the key
NCryptDeleteKey(self.hkey, 0)
};
If this function succeeds, the key handle is freed automatically.
It’s described in the Microsoft documentation:
Some of the return codes:
Final Thoughts
At first, since I didn’t know much about C++, I struggled to read the function signatures.
But through this implementation, I came to understand them better, and I learned how TPM works.
Thanks for reading!





































Top comments (0)