1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 16:23:44 +00:00

[EC-598] feat: fully wokring non-discoverable implementation

This commit is contained in:
Andreas Coroiu
2023-04-04 16:21:43 +02:00
parent 9dfd85dcd7
commit 55cd736ec3
13 changed files with 313 additions and 93 deletions

View File

@@ -47,6 +47,15 @@ export type BrowserFido2Message = { requestId: string } & (
| { | {
type: "ConfirmNewCredentialResponse"; type: "ConfirmNewCredentialResponse";
} }
| {
type: "ConfirmNewNonDiscoverableCredentialRequest";
credentialName: string;
userName: string;
}
| {
type: "ConfirmNewNonDiscoverableCredentialResponse";
cipherId: string;
}
| { | {
type: "AbortRequest"; type: "AbortRequest";
} }
@@ -201,10 +210,47 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi
} }
async confirmNewNonDiscoverableCredential( async confirmNewNonDiscoverableCredential(
params: NewCredentialParams, { credentialName, userName }: NewCredentialParams,
abortController?: AbortController abortController?: AbortController
): Promise<string> { ): 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( async informExcludedCredential(

View File

@@ -11,7 +11,12 @@
Authenticate Authenticate
</button> </button>
</ng-container> </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: A site is asking for authentication, please choose one of the following credentials to use:
<div class="box list"> <div class="box list">
<div class="box-content"> <div class="box-content">

View File

@@ -48,6 +48,10 @@ export class Fido2Component implements OnInit, OnDestroy {
return cipher.decrypt(); 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$) takeUntil(this.destroy$)
@@ -66,11 +70,19 @@ export class Fido2Component implements OnInit, OnDestroy {
} }
async pick(cipher: CipherView) { async pick(cipher: CipherView) {
BrowserFido2UserInterfaceService.sendMessage({ if (this.data?.type === "PickCredentialRequest") {
requestId: this.data.requestId, BrowserFido2UserInterfaceService.sendMessage({
cipherId: cipher.id, requestId: this.data.requestId,
type: "PickCredentialResponse", cipherId: cipher.id,
}); type: "PickCredentialResponse",
});
} else if (this.data?.type === "ConfirmNewNonDiscoverableCredentialRequest") {
BrowserFido2UserInterfaceService.sendMessage({
requestId: this.data.requestId,
cipherId: cipher.id,
type: "ConfirmNewNonDiscoverableCredentialResponse",
});
}
window.close(); window.close();
} }

View File

@@ -1,3 +1,4 @@
import { Fido2KeyApi } from "../../webauthn/models/api/fido2-key.api";
import { BaseResponse } from "../response/base.response"; import { BaseResponse } from "../response/base.response";
import { LoginUriApi } from "./login-uri.api"; import { LoginUriApi } from "./login-uri.api";
@@ -9,6 +10,7 @@ export class LoginApi extends BaseResponse {
passwordRevisionDate: string; passwordRevisionDate: string;
totp: string; totp: string;
autofillOnPageLoad: boolean; autofillOnPageLoad: boolean;
fido2Key?: Fido2KeyApi;
constructor(data: any = null) { constructor(data: any = null) {
super(data); super(data);
@@ -25,5 +27,10 @@ export class LoginApi extends BaseResponse {
if (uris != null) { if (uris != null) {
this.uris = uris.map((u: any) => new LoginUriApi(u)); this.uris = uris.map((u: any) => new LoginUriApi(u));
} }
const fido2Key = this.getResponseProperty("Fido2Key");
if (fido2Key != null) {
this.fido2Key = new Fido2KeyApi(fido2Key);
}
} }
} }

View File

@@ -60,6 +60,8 @@ export class CipherData {
switch (this.type) { switch (this.type) {
case CipherType.Login: case CipherType.Login:
this.login = new LoginData(response.login); this.login = new LoginData(response.login);
this.fido2Key =
response.fido2Key != undefined ? new Fido2KeyData(response.fido2Key) : undefined;
break; break;
case CipherType.SecureNote: case CipherType.SecureNote:
this.secureNote = new SecureNoteData(response.secureNote); this.secureNote = new SecureNoteData(response.secureNote);

View File

@@ -1,4 +1,5 @@
import { LoginApi } from "../../../models/api/login.api"; import { LoginApi } from "../../../models/api/login.api";
import { Fido2KeyData } from "../../../webauthn/models/data/fido2-key.data";
import { LoginUriData } from "./login-uri.data"; import { LoginUriData } from "./login-uri.data";
@@ -9,6 +10,7 @@ export class LoginData {
passwordRevisionDate: string; passwordRevisionDate: string;
totp: string; totp: string;
autofillOnPageLoad: boolean; autofillOnPageLoad: boolean;
fido2Key?: Fido2KeyData;
constructor(data?: LoginApi) { constructor(data?: LoginApi) {
if (data == null) { if (data == null) {
@@ -24,5 +26,9 @@ export class LoginData {
if (data.uris) { if (data.uris) {
this.uris = data.uris.map((u) => new LoginUriData(u)); this.uris = data.uris.map((u) => new LoginUriData(u));
} }
if (data.fido2Key) {
this.fido2Key = new Fido2KeyData(data.fido2Key);
}
} }
} }

View File

@@ -3,6 +3,7 @@ import { Jsonify } from "type-fest";
import Domain from "../../../models/domain/domain-base"; import Domain from "../../../models/domain/domain-base";
import { EncString } from "../../../models/domain/enc-string"; import { EncString } from "../../../models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../../models/domain/symmetric-crypto-key";
import { Fido2Key } from "../../../webauthn/models/domain/fido2-key";
import { LoginData } from "../data/login.data"; import { LoginData } from "../data/login.data";
import { LoginView } from "../view/login.view"; import { LoginView } from "../view/login.view";
@@ -15,6 +16,7 @@ export class Login extends Domain {
passwordRevisionDate?: Date; passwordRevisionDate?: Date;
totp: EncString; totp: EncString;
autofillOnPageLoad: boolean; autofillOnPageLoad: boolean;
fido2Key: Fido2Key;
constructor(obj?: LoginData) { constructor(obj?: LoginData) {
super(); super();
@@ -42,6 +44,10 @@ export class Login extends Domain {
this.uris.push(new LoginUri(u)); this.uris.push(new LoginUri(u));
}); });
} }
if (obj.fido2Key) {
this.fido2Key = new Fido2Key(obj.fido2Key);
}
} }
async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<LoginView> { 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; return view;
} }
@@ -85,6 +95,10 @@ export class Login extends Domain {
}); });
} }
if (this.fido2Key != null) {
l.fido2Key = this.fido2Key.toFido2KeyData();
}
return l; return l;
} }
@@ -99,13 +113,15 @@ export class Login extends Domain {
const passwordRevisionDate = const passwordRevisionDate =
obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate); obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate);
const uris = obj.uris?.map((uri: any) => LoginUri.fromJSON(uri)); 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, { return Object.assign(new Login(), obj, {
username, username,
password, password,
totp, totp,
passwordRevisionDate: passwordRevisionDate, passwordRevisionDate,
uris: uris, uris,
fido2Key,
}); });
} }
} }

View File

@@ -63,6 +63,44 @@ export class CipherRequest {
return uri; 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; break;
case CipherType.SecureNote: case CipherType.SecureNote:
this.secureNote = new SecureNoteApi(); this.secureNote = new SecureNoteApi();

View File

@@ -3,6 +3,7 @@ import { Jsonify } from "type-fest";
import { LoginLinkedId as LinkedId } from "../../../enums/linkedIdType"; import { LoginLinkedId as LinkedId } from "../../../enums/linkedIdType";
import { linkedFieldOption } from "../../../misc/linkedFieldOption.decorator"; import { linkedFieldOption } from "../../../misc/linkedFieldOption.decorator";
import { Utils } from "../../../misc/utils"; import { Utils } from "../../../misc/utils";
import { Fido2KeyView } from "../../../webauthn/models/view/fido2-key.view";
import { Login } from "../domain/login"; import { Login } from "../domain/login";
import { ItemView } from "./item.view"; import { ItemView } from "./item.view";
@@ -18,6 +19,7 @@ export class LoginView extends ItemView {
totp: string = null; totp: string = null;
uris: LoginUriView[] = null; uris: LoginUriView[] = null;
autofillOnPageLoad: boolean = null; autofillOnPageLoad: boolean = null;
fido2Key?: Fido2KeyView;
constructor(l?: Login) { constructor(l?: Login) {
super(); super();
@@ -67,10 +69,12 @@ export class LoginView extends ItemView {
const passwordRevisionDate = const passwordRevisionDate =
obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate); obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate);
const uris = obj.uris?.map((uri: any) => LoginUriView.fromJSON(uri)); 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, { return Object.assign(new LoginView(), obj, {
passwordRevisionDate: passwordRevisionDate, passwordRevisionDate: passwordRevisionDate,
uris: uris, uris,
fido2Key,
}); });
} }
} }

View File

@@ -1117,6 +1117,27 @@ export class CipherService implements CipherServiceAbstraction {
cipher.login.uris.push(loginUri); 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; return;
case CipherType.SecureNote: case CipherType.SecureNote:
cipher.secureNote = new SecureNote(); cipher.secureNote = new SecureNote();

View File

@@ -104,7 +104,7 @@ describe("FidoAuthenticatorService", () => {
params = await createParams({ params = await createParams({
excludeCredentialDescriptorList: [ excludeCredentialDescriptorList: [
{ {
id: Utils.guidToRawFormat(excludedCipher.fido2Key.nonDiscoverableId), id: Utils.guidToRawFormat(excludedCipher.login.fido2Key.nonDiscoverableId),
type: "public-key", type: "public-key",
}, },
], ],
@@ -162,15 +162,16 @@ describe("FidoAuthenticatorService", () => {
let params: Fido2AuthenticatorMakeCredentialsParams; let params: Fido2AuthenticatorMakeCredentialsParams;
beforeEach(async () => { beforeEach(async () => {
const excludedCipher = createCipherView(); excludedCipherView = createCipherView();
excludedCipherView = await excludedCipher;
params = await createParams({ params = await createParams({
excludeCredentialDescriptorList: [ excludeCredentialDescriptorList: [
{ id: Utils.guidToRawFormat(excludedCipher.id), type: "public-key" }, { id: Utils.guidToRawFormat(excludedCipherView.id), type: "public-key" },
], ],
}); });
cipherService.get.mockImplementation(async (id) => cipherService.get.mockImplementation(async (id) =>
id === excludedCipher.id ? excludedCipher : undefined id === excludedCipherView.id
? ({ decrypt: async () => excludedCipherView } as any)
: undefined
); );
cipherService.getAllDecrypted.mockResolvedValue([excludedCipherView]); cipherService.getAllDecrypted.mockResolvedValue([excludedCipherView]);
}); });
@@ -237,12 +238,15 @@ describe("FidoAuthenticatorService", () => {
return cipher; return cipher;
}); });
await authenticator.makeCredential(params); await authenticator.makeCredential(params, new AbortController());
expect(userInterface.confirmNewCredential).toHaveBeenCalledWith({ expect(userInterface.confirmNewCredential).toHaveBeenCalledWith(
credentialName: params.rpEntity.name, {
userName: params.userEntity.displayName, credentialName: params.rpEntity.name,
} as NewCredentialParams); userName: params.userEntity.displayName,
} as NewCredentialParams,
expect.anything()
);
}); });
it("should save credential to vault if request confirmed by user", async () => { 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 () => { it("should request confirmation from user", async () => {
userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(existingCipher.id); userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(existingCipher.id);
await authenticator.makeCredential(params); await authenticator.makeCredential(params, new AbortController());
expect(userInterface.confirmNewNonDiscoverableCredential).toHaveBeenCalledWith({ expect(userInterface.confirmNewNonDiscoverableCredential).toHaveBeenCalledWith(
credentialName: params.rpEntity.name, {
userName: params.userEntity.displayName, credentialName: params.rpEntity.name,
} as NewCredentialParams); userName: params.userEntity.displayName,
} as NewCredentialParams,
expect.anything()
);
}); });
it("should save credential to vault if request confirmed by user", async () => { it("should save credential to vault if request confirmed by user", async () => {
@@ -341,16 +348,18 @@ describe("FidoAuthenticatorService", () => {
type: CipherType.Login, type: CipherType.Login,
name: existingCipher.name, name: existingCipher.name,
fido2Key: expect.objectContaining({ login: expect.objectContaining({
nonDiscoverableId: expect.anything(), fido2Key: expect.objectContaining({
keyType: "public-key", nonDiscoverableId: expect.anything(),
keyAlgorithm: "ECDSA", keyType: "public-key",
keyCurve: "P-256", keyAlgorithm: "ECDSA",
rpId: params.rpEntity.id, keyCurve: "P-256",
rpName: params.rpEntity.name, rpId: params.rpEntity.id,
userHandle: Fido2Utils.bufferToString(params.userEntity.id), rpName: params.rpEntity.name,
counter: 0, userHandle: Fido2Utils.bufferToString(params.userEntity.id),
userName: params.userEntity.displayName, counter: 0,
userName: params.userEntity.displayName,
}),
}), }),
}) })
); );
@@ -406,7 +415,9 @@ describe("FidoAuthenticatorService", () => {
); );
cipherService.getAllDecrypted.mockResolvedValue([await cipher]); cipherService.getAllDecrypted.mockResolvedValue([await cipher]);
cipherService.encrypt.mockImplementation(async (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; return {} as any;
}); });
cipherService.createWithServer.mockImplementation(async (cipher) => { cipherService.createWithServer.mockImplementation(async (cipher) => {
@@ -561,8 +572,8 @@ describe("FidoAuthenticatorService", () => {
it("should throw error if credential exists but rpId does not match", async () => { it("should throw error if credential exists but rpId does not match", async () => {
const cipher = await createCipherView({ type: CipherType.Login }); const cipher = await createCipherView({ type: CipherType.Login });
cipher.fido2Key.nonDiscoverableId = credentialId; cipher.login.fido2Key.nonDiscoverableId = credentialId;
cipher.fido2Key.rpId = "mismatch-rpid"; cipher.login.fido2Key.rpId = "mismatch-rpid";
cipherService.getAllDecrypted.mockResolvedValue([cipher]); cipherService.getAllDecrypted.mockResolvedValue([cipher]);
const result = async () => await authenticator.getAssertion(params); const result = async () => await authenticator.getAssertion(params);
@@ -639,6 +650,7 @@ describe("FidoAuthenticatorService", () => {
let credentialIds: string[]; let credentialIds: string[];
let selectedCredentialId: string; let selectedCredentialId: string;
let ciphers: CipherView[]; let ciphers: CipherView[];
let fido2Keys: Fido2KeyView[];
let params: Fido2AuthenticatorGetAssertionParams; let params: Fido2AuthenticatorGetAssertionParams;
beforeEach(async () => { beforeEach(async () => {
@@ -654,6 +666,7 @@ describe("FidoAuthenticatorService", () => {
{ rpId: RpId, counter: 9000, keyValue } { rpId: RpId, counter: 9000, keyValue }
) )
); );
fido2Keys = ciphers.map((c) => c.fido2Key);
selectedCredentialId = ciphers[0].id; selectedCredentialId = ciphers[0].id;
params = await createParams({ params = await createParams({
allowCredentialDescriptorList: undefined, allowCredentialDescriptorList: undefined,
@@ -666,6 +679,7 @@ describe("FidoAuthenticatorService", () => {
{ nonDiscoverableId: id, rpId: RpId, counter: 9000 } { nonDiscoverableId: id, rpId: RpId, counter: 9000 }
) )
); );
fido2Keys = ciphers.map((c) => c.login.fido2Key);
selectedCredentialId = credentialIds[0]; selectedCredentialId = credentialIds[0];
params = await createParams({ params = await createParams({
allowCredentialDescriptorList: credentialIds.map((credentialId) => ({ allowCredentialDescriptorList: credentialIds.map((credentialId) => ({
@@ -686,15 +700,28 @@ describe("FidoAuthenticatorService", () => {
await authenticator.getAssertion(params); await authenticator.getAssertion(params);
expect(cipherService.encrypt).toHaveBeenCalledWith(
expect.objectContaining({
id: ciphers[0].id,
fido2Key: expect.objectContaining({
counter: 9001,
}),
})
);
expect(cipherService.updateWithServer).toHaveBeenCalledWith(encrypted); 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 () => { it("should return an assertion result", async () => {
@@ -707,7 +734,7 @@ describe("FidoAuthenticatorService", () => {
expect(result.selectedCredential.id).toEqual(Utils.guidToRawFormat(selectedCredentialId)); expect(result.selectedCredential.id).toEqual(Utils.guidToRawFormat(selectedCredentialId));
expect(result.selectedCredential.userHandle).toEqual( expect(result.selectedCredential.userHandle).toEqual(
Fido2Utils.stringToBuffer(ciphers[0].fido2Key.userHandle) Fido2Utils.stringToBuffer(fido2Keys[0].userHandle)
); );
expect(rpIdHash).toEqual( expect(rpIdHash).toEqual(
new Uint8Array([ new Uint8Array([
@@ -779,18 +806,25 @@ function createCipherView(
cipher.id = data.id ?? Utils.newGuid(); cipher.id = data.id ?? Utils.newGuid();
cipher.type = data.type ?? CipherType.Fido2Key; cipher.type = data.type ?? CipherType.Fido2Key;
cipher.localData = {}; cipher.localData = {};
cipher.login = data.type ?? data.type === CipherType.Login ? new LoginView() : null;
cipher.fido2Key = new Fido2KeyView(); const fido2KeyView = new Fido2KeyView();
cipher.fido2Key.nonDiscoverableId = fido2Key.nonDiscoverableId; fido2KeyView.nonDiscoverableId = fido2Key.nonDiscoverableId;
cipher.fido2Key.rpId = fido2Key.rpId ?? RpId; fido2KeyView.rpId = fido2Key.rpId ?? RpId;
cipher.fido2Key.counter = fido2Key.counter ?? 0; fido2KeyView.counter = fido2Key.counter ?? 0;
cipher.fido2Key.userHandle = fido2Key.userHandle ?? Fido2Utils.bufferToString(randomBytes(16)); fido2KeyView.userHandle = fido2Key.userHandle ?? Fido2Utils.bufferToString(randomBytes(16));
cipher.fido2Key.keyAlgorithm = fido2Key.keyAlgorithm ?? "ECDSA"; fido2KeyView.keyAlgorithm = fido2Key.keyAlgorithm ?? "ECDSA";
cipher.fido2Key.keyCurve = fido2Key.keyCurve ?? "P-256"; fido2KeyView.keyCurve = fido2Key.keyCurve ?? "P-256";
cipher.fido2Key.keyValue = fido2KeyView.keyValue =
fido2Key.keyValue ?? fido2KeyView.keyValue ??
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgTC-7XDZipXbaVBlnkjlBgO16ZmqBZWejK2iYo6lV0dehRANCAASOcM2WduNq1DriRYN7ZekvZz-bRhA-qNT4v0fbp5suUFJyWmgOQ0bybZcLXHaerK5Ep1JiSrQcewtQNgLtry7f"; "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgTC-7XDZipXbaVBlnkjlBgO16ZmqBZWejK2iYo6lV0dehRANCAASOcM2WduNq1DriRYN7ZekvZz-bRhA-qNT4v0fbp5suUFJyWmgOQ0bybZcLXHaerK5Ep1JiSrQcewtQNgLtry7f";
if (cipher.type === CipherType.Login) {
cipher.login = new LoginView();
cipher.login.fido2Key = fido2KeyView;
} else {
cipher.fido2Key = fido2KeyView;
}
return cipher; return cipher;
} }

View File

@@ -38,7 +38,8 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
private userInterface: Fido2UserInterfaceService private userInterface: Fido2UserInterfaceService
) {} ) {}
async makeCredential( async makeCredential(
params: Fido2AuthenticatorMakeCredentialsParams params: Fido2AuthenticatorMakeCredentialsParams,
abortController?: AbortController
): Promise<Fido2AuthenticatorMakeCredentialResult> { ): Promise<Fido2AuthenticatorMakeCredentialResult> {
if (params.credTypesAndPubKeyAlgs.every((p) => p.alg !== Fido2AlgorithmIdentifier.ES256)) { if (params.credTypesAndPubKeyAlgs.every((p) => p.alg !== Fido2AlgorithmIdentifier.ES256)) {
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotSupported); throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotSupported);
@@ -66,19 +67,24 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
{ {
credentialName: params.rpEntity.name, credentialName: params.rpEntity.name,
userName: params.userEntity.displayName, userName: params.userEntity.displayName,
} },
abortController
); );
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed); throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
} }
let cipher: CipherView; let cipher: CipherView;
let fido2Key: Fido2KeyView;
let keyPair: CryptoKeyPair; let keyPair: CryptoKeyPair;
if (params.requireResidentKey) { if (params.requireResidentKey) {
const userVerification = await this.userInterface.confirmNewCredential({ const userVerification = await this.userInterface.confirmNewCredential(
credentialName: params.rpEntity.name, {
userName: params.userEntity.displayName, credentialName: params.rpEntity.name,
}); userName: params.userEntity.displayName,
},
abortController
);
if (!userVerification) { if (!userVerification) {
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed); throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
@@ -90,7 +96,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
cipher = new CipherView(); cipher = new CipherView();
cipher.type = CipherType.Fido2Key; cipher.type = CipherType.Fido2Key;
cipher.name = params.rpEntity.name; 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); const encrypted = await this.cipherService.encrypt(cipher);
await this.cipherService.createWithServer(encrypted); // encrypted.id is assigned inside here await this.cipherService.createWithServer(encrypted); // encrypted.id is assigned inside here
cipher.id = encrypted.id; cipher.id = encrypted.id;
@@ -98,10 +104,13 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown); throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown);
} }
} else { } else {
const cipherId = await this.userInterface.confirmNewNonDiscoverableCredential({ const cipherId = await this.userInterface.confirmNewNonDiscoverableCredential(
credentialName: params.rpEntity.name, {
userName: params.userEntity.displayName, credentialName: params.rpEntity.name,
}); userName: params.userEntity.displayName,
},
abortController
);
if (cipherId === undefined) { if (cipherId === undefined) {
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed); throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
@@ -112,19 +121,21 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
const encrypted = await this.cipherService.get(cipherId); const encrypted = await this.cipherService.get(cipherId);
cipher = await encrypted.decrypt(); 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); const reencrypted = await this.cipherService.encrypt(cipher);
await this.cipherService.updateWithServer(reencrypted); await this.cipherService.updateWithServer(reencrypted);
} catch (error) { } catch {
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown); 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({ const authData = await generateAuthData({
rpId: params.rpEntity.id, rpId: params.rpEntity.id,
credentialId: Utils.guidToRawFormat(credentialId), credentialId: Utils.guidToRawFormat(credentialId),
counter: cipher.fido2Key.counter, counter: fido2Key.counter,
userPresence: true, userPresence: true,
userVerification: false, userVerification: false,
keyPair, keyPair,
@@ -185,12 +196,16 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
} }
try { try {
const selectedFido2Key =
selectedCipher.type === CipherType.Login
? selectedCipher.login.fido2Key
: selectedCipher.fido2Key;
const selectedCredentialId = const selectedCredentialId =
params.allowCredentialDescriptorList?.length > 0 selectedCipher.type === CipherType.Login
? selectedCipher.fido2Key.nonDiscoverableId ? selectedFido2Key.nonDiscoverableId
: selectedCipher.id; : selectedCipher.id;
++selectedCipher.fido2Key.counter; ++selectedFido2Key.counter;
selectedCipher.localData = { selectedCipher.localData = {
...selectedCipher.localData, ...selectedCipher.localData,
@@ -200,9 +215,9 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
await this.cipherService.updateWithServer(encrypted); await this.cipherService.updateWithServer(encrypted);
const authenticatorData = await generateAuthData({ const authenticatorData = await generateAuthData({
rpId: selectedCipher.fido2Key.rpId, rpId: selectedFido2Key.rpId,
credentialId: Utils.guidToRawFormat(selectedCredentialId), credentialId: Utils.guidToRawFormat(selectedCredentialId),
counter: selectedCipher.fido2Key.counter, counter: selectedFido2Key.counter,
userPresence: true, userPresence: true,
userVerification: false, userVerification: false,
}); });
@@ -210,14 +225,14 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
const signature = await generateSignature({ const signature = await generateSignature({
authData: authenticatorData, authData: authenticatorData,
clientDataHash: params.hash, clientDataHash: params.hash,
privateKey: await getPrivateKeyFromCipher(selectedCipher), privateKey: await getPrivateKeyFromFido2Key(selectedFido2Key),
}); });
return { return {
authenticatorData, authenticatorData,
selectedCredential: { selectedCredential: {
id: Utils.guidToRawFormat(selectedCredentialId), id: Utils.guidToRawFormat(selectedCredentialId),
userHandle: Fido2Utils.stringToBuffer(selectedCipher.fido2Key.userHandle), userHandle: Fido2Utils.stringToBuffer(selectedFido2Key.userHandle),
}, },
signature, signature,
}; };
@@ -247,8 +262,8 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
(cipher) => (cipher) =>
(cipher.type === CipherType.Fido2Key && ids.includes(cipher.id)) || (cipher.type === CipherType.Fido2Key && ids.includes(cipher.id)) ||
(cipher.type === CipherType.Login && (cipher.type === CipherType.Login &&
cipher.fido2Key != undefined && cipher.login.fido2Key != undefined &&
ids.includes(cipher.fido2Key.nonDiscoverableId)) ids.includes(cipher.login.fido2Key.nonDiscoverableId))
); );
} }
@@ -274,9 +289,9 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
(cipher) => (cipher) =>
!cipher.isDeleted && !cipher.isDeleted &&
cipher.type === CipherType.Login && cipher.type === CipherType.Login &&
cipher.fido2Key != undefined && cipher.login.fido2Key != undefined &&
cipher.fido2Key.rpId === rpId && cipher.login.fido2Key.rpId === rpId &&
ids.includes(cipher.fido2Key.nonDiscoverableId) ids.includes(cipher.login.fido2Key.nonDiscoverableId)
); );
} }
@@ -324,14 +339,14 @@ async function createKeyView(
return fido2Key; return fido2Key;
} }
async function getPrivateKeyFromCipher(cipher: CipherView): Promise<CryptoKey> { async function getPrivateKeyFromFido2Key(fido2Key: Fido2KeyView): Promise<CryptoKey> {
const keyBuffer = Fido2Utils.stringToBuffer(cipher.fido2Key.keyValue); const keyBuffer = Fido2Utils.stringToBuffer(fido2Key.keyValue);
return await crypto.subtle.importKey( return await crypto.subtle.importKey(
"pkcs8", "pkcs8",
keyBuffer, keyBuffer,
{ {
name: cipher.fido2Key.keyAlgorithm, name: fido2Key.keyAlgorithm,
namedCurve: cipher.fido2Key.keyCurve, namedCurve: fido2Key.keyCurve,
} as EcKeyImportParams, } as EcKeyImportParams,
true, true,
KeyUsages KeyUsages

View File

@@ -1,5 +1,8 @@
import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } from "../abstractions/fido2-user-interface.service.abstraction"; import { RequestAbortedError } from "../abstractions/fido2-client.service.abstraction";
import { RequestAbortedError } from "../abstractions/fido2.service.abstraction"; import {
Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction,
NewCredentialParams,
} from "../abstractions/fido2-user-interface.service.abstraction";
export class Fido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction { export class Fido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction {
async confirmCredential(): Promise<boolean> { async confirmCredential(): Promise<boolean> {
@@ -14,7 +17,18 @@ export class Fido2UserInterfaceService implements Fido2UserInterfaceServiceAbstr
return false; return false;
} }
async confirmDuplicateCredential() { async confirmNewNonDiscoverableCredential(
return false; params: NewCredentialParams,
abortController?: AbortController
): Promise<string> {
return null;
}
async informExcludedCredential(
existingCipherIds: string[],
newCredential: NewCredentialParams,
abortController?: AbortController
): Promise<void> {
// Not Implemented
} }
} }