working
This commit is contained in:
59
pkg/protocol/commands.go
Normal file
59
pkg/protocol/commands.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Command Parsing Helpers
|
||||
func ParseCommand(data []byte) (string, map[string]string) {
|
||||
s := string(data)
|
||||
parts := strings.Split(s, " ")
|
||||
cmd := parts[0]
|
||||
args := make(map[string]string)
|
||||
|
||||
for _, p := range parts[1:] {
|
||||
kv := strings.SplitN(p, "=", 2)
|
||||
if len(kv) == 2 {
|
||||
val := Unescape(kv[1])
|
||||
args[kv[0]] = val
|
||||
} else {
|
||||
args[p] = ""
|
||||
}
|
||||
}
|
||||
return cmd, args
|
||||
}
|
||||
|
||||
// Unescape TS3 string
|
||||
func Unescape(s string) string {
|
||||
r := strings.NewReplacer(
|
||||
`\/`, `/`,
|
||||
`\s`, ` `,
|
||||
`\p`, `|`,
|
||||
`\a`, "\a",
|
||||
`\b`, "\b",
|
||||
`\f`, "\f",
|
||||
`\n`, "\n",
|
||||
`\r`, "\r",
|
||||
`\t`, "\t",
|
||||
`\v`, "\v",
|
||||
`\\`, `\`,
|
||||
)
|
||||
return r.Replace(s)
|
||||
}
|
||||
|
||||
func Escape(s string) string {
|
||||
r := strings.NewReplacer(
|
||||
`\`, `\\`,
|
||||
`/`, `\/`,
|
||||
` `, `\s`,
|
||||
`|`, `\p`,
|
||||
"\a", `\a`,
|
||||
"\b", `\b`,
|
||||
"\f", `\f`,
|
||||
"\n", `\n`,
|
||||
"\r", `\r`,
|
||||
"\t", `\t`,
|
||||
"\v", `\v`,
|
||||
)
|
||||
return r.Replace(s)
|
||||
}
|
||||
182
pkg/protocol/crypto.go
Normal file
182
pkg/protocol/crypto.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Crypto Globals for Handshake (as per spec)
|
||||
var (
|
||||
HandshakeMac = [8]byte{0x54, 0x53, 0x33, 0x49, 0x4E, 0x49, 0x54, 0x31} // "TS3INIT1"
|
||||
HandshakeKey = []byte{0x63, 0x3A, 0x5C, 0x77, 0x69, 0x6E, 0x64, 0x6F, 0x77, 0x73, 0x5C, 0x73, 0x79, 0x73, 0x74, 0x65} // "c:\windows\syste"
|
||||
HandshakeNonce = []byte{0x6D, 0x5C, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6C, 0x6C, 0x33, 0x32, 0x2E, 0x63, 0x70, 0x6C} // "m\firewall32.cpl"
|
||||
)
|
||||
|
||||
// EAX Mode Implementation (AES-CTR + OMAC) -> For simplicity we will use a simplified approach or find a library if possible.
|
||||
// Since Go doesn't have EAX in standard lib, and we want to avoid complex dependencies if possible for this snippet,
|
||||
// we'll implement the key generation logic. The EAX encryption usually wraps AES-CTR.
|
||||
// Important: For a production client, use a verified crypto library.
|
||||
|
||||
type CryptoState struct {
|
||||
SharedIV []byte
|
||||
SharedMac []byte
|
||||
GenerationID uint32
|
||||
}
|
||||
|
||||
// GenerateKeyNonce generates the Key and Nonce for EAX encryption/decryption as per section 1.6.2
|
||||
func (s *CryptoState) GenerateKeyNonce(header *PacketHeader, isClientToServer bool) ([]byte, []byte) {
|
||||
// 1. Temporary buffer
|
||||
// length depends on protocol version/logic.
|
||||
// Spec: "The old protocol SharedIV ... will be 20 bytes long ... new protocol SharedIV ... will have 64 bytes"
|
||||
// GenerationID starts at 0.
|
||||
|
||||
var temp []byte
|
||||
sivLen := len(s.SharedIV)
|
||||
|
||||
if sivLen == 20 {
|
||||
temp = make([]byte, 26)
|
||||
} else {
|
||||
// Spec 1.6.2 New Protocol: 1 (type) + 1 (pt) + 4 (pgid) + 64 (sharedIV) = 70 bytes.
|
||||
temp = make([]byte, 70)
|
||||
}
|
||||
|
||||
// temp[0] 'c'/'s' logic:
|
||||
// Spec: "0x30 for Client, 0x31 for Server... Wait.
|
||||
// "Used for Client -> Server: 0x31"
|
||||
// "Used for Server -> Client: 0x30"
|
||||
if isClientToServer {
|
||||
temp[0] = 0x31
|
||||
} else { // Client <- Server
|
||||
temp[0] = 0x30
|
||||
}
|
||||
|
||||
// temp[1] = PT (Base Type only, no flags!)
|
||||
// ts3j uses getType().getIndex() which is the enum value.
|
||||
temp[1] = header.Type & 0x0F
|
||||
|
||||
// temp[2..6] = PGId (Network Order)
|
||||
binary.BigEndian.PutUint32(temp[2:6], s.GenerationID)
|
||||
|
||||
// Copy SharedIV
|
||||
// if SIV.length == 20 -> temp[6..26] = SIV[0..20]
|
||||
// if SIV.length == 64 -> temp[6..70] = SIV[0..64]
|
||||
copy(temp[6:], s.SharedIV)
|
||||
|
||||
// keynonce = sha256(temp)
|
||||
hash := sha256.Sum256(temp)
|
||||
|
||||
key := make([]byte, 16)
|
||||
nonce := make([]byte, 16)
|
||||
|
||||
copy(key, hash[0:16])
|
||||
copy(nonce, hash[16:32])
|
||||
|
||||
// XOR Key with PID
|
||||
// key[0] = key[0] xor ((PId & 0xFF00) >> 8)
|
||||
// key[1] = key[1] xor ((PId & 0x00FF) >> 0)
|
||||
key[0] ^= byte((header.PacketID & 0xFF00) >> 8)
|
||||
key[1] ^= byte((header.PacketID & 0x00FF))
|
||||
|
||||
return key, nonce
|
||||
}
|
||||
|
||||
// Base64 Helpers for TS3
|
||||
func Base64Encode(data []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(data)
|
||||
}
|
||||
|
||||
func Base64Decode(s string) ([]byte, error) {
|
||||
return base64.StdEncoding.DecodeString(s)
|
||||
}
|
||||
|
||||
// SHA1 Helper
|
||||
func Sha1(data []byte) []byte {
|
||||
h := sha1.New()
|
||||
h.Write(data)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// GenerateHashCash implements the puzzle solver (Section 4.1)
|
||||
// Finds the level of a public key + offset
|
||||
func GetHashCashLevel(key []byte, offset uint64) int {
|
||||
// data = sha1(publicKey + keyOffset)
|
||||
// Note: Reference implementation concatenates the string representation of offset?
|
||||
// Spec says: "The key offset is a u64 number, which gets converted to a string when concatenated."
|
||||
|
||||
offsetStr := fmt.Sprintf("%d", offset)
|
||||
buf := append(key, []byte(offsetStr)...)
|
||||
hash := Sha1(buf)
|
||||
|
||||
// Count leading zero bits
|
||||
level := 0
|
||||
for _, b := range hash {
|
||||
if b == 0 {
|
||||
level += 8
|
||||
} else {
|
||||
// Count bits in this byte
|
||||
for i := 7; i >= 0; i-- {
|
||||
if (b & (1 << i)) == 0 {
|
||||
level++
|
||||
} else {
|
||||
return level
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return level
|
||||
}
|
||||
|
||||
// Fake encryption for now to proceed until EAX is fully implemented or needed
|
||||
// The spec requires EAX for commands.
|
||||
func EncryptPacket(pkt *Packet, key, nonce []byte) error {
|
||||
// Placeholder: In a real implementation, we would use an EAX implementation here.
|
||||
// For the initial handshake (Init1 packets), encryption is often disabled or simplified (XOR).
|
||||
// Reviewing: Init1 packets are NOT encrypted (FlagUnencrypted set).
|
||||
// Command packets ARE encrypted.
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Standard CTR implementation (part of EAX)
|
||||
// EAX components: CTR for encryption, OMAC for authentication (MAC)
|
||||
// We strictly need the MAC for the packet header.
|
||||
|
||||
// For minimal functional requirement "Connect", we might need full EAX if the server enforces it on the first Command.
|
||||
// For now we will implement standard CTR for the data payload.
|
||||
// The MAC calculation is complex and requires OMAC1.
|
||||
|
||||
stream := cipher.NewCTR(block, nonce)
|
||||
stream.XORKeyStream(pkt.Data, pkt.Data)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DecryptPacket(pkt *Packet, key, nonce []byte) error {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stream := cipher.NewCTR(block, nonce)
|
||||
stream.XORKeyStream(pkt.Data, pkt.Data)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Curve25519 Helper
|
||||
func GenerateECDHKeypair() (public, private [32]byte, err error) {
|
||||
// Generate private key (use real random in prod)
|
||||
// For reproduction/dev we might use fixed.
|
||||
// Actually we need `crypto/rand`
|
||||
// private = rand(32)
|
||||
// curve25519.ScalarBaseMult(&public, &private)
|
||||
return
|
||||
}
|
||||
101
pkg/protocol/crypto_init2_test.go
Normal file
101
pkg/protocol/crypto_init2_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"filippo.io/edwards25519"
|
||||
)
|
||||
|
||||
// Helper to match ts3j hex string decoding
|
||||
func hexBytes(s string) []byte {
|
||||
b, _ := hex.DecodeString(s)
|
||||
return b
|
||||
}
|
||||
|
||||
func TestCryptoInit2_Ts3jVectors(t *testing.T) {
|
||||
// Vectors from ts3j CryptoInit2Test.java
|
||||
licenseBytes := hexBytes("0100358541498A24ACD30157918B8F50955C0DAE970AB65372CBE407" +
|
||||
"415FCF3E029B02084D15E00AA793600700000020416E6F6E796D6F7573000047D9E4DC25AA2E90ACD4DB5FA61C8F" +
|
||||
"ED369B346D84C2CA2FCCCA86F73AFEF092200A77C8810A787141")
|
||||
alpha := hexBytes("9500A5DB3B50ACECAB81")
|
||||
beta := hexBytes("EAFFC9A8BC996B25C8AA700264E99E372ECCDEB1C121D6EC0F4D49FB46" +
|
||||
"CEEBA4E3C724B3070FD70CB03D7BC08129205690ECE228CA7C")
|
||||
privateKeyBytes := hexBytes("102E591ABA4508129E812FF3437E2DDD3CA1F1EC341117CA35" +
|
||||
"14CC347A7C2A77")
|
||||
|
||||
expectedIvStructHex := "E4082A92F71C96A947452F5582EF2879B2051ED2D3" +
|
||||
"F2C6B0643CF5A266EE6B5180573C2F5F3F1C4AC579188366F16AE0EADC3AAF860805D8F2A831E9E49F4513"
|
||||
expectedFakeSigHex := "54F2B4D661E0F9AB"
|
||||
|
||||
// 1. Derive Server Public Key (License)
|
||||
serverPubBytes, err := ParseLicenseAndDeriveKey(licenseBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseLicenseAndDeriveKey failed: %v", err)
|
||||
}
|
||||
|
||||
// 2. Derive Shared Secret (simulating ts3j generateSharedSecret2)
|
||||
|
||||
// Load Server Point
|
||||
serverPoint, err := new(edwards25519.Point).SetBytes(serverPubBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("Invalid server pub key derived: %v", err)
|
||||
}
|
||||
|
||||
// Negate Server Point
|
||||
serverPoint.Negate(serverPoint)
|
||||
|
||||
// Create Scalar from private key
|
||||
scalarBytes := make([]byte, 32)
|
||||
copy(scalarBytes, privateKeyBytes)
|
||||
scalarBytes[31] &= 0x7F // ts3j specific masking
|
||||
|
||||
// Load Scalar
|
||||
scalar, err := new(edwards25519.Scalar).SetBytesWithClamping(scalarBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("Scalar load failed: %v", err)
|
||||
}
|
||||
|
||||
// Multiply
|
||||
sharedPoint := new(edwards25519.Point).ScalarMult(scalar, serverPoint)
|
||||
sharedBytes := sharedPoint.Bytes()
|
||||
|
||||
// Flip Sign
|
||||
sharedBytes[31] ^= 0x80
|
||||
|
||||
// Hash
|
||||
sharedSecret := sha512.Sum512(sharedBytes)
|
||||
|
||||
// 3. Calculate IV/Mac
|
||||
// IV = XOR(SharedSecret, Alpha) ++ XOR(SharedSecret, Beta)
|
||||
ivStruct := make([]byte, 64)
|
||||
copy(ivStruct, sharedSecret[:])
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
ivStruct[i] ^= alpha[i]
|
||||
}
|
||||
if len(beta) < 54 {
|
||||
t.Fatal("Beta too short")
|
||||
}
|
||||
for i := 0; i < 54; i++ {
|
||||
ivStruct[10+i] ^= beta[i]
|
||||
}
|
||||
|
||||
// FakeSig = SHA1(IV)[0..8]
|
||||
macHash := sha1.Sum(ivStruct)
|
||||
fakeSig := macHash[0:8]
|
||||
|
||||
// Compare
|
||||
gotIvHex := hex.EncodeToString(ivStruct)
|
||||
if !strings.EqualFold(gotIvHex, expectedIvStructHex) {
|
||||
t.Errorf("IV Struct Mismatch.\nGot: %s\nWant: %s", gotIvHex, expectedIvStructHex)
|
||||
}
|
||||
|
||||
gotSigHex := hex.EncodeToString(fakeSig)
|
||||
if !strings.EqualFold(gotSigHex, expectedFakeSigHex) {
|
||||
t.Errorf("FakeSig Mismatch.\nGot: %s\nWant: %s", gotSigHex, expectedFakeSigHex)
|
||||
}
|
||||
}
|
||||
56
pkg/protocol/crypto_test.go
Normal file
56
pkg/protocol/crypto_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateKeyNonce(t *testing.T) {
|
||||
// Vectors from ts3j EncryptionTest.java
|
||||
ivStructBase64 := "/rn6nR71hV8eFl+15WO68fRU8pOCBw3t0FmcG5c7WNxIkeZ1NtaWTVMBde0cdU5tTKwOl8sE6gpHjnCEF4hhDw=="
|
||||
expectedKeyBase64 := "BF+lO776+e45u+qYAOHihg=="
|
||||
expectedNonceBase64 := "1IVcTMuizpDHjQgn2yGCgg=="
|
||||
|
||||
ivStruct, _ := base64.StdEncoding.DecodeString(ivStructBase64)
|
||||
expectedKey, _ := base64.StdEncoding.DecodeString(expectedKeyBase64)
|
||||
expectedNonce, _ := base64.StdEncoding.DecodeString(expectedNonceBase64)
|
||||
|
||||
// CryptoState setup
|
||||
crypto := &CryptoState{
|
||||
SharedIV: ivStruct,
|
||||
GenerationID: 0,
|
||||
}
|
||||
|
||||
// Packet Header setup
|
||||
// Packet ID = 1
|
||||
// Type = COMMAND (2)
|
||||
// Flags = NEW_PROTOCOL (0x20)
|
||||
// Type in Header struct contains (Type | Flags)
|
||||
// byte(2) | byte(0x20) = 0x22
|
||||
|
||||
header := &PacketHeader{
|
||||
PacketID: 1,
|
||||
Type: uint8(PacketTypeCommand) | PacketFlagNewProtocol,
|
||||
}
|
||||
// Note: PacketTypeCommand is 0x02. PacketFlagNewProtocol is 0x20.
|
||||
// Header.Type is uint8.
|
||||
|
||||
// Generate (Client -> Server = true)
|
||||
// ts3j test uses ProtocolRole.CLIENT which maps to 0x31 for temporaryByteBuffer?
|
||||
// PacketTransformation.java: (header.getRole() == ProtocolRole.SERVER ? 0x30 : 0x31)
|
||||
// If EncryptionTest creates Packet(ProtocolRole.CLIENT), then header role is CLIENT.
|
||||
// So byte is 0x31.
|
||||
// In my crypto.go, GenerateKeyNonce(..., isClientToServer bool).
|
||||
// If client sends, isClientToServer should be true. (maps to 0x31).
|
||||
|
||||
key, nonce := crypto.GenerateKeyNonce(header, true)
|
||||
|
||||
if !reflect.DeepEqual(key, expectedKey) {
|
||||
t.Errorf("Key mismatch.\nGot: %x\nWant: %x", key, expectedKey)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(nonce, expectedNonce) {
|
||||
t.Errorf("Nonce mismatch.\nGot: %x\nWant: %x", nonce, expectedNonce)
|
||||
}
|
||||
}
|
||||
178
pkg/protocol/eax.go
Normal file
178
pkg/protocol/eax.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// OMAC1 (CMAC) Implementation for AES-128
|
||||
// RFC 4493
|
||||
|
||||
func shiftLeft(in []byte, out []byte) {
|
||||
var carry byte
|
||||
for i := 15; i >= 0; i-- {
|
||||
b := in[i]
|
||||
out[i] = (b << 1) | carry
|
||||
carry = (b >> 7) & 1
|
||||
}
|
||||
}
|
||||
|
||||
func xorBlock(a, b []byte) {
|
||||
for i := 0; i < 16; i++ {
|
||||
a[i] ^= b[i]
|
||||
}
|
||||
}
|
||||
|
||||
func generateSubkeys(cipherBlock cipher.Block) (k1, k2 []byte) {
|
||||
l := make([]byte, 16)
|
||||
cipherBlock.Encrypt(l, make([]byte, 16))
|
||||
|
||||
k1 = make([]byte, 16)
|
||||
shiftLeft(l, k1)
|
||||
if l[0]&0x80 != 0 {
|
||||
k1[15] ^= 0x87
|
||||
}
|
||||
|
||||
k2 = make([]byte, 16)
|
||||
shiftLeft(k1, k2)
|
||||
if k1[0]&0x80 != 0 {
|
||||
k2[15] ^= 0x87
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func omac1(cipherBlock cipher.Block, data []byte) []byte {
|
||||
k1, k2 := generateSubkeys(cipherBlock)
|
||||
|
||||
n := len(data)
|
||||
numBlocks := (n + 15) / 16
|
||||
if numBlocks == 0 {
|
||||
numBlocks = 1
|
||||
}
|
||||
|
||||
lastBlock := make([]byte, 16)
|
||||
flagComplete := (n > 0 && n%16 == 0)
|
||||
|
||||
if flagComplete {
|
||||
copy(lastBlock, data[n-16:])
|
||||
xorBlock(lastBlock, k1)
|
||||
} else {
|
||||
remaining := n % 16
|
||||
copy(lastBlock, data[n-remaining:])
|
||||
lastBlock[remaining] = 0x80 // padding
|
||||
xorBlock(lastBlock, k2)
|
||||
}
|
||||
|
||||
x := make([]byte, 16)
|
||||
y := make([]byte, 16)
|
||||
|
||||
for i := 0; i < numBlocks-1; i++ {
|
||||
copy(y, data[i*16:(i+1)*16])
|
||||
xorBlock(y, x)
|
||||
cipherBlock.Encrypt(x, y)
|
||||
}
|
||||
|
||||
xorBlock(x, lastBlock)
|
||||
cipherBlock.Encrypt(x, x)
|
||||
return x
|
||||
}
|
||||
|
||||
// EAX implementation functions
|
||||
|
||||
// omacWithTag computes OMAC(t || m)
|
||||
func omacWithTag(block cipher.Block, tag byte, m []byte) []byte {
|
||||
// EAX uses [t] as prefix for OMAC
|
||||
// but standard OMAC implies it's just the message.
|
||||
// EAX spec: OMAC^t_K(M) = OMAC_K ([t]_n || M)
|
||||
// where n is block size (16).
|
||||
|
||||
// We construct a new slice with the prefix
|
||||
buf := make([]byte, 16+len(m))
|
||||
buf[15] = tag // The last byte of the first block is the tag?
|
||||
// Wait, EAX spec: [t]_n is the byte t padded with zeros to block size n.
|
||||
// Usually it implies the last byte is t, or first?
|
||||
// "Let [t]_n denote the n-byte string that consists of n-1 zero bytes followed by the byte t."
|
||||
// So for n=16, 15 zeros then t.
|
||||
|
||||
copy(buf[16:], m)
|
||||
return omac1(block, buf)
|
||||
}
|
||||
|
||||
// EncryptEAX performs EAX encryption and returns ciphertext + mac
|
||||
// Key and Nonce must be valid for AES-128 (16 bytes).
|
||||
func EncryptEAX(key, nonce, header, data []byte) (ciphertext []byte, mac []byte, err error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 1. Calculate Nonce MAC (N)
|
||||
// N = OMAC^0_K(Nonce)
|
||||
nMac := omacWithTag(block, 0, nonce)
|
||||
|
||||
// 2. Calculate Header MAC (H)
|
||||
// H = OMAC^1_K(Header)
|
||||
hMac := omacWithTag(block, 1, header)
|
||||
|
||||
// 3. Encrypt Data (C)
|
||||
// CTR using N as counter/IV
|
||||
ciphertext = make([]byte, len(data))
|
||||
ctrStream := cipher.NewCTR(block, nMac)
|
||||
ctrStream.XORKeyStream(ciphertext, data)
|
||||
|
||||
// 4. Calculate Ciphertext MAC (C_MAC)
|
||||
// C_MAC = OMAC^2_K(Ciphertext)
|
||||
cMac := omacWithTag(block, 2, ciphertext)
|
||||
|
||||
// 5. Final Tag = N ^ H ^ C_MAC
|
||||
tag := make([]byte, 16)
|
||||
for i := 0; i < 16; i++ {
|
||||
tag[i] = nMac[i] ^ hMac[i] ^ cMac[i]
|
||||
}
|
||||
|
||||
// TS3 uses 8 bytes of the MAC
|
||||
return ciphertext, tag[:8], nil
|
||||
}
|
||||
|
||||
// DecryptEAX verifies and decrypts
|
||||
func DecryptEAX(key, nonce, header, data, expectedMac []byte) (plaintext []byte, err error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 1. N = OMAC^0_K(Nonce)
|
||||
nMac := omacWithTag(block, 0, nonce)
|
||||
|
||||
// 2. H = OMAC^1_K(Header)
|
||||
hMac := omacWithTag(block, 1, header)
|
||||
|
||||
// 3. C_MAC = OMAC^2_K(Data is Ciphertext here)
|
||||
cMac := omacWithTag(block, 2, data)
|
||||
|
||||
// 4. Calculate Tag
|
||||
tag := make([]byte, 16)
|
||||
for i := 0; i < 16; i++ {
|
||||
tag[i] = nMac[i] ^ hMac[i] ^ cMac[i]
|
||||
}
|
||||
|
||||
// Verify MAC
|
||||
if len(expectedMac) > 16 {
|
||||
return nil, errors.New("expected MAC too long")
|
||||
}
|
||||
|
||||
// Constant time compare needed ideally
|
||||
for i := 0; i < len(expectedMac); i++ {
|
||||
if tag[i] != expectedMac[i] {
|
||||
return nil, errors.New("mac verification failed")
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Decrypt
|
||||
plaintext = make([]byte, len(data))
|
||||
ctrStream := cipher.NewCTR(block, nMac)
|
||||
ctrStream.XORKeyStream(plaintext, data)
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
53
pkg/protocol/eax_test.go
Normal file
53
pkg/protocol/eax_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"encoding/hex"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOMAC1_RFC4493(t *testing.T) {
|
||||
// RFC 4493 Test Vectors for AES-128-CMAC
|
||||
keyHex := "2b7e151628aed2a6abf7158809cf4f3c"
|
||||
key, _ := hex.DecodeString(keyHex)
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Example 1: Empty Message
|
||||
msg1 := []byte{}
|
||||
want1Hex := "bb1d6929e95937287fa37d129b756746"
|
||||
want1, _ := hex.DecodeString(want1Hex)
|
||||
|
||||
got1 := omac1(block, msg1)
|
||||
if !reflect.DeepEqual(got1, want1) {
|
||||
t.Errorf("OMAC1 Empty Message Mismatch.\nGot: %x\nWant: %x", got1, want1)
|
||||
}
|
||||
|
||||
// Example 2: 16 bytes
|
||||
msg2Hex := "6bc1bee22e409f96e93d7e117393172a"
|
||||
msg2, _ := hex.DecodeString(msg2Hex)
|
||||
want2Hex := "070a16b46b4d4144f79bdd9dd04a287c"
|
||||
want2, _ := hex.DecodeString(want2Hex)
|
||||
|
||||
got2 := omac1(block, msg2)
|
||||
if !reflect.DeepEqual(got2, want2) {
|
||||
t.Errorf("OMAC1 16-byte Message Mismatch.\nGot: %x\nWant: %x", got2, want2)
|
||||
}
|
||||
|
||||
// Example 3: 40 bytes (not multiple of 16)
|
||||
msg3Hex := "6bc1bee22e409f96e93d7e117393172a" +
|
||||
"ae2d8a571e03ac9c9eb76fac45af8e51" +
|
||||
"30c81c46a35ce411"
|
||||
msg3, _ := hex.DecodeString(msg3Hex)
|
||||
want3Hex := "dfa66747de9ae63030ca32611497c827"
|
||||
want3, _ := hex.DecodeString(want3Hex)
|
||||
|
||||
got3 := omac1(block, msg3)
|
||||
if !reflect.DeepEqual(got3, want3) {
|
||||
t.Errorf("OMAC1 40-byte Message Mismatch.\nGot: %x\nWant: %x", got3, want3)
|
||||
}
|
||||
}
|
||||
39
pkg/protocol/enums.go
Normal file
39
pkg/protocol/enums.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package protocol
|
||||
|
||||
// Codec types
|
||||
const (
|
||||
CodecSpeexNarrowband = 0
|
||||
CodecSpeexWideband = 1
|
||||
CodecSpeexUltrawideband = 2
|
||||
CodecCeltMono = 3
|
||||
CodecOpusVoice = 4
|
||||
CodecOpusMusic = 5
|
||||
)
|
||||
|
||||
// Reason types for events
|
||||
type ReasonID int
|
||||
|
||||
const (
|
||||
ReasonNone ReasonID = 0
|
||||
ReasonMoved ReasonID = 1
|
||||
ReasonSubscription ReasonID = 2
|
||||
ReasonLostConnection ReasonID = 3
|
||||
ReasonKickChannel ReasonID = 4
|
||||
ReasonKickServer ReasonID = 5
|
||||
ReasonKickServerBan ReasonID = 6
|
||||
ReasonServerStop ReasonID = 7
|
||||
ReasonClientDisconnect ReasonID = 8
|
||||
ReasonChannelUpdate ReasonID = 9
|
||||
ReasonChannelEdit ReasonID = 10
|
||||
ReasonClientDisconnectServerShutdown ReasonID = 11
|
||||
)
|
||||
|
||||
// TextMessageTargetMode identifies who receives the message
|
||||
type TextMessageTargetMode int
|
||||
|
||||
const (
|
||||
TextMessageTarget_Unknown TextMessageTargetMode = 0
|
||||
TextMessageTarget_Client TextMessageTargetMode = 1
|
||||
TextMessageTarget_Channel TextMessageTargetMode = 2
|
||||
TextMessageTarget_Server TextMessageTargetMode = 3
|
||||
)
|
||||
152
pkg/protocol/license.go
Normal file
152
pkg/protocol/license.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha512"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"filippo.io/edwards25519"
|
||||
)
|
||||
|
||||
// Root Key (compressed Edwards25519 point)
|
||||
var LicenseRootKeyBytes = [32]byte{
|
||||
0xcd, 0x0d, 0xe2, 0xae, 0xd4, 0x63, 0x45, 0x50, 0x9a, 0x7e, 0x3c,
|
||||
0xfd, 0x8f, 0x68, 0xb3, 0xdc, 0x75, 0x55, 0xb2, 0x9d, 0xcc, 0xec,
|
||||
0x73, 0xcd, 0x18, 0x75, 0x0f, 0x99, 0x38, 0x12, 0x40, 0x8a,
|
||||
}
|
||||
|
||||
// ParseLicense and derive final Public Key
|
||||
func ParseLicenseAndDeriveKey(licenseData []byte) ([]byte, error) {
|
||||
// Header
|
||||
if len(licenseData) < 1 || licenseData[0] != 0x01 {
|
||||
return nil, errors.New("invalid license version")
|
||||
}
|
||||
|
||||
reader := bytes.NewReader(licenseData[1:])
|
||||
|
||||
// Initialize current key with Root
|
||||
currentPoint, err := new(edwards25519.Point).SetBytes(LicenseRootKeyBytes[:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load root key: %v", err)
|
||||
}
|
||||
|
||||
// Iterate blocks
|
||||
for reader.Len() > 0 {
|
||||
// Calculate Start Position
|
||||
bytesConsumed := (len(licenseData) - 1) - reader.Len()
|
||||
blockStartAbs := 1 + bytesConsumed
|
||||
|
||||
// Read KeyType
|
||||
_, err = reader.ReadByte()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read keyType failed: %v", err)
|
||||
}
|
||||
|
||||
// Read Public Key
|
||||
var pubKeyBytes [32]byte
|
||||
if _, err := reader.Read(pubKeyBytes[:]); err != nil {
|
||||
return nil, fmt.Errorf("read pubKey failed: %v", err)
|
||||
}
|
||||
|
||||
blockPubKey, err := new(edwards25519.Point).SetBytes(pubKeyBytes[:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid block public key: %v", err)
|
||||
}
|
||||
|
||||
// Read Block Type
|
||||
blockType, err := reader.ReadByte()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read blockType failed: %v", err)
|
||||
}
|
||||
|
||||
// Read Dates
|
||||
dates := make([]byte, 8)
|
||||
if _, err := reader.Read(dates); err != nil {
|
||||
return nil, fmt.Errorf("read dates failed: %v", err)
|
||||
}
|
||||
|
||||
// Parse Content
|
||||
switch blockType {
|
||||
case 0x00: // Intermediate
|
||||
// 4 bytes int
|
||||
tmp := make([]byte, 4)
|
||||
if _, err := reader.Read(tmp); err != nil {
|
||||
return nil, fmt.Errorf("read intermediate int failed: %v", err)
|
||||
}
|
||||
// String
|
||||
if _, err := readNullTerminatedString(reader); err != nil {
|
||||
return nil, fmt.Errorf("read intermediate string failed: %v", err)
|
||||
}
|
||||
case 0x01, 0x03: // Website / Code
|
||||
if _, err := readNullTerminatedString(reader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case 0x02: // Server
|
||||
// 5 bytes
|
||||
tmp := make([]byte, 5)
|
||||
if _, err := reader.Read(tmp); err != nil {
|
||||
return nil, fmt.Errorf("read server data failed: %v", err)
|
||||
}
|
||||
if _, err := readNullTerminatedString(reader); err != nil {
|
||||
return nil, fmt.Errorf("read server string failed: %v", err)
|
||||
}
|
||||
case 0x20: // Box
|
||||
// Ephemeral blocks might be empty content
|
||||
if reader.Len() > 0 {
|
||||
if _, err := readNullTerminatedString(reader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
default:
|
||||
// Fallback
|
||||
}
|
||||
|
||||
// Calculate End for Hashing
|
||||
bytesConsumedAfter := (len(licenseData) - 1) - reader.Len()
|
||||
blockEndAbs := 1 + bytesConsumedAfter
|
||||
hashStart := blockStartAbs + 1
|
||||
|
||||
if blockEndAbs <= hashStart {
|
||||
return nil, errors.New("block too short for hashing")
|
||||
}
|
||||
|
||||
hashableData := licenseData[hashStart:blockEndAbs]
|
||||
|
||||
// Calculate SHA512 Hash
|
||||
hash := sha512.Sum512(hashableData)
|
||||
var scalarBytes [32]byte
|
||||
copy(scalarBytes[:], hash[:32]) // Take first 32 bytes
|
||||
|
||||
// Clamp the hash
|
||||
scalarBytes[0] &= 248
|
||||
scalarBytes[31] &= 127
|
||||
scalarBytes[31] |= 64
|
||||
|
||||
scalar, err := new(edwards25519.Scalar).SetBytesWithClamping(scalarBytes[:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scalar creation failed: %v", err)
|
||||
}
|
||||
|
||||
// Derive: current = current + (blockKey * hash)
|
||||
term := new(edwards25519.Point).ScalarMult(scalar, blockPubKey)
|
||||
currentPoint.Add(currentPoint, term)
|
||||
}
|
||||
|
||||
return currentPoint.Bytes(), nil
|
||||
}
|
||||
|
||||
func readNullTerminatedString(r *bytes.Reader) (string, error) {
|
||||
var data []byte
|
||||
for {
|
||||
b, err := r.ReadByte()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if b == 0x00 {
|
||||
break
|
||||
}
|
||||
data = append(data, b)
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
56
pkg/protocol/license_test.go
Normal file
56
pkg/protocol/license_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLicenseDerivation_Ts3jVectors(t *testing.T) {
|
||||
// Vectors from ts3j LicenseDerivationTest.java
|
||||
tests := []struct {
|
||||
name string
|
||||
license string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Vector 1",
|
||||
license: "AQA1hUFJiiSs0wFXkYuPUJVcDa6XCrZTcsvkB0Ffzz4CmwIITRXgCqeTYA" +
|
||||
"cAAAAgQW5vbnltb3VzAADSN9wlGHZEHZvX7ImHoqYezibj5byDh0f4oMsG3afDxyAKePI" +
|
||||
"VCnma1Q==",
|
||||
expected: "z/bYm6TmHmuAil/osx8eGi6Oits2vIO4i6Bm13RuiGg=",
|
||||
},
|
||||
{
|
||||
name: "Vector 2",
|
||||
license: "AQA1hUFJiiSs0wFXkYuPUJVcDa6XCrZTcsvkB0Ffzz4C" +
|
||||
"mwIITRXgCqeTYAcAAAAgQW5vbnltb3VzAABx1YQfzCiB8b" +
|
||||
"ZZAdGwXNTLmdhiOpjaH3OOlISy5vrM3iAKePBVCnmZFQ==",
|
||||
expected: "lrukIi392D7ltdKFp5mURT3Ydk+oWYNjMt3kptbQl6I=",
|
||||
},
|
||||
{
|
||||
name: "Vector 3",
|
||||
license: "AQA1hUFJiiSs0wFXkYuPUJVcDa6XCrZTcsvkB0Ffzz4CmwIITR" +
|
||||
"XgCqeTYAcAAAAgQW5vbnltb3VzAAAK5C0l+xtOTAZGEA/GHHOySAUEBmq7fN5" +
|
||||
"PG7uSGPEADiAKePGHCnmaRw==",
|
||||
expected: "H+UcEreBUkCWN18nTYZp0QQkQqGA8IqzqvJ5qB225Z8=",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
licenseData, err := base64.StdEncoding.DecodeString(tt.license)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to decode license base64: %v", err)
|
||||
}
|
||||
|
||||
derivedKey, err := ParseLicenseAndDeriveKey(licenseData)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseLicenseAndDeriveKey failed: %v", err)
|
||||
}
|
||||
|
||||
derivedKeyBase64 := base64.StdEncoding.EncodeToString(derivedKey)
|
||||
if derivedKeyBase64 != tt.expected {
|
||||
t.Errorf("Mismatch.\nGot: %s\nWant: %s", derivedKeyBase64, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
97
pkg/protocol/math.go
Normal file
97
pkg/protocol/math.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"crypto/sha512"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// Curve25519 Prime: 2^255 - 19
|
||||
var P *big.Int
|
||||
|
||||
func init() {
|
||||
P, _ = new(big.Int).SetString("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed", 16)
|
||||
}
|
||||
|
||||
// Ed25519 Y (Compressed) to Curve25519 (Montgomery) U
|
||||
// u = (1 + y) / (1 - y)
|
||||
func EdwardsToMontgomery(yBytes []byte) ([]byte, error) {
|
||||
// 1. Decode y (little endian, clamped?)
|
||||
// "The 32-byte public key is the 32-byte little-endian encoding of the Point."
|
||||
// High bit is sign of x (ignored for U-coord conv usually?).
|
||||
|
||||
// Reverse bytes to big.Int
|
||||
y := new(big.Int).SetBytes(Reverse(yBytes))
|
||||
|
||||
// Clear high bit?
|
||||
// Ed25519 compressed form: high bit of last byte is X sign.
|
||||
// y &= ~(1 << 255)
|
||||
bit255 := big.NewInt(1)
|
||||
bit255.Lsh(bit255, 255)
|
||||
if y.Cmp(bit255) >= 0 {
|
||||
y.Sub(y, bit255)
|
||||
}
|
||||
|
||||
one := big.NewInt(1)
|
||||
|
||||
// num = 1 + y
|
||||
num := new(big.Int).Add(one, y)
|
||||
num.Mod(num, P)
|
||||
|
||||
// den = 1 - y
|
||||
den := new(big.Int).Sub(one, y)
|
||||
// Handle negative result for modular arithmetic
|
||||
if den.Sign() < 0 {
|
||||
den.Add(den, P)
|
||||
}
|
||||
den.Mod(den, P)
|
||||
|
||||
// denInv
|
||||
denInv := new(big.Int).ModInverse(den, P)
|
||||
|
||||
// u = num * denInv
|
||||
u := new(big.Int).Mul(num, denInv)
|
||||
u.Mod(u, P)
|
||||
|
||||
// Encode u (32 bytes little endian)
|
||||
uBytes := u.Bytes()
|
||||
uBytesRev := Reverse(uBytes)
|
||||
|
||||
// Pad to 32
|
||||
res := make([]byte, 32)
|
||||
copy(res, uBytesRev) // Copy into start (little endian usually means LSB at 0?)
|
||||
// Wait. BigInt Bytes() returns Big Endian.
|
||||
// Reverse(BigEndian) -> Little Endian.
|
||||
// If uBytes is shorter than 32, we need to pad ZEROS at the END (high bytes) in Little Endian.
|
||||
// e.g. Value 1. BigEndian: [1]. Reverse: [1].
|
||||
// Result [1, 0, 0, ...]
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func Reverse(b []byte) []byte {
|
||||
l := len(b)
|
||||
r := make([]byte, l)
|
||||
for i := 0; i < l; i++ {
|
||||
r[i] = b[l-1-i]
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// CalculateSharedSecret inputs:
|
||||
// serverPubKey (Montgomery U, 32 bytes)
|
||||
// clientPrivKey (Scalar, 32 bytes)
|
||||
func CalculateSharedSecret(serverPub, clientPriv []byte) []byte {
|
||||
// Use x/crypto/curve25519
|
||||
// ScalarMult(dst, scalar, point)
|
||||
// Placeholder as logic is in handshake.go currently
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper to expand hashes
|
||||
func Sha1Array(data []byte) [20]byte {
|
||||
return sha1.Sum(data)
|
||||
}
|
||||
func Sha512Array(data []byte) [64]byte {
|
||||
return sha512.Sum512(data)
|
||||
}
|
||||
147
pkg/protocol/packet.go
Normal file
147
pkg/protocol/packet.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// PacketType represents the type of a TeamSpeak 3 packet.
|
||||
type PacketType uint8
|
||||
|
||||
const (
|
||||
PacketTypeVoice PacketType = 0x00
|
||||
PacketTypeVoiceWhisper PacketType = 0x01
|
||||
PacketTypeCommand PacketType = 0x02
|
||||
PacketTypeCommandLow PacketType = 0x03
|
||||
PacketTypePing PacketType = 0x04
|
||||
PacketTypePong PacketType = 0x05
|
||||
PacketTypeAck PacketType = 0x06
|
||||
PacketTypeAckLow PacketType = 0x07
|
||||
PacketTypeInit1 PacketType = 0x08
|
||||
)
|
||||
|
||||
// Packet Flags
|
||||
const (
|
||||
PacketFlagUnencrypted = 0x80
|
||||
PacketFlagCompressed = 0x40
|
||||
PacketFlagNewProtocol = 0x20
|
||||
PacketFlagFragmented = 0x10
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderSizeClientToServer = 13 // 8 MAC + 2 PID + 2 CID + 1 PT
|
||||
HeaderSizeServerToClient = 11 // 8 MAC + 2 PID + 1 PT
|
||||
MACSize = 8
|
||||
)
|
||||
|
||||
// PacketHeader represents the common header fields.
|
||||
type PacketHeader struct {
|
||||
MAC [8]byte
|
||||
PacketID uint16
|
||||
ClientID uint16 // Only present in Client -> Server
|
||||
Type uint8 // Contains PacketType and Flags
|
||||
}
|
||||
|
||||
// Packet represents a parsed TeamSpeak 3 packet.
|
||||
type Packet struct {
|
||||
Header PacketHeader
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func (h *PacketHeader) FlagUnencrypted() bool { return h.Type&PacketFlagUnencrypted != 0 }
|
||||
func (h *PacketHeader) FlagCompressed() bool { return h.Type&PacketFlagCompressed != 0 }
|
||||
func (h *PacketHeader) FlagNewProtocol() bool { return h.Type&PacketFlagNewProtocol != 0 }
|
||||
func (h *PacketHeader) FlagFragmented() bool { return h.Type&PacketFlagFragmented != 0 }
|
||||
func (h *PacketHeader) PacketType() PacketType { return PacketType(h.Type & 0x0F) }
|
||||
|
||||
// SetType sets the packet type and preserves flags
|
||||
func (h *PacketHeader) SetType(pt PacketType) {
|
||||
flags := h.Type & 0xF0
|
||||
h.Type = flags | uint8(pt&0x0F)
|
||||
}
|
||||
|
||||
// Encode writes the packet to a byte slice.
|
||||
func (p *Packet) Encode(isClientToServer bool) ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
// MAC
|
||||
buf.Write(p.Header.MAC[:])
|
||||
|
||||
// Packet ID
|
||||
if err := binary.Write(buf, binary.BigEndian, p.Header.PacketID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Client ID (only C->S)
|
||||
if isClientToServer {
|
||||
if err := binary.Write(buf, binary.BigEndian, p.Header.ClientID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Packet Type
|
||||
if err := binary.Write(buf, binary.BigEndian, p.Header.Type); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Data
|
||||
buf.Write(p.Data)
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// Decode parses a packet from a byte slice.
|
||||
func Decode(data []byte, isClientToServer bool) (*Packet, error) {
|
||||
reader := bytes.NewReader(data)
|
||||
p := &Packet{}
|
||||
|
||||
// Check minimum size
|
||||
minSize := HeaderSizeServerToClient
|
||||
if isClientToServer {
|
||||
minSize = HeaderSizeClientToServer
|
||||
}
|
||||
if len(data) < minSize {
|
||||
return nil, errors.New("packet too short")
|
||||
}
|
||||
|
||||
// MAC
|
||||
if _, err := reader.Read(p.Header.MAC[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Packet ID
|
||||
if err := binary.Read(reader, binary.BigEndian, &p.Header.PacketID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Client ID (only C->S)
|
||||
if isClientToServer {
|
||||
if err := binary.Read(reader, binary.BigEndian, &p.Header.ClientID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Packet Type
|
||||
if err := binary.Read(reader, binary.BigEndian, &p.Header.Type); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Data
|
||||
p.Data = make([]byte, reader.Len())
|
||||
if _, err := reader.Read(p.Data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func NewPacket(pt PacketType, data []byte) *Packet {
|
||||
return &Packet{
|
||||
Header: PacketHeader{
|
||||
Type: uint8(pt),
|
||||
MAC: [8]byte{}, // Default empty MAC
|
||||
},
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
118
pkg/transport/socket.go
Normal file
118
pkg/transport/socket.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go-ts/pkg/protocol"
|
||||
)
|
||||
|
||||
// TS3Conn handles the UDP connection to the TeamSpeak server.
|
||||
type TS3Conn struct {
|
||||
conn *net.UDPConn
|
||||
readQueue chan *protocol.Packet
|
||||
closeChan chan struct{}
|
||||
wg sync.WaitGroup
|
||||
writeMu sync.Mutex
|
||||
}
|
||||
|
||||
// NewTS3Conn creates a new connection to the specified address.
|
||||
func NewTS3Conn(address string) (*TS3Conn, error) {
|
||||
raddr, err := net.ResolveUDPAddr("udp", address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve address: %w", err)
|
||||
}
|
||||
|
||||
conn, err := net.DialUDP("udp", nil, raddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to dial UDP: %w", err)
|
||||
}
|
||||
|
||||
ts3c := &TS3Conn{
|
||||
conn: conn,
|
||||
readQueue: make(chan *protocol.Packet, 100),
|
||||
closeChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
ts3c.startReader()
|
||||
|
||||
return ts3c, nil
|
||||
}
|
||||
|
||||
func (c *TS3Conn) startReader() {
|
||||
c.wg.Add(1)
|
||||
go func() {
|
||||
defer c.wg.Done()
|
||||
buf := make([]byte, 2048) // Max packet size should be around 500, but use larger buffer to be safe
|
||||
for {
|
||||
select {
|
||||
case <-c.closeChan:
|
||||
return
|
||||
default:
|
||||
c.conn.SetReadDeadline(time.Now().Add(1 * time.Second))
|
||||
n, _, err := c.conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
continue
|
||||
}
|
||||
// If closed, return
|
||||
select {
|
||||
case <-c.closeChan:
|
||||
return
|
||||
default:
|
||||
fmt.Printf("Error reading valid UDP packet: %v\n", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Parse packet
|
||||
pkt, err := protocol.Decode(buf[:n], false) // Server -> Client
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to decode packet: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case c.readQueue <- pkt:
|
||||
default:
|
||||
fmt.Println("Read queue full, dropping packet")
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// SendPacket sends a packet to the server.
|
||||
func (c *TS3Conn) SendPacket(pkt *protocol.Packet) error {
|
||||
c.writeMu.Lock()
|
||||
defer c.writeMu.Unlock()
|
||||
|
||||
// Client -> Server
|
||||
bytes, err := pkt.Encode(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.conn.Write(bytes)
|
||||
return err
|
||||
}
|
||||
|
||||
// ReadPacket returns the next received packet channel.
|
||||
func (c *TS3Conn) PacketChan() <-chan *protocol.Packet {
|
||||
return c.readQueue
|
||||
}
|
||||
|
||||
// RemoteAddr returns the remote network address.
|
||||
func (c *TS3Conn) RemoteAddr() net.Addr {
|
||||
return c.conn.RemoteAddr()
|
||||
}
|
||||
|
||||
// Close closes the connection.
|
||||
func (c *TS3Conn) Close() error {
|
||||
close(c.closeChan)
|
||||
c.conn.Close()
|
||||
c.wg.Wait()
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user