mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-20 21:01:29 +01:00
[EC-598] feat: fully wokring non-discoverable implementation
This commit is contained in:
parent
9dfd85dcd7
commit
55cd736ec3
@ -47,6 +47,15 @@ export type BrowserFido2Message = { requestId: string } & (
|
||||
| {
|
||||
type: "ConfirmNewCredentialResponse";
|
||||
}
|
||||
| {
|
||||
type: "ConfirmNewNonDiscoverableCredentialRequest";
|
||||
credentialName: string;
|
||||
userName: string;
|
||||
}
|
||||
| {
|
||||
type: "ConfirmNewNonDiscoverableCredentialResponse";
|
||||
cipherId: string;
|
||||
}
|
||||
| {
|
||||
type: "AbortRequest";
|
||||
}
|
||||
@ -201,10 +210,47 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi
|
||||
}
|
||||
|
||||
async confirmNewNonDiscoverableCredential(
|
||||
params: NewCredentialParams,
|
||||
{ credentialName, userName }: NewCredentialParams,
|
||||
abortController?: AbortController
|
||||
): Promise<string> {
|
||||
return null;
|
||||
const requestId = Utils.newGuid();
|
||||
const data: BrowserFido2Message = {
|
||||
type: "ConfirmNewNonDiscoverableCredentialRequest",
|
||||
requestId,
|
||||
credentialName,
|
||||
userName,
|
||||
};
|
||||
const queryParams = new URLSearchParams({ data: JSON.stringify(data) }).toString();
|
||||
|
||||
const abortHandler = () =>
|
||||
BrowserFido2UserInterfaceService.sendMessage({ type: "AbortRequest", requestId });
|
||||
abortController.signal.addEventListener("abort", abortHandler);
|
||||
|
||||
this.popupUtilsService.popOut(
|
||||
null,
|
||||
`popup/index.html?uilocation=popout#/fido2?${queryParams}`,
|
||||
{ center: true }
|
||||
);
|
||||
|
||||
const response = await lastValueFrom(
|
||||
this.messages$.pipe(
|
||||
filter((msg) => msg.requestId === requestId),
|
||||
first(),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
);
|
||||
|
||||
if (response.type === "ConfirmNewNonDiscoverableCredentialResponse") {
|
||||
return response.cipherId;
|
||||
}
|
||||
|
||||
if (response.type === "AbortResponse") {
|
||||
throw new RequestAbortedError(response.fallbackRequested);
|
||||
}
|
||||
|
||||
abortController.signal.removeEventListener("abort", abortHandler);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async informExcludedCredential(
|
||||
|
@ -11,7 +11,12 @@
|
||||
Authenticate
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="data.type == 'PickCredentialRequest'">
|
||||
<ng-container
|
||||
*ngIf="
|
||||
data.type == 'PickCredentialRequest' ||
|
||||
data.type == 'ConfirmNewNonDiscoverableCredentialRequest'
|
||||
"
|
||||
>
|
||||
A site is asking for authentication, please choose one of the following credentials to use:
|
||||
<div class="box list">
|
||||
<div class="box-content">
|
||||
|
@ -48,6 +48,10 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
return cipher.decrypt();
|
||||
})
|
||||
);
|
||||
} else if (this.data?.type === "ConfirmNewNonDiscoverableCredentialRequest") {
|
||||
this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
|
||||
(cipher) => cipher.type === CipherType.Login && !cipher.isDeleted
|
||||
);
|
||||
}
|
||||
}),
|
||||
takeUntil(this.destroy$)
|
||||
@ -66,11 +70,19 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async pick(cipher: CipherView) {
|
||||
BrowserFido2UserInterfaceService.sendMessage({
|
||||
requestId: this.data.requestId,
|
||||
cipherId: cipher.id,
|
||||
type: "PickCredentialResponse",
|
||||
});
|
||||
if (this.data?.type === "PickCredentialRequest") {
|
||||
BrowserFido2UserInterfaceService.sendMessage({
|
||||
requestId: this.data.requestId,
|
||||
cipherId: cipher.id,
|
||||
type: "PickCredentialResponse",
|
||||
});
|
||||
} else if (this.data?.type === "ConfirmNewNonDiscoverableCredentialRequest") {
|
||||
BrowserFido2UserInterfaceService.sendMessage({
|
||||
requestId: this.data.requestId,
|
||||
cipherId: cipher.id,
|
||||
type: "ConfirmNewNonDiscoverableCredentialResponse",
|
||||
});
|
||||
}
|
||||
|
||||
window.close();
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Fido2KeyApi } from "../../webauthn/models/api/fido2-key.api";
|
||||
import { BaseResponse } from "../response/base.response";
|
||||
|
||||
import { LoginUriApi } from "./login-uri.api";
|
||||
@ -9,6 +10,7 @@ export class LoginApi extends BaseResponse {
|
||||
passwordRevisionDate: string;
|
||||
totp: string;
|
||||
autofillOnPageLoad: boolean;
|
||||
fido2Key?: Fido2KeyApi;
|
||||
|
||||
constructor(data: any = null) {
|
||||
super(data);
|
||||
@ -25,5 +27,10 @@ export class LoginApi extends BaseResponse {
|
||||
if (uris != null) {
|
||||
this.uris = uris.map((u: any) => new LoginUriApi(u));
|
||||
}
|
||||
|
||||
const fido2Key = this.getResponseProperty("Fido2Key");
|
||||
if (fido2Key != null) {
|
||||
this.fido2Key = new Fido2KeyApi(fido2Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +60,8 @@ export class CipherData {
|
||||
switch (this.type) {
|
||||
case CipherType.Login:
|
||||
this.login = new LoginData(response.login);
|
||||
this.fido2Key =
|
||||
response.fido2Key != undefined ? new Fido2KeyData(response.fido2Key) : undefined;
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
this.secureNote = new SecureNoteData(response.secureNote);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { LoginApi } from "../../../models/api/login.api";
|
||||
import { Fido2KeyData } from "../../../webauthn/models/data/fido2-key.data";
|
||||
|
||||
import { LoginUriData } from "./login-uri.data";
|
||||
|
||||
@ -9,6 +10,7 @@ export class LoginData {
|
||||
passwordRevisionDate: string;
|
||||
totp: string;
|
||||
autofillOnPageLoad: boolean;
|
||||
fido2Key?: Fido2KeyData;
|
||||
|
||||
constructor(data?: LoginApi) {
|
||||
if (data == null) {
|
||||
@ -24,5 +26,9 @@ export class LoginData {
|
||||
if (data.uris) {
|
||||
this.uris = data.uris.map((u) => new LoginUriData(u));
|
||||
}
|
||||
|
||||
if (data.fido2Key) {
|
||||
this.fido2Key = new Fido2KeyData(data.fido2Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { Jsonify } from "type-fest";
|
||||
import Domain from "../../../models/domain/domain-base";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../../models/domain/symmetric-crypto-key";
|
||||
import { Fido2Key } from "../../../webauthn/models/domain/fido2-key";
|
||||
import { LoginData } from "../data/login.data";
|
||||
import { LoginView } from "../view/login.view";
|
||||
|
||||
@ -15,6 +16,7 @@ export class Login extends Domain {
|
||||
passwordRevisionDate?: Date;
|
||||
totp: EncString;
|
||||
autofillOnPageLoad: boolean;
|
||||
fido2Key: Fido2Key;
|
||||
|
||||
constructor(obj?: LoginData) {
|
||||
super();
|
||||
@ -42,6 +44,10 @@ export class Login extends Domain {
|
||||
this.uris.push(new LoginUri(u));
|
||||
});
|
||||
}
|
||||
|
||||
if (obj.fido2Key) {
|
||||
this.fido2Key = new Fido2Key(obj.fido2Key);
|
||||
}
|
||||
}
|
||||
|
||||
async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<LoginView> {
|
||||
@ -64,6 +70,10 @@ export class Login extends Domain {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.fido2Key != null) {
|
||||
view.fido2Key = await this.fido2Key.decrypt(orgId, encKey);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@ -85,6 +95,10 @@ export class Login extends Domain {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.fido2Key != null) {
|
||||
l.fido2Key = this.fido2Key.toFido2KeyData();
|
||||
}
|
||||
|
||||
return l;
|
||||
}
|
||||
|
||||
@ -99,13 +113,15 @@ export class Login extends Domain {
|
||||
const passwordRevisionDate =
|
||||
obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate);
|
||||
const uris = obj.uris?.map((uri: any) => LoginUri.fromJSON(uri));
|
||||
const fido2Key = obj.fido2Key == null ? null : Fido2Key.fromJSON(obj.fido2Key);
|
||||
|
||||
return Object.assign(new Login(), obj, {
|
||||
username,
|
||||
password,
|
||||
totp,
|
||||
passwordRevisionDate: passwordRevisionDate,
|
||||
uris: uris,
|
||||
passwordRevisionDate,
|
||||
uris,
|
||||
fido2Key,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -63,6 +63,44 @@ export class CipherRequest {
|
||||
return uri;
|
||||
});
|
||||
}
|
||||
|
||||
if (cipher.login.fido2Key != null) {
|
||||
this.login.fido2Key = new Fido2KeyApi();
|
||||
this.login.fido2Key.nonDiscoverableId =
|
||||
cipher.login.fido2Key.nonDiscoverableId != null
|
||||
? cipher.login.fido2Key.nonDiscoverableId.encryptedString
|
||||
: null;
|
||||
this.login.fido2Key.keyType =
|
||||
cipher.login.fido2Key.keyType != null
|
||||
? (cipher.login.fido2Key.keyType.encryptedString as "public-key")
|
||||
: null;
|
||||
this.login.fido2Key.keyAlgorithm =
|
||||
cipher.login.fido2Key.keyAlgorithm != null
|
||||
? (cipher.login.fido2Key.keyAlgorithm.encryptedString as "ECDSA")
|
||||
: null;
|
||||
this.login.fido2Key.keyCurve =
|
||||
cipher.login.fido2Key.keyCurve != null
|
||||
? (cipher.login.fido2Key.keyCurve.encryptedString as "P-256")
|
||||
: null;
|
||||
this.login.fido2Key.keyValue =
|
||||
cipher.login.fido2Key.keyValue != null
|
||||
? cipher.login.fido2Key.keyValue.encryptedString
|
||||
: null;
|
||||
this.login.fido2Key.rpId =
|
||||
cipher.login.fido2Key.rpId != null ? cipher.login.fido2Key.rpId.encryptedString : null;
|
||||
this.login.fido2Key.rpName =
|
||||
cipher.login.fido2Key.rpName != null
|
||||
? cipher.login.fido2Key.rpName.encryptedString
|
||||
: null;
|
||||
this.login.fido2Key.userHandle =
|
||||
cipher.login.fido2Key.userHandle != null
|
||||
? cipher.login.fido2Key.userHandle.encryptedString
|
||||
: null;
|
||||
this.login.fido2Key.userName =
|
||||
cipher.login.fido2Key.userName != null
|
||||
? cipher.login.fido2Key.userName.encryptedString
|
||||
: null;
|
||||
}
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
this.secureNote = new SecureNoteApi();
|
||||
|
@ -3,6 +3,7 @@ import { Jsonify } from "type-fest";
|
||||
import { LoginLinkedId as LinkedId } from "../../../enums/linkedIdType";
|
||||
import { linkedFieldOption } from "../../../misc/linkedFieldOption.decorator";
|
||||
import { Utils } from "../../../misc/utils";
|
||||
import { Fido2KeyView } from "../../../webauthn/models/view/fido2-key.view";
|
||||
import { Login } from "../domain/login";
|
||||
|
||||
import { ItemView } from "./item.view";
|
||||
@ -18,6 +19,7 @@ export class LoginView extends ItemView {
|
||||
totp: string = null;
|
||||
uris: LoginUriView[] = null;
|
||||
autofillOnPageLoad: boolean = null;
|
||||
fido2Key?: Fido2KeyView;
|
||||
|
||||
constructor(l?: Login) {
|
||||
super();
|
||||
@ -67,10 +69,12 @@ export class LoginView extends ItemView {
|
||||
const passwordRevisionDate =
|
||||
obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate);
|
||||
const uris = obj.uris?.map((uri: any) => LoginUriView.fromJSON(uri));
|
||||
const fido2Key = obj.fido2Key == null ? null : Fido2KeyView.fromJSON(obj.fido2Key);
|
||||
|
||||
return Object.assign(new LoginView(), obj, {
|
||||
passwordRevisionDate: passwordRevisionDate,
|
||||
uris: uris,
|
||||
uris,
|
||||
fido2Key,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1117,6 +1117,27 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
cipher.login.uris.push(loginUri);
|
||||
}
|
||||
}
|
||||
|
||||
if (model.login.fido2Key != null) {
|
||||
cipher.login.fido2Key = new Fido2Key();
|
||||
await this.encryptObjProperty(
|
||||
model.login.fido2Key,
|
||||
cipher.login.fido2Key,
|
||||
{
|
||||
nonDiscoverableId: null,
|
||||
keyType: null,
|
||||
keyAlgorithm: null,
|
||||
keyCurve: null,
|
||||
keyValue: null,
|
||||
rpId: null,
|
||||
rpName: null,
|
||||
userHandle: null,
|
||||
userName: null,
|
||||
origin: null,
|
||||
},
|
||||
key
|
||||
);
|
||||
}
|
||||
return;
|
||||
case CipherType.SecureNote:
|
||||
cipher.secureNote = new SecureNote();
|
||||
|
@ -104,7 +104,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
params = await createParams({
|
||||
excludeCredentialDescriptorList: [
|
||||
{
|
||||
id: Utils.guidToRawFormat(excludedCipher.fido2Key.nonDiscoverableId),
|
||||
id: Utils.guidToRawFormat(excludedCipher.login.fido2Key.nonDiscoverableId),
|
||||
type: "public-key",
|
||||
},
|
||||
],
|
||||
@ -162,15 +162,16 @@ describe("FidoAuthenticatorService", () => {
|
||||
let params: Fido2AuthenticatorMakeCredentialsParams;
|
||||
|
||||
beforeEach(async () => {
|
||||
const excludedCipher = createCipherView();
|
||||
excludedCipherView = await excludedCipher;
|
||||
excludedCipherView = createCipherView();
|
||||
params = await createParams({
|
||||
excludeCredentialDescriptorList: [
|
||||
{ id: Utils.guidToRawFormat(excludedCipher.id), type: "public-key" },
|
||||
{ id: Utils.guidToRawFormat(excludedCipherView.id), type: "public-key" },
|
||||
],
|
||||
});
|
||||
cipherService.get.mockImplementation(async (id) =>
|
||||
id === excludedCipher.id ? excludedCipher : undefined
|
||||
id === excludedCipherView.id
|
||||
? ({ decrypt: async () => excludedCipherView } as any)
|
||||
: undefined
|
||||
);
|
||||
cipherService.getAllDecrypted.mockResolvedValue([excludedCipherView]);
|
||||
});
|
||||
@ -237,12 +238,15 @@ describe("FidoAuthenticatorService", () => {
|
||||
return cipher;
|
||||
});
|
||||
|
||||
await authenticator.makeCredential(params);
|
||||
await authenticator.makeCredential(params, new AbortController());
|
||||
|
||||
expect(userInterface.confirmNewCredential).toHaveBeenCalledWith({
|
||||
credentialName: params.rpEntity.name,
|
||||
userName: params.userEntity.displayName,
|
||||
} as NewCredentialParams);
|
||||
expect(userInterface.confirmNewCredential).toHaveBeenCalledWith(
|
||||
{
|
||||
credentialName: params.rpEntity.name,
|
||||
userName: params.userEntity.displayName,
|
||||
} as NewCredentialParams,
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it("should save credential to vault if request confirmed by user", async () => {
|
||||
@ -320,12 +324,15 @@ describe("FidoAuthenticatorService", () => {
|
||||
it("should request confirmation from user", async () => {
|
||||
userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(existingCipher.id);
|
||||
|
||||
await authenticator.makeCredential(params);
|
||||
await authenticator.makeCredential(params, new AbortController());
|
||||
|
||||
expect(userInterface.confirmNewNonDiscoverableCredential).toHaveBeenCalledWith({
|
||||
credentialName: params.rpEntity.name,
|
||||
userName: params.userEntity.displayName,
|
||||
} as NewCredentialParams);
|
||||
expect(userInterface.confirmNewNonDiscoverableCredential).toHaveBeenCalledWith(
|
||||
{
|
||||
credentialName: params.rpEntity.name,
|
||||
userName: params.userEntity.displayName,
|
||||
} as NewCredentialParams,
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it("should save credential to vault if request confirmed by user", async () => {
|
||||
@ -341,16 +348,18 @@ describe("FidoAuthenticatorService", () => {
|
||||
type: CipherType.Login,
|
||||
name: existingCipher.name,
|
||||
|
||||
fido2Key: expect.objectContaining({
|
||||
nonDiscoverableId: expect.anything(),
|
||||
keyType: "public-key",
|
||||
keyAlgorithm: "ECDSA",
|
||||
keyCurve: "P-256",
|
||||
rpId: params.rpEntity.id,
|
||||
rpName: params.rpEntity.name,
|
||||
userHandle: Fido2Utils.bufferToString(params.userEntity.id),
|
||||
counter: 0,
|
||||
userName: params.userEntity.displayName,
|
||||
login: expect.objectContaining({
|
||||
fido2Key: expect.objectContaining({
|
||||
nonDiscoverableId: expect.anything(),
|
||||
keyType: "public-key",
|
||||
keyAlgorithm: "ECDSA",
|
||||
keyCurve: "P-256",
|
||||
rpId: params.rpEntity.id,
|
||||
rpName: params.rpEntity.name,
|
||||
userHandle: Fido2Utils.bufferToString(params.userEntity.id),
|
||||
counter: 0,
|
||||
userName: params.userEntity.displayName,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
);
|
||||
@ -406,7 +415,9 @@ describe("FidoAuthenticatorService", () => {
|
||||
);
|
||||
cipherService.getAllDecrypted.mockResolvedValue([await cipher]);
|
||||
cipherService.encrypt.mockImplementation(async (cipher) => {
|
||||
cipher.fido2Key.nonDiscoverableId = nonDiscoverableId; // Replace id for testability
|
||||
if (!requireResidentKey) {
|
||||
cipher.login.fido2Key.nonDiscoverableId = nonDiscoverableId; // Replace id for testability
|
||||
}
|
||||
return {} as any;
|
||||
});
|
||||
cipherService.createWithServer.mockImplementation(async (cipher) => {
|
||||
@ -561,8 +572,8 @@ describe("FidoAuthenticatorService", () => {
|
||||
|
||||
it("should throw error if credential exists but rpId does not match", async () => {
|
||||
const cipher = await createCipherView({ type: CipherType.Login });
|
||||
cipher.fido2Key.nonDiscoverableId = credentialId;
|
||||
cipher.fido2Key.rpId = "mismatch-rpid";
|
||||
cipher.login.fido2Key.nonDiscoverableId = credentialId;
|
||||
cipher.login.fido2Key.rpId = "mismatch-rpid";
|
||||
cipherService.getAllDecrypted.mockResolvedValue([cipher]);
|
||||
|
||||
const result = async () => await authenticator.getAssertion(params);
|
||||
@ -639,6 +650,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
let credentialIds: string[];
|
||||
let selectedCredentialId: string;
|
||||
let ciphers: CipherView[];
|
||||
let fido2Keys: Fido2KeyView[];
|
||||
let params: Fido2AuthenticatorGetAssertionParams;
|
||||
|
||||
beforeEach(async () => {
|
||||
@ -654,6 +666,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
{ rpId: RpId, counter: 9000, keyValue }
|
||||
)
|
||||
);
|
||||
fido2Keys = ciphers.map((c) => c.fido2Key);
|
||||
selectedCredentialId = ciphers[0].id;
|
||||
params = await createParams({
|
||||
allowCredentialDescriptorList: undefined,
|
||||
@ -666,6 +679,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
{ nonDiscoverableId: id, rpId: RpId, counter: 9000 }
|
||||
)
|
||||
);
|
||||
fido2Keys = ciphers.map((c) => c.login.fido2Key);
|
||||
selectedCredentialId = credentialIds[0];
|
||||
params = await createParams({
|
||||
allowCredentialDescriptorList: credentialIds.map((credentialId) => ({
|
||||
@ -686,15 +700,28 @@ describe("FidoAuthenticatorService", () => {
|
||||
|
||||
await authenticator.getAssertion(params);
|
||||
|
||||
expect(cipherService.encrypt).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: ciphers[0].id,
|
||||
fido2Key: expect.objectContaining({
|
||||
counter: 9001,
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(cipherService.updateWithServer).toHaveBeenCalledWith(encrypted);
|
||||
if (residentKey) {
|
||||
expect(cipherService.encrypt).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: ciphers[0].id,
|
||||
fido2Key: expect.objectContaining({
|
||||
counter: 9001,
|
||||
}),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
expect(cipherService.encrypt).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: ciphers[0].id,
|
||||
login: expect.objectContaining({
|
||||
fido2Key: expect.objectContaining({
|
||||
counter: 9001,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("should return an assertion result", async () => {
|
||||
@ -707,7 +734,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
|
||||
expect(result.selectedCredential.id).toEqual(Utils.guidToRawFormat(selectedCredentialId));
|
||||
expect(result.selectedCredential.userHandle).toEqual(
|
||||
Fido2Utils.stringToBuffer(ciphers[0].fido2Key.userHandle)
|
||||
Fido2Utils.stringToBuffer(fido2Keys[0].userHandle)
|
||||
);
|
||||
expect(rpIdHash).toEqual(
|
||||
new Uint8Array([
|
||||
@ -779,18 +806,25 @@ function createCipherView(
|
||||
cipher.id = data.id ?? Utils.newGuid();
|
||||
cipher.type = data.type ?? CipherType.Fido2Key;
|
||||
cipher.localData = {};
|
||||
cipher.login = data.type ?? data.type === CipherType.Login ? new LoginView() : null;
|
||||
cipher.fido2Key = new Fido2KeyView();
|
||||
cipher.fido2Key.nonDiscoverableId = fido2Key.nonDiscoverableId;
|
||||
cipher.fido2Key.rpId = fido2Key.rpId ?? RpId;
|
||||
cipher.fido2Key.counter = fido2Key.counter ?? 0;
|
||||
cipher.fido2Key.userHandle = fido2Key.userHandle ?? Fido2Utils.bufferToString(randomBytes(16));
|
||||
cipher.fido2Key.keyAlgorithm = fido2Key.keyAlgorithm ?? "ECDSA";
|
||||
cipher.fido2Key.keyCurve = fido2Key.keyCurve ?? "P-256";
|
||||
cipher.fido2Key.keyValue =
|
||||
fido2Key.keyValue ??
|
||||
|
||||
const fido2KeyView = new Fido2KeyView();
|
||||
fido2KeyView.nonDiscoverableId = fido2Key.nonDiscoverableId;
|
||||
fido2KeyView.rpId = fido2Key.rpId ?? RpId;
|
||||
fido2KeyView.counter = fido2Key.counter ?? 0;
|
||||
fido2KeyView.userHandle = fido2Key.userHandle ?? Fido2Utils.bufferToString(randomBytes(16));
|
||||
fido2KeyView.keyAlgorithm = fido2Key.keyAlgorithm ?? "ECDSA";
|
||||
fido2KeyView.keyCurve = fido2Key.keyCurve ?? "P-256";
|
||||
fido2KeyView.keyValue =
|
||||
fido2KeyView.keyValue ??
|
||||
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgTC-7XDZipXbaVBlnkjlBgO16ZmqBZWejK2iYo6lV0dehRANCAASOcM2WduNq1DriRYN7ZekvZz-bRhA-qNT4v0fbp5suUFJyWmgOQ0bybZcLXHaerK5Ep1JiSrQcewtQNgLtry7f";
|
||||
|
||||
if (cipher.type === CipherType.Login) {
|
||||
cipher.login = new LoginView();
|
||||
cipher.login.fido2Key = fido2KeyView;
|
||||
} else {
|
||||
cipher.fido2Key = fido2KeyView;
|
||||
}
|
||||
|
||||
return cipher;
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,8 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
private userInterface: Fido2UserInterfaceService
|
||||
) {}
|
||||
async makeCredential(
|
||||
params: Fido2AuthenticatorMakeCredentialsParams
|
||||
params: Fido2AuthenticatorMakeCredentialsParams,
|
||||
abortController?: AbortController
|
||||
): Promise<Fido2AuthenticatorMakeCredentialResult> {
|
||||
if (params.credTypesAndPubKeyAlgs.every((p) => p.alg !== Fido2AlgorithmIdentifier.ES256)) {
|
||||
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotSupported);
|
||||
@ -66,19 +67,24 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
{
|
||||
credentialName: params.rpEntity.name,
|
||||
userName: params.userEntity.displayName,
|
||||
}
|
||||
},
|
||||
abortController
|
||||
);
|
||||
|
||||
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
|
||||
}
|
||||
|
||||
let cipher: CipherView;
|
||||
let fido2Key: Fido2KeyView;
|
||||
let keyPair: CryptoKeyPair;
|
||||
if (params.requireResidentKey) {
|
||||
const userVerification = await this.userInterface.confirmNewCredential({
|
||||
credentialName: params.rpEntity.name,
|
||||
userName: params.userEntity.displayName,
|
||||
});
|
||||
const userVerification = await this.userInterface.confirmNewCredential(
|
||||
{
|
||||
credentialName: params.rpEntity.name,
|
||||
userName: params.userEntity.displayName,
|
||||
},
|
||||
abortController
|
||||
);
|
||||
|
||||
if (!userVerification) {
|
||||
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
|
||||
@ -90,7 +96,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
cipher = new CipherView();
|
||||
cipher.type = CipherType.Fido2Key;
|
||||
cipher.name = params.rpEntity.name;
|
||||
cipher.fido2Key = await createKeyView(params, keyPair.privateKey);
|
||||
cipher.fido2Key = fido2Key = await createKeyView(params, keyPair.privateKey);
|
||||
const encrypted = await this.cipherService.encrypt(cipher);
|
||||
await this.cipherService.createWithServer(encrypted); // encrypted.id is assigned inside here
|
||||
cipher.id = encrypted.id;
|
||||
@ -98,10 +104,13 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown);
|
||||
}
|
||||
} else {
|
||||
const cipherId = await this.userInterface.confirmNewNonDiscoverableCredential({
|
||||
credentialName: params.rpEntity.name,
|
||||
userName: params.userEntity.displayName,
|
||||
});
|
||||
const cipherId = await this.userInterface.confirmNewNonDiscoverableCredential(
|
||||
{
|
||||
credentialName: params.rpEntity.name,
|
||||
userName: params.userEntity.displayName,
|
||||
},
|
||||
abortController
|
||||
);
|
||||
|
||||
if (cipherId === undefined) {
|
||||
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
|
||||
@ -112,19 +121,21 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
|
||||
const encrypted = await this.cipherService.get(cipherId);
|
||||
cipher = await encrypted.decrypt();
|
||||
cipher.fido2Key = await createKeyView(params, keyPair.privateKey);
|
||||
cipher.login.fido2Key = fido2Key = await createKeyView(params, keyPair.privateKey);
|
||||
const reencrypted = await this.cipherService.encrypt(cipher);
|
||||
await this.cipherService.updateWithServer(reencrypted);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
const credentialId = params.requireResidentKey ? cipher.id : cipher.fido2Key.nonDiscoverableId;
|
||||
const credentialId =
|
||||
cipher.type === CipherType.Fido2Key ? cipher.id : cipher.login.fido2Key.nonDiscoverableId;
|
||||
|
||||
const authData = await generateAuthData({
|
||||
rpId: params.rpEntity.id,
|
||||
credentialId: Utils.guidToRawFormat(credentialId),
|
||||
counter: cipher.fido2Key.counter,
|
||||
counter: fido2Key.counter,
|
||||
userPresence: true,
|
||||
userVerification: false,
|
||||
keyPair,
|
||||
@ -185,12 +196,16 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
}
|
||||
|
||||
try {
|
||||
const selectedFido2Key =
|
||||
selectedCipher.type === CipherType.Login
|
||||
? selectedCipher.login.fido2Key
|
||||
: selectedCipher.fido2Key;
|
||||
const selectedCredentialId =
|
||||
params.allowCredentialDescriptorList?.length > 0
|
||||
? selectedCipher.fido2Key.nonDiscoverableId
|
||||
selectedCipher.type === CipherType.Login
|
||||
? selectedFido2Key.nonDiscoverableId
|
||||
: selectedCipher.id;
|
||||
|
||||
++selectedCipher.fido2Key.counter;
|
||||
++selectedFido2Key.counter;
|
||||
|
||||
selectedCipher.localData = {
|
||||
...selectedCipher.localData,
|
||||
@ -200,9 +215,9 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
await this.cipherService.updateWithServer(encrypted);
|
||||
|
||||
const authenticatorData = await generateAuthData({
|
||||
rpId: selectedCipher.fido2Key.rpId,
|
||||
rpId: selectedFido2Key.rpId,
|
||||
credentialId: Utils.guidToRawFormat(selectedCredentialId),
|
||||
counter: selectedCipher.fido2Key.counter,
|
||||
counter: selectedFido2Key.counter,
|
||||
userPresence: true,
|
||||
userVerification: false,
|
||||
});
|
||||
@ -210,14 +225,14 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
const signature = await generateSignature({
|
||||
authData: authenticatorData,
|
||||
clientDataHash: params.hash,
|
||||
privateKey: await getPrivateKeyFromCipher(selectedCipher),
|
||||
privateKey: await getPrivateKeyFromFido2Key(selectedFido2Key),
|
||||
});
|
||||
|
||||
return {
|
||||
authenticatorData,
|
||||
selectedCredential: {
|
||||
id: Utils.guidToRawFormat(selectedCredentialId),
|
||||
userHandle: Fido2Utils.stringToBuffer(selectedCipher.fido2Key.userHandle),
|
||||
userHandle: Fido2Utils.stringToBuffer(selectedFido2Key.userHandle),
|
||||
},
|
||||
signature,
|
||||
};
|
||||
@ -247,8 +262,8 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
(cipher) =>
|
||||
(cipher.type === CipherType.Fido2Key && ids.includes(cipher.id)) ||
|
||||
(cipher.type === CipherType.Login &&
|
||||
cipher.fido2Key != undefined &&
|
||||
ids.includes(cipher.fido2Key.nonDiscoverableId))
|
||||
cipher.login.fido2Key != undefined &&
|
||||
ids.includes(cipher.login.fido2Key.nonDiscoverableId))
|
||||
);
|
||||
}
|
||||
|
||||
@ -274,9 +289,9 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
(cipher) =>
|
||||
!cipher.isDeleted &&
|
||||
cipher.type === CipherType.Login &&
|
||||
cipher.fido2Key != undefined &&
|
||||
cipher.fido2Key.rpId === rpId &&
|
||||
ids.includes(cipher.fido2Key.nonDiscoverableId)
|
||||
cipher.login.fido2Key != undefined &&
|
||||
cipher.login.fido2Key.rpId === rpId &&
|
||||
ids.includes(cipher.login.fido2Key.nonDiscoverableId)
|
||||
);
|
||||
}
|
||||
|
||||
@ -324,14 +339,14 @@ async function createKeyView(
|
||||
return fido2Key;
|
||||
}
|
||||
|
||||
async function getPrivateKeyFromCipher(cipher: CipherView): Promise<CryptoKey> {
|
||||
const keyBuffer = Fido2Utils.stringToBuffer(cipher.fido2Key.keyValue);
|
||||
async function getPrivateKeyFromFido2Key(fido2Key: Fido2KeyView): Promise<CryptoKey> {
|
||||
const keyBuffer = Fido2Utils.stringToBuffer(fido2Key.keyValue);
|
||||
return await crypto.subtle.importKey(
|
||||
"pkcs8",
|
||||
keyBuffer,
|
||||
{
|
||||
name: cipher.fido2Key.keyAlgorithm,
|
||||
namedCurve: cipher.fido2Key.keyCurve,
|
||||
name: fido2Key.keyAlgorithm,
|
||||
namedCurve: fido2Key.keyCurve,
|
||||
} as EcKeyImportParams,
|
||||
true,
|
||||
KeyUsages
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } from "../abstractions/fido2-user-interface.service.abstraction";
|
||||
import { RequestAbortedError } from "../abstractions/fido2.service.abstraction";
|
||||
import { RequestAbortedError } from "../abstractions/fido2-client.service.abstraction";
|
||||
import {
|
||||
Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction,
|
||||
NewCredentialParams,
|
||||
} from "../abstractions/fido2-user-interface.service.abstraction";
|
||||
|
||||
export class Fido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction {
|
||||
async confirmCredential(): Promise<boolean> {
|
||||
@ -14,7 +17,18 @@ export class Fido2UserInterfaceService implements Fido2UserInterfaceServiceAbstr
|
||||
return false;
|
||||
}
|
||||
|
||||
async confirmDuplicateCredential() {
|
||||
return false;
|
||||
async confirmNewNonDiscoverableCredential(
|
||||
params: NewCredentialParams,
|
||||
abortController?: AbortController
|
||||
): Promise<string> {
|
||||
return null;
|
||||
}
|
||||
|
||||
async informExcludedCredential(
|
||||
existingCipherIds: string[],
|
||||
newCredential: NewCredentialParams,
|
||||
abortController?: AbortController
|
||||
): Promise<void> {
|
||||
// Not Implemented
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user