mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-25 12:15:18 +01:00
[PM-2241] Add PRF attestation flow during passkey registration (#6525)
* [PM-2241] chore: refactor into new "pending" view type * [PM-2241] feat: record PRF support * [PM-2241] feat: add prf checkbox to dialog * [PM-2241] chore: remove `disableMargin` instead Will expressed his concern that these things aren't sustainable, and that we should try using `!important` statements instead, which is a good point! * [PM-2241] feat: add prf registration * [PM-2241] feat: add support for `prfStatus` * [PM-2241] feat: add rotateable key set * [PM-2241] feat: add PRF creation error handling * [PM-2241] chore: improve rotateable key docs * [PM-2241] feat: add basic test * [PM-2241] chore: update `SaveCredentialRequest` docs * [PM-2241] chore: rename to `WebauthnLoginAdminService` * [PM-2241] fix: typo in `save-credential.request.ts` * [PM-2241] fix: typo in more places
This commit is contained in:
parent
c7b448cdc8
commit
65d2d74348
@ -0,0 +1,5 @@
|
|||||||
|
export enum WebauthnLoginCredentialPrfStatus {
|
||||||
|
Enabled = 0,
|
||||||
|
Supported = 1,
|
||||||
|
Unsupported = 2,
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
import { TestBed } from "@angular/core/testing";
|
||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
|
||||||
|
import { RotateableKeySetService } from "./rotateable-key-set.service";
|
||||||
|
|
||||||
|
describe("RotateableKeySetService", () => {
|
||||||
|
let testBed!: TestBed;
|
||||||
|
let cryptoService!: MockProxy<CryptoService>;
|
||||||
|
let encryptService!: MockProxy<EncryptService>;
|
||||||
|
let service!: RotateableKeySetService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cryptoService = mock<CryptoService>();
|
||||||
|
encryptService = mock<EncryptService>();
|
||||||
|
testBed = TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: CryptoService, useValue: cryptoService },
|
||||||
|
{ provide: EncryptService, useValue: encryptService },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
service = testBed.inject(RotateableKeySetService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createKeySet", () => {
|
||||||
|
it("should create a new key set", async () => {
|
||||||
|
const externalKey = createSymmetricKey();
|
||||||
|
const userKey = createSymmetricKey();
|
||||||
|
const encryptedUserKey = Symbol();
|
||||||
|
const encryptedPublicKey = Symbol();
|
||||||
|
const encryptedPrivateKey = Symbol();
|
||||||
|
cryptoService.makeKeyPair.mockResolvedValue(["publicKey", encryptedPrivateKey as any]);
|
||||||
|
cryptoService.getUserKey.mockResolvedValue({ key: userKey.key } as any);
|
||||||
|
cryptoService.rsaEncrypt.mockResolvedValue(encryptedUserKey as any);
|
||||||
|
encryptService.encrypt.mockResolvedValue(encryptedPublicKey as any);
|
||||||
|
|
||||||
|
const result = await service.createKeySet(externalKey as any);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
encryptedUserKey,
|
||||||
|
encryptedPublicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function createSymmetricKey() {
|
||||||
|
const key = Utils.fromB64ToArray(
|
||||||
|
"1h-TuPwSbX5qoX0aVgjmda_Lfq85qAcKssBlXZnPIsQC3HNDGIecunYqXhJnp55QpdXRh-egJiLH3a0wqlVQsQ"
|
||||||
|
);
|
||||||
|
return new SymmetricCryptoKey(key);
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import { inject, Injectable } from "@angular/core";
|
||||||
|
|
||||||
|
import { RotateableKeySet } from "@bitwarden/auth";
|
||||||
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
|
||||||
|
@Injectable({ providedIn: "root" })
|
||||||
|
export class RotateableKeySetService {
|
||||||
|
private readonly cryptoService = inject(CryptoService);
|
||||||
|
private readonly encryptService = inject(EncryptService);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new rotateable key set for the current user, using the provided external key.
|
||||||
|
* For more information on rotateable key sets, see {@link RotateableKeySet}
|
||||||
|
*
|
||||||
|
* @param externalKey The `ExternalKey` used to encrypt {@link RotateableKeySet.encryptedPrivateKey}
|
||||||
|
* @returns RotateableKeySet containing the current users `UserKey`
|
||||||
|
*/
|
||||||
|
async createKeySet<ExternalKey extends SymmetricCryptoKey>(
|
||||||
|
externalKey: ExternalKey
|
||||||
|
): Promise<RotateableKeySet<ExternalKey>> {
|
||||||
|
const [publicKey, encryptedPrivateKey] = await this.cryptoService.makeKeyPair(externalKey);
|
||||||
|
|
||||||
|
const userKey = await this.cryptoService.getUserKey();
|
||||||
|
const rawPublicKey = Utils.fromB64ToArray(publicKey);
|
||||||
|
const encryptedUserKey = await this.cryptoService.rsaEncrypt(userKey.key, rawPublicKey);
|
||||||
|
const encryptedPublicKey = await this.encryptService.encrypt(rawPublicKey, userKey);
|
||||||
|
return new RotateableKeySet(encryptedUserKey, encryptedPublicKey, encryptedPrivateKey);
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1 @@
|
|||||||
export * from "./webauthn-login.service";
|
export * from "./webauthn-login-admin.service";
|
||||||
|
@ -4,7 +4,10 @@ import { WebauthnLoginAttestationResponseRequest } from "./webauthn-login-attest
|
|||||||
* Request sent to the server to save a newly created webauthn login credential.
|
* Request sent to the server to save a newly created webauthn login credential.
|
||||||
*/
|
*/
|
||||||
export class SaveCredentialRequest {
|
export class SaveCredentialRequest {
|
||||||
/** The response recieved from the authenticator. This contains the public key */
|
/**
|
||||||
|
* The response received from the authenticator.
|
||||||
|
* This contains all information needed for future authentication flows.
|
||||||
|
*/
|
||||||
deviceResponse: WebauthnLoginAttestationResponseRequest;
|
deviceResponse: WebauthnLoginAttestationResponseRequest;
|
||||||
|
|
||||||
/** Nickname chosen by the user to identify this credential */
|
/** Nickname chosen by the user to identify this credential */
|
||||||
@ -15,4 +18,18 @@ export class SaveCredentialRequest {
|
|||||||
* It contains encrypted information that the server needs to verify the credential.
|
* It contains encrypted information that the server needs to verify the credential.
|
||||||
*/
|
*/
|
||||||
token: string;
|
token: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the credential was created with PRF support.
|
||||||
|
*/
|
||||||
|
supportsPrf: boolean;
|
||||||
|
|
||||||
|
/** 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 recieved from an authentiator after a successful attestation.
|
* The response received from an authentiator 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 {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An abstract class that represents responses recieved from the webauthn authenticator.
|
* An abstract class that represents responses received from the webauthn authenticator.
|
||||||
* It contains data that is commonly returned during different types of authenticator interactions.
|
* It contains data that is commonly returned during different types of authenticator interactions.
|
||||||
*/
|
*/
|
||||||
export abstract class WebauthnLoginAuthenticatorResponseRequest {
|
export abstract class WebauthnLoginAuthenticatorResponseRequest {
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||||
|
|
||||||
|
import { WebauthnLoginCredentialPrfStatus } from "../../../enums/webauthn-login-credential-prf-status.enum";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A webauthn login credential recieved from the server.
|
* A webauthn login credential received from the server.
|
||||||
*/
|
*/
|
||||||
export class WebauthnLoginCredentialResponse extends BaseResponse {
|
export class WebauthnLoginCredentialResponse extends BaseResponse {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
prfSupport: boolean;
|
prfStatus: WebauthnLoginCredentialPrfStatus;
|
||||||
|
|
||||||
constructor(response: unknown) {
|
constructor(response: unknown) {
|
||||||
super(response);
|
super(response);
|
||||||
this.id = this.getResponseProperty("id");
|
this.id = this.getResponseProperty("Id");
|
||||||
this.name = this.getResponseProperty("name");
|
this.name = this.getResponseProperty("Name");
|
||||||
this.prfSupport = this.getResponseProperty("prfSupport");
|
this.prfStatus = this.getResponseProperty("PrfStatus");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
15
apps/web/src/app/auth/core/services/webauthn-login/utils.ts
Normal file
15
apps/web/src/app/auth/core/services/webauthn-login/utils.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import {
|
||||||
|
PrfKey,
|
||||||
|
SymmetricCryptoKey,
|
||||||
|
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
|
||||||
|
const LoginWithPrfSalt = "passwordless-login";
|
||||||
|
|
||||||
|
export async function getLoginWithPrfSalt(): Promise<ArrayBuffer> {
|
||||||
|
return await crypto.subtle.digest("sha-256", Utils.fromUtf8ToArray(LoginWithPrfSalt));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSymmetricKeyFromPrf(prf: ArrayBuffer) {
|
||||||
|
return new SymmetricCryptoKey(new Uint8Array(prf)) as PrfKey;
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
|
|
||||||
|
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 { WebauthnLoginAdminService } from "./webauthn-login-admin.service";
|
||||||
|
import { WebauthnLoginApiService } from "./webauthn-login-api.service";
|
||||||
|
|
||||||
|
describe("WebauthnAdminService", () => {
|
||||||
|
let apiService!: MockProxy<WebauthnLoginApiService>;
|
||||||
|
let userVerificationService!: MockProxy<UserVerificationService>;
|
||||||
|
let rotateableKeySetService!: MockProxy<RotateableKeySetService>;
|
||||||
|
let credentials: MockProxy<CredentialsContainer>;
|
||||||
|
let service!: WebauthnLoginAdminService;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
// Polyfill missing class
|
||||||
|
window.PublicKeyCredential = class {} as any;
|
||||||
|
window.AuthenticatorAttestationResponse = class {} as any;
|
||||||
|
apiService = mock<WebauthnLoginApiService>();
|
||||||
|
userVerificationService = mock<UserVerificationService>();
|
||||||
|
rotateableKeySetService = mock<RotateableKeySetService>();
|
||||||
|
credentials = mock<CredentialsContainer>();
|
||||||
|
service = new WebauthnLoginAdminService(
|
||||||
|
apiService,
|
||||||
|
userVerificationService,
|
||||||
|
rotateableKeySetService,
|
||||||
|
credentials
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createCredential", () => {
|
||||||
|
it("should return undefined when navigator.credentials throws", async () => {
|
||||||
|
credentials.create.mockRejectedValue(new Error("Mocked error"));
|
||||||
|
const options = createCredentialCreateOptions();
|
||||||
|
|
||||||
|
const result = await service.createCredential(options);
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return credential when navigator.credentials does not throw", async () => {
|
||||||
|
const deviceResponse = createDeviceResponse({ prf: false });
|
||||||
|
credentials.create.mockResolvedValue(deviceResponse as PublicKeyCredential);
|
||||||
|
const createOptions = createCredentialCreateOptions();
|
||||||
|
|
||||||
|
const result = await service.createCredential(createOptions);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
deviceResponse,
|
||||||
|
createOptions,
|
||||||
|
supportsPrf: false,
|
||||||
|
} as PendingWebauthnLoginCredentialView);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return supportsPrf=true when extensions contain prf", async () => {
|
||||||
|
const deviceResponse = createDeviceResponse({ prf: true });
|
||||||
|
credentials.create.mockResolvedValue(deviceResponse as PublicKeyCredential);
|
||||||
|
const createOptions = createCredentialCreateOptions();
|
||||||
|
|
||||||
|
const result = await service.createCredential(createOptions);
|
||||||
|
|
||||||
|
expect(result.supportsPrf).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function createCredentialCreateOptions(): CredentialCreateOptionsView {
|
||||||
|
const challenge = {
|
||||||
|
publicKey: {
|
||||||
|
extensions: {},
|
||||||
|
},
|
||||||
|
rp: {
|
||||||
|
id: "bitwarden.com",
|
||||||
|
},
|
||||||
|
authenticatorSelection: {
|
||||||
|
userVerification: "required",
|
||||||
|
residentKey: "required",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return new CredentialCreateOptionsView(challenge as any, Symbol() as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDeviceResponse({ prf = false }: { prf?: boolean } = {}): PublicKeyCredential {
|
||||||
|
const credential = {
|
||||||
|
id: "Y29yb2l1IHdhcyBoZXJl",
|
||||||
|
rawId: new Uint8Array([0x74, 0x65, 0x73, 0x74]),
|
||||||
|
type: "public-key",
|
||||||
|
response: {
|
||||||
|
attestationObject: new Uint8Array([0, 0, 0]),
|
||||||
|
clientDataJSON: "eyJ0ZXN0IjoidGVzdCJ9",
|
||||||
|
},
|
||||||
|
getClientExtensionResults: () => {
|
||||||
|
if (!prf) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
prf: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
Object.setPrototypeOf(credential, PublicKeyCredential.prototype);
|
||||||
|
Object.setPrototypeOf(credential.response, AuthenticatorAttestationResponse.prototype);
|
||||||
|
|
||||||
|
return credential;
|
||||||
|
}
|
@ -1,19 +1,23 @@
|
|||||||
import { Injectable, Optional } from "@angular/core";
|
import { Injectable, Optional } from "@angular/core";
|
||||||
import { BehaviorSubject, filter, from, map, Observable, shareReplay, switchMap, tap } from "rxjs";
|
import { BehaviorSubject, filter, from, map, Observable, shareReplay, switchMap, tap } from "rxjs";
|
||||||
|
|
||||||
|
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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { Verification } from "@bitwarden/common/types/verification";
|
import { Verification } from "@bitwarden/common/types/verification";
|
||||||
|
|
||||||
import { CredentialCreateOptionsView } from "../../views/credential-create-options.view";
|
import { CredentialCreateOptionsView } from "../../views/credential-create-options.view";
|
||||||
import { WebauthnCredentialView } from "../../views/webauth-credential.view";
|
import { PendingWebauthnLoginCredentialView } from "../../views/pending-webauthn-login-credential.view";
|
||||||
|
import { WebauthnLoginCredentialView } from "../../views/webauthn-login-credential.view";
|
||||||
|
import { RotateableKeySetService } from "../rotateable-key-set.service";
|
||||||
|
|
||||||
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 { createSymmetricKeyFromPrf, getLoginWithPrfSalt } from "./utils";
|
||||||
import { WebauthnLoginApiService } from "./webauthn-login-api.service";
|
import { WebauthnLoginApiService } from "./webauthn-login-api.service";
|
||||||
|
|
||||||
@Injectable({ providedIn: "root" })
|
@Injectable({ providedIn: "root" })
|
||||||
export class WebauthnLoginService {
|
export class WebauthnLoginAdminService {
|
||||||
static readonly MaxCredentialCount = 5;
|
static readonly MaxCredentialCount = 5;
|
||||||
|
|
||||||
private navigatorCredentials: CredentialsContainer;
|
private navigatorCredentials: CredentialsContainer;
|
||||||
@ -31,6 +35,7 @@ export class WebauthnLoginService {
|
|||||||
constructor(
|
constructor(
|
||||||
private apiService: WebauthnLoginApiService,
|
private apiService: WebauthnLoginApiService,
|
||||||
private userVerificationService: UserVerificationService,
|
private userVerificationService: UserVerificationService,
|
||||||
|
private rotateableKeySetService: RotateableKeySetService,
|
||||||
@Optional() navigatorCredentials?: CredentialsContainer,
|
@Optional() navigatorCredentials?: CredentialsContainer,
|
||||||
@Optional() private logService?: LogService
|
@Optional() private logService?: LogService
|
||||||
) {
|
) {
|
||||||
@ -48,17 +53,60 @@ export class WebauthnLoginService {
|
|||||||
|
|
||||||
async createCredential(
|
async createCredential(
|
||||||
credentialOptions: CredentialCreateOptionsView
|
credentialOptions: CredentialCreateOptionsView
|
||||||
): Promise<PublicKeyCredential | undefined> {
|
): Promise<PendingWebauthnLoginCredentialView | undefined> {
|
||||||
const nativeOptions: CredentialCreationOptions = {
|
const nativeOptions: CredentialCreationOptions = {
|
||||||
publicKey: credentialOptions.options,
|
publicKey: credentialOptions.options,
|
||||||
};
|
};
|
||||||
|
// TODO: Remove `any` when typescript typings add support for PRF
|
||||||
|
nativeOptions.publicKey.extensions = {
|
||||||
|
prf: {},
|
||||||
|
} as any;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.navigatorCredentials.create(nativeOptions);
|
const response = await this.navigatorCredentials.create(nativeOptions);
|
||||||
if (!(response instanceof PublicKeyCredential)) {
|
if (!(response instanceof PublicKeyCredential)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return response;
|
// TODO: Remove `any` when typescript typings add support for PRF
|
||||||
|
const supportsPrf = Boolean((response.getClientExtensionResults() as any).prf?.enabled);
|
||||||
|
return new PendingWebauthnLoginCredentialView(credentialOptions, response, supportsPrf);
|
||||||
|
} catch (error) {
|
||||||
|
this.logService?.error(error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createKeySet(
|
||||||
|
pendingCredential: PendingWebauthnLoginCredentialView
|
||||||
|
): Promise<PrfKeySet | undefined> {
|
||||||
|
const nativeOptions: CredentialRequestOptions = {
|
||||||
|
publicKey: {
|
||||||
|
challenge: pendingCredential.createOptions.options.challenge,
|
||||||
|
allowCredentials: [{ id: pendingCredential.deviceResponse.rawId, type: "public-key" }],
|
||||||
|
rpId: pendingCredential.createOptions.options.rp.id,
|
||||||
|
timeout: pendingCredential.createOptions.options.timeout,
|
||||||
|
userVerification:
|
||||||
|
pendingCredential.createOptions.options.authenticatorSelection.userVerification,
|
||||||
|
// TODO: Remove `any` when typescript typings add support for PRF
|
||||||
|
extensions: { prf: { eval: { first: await getLoginWithPrfSalt() } } } as any,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.navigatorCredentials.get(nativeOptions);
|
||||||
|
if (!(response instanceof PublicKeyCredential)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove `any` when typescript typings add support for PRF
|
||||||
|
const prfResult = (response.getClientExtensionResults() as any).prf?.results?.first;
|
||||||
|
|
||||||
|
if (prfResult === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const symmetricPrfKey = createSymmetricKeyFromPrf(prfResult);
|
||||||
|
return await this.rotateableKeySetService.createKeySet(symmetricPrfKey);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logService?.error(error);
|
this.logService?.error(error);
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -66,14 +114,18 @@ export class WebauthnLoginService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveCredential(
|
async saveCredential(
|
||||||
credentialOptions: CredentialCreateOptionsView,
|
name: string,
|
||||||
deviceResponse: PublicKeyCredential,
|
credential: PendingWebauthnLoginCredentialView,
|
||||||
name: string
|
prfKeySet?: PrfKeySet
|
||||||
) {
|
) {
|
||||||
const request = new SaveCredentialRequest();
|
const request = new SaveCredentialRequest();
|
||||||
request.deviceResponse = new WebauthnLoginAttestationResponseRequest(deviceResponse);
|
request.deviceResponse = new WebauthnLoginAttestationResponseRequest(credential.deviceResponse);
|
||||||
request.token = credentialOptions.token;
|
request.token = credential.createOptions.token;
|
||||||
request.name = name;
|
request.name = name;
|
||||||
|
request.supportsPrf = credential.supportsPrf;
|
||||||
|
request.encryptedUserKey = prfKeySet?.encryptedUserKey.encryptedString;
|
||||||
|
request.encryptedPublicKey = prfKeySet?.encryptedPublicKey.encryptedString;
|
||||||
|
request.encryptedPrivateKey = prfKeySet?.encryptedPrivateKey.encryptedString;
|
||||||
await this.apiService.saveCredential(request);
|
await this.apiService.saveCredential(request);
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
@ -88,11 +140,11 @@ export class WebauthnLoginService {
|
|||||||
* - The observable is lazy and will only fetch credentials when subscribed to.
|
* - The observable is lazy and will only fetch credentials when subscribed to.
|
||||||
* - Don't subscribe to this in the constructor of a long-running service, as it will keep the observable alive.
|
* - Don't subscribe to this in the constructor of a long-running service, as it will keep the observable alive.
|
||||||
*/
|
*/
|
||||||
getCredentials$(): Observable<WebauthnCredentialView[]> {
|
getCredentials$(): Observable<WebauthnLoginCredentialView[]> {
|
||||||
return this.credentials$;
|
return this.credentials$;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCredential$(credentialId: string): Observable<WebauthnCredentialView> {
|
getCredential$(credentialId: string): Observable<WebauthnLoginCredentialView> {
|
||||||
return this.credentials$.pipe(
|
return this.credentials$.pipe(
|
||||||
map((credentials) => credentials.find((c) => c.id === credentialId)),
|
map((credentials) => credentials.find((c) => c.id === credentialId)),
|
||||||
filter((c) => c !== undefined)
|
filter((c) => c !== undefined)
|
||||||
@ -105,8 +157,15 @@ export class WebauthnLoginService {
|
|||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchCredentials$(): Observable<WebauthnCredentialView[]> {
|
private fetchCredentials$(): Observable<WebauthnLoginCredentialView[]> {
|
||||||
return from(this.apiService.getCredentials()).pipe(map((response) => response.data));
|
return from(this.apiService.getCredentials()).pipe(
|
||||||
|
map((response) =>
|
||||||
|
response.data.map(
|
||||||
|
(credential) =>
|
||||||
|
new WebauthnLoginCredentialView(credential.id, credential.name, credential.prfStatus)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private refresh() {
|
private refresh() {
|
@ -1,67 +0,0 @@
|
|||||||
import { mock, MockProxy } from "jest-mock-extended";
|
|
||||||
|
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
|
||||||
|
|
||||||
import { CredentialCreateOptionsView } from "../../views/credential-create-options.view";
|
|
||||||
|
|
||||||
import { WebauthnLoginApiService } from "./webauthn-login-api.service";
|
|
||||||
import { WebauthnLoginService } from "./webauthn-login.service";
|
|
||||||
|
|
||||||
describe("WebauthnService", () => {
|
|
||||||
let apiService!: MockProxy<WebauthnLoginApiService>;
|
|
||||||
let userVerificationService!: MockProxy<UserVerificationService>;
|
|
||||||
let credentials: MockProxy<CredentialsContainer>;
|
|
||||||
let webauthnService!: WebauthnLoginService;
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
// Polyfill missing class
|
|
||||||
window.PublicKeyCredential = class {} as any;
|
|
||||||
window.AuthenticatorAttestationResponse = class {} as any;
|
|
||||||
apiService = mock<WebauthnLoginApiService>();
|
|
||||||
userVerificationService = mock<UserVerificationService>();
|
|
||||||
credentials = mock<CredentialsContainer>();
|
|
||||||
webauthnService = new WebauthnLoginService(apiService, userVerificationService, credentials);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("createCredential", () => {
|
|
||||||
it("should return undefined when navigator.credentials throws", async () => {
|
|
||||||
credentials.create.mockRejectedValue(new Error("Mocked error"));
|
|
||||||
const options = createCredentialCreateOptions();
|
|
||||||
|
|
||||||
const result = await webauthnService.createCredential(options);
|
|
||||||
|
|
||||||
expect(result).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return credential when navigator.credentials does not throw", async () => {
|
|
||||||
const credential = createDeviceResponse();
|
|
||||||
credentials.create.mockResolvedValue(credential as PublicKeyCredential);
|
|
||||||
const options = createCredentialCreateOptions();
|
|
||||||
|
|
||||||
const result = await webauthnService.createCredential(options);
|
|
||||||
|
|
||||||
expect(result).toBe(credential);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function createCredentialCreateOptions(): CredentialCreateOptionsView {
|
|
||||||
return new CredentialCreateOptionsView(Symbol() as any, Symbol() as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDeviceResponse(): PublicKeyCredential {
|
|
||||||
const credential = {
|
|
||||||
id: "dGVzdA==",
|
|
||||||
rawId: new Uint8Array([0x74, 0x65, 0x73, 0x74]),
|
|
||||||
type: "public-key",
|
|
||||||
response: {
|
|
||||||
attestationObject: new Uint8Array([0, 0, 0]),
|
|
||||||
clientDataJSON: "eyJ0ZXN0IjoidGVzdCJ9",
|
|
||||||
},
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
Object.setPrototypeOf(credential, PublicKeyCredential.prototype);
|
|
||||||
Object.setPrototypeOf(credential.response, AuthenticatorAttestationResponse.prototype);
|
|
||||||
|
|
||||||
return credential;
|
|
||||||
}
|
|
@ -0,0 +1,12 @@
|
|||||||
|
import { CredentialCreateOptionsView } from "./credential-create-options.view";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a WebAuthn credential that has been created by an authenticator but not yet saved to the server.
|
||||||
|
*/
|
||||||
|
export class PendingWebauthnLoginCredentialView {
|
||||||
|
constructor(
|
||||||
|
readonly createOptions: CredentialCreateOptionsView,
|
||||||
|
readonly deviceResponse: PublicKeyCredential,
|
||||||
|
readonly supportsPrf: boolean
|
||||||
|
) {}
|
||||||
|
}
|
@ -1,5 +0,0 @@
|
|||||||
export class WebauthnCredentialView {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
prfSupport: boolean;
|
|
||||||
}
|
|
@ -0,0 +1,9 @@
|
|||||||
|
import { WebauthnLoginCredentialPrfStatus } from "../enums/webauthn-login-credential-prf-status.enum";
|
||||||
|
|
||||||
|
export class WebauthnLoginCredentialView {
|
||||||
|
constructor(
|
||||||
|
readonly id: string,
|
||||||
|
readonly name: string,
|
||||||
|
readonly prfStatus: WebauthnLoginCredentialPrfStatus
|
||||||
|
) {}
|
||||||
|
}
|
@ -32,12 +32,12 @@
|
|||||||
<p bitTypography="body1">{{ "errorCreatingPasskeyInfo" | i18n }}</p>
|
<p bitTypography="body1">{{ "errorCreatingPasskeyInfo" | i18n }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="currentStep === 'credentialNaming'">
|
<div *ngIf="currentStep === 'credentialNaming'" formGroupName="credentialNaming">
|
||||||
<h3 bitTypography="h3">{{ "passkeySuccessfullyCreated" | i18n }}</h3>
|
<h3 bitTypography="h3">{{ "passkeySuccessfullyCreated" | i18n }}</h3>
|
||||||
<p bitTypography="body1">
|
<p bitTypography="body1">
|
||||||
{{ "customPasskeyNameInfo" | i18n }}
|
{{ "customPasskeyNameInfo" | i18n }}
|
||||||
</p>
|
</p>
|
||||||
<bit-form-field disableMargin formGroupName="credentialNaming">
|
<bit-form-field class="!tw-mb-0">
|
||||||
<bit-label>{{ "customName" | i18n }}</bit-label>
|
<bit-label>{{ "customName" | i18n }}</bit-label>
|
||||||
<input type="text" bitInput formControlName="name" appAutofocus />
|
<input type="text" bitInput formControlName="name" appAutofocus />
|
||||||
<bit-hint>{{
|
<bit-hint>{{
|
||||||
@ -45,6 +45,11 @@
|
|||||||
| i18n : formGroup.value.credentialNaming.name.length : NameMaxCharacters
|
| i18n : formGroup.value.credentialNaming.name.length : NameMaxCharacters
|
||||||
}}</bit-hint>
|
}}</bit-hint>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
|
<bit-form-control *ngIf="pendingCredential?.supportsPrf" class="!tw-mb-0 tw-mt-6">
|
||||||
|
<input type="checkbox" bitCheckbox formControlName="useForEncryption" />
|
||||||
|
<bit-label>{{ "useForVaultEncryption" | i18n }}</bit-label>
|
||||||
|
<bit-hint>{{ "useForVaultEncryptionInfo" | i18n }}</bit-hint>
|
||||||
|
</bit-form-control>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container bitDialogFooter>
|
<ng-container bitDialogFooter>
|
||||||
|
@ -3,6 +3,7 @@ import { Component, OnInit } from "@angular/core";
|
|||||||
import { FormBuilder, Validators } from "@angular/forms";
|
import { FormBuilder, Validators } from "@angular/forms";
|
||||||
import { firstValueFrom, map, Observable } from "rxjs";
|
import { firstValueFrom, map, Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { PrfKeySet } from "@bitwarden/auth";
|
||||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
@ -10,8 +11,9 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
|||||||
import { Verification } from "@bitwarden/common/types/verification";
|
import { Verification } from "@bitwarden/common/types/verification";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { WebauthnLoginService } from "../../../core";
|
import { WebauthnLoginAdminService } from "../../../core";
|
||||||
import { CredentialCreateOptionsView } from "../../../core/views/credential-create-options.view";
|
import { CredentialCreateOptionsView } from "../../../core/views/credential-create-options.view";
|
||||||
|
import { PendingWebauthnLoginCredentialView } from "../../../core/views/pending-webauthn-login-credential.view";
|
||||||
|
|
||||||
import { CreatePasskeyFailedIcon } from "./create-passkey-failed.icon";
|
import { CreatePasskeyFailedIcon } from "./create-passkey-failed.icon";
|
||||||
import { CreatePasskeyIcon } from "./create-passkey.icon";
|
import { CreatePasskeyIcon } from "./create-passkey.icon";
|
||||||
@ -42,17 +44,19 @@ export class CreateCredentialDialogComponent implements OnInit {
|
|||||||
}),
|
}),
|
||||||
credentialNaming: this.formBuilder.group({
|
credentialNaming: this.formBuilder.group({
|
||||||
name: ["", Validators.maxLength(50)],
|
name: ["", Validators.maxLength(50)],
|
||||||
|
useForEncryption: [false],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
protected credentialOptions?: CredentialCreateOptionsView;
|
protected credentialOptions?: CredentialCreateOptionsView;
|
||||||
protected deviceResponse?: PublicKeyCredential;
|
protected pendingCredential?: PendingWebauthnLoginCredentialView;
|
||||||
protected hasPasskeys$?: Observable<boolean>;
|
protected hasPasskeys$?: Observable<boolean>;
|
||||||
protected loading$ = this.webauthnService.loading$;
|
protected loading$ = this.webauthnService.loading$;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private dialogRef: DialogRef,
|
private dialogRef: DialogRef,
|
||||||
private webauthnService: WebauthnLoginService,
|
private webauthnService: WebauthnLoginAdminService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private logService: LogService
|
private logService: LogService
|
||||||
@ -112,8 +116,8 @@ export class CreateCredentialDialogComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async submitCredentialCreation() {
|
protected async submitCredentialCreation() {
|
||||||
this.deviceResponse = await this.webauthnService.createCredential(this.credentialOptions);
|
this.pendingCredential = await this.webauthnService.createCredential(this.credentialOptions);
|
||||||
if (this.deviceResponse === undefined) {
|
if (this.pendingCredential === undefined) {
|
||||||
this.currentStep = "credentialCreationFailed";
|
this.currentStep = "credentialCreationFailed";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -128,16 +132,30 @@ export class CreateCredentialDialogComponent implements OnInit {
|
|||||||
|
|
||||||
protected async submitCredentialNaming() {
|
protected async submitCredentialNaming() {
|
||||||
this.formGroup.controls.credentialNaming.markAllAsTouched();
|
this.formGroup.controls.credentialNaming.markAllAsTouched();
|
||||||
if (this.formGroup.controls.credentialNaming.invalid) {
|
if (this.formGroup.controls.credentialNaming.controls.name.invalid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let keySet: PrfKeySet | undefined;
|
||||||
|
if (this.formGroup.value.credentialNaming.useForEncryption) {
|
||||||
|
keySet = await this.webauthnService.createKeySet(this.pendingCredential);
|
||||||
|
|
||||||
|
if (keySet === undefined) {
|
||||||
|
this.formGroup.controls.credentialNaming.controls.useForEncryption?.setErrors({
|
||||||
|
error: {
|
||||||
|
message: this.i18nService.t("useForVaultEncryptionErrorReadingPasskey"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const name = this.formGroup.value.credentialNaming.name;
|
const name = this.formGroup.value.credentialNaming.name;
|
||||||
try {
|
try {
|
||||||
await this.webauthnService.saveCredential(
|
await this.webauthnService.saveCredential(
|
||||||
this.credentialOptions,
|
this.formGroup.value.credentialNaming.name,
|
||||||
this.deviceResponse,
|
this.pendingCredential,
|
||||||
this.formGroup.value.credentialNaming.name
|
keySet
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logService?.error(error);
|
this.logService?.error(error);
|
||||||
|
@ -10,8 +10,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
|||||||
import { Verification } from "@bitwarden/common/types/verification";
|
import { Verification } from "@bitwarden/common/types/verification";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { WebauthnLoginService } from "../../../core";
|
import { WebauthnLoginAdminService } from "../../../core";
|
||||||
import { WebauthnCredentialView } from "../../../core/views/webauth-credential.view";
|
import { WebauthnLoginCredentialView } from "../../../core/views/webauthn-login-credential.view";
|
||||||
|
|
||||||
export interface DeleteCredentialDialogParams {
|
export interface DeleteCredentialDialogParams {
|
||||||
credentialId: string;
|
credentialId: string;
|
||||||
@ -27,14 +27,14 @@ export class DeleteCredentialDialogComponent implements OnInit, OnDestroy {
|
|||||||
protected formGroup = this.formBuilder.group({
|
protected formGroup = this.formBuilder.group({
|
||||||
secret: null as Verification | null,
|
secret: null as Verification | null,
|
||||||
});
|
});
|
||||||
protected credential?: WebauthnCredentialView;
|
protected credential?: WebauthnLoginCredentialView;
|
||||||
protected loading$ = this.webauthnService.loading$;
|
protected loading$ = this.webauthnService.loading$;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DIALOG_DATA) private params: DeleteCredentialDialogParams,
|
@Inject(DIALOG_DATA) private params: DeleteCredentialDialogParams,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private dialogRef: DialogRef,
|
private dialogRef: DialogRef,
|
||||||
private webauthnService: WebauthnLoginService,
|
private webauthnService: WebauthnLoginAdminService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private logService: LogService
|
private logService: LogService
|
||||||
|
@ -22,12 +22,20 @@
|
|||||||
<table *ngIf="hasCredentials" class="tw-mb-5">
|
<table *ngIf="hasCredentials" class="tw-mb-5">
|
||||||
<tr *ngFor="let credential of credentials">
|
<tr *ngFor="let credential of credentials">
|
||||||
<td class="tw-p-2 tw-pl-0 tw-font-semibold">{{ credential.name }}</td>
|
<td class="tw-p-2 tw-pl-0 tw-font-semibold">{{ credential.name }}</td>
|
||||||
<td class="tw-p-2 tw-pr-10">
|
<td class="tw-p-2 tw-pr-10 tw-text-left">
|
||||||
<ng-container *ngIf="credential.prfSupport">
|
<ng-container *ngIf="credential.prfStatus === WebauthnLoginCredentialPrfStatus.Enabled">
|
||||||
<i class="bwi bwi-lock-encrypted"></i>
|
<i class="bwi bwi-lock-encrypted"></i>
|
||||||
{{ "supportsEncryption" | i18n }}
|
<span bitTypography="body1" class="tw-text-muted">{{ "usedForEncryption" | i18n }}</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<span bitTypography="body1" class="tw-text-muted">
|
<ng-container *ngIf="credential.prfStatus === WebauthnLoginCredentialPrfStatus.Supported">
|
||||||
|
<i class="bwi bwi-lock-encrypted"></i>
|
||||||
|
<span bitTypography="body1" class="tw-text-muted">{{ "encryptionNotEnabled" | i18n }}</span>
|
||||||
|
</ng-container>
|
||||||
|
<span
|
||||||
|
*ngIf="credential.prfStatus === WebauthnLoginCredentialPrfStatus.Unsupported"
|
||||||
|
bitTypography="body1"
|
||||||
|
class="tw-text-muted"
|
||||||
|
>
|
||||||
{{ "encryptionNotSupported" | i18n }}
|
{{ "encryptionNotSupported" | i18n }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
@ -3,8 +3,9 @@ import { Subject, takeUntil } from "rxjs";
|
|||||||
|
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { WebauthnLoginService } from "../../core";
|
import { WebauthnLoginAdminService } from "../../core";
|
||||||
import { WebauthnCredentialView } from "../../core/views/webauth-credential.view";
|
import { WebauthnLoginCredentialPrfStatus } from "../../core/enums/webauthn-login-credential-prf-status.enum";
|
||||||
|
import { WebauthnLoginCredentialView } from "../../core/views/webauthn-login-credential.view";
|
||||||
|
|
||||||
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";
|
||||||
@ -19,13 +20,14 @@ import { openDeleteCredentialDialogComponent } from "./delete-credential-dialog/
|
|||||||
export class WebauthnLoginSettingsComponent implements OnInit, OnDestroy {
|
export class WebauthnLoginSettingsComponent implements OnInit, OnDestroy {
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
protected readonly MaxCredentialCount = WebauthnLoginService.MaxCredentialCount;
|
protected readonly MaxCredentialCount = WebauthnLoginAdminService.MaxCredentialCount;
|
||||||
|
protected readonly WebauthnLoginCredentialPrfStatus = WebauthnLoginCredentialPrfStatus;
|
||||||
|
|
||||||
protected credentials?: WebauthnCredentialView[];
|
protected credentials?: WebauthnLoginCredentialView[];
|
||||||
protected loading = true;
|
protected loading = true;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private webauthnService: WebauthnLoginService,
|
private webauthnService: WebauthnLoginAdminService,
|
||||||
private dialogService: DialogService
|
private dialogService: DialogService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||||
|
|
||||||
|
import { CheckboxModule } from "@bitwarden/components";
|
||||||
|
|
||||||
import { SharedModule } from "../../../shared/shared.module";
|
import { SharedModule } from "../../../shared/shared.module";
|
||||||
import { UserVerificationModule } from "../../shared/components/user-verification";
|
import { UserVerificationModule } from "../../shared/components/user-verification";
|
||||||
|
|
||||||
@ -9,7 +11,7 @@ import { DeleteCredentialDialogComponent } from "./delete-credential-dialog/dele
|
|||||||
import { WebauthnLoginSettingsComponent } from "./webauthn-login-settings.component";
|
import { WebauthnLoginSettingsComponent } from "./webauthn-login-settings.component";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [SharedModule, FormsModule, ReactiveFormsModule, UserVerificationModule],
|
imports: [SharedModule, FormsModule, ReactiveFormsModule, UserVerificationModule, CheckboxModule],
|
||||||
declarations: [
|
declarations: [
|
||||||
WebauthnLoginSettingsComponent,
|
WebauthnLoginSettingsComponent,
|
||||||
CreateCredentialDialogComponent,
|
CreateCredentialDialogComponent,
|
||||||
|
@ -647,9 +647,24 @@
|
|||||||
"customPasskeyNameInfo": {
|
"customPasskeyNameInfo": {
|
||||||
"message": "Name your passkey to help you identify it."
|
"message": "Name your passkey to help you identify it."
|
||||||
},
|
},
|
||||||
|
"useForVaultEncryption": {
|
||||||
|
"message": "Use for vault encryption"
|
||||||
|
},
|
||||||
|
"useForVaultEncryptionInfo": {
|
||||||
|
"message": "Log in and unlock on supported devices without your master password. Follow the prompts from your browser to finalize setup."
|
||||||
|
},
|
||||||
|
"useForVaultEncryptionErrorReadingPasskey": {
|
||||||
|
"message": "Error reading passkey. Try again or uncheck this option."
|
||||||
|
},
|
||||||
"encryptionNotSupported": {
|
"encryptionNotSupported": {
|
||||||
"message": "Encryption not supported"
|
"message": "Encryption not supported"
|
||||||
},
|
},
|
||||||
|
"encryptionNotEnabled": {
|
||||||
|
"message": "Encryption not enabled"
|
||||||
|
},
|
||||||
|
"usedForEncryption": {
|
||||||
|
"message": "Used for encryption"
|
||||||
|
},
|
||||||
"loginWithPasskeyEnabled": {
|
"loginWithPasskeyEnabled": {
|
||||||
"message": "Log in with passkey turned on"
|
"message": "Log in with passkey turned on"
|
||||||
},
|
},
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export * from "./components/fingerprint-dialog.component";
|
export * from "./components/fingerprint-dialog.component";
|
||||||
export * from "./password-callout/password-callout.component";
|
export * from "./password-callout/password-callout.component";
|
||||||
|
export * from "./models";
|
||||||
|
1
libs/auth/src/models/domain/index.ts
Normal file
1
libs/auth/src/models/domain/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./rotateable-key-set";
|
36
libs/auth/src/models/domain/rotateable-key-set.ts
Normal file
36
libs/auth/src/models/domain/rotateable-key-set.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
|
import {
|
||||||
|
PrfKey,
|
||||||
|
SymmetricCryptoKey,
|
||||||
|
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
|
||||||
|
declare const tag: unique symbol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of keys where a `UserKey` is protected by an encrypted public/private key-pair.
|
||||||
|
* The `UserKey` is used to encrypt/decrypt data, while the public/private key-pair is
|
||||||
|
* used to rotate the `UserKey`.
|
||||||
|
*
|
||||||
|
* The `PrivateKey` is protected by an `ExternalKey`, such as a `DeviceKey`, or `PrfKey`,
|
||||||
|
* and the `PublicKey` is protected by the `UserKey`. This setup allows:
|
||||||
|
*
|
||||||
|
* - Access to `UserKey` by knowing the `ExternalKey`
|
||||||
|
* - Rotation to a `NewUserKey` by knowing the current `UserKey`,
|
||||||
|
* without needing access to the `ExternalKey`
|
||||||
|
*/
|
||||||
|
export class RotateableKeySet<ExternalKey extends SymmetricCryptoKey = SymmetricCryptoKey> {
|
||||||
|
private readonly [tag]: ExternalKey;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
/** PublicKey encrypted UserKey */
|
||||||
|
readonly encryptedUserKey: EncString,
|
||||||
|
|
||||||
|
/** UserKey encrypted PublicKey */
|
||||||
|
readonly encryptedPublicKey: EncString,
|
||||||
|
|
||||||
|
/** ExternalKey encrypted PrivateKey */
|
||||||
|
readonly encryptedPrivateKey: EncString
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PrfKeySet = RotateableKeySet<PrfKey>;
|
1
libs/auth/src/models/index.ts
Normal file
1
libs/auth/src/models/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./domain";
|
@ -78,6 +78,7 @@ export class SymmetricCryptoKey {
|
|||||||
|
|
||||||
// Setup all separate key types as opaque types
|
// Setup all separate key types as opaque types
|
||||||
export type DeviceKey = Opaque<SymmetricCryptoKey, "DeviceKey">;
|
export type DeviceKey = Opaque<SymmetricCryptoKey, "DeviceKey">;
|
||||||
|
export type PrfKey = Opaque<SymmetricCryptoKey, "PrfKey">;
|
||||||
export type UserKey = Opaque<SymmetricCryptoKey, "UserKey">;
|
export type UserKey = Opaque<SymmetricCryptoKey, "UserKey">;
|
||||||
export type MasterKey = Opaque<SymmetricCryptoKey, "MasterKey">;
|
export type MasterKey = Opaque<SymmetricCryptoKey, "MasterKey">;
|
||||||
export type PinKey = Opaque<SymmetricCryptoKey, "PinKey">;
|
export type PinKey = Opaque<SymmetricCryptoKey, "PinKey">;
|
||||||
|
Loading…
Reference in New Issue
Block a user