diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3884bfda063..a768a9d51f6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 1b6522c94dd..718586a9b1a 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -187,6 +187,7 @@ "semver", "serde", "serde_json", + "serde_with", "simplelog", "style-loader", "sysinfo", diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 35228023224..5ff105a70c3 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -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", ] diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index da65db59e8c..6bddb234f45 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -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 } diff --git a/apps/desktop/desktop_native/macos_provider/.gitignore b/apps/desktop/desktop_native/autofill_provider/.gitignore similarity index 100% rename from apps/desktop/desktop_native/macos_provider/.gitignore rename to apps/desktop/desktop_native/autofill_provider/.gitignore diff --git a/apps/desktop/desktop_native/macos_provider/Cargo.toml b/apps/desktop/desktop_native/autofill_provider/Cargo.toml similarity index 82% rename from apps/desktop/desktop_native/macos_provider/Cargo.toml rename to apps/desktop/desktop_native/autofill_provider/Cargo.toml index d73bd2fa049..b90fd5d1171 100644 --- a/apps/desktop/desktop_native/macos_provider/Cargo.toml +++ b/apps/desktop/desktop_native/autofill_provider/Cargo.toml @@ -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"] } diff --git a/apps/desktop/desktop_native/macos_provider/README.md b/apps/desktop/desktop_native/autofill_provider/README.md similarity index 96% rename from apps/desktop/desktop_native/macos_provider/README.md rename to apps/desktop/desktop_native/autofill_provider/README.md index 1d4c1902465..86c49356161 100644 --- a/apps/desktop/desktop_native/macos_provider/README.md +++ b/apps/desktop/desktop_native/autofill_provider/README.md @@ -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. diff --git a/apps/desktop/desktop_native/macos_provider/build.sh b/apps/desktop/desktop_native/autofill_provider/build.sh similarity index 56% rename from apps/desktop/desktop_native/macos_provider/build.sh rename to apps/desktop/desktop_native/autofill_provider/build.sh index 2f7a2d03541..6807ef1fbc8 100755 --- a/apps/desktop/desktop_native/macos_provider/build.sh +++ b/apps/desktop/desktop_native/autofill_provider/build.sh @@ -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 diff --git a/apps/desktop/desktop_native/autofill_provider/src/assertion.rs b/apps/desktop/desktop_native/autofill_provider/src/assertion.rs new file mode 100644 index 00000000000..16d2f81fd61 --- /dev/null +++ b/apps/desktop/desktop_native/autofill_provider/src/assertion.rs @@ -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, + + /// User verification preference. + pub user_verification: UserVerification, + + /// List of allowed credential IDs. + pub allowed_credentials: Vec>, + + /// 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, + + /// 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, +} + +/// 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, + + /// 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, + + /// The app-specific local identifier for the credential, in our case, the + /// cipher ID. + #[cfg(target_os = "macos")] + pub record_identifier: Option, + + /// SHA-256 hash of the `clientDataJSON` for the assertion request. + pub client_data_hash: Vec, + + /// 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, + + /// 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, + + /// The signature for the WebAuthn attestation response. + pub signature: Vec, + + /// SHA-256 hash of the `clientDataJSON` used in the assertion. + pub client_data_hash: Vec, + + /// The WebAuthn authenticator data structure. + pub authenticator_data: Vec, + + /// The ID for the attested credential. + pub credential_id: Vec, +} + +/// 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 { + 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 { + 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/autofill_provider/src/lib.rs b/apps/desktop/desktop_native/autofill_provider/src/lib.rs new file mode 100644 index 00000000000..567fc9b3ac5 --- /dev/null +++ b/apps/desktop/desktop_native/autofill_provider/src/lib.rs @@ -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 { +/// 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, + + // 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, +} + +/// 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) { + 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) { + 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::(&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, + ) { + 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, + ) { + 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, + ) { + 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, + }, +} + +impl AutofillProviderClient { + 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 + } + + fn send_message( + &self, + message: impl Serialize + DeserializeOwned, + callback: Option>, + ) { + 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, +) -> 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 = Result; + +/// An implementation of a callback handler that can take a deadline. +pub struct TimedCallback { + tx: Arc>>>>, + rx: Arc>>>, +} + +impl Default for TimedCallback { + fn default() -> Self { + Self::new() + } +} + +impl TimedCallback { + /// 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>, + ) -> 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() + .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) { + 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 { + 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) -> Result + 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| { + 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); + } +} diff --git a/apps/desktop/desktop_native/autofill_provider/src/lock_status.rs b/apps/desktop/desktop_native/autofill_provider/src/lock_status.rs new file mode 100644 index 00000000000..134070bc54a --- /dev/null +++ b/apps/desktop/desktop_native/autofill_provider/src/lock_status.rs @@ -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 { + 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 { + 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/autofill_provider/src/registration.rs b/apps/desktop/desktop_native/autofill_provider/src/registration.rs new file mode 100644 index 00000000000..3f361588241 --- /dev/null +++ b/apps/desktop/desktop_native/autofill_provider/src/registration.rs @@ -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, + + /// SHA-256 hash of the `clientDataJSON` for the registration request. + pub client_data_hash: Vec, + + /// User verification preference. + pub user_verification: UserVerification, + + /// Supported key algorithms in COSE format. + pub supported_algorithms: Vec, + + /// 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>, + + /// 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, + + /// 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, + + /// The ID for the created credential. + pub credential_id: Vec, + + /// WebAuthn attestation object. + pub attestation_object: Vec, +} + +/// 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 { + 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/autofill_provider/src/window_handle_query.rs b/apps/desktop/desktop_native/autofill_provider/src/window_handle_query.rs new file mode 100644 index 00000000000..b4d388ae6c3 --- /dev/null +++ b/apps/desktop/desktop_native/autofill_provider/src/window_handle_query.rs @@ -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")] + pub handle: Vec, +} + +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); + } +} + +/// 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 { + fn on_complete(&self, response: WindowHandleQueryResponse) { + self.send(Ok(response)); + } + + fn on_error(&self, error: BitwardenError) { + self.send(Err(error)); + } +} diff --git a/apps/desktop/desktop_native/autofill_provider/uniffi-bindgen.rs b/apps/desktop/desktop_native/autofill_provider/uniffi-bindgen.rs new file mode 100644 index 00000000000..433c6c65b37 --- /dev/null +++ b/apps/desktop/desktop_native/autofill_provider/uniffi-bindgen.rs @@ -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."); +} diff --git a/apps/desktop/desktop_native/macos_provider/uniffi.toml b/apps/desktop/desktop_native/autofill_provider/uniffi.toml similarity index 100% rename from apps/desktop/desktop_native/macos_provider/uniffi.toml rename to apps/desktop/desktop_native/autofill_provider/uniffi.toml diff --git a/apps/desktop/desktop_native/macos_provider/src/assertion.rs b/apps/desktop/desktop_native/macos_provider/src/assertion.rs deleted file mode 100644 index c5b43bb87fa..00000000000 --- a/apps/desktop/desktop_native/macos_provider/src/assertion.rs +++ /dev/null @@ -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, - user_verification: UserVerification, - allowed_credentials: Vec>, - window_xy: Position, - //extension_input: Vec, TODO: Implement support for extensions -} - -#[derive(uniffi::Record, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PasskeyAssertionWithoutUserInterfaceRequest { - rp_id: String, - credential_id: Vec, - user_name: String, - user_handle: Vec, - record_identifier: Option, - client_data_hash: Vec, - user_verification: UserVerification, - window_xy: Position, -} - -#[derive(uniffi::Record, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PasskeyAssertionResponse { - rp_id: String, - user_handle: Vec, - signature: Vec, - client_data_hash: Vec, - authenticator_data: Vec, - credential_id: Vec, -} - -#[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 { - 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/macos_provider/src/lib.rs b/apps/desktop/desktop_native/macos_provider/src/lib.rs deleted file mode 100644 index 8619a77a0f2..00000000000 --- a/apps/desktop/desktop_native/macos_provider/src/lib.rs +++ /dev/null @@ -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, - - // 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 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::(&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_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 MacOSProviderClient { - #[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 + DeserializeOwned, - 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}" - ))); - } - } - } - } -} diff --git a/apps/desktop/desktop_native/macos_provider/src/registration.rs b/apps/desktop/desktop_native/macos_provider/src/registration.rs deleted file mode 100644 index c961566a86c..00000000000 --- a/apps/desktop/desktop_native/macos_provider/src/registration.rs +++ /dev/null @@ -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, - client_data_hash: Vec, - user_verification: UserVerification, - supported_algorithms: Vec, - window_xy: Position, - excluded_credentials: Vec>, -} - -#[derive(uniffi::Record, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PasskeyRegistrationResponse { - rp_id: String, - client_data_hash: Vec, - credential_id: Vec, - attestation_object: Vec, -} - -#[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 { - 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/macos_provider/uniffi-bindgen.rs b/apps/desktop/desktop_native/macos_provider/uniffi-bindgen.rs deleted file mode 100644 index f6cff6cf1d9..00000000000 --- a/apps/desktop/desktop_native/macos_provider/uniffi-bindgen.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - uniffi::uniffi_bindgen_main() -} diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index 3de9468c8ab..35c5b8e720e 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -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) } } } diff --git a/apps/desktop/macos/desktop.xcodeproj/project.pbxproj b/apps/desktop/macos/desktop.xcodeproj/project.pbxproj index c3cb34b6bea..ddca8550a0d 100644 --- a/apps/desktop/macos/desktop.xcodeproj/project.pbxproj +++ b/apps/desktop/macos/desktop.xcodeproj/project.pbxproj @@ -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 = ""; }; + 3368DB382C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BitwardenMacosProviderFFI.xcframework; path = ../desktop_native/autofill_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 = ""; }; diff --git a/apps/desktop/package.json b/apps/desktop/package.json index aabf26e76bd..7203e28d3ad 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -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",