1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-15 20:11:30 +01: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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 142 additions and 162 deletions

View File

@ -567,7 +567,7 @@ const safeProviders: SafeProvider[] = [
}),
safeProvider({
provide: SdkClientFactory,
useFactory: (logService) =>
useFactory: (logService: LogService) =>
flagEnabled("sdk") ? new BrowserSdkClientFactory(logService) : new NoopSdkClientFactory(),
deps: [LogService],
}),

View File

@ -27,6 +27,15 @@
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
{{ "note" | i18n }}
</a>
<a
bitMenuItem
[routerLink]="['/add-cipher']"
[queryParams]="buildQueryParams(cipherType.SshKey)"
*ngIf="sshKeysEnabled"
>
<i class="bwi bwi-key" slot="start" aria-hidden="true"></i>
{{ "typeSshKey" | i18n }}
</a>
<bit-menu-divider></bit-menu-divider>
<button type="button" bitMenuItem (click)="openFolderDialog()">
<i class="bwi bwi-folder" slot="start" aria-hidden="true"></i>

View File

@ -3,7 +3,9 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ActivatedRoute, RouterLink } from "@angular/router";
import { mock } from "jest-mock-extended";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
@ -45,6 +47,7 @@ describe("NewItemDropdownV2Component", () => {
await TestBed.configureTestingModule({
imports: [
JslibModule,
CommonModule,
RouterLink,
ButtonModule,
@ -53,6 +56,8 @@ describe("NewItemDropdownV2Component", () => {
NewItemDropdownV2Component,
],
providers: [
{ provide: I18nService, useValue: { t: (key: string) => key } },
{ provide: ConfigService, useValue: { getFeatureFlag: () => Promise.resolve(false) } },
{ provide: DialogService, useValue: dialogServiceMock },
{ provide: I18nService, useValue: i18nServiceMock },
{ provide: ActivatedRoute, useValue: activatedRouteMock },
@ -82,7 +87,7 @@ describe("NewItemDropdownV2Component", () => {
jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false);
jest.spyOn(Utils, "getHostname").mockReturnValue("example.com");
const params = component.buildQueryParams(CipherType.Login);
const params = await component.buildQueryParams(CipherType.Login);
expect(params).toEqual({
type: CipherType.Login.toString(),
@ -94,14 +99,14 @@ describe("NewItemDropdownV2Component", () => {
});
});
it("should build query params for a Login cipher when popped out", () => {
it("should build query params for a Login cipher when popped out", async () => {
component.initialValues = {
collectionId: "777-888-999",
} as NewItemInitialValues;
jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(true);
const params = component.buildQueryParams(CipherType.Login);
const params = await component.buildQueryParams(CipherType.Login);
expect(params).toEqual({
type: CipherType.Login.toString(),
@ -109,12 +114,12 @@ describe("NewItemDropdownV2Component", () => {
});
});
it("should build query params for a secure note", () => {
it("should build query params for a secure note", async () => {
component.initialValues = {
collectionId: "777-888-999",
} as NewItemInitialValues;
const params = component.buildQueryParams(CipherType.SecureNote);
const params = await component.buildQueryParams(CipherType.SecureNote);
expect(params).toEqual({
type: CipherType.SecureNote.toString(),
@ -122,12 +127,12 @@ describe("NewItemDropdownV2Component", () => {
});
});
it("should build query params for an Identity", () => {
it("should build query params for an Identity", async () => {
component.initialValues = {
collectionId: "777-888-999",
} as NewItemInitialValues;
const params = component.buildQueryParams(CipherType.Identity);
const params = await component.buildQueryParams(CipherType.Identity);
expect(params).toEqual({
type: CipherType.Identity.toString(),
@ -135,12 +140,12 @@ describe("NewItemDropdownV2Component", () => {
});
});
it("should build query params for a Card", () => {
it("should build query params for a Card", async () => {
component.initialValues = {
collectionId: "777-888-999",
} as NewItemInitialValues;
const params = component.buildQueryParams(CipherType.Card);
const params = await component.buildQueryParams(CipherType.Card);
expect(params).toEqual({
type: CipherType.Card.toString(),
@ -148,12 +153,12 @@ describe("NewItemDropdownV2Component", () => {
});
});
it("should build query params for a SshKey", () => {
it("should build query params for a SshKey", async () => {
component.initialValues = {
collectionId: "777-888-999",
} as NewItemInitialValues;
const params = component.buildQueryParams(CipherType.SshKey);
const params = await component.buildQueryParams(CipherType.SshKey);
expect(params).toEqual({
type: CipherType.SshKey.toString(),

View File

@ -2,9 +2,11 @@
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, Input, OnInit } from "@angular/core";
import { RouterLink } from "@angular/router";
import { Router, RouterLink } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums";
@ -35,14 +37,20 @@ export class NewItemDropdownV2Component implements OnInit {
*/
@Input()
initialValues: NewItemInitialValues;
constructor(
private router: Router,
private dialogService: DialogService,
private configService: ConfigService,
) {}
constructor(private dialogService: DialogService) {}
sshKeysEnabled = false;
async ngOnInit() {
this.sshKeysEnabled = await this.configService.getFeatureFlag(FeatureFlag.SSHKeyVaultItem);
this.tab = await BrowserApi.getTabFromCurrentWindow();
}
buildQueryParams(type: CipherType): AddEditQueryParams {
async buildQueryParams(type: CipherType): Promise<AddEditQueryParams> {
const poppedOut = BrowserPopupUtils.inPopout(window);
const loginDetails: { uri?: string; name?: string } = {};

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

View File

@ -15,12 +15,13 @@ 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 { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService } from "@bitwarden/components";
import { DialogService, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { PasswordRepromptService } from "@bitwarden/vault";
@ -56,6 +57,8 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent implem
configService: ConfigService,
billingAccountProfileStateService: BillingAccountProfileStateService,
cipherAuthorizationService: CipherAuthorizationService,
toastService: ToastService,
sdkService: SdkService,
) {
super(
cipherService,
@ -78,6 +81,8 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent implem
configService,
billingAccountProfileStateService,
cipherAuthorizationService,
toastService,
sdkService,
);
}

View File

@ -35,6 +35,7 @@
[(ngModel)]="cipher.type"
class="form-control"
[disabled]="cipher.isDeleted"
(change)="typeChange()"
appAutofocus
>
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{ o.name }}</option>

View File

@ -21,13 +21,14 @@ 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 { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { Launchable } from "@bitwarden/common/vault/interfaces/launchable";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService } from "@bitwarden/components";
import { DialogService, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { PasswordRepromptService } from "@bitwarden/vault";
@ -73,6 +74,8 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
configService: ConfigService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
cipherAuthorizationService: CipherAuthorizationService,
toastService: ToastService,
sdkService: SdkService,
) {
super(
cipherService,
@ -93,6 +96,8 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
datePipe,
configService,
cipherAuthorizationService,
toastService,
sdkService,
);
}

View File

@ -99,6 +99,10 @@
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
{{ "note" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.SshKey)">
<i class="bwi bwi-key" slot="start" aria-hidden="true"></i>
{{ "typeSshKey" | i18n }}
</button>
<bit-menu-divider />
<button type="button" bitMenuItem (click)="addFolder()">
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>

View File

@ -16,6 +16,7 @@ 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 { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
@ -23,7 +24,7 @@ import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService } from "@bitwarden/components";
import { DialogService, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { PasswordRepromptService } from "@bitwarden/vault";
@ -59,6 +60,8 @@ export class AddEditComponent extends BaseAddEditComponent {
configService: ConfigService,
billingAccountProfileStateService: BillingAccountProfileStateService,
cipherAuthorizationService: CipherAuthorizationService,
toastService: ToastService,
sdkService: SdkService,
) {
super(
cipherService,
@ -81,6 +84,8 @@ export class AddEditComponent extends BaseAddEditComponent {
configService,
billingAccountProfileStateService,
cipherAuthorizationService,
toastService,
sdkService,
);
}

View File

@ -16,7 +16,7 @@ import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils";
import { ClientType, EventType } from "@bitwarden/common/enums";
import { EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@ -24,6 +24,7 @@ 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 { Utils } from "@bitwarden/common/platform/misc/utils";
import { CollectionId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@ -40,7 +41,8 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService } from "@bitwarden/components";
import { DialogService, ToastService } from "@bitwarden/components";
import { generate_ssh_key } from "@bitwarden/sdk-internal";
import { PasswordRepromptService } from "@bitwarden/vault";
@Directive()
@ -132,6 +134,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
protected datePipe: DatePipe,
protected configService: ConfigService,
protected cipherAuthorizationService: CipherAuthorizationService,
protected toastService: ToastService,
private sdkService: SdkService,
) {
this.typeOptions = [
{ name: i18nService.t("typeLogin"), value: CipherType.Login },
@ -208,7 +212,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.canUseReprompt = await this.passwordRepromptService.enabled();
const sshKeysEnabled = await this.configService.getFeatureFlag(FeatureFlag.SSHKeyVaultItem);
if (this.platformUtilsService.getClientType() == ClientType.Desktop && sshKeysEnabled) {
if (sshKeysEnabled) {
this.typeOptions.push({ name: this.i18nService.t("typeSshKey"), value: CipherType.SshKey });
}
}
@ -339,6 +343,17 @@ export class AddEditComponent implements OnInit, OnDestroy {
[this.collectionId as CollectionId],
this.isAdminConsoleAction,
);
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);
}
}
}
async submit(): Promise<boolean> {
@ -786,4 +801,26 @@ export class AddEditComponent implements OnInit, OnDestroy {
return true;
}
private async generateSshKey(showNotification: boolean = true) {
await firstValueFrom(this.sdkService.client$);
const sshKey = generate_ssh_key("Ed25519");
this.cipher.sshKey.privateKey = sshKey.private_key;
this.cipher.sshKey.publicKey = sshKey.public_key;
this.cipher.sshKey.keyFingerprint = sshKey.key_fingerprint;
if (showNotification) {
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("sshKeyGenerated"),
});
}
}
async typeChange() {
if (this.cipher.type === CipherType.SshKey) {
await this.generateSshKey();
}
}
}

View File

@ -2,11 +2,15 @@
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, Input, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view";
import {
CardComponent,
FormFieldModule,
@ -16,6 +20,7 @@ import {
SelectModule,
TypographyModule,
} from "@bitwarden/components";
import { generate_ssh_key } from "@bitwarden/sdk-internal";
import { CipherFormContainer } from "../../cipher-form-container";
@ -50,20 +55,35 @@ export class SshKeySectionComponent implements OnInit {
* leaving as just null gets inferred as `unknown`
*/
sshKeyForm = this.formBuilder.group({
privateKey: null as string | null,
publicKey: null as string | null,
keyFingerprint: null as string | null,
privateKey: [""],
publicKey: [""],
keyFingerprint: [""],
});
constructor(
private cipherFormContainer: CipherFormContainer,
private formBuilder: FormBuilder,
private i18nService: I18nService,
) {}
private sdkService: SdkService,
) {
this.cipherFormContainer.registerChildForm("sshKeyDetails", this.sshKeyForm);
this.sshKeyForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => {
const data = new SshKeyView();
data.privateKey = value.privateKey;
data.publicKey = value.publicKey;
data.keyFingerprint = value.keyFingerprint;
this.cipherFormContainer.patchCipher((cipher) => {
cipher.sshKey = data;
return cipher;
});
});
}
ngOnInit() {
if (this.originalCipherView?.card) {
async ngOnInit() {
if (this.originalCipherView?.sshKey) {
this.setInitialValues();
} else {
await this.generateSshKey();
}
this.sshKeyForm.disable();
@ -79,4 +99,14 @@ export class SshKeySectionComponent implements OnInit {
keyFingerprint,
});
}
private async generateSshKey() {
await firstValueFrom(this.sdkService.client$);
const sshKey = generate_ssh_key("Ed25519");
this.sshKeyForm.setValue({
privateKey: sshKey.private_key,
publicKey: sshKey.public_key,
keyFingerprint: sshKey.key_fingerprint,
});
}
}