Add EMV Build/Parse ARQC Data and Parse EMV TLV operations (issues #11)

- EMV Build ARQC Data: assembles 10-field CDOL1 preimage from args; outputs hex (chainable into EMV Generate ARQC), JSON, or annotated TLV
- EMV Parse ARQC Data: inverse; parses flat 33-byte CDOL1 hex back into named fields
- Parse EMV TLV: BER-TLV parser with 102-entry EMV tag dictionary; handles constructed/nested tags, 1- and 2-byte tags, long-form lengths; dictionary mode lists all known tags
- Shared libs: EmvCdol.mjs (CDOL1 field defs), EmvTlv.mjs (parser), EmvTlvDictionary.mjs (tag dict)
- 12 new tests in Payment.mjs covering all three operations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
J8k3 2026-05-21 13:04:58 -04:00
parent f90fba92fd
commit 10bb87b320
8 changed files with 860 additions and 0 deletions

View File

@ -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",

118
src/core/lib/EmvCdol.mjs Normal file
View File

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

163
src/core/lib/EmvTlv.mjs Normal file
View File

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

View File

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

View File

@ -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 <b>EMV Generate ARQC</b> and <b>EMV Verify ARQC</b>. All data comes from arguments — the input field is not used.<br><br><b>Input:</b> ignored.<br><b>Arguments:</b> one hex field per CDOL1 element plus an output format selector.<br><br><b>Network coverage:</b> 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.<br><br><b>Chaining:</b> set Output format to <b>Hex</b> and place this operation first in a recipe to supply the preimage directly into <b>EMV Generate ARQC</b> without using the input field.";
this.inlineHelp = "<strong>Args:</strong> one hex field per CDOL1 element. Set format to <strong>Hex</strong> 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;

View File

@ -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.<br><br><b>Input:</b> preassembled ARQC data as hex (66 hex chars / 33 bytes).<br><b>Arguments:</b> output format.<br><br><b>Network coverage:</b> the 10-field layout is identical across Visa, Mastercard, Amex, Discover, JCB, and UnionPay acquirer flows. Use this as the inverse of <b>EMV Build ARQC Data</b>.";
this.inlineHelp = "<strong>Input:</strong> 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;

View File

@ -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.<br><br><b>Input:</b> hex-encoded BER-TLV data.<br><b>Output:</b> 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 <code>children</code> array with the recursively parsed inner TLVs.<br><br><b>Tag dictionary:</b> covers EMV Books 14, EMVCo contactless Book C, and common Nexo/acquirer tags (~90 entries). Unknown tags are decoded structurally but marked with name <code>Unknown</code>.<br><br><b>Constructed tags:</b> tags with the constructed bit set (e.g., <code>70</code>, <code>77</code>, <code>6F</code>, <code>A5</code>, <code>BF0C</code>) are recursively parsed into child arrays.<br><br><b>Note:</b> indefinite-length BER encoding is not supported; this covers the definite short- and long-form lengths used by all standard EMV cards.";
this.inlineHelp = "<strong>Input:</strong> 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;

View File

@ -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