diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 39e968d941b..d372a39b1e7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,7 +8,9 @@ apps/desktop/desktop_native @bitwarden/team-platform-dev apps/desktop/desktop_native/objc/src/native/autofill @bitwarden/team-autofill-desktop-dev apps/desktop/desktop_native/core/src/autofill @bitwarden/team-autofill-desktop-dev +apps/desktop/desktop_native/macos_provider @bitwarden/team-autofill-desktop-dev apps/desktop/desktop_native/core/src/secure_memory @bitwarden/team-key-management-dev + ## No ownership for Cargo.lock and Cargo.toml to allow dependency updates apps/desktop/desktop_native/Cargo.lock apps/desktop/desktop_native/Cargo.toml diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 39549c4580c..70ed55c08c4 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -908,7 +908,7 @@ jobs: macos-build: name: MacOS Build - runs-on: macos-13 + runs-on: macos-15 needs: - setup permissions: @@ -935,8 +935,13 @@ jobs: cache-dependency-path: '**/package-lock.json' node-version: ${{ env._NODE_VERSION }} + - name: Set up Python + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + with: + python-version: '3.13' + - name: Set up Node-gyp - run: python3 -m pip install setuptools + run: python -m pip install setuptools - name: Print environment run: | @@ -945,6 +950,7 @@ jobs: rustup show echo "GitHub ref: $GITHUB_REF" echo "GitHub event: $GITHUB_EVENT" + xcodebuild -showsdks - name: Cache Build id: build-cache @@ -1132,7 +1138,7 @@ jobs: macos-package-github: name: MacOS Package GitHub Release Assets - runs-on: macos-13 + runs-on: macos-15 if: ${{ needs.setup.outputs.has_secrets == 'true' }} needs: - browser-build @@ -1162,8 +1168,13 @@ jobs: cache-dependency-path: '**/package-lock.json' node-version: ${{ env._NODE_VERSION }} + - name: Set up Python + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + with: + python-version: '3.13' + - name: Set up Node-gyp - run: python3 -m pip install setuptools + run: python -m pip install setuptools - name: Print environment run: | @@ -1172,6 +1183,7 @@ jobs: rustup show echo "GitHub ref: $GITHUB_REF" echo "GitHub event: $GITHUB_EVENT" + xcodebuild -showsdks - name: Get Build Cache id: build-cache @@ -1393,7 +1405,7 @@ jobs: macos-package-mas: name: MacOS Package Prod Release Asset - runs-on: macos-13 + runs-on: macos-15 if: ${{ needs.setup.outputs.has_secrets == 'true' }} needs: - browser-build @@ -1423,8 +1435,13 @@ jobs: cache-dependency-path: '**/package-lock.json' node-version: ${{ env._NODE_VERSION }} + - name: Set up Python + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + with: + python-version: '3.13' + - name: Set up Node-gyp - run: python3 -m pip install setuptools + run: python -m pip install setuptools - name: Print environment run: | @@ -1433,6 +1450,7 @@ jobs: rustup show echo "GitHub ref: $GITHUB_REF" echo "GitHub event: $GITHUB_EVENT" + xcodebuild -showsdks - name: Get Build Cache id: build-cache diff --git a/.vscode/settings.json b/.vscode/settings.json index 8f89bc03b8c..3d6870b2f44 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,6 @@ }, "rust-analyzer.linkedProjects": ["apps/desktop/desktop_native/Cargo.toml"], "typescript.tsdk": "node_modules/typescript/lib", - "eslint.useFlatConfig": true + "eslint.useFlatConfig": true, + "rust-analyzer.server.path": null } diff --git a/apps/browser/src/autofill/popup/fido2/fido2.component.ts b/apps/browser/src/autofill/popup/fido2/fido2.component.ts index c6799f93a5e..c1982d27d24 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2.component.ts @@ -24,6 +24,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Fido2Utils } from "@bitwarden/common/platform/services/fido2/fido2-utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { SecureNoteType, CipherType } from "@bitwarden/common/vault/enums"; @@ -198,7 +199,7 @@ export class Fido2Component implements OnInit, OnDestroy { this.displayedCiphers = this.ciphers.filter( (cipher) => cipher.login.matchesUri(this.url, equivalentDomains) && - this.cipherHasNoOtherPasskeys(cipher, message.userHandle), + Fido2Utils.cipherHasNoOtherPasskeys(cipher, message.userHandle), ); this.passkeyAction = PasskeyActions.Register; @@ -472,16 +473,4 @@ export class Fido2Component implements OnInit, OnDestroy { ...msg, }); } - - /** - * This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle - * @param userHandle - */ - private cipherHasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean { - if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) { - return true; - } - - return cipher.login.fido2Credentials.some((passkey) => passkey.userHandle === userHandle); - } } diff --git a/apps/desktop/.vscode/launch.json b/apps/desktop/.vscode/launch.json index 66c1161be46..df54c4e5ba2 100644 --- a/apps/desktop/.vscode/launch.json +++ b/apps/desktop/.vscode/launch.json @@ -6,11 +6,28 @@ "type": "node", "request": "launch", "cwd": "${workspaceRoot}/build", - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", + "runtimeExecutable": "${workspaceRoot}/../../node_modules/.bin/electron", "windows": { - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" + "runtimeExecutable": "${workspaceRoot}/../../node_modules/.bin/electron.cmd" }, - "args": ["."] + "args": [".", "--remote-debugging-port=9223"] + }, + { + "name": "Debug Renderer Process", + "type": "chrome", + "request": "attach", + "port": 9223, + "webRoot": "${workspaceFolder}", + "timeout": 30000, + } + ], + "compounds": [ + { + "name": "Debug Electron: All", + "configurations": [ + "Debug Main Process", + "Debug Renderer Process" + ] } ] } diff --git a/apps/desktop/build.ps1 b/apps/desktop/build.ps1 new file mode 100644 index 00000000000..5dc0cd60f23 --- /dev/null +++ b/apps/desktop/build.ps1 @@ -0,0 +1,31 @@ +$ErrorActionPreference = "Stop" +$PSNativeCommandUseErrorActionPreference = $true + +if ($null -eq $env:ELECTRON_BUILDER_SIGN_CERT) { + $env:ELECTRON_BUILDER_SIGN_CERT = "C:\temp\code-signing.pfx" +} +if ($null -eq $env:ELECTRON_BUILDER_SIGN_CERT_PW) { + $env:ELECTRON_BUILDER_SIGN_CERT_PW = "1234" +} +$bwFolder = "$env:LOCALAPPDATA\Packages\bitwardendesktop_jhp7wx9v9pf64" + +$package = (Get-AppxPackage -name bitwardendesktop) +$appx = ".\dist\Bitwarden-2025.10.2-arm64.appx" +$backupDataFile = "C:\temp\bw-data.json" +$comLogFile = "C:\temp\bitwarden_com_debug.log" + +# Build Appx +npm run build-native && npm run build:dev && npm run pack:win:appx:arm64 + +# Backup tokens +Copy-Item -Path "$bwFolder\LocalCache\Roaming\Bitwarden\data.json" -Destination $backupDataFile + +# Reinstall Appx +Remove-AppxPackage $package && Add-AppxPackage $appx + +# Delete log files +Remove-Item -Path $comLogFile -Force -ErrorAction SilentlyContinue + +# Restore tokens +New-Item -Type Directory -Force -Path "$bwFolder\LocalCache\Roaming\Bitwarden\" +Copy-Item -Path $backupDataFile -Destination "$bwFolder\LocalCache\Roaming\Bitwarden\data.json" diff --git a/apps/desktop/cross-build.ps1 b/apps/desktop/cross-build.ps1 new file mode 100755 index 00000000000..54df02b41d8 --- /dev/null +++ b/apps/desktop/cross-build.ps1 @@ -0,0 +1,110 @@ +#!/usr/bin/env pwsh +param( + $CertificatePath, + $CertificatePassword, + $ElectronConfigFile="electron-builder.json", + $Target="debug" +) +$ErrorActionPreference = "Stop" +$PSNativeCommandUseErrorActionPreference = $true +$startTime = get-Date +$originalLocation = Get-Location +try { + +cd $PSScriptRoot + +$builderConfig = Get-Content $ElectronConfigFile | ConvertFrom-Json +$packageConfig = Get-Content package.json | ConvertFrom-Json +$manifestTemplate = Get-Content custom-appx-manifest.xml + +$srcDir = Get-Location +$assetsDir = Get-Item $builderConfig.directories.buildResources +$buildDir = Get-Item $builderConfig.directories.app +$outDir = Join-Path (Get-Location) ($builderConfig.directories.output ?? "dist") + +if ($target -eq "release") { + $targetStr = "--release" +} +$arch = 'arm64' +$ext = "appx" +$version = Get-Date -Format "yyyy.M.d.Hmm" +# $buildNumber = Get-Date -Format "HHmm" +# $version = "$($packageConfig.version).$buildNumber" +$productName = $builderConfig.productName +$artifactName = "${productName}-$($packageConfig.version)-${arch}.$ext" + +Write-Host "Building native code" +npm run build-native-win-cross -- $targetStr + +Write-Host "Building Javascript code" +if ($target -eq "release") { + npm run build +} +else { + npm run build:dev +} + +Write-Host "Cleaning output folder" +Remove-Item -Recurse -Force $outDir -ErrorAction Ignore + +Write-Host "Packaging Electron executable" +& npx electron-builder --config $ElectronConfigFile --publish never --dir --win --$arch + +cd $outDir +New-Item -Type Directory (Join-Path $outDir "appx") + +Write-Host "Building Appx directory structure" +$appxDir = (Join-Path $outDir appx/app) +Move-Item (Join-Path $outDir "win-${arch}-unpacked") $appxDir + +Write-Host "Copying Assets" +New-Item -Type Directory (Join-Path $outDir appx/assets) +Copy-Item $srcDir/resources/appx/* $outDir/appx/assets/ + +Write-Host "Building Appx manifest" +$translationMap = @{ + 'arch' = $arch + 'applicationId' = $builderConfig.appx.applicationId + 'displayName' = $productName + 'executable' = "app\${productName}.exe" + 'publisher' = $builderConfig.appx.publisher + 'publisherDisplayName' = $builderConfig.appx.publisherDisplayName + 'version' = $version +} + +$manifest = $manifestTemplate +$translationMap.Keys | ForEach-Object { + $manifest = $manifest.Replace("`${$_}", $translationMap[$_]) +} +$manifest | Out-File appx/AppxManifest.xml +$unsignedArtifactpath = [System.IO.Path]::GetFileNameWithoutExtension($artifactName) + "-unsigned.$ext" +Write-Host "Creating unsigned Appx" +makemsix pack -d appx -p $unsignedArtifactpath + +$outfile = Join-Path $outDir $unsignedArtifactPath +if ($null -eq $CertificatePath || $null -eq $CertificatePassword) { + Write-Warning "No Certificate specified. Not signing Appx." +} +else { + + $cert = (Get-Item $CertificatePath).FullName + $pw = $CertificatePassword + $unsigned = $outfile + $outfile = (Join-Path $outDir $artifactName) + Write-Host "Signing $artifactName with $cert" + osslsigncode sign ` + -pkcs12 "$cert" ` + -pass "$pw" ` + -in $unsigned ` + -out $outfile + Remove-Item $unsigned +} + +$endTime = Get-Date +$elapsed = $endTime - $startTime +Write-Host "Successfully packaged $(Get-Item $outfile)" +Write-Host ("Finished in $($elapsed.ToString('mm')) minutes and $($elapsed.ToString('ss')).$($elapsed.ToString('fff')) seconds") +} +finally { + Set-Location -Path $originalLocation +} \ No newline at end of file diff --git a/apps/desktop/custom-appx-manifest.xml b/apps/desktop/custom-appx-manifest.xml index c108e060e9d..dce447ea2bf 100644 --- a/apps/desktop/custom-appx-manifest.xml +++ b/apps/desktop/custom-appx-manifest.xml @@ -4,22 +4,22 @@ xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" - xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"> --> +xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"> --> + xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" + xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" + xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10" + 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"> + ProcessorArchitecture="${arch}" + Publisher='${publisher}' + Version="${version}" /> - Bitwarden - Bitwarden Inc + ${displayName} + ${publisherDisplayName} A secure and free password manager for all of your devices. assets\StoreLogo.png @@ -94,11 +94,11 @@ xmlns:build="http://schemas.microsoft.com/developer/appx/2015/build"> - @@ -109,12 +109,13 @@ xmlns:build="http://schemas.microsoft.com/developer/appx/2015/build"> - Bitwarden + ${displayName} - + diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 4182de59382..f5b21e57178 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -344,8 +344,8 @@ dependencies = [ "mockall", "serial_test", "tracing", - "windows 0.62.2", - "windows-core 0.62.2", + "windows 0.61.3", + "windows-core 0.61.2", ] [[package]] @@ -452,7 +452,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", - "windows 0.62.2", + "windows 0.61.3", ] [[package]] @@ -496,12 +496,6 @@ dependencies = [ "cipher", ] -[[package]] -name = "bumpalo" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" - [[package]] name = "byteorder" version = "1.5.0" @@ -617,7 +611,7 @@ dependencies = [ "pbkdf2", "rand 0.9.1", "rusqlite", - "security-framework 3.5.0", + "security-framework", "serde", "serde_json", "sha1", @@ -625,7 +619,7 @@ dependencies = [ "tracing", "tracing-subscriber", "verifysign", - "windows 0.62.2", + "windows 0.61.3", ] [[package]] @@ -756,16 +750,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation" version = "0.10.1" @@ -988,7 +972,7 @@ dependencies = [ "bytes", "cbc", "chacha20poly1305", - "core-foundation 0.10.1", + "core-foundation", "desktop_objc", "dirs", "ed25519", @@ -1007,7 +991,7 @@ dependencies = [ "russh-cryptovec", "scopeguard", "secmem-proc", - "security-framework 3.5.0", + "security-framework", "security-framework-sys", "serde", "serde_json", @@ -1022,8 +1006,9 @@ dependencies = [ "tracing", "typenum", "widestring", - "windows 0.62.2", - "windows-future 0.3.2", + "win_webauthn", + "windows 0.61.3", + "windows-future", "zbus", "zbus_polkit", "zeroizing-alloc", @@ -1050,7 +1035,7 @@ dependencies = [ "tokio-util", "tracing", "tracing-subscriber", - "windows-registry 0.6.1", + "windows-registry", "windows_plugin_authenticator", ] @@ -1060,7 +1045,7 @@ version = "0.0.0" dependencies = [ "anyhow", "cc", - "core-foundation 0.10.1", + "core-foundation", "glob", "thiserror 2.0.12", "tokio", @@ -1248,15 +1233,6 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - [[package]] name = "endi" version = "1.1.0" @@ -1367,12 +1343,6 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "foldhash" version = "0.1.5" @@ -1385,21 +1355,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1614,25 +1569,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "h2" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "half" version = "2.7.1" @@ -1722,126 +1658,6 @@ dependencies = [ "windows 0.57.0", ] -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2 0.6.1", - "system-configuration", - "tokio", - "tower-service", - "tracing", - "windows-registry 0.5.3", -] - [[package]] name = "icu_collections" version = "2.1.1" @@ -1979,22 +1795,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -2007,16 +1807,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" -[[package]] -name = "js-sys" -version = "0.3.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - [[package]] name = "keytar" version = "0.1.6" @@ -2330,23 +2120,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework 2.11.1", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nix" version = "0.29.0" @@ -2633,50 +2406,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "openssl" -version = "0.10.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-sys" -version = "0.9.110" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "option-ext" version = "0.2.0" @@ -3171,48 +2900,6 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" -[[package]] -name = "reqwest" -version = "0.12.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-tls", - "hyper-util", - "js-sys", - "log", - "mime", - "native-tls", - "percent-encoding", - "pin-project-lite", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-native-tls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "rfc6979" version = "0.4.0" @@ -3223,20 +2910,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ring" -version = "0.17.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e75ec5e92c4d8aede845126adc388046234541629e76029599ed35a003c7ed24" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - [[package]] name = "rsa" version = "0.9.6" @@ -3333,45 +3006,6 @@ dependencies = [ "rustix 1.1.2", ] -[[package]] -name = "rustls" -version = "0.23.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" -dependencies = [ - "once_cell", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - [[package]] name = "ryu" version = "1.0.20" @@ -3396,15 +3030,6 @@ dependencies = [ "sdd", ] -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -3483,19 +3108,6 @@ dependencies = [ "windows 0.61.3", ] -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - [[package]] name = "security-framework" version = "3.5.0" @@ -3503,7 +3115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc198e42d9b7510827939c9a15f5062a0c913f3371d765977e586d2fe6c16f4a" dependencies = [ "bitflags", - "core-foundation 0.10.1", + "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", @@ -3580,18 +3192,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "serial_test" version = "3.2.0" @@ -3707,16 +3307,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "socket2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - [[package]] name = "spin" version = "0.9.8" @@ -3816,15 +3406,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - [[package]] name = "synstructure" version = "0.13.2" @@ -3850,27 +3431,6 @@ dependencies = [ "windows 0.61.3", ] -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "tempfile" version = "3.23.0" @@ -3980,7 +3540,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.10", + "socket2", "tokio-macros", "tracing", "windows-sys 0.52.0", @@ -3997,26 +3557,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - [[package]] name = "tokio-stream" version = "0.1.15" @@ -4101,51 +3641,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - [[package]] name = "tracing" version = "0.1.41" @@ -4231,12 +3726,6 @@ dependencies = [ "petgraph", ] -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - [[package]] name = "typenum" version = "1.18.0" @@ -4418,12 +3907,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - [[package]] name = "url" version = "2.5.7" @@ -4466,7 +3949,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ebfe12e38930c3b851aea35e93fab1a6c29279cad7e8e273f29a21678fee8c0" dependencies = [ - "core-foundation 0.10.1", + "core-foundation", "sha1", "sha2", "windows-sys 0.61.2", @@ -4498,15 +3981,6 @@ dependencies = [ "libc", ] -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -4522,64 +3996,6 @@ dependencies = [ "wit-bindgen", ] -[[package]] -name = "wasm-bindgen" -version = "0.2.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" -dependencies = [ - "unicode-ident", -] - [[package]] name = "wayland-backend" version = "0.3.11" @@ -4650,16 +4066,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "web-sys" -version = "0.3.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "weedle2" version = "5.0.0" @@ -4675,6 +4081,18 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" +[[package]] +name = "win_webauthn" +version = "0.0.0" +dependencies = [ + "base64", + "ciborium", + "hex", + "tracing", + "windows 0.61.3", + "windows-core 0.61.2", +] + [[package]] name = "winapi" version = "0.3.9" @@ -4722,23 +4140,11 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections 0.2.0", + "windows-collections", "windows-core 0.61.2", - "windows-future 0.2.1", + "windows-future", "windows-link 0.1.3", - "windows-numerics 0.2.0", -] - -[[package]] -name = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections 0.3.2", - "windows-core 0.62.2", - "windows-future 0.3.2", - "windows-numerics 0.3.1", + "windows-numerics", ] [[package]] @@ -4750,15 +4156,6 @@ dependencies = [ "windows-core 0.61.2", ] -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core 0.62.2", -] - [[package]] name = "windows-core" version = "0.57.0" @@ -4784,19 +4181,6 @@ dependencies = [ "windows-strings 0.4.2", ] -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - [[package]] name = "windows-future" version = "0.2.1" @@ -4805,18 +4189,7 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading 0.1.0", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core 0.62.2", - "windows-link 0.2.1", - "windows-threading 0.2.1", + "windows-threading", ] [[package]] @@ -4885,27 +4258,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core 0.62.2", - "windows-link 0.2.1", -] - -[[package]] -name = "windows-registry" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" -dependencies = [ - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - [[package]] name = "windows-registry" version = "0.6.1" @@ -5064,15 +4416,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -5161,15 +4504,17 @@ checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" name = "windows_plugin_authenticator" version = "0.0.0" dependencies = [ + "base64", "ciborium", - "hex", - "reqwest", + "desktop_core", + "futures", "serde", "serde_json", - "sha2", "tokio", - "windows 0.62.2", - "windows-core 0.62.2", + "tracing", + "win_webauthn", + "windows 0.61.3", + "windows-core 0.61.2", ] [[package]] diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 1c0b8b7bed9..435330a01ba 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -8,7 +8,7 @@ members = [ "macos_provider", "napi", "process_isolation", - "proxy", + "proxy", "win_webauthn", "windows_plugin_authenticator" ] @@ -77,9 +77,9 @@ tracing-subscriber = { version = "=0.3.20", features = [ typenum = "=1.18.0" uniffi = "=0.28.3" widestring = "=1.2.0" -windows = { version = "=0.62.2", features = ["Win32_System_Threading"] } -windows-core = "=0.62.2" -windows-future = "=0.3.2" +windows = { version = "=0.61.3", features = ["Win32_System_Threading"] } +windows-core = "=0.61.2" +windows-future = "=0.2.1" windows-registry = "=0.6.1" zbus = "=5.11.0" zbus_polkit = "=5.0.0" diff --git a/apps/desktop/desktop_native/build.js b/apps/desktop/desktop_native/build.js index a7ed89a9c17..9294b45b69b 100644 --- a/apps/desktop/desktop_native/build.js +++ b/apps/desktop/desktop_native/build.js @@ -34,14 +34,15 @@ function buildNapiModule(target, release = true) { function buildProxyBin(target, release = true) { const targetArg = target ? `--target ${target}` : ""; const releaseArg = release ? "--release" : ""; - child_process.execSync(`cargo build --bin desktop_proxy ${releaseArg} ${targetArg}`, {stdio: 'inherit', cwd: path.join(__dirname, "proxy")}); + const xwin = target && target.includes('windows') && process.platform !== "win32" ? "xwin" : ""; + child_process.execSync(`cargo ${xwin} build --bin desktop_proxy ${releaseArg} ${targetArg}`, {stdio: 'inherit', cwd: path.join(__dirname, "proxy")}); if (target) { // Copy the resulting binary to the dist folder const targetFolder = release ? "release" : "debug"; - const ext = process.platform === "win32" ? ".exe" : ""; - const nodeArch = rustTargetsMap[target].nodeArch; - fs.copyFileSync(path.join(__dirname, "target", target, targetFolder, `desktop_proxy${ext}`), path.join(__dirname, "dist", `desktop_proxy.${process.platform}-${nodeArch}${ext}`)); + const { nodeArch, platform } = rustTargetsMap[target]; + const ext = platform === "win32" ? ".exe" : ""; + fs.copyFileSync(path.join(__dirname, "target", target, targetFolder, `desktop_proxy${ext}`), path.join(__dirname, "dist", `desktop_proxy.${platform}-${nodeArch}${ext}`)); } } diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index f6c9d669df6..491c8b7a5f9 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -69,12 +69,15 @@ windows = { workspace = true, features = [ "Win32_Foundation", "Win32_Security_Credentials", "Win32_Security_Cryptography", + "Win32_System_Com", + "Win32_System_LibraryLoader", "Win32_System_WinRT", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_WindowsAndMessaging", "Win32_System_Pipes", ], optional = true } windows-future = { workspace = true } +win_webauthn = { path = "../win_webauthn" } [target.'cfg(windows)'.dev-dependencies] keytar = { workspace = true } diff --git a/apps/desktop/desktop_native/core/src/autofill/mod.rs b/apps/desktop/desktop_native/core/src/autofill/mod.rs index aacec852e90..2a7a10491ec 100644 --- a/apps/desktop/desktop_native/core/src/autofill/mod.rs +++ b/apps/desktop/desktop_native/core/src/autofill/mod.rs @@ -4,3 +4,171 @@ #[cfg_attr(target_os = "macos", path = "macos.rs")] mod autofill; pub use autofill::*; +use serde::{de::Visitor, Deserialize, Deserializer, Serialize}; +use serde_json::Value; + +#[derive(Deserialize)] +struct RunCommandRequest { + #[serde(rename = "namespace")] + namespace: String, + #[serde(rename = "command")] + command: RunCommand, + #[serde(rename = "params")] + params: Value, +} + +#[derive(Deserialize)] +enum RunCommand { + #[serde(rename = "status")] + Status, + #[serde(rename = "sync")] + Sync, + #[serde(rename = "user-verification")] + UserVerification, +} + +#[derive(Debug, Deserialize)] +struct SyncParameters { + #[serde(rename = "credentials")] + pub(crate) credentials: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +enum SyncCredential { + #[serde(rename = "login")] + Login { + #[serde(rename = "cipherId")] + cipher_id: String, + password: String, + uri: String, + username: String, + }, + #[serde(rename = "fido2")] + Fido2 { + #[serde(rename = "cipherId")] + cipher_id: String, + + #[serde(rename = "rpId")] + rp_id: String, + + /// Base64-encoded + #[serde(rename = "credentialId")] + credential_id: String, + + #[serde(rename = "userName")] + user_name: String, + + /// Base64-encoded + #[serde(rename = "userHandle")] + user_handle: String, + }, +} + +#[derive(Serialize)] +struct StatusResponse { + support: StatusSupport, + state: StatusState, +} + +#[derive(Serialize)] +struct StatusSupport { + fido2: bool, + password: bool, + #[serde(rename = "incrementalUpdates")] + incremental_updates: bool, +} + +#[derive(Serialize)] +struct StatusState { + enabled: bool, +} + +#[derive(Serialize)] +struct SyncResponse { + added: u32, +} + +#[derive(Debug, Deserialize)] +struct UserVerificationParameters { + #[serde(rename = "windowHandle", deserialize_with = "deserialize_b64")] + window_handle: Vec, + #[serde(rename = "transactionContext", deserialize_with = "deserialize_b64")] + pub(crate) transaction_context: Vec, + #[serde(rename = "displayHint")] + pub(crate) display_hint: String, + pub(crate) username: String, +} +#[derive(Serialize)] +struct UserVerificationResponse {} + +#[derive(Serialize)] +#[serde(tag = "type")] +enum CommandResponse { + #[serde(rename = "success")] + Success { value: Value }, + #[serde(rename = "error")] + Error { error: String }, +} + +impl From> for CommandResponse { + fn from(value: anyhow::Result) -> Self { + match value { + Ok(response) => Self::Success { value: response }, + Err(err) => Self::Error { + error: err.to_string(), + }, + } + } +} + +impl TryFrom for CommandResponse { + type Error = anyhow::Error; + + fn try_from(response: StatusResponse) -> Result { + Ok(Self::Success { + value: serde_json::to_value(response)?, + }) + } +} + +impl TryFrom for CommandResponse { + type Error = anyhow::Error; + + fn try_from(response: SyncResponse) -> Result { + Ok(Self::Success { + value: serde_json::to_value(response)?, + }) + } +} + +impl TryFrom for CommandResponse { + type Error = anyhow::Error; + + fn try_from(response: UserVerificationResponse) -> Result { + Ok(Self::Success { + value: serde_json::to_value(response)?, + }) + } +} + +fn deserialize_b64<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { + deserializer.deserialize_str(Base64Visitor {}) +} + +struct Base64Visitor; +impl<'de> Visitor<'de> for Base64Visitor { + type Value = Vec; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("A valid base64 string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + use base64::{engine::general_purpose::STANDARD, Engine as _}; + STANDARD.decode(v).map_err(|err| E::custom(err)) + } +} diff --git a/apps/desktop/desktop_native/core/src/autofill/windows.rs b/apps/desktop/desktop_native/core/src/autofill/windows.rs index 09dc6867931..b91fdd93f68 100644 --- a/apps/desktop/desktop_native/core/src/autofill/windows.rs +++ b/apps/desktop/desktop_native/core/src/autofill/windows.rs @@ -1,6 +1,184 @@ -use anyhow::Result; +use anyhow::{anyhow, Result}; +use base64::engine::{general_purpose::URL_SAFE_NO_PAD, Engine}; +use win_webauthn::{CredentialId, UserId, plugin::{Clsid, PluginCredentialDetails, PluginUserVerificationRequest, WebAuthnPlugin}}; +use windows::{Win32::Foundation::HWND, core::GUID}; + +use crate::autofill::{ + CommandResponse, RunCommand, RunCommandRequest, StatusResponse, StatusState, StatusSupport, + SyncCredential, SyncParameters, SyncResponse, UserVerificationParameters, + UserVerificationResponse, +}; + +const PLUGIN_CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062"; #[allow(clippy::unused_async)] -pub async fn run_command(_value: String) -> Result { - todo!("Windows does not support autofill"); +pub async fn run_command(value: String) -> Result { + tracing::debug!("Received command request: {value}"); + let request: RunCommandRequest = serde_json::from_str(&value) + .map_err(|e| anyhow!("Failed to deserialize passkey request: {e}"))?; + + if request.namespace != "autofill" { + return Err(anyhow!("Unknown namespace: {}", request.namespace)); + } + let response: CommandResponse = match request.command { + RunCommand::Status => handle_status_request()?.try_into()?, + RunCommand::Sync => { + let params: SyncParameters = serde_json::from_value(request.params) + .map_err(|e| anyhow!("Could not parse sync parameters: {e}"))?; + handle_sync_request(params)?.try_into()? + } + RunCommand::UserVerification => { + let params: UserVerificationParameters = serde_json::from_value(request.params) + .map_err(|e| anyhow!("Could not parse user verification parameters: {e}"))?; + handle_user_verification_request(params)?.try_into()? + } + }; + serde_json::to_string(&response).map_err(|e| anyhow!("Failed to serialize response: {e}")) } + +fn handle_sync_request(params: SyncParameters) -> Result { + let credentials: Vec = params + .credentials + .into_iter() + .filter_map(|c| c.try_into().ok()) + .collect(); + let num_creds = credentials.len().try_into().unwrap_or(u32::MAX); + sync_credentials_to_windows(credentials, PLUGIN_CLSID) + .map_err(|e| anyhow!("Failed to sync credentials to Windows: {e}"))?; + Ok(SyncResponse { added: num_creds }) +} + +fn handle_status_request() -> Result { + Ok(StatusResponse { + support: StatusSupport { + fido2: true, + password: false, + incremental_updates: false, + }, + state: StatusState { enabled: true }, + }) +} + +fn handle_user_verification_request( + request: UserVerificationParameters, +) -> Result { + tracing::debug!(?request, "Handling user verification request"); + let (buf, _) = request.transaction_context[..16].split_at(16); + let guid_u128 = buf + .try_into() + .map_err(|e| anyhow!("Failed to parse transaction ID as u128: {e}"))?; + let transaction_id = GUID::from_u128(u128::from_le_bytes(guid_u128)); + let hwnd: HWND = unsafe { + // SAFETY: We check to make sure that the vec is the expected size + // before converting it. If the handle is invalid when passed to + // Windows, the request will be rejected. + if request.window_handle.len() == size_of::() { + *request.window_handle.as_ptr().cast() + } else { + return Err(anyhow!("Invalid window handle received: {:?}", request.window_handle)); + } + }; + + let uv_request = PluginUserVerificationRequest { + window_handle: hwnd, + transaction_id: transaction_id, + user_name: request.username, + display_hint: Some(request.display_hint), + }; + let _response = WebAuthnPlugin::perform_user_verification(uv_request) + .map_err(|err| anyhow!("User Verification request failed: {err}"))?; + return Ok(UserVerificationResponse {}); +} + +impl TryFrom for SyncedCredential { + type Error = anyhow::Error; + + fn try_from(value: SyncCredential) -> Result { + if let SyncCredential::Fido2 { + rp_id, + credential_id, + user_name, + user_handle, + .. + } = value + { + Ok(Self { + credential_id: URL_SAFE_NO_PAD + .decode(credential_id) + .map_err(|e| anyhow!("Could not decode credential ID: {e}"))?, + rp_id: rp_id, + user_name: user_name, + user_handle: URL_SAFE_NO_PAD + .decode(&user_handle) + .map_err(|e| anyhow!("Could not decode user handle: {e}"))?, + }) + } else { + Err(anyhow!("Only FIDO2 credentials are supported.")) + } + } +} + +/// Initiates credential sync from Electron to Windows - called when Electron wants to push credentials to Windows +fn sync_credentials_to_windows( + credentials: Vec, + plugin_clsid: &str, +) -> Result<(), String> { + tracing::debug!( + "[SYNC_TO_WIN] sync_credentials_to_windows called with {} credentials for plugin CLSID: {}", + credentials.len(), + plugin_clsid + ); + + let clsid = Clsid::try_from(plugin_clsid) + .map_err(|err| format!("Failed to parse CLSID from string {plugin_clsid}: {err}"))?; + let plugin = WebAuthnPlugin::new(clsid); + + // Convert Bitwarden credentials to Windows credential details + let win_credentials = credentials.into_iter().enumerate().filter_map(|(i, cred)| { + tracing::debug!("[SYNC_TO_WIN] Converting credential {}: RP ID: {}, User: {}, Credential ID: {:?} ({} bytes), User ID: {:?} ({} bytes)", + i + 1, cred.rp_id, cred.user_name, &cred.credential_id, cred.credential_id.len(), &cred.user_handle, cred.user_handle.len()); + + let cred_id = match CredentialId::try_from(cred.credential_id) { + Ok(id) => id, + Err(err) => { + tracing::warn!("Skipping sync of credential {} because of an invalid credential ID: {err}", i + 1); + return None; + } + }; + let user_id = match UserId::try_from(cred.user_handle) { + Ok(id) => id, + Err(err) => { + tracing::warn!("Skipping sync of credential {} because of an invalid user ID: {err}", i + 1); + return None; + } + }; + + let cred_details = PluginCredentialDetails { + credential_id: cred_id, + rp_id: cred.rp_id.clone(), + rp_friendly_name: Some(cred.rp_id.clone()), // Use RP ID as friendly name for now + user_id: user_id, + user_name: cred.user_name.clone(), + user_display_name: cred.user_name.clone(), // Use user name as display name for now + }; + tracing::debug!( + "[SYNC_TO_WIN] Converted credential {} to Windows format", + i + 1 + ); + Some(cred_details) + }).collect(); + + plugin + .sync_credentials(win_credentials) + .map_err(|err| format!("Failed to synchronize credentials: {err}")) +} + +/// Credential data for sync operations +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct SyncedCredential { + pub credential_id: Vec, + pub rp_id: String, + pub user_name: String, + pub user_handle: Vec, +} \ No newline at end of file diff --git a/apps/desktop/desktop_native/core/src/biometric/windows.rs b/apps/desktop/desktop_native/core/src/biometric/windows.rs index 39dcdb149d7..8013c21bf9a 100644 --- a/apps/desktop/desktop_native/core/src/biometric/windows.rs +++ b/apps/desktop/desktop_native/core/src/biometric/windows.rs @@ -48,7 +48,7 @@ impl super::BiometricTrait for Biometric { let operation: IAsyncOperation = unsafe { interop.RequestVerificationForWindowAsync(foreground_window, &HSTRING::from(message))? }; - let result = operation.join()?; + let result = operation.get()?; match result { UserConsentVerificationResult::Verified => Ok(true), @@ -57,7 +57,7 @@ impl super::BiometricTrait for Biometric { } async fn available() -> Result { - let ucv_available = UserConsentVerifier::CheckAvailabilityAsync()?.join()?; + let ucv_available = UserConsentVerifier::CheckAvailabilityAsync()?.get()?; match ucv_available { UserConsentVerifierAvailability::Available => Ok(true), diff --git a/apps/desktop/desktop_native/macos_provider/README.md b/apps/desktop/desktop_native/macos_provider/README.md new file mode 100644 index 00000000000..1d4c1902465 --- /dev/null +++ b/apps/desktop/desktop_native/macos_provider/README.md @@ -0,0 +1,35 @@ +# Explainer: Mac OS Native Passkey Provider + +This document describes the changes introduced in https://github.com/bitwarden/clients/pull/13963, where we introduce the MacOS Native Passkey Provider. It gives the high level explanation of the architecture and some of the quirks and additional good to know context. + +## The high level +MacOS has native APIs (similar to iOS) to allow Credential Managers to provide credentials to the MacOS autofill system (in the PR referenced above, we only provide passkeys). + +We’ve written a Swift-based native autofill-extension. It’s bundled in the app-bundle in PlugIns, similar to the safari-extension. + +This swift extension currently communicates with our Electron app through IPC based on a unix socket. The IPC implementation is done in Rust and utilized through UniFFI + NAPI bindings. + +Footnotes: + +* We're not using the IPC framework as the implementation pre-dates the IPC framework. +* Alternatives like XPC or CFMessagePort may have better support for when the app is sandboxed. + +Electron receives the messages and passes it to Angular (through the electron-renderer event system). + +Our existing fido2 services in the renderer respond to events, displaying UI as necessary, and returns the signature back through the same mechanism, allowing people to authenticate with passkeys through the native system + UI. See [Mac OS Native Passkey Workflows](https://bitwarden.atlassian.net/wiki/spaces/EN/pages/1828356098/Mac+OS+Native+Passkey+Workflows) for demo videos. + +## Typescript + UI implementations + +We utilize the same FIDO2 implementation and interface that is already present for our browser authentication. It was designed by @coroiu with multiple ‘ui environments' in mind. + +Therefore, a lot of the plumbing is implemented in /autofill/services/desktop-fido2-user-interface.service.ts, which implements the interface that our fido2 authenticator/client expects to drive UI related behaviors. + +We’ve also implemented a couple FIDO2 UI components to handle registration/sign in flows, but also improved the “modal mode” of the desktop app. + +## Modal mode + +When modal mode is activated, the desktop app turns into a smaller modal that is always on top and cannot be resized. This is done to improve the UX of performing a passkey operation (or SSH operation). Once the operation is completed, the app returns to normal mode and its previous position. + +We are not using electron modal windows, for a couple reason. It would require us to send data in yet another layer of IPC, but also because we'd need to bootstrap entire renderer/app instead of reusing the existing window. + +Some modal modes may hide the 'traffic buttons' (window controls) due to design requirements. diff --git a/apps/desktop/desktop_native/macos_provider/build.sh b/apps/desktop/desktop_native/macos_provider/build.sh index 21e2e045af4..2f7a2d03541 100755 --- a/apps/desktop/desktop_native/macos_provider/build.sh +++ b/apps/desktop/desktop_native/macos_provider/build.sh @@ -8,6 +8,9 @@ rm -r tmp mkdir -p ./tmp/target/universal-darwin/release/ +rustup target add aarch64-apple-darwin +rustup target add x86_64-apple-darwin + cargo build --package macos_provider --target aarch64-apple-darwin --release cargo build --package macos_provider --target x86_64-apple-darwin --release diff --git a/apps/desktop/desktop_native/macos_provider/src/lib.rs b/apps/desktop/desktop_native/macos_provider/src/lib.rs index 359fe213996..21da75a1d70 100644 --- a/apps/desktop/desktop_native/macos_provider/src/lib.rs +++ b/apps/desktop/desktop_native/macos_provider/src/lib.rs @@ -56,6 +56,14 @@ trait Callback: Send + Sync { fn error(&self, error: BitwardenError); } +#[derive(uniffi::Enum, Debug)] +/// Store the connection status between the macOS credential provider extension +/// and the desktop application's IPC server. +pub enum ConnectionStatus { + Connected, + Disconnected, +} + #[derive(uniffi::Object)] pub struct MacOSProviderClient { to_server_send: tokio::sync::mpsc::Sender, @@ -64,8 +72,23 @@ pub struct MacOSProviderClient { response_callbacks_counter: AtomicU32, #[allow(clippy::type_complexity)] response_callbacks_queue: Arc, Instant)>>>, + + // Flag to track connection status - atomic for thread safety without locks + connection_status: Arc, } +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +/// Store native desktop status information to use for IPC communication +/// between the application and the macOS credential provider. +pub struct NativeStatus { + key: String, + value: String, +} + +// In our callback management, 0 is a reserved sequence number indicating that a message does not have a callback. +const NO_CALLBACK_INDICATOR: u32 = 0; + #[uniffi::export] impl MacOSProviderClient { // FIXME: Remove unwraps! They panic and terminate the whole application. @@ -92,13 +115,15 @@ impl MacOSProviderClient { let client = MacOSProviderClient { to_server_send, - response_callbacks_counter: AtomicU32::new(0), + response_callbacks_counter: AtomicU32::new(1), // Start at 1 since 0 is reserved for "no callback" scenarios response_callbacks_queue: Arc::new(Mutex::new(HashMap::new())), + connection_status: Arc::new(std::sync::atomic::AtomicBool::new(false)), }; let path = desktop_core::ipc::path("af"); let queue = client.response_callbacks_queue.clone(); + let connection_status = client.connection_status.clone(); std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_current_thread() @@ -116,9 +141,11 @@ impl MacOSProviderClient { match serde_json::from_str::(&message) { Ok(SerializedMessage::Command(CommandMessage::Connected)) => { info!("Connected to server"); + connection_status.store(true, std::sync::atomic::Ordering::Relaxed); } Ok(SerializedMessage::Command(CommandMessage::Disconnected)) => { info!("Disconnected from server"); + connection_status.store(false, std::sync::atomic::Ordering::Relaxed); } Ok(SerializedMessage::Message { sequence_number, @@ -156,12 +183,17 @@ impl MacOSProviderClient { client } + pub fn send_native_status(&self, key: String, value: String) { + let status = NativeStatus { key, value }; + self.send_message(status, None); + } + pub fn prepare_passkey_registration( &self, request: PasskeyRegistrationRequest, callback: Arc, ) { - self.send_message(request, Box::new(callback)); + self.send_message(request, Some(Box::new(callback))); } pub fn prepare_passkey_assertion( @@ -169,7 +201,7 @@ impl MacOSProviderClient { request: PasskeyAssertionRequest, callback: Arc, ) { - self.send_message(request, Box::new(callback)); + self.send_message(request, Some(Box::new(callback))); } pub fn prepare_passkey_assertion_without_user_interface( @@ -177,7 +209,18 @@ impl MacOSProviderClient { request: PasskeyAssertionWithoutUserInterfaceRequest, callback: Arc, ) { - self.send_message(request, Box::new(callback)); + self.send_message(request, Some(Box::new(callback))); + } + + pub fn get_connection_status(&self) -> ConnectionStatus { + let is_connected = self + .connection_status + .load(std::sync::atomic::Ordering::Relaxed); + if is_connected { + ConnectionStatus::Connected + } else { + ConnectionStatus::Disconnected + } } } @@ -199,7 +242,6 @@ enum SerializedMessage { } impl MacOSProviderClient { - // FIXME: Remove unwraps! They panic and terminate the whole application. #[allow(clippy::unwrap_used)] fn add_callback(&self, callback: Box) -> u32 { let sequence_number = self @@ -208,20 +250,23 @@ impl MacOSProviderClient { self.response_callbacks_queue .lock() - .unwrap() + .expect("response callbacks queue mutex should not be poisoned") .insert(sequence_number, (callback, Instant::now())); sequence_number } - // FIXME: Remove unwraps! They panic and terminate the whole application. #[allow(clippy::unwrap_used)] fn send_message( &self, message: impl Serialize + DeserializeOwned, - callback: Box, + callback: Option>, ) { - let sequence_number = self.add_callback(callback); + let sequence_number = if let Some(callback) = callback { + self.add_callback(callback) + } else { + NO_CALLBACK_INDICATOR + }; let message = serde_json::to_string(&SerializedMessage::Message { sequence_number, @@ -231,15 +276,17 @@ impl MacOSProviderClient { if let Err(e) = self.to_server_send.blocking_send(message) { // Make sure we remove the callback from the queue if we can't send the message - if let Some((cb, _)) = self - .response_callbacks_queue - .lock() - .unwrap() - .remove(&sequence_number) - { - cb.error(BitwardenError::Internal(format!( - "Error sending message: {e}" - ))); + if sequence_number != NO_CALLBACK_INDICATOR { + if let Some((callback, _)) = self + .response_callbacks_queue + .lock() + .expect("response callbacks queue mutex should not be poisoned") + .remove(&sequence_number) + { + callback.error(BitwardenError::Internal(format!( + "Error sending message: {e}" + ))); + } } } } diff --git a/apps/desktop/desktop_native/macos_provider/src/registration.rs b/apps/desktop/desktop_native/macos_provider/src/registration.rs index 9e697b75c16..c961566a86c 100644 --- a/apps/desktop/desktop_native/macos_provider/src/registration.rs +++ b/apps/desktop/desktop_native/macos_provider/src/registration.rs @@ -14,6 +14,7 @@ pub struct PasskeyRegistrationRequest { user_verification: UserVerification, supported_algorithms: Vec, window_xy: Position, + excluded_credentials: Vec>, } #[derive(uniffi::Record, Serialize, Deserialize)] diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index d5b8a8242fe..25cc8553663 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -146,6 +146,12 @@ export declare namespace autofill { Required = 'required', Discouraged = 'discouraged' } + export interface LockStatusQueryRequest { + + } + export interface LockStatusQueryResponse { + isUnlocked: boolean + } export interface Position { x: number y: number @@ -158,6 +164,9 @@ export declare namespace autofill { userVerification: UserVerification supportedAlgorithms: Array windowXy: Position + excludedCredentials: Array> + clientWindowHandle?: Array + context?: string } export interface PasskeyRegistrationResponse { rpId: string @@ -171,16 +180,24 @@ export declare namespace autofill { userVerification: UserVerification allowedCredentials: Array> windowXy: Position + clientWindowHandle?: Array + context?: string } export interface PasskeyAssertionWithoutUserInterfaceRequest { rpId: string credentialId: Array - userName: string - userHandle: Array + userName?: string + userHandle?: Array recordIdentifier?: string clientDataHash: Array userVerification: UserVerification windowXy: Position + clientWindowHandle?: Array + context?: string + } + export interface NativeStatus { + key: string + value: string } export interface PasskeyAssertionResponse { rpId: string @@ -197,40 +214,19 @@ export declare namespace autofill { * @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client. * @param callback This function will be called whenever a message is received from a client. */ - static listen(name: string, registrationCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void, assertionCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void, assertionWithoutUserInterfaceCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void): Promise + static listen(name: string, registrationCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void, assertionCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void, assertionWithoutUserInterfaceCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void, nativeStatusCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void, lockStatusQueryCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: LockStatusQueryRequest) => void): Promise /** Return the path to the IPC server. */ getPath(): string /** Stop the IPC server. */ stop(): void completeRegistration(clientId: number, sequenceNumber: number, response: PasskeyRegistrationResponse): number completeAssertion(clientId: number, sequenceNumber: number, response: PasskeyAssertionResponse): number + completeLockStatusQuery(clientId: number, sequenceNumber: number, response: LockStatusQueryResponse): number completeError(clientId: number, sequenceNumber: number, error: string): number } } export declare namespace passkey_authenticator { - export interface PasskeyRequestEvent { - requestType: string - requestJson: string - } - export interface SyncedCredential { - credentialId: string - rpId: string - userName: string - userHandle: string - } - export interface PasskeySyncRequest { - rpId: string - } - export interface PasskeySyncResponse { - credentials: Array - } - export interface PasskeyErrorResponse { - message: string - } export function register(): void - export function onRequest(callback: (error: null | Error, event: PasskeyRequestEvent) => Promise): Promise - export function syncCredentialsToWindows(credentials: Array): void - export function getCredentialsFromWindows(): Array } export declare namespace logging { export const enum LogLevel { diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 8635f6a4d69..12476820b3b 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -656,6 +656,18 @@ pub mod autofill { Discouraged, } + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct LockStatusQueryRequest {} + + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct LockStatusQueryResponse { + pub is_unlocked: bool, + } + #[derive(Serialize, Deserialize)] #[serde(bound = "T: Serialize + DeserializeOwned")] pub struct PasskeyMessage { @@ -682,6 +694,9 @@ pub mod autofill { pub user_verification: UserVerification, pub supported_algorithms: Vec, pub window_xy: Position, + pub excluded_credentials: Vec>, + pub client_window_handle: Option>, + pub context: Option, } #[napi(object)] @@ -703,6 +718,8 @@ pub mod autofill { pub user_verification: UserVerification, pub allowed_credentials: Vec>, pub window_xy: Position, + pub client_window_handle: Option>, + pub context: Option, //extension_input: Vec, TODO: Implement support for extensions } @@ -712,12 +729,22 @@ pub mod autofill { pub struct PasskeyAssertionWithoutUserInterfaceRequest { pub rp_id: String, pub credential_id: Vec, - pub user_name: String, - pub user_handle: Vec, + pub user_name: Option, + pub user_handle: Option>, pub record_identifier: Option, pub client_data_hash: Vec, pub user_verification: UserVerification, pub window_xy: Position, + pub client_window_handle: Option>, + pub context: Option, + } + + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct NativeStatus { + pub key: String, + pub value: String, } #[napi(object)] @@ -772,6 +799,20 @@ pub mod autofill { (u32, u32, PasskeyAssertionWithoutUserInterfaceRequest), ErrorStrategy::CalleeHandled, >, + #[napi( + ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void" + )] + native_status_callback: ThreadsafeFunction< + (u32, u32, NativeStatus), + ErrorStrategy::CalleeHandled, + >, + #[napi( + ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: LockStatusQueryRequest) => void" + )] + lock_status_query_callback: ThreadsafeFunction< + (u32, u32, LockStatusQueryRequest), + ErrorStrategy::CalleeHandled, + >, ) -> napi::Result { let (send, mut recv) = tokio::sync::mpsc::channel::(32); tokio::spawn(async move { @@ -844,6 +885,38 @@ pub mod autofill { } } + match serde_json::from_str::>(&message) { + Ok(msg) => { + let value = msg + .value + .map(|value| (client_id, msg.sequence_number, value)) + .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); + native_status_callback + .call(value, ThreadsafeFunctionCallMode::NonBlocking); + continue; + } + Err(error) => { + error!(%error, "Unable to deserialze native status."); + } + } + + match serde_json::from_str::>( + &message, + ) { + Ok(msg) => { + let value = msg + .value + .map(|value| (client_id, msg.sequence_number, value)) + .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); + lock_status_query_callback + .call(value, ThreadsafeFunctionCallMode::NonBlocking); + continue; + } + Err(error) => { + error!(%error, "Unable to deserialze native status."); + } + } + error!(message, "Received an unknown message2"); } } @@ -902,6 +975,20 @@ pub mod autofill { self.send(client_id, serde_json::to_string(&message).unwrap()) } + #[napi] + pub fn complete_lock_status_query( + &self, + client_id: u32, + sequence_number: u32, + response: LockStatusQueryResponse, + ) -> napi::Result { + let message = PasskeyMessage { + sequence_number, + value: Ok(response), + }; + self.send(client_id, serde_json::to_string(&message).unwrap()) + } + #[napi] pub fn complete_error( &self, @@ -931,73 +1018,12 @@ pub mod autofill { #[napi] pub mod passkey_authenticator { - use napi::threadsafe_function::{ErrorStrategy::CalleeHandled, ThreadsafeFunction}; - - #[napi(object)] - #[derive(Debug)] - pub struct PasskeyRequestEvent { - pub request_type: String, - pub request_json: String, - } - - #[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_handle: String, // base64url encoded - } - - #[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 PasskeySyncResponse { - pub credentials: Vec, - } - - #[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" - )] - callback: ThreadsafeFunction, - ) -> napi::Result { - crate::passkey_authenticator_internal::on_request(callback).await - } - - #[napi] - pub fn sync_credentials_to_windows(credentials: Vec) -> napi::Result<()> { - crate::passkey_authenticator_internal::sync_credentials_to_windows(credentials) - } - - #[napi] - pub fn get_credentials_from_windows() -> napi::Result> { - crate::passkey_authenticator_internal::get_credentials_from_windows() - } } #[napi] diff --git a/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/windows.rs b/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/windows.rs index a062658479b..4ff51f5bce4 100644 --- a/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/windows.rs +++ b/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/windows.rs @@ -1,220 +1,7 @@ use anyhow::{anyhow, Result}; -use napi::{ - bindgen_prelude::Promise, - threadsafe_function::{ErrorStrategy::CalleeHandled, ThreadsafeFunction}, -}; -use serde_json; -use tokio::sync::mpsc; - -// Use the PasskeyRequestEvent from the parent module -pub use crate::passkey_authenticator::{PasskeyRequestEvent, SyncedCredential}; pub fn register() -> Result<()> { windows_plugin_authenticator::register().map_err(|e| anyhow!(e))?; Ok(()) } - -pub async fn on_request( - callback: ThreadsafeFunction, -) -> napi::Result { - 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, napi::Error> = - callback.call_async(Ok(napi_event)).await; - // awai promse - - match promise_result { - Ok(promise_result) => match promise_result.await { - Ok(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()) -} - -impl From 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_handle: base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&cred.user_handle), - } - } -} - -impl From 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_handle: base64::engine::general_purpose::URL_SAFE_NO_PAD - .decode(&cred.user_handle) - .unwrap_or_default(), - } - } -} - -pub fn sync_credentials_to_windows(credentials: Vec) -> 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_handle.len() > 16 { - format!("{}...", &cred.user_handle[..16]) - } else { - cred.user_handle.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 = - 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))) -} - -pub fn get_credentials_from_windows() -> napi::Result> { - 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 - let napi_credentials: Vec = 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) -} diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m b/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m index fc13c04591a..037a97c7590 100644 --- a/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m +++ b/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m @@ -14,40 +14,64 @@ void runSync(void* context, NSDictionary *params) { // Map credentials to ASPasswordCredential objects NSMutableArray *mappedCredentials = [NSMutableArray arrayWithCapacity:credentials.count]; + for (NSDictionary *credential in credentials) { - NSString *type = credential[@"type"]; - - if ([type isEqualToString:@"password"]) { - NSString *cipherId = credential[@"cipherId"]; - NSString *uri = credential[@"uri"]; - NSString *username = credential[@"username"]; - - ASCredentialServiceIdentifier *serviceId = [[ASCredentialServiceIdentifier alloc] - initWithIdentifier:uri type:ASCredentialServiceIdentifierTypeURL]; - ASPasswordCredentialIdentity *credential = [[ASPasswordCredentialIdentity alloc] - initWithServiceIdentifier:serviceId user:username recordIdentifier:cipherId]; - - [mappedCredentials addObject:credential]; - } - - if (@available(macos 14, *)) { - if ([type isEqualToString:@"fido2"]) { + @try { + NSString *type = credential[@"type"]; + + if ([type isEqualToString:@"password"]) { NSString *cipherId = credential[@"cipherId"]; - NSString *rpId = credential[@"rpId"]; - NSString *userName = credential[@"userName"]; - NSData *credentialId = decodeBase64URL(credential[@"credentialId"]); - NSData *userHandle = decodeBase64URL(credential[@"userHandle"]); + NSString *uri = credential[@"uri"]; + NSString *username = credential[@"username"]; + + // Skip credentials with null username since MacOS crashes if we send credentials with empty usernames + if ([username isKindOfClass:[NSNull class]] || username.length == 0) { + NSLog(@"Skipping credential, username is empty: %@", credential); + continue; + } - Class passkeyCredentialIdentityClass = NSClassFromString(@"ASPasskeyCredentialIdentity"); - id credential = [[passkeyCredentialIdentityClass alloc] - initWithRelyingPartyIdentifier:rpId - userName:userName - credentialID:credentialId - userHandle:userHandle - recordIdentifier:cipherId]; + ASCredentialServiceIdentifier *serviceId = [[ASCredentialServiceIdentifier alloc] + initWithIdentifier:uri type:ASCredentialServiceIdentifierTypeURL]; + ASPasswordCredentialIdentity *passwordIdentity = [[ASPasswordCredentialIdentity alloc] + initWithServiceIdentifier:serviceId user:username recordIdentifier:cipherId]; - [mappedCredentials addObject:credential]; + [mappedCredentials addObject:passwordIdentity]; + } + else if (@available(macos 14, *)) { + // Fido2CredentialView uses `userName` (camelCase) while Login uses `username`. + // This is intentional. Fido2 fields are flattened from the FIDO2 spec's nested structure + // (user.name -> userName, rp.id -> rpId) to maintain a clear distinction between these fields. + if ([type isEqualToString:@"fido2"]) { + NSString *cipherId = credential[@"cipherId"]; + NSString *rpId = credential[@"rpId"]; + NSString *userName = credential[@"userName"]; + + // Skip credentials with null username since MacOS crashes if we send credentials with empty usernames + if ([userName isKindOfClass:[NSNull class]] || userName.length == 0) { + NSLog(@"Skipping credential, username is empty: %@", credential); + continue; + } + + NSData *credentialId = decodeBase64URL(credential[@"credentialId"]); + NSData *userHandle = decodeBase64URL(credential[@"userHandle"]); + + Class passkeyCredentialIdentityClass = NSClassFromString(@"ASPasskeyCredentialIdentity"); + id passkeyIdentity = [[passkeyCredentialIdentityClass alloc] + initWithRelyingPartyIdentifier:rpId + userName:userName + credentialID:credentialId + userHandle:userHandle + recordIdentifier:cipherId]; + + [mappedCredentials addObject:passkeyIdentity]; + } } + } @catch (NSException *exception) { + // Silently skip any credential that causes an exception + // to make sure we don't fail the entire sync + // There is likely some invalid data in the credential, and not something the user should/could be asked to correct. + NSLog(@"ERROR: Exception processing credential: %@ - %@", exception.name, exception.reason); + continue; } } diff --git a/apps/desktop/desktop_native/objc/src/native/utils.m b/apps/desktop/desktop_native/objc/src/native/utils.m index 040c723a8ac..8f9493a7afb 100644 --- a/apps/desktop/desktop_native/objc/src/native/utils.m +++ b/apps/desktop/desktop_native/objc/src/native/utils.m @@ -18,9 +18,26 @@ NSString *serializeJson(NSDictionary *dictionary, NSError *error) { } NSData *decodeBase64URL(NSString *base64URLString) { + if (base64URLString.length == 0) { + return nil; + } + + // Replace URL-safe characters with standard base64 characters NSString *base64String = [base64URLString stringByReplacingOccurrencesOfString:@"-" withString:@"+"]; base64String = [base64String stringByReplacingOccurrencesOfString:@"_" withString:@"/"]; - + + // Add padding if needed + // Base 64 strings should be a multiple of 4 in length + NSUInteger paddingLength = 4 - (base64String.length % 4); + if (paddingLength < 4) { + NSMutableString *paddedString = [NSMutableString stringWithString:base64String]; + for (NSUInteger i = 0; i < paddingLength; i++) { + [paddedString appendString:@"="]; + } + base64String = paddedString; + } + + // Decode the string NSData *nsdataFromBase64String = [[NSData alloc] initWithBase64EncodedString:base64String options:0]; diff --git a/apps/desktop/desktop_native/rust-toolchain.toml b/apps/desktop/desktop_native/rust-toolchain.toml index 898a61f3f4b..c1ab6b3240a 100644 --- a/apps/desktop/desktop_native/rust-toolchain.toml +++ b/apps/desktop/desktop_native/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.85.0" +channel = "1.87.0" components = [ "rustfmt", "clippy" ] profile = "minimal" diff --git a/apps/desktop/desktop_native/win_webauthn/Cargo.toml b/apps/desktop/desktop_native/win_webauthn/Cargo.toml new file mode 100644 index 00000000000..312f580864c --- /dev/null +++ b/apps/desktop/desktop_native/win_webauthn/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "win_webauthn" +version.workspace = true +license.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +base64 = { workspace = true } +ciborium = "0.2" +tracing = { workspace = true } + +[dev-dependencies] +hex = { workspace = true } + +[lints] +workspace = true + +[target.'cfg(windows)'.dependencies] +windows = { workspace = true, features = [ + "Win32_Foundation", + "Win32_Security", + "Win32_System_Com", + "Win32_System_LibraryLoader", +] } +windows-core = { workspace = true } \ No newline at end of file diff --git a/apps/desktop/desktop_native/win_webauthn/include/pluginauthenticator.h b/apps/desktop/desktop_native/win_webauthn/include/pluginauthenticator.h new file mode 100644 index 00000000000..edb3ffd4f00 --- /dev/null +++ b/apps/desktop/desktop_native/win_webauthn/include/pluginauthenticator.h @@ -0,0 +1,263 @@ + + +/* 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 version is high enough to compile this file*/ +#ifndef __REQUIRED_RPCNDR_H_VERSION__ +#define __REQUIRED_RPCNDR_H_VERSION__ 501 +#endif + +/* verify that the 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 +#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 __IPluginAuthenticator_FWD_DEFINED__ +#define __IPluginAuthenticator_FWD_DEFINED__ +typedef interface IPluginAuthenticator IPluginAuthenticator; + +#endif /* __IPluginAuthenticator_FWD_DEFINED__ */ + + +/* header files for imported files */ +#include "oaidl.h" + +#ifdef __cplusplus +extern "C"{ +#endif + + +/* interface __MIDL_itf_pluginauthenticator_0000_0000 */ +/* [local] */ + +typedef +enum _WEBAUTHN_PLUGIN_REQUEST_TYPE + { + WEBAUTHN_PLUGIN_REQUEST_TYPE_CTAP2_CBOR = 0x1 + } WEBAUTHN_PLUGIN_REQUEST_TYPE; + +typedef struct _WEBAUTHN_PLUGIN_OPERATION_REQUEST + { + HWND hWnd; + GUID transactionId; + DWORD cbRequestSignature; + /* [size_is] */ byte *pbRequestSignature; + WEBAUTHN_PLUGIN_REQUEST_TYPE requestType; + DWORD cbEncodedRequest; + /* [size_is] */ byte *pbEncodedRequest; + } WEBAUTHN_PLUGIN_OPERATION_REQUEST; + +typedef struct _WEBAUTHN_PLUGIN_OPERATION_REQUEST *PWEBAUTHN_PLUGIN_OPERATION_REQUEST; + +typedef const WEBAUTHN_PLUGIN_OPERATION_REQUEST *PCWEBAUTHN_PLUGIN_OPERATION_REQUEST; + +typedef struct _WEBAUTHN_PLUGIN_OPERATION_RESPONSE + { + DWORD cbEncodedResponse; + /* [size_is] */ byte *pbEncodedResponse; + } WEBAUTHN_PLUGIN_OPERATION_RESPONSE; + +typedef struct _WEBAUTHN_PLUGIN_OPERATION_RESPONSE *PWEBAUTHN_PLUGIN_OPERATION_RESPONSE; + +typedef const WEBAUTHN_PLUGIN_OPERATION_RESPONSE *PCWEBAUTHN_PLUGIN_OPERATION_RESPONSE; + +typedef struct _WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST + { + GUID transactionId; + DWORD cbRequestSignature; + /* [size_is] */ byte *pbRequestSignature; + } WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST; + +typedef struct _WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *PWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST; + +typedef const WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST; + +typedef +enum _PLUGIN_LOCK_STATUS + { + PluginLocked = 0, + PluginUnlocked = ( PluginLocked + 1 ) + } PLUGIN_LOCK_STATUS; + + + +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 __IPluginAuthenticator_INTERFACE_DEFINED__ +#define __IPluginAuthenticator_INTERFACE_DEFINED__ + +/* interface IPluginAuthenticator */ +/* [ref][version][uuid][object] */ + + +EXTERN_C const IID IID_IPluginAuthenticator; + +#if defined(__cplusplus) && !defined(CINTERFACE) + + MIDL_INTERFACE("d26bcf6f-b54c-43ff-9f06-d5bf148625f7") + IPluginAuthenticator : public IUnknown + { + public: + virtual HRESULT STDMETHODCALLTYPE MakeCredential( + /* [in] */ __RPC__in PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request, + /* [retval][out] */ __RPC__out PWEBAUTHN_PLUGIN_OPERATION_RESPONSE response) = 0; + + virtual HRESULT STDMETHODCALLTYPE GetAssertion( + /* [in] */ __RPC__in PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request, + /* [retval][out] */ __RPC__out PWEBAUTHN_PLUGIN_OPERATION_RESPONSE response) = 0; + + virtual HRESULT STDMETHODCALLTYPE CancelOperation( + /* [in] */ __RPC__in PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request) = 0; + + virtual HRESULT STDMETHODCALLTYPE GetLockStatus( + /* [retval][out] */ __RPC__out PLUGIN_LOCK_STATUS *lockStatus) = 0; + + }; + + +#else /* C style interface */ + + typedef struct IPluginAuthenticatorVtbl + { + BEGIN_INTERFACE + + DECLSPEC_XFGVIRT(IUnknown, QueryInterface) + HRESULT ( STDMETHODCALLTYPE *QueryInterface )( + __RPC__in IPluginAuthenticator * This, + /* [in] */ __RPC__in REFIID riid, + /* [annotation][iid_is][out] */ + _COM_Outptr_ void **ppvObject); + + DECLSPEC_XFGVIRT(IUnknown, AddRef) + ULONG ( STDMETHODCALLTYPE *AddRef )( + __RPC__in IPluginAuthenticator * This); + + DECLSPEC_XFGVIRT(IUnknown, Release) + ULONG ( STDMETHODCALLTYPE *Release )( + __RPC__in IPluginAuthenticator * This); + + DECLSPEC_XFGVIRT(IPluginAuthenticator, MakeCredential) + HRESULT ( STDMETHODCALLTYPE *MakeCredential )( + __RPC__in IPluginAuthenticator * This, + /* [in] */ __RPC__in PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request, + /* [retval][out] */ __RPC__out PWEBAUTHN_PLUGIN_OPERATION_RESPONSE response); + + DECLSPEC_XFGVIRT(IPluginAuthenticator, GetAssertion) + HRESULT ( STDMETHODCALLTYPE *GetAssertion )( + __RPC__in IPluginAuthenticator * This, + /* [in] */ __RPC__in PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request, + /* [retval][out] */ __RPC__out PWEBAUTHN_PLUGIN_OPERATION_RESPONSE response); + + DECLSPEC_XFGVIRT(IPluginAuthenticator, CancelOperation) + HRESULT ( STDMETHODCALLTYPE *CancelOperation )( + __RPC__in IPluginAuthenticator * This, + /* [in] */ __RPC__in PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request); + + DECLSPEC_XFGVIRT(IPluginAuthenticator, GetLockStatus) + HRESULT ( STDMETHODCALLTYPE *GetLockStatus )( + __RPC__in IPluginAuthenticator * This, + /* [retval][out] */ __RPC__out PLUGIN_LOCK_STATUS *lockStatus); + + END_INTERFACE + } IPluginAuthenticatorVtbl; + + interface IPluginAuthenticator + { + CONST_VTBL struct IPluginAuthenticatorVtbl *lpVtbl; + }; + + + +#ifdef COBJMACROS + + +#define IPluginAuthenticator_QueryInterface(This,riid,ppvObject) \ + ( (This)->lpVtbl -> QueryInterface(This,riid,ppvObject) ) + +#define IPluginAuthenticator_AddRef(This) \ + ( (This)->lpVtbl -> AddRef(This) ) + +#define IPluginAuthenticator_Release(This) \ + ( (This)->lpVtbl -> Release(This) ) + + +#define IPluginAuthenticator_MakeCredential(This,request,response) \ + ( (This)->lpVtbl -> MakeCredential(This,request,response) ) + +#define IPluginAuthenticator_GetAssertion(This,request,response) \ + ( (This)->lpVtbl -> GetAssertion(This,request,response) ) + +#define IPluginAuthenticator_CancelOperation(This,request) \ + ( (This)->lpVtbl -> CancelOperation(This,request) ) + +#define IPluginAuthenticator_GetLockStatus(This,lockStatus) \ + ( (This)->lpVtbl -> GetLockStatus(This,lockStatus) ) + +#endif /* COBJMACROS */ + + +#endif /* C style interface */ + + + + +#endif /* __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 + + diff --git a/apps/desktop/desktop_native/win_webauthn/include/pluginauthenticator.idl b/apps/desktop/desktop_native/win_webauthn/include/pluginauthenticator.idl new file mode 100644 index 00000000000..b33ca0615a2 --- /dev/null +++ b/apps/desktop/desktop_native/win_webauthn/include/pluginauthenticator.idl @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import "oaidl.idl"; +import "objidl.idl"; +import "wtypes.idl"; + +typedef enum _WEBAUTHN_PLUGIN_REQUEST_TYPE { + WEBAUTHN_PLUGIN_REQUEST_TYPE_CTAP2_CBOR = 0x01 // CBOR encoded CTAP2 message. Refer to the FIDO Specifications: Client to Authenticator Protocol (CTAP) +} WEBAUTHN_PLUGIN_REQUEST_TYPE; + +typedef struct _WEBAUTHN_PLUGIN_OPERATION_REQUEST { + // Handle of the top level Window of the caller + HWND hWnd; + + // Transaction ID + GUID transactionId; + + // Request Hash Signature Bytes Buffer Size + DWORD cbRequestSignature; + + // Request Hash Signature Bytes Buffer - Signature verified using the "pbOpSignPubKey" in PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE + [size_is(cbRequestSignature)] byte* pbRequestSignature; + + // Request Type - Determines the encoding of the request and response buffers + WEBAUTHN_PLUGIN_REQUEST_TYPE requestType; + + // Encoded Request Buffer Size + DWORD cbEncodedRequest; + + // Encoded Request Buffer - Encoding type is determined by the requestType + [size_is(cbEncodedRequest)] byte* pbEncodedRequest; +} WEBAUTHN_PLUGIN_OPERATION_REQUEST, *PWEBAUTHN_PLUGIN_OPERATION_REQUEST; +typedef const WEBAUTHN_PLUGIN_OPERATION_REQUEST *PCWEBAUTHN_PLUGIN_OPERATION_REQUEST; + +typedef struct _WEBAUTHN_PLUGIN_OPERATION_RESPONSE { + // Encoded Response Buffer Size + DWORD cbEncodedResponse; + + // Encoded Response Buffer - Encoding type must match the request + [size_is(cbEncodedResponse)] byte* pbEncodedResponse; +} WEBAUTHN_PLUGIN_OPERATION_RESPONSE, *PWEBAUTHN_PLUGIN_OPERATION_RESPONSE; +typedef const WEBAUTHN_PLUGIN_OPERATION_RESPONSE *PCWEBAUTHN_PLUGIN_OPERATION_RESPONSE; + +typedef struct _WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST { + // Transaction ID + GUID transactionId; + + // Request Hash Signature Bytes Buffer Size + DWORD cbRequestSignature; + + // Request Hash Signature Bytes Buffer - Signature verified using the "pbOpSignPubKey" in PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE + [size_is(cbRequestSignature)] byte* pbRequestSignature; +} WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST, *PWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST; +typedef const WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST; + +typedef enum _PLUGIN_LOCK_STATUS { + PluginLocked = 0, + PluginUnlocked +} PLUGIN_LOCK_STATUS; + +[ + object, + uuid(d26bcf6f-b54c-43ff-9f06-d5bf148625f7), + version(1.0), + pointer_default(ref) +] +interface IPluginAuthenticator : IUnknown +{ + HRESULT MakeCredential( + [in] PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request, + [out, retval] PWEBAUTHN_PLUGIN_OPERATION_RESPONSE response); + + HRESULT GetAssertion( + [in] PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request, + [out, retval] PWEBAUTHN_PLUGIN_OPERATION_RESPONSE response); + + HRESULT CancelOperation( + [in] PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request); + + HRESULT GetLockStatus( + [out, retval] PLUGIN_LOCK_STATUS* lockStatus); +} \ No newline at end of file diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/samples/webauthn.h.sample b/apps/desktop/desktop_native/win_webauthn/include/webauthn.h similarity index 72% rename from apps/desktop/desktop_native/windows_plugin_authenticator/src/samples/webauthn.h.sample rename to apps/desktop/desktop_native/win_webauthn/include/webauthn.h index 2f50e771bed..b5eaca30f1c 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/samples/webauthn.h.sample +++ b/apps/desktop/desktop_native/win_webauthn/include/webauthn.h @@ -1,1727 +1,1381 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -#ifndef __WEBAUTHN_H_ -#define __WEBAUTHN_H_ - -#pragma once - -#include - -#pragma region Desktop Family or OneCore Family -#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_APP | WINAPI_PARTITION_SYSTEM) - -#ifdef __cplusplus -extern "C" { -#endif - -#ifndef WINAPI -#define WINAPI __stdcall -#endif - -#ifndef INITGUID -#define INITGUID -#include -#undef INITGUID -#else -#include -#endif - -//+------------------------------------------------------------------------------------------ -// API Version Information. -// Caller should check for WebAuthNGetApiVersionNumber to check the presence of relevant APIs -// and features for their usage. -//------------------------------------------------------------------------------------------- - -#define WEBAUTHN_API_VERSION_1 1 -// WEBAUTHN_API_VERSION_1 : Baseline Version -// Data Structures and their sub versions: -// - WEBAUTHN_RP_ENTITY_INFORMATION : 1 -// - WEBAUTHN_USER_ENTITY_INFORMATION : 1 -// - WEBAUTHN_CLIENT_DATA : 1 -// - WEBAUTHN_COSE_CREDENTIAL_PARAMETER : 1 -// - WEBAUTHN_COSE_CREDENTIAL_PARAMETERS : Not Applicable -// - WEBAUTHN_CREDENTIAL : 1 -// - WEBAUTHN_CREDENTIALS : Not Applicable -// - WEBAUTHN_CREDENTIAL_EX : 1 -// - WEBAUTHN_CREDENTIAL_LIST : Not Applicable -// - WEBAUTHN_EXTENSION : Not Applicable -// - WEBAUTHN_EXTENSIONS : Not Applicable -// - WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS : 3 -// - WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS : 4 -// - WEBAUTHN_COMMON_ATTESTATION : 1 -// - WEBAUTHN_CREDENTIAL_ATTESTATION : 3 -// - WEBAUTHN_ASSERTION : 1 -// Extensions: -// - WEBAUTHN_EXTENSIONS_IDENTIFIER_HMAC_SECRET -// APIs: -// - WebAuthNGetApiVersionNumber -// - WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable -// - WebAuthNAuthenticatorMakeCredential -// - WebAuthNAuthenticatorGetAssertion -// - WebAuthNFreeCredentialAttestation -// - WebAuthNFreeAssertion -// - WebAuthNGetCancellationId -// - WebAuthNCancelCurrentOperation -// - WebAuthNGetErrorName -// - WebAuthNGetW3CExceptionDOMError -// Transports: -// - WEBAUTHN_CTAP_TRANSPORT_USB -// - WEBAUTHN_CTAP_TRANSPORT_NFC -// - WEBAUTHN_CTAP_TRANSPORT_BLE -// - WEBAUTHN_CTAP_TRANSPORT_INTERNAL - -#define WEBAUTHN_API_VERSION_2 2 -// WEBAUTHN_API_VERSION_2 : Delta From WEBAUTHN_API_VERSION_1 -// Added Extensions: -// - WEBAUTHN_EXTENSIONS_IDENTIFIER_CRED_PROTECT -// - -#define WEBAUTHN_API_VERSION_3 3 -// WEBAUTHN_API_VERSION_3 : Delta From WEBAUTHN_API_VERSION_2 -// Data Structures and their sub versions: -// - WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS : 4 -// - WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS : 5 -// - WEBAUTHN_CREDENTIAL_ATTESTATION : 4 -// - WEBAUTHN_ASSERTION : 2 -// Added Extensions: -// - WEBAUTHN_EXTENSIONS_IDENTIFIER_CRED_BLOB -// - WEBAUTHN_EXTENSIONS_IDENTIFIER_MIN_PIN_LENGTH -// - -#define WEBAUTHN_API_VERSION_4 4 -// WEBAUTHN_API_VERSION_4 : Delta From WEBAUTHN_API_VERSION_3 -// Data Structures and their sub versions: -// - WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS : 5 -// - WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS : 6 -// - WEBAUTHN_ASSERTION : 3 -// - WEBAUTHN_CREDENTIAL_DETAILS : 1 -// APIs: -// - WebAuthNGetPlatformCredentialList -// - WebAuthNFreePlatformCredentialList -// - WebAuthNDeletePlatformCredential -// - -#define WEBAUTHN_API_VERSION_5 5 -// WEBAUTHN_API_VERSION_5 : Delta From WEBAUTHN_API_VERSION_4 -// Data Structures and their sub versions: -// - WEBAUTHN_CREDENTIAL_DETAILS : 2 -// Extension Changes: -// - Enabled LARGE_BLOB Support -// - -#define WEBAUTHN_API_VERSION_6 6 -// WEBAUTHN_API_VERSION_6 : Delta From WEBAUTHN_API_VERSION_5 -// Data Structures and their sub versions: -// - WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS : 6 -// - WEBAUTHN_CREDENTIAL_ATTESTATION : 5 -// - WEBAUTHN_ASSERTION : 4 -// Transports: -// - WEBAUTHN_CTAP_TRANSPORT_HYBRID - -#define WEBAUTHN_API_VERSION_7 7 -// WEBAUTHN_API_VERSION_7 : Delta From WEBAUTHN_API_VERSION_6 -// Data Structures and their sub versions: -// - WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS : 7 -// - WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS : 7 -// - WEBAUTHN_CREDENTIAL_ATTESTATION : 6 -// - WEBAUTHN_ASSERTION : 5 - -#define WEBAUTHN_API_VERSION_8 8 -// WEBAUTHN_API_VERSION_8 : Delta From WEBAUTHN_API_VERSION_7 -// Data Structures and their sub versions: -// - WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS : 8 -// - WEBAUTHN_CREDENTIAL_DETAILS : 3 -// - WEBAUTHN_CREDENTIAL_ATTESTATION : 7 -// - WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS : 8 - -#define WEBAUTHN_API_CURRENT_VERSION WEBAUTHN_API_VERSION_8 - -//+------------------------------------------------------------------------------------------ -// Information about an RP Entity -//------------------------------------------------------------------------------------------- - -#define WEBAUTHN_RP_ENTITY_INFORMATION_CURRENT_VERSION 1 - -typedef struct _WEBAUTHN_RP_ENTITY_INFORMATION { - // Version of this structure, to allow for modifications in the future. - // This field is required and should be set to CURRENT_VERSION above. - DWORD dwVersion; - - // Identifier for the RP. This field is required. - PCWSTR pwszId; - - // Contains the friendly name of the Relying Party, such as "Acme Corporation", "Widgets Inc" or "Awesome Site". - // This field is required. - PCWSTR pwszName; - - // Optional URL pointing to RP's logo. - PCWSTR pwszIcon; -} WEBAUTHN_RP_ENTITY_INFORMATION, *PWEBAUTHN_RP_ENTITY_INFORMATION; -typedef const WEBAUTHN_RP_ENTITY_INFORMATION *PCWEBAUTHN_RP_ENTITY_INFORMATION; - -//+------------------------------------------------------------------------------------------ -// Information about an User Entity -//------------------------------------------------------------------------------------------- -#define WEBAUTHN_MAX_USER_ID_LENGTH 64 - -#define WEBAUTHN_USER_ENTITY_INFORMATION_CURRENT_VERSION 1 - -typedef struct _WEBAUTHN_USER_ENTITY_INFORMATION { - // Version of this structure, to allow for modifications in the future. - // This field is required and should be set to CURRENT_VERSION above. - DWORD dwVersion; - - // Identifier for the User. This field is required. - DWORD cbId; - _Field_size_bytes_(cbId) - PBYTE pbId; - - // Contains a detailed name for this account, such as "john.p.smith@example.com". - PCWSTR pwszName; - - // Optional URL that can be used to retrieve an image containing the user's current avatar, - // or a data URI that contains the image data. - PCWSTR pwszIcon; - - // For User: Contains the friendly name associated with the user account by the Relying Party, such as "John P. Smith". - PCWSTR pwszDisplayName; -} WEBAUTHN_USER_ENTITY_INFORMATION, *PWEBAUTHN_USER_ENTITY_INFORMATION; -typedef const WEBAUTHN_USER_ENTITY_INFORMATION *PCWEBAUTHN_USER_ENTITY_INFORMATION; - -//+------------------------------------------------------------------------------------------ -// Information about client data. -//------------------------------------------------------------------------------------------- - -#define WEBAUTHN_HASH_ALGORITHM_SHA_256 L"SHA-256" -#define WEBAUTHN_HASH_ALGORITHM_SHA_384 L"SHA-384" -#define WEBAUTHN_HASH_ALGORITHM_SHA_512 L"SHA-512" - -#define WEBAUTHN_CLIENT_DATA_CURRENT_VERSION 1 - -typedef struct _WEBAUTHN_CLIENT_DATA { - // Version of this structure, to allow for modifications in the future. - // This field is required and should be set to CURRENT_VERSION above. - DWORD dwVersion; - - // Size of the pbClientDataJSON field. - DWORD cbClientDataJSON; - // UTF-8 encoded JSON serialization of the client data. - _Field_size_bytes_(cbClientDataJSON) - PBYTE pbClientDataJSON; - - // Hash algorithm ID used to hash the pbClientDataJSON field. - LPCWSTR pwszHashAlgId; -} WEBAUTHN_CLIENT_DATA, *PWEBAUTHN_CLIENT_DATA; -typedef const WEBAUTHN_CLIENT_DATA *PCWEBAUTHN_CLIENT_DATA; - -//+------------------------------------------------------------------------------------------ -// Information about credential parameters. -//------------------------------------------------------------------------------------------- - -#define WEBAUTHN_CREDENTIAL_TYPE_PUBLIC_KEY L"public-key" - -#define WEBAUTHN_COSE_ALGORITHM_ECDSA_P256_WITH_SHA256 -7 -#define WEBAUTHN_COSE_ALGORITHM_ECDSA_P384_WITH_SHA384 -35 -#define WEBAUTHN_COSE_ALGORITHM_ECDSA_P521_WITH_SHA512 -36 - -#define WEBAUTHN_COSE_ALGORITHM_RSASSA_PKCS1_V1_5_WITH_SHA256 -257 -#define WEBAUTHN_COSE_ALGORITHM_RSASSA_PKCS1_V1_5_WITH_SHA384 -258 -#define WEBAUTHN_COSE_ALGORITHM_RSASSA_PKCS1_V1_5_WITH_SHA512 -259 - -#define WEBAUTHN_COSE_ALGORITHM_RSA_PSS_WITH_SHA256 -37 -#define WEBAUTHN_COSE_ALGORITHM_RSA_PSS_WITH_SHA384 -38 -#define WEBAUTHN_COSE_ALGORITHM_RSA_PSS_WITH_SHA512 -39 - -#define WEBAUTHN_COSE_CREDENTIAL_PARAMETER_CURRENT_VERSION 1 - -typedef struct _WEBAUTHN_COSE_CREDENTIAL_PARAMETER { - // Version of this structure, to allow for modifications in the future. - DWORD dwVersion; - - // Well-known credential type specifying a credential to create. - LPCWSTR pwszCredentialType; - - // Well-known COSE algorithm specifying the algorithm to use for the credential. - LONG lAlg; -} WEBAUTHN_COSE_CREDENTIAL_PARAMETER, *PWEBAUTHN_COSE_CREDENTIAL_PARAMETER; -typedef const WEBAUTHN_COSE_CREDENTIAL_PARAMETER *PCWEBAUTHN_COSE_CREDENTIAL_PARAMETER; - -typedef struct _WEBAUTHN_COSE_CREDENTIAL_PARAMETERS { - DWORD cCredentialParameters; - _Field_size_(cCredentialParameters) - PWEBAUTHN_COSE_CREDENTIAL_PARAMETER pCredentialParameters; -} WEBAUTHN_COSE_CREDENTIAL_PARAMETERS, *PWEBAUTHN_COSE_CREDENTIAL_PARAMETERS; -typedef const WEBAUTHN_COSE_CREDENTIAL_PARAMETERS *PCWEBAUTHN_COSE_CREDENTIAL_PARAMETERS; - -//+------------------------------------------------------------------------------------------ -// Information about credential. -//------------------------------------------------------------------------------------------- -#define WEBAUTHN_CREDENTIAL_CURRENT_VERSION 1 - -typedef struct _WEBAUTHN_CREDENTIAL { - // Version of this structure, to allow for modifications in the future. - DWORD dwVersion; - - // Size of pbID. - DWORD cbId; - // Unique ID for this particular credential. - _Field_size_bytes_(cbId) - PBYTE pbId; - - // Well-known credential type specifying what this particular credential is. - LPCWSTR pwszCredentialType; -} WEBAUTHN_CREDENTIAL, *PWEBAUTHN_CREDENTIAL; -typedef const WEBAUTHN_CREDENTIAL *PCWEBAUTHN_CREDENTIAL; - -typedef struct _WEBAUTHN_CREDENTIALS { - DWORD cCredentials; - _Field_size_(cCredentials) - PWEBAUTHN_CREDENTIAL pCredentials; -} WEBAUTHN_CREDENTIALS, *PWEBAUTHN_CREDENTIALS; -typedef const WEBAUTHN_CREDENTIALS *PCWEBAUTHN_CREDENTIALS; - -//+------------------------------------------------------------------------------------------ -// Information about credential with extra information, such as, dwTransports -//------------------------------------------------------------------------------------------- - -#define WEBAUTHN_CTAP_TRANSPORT_USB 0x00000001 -#define WEBAUTHN_CTAP_TRANSPORT_NFC 0x00000002 -#define WEBAUTHN_CTAP_TRANSPORT_BLE 0x00000004 -#define WEBAUTHN_CTAP_TRANSPORT_TEST 0x00000008 -#define WEBAUTHN_CTAP_TRANSPORT_INTERNAL 0x00000010 -#define WEBAUTHN_CTAP_TRANSPORT_HYBRID 0x00000020 -#define WEBAUTHN_CTAP_TRANSPORT_FLAGS_MASK 0x0000003F - -#define WEBAUTHN_CREDENTIAL_EX_CURRENT_VERSION 1 - -typedef struct _WEBAUTHN_CREDENTIAL_EX { - // Version of this structure, to allow for modifications in the future. - DWORD dwVersion; - - // Size of pbID. - DWORD cbId; - // Unique ID for this particular credential. - _Field_size_bytes_(cbId) - PBYTE pbId; - - // Well-known credential type specifying what this particular credential is. - LPCWSTR pwszCredentialType; - - // Transports. 0 implies no transport restrictions. - DWORD dwTransports; -} WEBAUTHN_CREDENTIAL_EX, *PWEBAUTHN_CREDENTIAL_EX; -typedef const WEBAUTHN_CREDENTIAL_EX *PCWEBAUTHN_CREDENTIAL_EX; - -//+------------------------------------------------------------------------------------------ -// Information about credential list with extra information -//------------------------------------------------------------------------------------------- - -typedef struct _WEBAUTHN_CREDENTIAL_LIST { - DWORD cCredentials; - _Field_size_(cCredentials) - PWEBAUTHN_CREDENTIAL_EX *ppCredentials; -} WEBAUTHN_CREDENTIAL_LIST, *PWEBAUTHN_CREDENTIAL_LIST; -typedef const WEBAUTHN_CREDENTIAL_LIST *PCWEBAUTHN_CREDENTIAL_LIST; - -//+------------------------------------------------------------------------------------------ -// Information about linked devices -//------------------------------------------------------------------------------------------- - -#define CTAPCBOR_HYBRID_STORAGE_LINKED_DATA_VERSION_1 1 -#define CTAPCBOR_HYBRID_STORAGE_LINKED_DATA_CURRENT_VERSION CTAPCBOR_HYBRID_STORAGE_LINKED_DATA_VERSION_1 - -typedef struct _CTAPCBOR_HYBRID_STORAGE_LINKED_DATA -{ - // Version - DWORD dwVersion; - - // Contact Id - DWORD cbContactId; - _Field_size_bytes_(cbContactId) - PBYTE pbContactId; - - // Link Id - DWORD cbLinkId; - _Field_size_bytes_(cbLinkId) - PBYTE pbLinkId; - - // Link secret - DWORD cbLinkSecret; - _Field_size_bytes_(cbLinkSecret) - PBYTE pbLinkSecret; - - // Authenticator Public Key - DWORD cbPublicKey; - _Field_size_bytes_(cbPublicKey) - PBYTE pbPublicKey; - - // Authenticator Name - PCWSTR pwszAuthenticatorName; - - // Tunnel server domain - WORD wEncodedTunnelServerDomain; -} CTAPCBOR_HYBRID_STORAGE_LINKED_DATA, *PCTAPCBOR_HYBRID_STORAGE_LINKED_DATA; -typedef const CTAPCBOR_HYBRID_STORAGE_LINKED_DATA *PCCTAPCBOR_HYBRID_STORAGE_LINKED_DATA; - -//+------------------------------------------------------------------------------------------ -// Credential Information for WebAuthNGetPlatformCredentialList API -//------------------------------------------------------------------------------------------- - -#define WEBAUTHN_CREDENTIAL_DETAILS_VERSION_1 1 -#define WEBAUTHN_CREDENTIAL_DETAILS_VERSION_2 2 -#define WEBAUTHN_CREDENTIAL_DETAILS_VERSION_3 3 -#define WEBAUTHN_CREDENTIAL_DETAILS_CURRENT_VERSION WEBAUTHN_CREDENTIAL_DETAILS_VERSION_3 - -typedef struct _WEBAUTHN_CREDENTIAL_DETAILS { - // Version of this structure, to allow for modifications in the future. - DWORD dwVersion; - - // Size of pbCredentialID. - DWORD cbCredentialID; - _Field_size_bytes_(cbCredentialID) - PBYTE pbCredentialID; - - // RP Info - PWEBAUTHN_RP_ENTITY_INFORMATION pRpInformation; - - // User Info - PWEBAUTHN_USER_ENTITY_INFORMATION pUserInformation; - - // Removable or not. - BOOL bRemovable; - - // - // The following fields have been added in WEBAUTHN_CREDENTIAL_DETAILS_VERSION_2 - // - - // Backed Up or not. - BOOL bBackedUp; - - // - // The following fields have been added in WEBAUTHN_CREDENTIAL_DETAILS_VERSION_3 - // - PCWSTR pwszAuthenticatorName; - - // The logo is expected to be in the svg format - DWORD cbAuthenticatorLogo; - _Field_size_bytes_(cbAuthenticatorLogo) - PBYTE pbAuthenticatorLogo; - - // ThirdPartyPayment Credential or not. - BOOL bThirdPartyPayment; - -} WEBAUTHN_CREDENTIAL_DETAILS, *PWEBAUTHN_CREDENTIAL_DETAILS; -typedef const WEBAUTHN_CREDENTIAL_DETAILS *PCWEBAUTHN_CREDENTIAL_DETAILS; - -typedef struct _WEBAUTHN_CREDENTIAL_DETAILS_LIST { - DWORD cCredentialDetails; - _Field_size_(cCredentialDetails) - PWEBAUTHN_CREDENTIAL_DETAILS *ppCredentialDetails; -} WEBAUTHN_CREDENTIAL_DETAILS_LIST, *PWEBAUTHN_CREDENTIAL_DETAILS_LIST; -typedef const WEBAUTHN_CREDENTIAL_DETAILS_LIST *PCWEBAUTHN_CREDENTIAL_DETAILS_LIST; - -#define WEBAUTHN_GET_CREDENTIALS_OPTIONS_VERSION_1 1 -#define WEBAUTHN_GET_CREDENTIALS_OPTIONS_CURRENT_VERSION WEBAUTHN_GET_CREDENTIALS_OPTIONS_VERSION_1 - -typedef struct _WEBAUTHN_GET_CREDENTIALS_OPTIONS { - // Version of this structure, to allow for modifications in the future. - DWORD dwVersion; - - // Optional. - LPCWSTR pwszRpId; - - // Optional. BrowserInPrivate Mode. Defaulting to FALSE. - BOOL bBrowserInPrivateMode; -} WEBAUTHN_GET_CREDENTIALS_OPTIONS, *PWEBAUTHN_GET_CREDENTIALS_OPTIONS; -typedef const WEBAUTHN_GET_CREDENTIALS_OPTIONS *PCWEBAUTHN_GET_CREDENTIALS_OPTIONS; - -//+------------------------------------------------------------------------------------------ -// PRF values. -//------------------------------------------------------------------------------------------- - -#define WEBAUTHN_CTAP_ONE_HMAC_SECRET_LENGTH 32 - -// SALT values below by default are converted into RAW Hmac-Secret values as per PRF extension. -// - SHA-256(UTF8Encode("WebAuthn PRF") || 0x00 || Value) -// -// Set WEBAUTHN_AUTHENTICATOR_HMAC_SECRET_VALUES_FLAG in dwFlags in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS, -// if caller wants to provide RAW Hmac-Secret SALT values directly. In that case, -// values if provided MUST be of WEBAUTHN_CTAP_ONE_HMAC_SECRET_LENGTH size. - -typedef struct _WEBAUTHN_HMAC_SECRET_SALT { - // Size of pbFirst. - DWORD cbFirst; - _Field_size_bytes_(cbFirst) - PBYTE pbFirst; // Required - - // Size of pbSecond. - DWORD cbSecond; - _Field_size_bytes_(cbSecond) - PBYTE pbSecond; -} WEBAUTHN_HMAC_SECRET_SALT, *PWEBAUTHN_HMAC_SECRET_SALT; -typedef const WEBAUTHN_HMAC_SECRET_SALT *PCWEBAUTHN_HMAC_SECRET_SALT; - -typedef struct _WEBAUTHN_CRED_WITH_HMAC_SECRET_SALT { - // Size of pbCredID. - DWORD cbCredID; - _Field_size_bytes_(cbCredID) - PBYTE pbCredID; // Required - - // PRF Values for above credential - PWEBAUTHN_HMAC_SECRET_SALT pHmacSecretSalt; // Required -} WEBAUTHN_CRED_WITH_HMAC_SECRET_SALT, *PWEBAUTHN_CRED_WITH_HMAC_SECRET_SALT; -typedef const WEBAUTHN_CRED_WITH_HMAC_SECRET_SALT *PCWEBAUTHN_CRED_WITH_HMAC_SECRET_SALT; - -typedef struct _WEBAUTHN_HMAC_SECRET_SALT_VALUES { - PWEBAUTHN_HMAC_SECRET_SALT pGlobalHmacSalt; - - DWORD cCredWithHmacSecretSaltList; - _Field_size_(cCredWithHmacSecretSaltList) - PWEBAUTHN_CRED_WITH_HMAC_SECRET_SALT pCredWithHmacSecretSaltList; -} WEBAUTHN_HMAC_SECRET_SALT_VALUES, *PWEBAUTHN_HMAC_SECRET_SALT_VALUES; -typedef const WEBAUTHN_HMAC_SECRET_SALT_VALUES *PCWEBAUTHN_HMAC_SECRET_SALT_VALUES; - -//+------------------------------------------------------------------------------------------ -// Hmac-Secret extension -//------------------------------------------------------------------------------------------- - -#define WEBAUTHN_EXTENSIONS_IDENTIFIER_HMAC_SECRET L"hmac-secret" -// Below type definitions is for WEBAUTHN_EXTENSIONS_IDENTIFIER_HMAC_SECRET -// MakeCredential Input Type: BOOL. -// - pvExtension must point to a BOOL with the value TRUE. -// - cbExtension must contain the sizeof(BOOL). -// MakeCredential Output Type: BOOL. -// - pvExtension will point to a BOOL with the value TRUE if credential -// was successfully created with HMAC_SECRET. -// - cbExtension will contain the sizeof(BOOL). -// GetAssertion Input Type: Not Supported -// GetAssertion Output Type: Not Supported - -//+------------------------------------------------------------------------------------------ -// credProtect extension -//------------------------------------------------------------------------------------------- - -#define WEBAUTHN_USER_VERIFICATION_ANY 0 -#define WEBAUTHN_USER_VERIFICATION_OPTIONAL 1 -#define WEBAUTHN_USER_VERIFICATION_OPTIONAL_WITH_CREDENTIAL_ID_LIST 2 -#define WEBAUTHN_USER_VERIFICATION_REQUIRED 3 - -typedef struct _WEBAUTHN_CRED_PROTECT_EXTENSION_IN { - // One of the above WEBAUTHN_USER_VERIFICATION_* values - DWORD dwCredProtect; - // Set the following to TRUE to require authenticator support for the credProtect extension - BOOL bRequireCredProtect; -} WEBAUTHN_CRED_PROTECT_EXTENSION_IN, *PWEBAUTHN_CRED_PROTECT_EXTENSION_IN; -typedef const WEBAUTHN_CRED_PROTECT_EXTENSION_IN *PCWEBAUTHN_CRED_PROTECT_EXTENSION_IN; - - -#define WEBAUTHN_EXTENSIONS_IDENTIFIER_CRED_PROTECT L"credProtect" -// Below type definitions is for WEBAUTHN_EXTENSIONS_IDENTIFIER_CRED_PROTECT -// MakeCredential Input Type: WEBAUTHN_CRED_PROTECT_EXTENSION_IN. -// - pvExtension must point to a WEBAUTHN_CRED_PROTECT_EXTENSION_IN struct -// - cbExtension will contain the sizeof(WEBAUTHN_CRED_PROTECT_EXTENSION_IN). -// MakeCredential Output Type: DWORD. -// - pvExtension will point to a DWORD with one of the above WEBAUTHN_USER_VERIFICATION_* values -// if credential was successfully created with CRED_PROTECT. -// - cbExtension will contain the sizeof(DWORD). -// GetAssertion Input Type: Not Supported -// GetAssertion Output Type: Not Supported - -//+------------------------------------------------------------------------------------------ -// credBlob extension -//------------------------------------------------------------------------------------------- - -typedef struct _WEBAUTHN_CRED_BLOB_EXTENSION { - // Size of pbCredBlob. - DWORD cbCredBlob; - _Field_size_bytes_(cbCredBlob) - PBYTE pbCredBlob; -} WEBAUTHN_CRED_BLOB_EXTENSION, *PWEBAUTHN_CRED_BLOB_EXTENSION; -typedef const WEBAUTHN_CRED_BLOB_EXTENSION *PCWEBAUTHN_CRED_BLOB_EXTENSION; - - -#define WEBAUTHN_EXTENSIONS_IDENTIFIER_CRED_BLOB L"credBlob" -// Below type definitions is for WEBAUTHN_EXTENSIONS_IDENTIFIER_CRED_BLOB -// MakeCredential Input Type: WEBAUTHN_CRED_BLOB_EXTENSION. -// - pvExtension must point to a WEBAUTHN_CRED_BLOB_EXTENSION struct -// - cbExtension must contain the sizeof(WEBAUTHN_CRED_BLOB_EXTENSION). -// MakeCredential Output Type: BOOL. -// - pvExtension will point to a BOOL with the value TRUE if credBlob was successfully created -// - cbExtension will contain the sizeof(BOOL). -// GetAssertion Input Type: BOOL. -// - pvExtension must point to a BOOL with the value TRUE to request the credBlob. -// - cbExtension must contain the sizeof(BOOL). -// GetAssertion Output Type: WEBAUTHN_CRED_BLOB_EXTENSION. -// - pvExtension will point to a WEBAUTHN_CRED_BLOB_EXTENSION struct if the authenticator -// returns the credBlob in the signed extensions -// - cbExtension will contain the sizeof(WEBAUTHN_CRED_BLOB_EXTENSION). - -//+------------------------------------------------------------------------------------------ -// minPinLength extension -//------------------------------------------------------------------------------------------- - -#define WEBAUTHN_EXTENSIONS_IDENTIFIER_MIN_PIN_LENGTH L"minPinLength" -// Below type definitions is for WEBAUTHN_EXTENSIONS_IDENTIFIER_MIN_PIN_LENGTH -// MakeCredential Input Type: BOOL. -// - pvExtension must point to a BOOL with the value TRUE to request the minPinLength. -// - cbExtension must contain the sizeof(BOOL). -// MakeCredential Output Type: DWORD. -// - pvExtension will point to a DWORD with the minimum pin length if returned by the authenticator -// - cbExtension will contain the sizeof(DWORD). -// GetAssertion Input Type: Not Supported -// GetAssertion Output Type: Not Supported - -//+------------------------------------------------------------------------------------------ -// Information about Extensions. -//------------------------------------------------------------------------------------------- -typedef struct _WEBAUTHN_EXTENSION { - LPCWSTR pwszExtensionIdentifier; - DWORD cbExtension; - PVOID pvExtension; -} WEBAUTHN_EXTENSION, *PWEBAUTHN_EXTENSION; -typedef const WEBAUTHN_EXTENSION *PCWEBAUTHN_EXTENSION; - -typedef struct _WEBAUTHN_EXTENSIONS { - DWORD cExtensions; - _Field_size_(cExtensions) - PWEBAUTHN_EXTENSION pExtensions; -} WEBAUTHN_EXTENSIONS, *PWEBAUTHN_EXTENSIONS; -typedef const WEBAUTHN_EXTENSIONS *PCWEBAUTHN_EXTENSIONS; - -//+------------------------------------------------------------------------------------------ -// Options. -//------------------------------------------------------------------------------------------- - -#define WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY 0 -#define WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM 1 -#define WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM 2 -#define WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM_U2F_V2 3 - -#define WEBAUTHN_USER_VERIFICATION_REQUIREMENT_ANY 0 -#define WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED 1 -#define WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED 2 -#define WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED 3 - -#define WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_ANY 0 -#define WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE 1 -#define WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT 2 -#define WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT 3 - -#define WEBAUTHN_ENTERPRISE_ATTESTATION_NONE 0 -#define WEBAUTHN_ENTERPRISE_ATTESTATION_VENDOR_FACILITATED 1 -#define WEBAUTHN_ENTERPRISE_ATTESTATION_PLATFORM_MANAGED 2 - -#define WEBAUTHN_LARGE_BLOB_SUPPORT_NONE 0 -#define WEBAUTHN_LARGE_BLOB_SUPPORT_REQUIRED 1 -#define WEBAUTHN_LARGE_BLOB_SUPPORT_PREFERRED 2 - -#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_1 1 -#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_2 2 -#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_3 3 -#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_4 4 -#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_5 5 -#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_6 6 -#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_7 7 -#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_8 8 -#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_CURRENT_VERSION WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_8 - -typedef struct _WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS { - // Version of this structure, to allow for modifications in the future. - DWORD dwVersion; - - // Time that the operation is expected to complete within. - // This is used as guidance, and can be overridden by the platform. - DWORD dwTimeoutMilliseconds; - - // Credentials used for exclusion. - WEBAUTHN_CREDENTIALS CredentialList; - - // Optional extensions to parse when performing the operation. - WEBAUTHN_EXTENSIONS Extensions; - - // Optional. Platform vs Cross-Platform Authenticators. - DWORD dwAuthenticatorAttachment; - - // Optional. Require key to be resident or not. Defaulting to FALSE. - BOOL bRequireResidentKey; - - // User Verification Requirement. - DWORD dwUserVerificationRequirement; - - // Attestation Conveyance Preference. - DWORD dwAttestationConveyancePreference; - - // Reserved for future Use - DWORD dwFlags; - - // - // The following fields have been added in WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_2 - // - - // Cancellation Id - Optional - See WebAuthNGetCancellationId - GUID *pCancellationId; - - // - // The following fields have been added in WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_3 - // - - // Exclude Credential List. If present, "CredentialList" will be ignored. - PWEBAUTHN_CREDENTIAL_LIST pExcludeCredentialList; - - // - // The following fields have been added in WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_4 - // - - // Enterprise Attestation - DWORD dwEnterpriseAttestation; - - // Large Blob Support: none, required or preferred - // - // NTE_INVALID_PARAMETER when large blob required or preferred and - // bRequireResidentKey isn't set to TRUE - DWORD dwLargeBlobSupport; - - // Optional. Prefer key to be resident. Defaulting to FALSE. When TRUE, - // overrides the above bRequireResidentKey. - BOOL bPreferResidentKey; - - // - // The following fields have been added in WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_5 - // - - // Optional. BrowserInPrivate Mode. Defaulting to FALSE. - BOOL bBrowserInPrivateMode; - - // - // The following fields have been added in WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_6 - // - - // Enable PRF - BOOL bEnablePrf; - - // - // The following fields have been added in WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_7 - // - - // Optional. Linked Device Connection Info. - PCTAPCBOR_HYBRID_STORAGE_LINKED_DATA pLinkedDevice; - - // Size of pbJsonExt - DWORD cbJsonExt; - _Field_size_bytes_(cbJsonExt) - PBYTE pbJsonExt; - - // - // The following fields have been added in WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_8 - // - - // PRF extension "eval" values which will be converted into HMAC-SECRET values according to WebAuthn Spec. - // Set WEBAUTHN_AUTHENTICATOR_HMAC_SECRET_VALUES_FLAG in dwFlags above, if caller wants to provide RAW Hmac-Secret SALT values directly. - // In that case, values provided MUST be of WEBAUTHN_CTAP_ONE_HMAC_SECRET_LENGTH size. - PWEBAUTHN_HMAC_SECRET_SALT pPRFGlobalEval; - - // PublicKeyCredentialHints (https://w3c.github.io/webauthn/#enum-hints) - DWORD cCredentialHints; - _Field_size_(cCredentialHints) - LPCWSTR *ppwszCredentialHints; - - // Enable ThirdPartyPayment - BOOL bThirdPartyPayment; - -} WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS, *PWEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS; -typedef const WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS *PCWEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS; - -#define WEBAUTHN_CRED_LARGE_BLOB_OPERATION_NONE 0 -#define WEBAUTHN_CRED_LARGE_BLOB_OPERATION_GET 1 -#define WEBAUTHN_CRED_LARGE_BLOB_OPERATION_SET 2 -#define WEBAUTHN_CRED_LARGE_BLOB_OPERATION_DELETE 3 - -#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_1 1 -#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_2 2 -#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_3 3 -#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_4 4 -#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_5 5 -#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_6 6 -#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_7 7 -#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_8 8 -#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_CURRENT_VERSION WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_8 - -/* - Information about flags. -*/ - -#define WEBAUTHN_AUTHENTICATOR_HMAC_SECRET_VALUES_FLAG 0x00100000 - -typedef struct _WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS { - // Version of this structure, to allow for modifications in the future. - DWORD dwVersion; - - // Time that the operation is expected to complete within. - // This is used as guidance, and can be overridden by the platform. - DWORD dwTimeoutMilliseconds; - - // Allowed Credentials List. - WEBAUTHN_CREDENTIALS CredentialList; - - // Optional extensions to parse when performing the operation. - WEBAUTHN_EXTENSIONS Extensions; - - // Optional. Platform vs Cross-Platform Authenticators. - DWORD dwAuthenticatorAttachment; - - // User Verification Requirement. - DWORD dwUserVerificationRequirement; - - // Flags - DWORD dwFlags; - - // - // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_2 - // - - // Optional identifier for the U2F AppId. Converted to UTF8 before being hashed. Not lower cased. - PCWSTR pwszU2fAppId; - - // If the following is non-NULL, then, set to TRUE if the above pwszU2fAppid was used instead of - // PCWSTR pwszRpId; - BOOL *pbU2fAppId; - - // - // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_3 - // - - // Cancellation Id - Optional - See WebAuthNGetCancellationId - GUID *pCancellationId; - - // - // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_4 - // - - // Allow Credential List. If present, "CredentialList" will be ignored. - PWEBAUTHN_CREDENTIAL_LIST pAllowCredentialList; - - // - // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_5 - // - - DWORD dwCredLargeBlobOperation; - - // Size of pbCredLargeBlob - DWORD cbCredLargeBlob; - _Field_size_bytes_(cbCredLargeBlob) - PBYTE pbCredLargeBlob; - - // - // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_6 - // - - // PRF values which will be converted into HMAC-SECRET values according to WebAuthn Spec. - PWEBAUTHN_HMAC_SECRET_SALT_VALUES pHmacSecretSaltValues; - - // Optional. BrowserInPrivate Mode. Defaulting to FALSE. - BOOL bBrowserInPrivateMode; - - // - // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_7 - // - - // Optional. Linked Device Connection Info. - PCTAPCBOR_HYBRID_STORAGE_LINKED_DATA pLinkedDevice; - - // Optional. Allowlist MUST contain 1 credential applicable for Hybrid transport. - BOOL bAutoFill; - - // Size of pbJsonExt - DWORD cbJsonExt; - _Field_size_bytes_(cbJsonExt) - PBYTE pbJsonExt; - - // - // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_8 - // - - // PublicKeyCredentialHints (https://w3c.github.io/webauthn/#enum-hints) - DWORD cCredentialHints; - _Field_size_(cCredentialHints) - LPCWSTR *ppwszCredentialHints; - -} WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS, *PWEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS; -typedef const WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS *PCWEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS; - - -//+------------------------------------------------------------------------------------------ -// Attestation Info. -// -//------------------------------------------------------------------------------------------- -#define WEBAUTHN_ATTESTATION_DECODE_NONE 0 -#define WEBAUTHN_ATTESTATION_DECODE_COMMON 1 -// WEBAUTHN_ATTESTATION_DECODE_COMMON supports format types -// L"packed" -// L"fido-u2f" - -#define WEBAUTHN_ATTESTATION_VER_TPM_2_0 L"2.0" - -typedef struct _WEBAUTHN_X5C { - // Length of X.509 encoded certificate - DWORD cbData; - // X.509 encoded certificate bytes - _Field_size_bytes_(cbData) - PBYTE pbData; -} WEBAUTHN_X5C, *PWEBAUTHN_X5C; - -// Supports either Self or Full Basic Attestation - -// Note, new fields will be added to the following data structure to -// support additional attestation format types, such as, TPM. -// When fields are added, the dwVersion will be incremented. -// -// Therefore, your code must make the following check: -// "if (dwVersion >= WEBAUTHN_COMMON_ATTESTATION_CURRENT_VERSION)" - -#define WEBAUTHN_COMMON_ATTESTATION_CURRENT_VERSION 1 - -typedef struct _WEBAUTHN_COMMON_ATTESTATION { - // Version of this structure, to allow for modifications in the future. - DWORD dwVersion; - - // Hash and Padding Algorithm - // - // The following won't be set for "fido-u2f" which assumes "ES256". - PCWSTR pwszAlg; - LONG lAlg; // COSE algorithm - - // Signature that was generated for this attestation. - DWORD cbSignature; - _Field_size_bytes_(cbSignature) - PBYTE pbSignature; - - // Following is set for Full Basic Attestation. If not, set then, this is Self Attestation. - // Array of X.509 DER encoded certificates. The first certificate is the signer, leaf certificate. - DWORD cX5c; - _Field_size_(cX5c) - PWEBAUTHN_X5C pX5c; - - // Following are also set for tpm - PCWSTR pwszVer; // L"2.0" - DWORD cbCertInfo; - _Field_size_bytes_(cbCertInfo) - PBYTE pbCertInfo; - DWORD cbPubArea; - _Field_size_bytes_(cbPubArea) - PBYTE pbPubArea; -} WEBAUTHN_COMMON_ATTESTATION, *PWEBAUTHN_COMMON_ATTESTATION; -typedef const WEBAUTHN_COMMON_ATTESTATION *PCWEBAUTHN_COMMON_ATTESTATION; - -#define WEBAUTHN_ATTESTATION_TYPE_PACKED L"packed" -#define WEBAUTHN_ATTESTATION_TYPE_U2F L"fido-u2f" -#define WEBAUTHN_ATTESTATION_TYPE_TPM L"tpm" -#define WEBAUTHN_ATTESTATION_TYPE_NONE L"none" - -#define WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_1 1 -#define WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_2 2 -#define WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_3 3 -#define WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_4 4 -#define WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_5 5 -#define WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_6 6 -#define WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_7 7 -#define WEBAUTHN_CREDENTIAL_ATTESTATION_CURRENT_VERSION WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_7 - -typedef struct _WEBAUTHN_CREDENTIAL_ATTESTATION { - // Version of this structure, to allow for modifications in the future. - DWORD dwVersion; - - // Attestation format type - PCWSTR pwszFormatType; - - // Size of cbAuthenticatorData. - DWORD cbAuthenticatorData; - // Authenticator data that was created for this credential. - _Field_size_bytes_(cbAuthenticatorData) - PBYTE pbAuthenticatorData; - - // Size of CBOR encoded attestation information - //0 => encoded as CBOR null value. - DWORD cbAttestation; - //Encoded CBOR attestation information - _Field_size_bytes_(cbAttestation) - PBYTE pbAttestation; - - DWORD dwAttestationDecodeType; - // Following depends on the dwAttestationDecodeType - // WEBAUTHN_ATTESTATION_DECODE_NONE - // NULL - not able to decode the CBOR attestation information - // WEBAUTHN_ATTESTATION_DECODE_COMMON - // PWEBAUTHN_COMMON_ATTESTATION; - PVOID pvAttestationDecode; - - // The CBOR encoded Attestation Object to be returned to the RP. - DWORD cbAttestationObject; - _Field_size_bytes_(cbAttestationObject) - PBYTE pbAttestationObject; - - // The CredentialId bytes extracted from the Authenticator Data. - // Used by Edge to return to the RP. - DWORD cbCredentialId; - _Field_size_bytes_(cbCredentialId) - PBYTE pbCredentialId; - - // - // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_2 - // - - WEBAUTHN_EXTENSIONS Extensions; - - // - // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_3 - // - - // One of the WEBAUTHN_CTAP_TRANSPORT_* bits will be set corresponding to - // the transport that was used. - DWORD dwUsedTransport; - - // - // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_4 - // - - BOOL bEpAtt; - BOOL bLargeBlobSupported; - BOOL bResidentKey; - - // - // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_5 - // - - BOOL bPrfEnabled; - - // - // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_6 - // - - DWORD cbUnsignedExtensionOutputs; - _Field_size_bytes_(cbUnsignedExtensionOutputs) - PBYTE pbUnsignedExtensionOutputs; - - // - // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_7 - // - - PWEBAUTHN_HMAC_SECRET_SALT pHmacSecret; - - // ThirdPartyPayment Credential or not. - BOOL bThirdPartyPayment; - -} WEBAUTHN_CREDENTIAL_ATTESTATION, *PWEBAUTHN_CREDENTIAL_ATTESTATION; -typedef const WEBAUTHN_CREDENTIAL_ATTESTATION *PCWEBAUTHN_CREDENTIAL_ATTESTATION; - - -//+------------------------------------------------------------------------------------------ -// authenticatorGetAssertion output. -//------------------------------------------------------------------------------------------- - -#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_NONE 0 -#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_SUCCESS 1 -#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_NOT_SUPPORTED 2 -#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_INVALID_DATA 3 -#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_INVALID_PARAMETER 4 -#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_NOT_FOUND 5 -#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_MULTIPLE_CREDENTIALS 6 -#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_LACK_OF_SPACE 7 -#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_PLATFORM_ERROR 8 -#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_AUTHENTICATOR_ERROR 9 - -#define WEBAUTHN_ASSERTION_VERSION_1 1 -#define WEBAUTHN_ASSERTION_VERSION_2 2 -#define WEBAUTHN_ASSERTION_VERSION_3 3 -#define WEBAUTHN_ASSERTION_VERSION_4 4 -#define WEBAUTHN_ASSERTION_VERSION_5 5 -#define WEBAUTHN_ASSERTION_CURRENT_VERSION WEBAUTHN_ASSERTION_VERSION_5 - -typedef struct _WEBAUTHN_ASSERTION { - // Version of this structure, to allow for modifications in the future. - DWORD dwVersion; - - // Size of cbAuthenticatorData. - DWORD cbAuthenticatorData; - // Authenticator data that was created for this assertion. - _Field_size_bytes_(cbAuthenticatorData) - PBYTE pbAuthenticatorData; - - // Size of pbSignature. - DWORD cbSignature; - // Signature that was generated for this assertion. - _Field_size_bytes_(cbSignature) - PBYTE pbSignature; - - // Credential that was used for this assertion. - WEBAUTHN_CREDENTIAL Credential; - - // Size of User Id - DWORD cbUserId; - // UserId - _Field_size_bytes_(cbUserId) - PBYTE pbUserId; - - // - // Following fields have been added in WEBAUTHN_ASSERTION_VERSION_2 - // - - WEBAUTHN_EXTENSIONS Extensions; - - // Size of pbCredLargeBlob - DWORD cbCredLargeBlob; - _Field_size_bytes_(cbCredLargeBlob) - PBYTE pbCredLargeBlob; - - DWORD dwCredLargeBlobStatus; - - // - // Following fields have been added in WEBAUTHN_ASSERTION_VERSION_3 - // - - PWEBAUTHN_HMAC_SECRET_SALT pHmacSecret; - - // - // Following fields have been added in WEBAUTHN_ASSERTION_VERSION_4 - // - - // One of the WEBAUTHN_CTAP_TRANSPORT_* bits will be set corresponding to - // the transport that was used. - DWORD dwUsedTransport; - - // - // Following fields have been added in WEBAUTHN_ASSERTION_VERSION_5 - // - - DWORD cbUnsignedExtensionOutputs; - _Field_size_bytes_(cbUnsignedExtensionOutputs) - PBYTE pbUnsignedExtensionOutputs; -} WEBAUTHN_ASSERTION, *PWEBAUTHN_ASSERTION; -typedef const WEBAUTHN_ASSERTION *PCWEBAUTHN_ASSERTION; - -//+------------------------------------------------------------------------------------------ -// APIs. -//------------------------------------------------------------------------------------------- - -DWORD -WINAPI -WebAuthNGetApiVersionNumber(); - -HRESULT -WINAPI -WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable( - _Out_ BOOL *pbIsUserVerifyingPlatformAuthenticatorAvailable); - - -HRESULT -WINAPI -WebAuthNAuthenticatorMakeCredential( - _In_ HWND hWnd, - _In_ PCWEBAUTHN_RP_ENTITY_INFORMATION pRpInformation, - _In_ PCWEBAUTHN_USER_ENTITY_INFORMATION pUserInformation, - _In_ PCWEBAUTHN_COSE_CREDENTIAL_PARAMETERS pPubKeyCredParams, - _In_ PCWEBAUTHN_CLIENT_DATA pWebAuthNClientData, - _In_opt_ PCWEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS pWebAuthNMakeCredentialOptions, - _Outptr_result_maybenull_ PWEBAUTHN_CREDENTIAL_ATTESTATION *ppWebAuthNCredentialAttestation); - - -HRESULT -WINAPI -WebAuthNAuthenticatorGetAssertion( - _In_ HWND hWnd, - _In_ LPCWSTR pwszRpId, - _In_ PCWEBAUTHN_CLIENT_DATA pWebAuthNClientData, - _In_opt_ PCWEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS pWebAuthNGetAssertionOptions, - _Outptr_result_maybenull_ PWEBAUTHN_ASSERTION *ppWebAuthNAssertion); - -void -WINAPI -WebAuthNFreeCredentialAttestation( - _In_opt_ PWEBAUTHN_CREDENTIAL_ATTESTATION pWebAuthNCredentialAttestation); - -void -WINAPI -WebAuthNFreeAssertion( - _In_ PWEBAUTHN_ASSERTION pWebAuthNAssertion); - -HRESULT -WINAPI -WebAuthNGetCancellationId( - _Out_ GUID* pCancellationId); - -HRESULT -WINAPI -WebAuthNCancelCurrentOperation( - _In_ const GUID* pCancellationId); - -// Returns NTE_NOT_FOUND when credentials are not found. -HRESULT -WINAPI -WebAuthNGetPlatformCredentialList( - _In_ PCWEBAUTHN_GET_CREDENTIALS_OPTIONS pGetCredentialsOptions, - _Outptr_result_maybenull_ PWEBAUTHN_CREDENTIAL_DETAILS_LIST *ppCredentialDetailsList); - -void -WINAPI -WebAuthNFreePlatformCredentialList( - _In_ PWEBAUTHN_CREDENTIAL_DETAILS_LIST pCredentialDetailsList); - -HRESULT -WINAPI -WebAuthNDeletePlatformCredential( - _In_ DWORD cbCredentialId, - _In_reads_bytes_(cbCredentialId) const BYTE *pbCredentialId - ); - -// -// Returns the following Error Names: -// L"Success" - S_OK -// L"InvalidStateError" - NTE_EXISTS -// L"ConstraintError" - HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), -// NTE_NOT_SUPPORTED, -// NTE_TOKEN_KEYSET_STORAGE_FULL -// L"NotSupportedError" - NTE_INVALID_PARAMETER -// L"NotAllowedError" - NTE_DEVICE_NOT_FOUND, -// NTE_NOT_FOUND, -// HRESULT_FROM_WIN32(ERROR_CANCELLED), -// NTE_USER_CANCELLED, -// HRESULT_FROM_WIN32(ERROR_TIMEOUT) -// L"UnknownError" - All other hr values -// -PCWSTR -WINAPI -WebAuthNGetErrorName( - _In_ HRESULT hr); - -HRESULT -WINAPI -WebAuthNGetW3CExceptionDOMError( - _In_ HRESULT hr); - -typedef enum _EXPERIMENTAL_PLUGIN_AUTHENTICATOR_STATE -{ - PluginAuthenticatorState_Unknown = 0, - PluginAuthenticatorState_Disabled, - PluginAuthenticatorState_Enabled -} EXPERIMENTAL_PLUGIN_AUTHENTICATOR_STATE; - -// -// Plugin Authenticator API: WebAuthNPluginGetAuthenticatorState: Get Plugin Authenticator State -// -HRESULT -WINAPI -EXPERIMENTAL_WebAuthNPluginGetAuthenticatorState( - _In_ LPCWSTR pwszPluginClsId, - _Out_ EXPERIMENTAL_PLUGIN_AUTHENTICATOR_STATE* pluginAuthenticatorState -); - -// -// Plugin Authenticator API: WebAuthNAddPluginAuthenticator: Add Plugin Authenticator -// - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS { - // Authenticator Name - LPCWSTR pwszAuthenticatorName; - - // Plugin COM ClsId - LPCWSTR pwszPluginClsId; - - // Plugin RPID (Optional. Required for a nested WebAuthN call originating from a plugin) - LPCWSTR pwszPluginRpId; - - // Plugin Authenticator Logo for the Light themes. base64 svg (Optional) - LPCWSTR pwszLightThemeLogo; - - // Plugin Authenticator Logo for the Dark themes. base64 svg (Optional) - LPCWSTR pwszDarkThemeLogo; - - // CTAP CBOR encoded authenticatorGetInfo - DWORD cbAuthenticatorInfo; - _Field_size_bytes_(cbAuthenticatorInfo) - PBYTE pbAuthenticatorInfo; - -} EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS, *EXPERIMENTAL_PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS; -typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS; - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE { - // Plugin operation signing Public Key - Used to sign the request in the EXPERIMENTAL_PluginPerformOperation. Refer pluginauthenticator.h. - DWORD cbOpSignPubKey; - _Field_size_bytes_(cbOpSignPubKey) - PBYTE pbOpSignPubKey; - -} EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE, *EXPERIMENTAL_PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE; -typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE; - -HRESULT -WINAPI -EXPERIMENTAL_WebAuthNPluginAddAuthenticator( - _In_ EXPERIMENTAL_PCWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS pPluginAddAuthenticatorOptions, - _Outptr_result_maybenull_ EXPERIMENTAL_PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE *ppPluginAddAuthenticatorResponse); - -void -WINAPI -EXPERIMENTAL_WebAuthNPluginFreeAddAuthenticatorResponse( - _In_opt_ EXPERIMENTAL_PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE pPluginAddAuthenticatorResponse); - -// -// Plugin Authenticator API: WebAuthNRemovePluginAuthenticator: Remove Plugin Authenticator -// - -HRESULT -WINAPI -EXPERIMENTAL_WebAuthNPluginRemoveAuthenticator( - _In_ LPCWSTR pwszPluginClsId); - -// -// Plugin Authenticator API: WebAuthNPluginAuthenticatorUpdateDetails: Update Credential Metadata for Browser AutoFill Scenarios -// - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_UPDATE_AUTHENTICATOR_DETAILS { - // Authenticator Name (Optional) - LPCWSTR pwszAuthenticatorName; - - // Plugin COM ClsId - LPCWSTR pwszPluginClsId; - - // Plugin COM New ClsId (Optional) - LPCWSTR pwszNewPluginClsId; - - // Plugin Authenticator Logo for the Light themes. base64 svg (Optional) - LPCWSTR pwszLightThemeLogo; - - // Plugin Authenticator Logo for the Dark themes. base64 svg (Optional) - LPCWSTR pwszDarkThemeLogo; - - // CTAP CBOR encoded authenticatorGetInfo (Optional) - DWORD cbAuthenticatorInfo; - _Field_size_bytes_(cbAuthenticatorInfo) - PBYTE pbAuthenticatorInfo; - -} EXPERIMENTAL_WEBAUTHN_PLUGIN_UPDATE_AUTHENTICATOR_DETAILS, *EXPERIMENTAL_PWEBAUTHN_PLUGIN_UPDATE_AUTHENTICATOR_DETAILS; -typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_UPDATE_AUTHENTICATOR_DETAILS *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_UPDATE_AUTHENTICATOR_DETAILS; - -HRESULT -WINAPI -EXPERIMENTAL_WebAuthNPluginUpdateAuthenticatorDetails( - _In_ EXPERIMENTAL_PCWEBAUTHN_PLUGIN_UPDATE_AUTHENTICATOR_DETAILS pPluginUpdateAuthenticatorDetails); - -#endif //__midl - -// -// Plugin Authenticator API: WebAuthNPluginAuthenticatorAddCredentials: Add Credential Metadata for Browser AutoFill Scenarios -// - - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS { - // Size of pbCredentialId. - DWORD cbCredentialId; - - // Credential Identifier bytes. This field is required. - #ifdef __midl - [size_is(cbCredentialId)] - #else - _Field_size_bytes_(cbCredentialId) - #endif - PBYTE pbCredentialId; - - // Identifier for the RP. This field is required. - PWSTR pwszRpId; - - // Contains the friendly name of the Relying Party, such as "Acme Corporation", "Widgets Inc" or "Awesome Site". - // This field is required. - PWSTR pwszRpName; - - // Identifier for the User. This field is required. - DWORD cbUserId; - - // User Identifier bytes. This field is required. - #ifdef __midl - [size_is(cbUserId)] - #else - _Field_size_bytes_(cbUserId) - #endif - PBYTE pbUserId; - - // Contains a detailed name for this account, such as "john.p.smith@example.com". - PWSTR pwszUserName; - - // For User: Contains the friendly name associated with the user account such as "John P. Smith". - PWSTR pwszUserDisplayName; - -} EXPERIMENTAL_WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS, *EXPERIMENTAL_PWEBAUTHN_PLUGIN_CREDENTIAL_DETAILS; -typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CREDENTIAL_DETAILS; - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS_LIST { - // Plugin COM ClsId - PWSTR pwszPluginClsId; - - // count of credentials - DWORD cCredentialDetails; - - #ifdef __midl - [size_is(cCredentialDetails)] - #else - _Field_size_(cCredentialDetails) - #endif - EXPERIMENTAL_PWEBAUTHN_PLUGIN_CREDENTIAL_DETAILS *pCredentialDetails; - -} EXPERIMENTAL_WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS_LIST, *EXPERIMENTAL_PWEBAUTHN_PLUGIN_CREDENTIAL_DETAILS_LIST; -typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS_LIST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CREDENTIAL_DETAILS_LIST; - -#ifndef __midl - -HRESULT -WINAPI -EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials( - _In_ EXPERIMENTAL_PWEBAUTHN_PLUGIN_CREDENTIAL_DETAILS_LIST pCredentialDetailsList); - -// -// Plugin Authenticator API: WebAuthNPluginAuthenticatorRemoveCredentials: Remove Credential Metadata for Browser AutoFill Scenarios -// - -HRESULT -WINAPI -EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveCredentials( - _In_ EXPERIMENTAL_PWEBAUTHN_PLUGIN_CREDENTIAL_DETAILS_LIST pCredentialDetailsList); - -// -// Plugin Authenticator API: WebAuthNPluginAuthenticatorRemoveCredentials: Remove All Credential Metadata for Browser AutoFill Scenarios -// - -HRESULT -WINAPI -EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentials( - _In_ LPCWSTR pwszPluginClsId); - -// -// Plugin Authenticator API: WebAuthNPluginAuthenticatorGetAllCredentials: Get All Credential Metadata cached for Browser AutoFill Scenarios -// -HRESULT -WINAPI -EXPERIMENTAL_WebAuthNPluginAuthenticatorGetAllCredentials( - _In_ LPCWSTR pwszPluginClsId, - _Outptr_result_maybenull_ EXPERIMENTAL_PWEBAUTHN_PLUGIN_CREDENTIAL_DETAILS_LIST *ppCredentialDetailsList); - -// -// Hello UV API for Plugin: WebAuthNPluginPerformUv: Perform Hello UV related operations -// - -typedef enum _EXPERIMENTAL_WEBAUTHN_PLUGIN_PERFORM_UV_OPERATION_TYPE -{ - PerformUv = 1, - GetUvCount, - GetPubKey -} EXPERIMENTAL_WEBAUTHN_PLUGIN_PERFORM_UV_OPERATION_TYPE; - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_PERFORM_UV { - HWND hwnd; - GUID* transactionId; - EXPERIMENTAL_WEBAUTHN_PLUGIN_PERFORM_UV_OPERATION_TYPE type; - PCWSTR pwszUsername; - PCWSTR pwszContext; -} EXPERIMENTAL_WEBAUTHN_PLUGIN_PERFORM_UV, *EXPERIMENTAL_PWEBAUTHN_PLUGIN_PERFROM_UV; -typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_PERFORM_UV *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_PERFORM_UV; - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_PERFORM_UV_RESPONSE { - DWORD cbResponse; - PBYTE pbResponse; -} EXPERIMENTAL_WEBAUTHN_PLUGIN_PERFORM_UV_RESPONSE, *EXPERIMENTAL_PWEBAUTHN_PLUGIN_PERFORM_UV_RESPONSE; -typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_PERFORM_UV_RESPONSE *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_PERFORM_UV_RESPONSE; - -HRESULT -WINAPI -EXPERIMENTAL_WebAuthNPluginPerformUv( - _In_ EXPERIMENTAL_PCWEBAUTHN_PLUGIN_PERFORM_UV pPluginPerformUv, - _Outptr_result_maybenull_ EXPERIMENTAL_PWEBAUTHN_PLUGIN_PERFORM_UV_RESPONSE *ppPluginPerformUvRespose); - -void -WINAPI -EXPERIMENTAL_WebAuthNPluginFreePerformUvResponse( - _In_opt_ EXPERIMENTAL_PWEBAUTHN_PLUGIN_PERFORM_UV_RESPONSE ppPluginPerformUvResponse); - -#define EXPERIMENTAL_WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS_VERSION_1 1 -#define EXPERIMENTAL_WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS_CURRENT_VERSION EXPERIMENTAL_WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS_VERSION_1 -typedef struct _EXPERIMENTAL_WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS { - //Version of this structure, to allow for modifications in the future. - DWORD dwVersion; - - // Following have following values: - // +1 - TRUE - // 0 - Not defined - // -1 - FALSE - //up: "true" | "false" - LONG lUp; - //uv: "true" | "false" - LONG lUv; - //rk: "true" | "false" - LONG lRequireResidentKey; -} EXPERIMENTAL_WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS, *EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS; -typedef const EXPERIMENTAL_WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS *EXPERIMENTAL_PCWEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS; - -#define EXPERIMENTAL_WEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY_VERSION_1 1 -#define EXPERIMENTAL_WEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY_CURRENT_VERSION EXPERIMENTAL_WEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY_VERSION_1 -typedef struct _EXPERIMENTAL_WEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY { - //Version of this structure, to allow for modifications in the future. - DWORD dwVersion; - - // Key type - LONG lKty; - - // Hash Algorithm: ES256, ES384, ES512 - LONG lAlg; - - // Curve - LONG lCrv; - - //Size of "x" (X Coordinate) - DWORD cbX; - - //"x" (X Coordinate) data. Big Endian. - PBYTE pbX; - - //Size of "y" (Y Coordinate) - DWORD cbY; - - //"y" (Y Coordinate) data. Big Endian. - PBYTE pbY; -} EXPERIMENTAL_WEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY, *EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY; -typedef const EXPERIMENTAL_WEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY *EXPERIMENTAL_PCWEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY; - -#define EXPERIMENTAL_WEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION_VERSION_1 1 -#define EXPERIMENTAL_WEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION_CURRENT_VERSION EXPERIMENTAL_WEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION_VERSION_1 -typedef struct _EXPERIMENTAL_WEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION { - //Version of this structure, to allow for modifications in the future. - DWORD dwVersion; - - // Platform's key agreement public key - EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY pKeyAgreement; - - DWORD cbEncryptedSalt; - PBYTE pbEncryptedSalt; - - DWORD cbSaltAuth; - PBYTE pbSaltAuth; -} EXPERIMENTAL_WEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION, *EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION; -typedef const EXPERIMENTAL_WEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION *EXPERIMENTAL_PCWEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION; - -#define EXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST_VERSION_1 1 -#define EXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST_CURRENT_VERSION EXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST_VERSION_1 -typedef struct _EXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST { - //Version of this structure, to allow for modifications in the future. - DWORD dwVersion; - - //Input RP ID. Raw UTF8 bytes before conversion. - //These are the bytes to be hashed in the Authenticator Data. - DWORD cbRpId; - PBYTE pbRpId; - - //Client Data Hash - DWORD cbClientDataHash; - PBYTE pbClientDataHash; - - //RP Information - PCWEBAUTHN_RP_ENTITY_INFORMATION pRpInformation; - - //User Information - PCWEBAUTHN_USER_ENTITY_INFORMATION pUserInformation; - - // Crypto Parameters - WEBAUTHN_COSE_CREDENTIAL_PARAMETERS WebAuthNCredentialParameters; - - //Credentials used for exclusion - WEBAUTHN_CREDENTIAL_LIST CredentialList; - - //Optional extensions to parse when performing the operation. - DWORD cbCborExtensionsMap; - PBYTE pbCborExtensionsMap; - - // Authenticator Options (Optional) - EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS pAuthenticatorOptions; - - // Pin Auth (Optional) - BOOL fEmptyPinAuth; // Zero length PinAuth is included in the request - DWORD cbPinAuth; - PBYTE pbPinAuth; - - //"hmac-secret": true extension - LONG lHmacSecretExt; - - // "hmac-secret-mc" extension - EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION pHmacSecretMcExtension; - - //"prf" extension - LONG lPrfExt; - DWORD cbHmacSecretSaltValues; - PBYTE pbHmacSecretSaltValues; - - //"credProtect" extension. Nonzero if present - DWORD dwCredProtect; - - // Nonzero if present - DWORD dwPinProtocol; - - // Nonzero if present - DWORD dwEnterpriseAttestation; - - //"credBlob" extension. Nonzero if present - DWORD cbCredBlobExt; - PBYTE pbCredBlobExt; - - //"largeBlobKey": true extension - LONG lLargeBlobKeyExt; - - //"largeBlob": extension - DWORD dwLargeBlobSupport; - - //"minPinLength": true extension - LONG lMinPinLengthExt; - - // "json" extension. Nonzero if present - DWORD cbJsonExt; - PBYTE pbJsonExt; -} EXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST, *EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST; -typedef const EXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST *EXPERIMENTAL_PCWEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST; - -_Success_(return == S_OK) -HRESULT -WINAPI -EXPERIMENTAL_WebAuthNEncodeMakeCredentialResponse( - _In_ PCWEBAUTHN_CREDENTIAL_ATTESTATION pCredentialAttestation, - _Out_ DWORD *pcbResp, - _Outptr_result_buffer_maybenull_(*pcbResp) BYTE **ppbResp - ); - -_Success_(return == S_OK) -HRESULT -WINAPI -EXPERIMENTAL_WebAuthNDecodeMakeCredentialRequest( - _In_ DWORD cbEncoded, - _In_reads_bytes_(cbEncoded) const BYTE *pbEncoded, - _Outptr_ EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST *ppMakeCredentialRequest - ); - -void -WINAPI -EXPERIMENTAL_WebAuthNFreeDecodedMakeCredentialRequest( - _In_opt_ EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST pMakeCredentialRequest - ); - -#define EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST_VERSION_1 1 -#define EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST_CURRENT_VERSION EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST_VERSION_1 -typedef struct _EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST { - //Version of this structure, to allow for modifications in the future. - DWORD dwVersion; - - //RP ID. After UTF8 to Unicode conversion, - PCWSTR pwszRpId; - - //Input RP ID. Raw UTF8 bytes before conversion. - //These are the bytes to be hashed in the Authenticator Data. - DWORD cbRpId; - PBYTE pbRpId; - - //Client Data Hash - DWORD cbClientDataHash; - PBYTE pbClientDataHash; - - //Credentials used for inclusion - WEBAUTHN_CREDENTIAL_LIST CredentialList; - - //Optional extensions to parse when performing the operation. - DWORD cbCborExtensionsMap; - PBYTE pbCborExtensionsMap; - - // Authenticator Options (Optional) - EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS pAuthenticatorOptions; - - // Pin Auth (Optional) - BOOL fEmptyPinAuth; // Zero length PinAuth is included in the request - DWORD cbPinAuth; - PBYTE pbPinAuth; - - // HMAC Salt Extension (Optional) - EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION pHmacSaltExtension; - - // PRF Extension - DWORD cbHmacSecretSaltValues; - PBYTE pbHmacSecretSaltValues; - - DWORD dwPinProtocol; - - //"credBlob": true extension - LONG lCredBlobExt; - - //"largeBlobKey": true extension - LONG lLargeBlobKeyExt; - - //"largeBlob" extension - DWORD dwCredLargeBlobOperation; - DWORD cbCredLargeBlobCompressed; - PBYTE pbCredLargeBlobCompressed; - DWORD dwCredLargeBlobOriginalSize; - - // "json" extension. Nonzero if present - DWORD cbJsonExt; - PBYTE pbJsonExt; -} EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST, *EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST; -typedef const EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST; - -_Success_(return == S_OK) -HRESULT -WINAPI -EXPERIMENTAL_WebAuthNDecodeGetAssertionRequest( - _In_ DWORD cbEncoded, - _In_reads_bytes_(cbEncoded) const BYTE *pbEncoded, - _Outptr_ EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST *ppGetAssertionRequest - ); - -void -WINAPI -EXPERIMENTAL_WebAuthNFreeDecodedGetAssertionRequest( - _In_opt_ EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST pGetAssertionRequest - ); - -typedef struct _EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_RESPONSE { - // [1] credential (optional) - // [2] authenticatorData - // [3] signature - WEBAUTHN_ASSERTION WebAuthNAssertion; - - // [4] user (optional) - PCWEBAUTHN_USER_ENTITY_INFORMATION pUserInformation; - - // [5] numberOfCredentials (optional) - DWORD dwNumberOfCredentials; - - // [6] userSelected (optional) - LONG lUserSelected; - - // [7] largeBlobKey (optional) - DWORD cbLargeBlobKey; - PBYTE pbLargeBlobKey; - - // [8] unsignedExtensionOutputs - DWORD cbUnsignedExtensionOutputs; - PBYTE pbUnsignedExtensionOutputs; -} EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_RESPONSE, *EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_GET_ASSERTION_RESPONSE; -typedef const EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_GET_ASSERTION_RESPONSE *EXPERIMENTAL_PCWEBAUTHN_CTAPCBOR_GET_ASSERTION_RESPONSE; - -_Success_(return == S_OK) -HRESULT -WINAPI -EXPERIMENTAL_WebAuthNEncodeGetAssertionResponse( - _In_ EXPERIMENTAL_PCWEBAUTHN_CTAPCBOR_GET_ASSERTION_RESPONSE pGetAssertionResponse, - _Out_ DWORD *pcbResp, - _Outptr_result_buffer_maybenull_(*pcbResp) BYTE **ppbResp - ); - -#endif //__midl - - -#ifdef __cplusplus -} // Balance extern "C" above -#endif - -#endif // WINAPI_FAMILY_PARTITION -#pragma endregion - +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#ifndef __WEBAUTHN_H_ +#define __WEBAUTHN_H_ + +#pragma once + +#include + +#pragma region Desktop Family or OneCore Family +#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_APP | WINAPI_PARTITION_SYSTEM) + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef WINAPI +#define WINAPI __stdcall +#endif + +#ifndef INITGUID +#define INITGUID +#include +#undef INITGUID +#else +#include +#endif + +//+------------------------------------------------------------------------------------------ +// API Version Information. +// Caller should check for WebAuthNGetApiVersionNumber to check the presence of relevant APIs +// and features for their usage. +//------------------------------------------------------------------------------------------- + +#define WEBAUTHN_API_VERSION_1 1 +// WEBAUTHN_API_VERSION_1 : Baseline Version +// Data Structures and their sub versions: +// - WEBAUTHN_RP_ENTITY_INFORMATION : 1 +// - WEBAUTHN_USER_ENTITY_INFORMATION : 1 +// - WEBAUTHN_CLIENT_DATA : 1 +// - WEBAUTHN_COSE_CREDENTIAL_PARAMETER : 1 +// - WEBAUTHN_COSE_CREDENTIAL_PARAMETERS : Not Applicable +// - WEBAUTHN_CREDENTIAL : 1 +// - WEBAUTHN_CREDENTIALS : Not Applicable +// - WEBAUTHN_CREDENTIAL_EX : 1 +// - WEBAUTHN_CREDENTIAL_LIST : Not Applicable +// - WEBAUTHN_EXTENSION : Not Applicable +// - WEBAUTHN_EXTENSIONS : Not Applicable +// - WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS : 3 +// - WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS : 4 +// - WEBAUTHN_COMMON_ATTESTATION : 1 +// - WEBAUTHN_CREDENTIAL_ATTESTATION : 3 +// - WEBAUTHN_ASSERTION : 1 +// Extensions: +// - WEBAUTHN_EXTENSIONS_IDENTIFIER_HMAC_SECRET +// APIs: +// - WebAuthNGetApiVersionNumber +// - WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable +// - WebAuthNAuthenticatorMakeCredential +// - WebAuthNAuthenticatorGetAssertion +// - WebAuthNFreeCredentialAttestation +// - WebAuthNFreeAssertion +// - WebAuthNGetCancellationId +// - WebAuthNCancelCurrentOperation +// - WebAuthNGetErrorName +// - WebAuthNGetW3CExceptionDOMError +// Transports: +// - WEBAUTHN_CTAP_TRANSPORT_USB +// - WEBAUTHN_CTAP_TRANSPORT_NFC +// - WEBAUTHN_CTAP_TRANSPORT_BLE +// - WEBAUTHN_CTAP_TRANSPORT_INTERNAL + +#define WEBAUTHN_API_VERSION_2 2 +// WEBAUTHN_API_VERSION_2 : Delta From WEBAUTHN_API_VERSION_1 +// Added Extensions: +// - WEBAUTHN_EXTENSIONS_IDENTIFIER_CRED_PROTECT +// + +#define WEBAUTHN_API_VERSION_3 3 +// WEBAUTHN_API_VERSION_3 : Delta From WEBAUTHN_API_VERSION_2 +// Data Structures and their sub versions: +// - WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS : 4 +// - WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS : 5 +// - WEBAUTHN_CREDENTIAL_ATTESTATION : 4 +// - WEBAUTHN_ASSERTION : 2 +// Added Extensions: +// - WEBAUTHN_EXTENSIONS_IDENTIFIER_CRED_BLOB +// - WEBAUTHN_EXTENSIONS_IDENTIFIER_MIN_PIN_LENGTH +// + +#define WEBAUTHN_API_VERSION_4 4 +// WEBAUTHN_API_VERSION_4 : Delta From WEBAUTHN_API_VERSION_3 +// Data Structures and their sub versions: +// - WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS : 5 +// - WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS : 6 +// - WEBAUTHN_ASSERTION : 3 +// - WEBAUTHN_GET_CREDENTIALS_OPTIONS : 1 +// - WEBAUTHN_CREDENTIAL_DETAILS : 1 +// APIs: +// - WebAuthNGetPlatformCredentialList +// - WebAuthNFreePlatformCredentialList +// - WebAuthNDeletePlatformCredential +// + +#define WEBAUTHN_API_VERSION_5 5 +// WEBAUTHN_API_VERSION_5 : Delta From WEBAUTHN_API_VERSION_4 +// Data Structures and their sub versions: +// - WEBAUTHN_CREDENTIAL_DETAILS : 2 +// Extension Changes: +// - Enabled LARGE_BLOB Support +// + +#define WEBAUTHN_API_VERSION_6 6 +// WEBAUTHN_API_VERSION_6 : Delta From WEBAUTHN_API_VERSION_5 +// Data Structures and their sub versions: +// - WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS : 6 +// - WEBAUTHN_CREDENTIAL_ATTESTATION : 5 +// - WEBAUTHN_ASSERTION : 4 +// Transports: +// - WEBAUTHN_CTAP_TRANSPORT_HYBRID + +#define WEBAUTHN_API_VERSION_7 7 +// WEBAUTHN_API_VERSION_7 : Delta From WEBAUTHN_API_VERSION_6 +// Data Structures and their sub versions: +// - WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS : 7 +// - WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS : 7 +// - WEBAUTHN_CREDENTIAL_ATTESTATION : 6 +// - WEBAUTHN_ASSERTION : 5 + +#define WEBAUTHN_API_VERSION_8 8 +// WEBAUTHN_API_VERSION_8 : Delta From WEBAUTHN_API_VERSION_7 +// Data Structures and their sub versions: +// - WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS : 8 +// - WEBAUTHN_CREDENTIAL_DETAILS : 3 +// - WEBAUTHN_CREDENTIAL_ATTESTATION : 7 +// - WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS : 8 +// + +#define WEBAUTHN_API_VERSION_9 9 +// WEBAUTHN_API_VERSION_9 : Delta From WEBAUTHN_API_VERSION_8 +// Data Structures and their sub versions: +// - WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS : 9 +// - WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS : 9 +// - WEBAUTHN_ASSERTION : 6 +// - WEBAUTHN_CREDENTIAL_DETAILS : 4 +// - WEBAUTHN_CREDENTIAL_ATTESTATION : 8 +// - WEBAUTHN_AUTHENTICATOR_DETAILS : 1 +// - WEBAUTHN_AUTHENTICATOR_DETAILS_LIST : Not Applicable +// APIs: +// - WebAuthNGetAuthenticatorList +// - WebAuthNFreeAuthenticatorList + +#define WEBAUTHN_API_CURRENT_VERSION WEBAUTHN_API_VERSION_9 + +//+------------------------------------------------------------------------------------------ +// Information about an RP Entity +//------------------------------------------------------------------------------------------- + +#define WEBAUTHN_RP_ENTITY_INFORMATION_CURRENT_VERSION 1 + +typedef struct _WEBAUTHN_RP_ENTITY_INFORMATION { + // Version of this structure, to allow for modifications in the future. + // This field is required and should be set to CURRENT_VERSION above. + DWORD dwVersion; + + // Identifier for the RP. This field is required. + PCWSTR pwszId; + + // Contains the friendly name of the Relying Party, such as "Acme Corporation", "Widgets Inc" or "Awesome Site". + // This field is required. + PCWSTR pwszName; + + // Optional URL pointing to RP's logo. + PCWSTR pwszIcon; +} WEBAUTHN_RP_ENTITY_INFORMATION, *PWEBAUTHN_RP_ENTITY_INFORMATION; +typedef const WEBAUTHN_RP_ENTITY_INFORMATION *PCWEBAUTHN_RP_ENTITY_INFORMATION; + +//+------------------------------------------------------------------------------------------ +// Information about an User Entity +//------------------------------------------------------------------------------------------- +#define WEBAUTHN_MAX_USER_ID_LENGTH 64 + +#define WEBAUTHN_USER_ENTITY_INFORMATION_CURRENT_VERSION 1 + +typedef struct _WEBAUTHN_USER_ENTITY_INFORMATION { + // Version of this structure, to allow for modifications in the future. + // This field is required and should be set to CURRENT_VERSION above. + DWORD dwVersion; + + // Identifier for the User. This field is required. + DWORD cbId; + _Field_size_bytes_(cbId) + PBYTE pbId; + + // Contains a detailed name for this account, such as "john.p.smith@example.com". + PCWSTR pwszName; + + // Optional URL that can be used to retrieve an image containing the user's current avatar, + // or a data URI that contains the image data. + PCWSTR pwszIcon; + + // For User: Contains the friendly name associated with the user account by the Relying Party, such as "John P. Smith". + PCWSTR pwszDisplayName; +} WEBAUTHN_USER_ENTITY_INFORMATION, *PWEBAUTHN_USER_ENTITY_INFORMATION; +typedef const WEBAUTHN_USER_ENTITY_INFORMATION *PCWEBAUTHN_USER_ENTITY_INFORMATION; + +//+------------------------------------------------------------------------------------------ +// Information about client data. +//------------------------------------------------------------------------------------------- + +#define WEBAUTHN_HASH_ALGORITHM_SHA_256 L"SHA-256" +#define WEBAUTHN_HASH_ALGORITHM_SHA_384 L"SHA-384" +#define WEBAUTHN_HASH_ALGORITHM_SHA_512 L"SHA-512" + +#define WEBAUTHN_CLIENT_DATA_CURRENT_VERSION 1 + +typedef struct _WEBAUTHN_CLIENT_DATA { + // Version of this structure, to allow for modifications in the future. + // This field is required and should be set to CURRENT_VERSION above. + DWORD dwVersion; + + // Size of the pbClientDataJSON field. + DWORD cbClientDataJSON; + // UTF-8 encoded JSON serialization of the client data. + _Field_size_bytes_(cbClientDataJSON) + PBYTE pbClientDataJSON; + + // Hash algorithm ID used to hash the pbClientDataJSON field. + LPCWSTR pwszHashAlgId; +} WEBAUTHN_CLIENT_DATA, *PWEBAUTHN_CLIENT_DATA; +typedef const WEBAUTHN_CLIENT_DATA *PCWEBAUTHN_CLIENT_DATA; + +//+------------------------------------------------------------------------------------------ +// Information about credential parameters. +//------------------------------------------------------------------------------------------- + +#define WEBAUTHN_CREDENTIAL_TYPE_PUBLIC_KEY L"public-key" + +#define WEBAUTHN_COSE_ALGORITHM_ECDSA_P256_WITH_SHA256 -7 +#define WEBAUTHN_COSE_ALGORITHM_ECDSA_P384_WITH_SHA384 -35 +#define WEBAUTHN_COSE_ALGORITHM_ECDSA_P521_WITH_SHA512 -36 + +#define WEBAUTHN_COSE_ALGORITHM_RSASSA_PKCS1_V1_5_WITH_SHA256 -257 +#define WEBAUTHN_COSE_ALGORITHM_RSASSA_PKCS1_V1_5_WITH_SHA384 -258 +#define WEBAUTHN_COSE_ALGORITHM_RSASSA_PKCS1_V1_5_WITH_SHA512 -259 + +#define WEBAUTHN_COSE_ALGORITHM_RSA_PSS_WITH_SHA256 -37 +#define WEBAUTHN_COSE_ALGORITHM_RSA_PSS_WITH_SHA384 -38 +#define WEBAUTHN_COSE_ALGORITHM_RSA_PSS_WITH_SHA512 -39 + +#define WEBAUTHN_COSE_CREDENTIAL_PARAMETER_CURRENT_VERSION 1 + +typedef struct _WEBAUTHN_COSE_CREDENTIAL_PARAMETER { + // Version of this structure, to allow for modifications in the future. + DWORD dwVersion; + + // Well-known credential type specifying a credential to create. + LPCWSTR pwszCredentialType; + + // Well-known COSE algorithm specifying the algorithm to use for the credential. + LONG lAlg; +} WEBAUTHN_COSE_CREDENTIAL_PARAMETER, *PWEBAUTHN_COSE_CREDENTIAL_PARAMETER; +typedef const WEBAUTHN_COSE_CREDENTIAL_PARAMETER *PCWEBAUTHN_COSE_CREDENTIAL_PARAMETER; + +typedef struct _WEBAUTHN_COSE_CREDENTIAL_PARAMETERS { + DWORD cCredentialParameters; + _Field_size_(cCredentialParameters) + PWEBAUTHN_COSE_CREDENTIAL_PARAMETER pCredentialParameters; +} WEBAUTHN_COSE_CREDENTIAL_PARAMETERS, *PWEBAUTHN_COSE_CREDENTIAL_PARAMETERS; +typedef const WEBAUTHN_COSE_CREDENTIAL_PARAMETERS *PCWEBAUTHN_COSE_CREDENTIAL_PARAMETERS; + +//+------------------------------------------------------------------------------------------ +// Information about credential. +//------------------------------------------------------------------------------------------- +#define WEBAUTHN_CREDENTIAL_CURRENT_VERSION 1 + +typedef struct _WEBAUTHN_CREDENTIAL { + // Version of this structure, to allow for modifications in the future. + DWORD dwVersion; + + // Size of pbID. + DWORD cbId; + // Unique ID for this particular credential. + _Field_size_bytes_(cbId) + PBYTE pbId; + + // Well-known credential type specifying what this particular credential is. + LPCWSTR pwszCredentialType; +} WEBAUTHN_CREDENTIAL, *PWEBAUTHN_CREDENTIAL; +typedef const WEBAUTHN_CREDENTIAL *PCWEBAUTHN_CREDENTIAL; + +typedef struct _WEBAUTHN_CREDENTIALS { + DWORD cCredentials; + _Field_size_(cCredentials) + PWEBAUTHN_CREDENTIAL pCredentials; +} WEBAUTHN_CREDENTIALS, *PWEBAUTHN_CREDENTIALS; +typedef const WEBAUTHN_CREDENTIALS *PCWEBAUTHN_CREDENTIALS; + +//+------------------------------------------------------------------------------------------ +// Information about credential with extra information, such as, dwTransports +//------------------------------------------------------------------------------------------- + +#define WEBAUTHN_CTAP_TRANSPORT_USB 0x00000001 +#define WEBAUTHN_CTAP_TRANSPORT_NFC 0x00000002 +#define WEBAUTHN_CTAP_TRANSPORT_BLE 0x00000004 +#define WEBAUTHN_CTAP_TRANSPORT_TEST 0x00000008 +#define WEBAUTHN_CTAP_TRANSPORT_INTERNAL 0x00000010 +#define WEBAUTHN_CTAP_TRANSPORT_HYBRID 0x00000020 +#define WEBAUTHN_CTAP_TRANSPORT_SMART_CARD 0x00000040 +#define WEBAUTHN_CTAP_TRANSPORT_FLAGS_MASK 0x0000007F + +#define WEBAUTHN_CTAP_TRANSPORT_USB_STRING "usb" +#define WEBAUTHN_CTAP_TRANSPORT_NFC_STRING "nfc" +#define WEBAUTHN_CTAP_TRANSPORT_BLE_STRING "ble" +#define WEBAUTHN_CTAP_TRANSPORT_SMART_CARD_STRING "smart-card" +#define WEBAUTHN_CTAP_TRANSPORT_HYBRID_STRING "hybrid" +#define WEBAUTHN_CTAP_TRANSPORT_INTERNAL_STRING "internal" + +#define WEBAUTHN_CREDENTIAL_EX_CURRENT_VERSION 1 + +typedef struct _WEBAUTHN_CREDENTIAL_EX { + // Version of this structure, to allow for modifications in the future. + DWORD dwVersion; + + // Size of pbID. + DWORD cbId; + // Unique ID for this particular credential. + _Field_size_bytes_(cbId) + PBYTE pbId; + + // Well-known credential type specifying what this particular credential is. + LPCWSTR pwszCredentialType; + + // Transports. 0 implies no transport restrictions. + DWORD dwTransports; +} WEBAUTHN_CREDENTIAL_EX, *PWEBAUTHN_CREDENTIAL_EX; +typedef const WEBAUTHN_CREDENTIAL_EX *PCWEBAUTHN_CREDENTIAL_EX; + +//+------------------------------------------------------------------------------------------ +// Information about credential list with extra information +//------------------------------------------------------------------------------------------- + +typedef struct _WEBAUTHN_CREDENTIAL_LIST { + DWORD cCredentials; + _Field_size_(cCredentials) + PWEBAUTHN_CREDENTIAL_EX *ppCredentials; +} WEBAUTHN_CREDENTIAL_LIST, *PWEBAUTHN_CREDENTIAL_LIST; +typedef const WEBAUTHN_CREDENTIAL_LIST *PCWEBAUTHN_CREDENTIAL_LIST; + +//+------------------------------------------------------------------------------------------ +// Information about linked devices +//------------------------------------------------------------------------------------------- + +#define CTAPCBOR_HYBRID_STORAGE_LINKED_DATA_VERSION_1 1 +#define CTAPCBOR_HYBRID_STORAGE_LINKED_DATA_CURRENT_VERSION CTAPCBOR_HYBRID_STORAGE_LINKED_DATA_VERSION_1 + +// Deprecated +typedef struct _CTAPCBOR_HYBRID_STORAGE_LINKED_DATA +{ + // Version + DWORD dwVersion; + + // Contact Id + DWORD cbContactId; + _Field_size_bytes_(cbContactId) + PBYTE pbContactId; + + // Link Id + DWORD cbLinkId; + _Field_size_bytes_(cbLinkId) + PBYTE pbLinkId; + + // Link secret + DWORD cbLinkSecret; + _Field_size_bytes_(cbLinkSecret) + PBYTE pbLinkSecret; + + // Authenticator Public Key + DWORD cbPublicKey; + _Field_size_bytes_(cbPublicKey) + PBYTE pbPublicKey; + + // Authenticator Name + PCWSTR pwszAuthenticatorName; + + // Tunnel server domain + WORD wEncodedTunnelServerDomain; +} CTAPCBOR_HYBRID_STORAGE_LINKED_DATA, *PCTAPCBOR_HYBRID_STORAGE_LINKED_DATA; +typedef const CTAPCBOR_HYBRID_STORAGE_LINKED_DATA *PCCTAPCBOR_HYBRID_STORAGE_LINKED_DATA; + +//+------------------------------------------------------------------------------------------ +// Authenticator Information for WebAuthNGetAuthenticatorList API +//------------------------------------------------------------------------------------------- + +#define WEBAUTHN_AUTHENTICATOR_DETAILS_OPTIONS_VERSION_1 1 +#define WEBAUTHN_AUTHENTICATOR_DETAILS_OPTIONS_CURRENT_VERSION WEBAUTHN_AUTHENTICATOR_DETAILS_OPTIONS_VERSION_1 + +typedef struct _WEBAUTHN_AUTHENTICATOR_DETAILS_OPTIONS { + // Version of this structure, to allow for modifications in the future. + DWORD dwVersion; + +} WEBAUTHN_AUTHENTICATOR_DETAILS_OPTIONS, *PWEBAUTHN_AUTHENTICATOR_DETAILS_OPTIONS; +typedef const WEBAUTHN_AUTHENTICATOR_DETAILS_OPTIONS *PCWEBAUTHN_AUTHENTICATOR_DETAILS_OPTIONS; + +#define WEBAUTHN_AUTHENTICATOR_DETAILS_VERSION_1 1 +#define WEBAUTHN_AUTHENTICATOR_DETAILS_CURRENT_VERSION WEBAUTHN_AUTHENTICATOR_DETAILS_VERSION_1 + +typedef struct _WEBAUTHN_AUTHENTICATOR_DETAILS { + // Version of this structure, to allow for modifications in the future. + DWORD dwVersion; + + // Authenticator ID + DWORD cbAuthenticatorId; + _Field_size_bytes_(cbAuthenticatorId) + PBYTE pbAuthenticatorId; + + // Authenticator Name + PCWSTR pwszAuthenticatorName; + + // Authenticator logo (expected to be in SVG format) + DWORD cbAuthenticatorLogo; + _Field_size_bytes_(cbAuthenticatorLogo) + PBYTE pbAuthenticatorLogo; + + // Is the authenticator currently locked? When locked, this authenticator's credentials + // might not be present or updated in WebAuthNGetPlatformCredentialList. + BOOL bLocked; + +} WEBAUTHN_AUTHENTICATOR_DETAILS, *PWEBAUTHN_AUTHENTICATOR_DETAILS; +typedef const WEBAUTHN_AUTHENTICATOR_DETAILS *PCWEBAUTHN_AUTHENTICATOR_DETAILS; + +typedef struct _WEBAUTHN_AUTHENTICATOR_DETAILS_LIST { + // Authenticator Details + DWORD cAuthenticatorDetails; + _Field_size_(cAuthenticatorDetails) + PWEBAUTHN_AUTHENTICATOR_DETAILS *ppAuthenticatorDetails; + +} WEBAUTHN_AUTHENTICATOR_DETAILS_LIST, *PWEBAUTHN_AUTHENTICATOR_DETAILS_LIST; +typedef const WEBAUTHN_AUTHENTICATOR_DETAILS_LIST *PCWEBAUTHN_AUTHENTICATOR_DETAILS_LIST; + +//+------------------------------------------------------------------------------------------ +// Credential Information for WebAuthNGetPlatformCredentialList API +//------------------------------------------------------------------------------------------- + +#define WEBAUTHN_CREDENTIAL_DETAILS_VERSION_1 1 +#define WEBAUTHN_CREDENTIAL_DETAILS_VERSION_2 2 +#define WEBAUTHN_CREDENTIAL_DETAILS_VERSION_3 3 +#define WEBAUTHN_CREDENTIAL_DETAILS_VERSION_4 4 +#define WEBAUTHN_CREDENTIAL_DETAILS_CURRENT_VERSION WEBAUTHN_CREDENTIAL_DETAILS_VERSION_4 + +typedef struct _WEBAUTHN_CREDENTIAL_DETAILS { + // Version of this structure, to allow for modifications in the future. + DWORD dwVersion; + + // Size of pbCredentialID. + DWORD cbCredentialID; + _Field_size_bytes_(cbCredentialID) + PBYTE pbCredentialID; + + // RP Info + PWEBAUTHN_RP_ENTITY_INFORMATION pRpInformation; + + // User Info + PWEBAUTHN_USER_ENTITY_INFORMATION pUserInformation; + + // Removable or not. + BOOL bRemovable; + + // + // The following fields have been added in WEBAUTHN_CREDENTIAL_DETAILS_VERSION_2 + // + + // Backed Up or not. + BOOL bBackedUp; + + // + // The following fields have been added in WEBAUTHN_CREDENTIAL_DETAILS_VERSION_3 + // + PCWSTR pwszAuthenticatorName; + + // The logo is expected to be in the svg format + DWORD cbAuthenticatorLogo; + _Field_size_bytes_(cbAuthenticatorLogo) + PBYTE pbAuthenticatorLogo; + + // ThirdPartyPayment Credential or not. + BOOL bThirdPartyPayment; + + // + // The following fields have been added in WEBAUTHN_CREDENTIAL_DETAILS_VERSION_4 + // + + // Applicable Transports + DWORD dwTransports; + +} WEBAUTHN_CREDENTIAL_DETAILS, *PWEBAUTHN_CREDENTIAL_DETAILS; +typedef const WEBAUTHN_CREDENTIAL_DETAILS *PCWEBAUTHN_CREDENTIAL_DETAILS; + +typedef struct _WEBAUTHN_CREDENTIAL_DETAILS_LIST { + DWORD cCredentialDetails; + _Field_size_(cCredentialDetails) + PWEBAUTHN_CREDENTIAL_DETAILS *ppCredentialDetails; +} WEBAUTHN_CREDENTIAL_DETAILS_LIST, *PWEBAUTHN_CREDENTIAL_DETAILS_LIST; +typedef const WEBAUTHN_CREDENTIAL_DETAILS_LIST *PCWEBAUTHN_CREDENTIAL_DETAILS_LIST; + +#define WEBAUTHN_GET_CREDENTIALS_OPTIONS_VERSION_1 1 +#define WEBAUTHN_GET_CREDENTIALS_OPTIONS_CURRENT_VERSION WEBAUTHN_GET_CREDENTIALS_OPTIONS_VERSION_1 + +typedef struct _WEBAUTHN_GET_CREDENTIALS_OPTIONS { + // Version of this structure, to allow for modifications in the future. + DWORD dwVersion; + + // Optional. + LPCWSTR pwszRpId; + + // Optional. BrowserInPrivate Mode. Defaulting to FALSE. + BOOL bBrowserInPrivateMode; +} WEBAUTHN_GET_CREDENTIALS_OPTIONS, *PWEBAUTHN_GET_CREDENTIALS_OPTIONS; +typedef const WEBAUTHN_GET_CREDENTIALS_OPTIONS *PCWEBAUTHN_GET_CREDENTIALS_OPTIONS; + +//+------------------------------------------------------------------------------------------ +// PRF values. +//------------------------------------------------------------------------------------------- + +#define WEBAUTHN_CTAP_ONE_HMAC_SECRET_LENGTH 32 + +// SALT values below by default are converted into RAW Hmac-Secret values as per PRF extension. +// - SHA-256(UTF8Encode("WebAuthn PRF") || 0x00 || Value) +// +// Set WEBAUTHN_AUTHENTICATOR_HMAC_SECRET_VALUES_FLAG in dwFlags in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS, +// if caller wants to provide RAW Hmac-Secret SALT values directly. In that case, +// values if provided MUST be of WEBAUTHN_CTAP_ONE_HMAC_SECRET_LENGTH size. + +typedef struct _WEBAUTHN_HMAC_SECRET_SALT { + // Size of pbFirst. + DWORD cbFirst; + _Field_size_bytes_(cbFirst) + PBYTE pbFirst; // Required + + // Size of pbSecond. + DWORD cbSecond; + _Field_size_bytes_(cbSecond) + PBYTE pbSecond; +} WEBAUTHN_HMAC_SECRET_SALT, *PWEBAUTHN_HMAC_SECRET_SALT; +typedef const WEBAUTHN_HMAC_SECRET_SALT *PCWEBAUTHN_HMAC_SECRET_SALT; + +typedef struct _WEBAUTHN_CRED_WITH_HMAC_SECRET_SALT { + // Size of pbCredID. + DWORD cbCredID; + _Field_size_bytes_(cbCredID) + PBYTE pbCredID; // Required + + // PRF Values for above credential + PWEBAUTHN_HMAC_SECRET_SALT pHmacSecretSalt; // Required +} WEBAUTHN_CRED_WITH_HMAC_SECRET_SALT, *PWEBAUTHN_CRED_WITH_HMAC_SECRET_SALT; +typedef const WEBAUTHN_CRED_WITH_HMAC_SECRET_SALT *PCWEBAUTHN_CRED_WITH_HMAC_SECRET_SALT; + +typedef struct _WEBAUTHN_HMAC_SECRET_SALT_VALUES { + PWEBAUTHN_HMAC_SECRET_SALT pGlobalHmacSalt; + + DWORD cCredWithHmacSecretSaltList; + _Field_size_(cCredWithHmacSecretSaltList) + PWEBAUTHN_CRED_WITH_HMAC_SECRET_SALT pCredWithHmacSecretSaltList; +} WEBAUTHN_HMAC_SECRET_SALT_VALUES, *PWEBAUTHN_HMAC_SECRET_SALT_VALUES; +typedef const WEBAUTHN_HMAC_SECRET_SALT_VALUES *PCWEBAUTHN_HMAC_SECRET_SALT_VALUES; + +//+------------------------------------------------------------------------------------------ +// Hmac-Secret extension +//------------------------------------------------------------------------------------------- + +#define WEBAUTHN_EXTENSIONS_IDENTIFIER_HMAC_SECRET L"hmac-secret" +// Below type definitions is for WEBAUTHN_EXTENSIONS_IDENTIFIER_HMAC_SECRET +// MakeCredential Input Type: BOOL. +// - pvExtension must point to a BOOL with the value TRUE. +// - cbExtension must contain the sizeof(BOOL). +// MakeCredential Output Type: BOOL. +// - pvExtension will point to a BOOL with the value TRUE if credential +// was successfully created with HMAC_SECRET. +// - cbExtension will contain the sizeof(BOOL). +// GetAssertion Input Type: Not Supported +// GetAssertion Output Type: Not Supported + +//+------------------------------------------------------------------------------------------ +// credProtect extension +//------------------------------------------------------------------------------------------- + +#define WEBAUTHN_USER_VERIFICATION_ANY 0 +#define WEBAUTHN_USER_VERIFICATION_OPTIONAL 1 +#define WEBAUTHN_USER_VERIFICATION_OPTIONAL_WITH_CREDENTIAL_ID_LIST 2 +#define WEBAUTHN_USER_VERIFICATION_REQUIRED 3 + +typedef struct _WEBAUTHN_CRED_PROTECT_EXTENSION_IN { + // One of the above WEBAUTHN_USER_VERIFICATION_* values + DWORD dwCredProtect; + // Set the following to TRUE to require authenticator support for the credProtect extension + BOOL bRequireCredProtect; +} WEBAUTHN_CRED_PROTECT_EXTENSION_IN, *PWEBAUTHN_CRED_PROTECT_EXTENSION_IN; +typedef const WEBAUTHN_CRED_PROTECT_EXTENSION_IN *PCWEBAUTHN_CRED_PROTECT_EXTENSION_IN; + + +#define WEBAUTHN_EXTENSIONS_IDENTIFIER_CRED_PROTECT L"credProtect" +// Below type definitions is for WEBAUTHN_EXTENSIONS_IDENTIFIER_CRED_PROTECT +// MakeCredential Input Type: WEBAUTHN_CRED_PROTECT_EXTENSION_IN. +// - pvExtension must point to a WEBAUTHN_CRED_PROTECT_EXTENSION_IN struct +// - cbExtension will contain the sizeof(WEBAUTHN_CRED_PROTECT_EXTENSION_IN). +// MakeCredential Output Type: DWORD. +// - pvExtension will point to a DWORD with one of the above WEBAUTHN_USER_VERIFICATION_* values +// if credential was successfully created with CRED_PROTECT. +// - cbExtension will contain the sizeof(DWORD). +// GetAssertion Input Type: Not Supported +// GetAssertion Output Type: Not Supported + +//+------------------------------------------------------------------------------------------ +// credBlob extension +//------------------------------------------------------------------------------------------- + +typedef struct _WEBAUTHN_CRED_BLOB_EXTENSION { + // Size of pbCredBlob. + DWORD cbCredBlob; + _Field_size_bytes_(cbCredBlob) + PBYTE pbCredBlob; +} WEBAUTHN_CRED_BLOB_EXTENSION, *PWEBAUTHN_CRED_BLOB_EXTENSION; +typedef const WEBAUTHN_CRED_BLOB_EXTENSION *PCWEBAUTHN_CRED_BLOB_EXTENSION; + + +#define WEBAUTHN_EXTENSIONS_IDENTIFIER_CRED_BLOB L"credBlob" +// Below type definitions is for WEBAUTHN_EXTENSIONS_IDENTIFIER_CRED_BLOB +// MakeCredential Input Type: WEBAUTHN_CRED_BLOB_EXTENSION. +// - pvExtension must point to a WEBAUTHN_CRED_BLOB_EXTENSION struct +// - cbExtension must contain the sizeof(WEBAUTHN_CRED_BLOB_EXTENSION). +// MakeCredential Output Type: BOOL. +// - pvExtension will point to a BOOL with the value TRUE if credBlob was successfully created +// - cbExtension will contain the sizeof(BOOL). +// GetAssertion Input Type: BOOL. +// - pvExtension must point to a BOOL with the value TRUE to request the credBlob. +// - cbExtension must contain the sizeof(BOOL). +// GetAssertion Output Type: WEBAUTHN_CRED_BLOB_EXTENSION. +// - pvExtension will point to a WEBAUTHN_CRED_BLOB_EXTENSION struct if the authenticator +// returns the credBlob in the signed extensions +// - cbExtension will contain the sizeof(WEBAUTHN_CRED_BLOB_EXTENSION). + +//+------------------------------------------------------------------------------------------ +// minPinLength extension +//------------------------------------------------------------------------------------------- + +#define WEBAUTHN_EXTENSIONS_IDENTIFIER_MIN_PIN_LENGTH L"minPinLength" +// Below type definitions is for WEBAUTHN_EXTENSIONS_IDENTIFIER_MIN_PIN_LENGTH +// MakeCredential Input Type: BOOL. +// - pvExtension must point to a BOOL with the value TRUE to request the minPinLength. +// - cbExtension must contain the sizeof(BOOL). +// MakeCredential Output Type: DWORD. +// - pvExtension will point to a DWORD with the minimum pin length if returned by the authenticator +// - cbExtension will contain the sizeof(DWORD). +// GetAssertion Input Type: Not Supported +// GetAssertion Output Type: Not Supported + +//+------------------------------------------------------------------------------------------ +// Information about Extensions. +//------------------------------------------------------------------------------------------- +typedef struct _WEBAUTHN_EXTENSION { + LPCWSTR pwszExtensionIdentifier; + DWORD cbExtension; + PVOID pvExtension; +} WEBAUTHN_EXTENSION, *PWEBAUTHN_EXTENSION; +typedef const WEBAUTHN_EXTENSION *PCWEBAUTHN_EXTENSION; + +typedef struct _WEBAUTHN_EXTENSIONS { + DWORD cExtensions; + _Field_size_(cExtensions) + PWEBAUTHN_EXTENSION pExtensions; +} WEBAUTHN_EXTENSIONS, *PWEBAUTHN_EXTENSIONS; +typedef const WEBAUTHN_EXTENSIONS *PCWEBAUTHN_EXTENSIONS; + +//+------------------------------------------------------------------------------------------ +// Options. +//------------------------------------------------------------------------------------------- + +#define WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY 0 +#define WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM 1 +#define WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM 2 +#define WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM_U2F_V2 3 + +#define WEBAUTHN_USER_VERIFICATION_REQUIREMENT_ANY 0 +#define WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED 1 +#define WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED 2 +#define WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED 3 + +#define WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_ANY 0 +#define WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE 1 +#define WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT 2 +#define WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT 3 + +#define WEBAUTHN_ENTERPRISE_ATTESTATION_NONE 0 +#define WEBAUTHN_ENTERPRISE_ATTESTATION_VENDOR_FACILITATED 1 +#define WEBAUTHN_ENTERPRISE_ATTESTATION_PLATFORM_MANAGED 2 + +#define WEBAUTHN_LARGE_BLOB_SUPPORT_NONE 0 +#define WEBAUTHN_LARGE_BLOB_SUPPORT_REQUIRED 1 +#define WEBAUTHN_LARGE_BLOB_SUPPORT_PREFERRED 2 + +#define WEBAUTHN_CREDENTIAL_HINT_SECURITY_KEY L"security-key" +#define WEBAUTHN_CREDENTIAL_HINT_CLIENT_DEVICE L"client-device" +#define WEBAUTHN_CREDENTIAL_HINT_HYBRID L"hybrid" + +#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_1 1 +#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_2 2 +#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_3 3 +#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_4 4 +#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_5 5 +#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_6 6 +#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_7 7 +#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_8 8 +#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_9 9 +#define WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_CURRENT_VERSION WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_9 + +typedef struct _WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS { + // Version of this structure, to allow for modifications in the future. + DWORD dwVersion; + + // Time that the operation is expected to complete within. + // This is used as guidance, and can be overridden by the platform. + DWORD dwTimeoutMilliseconds; + + // Credentials used for exclusion. + WEBAUTHN_CREDENTIALS CredentialList; + + // Optional extensions to parse when performing the operation. + WEBAUTHN_EXTENSIONS Extensions; + + // Optional. Platform vs Cross-Platform Authenticators. + DWORD dwAuthenticatorAttachment; + + // Optional. Require key to be resident or not. Defaulting to FALSE. + BOOL bRequireResidentKey; + + // User Verification Requirement. + DWORD dwUserVerificationRequirement; + + // Attestation Conveyance Preference. + DWORD dwAttestationConveyancePreference; + + // Reserved for future Use + DWORD dwFlags; + + // + // The following fields have been added in WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_2 + // + + // Cancellation Id - Optional - See WebAuthNGetCancellationId + GUID *pCancellationId; + + // + // The following fields have been added in WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_3 + // + + // Exclude Credential List. If present, "CredentialList" will be ignored. + PWEBAUTHN_CREDENTIAL_LIST pExcludeCredentialList; + + // + // The following fields have been added in WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_4 + // + + // Enterprise Attestation + DWORD dwEnterpriseAttestation; + + // Large Blob Support: none, required or preferred + // + // NTE_INVALID_PARAMETER when large blob required or preferred and + // bRequireResidentKey isn't set to TRUE + DWORD dwLargeBlobSupport; + + // Optional. Prefer key to be resident. Defaulting to FALSE. When TRUE, + // overrides the above bRequireResidentKey. + BOOL bPreferResidentKey; + + // + // The following fields have been added in WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_5 + // + + // Optional. BrowserInPrivate Mode. Defaulting to FALSE. + BOOL bBrowserInPrivateMode; + + // + // The following fields have been added in WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_6 + // + + // Enable PRF + BOOL bEnablePrf; + + // + // The following fields have been added in WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_7 + // + + // Deprecated + // Optional. Linked Device Connection Info. + PCTAPCBOR_HYBRID_STORAGE_LINKED_DATA pLinkedDevice; + + // Size of pbJsonExt + DWORD cbJsonExt; + _Field_size_bytes_(cbJsonExt) + PBYTE pbJsonExt; + + // + // The following fields have been added in WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_8 + // + + // PRF extension "eval" values which will be converted into HMAC-SECRET values according to WebAuthn Spec. + // Set WEBAUTHN_AUTHENTICATOR_HMAC_SECRET_VALUES_FLAG in dwFlags above, if caller wants to provide RAW Hmac-Secret SALT values directly. + // In that case, values provided MUST be of WEBAUTHN_CTAP_ONE_HMAC_SECRET_LENGTH size. + PWEBAUTHN_HMAC_SECRET_SALT pPRFGlobalEval; + + // PublicKeyCredentialHints (https://w3c.github.io/webauthn/#enum-hints) + DWORD cCredentialHints; + _Field_size_(cCredentialHints) + LPCWSTR *ppwszCredentialHints; + + // Enable ThirdPartyPayment + BOOL bThirdPartyPayment; + + // + // The following fields have been added in WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_9 + // + + // Web Origin. For Remote Web App scenario. + PCWSTR pwszRemoteWebOrigin; + + // UTF-8 encoded JSON serialization of the PublicKeyCredentialCreationOptions. + DWORD cbPublicKeyCredentialCreationOptionsJSON; + _Field_size_bytes_(cbPublicKeyCredentialCreationOptionsJSON) + PBYTE pbPublicKeyCredentialCreationOptionsJSON; + + // Authenticator ID got from WebAuthNGetAuthenticatorList API. + DWORD cbAuthenticatorId; + _Field_size_bytes_(cbAuthenticatorId) + PBYTE pbAuthenticatorId; + +} WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS, *PWEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS; +typedef const WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS *PCWEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS; + +#define WEBAUTHN_CRED_LARGE_BLOB_OPERATION_NONE 0 +#define WEBAUTHN_CRED_LARGE_BLOB_OPERATION_GET 1 +#define WEBAUTHN_CRED_LARGE_BLOB_OPERATION_SET 2 +#define WEBAUTHN_CRED_LARGE_BLOB_OPERATION_DELETE 3 + +#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_1 1 +#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_2 2 +#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_3 3 +#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_4 4 +#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_5 5 +#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_6 6 +#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_7 7 +#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_8 8 +#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_9 9 +#define WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_CURRENT_VERSION WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_9 + +/* + Information about flags. +*/ + +#define WEBAUTHN_AUTHENTICATOR_HMAC_SECRET_VALUES_FLAG 0x00100000 + +typedef struct _WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS { + // Version of this structure, to allow for modifications in the future. + DWORD dwVersion; + + // Time that the operation is expected to complete within. + // This is used as guidance, and can be overridden by the platform. + DWORD dwTimeoutMilliseconds; + + // Allowed Credentials List. + WEBAUTHN_CREDENTIALS CredentialList; + + // Optional extensions to parse when performing the operation. + WEBAUTHN_EXTENSIONS Extensions; + + // Optional. Platform vs Cross-Platform Authenticators. + DWORD dwAuthenticatorAttachment; + + // User Verification Requirement. + DWORD dwUserVerificationRequirement; + + // Flags + DWORD dwFlags; + + // + // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_2 + // + + // Optional identifier for the U2F AppId. Converted to UTF8 before being hashed. Not lower cased. + PCWSTR pwszU2fAppId; + + // If the following is non-NULL, then, set to TRUE if the above pwszU2fAppid was used instead of + // PCWSTR pwszRpId; + BOOL *pbU2fAppId; + + // + // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_3 + // + + // Cancellation Id - Optional - See WebAuthNGetCancellationId + GUID *pCancellationId; + + // + // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_4 + // + + // Allow Credential List. If present, "CredentialList" will be ignored. + PWEBAUTHN_CREDENTIAL_LIST pAllowCredentialList; + + // + // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_5 + // + + DWORD dwCredLargeBlobOperation; + + // Size of pbCredLargeBlob + DWORD cbCredLargeBlob; + _Field_size_bytes_(cbCredLargeBlob) + PBYTE pbCredLargeBlob; + + // + // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_6 + // + + // PRF values which will be converted into HMAC-SECRET values according to WebAuthn Spec. + PWEBAUTHN_HMAC_SECRET_SALT_VALUES pHmacSecretSaltValues; + + // Optional. BrowserInPrivate Mode. Defaulting to FALSE. + BOOL bBrowserInPrivateMode; + + // + // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_7 + // + + // Deprecated + // Optional. Linked Device Connection Info. + PCTAPCBOR_HYBRID_STORAGE_LINKED_DATA pLinkedDevice; + + // Optional. Allowlist MUST contain 1 credential applicable for Hybrid transport. + BOOL bAutoFill; + + // Size of pbJsonExt + DWORD cbJsonExt; + _Field_size_bytes_(cbJsonExt) + PBYTE pbJsonExt; + + // + // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_8 + // + + // PublicKeyCredentialHints (https://w3c.github.io/webauthn/#enum-hints) + DWORD cCredentialHints; + _Field_size_(cCredentialHints) + LPCWSTR *ppwszCredentialHints; + + // + // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_9 + // + + // Web Origin. For Remote Web App scenario. + PCWSTR pwszRemoteWebOrigin; + + // UTF-8 encoded JSON serialization of the PublicKeyCredentialRequestOptions. + DWORD cbPublicKeyCredentialRequestOptionsJSON; + _Field_size_bytes_(cbPublicKeyCredentialRequestOptionsJSON) + PBYTE pbPublicKeyCredentialRequestOptionsJSON; + + // Authenticator ID got from WebAuthNGetAuthenticatorList API. + DWORD cbAuthenticatorId; + _Field_size_bytes_(cbAuthenticatorId) + PBYTE pbAuthenticatorId; + +} WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS, *PWEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS; +typedef const WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS *PCWEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS; + + +//+------------------------------------------------------------------------------------------ +// Attestation Info. +// +//------------------------------------------------------------------------------------------- +#define WEBAUTHN_ATTESTATION_DECODE_NONE 0 +#define WEBAUTHN_ATTESTATION_DECODE_COMMON 1 +// WEBAUTHN_ATTESTATION_DECODE_COMMON supports format types +// L"packed" +// L"fido-u2f" + +#define WEBAUTHN_ATTESTATION_VER_TPM_2_0 L"2.0" + +typedef struct _WEBAUTHN_X5C { + // Length of X.509 encoded certificate + DWORD cbData; + // X.509 encoded certificate bytes + _Field_size_bytes_(cbData) + PBYTE pbData; +} WEBAUTHN_X5C, *PWEBAUTHN_X5C; + +// Supports either Self or Full Basic Attestation + +// Note, new fields will be added to the following data structure to +// support additional attestation format types, such as, TPM. +// When fields are added, the dwVersion will be incremented. +// +// Therefore, your code must make the following check: +// "if (dwVersion >= WEBAUTHN_COMMON_ATTESTATION_CURRENT_VERSION)" + +#define WEBAUTHN_COMMON_ATTESTATION_CURRENT_VERSION 1 + +typedef struct _WEBAUTHN_COMMON_ATTESTATION { + // Version of this structure, to allow for modifications in the future. + DWORD dwVersion; + + // Hash and Padding Algorithm + // + // The following won't be set for "fido-u2f" which assumes "ES256". + PCWSTR pwszAlg; + LONG lAlg; // COSE algorithm + + // Signature that was generated for this attestation. + DWORD cbSignature; + _Field_size_bytes_(cbSignature) + PBYTE pbSignature; + + // Following is set for Full Basic Attestation. If not, set then, this is Self Attestation. + // Array of X.509 DER encoded certificates. The first certificate is the signer, leaf certificate. + DWORD cX5c; + _Field_size_(cX5c) + PWEBAUTHN_X5C pX5c; + + // Following are also set for tpm + PCWSTR pwszVer; // L"2.0" + DWORD cbCertInfo; + _Field_size_bytes_(cbCertInfo) + PBYTE pbCertInfo; + DWORD cbPubArea; + _Field_size_bytes_(cbPubArea) + PBYTE pbPubArea; +} WEBAUTHN_COMMON_ATTESTATION, *PWEBAUTHN_COMMON_ATTESTATION; +typedef const WEBAUTHN_COMMON_ATTESTATION *PCWEBAUTHN_COMMON_ATTESTATION; + +#define WEBAUTHN_ATTESTATION_TYPE_PACKED L"packed" +#define WEBAUTHN_ATTESTATION_TYPE_U2F L"fido-u2f" +#define WEBAUTHN_ATTESTATION_TYPE_TPM L"tpm" +#define WEBAUTHN_ATTESTATION_TYPE_NONE L"none" + +#define WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_1 1 +#define WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_2 2 +#define WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_3 3 +#define WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_4 4 +#define WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_5 5 +#define WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_6 6 +#define WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_7 7 +#define WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_8 8 +#define WEBAUTHN_CREDENTIAL_ATTESTATION_CURRENT_VERSION WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_8 + +typedef struct _WEBAUTHN_CREDENTIAL_ATTESTATION { + // Version of this structure, to allow for modifications in the future. + DWORD dwVersion; + + // Attestation format type + PCWSTR pwszFormatType; + + // Size of cbAuthenticatorData. + DWORD cbAuthenticatorData; + // Authenticator data that was created for this credential. + _Field_size_bytes_(cbAuthenticatorData) + PBYTE pbAuthenticatorData; + + // Size of CBOR encoded attestation information + //0 => encoded as CBOR null value. + DWORD cbAttestation; + //Encoded CBOR attestation information + _Field_size_bytes_(cbAttestation) + PBYTE pbAttestation; + + DWORD dwAttestationDecodeType; + // Following depends on the dwAttestationDecodeType + // WEBAUTHN_ATTESTATION_DECODE_NONE + // NULL - not able to decode the CBOR attestation information + // WEBAUTHN_ATTESTATION_DECODE_COMMON + // PWEBAUTHN_COMMON_ATTESTATION; + PVOID pvAttestationDecode; + + // The CBOR encoded Attestation Object to be returned to the RP. + DWORD cbAttestationObject; + _Field_size_bytes_(cbAttestationObject) + PBYTE pbAttestationObject; + + // The CredentialId bytes extracted from the Authenticator Data. + // Used by Edge to return to the RP. + DWORD cbCredentialId; + _Field_size_bytes_(cbCredentialId) + PBYTE pbCredentialId; + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_2 + // + + WEBAUTHN_EXTENSIONS Extensions; + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_3 + // + + // One of the WEBAUTHN_CTAP_TRANSPORT_* bits will be set corresponding to + // the transport that was used. + DWORD dwUsedTransport; + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_4 + // + + BOOL bEpAtt; + BOOL bLargeBlobSupported; + BOOL bResidentKey; + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_5 + // + + BOOL bPrfEnabled; + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_6 + // + + DWORD cbUnsignedExtensionOutputs; + _Field_size_bytes_(cbUnsignedExtensionOutputs) + PBYTE pbUnsignedExtensionOutputs; + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_7 + // + + PWEBAUTHN_HMAC_SECRET_SALT pHmacSecret; + + // ThirdPartyPayment Credential or not. + BOOL bThirdPartyPayment; + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_8 + // + + // Multiple WEBAUTHN_CTAP_TRANSPORT_* bits will be set corresponding to + // the transports that are supported. + DWORD dwTransports; + + // UTF-8 encoded JSON serialization of the client data. + DWORD cbClientDataJSON; + _Field_size_bytes_(cbClientDataJSON) + PBYTE pbClientDataJSON; + + // UTF-8 encoded JSON serialization of the RegistrationResponse. + DWORD cbRegistrationResponseJSON; + _Field_size_bytes_(cbRegistrationResponseJSON) + PBYTE pbRegistrationResponseJSON; + +} WEBAUTHN_CREDENTIAL_ATTESTATION, *PWEBAUTHN_CREDENTIAL_ATTESTATION; +typedef const WEBAUTHN_CREDENTIAL_ATTESTATION *PCWEBAUTHN_CREDENTIAL_ATTESTATION; + + +//+------------------------------------------------------------------------------------------ +// authenticatorGetAssertion output. +//------------------------------------------------------------------------------------------- + +#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_NONE 0 +#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_SUCCESS 1 +#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_NOT_SUPPORTED 2 +#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_INVALID_DATA 3 +#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_INVALID_PARAMETER 4 +#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_NOT_FOUND 5 +#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_MULTIPLE_CREDENTIALS 6 +#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_LACK_OF_SPACE 7 +#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_PLATFORM_ERROR 8 +#define WEBAUTHN_CRED_LARGE_BLOB_STATUS_AUTHENTICATOR_ERROR 9 + +#define WEBAUTHN_ASSERTION_VERSION_1 1 +#define WEBAUTHN_ASSERTION_VERSION_2 2 +#define WEBAUTHN_ASSERTION_VERSION_3 3 +#define WEBAUTHN_ASSERTION_VERSION_4 4 +#define WEBAUTHN_ASSERTION_VERSION_5 5 +#define WEBAUTHN_ASSERTION_VERSION_6 6 +#define WEBAUTHN_ASSERTION_CURRENT_VERSION WEBAUTHN_ASSERTION_VERSION_6 + +typedef struct _WEBAUTHN_ASSERTION { + // Version of this structure, to allow for modifications in the future. + DWORD dwVersion; + + // Size of cbAuthenticatorData. + DWORD cbAuthenticatorData; + // Authenticator data that was created for this assertion. + _Field_size_bytes_(cbAuthenticatorData) + PBYTE pbAuthenticatorData; + + // Size of pbSignature. + DWORD cbSignature; + // Signature that was generated for this assertion. + _Field_size_bytes_(cbSignature) + PBYTE pbSignature; + + // Credential that was used for this assertion. + WEBAUTHN_CREDENTIAL Credential; + + // Size of User Id + DWORD cbUserId; + // UserId + _Field_size_bytes_(cbUserId) + PBYTE pbUserId; + + // + // Following fields have been added in WEBAUTHN_ASSERTION_VERSION_2 + // + + WEBAUTHN_EXTENSIONS Extensions; + + // Size of pbCredLargeBlob + DWORD cbCredLargeBlob; + _Field_size_bytes_(cbCredLargeBlob) + PBYTE pbCredLargeBlob; + + DWORD dwCredLargeBlobStatus; + + // + // Following fields have been added in WEBAUTHN_ASSERTION_VERSION_3 + // + + PWEBAUTHN_HMAC_SECRET_SALT pHmacSecret; + + // + // Following fields have been added in WEBAUTHN_ASSERTION_VERSION_4 + // + + // One of the WEBAUTHN_CTAP_TRANSPORT_* bits will be set corresponding to + // the transport that was used. + DWORD dwUsedTransport; + + // + // Following fields have been added in WEBAUTHN_ASSERTION_VERSION_5 + // + + DWORD cbUnsignedExtensionOutputs; + _Field_size_bytes_(cbUnsignedExtensionOutputs) + PBYTE pbUnsignedExtensionOutputs; + + // + // Following fields have been added in WEBAUTHN_ASSERTION_VERSION_6 + // + + // UTF-8 encoded JSON serialization of the client data. + DWORD cbClientDataJSON; + _Field_size_bytes_(cbClientDataJSON) + PBYTE pbClientDataJSON; + + // UTF-8 encoded JSON serialization of the AuthenticationResponse. + DWORD cbAuthenticationResponseJSON; + _Field_size_bytes_(cbAuthenticationResponseJSON) + PBYTE pbAuthenticationResponseJSON; + +} WEBAUTHN_ASSERTION, *PWEBAUTHN_ASSERTION; +typedef const WEBAUTHN_ASSERTION *PCWEBAUTHN_ASSERTION; + +//+------------------------------------------------------------------------------------------ +// APIs. +//------------------------------------------------------------------------------------------- + +DWORD +WINAPI +WebAuthNGetApiVersionNumber(); + +HRESULT +WINAPI +WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable( + _Out_ BOOL *pbIsUserVerifyingPlatformAuthenticatorAvailable); + + +HRESULT +WINAPI +WebAuthNAuthenticatorMakeCredential( + _In_ HWND hWnd, + _In_ PCWEBAUTHN_RP_ENTITY_INFORMATION pRpInformation, + _In_ PCWEBAUTHN_USER_ENTITY_INFORMATION pUserInformation, + _In_ PCWEBAUTHN_COSE_CREDENTIAL_PARAMETERS pPubKeyCredParams, + _In_ PCWEBAUTHN_CLIENT_DATA pWebAuthNClientData, + _In_opt_ PCWEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS pWebAuthNMakeCredentialOptions, + _Outptr_result_maybenull_ PWEBAUTHN_CREDENTIAL_ATTESTATION *ppWebAuthNCredentialAttestation); + + +HRESULT +WINAPI +WebAuthNAuthenticatorGetAssertion( + _In_ HWND hWnd, + _In_ LPCWSTR pwszRpId, + _In_ PCWEBAUTHN_CLIENT_DATA pWebAuthNClientData, + _In_opt_ PCWEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS pWebAuthNGetAssertionOptions, + _Outptr_result_maybenull_ PWEBAUTHN_ASSERTION *ppWebAuthNAssertion); + +void +WINAPI +WebAuthNFreeCredentialAttestation( + _In_opt_ PWEBAUTHN_CREDENTIAL_ATTESTATION pWebAuthNCredentialAttestation); + +void +WINAPI +WebAuthNFreeAssertion( + _In_ PWEBAUTHN_ASSERTION pWebAuthNAssertion); + +HRESULT +WINAPI +WebAuthNGetCancellationId( + _Out_ GUID* pCancellationId); + +HRESULT +WINAPI +WebAuthNCancelCurrentOperation( + _In_ const GUID* pCancellationId); + +// Returns NTE_NOT_FOUND when credentials are not found. +HRESULT +WINAPI +WebAuthNGetPlatformCredentialList( + _In_ PCWEBAUTHN_GET_CREDENTIALS_OPTIONS pGetCredentialsOptions, + _Outptr_result_maybenull_ PWEBAUTHN_CREDENTIAL_DETAILS_LIST *ppCredentialDetailsList); + +void +WINAPI +WebAuthNFreePlatformCredentialList( + _In_ PWEBAUTHN_CREDENTIAL_DETAILS_LIST pCredentialDetailsList); + +HRESULT +WINAPI +WebAuthNDeletePlatformCredential( + _In_ DWORD cbCredentialId, + _In_reads_bytes_(cbCredentialId) const BYTE *pbCredentialId + ); + +// Returns NTE_NOT_FOUND when authenticator details are not found. +HRESULT +WINAPI +WebAuthNGetAuthenticatorList( + _In_opt_ PCWEBAUTHN_AUTHENTICATOR_DETAILS_OPTIONS pWebAuthNGetAuthenticatorListOptions, + _Outptr_result_maybenull_ PWEBAUTHN_AUTHENTICATOR_DETAILS_LIST* ppAuthenticatorDetailsList); + +void +WINAPI +WebAuthNFreeAuthenticatorList( + _In_ PWEBAUTHN_AUTHENTICATOR_DETAILS_LIST pAuthenticatorDetailsList); + +// +// Returns the following Error Names: +// L"Success" - S_OK +// L"InvalidStateError" - NTE_EXISTS +// L"ConstraintError" - HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), +// NTE_NOT_SUPPORTED, +// NTE_TOKEN_KEYSET_STORAGE_FULL +// L"NotSupportedError" - NTE_INVALID_PARAMETER +// L"NotAllowedError" - NTE_DEVICE_NOT_FOUND, +// NTE_NOT_FOUND, +// HRESULT_FROM_WIN32(ERROR_CANCELLED), +// NTE_USER_CANCELLED, +// HRESULT_FROM_WIN32(ERROR_TIMEOUT) +// L"UnknownError" - All other hr values +// +PCWSTR +WINAPI +WebAuthNGetErrorName( + _In_ HRESULT hr); + +HRESULT +WINAPI +WebAuthNGetW3CExceptionDOMError( + _In_ HRESULT hr); + + +#ifdef __cplusplus +} // Balance extern "C" above +#endif + +#endif // WINAPI_FAMILY_PARTITION +#pragma endregion + +#endif // __WEBAUTHN_H_ diff --git a/apps/desktop/desktop_native/win_webauthn/include/webauthnplugin.h b/apps/desktop/desktop_native/win_webauthn/include/webauthnplugin.h new file mode 100644 index 00000000000..bffd2049187 --- /dev/null +++ b/apps/desktop/desktop_native/win_webauthn/include/webauthnplugin.h @@ -0,0 +1,588 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include + +#pragma region Desktop Family or OneCore Family +#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_APP | WINAPI_PARTITION_SYSTEM) + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef WINAPI +#define WINAPI __stdcall +#endif + +#ifndef INITGUID +#define INITGUID +#include +#undef INITGUID +#else +#include +#endif + +//+------------------------------------------------------------------------------------------ +// APIs. +//------------------------------------------------------------------------------------------- + +typedef enum _PLUGIN_AUTHENTICATOR_STATE +{ + AuthenticatorState_Disabled = 0, + AuthenticatorState_Enabled +} AUTHENTICATOR_STATE; + +HRESULT +WINAPI +WebAuthNPluginGetAuthenticatorState( + _In_ REFCLSID rclsid, + _Out_ AUTHENTICATOR_STATE* pluginAuthenticatorState +); + +// +// Plugin Authenticator API: WebAuthNAddPluginAuthenticator: Add Plugin Authenticator +// + +typedef struct _WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS { + // Authenticator Name + LPCWSTR pwszAuthenticatorName; + + // Plugin COM ClsId + REFCLSID rclsid; + + // Plugin RPID (Optional. Required for a nested WebAuthN call originating from a plugin) + LPCWSTR pwszPluginRpId; + + // Plugin Authenticator Logo for the Light themes. base64 encoded SVG 1.1 (Optional) + LPCWSTR pwszLightThemeLogoSvg; + + // Plugin Authenticator Logo for the Dark themes. base64 encoded SVG 1.1 (Optional) + LPCWSTR pwszDarkThemeLogoSvg; + + // CTAP CBOR encoded authenticatorGetInfo + DWORD cbAuthenticatorInfo; + _Field_size_bytes_(cbAuthenticatorInfo) + const BYTE* pbAuthenticatorInfo; + + // List of supported RP IDs (Relying Party IDs). Should be 0/nullptr if all RPs are supported. + DWORD cSupportedRpIds; + const LPCWSTR* ppwszSupportedRpIds; + +} WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS, *PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS; +typedef const WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS *PCWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS; + +typedef struct _WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE { + // Plugin operation signing Public Key - Used to sign the request in PCWEBAUTHN_PLUGIN_OPERATION_REQUEST. Refer pluginauthenticator.h. + DWORD cbOpSignPubKey; + _Field_size_bytes_(cbOpSignPubKey) + PBYTE pbOpSignPubKey; + +} WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE, *PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE; +typedef const WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE *PCWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE; + +HRESULT +WINAPI +WebAuthNPluginAddAuthenticator( + _In_ PCWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS pPluginAddAuthenticatorOptions, + _Outptr_result_maybenull_ PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE* ppPluginAddAuthenticatorResponse); + +void +WINAPI +WebAuthNPluginFreeAddAuthenticatorResponse( + _In_opt_ PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE pPluginAddAuthenticatorResponse); + +// +// Plugin Authenticator API: WebAuthNRemovePluginAuthenticator: Remove Plugin Authenticator +// + +HRESULT +WINAPI +WebAuthNPluginRemoveAuthenticator( + _In_ REFCLSID rclsid); + +// +// Plugin Authenticator API: WebAuthNPluginAuthenticatorUpdateDetails: Update Credential Metadata for Browser AutoFill Scenarios +// + +typedef struct _WEBAUTHN_PLUGIN_UPDATE_AUTHENTICATOR_DETAILS { + // Authenticator Name (Optional) + LPCWSTR pwszAuthenticatorName; + + // Plugin COM ClsId + REFCLSID rclsid; + + // New Plugin COM ClsId (Optional) + REFCLSID rclsidNew; + + // Plugin Authenticator Logo for the Light themes. base64 encoded SVG 1.1 (Optional) + LPCWSTR pwszLightThemeLogoSvg; + + // Plugin Authenticator Logo for the Dark themes. base64 encoded SVG 1.1 (Optional) + LPCWSTR pwszDarkThemeLogoSvg; + + // CTAP CBOR encoded authenticatorGetInfo + DWORD cbAuthenticatorInfo; + _Field_size_bytes_(cbAuthenticatorInfo) + const BYTE* pbAuthenticatorInfo; + + // List of supported RP IDs (Relying Party IDs). Should be 0/nullptr if all RPs are supported. + DWORD cSupportedRpIds; + const LPCWSTR* ppwszSupportedRpIds; + +} WEBAUTHN_PLUGIN_UPDATE_AUTHENTICATOR_DETAILS, *PWEBAUTHN_PLUGIN_UPDATE_AUTHENTICATOR_DETAILS; +typedef const WEBAUTHN_PLUGIN_UPDATE_AUTHENTICATOR_DETAILS *PCWEBAUTHN_PLUGIN_UPDATE_AUTHENTICATOR_DETAILS; + +HRESULT +WINAPI +WebAuthNPluginUpdateAuthenticatorDetails( + _In_ PCWEBAUTHN_PLUGIN_UPDATE_AUTHENTICATOR_DETAILS pPluginUpdateAuthenticatorDetails); + +// +// Plugin Authenticator API: WebAuthNPluginAuthenticatorAddCredentials: Add Credential Metadata for Browser AutoFill Scenarios +// + +typedef struct _WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS { + // Size of pbCredentialId. + DWORD cbCredentialId; + + // Credential Identifier bytes. This field is required. + _Field_size_bytes_(cbCredentialId) + const BYTE* pbCredentialId; + + // Identifier for the RP. This field is required. + LPCWSTR pwszRpId; + + // Contains the friendly name of the Relying Party, such as "Acme Corporation", "Widgets Inc" or "Awesome Site". + // This field is required. + LPCWSTR pwszRpName; + + // Identifier for the User. This field is required. + DWORD cbUserId; + + // User Identifier bytes. This field is required. + _Field_size_bytes_(cbUserId) + const BYTE* pbUserId; + + // Contains a detailed name for this account, such as "john.p.smith@example.com". + LPCWSTR pwszUserName; + + // For User: Contains the friendly name associated with the user account such as "John P. Smith". + LPCWSTR pwszUserDisplayName; + +} WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS, *PWEBAUTHN_PLUGIN_CREDENTIAL_DETAILS; +typedef const WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS *PCWEBAUTHN_PLUGIN_CREDENTIAL_DETAILS; + +HRESULT +WINAPI +WebAuthNPluginAuthenticatorAddCredentials( + _In_ REFCLSID rclsid, + _In_ DWORD cCredentialDetails, + _In_reads_(cCredentialDetails) PCWEBAUTHN_PLUGIN_CREDENTIAL_DETAILS pCredentialDetails); + +// +// Plugin Authenticator API: WebAuthNPluginAuthenticatorRemoveCredentials: Remove Credential Metadata for Browser AutoFill Scenarios +// + +HRESULT +WINAPI +WebAuthNPluginAuthenticatorRemoveCredentials( + _In_ REFCLSID rclsid, + _In_ DWORD cCredentialDetails, + _In_reads_(cCredentialDetails) PCWEBAUTHN_PLUGIN_CREDENTIAL_DETAILS pCredentialDetails); + +// +// Plugin Authenticator API: WebAuthNPluginAuthenticatorRemoveCredentials: Remove All Credential Metadata for Browser AutoFill Scenarios +// + +HRESULT +WINAPI +WebAuthNPluginAuthenticatorRemoveAllCredentials( + _In_ REFCLSID rclsid); + +// +// Plugin Authenticator API: WebAuthNPluginAuthenticatorGetAllCredentials: Get All Credential Metadata cached for Browser AutoFill Scenarios +// + +HRESULT +WINAPI +WebAuthNPluginAuthenticatorGetAllCredentials( + _In_ REFCLSID rclsid, + _Out_ DWORD* pcCredentialDetails, + _Outptr_result_buffer_maybenull_(*pcCredentialDetails) PWEBAUTHN_PLUGIN_CREDENTIAL_DETAILS* ppCredentialDetailsArray); + +// +// Plugin Authenticator API: WebAuthNPluginAuthenticatorFreeCredentialDetailsList: Free Credential Metadata cached for Browser AutoFill Scenarios +// + +void +WINAPI +WebAuthNPluginAuthenticatorFreeCredentialDetailsArray( + _In_ DWORD cCredentialDetails, + _In_reads_(cCredentialDetails) PWEBAUTHN_PLUGIN_CREDENTIAL_DETAILS pCredentialDetailsArray); + +// +// Hello UV API for Plugin: WebAuthNPluginPerformUv: Perform Hello UV related operations +// + +typedef enum _WEBAUTHN_PLUGIN_PERFORM_UV_OPERATION_TYPE +{ + PerformUserVerification = 1, + GetUserVerificationCount, + GetPublicKey +} WEBAUTHN_PLUGIN_PERFORM_UV_OPERATION_TYPE; + +typedef struct _WEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST { + + // Windows handle of the top-level window displayed by the plugin and currently is in foreground as part of the ongoing webauthn operation. + HWND hwnd; + + // The webauthn transaction id from the WEBAUTHN_PLUGIN_OPERATION_REQUEST + REFGUID rguidTransactionId; + + // The username attached to the credential that is in use for this webauthn operation + LPCWSTR pwszUsername; + + // A text hint displayed on the windows hello prompt + LPCWSTR pwszDisplayHint; +} WEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST, *PWEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST; +typedef const WEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST *PCWEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST; + +HRESULT +WINAPI +WebAuthNPluginPerformUserVerification( + _In_ PCWEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST pPluginUserVerification, + _Out_ DWORD* pcbResponse, + _Outptr_result_bytebuffer_maybenull_(*pcbResponse) PBYTE* ppbResponse); + +void +WINAPI +WebAuthNPluginFreeUserVerificationResponse( + _In_opt_ PBYTE ppbResponse); + +HRESULT +WINAPI +WebAuthNPluginGetUserVerificationCount( + _In_ REFCLSID rclsid, + _Out_ DWORD* pdwVerificationCount); + +HRESULT +WINAPI +WebAuthNPluginGetUserVerificationPublicKey( + _In_ REFCLSID rclsid, + _Out_ DWORD* pcbPublicKey, + _Outptr_result_bytebuffer_(*pcbPublicKey) PBYTE* ppbPublicKey); // Free using WebAuthNPluginFreePublicKeyResponse + +HRESULT +WINAPI +WebAuthNPluginGetOperationSigningPublicKey( + _In_ REFCLSID rclsid, + _Out_ DWORD* pcbOpSignPubKey, + _Outptr_result_buffer_maybenull_(*pcbOpSignPubKey) PBYTE* ppbOpSignPubKey); // Free using WebAuthNPluginFreePublicKeyResponse + +void WINAPI WebAuthNPluginFreePublicKeyResponse( + _In_opt_ PBYTE pbOpSignPubKey); + +#define WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS_VERSION_1 1 +#define WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS_CURRENT_VERSION WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS_VERSION_1 +typedef struct _WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS { + //Version of this structure, to allow for modifications in the future. + DWORD dwVersion; + + // Following have following values: + // +1 - TRUE + // 0 - Not defined + // -1 - FALSE + //up: "true" | "false" + LONG lUp; + //uv: "true" | "false" + LONG lUv; + //rk: "true" | "false" + LONG lRequireResidentKey; +} WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS, *PWEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS; +typedef const WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS *PCWEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS; + +#define WEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY_VERSION_1 1 +#define WEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY_CURRENT_VERSION WEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY_VERSION_1 +typedef struct _WEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY { + //Version of this structure, to allow for modifications in the future. + DWORD dwVersion; + + // Key type + LONG lKty; + + // Hash Algorithm: ES256, ES384, ES512 + LONG lAlg; + + // Curve + LONG lCrv; + + //Size of "x" (X Coordinate) + DWORD cbX; + + //"x" (X Coordinate) data. Big Endian. + PBYTE pbX; + + //Size of "y" (Y Coordinate) + DWORD cbY; + + //"y" (Y Coordinate) data. Big Endian. + PBYTE pbY; +} WEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY, *PWEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY; +typedef const WEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY *PCWEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY; + +#define WEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION_VERSION_1 1 +#define WEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION_CURRENT_VERSION WEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION_VERSION_1 +typedef struct _WEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION { + //Version of this structure, to allow for modifications in the future. + DWORD dwVersion; + + // Platform's key agreement public key + PWEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY pKeyAgreement; + + DWORD cbEncryptedSalt; + PBYTE pbEncryptedSalt; + + DWORD cbSaltAuth; + PBYTE pbSaltAuth; +} WEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION, *PWEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION; +typedef const WEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION *PCWEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION; + +#define WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST_VERSION_1 1 +#define WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST_CURRENT_VERSION WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST_VERSION_1 +typedef struct _WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST { + //Version of this structure, to allow for modifications in the future. + DWORD dwVersion; + + //Input RP ID. Raw UTF8 bytes before conversion. + //These are the bytes to be hashed in the Authenticator Data. + DWORD cbRpId; + PBYTE pbRpId; + + //Client Data Hash + DWORD cbClientDataHash; + PBYTE pbClientDataHash; + + //RP Information + PCWEBAUTHN_RP_ENTITY_INFORMATION pRpInformation; + + //User Information + PCWEBAUTHN_USER_ENTITY_INFORMATION pUserInformation; + + // Crypto Parameters + WEBAUTHN_COSE_CREDENTIAL_PARAMETERS WebAuthNCredentialParameters; + + //Credentials used for exclusion + WEBAUTHN_CREDENTIAL_LIST CredentialList; + + //Optional extensions to parse when performing the operation. + DWORD cbCborExtensionsMap; + PBYTE pbCborExtensionsMap; + + // Authenticator Options (Optional) + PWEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS pAuthenticatorOptions; + + // Pin Auth (Optional) + BOOL fEmptyPinAuth; // Zero length PinAuth is included in the request + DWORD cbPinAuth; + PBYTE pbPinAuth; + + //"hmac-secret": true extension + LONG lHmacSecretExt; + + // "hmac-secret-mc" extension + PWEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION pHmacSecretMcExtension; + + //"prf" extension + LONG lPrfExt; + DWORD cbHmacSecretSaltValues; + PBYTE pbHmacSecretSaltValues; + + //"credProtect" extension. Nonzero if present + DWORD dwCredProtect; + + // Nonzero if present + DWORD dwPinProtocol; + + // Nonzero if present + DWORD dwEnterpriseAttestation; + + //"credBlob" extension. Nonzero if present + DWORD cbCredBlobExt; + PBYTE pbCredBlobExt; + + //"largeBlobKey": true extension + LONG lLargeBlobKeyExt; + + //"largeBlob": extension + DWORD dwLargeBlobSupport; + + //"minPinLength": true extension + LONG lMinPinLengthExt; + + // "json" extension. Nonzero if present + DWORD cbJsonExt; + PBYTE pbJsonExt; +} WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST, *PWEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST; +typedef const WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST *PCWEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST; + +_Success_(return == S_OK) +HRESULT +WINAPI +WebAuthNEncodeMakeCredentialResponse( + _In_ PCWEBAUTHN_CREDENTIAL_ATTESTATION pCredentialAttestation, + _Out_ DWORD* pcbResp, + _Outptr_result_buffer_maybenull_(*pcbResp) BYTE** ppbResp + ); + +_Success_(return == S_OK) +HRESULT +WINAPI +WebAuthNDecodeMakeCredentialRequest( + _In_ DWORD cbEncoded, + _In_reads_bytes_(cbEncoded) const BYTE* pbEncoded, + _Outptr_ PWEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST* ppMakeCredentialRequest + ); + +void +WINAPI +WebAuthNFreeDecodedMakeCredentialRequest( + _In_opt_ PWEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST pMakeCredentialRequest + ); + +#define WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST_VERSION_1 1 +#define WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST_CURRENT_VERSION WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST_VERSION_1 +typedef struct _WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST { + //Version of this structure, to allow for modifications in the future. + DWORD dwVersion; + + //RP ID. After UTF8 to Unicode conversion, + PCWSTR pwszRpId; + + //Input RP ID. Raw UTF8 bytes before conversion. + //These are the bytes to be hashed in the Authenticator Data. + DWORD cbRpId; + PBYTE pbRpId; + + //Client Data Hash + DWORD cbClientDataHash; + PBYTE pbClientDataHash; + + //Credentials used for inclusion + WEBAUTHN_CREDENTIAL_LIST CredentialList; + + //Optional extensions to parse when performing the operation. + DWORD cbCborExtensionsMap; + PBYTE pbCborExtensionsMap; + + // Authenticator Options (Optional) + PWEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS pAuthenticatorOptions; + + // Pin Auth (Optional) + BOOL fEmptyPinAuth; // Zero length PinAuth is included in the request + DWORD cbPinAuth; + PBYTE pbPinAuth; + + // HMAC Salt Extension (Optional) + PWEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION pHmacSaltExtension; + + // PRF Extension + DWORD cbHmacSecretSaltValues; + PBYTE pbHmacSecretSaltValues; + + DWORD dwPinProtocol; + + //"credBlob": true extension + LONG lCredBlobExt; + + //"largeBlobKey": true extension + LONG lLargeBlobKeyExt; + + //"largeBlob" extension + DWORD dwCredLargeBlobOperation; + DWORD cbCredLargeBlobCompressed; + PBYTE pbCredLargeBlobCompressed; + DWORD dwCredLargeBlobOriginalSize; + + // "json" extension. Nonzero if present + DWORD cbJsonExt; + PBYTE pbJsonExt; +} WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST, *PWEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST; +typedef const WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST *PCWEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST; + +_Success_(return == S_OK) +HRESULT +WINAPI +WebAuthNDecodeGetAssertionRequest( + _In_ DWORD cbEncoded, + _In_reads_bytes_(cbEncoded) const BYTE* pbEncoded, + _Outptr_ PWEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST* ppGetAssertionRequest + ); + +void +WINAPI +WebAuthNFreeDecodedGetAssertionRequest( + _In_opt_ PWEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST pGetAssertionRequest + ); + +typedef struct _WEBAUTHN_CTAPCBOR_GET_ASSERTION_RESPONSE { + // [1] credential (optional) + // [2] authenticatorData + // [3] signature + WEBAUTHN_ASSERTION WebAuthNAssertion; + + // [4] user (optional) + PCWEBAUTHN_USER_ENTITY_INFORMATION pUserInformation; + + // [5] numberOfCredentials (optional) + DWORD dwNumberOfCredentials; + + // [6] userSelected (optional) + LONG lUserSelected; + + // [7] largeBlobKey (optional) + DWORD cbLargeBlobKey; + PBYTE pbLargeBlobKey; + + // [8] unsignedExtensionOutputs + DWORD cbUnsignedExtensionOutputs; + PBYTE pbUnsignedExtensionOutputs; +} WEBAUTHN_CTAPCBOR_GET_ASSERTION_RESPONSE, *PWEBAUTHN_CTAPCBOR_GET_ASSERTION_RESPONSE; +typedef const WEBAUTHN_CTAPCBOR_GET_ASSERTION_RESPONSE *PCWEBAUTHN_CTAPCBOR_GET_ASSERTION_RESPONSE; + +_Success_(return == S_OK) +HRESULT +WINAPI +WebAuthNEncodeGetAssertionResponse( + _In_ PCWEBAUTHN_CTAPCBOR_GET_ASSERTION_RESPONSE pGetAssertionResponse, + _Out_ DWORD* pcbResp, + _Outptr_result_buffer_maybenull_(*pcbResp) BYTE** ppbResp + ); + +typedef void (CALLBACK* WEBAUTHN_PLUGIN_STATUS_CHANGE_CALLBACK )(void* context); + +HRESULT +WINAPI +WebAuthNPluginRegisterStatusChangeCallback( + _In_ WEBAUTHN_PLUGIN_STATUS_CHANGE_CALLBACK callback, + _In_ void* context, + _In_ REFCLSID rclsid, + _Out_ DWORD* pdwRegister + ); + +HRESULT +WINAPI +WebAuthNPluginUnregisterStatusChangeCallback( + _In_ DWORD* pdwRegister + ); + + +#ifdef __cplusplus +} // Balance extern "C" above +#endif + +#endif // WINAPI_FAMILY_PARTITION +#pragma endregion + + diff --git a/apps/desktop/desktop_native/win_webauthn/src/lib.rs b/apps/desktop/desktop_native/win_webauthn/src/lib.rs new file mode 100644 index 00000000000..7bbefd8ba9d --- /dev/null +++ b/apps/desktop/desktop_native/win_webauthn/src/lib.rs @@ -0,0 +1,71 @@ +pub mod plugin; +mod types; +mod util; + +use std::{error::Error, fmt::Display}; + +pub use types::{ + AuthenticatorInfo, CredentialId, CtapTransport, CtapVersion, PublicKeyCredentialParameters, + UserId, +}; + +#[derive(Debug)] +pub struct WinWebAuthnError { + kind: ErrorKind, + description: Option, + cause: Option>, +} + +impl WinWebAuthnError { + pub(crate) fn new(kind: ErrorKind, description: &str) -> Self { + Self { + kind, + description: Some(description.to_string()), + cause: None, + } + } + + pub(crate) fn with_cause( + kind: ErrorKind, + description: &str, + cause: E, + ) -> Self { + let cause: Box = Box::new(cause); + Self { + kind, + description: Some(description.to_string()), + cause: Some(cause), + } + } +} + +#[derive(Debug)] +enum ErrorKind { + DllLoad, + Serialization, + InvalidArguments, + Other, + WindowsInternal, +} + +impl Display for WinWebAuthnError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let msg = match self.kind { + ErrorKind::Serialization => "Failed to serialize data", + ErrorKind::DllLoad => "Failed to load function from DLL", + ErrorKind::InvalidArguments => "Invalid arguments passed to function", + ErrorKind::Other => "An error occurred", + ErrorKind::WindowsInternal => "A Windows error occurred", + }; + f.write_str(msg)?; + if let Some(d) = &self.description { + write!(f, ": {d}")?; + } + if let Some(e) = &self.cause { + write!(f, ". Caused by: {e}")?; + } + Ok(()) + } +} + +impl Error for WinWebAuthnError {} diff --git a/apps/desktop/desktop_native/win_webauthn/src/plugin/com.rs b/apps/desktop/desktop_native/win_webauthn/src/plugin/com.rs new file mode 100644 index 00000000000..bdc1e9fbe3f --- /dev/null +++ b/apps/desktop/desktop_native/win_webauthn/src/plugin/com.rs @@ -0,0 +1,419 @@ +//! Functions for interacting with Windows COM. +#![allow(non_snake_case)] +#![allow(non_camel_case_types)] + +use std::{ + alloc, + mem::MaybeUninit, + ptr::{self, NonNull}, + sync::{Arc, OnceLock}, +}; + +use windows::{ + core::{implement, interface, ComObjectInterface, IUnknown, GUID, HRESULT}, + Win32::{ + Foundation::{E_FAIL, E_INVALIDARG, S_OK}, + System::Com::*, + }, +}; +use windows_core::{IInspectable, Interface}; + +use super::types::{ + PluginLockStatus, WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST, WEBAUTHN_PLUGIN_OPERATION_REQUEST, + WEBAUTHN_PLUGIN_OPERATION_RESPONSE, +}; + +use super::PluginAuthenticator; +use crate::{ErrorKind, WinWebAuthnError}; + +static HANDLER: OnceLock> = OnceLock::new(); + +#[implement(IClassFactory)] +pub struct Factory; + +impl IClassFactory_Impl for Factory_Impl { + fn CreateInstance( + &self, + _outer: windows::core::Ref, + iid: *const windows::core::GUID, + object: *mut *mut core::ffi::c_void, + ) -> windows::core::Result<()> { + let handler = match HANDLER.get() { + Some(handler) => handler, + None => { + tracing::error!("Cannot create COM class object instance because the handler is not initialized. register_server() must be called before starting the COM server."); + return Err(E_FAIL.into()); + } + }.clone(); + let unknown: IInspectable = PluginAuthenticatorComObject { handler }.into(); + unsafe { unknown.query(iid, object).ok() } + } + + fn LockServer(&self, _lock: windows::core::BOOL) -> windows::core::Result<()> { + // TODO: Implement lock server + Ok(()) + } +} + +// IPluginAuthenticator interface +#[interface("d26bcf6f-b54c-43ff-9f06-d5bf148625f7")] +pub unsafe trait IPluginAuthenticator: windows::core::IUnknown { + fn MakeCredential( + &self, + request: *const WEBAUTHN_PLUGIN_OPERATION_REQUEST, + response: *mut WEBAUTHN_PLUGIN_OPERATION_RESPONSE, + ) -> HRESULT; + fn GetAssertion( + &self, + request: *const WEBAUTHN_PLUGIN_OPERATION_REQUEST, + response: *mut WEBAUTHN_PLUGIN_OPERATION_RESPONSE, + ) -> HRESULT; + fn CancelOperation(&self, request: *const WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST) -> HRESULT; + fn GetLockStatus(&self, lock_status: *mut PluginLockStatus) -> HRESULT; +} + +#[implement(IPluginAuthenticator)] +struct PluginAuthenticatorComObject { + handler: Arc, +} + +impl IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Impl { + unsafe fn MakeCredential( + &self, + request: *const WEBAUTHN_PLUGIN_OPERATION_REQUEST, + response: *mut WEBAUTHN_PLUGIN_OPERATION_RESPONSE, + ) -> HRESULT { + tracing::debug!("MakeCredential called"); + let response = match NonNull::new(response) { + Some(p) => p, + None => { + tracing::warn!( + "MakeCredential called with null response pointer from Windows. Aborting request." + ); + return E_INVALIDARG; + } + }; + let op_request_ptr = match NonNull::new(request.cast_mut()) { + Some(p) => p, + None => { + tracing::warn!( + "MakeCredential called with null request pointer from Windows. Aborting request." + ); + return E_INVALIDARG; + } + }; + + // TODO: verify request signature + + let registration_request = match op_request_ptr.try_into() { + Ok(r) => r, + Err(err) => { + tracing::error!("Could not deserialize MakeCredential request: {err}"); + return E_FAIL; + } + }; + match self.handler.make_credential(registration_request) { + Ok(registration_response) => { + // SAFETY: response pointer was given to us by Windows, so we assume it's valid. + match write_operation_response(®istration_response, response) { + Ok(()) => { + tracing::debug!("MakeCredential completed successfully"); + S_OK + } + Err(err) => { + tracing::error!( + "Failed to write MakeCredential response to Windows: {err}" + ); + return E_FAIL; + } + } + } + Err(err) => { + tracing::error!("MakeCredential failed: {err}"); + E_FAIL + } + } + } + + unsafe fn GetAssertion( + &self, + request: *const WEBAUTHN_PLUGIN_OPERATION_REQUEST, + response: *mut WEBAUTHN_PLUGIN_OPERATION_RESPONSE, + ) -> HRESULT { + tracing::debug!("GetAssertion called"); + let response = match NonNull::new(response) { + Some(p) => p, + None => { + tracing::warn!( + "GetAssertion called with null response pointer from Windows. Aborting request." + ); + return E_INVALIDARG; + } + }; + let op_request_ptr = match NonNull::new(request.cast_mut()) { + Some(p) => p, + None => { + tracing::warn!( + "GetAssertion called with null request pointer from Windows. Aborting request." + ); + return E_INVALIDARG; + } + }; + // TODO: verify request signature + let assertion_request = match op_request_ptr.try_into() { + Ok(assertion_request) => assertion_request, + Err(err) => { + tracing::error!("Could not deserialize GetAssertion request: {err}"); + return E_FAIL; + } + }; + match self.handler.get_assertion(assertion_request) { + Ok(assertion_response) => { + // SAFETY: response pointer was given to us by Windows, so we assume it's valid. + match write_operation_response(&assertion_response, response) { + Ok(()) => { + tracing::debug!("GetAssertion completed successfully"); + S_OK + } + Err(err) => { + tracing::error!("Failed to write GetCredential response to Windows: {err}"); + return E_FAIL; + } + } + } + Err(err) => { + tracing::error!("GetAssertion failed: {err}"); + E_FAIL + } + } + } + + unsafe fn CancelOperation( + &self, + request: *const WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST, + ) -> HRESULT { + tracing::debug!("CancelOperation called"); + let request = match NonNull::new(request as *mut WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST) { + Some(request) => request, + None => { + tracing::warn!("Received null CancelOperation request"); + return E_INVALIDARG; + } + }; + + match self.handler.cancel_operation(request.into()) { + Ok(()) => { + tracing::error!("CancelOperation completed successfully"); + S_OK + } + Err(err) => { + tracing::error!("CancelOperation failed: {err}"); + E_FAIL + } + } + } + + unsafe fn GetLockStatus(&self, lock_status: *mut PluginLockStatus) -> HRESULT { + tracing::debug!( + "GetLockStatus() called ", + std::process::id(), + std::thread::current().id() + ); + if lock_status.is_null() { + return HRESULT(-2147024809); // E_INVALIDARG + } + + match self.handler.lock_status() { + Ok(status) => { + tracing::debug!("GetLockStatus received {status:?}"); + *lock_status = status; + S_OK + } + Err(err) => { + tracing::error!("GetLockStatus failed: {err}"); + E_FAIL + } + } + } +} + +/// Copies data as COM-allocated buffer and writes to response pointer. +/// +/// Safety constraints: [response] must point to a valid +/// WEBAUTHN_PLUGIN_OPERATION_RESPONSE struct. +unsafe fn write_operation_response( + data: &[u8], + response: NonNull, +) -> Result<(), WinWebAuthnError> { + let len = match data.len().try_into() { + Ok(len) => len, + Err(err) => { + return Err(WinWebAuthnError::with_cause( + ErrorKind::Serialization, + "Response is too long to return to OS", + err, + )); + } + }; + let buf = data.to_com_buffer(); + + response.write(WEBAUTHN_PLUGIN_OPERATION_RESPONSE { + cbEncodedResponse: len, + pbEncodedResponse: buf.leak(), + }); + Ok(()) +} +/// Registers the plugin authenticator COM library with Windows. +pub(super) fn register_server(clsid: &GUID, handler: T) -> Result<(), WinWebAuthnError> +where + T: PluginAuthenticator + Send + Sync + 'static, +{ + // Store the handler as a static so it can be initialized + HANDLER.set(Arc::new(handler)).map_err(|_| { + WinWebAuthnError::new(ErrorKind::WindowsInternal, "Handler already initialized") + })?; + + static FACTORY: windows::core::StaticComObject = Factory.into_static(); + unsafe { + CoRegisterClassObject( + ptr::from_ref(clsid), + FACTORY.as_interface_ref(), + CLSCTX_LOCAL_SERVER, + REGCLS_MULTIPLEUSE, + ) + } + .map_err(|err| { + WinWebAuthnError::with_cause( + ErrorKind::WindowsInternal, + "Couldn't register the COM library with Windows", + err, + ) + })?; + Ok(()) +} + +/// Initializes the COM library for use on the calling thread, +/// and registers + sets the security values. +pub(super) fn initialize() -> std::result::Result<(), WinWebAuthnError> { + let result = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) }; + + if result.is_err() { + return Err(WinWebAuthnError::with_cause( + ErrorKind::WindowsInternal, + "Could not initialize the COM library", + windows::core::Error::from_hresult(result), + )); + } + + unsafe { + CoInitializeSecurity( + None, + -1, + None, + None, + RPC_C_AUTHN_LEVEL_DEFAULT, + RPC_C_IMP_LEVEL_IMPERSONATE, + None, + EOAC_NONE, + None, + ) + } + .map_err(|err| { + WinWebAuthnError::with_cause( + ErrorKind::WindowsInternal, + "Could not initialize COM security", + err, + ) + }) +} + +pub(super) fn uninitialize() -> std::result::Result<(), WinWebAuthnError> { + unsafe { CoUninitialize() }; + Ok(()) +} + +#[repr(transparent)] +pub(super) struct ComBuffer(NonNull>); + +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::()).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()) + } + + pub fn leak(self) -> *mut T { + self.0.cast().as_ptr() + } +} + +pub(super) trait ComBufferExt { + fn to_com_buffer(&self) -> ComBuffer; +} + +impl ComBufferExt for Vec { + fn to_com_buffer(&self) -> ComBuffer { + ComBuffer::from(&self) + } +} + +impl ComBufferExt for &[u8] { + fn to_com_buffer(&self) -> ComBuffer { + ComBuffer::from(self) + } +} + +impl ComBufferExt for Vec { + fn to_com_buffer(&self) -> ComBuffer { + let buffer: Vec = self.into_iter().flat_map(|x| x.to_le_bytes()).collect(); + ComBuffer::from(&buffer) + } +} + +impl ComBufferExt for &[u16] { + fn to_com_buffer(&self) -> ComBuffer { + let buffer: Vec = self + .as_ref() + .into_iter() + .flat_map(|x| x.to_le_bytes()) + .collect(); + ComBuffer::from(&buffer) + } +} + +impl> From for ComBuffer { + fn from(value: T) -> Self { + let buffer: Vec = value + .as_ref() + .into_iter() + .flat_map(|x| x.to_le_bytes()) + .collect(); + 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::(), len) + .as_mut() + .copy_from_slice(&buffer); + } + com_buffer + } +} diff --git a/apps/desktop/desktop_native/win_webauthn/src/plugin/mod.rs b/apps/desktop/desktop_native/win_webauthn/src/plugin/mod.rs new file mode 100644 index 00000000000..cc4c53c38ff --- /dev/null +++ b/apps/desktop/desktop_native/win_webauthn/src/plugin/mod.rs @@ -0,0 +1,335 @@ +pub(crate) mod com; +pub(crate) mod types; + +use std::{error::Error, ptr::NonNull}; +use types::*; +use windows::{ + core::GUID, + Win32::Foundation::{NTE_USER_CANCELLED, S_OK}, +}; + +pub use types::{ + PluginAddAuthenticatorOptions, PluginAddAuthenticatorResponse, PluginCancelOperationRequest, + PluginCredentialDetails, PluginGetAssertionRequest, PluginLockStatus, + PluginMakeCredentialRequest, PluginMakeCredentialResponse, PluginUserVerificationRequest, + PluginUserVerificationResponse, +}; + +use super::{ErrorKind, WinWebAuthnError}; +use crate::{ + plugin::com::{ComBuffer, ComBufferExt}, + util::WindowsString, +}; + +#[derive(Clone, Copy)] +pub struct Clsid(GUID); + +impl TryFrom<&str> for Clsid { + type Error = WinWebAuthnError; + + fn try_from(value: &str) -> Result { + // Remove hyphens and parse as hex + let clsid_clean = value.replace("-", "").replace("{", "").replace("}", ""); + if clsid_clean.len() != 32 { + return Err(WinWebAuthnError::new( + ErrorKind::Serialization, + "Invalid CLSID format", + )); + } + + // Convert to u128 and create GUID + let clsid_u128 = u128::from_str_radix(&clsid_clean, 16).map_err(|err| { + WinWebAuthnError::with_cause( + ErrorKind::Serialization, + "Failed to parse CLSID as hex", + err, + ) + })?; + + let clsid = Clsid(GUID::from_u128(clsid_u128)); + Ok(clsid) + } +} +pub struct WebAuthnPlugin { + clsid: Clsid, +} + +impl WebAuthnPlugin { + pub fn new(clsid: Clsid) -> Self { + WebAuthnPlugin { clsid } + } + + /// Registers a COM server with Windows. + /// + /// The handler should be an instance of your type that implements PluginAuthenticator. + /// The same instance will be shared across all COM calls. + /// + /// This only needs to be called on installation of your application. + pub fn register_server(&self, handler: T) -> Result<(), WinWebAuthnError> + where + T: PluginAuthenticator + Send + Sync + 'static, + { + com::register_server(&self.clsid.0, handler) + } + + /// Initializes the COM library for use on the calling thread, + /// and registers + sets the security values. + pub fn initialize() -> Result<(), WinWebAuthnError> { + com::initialize() + } + + /// Adds this implementation as a Windows WebAuthn plugin. + /// + /// This only needs to be called on installation of your application. + pub fn add_authenticator( + options: PluginAddAuthenticatorOptions, + ) -> Result { + #![allow(non_snake_case)] + let mut response_ptr: *mut WebAuthnPluginAddAuthenticatorResponse = std::ptr::null_mut(); + + // We need to be careful to use .as_ref() to ensure that we're not + // sending dangling pointers to API. + let authenticator_name = options.authenticator_name.to_utf16(); + + let rp_id = options.rp_id.as_ref().map(|rp_id| rp_id.to_utf16()); + let pwszPluginRpId = rp_id.as_ref().map_or(std::ptr::null(), |v| v.as_ptr()); + + let light_logo_b64 = options.light_theme_logo_b64(); + let pwszLightThemeLogoSvg = light_logo_b64 + .as_ref() + .map_or(std::ptr::null(), |v| v.as_ptr()); + let dark_logo_b64 = options.dark_theme_logo_b64(); + let pwszDarkThemeLogoSvg = dark_logo_b64 + .as_ref() + .map_or(std::ptr::null(), |v| v.as_ptr()); + + let authenticator_info = options.authenticator_info.as_ctap_bytes()?; + + let supported_rp_ids: Option>> = options + .supported_rp_ids + .map(|ids| ids.iter().map(|id| id.to_utf16()).collect()); + let supported_rp_id_ptrs: Option> = supported_rp_ids + .as_ref() + .map(|ids| ids.iter().map(Vec::as_ptr).collect()); + let pbSupportedRpIds = supported_rp_id_ptrs + .as_ref() + .map_or(std::ptr::null(), |v| v.as_ptr()); + + let options_c = WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS { + pwszAuthenticatorName: authenticator_name.as_ptr(), + rclsid: &options.clsid.0, + pwszPluginRpId, + pwszLightThemeLogoSvg, + pwszDarkThemeLogoSvg, + cbAuthenticatorInfo: authenticator_info.len() as u32, + pbAuthenticatorInfo: authenticator_info.as_ptr(), + cSupportedRpIds: supported_rp_id_ptrs.map_or(0, |ids| ids.len() as u32), + pbSupportedRpIds, + }; + let result = webauthn_plugin_add_authenticator(&options_c, &mut response_ptr)?; + result.ok().map_err(|err| { + WinWebAuthnError::with_cause( + ErrorKind::WindowsInternal, + "Failed to add authenticator", + err, + ) + })?; + + if let Some(response) = NonNull::new(response_ptr) { + Ok(response.into()) + } else { + Err(WinWebAuthnError::new( + ErrorKind::WindowsInternal, + "WebAuthNPluginAddAuthenticatorResponse returned null", + )) + } + } + + pub fn perform_user_verification( + request: PluginUserVerificationRequest, + ) -> Result { + tracing::debug!(?request, "Handling user verification request"); + let user_name = request.user_name.to_utf16().to_com_buffer(); + let hint = request.display_hint.map(|d| d.to_utf16().to_com_buffer()); + let uv_request = WEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST { + hwnd: request.window_handle, + rguidTransactionId: &request.transaction_id, + pwszUsername: user_name.leak(), + pwszDisplayHint: hint.map_or(std::ptr::null(), |buf| buf.leak()), + }; + let mut response_len = 0; + let mut response_ptr = std::ptr::null_mut(); + let hresult = webauthn_plugin_perform_user_verification( + &uv_request, + &mut response_len, + &mut response_ptr, + )?; + match hresult { + S_OK => { + let signature = if response_len > 0 { + Vec::new() + } else { + // SAFETY: Windows returned successful response code and length, so we assume that the data is initialized + unsafe { + // SAFETY: Windows only runs on platforms where usize >= u32; + let len = response_len as usize; + std::slice::from_raw_parts(response_ptr, len).to_vec() + } + }; + webauthn_plugin_free_user_verification_response(response_ptr)?; + Ok(PluginUserVerificationResponse { + transaction_id: request.transaction_id, + signature, + }) + } + NTE_USER_CANCELLED => Err(WinWebAuthnError::new( + ErrorKind::Other, + "User cancelled user verification", + )), + _ => Err(WinWebAuthnError::with_cause( + ErrorKind::WindowsInternal, + "Unknown error occurred while performing user verification", + windows::core::Error::from_hresult(hresult), + )), + } + } + + /// Synchronize credentials to Windows Hello cache. + /// + /// Number of credentials to sync must be less than [u32::MAX]. + pub fn sync_credentials( + &self, + credentials: Vec, + ) -> Result<(), WinWebAuthnError> { + if credentials.is_empty() { + tracing::debug!("[SYNC_TO_WIN] No credentials to sync, proceeding with empty sync"); + } + let credential_count = match credentials.len().try_into() { + Ok(c) => c, + Err(err) => { + return Err(WinWebAuthnError::with_cause( + ErrorKind::InvalidArguments, + "Too many credentials passed to sync", + err, + )); + } + }; + + // First try to remove all existing credentials for this plugin + tracing::debug!("Attempting to remove all existing credentials before sync..."); + match webauthn_plugin_authenticator_remove_all_credentials(&self.clsid.0)?.ok() { + Ok(()) => { + tracing::debug!("Successfully removed existing credentials"); + } + Err(e) => { + tracing::warn!("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() { + tracing::debug!("No credentials to add to Windows - sync completed successfully"); + Ok(()) + } else { + tracing::debug!("Adding new credentials to Windows..."); + + // Convert Bitwarden credentials to Windows credential details + let mut win_credentials = Vec::new(); + + for (i, cred) in credentials.iter().enumerate() { + tracing::debug!("[SYNC_TO_WIN] Converting credential {}: RP ID: {}, User: {}, Credential ID: {:?} ({} bytes), User ID: {:?} ({} bytes)", + i + 1, cred.rp_id, cred.user_name, &cred.credential_id, cred.credential_id.len(), &cred.user_id, cred.user_id.len()); + + // Allocate credential_id bytes with COM + let credential_id_buf = cred.credential_id.as_ref().to_com_buffer(); + + // Allocate user_id bytes with COM + let user_id_buf = cred.user_id.as_ref().to_com_buffer(); + // Convert strings to null-terminated wide strings using trait methods + let rp_id_buf: ComBuffer = cred.rp_id.to_utf16().to_com_buffer(); + let rp_friendly_name_buf: Option = cred + .rp_friendly_name + .as_ref() + .map(|display_name| display_name.to_utf16().to_com_buffer()); + let user_name_buf: ComBuffer = (cred.user_name.to_utf16()).to_com_buffer(); + let user_display_name_buf: ComBuffer = + cred.user_display_name.to_utf16().to_com_buffer(); + let win_cred = WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS { + credential_id_byte_count: u32::from(cred.credential_id.len()), + credential_id_pointer: credential_id_buf.leak(), + rpid: rp_id_buf.leak(), + rp_friendly_name: rp_friendly_name_buf + .map_or(std::ptr::null(), |buf| buf.leak()), + user_id_byte_count: u32::from(cred.user_id.len()), + user_id_pointer: user_id_buf.leak(), + user_name: user_name_buf.leak(), + user_display_name: user_display_name_buf.leak(), + }; + win_credentials.push(win_cred); + tracing::debug!( + "[SYNC_TO_WIN] Converted credential {} to Windows format", + i + 1 + ); + } + + match webauthn_plugin_authenticator_add_credentials( + &self.clsid.0, + credential_count, + win_credentials.as_ptr(), + ) { + Ok(hresult) => { + if let Err(err) = hresult.ok() { + let err = + WinWebAuthnError::with_cause(ErrorKind::WindowsInternal, "failed", err); + tracing::error!( + "Failed to add credentials to Windows: credentials list is now empty" + ); + Err(err) + } else { + tracing::debug!("Successfully synced credentials to Windows"); + Ok(()) + } + } + Err(e) => { + tracing::error!("Failed to add credentials to Windows: {}", e); + Err(e) + } + } + } + } +} +pub trait PluginAuthenticator { + /// Process a request to create a new credential. + /// + /// Returns a [CTAP authenticatorMakeCredential response structure](https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.html#authenticatormakecredential-response-structure). + fn make_credential( + &self, + request: PluginMakeCredentialRequest, + ) -> Result, Box>; + + /// Process a request to assert a credential. + /// + /// Returns a [CTAP authenticatorGetAssertion response structure](https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.html#authenticatorgetassertion-response-structure). + fn get_assertion(&self, request: PluginGetAssertionRequest) -> Result, Box>; + + /// Cancel an ongoing operation. + fn cancel_operation(&self, request: PluginCancelOperationRequest) + -> Result<(), Box>; + + /// Retrieve lock status. + fn lock_status(&self) -> Result>; +} + +#[cfg(test)] +mod tests { + use super::Clsid; + + const CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062"; + + #[test] + fn test_parse_clsid_to_guid() { + let result = Clsid::try_from(CLSID); + assert!(result.is_ok(), "CLSID parsing should succeed"); + } +} diff --git a/apps/desktop/desktop_native/win_webauthn/src/plugin/types.rs b/apps/desktop/desktop_native/win_webauthn/src/plugin/types.rs new file mode 100644 index 00000000000..687fd601337 --- /dev/null +++ b/apps/desktop/desktop_native/win_webauthn/src/plugin/types.rs @@ -0,0 +1,930 @@ +//! Types pertaining to registering a plugin implementation and handling plugin +//! authenticator requests. + +#![allow(non_snake_case)] +#![allow(non_camel_case_types)] + +use std::{mem::MaybeUninit, ptr::NonNull}; + +use base64::{engine::general_purpose::STANDARD, Engine as _}; +use windows::{ + core::{GUID, HRESULT}, + Win32::{Foundation::HWND, System::LibraryLoader::GetProcAddress}, +}; +use windows_core::s; + +use crate::{ + types::UserId, + util::{webauthn_call, WindowsString}, + CredentialId, ErrorKind, WinWebAuthnError, +}; + +use crate::types::{ + AuthenticatorInfo, CredentialList, CtapTransport, HmacSecretSalt, + WebAuthnExtensionMakeCredentialOutput, WEBAUTHN_COSE_CREDENTIAL_PARAMETERS, + WEBAUTHN_CREDENTIAL_ATTESTATION, WEBAUTHN_CREDENTIAL_LIST, WEBAUTHN_EXTENSIONS, + WEBAUTHN_RP_ENTITY_INFORMATION, WEBAUTHN_USER_ENTITY_INFORMATION, +}; + +use super::Clsid; + +// Plugin Registration types + +/// Windows WebAuthn Authenticator Options structure +/// Header File Name: _WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS { + dwVersion: u32, + // LONG lUp: +1=TRUE, 0=Not defined, -1=FALSE + lUp: i32, + // LONG lUv: +1=TRUE, 0=Not defined, -1=FALSE + lUv: i32, + // LONG lRequireResidentKey: +1=TRUE, 0=Not defined, -1=FALSE + lRequireResidentKey: i32, +} + +impl WebAuthnCtapCborAuthenticatorOptions { + pub fn version(&self) -> u32 { + self.dwVersion + } + + pub fn user_presence(&self) -> Option { + Self::to_optional_bool(self.lUp) + } + + pub fn user_verification(&self) -> Option { + Self::to_optional_bool(self.lUv) + } + + pub fn require_resident_key(&self) -> Option { + Self::to_optional_bool(self.lRequireResidentKey) + } + + fn to_optional_bool(value: i32) -> Option { + match value { + x if x > 0 => Some(true), + x if x < 0 => Some(false), + _ => None, + } + } +} + +pub type WebAuthnCtapCborAuthenticatorOptions = WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS; + +/// Used when adding a Windows plugin authenticator (stable API). +/// Header File Name: _WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS +/// Header File Usage: WebAuthNPluginAddAuthenticator() +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(super) struct WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS { + /// Authenticator Name + pub(super) pwszAuthenticatorName: *const u16, + + /// Plugin COM ClsId + pub(super) rclsid: *const GUID, + + /// Plugin RPID + /// + /// Required for a nested WebAuthN call originating from a plugin. + pub(super) pwszPluginRpId: *const u16, + + /// Plugin Authenticator Logo for the Light themes. base64-encoded SVG 1.1 + /// + /// The data should be encoded as `UTF16(BASE64(UTF8(svg_text)))`. + pub(super) pwszLightThemeLogoSvg: *const u16, + + /// Plugin Authenticator Logo for the Dark themes. base64-encoded SVG 1.1 + /// + /// The data should be encoded as `UTF16(BASE64(UTF8(svg_text)))`. + pub(super) pwszDarkThemeLogoSvg: *const u16, + + pub(super) cbAuthenticatorInfo: u32, + /// CTAP CBOR-encoded authenticatorGetInfo output + pub(super) pbAuthenticatorInfo: *const u8, + + pub(super) cSupportedRpIds: u32, + /// List of supported RP IDs (Relying Party IDs). + /// + /// Should be null if all RPs are supported. + pub(super) pbSupportedRpIds: *const *const u16, +} + +pub struct PluginAddAuthenticatorOptions { + /// Authenticator Name + pub authenticator_name: String, + + /// Plugin COM ClsId + pub clsid: Clsid, + + /// Plugin RPID + /// + /// Required for a nested WebAuthN call originating from a plugin. + pub rp_id: Option, + + /// Plugin Authenticator Logo for the Light themes. + /// + /// String should contain a valid SVG 1.1 document. + pub light_theme_logo_svg: Option, + + // Plugin Authenticator Logo for the Dark themes. Bytes of SVG 1.1. + /// + /// String should contain a valid SVG 1.1 element. + pub dark_theme_logo_svg: Option, + + /// CTAP authenticatorGetInfo values + pub authenticator_info: AuthenticatorInfo, + + /// List of supported RP IDs (Relying Party IDs). + /// + /// Should be [None] if all RPs are supported. + pub supported_rp_ids: Option>, +} + +impl PluginAddAuthenticatorOptions { + pub(super) fn light_theme_logo_b64(&self) -> Option> { + self.light_theme_logo_svg + .as_ref() + .map(|svg| Self::encode_svg(&svg)) + } + + pub(super) fn dark_theme_logo_b64(&self) -> Option> { + self.dark_theme_logo_svg + .as_ref() + .map(|svg| Self::encode_svg(&svg)) + } + + fn encode_svg(svg: &str) -> Vec { + let logo_b64: String = STANDARD.encode(svg); + logo_b64.to_utf16() + } +} + +/// Used as a response type when adding a Windows plugin authenticator. +/// Header File Name: _WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE +/// Header File Usage: WebAuthNPluginAddAuthenticator() +/// WebAuthNPluginFreeAddAuthenticatorResponse() +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(super) struct WebAuthnPluginAddAuthenticatorResponse { + pub plugin_operation_signing_key_byte_count: u32, + pub plugin_operation_signing_key: *mut u8, +} + +type WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE = WebAuthnPluginAddAuthenticatorResponse; + +/// Response received when registering a plugin +#[derive(Debug)] +pub struct PluginAddAuthenticatorResponse { + inner: NonNull, +} + +impl PluginAddAuthenticatorResponse { + pub fn plugin_operation_signing_key(&self) -> &[u8] { + unsafe { + let p = &*self.inner.as_ptr(); + std::slice::from_raw_parts( + p.plugin_operation_signing_key, + p.plugin_operation_signing_key_byte_count as usize, + ) + } + } +} + +#[doc(hidden)] +impl From> for PluginAddAuthenticatorResponse { + fn from(value: NonNull) -> Self { + Self { inner: value } + } +} + +impl Drop for PluginAddAuthenticatorResponse { + fn drop(&mut self) { + unsafe { + // SAFETY: This should only fail if: + // - we cannot load the webauthn.dll, which we already have if we have constructed this type, or + // - we spelled the function wrong, which is a library error. + webauthn_plugin_free_add_authenticator_response(self.inner.as_mut()) + .expect("function to load properly"); + } + } +} + +webauthn_call!("WebAuthNPluginAddAuthenticator" as +fn webauthn_plugin_add_authenticator( + pPluginAddAuthenticatorOptions: *const WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS, + ppPluginAddAuthenticatorResponse: *mut *mut WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE +) -> HRESULT); + +webauthn_call!("WebAuthNPluginFreeAddAuthenticatorResponse" as +fn webauthn_plugin_free_add_authenticator_response( + pPluginAddAuthenticatorOptions: *mut WebAuthnPluginAddAuthenticatorResponse +) -> ()); + +// Credential syncing types + +/// Represents a credential. +/// Header File Name: _WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS +/// Header File Usage: WebAuthNPluginAuthenticatorAddCredentials, etc. +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(super) struct WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS { + pub credential_id_byte_count: u32, + pub credential_id_pointer: *const u8, // Changed to const in stable + pub rpid: *const u16, // Changed to const (LPCWSTR) + pub rp_friendly_name: *const u16, // Changed to const (LPCWSTR) + pub user_id_byte_count: u32, + pub user_id_pointer: *const u8, // Changed to const + pub user_name: *const u16, // Changed to const (LPCWSTR) + pub user_display_name: *const u16, // Changed to const (LPCWSTR) +} + +/// Credential metadata to sync to Windows Hello credential autofill list. +#[derive(Debug)] +pub struct PluginCredentialDetails { + /// Credential ID. + pub credential_id: CredentialId, + + /// Relying party ID. + pub rp_id: String, + + /// Relying party display name. + pub rp_friendly_name: Option, + + /// User handle. + pub user_id: UserId, + + /// User name. + /// + /// Corresponds to [`name`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialentity-name) field of WebAuthn `PublicKeyCredentialUserEntity`. + pub user_name: String, + + /// User name. + /// + /// Corresponds to [`displayName`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentity-displayname) field of WebAuthn `PublicKeyCredentialUserEntity`. + pub user_display_name: String, +} + +// Stable API function signatures - now use REFCLSID and flat arrays +webauthn_call!("WebAuthNPluginAuthenticatorAddCredentials" as fn webauthn_plugin_authenticator_add_credentials( + rclsid: *const GUID, + cCredentialDetails: u32, + pCredentialDetails: *const WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS +) -> HRESULT); + +webauthn_call!("WebAuthNPluginAuthenticatorRemoveAllCredentials" as fn webauthn_plugin_authenticator_remove_all_credentials( + rclsid: *const GUID +) -> HRESULT); + +#[repr(C)] +#[derive(Debug)] +pub(super) struct WEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST { + /// Windows handle of the top-level window displayed by the plugin and + /// currently is in foreground as part of the ongoing WebAuthn operation. + pub(super) hwnd: HWND, + + /// The WebAuthn transaction id from the WEBAUTHN_PLUGIN_OPERATION_REQUEST + pub(super) rguidTransactionId: *const GUID, + + /// The username attached to the credential that is in use for this WebAuthn + /// operation. + pub(super) pwszUsername: *const u16, + + /// A text hint displayed on the Windows Hello prompt. + pub(super) pwszDisplayHint: *const u16, +} + +#[derive(Debug)] +pub struct PluginUserVerificationRequest { + /// Windows handle of the top-level window displayed by the plugin and + /// currently is in foreground as part of the ongoing WebAuthn operation. + pub window_handle: HWND, + + /// The WebAuthn transaction id from the WEBAUTHN_PLUGIN_OPERATION_REQUEST + pub transaction_id: GUID, + + /// The username attached to the credential that is in use for this WebAuthn + /// operation. + pub user_name: String, + + /// A text hint displayed on the Windows Hello prompt. + pub display_hint: Option, +} + +/// Response details from user verification. +pub struct PluginUserVerificationResponse { + pub transaction_id: GUID, + /// Bytes of the signature over the response. + pub signature: Vec, +} + +webauthn_call!("WebAuthNPluginPerformUserVerification" as fn webauthn_plugin_perform_user_verification( + pPluginUserVerification: *const WEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST, + pcbResponse: *mut u32, + ppbResponse: *mut *mut u8 +) -> HRESULT); + +webauthn_call!("WebAuthNPluginFreeUserVerificationResponse" as fn webauthn_plugin_free_user_verification_response( + pbResponse: *mut u8 +) -> ()); + +// Plugin Authenticator types + +/// Used when creating and asserting credentials. +/// Header File Name: _WEBAUTHN_PLUGIN_OPERATION_REQUEST +/// Header File Usage: MakeCredential() +/// GetAssertion() +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(super) struct WEBAUTHN_PLUGIN_OPERATION_REQUEST { + /// Window handle to client that requesting a WebAuthn credential. + pub hWnd: HWND, + pub transactionId: GUID, + pub cbRequestSignature: u32, + /// Signature over request made with the signing key created during authenticator registration. + pub pbRequestSignature: *mut u8, + pub requestType: WebAuthnPluginRequestType, + pub cbEncodedRequest: u32, + pub pbEncodedRequest: *const u8, +} + +/// Used as a response when creating and asserting credentials. +/// Header File Name: _WEBAUTHN_PLUGIN_OPERATION_RESPONSE +/// Header File Usage: MakeCredential() +/// GetAssertion() +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(crate) struct WEBAUTHN_PLUGIN_OPERATION_RESPONSE { + pub cbEncodedResponse: u32, + pub pbEncodedResponse: *mut u8, +} + +/// Plugin request type enum as defined in the IDL +#[repr(u32)] +#[derive(Debug, Copy, Clone)] +pub enum WebAuthnPluginRequestType { + CTAP2_CBOR = 0x01, +} + +// MakeCredential types + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(super) struct 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, + pub pAuthenticatorOptions: *const WebAuthnCtapCborAuthenticatorOptions, + // Add other fields as needed... +} + +#[derive(Debug)] +pub struct PluginMakeCredentialRequest { + inner: *const WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST, + pub window_handle: HWND, + pub transaction_id: GUID, + pub request_signature: Vec, +} + +impl PluginMakeCredentialRequest { + pub fn client_data_hash(&self) -> Result<&[u8], WinWebAuthnError> { + if self.as_ref().cbClientDataHash == 0 || self.as_ref().pbClientDataHash.is_null() { + return Err(WinWebAuthnError::new( + ErrorKind::WindowsInternal, + "Received invalid client data hash", + )); + } + unsafe { + Ok(std::slice::from_raw_parts( + self.as_ref().pbClientDataHash, + self.as_ref().cbClientDataHash as usize, + )) + } + } + + pub fn rp_information(&self) -> Option<&WEBAUTHN_RP_ENTITY_INFORMATION> { + let ptr = self.as_ref().pRpInformation; + if ptr.is_null() { + return None; + } + unsafe { Some(&*ptr) } + } + + pub fn user_information(&self) -> Option<&WEBAUTHN_USER_ENTITY_INFORMATION> { + let ptr = self.as_ref().pUserInformation; + if ptr.is_null() { + return None; + } + unsafe { Some(&*ptr) } + } + + pub fn pub_key_cred_params(&self) -> WEBAUTHN_COSE_CREDENTIAL_PARAMETERS { + self.as_ref().WebAuthNCredentialParameters + } + + pub fn exclude_credentials(&self) -> CredentialList { + self.as_ref().CredentialList + } + + /// CTAP CBOR extensions map + pub fn extensions(&self) -> Option<&[u8]> { + let (len, ptr) = ( + self.as_ref().cbCborExtensionsMap, + self.as_ref().pbCborExtensionsMap, + ); + if len == 0 || ptr.is_null() { + return None; + } + unsafe { Some(std::slice::from_raw_parts(ptr, len as usize)) } + } + + pub fn authenticator_options(&self) -> Option { + let ptr = self.as_ref().pAuthenticatorOptions; + if ptr.is_null() { + return None; + } + unsafe { Some(*ptr) } + } +} + +impl AsRef for PluginMakeCredentialRequest { + fn as_ref(&self) -> &WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST { + unsafe { &*self.inner } + } +} + +impl Drop for PluginMakeCredentialRequest { + fn drop(&mut self) { + if !self.inner.is_null() { + // leak memory if we cannot find the free function + _ = webauthn_free_decoded_make_credential_request( + self.inner as *mut WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST, + ); + } + } +} + +impl TryFrom> for PluginMakeCredentialRequest { + type Error = WinWebAuthnError; + + fn try_from(value: NonNull) -> Result { + unsafe { + let request = value.as_ref(); + if !matches!(request.requestType, WebAuthnPluginRequestType::CTAP2_CBOR) { + return Err(WinWebAuthnError::new( + ErrorKind::Serialization, + "Unknown plugin operation request type", + )); + } + let mut registration_request = MaybeUninit::uninit(); + webauthn_decode_make_credential_request( + request.cbEncodedRequest, + request.pbEncodedRequest, + registration_request.as_mut_ptr(), + )? + .ok() + .map_err(|err| { + WinWebAuthnError::with_cause( + ErrorKind::WindowsInternal, + "Failed to decode get assertion request", + err, + ) + })?; + + let registration_request = registration_request.assume_init(); + Ok(Self { + inner: registration_request as *const WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST, + window_handle: request.hWnd, + transaction_id: request.transactionId, + request_signature: std::slice::from_raw_parts( + request.pbRequestSignature, + request.cbEncodedRequest as usize, + ) + .to_vec(), + }) + } + } +} + +// Windows API function signatures for decoding make credential requests +webauthn_call!("WebAuthNDecodeMakeCredentialRequest" as fn webauthn_decode_make_credential_request( + cbEncoded: u32, + pbEncoded: *const u8, + ppMakeCredentialRequest: *mut *mut WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST +) -> HRESULT); + +webauthn_call!("WebAuthNFreeDecodedMakeCredentialRequest" as fn webauthn_free_decoded_make_credential_request( + pMakeCredentialRequest: *mut WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST +) -> ()); + +pub struct PluginMakeCredentialResponse { + /// Attestation format type + pub format_type: String, // PCWSTR + + /// Authenticator data that was created for this credential. + pub authenticator_data: Vec, + + ///Encoded CBOR attestation information + pub attestation_statement: Option>, + + // dwAttestationDecodeType: u32, + /// Following depends on the dwAttestationDecodeType + /// WEBAUTHN_ATTESTATION_DECODE_NONE + /// NULL - not able to decode the CBOR attestation information + /// WEBAUTHN_ATTESTATION_DECODE_COMMON + /// PWEBAUTHN_COMMON_ATTESTATION; + // pub pvAttestationDecode: *mut u8, + + /// The CBOR-encoded Attestation Object to be returned to the RP. + pub attestation_object: Option>, + + /// The CredentialId bytes extracted from the Authenticator Data. + /// Used by Edge to return to the RP. + pub credential_id: Option>, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_2 + // + /// Since VERSION 2 + pub extensions: Option>, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_3 + // + /// One of the WEBAUTHN_CTAP_TRANSPORT_* bits will be set corresponding to + /// the transport that was used. + pub used_transport: CtapTransport, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_4 + // + pub ep_att: bool, + pub large_blob_supported: bool, + pub resident_key: bool, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_5 + // + pub prf_enabled: bool, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_6 + // + pub unsigned_extension_outputs: Option>, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_7 + // + pub hmac_secret: Option, + + /// ThirdPartyPayment Credential or not. + pub third_party_payment: bool, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_8 + // + /// Multiple WEBAUTHN_CTAP_TRANSPORT_* bits will be set corresponding to + /// the transports that are supported. + pub transports: Option>, + + /// UTF-8 encoded JSON serialization of the client data. + pub client_data_json: Option>, + + /// UTF-8 encoded JSON serialization of the RegistrationResponse. + pub registration_response_json: Option>, +} + +impl PluginMakeCredentialResponse { + pub fn to_ctap_response(self) -> Result, WinWebAuthnError> { + let attestation = self.try_into()?; + let mut response_len = 0; + let mut response_ptr = std::ptr::null_mut(); + webauthn_encode_make_credential_response( + &attestation, + &mut response_len, + &mut response_ptr, + )? + .ok() + .map_err(|err| { + WinWebAuthnError::with_cause( + ErrorKind::WindowsInternal, + "WebAuthNEncodeMakeCredentialResponse() failed", + err, + ) + })?; + + if response_ptr.is_null() { + return Err(WinWebAuthnError::new( + ErrorKind::WindowsInternal, + "Received null pointer from WebAuthNEncodeMakeCredentialResponse", + )); + } + // SAFETY: Windows returned successful response code, so we assume that the pointer and length are valid. + let response = + unsafe { std::slice::from_raw_parts(response_ptr, response_len as usize).to_vec() }; + + Ok(response) + } +} + +impl TryFrom for WEBAUTHN_CREDENTIAL_ATTESTATION { + type Error = WinWebAuthnError; + + fn try_from(value: PluginMakeCredentialResponse) -> Result { + // Convert format type to UTF-16 + let format_type_utf16 = value.format_type.to_utf16(); + let pwszFormatType = format_type_utf16.as_ptr(); + std::mem::forget(format_type_utf16); + + // Get authenticator data pointer and length + let pbAuthenticatorData = value.authenticator_data.as_ptr(); + let cbAuthenticatorData = value.authenticator_data.len() as u32; + std::mem::forget(value.authenticator_data); + + // Get optional attestation statement pointer and length + let (pbAttestation, cbAttestation) = match value.attestation_statement.as_ref() { + Some(data) => (data.as_ptr(), data.len() as u32), + None => (std::ptr::null(), 0), + }; + std::mem::forget(value.attestation_statement); + + // Get optional attestation object pointer and length + let (pbAttestationObject, cbAttestationObject) = match value.attestation_object.as_ref() { + Some(data) => (data.as_ptr(), data.len() as u32), + None => (std::ptr::null(), 0), + }; + std::mem::forget(value.attestation_object); + + // Get optional credential ID pointer and length + let (pbCredentialId, cbCredentialId) = match value.credential_id.as_ref() { + Some(data) => (data.as_ptr(), data.len() as u32), + None => (std::ptr::null(), 0), + }; + std::mem::forget(value.credential_id); + + // Convert extensions (TODO: implement proper extension conversion) + let extensions = WEBAUTHN_EXTENSIONS { + cExtensions: 0, + pExtensions: std::ptr::null(), + }; + + // Convert used transport enum to bitmask + let dwUsedTransport = value.used_transport as u32; + + // Get optional unsigned extension outputs pointer and length + let (pbUnsignedExtensionOutputs, cbUnsignedExtensionOutputs) = + match value.unsigned_extension_outputs.as_ref() { + Some(data) => (data.as_ptr(), data.len() as u32), + None => (std::ptr::null(), 0), + }; + std::mem::forget(value.unsigned_extension_outputs); + + // Convert optional HMAC secret (TODO: implement proper conversion) + let pHmacSecret = std::ptr::null(); + + // Convert optional transports to bitmask + let dwTransports = value + .transports + .as_ref() + .map_or(0, |t| t.iter().map(|transport| *transport as u32).sum()); + + // Get optional client data JSON pointer and length + let (pbClientDataJSON, cbClientDataJSON) = match value.client_data_json.as_ref() { + Some(data) => (data.as_ptr(), data.len() as u32), + None => (std::ptr::null(), 0), + }; + std::mem::forget(value.client_data_json); + + // Get optional registration response JSON pointer and length + let (pbRegistrationResponseJSON, cbRegistrationResponseJSON) = + match value.registration_response_json.as_ref() { + Some(data) => (data.as_ptr(), data.len() as u32), + None => (std::ptr::null(), 0), + }; + std::mem::forget(value.registration_response_json); + + let attestation = WEBAUTHN_CREDENTIAL_ATTESTATION { + // Use version 8 to include all fields + dwVersion: 8, + pwszFormatType, + cbAuthenticatorData, + pbAuthenticatorData, + cbAttestation, + pbAttestation, + // TODO: Support decode type. Just using WEBAUTHN_ATTESTATION_DECODE_NONE (0) for now. + dwAttestationDecodeType: 0, + pvAttestationDecode: std::ptr::null(), + cbAttestationObject, + pbAttestationObject, + cbCredentialId, + pbCredentialId, + Extensions: extensions, + dwUsedTransport, + bEpAtt: value.ep_att, + bLargeBlobSupported: value.large_blob_supported, + bResidentKey: value.resident_key, + bPrfEnabled: value.prf_enabled, + cbUnsignedExtensionOutputs, + pbUnsignedExtensionOutputs, + pHmacSecret, + bThirdPartyPayment: value.third_party_payment, + dwTransports, + cbClientDataJSON, + pbClientDataJSON, + cbRegistrationResponseJSON, + pbRegistrationResponseJSON, + }; + Ok(attestation) + } +} + +webauthn_call!("WebAuthNEncodeMakeCredentialResponse" as fn webauthn_encode_make_credential_response( + cbEncoded: *const WEBAUTHN_CREDENTIAL_ATTESTATION, + pbEncoded: *mut u32, + response_bytes: *mut *mut u8 +) -> HRESULT); + +// GetAssertion types + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(super) struct 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, + pub pAuthenticatorOptions: *const WebAuthnCtapCborAuthenticatorOptions, + // Add other fields as needed... +} + +#[derive(Debug)] +pub struct PluginGetAssertionRequest { + inner: *const WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST, + pub window_handle: HWND, + pub transaction_id: GUID, + pub request_signature: Vec, +} + +impl PluginGetAssertionRequest { + pub fn rp_id(&self) -> &str { + unsafe { + let request = &*self.inner; + let slice = std::slice::from_raw_parts(request.pbRpId, request.cbRpId as usize); + str::from_utf8_unchecked(slice) + } + } + + pub fn client_data_hash(&self) -> &[u8] { + let inner = self.as_ref(); + // SAFETY: Verified by Windows + unsafe { + std::slice::from_raw_parts(inner.pbClientDataHash, inner.cbClientDataHash as usize) + } + } + + pub fn allow_credentials(&self) -> CredentialList { + self.as_ref().CredentialList + } + + // TODO: Support extensions + // pub fn extensions(&self) -> Options {} + + pub fn authenticator_options(&self) -> Option { + let ptr = self.as_ref().pAuthenticatorOptions; + if ptr.is_null() { + return None; + } + unsafe { Some(*ptr) } + } +} + +impl AsRef for PluginGetAssertionRequest { + fn as_ref(&self) -> &WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST { + unsafe { &*self.inner } + } +} + +impl Drop for PluginGetAssertionRequest { + fn drop(&mut self) { + if !self.inner.is_null() { + // leak memory if we cannot find the free function + _ = webauthn_free_decoded_get_assertion_request( + self.inner as *mut WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST, + ); + } + } +} + +impl TryFrom> for PluginGetAssertionRequest { + type Error = WinWebAuthnError; + + fn try_from(value: NonNull) -> Result { + unsafe { + let request = value.as_ref(); + if !matches!(request.requestType, WebAuthnPluginRequestType::CTAP2_CBOR) { + return Err(WinWebAuthnError::new( + ErrorKind::Serialization, + "Unknown plugin operation request type", + )); + } + let mut assertion_request: *mut WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST = + std::ptr::null_mut(); + webauthn_decode_get_assertion_request( + request.cbEncodedRequest, + request.pbEncodedRequest, + &mut assertion_request, + )? + .ok() + .map_err(|err| { + WinWebAuthnError::with_cause( + ErrorKind::WindowsInternal, + "Failed to decode get assertion request", + err, + ) + })?; + Ok(Self { + inner: assertion_request as *const WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST, + window_handle: request.hWnd, + transaction_id: request.transactionId, + request_signature: std::slice::from_raw_parts( + request.pbRequestSignature, + request.cbEncodedRequest as usize, + ) + .to_vec(), + }) + } + } +} +// Windows API function signatures for decoding get assertion requests +webauthn_call!("WebAuthNDecodeGetAssertionRequest" as fn webauthn_decode_get_assertion_request( + cbEncoded: u32, + pbEncoded: *const u8, + ppGetAssertionRequest: *mut *mut WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST +) -> HRESULT); + +webauthn_call!("WebAuthNFreeDecodedGetAssertionRequest" as fn webauthn_free_decoded_get_assertion_request( + pGetAssertionRequest: *mut WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST +) -> ()); + +// CancelOperation Types +pub(super) struct WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST { + transactionId: GUID, + cbRequestSignature: u32, + pbRequestSignature: *const u8, +} + +pub struct PluginCancelOperationRequest { + inner: NonNull, +} + +impl PluginCancelOperationRequest { + /// Request transaction ID + pub fn transaction_id(&self) -> GUID { + self.as_ref().transactionId + } + + /// Request signature. + pub fn request_signature(&self) -> &[u8] { + unsafe { + std::slice::from_raw_parts( + self.as_ref().pbRequestSignature, + self.as_ref().cbRequestSignature as usize, + ) + } + } +} + +impl AsRef for PluginCancelOperationRequest { + fn as_ref(&self) -> &WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST { + // SAFETY: Pointer is received from Windows so we assume it is correct. + unsafe { self.inner.as_ref() } + } +} + +#[doc(hidden)] +impl From> for PluginCancelOperationRequest { + fn from(value: NonNull) -> Self { + Self { inner: value } + } +} + +/// Plugin lock status enum as defined in the IDL +#[repr(u32)] +#[derive(Debug, Copy, Clone)] +pub enum PluginLockStatus { + PluginLocked = 0, + PluginUnlocked = 1, +} diff --git a/apps/desktop/desktop_native/win_webauthn/src/types/mod.rs b/apps/desktop/desktop_native/win_webauthn/src/types/mod.rs new file mode 100644 index 00000000000..103f657b7e8 --- /dev/null +++ b/apps/desktop/desktop_native/win_webauthn/src/types/mod.rs @@ -0,0 +1,777 @@ +//! Types and functions defined in the Windows WebAuthn API. + +#![allow(non_snake_case)] +#![allow(non_camel_case_types)] + +use std::{collections::HashSet, fmt::Display, ptr::NonNull}; + +use ciborium::Value; +use windows_core::PCWSTR; + +use crate::{util::ArrayPointerIterator, ErrorKind, WinWebAuthnError}; + +/// List of its supported protocol versions and extensions, its AAGUID, and +/// other aspects of its overall capabilities. +pub struct AuthenticatorInfo { + /// List of supported versions. + pub versions: HashSet, + + /// The claimed AAGUID. 16 bytes in length and encoded the same as + /// MakeCredential AuthenticatorData, as specified in [WebAuthn](https://www.w3.org/TR/webauthn-3/#aaguid). + /// + /// Note: even though the name has "guid" in it, this is actually an RFC 4122 + /// UUID, which is deserialized differently than a Windows GUID. + pub aaguid: Uuid, + + /// List of supported options. + pub options: Option>, + + /// List of supported transports. Values are taken from the + /// [AuthenticatorTransport enum in WebAuthn][authenticator-transport]. + /// The list MUST NOT include duplicate values nor be empty if present. + /// Platforms MUST tolerate unknown values. + /// [authenticator-transport]: https://www.w3.org/TR/webauthn-3/#enum-transport + pub transports: Option>, + + /// List of supported algorithms for credential generation, as specified in + /// [WebAuthn]. The array is ordered from most preferred to least preferred + /// and MUST NOT include duplicate entries nor be empty if present. + /// PublicKeyCredentialParameters' algorithm identifiers are values that + /// SHOULD be registered in the IANA COSE Algorithms registry + /// [IANA-COSE-ALGS-REG]. + pub algorithms: Option>, +} + +impl AuthenticatorInfo { + pub fn as_ctap_bytes(&self) -> Result, super::WinWebAuthnError> { + // Create the authenticator info map according to CTAP2 spec + // Using Vec<(Value, Value)> because that's what ciborium::Value::Map expects + let mut authenticator_info = Vec::new(); + + // 1: versions - Array of supported FIDO versions + let versions = self + .versions + .iter() + .map(|v| Value::Text(v.into())) + .collect(); + authenticator_info.push((Value::Integer(1.into()), Value::Array(versions))); + + // 2: extensions - Array of supported extensions (empty for now) + authenticator_info.push((Value::Integer(2.into()), Value::Array(vec![]))); + + // 3: aaguid - 16-byte AAGUID + authenticator_info.push(( + Value::Integer(3.into()), + Value::Bytes(self.aaguid.0.to_vec()), + )); + + // 4: options - Map of supported options + if let Some(options) = &self.options { + let options = options + .iter() + .map(|o| (Value::Text(o.into()), Value::Bool(true))) + .collect(); + authenticator_info.push((Value::Integer(4.into()), Value::Map(options))); + } + + // 9: transports - Array of supported transports + if let Some(transports) = &self.transports { + let transports = transports.iter().map(|t| Value::Text(t.clone())).collect(); + authenticator_info.push((Value::Integer(9.into()), Value::Array(transports))); + } + + // 10: algorithms - Array of supported algorithms + if let Some(algorithms) = &self.algorithms { + let algorithms: Vec = algorithms + .iter() + .map(|a| { + Value::Map(vec![ + (Value::Text("alg".to_string()), Value::Integer(a.alg.into())), + (Value::Text("type".to_string()), Value::Text(a.typ.clone())), + ]) + }) + .collect(); + authenticator_info.push((Value::Integer(10.into()), Value::Array(algorithms))); + } + + // Encode to CBOR + let mut buffer = Vec::new(); + ciborium::ser::into_writer(&Value::Map(authenticator_info), &mut buffer).map_err(|e| { + WinWebAuthnError::with_cause( + ErrorKind::Serialization, + "Failed to serialize authenticator info into CBOR", + e, + ) + })?; + + Ok(buffer) + } +} + +// A UUID is not the same as a Windows GUID +/// An RFC4122 UUID. +pub struct Uuid([u8; 16]); + +impl TryFrom<&str> for Uuid { + type Error = WinWebAuthnError; + + fn try_from(value: &str) -> Result { + let uuid_clean = value.replace("-", "").replace("{", "").replace("}", ""); + if uuid_clean.len() != 32 { + return Err(WinWebAuthnError::new( + ErrorKind::Serialization, + "Invalid UUID format", + )); + } + + let bytes = uuid_clean + .chars() + .collect::>() + .chunks(2) + .map(|chunk| { + let hex_str: String = chunk.iter().collect(); + u8::from_str_radix(&hex_str, 16).map_err(|_| { + WinWebAuthnError::new( + ErrorKind::Serialization, + &format!("Invalid hex character in UUID: {}", hex_str), + ) + }) + }) + .collect::, WinWebAuthnError>>()?; + + // SAFETY: We already checked the length of the string before, so this should result in the correct number of bytes. + let b: [u8; 16] = bytes.try_into().expect("16 bytes to be parsed"); + Ok(Uuid(b)) + } +} + +#[derive(Hash, Eq, PartialEq)] +pub enum CtapVersion { + Fido2_0, + Fido2_1, +} + +pub struct PublicKeyCredentialParameters { + pub alg: i32, + pub typ: String, +} + +impl From<&CtapVersion> for String { + fn from(value: &CtapVersion) -> Self { + match value { + CtapVersion::Fido2_0 => "FIDO_2_0", + CtapVersion::Fido2_1 => "FIDO_2_1", + } + .to_string() + } +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct WEBAUTHN_RP_ENTITY_INFORMATION { + dwVersion: u32, + pwszId: *const u16, // PCWSTR + pwszName: *const u16, // PCWSTR + pwszIcon: *const u16, // PCWSTR +} + +impl WEBAUTHN_RP_ENTITY_INFORMATION { + /// Relying party ID. + pub fn id(&self) -> Result { + if self.pwszId.is_null() { + return Err(WinWebAuthnError::new( + ErrorKind::WindowsInternal, + "Received invalid RP ID", + )); + } + unsafe { + PCWSTR(self.pwszId).to_string().map_err(|err| { + WinWebAuthnError::with_cause(ErrorKind::WindowsInternal, "Invalid RP ID", err) + }) + } + } + + /// Relying party name. + pub fn name(&self) -> Result { + if self.pwszName.is_null() { + return Err(WinWebAuthnError::new( + ErrorKind::WindowsInternal, + "Received invalid RP name", + )); + } + unsafe { + PCWSTR(self.pwszName).to_string().map_err(|err| { + WinWebAuthnError::with_cause(ErrorKind::WindowsInternal, "Invalid RP name", err) + }) + } + } +} + +#[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 +} + +impl WEBAUTHN_USER_ENTITY_INFORMATION { + /// User handle. + pub fn id(&self) -> Result<&[u8], WinWebAuthnError> { + if self.cbId == 0 || self.pbId.is_null() { + return Err(WinWebAuthnError::new( + ErrorKind::WindowsInternal, + "Received invalid user ID", + )); + } + unsafe { Ok(std::slice::from_raw_parts(self.pbId, self.cbId as usize)) } + } + + /// User name. + pub fn name(&self) -> Result { + if self.pwszName.is_null() { + return Err(WinWebAuthnError::new( + ErrorKind::WindowsInternal, + "Received invalid user name", + )); + } + unsafe { + PCWSTR(self.pwszName).to_string().map_err(|err| { + WinWebAuthnError::with_cause(ErrorKind::WindowsInternal, "Invalid user name", err) + }) + } + } + + /// User display name. + pub fn display_name(&self) -> Result { + if self.pwszDisplayName.is_null() { + return Err(WinWebAuthnError::new( + ErrorKind::WindowsInternal, + "Received invalid user name", + )); + } + unsafe { + PCWSTR(self.pwszDisplayName).to_string().map_err(|err| { + WinWebAuthnError::with_cause( + ErrorKind::WindowsInternal, + "Invalid user display name", + err, + ) + }) + } + } +} +#[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 +} + +impl WEBAUTHN_COSE_CREDENTIAL_PARAMETER { + pub fn credential_type(&self) -> Result { + if self.pwszCredentialType.is_null() { + return Err(WinWebAuthnError::new( + ErrorKind::WindowsInternal, + "Invalid credential type", + )); + } + unsafe { + PCWSTR(self.pwszCredentialType).to_string().map_err(|err| { + WinWebAuthnError::with_cause( + ErrorKind::WindowsInternal, + "Invalid credential type", + err, + ) + }) + } + } + pub fn alg(&self) -> i32 { + self.lAlg + } +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct WEBAUTHN_COSE_CREDENTIAL_PARAMETERS { + cCredentialParameters: u32, + pCredentialParameters: *const WEBAUTHN_COSE_CREDENTIAL_PARAMETER, +} + +impl WEBAUTHN_COSE_CREDENTIAL_PARAMETERS { + pub fn iter(&self) -> ArrayPointerIterator<'_, WEBAUTHN_COSE_CREDENTIAL_PARAMETER> { + unsafe { + ArrayPointerIterator::new( + self.pCredentialParameters, + self.cCredentialParameters as usize, + ) + } + } +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(crate) struct WEBAUTHN_CREDENTIAL_ATTESTATION { + /// Version of this structure, to allow for modifications in the future. + pub(crate) dwVersion: u32, + + /// Attestation format type + pub(crate) pwszFormatType: *const u16, // PCWSTR + + /// Size of cbAuthenticatorData. + pub(crate) cbAuthenticatorData: u32, + /// Authenticator data that was created for this credential. + //_Field_size_bytes_(cbAuthenticatorData) + pub(crate) pbAuthenticatorData: *const u8, + + /// Size of CBOR encoded attestation information + /// 0 => encoded as CBOR null value. + pub(crate) cbAttestation: u32, + ///Encoded CBOR attestation information + // _Field_size_bytes_(cbAttestation) + pub(crate) pbAttestation: *const u8, + + pub(crate) dwAttestationDecodeType: u32, + /// Following depends on the dwAttestationDecodeType + /// WEBAUTHN_ATTESTATION_DECODE_NONE + /// NULL - not able to decode the CBOR attestation information + /// WEBAUTHN_ATTESTATION_DECODE_COMMON + /// PWEBAUTHN_COMMON_ATTESTATION; + pub(crate) pvAttestationDecode: *const u8, + + /// The CBOR encoded Attestation Object to be returned to the RP. + pub(crate) cbAttestationObject: u32, + // _Field_size_bytes_(cbAttestationObject) + pub(crate) pbAttestationObject: *const u8, + + /// The CredentialId bytes extracted from the Authenticator Data. + /// Used by Edge to return to the RP. + pub(crate) cbCredentialId: u32, + // _Field_size_bytes_(cbCredentialId) + pub(crate) pbCredentialId: *const u8, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_2 + // + /// Since VERSION 2 + pub(crate) Extensions: WEBAUTHN_EXTENSIONS, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_3 + // + /// One of the WEBAUTHN_CTAP_TRANSPORT_* bits will be set corresponding to + /// the transport that was used. + pub(crate) dwUsedTransport: u32, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_4 + // + pub(crate) bEpAtt: bool, + pub(crate) bLargeBlobSupported: bool, + pub(crate) bResidentKey: bool, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_5 + // + pub(crate) bPrfEnabled: bool, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_6 + // + pub(crate) cbUnsignedExtensionOutputs: u32, + // _Field_size_bytes_(cbUnsignedExtensionOutputs) + pub(crate) pbUnsignedExtensionOutputs: *const u8, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_7 + // + pub(crate) pHmacSecret: *const WEBAUTHN_HMAC_SECRET_SALT, + + // ThirdPartyPayment Credential or not. + pub(crate) bThirdPartyPayment: bool, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_8 + // + + // Multiple WEBAUTHN_CTAP_TRANSPORT_* bits will be set corresponding to + // the transports that are supported. + pub(crate) dwTransports: u32, + + // UTF-8 encoded JSON serialization of the client data. + pub(crate) cbClientDataJSON: u32, + // _Field_size_bytes_(cbClientDataJSON) + pub(crate) pbClientDataJSON: *const u8, + + // UTF-8 encoded JSON serialization of the RegistrationResponse. + pub(crate) cbRegistrationResponseJSON: u32, + // _Field_size_bytes_(cbRegistrationResponseJSON) + pub(crate) pbRegistrationResponseJSON: *const u8, +} + +pub enum AttestationFormat { + Packed, + Tpm, + AndroidKey, + FidoU2f, + None, + Compound, + Apple, +} + +impl Display for AttestationFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Packed => "packed", + Self::Tpm => "tpm", + Self::AndroidKey => "android-key", + Self::FidoU2f => "fido-u2f", + Self::None => "none", + Self::Compound => "compound", + Self::Apple => "apple", + }) + } +} + +pub enum AttestationDecodeType { + None, + Common(), +} + +pub(crate) struct WEBAUTHN_HMAC_SECRET_SALT { + /// Size of pbFirst. + cbFirst: u32, + // _Field_size_bytes_(cbFirst) + /// Required + pbFirst: *mut u8, + + /// Size of pbSecond. + cbSecond: u32, + // _Field_size_bytes_(cbSecond) + pbSecond: *mut u8, +} + +pub struct HmacSecretSalt { + first: Vec, + second: Option>, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(crate) struct WEBAUTHN_EXTENSION { + pwszExtensionIdentifier: *const u16, + cbExtension: u32, + pvExtension: *mut u8, +} + +pub enum CredProtectOutput { + UserVerificationAny, + UserVerificationOptional, + UserVerificationOptionalWithCredentialIdList, + UserVerificationRequired, +} +pub enum WebAuthnExtensionMakeCredentialOutput { + HmacSecret(bool), + CredProtect(CredProtectOutput), + CredBlob(bool), + MinPinLength(u32), + // LargeBlob, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(crate) struct WEBAUTHN_EXTENSIONS { + pub(crate) cExtensions: u32, + // _Field_size_(cExtensions) + pub(crate) pExtensions: *const WEBAUTHN_EXTENSION, +} + +#[derive(Debug)] +pub struct UserId(Vec); + +impl UserId { + pub fn len(&self) -> u8 { + // SAFETY: User ID guaranteed to be <= 64 bytes + self.0.len() as u8 + } +} +impl AsRef<[u8]> for UserId { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl TryFrom> for UserId { + type Error = WinWebAuthnError; + + fn try_from(value: Vec) -> Result { + if value.len() > 64 { + return Err(WinWebAuthnError::new( + ErrorKind::Serialization, + &format!( + "User ID exceeds maximum length of 64, received {}", + value.len() + ), + )); + } + Ok(UserId(value)) + } +} + +#[derive(Debug)] +pub struct CredentialId(Vec); + +impl CredentialId { + pub fn len(&self) -> u16 { + // SAFETY: CredentialId guaranteed to be < 1024 bytes + self.0.len() as u16 + } +} + +impl AsRef<[u8]> for CredentialId { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl TryFrom> for CredentialId { + type Error = WinWebAuthnError; + + fn try_from(value: Vec) -> Result { + if value.len() > 1023 { + return Err(WinWebAuthnError::new( + ErrorKind::Serialization, + &format!( + "Credential ID exceeds maximum length of 1023, received {}", + value.len() + ), + )); + } + Ok(CredentialId(value)) + } +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct WEBAUTHN_CREDENTIAL_EX { + dwVersion: u32, + cbId: u32, + pbId: *const u8, + pwszCredentialType: *const u16, // LPCWSTR + dwTransports: u32, +} + +impl WEBAUTHN_CREDENTIAL_EX { + pub fn credential_id(&self) -> Option<&[u8]> { + if self.cbId == 0 || self.pbId.is_null() { + None + } else { + unsafe { Some(std::slice::from_raw_parts(self.pbId, self.cbId as usize)) } + } + } + + pub fn credential_type(&self) -> Result { + if self.pwszCredentialType.is_null() { + return Err(WinWebAuthnError::new( + ErrorKind::WindowsInternal, + "Received invalid credential ID", + )); + } + unsafe { + PCWSTR(self.pwszCredentialType).to_string().map_err(|err| { + WinWebAuthnError::with_cause( + ErrorKind::WindowsInternal, + "Invalid credential ID", + err, + ) + }) + } + } + + pub fn transports(&self) -> Vec { + let mut transports = Vec::new(); + let mut t = self.dwTransports; + if t == 0 { + return transports; + }; + const TRANSPORTS: [CtapTransport; 7] = [ + CtapTransport::Usb, + CtapTransport::Nfc, + CtapTransport::Ble, + CtapTransport::Test, + CtapTransport::Internal, + CtapTransport::Hybrid, + CtapTransport::SmartCard, + ]; + for a in TRANSPORTS { + if t == 0 { + break; + } + if a as u32 & t > 0 { + transports.push(a.clone()); + t -= a as u32; + } + } + transports + } +} + +pub struct CredentialEx { + inner: NonNull, +} + +impl AsRef for CredentialEx { + fn as_ref(&self) -> &WEBAUTHN_CREDENTIAL_EX { + // SAFETY: We initialize memory manually in constructors. + unsafe { self.inner.as_ref() } + } +} + +impl From> for CredentialEx { + fn from(value: NonNull) -> Self { + Self { inner: value } + } +} + +#[repr(u32)] +#[derive(Clone, Copy)] +pub enum CtapTransport { + Usb = 1, + Nfc = 2, + Ble = 4, + Test = 8, + Internal = 0x10, + Hybrid = 0x20, + SmartCard = 0x40, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct CredentialList { + pub cCredentials: u32, + pub ppCredentials: *const *const WEBAUTHN_CREDENTIAL_EX, +} + +pub(crate) type WEBAUTHN_CREDENTIAL_LIST = CredentialList; +pub struct CredentialListIterator<'a> { + inner: ArrayPointerIterator<'a, *const WEBAUTHN_CREDENTIAL_EX>, +} + +impl<'a> Iterator for CredentialListIterator<'a> { + type Item = &'a WEBAUTHN_CREDENTIAL_EX; + + fn next(&mut self) -> Option { + let item = self.inner.next()?; + // SAFETY: This type can only be constructed from this library using + // responses from Windows APIs, and we trust that the pointer and length + // of each inner item of the array is valid. + unsafe { item.as_ref() } + } +} + +impl CredentialList { + pub fn iter(&self) -> CredentialListIterator<'_> { + // SAFETY: This type can only be constructed from this library using + // responses from Windows APIs. The pointer is checked for null safety + // on construction. + unsafe { + CredentialListIterator { + inner: ArrayPointerIterator::new(self.ppCredentials, self.cCredentials as usize), + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const AAGUID: &str = "d548826e-79b4-db40-a3d8-11116f7e8349"; + #[test] + fn test_generate_cbor_authenticator_info() { + let aaguid = Uuid::try_from(AAGUID).unwrap(); + let authenticator_info = AuthenticatorInfo { + versions: HashSet::from([CtapVersion::Fido2_0, CtapVersion::Fido2_1]), + aaguid: aaguid, + options: Some(HashSet::from([ + "rk".to_string(), + "up".to_string(), + "uv".to_string(), + ])), + transports: Some(HashSet::from([ + "internal".to_string(), + "hybrid".to_string(), + ])), + algorithms: Some(vec![PublicKeyCredentialParameters { + alg: -7, + typ: "public-key".to_string(), + }]), + }; + let result = authenticator_info.as_ctap_bytes(); + assert!(result.is_ok(), "CBOR generation should succeed"); + + let cbor_bytes = result.unwrap(); + assert!(!cbor_bytes.is_empty(), "CBOR bytes should not be empty"); + + // Verify the CBOR can be decoded back + let decoded: Result = ciborium::de::from_reader(&cbor_bytes[..]); + assert!(decoded.is_ok(), "Generated CBOR should be valid"); + + // Verify it's a map with expected keys + if let Value::Map(map) = decoded.unwrap() { + assert!( + map.iter().any(|(k, _)| k == &Value::Integer(1.into())), + "Should contain versions (key 1)" + ); + assert!( + map.iter().any(|(k, _)| k == &Value::Integer(2.into())), + "Should contain extensions (key 2)" + ); + assert!( + map.iter().any(|(k, _)| k == &Value::Integer(3.into())), + "Should contain aaguid (key 3)" + ); + assert!( + map.iter().any(|(k, _)| k == &Value::Integer(4.into())), + "Should contain options (key 4)" + ); + assert!( + map.iter().any(|(k, _)| k == &Value::Integer(9.into())), + "Should contain transports (key 9)" + ); + assert!( + map.iter().any(|(k, _)| k == &Value::Integer(10.into())), + "Should contain algorithms (key 10)" + ); + } else { + panic!("CBOR should decode to a map"); + } + + // Print the generated CBOR for verification + println!("Generated CBOR hex: {}", hex::encode(&cbor_bytes)); + } + + #[test] + fn test_aaguid_parsing() { + let result = Uuid::try_from(AAGUID); + assert!(result.is_ok(), "AAGUID parsing should succeed"); + + let aaguid_bytes = result.unwrap(); + assert_eq!(aaguid_bytes.0.len(), 16, "AAGUID should be 16 bytes"); + assert_eq!(aaguid_bytes.0[0], 0xd5, "First byte should be 0xd5"); + assert_eq!(aaguid_bytes.0[1], 0x48, "Second byte should be 0x48"); + + // Verify full expected AAGUID + let expected_hex = "d548826e79b4db40a3d811116f7e8349"; + let expected_bytes = hex::decode(expected_hex).unwrap(); + assert_eq!( + &aaguid_bytes.0[..], + expected_bytes, + "AAGUID should match expected value" + ); + } +} diff --git a/apps/desktop/desktop_native/win_webauthn/src/util.rs b/apps/desktop/desktop_native/win_webauthn/src/util.rs new file mode 100644 index 00000000000..eee0a2edc7f --- /dev/null +++ b/apps/desktop/desktop_native/win_webauthn/src/util.rs @@ -0,0 +1,98 @@ +use windows::{ + core::s, + Win32::{ + Foundation::{FreeLibrary, HMODULE}, + System::LibraryLoader::{LoadLibraryExA, LOAD_LIBRARY_SEARCH_SYSTEM32}, + }, +}; + +use crate::{ErrorKind, WinWebAuthnError}; + +macro_rules! webauthn_call { + ($symbol:literal as fn $fn_name:ident($($arg:ident: $arg_type:ty),+) -> $result_type:ty) => ( + pub(super) fn $fn_name($($arg: $arg_type),*) -> Result<$result_type, WinWebAuthnError> { + let library = crate::util::load_webauthn_lib()?; + let response = unsafe { + let address = GetProcAddress(library, s!($symbol)).ok_or( + WinWebAuthnError::new( + ErrorKind::DllLoad, + &format!( + "Failed to load function {}", + $symbol + ), + ), + )?; + + let function: unsafe extern "cdecl" fn( + $($arg: $arg_type),* + ) -> $result_type = std::mem::transmute_copy(&address); + function($($arg),*) + }; + crate::util::free_webauthn_lib(library)?; + Ok(response) + } + ) +} + +pub(crate) use webauthn_call; + +pub(super) fn load_webauthn_lib() -> Result { + unsafe { + LoadLibraryExA(s!("webauthn.dll"), None, LOAD_LIBRARY_SEARCH_SYSTEM32).map_err(|err| { + WinWebAuthnError::with_cause(ErrorKind::DllLoad, "Failed to load webauthn.dll", err) + }) + } +} + +pub(super) fn free_webauthn_lib(library: HMODULE) -> Result<(), WinWebAuthnError> { + unsafe { + FreeLibrary(library).map_err(|err| { + WinWebAuthnError::with_cause( + ErrorKind::WindowsInternal, + "Failed to free webauthn.dll library", + err, + ) + }) + } +} +pub(super) trait WindowsString { + fn to_utf16(&self) -> Vec; +} + +impl WindowsString for str { + fn to_utf16(&self) -> Vec { + // null-terminated UTF-16 + self.encode_utf16().chain(std::iter::once(0)).collect() + } +} + +pub struct ArrayPointerIterator<'a, T> { + pos: usize, + list: Option<&'a [T]>, +} + +impl ArrayPointerIterator<'_, T> { + /// Safety constraints: The caller must ensure that the pointer and length is + /// valid. A null pointer returns an empty iterator. + pub unsafe fn new(data: *const T, len: usize) -> Self { + let slice = if !data.is_null() { + Some(std::slice::from_raw_parts(data, len)) + } else { + None + }; + Self { + pos: 0, + list: slice, + } + } +} + +impl<'a, T> Iterator for ArrayPointerIterator<'a, T> { + type Item = &'a T; + + fn next(&mut self) -> Option { + let current = self.list?.get(self.pos); + self.pos += 1; + current + } +} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml b/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml index fd60c1f43a2..4de416bb4f1 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml @@ -6,20 +6,23 @@ license = { workspace = true } publish = { workspace = true } [target.'cfg(windows)'.dependencies] +desktop_core = { path = "../core" } +futures = { workspace = true } windows = { workspace = true, features = [ "Win32_Foundation", "Win32_Security", "Win32_System_Com", "Win32_System_LibraryLoader", + "Win32_UI_HiDpi", ] } windows-core = { workspace = true } -hex = { workspace = true } -reqwest = { version = "0.12", features = ["json", "blocking"] } serde_json = { workspace = true } serde = { workspace = true, features = ["derive"] } +tracing = { workspace = true } ciborium = "0.2" -sha2 = "0.10" tokio = { workspace = true } +base64 = { workspace = true } +win_webauthn = { path = "../win_webauthn" } [lints] workspace = true diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/assert.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/assert.rs index b4210f5feb6..32dcf625aca 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/assert.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/assert.rs @@ -1,175 +1,141 @@ use serde_json; -use std::alloc::{alloc, Layout}; -use std::ptr; -use windows_core::{s, HRESULT}; - -use crate::com_provider::{ - parse_credential_list, ExperimentalWebAuthnPluginOperationRequest, - ExperimentalWebAuthnPluginOperationResponse, +use std::{ + sync::{mpsc::Receiver, Arc}, + time::Duration, }; -use crate::types::*; -use crate::util::{debug_log, delay_load, wstr_to_string}; -use crate::webauthn::WEBAUTHN_CREDENTIAL_LIST; -// 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, - pub pAuthenticatorOptions: - *const crate::webauthn::ExperimentalWebAuthnCtapCborAuthenticatorOptions, - // Add other fields as needed... -} +use win_webauthn::plugin::PluginGetAssertionRequest; -pub type PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST = - *mut EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST; +use crate::{ + ipc2::{ + CallbackError, PasskeyAssertionRequest, PasskeyAssertionResponse, + PasskeyAssertionWithoutUserInterfaceRequest, Position, TimedCallback, UserVerification, + WindowsProviderClient, + }, + util::{create_context_string, HwndExt}, +}; -// 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; +pub fn get_assertion( + ipc_client: &WindowsProviderClient, + request: PluginGetAssertionRequest, + cancellation_token: Receiver<()>, +) -> Result, Box> { + // Extract RP information + let rp_id = request.rp_id().to_string(); -type EXPERIMENTAL_WebAuthNFreeDecodedGetAssertionRequestFn = unsafe extern "stdcall" fn( - pGetAssertionRequest: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST, -); + // Extract client data hash + let client_data_hash = request.client_data_hash().to_vec(); -// RAII wrapper for decoded get assertion request -pub struct DecodedGetAssertionRequest { - ptr: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST, - free_fn: Option, -} + // Extract user verification requirement from authenticator options + let user_verification = match request + .authenticator_options() + .and_then(|opts| opts.user_verification()) + { + Some(true) => UserVerification::Required, + Some(false) => UserVerification::Discouraged, + None => UserVerification::Preferred, + }; -impl DecodedGetAssertionRequest { - fn new( - ptr: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST, - free_fn: Option, - ) -> Self { - Self { ptr, free_fn } - } + // Extract allowed credentials from credential list + let allowed_credential_ids: Vec> = request + .allow_credentials() + .iter() + .filter_map(|cred| cred.credential_id()) + .map(|id| id.to_vec()) + .collect(); - pub fn as_ref(&self) -> &EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST { - unsafe { &*self.ptr } - } -} + let client_window_handle = request.window_handle.0.addr().to_le_bytes().to_vec(); + let client_pos = request + .window_handle + .center_position() + .unwrap_or((640, 480)); -impl Drop for DecodedGetAssertionRequest { - fn drop(&mut self) { - if !self.ptr.is_null() { - if let Some(free_fn) = self.free_fn { - debug_log("Freeing decoded get assertion request"); - unsafe { - free_fn(self.ptr); - } - } - } - } -} - -// Function to decode get assertion request using Windows API -unsafe fn decode_get_assertion_request( - encoded_request: &[u8], -) -> Result { - debug_log("Attempting to decode get assertion request using Windows API"); - - // Load the Windows WebAuthn API function - let decode_fn: Option = delay_load( - s!("webauthn.dll"), - s!("EXPERIMENTAL_WebAuthNDecodeGetAssertionRequest"), + tracing::debug!( + "Get assertion request - RP: {}, Allowed credentials: {:?}", + rp_id, + allowed_credential_ids ); + let context = create_context_string(request.transaction_id); - let decode_fn = decode_fn - .ok_or("Failed to load EXPERIMENTAL_WebAuthNDecodeGetAssertionRequest from webauthn.dll")?; + // Send assertion request + let assertion_request = PasskeyAssertionRequest { + rp_id, + client_data_hash, + allowed_credentials: allowed_credential_ids, + user_verification, + client_window_handle, + window_xy: Position { + x: client_pos.0, + y: client_pos.1, + }, + context, + }; + let passkey_response = + send_assertion_request(ipc_client, assertion_request, cancellation_token) + .map_err(|err| format!("Failed to get assertion response from IPC channel: {err}"))?; + tracing::debug!("Assertion response received: {:?}", passkey_response); - // Load the free function - let free_fn: Option = delay_load( - s!("webauthn.dll"), - s!("EXPERIMENTAL_WebAuthNFreeDecodedGetAssertionRequest"), - ); + // Create proper WebAuthn response from passkey_response + tracing::debug!("Creating WebAuthn get assertion response"); - 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, - )) -} - -/// Windows WebAuthn assertion request context -#[derive(Debug, Clone)] -pub struct WindowsAssertionRequest { - pub rpid: String, - pub client_data_hash: Vec, - pub allowed_credentials: Vec>, - pub user_verification: UserVerificationRequirement, + let response = create_get_assertion_response( + passkey_response.credential_id, + passkey_response.authenticator_data, + passkey_response.signature, + passkey_response.user_handle, + )?; + Ok(response) } /// Helper for assertion requests fn send_assertion_request( - transaction_id: &str, - request: &WindowsAssertionRequest, -) -> Option { - let passkey_request = PasskeyAssertionRequest { - rp_id: request.rpid.clone(), - transaction_id: transaction_id.to_string(), - client_data_hash: request.client_data_hash.clone(), - allowed_credentials: request.allowed_credentials.clone(), - user_verification: request.user_verification.clone(), - window_xy: Position { x: 400, y: 400 }, - }; - - debug_log(&format!( - "Assertion request data - RP ID: {}, Client data hash: {} bytes, Allowed credentials: {}", - request.rpid, + ipc_client: &WindowsProviderClient, + request: PasskeyAssertionRequest, + cancellation_token: Receiver<()>, +) -> Result { + tracing::debug!( + "Assertion request data - RP ID: {}, Client data hash: {} bytes, Allowed credentials: {:?}", + request.rp_id, request.client_data_hash.len(), - request.allowed_credentials.len() - )); + request.allowed_credentials, + ); - match serde_json::to_string(&passkey_request) { - Ok(request_json) => { - debug_log(&format!("Sending assertion request: {}", request_json)); - crate::ipc::send_passkey_request(RequestType::Assertion, request_json, &request.rpid) - } - Err(e) => { - debug_log(&format!( - "ERROR: Failed to serialize assertion request: {}", - e - )); - None - } + let request_json = serde_json::to_string(&request) + .map_err(|err| format!("Failed to serialize assertion request: {err}"))?; + tracing::debug!(?request_json, "Sending assertion request"); + let callback = Arc::new(TimedCallback::new()); + if request.allowed_credentials.len() == 1 { + // copying this into another struct because I'm too lazy to make an enum right now. + let request = PasskeyAssertionWithoutUserInterfaceRequest { + rp_id: request.rp_id, + credential_id: request.allowed_credentials[0].clone(), + client_data_hash: request.client_data_hash, + user_verification: request.user_verification, + client_window_handle: request.client_window_handle, + window_xy: request.window_xy, + context: request.context, + }; + ipc_client.prepare_passkey_assertion_without_user_interface(request, callback.clone()); + } else { + ipc_client.prepare_passkey_assertion(request, callback.clone()); } + let wait_time = Duration::from_secs(600); + callback + .wait_for_response(wait_time, Some(cancellation_token)) + .map_err(|err| match err { + CallbackError::Timeout => "Assertion request timed out".to_string(), + CallbackError::Cancelled => "Assertion request cancelled".to_string(), + })? + .map_err(|err| err.to_string()) } /// Creates a WebAuthn get assertion response from Bitwarden's assertion response -unsafe fn create_get_assertion_response( +fn create_get_assertion_response( credential_id: Vec, authenticator_data: Vec, signature: Vec, user_handle: Vec, -) -> std::result::Result<*mut ExperimentalWebAuthnPluginOperationResponse, HRESULT> { +) -> std::result::Result, Box> { // Construct a CTAP2 response with the proper structure // Create CTAP2 GetAssertion response map according to CTAP2 specification @@ -215,214 +181,42 @@ unsafe fn create_get_assertion_response( )); } + // [5] numberOfCredentials (optional) + cbor_response.push(( + ciborium::Value::Integer(5.into()), + ciborium::Value::Integer(1.into()), + )); + 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) { - debug_log(&format!( - "ERROR: Failed to encode CBOR assertion response: {:?}", - e - )); - return Err(HRESULT(-1)); + return Err(format!("Failed to encode CBOR assertion response: {e}"))?; } - 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::(); - 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) + tracing::debug!("Formatted CBOR assertion response: {:?}", cbor_data); + Ok(cbor_data) } -/// Implementation of EXPERIMENTAL_PluginGetAssertion moved from com_provider.rs -pub unsafe fn experimental_plugin_get_assertion( - request: *const ExperimentalWebAuthnPluginOperationRequest, - response: *mut *mut ExperimentalWebAuthnPluginOperationResponse, -) -> HRESULT { - debug_log("EXPERIMENTAL_PluginGetAssertion() called"); +#[cfg(test)] +mod tests { + use super::create_get_assertion_response; - // Validate input parameters - if request.is_null() || response.is_null() { - debug_log("Invalid parameters passed to EXPERIMENTAL_PluginGetAssertion"); - return HRESULT(-1); - } - - let req = &*request; - let transaction_id = format!("{:?}", req.transaction_id); - - debug_log(&format!( - "Get assertion request - Transaction: {}", - transaction_id - )); - - if req.encoded_request_byte_count == 0 || req.encoded_request_pointer.is_null() { - debug_log("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(); - debug_log("Successfully decoded get assertion request using Windows API"); - - // Extract RP information - let rpid = if decoded_request.pwszRpId.is_null() { - debug_log("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) => { - debug_log(&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() - { - debug_log("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() - }; - - // Extract user verification requirement from authenticator options - let user_verification = if !decoded_request.pAuthenticatorOptions.is_null() { - let auth_options = &*decoded_request.pAuthenticatorOptions; - match auth_options.user_verification { - 1 => Some(UserVerificationRequirement::Required), - -1 => Some(UserVerificationRequirement::Discouraged), - 0 | _ => Some(UserVerificationRequirement::Preferred), // Default or undefined - } - } else { - None - }; - - // Extract allowed credentials from credential list - let allowed_credentials = parse_credential_list(&decoded_request.CredentialList); - - // Create Windows assertion request - let assertion_request = WindowsAssertionRequest { - rpid: rpid.clone(), - client_data_hash, - allowed_credentials: allowed_credentials.clone(), - user_verification: user_verification.unwrap_or_default(), - }; - - debug_log(&format!( - "Get assertion request - RP: {}, Allowed credentials: {}", - rpid, - allowed_credentials.len() - )); - - // Send assertion request - if let Some(passkey_response) = - send_assertion_request(&transaction_id, &assertion_request) - { - debug_log(&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, - rp_id: _, - client_data_hash: _, - } => { - debug_log("Creating WebAuthn get assertion response"); - - match create_get_assertion_response( - credential_id, - authenticator_data, - signature, - user_handle, - ) { - Ok(webauthn_response) => { - debug_log("Successfully created WebAuthn assertion response"); - *response = webauthn_response; - HRESULT(0) - } - Err(e) => { - debug_log(&format!( - "ERROR: Failed to create WebAuthn assertion response: {}", - e - )); - *response = ptr::null_mut(); - HRESULT(-1) - } - } - } - PasskeyResponse::Error { message } => { - debug_log(&format!("Assertion request failed: {}", message)); - *response = ptr::null_mut(); - HRESULT(-1) - } - _ => { - debug_log("ERROR: Unexpected response type for assertion request"); - *response = ptr::null_mut(); - HRESULT(-1) - } - } - } else { - debug_log("ERROR: No response from assertion request"); - *response = ptr::null_mut(); - HRESULT(-1) - } - } - Err(e) => { - debug_log(&format!( - "ERROR: Failed to decode get assertion request: {}", - e - )); - *response = ptr::null_mut(); - HRESULT(-1) - } + #[test] + fn test_create_native_assertion_response() { + let credential_id = vec![1, 2, 3, 4]; + let authenticator_data = vec![5, 6, 7, 8]; + let signature = vec![9, 10, 11, 12]; + let user_handle = vec![13, 14, 15, 16]; + let cbor = create_get_assertion_response( + credential_id, + authenticator_data, + signature, + user_handle, + ) + .unwrap(); + // CTAP2_OK, Map(5 elements) + assert_eq!([0x00, 0xa5], cbor[..2]); } } diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/com_buffer.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/com_buffer.rs deleted file mode 100644 index af34107a050..00000000000 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/com_buffer.rs +++ /dev/null @@ -1,84 +0,0 @@ -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>); - -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::()).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(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(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::(); - 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::().write(object) }; - buffer.into_ptr() - } - - pub fn from_buffer>(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::(), 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) - } -} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/com_provider.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/com_provider.rs deleted file mode 100644 index 11b7b26b798..00000000000 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/com_provider.rs +++ /dev/null @@ -1,292 +0,0 @@ -use std::ptr; -use windows::Win32::System::Com::*; -use windows_core::{implement, interface, IInspectable, IUnknown, Interface, HRESULT}; - -use crate::assert::experimental_plugin_get_assertion; -use crate::make_credential::experimental_plugin_make_credential; -use crate::util::debug_log; -use crate::webauthn::WEBAUTHN_CREDENTIAL_LIST; - -/// Plugin request type enum as defined in the IDL -#[repr(u32)] -#[derive(Debug, Copy, Clone)] -pub enum WebAuthnPluginRequestType { - CTAP2_CBOR = 0x01, -} - -/// Plugin lock status enum as defined in the IDL -#[repr(u32)] -#[derive(Debug, Copy, Clone)] -pub enum PluginLockStatus { - PluginLocked = 0, - PluginUnlocked = 1, -} - -/// 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 when creating and asserting credentials with EXPERIMENTAL2 interface. -/// Header File Name: _EXPERIMENTAL2_WEBAUTHN_PLUGIN_OPERATION_REQUEST -/// Header File Usage: EXPERIMENTAL_MakeCredential() -/// EXPERIMENTAL_GetAssertion() -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct Experimental2WebAuthnPluginOperationRequest { - 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 request_type: WebAuthnPluginRequestType, - 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; - fn EXPERIMENTAL_GetLockStatus( - &self, - lock_status: *mut PluginLockStatus, - ) -> HRESULT; -} - -#[interface("d26bcf6f-b54c-43ff-9f06-d5bf148625f7")] -pub unsafe trait EXPERIMENTAL2_IPluginAuthenticator: windows_core::IUnknown { - fn EXPERIMENTAL_MakeCredential( - &self, - request: *const Experimental2WebAuthnPluginOperationRequest, - response: *mut *mut ExperimentalWebAuthnPluginOperationResponse, - ) -> HRESULT; - fn EXPERIMENTAL_GetAssertion( - &self, - request: *const Experimental2WebAuthnPluginOperationRequest, - response: *mut *mut ExperimentalWebAuthnPluginOperationResponse, - ) -> HRESULT; - fn EXPERIMENTAL_CancelOperation( - &self, - request: *const ExperimentalWebAuthnPluginCancelOperationRequest, - ) -> HRESULT; - fn EXPERIMENTAL_GetLockStatus( - &self, - lock_status: *mut PluginLockStatus, - ) -> HRESULT; -} - - -pub unsafe fn parse_credential_list(credential_list: &WEBAUTHN_CREDENTIAL_LIST) -> Vec> { - let mut allowed_credentials = Vec::new(); - - if credential_list.cCredentials == 0 || credential_list.ppCredentials.is_null() { - debug_log("No credentials in credential list"); - return allowed_credentials; - } - - debug_log(&format!( - "Parsing {} credentials from credential list", - credential_list.cCredentials - )); - - // ppCredentials is an array of pointers to WEBAUTHN_CREDENTIAL_EX - let credentials_array = std::slice::from_raw_parts( - credential_list.ppCredentials, - credential_list.cCredentials as usize, - ); - - for (i, &credential_ptr) in credentials_array.iter().enumerate() { - if credential_ptr.is_null() { - debug_log(&format!("WARNING: Credential {} is null, skipping", i)); - continue; - } - - let credential = &*credential_ptr; - - if credential.cbId == 0 || credential.pbId.is_null() { - debug_log(&format!( - "WARNING: Credential {} has invalid ID, skipping", - i - )); - continue; - } - - // Extract credential ID bytes - let credential_id_slice = - std::slice::from_raw_parts(credential.pbId, credential.cbId as usize); - - allowed_credentials.push(credential_id_slice.to_vec()); - debug_log(&format!( - "Parsed credential {}: {} bytes", - i, credential.cbId - )); - } - - debug_log(&format!( - "Successfully parsed {} allowed credentials", - allowed_credentials.len() - )); - allowed_credentials -} - -#[implement(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL2_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 { - experimental_plugin_make_credential(request, response) - } - - unsafe fn EXPERIMENTAL_PluginGetAssertion( - &self, - request: *const ExperimentalWebAuthnPluginOperationRequest, - response: *mut *mut ExperimentalWebAuthnPluginOperationResponse, - ) -> HRESULT { - experimental_plugin_get_assertion(request, response) - } - - unsafe fn EXPERIMENTAL_PluginCancelOperation( - &self, - _request: *const ExperimentalWebAuthnPluginCancelOperationRequest, - ) -> HRESULT { - debug_log("EXPERIMENTAL_PluginCancelOperation() called"); - HRESULT(0) - } - - unsafe fn EXPERIMENTAL_GetLockStatus( - &self, - lock_status: *mut PluginLockStatus, - ) -> HRESULT { - debug_log("EXPERIMENTAL_GetLockStatus() called"); - if lock_status.is_null() { - return HRESULT(-2147024809); // E_INVALIDARG - } - *lock_status = PluginLockStatus::PluginUnlocked; - HRESULT(0) - } -} - -impl EXPERIMENTAL2_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Impl { - unsafe fn EXPERIMENTAL_MakeCredential( - &self, - request: *const Experimental2WebAuthnPluginOperationRequest, - response: *mut *mut ExperimentalWebAuthnPluginOperationResponse, - ) -> HRESULT { - debug_log("EXPERIMENTAL2_MakeCredential() called"); - let legacy_request = ExperimentalWebAuthnPluginOperationRequest { - window_handle: (*request).window_handle, - transaction_id: (*request).transaction_id, - request_signature_byte_count: (*request).request_signature_byte_count, - request_signature_pointer: (*request).request_signature_pointer, - encoded_request_byte_count: (*request).encoded_request_byte_count, - encoded_request_pointer: (*request).encoded_request_pointer, - }; - experimental_plugin_make_credential(&legacy_request, response) - } - - unsafe fn EXPERIMENTAL_GetAssertion( - &self, - request: *const Experimental2WebAuthnPluginOperationRequest, - response: *mut *mut ExperimentalWebAuthnPluginOperationResponse, - ) -> HRESULT { - debug_log("EXPERIMENTAL2_GetAssertion() called"); - let legacy_request = ExperimentalWebAuthnPluginOperationRequest { - window_handle: (*request).window_handle, - transaction_id: (*request).transaction_id, - request_signature_byte_count: (*request).request_signature_byte_count, - request_signature_pointer: (*request).request_signature_pointer, - encoded_request_byte_count: (*request).encoded_request_byte_count, - encoded_request_pointer: (*request).encoded_request_pointer, - }; - experimental_plugin_get_assertion(&legacy_request, response) - } - - unsafe fn EXPERIMENTAL_CancelOperation( - &self, - _request: *const ExperimentalWebAuthnPluginCancelOperationRequest, - ) -> HRESULT { - debug_log("EXPERIMENTAL2_CancelOperation() called"); - HRESULT(0) - } - - unsafe fn EXPERIMENTAL_GetLockStatus( - &self, - lock_status: *mut PluginLockStatus, - ) -> HRESULT { - debug_log("EXPERIMENTAL2_GetLockStatus() called"); - if lock_status.is_null() { - return HRESULT(-2147024809); // E_INVALIDARG - } - *lock_status = PluginLockStatus::PluginUnlocked; - HRESULT(0) - } -} - - -impl IClassFactory_Impl for Factory_Impl { - fn CreateInstance( - &self, - _outer: windows_core::Ref, - 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(()) - } -} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/com_registration.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/com_registration.rs deleted file mode 100644 index 2b0ab414aac..00000000000 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/com_registration.rs +++ /dev/null @@ -1,314 +0,0 @@ -use std::ffi::c_uchar; -use std::ptr; - -use windows::Win32::System::Com::*; -use windows_core::{s, ComObjectInterface, GUID, HRESULT, HSTRING, PCWSTR}; - -use crate::com_provider; -use crate::util::delay_load; -use crate::webauthn::*; -use ciborium::value::Value; -use hex; - -const AUTHENTICATOR_NAME: &str = "Bitwarden Desktop"; -const CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062"; -const RPID: &str = "bitwarden.com"; -const AAGUID: &str = "d548826e-79b4-db40-a3d8-11116f7e8349"; - -/// Parses a UUID string (with hyphens) into bytes -fn parse_uuid_to_bytes(uuid_str: &str) -> Result, String> { - let uuid_clean = uuid_str.replace("-", ""); - if uuid_clean.len() != 32 { - return Err("Invalid UUID format".to_string()); - } - - uuid_clean - .chars() - .collect::>() - .chunks(2) - .map(|chunk| { - let hex_str: String = chunk.iter().collect(); - u8::from_str_radix(&hex_str, 16) - .map_err(|_| format!("Invalid hex character in UUID: {}", hex_str)) - }) - .collect() -} - -/// Converts the CLSID constant string to a GUID -fn parse_clsid_to_guid() -> Result { - // Remove hyphens and parse as hex - let clsid_clean = CLSID.replace("-", ""); - if clsid_clean.len() != 32 { - return Err("Invalid CLSID format".to_string()); - } - - // Convert to u128 and create GUID - let clsid_u128 = u128::from_str_radix(&clsid_clean, 16) - .map_err(|_| "Failed to parse CLSID as hex".to_string())?; - - Ok(GUID::from_u128(clsid_u128)) -} - -/// Generates CBOR-encoded authenticator info according to FIDO CTAP2 specifications -/// See: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorGetInfo -fn generate_cbor_authenticator_info() -> Result, String> { - // Parse AAGUID from string format to bytes - let aaguid_bytes = parse_uuid_to_bytes(AAGUID)?; - - // Create the authenticator info map according to CTAP2 spec - // Using Vec<(Value, Value)> because that's what ciborium::Value::Map expects - let mut authenticator_info = Vec::new(); - - // 1: versions - Array of supported FIDO versions - authenticator_info.push(( - Value::Integer(1.into()), - Value::Array(vec![ - Value::Text("FIDO_2_0".to_string()), - Value::Text("FIDO_2_1".to_string()), - ]), - )); - - // 2: extensions - Array of supported extensions (empty for now) - authenticator_info.push((Value::Integer(2.into()), Value::Array(vec![]))); - - // 3: aaguid - 16-byte AAGUID - authenticator_info.push((Value::Integer(3.into()), Value::Bytes(aaguid_bytes))); - - // 4: options - Map of supported options - let options = vec![ - (Value::Text("rk".to_string()), Value::Bool(true)), // resident key - (Value::Text("up".to_string()), Value::Bool(true)), // user presence - (Value::Text("uv".to_string()), Value::Bool(true)), // user verification - ]; - authenticator_info.push((Value::Integer(4.into()), Value::Map(options))); - - // 9: transports - Array of supported transports - authenticator_info.push(( - Value::Integer(9.into()), - Value::Array(vec![ - Value::Text("internal".to_string()), - Value::Text("hybrid".to_string()), - ]), - )); - - // 10: algorithms - Array of supported algorithms - let algorithm = vec![ - (Value::Text("alg".to_string()), Value::Integer((-7).into())), // ES256 - ( - Value::Text("type".to_string()), - Value::Text("public-key".to_string()), - ), - ]; - authenticator_info.push(( - Value::Integer(10.into()), - Value::Array(vec![Value::Map(algorithm)]), - )); - - // Encode to CBOR - let mut buffer = Vec::new(); - ciborium::ser::into_writer(&Value::Map(authenticator_info), &mut buffer) - .map_err(|e| format!("Failed to encode CBOR: {}", e))?; - - Ok(buffer) -} - -/// 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.into_static(); - let clsid_guid = parse_clsid_to_guid().map_err(|e| format!("Failed to parse CLSID: {}", e))?; - let clsid: *const GUID = &clsid_guid; - - match unsafe { - CoRegisterClassObject( - clsid, - FACTORY.as_interface_ref(), - //FACTORY.as_interface::(), - CLSCTX_LOCAL_SERVER, - REGCLS_MULTIPLEUSE, - ) - } { - Ok(_) => Ok(()), - Err(e) => Err(format!( - "Error: couldn't register the COM library\n{}", - e.message() - )), - } -} - -/// 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(); - - // Generate CBOR authenticator info dynamically - let mut authenticator_info_bytes = generate_cbor_authenticator_info() - .map_err(|e| format!("Failed to generate authenticator info: {}", e))?; - - 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::( - 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; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_generate_cbor_authenticator_info() { - let result = generate_cbor_authenticator_info(); - assert!(result.is_ok(), "CBOR generation should succeed"); - - let cbor_bytes = result.unwrap(); - assert!(!cbor_bytes.is_empty(), "CBOR bytes should not be empty"); - - // Verify the CBOR can be decoded back - let decoded: Result = ciborium::de::from_reader(&cbor_bytes[..]); - assert!(decoded.is_ok(), "Generated CBOR should be valid"); - - // Verify it's a map with expected keys - if let Value::Map(map) = decoded.unwrap() { - assert!( - map.contains_key(&Value::Integer(1.into())), - "Should contain versions (key 1)" - ); - assert!( - map.contains_key(&Value::Integer(2.into())), - "Should contain extensions (key 2)" - ); - assert!( - map.contains_key(&Value::Integer(3.into())), - "Should contain aaguid (key 3)" - ); - assert!( - map.contains_key(&Value::Integer(4.into())), - "Should contain options (key 4)" - ); - assert!( - map.contains_key(&Value::Integer(9.into())), - "Should contain transports (key 9)" - ); - assert!( - map.contains_key(&Value::Integer(10.into())), - "Should contain algorithms (key 10)" - ); - } else { - panic!("CBOR should decode to a map"); - } - - // Print the generated CBOR for verification - println!("Generated CBOR hex: {}", hex::encode(&cbor_bytes)); - } - - #[test] - fn test_aaguid_parsing() { - let result = parse_uuid_to_bytes(AAGUID); - assert!(result.is_ok(), "AAGUID parsing should succeed"); - - let aaguid_bytes = result.unwrap(); - assert_eq!(aaguid_bytes.len(), 16, "AAGUID should be 16 bytes"); - assert_eq!(aaguid_bytes[0], 0xd5, "First byte should be 0xd5"); - assert_eq!(aaguid_bytes[1], 0x48, "Second byte should be 0x48"); - - // Verify full expected AAGUID - let expected_hex = "d548826e79b4db40a3d811116f7e8349"; - let expected_bytes = hex::decode(expected_hex).unwrap(); - assert_eq!( - aaguid_bytes, expected_bytes, - "AAGUID should match expected value" - ); - } - - #[test] - fn test_parse_clsid_to_guid() { - let result = parse_clsid_to_guid(); - assert!(result.is_ok(), "CLSID parsing should succeed"); - } -} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc.rs deleted file mode 100644 index 3b1bd7788ba..00000000000 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::sync::Mutex; -use tokio::sync::{mpsc, oneshot}; - -use crate::types::*; -use crate::util::debug_log; - -/// Global channel sender for request notifications -static REQUEST_SENDER: Mutex>> = Mutex::new(None); - -/// Sets the channel sender for request notifications -pub fn set_request_sender(sender: mpsc::UnboundedSender) { - match REQUEST_SENDER.lock() { - Ok(mut tx) => { - *tx = Some(sender); - debug_log("Passkey request callback registered"); - } - Err(e) => { - debug_log(&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 { - 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), - }; - - debug_log(&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) => { - debug_log(&format!("Received callback response {:?}", response)); - Some(response) - } - Err(_) => { - debug_log("No response from callback"); - None - } - } - } else { - debug_log("Failed to send event to callback"); - None - } - } else { - debug_log("No callback registered for passkey requests"); - None - } - } else { - None - } -} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/assertion.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/assertion.rs new file mode 100644 index 00000000000..85dded177aa --- /dev/null +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/assertion.rs @@ -0,0 +1,58 @@ +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; + +use super::{BitwardenError, Callback, Position, UserVerification}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasskeyAssertionRequest { + pub rp_id: String, + pub client_data_hash: Vec, + pub user_verification: UserVerification, + pub allowed_credentials: Vec>, + pub window_xy: Position, + pub client_window_handle: Vec, + pub context: String, + // pub extension_input: Vec, TODO: Implement support for extensions +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasskeyAssertionWithoutUserInterfaceRequest { + pub rp_id: String, + pub credential_id: Vec, + pub client_data_hash: Vec, + pub user_verification: UserVerification, + pub window_xy: Position, + pub client_window_handle: Vec, + pub context: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasskeyAssertionResponse { + pub rp_id: String, + pub user_handle: Vec, + pub signature: Vec, + pub client_data_hash: Vec, + pub authenticator_data: Vec, + pub credential_id: Vec, +} + +pub trait PreparePasskeyAssertionCallback: Send + Sync { + fn on_complete(&self, credential: PasskeyAssertionResponse); + fn on_error(&self, error: BitwardenError); +} + +impl Callback for Arc { + fn complete(&self, credential: serde_json::Value) -> Result<(), serde_json::Error> { + let credential = serde_json::from_value(credential)?; + PreparePasskeyAssertionCallback::on_complete(self.as_ref(), credential); + Ok(()) + } + + fn error(&self, error: BitwardenError) { + PreparePasskeyAssertionCallback::on_error(self.as_ref(), error); + } +} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/lock_status.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/lock_status.rs new file mode 100644 index 00000000000..f593376129f --- /dev/null +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/lock_status.rs @@ -0,0 +1,41 @@ +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; + +use crate::ipc2::{BitwardenError, Callback, TimedCallback}; + +#[derive(Debug, Serialize, Deserialize)] +pub(super) struct LockStatusRequest {} + +#[derive(Debug, Deserialize)] +pub struct LockStatusResponse { + #[serde(rename = "isUnlocked")] + pub(crate) is_unlocked: bool, +} + +impl Callback for Arc { + fn complete(&self, response: serde_json::Value) -> Result<(), serde_json::Error> { + let response = serde_json::from_value(response)?; + self.as_ref().on_complete(response); + Ok(()) + } + + fn error(&self, error: BitwardenError) { + self.as_ref().on_error(error); + } +} + +pub trait GetLockStatusCallback: Send + Sync { + fn on_complete(&self, response: LockStatusResponse); + fn on_error(&self, error: BitwardenError); +} + +impl GetLockStatusCallback for TimedCallback { + fn on_complete(&self, response: LockStatusResponse) { + self.send(Ok(response)); + } + + fn on_error(&self, error: BitwardenError) { + self.send(Err(error)) + } +} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/mod.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/mod.rs new file mode 100644 index 00000000000..4522005bbcf --- /dev/null +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/mod.rs @@ -0,0 +1,376 @@ +use std::{ + collections::HashMap, + error::Error, + fmt::Display, + sync::{ + atomic::AtomicU32, + mpsc::{self, Receiver, RecvTimeoutError, Sender}, + Arc, Mutex, + }, + time::{Duration, Instant}, +}; + +use futures::FutureExt; +use serde::{Deserialize, Serialize}; +use tracing::{error, info}; + +mod assertion; +mod lock_status; +mod registration; + +use crate::ipc2::lock_status::{GetLockStatusCallback, LockStatusRequest}; +pub use assertion::{ + PasskeyAssertionRequest, PasskeyAssertionResponse, PasskeyAssertionWithoutUserInterfaceRequest, + PreparePasskeyAssertionCallback, +}; +pub use registration::{ + PasskeyRegistrationRequest, PasskeyRegistrationResponse, PreparePasskeyRegistrationCallback, +}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum UserVerification { + Preferred, + Required, + Discouraged, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Position { + pub x: i32, + pub y: i32, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum BitwardenError { + Internal(String), +} + +impl Display for BitwardenError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Internal(msg) => write!(f, "Internal error occurred: {msg}"), + } + } +} + +impl Error for BitwardenError {} + +// TODO: These have to be named differently than the actual Uniffi traits otherwise +// the generated code will lead to ambiguous trait implementations +// These are only used internally, so it doesn't matter that much +trait Callback: Send + Sync { + fn complete(&self, credential: serde_json::Value) -> Result<(), serde_json::Error>; + fn error(&self, error: BitwardenError); +} + +#[derive(Debug)] +/// Store the connection status between the Windows credential provider extension +/// and the desktop application's IPC server. +pub enum ConnectionStatus { + Connected, + Disconnected, +} + +pub struct WindowsProviderClient { + to_server_send: tokio::sync::mpsc::Sender, + + // We need to keep track of the callbacks so we can call them when we receive a response + response_callbacks_counter: AtomicU32, + #[allow(clippy::type_complexity)] + response_callbacks_queue: Arc, Instant)>>>, + + // Flag to track connection status - atomic for thread safety without locks + connection_status: Arc, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +/// Store native desktop status information to use for IPC communication +/// between the application and the Windows credential provider. +pub struct NativeStatus { + key: String, + value: String, +} + +// In our callback management, 0 is a reserved sequence number indicating that a message does not have a callback. +const NO_CALLBACK_INDICATOR: u32 = 0; + +impl WindowsProviderClient { + // FIXME: Remove unwraps! They panic and terminate the whole application. + #[allow(clippy::unwrap_used)] + pub fn connect() -> Self { + tracing::debug!("Windows COM server trying to connect to Electron IPC..."); + + let (from_server_send, mut from_server_recv) = tokio::sync::mpsc::channel(32); + let (to_server_send, to_server_recv) = tokio::sync::mpsc::channel(32); + + let client = WindowsProviderClient { + to_server_send, + response_callbacks_counter: AtomicU32::new(1), // Start at 1 since 0 is reserved for "no callback" scenarios + response_callbacks_queue: Arc::new(Mutex::new(HashMap::new())), + connection_status: Arc::new(std::sync::atomic::AtomicBool::new(false)), + }; + + let path = desktop_core::ipc::path("af"); + + let queue = client.response_callbacks_queue.clone(); + let connection_status = client.connection_status.clone(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Can't create runtime"); + + rt.spawn( + desktop_core::ipc::client::connect(path, from_server_send, to_server_recv) + .map(|r| r.map_err(|e| e.to_string())), + ); + + rt.block_on(async move { + while let Some(message) = from_server_recv.recv().await { + match serde_json::from_str::(&message) { + Ok(SerializedMessage::Command(CommandMessage::Connected)) => { + info!("Connected to server"); + connection_status.store(true, std::sync::atomic::Ordering::Relaxed); + } + Ok(SerializedMessage::Command(CommandMessage::Disconnected)) => { + info!("Disconnected from server"); + connection_status.store(false, std::sync::atomic::Ordering::Relaxed); + } + Ok(SerializedMessage::Message { + sequence_number, + value, + }) => match queue.lock().unwrap().remove(&sequence_number) { + Some((cb, request_start_time)) => { + info!( + "Time to process request: {:?}", + request_start_time.elapsed() + ); + match value { + Ok(value) => { + if let Err(e) = cb.complete(value) { + error!(error = %e, "Error deserializing message"); + } + } + Err(e) => { + error!(error = ?e, "Error processing message"); + cb.error(e) + } + } + } + None => { + error!(sequence_number, "No callback found for sequence number") + } + }, + Err(e) => { + error!(error = %e, "Error deserializing message"); + } + }; + } + }); + }); + + client + } + + pub fn send_native_status(&self, key: String, value: String) { + let status = NativeStatus { key, value }; + self.send_message(status, None); + } + + pub fn prepare_passkey_registration( + &self, + request: PasskeyRegistrationRequest, + callback: Arc, + ) { + self.send_message(request, Some(Box::new(callback))); + } + + pub fn prepare_passkey_assertion( + &self, + request: PasskeyAssertionRequest, + callback: Arc, + ) { + self.send_message(request, Some(Box::new(callback))); + } + + pub fn prepare_passkey_assertion_without_user_interface( + &self, + request: PasskeyAssertionWithoutUserInterfaceRequest, + callback: Arc, + ) { + self.send_message(request, Some(Box::new(callback))); + } + + pub fn get_lock_status(&self, callback: Arc) { + self.send_message(LockStatusRequest {}, Some(Box::new(callback))); + } + + pub fn get_connection_status(&self) -> ConnectionStatus { + let is_connected = self + .connection_status + .load(std::sync::atomic::Ordering::Relaxed); + if is_connected { + ConnectionStatus::Connected + } else { + ConnectionStatus::Disconnected + } + } +} + +#[derive(Serialize, Deserialize)] +#[serde(tag = "command", rename_all = "camelCase")] +enum CommandMessage { + Connected, + Disconnected, +} + +#[derive(Serialize, Deserialize)] +#[serde(untagged, rename_all = "camelCase")] +enum SerializedMessage { + Command(CommandMessage), + Message { + sequence_number: u32, + value: Result, + }, +} + +impl WindowsProviderClient { + #[allow(clippy::unwrap_used)] + fn add_callback(&self, callback: Box) -> u32 { + let sequence_number = self + .response_callbacks_counter + .fetch_add(1, std::sync::atomic::Ordering::SeqCst); + + self.response_callbacks_queue + .lock() + .expect("response callbacks queue mutex should not be poisoned") + .insert(sequence_number, (callback, Instant::now())); + + sequence_number + } + + #[allow(clippy::unwrap_used)] + fn send_message(&self, message: impl Serialize, callback: Option>) { + let sequence_number = if let Some(callback) = callback { + self.add_callback(callback) + } else { + NO_CALLBACK_INDICATOR + }; + + let message = serde_json::to_string(&SerializedMessage::Message { + sequence_number, + value: Ok(serde_json::to_value(message).unwrap()), + }) + .expect("Can't serialize message"); + + if let Err(e) = self.to_server_send.blocking_send(message) { + // Make sure we remove the callback from the queue if we can't send the message + if sequence_number != NO_CALLBACK_INDICATOR { + if let Some((callback, _)) = self + .response_callbacks_queue + .lock() + .expect("response callbacks queue mutex should not be poisoned") + .remove(&sequence_number) + { + callback.error(BitwardenError::Internal(format!( + "Error sending message: {e}" + ))); + } + } + } + } +} + +pub enum CallbackError { + Timeout, + Cancelled, +} + +pub struct TimedCallback { + tx: Arc>>>>, + rx: Arc>>>, +} + +impl TimedCallback { + pub fn new() -> Self { + let (tx, rx) = mpsc::channel(); + Self { + tx: Arc::new(Mutex::new(Some(tx))), + rx: Arc::new(Mutex::new(rx)), + } + } + + pub fn wait_for_response( + &self, + timeout: Duration, + cancellation_token: Option>, + ) -> Result, CallbackError> { + let (tx, rx) = mpsc::channel(); + if let Some(cancellation_token) = cancellation_token { + let tx2 = tx.clone(); + let cancellation_token = Mutex::new(cancellation_token); + std::thread::spawn(move || { + if let Ok(()) = cancellation_token.lock().unwrap().recv_timeout(timeout) { + tracing::debug!("Forwarding cancellation"); + _ = tx2.send(Err(CallbackError::Cancelled)); + } + }); + } + let response_rx = self.rx.clone(); + std::thread::spawn(move || { + if let Ok(response) = response_rx.lock().unwrap().recv_timeout(timeout) { + _ = tx.send(Ok(response)); + } + }); + match rx.recv_timeout(timeout) { + Ok(Ok(response)) => Ok(response), + Ok(err @ Err(CallbackError::Cancelled)) => { + tracing::debug!("Received cancellation, dropping."); + err + } + Ok(err @ Err(CallbackError::Timeout)) => { + tracing::debug!("Request timed out, dropping."); + err + } + Err(RecvTimeoutError::Timeout) => Err(CallbackError::Timeout), + Err(_) => Err(CallbackError::Cancelled), + } + } + + fn send(&self, response: Result) { + match self.tx.lock().unwrap().take() { + Some(tx) => { + if let Err(_) = tx.send(response) { + tracing::error!("Windows provider channel closed before receiving IPC response from Electron") + } + } + None => { + tracing::error!("Callback channel used before response: multi-threading issue?"); + } + } + } +} + +impl PreparePasskeyRegistrationCallback for TimedCallback { + fn on_complete(&self, credential: PasskeyRegistrationResponse) { + self.send(Ok(credential)); + } + + fn on_error(&self, error: BitwardenError) { + self.send(Err(error)) + } +} + +impl PreparePasskeyAssertionCallback for TimedCallback { + fn on_complete(&self, credential: PasskeyAssertionResponse) { + self.send(Ok(credential)); + } + + fn on_error(&self, error: BitwardenError) { + self.send(Err(error)) + } +} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/registration.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/registration.rs new file mode 100644 index 00000000000..50adccbc8e2 --- /dev/null +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/registration.rs @@ -0,0 +1,46 @@ +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; + +use super::{BitwardenError, Callback, Position, UserVerification}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasskeyRegistrationRequest { + pub rp_id: String, + pub user_name: String, + pub user_handle: Vec, + pub client_data_hash: Vec, + pub user_verification: UserVerification, + pub supported_algorithms: Vec, + pub window_xy: Position, + pub client_window_handle: Vec, + pub excluded_credentials: Vec>, + pub context: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasskeyRegistrationResponse { + pub rp_id: String, + pub client_data_hash: Vec, + pub credential_id: Vec, + pub attestation_object: Vec, +} + +pub trait PreparePasskeyRegistrationCallback: Send + Sync { + fn on_complete(&self, credential: PasskeyRegistrationResponse); + fn on_error(&self, error: BitwardenError); +} + +impl Callback for Arc { + fn complete(&self, credential: serde_json::Value) -> Result<(), serde_json::Error> { + let credential = serde_json::from_value(credential)?; + PreparePasskeyRegistrationCallback::on_complete(self.as_ref(), credential); + Ok(()) + } + + fn error(&self, error: BitwardenError) { + PreparePasskeyRegistrationCallback::on_error(self.as_ref(), error); + } +} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs index be9c805dc25..9fc2c59b34f 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs @@ -4,42 +4,199 @@ // New modular structure mod assert; -mod com_buffer; -mod com_provider; -mod com_registration; -mod ipc; +mod ipc2; mod make_credential; -mod sync; mod types; mod util; -mod webauthn; -// Re-export main functionality -pub use assert::WindowsAssertionRequest; -pub use com_registration::{add_authenticator, initialize_com_library, register_com_library}; -pub use ipc::{send_passkey_request, set_request_sender}; -pub use make_credential::WindowsRegistrationRequest; -pub use sync::{get_credentials_from_windows, send_sync_request, sync_credentials_to_windows}; -pub use types::{ - PasskeyRequest, PasskeyResponse, RequestEvent, RequestType, SyncedCredential, - UserVerificationRequirement, +use std::{ + collections::{HashMap, HashSet}, + sync::{ + mpsc::{self, Sender}, + Arc, Mutex, + }, + time::Duration, }; -use crate::util::debug_log; +use base64::engine::{general_purpose::STANDARD, Engine as _}; +use win_webauthn::{ + plugin::{ + PluginAddAuthenticatorOptions, PluginAuthenticator, PluginCancelOperationRequest, + PluginGetAssertionRequest, PluginLockStatus, PluginMakeCredentialRequest, WebAuthnPlugin, + }, + AuthenticatorInfo, CtapVersion, PublicKeyCredentialParameters, +}; +use windows_core::GUID; + +use crate::ipc2::{ConnectionStatus, TimedCallback, WindowsProviderClient}; + +// Re-export main functionality +pub use types::UserVerificationRequirement; + +const AUTHENTICATOR_NAME: &str = "Bitwarden Desktop"; +const RPID: &str = "bitwarden.com"; +const CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062"; +const AAGUID: &str = "d548826e-79b4-db40-a3d8-11116f7e8349"; +const LOGO_SVG: &str = r##""##; /// Handles initialization and registration for the Bitwarden desktop app as a /// For now, also adds the authenticator pub fn register() -> std::result::Result<(), String> { - debug_log("register() called..."); + // TODO: Can we spawn a new named thread for debugging? + tracing::debug!("register() called..."); - let r = com_registration::initialize_com_library(); - debug_log(&format!("Initialized the com library: {:?}", r)); + let r = WebAuthnPlugin::initialize(); + tracing::debug!( + "Initialized the com library with WebAuthnPlugin::initialize(): {:?}", + r + ); - let r = com_registration::register_com_library(); - debug_log(&format!("Registered the com library: {:?}", r)); + let clsid = CLSID.try_into().expect("valid GUID string"); + let plugin = WebAuthnPlugin::new(clsid); - let r = com_registration::add_authenticator(); - debug_log(&format!("Added the authenticator: {:?}", r)); + let r = plugin.register_server(BitwardenPluginAuthenticator { + client: Mutex::new(None), + callbacks: Arc::new(Mutex::new(HashMap::new())), + }); + tracing::debug!("Registered the com library: {:?}", r); + + tracing::debug!("Parsing authenticator options"); + let aaguid = AAGUID + .try_into() + .map_err(|err| format!("Invalid AAGUID `{AAGUID}`: {err}"))?; + let options = PluginAddAuthenticatorOptions { + authenticator_name: AUTHENTICATOR_NAME.to_string(), + clsid, + rp_id: Some(RPID.to_string()), + light_theme_logo_svg: Some(LOGO_SVG.to_string()), + dark_theme_logo_svg: Some(LOGO_SVG.to_string()), + authenticator_info: AuthenticatorInfo { + versions: HashSet::from([CtapVersion::Fido2_0, CtapVersion::Fido2_1]), + aaguid: aaguid, + options: Some(HashSet::from([ + "rk".to_string(), + "up".to_string(), + "uv".to_string(), + ])), + transports: Some(HashSet::from([ + "internal".to_string(), + "hybrid".to_string(), + ])), + algorithms: Some(vec![PublicKeyCredentialParameters { + alg: -7, + typ: "public-key".to_string(), + }]), + }, + supported_rp_ids: None, + }; + let response = WebAuthnPlugin::add_authenticator(options); + tracing::debug!("Added the authenticator: {response:?}"); Ok(()) } + +struct BitwardenPluginAuthenticator { + /// Client to communicate with desktop app over IPC. + client: Mutex>>, + + /// Map of transaction IDs to cancellation tokens + callbacks: Arc>>>, +} + +impl BitwardenPluginAuthenticator { + fn get_client(&self) -> Arc { + tracing::debug!("Connecting to client via IPC"); + let mut client = self.client.lock().unwrap(); + match client.as_ref().map(|c| (c, c.get_connection_status())) { + Some((_, ConnectionStatus::Disconnected)) | None => { + tracing::debug!("Connecting to desktop app"); + let c = WindowsProviderClient::connect(); + tracing::debug!("Connected to client via IPC successfully"); + _ = client.insert(Arc::new(c)); + } + _ => {} + }; + client.as_ref().unwrap().clone() + } +} + +impl PluginAuthenticator for BitwardenPluginAuthenticator { + fn make_credential( + &self, + request: PluginMakeCredentialRequest, + ) -> Result, Box> { + tracing::debug!("Received MakeCredential: {request:?}"); + let client = self.get_client(); + let (cancel_tx, cancel_rx) = mpsc::channel(); + let transaction_id = request.transaction_id; + self.callbacks + .lock() + .expect("not poisoned") + .insert(transaction_id, cancel_tx); + let response = make_credential::make_credential(&client, request, cancel_rx); + self.callbacks + .lock() + .expect("not poisoned") + .remove(&transaction_id); + response + } + + fn get_assertion( + &self, + request: PluginGetAssertionRequest, + ) -> Result, Box> { + tracing::debug!("Received GetAssertion: {request:?}"); + let client = self.get_client(); + let (cancel_tx, cancel_rx) = mpsc::channel(); + let transaction_id = request.transaction_id; + self.callbacks + .lock() + .expect("not poisoned") + .insert(transaction_id, cancel_tx); + let response = assert::get_assertion(&client, request, cancel_rx); + self.callbacks + .lock() + .expect("not poisoned") + .remove(&transaction_id); + response + } + + fn cancel_operation( + &self, + request: PluginCancelOperationRequest, + ) -> Result<(), Box> { + let transaction_id = request.transaction_id(); + tracing::debug!(?transaction_id, "Received CancelOperation"); + + if let Some(cancellation_token) = self + .callbacks + .lock() + .expect("not poisoned") + .get(&request.transaction_id()) + { + _ = cancellation_token.send(()); + let client = self.get_client(); + let context = STANDARD.encode(transaction_id.to_u128().to_le_bytes().to_vec()); + tracing::debug!("Sending cancel operation for context: {context}"); + client.send_native_status("cancel-operation".to_string(), context); + } + Ok(()) + } + + fn lock_status(&self) -> Result> { + let callback = Arc::new(TimedCallback::new()); + let client = self.get_client(); + client.get_lock_status(callback.clone()); + match callback.wait_for_response(Duration::from_secs(3), None) { + Ok(Ok(response)) => { + if response.is_unlocked { + Ok(PluginLockStatus::PluginUnlocked) + } else { + Ok(PluginLockStatus::PluginLocked) + } + } + Ok(Err(err)) => Err(format!("GetLockStatus() call failed: {err}").into()), + Err(_) => Err(format!("GetLockStatus() call timed out").into()), + } + } +} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/make_credential.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/make_credential.rs index fa0a027c47f..f6dc17c4c5f 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/make_credential.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/make_credential.rs @@ -1,502 +1,233 @@ use serde_json; -use std::alloc::{alloc, Layout}; -use std::ptr; -use windows_core::{s, HRESULT}; +use std::collections::HashMap; +use std::sync::{mpsc::Receiver, Arc}; +use std::time::Duration; -use crate::com_provider::{ - parse_credential_list, ExperimentalWebAuthnPluginOperationRequest, - ExperimentalWebAuthnPluginOperationResponse, +use win_webauthn::{ + plugin::{PluginMakeCredentialRequest, PluginMakeCredentialResponse}, + CtapTransport, }; -use crate::types::*; -use crate::util::{debug_log, delay_load, wstr_to_string}; -use crate::webauthn::WEBAUTHN_CREDENTIAL_LIST; -/// Windows WebAuthn registration request context -#[derive(Debug, Clone)] -pub struct WindowsRegistrationRequest { - pub rpid: String, - pub user_id: Vec, - pub user_name: String, - pub user_display_name: Option, - pub client_data_hash: Vec, - pub excluded_credentials: Vec>, - pub user_verification: UserVerificationRequirement, - pub supported_algorithms: Vec, -} +use crate::ipc2::CallbackError; +use crate::util::create_context_string; +use crate::{ + ipc2::{ + PasskeyRegistrationRequest, PasskeyRegistrationResponse, Position, TimedCallback, + UserVerification, WindowsProviderClient, + }, + util::HwndExt, +}; -// 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 -} +pub fn make_credential( + ipc_client: &WindowsProviderClient, + request: PluginMakeCredentialRequest, + cancellation_token: Receiver<()>, +) -> Result, Box> { + tracing::debug!("=== PluginMakeCredential() called ==="); -#[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 -} + // Extract RP information + let rp_info = request + .rp_information() + .ok_or_else(|| "RP information is null".to_string())?; -#[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 -} + let rpid = rp_info.id()?; -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct WEBAUTHN_COSE_CREDENTIAL_PARAMETERS { - pub cCredentialParameters: u32, - pub pCredentialParameters: *const WEBAUTHN_COSE_CREDENTIAL_PARAMETER, -} + // let rp_name = rp_info.name().unwrap_or_else(|| String::new()); -// 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, - pub pAuthenticatorOptions: - *const crate::webauthn::ExperimentalWebAuthnCtapCborAuthenticatorOptions, - // Add other fields as needed... -} + // Extract user information + let user = request + .user_information() + .ok_or_else(|| "User information is null".to_string())?; -pub type PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST = - *mut EXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST; + let user_handle = user + .id() + .map_err(|err| format!("User ID is required for registration: {err}"))? + .to_vec(); -// 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; + let user_name = user + .name() + .map_err(|err| format!("User name is required for registration: {err}"))?; -type EXPERIMENTAL_WebAuthNFreeDecodedMakeCredentialRequestFn = unsafe extern "stdcall" fn( - pMakeCredentialRequest: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST, -); + // let user_display_name = user.display_name(); -// RAII wrapper for decoded make credential request -pub struct DecodedMakeCredentialRequest { - ptr: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST, - free_fn: Option, -} + // Extract client data hash + let client_data_hash = request + .client_data_hash() + .map_err(|err| format!("Client data hash is required for registration: {err}"))? + .to_vec(); -impl DecodedMakeCredentialRequest { - fn new( - ptr: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST, - free_fn: Option, - ) -> Self { - Self { ptr, free_fn } - } + // Extract supported algorithms + let supported_algorithms: Vec = request + .pub_key_cred_params() + .iter() + .map(|params| params.alg()) + .collect(); - 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 { - debug_log("Freeing decoded make credential request"); - unsafe { - free_fn(self.ptr); - } - } - } - } -} - -// Function to decode make credential request using Windows API -unsafe fn decode_make_credential_request( - encoded_request: &[u8], -) -> Result { - debug_log("Attempting to decode make credential request using Windows API"); - - // Try to load the Windows API decode function - let decode_fn = match delay_load::( - s!("webauthn.dll"), - s!("EXPERIMENTAL_WebAuthNDecodeMakeCredentialRequest"), - ) { - Some(func) => func, - None => { - return Err( - "Failed to load EXPERIMENTAL_WebAuthNDecodeMakeCredentialRequest from webauthn.dll" - .to_string(), - ); - } + // Extract user verification requirement from authenticator options + let user_verification = match request + .authenticator_options() + .and_then(|opts| opts.user_verification()) + { + Some(true) => UserVerification::Required, + Some(false) => UserVerification::Discouraged, + None => UserVerification::Preferred, }; - // Try to load the free function (optional, might not be available in all versions) - let free_fn = delay_load::( - s!("webauthn.dll"), - s!("EXPERIMENTAL_WebAuthNFreeDecodedMakeCredentialRequest"), + // Extract excluded credentials from credential list + let excluded_credentials: Vec> = request + .exclude_credentials() + .iter() + .filter_map(|cred| cred.credential_id()) + .map(|id| id.to_vec()) + .collect(); + if !excluded_credentials.is_empty() { + tracing::debug!( + "Found {} excluded credentials for make credential", + excluded_credentials.len() + ); + } + + let client_window_handle = request.window_handle.0.addr().to_le_bytes().to_vec(); + let client_pos = request + .window_handle + .center_position() + .unwrap_or((640, 480)); + + let context = create_context_string(request.transaction_id); + + // Create Windows registration request + let registration_request = PasskeyRegistrationRequest { + rp_id: rpid.clone(), + user_handle: user_handle, + user_name: user_name, + // user_display_name: user_info.2, + client_data_hash, + excluded_credentials, + user_verification: user_verification, + supported_algorithms, + client_window_handle, + window_xy: Position { + x: client_pos.0, + y: client_pos.1, + }, + context, + }; + + tracing::debug!( + "Make credential request - RP: {}, User: {}", + rpid, + registration_request.user_name ); - // 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() { - debug_log(&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 let Ok(()) = cancellation_token.try_recv() { + return Err(format!("Request {:?} cancelled", request.transaction_id))?; } - if pp_make_credential_request.is_null() { - debug_log("ERROR: Windows API succeeded but returned null pointer"); - return Err("Windows API returned null pointer".to_string()); - } + // Send registration request + let passkey_response = + send_registration_request(ipc_client, registration_request, cancellation_token) + .map_err(|err| format!("Registration request failed: {err}"))?; + tracing::debug!("Registration response received: {:?}", passkey_response); - Ok(DecodedMakeCredentialRequest::new( - pp_make_credential_request, - free_fn, - )) + // Create proper WebAuthn response from passkey_response + tracing::debug!("Creating WebAuthn make credential response"); + let webauthn_response = create_make_credential_response(passkey_response.attestation_object) + .map_err(|err| format!("Failed to create WebAuthn response: {err}"))?; + tracing::debug!("Successfully created WebAuthn response: {webauthn_response:?}"); + Ok(webauthn_response) } /// Helper for registration requests fn send_registration_request( - transaction_id: &str, - request: &WindowsRegistrationRequest, -) -> Option { - debug_log(&format!("Registration request data - RP ID: {}, User ID: {} bytes, User name: {}, Client data hash: {} bytes, Algorithms: {:?}, Excluded credentials: {}", - request.rpid, request.user_id.len(), request.user_name, request.client_data_hash.len(), request.supported_algorithms, request.excluded_credentials.len())); + ipc_client: &WindowsProviderClient, + request: PasskeyRegistrationRequest, + cancellation_token: Receiver<()>, +) -> Result { + tracing::debug!("Registration request data - RP ID: {}, User ID: {} bytes, User name: {}, Client data hash: {} bytes, Algorithms: {:?}, Excluded credentials: {}", + request.rp_id, request.user_handle.len(), request.user_name, request.client_data_hash.len(), request.supported_algorithms, request.excluded_credentials.len()); - let passkey_request = PasskeyRegistrationRequest { - rp_id: request.rpid.clone(), - transaction_id: transaction_id.to_string(), - user_handle: request.user_id.clone(), - user_name: request.user_name.clone(), - client_data_hash: request.client_data_hash.clone(), - user_verification: request.user_verification.clone(), - window_xy: Position { x: 400, y: 400 }, // TODO: Get actual window position - supported_algorithms: request.supported_algorithms.clone(), - excluded_credentials: request.excluded_credentials.clone(), - }; - - match serde_json::to_string(&passkey_request) { - Ok(request_json) => { - debug_log(&format!("Sending registration request: {}", request_json)); - crate::ipc::send_passkey_request(RequestType::Registration, request_json, &request.rpid) - } - Err(e) => { - debug_log(&format!( - "ERROR: Failed to serialize registration request: {}", - e - )); - None - } + let request_json = serde_json::to_string(&request) + .map_err(|err| format!("Failed to serialize registration request: {err}"))?; + tracing::debug!("Sending registration request: {}", request_json); + let callback = Arc::new(TimedCallback::new()); + ipc_client.prepare_passkey_registration(request, callback.clone()); + // Corresponds to maximum recommended timeout for WebAuthn. + // https://www.w3.org/TR/webauthn-3/#recommended-range-and-default-for-a-webauthn-ceremony-timeout + let wait_time = Duration::from_secs(600); + let response = callback + .wait_for_response(wait_time, Some(cancellation_token)) + .map_err(|err| match err { + CallbackError::Timeout => "Registration request timed out".to_string(), + CallbackError::Cancelled => "Registration request cancelled".to_string(), + })? + .map_err(|err| err.to_string()); + if response.is_ok() { + tracing::debug!("Requesting credential sync after registering a new credential."); + ipc_client.send_native_status("request-sync".to_string(), "".to_string()); } + response } -/// Creates a WebAuthn make credential response from Bitwarden's registration response -unsafe fn create_make_credential_response( +/// Creates a CTAP make credential response from Bitwarden's WebAuthn registration response +fn create_make_credential_response( attestation_object: Vec, -) -> std::result::Result<*mut ExperimentalWebAuthnPluginOperationResponse, HRESULT> { +) -> std::result::Result, Box> { + use ciborium::Value; // Use the attestation object directly as the encoded response - let response_data = attestation_object; - let response_len = response_data.len(); + let att_obj_items = ciborium::from_reader::(&attestation_object[..]) + .map_err(|err| format!("Failed to deserialize WebAuthn attestation object: {err}"))? + .into_map() + .map_err(|_| "object is not a CBOR map".to_string())?; - // 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)); - } + let webauthn_att_obj: HashMap<&str, &Value> = att_obj_items + .iter() + .map(|(k, v)| (k.as_text().unwrap(), v)) + .collect(); - // 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::(); - 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) + let att_fmt = webauthn_att_obj + .get("fmt") + .and_then(|s| s.as_text()) + .ok_or("could not read `fmt` key as a string".to_string())? + .to_string(); + let authenticator_data = webauthn_att_obj + .get("authData") + .and_then(|d| d.as_bytes()) + .ok_or("could not read `authData` key as bytes".to_string())? + .clone(); + let attestation = PluginMakeCredentialResponse { + format_type: att_fmt, + authenticator_data: authenticator_data, + attestation_statement: None, + attestation_object: None, + credential_id: None, + extensions: None, + used_transport: CtapTransport::Internal, + ep_att: false, + large_blob_supported: false, + resident_key: true, + prf_enabled: false, + unsigned_extension_outputs: None, + hmac_secret: None, + third_party_payment: false, + transports: Some(vec![CtapTransport::Internal, CtapTransport::Hybrid]), + client_data_json: None, + registration_response_json: None, + }; + Ok(attestation.to_ctap_response()?) } -/// Implementation of EXPERIMENTAL_PluginMakeCredential moved from com_provider.rs -pub unsafe fn experimental_plugin_make_credential( - request: *const ExperimentalWebAuthnPluginOperationRequest, - response: *mut *mut ExperimentalWebAuthnPluginOperationResponse, -) -> HRESULT { - debug_log("=== EXPERIMENTAL_PluginMakeCredential() called ==="); - - if request.is_null() { - debug_log("ERROR: NULL request pointer"); - return HRESULT(-1); - } - - if response.is_null() { - debug_log("ERROR: NULL response pointer"); - return HRESULT(-1); - } - - let req = &*request; - let transaction_id = format!("{:?}", req.transaction_id); - - if req.encoded_request_byte_count == 0 || req.encoded_request_pointer.is_null() { - debug_log("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, - ); - - debug_log(&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(); - debug_log("Successfully decoded make credential request using Windows API"); - - // Extract RP information - if decoded_request.pRpInformation.is_null() { - debug_log("ERROR: RP information is null"); - return HRESULT(-1); - } - - let rp_info = &*decoded_request.pRpInformation; - - let rpid = if rp_info.pwszId.is_null() { - debug_log("ERROR: RP ID is null"); - return HRESULT(-1); - } else { - match wstr_to_string(rp_info.pwszId) { - Ok(id) => id, - Err(e) => { - debug_log(&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() { - debug_log("ERROR: User information is null"); - return HRESULT(-1); - } - - let user = &*decoded_request.pUserInformation; - - let user_id = if user.pbId.is_null() || user.cbId == 0 { - debug_log("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() { - debug_log("ERROR: User name is required for registration"); - return HRESULT(-1); - } else { - match wstr_to_string(user.pwszName) { - Ok(name) => name, - Err(_) => { - debug_log("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() - { - debug_log("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 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() - }; - - // Extract user verification requirement from authenticator options - let user_verification = if !decoded_request.pAuthenticatorOptions.is_null() { - let auth_options = &*decoded_request.pAuthenticatorOptions; - match auth_options.user_verification { - 1 => Some(UserVerificationRequirement::Required), - -1 => Some(UserVerificationRequirement::Discouraged), - 0 | _ => Some(UserVerificationRequirement::Preferred), // Default or undefined - } - } else { - None - }; - - // Extract excluded credentials from credential list - let excluded_credentials = parse_credential_list(&decoded_request.CredentialList); - if !excluded_credentials.is_empty() { - debug_log(&format!( - "Found {} excluded credentials for make credential", - excluded_credentials.len() - )); - } - - // Create Windows registration request - let registration_request = WindowsRegistrationRequest { - rpid: rpid.clone(), - user_id: user_info.0, - user_name: user_info.1, - user_display_name: user_info.2, - client_data_hash, - excluded_credentials, - user_verification: user_verification.unwrap_or_default(), - supported_algorithms, - }; - - debug_log(&format!( - "Make credential request - RP: {}, User: {}", - rpid, registration_request.user_name - )); - - // Send registration request - if let Some(passkey_response) = - send_registration_request(&transaction_id, ®istration_request) - { - debug_log(&format!( - "Registration response received: {:?}", - passkey_response - )); - - // Create proper WebAuthn response from passkey_response - match passkey_response { - PasskeyResponse::RegistrationResponse { - credential_id: _, - attestation_object, - rp_id: _, - client_data_hash: _, - } => { - debug_log("Creating WebAuthn make credential response"); - - match create_make_credential_response(attestation_object) { - Ok(webauthn_response) => { - debug_log("Successfully created WebAuthn response"); - *response = webauthn_response; - HRESULT(0) - } - Err(e) => { - debug_log(&format!( - "ERROR: Failed to create WebAuthn response: {}", - e - )); - *response = ptr::null_mut(); - HRESULT(-1) - } - } - } - PasskeyResponse::Error { message } => { - debug_log(&format!("Registration request failed: {}", message)); - *response = ptr::null_mut(); - HRESULT(-1) - } - _ => { - debug_log("ERROR: Unexpected response type for registration request"); - *response = ptr::null_mut(); - HRESULT(-1) - } - } - } else { - debug_log("ERROR: No response from registration request"); - *response = ptr::null_mut(); - HRESULT(-1) - } - } - Err(e) => { - debug_log(&format!( - "ERROR: Failed to decode make credential request: {}", - e - )); - *response = ptr::null_mut(); - HRESULT(-1) - } +#[cfg(test)] +mod tests { + use super::create_make_credential_response; + #[test] + fn test_encode_make_credential_custom() { + let webauthn_att_obj = vec![ + 163, 99, 102, 109, 116, 100, 110, 111, 110, 101, 103, 97, 116, 116, 83, 116, 109, 116, + 160, 104, 97, 117, 116, 104, 68, 97, 116, 97, 68, 1, 2, 3, 4, + ]; + let ctap_att_obj = create_make_credential_response(webauthn_att_obj).unwrap(); + println!("{ctap_att_obj:?}"); + let expected = vec![163, 1, 100, 110, 111, 110, 101, 2, 68, 1, 2, 3, 4, 3, 160]; + assert_eq!(expected, ctap_att_obj); } } diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/samples/PluginAuthenticatorImpl.cpp.sample b/apps/desktop/desktop_native/windows_plugin_authenticator/src/samples/PluginAuthenticatorImpl.cpp.sample deleted file mode 100644 index 21025834182..00000000000 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/samples/PluginAuthenticatorImpl.cpp.sample +++ /dev/null @@ -1,977 +0,0 @@ -#include "pch.h" -#include "PluginAuthenticatorImpl.h" -#include -#include -#include -#include -#include -#include -#include -#include -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 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& 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(&objLenSize), - sizeof(objLenSize), - &bytesRead, 0)); - - auto objLen = wil::make_unique_cotaskmem(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(dataBuffer.size()), 0)); - - DWORD localHashByteCount = 0; - RETURN_IF_NTSTATUS_FAILED(BCryptGetProperty( - BCRYPT_SHA256_ALG_HANDLE, - BCRYPT_HASH_LENGTH, - reinterpret_cast(&localHashByteCount), - sizeof(localHashByteCount), - &bytesRead, 0)); - - auto localHashBuffer = wil::make_unique_cotaskmem(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 curApp = winrt::Microsoft::UI::Xaml::Application::Current().as(); - 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& curApp, - HWND hWnd, - wil::shared_hmodule webauthnDll, - GUID transactionId, - PluginOperationType operationType, - std::vector 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 ppbPubKeyData = wil::make_unique_hlocal(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& ppbpackedAuthenticatorData, - std::vector& 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(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(pbPubKeyBlob.get()); - DWORD cbXCoord = pPubKeyBlobHeader->cbKey; - PBYTE pbXCoord = reinterpret_cast(&pPubKeyBlobHeader[1]); - DWORD cbYCoord = pPubKeyBlobHeader->cbKey; - PBYTE pbYCoord = pbXCoord + cbXCoord; - - // create byte span for x and y - std::span xCoord(pbXCoord, cbXCoord); - std::span yCoord(pbYCoord, cbYCoord); - - // CBOR encode the public key in this order: kty, alg, crv, x, y - std::vector 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(pbXCoord), cbXCoord, 0)); - THROW_IF_NTSTATUS_FAILED(BCryptHashData(hashHandle.get(), reinterpret_cast(pbYCoord), cbYCoord, 0)); - - DWORD cbHash = 0; - DWORD bytesRead = 0; - THROW_IF_NTSTATUS_FAILED(BCryptGetProperty( - hashHandle.get(), - BCRYPT_HASH_LENGTH, - reinterpret_cast(&cbHash), - sizeof(cbHash), - &bytesRead, - 0)); - - wil::unique_hlocal_ptr pbCredentialId = wil::make_unique_hlocal(cbHash); - THROW_IF_NTSTATUS_FAILED(BCryptFinishHash(hashHandle.get(), pbCredentialId.get(), cbHash, 0)); - - // Close the key and hash handle - hKey.reset(); - hashHandle.reset(); - - com_ptr curApp = winrt::Microsoft::UI::Xaml::Application::Current().as(); - 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(buffer.size()); // public key - } - - std::vector vPackedAuthenticatorData(cbPackedAuthenticatorData); - auto writer = buffer_writer{ vPackedAuthenticatorData }; - - auto rgbRpIdHash = writer.reserve_space>(); // 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() = 0x1d; // credential data flags of size 1 byte - - *writer.reserve_space() = 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() = 0x5d; // credential data flags of size 1 byte - - *writer.reserve_space() = 0u; // Sign count of size 4 bytes is set to 0 - - *writer.reserve_space() = GUID_NULL; // aaGuid of size 16 bytes is set to 0 - - // Retrieve credential id - WORD cbCredentialId = static_cast(cbHash); - WORD cbCredentialIdBigEndian = _byteswap_ushort(cbCredentialId); - - *writer.reserve_space() = cbCredentialIdBigEndian; // Size of credential id in unsigned big endian of size 2 bytes - - writer.add(std::span(pbCredentialId.get(), cbHash)); // Set credential id - - vCredentialIdBuffer.assign(pbCredentialId.get(), pbCredentialId.get() + cbHash); - - writer.add(std::span(buffer.data(), buffer.size())); // Set CBOR encoded public key - } - - pcbPackedAuthenticatorData = static_cast(vPackedAuthenticatorData.size()); - ppbpackedAuthenticatorData = wil::make_unique_hlocal(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 curApp = winrt::Microsoft::UI::Xaml::Application::Current().as(); - - wil::shared_hmodule webauthnDll(LoadLibraryExW(L"webauthn.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32)); - if (webauthnDll == nullptr) - { - return E_ABORT; - } - - wil::unique_cotaskmem_ptr 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 requestBuffer( - pPluginMakeCredentialRequest->pbEncodedRequest, - pPluginMakeCredentialRequest->pbEncodedRequest + pPluginMakeCredentialRequest->cbEncodedRequest); - - auto ppbPubKeyData = GetRequestSigningPubKey(); - HRESULT requestSignResult = E_FAIL; - if (!ppbPubKeyData.empty()) - { - requestSignResult = VerifySignatureHelper( - requestBuffer, - ppbPubKeyData.data(), - static_cast(ppbPubKeyData.size()), - pPluginMakeCredentialRequest->pbRequestSignature, - pPluginMakeCredentialRequest->cbRequestSignature); - } - { - std::lock_guard 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(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(&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(&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(&hWnd), - sizeof(HWND), - 0)); - - // finalize the key - THROW_IF_FAILED(NCryptFinalizeKey(hKey.get(), 0)); - - DWORD cbPackedAuthenticatorData = 0; - wil::unique_hlocal_ptr packedAuthenticatorData; - std::vector vCredentialIdBuffer; - THROW_IF_FAILED(CreateAuthenticatorData( - std::move(hKey), - pDecodedMakeCredentialRequest->cbRpId, - pDecodedMakeCredentialRequest->pbRpId, - cbPackedAuthenticatorData, - packedAuthenticatorData, - vCredentialIdBuffer)); - - auto operationResponse = wil::make_unique_cotaskmem(); - - 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(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(pDecodedMakeCredentialRequest->pUserInformation); - credentialDetails.pRpInformation = const_cast(pDecodedMakeCredentialRequest->pRpInformation); - credentialDetails.cbCredentialID = static_cast(vCredentialIdBuffer.size()); - credentialDetails.pbCredentialID = wil::make_unique_cotaskmem(vCredentialIdBuffer.size()).release(); - memcpy_s(credentialDetails.pbCredentialID, credentialDetails.cbCredentialID, vCredentialIdBuffer.data(), static_cast(vCredentialIdBuffer.size())); - if (!PluginCredentialManager::getInstance().SaveCredentialMetadataToMockDB(credentialDetails)) - { - std::lock_guard 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 curApp = winrt::Microsoft::UI::Xaml::Application::Current().as(); - if (curApp) - { - hr = winrt::to_hresult(); - std::lock_guard 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 curApp = winrt::Microsoft::UI::Xaml::Application::Current().as(); - - wil::shared_hmodule webauthnDll(LoadLibraryExW(L"webauthn.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32)); - if (webauthnDll == nullptr) - { - return E_ABORT; - } - - wil::unique_cotaskmem_ptr 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 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 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 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 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 requestBuffer( - pPluginGetAssertionRequest->pbEncodedRequest, - pPluginGetAssertionRequest->pbEncodedRequest + pPluginGetAssertionRequest->cbEncodedRequest); - - auto ppbPubKeyData = GetRequestSigningPubKey(); - HRESULT requestSignResult = E_FAIL; - if (!ppbPubKeyData.empty()) - { - requestSignResult = VerifySignatureHelper( - requestBuffer, - ppbPubKeyData.data(), - static_cast(ppbPubKeyData.size()), - pPluginGetAssertionRequest->pbRequestSignature, - pPluginGetAssertionRequest->cbRequestSignature); - } - - { - std::lock_guard 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(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 packedAuthenticatorData; - std::vector vCredentialIdBuffer; - THROW_IF_FAILED(CreateAuthenticatorData(hKey, - pDecodedAssertionRequest->cbRpId, - pDecodedAssertionRequest->pbRpId, - cbPackedAuthenticatorData, - packedAuthenticatorData, - vCredentialIdBuffer)); - - wil::unique_hlocal_ptr 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(packedAuthenticatorData.get()), cbPackedAuthenticatorData, 0)); - THROW_IF_NTSTATUS_FAILED(BCryptHashData(hashHandle.get(), const_cast(pDecodedAssertionRequest->pbClientDataHash), pDecodedAssertionRequest->cbClientDataHash, 0)); - - DWORD bytesRead = 0; - DWORD cbSignatureBuffer = 0; - THROW_IF_NTSTATUS_FAILED(BCryptGetProperty( - hashHandle.get(), - BCRYPT_HASH_LENGTH, - reinterpret_cast(&cbSignatureBuffer), - sizeof(cbSignatureBuffer), - &bytesRead, - 0)); - - wil::unique_hlocal_ptr signatureBuffer = wil::make_unique_hlocal(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(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 encodedSignature{}; - encodedSignature.push_back(0x02); // ASN integer tag - encodedSignature.push_back(static_cast(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 encodedSignature{}; - encodedSignature.push_back(0x30); // ASN sequence tag - encodedSignature.push_back(static_cast(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(encodedSignature.size()); - pbSignature.reset(); - pbSignature = wil::make_unique_hlocal(cbSignature); - THROW_HR_IF(E_UNEXPECTED, pbSignature == nullptr); - memcpy_s(pbSignature.get(), cbSignature, encodedSignature.data(), static_cast(cbSignature)); - } - - // create the response - auto operationResponse = wil::make_unique_cotaskmem(); - - auto assertionResponse = wil::make_unique_cotaskmem(); - assertionResponse->dwVersion = WEBAUTHN_ASSERTION_CURRENT_VERSION; - - // [1] Credential (optional) - assertionResponse->Credential.dwVersion = WEBAUTHN_CREDENTIAL_CURRENT_VERSION; - assertionResponse->Credential.cbId = static_cast(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(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(); - 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(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 curApp = winrt::Microsoft::UI::Xaml::Application::Current().as(); - std::lock_guard 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 curApp = winrt::Microsoft::UI::Xaml::Application::Current().as(); - 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()->QueryInterface(iid, result); - } - catch (...) - { - return winrt::to_hresult(); - } - } - - HRESULT __stdcall ContosoPluginFactory::LockServer(BOOL) noexcept - { - return S_OK; - } - -} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/samples/PluginRegistrationManager.cpp.sample b/apps/desktop/desktop_native/windows_plugin_authenticator/src/samples/PluginRegistrationManager.cpp.sample deleted file mode 100644 index c5a5a52bfa5..00000000000 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/samples/PluginRegistrationManager.cpp.sample +++ /dev/null @@ -1,126 +0,0 @@ -#include "pch.h" -#include "MainPage.xaml.h" -#include "PluginRegistrationManager.h" -#include - -namespace winrt::PasskeyManager::implementation { - PluginRegistrationManager::PluginRegistrationManager() : - m_pluginRegistered(false), - m_initialized(false), - m_pluginState(EXPERIMENTAL_PLUGIN_AUTHENTICATOR_STATE::PluginAuthenticatorState_Unknown) - { - Initialize(); - m_webAuthnDll.reset(LoadLibraryExW(L"webauthn.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32)); - } - - PluginRegistrationManager::~PluginRegistrationManager() - { - } - - HRESULT PluginRegistrationManager::Initialize() - { - HRESULT hr = RefreshPluginState(); - RETURN_HR_IF_EXPECTED(S_OK, RefreshPluginState() == NTE_NOT_FOUND); - RETURN_HR(hr); - } - - HRESULT PluginRegistrationManager::RegisterPlugin() - { - // Get the function pointer of WebAuthNPluginAddAuthenticator - auto webAuthNPluginAddAuthenticator = GetProcAddressByFunctionDeclaration( - m_webAuthnDll.get(), - EXPERIMENTAL_WebAuthNPluginAddAuthenticator); - RETURN_HR_IF_NULL(E_FAIL, webAuthNPluginAddAuthenticator); - - /* - * This section creates a sample authenticatorInfo blob to include in the registration - * request. This blob must CBOR encoded using the format defined - * in https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorGetInfo - * - * 'AAGUID' maybe used to fetch information about the authenticator from the FIDO Metadata Service and other sources. - * Refer: https://fidoalliance.org/metadata/ - * - * 'extensions' field is used to perform feature detection on the authenticator - * and maybe used to determine if the authenticator is filtered out. - */ - std::string tempAaguidStr{ c_pluginAaguid }; - tempAaguidStr.erase(std::remove(tempAaguidStr.begin(), tempAaguidStr.end(), L'-'), tempAaguidStr.end()); - std::transform(tempAaguidStr.begin(), tempAaguidStr.end(), tempAaguidStr.begin(), [](unsigned char c) { return static_cast(std::toupper(c)); }); - // The following hex strings represent the encoding of - // {1: ["FIDO_2_0", "FIDO_2_1"], 2: ["prf", "hmac-secret"], 3: h'/* AAGUID */', 4: {"rk": true, "up": true, "uv": true}, - // 9: ["internal"], 10: [{"alg": -7, "type": "public-key"}]} - std::string authenticatorInfoStrPart1 = "A60182684649444F5F325F30684649444F5F325F310282637072666B686D61632D7365637265740350"; - std::string authenticatorInfoStrPart2 = "04A362726BF5627570F5627576F5098168696E7465726E616C0A81A263616C672664747970656A7075626C69632D6B6579"; - std::string fullAuthenticatorInfoStr = authenticatorInfoStrPart1 + tempAaguidStr + authenticatorInfoStrPart2; - std::vector authenticatorInfo = hexStringToBytes(fullAuthenticatorInfoStr); - - // Validate that c_pluginClsid is a valid CLSID - CLSID CLSID_ContosoPluginAuthenticator; - RETURN_IF_FAILED(CLSIDFromString(c_pluginClsid, &CLSID_ContosoPluginAuthenticator)); - - EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS addOptions{}; - addOptions.pwszAuthenticatorName = c_pluginName; - addOptions.pwszPluginRpId = c_pluginRpId; - addOptions.pwszPluginClsId = c_pluginClsid; - addOptions.pbAuthenticatorInfo = authenticatorInfo.data(); - addOptions.cbAuthenticatorInfo = static_cast(authenticatorInfo.size()); - - EXPERIMENTAL_PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE addResponse; - RETURN_IF_FAILED(webAuthNPluginAddAuthenticator(&addOptions, &addResponse)); - - // The response from plugin contains the public key used to sign plugin operation requests. Stash it for later use. - wil::unique_hkey hKey; - RETURN_IF_WIN32_ERROR(RegCreateKeyEx( - HKEY_CURRENT_USER, - c_pluginRegistryPath, - 0, - nullptr, - REG_OPTION_NON_VOLATILE, - KEY_WRITE, - nullptr, - &hKey, - nullptr)); - - RETURN_IF_WIN32_ERROR(RegSetValueEx( - hKey.get(), - c_windowsPluginRequestSigningKeyRegKeyName, - 0, - REG_BINARY, - addResponse->pbOpSignPubKey, - addResponse->cbOpSignPubKey)); - return S_OK; - } - - HRESULT PluginRegistrationManager::UnregisterPlugin() - { - // Get the function pointer of WebAuthNPluginRemoveAuthenticator - auto webAuthNPluginRemoveAuthenticator = GetProcAddressByFunctionDeclaration( - m_webAuthnDll.get(), - EXPERIMENTAL_WebAuthNPluginRemoveAuthenticator); - RETURN_HR_IF_NULL(E_FAIL, webAuthNPluginRemoveAuthenticator); - - RETURN_HR(webAuthNPluginRemoveAuthenticator(c_pluginClsid)); - } - - HRESULT PluginRegistrationManager::RefreshPluginState() - { - // Reset the plugin state and registration status - m_pluginRegistered = false; - m_pluginState = EXPERIMENTAL_PLUGIN_AUTHENTICATOR_STATE::PluginAuthenticatorState_Unknown; - - // Get handle to EXPERIMENTAL_WebAuthNPluginGetAuthenticatorState which takes in a GUID and returns EXPERIMENTAL_PLUGIN_AUTHENTICATOR_STATE - auto webAuthNPluginGetAuthenticatorState = GetProcAddressByFunctionDeclaration( - m_webAuthnDll.get(), - EXPERIMENTAL_WebAuthNPluginGetAuthenticatorState); - RETURN_HR_IF_NULL(E_FAIL, webAuthNPluginGetAuthenticatorState); - - // Get the plugin state - EXPERIMENTAL_PLUGIN_AUTHENTICATOR_STATE localPluginState; - RETURN_IF_FAILED(webAuthNPluginGetAuthenticatorState(c_pluginClsid, &localPluginState)); - - // If the EXPERIMENTAL_WebAuthNPluginGetAuthenticatorState function succeeded, that indicates the plugin is registered and localPluginState is the valid plugin state - m_pluginRegistered = true; - m_pluginState = localPluginState; - return S_OK; - } -} \ No newline at end of file diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/samples/PluginRegistrationManager.h.sample b/apps/desktop/desktop_native/windows_plugin_authenticator/src/samples/PluginRegistrationManager.h.sample deleted file mode 100644 index df0d3b6949b..00000000000 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/samples/PluginRegistrationManager.h.sample +++ /dev/null @@ -1,80 +0,0 @@ -#pragma once -#include "pch.h" -#include -#include -#include -#include -#include - -constexpr wchar_t c_pluginName[] = L"Contoso Passkey Manager"; -constexpr wchar_t c_pluginRpId[] = L"contoso.com"; - -/* The AAGUID is a unique identifier for the FIDO authenticator model. -*'AAGUID' maybe used to fetch information about the authenticator from the FIDO Metadata Service and other sources. -* Refer: https://fidoalliance.org/metadata/ -*/ -constexpr char c_pluginAaguid[] = "########-####-####-####-############"; -static_assert(c_pluginAaguid[1] != '#', "Please replace the ##### above with your AAGUID or a value you generated by running guidgen"); - -/* Generate a GUID using guidgen and replace below and in Package.appxmanifest file */ -constexpr wchar_t c_pluginClsid[] = L"{########-####-####-####-############}"; -static_assert(c_pluginClsid[1] != '#', "Please replace the ##### above with a GUID you generated by running guidgen"); - - -constexpr wchar_t c_pluginSigningKeyName[] = L"TestAppPluginIdKey"; -constexpr wchar_t c_pluginRegistryPath[] = L"Software\\Contoso\\PasskeyManager"; -constexpr wchar_t c_windowsPluginRequestSigningKeyRegKeyName[] = L"RequestSigningKeyBlob"; -constexpr wchar_t c_windowsPluginVaultLockedRegKeyName[] = L"VaultLocked"; -constexpr wchar_t c_windowsPluginSilentOperationRegKeyName[] = L"SilentOperation"; -constexpr wchar_t c_windowsPluginDBUpdateInd[] = L"SilentOperation"; - -namespace winrt::PasskeyManager::implementation -{ - class PluginRegistrationManager - { - public: - static PluginRegistrationManager& getInstance() - { - static PluginRegistrationManager instance; - return instance; - } - - // Initialize function which calls GetPluginState to check if the plugin is already registered - HRESULT Initialize(); - - HRESULT RegisterPlugin(); - HRESULT UnregisterPlugin(); - - HRESULT RefreshPluginState(); - - bool IsPluginRegistered() const - { - return m_pluginRegistered; - } - - EXPERIMENTAL_PLUGIN_AUTHENTICATOR_STATE GetPluginState() const - { - return m_pluginState; - } - - private: - EXPERIMENTAL_PLUGIN_AUTHENTICATOR_STATE m_pluginState; - bool m_initialized = false; - bool m_pluginRegistered = false; - wil::unique_hmodule m_webAuthnDll; - - PluginRegistrationManager(); - ~PluginRegistrationManager(); - PluginRegistrationManager(const PluginRegistrationManager&) = delete; - PluginRegistrationManager& operator=(const PluginRegistrationManager&) = delete; - - void UpdatePasskeyOperationStatusText(hstring const& statusText) - { - com_ptr curApp = winrt::Microsoft::UI::Xaml::Application::Current().as(); - curApp->GetDispatcherQueue().TryEnqueue([curApp, statusText]() - { - curApp->m_window.Content().try_as().Content().try_as()->UpdatePasskeyOperationStatusText(statusText); - }); - } - }; -}; \ No newline at end of file diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/samples/pluginauthenticator.h.sample b/apps/desktop/desktop_native/windows_plugin_authenticator/src/samples/pluginauthenticator.h.sample deleted file mode 100644 index 3e5bfcb80c9..00000000000 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/samples/pluginauthenticator.h.sample +++ /dev/null @@ -1,239 +0,0 @@ - - -/* 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 version is high enough to compile this file*/ -#ifndef __REQUIRED_RPCNDR_H_VERSION__ -#define __REQUIRED_RPCNDR_H_VERSION__ 501 -#endif - -/* verify that the 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 -#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 - - diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/sync.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/sync.rs deleted file mode 100644 index e42f9f65156..00000000000 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/sync.rs +++ /dev/null @@ -1,248 +0,0 @@ -use hex; -use serde_json; - -use crate::ipc::send_passkey_request; -use crate::types::*; -use crate::util::{debug_log, wstr_to_string}; -use crate::webauthn::*; - -/// Helper for sync requests - requests credentials from Electron for a specific RP ID -pub fn send_sync_request(rpid: &str) -> Option { - debug_log(&format!( - "[SYNC] send_sync_request called for RP ID: {}", - rpid - )); - - let request = PasskeySyncRequest { - rp_id: rpid.to_string(), - }; - - debug_log(&format!("[SYNC] Created sync request for RP ID: {}", rpid)); - - match serde_json::to_string(&request) { - Ok(request_json) => { - debug_log(&format!( - "[SYNC] Serialized sync request to JSON: {}", - request_json - )); - debug_log(&format!("[SYNC] Sending sync request to Electron via IPC")); - let response = send_passkey_request(RequestType::Sync, request_json, rpid); - match &response { - Some(resp) => debug_log(&format!( - "[SYNC] Received response from Electron: {:?}", - resp - )), - None => debug_log("[SYNC] No response received from Electron"), - } - response - } - Err(e) => { - debug_log(&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, - plugin_clsid: &str, -) -> Result<(), String> { - debug_log(&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() { - debug_log("[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_handle.len() > 16 { - format!("{}...", hex::encode(&cred.user_handle[..16])) - } else { - hex::encode(&cred.user_handle) - }; - - debug_log(&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_handle.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_handle.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); - debug_log(&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 - debug_log("Attempting to remove all existing credentials before sync..."); - match remove_all_credentials(formatted_clsid.clone()) { - Ok(()) => { - debug_log("Successfully removed existing credentials"); - } - Err(e) if e.contains("can't be loaded") => { - debug_log("RemoveAllCredentials function not available - this is expected for some Windows versions"); - // This is fine, the function might not exist in all versions - } - Err(e) => { - debug_log(&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() { - debug_log("No credentials to add to Windows - sync completed successfully"); - Ok(()) - } else { - debug_log("Adding new credentials to Windows..."); - match add_credentials(credentials_list) { - Ok(()) => { - debug_log("Successfully synced credentials to Windows"); - Ok(()) - } - Err(e) => { - debug_log(&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, String> { - debug_log(&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)) => { - debug_log(&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 - 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_handle: user_id, - }; - - debug_log(&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) => { - debug_log("No credentials found in Windows"); - Ok(Vec::new()) - } - Err(e) => { - debug_log(&format!( - "ERROR: Failed to get credentials from Windows: {}", - e - )); - Err(e) - } - } -} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/types.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/types.rs index 4de3eb44900..f64a73478f8 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/types.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/types.rs @@ -1,6 +1,3 @@ -use serde::{Deserialize, Serialize}; -use tokio::sync::oneshot; - /// User verification requirement as defined by WebAuthn spec #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "lowercase")] @@ -36,107 +33,3 @@ impl Into for UserVerificationRequirement { } } } - -/// IDENTICAL to napi/lib.rs/PasskeyAssertionRequest -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PasskeyAssertionRequest { - pub rp_id: String, - pub client_data_hash: Vec, - pub user_verification: UserVerificationRequirement, - pub allowed_credentials: Vec>, - pub window_xy: Position, - - pub transaction_id: String, -} - -// Identical to napi/lib.rs/Position -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Position { - pub x: i32, - pub y: i32, -} - -/// IDENTICAL to napi/lib.rs/PasskeyRegistrationRequest -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PasskeyRegistrationRequest { - pub rp_id: String, - pub user_name: String, - pub user_handle: Vec, - pub client_data_hash: Vec, - pub user_verification: UserVerificationRequirement, - pub supported_algorithms: Vec, - pub window_xy: Position, - pub excluded_credentials: Vec>, - - pub transaction_id: String, -} - -/// 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 { - rp_id: String, - user_handle: Vec, - signature: Vec, - client_data_hash: Vec, - authenticator_data: Vec, - credential_id: Vec, - }, - #[serde(rename = "registration_response", rename_all = "camelCase")] - RegistrationResponse { - rp_id: String, - client_data_hash: Vec, - credential_id: Vec, - attestation_object: Vec, - }, - #[serde(rename = "sync_response", rename_all = "camelCase")] - SyncResponse { credentials: Vec }, - #[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, - pub rp_id: String, - pub user_name: String, - pub user_handle: Vec, -} - -/// 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, -} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/util.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/util.rs index 035d9df06cf..3e7f5df5f7c 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/util.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/util.rs @@ -1,101 +1,46 @@ -use std::fs::{create_dir_all, OpenOptions}; -use std::io::Write; -use std::path::Path; -use std::time::{SystemTime, UNIX_EPOCH}; +use base64::engine::{general_purpose::STANDARD, Engine as _}; +use windows::{ + core::GUID, + Win32::{ + Foundation::*, + UI::{HiDpi::GetDpiForWindow, WindowsAndMessaging::GetWindowRect}, + }, +}; -use windows::Win32::Foundation::*; -use windows::Win32::System::LibraryLoader::*; -use windows_core::*; +const BASE_DPI: u32 = 96; -use crate::com_buffer::ComBuffer; - -pub unsafe fn delay_load(library: PCSTR, function: PCSTR) -> Option { - 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 HwndExt { + fn center_position(&self) -> windows::core::Result<(i32, i32)>; } -/// Trait for converting strings to Windows-compatible wide strings using COM allocation -pub trait WindowsString { - /// Converts to null-terminated UTF-16 using COM allocation - fn to_com_utf16(&self) -> (*mut u16, u32); - /// Converts to Vec for temporary use (caller must keep Vec alive) - fn to_utf16(&self) -> Vec; -} +impl HwndExt for HWND { + fn center_position(&self) -> windows::core::Result<(i32, i32)> { + let mut window: RECT = RECT::default(); + unsafe { + GetWindowRect(*self, &mut window)?; -impl WindowsString for str { - fn to_com_utf16(&self) -> (*mut u16, u32) { - let mut wide_vec: Vec = self.encode_utf16().collect(); - wide_vec.push(0); // null terminator - let wide_bytes: Vec = wide_vec.iter().flat_map(|&x| x.to_le_bytes()).collect(); - let (ptr, byte_count) = ComBuffer::from_buffer(&wide_bytes); - (ptr as *mut u16, byte_count) - } + // Calculate center in physical pixels + let center = ( + (window.right + window.left) / 2, + (window.bottom + window.top) / 2, + ); - fn to_utf16(&self) -> Vec { - let mut wide_vec: Vec = self.encode_utf16().collect(); - wide_vec.push(0); // null terminator - wide_vec - } -} - -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) + // Convert from physical to logical pixels + let dpi = GetDpiForWindow(*self); + if dpi == BASE_DPI { + return Ok(center); } - Err(_) => "??:??:??.???".to_string(), - }; + let scaling_factor: f64 = dpi as f64 / 96.0; + let scaled_center = ( + center.0 as f64 / scaling_factor, + center.1 as f64 / scaling_factor, + ); - let _ = writeln!(file, "[{}] {}", timestamp, msg); + Ok((scaled_center.0 as i32, scaled_center.1 as i32)) + } } } -pub fn debug_log(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 { - 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) +pub fn create_context_string(transaction_id: GUID) -> String { + STANDARD.encode(transaction_id.to_u128().to_le_bytes().to_vec()) } diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/webauthn.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/webauthn.rs deleted file mode 100644 index 5fd8fcc7832..00000000000 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/webauthn.rs +++ /dev/null @@ -1,338 +0,0 @@ -/* - This file exposes safe functions and types for interacting with the experimental - Windows WebAuthn API defined here: - - https://github.com/microsoft/webauthn/blob/master/experimental/webauthn.h -*/ - -use windows_core::*; - -use crate::util::{debug_log, delay_load, WindowsString}; -use crate::com_buffer::ComBuffer; - -/// Windows WebAuthn Authenticator Options structure -/// Header File Name: _EXPERIMENTAL_WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct ExperimentalWebAuthnCtapCborAuthenticatorOptions { - pub version: u32, // DWORD dwVersion - pub user_presence: i32, // LONG lUp: +1=TRUE, 0=Not defined, -1=FALSE - pub user_verification: i32, // LONG lUv: +1=TRUE, 0=Not defined, -1=FALSE - pub require_resident_key: i32, // LONG lRequireResidentKey: +1=TRUE, 0=Not defined, -1=FALSE -} - -/// Used when adding a Windows plugin authenticator. -/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS -/// Header File Usage: EXPERIMENTAL_WebAuthNPluginAddAuthenticator() -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct ExperimentalWebAuthnPluginAddAuthenticatorOptions { - pub authenticator_name: *const u16, - pub plugin_clsid: *const u16, - pub rpid: *const u16, - pub light_theme_logo: *const u16, - pub dark_theme_logo: *const u16, - pub cbor_authenticator_info_byte_count: u32, - pub cbor_authenticator_info: *const u8, -} - -/// Used as a response type when adding a Windows plugin authenticator. -/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE -/// Header File Usage: EXPERIMENTAL_WebAuthNPluginAddAuthenticator() -/// EXPERIMENTAL_WebAuthNPluginFreeAddAuthenticatorResponse() -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct ExperimentalWebAuthnPluginAddAuthenticatorResponse { - pub plugin_operation_signing_key_byte_count: u32, - pub plugin_operation_signing_key: *mut u8, -} - -/// Represents a credential. -/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS -/// Header File Usage: _EXPERIMENTAL_WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS_LIST -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct ExperimentalWebAuthnPluginCredentialDetails { - pub credential_id_byte_count: u32, - pub credential_id_pointer: *mut u8, - pub rpid: *mut u16, - pub rp_friendly_name: *mut u16, - pub user_id_byte_count: u32, - 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, -} - -impl ExperimentalWebAuthnPluginCredentialDetails { - pub fn create_from_bytes( - credential_id: Vec, - rpid: String, - rp_friendly_name: String, - user_id: Vec, - user_name: String, - user_display_name: String, - ) -> Self { - // 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 using trait methods - let (rpid_ptr, _) = rpid.to_com_utf16(); - let (rp_friendly_name_ptr, _) = rp_friendly_name.to_com_utf16(); - let (user_name_ptr, _) = user_name.to_com_utf16(); - let (user_display_name_ptr, _) = user_display_name.to_com_utf16(); - - Self { - credential_id_byte_count, - credential_id_pointer, - rpid: rpid_ptr, - rp_friendly_name: rp_friendly_name_ptr, - user_id_byte_count, - user_id_pointer, - user_name: user_name_ptr, - user_display_name: user_display_name_ptr, - } - } -} - -/// Represents a list of credentials. -/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS_LIST -/// Header File Usage: EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials() -/// EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveCredentials() -/// EXPERIMENTAL_WebAuthNPluginAuthenticatorGetAllCredentials() -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct ExperimentalWebAuthnPluginCredentialDetailsList { - pub plugin_clsid: *mut u16, - pub credential_count: u32, - pub credentials: *mut *mut ExperimentalWebAuthnPluginCredentialDetails, -} - -impl ExperimentalWebAuthnPluginCredentialDetailsList { - pub fn create( - clsid: String, - credentials: Vec, - ) -> Self { - // Convert credentials to COM-allocated pointers - let credential_pointers: Vec<*mut ExperimentalWebAuthnPluginCredentialDetails> = credentials - .into_iter() - .map(|cred| { - // Use COM allocation for each credential struct - ComBuffer::with_object(cred) - }) - .collect(); - - 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 using trait method - let (clsid_ptr, _) = clsid.to_com_utf16(); - - Self { - plugin_clsid: clsid_ptr, - credential_count: credentials_len as u32, - credentials: credentials_pointer, - } - } -} - -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> { - debug_log("Loading EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials function..."); - - let result = unsafe { - delay_load::( - s!("webauthn.dll"), - s!("EXPERIMENTAL_WebAuthNPluginAuthenticatorAddCredentials"), - ) - }; - - match result { - Some(api) => { - debug_log("Function loaded successfully, calling API..."); - debug_log(&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; - debug_log(&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() - )); - } - - debug_log("API call succeeded"); - Ok(()) - }, - None => { - debug_log("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::( - 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_WebAuthNPluginAuthenticatorRemoveCredentials()\n{}", - result.message() - )); - } - - Ok(()) - }, - None => { - 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, String> { - let result = unsafe { - delay_load::( - s!("webauthn.dll"), - s!("EXPERIMENTAL_WebAuthNPluginAuthenticatorGetAllCredentials"), - ) - }; - - match result { - Some(api) => { - // Create the wide string and keep it alive during the API call - let clsid_wide = plugin_clsid.to_utf16(); - 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> { - debug_log("Loading EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentials function..."); - - let result = unsafe { - delay_load::( - s!("webauthn.dll"), - s!("EXPERIMENTAL_WebAuthNPluginAuthenticatorRemoveAllCredentials"), - ) - }; - - match result { - Some(api) => { - debug_log("Function loaded successfully, calling API..."); - // Create the wide string and keep it alive during the API call - let clsid_wide = plugin_clsid.to_utf16(); - - let result = unsafe { api(clsid_wide.as_ptr()) }; - - if result.is_err() { - let error_code = result.0; - debug_log(&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() - )); - } - - debug_log("API call succeeded"); - Ok(()) - }, - None => { - debug_log("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.")) - } - } -} - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct WEBAUTHN_CREDENTIAL_EX { - pub dwVersion: u32, - pub cbId: u32, - pub pbId: *const u8, - pub pwszCredentialType: *const u16, // LPCWSTR - pub dwTransports: u32, -} - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct WEBAUTHN_CREDENTIAL_LIST { - pub cCredentials: u32, - pub ppCredentials: *const *const WEBAUTHN_CREDENTIAL_EX, -} diff --git a/apps/desktop/electron-builder.beta.json b/apps/desktop/electron-builder.beta.json index 630a956560d..2673dd213a3 100644 --- a/apps/desktop/electron-builder.beta.json +++ b/apps/desktop/electron-builder.beta.json @@ -13,14 +13,13 @@ }, "afterSign": "scripts/after-sign.js", "afterPack": "scripts/after-pack.js", - "asarUnpack": ["**/*.node"], "files": [ - "**/*", - "!**/node_modules/@bitwarden/desktop-napi/**/*", - "**/node_modules/@bitwarden/desktop-napi/index.js", - "**/node_modules/@bitwarden/desktop-napi/desktop_napi.${platform}-${arch}*.node" + "!node_modules/@bitwarden/desktop-napi/src", + "!node_modules/@bitwarden/desktop-napi/Cargo.toml", + "!node_modules/@bitwarden/desktop-napi/build.rs", + "!node_modules/@bitwarden/desktop-napi/package.json" ], - "electronVersion": "36.8.1", + "electronVersion": "36.9.3", "generateUpdatesFilesForAllChannels": true, "publish": { "provider": "generic", @@ -58,9 +57,10 @@ "appx": { "artifactName": "Bitwarden-Beta-${version}-${arch}.${ext}", "backgroundColor": "#175DDC", + "customManifestPath": "./custom-appx-manifest.xml", "applicationId": "BitwardenBeta", "identityName": "8bitSolutionsLLC.BitwardenBeta", - "publisher": "CN=14D52771-DE3C-4886-B8BF-825BA7690418", + "publisher": "CN=Bitwarden Inc., O=Bitwarden Inc., L=Santa Barbara, S=California, C=US, SERIALNUMBER=7654941, OID.2.5.4.15=Private Organization, OID.1.3.6.1.4.1.311.60.2.1.2=Delaware, OID.1.3.6.1.4.1.311.60.2.1.3=US", "publisherDisplayName": "Bitwarden Inc", "languages": [ "en-US", diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 285ee91b039..e619da007eb 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -15,12 +15,11 @@ }, "afterSign": "scripts/after-sign.js", "afterPack": "scripts/after-pack.js", - "asarUnpack": ["**/*.node"], "files": [ - "**/*", - "!**/node_modules/@bitwarden/desktop-napi/**/*", - "**/node_modules/@bitwarden/desktop-napi/index.js", - "**/node_modules/@bitwarden/desktop-napi/desktop_napi.${platform}-${arch}*.node" + "!node_modules/@bitwarden/desktop-napi/src", + "!node_modules/@bitwarden/desktop-napi/Cargo.toml", + "!node_modules/@bitwarden/desktop-napi/build.rs", + "!node_modules/@bitwarden/desktop-napi/package.json" ], "electronVersion": "36.9.3", "generateUpdatesFilesForAllChannels": true, @@ -90,7 +89,7 @@ }, "win": { "electronUpdaterCompatibility": ">=0.0.1", - "target": ["appx"], + "target": ["portable", "nsis-web", "appx"], "signtoolOptions": { "sign": "./sign.js", "publisherName": "CN=com.bitwarden.localdevelopment" @@ -178,7 +177,7 @@ "customManifestPath": "./custom-appx-manifest.xml", "applicationId": "bitwardendesktop", "identityName": "8bitSolutionsLLC.bitwardendesktop", - "publisher": "CN=14D52771-DE3C-4886-B8BF-825BA7690418", + "publisher": "CN=Bitwarden Inc., O=Bitwarden Inc., L=Santa Barbara, S=California, C=US, SERIALNUMBER=7654941, OID.2.5.4.15=Private Organization, OID.1.3.6.1.4.1.311.60.2.1.2=Delaware, OID.1.3.6.1.4.1.311.60.2.1.3=US", "publisherDisplayName": "Bitwarden Inc", "languages": [ "en-US", diff --git a/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib b/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib index 1e47cc54de2..132882c6477 100644 --- a/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib +++ b/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib @@ -8,63 +8,56 @@ + + + + + diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index 5568b2e75db..ec4f1dcb543 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -11,63 +11,138 @@ import os class CredentialProviderViewController: ASCredentialProviderViewController { let logger: Logger - // There is something a bit strange about the initialization/deinitialization in this class. - // Sometimes deinit won't be called after a request has successfully finished, - // which would leave this class hanging in memory and the IPC connection open. - // - // If instead I make this a static, the deinit gets called correctly after each request. - // I think we still might want a static regardless, to be able to reuse the connection if possible. - let client: MacOsProviderClient = { - let logger = Logger(subsystem: "com.bitwarden.desktop.autofill-extension", category: "credential-provider") + @IBOutlet weak var statusLabel: NSTextField! + @IBOutlet weak var logoImageView: NSImageView! + + // The IPC client to communicate with the Bitwarden desktop app + private var client: MacOsProviderClient? + + // Timer for checking connection status + private var connectionMonitorTimer: Timer? + private var lastConnectionStatus: ConnectionStatus = .disconnected + + // We changed the getClient method to be async, here's why: + // This is so that we can check if the app is running, and launch it, without blocking the main thread + // Blocking the main thread caused MacOS layouting to 'fail' or at least be very delayed, which caused our getWindowPositioning code to sent 0,0. + // We also properly retry the IPC connection which sometimes would take some time to be up and running, depending on CPU load, phase of jupiters moon, etc. + private func getClient() async -> MacOsProviderClient { + if let client = self.client { + return client + } + let logger = Logger(subsystem: "com.bitwarden.desktop.autofill-extension", category: "credential-provider") + // Check if the Electron app is running let workspace = NSWorkspace.shared let isRunning = workspace.runningApplications.contains { app in app.bundleIdentifier == "com.bitwarden.desktop" } - + if !isRunning { - logger.log("[autofill-extension] Bitwarden Desktop not running, attempting to launch") - - // Try to launch the app + logger.log("[autofill-extension] Bitwarden Desktop not running, attempting to launch") + + // Launch the app and wait for it to be ready if let appURL = workspace.urlForApplication(withBundleIdentifier: "com.bitwarden.desktop") { - let semaphore = DispatchSemaphore(value: 0) - - workspace.openApplication(at: appURL, - configuration: NSWorkspace.OpenConfiguration()) { app, error in - if let error = error { - logger.error("[autofill-extension] Failed to launch Bitwarden Desktop: \(error.localizedDescription)") - } else if let app = app { - logger.log("[autofill-extension] Successfully launched Bitwarden Desktop") - } else { - logger.error("[autofill-extension] Failed to launch Bitwarden Desktop: unknown error") + await withCheckedContinuation { continuation in + workspace.openApplication(at: appURL, configuration: NSWorkspace.OpenConfiguration()) { app, error in + if let error = error { + logger.error("[autofill-extension] Failed to launch Bitwarden Desktop: \(error.localizedDescription)") + } else { + logger.log("[autofill-extension] Successfully launched Bitwarden Desktop") + } + continuation.resume() } - semaphore.signal() } - - // Wait for launch completion with timeout - _ = semaphore.wait(timeout: .now() + 5.0) - - // Add a small delay to allow for initialization - Thread.sleep(forTimeInterval: 1.0) - } else { - logger.error("[autofill-extension] Could not find Bitwarden Desktop app") } - } else { - logger.log("[autofill-extension] Bitwarden Desktop is running") + } + + logger.log("[autofill-extension] Connecting to Bitwarden over IPC") + + // Retry connecting to the Bitwarden IPC with an increasing delay + let maxRetries = 20 + let delayMs = 500 + var newClient: MacOsProviderClient? + + for attempt in 1...maxRetries { + logger.log("[autofill-extension] Connection attempt \(attempt)") + + // Create a new client instance for each retry + newClient = MacOsProviderClient.connect() + try? await Task.sleep(nanoseconds: UInt64(100 * attempt + (delayMs * 1_000_000))) // Convert ms to nanoseconds + let connectionStatus = newClient!.getConnectionStatus() + + logger.log("[autofill-extension] Connection attempt \(attempt), status: \(connectionStatus == .connected ? "connected" : "disconnected")") + + if connectionStatus == .connected { + logger.log("[autofill-extension] Successfully connected to Bitwarden (attempt \(attempt))") + break + } else { + if attempt < maxRetries { + logger.log("[autofill-extension] Retrying connection") + } else { + logger.error("[autofill-extension] Failed to connect after \(maxRetries) attempts, final status: \(connectionStatus == .connected ? "connected" : "disconnected")") + } + } } - logger.log("[autofill-extension] Connecting to Bitwarden over IPC") - - return MacOsProviderClient.connect() - }() + self.client = newClient + return newClient! + } + + // Setup the connection monitoring timer + private func setupConnectionMonitoring() { + // Check connection status every 1 second + connectionMonitorTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in + self?.checkConnectionStatus() + } + + // Make sure timer runs even when UI is busy + RunLoop.current.add(connectionMonitorTimer!, forMode: .common) + + // Initial check + checkConnectionStatus() + } + + // Check the connection status by calling into Rust + // If the connection is has changed and is now disconnected, cancel the request + private func checkConnectionStatus() { + // Only check connection status if the client has been initialized. + // Initialization is done asynchronously, so we might be called before it's ready + // In that case we just skip this check and wait for the next timer tick and re-check + guard let client = self.client else { + return + } + + // Get the current connection status from Rust + let currentStatus = client.getConnectionStatus() + + // Only post notification if state changed + if currentStatus != lastConnectionStatus { + if(currentStatus == .connected) { + logger.log("[autofill-extension] Connection status changed: Connected") + } else { + logger.log("[autofill-extension] Connection status changed: Disconnected") + } + + // Save the new status + lastConnectionStatus = currentStatus + + // If we just disconnected, try to cancel the request + if currentStatus == .disconnected { + self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Bitwarden desktop app disconnected")) + } + } + } init() { logger = Logger(subsystem: "com.bitwarden.desktop.autofill-extension", category: "credential-provider") logger.log("[autofill-extension] initializing extension") - super.init(nibName: nil, bundle: nil) + super.init(nibName: "CredentialProviderViewController", bundle: nil) + + // Setup connection monitoring now that self is available + setupConnectionMonitoring() } required init?(coder: NSCoder) { @@ -76,45 +151,114 @@ class CredentialProviderViewController: ASCredentialProviderViewController { deinit { logger.log("[autofill-extension] deinitializing extension") - } - - - @IBAction func cancel(_ sender: AnyObject?) { - self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue)) - } - - @IBAction func passwordSelected(_ sender: AnyObject?) { - let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234") - self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) - } - - private func getWindowPosition() -> Position { - let frame = self.view.window?.frame ?? .zero - let screenHeight = NSScreen.main?.frame.height ?? 0 - // frame.width and frame.height is always 0. Estimating works OK for now. - let estimatedWidth:CGFloat = 400; - let estimatedHeight:CGFloat = 200; - let centerX = Int32(round(frame.origin.x + estimatedWidth/2)) - let centerY = Int32(round(screenHeight - (frame.origin.y + estimatedHeight/2))) - - return Position(x: centerX, y:centerY) + // Stop the connection monitor timer + connectionMonitorTimer?.invalidate() + connectionMonitorTimer = nil } - override func loadView() { - let view = NSView() - // Hide the native window since we only need the IPC connection - view.isHidden = true - self.view = view + private func getWindowPosition() async -> Position { + let screenHeight = NSScreen.main?.frame.height ?? 1440 + + logger.log("[autofill-extension] position: Getting window position") + + // To whomever is reading this. Sorry. But MacOS couldn't give us an accurate window positioning, possibly due to animations + // So I added some retry logic, as well as a fall back to the mouse position which is likely at the sort of the right place. + // In my testing we often succed after 4-7 attempts. + // Wait for window frame to stabilize (animation to complete) + var lastFrame: CGRect = .zero + var stableCount = 0 + let requiredStableChecks = 3 + let maxAttempts = 20 + var attempts = 0 + + while stableCount < requiredStableChecks && attempts < maxAttempts { + let currentFrame: CGRect = self.view.window?.frame ?? .zero + + if currentFrame.equalTo(lastFrame) && !currentFrame.equalTo(.zero) { + stableCount += 1 + } else { + stableCount = 0 + lastFrame = currentFrame + } + + try? await Task.sleep(nanoseconds: 16_666_666) // ~60fps (16.67ms) + attempts += 1 + } + + let finalWindowFrame = self.view.window?.frame ?? .zero + logger.log("[autofill-extension] position: Final window frame: \(NSStringFromRect(finalWindowFrame))") + + // Use stabilized window frame if available, otherwise fallback to mouse position + let x, y: Int32 + if finalWindowFrame.origin.x != 0 || finalWindowFrame.origin.y != 0 { + let centerX = Int32(round(finalWindowFrame.origin.x)) + let centerY = Int32(round(screenHeight - finalWindowFrame.origin.y)) + logger.log("[autofill-extension] position: Using window position: x=\(centerX), y=\(centerY)") + x = centerX + y = centerY + } else { + // Fallback to mouse position + let mouseLocation = NSEvent.mouseLocation + let mouseX = Int32(round(mouseLocation.x)) + let mouseY = Int32(round(screenHeight - mouseLocation.y)) + logger.log("[autofill-extension] position: Using mouse position fallback: x=\(mouseX), y=\(mouseY)") + x = mouseX + y = mouseY + } + // Add 100 pixels to the x-coordinate to offset the native OS dialog positioning. + return Position(x: x + 100, y: y) } - + + override func viewDidLoad() { + super.viewDidLoad() + + // Initially hide the view + self.view.isHidden = true + } + + override func prepareInterfaceForExtensionConfiguration() { + // Show the configuration UI + self.view.isHidden = false + + // Set the localized message + statusLabel.stringValue = NSLocalizedString("autofillConfigurationMessage", comment: "Message shown when Bitwarden is enabled in system settings") + + // Send the native status request asynchronously + Task { + let client = await getClient() + client.sendNativeStatus(key: "request-sync", value: "") + } + + // Complete the configuration after 2 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in + self?.extensionContext.completeExtensionConfigurationRequest() + } + } + + /* + In order to implement this method, we need to query the state of the vault to be unlocked and have one and only one matching credential so that it doesn't need to show ui. + If we do show UI, it's going to fail and disconnect after the platform timeout which is 3s. + For now we just claim to always need UI displayed. + */ override func provideCredentialWithoutUserInteraction(for credentialRequest: any ASCredentialRequest) { + let error = ASExtensionError(.userInteractionRequired) + self.extensionContext.cancelRequest(withError: error) + return + } + + /* + Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with + ASExtensionError.userInteractionRequired. In this case, the system may present your extension's + UI and call this method. Show appropriate UI for authenticating the user then provide the password + by completing the extension request with the associated ASPasswordCredential. + */ + override func prepareInterfaceToProvideCredential(for credentialRequest: ASCredentialRequest) { let timeoutTimer = createTimer() - if let request = credentialRequest as? ASPasskeyCredentialRequest { if let passkeyIdentity = request.credentialIdentity as? ASPasskeyCredentialIdentity { - - logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2(passkey) called \(request)") + + logger.log("[autofill-extension] prepareInterfaceToProvideCredential (passkey) called \(request)") class CallbackImpl: PreparePasskeyAssertionCallback { let ctx: ASCredentialProviderExtensionContext @@ -154,18 +298,25 @@ class CredentialProviderViewController: ASCredentialProviderViewController { UserVerification.discouraged } - let req = PasskeyAssertionWithoutUserInterfaceRequest( - rpId: passkeyIdentity.relyingPartyIdentifier, - credentialId: passkeyIdentity.credentialID, - userName: passkeyIdentity.userName, - userHandle: passkeyIdentity.userHandle, - recordIdentifier: passkeyIdentity.recordIdentifier, - clientDataHash: request.clientDataHash, - userVerification: userVerification, - windowXy: self.getWindowPosition() - ) - - self.client.preparePasskeyAssertionWithoutUserInterface(request: req, callback: CallbackImpl(self.extensionContext, self.logger, timeoutTimer)) + /* + We're still using the old request type here, because we're sending the same data, we're expecting a single credential to be used + */ + Task { + let windowPosition = await self.getWindowPosition() + let req = PasskeyAssertionWithoutUserInterfaceRequest( + rpId: passkeyIdentity.relyingPartyIdentifier, + credentialId: passkeyIdentity.credentialID, + userName: passkeyIdentity.userName, + userHandle: passkeyIdentity.userHandle, + recordIdentifier: passkeyIdentity.recordIdentifier, + clientDataHash: request.clientDataHash, + userVerification: userVerification, + windowXy: windowPosition + ) + + let client = await getClient() + client.preparePasskeyAssertionWithoutUserInterface(request: req, callback: CallbackImpl(self.extensionContext, self.logger, timeoutTimer)) + } return } } @@ -176,16 +327,6 @@ class CredentialProviderViewController: ASCredentialProviderViewController { self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Invalid authentication request")) } - /* - Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with - ASExtensionError.userInteractionRequired. In this case, the system may present your extension's - UI and call this method. Show appropriate UI for authenticating the user then provide the password - by completing the extension request with the associated ASPasswordCredential. - - override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) { - } - */ - private func createTimer() -> DispatchWorkItem { // Create a timer for 600 second timeout let timeoutTimer = DispatchWorkItem { [weak self] in @@ -246,18 +387,32 @@ class CredentialProviderViewController: ASCredentialProviderViewController { UserVerification.discouraged } - let req = PasskeyRegistrationRequest( - rpId: passkeyIdentity.relyingPartyIdentifier, - userName: passkeyIdentity.userName, - userHandle: passkeyIdentity.userHandle, - clientDataHash: request.clientDataHash, - userVerification: userVerification, - supportedAlgorithms: request.supportedAlgorithms.map{ Int32($0.rawValue) }, - windowXy: self.getWindowPosition() - ) + // Convert excluded credentials to an array of credential IDs + var excludedCredentialIds: [Data] = [] + if #available(macOSApplicationExtension 15.0, *) { + if let excludedCreds = request.excludedCredentials { + excludedCredentialIds = excludedCreds.map { $0.credentialID } + } + } + logger.log("[autofill-extension] prepareInterface(passkey) calling preparePasskeyRegistration") - self.client.preparePasskeyRegistration(request: req, callback: CallbackImpl(self.extensionContext, self.logger, timeoutTimer)) + Task { + let windowPosition = await self.getWindowPosition() + let req = PasskeyRegistrationRequest( + rpId: passkeyIdentity.relyingPartyIdentifier, + userName: passkeyIdentity.userName, + userHandle: passkeyIdentity.userHandle, + clientDataHash: request.clientDataHash, + userVerification: userVerification, + supportedAlgorithms: request.supportedAlgorithms.map{ Int32($0.rawValue) }, + windowXy: windowPosition, + excludedCredentials: excludedCredentialIds + ) + + let client = await getClient() + client.preparePasskeyRegistration(request: req, callback: CallbackImpl(self.extensionContext, self.logger, timeoutTimer)) + } return } } @@ -310,18 +465,22 @@ class CredentialProviderViewController: ASCredentialProviderViewController { UserVerification.discouraged } - let req = PasskeyAssertionRequest( - rpId: requestParameters.relyingPartyIdentifier, - clientDataHash: requestParameters.clientDataHash, - userVerification: userVerification, - allowedCredentials: requestParameters.allowedCredentials, - windowXy: self.getWindowPosition() - //extensionInput: requestParameters.extensionInput, - ) - let timeoutTimer = createTimer() - self.client.preparePasskeyAssertion(request: req, callback: CallbackImpl(self.extensionContext, self.logger, timeoutTimer)) + Task { + let windowPosition = await self.getWindowPosition() + let req = PasskeyAssertionRequest( + rpId: requestParameters.relyingPartyIdentifier, + clientDataHash: requestParameters.clientDataHash, + userVerification: userVerification, + allowedCredentials: requestParameters.allowedCredentials, + windowXy: windowPosition + //extensionInput: requestParameters.extensionInput, // We don't support extensions yet + ) + + let client = await getClient() + client.preparePasskeyAssertion(request: req, callback: CallbackImpl(self.extensionContext, self.logger, timeoutTimer)) + } return } } diff --git a/apps/desktop/macos/autofill-extension/Info.plist b/apps/desktop/macos/autofill-extension/Info.plist index 539cfa35b9d..7de0d4d152b 100644 --- a/apps/desktop/macos/autofill-extension/Info.plist +++ b/apps/desktop/macos/autofill-extension/Info.plist @@ -10,9 +10,9 @@ ProvidesPasskeys + ShowsConfigurationUI + - ASCredentialProviderExtensionShowsConfigurationUI - NSExtensionPointIdentifier com.apple.authentication-services-credential-provider-ui diff --git a/apps/desktop/macos/autofill-extension/autofill_extension.entitlements b/apps/desktop/macos/autofill-extension/autofill_extension.entitlements index 86c7195768e..d5c7b8a2cc8 100644 --- a/apps/desktop/macos/autofill-extension/autofill_extension.entitlements +++ b/apps/desktop/macos/autofill-extension/autofill_extension.entitlements @@ -2,11 +2,9 @@ - com.apple.developer.authentication-services.autofill-credential-provider - - com.apple.security.app-sandbox - - com.apple.security.application-groups + com.apple.security.app-sandbox + + com.apple.security.application-groups LTZ2PFU5D6.com.bitwarden.desktop diff --git a/apps/desktop/macos/autofill-extension/bitwarden-icon.png b/apps/desktop/macos/autofill-extension/bitwarden-icon.png new file mode 100644 index 00000000000..9a05bc7bbdd Binary files /dev/null and b/apps/desktop/macos/autofill-extension/bitwarden-icon.png differ diff --git a/apps/desktop/macos/autofill-extension/en.lproj/Localizable.strings b/apps/desktop/macos/autofill-extension/en.lproj/Localizable.strings new file mode 100644 index 00000000000..95730dff286 --- /dev/null +++ b/apps/desktop/macos/autofill-extension/en.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Message shown during passkey configuration */ +"autofillConfigurationMessage" = "Enabling Bitwarden..."; diff --git a/apps/desktop/macos/desktop.xcodeproj/project.pbxproj b/apps/desktop/macos/desktop.xcodeproj/project.pbxproj index ff257097f26..ed19fc9ef5d 100644 --- a/apps/desktop/macos/desktop.xcodeproj/project.pbxproj +++ b/apps/desktop/macos/desktop.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ 3368DB392C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3368DB382C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework */; }; 3368DB3B2C654F3800896B75 /* BitwardenMacosProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */; }; + 9AE299092DF9D82E00AAE454 /* bitwarden-icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 9AE299082DF9D82E00AAE454 /* bitwarden-icon.png */; }; + 9AE299122DFB57A200AAE454 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 9AE2990D2DFB57A200AAE454 /* Localizable.strings */; }; E1DF713F2B342F6900F29026 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */; }; E1DF71422B342F6900F29026 /* CredentialProviderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */; }; E1DF71452B342F6900F29026 /* CredentialProviderViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */; }; @@ -18,6 +20,8 @@ 3368DB382C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BitwardenMacosProviderFFI.xcframework; path = ../desktop_native/macos_provider/BitwardenMacosProviderFFI.xcframework; sourceTree = ""; }; 3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitwardenMacosProvider.swift; sourceTree = ""; }; 968ED08A2C52A47200FFFEE6 /* ReleaseAppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ReleaseAppStore.xcconfig; sourceTree = ""; }; + 9AE299082DF9D82E00AAE454 /* bitwarden-icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "bitwarden-icon.png"; sourceTree = ""; }; + 9AE2990C2DFB57A200AAE454 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Localizable.strings; sourceTree = ""; }; D83832AB2D67B9AE003FB9F8 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; D83832AD2D67B9D0003FB9F8 /* ReleaseDeveloper.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ReleaseDeveloper.xcconfig; sourceTree = ""; }; E1DF713C2B342F6900F29026 /* autofill-extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "autofill-extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -41,6 +45,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 9AE2990E2DFB57A200AAE454 /* en.lproj */ = { + isa = PBXGroup; + children = ( + 9AE2990D2DFB57A200AAE454 /* Localizable.strings */, + ); + path = en.lproj; + sourceTree = ""; + }; E1DF711D2B342E2800F29026 = { isa = PBXGroup; children = ( @@ -73,6 +85,8 @@ E1DF71402B342F6900F29026 /* autofill-extension */ = { isa = PBXGroup; children = ( + 9AE2990E2DFB57A200AAE454 /* en.lproj */, + 9AE299082DF9D82E00AAE454 /* bitwarden-icon.png */, 3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */, E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */, E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */, @@ -124,6 +138,7 @@ knownRegions = ( en, Base, + sv, ); mainGroup = E1DF711D2B342E2800F29026; productRefGroup = E1DF71272B342E2800F29026 /* Products */; @@ -141,6 +156,8 @@ buildActionMask = 2147483647; files = ( E1DF71452B342F6900F29026 /* CredentialProviderViewController.xib in Resources */, + 9AE299122DFB57A200AAE454 /* Localizable.strings in Resources */, + 9AE299092DF9D82E00AAE454 /* bitwarden-icon.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -159,6 +176,14 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ + 9AE2990D2DFB57A200AAE454 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 9AE2990C2DFB57A200AAE454 /* en */, + ); + name = Localizable.strings; + sourceTree = ""; + }; E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */ = { isa = PBXVariantGroup; children = ( diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 8ef19b2f180..32874688861 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -18,7 +18,9 @@ "scripts": { "postinstall": "electron-rebuild", "start": "cross-env ELECTRON_IS_DEV=0 ELECTRON_NO_UPDATER=1 electron ./build", + "build-native-macos": "cd desktop_native && ./macos_provider/build.sh && node build.js cross-platform", "build-native": "cd desktop_native && node build.js", + "build-native-win-cross": "cd desktop_native && node build.js cross-platform --target=aarch64-pc-windows-msvc", "build": "concurrently -n Main,Rend,Prel -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\" \"npm run build:preload\"", "build:dev": "concurrently -n Main,Rend,Prel -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\" \"npm run build:preload:dev\"", "build:preload": "cross-env NODE_ENV=production webpack --config webpack.config.js --config-name preload", @@ -28,7 +30,7 @@ "build:macos-extension:mas": "./desktop_native/macos_provider/build.sh && node scripts/build-macos-extension.js mas", "build:macos-extension:masdev": "./desktop_native/macos_provider/build.sh && node scripts/build-macos-extension.js mas-dev", "build:main": "cross-env NODE_ENV=production webpack --config webpack.config.js --config-name main", - "build:main:dev": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.config.js --config-name main", + "build:main:dev": "cross-env NODE_ENV=development webpack --config webpack.config.js --config-name main", "build:main:watch": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.config.js --config-name main --watch", "build:renderer": "cross-env NODE_ENV=production webpack --config webpack.config.js --config-name renderer", "build:renderer:dev": "cross-env NODE_ENV=development webpack --config webpack.config.js --config-name renderer", @@ -41,25 +43,23 @@ "pack:lin:flatpak": "flatpak-builder --repo=../../.flatpak-repo ../../.flatpak ./resources/com.bitwarden.desktop.devel.yaml --install-deps-from=flathub --force-clean && flatpak build-bundle ../../.flatpak-repo/ ./dist/com.bitwarden.desktop.flatpak com.bitwarden.desktop", "pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && snap pack --compression=lzo ./dist/tmp-snap/ && mv ./*.snap ./dist/ && rm -rf ./dist/tmp-snap/", "pack:lin:arm64": "npm run clean:dist && electron-builder --dir -p never && tar -czvf ./dist/bitwarden_desktop_arm64.tar.gz -C ./dist/linux-arm64-unpacked/ .", - "pack:mac": "npm run clean:dist && electron-builder --mac --universal -p never", - "pack:mac:with-extension": "npm run clean:dist && npm run build:macos-extension:mac && electron-builder --mac --universal -p never", + "pack:mac": "npm run clean:dist && npm run build:macos-extension:mac && electron-builder --mac --universal -p never", "pack:mac:arm64": "npm run clean:dist && electron-builder --mac --arm64 -p never", - "pack:mac:mas": "npm run clean:dist && electron-builder --mac mas --universal -p never", - "pack:mac:mas:with-extension": "npm run clean:dist && npm run build:macos-extension:mas && electron-builder --mac mas --universal -p never", - "pack:mac:masdev": "npm run clean:dist && electron-builder --mac mas-dev --universal -p never", - "pack:mac:masdev:with-extension": "npm run clean:dist && npm run build:macos-extension:masdev && electron-builder --mac mas-dev --universal -p never", + "pack:mac:mas": "npm run clean:dist && npm run build:macos-extension:mas && electron-builder --mac mas --universal -p never", + "pack:mac:masdev": "npm run clean:dist && npm run build:macos-extension:masdev && electron-builder --mac mas-dev --universal -p never", + "pack:local:mac": "npm run clean:dist && npm run build:macos-extension:masdev && electron-builder --mac mas-dev --universal -p never -c.mac.provisioningProfile=\"\" -c.mas.provisioningProfile=\"\"", "pack:win": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never -c.win.signtoolOptions.certificateSubjectName=\"8bit Solutions LLC\"", + "pack:win:arm64": "npm run clean:dist && electron-builder --win --arm64 -p never -c.win.signtoolOptions.certificateSubjectName=\"8bit Solutions LLC\"", + "pack:win:appx:arm64": "npm run clean:dist && electron-builder --win appx:arm64 --publish never", "pack:win:beta": "npm run clean:dist && electron-builder --config electron-builder.beta.json --win --x64 --arm64 --ia32 -p never -c.win.signtoolOptions.certificateSubjectName=\"8bit Solutions LLC\"", + "pack:win:beta:appx:arm64": "npm run clean:dist && electron-builder --config electron-builder.beta.json --win appx:arm64 --publish never", "pack:win:ci": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never", "dist:dir": "npm run build && npm run pack:dir", "dist:lin": "npm run build && npm run pack:lin", "dist:lin:arm64": "npm run build && npm run pack:lin:arm64", "dist:mac": "npm run build && npm run pack:mac", - "dist:mac:with-extension": "npm run build && npm run pack:mac:with-extension", "dist:mac:mas": "npm run build && npm run pack:mac:mas", - "dist:mac:mas:with-extension": "npm run build && npm run pack:mac:mas:with-extension", - "dist:mac:masdev": "npm run build:dev && npm run pack:mac:masdev", - "dist:mac:masdev:with-extension": "npm run build:dev && npm run pack:mac:masdev:with-extension", + "dist:mac:masdev": "npm run build && npm run pack:mac:masdev", "dist:win": "npm run build && npm run pack:win", "dist:win:ci": "npm run build && npm run pack:win:ci", "publish:lin": "npm run build && npm run clean:dist && electron-builder --linux --x64 -p always", diff --git a/apps/desktop/resources/entitlements.mac.plist b/apps/desktop/resources/entitlements.mac.plist index fe49256d71c..7763b84624d 100644 --- a/apps/desktop/resources/entitlements.mac.plist +++ b/apps/desktop/resources/entitlements.mac.plist @@ -6,8 +6,6 @@ LTZ2PFU5D6.com.bitwarden.desktop com.apple.developer.team-identifier LTZ2PFU5D6 - com.apple.developer.authentication-services.autofill-credential-provider - com.apple.security.cs.allow-jit diff --git a/apps/desktop/resources/entitlements.mas.inherit.plist b/apps/desktop/resources/entitlements.mas.inherit.plist index fca5f02d52d..7194d9409fc 100644 --- a/apps/desktop/resources/entitlements.mas.inherit.plist +++ b/apps/desktop/resources/entitlements.mas.inherit.plist @@ -4,9 +4,9 @@ com.apple.security.app-sandbox - com.apple.security.inherit - com.apple.security.cs.allow-jit + com.apple.security.inherit + diff --git a/apps/desktop/resources/entitlements.mas.plist b/apps/desktop/resources/entitlements.mas.plist index 3ebd56f0fd7..6d6d37d643e 100644 --- a/apps/desktop/resources/entitlements.mas.plist +++ b/apps/desktop/resources/entitlements.mas.plist @@ -6,19 +6,19 @@ LTZ2PFU5D6.com.bitwarden.desktop com.apple.developer.team-identifier LTZ2PFU5D6 - com.apple.developer.authentication-services.autofill-credential-provider - com.apple.security.app-sandbox com.apple.security.application-groups LTZ2PFU5D6.com.bitwarden.desktop - com.apple.security.network.client + com.apple.security.cs.allow-jit + + com.apple.security.device.usb com.apple.security.files.user-selected.read-write - com.apple.security.device.usb + com.apple.security.network.client com.apple.security.temporary-exception.files.home-relative-path.read-write @@ -32,10 +32,8 @@ /Library/Application Support/Microsoft Edge Beta/NativeMessagingHosts/ /Library/Application Support/Microsoft Edge Dev/NativeMessagingHosts/ /Library/Application Support/Microsoft Edge Canary/NativeMessagingHosts/ - /Library/Application Support/Vivaldi/NativeMessagingHosts/ + /Library/Application Support/Vivaldi/NativeMessagingHosts/ /Library/Application Support/Zen/NativeMessagingHosts/ - com.apple.security.cs.allow-jit - diff --git a/apps/desktop/sign.js b/apps/desktop/sign.js index 6a42666c46f..9ad66567711 100644 --- a/apps/desktop/sign.js +++ b/apps/desktop/sign.js @@ -1,8 +1,46 @@ /* eslint-disable @typescript-eslint/no-require-imports, no-console */ exports.default = async function (configuration) { - if (parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 && configuration.path.slice(-4) == ".exe") { + if ( + parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 && + (configuration.path.endsWith(".exe") || + configuration.path.endsWith(".appx") || + configuration.path.endsWith(".msix")) + ) { console.log(`[*] Signing file: ${configuration.path}`); + + // If signing APPX/MSIX, inspect the manifest Publisher before signing + if (configuration.path.endsWith(".appx") || configuration.path.endsWith(".msix")) { + try { + const path = require("path"); + const fs = require("fs"); + + // Extract architecture from filename (e.g., "Bitwarden-2025.10.2-x64.appx" -> "x64") + const filename = path.basename(configuration.path); + const archMatch = filename.match(/-(x64|arm64|ia32)\.(appx|msix)$/); + + if (archMatch) { + const arch = archMatch[1]; + const distDir = path.dirname(configuration.path); + const manifestPath = path.join(distDir, `__appx-${arch}`, "AppxManifest.xml"); + + if (fs.existsSync(manifestPath)) { + const manifestContent = fs.readFileSync(manifestPath, "utf8"); + + // Extract and display the Publisher line + const publisherMatch = manifestContent.match(/Publisher='([^']+)'/); + if (publisherMatch) { + console.log(`[*] APPX Manifest Publisher: ${publisherMatch[1]}`); + } + } else { + console.log(`[!] Manifest not found at: ${manifestPath}`); + } + } + } catch (error) { + console.log(`[!] Failed to read manifest: ${error.message}`); + } + } + require("child_process").execSync( `azuresigntool sign -v ` + `-kvu ${process.env.SIGNING_VAULT_URL} ` + @@ -18,5 +56,20 @@ exports.default = async function (configuration) { stdio: "inherit", }, ); + } else if (process.env.ELECTRON_BUILDER_SIGN_CERT) { + const certFile = process.env.ELECTRON_BUILDER_SIGN_CERT; + const certPw = process.env.ELECTRON_BUILDER_SIGN_CERT_PW; + console.log(`[*] Signing file: ${configuration.path} with ${certFile}`); + require("child_process").execSync( + "signtool.exe sign" + + " /fd SHA256" + + " /a" + + ` /f "${certFile}"` + + ` /p "${certPw}"` + + ` "${configuration.path}"`, + { + stdio: "inherit", + }, + ); } }; diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index b6e86ba19ff..ce84ca54bdb 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -43,10 +43,13 @@ import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/co import { LockComponent, ConfirmKeyConnectorDomainComponent } from "@bitwarden/key-management-ui"; import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; +import { reactiveUnlockVaultGuard } from "../autofill/guards/reactive-vault-guard"; +import { Fido2CreateComponent } from "../autofill/modal/credentials/fido2-create.component"; +import { Fido2ExcludedCiphersComponent } from "../autofill/modal/credentials/fido2-excluded-ciphers.component"; +import { Fido2VaultComponent } from "../autofill/modal/credentials/fido2-vault.component"; import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import { VaultV2Component } from "../vault/app/vault/vault-v2.component"; -import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component"; import { SendComponent } from "./tools/send/send.component"; /** @@ -112,12 +115,16 @@ const routes: Routes = [ canActivate: [authGuard], }, { - path: "passkeys", - component: Fido2PlaceholderComponent, + path: "fido2-assertion", + component: Fido2VaultComponent, }, { - path: "passkeys", - component: Fido2PlaceholderComponent, + path: "fido2-creation", + component: Fido2CreateComponent, + }, + { + path: "fido2-excluded", + component: Fido2ExcludedCiphersComponent, }, { path: "", @@ -263,7 +270,7 @@ const routes: Routes = [ }, { path: "lock", - canActivate: [lockGuard()], + canActivate: [lockGuard(), reactiveUnlockVaultGuard], data: { pageIcon: LockIcon, pageTitle: { diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 4b6dcab0dff..30ef27f7df9 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -103,7 +103,7 @@ const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours - +
@@ -140,6 +140,7 @@ export class AppComponent implements OnInit, OnDestroy { @ViewChild("loginApproval", { read: ViewContainerRef, static: true }) loginApprovalModalRef: ViewContainerRef; + showHeader$ = this.accountService.showHeader$; loading = false; private lastActivity: Date = null; diff --git a/apps/desktop/src/app/components/fido2placeholder.component.ts b/apps/desktop/src/app/components/fido2placeholder.component.ts deleted file mode 100644 index f1f52dae439..00000000000 --- a/apps/desktop/src/app/components/fido2placeholder.component.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { Router } from "@angular/router"; -import { BehaviorSubject, Observable } from "rxjs"; - -import { - DesktopFido2UserInterfaceService, - DesktopFido2UserInterfaceSession, -} from "../../autofill/services/desktop-fido2-user-interface.service"; -import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; - -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -@Component({ - standalone: true, - imports: [CommonModule], - template: ` -
-

Select your passkey

- -
- -
- -
- - -
- `, -}) -export class Fido2PlaceholderComponent implements OnInit, OnDestroy { - session?: DesktopFido2UserInterfaceSession = null; - private cipherIdsSubject = new BehaviorSubject([]); - cipherIds$: Observable; - - constructor( - private readonly desktopSettingsService: DesktopSettingsService, - private readonly fido2UserInterfaceService: DesktopFido2UserInterfaceService, - private readonly router: Router, - ) {} - - ngOnInit() { - this.session = this.fido2UserInterfaceService.getCurrentSession(); - this.cipherIds$ = this.session?.availableCipherIds$; - } - - async chooseCipher(cipherId: string) { - // For now: Set UV to true - this.session?.confirmChosenCipher(cipherId, true); - - await this.router.navigate(["/"]); - await this.desktopSettingsService.setModalMode(false); - } - - ngOnDestroy() { - this.cipherIdsSubject.complete(); // Clean up the BehaviorSubject - } - - async confirmPasskey() { - try { - // Retrieve the current UI session to control the flow - if (!this.session) { - // todo: handle error - throw new Error("No session found"); - } - - // If we want to we could submit information to the session in order to create the credential - // const cipher = await session.createCredential({ - // userHandle: "userHandle2", - // userName: "username2", - // credentialName: "zxsd2", - // rpId: "webauthn.io", - // userVerification: true, - // }); - - this.session.notifyConfirmNewCredential(true); - - // Not sure this clean up should happen here or in session. - // The session currently toggles modal on and send us here - // But if this route is somehow opened outside of session we want to make sure we clean up? - await this.router.navigate(["/"]); - await this.desktopSettingsService.setModalMode(false); - } catch { - // TODO: Handle error appropriately - } - } - - async closeModal() { - await this.router.navigate(["/"]); - await this.desktopSettingsService.setModalMode(false); - - this.session.notifyConfirmNewCredential(false); - // little bit hacky: - this.session.confirmChosenCipher(null); - } -} diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index a0ee33a459c..d6f29b122ea 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -336,6 +336,7 @@ const safeProviders: SafeProvider[] = [ ConfigService, Fido2AuthenticatorServiceAbstraction, AccountService, + AuthService, ], }), safeProvider({ diff --git a/apps/desktop/src/autofill/guards/reactive-vault-guard.ts b/apps/desktop/src/autofill/guards/reactive-vault-guard.ts new file mode 100644 index 00000000000..d16787ef46a --- /dev/null +++ b/apps/desktop/src/autofill/guards/reactive-vault-guard.ts @@ -0,0 +1,42 @@ +import { inject } from "@angular/core"; +import { CanActivateFn, Router } from "@angular/router"; +import { combineLatest, map, switchMap, distinctUntilChanged } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; + +import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; + +/** + * Reactive route guard that redirects to the unlocked vault. + * Redirects to vault when unlocked in main window. + */ +export const reactiveUnlockVaultGuard: CanActivateFn = () => { + const router = inject(Router); + const authService = inject(AuthService); + const accountService = inject(AccountService); + const desktopSettingsService = inject(DesktopSettingsService); + + return combineLatest([accountService.activeAccount$, desktopSettingsService.modalMode$]).pipe( + switchMap(([account, modalMode]) => { + if (!account) { + return [true]; + } + + // Monitor when the vault has been unlocked. + return authService.authStatusFor$(account.id).pipe( + distinctUntilChanged(), + map((authStatus) => { + // If vault is unlocked and we're not in modal mode, redirect to vault + if (authStatus === AuthenticationStatus.Unlocked && !modalMode?.isModalModeActive) { + return router.createUrlTree(["/vault"]); + } + + // Otherwise keep user on the lock screen + return true; + }), + ); + }), + ); +}; diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html new file mode 100644 index 00000000000..67fc76aa317 --- /dev/null +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.html @@ -0,0 +1,66 @@ +
+ + +
+ + +

+ {{ "savePasskeyQuestion" | i18n }} +

+
+ + +
+
+ + +
+
+ +
+ {{ "noMatchingLoginsForSite" | i18n }} +
+ +
+
+ + + + {{ c.subTitle }} + {{ "save" | i18n }} + + + + + + +
+
diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.spec.ts b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.spec.ts new file mode 100644 index 00000000000..778215895ee --- /dev/null +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.spec.ts @@ -0,0 +1,238 @@ +import { TestBed } from "@angular/core/testing"; +import { Router } from "@angular/router"; +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject, of } from "rxjs"; + +import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { DialogService } from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +import { DesktopAutofillService } from "../../../autofill/services/desktop-autofill.service"; +import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service"; +import { + DesktopFido2UserInterfaceService, + DesktopFido2UserInterfaceSession, +} from "../../services/desktop-fido2-user-interface.service"; + +import { Fido2CreateComponent } from "./fido2-create.component"; + +describe("Fido2CreateComponent", () => { + let component: Fido2CreateComponent; + let mockDesktopSettingsService: MockProxy; + let mockFido2UserInterfaceService: MockProxy; + let mockAccountService: MockProxy; + let mockCipherService: MockProxy; + let mockDesktopAutofillService: MockProxy; + let mockDialogService: MockProxy; + let mockDomainSettingsService: MockProxy; + let mockLogService: MockProxy; + let mockPasswordRepromptService: MockProxy; + let mockRouter: MockProxy; + let mockSession: MockProxy; + let mockI18nService: MockProxy; + + const activeAccountSubject = new BehaviorSubject({ + id: "test-user-id" as UserId, + email: "test@example.com", + emailVerified: true, + name: "Test User", + }); + + beforeEach(async () => { + mockDesktopSettingsService = mock(); + mockFido2UserInterfaceService = mock(); + mockAccountService = mock(); + mockCipherService = mock(); + mockDesktopAutofillService = mock(); + mockDialogService = mock(); + mockDomainSettingsService = mock(); + mockLogService = mock(); + mockPasswordRepromptService = mock(); + mockRouter = mock(); + mockSession = mock(); + mockI18nService = mock(); + + mockFido2UserInterfaceService.getCurrentSession.mockReturnValue(mockSession); + mockAccountService.activeAccount$ = activeAccountSubject; + + await TestBed.configureTestingModule({ + providers: [ + Fido2CreateComponent, + { provide: DesktopSettingsService, useValue: mockDesktopSettingsService }, + { provide: DesktopFido2UserInterfaceService, useValue: mockFido2UserInterfaceService }, + { provide: AccountService, useValue: mockAccountService }, + { provide: CipherService, useValue: mockCipherService }, + { provide: DesktopAutofillService, useValue: mockDesktopAutofillService }, + { provide: DialogService, useValue: mockDialogService }, + { provide: DomainSettingsService, useValue: mockDomainSettingsService }, + { provide: LogService, useValue: mockLogService }, + { provide: PasswordRepromptService, useValue: mockPasswordRepromptService }, + { provide: Router, useValue: mockRouter }, + { provide: I18nService, useValue: mockI18nService }, + ], + }).compileComponents(); + + component = TestBed.inject(Fido2CreateComponent); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + function createMockCiphers(): CipherView[] { + const cipher1 = new CipherView(); + cipher1.id = "cipher-1"; + cipher1.name = "Test Cipher 1"; + cipher1.type = CipherType.Login; + cipher1.login = { + username: "test1@example.com", + uris: [{ uri: "https://example.com", match: null }], + matchesUri: jest.fn().mockReturnValue(true), + get hasFido2Credentials() { + return false; + }, + } as any; + cipher1.reprompt = CipherRepromptType.None; + cipher1.deletedDate = null; + + return [cipher1]; + } + + describe("ngOnInit", () => { + beforeEach(() => { + mockSession.getRpId.mockResolvedValue("example.com"); + Object.defineProperty(mockDesktopAutofillService, "lastRegistrationRequest", { + get: jest.fn().mockReturnValue({ + userHandle: new Uint8Array([1, 2, 3]), + }), + configurable: true, + }); + mockDomainSettingsService.getUrlEquivalentDomains.mockReturnValue(of(new Set())); + }); + + it("should initialize session and set show header to false", async () => { + const mockCiphers = createMockCiphers(); + mockCipherService.getAllDecrypted.mockResolvedValue(mockCiphers); + + await component.ngOnInit(); + + expect(mockFido2UserInterfaceService.getCurrentSession).toHaveBeenCalled(); + expect(component.session).toBe(mockSession); + }); + + it("should show error dialog when no active session found", async () => { + mockFido2UserInterfaceService.getCurrentSession.mockReturnValue(null); + mockDialogService.openSimpleDialog.mockResolvedValue(false); + + await component.ngOnInit(); + + expect(mockDialogService.openSimpleDialog).toHaveBeenCalledWith({ + title: { key: "unableToSavePasskey" }, + content: { key: "closeThisBitwardenWindow" }, + type: "danger", + acceptButtonText: { key: "closeThisWindow" }, + acceptAction: expect.any(Function), + cancelButtonText: null, + }); + }); + }); + + describe("addCredentialToCipher", () => { + beforeEach(() => { + component.session = mockSession; + }); + + it("should add passkey to cipher", async () => { + const cipher = createMockCiphers()[0]; + + await component.addCredentialToCipher(cipher); + + expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(true, cipher); + }); + + it("should not add passkey when password reprompt is cancelled", async () => { + const cipher = createMockCiphers()[0]; + cipher.reprompt = CipherRepromptType.Password; + mockPasswordRepromptService.showPasswordPrompt.mockResolvedValue(false); + + await component.addCredentialToCipher(cipher); + + expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(false, cipher); + }); + + it("should call openSimpleDialog when cipher already has a fido2 credential", async () => { + const cipher = createMockCiphers()[0]; + Object.defineProperty(cipher.login, "hasFido2Credentials", { + get: jest.fn().mockReturnValue(true), + }); + mockDialogService.openSimpleDialog.mockResolvedValue(true); + + await component.addCredentialToCipher(cipher); + + expect(mockDialogService.openSimpleDialog).toHaveBeenCalledWith({ + title: { key: "overwritePasskey" }, + content: { key: "alreadyContainsPasskey" }, + type: "warning", + }); + expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(true, cipher); + }); + + it("should not add passkey when user cancels overwrite dialog", async () => { + const cipher = createMockCiphers()[0]; + Object.defineProperty(cipher.login, "hasFido2Credentials", { + get: jest.fn().mockReturnValue(true), + }); + mockDialogService.openSimpleDialog.mockResolvedValue(false); + + await component.addCredentialToCipher(cipher); + + expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(false, cipher); + }); + }); + + describe("confirmPasskey", () => { + beforeEach(() => { + component.session = mockSession; + }); + + it("should confirm passkey creation successfully", async () => { + await component.confirmPasskey(); + + expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(true); + }); + + it("should call openSimpleDialog when session is null", async () => { + component.session = null; + mockDialogService.openSimpleDialog.mockResolvedValue(false); + + await component.confirmPasskey(); + + expect(mockDialogService.openSimpleDialog).toHaveBeenCalledWith({ + title: { key: "unableToSavePasskey" }, + content: { key: "closeThisBitwardenWindow" }, + type: "danger", + acceptButtonText: { key: "closeThisWindow" }, + acceptAction: expect.any(Function), + cancelButtonText: null, + }); + }); + }); + + describe("closeModal", () => { + it("should close modal and notify session", async () => { + component.session = mockSession; + + await component.closeModal(); + + expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(false); + expect(mockSession.confirmChosenCipher).toHaveBeenCalledWith(null); + }); + }); +}); diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts new file mode 100644 index 00000000000..90dc18f4f48 --- /dev/null +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts @@ -0,0 +1,224 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnInit, OnDestroy } from "@angular/core"; +import { RouterModule, Router } from "@angular/router"; +import { combineLatest, map, Observable, Subject, switchMap } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { BitwardenShield, NoResults } from "@bitwarden/assets/svg"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { Fido2Utils } from "@bitwarden/common/platform/services/fido2/fido2-utils"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + DialogService, + BadgeModule, + ButtonModule, + DialogModule, + IconModule, + ItemModule, + SectionComponent, + TableModule, + SectionHeaderComponent, + BitIconButtonComponent, + SimpleDialogOptions, +} from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +import { DesktopAutofillService } from "../../../autofill/services/desktop-autofill.service"; +import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service"; +import { + DesktopFido2UserInterfaceService, + DesktopFido2UserInterfaceSession, +} from "../../services/desktop-fido2-user-interface.service"; + +@Component({ + standalone: true, + imports: [ + CommonModule, + RouterModule, + SectionHeaderComponent, + BitIconButtonComponent, + TableModule, + JslibModule, + IconModule, + ButtonModule, + DialogModule, + SectionComponent, + ItemModule, + BadgeModule, + ], + templateUrl: "fido2-create.component.html", +}) +export class Fido2CreateComponent implements OnInit, OnDestroy { + session?: DesktopFido2UserInterfaceSession = null; + ciphers$: Observable; + private destroy$ = new Subject(); + readonly Icons = { BitwardenShield, NoResults }; + + private get DIALOG_MESSAGES() { + return { + unexpectedErrorShort: { + title: { key: "unexpectedErrorShort" }, + content: { key: "closeThisBitwardenWindow" }, + type: "danger", + acceptButtonText: { key: "closeThisWindow" }, + cancelButtonText: null as null, + acceptAction: async () => this.dialogService.closeAll(), + }, + unableToSavePasskey: { + title: { key: "unableToSavePasskey" }, + content: { key: "closeThisBitwardenWindow" }, + type: "danger", + acceptButtonText: { key: "closeThisWindow" }, + cancelButtonText: null as null, + acceptAction: async () => this.dialogService.closeAll(), + }, + overwritePasskey: { + title: { key: "overwritePasskey" }, + content: { key: "alreadyContainsPasskey" }, + type: "warning", + }, + } as const satisfies Record; + } + + constructor( + private readonly desktopSettingsService: DesktopSettingsService, + private readonly fido2UserInterfaceService: DesktopFido2UserInterfaceService, + private readonly accountService: AccountService, + private readonly cipherService: CipherService, + private readonly desktopAutofillService: DesktopAutofillService, + private readonly dialogService: DialogService, + private readonly domainSettingsService: DomainSettingsService, + private readonly passwordRepromptService: PasswordRepromptService, + private readonly router: Router, + ) {} + + async ngOnInit(): Promise { + this.session = this.fido2UserInterfaceService.getCurrentSession(); + + if (this.session) { + const rpid = await this.session.getRpId(); + this.initializeCiphersObservable(rpid); + } else { + await this.showErrorDialog(this.DIALOG_MESSAGES.unableToSavePasskey); + } + } + + async ngOnDestroy(): Promise { + this.destroy$.next(); + this.destroy$.complete(); + // If we want to hide the UI while prompting for UV from the OS, we cannot call closeModal(). + // await this.closeModal(); + } + + async addCredentialToCipher(cipher: CipherView): Promise { + const isConfirmed = await this.validateCipherAccess(cipher); + + try { + if (!this.session) { + throw new Error("Missing session"); + } + + this.session.notifyConfirmCreateCredential(isConfirmed, cipher); + } catch { + await this.showErrorDialog(this.DIALOG_MESSAGES.unableToSavePasskey); + return; + } + + await this.closeModal(); + } + + async confirmPasskey(): Promise { + try { + if (!this.session) { + throw new Error("Missing session"); + } + + // TODO: We should know the username by now; we should pass that context here. + const username = "New Account" // placeholder + const isConfirmed = await this.session.promptForUserVerification("New Account", "Verify it's you to create a new credential") + this.session.notifyConfirmCreateCredential(isConfirmed); + } catch { + await this.showErrorDialog(this.DIALOG_MESSAGES.unableToSavePasskey); + } + + await this.closeModal(); + } + + async closeModal(): Promise { + await this.desktopSettingsService.setModalMode(false); + await this.accountService.setShowHeader(true); + + if (this.session) { + this.session.notifyConfirmCreateCredential(false); + this.session.confirmChosenCipher(null); + } + + await this.router.navigate(["/"]); + } + + private initializeCiphersObservable(rpid: string): void { + const lastRegistrationRequest = this.desktopAutofillService.lastRegistrationRequest; + + if (!lastRegistrationRequest || !rpid) { + return; + } + + const userHandle = Fido2Utils.bufferToString( + new Uint8Array(lastRegistrationRequest.userHandle), + ); + + this.ciphers$ = combineLatest([ + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + this.domainSettingsService.getUrlEquivalentDomains(rpid), + ]).pipe( + switchMap(async ([activeUserId, equivalentDomains]) => { + if (!activeUserId) { + return []; + } + + try { + const allCiphers = await this.cipherService.getAllDecrypted(activeUserId); + return allCiphers.filter( + (cipher) => + cipher != null && + cipher.type == CipherType.Login && + cipher.login?.matchesUri(rpid, equivalentDomains) && + Fido2Utils.cipherHasNoOtherPasskeys(cipher, userHandle) && + !cipher.deletedDate, + ); + } catch { + await this.showErrorDialog(this.DIALOG_MESSAGES.unexpectedErrorShort); + return []; + } + }), + ); + } + + private async validateCipherAccess(cipher: CipherView): Promise { + if (cipher.login.hasFido2Credentials) { + const overwriteConfirmed = await this.dialogService.openSimpleDialog( + this.DIALOG_MESSAGES.overwritePasskey, + ); + + if (!overwriteConfirmed) { + return false; + } + } + + if (cipher.reprompt) { + return this.passwordRepromptService.showPasswordPrompt(); + } + + let cred = cipher.login.fido2Credentials[0]; + const username = cred.userName ?? cred.userDisplayName + return this.session.promptForUserVerification(username, "Verify it's you to overwrite a credential") + } + + private async showErrorDialog(config: SimpleDialogOptions): Promise { + await this.dialogService.openSimpleDialog(config); + await this.closeModal(); + } +} diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html new file mode 100644 index 00000000000..792934deedc --- /dev/null +++ b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.html @@ -0,0 +1,44 @@ +
+ + +
+ + +

+ {{ "savePasskeyQuestion" | i18n }} +

+
+ + +
+
+ +
+ +
+ +
+ {{ "passkeyAlreadyExists" | i18n }} + {{ "applicationDoesNotSupportDuplicates" | i18n }} +
+ +
+
+
+
diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts new file mode 100644 index 00000000000..6a465136458 --- /dev/null +++ b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts @@ -0,0 +1,78 @@ +import { NO_ERRORS_SCHEMA } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { Router } from "@angular/router"; +import { mock, MockProxy } from "jest-mock-extended"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service"; +import { + DesktopFido2UserInterfaceService, + DesktopFido2UserInterfaceSession, +} from "../../services/desktop-fido2-user-interface.service"; + +import { Fido2ExcludedCiphersComponent } from "./fido2-excluded-ciphers.component"; + +describe("Fido2ExcludedCiphersComponent", () => { + let component: Fido2ExcludedCiphersComponent; + let fixture: ComponentFixture; + let mockDesktopSettingsService: MockProxy; + let mockFido2UserInterfaceService: MockProxy; + let mockAccountService: MockProxy; + let mockRouter: MockProxy; + let mockSession: MockProxy; + let mockI18nService: MockProxy; + + beforeEach(async () => { + mockDesktopSettingsService = mock(); + mockFido2UserInterfaceService = mock(); + mockAccountService = mock(); + mockRouter = mock(); + mockSession = mock(); + mockI18nService = mock(); + + mockFido2UserInterfaceService.getCurrentSession.mockReturnValue(mockSession); + + await TestBed.configureTestingModule({ + imports: [Fido2ExcludedCiphersComponent], + providers: [ + { provide: DesktopSettingsService, useValue: mockDesktopSettingsService }, + { provide: DesktopFido2UserInterfaceService, useValue: mockFido2UserInterfaceService }, + { provide: AccountService, useValue: mockAccountService }, + { provide: Router, useValue: mockRouter }, + { provide: I18nService, useValue: mockI18nService }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(Fido2ExcludedCiphersComponent); + component = fixture.componentInstance; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe("ngOnInit", () => { + it("should initialize session", async () => { + await component.ngOnInit(); + + expect(mockFido2UserInterfaceService.getCurrentSession).toHaveBeenCalled(); + expect(component.session).toBe(mockSession); + }); + }); + + describe("closeModal", () => { + it("should close modal and notify session when session exists", async () => { + component.session = mockSession; + + await component.closeModal(); + + expect(mockDesktopSettingsService.setModalMode).toHaveBeenCalledWith(false); + expect(mockAccountService.setShowHeader).toHaveBeenCalledWith(true); + expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(false); + expect(mockRouter.navigate).toHaveBeenCalledWith(["/"]); + }); + }); +}); diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts new file mode 100644 index 00000000000..ddcf95d7d08 --- /dev/null +++ b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts @@ -0,0 +1,77 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnInit, OnDestroy } from "@angular/core"; +import { RouterModule, Router } from "@angular/router"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { BitwardenShield, NoResults } from "@bitwarden/assets/svg"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { + BadgeModule, + ButtonModule, + DialogModule, + IconModule, + ItemModule, + SectionComponent, + TableModule, + SectionHeaderComponent, + BitIconButtonComponent, +} from "@bitwarden/components"; + +import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service"; +import { + DesktopFido2UserInterfaceService, + DesktopFido2UserInterfaceSession, +} from "../../services/desktop-fido2-user-interface.service"; + +@Component({ + standalone: true, + imports: [ + CommonModule, + RouterModule, + SectionHeaderComponent, + BitIconButtonComponent, + TableModule, + JslibModule, + IconModule, + ButtonModule, + DialogModule, + SectionComponent, + ItemModule, + BadgeModule, + ], + templateUrl: "fido2-excluded-ciphers.component.html", +}) +export class Fido2ExcludedCiphersComponent implements OnInit, OnDestroy { + session?: DesktopFido2UserInterfaceSession = null; + readonly Icons = { BitwardenShield, NoResults }; + + constructor( + private readonly desktopSettingsService: DesktopSettingsService, + private readonly fido2UserInterfaceService: DesktopFido2UserInterfaceService, + private readonly accountService: AccountService, + private readonly router: Router, + ) {} + + async ngOnInit(): Promise { + this.session = this.fido2UserInterfaceService.getCurrentSession(); + } + + async ngOnDestroy(): Promise { + await this.closeModal(); + } + + async closeModal(): Promise { + // Clean up modal state + await this.desktopSettingsService.setModalMode(false); + await this.accountService.setShowHeader(true); + + // Clean up session state + if (this.session) { + this.session.notifyConfirmCreateCredential(false); + this.session.confirmChosenCipher(null); + } + + // Navigate away + await this.router.navigate(["/"]); + } +} diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html new file mode 100644 index 00000000000..ed04993d09f --- /dev/null +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.html @@ -0,0 +1,37 @@ +
+ + +
+ + +

{{ "passkeyLogin" | i18n }}

+
+ +
+
+ + + + + {{ c.subTitle }} + {{ "select" | i18n }} + + + +
diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts new file mode 100644 index 00000000000..70ef4461f6a --- /dev/null +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts @@ -0,0 +1,196 @@ +import { NO_ERRORS_SCHEMA } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { Router } from "@angular/router"; +import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service"; +import { + DesktopFido2UserInterfaceService, + DesktopFido2UserInterfaceSession, +} from "../../services/desktop-fido2-user-interface.service"; + +import { Fido2VaultComponent } from "./fido2-vault.component"; + +describe("Fido2VaultComponent", () => { + let component: Fido2VaultComponent; + let fixture: ComponentFixture; + let mockDesktopSettingsService: MockProxy; + let mockFido2UserInterfaceService: MockProxy; + let mockCipherService: MockProxy; + let mockAccountService: MockProxy; + let mockLogService: MockProxy; + let mockPasswordRepromptService: MockProxy; + let mockRouter: MockProxy; + let mockSession: MockProxy; + let mockI18nService: MockProxy; + + const mockActiveAccount = { id: "test-user-id", email: "test@example.com" }; + const mockCipherIds = ["cipher-1", "cipher-2", "cipher-3"]; + + beforeEach(async () => { + mockDesktopSettingsService = mock(); + mockFido2UserInterfaceService = mock(); + mockCipherService = mock(); + mockAccountService = mock(); + mockLogService = mock(); + mockPasswordRepromptService = mock(); + mockRouter = mock(); + mockSession = mock(); + mockI18nService = mock(); + + mockAccountService.activeAccount$ = of(mockActiveAccount as Account); + mockFido2UserInterfaceService.getCurrentSession.mockReturnValue(mockSession); + mockSession.availableCipherIds$ = of(mockCipherIds); + mockCipherService.cipherListViews$ = jest.fn().mockReturnValue(of([])); + + await TestBed.configureTestingModule({ + imports: [Fido2VaultComponent], + providers: [ + { provide: DesktopSettingsService, useValue: mockDesktopSettingsService }, + { provide: DesktopFido2UserInterfaceService, useValue: mockFido2UserInterfaceService }, + { provide: CipherService, useValue: mockCipherService }, + { provide: AccountService, useValue: mockAccountService }, + { provide: LogService, useValue: mockLogService }, + { provide: PasswordRepromptService, useValue: mockPasswordRepromptService }, + { provide: Router, useValue: mockRouter }, + { provide: I18nService, useValue: mockI18nService }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(Fido2VaultComponent); + component = fixture.componentInstance; + }); + + const mockCiphers: any[] = [ + { + id: "cipher-1", + name: "Test Cipher 1", + type: CipherType.Login, + login: { + username: "test1@example.com", + }, + reprompt: CipherRepromptType.None, + deletedDate: null, + }, + { + id: "cipher-2", + name: "Test Cipher 2", + type: CipherType.Login, + login: { + username: "test2@example.com", + }, + reprompt: CipherRepromptType.None, + deletedDate: null, + }, + { + id: "cipher-3", + name: "Test Cipher 3", + type: CipherType.Login, + login: { + username: "test3@example.com", + }, + reprompt: CipherRepromptType.Password, + deletedDate: null, + }, + ]; + + describe("ngOnInit", () => { + it("should initialize session and load ciphers successfully", async () => { + mockCipherService.cipherListViews$ = jest.fn().mockReturnValue(of(mockCiphers)); + + await component.ngOnInit(); + + expect(mockFido2UserInterfaceService.getCurrentSession).toHaveBeenCalled(); + expect(component.session).toBe(mockSession); + expect(component.cipherIds$).toBe(mockSession.availableCipherIds$); + expect(mockCipherService.cipherListViews$).toHaveBeenCalledWith(mockActiveAccount.id); + }); + + it("should handle when no active session found", async () => { + mockFido2UserInterfaceService.getCurrentSession.mockReturnValue(null); + + await component.ngOnInit(); + + expect(component.session).toBeNull(); + }); + + it("should filter out deleted ciphers", async () => { + const ciphersWithDeleted = [ + ...mockCiphers.slice(0, 1), + { ...mockCiphers[1], deletedDate: new Date() }, + ...mockCiphers.slice(2), + ]; + mockCipherService.cipherListViews$ = jest.fn().mockReturnValue(of(ciphersWithDeleted)); + + await component.ngOnInit(); + await new Promise((resolve) => setTimeout(resolve, 0)); + + let ciphersResult: CipherView[] = []; + component.ciphers$.subscribe((ciphers) => { + ciphersResult = ciphers; + }); + + expect(ciphersResult).toHaveLength(2); + expect(ciphersResult.every((cipher) => !cipher.deletedDate)).toBe(true); + }); + }); + + describe("chooseCipher", () => { + const cipher = mockCiphers[0]; + + beforeEach(() => { + component.session = mockSession; + }); + + it("should choose cipher when access is validated", async () => { + cipher.reprompt = CipherRepromptType.None; + + await component.chooseCipher(cipher); + + expect(mockSession.confirmChosenCipher).toHaveBeenCalledWith(cipher.id, true); + expect(mockRouter.navigate).toHaveBeenCalledWith(["/"]); + }); + + it("should prompt for password when cipher requires reprompt", async () => { + cipher.reprompt = CipherRepromptType.Password; + mockPasswordRepromptService.showPasswordPrompt.mockResolvedValue(true); + + await component.chooseCipher(cipher); + + expect(mockPasswordRepromptService.showPasswordPrompt).toHaveBeenCalled(); + expect(mockSession.confirmChosenCipher).toHaveBeenCalledWith(cipher.id, true); + }); + + it("should not choose cipher when password reprompt is cancelled", async () => { + cipher.reprompt = CipherRepromptType.Password; + mockPasswordRepromptService.showPasswordPrompt.mockResolvedValue(false); + + await component.chooseCipher(cipher); + + expect(mockPasswordRepromptService.showPasswordPrompt).toHaveBeenCalled(); + expect(mockSession.confirmChosenCipher).toHaveBeenCalledWith(cipher.id, false); + }); + }); + + describe("closeModal", () => { + it("should close modal and notify session", async () => { + component.session = mockSession; + + await component.closeModal(); + + expect(mockRouter.navigate).toHaveBeenCalledWith(["/"]); + expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(false); + expect(mockSession.confirmChosenCipher).toHaveBeenCalledWith(null); + }); + }); +}); diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts new file mode 100644 index 00000000000..6faa3430b1c --- /dev/null +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts @@ -0,0 +1,162 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnInit, OnDestroy } from "@angular/core"; +import { RouterModule, Router } from "@angular/router"; +import { + firstValueFrom, + map, + combineLatest, + of, + BehaviorSubject, + Observable, + Subject, + takeUntil, +} from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { BitwardenShield } from "@bitwarden/assets/svg"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + BadgeModule, + ButtonModule, + DialogModule, + DialogService, + IconModule, + ItemModule, + SectionComponent, + TableModule, + BitIconButtonComponent, + SectionHeaderComponent, +} from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service"; +import { + DesktopFido2UserInterfaceService, + DesktopFido2UserInterfaceSession, +} from "../../services/desktop-fido2-user-interface.service"; + +@Component({ + standalone: true, + imports: [ + CommonModule, + RouterModule, + SectionHeaderComponent, + BitIconButtonComponent, + TableModule, + JslibModule, + IconModule, + ButtonModule, + DialogModule, + SectionComponent, + ItemModule, + BadgeModule, + ], + templateUrl: "fido2-vault.component.html", +}) +export class Fido2VaultComponent implements OnInit, OnDestroy { + session?: DesktopFido2UserInterfaceSession = null; + private destroy$ = new Subject(); + private ciphersSubject = new BehaviorSubject([]); + ciphers$: Observable = this.ciphersSubject.asObservable(); + cipherIds$: Observable | undefined; + readonly Icons = { BitwardenShield }; + + constructor( + private readonly desktopSettingsService: DesktopSettingsService, + private readonly fido2UserInterfaceService: DesktopFido2UserInterfaceService, + private readonly cipherService: CipherService, + private readonly accountService: AccountService, + private readonly dialogService: DialogService, + private readonly logService: LogService, + private readonly passwordRepromptService: PasswordRepromptService, + private readonly router: Router, + ) {} + + async ngOnInit(): Promise { + this.session = this.fido2UserInterfaceService.getCurrentSession(); + this.cipherIds$ = this.session?.availableCipherIds$; + await this.loadCiphers(); + } + + async ngOnDestroy(): Promise { + this.destroy$.next(); + this.destroy$.complete(); + } + + async chooseCipher(cipher: CipherView): Promise { + if (!this.session) { + await this.dialogService.openSimpleDialog({ + title: { key: "unexpectedErrorShort" }, + content: { key: "closeThisBitwardenWindow" }, + type: "danger", + acceptButtonText: { key: "closeThisWindow" }, + cancelButtonText: null, + }); + await this.closeModal(); + + return; + } + + const isConfirmed = await this.validateCipherAccess(cipher); + this.session.confirmChosenCipher(cipher.id, isConfirmed); + + await this.closeModal(); + } + + async closeModal(): Promise { + await this.desktopSettingsService.setModalMode(false); + await this.accountService.setShowHeader(true); + + if (this.session) { + this.session.notifyConfirmCreateCredential(false); + this.session.confirmChosenCipher(null); + } + + await this.router.navigate(["/"]); + } + + private async loadCiphers(): Promise { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + if (!activeUserId) { + return; + } + + // Combine cipher list with optional cipher IDs filter + combineLatest([this.cipherService.cipherListViews$(activeUserId), this.cipherIds$ || of(null)]) + .pipe( + map(([ciphers, cipherIds]) => { + // Filter out deleted ciphers + const activeCiphers = ciphers.filter((cipher) => !cipher.deletedDate); + + // If specific IDs provided, filter by them + if (cipherIds?.length > 0) { + return activeCiphers.filter((cipher) => cipherIds.includes(cipher.id as string)); + } + + return activeCiphers; + }), + takeUntil(this.destroy$), + ) + .subscribe({ + next: (ciphers) => this.ciphersSubject.next(ciphers as CipherView[]), + error: (error: unknown) => this.logService.error("Failed to load ciphers", error), + }); + } + + private async validateCipherAccess(cipher: CipherView): Promise { + if (cipher.reprompt !== CipherRepromptType.None) { + return this.passwordRepromptService.showPasswordPrompt(); + } else { + let cred = cipher.login.fido2Credentials[0]; + const username = cred.userName ?? cred.userDisplayName + return this.session.promptForUserVerification(username, "Verify it's you to log in") + } + } +} diff --git a/apps/desktop/src/autofill/preload.ts b/apps/desktop/src/autofill/preload.ts index fcb2f646743..b0c2d5a49a6 100644 --- a/apps/desktop/src/autofill/preload.ts +++ b/apps/desktop/src/autofill/preload.ts @@ -9,6 +9,8 @@ export default { runCommand: (params: RunCommandParams): Promise> => ipcRenderer.invoke("autofill.runCommand", params), + listenerReady: () => ipcRenderer.send("autofill.listenerReady"), + listenPasskeyRegistration: ( fn: ( clientId: number, @@ -32,6 +34,7 @@ export default { ) => { const { clientId, sequenceNumber, request } = data; fn(clientId, sequenceNumber, request, (error, response) => { + console.log("autofill.passkeyRegistration IPC response", error, response) if (error) { ipcRenderer.send("autofill.completeError", { clientId, @@ -127,6 +130,25 @@ export default { }, ); }, + + listenNativeStatus: ( + fn: (clientId: number, sequenceNumber: number, status: { key: string; value: string }) => void, + ) => { + ipcRenderer.on( + "autofill.nativeStatus", + ( + event, + data: { + clientId: number; + sequenceNumber: number; + status: { key: string; value: string }; + }, + ) => { + const { clientId, sequenceNumber, status } = data; + fn(clientId, sequenceNumber, status); + }, + ); + }, configureAutotype: (enabled: boolean, keyboardShortcut: string[]) => { ipcRenderer.send("autofill.configureAutotype", { enabled, keyboardShortcut }); }, @@ -166,4 +188,42 @@ export default { }, ); }, + listenLockStatusQuery: ( + fn: ( + clientId: number, + sequenceNumber: number, + request: autofill.LockStatusQueryRequest, + completeCallback: (error: Error | null, response: autofill.LockStatusQueryResponse) => void, + ) => void, + ) => { + ipcRenderer.on( + "autofill.lockStatusQuery", + ( + event, + data: { + clientId: number; + sequenceNumber: number; + request: autofill.LockStatusQueryRequest; + }, + ) => { + const { clientId, sequenceNumber, request } = data; + fn(clientId, sequenceNumber, request, (error, response) => { + if (error) { + ipcRenderer.send("autofill.completeError", { + clientId, + sequenceNumber, + error: error.message, + }); + return; + } + + ipcRenderer.send("autofill.completeLockStatusQuery", { + clientId, + sequenceNumber, + response, + }); + }); + }, + ); + }, }; diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index 7f443549c5b..5d30712f659 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -1,6 +1,8 @@ import { Injectable, OnDestroy } from "@angular/core"; import { Subject, + combineLatest, + debounceTime, distinctUntilChanged, filter, firstValueFrom, @@ -8,10 +10,11 @@ import { mergeMap, switchMap, takeUntil, - EMPTY, } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; @@ -40,12 +43,18 @@ import { NativeAutofillPasswordCredential, NativeAutofillSyncCommand, } from "../../platform/main/autofill/sync.command"; +import { NativeAutofillUserVerificationCommand } from "../../platform/main/autofill/user-verification.command"; import type { NativeWindowObject } from "./desktop-fido2-user-interface.service"; +import { DeviceType } from "@bitwarden/common/enums"; + +const NativeCredentialSyncFeatureFlag = ipc.platform.deviceType === DeviceType.WindowsDesktop ? FeatureFlag.WindowsNativeCredentialSync : FeatureFlag.MacOsNativeCredentialSync; @Injectable() export class DesktopAutofillService implements OnDestroy { private destroy$ = new Subject(); + private registrationRequest: autofill.PasskeyRegistrationRequest; + private inFlightRequests: Record = {}; constructor( private logService: LogService, @@ -53,71 +62,108 @@ export class DesktopAutofillService implements OnDestroy { private configService: ConfigService, private fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction, private accountService: AccountService, + private authService: AuthService, ) {} async init() { this.configService - .getFeatureFlag$(FeatureFlag.MacOsNativeCredentialSync) + .getFeatureFlag$(NativeCredentialSyncFeatureFlag) .pipe( distinctUntilChanged(), - switchMap((enabled) => { - return this.accountService.activeAccount$.pipe( - map((account) => account?.id), - filter((userId): userId is UserId => userId != null), - switchMap((userId) => this.cipherService.cipherViews$(userId)), + filter((enabled) => enabled === true), // Only proceed if feature is enabled + switchMap(() => { + return combineLatest([ + this.accountService.activeAccount$.pipe( + map((account) => account?.id), + filter((userId): userId is UserId => userId != null), + ), + this.authService.activeAccountStatus$, + ]).pipe( + // Only proceed when the vault is unlocked + filter(([, status]) => status === AuthenticationStatus.Unlocked), + // Then get cipher views + switchMap(([userId]) => this.cipherService.cipherViews$(userId)), ); }), - // TODO: This will unset all the autofill credentials on the OS - // when the account locks. We should instead explicilty clear the credentials - // when the user logs out. Maybe by subscribing to the encrypted ciphers observable instead. + // No filter for empty arrays here - we want to sync even if there are 0 items + filter((cipherViewMap) => cipherViewMap !== null), + debounceTime(100), // just a precaution to not spam the sync if there are multiple changes + mergeMap((cipherViewMap) => this.sync(Object.values(cipherViewMap ?? []))), takeUntil(this.destroy$), ) .subscribe(); + // Listen for sign out to clear credentials + this.authService.activeAccountStatus$ + .pipe( + filter((status) => status === AuthenticationStatus.LoggedOut), + mergeMap(() => this.sync([])), // sync an empty array + takeUntil(this.destroy$), + ) + .subscribe(); + this.listenIpc(); } + async adHocSync(): Promise { + this.logService.debug("Performing AdHoc sync"); + const account = await firstValueFrom(this.accountService.activeAccount$); + const userId = account?.id; + + if (!userId) { + throw new Error("No active user found"); + } + + const cipherViewMap = await firstValueFrom(this.cipherService.cipherViews$(userId)); + this.logService.info("Performing AdHoc sync", Object.values(cipherViewMap ?? [])); + await this.sync(Object.values(cipherViewMap ?? [])); + } + /** Give metadata about all available credentials in the users vault */ async sync(cipherViews: CipherView[]) { - 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); - // } + 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[]; + let fido2Credentials: NativeAutofillFido2Credential[] = []; + let passwordCredentials: NativeAutofillPasswordCredential[] = []; - fido2Credentials = (await getCredentialsForAutofill(cipherViews)).map((credential) => ({ - type: "fido2", - ...credential, - })); + 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, + })); + } - // Mock a couple of passkeys for testing purposes - fido2Credentials.push({ - type: "fido2", - cipherId: "mock-cipher-id-1", - credentialId: "passkey1", - rpId: "webauthn.io", - userHandle: "passkey1", - userName: "Mock passkey1", + if (status.value.support.fido2) { + fido2Credentials = (await getCredentialsForAutofill(cipherViews)).map((credential) => ({ + type: "fido2", + ...credential, + })); + } + + this.logService.info("Syncing autofill credentials", { + fido2Credentials, + passwordCredentials, }); - fido2Credentials.push({ - type: "fido2", - cipherId: "mock-cipher-id-2", - credentialId: "passkey2", - rpId: "webauthn.io", - userHandle: "passkey2", - userName: "Mock passkey2", - }); - - this.logService.info("Found FIDO2 credentials", fido2Credentials.length); const syncResult = await ipc.autofill.runCommand({ namespace: "autofill", @@ -144,107 +190,210 @@ export class DesktopAutofillService implements OnDestroy { }); } + get lastRegistrationRequest() { + return this.registrationRequest; + } + listenIpc() { - ipc.autofill.listenPasskeyRegistration((clientId, sequenceNumber, request, callback) => { - this.logService.warning("listenPasskeyRegistration", clientId, sequenceNumber, request); - this.logService.warning( - "listenPasskeyRegistration2", - this.convertRegistrationRequest(request), - ); + this.logService.debug("Setting up Native -> Electron IPC Handlers") + ipc.autofill.listenPasskeyRegistration(async (clientId, sequenceNumber, request, callback) => { + if (!(await this.configService.getFeatureFlag(NativeCredentialSyncFeatureFlag))) { + this.logService.debug( + `listenPasskeyRegistration: ${NativeCredentialSyncFeatureFlag} feature flag is disabled`, + ); + callback(new Error(`${NativeCredentialSyncFeatureFlag} feature flag is disabled`), null); + return; + } + + this.registrationRequest = request; + + this.logService.debug("listenPasskeyRegistration", clientId, sequenceNumber, request); + this.logService.debug("listenPasskeyRegistration2", this.convertRegistrationRequest(request)); const controller = new AbortController(); - void this.fido2AuthenticatorService - .makeCredential( + if (request.context) { + this.inFlightRequests[request.context] = controller; + } + + const clientHandle = request.clientWindowHandle ? new Uint8Array(request.clientWindowHandle) : null; + try { + const response = await this.fido2AuthenticatorService.makeCredential( this.convertRegistrationRequest(request), - { windowXy: request.windowXy }, + { windowXy: request.windowXy, handle: clientHandle }, controller, - ) - .then((response) => { - callback(null, this.convertRegistrationResponse(request, response)); - }) - .catch((error) => { - this.logService.error("listenPasskeyRegistration error", error); - callback(error, null); - }); + request.context, + ); + + this.logService.debug("Sending registration response to plugin via callback"); + callback(null, this.convertRegistrationResponse(request, response)); + } catch (error) { + this.logService.error("listenPasskeyRegistration error", error); + callback(error, null); + } + finally { + if (request.context) { + delete this.inFlightRequests[request.context]; + } + } + this.logService.info("Passkey registration completed.") }); ipc.autofill.listenPasskeyAssertionWithoutUserInterface( async (clientId, sequenceNumber, request, callback) => { - this.logService.warning( + if (!(await this.configService.getFeatureFlag(NativeCredentialSyncFeatureFlag))) { + this.logService.debug( + `listenPasskeyAssertionWithoutUserInterface: ${NativeCredentialSyncFeatureFlag} feature flag is disabled`, + ); + callback(new Error(`${NativeCredentialSyncFeatureFlag} feature flag is disabled`), null); + return; + } + + this.logService.debug( "listenPasskeyAssertion without user interface", clientId, sequenceNumber, request, ); - // For some reason the credentialId is passed as an empty array in the request, so we need to - // get it from the cipher. For that we use the recordIdentifier, which is the cipherId. - if (request.recordIdentifier && request.credentialId.length === 0) { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(getOptionalUserId), - ); - if (!activeUserId) { - this.logService.error("listenPasskeyAssertion error", "Active user not found"); - callback(new Error("Active user not found"), null); - return; - } - - const cipher = await this.cipherService.get(request.recordIdentifier, activeUserId); - if (!cipher) { - this.logService.error("listenPasskeyAssertion error", "Cipher not found"); - callback(new Error("Cipher not found"), null); - return; - } - - const decrypted = await this.cipherService.decrypt(cipher, activeUserId); - - const fido2Credential = decrypted.login.fido2Credentials?.[0]; - if (!fido2Credential) { - this.logService.error("listenPasskeyAssertion error", "Fido2Credential not found"); - callback(new Error("Fido2Credential not found"), null); - return; - } - - request.credentialId = Array.from( - new Uint8Array(parseCredentialId(decrypted.login.fido2Credentials?.[0].credentialId)), - ); + const controller = new AbortController(); + if (request.context) { + this.inFlightRequests[request.context] = controller; } - const controller = new AbortController(); - void this.fido2AuthenticatorService - .getAssertion( - this.convertAssertionRequest(request), - { windowXy: request.windowXy }, + try { + // For some reason the credentialId is passed as an empty array in the request, so we need to + // get it from the cipher. For that we use the recordIdentifier, which is the cipherId. + if (request.recordIdentifier && request.credentialId.length === 0) { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getOptionalUserId), + ); + if (!activeUserId) { + this.logService.error("listenPasskeyAssertion error", "Active user not found"); + callback(new Error("Active user not found"), null); + return; + } + + const cipher = await this.cipherService.get(request.recordIdentifier, activeUserId); + if (!cipher) { + this.logService.error("listenPasskeyAssertion error", "Cipher not found"); + callback(new Error("Cipher not found"), null); + return; + } + + const decrypted = await this.cipherService.decrypt(cipher, activeUserId); + + const fido2Credential = decrypted.login.fido2Credentials?.[0]; + if (!fido2Credential) { + this.logService.error("listenPasskeyAssertion error", "Fido2Credential not found"); + callback(new Error("Fido2Credential not found"), null); + return; + } + + request.credentialId = Array.from( + new Uint8Array(parseCredentialId(decrypted.login.fido2Credentials?.[0].credentialId)), + ); + } + + const clientHandle = request.clientWindowHandle ? new Uint8Array(request.clientWindowHandle) : null; + const response = await this.fido2AuthenticatorService.getAssertion( + this.convertAssertionRequest(request, true), + { windowXy: request.windowXy, handle: clientHandle }, controller, - ) - .then((response) => { - callback(null, this.convertAssertionResponse(request, response)); - }) - .catch((error) => { - this.logService.error("listenPasskeyAssertion error", error); - callback(error, null); - }); + request.context + ); + + callback(null, this.convertAssertionResponse(request, response)); + } catch (error) { + this.logService.error("listenPasskeyAssertion error", error); + callback(error, null); + return; + } + finally { + if (request.context) { + delete this.inFlightRequests[request.context]; + } + } }, ); ipc.autofill.listenPasskeyAssertion(async (clientId, sequenceNumber, request, callback) => { - this.logService.warning("listenPasskeyAssertion", clientId, sequenceNumber, request); + if (!(await this.configService.getFeatureFlag(NativeCredentialSyncFeatureFlag))) { + this.logService.debug( + `listenPasskeyAssertion: ${NativeCredentialSyncFeatureFlag} feature flag is disabled`, + ); + callback(new Error(`${NativeCredentialSyncFeatureFlag} feature flag is disabled`), null); + return; + } + this.logService.debug("listenPasskeyAssertion", clientId, sequenceNumber, request); const controller = new AbortController(); - void this.fido2AuthenticatorService - .getAssertion( + if (request.context) { + this.inFlightRequests[request.context] = controller; + } + + const clientHandle = request.clientWindowHandle ? new Uint8Array(request.clientWindowHandle) : null; + try { + const response = await this.fido2AuthenticatorService.getAssertion( this.convertAssertionRequest(request), - { windowXy: request.windowXy }, + { windowXy: request.windowXy, handle: clientHandle }, controller, - ) - .then((response) => { - callback(null, this.convertAssertionResponse(request, response)); - }) - .catch((error) => { - this.logService.error("listenPasskeyAssertion error", error); - callback(error, null); - }); + request.context, + ); + + callback(null, this.convertAssertionResponse(request, response)); + } catch (error) { + this.logService.error("listenPasskeyAssertion error", error); + callback(error, null); + } + finally { + if (request.context) { + delete this.inFlightRequests[request.context]; + } + } }); + + // Listen for native status messages + ipc.autofill.listenNativeStatus(async (clientId, sequenceNumber, status) => { + if (!(await this.configService.getFeatureFlag(NativeCredentialSyncFeatureFlag))) { + this.logService.debug( + `listenNativeStatus: ${NativeCredentialSyncFeatureFlag} feature flag is disabled`, + ); + return; + } + + this.logService.info("Received native status", status.key, status.value); + if (status.key === "request-sync") { + // perform ad-hoc sync + await this.adHocSync(); + } + + if (status.key === "cancel-operation" && status.value) { + const requestId = status.value + const controller = this.inFlightRequests[requestId] + if (controller) { + this.logService.debug(`Cancelling request ${requestId}`); + controller.abort("Operation cancelled") + } + else { + this.logService.debug(`Unknown request: ${requestId}`); + } + } + }); + + ipc.autofill.listenLockStatusQuery(async (clientId, sequenceNumber, request, callback) => { + if (!(await this.configService.getFeatureFlag(NativeCredentialSyncFeatureFlag))) { + this.logService.debug( + `listenLockStatusQuery: ${NativeCredentialSyncFeatureFlag} feature flag is disabled`, + ); + return; + } + + this.logService.debug("listenLockStatusQuery", clientId, sequenceNumber, request); + const isUnlocked = await firstValueFrom(this.authService.activeAccountStatus$) === AuthenticationStatus.Unlocked; + callback(null, { isUnlocked }) + }) + + ipc.autofill.listenerReady(); } private convertRegistrationRequest( @@ -266,7 +415,10 @@ export class DesktopAutofillService implements OnDestroy { alg, type: "public-key", })), - excludeCredentialDescriptorList: [], + excludeCredentialDescriptorList: request.excludedCredentials.map((credentialId) => ({ + id: new Uint8Array(credentialId), + type: "public-key" as const, + })), requireResidentKey: true, requireUserVerification: request.userVerification === "required" || request.userVerification === "preferred", @@ -298,18 +450,19 @@ export class DesktopAutofillService implements OnDestroy { request: | autofill.PasskeyAssertionRequest | autofill.PasskeyAssertionWithoutUserInterfaceRequest, + assumeUserPresence: boolean = false, ): Fido2AuthenticatorGetAssertionParams { let allowedCredentials; if ("credentialId" in request) { allowedCredentials = [ { - id: new Uint8Array(request.credentialId), + id: new Uint8Array(request.credentialId).buffer, type: "public-key" as const, }, ]; } else { allowedCredentials = request.allowedCredentials.map((credentialId) => ({ - id: new Uint8Array(credentialId), + id: new Uint8Array(credentialId).buffer, type: "public-key" as const, })); } @@ -322,7 +475,7 @@ export class DesktopAutofillService implements OnDestroy { requireUserVerification: request.userVerification === "required" || request.userVerification === "preferred", fallbackSupported: false, - assumeUserPresence: true, // For desktop assertions, it's safe to assume UP has been checked by OS dialogues + assumeUserPresence, }; } @@ -346,4 +499,4 @@ export class DesktopAutofillService implements OnDestroy { this.destroy$.next(); this.destroy$.complete(); } -} +} \ No newline at end of file diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index 3caf13fa5b7..253b2ea498d 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -32,6 +32,8 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; +import { NativeAutofillUserVerificationCommand } from "../../platform/main/autofill/user-verification.command"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; /** * This type is used to pass the window position from the native UI @@ -41,6 +43,7 @@ export type NativeWindowObject = { * The position of the window, first entry is the x position, second is the y position */ windowXy?: { x: number; y: number }; + handle?: Uint8Array; }; export class DesktopFido2UserInterfaceService @@ -65,8 +68,9 @@ export class DesktopFido2UserInterfaceService fallbackSupported: boolean, nativeWindowObject: NativeWindowObject, abortController?: AbortController, + transactionContext?: string, ): Promise { - this.logService.warning("newSession", fallbackSupported, abortController, nativeWindowObject); + this.logService.debug("newSession", fallbackSupported, abortController, nativeWindowObject, transactionContext); const session = new DesktopFido2UserInterfaceSession( this.authService, this.cipherService, @@ -75,6 +79,8 @@ export class DesktopFido2UserInterfaceService this.router, this.desktopSettingsService, nativeWindowObject, + abortController, + transactionContext, ); this.currentSession = session; @@ -91,12 +97,16 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi private router: Router, private desktopSettingsService: DesktopSettingsService, private windowObject: NativeWindowObject, + private abortController: AbortController, + private transactionContext: string, ) {} private confirmCredentialSubject = new Subject(); - private createdCipher: Cipher; - private availableCipherIdsSubject = new BehaviorSubject(null); + private updatedCipher: CipherView; + + private rpId = new BehaviorSubject(null); + private availableCipherIdsSubject = new BehaviorSubject([""]); /** * Observable that emits available cipher IDs once they're confirmed by the UI */ @@ -114,7 +124,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi assumeUserPresence, masterPasswordRepromptRequired, }: PickCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> { - this.logService.warning("pickCredential desktop function", { + this.logService.debug("pickCredential desktop function", { cipherIds, userVerification, assumeUserPresence, @@ -123,35 +133,72 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi try { // Check if we can return the credential without user interaction + await this.accountService.setShowHeader(false); if (assumeUserPresence && cipherIds.length === 1 && !masterPasswordRepromptRequired) { - this.logService.debug( - "shortcut - Assuming user presence and returning cipherId", - cipherIds[0], - ); - return { cipherId: cipherIds[0], userVerified: userVerification }; + const selectedCipherId = cipherIds[0]; + if (userVerification) { + // retrieve the cipher + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + if (!activeUserId) { + return; + } + const cipherView = await firstValueFrom(this.cipherService.cipherListViews$(activeUserId).pipe(map((ciphers) => { + return ciphers.find((cipher) => cipher.id == selectedCipherId && !cipher.deletedDate) as CipherView; + }))); + + let cred = cipherView.login.fido2Credentials[0]; + const username = cred.userName ?? cred.userDisplayName + try { + // TODO: internationalization + const isConfirmed = await this.promptForUserVerification(username, "Verify it's you to log in with Bitwarden."); + return { cipherId: cipherIds[0], userVerified: isConfirmed }; + } + catch (e) { + this.logService.debug("Failed to prompt for user verification without showing UI", e) + } + } + else { + this.logService.warning( + "shortcut - Assuming user presence and returning cipherId", + cipherIds[0], + ); + return { cipherId: cipherIds[0], userVerified: userVerification }; + } } this.logService.debug("Could not shortcut, showing UI"); + // TODO: We need to pass context from the original request whether this + // should be a silent request or not. Then, we can fail here if it's + // supposed to be silent. + // make the cipherIds available to the UI. this.availableCipherIdsSubject.next(cipherIds); - await this.showUi("/passkeys", this.windowObject.windowXy); + await this.showUi("/fido2-assertion", this.windowObject.windowXy, false); const chosenCipherResponse = await this.waitForUiChosenCipher(); this.logService.debug("Received chosen cipher", chosenCipherResponse); return { - cipherId: chosenCipherResponse.cipherId, - userVerified: chosenCipherResponse.userVerified, + cipherId: chosenCipherResponse?.cipherId, + userVerified: chosenCipherResponse?.userVerified, }; } finally { // Make sure to clean up so the app is never stuck in modal mode? await this.desktopSettingsService.setModalMode(false); + await this.accountService.setShowHeader(true); } } + async getRpId(): Promise { + return firstValueFrom(this.rpId.pipe(filter((id) => id != null))); + } + confirmChosenCipher(cipherId: string, userVerified: boolean = false): void { this.chosenCipherSubject.next({ cipherId, userVerified }); this.chosenCipherSubject.complete(); @@ -159,22 +206,32 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi private async waitForUiChosenCipher( timeoutMs: number = 60000, - ): Promise<{ cipherId: string; userVerified: boolean } | undefined> { + ): Promise<{ cipherId?: string; userVerified: boolean } | undefined> { + const { promise: cancelPromise, listener: abortFn } = this.subscribeToCancellation(); try { - return await lastValueFrom(this.chosenCipherSubject.pipe(timeout(timeoutMs))); - } catch { + this.abortController.signal.throwIfAborted(); + const confirmPromise = lastValueFrom(this.chosenCipherSubject.pipe(timeout(timeoutMs))); + return await Promise.race([confirmPromise, cancelPromise]); + } catch (e) { // If we hit a timeout, return undefined instead of throwing + this.logService.debug("Timed out or cancelled?", e); this.logService.warning("Timeout: User did not select a cipher within the allowed time", { timeoutMs, }); return { cipherId: undefined, userVerified: false }; } + finally { + this.unsusbscribeCancellation(abortFn); + } } /** * Notifies the Fido2UserInterfaceSession that the UI operations has completed and it can return to the OS. */ - notifyConfirmNewCredential(confirmed: boolean): void { + notifyConfirmCreateCredential(confirmed: boolean, updatedCipher?: CipherView): void { + if (updatedCipher) { + this.updatedCipher = updatedCipher; + } this.confirmCredentialSubject.next(confirmed); this.confirmCredentialSubject.complete(); } @@ -184,7 +241,19 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi * @returns */ private async waitForUiNewCredentialConfirmation(): Promise { - return lastValueFrom(this.confirmCredentialSubject); + const { promise: cancelPromise, listener: abortFn } = this.subscribeToCancellation(); + try { + this.abortController.signal.throwIfAborted(); + const confirmPromise = lastValueFrom(this.confirmCredentialSubject); + return await Promise.race([confirmPromise, cancelPromise]); + } catch (e) { + // If we hit a timeout, return undefined instead of throwing + this.logService.debug("Timed out or cancelled?", e); + return undefined; + } + finally { + this.unsusbscribeCancellation(abortFn); + } } /** @@ -195,60 +264,79 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi async confirmNewCredential({ credentialName, userName, + userHandle, userVerification, rpId, }: NewCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> { - this.logService.warning( + this.logService.debug( "confirmNewCredential", credentialName, userName, + userHandle, userVerification, rpId, ); + this.rpId.next(rpId); try { - await this.showUi("/passkeys", this.windowObject.windowXy); + await this.showUi("/fido2-creation", this.windowObject.windowXy, false); // Wait for the UI to wrap up const confirmation = await this.waitForUiNewCredentialConfirmation(); if (!confirmation) { return { cipherId: undefined, userVerified: false }; } - // Create the credential - await this.createCredential({ - credentialName, - userName, - rpId, - userHandle: "", - userVerification, - }); - // wait for 10ms to help RXJS catch up(?) - // We sometimes get a race condition from this.createCredential not updating cipherService in time - //console.log("waiting 10ms.."); - //await new Promise((resolve) => setTimeout(resolve, 10)); - //console.log("Just waited 10ms"); - - // Return the new cipher (this.createdCipher) - return { cipherId: this.createdCipher.id, userVerified: userVerification }; + if (this.updatedCipher) { + await this.updateCredential(this.updatedCipher); + return { cipherId: this.updatedCipher.id, userVerified: userVerification }; + } else { + // Create the cipher + const createdCipher = await this.createCipher({ + credentialName, + userName, + rpId, + userHandle, + userVerification, + }); + return { cipherId: createdCipher.id, userVerified: userVerification }; + } } finally { // Make sure to clean up so the app is never stuck in modal mode? await this.desktopSettingsService.setModalMode(false); + await this.accountService.setShowHeader(true); } } - private async showUi(route: string, position?: { x: number; y: number }): Promise { + private async hideUi(): Promise { + await this.desktopSettingsService.setModalMode(false); + await this.router.navigate(["/"]); + } + + private async showUi( + route: string, + position?: { x: number; y: number }, + showTrafficButtons: boolean = false, + disableRedirect?: boolean, + ): Promise { // Load the UI: - await this.desktopSettingsService.setModalMode(true, position); - await this.router.navigate(["/passkeys"]); + await this.desktopSettingsService.setModalMode(true, showTrafficButtons, position); + await this.accountService.setShowHeader(showTrafficButtons); + await this.router.navigate([ + route, + { + "disable-redirect": disableRedirect || null, + }, + ]); } /** - * Can be called by the UI to create a new credential with user input etc. + * Can be called by the UI to create a new cipher with user input etc. * @param param0 */ - async createCredential({ credentialName, userName, rpId }: NewCredentialParams): Promise { + async createCipher({ credentialName, userName, rpId }: NewCredentialParams): Promise { // Store the passkey on a new cipher to avoid replacing something important + const cipher = new CipherView(); cipher.name = credentialName; @@ -267,32 +355,143 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); + if (!activeUserId) { + throw new Error("No active user ID found!"); + } + const encCipher = await this.cipherService.encrypt(cipher, activeUserId); - const createdCipher = await this.cipherService.createWithServer(encCipher); - this.createdCipher = createdCipher; + try { + const createdCipher = await this.cipherService.createWithServer(encCipher); - return createdCipher; + return createdCipher; + } catch { + throw new Error("Unable to create cipher"); + } + } + + /** Called by the UI to prompt the user for verification. May be fulfilled by the OS. */ + async promptForUserVerification(username: string, displayHint: string): Promise { + this.logService.info("DesktopFido2UserInterfaceSession] Prompting for user verification") + // If the UI was showing before (to unlock the vault), then use our + // window for the handle; otherwise, use the WebAuthn client's + // handle. + // + // For Windows, if the selected window handle is not in the foreground, then the Windows + // Hello dialog will also be in the background. + // + // TODO: modalState is just a proxy for what we actually want: whether the window is visible. + // We should add a way for services to query window visibility. + const modalState = await firstValueFrom(this.desktopSettingsService.modalMode$); + const windowHandle = modalState.isModalModeActive ? await ipc.platform.getNativeWindowHandle() : this.windowObject.handle; + + const uvRequest = ipc.autofill.runCommand({ + namespace: "autofill", + command: "user-verification", + params: { + windowHandle: Utils.fromBufferToB64(windowHandle), + transactionContext: this.transactionContext, + username, + displayHint, + }, + }); + // Ensure our window is hidden when showing the OS user verification dialog. + // TODO: This is prone to data races and, on Windows, may cause the Windows + // Hello dialog not to have keyboard input focus. We need a better solution + // than this. + this.hideUi(); + const uvResult = await uvRequest; + if (uvResult.type === "error") { + this.logService.error("Error getting user verification", uvResult.error); + return false; + } + return uvResult.type === "success"; + } + + async updateCredential(cipher: CipherView): Promise { + this.logService.info("updateCredential"); + await firstValueFrom( + this.accountService.activeAccount$.pipe( + map(async (a) => { + if (a) { + const encCipher = await this.cipherService.encrypt(cipher, a.id); + await this.cipherService.updateWithServer(encCipher); + } + }), + ), + ); } async informExcludedCredential(existingCipherIds: string[]): Promise { - this.logService.warning("informExcludedCredential", existingCipherIds); + this.logService.debug("informExcludedCredential", existingCipherIds); + + // make the cipherIds available to the UI. + this.availableCipherIdsSubject.next(existingCipherIds); + + await this.accountService.setShowHeader(false); + await this.showUi("/fido2-excluded", this.windowObject.windowXy, false); } async ensureUnlockedVault(): Promise { - this.logService.warning("ensureUnlockedVault"); + this.logService.debug("ensureUnlockedVault"); const status = await firstValueFrom(this.authService.activeAccountStatus$); if (status !== AuthenticationStatus.Unlocked) { - throw new Error("Vault is not unlocked"); + await this.showUi("/lock", this.windowObject.windowXy, true, true); + + let status2: AuthenticationStatus; + const { promise: cancelPromise, listener: abortFn } = this.subscribeToCancellation(); + try { + status2 = await Promise.race([lastValueFrom( + this.authService.activeAccountStatus$.pipe( + filter((s) => s === AuthenticationStatus.Unlocked), + take(1), + timeout(1000 * 60 * 5), // 5 minutes + ), + ), cancelPromise]); + } catch (error) { + this.logService.warning("Error while waiting for vault to unlock", error); + } + finally { + this.unsusbscribeCancellation(abortFn); + } + + if (status2 === AuthenticationStatus.Unlocked) { + await this.router.navigate(["/"]); + } + + if (status2 !== AuthenticationStatus.Unlocked) { + await this.hideUi(); + throw new Error("Vault is not unlocked"); + } } } async informCredentialNotFound(): Promise { - this.logService.warning("informCredentialNotFound"); + this.logService.debug("informCredentialNotFound"); } async close() { - this.logService.warning("close"); + this.logService.debug("close"); + } + + subscribeToCancellation() { + let cancelReject: (reason?: any) => void; + const cancelPromise: Promise = new Promise((_, reject) => { + cancelReject = reject + }); + const abortFn = (ev: Event) => { + if (ev.target instanceof AbortSignal) { + cancelReject(ev.target.reason) + } + }; + this.abortController.signal.addEventListener("abort", abortFn, { once: true }); + + return { promise: cancelPromise, listener: abortFn }; + + } + + unsusbscribeCancellation(listener: (ev: Event) => void): void { + this.abortController.signal.removeEventListener("abort", listener); } } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 70242b6674f..6e4a42bed5e 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -905,6 +905,12 @@ "unexpectedError": { "message": "An unexpected error has occurred." }, + "unexpectedErrorShort": { + "message": "Unexpected error" + }, + "closeThisBitwardenWindow": { + "message": "Close this Bitwarden window and try again." + }, "itemInformation": { "message": "Item information" }, @@ -3324,7 +3330,7 @@ "orgTrustWarning1": { "message": "This organization has an Enterprise policy that will enroll you in account recovery. Enrollment will allow organization administrators to change your password. Only proceed if you recognize this organization and the fingerprint phrase displayed below matches the organization's fingerprint." }, - "trustUser":{ + "trustUser": { "message": "Trust user" }, "inputRequired": { @@ -3854,6 +3860,75 @@ "fileSavedToDevice": { "message": "File saved to device. Manage from your device downloads." }, + "importantNotice": { + "message": "Important notice" + }, + "setupTwoStepLogin": { + "message": "Set up two-step login" + }, + "newDeviceVerificationNoticeContentPage1": { + "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + }, + "newDeviceVerificationNoticeContentPage2": { + "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + }, + "remindMeLater": { + "message": "Remind me later" + }, + "newDeviceVerificationNoticePageOneFormContent": { + "message": "Do you have reliable access to your email, $EMAIL$?", + "placeholders": { + "email": { + "content": "$1", + "example": "your_name@email.com" + } + } + }, + "newDeviceVerificationNoticePageOneEmailAccessNo": { + "message": "No, I do not" + }, + "newDeviceVerificationNoticePageOneEmailAccessYes": { + "message": "Yes, I can reliably access my email" + }, + "turnOnTwoStepLogin": { + "message": "Turn on two-step login" + }, + "changeAcctEmail": { + "message": "Change account email" + }, + "passkeyLogin": { + "message": "Log in with passkey?" + }, + "savePasskeyQuestion": { + "message": "Save passkey?" + }, + "saveNewPasskey": { + "message": "Save as new login" + }, + "savePasskeyNewLogin": { + "message": "Save passkey as new login" + }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, + "overwritePasskey": { + "message": "Overwrite passkey?" + }, + "unableToSavePasskey": { + "message": "Unable to save passkey" + }, + "alreadyContainsPasskey": { + "message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?" + }, + "passkeyAlreadyExists": { + "message": "A passkey already exists for this application." + }, + "applicationDoesNotSupportDuplicates": { + "message": "This application does not support duplicates." + }, + "closeThisWindow": { + "message": "Close this window" + }, "allowScreenshots": { "message": "Allow screen capture" }, diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index fbb83a1bf56..6bd0454bd21 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -303,8 +303,15 @@ export class Main { new ChromiumImporterService(); + this.nativeAutofillMain = new NativeAutofillMain(this.logService, this.windowMain); - void this.nativeAutofillMain.init(); + app + .whenReady() + .then(async () => { + this.logService.debug("Initializing native autofill") + await this.nativeAutofillMain.init(); + + }) this.mainDesktopAutotypeService = new MainDesktopAutotypeService( this.logService, diff --git a/apps/desktop/src/main/tray.main.ts b/apps/desktop/src/main/tray.main.ts index b7ddefe6e1b..81df6497ca8 100644 --- a/apps/desktop/src/main/tray.main.ts +++ b/apps/desktop/src/main/tray.main.ts @@ -53,9 +53,14 @@ export class TrayMain { }, { visible: isDev(), - label: "Fake Popup", + label: "Fake Popup Select", click: () => this.fakePopup(), }, + { + visible: isDev(), + label: "Fake Popup Create", + click: () => this.fakePopupCreate(), + }, { type: "separator" }, { label: this.i18nService.t("exit"), @@ -218,4 +223,8 @@ export class TrayMain { private async fakePopup() { await this.messagingService.send("loadurl", { url: "/passkeys", modal: true }); } + + private async fakePopupCreate() { + await this.messagingService.send("loadurl", { url: "/create-passkey", modal: true }); + } } diff --git a/apps/desktop/src/main/window.main.ts b/apps/desktop/src/main/window.main.ts index f8ea7551c47..ad1e50bf44f 100644 --- a/apps/desktop/src/main/window.main.ts +++ b/apps/desktop/src/main/window.main.ts @@ -100,10 +100,10 @@ export class WindowMain { applyMainWindowStyles(this.win, this.windowStates[mainWindowSizeKey]); // Because modal is used in front of another app, UX wise it makes sense to hide the main window when leaving modal mode. this.win.hide(); - } else if (!lastValue.isModalModeActive && newValue.isModalModeActive) { + } else if (newValue.isModalModeActive) { // Apply the popup modal styles this.logService.info("Applying popup modal styles", newValue.modalPosition); - applyPopupModalStyles(this.win, newValue.modalPosition); + applyPopupModalStyles(this.win, newValue.showTrafficButtons, newValue.modalPosition); this.win.show(); } }), @@ -272,7 +272,7 @@ export class WindowMain { this.win = new BrowserWindow({ width: this.windowStates[mainWindowSizeKey].width, height: this.windowStates[mainWindowSizeKey].height, - minWidth: 680, + minWidth: 600, minHeight: 500, x: this.windowStates[mainWindowSizeKey].x, y: this.windowStates[mainWindowSizeKey].y, @@ -403,6 +403,10 @@ export class WindowMain { if (this.createWindowCallback) { this.createWindowCallback(this.win); } + + ipcMain.handle("get-native-window-handle", (_event) => { + return this.win.getNativeWindowHandle().toString("base64"); + }); } // Retrieve the background color diff --git a/apps/desktop/src/platform/main/autofill/command.ts b/apps/desktop/src/platform/main/autofill/command.ts index a8b5548052b..2549e617679 100644 --- a/apps/desktop/src/platform/main/autofill/command.ts +++ b/apps/desktop/src/platform/main/autofill/command.ts @@ -1,5 +1,6 @@ import { NativeAutofillStatusCommand } from "./status.command"; import { NativeAutofillSyncCommand } from "./sync.command"; +import { NativeAutofillUserVerificationCommand } from "./user-verification.command"; export type CommandDefinition = { namespace: string; @@ -20,4 +21,4 @@ export type IpcCommandInvoker = ( ) => Promise>; /** A list of all available commands */ -export type Command = NativeAutofillSyncCommand | NativeAutofillStatusCommand; +export type Command = NativeAutofillSyncCommand | NativeAutofillStatusCommand | NativeAutofillUserVerificationCommand; diff --git a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts index a9e8e7c5abb..74aa188b86f 100644 --- a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts +++ b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts @@ -1,12 +1,16 @@ 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 { NativeAutofillWindowsMain } from "./native-autofill.windows.main"; + +type BufferedMessage = { + channel: string; + data: any; +}; export type RunCommandParams = { namespace: C["namespace"]; @@ -18,21 +22,57 @@ export type RunCommandResult = C["output"]; export class NativeAutofillMain { private ipcServer: autofill.IpcServer | null; - private windowsMain: NativeAutofillWindowsMain; + private messageBuffer: BufferedMessage[] = []; + private listenerReady = false; constructor( private logService: LogService, private windowMain: WindowMain, ) { - this.windowsMain = new NativeAutofillWindowsMain(logService, windowMain); + } + + /** + * Safely sends a message to the renderer, buffering it if the server isn't ready yet + */ + private safeSend(channel: string, data: any) { + if (this.listenerReady && this.windowMain.win?.webContents) { + this.windowMain.win.webContents.send(channel, data); + } else { + this.messageBuffer.push({ channel, data }); + } + } + + /** + * Flushes all buffered messages to the renderer + */ + private flushMessageBuffer() { + if (!this.windowMain.win?.webContents) { + this.logService.error("Cannot flush message buffer - window not available"); + return; + } + + this.logService.info(`Flushing ${this.messageBuffer.length} buffered messages`); + + for (const { channel, data } of this.messageBuffer) { + this.windowMain.win.webContents.send(channel, data); + } + + this.messageBuffer = []; } async init() { - const enableWindowsPasskeyProvider = true; - if (enableWindowsPasskeyProvider) { - this.windowsMain.initWindows(); - this.windowsMain.setupWindowsRendererIPCHandlers(); - } + if (process.platform === "win32") { + try { + passkey_authenticator.register(); + } + catch (err) { + this.logService.error("Failed to register windows passkey plugin:", err) + return JSON.stringify({ + "type": "error", + "message": "Failed to register windows passkey plugin" + }) + } + } ipcMain.handle( "autofill.runCommand", @@ -53,7 +93,7 @@ export class NativeAutofillMain { this.ipcServer.completeError(clientId, sequenceNumber, String(error)); return; } - this.windowMain.win.webContents.send("autofill.passkeyRegistration", { + this.safeSend("autofill.passkeyRegistration", { clientId, sequenceNumber, request, @@ -66,7 +106,7 @@ export class NativeAutofillMain { this.ipcServer.completeError(clientId, sequenceNumber, String(error)); return; } - this.windowMain.win.webContents.send("autofill.passkeyAssertion", { + this.safeSend("autofill.passkeyAssertion", { clientId, sequenceNumber, request, @@ -79,7 +119,33 @@ export class NativeAutofillMain { this.ipcServer.completeError(clientId, sequenceNumber, String(error)); return; } - this.windowMain.win.webContents.send("autofill.passkeyAssertionWithoutUserInterface", { + this.safeSend("autofill.passkeyAssertionWithoutUserInterface", { + clientId, + sequenceNumber, + request, + }); + }, + // NativeStatusCallback + (error, clientId, sequenceNumber, status) => { + if (error) { + this.logService.error("autofill.IpcServer.nativeStatus", error); + this.ipcServer.completeError(clientId, sequenceNumber, String(error)); + return; + } + this.safeSend("autofill.nativeStatus", { + clientId, + sequenceNumber, + status, + }); + }, + // LockStatusQueryCallback + (error, clientId, sequenceNumber, request) => { + if (error) { + this.logService.error("autofill.IpcServer.lockStatusQuery", error); + this.ipcServer.completeError(clientId, sequenceNumber, String(error)); + return; + } + this.safeSend("autofill.lockStatusQuery", { clientId, sequenceNumber, request, @@ -87,20 +153,36 @@ export class NativeAutofillMain { }, ); + ipcMain.on("autofill.listenerReady", () => { + this.listenerReady = true; + this.logService.info( + `Listener is ready, flushing ${this.messageBuffer.length} buffered messages`, + ); + this.flushMessageBuffer(); + }); + ipcMain.on("autofill.completePasskeyRegistration", (event, data) => { - this.logService.warning("autofill.completePasskeyRegistration", data); + this.logService.debug("autofill.completePasskeyRegistration", data); const { clientId, sequenceNumber, response } = data; this.ipcServer.completeRegistration(clientId, sequenceNumber, response); }); ipcMain.on("autofill.completePasskeyAssertion", (event, data) => { - this.logService.warning("autofill.completePasskeyAssertion", data); + this.logService.debug("autofill.completePasskeyAssertion", data); const { clientId, sequenceNumber, response } = data; this.ipcServer.completeAssertion(clientId, sequenceNumber, response); }); + ipcMain.on("autofill.completeLockStatusQuery", (event, data) => { + this.logService.debug("autofill.completeLockStatusQuery", data); + const { clientId, sequenceNumber, response } = data; + this.ipcServer.completeLockStatusQuery(clientId, sequenceNumber, response); + }); + + + ipcMain.on("autofill.completeError", (event, data) => { - this.logService.warning("autofill.completeError", data); + this.logService.debug("autofill.completeError", data); const { clientId, sequenceNumber, error } = data; this.ipcServer.completeError(clientId, sequenceNumber, String(error)); }); @@ -128,4 +210,4 @@ export class NativeAutofillMain { return { type: "error", error: String(e) } as RunCommandResult; } } -} +} \ No newline at end of file diff --git a/apps/desktop/src/platform/main/autofill/native-autofill.windows.main.ts b/apps/desktop/src/platform/main/autofill/native-autofill.windows.main.ts deleted file mode 100644 index 05a88d6b0dc..00000000000 --- a/apps/desktop/src/platform/main/autofill/native-autofill.windows.main.ts +++ /dev/null @@ -1,363 +0,0 @@ -import { ipcMain } from "electron"; - -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { autofill, passkey_authenticator } from "@bitwarden/desktop-napi"; - -import { WindowMain } from "../../../main/window.main"; - -import { CommandDefinition } from "./command"; -import type { RunCommandParams, RunCommandResult } from "./native-autofill.main"; -import { NativeAutofillFido2Credential, NativeAutofillSyncParams } from "./sync.command"; - -export class NativeAutofillWindowsMain { - private pendingPasskeyRequests = new Map 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: autofill.PasskeyAssertionRequest): Promise { - this.logService.info("Handling assertion 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 request and wait for response - const response = await this.sendAndOptionallyWait( - "autofill.passkeyAssertion", - { - clientId, - sequenceNumber, - request: request, - }, - { waitForResponse: true, timeout: 60000 }, - ); - - if (response) { - // Convert the response to the format expected by the NAPI bridge - return JSON.stringify({ - type: "assertion_response", - ...response, - }); - } 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: autofill.PasskeyRegistrationRequest, - ): Promise { - this.logService.info("Handling registration 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 request and wait for response - const response = await this.sendAndOptionallyWait( - "autofill.passkeyRegistration", - { - clientId, - sequenceNumber, - request: 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", - ...response, - }); - } 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 { - 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( - "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( - channel: string, - data: any, - options?: { waitForResponse?: boolean; timeout?: number }, - ): Promise { - 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 timeout = options.timeout || 30000; // 30 second default timeout - - this.logService.info(`Sending awaitable request ${trackingKey} to ${channel}`, { data }); - - return new Promise((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, data); - }); - } - - /** - * These Handlers react to requests coming from the electron RENDERER process. - */ - setupWindowsRendererIPCHandlers() { - // This will run a command in windows and return the result. - // Only the "sync" command is supported for now. - ipcMain.handle( - "autofill.runCommand", - ( - _event: any, - params: RunCommandParams, - ): Promise> => { - return this.runCommand(params); - }, - ); - - ipcMain.on("autofill.completePasskeySync", (event, data) => { - this.logService.warning("autofill.completePasskeySync", data); - const { clientId, sequenceNumber, response } = data; - - // Handle awaitable passkey requests using clientId and sequenceNumber - if (clientId !== undefined && sequenceNumber !== undefined) { - const trackingKey = `${clientId}_${sequenceNumber}`; - this.handlePasskeyResponse(trackingKey, response); - } - }); - - ipcMain.on("autofill.completePasskeyRegistration", (event, data) => { - this.logService.warning("autofill.completePasskeyRegistration", data); - const { clientId, sequenceNumber, response } = data; - - // Handle awaitable passkey requests using clientId and sequenceNumber - if (clientId !== undefined && sequenceNumber !== undefined) { - const trackingKey = `${clientId}_${sequenceNumber}`; - this.handlePasskeyResponse(trackingKey, response); - } - }); - - ipcMain.on("autofill.completePasskeyAssertion", (event, data) => { - this.logService.warning("autofill.completePasskeyAssertion", data); - const { clientId, sequenceNumber, response } = data; - - // Handle awaitable passkey requests using clientId and sequenceNumber - if (clientId !== undefined && sequenceNumber !== undefined) { - const trackingKey = `${clientId}_${sequenceNumber}`; - this.handlePasskeyResponse(trackingKey, response); - } - }); - - ipcMain.on("autofill.completeError", (event, data) => { - this.logService.warning("autofill.completeError", data); - const { clientId, sequenceNumber, error } = data; - - // Handle awaitable passkey requests using clientId and sequenceNumber - if (clientId !== undefined && sequenceNumber !== undefined) { - const trackingKey = `${clientId}_${sequenceNumber}`; - this.handlePasskeyResponse(trackingKey, { 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( - command: RunCommandParams, - ): Promise> { - try { - this.logService.info("Windows runCommand (sync) is called with command:", command); - - if (command.namespace !== "autofill") { - this.logService.error("Invalid command namespace:", command.namespace); - return { type: "error", error: "Invalid command namespace" } as RunCommandResult; - } - - if (command.command !== "sync") { - this.logService.error("Invalid command:", command.command); - return { type: "error", error: "Invalid command" } as RunCommandResult; - } - - const syncParams = command.params as NativeAutofillSyncParams; - // Only sync FIDO2 credentials - const fido2Credentials = syncParams.credentials.filter((c) => c.type === "fido2"); - - const mappedCredentials = fido2Credentials.map((cred: NativeAutofillFido2Credential) => { - const credential: passkey_authenticator.SyncedCredential = { - credentialId: cred.credentialId, - rpId: cred.rpId, - userName: cred.userName, - userHandle: cred.userHandle, - }; - this.logService.info("Mapped credential:", credential); - return credential; - }); - - this.logService.info("Syncing passkeys to Windows:", mappedCredentials); - - passkey_authenticator.syncCredentialsToWindows(mappedCredentials); - - // TODO: Return a meaningful result - const res = { value: { added: 999 } } as RunCommandResult; - return res; - } catch (e) { - this.logService.error(`Error running autofill command '${command.command}':`, e); - - if (e instanceof Error) { - return { type: "error", error: e.stack ?? String(e) } as RunCommandResult; - } - - return { type: "error", error: String(e) } as RunCommandResult; - } - } -} diff --git a/apps/desktop/src/platform/main/autofill/user-verification.command.ts b/apps/desktop/src/platform/main/autofill/user-verification.command.ts new file mode 100644 index 00000000000..838c4e64d3f --- /dev/null +++ b/apps/desktop/src/platform/main/autofill/user-verification.command.ts @@ -0,0 +1,19 @@ +import { CommandDefinition, CommandOutput } from "./command"; + +export interface NativeAutofillUserVerificationCommand extends CommandDefinition { + name: "user-verification"; + input: NativeAutofillUserVerificationParams; + output: NativeAutofillUserVerificationResult; +} + +export type NativeAutofillUserVerificationParams = { + /** base64 string representing native window handle */ + windowHandle: string; + /** base64 string representing native transaction context */ + transactionContext: string; + displayHint: string; + username: string; +}; + + +export type NativeAutofillUserVerificationResult = CommandOutput<{}>; diff --git a/apps/desktop/src/platform/models/domain/window-state.ts b/apps/desktop/src/platform/models/domain/window-state.ts index 0efc9a1efab..ab52531bb5d 100644 --- a/apps/desktop/src/platform/models/domain/window-state.ts +++ b/apps/desktop/src/platform/models/domain/window-state.ts @@ -14,5 +14,6 @@ export class WindowState { export class ModalModeState { isModalModeActive: boolean; + showTrafficButtons?: boolean; modalPosition?: { x: number; y: number }; // Modal position is often passed from the native UI } diff --git a/apps/desktop/src/platform/popup-modal-styles.ts b/apps/desktop/src/platform/popup-modal-styles.ts index ae46ebb5c76..baf902df7c3 100644 --- a/apps/desktop/src/platform/popup-modal-styles.ts +++ b/apps/desktop/src/platform/popup-modal-styles.ts @@ -3,15 +3,19 @@ import { BrowserWindow } from "electron"; import { WindowState } from "./models/domain/window-state"; // change as needed, however limited by mainwindow minimum size -const popupWidth = 680; -const popupHeight = 500; +const popupWidth = 600; +const popupHeight = 660; type Position = { x: number; y: number }; -export function applyPopupModalStyles(window: BrowserWindow, position?: Position) { +export function applyPopupModalStyles( + window: BrowserWindow, + showTrafficButtons: boolean = true, + position?: Position, +) { window.unmaximize(); window.setSize(popupWidth, popupHeight); - window.setWindowButtonVisibility?.(false); + window.setWindowButtonVisibility?.(showTrafficButtons); window.setMenuBarVisibility?.(false); window.setResizable(false); window.setAlwaysOnTop(true); @@ -40,7 +44,7 @@ function positionWindow(window: BrowserWindow, position?: Position) { } export function applyMainWindowStyles(window: BrowserWindow, existingWindowState: WindowState) { - window.setMinimumSize(680, 500); + window.setMinimumSize(popupWidth, popupHeight); // need to guard against null/undefined values diff --git a/apps/desktop/src/platform/preload.ts b/apps/desktop/src/platform/preload.ts index 5af2fa571ec..a8dedc5e0e4 100644 --- a/apps/desktop/src/platform/preload.ts +++ b/apps/desktop/src/platform/preload.ts @@ -137,6 +137,7 @@ export default { hideWindow: () => ipcRenderer.send("window-hide"), log: (level: LogLevelType, message?: any, ...optionalParams: any[]) => ipcRenderer.invoke("ipc.log", { level, message, optionalParams }), + getNativeWindowHandle: async () => Buffer.from(await ipcRenderer.invoke("get-native-window-handle"), "base64"), openContextMenu: ( menu: { diff --git a/apps/desktop/src/platform/services/desktop-settings.service.ts b/apps/desktop/src/platform/services/desktop-settings.service.ts index c11f10646d7..d7c17433471 100644 --- a/apps/desktop/src/platform/services/desktop-settings.service.ts +++ b/apps/desktop/src/platform/services/desktop-settings.service.ts @@ -335,9 +335,14 @@ export class DesktopSettingsService { * Sets the modal mode of the application. Setting this changes the windows-size and other properties. * @param value `true` if the application is in modal mode, `false` if it is not. */ - async setModalMode(value: boolean, modalPosition?: { x: number; y: number }) { + async setModalMode( + value: boolean, + showTrafficButtons?: boolean, + modalPosition?: { x: number; y: number }, + ) { await this.modalModeState.update(() => ({ isModalModeActive: value, + showTrafficButtons, modalPosition, })); } diff --git a/apps/desktop/src/platform/services/electron-log.main.service.ts b/apps/desktop/src/platform/services/electron-log.main.service.ts index 947f4449271..2d5070527ab 100644 --- a/apps/desktop/src/platform/services/electron-log.main.service.ts +++ b/apps/desktop/src/platform/services/electron-log.main.service.ts @@ -22,7 +22,7 @@ export class ElectronLogMainService extends BaseLogService { return; } - log.transports.file.level = "info"; + log.transports.file.level = "debug"; if (this.logDir != null) { log.transports.file.resolvePathFn = () => path.join(this.logDir, "app.log"); } diff --git a/clients.code-workspace b/clients.code-workspace index f7d86d2a242..1cbae0be20d 100644 --- a/clients.code-workspace +++ b/clients.code-workspace @@ -66,6 +66,7 @@ "typescript.preferences.importModuleSpecifier": "project-relative", "javascript.preferences.importModuleSpecifier": "project-relative", "typescript.tsdk": "root/node_modules/typescript/lib", + "rust-analyzer.cargo.target": "aarch64-pc-windows-msvc", }, "extensions": { "recommendations": [ diff --git a/eslint.config.mjs b/eslint.config.mjs index 6c362a4dc43..54117b53e4e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -196,7 +196,7 @@ export default tseslint.config( { // uses negative lookahead to whitelist any class that doesn't start with "tw-" // in other words: classnames that start with tw- must be valid TailwindCSS classes - whitelist: ["(?!(tw)\\-).*"], + whitelist: ["(?!(tw)\\-).*", "tw-app-region-drag", "tw-app-region-no-drag"], }, ], "tailwindcss/enforces-negative-arbitrary-values": "error", @@ -348,6 +348,7 @@ export default tseslint.config( "file-selector", "mfaType.*", "filter.*", // Temporary until filters are migrated + "tw-app-region*", // Custom utility for native passkey modals "tw-@container", ], }, diff --git a/libs/common/spec/fake-account-service.ts b/libs/common/spec/fake-account-service.ts index ba48181faa2..389975dc2e1 100644 --- a/libs/common/spec/fake-account-service.ts +++ b/libs/common/spec/fake-account-service.ts @@ -37,6 +37,8 @@ export class FakeAccountService implements AccountService { accountActivitySubject = new ReplaySubject>(1); // eslint-disable-next-line rxjs/no-exposed-subjects -- test class accountVerifyDevicesSubject = new ReplaySubject(1); + // eslint-disable-next-line rxjs/no-exposed-subjects -- test class + showHeaderSubject = new ReplaySubject(1); private _activeUserId: UserId; get activeUserId() { return this._activeUserId; @@ -55,6 +57,7 @@ export class FakeAccountService implements AccountService { }), ); } + showHeader$ = this.showHeaderSubject.asObservable(); get nextUpAccount$(): Observable { return combineLatest([this.accounts$, this.activeAccount$, this.sortedUserIds$]).pipe( map(([accounts, activeAccount, sortedUserIds]) => { @@ -114,6 +117,10 @@ export class FakeAccountService implements AccountService { this.accountsSubject.next(updated); await this.mock.clean(userId); } + + async setShowHeader(value: boolean): Promise { + this.showHeaderSubject.next(value); + } } const loggedOutInfo: AccountInfo = { diff --git a/libs/common/src/auth/abstractions/account.service.ts b/libs/common/src/auth/abstractions/account.service.ts index a3dabeecf8a..8b0280feb01 100644 --- a/libs/common/src/auth/abstractions/account.service.ts +++ b/libs/common/src/auth/abstractions/account.service.ts @@ -47,6 +47,8 @@ export abstract class AccountService { abstract sortedUserIds$: Observable; /** Next account that is not the current active account */ abstract nextUpAccount$: Observable; + /** Observable to display the header */ + abstract showHeader$: Observable; /** * Updates the `accounts$` observable with the new account data. * @@ -100,6 +102,11 @@ export abstract class AccountService { * @param lastActivity */ abstract setAccountActivity(userId: UserId, lastActivity: Date): Promise; + /** + * Show the account switcher. + * @param value + */ + abstract setShowHeader(visible: boolean): Promise; } export abstract class InternalAccountService extends AccountService { diff --git a/libs/common/src/auth/services/account.service.spec.ts b/libs/common/src/auth/services/account.service.spec.ts index 3fc47002083..3e3c878eaac 100644 --- a/libs/common/src/auth/services/account.service.spec.ts +++ b/libs/common/src/auth/services/account.service.spec.ts @@ -429,6 +429,16 @@ describe("accountService", () => { }, ); }); + + describe("setShowHeader", () => { + it("should update _showHeader$ when setShowHeader is called", async () => { + expect(sut["_showHeader$"].value).toBe(true); + + await sut.setShowHeader(false); + + expect(sut["_showHeader$"].value).toBe(false); + }); + }); }); }); diff --git a/libs/common/src/auth/services/account.service.ts b/libs/common/src/auth/services/account.service.ts index 1be11b03461..fb4b590ce77 100644 --- a/libs/common/src/auth/services/account.service.ts +++ b/libs/common/src/auth/services/account.service.ts @@ -6,6 +6,7 @@ import { distinctUntilChanged, shareReplay, combineLatest, + BehaviorSubject, Observable, switchMap, filter, @@ -84,6 +85,7 @@ export const getOptionalUserId = map( export class AccountServiceImplementation implements InternalAccountService { private accountsState: GlobalState>; private activeAccountIdState: GlobalState; + private _showHeader$ = new BehaviorSubject(true); accounts$: Observable>; activeAccount$: Observable; @@ -91,6 +93,7 @@ export class AccountServiceImplementation implements InternalAccountService { accountVerifyNewDeviceLogin$: Observable; sortedUserIds$: Observable; nextUpAccount$: Observable; + showHeader$ = this._showHeader$.asObservable(); constructor( private messagingService: MessagingService, @@ -262,6 +265,10 @@ export class AccountServiceImplementation implements InternalAccountService { } } + async setShowHeader(visible: boolean): Promise { + this._showHeader$.next(visible); + } + private async setAccountInfo(userId: UserId, update: Partial): Promise { function newAccountInfo(oldAccountInfo: AccountInfo): AccountInfo { return { ...oldAccountInfo, ...update }; diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index bfb40aff106..07e0874f1fa 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -19,6 +19,7 @@ export enum FeatureFlag { /* Autofill */ MacOsNativeCredentialSync = "macos-native-credential-sync", + WindowsNativeCredentialSync = "windows-native-credential-sync", WindowsDesktopAutotype = "windows-desktop-autotype", /* Billing */ @@ -86,6 +87,7 @@ export const DefaultFeatureFlagValue = { /* Autofill */ [FeatureFlag.MacOsNativeCredentialSync]: FALSE, + [FeatureFlag.WindowsNativeCredentialSync]: true, [FeatureFlag.WindowsDesktopAutotype]: FALSE, /* Tools */ diff --git a/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts index c34c4b835cf..81f534a6cc3 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts @@ -19,6 +19,7 @@ export abstract class Fido2AuthenticatorService { params: Fido2AuthenticatorMakeCredentialsParams, window: ParentWindowReference, abortController?: AbortController, + transactionContext?: string, ): Promise; /** @@ -33,6 +34,7 @@ export abstract class Fido2AuthenticatorService { params: Fido2AuthenticatorGetAssertionParams, window: ParentWindowReference, abortController?: AbortController, + transactionContext?: string, ): Promise; /** @@ -138,7 +140,7 @@ export interface Fido2AuthenticatorGetAssertionParams { rpId: string; /** The hash of the serialized client data, provided by the client. */ hash: BufferSource; - allowCredentialDescriptorList: PublicKeyCredentialDescriptor[]; + allowCredentialDescriptorList?: PublicKeyCredentialDescriptor[]; /** The effective user verification requirement for assertion, a Boolean value provided by the client. */ requireUserVerification: boolean; /** The constant Boolean value true. It is included here as a pseudo-parameter to simplify applying this abstract authenticator model to implementations that may wish to make a test of user presence optional although WebAuthn does not. */ diff --git a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts index 28b199da78f..a94e053d9db 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts @@ -71,6 +71,7 @@ export abstract class Fido2UserInterfaceService { fallbackSupported: boolean, window: ParentWindowReference, abortController?: AbortController, + transactionContext?: string, ): Promise; } @@ -90,12 +91,11 @@ export abstract class Fido2UserInterfaceSession { * Ask the user to confirm the creation of a new credential. * * @param params The parameters to use when asking the user to confirm the creation of a new credential. - * @param abortController An abort controller that can be used to cancel/close the session. * @returns The ID of the cipher where the new credential should be saved. */ abstract confirmNewCredential( params: NewCredentialParams, - ): Promise<{ cipherId: string; userVerified: boolean }>; + ): Promise<{ cipherId?: string; userVerified: boolean }>; /** * Make sure that the vault is unlocked. diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts index e560a77cc2e..4f20eabc2b4 100644 --- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts +++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts @@ -61,11 +61,13 @@ export class Fido2AuthenticatorService params: Fido2AuthenticatorMakeCredentialsParams, window: ParentWindowReference, abortController?: AbortController, + transactionContext?: string, ): Promise { const userInterfaceSession = await this.userInterface.newSession( params.fallbackSupported, window, abortController, + transactionContext, ); try { @@ -128,6 +130,7 @@ export class Fido2AuthenticatorService let userVerified = false; let credentialId: string; let pubKeyDer: ArrayBuffer; + const response = await userInterfaceSession.confirmNewCredential({ credentialName: params.rpEntity.name, userName: params.userEntity.name, @@ -189,7 +192,6 @@ export class Fido2AuthenticatorService } const reencrypted = await this.cipherService.encrypt(cipher, activeUserId); await this.cipherService.updateWithServer(reencrypted); - await this.cipherService.clearCache(activeUserId); credentialId = fido2Credential.credentialId; } catch (error) { this.logService?.error( @@ -230,11 +232,13 @@ export class Fido2AuthenticatorService params: Fido2AuthenticatorGetAssertionParams, window: ParentWindowReference, abortController?: AbortController, + transactionContext?: string, ): Promise { const userInterfaceSession = await this.userInterface.newSession( params.fallbackSupported, window, abortController, + transactionContext, ); try { if ( @@ -330,7 +334,6 @@ export class Fido2AuthenticatorService ); const encrypted = await this.cipherService.encrypt(selectedCipher, activeUserId); await this.cipherService.updateWithServer(encrypted); - await this.cipherService.clearCache(activeUserId); } const authenticatorData = await generateAuthData({ @@ -452,7 +455,7 @@ export class Fido2AuthenticatorService credential.id, parseCredentialId(cipher.login.fido2Credentials[0].credentialId), ), - ), + ) ); } diff --git a/libs/common/src/platform/services/fido2/fido2-utils.spec.ts b/libs/common/src/platform/services/fido2/fido2-utils.spec.ts index 9bb4ed0a4c5..6b34f772798 100644 --- a/libs/common/src/platform/services/fido2/fido2-utils.spec.ts +++ b/libs/common/src/platform/services/fido2/fido2-utils.spec.ts @@ -1,3 +1,9 @@ +import { mock } from "jest-mock-extended"; + +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view"; + import { Fido2Utils } from "./fido2-utils"; describe("Fido2 Utils", () => { @@ -67,4 +73,62 @@ describe("Fido2 Utils", () => { expect(expectedArray).toBeNull(); }); }); + + describe("cipherHasNoOtherPasskeys(...)", () => { + const emptyPasskeyCipher = mock({ + id: "id-5", + localData: { lastUsedDate: 222 }, + name: "name-5", + type: CipherType.Login, + login: { + username: "username-5", + password: "password", + uri: "https://example.com", + fido2Credentials: [], + }, + }); + + const passkeyCipher = mock({ + id: "id-5", + localData: { lastUsedDate: 222 }, + name: "name-5", + type: CipherType.Login, + login: { + username: "username-5", + password: "password", + uri: "https://example.com", + fido2Credentials: [ + mock({ + credentialId: "credential-id", + rpName: "credential-name", + userHandle: "user-handle-1", + userName: "credential-username", + rpId: "jest-testing-website.com", + }), + mock({ + credentialId: "credential-id", + rpName: "credential-name", + userHandle: "user-handle-2", + userName: "credential-username", + rpId: "jest-testing-website.com", + }), + ], + }, + }); + + it("should return true when there is no userHandle", () => { + const userHandle = "user-handle-1"; + expect(Fido2Utils.cipherHasNoOtherPasskeys(emptyPasskeyCipher, userHandle)).toBeTruthy(); + }); + + it("should return true when userHandle matches", () => { + const userHandle = "user-handle-1"; + expect(Fido2Utils.cipherHasNoOtherPasskeys(passkeyCipher, userHandle)).toBeTruthy(); + }); + + it("should return false when userHandle doesn't match", () => { + const userHandle = "testing"; + expect(Fido2Utils.cipherHasNoOtherPasskeys(passkeyCipher, userHandle)).toBeFalsy(); + }); + }); }); diff --git a/libs/common/src/platform/services/fido2/fido2-utils.ts b/libs/common/src/platform/services/fido2/fido2-utils.ts index 99e260f4a53..8efd4734d81 100644 --- a/libs/common/src/platform/services/fido2/fido2-utils.ts +++ b/libs/common/src/platform/services/fido2/fido2-utils.ts @@ -1,3 +1,5 @@ +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + // FIXME: Update this file to be type safe and remove this and next line import type { AssertCredentialResult, @@ -111,4 +113,16 @@ export class Fido2Utils { return output; } + + /** + * This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle + * @param userHandle + */ + static cipherHasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean { + if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) { + return true; + } + + return cipher.login.fido2Credentials.some((passkey) => passkey.userHandle === userHandle); + } } diff --git a/libs/common/src/platform/services/fido2/guid-utils.spec.ts b/libs/common/src/platform/services/fido2/guid-utils.spec.ts index 098ea4bee75..c58bd2720fa 100644 --- a/libs/common/src/platform/services/fido2/guid-utils.spec.ts +++ b/libs/common/src/platform/services/fido2/guid-utils.spec.ts @@ -1,28 +1,63 @@ -import { guidToRawFormat } from "./guid-utils"; +import { guidToRawFormat, guidToStandardFormat } from "./guid-utils"; + +const workingExamples: [string, Uint8Array][] = [ + [ + "00000000-0000-0000-0000-000000000000", + new Uint8Array([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, + ]), + ], + [ + "08d70b74-e9f5-4522-a425-e5dcd40107e7", + new Uint8Array([ + 0x08, 0xd7, 0x0b, 0x74, 0xe9, 0xf5, 0x45, 0x22, 0xa4, 0x25, 0xe5, 0xdc, 0xd4, 0x01, 0x07, + 0xe7, + ]), + ], +]; describe("guid-utils", () => { describe("guidToRawFormat", () => { + it.each(workingExamples)( + "returns UUID in binary format when given a valid UUID string", + (input, expected) => { + const result = guidToRawFormat(input); + + expect(result).toEqual(expected); + }, + ); + it.each([ - [ - "00000000-0000-0000-0000-000000000000", - [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, - ], - "08d70b74-e9f5-4522-a425-e5dcd40107e7", - [ - 0x08, 0xd7, 0x0b, 0x74, 0xe9, 0xf5, 0x45, 0x22, 0xa4, 0x25, 0xe5, 0xdc, 0xd4, 0x01, 0x07, - 0xe7, - ], - ], - ])("returns UUID in binary format when given a valid UUID string", (input, expected) => { - const result = guidToRawFormat(input); - - expect(result).toEqual(new Uint8Array(expected)); + "invalid", + "", + "", + "00000000-0000-0000-0000-0000000000000000", + "00000000-0000-0000-0000-000000", + ])("throws an error when given an invalid UUID string", (input) => { + expect(() => guidToRawFormat(input)).toThrow(TypeError); }); + }); - it("throws an error when given an invalid UUID string", () => { - expect(() => guidToRawFormat("invalid")).toThrow(TypeError); + describe("guidToStandardFormat", () => { + it.each(workingExamples)( + "returns UUID in standard format when given a valid UUID array buffer", + (expected, input) => { + const result = guidToStandardFormat(input); + + expect(result).toEqual(expected); + }, + ); + + it.each([ + new Uint8Array(), + new Uint8Array([]), + new Uint8Array([ + 0x08, 0xd7, 0x0b, 0x74, 0xe9, 0xf5, 0x45, 0x22, 0xa4, 0x25, 0xe5, 0xdc, 0xd4, 0x01, 0x07, + 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, + ]), + ])("throws an error when given an invalid UUID array buffer", (input) => { + expect(() => guidToStandardFormat(input)).toThrow(TypeError); }); }); }); diff --git a/libs/common/src/platform/services/fido2/guid-utils.ts b/libs/common/src/platform/services/fido2/guid-utils.ts index 66e6cbb1d7c..e72fe84e930 100644 --- a/libs/common/src/platform/services/fido2/guid-utils.ts +++ b/libs/common/src/platform/services/fido2/guid-utils.ts @@ -53,6 +53,10 @@ export function guidToRawFormat(guid: string) { /** Convert raw 16 byte array to standard format (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX) UUID. */ export function guidToStandardFormat(bufferSource: BufferSource) { + if (bufferSource.byteLength !== 16) { + throw TypeError("BufferSource length is invalid"); + } + const arr = bufferSource instanceof ArrayBuffer ? new Uint8Array(bufferSource) diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 9aefd960b2f..b95d9023a7c 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -68,6 +68,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider; + abstract getAllDecryptedForIds(userId: UserId, ids: string[]): Promise; abstract filterCiphersForUrl( ciphers: C[], url: string, diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 1e7e5302d41..20ac1cf0eb3 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -620,6 +620,15 @@ export class CipherService implements CipherServiceAbstraction { ); } + async getAllDecryptedForIds(userId: UserId, ids: string[]): Promise { + return firstValueFrom( + this.cipherViews$(userId).pipe( + filter((ciphers) => ciphers != null), + map((ciphers) => ciphers.filter((cipher) => ids.includes(cipher.id))), + ), + ); + } + async filterCiphersForUrl( ciphers: C[], url: string, diff --git a/libs/components/src/icon-button/index.ts b/libs/components/src/icon-button/index.ts index cc52f263252..b753e53c96a 100644 --- a/libs/components/src/icon-button/index.ts +++ b/libs/components/src/icon-button/index.ts @@ -1,2 +1,2 @@ export * from "./icon-button.module"; -export { BitIconButtonComponent } from "./icon-button.component"; +export * from "./icon-button.component"; diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css index f8e02a7e668..f0e55ddd9e1 100644 --- a/libs/components/src/tw-theme.css +++ b/libs/components/src/tw-theme.css @@ -168,6 +168,18 @@ text-align: unset; } + /** + * tw-app-region-drag and tw-app-region-no-drag are used for Electron window dragging behavior + * These will replace direct -webkit-app-region usage as part of the migration to Tailwind CSS + */ + .tw-app-region-drag { + -webkit-app-region: drag; + } + + .tw-app-region-no-drag { + -webkit-app-region: no-drag; + } + /** * Bootstrap uses z-index: 1050 for modals, dialogs and drag-and-drop previews should appear above them. * When bootstrap is removed, test if these styles are still needed and that overlays display properly over other content. diff --git a/libs/key-management-ui/src/lock/components/lock.component.spec.ts b/libs/key-management-ui/src/lock/components/lock.component.spec.ts index 69f949fb843..e381f378ec6 100644 --- a/libs/key-management-ui/src/lock/components/lock.component.spec.ts +++ b/libs/key-management-ui/src/lock/components/lock.component.spec.ts @@ -2,7 +2,7 @@ import { DebugElement } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; import { By } from "@angular/platform-browser"; -import { Router } from "@angular/router"; +import { ActivatedRoute, Router } from "@angular/router"; import { mock } from "jest-mock-extended"; import { firstValueFrom, interval, map, of, takeWhile, timeout } from "rxjs"; import { ZXCVBNResult } from "zxcvbn"; @@ -92,6 +92,13 @@ describe("LockComponent", () => { const mockLockComponentService = mock(); const mockAnonLayoutWrapperDataService = mock(); const mockBroadcasterService = mock(); + const mockActivatedRoute = { + snapshot: { + paramMap: { + get: jest.fn().mockReturnValue(null), // return null for 'disable-redirect' param + }, + }, + }; const mockConfigService = mock(); beforeEach(async () => { @@ -150,6 +157,7 @@ describe("LockComponent", () => { { provide: LockComponentService, useValue: mockLockComponentService }, { provide: AnonLayoutWrapperDataService, useValue: mockAnonLayoutWrapperDataService }, { provide: BroadcasterService, useValue: mockBroadcasterService }, + { provide: ActivatedRoute, useValue: mockActivatedRoute }, { provide: ConfigService, useValue: mockConfigService }, ], }) @@ -465,6 +473,14 @@ describe("LockComponent", () => { component.clientType = clientType; mockLockComponentService.getPreviousUrl.mockReturnValue(null); + jest.spyOn(component as any, "doContinue").mockImplementation(async () => { + await mockBiometricStateService.resetUserPromptCancelled(); + mockMessagingService.send("unlocked"); + await mockSyncService.fullSync(false); + await mockUserAsymmetricKeysRegenerationService.regenerateIfNeeded(userId); + await mockRouter.navigate([navigateUrl]); + }); + await component.successfulMasterPasswordUnlock({ userKey: mockUserKey, masterPassword }); assertUnlocked(); @@ -476,6 +492,16 @@ describe("LockComponent", () => { component.shouldClosePopout = true; mockPlatformUtilsService.getDevice.mockReturnValue(DeviceType.FirefoxExtension); + jest.spyOn(component as any, "doContinue").mockImplementation(async () => { + await mockBiometricStateService.resetUserPromptCancelled(); + mockMessagingService.send("unlocked"); + await mockSyncService.fullSync(false); + await mockUserAsymmetricKeysRegenerationService.regenerateIfNeeded( + component.activeAccount!.id, + ); + mockLockComponentService.closeBrowserExtensionPopout(); + }); + await component.successfulMasterPasswordUnlock({ userKey: mockUserKey, masterPassword }); assertUnlocked(); @@ -610,6 +636,32 @@ describe("LockComponent", () => { ])( "should unlock and force set password change = %o when master password on login = %o and evaluated password against policy = %o and policy set during user verification by master password", async (forceSetPassword, masterPasswordPolicyOptions, evaluatedMasterPassword) => { + jest.spyOn(component as any, "doContinue").mockImplementation(async () => { + await mockBiometricStateService.resetUserPromptCancelled(); + mockMessagingService.send("unlocked"); + + if (masterPasswordPolicyOptions?.enforceOnLogin) { + const passwordStrengthResult = mockPasswordStrengthService.getPasswordStrength( + masterPassword, + component.activeAccount!.email, + ); + const evaluated = mockPolicyService.evaluateMasterPassword( + passwordStrengthResult.score, + masterPassword, + masterPasswordPolicyOptions, + ); + if (!evaluated) { + await mockMasterPasswordService.setForceSetPasswordReason( + ForceSetPasswordReason.WeakMasterPassword, + userId, + ); + } + } + + await mockSyncService.fullSync(false); + await mockUserAsymmetricKeysRegenerationService.regenerateIfNeeded(userId); + }); + mockUserVerificationService.verifyUserByMasterPassword.mockResolvedValue({ ...masterPasswordVerificationResponse, policyOptions: @@ -724,6 +776,14 @@ describe("LockComponent", () => { component.clientType = clientType; mockLockComponentService.getPreviousUrl.mockReturnValue(null); + jest.spyOn(component as any, "doContinue").mockImplementation(async () => { + await mockBiometricStateService.resetUserPromptCancelled(); + mockMessagingService.send("unlocked"); + await mockSyncService.fullSync(false); + await mockUserAsymmetricKeysRegenerationService.regenerateIfNeeded(userId); + await mockRouter.navigate([navigateUrl]); + }); + await component.unlockViaMasterPassword(); assertUnlocked(); @@ -735,6 +795,16 @@ describe("LockComponent", () => { component.shouldClosePopout = true; mockPlatformUtilsService.getDevice.mockReturnValue(DeviceType.FirefoxExtension); + jest.spyOn(component as any, "doContinue").mockImplementation(async () => { + await mockBiometricStateService.resetUserPromptCancelled(); + mockMessagingService.send("unlocked"); + await mockSyncService.fullSync(false); + await mockUserAsymmetricKeysRegenerationService.regenerateIfNeeded( + component.activeAccount!.id, + ); + mockLockComponentService.closeBrowserExtensionPopout(); + }); + await component.unlockViaMasterPassword(); assertUnlocked(); diff --git a/libs/key-management-ui/src/lock/components/lock.component.ts b/libs/key-management-ui/src/lock/components/lock.component.ts index 801a9d191f5..2573442d678 100644 --- a/libs/key-management-ui/src/lock/components/lock.component.ts +++ b/libs/key-management-ui/src/lock/components/lock.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from "@angular/common"; import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; -import { Router } from "@angular/router"; +import { Router, ActivatedRoute } from "@angular/router"; import { BehaviorSubject, filter, @@ -159,6 +159,7 @@ export class LockComponent implements OnInit, OnDestroy { private keyService: KeyService, private platformUtilsService: PlatformUtilsService, private router: Router, + private activatedRoute: ActivatedRoute, private dialogService: DialogService, private messagingService: MessagingService, private biometricStateService: BiometricStateService, @@ -697,7 +698,13 @@ export class LockComponent implements OnInit, OnDestroy { } // determine success route based on client type - if (this.clientType != null) { + // The disable-redirect parameter allows callers to prevent automatic navigation after unlock, + // useful when the lock component is used in contexts where custom post-unlock behavior is needed + // such as passkey modals. + if ( + this.clientType != null && + this.activatedRoute.snapshot.paramMap.get("disable-redirect") === null + ) { const successRoute = clientTypeToSuccessRouteRecord[this.clientType]; await this.router.navigate([successRoute]); } diff --git a/package-lock.json b/package-lock.json index e23afb8b859..e353a8ca0cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4940,6 +4940,33 @@ } } }, + "node_modules/@compodoc/compodoc/node_modules/@angular-devkit/schematics/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/@compodoc/compodoc/node_modules/@babel/core": { "version": "7.25.8", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.8.tgz", @@ -5122,6 +5149,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@compodoc/compodoc/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@compodoc/compodoc/node_modules/magic-string": { "version": "0.30.11", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", @@ -5132,6 +5174,36 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/@compodoc/compodoc/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/@compodoc/compodoc/node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@compodoc/live-server": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@compodoc/live-server/-/live-server-1.2.3.tgz", @@ -8254,6 +8326,21 @@ "@msgpack/msgpack": "^2.7.0" } }, + "node_modules/@microsoft/signalr/node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/@microsoft/signalr/node_modules/ws": { "version": "7.5.10", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", @@ -11743,6 +11830,21 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.0.tgz", + "integrity": "sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, "node_modules/@rollup/rollup-linux-s390x-gnu": { "version": "4.34.8", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz", @@ -40726,6 +40828,291 @@ } } }, + "node_modules/vite/node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.0.tgz", + "integrity": "sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true + }, + "node_modules/vite/node_modules/@rollup/rollup-android-arm64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.0.tgz", + "integrity": "sha512-uNSk/TgvMbskcHxXYHzqwiyBlJ/lGcv8DaUfcnNwict8ba9GTTNxfn3/FAoFZYgkaXXAdrAA+SLyKplyi349Jw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true + }, + "node_modules/vite/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.0.tgz", + "integrity": "sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/vite/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.0.tgz", + "integrity": "sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/vite/node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.0.tgz", + "integrity": "sha512-u5AZzdQJYJXByB8giQ+r4VyfZP+walV+xHWdaFx/1VxsOn6eWJhK2Vl2eElvDJFKQBo/hcYIBg/jaKS8ZmKeNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true + }, + "node_modules/vite/node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.0.tgz", + "integrity": "sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.0.tgz", + "integrity": "sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.0.tgz", + "integrity": "sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.0.tgz", + "integrity": "sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.0.tgz", + "integrity": "sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.0.tgz", + "integrity": "sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.0.tgz", + "integrity": "sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.0.tgz", + "integrity": "sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.0.tgz", + "integrity": "sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.0.tgz", + "integrity": "sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.0.tgz", + "integrity": "sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/vite/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.0.tgz", + "integrity": "sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/vite/node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.0.tgz", + "integrity": "sha512-3XJ0NQtMAXTWFW8FqZKcw3gOQwBtVWP/u8TpHP3CRPXD7Pd6s8lLdH3sHWh8vqKCyyiI8xW5ltJScQmBU9j7WA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/vite/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.0.tgz", + "integrity": "sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, "node_modules/vite/node_modules/rollup": { "version": "4.44.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.0.tgz",