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:
parent
259b9740a7
commit
eeb16eaaa8
@ -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 |
|
||||
|
||||
@ -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",
|
||||
|
||||
162
src/core/lib/EmvScript.mjs
Normal file
162
src/core/lib/EmvScript.mjs
Normal 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,
|
||||
};
|
||||
60
src/core/operations/BuildEMVPINChangeScriptData.mjs
Normal file
60
src/core/operations/BuildEMVPINChangeScriptData.mjs
Normal 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;
|
||||
61
src/core/operations/BuildEMVScriptData.mjs
Normal file
61
src/core/operations/BuildEMVScriptData.mjs
Normal 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;
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user