mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-21 02:11:54 +01:00
[BEEEP] [PM-565] Implement clipboard logic in rust (#4516)
Implement the Desktop clipboard logic natively using rust. This uses the arboard crate for clipboard functionality. This change consists of 3 portions: * Rust component. * Updating renderer to call main using electron ipc. * Update main to listen to renderer ipc and forward calls to the native clipboard module.
This commit is contained in:
parent
6aed74d241
commit
30feb60645
558
apps/desktop/desktop_native/Cargo.lock
generated
558
apps/desktop/desktop_native/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -15,6 +15,7 @@ manual_test = []
|
||||
[dependencies]
|
||||
aes = "=0.8.2"
|
||||
anyhow = "=1.0.71"
|
||||
arboard = { version = "=3.2.1", default-features = false }
|
||||
base64 = "=0.21.2"
|
||||
cbc = { version = "=0.1.2", features = ["alloc"] }
|
||||
napi = { version = "=2.13.1", features = ["async"] }
|
||||
@ -31,7 +32,7 @@ typenum = "=1.16.0"
|
||||
napi-build = "=2.0.1"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
widestring = "=0.5.1"
|
||||
widestring = "=1.0.2"
|
||||
windows = { version = "=0.48.0", features = [
|
||||
"Foundation",
|
||||
"Security_Credentials_UI",
|
||||
|
4
apps/desktop/desktop_native/index.d.ts
vendored
4
apps/desktop/desktop_native/index.d.ts
vendored
@ -37,3 +37,7 @@ export namespace biometrics {
|
||||
ivB64: string
|
||||
}
|
||||
}
|
||||
export namespace clipboards {
|
||||
export function read(): Promise<string>
|
||||
export function write(text: string, password: boolean): Promise<void>
|
||||
}
|
||||
|
@ -206,7 +206,8 @@ if (!nativeBinding) {
|
||||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { passwords, biometrics } = nativeBinding
|
||||
const { passwords, biometrics, clipboards } = nativeBinding
|
||||
|
||||
module.exports.passwords = passwords
|
||||
module.exports.biometrics = biometrics
|
||||
module.exports.clipboards = clipboards
|
||||
|
@ -7,8 +7,8 @@ use rand::RngCore;
|
||||
use retry::delay::Fixed;
|
||||
use sha2::{Digest, Sha256};
|
||||
use windows::{
|
||||
h,
|
||||
core::{factory, HSTRING},
|
||||
h,
|
||||
Foundation::IAsyncOperation,
|
||||
Security::{
|
||||
Credentials::{
|
||||
@ -241,7 +241,9 @@ fn set_focus(window: HWND) {
|
||||
impl KeyMaterial {
|
||||
fn digest_material(&self) -> String {
|
||||
match self.client_key_part_b64.as_deref() {
|
||||
Some(client_key_part_b64) => format!("{}|{}", self.os_key_part_b64, client_key_part_b64),
|
||||
Some(client_key_part_b64) => {
|
||||
format!("{}|{}", self.os_key_part_b64, client_key_part_b64)
|
||||
}
|
||||
None => self.os_key_part_b64.clone(),
|
||||
}
|
||||
}
|
||||
@ -419,7 +421,14 @@ mod tests {
|
||||
let mut key_material = key_material();
|
||||
key_material.client_key_part_b64 = None;
|
||||
let result = key_material.derive_key().unwrap();
|
||||
assert_eq!(result, [81, 100, 62, 172, 151, 119, 182, 58, 123, 38, 129, 116, 209, 253, 66, 118, 218, 237, 236, 155, 201, 234, 11, 198, 229, 171, 246, 144, 71, 188, 84, 246].into());
|
||||
assert_eq!(
|
||||
result,
|
||||
[
|
||||
81, 100, 62, 172, 151, 119, 182, 58, 123, 38, 129, 116, 209, 253, 66, 118, 218,
|
||||
237, 236, 155, 201, 234, 11, 198, 229, 171, 246, 144, 71, 188, 84, 246
|
||||
]
|
||||
.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
56
apps/desktop/desktop_native/src/clipboard.rs
Normal file
56
apps/desktop/desktop_native/src/clipboard.rs
Normal file
@ -0,0 +1,56 @@
|
||||
use anyhow::Result;
|
||||
use arboard::{Clipboard, Set};
|
||||
|
||||
pub fn read() -> Result<String> {
|
||||
let mut clipboard = Clipboard::new()?;
|
||||
|
||||
Ok(clipboard.get_text()?)
|
||||
}
|
||||
|
||||
pub fn write(text: &str, password: bool) -> Result<()> {
|
||||
let mut clipboard = Clipboard::new()?;
|
||||
|
||||
let set = clipboard_set(clipboard.set(), password);
|
||||
|
||||
set.text(text)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Exclude from windows clipboard history
|
||||
#[cfg(target_os = "windows")]
|
||||
fn clipboard_set(set: Set, password: bool) -> Set {
|
||||
use arboard::SetExtWindows;
|
||||
|
||||
if password {
|
||||
set.exclude_from_cloud().exclude_from_history()
|
||||
} else {
|
||||
set
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for clipboard to be available on linux
|
||||
#[cfg(target_os = "linux")]
|
||||
fn clipboard_set(set: Set, _password: bool) -> Set {
|
||||
use arboard::SetExtLinux;
|
||||
|
||||
set.wait()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn clipboard_set(set: Set, _password: bool) -> Set {
|
||||
set
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[cfg(any(feature = "manual_test", not(target_os = "linux")))]
|
||||
fn test_write_read() {
|
||||
let message = "Hello world!";
|
||||
|
||||
write(message, false).unwrap();
|
||||
assert_eq!(message, read().unwrap());
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
extern crate napi_derive;
|
||||
|
||||
mod biometric;
|
||||
mod clipboard;
|
||||
mod crypto;
|
||||
mod error;
|
||||
mod password;
|
||||
@ -107,3 +108,17 @@ pub mod biometrics {
|
||||
pub iv_b64: String,
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub mod clipboards {
|
||||
#[napi]
|
||||
pub async fn read() -> napi::Result<String> {
|
||||
super::clipboard::read().map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn write(text: String, password: bool) -> napi::Result<()> {
|
||||
super::clipboard::write(&text, password)
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ pub fn set_password(service: &str, account: &str, password: &str) -> Result<()>
|
||||
let credential = CREDENTIALW {
|
||||
Flags: CRED_FLAGS(CRED_FLAGS_NONE),
|
||||
Type: CRED_TYPE_GENERIC,
|
||||
TargetName: PWSTR(unsafe { target_name.as_mut_ptr() }),
|
||||
TargetName: PWSTR(target_name.as_mut_ptr()),
|
||||
Comment: PWSTR::null(),
|
||||
LastWritten: last_written,
|
||||
CredentialBlobSize: credential_len,
|
||||
@ -104,7 +104,7 @@ pub fn set_password(service: &str, account: &str, password: &str) -> Result<()>
|
||||
AttributeCount: 0,
|
||||
Attributes: std::ptr::null_mut(),
|
||||
TargetAlias: PWSTR::null(),
|
||||
UserName: PWSTR(unsafe { user_name.as_mut_ptr() }),
|
||||
UserName: PWSTR(user_name.as_mut_ptr()),
|
||||
};
|
||||
|
||||
let result = unsafe { CredWriteW(&credential, 0) };
|
||||
|
@ -18,7 +18,7 @@
|
||||
"scripts": {
|
||||
"postinstall": "electron-rebuild",
|
||||
"start": "cross-env ELECTRON_IS_DEV=0 ELECTRON_NO_UPDATER=1 electron ./build",
|
||||
"build-native": "cargo build --manifest-path=./desktop_native/Cargo.toml",
|
||||
"build-native": "cd desktop_native && npm run build",
|
||||
"build": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\"",
|
||||
"build:dev": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\"",
|
||||
"build:main": "cross-env NODE_ENV=production webpack --config webpack.main.js",
|
||||
|
@ -15,6 +15,7 @@ import { UpdaterMain } from "./main/updater.main";
|
||||
import { WindowMain } from "./main/window.main";
|
||||
import { Account } from "./models/account";
|
||||
import { BiometricsService, BiometricsServiceAbstraction } from "./platform/main/biometric/index";
|
||||
import { ClipboardMain } from "./platform/main/clipboard.main";
|
||||
import { DesktopCredentialStorageListener } from "./platform/main/desktop-credential-storage-listener";
|
||||
import { ElectronLogService } from "./platform/services/electron-log.service";
|
||||
import { ElectronStateService } from "./platform/services/electron-state.service";
|
||||
@ -39,6 +40,7 @@ export class Main {
|
||||
trayMain: TrayMain;
|
||||
biometricsService: BiometricsServiceAbstraction;
|
||||
nativeMessagingMain: NativeMessagingMain;
|
||||
clipboardMain: ClipboardMain;
|
||||
|
||||
constructor() {
|
||||
// Set paths for portable builds
|
||||
@ -138,6 +140,9 @@ export class Main {
|
||||
app.getPath("userData"),
|
||||
app.getPath("exe")
|
||||
);
|
||||
|
||||
this.clipboardMain = new ClipboardMain();
|
||||
this.clipboardMain.init();
|
||||
}
|
||||
|
||||
bootstrap() {
|
||||
|
17
apps/desktop/src/platform/main/clipboard.main.ts
Normal file
17
apps/desktop/src/platform/main/clipboard.main.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { ipcMain } from "electron";
|
||||
|
||||
import { clipboards } from "@bitwarden/desktop-native";
|
||||
|
||||
import { ClipboardWriteMessage } from "../types/clipboard";
|
||||
|
||||
export class ClipboardMain {
|
||||
init() {
|
||||
ipcMain.handle("clipboard.read", async (_event: any, _message: any) => {
|
||||
return await clipboards.read();
|
||||
});
|
||||
|
||||
ipcMain.handle("clipboard.write", async (_event: any, message: ClipboardWriteMessage) => {
|
||||
return await clipboards.write(message.text, message.password ?? false);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,12 +1,16 @@
|
||||
import { clipboard, ipcRenderer, shell } from "electron";
|
||||
import { ipcRenderer, shell } from "electron";
|
||||
|
||||
import { ClientType, DeviceType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
ClipboardOptions,
|
||||
PlatformUtilsService,
|
||||
} from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { BiometricMessage, BiometricStorageAction } from "../../types/biometric-message";
|
||||
import { isDev, isMacAppStore } from "../../utils";
|
||||
import { ClipboardWriteMessage } from "../types/clipboard";
|
||||
|
||||
export class ElectronPlatformUtilsService implements PlatformUtilsService {
|
||||
private deviceCache: DeviceType = null;
|
||||
@ -117,24 +121,26 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService {
|
||||
return false;
|
||||
}
|
||||
|
||||
copyToClipboard(text: string, options?: any): void {
|
||||
const type = options ? options.type : null;
|
||||
const clearing = options ? !!options.clearing : false;
|
||||
const clearMs: number = options && options.clearMs ? options.clearMs : null;
|
||||
clipboard.writeText(text, type);
|
||||
copyToClipboard(text: string, options?: ClipboardOptions): void {
|
||||
const clearing = options?.clearing === true;
|
||||
const clearMs = options?.clearMs ?? null;
|
||||
|
||||
ipcRenderer.invoke("clipboard.write", {
|
||||
text: text,
|
||||
password: (options?.allowHistory ?? false) === false, // default to false
|
||||
} satisfies ClipboardWriteMessage);
|
||||
|
||||
if (!clearing) {
|
||||
this.messagingService.send("copiedToClipboard", {
|
||||
clipboardValue: text,
|
||||
clearMs: clearMs,
|
||||
type: type,
|
||||
clearing: clearing,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
readFromClipboard(options?: any): Promise<string> {
|
||||
const type = options ? options.type : null;
|
||||
return Promise.resolve(clipboard.readText(type));
|
||||
readFromClipboard(): Promise<string> {
|
||||
return ipcRenderer.invoke("clipboard.read");
|
||||
}
|
||||
|
||||
async supportsBiometric(): Promise<boolean> {
|
||||
|
4
apps/desktop/src/platform/types/clipboard.ts
Normal file
4
apps/desktop/src/platform/types/clipboard.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type ClipboardWriteMessage = {
|
||||
text: string;
|
||||
password?: boolean;
|
||||
};
|
@ -4,6 +4,13 @@ interface ToastOptions {
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export type ClipboardOptions = {
|
||||
allowHistory?: boolean;
|
||||
clearing?: boolean;
|
||||
clearMs?: number;
|
||||
window?: Window;
|
||||
};
|
||||
|
||||
export abstract class PlatformUtilsService {
|
||||
getDevice: () => DeviceType;
|
||||
getDeviceString: () => string;
|
||||
@ -29,8 +36,8 @@ export abstract class PlatformUtilsService {
|
||||
) => void;
|
||||
isDev: () => boolean;
|
||||
isSelfHost: () => boolean;
|
||||
copyToClipboard: (text: string, options?: any) => void | boolean;
|
||||
readFromClipboard: (options?: any) => Promise<string>;
|
||||
copyToClipboard: (text: string, options?: ClipboardOptions) => void | boolean;
|
||||
readFromClipboard: () => Promise<string>;
|
||||
supportsBiometric: () => Promise<boolean>;
|
||||
authenticateBiometric: () => Promise<boolean>;
|
||||
supportsSecureStorage: () => boolean;
|
||||
|
Loading…
Reference in New Issue
Block a user