192 lines
4.1 KiB
Go
192 lines
4.1 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 bytes 60-61 with the correct DC ID
|
||
|
|
patched := make([]byte, len(data))
|
||
|
|
copy(patched, data)
|
||
|
|
|
||
|
|
newDC := make([]byte, 2)
|
||
|
|
binary.LittleEndian.PutUint16(newDC, uint16(dc))
|
||
|
|
|
||
|
|
patched[60] = keystream[0] ^ newDC[0]
|
||
|
|
patched[61] = keystream[1] ^ newDC[1]
|
||
|
|
|
||
|
|
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
|
||
|
|
}
|