1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-26 05:03:33 +00:00

[PM-10413] ssh keygen on web and browser (#12176)

* Move desktop to sdk ssh-key generation

* Add ssh keygen support on web and browser

* Move ssh keygen on all clients behind feature flag

* Update package lock

* Fix linting

* Fix build

* Fix build

* Remove rand_chacha

* Move libc to linux-only target

* Remove async-streams dep

* Make generateSshKey private

* Remove async from generate ssh key

* Update cargo lock

* Fix sdk init for ssh key generation

* Update index.d.ts

* Fix build on browser

* Fix build

* Fix build by updating libc dependency
This commit is contained in:
Bernd Schoolmann
2025-01-08 16:01:23 +01:00
committed by GitHub
parent 3949aae8e3
commit bb2961f4ca
21 changed files with 142 additions and 162 deletions

View File

@@ -324,28 +324,6 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "async-stream"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
dependencies = [
"async-stream-impl",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-stream-impl"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "async-task"
version = "4.7.1"
@@ -920,7 +898,6 @@ dependencies = [
"anyhow",
"arboard",
"argon2",
"async-stream",
"base64",
"bitwarden-russh",
"byteorder",
@@ -939,7 +916,6 @@ dependencies = [
"pin-project",
"pkcs8",
"rand",
"rand_chacha",
"retry",
"rsa",
"russh-cryptovec",

View File

@@ -26,12 +26,10 @@ arboard = { version = "=3.4.1", default-features = false, features = [
"wayland-data-control",
] }
argon2 = { version = "=0.5.3", features = ["zeroize"] }
async-stream = "=0.3.6"
base64 = "=0.22.1"
byteorder = "=1.5.0"
cbc = { version = "=0.1.2", features = ["alloc"] }
homedir = "=0.3.4"
libc = "=0.2.169"
pin-project = "=1.1.7"
dirs = "=5.0.1"
futures = "=0.3.31"
@@ -55,7 +53,6 @@ tokio-stream = { version = "=0.1.15", features = ["net"] }
tokio-util = { version = "=0.7.12", features = ["codec"] }
thiserror = "=1.0.69"
typenum = "=1.17.0"
rand_chacha = "=0.3.1"
pkcs8 = { version = "=0.10.2", features = ["alloc", "encryption", "pem"] }
rsa = "=0.9.6"
ed25519 = { version = "=2.2.3", features = ["pkcs8"] }
@@ -87,6 +84,7 @@ desktop_objc = { path = "../objc" }
[target.'cfg(target_os = "linux")'.dependencies]
oo7 = "=0.3.3"
libc = "=0.2.169"
zbus = { version = "=4.4.0", optional = true }
zbus_polkit = { version = "=4.0.0", optional = true }

View File

@@ -1,45 +0,0 @@
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
use ssh_key::{Algorithm, HashAlg, LineEnding};
use super::importer::SshKey;
pub async fn generate_keypair(key_algorithm: String) -> Result<SshKey, anyhow::Error> {
// sourced from cryptographically secure entropy source, with sources for all targets: https://docs.rs/getrandom
// if it cannot be securely sourced, this will panic instead of leading to a weak key
let mut rng: ChaCha8Rng = ChaCha8Rng::from_entropy();
let key = match key_algorithm.as_str() {
"ed25519" => ssh_key::PrivateKey::random(&mut rng, Algorithm::Ed25519),
"rsa2048" | "rsa3072" | "rsa4096" => {
let bits = match key_algorithm.as_str() {
"rsa2048" => 2048,
"rsa3072" => 3072,
"rsa4096" => 4096,
_ => return Err(anyhow::anyhow!("Unsupported RSA key size")),
};
let rsa_keypair = ssh_key::private::RsaKeypair::random(&mut rng, bits)
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
let private_key = ssh_key::PrivateKey::new(
ssh_key::private::KeypairData::from(rsa_keypair),
"".to_string(),
)
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
Ok(private_key)
}
_ => {
return Err(anyhow::anyhow!("Unsupported key algorithm"));
}
}
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
let private_key_openssh = key
.to_openssh(LineEnding::LF)
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
Ok(SshKey {
private_key: private_key_openssh.to_string(),
public_key: key.public_key().to_string(),
key_fingerprint: key.fingerprint(HashAlg::Sha256).to_string(),
})
}

View File

@@ -16,7 +16,6 @@ mod platform_ssh_agent;
#[cfg(any(target_os = "linux", target_os = "macos"))]
mod peercred_unix_listener_stream;
pub mod generator;
pub mod importer;
pub mod peerinfo;
#[derive(Clone)]

View File

@@ -74,7 +74,6 @@ export declare namespace sshagent {
export function lock(agentState: SshAgentState): void
export function importKey(encodedKey: string, password: string): SshKeyImportResult
export function clearKeys(agentState: SshAgentState): void
export function generateKeypair(keyAlgorithm: string): Promise<SshKey>
export class SshAgentState { }
}
export declare namespace processisolations {

View File

@@ -362,14 +362,6 @@ pub mod sshagent {
.clear_keys()
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[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]

View File

@@ -25,12 +25,6 @@ export class MainSshAgentService {
private logService: LogService,
private messagingService: MessagingService,
) {
ipcMain.handle(
"sshagent.generatekey",
async (event: any, { keyAlgorithm }: { keyAlgorithm: string }): Promise<sshagent.SshKey> => {
return await sshagent.generateKeypair(keyAlgorithm);
},
);
ipcMain.handle(
"sshagent.importkey",
async (

View File

@@ -58,9 +58,6 @@ const sshAgent = {
signRequestResponse: async (requestId: number, accepted: boolean) => {
await ipcRenderer.invoke("sshagent.signrequestresponse", { requestId, accepted });
},
generateKey: async (keyAlgorithm: string): Promise<ssh.SshKey> => {
return await ipcRenderer.invoke("sshagent.generatekey", { keyAlgorithm });
},
lock: async () => {
return await ipcRenderer.invoke("sshagent.lock");
},

View File

@@ -512,16 +512,6 @@
[ngClass]="{ 'bwi-eye': !showPrivateKey, 'bwi-eye-slash': showPrivateKey }"
></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'regenerateSshKey' | i18n }}"
(click)="generateSshKey()"
*ngIf="cipher.edit || !editMode"
>
<i class="bwi bwi-lg bwi-generate" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="box-content-row box-content-row-flex" appBoxRow>

View File

@@ -19,9 +19,9 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { SshKeyPasswordPromptComponent } from "@bitwarden/importer/ui";
@@ -56,8 +56,9 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
dialogService: DialogService,
datePipe: DatePipe,
configService: ConfigService,
private toastService: ToastService,
toastService: ToastService,
cipherAuthorizationService: CipherAuthorizationService,
sdkService: SdkService,
) {
super(
cipherService,
@@ -78,6 +79,8 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
datePipe,
configService,
cipherAuthorizationService,
toastService,
sdkService,
);
}
@@ -114,17 +117,6 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
}
await super.load();
if (!this.editMode || this.cloneMode) {
// Creating an ssh key directly while filtering to the ssh key category
// must force a key to be set. SSH keys must never be created with an empty private key field
if (
this.cipher.type === CipherType.SshKey &&
(this.cipher.sshKey.privateKey == null || this.cipher.sshKey.privateKey === "")
) {
await this.generateSshKey(false);
}
}
}
onWindowHidden() {
@@ -156,21 +148,6 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
);
}
async generateSshKey(showNotification: boolean = true) {
const sshKey = await ipc.platform.sshAgent.generateKey("ed25519");
this.cipher.sshKey.privateKey = sshKey.privateKey;
this.cipher.sshKey.publicKey = sshKey.publicKey;
this.cipher.sshKey.keyFingerprint = sshKey.keyFingerprint;
if (showNotification) {
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("sshKeyGenerated"),
});
}
}
async importSshKeyFromClipboard(password: string = "") {
const key = await this.platformUtilsService.readFromClipboard();
const parsedKey = await ipc.platform.sshAgent.importKey(key, password);
@@ -234,12 +211,6 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
return await lastValueFrom(dialog.closed);
}
async typeChange() {
if (this.cipher.type === CipherType.SshKey) {
await this.generateSshKey();
}
}
truncateString(value: string, length: number) {
return value.length > length ? value.substring(0, length) + "..." : value;
}