diff --git a/PAYMENT_RECIPES.md b/PAYMENT_RECIPES.md
index 6af8bc60..d35dd041 100644
--- a/PAYMENT_RECIPES.md
+++ b/PAYMENT_RECIPES.md
@@ -99,20 +99,34 @@ Important assumptions:
## 4) Generate / Verify EMV ARQC And ARPC
Operations:
+- `EMV Build ARQC Data`
+- `EMV Parse ARQC Data`
- `EMV Generate ARQC`
- `EMV Verify ARQC`
- `EMV Generate ARPC`
+- `EMV Build ARPC Data`
+- `EMV Parse ARPC Data`
+- `Parse EMV TLV`
Use this when:
+- you want to assemble or inspect ARQC/ARPC preimage data by named field
- you already know the exact preassembled EMV data block
- you already have the derived EMV session key
+- you need to parse BER-TLV encoded EMV data (DE 55, ICC responses)
Input:
-- preassembled EMV cryptogram input data as hex
+- `EMV Build ARQC Data` / `EMV Build ARPC Data`: all fields supplied via args; ignores the input field — use as the first step in a chained recipe
+- `EMV Parse ARQC Data` / `EMV Parse ARPC Data`: flat hex preimage
+- `EMV Generate ARQC` / `EMV Verify ARQC` / `EMV Generate ARPC`: preassembled EMV data as hex
+- `Parse EMV TLV`: BER-TLV encoded hex (DE 55, ICC response, GPO response)
Important assumptions:
-- current coverage is the implemented AES-CMAC profile
-- these operations do not assemble CDOL data or derive issuer/session keys
+- CDOL1 structure is network-agnostic: the same 10-field 33-byte layout applies across Visa, Mastercard, Amex, Discover, and JCB
+- ARPC has two structural variants: Method 1 (Visa/Amex/Discover) and Method 2 (Mastercard) — select the correct method in the arg
+- current ARQC/ARPC coverage is the AES-CMAC profile; session-key derivation is not performed here
+
+Recommended chain:
+- `EMV Build ARQC Data` → `EMV Generate ARQC` → `EMV Verify ARQC`
## 5) Generate / Verify Card Validation Data
@@ -303,14 +317,20 @@ Flow:
## E) EMV ARQC / ARPC Review
Operations:
+- `EMV Build ARQC Data`
+- `EMV Parse ARQC Data`
- `EMV Generate ARQC`
- `EMV Verify ARQC`
+- `EMV Build ARPC Data`
+- `EMV Parse ARPC Data`
- `EMV Generate ARPC`
Flow:
-- build the exact request-data preimage outside the op
-- generate or verify the ARQC with the derived session key
-- build the response preimage and generate the ARPC
+- use `EMV Build ARQC Data` (slot 1) to assemble the CDOL1 preimage from named fields
+- generate or verify the ARQC with `EMV Generate ARQC` / `EMV Verify ARQC` using the derived session key
+- use `EMV Build ARPC Data` (slot 1 of a second recipe) to assemble the ARPC preimage
+- generate the ARPC with `EMV Generate ARPC`
+- use `EMV Parse ARQC Data` / `EMV Parse ARPC Data` to reverse-parse any flat preimage hex back to named fields
## F) EMV Script MAC And PIN Change
@@ -414,9 +434,14 @@ Release guidance: `Publish` = safe with normal guardrails; `Publish with guardra
| `EMV Generate MAC` | Vendor-aligned | AWS EMV MAC use case | Publish with guardrails |
| `EMV Verify MAC` | Vendor-aligned | AWS EMV MAC use case | Publish with guardrails |
| `EMV Generate MAC (PIN Change)` | Test helper | AWS `GenerateMacEmvPinChange` | Publish with guardrails |
+| `EMV Build ARQC Data` | Verified | CDOL1 field layout per EMV Book 3 §10.1 | Publish |
+| `EMV Parse ARQC Data` | Verified | CDOL1 field layout per EMV Book 3 §10.1 | Publish |
| `EMV Generate ARQC` | Vendor-aligned | AWS `VerifyAuthRequestCryptogram` | Publish with guardrails |
| `EMV Verify ARQC` | Vendor-aligned | AWS `VerifyAuthRequestCryptogram` | Publish with guardrails |
| `EMV Generate ARPC` | Vendor-aligned | AWS `VerifyAuthRequestCryptogram` issuer flow | Publish with guardrails |
+| `EMV Build ARPC Data` | Verified | EMV Book 2 §8.2 (Method 1); Mastercard M/Chip (Method 2) | Publish |
+| `EMV Parse ARPC Data` | Verified | EMV Book 2 §8.2 (Method 1); Mastercard M/Chip (Method 2) | Publish |
+| `Parse EMV TLV` | Verified | ISO 8825-1 BER-TLV; EMV Books 1–4; EMVCo contactless Book C | Publish |
| `Card Validation Data Generate` | Vendor-aligned | AWS `GenerateCardValidationData` | Publish with guardrails |
| `Card Validation Data Verify` | Vendor-aligned | AWS `VerifyCardValidationData` | Publish with guardrails |
| `PIN IBM 3624 Offset Generate` | Vendor-aligned | AWS IBM 3624 PIN verification object | Publish with guardrails |
diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json
index 1647e904..f3878677 100644
--- a/src/core/config/Categories.json
+++ b/src/core/config/Categories.json
@@ -593,6 +593,8 @@
"DUKPT Derive TDES Key",
"EMV Build ARPC Data",
"EMV Build ARQC Data",
+ "EMV Build PIN Change Script Data",
+ "EMV Build Script Data",
"EMV Generate ARPC",
"EMV Generate ARQC",
"EMV Parse ARPC Data",
diff --git a/src/core/lib/EmvScript.mjs b/src/core/lib/EmvScript.mjs
new file mode 100644
index 00000000..2d1dc3f8
--- /dev/null
+++ b/src/core/lib/EmvScript.mjs
@@ -0,0 +1,162 @@
+/**
+ * @license Apache-2.0
+ * @author Jacob Marks [https://jacobmarks.com]
+ */
+
+import OperationError from "../errors/OperationError.mjs";
+
+/** Issuer script command display names mapped to INS byte values. */
+const COMMAND_TO_INS = {
+ "PUT DATA": "DA",
+ "PUT DATA (ODD)": "DB",
+ "UPDATE RECORD": "DC",
+ "WRITE BINARY": "D6",
+ "CHANGE REFERENCE DATA": "24",
+ "DISABLE VERIFICATION REQUIREMENT": "26",
+ "ENABLE VERIFICATION REQUIREMENT": "28",
+ "EXTERNAL AUTHENTICATE": "82",
+};
+
+/** Ordered option list for the Command selector arg. */
+const SCRIPT_COMMANDS = Object.keys(COMMAND_TO_INS);
+
+/** PIN change mode display names mapped to P1 byte values. */
+const CHANGE_MODE_TO_P1 = {
+ "Change with current PIN verification": "00",
+ "Change without verification": "01",
+};
+
+/** Ordered option list for the Change mode selector arg. */
+const PIN_CHANGE_MODES = Object.keys(CHANGE_MODE_TO_P1);
+
+/**
+ * Validates and normalises a 1-byte hex string.
+ *
+ * @param {string} hex
+ * @param {string} name
+ * @returns {string} Upper-case 2-char hex
+ */
+function normByte(hex, name) {
+ const s = (hex || "").replace(/\s+/g, "").toUpperCase();
+ if (!/^[0-9A-F]{2}$/.test(s)) {
+ throw new OperationError(`${name} must be exactly 1 byte (2 hex chars).`);
+ }
+ return s;
+}
+
+/**
+ * Validates and normalises an arbitrary hex data string (may be empty).
+ *
+ * @param {string} hex
+ * @returns {string} Upper-case hex, possibly empty
+ */
+function normData(hex) {
+ const s = (hex || "").replace(/\s+/g, "").toUpperCase();
+ if (s && !/^[0-9A-F]+$/.test(s)) {
+ throw new OperationError("Data must be hex.");
+ }
+ if (s.length % 2 !== 0) {
+ throw new OperationError("Data must be even-length hex.");
+ }
+ return s;
+}
+
+/**
+ * Builds an issuer script command APDU from its component fields.
+ * The Lc byte is computed from the length of the supplied data.
+ *
+ * @param {string} claHex
+ * @param {string} commandName - key from SCRIPT_COMMANDS
+ * @param {string} p1Hex
+ * @param {string} p2Hex
+ * @param {string} dataHex
+ * @returns {Object} fields including the full apdu hex
+ */
+function buildScriptApdu(claHex, commandName, p1Hex, p2Hex, dataHex) {
+ const cla = normByte(claHex, "CLA");
+ const ins = COMMAND_TO_INS[commandName] || normByte(commandName, "INS");
+ const p1 = normByte(p1Hex, "P1");
+ const p2 = normByte(p2Hex, "P2");
+ const data = normData(dataHex);
+ const lcDec = data.length / 2;
+ const lc = lcDec.toString(16).padStart(2, "0").toUpperCase();
+ const apdu = `${cla}${ins}${p1}${p2}${lc}${data}`;
+ return { cla, ins, commandName, p1, p2, lc, lcDec, data, apdu };
+}
+
+/**
+ * Builds the 5-byte CHANGE REFERENCE DATA command header for PIN change.
+ * The caller supplies Lc explicitly because it must account for the
+ * encrypted PIN block and MAC bytes that follow in the final APDU.
+ *
+ * @param {string} claHex
+ * @param {string} changeMode - key from PIN_CHANGE_MODES
+ * @param {string} p2Hex
+ * @param {string} lcHex
+ * @returns {Object} fields including the 5-byte header hex
+ */
+function buildPinChangeHeader(claHex, changeMode, p2Hex, lcHex) {
+ const cla = normByte(claHex, "CLA");
+ const p1 = CHANGE_MODE_TO_P1[changeMode];
+ if (!p1) {
+ throw new OperationError(`Unknown change mode: ${changeMode}`);
+ }
+ const p2 = normByte(p2Hex, "P2");
+ const lc = normByte(lcHex, "Lc");
+ const lcDec = parseInt(lc, 16);
+ const header = `${cla}24${p1}${p2}${lc}`;
+ return { cla, ins: "24", p1, changeMode, p2, lc, lcDec, header };
+}
+
+/**
+ * Formats APDU fields as an annotated line-by-line breakdown.
+ *
+ * @param {Object} f - result of buildScriptApdu
+ * @returns {string}
+ */
+function formatAnnotatedApdu(f) {
+ const insName = COMMAND_TO_INS[f.commandName] ? f.commandName : (f.commandName || "Custom instruction");
+ const pad = 24;
+ const lines = [
+ `CLA ${f.cla.padEnd(pad)}[Class byte]`,
+ `INS ${f.ins.padEnd(pad)}[${insName}]`,
+ `P1 ${f.p1.padEnd(pad)}[Parameter 1]`,
+ `P2 ${f.p2.padEnd(pad)}[Parameter 2]`,
+ `Lc ${f.lc.padEnd(pad)}[${f.lcDec} byte${f.lcDec === 1 ? "" : "s"} of data]`,
+ ];
+ if (f.data) {
+ lines.push(`Data ${f.data.padEnd(pad)}[Command data]`);
+ }
+ lines.push("─".repeat(40));
+ lines.push(`APDU ${f.apdu}`);
+ return lines.join("\n");
+}
+
+/**
+ * Formats PIN change header fields as an annotated breakdown.
+ *
+ * @param {Object} f - result of buildPinChangeHeader
+ * @returns {string}
+ */
+function formatAnnotatedPinChangeHeader(f) {
+ const pad = 24;
+ const lines = [
+ `CLA ${f.cla.padEnd(pad)}[Class byte]`,
+ `INS ${"24".padEnd(pad)}[CHANGE REFERENCE DATA]`,
+ `P1 ${f.p1.padEnd(pad)}[${f.changeMode}]`,
+ `P2 ${f.p2.padEnd(pad)}[PIN reference]`,
+ `Lc ${f.lc.padEnd(pad)}[${f.lcDec} bytes total: PIN block + MAC]`,
+ "─".repeat(40),
+ `Header ${f.header} [Feed as message into EMV Generate MAC (PIN Change)]`,
+ ];
+ return lines.join("\n");
+}
+
+export {
+ SCRIPT_COMMANDS,
+ PIN_CHANGE_MODES,
+ buildScriptApdu,
+ buildPinChangeHeader,
+ formatAnnotatedApdu,
+ formatAnnotatedPinChangeHeader,
+};
diff --git a/src/core/operations/BuildEMVPINChangeScriptData.mjs b/src/core/operations/BuildEMVPINChangeScriptData.mjs
new file mode 100644
index 00000000..6db4e0cb
--- /dev/null
+++ b/src/core/operations/BuildEMVPINChangeScriptData.mjs
@@ -0,0 +1,60 @@
+/**
+ * @license Apache-2.0
+ * @author Jacob Marks [https://jacobmarks.com]
+ */
+
+import Operation from "../Operation.mjs";
+import { PIN_CHANGE_MODES, buildPinChangeHeader, formatAnnotatedPinChangeHeader } from "../lib/EmvScript.mjs";
+
+/**
+ * Build EMV PIN Change Script Data operation.
+ */
+class BuildEMVPINChangeScriptData extends Operation {
+ /**
+ * BuildEMVPINChangeScriptData constructor.
+ */
+ constructor() {
+ super();
+
+ this.name = "EMV Build PIN Change Script Data";
+ this.module = "Payment";
+ this.description = "Assembles the 5-byte CHANGE REFERENCE DATA (INS=24) command header for a PIN-change issuer script. Use this as the first step in a recipe — the hex output feeds into the EMV Generate MAC (PIN Change) input field, which appends the encrypted PIN block before computing the MAC.
Output: CLA 24 P1 P2 Lc (5 bytes). The Lc field must cover all data bytes that follow in the final APDU: typically 8 bytes for the encrypted PIN block plus 8 bytes for the MAC = 0x10.
P1: 00 = change requires current PIN verification; 01 = change without verification.
P2: PIN reference — 80 is the global PIN reference used by most EMV cards.
Security: Software emulation for testing only.";
+ this.inlineHelp = "Output: 5-byte CHANGE REFERENCE DATA header. Feed into EMV Generate MAC (PIN Change) as input.";
+ this.testDataSamples = [
+ {
+ name: "PIN change header sample",
+ input: "",
+ args: ["84", "Change with current PIN verification", "80", "10", "Hex"]
+ }
+ ];
+ this.infoURL = "https://en.wikipedia.org/wiki/EMV";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ { name: "CLA (hex)", type: "string", value: "84", comment: "Class byte. 84 = secure messaging with key from current DF (standard for issuer scripts)." },
+ { name: "Change mode (P1)", type: "option", value: PIN_CHANGE_MODES, comment: "P1=00: change requires verification with the current PIN. P1=01: change without current PIN verification." },
+ { name: "PIN reference (P2, hex)", type: "string", value: "80", comment: "PIN reference data qualifier. 80 = global PIN reference (most EMV cards). Check card spec for other values." },
+ { name: "Lc (hex)", type: "string", value: "10", comment: "Total data length in the final APDU. Default 10 (hex) = 16 bytes: 8-byte encrypted PIN block + 8-byte MAC." },
+ { name: "Output format", type: "option", value: ["Hex", "JSON", "Annotated"], comment: "Hex: header ready to chain. JSON: named fields. Annotated: field-by-field breakdown." },
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const [claHex, changeMode, p2Hex, lcHex, outputFormat] = args;
+ const f = buildPinChangeHeader(claHex, changeMode, p2Hex, lcHex);
+ if (outputFormat === "JSON") {
+ return JSON.stringify({ cla: f.cla, ins: f.ins, p1: f.p1, p2: f.p2, lc: f.lc, header: f.header }, null, 4);
+ }
+ if (outputFormat === "Annotated") {
+ return formatAnnotatedPinChangeHeader(f);
+ }
+ return f.header;
+ }
+}
+
+export default BuildEMVPINChangeScriptData;
diff --git a/src/core/operations/BuildEMVScriptData.mjs b/src/core/operations/BuildEMVScriptData.mjs
new file mode 100644
index 00000000..fe0dbaac
--- /dev/null
+++ b/src/core/operations/BuildEMVScriptData.mjs
@@ -0,0 +1,61 @@
+/**
+ * @license Apache-2.0
+ * @author Jacob Marks [https://jacobmarks.com]
+ */
+
+import Operation from "../Operation.mjs";
+import { SCRIPT_COMMANDS, buildScriptApdu, formatAnnotatedApdu } from "../lib/EmvScript.mjs";
+
+/**
+ * Build EMV Script Data operation.
+ */
+class BuildEMVScriptData extends Operation {
+ /**
+ * BuildEMVScriptData constructor.
+ */
+ constructor() {
+ super();
+
+ this.name = "EMV Build Script Data";
+ this.module = "Payment";
+ this.description = "Assembles an issuer-script command APDU from named fields. Use this as the first step in a recipe chain — the hex output feeds directly into the EMV Generate MAC input field.
Output: CLA | INS | P1 | P2 | Lc | Data — Lc is computed automatically from the data length.
Common INS values: DA=PUT DATA, DB=PUT DATA (ODD), DC=UPDATE RECORD, D6=WRITE BINARY, 26=DISABLE VERIFICATION, 28=ENABLE VERIFICATION, 82=EXTERNAL AUTHENTICATE.
Security: Software emulation for testing only.";
+ this.inlineHelp = "Output: CLA INS P1 P2 Lc Data APDU hex. Feed into EMV Generate MAC as input.";
+ this.testDataSamples = [
+ {
+ name: "PUT DATA sample",
+ input: "",
+ args: ["84", "PUT DATA", "00", "42", "0102030405060708090A", "Hex"]
+ }
+ ];
+ this.infoURL = "https://en.wikipedia.org/wiki/EMV";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ { name: "CLA (hex)", type: "string", value: "84", comment: "Class byte. 84 = secure messaging with key from current DF (standard for issuer scripts)." },
+ { name: "Command", type: "option", value: SCRIPT_COMMANDS, comment: "Selects the INS byte. Common issuer script commands: PUT DATA (DA/DB), UPDATE RECORD (DC), WRITE BINARY (D6)." },
+ { name: "P1 (hex)", type: "string", value: "00", comment: "Parameter 1. Meaning depends on command: record number for UPDATE RECORD, data reference for PUT DATA." },
+ { name: "P2 (hex)", type: "string", value: "00", comment: "Parameter 2. Meaning depends on command: SFI+record selector for UPDATE RECORD, data object tag low byte for PUT DATA." },
+ { name: "Data (hex)", type: "string", value: "", comment: "Command data payload. Lc is computed automatically from the length." },
+ { name: "Output format", type: "option", value: ["Hex", "JSON", "Annotated"], comment: "Hex: APDU ready to chain. JSON: named fields. Annotated: field-by-field breakdown." },
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const [claHex, commandName, p1Hex, p2Hex, dataHex, outputFormat] = args;
+ const f = buildScriptApdu(claHex, commandName, p1Hex, p2Hex, dataHex);
+ if (outputFormat === "JSON") {
+ return JSON.stringify({ cla: f.cla, ins: f.ins, p1: f.p1, p2: f.p2, lc: f.lc, data: f.data, apdu: f.apdu }, null, 4);
+ }
+ if (outputFormat === "Annotated") {
+ return formatAnnotatedApdu(f);
+ }
+ return f.apdu;
+ }
+}
+
+export default BuildEMVScriptData;
diff --git a/tests/operations/tests/Payment.mjs b/tests/operations/tests/Payment.mjs
index c9e5ed44..2351272a 100644
--- a/tests/operations/tests/Payment.mjs
+++ b/tests/operations/tests/Payment.mjs
@@ -888,8 +888,7 @@ TestRegister.addTests([
{
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.",
+ expectedOutput: "Method 1 preimage requires 20 hex chars (10 bytes); got 8.",
recipeConfig: [{
op: "EMV Parse ARPC Data",
args: ["Method 1 (Visa/Amex/Discover)", "JSON"]
@@ -1004,8 +1003,7 @@ TestRegister.addTests([
{
name: "EMV Build ARQC Data: bad field length throws",
input: "",
- expectedError: true,
- expectedOutput: "Error: Amount Authorised: expected 12 hex chars (6 bytes), got 4.",
+ expectedOutput: "Amount Authorised: expected 12 hex chars (6 bytes), got 4.",
recipeConfig: [
{
op: "EMV Build ARQC Data",
@@ -1016,8 +1014,7 @@ TestRegister.addTests([
{
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.",
+ expectedOutput: "Standard CDOL1 requires 66 hex chars (33 bytes); got 12.",
recipeConfig: [
{
op: "EMV Parse ARQC Data",
@@ -1582,11 +1579,74 @@ TestRegister.addTests([
{
name: "Parse EMV TLV: bad hex throws",
input: "GG",
- expectedError: true,
- expectedOutput: "Error: Input is not valid hex (odd length or non-hex chars).",
+ expectedOutput: "Input is not valid hex (odd length or non-hex chars).",
recipeConfig: [{ op: "Parse EMV TLV", args: [false] }]
},
+ // ── EMV Build Script Data ─────────────────────────────────────────────────
+ {
+ name: "EMV Build Script Data: PUT DATA hex output",
+ input: "",
+ expectedOutput: "84DA00420A0102030405060708090A",
+ recipeConfig: [{ op: "EMV Build Script Data", args: ["84", "PUT DATA", "00", "42", "0102030405060708090A", "Hex"] }]
+ },
+ {
+ name: "EMV Build Script Data: PUT DATA JSON output",
+ input: "",
+ expectedOutput: JSON.stringify({ cla: "84", ins: "DA", p1: "00", p2: "42", lc: "0A", data: "0102030405060708090A", apdu: "84DA00420A0102030405060708090A" }, null, 4),
+ recipeConfig: [{ op: "EMV Build Script Data", args: ["84", "PUT DATA", "00", "42", "0102030405060708090A", "JSON"] }]
+ },
+ {
+ name: "EMV Build Script Data: empty data (DISABLE VERIFICATION REQUIREMENT)",
+ input: "",
+ expectedOutput: "8426000000",
+ recipeConfig: [{ op: "EMV Build Script Data", args: ["84", "DISABLE VERIFICATION REQUIREMENT", "00", "00", "", "Hex"] }]
+ },
+ {
+ name: "EMV Build Script Data: annotated output includes APDU line",
+ input: "",
+ expectedMatch: /APDU\s+84DC/,
+ recipeConfig: [{ op: "EMV Build Script Data", args: ["84", "UPDATE RECORD", "01", "04", "AABB", "Annotated"] }]
+ },
+ {
+ name: "EMV Build Script Data: bad CLA throws",
+ input: "",
+ expectedOutput: "CLA must be exactly 1 byte (2 hex chars).",
+ recipeConfig: [{ op: "EMV Build Script Data", args: ["8400", "PUT DATA", "00", "00", "", "Hex"] }]
+ },
+ {
+ name: "EMV Build Script Data: odd-length data throws",
+ input: "",
+ expectedOutput: "Data must be even-length hex.",
+ recipeConfig: [{ op: "EMV Build Script Data", args: ["84", "PUT DATA", "00", "00", "ABC", "Hex"] }]
+ },
+
+ // ── EMV Build PIN Change Script Data ──────────────────────────────────────
+ {
+ name: "EMV Build PIN Change Script Data: hex output (P1=00)",
+ input: "",
+ expectedOutput: "8424008010",
+ recipeConfig: [{ op: "EMV Build PIN Change Script Data", args: ["84", "Change with current PIN verification", "80", "10", "Hex"] }]
+ },
+ {
+ name: "EMV Build PIN Change Script Data: hex output (P1=01, no-verify)",
+ input: "",
+ expectedOutput: "8424018010",
+ recipeConfig: [{ op: "EMV Build PIN Change Script Data", args: ["84", "Change without verification", "80", "10", "Hex"] }]
+ },
+ {
+ name: "EMV Build PIN Change Script Data: JSON output",
+ input: "",
+ expectedOutput: JSON.stringify({ cla: "84", ins: "24", p1: "00", p2: "80", lc: "10", header: "8424008010" }, null, 4),
+ recipeConfig: [{ op: "EMV Build PIN Change Script Data", args: ["84", "Change with current PIN verification", "80", "10", "JSON"] }]
+ },
+ {
+ name: "EMV Build PIN Change Script Data: bad Lc throws",
+ input: "",
+ expectedOutput: "Lc must be exactly 1 byte (2 hex chars).",
+ recipeConfig: [{ op: "EMV Build PIN Change Script Data", args: ["84", "Change with current PIN verification", "80", "GG", "Hex"] }]
+ },
+
// ── PIN Block Translate Encrypted ─────────────────────────────────────────
// Vectors: PIN=1234, PAN=5432101234567890
// clear Format 0 block : 041215FEDCBA9876