1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 05:13:29 +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:
Maciej Zieniuk
2025-09-30 17:29:58 +02:00
committed by GitHub
parent 727689d827
commit d4f68e8bad
3 changed files with 88 additions and 2 deletions

View File

@@ -1,7 +1,16 @@
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum WebauthnLoginCredentialPrfStatus {
/**
* Encrypted user key present, PRF function is supported.
*/
Enabled = 0,
/**
* PRF function is supported.
*/
Supported = 1,
/**
* PRF function is not supported.
*/
Unsupported = 2,
}

View File

@@ -40,6 +40,6 @@ export class WebauthnLoginCredentialResponse extends BaseResponse {
}
hasPrfKeyset(): boolean {
return this.encryptedUserKey != null && this.encryptedPublicKey != null;
return this.prfStatus === WebauthnLoginCredentialPrfStatus.Enabled;
}
}

View File

@@ -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 { 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 { ListResponse } from "@bitwarden/common/models/response/list.response";
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 { UserId } from "@bitwarden/user-core";
import { WebauthnLoginCredentialPrfStatus } from "../../enums/webauthn-login-credential-prf-status.enum";
import { CredentialCreateOptionsView } from "../../views/credential-create-options.view";
import { PendingWebauthnLoginCredentialView } from "../../views/pending-webauthn-login-credential.view";
import { RotateableKeySetService } from "../rotateable-key-set.service";
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 { WebauthnLoginAdminService } from "./webauthn-login-admin.service";
@@ -248,6 +252,79 @@ describe("WebauthnAdminService", () => {
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 {