1
0

fix chat encoding

This commit is contained in:
Sky Johnson 2025-09-03 20:48:08 -05:00
parent 1a9b7effa6
commit 4ecdcc868f
2 changed files with 139 additions and 33 deletions

View File

@ -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]
}

View File

@ -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)
}
}