mirror of
https://github.com/bitwarden/browser
synced 2026-02-22 20:34:04 +00:00
231 lines
7.4 KiB
TypeScript
231 lines
7.4 KiB
TypeScript
import { CommonModule } from "@angular/common";
|
|
import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from "@angular/core";
|
|
import { RouterModule, Router } from "@angular/router";
|
|
import { combineLatest, map, Observable, Subject, switchMap } from "rxjs";
|
|
|
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
|
import { BitwardenShield, NoResults } from "@bitwarden/assets/svg";
|
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
|
import { Fido2Utils } from "@bitwarden/common/platform/services/fido2/fido2-utils";
|
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
|
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
|
import {
|
|
DialogService,
|
|
BadgeModule,
|
|
ButtonModule,
|
|
DialogModule,
|
|
IconModule,
|
|
ItemModule,
|
|
SectionComponent,
|
|
TableModule,
|
|
SectionHeaderComponent,
|
|
BitIconButtonComponent,
|
|
SimpleDialogOptions,
|
|
} from "@bitwarden/components";
|
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
|
|
|
import { DesktopAutofillService } from "../../../autofill/services/desktop-autofill.service";
|
|
import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service";
|
|
import {
|
|
DesktopFido2UserInterfaceService,
|
|
DesktopFido2UserInterfaceSession,
|
|
} from "../../services/desktop-fido2-user-interface.service";
|
|
|
|
@Component({
|
|
standalone: true,
|
|
imports: [
|
|
CommonModule,
|
|
RouterModule,
|
|
SectionHeaderComponent,
|
|
BitIconButtonComponent,
|
|
TableModule,
|
|
JslibModule,
|
|
IconModule,
|
|
ButtonModule,
|
|
DialogModule,
|
|
SectionComponent,
|
|
ItemModule,
|
|
BadgeModule,
|
|
],
|
|
templateUrl: "fido2-create.component.html",
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
})
|
|
export class Fido2CreateComponent implements OnInit, OnDestroy {
|
|
session?: DesktopFido2UserInterfaceSession = null;
|
|
ciphers$: Observable<CipherView[]>;
|
|
private destroy$ = new Subject<void>();
|
|
readonly Icons = { BitwardenShield, NoResults };
|
|
|
|
private get DIALOG_MESSAGES() {
|
|
return {
|
|
unexpectedErrorShort: {
|
|
title: { key: "unexpectedErrorShort" },
|
|
content: { key: "closeThisBitwardenWindow" },
|
|
type: "danger",
|
|
acceptButtonText: { key: "closeThisWindow" },
|
|
cancelButtonText: null as null,
|
|
acceptAction: async () => this.dialogService.closeAll(),
|
|
},
|
|
unableToSavePasskey: {
|
|
title: { key: "unableToSavePasskey" },
|
|
content: { key: "closeThisBitwardenWindow" },
|
|
type: "danger",
|
|
acceptButtonText: { key: "closeThisWindow" },
|
|
cancelButtonText: null as null,
|
|
acceptAction: async () => this.dialogService.closeAll(),
|
|
},
|
|
overwritePasskey: {
|
|
title: { key: "overwritePasskey" },
|
|
content: { key: "alreadyContainsPasskey" },
|
|
type: "warning",
|
|
},
|
|
} as const satisfies Record<string, SimpleDialogOptions>;
|
|
}
|
|
|
|
constructor(
|
|
private readonly desktopSettingsService: DesktopSettingsService,
|
|
private readonly fido2UserInterfaceService: DesktopFido2UserInterfaceService,
|
|
private readonly accountService: AccountService,
|
|
private readonly cipherService: CipherService,
|
|
private readonly desktopAutofillService: DesktopAutofillService,
|
|
private readonly dialogService: DialogService,
|
|
private readonly domainSettingsService: DomainSettingsService,
|
|
private readonly passwordRepromptService: PasswordRepromptService,
|
|
private readonly router: Router,
|
|
) {}
|
|
|
|
async ngOnInit(): Promise<void> {
|
|
this.session = this.fido2UserInterfaceService.getCurrentSession();
|
|
|
|
if (this.session) {
|
|
const rpid = await this.session.getRpId();
|
|
this.initializeCiphersObservable(rpid);
|
|
} else {
|
|
await this.showErrorDialog(this.DIALOG_MESSAGES.unableToSavePasskey);
|
|
}
|
|
}
|
|
|
|
async ngOnDestroy(): Promise<void> {
|
|
this.destroy$.next();
|
|
this.destroy$.complete();
|
|
await this.closeModal();
|
|
}
|
|
|
|
async addCredentialToCipher(cipher: CipherView): Promise<void> {
|
|
const isConfirmed = await this.validateCipherAccess(cipher);
|
|
|
|
try {
|
|
if (!this.session) {
|
|
throw new Error("Missing session");
|
|
}
|
|
|
|
this.session.notifyConfirmCreateCredential(isConfirmed, cipher);
|
|
} catch {
|
|
await this.showErrorDialog(this.DIALOG_MESSAGES.unableToSavePasskey);
|
|
return;
|
|
}
|
|
|
|
// If we want to hide the UI while prompting for UV from the OS, we cannot call closeModal().
|
|
// await this.closeModal();
|
|
}
|
|
|
|
async confirmPasskey(): Promise<void> {
|
|
try {
|
|
if (!this.session) {
|
|
throw new Error("Missing session");
|
|
}
|
|
|
|
const username = await this.session.getUserName();
|
|
const isConfirmed = await this.session.promptForUserVerification(
|
|
username,
|
|
"Verify it's you to create a new credential",
|
|
);
|
|
this.session.notifyConfirmCreateCredential(isConfirmed);
|
|
} catch {
|
|
await this.showErrorDialog(this.DIALOG_MESSAGES.unableToSavePasskey);
|
|
return;
|
|
}
|
|
|
|
await this.closeModal();
|
|
}
|
|
|
|
async closeModal(): Promise<void> {
|
|
await this.desktopSettingsService.setModalMode(false);
|
|
await this.accountService.setShowHeader(true);
|
|
|
|
if (this.session) {
|
|
this.session.notifyConfirmCreateCredential(false);
|
|
this.session.confirmChosenCipher(null);
|
|
}
|
|
|
|
await this.router.navigate(["/"]);
|
|
}
|
|
|
|
private initializeCiphersObservable(rpid: string): void {
|
|
const lastRegistrationRequest = this.desktopAutofillService.lastRegistrationRequest;
|
|
|
|
if (!lastRegistrationRequest || !rpid) {
|
|
return;
|
|
}
|
|
|
|
const userHandle = Fido2Utils.bufferToString(
|
|
new Uint8Array(lastRegistrationRequest.userHandle),
|
|
);
|
|
|
|
this.ciphers$ = combineLatest([
|
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
|
this.domainSettingsService.getUrlEquivalentDomains(rpid),
|
|
]).pipe(
|
|
switchMap(async ([activeUserId, equivalentDomains]) => {
|
|
if (!activeUserId) {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
const allCiphers = await this.cipherService.getAllDecrypted(activeUserId);
|
|
return allCiphers.filter(
|
|
(cipher) =>
|
|
cipher != null &&
|
|
cipher.type == CipherType.Login &&
|
|
cipher.login?.matchesUri(rpid, equivalentDomains) &&
|
|
Fido2Utils.cipherHasNoOtherPasskeys(cipher, userHandle) &&
|
|
!cipher.deletedDate,
|
|
);
|
|
} catch {
|
|
await this.showErrorDialog(this.DIALOG_MESSAGES.unexpectedErrorShort);
|
|
return [];
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
private async validateCipherAccess(cipher: CipherView): Promise<boolean> {
|
|
if (cipher.login.hasFido2Credentials) {
|
|
const overwriteConfirmed = await this.dialogService.openSimpleDialog(
|
|
this.DIALOG_MESSAGES.overwritePasskey,
|
|
);
|
|
|
|
if (!overwriteConfirmed) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (cipher.reprompt) {
|
|
return this.passwordRepromptService.showPasswordPrompt();
|
|
}
|
|
|
|
const username = cipher.login.username ?? cipher.name;
|
|
return this.session.promptForUserVerification(
|
|
username,
|
|
"Verify it's you to overwrite a credential",
|
|
);
|
|
}
|
|
|
|
private async showErrorDialog(config: SimpleDialogOptions): Promise<void> {
|
|
await this.dialogService.openSimpleDialog(config);
|
|
await this.closeModal();
|
|
}
|
|
}
|