Enhance ParseTR31KeyBlock with full X9.143 field decoding and compliance checks

This commit is contained in:
Jacob Marks 2026-05-16 22:45:22 -04:00
parent 9207d79356
commit 4451ddbee3

View File

@ -6,38 +6,156 @@
import Operation from "../Operation.mjs"; import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs"; import OperationError from "../errors/OperationError.mjs";
// ── X9.143 (TR-31) lookup tables ─────────────────────────────────────────────
const VERSION_IDS = {
A: "ANSI X9.24-1 (2009) — DEA, no MAC authentication (deprecated, insecure)",
B: "ANSI X9.24-1 (2009) — TDEA, Key Derivation Binding Method",
C: "ANSI X9.24-1 (2009) — TDEA, Key Variant Binding Method",
D: "ANSI X9.24-2 (2017) — AES, Key Derivation Binding Method (current PCI standard)",
R: "AS 2805.6.1 — Australian Standard extension",
};
const KEY_USAGE_CODES = {
B0: "BDK — Base Derivation Key (DUKPT)",
B1: "Initial DUKPT Key (IK)",
B2: "Base Derivation Key, version 2",
C0: "CVK — Card Verification Key",
D0: "Symmetric Data Encryption Key (DEK)",
D1: "Asymmetric Data Encryption Key",
D2: "Data Decryption Key",
E0: "EMV Issuer Master Key — Application Cryptogram",
E1: "EMV Issuer Master Key — Secure Messaging Confidentiality",
E2: "EMV Issuer Master Key — Secure Messaging Integrity",
E3: "EMV Issuer Master Key — Data Authentication Code",
E4: "EMV Issuer Master Key — Dynamic Number",
E5: "EMV Issuer Master Key — Card Personalization",
E6: "EMV Issuer Master Key — Session Key (DEA)",
I0: "Initialization Value (IV) — Encryption",
I1: "Initialization Value (IV) — MACs",
K0: "Key Encryption or Wrapping (KEK)",
K1: "TR-34 Asymmetric RSA Key for Key Wrapping",
K2: "TR-31 Key Block Protection Key (KBPK)",
K3: "DUKPT Key (Derived Unique Key Per Transaction)",
M0: "ISO 16609 MAC — Algorithm 1 (3DEA)",
M1: "ISO 9797-1 MAC — Algorithm 1",
M2: "ISO 9797-1 MAC — Algorithm 2",
M3: "ISO 9797-1 MAC — Algorithm 3",
M4: "ISO 9797-1 MAC — Algorithm 4",
M5: "ISO 9797-1 MAC — Algorithm 5",
M6: "ISO 9797-1 MAC — Algorithm 6 (CMAC; PCI default for AES)",
M7: "HMAC",
M8: "ISO 9797-1 MAC — Algorithm 3 Padded",
P0: "PIN Encryption",
S0: "Asymmetric Key Pair for Digital Signature",
S1: "Asymmetric Key Pair — CA Certificate",
S2: "Asymmetric Key Pair — Non-X9.24",
V0: "PIN Verification Key (PVK)",
V1: "PIN Verification Key — IBM 3624 PIN Offset Method",
V2: "PIN Verification Key — Visa PVV",
V3: "PIN Verification Key — PIN Change",
V4: "PIN Verification Key — Other",
};
const ALGORITHMS = {
A: "AES",
D: "DEA (Single DES) — PROHIBITED for new keys",
E: "Elliptic Curve",
H: "HMAC",
R: "RSA",
S: "DSA",
T: "Triple DEA (3DES / TDEA)",
"0": "Not applicable",
};
const MODES_OF_USE = {
B: "Both Encrypt and Decrypt / Both Generate and Verify",
C: "Combined MAC Generate and Verify",
D: "Decrypt only",
E: "Encrypt only",
G: "MAC Generate only",
N: "No restrictions / Not applicable",
S: "Secure Messaging (Sign/Verify)",
T: "Both Sign and Decrypt (asymmetric)",
V: "MAC Verify only",
X: "Key Derivation only",
Y: "Derivation Data (e.g. session keys)",
};
const EXPORTABILITY = {
E: "Exportable — can be wrapped under a KEK in a trusted key block",
N: "Non-exportable",
S: "Sensitive — exportable only to certain authorised systems",
};
const OPTIONAL_BLOCK_IDS = {
AL: "Algorithm — algorithm override for non-standard usage",
AT: "Asymmetric key type",
BI: "Key block identifier",
CT: "Certificate type",
DA: "Derivations allowed",
DD: "Derivation data",
HM: "Hash algorithm for HMAC",
IK: "Initial Key Identifier (AES DUKPT)",
IS: "Issuer identification",
KC: "Key check value — AES CMAC",
KP: "Key parity / KCV",
KS: "KSN Descriptor (DUKPT)",
LB: "Label",
PB: "Padding block",
TS: "Time stamp",
WP: "Wrapping key padding algorithm",
};
// ── Operation ─────────────────────────────────────────────────────────────────
/** /**
* Parse TR-31 key block header operation * Parse TR-31 key block operation.
*/ */
class ParseTR31KeyBlock extends Operation { class ParseTR31KeyBlock extends Operation {
/**
* ParseTR31KeyBlock constructor
*/
constructor() { constructor() {
super(); super();
this.name = "Parse TR-31 key block"; this.name = "Parse TR-31 key block";
this.module = "Payment"; this.module = "Payment";
this.description = "Paste the full TR-31 key block into the input field as text or hex characters.<br><br><b>Input:</b> complete TR-31 key block string, with or without spaces. If your source includes a leading <code>R</code> prefix, leave <b>Trim leading R prefix</b> enabled.<br><br>This operation parses the fixed header, any optional blocks it can identify, and reports the remaining body."; this.description = [
this.inlineHelp = "<strong>Input:</strong> full TR-31 key block text.<br><strong>Args:</strong> leave the prefix trim enabled if the block starts with <code>R</code>."; "Parses a <b>TR-31</b> (ANSI X9.143) key block and decodes every header field into a human-readable description.",
"<br><br>",
"<b>Input:</b> Complete TR-31 key block string, with or without spaces.",
" Enable <b>Trim leading R prefix</b> if the block begins with a transport <code>R</code>.",
"<br><br>",
"The 16-character fixed header layout: <code>V LLLL UU A M KK X CC RR</code>",
"<br>",
"<code>V</code>=version, <code>L</code>=block length, <code>U</code>=key usage, <code>A</code>=algorithm,",
" <code>M</code>=mode of use, <code>K</code>=key version, <code>X</code>=exportability,",
" <code>C</code>=optional block count, <code>R</code>=reserved.",
"<br><br>",
"<b>Version D</b> (AES Key Derivation) is the current PCI PIN standard.",
" Versions A/B/C use TDEA or lack MAC authentication — flag for migration.",
"<br><br>",
"<b>References:</b> ANSI X9.143 / TR-31, PCI PIN v3.1 Req 18-3.",
].join("");
this.inlineHelp = "<strong>Input:</strong> full TR-31 key block text.<br><strong>Args:</strong> enable R-prefix trim if the block starts with <code>R</code>.";
this.testDataSamples = [ this.testDataSamples = [
{ {
name: "Fixed-header parser sample", name: "AES KBPK header sample",
input: "D0016D0AB00E0000", input: "D0016K2AB00E0000",
args: [true] args: [true],
} },
]; ];
this.infoURL = "https://en.wikipedia.org/wiki/Key_block"; this.infoURL = "https://en.wikipedia.org/wiki/Key_block";
this.inputType = "string"; this.inputType = "string";
this.outputType = "string"; this.outputType = "string";
this.args = [ this.args = [
{ {
"name": "Trim leading R prefix", name: "Trim leading R prefix",
"type": "boolean", type: "boolean",
"value": true, value: true,
"comment": "Enable this if your source begins with an <code>R</code> transport prefix before the TR-31 block. The parser otherwise expects the block to start at the version byte." },
}
]; ];
} }
@ -50,81 +168,122 @@ class ParseTR31KeyBlock extends Operation {
const [trimLeadingR] = args; const [trimLeadingR] = args;
let keyBlock = (input || "").replace(/\s+/g, "").toUpperCase(); let keyBlock = (input || "").replace(/\s+/g, "").toUpperCase();
const notes = []; const notes = [];
const compliance = [];
if (!keyBlock.length) { if (!keyBlock.length) throw new OperationError("No input.");
throw new OperationError("No input.");
}
if (trimLeadingR && keyBlock.startsWith("R")) { if (trimLeadingR && keyBlock.startsWith("R")) {
keyBlock = keyBlock.substring(1); keyBlock = keyBlock.substring(1);
notes.push("Removed leading R prefix."); notes.push("Removed leading R prefix.");
} }
if (keyBlock.length < 16) { if (keyBlock.length < 16) throw new OperationError("Input too short for TR-31 header (need ≥16 characters).");
throw new OperationError("Input too short for TR-31 header.");
const fixedHeader = keyBlock.substring(0, 16);
const versionId = keyBlock[0];
const declaredBlockLength = parseInt(keyBlock.substring(1, 5), 10);
const keyUsage = keyBlock.substring(5, 7);
const algorithm = keyBlock[7];
const modeOfUse = keyBlock[8];
const keyVersionNumber = keyBlock.substring(9, 11);
const exportability = keyBlock[11];
const optionalBlocksDeclared = parseInt(keyBlock.substring(12, 14), 10);
const reserved = keyBlock.substring(14, 16);
// ── Compliance checks ───────────────────────────────────────────────
if (versionId === "A") {
compliance.push("HARD STOP: Version A has no MAC authentication — vulnerable to forgery; upgrade to D");
} else if (versionId === "B" || versionId === "C") {
compliance.push("WARN: Version B/C uses TDEA — consider migrating to AES (version D) per PCI PIN 18-3");
} else if (versionId === "D") {
compliance.push("OK: Version D (AES Key Derivation) — current PCI-required format");
} }
const fixedHeader = keyBlock.substring(0, 16); if (algorithm === "D") {
const declaredBlockLength = parseInt(keyBlock.substring(1, 5), 10); compliance.push("HARD STOP: Single DES (DEA) is prohibited for all new key deployments");
const optionalBlocksDeclared = parseInt(keyBlock.substring(12, 14), 10); }
if (keyUsage === "P0" && algorithm === "T") {
compliance.push("HARD STOP: Fixed TDES PIN Encryption key — prohibited since 1 January 2023 (PCI PIN Req 2-2)");
}
if (exportability === "E") {
compliance.push("NOTE: Exportable key — verify the wrapping KEK is a PCI-approved key block protection key");
}
// ── Optional block parsing ──────────────────────────────────────────
let offset = 16; let offset = 16;
let optionalBlocksParsed = 0; let optionalBlocksParsed = 0;
const optionalBlocks = []; const optionalBlocks = [];
while (optionalBlocksParsed < optionalBlocksDeclared && offset + 4 <= keyBlock.length) { while (optionalBlocksParsed < optionalBlocksDeclared && offset + 4 <= keyBlock.length) {
const blockId = keyBlock.substring(offset, offset + 2); const blockId = keyBlock.substring(offset, offset + 2);
const blockLength = parseInt(keyBlock.substring(offset + 2, offset + 4), 10); const blockLength = parseInt(keyBlock.substring(offset + 2, offset + 4), 10);
if (!Number.isFinite(blockLength) || blockLength < 4) { if (!Number.isFinite(blockLength) || blockLength < 4) {
notes.push(`Stopped optional block parsing due to invalid block length at offset ${offset}.`); notes.push(`Stopped optional block parsing: invalid block length at offset ${offset}.`);
break; break;
} }
if (offset + blockLength > keyBlock.length) { if (offset + blockLength > keyBlock.length) {
notes.push(`Stopped optional block parsing due to truncated block at offset ${offset}.`); notes.push(`Stopped optional block parsing: truncated block at offset ${offset}.`);
break; break;
} }
optionalBlocks.push({ optionalBlocks.push({
"id": blockId, id: blockId,
"length": blockLength, idDescription: OPTIONAL_BLOCK_IDS[blockId] || "Unknown optional block type",
"value": keyBlock.substring(offset + 4, offset + blockLength) length: blockLength,
value: keyBlock.substring(offset + 4, offset + blockLength),
}); });
optionalBlocksParsed += 1; optionalBlocksParsed++;
offset += blockLength; offset += blockLength;
} }
// ── Assemble result ─────────────────────────────────────────────────
const result = { const result = {
"raw": keyBlock, raw: keyBlock,
"fixedHeader": { fixedHeader: {
"raw": fixedHeader, raw: fixedHeader,
"versionId": keyBlock.substring(0, 1), versionId,
"declaredBlockLength": Number.isFinite(declaredBlockLength) ? declaredBlockLength : null, versionDescription: VERSION_IDS[versionId] || "Unknown version ID",
"keyUsage": keyBlock.substring(5, 7), declaredBlockLength: Number.isFinite(declaredBlockLength) ? declaredBlockLength : null,
"algorithm": keyBlock.substring(7, 8), keyUsage,
"modeOfUse": keyBlock.substring(8, 9), keyUsageDescription: KEY_USAGE_CODES[keyUsage] || "Unknown key usage code",
"keyVersionNumber": keyBlock.substring(9, 11), algorithm,
"exportability": keyBlock.substring(11, 12), algorithmDescription: ALGORITHMS[algorithm] || "Unknown algorithm code",
"optionalBlocksDeclared": Number.isFinite(optionalBlocksDeclared) ? optionalBlocksDeclared : null, modeOfUse,
"reserved": keyBlock.substring(14, 16) modeOfUseDescription: MODES_OF_USE[modeOfUse] || "Unknown mode of use",
keyVersionNumber,
exportability,
exportabilityDescription: EXPORTABILITY[exportability] || "Unknown exportability code",
optionalBlocksDeclared: Number.isFinite(optionalBlocksDeclared) ? optionalBlocksDeclared : null,
reserved,
}, },
"optionalBlocks": optionalBlocks, compliance,
"bodyOffset": offset, optionalBlocks,
"remainingBody": keyBlock.substring(offset), bodyOffset: offset,
"notes": notes remainingBody: keyBlock.substring(offset),
notes,
}; };
if (result.fixedHeader.declaredBlockLength !== null && result.fixedHeader.declaredBlockLength !== keyBlock.length) { if (result.fixedHeader.declaredBlockLength !== null &&
result.notes.push(`Declared block length ${result.fixedHeader.declaredBlockLength} does not match actual length ${keyBlock.length}.`); result.fixedHeader.declaredBlockLength !== keyBlock.length) {
result.notes.push(
`Declared block length ${result.fixedHeader.declaredBlockLength} ` +
`does not match actual length ${keyBlock.length}.`
);
} }
if (result.fixedHeader.optionalBlocksDeclared !== null && result.fixedHeader.optionalBlocksDeclared !== optionalBlocks.length) { if (result.fixedHeader.optionalBlocksDeclared !== null &&
result.notes.push(`Declared optional blocks ${result.fixedHeader.optionalBlocksDeclared} but parsed ${optionalBlocks.length}.`); result.fixedHeader.optionalBlocksDeclared !== optionalBlocks.length) {
result.notes.push(
`Declared ${result.fixedHeader.optionalBlocksDeclared} optional block(s) ` +
`but parsed ${optionalBlocks.length}.`
);
} }
return JSON.stringify(result, null, 4); return JSON.stringify(result, null, 4);
} }
} }
export default ParseTR31KeyBlock; export default ParseTR31KeyBlock;