From d5f5052c118226cf80ea6218944fc7214455b587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Mon, 20 Oct 2025 02:41:44 +0200 Subject: [PATCH 1/2] [PM-3162] Use system notification for update alert (#15606) --- apps/desktop/src/main.ts | 2 +- apps/desktop/src/main/updater.main.ts | 67 +++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index d5484213a90..fbb83a1bf56 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -221,7 +221,7 @@ export class Main { ); this.messagingMain = new MessagingMain(this, this.desktopSettingsService); - this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain); + this.updaterMain = new UpdaterMain(this.i18nService, this.logService, this.windowMain); const messageSubject = new Subject>>(); this.messagingService = MessageSender.combine( diff --git a/apps/desktop/src/main/updater.main.ts b/apps/desktop/src/main/updater.main.ts index 51d5073911e..60b4f282405 100644 --- a/apps/desktop/src/main/updater.main.ts +++ b/apps/desktop/src/main/updater.main.ts @@ -1,8 +1,9 @@ -import { dialog, shell } from "electron"; +import { dialog, shell, Notification } from "electron"; import log from "electron-log"; import { autoUpdater, UpdateDownloadedEvent, VerifyUpdateSupport } from "electron-updater"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/logging"; import { isAppImage, isDev, isMacAppStore, isWindowsPortable, isWindowsStore } from "../utils"; @@ -11,6 +12,8 @@ import { WindowMain } from "./window.main"; const UpdaterCheckInitialDelay = 5 * 1000; // 5 seconds const UpdaterCheckInterval = 12 * 60 * 60 * 1000; // 12 hours +const MaxTimeBeforeBlockingUpdateNotification = 7 * 24 * 60 * 60 * 1000; // 7 days + export class UpdaterMain { private doingUpdateCheck = false; private doingUpdateCheckWithFeedback = false; @@ -18,8 +21,19 @@ export class UpdaterMain { private updateDownloaded: UpdateDownloadedEvent = null; private originalRolloutFunction: VerifyUpdateSupport = null; + // This needs to be tracked to avoid the Notification being garbage collected, + // which would break the click handler. + private openedNotification: Notification | null = null; + + // This is used to set when the initial update notification was shown. + // The system notifications can be easy to miss or be disabled, so we want to + // ensure the user is eventually made aware of the update. If the user does not + // interact with the notification in a reasonable time, we will prompt them again. + private initialUpdateNotificationTime: number | null = null; + constructor( private i18nService: I18nService, + private logService: LogService, private windowMain: WindowMain, ) { autoUpdater.logger = log; @@ -43,6 +57,8 @@ export class UpdaterMain { }); autoUpdater.on("update-available", async () => { + this.initialUpdateNotificationTime ??= Date.now(); + if (this.doingUpdateCheckWithFeedback) { if (this.windowMain.win == null) { this.reset(); @@ -87,7 +103,7 @@ export class UpdaterMain { } this.updateDownloaded = info; - await this.promptRestartUpdate(info); + await this.promptRestartUpdate(info, this.doingUpdateCheckWithFeedback); }); autoUpdater.on("error", (error) => { @@ -108,7 +124,7 @@ export class UpdaterMain { } if (this.updateDownloaded && withFeedback) { - await this.promptRestartUpdate(this.updateDownloaded); + await this.promptRestartUpdate(this.updateDownloaded, true); return; } @@ -144,7 +160,50 @@ export class UpdaterMain { this.updateDownloaded = null; } - private async promptRestartUpdate(info: UpdateDownloadedEvent) { + private async promptRestartUpdate(info: UpdateDownloadedEvent, blocking: boolean) { + // If we have an initial notification, and it's from a long time ago, + // we will block the user with a dialog to ensure they see it. + const longTimeSinceInitialNotification = + this.initialUpdateNotificationTime != null && + Date.now() - this.initialUpdateNotificationTime > MaxTimeBeforeBlockingUpdateNotification; + + if (!longTimeSinceInitialNotification && !blocking && Notification.isSupported()) { + // If the prompt doesn't have to block and we support notifications, + // we will show a notification instead of a blocking dialog, which won't steal focus. + await this.promptRestartUpdateUsingSystemNotification(info); + } else { + // If we are blocking, or notifications are not supported, we will show a blocking dialog. + // This will steal the user's focus, so we should only do this for user initiated actions + // or when there are no other options. + await this.promptRestartUpdateUsingDialog(info); + } + } + + private async promptRestartUpdateUsingSystemNotification(info: UpdateDownloadedEvent) { + if (this.openedNotification != null) { + this.openedNotification.close(); + } + + this.openedNotification = new Notification({ + title: this.i18nService.t("bitwarden") + " - " + this.i18nService.t("restartToUpdate"), + body: this.i18nService.t("restartToUpdateDesc", info.version), + timeoutType: "never", + silent: false, + }); + + // If the user clicks the notification, prompt again to restart, this time with a blocking dialog. + this.openedNotification.on("click", () => { + void this.promptRestartUpdate(info, true); + }); + // If the notification fails to show, fall back to the blocking dialog as well. + this.openedNotification.on("failed", (error) => { + this.logService.error("Update notification failed", error); + void this.promptRestartUpdate(info, true); + }); + this.openedNotification.show(); + } + + private async promptRestartUpdateUsingDialog(info: UpdateDownloadedEvent) { const result = await dialog.showMessageBox(this.windowMain.win, { type: "info", title: this.i18nService.t("bitwarden") + " - " + this.i18nService.t("restartToUpdate"), From fa584f76b491be613de49f1735ef2de10fd3559e Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 20 Oct 2025 12:37:19 +0200 Subject: [PATCH 2/2] [PM-24683] Move change kdf service to SDK implementation (#16001) * Add new mp service api * Fix tests * Add test coverage * Add newline * Fix type * Rename to "unwrapUserKeyFromMasterPasswordUnlockData" * Fix build * Fix build on cli * Fix linting * Re-sort spec * Add tests * Fix test and build issues * Fix build * Clean up * Remove introduced function * Clean up comments * Fix abstract class types * Fix comments * Cleanup * Cleanup * Update libs/common/src/key-management/master-password/types/master-password.types.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/services/master-password.service.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/types/master-password.types.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Add comments * Fix build * Add arg null check * Cleanup * Fix build * Fix build on browser * Implement KDF change service * Deprecate encryptUserKeyWithMasterKey * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Add tests for null params * Fix builds * Cleanup and deprecate more functions * Fix formatting * Prettier * Clean up * Update libs/key-management/src/abstractions/key.service.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Make emailToSalt private and expose abstract saltForUser * Add tests * Add docs * Fix build * Fix tests * Fix tests * Address feedback and fix primitive obsession * Consolidate active account checks in change kdf confirmation component * Update libs/common/src/key-management/kdf/services/change-kdf-service.spec.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Add defensive parameter checks * Add tests * Add comment for follow-up epic * Move change kdf service, remove abstraction and add api service * Fix test * Drop redundant null check * Address feedback * Add throw on empty password * Fix tests * Mark change kdf service as internal * Add abstract classes * Switch to abstraction * Move change kdf to sdk * Update tests * Fix tests * Clean up sdk mapping * Clean up tests * Check the argument to make_update_kdf * Fix mock data * Fix tests * Fix relative imports --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --- .../src/services/jslib-services.module.ts | 2 +- .../kdf/change-kdf-service.spec.ts | 159 ++++++++++-------- .../key-management/kdf/change-kdf-service.ts | 65 +++---- .../types/master-password.types.ts | 33 +++- libs/key-management/src/index.ts | 1 + libs/key-management/src/models/kdf-config.ts | 14 ++ package-lock.json | 8 +- package.json | 2 +- 8 files changed, 175 insertions(+), 109 deletions(-) diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 954598de9ca..f5642f45b2e 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1277,7 +1277,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: ChangeKdfService, useClass: DefaultChangeKdfService, - deps: [MasterPasswordServiceAbstraction, KeyService, KdfConfigService, ChangeKdfApiService], + deps: [ChangeKdfApiService, SdkService], }), safeProvider({ provide: AuthRequestServiceAbstraction, diff --git a/libs/common/src/key-management/kdf/change-kdf-service.spec.ts b/libs/common/src/key-management/kdf/change-kdf-service.spec.ts index 07cba1cc7eb..c7df90f4790 100644 --- a/libs/common/src/key-management/kdf/change-kdf-service.spec.ts +++ b/libs/common/src/key-management/kdf/change-kdf-service.spec.ts @@ -1,14 +1,14 @@ import { mock } from "jest-mock-extended"; import { of } from "rxjs"; -import { KdfRequest } from "@bitwarden/common/models/request/kdf.request"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { UserId } from "@bitwarden/common/types/guid"; -import { UserKey } from "@bitwarden/common/types/key"; // eslint-disable-next-line no-restricted-imports -import { KdfConfigService, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management"; +import { PBKDF2KdfConfig } from "@bitwarden/key-management"; -import { MasterPasswordServiceAbstraction } from "../master-password/abstractions/master-password.service.abstraction"; +import { makeEncString } from "../../../spec"; +import { KdfRequest } from "../../models/request/kdf.request"; +import { SdkService } from "../../platform/abstractions/sdk/sdk.service"; +import { UserId } from "../../types/guid"; +import { EncString } from "../crypto/models/enc-string"; import { MasterKeyWrappedUserKey, MasterPasswordAuthenticationHash, @@ -21,35 +21,63 @@ import { DefaultChangeKdfService } from "./change-kdf-service"; describe("ChangeKdfService", () => { const changeKdfApiService = mock(); - const masterPasswordService = mock(); - const keyService = mock(); - const kdfConfigService = mock(); + const sdkService = mock(); - let sut: DefaultChangeKdfService = mock(); + let sut: DefaultChangeKdfService; - const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey; - const mockOldKdfConfig = new PBKDF2KdfConfig(100000); const mockNewKdfConfig = new PBKDF2KdfConfig(200000); + const mockOldKdfConfig = new PBKDF2KdfConfig(100000); const mockOldHash = "oldHash" as MasterPasswordAuthenticationHash; const mockNewHash = "newHash" as MasterPasswordAuthenticationHash; const mockUserId = "00000000-0000-0000-0000-000000000000" as UserId; const mockSalt = "test@bitwarden.com" as MasterPasswordSalt; - const mockWrappedUserKey = "wrappedUserKey"; + const mockWrappedUserKey: EncString = makeEncString("wrappedUserKey"); + + const mockSdkClient = { + crypto: jest.fn().mockReturnValue({ + make_update_kdf: jest.fn(), + }), + }; + const mockRef = { + value: mockSdkClient, + [Symbol.dispose]: jest.fn(), + }; + const mockSdk = { + take: jest.fn().mockReturnValue(mockRef), + }; beforeEach(() => { - sut = new DefaultChangeKdfService( - masterPasswordService, - keyService, - kdfConfigService, - changeKdfApiService, - ); + sdkService.userClient$ = jest.fn((userId: UserId) => of(mockSdk)) as any; + sut = new DefaultChangeKdfService(changeKdfApiService, sdkService); }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); describe("updateUserKdfParams", () => { + const mockUpdateKdfResult = { + masterPasswordAuthenticationData: { + kdf: mockNewKdfConfig.toSdkConfig(), + salt: mockSalt, + masterPasswordAuthenticationHash: mockNewHash, + }, + masterPasswordUnlockData: { + kdf: mockNewKdfConfig.toSdkConfig(), + salt: mockSalt, + masterKeyWrappedUserKey: mockWrappedUserKey.encryptedString, + }, + oldMasterPasswordAuthenticationData: { + kdf: mockOldKdfConfig.toSdkConfig(), + salt: mockSalt, + masterPasswordAuthenticationHash: mockOldHash, + }, + }; + + beforeEach(() => { + mockSdkClient.crypto().make_update_kdf.mockReturnValue(mockUpdateKdfResult); + }); + it("should throw an error if masterPassword is null", async () => { await expect( sut.updateUserKdfParams(null as unknown as string, mockNewKdfConfig, mockUserId), @@ -90,61 +118,31 @@ describe("ChangeKdfService", () => { ).rejects.toThrow("userId"); }); - it("should throw an error if userKey is null", async () => { - keyService.userKey$.mockReturnValueOnce(of(null)); - masterPasswordService.saltForUser$.mockReturnValueOnce(of(mockSalt)); - kdfConfigService.getKdfConfig$.mockReturnValueOnce(of(mockOldKdfConfig)); + it("should throw an error if SDK is not available", async () => { + sdkService.userClient$ = jest.fn().mockReturnValue(of(null)) as any; + await expect( sut.updateUserKdfParams("masterPassword", mockNewKdfConfig, mockUserId), - ).rejects.toThrow(); + ).rejects.toThrow("SDK not available"); }); - it("should throw an error if salt is null", async () => { - keyService.userKey$.mockReturnValueOnce(of(mockUserKey)); - masterPasswordService.saltForUser$.mockReturnValueOnce(of(null)); - kdfConfigService.getKdfConfig$.mockReturnValueOnce(of(mockOldKdfConfig)); - await expect( - sut.updateUserKdfParams("masterPassword", mockNewKdfConfig, mockUserId), - ).rejects.toThrow("Failed to get salt"); - }); + it("should call SDK update_kdf with correct parameters", async () => { + const masterPassword = "masterPassword"; - it("should throw an error if oldKdfConfig is null", async () => { - keyService.userKey$.mockReturnValueOnce(of(mockUserKey)); - masterPasswordService.saltForUser$.mockReturnValueOnce(of(mockSalt)); - kdfConfigService.getKdfConfig$.mockReturnValueOnce(of(null)); - await expect( - sut.updateUserKdfParams("masterPassword", mockNewKdfConfig, mockUserId), - ).rejects.toThrow("Failed to get oldKdfConfig"); - }); + await sut.updateUserKdfParams(masterPassword, mockNewKdfConfig, mockUserId); - it("should call apiService.send with correct parameters", async () => { - keyService.userKey$.mockReturnValueOnce(of(mockUserKey)); - masterPasswordService.saltForUser$.mockReturnValueOnce(of(mockSalt)); - kdfConfigService.getKdfConfig$.mockReturnValueOnce(of(mockOldKdfConfig)); - - masterPasswordService.makeMasterPasswordAuthenticationData - .mockResolvedValueOnce({ - salt: mockSalt, - kdf: mockOldKdfConfig, - masterPasswordAuthenticationHash: mockOldHash, - }) - .mockResolvedValueOnce({ - salt: mockSalt, - kdf: mockNewKdfConfig, - masterPasswordAuthenticationHash: mockNewHash, - }); - - masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValueOnce( - new MasterPasswordUnlockData( - mockSalt, - mockNewKdfConfig, - mockWrappedUserKey as MasterKeyWrappedUserKey, - ), + expect(mockSdkClient.crypto().make_update_kdf).toHaveBeenCalledWith( + masterPassword, + mockNewKdfConfig.toSdkConfig(), ); + }); - await sut.updateUserKdfParams("masterPassword", mockNewKdfConfig, mockUserId); + it("should call changeKdfApiService.updateUserKdfParams with correct request", async () => { + const masterPassword = "masterPassword"; - const expected = new KdfRequest( + await sut.updateUserKdfParams(masterPassword, mockNewKdfConfig, mockUserId); + + const expectedRequest = new KdfRequest( { salt: mockSalt, kdf: mockNewKdfConfig, @@ -153,15 +151,38 @@ describe("ChangeKdfService", () => { new MasterPasswordUnlockData( mockSalt, mockNewKdfConfig, - mockWrappedUserKey as MasterKeyWrappedUserKey, + mockWrappedUserKey.encryptedString as MasterKeyWrappedUserKey, ), - ).authenticateWith({ + ); + expectedRequest.authenticateWith({ salt: mockSalt, kdf: mockOldKdfConfig, masterPasswordAuthenticationHash: mockOldHash, }); - expect(changeKdfApiService.updateUserKdfParams).toHaveBeenCalledWith(expected); + expect(changeKdfApiService.updateUserKdfParams).toHaveBeenCalledWith(expectedRequest); + }); + + it("should properly dispose of SDK resources", async () => { + const masterPassword = "masterPassword"; + jest.spyOn(mockNewKdfConfig, "toSdkConfig").mockReturnValue({} as any); + + await sut.updateUserKdfParams(masterPassword, mockNewKdfConfig, mockUserId); + + expect(mockRef[Symbol.dispose]).toHaveBeenCalled(); + }); + + it("should handle SDK errors properly", async () => { + const masterPassword = "masterPassword"; + const sdkError = new Error("SDK update_kdf failed"); + jest.spyOn(mockNewKdfConfig, "toSdkConfig").mockReturnValue({} as any); + mockSdkClient.crypto().make_update_kdf.mockImplementation(() => { + throw sdkError; + }); + + await expect( + sut.updateUserKdfParams(masterPassword, mockNewKdfConfig, mockUserId), + ).rejects.toThrow("SDK update_kdf failed"); }); }); }); diff --git a/libs/common/src/key-management/kdf/change-kdf-service.ts b/libs/common/src/key-management/kdf/change-kdf-service.ts index d8bc3e21c1a..64fbd1fce05 100644 --- a/libs/common/src/key-management/kdf/change-kdf-service.ts +++ b/libs/common/src/key-management/kdf/change-kdf-service.ts @@ -1,55 +1,56 @@ +import { firstValueFrom, map } from "rxjs"; + import { assertNonNullish } from "@bitwarden/common/auth/utils"; -import { KdfRequest } from "@bitwarden/common/models/request/kdf.request"; import { UserId } from "@bitwarden/common/types/guid"; // eslint-disable-next-line no-restricted-imports -import { KdfConfig, KdfConfigService, KeyService } from "@bitwarden/key-management"; +import { KdfConfig } from "@bitwarden/key-management"; -import { MasterPasswordServiceAbstraction } from "../master-password/abstractions/master-password.service.abstraction"; -import { firstValueFromOrThrow } from "../utils"; +import { KdfRequest } from "../../models/request/kdf.request"; +import { SdkService } from "../../platform/abstractions/sdk/sdk.service"; +import { + fromSdkAuthenticationData, + MasterPasswordAuthenticationData, + MasterPasswordUnlockData, +} from "../master-password/types/master-password.types"; import { ChangeKdfApiService } from "./change-kdf-api.service.abstraction"; import { ChangeKdfService } from "./change-kdf-service.abstraction"; export class DefaultChangeKdfService implements ChangeKdfService { constructor( - private masterPasswordService: MasterPasswordServiceAbstraction, - private keyService: KeyService, - private kdfConfigService: KdfConfigService, private changeKdfApiService: ChangeKdfApiService, + private sdkService: SdkService, ) {} async updateUserKdfParams(masterPassword: string, kdf: KdfConfig, userId: UserId): Promise { assertNonNullish(masterPassword, "masterPassword"); assertNonNullish(kdf, "kdf"); assertNonNullish(userId, "userId"); + const updateKdfResult = await firstValueFrom( + this.sdkService.userClient$(userId).pipe( + map((sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } - const userKey = await firstValueFromOrThrow(this.keyService.userKey$(userId), "userKey"); - const salt = await firstValueFromOrThrow( - this.masterPasswordService.saltForUser$(userId), - "salt", - ); - const oldKdfConfig = await firstValueFromOrThrow( - this.kdfConfigService.getKdfConfig$(userId), - "oldKdfConfig", + using ref = sdk.take(); + + const updateKdfResponse = ref.value + .crypto() + .make_update_kdf(masterPassword, kdf.toSdkConfig()); + return updateKdfResponse; + }), + ), ); - const oldAuthenticationData = - await this.masterPasswordService.makeMasterPasswordAuthenticationData( - masterPassword, - oldKdfConfig, - salt, - ); - const authenticationData = - await this.masterPasswordService.makeMasterPasswordAuthenticationData( - masterPassword, - kdf, - salt, - ); - const unlockData = await this.masterPasswordService.makeMasterPasswordUnlockData( - masterPassword, - kdf, - salt, - userKey, + const authenticationData: MasterPasswordAuthenticationData = fromSdkAuthenticationData( + updateKdfResult.masterPasswordAuthenticationData, + ); + const unlockData: MasterPasswordUnlockData = MasterPasswordUnlockData.fromSdk( + updateKdfResult.masterPasswordUnlockData, + ); + const oldAuthenticationData: MasterPasswordAuthenticationData = fromSdkAuthenticationData( + updateKdfResult.oldMasterPasswordAuthenticationData, ); const request = new KdfRequest(authenticationData, unlockData); diff --git a/libs/common/src/key-management/master-password/types/master-password.types.ts b/libs/common/src/key-management/master-password/types/master-password.types.ts index 416b296623f..19c8c49c119 100644 --- a/libs/common/src/key-management/master-password/types/master-password.types.ts +++ b/libs/common/src/key-management/master-password/types/master-password.types.ts @@ -1,8 +1,18 @@ import { Jsonify, Opaque } from "type-fest"; // eslint-disable-next-line no-restricted-imports -import { Argon2KdfConfig, KdfConfig, KdfType, PBKDF2KdfConfig } from "@bitwarden/key-management"; -import { EncString } from "@bitwarden/sdk-internal"; +import { + fromSdkKdfConfig, + Argon2KdfConfig, + KdfConfig, + KdfType, + PBKDF2KdfConfig, +} from "@bitwarden/key-management"; +import { + EncString, + MasterPasswordUnlockData as SdkMasterPasswordUnlockData, + MasterPasswordAuthenticationData as SdkMasterPasswordAuthenticationData, +} from "@bitwarden/sdk-internal"; /** * The Base64-encoded master password authentication hash, that is sent to the server for authentication. @@ -24,6 +34,14 @@ export class MasterPasswordUnlockData { readonly masterKeyWrappedUserKey: MasterKeyWrappedUserKey, ) {} + static fromSdk(sdkData: SdkMasterPasswordUnlockData): MasterPasswordUnlockData { + return new MasterPasswordUnlockData( + sdkData.salt as MasterPasswordSalt, + fromSdkKdfConfig(sdkData.kdf), + sdkData.masterKeyWrappedUserKey as MasterKeyWrappedUserKey, + ); + } + toJSON(): any { return { salt: this.salt, @@ -55,3 +73,14 @@ export type MasterPasswordAuthenticationData = { kdf: KdfConfig; masterPasswordAuthenticationHash: MasterPasswordAuthenticationHash; }; + +export function fromSdkAuthenticationData( + sdkData: SdkMasterPasswordAuthenticationData, +): MasterPasswordAuthenticationData { + return { + salt: sdkData.salt as MasterPasswordSalt, + kdf: fromSdkKdfConfig(sdkData.kdf), + masterPasswordAuthenticationHash: + sdkData.masterPasswordAuthenticationHash as MasterPasswordAuthenticationHash, + }; +} diff --git a/libs/key-management/src/index.ts b/libs/key-management/src/index.ts index d21b79540e0..fb551b92cd6 100644 --- a/libs/key-management/src/index.ts +++ b/libs/key-management/src/index.ts @@ -16,6 +16,7 @@ export { Argon2KdfConfig, KdfConfig, DEFAULT_KDF_CONFIG, + fromSdkKdfConfig, } from "./models/kdf-config"; export { KdfConfigService } from "./abstractions/kdf-config.service"; export { DefaultKdfConfigService } from "./kdf-config.service"; diff --git a/libs/key-management/src/models/kdf-config.ts b/libs/key-management/src/models/kdf-config.ts index a2ed8a22505..3296247710e 100644 --- a/libs/key-management/src/models/kdf-config.ts +++ b/libs/key-management/src/models/kdf-config.ts @@ -145,4 +145,18 @@ export class Argon2KdfConfig { } } +export function fromSdkKdfConfig(sdkKdf: Kdf): KdfConfig { + if ("pBKDF2" in sdkKdf) { + return new PBKDF2KdfConfig(sdkKdf.pBKDF2.iterations); + } else if ("argon2id" in sdkKdf) { + return new Argon2KdfConfig( + sdkKdf.argon2id.iterations, + sdkKdf.argon2id.memory, + sdkKdf.argon2id.parallelism, + ); + } else { + throw new Error("Unsupported KDF type"); + } +} + export const DEFAULT_KDF_CONFIG = new PBKDF2KdfConfig(PBKDF2KdfConfig.ITERATIONS.defaultValue); diff --git a/package-lock.json b/package-lock.json index 2c1ca659d99..4e3707a60e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/sdk-internal": "0.2.0-main.311", + "@bitwarden/sdk-internal": "0.2.0-main.315", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", @@ -4690,9 +4690,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.311", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.311.tgz", - "integrity": "sha512-zJdQykNMFOyivpNaCB9jc85wZ1ci2HM8/E4hI+yS7FgRm0sRigK5rieF3+xRjiq7pEsZSD8AucR+u/XK9ADXiw==", + "version": "0.2.0-main.315", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.315.tgz", + "integrity": "sha512-hdpFRLrDYSJ6+cNXfMyHdTgg/xIePIlEUSn4JWzwru4PvTcEkkFwGJM3L2LoUqTdNMiDQlr0UjDahopT+C2r0g==", "license": "GPL-3.0", "dependencies": { "type-fest": "^4.41.0" diff --git a/package.json b/package.json index 5f2bb4fdfe6..4acf038b65a 100644 --- a/package.json +++ b/package.json @@ -159,7 +159,7 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/sdk-internal": "0.2.0-main.311", + "@bitwarden/sdk-internal": "0.2.0-main.315", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0",