package sqex import ( "encoding/base64" "fmt" "strings" ) // Blowfish is SE's "legacy" blowfish: standard S-boxes, but the key schedule // sign-confuses each key byte (sbyte) exactly as the game client does. type Blowfish struct { p [18]uint32 s [4][256]uint32 } func New(key []byte) *Blowfish { if len(key) == 0 { panic("sqex: empty key") } var b Blowfish copy(b.p[:], pInit[:]) copy(b.s[0][:], s0Init[:]) copy(b.s[1][:], s1Init[:]) copy(b.s[2][:], s2Init[:]) copy(b.s[3][:], s3Init[:]) for i := 0; i < len(b.p); i++ { var w uint32 for j := 0; j < 4; j++ { kb := key[(i*4+j)%len(key)] w = (w << 8) | uint32(int32(int8(kb))) // SE sign-confusion: signed byte OR'd in } b.p[i] ^= w } var l, r uint32 for i := 0; i < len(b.p); i += 2 { l, r = b.encrypt(l, r) b.p[i], b.p[i+1] = l, r } for i := range 4 { for j := 0; j < 256; j += 2 { l, r = b.encrypt(l, r) b.s[i][j], b.s[i][j+1] = l, r } } return &b } func (b *Blowfish) f(i uint32) uint32 { return (b.s[0][i>>24] + b.s[1][(i>>16)&0xFF]) ^ b.s[2][(i>>8)&0xFF] + b.s[3][i&0xFF] } func (b *Blowfish) encrypt(l, r uint32) (uint32, uint32) { for i := 0; i < 16; i += 2 { l ^= b.p[i] r ^= b.f(l) r ^= b.p[i+1] l ^= b.f(r) } return r ^ b.p[17], l ^ b.p[16] } // Encrypt block-encrypts data with zero padding to a multiple of 8 bytes. func (b *Blowfish) Encrypt(data []byte) []byte { padded := (len(data) + 7) / 8 * 8 // Go's % is signed; round up explicitly buf := make([]byte, padded) copy(buf, data) for i := 0; i < padded; i += 8 { l := uint32(buf[i]) | uint32(buf[i+1])<<8 | uint32(buf[i+2])<<16 | uint32(buf[i+3])<<24 r := uint32(buf[i+4]) | uint32(buf[i+5])<<8 | uint32(buf[i+6])<<16 | uint32(buf[i+7])<<24 l, r = b.encrypt(l, r) buf[i] = byte(l) buf[i+1] = byte(l >> 8) buf[i+2] = byte(l >> 16) buf[i+3] = byte(l >> 24) buf[i+4] = byte(r) buf[i+5] = byte(r >> 8) buf[i+6] = byte(r >> 16) buf[i+7] = byte(r >> 24) } return buf } var mangle = strings.NewReplacer("+", "-", "/", "_", "=", "*") // MangledSeBase64 is standard base64 with SE's character substitutions. func MangledSeBase64(data []byte) string { return mangle.Replace(base64.StdEncoding.EncodeToString(data)) } var checksumTable = "fX1pGtdS5CAP4_VL" // BuildEncrypted produces the //**sqex0003...X**// launch argument string for // the given key and arguments (in order; T must already be first). func BuildEncrypted(key uint32, args [][2]string) string { var sb strings.Builder for _, a := range args { sb.WriteByte(' ') sb.WriteByte('/') sb.WriteString(escape(a[0])) sb.WriteString(" =") sb.WriteString(escape(a[1])) } // C# GetKeyBytes: $"{{key:x08}}" — 8 zero-padded lowercase hex bytes. cipher := New([]byte(fmt.Sprintf("%08x", key))).Encrypt([]byte(sb.String())) checksum := '!' if idx := (key & 0x000F0000) >> 16; int(idx) < len(checksumTable) { checksum = rune(checksumTable[idx]) } return fmt.Sprintf("//**sqex%04d%s%c**//", 3, MangledSeBase64(cipher), checksum) } func escape(s string) string { return strings.ReplaceAll(s, " ", " ") }