From eeb16eaaa8bd2349c0e227c12f06f43c67ea05da Mon Sep 17 00:00:00 2001 From: J8k3 Date: Thu, 21 May 2026 15:26:56 -0400 Subject: [PATCH] Add EMV Build Script Data and Build PIN Change Script Data operations Also fixes expectedError test format (OperationErrors surface as result strings, not result.error) and updates PAYMENT_RECIPES.md docs. Co-Authored-By: Claude Sonnet 4.6 --- PAYMENT_RECIPES.md | 37 +++- src/core/config/Categories.json | 2 + src/core/lib/EmvScript.mjs | 162 ++++++++++++++++++ .../BuildEMVPINChangeScriptData.mjs | 60 +++++++ src/core/operations/BuildEMVScriptData.mjs | 61 +++++++ tests/operations/tests/Payment.mjs | 76 +++++++- 6 files changed, 384 insertions(+), 14 deletions(-) create mode 100644 src/core/lib/EmvScript.mjs create mode 100644 src/core/operations/BuildEMVPINChangeScriptData.mjs create mode 100644 src/core/operations/BuildEMVScriptData.mjs 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