From 388143a057a484db556c5ed0991480a7a75ee79a Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Thu, 15 Jul 2021 22:51:40 +0300 Subject: [PATCH] GramJs: New authorization algorithm, new RSA keys, message queues with `invokeAfter` (#1258) --- src/lib/gramjs/Helpers.js | 34 ++- src/lib/gramjs/client/TelegramClient.js | 9 +- src/lib/gramjs/client/downloadFile.ts | 17 +- src/lib/gramjs/crypto/RSA.js | 67 ----- src/lib/gramjs/crypto/RSA.ts | 57 ++++ src/lib/gramjs/errors/RPCErrorList.js | 8 + src/lib/gramjs/extensions/MessagePacker.js | 18 ++ src/lib/gramjs/network/Authenticator.js | 195 -------------- src/lib/gramjs/network/Authenticator.ts | 250 ++++++++++++++++++ src/lib/gramjs/network/MTProtoSender.js | 38 ++- src/lib/gramjs/network/MTProtoState.js | 7 +- src/lib/gramjs/network/RequestState.js | 14 +- src/lib/gramjs/network/index.js | 2 - src/lib/gramjs/tl/api.d.ts | 12 +- src/lib/gramjs/tl/schemaTl.js | 1 + src/lib/gramjs/tl/static/schema.reduced.tl | 1 + src/lib/gramjs/tl/static/schema.tl | 1 + src/lib/gramjs/tl/types-generator/template.js | 5 +- 18 files changed, 432 insertions(+), 304 deletions(-) delete mode 100644 src/lib/gramjs/crypto/RSA.js create mode 100644 src/lib/gramjs/crypto/RSA.ts delete mode 100644 src/lib/gramjs/network/Authenticator.js create mode 100644 src/lib/gramjs/network/Authenticator.ts diff --git a/src/lib/gramjs/Helpers.js b/src/lib/gramjs/Helpers.js index 59a71a969..0306fddb8 100644 --- a/src/lib/gramjs/Helpers.js +++ b/src/lib/gramjs/Helpers.js @@ -41,7 +41,7 @@ function toSignedLittleBuffer(big, number = 8) { /** * converts a big int to a buffer - * @param bigInt {BigInteger} + * @param bigInt {bigInt.BigInteger} * @param bytesNumber * @param little * @param signed @@ -273,6 +273,19 @@ function getRandomInt(min, max) { */ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +/** + * Helper to export two buffers of same length + * @returns {Buffer} + */ + +function bufferXor(a, b) { + const res = []; + for (let i = 0; i < a.length; i++) { + res.push(a[i] ^ b[i]); + } + return Buffer.from(res); +} + /** * Checks if the obj is an array * @param obj @@ -328,6 +341,22 @@ function crc32(buf) { return (crc ^ (-1)) >>> 0; } +/** + * Creates a deferred object + * @return {Deferred} + */ +function createDeferred() { + let resolve; + const promise = new Promise((_resolve) => { + resolve = _resolve; + }); + + return { + promise, + resolve, + }; +} + module.exports = { readBigIntFromBuffer, readBufferFromBigInt, @@ -347,5 +376,6 @@ module.exports = { // isArrayLike, toSignedLittleBuffer, convertToLittle, - + bufferXor, + createDeferred, }; diff --git a/src/lib/gramjs/client/TelegramClient.js b/src/lib/gramjs/client/TelegramClient.js index 540769375..1cab40171 100644 --- a/src/lib/gramjs/client/TelegramClient.js +++ b/src/lib/gramjs/client/TelegramClient.js @@ -592,11 +592,9 @@ class TelegramClient { this._lastRequest = new Date().getTime(); let attempt = 0; for (attempt = 0; attempt < this._requestRetries; attempt++) { + const promise = this._sender.sendWithInvokeSupport(request); try { - const promise = this._sender.send(request); - const result = await promise; - // this.session.processEntities(result) - // this._entityCache.add(result) + const result = await promise.promise; return result; } catch (e) { if (e instanceof errors.ServerError || e.message === 'RPC_CALL_FAIL' @@ -619,6 +617,9 @@ class TelegramClient { throw e; } await this._switchDC(e.newDc); + } else if (e instanceof errors.MsgWaitError) { + // we need to resend this after the old one was confirmed. + await promise.isReady(); } else { throw e; } diff --git a/src/lib/gramjs/client/downloadFile.ts b/src/lib/gramjs/client/downloadFile.ts index e538765ea..721cd2391 100644 --- a/src/lib/gramjs/client/downloadFile.ts +++ b/src/lib/gramjs/client/downloadFile.ts @@ -2,7 +2,7 @@ import { default as Api } from '../tl/api'; import TelegramClient from './TelegramClient'; import { getAppropriatedPartSize } from '../Utils'; -import { sleep } from '../Helpers'; +import { sleep, createDeferred } from '../Helpers'; export interface progressCallback { isCanceled?: boolean; @@ -49,7 +49,7 @@ class Foreman { if (this.activeWorkers > this.maxWorkers) { this.deferred = createDeferred(); - return this.deferred.promise; + return this.deferred!.promise; } return Promise.resolve(); @@ -185,16 +185,3 @@ export async function downloadFile( const totalLength = end ? (end + 1) - start : undefined; return Buffer.concat(buffers, totalLength); } - - -function createDeferred(): Deferred { - let resolve: Deferred['resolve']; - const promise = new Promise((_resolve) => { - resolve = _resolve; - }); - - return { - promise, - resolve: resolve!, - }; -} diff --git a/src/lib/gramjs/crypto/RSA.js b/src/lib/gramjs/crypto/RSA.js deleted file mode 100644 index 648c0ffde..000000000 --- a/src/lib/gramjs/crypto/RSA.js +++ /dev/null @@ -1,67 +0,0 @@ -/* eslint-disable max-len */ -const BigInt = require('big-integer'); -const { - readBigIntFromBuffer, - readBufferFromBigInt, - sha1, - generateRandomBytes, - modExp, -} = require('../Helpers'); - -const PUBLIC_KEYS = [{ - fingerprint: [40, 85, 94, 156, 117, 240, 61, 22, 65, 244, 169, 2, 33, 107, 232, 108, 2, 43, 180, 195], - n: BigInt('24403446649145068056824081744112065346446136066297307473868293895086332508101251964919587745984311372853053253457835208829824428441874946556659953519213382748319518214765985662663680818277989736779506318868003755216402538945900388706898101286548187286716959100102939636333452457308619454821845196109544157601096359148241435922125602449263164512290854366930013825808102403072317738266383237191313714482187326643144603633877219028262697593882410403273959074350849923041765639673335775605842311578109726403165298875058941765362622936097839775380070572921007586266115476975819175319995527916042178582540628652481530373407'), - e: 65537, -}, { - fingerprint: [140, 171, 9, 34, 146, 246, 166, 50, 10, 170, 229, 247, 155, 114, 28, 177, 29, 106, 153, 154], - n: BigInt('25081407810410225030931722734886059247598515157516470397242545867550116598436968553551465554653745201634977779380884774534457386795922003815072071558370597290368737862981871277312823942822144802509055492512145589734772907225259038113414940384446493111736999668652848440655603157665903721517224934142301456312994547591626081517162758808439979745328030376796953660042629868902013177751703385501412640560275067171555763725421377065095231095517201241069856888933358280729674273422117201596511978645878544308102076746465468955910659145532699238576978901011112475698963666091510778777356966351191806495199073754705289253783'), - e: 65537, -}, { - fingerprint: [243, 218, 109, 239, 16, 202, 176, 78, 167, 8, 255, 209, 120, 234, 205, 112, 111, 42, 91, 176], - n: BigInt('22347337644621997830323797217583448833849627595286505527328214795712874535417149457567295215523199212899872122674023936713124024124676488204889357563104452250187725437815819680799441376434162907889288526863223004380906766451781702435861040049293189979755757428366240570457372226323943522935844086838355728767565415115131238950994049041950699006558441163206523696546297006014416576123345545601004508537089192869558480948139679182328810531942418921113328804749485349441503927570568778905918696883174575510385552845625481490900659718413892216221539684717773483326240872061786759868040623935592404144262688161923519030977'), - e: 65537, -}, { - fingerprint: [128, 80, 214, 72, 77, 244, 98, 7, 201, 250, 37, 244, 227, 51, 96, 199, 182, 37, 224, 113], - n: BigInt('24573455207957565047870011785254215390918912369814947541785386299516827003508659346069416840622922416779652050319196701077275060353178142796963682024347858398319926119639265555410256455471016400261630917813337515247954638555325280392998950756512879748873422896798579889820248358636937659872379948616822902110696986481638776226860777480684653756042166610633513404129518040549077551227082262066602286208338952016035637334787564972991208252928951876463555456715923743181359826124083963758009484867346318483872552977652588089928761806897223231500970500186019991032176060579816348322451864584743414550721639495547636008351'), - e: 65537, -}]; - -const _serverKeys = {}; - -PUBLIC_KEYS.forEach(({ - fingerprint, - ...keyInfo -}) => { - _serverKeys[readBigIntFromBuffer(fingerprint.slice(-8), true, true)] = keyInfo; -}); - -/** - * Encrypts the given data known the fingerprint to be used - * in the way Telegram requires us to do so (sha1(data) + data + padding) - - * @param fingerprint the fingerprint of the RSA key. - * @param data the data to be encrypted. - * @returns {Buffer|*|undefined} the cipher text, or None if no key matching this fingerprint is found. - */ -async function encrypt(fingerprint, data) { - const key = _serverKeys[fingerprint]; - if (!key) { - return undefined; - } - - // len(sha1.digest) is always 20, so we're left with 255 - 20 - x padding - const rand = generateRandomBytes(235 - data.length); - - const toEncrypt = Buffer.concat([await sha1(data), data, rand]); - - // rsa module rsa.encrypt adds 11 bits for padding which we don't want - // rsa module uses rsa.transform.bytes2int(to_encrypt), easier way: - const payload = readBigIntFromBuffer(toEncrypt, false); - const encrypted = modExp(payload, BigInt(key.e), key.n); - // rsa module uses transform.int2bytes(encrypted, keylength), easier: - return readBufferFromBigInt(encrypted, 256, false); -} - -module.exports = { - encrypt, -}; diff --git a/src/lib/gramjs/crypto/RSA.ts b/src/lib/gramjs/crypto/RSA.ts new file mode 100644 index 000000000..a08836761 --- /dev/null +++ b/src/lib/gramjs/crypto/RSA.ts @@ -0,0 +1,57 @@ +import bigInt from 'big-integer'; +import { + generateRandomBytes, + modExp, + readBigIntFromBuffer, + readBufferFromBigInt, + sha1, +} from '../Helpers'; + +const PUBLIC_KEYS = [ + { + fingerprint: bigInt('-3414540481677951611'), + n: bigInt( + '2937959817066933702298617714945612856538843112005886376816255642404751219133084745514657634448776440866' + + '1701890505066208632169112269581063774293102577308490531282748465986139880977280302242772832972539403531' + + '3160108704012876427630091361567343395380424193887227773571344877461690935390938502512438971889287359033' + + '8945177273024525306296338410881284207988753897636046529094613963869149149606209957083647645485599631919' + + '2747663615955633778034897140982517446405334423701359108810182097749467210509584293428076654573384828809' + + '574217079944388301239431309115013843331317877374435868468779972014486325557807783825502498215169806323', + ), + e: 65537, + }, +]; + +export const _serverKeys = new Map(); + +PUBLIC_KEYS.forEach(({ fingerprint, ...keyInfo }) => { + _serverKeys.set(fingerprint.toString(), + keyInfo); +}); + +/** + * Encrypts the given data known the fingerprint to be used + * in the way Telegram requires us to do so (sha1(data) + data + padding) + + * @param fingerprint the fingerprint of the RSA key. + * @param data the data to be encrypted. + * @returns {Buffer|*|undefined} the cipher text, or undefined if no key matching this fingerprint is found. + */ +export async function encrypt(fingerprint: bigInt.BigInteger, data: Buffer) { + const key = _serverKeys.get(fingerprint.toString()); + if (!key) { + return undefined; + } + + // len(sha1.digest) is always 20, so we're left with 255 - 20 - x padding + const rand = generateRandomBytes(235 - data.length); + + const toEncrypt = Buffer.concat([await sha1(data), data, rand]); + + // rsa module rsa.encrypt adds 11 bits for padding which we don't want + // rsa module uses rsa.transform.bytes2int(to_encrypt), easier way: + const payload = readBigIntFromBuffer(toEncrypt, false); + const encrypted = modExp(payload, bigInt(key.e), key.n); + // rsa module uses transform.int2bytes(encrypted, keylength), easier: + return readBufferFromBigInt(encrypted, 256, false); +} diff --git a/src/lib/gramjs/errors/RPCErrorList.js b/src/lib/gramjs/errors/RPCErrorList.js index 5d70ed419..c203d7ec3 100644 --- a/src/lib/gramjs/errors/RPCErrorList.js +++ b/src/lib/gramjs/errors/RPCErrorList.js @@ -48,6 +48,12 @@ class FloodWaitError extends FloodError { this.seconds = seconds; } } +class MsgWaitError extends FloodError { + constructor(args) { + super(`Message failed to be sent.${RPCError._fmtRequest(args.request)}`); + this.message = `Message failed to be sent.${RPCError._fmtRequest(args.request)}`; + } +} class FloodTestPhoneWaitError extends FloodError { constructor(args) { @@ -92,6 +98,7 @@ const rpcErrorRe = [ [/FILE_MIGRATE_(\d+)/, FileMigrateError], [/FLOOD_TEST_PHONE_WAIT_(\d+)/, FloodTestPhoneWaitError], [/FLOOD_WAIT_(\d+)/, FloodWaitError], + [/MSG_WAIT_(.*)/, MsgWaitError], [/PHONE_MIGRATE_(\d+)/, PhoneMigrateError], [/SLOWMODE_WAIT_(\d+)/, SlowModeWaitError], [/USER_MIGRATE_(\d+)/, UserMigrateError], @@ -108,5 +115,6 @@ module.exports = { SlowModeWaitError, UserMigrateError, NetworkMigrateError, + MsgWaitError, EmailUnconfirmedError, }; diff --git a/src/lib/gramjs/extensions/MessagePacker.js b/src/lib/gramjs/extensions/MessagePacker.js index 2de2f98ef..f903b69bd 100644 --- a/src/lib/gramjs/extensions/MessagePacker.js +++ b/src/lib/gramjs/extensions/MessagePacker.js @@ -2,6 +2,11 @@ const MessageContainer = require('../tl/core/MessageContainer'); const TLMessage = require('../tl/core/TLMessage'); const BinaryWriter = require('../extensions/BinaryWriter'); +const USE_INVOKE_AFTER_WITH = [ + 'messages.SendMessage', 'messages.SendMedia', 'messages.SendMultiMedia', + 'messages.ForwardMessages', 'messages.SendInlineBotResult', +]; + class MessagePacker { constructor(state, logger) { this._state = state; @@ -18,6 +23,19 @@ class MessagePacker { } append(state) { + // we need to check if there is already a request with the same name that we should send after. + if (USE_INVOKE_AFTER_WITH.includes(state.request.className)) { + // we now need to check if there is any request in queue already. + // we loop backwards since the latest request is the most recent + for (let i = this._queue.length - 1; i >= 0; i--) { + if (USE_INVOKE_AFTER_WITH.includes(this._queue[i].request.className)) { + state.after = this._queue[i]; + break; + } + } + } + + this._queue.push(state); this.setReady(true); diff --git a/src/lib/gramjs/network/Authenticator.js b/src/lib/gramjs/network/Authenticator.js deleted file mode 100644 index 66a582461..000000000 --- a/src/lib/gramjs/network/Authenticator.js +++ /dev/null @@ -1,195 +0,0 @@ -const BigInt = require('big-integer'); -const IGE = require('../crypto/IGE'); -const AuthKey = require('../crypto/AuthKey'); -const Factorizator = require('../crypto/Factorizator'); -const RSA = require('../crypto/RSA'); -const Helpers = require('../Helpers'); -const { - constructors, - requests, -} = require('../tl'); -const BinaryReader = require('../extensions/BinaryReader'); -const { SecurityError } = require('../errors/Common'); - -/** - * Executes the authentication process with the Telegram servers. - * @param sender a connected {MTProtoPlainSender}. - * @param log - * @returns {Promise<{authKey: *, timeOffset: *}>} - */ -async function doAuthentication(sender, log) { - // Step 1 sending: PQ Request, endianness doesn't matter since it's random - let bytes = Helpers.generateRandomBytes(16); - - const nonce = Helpers.readBigIntFromBuffer(bytes, false, true); - - const resPQ = await sender.send(new requests.ReqPqMulti({ nonce })); - log.debug('Starting authKey generation step 1'); - - if (!(resPQ instanceof constructors.ResPQ)) { - throw new Error(`Step 1 answer was ${resPQ}`); - } - if (resPQ.nonce.neq(nonce)) { - throw new SecurityError('Step 1 invalid nonce from server'); - } - const pq = Helpers.readBigIntFromBuffer(resPQ.pq, false, true); - log.debug('Finished authKey generation step 1'); - log.debug('Starting authKey generation step 2'); - // Step 2 sending: DH Exchange - let { - p, - q, - } = Factorizator.factorize(pq); - - // TODO Bring back after `Factorizator` fix. - p = Helpers.getByteArray(p); - q = Helpers.getByteArray(q); - - bytes = Helpers.generateRandomBytes(32); - const newNonce = Helpers.readBigIntFromBuffer(bytes, true, true); - - const pqInnerData = new constructors.PQInnerData({ - pq: Helpers.getByteArray(pq), // unsigned - p, - q, - nonce: resPQ.nonce, - serverNonce: resPQ.serverNonce, - newNonce, - }); - - // sha_digest + data + random_bytes - let cipherText; - let targetFingerprint; - for (const fingerprint of resPQ.serverPublicKeyFingerprints) { - cipherText = await RSA.encrypt(fingerprint.toString(), pqInnerData.getBytes()); - if (cipherText !== undefined) { - targetFingerprint = fingerprint; - break; - } - } - if (cipherText === undefined) { - throw new SecurityError('Step 2 could not find a valid key for fingerprints'); - } - - const serverDhParams = await sender.send( - new requests.ReqDHParams({ - nonce: resPQ.nonce, - serverNonce: resPQ.serverNonce, - p, - q, - publicKeyFingerprint: targetFingerprint, - encryptedData: cipherText, - }), - ); - if (!(serverDhParams instanceof constructors.ServerDHParamsOk - || serverDhParams instanceof constructors.ServerDHParamsFail)) { - throw new Error(`Step 2.1 answer was ${serverDhParams}`); - } - if (serverDhParams.nonce.neq(resPQ.nonce)) { - throw new SecurityError('Step 2 invalid nonce from server'); - } - - if (serverDhParams.serverNonce.neq(resPQ.serverNonce)) { - throw new SecurityError('Step 2 invalid server nonce from server'); - } - - if (serverDhParams instanceof constructors.ServerDHParamsFail) { - const sh = await Helpers.sha1(Helpers.toSignedLittleBuffer(newNonce, 32) - .slice(4, 20)); - const nnh = Helpers.readBigIntFromBuffer(sh, true, true); - if (serverDhParams.newNonceHash.neq(nnh)) { - throw new SecurityError('Step 2 invalid DH fail nonce from server'); - } - } - if (!(serverDhParams instanceof constructors.ServerDHParamsOk)) { - throw new Error(`Step 2.2 answer was ${serverDhParams}`); - } - log.debug('Finished authKey generation step 2'); - log.debug('Starting authKey generation step 3'); - - // Step 3 sending: Complete DH Exchange - const { - key, - iv, - } = await Helpers.generateKeyDataFromNonce(resPQ.serverNonce, newNonce); - if (serverDhParams.encryptedAnswer.length % 16 !== 0) { - // See PR#453 - throw new SecurityError('Step 3 AES block size mismatch'); - } - const ige = new IGE(key, iv); - const plainTextAnswer = ige.decryptIge(serverDhParams.encryptedAnswer); - const reader = new BinaryReader(plainTextAnswer); - reader.read(20); // hash sum - const serverDhInner = reader.tgReadObject(); - if (!(serverDhInner instanceof constructors.ServerDHInnerData)) { - throw new Error(`Step 3 answer was ${serverDhInner}`); - } - - if (serverDhInner.nonce.neq(resPQ.nonce)) { - throw new SecurityError('Step 3 Invalid nonce in encrypted answer'); - } - if (serverDhInner.serverNonce.neq(resPQ.serverNonce)) { - throw new SecurityError('Step 3 Invalid server nonce in encrypted answer'); - } - const dhPrime = Helpers.readBigIntFromBuffer(serverDhInner.dhPrime, false, false); - const ga = Helpers.readBigIntFromBuffer(serverDhInner.gA, false, false); - const timeOffset = serverDhInner.serverTime - Math.floor(new Date().getTime() / 1000); - const b = Helpers.readBigIntFromBuffer(Helpers.generateRandomBytes(256), false, false); - const gb = Helpers.modExp(BigInt(serverDhInner.g), b, dhPrime); - const gab = Helpers.modExp(ga, b, dhPrime); - - // Prepare client DH Inner Data - const clientDhInner = new constructors.ClientDHInnerData({ - nonce: resPQ.nonce, - serverNonce: resPQ.serverNonce, - retryId: 0, // TODO Actual retry ID - gB: Helpers.getByteArray(gb, false), - }).getBytes(); - - const clientDdhInnerHashed = Buffer.concat([await Helpers.sha1(clientDhInner), clientDhInner]); - // Encryption - - const clientDhEncrypted = ige.encryptIge(clientDdhInnerHashed); - const dhGen = await sender.send( - new requests.SetClientDHParams({ - nonce: resPQ.nonce, - serverNonce: resPQ.serverNonce, - encryptedData: clientDhEncrypted, - }), - ); - const nonceTypes = [constructors.DhGenOk, constructors.DhGenRetry, constructors.DhGenFail]; - if (!(dhGen instanceof nonceTypes[0] || dhGen instanceof nonceTypes[1] || dhGen instanceof nonceTypes[2])) { - throw new Error(`Step 3.1 answer was ${dhGen}`); - } - const { name } = dhGen.constructor; - if (dhGen.nonce.neq(resPQ.nonce)) { - throw new SecurityError(`Step 3 invalid ${name} nonce from server`); - } - if (dhGen.serverNonce.neq(resPQ.serverNonce)) { - throw new SecurityError(`Step 3 invalid ${name} server nonce from server`); - } - const authKey = new AuthKey(); - await authKey.setKey(Helpers.getByteArray(gab)); - - const nonceNumber = 1 + nonceTypes.indexOf(dhGen.constructor); - - const newNonceHash = await authKey.calcNewNonceHash(newNonce, nonceNumber); - const dhHash = dhGen[`newNonceHash${nonceNumber}`]; - - if (dhHash.neq(newNonceHash)) { - throw new SecurityError('Step 3 invalid new nonce hash'); - } - - if (!(dhGen instanceof constructors.DhGenOk)) { - throw new Error(`Step 3.2 answer was ${dhGen}`); - } - log.debug('Finished authKey generation step 3'); - - return { - authKey, - timeOffset, - }; -} - - -module.exports = doAuthentication; diff --git a/src/lib/gramjs/network/Authenticator.ts b/src/lib/gramjs/network/Authenticator.ts new file mode 100644 index 000000000..3c4d27763 --- /dev/null +++ b/src/lib/gramjs/network/Authenticator.ts @@ -0,0 +1,250 @@ +/** + * Executes the authentication process with the Telegram servers. + * @param sender a connected {MTProtoPlainSender}. + * @param log + * @returns {Promise<{authKey: *, timeOffset: *}>} + */ +// eslint-disable-next-line import/no-named-default +import { default as Api } from '../tl/api'; +import { SecurityError } from '../errors'; +// eslint-disable-next-line import/no-named-default +import { default as MTProtoPlainSender } from './MTProtoPlainSender'; +import { _serverKeys } from '../crypto/RSA'; + +const bigInt = require('big-integer'); +const IGE = require('../crypto/IGE'); +const AuthKey = require('../crypto/AuthKey'); +const Factorizator = require('../crypto/Factorizator'); +const Helpers = require('../Helpers'); +const BinaryReader = require('../extensions/BinaryReader'); + +const RETRIES = 20; + +export async function doAuthentication(sender: MTProtoPlainSender, log: any) { + // Step 1 sending: PQ Request, endianness doesn't matter since it's random + let bytes = Helpers.generateRandomBytes(16); + + const nonce = Helpers.readBigIntFromBuffer(bytes, false, true); + const resPQ = await sender.send(new Api.ReqPqMulti({ nonce })); + log.debug('Starting authKey generation step 1'); + + if (!(resPQ instanceof Api.ResPQ)) { + throw new SecurityError(`Step 1 answer was ${resPQ}`); + } + if (resPQ.nonce.neq(nonce)) { + throw new SecurityError('Step 1 invalid nonce from server'); + } + const pq = Helpers.readBigIntFromBuffer(resPQ.pq, false, true); + log.debug('Finished authKey generation step 1'); + // Step 2 sending: DH Exchange + const { p, q } = Factorizator.factorize(pq); + + const pBuffer = Helpers.getByteArray(p); + const qBuffer = Helpers.getByteArray(q); + + bytes = Helpers.generateRandomBytes(32); + const newNonce = Helpers.readBigIntFromBuffer(bytes, true, true); + const pqInnerData = new Api.PQInnerData({ + pq: Helpers.getByteArray(pq), // unsigned + p: pBuffer, + q: qBuffer, + nonce: resPQ.nonce, + serverNonce: resPQ.serverNonce, + newNonce, + }).getBytes(); + if (pqInnerData.length > 144) { + throw new SecurityError('Step 1 invalid nonce from server'); + } + let targetFingerprint; + let targetKey; + for (const fingerprint of resPQ.serverPublicKeyFingerprints) { + targetKey = _serverKeys.get(fingerprint.toString()); + if (targetKey !== undefined) { + targetFingerprint = fingerprint; + break; + } + } + if (targetFingerprint === undefined || targetKey === undefined) { + throw new SecurityError( + 'Step 2 could not find a valid key for fingerprints', + ); + } + // Value should be padded to be made 192 exactly + const padding = Helpers.generateRandomBytes(192 - pqInnerData.length); + const dataWithPadding = Buffer.concat([pqInnerData, padding]); + const dataPadReversed = Buffer.from(dataWithPadding).reverse(); + + let encryptedData; + for (let i = 0; i < RETRIES; i++) { + const tempKey = Helpers.generateRandomBytes(32); + const shaDigestKeyWithData = await Helpers.sha256(Buffer.concat([tempKey, dataWithPadding])); + const dataWithHash = Buffer.concat([dataPadReversed, shaDigestKeyWithData]); + + const ige = new IGE(tempKey, Buffer.alloc(32)); + const aesEncrypted = ige.encryptIge(dataWithHash); + const tempKeyXor = Helpers.bufferXor(tempKey, await Helpers.sha256(aesEncrypted)); + + const keyAesEncrypted = Buffer.concat([tempKeyXor, aesEncrypted]); + const keyAesEncryptedInt = Helpers.readBigIntFromBuffer(keyAesEncrypted, false, false); + if (keyAesEncryptedInt.greaterOrEquals(targetKey.n)) { + log.debug('Aes key greater than RSA. retrying'); + continue; + } + const encryptedDataBuffer = Helpers.modExp(keyAesEncryptedInt, bigInt(targetKey.e), targetKey.n); + encryptedData = Helpers.readBufferFromBigInt(encryptedDataBuffer, 256, false, false); + + break; + } + if (encryptedData === undefined) { + throw new SecurityError( + 'Step 2 could create a secure encrypted key', + ); + } + log.debug('Step 2 : Generated a secure aes encrypted data'); + + const serverDhParams = await sender.send( + new Api.ReqDHParams({ + nonce: resPQ.nonce, + serverNonce: resPQ.serverNonce, + p: pBuffer, + q: qBuffer, + publicKeyFingerprint: targetFingerprint, + encryptedData, + }), + ); + + if ( + !( + serverDhParams instanceof Api.ServerDHParamsOk + || serverDhParams instanceof Api.ServerDHParamsFail + ) + ) { + throw new Error(`Step 2.1 answer was ${serverDhParams}`); + } + if (serverDhParams.nonce.neq(resPQ.nonce)) { + throw new SecurityError('Step 2 invalid nonce from server'); + } + + if (serverDhParams.serverNonce.neq(resPQ.serverNonce)) { + throw new SecurityError('Step 2 invalid server nonce from server'); + } + + if (serverDhParams instanceof Api.ServerDHParamsFail) { + const sh = await Helpers.sha1( + Helpers.toSignedLittleBuffer(newNonce, 32).slice(4, 20), + ); + const nnh = Helpers.readBigIntFromBuffer(sh, true, true); + if (serverDhParams.newNonceHash.neq(nnh)) { + throw new SecurityError('Step 2 invalid DH fail nonce from server'); + } + } + if (!(serverDhParams instanceof Api.ServerDHParamsOk)) { + throw new Error(`Step 2.2 answer was ${serverDhParams}`); + } + log.debug('Finished authKey generation step 2'); + log.debug('Starting authKey generation step 3'); + + // Step 3 sending: Complete DH Exchange + const { key, iv } = await Helpers.generateKeyDataFromNonce( + resPQ.serverNonce, + newNonce, + ); + if (serverDhParams.encryptedAnswer.length % 16 !== 0) { + // See PR#453 + throw new SecurityError('Step 3 AES block size mismatch'); + } + const ige = new IGE(key, iv); + const plainTextAnswer = ige.decryptIge(serverDhParams.encryptedAnswer); + const reader = new BinaryReader(plainTextAnswer); + reader.read(20); // hash sum + const serverDhInner = reader.tgReadObject(); + if (!(serverDhInner instanceof Api.ServerDHInnerData)) { + throw new Error(`Step 3 answer was ${serverDhInner}`); + } + + if (serverDhInner.nonce.neq(resPQ.nonce)) { + throw new SecurityError('Step 3 Invalid nonce in encrypted answer'); + } + if (serverDhInner.serverNonce.neq(resPQ.serverNonce)) { + throw new SecurityError( + 'Step 3 Invalid server nonce in encrypted answer', + ); + } + const dhPrime = Helpers.readBigIntFromBuffer( + serverDhInner.dhPrime, + false, + false, + ); + const ga = Helpers.readBigIntFromBuffer(serverDhInner.gA, false, false); + const timeOffset = serverDhInner.serverTime - Math.floor(new Date().getTime() / 1000); + const b = Helpers.readBigIntFromBuffer( + Helpers.generateRandomBytes(256), + false, + false, + ); + const gb = Helpers.modExp(bigInt(serverDhInner.g), b, dhPrime); + const gab = Helpers.modExp(ga, b, dhPrime); + + // Prepare client DH Inner Data + const clientDhInner = new Api.ClientDHInnerData({ + nonce: resPQ.nonce, + serverNonce: resPQ.serverNonce, + retryId: bigInt.zero, // TODO Actual retry ID + gB: Helpers.getByteArray(gb, false), + }).getBytes(); + + const clientDdhInnerHashed = Buffer.concat([ + await Helpers.sha1(clientDhInner), + clientDhInner, + ]); + // Encryption + + const clientDhEncrypted = ige.encryptIge(clientDdhInnerHashed); + const dhGen = await sender.send( + new Api.SetClientDHParams({ + nonce: resPQ.nonce, + serverNonce: resPQ.serverNonce, + encryptedData: clientDhEncrypted, + }), + ); + const nonceTypes = [Api.DhGenOk, Api.DhGenRetry, Api.DhGenFail]; + // TS being weird again. + const nonceTypesString = ['DhGenOk', 'DhGenRetry', 'DhGenFail']; + if ( + !( + dhGen instanceof nonceTypes[0] + || dhGen instanceof nonceTypes[1] + || dhGen instanceof nonceTypes[2] + ) + ) { + throw new Error(`Step 3.1 answer was ${dhGen}`); + } + const { name } = dhGen.constructor; + if (dhGen.nonce.neq(resPQ.nonce)) { + throw new SecurityError(`Step 3 invalid ${name} nonce from server`); + } + if (dhGen.serverNonce.neq(resPQ.serverNonce)) { + throw new SecurityError( + `Step 3 invalid ${name} server nonce from server`, + ); + } + const authKey = new AuthKey(); + await authKey.setKey(Helpers.getByteArray(gab)); + + const nonceNumber = 1 + nonceTypesString.indexOf(dhGen.className); + + const newNonceHash = await authKey.calcNewNonceHash(newNonce, nonceNumber); + // @ts-ignore + const dhHash = dhGen[`newNonceHash${nonceNumber}`]; + + if (dhHash.neq(newNonceHash)) { + throw new SecurityError('Step 3 invalid new nonce hash'); + } + + if (!(dhGen instanceof Api.DhGenOk)) { + throw new Error(`Step 3.2 answer was ${dhGen}`); + } + log.debug('Finished authKey generation step 3'); + + return { authKey, timeOffset }; +} diff --git a/src/lib/gramjs/network/MTProtoSender.js b/src/lib/gramjs/network/MTProtoSender.js index c073bda25..cbe168371 100644 --- a/src/lib/gramjs/network/MTProtoSender.js +++ b/src/lib/gramjs/network/MTProtoSender.js @@ -2,7 +2,7 @@ const MtProtoPlainSender = require('./MTProtoPlainSender'); const MTProtoState = require('./MTProtoState'); const Helpers = require('../Helpers'); const AuthKey = require('../crypto/AuthKey'); -const doAuthentication = require('./Authenticator'); +const { doAuthentication } = require('./Authenticator'); const RPCResult = require('../tl/core/RPCResult'); const MessageContainer = require('../tl/core/MessageContainer'); const GZIPPacked = require('../tl/core/GZIPPacked'); @@ -231,18 +231,23 @@ class MTProtoSender { if (!this._user_connected) { throw new Error('Cannot send requests while disconnected'); } - // CONTEST const state = new RequestState(request); this._send_queue.append(state); return state.promise; - /* - if (!Helpers.isArrayLike(request)) { - const state = new RequestState(request) - this._send_queue.append(state) - return state.promise - } else { - throw new Error('not supported') - } */ + } + + /** + * Same as send but returns the full state. usefull for invoke after logic + * @param request + * @return {RequestState} + */ + sendWithInvokeSupport(request) { + if (!this._user_connected) { + throw new Error('Cannot send requests while disconnected'); + } + const state = new RequestState(request, undefined, this._pending_state); + this._send_queue.append(state); + return state; } /** @@ -264,10 +269,10 @@ class MTProtoSender { this._log.debug('Generated new auth_key successfully'); await this.authKey.setKey(res.authKey); - this._state.time_offset = res.timeOffset; + this._state.timeOffset = res.timeOffset; if (this._updateCallback) { - this._updateCallback(new UpdateServerTimeOffset(this._state.time_offset)); + this._updateCallback(new UpdateServerTimeOffset(this._state.timeOffset)); } /** @@ -471,6 +476,7 @@ class MTProtoSender { _popStates(msgId) { let state = this._pending_state[msgId]; if (state) { + this._pending_state[msgId].deferred.resolve(); delete this._pending_state[msgId]; return [state]; } @@ -487,6 +493,7 @@ class MTProtoSender { const temp = []; for (const x of toPop) { temp.push(this._pending_state[x]); + this._pending_state[x].deferred.resolve(); delete this._pending_state[x]; } return temp; @@ -513,6 +520,7 @@ class MTProtoSender { const result = message.obj; const state = this._pending_state[result.reqMsgId]; if (state) { + state.deferred.resolve(); delete this._pending_state[result.reqMsgId]; } this._log.debug(`Handling RPC result for message ${result.reqMsgId}`); @@ -607,6 +615,7 @@ class MTProtoSender { this._log.debug(`Handling pong for message ${pong.msgId}`); const state = this._pending_state[pong.msgId]; + this._pending_state[pong.msgId].deferred.resolve(); delete this._pending_state[pong.msgId]; // Todo Check result @@ -743,6 +752,7 @@ class MTProtoSender { for (const msgId of ack.msgIds) { const state = this._pending_state[msgId]; if (state && state.request instanceof LogOut) { + this._pending_state[msgId].deferred.resolve(); delete this._pending_state[msgId]; state.resolve(true); } @@ -765,6 +775,7 @@ class MTProtoSender { const state = this._pending_state[message.msgId]; if (state) { + this._pending_state[message].deferred.resolve(); delete this._pending_state[message]; state.resolve(message.obj); } @@ -822,6 +833,9 @@ class MTProtoSender { this._reconnecting = false; // uncomment this if you want to resend // this._send_queue.extend(Object.values(this._pending_state)) + for (const state of this._pending_state) { + state.deferred.resolve(); + } this._pending_state = {}; if (this._autoReconnectCallback) { await this._autoReconnectCallback(); diff --git a/src/lib/gramjs/network/MTProtoState.js b/src/lib/gramjs/network/MTProtoState.js index 9f635b1cb..0f5550d24 100644 --- a/src/lib/gramjs/network/MTProtoState.js +++ b/src/lib/gramjs/network/MTProtoState.js @@ -107,7 +107,12 @@ class MTProtoState { if (!afterId) { body = await GZIPPacked.gzipIfSmaller(contentRelated, data); } else { - body = await GZIPPacked.gzipIfSmaller(contentRelated, new InvokeAfterMsg(afterId, data).getBytes()); + // Invoke query expects a query with a getBytes func + body = await GZIPPacked.gzipIfSmaller(contentRelated, new InvokeAfterMsg(afterId, { + getBytes() { + return data; + }, + }).getBytes()); } const s = Buffer.alloc(4); s.writeInt32LE(seqNo, 0); diff --git a/src/lib/gramjs/network/RequestState.js b/src/lib/gramjs/network/RequestState.js index 30bb94312..70cd79a56 100644 --- a/src/lib/gramjs/network/RequestState.js +++ b/src/lib/gramjs/network/RequestState.js @@ -1,16 +1,28 @@ +const { createDeferred } = require('../Helpers'); + class RequestState { - constructor(request, after = undefined) { + constructor(request, after = undefined, pending = {}) { this.containerId = undefined; this.msgId = undefined; this.request = request; this.data = request.getBytes(); this.after = after; this.result = undefined; + this.pending = pending; + this.deferred = createDeferred(); this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } + + isReady() { + const state = this.pending[this.after.id]; + if (!state) { + return true; + } + return state.deferred.promise; + } } module.exports = RequestState; diff --git a/src/lib/gramjs/network/index.js b/src/lib/gramjs/network/index.js index 98f833214..951340346 100644 --- a/src/lib/gramjs/network/index.js +++ b/src/lib/gramjs/network/index.js @@ -1,5 +1,4 @@ const MTProtoPlainSender = require('./MTProtoPlainSender'); -const doAuthentication = require('./Authenticator'); const MTProtoSender = require('./MTProtoSender'); class UpdateConnectionState { @@ -33,7 +32,6 @@ module.exports = { ConnectionTCPAbridged, ConnectionTCPObfuscated, MTProtoPlainSender, - doAuthentication, MTProtoSender, UpdateConnectionState, UpdateServerTimeOffset, diff --git a/src/lib/gramjs/tl/api.d.ts b/src/lib/gramjs/tl/api.d.ts index 2c4e6d018..43ee0e2a9 100644 --- a/src/lib/gramjs/tl/api.d.ts +++ b/src/lib/gramjs/tl/api.d.ts @@ -20,8 +20,8 @@ namespace Api { type Type = unknown; type Bool = boolean; type int = number; - type int128 = number; - type int256 = number; + type int128 = BigInteger; + type int256 = BigInteger; type long = BigInteger; type bytes = Buffer; @@ -35,6 +35,7 @@ namespace Api { static serializeDate(date: Date | number): Buffer; static fromReader(reader: Reader): VirtualClass; + getBytes(): Buffer; CONSTRUCTOR_ID: number; SUBCLASS_OF_ID: number; className: string; @@ -8552,6 +8553,11 @@ namespace Api { }>, Api.TypeResPQ> { nonce: int128; }; + export class ReqPqMultiNew extends Request, Api.TypeResPQ> { + nonce: int128; + }; export class ReqDHParams extends Request; + getBytes(): Buffer; CONSTRUCTOR_ID: number; SUBCLASS_OF_ID: number; className: string;