GramJs: New authorization algorithm, new RSA keys, message queues with invokeAfter (#1258)

This commit is contained in:
Alexander Zinchuk 2021-07-15 22:51:40 +03:00
parent 3aba9fa599
commit 388143a057
18 changed files with 432 additions and 304 deletions

View File

@ -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,
};

View File

@ -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;
}

View File

@ -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!,
};
}

View File

@ -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,
};

View File

@ -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<string, { n: bigInt.BigInteger; e: number }>();
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);
}

View File

@ -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,
};

View File

@ -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);

View File

@ -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;

View File

@ -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 };
}

View File

@ -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();

View File

@ -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);

View File

@ -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;

View File

@ -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,

View File

@ -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<Args>;
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<Partial<{
nonce: int128;
}>, Api.TypeResPQ> {
nonce: int128;
};
export class ReqDHParams extends Request<Partial<{
nonce: int128;
serverNonce: int128;
@ -11707,7 +11713,7 @@ namespace Api {
};
}
export type AnyRequest = InvokeAfterMsg | InvokeAfterMsgs | InitConnection | InvokeWithLayer | InvokeWithoutUpdates | InvokeWithMessagesRange | InvokeWithTakeout | ReqPq | ReqPqMulti | ReqDHParams | SetClientDHParams | DestroyAuthKey | RpcDropAnswer | GetFutureSalts | Ping | PingDelayDisconnect | DestroySession
export type AnyRequest = InvokeAfterMsg | InvokeAfterMsgs | InitConnection | InvokeWithLayer | InvokeWithoutUpdates | InvokeWithMessagesRange | InvokeWithTakeout | ReqPq | ReqPqMulti | ReqPqMultiNew | ReqDHParams | SetClientDHParams | DestroyAuthKey | RpcDropAnswer | GetFutureSalts | Ping | PingDelayDisconnect | DestroySession
| auth.SendCode | auth.SignUp | auth.SignIn | auth.LogOut | auth.ResetAuthorizations | auth.ExportAuthorization | auth.ImportAuthorization | auth.BindTempAuthKey | auth.ImportBotAuthorization | auth.CheckPassword | auth.RequestPasswordRecovery | auth.RecoverPassword | auth.ResendCode | auth.CancelCode | auth.DropTempAuthKeys | auth.ExportLoginToken | auth.ImportLoginToken | auth.AcceptLoginToken | auth.CheckRecoveryPassword
| account.RegisterDevice | account.UnregisterDevice | account.UpdateNotifySettings | account.GetNotifySettings | account.ResetNotifySettings | account.UpdateProfile | account.UpdateStatus | account.GetWallPapers | account.ReportPeer | account.CheckUsername | account.UpdateUsername | account.GetPrivacy | account.SetPrivacy | account.DeleteAccount | account.GetAccountTTL | account.SetAccountTTL | account.SendChangePhoneCode | account.ChangePhone | account.UpdateDeviceLocked | account.GetAuthorizations | account.ResetAuthorization | account.GetPassword | account.GetPasswordSettings | account.UpdatePasswordSettings | account.SendConfirmPhoneCode | account.ConfirmPhone | account.GetTmpPassword | account.GetWebAuthorizations | account.ResetWebAuthorization | account.ResetWebAuthorizations | account.GetAllSecureValues | account.GetSecureValue | account.SaveSecureValue | account.DeleteSecureValue | account.GetAuthorizationForm | account.AcceptAuthorization | account.SendVerifyPhoneCode | account.VerifyPhone | account.SendVerifyEmailCode | account.VerifyEmail | account.InitTakeoutSession | account.FinishTakeoutSession | account.ConfirmPasswordEmail | account.ResendPasswordEmail | account.CancelPasswordEmail | account.GetContactSignUpNotification | account.SetContactSignUpNotification | account.GetNotifyExceptions | account.GetWallPaper | account.UploadWallPaper | account.SaveWallPaper | account.InstallWallPaper | account.ResetWallPapers | account.GetAutoDownloadSettings | account.SaveAutoDownloadSettings | account.UploadTheme | account.CreateTheme | account.UpdateTheme | account.SaveTheme | account.InstallTheme | account.GetTheme | account.GetThemes | account.SetContentSettings | account.GetContentSettings | account.GetMultiWallPapers | account.GetGlobalPrivacySettings | account.SetGlobalPrivacySettings | account.ReportProfilePhoto | account.ResetPassword | account.DeclinePasswordReset
| users.GetUsers | users.GetFullUser | users.SetSecureValueErrors

View File

@ -13,6 +13,7 @@ destroy_auth_key_fail#ea109b13 = DestroyAuthKeyRes;
---functions---
req_pq#60469778 nonce:int128 = ResPQ;
req_pq_multi#be7e8ef1 nonce:int128 = ResPQ;
req_pq_multi_new#51b410fd nonce:int128 = ResPQ;
req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:string q:string public_key_fingerprint:long encrypted_data:string = Server_DH_Params;
set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:string = Set_client_DH_params_answer;
destroy_auth_key#d1435160 = DestroyAuthKeyRes;

View File

@ -13,6 +13,7 @@ destroy_auth_key_fail#ea109b13 = DestroyAuthKeyRes;
---functions---
req_pq#60469778 nonce:int128 = ResPQ;
req_pq_multi#be7e8ef1 nonce:int128 = ResPQ;
req_pq_multi_new#51b410fd nonce:int128 = ResPQ;
req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:string q:string public_key_fingerprint:long encrypted_data:string = Server_DH_Params;
set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:string = Set_client_DH_params_answer;
destroy_auth_key#d1435160 = DestroyAuthKeyRes;

View File

@ -32,6 +32,7 @@ destroy_auth_key_fail#ea109b13 = DestroyAuthKeyRes;
req_pq#60469778 nonce:int128 = ResPQ;
req_pq_multi#be7e8ef1 nonce:int128 = ResPQ;
req_pq_multi_new#51b410fd nonce:int128 = ResPQ;
req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:string q:string public_key_fingerprint:long encrypted_data:string = Server_DH_Params;

View File

@ -152,8 +152,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;
@ -167,6 +167,7 @@ namespace Api {
static serializeDate(date: Date | number): Buffer;
static fromReader(reader: Reader): VirtualClass<Args>;
getBytes(): Buffer;
CONSTRUCTOR_ID: number;
SUBCLASS_OF_ID: number;
className: string;