mirror of
https://github.com/bitwarden/browser
synced 2026-01-03 09:03:32 +00:00
[PM-25373] Windows native biometric rewrite (#16432)
* Extract windows biometrics v2 changes Co-authored-by: Bernd Schoolmann <mail@quexten.com> * Handle TDE edge cases * Make windows rust code async and fix restoring focus freezes * Add unit test coverage --------- Co-authored-by: Bernd Schoolmann <mail@quexten.com>
This commit is contained in:
16
apps/desktop/desktop_native/Cargo.lock
generated
16
apps/desktop/desktop_native/Cargo.lock
generated
@@ -591,6 +591,19 @@ dependencies = [
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chacha20poly1305"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"chacha20",
|
||||
"cipher",
|
||||
"poly1305",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
@@ -904,6 +917,7 @@ dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"cbc",
|
||||
"chacha20poly1305",
|
||||
"core-foundation",
|
||||
"desktop_objc",
|
||||
"dirs",
|
||||
@@ -923,6 +937,8 @@ dependencies = [
|
||||
"secmem-proc",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"ssh-encoding",
|
||||
"ssh-key",
|
||||
|
||||
@@ -27,6 +27,7 @@ bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", re
|
||||
byteorder = "=1.5.0"
|
||||
bytes = "=1.10.1"
|
||||
cbc = "=0.1.2"
|
||||
chacha20poly1305 = "=0.10.1"
|
||||
core-foundation = "=0.10.1"
|
||||
ctor = "=0.5.0"
|
||||
dirs = "=6.0.0"
|
||||
|
||||
@@ -26,6 +26,7 @@ bitwarden-russh = { workspace = true }
|
||||
byteorder = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
cbc = { workspace = true, features = ["alloc"] }
|
||||
chacha20poly1305 = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
ed25519 = { workspace = true, features = ["pkcs8"] }
|
||||
futures = { workspace = true }
|
||||
@@ -38,6 +39,8 @@ rsa = { workspace = true }
|
||||
russh-cryptovec = { workspace = true }
|
||||
scopeguard = { workspace = true }
|
||||
secmem-proc = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
ssh-encoding = { workspace = true }
|
||||
ssh-key = { workspace = true, features = [
|
||||
@@ -64,6 +67,7 @@ windows = { workspace = true, features = [
|
||||
"Storage_Streams",
|
||||
"Win32_Foundation",
|
||||
"Win32_Security_Credentials",
|
||||
"Win32_Security_Cryptography",
|
||||
"Win32_System_WinRT",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
|
||||
33
apps/desktop/desktop_native/core/src/biometric_v2/mod.rs
Normal file
33
apps/desktop/desktop_native/core/src/biometric_v2/mod.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use anyhow::Result;
|
||||
|
||||
#[allow(clippy::module_inception)]
|
||||
#[cfg_attr(target_os = "linux", path = "unimplemented.rs")]
|
||||
#[cfg_attr(target_os = "macos", path = "unimplemented.rs")]
|
||||
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||
mod biometric_v2;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod windows_focus;
|
||||
|
||||
pub use biometric_v2::BiometricLockSystem;
|
||||
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait BiometricTrait: Send + Sync {
|
||||
/// Authenticate the user
|
||||
async fn authenticate(&self, hwnd: Vec<u8>, message: String) -> Result<bool>;
|
||||
/// Check if biometric authentication is available
|
||||
async fn authenticate_available(&self) -> Result<bool>;
|
||||
/// Enroll a key for persistent unlock. If the implementation does not support persistent enrollment,
|
||||
/// this function should do nothing.
|
||||
async fn enroll_persistent(&self, user_id: &str, key: &[u8]) -> Result<()>;
|
||||
/// Clear the persistent and ephemeral keys
|
||||
async fn unenroll(&self, user_id: &str) -> Result<()>;
|
||||
/// Check if a persistent (survives app restarts and reboots) key is set for a user
|
||||
async fn has_persistent(&self, user_id: &str) -> Result<bool>;
|
||||
/// Provide a key to be ephemerally held. This should be called on every unlock.
|
||||
async fn provide_key(&self, user_id: &str, key: &[u8]);
|
||||
/// Perform biometric unlock and return the key
|
||||
async fn unlock(&self, user_id: &str, hwnd: Vec<u8>) -> Result<Vec<u8>>;
|
||||
/// Check if biometric unlock is available based on whether a key is present and whether authentication is possible
|
||||
async fn unlock_available(&self, user_id: &str) -> Result<bool>;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
pub struct BiometricLockSystem {}
|
||||
|
||||
impl BiometricLockSystem {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BiometricLockSystem {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl super::BiometricTrait for BiometricLockSystem {
|
||||
async fn authenticate(&self, _hwnd: Vec<u8>, _message: String) -> Result<bool, anyhow::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn authenticate_available(&self) -> Result<bool, anyhow::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn enroll_persistent(&self, _user_id: &str, _key: &[u8]) -> Result<(), anyhow::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn provide_key(&self, _user_id: &str, _key: &[u8]) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn unlock(&self, _user_id: &str, _hwnd: Vec<u8>) -> Result<Vec<u8>, anyhow::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn unlock_available(&self, _user_id: &str) -> Result<bool, anyhow::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn has_persistent(&self, _user_id: &str) -> Result<bool, anyhow::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn unenroll(&self, _user_id: &str) -> Result<(), anyhow::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
505
apps/desktop/desktop_native/core/src/biometric_v2/windows.rs
Normal file
505
apps/desktop/desktop_native/core/src/biometric_v2/windows.rs
Normal file
@@ -0,0 +1,505 @@
|
||||
//! This file implements Windows-Hello based biometric unlock.
|
||||
//!
|
||||
//! There are two paths implemented here.
|
||||
//! The former via UV + ephemerally (but protected) keys. This only works after first unlock.
|
||||
//! The latter via a signing API, that deterministically signs a challenge, from which a windows hello key is derived. This key
|
||||
//! is used to encrypt the protected key.
|
||||
//!
|
||||
//! # Security
|
||||
//! The security goal is that a locked vault - a running app - cannot be unlocked when the device (user-space)
|
||||
//! is compromised in this state.
|
||||
//!
|
||||
//! ## UV path
|
||||
//! When first unlocking the app, the app sends the user-key to this module, which holds it in secure memory,
|
||||
//! protected by DPAPI. This makes it inaccessible to other processes, unless they compromise the system administrator, or kernel.
|
||||
//! While the app is running this key is held in memory, even if locked. When unlocking, the app will prompt the user via
|
||||
//! `windows_hello_authenticate` to get a yes/no decision on whether to release the key to the app.
|
||||
//! Note: Further process isolation is needed here so that code cannot be injected into the running process, which may
|
||||
//! circumvent DPAPI.
|
||||
//!
|
||||
//! ## Sign path
|
||||
//! In this scenario, when enrolling, the app sends the user-key to this module, which derives the windows hello key
|
||||
//! with the Windows Hello prompt. This is done by signing a per-user challenge, which produces a deterministic
|
||||
//! signature which is hashed to obtain a key. This key is used to encrypt and persist the vault unlock key (user key).
|
||||
//!
|
||||
//! Since the keychain can be accessed by all user-space processes, the challenge is known to all userspace processes.
|
||||
//! Therefore, to circumvent the security measure, the attacker would need to create a fake Windows-Hello prompt, and
|
||||
//! get the user to confirm it.
|
||||
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use aes::cipher::KeyInit;
|
||||
use anyhow::{anyhow, Result};
|
||||
use chacha20poly1305::{aead::Aead, XChaCha20Poly1305, XNonce};
|
||||
use sha2::{Digest, Sha256};
|
||||
use tokio::sync::Mutex;
|
||||
use windows::{
|
||||
core::{factory, h, Interface, HSTRING},
|
||||
Security::{
|
||||
Credentials::{
|
||||
KeyCredentialCreationOption, KeyCredentialManager, KeyCredentialStatus,
|
||||
UI::{
|
||||
UserConsentVerificationResult, UserConsentVerifier, UserConsentVerifierAvailability,
|
||||
},
|
||||
},
|
||||
Cryptography::CryptographicBuffer,
|
||||
},
|
||||
Storage::Streams::IBuffer,
|
||||
Win32::{
|
||||
System::WinRT::{IBufferByteAccess, IUserConsentVerifierInterop},
|
||||
UI::WindowsAndMessaging::GetForegroundWindow,
|
||||
},
|
||||
};
|
||||
use windows_future::IAsyncOperation;
|
||||
|
||||
use super::windows_focus::{focus_security_prompt, restore_focus};
|
||||
use crate::{
|
||||
password::{self, PASSWORD_NOT_FOUND},
|
||||
secure_memory::*,
|
||||
};
|
||||
|
||||
const KEYCHAIN_SERVICE_NAME: &str = "BitwardenBiometricsV2";
|
||||
const CREDENTIAL_NAME: &HSTRING = h!("BitwardenBiometricsV2");
|
||||
const CHALLENGE_LENGTH: usize = 16;
|
||||
const XCHACHA20POLY1305_NONCE_LENGTH: usize = 24;
|
||||
const XCHACHA20POLY1305_KEY_LENGTH: usize = 32;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct WindowsHelloKeychainEntry {
|
||||
nonce: [u8; XCHACHA20POLY1305_NONCE_LENGTH],
|
||||
challenge: [u8; CHALLENGE_LENGTH],
|
||||
wrapped_key: Vec<u8>,
|
||||
}
|
||||
|
||||
/// The Windows OS implementation of the biometric trait.
|
||||
pub struct BiometricLockSystem {
|
||||
// The userkeys that are held in memory MUST be protected from memory dumping attacks, to ensure
|
||||
// locked vaults cannot be unlocked
|
||||
secure_memory: Arc<Mutex<crate::secure_memory::dpapi::DpapiSecretKVStore>>,
|
||||
}
|
||||
|
||||
impl BiometricLockSystem {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
secure_memory: Arc::new(Mutex::new(
|
||||
crate::secure_memory::dpapi::DpapiSecretKVStore::new(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BiometricLockSystem {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl super::BiometricTrait for BiometricLockSystem {
|
||||
async fn authenticate(&self, _hwnd: Vec<u8>, message: String) -> Result<bool> {
|
||||
windows_hello_authenticate(message).await
|
||||
}
|
||||
|
||||
async fn authenticate_available(&self) -> Result<bool> {
|
||||
match UserConsentVerifier::CheckAvailabilityAsync()?.await? {
|
||||
UserConsentVerifierAvailability::Available
|
||||
| UserConsentVerifierAvailability::DeviceBusy => Ok(true),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
async fn unenroll(&self, user_id: &str) -> Result<()> {
|
||||
self.secure_memory.lock().await.remove(user_id);
|
||||
delete_keychain_entry(user_id).await
|
||||
}
|
||||
|
||||
async fn enroll_persistent(&self, user_id: &str, key: &[u8]) -> Result<()> {
|
||||
// Enrollment works by first generating a random challenge unique to the user / enrollment. Then,
|
||||
// with the challenge and a Windows-Hello prompt, the "windows hello key" is derived. The windows
|
||||
// hello key is used to encrypt the key to store with XChaCha20Poly1305. The bundle of nonce,
|
||||
// challenge and wrapped-key are stored to the keychain
|
||||
|
||||
// Each enrollment (per user) has a unique challenge, so that the windows-hello key is unique
|
||||
let challenge: [u8; CHALLENGE_LENGTH] = rand::random();
|
||||
|
||||
// This key is unique to the challenge
|
||||
let windows_hello_key = windows_hello_authenticate_with_crypto(&challenge).await?;
|
||||
let (wrapped_key, nonce) = encrypt_data(&windows_hello_key, key)?;
|
||||
|
||||
set_keychain_entry(
|
||||
user_id,
|
||||
&WindowsHelloKeychainEntry {
|
||||
nonce,
|
||||
challenge,
|
||||
wrapped_key,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn provide_key(&self, user_id: &str, key: &[u8]) {
|
||||
self.secure_memory
|
||||
.lock()
|
||||
.await
|
||||
.put(user_id.to_string(), key);
|
||||
}
|
||||
|
||||
async fn unlock(&self, user_id: &str, _hwnd: Vec<u8>) -> Result<Vec<u8>> {
|
||||
// Allow restoring focus to the previous window (browser)
|
||||
let previous_active_window = super::windows_focus::get_active_window();
|
||||
let _focus_scopeguard = scopeguard::guard((), |_| {
|
||||
if let Some(hwnd) = previous_active_window {
|
||||
debug!("Restoring focus to previous window");
|
||||
restore_focus(hwnd.0);
|
||||
}
|
||||
});
|
||||
|
||||
let mut secure_memory = self.secure_memory.lock().await;
|
||||
// If the key is held ephemerally, always use UV API. Only use signing API if the key is not held
|
||||
// ephemerally but the keychain holds it persistently.
|
||||
if secure_memory.has(user_id) {
|
||||
if windows_hello_authenticate("Unlock your vault".to_string()).await? {
|
||||
secure_memory
|
||||
.get(user_id)
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("No key found for user"))
|
||||
} else {
|
||||
Err(anyhow!("Authentication failed"))
|
||||
}
|
||||
} else {
|
||||
let keychain_entry = get_keychain_entry(user_id).await?;
|
||||
let windows_hello_key =
|
||||
windows_hello_authenticate_with_crypto(&keychain_entry.challenge).await?;
|
||||
let decrypted_key = decrypt_data(
|
||||
&windows_hello_key,
|
||||
&keychain_entry.wrapped_key,
|
||||
&keychain_entry.nonce,
|
||||
)?;
|
||||
// The first unlock already sets the key for subsequent unlocks. The key may again be set externally after unlock finishes.
|
||||
secure_memory.put(user_id.to_string(), &decrypted_key.clone());
|
||||
Ok(decrypted_key)
|
||||
}
|
||||
}
|
||||
|
||||
async fn unlock_available(&self, user_id: &str) -> Result<bool> {
|
||||
let secure_memory = self.secure_memory.lock().await;
|
||||
let has_key =
|
||||
secure_memory.has(user_id) || has_keychain_entry(user_id).await.unwrap_or(false);
|
||||
Ok(has_key && self.authenticate_available().await.unwrap_or(false))
|
||||
}
|
||||
|
||||
async fn has_persistent(&self, user_id: &str) -> Result<bool> {
|
||||
Ok(get_keychain_entry(user_id).await.is_ok())
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a yes/no authorization without any cryptographic backing.
|
||||
/// This API has better focusing behavior
|
||||
async fn windows_hello_authenticate(message: String) -> Result<bool> {
|
||||
debug!(
|
||||
"[Windows Hello] Authenticating to perform UV with message: {}",
|
||||
message
|
||||
);
|
||||
|
||||
let userconsent_result: IAsyncOperation<UserConsentVerificationResult> = unsafe {
|
||||
// Windows Hello prompt must be in foreground, focused, otherwise the face or fingerprint
|
||||
// unlock will not work. We get the current foreground window, which will either be the
|
||||
// Bitwarden desktop app or the browser extension.
|
||||
let foreground_window = GetForegroundWindow();
|
||||
factory::<UserConsentVerifier, IUserConsentVerifierInterop>()?
|
||||
.RequestVerificationForWindowAsync(foreground_window, &HSTRING::from(message))?
|
||||
};
|
||||
|
||||
match userconsent_result.await? {
|
||||
UserConsentVerificationResult::Verified => Ok(true),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive the symmetric encryption key from the Windows Hello signature.
|
||||
///
|
||||
/// This works by signing a static challenge string with Windows Hello protected key store. The
|
||||
/// signed challenge is then hashed using SHA-256 and used as the symmetric encryption key for the
|
||||
/// Windows Hello protected keys.
|
||||
///
|
||||
/// Windows will only sign the challenge if the user has successfully authenticated with Windows,
|
||||
/// ensuring user presence.
|
||||
///
|
||||
/// Note: This API has inconsistent focusing behavior when called from another window
|
||||
async fn windows_hello_authenticate_with_crypto(
|
||||
challenge: &[u8; CHALLENGE_LENGTH],
|
||||
) -> Result<[u8; XCHACHA20POLY1305_KEY_LENGTH]> {
|
||||
debug!("[Windows Hello] Authenticating to sign challenge");
|
||||
|
||||
// Ugly hack: We need to focus the window via window focusing APIs until Microsoft releases a new API.
|
||||
// This is unreliable, and if it does not work, the operation may fail
|
||||
let stop_focusing = Arc::new(AtomicBool::new(false));
|
||||
let stop_focusing_clone = stop_focusing.clone();
|
||||
let _ = std::thread::spawn(move || loop {
|
||||
if !stop_focusing_clone.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
focus_security_prompt();
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
});
|
||||
// Only stop focusing once this function exits. The focus MUST run both during the initial creation
|
||||
// with RequestCreateAsync, and also with the subsequent use with RequestSignAsync.
|
||||
let _guard = scopeguard::guard((), |_| {
|
||||
stop_focusing.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
});
|
||||
|
||||
// First create or replace the Bitwarden Biometrics signing key
|
||||
let credential = {
|
||||
let key_credential_creation_result = KeyCredentialManager::RequestCreateAsync(
|
||||
CREDENTIAL_NAME,
|
||||
KeyCredentialCreationOption::FailIfExists,
|
||||
)?
|
||||
.await?;
|
||||
match key_credential_creation_result.Status()? {
|
||||
KeyCredentialStatus::CredentialAlreadyExists => {
|
||||
KeyCredentialManager::OpenAsync(CREDENTIAL_NAME)?.await?
|
||||
}
|
||||
KeyCredentialStatus::Success => key_credential_creation_result,
|
||||
_ => return Err(anyhow!("Failed to create key credential")),
|
||||
}
|
||||
}
|
||||
.Credential()?;
|
||||
|
||||
let signature = {
|
||||
let sign_operation = credential.RequestSignAsync(
|
||||
&CryptographicBuffer::CreateFromByteArray(challenge.as_slice())?,
|
||||
)?;
|
||||
|
||||
// We need to drop the credential here to avoid holding it across an await point.
|
||||
drop(credential);
|
||||
sign_operation.await?
|
||||
};
|
||||
|
||||
if signature.Status()? != KeyCredentialStatus::Success {
|
||||
return Err(anyhow!("Failed to sign data"));
|
||||
}
|
||||
|
||||
let signature_buffer = signature.Result()?;
|
||||
let signature_value = unsafe { as_mut_bytes(&signature_buffer)? };
|
||||
|
||||
// The signature is deterministic based on the challenge and keychain key. Thus, it can be hashed to a key.
|
||||
// It is unclear what entropy this key provides.
|
||||
let windows_hello_key = Sha256::digest(signature_value).into();
|
||||
Ok(windows_hello_key)
|
||||
}
|
||||
|
||||
async fn set_keychain_entry(user_id: &str, entry: &WindowsHelloKeychainEntry) -> Result<()> {
|
||||
password::set_password(
|
||||
KEYCHAIN_SERVICE_NAME,
|
||||
user_id,
|
||||
&serde_json::to_string(entry)?,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_keychain_entry(user_id: &str) -> Result<WindowsHelloKeychainEntry> {
|
||||
serde_json::from_str(&password::get_password(KEYCHAIN_SERVICE_NAME, user_id).await?)
|
||||
.map_err(|e| anyhow!(e))
|
||||
}
|
||||
|
||||
async fn delete_keychain_entry(user_id: &str) -> Result<()> {
|
||||
password::delete_password(KEYCHAIN_SERVICE_NAME, user_id)
|
||||
.await
|
||||
.or_else(|e| {
|
||||
if e.to_string() == PASSWORD_NOT_FOUND {
|
||||
debug!(
|
||||
"[Windows Hello] No keychain entry found for user {}, nothing to delete",
|
||||
user_id
|
||||
);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn has_keychain_entry(user_id: &str) -> Result<bool> {
|
||||
password::get_password(KEYCHAIN_SERVICE_NAME, user_id)
|
||||
.await
|
||||
.map(|entry| !entry.is_empty())
|
||||
.or_else(|e| {
|
||||
if e.to_string() == PASSWORD_NOT_FOUND {
|
||||
Ok(false)
|
||||
} else {
|
||||
warn!(
|
||||
"[Windows Hello] Error checking keychain entry for user {}: {}",
|
||||
user_id, e
|
||||
);
|
||||
Err(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Encrypt data with XChaCha20Poly1305
|
||||
fn encrypt_data(
|
||||
key: &[u8; XCHACHA20POLY1305_KEY_LENGTH],
|
||||
plaintext: &[u8],
|
||||
) -> Result<(Vec<u8>, [u8; XCHACHA20POLY1305_NONCE_LENGTH])> {
|
||||
let cipher = XChaCha20Poly1305::new(key.into());
|
||||
let mut nonce = [0u8; XCHACHA20POLY1305_NONCE_LENGTH];
|
||||
rand::fill(&mut nonce);
|
||||
let ciphertext = cipher
|
||||
.encrypt(XNonce::from_slice(&nonce), plaintext)
|
||||
.map_err(|e| anyhow!(e))?;
|
||||
Ok((ciphertext, nonce))
|
||||
}
|
||||
|
||||
/// Decrypt data with XChaCha20Poly1305
|
||||
fn decrypt_data(
|
||||
key: &[u8; XCHACHA20POLY1305_KEY_LENGTH],
|
||||
ciphertext: &[u8],
|
||||
nonce: &[u8; XCHACHA20POLY1305_NONCE_LENGTH],
|
||||
) -> Result<Vec<u8>> {
|
||||
let cipher = XChaCha20Poly1305::new(key.into());
|
||||
let plaintext = cipher
|
||||
.decrypt(XNonce::from_slice(nonce), ciphertext)
|
||||
.map_err(|e| anyhow!(e))?;
|
||||
Ok(plaintext)
|
||||
}
|
||||
|
||||
unsafe fn as_mut_bytes(buffer: &IBuffer) -> Result<&mut [u8]> {
|
||||
let interop = buffer.cast::<IBufferByteAccess>()?;
|
||||
|
||||
unsafe {
|
||||
let data = interop.Buffer()?;
|
||||
Ok(std::slice::from_raw_parts_mut(
|
||||
data,
|
||||
buffer.Length()? as usize,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::biometric_v2::{
|
||||
biometric_v2::{
|
||||
decrypt_data, encrypt_data, has_keychain_entry, windows_hello_authenticate,
|
||||
windows_hello_authenticate_with_crypto, CHALLENGE_LENGTH, XCHACHA20POLY1305_KEY_LENGTH,
|
||||
},
|
||||
BiometricLockSystem, BiometricTrait,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt() {
|
||||
let key = [0u8; 32];
|
||||
let plaintext = b"Test data";
|
||||
let (ciphertext, nonce) = encrypt_data(&key, plaintext).unwrap();
|
||||
let decrypted = decrypt_data(&key, &ciphertext, &nonce).unwrap();
|
||||
assert_eq!(plaintext.to_vec(), decrypted);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_has_keychain_entry_no_entry() {
|
||||
let user_id = "test_user";
|
||||
let has_entry = has_keychain_entry(user_id).await.unwrap();
|
||||
assert!(!has_entry);
|
||||
}
|
||||
|
||||
// Note: These tests are ignored because they require manual intervention to run
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_windows_hello_authenticate_with_crypto_manual() {
|
||||
let challenge = [0u8; CHALLENGE_LENGTH];
|
||||
let windows_hello_key = windows_hello_authenticate_with_crypto(&challenge)
|
||||
.await
|
||||
.unwrap();
|
||||
println!(
|
||||
"Windows hello key {:?} for challenge {:?}",
|
||||
windows_hello_key, challenge
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_windows_hello_authenticate() {
|
||||
let authenticated =
|
||||
windows_hello_authenticate("Test Windows Hello authentication".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
println!("Windows Hello authentication result: {:?}", authenticated);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_double_unenroll() {
|
||||
let user_id = "test_user";
|
||||
let mut key = [0u8; XCHACHA20POLY1305_KEY_LENGTH];
|
||||
rand::fill(&mut key);
|
||||
|
||||
let windows_hello_lock_system = BiometricLockSystem::new();
|
||||
|
||||
println!("Enrolling user");
|
||||
windows_hello_lock_system
|
||||
.enroll_persistent(user_id, &key)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(windows_hello_lock_system
|
||||
.has_persistent(user_id)
|
||||
.await
|
||||
.unwrap());
|
||||
|
||||
println!("Unlocking user");
|
||||
let key_after_unlock = windows_hello_lock_system
|
||||
.unlock(user_id, Vec::new())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(key_after_unlock, key);
|
||||
|
||||
println!("Unenrolling user");
|
||||
windows_hello_lock_system.unenroll(user_id).await.unwrap();
|
||||
assert!(!windows_hello_lock_system
|
||||
.has_persistent(user_id)
|
||||
.await
|
||||
.unwrap());
|
||||
|
||||
println!("Unenrolling user again");
|
||||
|
||||
// This throws PASSWORD_NOT_FOUND but our code should handle that and not throw.
|
||||
windows_hello_lock_system.unenroll(user_id).await.unwrap();
|
||||
assert!(!windows_hello_lock_system
|
||||
.has_persistent(user_id)
|
||||
.await
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_enroll_unlock_unenroll() {
|
||||
let user_id = "test_user";
|
||||
let mut key = [0u8; XCHACHA20POLY1305_KEY_LENGTH];
|
||||
rand::fill(&mut key);
|
||||
|
||||
let windows_hello_lock_system = BiometricLockSystem::new();
|
||||
|
||||
println!("Enrolling user");
|
||||
windows_hello_lock_system
|
||||
.enroll_persistent(user_id, &key)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(windows_hello_lock_system
|
||||
.has_persistent(user_id)
|
||||
.await
|
||||
.unwrap());
|
||||
|
||||
println!("Unlocking user");
|
||||
let key_after_unlock = windows_hello_lock_system
|
||||
.unlock(user_id, Vec::new())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(key_after_unlock, key);
|
||||
|
||||
println!("Unenrolling user");
|
||||
windows_hello_lock_system.unenroll(user_id).await.unwrap();
|
||||
assert!(!windows_hello_lock_system
|
||||
.has_persistent(user_id)
|
||||
.await
|
||||
.unwrap());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
use windows::{
|
||||
core::s,
|
||||
Win32::{
|
||||
Foundation::HWND,
|
||||
System::Threading::{AttachThreadInput, GetCurrentThreadId},
|
||||
UI::{
|
||||
Input::KeyboardAndMouse::{EnableWindow, SetActiveWindow, SetCapture, SetFocus},
|
||||
WindowsAndMessaging::{
|
||||
BringWindowToTop, FindWindowA, GetForegroundWindow, GetWindowThreadProcessId,
|
||||
SetForegroundWindow, SwitchToThisWindow, SystemParametersInfoW, SPIF_SENDCHANGE,
|
||||
SPIF_UPDATEINIFILE, SPI_GETFOREGROUNDLOCKTIMEOUT, SPI_SETFOREGROUNDLOCKTIMEOUT,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
pub(crate) struct HwndHolder(pub(crate) HWND);
|
||||
unsafe impl Send for HwndHolder {}
|
||||
|
||||
pub(crate) fn get_active_window() -> Option<HwndHolder> {
|
||||
unsafe { Some(HwndHolder(GetForegroundWindow())) }
|
||||
}
|
||||
|
||||
/// Searches for a window that looks like a security prompt and set it as focused.
|
||||
/// Only works when the process has permission to foreground, either by being in foreground
|
||||
/// Or by being given foreground permission https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setforegroundwindow#remarks
|
||||
pub fn focus_security_prompt() {
|
||||
let hwnd_result = unsafe { FindWindowA(s!("Credential Dialog Xaml Host"), None) };
|
||||
if let Ok(hwnd) = hwnd_result {
|
||||
set_focus(hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets focus to a window using a few unstable methods
|
||||
fn set_focus(hwnd: HWND) {
|
||||
unsafe {
|
||||
// Windows REALLY does not like apps stealing focus, even if it is for fixing Windows-Hello bugs.
|
||||
// The windows hello signing prompt NEEDS to be focused instantly, or it will error, but it does
|
||||
// not focus itself.
|
||||
|
||||
// This function implements forced focusing of windows using a few hacks.
|
||||
// The conditions to successfully foreground a window are:
|
||||
// All of the following conditions are true:
|
||||
// The calling process belongs to a desktop application, not a UWP app or a Windows Store app designed for Windows 8 or 8.1.
|
||||
// The foreground process has not disabled calls to SetForegroundWindow by a previous call to the LockSetForegroundWindow function.
|
||||
// The foreground lock time-out has expired (see SPI_GETFOREGROUNDLOCKTIMEOUT in SystemParametersInfo).
|
||||
// No menus are active.
|
||||
// Additionally, at least one of the following conditions is true:
|
||||
// The calling process is the foreground process.
|
||||
// The calling process was started by the foreground process.
|
||||
// There is currently no foreground window, and thus no foreground process.
|
||||
// The calling process received the last input event.
|
||||
// Either the foreground process or the calling process is being debugged.
|
||||
|
||||
// Update the foreground lock timeout temporarily
|
||||
let mut old_timeout = 0;
|
||||
let _ = SystemParametersInfoW(
|
||||
SPI_GETFOREGROUNDLOCKTIMEOUT,
|
||||
0,
|
||||
Some(&mut old_timeout as *mut _ as *mut std::ffi::c_void),
|
||||
windows::Win32::UI::WindowsAndMessaging::SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0),
|
||||
);
|
||||
let _ = SystemParametersInfoW(
|
||||
SPI_SETFOREGROUNDLOCKTIMEOUT,
|
||||
0,
|
||||
None,
|
||||
SPIF_UPDATEINIFILE | SPIF_SENDCHANGE,
|
||||
);
|
||||
let _scopeguard = scopeguard::guard((), |_| {
|
||||
let _ = SystemParametersInfoW(
|
||||
SPI_SETFOREGROUNDLOCKTIMEOUT,
|
||||
old_timeout,
|
||||
None,
|
||||
SPIF_UPDATEINIFILE | SPIF_SENDCHANGE,
|
||||
);
|
||||
});
|
||||
|
||||
// Attach to the foreground thread once attached, we can foreground, even if in the background
|
||||
let dw_current_thread = GetCurrentThreadId();
|
||||
let dw_fg_thread = GetWindowThreadProcessId(GetForegroundWindow(), None);
|
||||
|
||||
let _ = AttachThreadInput(dw_current_thread, dw_fg_thread, true);
|
||||
let _ = SetForegroundWindow(hwnd);
|
||||
SetCapture(hwnd);
|
||||
let _ = SetFocus(Some(hwnd));
|
||||
let _ = SetActiveWindow(hwnd);
|
||||
let _ = EnableWindow(hwnd, true);
|
||||
let _ = BringWindowToTop(hwnd);
|
||||
SwitchToThisWindow(hwnd, true);
|
||||
let _ = AttachThreadInput(dw_current_thread, dw_fg_thread, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// When restoring focus to the application window, we need a less aggressive method so the electron window doesn't get frozen.
|
||||
pub(crate) fn restore_focus(hwnd: HWND) {
|
||||
unsafe {
|
||||
let _ = SetForegroundWindow(hwnd);
|
||||
let _ = SetFocus(Some(hwnd));
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
pub mod autofill;
|
||||
pub mod autostart;
|
||||
pub mod biometric;
|
||||
pub mod biometric_v2;
|
||||
pub mod clipboard;
|
||||
pub mod crypto;
|
||||
pub(crate) mod crypto;
|
||||
pub mod error;
|
||||
pub mod ipc;
|
||||
pub mod password;
|
||||
pub mod powermonitor;
|
||||
pub mod process_isolation;
|
||||
pub(crate) mod secure_memory;
|
||||
pub mod ssh_agent;
|
||||
|
||||
use zeroizing_alloc::ZeroAlloc;
|
||||
|
||||
134
apps/desktop/desktop_native/core/src/secure_memory/dpapi.rs
Normal file
134
apps/desktop/desktop_native/core/src/secure_memory/dpapi.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use windows::Win32::Security::Cryptography::{
|
||||
CryptProtectMemory, CryptUnprotectMemory, CRYPTPROTECTMEMORY_BLOCK_SIZE,
|
||||
CRYPTPROTECTMEMORY_SAME_PROCESS,
|
||||
};
|
||||
|
||||
use crate::secure_memory::SecureMemoryStore;
|
||||
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectdata
|
||||
/// The DPAPI store encrypts data using the Windows Data Protection API (DPAPI). The key is bound
|
||||
/// to the current process, and cannot be decrypted by other user-mode processes.
|
||||
///
|
||||
/// Note: Admin processes can still decrypt this memory:
|
||||
/// https://blog.slowerzs.net/posts/cryptdecryptmemory/
|
||||
pub(crate) struct DpapiSecretKVStore {
|
||||
map: HashMap<String, Vec<u8>>,
|
||||
}
|
||||
|
||||
impl DpapiSecretKVStore {
|
||||
pub(crate) fn new() -> Self {
|
||||
DpapiSecretKVStore {
|
||||
map: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SecureMemoryStore for DpapiSecretKVStore {
|
||||
fn put(&mut self, key: String, value: &[u8]) {
|
||||
let length_header_len = std::mem::size_of::<usize>();
|
||||
|
||||
// The allocated data has to be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE, so we pad it and write the length in front
|
||||
// We are storing LENGTH|DATA|00..00, where LENGTH is the length of DATA, the total length is a multiple
|
||||
// of CRYPTPROTECTMEMORY_BLOCK_SIZE, and the padding is filled with zeros.
|
||||
|
||||
let data_len = value.len();
|
||||
let len_with_header = data_len + length_header_len;
|
||||
let padded_length = len_with_header + CRYPTPROTECTMEMORY_BLOCK_SIZE as usize
|
||||
- (len_with_header % CRYPTPROTECTMEMORY_BLOCK_SIZE as usize);
|
||||
let mut padded_data = vec![0u8; padded_length];
|
||||
padded_data[..length_header_len].copy_from_slice(&data_len.to_le_bytes());
|
||||
padded_data[length_header_len..][..data_len].copy_from_slice(value);
|
||||
|
||||
// Protect the memory using DPAPI
|
||||
unsafe {
|
||||
CryptProtectMemory(
|
||||
padded_data.as_mut_ptr() as *mut core::ffi::c_void,
|
||||
padded_length as u32,
|
||||
CRYPTPROTECTMEMORY_SAME_PROCESS,
|
||||
)
|
||||
}
|
||||
.expect("crypt_protect_memory should work");
|
||||
|
||||
self.map.insert(key, padded_data);
|
||||
}
|
||||
|
||||
fn get(&self, key: &str) -> Option<Vec<u8>> {
|
||||
self.map.get(key).map(|data| {
|
||||
// A copy is created, that is then mutated by the DPAPI unprotect function.
|
||||
let mut data = data.clone();
|
||||
unsafe {
|
||||
CryptUnprotectMemory(
|
||||
data.as_mut_ptr() as *mut core::ffi::c_void,
|
||||
data.len() as u32,
|
||||
CRYPTPROTECTMEMORY_SAME_PROCESS,
|
||||
)
|
||||
}
|
||||
.expect("crypt_unprotect_memory should work");
|
||||
|
||||
// Unpad the data to retrieve the original value
|
||||
let length_header_size = std::mem::size_of::<usize>();
|
||||
let length_bytes = &data[..length_header_size];
|
||||
let data_length = usize::from_le_bytes(
|
||||
length_bytes
|
||||
.try_into()
|
||||
.expect("length header should be usize"),
|
||||
);
|
||||
|
||||
data[length_header_size..length_header_size + data_length].to_vec()
|
||||
})
|
||||
}
|
||||
|
||||
fn has(&self, key: &str) -> bool {
|
||||
self.map.contains_key(key)
|
||||
}
|
||||
|
||||
fn remove(&mut self, key: &str) {
|
||||
self.map.remove(key);
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.map.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DpapiSecretKVStore {
|
||||
fn drop(&mut self) {
|
||||
self.clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_dpapi_secret_kv_store_various_sizes() {
|
||||
let mut store = DpapiSecretKVStore::new();
|
||||
for size in 0..=2048 {
|
||||
let key = format!("test_key_{}", size);
|
||||
let value: Vec<u8> = (0..size).map(|i| (i % 256) as u8).collect();
|
||||
store.put(key.clone(), &value);
|
||||
assert!(store.has(&key), "Store should have key for size {}", size);
|
||||
assert_eq!(
|
||||
store.get(&key),
|
||||
Some(value),
|
||||
"Value mismatch for size {}",
|
||||
size
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dpapi_crud() {
|
||||
let mut store = DpapiSecretKVStore::new();
|
||||
let key = "test_key".to_string();
|
||||
let value = vec![1, 2, 3, 4, 5];
|
||||
store.put(key.clone(), &value);
|
||||
assert!(store.has(&key));
|
||||
assert_eq!(store.get(&key), Some(value));
|
||||
store.remove(&key);
|
||||
assert!(!store.has(&key));
|
||||
}
|
||||
}
|
||||
22
apps/desktop/desktop_native/core/src/secure_memory/mod.rs
Normal file
22
apps/desktop/desktop_native/core/src/secure_memory/mod.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) mod dpapi;
|
||||
|
||||
/// The secure memory store provides an ephemeral key-value store for sensitive data.
|
||||
/// Data stored in this store is prevented from being swapped to disk and zeroed out. Additionally,
|
||||
/// platform-specific protections are applied to prevent memory dumps or debugger access from
|
||||
/// reading the stored values.
|
||||
#[allow(unused)]
|
||||
pub(crate) trait SecureMemoryStore {
|
||||
/// Stores a copy of the provided value in secure memory.
|
||||
fn put(&mut self, key: String, value: &[u8]);
|
||||
/// Retrieves a copy of the value associated with the given key from secure memory.
|
||||
/// This copy does not have additional memory protections applied, and should be zeroed when no
|
||||
/// longer needed.
|
||||
fn get(&self, key: &str) -> Option<Vec<u8>>;
|
||||
/// Checks if a value is stored under the given key.
|
||||
fn has(&self, key: &str) -> bool;
|
||||
/// Removes the value associated with the given key from secure memory.
|
||||
fn remove(&mut self, key: &str);
|
||||
/// Clears all values stored in secure memory.
|
||||
fn clear(&mut self);
|
||||
}
|
||||
12
apps/desktop/desktop_native/napi/index.d.ts
vendored
12
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -58,6 +58,18 @@ export declare namespace biometrics {
|
||||
ivB64: string
|
||||
}
|
||||
}
|
||||
export declare namespace biometrics_v2 {
|
||||
export function initBiometricSystem(): BiometricLockSystem
|
||||
export function authenticate(biometricLockSystem: BiometricLockSystem, hwnd: Buffer, message: string): Promise<boolean>
|
||||
export function authenticateAvailable(biometricLockSystem: BiometricLockSystem): Promise<boolean>
|
||||
export function enrollPersistent(biometricLockSystem: BiometricLockSystem, userId: string, key: Buffer): Promise<void>
|
||||
export function provideKey(biometricLockSystem: BiometricLockSystem, userId: string, key: Buffer): Promise<void>
|
||||
export function unlock(biometricLockSystem: BiometricLockSystem, userId: string, hwnd: Buffer): Promise<Buffer>
|
||||
export function unlockAvailable(biometricLockSystem: BiometricLockSystem, userId: string): Promise<boolean>
|
||||
export function hasPersistent(biometricLockSystem: BiometricLockSystem, userId: string): Promise<boolean>
|
||||
export function unenroll(biometricLockSystem: BiometricLockSystem, userId: string): Promise<void>
|
||||
export class BiometricLockSystem { }
|
||||
}
|
||||
export declare namespace clipboards {
|
||||
export function read(): Promise<string>
|
||||
export function write(text: string, password: boolean): Promise<void>
|
||||
|
||||
@@ -149,6 +149,123 @@ pub mod biometrics {
|
||||
}
|
||||
}
|
||||
|
||||
#[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!
|
||||
|
||||
Reference in New Issue
Block a user