1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-31 22:51:28 +01:00

[EC-598] feat: make everything compile again

This commit is contained in:
Andreas Coroiu 2023-03-31 10:26:19 +02:00
parent 25ebbec0eb
commit 380e545c90
No known key found for this signature in database
GPG Key ID: E70B5FFC81DFEC1A
12 changed files with 657 additions and 600 deletions

View File

@ -82,9 +82,11 @@ import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
import { SyncNotifierService } from "@bitwarden/common/vault/services/sync/sync-notifier.service";
import { SyncService } from "@bitwarden/common/vault/services/sync/sync.service";
import { Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction } from "@bitwarden/common/webauthn/abstractions/fido2-authenticator.service.abstraction";
import { Fido2ClientService as Fido2ClientServiceAbstraction } from "@bitwarden/common/webauthn/abstractions/fido2-client.service.abstraction";
import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } from "@bitwarden/common/webauthn/abstractions/fido2-user-interface.service.abstraction";
import { Fido2Service as Fido2ServiceAbstraction } from "@bitwarden/common/webauthn/abstractions/fido2.service.abstraction";
import { Fido2Service } from "@bitwarden/common/webauthn/services/fido2.service";
import { Fido2AuthenticatorService } from "@bitwarden/common/webauthn/services/fido2-authenticator.service";
import { Fido2ClientService } from "@bitwarden/common/webauthn/services/fido2-client.service";
import ContextMenusBackground from "../autofill/background/context-menus.background";
import NotificationBackground from "../autofill/background/notification.background";
@ -178,7 +180,8 @@ export default class MainBackground {
userVerificationApiService: UserVerificationApiServiceAbstraction;
syncNotifierService: SyncNotifierServiceAbstraction;
fido2UserInterfaceService: Fido2UserInterfaceServiceAbstraction;
fido2Service: Fido2ServiceAbstraction;
fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction;
fido2ClientService: Fido2ClientServiceAbstraction;
avatarUpdateService: AvatarUpdateServiceAbstraction;
mainContextMenuHandler: MainContextMenuHandler;
cipherContextMenuHandler: CipherContextMenuHandler;
@ -481,7 +484,11 @@ export default class MainBackground {
);
this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.popupUtilsService);
this.fido2Service = new Fido2Service(this.fido2UserInterfaceService, this.cipherService);
this.fido2AuthenticatorService = new Fido2AuthenticatorService(
this.cipherService,
this.fido2UserInterfaceService
);
this.fido2ClientService = new Fido2ClientService(this.fido2AuthenticatorService);
const systemUtilsServiceReloadCallback = () => {
const forceWindowReload =

View File

@ -223,11 +223,11 @@ export default class RuntimeBackground {
this.abortControllers.get(msg.abortedRequestId)?.abort();
break;
case "fido2RegisterCredentialRequest":
return await this.main.fido2Service
return await this.main.fido2ClientService
.createCredential(msg.data, this.createAbortController(msg.requestId))
.finally(() => this.abortControllers.delete(msg.requestId));
case "fido2GetCredentialRequest":
return await this.main.fido2Service
return await this.main.fido2ClientService
.assertCredential(msg.data, this.createAbortController(msg.requestId))
.finally(() => this.abortControllers.delete(msg.requestId));
}

View File

@ -1,16 +1,16 @@
import { Fido2Utils } from "@bitwarden/common/webauthn/abstractions/fido2-utils";
import {
CredentialAssertParams,
CredentialAssertResult,
CredentialRegistrationParams,
CredentialRegistrationResult,
} from "@bitwarden/common/webauthn/abstractions/fido2.service.abstraction";
CreateCredentialParams,
CreateCredentialResult,
AssertCredentialParams,
AssertCredentialResult,
} from "@bitwarden/common/webauthn/abstractions/fido2-client.service.abstraction";
import { Fido2Utils } from "@bitwarden/common/webauthn/abstractions/fido2-utils";
class BitAuthenticatorAttestationResponse implements AuthenticatorAttestationResponse {
clientDataJSON: ArrayBuffer;
attestationObject: ArrayBuffer;
constructor(private result: CredentialRegistrationResult) {
constructor(private result: CreateCredentialResult) {
this.clientDataJSON = Fido2Utils.stringToBuffer(result.clientDataJSON);
this.attestationObject = Fido2Utils.stringToBuffer(result.attestationObject);
}
@ -35,8 +35,9 @@ class BitAuthenticatorAttestationResponse implements AuthenticatorAttestationRes
export class WebauthnUtils {
static mapCredentialCreationOptions(
options: CredentialCreationOptions,
origin: string
): CredentialRegistrationParams {
origin: string,
sameOriginWithAncestors: boolean
): CreateCredentialParams {
const keyOptions = options.publicKey;
if (keyOptions == undefined) {
@ -55,15 +56,12 @@ export class WebauthnUtils {
excludeCredentials: keyOptions.excludeCredentials?.map((credential) => ({
id: Fido2Utils.bufferToString(credential.id),
transports: credential.transports,
type: credential.type,
})),
extensions: {
appid: keyOptions.extensions?.appid,
appidExclude: keyOptions.extensions?.appidExclude,
credProps: keyOptions.extensions?.credProps,
uvm: keyOptions.extensions?.uvm,
},
extensions: undefined, // extensions not currently supported
pubKeyCredParams: keyOptions.pubKeyCredParams.map((params) => ({
alg: params.alg,
type: params.type,
})),
rp: {
id: keyOptions.rp.id,
@ -74,12 +72,11 @@ export class WebauthnUtils {
displayName: keyOptions.user.displayName,
},
timeout: keyOptions.timeout,
sameOriginWithAncestors,
};
}
static mapCredentialRegistrationResult(
result: CredentialRegistrationResult
): PublicKeyCredential {
static mapCredentialRegistrationResult(result: CreateCredentialResult): PublicKeyCredential {
return {
id: result.credentialId,
rawId: Fido2Utils.stringToBuffer(result.credentialId),
@ -92,8 +89,9 @@ export class WebauthnUtils {
static mapCredentialRequestOptions(
options: CredentialRequestOptions,
origin: string
): CredentialAssertParams {
origin: string,
sameOriginWithAncestors: boolean
): AssertCredentialParams {
const keyOptions = options.publicKey;
if (keyOptions == undefined) {
@ -108,10 +106,11 @@ export class WebauthnUtils {
rpId: keyOptions.rpId,
userVerification: keyOptions.userVerification,
timeout: keyOptions.timeout,
sameOriginWithAncestors,
};
}
static mapCredentialAssertResult(result: CredentialAssertResult): PublicKeyCredential {
static mapCredentialAssertResult(result: AssertCredentialResult): PublicKeyCredential {
return {
id: result.credentialId,
rawId: Fido2Utils.stringToBuffer(result.credentialId),

View File

@ -5,13 +5,24 @@ import {
Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction,
NewCredentialParams,
} from "@bitwarden/common/webauthn/abstractions/fido2-user-interface.service.abstraction";
import { RequestAbortedError } from "@bitwarden/common/webauthn/abstractions/fido2.service.abstraction";
import { BrowserApi } from "../../browser/browserApi";
import { PopupUtilsService } from "../../popup/services/popup-utils.service";
const BrowserFido2MessageName = "BrowserFido2UserInterfaceServiceMessage";
export class Fido2Error extends Error {
constructor(message: string, readonly fallbackRequested = false) {
super(message);
}
}
export class RequestAbortedError extends Fido2Error {
constructor(fallbackRequested = false) {
super("Fido2 request was aborted", fallbackRequested);
}
}
export type BrowserFido2Message = { requestId: string } & (
| {
type: "PickCredentialRequest";
@ -198,6 +209,21 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi
return false;
}
async confirmNewNonDiscoverableCredential(
params: NewCredentialParams,
abortController?: AbortController
): Promise<string> {
return null;
}
async informExcludedCredential(
existingCipherIds: string[],
newCredential: NewCredentialParams,
abortController?: AbortController
): Promise<void> {
// Not Implemented
}
private setAbortTimeout(abortController: AbortController) {
return setTimeout(() => abortController.abort());
}

View File

@ -1,9 +1,9 @@
import {
CredentialAssertParams,
CredentialAssertResult,
CredentialRegistrationParams,
CredentialRegistrationResult,
} from "@bitwarden/common/webauthn/abstractions/fido2.service.abstraction";
CreateCredentialParams,
CreateCredentialResult,
AssertCredentialParams,
AssertCredentialResult,
} from "@bitwarden/common/webauthn/abstractions/fido2-client.service.abstraction";
export enum MessageType {
CredentialCreationRequest,
@ -17,22 +17,22 @@ export enum MessageType {
export type CredentialCreationRequest = {
type: MessageType.CredentialCreationRequest;
data: CredentialRegistrationParams;
data: CreateCredentialParams;
};
export type CredentialCreationResponse = {
type: MessageType.CredentialCreationResponse;
result?: CredentialRegistrationResult;
result?: CreateCredentialResult;
};
export type CredentialGetRequest = {
type: MessageType.CredentialGetRequest;
data: CredentialAssertParams;
data: AssertCredentialParams;
};
export type CredentialGetResponse = {
type: MessageType.CredentialGetResponse;
result?: CredentialAssertResult;
result?: AssertCredentialResult;
};
export type AbortRequest = {

View File

@ -22,7 +22,8 @@ navigator.credentials.create = async (
const response = await messenger.request(
{
type: MessageType.CredentialCreationRequest,
data: WebauthnUtils.mapCredentialCreationOptions(options, window.location.origin),
// TODO: Fix sameOriginWithAncestors!
data: WebauthnUtils.mapCredentialCreationOptions(options, window.location.origin, true),
},
abortController
);
@ -49,7 +50,8 @@ navigator.credentials.get = async (
const response = await messenger.request(
{
type: MessageType.CredentialGetRequest,
data: WebauthnUtils.mapCredentialRequestOptions(options, window.location.origin),
// TODO: Fix sameOriginWithAncestors!
data: WebauthnUtils.mapCredentialRequestOptions(options, window.location.origin, true),
},
abortController
);

View File

@ -127,7 +127,11 @@ export class CipherRequest {
this.fido2Key = new Fido2KeyApi();
this.fido2Key.keyType =
cipher.fido2Key.keyType != null
? (cipher.fido2Key.keyType.encryptedString as "ECDSA")
? (cipher.fido2Key.keyType.encryptedString as "public-key")
: null;
this.fido2Key.keyAlgorithm =
cipher.fido2Key.keyAlgorithm != null
? (cipher.fido2Key.keyAlgorithm.encryptedString as "ECDSA")
: null;
this.fido2Key.keyCurve =
cipher.fido2Key.keyCurve != null

View File

@ -39,7 +39,7 @@ export class Fido2AutenticatorError extends Error {
export interface PublicKeyCredentialDescriptor {
id: BufferSource;
transports?: ("ble" | "internal" | "nfc" | "usb")[];
transports?: ("ble" | "hybrid" | "internal" | "nfc" | "usb")[];
type: "public-key";
}

View File

@ -24,7 +24,7 @@ export interface CreateCredentialParams {
challenge: string; // b64 encoded
excludeCredentials?: {
id: string; // b64 encoded
transports?: ("ble" | "internal" | "nfc" | "usb")[];
transports?: ("ble" | "hybrid" | "internal" | "nfc" | "usb")[];
type: "public-key";
}[];
extensions?: {

View File

@ -1,101 +1,110 @@
export type UserVerification = "discouraged" | "preferred" | "required";
/**
*
* REMOVE BEFORE MERGE
*
* This is the old version of our FIDO2 client which was built according to spec.
* It left here for reference purposes until we no longer need it.
*
*/
export interface CredentialRegistrationParams {
origin: string;
attestation?: "direct" | "enterprise" | "indirect" | "none";
authenticatorSelection?: {
// authenticatorAttachment?: AuthenticatorAttachment; // not used
requireResidentKey?: boolean;
residentKey?: "discouraged" | "preferred" | "required";
userVerification?: UserVerification;
};
challenge: string; // b64 encoded
excludeCredentials?: {
id: string; // b64 encoded
transports?: ("ble" | "internal" | "nfc" | "usb")[];
// type: "public-key"; // not used
}[];
extensions?: {
appid?: string;
appidExclude?: string;
credProps?: boolean;
uvm?: boolean;
};
pubKeyCredParams: {
alg: number;
// type: "public-key"; // not used
}[];
rp: {
id?: string;
name: string;
};
user: {
id: string; // b64 encoded
displayName: string;
};
timeout: number;
}
// export type UserVerification = "discouraged" | "preferred" | "required";
export interface CredentialRegistrationResult {
credentialId: string;
clientDataJSON: string;
attestationObject: string;
authData: string;
publicKeyAlgorithm: number;
transports: string[];
}
// export interface CredentialRegistrationParams {
// origin: string;
// attestation?: "direct" | "enterprise" | "indirect" | "none";
// authenticatorSelection?: {
// // authenticatorAttachment?: AuthenticatorAttachment; // not used
// requireResidentKey?: boolean;
// residentKey?: "discouraged" | "preferred" | "required";
// userVerification?: UserVerification;
// };
// challenge: string; // b64 encoded
// excludeCredentials?: {
// id: string; // b64 encoded
// transports?: ("ble" | "internal" | "nfc" | "usb")[];
// // type: "public-key"; // not used
// }[];
// extensions?: {
// appid?: string;
// appidExclude?: string;
// credProps?: boolean;
// uvm?: boolean;
// };
// pubKeyCredParams: {
// alg: number;
// // type: "public-key"; // not used
// }[];
// rp: {
// id?: string;
// name: string;
// };
// user: {
// id: string; // b64 encoded
// displayName: string;
// };
// timeout: number;
// }
export interface CredentialAssertParams {
allowedCredentialIds: string[];
rpId: string;
origin: string;
challenge: string;
userVerification?: UserVerification;
timeout: number;
}
// export interface CredentialRegistrationResult {
// credentialId: string;
// clientDataJSON: string;
// attestationObject: string;
// authData: string;
// publicKeyAlgorithm: number;
// transports: string[];
// }
export interface CredentialAssertResult {
credentialId: string;
clientDataJSON: string;
authenticatorData: string;
signature: string;
userHandle: string;
}
// export interface CredentialAssertParams {
// allowedCredentialIds: string[];
// rpId: string;
// origin: string;
// challenge: string;
// userVerification?: UserVerification;
// timeout: number;
// }
export class Fido2Error extends Error {
constructor(message: string, readonly fallbackRequested = false) {
super(message);
}
}
// export interface CredentialAssertResult {
// credentialId: string;
// clientDataJSON: string;
// authenticatorData: string;
// signature: string;
// userHandle: string;
// }
export class RequestAbortedError extends Fido2Error {
constructor(fallbackRequested = false) {
super("Fido2 request was aborted", fallbackRequested);
}
}
// export class Fido2Error extends Error {
// constructor(message: string, readonly fallbackRequested = false) {
// super(message);
// }
// }
export class NoCredentialFoundError extends Fido2Error {
constructor() {
super("No valid credential found", true);
}
}
// export class RequestAbortedError extends Fido2Error {
// constructor(fallbackRequested = false) {
// super("Fido2 request was aborted", fallbackRequested);
// }
// }
export class OriginMismatchError extends Fido2Error {
constructor() {
super(
"Authentication requests must originate from the same source that created the credential.",
false
);
}
}
// export class NoCredentialFoundError extends Fido2Error {
// constructor() {
// super("No valid credential found", true);
// }
// }
export abstract class Fido2Service {
createCredential: (
params: CredentialRegistrationParams,
abortController?: AbortController
) => Promise<CredentialRegistrationResult>;
assertCredential: (
params: CredentialAssertParams,
abortController?: AbortController
) => Promise<CredentialAssertResult>;
}
// export class OriginMismatchError extends Fido2Error {
// constructor() {
// super(
// "Authentication requests must originate from the same source that created the credential.",
// false
// );
// }
// }
// export abstract class Fido2Service {
// createCredential: (
// params: CredentialRegistrationParams,
// abortController?: AbortController
// ) => Promise<CredentialRegistrationResult>;
// assertCredential: (
// params: CredentialAssertParams,
// abortController?: AbortController
// ) => Promise<CredentialAssertResult>;
// }

View File

@ -1,9 +1,9 @@
import { CBOR } from "cbor-redux";
import { Utils } from "../../misc/utils";
import { CipherService } from "../../vault/abstractions/cipher.service";
import { CipherType } from "../../vault/enums/cipher-type";
import { CipherView } from "../../vault/models/view/cipher.view";
import { CipherService } from "../../vault/services/cipher.service";
import {
Fido2AlgorithmIdentifier,
Fido2AutenticatorError,
@ -13,6 +13,7 @@ import {
Fido2AuthenticatorMakeCredentialResult,
Fido2AuthenticatorMakeCredentialsParams,
Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction,
PublicKeyCredentialDescriptor,
} from "../abstractions/fido2-authenticator.service.abstraction";
import { Fido2UserInterfaceService } from "../abstractions/fido2-user-interface.service.abstraction";
import { Fido2Utils } from "../abstractions/fido2-utils";

View File

@ -1,465 +1,474 @@
import { CBOR } from "cbor-redux";
import { Utils } from "../../misc/utils";
import { CipherService } from "../../vault/abstractions/cipher.service";
import { CipherType } from "../../vault/enums/cipher-type";
import { Cipher } from "../../vault/models/domain/cipher";
import { CipherView } from "../../vault/models/view/cipher.view";
import { Fido2UserInterfaceService } from "../abstractions/fido2-user-interface.service.abstraction";
import { Fido2Utils } from "../abstractions/fido2-utils";
import {
CredentialAssertParams,
CredentialAssertResult,
CredentialRegistrationParams,
CredentialRegistrationResult,
Fido2Service as Fido2ServiceAbstraction,
NoCredentialFoundError,
UserVerification,
} from "../abstractions/fido2.service.abstraction";
import { Fido2KeyView } from "../models/view/fido2-key.view";
import { CredentialId } from "./credential-id";
import { joseToDer } from "./ecdsa-utils";
// We support self-signing, but Google won't accept it.
// TODO: Look into supporting self-signed packed format.
const STANDARD_ATTESTATION_FORMAT: "none" | "packed" = "none";
const TIMEOUTS = {
NO_VERIFICATION: {
DEFAULT: 120000,
MIN: 30000,
MAX: 180000,
},
WITH_VERIFICATION: {
DEFAULT: 300000,
MIN: 30000,
MAX: 600000,
},
};
interface BitCredential {
credentialId: CredentialId;
keyType: "ECDSA";
keyCurve: "P-256";
keyValue: CryptoKey;
rpId: string;
rpName: string;
userHandle: Uint8Array;
userName: string;
origin: string;
}
const KeyUsages: KeyUsage[] = ["sign"];
export class Fido2Service implements Fido2ServiceAbstraction {
constructor(
private fido2UserInterfaceService: Fido2UserInterfaceService,
private cipherService: CipherService
) {}
async createCredential(
params: CredentialRegistrationParams,
abortController = new AbortController()
): Promise<CredentialRegistrationResult> {
// Comment: Timeouts could potentially be implemented using decorators.
// But since I try to use decorators a little as possible and only
// for the most generic solutions, I'm gonne leave this as is untill peer review.
const timeout = setAbortTimeout(
abortController,
params.authenticatorSelection.userVerification,
params.timeout
);
const presence = await this.fido2UserInterfaceService.confirmNewCredential(
{
credentialName: params.rp.name,
userName: params.user.displayName,
},
abortController
);
const attestationFormat = STANDARD_ATTESTATION_FORMAT;
const encoder = new TextEncoder();
const clientData = encoder.encode(
JSON.stringify({
type: "webauthn.create",
challenge: params.challenge,
origin: params.origin,
crossOrigin: false,
})
);
const keyPair = await crypto.subtle.generateKey(
{
name: "ECDSA",
namedCurve: "P-256",
},
true,
KeyUsages
);
const credentialId = await this.saveCredential({
keyType: "ECDSA",
keyCurve: "P-256",
keyValue: keyPair.privateKey,
origin: params.origin,
rpId: params.rp.id,
rpName: params.rp.name,
userHandle: Fido2Utils.stringToBuffer(params.user.id),
userName: params.user.displayName,
});
const authData = await generateAuthData({
rpId: params.rp.id,
credentialId,
userPresence: presence,
userVerification: true, // TODO: Change to false
keyPair,
});
const asn1Der_signature = await generateSignature({
authData,
clientData,
privateKey: keyPair.privateKey,
});
const attestationObject = new Uint8Array(
CBOR.encode({
fmt: attestationFormat,
attStmt:
attestationFormat === "packed"
? {
alg: -7,
sig: asn1Der_signature,
}
: {},
authData,
})
);
clearTimeout(timeout);
return {
credentialId: Fido2Utils.bufferToString(credentialId.raw),
clientDataJSON: Fido2Utils.bufferToString(clientData),
attestationObject: Fido2Utils.bufferToString(attestationObject),
authData: Fido2Utils.bufferToString(authData),
publicKeyAlgorithm: -7,
transports: ["nfc", "usb"],
};
}
async assertCredential(
params: CredentialAssertParams,
abortController = new AbortController()
): Promise<CredentialAssertResult> {
const timeout = setAbortTimeout(abortController, params.userVerification, params.timeout);
let credential: BitCredential | undefined;
if (params.allowedCredentialIds && params.allowedCredentialIds.length > 0) {
// We're looking for regular non-resident keys
credential = await this.getCredential(params.allowedCredentialIds);
if (credential === undefined) {
throw new NoCredentialFoundError();
}
// TODO: Google doesn't work with this. Look into how we're supposed to check this
// if (credential.origin !== params.origin) {
// throw new OriginMismatchError();
// }
await this.fido2UserInterfaceService.confirmCredential(
credential.credentialId.encoded,
abortController
);
} else {
// We're looking for a resident key
const credentials = await this.getCredentialsByRp(params.rpId);
if (credentials.length === 0) {
throw new NoCredentialFoundError();
}
const pickedId = await this.fido2UserInterfaceService.pickCredential(
credentials.map((c) => c.credentialId.encoded),
abortController
);
credential = credentials.find((c) => c.credentialId.encoded === pickedId);
}
const encoder = new TextEncoder();
const clientData = encoder.encode(
JSON.stringify({
type: "webauthn.get",
challenge: params.challenge,
origin: params.origin,
})
);
const authData = await generateAuthData({
credentialId: credential.credentialId,
rpId: params.rpId,
userPresence: true,
userVerification: true, // TODO: Change to false!
});
const signature = await generateSignature({
authData,
clientData,
privateKey: credential.keyValue,
});
clearTimeout(timeout);
return {
credentialId: credential.credentialId.encoded,
clientDataJSON: Fido2Utils.bufferToString(clientData),
authenticatorData: Fido2Utils.bufferToString(authData),
signature: Fido2Utils.bufferToString(signature),
userHandle: Fido2Utils.bufferToString(credential.userHandle),
};
}
private async getCredential(allowedCredentialIds: string[]): Promise<BitCredential | undefined> {
let cipher: Cipher | undefined;
for (const allowedCredential of allowedCredentialIds) {
cipher = await this.cipherService.get(allowedCredential);
if (cipher?.deletedDate != undefined) {
cipher = undefined;
}
if (cipher != undefined) {
break;
}
}
if (cipher == undefined) {
return undefined;
}
const cipherView = await cipher.decrypt();
return await mapCipherViewToBitCredential(cipherView);
}
private async saveCredential(
credential: Omit<BitCredential, "credentialId">
): Promise<CredentialId> {
const pcks8Key = await crypto.subtle.exportKey("pkcs8", credential.keyValue);
const view = new CipherView();
view.type = CipherType.Fido2Key;
view.name = credential.rpName;
view.fido2Key = new Fido2KeyView();
view.fido2Key.origin = credential.origin;
view.fido2Key.keyType = credential.keyType;
view.fido2Key.keyCurve = credential.keyCurve;
view.fido2Key.keyValue = Fido2Utils.bufferToString(pcks8Key);
view.fido2Key.rpId = credential.rpId;
view.fido2Key.rpName = credential.rpName;
view.fido2Key.userHandle = Fido2Utils.bufferToString(credential.userHandle);
view.fido2Key.userName = credential.userName;
view.fido2Key.origin = credential.origin;
const cipher = await this.cipherService.encrypt(view);
await this.cipherService.createWithServer(cipher);
// TODO: Cipher service modifies supplied object, we might wanna change that.
return new CredentialId(cipher.id);
}
private async getCredentialsByRp(rpId: string): Promise<BitCredential[]> {
const allCipherViews = await this.cipherService.getAllDecrypted();
const cipherViews = allCipherViews.filter(
(cv) => !cv.isDeleted && cv.type === CipherType.Fido2Key && cv.fido2Key?.rpId === rpId
);
return await Promise.all(cipherViews.map((view) => mapCipherViewToBitCredential(view)));
}
}
interface AuthDataParams {
rpId: string;
credentialId: CredentialId;
userPresence: boolean;
userVerification: boolean;
keyPair?: CryptoKeyPair;
}
async function mapCipherViewToBitCredential(cipherView: CipherView): Promise<BitCredential> {
const keyBuffer = Fido2Utils.stringToBuffer(cipherView.fido2Key.keyValue);
const privateKey = await crypto.subtle.importKey(
"pkcs8",
keyBuffer,
{
name: cipherView.fido2Key.keyType,
namedCurve: cipherView.fido2Key.keyCurve,
},
true,
KeyUsages
);
return {
credentialId: new CredentialId(cipherView.id),
keyType: cipherView.fido2Key.keyType,
keyCurve: cipherView.fido2Key.keyCurve,
keyValue: privateKey,
rpId: cipherView.fido2Key.rpId,
rpName: cipherView.fido2Key.rpName,
userHandle: Fido2Utils.stringToBuffer(cipherView.fido2Key.userHandle),
userName: cipherView.fido2Key.userName,
origin: cipherView.fido2Key.origin,
};
}
async function generateAuthData(params: AuthDataParams) {
const encoder = new TextEncoder();
const authData: Array<number> = [];
const rpIdHash = new Uint8Array(
await crypto.subtle.digest({ name: "SHA-256" }, encoder.encode(params.rpId))
);
authData.push(...rpIdHash);
const flags = authDataFlags({
extensionData: false,
attestationData: params.keyPair !== undefined,
userVerification: params.userVerification,
userPresence: params.userPresence,
});
authData.push(flags);
// add 4 bytes of counter - we use time in epoch seconds as monotonic counter
// TODO: Consider changing this to a cryptographically safe random number
const now = new Date().getTime() / 1000;
authData.push(
((now & 0xff000000) >> 24) & 0xff,
((now & 0x00ff0000) >> 16) & 0xff,
((now & 0x0000ff00) >> 8) & 0xff,
now & 0x000000ff
);
// attestedCredentialData
const attestedCredentialData: Array<number> = [];
// Use 0 because we're self-signing at the moment
const aaguid = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
attestedCredentialData.push(...aaguid);
// credentialIdLength (2 bytes) and credential Id
const rawId = params.credentialId.raw;
const credentialIdLength = [(rawId.length - (rawId.length & 0xff)) / 256, rawId.length & 0xff];
attestedCredentialData.push(...credentialIdLength);
attestedCredentialData.push(...rawId);
if (params.keyPair) {
const publicKeyJwk = await crypto.subtle.exportKey("jwk", params.keyPair.publicKey);
// COSE format of the EC256 key
const keyX = Utils.fromUrlB64ToArray(publicKeyJwk.x);
const keyY = Utils.fromUrlB64ToArray(publicKeyJwk.y);
// const credPublicKeyCOSE = {
// "1": 2, // kty
// "3": -7, // alg
// "-1": 1, // crv
// "-2": keyX,
// "-3": keyY,
// };
// const coseBytes = new Uint8Array(cbor.encode(credPublicKeyCOSE));
// Can't get `cbor-redux` to encode in CTAP2 canonical CBOR. So we do it manually:
const coseBytes = new Uint8Array(77);
coseBytes.set([0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20], 0);
coseBytes.set(keyX, 10);
coseBytes.set([0x22, 0x58, 0x20], 10 + 32);
coseBytes.set(keyY, 10 + 32 + 3);
// credential public key - convert to array from CBOR encoded COSE key
attestedCredentialData.push(...coseBytes);
authData.push(...attestedCredentialData);
}
return new Uint8Array(authData);
}
interface SignatureParams {
authData: Uint8Array;
clientData: Uint8Array;
privateKey: CryptoKey;
}
async function generateSignature(params: SignatureParams) {
const clientDataHash = await crypto.subtle.digest({ name: "SHA-256" }, params.clientData);
const sigBase = new Uint8Array([...params.authData, ...new Uint8Array(clientDataHash)]);
const p1336_signature = new Uint8Array(
await window.crypto.subtle.sign(
{
name: "ECDSA",
hash: { name: "SHA-256" },
},
params.privateKey,
sigBase
)
);
const asn1Der_signature = joseToDer(p1336_signature, "ES256");
return asn1Der_signature;
}
interface Flags {
extensionData: boolean;
attestationData: boolean;
userVerification: boolean;
userPresence: boolean;
}
function authDataFlags(options: Flags): number {
let flags = 0;
if (options.extensionData) {
flags |= 0b1000000;
}
if (options.attestationData) {
flags |= 0b01000000;
}
if (options.userVerification) {
flags |= 0b00000100;
}
if (options.userPresence) {
flags |= 0b00000001;
}
return flags;
}
function setAbortTimeout(
abortController: AbortController,
userVerification: UserVerification,
timeout?: number
): number {
let clampedTimeout: number;
if (userVerification === "discouraged") {
timeout = timeout ?? TIMEOUTS.NO_VERIFICATION.DEFAULT;
clampedTimeout = Math.max(
TIMEOUTS.NO_VERIFICATION.MIN,
Math.min(timeout, TIMEOUTS.NO_VERIFICATION.MAX)
);
} else {
timeout = timeout ?? TIMEOUTS.WITH_VERIFICATION.DEFAULT;
clampedTimeout = Math.max(
TIMEOUTS.WITH_VERIFICATION.MIN,
Math.min(timeout, TIMEOUTS.WITH_VERIFICATION.MAX)
);
}
return window.setTimeout(() => abortController.abort(), clampedTimeout);
}
/**
*
* REMOVE BEFORE MERGE
*
* This is the old version of our FIDO2 client which was built according to spec.
* It left here for reference purposes until we no longer need it.
*
*/
// import { CBOR } from "cbor-redux";
// import { Utils } from "../../misc/utils";
// import { CipherService } from "../../vault/abstractions/cipher.service";
// import { CipherType } from "../../vault/enums/cipher-type";
// import { Cipher } from "../../vault/models/domain/cipher";
// import { CipherView } from "../../vault/models/view/cipher.view";
// import { Fido2UserInterfaceService } from "../abstractions/fido2-user-interface.service.abstraction";
// import { Fido2Utils } from "../abstractions/fido2-utils";
// import {
// CredentialAssertParams,
// CredentialAssertResult,
// CredentialRegistrationParams,
// CredentialRegistrationResult,
// Fido2Service as Fido2ServiceAbstraction,
// NoCredentialFoundError,
// UserVerification,
// } from "../abstractions/fido2.service.abstraction";
// import { Fido2KeyView } from "../models/view/fido2-key.view";
// import { CredentialId } from "./credential-id";
// import { joseToDer } from "./ecdsa-utils";
// // We support self-signing, but Google won't accept it.
// // TODO: Look into supporting self-signed packed format.
// const STANDARD_ATTESTATION_FORMAT: "none" | "packed" = "none";
// const TIMEOUTS = {
// NO_VERIFICATION: {
// DEFAULT: 120000,
// MIN: 30000,
// MAX: 180000,
// },
// WITH_VERIFICATION: {
// DEFAULT: 300000,
// MIN: 30000,
// MAX: 600000,
// },
// };
// interface BitCredential {
// credentialId: CredentialId;
// keyType: "ECDSA";
// keyCurve: "P-256";
// keyValue: CryptoKey;
// rpId: string;
// rpName: string;
// userHandle: Uint8Array;
// userName: string;
// origin: string;
// }
// const KeyUsages: KeyUsage[] = ["sign"];
// export class Fido2Service implements Fido2ServiceAbstraction {
// constructor(
// private fido2UserInterfaceService: Fido2UserInterfaceService,
// private cipherService: CipherService
// ) {}
// async createCredential(
// params: CredentialRegistrationParams,
// abortController = new AbortController()
// ): Promise<CredentialRegistrationResult> {
// // Comment: Timeouts could potentially be implemented using decorators.
// // But since I try to use decorators a little as possible and only
// // for the most generic solutions, I'm gonne leave this as is untill peer review.
// const timeout = setAbortTimeout(
// abortController,
// params.authenticatorSelection.userVerification,
// params.timeout
// );
// const presence = await this.fido2UserInterfaceService.confirmNewCredential(
// {
// credentialName: params.rp.name,
// userName: params.user.displayName,
// },
// abortController
// );
// const attestationFormat = STANDARD_ATTESTATION_FORMAT;
// const encoder = new TextEncoder();
// const clientData = encoder.encode(
// JSON.stringify({
// type: "webauthn.create",
// challenge: params.challenge,
// origin: params.origin,
// crossOrigin: false,
// })
// );
// const keyPair = await crypto.subtle.generateKey(
// {
// name: "ECDSA",
// namedCurve: "P-256",
// },
// true,
// KeyUsages
// );
// const credentialId = await this.saveCredential({
// keyType: "ECDSA",
// keyCurve: "P-256",
// keyValue: keyPair.privateKey,
// origin: params.origin,
// rpId: params.rp.id,
// rpName: params.rp.name,
// userHandle: Fido2Utils.stringToBuffer(params.user.id),
// userName: params.user.displayName,
// });
// const authData = await generateAuthData({
// rpId: params.rp.id,
// credentialId,
// userPresence: presence,
// userVerification: true, // TODO: Change to false
// keyPair,
// });
// const asn1Der_signature = await generateSignature({
// authData,
// clientData,
// privateKey: keyPair.privateKey,
// });
// const attestationObject = new Uint8Array(
// CBOR.encode({
// fmt: attestationFormat,
// attStmt:
// attestationFormat === "packed"
// ? {
// alg: -7,
// sig: asn1Der_signature,
// }
// : {},
// authData,
// })
// );
// clearTimeout(timeout);
// return {
// credentialId: Fido2Utils.bufferToString(credentialId.raw),
// clientDataJSON: Fido2Utils.bufferToString(clientData),
// attestationObject: Fido2Utils.bufferToString(attestationObject),
// authData: Fido2Utils.bufferToString(authData),
// publicKeyAlgorithm: -7,
// transports: ["nfc", "usb"],
// };
// }
// async assertCredential(
// params: CredentialAssertParams,
// abortController = new AbortController()
// ): Promise<CredentialAssertResult> {
// const timeout = setAbortTimeout(abortController, params.userVerification, params.timeout);
// let credential: BitCredential | undefined;
// if (params.allowedCredentialIds && params.allowedCredentialIds.length > 0) {
// // We're looking for regular non-resident keys
// credential = await this.getCredential(params.allowedCredentialIds);
// if (credential === undefined) {
// throw new NoCredentialFoundError();
// }
// // TODO: Google doesn't work with this. Look into how we're supposed to check this
// // if (credential.origin !== params.origin) {
// // throw new OriginMismatchError();
// // }
// await this.fido2UserInterfaceService.confirmCredential(
// credential.credentialId.encoded,
// abortController
// );
// } else {
// // We're looking for a resident key
// const credentials = await this.getCredentialsByRp(params.rpId);
// if (credentials.length === 0) {
// throw new NoCredentialFoundError();
// }
// const pickedId = await this.fido2UserInterfaceService.pickCredential(
// credentials.map((c) => c.credentialId.encoded),
// abortController
// );
// credential = credentials.find((c) => c.credentialId.encoded === pickedId);
// }
// const encoder = new TextEncoder();
// const clientData = encoder.encode(
// JSON.stringify({
// type: "webauthn.get",
// challenge: params.challenge,
// origin: params.origin,
// })
// );
// const authData = await generateAuthData({
// credentialId: credential.credentialId,
// rpId: params.rpId,
// userPresence: true,
// userVerification: true, // TODO: Change to false!
// });
// const signature = await generateSignature({
// authData,
// clientData,
// privateKey: credential.keyValue,
// });
// clearTimeout(timeout);
// return {
// credentialId: credential.credentialId.encoded,
// clientDataJSON: Fido2Utils.bufferToString(clientData),
// authenticatorData: Fido2Utils.bufferToString(authData),
// signature: Fido2Utils.bufferToString(signature),
// userHandle: Fido2Utils.bufferToString(credential.userHandle),
// };
// }
// private async getCredential(allowedCredentialIds: string[]): Promise<BitCredential | undefined> {
// let cipher: Cipher | undefined;
// for (const allowedCredential of allowedCredentialIds) {
// cipher = await this.cipherService.get(allowedCredential);
// if (cipher?.deletedDate != undefined) {
// cipher = undefined;
// }
// if (cipher != undefined) {
// break;
// }
// }
// if (cipher == undefined) {
// return undefined;
// }
// const cipherView = await cipher.decrypt();
// return await mapCipherViewToBitCredential(cipherView);
// }
// private async saveCredential(
// credential: Omit<BitCredential, "credentialId">
// ): Promise<CredentialId> {
// const pcks8Key = await crypto.subtle.exportKey("pkcs8", credential.keyValue);
// const view = new CipherView();
// view.type = CipherType.Fido2Key;
// view.name = credential.rpName;
// view.fido2Key = new Fido2KeyView();
// view.fido2Key.origin = credential.origin;
// view.fido2Key.keyType = credential.keyType;
// view.fido2Key.keyCurve = credential.keyCurve;
// view.fido2Key.keyValue = Fido2Utils.bufferToString(pcks8Key);
// view.fido2Key.rpId = credential.rpId;
// view.fido2Key.rpName = credential.rpName;
// view.fido2Key.userHandle = Fido2Utils.bufferToString(credential.userHandle);
// view.fido2Key.userName = credential.userName;
// view.fido2Key.origin = credential.origin;
// const cipher = await this.cipherService.encrypt(view);
// await this.cipherService.createWithServer(cipher);
// // TODO: Cipher service modifies supplied object, we might wanna change that.
// return new CredentialId(cipher.id);
// }
// private async getCredentialsByRp(rpId: string): Promise<BitCredential[]> {
// const allCipherViews = await this.cipherService.getAllDecrypted();
// const cipherViews = allCipherViews.filter(
// (cv) => !cv.isDeleted && cv.type === CipherType.Fido2Key && cv.fido2Key?.rpId === rpId
// );
// return await Promise.all(cipherViews.map((view) => mapCipherViewToBitCredential(view)));
// }
// }
// interface AuthDataParams {
// rpId: string;
// credentialId: CredentialId;
// userPresence: boolean;
// userVerification: boolean;
// keyPair?: CryptoKeyPair;
// }
// async function mapCipherViewToBitCredential(cipherView: CipherView): Promise<BitCredential> {
// const keyBuffer = Fido2Utils.stringToBuffer(cipherView.fido2Key.keyValue);
// const privateKey = await crypto.subtle.importKey(
// "pkcs8",
// keyBuffer,
// {
// name: cipherView.fido2Key.keyType,
// namedCurve: cipherView.fido2Key.keyCurve,
// },
// true,
// KeyUsages
// );
// return {
// credentialId: new CredentialId(cipherView.id),
// keyType: cipherView.fido2Key.keyType,
// keyCurve: cipherView.fido2Key.keyCurve,
// keyValue: privateKey,
// rpId: cipherView.fido2Key.rpId,
// rpName: cipherView.fido2Key.rpName,
// userHandle: Fido2Utils.stringToBuffer(cipherView.fido2Key.userHandle),
// userName: cipherView.fido2Key.userName,
// origin: cipherView.fido2Key.origin,
// };
// }
// async function generateAuthData(params: AuthDataParams) {
// const encoder = new TextEncoder();
// const authData: Array<number> = [];
// const rpIdHash = new Uint8Array(
// await crypto.subtle.digest({ name: "SHA-256" }, encoder.encode(params.rpId))
// );
// authData.push(...rpIdHash);
// const flags = authDataFlags({
// extensionData: false,
// attestationData: params.keyPair !== undefined,
// userVerification: params.userVerification,
// userPresence: params.userPresence,
// });
// authData.push(flags);
// // add 4 bytes of counter - we use time in epoch seconds as monotonic counter
// // TODO: Consider changing this to a cryptographically safe random number
// const now = new Date().getTime() / 1000;
// authData.push(
// ((now & 0xff000000) >> 24) & 0xff,
// ((now & 0x00ff0000) >> 16) & 0xff,
// ((now & 0x0000ff00) >> 8) & 0xff,
// now & 0x000000ff
// );
// // attestedCredentialData
// const attestedCredentialData: Array<number> = [];
// // Use 0 because we're self-signing at the moment
// const aaguid = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
// attestedCredentialData.push(...aaguid);
// // credentialIdLength (2 bytes) and credential Id
// const rawId = params.credentialId.raw;
// const credentialIdLength = [(rawId.length - (rawId.length & 0xff)) / 256, rawId.length & 0xff];
// attestedCredentialData.push(...credentialIdLength);
// attestedCredentialData.push(...rawId);
// if (params.keyPair) {
// const publicKeyJwk = await crypto.subtle.exportKey("jwk", params.keyPair.publicKey);
// // COSE format of the EC256 key
// const keyX = Utils.fromUrlB64ToArray(publicKeyJwk.x);
// const keyY = Utils.fromUrlB64ToArray(publicKeyJwk.y);
// // const credPublicKeyCOSE = {
// // "1": 2, // kty
// // "3": -7, // alg
// // "-1": 1, // crv
// // "-2": keyX,
// // "-3": keyY,
// // };
// // const coseBytes = new Uint8Array(cbor.encode(credPublicKeyCOSE));
// // Can't get `cbor-redux` to encode in CTAP2 canonical CBOR. So we do it manually:
// const coseBytes = new Uint8Array(77);
// coseBytes.set([0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20], 0);
// coseBytes.set(keyX, 10);
// coseBytes.set([0x22, 0x58, 0x20], 10 + 32);
// coseBytes.set(keyY, 10 + 32 + 3);
// // credential public key - convert to array from CBOR encoded COSE key
// attestedCredentialData.push(...coseBytes);
// authData.push(...attestedCredentialData);
// }
// return new Uint8Array(authData);
// }
// interface SignatureParams {
// authData: Uint8Array;
// clientData: Uint8Array;
// privateKey: CryptoKey;
// }
// async function generateSignature(params: SignatureParams) {
// const clientDataHash = await crypto.subtle.digest({ name: "SHA-256" }, params.clientData);
// const sigBase = new Uint8Array([...params.authData, ...new Uint8Array(clientDataHash)]);
// const p1336_signature = new Uint8Array(
// await window.crypto.subtle.sign(
// {
// name: "ECDSA",
// hash: { name: "SHA-256" },
// },
// params.privateKey,
// sigBase
// )
// );
// const asn1Der_signature = joseToDer(p1336_signature, "ES256");
// return asn1Der_signature;
// }
// interface Flags {
// extensionData: boolean;
// attestationData: boolean;
// userVerification: boolean;
// userPresence: boolean;
// }
// function authDataFlags(options: Flags): number {
// let flags = 0;
// if (options.extensionData) {
// flags |= 0b1000000;
// }
// if (options.attestationData) {
// flags |= 0b01000000;
// }
// if (options.userVerification) {
// flags |= 0b00000100;
// }
// if (options.userPresence) {
// flags |= 0b00000001;
// }
// return flags;
// }
// function setAbortTimeout(
// abortController: AbortController,
// userVerification: UserVerification,
// timeout?: number
// ): number {
// let clampedTimeout: number;
// if (userVerification === "discouraged") {
// timeout = timeout ?? TIMEOUTS.NO_VERIFICATION.DEFAULT;
// clampedTimeout = Math.max(
// TIMEOUTS.NO_VERIFICATION.MIN,
// Math.min(timeout, TIMEOUTS.NO_VERIFICATION.MAX)
// );
// } else {
// timeout = timeout ?? TIMEOUTS.WITH_VERIFICATION.DEFAULT;
// clampedTimeout = Math.max(
// TIMEOUTS.WITH_VERIFICATION.MIN,
// Math.min(timeout, TIMEOUTS.WITH_VERIFICATION.MAX)
// );
// }
// return window.setTimeout(() => abortController.abort(), clampedTimeout);
// }