From 20c60aaa1cf32ea53c99b0e511de1225fcabe83e Mon Sep 17 00:00:00 2001 From: J8k3 Date: Sat, 11 Jul 2026 19:30:53 -0400 Subject: [PATCH] Add Key Check Value operation Verifies that two parties hold the same symmetric key without exposing it. Supports the three standard forms: TDES-ECB (ANSI X9.24-1), AES-ECB, and AES-CMAC of the empty message (RFC 4493 / TR-31 KC block). Test vectors sourced from NIST SP 800-67 Rev 2 and NIST SP 800-38B. --- src/core/config/Categories.json | 1 + src/core/operations/KeyCheckValue.mjs | 122 +++++++++++++++++++++++ tests/operations/tests/KeyCheckValue.mjs | 92 +++++++++++++++++ 3 files changed, 215 insertions(+) create mode 100644 src/core/operations/KeyCheckValue.mjs create mode 100644 tests/operations/tests/KeyCheckValue.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index c652ed48..91ee1f52 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -466,6 +466,7 @@ "Compare CTPH hashes", "HMAC", "CMAC", + "Key Check Value", "Bcrypt", "Bcrypt compare", "Bcrypt parse", diff --git a/src/core/operations/KeyCheckValue.mjs b/src/core/operations/KeyCheckValue.mjs new file mode 100644 index 00000000..1a8c98f9 --- /dev/null +++ b/src/core/operations/KeyCheckValue.mjs @@ -0,0 +1,122 @@ +/** + * @author J8k3 [https://jacobmarks.com] + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +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"; + +/** + * Key Check Value operation. + * + * A KCV is a short public value derived from a symmetric key so operators can + * verify two systems hold the same key without exposing the key itself. + * + * Three standard methods are supported: + * - TDES-ECB (Zeros): ANSI X9.24-1 legacy KCV — encrypt an 8-byte zero block. + * - AES-ECB (Zeros): encrypt a 16-byte zero block, an AES analogue of the TDES form. + * - AES-CMAC (Empty): RFC 4493 / TR-31 KC-block KCV — CMAC of the empty message. + */ +class KeyCheckValue extends Operation { + + /** + * KeyCheckValue constructor + */ + constructor() { + super(); + + this.name = "Key Check Value"; + this.module = "Crypto"; + this.description = "Computes a Key Check Value (KCV) for a symmetric key. A KCV lets two parties verify they hold the same key without revealing it.

Returns the leading N hex characters of the resulting cryptogram."; + this.infoURL = "https://wikipedia.org/wiki/Key_checksum_value"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + "name": "Key", + "type": "toggleString", + "value": "", + "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"] + }, + { + "name": "Method", + "type": "option", + "value": ["TDES-ECB (Zeros)", "AES-ECB (Zeros)", "AES-CMAC (Empty)"] + }, + { + "name": "Output length (hex chars)", + "type": "number", + "value": 6 + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [keyArg, method, outputHexChars] = args; + const keyBytes = Utils.convertToByteString(keyArg.string, keyArg.option); + + if (!keyBytes.length) { + throw new OperationError("No key material was provided."); + } + + const truncLength = Math.max(1, Number(outputHexChars) || 6); + 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 (currently " + keyBytes.length + " bytes)."); + } + const key = keyBytes.length === 16 ? keyBytes + keyBytes.substring(0, 8) : keyBytes; + const cipher = forge.cipher.createCipher("3DES-ECB", key); + cipher.mode.pad = function() { + return true; + }; + 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-ECB (Zeros)": { + if (keyBytes.length !== 16 && keyBytes.length !== 24 && keyBytes.length !== 32) { + throw new OperationError("AES key must be 16, 24, or 32 bytes (currently " + keyBytes.length + " 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 "AES-CMAC (Empty)": { + if (keyBytes.length !== 16 && keyBytes.length !== 24 && keyBytes.length !== 32) { + throw new OperationError("AES key must be 16, 24, or 32 bytes (currently " + keyBytes.length + " bytes)."); + } + const cmacOp = new CMAC(); + const emptyInput = new ArrayBuffer(0); + hexOut = cmacOp.run(emptyInput, [{string: keyBytes, option: "Latin1"}, "AES"]).toUpperCase(); + break; + } + default: + throw new OperationError("Unsupported method: " + method); + } + + return hexOut.substring(0, truncLength); + } + +} + +export default KeyCheckValue; diff --git a/tests/operations/tests/KeyCheckValue.mjs b/tests/operations/tests/KeyCheckValue.mjs new file mode 100644 index 00000000..46d5c000 --- /dev/null +++ b/tests/operations/tests/KeyCheckValue.mjs @@ -0,0 +1,92 @@ +/** + * @author J8k3 [https://jacobmarks.com] + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +// TDES-ECB (Zeros) — key from NIST SP 800-67 Rev 2 §B.1, "Numerical Example for TDEA". +// AES-ECB (Zeros) and AES-CMAC (Empty) — key from NIST SP 800-38B "Recommendation for +// Block Cipher Modes of Operation: The CMAC Mode for Authentication", Appendix D.1 +// (also RFC 4493 §4). The AES-CMAC empty-message MAC 0xbb1d6929e95937287fa37d129b756746 +// is corroborated by the existing "CMAC-AES128 NIST's CSRC Example #1" test. + +TestRegister.addTests([ + { + "name": "Key Check Value: TDES-ECB (Zeros), NIST SP 800-67 key", + "input": "0123456789ABCDEF23456789ABCDEF01456789ABCDEF0123", + "expectedOutput": "4EBA73", + "recipeConfig": [ + { + "op": "Key Check Value", + "args": [{"option": "Hex", "string": "0123456789ABCDEF23456789ABCDEF01456789ABCDEF0123"}, "TDES-ECB (Zeros)", 6] + } + ] + }, + { + "name": "Key Check Value: TDES-ECB (Zeros), double-length 16-byte key expanded to k1||k2||k1", + "input": "0123456789ABCDEF23456789ABCDEF01", + "expectedOutput": "86E965", + "recipeConfig": [ + { + "op": "Key Check Value", + "args": [{"option": "Hex", "string": "0123456789ABCDEF23456789ABCDEF01"}, "TDES-ECB (Zeros)", 6] + } + ] + }, + { + "name": "Key Check Value: AES-ECB (Zeros), NIST SP 800-38B AES-128 key", + "input": "2b7e151628aed2a6abf7158809cf4f3c", + "expectedOutput": "7DF76B", + "recipeConfig": [ + { + "op": "Key Check Value", + "args": [{"option": "Hex", "string": "2b7e151628aed2a6abf7158809cf4f3c"}, "AES-ECB (Zeros)", 6] + } + ] + }, + { + "name": "Key Check Value: AES-CMAC (Empty), NIST SP 800-38B AES-128 key", + "input": "2b7e151628aed2a6abf7158809cf4f3c", + "expectedOutput": "BB1D69", + "recipeConfig": [ + { + "op": "Key Check Value", + "args": [{"option": "Hex", "string": "2b7e151628aed2a6abf7158809cf4f3c"}, "AES-CMAC (Empty)", 6] + } + ] + }, + { + "name": "Key Check Value: output length respects hex-char argument", + "input": "2b7e151628aed2a6abf7158809cf4f3c", + "expectedOutput": "BB1D6929", + "recipeConfig": [ + { + "op": "Key Check Value", + "args": [{"option": "Hex", "string": "2b7e151628aed2a6abf7158809cf4f3c"}, "AES-CMAC (Empty)", 8] + } + ] + }, + { + "name": "Key Check Value: empty key rejected", + "input": "", + "expectedOutput": "No key material was provided.", + "recipeConfig": [ + { + "op": "Key Check Value", + "args": [{"option": "Hex", "string": ""}, "AES-CMAC (Empty)", 6] + } + ] + }, + { + "name": "Key Check Value: invalid TDES key length rejected", + "input": "0123456789ABCDEF", + "expectedOutput": "TDES key must be 16 or 24 bytes (currently 8 bytes).", + "recipeConfig": [ + { + "op": "Key Check Value", + "args": [{"option": "Hex", "string": "0123456789ABCDEF"}, "TDES-ECB (Zeros)", 6] + } + ] + } +]);