Files
go-ts/pkg/protocol/crypto.go

183 lines
5.3 KiB
Go
Raw Permalink Normal View History

2026-01-15 16:49:16 +01:00
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
}