Add EMV Build/Parse ARPC Data operations

- EMV Build ARPC Data: assembles ARPC preimage from named fields; Method 1 (Visa/Amex/Discover: ARQC+ARC, 10 bytes) and Method 2 (Mastercard: ARQC+CSU+optional PAD, 12-20 bytes); outputs hex (chainable into EMV Generate ARPC), JSON, or annotated
- EMV Parse ARPC Data: inverse; parses hex preimage back into named fields by method
- Shared lib EmvArpc.mjs with build/parse/format functions
- 6 new tests in Payment.mjs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
J8k3 2026-05-21 14:54:36 -04:00
parent 10bb87b320
commit 259b9740a7
5 changed files with 413 additions and 0 deletions

View File

@ -591,9 +591,11 @@
"Card Validation Data Verify", "Card Validation Data Verify",
"DUKPT Derive AES Key", "DUKPT Derive AES Key",
"DUKPT Derive TDES Key", "DUKPT Derive TDES Key",
"EMV Build ARPC Data",
"EMV Build ARQC Data", "EMV Build ARQC Data",
"EMV Generate ARPC", "EMV Generate ARPC",
"EMV Generate ARQC", "EMV Generate ARQC",
"EMV Parse ARPC Data",
"EMV Parse ARQC Data", "EMV Parse ARQC Data",
"Parse EMV TLV", "Parse EMV TLV",
"EMV Generate MAC", "EMV Generate MAC",

167
src/core/lib/EmvArpc.mjs Normal file
View File

@ -0,0 +1,167 @@
/**
* @license Apache-2.0
* @author Jacob Marks [https://jacobmarks.com]
*
* ARPC preimage assembly and parsing for EMV Method 1 and Method 2.
*
* Method 1 (Visa, Amex, Discover, JCB):
* Preimage = ARQC (8 bytes) || ARC (2 bytes) 10 bytes
*
* Method 2 (Mastercard M/Chip):
* Preimage = ARQC (8 bytes) || CSU (4 bytes) || ProprietaryAuthData (08 bytes) 1220 bytes
*/
import OperationError from "../errors/OperationError.mjs";
const METHOD1 = "Method 1 (Visa/Amex/Discover)";
const METHOD2 = "Method 2 (Mastercard)";
const METHODS = [METHOD1, METHOD2];
const METHOD1_FIELDS = [
{ name: "ARQC", bytes: 8, description: "Authorization Request Cryptogram from the card" },
{ name: "ARC", bytes: 2, description: "Authorization Response Code (e.g. Y1=5931, Z1=5A31, 00=3030)" },
];
const METHOD2_FIXED_FIELDS = [
{ name: "ARQC", bytes: 8, description: "Authorization Request Cryptogram from the card" },
{ name: "Card Status Update (CSU)", bytes: 4, description: "Issuer response flags (PIN change/unblock, go-online indicators)" },
];
/**
* @param {string} value
* @param {string} name
* @param {number} bytes
* @returns {string} uppercase hex, validated
*/
function validateHex(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} value
* @param {string} name
* @param {number} maxBytes
* @returns {string} uppercase hex, validated (may be empty)
*/
function validateOptionalHex(value, name, maxBytes) {
const h = (value || "").replace(/\s+/g, "").toUpperCase();
if (h.length === 0) return "";
if (!/^[0-9A-F]+$/.test(h) || h.length % 2 !== 0)
throw new OperationError(`${name}: not valid hex.`);
if (h.length > maxBytes * 2)
throw new OperationError(`${name}: max ${maxBytes * 2} hex chars (${maxBytes} bytes), got ${h.length}.`);
return h;
}
/**
* Build Method 1 preimage.
* @param {string} arqcHex
* @param {string} arcHex
* @returns {{ fields: object[], hex: string }}
*/
function buildMethod1(arqcHex, arcHex) {
const arqc = validateHex(arqcHex, "ARQC", 8);
const arc = validateHex(arcHex, "ARC", 2);
const fields = [
{ ...METHOD1_FIELDS[0], value: arqc },
{ ...METHOD1_FIELDS[1], value: arc },
];
return { fields, hex: arqc + arc };
}
/**
* Build Method 2 preimage.
* @param {string} arqcHex
* @param {string} csuHex
* @param {string} padHex optional, 08 bytes
* @returns {{ fields: object[], hex: string }}
*/
function buildMethod2(arqcHex, csuHex, padHex) {
const arqc = validateHex(arqcHex, "ARQC", 8);
const csu = validateHex(csuHex, "Card Status Update (CSU)", 4);
const pad = validateOptionalHex(padHex, "Proprietary Auth Data", 8);
const fields = [
{ ...METHOD2_FIXED_FIELDS[0], value: arqc },
{ ...METHOD2_FIXED_FIELDS[1], value: csu },
{ name: "Proprietary Auth Data", bytes: pad.length / 2, description: "Optional issuer-specific bytes (08)", value: pad },
];
return { fields: pad.length > 0 ? fields : fields.slice(0, 2), hex: arqc + csu + pad };
}
/**
* Parse Method 1 hex preimage.
* @param {string} hex
* @returns {{ fields: object[] }}
*/
function parseMethod1(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 !== 20)
throw new OperationError(`Method 1 preimage requires 20 hex chars (10 bytes); got ${h.length}.`);
return {
fields: [
{ ...METHOD1_FIELDS[0], value: h.substring(0, 16) },
{ ...METHOD1_FIELDS[1], value: h.substring(16, 20) },
]
};
}
/**
* Parse Method 2 hex preimage.
* @param {string} hex
* @returns {{ fields: object[] }}
*/
function parseMethod2(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 < 24 || h.length > 40 || h.length % 2 !== 0)
throw new OperationError(`Method 2 preimage requires 2440 hex chars (1220 bytes); got ${h.length}.`);
const padBytes = (h.length - 24) / 2;
const fields = [
{ ...METHOD2_FIXED_FIELDS[0], value: h.substring(0, 16) },
{ ...METHOD2_FIXED_FIELDS[1], value: h.substring(16, 24) },
];
if (padBytes > 0)
fields.push({ name: "Proprietary Auth Data", bytes: padBytes, description: "Optional issuer-specific bytes (08)", value: h.substring(24) });
return { fields };
}
/**
* Format parsed fields as JSON.
* @param {object[]} fields
* @param {string} method
* @returns {string}
*/
function formatJson(fields, method) {
const obj = { method };
for (const f of fields) obj[f.name] = f.value;
return JSON.stringify(obj, null, 4);
}
/**
* Format parsed fields as annotated list.
* @param {object[]} fields
* @param {string} method
* @returns {string}
*/
function formatAnnotated(fields, method) {
const header = `ARPC ${method} preimage\n${"─".repeat(50)}`;
const rows = fields.map(f =>
`${f.name.padEnd(30)} ${f.value.padEnd(16)} [${f.bytes} byte${f.bytes === 1 ? "" : "s"}]`
);
return [header, ...rows].join("\n");
}
export {
METHODS, METHOD1, METHOD2,
buildMethod1, buildMethod2,
parseMethod1, parseMethod2,
formatJson, formatAnnotated,
};

View File

@ -0,0 +1,103 @@
/**
* @license Apache-2.0
* @author Jacob Marks [https://jacobmarks.com]
*/
import Operation from "../Operation.mjs";
import {
METHODS, METHOD1, METHOD2,
buildMethod1, buildMethod2,
formatJson, formatAnnotated,
} from "../lib/EmvArpc.mjs";
/**
* EMV Build ARPC Data operation.
*/
class BuildEMVARPCData extends Operation {
constructor() {
super();
this.name = "EMV Build ARPC Data";
this.module = "Payment";
this.description = "Assemble the EMV authorization-response preimage from named fields and output it as hex for use with <b>EMV Generate ARPC</b>. All data comes from arguments — the input field is not used.<br><br><b>Method 1</b> (Visa, Amex, Discover, JCB): <code>ARQC (8 bytes) || ARC (2 bytes)</code> — 10 bytes total.<br><b>Method 2</b> (Mastercard M/Chip): <code>ARQC (8 bytes) || CSU (4 bytes) || ProprietaryAuthData (08 bytes)</code> — 1220 bytes total.<br><br><b>Input:</b> ignored.<br><b>Arguments:</b> method selector plus one field per preimage element. Fields irrelevant to the selected method are ignored.<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 ARPC</b>.";
this.inlineHelp = "<strong>Args:</strong> select method (1 = Visa/Amex, 2 = Mastercard) and fill the relevant fields. Set format to <strong>Hex</strong> to chain into EMV Generate ARPC.";
this.testDataSamples = [
{
name: "Method 1 (Visa/Amex) — hex output",
input: "",
args: [METHOD1, "A1B2C3D4E5F60708", "5931", "00000000", "", "Hex"]
},
{
name: "Method 2 (Mastercard) — hex output",
input: "",
args: [METHOD2, "A1B2C3D4E5F60708", "5931", "00000000", "", "Hex"]
},
{
name: "Method 2 with Proprietary Auth Data — annotated",
input: "",
args: [METHOD2, "A1B2C3D4E5F60708", "5931", "00000000", "AABBCCDD", "Annotated"]
},
];
this.infoURL = "https://en.wikipedia.org/wiki/EMV";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
name: "ARPC method",
type: "option",
value: METHODS,
comment: "Method 1: Visa, Amex, Discover, JCB. Method 2: Mastercard M/Chip.",
},
{
name: "ARQC (hex, 8 bytes)",
type: "string",
value: "",
comment: "Authorization Request Cryptogram — output of EMV Generate ARQC.",
},
{
name: "ARC (hex, 2 bytes) — Method 1",
type: "string",
value: "3030",
comment: "Authorization Response Code. Common values: 3030=00, 5931=Y1 (approve), 5933=Y3, 5A31=Z1 (decline). Used only for Method 1.",
},
{
name: "Card Status Update / CSU (hex, 4 bytes) — Method 2",
type: "string",
value: "00000000",
comment: "Issuer response flags for PIN change/unblock and go-online. Used only for Method 2.",
},
{
name: "Proprietary Auth Data (hex, 08 bytes) — Method 2",
type: "string",
value: "",
comment: "Optional scheme-specific data appended after CSU. Leave empty if not used. Used only for Method 2.",
},
{
name: "Output format",
type: "option",
value: ["Hex", "JSON", "Annotated"],
comment: "Hex: flat hex for piping into EMV Generate ARPC. JSON/Annotated: human-readable inspection.",
},
];
}
/**
* @param {string} input ignored
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const [method, arqc, arc, csu, pad, fmt] = args;
const { fields, hex } = method === METHOD2
? buildMethod2(arqc, csu, pad)
: buildMethod1(arqc, arc);
if (fmt === "JSON") return formatJson(fields, method);
if (fmt === "Annotated") return formatAnnotated(fields, method);
return hex;
}
}
export default BuildEMVARPCData;

View File

@ -0,0 +1,73 @@
/**
* @license Apache-2.0
* @author Jacob Marks [https://jacobmarks.com]
*/
import Operation from "../Operation.mjs";
import {
METHODS, METHOD1, METHOD2,
parseMethod1, parseMethod2,
formatJson, formatAnnotated,
} from "../lib/EmvArpc.mjs";
/**
* EMV Parse ARPC Data operation.
*/
class ParseEMVARPCData extends Operation {
constructor() {
super();
this.name = "EMV Parse ARPC Data";
this.module = "Payment";
this.description = "Parse a preassembled EMV authorization-response preimage and display each field by name. Inverse of <b>EMV Build ARPC Data</b>.<br><br><b>Method 1</b> (Visa, Amex, Discover, JCB): expects exactly 20 hex chars (10 bytes) — <code>ARQC || ARC</code>.<br><b>Method 2</b> (Mastercard M/Chip): expects 2440 hex chars (1220 bytes) — <code>ARQC || CSU || [ProprietaryAuthData]</code>.<br><br><b>Input:</b> preassembled ARPC data as hex.<br><b>Arguments:</b> method selector and output format.";
this.inlineHelp = "<strong>Input:</strong> hex ARPC preimage. Select method to control field layout. Inverse of EMV Build ARPC Data.";
this.testDataSamples = [
{
name: "Method 1 parse (ARQC + ARC)",
input: "A1B2C3D4E5F607085931",
args: [METHOD1, "Annotated"]
},
{
name: "Method 2 parse (ARQC + CSU)",
input: "A1B2C3D4E5F6070800000000",
args: [METHOD2, "Annotated"]
},
{
name: "Method 2 parse with Proprietary Auth Data",
input: "A1B2C3D4E5F60708000000 00AABBCCDD",
args: [METHOD2, "JSON"]
},
];
this.infoURL = "https://en.wikipedia.org/wiki/EMV";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
name: "ARPC method",
type: "option",
value: METHODS,
comment: "Method 1: Visa, Amex, Discover, JCB (10 bytes). Method 2: Mastercard M/Chip (1220 bytes).",
},
{
name: "Output format",
type: "option",
value: ["Annotated", "JSON"],
comment: "Annotated: one line per field with name, value, and length. JSON: key-value object.",
},
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const [method, fmt] = args;
const { fields } = method === METHOD2 ? parseMethod2(input) : parseMethod1(input);
return fmt === "JSON" ? formatJson(fields, method) : formatAnnotated(fields, method);
}
}
export default ParseEMVARPCData;

View File

@ -828,6 +828,74 @@ TestRegister.addTests([
} }
] ]
}, },
// ── EMV Build / Parse ARPC Data ───────────────────────────────────────────
// Method 1 (Visa/Amex): ARQC=A1B2C3D4E5F60708, ARC=5931 → 10 bytes
// Method 2 (Mastercard): ARQC=A1B2C3D4E5F60708, CSU=00000000 → 12 bytes
{
name: "EMV Build ARPC Data: Method 1 hex output",
input: "",
expectedOutput: "A1B2C3D4E5F607085931",
recipeConfig: [{
op: "EMV Build ARPC Data",
args: ["Method 1 (Visa/Amex/Discover)", "A1B2C3D4E5F60708", "5931", "00000000", "", "Hex"]
}]
},
{
name: "EMV Build ARPC Data: Method 2 hex output (no PAD)",
input: "",
expectedOutput: "A1B2C3D4E5F6070800000000",
recipeConfig: [{
op: "EMV Build ARPC Data",
args: ["Method 2 (Mastercard)", "A1B2C3D4E5F60708", "5931", "00000000", "", "Hex"]
}]
},
{
name: "EMV Build ARPC Data: Method 2 hex output (with PAD)",
input: "",
expectedOutput: "A1B2C3D4E5F6070800000000AABBCCDD",
recipeConfig: [{
op: "EMV Build ARPC Data",
args: ["Method 2 (Mastercard)", "A1B2C3D4E5F60708", "5931", "00000000", "AABBCCDD", "Hex"]
}]
},
{
name: "EMV Parse ARPC Data: Method 1 JSON",
input: "A1B2C3D4E5F607085931",
expectedOutput: JSON.stringify({
method: "Method 1 (Visa/Amex/Discover)",
ARQC: "A1B2C3D4E5F60708",
ARC: "5931",
}, null, 4),
recipeConfig: [{
op: "EMV Parse ARPC Data",
args: ["Method 1 (Visa/Amex/Discover)", "JSON"]
}]
},
{
name: "EMV Parse ARPC Data: Method 2 JSON (with PAD)",
input: "A1B2C3D4E5F6070800000000AABBCCDD",
expectedOutput: JSON.stringify({
method: "Method 2 (Mastercard)",
ARQC: "A1B2C3D4E5F60708",
"Card Status Update (CSU)": "00000000",
"Proprietary Auth Data": "AABBCCDD",
}, null, 4),
recipeConfig: [{
op: "EMV Parse ARPC Data",
args: ["Method 2 (Mastercard)", "JSON"]
}]
},
{
name: "EMV Parse ARPC Data: wrong length for Method 1 throws",
input: "A1B2C3D4",
expectedError: true,
expectedOutput: "Error: Method 1 preimage requires 20 hex chars (10 bytes); got 8.",
recipeConfig: [{
op: "EMV Parse ARPC Data",
args: ["Method 1 (Visa/Amex/Discover)", "JSON"]
}]
},
// ── EMV Build / Parse ARQC Data ─────────────────────────────────────────── // ── EMV Build / Parse ARQC Data ───────────────────────────────────────────
// CDOL1 sample: Visa $10.00 USD, USA terminal, date 2026-05-21 // CDOL1 sample: Visa $10.00 USD, USA terminal, date 2026-05-21
// 9F02 000000001000 9F03 000000000000 9F1A 0840 95 0000000000 // 9F02 000000001000 9F03 000000000000 9F1A 0840 95 0000000000