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 <noreply@anthropic.com>
This commit is contained in:
J8k3 2026-05-21 15:26:56 -04:00
parent 259b9740a7
commit eeb16eaaa8
6 changed files with 384 additions and 14 deletions

View File

@ -99,20 +99,34 @@ Important assumptions:
## 4) Generate / Verify EMV ARQC And ARPC ## 4) Generate / Verify EMV ARQC And ARPC
Operations: Operations:
- `EMV Build ARQC Data`
- `EMV Parse ARQC Data`
- `EMV Generate ARQC` - `EMV Generate ARQC`
- `EMV Verify ARQC` - `EMV Verify ARQC`
- `EMV Generate ARPC` - `EMV Generate ARPC`
- `EMV Build ARPC Data`
- `EMV Parse ARPC Data`
- `Parse EMV TLV`
Use this when: 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 know the exact preassembled EMV data block
- you already have the derived EMV session key - you already have the derived EMV session key
- you need to parse BER-TLV encoded EMV data (DE 55, ICC responses)
Input: 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: Important assumptions:
- current coverage is the implemented AES-CMAC profile - CDOL1 structure is network-agnostic: the same 10-field 33-byte layout applies across Visa, Mastercard, Amex, Discover, and JCB
- these operations do not assemble CDOL data or derive issuer/session keys - 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 ## 5) Generate / Verify Card Validation Data
@ -303,14 +317,20 @@ Flow:
## E) EMV ARQC / ARPC Review ## E) EMV ARQC / ARPC Review
Operations: Operations:
- `EMV Build ARQC Data`
- `EMV Parse ARQC Data`
- `EMV Generate ARQC` - `EMV Generate ARQC`
- `EMV Verify ARQC` - `EMV Verify ARQC`
- `EMV Build ARPC Data`
- `EMV Parse ARPC Data`
- `EMV Generate ARPC` - `EMV Generate ARPC`
Flow: Flow:
- build the exact request-data preimage outside the op - use `EMV Build ARQC Data` (slot 1) to assemble the CDOL1 preimage from named fields
- generate or verify the ARQC with the derived session key - generate or verify the ARQC with `EMV Generate ARQC` / `EMV Verify ARQC` using the derived session key
- build the response preimage and generate the ARPC - 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 ## 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 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 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 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 Generate ARQC` | Vendor-aligned | AWS `VerifyAuthRequestCryptogram` | Publish with guardrails |
| `EMV Verify 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 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 14; EMVCo contactless Book C | Publish |
| `Card Validation Data Generate` | Vendor-aligned | AWS `GenerateCardValidationData` | Publish with guardrails | | `Card Validation Data Generate` | Vendor-aligned | AWS `GenerateCardValidationData` | Publish with guardrails |
| `Card Validation Data Verify` | Vendor-aligned | AWS `VerifyCardValidationData` | 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 | | `PIN IBM 3624 Offset Generate` | Vendor-aligned | AWS IBM 3624 PIN verification object | Publish with guardrails |

View File

@ -593,6 +593,8 @@
"DUKPT Derive TDES Key", "DUKPT Derive TDES Key",
"EMV Build ARPC Data", "EMV Build ARPC Data",
"EMV Build ARQC Data", "EMV Build ARQC Data",
"EMV Build PIN Change Script Data",
"EMV Build Script Data",
"EMV Generate ARPC", "EMV Generate ARPC",
"EMV Generate ARQC", "EMV Generate ARQC",
"EMV Parse ARPC Data", "EMV Parse ARPC Data",

162
src/core/lib/EmvScript.mjs Normal file
View File

@ -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,
};

View File

@ -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 <b>EMV Generate MAC (PIN Change)</b> input field, which appends the encrypted PIN block before computing the MAC.<br><br><b>Output:</b> <code>CLA 24 P1 P2 Lc</code> (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.<br><br><b>P1:</b> 00 = change requires current PIN verification; 01 = change without verification.<br><b>P2:</b> PIN reference — 80 is the global PIN reference used by most EMV cards.<br><br><b>Security:</b> Software emulation for testing only.";
this.inlineHelp = "<strong>Output:</strong> 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;

View File

@ -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 <b>EMV Generate MAC</b> input field.<br><br><b>Output:</b> <code>CLA | INS | P1 | P2 | Lc | Data</code> — Lc is computed automatically from the data length.<br><br><b>Common INS values:</b> DA=PUT DATA, DB=PUT DATA (ODD), DC=UPDATE RECORD, D6=WRITE BINARY, 26=DISABLE VERIFICATION, 28=ENABLE VERIFICATION, 82=EXTERNAL AUTHENTICATE.<br><br><b>Security:</b> Software emulation for testing only.";
this.inlineHelp = "<strong>Output:</strong> 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;

View File

@ -888,8 +888,7 @@ TestRegister.addTests([
{ {
name: "EMV Parse ARPC Data: wrong length for Method 1 throws", name: "EMV Parse ARPC Data: wrong length for Method 1 throws",
input: "A1B2C3D4", input: "A1B2C3D4",
expectedError: true, expectedOutput: "Method 1 preimage requires 20 hex chars (10 bytes); got 8.",
expectedOutput: "Error: Method 1 preimage requires 20 hex chars (10 bytes); got 8.",
recipeConfig: [{ recipeConfig: [{
op: "EMV Parse ARPC Data", op: "EMV Parse ARPC Data",
args: ["Method 1 (Visa/Amex/Discover)", "JSON"] args: ["Method 1 (Visa/Amex/Discover)", "JSON"]
@ -1004,8 +1003,7 @@ TestRegister.addTests([
{ {
name: "EMV Build ARQC Data: bad field length throws", name: "EMV Build ARQC Data: bad field length throws",
input: "", input: "",
expectedError: true, expectedOutput: "Amount Authorised: expected 12 hex chars (6 bytes), got 4.",
expectedOutput: "Error: Amount Authorised: expected 12 hex chars (6 bytes), got 4.",
recipeConfig: [ recipeConfig: [
{ {
op: "EMV Build ARQC Data", op: "EMV Build ARQC Data",
@ -1016,8 +1014,7 @@ TestRegister.addTests([
{ {
name: "EMV Parse ARQC Data: too-short input throws", name: "EMV Parse ARQC Data: too-short input throws",
input: "000000001000", input: "000000001000",
expectedError: true, expectedOutput: "Standard CDOL1 requires 66 hex chars (33 bytes); got 12.",
expectedOutput: "Error: Standard CDOL1 requires 66 hex chars (33 bytes); got 12.",
recipeConfig: [ recipeConfig: [
{ {
op: "EMV Parse ARQC Data", op: "EMV Parse ARQC Data",
@ -1582,11 +1579,74 @@ TestRegister.addTests([
{ {
name: "Parse EMV TLV: bad hex throws", name: "Parse EMV TLV: bad hex throws",
input: "GG", input: "GG",
expectedError: true, expectedOutput: "Input is not valid hex (odd length or non-hex chars).",
expectedOutput: "Error: Input is not valid hex (odd length or non-hex chars).",
recipeConfig: [{ op: "Parse EMV TLV", args: [false] }] 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 ───────────────────────────────────────── // ── PIN Block Translate Encrypted ─────────────────────────────────────────
// Vectors: PIN=1234, PAN=5432101234567890 // Vectors: PIN=1234, PAN=5432101234567890
// clear Format 0 block : 041215FEDCBA9876 // clear Format 0 block : 041215FEDCBA9876