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:
parent
3949aae8e3
commit
bb2961f4ca
@ -567,7 +567,7 @@ const safeProviders: SafeProvider[] = [
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SdkClientFactory,
|
||||
useFactory: (logService) =>
|
||||
useFactory: (logService: LogService) =>
|
||||
flagEnabled("sdk") ? new BrowserSdkClientFactory(logService) : new NoopSdkClientFactory(),
|
||||
deps: [LogService],
|
||||
}),
|
||||
|
@ -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>
|
||||
|
@ -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(),
|
||||
|
@ -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 } = {};
|
||||
|
24
apps/desktop/desktop_native/Cargo.lock
generated
24
apps/desktop/desktop_native/Cargo.lock
generated
@ -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",
|
||||
|
@ -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 }
|
||||
|
@ -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(),
|
||||
})
|
||||
}
|
@ -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)]
|
||||
|
1
apps/desktop/desktop_native/napi/index.d.ts
vendored
1
apps/desktop/desktop_native/napi/index.d.ts
vendored
@ -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 {
|
||||
|
@ -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]
|
||||
|
@ -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 (
|
||||
|
@ -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");
|
||||
},
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user