feat: mtproxy support

also some refactor in core transports
This commit is contained in:
teidesu 2021-05-24 20:29:18 +03:00
parent 76f078d931
commit 2bd94782e9
17 changed files with 839 additions and 71 deletions

View file

@ -9,7 +9,12 @@ export * from './utils/tl-json'
export * from './utils/async-lock'
export * from './utils/lru-map'
export * from './utils/function-utils'
export { encodeUrlSafeBase64, parseUrlSafeBase64 } from './utils/buffer-utils'
export {
encodeUrlSafeBase64,
parseUrlSafeBase64,
randomBytes,
} from './utils/buffer-utils'
export * from './utils/bigint-utils'
export { BinaryReader } from './utils/binary/binary-reader'
export { BinaryWriter } from './utils/binary/binary-writer'

View file

@ -112,7 +112,7 @@ export class MtprotoSession {
const authKeyId = reader.raw(8)
const messageKey = reader.int128()
const encryptedData = reader.raw()
let encryptedData = reader.raw()
if (!buffersEqual(authKeyId, this._authKeyId!)) {
debug(
@ -124,6 +124,13 @@ export class MtprotoSession {
return null
}
const padSize = encryptedData.length % 16
if (padSize !== 0) {
// data came from a codec that uses non-16-based padding.
// it is safe to drop those padding bytes
encryptedData = encryptedData.slice(0, -padSize)
}
const ige = await createAesIgeForMessage(
this._crypto,
this._authKey!,

View file

@ -84,6 +84,9 @@ export interface PacketCodec {
/** Reset codec state (for example, reset buffer) */
reset(): void
/** Remove all listeners from a codec, used to safely dispose it */
removeAllListeners(): void
/**
* Emitted when a packet containing a
* (Transport error)[https://core.telegram.org/mtproto/mtproto-transports#transport-errors] is encountered.

View file

@ -2,16 +2,17 @@ import { TransportFactory } from './abstract'
export * from './abstract'
export * from './streamed'
export * from './wrapped'
export * from './tcp'
export * from './tcp-intermediate'
export * from './websocket'
export * from './ws-obfuscated'
export * from './intermediate'
export * from './obfuscated'
/** Platform-defined default transport factory */
export let defaultTransportFactory: TransportFactory
if (typeof process !== 'undefined') {
// we are in node, use tcp transport by default
const { TcpIntermediateTransport } = require('./tcp-intermediate')
const { TcpIntermediateTransport } = require('./tcp')
defaultTransportFactory = () => new TcpIntermediateTransport()
} else {
// we are in browser (probably), use websocket
@ -24,7 +25,7 @@ if (typeof process !== 'undefined') {
)
}
} else {
const { WebSocketObfuscatedTransport } = require('./ws-obfuscated')
defaultTransportFactory = () => new WebSocketObfuscatedTransport()
const { WebSocketIntermediateTransport } = require('./websocket')
defaultTransportFactory = () => new WebSocketIntermediateTransport()
}
}

View file

@ -1,8 +1,9 @@
import { PacketCodec, TransportError } from './abstract'
import { StreamedCodec } from './streamed'
import { TcpTransport } from './tcp'
import { randomBytes } from '../../utils/buffer-utils'
const TAG = Buffer.from([0xee, 0xee, 0xee, 0xee])
const PADDED_TAG = Buffer.from([0xdd, 0xdd, 0xdd, 0xdd])
/**
* Intermediate packet codec.
@ -30,13 +31,11 @@ export class IntermediatePacketCodec
const payloadLength = this._stream.readUInt32LE(0)
if (payloadLength <= this._stream.length - 4) {
const payload = this._stream.slice(4, payloadLength + 4)
if (payloadLength === 4) {
const code = this._stream.readInt32LE(4) * -1
this.emit('error', new TransportError(code))
} else {
const payload = this._stream.slice(4, payloadLength + 4)
this.emit('packet', payload)
}
@ -48,6 +47,24 @@ export class IntermediatePacketCodec
}
}
export class TcpIntermediateTransport extends TcpTransport {
_packetCodec = new IntermediatePacketCodec()
/**
* Padded intermediate packet codec.
* See https://core.telegram.org/mtproto/mtproto-transports#padded-intermediate
*/
export class PaddedIntermediatePacketCodec extends IntermediatePacketCodec {
tag(): Buffer {
return PADDED_TAG
}
encode(packet: Buffer): Buffer {
// padding size, 0-15
const padSize = Math.floor(Math.random() * 16)
const padding = randomBytes(padSize)
const ret = Buffer.alloc(packet.length + 4 + padSize)
ret.writeUInt32LE(packet.length + padSize)
packet.copy(ret, 4)
padding.copy(ret, 4 + ret.length)
return ret
}
}

View file

@ -1,12 +1,8 @@
import { PacketCodec } from './abstract'
import { ICryptoProvider, IEncryptionScheme } from '../../utils/crypto'
import { EventEmitter } from 'events'
import {
buffersEqual,
randomBytes,
} from '../../utils/buffer-utils'
import { WebSocketTransport } from './websocket'
import { IntermediatePacketCodec } from './tcp-intermediate'
import { buffersEqual, randomBytes } from '../../utils/buffer-utils'
import { WrappedCodec } from './wrapped'
// initial payload can't start with these
const BAD_HEADERS = [
@ -17,21 +13,22 @@ const BAD_HEADERS = [
Buffer.from('eeeeeeee', 'hex'),
]
export class ObfuscatedPacketCodec extends EventEmitter implements PacketCodec {
private _inner: PacketCodec
private _crypto: ICryptoProvider
interface MtProxyInfo {
dcId: number
secret: Buffer
test: boolean
media: boolean
}
export class ObfuscatedPacketCodec extends WrappedCodec implements PacketCodec {
private _encryptor?: IEncryptionScheme
private _decryptor?: IEncryptionScheme
constructor(inner: PacketCodec) {
super()
this._inner = inner
this._inner.on('error', (err) => this.emit('error', err))
this._inner.on('packet', (buf) => this.emit('packet', buf))
}
private _proxy?: MtProxyInfo
setupCrypto(crypto: ICryptoProvider): void {
this._crypto = crypto
constructor(inner: PacketCodec, proxy?: MtProxyInfo) {
super(inner)
this._proxy = proxy
}
async tag(): Promise<Buffer> {
@ -54,16 +51,37 @@ export class ObfuscatedPacketCodec extends EventEmitter implements PacketCodec {
}
innerTag.copy(random, 56)
if (this._proxy) {
let dcId = this._proxy.dcId
if (this._proxy.test) dcId += 10000
if (this._proxy.media) dcId = -dcId
random.writeInt16LE(dcId, 60)
}
const randomRev = Buffer.from(random.slice(8, 56)).reverse()
const encryptKey = random.slice(8, 40)
let encryptKey = random.slice(8, 40)
const encryptIv = random.slice(40, 56)
const decryptKey = randomRev.slice(0, 32)
let decryptKey = randomRev.slice(0, 32)
const decryptIv = randomRev.slice(32, 48)
if (this._proxy) {
encryptKey = await this._crypto.sha256(
Buffer.concat([encryptKey, this._proxy.secret])
)
decryptKey = await this._crypto.sha256(
Buffer.concat([decryptKey, this._proxy.secret])
)
}
this._encryptor = this._crypto.createAesCtr(encryptKey, encryptIv, true)
this._decryptor = this._crypto.createAesCtr(decryptKey, decryptIv, false)
this._decryptor = this._crypto.createAesCtr(
decryptKey,
decryptIv,
false
)
const encrypted = await this._encryptor.encrypt(random)
encrypted.copy(random, 56, 56, 64)
@ -87,7 +105,3 @@ export class ObfuscatedPacketCodec extends EventEmitter implements PacketCodec {
delete this._decryptor
}
}
export class WebSocketObfuscatedTransport extends WebSocketTransport {
_packetCodec = new ObfuscatedPacketCodec(new IntermediatePacketCodec())
}

View file

@ -3,6 +3,7 @@ import { tl } from '@mtcute/tl'
import { Socket, connect } from 'net'
import EventEmitter from 'events'
import { ICryptoProvider } from '../../utils/crypto'
import { IntermediatePacketCodec } from './intermediate'
const debug = require('debug')('mtcute:tcp')
@ -39,6 +40,7 @@ export abstract class TcpTransport
throw new Error('Transport is not IDLE')
if (!this.packetCodecInitialized) {
this._packetCodec.setupCrypto?.(this._crypto)
this._packetCodec.on('error', (err) => this.emit('error', err))
this._packetCodec.on('packet', (buf) => this.emit('message', buf))
this.packetCodecInitialized = true
@ -79,15 +81,20 @@ export abstract class TcpTransport
debug('%s: connected', this._currentDc!.ipAddress)
const initialMessage = await this._packetCodec.tag()
this._socket!.write(initialMessage, (err) => {
if (err) {
this.emit('error', err)
this.close()
} else {
this._state = TransportState.Ready
this.emit('ready')
}
})
if (initialMessage.length) {
this._socket!.write(initialMessage, (err) => {
if (err) {
this.emit('error', err)
this.close()
} else {
this._state = TransportState.Ready
this.emit('ready')
}
})
} else {
this._state = TransportState.Ready
this.emit('ready')
}
}
async send(bytes: Buffer): Promise<void> {
@ -101,3 +108,7 @@ export abstract class TcpTransport
})
}
}
export class TcpIntermediateTransport extends TcpTransport {
_packetCodec = new IntermediatePacketCodec()
}

View file

@ -4,6 +4,8 @@ import EventEmitter from 'events'
import { typedArrayToBuffer } from '../../utils/buffer-utils'
import { ICryptoProvider } from '../../utils/crypto'
import type WebSocket from 'ws'
import { IntermediatePacketCodec } from './intermediate'
import { ObfuscatedPacketCodec } from './obfuscated'
const debug = require('debug')('mtcute:ws')
@ -41,7 +43,7 @@ export abstract class WebSocketTransport
private _currentDc: tl.RawDcOption | null = null
private _state: TransportState = TransportState.Idle
private _socket: WebSocket | null = null
private _crypto?: ICryptoProvider
private _crypto: ICryptoProvider
abstract _packetCodec: PacketCodec
packetCodecInitialized = false
@ -91,7 +93,7 @@ export abstract class WebSocketTransport
throw new Error('Transport is not IDLE')
if (!this.packetCodecInitialized) {
if (this._crypto) this._packetCodec.setupCrypto?.(this._crypto)
this._packetCodec.setupCrypto?.(this._crypto)
this._packetCodec.on('error', (err) => this.emit('error', err))
this._packetCodec.on('packet', (buf) => this.emit('message', buf))
this.packetCodecInitialized = true
@ -149,3 +151,7 @@ export abstract class WebSocketTransport
this._socket!.send(framed)
}
}
export class WebSocketIntermediateTransport extends WebSocketTransport {
_packetCodec = new ObfuscatedPacketCodec(new IntermediatePacketCodec())
}

View file

@ -0,0 +1,27 @@
import { EventEmitter } from 'events'
import { PacketCodec } from './abstract'
import { ICryptoProvider } from '../../utils/crypto'
export abstract class WrappedCodec extends EventEmitter {
protected _crypto: ICryptoProvider
protected _inner: PacketCodec
constructor (inner: PacketCodec) {
super()
this._inner = inner
this._inner.on('error', (err) => this.emit('error', err))
this._inner.on('packet', (buf) => this.emit('packet', buf))
}
removeAllListeners(): this {
super.removeAllListeners()
this._inner.removeAllListeners()
return this
}
setupCrypto(crypto: ICryptoProvider): void {
this._crypto = crypto
this._inner.setupCrypto?.(crypto)
}
}

View file

@ -27,6 +27,7 @@ export interface ICryptoProvider {
iterations: number
): MaybeAsync<Buffer>
rsaEncrypt(data: Buffer, key: TlPublicKey): MaybeAsync<Buffer>
hmacSha256(data: Buffer, key: Buffer): MaybeAsync<Buffer>
// in telegram, iv is always either used only once, or is the same for all calls for the key
createAesCtr(key: Buffer, iv: Buffer, encrypt: boolean): IEncryptionScheme
@ -79,6 +80,8 @@ export abstract class BaseCryptoProvider implements ICryptoProvider {
abstract sha256(data: Buffer): MaybeAsync<Buffer>
abstract hmacSha256(data: Buffer, key: Buffer): MaybeAsync<Buffer>
abstract createMd5(): IHashMethod
}

View file

@ -16,7 +16,9 @@ export class ForgeCryptoProvider extends BaseCryptoProvider {
}
createAesCtr(key: Buffer, iv: Buffer, encrypt: boolean): IEncryptionScheme {
const cipher = forge.cipher[encrypt ? 'createCipher' : 'createDecipher']('AES-CTR', key.toString('binary'))
const cipher = forge.cipher[
encrypt ? 'createCipher' : 'createDecipher'
]('AES-CTR', key.toString('binary'))
cipher.start({ iv: iv.toString('binary') })
const update = (data: Buffer): Buffer => {
@ -27,7 +29,7 @@ export class ForgeCryptoProvider extends BaseCryptoProvider {
return {
encrypt: update,
decrypt: update
decrypt: update,
}
}
@ -44,10 +46,7 @@ export class ForgeCryptoProvider extends BaseCryptoProvider {
return Buffer.from(cipher.output.data, 'binary')
},
decrypt(data: Buffer) {
const cipher = forge.cipher.createDecipher(
'AES-ECB',
keyBuffer
)
const cipher = forge.cipher.createDecipher('AES-ECB', keyBuffer)
cipher.start({})
cipher.mode.pad = cipher.mode.unpad = false
cipher.update(forge.util.createBuffer(data.toString('binary')))
@ -100,4 +99,14 @@ export class ForgeCryptoProvider extends BaseCryptoProvider {
digest: () => Buffer.from(hash.digest().data, 'binary'),
}
}
hmacSha256(data: Buffer, key: Buffer): MaybeAsync<Buffer> {
const hmac = forge.hmac.create()
hmac.start('sha256', key.toString('binary'))
hmac.update(data.toString('binary'))
return Buffer.from(
hmac.digest().data,
'binary'
)
}
}

View file

@ -10,7 +10,9 @@ export class NodeCryptoProvider extends BaseCryptoProvider {
}
createAesCtr(key: Buffer, iv: Buffer, encrypt: boolean): IEncryptionScheme {
const cipher = nodeCrypto[encrypt ? 'createCipheriv' : 'createDecipheriv'](`aes-${key.length * 8}-ctr`, key, iv)
const cipher = nodeCrypto[
encrypt ? 'createCipheriv' : 'createDecipheriv'
](`aes-${key.length * 8}-ctr`, key, iv)
const update = (data: Buffer) => cipher.update(data)
@ -30,7 +32,11 @@ export class NodeCryptoProvider extends BaseCryptoProvider {
return Buffer.concat([cipher.update(data), cipher.final()])
},
decrypt(data: Buffer) {
const cipher = nodeCrypto.createDecipheriv(methodName, key, null)
const cipher = nodeCrypto.createDecipheriv(
methodName,
key,
null
)
cipher.setAutoPadding(false)
return Buffer.concat([cipher.update(data), cipher.final()])
},
@ -66,4 +72,8 @@ export class NodeCryptoProvider extends BaseCryptoProvider {
createMd5(): IHashMethod {
return nodeCrypto.createHash('md5')
}
hmacSha256(data: Buffer, key: Buffer): MaybeAsync<Buffer> {
return nodeCrypto.createHmac('sha256', key).update(data).digest()
}
}

View file

@ -31,6 +31,28 @@ export function testCryptoProvider(c: ICryptoProvider): void {
)
})
it('should calculate hmac-sha256', async () => {
const key = Buffer.from('aaeeff', 'hex')
expect(
(await c.hmacSha256(Buffer.from(''), key)).toString('hex')
).to.eq(
'642711307c9e4437df09d6ebaa6bdc1b3a810c7f15c50fd1d0f8d7d5490f44dd'
)
expect(
(await c.hmacSha256(Buffer.from('hello'), key)).toString('hex')
).to.eq(
'39b00bab151f9868e6501655c580b5542954711181243474d46b894703b1c1c2'
)
expect(
(await c.hmacSha256(Buffer.from('aebb1f', 'hex'), key)).toString(
'hex'
)
).to.eq(
'a3a7273871808711cab17aba14f58e96f63f3ccfc5097d206f0f00ead2c3dd35'
)
})
it('should derive pbkdf2 key', async () => {
expect(
(
@ -47,35 +69,68 @@ export function testCryptoProvider(c: ICryptoProvider): void {
it('should encrypt and decrypt aes-ctr', async () => {
let aes = c.createAesCtr(
Buffer.from('d450aae0bf0060a4af1044886b42a13f7c506b35255d134a7e87ab3f23a9493b', 'hex'),
Buffer.from(
'd450aae0bf0060a4af1044886b42a13f7c506b35255d134a7e87ab3f23a9493b',
'hex'
),
Buffer.from('0182de2bd789c295c3c6c875c5e9e190', 'hex'),
true
)
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq('a5fea1')
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq('ab51ca')
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq('365e5c')
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq('4b94a9')
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq('776387')
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq('c940be')
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq(
'a5fea1'
)
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq(
'ab51ca'
)
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq(
'365e5c'
)
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq(
'4b94a9'
)
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq(
'776387'
)
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq(
'c940be'
)
aes = c.createAesCtr(
Buffer.from('d450aae0bf0060a4af1044886b42a13f7c506b35255d134a7e87ab3f23a9493b', 'hex'),
Buffer.from(
'd450aae0bf0060a4af1044886b42a13f7c506b35255d134a7e87ab3f23a9493b',
'hex'
),
Buffer.from('0182de2bd789c295c3c6c875c5e9e190', 'hex'),
false
)
expect((await aes.decrypt(Buffer.from('a5fea1', 'hex'))).toString('hex')).eq('010203')
expect((await aes.decrypt(Buffer.from('ab51ca', 'hex'))).toString('hex')).eq('010203')
expect((await aes.decrypt(Buffer.from('365e5c', 'hex'))).toString('hex')).eq('010203')
expect((await aes.decrypt(Buffer.from('4b94a9', 'hex'))).toString('hex')).eq('010203')
expect((await aes.decrypt(Buffer.from('776387', 'hex'))).toString('hex')).eq('010203')
expect((await aes.decrypt(Buffer.from('c940be', 'hex'))).toString('hex')).eq('010203')
expect(
(await aes.decrypt(Buffer.from('a5fea1', 'hex'))).toString('hex')
).eq('010203')
expect(
(await aes.decrypt(Buffer.from('ab51ca', 'hex'))).toString('hex')
).eq('010203')
expect(
(await aes.decrypt(Buffer.from('365e5c', 'hex'))).toString('hex')
).eq('010203')
expect(
(await aes.decrypt(Buffer.from('4b94a9', 'hex'))).toString('hex')
).eq('010203')
expect(
(await aes.decrypt(Buffer.from('776387', 'hex'))).toString('hex')
).eq('010203')
expect(
(await aes.decrypt(Buffer.from('c940be', 'hex'))).toString('hex')
).eq('010203')
})
it('should encrypt and decrypt aes-ige', async () => {
const aes = c.createAesIge(
Buffer.from('5468697320697320616E20696D706C655468697320697320616E20696D706C65', 'hex'),
Buffer.from(
'5468697320697320616E20696D706C655468697320697320616E20696D706C65',
'hex'
),
Buffer.from(
'6D656E746174696F6E206F6620494745206D6F646520666F72204F70656E5353',
'hex'

View file

@ -0,0 +1,351 @@
import {
bigIntToBuffer,
bufferToBigInt,
ICryptoProvider,
PacketCodec,
WrappedCodec,
randomBytes
} from '@mtcute/core'
import bigInt, { BigInteger } from 'big-integer'
const MAX_TLS_PACKET_LENGTH = 2878
const TLS_FIRST_PREFIX = Buffer.from('140303000101', 'hex')
// ref: https://github.com/tdlib/td/blob/master/td/mtproto/TlsInit.cpp
const KEY_MOD = bigInt(
'7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed',
16
)
// 2^255 - 19
const QUAD_RES_MOD = bigInt('7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed', 16)
// (mod - 1) / 2 = 2^254 - 10
const QUAD_RES_POW = bigInt('3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6', 16)
function _getY2(x: BigInteger, mod: BigInteger): BigInteger {
// returns y = x^3 + x^2 * 486662 + x
let y = x
y = y.add(486662).mod(mod)
y = y.multiply(x).mod(mod)
y = y.plus(1).mod(mod)
y = y.multiply(x).mod(mod)
return y
}
function _getDoubleX(x: BigInteger, mod: BigInteger): BigInteger {
// returns x_2 = (x^2 - 1)^2/(4*y^2)
let denominator = _getY2(x, mod)
denominator = denominator.multiply(4).mod(mod)
let numerator = x.multiply(x).mod(mod)
numerator = numerator.minus(1).mod(mod)
numerator = numerator.multiply(numerator).mod(mod)
denominator = denominator.modInv(mod)
numerator = numerator.multiply(denominator).mod(mod)
return numerator
}
function _isQuadraticResidue(a: BigInteger): boolean {
const r = a.modPow(QUAD_RES_POW, QUAD_RES_MOD)
return r.eq(1)
}
interface TlsOperationHandler {
string(buf: Buffer): void
zero(size: number): void
random(size: number): void
domain(): void
grease(seed: number): void
beginScope(): void
endScope(): void
key(): void
}
function executeTlsOperations(h: TlsOperationHandler): void {
h.string(Buffer.from('1603010200010001fc0303', 'hex'))
h.zero(32)
h.string(Buffer.from('20', 'hex'))
h.random(32)
h.string(Buffer.from('0020', 'hex'))
h.grease(0)
h.string(
Buffer.from(
'130113021303c02bc02fc02cc030cca9cca8c013c014009c009d002f003501000193',
'hex'
)
)
h.grease(2)
h.string(Buffer.from('00000000', 'hex'))
h.beginScope()
h.beginScope()
h.string(Buffer.from('00', 'hex'))
h.beginScope()
h.domain()
h.endScope()
h.endScope()
h.endScope()
h.string(Buffer.from('00170000ff01000100000a000a0008', 'hex'))
h.grease(4)
h.string(
Buffer.from(
'001d00170018000b00020100002300000010000e000c02683208687474702f312e31000500050100000000000d0012001004030804040105030805050108060601001200000033002b0029',
'hex'
)
)
h.grease(4)
h.string(Buffer.from('000100001d0020', 'hex'))
h.key()
h.string(Buffer.from('002d00020101002b000b0a', 'hex'))
h.grease(6)
h.string(Buffer.from('0304030303020301001b0003020002', 'hex'))
h.grease(3)
h.string(Buffer.from('0001000015', 'hex'))
}
// i dont know why is this needed, since it is always padded to 517 bytes
// this was in tdlib sources, so whatever. not used here though, and works just fine
// class TlsHelloCounter implements TlsOperationHandler {
// size = 0
//
// private _domain: Buffer
//
// constructor(domain: Buffer) {
// this._domain = domain
// }
//
// string(buf: Buffer) {
// this.size += buf.length
// }
//
// random(size: number) {
// this.size += size
// }
//
// zero(size: number) {
// this.size += size
// }
//
// domain() {
// this.size += this._domain.length
// }
//
// grease() {
// this.size += 2
// }
//
// key() {
// this.size += 32
// }
//
// beginScope() {
// this.size += 2
// }
//
// endScope() {
// // no-op, since this does not affect size
// }
//
// finish(): number {
// const zeroPad = 515 - this.size
// this.beginScope()
// this.zero(zeroPad)
// this.endScope()
//
// return this.size
// }
// }
function initGrease(size: number): Buffer {
const buf = randomBytes(size)
for (let i = 0; i < size; i++) {
buf[i] = (buf[i] & 0xf0) + 0x0a
}
for (let i = 1; i < size; i += 2) {
if (buf[i] === buf[i - 1]) {
buf[i] ^= 0x10
}
}
return buf
}
class TlsHelloWriter implements TlsOperationHandler {
buf: Buffer
pos = 0
private _domain: Buffer
private _grease = initGrease(7)
private _scopes: number[] = []
constructor(size: number, domain: Buffer) {
this._domain = domain
this.buf = Buffer.allocUnsafe(size)
}
string(buf: Buffer) {
buf.copy(this.buf, this.pos)
this.pos += buf.length
}
random(size: number) {
this.string(randomBytes(size))
}
zero(size: number) {
this.string(Buffer.alloc(size, 0))
}
domain() {
this.string(this._domain)
}
grease(seed: number) {
this.buf[this.pos] = this.buf[this.pos + 1] = this._grease[seed]
this.pos += 2
}
key() {
for (;;) {
const key = randomBytes(32)
key[31] &= 127
let x = bufferToBigInt(key)
const y = _getY2(x, KEY_MOD)
if (_isQuadraticResidue(y)) {
for (let i = 0; i < 3; i++) {
x = _getDoubleX(x, KEY_MOD)
}
const key = bigIntToBuffer(x, 32, true)
this.string(key)
return
}
}
}
beginScope() {
this._scopes.push(this.pos)
this.pos += 2
}
endScope() {
const begin = this._scopes.pop()!
const end = this.pos
const size = end - begin - 2
this.buf.writeUInt16BE(size, begin)
}
async finish(secret: Buffer, crypto: ICryptoProvider): Promise<Buffer> {
const padSize = 515 - this.pos
const unixTime = ~~(Date.now() / 1000)
this.beginScope()
this.zero(padSize)
this.endScope()
const hash = await crypto.hmacSha256(this.buf, secret)
const old = hash.readInt32LE(28)
hash.writeInt32LE(old ^ unixTime, 28)
hash.copy(this.buf, 11)
return this.buf
}
}
/** @internal */
export async function generateFakeTlsHeader(domain: string, secret: Buffer, crypto: ICryptoProvider): Promise<Buffer> {
const domainBuf = Buffer.from(domain)
const writer = new TlsHelloWriter(517, domainBuf)
executeTlsOperations(writer)
return writer.finish(secret, crypto)
}
/**
* Fake TLS packet codec, used for some MTProxies.
*
* Must only be used inside {@link MtProxyTcpTransport}
* @internal
*/
export class FakeTlsPacketCodec extends WrappedCodec implements PacketCodec {
protected _stream = Buffer.alloc(0)
private _header: Buffer
private _isFirstTls = true
async tag(): Promise<Buffer> {
this._header = await this._inner.tag()
return Buffer.alloc(0)
}
private _encodeTls(packet: Buffer): Buffer {
if (this._header.length) {
packet = Buffer.concat([this._header, packet])
this._header = Buffer.alloc(0)
}
const header = Buffer.from([0x17, 0x03, 0x03, 0x00, 0x00])
header.writeUInt16BE(packet.length, 3)
if (this._isFirstTls) {
this._isFirstTls = false
return Buffer.concat([TLS_FIRST_PREFIX, header, packet])
} else {
return Buffer.concat([header, packet])
}
}
async encode(packet: Buffer): Promise<Buffer> {
packet = await this._inner.encode(packet)
if (packet.length + this._header.length > MAX_TLS_PACKET_LENGTH) {
const ret: Buffer[] = []
while (packet.length) {
const buf = packet.slice(0, MAX_TLS_PACKET_LENGTH - this._header.length)
packet = packet.slice(buf.length)
ret.push(this._encodeTls(buf))
}
return Buffer.concat(ret)
}
return this._encodeTls(packet)
}
feed(data: Buffer): void {
this._stream = Buffer.concat([this._stream, data])
for (;;) {
if (this._stream.length < 5) return
if (
!(
this._stream[0] === 0x17 &&
this._stream[1] === 0x03 &&
this._stream[2] === 0x03
)
) {
this.emit('error', new Error('Invalid TLS header'))
return
}
const length = this._stream.readUInt16BE(3)
if (length < this._stream.length - 5) return
this._inner.feed(this._stream.slice(5, length + 5))
this._stream = this._stream.slice(length + 5)
}
}
reset(): void {
this._stream = Buffer.alloc(0)
this._isFirstTls = true
}
}

213
packages/mtproxy/index.ts Normal file
View file

@ -0,0 +1,213 @@
import {
IntermediatePacketCodec,
ObfuscatedPacketCodec,
PacketCodec,
PaddedIntermediatePacketCodec,
parseUrlSafeBase64,
TcpTransport,
TransportState,
} from '@mtcute/core'
import { tl } from '@mtcute/tl'
import { connect } from 'net'
import { FakeTlsPacketCodec, generateFakeTlsHeader } from './fake-tls'
export interface MtProxySettings {
/**
* Host or IP of the proxy (e.g. `proxy.example.com`, `1.2.3.4`)
*/
host: string
/**
* Port of the proxy (e.g. `8888`)
*/
port: number
/**
* Secret of the proxy, optionally encoded either as hex or base64
*/
secret: string | Buffer
}
const MAX_DOMAIN_LENGTH = 182 // must be small enough not to overflow TLS-hello length
const TLS_START = [
Buffer.from('160303', 'hex'),
Buffer.from('140303000101170303', 'hex'),
]
/**
* TCP transport that connects via a SOCKS4/5 proxy.
*/
export class MtProxyTcpTransport extends TcpTransport {
readonly _proxy: MtProxySettings
private _rawSecret: Buffer
private _randomPadding = false
private _fakeTlsDomain: string | null = null
private _test: boolean
/**
* @param proxy Information about the proxy
* @param test Whether test servers should be used
*/
constructor(proxy: MtProxySettings, test = false) {
super()
this._test = test
this._proxy = proxy
// validate and parse secret
let secret: Buffer
if (Buffer.isBuffer(proxy.secret)) {
secret = proxy.secret
} else if (proxy.secret.match(/^[0-9a-f]+$/i)) {
secret = Buffer.from(proxy.secret, 'hex')
} else {
secret = parseUrlSafeBase64(proxy.secret)
}
if (secret.length > 17 + MAX_DOMAIN_LENGTH) {
throw new Error('Invalid secret: too long')
}
if (secret.length < 16) {
throw new Error('Invalid secret: too short')
}
if (secret.length === 16) {
this._rawSecret = secret
} else if (secret.length === 17 && secret[0] === 0xdd) {
this._rawSecret = secret.slice(1)
this._randomPadding = true
} else if (secret.length >= 18 && secret[0] === 0xee) {
this._rawSecret = secret.slice(1, 17)
this._fakeTlsDomain = secret.slice(17).toString()
} else {
throw new Error('Unsupported secret')
}
}
_packetCodec!: PacketCodec
connect(dc: tl.RawDcOption): void {
if (this._state !== TransportState.Idle)
throw new Error('Transport is not IDLE')
if (this._packetCodec && this._currentDc?.id !== dc.id) {
// dc changed, thus the codec's init will change too
// clean up to avoid memory leaks
this.packetCodecInitialized = false
this._packetCodec.reset()
this._packetCodec.removeAllListeners()
delete (this as any)._packetCodec
}
if (!this._packetCodec) {
const proxy = {
dcId: dc.id,
media: dc.mediaOnly!,
test: this._test,
secret: this._rawSecret,
}
if (!this._fakeTlsDomain) {
let inner: PacketCodec
if (this._randomPadding) {
inner = new PaddedIntermediatePacketCodec()
} else {
inner = new IntermediatePacketCodec()
}
this._packetCodec = new ObfuscatedPacketCodec(inner, proxy)
} else {
this._packetCodec = new FakeTlsPacketCodec(
new ObfuscatedPacketCodec(
new PaddedIntermediatePacketCodec(),
proxy
)
)
}
this._packetCodec.setupCrypto?.(this._crypto)
this._packetCodec.on('error', (err) => this.emit('error', err))
this._packetCodec.on('packet', (buf) => this.emit('message', buf))
}
this._state = TransportState.Connecting
this._currentDc = dc
if (this._fakeTlsDomain) {
this._socket = connect(
this._proxy.port,
this._proxy.host,
this._handleConnectFakeTls.bind(this)
)
} else {
this._socket = connect(
this._proxy.port,
this._proxy.host,
this.handleConnect.bind(this)
)
this._socket.on('data', (data) => this._packetCodec.feed(data))
}
this._socket.on('error', this.handleError.bind(this))
this._socket.on('close', this.close.bind(this))
}
private async _handleConnectFakeTls(): Promise<void> {
try {
const hello = await generateFakeTlsHeader(this._fakeTlsDomain!, this._rawSecret, this._crypto)
const helloRand = hello.slice(11, 11 + 32)
const checkHelloResponse = async (buf: Buffer): Promise<void> => {
const resp = buf
for (const first of TLS_START) {
if (buf.length < first.length + 2) {
return
}
if (first.compare(first, 0, first.length) !== 0) {
throw new Error('First part of hello response is invalid')
}
buf = buf.slice(first.length)
const skipSize = buf.readUInt16BE()
buf = buf.slice(2)
if (buf.length < skipSize) {
return
}
buf = buf.slice(skipSize)
}
const respRand = resp.slice(11, 11 + 32)
const hash = await this._crypto.hmacSha256(Buffer.concat([
helloRand,
resp.slice(0, 11),
Buffer.alloc(32, 0),
resp.slice(11 + 32)
]), this._rawSecret)
if (hash.compare(respRand) !== 0) {
throw new Error('Response hash is invalid')
}
}
const packetHandler = async (buf: Buffer): Promise<void> => {
try {
await checkHelloResponse(buf)
this._socket!.on('data', (data) => this._packetCodec.feed(data))
this._socket!.off('data', packetHandler)
this.handleConnect()
} catch (e) {
this._socket!.emit('error', e)
}
}
this._socket!.write(hello)
this._socket!.on('data', packetHandler)
} catch (e) {
this._socket!.emit('error', e)
}
}
}

View file

@ -0,0 +1,17 @@
{
"name": "@mtcute/mtproxy",
"private": true,
"version": "0.0.0",
"description": "MTProto proxy (MTProxy) support for MTCute",
"author": "Alisa Sireneva <me@tei.su>",
"license": "MIT",
"main": "index.ts",
"scripts": {
"test": "mocha -r ts-node/register tests/**/*.spec.ts",
"docs": "npx typedoc",
"build": "tsc"
},
"dependencies": {
"@mtcute/core": "^0.0.0"
}
}

View file

@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist"
},
"include": [
"./index.ts",
],
"typedocOptions": {
"name": "@mtcute/mtproxy",
"includeVersion": true,
"out": "../../docs/packages/mtproxy",
"listInvalidSymbolLinks": true,
"excludePrivate": true,
"entryPoints": [
"./index.ts"
]
}
}