/** * LibHaLo - Programmatically interact with HaLo tags from the web browser, mobile application or the desktop. * Copyright by Arx Research, Inc., a Delaware corporation * License: MIT */ import {readNDEF} from "./read_ndef.js"; import {HaloLogicError, NFCOperationError} from "../halo/exceptions.js"; import {execHaloCmd, wrapCommandForU2F, unwrapResultFromU2F, checkHaloTagError} from "./common.js"; import { ExecHaloCmdOptions, ExecOptions, HaloCommandObject, Reader } from "../types.js"; import {Buffer} from 'buffer/index.js'; import {ISO7816_SELECT_CMDS} from "../aid.js"; import {unlockHW} from "./unlock_hw.js"; async function selectCore(reader: Reader) { let res; for (const aid of ISO7816_SELECT_CMDS) { try { res = await reader.transmit(Buffer.from(aid, "hex"), 255); } catch (e) { throw new NFCOperationError(( e).message); } if (res.slice(-2).compare(Buffer.from([0x90, 0x00])) === 0) { return; } } throw new HaloLogicError("Unable to select HaLo core."); } async function selectU2FLayer(reader: Reader) { try { const res = await reader.transmit(Buffer.from("00A4040008A0000006472F0001", "hex"), 255); const statusCheck = res.slice(-2).compare(Buffer.from([0x90, 0x00])) === 0; if (!statusCheck) { throw new HaloLogicError("Unable to select HaLo U2F layer."); } } catch (e) { throw new HaloLogicError("Unable to access HaLo U2F layer. " + "This command must be run with administrative privileges."); } } async function transceive(reader: Reader, command: Buffer, options: ExecOptions) { options = options || {}; const start = performance.now(); let res; try { res = await reader.transmit(command, 255); } catch (e) { throw new NFCOperationError(( e).message); } const end = performance.now(); if (process.env.DEBUG_PCSC === "1") { console.log('=> ' + command.toString('hex')); console.log('<= [' + Math.round(end - start) + ' ms] ' + res.toString('hex')); } const check1 = res.slice(-2).compare(Buffer.from([0x90, 0x00])) !== 0; const check2 = res.slice(-2).compare(Buffer.from([0x91, 0x00])) !== 0; if (!options.noCheck) { if (check1 && check2) { throw new HaloLogicError("Command failed, cmd: " + command.toString('hex') + ", response: " + res.toString('hex')); } return res.slice(0, -2); } else { return res; } } async function getVersion(reader: Reader): Promise { const versionRes = await transceive(reader, Buffer.from("00510000010700", "hex"), {noCheck: true}); if (versionRes.slice(-2).compare(Buffer.from([0x90, 0x00])) !== 0) { // GET_FV_VERSION command not supported, fallback to NDEF const wrappedTransceive = async (payload: Buffer) => await transceive(reader, payload, {noCheck: true}); const url = await readNDEF(wrappedTransceive); if (!url.qs.v) { return '01.C1.000001.00000000'; } else if ((url.qs.v as string).toLowerCase() === 'c2') { return '01.C2.000002.00000000'; } else if ((url.qs.v as string).toLowerCase() === 'c3') { return '01.C3.000003.00000000'; } else { return (url.qs.v as string); } } else { return versionRes.slice(0, -2).toString(); } } async function getAddonVersion(reader: Reader) { const addonVersionRes = await transceive(reader, Buffer.from("00510000011000", "hex"), {noCheck: true}); if (addonVersionRes.slice(-2).compare(Buffer.from([0x90, 0x00])) !== 0) { return null; } return addonVersionRes.slice(0, -2).toString(); } async function execCoreCommand(reader: Reader, command: Buffer, options?: ExecOptions) { options = Object.assign({}, options); let cmdBuf; if (options.pcscExecLayer === "u2f") { await selectU2FLayer(reader); cmdBuf = wrapCommandForU2F(command); } else { cmdBuf = Buffer.concat([ Buffer.from("B0510000", "hex"), Buffer.from([command.length]), command, Buffer.from("00", "hex") ]); } let res = await transceive(reader, cmdBuf, options); if (options.pcscExecLayer === "u2f") { res = unwrapResultFromU2F(res); } checkHaloTagError(res); if (options.pcscExecLayer === "u2f") { await selectCore(reader); } return { result: res.toString('hex'), extra: {} }; } function makeOptions(reader: Reader): ExecHaloCmdOptions { return { method: 'pcsc', exec: async (command, options) => await execCoreCommand(reader, command, options), } } async function execHaloCmdPCSC(command: HaloCommandObject, reader: Reader) { const wrappedTransceive = async (payload: Buffer) => await transceive(reader, payload, {noCheck: true}); await selectCore(reader); const options = makeOptions(reader); command = {...command}; if (command.name === "version") { const version = await getVersion(reader); const addonVersion = await getAddonVersion(reader); return { "core": { "ver": version }, "addons": { "ver": addonVersion } }; } else if (command.name === "read_ndef") { // PCSC-specific NDEF reader command return await readNDEF(wrappedTransceive); } else if (command.name === "unlock_hw") { await selectCore(reader); const cb = (cur: number, max: number) => { console.log(JSON.stringify({"status": "in_progress", "cur": cur, "max": max})); }; return await unlockHW(wrappedTransceive, command.keyNo, cb); } else if (command.name === "full_gen_key") { await selectCore(reader); let rootPkRes = await execHaloCmd({ "name": "gen_key", "keyNo": command.keyNo, "entropy": command.entropy }, options); if (rootPkRes.needsConfirmPK) { rootPkRes = await execHaloCmd({ "name": "gen_key_confirm", "keyNo": command.keyNo, "publicKey": rootPkRes.publicKey }, options); } const subPkRes = await execHaloCmd({ "name": "gen_key_finalize", "keyNo": command.keyNo, "password": command.password }, options); return { generatedPublicKey: {...subPkRes, attestedWith: rootPkRes} }; } else { // divert to the common command execution flow await selectCore(reader); return await execHaloCmd(command, options); } } export {execHaloCmdPCSC};