1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-03 17:13:47 +00:00

PM-10393 SSH keys (#10825)

* [PM-10395] Add new item type ssh key (#10360)

* Implement ssh-key cipher type

* Fix linting

* Fix edit and view components for ssh-keys on desktop

* Fix tests

* Remove ssh key type references

* Remove add ssh key option

* Fix typo

* Add tests

* [PM-10399] Add ssh key import export for bitwarden json (#10529)

* Add ssh key import export for bitwarden json

* Remove key type from ssh key export

* [PM-10406] Add privatekey publickey and fingerprint to both add-edit and view co… (#11046)

* Add privatekey publickey and fingerprint to both add-edit and view components

* Remove wrong a11y title

* Fix testid

* [PM-10098] SSH Agent & SSH Key creation for Bitwarden Desktop (#10293)

* Add ssh agent, generator & import

* Move ssh agent code to bitwarden-russh crate

* Remove generator component

* Cleanup

* Cleanup

* Remove left over sshGenerator reference

* Cleanup

* Add documentation to sshkeyimportstatus

* Fix outdated variable name

* Update apps/desktop/src/platform/preload.ts

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Rename renderersshagent

* Rename MainSshAgentService

* Improve clarity of 'id' variables being used

* Improve clarity of 'id' variables being used

* Update apps/desktop/src/vault/app/vault/add-edit.component.html

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Fix outdated cipher/messageid names

* Rename SSH to Ssh

* Make agent syncing more reactive

* Move constants to top of class

* Make sshkey cipher filtering clearer

* Add stricter equality check on ssh key unlock

* Fix build and messages

* Fix incorrect featureflag name

* Replace anonymous async function with switchmap pipe

* Fix build

* Update apps/desktop/desktop_native/napi/src/lib.rs

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Revert incorrectly renamed 'Ssh' usages to SSH

* Run cargo fmt

* Clean up ssh agent sock path logic

* Cleanup and split to platform specific files

* Small cleanup

* Pull out generator and importer into core

* Rename renderersshagentservice to sshagentservice

* Rename cipheruuid to cipher_id

* Drop ssh dependencies from napi crate

* Clean up windows build

* Small cleanup

* Small cleanup

* Cleanup

* Add rxjs pipeline for agent services

* [PM-12555] Pkcs8 sshkey import & general ssh key import tests (#11048)

* Add pkcs8 import and tests

* Add key type unsupported error

* Remove unsupported formats

* Remove code for unsupported formats

* Fix encrypted pkcs8 import

* Add ed25519 pkcs8 unencrypted test file

* SSH agent rxjs tweaks (#11148)

* feat: rewrite sshagent.signrequest as purely observable

* feat: fail the request when unlock times out

* chore: clean up, add some clarifying comments

* chore: remove unused dependency

* fix: result `undefined` crashing in NAPI -> Rust

* Allow concurrent SSH requests in rust

* Remove unwraps

* Cleanup and add init service init call

* Fix windows

* Fix timeout behavior on locked vault

---------

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Fix libc dependency being duplicated

* fix SSH casing (#11840)

* Move ssh agent behind feature flag (#11841)

* Move ssh agent behind feature flag

* Add separate flag for ssh agent

* [PM-14215] fix unsupported key type error message (#11788)

* Fix error message for import of unsupported ssh keys

* Use triple equals in add-edit component for ssh keys

---------

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
Co-authored-by: aj-bw <81774843+aj-bw@users.noreply.github.com>
This commit is contained in:
Bernd Schoolmann
2024-11-08 11:01:31 +01:00
committed by GitHub
parent 2c914def29
commit 081fe83d83
98 changed files with 3572 additions and 60 deletions

View File

@@ -8,6 +8,7 @@ import { CustomFieldsComponent } from "./components/custom-fields/custom-fields.
import { IdentitySectionComponent } from "./components/identity/identity.component";
import { ItemDetailsSectionComponent } from "./components/item-details/item-details-section.component";
import { LoginDetailsSectionComponent } from "./components/login-details-section/login-details-section.component";
import { SshKeySectionComponent } from "./components/sshkey-section/sshkey-section.component";
/**
* The complete form for a cipher. Includes all the sub-forms from their respective section components.
@@ -20,6 +21,7 @@ export type CipherForm = {
autoFillOptions?: AutofillOptionsComponent["autofillOptionsForm"];
cardDetails?: CardDetailsSectionComponent["cardDetailsForm"];
identityDetails?: IdentitySectionComponent["identityForm"];
sshKeyDetails?: SshKeySectionComponent["sshKeyForm"];
customFields?: CustomFieldsComponent["customFieldsForm"];
};

View File

@@ -22,6 +22,12 @@
[disabled]="config.mode === 'partial-edit'"
></vault-card-details-section>
<vault-sshkey-section
*ngIf="config.cipherType === CipherType.SshKey"
[disabled]="config.mode === 'partial-edit'"
[originalCipherView]="originalCipherView"
></vault-sshkey-section>
<vault-additional-options-section></vault-additional-options-section>
<!-- Attachments are only available for existing ciphers -->

View File

@@ -42,6 +42,7 @@ import { CardDetailsSectionComponent } from "./card-details-section/card-details
import { IdentitySectionComponent } from "./identity/identity.component";
import { ItemDetailsSectionComponent } from "./item-details/item-details-section.component";
import { LoginDetailsSectionComponent } from "./login-details-section/login-details-section.component";
import { SshKeySectionComponent } from "./sshkey-section/sshkey-section.component";
@Component({
selector: "vault-cipher-form",
@@ -65,6 +66,7 @@ import { LoginDetailsSectionComponent } from "./login-details-section/login-deta
ItemDetailsSectionComponent,
CardDetailsSectionComponent,
IdentitySectionComponent,
SshKeySectionComponent,
NgIf,
AdditionalOptionsSectionComponent,
LoginDetailsSectionComponent,

View File

@@ -0,0 +1,30 @@
<bit-section [formGroup]="sshKeyForm">
<bit-section-header>
<h2 bitTypography="h6">
{{ "typeSshKey" | i18n }}
</h2>
</bit-section-header>
<bit-card>
<bit-form-field>
<bit-label>{{ "sshPrivateKey" | i18n }}</bit-label>
<input id="privateKey" bitInput formControlName="privateKey" type="password" />
<button
type="button"
bitIconButton
bitSuffix
data-testid="toggle-privateKey-visibility"
bitPasswordInputToggle
></button>
</bit-form-field>
<bit-form-field>
<bit-label>{{ "sshPublicKey" | i18n }}</bit-label>
<input id="publicKey" bitInput formControlName="publicKey" />
</bit-form-field>
<bit-form-field>
<bit-label>{{ "sshFingerprint" | i18n }}</bit-label>
<input id="keyFingerprint" bitInput formControlName="keyFingerprint" />
</bit-form-field>
</bit-card>
</bit-section>

View File

@@ -0,0 +1,80 @@
import { CommonModule } from "@angular/common";
import { Component, Input, OnInit } from "@angular/core";
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
CardComponent,
FormFieldModule,
IconButtonModule,
SectionComponent,
SectionHeaderComponent,
SelectModule,
TypographyModule,
} from "@bitwarden/components";
import { CipherFormContainer } from "../../cipher-form-container";
@Component({
selector: "vault-sshkey-section",
templateUrl: "./sshkey-section.component.html",
standalone: true,
imports: [
CardComponent,
SectionComponent,
TypographyModule,
FormFieldModule,
ReactiveFormsModule,
SelectModule,
SectionHeaderComponent,
IconButtonModule,
JslibModule,
CommonModule,
],
})
export class SshKeySectionComponent implements OnInit {
/** The original cipher */
@Input() originalCipherView: CipherView;
/** True when all fields should be disabled */
@Input() disabled: boolean;
/**
* All form fields associated with the ssh key
*
* Note: `as` is used to assert the type of the form control,
* 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,
});
constructor(
private cipherFormContainer: CipherFormContainer,
private formBuilder: FormBuilder,
private i18nService: I18nService,
) {}
ngOnInit() {
if (this.originalCipherView?.card) {
this.setInitialValues();
}
this.sshKeyForm.disable();
}
/** Set form initial form values from the current cipher */
private setInitialValues() {
const { privateKey, publicKey, keyFingerprint } = this.originalCipherView.sshKey;
this.sshKeyForm.setValue({
privateKey,
publicKey,
keyFingerprint,
});
}
}

View File

@@ -40,6 +40,9 @@
<app-view-identity-sections *ngIf="cipher.identity" [cipher]="cipher">
</app-view-identity-sections>
<!-- SshKEY SECTIONS -->
<app-sshkey-view *ngIf="hasSshKey" [sshKey]="cipher.sshKey"></app-sshkey-view>
<!-- ADDITIONAL OPTIONS -->
<ng-container *ngIf="cipher.notes">
<app-additional-options [notes]="cipher.notes"> </app-additional-options>

View File

@@ -21,6 +21,7 @@ import { CustomFieldV2Component } from "./custom-fields/custom-fields-v2.compone
import { ItemDetailsV2Component } from "./item-details/item-details-v2.component";
import { ItemHistoryV2Component } from "./item-history/item-history-v2.component";
import { LoginCredentialsViewComponent } from "./login-credentials/login-credentials-view.component";
import { SshKeyViewComponent } from "./sshkey-sections/sshkey-view.component";
import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-identity-sections.component";
@Component({
@@ -38,6 +39,7 @@ import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-ide
ItemHistoryV2Component,
CustomFieldV2Component,
CardDetailsComponent,
SshKeyViewComponent,
ViewIdentitySectionsComponent,
LoginCredentialsViewComponent,
AutofillOptionsViewComponent,
@@ -95,6 +97,10 @@ export class CipherViewComponent implements OnChanges, OnDestroy {
return this.cipher.login?.uris.length > 0;
}
get hasSshKey() {
return this.cipher.sshKey?.privateKey;
}
async loadCipherData() {
// Load collections if not provided and the cipher has collectionIds
if (

View File

@@ -0,0 +1,50 @@
<bit-section>
<bit-section-header>
<h2 bitTypography="h6">{{ "typeSshKey" | i18n }}</h2>
</bit-section-header>
<bit-card class="[&_bit-form-field:last-of-type]:tw-mb-0">
<bit-form-field>
<bit-label>{{ "sshPrivateKey" | i18n }}</bit-label>
<input readonly bitInput [value]="sshKey.privateKey" aria-readonly="true" type="password" />
<button
bitSuffix
type="button"
bitIconButton
bitPasswordInputToggle
data-testid="toggle-privateKey"
></button>
<button
bitIconButton="bwi-clone"
bitSuffix
type="button"
[appCopyClick]="sshKey.privateKey"
showToast
[appA11yTitle]="'copyValue' | i18n"
></button>
</bit-form-field>
<bit-form-field>
<bit-label>{{ "sshPublicKey" | i18n }}</bit-label>
<input readonly bitInput [value]="sshKey.publicKey" aria-readonly="true" />
<button
bitIconButton="bwi-clone"
bitSuffix
type="button"
[appCopyClick]="sshKey.publicKey"
showToast
[appA11yTitle]="'copyValue' | i18n"
></button>
</bit-form-field>
<bit-form-field>
<bit-label>{{ "sshFingerprint" | i18n }}</bit-label>
<input readonly bitInput [value]="sshKey.keyFingerprint" aria-readonly="true" />
<button
bitIconButton="bwi-clone"
bitSuffix
type="button"
[appCopyClick]="sshKey.keyFingerprint"
showToast
[appA11yTitle]="'copyValue' | i18n"
></button>
</bit-form-field>
</bit-card>
</bit-section>

View File

@@ -0,0 +1,35 @@
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view";
import {
CardComponent,
SectionComponent,
SectionHeaderComponent,
TypographyModule,
FormFieldModule,
IconButtonModule,
} from "@bitwarden/components";
import { OrgIconDirective } from "../../components/org-icon.directive";
@Component({
selector: "app-sshkey-view",
templateUrl: "sshkey-view.component.html",
standalone: true,
imports: [
CommonModule,
JslibModule,
CardComponent,
SectionComponent,
SectionHeaderComponent,
TypographyModule,
OrgIconDirective,
FormFieldModule,
IconButtonModule,
],
})
export class SshKeyViewComponent {
@Input() sshKey: SshKeyView;
}

View File

@@ -91,6 +91,12 @@ export class CopyCipherFieldDirective implements OnChanges {
return this.cipher.identity?.fullAddressForCopy;
case "secureNote":
return this.cipher.notes;
case "privateKey":
return this.cipher.sshKey?.privateKey;
case "publicKey":
return this.cipher.sshKey?.publicKey;
case "keyFingerprint":
return this.cipher.sshKey?.keyFingerprint;
default:
return null;
}

View File

@@ -25,7 +25,10 @@ export type CopyAction =
| "phone"
| "address"
| "secureNote"
| "hiddenField";
| "hiddenField"
| "privateKey"
| "publicKey"
| "keyFingerprint";
type CopyActionInfo = {
/**
@@ -62,6 +65,9 @@ const CopyActions: Record<CopyAction, CopyActionInfo> = {
phone: { typeI18nKey: "phone", protected: true },
address: { typeI18nKey: "address", protected: true },
secureNote: { typeI18nKey: "note", protected: true },
privateKey: { typeI18nKey: "sshPrivateKey", protected: true },
publicKey: { typeI18nKey: "sshPublicKey", protected: true },
keyFingerprint: { typeI18nKey: "sshFingerprint", protected: true },
hiddenField: {
typeI18nKey: "value",
protected: true,