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:
@@ -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"];
|
||||
};
|
||||
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user