diff --git a/packets/helpers.go b/packets/helpers.go index c8985e5..99da77a 100644 --- a/packets/helpers.go +++ b/packets/helpers.go @@ -8,22 +8,38 @@ import ( "io" "git.sharkk.net/EQ2/Protocol/crypto" + "git.sharkk.net/EQ2/Protocol/opcodes" ) -// ValidateCRC validates packet CRC using EQ2's custom CRC16 +// ValidateCRC validates packet CRC using EQ2's custom CRC16 (matches C++ EQProtocolPacket::ValidateCRC) func ValidateCRC(buffer []byte, key uint32) bool { if len(buffer) < 2 { return false } - // Extract CRC from last 2 bytes (EQ2 uses CRC16) + // Session packets are not CRC protected + if len(buffer) >= 2 && buffer[0] == 0x00 && + (buffer[1] == byte(opcodes.OP_SessionRequest) || + buffer[1] == byte(opcodes.OP_SessionResponse) || + buffer[1] == byte(opcodes.OP_OutOfSession)) { + return true + } + + // Combined application packets are also exempt (OP_AppCombined = 0x0019) + if len(buffer) >= 4 && buffer[2] == 0x00 && buffer[3] == 0x19 { + return true + } + + // All other packets must have valid CRC + // Extract CRC from last 2 bytes (network byte order) packetCRC := binary.BigEndian.Uint16(buffer[len(buffer)-2:]) // Calculate CRC on data portion (excluding CRC bytes) data := buffer[:len(buffer)-2] calculatedCRC := crypto.CalculateCRC(data, key) - return packetCRC == calculatedCRC + // Valid if no CRC present (packetCRC == 0) or CRCs match + return packetCRC == 0 || calculatedCRC == packetCRC } // AppendCRC appends CRC16 to packet buffer using EQ2's custom CRC @@ -214,27 +230,72 @@ func DecompressPacket(buffer []byte) ([]byte, error) { return buffer, nil } -// ChatEncode encodes chat data using EQ's XOR-based encoding +// ChatEncode encodes chat data using EQ's XOR-based encoding (matches C++ EQProtocolPacket::ChatEncode) +// Uses 4-byte block XOR with rolling key that updates from encrypted data func ChatEncode(buffer []byte, encodeKey int) { - if len(buffer) == 0 || encodeKey == 0 { + if len(buffer) <= 2 || encodeKey == 0 { return } - key := byte(encodeKey & 0xFF) - for i := range buffer { - buffer[i] ^= key - // Rotate key for next byte - key = ((key << 1) | (key >> 7)) & 0xFF - // Add position-based variation - if i%3 == 0 { - key ^= byte(i & 0xFF) - } + // Skip encoding for certain packet types (matches C++ conditions) + if buffer[1] == 0x01 || buffer[0] == 0x02 || buffer[0] == 0x1d { + return + } + + // Skip first 2 bytes (opcode) + data := buffer[2:] + size := len(data) + key := int32(encodeKey) + + // Encode 4-byte blocks with rolling key + i := 0 + for ; i+4 <= size; i += 4 { + // Read 4 bytes as int32 (little-endian like C++) + pt := binary.LittleEndian.Uint32(data[i:i+4]) + encrypted := pt ^ uint32(key) + key = int32(encrypted) // Update key with encrypted data + binary.LittleEndian.PutUint32(data[i:i+4], encrypted) + } + + // Encode remaining bytes with last key byte + keyByte := byte(key & 0xFF) + for ; i < size; i++ { + data[i] ^= keyByte } } -// ChatDecode decodes chat data (XOR is symmetric) +// ChatDecode decodes chat data using EQ's XOR-based encoding (matches C++ EQProtocolPacket::ChatDecode) +// Uses 4-byte block XOR with rolling key that updates from encrypted data func ChatDecode(buffer []byte, decodeKey int) { - ChatEncode(buffer, decodeKey) + if len(buffer) <= 2 || decodeKey == 0 { + return + } + + // Skip decoding for certain packet types (matches C++ conditions) + if buffer[1] == 0x01 || buffer[0] == 0x02 || buffer[0] == 0x1d { + return + } + + // Skip first 2 bytes (opcode) + data := buffer[2:] + size := len(data) + key := int32(decodeKey) + + // Decode 4-byte blocks with rolling key + i := 0 + for ; i+4 <= size; i += 4 { + // Read 4 bytes as int32 (little-endian like C++) + encrypted := binary.LittleEndian.Uint32(data[i:i+4]) + decrypted := encrypted ^ uint32(key) + key = int32(encrypted) // Update key with encrypted data (before decryption) + binary.LittleEndian.PutUint32(data[i:i+4], decrypted) + } + + // Decode remaining bytes with last key byte + keyByte := byte(key & 0xFF) + for ; i < size; i++ { + data[i] ^= keyByte + } } // IsChatPacket checks if opcode is a chat-related packet @@ -256,7 +317,7 @@ func longToIP(ip uint32) string { byte(ip>>24), byte(ip>>16), byte(ip>>8), byte(ip)) } -// IsProtocolPacket checks if buffer contains a valid protocol packet +// IsProtocolPacket checks if buffer contains a valid protocol packet (matches C++ EQProtocolPacket::IsProtocolPacket) func IsProtocolPacket(buffer []byte) bool { if len(buffer) < 2 { return false @@ -264,21 +325,23 @@ func IsProtocolPacket(buffer []byte) bool { opcode := binary.BigEndian.Uint16(buffer[:2]) - validOpcodes := map[uint16]bool{ - 0x0001: true, // OP_SessionRequest - 0x0002: true, // OP_SessionResponse - 0x0003: true, // OP_Combined - 0x0005: true, // OP_SessionDisconnect - 0x0006: true, // OP_KeepAlive - 0x0007: true, // OP_SessionStatRequest - 0x0008: true, // OP_SessionStatResponse - 0x0009: true, // OP_Packet - 0x000d: true, // OP_Fragment - 0x0015: true, // OP_Ack - 0x0019: true, // OP_AppCombined - 0x001d: true, // OP_OutOfOrderAck - 0x001e: true, // OP_OutOfSession + // Check against known protocol opcodes + switch opcode { + case opcodes.OP_SessionRequest, + opcodes.OP_SessionResponse, + opcodes.OP_Combined, + opcodes.OP_SessionDisconnect, + opcodes.OP_KeepAlive, + opcodes.OP_SessionStatRequest, + opcodes.OP_SessionStatResponse, + opcodes.OP_Packet, + opcodes.OP_Fragment, + opcodes.OP_Ack, + opcodes.OP_AppCombined, + opcodes.OP_OutOfOrderAck, + opcodes.OP_OutOfSession: + return true + default: + return false } - - return validOpcodes[opcode] } diff --git a/packets/protopacket.go b/packets/protopacket.go index 103b746..b56c8dc 100644 --- a/packets/protopacket.go +++ b/packets/protopacket.go @@ -2,6 +2,7 @@ package packets import ( "encoding/binary" + "fmt" "time" "git.sharkk.net/EQ2/Protocol/crypto" @@ -516,3 +517,45 @@ func (p *ProtoPacket) Copy() *ProtoPacket { newPacket.Packet.CopyInfo(p.Packet) return newPacket } + +// GetOpcodeName returns human-readable name for the opcode (matches C++ EQ2Packet::GetOpcodeName) +func (p *ProtoPacket) GetOpcodeName() string { + // Use manager to get opcode name if available + if p.manager != nil { + if name := p.manager.EmuToName(p.LoginOp); name != "" { + return name + } + } + + // Fall back to protocol opcode names + switch p.Opcode { + case opcodes.OP_SessionRequest: + return "OP_SessionRequest" + case opcodes.OP_SessionResponse: + return "OP_SessionResponse" + case opcodes.OP_Combined: + return "OP_Combined" + case opcodes.OP_SessionDisconnect: + return "OP_SessionDisconnect" + case opcodes.OP_KeepAlive: + return "OP_KeepAlive" + case opcodes.OP_SessionStatRequest: + return "OP_SessionStatRequest" + case opcodes.OP_SessionStatResponse: + return "OP_SessionStatResponse" + case opcodes.OP_Packet: + return "OP_Packet" + case opcodes.OP_Fragment: + return "OP_Fragment" + case opcodes.OP_Ack: + return "OP_Ack" + case opcodes.OP_AppCombined: + return "OP_AppCombined" + case opcodes.OP_OutOfOrderAck: + return "OP_OutOfOrderAck" + case opcodes.OP_OutOfSession: + return "OP_OutOfSession" + default: + return fmt.Sprintf("Unknown(0x%04X)", p.Opcode) + } +}