Merge 9afca78c9d6abe07e7fbd001fd92a1b7b1d4f54f into 4290ea753912378913b1f3f54e0fc5720afeda5d

This commit is contained in:
MAN$I VERMA 2026-08-10 00:00:56 -07:00 committed by GitHub
commit 93437be058
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 466 additions and 0 deletions

View File

@ -165,6 +165,7 @@
"Derive PBKDF2 key",
"Derive EVP key",
"Derive HKDF key",
"Generate Kerberos Keytab",
"Bcrypt",
"Scrypt",
"JWT Sign",

View File

@ -0,0 +1,381 @@
/**
* @author mansiverma897993
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import Utils from "../Utils.mjs";
/**
* Serialize a single keytab entry into a binary Uint8Array.
*
* @param {string} principal
* @param {string} realm
* @param {number} kvno
* @param {number} etype
* @param {Uint8Array} keyBytes
* @returns {Uint8Array}
*/
function serializeEntry(principal, realm, kvno, etype, keyBytes) {
const components = principal.split("/").map(c => c.trim()).filter(Boolean);
const realmBytes = new TextEncoder().encode(realm.trim().toUpperCase());
let componentsSize = 0;
const componentsBytes = components.map(c => {
const bytes = new TextEncoder().encode(c);
componentsSize += 2 + bytes.length;
return bytes;
});
const nameType = 1; // KRB5_NT_PRINCIPAL
const timestamp = 0; // Fixed to 0 for deterministic testing
const vno8 = kvno & 0xff;
// Entry content size:
// num_components (2)
// realm len (2) + realmBytes.length
// componentsSize (each comp: 2 len + data)
// name_type (4)
// timestamp (4)
// vno8 (1)
// keyblock: keytype (2) + keylen (2) + keyBytes.length
// vno (4)
const entrySize = 2 + 2 + realmBytes.length + componentsSize + 4 + 4 + 1 + 2 + 2 + keyBytes.length + 4;
const buffer = new Uint8Array(4 + entrySize);
const view = new DataView(buffer.buffer);
let offset = 0;
// 1. size (4 bytes, signed 32-bit big-endian)
view.setInt32(offset, entrySize, false);
offset += 4;
// 2. num_components (2 bytes, big-endian)
view.setInt16(offset, components.length, false);
offset += 2;
// 3. realm length (2 bytes, big-endian)
view.setUint16(offset, realmBytes.length, false);
offset += 2;
// 4. realm data
buffer.set(realmBytes, offset);
offset += realmBytes.length;
// 5. components
for (const compBytes of componentsBytes) {
view.setUint16(offset, compBytes.length, false);
offset += 2;
buffer.set(compBytes, offset);
offset += compBytes.length;
}
// 6. name_type (4 bytes, big-endian)
view.setUint32(offset, nameType, false);
offset += 4;
// 7. timestamp (4 bytes, big-endian)
view.setUint32(offset, timestamp, false);
offset += 4;
// 8. vno8 (1 byte)
view.setUint8(offset, vno8);
offset += 1;
// 9. keytype (2 bytes, big-endian)
view.setUint16(offset, etype, false);
offset += 2;
// 10. keylen (2 bytes, big-endian)
view.setUint16(offset, keyBytes.length, false);
offset += 2;
// 11. key data
buffer.set(keyBytes, offset);
offset += keyBytes.length;
// 12. vno (4 bytes, big-endian)
view.setUint32(offset, kvno, false);
offset += 4;
return buffer;
}
/**
* Generate Kerberos Keytab operation
*/
class GenerateKerberosKeytab extends Operation {
/**
* GenerateKerberosKeytab constructor
*/
constructor() {
super();
this.name = "Generate Kerberos Keytab";
this.module = "Crypto";
this.description = "Generates a Wireshark-compatible Kerberos (KRB5) keytab file from user-provided principal keys or NT hashes. The input can be a single key in the arguments or a list of keys in the main input (in CSV/lines or JSON format).<br><br><b>Input formats:</b><br>- CSV/lines: <code>principal,realm,kvno,etype,key</code> (one per line). Supports shorter forms like <code>principal,key</code> or <code>principal,realm,key</code>.<br>- JSON: an array of keytab entry objects, e.g. <code>[{\"principal\": \"eshellstrop\", \"realm\": \"WORKGROUP\", \"kvno\": 1, \"etype\": 23, \"key\": \"hex_key\"}]</code>.<br><br><b>Supported encryption types:</b><br>- RC4-HMAC / 23 (16-byte key / 32 hex characters)<br>- AES128-CTS-HMAC-SHA1-96 / 17 (16-byte key / 32 hex characters)<br>- AES256-CTS-HMAC-SHA1-96 / 18 (32-byte key / 64 hex characters)";
this.infoURL = "https://web.mit.edu/kerberos/krb5-latest/doc/basic/keytab_def.html";
this.inputType = "string";
this.outputType = "byteArray";
this.args = [
{
"name": "Principal",
"type": "string",
"value": ""
},
{
"name": "Realm",
"type": "string",
"value": ""
},
{
"name": "KVNO",
"type": "number",
"value": 1
},
{
"name": "Encryption Type",
"type": "option",
"value": [
"RC4-HMAC (23)",
"AES128-CTS-HMAC-SHA1-96 (17)",
"AES256-CTS-HMAC-SHA1-96 (18)"
]
},
{
"name": "Key / NT Hash (Hex)",
"type": "string",
"value": ""
},
{
"name": "Output Format",
"type": "option",
"value": [
"Raw Keytab File",
"Hex",
"Base64",
"Detailed Summary Text"
]
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {byteArray}
*/
run(input, args) {
const entries = [];
// 1. Try parsing main input first
if (input && input.trim()) {
let parsedJson = null;
try {
const trimmed = input.trim();
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
parsedJson = JSON.parse(trimmed);
}
} catch (e) {
// Not valid JSON, fallback to CSV/lines
}
if (parsedJson) {
const list = Array.isArray(parsedJson) ? parsedJson : [parsedJson];
for (const item of list) {
if (typeof item === "object" && item !== null) {
const principal = item.principal || item.username || "";
const realm = item.realm || "";
const kvno = item.kvno !== undefined ? item.kvno : 1;
const etypeInput = item.etype || item.encryption_type || 23;
const key = item.key || item.nt_hash || item.value || "";
if (principal && key) {
entries.push({ principal, realm, kvno, etypeInput, key });
}
}
}
} else {
const rows = Utils.parseCSV(input.trim());
for (const row of rows) {
if (row.length === 0) continue;
const firstCol = row[0].trim();
if (firstCol.startsWith("#") || firstCol === "") continue;
// Ignore header line
const col1Lower = firstCol.toLowerCase();
if (col1Lower === "principal" || col1Lower === "username") continue;
if (row.length < 2) continue;
let principal = "";
let realm = "";
let kvno = 1;
let etypeInput = 23;
let key = "";
if (row.length === 2) {
principal = row[0];
key = row[1];
} else if (row.length === 3) {
principal = row[0];
realm = row[1];
key = row[2];
} else if (row.length === 4) {
principal = row[0];
realm = row[1];
const val = parseInt(row[2].trim(), 10);
if (isNaN(val)) {
etypeInput = row[2];
} else if (val === 23 || val === 17 || val === 18) {
etypeInput = val;
} else {
kvno = val;
}
key = row[3];
} else {
principal = row[0];
realm = row[1];
kvno = parseInt(row[2].trim(), 10) || 1;
etypeInput = row[3];
key = row[4];
}
if (principal && key) {
entries.push({ principal, realm, kvno, etypeInput, key });
}
}
}
}
// 2. Fall back to UI args if no entries found in main input
if (entries.length === 0) {
const principal = args[0] || "";
const realm = args[1] || "";
const kvno = args[2] !== undefined ? args[2] : 1;
const etypeInput = args[3];
const key = args[4] || "";
if (principal && key) {
entries.push({ principal, realm, kvno, etypeInput, key });
}
}
if (entries.length === 0) {
throw new OperationError("No principal/key entries provided or found in input/arguments.");
}
const finalEntries = [];
for (const entry of entries) {
const principal = entry.principal.trim();
const realm = entry.realm.trim() || "LOCAL";
const kvno = parseInt(entry.kvno, 10) || 1;
let etype = 23;
const etypeStr = String(entry.etypeInput).toLowerCase().trim();
if (etypeStr.includes("17") || etypeStr.includes("aes128")) {
etype = 17;
} else if (etypeStr.includes("18") || etypeStr.includes("aes256")) {
etype = 18;
} else if (etypeStr.includes("23") || etypeStr.includes("rc4") || etypeStr.includes("arcfour")) {
etype = 23;
} else {
const parsed = parseInt(etypeStr, 10);
if (!isNaN(parsed)) {
etype = parsed;
} else {
throw new OperationError(`Unsupported or unrecognized encryption type: ${entry.etypeInput}`);
}
}
const cleanKey = entry.key.trim().replace(/^0x/i, "").replace(/[\s:]/g, "");
if (!/^[a-fA-F0-9]+$/.test(cleanKey)) {
throw new OperationError(`Key for principal ${principal} must be a hex-encoded string.`);
}
const keyLengthInBytes = cleanKey.length / 2;
if (etype === 23 && keyLengthInBytes !== 16) {
throw new OperationError(`RC4 key (etype 23) for principal ${principal} must be 16 bytes (32 hex characters). Got ${keyLengthInBytes} bytes.`);
} else if (etype === 17 && keyLengthInBytes !== 16) {
throw new OperationError(`AES128 key (etype 17) for principal ${principal} must be 16 bytes (32 hex characters). Got ${keyLengthInBytes} bytes.`);
} else if (etype === 18 && keyLengthInBytes !== 32) {
throw new OperationError(`AES256 key (etype 18) for principal ${principal} must be 32 bytes (64 hex characters). Got ${keyLengthInBytes} bytes.`);
}
const keyBytes = Utils.convertToByteArray(cleanKey, "hex");
finalEntries.push({ principal, realm, kvno, etype, keyBytes, cleanKey });
}
// Build the binary keytab data
let totalLength = 2; // For format version bytes [0x05, 0x02]
const serializedEntries = [];
for (const entry of finalEntries) {
const bytes = serializeEntry(entry.principal, entry.realm, entry.kvno, entry.etype, new Uint8Array(entry.keyBytes));
serializedEntries.push(bytes);
totalLength += bytes.length;
}
const keytabFile = new Uint8Array(totalLength);
keytabFile[0] = 0x05;
keytabFile[1] = 0x02;
let offset = 2;
for (const bytes of serializedEntries) {
keytabFile.set(bytes, offset);
offset += bytes.length;
}
const outputFormat = args[5];
switch (outputFormat) {
case "Raw Keytab File":
return Array.from(keytabFile);
case "Hex": {
const hexStr = Array.from(keytabFile).map(b => b.toString(16).padStart(2, "0")).join("");
return Utils.strToByteArray(hexStr);
}
case "Base64": {
// Convert byteArray to string of chars, then base64 encode
const chars = Utils.byteArrayToChars(Array.from(keytabFile));
const b64 = btoa(chars);
return Utils.strToByteArray(b64);
}
case "Detailed Summary Text": {
let out = `Kerberos KRB5 Keytab Generation Summary:\n`;
out += `-----------------------------------------\n`;
out += `Successfully generated a keytab file with ${finalEntries.length} entries:\n\n`;
finalEntries.forEach((entry, idx) => {
let etypeName = "Unknown";
if (entry.etype === 23) etypeName = "RC4-HMAC (23)";
else if (entry.etype === 17) etypeName = "AES128-CTS-HMAC-SHA1-96 (17)";
else if (entry.etype === 18) etypeName = "AES256-CTS-HMAC-SHA1-96 (18)";
out += `Entry ${idx + 1}:\n`;
out += ` Principal: ${entry.principal}\n`;
out += ` Realm: ${entry.realm}\n`;
out += ` KVNO: ${entry.kvno}\n`;
out += ` Etype: ${etypeName}\n`;
out += ` Key (Hex): ${entry.cleanKey}\n\n`;
});
out += `Wireshark Configuration Instructions:\n`;
out += `-------------------------------------\n`;
out += `1. Save the generated keytab output as a file (e.g. krb5.keytab).\n`;
out += `2. Open Wireshark and go to: Edit -> Preferences -> Protocols -> KRB5\n`;
out += `3. Enable "Decrypt Kerberos traffic" (if available).\n`;
out += `4. Click "Browse..." next to "Kerberos keytab file" and select the generated file.\n`;
out += `5. Wireshark will now automatically decrypt Kerberos, DCE/RPC, LDAP, and SMB traffic where these keys are valid.\n`;
return Utils.strToByteArray(out);
}
default:
throw new OperationError(`Unknown output format: ${outputFormat}`);
}
}
}
export default GenerateKerberosKeytab;

View File

@ -0,0 +1,84 @@
/**
* Generate Kerberos Keytab operation tests
*
* @author mansiverma897993
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
name: "Generate Kerberos Keytab: Single key via UI args (RC4)",
input: "",
expectedOutput: "05020000003b00010009574f524b47524f5550000b657368656c6c7374726f70000000010000000001001700103f29138a04aadc19214e9c04028bf38100000001",
recipeConfig: [
{
op: "Generate Kerberos Keytab",
args: [
"eshellstrop",
"WORKGROUP",
1,
"RC4-HMAC (23)",
"3f29138a04aadc19214e9c04028bf381",
"Hex"
],
},
],
},
{
name: "Generate Kerberos Keytab: Multiple keys via CSV input",
input: "eshellstrop,WORKGROUP,1,23,3f29138a04aadc19214e9c04028bf381\ncifs/server.example.local,EXAMPLE.LOCAL,2,18,00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
expectedOutput: "05020000003b00010009574f524b47524f5550000b657368656c6c7374726f70000000010000000001001700103f29138a04aadc19214e9c04028bf381000000010000005e0002000d4558414d504c452e4c4f43414c00046369667300147365727665722e6578616d706c652e6c6f63616c0000000100000000020012002000112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00000002",
recipeConfig: [
{
op: "Generate Kerberos Keytab",
args: [
"",
"",
1,
"RC4-HMAC (23)",
"",
"Hex"
],
},
],
},
{
name: "Generate Kerberos Keytab: JSON input format",
input: '[\n {"principal": "eshellstrop", "realm": "WORKGROUP", "kvno": 1, "etype": 23, "key": "3f29138a04aadc19214e9c04028bf381"}\n]',
expectedOutput: "05020000003b00010009574f524b47524f5550000b657368656c6c7374726f70000000010000000001001700103f29138a04aadc19214e9c04028bf38100000001",
recipeConfig: [
{
op: "Generate Kerberos Keytab",
args: [
"",
"",
1,
"RC4-HMAC (23)",
"",
"Hex"
],
},
],
},
{
name: "Generate Kerberos Keytab: Detailed Summary Text output",
input: "",
expectedOutput: "Kerberos KRB5 Keytab Generation Summary:\n-----------------------------------------\nSuccessfully generated a keytab file with 1 entries:\n\nEntry 1:\n Principal: eshellstrop\n Realm: WORKGROUP\n KVNO: 1\n Etype: RC4-HMAC (23)\n Key (Hex): 3f29138a04aadc19214e9c04028bf381\n\nWireshark Configuration Instructions:\n-------------------------------------\n1. Save the generated keytab output as a file (e.g. krb5.keytab).\n2. Open Wireshark and go to: Edit -> Preferences -> Protocols -> KRB5\n3. Enable \"Decrypt Kerberos traffic\" (if available).\n4. Click \"Browse...\" next to \"Kerberos keytab file\" and select the generated file.\n5. Wireshark will now automatically decrypt Kerberos, DCE/RPC, LDAP, and SMB traffic where these keys are valid.\n",
recipeConfig: [
{
op: "Generate Kerberos Keytab",
args: [
"eshellstrop",
"WORKGROUP",
1,
"RC4-HMAC (23)",
"3f29138a04aadc19214e9c04028bf381",
"Detailed Summary Text"
],
},
],
},
]);