Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/core/config/Categories.json
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,7 @@
"Compare CTPH hashes",
"HMAC",
"CMAC",
"Key Check Value",
"Bcrypt",
"Bcrypt compare",
"Bcrypt parse",
Expand Down
122 changes: 122 additions & 0 deletions src/core/operations/KeyCheckValue.mjs
Original file line number Diff line number Diff line change
@@ -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.<br><br><ul><li><b>TDES-ECB (Zeros)</b> — ANSI X9.24-1 legacy KCV: encrypt an 8-byte zero block with the key.</li><li><b>AES-ECB (Zeros)</b> — encrypt a 16-byte zero block with the key.</li><li><b>AES-CMAC (Empty)</b> — RFC 4493 CMAC of the empty message, used by TR-31 (ANSI X9.143) KC optional blocks.</li></ul>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;
92 changes: 92 additions & 0 deletions tests/operations/tests/KeyCheckValue.mjs
Original file line number Diff line number Diff line change
@@ -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]
}
]
}
]);