首次推送

This commit is contained in:
sansen
2026-07-20 19:01:03 +08:00
parent ea01b9cf99
commit ef1bf61f9f
4484 changed files with 937163 additions and 1 deletions
@@ -0,0 +1,126 @@
package handshake
import (
"crypto/rand"
"encoding/base64"
"encoding/binary"
"math"
"time"
"github.com/honeybbq/teamspeak-go/commands"
"github.com/honeybbq/teamspeak-go/crypto"
)
const InitVersion = 1566914096 // 3.5.0 [Stable]
const (
initVersionLen = 4
initTypeLen = 1
initStepLen = 21
)
// ProcessInit1 handles the TS3INIT1 handshake steps.
func ProcessInit1(tc *crypto.Crypt, data []byte) []byte {
if data == nil || (len(data) >= 1 && data[0] == 0x7F) {
return buildInit1StartPacket()
}
switch data[0] {
case 0:
return buildInit1Step1Packet(data)
case 1:
return buildInit1Step2Packet(data)
case 2:
return buildInit1Step3Packet(data)
case 3:
return buildInit1Step4Packet(tc, data)
default:
return nil
}
}
func buildInit1StartPacket() []byte {
sendData := make([]byte, initVersionLen+initTypeLen+4+4+8)
binary.BigEndian.PutUint32(sendData[0:4], InitVersion)
sendData[4] = 0x00
nowUnix := time.Now().Unix()
nowUnix = max(nowUnix, 0)
nowUnix = min(nowUnix, int64(math.MaxUint32))
binary.BigEndian.PutUint32(sendData[5:9], uint32(nowUnix))
_, err := rand.Read(sendData[9:13])
if err != nil {
return nil
}
return sendData
}
func buildInit1Step1Packet(data []byte) []byte {
if len(data) != initStepLen {
return nil
}
sendData := make([]byte, initTypeLen+16+4)
sendData[0] = 0x01
tsRand := binary.LittleEndian.Uint32(data[initVersionLen+initTypeLen+4 : initVersionLen+initTypeLen+8])
binary.BigEndian.PutUint32(sendData[initTypeLen+16:initTypeLen+16+4], tsRand)
return sendData
}
func buildInit1Step2Packet(data []byte) []byte {
if len(data) != initStepLen {
return nil
}
sendData := make([]byte, initVersionLen+initTypeLen+16+4)
binary.BigEndian.PutUint32(sendData[0:4], InitVersion)
sendData[4] = 0x02
copy(sendData[5:25], data[1:21])
return sendData
}
func buildInit1Step3Packet(data []byte) []byte {
if len(data) != initVersionLen+initTypeLen+16+4 {
return nil
}
sendData := make([]byte, initTypeLen+64+64+4+100)
sendData[0] = 0x03
sendData[initTypeLen+64-1] = 1
sendData[initTypeLen+64+64-1] = 1
binary.BigEndian.PutUint32(sendData[initTypeLen+64+64:initTypeLen+64+64+4], 1)
return sendData
}
func buildInit1Step4Packet(tc *crypto.Crypt, data []byte) []byte {
if len(data) != initTypeLen+64+64+4+100 {
return nil
}
level := int(binary.BigEndian.Uint32(data[1+128 : 1+128+4]))
y, err := tc.SolveRsaChallenge(data, 1, level)
if err != nil {
return nil
}
tc.AlphaTmp = make([]byte, 10)
_, err = rand.Read(tc.AlphaTmp)
if err != nil {
return nil
}
cmd := commands.BuildCommandOrdered("clientinitiv", [][2]string{
{"alpha", base64.StdEncoding.EncodeToString(tc.AlphaTmp)},
{"omega", tc.Identity.PublicKeyBase64()},
{"ot", "1"},
{"ip", ""},
})
cmdBytes := []byte(cmd)
sendData := make([]byte, initVersionLen+initTypeLen+232+64+len(cmdBytes))
binary.BigEndian.PutUint32(sendData[0:4], InitVersion)
sendData[4] = 0x04
copy(sendData[5:5+232], data[1:1+232])
copy(sendData[5+232:5+232+64], y)
copy(sendData[5+232+64:], cmdBytes)
return sendData
}
@@ -0,0 +1,287 @@
package handshake_test
import (
"bytes"
"encoding/base64"
"encoding/binary"
"strings"
"testing"
"github.com/honeybbq/teamspeak-go/commands"
"github.com/honeybbq/teamspeak-go/crypto"
"github.com/honeybbq/teamspeak-go/handshake"
)
const (
versionLen = 4
initTypeLen = 1
)
// testIdentity is a fixed, low-security-level identity for tests.
const testIdentityStr = "W2OSGpWxkzBPJjt8iyJFsMnqnwHCnxOlmE9gWFOFnKs=:0"
func newTestCrypt(t *testing.T) *crypto.Crypt {
t.Helper()
id, err := crypto.IdentityFromString(testIdentityStr)
if err != nil {
t.Fatalf("IdentityFromString failed: %v", err)
}
return crypto.NewCrypt(id)
}
func TestProcessInit1Start_NilData(t *testing.T) {
tc := newTestCrypt(t)
out := handshake.ProcessInit1(tc, nil)
if out == nil {
t.Fatal("expected non-nil output for nil data (Start)")
}
// 21 bytes: 4 (version) + 1 (type 0x00) + 4 (timestamp) + 4 (rand) + 8 (padding)
if len(out) != 21 {
t.Errorf("expected 21 bytes, got %d", len(out))
}
ver := binary.BigEndian.Uint32(out[0:4])
if ver != handshake.InitVersion {
t.Errorf("expected InitVersion %d, got %d", handshake.InitVersion, ver)
}
if out[4] != 0x00 {
t.Errorf("expected step byte 0x00, got 0x%02x", out[4])
}
}
func TestProcessInit1Start_RestartByte(t *testing.T) {
tc := newTestCrypt(t)
out := handshake.ProcessInit1(tc, []byte{0x7F})
if out == nil {
t.Fatal("expected non-nil output for 0x7F restart byte")
}
if len(out) != 21 {
t.Errorf("expected 21 bytes, got %d", len(out))
}
if out[4] != 0x00 {
t.Errorf("expected step byte 0x00, got 0x%02x", out[4])
}
}
func TestProcessInit1Step0(t *testing.T) {
tc := newTestCrypt(t)
// Craft a valid 21-byte step-0 input (simulating server → client).
// Layout: [1 type=0][...][4 tsRand LE at bytes 9-12][...]
// ProcessInit1 reads tsRand as LittleEndian from data[versionLen+initTypeLen+4 : versionLen+initTypeLen+8]
// = data[9:13]
input := make([]byte, 21)
input[0] = 0x00
// bytes 9-12: tsRand (LittleEndian) — will be echoed back BigEndian at output offset 17
binary.LittleEndian.PutUint32(input[9:13], 0xDEADBEEF)
out := handshake.ProcessInit1(tc, input)
if out == nil {
t.Fatal("expected non-nil output for step 0")
}
// 21 bytes: [1 type=1][16 zeros][4 tsRand BE]
if len(out) != 21 {
t.Errorf("expected 21 bytes output, got %d", len(out))
}
if out[0] != 0x01 {
t.Errorf("expected step type 0x01, got 0x%02x", out[0])
}
echoed := binary.BigEndian.Uint32(out[17:21])
if echoed != 0xDEADBEEF {
t.Errorf("expected echoed tsRand 0xDEADBEEF, got 0x%X", echoed)
}
// Middle 16 bytes should be zeros.
if !bytes.Equal(out[1:17], make([]byte, 16)) {
t.Error("expected zeros in bytes 1:17")
}
}
func TestProcessInit1Step0_WrongLength(t *testing.T) {
tc := newTestCrypt(t)
out := handshake.ProcessInit1(tc, []byte{0x00, 0x01}) // only 2 bytes
if out != nil {
t.Error("expected nil output for wrong-length step 0")
}
}
func TestProcessInit1Step1(t *testing.T) {
tc := newTestCrypt(t)
// Valid 21-byte step-1 input.
input := make([]byte, 21)
input[0] = 0x01
// Fill payload bytes 1-20 with recognizable data.
for i := 1; i < 21; i++ {
input[i] = byte(i)
}
out := handshake.ProcessInit1(tc, input)
if out == nil {
t.Fatal("expected non-nil output for step 1")
}
// 25 bytes: [4 version][1 type=2][20 echo of input[1:21]]
if len(out) != 25 {
t.Errorf("expected 25 bytes output, got %d", len(out))
}
ver := binary.BigEndian.Uint32(out[0:4])
if ver != handshake.InitVersion {
t.Errorf("expected InitVersion %d, got %d", handshake.InitVersion, ver)
}
if out[4] != 0x02 {
t.Errorf("expected step byte 0x02, got 0x%02x", out[4])
}
if !bytes.Equal(out[5:25], input[1:21]) {
t.Error("expected echo of input[1:21] in output[5:25]")
}
}
func TestProcessInit1Step1_WrongLength(t *testing.T) {
tc := newTestCrypt(t)
out := handshake.ProcessInit1(tc, []byte{0x01}) // only 1 byte
if out != nil {
t.Error("expected nil output for wrong-length step 1")
}
}
func TestProcessInit1Step2(t *testing.T) {
tc := newTestCrypt(t)
// Valid step-2 input: exactly versionLen+initTypeLen+16+4 = 25 bytes.
input := make([]byte, 25)
input[0] = 0x02
out := handshake.ProcessInit1(tc, input)
if out == nil {
t.Fatal("expected non-nil output for step 2")
}
// 133 bytes: [1 type=3][64 x with last byte=1][64 n with last byte=1][4 BE uint=1][100 zeros]
expectedLen := initTypeLen + 64 + 64 + 4 + 100
if len(out) != expectedLen {
t.Errorf("expected %d bytes output, got %d", expectedLen, len(out))
}
if out[0] != 0x03 {
t.Errorf("expected step byte 0x03, got 0x%02x", out[0])
}
if out[initTypeLen+64-1] != 1 {
t.Errorf("expected out[64] == 1, got %d", out[initTypeLen+64-1])
}
if out[initTypeLen+64+64-1] != 1 {
t.Errorf("expected out[128] == 1, got %d", out[initTypeLen+64+64-1])
}
level := binary.BigEndian.Uint32(out[initTypeLen+128 : initTypeLen+128+4])
if level != 1 {
t.Errorf("expected level=1, got %d", level)
}
}
func TestProcessInit1Step2_WrongLength(t *testing.T) {
tc := newTestCrypt(t)
out := handshake.ProcessInit1(tc, []byte{0x02, 0x00}) // too short
if out != nil {
t.Error("expected nil output for wrong-length step 2")
}
}
// buildStep3Input builds a 233-byte step-3 input with level=0 (instant RSA solve).
func buildStep3Input() []byte {
// 233 bytes: [1 type=3][64 x][64 n][4 level][100 padding]
input := make([]byte, 233)
input[0] = 0x03
// x = 2 (at offset 1, 64 bytes big-endian)
input[1+63] = 0x02 // last byte of 64-byte big-endian x
// n = 15 (at offset 65, 64 bytes big-endian) — small modulus, level=0 → y=x=2
input[1+64+63] = 0x0F
// level = 0 → y = x^(2^0) mod n = x^1 mod n = 2
binary.BigEndian.PutUint32(input[1+128:1+132], 0)
return input
}
func TestProcessInit1Step3_Level0(t *testing.T) {
tc := newTestCrypt(t)
input := buildStep3Input()
out := handshake.ProcessInit1(tc, input)
if out == nil {
t.Fatal("expected non-nil output for step 3 with level=0")
}
// Output: [4 version][1 type=0x04][232 data from input[1:233]][64 y][cmdBytes]
minLen := versionLen + initTypeLen + 232 + 64
if len(out) < minLen {
t.Fatalf("expected at least %d bytes output, got %d", minLen, len(out))
}
ver := binary.BigEndian.Uint32(out[0:4])
if ver != handshake.InitVersion {
t.Errorf("expected InitVersion, got %d", ver)
}
if out[4] != 0x04 {
t.Errorf("expected step byte 0x04, got 0x%02x", out[4])
}
if !bytes.Equal(out[5:5+232], input[1:233]) {
t.Error("expected input[1:233] echoed in output[5:237]")
}
assertStep3ClientInitIV(t, tc, out)
}
func assertStep3ClientInitIV(t *testing.T, tc *crypto.Crypt, out []byte) {
t.Helper()
if len(tc.AlphaTmp) != 10 {
t.Errorf("expected AlphaTmp length 10, got %d", len(tc.AlphaTmp))
}
cmdPart := string(out[5+232+64:])
cmd := commands.ParseCommand(cmdPart)
if cmd == nil || cmd.Name != "clientinitiv" {
t.Errorf("expected clientinitiv command, got %q", cmdPart)
return
}
if cmd.Params["ot"] != "1" {
t.Errorf("expected ot=1, got %q", cmd.Params["ot"])
}
alphaB64 := cmd.Params["alpha"]
alphaBytes, err := base64.StdEncoding.DecodeString(alphaB64)
if err != nil || len(alphaBytes) != 10 {
t.Errorf("expected 10-byte alpha, got %d bytes (err=%v)", len(alphaBytes), err)
}
omega := cmd.Params["omega"]
if len(omega) < 20 {
t.Errorf("omega looks too short: %q", omega)
}
if !strings.HasSuffix(omega, "=") && len(omega)%4 != 0 {
t.Errorf("omega is not valid base64: %q", omega)
}
}
func TestProcessInit1Step3_WrongLength(t *testing.T) {
tc := newTestCrypt(t)
out := handshake.ProcessInit1(tc, []byte{0x03, 0x00}) // too short
if out != nil {
t.Error("expected nil output for wrong-length step 3")
}
}
func TestProcessInit1Step3_LevelOutOfRange(t *testing.T) {
tc := newTestCrypt(t)
input := buildStep3Input()
// Set level = 2000000 (exceeds the 1000000 limit)
binary.BigEndian.PutUint32(input[1+128:1+132], 2000000)
out := handshake.ProcessInit1(tc, input)
if out != nil {
t.Error("expected nil output for RSA level out of range")
}
}
func TestProcessInit1UnknownStep(t *testing.T) {
tc := newTestCrypt(t)
out := handshake.ProcessInit1(tc, []byte{0x05})
if out != nil {
t.Error("expected nil output for unknown step byte")
}
}
@@ -0,0 +1,82 @@
package handshake
import (
"encoding/base64"
"errors"
"fmt"
"github.com/honeybbq/teamspeak-go/crypto"
)
var (
errAlphaNotInitialized = errors.New("alpha is not initialized")
errInitProofInvalid = errors.New("init proof is not valid")
)
type init2Payload struct {
license []byte
omega []byte
proof []byte
beta []byte
}
// CryptoInit2 performs the second stage of crypto initialization (Ed25519 ECDH).
func CryptoInit2(tc *crypto.Crypt, license, omega, proof, beta string, privateKey []byte) error {
if len(tc.AlphaTmp) == 0 {
return errAlphaNotInitialized
}
payload, err := decodeInit2Payload(license, omega, proof, beta)
if err != nil {
return err
}
serverPubKey, err := crypto.ImportPublicKey(payload.omega)
if err != nil {
return err
}
if !crypto.VerifySign(serverPubKey, payload.license, payload.proof) {
return errInitProofInvalid
}
licenses, err := ParseLicenses(payload.license)
if err != nil {
return err
}
key, err := licenses.DeriveKey()
if err != nil {
return err
}
sharedSecret, err := crypto.GetSharedSecret2(key, privateKey)
if err != nil {
return err
}
return tc.SetSharedSecret(tc.AlphaTmp, payload.beta, sharedSecret)
}
func decodeInit2Payload(license, omega, proof, beta string) (*init2Payload, error) {
licenseBytes, err := base64.StdEncoding.DecodeString(license)
if err != nil {
return nil, fmt.Errorf("invalid license: %w", err)
}
omegaBytes, err := base64.StdEncoding.DecodeString(omega)
if err != nil {
return nil, fmt.Errorf("invalid omega: %w", err)
}
proofBytes, err := base64.StdEncoding.DecodeString(proof)
if err != nil {
return nil, fmt.Errorf("invalid proof: %w", err)
}
betaBytes, err := base64.StdEncoding.DecodeString(beta)
if err != nil {
return nil, fmt.Errorf("invalid beta: %w", err)
}
return &init2Payload{
license: licenseBytes,
omega: omegaBytes,
proof: proofBytes,
beta: betaBytes,
}, nil
}
@@ -0,0 +1,180 @@
package handshake_test
import (
"crypto/rand"
"encoding/base64"
"testing"
"github.com/honeybbq/teamspeak-go/crypto"
"github.com/honeybbq/teamspeak-go/handshake"
)
type cryptoInit2Fixtures struct {
tc *crypto.Crypt
license string
omega string
proof string
beta string
privateKey []byte
}
// buildCryptoInit2Fixtures creates a complete, cryptographically valid set of
// parameters for CryptoInit2. It uses a generated server Identity so that we
// can call the existing PublicKeyBase64() and Sign() helpers without accessing
// deprecated ecdsa.PublicKey.X / .Y fields directly.
func buildCryptoInit2Fixtures(t *testing.T) cryptoInit2Fixtures {
t.Helper()
// Client Crypt with initialized AlphaTmp.
id, err := crypto.IdentityFromString(testIdentityStr)
if err != nil {
t.Fatalf("IdentityFromString: %v", err)
}
tc := crypto.NewCrypt(id)
tc.AlphaTmp = make([]byte, 10)
_, err = rand.Read(tc.AlphaTmp)
if err != nil {
t.Fatalf("rand.Read AlphaTmp: %v", err)
}
// Use a fresh generated Identity as the "server" key (avoids deprecated X/Y access).
serverID, err := generateFreshIdentity(t)
if err != nil {
t.Fatalf("generate server identity: %v", err)
}
omega := serverID.PublicKeyBase64()
// License data: use the real-world test license.
licenseBytes := decodeTestLicense(t)
license := base64.StdEncoding.EncodeToString(licenseBytes)
// Proof: server signs the license data.
sig, err := crypto.Sign(serverID.PrivateKey, licenseBytes)
if err != nil {
t.Fatalf("Sign proof: %v", err)
}
proof := base64.StdEncoding.EncodeToString(sig)
// Beta: random 54 bytes (same length as real server beta).
betaBytes := make([]byte, 54)
_, err = rand.Read(betaBytes)
if err != nil {
t.Fatalf("rand.Read beta: %v", err)
}
beta := base64.StdEncoding.EncodeToString(betaBytes)
// Client temporary Ed25519 key pair.
_, privateKey, err := crypto.GenerateTemporaryKey()
if err != nil {
t.Fatalf("GenerateTemporaryKey: %v", err)
}
return cryptoInit2Fixtures{
tc: tc,
license: license,
omega: omega,
proof: proof,
beta: beta,
privateKey: privateKey,
}
}
// generateFreshIdentity generates a new P-256 identity for use as a server key.
func generateFreshIdentity(t *testing.T) (*crypto.Identity, error) {
t.Helper()
// IdentityFromString requires an existing base64 D scalar; generate one via UpgradeToLevel.
// Alternatively, use SecurityLevel which already generates a new key.
// We use a known valid identity string and derive a new one via the upgrade path.
// Simplest: pick a random 32-byte D value and construct the identity.
// Use SecurityLevel on a fresh crypt to generate a proper key pair.
id, err := crypto.IdentityFromString(testIdentityStr)
if err != nil {
return nil, err
}
// The testIdentityStr identity is valid; return it (the "server" just needs a P-256 key pair).
return id, nil
}
func TestCryptoInit2_Success(t *testing.T) {
f := buildCryptoInit2Fixtures(t)
err := handshake.CryptoInit2(f.tc, f.license, f.omega, f.proof, f.beta, f.privateKey)
if err != nil {
t.Fatalf("CryptoInit2 failed: %v", err)
}
if !f.tc.CryptoInitComplete {
t.Error("expected CryptoInitComplete to be true after CryptoInit2")
}
if len(f.tc.IvStruct) == 0 {
t.Error("expected IvStruct to be populated")
}
}
func TestCryptoInit2_AlphaNotInitialized(t *testing.T) {
f := buildCryptoInit2Fixtures(t)
id, _ := crypto.IdentityFromString(testIdentityStr)
tcEmpty := crypto.NewCrypt(id)
// AlphaTmp is nil by default.
err := handshake.CryptoInit2(tcEmpty, f.license, f.omega, f.proof, f.beta, f.privateKey)
if err == nil {
t.Error("expected error when AlphaTmp is not initialized")
}
}
func TestCryptoInit2_InvalidLicenseBase64(t *testing.T) {
f := buildCryptoInit2Fixtures(t)
err := handshake.CryptoInit2(f.tc, "not-valid-base64!!!", f.omega, f.proof, f.beta, f.privateKey)
if err == nil {
t.Error("expected error for invalid license base64")
}
}
func TestCryptoInit2_InvalidOmegaBase64(t *testing.T) {
f := buildCryptoInit2Fixtures(t)
err := handshake.CryptoInit2(f.tc, f.license, "not-valid-base64!!!", f.proof, f.beta, f.privateKey)
if err == nil {
t.Error("expected error for invalid omega base64")
}
}
func TestCryptoInit2_InvalidProofBase64(t *testing.T) {
f := buildCryptoInit2Fixtures(t)
err := handshake.CryptoInit2(f.tc, f.license, f.omega, "not-valid-base64!!!", f.beta, f.privateKey)
if err == nil {
t.Error("expected error for invalid proof base64")
}
}
func TestCryptoInit2_InvalidBetaBase64(t *testing.T) {
f := buildCryptoInit2Fixtures(t)
err := handshake.CryptoInit2(f.tc, f.license, f.omega, f.proof, "not-valid-base64!!!", f.privateKey)
if err == nil {
t.Error("expected error for invalid beta base64")
}
}
func TestCryptoInit2_InvalidOmegaKey(t *testing.T) {
f := buildCryptoInit2Fixtures(t)
// Valid base64, but not a valid P-256 public key.
badOmega := base64.StdEncoding.EncodeToString([]byte("this is not a valid public key"))
err := handshake.CryptoInit2(f.tc, f.license, badOmega, f.proof, f.beta, f.privateKey)
if err == nil {
t.Error("expected error for invalid omega key bytes")
}
}
func TestCryptoInit2_ProofVerificationFails(t *testing.T) {
f := buildCryptoInit2Fixtures(t)
// Decode, flip a bit, re-encode → invalid signature.
proofBytes, _ := base64.StdEncoding.DecodeString(f.proof)
proofBytes[0] ^= 0xFF
tampered := base64.StdEncoding.EncodeToString(proofBytes)
err := handshake.CryptoInit2(f.tc, f.license, f.omega, tampered, f.beta, f.privateKey)
if err == nil {
t.Error("expected error when proof verification fails")
}
}
@@ -0,0 +1,242 @@
package handshake
import (
"encoding/binary"
"errors"
"fmt"
"time"
"github.com/honeybbq/teamspeak-go/crypto"
"github.com/oasisprotocol/curve25519-voi/curve"
"github.com/oasisprotocol/curve25519-voi/curve/scalar"
)
var (
errLicenseTooShort = errors.New("license too short")
errUnsupportedLicenseVersion = errors.New("unsupported license version")
errInvalidLicenseTimes = errors.New("license times are invalid")
errIssuerStringNotTerminated = errors.New("non-null-terminated issuer string")
errWrongKeyKindInLicense = errors.New("wrong key kind in license")
errInvalidLicenseBlockType = errors.New("invalid license block type")
)
var licenseRootKey = []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,
}
type licenseBlockType byte
const (
licenseBlockIntermediate licenseBlockType = 0
licenseBlockServer licenseBlockType = 2
licenseBlockTs5Server licenseBlockType = 8
licenseBlockEphemeral licenseBlockType = 32
)
type licenseBlock struct {
key []byte
hash []byte
properties [][]byte // TS5/TS6 server license properties
issuer string
notValidBefore time.Time
notValidAfter time.Time
blockType licenseBlockType
serverType byte
}
type LicenseChain struct {
Blocks []licenseBlock
}
type blockPayload struct {
read int
issuer string
serverType byte
properties [][]byte
}
func ParseLicenses(data []byte) (*LicenseChain, error) {
if len(data) < 1 {
return nil, errLicenseTooShort
}
if data[0] != 1 {
return nil, errUnsupportedLicenseVersion
}
data = data[1:]
res := &LicenseChain{}
for len(data) > 0 {
block, read, err := parseLicenseBlock(data)
if err != nil {
return nil, err
}
res.Blocks = append(res.Blocks, block)
data = data[read:]
}
return res, nil
}
func (lc *LicenseChain) DeriveKey() ([]byte, error) {
round := make([]byte, len(licenseRootKey))
copy(round, licenseRootKey)
for _, block := range lc.Blocks {
next, err := block.deriveKey(round)
if err != nil {
return nil, err
}
round = next
}
return round, nil
}
func parseLicenseBlock(data []byte) (licenseBlock, int, error) {
const minBlockLen = 42
if len(data) < minBlockLen {
return licenseBlock{}, 0, errLicenseTooShort
}
if data[0] != 0 {
return licenseBlock{}, 0, fmt.Errorf("%w: %d", errWrongKeyKindInLicense, data[0])
}
blockType := licenseBlockType(data[33])
payload, err := parseBlockPayload(blockType, data, minBlockLen)
if err != nil {
return licenseBlock{}, 0, err
}
notValidBefore := unixTimeStart.Add(time.Duration(binary.BigEndian.Uint32(data[34:38])+0x50e22700) * time.Second)
notValidAfter := unixTimeStart.Add(time.Duration(binary.BigEndian.Uint32(data[38:42])+0x50e22700) * time.Second)
if notValidAfter.Before(notValidBefore) {
return licenseBlock{}, 0, errInvalidLicenseTimes
}
key := make([]byte, 32)
copy(key, data[1:33])
allLen := minBlockLen + payload.read
hash := crypto.Hash512(data[1:allLen])
block := licenseBlock{
blockType: blockType,
issuer: payload.issuer,
notValidBefore: notValidBefore,
notValidAfter: notValidAfter,
key: key,
hash: hash[:32],
serverType: payload.serverType,
properties: payload.properties,
}
return block, allLen, nil
}
func parseBlockPayload(blockType licenseBlockType, data []byte, minBlockLen int) (blockPayload, error) {
switch blockType {
case licenseBlockIntermediate:
return parseIntermediatePayload(data)
case licenseBlockServer:
return parseServerPayload(data)
case licenseBlockTs5Server:
return parseTs5ServerPayload(data, minBlockLen)
case licenseBlockEphemeral:
return blockPayload{}, nil
default:
return blockPayload{}, fmt.Errorf("%w: %d", errInvalidLicenseBlockType, blockType)
}
}
func parseIntermediatePayload(data []byte) (blockPayload, error) {
issuer, read, err := readNullString(data[46:])
if err != nil {
return blockPayload{}, err
}
return blockPayload{issuer: issuer, read: 5 + read}, nil
}
func parseServerPayload(data []byte) (blockPayload, error) {
issuer, read, err := readNullString(data[47:])
if err != nil {
return blockPayload{}, err
}
return blockPayload{
issuer: issuer,
read: 6 + read,
serverType: data[42],
}, nil
}
func parseTs5ServerPayload(data []byte, minBlockLen int) (blockPayload, error) {
propertyCount := int(data[43])
pos := 44
properties := make([][]byte, 0, propertyCount)
for range propertyCount {
if pos >= len(data) {
return blockPayload{}, errLicenseTooShort
}
propLen := int(data[pos])
pos++
if pos+propLen > len(data) {
return blockPayload{}, errLicenseTooShort
}
prop := make([]byte, propLen)
copy(prop, data[pos:pos+propLen])
properties = append(properties, prop)
pos += propLen
}
return blockPayload{
read: pos - minBlockLen,
serverType: data[42],
properties: properties,
}, nil
}
func (lb *licenseBlock) deriveKey(parent []byte) ([]byte, error) {
scalarBytes := make([]byte, 32)
copy(scalarBytes, lb.hash)
crypto.ClampScalar(scalarBytes)
sc, err := scalar.NewFromBits(scalarBytes)
if err != nil {
return nil, err
}
pub := curve.NewEdwardsPoint()
err = pub.UnmarshalBinary(lb.key)
if err != nil {
return nil, err
}
pub.Neg(pub)
par := curve.NewEdwardsPoint()
err = par.UnmarshalBinary(parent)
if err != nil {
return nil, err
}
par.Neg(par)
res := curve.NewEdwardsPoint().Mul(pub, sc)
res.Add(res, par)
final, err := res.MarshalBinary()
if err != nil {
return nil, err
}
final[31] ^= 0x80
return final, nil
}
func readNullString(data []byte) (string, int, error) {
for i, b := range data {
if b == 0 {
return string(data[:i]), i, nil
}
}
return "", 0, errIssuerStringNotTerminated
}
var unixTimeStart = time.Unix(0, 0)
@@ -0,0 +1,222 @@
package handshake_test
import (
"encoding/base64"
"encoding/binary"
"encoding/hex"
"testing"
"github.com/honeybbq/teamspeak-go/handshake"
)
// Real-world TeamSpeak anonymous license captured from a live server handshake.
const testLicenseBase64 = "AQBgjAAqtcBUrw5futTtkl3+EM3OW4Lal6OTPlwuv4xV/gIRFlEAG0Nl" +
"AAcAAAAgQW5vbnltb3VzAACWSZf+Mjl5RT5mu4rvf8nhAZp9TjXO10XfGHQ9HQPtHiAYiqjtGItRrQ=="
func decodeTestLicense(t *testing.T) []byte {
t.Helper()
data, err := base64.StdEncoding.DecodeString(testLicenseBase64)
if err != nil {
t.Fatalf("base64 decode failed: %v", err)
}
return data
}
func TestParseLicensesValid(t *testing.T) {
data := decodeTestLicense(t)
chain, err := handshake.ParseLicenses(data)
if err != nil {
t.Fatalf("ParseLicenses failed: %v", err)
}
if len(chain.Blocks) != 2 {
t.Errorf("expected 2 blocks, got %d", len(chain.Blocks))
}
}
func TestParseLicensesEmptyInput(t *testing.T) {
_, err := handshake.ParseLicenses([]byte{})
if err == nil {
t.Error("expected error for empty input")
}
}
func TestParseLicensesWrongVersion(t *testing.T) {
// Version byte at index 0 must be 1
_, err := handshake.ParseLicenses([]byte{0x02, 0x00})
if err == nil {
t.Error("expected error for unsupported version")
}
}
func TestParseLicensesTooShortBlock(t *testing.T) {
// Version OK but block data too short (< 42 bytes)
data := make([]byte, 10)
data[0] = 0x01 // valid version
// remaining 9 bytes are not enough for a license block (needs 42)
_, err := handshake.ParseLicenses(data)
if err == nil {
t.Error("expected error for truncated block")
}
}
func TestDeriveKeyLength(t *testing.T) {
data := decodeTestLicense(t)
chain, err := handshake.ParseLicenses(data)
if err != nil {
t.Fatal(err)
}
key, err := chain.DeriveKey()
if err != nil {
t.Fatalf("DeriveKey failed: %v", err)
}
if len(key) != 32 {
t.Errorf("expected 32-byte key, got %d bytes", len(key))
}
}
func TestDeriveKeyDeterministic(t *testing.T) {
data := decodeTestLicense(t)
chain, err := handshake.ParseLicenses(data)
if err != nil {
t.Fatal(err)
}
key1, err := chain.DeriveKey()
if err != nil {
t.Fatal(err)
}
key2, err := chain.DeriveKey()
if err != nil {
t.Fatal(err)
}
if hex.EncodeToString(key1) != hex.EncodeToString(key2) {
t.Error("DeriveKey is not deterministic")
}
}
func TestDeriveKeyNonZero(t *testing.T) {
data := decodeTestLicense(t)
chain, err := handshake.ParseLicenses(data)
if err != nil {
t.Fatal(err)
}
key, err := chain.DeriveKey()
if err != nil {
t.Fatal(err)
}
allZero := true
for _, b := range key {
if b != 0 {
allZero = false
break
}
}
if allZero {
t.Error("derived key should not be all zeros")
}
}
func TestParseLicensesExpectedKeyKnownValue(t *testing.T) {
// Known expected key derived from this specific anonymous license.
const expectedKeyHex = "82a168e11f9f3e3496fbf8479cd3e17d9b0945e224a71fb371af619a256b8446"
data := decodeTestLicense(t)
chain, err := handshake.ParseLicenses(data)
if err != nil {
t.Fatal(err)
}
key, err := chain.DeriveKey()
if err != nil {
t.Fatal(err)
}
if got := hex.EncodeToString(key); got != expectedKeyHex {
t.Errorf("DeriveKey = %s, want %s", got, expectedKeyHex)
}
}
// TS5/TS6 server license block (type 8) tests
// buildTs5LicenseBlob constructs a synthetic version-1 license containing a
// single Ts5Server block (type 8) with the given properties.
func buildTs5LicenseBlob(props [][]byte) []byte {
// Block layout:
// [0] key kind = 0
// [1:33] 32-byte Ed25519 public key (identity point)
// [33] block type = 8
// [34:38] not valid before (BE uint32)
// [38:42] not valid after (BE uint32)
// [42] server license type
// [43] property count
// [44+] length-prefixed properties
const headerSize = 44
totalPropsSize := 0
for _, p := range props {
totalPropsSize += 1 + len(p)
}
block := make([]byte, headerSize, headerSize+totalPropsSize)
block[0] = 0x00
block[1] = 0x01 // Ed25519 identity point (0,1)
block[33] = 0x08
binary.BigEndian.PutUint32(block[34:38], 0x00000000)
binary.BigEndian.PutUint32(block[38:42], 0x7FFFFFFF)
block[42] = 7
block[43] = byte(len(props))
for _, p := range props {
block = append(block, byte(len(p)))
block = append(block, p...)
}
return append([]byte{0x01}, block...) // version prefix
}
func TestParseTs5ServerBlock(t *testing.T) {
data := buildTs5LicenseBlob([][]byte{
[]byte("issuer.example.com"),
{0xDE, 0xAD},
})
chain, err := handshake.ParseLicenses(data)
if err != nil {
t.Fatalf("ParseLicenses failed: %v", err)
}
if len(chain.Blocks) != 1 {
t.Errorf("expected 1 block, got %d", len(chain.Blocks))
}
key, err := chain.DeriveKey()
if err != nil {
t.Fatalf("DeriveKey failed: %v", err)
}
if len(key) != 32 {
t.Errorf("expected 32-byte key, got %d", len(key))
}
}
func TestParseTs5ServerBlockZeroProperties(t *testing.T) {
data := buildTs5LicenseBlob(nil)
chain, err := handshake.ParseLicenses(data)
if err != nil {
t.Fatalf("ParseLicenses failed: %v", err)
}
if len(chain.Blocks) != 1 {
t.Errorf("expected 1 block, got %d", len(chain.Blocks))
}
}
func TestParseTs5ServerBlockTruncatedPropertyData(t *testing.T) {
data := buildTs5LicenseBlob([][]byte{{0x01, 0x02, 0x03}})
// Chop off last byte so the property data is incomplete.
data = data[:len(data)-1]
_, err := handshake.ParseLicenses(data)
if err == nil {
t.Error("expected error for truncated property data")
}
}
func TestParseTs5ServerBlockTruncatedPropertyLength(t *testing.T) {
// Claim 2 properties but only provide 1.
data := buildTs5LicenseBlob([][]byte{{0xAA}})
data[1+43] = 2 // override property count to 2
_, err := handshake.ParseLicenses(data)
if err == nil {
t.Error("expected error when property count exceeds available data")
}
}