From 1b4717a78fbd61d040f84c685f77242d463c21e7 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 3 Jan 2024 19:20:17 +0100 Subject: [PATCH] [PM-3343] Capture TOTP QR codes from websites in the browser extension (#5985) * Implement totp capture for browser extensions --- apps/browser/src/_locales/en/messages.json | 15 ++++++ apps/browser/src/manifest.json | 1 + apps/browser/src/manifest.v3.json | 1 + .../src/platform/browser/browser-api.ts | 6 +++ .../components/vault/add-edit.component.html | 49 ++++++++++++++----- .../components/vault/add-edit.component.ts | 24 +++++++++ .../src/vault/app/vault/add-edit.component.ts | 2 + .../emergency-add-edit-cipher.component.ts | 2 + .../individual-vault/add-edit.component.ts | 8 ++- .../app/vault/org-vault/add-edit.component.ts | 2 + .../vault/components/add-edit.component.ts | 25 ++++++++++ package-lock.json | 14 ++++++ package.json | 1 + 13 files changed, 136 insertions(+), 14 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index dfbda4c1b2..9fb10bb0ba 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -326,6 +326,9 @@ "password": { "message": "Password" }, + "totp": { + "message": "Authenticator secret" + }, "passphrase": { "message": "Passphrase" }, @@ -512,6 +515,18 @@ "autofillError": { "message": "Unable to auto-fill the selected item on this page. Copy and paste the information instead." }, + "totpCaptureError": { + "message": "Unable to scan QR code from the current webpage" + }, + "totpCaptureSuccess": { + "message": "Authenticator key added" + }, + "totpCapture": { + "message": "Scan authenticator QR code from current webpage" + }, + "copyTOTP": { + "message": "Copy Authenticator key (TOTP)" + }, "loggedOut": { "message": "Logged out" }, diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 7022b38962..41d70d3116 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -56,6 +56,7 @@ "default_popup": "popup/index.html" }, "permissions": [ + "", "tabs", "contextMenus", "storage", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index dad4524c4e..67f65599e0 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -59,6 +59,7 @@ "default_popup": "popup/index.html" }, "permissions": [ + "", "tabs", "contextMenus", "storage", diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index aaab7e113a..229d993fb6 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -413,6 +413,12 @@ export class BrowserApi { return win.opr?.sidebarAction || browser.sidebarAction; } + static captureVisibleTab(): Promise { + return new Promise((resolve) => { + chrome.tabs.captureVisibleTab(null, { format: "png" }, resolve); + }); + } + /** * Extension API helper method used to execute a script in a tab. * @see https://developer.chrome.com/docs/extensions/reference/tabs/#method-executeScript diff --git a/apps/browser/src/vault/popup/components/vault/add-edit.component.html b/apps/browser/src/vault/popup/components/vault/add-edit.component.html index 78635ac482..33f3fc3a4f 100644 --- a/apps/browser/src/vault/popup/components/vault/add-edit.component.html +++ b/apps/browser/src/vault/popup/components/vault/add-edit.component.html @@ -146,20 +146,45 @@ -
- - +
+
+ + +
+
+ + +
+
diff --git a/apps/browser/src/vault/popup/components/vault/add-edit.component.ts b/apps/browser/src/vault/popup/components/vault/add-edit.component.ts index 8ada1a36df..2f89d09b8c 100644 --- a/apps/browser/src/vault/popup/components/vault/add-edit.component.ts +++ b/apps/browser/src/vault/popup/components/vault/add-edit.component.ts @@ -1,6 +1,7 @@ import { DatePipe, Location } from "@angular/common"; import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; +import qrcodeParser from "qrcode-parser"; import { firstValueFrom } from "rxjs"; import { first } from "rxjs/operators"; @@ -84,6 +85,7 @@ export class AddEditComponent extends BaseAddEditComponent { organizationService, sendApiService, dialogService, + window, datePipe, ); } @@ -342,4 +344,26 @@ export class AddEditComponent extends BaseAddEditComponent { this.singleActionKey || VaultPopoutType.addEditVaultItem, ); } + + async captureTOTPFromTab() { + try { + const screenshot = await BrowserApi.captureVisibleTab(); + const data = await qrcodeParser(screenshot); + const url = new URL(data.toString()); + if (url.protocol == "otpauth:" && url.searchParams.has("secret")) { + this.cipher.login.totp = data.toString(); + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("totpCaptureSuccess") + ); + } + } catch (e) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("totpCaptureError") + ); + } + } } diff --git a/apps/desktop/src/vault/app/vault/add-edit.component.ts b/apps/desktop/src/vault/app/vault/add-edit.component.ts index d786ba21d2..31f7abddd7 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.ts +++ b/apps/desktop/src/vault/app/vault/add-edit.component.ts @@ -47,6 +47,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnChanges, organizationService: OrganizationService, sendApiService: SendApiService, dialogService: DialogService, + window: Window, datePipe: DatePipe, ) { super( @@ -65,6 +66,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnChanges, organizationService, sendApiService, dialogService, + window, datePipe, ); } diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts index 146d12e734..29c13d2289 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts @@ -49,6 +49,7 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent { logService: LogService, sendApiService: SendApiService, dialogService: DialogService, + window: Window, datePipe: DatePipe, ) { super( @@ -69,6 +70,7 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent { passwordRepromptService, sendApiService, dialogService, + window, datePipe, ); } diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.ts b/apps/web/src/app/vault/individual-vault/add-edit.component.ts index 839555f697..3ebe84f2b5 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/individual-vault/add-edit.component.ts @@ -61,6 +61,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On passwordRepromptService: PasswordRepromptService, sendApiService: SendApiService, dialogService: DialogService, + window: Window, datePipe: DatePipe, ) { super( @@ -79,6 +80,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On organizationService, sendApiService, dialogService, + window, datePipe, ); } @@ -142,9 +144,9 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On this.platformUtilsService.launchUri(uri.launchUri); } - copy(value: string, typeI18nKey: string, aType: string) { + async copy(value: string, typeI18nKey: string, aType: string): Promise { if (value == null) { - return; + return false; } this.platformUtilsService.copyToClipboard(value, { window: window }); @@ -166,6 +168,8 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On ); } } + + return true; } async generatePassword(): Promise { diff --git a/apps/web/src/app/vault/org-vault/add-edit.component.ts b/apps/web/src/app/vault/org-vault/add-edit.component.ts index 551e21263d..96d498de9a 100644 --- a/apps/web/src/app/vault/org-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/org-vault/add-edit.component.ts @@ -51,6 +51,7 @@ export class AddEditComponent extends BaseAddEditComponent { organizationService: OrganizationService, sendApiService: SendApiService, dialogService: DialogService, + window: Window, datePipe: DatePipe, ) { super( @@ -71,6 +72,7 @@ export class AddEditComponent extends BaseAddEditComponent { passwordRepromptService, sendApiService, dialogService, + window, datePipe, ); } diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 6b903383fb..eac1c4c518 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -111,6 +111,7 @@ export class AddEditComponent implements OnInit, OnDestroy { private organizationService: OrganizationService, protected sendApiService: SendApiService, protected dialogService: DialogService, + protected win: Window, protected datePipe: DatePipe, ) { this.typeOptions = [ @@ -653,4 +654,28 @@ export class AddEditComponent implements OnInit, OnDestroy { return loadedSavedInfo; } + + async copy(value: string, typeI18nKey: string, aType: string): Promise { + if (value == null) { + return false; + } + + const copyOptions = this.win != null ? { window: this.win } : null; + this.platformUtilsService.copyToClipboard(value, copyOptions); + this.platformUtilsService.showToast( + "info", + null, + this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)) + ); + + if (typeI18nKey === "password") { + this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, this.cipherId); + } else if (typeI18nKey === "securityCode") { + this.eventCollectionService.collect(EventType.Cipher_ClientCopiedCardCode, this.cipherId); + } else if (aType === "H_Field") { + this.eventCollectionService.collect(EventType.Cipher_ClientCopiedHiddenField, this.cipherId); + } + + return true; + } } diff --git a/package-lock.json b/package-lock.json index 182a7bd6a0..20655f45ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,6 +62,7 @@ "patch-package": "8.0.0", "popper.js": "1.16.1", "proper-lockfile": "4.1.2", + "qrcode-parser": "^2.1.3", "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", @@ -26776,6 +26777,11 @@ "node >= 0.2.0" ] }, + "node_modules/jsqr": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz", + "integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==" + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -32998,6 +33004,14 @@ ], "peer": true }, + "node_modules/qrcode-parser": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/qrcode-parser/-/qrcode-parser-2.1.3.tgz", + "integrity": "sha512-tyakoHUQXCjH1+RGiqxH3/6XqbQuXuSaW0CkUp1AlYT0+XA4ndG7bxxyyWpdnr0Z2Vuv0GRwgKSq6sOzNiQfog==", + "dependencies": { + "jsqr": "^1.4.0" + } + }, "node_modules/qrious": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/qrious/-/qrious-4.0.2.tgz", diff --git a/package.json b/package.json index 904868a056..cfd919bb1f 100644 --- a/package.json +++ b/package.json @@ -197,6 +197,7 @@ "patch-package": "8.0.0", "popper.js": "1.16.1", "proper-lockfile": "4.1.2", + "qrcode-parser": "^2.1.3", "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0",