1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 13:23:34 +00:00
Files
browser/apps/desktop/desktop_native/napi/src/lib.rs
Bernd Schoolmann 081fe83d83 PM-10393 SSH keys (#10825)
* [PM-10395] Add new item type ssh key (#10360)

* Implement ssh-key cipher type

* Fix linting

* Fix edit and view components for ssh-keys on desktop

* Fix tests

* Remove ssh key type references

* Remove add ssh key option

* Fix typo

* Add tests

* [PM-10399] Add ssh key import export for bitwarden json (#10529)

* Add ssh key import export for bitwarden json

* Remove key type from ssh key export

* [PM-10406] Add privatekey publickey and fingerprint to both add-edit and view co… (#11046)

* Add privatekey publickey and fingerprint to both add-edit and view components

* Remove wrong a11y title

* Fix testid

* [PM-10098] SSH Agent & SSH Key creation for Bitwarden Desktop (#10293)

* Add ssh agent, generator & import

* Move ssh agent code to bitwarden-russh crate

* Remove generator component

* Cleanup

* Cleanup

* Remove left over sshGenerator reference

* Cleanup

* Add documentation to sshkeyimportstatus

* Fix outdated variable name

* Update apps/desktop/src/platform/preload.ts

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Rename renderersshagent

* Rename MainSshAgentService

* Improve clarity of 'id' variables being used

* Improve clarity of 'id' variables being used

* Update apps/desktop/src/vault/app/vault/add-edit.component.html

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Fix outdated cipher/messageid names

* Rename SSH to Ssh

* Make agent syncing more reactive

* Move constants to top of class

* Make sshkey cipher filtering clearer

* Add stricter equality check on ssh key unlock

* Fix build and messages

* Fix incorrect featureflag name

* Replace anonymous async function with switchmap pipe

* Fix build

* Update apps/desktop/desktop_native/napi/src/lib.rs

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Revert incorrectly renamed 'Ssh' usages to SSH

* Run cargo fmt

* Clean up ssh agent sock path logic

* Cleanup and split to platform specific files

* Small cleanup

* Pull out generator and importer into core

* Rename renderersshagentservice to sshagentservice

* Rename cipheruuid to cipher_id

* Drop ssh dependencies from napi crate

* Clean up windows build

* Small cleanup

* Small cleanup

* Cleanup

* Add rxjs pipeline for agent services

* [PM-12555] Pkcs8 sshkey import & general ssh key import tests (#11048)

* Add pkcs8 import and tests

* Add key type unsupported error

* Remove unsupported formats

* Remove code for unsupported formats

* Fix encrypted pkcs8 import

* Add ed25519 pkcs8 unencrypted test file

* SSH agent rxjs tweaks (#11148)

* feat: rewrite sshagent.signrequest as purely observable

* feat: fail the request when unlock times out

* chore: clean up, add some clarifying comments

* chore: remove unused dependency

* fix: result `undefined` crashing in NAPI -> Rust

* Allow concurrent SSH requests in rust

* Remove unwraps

* Cleanup and add init service init call

* Fix windows

* Fix timeout behavior on locked vault

---------

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Fix libc dependency being duplicated

* fix SSH casing (#11840)

* Move ssh agent behind feature flag (#11841)

* Move ssh agent behind feature flag

* Add separate flag for ssh agent

* [PM-14215] fix unsupported key type error message (#11788)

* Fix error message for import of unsupported ssh keys

* Use triple equals in add-edit component for ssh keys

---------

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
Co-authored-by: aj-bw <81774843+aj-bw@users.noreply.github.com>
2024-11-08 11:01:31 +01:00

513 lines
18 KiB
Rust

#[macro_use]
extern crate napi_derive;
mod registry;
#[napi]
pub mod passwords {
/// Fetch the stored password from the keychain.
#[napi]
pub async fn get_password(service: String, account: String) -> napi::Result<String> {
desktop_core::password::get_password(&service, &account)
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
/// Fetch the stored password from the keychain that was stored with Keytar.
#[napi]
pub async fn get_password_keytar(service: String, account: String) -> napi::Result<String> {
desktop_core::password::get_password_keytar(&service, &account)
.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)
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
/// Delete the stored password from the keychain.
#[napi]
pub async fn delete_password(service: String, account: String) -> napi::Result<()> {
desktop_core::password::delete_password(&service, &account)
.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().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,
)
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn get_biometric_secret(
service: String,
account: String,
key_material: Option<KeyMaterial>,
) -> napi::Result<String> {
let result =
Biometric::get_biometric_secret(&service, &account, key_material.map(|m| m.into()))
.map_err(|e| napi::Error::from_reason(e.to_string()));
result
}
/// 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>")`
#[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 clipboards {
#[napi]
pub async fn read() -> napi::Result<String> {
desktop_core::clipboard::read().map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[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};
#[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,
}
impl From<desktop_core::ssh_agent::importer::SshKey> for SshKey {
fn from(key: desktop_core::ssh_agent::importer::SshKey) -> Self {
SshKey {
private_key: key.private_key,
public_key: key.public_key,
key_fingerprint: key.key_fingerprint,
}
}
}
#[napi]
pub enum SshKeyImportStatus {
/// ssh key was parsed correctly and will be returned in the result
Success,
/// ssh key was parsed correctly but is encrypted and requires a password
PasswordRequired,
/// ssh key was parsed correctly, and a password was provided when calling the import, but it was incorrect
WrongPassword,
/// ssh key could not be parsed, either due to an incorrect / unsupported format (pkcs#8) or key type (ecdsa), or because the input is not an ssh key
ParsingError,
/// ssh key type is not supported (e.g. ecdsa)
UnsupportedKeyType,
}
impl From<desktop_core::ssh_agent::importer::SshKeyImportStatus> for SshKeyImportStatus {
fn from(status: desktop_core::ssh_agent::importer::SshKeyImportStatus) -> Self {
match status {
desktop_core::ssh_agent::importer::SshKeyImportStatus::Success => {
SshKeyImportStatus::Success
}
desktop_core::ssh_agent::importer::SshKeyImportStatus::PasswordRequired => {
SshKeyImportStatus::PasswordRequired
}
desktop_core::ssh_agent::importer::SshKeyImportStatus::WrongPassword => {
SshKeyImportStatus::WrongPassword
}
desktop_core::ssh_agent::importer::SshKeyImportStatus::ParsingError => {
SshKeyImportStatus::ParsingError
}
desktop_core::ssh_agent::importer::SshKeyImportStatus::UnsupportedKeyType => {
SshKeyImportStatus::UnsupportedKeyType
}
}
}
}
#[napi(object)]
pub struct SshKeyImportResult {
pub status: SshKeyImportStatus,
pub ssh_key: Option<SshKey>,
}
impl From<desktop_core::ssh_agent::importer::SshKeyImportResult> for SshKeyImportResult {
fn from(result: desktop_core::ssh_agent::importer::SshKeyImportResult) -> Self {
SshKeyImportResult {
status: result.status.into(),
ssh_key: result.ssh_key.map(|k| k.into()),
}
}
}
#[napi]
pub async fn serve(
callback: ThreadsafeFunction<String, CalleeHandled>,
) -> napi::Result<SshAgentState> {
let (auth_request_tx, mut auth_request_rx) = tokio::sync::mpsc::channel::<(u32, String)>(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_id, cipher_uuid)) = auth_request_rx.recv().await {
let cloned_request_id = request_id.clone();
let cloned_cipher_uuid = cipher_uuid.clone();
let cloned_response_tx_arc = auth_response_tx_arc.clone();
let cloned_callback = callback.clone();
tokio::spawn(async move {
let request_id = cloned_request_id;
let cipher_uuid = cloned_cipher_uuid;
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(cipher_uuid)).await;
match promise_result {
Ok(promise_result) => match promise_result.await {
Ok(result) => {
let _ = auth_response_tx_arc.lock().await.send((request_id, result))
.expect("should be able to send auth response to agent");
}
Err(e) => {
println!("[SSH Agent Native Module] calling UI callback promise was rejected: {}", e);
let _ = auth_response_tx_arc.lock().await.send((request_id, false))
.expect("should be able to send auth response to agent");
}
},
Err(e) => {
println!("[SSH Agent Native Module] calling UI callback could not create promise: {}", e);
let _ = auth_response_tx_arc.lock().await.send((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)),
)
.await
{
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 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 import_key(encoded_key: String, password: String) -> napi::Result<SshKeyImportResult> {
let result = desktop_core::ssh_agent::importer::import_key(encoded_key, password)
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
Ok(result.into())
}
#[napi]
pub async fn generate_keypair(key_algorithm: String) -> napi::Result<SshKey> {
desktop_core::ssh_agent::generator::generate_keypair(key_algorithm)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
.map(|k| k.into())
}
}
#[napi]
pub mod processisolations {
#[napi]
pub async fn disable_coredumps() -> napi::Result<()> {
desktop_core::process_isolation::disable_coredumps()
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[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()))
}
#[napi]
pub async fn disable_memory_access() -> napi::Result<()> {
desktop_core::process_isolation::disable_memory_access()
.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(message) = rx.recv().await {
callback.call(Ok(message.into()), 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 {
#[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()))
}
#[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.
#[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 })
}
/// 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())
}
}
}