1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-12 06:23:38 +00:00
draft
This commit is contained in:
Anders Åberg
2025-06-23 11:18:35 +02:00
parent 746bdfab00
commit b2017ed9ee
32 changed files with 5835 additions and 781 deletions

Binary file not shown.

View File

@@ -13,10 +13,10 @@ xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
IgnorableNamespaces="uap rescap com uap10 build"
xmlns:build="http://schemas.microsoft.com/developer/appx/2015/build">
<!-- use single quotes to avoid double quotes escaping in the publisher value -->
<Identity Name="8bitSolutionsLLC.bitwardendesktop"
ProcessorArchitecture="x64"
Publisher='CN=com.bitwarden.localdevelopment'
Version="2025.6.0.0" />
<Identity Name="${applicationId}"
ProcessorArchitecture="${arch}"
Publisher='${publisher}'
Version="${version}" />
<Properties>
<DisplayName>Bitwarden</DisplayName>
<PublisherDisplayName>Bitwarden Inc</PublisherDisplayName>

View File

@@ -581,6 +581,33 @@ dependencies = [
"cpufeatures",
]
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "cipher"
version = "0.4.4"
@@ -723,6 +750,12 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
[[package]]
name = "crypto-common"
version = "0.1.6"
@@ -1401,6 +1434,16 @@ dependencies = [
"tracing",
]
[[package]]
name = "half"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
dependencies = [
"cfg-if",
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
@@ -4547,9 +4590,13 @@ checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
name = "windows_plugin_authenticator"
version = "0.0.0"
dependencies = [
"ciborium",
"hex",
"reqwest",
"serde",
"serde_json",
"sha2",
"tokio",
"windows 0.61.1 (git+https://github.com/microsoft/windows-rs.git?rev=d09b4681de02560cf05bd3e57d7ea56b73f3b2f8)",
"windows-core 0.61.2 (git+https://github.com/microsoft/windows-rs.git?rev=d09b4681de02560cf05bd3e57d7ea56b73f3b2f8)",
]

View File

@@ -3,6 +3,10 @@
/* auto-generated by NAPI-RS */
export interface PasskeyRequestEvent {
requestType: string
requestJson: string
}
export declare namespace passwords {
/** Fetch the stored password from the keychain. */
export function getPassword(service: string, account: string): Promise<string>
@@ -186,7 +190,51 @@ export declare namespace crypto {
export function argon2(secret: Buffer, salt: Buffer, iterations: number, memory: number, parallelism: number): Promise<Buffer>
}
export declare namespace passkey_authenticator {
export interface SyncedCredential {
credentialId: string
rpId: string
userName: string
userId: string
}
export interface PasskeyAssertionRequest {
rpId: string
transactionId: string
clientDataHash: Array<number>
allowedCredentials: Array<Array<number>>
userVerification: boolean
}
export interface PasskeyRegistrationRequest {
rpId: string
transactionId: string
userId: Array<number>
userName: string
clientDataHash: Array<number>
userVerification: boolean
supportedAlgorithms: Array<number>
}
export interface PasskeySyncRequest {
rpId: string
}
export interface PasskeyAssertionResponse {
credentialId: Array<number>
authenticatorData: Array<number>
signature: Array<number>
userHandle: Array<number>
}
export interface PasskeyRegistrationResponse {
credentialId: Array<number>
attestationObject: Array<number>
}
export interface PasskeySyncResponse {
credentials: Array<SyncedCredential>
}
export interface PasskeyErrorResponse {
message: string
}
export function register(): void
export function onRequest(callback: (error: null | Error, event: PasskeyRequestEvent) => Promise<string>): Promise<string>
export function syncCredentialsToWindows(credentials: Array<SyncedCredential>): void
export function getCredentialsFromWindows(): Array<SyncedCredential>
}
export declare namespace logging {
export const enum LogLevel {

View File

@@ -810,12 +810,213 @@ pub mod crypto {
#[napi]
pub mod passkey_authenticator {
use napi::threadsafe_function::{
ErrorStrategy::CalleeHandled, ThreadsafeFunction,
};
use serde_json;
// Re-export the platform-specific types
pub use crate::passkey_authenticator_internal::PasskeyRequestEvent;
#[napi(object)]
#[derive(serde::Serialize, serde::Deserialize)]
pub struct SyncedCredential {
pub credential_id: String, // base64url encoded
pub rp_id: String,
pub user_name: String,
pub user_id: String, // base64url encoded
}
impl From<windows_plugin_authenticator::SyncedCredential> for SyncedCredential {
fn from(cred: windows_plugin_authenticator::SyncedCredential) -> Self {
use base64::Engine;
Self {
credential_id: base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&cred.credential_id),
rp_id: cred.rp_id,
user_name: cred.user_name,
user_id: base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&cred.user_id),
}
}
}
impl From<SyncedCredential> for windows_plugin_authenticator::SyncedCredential {
fn from(cred: SyncedCredential) -> Self {
use base64::Engine;
Self {
credential_id: base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(&cred.credential_id)
.unwrap_or_default(),
rp_id: cred.rp_id,
user_name: cred.user_name,
user_id: base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(&cred.user_id)
.unwrap_or_default(),
}
}
}
#[napi(object)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyAssertionRequest {
pub rp_id: String,
pub transaction_id: String,
pub client_data_hash: Vec<u8>,
pub allowed_credentials: Vec<Vec<u8>>,
pub user_verification: bool,
}
#[napi(object)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyRegistrationRequest {
pub rp_id: String,
pub transaction_id: String,
pub user_id: Vec<u8>,
pub user_name: String,
pub client_data_hash: Vec<u8>,
pub user_verification: bool,
pub supported_algorithms: Vec<i32>,
}
#[napi(object)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeySyncRequest {
pub rp_id: String,
}
#[napi(object)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyAssertionResponse {
pub credential_id: Vec<u8>,
pub authenticator_data: Vec<u8>,
pub signature: Vec<u8>,
pub user_handle: Vec<u8>,
}
#[napi(object)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyRegistrationResponse {
pub credential_id: Vec<u8>,
pub attestation_object: Vec<u8>,
}
#[napi(object)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeySyncResponse {
pub credentials: Vec<SyncedCredential>,
}
#[napi(object)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyErrorResponse {
pub message: String,
}
#[napi]
pub fn register() -> napi::Result<()> {
crate::passkey_authenticator_internal::register().map_err(|e| {
napi::Error::from_reason(format!("Passkey registration failed - Error: {e} - {e:?}"))
})
}
#[napi]
pub async fn on_request(
#[napi(ts_arg_type = "(error: null | Error, event: PasskeyRequestEvent) => Promise<string>")]
callback: ThreadsafeFunction<PasskeyRequestEvent, CalleeHandled>,
) -> napi::Result<String> {
crate::passkey_authenticator_internal::on_request(callback).await
}
#[napi]
pub fn sync_credentials_to_windows(credentials: Vec<SyncedCredential>) -> napi::Result<()> {
const PLUGIN_CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062";
log::info!("[NAPI] sync_credentials_to_windows called with {} credentials", credentials.len());
// Log each credential being synced (with truncated IDs for security)
for (i, cred) in credentials.iter().enumerate() {
let truncated_cred_id = if cred.credential_id.len() > 16 {
format!("{}...", &cred.credential_id[..16])
} else {
cred.credential_id.clone()
};
let truncated_user_id = if cred.user_id.len() > 16 {
format!("{}...", &cred.user_id[..16])
} else {
cred.user_id.clone()
};
log::info!("[NAPI] Credential {}: RP={}, User={}, CredID={}, UserID={}",
i + 1, cred.rp_id, cred.user_name, truncated_cred_id, truncated_user_id);
}
// Convert NAPI types to internal types using From trait
let internal_credentials: Vec<windows_plugin_authenticator::SyncedCredential> = credentials
.into_iter()
.map(|cred| cred.into())
.collect();
log::info!("[NAPI] Calling Windows Plugin Authenticator sync with CLSID: {}", PLUGIN_CLSID);
let result = windows_plugin_authenticator::sync_credentials_to_windows(internal_credentials, PLUGIN_CLSID);
match &result {
Ok(()) => log::info!("[NAPI] sync_credentials_to_windows completed successfully"),
Err(e) => log::error!("[NAPI] sync_credentials_to_windows failed: {}", e),
}
result.map_err(|e| napi::Error::from_reason(format!("Sync credentials failed: {}", e)))
}
#[napi]
pub fn get_credentials_from_windows() -> napi::Result<Vec<SyncedCredential>> {
const PLUGIN_CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062";
log::info!("[NAPI] get_credentials_from_windows called with CLSID: {}", PLUGIN_CLSID);
let result = windows_plugin_authenticator::get_credentials_from_windows(PLUGIN_CLSID);
let internal_credentials = match &result {
Ok(creds) => {
log::info!("[NAPI] Retrieved {} credentials from Windows", creds.len());
result.map_err(|e| napi::Error::from_reason(format!("Get credentials failed: {}", e)))?
}
Err(e) => {
log::error!("[NAPI] get_credentials_from_windows failed: {}", e);
return Err(napi::Error::from_reason(format!("Get credentials failed: {}", e)));
}
};
// Convert internal types to NAPI types using From trait
let napi_credentials: Vec<SyncedCredential> = internal_credentials
.into_iter()
.enumerate()
.map(|(i, cred)| {
let result_cred: SyncedCredential = cred.into();
let truncated_cred_id = if result_cred.credential_id.len() > 16 {
format!("{}...", &result_cred.credential_id[..16])
} else {
result_cred.credential_id.clone()
};
log::info!("[NAPI] Retrieved credential {}: RP={}, User={}, CredID={}",
i + 1, result_cred.rp_id, result_cred.user_name, truncated_cred_id);
result_cred
})
.collect();
log::info!("[NAPI] get_credentials_from_windows completed successfully, returning {} credentials", napi_credentials.len());
Ok(napi_credentials)
}
}
#[napi]

View File

@@ -1,5 +1,22 @@
use anyhow::{bail, Result};
use napi::threadsafe_function::{
ErrorStrategy::CalleeHandled, ThreadsafeFunction,
};
#[napi(object)]
#[derive(Debug)]
pub struct PasskeyRequestEvent {
pub operation: String,
pub rpid: String,
pub transaction_id: String,
}
pub fn register() -> Result<()> {
bail!("Not implemented")
}
pub async fn on_request(
_callback: ThreadsafeFunction<PasskeyRequestEvent, CalleeHandled>,
) -> napi::Result<String> {
Err(napi::Error::from_reason("Passkey authenticator is not supported on this platform".to_string()))
}

View File

@@ -1,7 +1,82 @@
use anyhow::{anyhow, Result};
use napi::{bindgen_prelude::Promise, threadsafe_function::{
ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode,
}, JsObject};
use tokio::sync::mpsc;
use serde_json;
use windows_plugin_authenticator::util;
// Simple wrapper for passing JSON strings to TypeScript
#[napi(object)]
#[derive(Debug)]
pub struct PasskeyRequestEvent {
pub request_type: String,
pub request_json: String,
}
pub fn register() -> Result<()> {
windows_plugin_authenticator::register().map_err(|e| anyhow!(e))?;
Ok(())
}
pub async fn on_request(
callback: ThreadsafeFunction<PasskeyRequestEvent, CalleeHandled>,
) -> napi::Result<String> {
let (tx, mut rx) = mpsc::unbounded_channel();
// Set the sender in the Windows plugin authenticator
windows_plugin_authenticator::set_request_sender(tx);
// Spawn task to handle incoming events
tokio::spawn(async move {
while let Some(event) = rx.recv().await {
// The request is already serialized as JSON in the event
let request_json = event.request_json;
// Get the request type as a string
let request_type = match event.request_type {
windows_plugin_authenticator::RequestType::Assertion => "assertion".to_string(),
windows_plugin_authenticator::RequestType::Registration => "registration".to_string(),
windows_plugin_authenticator::RequestType::Sync => "sync".to_string(),
};
let napi_event = PasskeyRequestEvent { request_type, request_json };
// Call the callback asynchronously and capture the return value
let promise_result: Result<Promise<String>, napi::Error> = callback.call_async(Ok(napi_event)).await;
// awai promse
match promise_result {
Ok(promise_result) => match promise_result.await {
Ok(result) => {
util::message(&format!("CALLBACK COMPLETED WITH RESPONSE: {}", result));
// Parse the JSON response directly back to Rust enum
let response: windows_plugin_authenticator::PasskeyResponse = match serde_json::from_str(&result) {
Ok(resp) => resp,
Err(e) => windows_plugin_authenticator::PasskeyResponse::Error {
message: format!("JSON parse error: {}", e),
}
};
let _ = event.response_sender.send(response);
}
Err(e) => {
eprintln!("Error calling passkey callback inner: {}", e);
let _ = event.response_sender.send(windows_plugin_authenticator::PasskeyResponse::Error {
message: format!("Inner Callback error: {}", e),
});
}
}
Err(e) => {
eprintln!("Error calling passkey callback: {}", e);
let _ = event.response_sender.send(windows_plugin_authenticator::PasskeyResponse::Error {
message: format!("Callback error: {}", e),
});
}
}
}
});
Ok("Event listener registered successfully".to_string())
}

View File

@@ -11,3 +11,7 @@ windows-core = { workspace = true }
hex = { workspace = true }
reqwest = { version = "0.12", features = ["json", "blocking"] }
serde_json = { workspace = true }
serde = { workspace = true, features = ["derive"] }
ciborium = "0.2"
sha2 = "0.10"
tokio = { workspace = true }

View File

@@ -0,0 +1,977 @@
#include "pch.h"
#include "PluginAuthenticatorImpl.h"
#include <App.xaml.h>
#include <PluginManagement/PluginRegistrationManager.h>
#include <PluginManagement/PluginCredentialManager.h>
#include <include/cbor-lite/codec.h>
#include <string>
#include <iostream>
#include <fstream>
#include <helpers/buffer_read_write.h>
namespace winrt
{
using namespace winrt::Windows::Foundation;
using namespace winrt::Microsoft::UI::Windowing;
using namespace winrt::Microsoft::UI::Xaml;
using namespace winrt::Microsoft::UI::Xaml::Controls;
using namespace winrt::Microsoft::UI::Xaml::Navigation;
using namespace PasskeyManager;
using namespace PasskeyManager::implementation;
using namespace CborLite;
}
namespace winrt::PasskeyManager::implementation
{
static std::vector<uint8_t> GetRequestSigningPubKey()
{
return wil::reg::get_value_binary(HKEY_CURRENT_USER, c_pluginRegistryPath, c_windowsPluginRequestSigningKeyRegKeyName, REG_BINARY);
}
/*
* This function is used to verify the signature of a request buffer.
* The public key is part of response to plugin registration.
*/
static HRESULT VerifySignatureHelper(
std::vector<BYTE>& dataBuffer,
PBYTE pbKeyData,
DWORD cbKeyData,
PBYTE pbSignature,
DWORD cbSignature)
{
// Create key provider
wil::unique_ncrypt_prov hProvider;
wil::unique_ncrypt_key reqSigningKey;
// Get the provider
RETURN_IF_FAILED(NCryptOpenStorageProvider(&hProvider, nullptr, 0));
// Create a NCrypt key handle from the public key
RETURN_IF_FAILED(NCryptImportKey(
hProvider.get(),
NULL,
BCRYPT_ECCPUBLIC_BLOB,
NULL,
&reqSigningKey,
pbKeyData,
cbKeyData, 0));
// Verify the signature over the hash of dataBuffer using the hKey
DWORD objLenSize = 0;
DWORD bytesRead = 0;
RETURN_IF_NTSTATUS_FAILED(BCryptGetProperty(
BCRYPT_SHA256_ALG_HANDLE,
BCRYPT_OBJECT_LENGTH,
reinterpret_cast<PBYTE>(&objLenSize),
sizeof(objLenSize),
&bytesRead, 0));
auto objLen = wil::make_unique_cotaskmem<BYTE[]>(objLenSize);
wil::unique_bcrypt_hash hashHandle;
RETURN_IF_NTSTATUS_FAILED(BCryptCreateHash(
BCRYPT_SHA256_ALG_HANDLE,
wil::out_param(hashHandle),
objLen.get(),
objLenSize,
nullptr, 0, 0));
RETURN_IF_NTSTATUS_FAILED(BCryptHashData(
hashHandle.get(),
dataBuffer.data(),
static_cast<ULONG>(dataBuffer.size()), 0));
DWORD localHashByteCount = 0;
RETURN_IF_NTSTATUS_FAILED(BCryptGetProperty(
BCRYPT_SHA256_ALG_HANDLE,
BCRYPT_HASH_LENGTH,
reinterpret_cast<PBYTE>(&localHashByteCount),
sizeof(localHashByteCount),
&bytesRead, 0));
auto localHashBuffer = wil::make_unique_cotaskmem<BYTE[]>(localHashByteCount);
RETURN_IF_NTSTATUS_FAILED(BCryptFinishHash(hashHandle.get(), localHashBuffer.get(), localHashByteCount, 0));
RETURN_IF_WIN32_ERROR(NCryptVerifySignature(
reqSigningKey.get(),
nullptr,
localHashBuffer.get(),
localHashByteCount,
pbSignature,
cbSignature, 0));
return S_OK;
}
HRESULT CheckHelloConsentCompleted()
{
winrt::com_ptr<App> curApp = winrt::Microsoft::UI::Xaml::Application::Current().as<App>();
HANDLE handles[2] = { curApp->m_hVaultConsentComplete.get(), curApp->m_hVaultConsentFailed.get() };
DWORD cWait = ARRAYSIZE(handles);
DWORD hIndex = 0;
RETURN_IF_FAILED(CoWaitForMultipleHandles(COWAIT_DISPATCH_WINDOW_MESSAGES | COWAIT_DISPATCH_CALLS, INFINITE, cWait, handles, &hIndex));
if (hIndex == 1) // Consent failed
{
RETURN_HR(E_FAIL);
}
return S_OK;
}
HRESULT PerformUv(
winrt::com_ptr<winrt::PasskeyManager::implementation::App>& curApp,
HWND hWnd,
wil::shared_hmodule webauthnDll,
GUID transactionId,
PluginOperationType operationType,
std::vector<BYTE> requestBuffer,
wil::shared_cotaskmem_string rpName,
wil::shared_cotaskmem_string userName)
{
curApp->SetPluginPerformOperationOptions(hWnd, operationType, rpName.get(), userName.get());
// Wait for the app main window to be ready.
DWORD hIndex = 0;
RETURN_IF_FAILED(CoWaitForMultipleHandles(COWAIT_DISPATCH_WINDOW_MESSAGES | COWAIT_DISPATCH_CALLS, INFINITE, 1, curApp->m_hWindowReady.addressof(), &hIndex));
// Trigger a Consent Verifier Dialog to simulate a Windows Hello unlock flow
// This is to demonstrate a vault unlock flow using Windows Hello and is not the recommended way to secure the vault
if (PluginCredentialManager::getInstance().GetVaultLock())
{
curApp->GetDispatcherQueue().TryEnqueue([curApp]()
{
curApp->SimulateUnLockVaultUsingConsentVerifier();
});
RETURN_IF_FAILED(CheckHelloConsentCompleted());
}
else
{
SetEvent(curApp->m_hVaultConsentComplete.get());
}
// Wait for user confirmation to proceed with the operation Create/Signin/Cancel button
// This is a mock up for plugin requiring UI.
{
HANDLE handles[2] = { curApp->m_hPluginProceedButtonEvent.get(), curApp->m_hPluginUserCancelEvent.get() };
DWORD cWait = ARRAYSIZE(handles);
RETURN_IF_FAILED(CoWaitForMultipleHandles(COWAIT_DISPATCH_WINDOW_MESSAGES | COWAIT_DISPATCH_CALLS, INFINITE, cWait, handles, &hIndex));
if (hIndex == 1) // Cancel button clicked
{
// User cancelled the operation. NTE_USER_CANCELLED allows Windows to distinguish between user cancellation and other errors.
return NTE_USER_CANCELLED;
}
}
// Skip user verification if the user has already performed a gesture to unlock the vault to avoid double prompting
if (PluginCredentialManager::getInstance().GetVaultLock())
{
return S_OK;
}
EXPERIMENTAL_WEBAUTHN_PLUGIN_PERFORM_UV pluginPerformUv{};
pluginPerformUv.transactionId = &transactionId;
if (curApp->m_silentMode)
{
// If the app did not display any UI, use the hwnd of the caller here. This was included in the request to the plugin. Refer: EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST
pluginPerformUv.hwnd = hWnd;
}
else
{
// If the app displayed UI, use the hwnd of the app window here
pluginPerformUv.hwnd = curApp->GetNativeWindowHandle();
}
EXPERIMENTAL_PWEBAUTHN_PLUGIN_PERFORM_UV_RESPONSE pPluginPerformUvResponse = nullptr;
auto webAuthNPluginPerformUv = GetProcAddressByFunctionDeclaration(webauthnDll.get(), EXPERIMENTAL_WebAuthNPluginPerformUv);
RETURN_HR_IF_NULL(E_NOTIMPL, webAuthNPluginPerformUv);
// Step 1: Get the UV count
pluginPerformUv.type = EXPERIMENTAL_WEBAUTHN_PLUGIN_PERFORM_UV_OPERATION_TYPE::GetUvCount;
RETURN_IF_FAILED(webAuthNPluginPerformUv(&pluginPerformUv, &pPluginPerformUvResponse));
/*
* pPluginPerformUvResponse->pbResponse contains the UV count
* The UV count tracks the number of times the user has performed a gesture to unlock the vault
*/
// Step 2: Get the public key
pluginPerformUv.type = EXPERIMENTAL_WEBAUTHN_PLUGIN_PERFORM_UV_OPERATION_TYPE::GetPubKey;
RETURN_IF_FAILED(webAuthNPluginPerformUv(&pluginPerformUv, &pPluginPerformUvResponse));
// stash public key in a new buffer for later use
DWORD cbPubData = pPluginPerformUvResponse->cbResponse;
wil::unique_hlocal_ptr<BYTE[]> ppbPubKeyData = wil::make_unique_hlocal<BYTE[]>(cbPubData);
memcpy_s(ppbPubKeyData.get(), cbPubData, pPluginPerformUvResponse->pbResponse, pPluginPerformUvResponse->cbResponse);
// Step 3: Perform UV. This step uses a Windows Hello prompt to authenticate the user
pluginPerformUv.type = EXPERIMENTAL_WEBAUTHN_PLUGIN_PERFORM_UV_OPERATION_TYPE::PerformUv;
pluginPerformUv.pwszUsername = wil::make_cotaskmem_string(userName.get()).release();
// pwszContext can be used to provide additional context to the user. This is displayed alongside the username in the Windows Hello passkey user verification dialog.
pluginPerformUv.pwszContext = wil::make_cotaskmem_string(L"Context String").release();
RETURN_IF_FAILED(webAuthNPluginPerformUv(&pluginPerformUv, &pPluginPerformUvResponse));
// Verify the signature over the hash of requestBuffer using the hKey
auto signatureVerifyResult = VerifySignatureHelper(
requestBuffer,
ppbPubKeyData.get(),
cbPubData,
pPluginPerformUvResponse->pbResponse,
pPluginPerformUvResponse->cbResponse);
curApp->GetDispatcherQueue().TryEnqueue([curApp, signatureVerifyResult]()
{
if (FAILED(signatureVerifyResult))
{
curApp->m_pluginOperationStatus.uvSignatureVerificationStatus = signatureVerifyResult;
}
});
return S_OK;
}
/*
* This function is used to create a simplified version of authenticator data for the webauthn authenticator operations.
* Refer: https://www.w3.org/TR/webauthn-3/#authenticator-data for more details.
*/
HRESULT CreateAuthenticatorData(wil::shared_ncrypt_key hKey,
DWORD cbRpId,
PBYTE pbRpId,
DWORD& pcbPackedAuthenticatorData,
wil::unique_hlocal_ptr<BYTE[]>& ppbpackedAuthenticatorData,
std::vector<uint8_t>& vCredentialIdBuffer)
{
// Get the public key blob
DWORD cbPubKeyBlob = 0;
THROW_IF_FAILED(NCryptExportKey(
hKey.get(),
NULL,
BCRYPT_ECCPUBLIC_BLOB,
NULL,
NULL,
0,
&cbPubKeyBlob,
0));
auto pbPubKeyBlob = std::make_unique<BYTE[]>(cbPubKeyBlob);
THROW_HR_IF(E_UNEXPECTED, pbPubKeyBlob == nullptr);
DWORD cbPubKeyBlobOutput = 0;
THROW_IF_FAILED(NCryptExportKey(
hKey.get(),
NULL,
BCRYPT_ECCPUBLIC_BLOB,
NULL,
pbPubKeyBlob.get(),
cbPubKeyBlob,
&cbPubKeyBlobOutput,
0));
BCRYPT_ECCKEY_BLOB* pPubKeyBlobHeader = reinterpret_cast<BCRYPT_ECCKEY_BLOB*>(pbPubKeyBlob.get());
DWORD cbXCoord = pPubKeyBlobHeader->cbKey;
PBYTE pbXCoord = reinterpret_cast<PBYTE>(&pPubKeyBlobHeader[1]);
DWORD cbYCoord = pPubKeyBlobHeader->cbKey;
PBYTE pbYCoord = pbXCoord + cbXCoord;
// create byte span for x and y
std::span<const BYTE> xCoord(pbXCoord, cbXCoord);
std::span<const BYTE> yCoord(pbYCoord, cbYCoord);
// CBOR encode the public key in this order: kty, alg, crv, x, y
std::vector<BYTE> buffer;
#pragma warning(push)
#pragma warning(disable: 4293)
size_t bufferSize = CborLite::encodeMapSize(buffer, 5u);
#pragma warning(pop)
// COSE CBOR encoding format. Refer to https://datatracker.ietf.org/doc/html/rfc9052#section-7 for more details.
const int8_t ktyIndex = 1;
const int8_t algIndex = 3;
const int8_t crvIndex = -1;
const int8_t xIndex = -2;
const int8_t yIndex = -3;
// Example values for EC2 P-256 ES256 Keys. Refer to https://www.w3.org/TR/webauthn-3/#example-bdbd14cc
// Note that this sample authenticator only supports ES256 keys.
const int8_t kty = 2; // Key type is EC2
const int8_t crv = 1; // Curve is P-256
const int8_t alg = -7; // Algorithm is ES256
bufferSize += CborLite::encodeInteger(buffer, ktyIndex);
bufferSize += CborLite::encodeInteger(buffer, kty);
bufferSize += CborLite::encodeInteger(buffer, algIndex);
bufferSize += CborLite::encodeInteger(buffer, alg);
bufferSize += CborLite::encodeInteger(buffer, crvIndex);
bufferSize += CborLite::encodeInteger(buffer, crv);
bufferSize += CborLite::encodeInteger(buffer, xIndex);
bufferSize += CborLite::encodeBytes(buffer, xCoord);
bufferSize += CborLite::encodeInteger(buffer, yIndex);
bufferSize += CborLite::encodeBytes(buffer, yCoord);
wil::unique_bcrypt_hash hashHandle;
THROW_IF_NTSTATUS_FAILED(BCryptCreateHash(
BCRYPT_SHA256_ALG_HANDLE,
&hashHandle,
nullptr,
0,
nullptr,
0,
0));
THROW_IF_NTSTATUS_FAILED(BCryptHashData(hashHandle.get(), reinterpret_cast<PUCHAR>(pbXCoord), cbXCoord, 0));
THROW_IF_NTSTATUS_FAILED(BCryptHashData(hashHandle.get(), reinterpret_cast<PUCHAR>(pbYCoord), cbYCoord, 0));
DWORD cbHash = 0;
DWORD bytesRead = 0;
THROW_IF_NTSTATUS_FAILED(BCryptGetProperty(
hashHandle.get(),
BCRYPT_HASH_LENGTH,
reinterpret_cast<PBYTE>(&cbHash),
sizeof(cbHash),
&bytesRead,
0));
wil::unique_hlocal_ptr<BYTE[]> pbCredentialId = wil::make_unique_hlocal<BYTE[]>(cbHash);
THROW_IF_NTSTATUS_FAILED(BCryptFinishHash(hashHandle.get(), pbCredentialId.get(), cbHash, 0));
// Close the key and hash handle
hKey.reset();
hashHandle.reset();
com_ptr<App> curApp = winrt::Microsoft::UI::Xaml::Application::Current().as<App>();
PluginOperationType operationType = PLUGIN_OPERATION_TYPE_MAKE_CREDENTIAL;
if (curApp &&
curApp->m_pluginOperationOptions.operationType == PLUGIN_OPERATION_TYPE_GET_ASSERTION)
{
operationType = PLUGIN_OPERATION_TYPE_GET_ASSERTION;
}
// Refer to learn about packing credential data https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data
const DWORD rpidsha256Size = 32; // SHA256 hash of rpId
const DWORD flagsSize = 1; // flags
const DWORD signCountSize = 4; // signCount
DWORD cbPackedAuthenticatorData = rpidsha256Size + flagsSize + signCountSize;
if (operationType == PLUGIN_OPERATION_TYPE_MAKE_CREDENTIAL)
{
cbPackedAuthenticatorData += sizeof(GUID); // aaGuid
cbPackedAuthenticatorData += sizeof(WORD); // credentialId length
cbPackedAuthenticatorData += cbHash; // credentialId
cbPackedAuthenticatorData += static_cast<DWORD>(buffer.size()); // public key
}
std::vector<BYTE> vPackedAuthenticatorData(cbPackedAuthenticatorData);
auto writer = buffer_writer{ vPackedAuthenticatorData };
auto rgbRpIdHash = writer.reserve_space<std::array<BYTE, rpidsha256Size>>(); // 32 bytes of rpIdHash which is SHA256 hash of rpName. https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data
DWORD cbRpIdHash;
THROW_IF_WIN32_BOOL_FALSE(CryptHashCertificate2(BCRYPT_SHA256_ALGORITHM,
0,
nullptr,
pbRpId,
cbRpId,
rgbRpIdHash->data(),
&cbRpIdHash));
// Flags uv, up, be, and at are set
if (operationType == PLUGIN_OPERATION_TYPE_GET_ASSERTION)
{
// Refer https://www.w3.org/TR/webauthn-3/#authdata-flags
*writer.reserve_space<uint8_t>() = 0x1d; // credential data flags of size 1 byte
*writer.reserve_space<uint32_t>() = 0u; // Sign count of size 4 bytes is set to 0
vCredentialIdBuffer.assign(pbCredentialId.get(), pbCredentialId.get() + cbHash);
}
else
{
// Refer https://www.w3.org/TR/webauthn-3/#authdata-flags
*writer.reserve_space<uint8_t>() = 0x5d; // credential data flags of size 1 byte
*writer.reserve_space<uint32_t>() = 0u; // Sign count of size 4 bytes is set to 0
*writer.reserve_space<GUID>() = GUID_NULL; // aaGuid of size 16 bytes is set to 0
// Retrieve credential id
WORD cbCredentialId = static_cast<WORD>(cbHash);
WORD cbCredentialIdBigEndian = _byteswap_ushort(cbCredentialId);
*writer.reserve_space<WORD>() = cbCredentialIdBigEndian; // Size of credential id in unsigned big endian of size 2 bytes
writer.add(std::span<BYTE>(pbCredentialId.get(), cbHash)); // Set credential id
vCredentialIdBuffer.assign(pbCredentialId.get(), pbCredentialId.get() + cbHash);
writer.add(std::span<BYTE>(buffer.data(), buffer.size())); // Set CBOR encoded public key
}
pcbPackedAuthenticatorData = static_cast<DWORD>(vPackedAuthenticatorData.size());
ppbpackedAuthenticatorData = wil::make_unique_hlocal<BYTE[]>(pcbPackedAuthenticatorData);
memcpy_s(ppbpackedAuthenticatorData.get(), pcbPackedAuthenticatorData, vPackedAuthenticatorData.data(), pcbPackedAuthenticatorData);
return S_OK;
}
/*
* This function is invoked by the platform to request the plugin to handle a make credential operation.
* Refer: pluginauthenticator.h/pluginauthenticator.idl
*/
HRESULT STDMETHODCALLTYPE ContosoPlugin::EXPERIMENTAL_PluginMakeCredential(
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST pPluginMakeCredentialRequest,
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE* response) noexcept
{
try
{
SetEvent(App::s_pluginOpRequestRecievedEvent.get()); // indicate COM message received
DWORD hIndex = 0;
RETURN_IF_FAILED(CoWaitForMultipleHandles( // wait for app to be ready
COWAIT_DISPATCH_WINDOW_MESSAGES | COWAIT_DISPATCH_CALLS,
INFINITE,
1,
App::s_hAppReadyForPluginOpEvent.addressof(),
&hIndex));
com_ptr<App> curApp = winrt::Microsoft::UI::Xaml::Application::Current().as<App>();
wil::shared_hmodule webauthnDll(LoadLibraryExW(L"webauthn.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32));
if (webauthnDll == nullptr)
{
return E_ABORT;
}
wil::unique_cotaskmem_ptr<EXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST> pDecodedMakeCredentialRequest;
auto webauthnDecodeMakeCredentialRequest = GetProcAddressByFunctionDeclaration(webauthnDll.get(), EXPERIMENTAL_WebAuthNDecodeMakeCredentialRequest);
THROW_IF_FAILED(webauthnDecodeMakeCredentialRequest(
pPluginMakeCredentialRequest->cbEncodedRequest,
pPluginMakeCredentialRequest->pbEncodedRequest,
wil::out_param(pDecodedMakeCredentialRequest)));
auto rpName = wil::make_cotaskmem_string(pDecodedMakeCredentialRequest->pRpInformation->pwszName);
auto userName = wil::make_cotaskmem_string(pDecodedMakeCredentialRequest->pUserInformation->pwszName);
std::vector<BYTE> requestBuffer(
pPluginMakeCredentialRequest->pbEncodedRequest,
pPluginMakeCredentialRequest->pbEncodedRequest + pPluginMakeCredentialRequest->cbEncodedRequest);
auto ppbPubKeyData = GetRequestSigningPubKey();
HRESULT requestSignResult = E_FAIL;
if (!ppbPubKeyData.empty())
{
requestSignResult = VerifySignatureHelper(
requestBuffer,
ppbPubKeyData.data(),
static_cast<DWORD>(ppbPubKeyData.size()),
pPluginMakeCredentialRequest->pbRequestSignature,
pPluginMakeCredentialRequest->cbRequestSignature);
}
{
std::lock_guard<std::mutex> lock(curApp->m_pluginOperationOptionsMutex);
curApp->m_pluginOperationStatus.requestSignatureVerificationStatus = requestSignResult;
}
THROW_IF_FAILED(PerformUv(curApp,
pPluginMakeCredentialRequest->hWnd,
webauthnDll,
pPluginMakeCredentialRequest->transactionId,
PLUGIN_OPERATION_TYPE_MAKE_CREDENTIAL,
requestBuffer,
std::move(rpName),
std::move(userName)));
//create a persisted key using ncrypt
wil::unique_ncrypt_prov hProvider;
wil::unique_ncrypt_key hKey;
// get the provider
THROW_IF_FAILED(NCryptOpenStorageProvider(&hProvider, nullptr, 0));
// get the user handle as a string
std::wstring keyNameStr = contosoplugin_key_domain;
std::wstringstream keyNameStream;
for (DWORD idx = 0; idx < pDecodedMakeCredentialRequest->pUserInformation->cbId; idx++)
{
keyNameStream << std::hex << std::setw(2) << std::setfill(L'0') <<
static_cast<int>(pDecodedMakeCredentialRequest->pUserInformation->pbId[idx]);
}
keyNameStr += keyNameStream.str();
// create the key
THROW_IF_FAILED(NCryptCreatePersistedKey(
hProvider.get(),
&hKey,
BCRYPT_ECDH_P256_ALGORITHM,
keyNameStr.c_str(),
0,
NCRYPT_OVERWRITE_KEY_FLAG));
// set the export policy
DWORD exportPolicy = NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG;
THROW_IF_FAILED(NCryptSetProperty(
hKey.get(),
NCRYPT_EXPORT_POLICY_PROPERTY,
reinterpret_cast<PBYTE>(&exportPolicy),
sizeof(exportPolicy),
NCRYPT_PERSIST_FLAG));
// allow both signing and encryption
DWORD keyUsage = NCRYPT_ALLOW_SIGNING_FLAG | NCRYPT_ALLOW_DECRYPT_FLAG;
THROW_IF_FAILED(NCryptSetProperty(
hKey.get(),
NCRYPT_KEY_USAGE_PROPERTY,
reinterpret_cast<PBYTE>(&keyUsage),
sizeof(keyUsage),
NCRYPT_PERSIST_FLAG));
HWND hWnd;
if (curApp->m_silentMode)
{
hWnd = curApp->m_pluginOperationOptions.hWnd;
}
else
{
hWnd = curApp->GetNativeWindowHandle();
}
THROW_IF_FAILED(NCryptSetProperty(
hKey.get(),
NCRYPT_WINDOW_HANDLE_PROPERTY,
reinterpret_cast<PBYTE>(&hWnd),
sizeof(HWND),
0));
// finalize the key
THROW_IF_FAILED(NCryptFinalizeKey(hKey.get(), 0));
DWORD cbPackedAuthenticatorData = 0;
wil::unique_hlocal_ptr<BYTE[]> packedAuthenticatorData;
std::vector<uint8_t> vCredentialIdBuffer;
THROW_IF_FAILED(CreateAuthenticatorData(
std::move(hKey),
pDecodedMakeCredentialRequest->cbRpId,
pDecodedMakeCredentialRequest->pbRpId,
cbPackedAuthenticatorData,
packedAuthenticatorData,
vCredentialIdBuffer));
auto operationResponse = wil::make_unique_cotaskmem<EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE>();
WEBAUTHN_CREDENTIAL_ATTESTATION attestationResponse{};
attestationResponse.dwVersion = WEBAUTHN_CREDENTIAL_ATTESTATION_CURRENT_VERSION;
attestationResponse.pwszFormatType = WEBAUTHN_ATTESTATION_TYPE_NONE;
attestationResponse.cbAttestation = 0;
attestationResponse.pbAttestation = nullptr;
attestationResponse.cbAuthenticatorData = 0;
attestationResponse.pbAuthenticatorData = nullptr;
attestationResponse.pbAuthenticatorData = packedAuthenticatorData.get();
attestationResponse.cbAuthenticatorData = cbPackedAuthenticatorData;
DWORD cbAttestationBuffer = 0;
PBYTE pbattestationBuffer;
auto webauthnEncodeMakeCredentialResponse = GetProcAddressByFunctionDeclaration(webauthnDll.get(), EXPERIMENTAL_WebAuthNEncodeMakeCredentialResponse);
THROW_IF_FAILED(webauthnEncodeMakeCredentialResponse(
&attestationResponse,
&cbAttestationBuffer,
&pbattestationBuffer));
operationResponse->cbEncodedResponse = cbAttestationBuffer;
operationResponse->pbEncodedResponse = wil::make_unique_cotaskmem<BYTE[]>(cbAttestationBuffer).release();
memcpy_s(operationResponse->pbEncodedResponse,
operationResponse->cbEncodedResponse,
pbattestationBuffer,
cbAttestationBuffer);
*response = operationResponse.release();
WEBAUTHN_CREDENTIAL_DETAILS credentialDetails{};
credentialDetails.dwVersion = WEBAUTHN_CREDENTIAL_DETAILS_CURRENT_VERSION;
credentialDetails.pUserInformation = const_cast<PWEBAUTHN_USER_ENTITY_INFORMATION>(pDecodedMakeCredentialRequest->pUserInformation);
credentialDetails.pRpInformation = const_cast<PWEBAUTHN_RP_ENTITY_INFORMATION>(pDecodedMakeCredentialRequest->pRpInformation);
credentialDetails.cbCredentialID = static_cast<DWORD>(vCredentialIdBuffer.size());
credentialDetails.pbCredentialID = wil::make_unique_cotaskmem<BYTE[]>(vCredentialIdBuffer.size()).release();
memcpy_s(credentialDetails.pbCredentialID, credentialDetails.cbCredentialID, vCredentialIdBuffer.data(), static_cast<DWORD>(vCredentialIdBuffer.size()));
if (!PluginCredentialManager::getInstance().SaveCredentialMetadataToMockDB(credentialDetails))
{
std::lock_guard<std::mutex> lock(curApp->m_pluginOperationOptionsMutex);
curApp->m_pluginOperationStatus.performOperationStatus = E_FAIL;
}
pDecodedMakeCredentialRequest.reset();
SetEvent(App::s_hPluginOpCompletedEvent.get());
return S_OK;
}
catch (...)
{
HRESULT hr = wil::ResultFromCaughtException();
com_ptr<App> curApp = winrt::Microsoft::UI::Xaml::Application::Current().as<App>();
if (curApp)
{
hr = winrt::to_hresult();
std::lock_guard<std::mutex> lock(curApp->m_pluginOperationOptionsMutex);
curApp->m_pluginOperationStatus.performOperationStatus = hr;
};
SetEvent(App::s_hPluginOpCompletedEvent.get());
return hr;
}
}
/*
* This function is invoked by the platform to request the plugin to handle a get assertion operation.
* Refer: pluginauthenticator.h/pluginauthenticator.idl
*/
HRESULT STDMETHODCALLTYPE ContosoPlugin::EXPERIMENTAL_PluginGetAssertion(
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST pPluginGetAssertionRequest,
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE* response) noexcept
{
try
{
SetEvent(App::s_pluginOpRequestRecievedEvent.get());
DWORD hIndex = 0;
RETURN_IF_FAILED(CoWaitForMultipleHandles(
COWAIT_DISPATCH_WINDOW_MESSAGES | COWAIT_DISPATCH_CALLS,
INFINITE,
1,
App::s_hAppReadyForPluginOpEvent.addressof(),
&hIndex));
com_ptr<App> curApp = winrt::Microsoft::UI::Xaml::Application::Current().as<App>();
wil::shared_hmodule webauthnDll(LoadLibraryExW(L"webauthn.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32));
if (webauthnDll == nullptr)
{
return E_ABORT;
}
wil::unique_cotaskmem_ptr<EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST> pDecodedAssertionRequest;
// The EXPERIMENTAL_WebAuthNDecodeGetAssertionRequest function can be optionally used to decode the CBOR encoded request to a EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST structure.
auto webauthnDecodeGetAssertionRequest = GetProcAddressByFunctionDeclaration(webauthnDll.get(), EXPERIMENTAL_WebAuthNDecodeGetAssertionRequest);
webauthnDecodeGetAssertionRequest(pPluginGetAssertionRequest->cbEncodedRequest, pPluginGetAssertionRequest->pbEncodedRequest, wil::out_param(pDecodedAssertionRequest));
wil::shared_cotaskmem_string rpName = wil::make_cotaskmem_string(pDecodedAssertionRequest->pwszRpId);
//load the user handle
auto& credManager = PluginCredentialManager::getInstance();
const WEBAUTHN_CREDENTIAL_DETAILS* selectedCredential{};
// create a list of credentials
std::vector<const WEBAUTHN_CREDENTIAL_DETAILS *> selectedCredentials;
while (true)
{
Sleep(100);
if (credManager.IsLocalCredentialMetadataLoaded())
{
credManager.GetLocalCredsByRpIdAndAllowList(pDecodedAssertionRequest->pwszRpId,
pDecodedAssertionRequest->CredentialList.ppCredentials,
pDecodedAssertionRequest->CredentialList.cCredentials,
selectedCredentials);
break;
}
}
if (selectedCredentials.empty())
{
{
std::lock_guard<std::mutex> lock(curApp->m_pluginOperationOptionsMutex);
curApp->m_pluginOperationStatus.performOperationStatus = NTE_NOT_FOUND;
}
SetEvent(App::s_hPluginOpCompletedEvent.get());
return NTE_NOT_FOUND;
}
else if (selectedCredentials.size() == 1 && credManager.GetSilentOperation())
{
selectedCredential = selectedCredentials[0];
}
else
{
curApp->SetMatchingCredentials(pDecodedAssertionRequest->pwszRpId, selectedCredentials, pPluginGetAssertionRequest->hWnd);
hIndex = 0;
RETURN_IF_FAILED(CoWaitForMultipleHandles(COWAIT_DISPATCH_WINDOW_MESSAGES | COWAIT_DISPATCH_CALLS, INFINITE, 1, curApp->m_hPluginCredentialSelected.addressof(), &hIndex));
{
std::lock_guard<std::mutex> lock(curApp->m_pluginOperationOptionsMutex);
selectedCredential = curApp->m_pluginOperationOptions.selectedCredential;
}
// Failed to select a credential
if (selectedCredential->cbCredentialID == 0 ||
selectedCredential->pbCredentialID == nullptr ||
selectedCredential->pUserInformation == nullptr ||
selectedCredential->pUserInformation->pwszName == nullptr)
{
{
std::lock_guard<std::mutex> lock(curApp->m_pluginOperationOptionsMutex);
curApp->m_pluginOperationStatus.performOperationStatus = NTE_NOT_FOUND;
}
SetEvent(App::s_hPluginOpCompletedEvent.get());
return NTE_NOT_FOUND;
}
}
wil::shared_cotaskmem_string userName = wil::make_cotaskmem_string(selectedCredential->pUserInformation->pwszName);
std::vector<BYTE> requestBuffer(
pPluginGetAssertionRequest->pbEncodedRequest,
pPluginGetAssertionRequest->pbEncodedRequest + pPluginGetAssertionRequest->cbEncodedRequest);
auto ppbPubKeyData = GetRequestSigningPubKey();
HRESULT requestSignResult = E_FAIL;
if (!ppbPubKeyData.empty())
{
requestSignResult = VerifySignatureHelper(
requestBuffer,
ppbPubKeyData.data(),
static_cast<DWORD>(ppbPubKeyData.size()),
pPluginGetAssertionRequest->pbRequestSignature,
pPluginGetAssertionRequest->cbRequestSignature);
}
{
std::lock_guard<std::mutex> lock(curApp->m_pluginOperationOptionsMutex);
curApp->m_pluginOperationStatus.requestSignatureVerificationStatus = requestSignResult;
}
THROW_IF_FAILED(PerformUv(curApp,
pPluginGetAssertionRequest->hWnd,
webauthnDll,
pPluginGetAssertionRequest->transactionId,
PLUGIN_OPERATION_TYPE_GET_ASSERTION,
requestBuffer,
rpName,
userName));
// convert user handle to a string
std::wstring keyNameStr = contosoplugin_key_domain;
std::wstringstream keyNameStream;
for (DWORD idx = 0; idx < selectedCredential->pUserInformation->cbId; idx++)
{
keyNameStream << std::hex << std::setw(2) << std::setfill(L'0') <<
static_cast<int>(selectedCredential->pUserInformation->pbId[idx]);
}
keyNameStr += keyNameStream.str();
//open the key using ncrypt and sign the data
wil::unique_ncrypt_prov hProvider;
wil::shared_ncrypt_key hKey;
// get the provider
THROW_IF_FAILED(NCryptOpenStorageProvider(&hProvider, nullptr, 0));
// open the key
THROW_IF_FAILED(NCryptOpenKey(hProvider.get(), &hKey, keyNameStr.c_str(), 0, 0));
// set hwnd property
wil::unique_hwnd hWnd;
if (curApp->m_silentMode)
{
hWnd.reset(curApp->m_pluginOperationOptions.hWnd);
}
else
{
hWnd.reset(curApp->GetNativeWindowHandle());
}
THROW_IF_FAILED(NCryptSetProperty(
hKey.get(),
NCRYPT_WINDOW_HANDLE_PROPERTY,
(BYTE*)(hWnd.addressof()),
sizeof(HWND),
0));
// create authenticator data
DWORD cbPackedAuthenticatorData = 0;
wil::unique_hlocal_ptr<BYTE[]> packedAuthenticatorData;
std::vector<uint8_t> vCredentialIdBuffer;
THROW_IF_FAILED(CreateAuthenticatorData(hKey,
pDecodedAssertionRequest->cbRpId,
pDecodedAssertionRequest->pbRpId,
cbPackedAuthenticatorData,
packedAuthenticatorData,
vCredentialIdBuffer));
wil::unique_hlocal_ptr<BYTE[]> pbSignature = nullptr;
DWORD cbSignature = 0;
{
wil::unique_bcrypt_hash hashHandle;
THROW_IF_NTSTATUS_FAILED(BCryptCreateHash(
BCRYPT_SHA256_ALG_HANDLE,
&hashHandle,
nullptr,
0,
nullptr,
0,
0));
THROW_IF_NTSTATUS_FAILED(BCryptHashData(hashHandle.get(), const_cast<PUCHAR>(packedAuthenticatorData.get()), cbPackedAuthenticatorData, 0));
THROW_IF_NTSTATUS_FAILED(BCryptHashData(hashHandle.get(), const_cast<PUCHAR>(pDecodedAssertionRequest->pbClientDataHash), pDecodedAssertionRequest->cbClientDataHash, 0));
DWORD bytesRead = 0;
DWORD cbSignatureBuffer = 0;
THROW_IF_NTSTATUS_FAILED(BCryptGetProperty(
hashHandle.get(),
BCRYPT_HASH_LENGTH,
reinterpret_cast<PBYTE>(&cbSignatureBuffer),
sizeof(cbSignatureBuffer),
&bytesRead,
0));
wil::unique_hlocal_ptr<BYTE[]> signatureBuffer = wil::make_unique_hlocal<BYTE[]>(cbSignatureBuffer);
THROW_HR_IF(E_UNEXPECTED, signatureBuffer == nullptr);
THROW_IF_NTSTATUS_FAILED(BCryptFinishHash(hashHandle.get(), signatureBuffer.get(), cbSignatureBuffer, 0));
// sign the data
THROW_IF_FAILED(NCryptSignHash(hKey.get(), nullptr, signatureBuffer.get(), cbSignatureBuffer, nullptr, 0, &cbSignature, 0));
pbSignature = wil::make_unique_hlocal<BYTE[]>(cbSignature);
THROW_HR_IF(E_UNEXPECTED, pbSignature == nullptr);
THROW_IF_FAILED(NCryptSignHash(hKey.get(), nullptr, signatureBuffer.get(), cbSignatureBuffer, pbSignature.get(), cbSignature, &cbSignature, 0));
signatureBuffer.reset();
auto encodeSignature = [](PBYTE signature, size_t signatureSize)
{
std::vector<BYTE> encodedSignature{};
encodedSignature.push_back(0x02); // ASN integer tag
encodedSignature.push_back(static_cast<BYTE>(signatureSize)); // length of the signature
if (WI_IsFlagSet(signature[0], 0x80))
{
encodedSignature[encodedSignature.size() - 1]++;
encodedSignature.push_back(0x00); // add a padding byte if the first byte has the high bit set
}
encodedSignature.insert(encodedSignature.end(), signature, signature + signatureSize);
return encodedSignature;
};
auto signatureR = encodeSignature(pbSignature.get(), cbSignature / 2);
auto signatureS = encodeSignature(pbSignature.get() + cbSignature / 2, cbSignature / 2);
std::vector<BYTE> encodedSignature{};
encodedSignature.push_back(0x30); // ASN sequence tag
encodedSignature.push_back(static_cast<BYTE>(signatureR.size() + signatureS.size())); // length of the sequence
encodedSignature.insert(encodedSignature.end(), signatureR.begin(), signatureR.end());
encodedSignature.insert(encodedSignature.end(), signatureS.begin(), signatureS.end());
cbSignature = static_cast<DWORD>(encodedSignature.size());
pbSignature.reset();
pbSignature = wil::make_unique_hlocal<BYTE[]>(cbSignature);
THROW_HR_IF(E_UNEXPECTED, pbSignature == nullptr);
memcpy_s(pbSignature.get(), cbSignature, encodedSignature.data(), static_cast<DWORD>(cbSignature));
}
// create the response
auto operationResponse = wil::make_unique_cotaskmem<EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE>();
auto assertionResponse = wil::make_unique_cotaskmem<WEBAUTHN_ASSERTION>();
assertionResponse->dwVersion = WEBAUTHN_ASSERTION_CURRENT_VERSION;
// [1] Credential (optional)
assertionResponse->Credential.dwVersion = WEBAUTHN_CREDENTIAL_CURRENT_VERSION;
assertionResponse->Credential.cbId = static_cast<DWORD>(vCredentialIdBuffer.size());
assertionResponse->Credential.pbId = vCredentialIdBuffer.data();
assertionResponse->Credential.pwszCredentialType = WEBAUTHN_CREDENTIAL_TYPE_PUBLIC_KEY;
// [2] AuthenticatorData
assertionResponse->cbAuthenticatorData = cbPackedAuthenticatorData;
assertionResponse->pbAuthenticatorData = packedAuthenticatorData.get();
// [3] Signature
assertionResponse->cbSignature = cbSignature;
assertionResponse->pbSignature = pbSignature.get();
// [4] User (optional)
assertionResponse->cbUserId = selectedCredential->pUserInformation->cbId;
auto userIdBuffer = wil::make_unique_cotaskmem<BYTE[]>(selectedCredential->pUserInformation->cbId);
memcpy_s(userIdBuffer.get(),
selectedCredential->pUserInformation->cbId,
selectedCredential->pUserInformation->pbId,
selectedCredential->pUserInformation->cbId);
assertionResponse->pbUserId = userIdBuffer.get();
WEBAUTHN_USER_ENTITY_INFORMATION userEntityInformation{};
userEntityInformation.dwVersion = WEBAUTHN_USER_ENTITY_INFORMATION_CURRENT_VERSION;
userEntityInformation.cbId = assertionResponse->cbUserId;
userEntityInformation.pbId = assertionResponse->pbUserId;
auto ctapGetAssertionResponse = wil::make_unique_cotaskmem<EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_RESPONSE>();
ctapGetAssertionResponse->WebAuthNAssertion = *(assertionResponse.get()); // [1] Credential, [2] AuthenticatorData, [3] Signature
ctapGetAssertionResponse->pUserInformation = &userEntityInformation; // [4] User
ctapGetAssertionResponse->dwNumberOfCredentials = 1; // [5] NumberOfCredentials
DWORD cbAssertionBuffer = 0;
PBYTE pbAssertionBuffer;
// The EXPERIMENTAL_WebAuthNEncodeGetAssertionResponse function can be optionally used to encode the
// EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_RESPONSE structure to a CBOR encoded response.
auto webAuthNEncodeGetAssertionResponse = GetProcAddressByFunctionDeclaration(webauthnDll.get(), EXPERIMENTAL_WebAuthNEncodeGetAssertionResponse);
THROW_IF_FAILED(webAuthNEncodeGetAssertionResponse(
(EXPERIMENTAL_PCWEBAUTHN_CTAPCBOR_GET_ASSERTION_RESPONSE)(ctapGetAssertionResponse.get()),
&cbAssertionBuffer,
&pbAssertionBuffer));
assertionResponse.reset();
ctapGetAssertionResponse.reset();
userIdBuffer.reset();
packedAuthenticatorData.reset();
pbSignature.reset();
pDecodedAssertionRequest.reset();
operationResponse->cbEncodedResponse = cbAssertionBuffer;
// pbEncodedResponse must contain a CBOR encoded response as specified the FIDO CTAP.
// Refer: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#message-encoding.
operationResponse->pbEncodedResponse = wil::make_unique_cotaskmem<BYTE[]>(cbAssertionBuffer).release();
memcpy_s(
operationResponse->pbEncodedResponse,
operationResponse->cbEncodedResponse,
pbAssertionBuffer,
cbAssertionBuffer);
*response = operationResponse.release();
SetEvent(App::s_hPluginOpCompletedEvent.get());
return S_OK;
}
catch (...)
{
HRESULT localHr = wil::ResultFromCaughtException();
{
winrt::com_ptr<App> curApp = winrt::Microsoft::UI::Xaml::Application::Current().as<App>();
std::lock_guard<std::mutex> lock(curApp->m_pluginOperationOptionsMutex);
curApp->m_pluginOperationStatus.performOperationStatus = localHr;
}
SetEvent(App::s_hPluginOpCompletedEvent.get());
return localHr;
}
}
/*
* This function is invoked by the platform to request the plugin to cancel an ongoing operation.
*/
HRESULT STDMETHODCALLTYPE ContosoPlugin::EXPERIMENTAL_PluginCancelOperation(
/* [out] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST)
{
SetEvent(App::s_pluginOpRequestRecievedEvent.get());
com_ptr<App> curApp = winrt::Microsoft::UI::Xaml::Application::Current().as<App>();
curApp->GetDispatcherQueue().TryEnqueue([curApp]()
{
curApp->PluginCancelAction();
});
return S_OK;
}
/*
* This is a sample implementation of a factory method that creates an instance of the Class that implements the EXPERIMENTAL_IPluginAuthenticator interface.
* Refer: pluginauthenticator.h/pluginauthenticator.idl for the interface definition.
*/
HRESULT __stdcall ContosoPluginFactory::CreateInstance(
::IUnknown* outer,
GUID const& iid,
void** result) noexcept
{
*result = nullptr;
if (outer)
{
return CLASS_E_NOAGGREGATION;
}
try
{
return make<ContosoPlugin>()->QueryInterface(iid, result);
}
catch (...)
{
return winrt::to_hresult();
}
}
HRESULT __stdcall ContosoPluginFactory::LockServer(BOOL) noexcept
{
return S_OK;
}
}

View File

@@ -0,0 +1,249 @@
use std::alloc::{alloc, Layout};
use std::ptr;
use serde_json;
use windows_core::{HRESULT, s};
use crate::types::*;
use crate::utils::{self as util, delay_load};
use crate::com_provider::ExperimentalWebAuthnPluginOperationResponse;
// Windows API types for WebAuthn (from webauthn.h.sample)
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST {
pub dwVersion: u32,
pub pwszRpId: *const u16, // PCWSTR
pub cbRpId: u32,
pub pbRpId: *const u8,
pub cbClientDataHash: u32,
pub pbClientDataHash: *const u8,
pub CredentialList: WEBAUTHN_CREDENTIAL_LIST,
pub cbCborExtensionsMap: u32,
pub pbCborExtensionsMap: *const u8,
// Add other fields as needed...
}
pub type PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST = *mut EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST;
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct WEBAUTHN_CREDENTIAL_LIST {
pub cCredentials: u32,
pub pCredentials: *const u8, // Placeholder
}
// Windows API function signatures for decoding get assertion requests
type EXPERIMENTAL_WebAuthNDecodeGetAssertionRequestFn = unsafe extern "stdcall" fn(
cbEncoded: u32,
pbEncoded: *const u8,
ppGetAssertionRequest: *mut PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST,
) -> HRESULT;
type EXPERIMENTAL_WebAuthNFreeDecodedGetAssertionRequestFn = unsafe extern "stdcall" fn(
pGetAssertionRequest: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST,
);
// RAII wrapper for decoded get assertion request
pub struct DecodedGetAssertionRequest {
ptr: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST,
free_fn: Option<EXPERIMENTAL_WebAuthNFreeDecodedGetAssertionRequestFn>,
}
impl DecodedGetAssertionRequest {
fn new(ptr: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST, free_fn: Option<EXPERIMENTAL_WebAuthNFreeDecodedGetAssertionRequestFn>) -> Self {
Self { ptr, free_fn }
}
pub fn as_ref(&self) -> &EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST {
unsafe { &*self.ptr }
}
}
impl Drop for DecodedGetAssertionRequest {
fn drop(&mut self) {
if !self.ptr.is_null() {
if let Some(free_fn) = self.free_fn {
util::message("Freeing decoded get assertion request");
unsafe { free_fn(self.ptr); }
}
}
}
}
// Function to decode get assertion request using Windows API
pub unsafe fn decode_get_assertion_request(encoded_request: &[u8]) -> Result<DecodedGetAssertionRequest, String> {
util::message("Attempting to decode get assertion request using Windows API");
// Load the Windows WebAuthn API function
let decode_fn: Option<EXPERIMENTAL_WebAuthNDecodeGetAssertionRequestFn> = delay_load(
s!("webauthn.dll"),
s!("EXPERIMENTAL_WebAuthNDecodeGetAssertionRequest")
);
let decode_fn = decode_fn.ok_or("Failed to load EXPERIMENTAL_WebAuthNDecodeGetAssertionRequest from webauthn.dll")?;
// Load the free function
let free_fn: Option<EXPERIMENTAL_WebAuthNFreeDecodedGetAssertionRequestFn> = delay_load(
s!("webauthn.dll"),
s!("EXPERIMENTAL_WebAuthNFreeDecodedGetAssertionRequest")
);
let mut pp_get_assertion_request: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST = ptr::null_mut();
let result = decode_fn(
encoded_request.len() as u32,
encoded_request.as_ptr(),
&mut pp_get_assertion_request,
);
if result.is_err() || pp_get_assertion_request.is_null() {
return Err(format!("EXPERIMENTAL_WebAuthNDecodeGetAssertionRequest failed with HRESULT: {}", result.0));
}
Ok(DecodedGetAssertionRequest::new(pp_get_assertion_request, free_fn))
}
/// Context information parsed from the incoming request
#[derive(Debug, Clone)]
pub struct RequestContext {
pub rpid: Option<String>,
pub allowed_credentials: Vec<Vec<u8>>,
pub user_verification: Option<bool>,
pub user_id: Option<Vec<u8>>,
pub user_name: Option<String>,
pub user_display_name: Option<String>,
pub client_data_hash: Option<Vec<u8>>,
pub supported_algorithms: Vec<i32>, // COSE algorithm identifiers
}
impl Default for RequestContext {
fn default() -> Self {
Self {
rpid: None,
allowed_credentials: Vec::new(),
user_verification: None,
user_id: None,
user_name: None,
user_display_name: None,
client_data_hash: None,
supported_algorithms: Vec::new(),
}
}
}
/// Helper for assertion requests
pub fn send_assertion_request(rpid: &str, transaction_id: &str, context: &RequestContext) -> Option<PasskeyResponse> {
// Extract client data hash from context - this is required for WebAuthn
let client_data_hash = match &context.client_data_hash {
Some(hash) if !hash.is_empty() => hash.clone(),
_ => {
util::message("ERROR: Client data hash is required for assertion but not provided");
return None;
}
};
let request = PasskeyAssertionRequest {
rp_id: rpid.to_string(),
transaction_id: transaction_id.to_string(),
client_data_hash,
allowed_credentials: context.allowed_credentials.clone(),
user_verification: context.user_verification.unwrap_or(false),
};
util::message(&format!("Assertion request data - RP ID: {}, Client data hash: {} bytes, Allowed credentials: {}",
rpid, request.client_data_hash.len(), request.allowed_credentials.len()));
match serde_json::to_string(&request) {
Ok(request_json) => {
util::message(&format!("Sending assertion request: {}", request_json));
crate::ipc::send_passkey_request(RequestType::Assertion, request_json, rpid)
},
Err(e) => {
util::message(&format!("ERROR: Failed to serialize assertion request: {}", e));
None
}
}
}
/// Creates a WebAuthn get assertion response from Bitwarden's assertion response
pub unsafe fn create_get_assertion_response(
credential_id: Vec<u8>,
authenticator_data: Vec<u8>,
signature: Vec<u8>,
user_handle: Vec<u8>
) -> std::result::Result<*mut ExperimentalWebAuthnPluginOperationResponse, HRESULT> {
// Construct a CTAP2 response with the proper structure
// Create CTAP2 GetAssertion response map according to CTAP2 specification
let mut cbor_response: Vec<(ciborium::Value, ciborium::Value)> = Vec::new();
// [1] credential (optional) - Always include credential descriptor
let credential_map = vec![
(ciborium::Value::Text("id".to_string()), ciborium::Value::Bytes(credential_id.clone())),
(ciborium::Value::Text("type".to_string()), ciborium::Value::Text("public-key".to_string())),
];
cbor_response.push((
ciborium::Value::Integer(1.into()),
ciborium::Value::Map(credential_map)
));
// [2] authenticatorData (required)
cbor_response.push((
ciborium::Value::Integer(2.into()),
ciborium::Value::Bytes(authenticator_data)
));
// [3] signature (required)
cbor_response.push((
ciborium::Value::Integer(3.into()),
ciborium::Value::Bytes(signature)
));
// [4] user (optional) - include if user handle is provided
if !user_handle.is_empty() {
let user_map = vec![
(ciborium::Value::Text("id".to_string()), ciborium::Value::Bytes(user_handle)),
];
cbor_response.push((
ciborium::Value::Integer(4.into()),
ciborium::Value::Map(user_map)
));
}
let cbor_value = ciborium::Value::Map(cbor_response);
// Encode to CBOR with error handling
let mut cbor_data = Vec::new();
if let Err(e) = ciborium::ser::into_writer(&cbor_value, &mut cbor_data) {
util::message(&format!("ERROR: Failed to encode CBOR assertion response: {:?}", e));
return Err(HRESULT(-1));
}
let response_len = cbor_data.len();
// Allocate memory for the response data
let layout = Layout::from_size_align(response_len, 1).map_err(|_| HRESULT(-1))?;
let response_ptr = alloc(layout);
if response_ptr.is_null() {
return Err(HRESULT(-1));
}
// Copy response data
ptr::copy_nonoverlapping(cbor_data.as_ptr(), response_ptr, response_len);
// Allocate memory for the response structure
let response_layout = Layout::new::<ExperimentalWebAuthnPluginOperationResponse>();
let operation_response_ptr = alloc(response_layout) as *mut ExperimentalWebAuthnPluginOperationResponse;
if operation_response_ptr.is_null() {
return Err(HRESULT(-1));
}
// Initialize the response
ptr::write(operation_response_ptr, ExperimentalWebAuthnPluginOperationResponse {
encoded_response_byte_count: response_len as u32,
encoded_response_pointer: response_ptr,
});
Ok(operation_response_ptr)
}

View File

@@ -0,0 +1,84 @@
use std::alloc;
use std::mem::{align_of, MaybeUninit};
use std::ptr::NonNull;
use windows::Win32::System::Com::CoTaskMemAlloc;
#[repr(transparent)]
pub struct ComBuffer(NonNull<MaybeUninit<u8>>);
impl ComBuffer {
/// Returns an COM-allocated buffer of `size`.
fn alloc(size: usize, for_slice: bool) -> Self {
#[expect(clippy::as_conversions)]
{
assert!(size <= isize::MAX as usize, "requested bad object size");
}
// SAFETY: Any size is valid to pass to Windows, even `0`.
let ptr = NonNull::new(unsafe { CoTaskMemAlloc(size) }).unwrap_or_else(|| {
// XXX: This doesn't have to be correct, just close enough for an OK OOM error.
let layout = alloc::Layout::from_size_align(size, align_of::<u8>()).unwrap();
alloc::handle_alloc_error(layout)
});
if for_slice {
// Ininitialize the buffer so it can later be treated as `&mut [u8]`.
// SAFETY: The pointer is valid and we are using a valid value for a byte-wise allocation.
unsafe { ptr.write_bytes(0, size) };
}
Self(ptr.cast())
}
fn into_ptr<T>(self) -> *mut T {
self.0.cast().as_ptr()
}
/// Creates a new COM-allocated structure.
///
/// Note that `T` must be [Copy] to avoid any possible memory leaks.
pub fn with_object<T: Copy>(object: T) -> *mut T {
// NB: Vendored from Rust's alloc code since we can't yet allocate `Box` with a custom allocator.
const MIN_ALIGN: usize = if cfg!(target_pointer_width = "64") {
16
} else if cfg!(target_pointer_width = "32") {
8
} else {
panic!("unsupported arch")
};
// SAFETY: Validate that our alignment works for a normal size-based allocation for soundness.
let layout = const {
let layout = alloc::Layout::new::<T>();
assert!(layout.align() <= MIN_ALIGN);
layout
};
let buffer = Self::alloc(layout.size(), false);
// SAFETY: `ptr` is valid for writes of `T` because we correctly allocated the right sized buffer that
// accounts for any alignment requirements.
//
// Additionally, we ensure the value is treated as moved by forgetting the source.
unsafe { buffer.0.cast::<T>().write(object) };
buffer.into_ptr()
}
pub fn from_buffer<T: AsRef<[u8]>>(buffer: T) -> (*mut u8, u32) {
let buffer = buffer.as_ref();
let len = buffer.len();
let com_buffer = Self::alloc(len, true);
// SAFETY: `ptr` points to a valid allocation that `len` matches, and we made sure
// the bytes were initialized. Additionally, bytes have no alignment requirements.
unsafe {
NonNull::slice_from_raw_parts(com_buffer.0.cast::<u8>(), len)
.as_mut()
.copy_from_slice(buffer)
}
// Safety: The Windows API structures these buffers are placed into use `u32` (`DWORD`) to
// represent length.
#[expect(clippy::as_conversions)]
(com_buffer.into_ptr(), len as u32)
}
}

View File

@@ -0,0 +1,417 @@
use windows::Win32::System::Com::*;
use windows_core::{implement, interface, IInspectable, IUnknown, Interface, HRESULT};
use std::ptr;
use crate::types::*;
use crate::utils::{self as util, wstr_to_string};
use crate::assert::{RequestContext, decode_get_assertion_request, create_get_assertion_response, send_assertion_request};
use crate::make_credential::{decode_make_credential_request, create_make_credential_response, send_registration_request};
/// Used when creating and asserting credentials.
/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST
/// Header File Usage: EXPERIMENTAL_PluginMakeCredential()
/// EXPERIMENTAL_PluginGetAssertion()
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct ExperimentalWebAuthnPluginOperationRequest {
pub window_handle: windows::Win32::Foundation::HWND,
pub transaction_id: windows_core::GUID,
pub request_signature_byte_count: u32,
pub request_signature_pointer: *mut u8,
pub encoded_request_byte_count: u32,
pub encoded_request_pointer: *mut u8,
}
/// Used as a response when creating and asserting credentials.
/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE
/// Header File Usage: EXPERIMENTAL_PluginMakeCredential()
/// EXPERIMENTAL_PluginGetAssertion()
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct ExperimentalWebAuthnPluginOperationResponse {
pub encoded_response_byte_count: u32,
pub encoded_response_pointer: *mut u8,
}
/// Used to cancel an operation.
/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST
/// Header File Usage: EXPERIMENTAL_PluginCancelOperation()
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct ExperimentalWebAuthnPluginCancelOperationRequest {
pub transaction_id: windows_core::GUID,
pub request_signature_byte_count: u32,
pub request_signature_pointer: *mut u8,
}
#[interface("e6466e9a-b2f3-47c5-b88d-89bc14a8d998")]
pub unsafe trait EXPERIMENTAL_IPluginAuthenticator: windows_core::IUnknown {
fn EXPERIMENTAL_PluginMakeCredential(
&self,
request: *const ExperimentalWebAuthnPluginOperationRequest,
response: *mut *mut ExperimentalWebAuthnPluginOperationResponse,
) -> HRESULT;
fn EXPERIMENTAL_PluginGetAssertion(
&self,
request: *const ExperimentalWebAuthnPluginOperationRequest,
response: *mut *mut ExperimentalWebAuthnPluginOperationResponse,
) -> HRESULT;
fn EXPERIMENTAL_PluginCancelOperation(
&self,
request: *const ExperimentalWebAuthnPluginCancelOperationRequest,
) -> HRESULT;
}
#[implement(EXPERIMENTAL_IPluginAuthenticator)]
pub struct PluginAuthenticatorComObject;
#[implement(IClassFactory)]
pub struct Factory;
impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Impl {
unsafe fn EXPERIMENTAL_PluginMakeCredential(
&self,
request: *const ExperimentalWebAuthnPluginOperationRequest,
response: *mut *mut ExperimentalWebAuthnPluginOperationResponse,
) -> HRESULT {
util::message("=== EXPERIMENTAL_PluginMakeCredential() called ===");
if request.is_null() {
util::message("ERROR: NULL request pointer");
return HRESULT(-1);
}
if response.is_null() {
util::message("ERROR: NULL response pointer");
return HRESULT(-1);
}
let req = &*request;
let transaction_id = format!("{:?}", req.transaction_id);
util::message(&format!("Transaction ID: {}", transaction_id));
util::message(&format!("Window Handle: {:?}", req.window_handle));
util::message(&format!("Request Signature Byte Count: {}", req.request_signature_byte_count));
util::message(&format!("Encoded Request Byte Count: {}", req.encoded_request_byte_count));
if req.encoded_request_byte_count == 0 || req.encoded_request_pointer.is_null() {
util::message("ERROR: No encoded request data provided");
return HRESULT(-1);
}
let encoded_request_slice = std::slice::from_raw_parts(
req.encoded_request_pointer,
req.encoded_request_byte_count as usize
);
util::message(&format!("Encoded request: {} bytes", encoded_request_slice.len()));
// Try to decode the request using Windows API
match decode_make_credential_request(encoded_request_slice) {
Ok(decoded_wrapper) => {
let decoded_request = decoded_wrapper.as_ref();
util::message("Successfully decoded make credential request using Windows API");
// Extract RP information
if decoded_request.pRpInformation.is_null() {
util::message("ERROR: RP information is null");
return HRESULT(-1);
}
let rp_info = &*decoded_request.pRpInformation;
let rpid = if rp_info.pwszId.is_null() {
util::message("ERROR: RP ID is null");
return HRESULT(-1);
} else {
match wstr_to_string(rp_info.pwszId) {
Ok(id) => id,
Err(e) => {
util::message(&format!("ERROR: Failed to decode RP ID: {}", e));
return HRESULT(-1);
}
}
};
let rp_name = if rp_info.pwszName.is_null() {
String::new()
} else {
wstr_to_string(rp_info.pwszName).unwrap_or_default()
};
// Extract user information
if decoded_request.pUserInformation.is_null() {
util::message("ERROR: User information is null");
return HRESULT(-1);
}
let user = &*decoded_request.pUserInformation;
let user_id = if user.pbId.is_null() || user.cbId == 0 {
util::message("ERROR: User ID is required for registration");
return HRESULT(-1);
} else {
let id_slice = std::slice::from_raw_parts(user.pbId, user.cbId as usize);
id_slice.to_vec()
};
let user_name = if user.pwszName.is_null() {
util::message("ERROR: User name is required for registration");
return HRESULT(-1);
} else {
match wstr_to_string(user.pwszName) {
Ok(name) => name,
Err(_) => {
util::message("ERROR: Failed to decode user name");
return HRESULT(-1);
}
}
};
let user_display_name = if user.pwszDisplayName.is_null() {
None
} else {
wstr_to_string(user.pwszDisplayName).ok()
};
let user_info = (user_id, user_name, user_display_name);
// Extract client data hash
let client_data_hash = if decoded_request.cbClientDataHash == 0 || decoded_request.pbClientDataHash.is_null() {
util::message("ERROR: Client data hash is required for registration");
return HRESULT(-1);
} else {
let hash_slice = std::slice::from_raw_parts(
decoded_request.pbClientDataHash,
decoded_request.cbClientDataHash as usize
);
hash_slice.to_vec()
};
// Extract RP ID raw bytes for authenticator data
let rpid_bytes = if decoded_request.cbRpId > 0 && !decoded_request.pbRpId.is_null() {
let rpid_slice = std::slice::from_raw_parts(
decoded_request.pbRpId,
decoded_request.cbRpId as usize
);
rpid_slice.to_vec()
} else {
rpid.as_bytes().to_vec()
};
// Extract supported algorithms
let supported_algorithms = if decoded_request.WebAuthNCredentialParameters.cCredentialParameters > 0 &&
!decoded_request.WebAuthNCredentialParameters.pCredentialParameters.is_null() {
let params_count = decoded_request.WebAuthNCredentialParameters.cCredentialParameters as usize;
let params_ptr = decoded_request.WebAuthNCredentialParameters.pCredentialParameters;
(0..params_count)
.map(|i| unsafe { &*params_ptr.add(i) }.lAlg)
.collect()
} else {
Vec::new()
};
// Create request context from properly decoded data
let mut request_context = RequestContext::default();
request_context.rpid = Some(rpid.clone());
request_context.user_id = Some(user_info.0);
request_context.user_name = Some(user_info.1);
request_context.user_display_name = user_info.2;
request_context.client_data_hash = Some(client_data_hash);
request_context.supported_algorithms = supported_algorithms;
util::message(&format!("Make credential request - RP: {}, User: {}",
rpid,
request_context.user_name.as_deref().unwrap_or("unknown")));
// Send registration request
if let Some(passkey_response) = send_registration_request(&rpid, &transaction_id, &request_context) {
util::message(&format!("Registration response received: {:?}", passkey_response));
// Create proper WebAuthn response from passkey_response
match passkey_response {
PasskeyResponse::RegistrationResponse { credential_id, attestation_object } => {
util::message("Creating WebAuthn make credential response");
match create_make_credential_response(credential_id, attestation_object) {
Ok(webauthn_response) => {
util::message("Successfully created WebAuthn response");
*response = webauthn_response;
HRESULT(0)
},
Err(e) => {
util::message(&format!("ERROR: Failed to create WebAuthn response: {}", e));
*response = ptr::null_mut();
HRESULT(-1)
}
}
},
PasskeyResponse::Error { message } => {
util::message(&format!("Registration request failed: {}", message));
*response = ptr::null_mut();
HRESULT(-1)
},
_ => {
util::message("ERROR: Unexpected response type for registration request");
*response = ptr::null_mut();
HRESULT(-1)
}
}
} else {
util::message("ERROR: No response from registration request");
*response = ptr::null_mut();
HRESULT(-1)
}
},
Err(e) => {
util::message(&format!("ERROR: Failed to decode make credential request: {}", e));
*response = ptr::null_mut();
HRESULT(-1)
}
}
}
unsafe fn EXPERIMENTAL_PluginGetAssertion(
&self,
request: *const ExperimentalWebAuthnPluginOperationRequest,
response: *mut *mut ExperimentalWebAuthnPluginOperationResponse,
) -> HRESULT {
util::message("EXPERIMENTAL_PluginGetAssertion() called");
// Validate input parameters
if request.is_null() || response.is_null() {
util::message("Invalid parameters passed to EXPERIMENTAL_PluginGetAssertion");
return HRESULT(-1);
}
let req = &*request;
let transaction_id = format!("{:?}", req.transaction_id);
util::message(&format!("Get assertion request - Transaction: {}", transaction_id));
if req.encoded_request_byte_count == 0 || req.encoded_request_pointer.is_null() {
util::message("ERROR: No encoded request data provided");
*response = ptr::null_mut();
return HRESULT(-1);
}
let encoded_request_slice = std::slice::from_raw_parts(
req.encoded_request_pointer,
req.encoded_request_byte_count as usize
);
// Try to decode the request using Windows API
match decode_get_assertion_request(encoded_request_slice) {
Ok(decoded_wrapper) => {
let decoded_request = decoded_wrapper.as_ref();
util::message("Successfully decoded get assertion request using Windows API");
// Extract RP information
let rpid = if decoded_request.pwszRpId.is_null() {
util::message("ERROR: RP ID is null");
*response = ptr::null_mut();
return HRESULT(-1);
} else {
match wstr_to_string(decoded_request.pwszRpId) {
Ok(id) => id,
Err(e) => {
util::message(&format!("ERROR: Failed to decode RP ID: {}", e));
*response = ptr::null_mut();
return HRESULT(-1);
}
}
};
// Extract client data hash
let client_data_hash = if decoded_request.cbClientDataHash == 0 || decoded_request.pbClientDataHash.is_null() {
util::message("ERROR: Client data hash is required for assertion");
*response = ptr::null_mut();
return HRESULT(-1);
} else {
let hash_slice = std::slice::from_raw_parts(
decoded_request.pbClientDataHash,
decoded_request.cbClientDataHash as usize
);
hash_slice.to_vec()
};
// Create request context from properly decoded data
let mut request_context = RequestContext::default();
request_context.rpid = Some(rpid.clone());
request_context.client_data_hash = Some(client_data_hash);
// TODO: Extract allowed credentials from CredentialList if available
util::message(&format!("Get assertion request - RP: {}", rpid));
// Send assertion request
if let Some(passkey_response) = send_assertion_request(&rpid, &transaction_id, &request_context) {
util::message(&format!("Assertion response received: {:?}", passkey_response));
// Create proper WebAuthn response from passkey_response
match passkey_response {
PasskeyResponse::AssertionResponse { credential_id, authenticator_data, signature, user_handle } => {
util::message("Creating WebAuthn get assertion response");
match create_get_assertion_response(credential_id, authenticator_data, signature, user_handle) {
Ok(webauthn_response) => {
util::message("Successfully created WebAuthn assertion response");
*response = webauthn_response;
HRESULT(0)
},
Err(e) => {
util::message(&format!("ERROR: Failed to create WebAuthn assertion response: {}", e));
*response = ptr::null_mut();
HRESULT(-1)
}
}
},
PasskeyResponse::Error { message } => {
util::message(&format!("Assertion request failed: {}", message));
*response = ptr::null_mut();
HRESULT(-1)
},
_ => {
util::message("ERROR: Unexpected response type for assertion request");
*response = ptr::null_mut();
HRESULT(-1)
}
}
} else {
util::message("ERROR: No response from assertion request");
*response = ptr::null_mut();
HRESULT(-1)
}
},
Err(e) => {
util::message(&format!("ERROR: Failed to decode get assertion request: {}", e));
*response = ptr::null_mut();
HRESULT(-1)
}
}
}
unsafe fn EXPERIMENTAL_PluginCancelOperation(
&self,
request: *const ExperimentalWebAuthnPluginCancelOperationRequest,
) -> HRESULT {
util::message("EXPERIMENTAL_PluginCancelOperation() called");
HRESULT(0)
}
}
impl IClassFactory_Impl for Factory_Impl {
fn CreateInstance(
&self,
outer: windows_core::Ref<IUnknown>,
iid: *const windows_core::GUID,
object: *mut *mut core::ffi::c_void,
) -> windows_core::Result<()> {
let unknown: IInspectable = PluginAuthenticatorComObject.into(); // TODO: IUnknown ?
unsafe { unknown.query(iid, object).ok() }
}
fn LockServer(&self, lock: windows_core::BOOL) -> windows_core::Result<()> {
Ok(())
}
}

View File

@@ -0,0 +1,212 @@
use std::ffi::OsString;
use std::os::windows::ffi::OsStrExt;
use std::ffi::c_uchar;
use std::ptr;
use windows::Win32::Foundation::*;
use windows::Win32::System::Com::*;
use windows_core::{s, HRESULT, PCWSTR, ComObjectInterface, GUID, HSTRING};
use crate::utils::{WindowsString, delay_load, message};
use crate::webauthn::*;
use crate::com_provider;
use hex;
const AUTHENTICATOR_NAME: &str = "Bitwarden Desktop Authenticator";
const CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062";
const RPID: &str = "bitwarden.com";
/// Initializes the COM library for use on the calling thread,
/// and registers + sets the security values.
pub fn initialize_com_library() -> std::result::Result<(), String> {
let result = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
if result.is_err() {
return Err(format!(
"Error: couldn't initialize the COM library\n{}",
result.message()
));
}
match unsafe {
CoInitializeSecurity(
None,
-1,
None,
None,
RPC_C_AUTHN_LEVEL_DEFAULT,
RPC_C_IMP_LEVEL_IMPERSONATE,
None,
EOAC_NONE,
None,
)
} {
Ok(_) => Ok(()),
Err(e) => Err(format!(
"Error: couldn't initialize COM security\n{}",
e.message()
)),
}
}
/// Registers the Bitwarden Plugin Authenticator COM library with Windows.
pub fn register_com_library() -> std::result::Result<(), String> {
static FACTORY: windows_core::StaticComObject<com_provider::Factory> =
com_provider::Factory.into_static();
//let clsid: *const GUID = &GUID::from_u128(0xa98925d161f640de9327dc418fcb2ff4);
let clsid: *const GUID = &GUID::from_u128(0x0f7dc5d969ce465285726877fd695062);
match unsafe {
CoRegisterClassObject(
clsid,
FACTORY.as_interface_ref(),
//FACTORY.as_interface::<pluginauthenticator::EXPERIMENTAL_IPluginAuthenticator>(),
CLSCTX_LOCAL_SERVER,
REGCLS_MULTIPLEUSE,
)
} {
Ok(_) => Ok(()),
Err(e) => Err(format!(
"Error: couldn't register the COM library\n{}",
e.message()
)),
}
}
// testing wide encoding
pub fn add_authenticator_using_wide_encoding() -> std::result::Result<(), String> {
// let (authenticator_name_pointer, authenticator_name_bytes) = String::from(AUTHENTICATOR_NAME).into_win_utf16_wide();
let mut authenticator_name: Vec<u16> = OsString::from(AUTHENTICATOR_NAME).encode_wide().collect();
//authenticator_name.push(0);
let authenticator_name_pointer = authenticator_name.as_mut_ptr();
// let (clsid_pointer, clsid_bytes) = String::from(CLSID).into_win_utf16_wide();
let mut clsid: Vec<u16> = OsString::from(CLSID).encode_wide().collect();
//clsid.push(0);
let clsid_pointer = clsid.as_mut_ptr();
// let (rpid_pointer, rpid_bytes) = String::from(RPID).into_win_utf16_wide();
let mut rpid: Vec<u16> = OsString::from(RPID).encode_wide().collect();
//rpid.push(0);
let rpid_pointer = rpid.as_mut_ptr();
// Example authenticator info blob
let cbor_authenticator_info = "A60182684649444F5F325F30684649444F5F325F310282637072666B686D61632D7365637265740350D548826E79B4DB40A3D811116F7E834904A362726BF5627570F5627576F5098168696E7465726E616C0A81A263616C672664747970656A7075626C69632D6B6579";
let mut authenticator_info_bytes = hex::decode(cbor_authenticator_info).unwrap();
let add_authenticator_options = ExperimentalWebAuthnPluginAddAuthenticatorOptions {
authenticator_name: authenticator_name_pointer,
plugin_clsid: clsid_pointer,
rpid: rpid_pointer,
light_theme_logo: ptr::null(),
dark_theme_logo: ptr::null(),
cbor_authenticator_info_byte_count: authenticator_info_bytes.len() as u32,
cbor_authenticator_info: authenticator_info_bytes.as_mut_ptr(),
};
let plugin_signing_public_key_byte_count: u32 = 0;
let mut plugin_signing_public_key: c_uchar = 0;
let plugin_signing_public_key_ptr = &mut plugin_signing_public_key;
let mut add_response = ExperimentalWebAuthnPluginAddAuthenticatorResponse {
plugin_operation_signing_key_byte_count: plugin_signing_public_key_byte_count,
plugin_operation_signing_key: plugin_signing_public_key_ptr,
};
let mut add_response_ptr: *mut ExperimentalWebAuthnPluginAddAuthenticatorResponse =
&mut add_response;
let result = unsafe {
delay_load::<EXPERIMENTAL_WebAuthNPluginAddAuthenticatorFnDeclaration>(
s!("webauthn.dll"),
s!("EXPERIMENTAL_WebAuthNPluginAddAuthenticator"),
)
};
match result {
Some(api) => {
let result = unsafe { api(&add_authenticator_options, &mut add_response_ptr) };
if result.is_err() {
return Err(format!(
"Error: Error response from EXPERIMENTAL_WebAuthNPluginAddAuthenticator()\n{}",
result.message()
));
}
Ok(())
},
None => {
Err(String::from("Error: Can't complete add_authenticator(), as the function EXPERIMENTAL_WebAuthNPluginAddAuthenticator can't be found."))
}
}
}
/// Adds Bitwarden as a plugin authenticator.
pub fn add_authenticator() -> std::result::Result<(), String> {
let authenticator_name: HSTRING = AUTHENTICATOR_NAME.into();
let authenticator_name_ptr = PCWSTR(authenticator_name.as_ptr()).as_ptr();
let clsid: HSTRING = format!("{{{}}}", CLSID).into();
let clsid_ptr = PCWSTR(clsid.as_ptr()).as_ptr();
let relying_party_id: HSTRING = RPID.into();
let relying_party_id_ptr = PCWSTR(relying_party_id.as_ptr()).as_ptr();
// let aaguid: HSTRING = format!("{{{}}}", AAGUID).into();
// let aaguid_ptr = PCWSTR(aaguid.as_ptr()).as_ptr();
// Example authenticator info blob
let cbor_authenticator_info = "A60182684649444F5F325F30684649444F5F325F310282637072666B686D61632D7365637265740350D548826E79B4DB40A3D811116F7E834904A362726BF5627570F5627576F5098168696E7465726E616C0A81A263616C672664747970656A7075626C69632D6B6579";
let mut authenticator_info_bytes = hex::decode(cbor_authenticator_info).unwrap();
let add_authenticator_options = ExperimentalWebAuthnPluginAddAuthenticatorOptions {
authenticator_name: authenticator_name_ptr,
plugin_clsid: clsid_ptr,
rpid: relying_party_id_ptr,
light_theme_logo: ptr::null(),
dark_theme_logo: ptr::null(),
cbor_authenticator_info_byte_count: authenticator_info_bytes.len() as u32,
cbor_authenticator_info: authenticator_info_bytes.as_mut_ptr(),
};
let plugin_signing_public_key_byte_count: u32 = 0;
let mut plugin_signing_public_key: c_uchar = 0;
let plugin_signing_public_key_ptr = &mut plugin_signing_public_key;
let mut add_response = ExperimentalWebAuthnPluginAddAuthenticatorResponse {
plugin_operation_signing_key_byte_count: plugin_signing_public_key_byte_count,
plugin_operation_signing_key: plugin_signing_public_key_ptr,
};
let mut add_response_ptr: *mut ExperimentalWebAuthnPluginAddAuthenticatorResponse =
&mut add_response;
let result = unsafe {
delay_load::<EXPERIMENTAL_WebAuthNPluginAddAuthenticatorFnDeclaration>(
s!("webauthn.dll"),
s!("EXPERIMENTAL_WebAuthNPluginAddAuthenticator"),
)
};
match result {
Some(api) => {
let result = unsafe { api(&add_authenticator_options, &mut add_response_ptr) };
if result.is_err() {
return Err(format!(
"Error: Error response from EXPERIMENTAL_WebAuthNPluginAddAuthenticator()\n{}",
result.message()
));
}
Ok(())
},
None => {
Err(String::from("Error: Can't complete add_authenticator(), as the function EXPERIMENTAL_WebAuthNPluginAddAuthenticator can't be found."))
}
}
}
type EXPERIMENTAL_WebAuthNPluginAddAuthenticatorFnDeclaration = unsafe extern "cdecl" fn(
pPluginAddAuthenticatorOptions: *const ExperimentalWebAuthnPluginAddAuthenticatorOptions,
ppPluginAddAuthenticatorResponse: *mut *mut ExperimentalWebAuthnPluginAddAuthenticatorResponse,
) -> HRESULT;

View File

@@ -0,0 +1,65 @@
use tokio::sync::{mpsc, oneshot};
use std::sync::Mutex;
use crate::types::*;
use crate::utils::{self as util};
/// Global channel sender for request notifications
static REQUEST_SENDER: Mutex<Option<mpsc::UnboundedSender<RequestEvent>>> = Mutex::new(None);
/// Sets the channel sender for request notifications
pub fn set_request_sender(sender: mpsc::UnboundedSender<RequestEvent>) {
match REQUEST_SENDER.lock() {
Ok(mut tx) => {
*tx = Some(sender);
util::message("Passkey request callback registered");
},
Err(e) => {
util::message(&format!("Failed to register passkey callback: {:?}", e));
}
}
}
/// Sends a passkey request and waits for response
pub fn send_passkey_request(request_type: RequestType, request_json: String, rpid: &str) -> Option<PasskeyResponse> {
let request_desc = match &request_type {
RequestType::Assertion => format!("assertion request for {}", rpid),
RequestType::Registration => format!("registration request for {}", rpid),
RequestType::Sync => format!("sync request for {}", rpid),
};
util::message(&format!("Passkey {}", request_desc));
if let Ok(tx_guard) = REQUEST_SENDER.lock() {
if let Some(sender) = tx_guard.as_ref() {
let (response_tx, response_rx) = oneshot::channel();
let event = RequestEvent {
request_type,
request_json,
response_sender: response_tx,
};
if let Ok(()) = sender.send(event) {
// Wait for response from TypeScript callback
match response_rx.blocking_recv() {
Ok(response) => {
util::message(&format!("Received callback response {:?}", response));
Some(response)
},
Err(_) => {
util::message("No response from callback");
None
}
}
} else {
util::message("Failed to send event to callback");
None
}
} else {
util::message("No callback registered for passkey requests");
None
}
} else {
None
}
}

View File

@@ -2,23 +2,32 @@
#![allow(non_snake_case)]
#![allow(non_camel_case_types)]
use std::ffi::OsString;
use std::ffi::{c_uchar, c_ulong, OsString};
use std::os::windows::ffi::OsStrExt;
use std::ffi::c_uchar;
use std::ffi::c_ulong;
use std::ptr;
use std::{thread, time::Duration};
use util::WindowsString;
use webauthn::*;
use windows::Win32::Foundation::*;
use windows::Win32::System::Com::*;
use windows::Win32::System::LibraryLoader::*;
use windows_core::*;
mod pluginauthenticator;
mod util;
// New modular structure
mod assert;
mod make_credential;
mod sync;
mod ipc;
mod com_registration;
pub mod utils;
mod types;
mod webauthn;
mod com_provider;
mod com_buffer;
// Re-export main functionality
pub use sync::{send_sync_request, sync_credentials_to_windows, get_credentials_from_windows};
pub use ipc::{set_request_sender, send_passkey_request};
pub use types::{PasskeyRequest, PasskeyResponse, SyncedCredential, RequestEvent, RequestType};
pub use com_registration::{initialize_com_library, register_com_library, add_authenticator};
// Re-export utilities
pub use utils as util;
const AUTHENTICATOR_NAME: &str = "Bitwarden Desktop Authenticator";
//const AAGUID: &str = "d548826e-79b4-db40-a3d8-11116f7e8349";
@@ -29,518 +38,55 @@ const RPID: &str = "bitwarden.com";
/// plugin authenticator with Windows.
/// For now, also adds the authenticator
pub fn register() -> std::result::Result<(), String> {
util::message(String::from("register() called"));
util::message("register() called");
util::message(String::from("About to call initialize_com_library()"));
let r = initialize_com_library();
util::message(format!("initialized the com library: {:?}", r));
util::message("About to call initialize_com_library()");
let r = com_registration::initialize_com_library();
util::message(&format!("initialized the com library: {:?}", r));
util::message(String::from("About to call register_com_library()"));
let r = register_com_library();
util::message(format!("registered the com library: {:?}", r));
util::message("About to call register_com_library()");
let r = com_registration::register_com_library();
util::message(&format!("registered the com library: {:?}", r));
util::message(String::from("About to call add_authenticator()"));
let r = add_authenticator();
util::message("About to call add_authenticator()");
let r = com_registration::add_authenticator();
//let r = add_authenticator_using_wide_encoding();
util::message(format!("added the authenticator: {:?}", r));
util::message(&format!("added the authenticator: {:?}", r));
util::message(String::from("sleeping for 20 seconds..."));
thread::sleep(Duration::from_millis(20000));
util::message(String::from("sleeping done"));
util::message("sleeping for 5 seconds...");
thread::sleep(Duration::from_millis(5000));
util::message("sleeping done");
// // ---------------------------------------
// // ----- *** add test credential *** -----
// // ----- using encode_utf16 -----
// // ---------------------------------------
// // Style 1, currently used: mem::forget
// let mut credential_id_string = String::from("32");
// let credential_id_byte_count = credential_id_string.as_bytes().len() as c_ulong;
// let credential_id_pointer: *mut c_uchar = credential_id_string.as_mut_ptr();
// std::mem::forget(credential_id_string);
// // Style 2, experimental: Box::leak
// // Additionally, might need to Pin (same for style 1)
// //
// // let credential_id_string = String::from("32");
// // let credential_id_byte_count = credential_id_string.as_bytes().len() as c_ulong;
// // let credential_id_box = Box::new(credential_id_string);
// // let credential_id_pointer: *mut c_uchar = credential_id_box.leak().as_mut_ptr();
// let mut rpid_string = String::from("webauthn.io");
// let mut rpid_vec: Vec<u16> = rpid_string.encode_utf16().collect();
// rpid_vec.push(0);
// let rpid: *mut u16 = rpid_vec.as_mut_ptr();
// std::mem::forget(rpid_string);
// std::mem::forget(rpid_vec);
// let mut rp_friendly_name_string = String::from("WebAuthn Website");
// let mut rp_friendly_name_vec: Vec<u16> = rp_friendly_name_string.encode_utf16().collect();
// rp_friendly_name_vec.push(0);
// let rp_friendly_name: *mut u16 = rp_friendly_name_vec.as_mut_ptr();
// std::mem::forget(rp_friendly_name_string);
// std::mem::forget(rp_friendly_name_vec);
// let mut user_id_string = String::from("14");
// let user_id_byte_count = user_id_string.as_bytes().len() as c_ulong;
// let user_id_pointer: *mut c_uchar = user_id_string.as_mut_ptr();
// std::mem::forget(user_id_string);
// let mut user_name_string = String::from("webauthn.io username");
// let mut user_name_vec: Vec<u16> = user_name_string.encode_utf16().collect();
// user_name_vec.push(0);
// let user_name: *mut u16 = user_name_vec.as_mut_ptr();
// std::mem::forget(user_name_string);
// std::mem::forget(user_name_vec);
// let mut user_display_name_string = String::from("webauthn.io display name");
// let mut user_display_name_vec: Vec<u16> = user_display_name_string.encode_utf16().collect();
// user_display_name_vec.push(0);
// let user_display_name: *mut u16 = user_display_name_vec.as_mut_ptr();
// std::mem::forget(user_display_name_string);
// std::mem::forget(user_display_name_vec);
// let mut credential_details = ExperimentalWebAuthnPluginCredentialDetails {
// credential_id_byte_count,
// credential_id_pointer,
// rpid,
// rp_friendly_name,
// user_id_byte_count,
// user_id_pointer,
// user_name,
// user_display_name,
// };
// let credential_details_ptr: *mut ExperimentalWebAuthnPluginCredentialDetails =
// &mut credential_details;
// std::mem::forget(credential_details);
// let mut clsid_string = String::from(format!("{{{}}}", CLSID));
// let mut clsid_vec: Vec<u16> = clsid_string.encode_utf16().collect();
// clsid_vec.push(0);
// let plugin_clsid: *mut u16 = clsid_vec.as_mut_ptr();
// std::mem::forget(clsid_string);
// std::mem::forget(clsid_vec);
// let mut credentials: Vec<*mut ExperimentalWebAuthnPluginCredentialDetails> =
// vec![credential_details_ptr];
// let credential_count: c_ulong = credentials.len() as c_ulong;
// let credentials_ptr: *mut *mut ExperimentalWebAuthnPluginCredentialDetails =
// credentials.as_mut_ptr();
// std::mem::forget(credentials);
// let mut credentials_details_list = ExperimentalWebAuthnPluginCredentialDetailsList {
// plugin_clsid,
// credential_count,
// credentials: credentials_ptr,
// };
// let credentials_details_list_ptr: *mut ExperimentalWebAuthnPluginCredentialDetailsList =
// &mut credentials_details_list;
// std::mem::forget(credentials_details_list);
// util::message(format!("about to link the fn pointer for add credentials"));
// let result = unsafe {
// delay_load::<EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentialsFnDeclaration>(
// s!("webauthn.dll"),
// s!("EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials"),
// )
// };
// util::message(format!("about to call add credentials"));
// let result = match result {
// Some(api) => {
// let result = unsafe { api(credentials_details_list_ptr) };
// if result.is_err() {
// return Err(format!(
// "Error: Error response from EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials()\n{}",
// result.message()
// ));
// }
// Ok(())
// },
// None => {
// Err(String::from("Error: Can't complete add_credentials(), as the function EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials can't be loaded."))
// }
// };
// util::message(format!("add credentials attempt: {:?}", result));
// --------------------------------------------------------------------------------------------
// std::mem::forget(credential_id);
// let mut test_credential = ExperimentalWebAuthnPluginCredentialDetails::create(
// String::from("32"),
// String::from("webauthn.io"),
// String::from("WebAuthn Website"),
// String::from("14"),
// String::from("web user name"),
// String::from("web user display name"),
// );
// let test_credential_ptr: *mut ExperimentalWebAuthnPluginCredentialDetails = &mut test_credential;
// //std::mem::forget(test_credential);
// let mut test_credential_list: Vec<*mut ExperimentalWebAuthnPluginCredentialDetails> = vec![test_credential_ptr];
// let test_credential_list_ptr: *mut *mut ExperimentalWebAuthnPluginCredentialDetails = test_credential_list.as_mut_ptr();
// let pluginclsid = String::from(CLSID).into_win_utf16().0;
// let credentials = ExperimentalWebAuthnPluginCredentialDetailsList {
// plugin_clsid: pluginclsid,
// credential_count: 1,
// credentials: test_credential_list_ptr,
// };
// let r = add_credentials(credentials);
// util::message(format!("added the credentials: {:?}", r));
// --------------------------------------------------------------------------------------------
// ---------------------------------------
// ----- *** add test credential *** -----
// ----- using encode_wide -----
// ---------------------------------------
// Style 1, currently used: mem::forget
let mut credential_id_string = String::from("32");
let credential_id_byte_count = credential_id_string.as_bytes().len() as c_ulong;
let credential_id_pointer: *mut c_uchar = credential_id_string.as_mut_ptr();
std::mem::forget(credential_id_string);
let rpid_string = String::from("webauthn.io");
let mut rpid_vec: Vec<u16> = OsString::from(rpid_string).encode_wide().collect();
rpid_vec.push(0);
let rpid: *mut u16 = rpid_vec.as_mut_ptr();
std::mem::forget(rpid_vec);
let rp_friendly_name_string = String::from("WebAuthn Websitewide ");
let mut rp_friendly_name_vec: Vec<u16> = OsString::from(rp_friendly_name_string).encode_wide().collect();
rp_friendly_name_vec.push(0);
let rp_friendly_name: *mut u16 = rp_friendly_name_vec.as_mut_ptr();
std::mem::forget(rp_friendly_name_vec);
let mut user_id_string = String::from("14");
let user_id_byte_count = user_id_string.as_bytes().len() as c_ulong;
let user_id_pointer: *mut c_uchar = user_id_string.as_mut_ptr();
std::mem::forget(user_id_string);
let user_name_string = String::from("webauthn.io wide username");
let mut user_name_vec: Vec<u16> = OsString::from(user_name_string).encode_wide().collect();
user_name_vec.push(0);
let user_name: *mut u16 = user_name_vec.as_mut_ptr();
std::mem::forget(user_name_vec);
let user_display_name_string = String::from("webauthn.io wide display name");
let mut user_display_name_vec: Vec<u16> = OsString::from(user_display_name_string).encode_wide().collect();
user_display_name_vec.push(0);
let user_display_name: *mut u16 = user_display_name_vec.as_mut_ptr();
std::mem::forget(user_display_name_vec);
let mut credential_details = ExperimentalWebAuthnPluginCredentialDetails {
credential_id_byte_count,
credential_id_pointer,
rpid,
rp_friendly_name,
user_id_byte_count,
user_id_pointer,
user_name,
user_display_name,
};
let credential_details_ptr: *mut ExperimentalWebAuthnPluginCredentialDetails =
&mut credential_details;
std::mem::forget(credential_details);
let clsid_string = String::from(format!("{{{}}}", CLSID));
let mut clsid_vec: Vec<u16> = OsString::from(clsid_string).encode_wide().collect();
clsid_vec.push(0);
let plugin_clsid: *mut u16 = clsid_vec.as_mut_ptr();
std::mem::forget(clsid_vec);
let mut credentials: Vec<*mut ExperimentalWebAuthnPluginCredentialDetails> =
vec![credential_details_ptr];
let credential_count: c_ulong = credentials.len() as c_ulong;
let credentials_ptr: *mut *mut ExperimentalWebAuthnPluginCredentialDetails =
credentials.as_mut_ptr();
std::mem::forget(credentials);
let mut credentials_details_list = ExperimentalWebAuthnPluginCredentialDetailsList {
plugin_clsid,
credential_count,
credentials: credentials_ptr,
};
let credentials_details_list_ptr: *mut ExperimentalWebAuthnPluginCredentialDetailsList =
&mut credentials_details_list;
std::mem::forget(credentials_details_list);
util::message(format!("about to link the fn pointer for add credentials"));
let result = unsafe {
delay_load::<EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentialsFnDeclaration>(
s!("webauthn.dll"),
s!("EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials"),
)
};
util::message(format!("about to call add credentials"));
let result = match result {
Some(api) => {
let result = unsafe { api(credentials_details_list_ptr) };
if result.is_err() {
return Err(format!(
"Error: Error response from EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials()\n{}",
result.message()
));
}
Ok(())
},
None => {
Err(String::from("Error: Can't complete add_credentials(), as the function EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials can't be loaded."))
}
};
util::message(format!("add credentials attempt: {:?}", result));
// --------------------------------------------------------------------------------------------
let r = syncCredentials();
util::message(&format!("sync credentials: {:?}", r));
if let Err(e) = r {
util::message(&format!("syncCredentials failed: {}", e));
}
Ok(())
}
// -----
#[repr(C)]
pub struct ExperimentalWebAuthnPluginCredentialDetails {
pub credential_id_byte_count: c_ulong, // DWORD cbCredentialId
pub credential_id_pointer: *mut c_uchar, // PBYTE pbCredentialId
pub rpid: *mut u16, // PWSTR pwszRpId
pub rp_friendly_name: *mut u16, // PWSTR pwszRpName
pub user_id_byte_count: u32, // DWORD cbUserId
pub user_id_pointer: *mut c_uchar, // PBYTE pbUserId
pub user_name: *mut u16, // PWSTR pwszUserName
pub user_display_name: *mut u16, // PWSTR pwszUserDisplayName
}
#[repr(C)]
pub struct ExperimentalWebAuthnPluginCredentialDetailsList {
pub plugin_clsid: *mut u16, // PWSTR pwszPluginClsId
pub credential_count: c_ulong, // DWORD cCredentialDetails
pub credentials: *mut *mut ExperimentalWebAuthnPluginCredentialDetails, // CredentialDetailsPtr *pCredentialDetails
}
type EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentialsFnDeclaration =
unsafe extern "cdecl" fn(
pCredentialDetailsList: *mut ExperimentalWebAuthnPluginCredentialDetailsList,
) -> HRESULT;
// -----
/// Initializes the COM library for use on the calling thread,
/// and registers + sets the security values.
fn initialize_com_library() -> std::result::Result<(), String> {
let result = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
if result.is_err() {
return Err(format!(
"Error: couldn't initialize the COM library\n{}",
result.message()
));
}
match unsafe {
CoInitializeSecurity(
None,
-1,
None,
None,
RPC_C_AUTHN_LEVEL_DEFAULT,
RPC_C_IMP_LEVEL_IMPERSONATE,
None,
EOAC_NONE,
None,
)
} {
Ok(_) => Ok(()),
Err(e) => Err(format!(
"Error: couldn't initialize COM security\n{}",
e.message()
)),
}
fn syncCredentials() -> std::result::Result<(), String> {
// Create a test credential using the new sync module with more realistic data
let test_credential = types::SyncedCredential {
credential_id: vec![
0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08
], // 32 byte credential ID
rp_id: "webauthn.io".to_string(),
user_name: "testuser".to_string(),
user_id: vec![0x75, 0x73, 0x65, 0x72, 0x31, 0x32, 0x33, 0x34], // "user1234" as bytes
};
let credentials = vec![test_credential];
// Use the sync module to sync credentials
sync_credentials_to_windows(credentials, CLSID)
}
/// Registers the Bitwarden Plugin Authenticator COM library with Windows.
fn register_com_library() -> std::result::Result<(), String> {
static FACTORY: windows_core::StaticComObject<pluginauthenticator::Factory> =
pluginauthenticator::Factory.into_static();
//let clsid: *const GUID = &GUID::from_u128(0xa98925d161f640de9327dc418fcb2ff4);
let clsid: *const GUID = &GUID::from_u128(0x0f7dc5d969ce465285726877fd695062);
match unsafe {
CoRegisterClassObject(
clsid,
FACTORY.as_interface_ref(),
//FACTORY.as_interface::<pluginauthenticator::EXPERIMENTAL_IPluginAuthenticator>(),
CLSCTX_LOCAL_SERVER,
REGCLS_MULTIPLEUSE,
)
} {
Ok(_) => Ok(()),
Err(e) => Err(format!(
"Error: couldn't register the COM library\n{}",
e.message()
)),
}
}
// testing wide encoding
fn add_authenticator_using_wide_encoding() -> std::result::Result<(), String> {
// let (authenticator_name_pointer, authenticator_name_bytes) = String::from(AUTHENTICATOR_NAME).into_win_utf16_wide();
let mut authenticator_name: Vec<u16> = OsString::from(AUTHENTICATOR_NAME).encode_wide().collect();
//authenticator_name.push(0);
let authenticator_name_pointer = authenticator_name.as_mut_ptr();
// let (clsid_pointer, clsid_bytes) = String::from(CLSID).into_win_utf16_wide();
let mut clsid: Vec<u16> = OsString::from(CLSID).encode_wide().collect();
//clsid.push(0);
let clsid_pointer = clsid.as_mut_ptr();
// let (rpid_pointer, rpid_bytes) = String::from(RPID).into_win_utf16_wide();
let mut rpid: Vec<u16> = OsString::from(RPID).encode_wide().collect();
//rpid.push(0);
let rpid_pointer = rpid.as_mut_ptr();
// Example authenticator info blob
let cbor_authenticator_info = "A60182684649444F5F325F30684649444F5F325F310282637072666B686D61632D7365637265740350D548826E79B4DB40A3D811116F7E834904A362726BF5627570F5627576F5098168696E7465726E616C0A81A263616C672664747970656A7075626C69632D6B6579";
let mut authenticator_info_bytes = hex::decode(cbor_authenticator_info).unwrap();
let add_authenticator_options = webauthn::ExperimentalWebAuthnPluginAddAuthenticatorOptions {
authenticator_name: authenticator_name_pointer,
plugin_clsid: clsid_pointer,
rpid: rpid_pointer,
light_theme_logo: ptr::null(),
dark_theme_logo: ptr::null(),
cbor_authenticator_info_byte_count: authenticator_info_bytes.len() as u32,
cbor_authenticator_info: authenticator_info_bytes.as_mut_ptr(),
};
let plugin_signing_public_key_byte_count: u32 = 0;
let mut plugin_signing_public_key: c_uchar = 0;
let plugin_signing_public_key_ptr = &mut plugin_signing_public_key;
let mut add_response = webauthn::ExperimentalWebAuthnPluginAddAuthenticatorResponse {
plugin_operation_signing_key_byte_count: plugin_signing_public_key_byte_count,
plugin_operation_signing_key: plugin_signing_public_key_ptr,
};
let mut add_response_ptr: *mut webauthn::ExperimentalWebAuthnPluginAddAuthenticatorResponse =
&mut add_response;
let result = unsafe {
delay_load::<EXPERIMENTAL_WebAuthNPluginAddAuthenticatorFnDeclaration>(
s!("webauthn.dll"),
s!("EXPERIMENTAL_WebAuthNPluginAddAuthenticator"),
)
};
match result {
Some(api) => {
let result = unsafe { api(&add_authenticator_options, &mut add_response_ptr) };
if result.is_err() {
return Err(format!(
"Error: Error response from EXPERIMENTAL_WebAuthNPluginAddAuthenticator()\n{}",
result.message()
));
}
Ok(())
},
None => {
Err(String::from("Error: Can't complete add_authenticator(), as the function EXPERIMENTAL_WebAuthNPluginAddAuthenticator can't be found."))
}
}
}
/// Adds Bitwarden as a plugin authenticator.
fn add_authenticator() -> std::result::Result<(), String> {
let authenticator_name: HSTRING = AUTHENTICATOR_NAME.into();
let authenticator_name_ptr = PCWSTR(authenticator_name.as_ptr()).as_ptr();
let clsid: HSTRING = format!("{{{}}}", CLSID).into();
let clsid_ptr = PCWSTR(clsid.as_ptr()).as_ptr();
let relying_party_id: HSTRING = RPID.into();
let relying_party_id_ptr = PCWSTR(relying_party_id.as_ptr()).as_ptr();
// let aaguid: HSTRING = format!("{{{}}}", AAGUID).into();
// let aaguid_ptr = PCWSTR(aaguid.as_ptr()).as_ptr();
// Example authenticator info blob
let cbor_authenticator_info = "A60182684649444F5F325F30684649444F5F325F310282637072666B686D61632D7365637265740350D548826E79B4DB40A3D811116F7E834904A362726BF5627570F5627576F5098168696E7465726E616C0A81A263616C672664747970656A7075626C69632D6B6579";
let mut authenticator_info_bytes = hex::decode(cbor_authenticator_info).unwrap();
let add_authenticator_options = webauthn::ExperimentalWebAuthnPluginAddAuthenticatorOptions {
authenticator_name: authenticator_name_ptr,
plugin_clsid: clsid_ptr,
rpid: relying_party_id_ptr,
light_theme_logo: ptr::null(),
dark_theme_logo: ptr::null(),
cbor_authenticator_info_byte_count: authenticator_info_bytes.len() as u32,
cbor_authenticator_info: authenticator_info_bytes.as_mut_ptr(),
};
let plugin_signing_public_key_byte_count: u32 = 0;
let mut plugin_signing_public_key: c_uchar = 0;
let plugin_signing_public_key_ptr = &mut plugin_signing_public_key;
let mut add_response = webauthn::ExperimentalWebAuthnPluginAddAuthenticatorResponse {
plugin_operation_signing_key_byte_count: plugin_signing_public_key_byte_count,
plugin_operation_signing_key: plugin_signing_public_key_ptr,
};
let mut add_response_ptr: *mut webauthn::ExperimentalWebAuthnPluginAddAuthenticatorResponse =
&mut add_response;
let result = unsafe {
delay_load::<EXPERIMENTAL_WebAuthNPluginAddAuthenticatorFnDeclaration>(
s!("webauthn.dll"),
s!("EXPERIMENTAL_WebAuthNPluginAddAuthenticator"),
)
};
match result {
Some(api) => {
let result = unsafe { api(&add_authenticator_options, &mut add_response_ptr) };
if result.is_err() {
return Err(format!(
"Error: Error response from EXPERIMENTAL_WebAuthNPluginAddAuthenticator()\n{}",
result.message()
));
}
Ok(())
},
None => {
Err(String::from("Error: Can't complete add_authenticator(), as the function EXPERIMENTAL_WebAuthNPluginAddAuthenticator can't be found."))
}
}
}
type EXPERIMENTAL_WebAuthNPluginAddAuthenticatorFnDeclaration = unsafe extern "cdecl" fn(
pPluginAddAuthenticatorOptions: *const webauthn::ExperimentalWebAuthnPluginAddAuthenticatorOptions,
ppPluginAddAuthenticatorResponse: *mut *mut webauthn::ExperimentalWebAuthnPluginAddAuthenticatorResponse,
) -> HRESULT;
unsafe fn delay_load<T>(library: PCSTR, function: PCSTR) -> Option<T> {
let library = LoadLibraryExA(library, None, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
let Ok(library) = library else {
return None;
};
let address = GetProcAddress(library, function);
if address.is_some() {
return Some(std::mem::transmute_copy(&address));
}
_ = FreeLibrary(library);
None
}

View File

@@ -0,0 +1,253 @@
use std::alloc::{alloc, Layout};
use std::ptr;
use serde_json;
use windows_core::{HRESULT, s};
use crate::types::*;
use crate::utils::{self as util, delay_load};
use crate::com_provider::ExperimentalWebAuthnPluginOperationResponse;
use crate::assert::RequestContext;
// Windows API types for WebAuthn (from webauthn.h.sample)
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct WEBAUTHN_RP_ENTITY_INFORMATION {
pub dwVersion: u32,
pub pwszId: *const u16, // PCWSTR
pub pwszName: *const u16, // PCWSTR
pub pwszIcon: *const u16, // PCWSTR
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct WEBAUTHN_USER_ENTITY_INFORMATION {
pub dwVersion: u32,
pub cbId: u32, // DWORD
pub pbId: *const u8, // PBYTE
pub pwszName: *const u16, // PCWSTR
pub pwszIcon: *const u16, // PCWSTR
pub pwszDisplayName: *const u16, // PCWSTR
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct WEBAUTHN_COSE_CREDENTIAL_PARAMETER {
pub dwVersion: u32,
pub pwszCredentialType: *const u16, // LPCWSTR
pub lAlg: i32, // LONG - COSE algorithm identifier
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct WEBAUTHN_COSE_CREDENTIAL_PARAMETERS {
pub cCredentialParameters: u32,
pub pCredentialParameters: *const WEBAUTHN_COSE_CREDENTIAL_PARAMETER,
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct WEBAUTHN_CREDENTIAL_LIST {
pub cCredentials: u32,
pub pCredentials: *const u8, // Placeholder
}
// Make Credential Request structure (from sample header)
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct EXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST {
pub dwVersion: u32,
pub cbRpId: u32,
pub pbRpId: *const u8,
pub cbClientDataHash: u32,
pub pbClientDataHash: *const u8,
pub pRpInformation: *const WEBAUTHN_RP_ENTITY_INFORMATION,
pub pUserInformation: *const WEBAUTHN_USER_ENTITY_INFORMATION,
pub WebAuthNCredentialParameters: WEBAUTHN_COSE_CREDENTIAL_PARAMETERS, // Matches C++ sample
pub CredentialList: WEBAUTHN_CREDENTIAL_LIST,
pub cbCborExtensionsMap: u32,
pub pbCborExtensionsMap: *const u8,
// Add other fields as needed...
}
pub type PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST = *mut EXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST;
// Windows API function signatures
type EXPERIMENTAL_WebAuthNDecodeMakeCredentialRequestFn = unsafe extern "stdcall" fn(
cbEncoded: u32,
pbEncoded: *const u8,
ppMakeCredentialRequest: *mut PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST,
) -> HRESULT;
type EXPERIMENTAL_WebAuthNFreeDecodedMakeCredentialRequestFn = unsafe extern "stdcall" fn(
pMakeCredentialRequest: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST,
);
// RAII wrapper for decoded make credential request
pub struct DecodedMakeCredentialRequest {
ptr: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST,
free_fn: Option<EXPERIMENTAL_WebAuthNFreeDecodedMakeCredentialRequestFn>,
}
impl DecodedMakeCredentialRequest {
fn new(ptr: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST, free_fn: Option<EXPERIMENTAL_WebAuthNFreeDecodedMakeCredentialRequestFn>) -> Self {
Self { ptr, free_fn }
}
pub fn as_ref(&self) -> &EXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST {
unsafe { &*self.ptr }
}
}
impl Drop for DecodedMakeCredentialRequest {
fn drop(&mut self) {
if !self.ptr.is_null() {
if let Some(free_fn) = self.free_fn {
util::message("Freeing decoded make credential request");
unsafe { free_fn(self.ptr); }
}
}
}
}
// Function to decode make credential request using Windows API
pub unsafe fn decode_make_credential_request(encoded_request: &[u8]) -> Result<DecodedMakeCredentialRequest, String> {
util::message("Attempting to decode make credential request using Windows API");
// Try to load the Windows API decode function
let decode_fn = match delay_load::<EXPERIMENTAL_WebAuthNDecodeMakeCredentialRequestFn>(
s!("webauthn.dll"),
s!("EXPERIMENTAL_WebAuthNDecodeMakeCredentialRequest"),
) {
Some(func) => func,
None => {
return Err("Failed to load EXPERIMENTAL_WebAuthNDecodeMakeCredentialRequest from webauthn.dll".to_string());
}
};
// Try to load the free function (optional, might not be available in all versions)
let free_fn = delay_load::<EXPERIMENTAL_WebAuthNFreeDecodedMakeCredentialRequestFn>(
s!("webauthn.dll"),
s!("EXPERIMENTAL_WebAuthNFreeDecodedMakeCredentialRequest"),
);
// Prepare parameters for the API call
let cb_encoded = encoded_request.len() as u32;
let pb_encoded = encoded_request.as_ptr();
let mut pp_make_credential_request: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST = std::ptr::null_mut();
// Call the Windows API function
let result = decode_fn(
cb_encoded,
pb_encoded,
&mut pp_make_credential_request,
);
// Check if the call succeeded (following C++ THROW_IF_FAILED pattern)
if result.is_err() {
util::message(&format!("ERROR: EXPERIMENTAL_WebAuthNDecodeMakeCredentialRequest failed with HRESULT: 0x{:08x}", result.0));
return Err(format!("Windows API call failed with HRESULT: 0x{:08x}", result.0));
}
if pp_make_credential_request.is_null() {
util::message("ERROR: Windows API succeeded but returned null pointer");
return Err("Windows API returned null pointer".to_string());
}
Ok(DecodedMakeCredentialRequest::new(pp_make_credential_request, free_fn))
}
/// Helper for registration requests
pub fn send_registration_request(rpid: &str, transaction_id: &str, context: &RequestContext) -> Option<PasskeyResponse> {
// Validate required fields
if rpid.is_empty() {
util::message("ERROR: RP ID is required but empty");
return None;
}
// Extract user ID from context - this is required for registration
let user_id = match &context.user_id {
Some(id) if !id.is_empty() => id.clone(),
_ => {
util::message("ERROR: User ID is required for registration but not provided");
return None;
}
};
// Extract user name from context - this is required for registration
let user_name = match &context.user_name {
Some(name) if !name.is_empty() => name.clone(),
_ => {
util::message("ERROR: User name is required for registration but not provided");
return None;
}
};
// Extract client data hash from context - this is required for WebAuthn
let client_data_hash = match &context.client_data_hash {
Some(hash) if !hash.is_empty() => hash.clone(),
_ => {
util::message("ERROR: Client data hash is required for registration but not provided");
return None;
}
};
util::message(&format!("Registration request data - RP ID: {}, User ID: {} bytes, User name: {}, Client data hash: {} bytes, Algorithms: {:?}",
rpid, user_id.len(), user_name, client_data_hash.len(), context.supported_algorithms));
let request = PasskeyRegistrationRequest {
rp_id: rpid.to_string(),
transaction_id: transaction_id.to_string(),
user_id,
user_name,
client_data_hash,
user_verification: context.user_verification.unwrap_or(false),
supported_algorithms: context.supported_algorithms.clone(),
};
match serde_json::to_string(&request) {
Ok(request_json) => {
util::message(&format!("Sending registration request: {}", request_json));
crate::ipc::send_passkey_request(RequestType::Registration, request_json, rpid)
},
Err(e) => {
util::message(&format!("ERROR: Failed to serialize registration request: {}", e));
None
}
}
}
/// Creates a WebAuthn make credential response from Bitwarden's registration response
pub unsafe fn create_make_credential_response(credential_id: Vec<u8>, attestation_object: Vec<u8>) -> std::result::Result<*mut ExperimentalWebAuthnPluginOperationResponse, HRESULT> {
// Use the attestation object directly as the encoded response
let response_data = attestation_object;
let response_len = response_data.len();
// Allocate memory for the response data
let layout = Layout::from_size_align(response_len, 1).map_err(|_| HRESULT(-1))?;
let response_ptr = alloc(layout);
if response_ptr.is_null() {
return Err(HRESULT(-1));
}
// Copy response data
ptr::copy_nonoverlapping(response_data.as_ptr(), response_ptr, response_len);
// Allocate memory for the response structure
let response_layout = Layout::new::<ExperimentalWebAuthnPluginOperationResponse>();
let operation_response_ptr = alloc(response_layout) as *mut ExperimentalWebAuthnPluginOperationResponse;
if operation_response_ptr.is_null() {
return Err(HRESULT(-1));
}
// Initialize the response
ptr::write(operation_response_ptr, ExperimentalWebAuthnPluginOperationResponse {
encoded_response_byte_count: response_len as u32,
encoded_response_pointer: response_ptr,
});
Ok(operation_response_ptr)
}

View File

@@ -0,0 +1,239 @@
/* this ALWAYS GENERATED file contains the definitions for the interfaces */
/* File created by MIDL compiler version 8.01.0628 */
/* @@MIDL_FILE_HEADING( ) */
/* verify that the <rpcndr.h> version is high enough to compile this file*/
#ifndef __REQUIRED_RPCNDR_H_VERSION__
#define __REQUIRED_RPCNDR_H_VERSION__ 501
#endif
/* verify that the <rpcsal.h> version is high enough to compile this file*/
#ifndef __REQUIRED_RPCSAL_H_VERSION__
#define __REQUIRED_RPCSAL_H_VERSION__ 100
#endif
#include "rpc.h"
#include "rpcndr.h"
#ifndef __RPCNDR_H_VERSION__
#error this stub requires an updated version of <rpcndr.h>
#endif /* __RPCNDR_H_VERSION__ */
#ifndef COM_NO_WINDOWS_H
#include "windows.h"
#include "ole2.h"
#endif /*COM_NO_WINDOWS_H*/
#ifndef __pluginauthenticator_h__
#define __pluginauthenticator_h__
#if defined(_MSC_VER) && (_MSC_VER >= 1020)
#pragma once
#endif
#ifndef DECLSPEC_XFGVIRT
#if defined(_CONTROL_FLOW_GUARD_XFG)
#define DECLSPEC_XFGVIRT(base, func) __declspec(xfg_virtual(base, func))
#else
#define DECLSPEC_XFGVIRT(base, func)
#endif
#endif
/* Forward Declarations */
#ifndef __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__
#define __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__
typedef interface EXPERIMENTAL_IPluginAuthenticator EXPERIMENTAL_IPluginAuthenticator;
#endif /* __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__ */
/* header files for imported files */
#include "oaidl.h"
#include "webauthn.h"
#ifdef __cplusplus
extern "C"{
#endif
/* interface __MIDL_itf_pluginauthenticator_0000_0000 */
/* [local] */
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST
{
HWND hWnd;
GUID transactionId;
DWORD cbRequestSignature;
/* [size_is] */ byte *pbRequestSignature;
DWORD cbEncodedRequest;
/* [size_is] */ byte *pbEncodedRequest;
} EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST;
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST *EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_REQUEST;
typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST;
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE
{
DWORD cbEncodedResponse;
/* [size_is] */ byte *pbEncodedResponse;
} EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE;
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE *EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE;
typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_RESPONSE;
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST
{
GUID transactionId;
DWORD cbRequestSignature;
/* [size_is] */ byte *pbRequestSignature;
} EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *EXPERIMENTAL_PWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_c_ifspec;
extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_s_ifspec;
#ifndef __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__
#define __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__
/* interface EXPERIMENTAL_IPluginAuthenticator */
/* [unique][version][uuid][object] */
EXTERN_C const IID IID_EXPERIMENTAL_IPluginAuthenticator;
#if defined(__cplusplus) && !defined(CINTERFACE)
MIDL_INTERFACE("e6466e9a-b2f3-47c5-b88d-89bc14a8d998")
EXPERIMENTAL_IPluginAuthenticator : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginMakeCredential(
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response) = 0;
virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginGetAssertion(
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response) = 0;
virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginCancelOperation(
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request) = 0;
};
#else /* C style interface */
typedef struct EXPERIMENTAL_IPluginAuthenticatorVtbl
{
BEGIN_INTERFACE
DECLSPEC_XFGVIRT(IUnknown, QueryInterface)
HRESULT ( STDMETHODCALLTYPE *QueryInterface )(
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
/* [in] */ __RPC__in REFIID riid,
/* [annotation][iid_is][out] */
_COM_Outptr_ void **ppvObject);
DECLSPEC_XFGVIRT(IUnknown, AddRef)
ULONG ( STDMETHODCALLTYPE *AddRef )(
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This);
DECLSPEC_XFGVIRT(IUnknown, Release)
ULONG ( STDMETHODCALLTYPE *Release )(
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This);
DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginMakeCredential)
HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginMakeCredential )(
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response);
DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginGetAssertion)
HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginGetAssertion )(
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response);
DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginCancelOperation)
HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginCancelOperation )(
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request);
END_INTERFACE
} EXPERIMENTAL_IPluginAuthenticatorVtbl;
interface EXPERIMENTAL_IPluginAuthenticator
{
CONST_VTBL struct EXPERIMENTAL_IPluginAuthenticatorVtbl *lpVtbl;
};
#ifdef COBJMACROS
#define EXPERIMENTAL_IPluginAuthenticator_QueryInterface(This,riid,ppvObject) \
( (This)->lpVtbl -> QueryInterface(This,riid,ppvObject) )
#define EXPERIMENTAL_IPluginAuthenticator_AddRef(This) \
( (This)->lpVtbl -> AddRef(This) )
#define EXPERIMENTAL_IPluginAuthenticator_Release(This) \
( (This)->lpVtbl -> Release(This) )
#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginMakeCredential(This,request,response) \
( (This)->lpVtbl -> EXPERIMENTAL_PluginMakeCredential(This,request,response) )
#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginGetAssertion(This,request,response) \
( (This)->lpVtbl -> EXPERIMENTAL_PluginGetAssertion(This,request,response) )
#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginCancelOperation(This,request) \
( (This)->lpVtbl -> EXPERIMENTAL_PluginCancelOperation(This,request) )
#endif /* COBJMACROS */
#endif /* C style interface */
#endif /* __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__ */
/* Additional Prototypes for ALL interfaces */
unsigned long __RPC_USER HWND_UserSize( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * );
unsigned char * __RPC_USER HWND_UserMarshal( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * );
unsigned char * __RPC_USER HWND_UserUnmarshal(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * );
void __RPC_USER HWND_UserFree( __RPC__in unsigned long *, __RPC__in HWND * );
unsigned long __RPC_USER HWND_UserSize64( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * );
unsigned char * __RPC_USER HWND_UserMarshal64( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * );
unsigned char * __RPC_USER HWND_UserUnmarshal64(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * );
void __RPC_USER HWND_UserFree64( __RPC__in unsigned long *, __RPC__in HWND * );
/* end of Additional Prototypes */
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -1,126 +0,0 @@
/*
This file exposes safe functions and types for interacting with the experimental
Windows Plugin Authenticator API defined here:
https://github.com/microsoft/webauthn/blob/master/experimental/pluginauthenticator.h
The Factory pattern & COM interactions are based on the examples provided here:
- https://github.com/microsoft/windows-rs/blob/bb15076311bf185400ecd244d47596b8415450fa/crates/tests/libs/implement/tests/class_factory.rs
- https://github.com/microsoft/windows-rs/pull/3531
- https://kennykerr.ca/rust-getting-started/how-to-implement-com-interface.html
- https://github.com/bitwarden/clients/pull/10204/files#diff-a4de81fd5a2389d7b512dd37989a42a452fe36cbc1f32d16d5832880355d5669R106
*/
use windows::Win32::System::Com::*;
//use windows::{Foundation::*, Win32::System::Com::*};
use windows_core::*;
use crate::util;
/// Used when creating and asserting credentials.
/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST
/// Header File Usage: EXPERIMENTAL_PluginMakeCredential()
/// EXPERIMENTAL_PluginGetAssertion()
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct ExperimentalWebAuthnPluginOperationRequest {
pub window_handle: windows::Win32::Foundation::HWND,
pub transaction_id: windows_core::GUID,
pub request_signature_byte_count: u32,
pub request_signature_pointer: *mut u8,
pub encoded_request_byte_count: u32,
pub encoded_request_pointer: *mut u8,
}
/// Used as a response when creating and asserting credentials.
/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE
/// Header File Usage: EXPERIMENTAL_PluginMakeCredential()
/// EXPERIMENTAL_PluginGetAssertion()
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct ExperimentalWebAuthnPluginOperationResponse {
pub encoded_response_byte_count: u32,
pub encoded_response_pointer: *mut u8,
}
/// Used to cancel an operation.
/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST
/// Header File Usage: EXPERIMENTAL_PluginCancelOperation()
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct ExperimentalWebAuthnPluginCancelOperationRequest {
pub transaction_id: windows_core::GUID,
pub request_signature_byte_count: u32,
pub request_signature_pointer: *mut u8,
}
#[interface("e6466e9a-b2f3-47c5-b88d-89bc14a8d998")]
pub unsafe trait EXPERIMENTAL_IPluginAuthenticator: IUnknown {
fn EXPERIMENTAL_PluginMakeCredential(
&self,
request: *const ExperimentalWebAuthnPluginOperationRequest,
response: *mut *mut ExperimentalWebAuthnPluginOperationResponse,
) -> HRESULT;
fn EXPERIMENTAL_PluginGetAssertion(
&self,
request: *const ExperimentalWebAuthnPluginOperationRequest,
response: *mut *mut ExperimentalWebAuthnPluginOperationResponse,
) -> HRESULT;
fn EXPERIMENTAL_PluginCancelOperation(
&self,
request: *const ExperimentalWebAuthnPluginCancelOperationRequest,
) -> HRESULT;
}
#[implement(EXPERIMENTAL_IPluginAuthenticator)]
pub struct PluginAuthenticatorComObject;
#[implement(IClassFactory)]
pub struct Factory;
impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Impl {
unsafe fn EXPERIMENTAL_PluginMakeCredential(
&self,
request: *const ExperimentalWebAuthnPluginOperationRequest,
response: *mut *mut ExperimentalWebAuthnPluginOperationResponse,
) -> HRESULT {
//panic!("EXPERIMENTAL_PluginMakeCredential() called");
util::message(String::from("EXPERIMENTAL_PluginMakeCredential() called"));
HRESULT(0)
}
unsafe fn EXPERIMENTAL_PluginGetAssertion(
&self,
request: *const ExperimentalWebAuthnPluginOperationRequest,
response: *mut *mut ExperimentalWebAuthnPluginOperationResponse,
) -> HRESULT {
//panic!("EXPERIMENTAL_PluginGetAssertion() called");
util::message(String::from("EXPERIMENTAL_PluginGetAssertion() called"));
HRESULT(0)
}
unsafe fn EXPERIMENTAL_PluginCancelOperation(
&self,
request: *const ExperimentalWebAuthnPluginCancelOperationRequest,
) -> HRESULT {
//panic!("EXPERIMENTAL_PluginCancelOperation() called");
util::message(String::from("EXPERIMENTAL_PluginCancelOperation() called"));
HRESULT(0)
}
}
impl IClassFactory_Impl for Factory_Impl {
fn CreateInstance(
&self,
outer: Ref<IUnknown>,
iid: *const GUID,
object: *mut *mut core::ffi::c_void,
) -> Result<()> {
let unknown: IInspectable = PluginAuthenticatorComObject.into(); // TODO: IUnknown ?
unsafe { unknown.query(iid, object).ok() }
}
fn LockServer(&self, lock: BOOL) -> Result<()> {
Ok(())
}
}

View File

@@ -0,0 +1,207 @@
use serde_json;
use hex;
use crate::types::*;
use crate::utils::{self as util, wstr_to_string};
use crate::webauthn::*;
use crate::ipc::send_passkey_request;
/// Helper for sync requests - requests credentials from Electron for a specific RP ID
pub fn send_sync_request(rpid: &str) -> Option<PasskeyResponse> {
util::message(&format!("[SYNC] send_sync_request called for RP ID: {}", rpid));
let request = PasskeySyncRequest {
rp_id: rpid.to_string(),
};
util::message(&format!("[SYNC] Created sync request for RP ID: {}", rpid));
match serde_json::to_string(&request) {
Ok(request_json) => {
util::message(&format!("[SYNC] Serialized sync request to JSON: {}", request_json));
util::message(&format!("[SYNC] Sending sync request to Electron via IPC"));
let response = send_passkey_request(RequestType::Sync, request_json, rpid);
match &response {
Some(resp) => util::message(&format!("[SYNC] Received response from Electron: {:?}", resp)),
None => util::message("[SYNC] No response received from Electron"),
}
response
},
Err(e) => {
util::message(&format!("[SYNC] ERROR: Failed to serialize sync request: {}", e));
None
}
}
}
/// Initiates credential sync from Electron to Windows - called when Electron wants to push credentials to Windows
pub fn sync_credentials_to_windows(credentials: Vec<SyncedCredential>, plugin_clsid: &str) -> Result<(), String> {
util::message(&format!("[SYNC_TO_WIN] sync_credentials_to_windows called with {} credentials for plugin CLSID: {}", credentials.len(), plugin_clsid));
// Format CLSID with curly braces to match Windows registration format
let formatted_clsid = format!("{{{}}}", plugin_clsid);
if credentials.is_empty() {
util::message("[SYNC_TO_WIN] No credentials to sync, proceeding with empty sync");
}
// Convert Bitwarden credentials to Windows credential details
let mut win_credentials = Vec::new();
for (i, cred) in credentials.iter().enumerate() {
let truncated_cred_id = if cred.credential_id.len() > 16 {
format!("{}...", hex::encode(&cred.credential_id[..16]))
} else {
hex::encode(&cred.credential_id)
};
let truncated_user_id = if cred.user_id.len() > 16 {
format!("{}...", hex::encode(&cred.user_id[..16]))
} else {
hex::encode(&cred.user_id)
};
util::message(&format!("[SYNC_TO_WIN] Converting credential {}: RP ID: {}, User: {}, Credential ID: {} ({} bytes), User ID: {} ({} bytes)",
i + 1, cred.rp_id, cred.user_name, truncated_cred_id, cred.credential_id.len(), truncated_user_id, cred.user_id.len()));
let win_cred = ExperimentalWebAuthnPluginCredentialDetails::create_from_bytes(
cred.credential_id.clone(), // Pass raw bytes
cred.rp_id.clone(),
cred.rp_id.clone(), // Use RP ID as friendly name for now
cred.user_id.clone(), // Pass raw bytes
cred.user_name.clone(),
cred.user_name.clone(), // Use user name as display name for now
);
win_credentials.push(win_cred);
util::message(&format!("[SYNC_TO_WIN] Converted credential {} to Windows format", i + 1));
}
// Create credentials list
let credentials_list = ExperimentalWebAuthnPluginCredentialDetailsList::create(
formatted_clsid.clone(),
win_credentials
);
// First try to remove all existing credentials for this plugin
util::message("Attempting to remove all existing credentials before sync...");
match remove_all_credentials(formatted_clsid.clone()) {
Ok(()) => {
util::message("Successfully removed existing credentials");
},
Err(e) if e.contains("can't be loaded") => {
util::message("RemoveAllCredentials function not available - this is expected for some Windows versions");
// This is fine, the function might not exist in all versions
},
Err(e) => {
util::message(&format!("Warning: Failed to remove existing credentials: {}", e));
// Continue anyway, as this might be the first sync or an older Windows version
}
}
// Add the new credentials (only if we have any)
if credentials.is_empty() {
util::message("No credentials to add to Windows - sync completed successfully");
Ok(())
} else {
util::message("Adding new credentials to Windows...");
match add_credentials(credentials_list) {
Ok(()) => {
util::message("Successfully synced credentials to Windows");
Ok(())
},
Err(e) => {
util::message(&format!("ERROR: Failed to add credentials to Windows: {}", e));
Err(e)
}
}
}
}
/// Gets all credentials from Windows for a specific plugin - used when Electron requests current state
pub fn get_credentials_from_windows(plugin_clsid: &str) -> Result<Vec<SyncedCredential>, String> {
util::message(&format!("Getting all credentials from Windows for plugin CLSID: {}", plugin_clsid));
// Format CLSID with curly braces to match Windows registration format
let formatted_clsid = format!("{{{}}}", plugin_clsid);
match get_all_credentials(formatted_clsid) {
Ok(Some(credentials_list)) => {
util::message(&format!("Retrieved {} credentials from Windows", credentials_list.credential_count));
let mut bitwarden_credentials = Vec::new();
// Convert Windows credentials to Bitwarden format
unsafe {
let credentials_array = std::slice::from_raw_parts(
credentials_list.credentials,
credentials_list.credential_count as usize
);
for &cred_ptr in credentials_array {
if !cred_ptr.is_null() {
let cred = &*cred_ptr;
// Convert credential data back to Bitwarden format
let credential_id = if cred.credential_id_byte_count > 0 && !cred.credential_id_pointer.is_null() {
let id_slice = std::slice::from_raw_parts(
cred.credential_id_pointer,
cred.credential_id_byte_count as usize
);
// Assume it's hex-encoded, try to decode
hex::decode(std::str::from_utf8(id_slice).unwrap_or("")).unwrap_or_else(|_| id_slice.to_vec())
} else {
Vec::new()
};
let rp_id = if !cred.rpid.is_null() {
wstr_to_string(cred.rpid).unwrap_or_default()
} else {
String::new()
};
let user_name = if !cred.user_name.is_null() {
wstr_to_string(cred.user_name).unwrap_or_default()
} else {
String::new()
};
let user_id = if cred.user_id_byte_count > 0 && !cred.user_id_pointer.is_null() {
// Convert from UTF-8 bytes back to Vec<u8>
let user_id_slice = std::slice::from_raw_parts(
cred.user_id_pointer,
cred.user_id_byte_count as usize
);
// Try to decode as hex string, or use raw bytes
let user_id_str = std::str::from_utf8(user_id_slice).unwrap_or("");
hex::decode(user_id_str).unwrap_or_else(|_| user_id_slice.to_vec())
} else {
Vec::new()
};
let synced_cred = SyncedCredential {
credential_id,
rp_id,
user_name,
user_id,
};
util::message(&format!("Converted Windows credential: RP ID: {}, User: {}, Credential ID: {} bytes",
synced_cred.rp_id, synced_cred.user_name, synced_cred.credential_id.len()));
bitwarden_credentials.push(synced_cred);
}
}
}
Ok(bitwarden_credentials)
},
Ok(None) => {
util::message("No credentials found in Windows");
Ok(Vec::new())
},
Err(e) => {
util::message(&format!("ERROR: Failed to get credentials from Windows: {}", e));
Err(e)
}
}
}

View File

@@ -0,0 +1,92 @@
use tokio::sync::oneshot;
/// Assertion request structure
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyAssertionRequest {
pub rp_id: String,
pub transaction_id: String,
pub client_data_hash: Vec<u8>,
pub allowed_credentials: Vec<Vec<u8>>,
pub user_verification: bool,
}
/// Registration request structure
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyRegistrationRequest {
pub rp_id: String,
pub transaction_id: String,
pub user_id: Vec<u8>,
pub user_name: String,
pub client_data_hash: Vec<u8>,
pub user_verification: bool,
pub supported_algorithms: Vec<i32>, // COSE algorithm identifiers
}
/// Sync request structure
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeySyncRequest {
pub rp_id: String,
}
/// Union type for different request types
#[derive(Debug, Clone)]
pub enum PasskeyRequest {
AssertionRequest(PasskeyAssertionRequest),
RegistrationRequest(PasskeyRegistrationRequest),
SyncRequest(PasskeySyncRequest),
}
/// Response types for different operations - kept as tagged enum for JSON compatibility
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum PasskeyResponse {
#[serde(rename = "assertion_response",rename_all = "camelCase")]
AssertionResponse {
credential_id: Vec<u8>,
authenticator_data: Vec<u8>,
signature: Vec<u8>,
user_handle: Vec<u8>,
},
#[serde(rename = "registration_response",rename_all = "camelCase")]
RegistrationResponse {
credential_id: Vec<u8>,
attestation_object: Vec<u8>,
},
#[serde(rename = "sync_response",rename_all = "camelCase")]
SyncResponse {
credentials: Vec<SyncedCredential>,
},
#[serde(rename = "error",rename_all = "camelCase")]
Error {
message: String,
},
}
/// Credential data for sync operations
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SyncedCredential {
pub credential_id: Vec<u8>,
pub rp_id: String,
pub user_name: String,
pub user_id: Vec<u8>,
}
/// Request type enumeration for type discrimination
#[derive(Debug, Clone)]
pub enum RequestType {
Assertion,
Registration,
Sync,
}
/// Internal request event with response channel and serializable request data
#[derive(Debug)]
pub struct RequestEvent {
pub request_type: RequestType,
pub request_json: String,
pub response_sender: oneshot::Sender<PasskeyResponse>,
}

View File

@@ -1,66 +0,0 @@
use std::ffi::OsString;
use std::os::windows::ffi::OsStrExt;
use serde_json::json;
use windows::Win32::Foundation::*;
use windows::Win32::System::LibraryLoader::*;
use windows_core::*;
pub unsafe fn delay_load<T>(library: PCSTR, function: PCSTR) -> Option<T> {
let library = LoadLibraryExA(library, None, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
let Ok(library) = library else {
return None;
};
let address = GetProcAddress(library, function);
if address.is_some() {
return Some(std::mem::transmute_copy(&address));
}
_ = FreeLibrary(library);
None
}
pub trait WindowsString {
fn into_win_utf8(self: Self) -> (*mut u8, u32);
fn into_win_utf16(self: Self) -> (*mut u16, u32);
fn into_win_utf16_wide(self: Self) -> (*mut u16, u32);
}
impl WindowsString for String {
fn into_win_utf8(self: Self) -> (*mut u8, u32) {
let mut v = self.into_bytes();
v.push(0);
(v.as_mut_ptr(), v.len() as u32)
}
fn into_win_utf16(self: Self) -> (*mut u16, u32) {
let mut v: Vec<u16> = self.encode_utf16().collect();
v.push(0);
(v.as_mut_ptr(), v.len() as u32)
}
fn into_win_utf16_wide(self: Self) -> (*mut u16, u32) {
let mut v: Vec<u16> = OsString::from(self).encode_wide().collect();
v.push(0);
(v.as_mut_ptr(), v.len() as u32)
}
}
pub fn message(message: String) {
let json_data = json!({
"message": message,
});
let request = reqwest::blocking::Client::new();
let _ = request
.post("http://127.0.0.1:3000/message")
.json(&json_data)
.send();
}

View File

@@ -0,0 +1,109 @@
use std::ffi::OsString;
use std::os::windows::ffi::OsStrExt;
use std::fs::{OpenOptions, create_dir_all};
use std::io::Write;
use std::time::{SystemTime, UNIX_EPOCH};
use std::path::Path;
use windows::Win32::Foundation::*;
use windows::Win32::System::LibraryLoader::*;
use windows_core::*;
pub unsafe fn delay_load<T>(library: PCSTR, function: PCSTR) -> Option<T> {
let library = LoadLibraryExA(library, None, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
let Ok(library) = library else {
return None;
};
let address = GetProcAddress(library, function);
if address.is_some() {
return Some(std::mem::transmute_copy(&address));
}
_ = FreeLibrary(library);
None
}
pub trait WindowsString {
fn into_win_utf8(self: Self) -> (*mut u8, u32);
fn into_win_utf16(self: Self) -> (*mut u16, u32);
fn into_win_utf16_wide(self: Self) -> (*mut u16, u32);
}
impl WindowsString for String {
fn into_win_utf8(self: Self) -> (*mut u8, u32) {
let mut v = self.into_bytes();
v.push(0);
(v.as_mut_ptr(), v.len() as u32)
}
fn into_win_utf16(self: Self) -> (*mut u16, u32) {
let mut v: Vec<u16> = self.encode_utf16().collect();
v.push(0);
(v.as_mut_ptr(), v.len() as u32)
}
fn into_win_utf16_wide(self: Self) -> (*mut u16, u32) {
let mut v: Vec<u16> = OsString::from(self).encode_wide().collect();
v.push(0);
(v.as_mut_ptr(), v.len() as u32)
}
}
pub fn file_log(msg: &str) {
let log_path = "C:\\temp\\bitwarden_com_debug.log";
// Create the temp directory if it doesn't exist
if let Some(parent) = Path::new(log_path).parent() {
let _ = create_dir_all(parent);
}
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open(log_path)
{
let now = SystemTime::now();
let timestamp = match now.duration_since(UNIX_EPOCH) {
Ok(duration) => {
let total_secs = duration.as_secs();
let millis = duration.subsec_millis();
let secs = total_secs % 60;
let mins = (total_secs / 60) % 60;
let hours = (total_secs / 3600) % 24;
format!("{:02}:{:02}:{:02}.{:03}", hours, mins, secs, millis)
},
Err(_) => "??:??:??.???".to_string()
};
let _ = writeln!(file, "[{}] {}", timestamp, msg);
}
}
pub fn message(message: &str) {
file_log(message)
}
// Helper function to convert Windows wide string (UTF-16) to Rust String
pub unsafe fn wstr_to_string(wstr_ptr: *const u16) -> std::result::Result<String, std::string::FromUtf16Error> {
if wstr_ptr.is_null() {
return Ok(String::new());
}
// Find the length of the null-terminated wide string
let mut len = 0;
while *wstr_ptr.add(len) != 0 {
len += 1;
}
// Convert to Rust string
let wide_slice = std::slice::from_raw_parts(wstr_ptr, len);
String::from_utf16(wide_slice)
}

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,7 @@ use windows::Win32::System::LibraryLoader::*;
use windows_core::*;
use crate::util::*;
use crate::com_buffer::ComBuffer;
/// Used when adding a Windows plugin authenticator.
/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS
@@ -51,7 +52,7 @@ pub struct ExperimentalWebAuthnPluginCredentialDetails {
pub rpid: *mut u16,
pub rp_friendly_name: *mut u16,
pub user_id_byte_count: u32,
pub user_id: *mut u16,
pub user_id_pointer: *mut u8, // Should be *mut u8 like credential_id_pointer
pub user_name: *mut u16,
pub user_display_name: *mut u16,
}
@@ -65,18 +66,92 @@ impl ExperimentalWebAuthnPluginCredentialDetails {
user_name: String,
user_display_name: String,
) -> Self {
let (credential_id_pointer, credential_id_byte_count) = credential_id.into_win_utf8();
let (user_id, user_id_byte_count) = user_id.into_win_utf16();
// Use COM allocation for all strings
let (credential_id_pointer, credential_id_byte_count) = ComBuffer::from_buffer(credential_id.as_bytes());
let (user_id_pointer, user_id_byte_count) = ComBuffer::from_buffer(user_id.as_bytes());
// Convert to wide strings and allocate with COM
let mut rpid_wide: Vec<u16> = rpid.encode_utf16().collect();
rpid_wide.push(0);
let rpid_bytes: Vec<u8> = rpid_wide.iter().flat_map(|&x| x.to_le_bytes()).collect();
let (rpid_ptr, _) = ComBuffer::from_buffer(rpid_bytes);
let mut rp_friendly_name_wide: Vec<u16> = rp_friendly_name.encode_utf16().collect();
rp_friendly_name_wide.push(0);
let rp_friendly_name_bytes: Vec<u8> = rp_friendly_name_wide.iter().flat_map(|&x| x.to_le_bytes()).collect();
let (rp_friendly_name_ptr, _) = ComBuffer::from_buffer(rp_friendly_name_bytes);
let mut user_name_wide: Vec<u16> = user_name.encode_utf16().collect();
user_name_wide.push(0);
let user_name_bytes: Vec<u8> = user_name_wide.iter().flat_map(|&x| x.to_le_bytes()).collect();
let (user_name_ptr, _) = ComBuffer::from_buffer(user_name_bytes);
let mut user_display_name_wide: Vec<u16> = user_display_name.encode_utf16().collect();
user_display_name_wide.push(0);
let user_display_name_bytes: Vec<u8> = user_display_name_wide.iter().flat_map(|&x| x.to_le_bytes()).collect();
let (user_display_name_ptr, _) = ComBuffer::from_buffer(user_display_name_bytes);
Self {
credential_id_byte_count,
credential_id_pointer,
rpid: rpid.into_win_utf16().0,
rp_friendly_name: rp_friendly_name.into_win_utf16().0,
rpid: rpid_ptr as *mut u16,
rp_friendly_name: rp_friendly_name_ptr as *mut u16,
user_id_byte_count,
user_id,
user_name: user_name.into_win_utf16().0,
user_display_name: user_display_name.into_win_utf16().0,
user_id_pointer,
user_name: user_name_ptr as *mut u16,
user_display_name: user_display_name_ptr as *mut u16,
}
}
pub fn create_from_bytes(
credential_id: Vec<u8>,
rpid: String,
rp_friendly_name: String,
user_id: Vec<u8>,
user_name: String,
user_display_name: String,
) -> Self {
use std::ffi::OsString;
use std::os::windows::ffi::OsStrExt;
// Convert credential_id bytes to hex string, then allocate with COM
let credential_id_string = hex::encode(&credential_id);
let (credential_id_pointer, credential_id_byte_count) = ComBuffer::from_buffer(credential_id_string.as_bytes());
// Convert user_id bytes to hex string, then allocate with COM
let user_id_string = hex::encode(&user_id);
let (user_id_pointer, user_id_byte_count) = ComBuffer::from_buffer(user_id_string.as_bytes());
// Convert strings to null-terminated wide strings and allocate with COM
let mut rpid_vec: Vec<u16> = OsString::from(rpid).encode_wide().collect();
rpid_vec.push(0);
let rpid_bytes: Vec<u8> = rpid_vec.iter().flat_map(|&x| x.to_le_bytes()).collect();
let (rpid_ptr, _) = ComBuffer::from_buffer(rpid_bytes);
let mut rp_friendly_name_vec: Vec<u16> = OsString::from(rp_friendly_name).encode_wide().collect();
rp_friendly_name_vec.push(0);
let rp_friendly_name_bytes: Vec<u8> = rp_friendly_name_vec.iter().flat_map(|&x| x.to_le_bytes()).collect();
let (rp_friendly_name_ptr, _) = ComBuffer::from_buffer(rp_friendly_name_bytes);
let mut user_name_vec: Vec<u16> = OsString::from(user_name).encode_wide().collect();
user_name_vec.push(0);
let user_name_bytes: Vec<u8> = user_name_vec.iter().flat_map(|&x| x.to_le_bytes()).collect();
let (user_name_ptr, _) = ComBuffer::from_buffer(user_name_bytes);
let mut user_display_name_vec: Vec<u16> = OsString::from(user_display_name).encode_wide().collect();
user_display_name_vec.push(0);
let user_display_name_bytes: Vec<u8> = user_display_name_vec.iter().flat_map(|&x| x.to_le_bytes()).collect();
let (user_display_name_ptr, _) = ComBuffer::from_buffer(user_display_name_bytes);
Self {
credential_id_byte_count,
credential_id_pointer,
rpid: rpid_ptr as *mut u16,
rp_friendly_name: rp_friendly_name_ptr as *mut u16,
user_id_byte_count,
user_id_pointer,
user_name: user_name_ptr as *mut u16,
user_display_name: user_display_name_ptr as *mut u16,
}
}
}
@@ -111,38 +186,73 @@ std::mem::forget(credentials);
impl ExperimentalWebAuthnPluginCredentialDetailsList {
pub fn create(
clsid: String,
mut credentials: Vec<ExperimentalWebAuthnPluginCredentialDetails>,
credentials: Vec<ExperimentalWebAuthnPluginCredentialDetails>,
) -> Self {
let mut credentials: Vec<*mut ExperimentalWebAuthnPluginCredentialDetails> = credentials
// Convert credentials to COM-allocated pointers
let mut credential_pointers: Vec<*mut ExperimentalWebAuthnPluginCredentialDetails> = credentials
.into_iter()
.map(|mut cred| {
let cred_pointer: *mut ExperimentalWebAuthnPluginCredentialDetails = &mut cred;
cred_pointer
.map(|cred| {
// Use COM allocation for each credential struct
ComBuffer::with_object(cred)
})
.collect();
let credentials_len = credentials.len();
let _credentials_capacity = credentials.capacity();
let mut credentials_pointer = credentials.as_mut_ptr();
// TODO: remember the above 3 so it can be re-created and dropped later - refactor this
std::mem::forget(credentials); // forget so Rust doesn't drop the memory
let credentials_len = credential_pointers.len();
// Allocate the array of pointers using COM as well
let credentials_pointer = if credentials_len > 0 {
let pointer_array_bytes = credential_pointers.len() * std::mem::size_of::<*mut ExperimentalWebAuthnPluginCredentialDetails>();
let (ptr, _) = ComBuffer::from_buffer(unsafe {
std::slice::from_raw_parts(
credential_pointers.as_ptr() as *const u8,
pointer_array_bytes
)
});
ptr as *mut *mut ExperimentalWebAuthnPluginCredentialDetails
} else {
std::ptr::null_mut()
};
// Convert CLSID to wide string and allocate with COM
let mut clsid_wide: Vec<u16> = clsid.encode_utf16().collect();
clsid_wide.push(0); // null terminator
let clsid_bytes: Vec<u8> = clsid_wide.iter().flat_map(|&x| x.to_le_bytes()).collect();
let (clsid_ptr, _) = ComBuffer::from_buffer(clsid_bytes);
Self {
plugin_clsid: clsid.into_win_utf16().0,
plugin_clsid: clsid_ptr as *mut u16,
credential_count: credentials_len as u32,
credentials: credentials_pointer,
}
}
}
type EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentialsFnDeclaration =
pub type EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentialsFnDeclaration =
unsafe extern "cdecl" fn(
pCredentialDetailsList: *mut ExperimentalWebAuthnPluginCredentialDetailsList,
) -> HRESULT;
pub type EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveCredentialsFnDeclaration =
unsafe extern "cdecl" fn(
pCredentialDetailsList: *mut ExperimentalWebAuthnPluginCredentialDetailsList,
) -> HRESULT;
pub type EXPERIMENTAL_WebAuthNPluginAuthenticatorGetAllCredentialsFnDeclaration =
unsafe extern "cdecl" fn(
pwszPluginClsId: *const u16,
ppCredentialDetailsList: *mut *mut ExperimentalWebAuthnPluginCredentialDetailsList,
) -> HRESULT;
pub type EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentialsFnDeclaration =
unsafe extern "cdecl" fn(
pwszPluginClsId: *const u16,
) -> HRESULT;
pub fn add_credentials(
mut credentials_list: ExperimentalWebAuthnPluginCredentialDetailsList,
) -> std::result::Result<(), String> {
crate::utils::message("Loading EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials function...");
let result = unsafe {
delay_load::<EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentialsFnDeclaration>(
s!("webauthn.dll"),
@@ -150,13 +260,50 @@ pub fn add_credentials(
)
};
match result {
Some(api) => {
crate::utils::message("Function loaded successfully, calling API...");
crate::utils::message(&format!("Credential list: plugin_clsid valid: {}, credential_count: {}",
!credentials_list.plugin_clsid.is_null(), credentials_list.credential_count));
let result = unsafe { api(&mut credentials_list) };
if result.is_err() {
let error_code = result.0;
crate::utils::message(&format!("API call failed with HRESULT: 0x{:x}", error_code));
return Err(format!(
"Error: Error response from EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials()\nHRESULT: 0x{:x}\n{}",
error_code, result.message()
));
}
crate::utils::message("API call succeeded");
Ok(())
},
None => {
crate::utils::message("Failed to load EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials function from webauthn.dll");
Err(String::from("Error: Can't complete add_credentials(), as the function EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials can't be loaded."))
}
}
}
pub fn remove_credentials(
mut credentials_list: ExperimentalWebAuthnPluginCredentialDetailsList,
) -> std::result::Result<(), String> {
let result = unsafe {
delay_load::<EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveCredentialsFnDeclaration>(
s!("webauthn.dll"),
s!("EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveCredentials"),
)
};
match result {
Some(api) => {
let result = unsafe { api(&mut credentials_list) };
if result.is_err() {
return Err(format!(
"Error: Error response from EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials()\n{}",
"Error: Error response from EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveCredentials()\n{}",
result.message()
));
}
@@ -164,7 +311,196 @@ pub fn add_credentials(
Ok(())
},
None => {
Err(String::from("Error: Can't complete add_credentials(), as the function EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials can't be loaded."))
Err(String::from("Error: Can't complete remove_credentials(), as the function EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveCredentials can't be loaded."))
}
}
}
pub fn get_all_credentials(
plugin_clsid: String,
) -> std::result::Result<Option<ExperimentalWebAuthnPluginCredentialDetailsList>, String> {
let result = unsafe {
delay_load::<EXPERIMENTAL_WebAuthNPluginAuthenticatorGetAllCredentialsFnDeclaration>(
s!("webauthn.dll"),
s!("EXPERIMENTAL_WebAuthNPluginAuthenticatorGetAllCredentials"),
)
};
match result {
Some(api) => {
// Create the wide string and keep it alive during the API call
let mut clsid_wide: Vec<u16> = plugin_clsid.encode_utf16().collect();
clsid_wide.push(0); // null terminator
let mut credentials_list_ptr: *mut ExperimentalWebAuthnPluginCredentialDetailsList = std::ptr::null_mut();
let result = unsafe { api(clsid_wide.as_ptr(), &mut credentials_list_ptr) };
if result.is_err() {
return Err(format!(
"Error: Error response from EXPERIMENTAL_WebAuthNPluginAuthenticatorGetAllCredentials()\n{}",
result.message()
));
}
if credentials_list_ptr.is_null() {
Ok(None)
} else {
// Note: The caller is responsible for managing the memory of the returned list
Ok(Some(unsafe { *credentials_list_ptr }))
}
},
None => {
Err(String::from("Error: Can't complete get_all_credentials(), as the function EXPERIMENTAL_WebAuthNPluginAuthenticatorGetAllCredentials can't be loaded."))
}
}
}
pub fn remove_all_credentials(
plugin_clsid: String,
) -> std::result::Result<(), String> {
crate::utils::message("Loading EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentials function...");
let result = unsafe {
delay_load::<EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentialsFnDeclaration>(
s!("webauthn.dll"),
s!("EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentials"),
)
};
match result {
Some(api) => {
crate::utils::message("Function loaded successfully, calling API...");
// Create the wide string and keep it alive during the API call
let mut clsid_wide: Vec<u16> = plugin_clsid.encode_utf16().collect();
clsid_wide.push(0); // null terminator
let result = unsafe { api(clsid_wide.as_ptr()) };
if result.is_err() {
let error_code = result.0;
crate::utils::message(&format!("API call failed with HRESULT: 0x{:x}", error_code));
return Err(format!(
"Error: Error response from EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentials()\nHRESULT: 0x{:x}\n{}",
error_code, result.message()
));
}
crate::utils::message("API call succeeded");
Ok(())
},
None => {
crate::utils::message("Failed to load EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentials function from webauthn.dll");
Err(String::from("Error: Can't complete remove_all_credentials(), as the function EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentials can't be loaded."))
}
}
}
// Forward declarations for Windows types we need
type WEBAUTHN_ASSERTION = *const u8; // Placeholder - would need actual definition
type PCWEBAUTHN_USER_ENTITY_INFORMATION = *const u8; // Placeholder - would need actual definition
type WEBAUTHN_CREDENTIAL_LIST = *const u8; // Placeholder - would need actual definition
type EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS = *const u8; // Placeholder
type EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION = *const u8; // Placeholder
/// CTAP CBOR Get Assertion Request structure
/// Header File Name: _EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct ExperimentalWebAuthnCtapCborGetAssertionRequest {
// Version of this structure, to allow for modifications in the future.
pub version: u32, // DWORD dwVersion
// RP ID. After UTF8 to Unicode conversion,
pub rpid_unicode: *const u16, // PCWSTR pwszRpId
// Input RP ID. Raw UTF8 bytes before conversion.
// These are the bytes to be hashed in the Authenticator Data.
pub rpid_byte_count: u32, // DWORD cbRpId
pub rpid_bytes: *const u8, // PBYTE pbRpId
// Client Data Hash
pub client_data_hash_byte_count: u32, // DWORD cbClientDataHash
pub client_data_hash: *const u8, // PBYTE pbClientDataHash
// Credentials used for inclusion
pub credential_list: WEBAUTHN_CREDENTIAL_LIST, // WEBAUTHN_CREDENTIAL_LIST CredentialList
// Optional extensions to parse when performing the operation.
pub cbor_extensions_map_byte_count: u32, // DWORD cbCborExtensionsMap
pub cbor_extensions_map: *const u8, // PBYTE pbCborExtensionsMap
// Authenticator Options (Optional)
pub authenticator_options: EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS,
// Pin Auth (Optional)
pub empty_pin_auth: i32, // BOOL fEmptyPinAuth
pub pin_auth_byte_count: u32, // DWORD cbPinAuth
pub pin_auth: *const u8, // PBYTE pbPinAuth
// HMAC Salt Extension (Optional)
pub hmac_salt_extension: EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION,
// PRF Extension
pub hmac_secret_salt_values_byte_count: u32, // DWORD cbHmacSecretSaltValues
pub hmac_secret_salt_values: *const u8, // PBYTE pbHmacSecretSaltValues
pub pin_protocol: u32, // DWORD dwPinProtocol
// "credBlob": true extension
pub cred_blob_ext: i32, // LONG lCredBlobExt
// "largeBlobKey": true extension
pub large_blob_key_ext: i32, // LONG lLargeBlobKeyExt
// "largeBlob" extension
pub cred_large_blob_operation: u32, // DWORD dwCredLargeBlobOperation
pub cred_large_blob_compressed_byte_count: u32, // DWORD cbCredLargeBlobCompressed
pub cred_large_blob_compressed: *const u8, // PBYTE pbCredLargeBlobCompressed
pub cred_large_blob_original_size: u32, // DWORD dwCredLargeBlobOriginalSize
// "json" extension. Nonzero if present
pub json_ext_byte_count: u32, // DWORD cbJsonExt
pub json_ext: *const u8, // PBYTE pbJsonExt
}
pub type ExperimentalPWebAuthnCtapCborGetAssertionRequest = *mut ExperimentalWebAuthnCtapCborGetAssertionRequest;
pub type ExperimentalPcWebAuthnCtapCborGetAssertionRequest = *const ExperimentalWebAuthnCtapCborGetAssertionRequest;
/// CTAP CBOR Get Assertion Response structure
/// Header File Name: _EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_RESPONSE
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct ExperimentalWebAuthnCtapCborGetAssertionResponse {
// [1] credential (optional)
// [2] authenticatorData
// [3] signature
pub webauthn_assertion: WEBAUTHN_ASSERTION,
// [4] user (optional)
pub user_information: PCWEBAUTHN_USER_ENTITY_INFORMATION,
// [5] numberOfCredentials (optional)
pub number_of_credentials: u32, // DWORD
// [6] userSelected (optional)
pub user_selected: i32, // LONG
// [7] largeBlobKey (optional)
pub large_blob_key_byte_count: u32, // DWORD
pub large_blob_key: *mut u8, // PBYTE
// [8] unsignedExtensionOutputs
pub unsigned_extension_outputs_byte_count: u32, // DWORD
pub unsigned_extension_outputs: *mut u8, // PBYTE
}
pub type ExperimentalPWebAuthnCtapCborGetAssertionResponse = *mut ExperimentalWebAuthnCtapCborGetAssertionResponse;
pub type ExperimentalPcWebAuthnCtapCborGetAssertionResponse = *const ExperimentalWebAuthnCtapCborGetAssertionResponse;
/// Function signature for encoding get assertion response
type EXPERIMENTAL_WebAuthNEncodeGetAssertionResponseFnDeclaration = unsafe extern "stdcall" fn(
pGetAssertionResponse: ExperimentalPcWebAuthnCtapCborGetAssertionResponse,
pcbResp: *mut u32,
ppbResp: *mut *mut u8,
) -> HRESULT;

View File

@@ -1,4 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json",
"extraMetadata": {
"name": "bitwarden"
},
@@ -88,9 +90,10 @@
},
"win": {
"electronUpdaterCompatibility": ">=0.0.1",
"target": ["portable", "nsis-web", "appx"],
"target": ["appx"],
"signtoolOptions": {
"sign": "./sign.js"
"sign": "./sign.js",
"publisherName": "CN=com.bitwarden.localdevelopment"
},
"extraFiles": [
{
@@ -162,8 +165,9 @@
"artifactName": "${productName}-Portable-${version}.${ext}"
},
"appx": {
"artifactName": "${productName}-${version}-${arch}.${ext}",
"customManifestPath": "./custom-appx-manifest.xml"
"artifactName": "${productName}-${arch}.${ext}",
"customManifestPath": "./custom-appx-manifest.xml",
"publisher": "CN=com.bitwarden.localdevelopment"
},
"deb": {
"artifactName": "${productName}-${version}-${arch}.${ext}",

View File

@@ -1,7 +1,7 @@
{
"name": "@bitwarden/desktop",
"description": "A secure and free password manager for all of your devices.",
"version": "2025.6.0",
"version": "2025.6.4",
"keywords": [
"bitwarden",
"password",
@@ -67,6 +67,7 @@
"upload:mas": "xcrun altool --upload-app --type osx --file \"$(find ./dist/mas-universal/Bitwarden*.pkg)\" --apiKey $APP_STORE_CONNECT_AUTH_KEY --apiIssuer $APP_STORE_CONNECT_TEAM_ISSUER",
"test": "jest",
"test:watch": "jest --watch",
"test:watch:all": "jest --watchAll"
"test:watch:all": "jest --watchAll",
"local:win": "cd desktop_native/napi && npm run build && cd ../.. && npm run build:dev && npm run pack:win"
}
}

BIN
apps/desktop/sign.ps1 Normal file

Binary file not shown.

View File

@@ -4,8 +4,12 @@ import type { autofill } from "@bitwarden/desktop-napi";
import { Command } from "../platform/main/autofill/command";
import { RunCommandParams, RunCommandResult } from "../platform/main/autofill/native-autofill.main";
import { NativeAutofillSyncParams } from "../platform/main/autofill/sync.command";
export default {
syncPasskeys: (params: NativeAutofillSyncParams): Promise<string> => ipcRenderer.invoke("autofill.syncPasskeys", params),
runCommand: <C extends Command>(params: RunCommandParams<C>): Promise<RunCommandResult<C>> =>
ipcRenderer.invoke("autofill.runCommand", params),

View File

@@ -61,10 +61,7 @@ export class DesktopAutofillService implements OnDestroy {
.pipe(
distinctUntilChanged(),
switchMap((enabled) => {
if (!enabled) {
return EMPTY;
}
return this.accountService.activeAccount$.pipe(
map((account) => account?.id),
filter((userId): userId is UserId => userId != null),
@@ -80,47 +77,40 @@ export class DesktopAutofillService implements OnDestroy {
.subscribe();
this.listenIpc();
}
/** Give metadata about all available credentials in the users vault */
async sync(cipherViews: CipherView[]) {
const status = await this.status();
if (status.type === "error") {
return this.logService.error("Error getting autofill status", status.error);
}
this.logService.info("Syncing autofill credentials: ", cipherViews.length);
// const status = await this.status();
// if (status.type === "error") {
// return this.logService.error("Error getting autofill status", status.error);
// }
if (!status.value.state.enabled) {
// Autofill is disabled
return;
}
// if (!status.value.state.enabled) {
// // Autofill is disabled
// return;
// }
let fido2Credentials: NativeAutofillFido2Credential[];
let passwordCredentials: NativeAutofillPasswordCredential[];
if (status.value.support.password) {
passwordCredentials = cipherViews
.filter(
(cipher) =>
cipher.type === CipherType.Login &&
cipher.login.uris?.length > 0 &&
cipher.login.uris.some((uri) => uri.match !== UriMatchStrategy.Never) &&
cipher.login.uris.some((uri) => !Utils.isNullOrWhitespace(uri.uri)) &&
!Utils.isNullOrWhitespace(cipher.login.username),
)
.map((cipher) => ({
type: "password",
cipherId: cipher.id,
uri: cipher.login.uris.find((uri) => uri.match !== UriMatchStrategy.Never).uri,
username: cipher.login.username,
}));
}
if (status.value.support.fido2) {
fido2Credentials = (await getCredentialsForAutofill(cipherViews)).map((credential) => ({
fido2Credentials = (await getCredentialsForAutofill(cipherViews)).map((credential) => ({
type: "fido2",
...credential,
}));
}
this.logService.info("Found FIDO2 credentials", fido2Credentials.length);
console.log("ipc.autofill",ipc.autofill);
console.log("ipc.autofill.syncpasskeys", ipc.autofill.syncPasskeys);
const res = await ipc.autofill.syncPasskeys({
credentials: [...fido2Credentials],
});
this.logService.warning("syncPasskeys result", res);
const syncResult = await ipc.autofill.runCommand<NativeAutofillSyncCommand>({
namespace: "autofill",

View File

@@ -3,8 +3,6 @@ import * as path from "path";
import { app } from "electron";
import { passkey_authenticator } from "@bitwarden/desktop-napi";
if (
process.platform === "darwin" &&
process.argv.some((arg) => arg.indexOf("chrome-extension://") !== -1 || arg.indexOf("{") !== -1)
@@ -42,8 +40,6 @@ if (
// eslint-disable-next-line
const Main = require("./main").Main;
passkey_authenticator.register();
const main = new Main();
main.bootstrap();
}

View File

@@ -2,7 +2,7 @@
"name": "@bitwarden/desktop",
"productName": "Bitwarden",
"description": "A secure and free password manager for all of your devices.",
"version": "2025.6.0",
"version": "2025.6.17",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"license": "GPL-3.0",

View File

@@ -1,11 +1,12 @@
import { ipcMain } from "electron";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { autofill } from "@bitwarden/desktop-napi";
import { autofill, passkey_authenticator } from "@bitwarden/desktop-napi";
import { WindowMain } from "../../../main/window.main";
import { CommandDefinition } from "./command";
import { NativeAutofillFido2Credential, NativeAutofillSyncParams } from "./sync.command";
export type RunCommandParams<C extends CommandDefinition> = {
namespace: C["namespace"];
@@ -17,13 +18,278 @@ export type RunCommandResult<C extends CommandDefinition> = C["output"];
export class NativeAutofillMain {
private ipcServer: autofill.IpcServer | null;
private pendingPasskeyRequests = new Map<string, (response: any) => void>();
constructor(
private logService: LogService,
private windowMain: WindowMain,
) {}
initWindows() {
passkey_authenticator.register();
void passkey_authenticator.onRequest(async (error, event) => {
this.logService.info("Passkey request received:", { error, event });
try {
const request = JSON.parse(event.requestJson);
this.logService.info("Parsed passkey request:", { type: event.requestType, request });
// Handle different request types based on the requestType field
switch (event.requestType) {
case "assertion":
return await this.handleAssertionRequest(request);
case "registration":
return await this.handleRegistrationRequest(request);
case "sync":
return await this.handleSyncRequest(request);
default:
this.logService.error("Unknown passkey request type:", event.requestType);
return JSON.stringify({
type: "error",
message: `Unknown request type: ${event.requestType}`,
});
}
} catch (parseError) {
this.logService.error("Failed to parse passkey request:", parseError);
return JSON.stringify({
type: "error",
message: "Failed to parse request JSON",
});
}
});
}
private async handleAssertionRequest(
request: passkey_authenticator.PasskeyAssertionRequest,
): Promise<string> {
this.logService.info("Handling assertion request for rpId:", request.rpId);
const normalized_request: autofill.PasskeyAssertionRequest = {
rpId: request.rpId,
allowedCredentials: request.allowedCredentials,
clientDataHash: request.clientDataHash,
userVerification: autofill.UserVerification.Required,
windowXy: { x: 400, y: 400 },
};
try {
// Generate unique identifiers for tracking this request
const clientId = Date.now();
const sequenceNumber = Math.floor(Math.random() * 1000000);
// Send request and wait for response
const response = await this.sendAndOptionallyWait<autofill.PasskeyAssertionResponse>(
"autofill.passkeyAssertion",
{
clientId,
sequenceNumber,
request: normalized_request,
},
{ waitForResponse: true, timeout: 60000 },
);
if (response) {
// Convert the response to the format expected by the NAPI bridge
return JSON.stringify({
type: "assertion_response",
credentialId: response.credentialId,
authenticatorData: response.authenticatorData,
signature: response.signature,
userHandle: response.userHandle,
});
} else {
return JSON.stringify({
type: "error",
message: "No response received from renderer",
});
}
} catch (error) {
this.logService.error("Error in assertion request:", error);
return JSON.stringify({
type: "error",
message: `Assertion request failed: ${error.message}`,
});
}
}
private async handleRegistrationRequest(
request: passkey_authenticator.PasskeyRegistrationRequest,
): Promise<string> {
this.logService.info("Handling registration request for rpId:", request.rpId);
const normalized_request: autofill.PasskeyRegistrationRequest = {
rpId: request.rpId,
clientDataHash: request.clientDataHash,
userName: request.userName,
userHandle: request.userId,
userVerification: autofill.UserVerification.Required,
supportedAlgorithms: request.supportedAlgorithms,
windowXy: { x: 400, y: 400 },
};
try {
// Generate unique identifiers for tracking this request
const clientId = Date.now();
const sequenceNumber = Math.floor(Math.random() * 1000000);
// Send request and wait for response
const response = await this.sendAndOptionallyWait<autofill.PasskeyRegistrationResponse>(
"autofill.passkeyRegistration",
{
clientId,
sequenceNumber,
request: normalized_request,
},
{ waitForResponse: true, timeout: 60000 },
);
this.logService.info("Received response for registration request:", response);
if (response) {
// Convert the response to the format expected by the NAPI bridge
return JSON.stringify({
type: "registration_response",
credentialId: response.credentialId,
attestationObject: response.attestationObject,
});
} else {
return JSON.stringify({
type: "error",
message: "No response received from renderer",
});
}
} catch (error) {
this.logService.error("Error in registration request:", error);
return JSON.stringify({
type: "error",
message: `Registration request failed: ${error.message}`,
});
}
}
private async handleSyncRequest(
request: passkey_authenticator.PasskeySyncRequest,
): Promise<string> {
this.logService.info("Handling sync request for rpId:", request.rpId);
try {
// Generate unique identifiers for tracking this request
const clientId = Date.now();
const sequenceNumber = Math.floor(Math.random() * 1000000);
// Send sync request and wait for response
const response = await this.sendAndOptionallyWait<passkey_authenticator.PasskeySyncResponse>(
"autofill.passkeySync",
{
clientId,
sequenceNumber,
request: { rpId: request.rpId },
},
{ waitForResponse: true, timeout: 60000 },
);
this.logService.info("Received response for sync request:", response);
if (response && response.credentials) {
// Convert the response to the format expected by the NAPI bridge
return JSON.stringify({
type: "sync_response",
credentials: response.credentials,
});
} else {
return JSON.stringify({
type: "error",
message: "No credentials received from renderer",
});
}
} catch (error) {
this.logService.error("Error in sync request:", error);
return JSON.stringify({
type: "error",
message: `Sync request failed: ${error.message}`,
});
}
}
/**
* Wrapper for webContents.send that optionally waits for a response
* @param channel The IPC channel to send to
* @param data The data to send
* @param options Optional configuration
* @returns Promise that resolves with the response if waitForResponse is true
*/
private async sendAndOptionallyWait<T = any>(
channel: string,
data: any,
options?: { waitForResponse?: boolean; timeout?: number; responseChannel?: string },
): Promise<T | void> {
if (!options?.waitForResponse) {
// Just send without waiting for response (existing behavior)
this.logService.info(`Sending fire-and-forget message to ${channel}`);
this.windowMain.win.webContents.send(channel, data);
return;
}
// Use clientId and sequenceNumber as the tracking key
const trackingKey = `${data.clientId}_${data.sequenceNumber}`;
const responseChannel = options.responseChannel || `${channel}_response`;
const timeout = options.timeout || 30000; // 30 second default timeout
// Send the original data without adding requestId
const dataWithId = { ...data };
this.logService.info(`Sending awaitable request ${trackingKey} to ${channel}`, { dataWithId });
return new Promise<T>((resolve, reject) => {
// Set up timeout
const timeoutId = setTimeout(() => {
this.logService.warning(`Request ${trackingKey} timed out after ${timeout}ms`);
this.pendingPasskeyRequests.delete(trackingKey);
reject(new Error(`Request timeout after ${timeout}ms`));
}, timeout);
// Store the resolver
this.pendingPasskeyRequests.set(trackingKey, (response: T) => {
this.logService.info(`Request ${trackingKey} resolved with response:`, response);
clearTimeout(timeoutId);
this.pendingPasskeyRequests.delete(trackingKey);
resolve(response);
});
this.logService.info(
`Stored resolver for request ${trackingKey}, total pending: ${this.pendingPasskeyRequests.size}`,
);
// Send the request
this.windowMain.win.webContents.send(channel, dataWithId);
});
}
async init() {
this.initWindows();
ipcMain.handle("autofill.syncPasskeys", async (event, data: NativeAutofillSyncParams): Promise<string> => {
this.logService.info("autofill.syncPasskeys", data);
const { credentials } = data;
const mapped = credentials.map((cred: NativeAutofillFido2Credential) => {
const x: passkey_authenticator.SyncedCredential = {
credentialId: cred.credentialId,
rpId: cred.rpId,
userName: cred.userName,
userId: cred.userHandle
};
this.logService.info("Mapped credential:", x);
return x;
});
this.logService.info("Syncing passkeys to Windows:", mapped);
passkey_authenticator.syncCredentialsToWindows(mapped);
return "worked";
});
ipcMain.handle(
"autofill.runCommand",
<C extends CommandDefinition>(
@@ -79,23 +345,103 @@ export class NativeAutofillMain {
ipcMain.on("autofill.completePasskeyRegistration", (event, data) => {
this.logService.warning("autofill.completePasskeyRegistration", data);
const { clientId, sequenceNumber, response } = data;
this.ipcServer.completeRegistration(clientId, sequenceNumber, response);
const { clientId, sequenceNumber, response, requestId } = data;
// Handle both IpcServer and awaitable requests
if (this.ipcServer && clientId !== -1) {
this.ipcServer.completeRegistration(clientId, sequenceNumber, response);
}
// Handle awaitable passkey requests using clientId and sequenceNumber
if (clientId !== undefined && sequenceNumber !== undefined) {
const trackingKey = `${clientId}_${sequenceNumber}`;
this.handlePasskeyResponse(trackingKey, response);
}
// Fallback to requestId for backward compatibility
else if (requestId) {
this.handlePasskeyResponse(requestId, response);
}
});
ipcMain.on("autofill.completePasskeyAssertion", (event, data) => {
this.logService.warning("autofill.completePasskeyAssertion", data);
const { clientId, sequenceNumber, response } = data;
this.ipcServer.completeAssertion(clientId, sequenceNumber, response);
const { clientId, sequenceNumber, response, requestId } = data;
// Handle both IpcServer and awaitable requests
if (this.ipcServer && clientId !== -1) {
this.ipcServer.completeAssertion(clientId, sequenceNumber, response);
}
// Handle awaitable passkey requests using clientId and sequenceNumber
if (clientId !== undefined && sequenceNumber !== undefined) {
const trackingKey = `${clientId}_${sequenceNumber}`;
this.handlePasskeyResponse(trackingKey, response);
}
// Fallback to requestId for backward compatibility
else if (requestId) {
this.handlePasskeyResponse(requestId, response);
}
});
ipcMain.on("autofill.completePasskeySync", (event, data) => {
this.logService.warning("autofill.completePasskeySync", data);
const { clientId, sequenceNumber, response, requestId } = data;
// Handle awaitable passkey requests using clientId and sequenceNumber
if (clientId !== undefined && sequenceNumber !== undefined) {
const trackingKey = `${clientId}_${sequenceNumber}`;
this.handlePasskeyResponse(trackingKey, response);
}
// Fallback to requestId for backward compatibility
else if (requestId) {
this.handlePasskeyResponse(requestId, response);
}
});
ipcMain.on("autofill.completeError", (event, data) => {
this.logService.warning("autofill.completeError", data);
const { clientId, sequenceNumber, error } = data;
this.ipcServer.completeError(clientId, sequenceNumber, String(error));
const { clientId, sequenceNumber, error, requestId } = data;
// Handle both IpcServer and awaitable requests
if (this.ipcServer && clientId !== -1) {
this.ipcServer.completeError(clientId, sequenceNumber, String(error));
}
// Handle awaitable passkey requests using clientId and sequenceNumber
if (clientId !== undefined && sequenceNumber !== undefined) {
const trackingKey = `${clientId}_${sequenceNumber}`;
this.handlePasskeyResponse(trackingKey, { error: String(error) });
}
// Fallback to requestId for backward compatibility
else if (requestId) {
this.handlePasskeyResponse(requestId, { error: String(error) });
}
});
}
private handlePasskeyResponse(trackingKey: string, response: any): void {
this.logService.info("Received passkey response for tracking key:", trackingKey, response);
if (!trackingKey) {
this.logService.error("Response missing tracking key:", response);
return;
}
this.logService.info(`Looking for pending request with tracking key: ${trackingKey}`);
this.logService.info(
`Current pending requests: ${Array.from(this.pendingPasskeyRequests.keys())}`,
);
const resolver = this.pendingPasskeyRequests.get(trackingKey);
if (resolver) {
this.logService.info("Found resolver, calling with response data:", response);
resolver(response);
} else {
this.logService.warning("No pending request found for tracking key:", trackingKey);
}
}
private async runCommand<C extends CommandDefinition>(
command: RunCommandParams<C>,
): Promise<RunCommandResult<C>> {