[PM-3343] Capture TOTP QR codes from websites in the browser extension (#5985)
* Implement totp capture for browser extensions
This commit is contained in:
parent
364e23d8a5
commit
1b4717a78f
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
"default_popup": "popup/index.html"
|
||||
},
|
||||
"permissions": [
|
||||
"<all_urls>",
|
||||
"tabs",
|
||||
"contextMenus",
|
||||
"storage",
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
"default_popup": "popup/index.html"
|
||||
},
|
||||
"permissions": [
|
||||
"<all_urls>",
|
||||
"tabs",
|
||||
"contextMenus",
|
||||
"storage",
|
||||
|
|
|
@ -413,6 +413,12 @@ export class BrowserApi {
|
|||
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.
|
||||
* @see https://developer.chrome.com/docs/extensions/reference/tabs/#method-executeScript
|
||||
|
|
|
@ -146,20 +146,45 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="loginTotp">{{ "authenticatorKeyTotp" | i18n }}</label>
|
||||
<input
|
||||
id="loginTotp"
|
||||
type="{{ cipher.viewPassword ? 'text' : 'password' }}"
|
||||
name="Login.Totp"
|
||||
class="monospaced"
|
||||
[(ngModel)]="cipher.login.totp"
|
||||
appInputVerbatim
|
||||
[disabled]="!cipher.viewPassword"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<label for="loginTotp">{{ "authenticatorKeyTotp" | i18n }}</label>
|
||||
<input
|
||||
id="loginTotp"
|
||||
type="{{ cipher.viewPassword ? 'text' : 'password' }}"
|
||||
name="Login.Totp"
|
||||
class="monospaced"
|
||||
[(ngModel)]="cipher.login.totp"
|
||||
appInputVerbatim
|
||||
[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>
|
||||
|
||||
<!-- Card -->
|
||||
<div *ngIf="cipher.type === cipherType.Card">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
|
|
|
@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<boolean> {
|
||||
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<boolean> {
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue