diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json
index ee36b087..1647e904 100644
--- a/src/core/config/Categories.json
+++ b/src/core/config/Categories.json
@@ -591,9 +591,11 @@
"Card Validation Data Verify",
"DUKPT Derive AES Key",
"DUKPT Derive TDES Key",
+ "EMV Build ARPC Data",
"EMV Build ARQC Data",
"EMV Generate ARPC",
"EMV Generate ARQC",
+ "EMV Parse ARPC Data",
"EMV Parse ARQC Data",
"Parse EMV TLV",
"EMV Generate MAC",
diff --git a/src/core/lib/EmvArpc.mjs b/src/core/lib/EmvArpc.mjs
new file mode 100644
index 00000000..207d5176
--- /dev/null
+++ b/src/core/lib/EmvArpc.mjs
@@ -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 (0–8 bytes) → 12–20 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, 0–8 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 (0–8)", 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 24–40 hex chars (12–20 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 (0–8)", 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,
+};
diff --git a/src/core/operations/BuildEMVARPCData.mjs b/src/core/operations/BuildEMVARPCData.mjs
new file mode 100644
index 00000000..0c281941
--- /dev/null
+++ b/src/core/operations/BuildEMVARPCData.mjs
@@ -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 EMV Generate ARPC. All data comes from arguments — the input field is not used.
Method 1 (Visa, Amex, Discover, JCB): ARQC (8 bytes) || ARC (2 bytes) — 10 bytes total.
Method 2 (Mastercard M/Chip): ARQC (8 bytes) || CSU (4 bytes) || ProprietaryAuthData (0–8 bytes) — 12–20 bytes total.
Input: ignored.
Arguments: method selector plus one field per preimage element. Fields irrelevant to the selected method are ignored.
Chaining: set Output format to Hex and place this operation first in a recipe to supply the preimage directly into EMV Generate ARPC.";
+ this.inlineHelp = "Args: select method (1 = Visa/Amex, 2 = Mastercard) and fill the relevant fields. Set format to Hex 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, 0–8 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;
diff --git a/src/core/operations/ParseEMVARPCData.mjs b/src/core/operations/ParseEMVARPCData.mjs
new file mode 100644
index 00000000..ffd5f2c8
--- /dev/null
+++ b/src/core/operations/ParseEMVARPCData.mjs
@@ -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 EMV Build ARPC Data.
Method 1 (Visa, Amex, Discover, JCB): expects exactly 20 hex chars (10 bytes) — ARQC || ARC.
Method 2 (Mastercard M/Chip): expects 24–40 hex chars (12–20 bytes) — ARQC || CSU || [ProprietaryAuthData].
Input: preassembled ARPC data as hex.
Arguments: method selector and output format.";
+ this.inlineHelp = "Input: 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 (12–20 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;
diff --git a/tests/operations/tests/Payment.mjs b/tests/operations/tests/Payment.mjs
index c4d635fe..c9e5ed44 100644
--- a/tests/operations/tests/Payment.mjs
+++ b/tests/operations/tests/Payment.mjs
@@ -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 ───────────────────────────────────────────
// CDOL1 sample: Visa $10.00 USD, USA terminal, date 2026-05-21
// 9F02 000000001000 9F03 000000000000 9F1A 0840 95 0000000000