- CalculatePaymentKCV: fix HMAC SHA-224 using SHA-512/224 (forge.md.sha512.sha224) instead of standard SHA-224 (now uses "sha224" string, consistent with other HMAC methods) - PaymentMac: collapse 3-clause DUKPT variant ternary to single expression; the ISO 9797-1 fallback to "MAC Request" was already correct and is now explicit - AGENTS.md: renumber steps 1-7 sequentially; add step 6 — review and update this.description/inlineHelp/testDataSamples whenever changing a recipe Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
153 lines
6.3 KiB
JavaScript
153 lines
6.3 KiB
JavaScript
/**
|
|
* @license Apache-2.0
|
|
* @author Jacob Marks [https://jacobmarks.com]
|
|
*/
|
|
|
|
import forge from "node-forge";
|
|
import Operation from "../Operation.mjs";
|
|
import OperationError from "../errors/OperationError.mjs";
|
|
import Utils from "../Utils.mjs";
|
|
import CMAC from "./CMAC.mjs";
|
|
|
|
/**
|
|
* Calculate payment KCV operation
|
|
*/
|
|
class CalculatePaymentKCV extends Operation {
|
|
|
|
/**
|
|
* CalculatePaymentKCV constructor
|
|
*/
|
|
constructor() {
|
|
super();
|
|
|
|
this.name = "Payment Calculate KCV";
|
|
this.module = "Payment";
|
|
this.description = "Paste the key into the input field and choose how that key is encoded using <b>Key format</b>.<br><br>Use <b>Method</b> to choose the KCV style: TDES, AES-CMAC, AES-ECB, or HMAC.<br><br><b>Input:</b> raw key material such as hex, UTF-8, Latin1, or Base64.<br><b>Arguments:</b> select the key format, method, and output length in hex characters.<br><br>Returns an uppercase truncated hex KCV value.";
|
|
this.inlineHelp = "<strong>Input:</strong> key material.<br><strong>Args:</strong> tell the op how the key is encoded, choose the KCV method, then set the output length.";
|
|
this.testDataSamples = [
|
|
{
|
|
name: "Random AES-CMAC sample",
|
|
input: "__RANDOM_AES_128_HEX__",
|
|
args: ["Hex", "AES-CMAC (Empty)", 6]
|
|
},
|
|
{
|
|
name: "Generate key then compute KCV",
|
|
recipeConfig: [
|
|
{ op: "Key Generate", args: ["AES-128 (16 bytes)", 16, false, false] },
|
|
{ op: "Payment Calculate KCV", args: ["Hex", "AES-CMAC (Empty)", 6] }
|
|
]
|
|
}
|
|
];
|
|
this.infoURL = "https://en.wikipedia.org/wiki/Message_authentication_code";
|
|
this.inputType = "string";
|
|
this.outputType = "string";
|
|
this.args = [
|
|
{
|
|
"name": "Key format",
|
|
"type": "option",
|
|
"value": ["Hex", "UTF8", "Latin1", "Base64"],
|
|
"comment": "How the input field should be decoded before KCV calculation. Use <code>Hex</code> for payment keys entered as hexadecimal characters."
|
|
},
|
|
{
|
|
"name": "Method",
|
|
"type": "option",
|
|
"value": ["TDES-ECB (Zeros)", "AES-CMAC (Empty)", "AES-CMAC (Zeros)", "AES-CMAC (Ones)", "AES-ECB (Zeros)", "HMAC SHA-224", "HMAC SHA-256", "HMAC SHA-384", "HMAC SHA-512"],
|
|
"comment": "Assumption: TDES expects a 16-byte or 24-byte key, AES expects 16/24/32 bytes, and the method name states the exact data block used for the KCV."
|
|
},
|
|
{
|
|
"name": "Output hex chars",
|
|
"type": "number",
|
|
"value": 6,
|
|
"comment": "Number of uppercase hex characters returned from the left side of the calculated value. Common payment KCV length is <code>6</code>."
|
|
}
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param {string} input
|
|
* @param {Object[]} args
|
|
* @returns {string}
|
|
*/
|
|
run(input, args) {
|
|
const [keyFormat, method, outputHexChars] = args;
|
|
const truncLength = Math.max(1, Number(outputHexChars) || 6);
|
|
const keyBytes = Utils.convertToByteString(input || "", keyFormat);
|
|
|
|
if (!keyBytes.length) {
|
|
throw new OperationError("No key material was provided.");
|
|
}
|
|
|
|
let hexOut;
|
|
|
|
switch (method) {
|
|
case "TDES-ECB (Zeros)": {
|
|
if (keyBytes.length !== 16 && keyBytes.length !== 24) {
|
|
throw new OperationError("TDES key must be 16 or 24 bytes.");
|
|
}
|
|
const key = keyBytes.length === 16 ? keyBytes + keyBytes.substring(0, 8) : keyBytes;
|
|
const cipher = forge.cipher.createCipher("3DES-ECB", key);
|
|
cipher.start();
|
|
cipher.update(forge.util.createBuffer("\x00\x00\x00\x00\x00\x00\x00\x00"));
|
|
cipher.finish();
|
|
hexOut = cipher.output.toHex().toUpperCase();
|
|
break;
|
|
}
|
|
case "AES-CMAC (Empty)":
|
|
case "AES-CMAC (Zeros)":
|
|
case "AES-CMAC (Ones)": {
|
|
if (keyBytes.length !== 16 && keyBytes.length !== 24 && keyBytes.length !== 32) {
|
|
throw new OperationError("AES key must be 16, 24, or 32 bytes.");
|
|
}
|
|
const cmacOp = new CMAC();
|
|
let data;
|
|
if (method === "AES-CMAC (Empty)") {
|
|
data = new Uint8Array(0).buffer;
|
|
} else if (method === "AES-CMAC (Zeros)") {
|
|
data = new Uint8Array(16).buffer;
|
|
} else {
|
|
data = Uint8Array.from(new Array(16).fill(0xFF)).buffer;
|
|
}
|
|
hexOut = cmacOp.run(data, [{ string: keyBytes, option: "Latin1" }, "AES"]).toUpperCase();
|
|
break;
|
|
}
|
|
case "AES-ECB (Zeros)": {
|
|
if (keyBytes.length !== 16 && keyBytes.length !== 24 && keyBytes.length !== 32) {
|
|
throw new OperationError("AES key must be 16, 24, or 32 bytes.");
|
|
}
|
|
const cipher = forge.cipher.createCipher("AES-ECB", keyBytes);
|
|
cipher.mode.pad = function() {
|
|
return true;
|
|
};
|
|
cipher.start();
|
|
cipher.update(forge.util.createBuffer("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"));
|
|
cipher.finish();
|
|
hexOut = cipher.output.toHex().toUpperCase();
|
|
break;
|
|
}
|
|
case "HMAC SHA-224":
|
|
case "HMAC SHA-256":
|
|
case "HMAC SHA-384":
|
|
case "HMAC SHA-512": {
|
|
const algorithmMap = {
|
|
"HMAC SHA-224": "sha224",
|
|
"HMAC SHA-256": "sha256",
|
|
"HMAC SHA-384": "sha384",
|
|
"HMAC SHA-512": "sha512"
|
|
};
|
|
const hmac = forge.hmac.create();
|
|
hmac.start(algorithmMap[method], keyBytes);
|
|
hmac.update("");
|
|
hexOut = hmac.digest().toHex().toUpperCase();
|
|
break;
|
|
}
|
|
default:
|
|
throw new OperationError("Unsupported method.");
|
|
}
|
|
|
|
return hexOut.substring(0, truncLength);
|
|
}
|
|
|
|
}
|
|
|
|
export default CalculatePaymentKCV;
|