diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index b7ebf991e3..50eded416b 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -323,10 +323,7 @@ export class LockComponent implements OnInit, OnDestroy { private async load(userId: UserId) { this.pinLockType = await this.pinService.getPinLockType(userId); - const ephemeralPinSet = await this.pinService.getPinKeyEncryptedUserKeyEphemeral(userId); - - this.pinEnabled = - (this.pinLockType === "EPHEMERAL" && !!ephemeralPinSet) || this.pinLockType === "PERSISTENT"; + this.pinEnabled = await this.pinService.isPinDecryptionAvailable(userId); this.masterPasswordEnabled = await this.userVerificationService.hasMasterPassword(); diff --git a/libs/auth/src/common/abstractions/pin.service.abstraction.ts b/libs/auth/src/common/abstractions/pin.service.abstraction.ts index 9090cbe391..00ccf934f6 100644 --- a/libs/auth/src/common/abstractions/pin.service.abstraction.ts +++ b/libs/auth/src/common/abstractions/pin.service.abstraction.ts @@ -113,9 +113,17 @@ export abstract class PinServiceAbstraction { /** * Declares whether or not the user has a PIN set (either persistent or ephemeral). + * Note: for ephemeral, this does not check if we actual have an ephemeral PIN-encrypted UserKey stored in memory. + * Decryption might not be possible even if this returns true. Use {@link isPinDecryptionAvailable} if decryption is required. */ abstract isPinSet: (userId: UserId) => Promise; + /** + * Checks if PIN-encrypted keys are stored for the user. + * Used for unlock / user verification scenarios where we will need to decrypt the UserKey with the PIN. + */ + abstract isPinDecryptionAvailable: (userId: UserId) => Promise; + /** * Decrypts the UserKey with the provided PIN. * diff --git a/libs/auth/src/common/services/pin/pin.service.implementation.ts b/libs/auth/src/common/services/pin/pin.service.implementation.ts index ac2493a8c4..39bb80e0b7 100644 --- a/libs/auth/src/common/services/pin/pin.service.implementation.ts +++ b/libs/auth/src/common/services/pin/pin.service.implementation.ts @@ -292,6 +292,34 @@ export class PinService implements PinServiceAbstraction { return (await this.getPinLockType(userId)) !== "DISABLED"; } + async isPinDecryptionAvailable(userId: UserId): Promise { + this.validateUserId(userId, "Cannot determine if decryption of user key via PIN is available."); + + const pinLockType = await this.getPinLockType(userId); + + switch (pinLockType) { + case "DISABLED": + return false; + case "PERSISTENT": + // The above getPinLockType call ensures that we have either a PinKeyEncryptedUserKey or OldPinKeyEncryptedMasterKey set. + return true; + case "EPHEMERAL": { + // The above getPinLockType call ensures that we have a UserKeyEncryptedPin set. + // However, we must additively check to ensure that we have a set PinKeyEncryptedUserKeyEphemeral b/c otherwise + // we cannot take a PIN, derive a PIN key, and decrypt the ephemeral UserKey. + const pinKeyEncryptedUserKeyEphemeral = + await this.getPinKeyEncryptedUserKeyEphemeral(userId); + return Boolean(pinKeyEncryptedUserKeyEphemeral); + } + + default: { + // Compile-time check for exhaustive switch + const _exhaustiveCheck: never = pinLockType; + throw new Error(`Unexpected pinLockType: ${_exhaustiveCheck}`); + } + } + } + async decryptUserKeyWithPin(pin: string, userId: UserId): Promise { this.validateUserId(userId, "Cannot decrypt user key with PIN."); diff --git a/libs/auth/src/common/services/pin/pin.service.spec.ts b/libs/auth/src/common/services/pin/pin.service.spec.ts index 81009993d2..6befec0699 100644 --- a/libs/auth/src/common/services/pin/pin.service.spec.ts +++ b/libs/auth/src/common/services/pin/pin.service.spec.ts @@ -416,6 +416,66 @@ describe("PinService", () => { }); }); + describe("isPinDecryptionAvailable()", () => { + it("should return false if pinLockType is DISABLED", async () => { + // Arrange + sut.getPinLockType = jest.fn().mockResolvedValue("DISABLED"); + + // Act + const result = await sut.isPinDecryptionAvailable(mockUserId); + + // Assert + expect(result).toBe(false); + }); + + it("should return true if pinLockType is PERSISTENT", async () => { + // Arrange + sut.getPinLockType = jest.fn().mockResolvedValue("PERSISTENT"); + + // Act + const result = await sut.isPinDecryptionAvailable(mockUserId); + + // Assert + expect(result).toBe(true); + }); + + it("should return true if pinLockType is EPHEMERAL and we have an ephemeral PIN key encrypted user key", async () => { + // Arrange + sut.getPinLockType = jest.fn().mockResolvedValue("EPHEMERAL"); + sut.getPinKeyEncryptedUserKeyEphemeral = jest + .fn() + .mockResolvedValue(pinKeyEncryptedUserKeyEphemeral); + + // Act + const result = await sut.isPinDecryptionAvailable(mockUserId); + + // Assert + expect(result).toBe(true); + }); + + it("should return false if pinLockType is EPHEMERAL and we do not have an ephemeral PIN key encrypted user key", async () => { + // Arrange + sut.getPinLockType = jest.fn().mockResolvedValue("EPHEMERAL"); + sut.getPinKeyEncryptedUserKeyEphemeral = jest.fn().mockResolvedValue(null); + + // Act + const result = await sut.isPinDecryptionAvailable(mockUserId); + + // Assert + expect(result).toBe(false); + }); + + it("should throw an error if an unexpected pinLockType is returned", async () => { + // Arrange + sut.getPinLockType = jest.fn().mockResolvedValue("UNKNOWN"); + + // Act & Assert + await expect(sut.isPinDecryptionAvailable(mockUserId)).rejects.toThrow( + "Unexpected pinLockType: UNKNOWN", + ); + }); + }); + describe("decryptUserKeyWithPin()", () => { async function setupDecryptUserKeyWithPinMocks( pinLockType: PinLockType, diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts b/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts index 653c7a13b3..73a97cbc8b 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts @@ -410,6 +410,12 @@ describe("UserVerificationService", () => { function setPinAvailability(type: PinLockType) { pinService.getPinLockType.mockResolvedValue(type); + + if (type === "EPHEMERAL" || type === "PERSISTENT") { + pinService.isPinDecryptionAvailable.mockResolvedValue(true); + } else if (type === "DISABLED") { + pinService.isPinDecryptionAvailable.mockResolvedValue(false); + } } function disableBiometricsAvailability() { diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts index 50fe7b3add..3b133891c9 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.ts @@ -57,13 +57,17 @@ export class UserVerificationService implements UserVerificationServiceAbstracti ): Promise { const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; if (verificationType === "client") { - const [userHasMasterPassword, pinLockType, biometricsLockSet, biometricsUserKeyStored] = - await Promise.all([ - this.hasMasterPasswordAndMasterKeyHash(userId), - this.pinService.getPinLockType(userId), - this.vaultTimeoutSettingsService.isBiometricLockSet(userId), - this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric, userId), - ]); + const [ + userHasMasterPassword, + isPinDecryptionAvailable, + biometricsLockSet, + biometricsUserKeyStored, + ] = await Promise.all([ + this.hasMasterPasswordAndMasterKeyHash(userId), + this.pinService.isPinDecryptionAvailable(userId), + this.vaultTimeoutSettingsService.isBiometricLockSet(userId), + this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric, userId), + ]); // note: we do not need to check this.platformUtilsService.supportsBiometric() because // we can just use the logic below which works for both desktop & the browser extension. @@ -71,7 +75,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti return { client: { masterPassword: userHasMasterPassword, - pin: pinLockType !== "DISABLED", + pin: isPinDecryptionAvailable, biometrics: biometricsLockSet && (biometricsUserKeyStored || !this.platformUtilsService.supportsSecureStorage()),