UNPKG

fido2-lib

Version:

A library for performing FIDO 2.0 / WebAuthn functionality

400 lines (328 loc) 10.8 kB
import { ab2str, coerceToArrayBuffer, coerceToBase64Url, tools } from "./utils.js"; import { PublicKey } from "./keyUtils.js"; import { Fido2Lib } from "./main.js"; // NOTE: throws if origin is https and has port 443 // use `new URL(originstr).origin` to create a properly formatted origin function parseExpectations(exp) { if (typeof exp !== "object") { throw new TypeError( "expected 'expectations' to be of type object, got " + typeof exp, ); } const ret = new Map(); // origin if (exp.origin) { if (typeof exp.origin !== "string") { throw new TypeError( "expected 'origin' should be string, got " + typeof exp.origin, ); } const origin = tools.checkOrigin(exp.origin); ret.set("origin", origin); } // rpId if (exp.rpId) { if (typeof exp.rpId !== "string") { throw new TypeError( "expected 'rpId' should be string, got " + typeof exp.rpId, ); } const rpId = tools.checkRpId(exp.rpId); ret.set("rpId", rpId); } // challenge if (exp.challenge) { let challenge = exp.challenge; challenge = coerceToBase64Url(challenge, "expected challenge"); ret.set("challenge", challenge); } // flags if (exp.flags) { let flags = exp.flags; if (Array.isArray(flags)) { flags = new Set(flags); } if (!(flags instanceof Set)) { throw new TypeError( "expected flags to be an Array or a Set, got: " + typeof flags, ); } ret.set("flags", flags); } // counter if (exp.prevCounter !== undefined) { if (typeof exp.prevCounter !== "number") { throw new TypeError( "expected 'prevCounter' should be Number, got " + typeof exp.prevCounter, ); } ret.set("prevCounter", exp.prevCounter); } // publicKey if (exp.publicKey) { if (typeof exp.publicKey !== "string") { throw new TypeError( "expected 'publicKey' should be String, got " + typeof exp.publicKey, ); } ret.set("publicKey", exp.publicKey); } // userHandle if (exp.userHandle !== undefined) { let userHandle = exp.userHandle; if (userHandle !== null && userHandle !== "") { userHandle = coerceToBase64Url(userHandle, "userHandle"); } ret.set("userHandle", userHandle); } // allowCredentials if (exp.allowCredentials !== undefined) { const allowCredentials = exp.allowCredentials; if (allowCredentials !== null && !Array.isArray(allowCredentials)) { throw new TypeError( "expected 'allowCredentials' to be null or array, got " + typeof allowCredentials, ); } for (const index in allowCredentials) { if (allowCredentials[index].id != null) { allowCredentials[index].id = coerceToArrayBuffer( allowCredentials[index].id, "allowCredentials[" + index + "].id", ); } } ret.set("allowCredentials", allowCredentials); } return ret; } /** * Parses the clientData JSON byte stream into an Object * @param {ArrayBuffer} clientDataJSON The ArrayBuffer containing the properly formatted JSON of the clientData object * @return {Object} The parsed clientData object */ function parseClientResponse(msg) { if (typeof msg !== "object") { throw new TypeError("expected msg to be Object"); } if (msg.id && !msg.rawId) { msg.rawId = msg.id; } const rawId = coerceToArrayBuffer(msg.rawId, "rawId"); if (typeof msg.response !== "object") { throw new TypeError("expected response to be Object"); } const clientDataJSON = coerceToArrayBuffer( msg.response.clientDataJSON, "clientDataJSON", ); if (!(clientDataJSON instanceof ArrayBuffer)) { throw new TypeError("expected 'clientDataJSON' to be ArrayBuffer"); } // convert to string const clientDataJson = ab2str(clientDataJSON); // parse JSON string let parsed; try { parsed = JSON.parse(clientDataJson); } catch (err) { throw new Error("couldn't parse clientDataJson: " + err); } const ret = new Map([ ["challenge", parsed.challenge], ["origin", parsed.origin], ["type", parsed.type], ["tokenBinding", parsed.tokenBinding], ["rawClientDataJson", clientDataJSON], ["rawId", rawId], ]); return ret; } /** * @deprecated * Parses the CBOR attestation statement * @param {ArrayBuffer} attestationObject The CBOR byte array representing the attestation statement * @return {Object} The Object containing all the attestation information * @see https://w3c.github.io/webauthn/#generating-an-attestation-object * @see https://w3c.github.io/webauthn/#defined-attestation-formats */ async function parseAttestationObject(attestationObject) { // update docs to say ArrayBuffer-ish object attestationObject = coerceToArrayBuffer( attestationObject, "attestationObject", ); // parse attestation let parsed; try { parsed = tools.cbor.decode(new Uint8Array(attestationObject)); } catch (_err) { throw new TypeError("couldn't parse attestationObject CBOR"); } if (typeof parsed !== "object") { throw new TypeError("invalid parsing of attestationObject cbor"); } if (typeof parsed.fmt !== "string") { throw new Error("expected attestation CBOR to contain a 'fmt' string"); } if (typeof parsed.attStmt !== "object") { throw new Error( "expected attestation CBOR to contain a 'attStmt' object", ); } if (!(parsed.authData instanceof Uint8Array)) { throw new Error( "expected attestation CBOR to contain a 'authData' byte sequence", ); } const ret = new Map([ ...Fido2Lib.parseAttestation(parsed.fmt, parsed.attStmt), // return raw buffer for future signature verification ["rawAuthnrData", coerceToArrayBuffer(parsed.authData, "authData")], // Added for compatibility with parseAuthnrAttestationResponse ["transports", undefined], // parse authData ...await parseAuthenticatorData(parsed.authData), ]); return ret; } async function parseAuthnrAttestationResponse(msg) { if (typeof msg !== "object") { throw new TypeError("expected msg to be Object"); } if (typeof msg.response !== "object") { throw new TypeError("expected response to be Object"); } let attestationObject = msg.response.attestationObject; // update docs to say ArrayBuffer-ish object attestationObject = coerceToArrayBuffer( attestationObject, "attestationObject", ); let parsed; try { parsed = tools.cbor.decode(new Uint8Array(attestationObject)); } catch (_err) { throw new TypeError("couldn't parse attestationObject CBOR"); } if (typeof parsed !== "object") { throw new TypeError("invalid parsing of attestationObject CBOR"); } if (typeof parsed.fmt !== "string") { throw new Error("expected attestation CBOR to contain a 'fmt' string"); } if (typeof parsed.attStmt !== "object") { throw new Error("expected attestation CBOR to contain a 'attStmt' object"); } if (!(parsed.authData instanceof Uint8Array)) { throw new Error("expected attestation CBOR to contain a 'authData' byte sequence"); } if (msg.transports != undefined && !Array.isArray(msg.transports)) { throw new Error("expected transports to be 'null' or 'array<string>'"); } // have to require here to prevent circular dependency const ret = new Map([ ...Fido2Lib.parseAttestation(parsed.fmt, parsed.attStmt), // return raw buffer for future signature verification ["rawAuthnrData", coerceToArrayBuffer(parsed.authData, "authData")], ["transports", msg.transports], // parse authData ...await parseAuthenticatorData(parsed.authData), ]); return ret; } async function parseAuthenticatorData(authnrDataArrayBuffer) { // convert to ArrayBuffer authnrDataArrayBuffer = coerceToArrayBuffer(authnrDataArrayBuffer, "authnrDataArrayBuffer"); const ret = new Map(); // console.log("authnrDataArrayBuffer", authnrDataArrayBuffer); // console.log("typeof authnrDataArrayBuffer", typeof authnrDataArrayBuffer); // printHex("authnrDataArrayBuffer", authnrDataArrayBuffer); const authnrDataBuf = new DataView(authnrDataArrayBuffer); let offset = 0; ret.set("rpIdHash", authnrDataBuf.buffer.slice(offset, offset + 32)); offset += 32; const flags = authnrDataBuf.getUint8(offset); const flagsSet = new Set(); ret.set("flags", flagsSet); if (flags & 0x01) flagsSet.add("UP"); if (flags & 0x02) flagsSet.add("RFU1"); if (flags & 0x04) flagsSet.add("UV"); if (flags & 0x08) flagsSet.add("RFU3"); if (flags & 0x10) flagsSet.add("RFU4"); if (flags & 0x20) flagsSet.add("RFU5"); if (flags & 0x40) flagsSet.add("AT"); if (flags & 0x80) flagsSet.add("ED"); offset++; ret.set("counter", authnrDataBuf.getUint32(offset, false)); offset += 4; // see if there's more data to process const attestation = flagsSet.has("AT"); const extensions = flagsSet.has("ED"); if (attestation) { ret.set("aaguid", authnrDataBuf.buffer.slice(offset, offset + 16)); offset += 16; const credIdLen = authnrDataBuf.getUint16(offset, false); ret.set("credIdLen", credIdLen); offset += 2; ret.set( "credId", authnrDataBuf.buffer.slice(offset, offset + credIdLen), ); offset += credIdLen; // Import public key const publicKey = new PublicKey(); await publicKey.fromCose( authnrDataBuf.buffer.slice(offset, authnrDataBuf.buffer.byteLength), ); // TODO: does not only contain the COSE if the buffer contains extensions ret.set("credentialPublicKeyCose", await publicKey.toCose()); ret.set("credentialPublicKeyJwk", await publicKey.toJwk()); ret.set("credentialPublicKeyPem", await publicKey.toPem()); } if (extensions) { const cborObjects = tools.cbor.decodeMultiple(new Uint8Array(authnrDataBuf.buffer.slice(offset, authnrDataBuf.buffer.byteLength))); // skip publicKey if present if (attestation) { cborObjects.shift(); } if (cborObjects.length === 0) { throw new Error("extensions missing"); } ret.set("webAuthnExtensions", cborObjects); } return ret; } async function parseAuthnrAssertionResponse(msg) { if (typeof msg !== "object") { throw new TypeError("expected msg to be Object"); } if (typeof msg.response !== "object") { throw new TypeError("expected response to be Object"); } let userHandle; if (msg.response.userHandle !== undefined && msg.response.userHandle !== null) { userHandle = coerceToArrayBuffer(msg.response.userHandle, "response.userHandle"); if (userHandle.byteLength === 0) { userHandle = undefined; } } const sigAb = coerceToArrayBuffer(msg.response.signature, "response.signature"); const ret = new Map([ ["sig", sigAb], ["userHandle", userHandle], ["rawAuthnrData", coerceToArrayBuffer(msg.response.authenticatorData, "response.authenticatorData")], ...await parseAuthenticatorData(msg.response.authenticatorData), ]); return ret; } export { parseAttestationObject, parseAuthenticatorData, parseAuthnrAssertionResponse, parseAuthnrAttestationResponse, parseClientResponse, parseExpectations };