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
};