mirror of
https://github.com/bitwarden/browser
synced 2026-02-19 02:44:01 +00:00
Add generic autofill provider library [PM-29786] (#18075)
* Rename macos_provider to autofill_provider * Add autofill IPC client methods needed for Windows IPC
This commit is contained in:
committed by
jaasen-livefront
parent
22c1887990
commit
f257d62c20
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -8,7 +8,7 @@
|
||||
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/autofill_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
|
||||
|
||||
1
.github/renovate.json5
vendored
1
.github/renovate.json5
vendored
@@ -187,6 +187,7 @@
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"simplelog",
|
||||
"style-loader",
|
||||
"sysinfo",
|
||||
|
||||
281
apps/desktop/desktop_native/Cargo.lock
generated
281
apps/desktop/desktop_native/Cargo.lock
generated
@@ -47,6 +47,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.18"
|
||||
@@ -324,6 +333,23 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "autofill_provider"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"desktop_core",
|
||||
"futures",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-oslog",
|
||||
"tracing-subscriber",
|
||||
"uniffi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autotype"
|
||||
version = "0.0.0"
|
||||
@@ -603,6 +629,18 @@ dependencies = [
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
@@ -799,6 +837,41 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
@@ -810,6 +883,16 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "desktop_core"
|
||||
version = "0.0.0"
|
||||
@@ -988,6 +1071,12 @@ version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5"
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.16.9"
|
||||
@@ -1410,6 +1499,12 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.3"
|
||||
@@ -1425,7 +1520,7 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1476,6 +1571,30 @@ dependencies = [
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.0.0"
|
||||
@@ -1562,6 +1681,12 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.0.3"
|
||||
@@ -1583,6 +1708,17 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown 0.12.3",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.9.0"
|
||||
@@ -1590,7 +1726,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.3",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1744,21 +1881,6 @@ version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "macos_provider"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"desktop_core",
|
||||
"futures",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-oslog",
|
||||
"tracing-subscriber",
|
||||
"uniffi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
@@ -2017,6 +2139,12 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
@@ -2315,7 +2443,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"indexmap",
|
||||
"indexmap 2.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2441,6 +2569,12 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
@@ -2622,6 +2756,26 @@ dependencies = [
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ref-cast"
|
||||
version = "1.0.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
|
||||
dependencies = [
|
||||
"ref-cast-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ref-cast-impl"
|
||||
version = "1.0.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.9"
|
||||
@@ -2766,6 +2920,30 @@ dependencies = [
|
||||
"sdd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"ref-cast",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"ref-cast",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@@ -2911,6 +3089,38 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.9.0",
|
||||
"schemars 0.9.0",
|
||||
"schemars 1.2.0",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_with_macros",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serial_test"
|
||||
version = "3.3.1"
|
||||
@@ -3231,6 +3441,37 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.1"
|
||||
@@ -3298,7 +3539,7 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"indexmap 2.9.0",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime 0.7.0",
|
||||
@@ -3328,7 +3569,7 @@ version = "0.22.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"indexmap 2.9.0",
|
||||
"toml_datetime 0.6.9",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"autofill_provider",
|
||||
"autotype",
|
||||
"bitwarden_chromium_import_helper",
|
||||
"chromium_importer",
|
||||
"core",
|
||||
"macos_provider",
|
||||
"napi",
|
||||
"process_isolation",
|
||||
"proxy",
|
||||
@@ -58,6 +58,7 @@ security-framework = "=3.5.1"
|
||||
security-framework-sys = "=2.15.0"
|
||||
serde = "=1.0.209"
|
||||
serde_json = "=1.0.127"
|
||||
serde_with = "=3.14.1"
|
||||
sha2 = "=0.10.9"
|
||||
ssh-encoding = "=0.2.0"
|
||||
ssh-key = { version = "=0.6.7", default-features = false }
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "macos_provider"
|
||||
name = "autofill_provider"
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
version = { workspace = true }
|
||||
publish = { workspace = true }
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib", "cdylib"]
|
||||
crate-type = ["lib", "staticlib", "cdylib"]
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
@@ -14,17 +14,19 @@ name = "uniffi-bindgen"
|
||||
path = "uniffi-bindgen.rs"
|
||||
|
||||
[dependencies]
|
||||
uniffi = { workspace = true, features = ["cli"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
base64 = { workspace = true }
|
||||
desktop_core = { path = "../core" }
|
||||
futures = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_with = { workspace = true, features = ["base64"] }
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
tracing-oslog = "=0.3.0"
|
||||
tracing-subscriber = { workspace = true }
|
||||
uniffi = { workspace = true, features = ["cli"] }
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
@@ -1,3 +1,7 @@
|
||||
# Autofill Provider
|
||||
|
||||
A library for native autofill providers to interact with a host Bitwarden desktop app.
|
||||
|
||||
# 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.
|
||||
@@ -2,8 +2,12 @@
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
rm -r BitwardenMacosProviderFFI.xcframework
|
||||
rm -r tmp
|
||||
if [ -d "BitwardenMacosProviderFFI.xcframework" ]; then
|
||||
rm -r "BitwardenMacosProviderFFI.xcframework"
|
||||
fi
|
||||
if [ -d "tmp" ]; then
|
||||
rm -r "tmp"
|
||||
fi
|
||||
|
||||
mkdir -p ./tmp/target/universal-darwin/release/
|
||||
|
||||
@@ -11,17 +15,17 @@ 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
|
||||
cargo build --package autofill_provider --target aarch64-apple-darwin --release
|
||||
cargo build --package autofill_provider --target x86_64-apple-darwin --release
|
||||
|
||||
# Create universal libraries
|
||||
lipo -create ../target/aarch64-apple-darwin/release/libmacos_provider.a \
|
||||
../target/x86_64-apple-darwin/release/libmacos_provider.a \
|
||||
-output ./tmp/target/universal-darwin/release/libmacos_provider.a
|
||||
lipo -create ../target/aarch64-apple-darwin/release/libautofill_provider.a \
|
||||
../target/x86_64-apple-darwin/release/libautofill_provider.a \
|
||||
-output ./tmp/target/universal-darwin/release/libautofill_provider.a
|
||||
|
||||
# Generate swift bindings
|
||||
cargo run --bin uniffi-bindgen --features uniffi/cli generate \
|
||||
../target/aarch64-apple-darwin/release/libmacos_provider.dylib \
|
||||
../target/aarch64-apple-darwin/release/libautofill_provider.dylib \
|
||||
--library \
|
||||
--language swift \
|
||||
--no-format \
|
||||
@@ -38,7 +42,7 @@ cat ./tmp/bindings/*.modulemap > ./tmp/Headers/module.modulemap
|
||||
|
||||
# Build xcframework
|
||||
xcodebuild -create-xcframework \
|
||||
-library ./tmp/target/universal-darwin/release/libmacos_provider.a \
|
||||
-library ./tmp/target/universal-darwin/release/libautofill_provider.a \
|
||||
-headers ./tmp/Headers \
|
||||
-output ./BitwardenMacosProviderFFI.xcframework
|
||||
|
||||
184
apps/desktop/desktop_native/autofill_provider/src/assertion.rs
Normal file
184
apps/desktop/desktop_native/autofill_provider/src/assertion.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
use crate::TimedCallback;
|
||||
use crate::{BitwardenError, Callback, Position, UserVerification};
|
||||
|
||||
/// Request to assert a credential.
|
||||
#[cfg_attr(target_os = "macos", derive(uniffi::Record))]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyAssertionRequest {
|
||||
/// Relying Party ID for the request.
|
||||
pub rp_id: String,
|
||||
|
||||
/// SHA-256 hash of the `clientDataJSON` for the assertion request.
|
||||
pub client_data_hash: Vec<u8>,
|
||||
|
||||
/// User verification preference.
|
||||
pub user_verification: UserVerification,
|
||||
|
||||
/// List of allowed credential IDs.
|
||||
pub allowed_credentials: Vec<Vec<u8>>,
|
||||
|
||||
/// Coordinates of the center of the WebAuthn client's window, relative to
|
||||
/// the top-left point on the screen.
|
||||
/// # Operating System Differences
|
||||
///
|
||||
/// ## macOS
|
||||
/// Note that macOS APIs gives points relative to the bottom-left point on the
|
||||
/// screen by default, so the y-coordinate will be flipped.
|
||||
///
|
||||
/// ## Windows
|
||||
/// On Windows, this must be logical pixels, not physical pixels.
|
||||
pub window_xy: Position,
|
||||
|
||||
/// Byte string representing the native OS window handle for the WebAuthn client.
|
||||
/// # Operating System Differences
|
||||
///
|
||||
/// ## macOS
|
||||
/// Unused.
|
||||
///
|
||||
/// ## Windows
|
||||
/// On Windows, this is a HWND.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub client_window_handle: Vec<u8>,
|
||||
|
||||
/// Native context required for callbacks to the OS. Format differs on the OS.
|
||||
/// # Operating System Differences
|
||||
///
|
||||
/// ## macOS
|
||||
/// Unused.
|
||||
///
|
||||
/// ## Windows
|
||||
/// On Windows, this is a base64-string representing the following data:
|
||||
/// `request transaction id (GUID, 16 bytes) || SHA-256(pluginOperationRequest)`
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub context: String,
|
||||
// TODO(PM-30510): Implement support for extensions
|
||||
// pub extension_input: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Request to assert a credential without user interaction.
|
||||
#[cfg_attr(target_os = "macos", derive(uniffi::Record))]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyAssertionWithoutUserInterfaceRequest {
|
||||
/// Relying Party ID.
|
||||
pub rp_id: String,
|
||||
|
||||
/// The allowed credential ID for the request.
|
||||
pub credential_id: Vec<u8>,
|
||||
|
||||
/// The user name for the credential that was previously given to the OS.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub user_name: String,
|
||||
|
||||
/// The user ID for the credential that was previously given to the OS.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub user_handle: Vec<u8>,
|
||||
|
||||
/// The app-specific local identifier for the credential, in our case, the
|
||||
/// cipher ID.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub record_identifier: Option<String>,
|
||||
|
||||
/// SHA-256 hash of the `clientDataJSON` for the assertion request.
|
||||
pub client_data_hash: Vec<u8>,
|
||||
|
||||
/// User verification preference.
|
||||
pub user_verification: UserVerification,
|
||||
|
||||
/// Coordinates of the center of the WebAuthn client's window, relative to
|
||||
/// the top-left point on the screen.
|
||||
/// # Operating System Differences
|
||||
///
|
||||
/// ## macOS
|
||||
/// Note that macOS APIs gives points relative to the bottom-left point on the
|
||||
/// screen by default, so the y-coordinate will be flipped.
|
||||
///
|
||||
/// ## Windows
|
||||
/// On Windows, this must be logical pixels, not physical pixels.
|
||||
pub window_xy: Position,
|
||||
|
||||
/// Byte string representing the native OS window handle for the WebAuthn client.
|
||||
/// # Operating System Differences
|
||||
///
|
||||
/// ## macOS
|
||||
/// Unused.
|
||||
///
|
||||
/// ## Windows
|
||||
/// On Windows, this is a HWND.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub client_window_handle: Vec<u8>,
|
||||
|
||||
/// Native context required for callbacks to the OS. Format differs on the OS.
|
||||
/// # Operating System Differences
|
||||
///
|
||||
/// ## macOS
|
||||
/// Unused.
|
||||
///
|
||||
/// ## Windows
|
||||
/// On Windows, this is `request transaction id () || SHA-256(pluginOperationRequest)`.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub context: String,
|
||||
}
|
||||
|
||||
/// Response for a passkey assertion request.
|
||||
#[cfg_attr(target_os = "macos", derive(uniffi::Record))]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyAssertionResponse {
|
||||
/// Relying Party ID.
|
||||
pub rp_id: String,
|
||||
|
||||
/// The user ID for the credential that was previously given to the OS.
|
||||
pub user_handle: Vec<u8>,
|
||||
|
||||
/// The signature for the WebAuthn attestation response.
|
||||
pub signature: Vec<u8>,
|
||||
|
||||
/// SHA-256 hash of the `clientDataJSON` used in the assertion.
|
||||
pub client_data_hash: Vec<u8>,
|
||||
|
||||
/// The WebAuthn authenticator data structure.
|
||||
pub authenticator_data: Vec<u8>,
|
||||
|
||||
/// The ID for the attested credential.
|
||||
pub credential_id: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Callback to process a response to passkey assertion request.
|
||||
#[cfg_attr(target_os = "macos", uniffi::export(with_foreign))]
|
||||
pub trait PreparePasskeyAssertionCallback: Send + Sync {
|
||||
/// Function to call if a successful response is returned.
|
||||
fn on_complete(&self, credential: PasskeyAssertionResponse);
|
||||
|
||||
/// Function to call if an error response is returned.
|
||||
fn on_error(&self, error: BitwardenError);
|
||||
}
|
||||
|
||||
impl Callback for Arc<dyn PreparePasskeyAssertionCallback> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
impl PreparePasskeyAssertionCallback for TimedCallback<PasskeyAssertionResponse> {
|
||||
fn on_complete(&self, credential: PasskeyAssertionResponse) {
|
||||
self.send(Ok(credential));
|
||||
}
|
||||
|
||||
fn on_error(&self, error: BitwardenError) {
|
||||
self.send(Err(error));
|
||||
}
|
||||
}
|
||||
754
apps/desktop/desktop_native/autofill_provider/src/lib.rs
Normal file
754
apps/desktop/desktop_native/autofill_provider/src/lib.rs
Normal file
@@ -0,0 +1,754 @@
|
||||
#![allow(clippy::disallowed_macros)] // uniffi macros trip up clippy's evaluation
|
||||
mod assertion;
|
||||
mod lock_status;
|
||||
mod registration;
|
||||
mod window_handle_query;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::sync::Once;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
error::Error,
|
||||
fmt::Display,
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
atomic::AtomicU32,
|
||||
mpsc::{self, Receiver, RecvTimeoutError, Sender},
|
||||
Arc, Mutex,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub use assertion::{
|
||||
PasskeyAssertionRequest, PasskeyAssertionResponse, PasskeyAssertionWithoutUserInterfaceRequest,
|
||||
PreparePasskeyAssertionCallback,
|
||||
};
|
||||
use futures::FutureExt;
|
||||
pub use lock_status::LockStatusResponse;
|
||||
pub use registration::{
|
||||
PasskeyRegistrationRequest, PasskeyRegistrationResponse, PreparePasskeyRegistrationCallback,
|
||||
};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use tracing::{error, info};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tracing_subscriber::{
|
||||
filter::{EnvFilter, LevelFilter},
|
||||
layer::SubscriberExt,
|
||||
util::SubscriberInitExt,
|
||||
};
|
||||
pub use window_handle_query::WindowHandleQueryResponse;
|
||||
|
||||
use crate::{
|
||||
lock_status::{GetLockStatusCallback, LockStatusRequest},
|
||||
window_handle_query::{GetWindowHandleQueryCallback, WindowHandleQueryRequest},
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
uniffi::setup_scaffolding!();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
/// User verification preference for WebAuthn requests.
|
||||
#[cfg_attr(target_os = "macos", derive(uniffi::Enum))]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum UserVerification {
|
||||
Preferred,
|
||||
Required,
|
||||
Discouraged,
|
||||
}
|
||||
|
||||
/// Coordinates representing a point on the screen.
|
||||
#[cfg_attr(target_os = "macos", derive(uniffi::Record))]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Position {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
#[cfg_attr(target_os = "macos", derive(uniffi::Error))]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum BitwardenError {
|
||||
Internal(String),
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
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}"),
|
||||
Self::Disconnected => {
|
||||
write!(f, "Client is disconnected from autofill IPC service")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for BitwardenError {}
|
||||
|
||||
// These methods are named differently than the actual Uniffi traits (without
|
||||
// the `on_` prefix) to avoid ambiguous trait implementations in the generated
|
||||
// code.
|
||||
trait Callback: Send + Sync {
|
||||
fn complete(&self, credential: serde_json::Value) -> Result<(), serde_json::Error>;
|
||||
fn error(&self, error: BitwardenError);
|
||||
}
|
||||
|
||||
/// Store the connection status between the credential provider extension
|
||||
/// and the desktop application's IPC server.
|
||||
#[cfg_attr(target_os = "macos", derive(uniffi::Enum))]
|
||||
#[derive(Debug)]
|
||||
pub enum ConnectionStatus {
|
||||
Connected,
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
/// A client to send and receive messages to the autofill service on the desktop
|
||||
/// client.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// In order to accommodate desktop app startup delays and non-blocking
|
||||
/// requirements for native providers, this initialization of the client is
|
||||
/// non-blocking. When calling [`AutofillProviderClient::connect()`], the
|
||||
/// connection is not established immediately, but may be established later in
|
||||
/// the background or may fail to be established.
|
||||
///
|
||||
/// Before calling [`AutofillProviderClient::connect()`], first check whether
|
||||
/// the desktop app is running with [`AutofillProviderClient::is_available`],
|
||||
/// and attempt to start it if it is not running. Then, attempt to connect, retrying as necessary.
|
||||
/// Before calling any other methods, check the connection status using
|
||||
/// [`AutofillProviderClient::get_connection_status()`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use std::{sync::Arc, time::Duration};
|
||||
///
|
||||
/// use autofill_provider::{AutofillProviderClient, ConnectionStatus, TimedCallback};
|
||||
///
|
||||
/// fn establish_connection() -> Option<AutofillProviderClient> {
|
||||
/// if !AutofillProviderClient::is_available() {
|
||||
/// // Start application
|
||||
/// }
|
||||
/// let max_attempts = 20;
|
||||
/// let delay = Duration::from_millis(300);
|
||||
///
|
||||
/// for attempt in 0..=max_attempts {
|
||||
/// let client = AutofillProviderClient::connect();
|
||||
/// if attempt != 0 {
|
||||
/// // Use whatever sleep method is appropriate
|
||||
/// std::thread::sleep(delay + Duration::from_millis(100 * attempt));
|
||||
/// }
|
||||
/// if let ConnectionStatus::Connected = client.get_connection_status() {
|
||||
/// return Some(client);
|
||||
/// }
|
||||
/// };
|
||||
/// None
|
||||
/// }
|
||||
///
|
||||
/// if let Some(client) = establish_connection() {
|
||||
/// // use client here
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg_attr(target_os = "macos", derive(uniffi::Object))]
|
||||
pub struct AutofillProviderClient {
|
||||
to_server_send: tokio::sync::mpsc::Sender<String>,
|
||||
|
||||
// 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<Mutex<HashMap<u32, (Box<dyn Callback>, Instant)>>>,
|
||||
|
||||
// Flag to track connection status - atomic for thread safety without locks
|
||||
connection_status: Arc<std::sync::atomic::AtomicBool>,
|
||||
}
|
||||
|
||||
/// Store native desktop status information to use for IPC communication
|
||||
/// between the application and the credential provider.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
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;
|
||||
|
||||
#[cfg(not(test))]
|
||||
static IPC_PATH: &str = "af";
|
||||
#[cfg(test)]
|
||||
static IPC_PATH: &str = "af-test";
|
||||
|
||||
// These methods are not currently needed in macOS and/or cannot be exported via FFI
|
||||
impl AutofillProviderClient {
|
||||
/// Whether the client is immediately available for connection.
|
||||
pub fn is_available() -> bool {
|
||||
desktop_core::ipc::path(IPC_PATH).exists()
|
||||
}
|
||||
|
||||
/// Request the desktop client's lock status.
|
||||
pub fn get_lock_status(&self, callback: Arc<dyn GetLockStatusCallback>) {
|
||||
self.send_message(LockStatusRequest {}, Some(Box::new(callback)));
|
||||
}
|
||||
|
||||
/// Requests details about the desktop client's native window.
|
||||
pub fn get_window_handle(&self, callback: Arc<dyn GetWindowHandleQueryCallback>) {
|
||||
self.send_message(
|
||||
WindowHandleQueryRequest::default(),
|
||||
Some(Box::new(callback)),
|
||||
);
|
||||
}
|
||||
|
||||
fn connect_to_path(path: PathBuf) -> Self {
|
||||
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 = AutofillProviderClient {
|
||||
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 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.clone(), from_server_send, to_server_recv)
|
||||
.map(move |r| {
|
||||
if let Err(err) = r {
|
||||
tracing::error!(
|
||||
?path,
|
||||
"Failed to connect to autofill IPC server: {err}"
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
rt.block_on(async move {
|
||||
while let Some(message) = from_server_recv.recv().await {
|
||||
match serde_json::from_str::<SerializedMessage>(&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().expect("not poisoned").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, %message, "Error deserializing message");
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
client
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_os = "macos", uniffi::export)]
|
||||
impl AutofillProviderClient {
|
||||
/// Asynchronously initiates a connection to the autofill service on the desktop client.
|
||||
///
|
||||
/// See documentation at the top-level of [this struct][AutofillProviderClient] for usage
|
||||
/// information.
|
||||
#[cfg_attr(target_os = "macos", uniffi::constructor)]
|
||||
pub fn connect() -> Self {
|
||||
tracing::trace!("Autofill provider attempting to connect to Electron IPC...");
|
||||
let path = desktop_core::ipc::path(IPC_PATH);
|
||||
Self::connect_to_path(path)
|
||||
}
|
||||
|
||||
/// Send a one-way key-value message to the desktop client.
|
||||
pub fn send_native_status(&self, key: String, value: String) {
|
||||
let status = NativeStatus { key, value };
|
||||
self.send_message(status, None);
|
||||
}
|
||||
|
||||
/// Send a request to create a new passkey to the desktop client.
|
||||
pub fn prepare_passkey_registration(
|
||||
&self,
|
||||
request: PasskeyRegistrationRequest,
|
||||
callback: Arc<dyn PreparePasskeyRegistrationCallback>,
|
||||
) {
|
||||
self.send_message(request, Some(Box::new(callback)));
|
||||
}
|
||||
|
||||
/// Send a request to assert a passkey to the desktop client.
|
||||
pub fn prepare_passkey_assertion(
|
||||
&self,
|
||||
request: PasskeyAssertionRequest,
|
||||
callback: Arc<dyn PreparePasskeyAssertionCallback>,
|
||||
) {
|
||||
self.send_message(request, Some(Box::new(callback)));
|
||||
}
|
||||
|
||||
/// Send a request to assert a passkey, without prompting the user, to the desktop client.
|
||||
pub fn prepare_passkey_assertion_without_user_interface(
|
||||
&self,
|
||||
request: PasskeyAssertionWithoutUserInterfaceRequest,
|
||||
callback: Arc<dyn PreparePasskeyAssertionCallback>,
|
||||
) {
|
||||
self.send_message(request, Some(Box::new(callback)));
|
||||
}
|
||||
|
||||
/// Return the status this client's connection to the desktop client.
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[uniffi::export]
|
||||
pub fn initialize_logging() {
|
||||
INIT.call_once(|| {
|
||||
let filter = EnvFilter::builder()
|
||||
// Everything logs at `INFO`
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy();
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.with(tracing_oslog::OsLogger::new(
|
||||
"com.bitwarden.desktop.autofill-extension",
|
||||
"default",
|
||||
))
|
||||
.init();
|
||||
});
|
||||
}
|
||||
|
||||
#[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<serde_json::Value, BitwardenError>,
|
||||
},
|
||||
}
|
||||
|
||||
impl AutofillProviderClient {
|
||||
fn add_callback(&self, callback: Box<dyn Callback>) -> 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
|
||||
}
|
||||
|
||||
fn send_message(
|
||||
&self,
|
||||
message: impl Serialize + DeserializeOwned,
|
||||
callback: Option<Box<dyn Callback>>,
|
||||
) {
|
||||
if let ConnectionStatus::Disconnected = self.get_connection_status() {
|
||||
if let Some(callback) = callback {
|
||||
callback.error(BitwardenError::Disconnected);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let sequence_number = if let Some(callback) = callback {
|
||||
self.add_callback(callback)
|
||||
} else {
|
||||
NO_CALLBACK_INDICATOR
|
||||
};
|
||||
|
||||
if let Err(e) = send_message_helper(sequence_number, message, &self.to_server_send) {
|
||||
// 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}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapped in Result<> to allow using ? for clarity.
|
||||
fn send_message_helper(
|
||||
sequence_number: u32,
|
||||
message: impl Serialize + DeserializeOwned,
|
||||
tx: &tokio::sync::mpsc::Sender<String>,
|
||||
) -> Result<(), BitwardenError> {
|
||||
let value = serde_json::to_value(message).map_err(|err| {
|
||||
BitwardenError::Internal(format!("Could not represent message as JSON: {err}"))
|
||||
})?;
|
||||
let message = SerializedMessage::Message {
|
||||
sequence_number,
|
||||
value: Ok(value),
|
||||
};
|
||||
let json = serde_json::to_string(&message).map_err(|err| {
|
||||
BitwardenError::Internal(format!("Could not serialize message as JSON: {err}"))
|
||||
})?;
|
||||
// The OS calls us serially, and we only need 1-3 concurrent requests
|
||||
// (passkey request, cancellation, maybe user verification).
|
||||
// So it's safe to send on this thread since there should always be enough
|
||||
// room in the receiver buffer to send.
|
||||
tx.blocking_send(json)
|
||||
.map_err(|_| BitwardenError::Disconnected)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Types of errors for callbacks.
|
||||
#[derive(Debug)]
|
||||
pub enum CallbackError {
|
||||
Timeout,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl Display for CallbackError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Timeout => f.write_str("Callback timed out"),
|
||||
Self::Cancelled => f.write_str("Callback cancelled"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::error::Error for CallbackError {}
|
||||
|
||||
type CallbackResponse<T> = Result<T, BitwardenError>;
|
||||
|
||||
/// An implementation of a callback handler that can take a deadline.
|
||||
pub struct TimedCallback<T> {
|
||||
tx: Arc<Mutex<Option<Sender<CallbackResponse<T>>>>>,
|
||||
rx: Arc<Mutex<Receiver<CallbackResponse<T>>>>,
|
||||
}
|
||||
|
||||
impl<T: Send + 'static> Default for TimedCallback<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + 'static> TimedCallback<T> {
|
||||
/// Instantiates a new callback handler.
|
||||
pub fn new() -> Self {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
Self {
|
||||
tx: Arc::new(Mutex::new(Some(tx))),
|
||||
rx: Arc::new(Mutex::new(rx)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Block the current thread until either a response is received, or the
|
||||
/// specified timeout has passed.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use std::{sync::Arc, time::Duration};
|
||||
///
|
||||
/// use autofill_provider::{AutofillProviderClient, TimedCallback};
|
||||
///
|
||||
/// let client = AutofillProviderClient::connect();
|
||||
/// let callback = Arc::new(TimedCallback::new());
|
||||
/// client.get_lock_status(callback.clone());
|
||||
/// match callback.wait_for_response(Duration::from_secs(3), None) {
|
||||
/// Ok(Ok(response)) => Ok(response),
|
||||
/// Ok(Err(err)) => Err(format!("GetLockStatus() call failed: {err}")),
|
||||
/// Err(_) => Err(format!("GetLockStatus() call timed out")),
|
||||
/// }.unwrap();
|
||||
/// ```
|
||||
pub fn wait_for_response(
|
||||
&self,
|
||||
timeout: Duration,
|
||||
cancellation_token: Option<Receiver<()>>,
|
||||
) -> Result<Result<T, BitwardenError>, 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()
|
||||
.expect("not poisoned")
|
||||
.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()
|
||||
.expect("not poisoned")
|
||||
.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::warn!("Request timed out, dropping.");
|
||||
err
|
||||
}
|
||||
Err(RecvTimeoutError::Timeout) => Err(CallbackError::Timeout),
|
||||
Err(_) => Err(CallbackError::Cancelled),
|
||||
}
|
||||
}
|
||||
|
||||
fn send(&self, response: Result<T, BitwardenError>) {
|
||||
match self.tx.lock().expect("not poisoned").take() {
|
||||
Some(tx) => {
|
||||
if tx.send(response).is_err() {
|
||||
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<PasskeyRegistrationResponse> {
|
||||
fn on_complete(&self, credential: PasskeyRegistrationResponse) {
|
||||
self.send(Ok(credential));
|
||||
}
|
||||
|
||||
fn on_error(&self, error: BitwardenError) {
|
||||
self.send(Err(error));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
//! For debugging test failures, it may be useful to enable tracing to see
|
||||
//! the request flow more easily. You can do that by adding the following
|
||||
//! line to the beginning of the `#[test]` function you're working on:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! tracing_subscriber::fmt::init();
|
||||
//! ```
|
||||
//!
|
||||
//! After that, you can set `RUST_LOG=debug` and run `cargo test` to see the traces.
|
||||
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{atomic::AtomicU32, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use desktop_core::ipc::server::MessageType;
|
||||
use serde_json::{json, Value};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::Level;
|
||||
|
||||
use crate::{
|
||||
AutofillProviderClient, BitwardenError, ConnectionStatus, LockStatusRequest,
|
||||
SerializedMessage, TimedCallback, IPC_PATH,
|
||||
};
|
||||
|
||||
/// Generates a path for a server and client to connect with.
|
||||
///
|
||||
/// [`AutofillProviderClient`] is currently hardcoded to use sockets from the filesystem.
|
||||
/// In order for paths not to conflict between tests, we use a counter and add it to the path
|
||||
/// name.
|
||||
fn get_server_path() -> PathBuf {
|
||||
static SERVER_COUNTER: AtomicU32 = AtomicU32::new(0);
|
||||
let counter = SERVER_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let name = format!("{}-{}", IPC_PATH, counter);
|
||||
desktop_core::ipc::path(&name)
|
||||
}
|
||||
|
||||
/// Sets up an in-memory server based on the passed handler and returns a client to the server.
|
||||
fn get_client<
|
||||
F: Fn(Result<Value, BitwardenError>) -> Result<Value, BitwardenError> + Send + 'static,
|
||||
>(
|
||||
handler: F,
|
||||
) -> AutofillProviderClient {
|
||||
let (signal_tx, signal_rx) = std::sync::mpsc::channel();
|
||||
let path = get_server_path();
|
||||
let server_path = path.clone();
|
||||
|
||||
// Start server thread
|
||||
std::thread::spawn(move || {
|
||||
let _span = tracing::span!(Level::DEBUG, "server").entered();
|
||||
tracing::info!("Starting server thread");
|
||||
let (tx, mut rx) = mpsc::channel(8);
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.build()
|
||||
.unwrap();
|
||||
rt.block_on(async move {
|
||||
tracing::debug!(?server_path, "Starting server");
|
||||
let server = desktop_core::ipc::server::Server::start(&server_path, tx).unwrap();
|
||||
|
||||
// Signal to main thread that the server is ready to process messages.
|
||||
tracing::debug!("Server started");
|
||||
signal_tx.send(()).unwrap();
|
||||
|
||||
// Handle incoming messages
|
||||
tracing::debug!("Waiting for messages");
|
||||
while let Some(data) = rx.recv().await {
|
||||
tracing::debug!("Received {data:?}");
|
||||
match data.kind {
|
||||
MessageType::Connected => {}
|
||||
MessageType::Disconnected => {}
|
||||
MessageType::Message => {
|
||||
// Deserialize and handle messages using the given handler function.
|
||||
let msg: SerializedMessage =
|
||||
serde_json::from_str(&data.message.unwrap()).unwrap();
|
||||
|
||||
if let SerializedMessage::Message {
|
||||
sequence_number,
|
||||
value,
|
||||
} = msg
|
||||
{
|
||||
let response = serde_json::to_string(&SerializedMessage::Message {
|
||||
sequence_number,
|
||||
value: handler(value),
|
||||
})
|
||||
.unwrap();
|
||||
server.send(response).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Wait for server to startup and client to connect to server before returning client to
|
||||
// test method.
|
||||
let _span = tracing::span!(Level::DEBUG, "client");
|
||||
tracing::debug!("Waiting for server...");
|
||||
signal_rx.recv_timeout(Duration::from_millis(1000)).unwrap();
|
||||
|
||||
// This starts a background task to connect to the server.
|
||||
tracing::debug!("Starting client...");
|
||||
let client = AutofillProviderClient::connect_to_path(path.to_path_buf());
|
||||
|
||||
// The client connects to the server asynchronously in a background
|
||||
// thread, so wait for client to report itself as Connected so that test
|
||||
// methods don't have to do this everytime.
|
||||
// Note, this has the potential to be flaky on a very busy server, but that's unavoidable
|
||||
// with the current API.
|
||||
tracing::debug!("Client connecting...");
|
||||
for _ in 0..20 {
|
||||
if let ConnectionStatus::Connected = client.get_connection_status() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
|
||||
assert!(matches!(
|
||||
client.get_connection_status(),
|
||||
ConnectionStatus::Connected
|
||||
));
|
||||
|
||||
client
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_throws_error_on_method_call_when_disconnected() {
|
||||
// There is no server running at this path, so this client should always be disconnected.
|
||||
let client = AutofillProviderClient::connect_to_path(get_server_path());
|
||||
|
||||
// use an arbitrary request to test whether the client is disconnected.
|
||||
let callback = Arc::new(TimedCallback::new());
|
||||
client.get_lock_status(callback.clone());
|
||||
let response = callback
|
||||
.wait_for_response(Duration::from_millis(10), None)
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(response, Err(BitwardenError::Disconnected)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_parses_get_lock_status_response_when_valid_json_is_returned() {
|
||||
// The server should expect a lock status request and return a valid response.
|
||||
let handler = |value: Result<Value, BitwardenError>| {
|
||||
let value = value.unwrap();
|
||||
if let Ok(LockStatusRequest {}) = serde_json::from_value(value.clone()) {
|
||||
Ok(json!({"isUnlocked": true}))
|
||||
} else {
|
||||
Err(BitwardenError::Internal(format!(
|
||||
"Expected LockStatusRequest, received: {value:?}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
// send a lock status request
|
||||
let client = get_client(handler);
|
||||
let callback = Arc::new(TimedCallback::new());
|
||||
client.get_lock_status(callback.clone());
|
||||
let response = callback
|
||||
.wait_for_response(Duration::from_millis(3000), None)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(response.is_unlocked);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{BitwardenError, Callback, TimedCallback};
|
||||
|
||||
/// Request to retrieve the lock status of the desktop client.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(super) struct LockStatusRequest {}
|
||||
|
||||
/// Response for the lock status of the desktop client.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct LockStatusResponse {
|
||||
/// Whether the desktop client is unlocked.
|
||||
#[serde(rename = "isUnlocked")]
|
||||
pub is_unlocked: bool,
|
||||
}
|
||||
|
||||
impl Callback for Arc<dyn GetLockStatusCallback> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// Callback to process a response to a lock status request.
|
||||
pub trait GetLockStatusCallback: Send + Sync {
|
||||
/// Function to call if a successful response is returned.
|
||||
fn on_complete(&self, response: LockStatusResponse);
|
||||
|
||||
/// Function to call if an error response is returned.
|
||||
fn on_error(&self, error: BitwardenError);
|
||||
}
|
||||
|
||||
impl GetLockStatusCallback for TimedCallback<LockStatusResponse> {
|
||||
fn on_complete(&self, response: LockStatusResponse) {
|
||||
self.send(Ok(response));
|
||||
}
|
||||
|
||||
fn on_error(&self, error: BitwardenError) {
|
||||
self.send(Err(error));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{BitwardenError, Callback, Position, UserVerification};
|
||||
|
||||
/// Request to create a credential.
|
||||
#[cfg_attr(target_os = "macos", derive(uniffi::Record))]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyRegistrationRequest {
|
||||
/// Relying Party ID for the request.
|
||||
pub rp_id: String,
|
||||
|
||||
/// The user name for the credential that was previously given to the OS.
|
||||
pub user_name: String,
|
||||
|
||||
/// The user ID for the credential that was previously given to the OS.
|
||||
pub user_handle: Vec<u8>,
|
||||
|
||||
/// SHA-256 hash of the `clientDataJSON` for the registration request.
|
||||
pub client_data_hash: Vec<u8>,
|
||||
|
||||
/// User verification preference.
|
||||
pub user_verification: UserVerification,
|
||||
|
||||
/// Supported key algorithms in COSE format.
|
||||
pub supported_algorithms: Vec<i32>,
|
||||
|
||||
/// Coordinates of the center of the WebAuthn client's window, relative to
|
||||
/// the top-left point on the screen.
|
||||
/// # Operating System Differences
|
||||
///
|
||||
/// ## macOS
|
||||
/// Note that macOS APIs gives points relative to the bottom-left point on the
|
||||
/// screen by default, so the y-coordinate will be flipped.
|
||||
///
|
||||
/// ## Windows
|
||||
/// On Windows, this must be logical pixels, not physical pixels.
|
||||
pub window_xy: Position,
|
||||
|
||||
/// List of excluded credential IDs.
|
||||
pub excluded_credentials: Vec<Vec<u8>>,
|
||||
|
||||
/// Byte string representing the native OS window handle for the WebAuthn client.
|
||||
/// # Operating System Differences
|
||||
///
|
||||
/// ## macOS
|
||||
/// Unused.
|
||||
///
|
||||
/// ## Windows
|
||||
/// On Windows, this is a HWND.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub client_window_handle: Vec<u8>,
|
||||
|
||||
/// Native context required for callbacks to the OS. Format differs by OS.
|
||||
/// # Operating System Differences
|
||||
///
|
||||
/// ## macOS
|
||||
/// Unused.
|
||||
///
|
||||
/// ## Windows
|
||||
/// On Windows, this is a base64-string representing the following data:
|
||||
/// `request transaction id (GUID, 16 bytes) || SHA-256(pluginOperationRequest)`
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub context: String,
|
||||
}
|
||||
|
||||
/// Response for a passkey registration request.
|
||||
#[cfg_attr(target_os = "macos", derive(uniffi::Record))]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyRegistrationResponse {
|
||||
/// Relying Party ID.
|
||||
pub rp_id: String,
|
||||
|
||||
/// SHA-256 hash of the `clientDataJSON` used in the registration.
|
||||
pub client_data_hash: Vec<u8>,
|
||||
|
||||
/// The ID for the created credential.
|
||||
pub credential_id: Vec<u8>,
|
||||
|
||||
/// WebAuthn attestation object.
|
||||
pub attestation_object: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Callback to process a response to passkey registration request.
|
||||
#[cfg_attr(target_os = "macos", uniffi::export(with_foreign))]
|
||||
pub trait PreparePasskeyRegistrationCallback: Send + Sync {
|
||||
/// Function to call if a successful response is returned.
|
||||
fn on_complete(&self, credential: PasskeyRegistrationResponse);
|
||||
|
||||
/// Function to call if an error response is returned.
|
||||
fn on_error(&self, error: BitwardenError);
|
||||
}
|
||||
|
||||
impl Callback for Arc<dyn PreparePasskeyRegistrationCallback> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::{
|
||||
base64::{Base64, Standard},
|
||||
formats::Padded,
|
||||
serde_as,
|
||||
};
|
||||
|
||||
use crate::{BitwardenError, Callback, TimedCallback};
|
||||
|
||||
/// Request to get the window handle of the desktop client.
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(super) struct WindowHandleQueryRequest {
|
||||
/// Marker field for parsing; data is never read.
|
||||
///
|
||||
/// TODO: this is used to disambiguate parsing the type in desktop_napi.
|
||||
/// This will be cleaned up in PM-23485.
|
||||
window_handle: String,
|
||||
}
|
||||
|
||||
/// Response to window handle request.
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WindowHandleQueryResponse {
|
||||
/// Whether the desktop client is currently visible.
|
||||
pub is_visible: bool,
|
||||
|
||||
/// Whether the desktop client is currently focused.
|
||||
pub is_focused: bool,
|
||||
|
||||
/// Byte string representing the native OS window handle for the desktop client.
|
||||
/// # Operating System Differences
|
||||
///
|
||||
/// ## macOS
|
||||
/// Unused.
|
||||
///
|
||||
/// ## Windows
|
||||
/// On Windows, this is a HWND.
|
||||
#[serde_as(as = "Base64<Standard, Padded>")]
|
||||
pub handle: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Callback for Arc<dyn GetWindowHandleQueryCallback> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// Callback to process a response to a window handle query request.
|
||||
pub trait GetWindowHandleQueryCallback: Send + Sync {
|
||||
/// Function to call if a successful response is returned.
|
||||
fn on_complete(&self, response: WindowHandleQueryResponse);
|
||||
|
||||
/// Function to call if an error response is returned.
|
||||
fn on_error(&self, error: BitwardenError);
|
||||
}
|
||||
|
||||
impl GetWindowHandleQueryCallback for TimedCallback<WindowHandleQueryResponse> {
|
||||
fn on_complete(&self, response: WindowHandleQueryResponse) {
|
||||
self.send(Ok(response));
|
||||
}
|
||||
|
||||
fn on_error(&self, error: BitwardenError) {
|
||||
self.send(Err(error));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
fn main() {
|
||||
uniffi::uniffi_bindgen_main()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn main() {
|
||||
unimplemented!("uniffi-bindgen is not enabled on this target.");
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{BitwardenError, Callback, Position, UserVerification};
|
||||
|
||||
#[derive(uniffi::Record, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyAssertionRequest {
|
||||
rp_id: String,
|
||||
client_data_hash: Vec<u8>,
|
||||
user_verification: UserVerification,
|
||||
allowed_credentials: Vec<Vec<u8>>,
|
||||
window_xy: Position,
|
||||
//extension_input: Vec<u8>, TODO: Implement support for extensions
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyAssertionWithoutUserInterfaceRequest {
|
||||
rp_id: String,
|
||||
credential_id: Vec<u8>,
|
||||
user_name: String,
|
||||
user_handle: Vec<u8>,
|
||||
record_identifier: Option<String>,
|
||||
client_data_hash: Vec<u8>,
|
||||
user_verification: UserVerification,
|
||||
window_xy: Position,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyAssertionResponse {
|
||||
rp_id: String,
|
||||
user_handle: Vec<u8>,
|
||||
signature: Vec<u8>,
|
||||
client_data_hash: Vec<u8>,
|
||||
authenticator_data: Vec<u8>,
|
||||
credential_id: Vec<u8>,
|
||||
}
|
||||
|
||||
#[uniffi::export(with_foreign)]
|
||||
pub trait PreparePasskeyAssertionCallback: Send + Sync {
|
||||
fn on_complete(&self, credential: PasskeyAssertionResponse);
|
||||
fn on_error(&self, error: BitwardenError);
|
||||
}
|
||||
|
||||
impl Callback for Arc<dyn PreparePasskeyAssertionCallback> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
#![cfg(target_os = "macos")]
|
||||
#![allow(clippy::disallowed_macros)] // uniffi macros trip up clippy's evaluation
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{atomic::AtomicU32, Arc, Mutex, Once},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use futures::FutureExt;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use tracing::{error, info};
|
||||
use tracing_subscriber::{
|
||||
filter::{EnvFilter, LevelFilter},
|
||||
layer::SubscriberExt,
|
||||
util::SubscriberInitExt,
|
||||
};
|
||||
|
||||
uniffi::setup_scaffolding!();
|
||||
|
||||
mod assertion;
|
||||
mod registration;
|
||||
|
||||
use assertion::{
|
||||
PasskeyAssertionRequest, PasskeyAssertionWithoutUserInterfaceRequest,
|
||||
PreparePasskeyAssertionCallback,
|
||||
};
|
||||
use registration::{PasskeyRegistrationRequest, PreparePasskeyRegistrationCallback};
|
||||
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
#[derive(uniffi::Enum, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum UserVerification {
|
||||
Preferred,
|
||||
Required,
|
||||
Discouraged,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Position {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, uniffi::Error, Serialize, Deserialize)]
|
||||
pub enum BitwardenError {
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
// 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(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<String>,
|
||||
|
||||
// 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<Mutex<HashMap<u32, (Box<dyn Callback>, Instant)>>>,
|
||||
|
||||
// Flag to track connection status - atomic for thread safety without locks
|
||||
connection_status: Arc<std::sync::atomic::AtomicBool>,
|
||||
}
|
||||
|
||||
#[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.
|
||||
#[allow(clippy::unwrap_used)]
|
||||
#[uniffi::constructor]
|
||||
pub fn connect() -> Self {
|
||||
INIT.call_once(|| {
|
||||
let filter = EnvFilter::builder()
|
||||
// Everything logs at `INFO`
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy();
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.with(tracing_oslog::OsLogger::new(
|
||||
"com.bitwarden.desktop.autofill-extension",
|
||||
"default",
|
||||
))
|
||||
.init();
|
||||
});
|
||||
|
||||
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 = MacOSProviderClient {
|
||||
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::<SerializedMessage>(&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<dyn PreparePasskeyRegistrationCallback>,
|
||||
) {
|
||||
self.send_message(request, Some(Box::new(callback)));
|
||||
}
|
||||
|
||||
pub fn prepare_passkey_assertion(
|
||||
&self,
|
||||
request: PasskeyAssertionRequest,
|
||||
callback: Arc<dyn PreparePasskeyAssertionCallback>,
|
||||
) {
|
||||
self.send_message(request, Some(Box::new(callback)));
|
||||
}
|
||||
|
||||
pub fn prepare_passkey_assertion_without_user_interface(
|
||||
&self,
|
||||
request: PasskeyAssertionWithoutUserInterfaceRequest,
|
||||
callback: Arc<dyn PreparePasskeyAssertionCallback>,
|
||||
) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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<serde_json::Value, BitwardenError>,
|
||||
},
|
||||
}
|
||||
|
||||
impl MacOSProviderClient {
|
||||
#[allow(clippy::unwrap_used)]
|
||||
fn add_callback(&self, callback: Box<dyn Callback>) -> 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 + DeserializeOwned,
|
||||
callback: Option<Box<dyn 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,
|
||||
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}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{BitwardenError, Callback, Position, UserVerification};
|
||||
|
||||
#[derive(uniffi::Record, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyRegistrationRequest {
|
||||
rp_id: String,
|
||||
user_name: String,
|
||||
user_handle: Vec<u8>,
|
||||
client_data_hash: Vec<u8>,
|
||||
user_verification: UserVerification,
|
||||
supported_algorithms: Vec<i32>,
|
||||
window_xy: Position,
|
||||
excluded_credentials: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyRegistrationResponse {
|
||||
rp_id: String,
|
||||
client_data_hash: Vec<u8>,
|
||||
credential_id: Vec<u8>,
|
||||
attestation_object: Vec<u8>,
|
||||
}
|
||||
|
||||
#[uniffi::export(with_foreign)]
|
||||
pub trait PreparePasskeyRegistrationCallback: Send + Sync {
|
||||
fn on_complete(&self, credential: PasskeyRegistrationResponse);
|
||||
fn on_error(&self, error: BitwardenError);
|
||||
}
|
||||
|
||||
impl Callback for Arc<dyn PreparePasskeyRegistrationCallback> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
uniffi::uniffi_bindgen_main()
|
||||
}
|
||||
@@ -15,7 +15,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
@IBOutlet weak var logoImageView: NSImageView!
|
||||
|
||||
// The IPC client to communicate with the Bitwarden desktop app
|
||||
private var client: MacOsProviderClient?
|
||||
private var client: AutofillProviderClient?
|
||||
|
||||
// Timer for checking connection status
|
||||
private var connectionMonitorTimer: Timer?
|
||||
@@ -25,11 +25,12 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
// 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 {
|
||||
private func getClient() async -> AutofillProviderClient {
|
||||
if let client = self.client {
|
||||
return client
|
||||
}
|
||||
|
||||
initializeLogging()
|
||||
let logger = Logger(subsystem: "com.bitwarden.desktop.autofill-extension", category: "credential-provider")
|
||||
|
||||
// Check if the Electron app is running
|
||||
@@ -61,13 +62,13 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
// Retry connecting to the Bitwarden IPC with an increasing delay
|
||||
let maxRetries = 20
|
||||
let delayMs = 500
|
||||
var newClient: MacOsProviderClient?
|
||||
var newClient: AutofillProviderClient?
|
||||
|
||||
for attempt in 1...maxRetries {
|
||||
logger.log("[autofill-extension] Connection attempt \(attempt)")
|
||||
|
||||
// Create a new client instance for each retry
|
||||
newClient = MacOsProviderClient.connect()
|
||||
newClient = AutofillProviderClient.connect()
|
||||
try? await Task.sleep(nanoseconds: UInt64(100 * attempt + (delayMs * 1_000_000))) // Convert ms to nanoseconds
|
||||
let connectionStatus = newClient!.getConnectionStatus()
|
||||
|
||||
@@ -129,7 +130,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
|
||||
// If we just disconnected, try to cancel the request
|
||||
if currentStatus == .disconnected {
|
||||
self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Bitwarden desktop app disconnected"))
|
||||
self.extensionContext.cancelRequest(withError: BitwardenError.Disconnected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
3368DB382C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BitwardenMacosProviderFFI.xcframework; path = ../desktop_native/macos_provider/BitwardenMacosProviderFFI.xcframework; sourceTree = "<group>"; };
|
||||
3368DB382C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BitwardenMacosProviderFFI.xcframework; path = ../desktop_native/autofill_provider/BitwardenMacosProviderFFI.xcframework; sourceTree = "<group>"; };
|
||||
3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitwardenMacosProvider.swift; sourceTree = "<group>"; };
|
||||
968ED08A2C52A47200FFFEE6 /* ReleaseAppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ReleaseAppStore.xcconfig; sourceTree = "<group>"; };
|
||||
9AE299082DF9D82E00AAE454 /* bitwarden-icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "bitwarden-icon.png"; sourceTree = "<group>"; };
|
||||
|
||||
@@ -18,16 +18,16 @@
|
||||
"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-macos": "cd desktop_native && ./autofill_provider/build.sh && node build.js cross-platform",
|
||||
"build-native": "cd desktop_native && node build.js",
|
||||
"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",
|
||||
"build:preload:dev": "cross-env NODE_ENV=development webpack --config webpack.config.js --config-name preload",
|
||||
"build:preload:watch": "cross-env NODE_ENV=development webpack --config webpack.config.js --config-name preload --watch",
|
||||
"build:macos-extension:mac": "./desktop_native/macos_provider/build.sh && node scripts/build-macos-extension.js mac",
|
||||
"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:macos-extension:mac": "./desktop_native/autofill_provider/build.sh && node scripts/build-macos-extension.js mac",
|
||||
"build:macos-extension:mas": "./desktop_native/autofill_provider/build.sh && node scripts/build-macos-extension.js mas",
|
||||
"build:macos-extension:masdev": "./desktop_native/autofill_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": "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",
|
||||
|
||||
Reference in New Issue
Block a user