From 7051f255edbd4ec3ca56e4e995aa35961d035b38 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Wed, 13 Dec 2023 07:02:35 -0800 Subject: [PATCH] [PM-4168] Enable encryption for registered passkeys (#7074) * Added enable encryption * various updates and tests added. * fixing linter errors * updated spec file --- .../enable-credential-encryption.request.ts | 25 +++ ...uthn-login-attestation-response.request.ts | 2 +- .../webauthn-login-admin-api.service.ts | 27 ++- .../webauthn-login-admin.service.spec.ts | 169 ++++++++++++++++++ .../webauthn-login-admin.service.ts | 54 +++++- .../create-credential-dialog.component.ts | 2 +- .../enable-encryption-dialog.component.html | 34 ++++ .../enable-encryption-dialog.component.ts | 91 ++++++++++ .../webauthn-login-settings.component.html | 12 +- .../webauthn-login-settings.component.ts | 5 + .../webauthn-login-settings.module.ts | 2 + apps/web/src/locales/en/messages.json | 4 +- 12 files changed, 418 insertions(+), 9 deletions(-) create mode 100644 apps/web/src/app/auth/core/services/webauthn-login/request/enable-credential-encryption.request.ts create mode 100644 apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.html create mode 100644 apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts diff --git a/apps/web/src/app/auth/core/services/webauthn-login/request/enable-credential-encryption.request.ts b/apps/web/src/app/auth/core/services/webauthn-login/request/enable-credential-encryption.request.ts new file mode 100644 index 0000000000..6dc08728ad --- /dev/null +++ b/apps/web/src/app/auth/core/services/webauthn-login/request/enable-credential-encryption.request.ts @@ -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; +} diff --git a/apps/web/src/app/auth/core/services/webauthn-login/request/webauthn-login-attestation-response.request.ts b/apps/web/src/app/auth/core/services/webauthn-login/request/webauthn-login-attestation-response.request.ts index 249b2ebffa..ef3d657f2f 100644 --- a/apps/web/src/app/auth/core/services/webauthn-login/request/webauthn-login-attestation-response.request.ts +++ b/apps/web/src/app/auth/core/services/webauthn-login/request/webauthn-login-attestation-response.request.ts @@ -3,7 +3,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; 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. */ export class WebauthnLoginAttestationResponseRequest extends WebauthnLoginAuthenticatorResponseRequest { diff --git a/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin-api.service.ts b/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin-api.service.ts index 8b99396b0f..efa32d0c6f 100644 --- a/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin-api.service.ts +++ b/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin-api.service.ts @@ -2,8 +2,10 @@ import { Injectable } from "@angular/core"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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 { EnableCredentialEncryptionRequest } from "./request/enable-credential-encryption.request"; import { SaveCredentialRequest } from "./request/save-credential.request"; import { WebauthnLoginCredentialCreateOptionsResponse } from "./response/webauthn-login-credential-create-options.response"; import { WebauthnLoginCredentialResponse } from "./response/webauthn-login-credential.response"; @@ -15,10 +17,29 @@ export class WebAuthnLoginAdminApiService { async getCredentialCreateOptions( request: SecretVerificationRequest, ): Promise { - 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); } + async getCredentialAssertionOptions( + request: SecretVerificationRequest, + ): Promise { + const response = await this.apiService.send( + "POST", + "/webauthn/assertion-options", + request, + true, + true, + ); + return new CredentialAssertionOptionsResponse(response); + } + async saveCredential(request: SaveCredentialRequest): Promise { await this.apiService.send("POST", "/webauthn", request, true, true); return true; @@ -31,4 +52,8 @@ export class WebAuthnLoginAdminApiService { async deleteCredential(credentialId: string, request: SecretVerificationRequest): Promise { await this.apiService.send("POST", `/webauthn/${credentialId}/delete`, request, true, true); } + + async updateCredential(request: EnableCredentialEncryptionRequest): Promise { + await this.apiService.send("PUT", `/webauthn`, request, true, true); + } } diff --git a/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.spec.ts b/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.spec.ts index 49c1f89052..bc92114e87 100644 --- a/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.spec.ts +++ b/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.spec.ts @@ -1,12 +1,21 @@ +import { randomBytes } from "crypto"; + 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 { 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 { PendingWebauthnLoginCredentialView } from "../../views/pending-webauthn-login-credential.view"; import { RotateableKeySetService } from "../rotateable-key-set.service"; +import { EnableCredentialEncryptionRequest } from "./request/enable-credential-encryption.request"; import { WebAuthnLoginAdminApiService } from "./webauthn-login-admin-api.service"; import { WebauthnLoginAdminService } from "./webauthn-login-admin.service"; @@ -18,10 +27,13 @@ describe("WebauthnAdminService", () => { let credentials: MockProxy; let service!: WebauthnLoginAdminService; + let originalAuthenticatorAssertionResponse!: AuthenticatorAssertionResponse | any; + beforeAll(() => { // Polyfill missing class window.PublicKeyCredential = class {} as any; window.AuthenticatorAttestationResponse = class {} as any; + window.AuthenticatorAssertionResponse = class {} as any; apiService = mock(); userVerificationService = mock(); rotateableKeySetService = mock(); @@ -34,6 +46,20 @@ describe("WebauthnAdminService", () => { webAuthnLoginPrfCryptoService, 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", () => { @@ -70,6 +96,94 @@ describe("WebauthnAdminService", () => { 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( + 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 { @@ -115,3 +229,58 @@ function createDeviceResponse({ prf = false }: { prf?: boolean } = {}): PublicKe 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 { + return Promise.resolve(false); + } + + static isUserVerifyingPlatformAuthenticatorAvailable(): Promise { + return Promise.resolve(false); + } +} diff --git a/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.ts b/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.ts index 1fee82c74b..a59b2395e1 100644 --- a/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.ts +++ b/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.ts @@ -4,6 +4,8 @@ import { BehaviorSubject, filter, from, map, Observable, shareReplay, switchMap, import { PrfKeySet } from "@bitwarden/auth"; 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 { 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 { 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 { RotateableKeySetService } from "../rotateable-key-set.service"; +import { EnableCredentialEncryptionRequest } from "./request/enable-credential-encryption.request"; import { SaveCredentialRequest } from "./request/save-credential.request"; import { WebauthnLoginAttestationResponseRequest } from "./request/webauthn-login-attestation-response.request"; 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 { + 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. * This method requires user verification. * * @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. */ - async getCredentialCreateOptions( + + async getCredentialAttestationOptions( verification: Verification, ): Promise { const request = await this.userVerificationService.buildRequest(verification); @@ -169,6 +189,36 @@ export class WebauthnLoginAdminService { 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 { + 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. * diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts index fcb0e995f4..4c5198ea13 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts @@ -94,7 +94,7 @@ export class CreateCredentialDialogComponent implements OnInit { } try { - this.credentialOptions = await this.webauthnService.getCredentialCreateOptions( + this.credentialOptions = await this.webauthnService.getCredentialAttestationOptions( this.formGroup.value.userVerification.secret, ); } catch (error) { diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.html b/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.html new file mode 100644 index 0000000000..3fe6f43a05 --- /dev/null +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.html @@ -0,0 +1,34 @@ +
+ + {{ "enablePasskeyEncryption" | i18n }} + {{ + credential.name + }} + + + + + + + +

{{ "useForVaultEncryptionInfo" | i18n }}

+ + + + +
+
+ + + + +
+
diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts new file mode 100644 index 0000000000..741b71abcf --- /dev/null +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts @@ -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(); + + 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, +) => { + return dialogService.open(EnableEncryptionDialogComponent, config); +}; diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html b/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html index dc55be99f1..968b8565a6 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html @@ -39,8 +39,16 @@ {{ "usedForEncryption" | i18n }} - - {{ "encryptionNotEnabled" | i18n }} +