From 2bd94782e951a3f40534955591a4466041146bcf Mon Sep 17 00:00:00 2001 From: teidesu Date: Mon, 24 May 2021 20:29:18 +0300 Subject: [PATCH] feat: mtproxy support also some refactor in core transports --- packages/core/src/index.ts | 7 +- packages/core/src/network/mtproto-session.ts | 9 +- .../core/src/network/transports/abstract.ts | 3 + packages/core/src/network/transports/index.ts | 11 +- .../{tcp-intermediate.ts => intermediate.ts} | 29 +- .../{ws-obfuscated.ts => obfuscated.ts} | 62 ++-- packages/core/src/network/transports/tcp.ts | 29 +- .../core/src/network/transports/websocket.ts | 10 +- .../core/src/network/transports/wrapped.ts | 27 ++ packages/core/src/utils/crypto/abstract.ts | 3 + .../core/src/utils/crypto/forge-crypto.ts | 21 +- packages/core/src/utils/crypto/node-crypto.ts | 14 +- packages/core/tests/crypto-providers.spec.ts | 85 ++++- packages/mtproxy/fake-tls.ts | 351 ++++++++++++++++++ packages/mtproxy/index.ts | 213 +++++++++++ packages/mtproxy/package.json | 17 + packages/mtproxy/tsconfig.json | 19 + 17 files changed, 839 insertions(+), 71 deletions(-) rename packages/core/src/network/transports/{tcp-intermediate.ts => intermediate.ts} (59%) rename packages/core/src/network/transports/{ws-obfuscated.ts => obfuscated.ts} (62%) create mode 100644 packages/core/src/network/transports/wrapped.ts create mode 100644 packages/mtproxy/fake-tls.ts create mode 100644 packages/mtproxy/index.ts create mode 100644 packages/mtproxy/package.json create mode 100644 packages/mtproxy/tsconfig.json diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a71ded23..7dc1167a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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' diff --git a/packages/core/src/network/mtproto-session.ts b/packages/core/src/network/mtproto-session.ts index 9c358dca..60ee9aa6 100644 --- a/packages/core/src/network/mtproto-session.ts +++ b/packages/core/src/network/mtproto-session.ts @@ -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!, diff --git a/packages/core/src/network/transports/abstract.ts b/packages/core/src/network/transports/abstract.ts index bc8eee44..fbffb22e 100644 --- a/packages/core/src/network/transports/abstract.ts +++ b/packages/core/src/network/transports/abstract.ts @@ -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. diff --git a/packages/core/src/network/transports/index.ts b/packages/core/src/network/transports/index.ts index a1b7510b..776bfa57 100644 --- a/packages/core/src/network/transports/index.ts +++ b/packages/core/src/network/transports/index.ts @@ -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() } } diff --git a/packages/core/src/network/transports/tcp-intermediate.ts b/packages/core/src/network/transports/intermediate.ts similarity index 59% rename from packages/core/src/network/transports/tcp-intermediate.ts rename to packages/core/src/network/transports/intermediate.ts index 1610dabe..ec490ef7 100644 --- a/packages/core/src/network/transports/tcp-intermediate.ts +++ b/packages/core/src/network/transports/intermediate.ts @@ -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 + } } diff --git a/packages/core/src/network/transports/ws-obfuscated.ts b/packages/core/src/network/transports/obfuscated.ts similarity index 62% rename from packages/core/src/network/transports/ws-obfuscated.ts rename to packages/core/src/network/transports/obfuscated.ts index b2afdc89..6524812c 100644 --- a/packages/core/src/network/transports/ws-obfuscated.ts +++ b/packages/core/src/network/transports/obfuscated.ts @@ -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 { @@ -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()) -} diff --git a/packages/core/src/network/transports/tcp.ts b/packages/core/src/network/transports/tcp.ts index 1dc9babd..3fd32746 100644 --- a/packages/core/src/network/transports/tcp.ts +++ b/packages/core/src/network/transports/tcp.ts @@ -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 { @@ -101,3 +108,7 @@ export abstract class TcpTransport }) } } + +export class TcpIntermediateTransport extends TcpTransport { + _packetCodec = new IntermediatePacketCodec() +} diff --git a/packages/core/src/network/transports/websocket.ts b/packages/core/src/network/transports/websocket.ts index 7facd5dc..e94cedc9 100644 --- a/packages/core/src/network/transports/websocket.ts +++ b/packages/core/src/network/transports/websocket.ts @@ -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()) +} diff --git a/packages/core/src/network/transports/wrapped.ts b/packages/core/src/network/transports/wrapped.ts new file mode 100644 index 00000000..42ab6356 --- /dev/null +++ b/packages/core/src/network/transports/wrapped.ts @@ -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) + } +} diff --git a/packages/core/src/utils/crypto/abstract.ts b/packages/core/src/utils/crypto/abstract.ts index 2006d0bb..860044f2 100644 --- a/packages/core/src/utils/crypto/abstract.ts +++ b/packages/core/src/utils/crypto/abstract.ts @@ -27,6 +27,7 @@ export interface ICryptoProvider { iterations: number ): MaybeAsync rsaEncrypt(data: Buffer, key: TlPublicKey): MaybeAsync + hmacSha256(data: Buffer, key: Buffer): MaybeAsync // 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 + abstract hmacSha256(data: Buffer, key: Buffer): MaybeAsync + abstract createMd5(): IHashMethod } diff --git a/packages/core/src/utils/crypto/forge-crypto.ts b/packages/core/src/utils/crypto/forge-crypto.ts index cc948e22..ea918fa6 100644 --- a/packages/core/src/utils/crypto/forge-crypto.ts +++ b/packages/core/src/utils/crypto/forge-crypto.ts @@ -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 { + const hmac = forge.hmac.create() + hmac.start('sha256', key.toString('binary')) + hmac.update(data.toString('binary')) + return Buffer.from( + hmac.digest().data, + 'binary' + ) + } } diff --git a/packages/core/src/utils/crypto/node-crypto.ts b/packages/core/src/utils/crypto/node-crypto.ts index 6e43f445..d582632d 100644 --- a/packages/core/src/utils/crypto/node-crypto.ts +++ b/packages/core/src/utils/crypto/node-crypto.ts @@ -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 { + return nodeCrypto.createHmac('sha256', key).update(data).digest() + } } diff --git a/packages/core/tests/crypto-providers.spec.ts b/packages/core/tests/crypto-providers.spec.ts index ec2dd468..999b065e 100644 --- a/packages/core/tests/crypto-providers.spec.ts +++ b/packages/core/tests/crypto-providers.spec.ts @@ -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' diff --git a/packages/mtproxy/fake-tls.ts b/packages/mtproxy/fake-tls.ts new file mode 100644 index 00000000..42e9dce2 --- /dev/null +++ b/packages/mtproxy/fake-tls.ts @@ -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 { + 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 { + 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 { + 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 { + 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 + } +} diff --git a/packages/mtproxy/index.ts b/packages/mtproxy/index.ts new file mode 100644 index 00000000..31cb6e06 --- /dev/null +++ b/packages/mtproxy/index.ts @@ -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 { + try { + const hello = await generateFakeTlsHeader(this._fakeTlsDomain!, this._rawSecret, this._crypto) + const helloRand = hello.slice(11, 11 + 32) + + const checkHelloResponse = async (buf: Buffer): Promise => { + 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 => { + 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) + } + } +} diff --git a/packages/mtproxy/package.json b/packages/mtproxy/package.json new file mode 100644 index 00000000..848272ca --- /dev/null +++ b/packages/mtproxy/package.json @@ -0,0 +1,17 @@ +{ + "name": "@mtcute/mtproxy", + "private": true, + "version": "0.0.0", + "description": "MTProto proxy (MTProxy) support for MTCute", + "author": "Alisa Sireneva ", + "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" + } +} diff --git a/packages/mtproxy/tsconfig.json b/packages/mtproxy/tsconfig.json new file mode 100644 index 00000000..0d95e3cc --- /dev/null +++ b/packages/mtproxy/tsconfig.json @@ -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" + ] + } +}