1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-22 11:13:46 +00:00

[EC-598] feat: complete implementation of makeCredential

This commit is contained in:
Andreas Coroiu
2023-03-27 13:38:21 +02:00
parent 343df7efdb
commit f31bd3eca9
4 changed files with 295 additions and 10 deletions

View File

@@ -1,3 +1,6 @@
import { CBOR } from "cbor-redux";
import { Utils } from "../../misc/utils";
import { CipherType } from "../../vault/enums/cipher-type";
import { CipherView } from "../../vault/models/view/cipher.view";
import { CipherService } from "../../vault/services/cipher.service";
@@ -12,6 +15,11 @@ import { Fido2UserInterfaceService } from "../abstractions/fido2-user-interface.
import { Fido2Utils } from "../abstractions/fido2-utils";
import { Fido2KeyView } from "../models/view/fido2-key.view";
// AAGUID: 6e8248d5-b479-40db-a3d8-11116f7e8349
export const AAGUID = new Uint8Array([
0xd5, 0x48, 0x82, 0x6e, 0x79, 0xb4, 0xdb, 0x40, 0xa3, 0xd8, 0x11, 0x11, 0x6f, 0x7e, 0x83, 0x49,
]);
const KeyUsages: KeyUsage[] = ["sign"];
/**
@@ -24,7 +32,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
private userInterface: Fido2UserInterfaceService
) {}
async makeCredential(params: Fido2AuthenticatorMakeCredentialsParams): Promise<void> {
async makeCredential(params: Fido2AuthenticatorMakeCredentialsParams): Promise<Uint8Array> {
if (params.credTypesAndPubKeyAlgs.every((p) => p.alg !== Fido2AlgorithmIdentifier.ES256)) {
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotSupported);
}
@@ -60,6 +68,8 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
}
let cipher: CipherView;
let keyPair: CryptoKeyPair;
if (params.requireResidentKey) {
const userVerification = await this.userInterface.confirmNewCredential({
credentialName: params.rpEntity.name,
@@ -71,14 +81,15 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
}
try {
const keyPair = await this.createKeyPair();
keyPair = await this.createKeyPair();
const cipher = new CipherView();
cipher = new CipherView();
cipher.type = CipherType.Fido2Key;
cipher.name = params.rpEntity.name;
cipher.fido2Key = await this.createKeyView(params, keyPair.privateKey);
const encrypted = await this.cipherService.encrypt(cipher);
await this.cipherService.createWithServer(encrypted);
await this.cipherService.createWithServer(encrypted); // encrypted.id is assigned inside here
cipher.id = encrypted.id;
} catch {
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown);
}
@@ -93,10 +104,10 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
}
try {
const keyPair = await this.createKeyPair();
keyPair = await this.createKeyPair();
const encrypted = await this.cipherService.get(cipherId);
const cipher = await encrypted.decrypt();
cipher = await encrypted.decrypt();
cipher.fido2Key = await this.createKeyView(params, keyPair.privateKey);
const reencrypted = await this.cipherService.encrypt(cipher);
await this.cipherService.updateWithServer(reencrypted);
@@ -104,6 +115,23 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown);
}
}
const attestationObject = new Uint8Array(
CBOR.encode({
fmt: "none",
attStmt: {},
authData: await generateAuthData({
rpId: params.rpEntity.id,
credentialId: cipher.id,
counter: cipher.fido2Key.counter,
userPresence: true,
userVerification: false,
keyPair,
}),
})
);
return attestationObject;
}
private async vaultContainsId(ids: string[]): Promise<boolean> {
@@ -147,3 +175,100 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
return fido2Key;
}
}
interface AuthDataParams {
rpId: string;
credentialId: string;
userPresence: boolean;
userVerification: boolean;
counter: number;
keyPair?: CryptoKeyPair;
}
async function generateAuthData(params: AuthDataParams) {
const authData: Array<number> = [];
const rpIdHash = new Uint8Array(
await crypto.subtle.digest({ name: "SHA-256" }, Utils.fromByteStringToArray(params.rpId))
);
authData.push(...rpIdHash);
const flags = authDataFlags({
extensionData: false,
attestationData: false,
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 counter = params.counter;
authData.push(
((counter & 0xff000000) >> 24) & 0xff,
((counter & 0x00ff0000) >> 16) & 0xff,
((counter & 0x0000ff00) >> 8) & 0xff,
counter & 0x000000ff
);
// attestedCredentialData
const attestedCredentialData: Array<number> = [];
attestedCredentialData.push(...AAGUID);
// credentialIdLength (2 bytes) and credential Id
const rawId = Utils.guidToRawFormat(params.credentialId);
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);
// 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 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;
}