diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json
index b5d5fea8..ee36b087 100644
--- a/src/core/config/Categories.json
+++ b/src/core/config/Categories.json
@@ -591,8 +591,11 @@
"Card Validation Data Verify",
"DUKPT Derive AES Key",
"DUKPT Derive TDES Key",
+ "EMV Build ARQC Data",
"EMV Generate ARPC",
"EMV Generate ARQC",
+ "EMV Parse ARQC Data",
+ "Parse EMV TLV",
"EMV Generate MAC",
"EMV Generate MAC (PIN Change)",
"EMV Verify ARQC",
diff --git a/src/core/lib/EmvCdol.mjs b/src/core/lib/EmvCdol.mjs
new file mode 100644
index 00000000..e50e06a1
--- /dev/null
+++ b/src/core/lib/EmvCdol.mjs
@@ -0,0 +1,118 @@
+/**
+ * @license Apache-2.0
+ * @author Jacob Marks [https://jacobmarks.com]
+ */
+
+import OperationError from "../errors/OperationError.mjs";
+
+/**
+ * Standard EMVCo CDOL1 field template.
+ *
+ * This 10-field, 33-byte layout covers Visa, Mastercard, Amex, Discover,
+ * JCB, and UnionPay acquirer flows. Network differences (Option A vs
+ * Option B session-key derivation) affect key derivation upstream, not
+ * the structure of the CDOL1 data block itself.
+ */
+const CDOL1_FIELDS = [
+ { tag: "9F02", name: "Amount Authorised", bytes: 6 },
+ { tag: "9F03", name: "Amount Other", bytes: 6 },
+ { tag: "9F1A", name: "Terminal Country Code", bytes: 2 },
+ { tag: "95", name: "TVR", bytes: 5 },
+ { tag: "5F2A", name: "Transaction Currency Code", bytes: 2 },
+ { tag: "9A", name: "Transaction Date", bytes: 3 },
+ { tag: "9C", name: "Transaction Type", bytes: 1 },
+ { tag: "9F37", name: "Unpredictable Number", bytes: 4 },
+ { tag: "82", name: "AIP", bytes: 2 },
+ { tag: "9F36", name: "ATC", bytes: 2 },
+];
+
+const CDOL1_TOTAL_BYTES = CDOL1_FIELDS.reduce((sum, f) => sum + f.bytes, 0); // 33
+
+/**
+ * @param {string} value
+ * @param {string} name
+ * @param {number} bytes
+ * @returns {string} uppercase hex, validated
+ */
+function validateFieldHex(value, name, bytes) {
+ const h = (value || "").replace(/\s+/g, "").toUpperCase();
+ if (!/^[0-9A-F]*$/.test(h))
+ throw new OperationError(`${name}: not valid hex.`);
+ if (h.length !== bytes * 2)
+ throw new OperationError(`${name}: expected ${bytes * 2} hex chars (${bytes} bytes), got ${h.length}.`);
+ return h;
+}
+
+/**
+ * @param {string[]} values — one hex string per CDOL1 field, in template order
+ * @returns {{ tag: string, name: string, bytes: number, value: string }[]}
+ */
+function buildCdol1(values) {
+ if (values.length !== CDOL1_FIELDS.length)
+ throw new OperationError(`Expected ${CDOL1_FIELDS.length} field values, got ${values.length}.`);
+ return CDOL1_FIELDS.map((f, i) => ({
+ ...f,
+ value: validateFieldHex(values[i], f.name, f.bytes),
+ }));
+}
+
+/**
+ * @param {string} hex — flat 66-char (33-byte) CDOL1 preimage
+ * @returns {{ tag: string, name: string, bytes: number, value: string }[]}
+ */
+function parseCdol1(hex) {
+ const h = (hex || "").replace(/\s+/g, "").toUpperCase();
+ if (!h || !/^[0-9A-F]+$/.test(h))
+ throw new OperationError("Input is not valid hex.");
+ if (h.length < CDOL1_TOTAL_BYTES * 2)
+ throw new OperationError(
+ `Standard CDOL1 requires ${CDOL1_TOTAL_BYTES * 2} hex chars (${CDOL1_TOTAL_BYTES} bytes); got ${h.length}.`
+ );
+ let offset = 0;
+ return CDOL1_FIELDS.map(f => {
+ const value = h.substring(offset, offset + f.bytes * 2);
+ offset += f.bytes * 2;
+ return { ...f, value };
+ });
+}
+
+/**
+ * @param {{ value: string }[]} parsed
+ * @returns {string} flat uppercase hex
+ */
+function formatHex(parsed) {
+ return parsed.map(f => f.value).join("");
+}
+
+/**
+ * @param {{ tag: string, name: string, value: string }[]} parsed
+ * @returns {string} pretty-printed JSON
+ */
+function formatJson(parsed) {
+ const obj = {};
+ for (const f of parsed) obj[`${f.name} (${f.tag})`] = f.value;
+ return JSON.stringify(obj, null, 4);
+}
+
+/**
+ * @param {{ tag: string, name: string, bytes: number, value: string }[]} parsed
+ * @returns {string} annotated TLV lines: TAG LEN VALUE [name]
+ */
+function formatAnnotatedTlv(parsed) {
+ return parsed
+ .map(f => {
+ const lenHex = f.bytes.toString(16).padStart(2, "0").toUpperCase();
+ return `${f.tag.padEnd(6)} ${lenHex} ${f.value.padEnd(12)} [${f.name}]`;
+ })
+ .join("\n");
+}
+
+export {
+ CDOL1_FIELDS,
+ CDOL1_TOTAL_BYTES,
+ buildCdol1,
+ parseCdol1,
+ formatHex,
+ formatJson,
+ formatAnnotatedTlv,
+};
diff --git a/src/core/lib/EmvTlv.mjs b/src/core/lib/EmvTlv.mjs
new file mode 100644
index 00000000..addbe480
--- /dev/null
+++ b/src/core/lib/EmvTlv.mjs
@@ -0,0 +1,163 @@
+/**
+ * @license Apache-2.0
+ * @author Jacob Marks [https://jacobmarks.com]
+ *
+ * BER-TLV parser for EMV data. Handles:
+ * - 1- and 2-byte tags (short-form and long-form tags up to 2 bytes)
+ * - Short-form and long-form lengths (up to 3 length bytes)
+ * - Recursive constructed-tag parsing
+ * - EMV tag dictionary enrichment (name, source, format, class)
+ */
+
+import OperationError from "../errors/OperationError.mjs";
+import EMV_TAG_DICTIONARY from "./EmvTlvDictionary.mjs";
+
+/**
+ * Parse a hex string into a Uint8Array of bytes.
+ * @param {string} hex
+ * @returns {Uint8Array}
+ */
+function hexToBytes(hex) {
+ const h = hex.replace(/\s+/g, "").toUpperCase();
+ if (!/^[0-9A-F]*$/.test(h) || h.length % 2 !== 0)
+ throw new OperationError("Input is not valid hex (odd length or non-hex chars).");
+ const bytes = new Uint8Array(h.length / 2);
+ for (let i = 0; i < bytes.length; i++)
+ bytes[i] = parseInt(h.substring(i * 2, i * 2 + 2), 16);
+ return bytes;
+}
+
+/**
+ * Determine tag class name from the high two bits of the first tag byte.
+ * @param {number} firstByte
+ * @returns {string}
+ */
+function tagClassName(firstByte) {
+ switch ((firstByte & 0xC0) >> 6) {
+ case 0: return "Universal";
+ case 1: return "Application";
+ case 2: return "Context-Specific";
+ case 3: return "Private";
+ }
+}
+
+/**
+ * Read one BER-TLV record starting at `offset` in `bytes`.
+ * Returns { tag, tagHex, rawBytes, constructed, class, length, valueBytes, offset: nextOffset }.
+ * @param {Uint8Array} bytes
+ * @param {number} offset
+ * @returns {object}
+ */
+function readTlv(bytes, offset) {
+ if (offset >= bytes.length)
+ throw new OperationError(`Unexpected end of data at offset ${offset}.`);
+
+ const firstByte = bytes[offset];
+ const isConstructed = !!(firstByte & 0x20);
+ const cls = tagClassName(firstByte);
+
+ // Tag: 1 or 2 bytes
+ let tagHex;
+ if ((firstByte & 0x1F) === 0x1F) {
+ // Long-form tag: second byte follows
+ if (offset + 1 >= bytes.length)
+ throw new OperationError(`Truncated long-form tag at offset ${offset}.`);
+ tagHex = bytes[offset].toString(16).padStart(2, "0").toUpperCase() +
+ bytes[offset + 1].toString(16).padStart(2, "0").toUpperCase();
+ offset += 2;
+ } else {
+ tagHex = firstByte.toString(16).padStart(2, "0").toUpperCase();
+ offset += 1;
+ }
+
+ // Length
+ if (offset >= bytes.length)
+ throw new OperationError(`Missing length byte for tag ${tagHex}.`);
+
+ const lenByte = bytes[offset++];
+ let length;
+ if (lenByte === 0x80) {
+ throw new OperationError(`Indefinite-length form is not supported (tag ${tagHex}).`);
+ } else if (lenByte > 0x80) {
+ const numLenBytes = lenByte & 0x7F;
+ if (numLenBytes > 3)
+ throw new OperationError(`Length encoding too long (${numLenBytes} bytes) for tag ${tagHex}.`);
+ if (offset + numLenBytes > bytes.length)
+ throw new OperationError(`Truncated length field for tag ${tagHex}.`);
+ length = 0;
+ for (let i = 0; i < numLenBytes; i++)
+ length = (length << 8) | bytes[offset++];
+ } else {
+ length = lenByte;
+ }
+
+ if (offset + length > bytes.length)
+ throw new OperationError(`Value of tag ${tagHex} extends past end of data (need ${length} bytes at offset ${offset}, have ${bytes.length - offset}).`);
+
+ const valueBytes = bytes.slice(offset, offset + length);
+ offset += length;
+
+ return { tagHex, isConstructed, class: cls, length, valueBytes, nextOffset: offset };
+}
+
+/**
+ * Recursively parse all BER-TLV records in `bytes[start..end]`.
+ * @param {Uint8Array} bytes
+ * @param {number} start
+ * @param {number} end
+ * @param {number} depth
+ * @returns {object[]}
+ */
+function parseTlvSequence(bytes, start, end, depth) {
+ const records = [];
+ let offset = start;
+ while (offset < end) {
+ // Skip 0x00 padding bytes (common in EMV records)
+ if (bytes[offset] === 0x00) { offset++; continue; }
+
+ const tlv = readTlv(bytes, offset);
+ offset = tlv.nextOffset;
+
+ const valueHex = Array.from(tlv.valueBytes)
+ .map(b => b.toString(16).padStart(2, "0").toUpperCase())
+ .join("");
+
+ const dict = EMV_TAG_DICTIONARY[tlv.tagHex] || null;
+
+ const record = {
+ tag: tlv.tagHex,
+ name: dict ? dict.name : "Unknown",
+ constructed: tlv.isConstructed,
+ class: dict ? dict.class : tlv.class,
+ source: dict ? dict.source : null,
+ format: dict ? dict.format : null,
+ length: tlv.length,
+ valueHex,
+ };
+
+ if (tlv.isConstructed && tlv.length > 0) {
+ try {
+ record.children = parseTlvSequence(tlv.valueBytes, 0, tlv.valueBytes.length, depth + 1);
+ } catch (_) {
+ record.children = [];
+ record.parseWarning = "Could not parse constructed value as BER-TLV.";
+ }
+ }
+
+ records.push(record);
+ }
+ return records;
+}
+
+/**
+ * Entry point: parse a hex-encoded EMV TLV blob.
+ * @param {string} hex
+ * @returns {object[]} parsed TLV records
+ */
+function parseEmvTlv(hex) {
+ const bytes = hexToBytes(hex);
+ if (bytes.length === 0) throw new OperationError("Input is empty.");
+ return parseTlvSequence(bytes, 0, bytes.length, 0);
+}
+
+export { parseEmvTlv, EMV_TAG_DICTIONARY };
diff --git a/src/core/lib/EmvTlvDictionary.mjs b/src/core/lib/EmvTlvDictionary.mjs
new file mode 100644
index 00000000..f80370b6
--- /dev/null
+++ b/src/core/lib/EmvTlvDictionary.mjs
@@ -0,0 +1,167 @@
+/**
+ * @license Apache-2.0
+ * @author Jacob Marks [https://jacobmarks.com]
+ *
+ * EMV tag dictionary covering EMV Books 1-4, EMVCo contactless, Nexo, and
+ * common acquirer/terminal tags. Each entry carries metadata used by the
+ * Parse EMV TLV operation.
+ *
+ * Sources: EMV Book 1 §A; EMV Book 3 §A; Nexo FAST 3.x; ISO 8583 DE 55 common tags.
+ */
+
+/**
+ * Tag source abbreviations:
+ * "ICC" — generated or maintained by the card
+ * "T" — generated or maintained by the terminal
+ * "Both" — may originate from either
+ * "Host" — generated or maintained by the issuer host
+ */
+
+/**
+ * Value format codes (EMV Book 3, Annex A):
+ * "a" — alphabetic
+ * "an" — alphanumeric
+ * "ans" — alphanumeric special
+ * "b" — binary
+ * "cn" — compressed numeric (BCD)
+ * "n" — numeric (BCD)
+ * "var" — variable / scheme-specific
+ */
+
+const EMV_TAG_DICTIONARY = {
+ // ── File Control Information ───────────────────────────────────────────────
+ "6F": { name: "File Control Information (FCI) Template", constructed: true, source: "ICC", format: "b", class: "Application" },
+ "A5": { name: "FCI Proprietary Template", constructed: true, source: "ICC", format: "b", class: "Context-Specific" },
+ "BF0C":{ name: "FCI Issuer Discretionary Data", constructed: true, source: "ICC", format: "b", class: "Private" },
+
+ // ── Record / Response Templates ────────────────────────────────────────────
+ "70": { name: "Record Template", constructed: true, source: "ICC", format: "b", class: "Application" },
+ "71": { name: "Issuer Script Template 1", constructed: true, source: "Host", format: "b", class: "Application" },
+ "72": { name: "Issuer Script Template 2", constructed: true, source: "Host", format: "b", class: "Application" },
+ "77": { name: "Response Message Template Format 2", constructed: true, source: "ICC", format: "b", class: "Application" },
+ "80": { name: "Response Message Template Format 1", constructed: false, source: "ICC", format: "b", class: "Context-Specific" },
+ "83": { name: "Command Template", constructed: false, source: "T", format: "b", class: "Context-Specific" },
+
+ // ── Application Labels / Identifiers ───────────────────────────────────────
+ "4F": { name: "Application Identifier (AID)", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "50": { name: "Application Label", constructed: false, source: "ICC", format: "an", class: "Application" },
+ "84": { name: "Dedicated File (DF) Name", constructed: false, source: "ICC", format: "b", class: "Context-Specific" },
+ "87": { name: "Application Priority Indicator", constructed: false, source: "ICC", format: "b", class: "Context-Specific" },
+ "9D": { name: "Directory Definition File (DDF) Name", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F06": { name: "Application Identifier (AID) — Terminal", constructed: false, source: "T", format: "b", class: "Application" },
+ "9F11": { name: "Issuer Code Table Index", constructed: false, source: "ICC", format: "n", class: "Application" },
+ "9F12": { name: "Application Preferred Name", constructed: false, source: "ICC", format: "ans", class: "Application" },
+ "9F38": { name: "Processing Options Data Object List (PDOL)",constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F4D": { name: "Log Entry", constructed: false, source: "ICC", format: "b", class: "Application" },
+
+ // ── Card / Cardholder Data ─────────────────────────────────────────────────
+ "5A": { name: "Application PAN", constructed: false, source: "ICC", format: "cn", class: "Application" },
+ "56": { name: "Track 1 Equivalent Data", constructed: false, source: "ICC", format: "ans", class: "Application" },
+ "57": { name: "Track 2 Equivalent Data", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "5F20": { name: "Cardholder Name", constructed: false, source: "ICC", format: "ans", class: "Application" },
+ "5F24": { name: "Application Expiry Date (YYMMDD)", constructed: false, source: "ICC", format: "n", class: "Application" },
+ "5F25": { name: "Application Effective Date (YYMMDD)", constructed: false, source: "ICC", format: "n", class: "Application" },
+ "5F28": { name: "Issuer Country Code", constructed: false, source: "ICC", format: "n", class: "Application" },
+ "5F2D": { name: "Language Preference", constructed: false, source: "ICC", format: "an", class: "Application" },
+ "5F30": { name: "Service Code", constructed: false, source: "ICC", format: "n", class: "Application" },
+ "5F34": { name: "Application PAN Sequence Number", constructed: false, source: "ICC", format: "n", class: "Application" },
+
+ // ── Transaction Amount / Currency ──────────────────────────────────────────
+ "5F2A": { name: "Transaction Currency Code", constructed: false, source: "T", format: "n", class: "Application" },
+ "5F36": { name: "Transaction Currency Exponent", constructed: false, source: "T", format: "n", class: "Application" },
+ "9F02": { name: "Amount, Authorised", constructed: false, source: "T", format: "n", class: "Application" },
+ "9F03": { name: "Amount, Other", constructed: false, source: "T", format: "n", class: "Application" },
+ "9F04": { name: "Amount, Other (Binary)", constructed: false, source: "T", format: "b", class: "Application" },
+
+ // ── Transaction Identification ─────────────────────────────────────────────
+ "9A": { name: "Transaction Date", constructed: false, source: "T", format: "n", class: "Application" },
+ "9C": { name: "Transaction Type", constructed: false, source: "T", format: "n", class: "Application" },
+ "9F21": { name: "Transaction Time", constructed: false, source: "T", format: "n", class: "Application" },
+ "9F37": { name: "Unpredictable Number", constructed: false, source: "T", format: "b", class: "Application" },
+ "9F41": { name: "Transaction Sequence Counter", constructed: false, source: "T", format: "n", class: "Application" },
+ "9F7C": { name: "Merchant Custom Data", constructed: false, source: "T", format: "b", class: "Application" },
+
+ // ── Terminal Data ──────────────────────────────────────────────────────────
+ "9F1A": { name: "Terminal Country Code", constructed: false, source: "T", format: "n", class: "Application" },
+ "9F33": { name: "Terminal Capabilities", constructed: false, source: "T", format: "b", class: "Application" },
+ "9F35": { name: "Terminal Type", constructed: false, source: "T", format: "n", class: "Application" },
+ "9F40": { name: "Additional Terminal Capabilities", constructed: false, source: "T", format: "b", class: "Application" },
+ "9F1B": { name: "Terminal Floor Limit", constructed: false, source: "T", format: "b", class: "Application" },
+ "9F1C": { name: "Terminal Identification", constructed: false, source: "T", format: "an", class: "Application" },
+ "9F1D": { name: "Terminal Risk Management Data", constructed: false, source: "T", format: "b", class: "Application" },
+ "9F1E": { name: "Interface Device (IFD) Serial Number", constructed: false, source: "T", format: "an", class: "Application" },
+ "9F15": { name: "Merchant Category Code", constructed: false, source: "T", format: "n", class: "Application" },
+ "9F16": { name: "Merchant Identifier", constructed: false, source: "T", format: "ans", class: "Application" },
+
+ // ── Cryptographic Data ─────────────────────────────────────────────────────
+ "82": { name: "Application Interchange Profile (AIP)", constructed: false, source: "ICC", format: "b", class: "Context-Specific" },
+ "9F26": { name: "Application Cryptogram (ARQC/TC/AAC)", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F27": { name: "Cryptogram Information Data (CID)", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F36": { name: "Application Transaction Counter (ATC)", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F10": { name: "Issuer Application Data (IAD)", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F4B": { name: "Signed Dynamic Application Data", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F4C": { name: "ICC Dynamic Number", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F45": { name: "Data Authentication Code", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F4A": { name: "Static Data Authentication Tag List", constructed: false, source: "ICC", format: "b", class: "Application" },
+
+ // ── Risk Management ────────────────────────────────────────────────────────
+ "95": { name: "Terminal Verification Results (TVR)", constructed: false, source: "T", format: "b", class: "Application" },
+ "9B": { name: "Transaction Status Information (TSI)", constructed: false, source: "Both", format: "b", class: "Application" },
+ "9F0D": { name: "Issuer Action Code — Default", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F0E": { name: "Issuer Action Code — Denial", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F0F": { name: "Issuer Action Code — Online", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F07": { name: "Application Usage Control", constructed: false, source: "ICC", format: "b", class: "Application" },
+
+ // ── CDOL / Script ──────────────────────────────────────────────────────────
+ "8C": { name: "Card Risk Management Data Object List 1 (CDOL1)", constructed: false, source: "ICC", format: "b", class: "Context-Specific" },
+ "8D": { name: "Card Risk Management Data Object List 2 (CDOL2)", constructed: false, source: "ICC", format: "b", class: "Context-Specific" },
+ "86": { name: "Issuer Script Command", constructed: false, source: "Host", format: "b", class: "Context-Specific" },
+ "9F18": { name: "Issuer Script Identifier", constructed: false, source: "Host", format: "b", class: "Application" },
+
+ // ── CVM ────────────────────────────────────────────────────────────────────
+ "8E": { name: "Cardholder Verification Method (CVM) List", constructed: false, source: "ICC", format: "b", class: "Context-Specific" },
+ "9F34": { name: "CVM Results", constructed: false, source: "T", format: "b", class: "Application" },
+
+ // ── Short File Identifier / AFL ───────────────────────────────────────────
+ "88": { name: "Short File Identifier (SFI)", constructed: false, source: "ICC", format: "b", class: "Context-Specific" },
+ "94": { name: "Application File Locator (AFL)", constructed: false, source: "ICC", format: "b", class: "Context-Specific" },
+ "8F": { name: "Certification Authority Public Key Index", constructed: false, source: "ICC", format: "b", class: "Context-Specific" },
+
+ // ── Issuer / Online Auth ───────────────────────────────────────────────────
+ "89": { name: "Authorization Code", constructed: false, source: "Host", format: "an", class: "Context-Specific" },
+ "8A": { name: "Authorization Response Code", constructed: false, source: "Host", format: "an", class: "Context-Specific" },
+ "91": { name: "Issuer Authentication Data", constructed: false, source: "Host", format: "b", class: "Context-Specific" },
+ "9F08": { name: "Application Version Number — ICC", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F09": { name: "Application Version Number — Terminal", constructed: false, source: "T", format: "b", class: "Application" },
+ "9F0B": { name: "Cardholder Name Extended", constructed: false, source: "ICC", format: "ans", class: "Application" },
+ "9F0C": { name: "Issuer Country Code (alpha2)", constructed: false, source: "ICC", format: "a", class: "Application" },
+ "9F13": { name: "Last Online ATC Register", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F14": { name: "Lower Consecutive Offline Limit", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F23": { name: "Upper Consecutive Offline Limit", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F17": { name: "PIN Try Counter", constructed: false, source: "ICC", format: "b", class: "Application" },
+
+ // ── Public Key Data ────────────────────────────────────────────────────────
+ "90": { name: "Issuer Public Key Certificate", constructed: false, source: "ICC", format: "b", class: "Context-Specific" },
+ "92": { name: "Issuer Public Key Remainder", constructed: false, source: "ICC", format: "b", class: "Context-Specific" },
+ "93": { name: "Signed Static Application Data", constructed: false, source: "ICC", format: "b", class: "Context-Specific" },
+ "9F2D": { name: "ICC PIN Encipherment Public Key Certificate",constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F2E": { name: "ICC PIN Encipherment Public Key Exponent", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F2F": { name: "ICC PIN Encipherment Public Key Remainder", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F32": { name: "Issuer Public Key Exponent", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F46": { name: "ICC Public Key Certificate", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F47": { name: "ICC Public Key Exponent", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F48": { name: "ICC Public Key Remainder", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "9F49": { name: "Dynamic Data Authentication Data Object List (DDOL)", constructed: false, source: "ICC", format: "b", class: "Application" },
+
+ // ── Contactless (EMVCo Book C / MSD) ──────────────────────────────────────
+ "9F6D": { name: "Mag-Stripe Application Version Number — Reader", constructed: false, source: "T", format: "b", class: "Application" },
+ "9F6E": { name: "Third Party Data", constructed: false, source: "T", format: "b", class: "Application" },
+ "9F7D": { name: "Application Capabilities Information", constructed: false, source: "ICC", format: "b", class: "Application" },
+ "DF8117": { name: "Card Data Input Capability", constructed: false, source: "T", format: "b", class: "Private" },
+
+ // ── Directory ──────────────────────────────────────────────────────────────
+ "61": { name: "Application Template", constructed: true, source: "ICC", format: "b", class: "Application" },
+ "73": { name: "Directory Discretionary Template", constructed: true, source: "ICC", format: "b", class: "Application" },
+};
+
+export default EMV_TAG_DICTIONARY;
diff --git a/src/core/operations/BuildEMVARQCData.mjs b/src/core/operations/BuildEMVARQCData.mjs
new file mode 100644
index 00000000..d2eface6
--- /dev/null
+++ b/src/core/operations/BuildEMVARQCData.mjs
@@ -0,0 +1,103 @@
+/**
+ * @license Apache-2.0
+ * @author Jacob Marks [https://jacobmarks.com]
+ */
+
+import Operation from "../Operation.mjs";
+import { buildCdol1, formatHex, formatJson, formatAnnotatedTlv } from "../lib/EmvCdol.mjs";
+
+/**
+ * EMV Build ARQC Data operation.
+ */
+class BuildEMVARQCData extends Operation {
+
+ constructor() {
+ super();
+
+ this.name = "EMV Build ARQC Data";
+ this.module = "Payment";
+ this.description = "Assemble the 10 standard EMVCo CDOL1 fields into the preassembled ARQC input data block used as input to EMV Generate ARQC and EMV Verify ARQC. All data comes from arguments — the input field is not used.
Input: ignored.
Arguments: one hex field per CDOL1 element plus an output format selector.
Network coverage: the 10-field, 33-byte layout is identical across Visa, Mastercard, Amex, Discover, JCB, and UnionPay acquirer flows. Network differences (Visa/Amex Option A vs Mastercard Option B session-key derivation) occur upstream in key derivation and do not affect the CDOL1 data block structure.
Chaining: set Output format to Hex and place this operation first in a recipe to supply the preimage directly into EMV Generate ARQC without using the input field.";
+ this.inlineHelp = "Args: one hex field per CDOL1 element. Set format to Hex to chain into EMV Generate ARQC.";
+ this.testDataSamples = [
+ {
+ name: "Standard CDOL1 — hex output (Visa $10.00 USD, USA terminal)",
+ input: "",
+ args: [
+ "000000001000",
+ "000000000000",
+ "0840",
+ "0000000000",
+ "0840",
+ "260521",
+ "00",
+ "A1B2C3D4",
+ "5900",
+ "0001",
+ "Hex",
+ ]
+ },
+ {
+ name: "Standard CDOL1 — annotated TLV",
+ input: "",
+ args: [
+ "000000001000",
+ "000000000000",
+ "0840",
+ "0000000000",
+ "0840",
+ "260521",
+ "00",
+ "A1B2C3D4",
+ "5900",
+ "0001",
+ "Annotated TLV",
+ ]
+ },
+ ];
+ this.infoURL = "https://en.wikipedia.org/wiki/EMV";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ { name: "Amount Authorised (9F02)", type: "string", value: "000000001000", comment: "6-byte BCD minor-unit amount, e.g. 000000001000 = $10.00" },
+ { name: "Amount Other (9F03)", type: "string", value: "000000000000", comment: "6-byte BCD cashback amount; 000000000000 if none" },
+ { name: "Terminal Country Code (9F1A)", type: "string", value: "0840", comment: "ISO 3166-1 numeric, e.g. 0840 = USA" },
+ { name: "TVR (95)", type: "string", value: "0000000000", comment: "5-byte Terminal Verification Results" },
+ { name: "Transaction Currency Code (5F2A)", type: "string", value: "0840", comment: "ISO 4217 numeric, e.g. 0840 = USD" },
+ { name: "Transaction Date (9A)", type: "string", value: "260521", comment: "3-byte YYMMDD, e.g. 260521 = 2026-05-21" },
+ { name: "Transaction Type (9C)", type: "string", value: "00", comment: "1-byte EMV type: 00 = Purchase, 01 = Cash, 09 = Cashback" },
+ { name: "Unpredictable Number (9F37)", type: "string", value: "00000000", comment: "4-byte terminal random; use a real random value in production flows" },
+ { name: "AIP (82)", type: "string", value: "5900", comment: "2-byte Application Interchange Profile" },
+ { name: "ATC (9F36)", type: "string", value: "0001", comment: "2-byte Application Transaction Counter" },
+ {
+ name: "Output format",
+ type: "option",
+ value: ["Hex", "JSON", "Annotated TLV"],
+ comment: "Hex: flat hex suitable for piping into EMV Generate ARQC. JSON/Annotated TLV: human-readable inspection.",
+ },
+ ];
+ }
+
+ /**
+ * @param {string} input ignored
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const [
+ amountAuth, amountOther, countryCode, tvr, currencyCode,
+ txDate, txType, unpredictable, aip, atc,
+ fmt,
+ ] = args;
+
+ const parsed = buildCdol1([
+ amountAuth, amountOther, countryCode, tvr, currencyCode,
+ txDate, txType, unpredictable, aip, atc,
+ ]);
+
+ if (fmt === "JSON") return formatJson(parsed);
+ if (fmt === "Annotated TLV") return formatAnnotatedTlv(parsed);
+ return formatHex(parsed);
+ }
+}
+
+export default BuildEMVARQCData;
diff --git a/src/core/operations/ParseEMVARQCData.mjs b/src/core/operations/ParseEMVARQCData.mjs
new file mode 100644
index 00000000..7a4accda
--- /dev/null
+++ b/src/core/operations/ParseEMVARQCData.mjs
@@ -0,0 +1,58 @@
+/**
+ * @license Apache-2.0
+ * @author Jacob Marks [https://jacobmarks.com]
+ */
+
+import Operation from "../Operation.mjs";
+import { parseCdol1, formatJson, formatAnnotatedTlv } from "../lib/EmvCdol.mjs";
+
+/**
+ * EMV Parse ARQC Data operation.
+ */
+class ParseEMVARQCData extends Operation {
+
+ constructor() {
+ super();
+
+ this.name = "EMV Parse ARQC Data";
+ this.module = "Payment";
+ this.description = "Parse a preassembled EMV ARQC input data block (standard 10-field CDOL1, 33 bytes) and display each field by name and tag.
Input: preassembled ARQC data as hex (66 hex chars / 33 bytes).
Arguments: output format.
Network coverage: the 10-field layout is identical across Visa, Mastercard, Amex, Discover, JCB, and UnionPay acquirer flows. Use this as the inverse of EMV Build ARQC Data.";
+ this.inlineHelp = "Input: 33-byte CDOL1 hex block. Inverse of EMV Build ARQC Data.";
+ this.testDataSamples = [
+ {
+ name: "Standard CDOL1 parse — annotated TLV",
+ input: "000000001000000000000000084000000000000840260521 00A1B2C3D459000001",
+ args: ["Annotated TLV"]
+ },
+ {
+ name: "Standard CDOL1 parse — JSON",
+ input: "000000001000000000000000084000000000000840260521 00A1B2C3D459000001",
+ args: ["JSON"]
+ },
+ ];
+ this.infoURL = "https://en.wikipedia.org/wiki/EMV";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Output format",
+ type: "option",
+ value: ["Annotated TLV", "JSON"],
+ comment: "Annotated TLV: one line per field with tag, length, value, and name. JSON: key-value object.",
+ },
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const [fmt] = args;
+ const parsed = parseCdol1(input);
+ return fmt === "JSON" ? formatJson(parsed) : formatAnnotatedTlv(parsed);
+ }
+}
+
+export default ParseEMVARQCData;
diff --git a/src/core/operations/ParseEMVTLV.mjs b/src/core/operations/ParseEMVTLV.mjs
new file mode 100644
index 00000000..4342a0f1
--- /dev/null
+++ b/src/core/operations/ParseEMVTLV.mjs
@@ -0,0 +1,72 @@
+/**
+ * @license Apache-2.0
+ * @author Jacob Marks [https://jacobmarks.com]
+ */
+
+import Operation from "../Operation.mjs";
+import { parseEmvTlv, EMV_TAG_DICTIONARY } from "../lib/EmvTlv.mjs";
+
+/**
+ * Parse EMV TLV operation.
+ */
+class ParseEMVTLV extends Operation {
+
+ constructor() {
+ super();
+
+ this.name = "Parse EMV TLV";
+ this.module = "Payment";
+ this.description = "Parse hex-encoded BER-TLV data (e.g., DE 55 field, ICC response, terminal data, ARQC preimage in TLV form) and annotate each tag using the built-in EMV tag dictionary.
Input: hex-encoded BER-TLV data.
Output: JSON tree. Each record includes the tag hex value, name from the EMV tag dictionary, source (ICC / Terminal / Host / Both), value format, length, value in hex, and — for constructed tags — a children array with the recursively parsed inner TLVs.
Tag dictionary: covers EMV Books 1–4, EMVCo contactless Book C, and common Nexo/acquirer tags (~90 entries). Unknown tags are decoded structurally but marked with name Unknown.
Constructed tags: tags with the constructed bit set (e.g., 70, 77, 6F, A5, BF0C) are recursively parsed into child arrays.
Note: indefinite-length BER encoding is not supported; this covers the definite short- and long-form lengths used by all standard EMV cards.";
+ this.inlineHelp = "Input: hex-encoded BER-TLV (DE 55, ICC response, GPO reply, etc.). Outputs annotated JSON with EMV tag names and nested children.";
+ this.testDataSamples = [
+ {
+ name: "GPO response (Format 2): AIP=5900 + AFL",
+ input: "770A82025900940408010401",
+ args: [false]
+ },
+ {
+ name: "DE 55 fragment: ARQC cryptogram tags",
+ input: "9F2608A1B2C3D4E5F607089F2701809F360200019F10120110A0000F040000000000000000000000FF",
+ args: [false]
+ },
+ {
+ name: "Tag dictionary listing",
+ input: "",
+ args: [true]
+ }
+ ];
+ this.infoURL = "https://en.wikipedia.org/wiki/EMV";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Show tag dictionary only",
+ type: "boolean",
+ value: false,
+ comment: "When enabled, ignores input and prints the full EMV tag dictionary as JSON.",
+ },
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const [dictionaryMode] = args;
+
+ if (dictionaryMode) {
+ const dict = {};
+ for (const [tag, meta] of Object.entries(EMV_TAG_DICTIONARY)) {
+ dict[tag] = { name: meta.name, constructed: meta.constructed, source: meta.source, format: meta.format, class: meta.class };
+ }
+ return JSON.stringify(dict, null, 4);
+ }
+
+ const parsed = parseEmvTlv(input);
+ return JSON.stringify(parsed, null, 4);
+ }
+}
+
+export default ParseEMVTLV;
diff --git a/tests/operations/tests/Payment.mjs b/tests/operations/tests/Payment.mjs
index de8dee46..c4d635fe 100644
--- a/tests/operations/tests/Payment.mjs
+++ b/tests/operations/tests/Payment.mjs
@@ -828,6 +828,135 @@ TestRegister.addTests([
}
]
},
+ // ── EMV Build / Parse ARQC Data ───────────────────────────────────────────
+ // CDOL1 sample: Visa $10.00 USD, USA terminal, date 2026-05-21
+ // 9F02 000000001000 9F03 000000000000 9F1A 0840 95 0000000000
+ // 5F2A 0840 9A 260521 9C 00 9F37 A1B2C3D4 82 5900 9F36 0001
+ // Assembled hex (33 bytes / 66 chars):
+ // 00000000100000000000000008400000000000084026052100A1B2C3D459000001
+ {
+ name: "EMV Build ARQC Data: hex output",
+ input: "",
+ expectedOutput: "00000000100000000000000008400000000000084026052100A1B2C3D459000001",
+ recipeConfig: [
+ {
+ op: "EMV Build ARQC Data",
+ args: ["000000001000", "000000000000", "0840", "0000000000", "0840", "260521", "00", "A1B2C3D4", "5900", "0001", "Hex"]
+ }
+ ]
+ },
+ {
+ name: "EMV Build ARQC Data: JSON output",
+ input: "",
+ expectedOutput: JSON.stringify({
+ "Amount Authorised (9F02)": "000000001000",
+ "Amount Other (9F03)": "000000000000",
+ "Terminal Country Code (9F1A)": "0840",
+ "TVR (95)": "0000000000",
+ "Transaction Currency Code (5F2A)": "0840",
+ "Transaction Date (9A)": "260521",
+ "Transaction Type (9C)": "00",
+ "Unpredictable Number (9F37)": "A1B2C3D4",
+ "AIP (82)": "5900",
+ "ATC (9F36)": "0001",
+ }, null, 4),
+ recipeConfig: [
+ {
+ op: "EMV Build ARQC Data",
+ args: ["000000001000", "000000000000", "0840", "0000000000", "0840", "260521", "00", "A1B2C3D4", "5900", "0001", "JSON"]
+ }
+ ]
+ },
+ {
+ name: "EMV Build ARQC Data: annotated TLV output",
+ input: "",
+ expectedOutput: [
+ "9F02 06 000000001000 [Amount Authorised]",
+ "9F03 06 000000000000 [Amount Other]",
+ "9F1A 02 0840 [Terminal Country Code]",
+ "95 05 0000000000 [TVR]",
+ "5F2A 02 0840 [Transaction Currency Code]",
+ "9A 03 260521 [Transaction Date]",
+ "9C 01 00 [Transaction Type]",
+ "9F37 04 A1B2C3D4 [Unpredictable Number]",
+ "82 02 5900 [AIP]",
+ "9F36 02 0001 [ATC]",
+ ].join("\n"),
+ recipeConfig: [
+ {
+ op: "EMV Build ARQC Data",
+ args: ["000000001000", "000000000000", "0840", "0000000000", "0840", "260521", "00", "A1B2C3D4", "5900", "0001", "Annotated TLV"]
+ }
+ ]
+ },
+ {
+ name: "EMV Parse ARQC Data: annotated TLV",
+ input: "00000000100000000000000008400000000000084026052100A1B2C3D459000001",
+ expectedOutput: [
+ "9F02 06 000000001000 [Amount Authorised]",
+ "9F03 06 000000000000 [Amount Other]",
+ "9F1A 02 0840 [Terminal Country Code]",
+ "95 05 0000000000 [TVR]",
+ "5F2A 02 0840 [Transaction Currency Code]",
+ "9A 03 260521 [Transaction Date]",
+ "9C 01 00 [Transaction Type]",
+ "9F37 04 A1B2C3D4 [Unpredictable Number]",
+ "82 02 5900 [AIP]",
+ "9F36 02 0001 [ATC]",
+ ].join("\n"),
+ recipeConfig: [
+ {
+ op: "EMV Parse ARQC Data",
+ args: ["Annotated TLV"]
+ }
+ ]
+ },
+ {
+ name: "EMV Parse ARQC Data: JSON",
+ input: "00000000100000000000000008400000000000084026052100A1B2C3D459000001",
+ expectedOutput: JSON.stringify({
+ "Amount Authorised (9F02)": "000000001000",
+ "Amount Other (9F03)": "000000000000",
+ "Terminal Country Code (9F1A)": "0840",
+ "TVR (95)": "0000000000",
+ "Transaction Currency Code (5F2A)": "0840",
+ "Transaction Date (9A)": "260521",
+ "Transaction Type (9C)": "00",
+ "Unpredictable Number (9F37)": "A1B2C3D4",
+ "AIP (82)": "5900",
+ "ATC (9F36)": "0001",
+ }, null, 4),
+ recipeConfig: [
+ {
+ op: "EMV Parse ARQC Data",
+ args: ["JSON"]
+ }
+ ]
+ },
+ {
+ name: "EMV Build ARQC Data: bad field length throws",
+ input: "",
+ expectedError: true,
+ expectedOutput: "Error: Amount Authorised: expected 12 hex chars (6 bytes), got 4.",
+ recipeConfig: [
+ {
+ op: "EMV Build ARQC Data",
+ args: ["0001", "000000000000", "0840", "0000000000", "0840", "260521", "00", "A1B2C3D4", "5900", "0001", "Hex"]
+ }
+ ]
+ },
+ {
+ name: "EMV Parse ARQC Data: too-short input throws",
+ input: "000000001000",
+ expectedError: true,
+ expectedOutput: "Error: Standard CDOL1 requires 66 hex chars (33 bytes); got 12.",
+ recipeConfig: [
+ {
+ op: "EMV Parse ARQC Data",
+ args: ["JSON"]
+ }
+ ]
+ },
{
name: "Payment Encrypt Data: AES CBC",
input: "00112233445566778899AABBCCDDEEFF",
@@ -1343,6 +1472,53 @@ TestRegister.addTests([
]
},
+ // ── Parse EMV TLV ─────────────────────────────────────────────────────────
+ {
+ name: "Parse EMV TLV: GPO Format 2 (constructed 77 > AIP + AFL)",
+ input: "770A82025900940408010401",
+ expectedOutput: JSON.stringify([
+ {
+ tag: "77", name: "Response Message Template Format 2",
+ constructed: true, class: "Application", source: "ICC", format: "b",
+ length: 10, valueHex: "82025900940408010401",
+ children: [
+ { tag: "82", name: "Application Interchange Profile (AIP)", constructed: false, class: "Context-Specific", source: "ICC", format: "b", length: 2, valueHex: "5900" },
+ { tag: "94", name: "Application File Locator (AFL)", constructed: false, class: "Context-Specific", source: "ICC", format: "b", length: 4, valueHex: "08010401" },
+ ],
+ },
+ ], null, 4),
+ recipeConfig: [{ op: "Parse EMV TLV", args: [false] }]
+ },
+ {
+ name: "Parse EMV TLV: primitive tags (ARQC / CID / ATC)",
+ input: "9F2608A1B2C3D4E5F607089F2701809F360200 01",
+ expectedOutput: JSON.stringify([
+ { tag: "9F26", name: "Application Cryptogram (ARQC/TC/AAC)", constructed: false, class: "Application", source: "ICC", format: "b", length: 8, valueHex: "A1B2C3D4E5F60708" },
+ { tag: "9F27", name: "Cryptogram Information Data (CID)", constructed: false, class: "Application", source: "ICC", format: "b", length: 1, valueHex: "80" },
+ { tag: "9F36", name: "Application Transaction Counter (ATC)",constructed: false, class: "Application", source: "ICC", format: "b", length: 2, valueHex: "0001" },
+ ], null, 4),
+ recipeConfig: [{ op: "Parse EMV TLV", args: [false] }]
+ },
+ {
+ name: "Parse EMV TLV: unknown tag decoded structurally",
+ input: "FF0203AABBCC",
+ expectedMatch: /"name":\s*"Unknown"/,
+ recipeConfig: [{ op: "Parse EMV TLV", args: [false] }]
+ },
+ {
+ name: "Parse EMV TLV: dictionary mode returns tag index",
+ input: "",
+ expectedMatch: /"9F26":/,
+ recipeConfig: [{ op: "Parse EMV TLV", args: [true] }]
+ },
+ {
+ name: "Parse EMV TLV: bad hex throws",
+ input: "GG",
+ expectedError: true,
+ expectedOutput: "Error: Input is not valid hex (odd length or non-hex chars).",
+ recipeConfig: [{ op: "Parse EMV TLV", args: [false] }]
+ },
+
// ── PIN Block Translate Encrypted ─────────────────────────────────────────
// Vectors: PIN=1234, PAN=5432101234567890
// clear Format 0 block : 041215FEDCBA9876