mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 06:13:38 +00:00
[PM-25473] Non-encryption passkeys prevent key rotation (#16514)
* consistent webauthn filtering as in server by prfStatus, better docs * test coverage
This commit is contained in:
@@ -1,7 +1,16 @@
|
|||||||
// FIXME: update to use a const object instead of a typescript enum
|
// FIXME: update to use a const object instead of a typescript enum
|
||||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||||
export enum WebauthnLoginCredentialPrfStatus {
|
export enum WebauthnLoginCredentialPrfStatus {
|
||||||
|
/**
|
||||||
|
* Encrypted user key present, PRF function is supported.
|
||||||
|
*/
|
||||||
Enabled = 0,
|
Enabled = 0,
|
||||||
|
/**
|
||||||
|
* PRF function is supported.
|
||||||
|
*/
|
||||||
Supported = 1,
|
Supported = 1,
|
||||||
|
/**
|
||||||
|
* PRF function is not supported.
|
||||||
|
*/
|
||||||
Unsupported = 2,
|
Unsupported = 2,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,6 @@ export class WebauthnLoginCredentialResponse extends BaseResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hasPrfKeyset(): boolean {
|
hasPrfKeyset(): boolean {
|
||||||
return this.encryptedUserKey != null && this.encryptedPublicKey != null;
|
return this.prfStatus === WebauthnLoginCredentialPrfStatus.Enabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,15 +10,19 @@ import { WebAuthnLoginPrfKeyServiceAbstraction } from "@bitwarden/common/auth/ab
|
|||||||
import { WebAuthnLoginCredentialAssertionView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion.view";
|
import { WebAuthnLoginCredentialAssertionView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion.view";
|
||||||
import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request";
|
import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request";
|
||||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||||
|
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { makeSymmetricCryptoKey } from "@bitwarden/common/spec";
|
import { makeEncString, makeSymmetricCryptoKey } from "@bitwarden/common/spec";
|
||||||
import { PrfKey, UserKey } from "@bitwarden/common/types/key";
|
import { PrfKey, UserKey } from "@bitwarden/common/types/key";
|
||||||
|
import { UserId } from "@bitwarden/user-core";
|
||||||
|
|
||||||
|
import { WebauthnLoginCredentialPrfStatus } from "../../enums/webauthn-login-credential-prf-status.enum";
|
||||||
import { CredentialCreateOptionsView } from "../../views/credential-create-options.view";
|
import { CredentialCreateOptionsView } from "../../views/credential-create-options.view";
|
||||||
import { PendingWebauthnLoginCredentialView } from "../../views/pending-webauthn-login-credential.view";
|
import { PendingWebauthnLoginCredentialView } from "../../views/pending-webauthn-login-credential.view";
|
||||||
import { RotateableKeySetService } from "../rotateable-key-set.service";
|
import { RotateableKeySetService } from "../rotateable-key-set.service";
|
||||||
|
|
||||||
import { EnableCredentialEncryptionRequest } from "./request/enable-credential-encryption.request";
|
import { EnableCredentialEncryptionRequest } from "./request/enable-credential-encryption.request";
|
||||||
|
import { WebauthnLoginCredentialResponse } from "./response/webauthn-login-credential.response";
|
||||||
import { WebAuthnLoginAdminApiService } from "./webauthn-login-admin-api.service";
|
import { WebAuthnLoginAdminApiService } from "./webauthn-login-admin-api.service";
|
||||||
import { WebauthnLoginAdminService } from "./webauthn-login-admin.service";
|
import { WebauthnLoginAdminService } from "./webauthn-login-admin.service";
|
||||||
|
|
||||||
@@ -248,6 +252,79 @@ describe("WebauthnAdminService", () => {
|
|||||||
expect(rotateKeySetMock).not.toHaveBeenCalled();
|
expect(rotateKeySetMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getRotatedData", () => {
|
||||||
|
const mockRotatedPublicKey = makeEncString("rotated_encryptedPublicKey");
|
||||||
|
const mockRotatedUserKey = makeEncString("rotated_encryptedUserKey");
|
||||||
|
const oldUserKey = makeSymmetricCryptoKey(64) as UserKey;
|
||||||
|
const newUserKey = makeSymmetricCryptoKey(64) as UserKey;
|
||||||
|
const userId = Utils.newGuid() as UserId;
|
||||||
|
|
||||||
|
it("should only include credentials with PRF keysets", async () => {
|
||||||
|
const responseUnsupported = new WebauthnLoginCredentialResponse({
|
||||||
|
id: "test-credential-id-1",
|
||||||
|
name: "Test Credential 1",
|
||||||
|
prfStatus: WebauthnLoginCredentialPrfStatus.Unsupported,
|
||||||
|
encryptedPublicKey: null,
|
||||||
|
encryptedUserKey: null,
|
||||||
|
});
|
||||||
|
const responseSupported = new WebauthnLoginCredentialResponse({
|
||||||
|
id: "test-credential-id-2",
|
||||||
|
name: "Test Credential 2",
|
||||||
|
prfStatus: WebauthnLoginCredentialPrfStatus.Supported,
|
||||||
|
encryptedPublicKey: null,
|
||||||
|
encryptedUserKey: null,
|
||||||
|
});
|
||||||
|
const responseEnabled = new WebauthnLoginCredentialResponse({
|
||||||
|
id: "test-credential-id-3",
|
||||||
|
name: "Test Credential 3",
|
||||||
|
prfStatus: WebauthnLoginCredentialPrfStatus.Enabled,
|
||||||
|
encryptedPublicKey: makeEncString("encryptedPublicKey").toJSON(),
|
||||||
|
encryptedUserKey: makeEncString("encryptedUserKey").toJSON(),
|
||||||
|
});
|
||||||
|
|
||||||
|
apiService.getCredentials.mockResolvedValue(
|
||||||
|
new ListResponse<WebauthnLoginCredentialResponse>(
|
||||||
|
{
|
||||||
|
data: [responseUnsupported, responseSupported, responseEnabled],
|
||||||
|
},
|
||||||
|
WebauthnLoginCredentialResponse,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
rotateableKeySetService.rotateKeySet.mockResolvedValue(
|
||||||
|
new RotateableKeySet<PrfKey>(mockRotatedUserKey, mockRotatedPublicKey),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await service.getRotatedData(oldUserKey, newUserKey, userId);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: "test-credential-id-3",
|
||||||
|
encryptedPublicKey: mockRotatedPublicKey,
|
||||||
|
encryptedUserKey: mockRotatedUserKey,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(rotateableKeySetService.rotateKeySet).toHaveBeenCalledTimes(1);
|
||||||
|
expect(rotateableKeySetService.rotateKeySet).toHaveBeenCalledWith(
|
||||||
|
responseEnabled.getRotateableKeyset(),
|
||||||
|
oldUserKey,
|
||||||
|
newUserKey,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should error when getCredentials fails", async () => {
|
||||||
|
const expectedError = "API connection failed";
|
||||||
|
apiService.getCredentials.mockRejectedValue(new Error(expectedError));
|
||||||
|
|
||||||
|
await expect(service.getRotatedData(oldUserKey, newUserKey, userId)).rejects.toThrow(
|
||||||
|
expectedError,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(rotateableKeySetService.rotateKeySet).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function createCredentialCreateOptions(): CredentialCreateOptionsView {
|
function createCredentialCreateOptions(): CredentialCreateOptionsView {
|
||||||
|
|||||||
Reference in New Issue
Block a user