1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-06 10:33:57 +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:
SmithThe4th
2025-03-06 14:01:07 -05:00
committed by GitHub
parent 1415041fd7
commit e327816bc4
24 changed files with 345 additions and 443 deletions

View File

@@ -190,7 +190,9 @@ describe("OverlayBackground", () => {
inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
themeStateService = mock<ThemeStateService>();
themeStateService.selectedTheme$ = selectedThemeMock$;
totpService = mock<TotpService>();
totpService = mock<TotpService>({
getCode$: jest.fn().mockReturnValue(of(undefined)),
});
overlayBackground = new OverlayBackground(
logService,
cipherService,

View File

@@ -707,13 +707,15 @@ export class OverlayBackground implements OverlayBackgroundInterface {
};
if (cipher.type === CipherType.Login) {
const totpCode = await this.totpService.getCode(cipher.login?.totp);
const totpCodeTimeInterval = this.totpService.getTimeInterval(cipher.login?.totp);
const totpResponse = cipher.login?.totp
? await firstValueFrom(this.totpService.getCode$(cipher.login.totp))
: undefined;
inlineMenuData.login = {
username: cipher.login.username,
totp: totpCode,
totp: totpResponse?.code,
totpField: this.isTotpFieldForCurrentField(),
totpCodeTimeInterval: totpCodeTimeInterval,
totpCodeTimeInterval: totpResponse?.period,
passkey: hasPasskey
? {
rpName: cipher.login.fido2Credentials[0].rpName,
@@ -1131,9 +1133,13 @@ export class OverlayBackground implements OverlayBackgroundInterface {
this.updateLastUsedInlineMenuCipher(inlineMenuCipherId, cipher);
if (cipher.login?.totp) {
this.platformUtilsService.copyToClipboard(
await this.totpService.getCode(cipher.login.totp),
);
const totpResponse = await firstValueFrom(this.totpService.getCode$(cipher.login.totp));
if (totpResponse?.code) {
this.platformUtilsService.copyToClipboard(totpResponse.code);
} else {
this.logService.error("Failed to get TOTP code for inline menu cipher");
}
}
return;
}

View File

@@ -1,4 +1,5 @@
import { mock, MockProxy } from "jest-mock-extended";
import { of } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
@@ -159,19 +160,25 @@ describe("ContextMenuClickedHandler", () => {
it("copies totp code to clipboard", async () => {
cipherService.getAllDecrypted.mockResolvedValue([createCipher({ totp: "TEST_TOTP_SEED" })]);
totpService.getCode.mockImplementation((seed) => {
jest.spyOn(totpService, "getCode$").mockImplementation((seed: string) => {
if (seed === "TEST_TOTP_SEED") {
return Promise.resolve("123456");
return of({
code: "123456",
period: 30,
});
}
return Promise.resolve("654321");
return of({
code: "654321",
period: 30,
});
});
await sut.run(createData(`${COPY_VERIFICATION_CODE_ID}_1`, COPY_VERIFICATION_CODE_ID), {
url: "https://test.com",
} as any);
expect(totpService.getCode).toHaveBeenCalledTimes(1);
expect(totpService.getCode$).toHaveBeenCalledTimes(1);
expect(copyToClipboard).toHaveBeenCalledWith({
text: "123456",

View File

@@ -205,8 +205,9 @@ export class ContextMenuClickedHandler {
action: COPY_VERIFICATION_CODE_ID,
});
} else {
const totpResponse = await firstValueFrom(this.totpService.getCode$(cipher.login.totp));
this.copyToClipboard({
text: await this.totpService.getCode(cipher.login.totp),
text: totpResponse.code,
tab: tab,
});
}

View File

@@ -921,12 +921,12 @@ describe("AutofillService", () => {
.spyOn(billingAccountProfileStateService, "hasPremiumFromAnySource$")
.mockImplementation(() => of(true));
jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(true);
jest.spyOn(totpService, "getCode").mockResolvedValue(totpCode);
totpService.getCode$.mockReturnValue(of({ code: totpCode, period: 30 }));
const autofillResult = await autofillService.doAutoFill(autofillOptions);
expect(autofillService.getShouldAutoCopyTotp).toHaveBeenCalled();
expect(totpService.getCode).toHaveBeenCalledWith(autofillOptions.cipher.login.totp);
expect(totpService.getCode$).toHaveBeenCalledWith(autofillOptions.cipher.login.totp);
expect(autofillResult).toBe(totpCode);
});
@@ -940,7 +940,7 @@ describe("AutofillService", () => {
const autofillResult = await autofillService.doAutoFill(autofillOptions);
expect(autofillService.getShouldAutoCopyTotp).not.toHaveBeenCalled();
expect(totpService.getCode).not.toHaveBeenCalled();
expect(totpService.getCode$).not.toHaveBeenCalled();
expect(autofillResult).toBeNull();
});
@@ -956,12 +956,12 @@ describe("AutofillService", () => {
it("returns a null value if the login does not contain a TOTP value", async () => {
autofillOptions.cipher.login.totp = undefined;
jest.spyOn(autofillService, "getShouldAutoCopyTotp");
jest.spyOn(totpService, "getCode");
jest.spyOn(totpService, "getCode$");
const autofillResult = await autofillService.doAutoFill(autofillOptions);
expect(autofillService.getShouldAutoCopyTotp).not.toHaveBeenCalled();
expect(totpService.getCode).not.toHaveBeenCalled();
expect(totpService.getCode$).not.toHaveBeenCalled();
expect(autofillResult).toBeNull();
});
@@ -984,12 +984,12 @@ describe("AutofillService", () => {
.spyOn(billingAccountProfileStateService, "hasPremiumFromAnySource$")
.mockImplementation(() => of(true));
jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(false);
jest.spyOn(totpService, "getCode");
jest.spyOn(totpService, "getCode$");
const autofillResult = await autofillService.doAutoFill(autofillOptions);
expect(autofillService.getShouldAutoCopyTotp).toHaveBeenCalled();
expect(totpService.getCode).not.toHaveBeenCalled();
expect(totpService.getCode$).not.toHaveBeenCalled();
expect(autofillResult).toBeNull();
});
});

View File

@@ -494,7 +494,7 @@ export default class AutofillService implements AutofillServiceInterface {
const shouldAutoCopyTotp = await this.getShouldAutoCopyTotp();
totp = shouldAutoCopyTotp
? await this.totpService.getCode(options.cipher.login.totp)
? (await firstValueFrom(this.totpService.getCode$(options.cipher.login.totp))).code
: null;
}),
);
@@ -992,7 +992,10 @@ export default class AutofillService implements AutofillServiceInterface {
}
filledFields[t.opid] = t;
let totpValue = await this.totpService.getCode(login.totp);
const totpResponse = await firstValueFrom(
this.totpService.getCode$(options.cipher.login.totp),
);
let totpValue = totpResponse.code;
if (totpValue.length == totps.length) {
totpValue = totpValue.charAt(i);
}

View File

@@ -978,7 +978,7 @@ export default class MainBackground {
this.authService,
this.accountService,
);
this.totpService = new TotpService(this.cryptoFunctionService, this.logService);
this.totpService = new TotpService(this.sdkService);
this.scriptInjectorService = new BrowserScriptInjectorService(
this.domainSettingsService,

View File

@@ -84,6 +84,7 @@ import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/comm
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import {
AbstractStorageService,
@@ -285,7 +286,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: TotpServiceAbstraction,
useClass: TotpService,
deps: [CryptoFunctionService, LogService],
deps: [SdkService],
}),
safeProvider({
provide: OffscreenDocumentService,