use crate::device::{DevicePublicKey, DeviceSignature};
use crate::keystore::traits::{
Backend, DIGEST_LEN, DeviceKeyStore, DeviceSigner, DirectMasterKeyStore, MASTER_KEY_LEN,
MasterKey, WrappedMasterKey, WrappedMasterKeyStore, public_key_from_sec1, secrets_equal,
signature_from_raw,
};
use crate::{Error, Result};
use std::fmt;
use windows_sys::Win32::Security::Cryptography::{
BCRYPT_ECCPRIVATE_BLOB, BCRYPT_ECCPUBLIC_BLOB, BCRYPT_ECDSA_PRIVATE_P256_MAGIC,
BCRYPT_OAEP_PADDING_INFO, BCRYPT_SHA256_ALGORITHM, BCryptBuffer, BCryptBufferDesc,
MS_KEY_STORAGE_PROVIDER, MS_PLATFORM_CRYPTO_PROVIDER, NCRYPT_ALLOW_EXPORT_FLAG,
NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG, NCRYPT_DO_NOT_FINALIZE_FLAG, NCRYPT_ECDSA_P256_ALGORITHM,
NCRYPT_EXPORT_POLICY_PROPERTY, NCRYPT_KEY_HANDLE, NCRYPT_LENGTH_PROPERTY, NCRYPT_PAD_OAEP_FLAG,
NCRYPT_PROV_HANDLE, NCRYPT_RSA_ALGORITHM, NCRYPT_SILENT_FLAG, NCRYPTBUFFER_PKCS_KEY_NAME,
NCRYPTBUFFER_VERSION, NCryptCreatePersistedKey, NCryptDecrypt, NCryptDeleteKey, NCryptEncrypt,
NCryptExportKey, NCryptFinalizeKey, NCryptFreeObject, NCryptImportKey, NCryptOpenKey,
NCryptOpenStorageProvider, NCryptSetProperty, NCryptSignHash,
};
#[allow(dead_code, reason = "pinned for tests to check against winerror.h")]
const NTE_BAD_FLAGS: i32 = 0x8009_0009_u32 as i32;
const NTE_NO_KEY: i32 = 0x8009_000D_u32 as i32;
const NTE_EXISTS: i32 = 0x8009_000F_u32 as i32;
const NTE_NOT_FOUND: i32 = 0x8009_0011_u32 as i32;
const NTE_BAD_KEYSET: i32 = 0x8009_0016_u32 as i32;
const KEY_NAME_PREFIX: &str = "Qualia2FA-Device-";
const MAX_LABEL_LEN: usize = 128;
fn key_name_with_prefix(prefix: &str, label: &str) -> Result<Vec<u16>> {
if label.is_empty() {
return Err(Error::InvalidKeyLabel {
reason: "the label is empty",
});
}
if label.len() > MAX_LABEL_LEN {
return Err(Error::InvalidKeyLabel {
reason: "the label is too long (limit is 128 bytes)",
});
}
if !label
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
{
return Err(Error::InvalidKeyLabel {
reason: "the label contains characters outside the allowed set (ASCII letters, digits, - _ .)",
});
}
let name = format!("{prefix}{label}");
let mut wide: Vec<u16> = name.encode_utf16().collect();
wide.push(0);
Ok(wide)
}
fn key_name(label: &str) -> Result<Vec<u16>> {
key_name_with_prefix(KEY_NAME_PREFIX, label)
}
struct CngHandle {
raw: usize,
}
impl CngHandle {
fn new(raw: usize) -> Self {
Self { raw }
}
}
impl Drop for CngHandle {
fn drop(&mut self) {
if self.raw != 0 {
unsafe { NCryptFreeObject(self.raw) };
}
}
}
const fn means_no_such_key(status: i32) -> bool {
matches!(status, NTE_BAD_KEYSET | NTE_NO_KEY | NTE_NOT_FOUND)
}
fn open_provider(provider: *const u16) -> Result<CngHandle> {
let mut handle: NCRYPT_PROV_HANDLE = 0;
let status = unsafe { NCryptOpenStorageProvider(&raw mut handle, provider, 0) };
if status < 0 {
return Err(Error::Hardware {
operation: "NCryptOpenStorageProvider",
code: status,
});
}
Ok(CngHandle::new(handle))
}
fn open_key(provider: &CngHandle, name: &[u16]) -> Result<Option<CngHandle>> {
let mut handle: NCRYPT_KEY_HANDLE = 0;
let status = unsafe {
NCryptOpenKey(
provider.raw,
&raw mut handle,
name.as_ptr(),
0,
NCRYPT_SILENT_FLAG,
)
};
if means_no_such_key(status) {
return Ok(None);
}
if status < 0 {
return Err(Error::Hardware {
operation: "NCryptOpenKey",
code: status,
});
}
Ok(Some(CngHandle::new(handle)))
}
fn create_key(provider: &CngHandle, name: &[u16]) -> Result<Option<CngHandle>> {
let mut handle: NCRYPT_KEY_HANDLE = 0;
let status = unsafe {
NCryptCreatePersistedKey(
provider.raw,
&raw mut handle,
NCRYPT_ECDSA_P256_ALGORITHM,
name.as_ptr(),
0,
0,
)
};
if status == NTE_EXISTS {
return Ok(None);
}
if status < 0 {
return Err(Error::Hardware {
operation: "NCryptCreatePersistedKey",
code: status,
});
}
let guarded = CngHandle::new(handle);
let status = unsafe { NCryptFinalizeKey(guarded.raw, NCRYPT_SILENT_FLAG) };
if status < 0 {
return Err(Error::Hardware {
operation: "NCryptFinalizeKey",
code: status,
});
}
Ok(Some(guarded))
}
fn export_public_key(key: &CngHandle) -> Result<DevicePublicKey> {
let mut needed: u32 = 0;
let status = unsafe {
NCryptExportKey(
key.raw,
0,
BCRYPT_ECCPUBLIC_BLOB,
std::ptr::null(),
std::ptr::null_mut(),
0,
&raw mut needed,
0,
)
};
if status < 0 {
return Err(Error::Hardware {
operation: "NCryptExportKey(query length)",
code: status,
});
}
if needed > 1024 {
return Err(Error::InvalidPublicKey {
reason: "the platform reported an implausible public key length (over 1024 bytes)",
});
}
let mut blob = vec![0_u8; needed as usize];
let status = unsafe {
NCryptExportKey(
key.raw,
0,
BCRYPT_ECCPUBLIC_BLOB,
std::ptr::null(),
blob.as_mut_ptr(),
needed,
&raw mut needed,
0,
)
};
if status < 0 {
return Err(Error::Hardware {
operation: "NCryptExportKey",
code: status,
});
}
blob.truncate(needed as usize);
if blob.len() < 8 {
return Err(Error::InvalidPublicKey {
reason: "the public key blob returned by the platform is shorter than its 8-byte header",
});
}
let cb_key = u32::from_le_bytes([blob[4], blob[5], blob[6], blob[7]]) as usize;
let payload = &blob[8..];
if payload.len() != cb_key * 2 {
return Err(Error::InvalidPublicKey {
reason: "the public key payload length returned by the platform does not match cbKey",
});
}
if cb_key != 32 {
return Err(Error::InvalidPublicKey {
reason: "the public key returned by the platform is not a 32-byte coordinate (not P-256)",
});
}
let mut sec1 = [0_u8; 65];
sec1[0] = 0x04;
sec1[1..].copy_from_slice(payload);
public_key_from_sec1(&sec1)
}
fn sign_digest_raw(key: &CngHandle, digest: &[u8]) -> Result<DeviceSignature> {
if digest.len() != DIGEST_LEN {
return Err(Error::InvalidDigestLength {
expected: DIGEST_LEN,
actual: digest.len(),
});
}
let mut needed: u32 = 0;
let status = unsafe {
NCryptSignHash(
key.raw,
std::ptr::null(),
digest.as_ptr(),
digest.len() as u32,
std::ptr::null_mut(),
0,
&raw mut needed,
0,
)
};
if status < 0 {
return Err(Error::Hardware {
operation: "NCryptSignHash(query length)",
code: status,
});
}
if needed > 1024 {
return Err(Error::InvalidSignatureLength {
expected: 64,
actual: needed as usize,
});
}
let mut signature = vec![0_u8; needed as usize];
let status = unsafe {
NCryptSignHash(
key.raw,
std::ptr::null(),
digest.as_ptr(),
digest.len() as u32,
signature.as_mut_ptr(),
needed,
&raw mut needed,
0,
)
};
if status < 0 {
return Err(Error::Hardware {
operation: "NCryptSignHash",
code: status,
});
}
signature.truncate(needed as usize);
signature_from_raw(&signature)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WindowsProvider {
Tpm,
Software,
}
impl WindowsProvider {
const fn provider_name(self) -> *const u16 {
match self {
Self::Tpm => MS_PLATFORM_CRYPTO_PROVIDER,
Self::Software => MS_KEY_STORAGE_PROVIDER,
}
}
const fn backend(self) -> Backend {
match self {
Self::Tpm => Backend::Tpm,
Self::Software => Backend::Software,
}
}
const fn describe(self) -> &'static str {
match self {
Self::Tpm => "platform crypto provider (TPM)",
Self::Software => "software key storage provider",
}
}
}
pub struct WindowsKeyStore {
provider: CngHandle,
backend: Backend,
}
impl fmt::Debug for WindowsKeyStore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WindowsKeyStore")
.field("backend", &self.backend)
.finish_non_exhaustive()
}
}
impl WindowsKeyStore {
pub fn open(preferred: WindowsProvider) -> Result<Self> {
let provider = open_provider(preferred.provider_name()).map_err(|error| {
Error::HardwareUnavailable {
reason: format!("cannot open {} ({error})", preferred.describe()),
}
})?;
Ok(Self {
provider,
backend: preferred.backend(),
})
}
pub fn open_best_available() -> Result<Self> {
match Self::open(WindowsProvider::Tpm) {
Ok(store) => Ok(store),
Err(tpm_error) => Self::open(WindowsProvider::Software).map_err(|software_error| {
Error::HardwareUnavailable {
reason: format!(
"neither the TPM nor the software key store is available (TPM: {tpm_error}; software: {software_error})"
),
}
}),
}
}
}
impl DeviceKeyStore for WindowsKeyStore {
fn backend(&self) -> Backend {
self.backend
}
fn open(&self, label: &str) -> Result<Box<dyn DeviceSigner>> {
let name = key_name(label)?;
let key = open_key(&self.provider, &name)?.ok_or_else(|| Error::KeyNotFound {
label: label.to_string(),
})?;
Ok(Box::new(WindowsSigner::new(key, label, self.backend)?))
}
fn create(&self, label: &str) -> Result<Box<dyn DeviceSigner>> {
let name = key_name(label)?;
if open_key(&self.provider, &name)?.is_some() {
return Err(Error::KeyAlreadyExists {
label: label.to_string(),
});
}
let key = match create_key(&self.provider, &name)? {
Some(key) => key,
None => {
return Err(Error::KeyAlreadyExists {
label: label.to_string(),
});
}
};
Ok(Box::new(WindowsSigner::new(key, label, self.backend)?))
}
fn delete(&self, label: &str) -> Result<()> {
let name = key_name(label)?;
let Some(key) = open_key(&self.provider, &name)? else {
return Err(Error::KeyNotFound {
label: label.to_string(),
});
};
let status = unsafe { NCryptDeleteKey(key.raw, 0) };
let mut key = key;
key.raw = 0;
if status < 0 {
return Err(Error::Hardware {
operation: "NCryptDeleteKey",
code: status,
});
}
Ok(())
}
}
pub struct WindowsSigner {
key: CngHandle,
label: String,
backend: Backend,
public_key: DevicePublicKey,
}
impl fmt::Debug for WindowsSigner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WindowsSigner")
.field("label", &self.label)
.field("backend", &self.backend)
.field("device_id", &self.public_key.device_id())
.finish_non_exhaustive()
}
}
impl WindowsSigner {
fn new(key: CngHandle, label: &str, backend: Backend) -> Result<Self> {
let public_key = export_public_key(&key)?;
Ok(Self {
key,
label: label.to_string(),
backend,
public_key,
})
}
}
impl DeviceSigner for WindowsSigner {
fn backend(&self) -> Backend {
self.backend
}
fn public_key(&self) -> &DevicePublicKey {
&self.public_key
}
fn sign_digest(&self, digest: &[u8]) -> Result<DeviceSignature> {
sign_digest_raw(&self.key, digest)
}
}
const MASTER_KEY_NAME_PREFIX: &str = "Qualia2FA-MasterKey-";
const MASTER_KEK_NAME_PREFIX: &str = "Qualia2FA-MasterKek-";
const MASTER_PROBE_KEY_NAME: &str = "Qualia2FA-MasterProbe";
static PROBE_SEQUENCE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
const MASTER_KEK_BITS: u32 = 2048;
const WRAPPED_MASTER_KEY_LEN: usize = (MASTER_KEK_BITS / 8) as usize;
const MATERIAL_EXPORT_POLICY: u32 = NCRYPT_ALLOW_EXPORT_FLAG | NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG;
const KEK_EXPORT_POLICY: u32 = 0;
const ECC_PRIVATE_BLOB_LEN: usize = 8 + 3 * 32;
const ECC_PRIVATE_SCALAR_OFFSET: usize = 8 + 2 * 32;
fn master_key_name(label: &str) -> Result<Vec<u16>> {
key_name_with_prefix(MASTER_KEY_NAME_PREFIX, label)
}
fn master_kek_name(label: &str) -> Result<Vec<u16>> {
key_name_with_prefix(MASTER_KEK_NAME_PREFIX, label)
}
fn probe_key_name() -> Vec<u16> {
let sequence = PROBE_SEQUENCE.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let mut name: Vec<u16> = format!("{MASTER_PROBE_KEY_NAME}-{}-{sequence}", std::process::id())
.encode_utf16()
.collect();
name.push(0);
name
}
fn material_to_private_blob(material: &[u8; MASTER_KEY_LEN]) -> Result<Vec<u8>> {
let signing = p256::ecdsa::SigningKey::from_slice(material).map_err(|_| {
Error::InvalidMasterKey {
reason: "the material is not a valid P-256 scalar, so the software provider cannot hold it as a key",
}
})?;
let point = signing.verifying_key().to_sec1_point(false);
let sec1 = point.as_bytes();
if sec1.len() != 65 {
return Err(Error::InvalidMasterKey {
reason: "the P-256 public point is not 65 bytes long",
});
}
let mut blob = Vec::with_capacity(ECC_PRIVATE_BLOB_LEN);
blob.extend_from_slice(&BCRYPT_ECDSA_PRIVATE_P256_MAGIC.to_le_bytes());
blob.extend_from_slice(&32_u32.to_le_bytes());
blob.extend_from_slice(&sec1[1..33]);
blob.extend_from_slice(&sec1[33..65]);
blob.extend_from_slice(material);
Ok(blob)
}
fn private_blob_to_material(blob: &[u8]) -> Result<MasterKey> {
if blob.len() != ECC_PRIVATE_BLOB_LEN {
return Err(Error::InvalidMasterKey {
reason: "the private key blob returned by the platform is not 104 bytes long",
});
}
let magic = u32::from_le_bytes([blob[0], blob[1], blob[2], blob[3]]);
if magic != BCRYPT_ECDSA_PRIVATE_P256_MAGIC {
return Err(Error::InvalidMasterKey {
reason: "the private key blob returned by the platform is not a P-256 private blob",
});
}
let cb_key = u32::from_le_bytes([blob[4], blob[5], blob[6], blob[7]]);
if cb_key != 32 {
return Err(Error::InvalidMasterKey {
reason: "the private key blob returned by the platform is not a 32-byte coordinate (not P-256)",
});
}
let mut material = [0_u8; MASTER_KEY_LEN];
material.copy_from_slice(&blob[ECC_PRIVATE_SCALAR_OFFSET..]);
Ok(MasterKey::from_bytes(material))
}
fn set_key_dword(key: &CngHandle, property: *const u16, value: u32) -> Result<()> {
let status = unsafe {
NCryptSetProperty(
key.raw,
property,
(&raw const value).cast::<u8>(),
4,
NCRYPT_SILENT_FLAG,
)
};
if status < 0 {
return Err(Error::Hardware {
operation: "NCryptSetProperty",
code: status,
});
}
Ok(())
}
fn export_blob(key: &CngHandle, blob_type: *const u16, operation: &'static str) -> Result<Vec<u8>> {
let mut needed: u32 = 0;
let status = unsafe {
NCryptExportKey(
key.raw,
0,
blob_type,
std::ptr::null(),
std::ptr::null_mut(),
0,
&raw mut needed,
0,
)
};
if status < 0 {
return Err(Error::Hardware {
operation,
code: status,
});
}
if needed > 4096 {
return Err(Error::InvalidMasterKey {
reason: "the platform reported an implausible key blob length (over 4096 bytes)",
});
}
let mut blob = vec![0_u8; needed as usize];
let status = unsafe {
NCryptExportKey(
key.raw,
0,
blob_type,
std::ptr::null(),
blob.as_mut_ptr(),
needed,
&raw mut needed,
0,
)
};
if status < 0 {
return Err(Error::Hardware {
operation,
code: status,
});
}
blob.truncate(needed as usize);
Ok(blob)
}
fn import_named_key(
provider: &CngHandle,
name: &[u16],
blob_type: *const u16,
blob: &[u8],
flags: u32,
) -> Result<CngHandle> {
let mut name_buffer = BCryptBuffer {
cbBuffer: (name.len() * 2) as u32,
BufferType: NCRYPTBUFFER_PKCS_KEY_NAME,
pvBuffer: name.as_ptr().cast_mut().cast(),
};
let parameters = BCryptBufferDesc {
ulVersion: NCRYPTBUFFER_VERSION,
cBuffers: 1,
pBuffers: &raw mut name_buffer,
};
let mut key: NCRYPT_KEY_HANDLE = 0;
let status = unsafe {
NCryptImportKey(
provider.raw,
0,
blob_type,
¶meters,
&raw mut key,
blob.as_ptr(),
blob.len() as u32,
flags,
)
};
if status < 0 {
return Err(Error::Hardware {
operation: "NCryptImportKey",
code: status,
});
}
Ok(CngHandle::new(key))
}
fn delete_key_by_name(provider: &CngHandle, name: &[u16]) -> Result<()> {
let mut handle: NCRYPT_KEY_HANDLE = 0;
let status = unsafe {
NCryptOpenKey(
provider.raw,
&raw mut handle,
name.as_ptr(),
0,
NCRYPT_SILENT_FLAG,
)
};
if means_no_such_key(status) {
return Err(Error::KeyNotFound {
label: String::from_utf16_lossy(&name[..name.len().saturating_sub(1)]),
});
}
if status < 0 {
return Err(Error::Hardware {
operation: "NCryptOpenKey",
code: status,
});
}
let mut key = CngHandle::new(handle);
let status = unsafe { NCryptDeleteKey(key.raw, 0) };
key.raw = 0;
if status < 0 {
return Err(Error::Hardware {
operation: "NCryptDeleteKey",
code: status,
});
}
Ok(())
}
fn remove_unfinished_key(provider: &CngHandle, name: &[u16]) {
let Ok(Some(mut key)) = open_key(provider, name) else {
return;
};
let _ = unsafe { NCryptFinalizeKey(key.raw, NCRYPT_SILENT_FLAG) };
let _ = unsafe { NCryptDeleteKey(key.raw, 0) };
key.raw = 0;
}
fn store_material_key(
provider: &CngHandle,
name: &[u16],
material: &[u8; MASTER_KEY_LEN],
) -> Result<()> {
let blob = material_to_private_blob(material)?;
let result = (|| -> Result<()> {
let key = import_named_key(
provider,
name,
BCRYPT_ECCPRIVATE_BLOB,
&blob,
NCRYPT_SILENT_FLAG | NCRYPT_DO_NOT_FINALIZE_FLAG,
)?;
set_key_dword(&key, NCRYPT_EXPORT_POLICY_PROPERTY, MATERIAL_EXPORT_POLICY)?;
let status = unsafe { NCryptFinalizeKey(key.raw, NCRYPT_SILENT_FLAG) };
if status < 0 {
return Err(Error::Hardware {
operation: "NCryptFinalizeKey",
code: status,
});
}
Ok(())
})();
if result.is_err() {
remove_unfinished_key(provider, name);
}
result
}
fn load_material(provider: &CngHandle, name: &[u16]) -> Result<MasterKey> {
let key = open_key(provider, name)?.ok_or_else(|| Error::KeyNotFound {
label: String::from_utf16_lossy(&name[..name.len().saturating_sub(1)]),
})?;
let blob = export_blob(&key, BCRYPT_ECCPRIVATE_BLOB, "NCryptExportKey")?;
private_blob_to_material(&blob)
}
fn provider_can_hold_material(provider: &CngHandle) -> Result<()> {
let name = probe_key_name();
let probe = [0x5A_u8; MASTER_KEY_LEN];
let result = (|| -> Result<()> {
store_material_key(provider, &name, &probe)?;
let read_back = load_material(provider, &name)?;
if !secrets_equal(read_back.as_bytes(), &probe) {
return Err(Error::InvalidMasterKey {
reason: "the provider returned material that differs from what was stored in it",
});
}
Ok(())
})();
let _ = delete_key_by_name(provider, &name);
result
}
fn open_or_create_kek(provider: &CngHandle, name: &[u16]) -> Result<CngHandle> {
if let Some(key) = open_key(provider, name)? {
return Ok(key);
}
let mut handle: NCRYPT_KEY_HANDLE = 0;
let status = unsafe {
NCryptCreatePersistedKey(
provider.raw,
&raw mut handle,
NCRYPT_RSA_ALGORITHM,
name.as_ptr(),
0,
0,
)
};
if status == NTE_EXISTS {
return open_key(provider, name)?.ok_or(Error::KeyNotFound {
label: String::from_utf16_lossy(&name[..name.len().saturating_sub(1)]),
});
}
if status < 0 {
return Err(Error::Hardware {
operation: "NCryptCreatePersistedKey",
code: status,
});
}
let key = CngHandle::new(handle);
let result = (|| -> Result<()> {
set_key_dword(&key, NCRYPT_LENGTH_PROPERTY, MASTER_KEK_BITS)?;
set_key_dword(&key, NCRYPT_EXPORT_POLICY_PROPERTY, KEK_EXPORT_POLICY)?;
let status = unsafe { NCryptFinalizeKey(key.raw, NCRYPT_SILENT_FLAG) };
if status < 0 {
return Err(Error::Hardware {
operation: "NCryptFinalizeKey",
code: status,
});
}
Ok(())
})();
if let Err(error) = result {
remove_unfinished_key(provider, name);
return Err(error);
}
Ok(key)
}
fn oaep_padding() -> BCRYPT_OAEP_PADDING_INFO {
BCRYPT_OAEP_PADDING_INFO {
pszAlgId: BCRYPT_SHA256_ALGORITHM,
pbLabel: std::ptr::null_mut(),
cbLabel: 0,
}
}
fn wrap_with_kek(kek: &CngHandle, material: &[u8; MASTER_KEY_LEN]) -> Result<WrappedMasterKey> {
let padding = oaep_padding();
let mut needed: u32 = 0;
let status = unsafe {
NCryptEncrypt(
kek.raw,
material.as_ptr(),
material.len() as u32,
(&raw const padding).cast(),
std::ptr::null_mut(),
0,
&raw mut needed,
NCRYPT_PAD_OAEP_FLAG,
)
};
if status < 0 {
return Err(Error::Hardware {
operation: "NCryptEncrypt(query length)",
code: status,
});
}
if needed as usize != WRAPPED_MASTER_KEY_LEN {
return Err(Error::Hardware {
operation: "NCryptEncrypt reported an unexpected ciphertext length",
code: needed as i32,
});
}
let mut wrapped = vec![0_u8; WRAPPED_MASTER_KEY_LEN];
let status = unsafe {
NCryptEncrypt(
kek.raw,
material.as_ptr(),
material.len() as u32,
(&raw const padding).cast(),
wrapped.as_mut_ptr(),
needed,
&raw mut needed,
NCRYPT_PAD_OAEP_FLAG,
)
};
if status < 0 {
return Err(Error::Hardware {
operation: "NCryptEncrypt",
code: status,
});
}
wrapped.truncate(needed as usize);
Ok(WrappedMasterKey::from_bytes(wrapped))
}
fn unwrap_with_kek(kek: &CngHandle, wrapped: &[u8]) -> Result<MasterKey> {
let padding = oaep_padding();
let mut plain = vec![0_u8; MASTER_KEY_LEN];
let mut needed: u32 = MASTER_KEY_LEN as u32;
let status = unsafe {
NCryptDecrypt(
kek.raw,
wrapped.as_ptr(),
wrapped.len() as u32,
(&raw const padding).cast(),
plain.as_mut_ptr(),
needed,
&raw mut needed,
NCRYPT_PAD_OAEP_FLAG,
)
};
if status < 0 {
return Err(Error::InvalidWrappedKey {
reason: "the key-encryption key rejected the bytes (damaged, or wrapped by a different key)",
code: status,
});
}
if needed as usize != MASTER_KEY_LEN {
return Err(Error::InvalidWrappedKey {
reason: "the wrapped bytes do not decrypt to a 32-byte master key",
code: 0,
});
}
let mut material = [0_u8; MASTER_KEY_LEN];
material.copy_from_slice(&plain);
Ok(MasterKey::from_bytes(material))
}
pub struct WindowsDirectMasterKeyStore {
provider: CngHandle,
backend: Backend,
}
impl fmt::Debug for WindowsDirectMasterKeyStore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WindowsDirectMasterKeyStore")
.field("backend", &self.backend)
.finish_non_exhaustive()
}
}
impl WindowsDirectMasterKeyStore {
pub fn open(preferred: WindowsProvider) -> Result<Self> {
let provider = open_provider(preferred.provider_name()).map_err(|error| {
Error::HardwareUnavailable {
reason: format!("cannot open {} ({error})", preferred.describe()),
}
})?;
provider_can_hold_material(&provider).map_err(|error| Error::HardwareUnavailable {
reason: format!(
"{} cannot hold master key material: {error}",
preferred.describe()
),
})?;
Ok(Self {
provider,
backend: preferred.backend(),
})
}
pub fn open_best_available() -> Result<Self> {
match Self::open(WindowsProvider::Tpm) {
Ok(store) => Ok(store),
Err(tpm_error) => Self::open(WindowsProvider::Software).map_err(|software_error| {
Error::HardwareUnavailable {
reason: format!(
"neither the TPM nor the software key store can hold master key material (TPM: {tpm_error}; software: {software_error})"
),
}
}),
}
}
}
impl DirectMasterKeyStore for WindowsDirectMasterKeyStore {
fn backend(&self) -> Backend {
self.backend
}
fn store(&self, label: &str, key: &MasterKey) -> Result<()> {
let name = master_key_name(label)?;
match delete_key_by_name(&self.provider, &name) {
Ok(()) | Err(Error::KeyNotFound { .. }) => {}
Err(other) => return Err(other),
}
store_material_key(&self.provider, &name, key.as_bytes())?;
let read_back = load_material(&self.provider, &name)?;
if !secrets_equal(read_back.as_bytes(), key.as_bytes()) {
let _ = delete_key_by_name(&self.provider, &name);
return Err(Error::InvalidMasterKey {
reason: "the provider returned material that differs from what was stored in it",
});
}
Ok(())
}
fn load(&self, label: &str) -> Result<MasterKey> {
let name = master_key_name(label)?;
load_material(&self.provider, &name)
}
fn delete(&self, label: &str) -> Result<()> {
let name = master_key_name(label)?;
delete_key_by_name(&self.provider, &name)
}
}
pub struct WindowsWrappedMasterKeyStore {
provider: CngHandle,
backend: Backend,
}
impl fmt::Debug for WindowsWrappedMasterKeyStore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WindowsWrappedMasterKeyStore")
.field("backend", &self.backend)
.finish_non_exhaustive()
}
}
impl WindowsWrappedMasterKeyStore {
pub fn open(preferred: WindowsProvider) -> Result<Self> {
let provider = open_provider(preferred.provider_name()).map_err(|error| {
Error::HardwareUnavailable {
reason: format!("cannot open {} ({error})", preferred.describe()),
}
})?;
Ok(Self {
provider,
backend: preferred.backend(),
})
}
pub fn open_best_available() -> Result<Self> {
match Self::open(WindowsProvider::Tpm) {
Ok(store) => Ok(store),
Err(tpm_error) => Self::open(WindowsProvider::Software).map_err(|software_error| {
Error::HardwareUnavailable {
reason: format!(
"neither the TPM nor the software key store is available (TPM: {tpm_error}; software: {software_error})"
),
}
}),
}
}
}
impl WrappedMasterKeyStore for WindowsWrappedMasterKeyStore {
fn backend(&self) -> Backend {
self.backend
}
fn store(&self, label: &str, key: &MasterKey) -> Result<WrappedMasterKey> {
let name = master_kek_name(label)?;
let kek = open_or_create_kek(&self.provider, &name)?;
wrap_with_kek(&kek, key.as_bytes())
}
fn load(&self, label: &str, wrapped: &WrappedMasterKey) -> Result<MasterKey> {
let name = master_kek_name(label)?;
if wrapped.as_bytes().len() != WRAPPED_MASTER_KEY_LEN {
return Err(Error::InvalidWrappedKey {
reason: "the wrapped master key is not the 256 bytes a 2048-bit key-encryption key produces",
code: 0,
});
}
let kek = open_key(&self.provider, &name)?.ok_or_else(|| Error::KeyNotFound {
label: label.to_string(),
})?;
unwrap_with_kek(&kek, wrapped.as_bytes())
}
fn delete(&self, label: &str) -> Result<()> {
let name = master_kek_name(label)?;
delete_key_by_name(&self.provider, &name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nte_codes_match_winerror() {
assert_eq!(NTE_BAD_FLAGS, 0x8009_0009_u32 as i32, "NTE_BAD_FLAGS");
assert_eq!(NTE_NO_KEY, 0x8009_000D_u32 as i32, "NTE_NO_KEY");
assert_eq!(NTE_EXISTS, 0x8009_000F_u32 as i32, "NTE_EXISTS");
assert_eq!(NTE_NOT_FOUND, 0x8009_0011_u32 as i32, "NTE_NOT_FOUND");
assert_eq!(NTE_BAD_KEYSET, 0x8009_0016_u32 as i32, "NTE_BAD_KEYSET");
}
#[test]
fn no_such_key_codes_are_recognized() {
assert!(means_no_such_key(0x8009_0016_u32 as i32), "NTE_BAD_KEYSET");
assert!(means_no_such_key(0x8009_000D_u32 as i32), "NTE_NO_KEY");
assert!(means_no_such_key(0x8009_0011_u32 as i32), "NTE_NOT_FOUND");
}
#[test]
fn real_failures_are_not_mistaken_for_missing_keys() {
for status in [
0x8009_0009_u32 as i32,
0x8009_0010_u32 as i32,
0x8009_0026_u32 as i32,
0x8009_0029_u32 as i32,
0x8007_0005_u32 as i32,
0,
] {
assert!(
!means_no_such_key(status),
"0x{:08X} was mistaken for a missing key",
status as u32
);
}
}
#[test]
fn key_name_has_prefix_and_nul_terminator() {
let wide = key_name("device").expect("valid label");
assert_eq!(wide.last(), Some(&0), "must end with NUL");
let text = String::from_utf16(&wide[..wide.len() - 1]).expect("valid UTF-16");
assert_eq!(text, "Qualia2FA-Device-device");
}
#[test]
fn label_with_nul_is_rejected() {
assert!(key_name("a\0b").is_err());
assert!(key_name("a\u{0}").is_err());
}
#[test]
fn label_with_special_characters_is_rejected() {
for bad in [
"a/b", "a\\b", "a b", "a:b", "a*b", "a?b", "a\"b", "a<b", "a|b", "设备", "a\nb", "a\tb",
] {
assert!(key_name(bad).is_err(), "{bad:?} must not be accepted");
}
}
#[test]
fn label_with_allowed_characters_is_accepted() {
for good in ["device", "Device-1", "a_b.c", "ABC123", "x"] {
assert!(key_name(good).is_ok(), "{good:?} must be accepted");
}
}
#[test]
fn empty_and_oversized_labels_are_rejected() {
assert!(key_name("").is_err());
assert!(key_name(&"a".repeat(MAX_LABEL_LEN)).is_ok());
assert!(key_name(&"a".repeat(MAX_LABEL_LEN + 1)).is_err());
}
#[test]
fn provider_maps_to_backend() {
assert_eq!(WindowsProvider::Tpm.backend(), Backend::Tpm);
assert_eq!(WindowsProvider::Software.backend(), Backend::Software);
assert!(WindowsProvider::Tpm.backend().is_hardware_backed());
assert!(!WindowsProvider::Software.backend().is_hardware_backed());
}
#[test]
fn key_prefixes_are_distinct() {
let identity = key_name("shared").expect("valid label");
let material = master_key_name("shared").expect("valid label");
let kek = master_kek_name("shared").expect("valid label");
assert_ne!(identity, material);
assert_ne!(identity, kek);
assert_ne!(material, kek);
let text =
|wide: &[u16]| String::from_utf16(&wide[..wide.len() - 1]).expect("valid UTF-16");
assert_eq!(text(&material), "Qualia2FA-MasterKey-shared");
assert_eq!(text(&kek), "Qualia2FA-MasterKek-shared");
}
#[test]
fn master_key_labels_are_validated() {
for bad in ["", "a/b", "a\0b", "has space", "设备"] {
assert!(master_key_name(bad).is_err(), "{bad:?} must be rejected");
assert!(master_kek_name(bad).is_err(), "{bad:?} must be rejected");
}
assert!(master_key_name("account-1").is_ok());
assert!(master_kek_name("account-1").is_ok());
}
#[test]
fn material_survives_the_private_blob() {
let material = [
0x5A_u8, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD,
0xEE, 0xFF, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C,
0x0D, 0x0E, 0x0F, 0x10,
];
let blob = material_to_private_blob(&material).expect("the material is a valid scalar");
assert_eq!(blob.len(), ECC_PRIVATE_BLOB_LEN);
let magic = u32::from_le_bytes([blob[0], blob[1], blob[2], blob[3]]);
assert_eq!(magic, BCRYPT_ECDSA_PRIVATE_P256_MAGIC);
assert_eq!(&blob[ECC_PRIVATE_SCALAR_OFFSET..], &material[..]);
let back = private_blob_to_material(&blob).expect("the blob is a P-256 private blob");
assert!(secrets_equal(back.as_bytes(), &material));
}
#[test]
fn invalid_scalar_is_rejected() {
let error = material_to_private_blob(&[0xFF_u8; MASTER_KEY_LEN])
.expect_err("all-ones is above the group order");
assert!(matches!(error, Error::InvalidMasterKey { .. }));
assert!(
material_to_private_blob(&[0x00_u8; MASTER_KEY_LEN]).is_err(),
"zero is not a valid scalar either"
);
}
#[test]
fn foreign_private_blobs_are_rejected() {
let material = [0x11_u8; MASTER_KEY_LEN];
let good = material_to_private_blob(&material).expect("valid scalar");
let mut short = good.clone();
short.truncate(ECC_PRIVATE_BLOB_LEN - 1);
let mut wrong_magic = good.clone();
wrong_magic[0] ^= 0x01;
let mut wrong_key_size = good.clone();
wrong_key_size[4] = 64;
for blob in [short, wrong_magic, wrong_key_size, Vec::new()] {
let error = private_blob_to_material(&blob).expect_err("must be rejected");
assert!(matches!(error, Error::InvalidMasterKey { .. }));
}
}
#[test]
fn wrapped_length_matches_the_kek_size() {
assert_eq!(WRAPPED_MASTER_KEY_LEN, 256);
assert_eq!(MASTER_KEK_BITS, 2048);
}
#[test]
fn export_policies_match_the_forms() {
assert_ne!(MATERIAL_EXPORT_POLICY & NCRYPT_ALLOW_EXPORT_FLAG, 0);
assert_ne!(
MATERIAL_EXPORT_POLICY & NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG,
0
);
assert_eq!(KEK_EXPORT_POLICY, 0);
}
#[test]
fn a_provider_that_refuses_the_material_keeps_no_key() {
let mut name: Vec<u16> = "Qualia2FA-MasterProbe-UnitTest".encode_utf16().collect();
name.push(0);
let material = [0x3C_u8; MASTER_KEY_LEN];
for preferred in [WindowsProvider::Tpm, WindowsProvider::Software] {
let Ok(provider) = open_provider(preferred.provider_name()) else {
println!("skipping {preferred:?}: the provider is not available");
continue;
};
remove_unfinished_key(&provider, &name);
match store_material_key(&provider, &name, &material) {
Ok(()) => {
let read_back = load_material(&provider, &name).expect("read back");
assert!(
secrets_equal(read_back.as_bytes(), &material),
"{preferred:?} returned material that differs from what was stored"
);
delete_key_by_name(&provider, &name).expect("clean up");
}
Err(error) => {
println!("{preferred:?} refused the material: {error}");
assert!(
open_key(&provider, &name)
.expect("the probe must not fail to look")
.is_none(),
"{preferred:?} left a key behind under the probe name"
);
}
}
}
}
}