mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-02 18:17:46 +01:00
[PM-4168] Enable encryption for registered passkeys (#7074)
* Added enable encryption * various updates and tests added. * fixing linter errors * updated spec file
This commit is contained in:
parent
180d3a99e3
commit
7051f255ed
@ -0,0 +1,25 @@
|
|||||||
|
import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request sent to the server to save a newly created prf key set for a credential.
|
||||||
|
*/
|
||||||
|
export class EnableCredentialEncryptionRequest {
|
||||||
|
/**
|
||||||
|
* The response received from the authenticator.
|
||||||
|
*/
|
||||||
|
deviceResponse: WebAuthnLoginAssertionResponseRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An encrypted token containing information the server needs to verify the credential.
|
||||||
|
*/
|
||||||
|
token: string;
|
||||||
|
|
||||||
|
/** Used for vault encryption. See {@link RotateableKeySet.encryptedUserKey } */
|
||||||
|
encryptedUserKey?: string;
|
||||||
|
|
||||||
|
/** Used for vault encryption. See {@link RotateableKeySet.encryptedPublicKey } */
|
||||||
|
encryptedPublicKey?: string;
|
||||||
|
|
||||||
|
/** Used for vault encryption. See {@link RotateableKeySet.encryptedPrivateKey } */
|
||||||
|
encryptedPrivateKey?: string;
|
||||||
|
}
|
@ -3,7 +3,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
|||||||
import { WebauthnLoginAuthenticatorResponseRequest } from "./webauthn-login-authenticator-response.request";
|
import { WebauthnLoginAuthenticatorResponseRequest } from "./webauthn-login-authenticator-response.request";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The response received from an authentiator after a successful attestation.
|
* The response received from an authenticator after a successful attestation.
|
||||||
* This request is used to save newly created webauthn login credentials to the server.
|
* This request is used to save newly created webauthn login credentials to the server.
|
||||||
*/
|
*/
|
||||||
export class WebauthnLoginAttestationResponseRequest extends WebauthnLoginAuthenticatorResponseRequest {
|
export class WebauthnLoginAttestationResponseRequest extends WebauthnLoginAuthenticatorResponseRequest {
|
||||||
|
@ -2,8 +2,10 @@ import { Injectable } from "@angular/core";
|
|||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
|
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
|
||||||
|
import { CredentialAssertionOptionsResponse } from "@bitwarden/common/auth/services/webauthn-login/response/credential-assertion-options.response";
|
||||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||||
|
|
||||||
|
import { EnableCredentialEncryptionRequest } from "./request/enable-credential-encryption.request";
|
||||||
import { SaveCredentialRequest } from "./request/save-credential.request";
|
import { SaveCredentialRequest } from "./request/save-credential.request";
|
||||||
import { WebauthnLoginCredentialCreateOptionsResponse } from "./response/webauthn-login-credential-create-options.response";
|
import { WebauthnLoginCredentialCreateOptionsResponse } from "./response/webauthn-login-credential-create-options.response";
|
||||||
import { WebauthnLoginCredentialResponse } from "./response/webauthn-login-credential.response";
|
import { WebauthnLoginCredentialResponse } from "./response/webauthn-login-credential.response";
|
||||||
@ -15,10 +17,29 @@ export class WebAuthnLoginAdminApiService {
|
|||||||
async getCredentialCreateOptions(
|
async getCredentialCreateOptions(
|
||||||
request: SecretVerificationRequest,
|
request: SecretVerificationRequest,
|
||||||
): Promise<WebauthnLoginCredentialCreateOptionsResponse> {
|
): Promise<WebauthnLoginCredentialCreateOptionsResponse> {
|
||||||
const response = await this.apiService.send("POST", "/webauthn/options", request, true, true);
|
const response = await this.apiService.send(
|
||||||
|
"POST",
|
||||||
|
"/webauthn/attestation-options",
|
||||||
|
request,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
return new WebauthnLoginCredentialCreateOptionsResponse(response);
|
return new WebauthnLoginCredentialCreateOptionsResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCredentialAssertionOptions(
|
||||||
|
request: SecretVerificationRequest,
|
||||||
|
): Promise<CredentialAssertionOptionsResponse> {
|
||||||
|
const response = await this.apiService.send(
|
||||||
|
"POST",
|
||||||
|
"/webauthn/assertion-options",
|
||||||
|
request,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return new CredentialAssertionOptionsResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
async saveCredential(request: SaveCredentialRequest): Promise<boolean> {
|
async saveCredential(request: SaveCredentialRequest): Promise<boolean> {
|
||||||
await this.apiService.send("POST", "/webauthn", request, true, true);
|
await this.apiService.send("POST", "/webauthn", request, true, true);
|
||||||
return true;
|
return true;
|
||||||
@ -31,4 +52,8 @@ export class WebAuthnLoginAdminApiService {
|
|||||||
async deleteCredential(credentialId: string, request: SecretVerificationRequest): Promise<void> {
|
async deleteCredential(credentialId: string, request: SecretVerificationRequest): Promise<void> {
|
||||||
await this.apiService.send("POST", `/webauthn/${credentialId}/delete`, request, true, true);
|
await this.apiService.send("POST", `/webauthn/${credentialId}/delete`, request, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateCredential(request: EnableCredentialEncryptionRequest): Promise<void> {
|
||||||
|
await this.apiService.send("PUT", `/webauthn`, request, true, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,21 @@
|
|||||||
|
import { randomBytes } from "crypto";
|
||||||
|
|
||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { RotateableKeySet } from "@bitwarden/auth";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { WebAuthnLoginPrfCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-prf-crypto.service.abstraction";
|
import { WebAuthnLoginPrfCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-prf-crypto.service.abstraction";
|
||||||
|
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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
|
import { PrfKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
|
||||||
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 { 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";
|
||||||
|
|
||||||
@ -18,10 +27,13 @@ describe("WebauthnAdminService", () => {
|
|||||||
let credentials: MockProxy<CredentialsContainer>;
|
let credentials: MockProxy<CredentialsContainer>;
|
||||||
let service!: WebauthnLoginAdminService;
|
let service!: WebauthnLoginAdminService;
|
||||||
|
|
||||||
|
let originalAuthenticatorAssertionResponse!: AuthenticatorAssertionResponse | any;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
// Polyfill missing class
|
// Polyfill missing class
|
||||||
window.PublicKeyCredential = class {} as any;
|
window.PublicKeyCredential = class {} as any;
|
||||||
window.AuthenticatorAttestationResponse = class {} as any;
|
window.AuthenticatorAttestationResponse = class {} as any;
|
||||||
|
window.AuthenticatorAssertionResponse = class {} as any;
|
||||||
apiService = mock<WebAuthnLoginAdminApiService>();
|
apiService = mock<WebAuthnLoginAdminApiService>();
|
||||||
userVerificationService = mock<UserVerificationService>();
|
userVerificationService = mock<UserVerificationService>();
|
||||||
rotateableKeySetService = mock<RotateableKeySetService>();
|
rotateableKeySetService = mock<RotateableKeySetService>();
|
||||||
@ -34,6 +46,20 @@ describe("WebauthnAdminService", () => {
|
|||||||
webAuthnLoginPrfCryptoService,
|
webAuthnLoginPrfCryptoService,
|
||||||
credentials,
|
credentials,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Save original global class
|
||||||
|
originalAuthenticatorAssertionResponse = global.AuthenticatorAssertionResponse;
|
||||||
|
// Mock the global AuthenticatorAssertionResponse class b/c the class is only available in secure contexts
|
||||||
|
global.AuthenticatorAssertionResponse = MockAuthenticatorAssertionResponse;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
// Restore global after all tests are done
|
||||||
|
global.AuthenticatorAssertionResponse = originalAuthenticatorAssertionResponse;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("createCredential", () => {
|
describe("createCredential", () => {
|
||||||
@ -70,6 +96,94 @@ describe("WebauthnAdminService", () => {
|
|||||||
expect(result.supportsPrf).toBe(true);
|
expect(result.supportsPrf).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("enableCredentialEncryption", () => {
|
||||||
|
it("should call the necessary methods to update the credential", async () => {
|
||||||
|
// Arrange
|
||||||
|
const response = new MockPublicKeyCredential();
|
||||||
|
const prfKeySet = new RotateableKeySet<PrfKey>(
|
||||||
|
new EncString("test_encryptedUserKey"),
|
||||||
|
new EncString("test_encryptedPublicKey"),
|
||||||
|
new EncString("test_encryptedPrivateKey"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const assertionOptions: WebAuthnLoginCredentialAssertionView =
|
||||||
|
new WebAuthnLoginCredentialAssertionView(
|
||||||
|
"enable_credential_encryption_test_token",
|
||||||
|
new WebAuthnLoginAssertionResponseRequest(response),
|
||||||
|
{} as PrfKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
const request = new EnableCredentialEncryptionRequest();
|
||||||
|
request.token = assertionOptions.token;
|
||||||
|
request.deviceResponse = assertionOptions.deviceResponse;
|
||||||
|
request.encryptedUserKey = prfKeySet.encryptedUserKey.encryptedString;
|
||||||
|
request.encryptedPublicKey = prfKeySet.encryptedPublicKey.encryptedString;
|
||||||
|
request.encryptedPrivateKey = prfKeySet.encryptedPrivateKey.encryptedString;
|
||||||
|
|
||||||
|
// Mock the necessary methods and services
|
||||||
|
const createKeySetMock = jest
|
||||||
|
.spyOn(rotateableKeySetService, "createKeySet")
|
||||||
|
.mockResolvedValue(prfKeySet);
|
||||||
|
const updateCredentialMock = jest.spyOn(apiService, "updateCredential").mockResolvedValue();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await service.enableCredentialEncryption(assertionOptions);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(createKeySetMock).toHaveBeenCalledWith(assertionOptions.prfKey);
|
||||||
|
expect(updateCredentialMock).toHaveBeenCalledWith(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error when PRF Key is undefined", async () => {
|
||||||
|
// Arrange
|
||||||
|
const response = new MockPublicKeyCredential();
|
||||||
|
|
||||||
|
const assertionOptions: WebAuthnLoginCredentialAssertionView =
|
||||||
|
new WebAuthnLoginCredentialAssertionView(
|
||||||
|
"enable_credential_encryption_test_token",
|
||||||
|
new WebAuthnLoginAssertionResponseRequest(response),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock the necessary methods and services
|
||||||
|
const createKeySetMock = jest
|
||||||
|
.spyOn(rotateableKeySetService, "createKeySet")
|
||||||
|
.mockResolvedValue(null);
|
||||||
|
const updateCredentialMock = jest.spyOn(apiService, "updateCredential").mockResolvedValue();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
try {
|
||||||
|
await service.enableCredentialEncryption(assertionOptions);
|
||||||
|
} catch (error) {
|
||||||
|
// Assert
|
||||||
|
expect(error).toEqual(new Error("invalid credential"));
|
||||||
|
expect(createKeySetMock).not.toHaveBeenCalled();
|
||||||
|
expect(updateCredentialMock).not.toHaveBeenCalled();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error when WehAuthnLoginCredentialAssertionView is undefined", async () => {
|
||||||
|
// Arrange
|
||||||
|
const assertionOptions: WebAuthnLoginCredentialAssertionView = undefined;
|
||||||
|
|
||||||
|
// Mock the necessary methods and services
|
||||||
|
const createKeySetMock = jest
|
||||||
|
.spyOn(rotateableKeySetService, "createKeySet")
|
||||||
|
.mockResolvedValue(null);
|
||||||
|
const updateCredentialMock = jest.spyOn(apiService, "updateCredential").mockResolvedValue();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
try {
|
||||||
|
await service.enableCredentialEncryption(assertionOptions);
|
||||||
|
} catch (error) {
|
||||||
|
// Assert
|
||||||
|
expect(error).toEqual(new Error("invalid credential"));
|
||||||
|
expect(createKeySetMock).not.toHaveBeenCalled();
|
||||||
|
expect(updateCredentialMock).not.toHaveBeenCalled();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function createCredentialCreateOptions(): CredentialCreateOptionsView {
|
function createCredentialCreateOptions(): CredentialCreateOptionsView {
|
||||||
@ -115,3 +229,58 @@ function createDeviceResponse({ prf = false }: { prf?: boolean } = {}): PublicKe
|
|||||||
|
|
||||||
return credential;
|
return credential;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mocks for the PublicKeyCredential and AuthenticatorAssertionResponse classes copied from webauthn-login.service.spec.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
// AuthenticatorAssertionResponse && PublicKeyCredential are only available in secure contexts
|
||||||
|
// so we need to mock them and assign them to the global object to make them available
|
||||||
|
// for the tests
|
||||||
|
class MockAuthenticatorAssertionResponse implements AuthenticatorAssertionResponse {
|
||||||
|
clientDataJSON: ArrayBuffer = randomBytes(32).buffer;
|
||||||
|
authenticatorData: ArrayBuffer = randomBytes(196).buffer;
|
||||||
|
signature: ArrayBuffer = randomBytes(72).buffer;
|
||||||
|
userHandle: ArrayBuffer = randomBytes(16).buffer;
|
||||||
|
|
||||||
|
clientDataJSONB64Str = Utils.fromBufferToUrlB64(this.clientDataJSON);
|
||||||
|
authenticatorDataB64Str = Utils.fromBufferToUrlB64(this.authenticatorData);
|
||||||
|
signatureB64Str = Utils.fromBufferToUrlB64(this.signature);
|
||||||
|
userHandleB64Str = Utils.fromBufferToUrlB64(this.userHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockPublicKeyCredential implements PublicKeyCredential {
|
||||||
|
authenticatorAttachment = "cross-platform";
|
||||||
|
id = "mockCredentialId";
|
||||||
|
type = "public-key";
|
||||||
|
rawId: ArrayBuffer = randomBytes(32).buffer;
|
||||||
|
rawIdB64Str = Utils.fromBufferToUrlB64(this.rawId);
|
||||||
|
|
||||||
|
response: MockAuthenticatorAssertionResponse = new MockAuthenticatorAssertionResponse();
|
||||||
|
|
||||||
|
// Use random 64 character hex string (32 bytes - matters for symmetric key creation)
|
||||||
|
// to represent the prf key binary data and convert to ArrayBuffer
|
||||||
|
// Creating the array buffer from a known hex value allows us to
|
||||||
|
// assert on the value in tests
|
||||||
|
private prfKeyArrayBuffer: ArrayBuffer = Utils.hexStringToArrayBuffer(
|
||||||
|
"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||||
|
);
|
||||||
|
|
||||||
|
getClientExtensionResults(): any {
|
||||||
|
return {
|
||||||
|
prf: {
|
||||||
|
results: {
|
||||||
|
first: this.prfKeyArrayBuffer,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static isConditionalMediationAvailable(): Promise<boolean> {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static isUserVerifyingPlatformAuthenticatorAvailable(): Promise<boolean> {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,6 +4,8 @@ import { BehaviorSubject, filter, from, map, Observable, shareReplay, switchMap,
|
|||||||
import { PrfKeySet } from "@bitwarden/auth";
|
import { PrfKeySet } from "@bitwarden/auth";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { WebAuthnLoginPrfCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-prf-crypto.service.abstraction";
|
import { WebAuthnLoginPrfCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-prf-crypto.service.abstraction";
|
||||||
|
import { WebAuthnLoginCredentialAssertionOptionsView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion-options.view";
|
||||||
|
import { WebAuthnLoginCredentialAssertionView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion.view";
|
||||||
import { Verification } from "@bitwarden/common/auth/types/verification";
|
import { Verification } from "@bitwarden/common/auth/types/verification";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
|
||||||
@ -12,6 +14,7 @@ import { PendingWebauthnLoginCredentialView } from "../../views/pending-webauthn
|
|||||||
import { WebauthnLoginCredentialView } from "../../views/webauthn-login-credential.view";
|
import { WebauthnLoginCredentialView } from "../../views/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 { SaveCredentialRequest } from "./request/save-credential.request";
|
import { SaveCredentialRequest } from "./request/save-credential.request";
|
||||||
import { WebauthnLoginAttestationResponseRequest } from "./request/webauthn-login-attestation-response.request";
|
import { WebauthnLoginAttestationResponseRequest } from "./request/webauthn-login-attestation-response.request";
|
||||||
import { WebAuthnLoginAdminApiService } from "./webauthn-login-admin-api.service";
|
import { WebAuthnLoginAdminApiService } from "./webauthn-login-admin-api.service";
|
||||||
@ -52,14 +55,31 @@ export class WebauthnLoginAdminService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the credential attestation options needed for initiating the WebAuthnLogin credentail creation process.
|
* Get the credential assertion options needed for initiating the WebAuthnLogin credential update process.
|
||||||
|
* The options contains assertion options and other data for the authenticator.
|
||||||
|
* This method requires user verification.
|
||||||
|
*
|
||||||
|
* @param verification User verification data to be used for the request.
|
||||||
|
* @returns The credential assertion options and a token to be used for the credential update request.
|
||||||
|
*/
|
||||||
|
async getCredentialAssertOptions(
|
||||||
|
verification: Verification,
|
||||||
|
): Promise<WebAuthnLoginCredentialAssertionOptionsView> {
|
||||||
|
const request = await this.userVerificationService.buildRequest(verification);
|
||||||
|
const response = await this.apiService.getCredentialAssertionOptions(request);
|
||||||
|
return new WebAuthnLoginCredentialAssertionOptionsView(response.options, response.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the credential attestation options needed for initiating the WebAuthnLogin credential creation process.
|
||||||
* The options contains a challenge and other data for the authenticator.
|
* The options contains a challenge and other data for the authenticator.
|
||||||
* This method requires user verification.
|
* This method requires user verification.
|
||||||
*
|
*
|
||||||
* @param verification User verification data to be used for the request.
|
* @param verification User verification data to be used for the request.
|
||||||
* @returns The credential attestation options and a token to be used for the credential creation request.
|
* @returns The credential attestation options and a token to be used for the credential creation request.
|
||||||
*/
|
*/
|
||||||
async getCredentialCreateOptions(
|
|
||||||
|
async getCredentialAttestationOptions(
|
||||||
verification: Verification,
|
verification: Verification,
|
||||||
): Promise<CredentialCreateOptionsView> {
|
): Promise<CredentialCreateOptionsView> {
|
||||||
const request = await this.userVerificationService.buildRequest(verification);
|
const request = await this.userVerificationService.buildRequest(verification);
|
||||||
@ -169,6 +189,36 @@ export class WebauthnLoginAdminService {
|
|||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable encryption for a credential that has already been saved to the server.
|
||||||
|
* This will update the KeySet associated with the credential in the database.
|
||||||
|
* We short circuit the process here incase the WebAuthnLoginCredential doesn't support PRF or
|
||||||
|
* if there was a problem with the Credential Assertion.
|
||||||
|
*
|
||||||
|
* @param assertionOptions Options received from the server using `getCredentialAssertOptions`.
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
async enableCredentialEncryption(
|
||||||
|
assertionOptions: WebAuthnLoginCredentialAssertionView,
|
||||||
|
): Promise<void> {
|
||||||
|
if (assertionOptions === undefined || assertionOptions?.prfKey === undefined) {
|
||||||
|
throw new Error("invalid credential");
|
||||||
|
}
|
||||||
|
|
||||||
|
const prfKeySet: PrfKeySet = await this.rotateableKeySetService.createKeySet(
|
||||||
|
assertionOptions.prfKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
const request = new EnableCredentialEncryptionRequest();
|
||||||
|
request.token = assertionOptions.token;
|
||||||
|
request.deviceResponse = assertionOptions.deviceResponse;
|
||||||
|
request.encryptedUserKey = prfKeySet.encryptedUserKey.encryptedString;
|
||||||
|
request.encryptedPublicKey = prfKeySet.encryptedPublicKey.encryptedString;
|
||||||
|
request.encryptedPrivateKey = prfKeySet.encryptedPrivateKey.encryptedString;
|
||||||
|
await this.apiService.updateCredential(request);
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of webauthn credentials saved on the server.
|
* List of webauthn credentials saved on the server.
|
||||||
*
|
*
|
||||||
|
@ -94,7 +94,7 @@ export class CreateCredentialDialogComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.credentialOptions = await this.webauthnService.getCredentialCreateOptions(
|
this.credentialOptions = await this.webauthnService.getCredentialAttestationOptions(
|
||||||
this.formGroup.value.userVerification.secret,
|
this.formGroup.value.userVerification.secret,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||||
|
<bit-dialog dialogSize="large" [loading]="loading$ | async">
|
||||||
|
<span bitDialogTitle
|
||||||
|
>{{ "enablePasskeyEncryption" | i18n }}
|
||||||
|
<span *ngIf="credential" class="tw-text-sm tw-normal-case tw-text-muted">{{
|
||||||
|
credential.name
|
||||||
|
}}</span>
|
||||||
|
</span>
|
||||||
|
<ng-container bitDialogContent>
|
||||||
|
<ng-container *ngIf="!credential">
|
||||||
|
<i class="bwi bwi-spinner bwi-spin tw-ml-1" aria-hidden="true"></i>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="credential">
|
||||||
|
<p bitTypography="body1">{{ "useForVaultEncryptionInfo" | i18n }}</p>
|
||||||
|
|
||||||
|
<ng-container formGroupName="userVerification">
|
||||||
|
<app-user-verification
|
||||||
|
formControlName="secret"
|
||||||
|
[(invalidSecret)]="invalidSecret"
|
||||||
|
></app-user-verification>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container bitDialogFooter>
|
||||||
|
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||||
|
{{ "submit" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button type="button" bitButton bitFormButton buttonType="secondary" bitDialogClose>
|
||||||
|
{{ "cancel" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</bit-dialog>
|
||||||
|
</form>
|
@ -0,0 +1,91 @@
|
|||||||
|
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||||
|
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { FormBuilder, Validators } from "@angular/forms";
|
||||||
|
import { Subject } from "rxjs";
|
||||||
|
import { takeUntil } from "rxjs/operators";
|
||||||
|
|
||||||
|
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
|
||||||
|
import { WebAuthnLoginCredentialAssertionOptionsView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion-options.view";
|
||||||
|
import { Verification } from "@bitwarden/common/auth/types/verification";
|
||||||
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
|
import { DialogService } from "@bitwarden/components/src/dialog/dialog.service";
|
||||||
|
|
||||||
|
import { WebauthnLoginAdminService } from "../../../core/services/webauthn-login/webauthn-login-admin.service";
|
||||||
|
import { WebauthnLoginCredentialView } from "../../../core/views/webauthn-login-credential.view";
|
||||||
|
|
||||||
|
export interface EnableEncryptionDialogParams {
|
||||||
|
credentialId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: "enable-encryption-dialog.component.html",
|
||||||
|
})
|
||||||
|
export class EnableEncryptionDialogComponent implements OnInit, OnDestroy {
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
protected invalidSecret = false;
|
||||||
|
protected formGroup = this.formBuilder.group({
|
||||||
|
userVerification: this.formBuilder.group({
|
||||||
|
secret: [null as Verification | null, Validators.required],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
protected credential?: WebauthnLoginCredentialView;
|
||||||
|
protected credentialOptions?: WebAuthnLoginCredentialAssertionOptionsView;
|
||||||
|
protected loading$ = this.webauthnService.loading$;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DIALOG_DATA) private params: EnableEncryptionDialogParams,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private dialogRef: DialogRef,
|
||||||
|
private webauthnService: WebauthnLoginAdminService,
|
||||||
|
private webauthnLoginService: WebAuthnLoginServiceAbstraction,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.webauthnService
|
||||||
|
.getCredential$(this.params.credentialId)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe((credential: any) => (this.credential = credential));
|
||||||
|
}
|
||||||
|
|
||||||
|
submit = async () => {
|
||||||
|
if (this.credential === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dialogRef.disableClose = true;
|
||||||
|
try {
|
||||||
|
this.credentialOptions = await this.webauthnService.getCredentialAssertOptions(
|
||||||
|
this.formGroup.value.userVerification.secret,
|
||||||
|
);
|
||||||
|
await this.webauthnService.enableCredentialEncryption(
|
||||||
|
await this.webauthnLoginService.assertCredential(this.credentialOptions),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ErrorResponse && error.statusCode === 400) {
|
||||||
|
this.invalidSecret = true;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dialogRef.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strongly typed helper to open a EnableEncryptionDialogComponent
|
||||||
|
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||||
|
* @param config Configuration for the dialog
|
||||||
|
*/
|
||||||
|
export const openEnableCredentialDialogComponent = (
|
||||||
|
dialogService: DialogService,
|
||||||
|
config: DialogConfig<EnableEncryptionDialogParams>,
|
||||||
|
) => {
|
||||||
|
return dialogService.open<unknown>(EnableEncryptionDialogComponent, config);
|
||||||
|
};
|
@ -39,8 +39,16 @@
|
|||||||
<span bitTypography="body1" class="tw-text-muted">{{ "usedForEncryption" | i18n }}</span>
|
<span bitTypography="body1" class="tw-text-muted">{{ "usedForEncryption" | i18n }}</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="credential.prfStatus === WebauthnLoginCredentialPrfStatus.Supported">
|
<ng-container *ngIf="credential.prfStatus === WebauthnLoginCredentialPrfStatus.Supported">
|
||||||
<i class="bwi bwi-lock-encrypted"></i>
|
<button
|
||||||
<span bitTypography="body1" class="tw-text-muted">{{ "encryptionNotEnabled" | i18n }}</span>
|
type="button"
|
||||||
|
bitLink
|
||||||
|
[disabled]="loading"
|
||||||
|
[attr.aria-label]="('enablePasskeyEncryption' | i18n) + ' ' + credential.name"
|
||||||
|
(click)="enableEncryption(credential.id)"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-lock-encrypted"></i>
|
||||||
|
{{ "enablePasskeyEncryption" | i18n }}
|
||||||
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<span
|
<span
|
||||||
*ngIf="credential.prfStatus === WebauthnLoginCredentialPrfStatus.Unsupported"
|
*ngIf="credential.prfStatus === WebauthnLoginCredentialPrfStatus.Unsupported"
|
||||||
|
@ -11,6 +11,7 @@ import { WebauthnLoginCredentialView } from "../../core/views/webauthn-login-cre
|
|||||||
|
|
||||||
import { openCreateCredentialDialog } from "./create-credential-dialog/create-credential-dialog.component";
|
import { openCreateCredentialDialog } from "./create-credential-dialog/create-credential-dialog.component";
|
||||||
import { openDeleteCredentialDialogComponent } from "./delete-credential-dialog/delete-credential-dialog.component";
|
import { openDeleteCredentialDialogComponent } from "./delete-credential-dialog/delete-credential-dialog.component";
|
||||||
|
import { openEnableCredentialDialogComponent } from "./enable-encryption-dialog/enable-encryption-dialog.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-webauthn-login-settings",
|
selector: "app-webauthn-login-settings",
|
||||||
@ -83,4 +84,8 @@ export class WebauthnLoginSettingsComponent implements OnInit, OnDestroy {
|
|||||||
protected deleteCredential(credentialId: string) {
|
protected deleteCredential(credentialId: string) {
|
||||||
openDeleteCredentialDialogComponent(this.dialogService, { data: { credentialId } });
|
openDeleteCredentialDialogComponent(this.dialogService, { data: { credentialId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected enableEncryption(credentialId: string) {
|
||||||
|
openEnableCredentialDialogComponent(this.dialogService, { data: { credentialId } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import { UserVerificationModule } from "../../shared/components/user-verificatio
|
|||||||
|
|
||||||
import { CreateCredentialDialogComponent } from "./create-credential-dialog/create-credential-dialog.component";
|
import { CreateCredentialDialogComponent } from "./create-credential-dialog/create-credential-dialog.component";
|
||||||
import { DeleteCredentialDialogComponent } from "./delete-credential-dialog/delete-credential-dialog.component";
|
import { DeleteCredentialDialogComponent } from "./delete-credential-dialog/delete-credential-dialog.component";
|
||||||
|
import { EnableEncryptionDialogComponent } from "./enable-encryption-dialog/enable-encryption-dialog.component";
|
||||||
import { WebauthnLoginSettingsComponent } from "./webauthn-login-settings.component";
|
import { WebauthnLoginSettingsComponent } from "./webauthn-login-settings.component";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@ -16,6 +17,7 @@ import { WebauthnLoginSettingsComponent } from "./webauthn-login-settings.compon
|
|||||||
WebauthnLoginSettingsComponent,
|
WebauthnLoginSettingsComponent,
|
||||||
CreateCredentialDialogComponent,
|
CreateCredentialDialogComponent,
|
||||||
DeleteCredentialDialogComponent,
|
DeleteCredentialDialogComponent,
|
||||||
|
EnableEncryptionDialogComponent,
|
||||||
],
|
],
|
||||||
exports: [WebauthnLoginSettingsComponent],
|
exports: [WebauthnLoginSettingsComponent],
|
||||||
})
|
})
|
||||||
|
@ -674,8 +674,8 @@
|
|||||||
"encryptionNotSupported": {
|
"encryptionNotSupported": {
|
||||||
"message": "Encryption not supported"
|
"message": "Encryption not supported"
|
||||||
},
|
},
|
||||||
"encryptionNotEnabled": {
|
"enablePasskeyEncryption": {
|
||||||
"message": "Encryption not enabled"
|
"message": "Set up encryption"
|
||||||
},
|
},
|
||||||
"usedForEncryption": {
|
"usedForEncryption": {
|
||||||
"message": "Used for encryption"
|
"message": "Used for encryption"
|
||||||
|
Loading…
Reference in New Issue
Block a user