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:
parent
f90fba92fd
commit
10bb87b320
@ -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
118
src/core/lib/EmvCdol.mjs
Normal 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
163
src/core/lib/EmvTlv.mjs
Normal 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 };
|
||||
167
src/core/lib/EmvTlvDictionary.mjs
Normal file
167
src/core/lib/EmvTlvDictionary.mjs
Normal 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;
|
||||
103
src/core/operations/BuildEMVARQCData.mjs
Normal file
103
src/core/operations/BuildEMVARQCData.mjs
Normal 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;
|
||||
58
src/core/operations/ParseEMVARQCData.mjs
Normal file
58
src/core/operations/ParseEMVARQCData.mjs
Normal 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;
|
||||
72
src/core/operations/ParseEMVTLV.mjs
Normal file
72
src/core/operations/ParseEMVTLV.mjs
Normal 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 1–4, 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;
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user