mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 06:23:38 +00:00
draft
draft
This commit is contained in:
BIN
apps/desktop/com.bitwarden.pfx
Normal file
BIN
apps/desktop/com.bitwarden.pfx
Normal file
Binary file not shown.
@@ -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>
|
||||
|
||||
47
apps/desktop/desktop_native/Cargo.lock
generated
47
apps/desktop/desktop_native/Cargo.lock
generated
@@ -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)",
|
||||
]
|
||||
|
||||
48
apps/desktop/desktop_native/napi/index.d.ts
vendored
48
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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
BIN
apps/desktop/sign.ps1
Normal file
Binary file not shown.
@@ -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),
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
Reference in New Issue
Block a user