mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 00:03:56 +00:00
[PM-11941] Migrate TOTP Generator to use SDK (#12987)
* Refactored totp service to use sdk Fixed strict typescript issues * Fixed dependency issues * Returned object that contains code and period, removed get interval function * removed dependencies * Updated to use refactored totp service * removed sdk service undefined check * removed undefined as an input from the getCode function * Made getcode$ an observable * refactored to use getcodee$ * Filter out emmissions * updated sdk version * Fixed readability nit * log error on overlay if totp response does not return a code * fix(totpGeneration): [PM-11941] Totp countdown not working on clients * Used optional chaining if totpresponse returns null or undefined
This commit is contained in:
@@ -1,34 +1,42 @@
|
||||
<div class="tw-flex tw-items-center tw-justify-center totp-v2">
|
||||
<span class="tw-relative tw-flex tw-justify-center tw-items-center" aria-hidden="true">
|
||||
<span
|
||||
class="tw-absolute"
|
||||
[ngClass]="{ 'tw-text-main': !totpLow, 'tw-text-danger': totpLow }"
|
||||
bitTypography="helper"
|
||||
>{{ totpSec }}</span
|
||||
>
|
||||
<svg class="tw-size-7" transform="rotate(-90)">
|
||||
<g>
|
||||
<circle
|
||||
class="tw-fill-none"
|
||||
[ngClass]="{ 'tw-stroke-text-main': !totpLow, 'tw-stroke-danger-600': totpLow }"
|
||||
r="9.5"
|
||||
cy="14"
|
||||
cx="14"
|
||||
stroke-width="2"
|
||||
stroke-dasharray="60"
|
||||
[ngStyle]="{ 'stroke-dashoffset.px': totpDash }"
|
||||
></circle>
|
||||
<circle
|
||||
class="tw-fill-none"
|
||||
[ngClass]="{ 'tw-stroke-text-main': !totpLow, 'tw-stroke-danger-600': totpLow }"
|
||||
r="11"
|
||||
cy="14"
|
||||
cx="14"
|
||||
stroke-width="1"
|
||||
stroke-dasharray="71"
|
||||
stroke-dashoffset="0"
|
||||
></circle>
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<ng-container *ngIf="totpInfo$ | async as totpInfo">
|
||||
<div class="tw-flex tw-items-center tw-justify-center totp-v2">
|
||||
<span class="tw-relative tw-flex tw-justify-center tw-items-center" aria-hidden="true">
|
||||
<span
|
||||
class="tw-absolute"
|
||||
[ngClass]="{ 'tw-text-main': !totpInfo.totpLow, 'tw-text-danger': totpInfo.totpLow }"
|
||||
bitTypography="helper"
|
||||
>{{ totpInfo.totpSec }}</span
|
||||
>
|
||||
<svg class="tw-size-7" transform="rotate(-90)">
|
||||
<g>
|
||||
<circle
|
||||
class="tw-fill-none"
|
||||
[ngClass]="{
|
||||
'tw-stroke-text-main': !totpInfo.totpLow,
|
||||
'tw-stroke-danger-600': totpInfo.totpLow,
|
||||
}"
|
||||
r="9.5"
|
||||
cy="14"
|
||||
cx="14"
|
||||
stroke-width="2"
|
||||
stroke-dasharray="60"
|
||||
[ngStyle]="{ 'stroke-dashoffset.px': totpInfo.totpDash }"
|
||||
></circle>
|
||||
<circle
|
||||
class="tw-fill-none"
|
||||
[ngClass]="{
|
||||
'tw-stroke-text-main': !totpInfo.totpLow,
|
||||
'tw-stroke-danger-600': totpInfo.totpLow,
|
||||
}"
|
||||
r="11"
|
||||
cy="14"
|
||||
cx="14"
|
||||
stroke-width="1"
|
||||
stroke-dasharray="71"
|
||||
stroke-dashoffset="0"
|
||||
></circle>
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { Observable, map, tap } from "rxjs";
|
||||
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { TotpInfo } from "@bitwarden/common/vault/services/totp.service";
|
||||
import { TypographyModule } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
@@ -17,69 +19,45 @@ export class BitTotpCountdownComponent implements OnInit {
|
||||
@Input() cipher: CipherView;
|
||||
@Output() sendCopyCode = new EventEmitter();
|
||||
|
||||
totpCode: string;
|
||||
totpCodeFormatted: string;
|
||||
totpDash: number;
|
||||
totpSec: number;
|
||||
totpLow: boolean;
|
||||
private totpInterval: any;
|
||||
/**
|
||||
* Represents TOTP information including display formatting and timing
|
||||
*/
|
||||
totpInfo$: Observable<TotpInfo> | undefined;
|
||||
|
||||
constructor(protected totpService: TotpService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.totpUpdateCode();
|
||||
const interval = this.totpService.getTimeInterval(this.cipher.login.totp);
|
||||
await this.totpTick(interval);
|
||||
this.totpInfo$ = this.cipher?.login?.totp
|
||||
? this.totpService.getCode$(this.cipher.login.totp).pipe(
|
||||
map((response) => {
|
||||
const epoch = Math.round(new Date().getTime() / 1000.0);
|
||||
const mod = epoch % response.period;
|
||||
|
||||
this.totpInterval = setInterval(async () => {
|
||||
await this.totpTick(interval);
|
||||
}, 1000);
|
||||
return {
|
||||
totpCode: response.code,
|
||||
totpCodeFormatted: this.formatTotpCode(response.code),
|
||||
totpSec: response.period - mod,
|
||||
totpDash: +(Math.round(((60 / response.period) * mod + "e+2") as any) + "e-2"),
|
||||
totpLow: response.period - mod <= 7,
|
||||
} as TotpInfo;
|
||||
}),
|
||||
tap((totpInfo) => {
|
||||
if (totpInfo.totpCode && totpInfo.totpCode.length > 4) {
|
||||
this.sendCopyCode.emit({
|
||||
totpCode: totpInfo.totpCode,
|
||||
totpCodeFormatted: totpInfo.totpCodeFormatted,
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private async totpUpdateCode() {
|
||||
if (this.cipher.login.totp == null) {
|
||||
this.clearTotp();
|
||||
return;
|
||||
}
|
||||
|
||||
this.totpCode = await this.totpService.getCode(this.cipher.login.totp);
|
||||
if (this.totpCode != null) {
|
||||
if (this.totpCode.length > 4) {
|
||||
this.totpCodeFormatted = this.formatTotpCode();
|
||||
this.sendCopyCode.emit({
|
||||
totpCode: this.totpCode,
|
||||
totpCodeFormatted: this.totpCodeFormatted,
|
||||
});
|
||||
} else {
|
||||
this.totpCodeFormatted = this.totpCode;
|
||||
}
|
||||
} else {
|
||||
this.totpCodeFormatted = null;
|
||||
this.sendCopyCode.emit({ totpCode: null, totpCodeFormatted: null });
|
||||
this.clearTotp();
|
||||
}
|
||||
}
|
||||
|
||||
private async totpTick(intervalSeconds: number) {
|
||||
const epoch = Math.round(new Date().getTime() / 1000.0);
|
||||
const mod = epoch % intervalSeconds;
|
||||
|
||||
this.totpSec = intervalSeconds - mod;
|
||||
this.totpDash = +(Math.round(((60 / intervalSeconds) * mod + "e+2") as any) + "e-2");
|
||||
this.totpLow = this.totpSec <= 7;
|
||||
if (mod === 0) {
|
||||
await this.totpUpdateCode();
|
||||
}
|
||||
}
|
||||
|
||||
private formatTotpCode(): string {
|
||||
const half = Math.floor(this.totpCode.length / 2);
|
||||
return this.totpCode.substring(0, half) + " " + this.totpCode.substring(half);
|
||||
}
|
||||
|
||||
private clearTotp() {
|
||||
if (this.totpInterval) {
|
||||
clearInterval(this.totpInterval);
|
||||
private formatTotpCode(code: string): string {
|
||||
if (code.length > 4) {
|
||||
const half = Math.floor(code.length / 2);
|
||||
return code.substring(0, half) + " " + code.substring(half);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,10 +136,10 @@ describe("CopyCipherFieldService", () => {
|
||||
|
||||
it("should get TOTP code when allowed from premium", async () => {
|
||||
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
|
||||
totpService.getCode.mockResolvedValue("123456");
|
||||
totpService.getCode$.mockReturnValue(of({ code: "123456", period: 30 }));
|
||||
const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt);
|
||||
expect(result).toBeTruthy();
|
||||
expect(totpService.getCode).toHaveBeenCalledWith(valueToCopy);
|
||||
expect(totpService.getCode$).toHaveBeenCalledWith(valueToCopy);
|
||||
expect(platformUtilsService.copyToClipboard).toHaveBeenCalledWith("123456");
|
||||
expect(billingAccountProfileStateService.hasPremiumFromAnySource$).toHaveBeenCalledWith(
|
||||
userId,
|
||||
@@ -148,10 +148,10 @@ describe("CopyCipherFieldService", () => {
|
||||
|
||||
it("should get TOTP code when allowed from organization", async () => {
|
||||
cipher.organizationUseTotp = true;
|
||||
totpService.getCode.mockResolvedValue("123456");
|
||||
totpService.getCode$.mockReturnValue(of({ code: "123456", period: 30 }));
|
||||
const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt);
|
||||
expect(result).toBeTruthy();
|
||||
expect(totpService.getCode).toHaveBeenCalledWith(valueToCopy);
|
||||
expect(totpService.getCode$).toHaveBeenCalledWith(valueToCopy);
|
||||
expect(platformUtilsService.copyToClipboard).toHaveBeenCalledWith("123456");
|
||||
});
|
||||
|
||||
@@ -159,7 +159,7 @@ describe("CopyCipherFieldService", () => {
|
||||
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||
const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt);
|
||||
expect(result).toBeFalsy();
|
||||
expect(totpService.getCode).not.toHaveBeenCalled();
|
||||
expect(totpService.getCode$).not.toHaveBeenCalled();
|
||||
expect(platformUtilsService.copyToClipboard).not.toHaveBeenCalled();
|
||||
expect(billingAccountProfileStateService.hasPremiumFromAnySource$).toHaveBeenCalledWith(
|
||||
userId,
|
||||
@@ -170,7 +170,7 @@ describe("CopyCipherFieldService", () => {
|
||||
cipher.login.totp = null;
|
||||
const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt);
|
||||
expect(result).toBeFalsy();
|
||||
expect(totpService.getCode).not.toHaveBeenCalled();
|
||||
expect(totpService.getCode$).not.toHaveBeenCalled();
|
||||
expect(platformUtilsService.copyToClipboard).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -124,7 +124,11 @@ export class CopyCipherFieldService {
|
||||
if (!(await this.totpAllowed(cipher))) {
|
||||
return false;
|
||||
}
|
||||
valueToCopy = await this.totpService.getCode(valueToCopy);
|
||||
const totpResponse = await firstValueFrom(this.totpService.getCode$(valueToCopy));
|
||||
if (!totpResponse?.code) {
|
||||
return false;
|
||||
}
|
||||
valueToCopy = totpResponse.code;
|
||||
}
|
||||
|
||||
this.platformUtilsService.copyToClipboard(valueToCopy);
|
||||
|
||||
Reference in New Issue
Block a user