1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-09-27 04:03:00 +02: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:
Oscar Hinton 2023-10-02 18:50:57 +02:00 committed by GitHub
parent 6aed74d241
commit 30feb60645
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 485 additions and 240 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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>
}

View File

@ -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

View File

@ -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]

View 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());
}
}

View File

@ -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()))
}
}

View File

@ -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) };

View File

@ -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",

View File

@ -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() {

View 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);
});
}
}

View File

@ -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> {

View File

@ -0,0 +1,4 @@
export type ClipboardWriteMessage = {
text: string;
password?: boolean;
};

View File

@ -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;