From 8d08bf797aac086b15fd63c041d69025e24e72f0 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Tue, 9 Jul 2024 16:40:45 -0700 Subject: [PATCH] [PM-8524] Introduce TotpCaptureService and the Browser implementation --- .../add-edit/add-edit-v2.component.ts | 7 +++- .../services/browser-totp-capture.service.ts | 23 +++++++++++++ .../abstractions/totp-capture.service.ts | 9 +++++ .../login-details-section.component.ts | 33 ++++++++++++++++--- libs/vault/src/cipher-form/index.ts | 1 + 5 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 apps/browser/src/vault/popup/services/browser-totp-capture.service.ts create mode 100644 libs/vault/src/cipher-form/abstractions/totp-capture.service.ts diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts index 52f174f9e5..7254f6a7d5 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts @@ -17,11 +17,13 @@ import { CipherFormMode, CipherFormModule, DefaultCipherFormConfigService, + TotpCaptureService, } from "@bitwarden/vault"; import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component"; import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component"; +import { BrowserTotpCaptureService } from "../../../services/browser-totp-capture.service"; import { OpenAttachmentsComponent } from "../attachments/open-attachments/open-attachments.component"; /** @@ -80,7 +82,10 @@ export type AddEditQueryParams = Partial>; selector: "app-add-edit-v2", templateUrl: "add-edit-v2.component.html", standalone: true, - providers: [{ provide: CipherFormConfigService, useClass: DefaultCipherFormConfigService }], + providers: [ + { provide: CipherFormConfigService, useClass: DefaultCipherFormConfigService }, + { provide: TotpCaptureService, useClass: BrowserTotpCaptureService }, + ], imports: [ CommonModule, SearchModule, diff --git a/apps/browser/src/vault/popup/services/browser-totp-capture.service.ts b/apps/browser/src/vault/popup/services/browser-totp-capture.service.ts new file mode 100644 index 0000000000..3f8ba61ed3 --- /dev/null +++ b/apps/browser/src/vault/popup/services/browser-totp-capture.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from "@angular/core"; +import qrcodeParser from "qrcode-parser"; + +import { TotpCaptureService } from "@bitwarden/vault"; + +import { BrowserApi } from "../../../platform/browser/browser-api"; + +/** + * Implementation of TotpCaptureService for the browser which captures the + * TOTP secret from the currently visible tab. + */ +@Injectable() +export class BrowserTotpCaptureService implements TotpCaptureService { + async captureTotpSecret() { + const screenshot = await BrowserApi.captureVisibleTab(); + const data = await qrcodeParser(screenshot); + const url = new URL(data.toString()); + if (url.protocol === "otpauth:" && url.searchParams.has("secret")) { + return data.toString(); + } + return null; + } +} diff --git a/libs/vault/src/cipher-form/abstractions/totp-capture.service.ts b/libs/vault/src/cipher-form/abstractions/totp-capture.service.ts new file mode 100644 index 0000000000..d6d9556586 --- /dev/null +++ b/libs/vault/src/cipher-form/abstractions/totp-capture.service.ts @@ -0,0 +1,9 @@ +/** + * Service to capture TOTP secret from a client application. + */ +export abstract class TotpCaptureService { + /** + * Captures a TOTP secret and returns it as a string. Returns null if no TOTP secret was found. + */ + abstract captureTotpSecret(): Promise; +} diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts index f4dfed4b44..bd6e2ee7b7 100644 --- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts @@ -1,5 +1,5 @@ import { DatePipe, NgIf } from "@angular/common"; -import { Component, inject, OnInit } from "@angular/core"; +import { Component, inject, OnInit, Optional } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; import { map } from "rxjs"; @@ -15,10 +15,12 @@ import { PopoverModule, SectionComponent, SectionHeaderComponent, + ToastService, TypographyModule, } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; +import { TotpCaptureService } from "../../abstractions/totp-capture.service"; import { CipherFormContainer } from "../../cipher-form-container"; @Component({ @@ -47,11 +49,11 @@ export class LoginDetailsSectionComponent implements OnInit { }); /** - * Whether the TOTP field can be captured from the current tab. Only available in the web extension. + * Whether the TOTP field can be captured from the current tab. Only available in the browser extension. * @protected */ protected get canCaptureTotp() { - return false; //BrowserApi.isWebExtensionsApi && this.loginDetailsForm.controls.totp.enabled; + return this.totpCaptureService != null && this.loginDetailsForm.controls.totp.enabled; } private datePipe = inject(DatePipe); @@ -83,6 +85,8 @@ export class LoginDetailsSectionComponent implements OnInit { private formBuilder: FormBuilder, private i18nService: I18nService, private generatorService: PasswordGenerationServiceAbstraction, + private toastService: ToastService, + @Optional() private totpCaptureService?: TotpCaptureService, ) { this.cipherFormContainer.registerChildForm("loginDetails", this.loginDetailsForm); @@ -136,7 +140,28 @@ export class LoginDetailsSectionComponent implements OnInit { this.loginDetailsForm.controls.password.patchValue(await this.generateNewPassword()); } - captureTotpFromTab = async () => {}; + captureTotpFromTab = async () => { + if (!this.canCaptureTotp) { + return; + } + try { + const totp = await this.totpCaptureService.captureTotpSecret(); + if (totp) { + this.loginDetailsForm.controls.totp.patchValue(totp); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("totpCaptureSuccess"), + }); + } + } catch { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("totpCaptureError"), + }); + } + }; removePasskey = async () => { // Fido2Credentials do not have a form control, so update directly diff --git a/libs/vault/src/cipher-form/index.ts b/libs/vault/src/cipher-form/index.ts index 4cc762ffb6..7c9032435e 100644 --- a/libs/vault/src/cipher-form/index.ts +++ b/libs/vault/src/cipher-form/index.ts @@ -5,4 +5,5 @@ export { CipherFormMode, OptionalInitialValues, } from "./abstractions/cipher-form-config.service"; +export { TotpCaptureService } from "./abstractions/totp-capture.service"; export { DefaultCipherFormConfigService } from "./services/default-cipher-form-config.service";