From 9afca78c9d6abe07e7fbd001fd92a1b7b1d4f54f Mon Sep 17 00:00:00 2001 From: mansiverma897993 Date: Mon, 13 Jul 2026 14:45:26 +0530 Subject: [PATCH] feat(Crypto): Add Generate Kerberos Keytab operation --- src/core/config/Categories.json | 1 + .../operations/GenerateKerberosKeytab.mjs | 381 ++++++++++++++++++ .../tests/GenerateKerberosKeytab.mjs | 84 ++++ 3 files changed, 466 insertions(+) create mode 100644 src/core/operations/GenerateKerberosKeytab.mjs create mode 100644 tests/operations/tests/GenerateKerberosKeytab.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index c652ed480d..352fa12a78 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -163,6 +163,7 @@ "Derive PBKDF2 key", "Derive EVP key", "Derive HKDF key", + "Generate Kerberos Keytab", "Bcrypt", "Scrypt", "JWT Sign", diff --git a/src/core/operations/GenerateKerberosKeytab.mjs b/src/core/operations/GenerateKerberosKeytab.mjs new file mode 100644 index 0000000000..c7af6fc803 --- /dev/null +++ b/src/core/operations/GenerateKerberosKeytab.mjs @@ -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).

Input formats:
- CSV/lines: principal,realm,kvno,etype,key (one per line). Supports shorter forms like principal,key or principal,realm,key.
- JSON: an array of keytab entry objects, e.g. [{\"principal\": \"eshellstrop\", \"realm\": \"WORKGROUP\", \"kvno\": 1, \"etype\": 23, \"key\": \"hex_key\"}].

Supported encryption types:
- RC4-HMAC / 23 (16-byte key / 32 hex characters)
- AES128-CTS-HMAC-SHA1-96 / 17 (16-byte key / 32 hex characters)
- 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; diff --git a/tests/operations/tests/GenerateKerberosKeytab.mjs b/tests/operations/tests/GenerateKerberosKeytab.mjs new file mode 100644 index 0000000000..6ddc62a58c --- /dev/null +++ b/tests/operations/tests/GenerateKerberosKeytab.mjs @@ -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" + ], + }, + ], + }, +]);