1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-31 07:33:23 +00:00
Files
browser/apps/desktop/desktop_native/napi/src/lib.rs
Anders Åberg 7cba6f4170 PM-8353 MacOS passkey provider (#13963)
* 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>
2025-12-05 12:58:20 -05:00

1239 lines
42 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#[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())
})
}
}