mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
Auth/PM-10601 - Tech Debt Cleanup - Refactor Lock Component and User Verification to use PinService (#10408)
* PM-10601 - PinSvc new unlock check first draft * PM-10601 - PinSvc - add new method for determining if pin decryption is available. * PM-10601 - Add more docs on PinSvc * PM-10601 - Update Lock Comp & User Verification service + tests to use new isPinDecryptionAvailable method
This commit is contained in:
@@ -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<boolean>;
|
||||
|
||||
/**
|
||||
* 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<boolean>;
|
||||
|
||||
/**
|
||||
* Decrypts the UserKey with the provided PIN.
|
||||
*
|
||||
|
||||
@@ -292,6 +292,34 @@ export class PinService implements PinServiceAbstraction {
|
||||
return (await this.getPinLockType(userId)) !== "DISABLED";
|
||||
}
|
||||
|
||||
async isPinDecryptionAvailable(userId: UserId): Promise<boolean> {
|
||||
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<UserKey | null> {
|
||||
this.validateUserId(userId, "Cannot decrypt user key with PIN.");
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user