/** * SMB2/3 Message Parser * Handles parsing of SMB2 protocol messages from binary data */ import type { SMB2Header, SMB2TransformHeader, SMB2NegotiateRequest, SMB2NegotiateResponse, SMB2NegotiateContext, SMB2SessionSetupRequest, SMB2SessionSetupResponse, SMB2TreeConnectRequest, SMB2TreeConnectResponse, SMB2CreateRequest, SMB2CreateResponse, SMB2CreateContext, SMB2CloseRequest, SMB2CloseResponse, SMB2ReadRequest, SMB2ReadResponse, SMB2WriteRequest, SMB2WriteResponse, SMB2QueryDirectoryRequest, SMB2QueryDirectoryResponse, SMB2QueryInfoRequest, SMB2QueryInfoResponse, SMB2SetInfoRequest, SMB2SetInfoResponse, SMB2FileId, SMB2Guid, FileBasicInformation, FileStandardInformation, FileDirectoryInformation, } from './types.js'; import { SMB2_PROTOCOL_ID, SMB2_TRANSFORM_PROTOCOL_ID, SMB2_HEADER_SIZE, SMB2_TRANSFORM_HEADER_SIZE, SMB2_COMMANDS, SMB2_DIALECTS, } from './constants.js'; export interface ParseResult { readonly result: T; readonly bytesRead: number; } export class SMB2MessageParser { /** * Parse SMB2 header from buffer */ public parseHeader(buffer: Buffer, offset: number = 0): ParseResult { if (buffer.length < offset + SMB2_HEADER_SIZE) { throw new Error('Buffer too small for SMB2 header'); } const protocolId = buffer.readUInt32LE(offset); if (protocolId !== SMB2_PROTOCOL_ID) { throw new Error(`Invalid SMB2 protocol ID: 0x${protocolId.toString(16)}`); } const header: SMB2Header = { protocolId, structureSize: buffer.readUInt16LE(offset + 4), creditCharge: buffer.readUInt16LE(offset + 6), status: buffer.readUInt32LE(offset + 8), command: buffer.readUInt16LE(offset + 12), creditRequestResponse: buffer.readUInt16LE(offset + 14), flags: buffer.readUInt32LE(offset + 16), nextCommand: buffer.readUInt32LE(offset + 20), messageId: buffer.readBigUInt64LE(offset + 24), reserved: buffer.readUInt32LE(offset + 32), treeId: buffer.readUInt32LE(offset + 36), sessionId: buffer.readBigUInt64LE(offset + 40), signature: buffer.subarray(offset + 48, offset + 64), }; return { result: header, bytesRead: SMB2_HEADER_SIZE, }; } /** * Parse SMB2 Transform header for encrypted messages */ public parseTransformHeader(buffer: Buffer, offset: number = 0): ParseResult { if (buffer.length < offset + SMB2_TRANSFORM_HEADER_SIZE) { throw new Error('Buffer too small for SMB2 transform header'); } const protocolId = buffer.readUInt32LE(offset); if (protocolId !== SMB2_TRANSFORM_PROTOCOL_ID) { throw new Error(`Invalid SMB2 transform protocol ID: 0x${protocolId.toString(16)}`); } const header: SMB2TransformHeader = { protocolId, signature: buffer.subarray(offset + 4, offset + 20), nonce: buffer.subarray(offset + 20, offset + 36), originalMessageSize: buffer.readUInt32LE(offset + 36), reserved: buffer.readUInt16LE(offset + 40), flags: buffer.readUInt16LE(offset + 42), sessionId: buffer.readBigUInt64LE(offset + 44), }; return { result: header, bytesRead: SMB2_TRANSFORM_HEADER_SIZE, }; } /** * Parse SMB2 Negotiate Request */ public parseNegotiateRequest(buffer: Buffer, offset: number = 0): ParseResult { if (buffer.length < offset + 36) { throw new Error('Buffer too small for negotiate request'); } const structureSize = buffer.readUInt16LE(offset); const dialectCount = buffer.readUInt16LE(offset + 2); const securityMode = buffer.readUInt16LE(offset + 4); const reserved = buffer.readUInt16LE(offset + 6); const capabilities = buffer.readUInt32LE(offset + 8); const clientGuid = this.parseGuid(buffer, offset + 12); const clientStartTime = buffer.readBigUInt64LE(offset + 28); // Parse dialects const dialects: SMB2_DIALECTS[] = []; let dialectOffset = offset + 36; for (let i = 0; i < dialectCount; i++) { if (dialectOffset + 2 > buffer.length) break; dialects.push(buffer.readUInt16LE(dialectOffset)); dialectOffset += 2; } // Parse negotiate contexts (SMB 3.1.1+) const negotiateContexts: SMB2NegotiateContext[] = []; // TODO: Implement negotiate context parsing for SMB 3.1.1 const request: SMB2NegotiateRequest = { structureSize, dialectCount, securityMode, reserved, capabilities, clientGuid, clientStartTime, dialects, negotiateContexts, }; return { result: request, bytesRead: dialectOffset - offset, }; } /** * Parse SMB2 Session Setup Request */ public parseSessionSetupRequest(buffer: Buffer, offset: number = 0): ParseResult { if (buffer.length < offset + 25) { throw new Error('Buffer too small for session setup request'); } const structureSize = buffer.readUInt16LE(offset); const flags = buffer.readUInt8(offset + 2); const securityMode = buffer.readUInt8(offset + 3); const capabilities = buffer.readUInt32LE(offset + 4); const channel = buffer.readUInt32LE(offset + 8); const securityBufferOffset = buffer.readUInt16LE(offset + 12); const securityBufferLength = buffer.readUInt16LE(offset + 14); const previousSessionId = buffer.readBigUInt64LE(offset + 16); // Parse security buffer let securityBuffer = Buffer.alloc(0); if (securityBufferLength > 0 && securityBufferOffset > 0) { const bufferStart = securityBufferOffset - SMB2_HEADER_SIZE; securityBuffer = buffer.subarray(bufferStart, bufferStart + securityBufferLength); } const request: SMB2SessionSetupRequest = { structureSize, flags, securityMode, capabilities, channel, securityBufferOffset, securityBufferLength, previousSessionId, securityBuffer, }; return { result: request, bytesRead: 24, }; } /** * Parse SMB2 Tree Connect Request */ public parseTreeConnectRequest(buffer: Buffer, offset: number = 0): ParseResult { if (buffer.length < offset + 9) { throw new Error('Buffer too small for tree connect request'); } const structureSize = buffer.readUInt16LE(offset); const flags = buffer.readUInt16LE(offset + 2); const pathOffset = buffer.readUInt16LE(offset + 4); const pathLength = buffer.readUInt16LE(offset + 6); // Parse path (UTF-16LE) let path = ''; if (pathLength > 0 && pathOffset > 0) { const pathStart = pathOffset - SMB2_HEADER_SIZE; const pathBuffer = buffer.subarray(pathStart, pathStart + pathLength); path = pathBuffer.toString('utf16le'); } const request: SMB2TreeConnectRequest = { structureSize, flags, pathOffset, pathLength, path, }; return { result: request, bytesRead: 8, }; } /** * Parse SMB2 Create Request */ public parseCreateRequest(buffer: Buffer, offset: number = 0): ParseResult { if (buffer.length < offset + 57) { throw new Error('Buffer too small for create request'); } const structureSize = buffer.readUInt16LE(offset); const securityFlags = buffer.readUInt8(offset + 2); const requestedOplockLevel = buffer.readUInt8(offset + 3); const impersonationLevel = buffer.readUInt32LE(offset + 4); const smbCreateFlags = buffer.readBigUInt64LE(offset + 8); const reserved = buffer.readBigUInt64LE(offset + 16); const desiredAccess = buffer.readUInt32LE(offset + 24); const fileAttributes = buffer.readUInt32LE(offset + 28); const shareAccess = buffer.readUInt32LE(offset + 32); const createDisposition = buffer.readUInt32LE(offset + 36); const createOptions = buffer.readUInt32LE(offset + 40); const nameOffset = buffer.readUInt16LE(offset + 44); const nameLength = buffer.readUInt16LE(offset + 46); const createContextsOffset = buffer.readUInt32LE(offset + 48); const createContextsLength = buffer.readUInt32LE(offset + 52); // Parse name (UTF-16LE) let name = ''; if (nameLength > 0 && nameOffset > 0) { const nameStart = nameOffset - SMB2_HEADER_SIZE; const nameBuffer = buffer.subarray(nameStart, nameStart + nameLength); name = nameBuffer.toString('utf16le'); } // Parse create contexts const createContexts: SMB2CreateContext[] = []; if (createContextsLength > 0 && createContextsOffset > 0) { // TODO: Implement create context parsing } const request: SMB2CreateRequest = { structureSize, securityFlags, requestedOplockLevel, impersonationLevel, smbCreateFlags, reserved, desiredAccess, fileAttributes, shareAccess, createDisposition, createOptions, nameOffset, nameLength, createContextsOffset, createContextsLength, name, createContexts, }; return { result: request, bytesRead: 56, }; } /** * Parse SMB2 Close Request */ public parseCloseRequest(buffer: Buffer, offset: number = 0): ParseResult { if (buffer.length < offset + 24) { throw new Error('Buffer too small for close request'); } const structureSize = buffer.readUInt16LE(offset); const flags = buffer.readUInt16LE(offset + 2); const reserved = buffer.readUInt32LE(offset + 4); const fileId = this.parseFileId(buffer, offset + 8); const request: SMB2CloseRequest = { structureSize, flags, reserved, fileId, }; return { result: request, bytesRead: 24, }; } /** * Parse SMB2 Read Request */ public parseReadRequest(buffer: Buffer, offset: number = 0): ParseResult { if (buffer.length < offset + 49) { throw new Error('Buffer too small for read request'); } const structureSize = buffer.readUInt16LE(offset); const padding = buffer.readUInt8(offset + 2); const flags = buffer.readUInt8(offset + 3); const length = buffer.readUInt32LE(offset + 4); const offsetValue = buffer.readBigUInt64LE(offset + 8); const fileId = this.parseFileId(buffer, offset + 16); const minimumCount = buffer.readUInt32LE(offset + 32); const channel = buffer.readUInt32LE(offset + 36); const remainingBytes = buffer.readUInt32LE(offset + 40); const readChannelInfoOffset = buffer.readUInt16LE(offset + 44); const readChannelInfoLength = buffer.readUInt16LE(offset + 46); // Parse read channel info let readChannelInfo = Buffer.alloc(0); if (readChannelInfoLength > 0 && readChannelInfoOffset > 0) { const infoStart = readChannelInfoOffset - SMB2_HEADER_SIZE; readChannelInfo = buffer.subarray(infoStart, infoStart + readChannelInfoLength); } const request: SMB2ReadRequest = { structureSize, padding, flags, length, offset: offsetValue, fileId, minimumCount, channel, remainingBytes, readChannelInfoOffset, readChannelInfoLength, readChannelInfo, }; return { result: request, bytesRead: 48, }; } /** * Parse SMB2 Write Request */ public parseWriteRequest(buffer: Buffer, offset: number = 0): ParseResult { if (buffer.length < offset + 49) { throw new Error('Buffer too small for write request'); } const structureSize = buffer.readUInt16LE(offset); const dataOffset = buffer.readUInt16LE(offset + 2); const length = buffer.readUInt32LE(offset + 4); const offsetValue = buffer.readBigUInt64LE(offset + 8); const fileId = this.parseFileId(buffer, offset + 16); const channel = buffer.readUInt32LE(offset + 32); const remainingBytes = buffer.readUInt32LE(offset + 36); const writeChannelInfoOffset = buffer.readUInt16LE(offset + 40); const writeChannelInfoLength = buffer.readUInt16LE(offset + 42); const flags = buffer.readUInt32LE(offset + 44); // Parse write channel info let writeChannelInfo = Buffer.alloc(0); if (writeChannelInfoLength > 0 && writeChannelInfoOffset > 0) { const infoStart = writeChannelInfoOffset - SMB2_HEADER_SIZE; writeChannelInfo = buffer.subarray(infoStart, infoStart + writeChannelInfoLength); } // Parse data let data = Buffer.alloc(0); if (length > 0 && dataOffset > 0) { const dataStart = dataOffset - SMB2_HEADER_SIZE; data = buffer.subarray(dataStart, dataStart + length); } const request: SMB2WriteRequest = { structureSize, dataOffset, length, offset: offsetValue, fileId, channel, remainingBytes, writeChannelInfoOffset, writeChannelInfoLength, flags, writeChannelInfo, data, }; return { result: request, bytesRead: 48, }; } /** * Parse SMB2 Query Directory Request */ public parseQueryDirectoryRequest(buffer: Buffer, offset: number = 0): ParseResult { if (buffer.length < offset + 33) { throw new Error('Buffer too small for query directory request'); } const structureSize = buffer.readUInt16LE(offset); const fileInformationClass = buffer.readUInt8(offset + 2); const flags = buffer.readUInt8(offset + 3); const fileIndex = buffer.readUInt32LE(offset + 4); const fileId = this.parseFileId(buffer, offset + 8); const fileNameOffset = buffer.readUInt16LE(offset + 24); const fileNameLength = buffer.readUInt16LE(offset + 26); const outputBufferLength = buffer.readUInt32LE(offset + 28); // Parse filename (UTF-16LE) let fileName = ''; if (fileNameLength > 0 && fileNameOffset > 0) { const nameStart = fileNameOffset - SMB2_HEADER_SIZE; const nameBuffer = buffer.subarray(nameStart, nameStart + fileNameLength); fileName = nameBuffer.toString('utf16le'); } const request: SMB2QueryDirectoryRequest = { structureSize, fileInformationClass, flags, fileIndex, fileId, fileNameOffset, fileNameLength, outputBufferLength, fileName, }; return { result: request, bytesRead: 32, }; } /** * Parse SMB2 Query Info Request */ public parseQueryInfoRequest(buffer: Buffer, offset: number = 0): ParseResult { if (buffer.length < offset + 41) { throw new Error('Buffer too small for query info request'); } const structureSize = buffer.readUInt16LE(offset); const infoType = buffer.readUInt8(offset + 2); const fileInfoClass = buffer.readUInt8(offset + 3); const outputBufferLength = buffer.readUInt32LE(offset + 4); const inputBufferOffset = buffer.readUInt16LE(offset + 8); const reserved = buffer.readUInt16LE(offset + 10); const inputBufferLength = buffer.readUInt32LE(offset + 12); const additionalInformation = buffer.readUInt32LE(offset + 16); const flags = buffer.readUInt32LE(offset + 20); const fileId = this.parseFileId(buffer, offset + 24); // Parse input buffer let inputBuffer = Buffer.alloc(0); if (inputBufferLength > 0 && inputBufferOffset > 0) { const bufferStart = inputBufferOffset - SMB2_HEADER_SIZE; inputBuffer = buffer.subarray(bufferStart, bufferStart + inputBufferLength); } const request: SMB2QueryInfoRequest = { structureSize, infoType, fileInfoClass, outputBufferLength, inputBufferOffset, reserved, inputBufferLength, additionalInformation, flags, fileId, inputBuffer, }; return { result: request, bytesRead: 40, }; } /** * Parse SMB2 Set Info Request */ public parseSetInfoRequest(buffer: Buffer, offset: number = 0): ParseResult { if (buffer.length < offset + 33) { throw new Error('Buffer too small for set info request'); } const structureSize = buffer.readUInt16LE(offset); const infoType = buffer.readUInt8(offset + 2); const fileInfoClass = buffer.readUInt8(offset + 3); const bufferLength = buffer.readUInt32LE(offset + 4); const bufferOffset = buffer.readUInt16LE(offset + 8); const reserved = buffer.readUInt16LE(offset + 10); const additionalInformation = buffer.readUInt32LE(offset + 12); const fileId = this.parseFileId(buffer, offset + 16); // Parse buffer let bufferData = Buffer.alloc(0); if (bufferLength > 0 && bufferOffset > 0) { const dataStart = bufferOffset - SMB2_HEADER_SIZE; bufferData = buffer.subarray(dataStart, dataStart + bufferLength); } const request: SMB2SetInfoRequest = { structureSize, infoType, fileInfoClass, bufferLength, bufferOffset, reserved, additionalInformation, fileId, buffer: bufferData, }; return { result: request, bytesRead: 32, }; } /** * Parse File Basic Information */ public parseFileBasicInformation(buffer: Buffer, offset: number = 0): ParseResult { if (buffer.length < offset + 40) { throw new Error('Buffer too small for file basic information'); } const info: FileBasicInformation = { creationTime: buffer.readBigUInt64LE(offset), lastAccessTime: buffer.readBigUInt64LE(offset + 8), lastWriteTime: buffer.readBigUInt64LE(offset + 16), changeTime: buffer.readBigUInt64LE(offset + 24), fileAttributes: buffer.readUInt32LE(offset + 32), reserved: buffer.readUInt32LE(offset + 36), }; return { result: info, bytesRead: 40, }; } /** * Parse File Standard Information */ public parseFileStandardInformation(buffer: Buffer, offset: number = 0): ParseResult { if (buffer.length < offset + 24) { throw new Error('Buffer too small for file standard information'); } const info: FileStandardInformation = { allocationSize: buffer.readBigUInt64LE(offset), endOfFile: buffer.readBigUInt64LE(offset + 8), numberOfLinks: buffer.readUInt32LE(offset + 16), deletePending: buffer.readUInt8(offset + 20) !== 0, directory: buffer.readUInt8(offset + 21) !== 0, reserved: buffer.readUInt16LE(offset + 22), }; return { result: info, bytesRead: 24, }; } /** * Parse File Directory Information */ public parseFileDirectoryInformation(buffer: Buffer, offset: number = 0): ParseResult { if (buffer.length < offset + 64) { throw new Error('Buffer too small for file directory information'); } const nextEntryOffset = buffer.readUInt32LE(offset); const fileIndex = buffer.readUInt32LE(offset + 4); const creationTime = buffer.readBigUInt64LE(offset + 8); const lastAccessTime = buffer.readBigUInt64LE(offset + 16); const lastWriteTime = buffer.readBigUInt64LE(offset + 24); const changeTime = buffer.readBigUInt64LE(offset + 32); const endOfFile = buffer.readBigUInt64LE(offset + 40); const allocationSize = buffer.readBigUInt64LE(offset + 48); const fileAttributes = buffer.readUInt32LE(offset + 56); const fileNameLength = buffer.readUInt32LE(offset + 60); // Parse filename (UTF-16LE) let fileName = ''; if (fileNameLength > 0) { const nameBuffer = buffer.subarray(offset + 64, offset + 64 + fileNameLength); fileName = nameBuffer.toString('utf16le'); } const info: FileDirectoryInformation = { nextEntryOffset, fileIndex, creationTime, lastAccessTime, lastWriteTime, changeTime, endOfFile, allocationSize, fileAttributes, fileNameLength, fileName, }; return { result: info, bytesRead: 64 + fileNameLength, }; } /** * Parse SMB2 File ID */ private parseFileId(buffer: Buffer, offset: number): SMB2FileId { if (buffer.length < offset + 16) { throw new Error('Buffer too small for file ID'); } return { persistent: buffer.readBigUInt64LE(offset), volatile: buffer.readBigUInt64LE(offset + 8), }; } /** * Parse SMB2 GUID */ private parseGuid(buffer: Buffer, offset: number): SMB2Guid { if (buffer.length < offset + 16) { throw new Error('Buffer too small for GUID'); } const guidBytes = buffer.subarray(offset, offset + 16); return Array.from(guidBytes) .map(b => b.toString(16).padStart(2, '0')) .join('') .toUpperCase(); } /** * Parse UTF-16LE string */ private parseUtf16String(buffer: Buffer, offset: number, length: number): string { if (buffer.length < offset + length) { throw new Error('Buffer too small for UTF-16 string'); } return buffer.subarray(offset, offset + length).toString('utf16le'); } /** * Parse FILETIME (Windows 64-bit timestamp) */ private parseFileTime(buffer: Buffer, offset: number): bigint { if (buffer.length < offset + 8) { throw new Error('Buffer too small for FILETIME'); } return buffer.readBigUInt64LE(offset); } /** * Convert FILETIME to JavaScript Date */ private fileTimeToDate(filetime: bigint): Date { // FILETIME is 100-nanosecond intervals since January 1, 1601 const FILETIME_EPOCH_DIFF = 11644473600000n; // milliseconds between 1601 and 1970 const timestamp = Number(filetime / 10000n) - Number(FILETIME_EPOCH_DIFF); return new Date(timestamp); } /** * Validate buffer has minimum required size */ private validateBufferSize(buffer: Buffer, offset: number, requiredSize: number, context: string): void { if (buffer.length < offset + requiredSize) { throw new Error(`Buffer too small for ${context}: need ${requiredSize} bytes, have ${buffer.length - offset}`); } } }