mirror of
https://github.com/bitwarden/browser
synced 2025-12-31 07:33:23 +00:00
* Turn on passkeys and dev mode
* PM-19138: Add try-catch to desktop-autofill (#13964)
* PM-19424: React to IPC disconnect (#14123)
* React to IPC disconnects
* Minor cleanup
* Update apps/desktop/package.json
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
* Relaxed ordering
---------
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
* Autofill/pm 9034 implement passkey for unlocked accounts (#13826)
* Passkey stuff
Co-authored-by: Anders Åberg <github@andersaberg.com>
* Ugly hacks
* Work On Modal State Management
* Applying modalStyles
* modal
* Improved hide/show
* fixed promise
* File name
* fix prettier
* Protecting against null API's and undefined data
* Only show fake popup to devs
* cleanup mock code
* rename minmimal-app to modal-app
* Added comment
* Added comment
* removed old comment
* Avoided changing minimum size
* Add small comment
* Rename component
* adress feedback
* Fixed uppercase file
* Fixed build
* Added codeowners
* added void
* commentary
* feat: reset setting on app start
* Moved reset to be in main / process launch
* Add comment to create window
* Added a little bit of styling
* Use Messaging service to loadUrl
* Enable passkeysautofill
* Add logging
* halfbaked
* Integration working
* And now it works without extra delay
* Clean up
* add note about messaging
* lb
* removed console.logs
* Cleanup and adress review feedback
* This hides the swift UI
* add modal components
* update modal with correct ciphers and functionality
* add create screen
* pick credential, draft
* Remove logger
* a whole lot of wiring
* not working
* Improved wiring
* Cancel after 90s
* Introduced observable
* update cipher handling
* update to use matchesUri
* Launching bitwarden if its not running
* Passing position from native to electron
* Rename inModalMode to modalMode
* remove tap
* revert spaces
* added back isDev
* cleaned up a bit
* Cleanup swift file
* tweaked logging
* clean up
* Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* Update apps/desktop/src/platform/main/autofill/native-autofill.main.ts
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* Update apps/desktop/src/platform/services/desktop-settings.service.ts
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* adress position feedback
* Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* Removed extra logging
* Adjusted error logging
* Use .error to log errors
* remove dead code
* Update desktop-autofill.service.ts
* use parseCredentialId instead of guidToRawFormat
* Update apps/desktop/src/autofill/services/desktop-autofill.service.ts
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* Change windowXy to a Record instead of [number,number]
* Update apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* Remove unsued dep and comment
* changed timeout to be spec recommended maxium, 10 minutes, for now.
* Correctly assume UP
* Removed extra cancelRequest in deinint
* Add timeout and UV to confirmChoseCipher
UV is performed by UI, not the service
* Improved docs regarding undefined cipherId
* cleanup: UP is no longer undefined
* Run completeError if ipc messages conversion failed
* don't throw, instead return undefined
* Disabled passkey provider
* Throw error if no activeUserId was found
* removed comment
* Fixed lint
* removed unsued service
* reset entitlement formatting
* Update entitlements.mas.plist
* Fix build issues
* Fix import issues
* Update route names to use `fido2`
* Fix being unable to select a passkey
* Fix linting issues
* Followup to fix merge issues and other comments
* Update `userHandle` value
* Add error handling for missing session or other errors
* Remove unused route
* Fix linting issues
* Simplify updateCredential method
* Followup to remove comments and timeouts and handle errors
* Address lint issue by using `takeUntilDestroyed`
* PR Followup for typescript and vault concerns
* Add try block for cipher creation
* Make userId manditory for cipher service
---------
Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
Co-authored-by: Anders Åberg <github@andersaberg.com>
Co-authored-by: Anders Åberg <anders@andersaberg.com>
Co-authored-by: Colton Hurst <colton@coltonhurst.com>
Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
Co-authored-by: Evan Bassler <evanbassler@Mac.attlocal.net>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* PM-11455: Trigger sync when user enables OS setting (#14127)
* Implemented a SendNativeStatus command
This allows reporting status or asking the electron app to do something.
* fmt
* Update apps/desktop/src/autofill/services/desktop-autofill.service.ts
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
* clean up
* Don't add empty callbacks
* Removed comment
---------
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
* Added support for handling a locked vault
Handle unlocktimeout
* PM-19511: Add support for ExcludedCredentials (#14128)
* works
* Add mapping
* remove the build script
* cleanup
* simplify updatedCipher (#14179)
* Fix base64url decode on MacOS passkeys (#14227)
* Add support for padding in base64url decode
* whitespace
* whitespace
* Autofill/pm 17444 use reprompt (#14004)
* Passkey stuff
Co-authored-by: Anders Åberg <github@andersaberg.com>
* Ugly hacks
* Work On Modal State Management
* Applying modalStyles
* modal
* Improved hide/show
* fixed promise
* File name
* fix prettier
* Protecting against null API's and undefined data
* Only show fake popup to devs
* cleanup mock code
* rename minmimal-app to modal-app
* Added comment
* Added comment
* removed old comment
* Avoided changing minimum size
* Add small comment
* Rename component
* adress feedback
* Fixed uppercase file
* Fixed build
* Added codeowners
* added void
* commentary
* feat: reset setting on app start
* Moved reset to be in main / process launch
* Add comment to create window
* Added a little bit of styling
* Use Messaging service to loadUrl
* Enable passkeysautofill
* Add logging
* halfbaked
* Integration working
* And now it works without extra delay
* Clean up
* add note about messaging
* lb
* removed console.logs
* Cleanup and adress review feedback
* This hides the swift UI
* add modal components
* update modal with correct ciphers and functionality
* add create screen
* pick credential, draft
* Remove logger
* a whole lot of wiring
* not working
* Improved wiring
* Cancel after 90s
* Introduced observable
* update cipher handling
* update to use matchesUri
* Launching bitwarden if its not running
* Passing position from native to electron
* Rename inModalMode to modalMode
* remove tap
* revert spaces
* added back isDev
* cleaned up a bit
* Cleanup swift file
* tweaked logging
* clean up
* Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* Update apps/desktop/src/platform/main/autofill/native-autofill.main.ts
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* Update apps/desktop/src/platform/services/desktop-settings.service.ts
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* adress position feedback
* Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* Removed extra logging
* Adjusted error logging
* Use .error to log errors
* remove dead code
* Update desktop-autofill.service.ts
* use parseCredentialId instead of guidToRawFormat
* Update apps/desktop/src/autofill/services/desktop-autofill.service.ts
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* Change windowXy to a Record instead of [number,number]
* Update apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* Remove unsued dep and comment
* changed timeout to be spec recommended maxium, 10 minutes, for now.
* Correctly assume UP
* Removed extra cancelRequest in deinint
* Add timeout and UV to confirmChoseCipher
UV is performed by UI, not the service
* Improved docs regarding undefined cipherId
* cleanup: UP is no longer undefined
* Run completeError if ipc messages conversion failed
* don't throw, instead return undefined
* Disabled passkey provider
* Throw error if no activeUserId was found
* removed comment
* Fixed lint
* removed unsued service
* reset entitlement formatting
* Update entitlements.mas.plist
* Fix build issues
* Fix import issues
* Update route names to use `fido2`
* Fix being unable to select a passkey
* Fix linting issues
* Added support for handling a locked vault
* Followup to fix merge issues and other comments
* Update `userHandle` value
* Add error handling for missing session or other errors
* Remove unused route
* Fix linting issues
* Simplify updateCredential method
* Add master password reprompt on passkey create
* Followup to remove comments and timeouts and handle errors
* Address lint issue by using `takeUntilDestroyed`
* Add MP prompt to cipher selection
* Change how timeout is handled
* Include `of` from rxjs
* Hide blue header for passkey popouts (#14095)
* Hide blue header for passkey popouts
* Fix issue with test
* Fix ngOnDestroy complaint
* Import OnDestroy correctly
* Only require master password if item requires it
---------
Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
Co-authored-by: Anders Åberg <github@andersaberg.com>
Co-authored-by: Anders Åberg <anders@andersaberg.com>
Co-authored-by: Colton Hurst <colton@coltonhurst.com>
Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
Co-authored-by: Evan Bassler <evanbassler@Mac.attlocal.net>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* Change modal size to 600x600
* Improve MacOS Syncing
This changes the behaviour to react to logoff, but not to account locks. It also adds better error handling on the native side.
* Improved modalPosition by allowing multiple calls to applyModalStyles
* moved imports to please lint
* Make passkey header stick for select and create (#14357)
* Added local build command
* Exclude credentials using kvc to avoid comilation error in cicd (#14568)
* Fix syntax error
* Don't use kvc
* Enables the autofill extension in mac and mas builds (#14373)
* Enables autofill extension building
* Try use macos-14
* add --break-system-packages for macos14
* revert using build-native
* try add rustup target add x86_64-apple-darwin
* add more rustup target add x86_64-apple-darwin
* try to force sdk version
* Show SDK versions
* USE KVC for excludedCredentials
* added xcodebuild deugging
* Revert "try to force sdk version"
This reverts commit d94f2550ad.
* Use macos-15
* undo merge
* remove macos-15 from cli
* remove macos-15 from browser
---------
Co-authored-by: Anders Åberg <anders@andersaberg.com>
* Improve Autofill IPC reliability (#14358)
* Delay IPC server start
* Better ipc handling
* Rename ready() to listenerReady()
---------
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
* feat: add test and check for too long buffers (#14775)
* Autofill/PM-19511: Overwrite and reprompt (#14288)
* Show items for url that don't have passkey
* Show existing login items in the UI
* Filter available cipher results (#14399)
* Filter available cipher results
* Fix linting issues
* Update logic for eligible ciphers
* Remove unused method to check matching username
* PM-20608 update styling for excludedCredentials (#14444)
* PM-20608 update styling for excludedCredentials
* Have flow correctly move to creation for excluded cipher
* Remove duplicate confirmNeCredential call
* Revert fido2-authenticator changes and move the excluded check
* Create a separate component for excluded cipher view
* Display traffic light MacOS buttons when the vault is locked (#14673)
* Remove unneccessary filter for excludedCiphers
* Remove dead code from the excluded ciphers work
* Remove excludedCipher checks from fido2 create and vault
* Remove excludedCipher remnants from vault and simplify create cipher logic
* Move cipherHasNoOtherPasskeys to shared fido2-utils
* Remove all containsExcludedCipher references
* Use `bufferToString` to convert `userHandle`
---------
Co-authored-by: Jeffrey Holland <jholland@livefront.com>
Co-authored-by: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com>
* Move modal files to `autofill` and rename dir to `credentials` (#14757)
* Show existing login items in the UI
* Filter available cipher results (#14399)
* Filter available cipher results
* Fix linting issues
* Update logic for eligible ciphers
* Remove unused method to check matching username
* PM-20608 update styling for excludedCredentials (#14444)
* PM-20608 update styling for excludedCredentials
* Have flow correctly move to creation for excluded cipher
* Remove duplicate confirmNeCredential call
* Revert fido2-authenticator changes and move the excluded check
* Create a separate component for excluded cipher view
* Display traffic light MacOS buttons when the vault is locked (#14673)
* Remove unneccessary filter for excludedCiphers
* Remove dead code from the excluded ciphers work
* Remove excludedCipher checks from fido2 create and vault
* Move modal files to `autofill` and rename dir to `credentials`
* Update merge issues
* Add tests for `cipherHasNoOtherPasskeys` (#14829)
* Adjust spacing to place new login button below other items (#14877)
* Adjust spacing to place new login button below other items
* Add correct design when no credentials available (#14879)
* Autofill/pm 21903 use translations everywhere for passkeys (#14908)
* Adjust spacing to place new login button below other items
* Add correct design when no credentials available
* Add correct design when no credentials available (#14879)
* Remove hardcoded strings and use translations in passkey flow
* Remove duplicate `select` translation
* Autofill/pm 21864 center unlock vault modal (#14867)
* Center the Locked Vault modal when using passkeys
* Revert swift changes and handle offscreen modals
* Remove comments
* Add rustup for cicd to work (#15055)
* Hide credentials that are in the bin (#15034)
* Add tests for passkey components (#15185)
* Add tests for passkey components
* Reuse cipher in chooseCipher tests and simplify mock creation
* Autofill/pm 22821 center vault modal (#15243)
* Center the vault modal for passkeys
* Add comments and fix electron-builder.json
* Set values to Int32 in the ternaries
* Refactor Fido2 Components (#15105)
* Refactor Fido2 Components
* Address error message and missing session
* Address remaining missing session
* Reset modals so subsequent creates work (#15145)
* Fix broken test
* Rename relevantCiphers to displayedCiphers
* Clean up heading settings, errors, and other concerns
* Address missing comments and throw error in try block
* fix type issue for SimpleDialogType
* fix type issue for SimpleDialogType
* Revert new type
* try using as null to satisfy type issue
* Remove use of firstValueFrom in create component
* PM-22476: Show config UI while enabling Bitwarden (#15149)
* Show config ui while enabling Bitwarden
* locals
* Added Localizable strings
* Changed the linebreakmode
* Removed swedish locals
* Add provisioning profile values to electron build (#15412)
* Address BitwardenShield icon issue
* Fix fido2-vault component
* Display the vault modal when selecting Bitwarden... (#15257)
* Passkeys filtering breaks on SSH keys (#15448)
* Display the blue header on the locked vault passkey flow (#15655)
* PM-23848: Use the MacOS UI-friendly API instead (#15650)
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Fix action text and close vault modal (#15634)
* Fix action text and close vault modal
* Fix broken tests
* Update SVG to support dark mode (#15805)
* When a locked vault is unlocked displays correctly (#15612)
* When a locked vault is unlocked displays correctly
* Keep old behavior while checking for recently unlocked vault
* Revert the electron-builder
* Simplify by using a simple redirect when vault unlocked
* Remove single use of `userSelectedCipher`
* Add a guard clause to unlock
* Revert to original spacing
* Add reactive guard to unlock vault
* Fix for passkey picker closing prematurely
* Remove unneeded root navigation in ensureUnlockedVault
* Fix vault not unlocking
* Update broken tests for lock component
* Add missing brace to preload.ts
* Run lint
* Added explainer
* Moved the explainer
* Tidying up readme
* Add feature flag to short-circuit the passkey provider (#16003)
* Add feature flag to short-circuit the passkey provider
* Check FF in renderer instead
* Lint fixes
* PM-22175: Improve launch of app + window positioning (#15658)
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Fix launch of app + window pos
* Wait for animation to complete and use proper position
* Wait for animation to complete and use proper position
* Added commentary
* Remove console.log
* Remove call to removed function
---------
Co-authored-by: Jeffrey Holland <jholland@livefront.com>
Co-authored-by: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com>
* Update fido2-vault and fido2-service implementations
* Use tailwind-alike classes for new styles
* Add label to biticons in passkey modals
* Fix broken vault test
* Revert to original `isDev` function
* Add comment to lock component describing `disable-redirect` param
* Use tailwind classes instead of custom sticky header class
* Use standard `tw-z-10` for z-index
* Change log service levels
* Mock svg icons for CI
* Add back provisioning profiles
* Remove `--break-system-packages` and simplify commands
* Revert `cipherId` param for `confirmNewCredential`
* Remove placeholder UI
* Small improvements to the readme
* Remove optional userId and deprecated method
* Autofill should own the macos_provider (#16271)
* Autofill should own the macos_provider
* Autofill should own the macos_provider
* Remove unnecessary logs, no magic numbers, revert `cipherId?`
* Fixes for broken build
* Update test issues
* [BEEEP] Use tracing in macOS provider
* Update comments and add null check for ciphers
* Update status comments and readme
* Remove electron modal mode link
* Clarify modal mode use
* Add comment about usernames
* Add comment that we don't support extensions yet
* Added comment about base64 format
* Use NO_CALLBACK_INDICATOR
* cb -> callback
* Update apps/desktop/desktop_native/napi/src/lib.rs
Co-authored-by: neuronull <9162534+neuronull@users.noreply.github.com>
* Clean up Fido2Create subscriptions and update comments
* added comment to clarify silent exception
* Add comments
* clean up unwrap()
* set log level filter to INFO
* Address modal popup issue
* plutil on Info.plist
* Adhere to style guides
* Fix broken lock ui component tests
* Fix broken lock ui component tests
* Added codeowners entry
* logservice.warning -> debug
* Uint8Array -> ArrayBuffer
* Remove autofill entitlement
* Fix linting issues
* Fix arm build issue
* Adjust build command
* Add missing entitlement
* revert missing entitlement change
* Add proper autofill entitlements
* Remove autofill extension from mas builds
* Run rust formatter
---------
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
Co-authored-by: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com>
Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
Co-authored-by: Colton Hurst <colton@coltonhurst.com>
Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
Co-authored-by: Evan Bassler <evanbassler@Mac.attlocal.net>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
Co-authored-by: Nathan Ansel <nathan@livefront.com>
Co-authored-by: Jeffrey Holland <jholland@livefront.com>
Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com>
Co-authored-by: neuronull <9162534+neuronull@users.noreply.github.com>
1239 lines
42 KiB
Rust
1239 lines
42 KiB
Rust
#[macro_use]
|
||
extern crate napi_derive;
|
||
|
||
mod passkey_authenticator_internal;
|
||
mod registry;
|
||
|
||
#[napi]
|
||
pub mod passwords {
|
||
/// The error message returned when a password is not found during retrieval or deletion.
|
||
#[napi]
|
||
pub const PASSWORD_NOT_FOUND: &str = desktop_core::password::PASSWORD_NOT_FOUND;
|
||
|
||
/// Fetch the stored password from the keychain.
|
||
/// Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist.
|
||
#[napi]
|
||
pub async fn get_password(service: String, account: String) -> napi::Result<String> {
|
||
desktop_core::password::get_password(&service, &account)
|
||
.await
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
|
||
/// Save the password to the keychain. Adds an entry if none exists otherwise updates the
|
||
/// existing entry.
|
||
#[napi]
|
||
pub async fn set_password(
|
||
service: String,
|
||
account: String,
|
||
password: String,
|
||
) -> napi::Result<()> {
|
||
desktop_core::password::set_password(&service, &account, &password)
|
||
.await
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
|
||
/// Delete the stored password from the keychain.
|
||
/// Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist.
|
||
#[napi]
|
||
pub async fn delete_password(service: String, account: String) -> napi::Result<()> {
|
||
desktop_core::password::delete_password(&service, &account)
|
||
.await
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
|
||
/// Checks if the os secure storage is available
|
||
#[napi]
|
||
pub async fn is_available() -> napi::Result<bool> {
|
||
desktop_core::password::is_available()
|
||
.await
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
}
|
||
|
||
#[napi]
|
||
pub mod biometrics {
|
||
use desktop_core::biometric::{Biometric, BiometricTrait};
|
||
|
||
// Prompt for biometric confirmation
|
||
#[napi]
|
||
pub async fn prompt(
|
||
hwnd: napi::bindgen_prelude::Buffer,
|
||
message: String,
|
||
) -> napi::Result<bool> {
|
||
Biometric::prompt(hwnd.into(), message)
|
||
.await
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
|
||
#[napi]
|
||
pub async fn available() -> napi::Result<bool> {
|
||
Biometric::available()
|
||
.await
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
|
||
#[napi]
|
||
pub async fn set_biometric_secret(
|
||
service: String,
|
||
account: String,
|
||
secret: String,
|
||
key_material: Option<KeyMaterial>,
|
||
iv_b64: String,
|
||
) -> napi::Result<String> {
|
||
Biometric::set_biometric_secret(
|
||
&service,
|
||
&account,
|
||
&secret,
|
||
key_material.map(|m| m.into()),
|
||
&iv_b64,
|
||
)
|
||
.await
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
|
||
/// Retrieves the biometric secret for the given service and account.
|
||
/// Throws Error with message [`passwords::PASSWORD_NOT_FOUND`] if the secret does not exist.
|
||
#[napi]
|
||
pub async fn get_biometric_secret(
|
||
service: String,
|
||
account: String,
|
||
key_material: Option<KeyMaterial>,
|
||
) -> napi::Result<String> {
|
||
Biometric::get_biometric_secret(&service, &account, key_material.map(|m| m.into()))
|
||
.await
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
|
||
/// Derives key material from biometric data. Returns a string encoded with a
|
||
/// base64 encoded key and the base64 encoded challenge used to create it
|
||
/// separated by a `|` character.
|
||
///
|
||
/// If the iv is provided, it will be used as the challenge. Otherwise a random challenge will
|
||
/// be generated.
|
||
///
|
||
/// `format!("<key_base64>|<iv_base64>")`
|
||
#[allow(clippy::unused_async)] // FIXME: Remove unused async!
|
||
#[napi]
|
||
pub async fn derive_key_material(iv: Option<String>) -> napi::Result<OsDerivedKey> {
|
||
Biometric::derive_key_material(iv.as_deref())
|
||
.map(|k| k.into())
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
|
||
#[napi(object)]
|
||
pub struct KeyMaterial {
|
||
pub os_key_part_b64: String,
|
||
pub client_key_part_b64: Option<String>,
|
||
}
|
||
|
||
impl From<KeyMaterial> for desktop_core::biometric::KeyMaterial {
|
||
fn from(km: KeyMaterial) -> Self {
|
||
desktop_core::biometric::KeyMaterial {
|
||
os_key_part_b64: km.os_key_part_b64,
|
||
client_key_part_b64: km.client_key_part_b64,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[napi(object)]
|
||
pub struct OsDerivedKey {
|
||
pub key_b64: String,
|
||
pub iv_b64: String,
|
||
}
|
||
|
||
impl From<desktop_core::biometric::OsDerivedKey> for OsDerivedKey {
|
||
fn from(km: desktop_core::biometric::OsDerivedKey) -> Self {
|
||
OsDerivedKey {
|
||
key_b64: km.key_b64,
|
||
iv_b64: km.iv_b64,
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[napi]
|
||
pub mod biometrics_v2 {
|
||
use desktop_core::biometric_v2::BiometricTrait;
|
||
|
||
#[napi]
|
||
pub struct BiometricLockSystem {
|
||
inner: desktop_core::biometric_v2::BiometricLockSystem,
|
||
}
|
||
|
||
#[napi]
|
||
pub fn init_biometric_system() -> napi::Result<BiometricLockSystem> {
|
||
Ok(BiometricLockSystem {
|
||
inner: desktop_core::biometric_v2::BiometricLockSystem::new(),
|
||
})
|
||
}
|
||
|
||
#[napi]
|
||
pub async fn authenticate(
|
||
biometric_lock_system: &BiometricLockSystem,
|
||
hwnd: napi::bindgen_prelude::Buffer,
|
||
message: String,
|
||
) -> napi::Result<bool> {
|
||
biometric_lock_system
|
||
.inner
|
||
.authenticate(hwnd.into(), message)
|
||
.await
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
|
||
#[napi]
|
||
pub async fn authenticate_available(
|
||
biometric_lock_system: &BiometricLockSystem,
|
||
) -> napi::Result<bool> {
|
||
biometric_lock_system
|
||
.inner
|
||
.authenticate_available()
|
||
.await
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
|
||
#[napi]
|
||
pub async fn enroll_persistent(
|
||
biometric_lock_system: &BiometricLockSystem,
|
||
user_id: String,
|
||
key: napi::bindgen_prelude::Buffer,
|
||
) -> napi::Result<()> {
|
||
biometric_lock_system
|
||
.inner
|
||
.enroll_persistent(&user_id, &key)
|
||
.await
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
|
||
#[napi]
|
||
pub async fn provide_key(
|
||
biometric_lock_system: &BiometricLockSystem,
|
||
user_id: String,
|
||
key: napi::bindgen_prelude::Buffer,
|
||
) -> napi::Result<()> {
|
||
biometric_lock_system
|
||
.inner
|
||
.provide_key(&user_id, &key)
|
||
.await;
|
||
Ok(())
|
||
}
|
||
|
||
#[napi]
|
||
pub async fn unlock(
|
||
biometric_lock_system: &BiometricLockSystem,
|
||
user_id: String,
|
||
hwnd: napi::bindgen_prelude::Buffer,
|
||
) -> napi::Result<napi::bindgen_prelude::Buffer> {
|
||
biometric_lock_system
|
||
.inner
|
||
.unlock(&user_id, hwnd.into())
|
||
.await
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
.map(|v| v.into())
|
||
}
|
||
|
||
#[napi]
|
||
pub async fn unlock_available(
|
||
biometric_lock_system: &BiometricLockSystem,
|
||
user_id: String,
|
||
) -> napi::Result<bool> {
|
||
biometric_lock_system
|
||
.inner
|
||
.unlock_available(&user_id)
|
||
.await
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
|
||
#[napi]
|
||
pub async fn has_persistent(
|
||
biometric_lock_system: &BiometricLockSystem,
|
||
user_id: String,
|
||
) -> napi::Result<bool> {
|
||
biometric_lock_system
|
||
.inner
|
||
.has_persistent(&user_id)
|
||
.await
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
|
||
#[napi]
|
||
pub async fn unenroll(
|
||
biometric_lock_system: &BiometricLockSystem,
|
||
user_id: String,
|
||
) -> napi::Result<()> {
|
||
biometric_lock_system
|
||
.inner
|
||
.unenroll(&user_id)
|
||
.await
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
}
|
||
|
||
#[napi]
|
||
pub mod clipboards {
|
||
#[allow(clippy::unused_async)] // FIXME: Remove unused async!
|
||
#[napi]
|
||
pub async fn read() -> napi::Result<String> {
|
||
desktop_core::clipboard::read().map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
|
||
#[allow(clippy::unused_async)] // FIXME: Remove unused async!
|
||
#[napi]
|
||
pub async fn write(text: String, password: bool) -> napi::Result<()> {
|
||
desktop_core::clipboard::write(&text, password)
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
}
|
||
|
||
#[napi]
|
||
pub mod sshagent {
|
||
use std::sync::Arc;
|
||
|
||
use napi::{
|
||
bindgen_prelude::Promise,
|
||
threadsafe_function::{ErrorStrategy::CalleeHandled, ThreadsafeFunction},
|
||
};
|
||
use tokio::{self, sync::Mutex};
|
||
use tracing::error;
|
||
|
||
#[napi]
|
||
pub struct SshAgentState {
|
||
state: desktop_core::ssh_agent::BitwardenDesktopAgent,
|
||
}
|
||
|
||
#[napi(object)]
|
||
pub struct PrivateKey {
|
||
pub private_key: String,
|
||
pub name: String,
|
||
pub cipher_id: String,
|
||
}
|
||
|
||
#[napi(object)]
|
||
pub struct SshKey {
|
||
pub private_key: String,
|
||
pub public_key: String,
|
||
pub key_fingerprint: String,
|
||
}
|
||
|
||
#[napi(object)]
|
||
pub struct SshUIRequest {
|
||
pub cipher_id: Option<String>,
|
||
pub is_list: bool,
|
||
pub process_name: String,
|
||
pub is_forwarding: bool,
|
||
pub namespace: Option<String>,
|
||
}
|
||
|
||
#[allow(clippy::unused_async)] // FIXME: Remove unused async!
|
||
#[napi]
|
||
pub async fn serve(
|
||
callback: ThreadsafeFunction<SshUIRequest, CalleeHandled>,
|
||
) -> napi::Result<SshAgentState> {
|
||
let (auth_request_tx, mut auth_request_rx) =
|
||
tokio::sync::mpsc::channel::<desktop_core::ssh_agent::SshAgentUIRequest>(32);
|
||
let (auth_response_tx, auth_response_rx) =
|
||
tokio::sync::broadcast::channel::<(u32, bool)>(32);
|
||
let auth_response_tx_arc = Arc::new(Mutex::new(auth_response_tx));
|
||
tokio::spawn(async move {
|
||
let _ = auth_response_rx;
|
||
|
||
while let Some(request) = auth_request_rx.recv().await {
|
||
let cloned_response_tx_arc = auth_response_tx_arc.clone();
|
||
let cloned_callback = callback.clone();
|
||
tokio::spawn(async move {
|
||
let auth_response_tx_arc = cloned_response_tx_arc;
|
||
let callback = cloned_callback;
|
||
let promise_result: Result<Promise<bool>, napi::Error> = callback
|
||
.call_async(Ok(SshUIRequest {
|
||
cipher_id: request.cipher_id,
|
||
is_list: request.is_list,
|
||
process_name: request.process_name,
|
||
is_forwarding: request.is_forwarding,
|
||
namespace: request.namespace,
|
||
}))
|
||
.await;
|
||
match promise_result {
|
||
Ok(promise_result) => match promise_result.await {
|
||
Ok(result) => {
|
||
let _ = auth_response_tx_arc
|
||
.lock()
|
||
.await
|
||
.send((request.request_id, result))
|
||
.expect("should be able to send auth response to agent");
|
||
}
|
||
Err(e) => {
|
||
error!(error = %e, "Calling UI callback promise was rejected");
|
||
let _ = auth_response_tx_arc
|
||
.lock()
|
||
.await
|
||
.send((request.request_id, false))
|
||
.expect("should be able to send auth response to agent");
|
||
}
|
||
},
|
||
Err(e) => {
|
||
error!(error = %e, "Calling UI callback could not create promise");
|
||
let _ = auth_response_tx_arc
|
||
.lock()
|
||
.await
|
||
.send((request.request_id, false))
|
||
.expect("should be able to send auth response to agent");
|
||
}
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
match desktop_core::ssh_agent::BitwardenDesktopAgent::start_server(
|
||
auth_request_tx,
|
||
Arc::new(Mutex::new(auth_response_rx)),
|
||
) {
|
||
Ok(state) => Ok(SshAgentState { state }),
|
||
Err(e) => Err(napi::Error::from_reason(e.to_string())),
|
||
}
|
||
}
|
||
|
||
#[napi]
|
||
pub fn stop(agent_state: &mut SshAgentState) -> napi::Result<()> {
|
||
let bitwarden_agent_state = &mut agent_state.state;
|
||
bitwarden_agent_state.stop();
|
||
Ok(())
|
||
}
|
||
|
||
#[napi]
|
||
pub fn is_running(agent_state: &mut SshAgentState) -> bool {
|
||
let bitwarden_agent_state = agent_state.state.clone();
|
||
bitwarden_agent_state.is_running()
|
||
}
|
||
|
||
#[napi]
|
||
pub fn set_keys(
|
||
agent_state: &mut SshAgentState,
|
||
new_keys: Vec<PrivateKey>,
|
||
) -> napi::Result<()> {
|
||
let bitwarden_agent_state = &mut agent_state.state;
|
||
bitwarden_agent_state
|
||
.set_keys(
|
||
new_keys
|
||
.iter()
|
||
.map(|k| (k.private_key.clone(), k.name.clone(), k.cipher_id.clone()))
|
||
.collect(),
|
||
)
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
|
||
Ok(())
|
||
}
|
||
|
||
#[napi]
|
||
pub fn lock(agent_state: &mut SshAgentState) -> napi::Result<()> {
|
||
let bitwarden_agent_state = &mut agent_state.state;
|
||
bitwarden_agent_state
|
||
.lock()
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
|
||
#[napi]
|
||
pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> {
|
||
let bitwarden_agent_state = &mut agent_state.state;
|
||
bitwarden_agent_state
|
||
.clear_keys()
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
}
|
||
|
||
#[napi]
|
||
pub mod processisolations {
|
||
#[allow(clippy::unused_async)] // FIXME: Remove unused async!
|
||
#[napi]
|
||
pub async fn disable_coredumps() -> napi::Result<()> {
|
||
desktop_core::process_isolation::disable_coredumps()
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
|
||
#[allow(clippy::unused_async)] // FIXME: Remove unused async!
|
||
#[napi]
|
||
pub async fn is_core_dumping_disabled() -> napi::Result<bool> {
|
||
desktop_core::process_isolation::is_core_dumping_disabled()
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
|
||
#[allow(clippy::unused_async)] // FIXME: Remove unused async!
|
||
#[napi]
|
||
pub async fn isolate_process() -> napi::Result<()> {
|
||
desktop_core::process_isolation::isolate_process()
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
}
|
||
|
||
#[napi]
|
||
pub mod powermonitors {
|
||
use napi::{
|
||
threadsafe_function::{
|
||
ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode,
|
||
},
|
||
tokio,
|
||
};
|
||
|
||
#[napi]
|
||
pub async fn on_lock(callback: ThreadsafeFunction<(), CalleeHandled>) -> napi::Result<()> {
|
||
let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(32);
|
||
desktop_core::powermonitor::on_lock(tx)
|
||
.await
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
|
||
tokio::spawn(async move {
|
||
while let Some(()) = rx.recv().await {
|
||
callback.call(Ok(()), ThreadsafeFunctionCallMode::NonBlocking);
|
||
}
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[napi]
|
||
pub async fn is_lock_monitor_available() -> napi::Result<bool> {
|
||
Ok(desktop_core::powermonitor::is_lock_monitor_available().await)
|
||
}
|
||
}
|
||
|
||
#[napi]
|
||
pub mod windows_registry {
|
||
#[allow(clippy::unused_async)] // FIXME: Remove unused async!
|
||
#[napi]
|
||
pub async fn create_key(key: String, subkey: String, value: String) -> napi::Result<()> {
|
||
crate::registry::create_key(&key, &subkey, &value)
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
|
||
#[allow(clippy::unused_async)] // FIXME: Remove unused async!
|
||
#[napi]
|
||
pub async fn delete_key(key: String, subkey: String) -> napi::Result<()> {
|
||
crate::registry::delete_key(&key, &subkey)
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
}
|
||
|
||
#[napi]
|
||
pub mod ipc {
|
||
use desktop_core::ipc::server::{Message, MessageType};
|
||
use napi::threadsafe_function::{
|
||
ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode,
|
||
};
|
||
|
||
#[napi(object)]
|
||
pub struct IpcMessage {
|
||
pub client_id: u32,
|
||
pub kind: IpcMessageType,
|
||
pub message: Option<String>,
|
||
}
|
||
|
||
impl From<Message> for IpcMessage {
|
||
fn from(message: Message) -> Self {
|
||
IpcMessage {
|
||
client_id: message.client_id,
|
||
kind: message.kind.into(),
|
||
message: message.message,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[napi]
|
||
pub enum IpcMessageType {
|
||
Connected,
|
||
Disconnected,
|
||
Message,
|
||
}
|
||
|
||
impl From<MessageType> for IpcMessageType {
|
||
fn from(message_type: MessageType) -> Self {
|
||
match message_type {
|
||
MessageType::Connected => IpcMessageType::Connected,
|
||
MessageType::Disconnected => IpcMessageType::Disconnected,
|
||
MessageType::Message => IpcMessageType::Message,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[napi]
|
||
pub struct IpcServer {
|
||
server: desktop_core::ipc::server::Server,
|
||
}
|
||
|
||
#[napi]
|
||
impl IpcServer {
|
||
/// Create and start the IPC server without blocking.
|
||
///
|
||
/// @param name The endpoint name to listen on. This name uniquely identifies the IPC
|
||
/// connection and must be the same for both the server and client. @param callback
|
||
/// This function will be called whenever a message is received from a client.
|
||
#[allow(clippy::unused_async)] // FIXME: Remove unused async!
|
||
#[napi(factory)]
|
||
pub async fn listen(
|
||
name: String,
|
||
#[napi(ts_arg_type = "(error: null | Error, message: IpcMessage) => void")]
|
||
callback: ThreadsafeFunction<IpcMessage, ErrorStrategy::CalleeHandled>,
|
||
) -> napi::Result<Self> {
|
||
let (send, mut recv) = tokio::sync::mpsc::channel::<Message>(32);
|
||
tokio::spawn(async move {
|
||
while let Some(message) = recv.recv().await {
|
||
callback.call(Ok(message.into()), ThreadsafeFunctionCallMode::NonBlocking);
|
||
}
|
||
});
|
||
|
||
let path = desktop_core::ipc::path(&name);
|
||
|
||
let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| {
|
||
napi::Error::from_reason(format!(
|
||
"Error listening to server - Path: {path:?} - Error: {e} - {e:?}"
|
||
))
|
||
})?;
|
||
|
||
Ok(IpcServer { server })
|
||
}
|
||
|
||
/// Return the path to the IPC server.
|
||
#[napi]
|
||
pub fn get_path(&self) -> String {
|
||
self.server.path.to_string_lossy().to_string()
|
||
}
|
||
|
||
/// Stop the IPC server.
|
||
#[napi]
|
||
pub fn stop(&self) -> napi::Result<()> {
|
||
self.server.stop();
|
||
Ok(())
|
||
}
|
||
|
||
/// Send a message over the IPC server to all the connected clients
|
||
///
|
||
/// @return The number of clients that the message was sent to. Note that the number of
|
||
/// messages actually received may be less, as some clients could disconnect before
|
||
/// receiving the message.
|
||
#[napi]
|
||
pub fn send(&self, message: String) -> napi::Result<u32> {
|
||
self.server
|
||
.send(message)
|
||
.map_err(|e| {
|
||
napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}"))
|
||
})
|
||
// NAPI doesn't support u64 or usize, so we need to convert to u32
|
||
.map(|u| u32::try_from(u).unwrap_or_default())
|
||
}
|
||
}
|
||
}
|
||
|
||
#[napi]
|
||
pub mod autostart {
|
||
#[napi]
|
||
pub async fn set_autostart(autostart: bool, params: Vec<String>) -> napi::Result<()> {
|
||
desktop_core::autostart::set_autostart(autostart, params)
|
||
.await
|
||
.map_err(|e| napi::Error::from_reason(format!("Error setting autostart - {e} - {e:?}")))
|
||
}
|
||
}
|
||
|
||
#[napi]
|
||
pub mod autofill {
|
||
use desktop_core::ipc::server::{Message, MessageType};
|
||
use napi::threadsafe_function::{
|
||
ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode,
|
||
};
|
||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||
use tracing::error;
|
||
|
||
#[napi]
|
||
pub async fn run_command(value: String) -> napi::Result<String> {
|
||
desktop_core::autofill::run_command(value)
|
||
.await
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
|
||
#[derive(Debug, serde::Serialize, serde:: Deserialize)]
|
||
pub enum BitwardenError {
|
||
Internal(String),
|
||
}
|
||
|
||
#[napi(string_enum)]
|
||
#[derive(Debug, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub enum UserVerification {
|
||
#[napi(value = "preferred")]
|
||
Preferred,
|
||
#[napi(value = "required")]
|
||
Required,
|
||
#[napi(value = "discouraged")]
|
||
Discouraged,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize)]
|
||
#[serde(bound = "T: Serialize + DeserializeOwned")]
|
||
pub struct PasskeyMessage<T: Serialize + DeserializeOwned> {
|
||
pub sequence_number: u32,
|
||
pub value: Result<T, BitwardenError>,
|
||
}
|
||
|
||
#[napi(object)]
|
||
#[derive(Debug, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct Position {
|
||
pub x: i32,
|
||
pub y: i32,
|
||
}
|
||
|
||
#[napi(object)]
|
||
#[derive(Debug, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct PasskeyRegistrationRequest {
|
||
pub rp_id: String,
|
||
pub user_name: String,
|
||
pub user_handle: Vec<u8>,
|
||
pub client_data_hash: Vec<u8>,
|
||
pub user_verification: UserVerification,
|
||
pub supported_algorithms: Vec<i32>,
|
||
pub window_xy: Position,
|
||
pub excluded_credentials: Vec<Vec<u8>>,
|
||
}
|
||
|
||
#[napi(object)]
|
||
#[derive(Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct PasskeyRegistrationResponse {
|
||
pub rp_id: String,
|
||
pub client_data_hash: Vec<u8>,
|
||
pub credential_id: Vec<u8>,
|
||
pub attestation_object: Vec<u8>,
|
||
}
|
||
|
||
#[napi(object)]
|
||
#[derive(Debug, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct PasskeyAssertionRequest {
|
||
pub rp_id: String,
|
||
pub client_data_hash: Vec<u8>,
|
||
pub user_verification: UserVerification,
|
||
pub allowed_credentials: Vec<Vec<u8>>,
|
||
pub window_xy: Position,
|
||
//extension_input: Vec<u8>, TODO: Implement support for extensions
|
||
}
|
||
|
||
#[napi(object)]
|
||
#[derive(Debug, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct PasskeyAssertionWithoutUserInterfaceRequest {
|
||
pub rp_id: String,
|
||
pub credential_id: Vec<u8>,
|
||
pub user_name: String,
|
||
pub user_handle: Vec<u8>,
|
||
pub record_identifier: Option<String>,
|
||
pub client_data_hash: Vec<u8>,
|
||
pub user_verification: UserVerification,
|
||
pub window_xy: Position,
|
||
}
|
||
|
||
#[napi(object)]
|
||
#[derive(Debug, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct NativeStatus {
|
||
pub key: String,
|
||
pub value: String,
|
||
}
|
||
|
||
#[napi(object)]
|
||
#[derive(Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct PasskeyAssertionResponse {
|
||
pub rp_id: String,
|
||
pub user_handle: Vec<u8>,
|
||
pub signature: Vec<u8>,
|
||
pub client_data_hash: Vec<u8>,
|
||
pub authenticator_data: Vec<u8>,
|
||
pub credential_id: Vec<u8>,
|
||
}
|
||
|
||
#[napi]
|
||
pub struct IpcServer {
|
||
server: desktop_core::ipc::server::Server,
|
||
}
|
||
|
||
// FIXME: Remove unwraps! They panic and terminate the whole application.
|
||
#[allow(clippy::unwrap_used)]
|
||
#[napi]
|
||
impl IpcServer {
|
||
/// Create and start the IPC server without blocking.
|
||
///
|
||
/// @param name The endpoint name to listen on. This name uniquely identifies the IPC
|
||
/// connection and must be the same for both the server and client. @param callback
|
||
/// This function will be called whenever a message is received from a client.
|
||
#[allow(clippy::unused_async)] // FIXME: Remove unused async!
|
||
#[napi(factory)]
|
||
pub async fn listen(
|
||
name: String,
|
||
// Ideally we'd have a single callback that has an enum containing the request values,
|
||
// but NAPI doesn't support that just yet
|
||
#[napi(
|
||
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void"
|
||
)]
|
||
registration_callback: ThreadsafeFunction<
|
||
(u32, u32, PasskeyRegistrationRequest),
|
||
ErrorStrategy::CalleeHandled,
|
||
>,
|
||
#[napi(
|
||
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void"
|
||
)]
|
||
assertion_callback: ThreadsafeFunction<
|
||
(u32, u32, PasskeyAssertionRequest),
|
||
ErrorStrategy::CalleeHandled,
|
||
>,
|
||
#[napi(
|
||
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void"
|
||
)]
|
||
assertion_without_user_interface_callback: ThreadsafeFunction<
|
||
(u32, u32, PasskeyAssertionWithoutUserInterfaceRequest),
|
||
ErrorStrategy::CalleeHandled,
|
||
>,
|
||
#[napi(
|
||
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void"
|
||
)]
|
||
native_status_callback: ThreadsafeFunction<
|
||
(u32, u32, NativeStatus),
|
||
ErrorStrategy::CalleeHandled,
|
||
>,
|
||
) -> napi::Result<Self> {
|
||
let (send, mut recv) = tokio::sync::mpsc::channel::<Message>(32);
|
||
tokio::spawn(async move {
|
||
while let Some(Message {
|
||
client_id,
|
||
kind,
|
||
message,
|
||
}) = recv.recv().await
|
||
{
|
||
match kind {
|
||
// TODO: We're ignoring the connection and disconnection messages for now
|
||
MessageType::Connected | MessageType::Disconnected => continue,
|
||
MessageType::Message => {
|
||
let Some(message) = message else {
|
||
error!("Message is empty");
|
||
continue;
|
||
};
|
||
|
||
match serde_json::from_str::<PasskeyMessage<PasskeyAssertionRequest>>(
|
||
&message,
|
||
) {
|
||
Ok(msg) => {
|
||
let value = msg
|
||
.value
|
||
.map(|value| (client_id, msg.sequence_number, value))
|
||
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
|
||
|
||
assertion_callback
|
||
.call(value, ThreadsafeFunctionCallMode::NonBlocking);
|
||
continue;
|
||
}
|
||
Err(e) => {
|
||
error!(error = %e, "Error deserializing message1");
|
||
}
|
||
}
|
||
|
||
match serde_json::from_str::<
|
||
PasskeyMessage<PasskeyAssertionWithoutUserInterfaceRequest>,
|
||
>(&message)
|
||
{
|
||
Ok(msg) => {
|
||
let value = msg
|
||
.value
|
||
.map(|value| (client_id, msg.sequence_number, value))
|
||
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
|
||
|
||
assertion_without_user_interface_callback
|
||
.call(value, ThreadsafeFunctionCallMode::NonBlocking);
|
||
continue;
|
||
}
|
||
Err(e) => {
|
||
error!(error = %e, "Error deserializing message1");
|
||
}
|
||
}
|
||
|
||
match serde_json::from_str::<PasskeyMessage<PasskeyRegistrationRequest>>(
|
||
&message,
|
||
) {
|
||
Ok(msg) => {
|
||
let value = msg
|
||
.value
|
||
.map(|value| (client_id, msg.sequence_number, value))
|
||
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
|
||
registration_callback
|
||
.call(value, ThreadsafeFunctionCallMode::NonBlocking);
|
||
continue;
|
||
}
|
||
Err(e) => {
|
||
error!(error = %e, "Error deserializing message2");
|
||
}
|
||
}
|
||
|
||
match serde_json::from_str::<PasskeyMessage<NativeStatus>>(&message) {
|
||
Ok(msg) => {
|
||
let value = msg
|
||
.value
|
||
.map(|value| (client_id, msg.sequence_number, value))
|
||
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
|
||
native_status_callback
|
||
.call(value, ThreadsafeFunctionCallMode::NonBlocking);
|
||
continue;
|
||
}
|
||
Err(error) => {
|
||
error!(%error, "Unable to deserialze native status.");
|
||
}
|
||
}
|
||
|
||
error!(message, "Received an unknown message2");
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
let path = desktop_core::ipc::path(&name);
|
||
|
||
let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| {
|
||
napi::Error::from_reason(format!(
|
||
"Error listening to server - Path: {path:?} - Error: {e} - {e:?}"
|
||
))
|
||
})?;
|
||
|
||
Ok(IpcServer { server })
|
||
}
|
||
|
||
/// Return the path to the IPC server.
|
||
#[napi]
|
||
pub fn get_path(&self) -> String {
|
||
self.server.path.to_string_lossy().to_string()
|
||
}
|
||
|
||
/// Stop the IPC server.
|
||
#[napi]
|
||
pub fn stop(&self) -> napi::Result<()> {
|
||
self.server.stop();
|
||
Ok(())
|
||
}
|
||
|
||
#[napi]
|
||
pub fn complete_registration(
|
||
&self,
|
||
client_id: u32,
|
||
sequence_number: u32,
|
||
response: PasskeyRegistrationResponse,
|
||
) -> napi::Result<u32> {
|
||
let message = PasskeyMessage {
|
||
sequence_number,
|
||
value: Ok(response),
|
||
};
|
||
self.send(client_id, serde_json::to_string(&message).unwrap())
|
||
}
|
||
|
||
#[napi]
|
||
pub fn complete_assertion(
|
||
&self,
|
||
client_id: u32,
|
||
sequence_number: u32,
|
||
response: PasskeyAssertionResponse,
|
||
) -> napi::Result<u32> {
|
||
let message = PasskeyMessage {
|
||
sequence_number,
|
||
value: Ok(response),
|
||
};
|
||
self.send(client_id, serde_json::to_string(&message).unwrap())
|
||
}
|
||
|
||
#[napi]
|
||
pub fn complete_error(
|
||
&self,
|
||
client_id: u32,
|
||
sequence_number: u32,
|
||
error: String,
|
||
) -> napi::Result<u32> {
|
||
let message: PasskeyMessage<()> = PasskeyMessage {
|
||
sequence_number,
|
||
value: Err(BitwardenError::Internal(error)),
|
||
};
|
||
self.send(client_id, serde_json::to_string(&message).unwrap())
|
||
}
|
||
|
||
// TODO: Add a way to send a message to a specific client?
|
||
fn send(&self, _client_id: u32, message: String) -> napi::Result<u32> {
|
||
self.server
|
||
.send(message)
|
||
.map_err(|e| {
|
||
napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}"))
|
||
})
|
||
// NAPI doesn't support u64 or usize, so we need to convert to u32
|
||
.map(|u| u32::try_from(u).unwrap_or_default())
|
||
}
|
||
}
|
||
}
|
||
|
||
#[napi]
|
||
pub mod passkey_authenticator {
|
||
#[napi]
|
||
pub fn register() -> napi::Result<()> {
|
||
crate::passkey_authenticator_internal::register().map_err(|e| {
|
||
napi::Error::from_reason(format!("Passkey registration failed - Error: {e} - {e:?}"))
|
||
})
|
||
}
|
||
}
|
||
|
||
#[napi]
|
||
pub mod logging {
|
||
//! `logging` is the interface between the native desktop's usage of the `tracing` crate
|
||
//! for logging, to intercept events and write to the JS space.
|
||
//!
|
||
//! # Example
|
||
//!
|
||
//! [Elec] 14:34:03.517 › [NAPI] [INFO] desktop_core::ssh_agent::platform_ssh_agent: Starting
|
||
//! SSH Agent server {socket=/Users/foo/.bitwarden-ssh-agent.sock}
|
||
|
||
use std::{fmt::Write, sync::OnceLock};
|
||
|
||
use napi::threadsafe_function::{
|
||
ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode,
|
||
};
|
||
use tracing::Level;
|
||
use tracing_subscriber::{
|
||
filter::{EnvFilter, LevelFilter},
|
||
fmt::format::{DefaultVisitor, Writer},
|
||
layer::SubscriberExt,
|
||
util::SubscriberInitExt,
|
||
Layer,
|
||
};
|
||
|
||
struct JsLogger(OnceLock<ThreadsafeFunction<(LogLevel, String), CalleeHandled>>);
|
||
static JS_LOGGER: JsLogger = JsLogger(OnceLock::new());
|
||
|
||
#[napi]
|
||
pub enum LogLevel {
|
||
Trace,
|
||
Debug,
|
||
Info,
|
||
Warn,
|
||
Error,
|
||
}
|
||
|
||
impl From<&Level> for LogLevel {
|
||
fn from(level: &Level) -> Self {
|
||
match *level {
|
||
Level::TRACE => LogLevel::Trace,
|
||
Level::DEBUG => LogLevel::Debug,
|
||
Level::INFO => LogLevel::Info,
|
||
Level::WARN => LogLevel::Warn,
|
||
Level::ERROR => LogLevel::Error,
|
||
}
|
||
}
|
||
}
|
||
|
||
// JsLayer lets us intercept events and write them to the JS Logger.
|
||
struct JsLayer;
|
||
|
||
impl<S> Layer<S> for JsLayer
|
||
where
|
||
S: tracing::Subscriber,
|
||
{
|
||
// This function builds a log message buffer from the event data and
|
||
// calls the JS logger with it.
|
||
//
|
||
// For example, this log call:
|
||
//
|
||
// ```
|
||
// mod supreme {
|
||
// mod module {
|
||
// let foo = "bar";
|
||
// info!(best_variable_name = %foo, "Foo done it again.");
|
||
// }
|
||
// }
|
||
// ```
|
||
//
|
||
// , results in the following string:
|
||
//
|
||
// [INFO] supreme::module: Foo done it again. {best_variable_name=bar}
|
||
fn on_event(
|
||
&self,
|
||
event: &tracing::Event<'_>,
|
||
_ctx: tracing_subscriber::layer::Context<'_, S>,
|
||
) {
|
||
let mut buffer = String::new();
|
||
|
||
// create the preamble text that precedes the message and vars. e.g.:
|
||
// [INFO] desktop_core::ssh_agent::platform_ssh_agent:
|
||
let level = event.metadata().level().as_str();
|
||
let module_path = event.metadata().module_path().unwrap_or_default();
|
||
|
||
write!(&mut buffer, "[{level}] {module_path}:")
|
||
.expect("Failed to write tracing event to buffer");
|
||
|
||
let writer = Writer::new(&mut buffer);
|
||
|
||
// DefaultVisitor adds the message and variables to the buffer
|
||
let mut visitor = DefaultVisitor::new(writer, false);
|
||
event.record(&mut visitor);
|
||
|
||
let msg = (event.metadata().level().into(), buffer);
|
||
|
||
if let Some(logger) = JS_LOGGER.0.get() {
|
||
let _ = logger.call(Ok(msg), ThreadsafeFunctionCallMode::NonBlocking);
|
||
};
|
||
}
|
||
}
|
||
|
||
#[napi]
|
||
pub fn init_napi_log(js_log_fn: ThreadsafeFunction<(LogLevel, String), CalleeHandled>) {
|
||
let _ = JS_LOGGER.0.set(js_log_fn);
|
||
|
||
let filter = EnvFilter::builder()
|
||
// set the default log level to INFO.
|
||
.with_default_directive(LevelFilter::INFO.into())
|
||
// parse directives from the RUST_LOG environment variable,
|
||
// overriding the default directive for matching targets.
|
||
.from_env_lossy();
|
||
|
||
// With the `tracing-log` feature enabled for the `tracing_subscriber`,
|
||
// the registry below will initialize a log compatibility layer, which allows
|
||
// the subscriber to consume log::Records as though they were tracing Events.
|
||
// https://docs.rs/tracing-subscriber/latest/tracing_subscriber/util/trait.SubscriberInitExt.html#method.init
|
||
tracing_subscriber::registry()
|
||
.with(filter)
|
||
.with(JsLayer)
|
||
.init();
|
||
}
|
||
}
|
||
|
||
#[napi]
|
||
pub mod chromium_importer {
|
||
use std::collections::HashMap;
|
||
|
||
use chromium_importer::{
|
||
chromium::{
|
||
DefaultInstalledBrowserRetriever, LoginImportResult as _LoginImportResult,
|
||
ProfileInfo as _ProfileInfo,
|
||
},
|
||
metadata::NativeImporterMetadata as _NativeImporterMetadata,
|
||
};
|
||
|
||
#[napi(object)]
|
||
pub struct ProfileInfo {
|
||
pub id: String,
|
||
pub name: String,
|
||
}
|
||
|
||
#[napi(object)]
|
||
pub struct Login {
|
||
pub url: String,
|
||
pub username: String,
|
||
pub password: String,
|
||
pub note: String,
|
||
}
|
||
|
||
#[napi(object)]
|
||
pub struct LoginImportFailure {
|
||
pub url: String,
|
||
pub username: String,
|
||
pub error: String,
|
||
}
|
||
|
||
#[napi(object)]
|
||
pub struct LoginImportResult {
|
||
pub login: Option<Login>,
|
||
pub failure: Option<LoginImportFailure>,
|
||
}
|
||
|
||
#[napi(object)]
|
||
pub struct NativeImporterMetadata {
|
||
pub id: String,
|
||
pub loaders: Vec<&'static str>,
|
||
pub instructions: &'static str,
|
||
}
|
||
|
||
impl From<_LoginImportResult> for LoginImportResult {
|
||
fn from(l: _LoginImportResult) -> Self {
|
||
match l {
|
||
_LoginImportResult::Success(l) => LoginImportResult {
|
||
login: Some(Login {
|
||
url: l.url,
|
||
username: l.username,
|
||
password: l.password,
|
||
note: l.note,
|
||
}),
|
||
failure: None,
|
||
},
|
||
_LoginImportResult::Failure(l) => LoginImportResult {
|
||
login: None,
|
||
failure: Some(LoginImportFailure {
|
||
url: l.url,
|
||
username: l.username,
|
||
error: l.error,
|
||
}),
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<_ProfileInfo> for ProfileInfo {
|
||
fn from(p: _ProfileInfo) -> Self {
|
||
ProfileInfo {
|
||
id: p.folder,
|
||
name: p.name,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<_NativeImporterMetadata> for NativeImporterMetadata {
|
||
fn from(m: _NativeImporterMetadata) -> Self {
|
||
NativeImporterMetadata {
|
||
id: m.id,
|
||
loaders: m.loaders,
|
||
instructions: m.instructions,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[napi]
|
||
/// Returns OS aware metadata describing supported Chromium based importers as a JSON string.
|
||
pub fn get_metadata() -> HashMap<String, NativeImporterMetadata> {
|
||
chromium_importer::metadata::get_supported_importers::<DefaultInstalledBrowserRetriever>()
|
||
.into_iter()
|
||
.map(|(browser, metadata)| (browser, NativeImporterMetadata::from(metadata)))
|
||
.collect()
|
||
}
|
||
|
||
#[napi]
|
||
pub fn get_available_profiles(browser: String) -> napi::Result<Vec<ProfileInfo>> {
|
||
chromium_importer::chromium::get_available_profiles(&browser)
|
||
.map(|profiles| profiles.into_iter().map(ProfileInfo::from).collect())
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
|
||
#[napi]
|
||
pub async fn import_logins(
|
||
browser: String,
|
||
profile_id: String,
|
||
) -> napi::Result<Vec<LoginImportResult>> {
|
||
chromium_importer::chromium::import_logins(&browser, &profile_id)
|
||
.await
|
||
.map(|logins| logins.into_iter().map(LoginImportResult::from).collect())
|
||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||
}
|
||
}
|
||
|
||
#[napi]
|
||
pub mod autotype {
|
||
#[napi]
|
||
pub fn get_foreground_window_title() -> napi::Result<String, napi::Status> {
|
||
autotype::get_foreground_window_title().map_err(|_| {
|
||
napi::Error::from_reason(
|
||
"Autotype Error: failed to get foreground window title".to_string(),
|
||
)
|
||
})
|
||
}
|
||
|
||
#[napi]
|
||
pub fn type_input(
|
||
input: Vec<u16>,
|
||
keyboard_shortcut: Vec<String>,
|
||
) -> napi::Result<(), napi::Status> {
|
||
autotype::type_input(input, keyboard_shortcut).map_err(|_| {
|
||
napi::Error::from_reason("Autotype Error: failed to type input".to_string())
|
||
})
|
||
}
|
||
}
|