/* eslint-disable no-restricted-globals */ import type { IPacketCodec } from '@mtcute/node' import { WrappedCodec } from '@mtcute/node' import type { ICryptoProvider } from '@mtcute/node/utils.js' import { bigIntModInv, bigIntModPow, bigIntToBuffer, bufferToBigInt } from '@mtcute/node/utils.js' 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 = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEDn // 2^255 - 19 const QUAD_RES_MOD = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEDn // (mod - 1) / 2 = 2^254 - 10 const QUAD_RES_POW = 0x3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6n function _getY2(x: bigint, mod: bigint): bigint { // returns y = x^3 + x^2 * 486662 + x let y = x y = (y + 486662n) % mod y = (y * x) % mod y = (y + 1n) % mod y = (y * x) % mod return y } function _getDoubleX(x: bigint, mod: bigint): bigint { // returns x_2 = (x^2 - 1)^2/(4*y^2) let denominator = _getY2(x, mod) denominator = (denominator * 4n) % mod let numerator = (x * x) % mod numerator = (numerator - 1n) % mod numerator = (numerator * numerator) % mod denominator = bigIntModInv(denominator, mod) numerator = (numerator * denominator) % mod return numerator } function _isQuadraticResidue(a: bigint): boolean { const r = bigIntModPow(a, QUAD_RES_POW, QUAD_RES_MOD) return r === 1n } 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(crypto: ICryptoProvider, size: number): Buffer { const buf = crypto.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 Buffer.from(buf) } class TlsHelloWriter implements TlsOperationHandler { buf: Buffer pos = 0 private _domain: Buffer private _grease private _scopes: number[] = [] constructor( readonly crypto: ICryptoProvider, size: number, domain: Buffer, ) { this._domain = domain this.buf = Buffer.allocUnsafe(size) this._grease = initGrease(this.crypto, 7) } string(buf: Buffer) { buf.copy(this.buf, this.pos) this.pos += buf.length } random(size: number) { this.string(Buffer.from(this.crypto.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 = this.crypto.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(Buffer.from(key)) return } } } beginScope() { this._scopes.push(this.pos) this.pos += 2 } endScope() { const begin = this._scopes.pop() if (begin === undefined) { throw new Error('endScope called without beginScope') } const end = this.pos const size = end - begin - 2 this.buf.writeUInt16BE(size, begin) } async finish(secret: Buffer): Promise { const padSize = 515 - this.pos const unixTime = ~~(Date.now() / 1000) this.beginScope() this.zero(padSize) this.endScope() const hash = Buffer.from(await this.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(crypto, 517, domainBuf) executeTlsOperations(writer) return writer.finish(secret) } /** * Fake TLS packet codec, used for some MTProxies. * * Must only be used inside {@link MtProxyTcpTransport} * @internal */ export class FakeTlsPacketCodec extends WrappedCodec implements IPacketCodec { protected _stream: Buffer = Buffer.alloc(0) private _header!: Buffer private _isFirstTls = true async tag(): Promise { this._header = Buffer.from(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]) } return Buffer.concat([header, packet]) } async encode(packet: Buffer): Promise { packet = Buffer.from(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 } }