/** * SMB2/3 Message Serializer * Handles serialization of SMB2 protocol messages to 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, SMB2ErrorResponse, SMB2FileId, SMB2Guid, FileBasicInformation, FileStandardInformation, FileDirectoryInformation, } from './types.js'; import { SMB2_PROTOCOL_ID, SMB2_TRANSFORM_PROTOCOL_ID, SMB2_HEADER_SIZE, SMB2_TRANSFORM_HEADER_SIZE, } from './constants.js'; export class SMB2MessageSerializer { /** * Serialize SMB2 header to buffer */ public serializeHeader(header: SMB2Header): Buffer { const buffer = Buffer.allocUnsafe(SMB2_HEADER_SIZE); buffer.writeUInt32LE(header.protocolId, 0); buffer.writeUInt16LE(header.structureSize, 4); buffer.writeUInt16LE(header.creditCharge, 6); buffer.writeUInt32LE(header.status, 8); buffer.writeUInt16LE(header.command, 12); buffer.writeUInt16LE(header.creditRequestResponse, 14); buffer.writeUInt32LE(header.flags, 16); buffer.writeUInt32LE(header.nextCommand, 20); buffer.writeBigUInt64LE(header.messageId, 24); buffer.writeUInt32LE(header.reserved, 32); buffer.writeUInt32LE(header.treeId, 36); buffer.writeBigUInt64LE(header.sessionId, 40); // Copy signature (16 bytes) header.signature.copy(buffer, 48, 0, 16); return buffer; } /** * Serialize SMB2 Transform header for encrypted messages */ public serializeTransformHeader(header: SMB2TransformHeader): Buffer { const buffer = Buffer.allocUnsafe(SMB2_TRANSFORM_HEADER_SIZE); buffer.writeUInt32LE(header.protocolId, 0); header.signature.copy(buffer, 4, 0, 16); header.nonce.copy(buffer, 20, 0, 16); buffer.writeUInt32LE(header.originalMessageSize, 36); buffer.writeUInt16LE(header.reserved, 40); buffer.writeUInt16LE(header.flags, 42); buffer.writeBigUInt64LE(header.sessionId, 44); return buffer; } /** * Serialize SMB2 Negotiate Response */ public serializeNegotiateResponse(response: SMB2NegotiateResponse): Buffer { const baseSize = 65; // Base structure size const contextSize = this.calculateNegotiateContextsSize(response.negotiateContexts); const totalSize = baseSize + response.securityBufferLength + contextSize; const buffer = Buffer.allocUnsafe(totalSize); let offset = 0; buffer.writeUInt16LE(response.structureSize, offset); offset += 2; buffer.writeUInt16LE(response.securityMode, offset); offset += 2; buffer.writeUInt16LE(response.dialectRevision, offset); offset += 2; buffer.writeUInt16LE(response.negotiateContextCount, offset); offset += 2; this.serializeGuid(response.serverGuid, buffer, offset); offset += 16; buffer.writeUInt32LE(response.capabilities, offset); offset += 4; buffer.writeUInt32LE(response.maxTransactSize, offset); offset += 4; buffer.writeUInt32LE(response.maxReadSize, offset); offset += 4; buffer.writeUInt32LE(response.maxWriteSize, offset); offset += 4; buffer.writeBigUInt64LE(response.systemTime, offset); offset += 8; buffer.writeBigUInt64LE(response.serverStartTime, offset); offset += 8; buffer.writeUInt16LE(response.securityBufferOffset, offset); offset += 2; buffer.writeUInt16LE(response.securityBufferLength, offset); offset += 2; buffer.writeUInt32LE(response.negotiateContextOffset, offset); offset += 4; // Copy security buffer if (response.securityBufferLength > 0) { response.securityBuffer.copy(buffer, offset, 0, response.securityBufferLength); offset += response.securityBufferLength; } // Serialize negotiate contexts for (const context of response.negotiateContexts) { offset += this.serializeNegotiateContext(context, buffer, offset); } return buffer.subarray(0, offset); } /** * Serialize SMB2 Session Setup Response */ public serializeSessionSetupResponse(response: SMB2SessionSetupResponse): Buffer { const totalSize = 9 + response.securityBufferLength; const buffer = Buffer.allocUnsafe(totalSize); let offset = 0; buffer.writeUInt16LE(response.structureSize, offset); offset += 2; buffer.writeUInt16LE(response.sessionFlags, offset); offset += 2; buffer.writeUInt16LE(response.securityBufferOffset, offset); offset += 2; buffer.writeUInt16LE(response.securityBufferLength, offset); offset += 2; // Copy security buffer if (response.securityBufferLength > 0) { response.securityBuffer.copy(buffer, offset, 0, response.securityBufferLength); offset += response.securityBufferLength; } return buffer.subarray(0, offset); } /** * Serialize SMB2 Tree Connect Response */ public serializeTreeConnectResponse(response: SMB2TreeConnectResponse): Buffer { const buffer = Buffer.allocUnsafe(16); buffer.writeUInt16LE(response.structureSize, 0); buffer.writeUInt8(response.shareType, 2); buffer.writeUInt8(response.reserved, 3); buffer.writeUInt32LE(response.shareFlags, 4); buffer.writeUInt32LE(response.capabilities, 8); buffer.writeUInt32LE(response.maximalAccess, 12); return buffer; } /** * Serialize SMB2 Create Response */ public serializeCreateResponse(response: SMB2CreateResponse): Buffer { const baseSize = 89; const contextSize = this.calculateCreateContextsSize(response.createContexts); const totalSize = baseSize + contextSize; const buffer = Buffer.allocUnsafe(totalSize); let offset = 0; buffer.writeUInt16LE(response.structureSize, offset); offset += 2; buffer.writeUInt8(response.oplockLevel, offset); offset += 1; buffer.writeUInt8(response.flags, offset); offset += 1; buffer.writeUInt32LE(response.createAction, offset); offset += 4; buffer.writeBigUInt64LE(response.creationTime, offset); offset += 8; buffer.writeBigUInt64LE(response.lastAccessTime, offset); offset += 8; buffer.writeBigUInt64LE(response.lastWriteTime, offset); offset += 8; buffer.writeBigUInt64LE(response.changeTime, offset); offset += 8; buffer.writeBigUInt64LE(response.allocationSize, offset); offset += 8; buffer.writeBigUInt64LE(response.endOfFile, offset); offset += 8; buffer.writeUInt32LE(response.fileAttributes, offset); offset += 4; buffer.writeUInt32LE(response.reserved2, offset); offset += 4; this.serializeFileId(response.fileId, buffer, offset); offset += 16; buffer.writeUInt32LE(response.createContextsOffset, offset); offset += 4; buffer.writeUInt32LE(response.createContextsLength, offset); offset += 4; // Serialize create contexts for (const context of response.createContexts) { offset += this.serializeCreateContext(context, buffer, offset); } return buffer.subarray(0, offset); } /** * Serialize SMB2 Close Response */ public serializeCloseResponse(response: SMB2CloseResponse): Buffer { const buffer = Buffer.allocUnsafe(60); let offset = 0; buffer.writeUInt16LE(response.structureSize, offset); offset += 2; buffer.writeUInt16LE(response.flags, offset); offset += 2; buffer.writeUInt32LE(response.reserved, offset); offset += 4; buffer.writeBigUInt64LE(response.creationTime, offset); offset += 8; buffer.writeBigUInt64LE(response.lastAccessTime, offset); offset += 8; buffer.writeBigUInt64LE(response.lastWriteTime, offset); offset += 8; buffer.writeBigUInt64LE(response.changeTime, offset); offset += 8; buffer.writeBigUInt64LE(response.allocationSize, offset); offset += 8; buffer.writeBigUInt64LE(response.endOfFile, offset); offset += 8; buffer.writeUInt32LE(response.fileAttributes, offset); offset += 4; return buffer; } /** * Serialize SMB2 Read Response */ public serializeReadResponse(response: SMB2ReadResponse): Buffer { const totalSize = 17 + response.dataLength; const buffer = Buffer.allocUnsafe(totalSize); let offset = 0; buffer.writeUInt16LE(response.structureSize, offset); offset += 2; buffer.writeUInt8(response.dataOffset, offset); offset += 1; buffer.writeUInt8(0, offset); offset += 1; // Reserved buffer.writeUInt32LE(response.dataLength, offset); offset += 4; buffer.writeUInt32LE(response.dataRemaining, offset); offset += 4; buffer.writeUInt32LE(response.reserved, offset); offset += 4; // Copy data if (response.dataLength > 0) { response.data.copy(buffer, offset, 0, response.dataLength); } return buffer; } /** * Serialize SMB2 Write Response */ public serializeWriteResponse(response: SMB2WriteResponse): Buffer { const totalSize = 17 + response.writeChannelInfoLength; const buffer = Buffer.allocUnsafe(totalSize); let offset = 0; buffer.writeUInt16LE(response.structureSize, offset); offset += 2; buffer.writeUInt16LE(response.reserved, offset); offset += 2; buffer.writeUInt32LE(response.count, offset); offset += 4; buffer.writeUInt32LE(response.remaining, offset); offset += 4; buffer.writeUInt16LE(response.writeChannelInfoOffset, offset); offset += 2; buffer.writeUInt16LE(response.writeChannelInfoLength, offset); offset += 2; // Copy write channel info if (response.writeChannelInfoLength > 0) { response.writeChannelInfo.copy(buffer, offset, 0, response.writeChannelInfoLength); } return buffer; } /** * Serialize SMB2 Query Directory Response */ public serializeQueryDirectoryResponse(response: SMB2QueryDirectoryResponse): Buffer { const totalSize = 9 + response.outputBufferLength; const buffer = Buffer.allocUnsafe(totalSize); let offset = 0; buffer.writeUInt16LE(response.structureSize, offset); offset += 2; buffer.writeUInt16LE(response.outputBufferOffset, offset); offset += 2; buffer.writeUInt32LE(response.outputBufferLength, offset); offset += 4; // Copy output buffer if (response.outputBufferLength > 0) { response.outputBuffer.copy(buffer, offset, 0, response.outputBufferLength); } return buffer; } /** * Serialize SMB2 Query Info Response */ public serializeQueryInfoResponse(response: SMB2QueryInfoResponse): Buffer { const totalSize = 9 + response.outputBufferLength; const buffer = Buffer.allocUnsafe(totalSize); let offset = 0; buffer.writeUInt16LE(response.structureSize, offset); offset += 2; buffer.writeUInt16LE(response.outputBufferOffset, offset); offset += 2; buffer.writeUInt32LE(response.outputBufferLength, offset); offset += 4; // Copy output buffer if (response.outputBufferLength > 0) { response.outputBuffer.copy(buffer, offset, 0, response.outputBufferLength); } return buffer; } /** * Serialize SMB2 Set Info Response */ public serializeSetInfoResponse(response: SMB2SetInfoResponse): Buffer { const buffer = Buffer.allocUnsafe(2); buffer.writeUInt16LE(response.structureSize, 0); return buffer; } /** * Serialize SMB2 Error Response */ public serializeErrorResponse(response: SMB2ErrorResponse): Buffer { const totalSize = 9 + response.byteCount; const buffer = Buffer.allocUnsafe(totalSize); let offset = 0; buffer.writeUInt16LE(response.structureSize, offset); offset += 2; buffer.writeUInt16LE(response.reserved, offset); offset += 2; buffer.writeUInt32LE(response.byteCount, offset); offset += 4; // Copy error data if (response.byteCount > 0) { response.errorData.copy(buffer, offset, 0, response.byteCount); } return buffer; } /** * Serialize File Basic Information */ public serializeFileBasicInformation(info: FileBasicInformation): Buffer { const buffer = Buffer.allocUnsafe(40); let offset = 0; buffer.writeBigUInt64LE(info.creationTime, offset); offset += 8; buffer.writeBigUInt64LE(info.lastAccessTime, offset); offset += 8; buffer.writeBigUInt64LE(info.lastWriteTime, offset); offset += 8; buffer.writeBigUInt64LE(info.changeTime, offset); offset += 8; buffer.writeUInt32LE(info.fileAttributes, offset); offset += 4; buffer.writeUInt32LE(info.reserved, offset); offset += 4; return buffer; } /** * Serialize File Standard Information */ public serializeFileStandardInformation(info: FileStandardInformation): Buffer { const buffer = Buffer.allocUnsafe(24); let offset = 0; buffer.writeBigUInt64LE(info.allocationSize, offset); offset += 8; buffer.writeBigUInt64LE(info.endOfFile, offset); offset += 8; buffer.writeUInt32LE(info.numberOfLinks, offset); offset += 4; buffer.writeUInt8(info.deletePending ? 1 : 0, offset); offset += 1; buffer.writeUInt8(info.directory ? 1 : 0, offset); offset += 1; buffer.writeUInt16LE(info.reserved, offset); offset += 2; return buffer; } /** * Serialize File Directory Information */ public serializeFileDirectoryInformation(info: FileDirectoryInformation): Buffer { const fileNameBytes = Buffer.from(info.fileName, 'utf16le'); const totalSize = 64 + fileNameBytes.length; const buffer = Buffer.allocUnsafe(totalSize); let offset = 0; buffer.writeUInt32LE(info.nextEntryOffset, offset); offset += 4; buffer.writeUInt32LE(info.fileIndex, offset); offset += 4; buffer.writeBigUInt64LE(info.creationTime, offset); offset += 8; buffer.writeBigUInt64LE(info.lastAccessTime, offset); offset += 8; buffer.writeBigUInt64LE(info.lastWriteTime, offset); offset += 8; buffer.writeBigUInt64LE(info.changeTime, offset); offset += 8; buffer.writeBigUInt64LE(info.endOfFile, offset); offset += 8; buffer.writeBigUInt64LE(info.allocationSize, offset); offset += 8; buffer.writeUInt32LE(info.fileAttributes, offset); offset += 4; buffer.writeUInt32LE(info.fileNameLength, offset); offset += 4; // Copy filename fileNameBytes.copy(buffer, offset); return buffer; } /** * Serialize SMB2 File ID */ public serializeFileId(fileId: SMB2FileId, buffer: Buffer, offset: number): void { buffer.writeBigUInt64LE(fileId.persistent, offset); buffer.writeBigUInt64LE(fileId.volatile, offset + 8); } /** * Serialize SMB2 GUID */ public serializeGuid(guid: SMB2Guid, buffer: Buffer, offset: number): void { // Convert hex string to bytes const guidBytes = Buffer.from(guid.replace(/-/g, ''), 'hex'); if (guidBytes.length !== 16) { throw new Error('Invalid GUID format'); } guidBytes.copy(buffer, offset); } /** * Serialize UTF-16LE string */ public serializeUtf16String(str: string, buffer: Buffer, offset: number): number { const stringBytes = Buffer.from(str, 'utf16le'); stringBytes.copy(buffer, offset); return stringBytes.length; } /** * Serialize FILETIME (Windows 64-bit timestamp) */ public serializeFileTime(date: Date, buffer: Buffer, offset: number): void { const filetime = this.dateToFileTime(date); buffer.writeBigUInt64LE(filetime, offset); } /** * Serialize SMB2 Negotiate Context */ private serializeNegotiateContext(context: SMB2NegotiateContext, buffer: Buffer, offset: number): number { const startOffset = offset; buffer.writeUInt16LE(context.contextType, offset); offset += 2; buffer.writeUInt16LE(context.dataLength, offset); offset += 2; buffer.writeUInt32LE(context.reserved, offset); offset += 4; // Copy context data if (context.dataLength > 0) { context.data.copy(buffer, offset, 0, context.dataLength); offset += context.dataLength; } // Add padding to align to 8-byte boundary const padding = (8 - (offset % 8)) % 8; buffer.fill(0, offset, offset + padding); offset += padding; return offset - startOffset; } /** * Serialize SMB2 Create Context */ private serializeCreateContext(context: SMB2CreateContext, buffer: Buffer, offset: number): number { const startOffset = offset; const nameBytes = Buffer.from(context.name, 'utf16le'); buffer.writeUInt32LE(context.next, offset); offset += 4; buffer.writeUInt16LE(context.nameOffset, offset); offset += 2; buffer.writeUInt16LE(context.nameLength, offset); offset += 2; buffer.writeUInt16LE(context.reserved, offset); offset += 2; buffer.writeUInt16LE(context.dataOffset, offset); offset += 2; buffer.writeUInt32LE(context.dataLength, offset); offset += 4; // Copy name nameBytes.copy(buffer, offset); offset += nameBytes.length; // Add padding const namePadding = (8 - (offset % 8)) % 8; buffer.fill(0, offset, offset + namePadding); offset += namePadding; // Copy data if (context.dataLength > 0) { context.data.copy(buffer, offset, 0, context.dataLength); offset += context.dataLength; } // Add padding to align to 8-byte boundary const dataPadding = (8 - (offset % 8)) % 8; buffer.fill(0, offset, offset + dataPadding); offset += dataPadding; return offset - startOffset; } /** * Calculate total size needed for negotiate contexts */ private calculateNegotiateContextsSize(contexts: SMB2NegotiateContext[]): number { let totalSize = 0; for (const context of contexts) { totalSize += 8 + context.dataLength; // Header + data totalSize += (8 - (totalSize % 8)) % 8; // Padding } return totalSize; } /** * Calculate total size needed for create contexts */ private calculateCreateContextsSize(contexts: SMB2CreateContext[]): number { let totalSize = 0; for (const context of contexts) { const nameLength = Buffer.from(context.name, 'utf16le').length; totalSize += 16 + nameLength + context.dataLength; // Header + name + data totalSize += (8 - (totalSize % 8)) % 8; // Padding } return totalSize; } /** * Convert JavaScript Date to FILETIME */ private dateToFileTime(date: Date): bigint { // FILETIME is 100-nanosecond intervals since January 1, 1601 const FILETIME_EPOCH_DIFF = 11644473600000n; // milliseconds between 1601 and 1970 const timestamp = BigInt(date.getTime()) + FILETIME_EPOCH_DIFF; return timestamp * 10000n; // Convert to 100-nanosecond intervals } /** * Pad buffer to specified alignment */ private padToAlignment(buffer: Buffer, offset: number, alignment: number): number { const padding = (alignment - (offset % alignment)) % alignment; buffer.fill(0, offset, offset + padding); return offset + padding; } /** * Get the serialized size of a structure */ public getSerializedSize(type: string, data: any): number { switch (type) { case 'header': return SMB2_HEADER_SIZE; case 'transform-header': return SMB2_TRANSFORM_HEADER_SIZE; case 'file-id': return 16; case 'guid': return 16; case 'file-basic-info': return 40; case 'file-standard-info': return 24; case 'file-directory-info': return 64 + Buffer.from(data.fileName, 'utf16le').length; default: throw new Error(`Unknown type: ${type}`); } } }