From f494037c34a4579afa4e6b52c98ec8caf33317f3 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 8 Apr 2026 12:43:53 +0900 Subject: [PATCH] Set user ever had user key and add comment --- .../services/key-state/user-key.state.ts | 11 ++++ .../unlock/src/default-unlock.service.spec.ts | 51 ++++++++++++++++++- libs/unlock/src/default-unlock.service.ts | 2 + 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/libs/common/src/platform/services/key-state/user-key.state.ts b/libs/common/src/platform/services/key-state/user-key.state.ts index 168bc7fe6c8..34789b95bb4 100644 --- a/libs/common/src/platform/services/key-state/user-key.state.ts +++ b/libs/common/src/platform/services/key-state/user-key.state.ts @@ -2,6 +2,17 @@ import { UserKey } from "../../../types/key"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; import { CRYPTO_DISK, CRYPTO_MEMORY, UserKeyDefinition } from "../../state"; +/** + * Ever had user key is a hack that allows differentiating TDE users that + * have logged and unlocked in at least once fully, from ones that have not. + * Depending on this, routing happens to the login-initiated or lock component. + * The former allows trusting the device, and has master password (if available) + * or trusted-device / admin approval as unlock methods. The latter has regular + * lock methods. + * + * Ideally, this state hack would be replaced by a more robust solution that just + * checks the available unlock methods, and routes depending on those. + */ export const USER_EVER_HAD_USER_KEY = new UserKeyDefinition( CRYPTO_DISK, "everHadUserKey", diff --git a/libs/unlock/src/default-unlock.service.spec.ts b/libs/unlock/src/default-unlock.service.spec.ts index 82b65a8b7a7..3249605f58b 100644 --- a/libs/unlock/src/default-unlock.service.spec.ts +++ b/libs/unlock/src/default-unlock.service.spec.ts @@ -23,6 +23,8 @@ import { LogService } from "@bitwarden/logging"; import { PureCrypto } from "@bitwarden/sdk-internal"; import { StateProvider, StateService } from "@bitwarden/state"; +import { USER_EVER_HAD_USER_KEY } from "@bitwarden/common/platform/services/key-state/user-key.state"; + import { DefaultUnlockService } from "./default-unlock.service"; const mockUserId = "b1e2d3c4-a1b2-c3d4-e5f6-a1b2c3d4e5f6" as UserId; @@ -106,6 +108,7 @@ describe("DefaultUnlockService", () => { const mockStateUpdate = jest.fn().mockResolvedValue(undefined); stateProvider.getUser.mockReturnValue({ update: mockStateUpdate } as any); + stateProvider.setUserState.mockResolvedValue(undefined); service = new DefaultUnlockService( registerSdkService, @@ -180,6 +183,11 @@ describe("DefaultUnlockService", () => { expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(userEncryptionKey.toBase64(), { userId: mockUserId, }); + expect(stateProvider.setUserState).toHaveBeenCalledWith( + USER_EVER_HAD_USER_KEY, + true, + mockUserId, + ); }); }); @@ -208,6 +216,26 @@ describe("DefaultUnlockService", () => { service.unlockWithMasterPassword(mockUserId, mockMasterPassword), ).rejects.toThrow("SDK not available"); }); + + it("sets unlock side effects after successful unlock", async () => { + const userEncryptionKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray); + mockCrypto.get_user_encryption_key.mockResolvedValue(userEncryptionKey.toBase64()); + + await service.unlockWithMasterPassword(mockUserId, mockMasterPassword); + + expect(biometricsService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith( + mockUserId, + expect.any(SymmetricCryptoKey), + ); + expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(userEncryptionKey.toBase64(), { + userId: mockUserId, + }); + expect(stateProvider.setUserState).toHaveBeenCalledWith( + USER_EVER_HAD_USER_KEY, + true, + mockUserId, + ); + }); }); describe("unlockWithBiometrics", () => { @@ -246,5 +274,24 @@ describe("DefaultUnlockService", () => { await expect(service.unlockWithBiometrics(mockUserId)).rejects.toThrow("SDK not available"); }); - }); -}); + + it("sets unlock side effects after successful unlock", async () => { + biometricsService.unlockWithBiometricsForUser.mockResolvedValue(mockUserKey); + const userEncryptionKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray); + mockCrypto.get_user_encryption_key.mockResolvedValue(userEncryptionKey.toBase64()); + + await service.unlockWithBiometrics(mockUserId); + + expect(biometricsService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith( + mockUserId, + expect.any(SymmetricCryptoKey), + ); + expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(userEncryptionKey.toBase64(), { + userId: mockUserId, + }); + expect(stateProvider.setUserState).toHaveBeenCalledWith( + USER_EVER_HAD_USER_KEY, + true, + mockUserId, + ); + }); diff --git a/libs/unlock/src/default-unlock.service.ts b/libs/unlock/src/default-unlock.service.ts index a34bc575581..98e212387ec 100644 --- a/libs/unlock/src/default-unlock.service.ts +++ b/libs/unlock/src/default-unlock.service.ts @@ -37,6 +37,7 @@ import { StateProvider, StateService } from "@bitwarden/state"; import { UserId } from "@bitwarden/user-core"; import { UnlockService } from "./unlock.service"; +import { USER_EVER_HAD_USER_KEY } from "@bitwarden/common/platform/services/key-state/user-key.state"; export class DefaultUnlockService implements UnlockService { constructor( @@ -251,6 +252,7 @@ export class DefaultUnlockService implements UnlockService { if (await this.shouldStoreSessionKey(userId)) { await this.stateService.setUserKeyAutoUnlock(userKey.toBase64(), { userId: userId }); } + await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, true, userId); } private async shouldStoreSessionKey(userId: UserId): Promise {