[PM-3343] Capture TOTP QR codes from websites in the browser extension (#5985)

* Implement totp capture for browser extensions
This commit is contained in:
Bernd Schoolmann 2024-01-03 19:20:17 +01:00 committed by GitHub
parent 364e23d8a5
commit 1b4717a78f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 136 additions and 14 deletions

View File

@ -326,6 +326,9 @@
"password": { "password": {
"message": "Password" "message": "Password"
}, },
"totp": {
"message": "Authenticator secret"
},
"passphrase": { "passphrase": {
"message": "Passphrase" "message": "Passphrase"
}, },
@ -512,6 +515,18 @@
"autofillError": { "autofillError": {
"message": "Unable to auto-fill the selected item on this page. Copy and paste the information instead." "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": { "loggedOut": {
"message": "Logged out" "message": "Logged out"
}, },

View File

@ -56,6 +56,7 @@
"default_popup": "popup/index.html" "default_popup": "popup/index.html"
}, },
"permissions": [ "permissions": [
"<all_urls>",
"tabs", "tabs",
"contextMenus", "contextMenus",
"storage", "storage",

View File

@ -59,6 +59,7 @@
"default_popup": "popup/index.html" "default_popup": "popup/index.html"
}, },
"permissions": [ "permissions": [
"<all_urls>",
"tabs", "tabs",
"contextMenus", "contextMenus",
"storage", "storage",

View File

@ -413,6 +413,12 @@ export class BrowserApi {
return win.opr?.sidebarAction || browser.sidebarAction; return win.opr?.sidebarAction || browser.sidebarAction;
} }
static captureVisibleTab(): Promise<string> {
return new Promise((resolve) => {
chrome.tabs.captureVisibleTab(null, { format: "png" }, resolve);
});
}
/** /**
* Extension API helper method used to execute a script in a tab. * Extension API helper method used to execute a script in a tab.
* @see https://developer.chrome.com/docs/extensions/reference/tabs/#method-executeScript * @see https://developer.chrome.com/docs/extensions/reference/tabs/#method-executeScript

View File

@ -146,20 +146,45 @@
</div> </div>
</div> </div>
<div class="box-content-row" appBoxRow> <div class="box-content-row box-content-row-flex" appBoxRow>
<label for="loginTotp">{{ "authenticatorKeyTotp" | i18n }}</label> <div class="row-main">
<input <label for="loginTotp">{{ "authenticatorKeyTotp" | i18n }}</label>
id="loginTotp" <input
type="{{ cipher.viewPassword ? 'text' : 'password' }}" id="loginTotp"
name="Login.Totp" type="{{ cipher.viewPassword ? 'text' : 'password' }}"
class="monospaced" name="Login.Totp"
[(ngModel)]="cipher.login.totp" class="monospaced"
appInputVerbatim [(ngModel)]="cipher.login.totp"
[disabled]="!cipher.viewPassword" appInputVerbatim
[readonly]="!cipher.edit && editMode" [disabled]="!cipher.viewPassword"
/> [readonly]="!cipher.edit && editMode"
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'copyTOTP' | i18n }}"
(click)="copy(cipher.login.totp, 'totp', 'TOTP')"
*ngIf="cipher.viewPassword"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'totpCapture' | i18n }}"
(click)="captureTOTPFromTab()"
*ngIf="!(!cipher.edit && editMode)"
>
<i class="bwi bwi-lg bwi-camera" aria-hidden="true"></i>
</button>
</div>
</div> </div>
</div> </div>
<!-- Card --> <!-- Card -->
<div *ngIf="cipher.type === cipherType.Card"> <div *ngIf="cipher.type === cipherType.Card">
<div class="box-content-row" appBoxRow> <div class="box-content-row" appBoxRow>

View File

@ -1,6 +1,7 @@
import { DatePipe, Location } from "@angular/common"; import { DatePipe, Location } from "@angular/common";
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import qrcodeParser from "qrcode-parser";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { first } from "rxjs/operators"; import { first } from "rxjs/operators";
@ -84,6 +85,7 @@ export class AddEditComponent extends BaseAddEditComponent {
organizationService, organizationService,
sendApiService, sendApiService,
dialogService, dialogService,
window,
datePipe, datePipe,
); );
} }
@ -342,4 +344,26 @@ export class AddEditComponent extends BaseAddEditComponent {
this.singleActionKey || VaultPopoutType.addEditVaultItem, 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")
);
}
}
} }

View File

@ -47,6 +47,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnChanges,
organizationService: OrganizationService, organizationService: OrganizationService,
sendApiService: SendApiService, sendApiService: SendApiService,
dialogService: DialogService, dialogService: DialogService,
window: Window,
datePipe: DatePipe, datePipe: DatePipe,
) { ) {
super( super(
@ -65,6 +66,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnChanges,
organizationService, organizationService,
sendApiService, sendApiService,
dialogService, dialogService,
window,
datePipe, datePipe,
); );
} }

View File

@ -49,6 +49,7 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent {
logService: LogService, logService: LogService,
sendApiService: SendApiService, sendApiService: SendApiService,
dialogService: DialogService, dialogService: DialogService,
window: Window,
datePipe: DatePipe, datePipe: DatePipe,
) { ) {
super( super(
@ -69,6 +70,7 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent {
passwordRepromptService, passwordRepromptService,
sendApiService, sendApiService,
dialogService, dialogService,
window,
datePipe, datePipe,
); );
} }

View File

@ -61,6 +61,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
passwordRepromptService: PasswordRepromptService, passwordRepromptService: PasswordRepromptService,
sendApiService: SendApiService, sendApiService: SendApiService,
dialogService: DialogService, dialogService: DialogService,
window: Window,
datePipe: DatePipe, datePipe: DatePipe,
) { ) {
super( super(
@ -79,6 +80,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
organizationService, organizationService,
sendApiService, sendApiService,
dialogService, dialogService,
window,
datePipe, datePipe,
); );
} }
@ -142,9 +144,9 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
this.platformUtilsService.launchUri(uri.launchUri); this.platformUtilsService.launchUri(uri.launchUri);
} }
copy(value: string, typeI18nKey: string, aType: string) { async copy(value: string, typeI18nKey: string, aType: string): Promise<boolean> {
if (value == null) { if (value == null) {
return; return false;
} }
this.platformUtilsService.copyToClipboard(value, { window: window }); this.platformUtilsService.copyToClipboard(value, { window: window });
@ -166,6 +168,8 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
); );
} }
} }
return true;
} }
async generatePassword(): Promise<boolean> { async generatePassword(): Promise<boolean> {

View File

@ -51,6 +51,7 @@ export class AddEditComponent extends BaseAddEditComponent {
organizationService: OrganizationService, organizationService: OrganizationService,
sendApiService: SendApiService, sendApiService: SendApiService,
dialogService: DialogService, dialogService: DialogService,
window: Window,
datePipe: DatePipe, datePipe: DatePipe,
) { ) {
super( super(
@ -71,6 +72,7 @@ export class AddEditComponent extends BaseAddEditComponent {
passwordRepromptService, passwordRepromptService,
sendApiService, sendApiService,
dialogService, dialogService,
window,
datePipe, datePipe,
); );
} }

View File

@ -111,6 +111,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
private organizationService: OrganizationService, private organizationService: OrganizationService,
protected sendApiService: SendApiService, protected sendApiService: SendApiService,
protected dialogService: DialogService, protected dialogService: DialogService,
protected win: Window,
protected datePipe: DatePipe, protected datePipe: DatePipe,
) { ) {
this.typeOptions = [ this.typeOptions = [
@ -653,4 +654,28 @@ export class AddEditComponent implements OnInit, OnDestroy {
return loadedSavedInfo; return loadedSavedInfo;
} }
async copy(value: string, typeI18nKey: string, aType: string): Promise<boolean> {
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;
}
} }

14
package-lock.json generated
View File

@ -62,6 +62,7 @@
"patch-package": "8.0.0", "patch-package": "8.0.0",
"popper.js": "1.16.1", "popper.js": "1.16.1",
"proper-lockfile": "4.1.2", "proper-lockfile": "4.1.2",
"qrcode-parser": "^2.1.3",
"qrious": "4.0.2", "qrious": "4.0.2",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"tabbable": "6.2.0", "tabbable": "6.2.0",
@ -26776,6 +26777,11 @@
"node >= 0.2.0" "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": { "node_modules/jszip": {
"version": "3.10.1", "version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
@ -32998,6 +33004,14 @@
], ],
"peer": true "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": { "node_modules/qrious": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/qrious/-/qrious-4.0.2.tgz", "resolved": "https://registry.npmjs.org/qrious/-/qrious-4.0.2.tgz",

View File

@ -197,6 +197,7 @@
"patch-package": "8.0.0", "patch-package": "8.0.0",
"popper.js": "1.16.1", "popper.js": "1.16.1",
"proper-lockfile": "4.1.2", "proper-lockfile": "4.1.2",
"qrcode-parser": "^2.1.3",
"qrious": "4.0.2", "qrious": "4.0.2",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"tabbable": "6.2.0", "tabbable": "6.2.0",