1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-08 03:23:50 +00:00

[PM-2241] Add PRF attestation flow during passkey registration (#6525)

* [PM-2241] chore: refactor into new "pending" view type

* [PM-2241] feat: record PRF support

* [PM-2241] feat: add prf checkbox to dialog

* [PM-2241] chore: remove `disableMargin` instead

Will expressed his concern that these things aren't sustainable, and that we should try using `!important` statements instead, which is a good point!

* [PM-2241] feat: add prf registration

* [PM-2241] feat: add support for `prfStatus`

* [PM-2241] feat: add rotateable key set

* [PM-2241] feat: add PRF creation error handling

* [PM-2241] chore: improve rotateable key docs

* [PM-2241] feat: add basic test

* [PM-2241] chore: update `SaveCredentialRequest` docs

* [PM-2241] chore: rename to `WebauthnLoginAdminService`

* [PM-2241] fix: typo in `save-credential.request.ts`

* [PM-2241] fix: typo in more places
This commit is contained in:
Andreas Coroiu
2023-11-08 14:35:36 +01:00
committed by GitHub
parent c7b448cdc8
commit 65d2d74348
27 changed files with 458 additions and 119 deletions

View File

@@ -32,12 +32,12 @@
<p bitTypography="body1">{{ "errorCreatingPasskeyInfo" | i18n }}</p>
</div>
<div *ngIf="currentStep === 'credentialNaming'">
<div *ngIf="currentStep === 'credentialNaming'" formGroupName="credentialNaming">
<h3 bitTypography="h3">{{ "passkeySuccessfullyCreated" | i18n }}</h3>
<p bitTypography="body1">
{{ "customPasskeyNameInfo" | i18n }}
</p>
<bit-form-field disableMargin formGroupName="credentialNaming">
<bit-form-field class="!tw-mb-0">
<bit-label>{{ "customName" | i18n }}</bit-label>
<input type="text" bitInput formControlName="name" appAutofocus />
<bit-hint>{{
@@ -45,6 +45,11 @@
| i18n : formGroup.value.credentialNaming.name.length : NameMaxCharacters
}}</bit-hint>
</bit-form-field>
<bit-form-control *ngIf="pendingCredential?.supportsPrf" class="!tw-mb-0 tw-mt-6">
<input type="checkbox" bitCheckbox formControlName="useForEncryption" />
<bit-label>{{ "useForVaultEncryption" | i18n }}</bit-label>
<bit-hint>{{ "useForVaultEncryptionInfo" | i18n }}</bit-hint>
</bit-form-control>
</div>
</ng-container>
<ng-container bitDialogFooter>

View File

@@ -3,6 +3,7 @@ import { Component, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { firstValueFrom, map, Observable } from "rxjs";
import { PrfKeySet } from "@bitwarden/auth";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -10,8 +11,9 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { Verification } from "@bitwarden/common/types/verification";
import { DialogService } from "@bitwarden/components";
import { WebauthnLoginService } from "../../../core";
import { WebauthnLoginAdminService } from "../../../core";
import { CredentialCreateOptionsView } from "../../../core/views/credential-create-options.view";
import { PendingWebauthnLoginCredentialView } from "../../../core/views/pending-webauthn-login-credential.view";
import { CreatePasskeyFailedIcon } from "./create-passkey-failed.icon";
import { CreatePasskeyIcon } from "./create-passkey.icon";
@@ -42,17 +44,19 @@ export class CreateCredentialDialogComponent implements OnInit {
}),
credentialNaming: this.formBuilder.group({
name: ["", Validators.maxLength(50)],
useForEncryption: [false],
}),
});
protected credentialOptions?: CredentialCreateOptionsView;
protected deviceResponse?: PublicKeyCredential;
protected pendingCredential?: PendingWebauthnLoginCredentialView;
protected hasPasskeys$?: Observable<boolean>;
protected loading$ = this.webauthnService.loading$;
constructor(
private formBuilder: FormBuilder,
private dialogRef: DialogRef,
private webauthnService: WebauthnLoginService,
private webauthnService: WebauthnLoginAdminService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private logService: LogService
@@ -112,8 +116,8 @@ export class CreateCredentialDialogComponent implements OnInit {
}
protected async submitCredentialCreation() {
this.deviceResponse = await this.webauthnService.createCredential(this.credentialOptions);
if (this.deviceResponse === undefined) {
this.pendingCredential = await this.webauthnService.createCredential(this.credentialOptions);
if (this.pendingCredential === undefined) {
this.currentStep = "credentialCreationFailed";
return;
}
@@ -128,16 +132,30 @@ export class CreateCredentialDialogComponent implements OnInit {
protected async submitCredentialNaming() {
this.formGroup.controls.credentialNaming.markAllAsTouched();
if (this.formGroup.controls.credentialNaming.invalid) {
if (this.formGroup.controls.credentialNaming.controls.name.invalid) {
return;
}
let keySet: PrfKeySet | undefined;
if (this.formGroup.value.credentialNaming.useForEncryption) {
keySet = await this.webauthnService.createKeySet(this.pendingCredential);
if (keySet === undefined) {
this.formGroup.controls.credentialNaming.controls.useForEncryption?.setErrors({
error: {
message: this.i18nService.t("useForVaultEncryptionErrorReadingPasskey"),
},
});
return;
}
}
const name = this.formGroup.value.credentialNaming.name;
try {
await this.webauthnService.saveCredential(
this.credentialOptions,
this.deviceResponse,
this.formGroup.value.credentialNaming.name
this.formGroup.value.credentialNaming.name,
this.pendingCredential,
keySet
);
} catch (error) {
this.logService?.error(error);

View File

@@ -10,8 +10,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { Verification } from "@bitwarden/common/types/verification";
import { DialogService } from "@bitwarden/components";
import { WebauthnLoginService } from "../../../core";
import { WebauthnCredentialView } from "../../../core/views/webauth-credential.view";
import { WebauthnLoginAdminService } from "../../../core";
import { WebauthnLoginCredentialView } from "../../../core/views/webauthn-login-credential.view";
export interface DeleteCredentialDialogParams {
credentialId: string;
@@ -27,14 +27,14 @@ export class DeleteCredentialDialogComponent implements OnInit, OnDestroy {
protected formGroup = this.formBuilder.group({
secret: null as Verification | null,
});
protected credential?: WebauthnCredentialView;
protected credential?: WebauthnLoginCredentialView;
protected loading$ = this.webauthnService.loading$;
constructor(
@Inject(DIALOG_DATA) private params: DeleteCredentialDialogParams,
private formBuilder: FormBuilder,
private dialogRef: DialogRef,
private webauthnService: WebauthnLoginService,
private webauthnService: WebauthnLoginAdminService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private logService: LogService

View File

@@ -22,12 +22,20 @@
<table *ngIf="hasCredentials" class="tw-mb-5">
<tr *ngFor="let credential of credentials">
<td class="tw-p-2 tw-pl-0 tw-font-semibold">{{ credential.name }}</td>
<td class="tw-p-2 tw-pr-10">
<ng-container *ngIf="credential.prfSupport">
<td class="tw-p-2 tw-pr-10 tw-text-left">
<ng-container *ngIf="credential.prfStatus === WebauthnLoginCredentialPrfStatus.Enabled">
<i class="bwi bwi-lock-encrypted"></i>
{{ "supportsEncryption" | i18n }}
<span bitTypography="body1" class="tw-text-muted">{{ "usedForEncryption" | i18n }}</span>
</ng-container>
<span bitTypography="body1" class="tw-text-muted">
<ng-container *ngIf="credential.prfStatus === WebauthnLoginCredentialPrfStatus.Supported">
<i class="bwi bwi-lock-encrypted"></i>
<span bitTypography="body1" class="tw-text-muted">{{ "encryptionNotEnabled" | i18n }}</span>
</ng-container>
<span
*ngIf="credential.prfStatus === WebauthnLoginCredentialPrfStatus.Unsupported"
bitTypography="body1"
class="tw-text-muted"
>
{{ "encryptionNotSupported" | i18n }}
</span>
</td>

View File

@@ -3,8 +3,9 @@ import { Subject, takeUntil } from "rxjs";
import { DialogService } from "@bitwarden/components";
import { WebauthnLoginService } from "../../core";
import { WebauthnCredentialView } from "../../core/views/webauth-credential.view";
import { WebauthnLoginAdminService } from "../../core";
import { WebauthnLoginCredentialPrfStatus } from "../../core/enums/webauthn-login-credential-prf-status.enum";
import { WebauthnLoginCredentialView } from "../../core/views/webauthn-login-credential.view";
import { openCreateCredentialDialog } from "./create-credential-dialog/create-credential-dialog.component";
import { openDeleteCredentialDialogComponent } from "./delete-credential-dialog/delete-credential-dialog.component";
@@ -19,13 +20,14 @@ import { openDeleteCredentialDialogComponent } from "./delete-credential-dialog/
export class WebauthnLoginSettingsComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
protected readonly MaxCredentialCount = WebauthnLoginService.MaxCredentialCount;
protected readonly MaxCredentialCount = WebauthnLoginAdminService.MaxCredentialCount;
protected readonly WebauthnLoginCredentialPrfStatus = WebauthnLoginCredentialPrfStatus;
protected credentials?: WebauthnCredentialView[];
protected credentials?: WebauthnLoginCredentialView[];
protected loading = true;
constructor(
private webauthnService: WebauthnLoginService,
private webauthnService: WebauthnLoginAdminService,
private dialogService: DialogService
) {}

View File

@@ -1,6 +1,8 @@
import { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { CheckboxModule } from "@bitwarden/components";
import { SharedModule } from "../../../shared/shared.module";
import { UserVerificationModule } from "../../shared/components/user-verification";
@@ -9,7 +11,7 @@ import { DeleteCredentialDialogComponent } from "./delete-credential-dialog/dele
import { WebauthnLoginSettingsComponent } from "./webauthn-login-settings.component";
@NgModule({
imports: [SharedModule, FormsModule, ReactiveFormsModule, UserVerificationModule],
imports: [SharedModule, FormsModule, ReactiveFormsModule, UserVerificationModule, CheckboxModule],
declarations: [
WebauthnLoginSettingsComponent,
CreateCredentialDialogComponent,