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 PBKDF2 key",
"Derive EVP key", "Derive EVP key",
"Derive HKDF key", "Derive HKDF key",
"Generate Kerberos Keytab",
"Bcrypt", "Bcrypt",
"Scrypt", "Scrypt",
"JWT Sign", "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"
],
},
],
},
]);