tg-ws-proxy-go/internal/mtproto/mtproto.go

190 lines
4.0 KiB
Go

// Package mtproto provides MTProto protocol utilities for Telegram.
package mtproto
import (
"crypto/aes"
"crypto/cipher"
"encoding/binary"
"errors"
)
var (
// Valid protocol magic constants for MTProto obfuscation
ValidProtos = map[uint32]bool{
0xEFEFEFEF: true,
0xEEEEEEEE: true,
0xDDDDDDDD: true,
}
zero64 = make([]byte, 64)
)
// DCInfo contains extracted DC information from init packet.
type DCInfo struct {
DC int
IsMedia bool
Valid bool
Patched bool
}
// ExtractDCFromInit extracts DC ID from the 64-byte MTProto obfuscation init packet.
// Returns DCInfo with Valid=true if successful.
func ExtractDCFromInit(data []byte) DCInfo {
if len(data) < 64 {
return DCInfo{Valid: false}
}
// AES key is at [8:40], IV at [40:56]
aesKey := data[8:40]
iv := data[40:56]
// Create AES-CTR decryptor
block, err := aes.NewCipher(aesKey)
if err != nil {
return DCInfo{Valid: false}
}
stream := cipher.NewCTR(block, iv)
// Decrypt bytes [56:64] to get protocol magic and DC ID
plaintext := make([]byte, 8)
stream.XORKeyStream(plaintext, data[56:64])
// Parse protocol magic (4 bytes) and DC raw (int16)
proto := binary.LittleEndian.Uint32(plaintext[0:4])
dcRaw := int16(binary.LittleEndian.Uint16(plaintext[4:6]))
if ValidProtos[proto] {
dc := int(dcRaw)
if dc < 0 {
dc = -dc
}
if dc >= 1 && dc <= 5 || dc == 203 {
return DCInfo{
DC: dc,
IsMedia: dcRaw < 0,
Valid: true,
}
}
}
return DCInfo{Valid: false}
}
// PatchInitDC patches the dc_id in the 64-byte MTProto init packet.
// Mobile clients with useSecret=0 leave bytes 60-61 as random.
// The WS relay needs a valid dc_id to route correctly.
func PatchInitDC(data []byte, dc int) ([]byte, bool) {
if len(data) < 64 {
return data, false
}
aesKey := data[8:40]
iv := data[40:56]
block, err := aes.NewCipher(aesKey)
if err != nil {
return data, false
}
stream := cipher.NewCTR(block, iv)
// Generate keystream for bytes 56-64
keystream := make([]byte, 8)
stream.XORKeyStream(keystream, zero64[56:64])
// Patch in-place to avoid allocation
patched := make([]byte, len(data))
copy(patched, data)
// Patch bytes 60-61 directly
patched[60] = keystream[0] ^ byte(dc)
patched[61] = keystream[1] ^ byte(dc>>8)
return patched, true
}
// MsgSplitter splits client TCP data into individual MTProto messages.
// Telegram WS relay processes one MTProto message per WS frame.
type MsgSplitter struct {
aesKey []byte
iv []byte
stream cipher.Stream
}
// NewMsgSplitter creates a new message splitter from init data.
func NewMsgSplitter(initData []byte) (*MsgSplitter, error) {
if len(initData) < 64 {
return nil, errors.New("init data too short")
}
aesKey := initData[8:40]
iv := initData[40:56]
block, err := aes.NewCipher(aesKey)
if err != nil {
return nil, err
}
stream := cipher.NewCTR(block, iv)
// Skip init packet (64 bytes of keystream)
stream.XORKeyStream(make([]byte, 64), zero64[:64])
return &MsgSplitter{
aesKey: aesKey,
iv: iv,
stream: stream,
}, nil
}
// Split decrypts chunk and finds message boundaries.
// Returns split ciphertext parts.
func (s *MsgSplitter) Split(chunk []byte) [][]byte {
// Decrypt to find boundaries
plaintext := make([]byte, len(chunk))
s.stream.XORKeyStream(plaintext, chunk)
boundaries := []int{}
pos := 0
plainLen := len(plaintext)
for pos < plainLen {
first := plaintext[pos]
var msgLen int
if first == 0x7f {
if pos+4 > plainLen {
break
}
// Read 3 bytes starting from pos+1 (skip the 0x7f byte)
msgLen = int(binary.LittleEndian.Uint32(append(plaintext[pos+1:pos+4], 0))) & 0xFFFFFF
msgLen *= 4
pos += 4
} else {
msgLen = int(first) * 4
pos += 1
}
if msgLen == 0 || pos+msgLen > plainLen {
break
}
pos += msgLen
boundaries = append(boundaries, pos)
}
if len(boundaries) <= 1 {
return [][]byte{chunk}
}
parts := make([][]byte, 0, len(boundaries)+1)
prev := 0
for _, b := range boundaries {
parts = append(parts, chunk[prev:b])
prev = b
}
if prev < len(chunk) {
parts = append(parts, chunk[prev:])
}
return parts
}