working
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ts3j/
|
||||||
BIN
cmd/client/client.exe
Normal file
BIN
cmd/client/client.exe
Normal file
Binary file not shown.
41
cmd/client/main.go
Normal file
41
cmd/client/main.go
Normal file
@@ -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...")
|
||||||
|
}
|
||||||
|
}
|
||||||
332
cmd/fakeserver/main.go
Normal file
332
cmd/fakeserver/main.go
Normal file
@@ -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))
|
||||||
|
}
|
||||||
138
cmd/proxy/main_design.go
Normal file
138
cmd/proxy/main_design.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
23
docker-compose.yml
Normal file
23
docker-compose.yml
Normal file
@@ -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:
|
||||||
9
go.mod
Normal file
9
go.mod
Normal file
@@ -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
|
||||||
4
go.sum
Normal file
4
go.sum
Normal file
@@ -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=
|
||||||
563
internal/client/client.go
Normal file
563
internal/client/client.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
504
internal/client/handshake.go
Normal file
504
internal/client/handshake.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
11
timestamp.go
Normal file
11
timestamp.go
Normal file
@@ -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())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user