diff --git a/src/core/operations/GenerateKey.mjs b/src/core/operations/GenerateKey.mjs
new file mode 100644
index 00000000..41f8f5b1
--- /dev/null
+++ b/src/core/operations/GenerateKey.mjs
@@ -0,0 +1,174 @@
+/**
+ * @license Apache-2.0
+ * @author Jacob Marks [https://jacobmarks.com]
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import forge from "node-forge";
+
+// ── Key / IV specs ────────────────────────────────────────────────────────────
+
+const KEY_SPECS = {
+ "AES-128 (16 bytes)": { bytes: 16, algorithm: "A", type: "key", pciOk: true },
+ "AES-192 (24 bytes)": { bytes: 24, algorithm: "A", type: "key", pciOk: true },
+ "AES-256 (32 bytes)": { bytes: 32, algorithm: "A", type: "key", pciOk: true },
+ "TDES Double-length (16 bytes)": { bytes: 16, algorithm: "T", type: "key", pciOk: false,
+ warn: "TDES prohibited for new PIN keys since 1 January 2023 (PCI PIN Req 2-2)" },
+ "TDES Triple-length (24 bytes)": { bytes: 24, algorithm: "T", type: "key", pciOk: false,
+ warn: "TDES prohibited for new PIN keys since 1 January 2023 (PCI PIN Req 2-2)" },
+ "AES IV / Nonce (16 bytes)": { bytes: 16, algorithm: "A", type: "iv", pciOk: true },
+ "Custom random bytes (specify below)": { bytes: null, algorithm: null, type: "custom", pciOk: true },
+};
+
+// ── Helpers ───────────────────────────────────────────────────────────────────
+
+function randomBytes(n) {
+ const buf = new Uint8Array(n);
+ if (typeof globalThis !== "undefined" && globalThis.crypto && globalThis.crypto.getRandomValues) {
+ globalThis.crypto.getRandomValues(buf);
+ } else {
+ const raw = forge.random.getBytesSync(n);
+ for (let i = 0; i < n; i++) buf[i] = raw.charCodeAt(i);
+ }
+ return buf;
+}
+
+function toHex(bytes) {
+ return Array.from(bytes, b => b.toString(16).padStart(2, "0").toUpperCase()).join("");
+}
+
+function toByteStr(bytes) {
+ return Array.from(bytes, b => String.fromCharCode(b)).join("");
+}
+
+function shiftLeft1(a) {
+ const out = new Uint8Array(a.length);
+ for (let i = 0; i < a.length - 1; i++)
+ out[i] = ((a[i] << 1) | (a[i + 1] >> 7)) & 0xFF;
+ out[a.length - 1] = (a[a.length - 1] << 1) & 0xFF;
+ return out;
+}
+
+/** AES-CMAC KCV: CMAC(key, zero-block), first 3 bytes (AES-128 key). */
+function aesCmacKcv(key) {
+ const k = key.slice(0, 16);
+ const RB = new Uint8Array(16); RB[15] = 0x87;
+ const cipher = forge.cipher.createCipher("AES-ECB", toByteStr(k));
+
+ const ecb = block => {
+ cipher.start();
+ cipher.update(forge.util.createBuffer(toByteStr(block)));
+ cipher.finish();
+ return Uint8Array.from(cipher.output.getBytes(), c => c.charCodeAt(0)).slice(0, 16);
+ };
+
+ const L = ecb(new Uint8Array(16));
+ const K1 = shiftLeft1(L);
+ if (L[0] & 0x80) for (let i = 0; i < 16; i++) K1[i] ^= RB[i];
+
+ // Single full block (16 zero bytes) — complete block uses K1
+ const finalBlock = new Uint8Array(16);
+ for (let i = 0; i < 16; i++) finalBlock[i] = K1[i]; // 0x00 XOR K1[i]
+
+ return toHex(ecb(finalBlock).slice(0, 3));
+}
+
+// ── Operation ─────────────────────────────────────────────────────────────────
+
+/**
+ * Generate random payment key or IV.
+ */
+class GenerateKey extends Operation {
+
+ constructor() {
+ super();
+
+ this.name = "Generate Key";
+ this.module = "Payment";
+ this.description = [
+ "Generates a cryptographically random payment key, IV, or custom-length byte string.",
+ "
",
+ "Supported types: AES-128/192/256, TDES double/triple-length,",
+ " AES IV/Nonce (16 bytes), or a custom length for any other use.",
+ "
",
+ "For AES keys, optionally computes a CMAC KCV (3-byte, AES-CMAC of a zero block),",
+ " which is the PCI-required check value method — not the legacy ECB-zeros method.",
+ "
",
+ "Important: Keys generated in the browser are suitable for testing only.",
+ " For production, keys must be generated in an HSM or other FIPS 140-2+ approved device.",
+ ].join("");
+
+ this.inlineHelp = "Select a key type; output is hex. Use JSON output for KCV and metadata.";
+
+ this.testDataSamples = [
+ { name: "AES-128 key", input: "", args: ["AES-128 (16 bytes)", 16, true, true] },
+ ];
+
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Key / material type",
+ type: "option",
+ value: Object.keys(KEY_SPECS),
+ },
+ {
+ name: "Custom length (bytes)",
+ type: "number",
+ value: 16,
+ min: 1,
+ max: 256,
+ },
+ {
+ name: "Compute AES CMAC KCV",
+ type: "boolean",
+ value: true,
+ },
+ {
+ name: "Output as JSON",
+ type: "boolean",
+ value: false,
+ },
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const [keyType, customLength, computeKcv, outputJson] = args;
+
+ const spec = KEY_SPECS[keyType];
+ if (!spec) throw new OperationError("Unknown key / material type.");
+
+ const byteCount = spec.type === "custom" ? Math.max(1, Math.min(256, customLength)) : spec.bytes;
+ const material = randomBytes(byteCount);
+ const hex = toHex(material);
+
+ if (!outputJson) return hex;
+
+ const out = {
+ type: keyType,
+ lengthBytes: byteCount,
+ lengthBits: byteCount * 8,
+ hex,
+ };
+
+ if (spec.algorithm) out.algorithm = spec.algorithm === "A" ? "AES" : "TDES";
+ if (spec.warn) out.warning = spec.warn;
+
+ if (computeKcv && spec.algorithm === "A" && byteCount >= 16) {
+ out.kcv = aesCmacKcv(material);
+ out.kcvMethod = "AES-CMAC of 16 zero bytes, first 3 bytes (PCI PIN compliant)";
+ }
+
+ out.note = "For testing only — production keys must be generated in an approved HSM.";
+
+ return JSON.stringify(out, null, 4);
+ }
+}
+
+export default GenerateKey;