commit 47b8173045795279a9f9ad55227619bf93aeb597 Author: Jose Luis Montañes Ojados Date: Thu Jan 15 16:49:16 2026 +0100 working diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..54634d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +ts3j/ diff --git a/cmd/client/client.exe b/cmd/client/client.exe new file mode 100644 index 0000000..be6cbfa Binary files /dev/null and b/cmd/client/client.exe differ diff --git a/cmd/client/main.go b/cmd/client/main.go new file mode 100644 index 0000000..d07b035 --- /dev/null +++ b/cmd/client/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "flag" + "log" + "os" + "os/signal" + "syscall" + + "go-ts/internal/client" +) + +func main() { + serverAddr := flag.String("server", "127.0.0.1:9987", "TeamSpeak 3 Server Address") + nickname := flag.String("nickname", "GoCient", "Nickname") + flag.Parse() + + log.Printf("Starting TS3 Client...") + log.Printf("Server: %s", *serverAddr) + log.Printf("Nickname: %s", *nickname) + + c := client.NewClient(*nickname) + + errChan := make(chan error) + go func() { + if err := c.Connect(*serverAddr); err != nil { + errChan <- err + } + }() + + // Wait for signals + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + select { + case err := <-errChan: + log.Fatalf("Client Error: %v", err) + case <-sigChan: + log.Println("Shutting down...") + } +} diff --git a/cmd/fakeserver/main.go b/cmd/fakeserver/main.go new file mode 100644 index 0000000..c95a581 --- /dev/null +++ b/cmd/fakeserver/main.go @@ -0,0 +1,332 @@ +package main + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/asn1" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "fmt" + "log" + "math/big" + "net" + "time" + + "go-ts/pkg/protocol" + + "filippo.io/edwards25519" +) + +type ServerState struct { + Step int + A1 [16]byte + A2 [100]byte + + Identity *ecdsa.PrivateKey + + LicensePriv *edwards25519.Scalar + LicensePub *edwards25519.Point + LicenseBlock []byte + + Alpha []byte + Beta []byte + SharedSecret []byte + SharedIV []byte + SharedMac [8]byte +} + +func main() { + addr, _ := net.ResolveUDPAddr("udp", ":9988") + conn, _ := net.ListenUDP("udp", addr) + log.Println("FakeServer listening on :9988") + + // 1. Generate Server Identity (P-256) + privKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + + // 2. Generate Transport/License Key (Ed25519) + var seed [32]byte + rand.Read(seed[:]) + lPriv, _ := new(edwards25519.Scalar).SetBytesWithClamping(seed[:]) + lPub := new(edwards25519.Point).ScalarBaseMult(lPriv) + + // 3. Create 'l' (License Block) - 32 random bytes for now + lData := make([]byte, 32) + rand.Read(lData) + + state := &ServerState{ + Step: 0, + Identity: privKey, + LicensePriv: lPriv, + LicensePub: lPub, + LicenseBlock: lData, + Alpha: make([]byte, 10), + Beta: make([]byte, 54), + } + rand.Read(state.Alpha) + rand.Read(state.Beta) + rand.Read(state.A2[:]) + + buf := make([]byte, 4096) + for { + n, rAddr, err := conn.ReadFromUDP(buf) + if err != nil { + continue + } + data := make([]byte, n) + copy(data, buf[:n]) + handlePacket(conn, rAddr, data, state) + } +} + +func handlePacket(conn *net.UDPConn, addr *net.UDPAddr, data []byte, s *ServerState) { + pkt, err := protocol.Decode(data, true) + if err != nil { + return + } + + if pkt.Header.PacketType() == protocol.PacketTypeInit1 { + if len(pkt.Data) > 5 && pkt.Data[4] == 0x00 { // Step 0 + log.Println("Recv Step 0, Sending Step 1") + sendStep1(conn, addr, s) + } else if len(pkt.Data) > 5 && pkt.Data[4] == 0x02 { // Step 2 + log.Println("Recv Step 2, Sending Step 3") + sendStep3(conn, addr, s) + } + } else if pkt.Header.PacketType() == protocol.PacketTypeCommand { + decrypted, err := decryptHandshake(pkt) + if err == nil { + sStr := string(decrypted) + if len(sStr) > 8 && sStr[0:8] == "clientek" { + log.Printf("Recv clientek (Decrypted): %s", sStr) + if err := s.processClientEk(decrypted); err != nil { + log.Printf("Error processing clientek: %v", err) + } else { + log.Println("Shared Secret Derived. Waiting for clientinit.") + } + sendAck(conn, addr, pkt.Header.PacketID) + return + } else if len(sStr) > 12 && sStr[0:12] == "clientinitiv" { + log.Println("Recv clientinitiv. Sending initivexpand2...") + sendInitivexpand2(conn, addr, s) + return + } + } + + if len(s.SharedSecret) > 0 { + s.decryptClientInit(pkt) + } else if err != nil && pkt.Header.FlagUnencrypted() == false { + // log.Printf("Decrypt failed: %v", err) + } + } +} + +func decryptHandshake(pkt *protocol.Packet) ([]byte, error) { + key := protocol.HandshakeKey + nonce := protocol.HandshakeNonce + + meta := make([]byte, 5) + binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID) + binary.BigEndian.PutUint16(meta[2:4], pkt.Header.ClientID) + meta[4] = pkt.Header.Type + + return protocol.DecryptEAX(key, nonce, meta, pkt.Data, pkt.Header.MAC[:]) +} + +func sendStep1(conn *net.UDPConn, addr *net.UDPAddr, s *ServerState) { + buf := new(bytes.Buffer) + binary.Write(buf, binary.BigEndian, int32(time.Now().Unix())) + buf.WriteByte(0x01) + rand.Read(s.A1[:]) + buf.Write(s.A1[:]) + + pkt := protocol.NewPacket(protocol.PacketTypeInit1, buf.Bytes()) + pkt.Header.PacketID = 1 + copy(pkt.Header.MAC[:], []byte("TS3INIT1")) + + encoded, _ := pkt.Encode(false) + conn.WriteToUDP(encoded, addr) +} + +func sendStep3(conn *net.UDPConn, addr *net.UDPAddr, s *ServerState) { + buf := new(bytes.Buffer) + binary.Write(buf, binary.BigEndian, int32(time.Now().Unix())) + buf.WriteByte(0x03) + + x := make([]byte, 64) + rand.Read(x) + n := make([]byte, 64) + rand.Read(n) + buf.Write(x) + buf.Write(n) + binary.Write(buf, binary.BigEndian, uint32(0)) + buf.Write(s.A2[:]) + + pkt := protocol.NewPacket(protocol.PacketTypeInit1, buf.Bytes()) + pkt.Header.PacketID = 2 + copy(pkt.Header.MAC[:], []byte("TS3INIT1")) + + encoded, _ := pkt.Encode(false) + conn.WriteToUDP(encoded, addr) +} + +func sendInitivexpand2(conn *net.UDPConn, addr *net.UDPAddr, s *ServerState) { + lStr := base64.StdEncoding.EncodeToString(s.LicenseBlock) + base64.StdEncoding.EncodeToString(s.Beta) // unused? + betaStr := base64.StdEncoding.EncodeToString(s.Beta) + + // Encode Omega (P-256 Public Key) + pub := s.Identity.PublicKey + omegaBytes := elliptic.Marshal(pub.Curve, pub.X, pub.Y) + omegaStr := base64.StdEncoding.EncodeToString(omegaBytes) + + // Create Proof: Sign(SHA256(lBytes)) with Identity + hash := sha256.Sum256(s.LicenseBlock) + r, sb, err := ecdsa.Sign(rand.Reader, s.Identity, hash[:]) + if err != nil { + log.Printf("Signing failed: %v", err) + } + + type ECDSASignature struct { + R, S *big.Int + } + sig := ECDSASignature{R: r, S: sb} + proofBytes, _ := asn1.Marshal(sig) + proofStr := base64.StdEncoding.EncodeToString(proofBytes) + + // Send command + cmd := fmt.Sprintf("initivexpand2 l=%s beta=%s omega=%s ot=1 proof=%s tvd=C", + lStr, betaStr, omegaStr, proofStr) + + pkt := protocol.NewPacket(protocol.PacketTypeCommand, []byte(cmd)) + pkt.Header.PacketID = 3 + + // Encrypt with HandshakeKey + key := protocol.HandshakeKey + nonce := protocol.HandshakeNonce + + // Meta S->C (3 bytes: PID(2)+Type(1)) + meta := make([]byte, 3) + binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID) + meta[2] = pkt.Header.Type + + encData, mac, _ := protocol.EncryptEAX(key, nonce, meta, pkt.Data) + pkt.Data = encData + copy(pkt.Header.MAC[:], mac) + + encoded, _ := pkt.Encode(false) + conn.WriteToUDP(encoded, addr) +} + +func sendAck(conn *net.UDPConn, addr *net.UDPAddr, pid uint16) { + pkt := protocol.NewPacket(protocol.PacketTypeAck, nil) + pkt.Header.PacketID = pid + + // Encrypt ACK + key := protocol.HandshakeKey + nonce := protocol.HandshakeNonce + meta := make([]byte, 3) + binary.BigEndian.PutUint16(meta[0:2], pid) + meta[2] = pkt.Header.Type + + encData, mac, _ := protocol.EncryptEAX(key, nonce, meta, pkt.Data) + pkt.Data = encData + copy(pkt.Header.MAC[:], mac) + + encoded, _ := pkt.Encode(false) + conn.WriteToUDP(encoded, addr) +} + +func (s *ServerState) deriveSharedSecret(clientEkPub []byte) error { + clientPoint, err := new(edwards25519.Point).SetBytes(clientEkPub) + if err != nil { + return fmt.Errorf("invalid client point: %v", err) + } + + // Server negates client point + clientPoint.Negate(clientPoint) + + sharedPoint := new(edwards25519.Point).ScalarMult(s.LicensePriv, clientPoint) + sharedBytes := sharedPoint.Bytes() + + sharedBytes[31] ^= 0x80 + + hash := sha512.Sum512(sharedBytes) + s.SharedSecret = hash[:] + + s.SharedIV = make([]byte, 64) + copy(s.SharedIV, s.SharedSecret) + for i := 0; i < 10; i++ { + s.SharedIV[i] ^= s.Alpha[i] + } + if len(s.Beta) >= 54 { + for i := 0; i < 54; i++ { + s.SharedIV[10+i] ^= s.Beta[i] + } + } + + macHash := sha1.Sum(s.SharedIV) + copy(s.SharedMac[:], macHash[0:8]) + + log.Printf("Shared Secret Derived! IV: %s", hex.EncodeToString(s.SharedIV)) + return nil +} + +func (s *ServerState) processClientEk(data []byte) error { + str := string(data) + start := "ek=" + idx := 0 + for i := 0; i < len(str)-len(start); i++ { + if str[i:i+3] == start { + idx = i + 3 + break + } + } + if idx == 0 { + return fmt.Errorf("no ek found") + } + + end := idx + for i := idx; i < len(str); i++ { + if str[i] == ' ' { + end = i + break + } + } + + ekStr := str[idx:end] + ekBytes, err := base64.StdEncoding.DecodeString(ekStr) + if err != nil { + return err + } + + return s.deriveSharedSecret(ekBytes) +} + +func (s *ServerState) decryptClientInit(pkt *protocol.Packet) { + crypto := &protocol.CryptoState{ + SharedIV: s.SharedIV, + SharedMac: s.SharedMac[:], + GenerationID: 0, + } + + key, nonce := crypto.GenerateKeyNonce(&pkt.Header, true) // isClientToServer + + meta := make([]byte, 5) + binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID) + binary.BigEndian.PutUint16(meta[2:4], pkt.Header.ClientID) + meta[4] = pkt.Header.Type + + dec, err := protocol.DecryptEAX(key, nonce, meta, pkt.Data, pkt.Header.MAC[:]) + if err != nil { + log.Printf("Decryption Failed: %v", err) + return + } + + log.Printf(">>> DECRYPTED CLIENTINIT <<<\n%s\n", string(dec)) +} diff --git a/cmd/proxy/main_design.go b/cmd/proxy/main_design.go new file mode 100644 index 0000000..e39f7e0 --- /dev/null +++ b/cmd/proxy/main_design.go @@ -0,0 +1,138 @@ +package main + +import ( + "flag" + "log" + "net" + "strings" +) + +// TS3 Proxy to capture clientinit parameters +func main() { + var localAddr string + var serverAddr string + + flag.StringVar(&localAddr, "listen", ":9988", "Local listen address") + flag.StringVar(&serverAddr, "server", "localhost:9987", "Real TeamSpeak server address") + flag.Parse() + + // Resolve addresses + localUDP, err := net.ResolveUDPAddr("udp", localAddr) + if err != nil { + log.Fatalf("Invalid local address: %v", err) + } + serverUDP, err := net.ResolveUDPAddr("udp", serverAddr) + if err != nil { + log.Fatalf("Invalid server address: %v", err) + } + + // Listen on local port + conn, err := net.ListenUDP("udp", localUDP) + if err != nil { + log.Fatalf("Failed to listen: %v", err) + } + defer conn.Close() + + log.Printf("Proxy listening on %s, forwarding to %s", localAddr, serverAddr) + log.Println("Connect your Real TeamSpeak Client to 'localhost:9988' to capture credentials.") + + // Map client addresses to server connections (for multiple clients support, simplifying for 1) + // Simple forwarder: We only expect one client for this task. + var clientAddr *net.UDPAddr + + buf := make([]byte, 2048) + + for { + n, addr, err := conn.ReadFromUDP(buf) + if err != nil { + log.Printf("Read error: %v", err) + continue + } + + // Check if packet is from client or server + if clientAddr == nil || addr.String() != clientAddr.String() { + // New client or client packet + // If it's from our known server (unlikely as we don't bind to serverAddr), it's a client. + // But wait, we are listening on localUDP. + // We need to dial the server to forward. + + // We'll use a separate socket to talk to the server to keep track of sessions? + // Simplest UDP proxy: + // 1. Receive from Client -> Send to Server (using a dialer) + // 2. Receive from Server -> Send to Client + + // Let's reset for new clients to keep it simple + clientAddr = addr + handleClientPacket(conn, serverUDP, buf[:n], addr) + } else { + // Known client + handleClientPacket(conn, serverUDP, buf[:n], addr) + } + } +} + +// We need a persistent connection to the server to receive responses +// In a simple loop, we can't easily run two blocking reads (from client and from server) without goroutines. +// Let's structure: +// Main loop reads from Client (ListenUDP). +// On first packet, start a Goroutine that Dials the Server. +// That goroutine reads from Server and writes to Client. +// Main loop writes to Server using that connection. + +var serverConn *net.UDPConn + +func handleClientPacket(clientConn *net.UDPConn, serverAddr *net.UDPAddr, data []byte, clientAddr *net.UDPAddr) { + // Inspect Packet for clientinit strings (Naive text search) + // clientinit is encrypted in standard flow? + // WAIT. If we proxy, the client and server negotiate encryption. + // We CANNOT decrypt the traffic unless we perform a MitM attack on the Diffie-Hellman handshake. + // TS3 uses ECDH. We cannot derive the shared secret just by passively observing. + // We would need to: + // 1. Intercept `initivexpand2` (Step 2/3/4?) + // 2. Replace the Server's Public Key with OUR Public Key. + // 3. Negotiate SharedSecret1 with Client. + // 4. Negotiate SharedSecret2 with Server (using our Private Key). + // 5. Decrypt Client packet -> Re-encrypt for Server. + + // This is significantly complex. + // BUT, `clientinit` might be sent *plaintext* in some legacy modes? + // No, the user's issue is specifically about the encrypted handshake. + + // However, `client_version` is sometimes sent in cleartext in the INIT1 packet? + // No, INIT1 is just 4 timestamps + random bytes. + + // WAIT. The very first packet is valid? + // `clientinit` is sent AFTER the handshake (Step 6). It is definitely encrypted. + + // If the user connects with a real client, the client calculates the SharedSecret using the REAL server's key. + // We verify the packets pass through. + // WE CANNOT READ THE PAYLOAD without a full MitM (replacing keys). + + // IS THERE ANOTHER WAY? + // Does the client send version in clear text anywhere? + // Maybe in the UserAgent? (UDP doesn't have headers like HTTP) + + // Proposal: Implement Full MitM. + // We already have the ECDH logic implemented in Go! + // We can act as the "Server" to the Client, and "Client" to the Server. + // 1. Client connects to Proxy. Proxy acts as Server. + // 2. Proxy sends its OWN Identity/Key to Client. + // 3. Proxy connects to Real Server. + // 4. Proxy completes handshake with Real Server. + // 5. Client completes handshake with Proxy. + // 6. Proxy decrypts Client's `clientinit`, LOGS IT, then re-encrypts for Server. + + // This is the chosen path. It reuses our `handshake.go` and `license.go` logic. + + inspectPacket(data) + + // Forwarding logic placeholder (Passive won't work for decryption) +} + +func inspectPacket(data []byte) { + // Check for "clientinit" ASCII pattern + s := string(data) + if strings.Contains(s, "clientinit") { + log.Printf("[!] FOUND POSSIBLE CLIENTINIT (Plaintext?): %s", s) + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3d2d727 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +version: "3.9" + +services: + teamspeak: + image: teamspeak:3.13 + container_name: ts3-server + restart: unless-stopped + ports: + - "9987:9987/udp" # Voice + - "10011:10011" # ServerQuery + - "30033:30033" # FileTransfer + environment: + TS3SERVER_LICENSE: accept + volumes: + - ts3-data:/var/ts3server + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + +volumes: + ts3-data: diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..17bafa0 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module go-ts + +go 1.24.0 + +toolchain go1.24.11 + +require filippo.io/edwards25519 v1.1.0 + +require golang.org/x/crypto v0.47.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e1ea31c --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= diff --git a/internal/client/client.go b/internal/client/client.go new file mode 100644 index 0000000..9906d51 --- /dev/null +++ b/internal/client/client.go @@ -0,0 +1,563 @@ +package client + +import ( + "bytes" + "encoding/binary" + "fmt" + "log" + "strings" + "time" + + "go-ts/pkg/protocol" + "go-ts/pkg/transport" +) + +type Channel struct { + ID uint64 + ParentID uint64 + Name string + Order uint64 +} + +type Client struct { + Conn *transport.TS3Conn + Handshake *HandshakeState + + Nickname string + ClientID uint16 + + // Counters + PacketIDCounterC2S uint16 + + // State + Connected bool + + // Server Data + Channels map[uint64]*Channel +} + +func NewClient(nickname string) *Client { + return &Client{ + Nickname: nickname, + PacketIDCounterC2S: 1, + Channels: make(map[uint64]*Channel), + } +} + +func (c *Client) Connect(address string) error { + conn, err := transport.NewTS3Conn(address) + if err != nil { + return err + } + c.Conn = conn + // Initialize handshake state + hs, err := NewHandshakeState(c.Conn) + if err != nil { + return err + } + c.Handshake = hs + + // Improve Identity Security Level to 8 (Standard Requirement) + c.Handshake.ImproveSecurityLevel(8) + + log.Println("Connected to UDP. Starting Handshake...") + + // Start Handshake Flow + // Step 0 + if err := c.Handshake.SendPacket0(); err != nil { + return err + } + + // Read Loop for Handshake + timeout := time.After(5 * time.Second) + + for !c.Connected { + select { + case pkt := <-c.Conn.PacketChan(): + if err := c.handlePacket(pkt); err != nil { + log.Printf("Error handling packet: %v", err) + } + case <-timeout: + return fmt.Errorf("connection timed out") + } + } + + log.Println("=== Connected! Now listening for server data... ===") + + // Send Ping every 3 seconds + ticker := time.NewTicker(3 * time.Second) + defer ticker.Stop() + + // KeepAlive Loop + for { + select { + case pkt := <-c.Conn.PacketChan(): + if err := c.handlePacket(pkt); err != nil { + log.Printf("Error handling packet: %v", err) + } + case <-ticker.C: + // Send Ping + c.PacketIDCounterC2S++ + ping := protocol.NewPacket(protocol.PacketTypePing, nil) + ping.Header.PacketID = c.PacketIDCounterC2S + ping.Header.ClientID = c.ClientID // Should be assigned by server usually, but we use 0 or what? + + // Encrypt Ping (if past handshake) + // For now, assuming unencrypted ping is ignored or we need to encrypt it if in full session + // Protocol says: "Everything is encrypted" + // Using correct keys... + + // Actually handlePacket sends PONG. We need to Initiate PING? + // Simplified: Just printing "Ping" for now, or just wait for server to Ping us. + // The server usually pings. We must reply Pong. + // BUT if we don't send anything, we might time out. + // Let's rely on Server Pings for now, but remove the 5s exit timeout. + } + } +} + +func (c *Client) handlePacket(pkt *protocol.Packet) error { + log.Printf("Received Packet: ID=%d, Type=%v, Len=%d", pkt.Header.PacketID, pkt.Header.PacketType(), len(pkt.Data)) + + switch pkt.Header.PacketType() { + case protocol.PacketTypeInit1: + return c.handleInit(pkt) + case protocol.PacketTypeCommand: + // Send ACK + // Ack Data: PacketID of the packet we're acknowledging (2 bytes) + ackData := make([]byte, 2) + binary.BigEndian.PutUint16(ackData, pkt.Header.PacketID) + + ack := protocol.NewPacket(protocol.PacketTypeAck, ackData) + + // ACK header PacketID should match the packet being acknowledged + ack.Header.PacketID = pkt.Header.PacketID + + // ACKs for Command packets during handshake are encrypted with HandshakeKey + key := protocol.HandshakeKey + nonce := protocol.HandshakeNonce + + // Meta for Client->Server: PID(2) + CID(2) + PT(1) = 5 bytes + meta := make([]byte, 5) + binary.BigEndian.PutUint16(meta[0:2], ack.Header.PacketID) + binary.BigEndian.PutUint16(meta[2:4], ack.Header.ClientID) // ClientID (usually 0 during handshake) + meta[4] = ack.Header.Type + + encData, mac, _ := protocol.EncryptEAX(key, nonce, meta, ack.Data) + ack.Data = encData + copy(ack.Header.MAC[:], mac) + log.Printf("Sending ACK for PacketID %d", pkt.Header.PacketID) + + c.Conn.SendPacket(ack) + + return c.handleCommand(pkt) + case protocol.PacketTypeVoice: + c.handleVoice(pkt) + case protocol.PacketTypePing: + // Respond with Pong + pong := protocol.NewPacket(protocol.PacketTypePong, nil) + pong.Header.PacketID = pkt.Header.PacketID // Acknowledgement + pong.Header.MAC = pkt.Header.MAC // TODO: calculate real mac + c.Conn.SendPacket(pong) + case protocol.PacketTypeAck: + // Server acknowledged our packet - ACKs are encrypted + // Decrypt with HandshakeKey + key := protocol.HandshakeKey + nonce := protocol.HandshakeNonce + + meta := make([]byte, 3) // Server->Client is 3 bytes + binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID) + meta[2] = pkt.Header.Type + + data, err := protocol.DecryptEAX(key, nonce, meta, pkt.Data, pkt.Header.MAC[:]) + if err != nil { + log.Printf("ACK decryption failed: %v", err) + return nil + } + + ackPId := uint16(0) + if len(data) >= 2 { + ackPId = binary.BigEndian.Uint16(data[0:2]) + } + log.Printf("Received ACK for PacketID %d", ackPId) + + // If ACK is for clientek (PID=1), proceed with clientinit + if ackPId == 1 && c.Handshake != nil && c.Handshake.Step == 5 { + log.Println("clientek acknowledged! Sending clientinit...") + c.Handshake.Step = 6 + return c.sendClientInit() + } + // If ACK is for clientinit (PID=2), we're connected! + if ackPId == 2 && c.Handshake != nil && c.Handshake.Step == 6 { + log.Println("clientinit acknowledged! Connection established!") + c.Connected = true + } + } + return nil +} + +func (c *Client) handleInit(pkt *protocol.Packet) error { + // Determine step based on packet content or local state + // Simple state machine + if c.Handshake.Step == 0 { + if err := c.Handshake.HandlePacket1(pkt); err != nil { + return err + } + log.Println("Handshake Step 1 Completed. Sending Step 2...") + return c.Handshake.SendPacket2() + } else if c.Handshake.Step == 1 { + // Wait, step 1 is processed, we sent step 2. + // We expect Step 3. + if pkt.Data[0] == 0x03 { + if err := c.Handshake.HandlePacket3(pkt); err != nil { + return err + } + log.Println("Handshake Step 3 Completed. Sending Step 4 (Puzzle Solution)...") + // Send Packet 4 (Not fully implemented in this snippet due to puzzle complexity) + // c.Handshake.SendPacket4() + } + } + return nil +} + +func (c *Client) handleCommand(pkt *protocol.Packet) error { + // Check if Encrypted + // PacketTypeCommand is usually encrypted. + // Flag check? The flag is in the Header (e.g. Unencrypted flag). + // If Unencrypted flag is SET, it's cleartext. + // Spec: "Command ... Encrypted: ✓". So Unencrypted flag is CLEARED. + + // Decrypt if necessary + var data []byte + var err error + + if pkt.Header.FlagUnencrypted() { + data = pkt.Data + } else { + var key, nonce []byte + decrypted := false + + // 1. Try SharedSecret if available + if c.Handshake != nil && c.Handshake.Step >= 6 && len(c.Handshake.SharedIV) > 0 { + // Use SharedSecret-based encryption + crypto := &protocol.CryptoState{ + SharedIV: c.Handshake.SharedIV, + SharedMac: c.Handshake.SharedMac, + GenerationID: 0, + } + // Server->Client = false + key, nonce = crypto.GenerateKeyNonce(&pkt.Header, false) + + // AAD for Server->Client: PacketID (2) + Type|Flags (1) + meta := make([]byte, 3) + binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID) + meta[2] = pkt.Header.Type // Type includes Flags + + data, err = protocol.DecryptEAX(key, nonce, meta, pkt.Data, pkt.Header.MAC[:]) + if err == nil { + decrypted = true + } else { + log.Printf("SharedSecret decrypt failed (PID=%d): %v. Trying HandshakeKey...", pkt.Header.PacketID, err) + } + } + + // 2. Fallback to HandshakeKey + if !decrypted { + key = protocol.HandshakeKey[:] + nonce = protocol.HandshakeNonce[:] + + // AAD matching KeyNonce derivation context? + // HandshakeKey usage usually has same AAD requirements? + meta := make([]byte, 3) + binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID) + meta[2] = pkt.Header.Type // Type includes Flags + + data, err = protocol.DecryptEAX(key, nonce, meta, pkt.Data, pkt.Header.MAC[:]) + if err != nil { + log.Printf("All decryption attempts failed for PID=%d: %v", pkt.Header.PacketID, err) + return fmt.Errorf("decryption failed: %v", err) + } + } + } + // On first encrypted command set Connected = true (Fallback if ACK missed) + if !c.Connected && pkt.Header.PacketID > 2 { + c.Connected = true + } + + cmdStr := string(data) + + // Fix Garbage Headers (TS3 often sends binary garbage before command) + // Scan for first valid lower case [a-z] char (Most commands are lowercase) + validStart := strings.IndexFunc(cmdStr, func(r rune) bool { + return (r >= 'a' && r <= 'z') + }) + + if validStart > 0 && validStart < 50 { + cmdStr = cmdStr[validStart:] + } + + log.Printf("Command: %s", cmdStr) + + // Parse Command + cmd, args := protocol.ParseCommand([]byte(cmdStr)) + + switch cmd { + case "initivexpand2": + err := c.Handshake.ProcessInitivexpand2(args) + if err != nil { + log.Printf("Error processing initivexpand2: %v", err) + } + case "initserver": + // Server sends this after clientinit - contains our clientID + if cid, ok := args["aclid"]; ok { + var id uint64 + fmt.Sscanf(cid, "%d", &id) + c.ClientID = uint16(id) + log.Printf("Assigned ClientID: %d", c.ClientID) + } + if name, ok := args["virtualserver_name"]; ok { + log.Printf("Server Name: %s", protocol.Unescape(name)) + } + case "channellist": + // Parse channel info + ch := &Channel{} + if cid, ok := args["cid"]; ok { + fmt.Sscanf(cid, "%d", &ch.ID) + } + if pid, ok := args["cpid"]; ok { + fmt.Sscanf(pid, "%d", &ch.ParentID) + } + if name, ok := args["channel_name"]; ok { + ch.Name = protocol.Unescape(name) + } + if order, ok := args["channel_order"]; ok { + fmt.Sscanf(order, "%d", &ch.Order) + } + c.Channels[ch.ID] = ch + log.Printf("Channel: [%d] NameRaw=%q Order=%d Args=%v", ch.ID, ch.Name, ch.Order, args) + case "channellistfinished": + log.Printf("=== Channel List Complete (%d channels) ===", len(c.Channels)) + var targetChan *Channel + for _, ch := range c.Channels { + log.Printf(" - [%d] %s (parent=%d)", ch.ID, ch.Name, ch.ParentID) + if ch.Name == "Test" { + targetChan = ch + } + } + + if targetChan != nil { + log.Printf("Found target channel 'Test' (ID=%d). Joining...", targetChan.ID) + + if c.ClientID == 0 { + log.Println("ERROR: ClientID is 0. Cannot join channel. 'initserver' missing?") + return nil + } + + // clientmove clid={clid} cid={cid} cpw= + cmd := fmt.Sprintf("clientmove clid=%d cid=%d cpw=", c.ClientID, targetChan.ID) + + pkt := protocol.NewPacket(protocol.PacketTypeCommand, []byte(cmd)) + + // Encrypt + // key, nonce, mac, _ := c.getCryptoState() // Unused + + // Meta + meta := make([]byte, 5) + binary.BigEndian.PutUint16(meta[0:2], c.PacketIDCounterC2S+1) // Next ID + pkt.Header.PacketID = c.PacketIDCounterC2S + 1 + c.PacketIDCounterC2S++ + binary.BigEndian.PutUint16(meta[2:4], c.ClientID) + meta[4] = pkt.Header.Type + + // TODO: Use correct crypto keys (SharedSecret if available) + // My getCryptoState returns correct ones? + // Let's manually do it to match sendClientInit logic for now or refactor later. + // Actually, if we are in Full Session, we should use SharedSecret. + // Handshake Step 6 -> SharedSecret. + + crypto := &protocol.CryptoState{ + SharedIV: c.Handshake.SharedIV, + SharedMac: c.Handshake.SharedMac, + GenerationID: 0, + } + k, n := crypto.GenerateKeyNonce(&pkt.Header, true) + + encData, mac, _ := protocol.EncryptEAX(k, n, meta, pkt.Data) + pkt.Data = encData + copy(pkt.Header.MAC[:], mac) + + c.Conn.SendPacket(pkt) + } + case "notifycliententerview": + // A client entered the server + if nick, ok := args["client_nickname"]; ok { + log.Printf("Client entered: %s", protocol.Unescape(nick)) + } + case "notifytextmessage": + if msg, ok := args["msg"]; ok { + log.Printf("Text Message: %s", protocol.Unescape(msg)) + } + case "notifychannelgrouplist": + // Ignore for now + case "notifyservergrouplist": + // Ignore for now + case "notifyclientneededpermissions": + // Ignore for now + case "notifyclientleftview": + if nick, ok := args["client_nickname"]; ok { + log.Printf("Client left: %s", protocol.Unescape(nick)) + } + case "notifyconnectioninfo": + // Ignore + case "badges": + // Server badges info + case "notifyclientchatcomposing": + if nick, ok := args["client_nickname"]; ok { + // This often comes as clid, need to lookup in future + log.Printf("Client typing: %s", protocol.Unescape(nick)) + } + case "notifyclientmoved": + if nick, ok := args["client_nickname"]; ok { + log.Printf("Client moved: %s", protocol.Unescape(nick)) + } + case "error": + if id, ok := args["id"]; ok && id == "522" { + log.Println("WARNING: Server rejected client version (Error 522). Ignoring as requested.") + // We pretend we are connected? + // The server might not send further data, but we won't crash. + c.Connected = true + } else { + log.Printf("Server Error: %v", args) + } + default: + // Handle prefixes for weirdly updated commands + if strings.HasPrefix(cmd, "badges") { + // ignore badges garbage + log.Println("Received Badges (Ignored)") + return nil + } + // Log unknown commands for debugging + log.Printf("Unhandled command: %s", cmd) + } + + return nil +} + +// Helper to encrypt/decrypt based on state +func (c *Client) getCryptoState() (key, nonce, mac []byte, isHandshake bool) { + if c.Handshake != nil && len(c.Handshake.SharedSecret) > 0 { + // Use Derived Keys + // But we need to Generate Key/Nonce per packet! + // This logic belongs in the Packet Encode/Decode flow or a higher level wrapper? + return nil, nil, c.Handshake.SharedMac, false + } + return protocol.HandshakeKey, protocol.HandshakeNonce, protocol.HandshakeMac[:], true +} + +// Update encryption in Send/Receive +// Packet handling needs to know WHICH key to use. +// Simple rule: +// - Init1 (Type 8): Handshake Keys (Unencrypted payload, but MAC is HandshakeMac) +// - Command (Type 2): Encrypted. +// - CommandLow (Type 3): Encrypted. +// - Voice (Type 0): Encrypted. +// - Ping/Pong: Encrypted. +// - Ack: Encrypted. + +// IF c.Handshake.SharedSecret is set, we SHOULD use it for Commands? +// "The crypto handshake is now completed. The normal encryption scheme ... is from now on used." +// This starts AFTER clientek? Or WITH clientek? "clientek already has the packet id 1" + +func (c *Client) handleVoice(pkt *protocol.Packet) { + // Parse Voice Header + // 2 bytes VID, 1 byte Codec, Data + if len(pkt.Data) < 3 { + return + } + + // vid := binary.BigEndian.Uint16(pkt.Data[0:2]) + // codec := pkt.Data[2] + voiceData := pkt.Data[3:] + + // Calculate "Volume" (RMS of encrypted/compressed data is meaningless, but existing requirement asks for it) + // To do this properly, we need to decrypt -> decode[Opus] -> PCM -> RMS. + // For "Eco" (Echo), we can just re-wrap this data and send it back. + + vol := len(voiceData) // Placeholder "volume" + log.Printf("Voice Packet received. Approx Size/Vol: %d", vol) + + // Echo back + // Client -> Server Voice Packet + // VID + Codec + Data + // We can reuse the data payload structure mostly? + // C->S: VID(2) + Codec(1) + Data + + echoPkt := protocol.NewPacket(protocol.PacketTypeVoice, pkt.Data) + // ID Counter handling? + c.Conn.SendPacket(echoPkt) +} + +func (c *Client) sendClientInit() error { + // Build clientinit command + // Build clientinit command using TeamSpeak 3.6.2 credentials + params := map[string]string{ + "client_nickname": c.Nickname, + "client_version": "3.6.2 [Build: 1690976575]", + "client_platform": "Windows", + "client_input_hardware": "1", + "client_output_hardware": "1", + "client_default_channel": "", + "client_default_channel_password": "", + "client_server_password": "", + "client_meta_data": "", + "client_version_sign": "OyuLO/1bVJtBsXLRWzfGVhNaQd7B9D4QTolZm14DM1uCbSXVvqX3Ssym3sLi/PcvOl+SAUlX6NwBPOsQdwOGDw==", + "client_key_offset": fmt.Sprintf("%d", c.Handshake.IdentityOffset), + "client_nickname_phonetic": "", + "client_default_token": "", + "hwid": "1234567890", + } + + // Construct command string manually to ensure key correctness + var buf bytes.Buffer + buf.WriteString("clientinit") + for k, v := range params { + buf.WriteString(" ") + buf.WriteString(k) + buf.WriteString("=") + buf.WriteString(protocol.Escape(v)) + } + cmd := buf.String() + + pkt := protocol.NewPacket(protocol.PacketTypeCommand, []byte(cmd)) + pkt.Header.PacketID = 2 // After clientek (1) + pkt.Header.Type |= protocol.PacketFlagNewProtocol + + // After clientek, use SharedSecret encryption (Now that we fixed derivation logic) + crypto := &protocol.CryptoState{ + SharedIV: c.Handshake.SharedIV, + SharedMac: c.Handshake.SharedMac, + GenerationID: 0, + } + // Client->Server = true + key, nonce := crypto.GenerateKeyNonce(&pkt.Header, true) + + // AAD must match the header structure exactly (excluding MAC) + // Client Header: PacketID (2) + ClientID (2) + Type|Flags (1) + meta := make([]byte, 5) + binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID) + binary.BigEndian.PutUint16(meta[2:4], pkt.Header.ClientID) + + // Byte 4 is Type (which includes Flags) + meta[4] = pkt.Header.Type + + encData, mac, err := protocol.EncryptEAX(key, nonce, meta, pkt.Data) + if err != nil { + return err + } + + pkt.Data = encData + copy(pkt.Header.MAC[:], mac) + + log.Println("Sending clientinit (Packet 2) [Encrypted with SharedSecret]...") + return c.Conn.SendPacket(pkt) +} diff --git a/internal/client/handshake.go b/internal/client/handshake.go new file mode 100644 index 0000000..663c2f8 --- /dev/null +++ b/internal/client/handshake.go @@ -0,0 +1,504 @@ +package client + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "log" + "math/big" + "net" + "time" + + // Unused but placeholder + "go-ts/pkg/protocol" + "go-ts/pkg/transport" + + "filippo.io/edwards25519" +) + +// HandshakeState tracks the progress of the connection initialization. +type HandshakeState struct { + Step int + + // Init Cookies/Buffers + A0 [4]byte + A1 [16]byte + A2 [100]byte + + // Puzzle Data + X *big.Int + N *big.Int + Level uint32 + + // Identity + IdentityKey *ecdsa.PrivateKey + Alpha []byte // 10 random bytes + + // Server Data + Beta []byte + Omega []byte // Server Public Key (DER) + License []byte + + // Crypto + ClientEkPub [32]byte + ClientEkPriv [32]byte + ClientScalar *edwards25519.Scalar // Client ephemeral private key (scalar) + + SharedSecret []byte + SharedIV []byte + SharedMac []byte + + IdentityOffset uint64 // Extracted/Mined offset + IdentityLevel int + + Conn *transport.TS3Conn +} + +func NewHandshakeState(conn *transport.TS3Conn) (*HandshakeState, error) { + // Generate Identity On Startup (or load from disk in future) + + privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("failed to generate identity key: %v", err) + } + + alpha := make([]byte, 10) + if _, err := rand.Read(alpha); err != nil { + return nil, fmt.Errorf("failed to generate alpha: %v", err) + } + + // Generate Client Ephemeral Key (TS3 Style) + // TS3 uses a random 32-byte scalar with the high bit masked (&= 0x7F). + // It does NOT use standard Ed25519 clamping (bits 0-2 cleared, bit 254 set). + var clientScalar *edwards25519.Scalar + var clientKeyBytes [32]byte + + for { + if _, err := rand.Read(clientKeyBytes[:]); err != nil { + return nil, err + } + clientKeyBytes[31] &= 0x7F // Mask high bit (positive scalar) + + // Try to load as scalar. Might fail if >= L (very unlikely) + clientScalar, err = new(edwards25519.Scalar).SetCanonicalBytes(clientKeyBytes[:]) + if err == nil { + break + } + } + + return &HandshakeState{ + Step: 0, + Conn: conn, + IdentityKey: privKey, + Alpha: alpha, + ClientScalar: clientScalar, + }, nil +} + +// ... SendPacket0, HandlePacket1, SendPacket2, HandlePacket3 logic unchanged ... +// (Omitting for brevity if replacing, but user asked for full file usually. +// Just including necessary parts and new methods) + +func (h *HandshakeState) SendPacket0() error { + buf := new(bytes.Buffer) + ts := int32(time.Now().Unix()) - 1356998400 + binary.Write(buf, binary.BigEndian, ts) + buf.WriteByte(0x00) // Step 0 + now := int32(time.Now().Unix()) + binary.Write(buf, binary.BigEndian, now) + rand.Read(h.A0[:]) + buf.Write(h.A0[:]) + buf.Write(make([]byte, 8)) // 8 Zeros + pkt := protocol.NewPacket(protocol.PacketTypeInit1, buf.Bytes()) + pkt.Header.PacketID = 101 + pkt.Header.ClientID = 0 + pkt.Header.MAC = protocol.HandshakeMac + return h.Conn.SendPacket(pkt) +} + +func (h *HandshakeState) HandlePacket1(pkt *protocol.Packet) error { + if len(pkt.Data) < 21 { + return fmt.Errorf("packet 1 too short") + } + if pkt.Data[0] != 0x01 { + return fmt.Errorf("expected step 1, got %d", pkt.Data[0]) + } + copy(h.A1[:], pkt.Data[1:17]) + h.Step = 1 + return nil +} + +func (h *HandshakeState) SendPacket2() error { + buf := new(bytes.Buffer) + ts := int32(time.Now().Unix()) - 1356998400 + binary.Write(buf, binary.BigEndian, ts) + buf.WriteByte(0x02) // Step 2 + buf.Write(h.A1[:]) + a0Rev := [4]byte{h.A0[3], h.A0[2], h.A0[1], h.A0[0]} + buf.Write(a0Rev[:]) + pkt := protocol.NewPacket(protocol.PacketTypeInit1, buf.Bytes()) + pkt.Header.PacketID = 102 + pkt.Header.MAC = protocol.HandshakeMac + return h.Conn.SendPacket(pkt) +} + +func (h *HandshakeState) HandlePacket3(pkt *protocol.Packet) error { + if len(pkt.Data) < 233 { + return fmt.Errorf("packet 3 too short") + } + if pkt.Data[0] != 0x03 { + return fmt.Errorf("expected step 3, got %d", pkt.Data[0]) + } + h.X = new(big.Int).SetBytes(pkt.Data[1:65]) + h.N = new(big.Int).SetBytes(pkt.Data[65:129]) + h.Level = binary.BigEndian.Uint32(pkt.Data[129:133]) + copy(h.A2[:], pkt.Data[133:233]) + h.Step = 3 + log.Printf("Received Puzzle: Level=%d", h.Level) + return h.SendPacket4() +} + +func (h *HandshakeState) SendPacket4() error { + e := new(big.Int).Lsh(big.NewInt(1), uint(h.Level)) + y := new(big.Int).Exp(h.X, e, h.N) + yBytes := y.Bytes() + yPadded := make([]byte, 64) + if len(yBytes) > 64 { + copy(yPadded, yBytes[len(yBytes)-64:]) + } else { + copy(yPadded[64-len(yBytes):], yBytes) + } + buf := new(bytes.Buffer) + ts := int32(time.Now().Unix()) - 1356998400 + binary.Write(buf, binary.BigEndian, ts) + buf.WriteByte(0x04) // Step 4 + xPadded := make([]byte, 64) + xb := h.X.Bytes() + if len(xb) > 64 { + copy(xPadded, xb[len(xb)-64:]) + } else { + copy(xPadded[64-len(xb):], xb) + } + buf.Write(xPadded) + nPadded := make([]byte, 64) + nb := h.N.Bytes() + if len(nb) > 64 { + copy(nPadded, nb[len(nb)-64:]) + } else { + copy(nPadded[64-len(nb):], nb) + } + buf.Write(nPadded) + binary.Write(buf, binary.BigEndian, h.Level) + buf.Write(h.A2[:]) + buf.Write(yPadded) + cmdData := h.GenerateClientInitIV() + buf.Write([]byte(cmdData)) + pkt := protocol.NewPacket(protocol.PacketTypeInit1, buf.Bytes()) + pkt.Header.PacketID = 103 + pkt.Header.MAC = protocol.HandshakeMac + log.Println("Sending Packet 4 with Solution and clientinitiv...") + return h.Conn.SendPacket(pkt) +} + +// ProcessInitivexpand2 handles the decrypted command logic +func (h *HandshakeState) ProcessInitivexpand2(cmdArgs map[string]string) error { + if h.Step >= 5 { + log.Println("Ignoring duplicate initivexpand2 (Step already advanced)") + return nil + } + + lStr, ok := cmdArgs["l"] + if !ok { + return errors.New("missing license (l)") + } + + lData, err := base64.StdEncoding.DecodeString(lStr) + if err != nil { + return err + } + + betaStr, ok := cmdArgs["beta"] + if !ok { + return errors.New("missing beta") + } + h.Beta, err = base64.StdEncoding.DecodeString(betaStr) + if err != nil { + return err + } + + // 1. Derive Server Public Key (Edwards Y) + serverEdPubBytes, err := protocol.ParseLicenseAndDeriveKey(lData) + if err != nil { + return fmt.Errorf("LICENSE FAIL: %v", err) + } + + // Load Server Public Key as Edwards Point + serverPoint, err := new(edwards25519.Point).SetBytes(serverEdPubBytes) + if err != nil { + return fmt.Errorf("invalid server public key point: %v", err) + } + + // 2. Client Ephemeral Key (Scalar) is pre-generated in NewHandshakeState (h.ClientScalar) + // Compute Client Public Key (Point) = Scalar * Base + clientPubPoint := new(edwards25519.Point).ScalarBaseMult(h.ClientScalar) + h.ClientEkPub = [32]byte(clientPubPoint.Bytes()) + + // 3. Calculate Shared Secret (Ed25519 Scalar Mult) + + // Negate Server Public Key (TS3/Punisher.NaCl logic) + serverPointNeg := new(edwards25519.Point).Negate(serverPoint) + + // Multiply: Result = Scalar * (-ServerPoint) + sharedPoint := new(edwards25519.Point).ScalarMult(h.ClientScalar, serverPointNeg) + sharedBytes := sharedPoint.Bytes() + + // XOR the last byte with 0x80 (Flip sign bit of X coordinate) + sharedBytes[31] ^= 0x80 + + // 4. SHA512 Hash of the result + hash := sha512.Sum512(sharedBytes) + h.SharedSecret = hash[:] + + h.SharedIV = make([]byte, 64) + copy(h.SharedIV, h.SharedSecret) + + // XOR operations + // SharedIV[0..10] xor alpha + // SharedIV[10..64] xor beta + + // Alpha is 10 bytes (h.Alpha) + for i := 0; i < 10; i++ { + h.SharedIV[i] ^= h.Alpha[i] + } + + // Beta should be 54 bytes + log.Printf("Debug - Beta Length: %d", len(h.Beta)) + if len(h.Beta) >= 54 { + for i := 0; i < 54; i++ { + h.SharedIV[10+i] ^= h.Beta[i] + } + } + + // SharedMac = SHA1(SharedIV)[0..8] + macHash := sha1.Sum(h.SharedIV) + copy(h.SharedMac[:], macHash[0:8]) + + log.Printf("Debug - SharedSecret (SHA512): %s", hex.EncodeToString(h.SharedSecret)) + log.Printf("Debug - SharedIV: %s", hex.EncodeToString(h.SharedIV)) + log.Printf("Debug - SharedMac: %s", hex.EncodeToString(h.SharedMac[:])) + log.Println("Shared Secret & Keys Calculated using TS3 Ed25519 logic.") + + return h.SendClientEk() +} + +func (h *HandshakeState) SendClientEk() error { + // clientek ek={ek} proof={proof} + + // ek = base64(client_public_key) [Ed25519 Compressed Point] + ekStr := base64.StdEncoding.EncodeToString(h.ClientEkPub[:]) + + // proof = base64(ecdsa_sign(client_public_key + beta)) + // "sign must be done with the private key from the identity keypair." (P-256) + + proofBuf := append(h.ClientEkPub[:], h.Beta...) + hash := sha256.Sum256(proofBuf) + + r, s, err := ecdsa.Sign(rand.Reader, h.IdentityKey, hash[:]) + if err != nil { + return err + } + + // Encode Signature (ASN.1 DER) + // Reverting to DER as server uses DER. + + sigContent := new(bytes.Buffer) + writeBigInt(sigContent, r) + writeBigInt(sigContent, s) + + sigSeq := new(bytes.Buffer) + sigSeq.WriteByte(0x30) + writeLength(sigSeq, sigContent.Len()) + sigSeq.Write(sigContent.Bytes()) + + proofBytes := sigSeq.Bytes() + proofStr := base64.StdEncoding.EncodeToString(proofBytes) + + log.Printf("Debug - ClientEk: %s", ekStr) + log.Printf("Debug - Proof (DER): %s", hex.EncodeToString(proofBytes)) + + // Construct Command + cmd := fmt.Sprintf("clientek ek=%s proof=%s", protocol.Escape(ekStr), protocol.Escape(proofStr)) + + // Packet 1 (New counter logic? Spec: "clientek already has the packet id 1") + // This is the START of the new encrypted session? + // "The normal packet id counting starts with this packet." + + buf := new(bytes.Buffer) + buf.Write([]byte(cmd)) + + pkt := protocol.NewPacket(protocol.PacketTypeCommand, buf.Bytes()) + pkt.Header.PacketID = 1 // Reset to 1 + pkt.Header.SetType(protocol.PacketTypeCommand) // Ensure flag (NewProtocol? Unencrypted?) + + // Encryption? + // "All Command ... Packets must get encrypted." (Using OLD Handshake keys? Or NEW?) + // "The crypto handshake is now completed. The normal encryption scheme ... is from now on used." + // Usually implies clientek IS encrypted with the NEW keys. + + // Add PacketFlagNewProtocol + pkt.Header.Type |= protocol.PacketFlagNewProtocol // 0x20 + + // Encryption + // Try using HandshakeKey like initivexpand2 instead of SharedSecret + // The crypto switch might happen AFTER clientek is accepted + key := protocol.HandshakeKey + nonce := protocol.HandshakeNonce + + // Prepare Meta for EAX + // Meta for Client->Server: PID(2) + CID(2) + PT(1) = 5 bytes + meta := make([]byte, 5) + binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID) + binary.BigEndian.PutUint16(meta[2:4], pkt.Header.ClientID) + meta[4] = pkt.Header.Type + + // Encrypt + cipherDict, mac, err := protocol.EncryptEAX(key, nonce, meta, pkt.Data) + if err != nil { + return err + } + + pkt.Data = cipherDict + copy(pkt.Header.MAC[:], mac) + + log.Println("Sending clientek (Packet 1) [Encrypted]") + h.Step = 5 + return h.Conn.SendPacket(pkt) +} + +func (h *HandshakeState) GenerateClientInitIV() string { + // ... (Existing implementation) ... + // Copy from previous step + + alphaStr := base64.StdEncoding.EncodeToString(h.Alpha) + omegaStr := h.GenerateOmega() + + ip := "127.0.0.1" + if addr, ok := h.Conn.RemoteAddr().(*net.UDPAddr); ok { + ip = addr.IP.String() + } else if h.Conn.RemoteAddr() != nil { + ip = h.Conn.RemoteAddr().String() + if host, _, err := net.SplitHostPort(ip); err == nil { + ip = host + } + } + + return fmt.Sprintf("clientinitiv alpha=%s omega=%s ot=1 ip=%s", + protocol.Escape(alphaStr), protocol.Escape(omegaStr), protocol.Escape(ip)) +} + +func (h *HandshakeState) GenerateOmega() string { + // ... (Existing implementation) ... + pub := h.IdentityKey.PublicKey + + content := new(bytes.Buffer) + content.Write([]byte{0x03, 0x02, 0x07, 0x00}) + content.Write([]byte{0x02, 0x01, 0x20}) + writeBigInt(content, pub.X) + writeBigInt(content, pub.Y) + seq := new(bytes.Buffer) + seq.WriteByte(0x30) + writeLength(seq, content.Len()) + seq.Write(content.Bytes()) + return base64.StdEncoding.EncodeToString(seq.Bytes()) +} + +func writeBigInt(buf *bytes.Buffer, n *big.Int) { + b := n.Bytes() + buf.WriteByte(0x02) + padded := b + if len(b) > 0 && b[0] >= 0x80 { + padded = make([]byte, len(b)+1) + padded[0] = 0x00 + copy(padded[1:], b) + } else if len(b) == 0 { + padded = []byte{0x00} + } + writeLength(buf, len(padded)) + buf.Write(padded) +} + +func writeLength(buf *bytes.Buffer, length int) { + if length < 128 { + buf.WriteByte(byte(length)) + } else { + s := fmt.Sprintf("%x", length) + if len(s)%2 != 0 { + s = "0" + s + } + b, _ := hex.DecodeString(s) + buf.WriteByte(0x80 | byte(len(b))) + buf.Write(b) + } +} + +// ImproveSecurityLevel mines a counter to achieve the target security level. +func (h *HandshakeState) ImproveSecurityLevel(targetLevel int) { + omega := h.GenerateOmega() // Base64 of ASN.1 Public Key + + // Start from current offset (usually 0) + counter := h.IdentityOffset + + fmt.Printf("Mining Identity Level %d... ", targetLevel) + + for { + // Construct data: Omega + Counter (ASCII) + data := fmt.Sprintf("%s%d", omega, counter) + + // SHA1 + hash := sha1.Sum([]byte(data)) + + // Count leading zero bits + zeros := countLeadingZeros(hash[:]) + + if zeros >= targetLevel { + h.IdentityLevel = zeros + h.IdentityOffset = counter + fmt.Printf("Found! Offset=%d, Level=%d\n", counter, zeros) + return + } + + counter++ + if counter%100000 == 0 { + // fmt.Print(".") + } + } +} + +func countLeadingZeros(hash []byte) int { + zeros := 0 + for _, b := range hash { + if b == 0 { + zeros += 8 + } else { + // Count bits in this byte + for i := 7; i >= 0; i-- { + if (b>>i)&1 == 0 { + zeros++ + } else { + return zeros + } + } + return zeros + } + } + return zeros +} diff --git a/pkg/protocol/commands.go b/pkg/protocol/commands.go new file mode 100644 index 0000000..893df65 --- /dev/null +++ b/pkg/protocol/commands.go @@ -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) +} diff --git a/pkg/protocol/crypto.go b/pkg/protocol/crypto.go new file mode 100644 index 0000000..a47fa32 --- /dev/null +++ b/pkg/protocol/crypto.go @@ -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 +} diff --git a/pkg/protocol/crypto_init2_test.go b/pkg/protocol/crypto_init2_test.go new file mode 100644 index 0000000..ad768d1 --- /dev/null +++ b/pkg/protocol/crypto_init2_test.go @@ -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) + } +} diff --git a/pkg/protocol/crypto_test.go b/pkg/protocol/crypto_test.go new file mode 100644 index 0000000..35d7d69 --- /dev/null +++ b/pkg/protocol/crypto_test.go @@ -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) + } +} diff --git a/pkg/protocol/eax.go b/pkg/protocol/eax.go new file mode 100644 index 0000000..f021b24 --- /dev/null +++ b/pkg/protocol/eax.go @@ -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 +} diff --git a/pkg/protocol/eax_test.go b/pkg/protocol/eax_test.go new file mode 100644 index 0000000..35c4406 --- /dev/null +++ b/pkg/protocol/eax_test.go @@ -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) + } +} diff --git a/pkg/protocol/enums.go b/pkg/protocol/enums.go new file mode 100644 index 0000000..d35397a --- /dev/null +++ b/pkg/protocol/enums.go @@ -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 +) diff --git a/pkg/protocol/license.go b/pkg/protocol/license.go new file mode 100644 index 0000000..a828dda --- /dev/null +++ b/pkg/protocol/license.go @@ -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 +} diff --git a/pkg/protocol/license_test.go b/pkg/protocol/license_test.go new file mode 100644 index 0000000..1813c4c --- /dev/null +++ b/pkg/protocol/license_test.go @@ -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) + } + }) + } +} diff --git a/pkg/protocol/math.go b/pkg/protocol/math.go new file mode 100644 index 0000000..35b9638 --- /dev/null +++ b/pkg/protocol/math.go @@ -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) +} diff --git a/pkg/protocol/packet.go b/pkg/protocol/packet.go new file mode 100644 index 0000000..8fa29b0 --- /dev/null +++ b/pkg/protocol/packet.go @@ -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, + } +} diff --git a/pkg/transport/socket.go b/pkg/transport/socket.go new file mode 100644 index 0000000..05540d4 --- /dev/null +++ b/pkg/transport/socket.go @@ -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 +} diff --git a/timestamp.go b/timestamp.go new file mode 100644 index 0000000..5f304f2 --- /dev/null +++ b/timestamp.go @@ -0,0 +1,11 @@ +package main + +import ( + "fmt" + "time" +) + +func main() { + t, _ := time.Parse("2006-01-02 15:04:05", "2023-07-24 10:06:33") + fmt.Println(t.Unix()) +}